0. 作品介紹
預設圖片
1. AI 提示詞
把這段指令貼給 AI,讓它幫你生成程式碼。
1. 我想要建立一個 【老師職務選填表】
2. 幫我用Google 試算表建立 【老師資料庫】
3. 我希望有些資料我可以直接在試算表更改首頁可以直接呈現
4. 需求說明 【11×5 職務位欄位、可指定位置固定、位置上要可以填位置的名稱、可計算年資,公式:本校年資×1+總年資×0.5、可依據年資排序順序、最好列點】
5. 要把它變成程式碼我要發佈
6. 請完整敘述操作步驟 越詳細越好
7. 作業環境
8. Google試算表
9. Google Apps Script
10. 我看不懂程式碼,請給我後端gs完整程式碼、前端index完整程式碼
2. 幫我用Google 試算表建立 【老師資料庫】
3. 我希望有些資料我可以直接在試算表更改首頁可以直接呈現
4. 需求說明 【11×5 職務位欄位、可指定位置固定、位置上要可以填位置的名稱、可計算年資,公式:本校年資×1+總年資×0.5、可依據年資排序順序、最好列點】
5. 要把它變成程式碼我要發佈
6. 請完整敘述操作步驟 越詳細越好
7. 作業環境
8. Google試算表
9. Google Apps Script
10. 我看不懂程式碼,請給我後端gs完整程式碼、前端index完整程式碼
2. 資料表規則
請照下面規則建立分頁與欄位。
第1分頁: 老師資料
A 欄: 編號
B 欄: 姓名
C 欄: 本校年資
D 欄: 總年資
E 欄: 計算年資
F 欄: 已選職務
第2分頁: 職務設定
A 欄: 職務1
B 欄: 職務2
C 欄: 職務3
D 欄: 職務4
E 欄: 職務5
第3分頁: 固定職務
A 欄: 職務名稱
B 欄: 是否固定
📄 解析結果(自動表格)
分頁:老師資料
| A 欄 | B 欄 | C 欄 | D 欄 | E 欄 | F 欄 |
|---|---|---|---|---|---|
| 編號 | 姓名 | 本校年資 | 總年資 | 計算年資 | 已選職務 |
分頁:職務設定
| A 欄 | B 欄 | C 欄 | D 欄 | E 欄 |
|---|---|---|---|---|
| 職務1 | 職務2 | 職務3 | 職務4 | 職務5 |
分頁:固定職務
| A 欄 | B 欄 |
|---|---|
| 職務名稱 | 是否固定 |
3. 開啟 Apps Script
從試算表開 Apps Script,才能直接用 SpreadsheetApp 讀寫資料。
- 試算表上方選單:擴充功能 → Apps Script
- 刪掉預設的程式碼(或全部選取覆蓋)
-
保留/建立檔案:
- Code.gs
- index.html
4. 貼上後端(Code.gs)
把後端程式貼到 Apps Script 的 Code.gs。
// ============================================
// 老師職務選填系統 v2.0 - 後端程式
// 年資計算:本校年資×1 + 總年資×0.5
// 職務表格:11列×5欄(共55個位置)
// ============================================
// 開啟網頁介面
function doGet() {
return HtmlService.createHtmlOutputFromFile('index')
.setTitle('老師職務選填系統')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
// 取得試算表物件
function getSpreadsheet() {
return SpreadsheetApp.getActiveSpreadsheet();
}
// 取得老師資料(依計算年資排序)
function getTeacherData() {
var ss = getSpreadsheet();
var sheet = ss.getSheetByName('老師資料');
if (!sheet) {
return { success: false, message: '找不到「老師資料」工作表' };
}
var lastRow = sheet.getLastRow();
if (lastRow < 2) {
return { success: false, message: '沒有老師資料' };
}
// 讀取所有資料(從第2列開始)
// A:編號, B:姓名, C:本校年資, D:總年資, E:計算年資, F:已選職務
var data = sheet.getRange(2, 1, lastRow - 1, 6).getValues();
// 轉換成物件陣列
var teachers = [];
for (var i = 0; i < data.length; i++) {
var row = data[i];
// 跳過空白列
if (!row[0] && !row[1]) continue;
// 計算年資公式:本校*1 + 總年資*0.5
var calcScore = (row[2] || 0) * 1 + (row[3] || 0) * 0.5;
// 回寫計算年資到試算表 E 欄 (第5欄),確保試算表也是最新的
// 注意:為了效能,這裡暫時不逐筆回寫,僅在記憶體中計算並排序
teachers.push({
id: row[0],
name: row[1],
schoolYears: row[2] || 0, // 本校年資
totalYears: row[3] || 0, // 總年資
calculatedYears: calcScore, // 計算年資(程式計算)
selectedDuty: row[5] || '', // 已選職務
rowIndex: i + 2 // 試算表中的列號
});
}
// 依計算年資排序(年資高的在前面)
teachers.sort(function(a, b) {
return b.calculatedYears - a.calculatedYears;
});
return { success: true, data: teachers };
}
// 取得職務設定資料(11列×5欄)
function getDutyData() {
var ss = getSpreadsheet();
var sheet = ss.getSheetByName('職務設定');
if (!sheet) {
return { success: false, message: '找不到「職務設定」工作表' };
}
// 取得固定職務清單
var fixedDuties = getFixedDuties();
// 讀取11列5欄的資料(從第2列開始)
var numRows = 11;
var numCols = 5;
// 確保範圍內有資料,避免錯誤
var lastRow = sheet.getLastRow();
if(lastRow < 2) return { success: true, data: [] };
var data = sheet.getRange(2, 1, numRows, numCols).getValues();
var duties = [];
var position = 1;
// 將資料轉換成一維陣列
for (var row = 0; row < numRows; row++) {
for (var col = 0; col < numCols; col++) {
var dutyName = '';
// 避免超出範圍
if (data[row] && data[row][col]) {
dutyName = data[row][col];
}
var isFixed = fixedDuties.indexOf(dutyName) !== -1;
duties.push({
position: position, // 位置編號 1-55
name: dutyName, // 職務名稱
fixed: isFixed, // 是否固定
row: row + 2, // 試算表中的列號
col: col + 1, // 試算表中的欄號
displayRow: row + 1, // 顯示用的列號(1-11)
displayCol: col + 1 // 顯示用的欄號(1-5)
});
position++;
}
}
return { success: true, data: duties };
}
// 取得固定職務清單
function getFixedDuties() {
var ss = getSpreadsheet();
var sheet = ss.getSheetByName('固定職務');
if (!sheet) {
return [];
}
var lastRow = sheet.getLastRow();
if (lastRow < 2) {
return [];
}
var data = sheet.getRange(2, 1, lastRow - 1, 2).getValues();
var fixedList = [];
for (var i = 0; i < data.length; i++) {
var dutyName = data[i][0];
var isFixed = data[i][1];
// 如果標記為「是」,加入固定清單
if (isFixed === '是' || isFixed === 'TRUE' || isFixed === true) {
fixedList.push(dutyName);
}
}
return fixedList;
}
// 取得所有資料(供前端顯示)
function getAllData() {
var teacherResult = getTeacherData();
var dutyResult = getDutyData();
if (!teacherResult.success) {
return teacherResult;
}
if (!dutyResult.success) {
return dutyResult;
}
return {
success: true,
teachers: teacherResult.data,
duties: dutyResult.data
};
}
// 儲存老師的職務選填
function saveSelection(teacherId, dutyPosition, dutyName) {
var ss = getSpreadsheet();
var sheet = ss.getSheetByName('老師資料');
if (!sheet) {
return { success: false, message: '找不到「老師資料」工作表' };
}
// 檢查該職務是否為固定職務
var fixedDuties = getFixedDuties();
if (fixedDuties.indexOf(dutyName) !== -1) {
return { success: false, message: '此職務為固定職務,無法選填' };
}
// 找到該老師的列
var lastRow = sheet.getLastRow();
var data = sheet.getRange(2, 1, lastRow - 1, 1).getValues();
var targetRow = -1;
for (var i = 0; i < data.length; i++) {
if (data[i][0] == teacherId) {
targetRow = i + 2;
break;
}
}
if (targetRow === -1) {
return { success: false, message: '找不到該老師' };
}
// 檢查該職務是否已被其他人選取 (排除自己)
var allSelections = sheet.getRange(2, 6, lastRow - 1, 1).getValues();
for (var i = 0; i < allSelections.length; i++) {
if (allSelections[i][0] === dutyName && (i + 2) !== targetRow) {
return { success: false, message: '該職務已被其他老師選取' };
}
}
// 寫入已選職務(F欄)
sheet.getRange(targetRow, 6).setValue(dutyName);
// 順便寫入計算年資到 E 欄 (選擇性的,方便查看)
var teacherData = sheet.getRange(targetRow, 3, 1, 2).getValues()[0];
var calcScore = (teacherData[0] || 0) * 1 + (teacherData[1] || 0) * 0.5;
sheet.getRange(targetRow, 5).setValue(calcScore);
return { success: true, message: '選填成功' };
}
// 取消選填(清除該老師的職務)
function cancelSelection(teacherId) {
var ss = getSpreadsheet();
var sheet = ss.getSheetByName('老師資料');
if (!sheet) {
return { success: false, message: '找不到「老師資料」工作表' };
}
var lastRow = sheet.getLastRow();
var data = sheet.getRange(2, 1, lastRow - 1, 1).getValues();
var targetRow = -1;
for (var i = 0; i < data.length; i++) {
if (data[i][0] == teacherId) {
targetRow = i + 2;
break;
}
}
if (targetRow === -1) {
return { success: false, message: '找不到該老師' };
}
// 清除已選職務
sheet.getRange(targetRow, 6).clearContent();
return { success: true, message: '已取消選填' };
}
// 清除所有選填紀錄
function clearAllSelections() {
var ss = getSpreadsheet();
var sheet = ss.getSheetByName('老師資料');
if (!sheet) {
return { success: false, message: '找不到「老師資料」工作表' };
}
var lastRow = sheet.getLastRow();
if (lastRow < 2) {
return { success: true, message: '沒有資料需要清除' };
}
// 清除F欄(已選職務)
sheet.getRange(2, 6, lastRow - 1, 1).clearContent();
return { success: true, message: '已清除所有選填紀錄' };
}
// 匯出選填結果
function exportResults() {
var result = getAllData();
if (!result.success) {
return result;
}
var teachers = result.teachers;
var duties = result.duties;
// 建立職務對應表
var dutyMap = {};
for (var i = 0; i < duties.length; i++) {
var duty = duties[i];
if (duty.name) {
dutyMap[duty.name] = {
position: duty.position,
row: duty.displayRow,
col: duty.displayCol
};
}
}
// 整理選填結果
var exportData = [];
for (var i = 0; i < teachers.length; i++) {
var teacher = teachers[i];
var dutyInfo = teacher.selectedDuty ? dutyMap[teacher.selectedDuty] : null;
exportData.push({
排序: i + 1,
姓名: teacher.name,
本校年資: teacher.schoolYears,
總年資: teacher.totalYears,
計算年資: teacher.calculatedYears.toFixed(1),
已選職務: teacher.selectedDuty || '尚未選填',
職務位置: dutyInfo ? '第' + dutyInfo.row + '列 第' + dutyInfo.col + '欄' : '-'
});
}
return {
success: true,
data: exportData
};
} 貼到 Apps Script 的 Code.gs 檔案
5. 貼上前端(index.html)
把前端程式貼到 Apps Script 的 index.html。
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>老師職務選填系統 v2.0</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: "Microsoft JhengHei", "微軟正黑體", sans-serif; background: #f0f2f5; min-height: 100vh; padding: 20px; }
.container { max-width: 1400px; margin: 0 auto; background: white; border-radius: 15px; box-shadow: 0 10px 25px rgba(0,0,0,0.1); overflow: hidden; }
.header { background: #3b5998; color: white; padding: 20px; text-align: center; }
.header h1 { font-size: 28px; margin-bottom: 5px; }
.header p { font-size: 14px; opacity: 0.9; }
.content { padding: 20px; }
.section-title { font-size: 20px; color: #333; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid #3b5998; display: flex; justify-content: space-between; align-items: center; }
/* 老師列表樣式 */
.teacher-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; margin-bottom: 30px; max-height: 300px; overflow-y: auto; padding: 5px; border: 1px solid #ddd; border-radius: 5px;}
.teacher-card { background: #fff; padding: 10px; border-radius: 8px; border: 1px solid #ddd; cursor: pointer; position: relative; transition: 0.2s; }
.teacher-card:hover { border-color: #3b5998; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.teacher-card.selected { background: #3b5998; color: white; border-color: #3b5998; }
.teacher-rank { position: absolute; top: 5px; right: 5px; background: #ff9800; color: white; padding: 2px 6px; border-radius: 10px; font-size: 11px; }
.teacher-name { font-size: 16px; font-weight: bold; margin-bottom: 5px; }
.teacher-info { font-size: 12px; color: #666; }
.teacher-card.selected .teacher-info { color: #eee; }
.teacher-duty { margin-top: 5px; font-size: 12px; font-weight: bold; color: #2e7d32; background: #e8f5e9; padding: 3px; border-radius: 3px; text-align: center; }
/* 職務表格樣式 */
.duty-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; margin-bottom: 20px; }
.duty-cell { background: #fff; padding: 15px 5px; border-radius: 8px; border: 1px solid #ccc; text-align: center; min-height: 80px; display: flex; flex-direction: column; justify-content: center; align-items: center; cursor: pointer; transition: 0.2s; position: relative; }
.duty-cell:hover { transform: translateY(-2px); border-color: #3b5998; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
/* 狀態顏色 */
.duty-cell.fixed { background: #ffebee; border-color: #ef5350; cursor: not-allowed; } /* 固定職務 (紅) */
.duty-cell.occupied { background: #e0e0e0; border-color: #9e9e9e; cursor: not-allowed; opacity: 0.8; } /* 已被選 (灰) */
.duty-cell.selected { background: #e8f5e9; border: 2px solid #4caf50; } /* 當前選中 (綠) */
.duty-cell.empty { background: #f9f9f9; border: 1px dashed #ccc; cursor: default; }
.duty-name { font-weight: bold; font-size: 14px; line-height: 1.2; word-break: break-all; }
.duty-pos { font-size: 10px; color: #888; margin-top: 4px; }
.badge { position: absolute; font-size: 10px; padding: 2px 5px; border-radius: 4px; color: white; top: 2px; right: 2px; }
.badge-fixed { background: #ef5350; }
.badge-user { position: absolute; bottom: 2px; left: 50%; transform: translateX(-50%); background: #757575; color: white; font-size: 10px; padding: 1px 5px; border-radius: 4px; width: 90%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* 按鈕區 */
.button-group { display: flex; gap: 10px; flex-wrap: wrap; margin-left: auto; }
.btn { padding: 8px 16px; border: none; border-radius: 5px; font-size: 14px; cursor: pointer; color: white; transition: 0.2s; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-primary { background: #3b5998; }
.btn-success { background: #4caf50; }
.btn-danger { background: #ef5350; }
.btn-warning { background: #ff9800; }
.btn-info { background: #00bcd4; }
/* 訊息框 */
.message { padding: 10px; margin-bottom: 15px; border-radius: 5px; text-align: center; display: none; }
.message.success { background: #d4edda; color: #155724; }
.message.error { background: #f8d7da; color: #721c24; }
.loading { text-align: center; padding: 50px; color: #666; display: none;}
/* 統計資訊 */
.stats-bar { display: flex; gap: 15px; background: #e3f2fd; padding: 10px; border-radius: 8px; margin-bottom: 20px; font-size: 14px; flex-wrap: wrap;}
.stat-item strong { color: #1565c0; font-size: 16px; }
@media (max-width: 768px) {
.duty-grid { grid-template-columns: repeat(3, 1fr); }
.teacher-list { max-height: 200px; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎓 老師職務選填系統</h1>
<p>年資計算:本校年資×1 + 總年資×0.5 | 排序依據:計算年資 (高→低)</p>
</div>
<div class="content">
<div id="message" class="message"></div>
<div id="loading" class="loading">資料讀取中,請稍候...</div>
<div id="mainApp" style="display:none;">
<div class="stats-bar">
<div class="stat-item">總人數: <strong id="totalT">0</strong></div>
<div class="stat-item">已選填: <strong id="selectedT">0</strong></div>
<div class="stat-item">剩餘空缺: <strong id="remainD">0</strong></div>
</div>
<div class="section-title">
<span>1. 點選老師 (依年資排序)</span>
</div>
<div id="teacherList" class="teacher-list"></div>
<div class="section-title">
<span>2. 點選職務 (11列 x 5欄)</span>
<div class="button-group">
<button class="btn btn-success" id="confirmBtn" onclick="submitSelection()" disabled>✓ 確認選填</button>
<button class="btn btn-warning" id="cancelBtn" onclick="cancelMySelection()" disabled>✗ 取消選填</button>
<button class="btn btn-primary" onclick="initData()">🔄 重新整理</button>
<button class="btn btn-info" onclick="exportData()">📊 匯出結果</button>
<button class="btn btn-danger" onclick="clearAll()">🗑️ 清除全部</button>
</div>
</div>
<div id="dutyGrid" class="duty-grid"></div>
</div>
</div>
</div>
<script>
let teachers = [];
let duties = [];
let currentTeacherId = null;
let currentDutyPos = null;
window.onload = initData;
function initData() {
showLoading(true);
google.script.run.withSuccessHandler(renderApp).withFailureHandler(showError).getAllData();
}
function renderApp(result) {
showLoading(false);
if(!result.success) return showError(result.message);
teachers = result.teachers;
duties = result.duties;
updateStats();
renderTeachers();
renderDuties();
// 重置選取狀態
currentTeacherId = null;
currentDutyPos = null;
document.getElementById('confirmBtn').disabled = true;
document.getElementById('cancelBtn').disabled = true;
document.getElementById('mainApp').style.display = 'block';
}
function updateStats() {
document.getElementById('totalT').innerText = teachers.length;
document.getElementById('selectedT').innerText = teachers.filter(t => t.selectedDuty).length;
const occupied = teachers.map(t => t.selectedDuty).filter(d => d);
const available = duties.filter(d => d.name && !d.fixed && !occupied.includes(d.name)).length;
document.getElementById('remainD').innerText = available;
}
function renderTeachers() {
const list = document.getElementById('teacherList');
list.innerHTML = '';
teachers.forEach((t, idx) => {
const div = document.createElement('div');
div.className = `teacher-card ${t.selectedDuty ? 'done' : ''}`;
div.onclick = () => selectTeacher(t.id, div);
div.innerHTML = `
<div class="teacher-rank">#${idx + 1}</div>
<div class="teacher-name">${t.name}</div>
<div class="teacher-info">計算年資: ${t.calculatedYears.toFixed(1)}</div>
${t.selectedDuty ? `<div class="teacher-duty">已選: ${t.selectedDuty}</div>` : ''}
`;
list.appendChild(div);
});
}
function renderDuties() {
const grid = document.getElementById('dutyGrid');
grid.innerHTML = '';
const occupiedMap = {};
teachers.forEach(t => { if(t.selectedDuty) occupiedMap[t.selectedDuty] = t.name; });
duties.forEach(d => {
const div = document.createElement('div');
let classes = 'duty-cell';
let content = `<div class="duty-name">${d.name || ''}</div><div class="duty-pos">${d.displayRow}-${d.displayCol}</div>`;
if (!d.name) {
classes += ' empty';
} else if (d.fixed) {
classes += ' fixed';
content += `<span class="badge badge-fixed">固定</span>`;
} else if (occupiedMap[d.name]) {
classes += ' occupied';
content += `<div class="badge-user">${occupiedMap[d.name]}</div>`;
} else {
div.onclick = () => selectDuty(d.position, div);
}
div.className = classes;
div.innerHTML = content;
div.dataset.pos = d.position; // 標記位置
grid.appendChild(div);
});
}
function selectTeacher(id, el) {
currentTeacherId = id;
// 移除其他老師選取狀態
document.querySelectorAll('.teacher-card').forEach(c => c.classList.remove('selected'));
el.classList.add('selected');
// 檢查該老師是否已選
const teacher = teachers.find(t => t.id === id);
const cancelBtn = document.getElementById('cancelBtn');
if (teacher.selectedDuty) {
showMessage(`資訊:${teacher.name} 已經選擇了 ${teacher.selectedDuty}`, 'success');
cancelBtn.disabled = false;
document.getElementById('confirmBtn').disabled = true;
} else {
showMessage(`請選擇 ${teacher.name} 要擔任的職務`, 'success');
cancelBtn.disabled = true;
}
// 清除職務選取
currentDutyPos = null;
document.querySelectorAll('.duty-cell').forEach(c => c.classList.remove('selected'));
}
function selectDuty(pos, el) {
if (!currentTeacherId) {
alert('請先點選上方的老師!');
return;
}
const teacher = teachers.find(t => t.id === currentTeacherId);
if (teacher.selectedDuty) {
alert('這位老師已經選過職務了,請先「取消選填」才能重選。');
return;
}
currentDutyPos = pos;
document.querySelectorAll('.duty-cell').forEach(c => c.classList.remove('selected'));
el.classList.add('selected');
document.getElementById('confirmBtn').disabled = false;
}
function submitSelection() {
if(!currentTeacherId || !currentDutyPos) return;
const teacher = teachers.find(t => t.id === currentTeacherId);
const duty = duties.find(d => d.position === currentDutyPos);
if(!confirm(`確定要將「${teacher.name}」安排在「${duty.name}」嗎?`)) return;
showLoading(true);
google.script.run.withSuccessHandler((res) => {
if(res.success) {
alert('選填成功!');
initData();
} else {
showError(res.message);
showLoading(false);
}
}).saveSelection(teacher.id, duty.position, duty.name);
}
function cancelMySelection() {
if(!currentTeacherId) return;
const teacher = teachers.find(t => t.id === currentTeacherId);
if(!confirm(`確定要取消 ${teacher.name} 的職務嗎?`)) return;
showLoading(true);
google.script.run.withSuccessHandler((res) => {
if(res.success) {
alert('已取消!');
initData();
} else {
showError(res.message);
showLoading(false);
}
}).cancelSelection(teacher.id);
}
function clearAll() {
if(!confirm('危險!這將會清除「所有」老師的選填結果,確定嗎?')) return;
showLoading(true);
google.script.run.withSuccessHandler(() => {
alert('所有紀錄已清除');
initData();
}).clearAllSelections();
}
function exportData() {
showLoading(true);
google.script.run.withSuccessHandler((res) => {
showLoading(false);
if(res.success) {
let text = "排序\t姓名\t計算年資\t已選職務\t職務位置\n";
res.data.forEach(row => {
text += `${row.排序}\t${row.姓名}\t${row.計算年資}\t${row.已選職務}\t${row.職務位置}\n`;
});
// 下載檔案
const blob = new Blob([text], { type: 'text/plain' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = '老師選填結果.txt';
a.click();
}
}).exportResults();
}
function showLoading(isLoading) {
document.getElementById('loading').style.display = isLoading ? 'block' : 'none';
if(isLoading) document.getElementById('message').style.display = 'none';
}
function showMessage(msg, type) {
const el = document.getElementById('message');
el.innerText = msg;
el.className = `message ${type}`;
el.style.display = 'block';
}
function showError(msg) {
showMessage(msg, 'error');
alert('錯誤:' + msg);
}
</script>
</body>
</html>
貼到 Apps Script 的 index.html 檔案
6. 部署
部署成 Web App 後,就會拿到可分享網址。
- Apps Script 右上:部署 → 新部署
- 類型選:Web app
- 執行身分:你
- 存取權限:任何人
- 完成後複製網址
💬 評論區
老師們的交流與提問
登入 後即可發表評論