0. 總覽與需求
目標:用 Google 試算表「資料收集」,用 Apps Script 發佈成教學工具網站。
完整教學影片
完整教學影片(約 1 小時)・從 AI 工具介紹到作品上傳,一次學完整個流程
互動式 Prompt 產生器
填寫你的需求,系統幫你組出完整 AI 指令,直接複製貼給 Claude / ChatGPT。
填寫上方欄位,指令會在這裡自動產生
1. 建立試算表與分頁
先準備資料結構,後端程式會依分頁名稱讀取。
- 開啟 Google 試算表 → 新增空白試算表
-
底下建立 2 個分頁(工作表):
- 學生資料
- 座位表
2. 建立資料分頁與欄位結構
本範例會使用 兩個試算表分頁,欄位位置需完全一致,請依下列說明建立。
📄 分頁一:學生資料
| A 欄 | B 欄 |
|---|---|
| 學號 | 姓名 |
說明:
• 分頁名稱請命名為 學生資料
• 程式會從 第 2 列 開始讀取學生資料(第 1 列為標題)
• 分頁名稱請命名為 學生資料
• 程式會從 第 2 列 開始讀取學生資料(第 1 列為標題)
📄 分頁二:座位表
| A 欄 | B 欄 | C 欄 |
|---|---|---|
| 列 | 欄 | 學號 |
說明:
• 分頁名稱請命名為 座位表
• A 欄:座位列(row)
• B 欄:座位欄(col)
• C 欄:對應的學生學號(需與「學生資料」分頁一致)
• 分頁名稱請命名為 座位表
• A 欄:座位列(row)
• B 欄:座位欄(col)
• C 欄:對應的學生學號(需與「學生資料」分頁一致)
3. 開啟 Apps Script
從試算表開 Apps Script,才能直接用 SpreadsheetApp 讀寫資料。
- 試算表上方選單:擴充功能 → Apps Script
- 刪掉預設的程式碼(或全部選取覆蓋)
-
保留/建立檔案:
- Code.gs
- index.html
4. 貼上後端(Code.gs)
負責讀取「學生資料」、讀取/儲存「座位表」、以及輸出網頁。
function doGet() {
return HtmlService.createHtmlOutputFromFile('index')
.setTitle('座位表示範');
}
// 讀取學生資料
function getStudents() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName('學生資料');
if (!sheet) return [];
var lastRow = sheet.getLastRow();
if (lastRow <= 1) return [];
var values = sheet.getRange(2, 1, lastRow - 1, 2).getValues(); // A,B 欄
return values.map(function(row) {
return { id: String(row[0]), name: String(row[1]) };
});
}
// 讀取既有座位配置
function getLayout() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName('座位表');
var layout = [];
if (!sheet) return layout;
var lastRow = sheet.getLastRow();
if (lastRow <= 1) return layout;
var values = sheet.getRange(2, 1, lastRow - 1, 3).getValues(); // row,col,studentId
values.forEach(function(r) {
var row = r[0], col = r[1], studentId = r[2];
if (row && col && studentId) {
layout.push({ row: row, col: col, studentId: String(studentId) });
}
});
return layout;
}
// 儲存座位配置
function saveLayout(layout) {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName('座位表');
if (!sheet) sheet = ss.insertSheet('座位表');
sheet.clear();
sheet.getRange(1, 1, 1, 3).setValues([['列', '欄', '學號']]);
if (!layout || layout.length === 0) return;
var data = layout.map(function(item) {
return [item.row, item.col, item.studentId || ''];
});
sheet.getRange(2, 1, data.length, 3).setValues(data);
}
貼到 Apps Script 的 Code.gs 檔案
5. 貼上前端(index.html)
負責座位格呈現、點選/放置/隨機、以及呼叫後端儲存。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: Arial, "微軟正黑體"; }
#studentPool, .seat {
border: 1px solid #ccc;
min-height: 40px;
min-width: 80px;
margin: 4px;
padding: 4px;
text-align: center;
vertical-align: middle;
}
#studentPool { display: flex; flex-wrap: wrap; }
.student {
border: 1px solid #666;
margin: 2px;
padding: 2px 4px;
cursor: pointer;
background: #f0f8ff;
user-select: none;
}
.student.selected { background: #ffd966; }
.seat { background: #fafafa; cursor: pointer; }
.seat.hasStudent { background: #d0f5d0; }
table { border-collapse: collapse; }
td { padding: 2px; }
button { margin-right: 8px; }
</style>
</head>
<body>
<h2>座位表示範(6 × 5)</h2>
<button onclick="randomAssign()">隨機安排剩餘同學</button>
<button onclick="resetAll()">全部重來</button>
<button onclick="save()">儲存座位表到試算表</button>
<h3>未分配學生</h3>
<div id="studentPool"></div>
<h3>教室座位</h3>
<table id="seatTable"></table>
<script>
var students = [];
var seatMap = {};
var selectedStudentId = null;
function init() {
google.script.run.withSuccessHandler(function(stuData) {
students = stuData || [];
google.script.run.withSuccessHandler(function(layout) {
layout = layout || [];
layout.forEach(function(item) {
seatMap[item.row + '-' + item.col] = item.studentId;
});
buildSeatGrid(6, 5);
renderAll();
}).getLayout();
}).getStudents();
}
function buildSeatGrid(rows, cols) {
var table = document.getElementById('seatTable');
table.innerHTML = '';
for (var r = 1; r <= rows; r++) {
var tr = document.createElement('tr');
for (var c = 1; c <= cols; c++) {
var td = document.createElement('td');
var seatDiv = document.createElement('div');
seatDiv.className = 'seat';
seatDiv.dataset.row = r;
seatDiv.dataset.col = c;
seatDiv.addEventListener('click', onSeatClick);
td.appendChild(seatDiv);
tr.appendChild(td);
}
table.appendChild(tr);
}
}
function renderAll() { renderSeats(); renderStudentPool(); }
function renderSeats() {
var seats = document.querySelectorAll('.seat');
seats.forEach(function(seat) {
var key = seat.dataset.row + '-' + seat.dataset.col;
var studentId = seatMap[key];
seat.textContent = '';
seat.classList.remove('hasStudent');
if (studentId) {
var s = findStudentById(studentId);
if (s) {
seat.textContent = s.name;
seat.classList.add('hasStudent');
}
}
});
}
function renderStudentPool() {
var pool = document.getElementById('studentPool');
pool.innerHTML = '';
var assignedIds = Object.values(seatMap).filter(function(v){ return v; });
students.forEach(function(s) {
if (assignedIds.indexOf(s.id) === -1) {
var div = document.createElement('div');
div.className = 'student';
div.textContent = s.name;
div.dataset.id = s.id;
if (selectedStudentId === s.id) div.classList.add('selected');
div.addEventListener('click', onStudentClick);
pool.appendChild(div);
}
});
}
function onStudentClick(e) {
var id = e.currentTarget.dataset.id;
selectedStudentId = (selectedStudentId === id) ? null : id;
renderStudentPool();
}
function onSeatClick(e) {
var seat = e.currentTarget;
var key = seat.dataset.row + '-' + seat.dataset.col;
if (selectedStudentId) {
removeStudentFromSeats(selectedStudentId);
seatMap[key] = selectedStudentId;
selectedStudentId = null;
} else {
if (seatMap[key]) seatMap[key] = '';
}
renderAll();
}
function removeStudentFromSeats(studentId) {
Object.keys(seatMap).forEach(function(k) {
if (seatMap[k] === studentId) seatMap[k] = '';
});
}
function findStudentById(id) {
for (var i = 0; i < students.length; i++) {
if (students[i].id === id) return students[i];
}
return null;
}
function randomAssign() {
var assignedIds = Object.values(seatMap).filter(function(v){ return v; });
var unassigned = students.filter(function(s) {
return assignedIds.indexOf(s.id) === -1;
});
var seats = [];
document.querySelectorAll('.seat').forEach(function(seat) {
var key = seat.dataset.row + '-' + seat.dataset.col;
if (!seatMap[key]) seats.push(key);
});
function shuffle(arr) {
for (var i = arr.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp;
}
}
shuffle(unassigned);
shuffle(seats);
var count = Math.min(unassigned.length, seats.length);
for (var k = 0; k < count; k++) {
seatMap[seats[k]] = unassigned[k].id;
}
selectedStudentId = null;
renderAll();
}
function resetAll() {
seatMap = {};
selectedStudentId = null;
renderAll();
}
function save() {
var layout = [];
document.querySelectorAll('.seat').forEach(function(seat) {
var row = Number(seat.dataset.row);
var col = Number(seat.dataset.col);
var key = row + '-' + col;
layout.push({ row: row, col: col, studentId: seatMap[key] || '' });
});
google.script.run.saveLayout(layout);
alert('已儲存到「座位表」工作表!');
}
init();
</script>
</body>
</html>
貼到 Apps Script 的 index.html 檔案
6. 部署給所有人
部署成 Web App 後,就會拿到可分享網址。
- Apps Script 右上:部署 → 新部署
- 類型選:Web app
- 執行身分:你
- 存取權限:任何人(或你要的範圍)
- 完成後複製網址
7. 驗證:改資料庫看前端會不會變
改「學生資料」分頁,再刷新座位表頁面,確認是否同步。
- 新增學生後:前端「未分配學生」會出現
- 儲存座位後:試算表「座位表」分頁會更新
8. 下指令修飾前端(提示詞範例)
你可以把需求丟給 AI 產生更漂亮的 UI,但保留原功能。
提示詞範例
我現在的版面不好看。
需求說明:【把座位表改成更科幻的,學生選座位時會很開心的風格】
需求說明:【把座位表改成更科幻的,學生選座位時會很開心的風格】
9. 大功告成
你已經完成「試算表當資料庫 → Web App 前端操作 → 儲存座位配置」。
下一步你可以做:
- 改成支援拖曳(Drag & Drop)
- 支援固定幾位學生、剩餘再隨機
- 加入座號、分組、特殊座位區