字數:26279 字 | 預估閱讀時間:132 分鐘
3. main.js
功能:管理應用狀態、渲染介面、綁定事件,實現上傳、刪除、還原及 ZIP 下載。
3.1 檔案選取與初始設定
JavaScript
// 取得 DOM 元素
const fileInput = document.getElementById('fileInput');
const gallery = document.getElementById('gallery');
const resultSection= document.getElementById('resultSection');
const zipBtn = document.getElementById('zipBtn');
// 狀態陣列
let images = []; // 保留的 File 物件
let deletedList = []; // 已刪除但可還原的 File 物件
// 綁定事件
fileInput.addEventListener('change', onFilesSelected);
zipBtn.addEventListener('click', onDownloadZip);
JavaScriptimages
:存放使用者目前「保留」的照片檔。deletedList
:存放使用者點掉「✕」標記為刪除、且可還原的照片檔。change
事件:使用者每次重新選檔都會觸發 onFilesSelected()
,檢查檔案數量、清空舊畫面、載入新檔案。click
事件:點擊「下載 ZIP」後執行 onDownloadZip()
。
3.2 檔案選取處理:onFilesSelected()
JavaScript
function onFilesSelected() {
const selected = Array.from(fileInput.files);
// 限制上限 30 張
if (selected.length > 30) {
alert('照片數量過多,請重新選擇上傳');
fileInput.value = '';
return;
}
// 重置狀態
images = selected;
deletedList = [];
renderGallery();
renderResults();
}
JavaScript- 轉陣列:
Array.from()
方便使用陣列方法。 - 數量檢查:若超過 30 張,跳出提醒並重置檔案選取欄位,不再往下執行。
- 狀態重置:把
images
設為新檔案、清空deletedList
。 - 重畫:呼叫
renderGallery()
和renderResults()
,同步更新 UI。
3.3 縮圖畫面:renderGallery()
JavaScript
function renderGallery() {
gallery.innerHTML = '';
images.forEach((file, idx) => {
// 外層容器
const thumb = document.createElement('div');
thumb.className = 'thumb';
// 圖片
const img = document.createElement('img');
img.src = URL.createObjectURL(file);
img.dataset.idx = idx;
img.addEventListener('click', () => img.classList.toggle('selected'));
// 刪除按鈕
const del = document.createElement('div');
del.className = 'delete-overlay';
del.textContent = '✕';
del.addEventListener('click', () => deleteImage(idx));
// 檔名
const name = document.createElement('div');
name.className = 'filename';
name.textContent = file.name;
// 組合
thumb.append(img, del, name);
gallery.appendChild(thumb);
});
}
JavaScript- 清空舊的
<div id="gallery">
。 <div.thumb>
作為每張縮圖的定位容器:<img>
:用URL.createObjectURL
建立快取 URL,並綁定點選效果(添加.selected
)。<div.delete-overlay>
:浮在右上,點擊時呼deleteImage(idx)
。<div.filename>
:顯示檔名。
- 最後一併 append 至畫面。
3.4 刪除並可還原:deleteImage(idx)
JavaScript
function deleteImage(idx) {
// 從 images 中移除並推入 deletedList
const [removed] = images.splice(idx, 1);
deletedList.push(removed);
// 重畫縮圖與結果
renderGallery();
renderResults();
}
JavaScriptArray.splice(idx, 1)
回傳被移除元素陣列,用解構直接取出。- 同步更新
images
&deletedList
,再呼兩次 render。
3.5 結果清單渲染:renderResults()
JavaScript
function renderResults() {
resultSection.innerHTML = ''; // 清空舊結果
if (images.length === 0 && deletedList.length === 0) {
resultSection.style.display = 'none';
return;
}
resultSection.style.display = 'block';
// 建立標題
const title = document.createElement('h3');
title.textContent = '比較結果';
resultSection.appendChild(title);
// 建立兩欄容器
const container = document.createElement('div');
container.className = 'result-container';
// 保留欄
const keepCol = document.createElement('div');
keepCol.className = 'result-column';
keepCol.innerHTML = '<h4>保留</h4>';
const keepList = document.createElement('ul');
images.forEach(f => {
const li = document.createElement('li');
li.textContent = f.name;
keepList.appendChild(li);
});
keepCol.appendChild(keepList);
// 刪除欄(可還原)
const delCol = document.createElement('div');
delCol.className = 'result-column';
delCol.innerHTML = '<h4>刪除</h4>';
const delListEl = document.createElement('ul');
deletedList.forEach((f, i) => {
const li = document.createElement('li');
li.textContent = f.name;
li.className = 'restoreable';
li.addEventListener('click', () => restoreImage(i));
delListEl.appendChild(li);
});
delCol.appendChild(delListEl);
container.append(keepCol, delCol);
resultSection.appendChild(container);
// 下載按鈕
const actions = document.createElement('div');
actions.className = 'result-actions';
actions.appendChild(zipBtn);
resultSection.appendChild(actions);
}
JavaScript- 若沒有任何檔案,隱藏結果區。
- 用動態建立的
<ul>
來呈現保留與刪除清單。 - 刪除清單每項加上
.restoreable
,點擊呼restoreImage(i)
。
3.6 還原功能:restoreImage(idx)
JavaScript
function restoreImage(idx) {
const [restored] = deletedList.splice(idx, 1);
images.push(restored);
renderGallery();
renderResults();
}
JavaScript- 與
deleteImage
方向相反:從deletedList
拿回images
,再渲染。
3.7 ZIP 下載:onDownloadZip()
JavaScript
async function onDownloadZip() {
const zip = new JSZip();
for (const file of images) {
const buffer = await file.arrayBuffer();
zip.file(file.name, buffer);
}
const blob = await zip.generateAsync({ type: 'blob' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'kept_photos.zip';
a.click();
}
JavaScript- 讀取 每個
File
的arrayBuffer()
。 - 加入 到
JSZip
實例:zip.file(name, buffer)
。 - 生成 Blob → 用隱藏
<a>
觸發下載。
3.8 完整 main.js
JavaScript
// 1. 參考元素與初始化狀態
const fileInput = document.getElementById('fileInput');
const gallery = document.getElementById('gallery');
const resultSection = document.getElementById('resultSection');
const keptListEl = document.getElementById('keptList');
const deletedListEl = document.getElementById('deletedList');
const zipBtn = document.getElementById('zipBtn');
let images = []; // 保留照片陣列
let deletedList = []; // 已刪除(可還原)陣列
// 2. 上傳事件:驗證、初始化、渲染
fileInput.addEventListener('change', () => {
const selected = Array.from(fileInput.files);
if (selected.length > 30) {
alert('照片數量過多,請重新選擇上傳');
fileInput.value = '';
gallery.innerHTML = '';
resultSection.classList.add('hidden');
return;
}
images = selected;
deletedList = [];
renderGallery();
renderResults();
});
// 3. 渲染縮圖區
function renderGallery() {
gallery.innerHTML = '';
images.forEach((file, idx) => {
const thumb = document.createElement('div');
thumb.className = 'thumb';
const img = document.createElement('img');
img.src = URL.createObjectURL(file);
thumb.appendChild(img);
const overlay = document.createElement('div');
overlay.className = 'delete-overlay';
overlay.textContent = '✕';
overlay.addEventListener('click', e => {
e.stopPropagation();
deleteImage(idx);
});
thumb.appendChild(overlay);
const label = document.createElement('div');
label.className = 'filename';
label.textContent = file.name;
thumb.appendChild(label);
gallery.appendChild(thumb);
});
}
// 4. 刪除並更新畫面
function deleteImage(idx) {
deletedList.push(images[idx]);
images.splice(idx, 1);
renderGallery();
renderResults();
}
// 5. 渲染結果清單 (保留 & 已刪除)
function renderResults() {
keptListEl.innerHTML = '';
deletedListEl.innerHTML = '';
images.forEach(file => {
const li = document.createElement('li');
li.textContent = file.name;
keptListEl.appendChild(li);
});
deletedList.forEach((file, idx) => {
const li = document.createElement('li');
li.textContent = file.name;
li.className = 'restoreable';
li.addEventListener('click', () => {
const restored = deletedList.splice(idx, 1)[0];
images.push(restored);
renderGallery();
renderResults();
});
deletedListEl.appendChild(li);
});
resultSection.classList.remove('hidden');
}
// 6. 打包 ZIP 並下載
zipBtn.addEventListener('click', async () => {
const zip = new JSZip();
for (const file of images) {
const buffer = await file.arrayBuffer();
zip.file(file.name, buffer);
}
const content = await zip.generateAsync({ type: 'blob' });
const a = document.createElement('a');
a.href = URL.createObjectURL(content);
a.download = 'kept_photos.zip';
a.click();
});
JavaScript4. 小結
- 本專案作為一款純前端、零後端依賴的「照片比較小工具」,關鍵設計與技術亮點如下:
- 狀態驅動、重渲染架構
- 以兩組 JavaScript 陣列(
images
與deletedList
)作為唯一的狀態來源,所有使用者互動(上傳、刪除、還原)都透過更新陣列後呼叫同一組渲染函式(renderGallery()
、renderResults()
),自動重建 DOM。 - 避免了手動追蹤 DOM 狀態的複雜度,並且將程式邏輯與畫面顯示分離,易於擴充新功能(如分頁、篩選、分群、標籤系統等)。
- 無需引入大型框架(React、Vue 等),僅憑原生 DOM API 就能實現接近 MVVM 的開發體驗,減少打包體積、提升載入速度。
- 以兩組 JavaScript 陣列(
- 響應式 & 動態字體的 CSS 實現
- CSS Grid:採用
auto-fit
+minmax()
讓縮圖自動調整欄數與寬度,不須為不同裝置寫死欄位數,手機、平板、桌面皆能自適應。 - **clamp()**:利用
clamp(min, preferred, max)
讓標題、按鈕與檔名字體大小根據視窗寬度動態縮放,兼具可讀性與空間利用率。 - 相對單位(
rem
、%
、vw
)結合 Grid 與 Flex,排版彈性高、易於維護;同時在@media
斷點中微調間距與排版,確保各種螢幕下都有良好體驗。
- CSS Grid:採用
- 完全客戶端離線打包
- 引入 JSZip,僅在瀏覽器端讀取
File
物件的arrayBuffer()
,再透過.file()
與.generateAsync()
生成 ZIP Blob,最後用隱藏<a>
標籤觸發下載。 - 這種做法免去了伺服器端的檔案上傳與處理,不僅降低後端成本,也保護使用者隱私(照片不離開本機),並且能在無網路或內網環境下正常運行。
- 引入 JSZip,僅在瀏覽器端讀取
- 輕量且易於部屬
- 純靜態三檔案結構(
index.html
、style.css
、main.js
),可直接部署至任何支援靜態文件的服務(GitHub Pages、Vercel、WordPress 主機…)。 - 推薦使用
<iframe>
隔離部屬,避免與現有 WordPress 主題 CSS/JS 衝突;也可選擇將檔案放入wp-content/uploads/
,再以絕對路徑引用。 - 無需打包工具或額外建置流程,上手門檻低,方便快速驗證與迭代。
- 純靜態三檔案結構(
- 未來擴充方向
- 自動分群:可利用前端圖像 Hash(pHash、aHash)或 TensorFlow.js 模型,輔助自動將相似照片分組;
- 自訂標籤與打分:在每張縮圖下方新增星等打分或文字標籤功能,幫助更細緻的篩選;
- 行動端優化:加入手勢滑動比較模式、觸控友善的拖放選圖;
- 外部存儲整合:連結到 Google Drive、Dropbox API,讓保留檔案能一鍵儲存到雲端。
- 透過上述設計,本專案在不依賴後端、維持高度可維護性的前提下,成功實現了直觀、高效且跨裝置一致的照片比較與打包下載功能。
好抱歉 AI 味道超重…
頁次: 1 2