0. 作品介紹

作者 施咅均 育賢國中

可協助老師分配座位


39 2 0
班級經營
封面圖 封面

1. AI 提示詞

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

我想要建立一個 【學生座位分配表】
幫我用Google 試算表建立 
我希望有些資料我可以直接在試算表更改首頁可以直接呈現
需求說明 【老師可選擇班級、8×8 座位欄位、有一區呈現全部同學方框、可拖曳同學進入固定座位、其餘隨機排入、學生拖曳方框需有該學生座號、點選要啟用座位後再開始排入學生,可選擇排入座位數同學生人數】
要把它變成程式碼我要發佈
請完整敘述操作步驟 越詳細越好
作業環境
Google試算表
Google Apps Script
請給我後端gs完整程式碼、前端index完整程式碼
網頁風格為可愛,網頁感覺如圖(可附圖給AI)

2. 資料表規則

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

第1分頁: 學生名單 A 欄: 班級 B 欄: 座號 C 欄: 姓名 第2分頁: 儲存的座位表 A 欄: 欄位A

📄 解析結果(自動表格)

分頁:學生名單
A 欄 B 欄 C 欄
班級 座號 姓名
分頁:儲存的座位表
A 欄
欄位A

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 getClassList() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('學生名單');
  // 防呆:如果沒有工作表
  if (!sheet) return [];
  
  var data = sheet.getRange(2, 1, sheet.getLastRow() - 1, 1).getValues();
  var classes = [...new Set(data.flat())].filter(String);
  return classes;
}

/**
 * 根據班級取得學生資料
 */
function getStudentsByClass(className) {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('學生名單');
  if (!sheet) return [];

  var data = sheet.getDataRange().getValues();
  var students = [];
  
  for (var i = 1; i < data.length; i++) {
    if (data[i][0] == className) {
      students.push({
        class: data[i][0],
        num: data[i][1],
        name: data[i][2]
      });
    }
  }
  // 依照座號排序
  students.sort(function(a, b) { return a.num - b.num; });
  return students;
}

/**
 * 儲存座位表
 */
function saveSeatLayoutToSheet(layoutData, className) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName('儲存的座位表');
  if (!sheet) {
    sheet = ss.insertSheet('儲存的座位表');
    sheet.appendRow(['時間', '班級', '座位配置(JSON)']);
  }
  
  var time = new Date();
  var jsonString = JSON.stringify(layoutData);
  sheet.appendRow([time, className, jsonString]);
  return "✨ 儲存成功!";
}

/**
 * 讀取該班級「最後一次」的存檔
 */
function loadLastLayout(className) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName('儲存的座位表');
  if (!sheet) return null; // 沒存過檔

  var data = sheet.getDataRange().getValues();
  // 從最後一行開始往回找
  for (var i = data.length - 1; i >= 1; i--) {
    // data[i][1] 是班級欄位
    if (data[i][1] == className) {
      return data[i][2]; // 回傳 JSON 字串
    }
  }
  return null; // 找不到該班級的紀錄
}
貼到 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=Zen+Maru+Gothic:wght@500;700&display=swap" rel="stylesheet">
  <style>
    :root {
      --bg-color: #fdf6e3;     /* 溫暖米色背景 */
      --panel-bg: #fff;        /* 純白面板 */
      --accent-color: #88d8b0; /* 薄荷綠 */
      --btn-color: #ffcc5c;    /* 奶油黃 */
      --btn-text: #5d4037;     /* 咖啡色文字 */
      --seat-inactive: #eee;   /* 未啟用座位 */
      --seat-active: #fff;     /* 啟用座位 */
      --text-color: #555;
    }

    body {
      font-family: 'Zen Maru Gothic', sans-serif;
      background-color: var(--bg-color);
      /* 點點背景 */
      background-image: radial-gradient(#d0d0d0 1px, transparent 1px);
      background-size: 20px 20px;
      color: var(--text-color);
      padding: 20px;
      margin: 0;
      display: flex;
      flex-direction: column;
      align-items: center;
    }

    h1 {
      color: #ff6f69;
      font-size: 2.2em;
      margin-bottom: 15px;
      letter-spacing: 2px;
      text-shadow: 2px 2px 0px #fff;
    }

    /* 控制列樣式 */
    .control-panel {
      background: var(--panel-bg);
      border-radius: 50px; /* 膠囊狀 */
      padding: 10px 30px;
      box-shadow: 0 4px 10px rgba(0,0,0,0.05);
      margin-bottom: 25px;
      display: flex;
      gap: 15px;
      align-items: center;
      flex-wrap: wrap;
      border: 3px solid #ffcc5c;
    }

    select {
      font-family: inherit;
      padding: 8px 15px;
      border-radius: 20px;
      border: 2px solid #ddd;
      outline: none;
      font-size: 1em;
      color: #666;
    }

    button {
      font-family: inherit;
      font-weight: bold;
      padding: 8px 20px;
      border-radius: 25px;
      border: none;
      cursor: pointer;
      transition: all 0.2s;
      color: var(--btn-text);
      box-shadow: 0 3px 0 rgba(0,0,0,0.1);
      font-size: 0.95em;
    }

    button:active { transform: translateY(3px); box-shadow: none; }

    .btn-reset { background: #e0e0e0; color: #666; }
    .btn-load { background: #88d8b0; color: #fff; }
    .btn-random { background: #ffcc5c; }
    .btn-save { background: #ff6f69; color: #fff; }

    /* 說明文字 */
    .instruction {
      background: rgba(255, 255, 255, 0.7);
      padding: 8px 20px;
      border-radius: 15px;
      margin-bottom: 10px;
      font-size: 0.9em;
      color: #777;
    }

    /* 主區域佈局 */
    .main-container {
      display: flex;
      flex-wrap: wrap;
      gap: 25px;
      justify-content: center;
      width: 100%;
      max-width: 1100px;
    }

    /* 學生名單區 */
    .student-pool-container {
      flex: 1;
      min-width: 200px;
      background: #fff;
      border-radius: 20px;
      padding: 20px;
      border: 2px dashed #b0e0e6; /* 淡藍色虛線 */
      min-height: 200px;
    }

    .section-title {
      text-align: center;
      margin-bottom: 15px;
      font-weight: bold;
      color: #88d8b0;
      font-size: 1.1em;
    }

    #studentPool {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
      align-content: flex-start;
    }

    /* 座位區 */
    .seating-area {
      flex: 3;
      min-width: 500px;
      background: #fff;
      border-radius: 30px;
      padding: 25px;
      box-shadow: 0 10px 25px rgba(0,0,0,0.05);
      border: 6px solid #ffecb3; /* 鵝黃色邊框 */
    }

    .blackboard {
      width: 50%;
      height: 30px;
      background: #5d4037;
      border-radius: 5px;
      margin: 0 auto 20px auto;
      display: flex;
      align-items: center;
      justify-content: center;
      color: white;
      font-size: 0.8em;
      box-shadow: 0 3px 0 #3e2723;
    }

    /* 座位網格 8x8 */
    .seat-grid {
      display: grid;
      grid-template-columns: repeat(8, 1fr);
      gap: 10px;
    }

    .seat {
      aspect-ratio: 1.1 / 1;
      background: var(--seat-inactive);
      border-radius: 12px;
      position: relative;
      cursor: pointer;
      transition: all 0.2s;
      display: flex;
      align-items: center;
      justify-content: center;
    }

    /* 啟用的座位 */
    .seat.active {
      background: var(--seat-active);
      border: 2px solid #88d8b0;
      box-shadow: inset 0 0 10px #f0fdf4;
    }
    
    .seat.active::after {
      content: ''; /* 把 OK 文字拿掉,改用顏色區分比較乾淨 */
      position: absolute;
      width: 8px;
      height: 8px;
      background: #88d8b0;
      border-radius: 50%;
      bottom: 5px;
      right: 5px;
      opacity: 0.5;
    }

    /* 學生卡片 */
    .student-card {
      width: 90%;
      height: 85%;
      background: #fff9c4; /* 淺黃便利貼 */
      border-radius: 10px;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      font-size: 0.85em;
      font-weight: bold;
      color: #555;
      cursor: grab;
      box-shadow: 2px 2px 5px rgba(0,0,0,0.1);
      user-select: none;
      border: 1px solid #f0f0f0;
      position: relative;
    }

    .student-card:active {
      cursor: grabbing;
      transform: scale(0.95);
    }

    .student-num {
      position: absolute;
      top: 3px;
      left: 3px;
      background: #ff8a65;
      color: white;
      font-size: 0.7em;
      width: 18px;
      height: 18px;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
    }

    /* 拖曳效果 */
    .seat.drag-over {
      background: #fff3e0;
      border: 2px dashed #ffb74d;
    }

    #statusMsg {
      color: #ff6f69;
      font-weight: bold;
      min-height: 20px;
    }

    #loading {
      color: #888;
      font-size: 0.9em;
      display: none;
    }

  </style>
</head>
<body>

  <h1>座位表</h1>

  <div class="control-panel">
    <select id="classSelect" onchange="handleClassChange()">
      <option value="">選擇班級...</option>
    </select>
    
    <button class="btn-load" onclick="loadSavedData()">📂 讀取上次存檔</button>
    <button class="btn-random" onclick="randomFill()">🎲 隨機排入</button>
    <button class="btn-reset" onclick="resetAll()">🗑️ 全部重置</button>
    <button class="btn-save" onclick="saveLayout()">💾 儲存</button>
  </div>

  <div id="loading">資料讀取中...</div>
  <div id="statusMsg"></div>
  <div class="instruction">
    操作:1. 選班級 ➜ 2. 點擊格子啟用座位 (變白框) 或「讀取存檔」 ➜ 3. 拖曳或隨機排入
  </div>

  <div class="main-container">
    
    <div class="student-pool-container">
      <div class="section-title">未分配同學</div>
      <div id="studentPool"></div>
    </div>

    <div class="seating-area">
      <div class="blackboard">講 台</div>
      <div class="seat-grid" id="seatGrid">
        </div>
    </div>

  </div>

  <script>
    var allStudents = [];
    var draggedStudent = null;

    window.onload = function() {
      createGrid();
      google.script.run.withSuccessHandler(populateClassSelect).getClassList();
    };

    function populateClassSelect(classes) {
      var select = document.getElementById('classSelect');
      classes.forEach(function(c) {
        var opt = document.createElement('option');
        opt.value = c;
        opt.innerText = c;
        select.appendChild(opt);
      });
    }

    function createGrid() {
      var grid = document.getElementById('seatGrid');
      grid.innerHTML = '';
      for (var i = 0; i < 64; i++) {
        var seat = document.createElement('div');
        seat.className = 'seat';
        seat.dataset.index = i;
        
        // 點擊切換座位啟用狀態
        seat.onclick = function() {
          if (this.children.length > 0) return; // 有人時不能關閉
          this.classList.toggle('active');
        };

        // 拖放事件
        seat.ondragover = function(e) {
          e.preventDefault();
          if (this.classList.contains('active')) {
            this.classList.add('drag-over');
            e.dataTransfer.dropEffect = 'move';
          }
        };
        seat.ondragleave = function() {
          this.classList.remove('drag-over');
        };
        seat.ondrop = handleDrop;

        grid.appendChild(seat);
      }
    }

    function handleClassChange() {
      var className = document.getElementById('classSelect').value;
      if (!className) return;
      
      showLoading(true);
      resetAll(true); // 切換班級時,保留格子狀態嗎?預設全部重置比較保險

      google.script.run.withSuccessHandler(function(students) {
        allStudents = students;
        renderStudentPool(students);
        showLoading(false);
        updateStatus(`已載入 ${students.length} 位同學`);
      }).getStudentsByClass(className);
    }

    // 讀取上次存檔
    function loadSavedData() {
      var className = document.getElementById('classSelect').value;
      if (!className) {
        alert("請先選擇班級!");
        return;
      }

      showLoading(true);
      
      google.script.run.withSuccessHandler(function(jsonString) {
        showLoading(false);
        if (!jsonString) {
          alert("這個班級還沒有存檔紀錄喔!");
          return;
        }
        
        var layoutData = JSON.parse(jsonString);
        applyLayout(layoutData);
        
      }).loadLastLayout(className);
    }

    // 將存檔應用到畫面上
    function applyLayout(layoutData) {
      // 1. 先把所有學生趕回池子 (確保狀態正確)
      var pool = document.getElementById('studentPool');
      var seats = document.querySelectorAll('.seat');
      
      seats.forEach(s => {
        if(s.firstChild) pool.appendChild(s.firstChild);
        s.classList.remove('active'); // 先全部關閉
      });

      // 2. 依照存檔配置
      layoutData.forEach(item => {
        var seat = seats[item.seatIndex];
        if (seat) {
          // 啟用座位
          seat.classList.add('active');
          
          // 找對應的學生卡片
          // 我們用 studentNum 來找 (假設座號唯一)
          // 這裡需要遍歷池子裡的卡片
          var cards = Array.from(pool.children);
          var targetCard = cards.find(card => card.dataset.num == item.studentNum);
          
          if (targetCard) {
            seat.appendChild(targetCard);
          }
        }
      });
      
      updateStatus("已還原上次座位紀錄!");
      sortPool(); // 剩下的同學整理一下順序
    }

    function renderStudentPool(students) {
      var pool = document.getElementById('studentPool');
      pool.innerHTML = '';
      students.forEach(function(s) {
        var card = createStudentCard(s);
        pool.appendChild(card);
      });
    }

    function createStudentCard(student) {
      var div = document.createElement('div');
      div.className = 'student-card';
      div.draggable = true;
      div.id = 'stu-' + student.num;
      div.dataset.num = student.num;
      div.dataset.name = student.name;
      
      div.innerHTML = `<span class="student-num">${student.num}</span><span>${student.name}</span>`;

      div.ondragstart = function(e) {
        draggedStudent = this;
        this.style.opacity = '0.5';
      };
      div.ondragend = function() {
        this.style.opacity = '1';
        draggedStudent = null;
      };

      return div;
    }

    function handleDrop(e) {
      e.stopPropagation();
      this.classList.remove('drag-over');

      if (!this.classList.contains('active')) {
        updateStatus("⚠️ 請先點擊格子啟用座位!");
        return;
      }

      // 交換功能:如果座位上有人,把他送回來源地 (或池子)
      if (this.children.length > 0) {
        var existing = this.children[0];
        document.getElementById('studentPool').appendChild(existing);
      }

      if (draggedStudent) {
        this.appendChild(draggedStudent);
      }
      sortPool();
    }

    function randomFill() {
      var pool = document.getElementById('studentPool');
      var unassigned = Array.from(pool.children);
      var emptySeats = Array.from(document.querySelectorAll('.seat.active')).filter(s => s.children.length === 0);

      if (unassigned.length === 0) {
        updateStatus("同學都坐好囉!");
        return;
      }

      // 洗牌
      unassigned.sort(() => Math.random() - 0.5);
      emptySeats.sort(() => Math.random() - 0.5);

      var count = Math.min(unassigned.length, emptySeats.length);
      for (var i = 0; i < count; i++) {
        emptySeats[i].appendChild(unassigned[i]);
      }
      sortPool();
    }

    function resetAll(forceClearSeats = false) {
      var pool = document.getElementById('studentPool');
      var seats = document.querySelectorAll('.seat');
      
      seats.forEach(seat => {
        if (seat.firstChild) pool.appendChild(seat.firstChild);
        if (forceClearSeats) seat.classList.remove('active');
      });
      sortPool();
      updateStatus("已重置");
    }

    function sortPool() {
      var pool = document.getElementById('studentPool');
      var cards = Array.from(pool.children);
      cards.sort((a, b) => a.dataset.num - b.dataset.num);
      cards.forEach(c => pool.appendChild(c));
    }

    function saveLayout() {
      var layout = [];
      var seats = document.querySelectorAll('.seat');
      var className = document.getElementById('classSelect').value;
      
      if (!className) {
        alert("請先選擇班級!");
        return;
      }

      seats.forEach((seat, index) => {
        if (seat.children.length > 0) {
          var card = seat.children[0];
          layout.push({
            seatIndex: index,
            studentNum: card.dataset.num,
            studentName: card.dataset.name
          });
        }
      });

      showLoading(true);
      google.script.run.withSuccessHandler(function(res) {
        showLoading(false);
        alert(res);
      }).saveSeatLayoutToSheet(layout, className);
    }

    function updateStatus(msg) {
      document.getElementById('statusMsg').innerText = msg;
    }

    function showLoading(show) {
      document.getElementById('loading').style.display = show ? 'block' : 'none';
    }
  </script>
</body>
</html>
貼到 Apps Script 的 index.html 檔案

6. 部署

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

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

💬 評論區

老師們的交流與提問

載入評論中...