0. 作品介紹
封面
2. 資料表規則
請照下面規則建立分頁與欄位。
第1分頁: Students
A 欄: 建立一個新的 Google 試算表。
B 欄: 將工作表(分頁)名稱更改為 Students。
C 欄: 在第一行建立以下標題:
D 欄: A1: 座號 | B1: 姓名 | C1: 小組 | D1: 個人分數 | E1: 頭像網址
E 欄: 填入資料:請至少填入 30 位學生的資料(1-30 號)。「小組」欄位請填入 1~6(代表第幾組)。
📄 解析結果(自動表格)
分頁:Students
| A 欄 | B 欄 | C 欄 | D 欄 | E 欄 |
|---|---|---|---|---|
| 建立一個新的 Google 試算表。 | 將工作表(分頁)名稱更改為 Students。 | 在第一行建立以下標題: | A1: 座號 | B1: 姓名 | C1: 小組 | D1: 個人分數 | E1: 頭像網址 | 填入資料:請至少填入 30 位學生的資料(1-30 號)。「小組」欄位請填入 1~6(代表第幾組)。 |
3. 開啟 Apps Script
從試算表開 Apps Script,才能直接用 SpreadsheetApp 讀寫資料。
- 試算表上方選單:擴充功能 → Apps Script
- 刪掉預設的程式碼(或全部選取覆蓋)
-
保留/建立檔案:
- Code.gs
- index.html
4. 貼上後端(Code.gs)
把後端程式貼到 Apps Script 的 Code.gs。
/** Code.gs **/
function doGet() {
return HtmlService.createTemplateFromFile('index')
.evaluate()
.setTitle('溫馨班級座位管理系統')
.addMetaTag('viewport', 'width=device-width, initial-scale=1')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
// 取得學生名單
function getStudentData() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName('Students');
const data = sheet.getDataRange().getValues();
data.shift(); // 移除標題
return data.map(row => ({
id: row[0].toString(),
name: row[1],
group: row[2],
score: row[3],
img: row[4] || 'https://cdn-icons-png.flaticon.com/512/194/194931.png'
}));
}
// 更新分數
function updateScore(studentId, addScore) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName('Students');
const data = sheet.getRange("A:A").getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][0].toString() == studentId) {
const cell = sheet.getRange(i + 1, 4);
const newScore = Number(cell.getValue()) + Number(addScore);
cell.setValue(newScore);
return { success: true, newScore: newScore };
}
}
}
// 儲存目前的座位順序到試算表隱藏欄位或 ScriptProperties
function saveSeating(orderArray) {
PropertiesService.getScriptProperties().setProperty('savedOrder', JSON.stringify(orderArray));
return "已儲存座位!";
}
function getSavedSeating() {
return JSON.parse(PropertiesService.getScriptProperties().getProperty('savedOrder') || "[]");
}/** Code.gs 額外新增的部分 **/
// 清除所有學生的分數
function resetAllScores() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName('Students');
const lastRow = sheet.getLastRow();
if (lastRow > 1) {
// 假設分數在第 4 欄 (D 欄),從第 2 列開始全部設為 0
const scoreRange = sheet.getRange(2, 4, lastRow - 1, 1);
scoreRange.setValue(0);
return "分數已全部歸零!";
}
return "目前沒有學生資料。";
} 貼到 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: #ffb6c1; --secondary: #fff0f5; --accent: #87ceeb;
}
body { font-family: 'Comic Sans MS', 'Microsoft JhengHei', sans-serif; background: var(--secondary); padding: 20px; }
/* 頂部看板 */
.dashboard { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; background: white; padding: 15px; border-radius: 25px; box-shadow: 0 5px 15px rgba(0,0,0,0.1); margin-bottom: 20px; }
.rank-box { padding: 10px; border: 2px solid var(--secondary); border-radius: 15px; }
h2 { color: #db7093; font-size: 1.2rem; margin-top: 0; }
/* 控制列 */
.toolbar { display: flex; justify-content: center; gap: 10px; margin-bottom: 20px; }
button { background: var(--primary); color: white; border: none; padding: 10px 20px; border-radius: 50px; cursor: pointer; font-weight: bold; transition: 0.3s; }
button:hover { background: #ff69b4; transform: translateY(-2px); }
.save-btn { background: var(--accent); }
/* 5x6 座位 */
.grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; max-width: 900px; margin: 0 auto; }
.seat-slot { background: white; border: 3px dashed var(--primary); border-radius: 15px; padding: 10px; min-height: 110px; position: relative; }
.student-card { cursor: pointer; text-align: center; }
.student-card img { width: 45px; height: 45px; border-radius: 50%; border: 2px solid var(--primary); }
.name-tag { font-weight: bold; font-size: 14px; margin: 5px 0; display: block; }
.score-tag { font-size: 12px; background: #fffacd; padding: 2px 8px; border-radius: 10px; }
/* 下拉選單樣式 */
select { width: 100%; margin-top: 5px; border-radius: 5px; border: 1px solid #ccc; font-size: 12px; }
/* 彈窗 */
.modal { display:none; position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); background:white; padding:25px; border-radius:20px; box-shadow:0 10px 30px rgba(0,0,0,0.2); z-index:100; }
.overlay { display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.4); z-index:99; }
</style>
</head>
<body>
<div class="dashboard">
<div class="rank-box"><h2>🏆 個人排行</h2><div id="top-3"></div></div>
<div class="rank-box"><h2>👥 小組排行</h2><div id="group-rank"></div></div>
</div>
<div class="toolbar">
<button onclick="randomShuffle()"><i class="fas fa-random"></i> 全部隨機</button>
<button onclick="clearSeats()"><i class="fas fa-eraser"></i> 清空座位</button>
<button style="background: #ff4d4d;" onclick="resetScores()"><i class="fas fa-trash-alt"></i> 清除分數</button>
<button class="save-btn" onclick="saveToCloud()"><i class="fas fa-cloud-upload-alt"></i> 儲存佈局</button>
</div>
<div class="grid" id="grid"></div>
<div class="overlay" id="overlay" onclick="closeModal()"></div>
<div class="modal" id="modal">
<h3 id="m-name">姓名</h3>
<input type="number" id="m-score" value="1" style="width:80px; font-size:20px; text-align:center;">
<p>請輸入加分分值</p>
<button onclick="applyScore()">確認加分</button>
</div>
<script>
let students = []; // 原始學生資料
let currentOrder = Array(30).fill(""); // 目前 30 個座位的學生 ID
window.onload = () => {
google.script.run.withSuccessHandler(data => {
students = data;
google.script.run.withSuccessHandler(saved => {
if(saved && saved.length > 0) currentOrder = saved;
render();
}).getSavedSeating();
}).getStudentData();
};
function render() {
const grid = document.getElementById('grid');
grid.innerHTML = '';
// 找出目前哪些學生還沒被選 (用於下拉選單)
const usedIds = currentOrder.filter(id => id !== "");
for (let i = 0; i < 30; i++) {
const slot = document.createElement('div');
slot.className = 'seat-slot';
const studentId = currentOrder[i];
const student = students.find(s => s.id == studentId);
if (student) {
slot.innerHTML = `
<div class="student-card" onclick="openModal('${student.id}', '${student.name}')">
<img src="${student.img}">
<span class="name-tag">${student.name}</span>
<span class="score-tag">${student.score} 分</span>
</div>
<button style="position:absolute; bottom:5px; right:5px; padding:2px 5px; font-size:10px;" onclick="removeStudent(${i})">撤換</button>
`;
} else {
// 顯示選單
let options = `<option value="">--挑選學生--</option>`;
students.forEach(s => {
if (!usedIds.includes(s.id)) {
options += `<option value="${s.id}">${s.id}. ${s.name}</option>`;
}
});
slot.innerHTML = `<select onchange="assignStudent(${i}, this.value)">${options}</select>`;
}
grid.appendChild(slot);
}
updateRankings();
}
function assignStudent(index, id) {
currentOrder[index] = id;
render();
}
function removeStudent(index) {
currentOrder[index] = "";
render();
}
function randomShuffle() {
let ids = students.map(s => s.id);
// 洗牌
for (let i = ids.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[ids[i], ids[j]] = [ids[j], ids[i]];
}
// 填入前 30 名
currentOrder = Array(30).fill("").map((_, i) => ids[i] || "");
render();
}
function clearSeats() {
if(confirm("確定要清空目前座位重新安排嗎?")) {
currentOrder = Array(30).fill("");
render();
}
}
function saveToCloud() {
google.script.run.withSuccessHandler(() => alert("座位表已同步到雲端!")).saveSeating(currentOrder);
}
// --- 加分邏輯 ---
let targetId = null;
function openModal(id, name) {
targetId = id;
document.getElementById('m-name').innerText = name;
document.getElementById('modal').style.display = 'block';
document.getElementById('overlay').style.display = 'block';
}
function closeModal() {
document.getElementById('modal').style.display = 'none';
document.getElementById('overlay').style.display = 'none';
}
function applyScore() {
const val = document.getElementById('m-score').value;
google.script.run.withSuccessHandler(res => {
const s = students.find(x => x.id == targetId);
s.score = res.newScore;
render();
closeModal();
}).updateScore(targetId, val);
}
function updateRankings() {
const top3 = [...students].sort((a,b) => b.score - a.score).slice(0,3);
document.getElementById('top-3').innerHTML = top3.map((s,i) => `<b>${i+1}.</b> ${s.name}(${s.score})`).join(' | ');
const groups = {};
students.forEach(s => { groups[s.group] = (groups[s.group] || 0) + s.score; });
const gRank = Object.entries(groups).sort((a,b) => b[1]-a[1]);
document.getElementById('group-rank').innerHTML = gRank.map(g => `第${g[0]}組: ${g[1]}分`).join(' / ');
}
</script>/** script 區塊新增部分 **/
function resetScores() {
// 第一次確認
if (confirm("⚠️ 注意!這會將所有學生的個人與小組分數「歸零」,確定要執行嗎?")) {
// 第二次確認,避免手滑
const finalCheck = prompt("請輸入 'RESET' 以確認清除所有分數:");
if (finalCheck === "RESET") {
google.script.run.withSuccessHandler(msg => {
alert(msg);
// 更新網頁上的學生資料狀態
students.forEach(s => s.score = 0);
render(); // 重新渲染畫面
}).resetAllScores();
} else {
alert("已取消操作。");
}
}
}
</body>
</html>
貼到 Apps Script 的 index.html 檔案
6. 部署
部署成 Web App 後,就會拿到可分享網址。
- Apps Script 右上:部署 → 新部署
- 類型選:Web app
- 執行身分:你
- 存取權限:任何人
- 完成後複製網址
💬 評論區
老師們的交流與提問
登入 後即可發表評論