0. 作品介紹
2. 資料表規則
請照下面規則建立分頁與欄位。
第1分頁: config
A 欄: 設定項目
B 欄: 內容
📄 解析結果(自動表格)
分頁:config
| A 欄 | B 欄 |
|---|---|
| 設定項目 | 內容 |
3. 開啟 Apps Script
從試算表開 Apps Script,才能直接用 SpreadsheetApp 讀寫資料。
- 試算表上方選單:擴充功能 → Apps Script
- 刪掉預設的程式碼(或全部選取覆蓋)
-
保留/建立檔案:
- Code.gs
- index.html
4. 貼上後端(Code.gs)
把後端程式貼到 Apps Script 的 Code.gs。
/**
* 東門國小校園活動照片系統 - 後端 V2.1
*/
function doGet() {
return HtmlService.createTemplateFromFile('index')
.evaluate()
.setTitle('東門國小校園活動照片 SerialRename 系統')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
// 取得試算表中的設定資料 (系統標題等)
function getAppData() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let sheet = ss.getSheetByName('Config');
// 如果找不到 Config 工作表,提供預設值
if (!sheet) {
return { title: "東門國小校園活動照片系統", welcome: "歡迎使用" };
}
const data = sheet.getDataRange().getValues();
return {
title: data[1] ? data[1][1] : "東門國小校園活動照片系統",
welcome: data[2] ? data[2][1] : "歡迎使用"
};
}
// 取得根目錄資料夾
function getRootFolders() {
const root = DriveApp.getRootFolder();
return getChildFolders(root.getId());
}
// 取得指定資料夾下的子資料夾 (樹狀結構用)
function getChildFolders(parentId) {
try {
const parent = DriveApp.getFolderById(parentId);
const folders = parent.getFolders();
let list = [];
while (folders.hasNext()) {
let f = folders.next();
list.push({ name: f.getName(), id: f.getId() });
}
return list.sort((a, b) => a.name.localeCompare(b.name));
} catch (e) {
return [];
}
}
// 取得圖片並轉換為 Base64 縮圖
function getFilesInFolder(folderId) {
const folder = DriveApp.getFolderById(folderId);
const files = folder.getFiles();
let fileList = [];
const allowedTypes = [MimeType.JPEG, MimeType.PNG, MimeType.GIF, MimeType.BMP];
while (files.hasNext()) {
let file = files.next();
if (allowedTypes.indexOf(file.getMimeType()) !== -1) {
// 優先使用縮圖,若無則抓取內容
let thumb = file.getThumbnail();
let bytes = thumb ? thumb.getBytes() : file.getBlob().getBytes();
let b64 = Utilities.base64Encode(bytes);
fileList.push({
name: file.getName(),
id: file.getId(),
base64: b64,
mimeType: file.getMimeType()
});
}
}
return fileList.sort((a, b) => a.name.localeCompare(b.name));
}
// 執行重新命名
function renameFiles(folderId, fileIds, prefix, digits) {
fileIds.forEach((id, index) => {
let file = DriveApp.getFileById(id);
let nameParts = file.getName().split('.');
let extension = nameParts.length > 1 ? nameParts.pop() : '';
let sequence = (index + 1).toString().padStart(parseInt(digits), '0');
let newName = prefix + sequence + (extension ? '.' + extension : '');
file.setName(newName);
});
return "重新命名成功!共處理 " + fileIds.length + " 個檔案。";
} 貼到 Apps Script 的 Code.gs 檔案
5. 貼上前端(index.html)
把前端程式貼到 Apps Script 的 index.html。
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--primary: #2c3e50; /* 深藍學術風 */
--accent: #e67e22; /* 青春橘 */
--sidebar-bg: #ffffff;
--main-bg: #f0f3f6;
}
body { font-family: "Microsoft JhengHei", "PingFang TC", sans-serif; margin: 0; display: flex; flex-direction: column; height: 100vh; background: var(--main-bg); color: #333; }
header { background: linear-gradient(135deg, var(--primary), #34495e); color: white; padding: 1rem 2rem; box-shadow: 0 2px 10px rgba(0,0,0,0.2); z-index: 10; }
header h1 { margin: 0; font-size: 1.4rem; display: flex; align-items: center; gap: 12px; }
.main { display: flex; flex: 1; overflow: hidden; }
/* 左側:樹狀目錄 (20%) */
#sidebar { width: 20%; background: var(--sidebar-bg); border-right: 1px solid #d1d8e0; overflow-y: auto; padding: 15px; }
#sidebar h3 { font-size: 1rem; color: var(--primary); border-bottom: 2px solid var(--accent); padding-bottom: 5px; margin-top: 0; }
.tree-node { margin-left: 12px; margin-top: 4px; }
.folder-label { display: flex; align-items: center; padding: 8px; border-radius: 6px; transition: 0.2s; cursor: pointer; font-size: 0.95rem; }
.folder-label:hover { background: #f1f2f6; }
.folder-label i { margin-right: 10px; color: #f1c40f; font-size: 1.1rem; }
.active-folder { background: #e3f2fd !important; font-weight: bold; color: var(--primary); box-shadow: inset 3px 0 0 var(--accent); }
.loading-text { font-size: 12px; color: #888; margin-left: 25px; font-style: italic; }
/* 中間:檔案列表 (60%) */
#content { width: 60%; padding: 20px; overflow-y: auto; position: relative; }
.toolbar { background: white; padding: 12px; border-radius: 10px; margin-bottom: 20px; display: flex; gap: 10px; align-items: center; position: sticky; top: 0; z-index: 5; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
.photo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 18px; }
.photo-card { background: white; border: 1px solid #e1e8ed; padding: 10px; border-radius: 10px; text-align: center; transition: 0.3s; cursor: pointer; position: relative; }
.photo-card:hover { transform: translateY(-4px); box-shadow: 0 8px 20px rgba(0,0,0,0.1); border-color: var(--accent); }
.photo-card input { position: absolute; top: 8px; left: 8px; transform: scale(1.4); cursor: pointer; z-index: 2; }
.img-container { height: 100px; display: flex; align-items: center; justify-content: center; overflow: hidden; margin-bottom: 8px; background: #f8f9fa; border-radius: 6px; }
.img-container img { max-width: 100%; max-height: 100%; object-fit: contain; }
.photo-card p { font-size: 12px; margin: 0; color: #444; word-break: break-all; height: 2.8em; overflow: hidden; line-height: 1.4; }
/* 右側:預覽 (20%) */
#preview-panel { width: 20%; background: white; border-left: 1px solid #d1d8e0; padding: 20px; display: flex; flex-direction: column; align-items: center; }
.preview-box { width: 100%; border: 2px dashed #cbd5e0; border-radius: 12px; min-height: 250px; display: flex; align-items: center; justify-content: center; overflow: hidden; background: #fcfcfc; }
#full-preview { max-width: 100%; max-height: 450px; display: none; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
#p-hint { color: #a0aec0; text-align: center; padding: 20px; font-size: 0.9rem; }
/* 底部:控制列 */
footer { background: white; border-top: 4px solid var(--accent); padding: 15px 30px; display: flex; justify-content: center; gap: 25px; align-items: center; box-shadow: 0 -2px 10px rgba(0,0,0,0.05); }
.input-group { display: flex; align-items: center; gap: 10px; font-weight: bold; color: var(--primary); }
input[type="text"], select { padding: 10px; border: 1px solid #cbd5e0; border-radius: 6px; outline: none; transition: 0.2s; }
input[type="text"]:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(230, 126, 34, 0.2); }
.btn-main { background: var(--accent); color: white; border: none; padding: 10px 30px; border-radius: 6px; cursor: pointer; font-weight: bold; font-size: 1rem; transition: 0.2s; display: flex; align-items: center; gap: 8px; }
.btn-main:hover { background: #d35400; transform: scale(1.05); }
/* Loading 遮罩 */
#mask { display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(255,255,255,0.8); z-index:1000; justify-content:center; align-items:center; flex-direction:column; }
.spinner { color: var(--accent); font-size: 3rem; margin-bottom: 15px; animation: spin 1s infinite linear; }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div id="mask">
<i class="fas fa-circle-notch spinner"></i>
<p style="font-weight:bold; color:var(--primary)">系統處理中,請稍候...</p>
</div>
<header>
<h1 id="app-title"><i class="fas fa-graduation-cap"></i> 載入中...</h1>
</header>
<div class="main">
<div id="sidebar">
<h3><i class="fas fa-folder-tree"></i> 校園雲端目錄</h3>
<div id="tree-root">正在連線至雲端硬碟...</div>
</div>
<div id="content">
<div class="toolbar">
<button onclick="checkAll(true)" style="background:#2ecc71; color:white; border:none; padding:8px 15px; border-radius:6px; cursor:pointer;"><i class="fas fa-check-square"></i> 全選照片</button>
<button onclick="checkAll(false)" style="background:#95a5a6; color:white; border:none; padding:8px 15px; border-radius:6px; cursor:pointer;"><i class="fas fa-minus-square"></i> 取消選取</button>
<span id="status-msg" style="margin-left:auto; font-weight:bold; color:var(--accent)"></span>
</div>
<div id="photo-list" class="photo-grid">請從左側點選活動資料夾...</div>
</div>
<div id="preview-panel">
<h3 style="color:var(--primary)"><i class="fas fa-eye"></i> 畫面預覽</h3>
<div class="preview-box">
<span id="p-hint"><i class="fas fa-mouse-pointer"></i><br>點擊左側照片<br>可在此查看大圖</span>
<img id="full-preview" src="">
</div>
<p id="p-filename" style="margin-top:20px; text-align:center; font-weight:bold; color:var(--primary)"></p>
</div>
</div>
<footer>
<div class="input-group">
<label><i class="fas fa-tag"></i> 新檔名前綴:</label>
<input type="text" id="prefix" placeholder="例如:2024校慶_">
</div>
<div class="input-group">
<label><i class="fas fa-list-ol"></i> 序號位數:</label>
<select id="digits">
<option value="1">1 (1, 2...)</option>
<option value="2" selected>01 (01, 02...)</option>
<option value="3">001 (001, 002...)</option>
</select>
</div>
<button class="btn-main" onclick="runRename()"><i class="fas fa-sync-alt"></i> 執行批次改名</button>
</footer>
<script>
let currentFolderId = '';
let allFiles = [];
window.onload = () => {
// 1. 初始化讀取試算表標題
google.script.run.withSuccessHandler(data => {
document.getElementById('app-title').innerHTML = `<i class="fas fa-university"></i> ${data.title}`;
}).getAppData();
// 2. 載入第一層資料夾
initRoot();
};
function initRoot() {
const rootContainer = document.getElementById('tree-root');
google.script.run.withSuccessHandler(folders => {
renderTree(folders, rootContainer);
}).getRootFolders();
}
// 渲染樹狀結構元件
function renderTree(folders, container) {
if (folders.length === 0) {
container.innerHTML = '<div class="loading-text">無子資料夾</div>';
return;
}
container.innerHTML = '';
folders.forEach(f => {
const node = document.createElement('div');
node.className = 'tree-node';
node.innerHTML = `
<div class="folder-label" id="label-${f.id}" onclick="handleFolderClick('${f.id}', '${f.name}')">
<i class="fas fa-folder"></i> ${f.name}
</div>
<div id="child-${f.id}" style="display:none"></div>
`;
container.appendChild(node);
});
}
// 點擊資料夾處理
function handleFolderClick(id, name) {
// UI 反饋:切換選取顏色
document.querySelectorAll('.folder-label').forEach(el => el.classList.remove('active-folder'));
document.getElementById(`label-${id}`).classList.add('active-folder');
const childDiv = document.getElementById(`child-${id}`);
// 如果是第一次點擊(內容為空),則顯示讀取中並抓取子目錄
if (childDiv.innerHTML === '') {
childDiv.style.display = 'block';
childDiv.innerHTML = '<div class="loading-text">讀取中...</div>';
google.script.run.withSuccessHandler(subFolders => {
// 完成讀取後,renderTree 會覆寫 innerHTML,原本的「讀取中...」會自動消失
renderTree(subFolders, childDiv);
}).getChildFolders(id);
} else {
// 若已有內容,則切換顯示/隱藏
childDiv.style.display = (childDiv.style.display === 'none') ? 'block' : 'none';
}
// 抓取該資料夾的照片
currentFolderId = id;
loadFiles(id);
}
// 載入照片列表
function loadFiles(id) {
showLoading(true);
google.script.run.withSuccessHandler(files => {
allFiles = files;
const list = document.getElementById('photo-list');
list.innerHTML = '';
if (files.length === 0) {
list.innerHTML = '<div style="grid-column: 1/-1; text-align:center; padding:50px; color:#95a5a6;">此資料夾內沒有照片檔案。</div>';
} else {
files.forEach(f => {
const card = document.createElement('div');
card.className = 'photo-card';
// 點擊卡片任何地方啟動預覽(除了勾選框以外)
card.onclick = (e) => {
if(e.target.tagName !== 'INPUT') showPreview(f.id);
};
card.innerHTML = `
<input type="checkbox" class="file-chk" value="${f.id}">
<div class="img-container">
<img src="data:${f.mimeType};base64,${f.base64}">
</div>
<p title="${f.name}">${f.name}</p>
`;
list.appendChild(card);
});
}
showLoading(false);
}).getFilesInFolder(id);
}
// 預覽大圖
function showPreview(id) {
const file = allFiles.find(f => f.id === id);
if (!file) return;
const img = document.getElementById('full-preview');
const hint = document.getElementById('p-hint');
img.src = `data:${file.mimeType};base64,${file.base64}`;
img.style.display = 'block';
hint.style.display = 'none';
document.getElementById('p-filename').innerText = file.name;
}
// 全選/取消
function checkAll(bool) {
document.querySelectorAll('.file-chk').forEach(c => c.checked = bool);
}
// 執行重新命名
function runRename() {
const prefixInput = document.getElementById('prefix');
const prefix = prefixInput.value;
const digits = document.getElementById('digits').value;
const selectedIds = Array.from(document.querySelectorAll('.file-chk:checked')).map(c => c.value);
if (!prefix) return alert('請輸入新檔名的前綴字!');
if (selectedIds.length === 0) return alert('請先勾選想要更改檔名的照片!');
if (!confirm(`確定要將這 ${selectedIds.length} 張照片重新命名嗎?`)) return;
showLoading(true);
google.script.run.withSuccessHandler(msg => {
alert(msg);
// 成功後清空前綴輸入框
prefixInput.value = '';
// 重新整理目前的檔案列表
loadFiles(currentFolderId);
}).renameFiles(currentFolderId, selectedIds, prefix, digits);
}
function showLoading(bool) {
document.getElementById('mask').style.display = bool ? 'flex' : 'none';
}
</script>
</body>
</html>
貼到 Apps Script 的 index.html 檔案
6. 部署
部署成 Web App 後,就會拿到可分享網址。
- Apps Script 右上:部署 → 新部署
- 類型選:Web app
- 執行身分:你
- 存取權限:任何人
- 完成後複製網址
💬 評論區
老師們的交流與提問
登入 後即可發表評論