網頁端照片整理工具

字數: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);
JavaScript

images:存放使用者目前「保留」的照片檔。
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
  1. 轉陣列Array.from() 方便使用陣列方法。
  2. 數量檢查:若超過 30 張,跳出提醒並重置檔案選取欄位,不再往下執行
  3. 狀態重置:把 images 設為新檔案、清空 deletedList
  4. 重畫:呼叫 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();
}
JavaScript
  • Array.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
  1. 讀取 每個 FilearrayBuffer()
  2. 加入JSZip 實例:zip.file(name, buffer)
  3. 生成 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();
});
JavaScript

4. 小結

  • 本專案作為一款純前端、零後端依賴的「照片比較小工具」,關鍵設計與技術亮點如下:
  • 狀態驅動、重渲染架構
    • 以兩組 JavaScript 陣列(imagesdeletedList)作為唯一的狀態來源,所有使用者互動(上傳、刪除、還原)都透過更新陣列後呼叫同一組渲染函式(renderGallery()renderResults()),自動重建 DOM。
    • 避免了手動追蹤 DOM 狀態的複雜度,並且將程式邏輯與畫面顯示分離,易於擴充新功能(如分頁、篩選、分群、標籤系統等)。
    • 無需引入大型框架(React、Vue 等),僅憑原生 DOM API 就能實現接近 MVVM 的開發體驗,減少打包體積、提升載入速度。
  • 響應式 & 動態字體的 CSS 實現
    • CSS Grid:採用 auto-fit + minmax() 讓縮圖自動調整欄數與寬度,不須為不同裝置寫死欄位數,手機、平板、桌面皆能自適應。
    • **clamp()**:利用 clamp(min, preferred, max) 讓標題、按鈕與檔名字體大小根據視窗寬度動態縮放,兼具可讀性與空間利用率。
    • 相對單位rem%vw)結合 Grid 與 Flex,排版彈性高、易於維護;同時在 @media 斷點中微調間距與排版,確保各種螢幕下都有良好體驗。
  • 完全客戶端離線打包
    • 引入 JSZip,僅在瀏覽器端讀取 File 物件的 arrayBuffer(),再透過 .file().generateAsync() 生成 ZIP Blob,最後用隱藏 <a> 標籤觸發下載。
    • 這種做法免去了伺服器端的檔案上傳與處理,不僅降低後端成本,也保護使用者隱私(照片不離開本機),並且能在無網路或內網環境下正常運行。
  • 輕量且易於部屬
    • 純靜態三檔案結構(index.htmlstyle.cssmain.js),可直接部署至任何支援靜態文件的服務(GitHub Pages、Vercel、WordPress 主機…)。
    • 推薦使用 <iframe> 隔離部屬,避免與現有 WordPress 主題 CSS/JS 衝突;也可選擇將檔案放入 wp-content/uploads/,再以絕對路徑引用。
    • 無需打包工具或額外建置流程,上手門檻低,方便快速驗證與迭代。
  • 未來擴充方向
    • 自動分群:可利用前端圖像 Hash(pHash、aHash)或 TensorFlow.js 模型,輔助自動將相似照片分組;
    • 自訂標籤與打分:在每張縮圖下方新增星等打分或文字標籤功能,幫助更細緻的篩選;
    • 行動端優化:加入手勢滑動比較模式、觸控友善的拖放選圖;
    • 外部存儲整合:連結到 Google Drive、Dropbox API,讓保留檔案能一鍵儲存到雲端。
  • 透過上述設計,本專案在不依賴後端、維持高度可維護性的前提下,成功實現了直觀、高效且跨裝置一致的照片比較與打包下載功能。

好抱歉 AI 味道超重…

WaynSpace 部落格 Logo,黑白風格,包含鍵盤、相機與羽毛筆

喜歡攝影、程式、生活隨筆?
訂閱 WaynSpace,新文章第一時間通知你📬
✨ 精選內容,無垃圾信,隨時可取消

We don’t spam! Read our privacy policy for more info.

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

返回頂端