0. 作品介紹
封面
2. 資料表規則
請照下面規則建立分頁與欄位。
第1分頁: 分頁1
A 欄: 班級
B 欄: 座號
C 欄: 姓名
D 欄: 註記
E 欄: 行
F 欄: 列
📄 解析結果(自動表格)
分頁:分頁1
| A 欄 | B 欄 | C 欄 | D 欄 | E 欄 | F 欄 |
|---|---|---|---|---|---|
| 班級 | 座號 | 姓名 | 註記 | 行 | 列 |
3. 開啟 Apps Script
從試算表開 Apps Script,才能直接用 SpreadsheetApp 讀寫資料。
- 試算表上方選單:擴充功能 → Apps Script
- 刪掉預設的程式碼(或全部選取覆蓋)
-
保留/建立檔案:
- Code.gs
- index.html
4. 貼上後端(Code.gs)
把後端程式貼到 Apps Script 的 Code.gs。
function doGet() {
return HtmlService.createTemplateFromFile('index')
.evaluate()
.setTitle('作伙來上課 - 森林班級管理系統')
.addMetaTag('viewport', 'width=device-width, initial-scale=1')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
// 讀取試算表資料(包含學生名單與班級尺寸設定)
function getSpreadsheetData() {
try {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var studentSheet = ss.getSheetByName("學生名單");
if (!studentSheet) {
return { success: false, msg: "找不到名為 '學生名單' 的工作表,請檢查名稱。" };
}
// 🔥 自動檢查並建立「班級設定」分頁(超強防呆)
var configSheet = ss.getSheetByName("班級設定");
if (!configSheet) {
configSheet = ss.insertSheet("班級設定");
configSheet.appendRow(["班級", "前後排數", "左右人數"]);
}
// 1. 抓取學生名單
var studentData = studentSheet.getDataRange().getValues();
var students = [];
for (var i = 1; i < studentData.length; i++) {
var row = studentData[i];
if (!row[0]) continue;
students.push({
"班級": String(row[0]).trim(),
"座號": String(row[1]).trim(),
"姓名": String(row[2]).trim(),
"註記": row[3] ? String(row[3]).trim() : "",
"組別": row[4] ? String(row[4]).trim() : "",
"行": row[5] ? parseInt(row[5]) : "",
"列": row[6] ? parseInt(row[6]) : ""
});
}
// 2. 抓取班級尺寸設定
var configData = configSheet.getDataRange().getValues();
var configs = {};
for (var i = 1; i < configData.length; i++) {
var cRow = configData[i];
if (!cRow[0]) continue;
configs[String(cRow[0]).trim()] = {
rows: cRow[1] ? parseInt(cRow[1]) : 6, // 預設 6 排
cols: cRow[2] ? parseInt(cRow[2]) : 6 // 預設 6 人
};
}
return { success: true, students: students, configs: configs };
} catch(e) {
return { success: false, msg: e.toString() };
}
}
// 儲存特定班級的座位網格尺寸
function saveClassConfig(className, rows, cols) {
try {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var configSheet = ss.getSheetByName("班級設定");
var data = configSheet.getDataRange().getValues();
var found = false;
for (var i = 1; i < data.length; i++) {
if (String(data[i][0]).trim() === String(className).trim()) {
configSheet.getRange(i + 1, 2).setValue(rows);
configSheet.getRange(i + 1, 3).setValue(cols);
found = true;
break;
}
}
if (!found) {
configSheet.appendRow([className, rows, cols]);
}
return { success: true };
} catch(e) {
return { success: false, msg: e.toString() };
}
}
// 儲存學生座位座標
function saveSeatPositions(positions) {
try {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("學生名單");
var range = sheet.getDataRange();
var data = range.getValues();
var posMap = {};
positions.forEach(function(p) {
posMap[String(p.class).trim() + '_' + String(p.seatNo).trim()] = { r: p.row, c: p.col };
});
for (var i = 1; i < data.length; i++) {
var key = String(data[i][0]).trim() + '_' + String(data[i][1]).trim();
if (posMap[key]) {
data[i][5] = posMap[key].r;
data[i][6] = posMap[key].c;
}
}
range.setValues(data);
return { success: true };
} catch(e) {
return { success: false, msg: e.toString() };
}
}
// 重置特定班級座位紀錄
function clearClassPositions(className) {
try {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("學生名單");
var range = sheet.getDataRange();
var data = range.getValues();
for (var i = 1; i < data.length; i++) {
if (String(data[i][0]).trim() === String(className).trim()) {
data[i][5] = "";
data[i][6] = "";
}
}
range.setValues(data);
return { success: true };
} catch(e) {
return { success: false, msg: e.toString() };
}
} 貼到 Apps Script 的 Code.gs 檔案
5. 貼上前端(index.html)
把前端程式貼到 Apps Script 的 index.html。
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<meta charset="UTF-8">
<title>作伙來上課 - 森林管理系統</title>
<style>
:root {
--forest-green: #3A5A40;
--forest-dark: #273E2B;
--wood-brown: #8B5A2B;
--bark-dark: #4A3525;
--honey-orange: #E9C46A;
--paper-cream: #F4F0E6;
--card-bg: #FFFDF9;
--reset-red: #BC4749;
}
body {
font-family: "Chalkboard SE", "Comic Sans MS", "Comic Sans", "華康少女體", "Noto Sans TC", sans-serif;
background-color: var(--paper-cream);
color: var(--bark-dark);
margin: 0;
padding: 20px;
background-image: radial-gradient(rgba(74, 53, 37, 0.04) 1px, transparent 0);
background-size: 24px 24px;
}
header {
background-color: var(--forest-green);
color: white;
padding: 15px 25px;
border: 3px solid var(--bark-dark);
border-radius: 255px 15px 225px 15px/15px 225px 15px 255px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 4px 4px 0px var(--bark-dark);
margin-bottom: 25px;
}
header h1 {
margin: 0;
font-size: 26px;
color: var(--honey-orange);
display: flex;
align-items: center;
gap: 10px;
text-shadow: 2px 2px 0px rgba(0,0,0,0.2);
}
.controls {
display: flex;
gap: 10px;
align-items: center;
font-size: 14px;
font-weight: bold;
}
.size-input-group {
display: flex;
align-items: center;
gap: 4px;
background: rgba(255,255,255,0.15);
padding: 4px 8px;
border-radius: 10px;
border: 1.5px solid var(--bark-dark);
}
select, input[type="text"], input[type="number"] {
padding: 6px 10px;
border: 2px solid var(--bark-dark);
border-radius: 20px 10px 20px 10px/10px 20px 10px 20px;
font-size: 14px;
outline: none;
background-color: #FFF;
font-family: inherit;
}
select:focus, input:focus {
background-color: #FFFDF0;
}
button {
background-color: var(--honey-orange);
color: var(--bark-dark);
border: 2px solid var(--bark-dark);
padding: 8px 14px;
border-radius: 15px 5px 15px 5px/5px 15px 5px 15px;
font-weight: bold;
font-family: inherit;
cursor: pointer;
box-shadow: 2px 2px 0px var(--bark-dark);
transition: all 0.1s;
}
button:hover {
transform: translate(-1px, -1px);
box-shadow: 3px 3px 0px var(--bark-dark);
}
button:active {
transform: translate(1px, 1px);
box-shadow: 1px 1px 0px var(--bark-dark);
}
.main-container {
display: grid;
grid-template-columns: 3fr 1fr;
gap: 25px;
}
.classroom-box {
background: #FFFDF4;
padding: 25px;
border: 3px solid var(--bark-dark);
border-radius: 30px 15px 40px 20px / 20px 40px 15px 30px;
box-shadow: 5px 5px 0px var(--bark-dark);
display: flex;
flex-direction: column;
position: relative;
}
.classroom-box::after {
content: "🦊🌳🐰🍃";
position: absolute;
top: -18px;
right: 25px;
font-size: 24px;
background: var(--paper-cream);
padding: 0 8px;
}
.podium {
background-color: #D2B48C;
color: var(--bark-dark);
text-align: center;
padding: 10px;
border: 3px solid var(--bark-dark);
border-radius: 60px 40px 70px 50px / 15px 15px 15px 15px;
font-weight: bold;
margin: 25px auto 0 auto;
width: 220px;
box-shadow: 3px 3px 0px var(--bark-dark);
order: 2;
background-image: repeating-radial-gradient(circle, #C5A070, #D2B48C 20px);
}
/* 🔥 移除固定的 6x6 改由 JS 控管動態直排橫列數 */
.grid-dynamic {
display: grid;
gap: 14px;
order: 1;
}
.seat-slot {
background-color: #EAF2E8;
border: 2px dashed #A2B997;
border-radius: 12px;
aspect-ratio: 1 / 1;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.student-card {
background-color: var(--card-bg);
border: 2.5px solid var(--bark-dark);
border-radius: 255px 15px 225px 15px/15px 225px 15px 255px;
width: 100%;
height: 100%;
box-shadow: 3px 3px 0px rgba(74, 53, 37, 0.15);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: grab;
user-select: none;
transition: transform 0.1s, box-shadow 0.1s;
box-sizing: border-box;
padding: 4px;
text-align: center;
}
.student-card:active {
cursor: grabbing;
transform: scale(1.03) rotate(2deg);
}
.student-card.picked {
animation: flash-card 0.4s ease 5;
background-color: #FFF0F0;
border-color: #E53E3E;
}
@keyframes flash-card {
0%, 100% { background-color: var(--card-bg); transform: scale(1); }
50% { background-color: #FFC0CB; transform: scale(1.08) rotate(-2deg); }
}
.student-no {
font-size: 11px;
color: #8B7355;
font-weight: bold;
}
.student-name {
font-size: 17px;
font-weight: bold;
margin: 1px 0;
color: var(--bark-dark);
}
.student-note {
font-size: 11px;
color: #C0392B;
background-color: #FDF2F2;
border: 1px solid #F5C6CB;
padding: 1px 5px;
border-radius: 10px;
max-width: 90%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hide-notes .student-note {
display: none !important;
}
.sidebar {
display: flex;
flex-direction: column;
gap: 20px;
}
.tool-card {
background: #FFFDF9;
padding: 20px;
border: 2.5px solid var(--bark-dark);
border-radius: 15px 25px 12px 20px / 20px 12px 25px 15px;
box-shadow: 4px 4px 0px var(--bark-dark);
}
.tool-card h3 {
margin-top: 0;
border-bottom: 3px dashed var(--primary);
padding-bottom: 6px;
margin-bottom: 15px;
color: var(--forest-green);
display: flex;
align-items: center;
gap: 5px;
}
.timer-display {
font-size: 38px;
font-weight: bold;
text-align: center;
font-family: monospace;
margin: 10px 0;
color: var(--bark-dark);
background: #F4EFE3;
padding: 5px;
border-radius: 8px;
border: 1.5px solid var(--bark-dark);
}
.timer-alert {
animation: blink-bg 0.5s infinite alternate;
}
@keyframes blink-bg {
0% { background-color: #FFFDF9; }
100% { background-color: #FFD2D2; }
}
.timer-inputs {
display: flex;
gap: 5px;
justify-content: center;
margin-bottom: 10px;
align-items: center;
}
.timer-inputs input {
width: 45px;
text-align: center;
}
.btn-group {
display: flex;
gap: 5px;
justify-content: center;
flex-wrap: wrap;
}
.lucky-result {
font-size: 19px;
font-weight: bold;
color: #A0522D;
text-align: center;
margin-top: 12px;
min-height: 27px;
}
.pool-count {
font-size: 12px;
color: #7A6048;
text-align: center;
margin-bottom: 8px;
}
#print-zone {
display: none;
}
@media print {
body * {
visibility: hidden;
}
#print-zone, #print-zone * {
visibility: visible;
}
#print-zone {
display: block;
position: absolute;
left: 0;
top: 0;
width: 100%;
}
.print-page {
page-break-after: always;
padding: 20px;
display: flex;
flex-direction: column;
}
.print-grid {
display: grid;
gap: 15px;
order: 1;
}
.print-card {
border: 2px solid #000;
border-radius: 10px;
padding: 10px 5px;
text-align: center;
aspect-ratio: 1/1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.print-title {
font-size: 24px;
font-weight: bold;
text-align: center;
margin-bottom: 20px;
}
.print-podium {
text-align: center;
font-weight: bold;
margin-top: 20px;
padding: 8px;
border: 2px solid black;
border-radius: 20px;
width: 160px;
margin-left: auto;
margin-right: auto;
order: 2;
}
}
</style>
</head>
<body>
<header id="header-bar">
<h1>🌲 作伙來上課 <span id="title-class-sub"></span></h1>
<div class="controls">
<select id="class-select" onchange="switchClass()">
<option value="">讀取森林班級中...</option>
</select>
<div class="size-input-group">
<span>前後排數:</span>
<input type="number" id="class-rows" min="1" max="10" value="6" style="width: 40px;" onchange="updateClassSizeConfig()">
<span style="margin-left:5px;">左右人數:</span>
<input type="number" id="class-cols" min="1" max="10" value="6" style="width: 40px;" onchange="updateClassSizeConfig()">
</div>
<input type="text" id="subject-input" placeholder="🐿️ 填寫科目(如:閩語)" oninput="updateTitle()">
<button onclick="toggleNotes()">🍃 顯示註記</button>
<button onclick="resetClassSeats()" style="background-color: var(--reset-red); color: white;">🔄 重置本班座位</button>
<button onclick="openPrintMenu()">🖨️ 列印座位表</button>
</div>
</header>
<div class="main-container" id="main-view">
<div class="classroom-box">
<div class="podium">🪵 講台</div>
<div class="grid-dynamic" id="seating-grid"></div>
</div>
<div class="sidebar">
<div class="tool-card" id="timer-card">
<h3>⏱️ 森林計時器</h3>
<div class="timer-inputs">
<input type="number" id="timer-min" min="0" value="5">分
<input type="number" id="timer-sec" min="0" max="59" value="0">秒
</div>
<div class="timer-display" id="timer-clock">05:00</div>
<div class="btn-group">
<button onclick="startTimer(true)">倒數</button>
<button onclick="startTimer(false)">正計</button>
<button onclick="pauseTimer()">暫停</button>
<button onclick="resetTimer()">重置</button>
</div>
</div>
<div class="tool-card">
<h3>🎰 幸運抽松果</h3>
<div class="pool-count" id="pool-info">剩餘動物池:0 人</div>
<div style="display: flex; gap: 8px; margin-bottom: 10px; justify-content: center; font-size: 13px;">
<label><input type="number" id="draw-num" value="1" min="1" style="width: 35px;"> 人</label>
<label><input type="checkbox" id="no-repeat-chk" checked> 不重複</label>
</div>
<div class="btn-group">
<button onclick="drawLucky()" style="background-color: #E76F51; color:white;">開始抽籤</button>
<button onclick="resetLuckyPool()">重置籤池</button>
</div>
<div class="lucky-result" id="lucky-display"></div>
</div>
</div>
</div>
<div id="print-zone"></div>
<script>
let allRawData = [];
let allConfigs = {}; // 存放各班級的行列尺寸對照表
let currentClass = "";
let studentPool = [];
let timerInterval = null;
let timeRemaining = 300;
let isCountdown = true;
let audioCtx = null;
window.onload = function() {
google.script.run.withSuccessHandler(function(response) {
if(response.success) {
allRawData = response.students;
allConfigs = response.configs;
populateClassSelect();
} else {
alert("讀取失敗:" + response.msg);
}
}).getSpreadsheetData();
};
// 建立指定尺寸的動態格子
function createDynamicGrid(rows, cols) {
const grid = document.getElementById('seating-grid');
grid.innerHTML = '';
grid.style.gridTemplateColumns = `repeat(${cols}, 1fr)`; // 動態設定寬度
let totalSlots = rows * cols;
for (let i = 0; i < totalSlots; i++) {
const slot = document.createElement('div');
slot.className = 'seat-slot';
slot.dataset.slotIndex = i;
slot.addEventListener('dragover', allowDrop);
slot.addEventListener('drop', handleDrop);
grid.appendChild(slot);
}
}
function populateClassSelect() {
const select = document.getElementById('class-select');
select.innerHTML = '';
const classes = [...new Set(allRawData.map(item => item.班級))];
if(classes.length === 0) {
select.innerHTML = '<option>尚無班級資料</option>';
return;
}
classes.forEach(c => {
const opt = document.createElement('option');
opt.value = c;
opt.innerText = c + ' 班';
select.appendChild(opt);
});
switchClass();
}
function switchClass() {
currentClass = document.getElementById('class-select').value;
updateTitle();
if (!currentClass) return;
document.getElementById('seating-grid').innerHTML = '<div style="grid-column: span 3; text-align:center; padding:20px; color:#7A6048;">🌲 森林夥伴報到中...</div>';
google.script.run.withSuccessHandler(function(response) {
if(response.success) {
allRawData = response.students;
allConfigs = response.configs;
// 載入該班級專屬尺寸設定(若無則預設 6x6)
let clsConfig = allConfigs[currentClass] || { rows: 6, cols: 6 };
document.getElementById('class-rows').value = clsConfig.rows;
document.getElementById('class-cols').value = clsConfig.cols;
resetLuckyPool();
renderSeats();
} else {
alert("同步失敗:" + response.msg);
}
}).getSpreadsheetData();
}
// 當老師在網頁上修改前後排數或左右人數時觸發
function updateClassSizeConfig() {
if(!currentClass) return;
let rows = parseInt(document.getElementById('class-rows').value) || 6;
let cols = parseInt(document.getElementById('class-cols').value) || 6;
// 更新本機快取
allConfigs[currentClass] = { rows: rows, cols: cols };
// 存回試算表背景
google.script.run.withSuccessHandler(function(res){
if(res.success) {
renderSeats(); // 重新畫圖對齊
}
}).saveClassConfig(currentClass, rows, cols);
}
function updateTitle() {
const sub = document.getElementById('subject-input').value;
const subText = sub ? ` - 【${sub}】` : '';
document.getElementById('title-class-sub').innerText = `[ ${currentClass} 班 ] ${subText}`;
}
function renderSeats() {
let totalRows = parseInt(document.getElementById('class-rows').value) || 6;
let totalCols = parseInt(document.getElementById('class-cols').value) || 6;
createDynamicGrid(totalRows, totalCols);
const students = allRawData.filter(item => item.班級 == currentClass);
const slots = document.querySelectorAll('.seat-slot');
let unassigned = [];
students.forEach(stu => {
let r = parseInt(stu.行);
let c = parseInt(stu.列);
// 檢查是否有儲存紀錄,且不能超出老師剛剛自訂的尺寸界限
if(r >= 1 && r <= totalRows && c >= 1 && c <= totalCols) {
let gridRowIndex = totalRows - r; // 核心排列模型:第1排在網格底部
let idx = gridRowIndex * totalCols + (c - 1);
if (idx >= 0 && idx < slots.length) {
const card = createStudentCard(stu);
slots[idx].appendChild(card);
} else {
unassigned.push(stu);
}
} else {
unassigned.push(stu);
}
});
unassigned.sort((a, b) => parseInt(a.座號) - parseInt(b.座號));
unassigned.forEach(stu => {
let placed = false;
for (let r = 1; r <= totalRows; r++) {
let gridRowIndex = totalRows - r;
for (let c = 1; c <= totalCols; c++) {
let idx = gridRowIndex * totalCols + (c - 1);
if (idx < slots.length && !slots[idx].firstElementChild) {
const card = createStudentCard(stu);
slots[idx].appendChild(card);
stu.行 = r;
stu.列 = c;
placed = true;
break;
}
}
if (placed) break;
}
});
}
function resetClassSeats() {
if(!currentClass) return;
if(!confirm(`確定要重置 [ ${currentClass} 班 ] 的座位排列嗎?\n這會清除此班級在試算表中的行列紀錄,並重新從第一排(最下方)依序排起。`)) return;
document.getElementById('seating-grid').innerHTML = '<div style="text-align:center; padding:20px; color:#7A6048;">正在清除舊紀錄並重新排列...</div>';
google.script.run.withSuccessHandler(function(response) {
if(response.success) {
google.script.run.withSuccessHandler(function(res) {
if(res.success) {
allRawData = res.students;
renderSeats();
}
}).getSpreadsheetData();
} else {
alert("重置失敗:" + response.msg);
}
}).clearClassPositions(currentClass);
}
function createStudentCard(stu) {
const card = document.createElement('div');
card.className = 'student-card';
card.id = `stu-${stu.座號}`;
card.draggable = true;
card.dataset.seatNo = stu.座號;
card.dataset.name = stu.姓名;
card.innerHTML = `
<div class="student-no">N°.${stu.座號}</div>
<div class="student-name">${stu.姓名 || '無姓名'}</div>
<div class="student-note" title="${stu.註記 || ''}">${stu.註記 || ''}</div>
`;
card.addEventListener('dragstart', handleDragStart);
return card;
}
function toggleNotes() {
document.getElementById('main-view').classList.toggle('hide-notes');
}
let draggedCard = null;
function handleDragStart(e) {
draggedCard = this;
e.dataTransfer.setData('text/plain', this.id);
}
function allowDrop(e) {
e.preventDefault();
}
function handleDrop(e) {
e.preventDefault();
let slot = e.target;
while(slot && !slot.classList.contains('seat-slot')) {
slot = slot.parentElement;
}
if(!slot || !draggedCard) return;
if(slot.firstElementChild) {
let occupant = slot.firstElementChild;
let originSlot = draggedCard.parentElement;
originSlot.appendChild(occupant);
slot.appendChild(draggedCard);
} else {
slot.appendChild(draggedCard);
}
autoSavePositions();
}
function autoSavePositions() {
let totalRows = parseInt(document.getElementById('class-rows').value) || 6;
let totalCols = parseInt(document.getElementById('class-cols').value) || 6;
let positions = [];
const slots = document.querySelectorAll('.seat-slot');
slots.forEach(slot => {
if(slot.firstElementChild) {
let idx = parseInt(slot.dataset.slotIndex);
let gridRowIndex = Math.floor(idx / totalCols);
let r = totalRows - gridRowIndex; // 基於動態排數的反轉換算
let c = (idx % totalCols) + 1;
let seatNo = slot.firstElementChild.dataset.seatNo;
positions.push({
class: currentClass,
seatNo: seatNo,
row: r,
col: c
});
let match = allRawData.find(item => item.班級 == currentClass && item.座號 == seatNo);
if(match) { match.行 = r; match.列 = c; }
}
});
if(positions.length > 0) {
google.script.run.saveSeatPositions(positions);
}
}
// --- 工具箱功能保持成功完美版 ---
function startTimer(countdownMode) {
clearInterval(timerInterval);
isCountdown = countdownMode;
document.getElementById('timer-card').classList.remove('timer-alert');
if(isCountdown) {
let mins = parseInt(document.getElementById('timer-min').value) || 0;
let secs = parseInt(document.getElementById('timer-sec').value) || 0;
timeRemaining = mins * 60 + secs;
} else {
timeRemaining = 0;
}
updateTimerDisplay();
timerInterval = setInterval(function() {
if(isCountdown) {
if(timeRemaining <= 0) {
clearInterval(timerInterval);
triggerAlarm();
} else {
timeRemaining--;
}
} else {
timeRemaining++;
}
updateTimerDisplay();
}, 1000);
}
function pauseTimer() { clearInterval(timerInterval); }
function resetTimer() {
clearInterval(timerInterval);
document.getElementById('timer-card').classList.remove('timer-alert');
document.getElementById('timer-min').value = 5;
document.getElementById('timer-sec').value = 0;
document.getElementById('timer-clock').innerText = "05:00";
}
function updateTimerDisplay() {
let m = Math.floor(Math.abs(timeRemaining) / 60);
let s = Math.abs(timeRemaining) % 60;
document.getElementById('timer-clock').innerText = (m < 10 ? '0':'') + m + ':' + (s < 10 ? '0':'') + s;
}
function triggerAlarm() {
document.getElementById('timer-card').classList.add('timer-alert');
try {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let osc = audioCtx.createOscillator();
let gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'sine';
osc.frequency.value = 800;
gain.gain.setValueAtTime(0.5, audioCtx.currentTime);
osc.start();
osc.stop(audioCtx.currentTime + 1);
} catch(e) {}
}
function resetLuckyPool() {
const students = allRawData.filter(item => item.班級 == currentClass);
studentPool = students.map(s => ({ seatNo: s.座號, name: s.姓名 }));
updatePoolInfo();
document.getElementById('lucky-display').innerText = "";
}
function updatePoolInfo() { document.getElementById('pool-info').innerText = `剩餘動物池:${studentPool.length} 人`; }
function drawLucky() {
document.querySelectorAll('.student-card').forEach(c => c.classList.remove('picked'));
const students = allRawData.filter(item => item.班級 == currentClass);
if(students.length === 0) return;
let count = parseInt(document.getElementById('draw-num').value) || 1;
let noRepeat = document.getElementById('no-repeat-chk').checked;
let winners = [];
if(noRepeat) {
if(studentPool.length === 0) { alert("抽籤池已空!系統為您自動重置。"); resetLuckyPool(); }
let realCount = Math.min(count, studentPool.length);
for(let i=0; i<realCount; i++) {
let randIdx = Math.floor(Math.random() * studentPool.length);
winners.push(studentPool.splice(randIdx, 1)[0]);
}
updatePoolInfo();
} else {
for(let i=0; i<count; i++) {
let randIdx = Math.floor(Math.random() * students.length);
winners.push({ seatNo: students[randIdx].座號, name: students[randIdx].姓名 });
}
}
if(winners.length > 0) {
document.getElementById('lucky-display').innerText = "🎉 中籤者:" + winners.map(w => w.name).join(', ');
winners.forEach(w => {
let card = document.getElementById(`stu-${w.seatNo}`);
if(card) card.classList.add('picked');
});
}
}
function openPrintMenu() {
const classes = [...new Set(allRawData.map(item => item.班級))];
let promptMsg = "請輸入您想列印的班級(多個班級請用逗號隔開)\n\n可選班級有:" + classes.join(', ') + "\n";
let inputClasses = prompt(promptMsg, currentClass);
if(!inputClasses) return;
let targetClasses = inputClasses.split(/[,,]/).map(c => c.trim());
const sub = document.getElementById('subject-input').value;
const subText = sub ? ` - 【${sub}】` : '';
const isNotesHidden = document.getElementById('main-view').classList.contains('hide-notes');
let printZone = document.getElementById('print-zone');
printZone.innerHTML = '';
targetClasses.forEach(cls => {
const students = allRawData.filter(item => item.班級 == cls);
if(students.length === 0) return;
// 🌟 列印時動態讀取各班專屬尺寸
let clsConfig = allConfigs[cls] || { rows: 6, cols: 6 };
let pRows = clsConfig.rows;
let pCols = clsConfig.cols;
let page = document.createElement('div');
page.className = 'print-page';
page.innerHTML = `<div class="print-title">🎒 作伙來上課 - [ ${cls} 班 ] ${subText} 座位表</div>`;
let printGrid = document.createElement('div');
printGrid.className = 'print-grid';
printGrid.style.gridTemplateColumns = `repeat(${pCols}, 1fr)`; // 動態寬度
let totalSlots = pRows * pCols;
let gridArray = Array(totalSlots).fill(null);
let unassigned = [];
students.forEach(stu => {
let r = parseInt(stu.行);
let c = parseInt(stu.列);
if(r >= 1 && r <= pRows && c >= 1 && c <= pCols) {
let gridRowIndex = pRows - r;
let idx = gridRowIndex * pCols + (c - 1);
if(idx >= 0 && idx < totalSlots) gridArray[idx] = stu;
} else {
unassigned.push(stu);
}
});
unassigned.sort((a, b) => parseInt(a.座號) - parseInt(b.座號));
unassigned.forEach(stu => {
for(let i=0; i<totalSlots; i++) {
if(!gridArray[i]) { gridArray[i] = stu; break; }
}
});
for(let i=0; i<totalSlots; i++) {
let pCard = document.createElement('div');
pCard.className = 'print-card';
let stu = gridArray[i];
if(stu) {
let noteHtml = (!isNotesHidden && stu.註記) ? `<div style="font-size:11px; color:#555; border:1px solid #ccc; border-radius:3px; margin-top:2px; padding:0 2px;">${stu.註記}</div>` : '';
pCard.innerHTML = `
<div style="font-size:12px;">${stu.座號}</div>
<div style="font-size:18px; font-weight:bold;">${stu.姓名}</div>
${noteHtml}
`;
}
printGrid.appendChild(pCard);
}
page.appendChild(printGrid);
let pPodium = document.createElement('div');
pPodium.className = 'print-podium';
pPodium.innerText = '講台';
page.appendChild(pPodium);
printZone.appendChild(page);
});
window.print();
}
</script>
</body>
</html>
貼到 Apps Script 的 index.html 檔案
6. 部署
部署成 Web App 後,就會拿到可分享網址。
- Apps Script 右上:部署 → 新部署
- 類型選:Web app
- 執行身分:你
- 存取權限:任何人
- 完成後複製網址
💬 評論區
老師們的交流與提問
登入 後即可發表評論