0. 作品介紹

作者 王俊富 關東國小

給一張平面圖 自行建立學生位置 給班級名單 可隨機編排座位


44 1 0
班級經營
封面圖 封面

1. AI 提示詞

把這段指令貼給 AI,讓它幫你生成程式碼。

這是一份濃縮成 10 句話、完全不含程式碼語法的精華版提示詞,適合直接丟給 AI 生成最終成果:

1. 請製作一個現代風格的教室座位表產生器,啟動時先讓使用者上傳一張平面圖作為背景。
2. 系統必須嚴格區分「新增」、「選取」與「拖曳」三種互斥模式,確保在拖曳時絕不會誤觸勾選,選取時絕不會移動位置。
3. 座位外觀請設計成寬長方形(雙倍寬度)以容納長姓名,且只有在「選取模式」下,座位右上角才會顯示可供勾選的視覺提示(如圓圈)。
4. 提供垂直與水平對齊功能,計算已選座位的平均位置來進行排列。
5. **關鍵邏輯:執行對齊動作後,系統必須自動清除所有選取狀態,讓使用者能乾淨地進行下一次操作。**
6. 當編輯完成進入「排課模式」時,從後端讀取班級名單顯示於側邊欄。
7. 支援將學生名牌拖曳至座位,若座位已有人則自動交換或將舊生踢回待分配區,點擊座位亦可移除學生。
8. 加入「隨機補滿」按鈕,可一鍵將剩餘學生分配至空位。
9. 實作截圖功能,需在截圖瞬間自動隱藏所有工具列與按鈕,產出乾淨的座位表圖片。
10. 整體介面請採用圓角、陰影與柔和配色的現代化設計,並確保所有互動都有清晰的視覺回饋。

2. 資料表規則

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

第1分頁: ClassData A 欄: 班級名稱 B 欄: 學生名單

📄 解析結果(自動表格)

分頁:ClassData
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(後端)
// Code.gs
function doGet() {
  return HtmlService.createTemplateFromFile('index')
      .evaluate()
      .setTitle('電腦教室座位分配系統')
      .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
      .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

// 取得所有班級列表與學生名單
function getClassData() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('ClassData');
  var data = sheet.getDataRange().getValues();
  var classes = {};
  
  // 從第二列開始讀取 (跳過標題)
  for (var i = 1; i < data.length; i++) {
    var className = data[i][0];
    var studentString = data[i][1];
    
    if (className && studentString) {
      // 將逗號分隔的字串轉為陣列,並去除空白
      var students = studentString.toString().split(/[,,、]/).map(function(s) {
        return s.trim();
      }).filter(function(s) { return s !== "" }); // 過濾空值
      
      classes[className] = students;
    }
  }
  return classes;
}

// 包含 CSS 與 JS 的輔助函式
function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename)
      .getContent();
}// Code.gs
function doGet() {
  return HtmlService.createTemplateFromFile('index')
      .evaluate()
      .setTitle('電腦教室座位分配系統')
      .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
      .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

// 取得所有班級列表與學生名單
function getClassData() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('ClassData');
  var data = sheet.getDataRange().getValues();
  var classes = {};
  
  // 從第二列開始讀取 (跳過標題)
  for (var i = 1; i < data.length; i++) {
    var className = data[i][0];
    var studentString = data[i][1];
    
    if (className && studentString) {
      // 將逗號分隔的字串轉為陣列,並去除空白
      var students = studentString.toString().split(/[,,、]/).map(function(s) {
        return s.trim();
      }).filter(function(s) { return s !== "" }); // 過濾空值
      
      classes[className] = students;
    }
  }
  return classes;
}

// 包含 CSS 與 JS 的輔助函式
function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename)
      .getContent();
}
貼到 Apps Script 的 Code.gs 檔案

5. 貼上前端(index.html)

把前端程式貼到 Apps Script 的 index.html。

index.html(前端)
<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <link href="https://fonts.googleapis.com/css2?family=Varela+Round&display=swap" rel="stylesheet">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
  <style>
    :root {
      --bg-color: #2c3e50;
      --seat-empty: rgba(255, 255, 255, 0.95);
      --seat-border: #bdc3c7;
      --seat-occupied: #ffecb3;
      --accent-blue: #3498db;
      --accent-green: #2ecc71;
      --text-dark: #34495e;
    }

    body {
      font-family: 'Varela Round', sans-serif;
      background-color: var(--bg-color);
      color: white;
      margin: 0; padding: 20px;
      display: flex; flex-direction: column; align-items: center;
      min-height: 100vh; box-sizing: border-box;
      user-select: none;
    }

    /* --- 設定精靈 --- */
    #setupPanel {
      position: fixed; top: 0; left: 0; width: 100%; height: 100%;
      background: rgba(44, 62, 80, 0.98); z-index: 2000;
      display: flex; flex-direction: column; align-items: center; justify-content: center;
    }
    .step-box {
      background: white; color: var(--text-dark); padding: 30px;
      border-radius: 20px; text-align: center; max-width: 600px;
      box-shadow: 0 10px 25px rgba(0,0,0,0.5);
    }
    .hidden { display: none !important; }

    /* --- 控制面板 --- */
    .controls {
      background: rgba(255, 255, 255, 0.1); padding: 10px 20px;
      border-radius: 20px; display: flex; gap: 10px; align-items: center;
      margin-bottom: 15px; backdrop-filter: blur(5px); flex-wrap: wrap; justify-content: center;
      z-index: 100;
      box-shadow: 0 4px 10px rgba(0,0,0,0.2);
    }

    button, select {
      padding: 8px 12px; border-radius: 8px; border: none; font-size: 0.9rem;
      cursor: pointer; transition: 0.2s; display: flex; align-items: center; gap: 5px;
    }
    button:hover { filter: brightness(1.1); transform: translateY(-1px); }
    button:active { transform: translateY(1px); }
    
    .btn-tool { background: #546e7a; color: white; }
    .btn-tool.active { background: #ff7043; box-shadow: 0 0 8px #ff7043; font-weight: bold; transform: scale(1.05); }
    
    .btn-action { background: var(--accent-blue); color: white; }
    .btn-success { background: var(--accent-green); color: white; font-weight: bold; }
    .btn-danger { background: #e74c3c; color: white; }
    .btn-screenshot { background: #f1c40f; color: var(--text-dark); font-weight: bold; }

    /* --- 工作區 --- */
    .workspace { display: flex; width: 100%; height: 85vh; gap: 20px; }

    .student-pool-container {
      flex: 1; background: rgba(255,255,255,0.95); border-radius: 20px; padding: 15px;
      color: var(--text-dark); display: flex; flex-direction: column; max-width: 250px;
      box-shadow: 0 5px 15px rgba(0,0,0,0.3);
    }
    .student-pool { flex: 1; overflow-y: auto; display: flex; flex-wrap: wrap; align-content: flex-start; gap: 8px; }
    .student-card {
      background: white; border: 2px solid var(--accent-blue); border-radius: 10px;
      padding: 5px 10px; font-size: 0.9rem; cursor: grab;
    }

    /* 地圖區域 */
    .map-container {
      flex: 4; position: relative; 
      background: #2c3e50; 
      border: 4px solid #34495e;
      border-radius: 20px; 
      overflow: hidden; 
      box-shadow: 0 10px 30px rgba(0,0,0,0.5);
    }
    
    #mapImage { 
      position: absolute; top: 0; left: 0;
      width: 100%; height: 100%; 
      object-fit: fill; 
      pointer-events: none;
    }

    /* --- 座位樣式 --- */
    .seat {
      position: absolute; 
      width: 100px; /* 寬度維持兩倍 */
      height: 50px;
      background: var(--seat-empty); 
      border: 2px solid var(--seat-border);
      border-radius: 8px; display: flex; flex-direction: column;
      justify-content: center; align-items: center;
      transform: translate(-50%, -50%);
      box-shadow: 0 4px 6px rgba(0,0,0,0.2); z-index: 10;
      transition: left 0.2s, top 0.2s, transform 0.1s; 
    }
    
    .seat-number { font-size: 1.1rem; font-weight: bold; color: var(--text-dark); pointer-events: none; margin-bottom: 2px; }
    .seat-student { 
        font-size: 0.9rem; 
        color: #d35400; 
        font-weight: bold; 
        text-align: center; 
        width: 100%; 
        line-height: 1.1; 
        white-space: nowrap; 
        overflow: hidden; 
        text-overflow: ellipsis; 
        display: none; 
        pointer-events: none;
    }
    
    .seat.occupied { background: var(--seat-occupied); border-color: #f39c12; }
    .seat.occupied .seat-number { font-size: 0.8rem; opacity: 0.6; }
    .seat.occupied .seat-student { display: block; }

    /* --- 勾選框 UI --- */
    .checkbox-icon {
      position: absolute; top: -10px; right: -10px;
      width: 22px; height: 22px;
      background: #fff; 
      border: 2px solid #bdc3c7; 
      border-radius: 50%;
      display: none; 
      justify-content: center; align-items: center;
      font-size: 14px; color: white;
      box-shadow: 0 2px 5px rgba(0,0,0,0.2);
      z-index: 25;
      pointer-events: none; 
    }
    
    body.tool-select .checkbox-icon { display: flex; }

    /* --- 選取狀態 --- */
    .seat.selected {
      border: 3px solid var(--accent-green) !important;
      background-color: #e8f5e9 !important;
      transform: translate(-50%, -50%) scale(1.05);
      z-index: 30; 
    }
    
    .seat.selected .checkbox-icon {
      background: var(--accent-green);
      border-color: var(--accent-green);
    }
    .seat.selected .checkbox-icon::after {
      content: '✔';
    }

    /* --- 游標控制 --- */
    body.tool-add .map-container { cursor: crosshair; }
    body.tool-select .seat { cursor: pointer; } 
    body.tool-select .map-container { cursor: default; }
    body.tool-drag .seat { cursor: move; }
    body.tool-drag .seat:active { cursor: grabbing; }

    .divider { width: 1px; height: 20px; background: rgba(255,255,255,0.3); margin: 0 5px; }
  </style>
</head>
<body class="edit-mode tool-add">

  <div id="setupPanel">
    <div class="step-box">
      <h1>📸 建立教室座位表</h1>
      <p>請上傳教室平面圖。</p>
      <input type="file" id="fileInput" accept="image/*" onchange="handleImageUpload(event)">
      <button class="btn-success" id="startBtn" disabled onclick="closeSetup()">使用此圖片</button>
    </div>
  </div>

  <div class="controls" id="topControls">
    <div id="editTools" style="display:flex; gap:10px; align-items:center;">
      <button class="btn-tool active" id="toolAdd" onclick="setTool('add')">➕ 新增</button>
      <button class="btn-tool" id="toolSelect" onclick="setTool('select')">✋ 選取模式</button>
      <button class="btn-tool" id="toolDrag" onclick="setTool('drag')">✥ 拖曳模式</button>
      <div class="divider"></div>
      <button class="btn-action" onclick="alignSeats('v')">↕️ 垂直對齊(直)</button>
      <button class="btn-action" onclick="alignSeats('h')">↔️ 水平對齊(橫)</button>
      <button class="btn-danger" onclick="deleteSelected()">🗑️ 刪除勾選</button>
      <div class="divider"></div>
      <button class="btn-success" onclick="finishEditing()">✅ 完成設定</button>
    </div>

    <div id="assignTools" class="hidden" style="display:flex; gap:10px; align-items:center;">
      <select id="classSelect" onchange="loadClassStudents()">
        <option value="" disabled selected>請選擇班級</option>
      </select>
      <button class="btn-action" onclick="resetStudents()">重置</button>
      <button class="btn-tool" onclick="randomizeRemaining()">🎲 隨機補滿</button>
      <button class="btn-screenshot" onclick="downloadScreenshot()">📸 下載截圖</button>
      <button class="btn-action" onclick="backToEdit()">✏️ 修改座位</button>
    </div>
  </div>

  <div class="workspace">
    <div class="student-pool-container">
      <div style="font-weight:bold; margin-bottom:10px; text-align:center;">待分配 (<span id="poolCount">0</span>)</div>
      <div id="studentPool" class="student-pool" ondrop="dropToPool(event)" ondragover="allowDrop(event)"></div>
    </div>

    <div class="map-container" id="mapContainer" onclick="handleMapClick(event)">
      <img id="mapImage" src="" alt="請上傳地圖">
      </div>
  </div>

  <script>
    const DEFAULT_MAP_IMAGE = ""; 

    let allClassData = {};
    let seatLayout = []; 
    let assignedData = {}; 
    let currentStudents = [];
    
    let isEditMode = true;
    let currentTool = 'add'; 
    let selectedSeatIds = new Set();
    
    let isDragging = false;
    let dragItem = null;
    let startX, startY;
    let initialLeft, initialTop;

    window.onload = function() {
      if (DEFAULT_MAP_IMAGE && DEFAULT_MAP_IMAGE.trim() !== "") {
        document.getElementById('mapImage').src = DEFAULT_MAP_IMAGE;
        document.getElementById('setupPanel').classList.add('hidden');
      }

      google.script.run.withSuccessHandler(data => {
        allClassData = data;
        const select = document.getElementById('classSelect');
        for (let cls in data) {
          let opt = document.createElement('option');
          opt.value = cls; opt.text = cls; select.add(opt);
        }
      }).getClassData();
      
      setTool('add');
    };

    function handleImageUpload(event) {
      const file = event.target.files[0];
      if (file) {
        const reader = new FileReader();
        reader.onload = function(e) {
          document.getElementById('mapImage').src = e.target.result;
          document.getElementById('startBtn').disabled = false;
        }
        reader.readAsDataURL(file);
      }
    }
    function closeSetup() { document.getElementById('setupPanel').classList.add('hidden'); }

    // --- 工具切換 ---
    function setTool(mode) {
      currentTool = mode;
      
      document.querySelectorAll('.btn-tool').forEach(b => b.classList.remove('active'));
      if(mode === 'add') document.getElementById('toolAdd').classList.add('active');
      if(mode === 'select') document.getElementById('toolSelect').classList.add('active');
      if(mode === 'drag') document.getElementById('toolDrag').classList.add('active');

      document.body.classList.remove('tool-add', 'tool-select', 'tool-drag');
      document.body.classList.add('tool-' + mode);
      
      clearSelection();
    }

    function getNextAvailableId() {
        let id = 1;
        while (seatLayout.find(s => s.id === id)) { id++; }
        return id;
    }

    // --- 地圖互動 ---
    function handleMapClick(e) {
      if (!isEditMode) return;
      const isBackground = e.target.id === 'mapContainer' || e.target.id === 'mapImage';
      
      if (currentTool === 'add' && isBackground) {
        const container = document.getElementById('mapContainer');
        const rect = container.getBoundingClientRect();
        const x = ((e.clientX - rect.left) / rect.width) * 100;
        const y = ((e.clientY - rect.top) / rect.height) * 100;
        createSeat(getNextAvailableId(), x, y);

      } else if (currentTool === 'select' && isBackground) {
        clearSelection();
      }
    }

    function createSeat(id, x, y) {
      const seat = document.createElement('div');
      seat.className = 'seat';
      seat.id = 'seat-' + id;
      seat.style.left = x + '%';
      seat.style.top = y + '%';
      seatLayout.push({id: id, x: x, y: y});

      seat.innerHTML = `
        <div class="checkbox-icon"></div>
        <div class="seat-number">${id}</div>
        <div class="seat-student" id="stu-seat-${id}"></div>
      `;
      
      seat.onclick = (e) => handleSeatClick(e, id);
      seat.onmousedown = (e) => handleSeatMouseDown(e, id);
      seat.ondragover = allowDrop;
      seat.ondrop = (e) => dropToSeat(e, id);
      document.getElementById('mapContainer').appendChild(seat);
    }

    // --- 點擊:選取/排課踢人 ---
    function handleSeatClick(e, id) {
      if (!isEditMode) {
          if (assignedData[id]) { addStudentToPool(assignedData[id]); clearSeat(id); }
          return;
      }
      if (currentTool === 'select') {
        e.stopPropagation(); 
        toggleSelection(id);
      }
    }

    // --- 拖曳:移動座位 ---
    function handleSeatMouseDown(e, id) {
        if (!isEditMode) return;
        if (currentTool !== 'drag') return;
        if (e.button !== 0) return; 
        
        e.stopPropagation();
        
        isDragging = true;
        dragItem = document.getElementById('seat-' + id);
        dragItem.style.transition = 'none'; 
        
        startX = e.clientX;
        startY = e.clientY;
        initialLeft = parseFloat(dragItem.style.left);
        initialTop = parseFloat(dragItem.style.top);
        
        document.addEventListener('mousemove', onMouseMove);
        document.addEventListener('mouseup', (ev) => onMouseUp(ev, id));
    }

    function onMouseMove(e) {
        if (!isDragging || !dragItem) return;

        const dx = e.clientX - startX;
        const dy = e.clientY - startY;
        const container = document.getElementById('mapContainer');
        const rect = container.getBoundingClientRect();
        
        const newX = initialLeft + (dx / rect.width) * 100;
        const newY = initialTop + (dy / rect.height) * 100;
        
        dragItem.style.left = newX + '%';
        dragItem.style.top = newY + '%';
    }

    function onMouseUp(e, id) {
        document.removeEventListener('mousemove', onMouseMove);
        e.target.removeEventListener('mouseup', onMouseUp); 
        
        if (isDragging && dragItem) {
            dragItem.style.transition = 'left 0.2s, top 0.2s, transform 0.1s';
            const seat = seatLayout.find(s => s.id === id);
            if (seat) {
                seat.x = parseFloat(dragItem.style.left);
                seat.y = parseFloat(dragItem.style.top);
            }
        }
        isDragging = false;
        dragItem = null;
    }

    // --- 選取功能 ---
    function toggleSelection(id) {
      const el = document.getElementById('seat-' + id);
      if (!el) return;

      if (selectedSeatIds.has(id)) {
        selectedSeatIds.delete(id);
        el.classList.remove('selected');
      } else {
        selectedSeatIds.add(id);
        el.classList.add('selected');
      }
    }

    function clearSelection() {
      selectedSeatIds.forEach(id => {
        const el = document.getElementById('seat-' + id);
        if(el) el.classList.remove('selected');
      });
      selectedSeatIds.clear();
    }

    // --- 對齊功能 (修正點:對齊後自動清空選取) ---
    function alignSeats(direction) {
      if (currentTool !== 'select') { 
          alert('請先切換至「✋ 選取模式」並勾選要對齊的座位'); 
          setTool('select');
          return; 
      }
      if (selectedSeatIds.size < 2) { alert('請勾選至少兩個座位進行對齊'); return; }
      
      let sum = 0; 
      let targets = [];

      selectedSeatIds.forEach(id => {
        const seat = seatLayout.find(s => s.id === id);
        if (seat) {
          sum += (direction === 'v' ? seat.x : seat.y);
          targets.push(seat);
        }
      });
      
      if (targets.length === 0) return;
      const average = sum / targets.length;

      targets.forEach(seat => {
        const el = document.getElementById('seat-' + seat.id);
        if (direction === 'v') {
          seat.x = average;
          if(el) el.style.left = average + '%';
        } else {
          seat.y = average;
          if(el) el.style.top = average + '%';
        }
      });

      // 關鍵修正:對齊動作完成後,自動清除選取
      // 這樣使用者進行下一次對齊時,不會被舊的選取影響
      clearSelection();
    }

    function deleteSelected() {
      if (selectedSeatIds.size === 0) { alert('請切換至「選取模式」並勾選要刪除的座位'); return; }
      if (!confirm(`確定刪除這 ${selectedSeatIds.size} 個座位?`)) return;

      const idsToDelete = Array.from(selectedSeatIds);
      idsToDelete.forEach(id => {
        const el = document.getElementById('seat-' + id);
        if (el) el.remove();
        const idx = seatLayout.findIndex(s => s.id === id);
        if (idx > -1) seatLayout.splice(idx, 1);
        delete assignedData[id];
      });
      selectedSeatIds.clear();
    }

    // --- 流程控制 ---
    function finishEditing() {
      if (seatLayout.length === 0) { alert('請至少新增一個座位'); return; }
      isEditMode = false;
      clearSelection();
      document.body.classList.remove('edit-mode');
      document.body.classList.remove('tool-add', 'tool-select', 'tool-drag');
      document.getElementById('editTools').classList.add('hidden');
      document.getElementById('assignTools').classList.remove('hidden');
    }

    function backToEdit() {
      isEditMode = true;
      document.body.classList.add('edit-mode');
      setTool('select');
      document.getElementById('editTools').classList.remove('hidden');
      document.getElementById('assignTools').classList.add('hidden');
    }

    // --- 排課邏輯 ---
    function loadClassStudents() {
      const cls = document.getElementById('classSelect').value;
      currentStudents = allClassData[cls] ? allClassData[cls].slice() : [];
      resetStudents();
    }
    function resetStudents() {
      assignedData = {};
      document.querySelectorAll('.seat-student').forEach(el => el.innerText = '');
      document.querySelectorAll('.seat').forEach(el => el.classList.remove('occupied'));
      renderPool(currentStudents);
    }
    function renderPool(students) {
      const pool = document.getElementById('studentPool');
      pool.innerHTML = '';
      students.forEach(stu => {
        let card = document.createElement('div');
        card.className = 'student-card';
        card.draggable = true;
        card.id = 'card-' + stu;
        card.innerText = stu;
        card.ondragstart = (e) => e.dataTransfer.setData("text", stu);
        pool.appendChild(card);
      });
      document.getElementById('poolCount').innerText = students.length;
    }
    function allowDrop(ev) { ev.preventDefault(); }
    function dropToSeat(ev, seatId) {
      ev.preventDefault();
      if (isEditMode) return;
      const studentName = ev.dataTransfer.getData("text");
      if (assignedData[seatId]) addStudentToPool(assignedData[seatId]);
      assignStudentToSeat(studentName, seatId);
      removeStudentFromPoolUI(studentName);
      for(let sid in assignedData) {
        if(assignedData[sid] === studentName && parseInt(sid) !== seatId) clearSeat(sid);
      }
      assignedData[seatId] = studentName;
    }
    function dropToPool(ev) {
        ev.preventDefault();
        const studentName = ev.dataTransfer.getData("text");
        let currentSeatId = null;
        for(let sid in assignedData) {
            if(assignedData[sid] === studentName) { currentSeatId = sid; break; }
        }
        if (currentSeatId) { clearSeat(currentSeatId); addStudentToPool(studentName); }
    }
    function assignStudentToSeat(name, seatId) {
      document.getElementById('stu-seat-' + seatId).innerText = name;
      document.getElementById('seat-' + seatId).classList.add('occupied');
      assignedData[seatId] = name;
    }
    function clearSeat(seatId) {
      document.getElementById('stu-seat-' + seatId).innerText = '';
      document.getElementById('seat-' + seatId).classList.remove('occupied');
      delete assignedData[seatId];
    }
    function addStudentToPool(name) {
       if (document.getElementById('card-' + name)) return;
       const pool = document.getElementById('studentPool');
       let card = document.createElement('div');
       card.className = 'student-card';
       card.draggable = true;
       card.id = 'card-' + name;
       card.innerText = name;
       card.ondragstart = (e) => e.dataTransfer.setData("text", name);
       pool.appendChild(card);
       document.getElementById('poolCount').innerText = pool.childElementCount;
    }
    function removeStudentFromPoolUI(name) {
       const card = document.getElementById('card-' + name);
       if(card) card.remove();
       document.getElementById('poolCount').innerText = document.getElementById('studentPool').childElementCount;
    }
    function randomizeRemaining() {
      let available = [];
      seatLayout.forEach(pos => { if (!assignedData[pos.id]) available.push(pos.id); });
      let pool = [];
      document.querySelectorAll('#studentPool .student-card').forEach(c => pool.push(c.innerText));
      if (pool.length === 0) { alert('無學生可排'); return; }
      if (available.length < pool.length) { alert('座位不足'); return; }
      pool.sort(() => Math.random() - 0.5);
      pool.forEach((stu, i) => { assignStudentToSeat(stu, available[i]); removeStudentFromPoolUI(stu); });
    }
    function downloadScreenshot() {
      const btn = event.target;
      const controls = document.getElementById('topControls');
      const originalText = btn.innerText;
      btn.innerText = '📷';
      controls.classList.add('hidden');
      html2canvas(document.body, { backgroundColor: '#2c3e50', scale: 2, ignoreElements: (el) => el.id === 'setupPanel' }).then(canvas => {
        controls.classList.remove('hidden');
        btn.innerText = originalText;
        const link = document.createElement('a');
        link.download = `座位表_${new Date().getTime()}.png`;
        link.href = canvas.toDataURL("image/png");
        link.click();
      });
    }
  </script>
</body>
</html>
貼到 Apps Script 的 index.html 檔案

6. 部署

部署成 Web App 後,就會拿到可分享網址。

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

💬 評論區

老師們的交流與提問

載入評論中...