0. 作品介紹

作者 李信恆 大庄國小

解決因為填職位吵架的問題


28 1 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. 資料表規則

請照下面規則建立分頁與欄位。

第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 讀寫資料。

  1. 試算表上方選單:擴充功能 → Apps Script
  2. 刪掉預設的程式碼(或全部選取覆蓋)
  3. 保留/建立檔案:
    • Code.gs
    • index.html

4. 貼上後端(Code.gs)

把後端程式貼到 Apps Script 的 Code.gs。

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。

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 後,就會拿到可分享網址。

  1. Apps Script 右上:部署 → 新部署
  2. 類型選:Web app
  3. 執行身分:你
  4. 存取權限:任何人
  5. 完成後複製網址

💬 評論區

老師們的交流與提問

載入評論中...