0. 總覽與需求

目標:用 Google 試算表「資料收集」,用 Apps Script 發佈成教學工具網站。

成果示意圖

完整教學影片

完整教學影片(約 1 小時)・從 AI 工具介紹到作品上傳,一次學完整個流程

互動式 Prompt 產生器

填寫你的需求,系統幫你組出完整 AI 指令,直接複製貼給 Claude / ChatGPT。

自動產生的 AI 指令
填寫上方欄位,指令會在這裡自動產生

1. 建立試算表與分頁

先準備資料結構,後端程式會依分頁名稱讀取。

建立分頁示意
  1. 開啟 Google 試算表 → 新增空白試算表
  2. 底下建立 2 個分頁(工作表):
    • 學生資料
    • 座位表

2. 建立資料分頁與欄位結構

本範例會使用 兩個試算表分頁,欄位位置需完全一致,請依下列說明建立。

📄 分頁一:學生資料

A 欄B 欄
學號姓名
說明:
• 分頁名稱請命名為 學生資料
• 程式會從 第 2 列 開始讀取學生資料(第 1 列為標題)

📄 分頁二:座位表

A 欄B 欄C 欄
學號
說明:
• 分頁名稱請命名為 座位表
• A 欄:座位列(row)
• B 欄:座位欄(col)
• C 欄:對應的學生學號(需與「學生資料」分頁一致)

3. 開啟 Apps Script

從試算表開 Apps Script,才能直接用 SpreadsheetApp 讀寫資料。

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

4. 貼上後端(Code.gs)

負責讀取「學生資料」、讀取/儲存「座位表」、以及輸出網頁。

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)

負責座位格呈現、點選/放置/隨機、以及呼叫後端儲存。

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

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

7. 驗證:改資料庫看前端會不會變

改「學生資料」分頁,再刷新座位表頁面,確認是否同步。

  • 新增學生後:前端「未分配學生」會出現
  • 儲存座位後:試算表「座位表」分頁會更新

8. 下指令修飾前端(提示詞範例)

你可以把需求丟給 AI 產生更漂亮的 UI,但保留原功能。

UI 參考
提示詞範例
我現在的版面不好看。
需求說明:【把座位表改成更科幻的,學生選座位時會很開心的風格】

9. 大功告成

你已經完成「試算表當資料庫 → Web App 前端操作 → 儲存座位配置」。

下一步你可以做:
  • 改成支援拖曳(Drag & Drop)
  • 支援固定幾位學生、剩餘再隨機
  • 加入座號、分組、特殊座位區