0. 作品介紹
預設圖片
1. AI 提示詞
把這段指令貼給 AI,讓它幫你生成程式碼。
1.老師可以排志願序
2.老師不能看到別人的選擇 所以每個老師會有一個特定代碼 輸入才能夠填資料
3.他有計算年資 應該是我輸入了 校內跟 總年資之後要幫我相加他沒有做到
4.管理員可以看到所有老師的志願序
5.依照志願序可以看到結果 先比較大家第一志願 跟職務 沒有人填相同職務 那就是那個人當選 那有衝突的話就看誰的積分高 就是那個人當選
6.第一志願比完 剩下的人就比第二志願序 以此類推
這個志願的結果分析只有管理員可以看到
2.老師不能看到別人的選擇 所以每個老師會有一個特定代碼 輸入才能夠填資料
3.他有計算年資 應該是我輸入了 校內跟 總年資之後要幫我相加他沒有做到
4.管理員可以看到所有老師的志願序
5.依照志願序可以看到結果 先比較大家第一志願 跟職務 沒有人填相同職務 那就是那個人當選 那有衝突的話就看誰的積分高 就是那個人當選
6.第一志願比完 剩下的人就比第二志願序 以此類推
這個志願的結果分析只有管理員可以看到
2. 資料表規則
請照下面規則建立分頁與欄位。
第1分頁: 老師資料
A 欄: 編號
B 欄: 姓名
C 欄: 本校年資
D 欄: 總年資
E 欄: 計算年資
F 欄: 專長
G 欄: 登入代碼
H 欄: 已選職務
第2分頁: 職務設定
A 欄: 職務1
B 欄: 職務2
C 欄: 職務3
D 欄: 職務4
E 欄: 職務5
第3分頁: 固定職務
A 欄: 職務名稱
B 欄: 是否固定
第4分頁: 志願紀錄
A 欄: 時間戳記
B 欄: 姓名
C 欄: 第1志願
D 欄: 第2志願
E 欄: 第3志願
F 欄: 第4志願
G 欄: 第5志願
第5分頁: 系統設定
A 欄: 設定項目
B 欄: 開始時間
C 欄: 結束時間
第6分頁: 專長對照
A 欄: 職務名稱
B 欄: 需求專長
📄 解析結果(自動表格)
分頁:老師資料
| A 欄 | B 欄 | C 欄 | D 欄 | E 欄 | F 欄 | G 欄 | H 欄 |
|---|---|---|---|---|---|---|---|
| 編號 | 姓名 | 本校年資 | 總年資 | 計算年資 | 專長 | 登入代碼 | 已選職務 |
分頁:職務設定
| A 欄 | B 欄 | C 欄 | D 欄 | E 欄 |
|---|---|---|---|---|
| 職務1 | 職務2 | 職務3 | 職務4 | 職務5 |
分頁:固定職務
| A 欄 | B 欄 |
|---|---|
| 職務名稱 | 是否固定 |
分頁:志願紀錄
| A 欄 | B 欄 | C 欄 | D 欄 | E 欄 | F 欄 | G 欄 |
|---|---|---|---|---|---|---|
| 時間戳記 | 姓名 | 第1志願 | 第2志願 | 第3志願 | 第4志願 | 第5志願 |
分頁:系統設定
| A 欄 | B 欄 | C 欄 |
|---|---|---|
| 設定項目 | 開始時間 | 結束時間 |
分頁:專長對照
| A 欄 | B 欄 |
|---|---|
| 職務名稱 | 需求專長 |
3. 開啟 Apps Script
從試算表開 Apps Script,才能直接用 SpreadsheetApp 讀寫資料。
- 試算表上方選單:擴充功能 → Apps Script
- 刪掉預設的程式碼(或全部選取覆蓋)
-
保留/建立檔案:
- Code.gs
- index.html
4. 貼上後端(Code.gs)
把後端程式貼到 Apps Script 的 Code.gs。
// ============================================
// 老師職務選填系統 v5.3 - 數字轉文字修復版
// ============================================
function doGet() {
return HtmlService.createTemplateFromFile('index')
.evaluate()
.setTitle('老師職務選填系統 v5.3')
.addMetaTag('viewport', 'width=device-width, initial-scale=1')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
// --- 1. 系統檢核與登入 ---
function checkSystemStatus() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('系統設定');
if(!sheet) return { isOpen: true };
var startRange = sheet.getRange('B2').getValue();
var endRange = sheet.getRange('B3').getValue();
if(!startRange || !endRange) return { isOpen: true };
var start = new Date(startRange);
var end = new Date(endRange);
var now = new Date();
if (now < start) return { isOpen: false, msg: "系統尚未開放,開放時間:" + formatTime(start) };
if (now > end) return { isOpen: false, msg: "登記時間已截止,截止時間:" + formatTime(end) };
return { isOpen: true };
}
function formatTime(date) {
return Utilities.formatDate(date, Session.getScriptTimeZone(), "yyyy/MM/dd HH:mm");
}
function login(name, code) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('老師資料');
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
if (data[i][1] == name && data[i][6].toString() == code.toString()) {
return {
success: true,
name: data[i][1],
specialty: data[i][5],
isAdmin: (name === '管理員')
};
}
}
return { success: false, message: '姓名或登入代碼錯誤' };
}
// --- 2. 資料讀取 ---
function getUserData(teacherName) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var dutySheet = ss.getSheetByName('職務設定');
var fixedSheet = ss.getSheetByName('固定職務');
var fixedDuties = [];
if(fixedSheet) {
var fData = fixedSheet.getDataRange().getValues();
for(var i=1; i<fData.length; i++) {
if(fData[i][1] === '是' || fData[i][1] === true) fixedDuties.push(fData[i][0]);
}
}
var lastRow = dutySheet.getLastRow();
if (lastRow < 2) return { duties: [], savedPrefs: ["","","","",""] };
var rawData = dutySheet.getRange(2, 1, lastRow - 1, 5).getValues();
var dutyList = [];
for(var r=0; r<rawData.length; r++){
for(var c=0; c<rawData[r].length; c++){
var dName = rawData[r][c];
if(dName && fixedDuties.indexOf(dName) === -1) dutyList.push(dName);
}
}
var prefSheet = ss.getSheetByName('志願紀錄');
var savedPrefs = ["", "", "", "", ""];
if (prefSheet && prefSheet.getLastRow() > 1) {
var pData = prefSheet.getDataRange().getValues();
for (var i = pData.length - 1; i >= 1; i--) {
if (pData[i][1] === teacherName) {
savedPrefs = pData[i].slice(2, 7);
break;
}
}
}
return { duties: dutyList, savedPrefs: savedPrefs };
}
// --- 3. 提交資料 ---
function submitPreferences(teacherName, prefs) {
var status = checkSystemStatus();
if (!status.isOpen) return { success: false, message: status.msg };
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('志願紀錄');
var data = sheet.getDataRange().getValues();
for (var i = data.length - 1; i >= 1; i--) {
if (data[i][1] === teacherName) {
sheet.deleteRow(i + 1);
}
}
var row = [new Date(), teacherName].concat(prefs);
sheet.appendRow(row);
return { success: true, message: "志願已儲存!" };
}
// --- 4. 核心演算法 ---
function runMatching() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var tSheet = ss.getSheetByName('老師資料');
var pSheet = ss.getSheetByName('志願紀錄');
var mapSheet = ss.getSheetByName('專長對照');
var specMap = {};
if (mapSheet && mapSheet.getLastRow() > 1) {
var mData = mapSheet.getDataRange().getValues();
for(var i=1; i<mData.length; i++) {
if(mData[i][0]) specMap[mData[i][0]] = mData[i][1];
}
}
var lastRow = tSheet.getLastRow();
var tData = tSheet.getRange(2, 1, lastRow - 1, 8).getValues();
var teachers = [];
tData.forEach((row, idx) => {
if(row[1]) {
teachers.push({
id: row[0],
name: row[1],
score: parseFloat(row[4] || 0),
specialty: row[5],
result: null,
rowIndex: idx
});
}
});
var prefMap = {};
if (pSheet && pSheet.getLastRow() > 1) {
var pData = pSheet.getDataRange().getValues();
for(var i=1; i<pData.length; i++) {
prefMap[pData[i][1]] = pData[i].slice(2).filter(String);
}
}
var allDutiesData = getUserData('admin');
var allDuties = allDutiesData.duties;
var assignedDuties = new Set();
// 撮合
for (var round = 0; round < 5; round++) {
var candidates = {};
teachers.forEach(t => {
if (t.result) return;
var userPrefs = prefMap[t.name];
if (userPrefs && userPrefs[round]) {
var dutyName = userPrefs[round];
if (!assignedDuties.has(dutyName)) {
if (!candidates[dutyName]) candidates[dutyName] = [];
candidates[dutyName].push(t);
}
}
});
for (var dutyName in candidates) {
var applicants = candidates[dutyName];
var requiredSpec = specMap[dutyName];
applicants.sort((a, b) => {
var aHasSpec = (requiredSpec && a.specialty === requiredSpec) ? 1 : 0;
var bHasSpec = (requiredSpec && b.specialty === requiredSpec) ? 1 : 0;
if (aHasSpec !== bHasSpec) return bHasSpec - aHasSpec;
return b.score - a.score;
});
var winner = applicants[0];
winner.result = dutyName;
assignedDuties.add(dutyName);
}
}
// 補位
var unluckyTeachers = teachers.filter(t => !t.result);
var remainingDuties = allDuties.filter(d => !assignedDuties.has(d));
for (var i = remainingDuties.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = remainingDuties[i];
remainingDuties[i] = remainingDuties[j];
remainingDuties[j] = temp;
}
unluckyTeachers.forEach((t, idx) => {
if (idx < remainingDuties.length) {
t.result = remainingDuties[idx] + " (隨機補位)";
} else {
t.result = "無職務可派";
}
});
// 回寫
var output = tData.map(function() { return [""]; });
teachers.forEach(t => {
output[t.rowIndex] = [t.result];
});
tSheet.getRange(2, 8, output.length, 1).setValues(output);
return getAdminView();
}
// *** 關鍵修正區:強制轉文字 ***
function getAdminView() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var tSheet = ss.getSheetByName('老師資料');
var lastRow = tSheet.getLastRow();
if(lastRow < 2) return { success: true, data: [] };
var data = tSheet.getRange(2, 1, lastRow - 1, 8).getValues();
var results = [];
data.forEach(r => {
if (r[1]) {
results.push({
name: r[1],
score: parseFloat(r[4]||0).toFixed(1),
specialty: r[5] || "",
// 修正重點:加上 String() 確保 401 變成 "401"
result: String(r[7] || "尚未分派")
});
}
});
results.sort((a,b) => b.score - a.score);
return { success: true, data: results, count: results.length };
} 貼到 Apps Script 的 Code.gs 檔案
5. 貼上前端(index.html)
把前端程式貼到 Apps Script 的 index.html。
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>老師職務選填系統 v5.3</title>
<style>
body { font-family: "Microsoft JhengHei", sans-serif; background: #f4f7f9; padding: 20px; color: #333; margin: 0; }
.container { max-width: 900px; margin: 0 auto; background: #fff; padding: 30px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.08); }
h2 { text-align: center; color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 15px; margin-top: 0; }
.btn { width: 100%; padding: 12px; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; transition: 0.2s; color: white; margin-top: 15px; font-weight: bold; }
.btn-primary { background: #3498db; }
.btn-danger { background: #e74c3c; }
.btn-success { background: #2ecc71; }
.btn-warning { background: #95a5a6; }
input, select { width: 100%; padding: 12px; margin: 8px 0; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; font-size: 14px; }
.info-box { background: #f8f9fa; padding: 15px; border-radius: 6px; margin-bottom: 20px; border-left: 4px solid #3498db; }
.table-container { overflow-x: auto; margin-top: 20px; }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th, td { border: 1px solid #eee; padding: 12px; text-align: left; }
th { background: #eef2f5; font-weight: bold; color: #555; }
.badge-random { background-color: #fff3cd; color: #856404; padding: 2px 6px; border-radius: 4px; font-size: 12px; }
.loading { position: fixed; top:0; left:0; width:100%; height:100%; background:rgba(255,255,255,0.9); display:none; flex-direction: column; justify-content:center; align-items:center; z-index:9999; }
.spinner { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin-bottom: 10px; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
#pageLogin, #pageUser, #pageAdmin { display: none; }
</style>
</head>
<body>
<div id="loading" class="loading">
<div class="spinner"></div>
<div id="loadingText">系統處理中...</div>
</div>
<div class="container">
<div id="pageLogin">
<h2>🏫 老師職務選填系統</h2>
<div id="systemMsg" style="text-align:center; color:#e74c3c; margin-bottom:15px; font-weight:bold;"></div>
<label>姓名</label><input type="text" id="inputName" placeholder="請輸入姓名">
<label>登入代碼</label><input type="password" id="inputCode" placeholder="請輸入代碼">
<button type="button" class="btn btn-primary" onclick="doLogin()">登入系統</button>
</div>
<div id="pageUser">
<h2>志願選填</h2>
<div class="info-box">
<p><strong>姓名:</strong><span id="userName"></span></p>
<p><strong>您的專長:</strong><span id="userSpec" style="color:#d35400; font-weight:bold;"></span></p>
</div>
<form id="prefForm">
<label>第 1 志願</label><select id="p1"></select>
<label>第 2 志願</label><select id="p2"></select>
<label>第 3 志願</label><select id="p3"></select>
<label>第 4 志願</label><select id="p4"></select>
<label>第 5 志願</label><select id="p5"></select>
</form>
<button type="button" class="btn btn-success" onclick="submitData()">💾 儲存我的志願</button>
<button type="button" class="btn btn-warning" onclick="logout()">登出</button>
</div>
<div id="pageAdmin">
<h2>後台管理</h2>
<button type="button" class="btn btn-primary" onclick="runMatch()">🚀 執行自動撮合 (含補位)</button>
<button type="button" class="btn btn-danger" style="margin-top:5px;" onclick="loadAdminData()">🔄 重新讀取表格</button>
<div style="margin-top:10px; font-size:12px; color:#666;">
系統狀態:<span id="dataCount">...</span>
</div>
<div class="table-container">
<table id="resultTable">
<thead>
<tr>
<th>姓名</th>
<th>積分</th>
<th>專長</th>
<th>分配結果</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<button type="button" class="btn btn-warning" onclick="logout()">登出</button>
</div>
</div>
<script>
let currentUser = {};
window.onload = function() {
toggleLoading(true, "檢查系統狀態...");
google.script.run.withSuccessHandler(res => {
toggleLoading(false);
if(!res.isOpen) document.getElementById('systemMsg').innerHTML = res.msg;
document.getElementById('pageLogin').style.display = 'block';
}).checkSystemStatus();
};
function toggleLoading(show, text="處理中...") {
document.getElementById('loadingText').innerText = text;
document.getElementById('loading').style.display = show ? 'flex' : 'none';
}
function doLogin() {
const name = document.getElementById('inputName').value.trim();
const code = document.getElementById('inputCode').value.trim();
if(!name || !code) return alert('請輸入完整資訊');
toggleLoading(true, "驗證中...");
google.script.run.withSuccessHandler(res => {
toggleLoading(false);
if(res.success) {
currentUser = res;
document.getElementById('pageLogin').style.display = 'none';
if(res.isAdmin) showAdminPage();
else showUserPage();
} else alert(res.message);
}).login(name, code);
}
function showUserPage() {
document.getElementById('pageUser').style.display = 'block';
document.getElementById('userName').innerText = currentUser.name;
document.getElementById('userSpec').innerText = currentUser.specialty || "無";
toggleLoading(true, "讀取資料...");
google.script.run.withSuccessHandler(data => {
toggleLoading(false);
const selects = ['p1','p2','p3','p4','p5'];
selects.forEach((id, idx) => {
const el = document.getElementById(id);
el.innerHTML = '<option value="">-- 請選擇 --</option>';
data.duties.forEach(d => { el.innerHTML += `<option value="${d}">${d}</option>`; });
if(data.savedPrefs[idx]) el.value = data.savedPrefs[idx];
});
}).getUserData(currentUser.name);
}
function submitData() {
const prefs = [
document.getElementById('p1').value,
document.getElementById('p2').value,
document.getElementById('p3').value,
document.getElementById('p4').value,
document.getElementById('p5').value
].filter(v => v !== "");
if (new Set(prefs).size !== prefs.length) return alert("錯誤:志願不能重複!");
if (prefs.length === 0) return alert("請至少選擇一個志願");
if (!confirm("確定要儲存嗎?")) return;
toggleLoading(true, "儲存中...");
google.script.run.withSuccessHandler(res => {
toggleLoading(false);
alert(res.message);
showUserPage();
}).submitPreferences(currentUser.name, prefs);
}
function showAdminPage() {
document.getElementById('pageAdmin').style.display = 'block';
loadAdminData();
}
function runMatch() {
if(!confirm("確定要執行撮合嗎?\n這將覆蓋目前的分配結果!")) return;
toggleLoading(true, "正在計算分配...");
google.script.run.withSuccessHandler(res => {
toggleLoading(false);
renderTable(res.data, res.count);
alert("撮合完成!");
}).runMatching();
}
function loadAdminData() {
toggleLoading(true, "讀取列表...");
google.script.run.withSuccessHandler(res => {
toggleLoading(false);
renderTable(res.data, res.count);
}).getAdminView();
}
// *** 關鍵修正區 ***
function renderTable(data, count) {
const tbody = document.querySelector('#resultTable tbody');
tbody.innerHTML = '';
document.getElementById('dataCount').innerText = '共讀取到 ' + (count || 0) + ' 位老師資料';
if(!data || data.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;">尚無資料</td></tr>';
return;
}
data.forEach(row => {
let bgStyle = '';
// 強制轉字串,防止數字 401 導致當機
let resultText = String(row.result);
if(resultText.indexOf('隨機') > -1) {
bgStyle = 'background-color: #fff3cd;';
resultText = resultText.replace('(隨機補位)', '<span class="badge-random">補</span>');
} else if (resultText === '尚未分派') {
bgStyle = 'background-color: #ffffff; color: #aaa;';
} else if (resultText.indexOf('無職務') > -1) {
bgStyle = 'background-color: #f8d7da; color: #721c24;';
} else {
bgStyle = 'background-color: #d4edda; color: #155724;';
}
tbody.innerHTML += `
<tr style="${bgStyle}">
<td>${row.name}</td>
<td>${row.score}</td>
<td>${row.specialty}</td>
<td>${resultText}</td>
</tr>
`;
});
}
function logout() {
if(!confirm("確定要登出嗎?")) return;
currentUser = {};
document.getElementById('inputName').value = '';
document.getElementById('inputCode').value = '';
document.getElementById('pageUser').style.display = 'none';
document.getElementById('pageAdmin').style.display = 'none';
document.getElementById('pageLogin').style.display = 'block';
document.querySelector('#resultTable tbody').innerHTML = '';
}
</script>
</body>
</html>
貼到 Apps Script 的 index.html 檔案
6. 部署
部署成 Web App 後,就會拿到可分享網址。
- Apps Script 右上:部署 → 新部署
- 類型選:Web app
- 執行身分:你
- 存取權限:任何人
- 完成後複製網址
💬 評論區
老師們的交流與提問
登入 後即可發表評論