<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">📸 相册生成器 · 图片批量分享</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Roboto, system-ui, -apple-system, sans-serif;
background: linear-gradient(145deg, #f6f9fc 0%, #e9f0f5 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 1.5rem 1rem;
}
.app-card {
max-width: 1100px;
width: 100%;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 48px;
padding: 2rem 1.8rem 2.5rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.6);
transition: all 0.2s ease;
}
h1 {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
font-size: 2.2rem;
letter-spacing: -0.02em;
color: #1e293b;
margin-bottom: 0.2rem;
}
.subhead {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
margin-bottom: 1.2rem;
border-bottom: 2px solid rgba(148, 163, 184, 0.3);
padding-bottom: 0.8rem;
}
.subhead p {
color: #475569;
font-weight: 450;
font-size: 1rem;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 0.8rem 1rem;
align-items: center;
margin: 1rem 0 1.8rem 0;
}
.btn {
background: white;
border: none;
padding: 0.65rem 1.6rem;
border-radius: 60px;
font-weight: 500;
font-size: 0.95rem;
display: inline-flex;
align-items: center;
gap: 0.4rem;
box-shadow: 0 2px 6px rgba(0, 20, 30, 0.08);
transition: 0.2s;
cursor: pointer;
color: #0f172a;
border: 1px solid rgba(148, 163, 184, 0.25);
background: rgba(255,255,255,0.75);
backdrop-filter: blur(4px);
}
.btn-primary {
background: #1e293b;
border: 1px solid #1e293b;
color: white;
box-shadow: 0 6px 14px rgba(30, 41, 59, 0.25);
}
.btn-primary:hover {
background: #0f172a;
transform: scale(0.97);
}
.btn-outline {
background: transparent;
border: 1px solid #94a3b8;
}
.btn:hover:not(:disabled) {
background: #f1f5f9;
transform: translateY(-1px);
}
.btn-primary:hover:not(:disabled) {
background: #0f172a;
}
.btn:disabled {
opacity: 0.5;
cursor: default;
transform: none;
}
.badge {
background: #dbeafe;
color: #1e40af;
padding: 0.2rem 0.9rem;
border-radius: 40px;
font-size: 0.85rem;
font-weight: 500;
margin-left: 0.3rem;
}
.upload-area {
background: rgba(255, 255, 255, 0.5);
border: 2px dashed #94a3b8;
border-radius: 30px;
padding: 1.8rem 1.5rem;
text-align: center;
margin: 0.5rem 0 1.8rem 0;
transition: 0.2s;
cursor: pointer;
}
.upload-area:hover {
background: rgba(255, 255, 255, 0.8);
border-color: #3b82f6;
}
.upload-area svg {
width: 42px;
height: 42px;
color: #64748b;
margin-bottom: 0.2rem;
}
.upload-area p {
color: #334155;
font-weight: 450;
}
.upload-area input[type="file"] {
display: none;
}
.gallery-container {
background: rgba(255, 255, 255, 0.3);
border-radius: 32px;
padding: 1rem 1rem 0.2rem 1rem;
backdrop-filter: blur(2px);
margin: 1rem 0 1.2rem;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 0.8rem;
margin-bottom: 0.5rem;
}
.grid-item {
position: relative;
background: white;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 4px 8px rgba(0,0,0,0.04);
aspect-ratio: 1 / 1;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(255, 255, 255, 0.6);
transition: 0.1s ease;
}
.grid-item img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
background: #f1f5f9;
}
.grid-item .remove-btn {
position: absolute;
top: 6px;
right: 6px;
background: rgba(0,0,0,0.5);
backdrop-filter: blur(4px);
color: white;
border: none;
border-radius: 40px;
width: 26px;
height: 26px;
font-size: 16px;
line-height: 1;
cursor: pointer;
transition: 0.15s;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.7;
}
.grid-item .remove-btn:hover {
opacity: 1;
background: rgba(180, 35, 35, 0.85);
transform: scale(1.05);
}
.grid-empty {
grid-column: 1 / -1;
text-align: center;
padding: 2.5rem 1rem;
color: #64748b;
background: rgba(255,255,255,0.2);
border-radius: 40px;
font-size: 1rem;
}
.action-bar {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 1rem 0.8rem;
margin-top: 1.2rem;
}
.share-card {
background: rgba(255, 255, 255, 0.4);
backdrop-filter: blur(6px);
padding: 1rem 1.5rem;
border-radius: 60px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.8rem 1.2rem;
box-shadow: 0 2px 12px rgba(0,0,0,0.02);
border: 1px solid rgba(255,255,255,0.7);
}
.share-card span {
color: #1e293b;
font-weight: 500;
}
.share-link-box {
background: white;
border-radius: 60px;
padding: 0.4rem 0.4rem 0.4rem 1.5rem;
display: flex;
align-items: center;
flex: 1;
min-width: 180px;
border: 1px solid #d1d9e8;
}
.share-link-box input {
border: none;
background: transparent;
padding: 0.4rem 0;
flex: 1;
font-size: 0.9rem;
color: #0f172a;
outline: none;
width: 100px;
}
.share-link-box button {
background: #1e293b;
border: none;
color: white;
padding: 0.4rem 1.2rem;
border-radius: 40px;
font-weight: 500;
font-size: 0.85rem;
cursor: pointer;
transition: 0.15s;
white-space: nowrap;
}
.share-link-box button:hover {
background: #0f172a;
}
.footer-note {
margin-top: 1.5rem;
font-size: 0.8rem;
color: #64748b;
text-align: center;
}
@media (max-width: 650px) {
.app-card { padding: 1.2rem; }
h1 { font-size: 1.8rem; }
.grid { grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); }
.share-card { flex-direction: column; align-items: stretch; }
.share-link-box { min-width: unset; }
}
</style>
</head>
<body>
<div class="app-card">
<div class="subhead">
<h1>🖼️ 相册生成器</h1>
<p><span class="badge" id="imageCountDisplay">0 张</span> 一键分享</p>
</div>
<!-- 上传区域 -->
<div class="upload-area" id="uploadArea">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
</svg>
<p>拖拽或点击上传图片 <span style="opacity:0.6; margin-left: 6px;">(JPEG, PNG, WEBP)</span></p>
<input type="file" id="fileInput" multiple accept="image/png, image/jpeg, image/webp">
</div>
<!-- 工具栏 -->
<div class="toolbar">
<button class="btn" id="clearAllBtn" disabled>🗑️ 清空相册</button>
<button class="btn btn-outline" id="sortByNameBtn">📛 按名称排序</button>
<button class="btn btn-outline" id="reverseOrderBtn">🔄 反转顺序</button>
</div>
<!-- 图片网格 -->
<div class="gallery-container">
<div class="grid" id="imageGrid">
<div class="grid-empty">✨ 添加图片,构建你的相册</div>
</div>
</div>
<!-- 分享区域 -->
<div class="action-bar">
<div class="share-card">
<span>🔗 分享相册</span>
<div class="share-link-box">
<input type="text" id="shareLinkInput" placeholder="生成分享链接" readonly value="">
<button id="copyShareBtn" disabled>复制</button>
</div>
<button class="btn btn-primary" id="generateShareBtn" disabled>✨ 生成分享</button>
</div>
</div>
<div class="footer-note">
⚡ 图片仅保存在本地浏览器,生成相册链接可分享 (Base64 编码)
</div>
</div>
<script>
(function() {
// ----- 核心状态 -----
let images = []; // 存储 { id, name, dataUrl, sortKey }
let shareLinkGenerated = '';
const grid = document.getElementById('imageGrid');
const fileInput = document.getElementById('fileInput');
const uploadArea = document.getElementById('uploadArea');
const clearAllBtn = document.getElementById('clearAllBtn');
const sortByNameBtn = document.getElementById('sortByNameBtn');
const reverseOrderBtn = document.getElementById('reverseOrderBtn');
const generateShareBtn = document.getElementById('generateShareBtn');
const copyShareBtn = document.getElementById('copyShareBtn');
const shareLinkInput = document.getElementById('shareLinkInput');
const imageCountDisplay = document.getElementById('imageCountDisplay');
// ----- 辅助函数 -----
function updateUI() {
// 更新计数
const count = images.length;
imageCountDisplay.textContent = count + ' 张';
clearAllBtn.disabled = count === 0;
generateShareBtn.disabled = count === 0;
// 如果图片为0,清空分享链接
if (count === 0) {
shareLinkInput.value = '';
shareLinkGenerated = '';
copyShareBtn.disabled = true;
} else {
// 如果已有分享链接,但图片变化,置为过期 (但保留显示, 用户可重新生成)
// 每次图片变动, 令分享链接失效,但不清除显示 (重新生成会覆盖)
// 但更合理: 图片变动时清空分享链接显示
shareLinkInput.value = '';
shareLinkGenerated = '';
copyShareBtn.disabled = true;
}
renderGrid();
}
// 渲染网格
function renderGrid() {
if (images.length === 0) {
grid.innerHTML = `<div class="grid-empty">🖼️ 暂无图片,点击上方添加</div>`;
return;
}
let html = '';
images.forEach((img, index) => {
html += `
<div class="grid-item" data-index="${index}">
<img src="${img.dataUrl}" alt="${img.name || '图片'}">
<button class="remove-btn" data-index="${index}" title="移除图片">✕</button>
</div>
`;
});
grid.innerHTML = html;
// 绑定移除事件 (事件委托也可,但直接绑定更清晰)
document.querySelectorAll('.grid-item .remove-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.stopPropagation();
const idx = parseInt(this.dataset.index, 10);
if (!isNaN(idx) && idx >= 0 && idx < images.length) {
images.splice(idx, 1);
updateUI();
}
});
});
}
// 添加图片 (FileList)
function addImagesFromFileList(fileList) {
if (!fileList || fileList.length === 0) return;
const validTypes = ['image/jpeg', 'image/png', 'image/webp'];
const files = Array.from(fileList).filter(f => validTypes.includes(f.type));
if (files.length === 0) {
alert('请选择 JPEG / PNG / WEBP 格式的图片');
return;
}
let loaded = 0;
files.forEach(file => {
const reader = new FileReader();
reader.onload = function(e) {
const dataUrl = e.target.result;
// 生成简短唯一标识
const id = Date.now() + '-' + Math.random().toString(36).substr(2, 6) + '-' + loaded;
const name = file.name || `image_${Date.now()}`;
images.push({
id,
name,
dataUrl,
sortKey: name.toLowerCase()
});
loaded++;
// 最后一个文件加载完成再更新UI (批量更新)
if (loaded === files.length) {
updateUI();
}
};
reader.onerror = function() {
console.warn('读取失败:', file.name);
loaded++;
if (loaded === files.length) {
updateUI();
}
};
reader.readAsDataURL(file);
});
// 若文件都无效, loaded不会增加,但触发更新由上面逻辑完成?files长度可能为0, 但之前有return
// 如果没有有效文件,loaded不会增加, 但已经在files空时return,没问题。
}
// ----- 排序功能 -----
function sortByName() {
images.sort((a, b) => {
const nameA = a.sortKey || '';
const nameB = b.sortKey || '';
return nameA.localeCompare(nameB);
});
updateUI();
}
function reverseOrder() {
images.reverse();
updateUI();
}
// ----- 生成分享 (将图片数据编码为 JSON + Base64) -----
function generateShare() {
if (images.length === 0) {
shareLinkInput.value = '';
shareLinkGenerated = '';
copyShareBtn.disabled = true;
return;
}
// 提取必要数据 (不存储大base64? 但dataUrl本身就是base64, 需要保留)
const shareData = images.map(img => ({
name: img.name,
data: img.dataUrl
}));
try {
const jsonStr = JSON.stringify(shareData);
const encoded = btoa(encodeURIComponent(jsonStr)); // 安全base64
const shareUrl = `album://${encoded}`; // 自定义协议,但这里仅仅作为分享文本
shareLinkGenerated = shareUrl;
shareLinkInput.value = shareUrl;
copyShareBtn.disabled = false;
} catch (e) {
alert('生成分享链接失败,请减少图片数量或尺寸');
console.error(e);
}
}
// 复制到剪贴板
function copyShareLink() {
if (!shareLinkGenerated || shareLinkInput.value === '') {
copyShareBtn.disabled = true;
return;
}
// 使用现代 clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(shareLinkInput.value).then(() => {
alert('✅ 相册链接已复制!');
}).catch(() => {
fallbackCopy();
});
} else {
fallbackCopy();
}
}
function fallbackCopy() {
shareLinkInput.select();
try {
const success = document.execCommand('copy');
if (success) {
alert('✅ 相册链接已复制!');
} else {
alert('⚠️ 复制失败,请手动复制');
}
} catch (e) {
alert('⚠️ 复制失败,请手动复制');
}
}
// ----- 事件绑定 -----
// 1. 上传区域点击
uploadArea.addEventListener('click', function(e) {
if (e.target.tagName !== 'INPUT') {
fileInput.click();
}
});
fileInput.addEventListener('change', function(e) {
if (this.files && this.files.length > 0) {
addImagesFromFileList(this.files);
}
this.value = ''; // 允许重复选择相同文件
});
// 2. 拖拽上传 (整个区域)
uploadArea.addEventListener('dragover', function(e) {
e.preventDefault();
e.stopPropagation();
this.style.borderColor = '#3b82f6';
this.style.background = 'rgba(59, 130, 246, 0.08)';
});
uploadArea.addEventListener('dragleave', function(e) {
e.preventDefault();
e.stopPropagation();
this.style.borderColor = '#94a3b8';
this.style.background = 'rgba(255,255,255,0.5)';
});
uploadArea.addEventListener('drop', function(e) {
e.preventDefault();
e.stopPropagation();
this.style.borderColor = '#94a3b8';
this.style.background = 'rgba(255,255,255,0.5)';
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
addImagesFromFileList(e.dataTransfer.files);
}
});
// 3. 清空
clearAllBtn.addEventListener('click', function() {
if (images.length === 0) return;
if (confirm('确定清空所有图片吗?')) {
images = [];
updateUI();
}
});
// 4. 排序
sortByNameBtn.addEventListener('click', sortByName);
reverseOrderBtn.addEventListener('click', reverseOrder);
// 5. 生成分享
generateShareBtn.addEventListener('click', generateShare);
// 6. 复制分享
copyShareBtn.addEventListener('click', copyShareLink);
// 7. 点击分享输入框全选
shareLinkInput.addEventListener('click', function() {
this.select();
});
// 初始化UI
updateUI();
// 额外:演示预置图片 (可选) —— 为了方便测试,添加几张示例 (注释掉,用户自行上传)
// 如果您希望开箱即用示例,可解除注释 (但为保持整洁,不添加)
// 但是我们可以提示用户
console.log('📸 相册生成器已启动,上传图片即可生成分享链接');
})();
</script>
</body>
</html>
相册制作与分享流程
您可以通过上传、管理和分享三个核心步骤,快速创建一个可分享的图片集合。

- 图片管理:您可以通过点击或拖拽的方式上传 JPEG、PNG 或 WEBP 格式的图片,上传后,图片会以网格形式清晰展示,您还可以对单张图片进行移除,或使用“清空相册”、“按名称排序”、“反转顺序”等工具进行整体管理。
- 分享链接生成:在相册内容确定后,点击“✨ 生成分享”按钮,工具会将您相册中的所有图片数据进行编码,并生成一个专属的分享链接,这个链接包含了所有图片信息。
- 链接复制与分享:生成的链接会显示在输入框中,您只需点击“复制”按钮即可将链接分享给他人,此链接基于图片数据生成,图片数量或尺寸较大时可能影响生成效率。
优化建议: 您可以根据需要调整 grid 的 grid-template-columns 属性值(第103行),以更改相册网格的列宽和布局密度。
标签: 相册生成
版权声明:除非特别标注,否则均为本站原创文章,转载时请以链接形式注明文章出处。