0. 作品介紹

作者 羅婉柔 西門國小

適合座位的安排及上課時的個人、小組加分


37 1 0
封面圖 封面

2. 資料表規則

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

第1分頁: Students A 欄: 建立一個新的 Google 試算表。 B 欄: 將工作表(分頁)名稱更改為 Students。 C 欄: 在第一行建立以下標題: D 欄: A1: 座號 | B1: 姓名 | C1: 小組 | D1: 個人分數 | E1: 頭像網址 E 欄: 填入資料:請至少填入 30 位學生的資料(1-30 號)。「小組」欄位請填入 1~6(代表第幾組)。

📄 解析結果(自動表格)

分頁:Students
A 欄 B 欄 C 欄 D 欄 E 欄
建立一個新的 Google 試算表。 將工作表(分頁)名稱更改為 Students。 在第一行建立以下標題: A1: 座號 | B1: 姓名 | C1: 小組 | D1: 個人分數 | E1: 頭像網址 填入資料:請至少填入 30 位學生的資料(1-30 號)。「小組」欄位請填入 1~6(代表第幾組)。

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('溫馨班級座位管理系統')
    .addMetaTag('viewport', 'width=device-width, initial-scale=1')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

// 取得學生名單
function getStudentData() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('Students');
  const data = sheet.getDataRange().getValues();
  data.shift(); // 移除標題
  
  return data.map(row => ({
    id: row[0].toString(),
    name: row[1],
    group: row[2],
    score: row[3],
    img: row[4] || 'https://cdn-icons-png.flaticon.com/512/194/194931.png'
  }));
}

// 更新分數
function updateScore(studentId, addScore) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('Students');
  const data = sheet.getRange("A:A").getValues();
  
  for (let i = 1; i < data.length; i++) {
    if (data[i][0].toString() == studentId) {
      const cell = sheet.getRange(i + 1, 4);
      const newScore = Number(cell.getValue()) + Number(addScore);
      cell.setValue(newScore);
      return { success: true, newScore: newScore };
    }
  }
}

// 儲存目前的座位順序到試算表隱藏欄位或 ScriptProperties
function saveSeating(orderArray) {
  PropertiesService.getScriptProperties().setProperty('savedOrder', JSON.stringify(orderArray));
  return "已儲存座位!";
}

function getSavedSeating() {
  return JSON.parse(PropertiesService.getScriptProperties().getProperty('savedOrder') || "[]");
}/** Code.gs 額外新增的部分 **/

// 清除所有學生的分數
function resetAllScores() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('Students');
  const lastRow = sheet.getLastRow();
  
  if (lastRow > 1) {
    // 假設分數在第 4 欄 (D 欄),從第 2 列開始全部設為 0
    const scoreRange = sheet.getRange(2, 4, lastRow - 1, 1);
    scoreRange.setValue(0);
    return "分數已全部歸零!";
  }
  return "目前沒有學生資料。";
}
貼到 Apps Script 的 Code.gs 檔案

5. 貼上前端(index.html)

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

index.html(前端)
<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
  <style>
    :root {
      --primary: #ffb6c1; --secondary: #fff0f5; --accent: #87ceeb;
    }
    body { font-family: 'Comic Sans MS', 'Microsoft JhengHei', sans-serif; background: var(--secondary); padding: 20px; }
    
    /* 頂部看板 */
    .dashboard { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; background: white; padding: 15px; border-radius: 25px; box-shadow: 0 5px 15px rgba(0,0,0,0.1); margin-bottom: 20px; }
    .rank-box { padding: 10px; border: 2px solid var(--secondary); border-radius: 15px; }
    h2 { color: #db7093; font-size: 1.2rem; margin-top: 0; }

    /* 控制列 */
    .toolbar { display: flex; justify-content: center; gap: 10px; margin-bottom: 20px; }
    button { background: var(--primary); color: white; border: none; padding: 10px 20px; border-radius: 50px; cursor: pointer; font-weight: bold; transition: 0.3s; }
    button:hover { background: #ff69b4; transform: translateY(-2px); }
    .save-btn { background: var(--accent); }

    /* 5x6 座位 */
    .grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; max-width: 900px; margin: 0 auto; }
    .seat-slot { background: white; border: 3px dashed var(--primary); border-radius: 15px; padding: 10px; min-height: 110px; position: relative; }
    
    .student-card { cursor: pointer; text-align: center; }
    .student-card img { width: 45px; height: 45px; border-radius: 50%; border: 2px solid var(--primary); }
    .name-tag { font-weight: bold; font-size: 14px; margin: 5px 0; display: block; }
    .score-tag { font-size: 12px; background: #fffacd; padding: 2px 8px; border-radius: 10px; }

    /* 下拉選單樣式 */
    select { width: 100%; margin-top: 5px; border-radius: 5px; border: 1px solid #ccc; font-size: 12px; }

    /* 彈窗 */
    .modal { display:none; position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); background:white; padding:25px; border-radius:20px; box-shadow:0 10px 30px rgba(0,0,0,0.2); z-index:100; }
    .overlay { display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.4); z-index:99; }
  </style>
</head>
<body>

<div class="dashboard">
  <div class="rank-box"><h2>🏆 個人排行</h2><div id="top-3"></div></div>
  <div class="rank-box"><h2>👥 小組排行</h2><div id="group-rank"></div></div>
</div>

<div class="toolbar">
  <button onclick="randomShuffle()"><i class="fas fa-random"></i> 全部隨機</button>
  <button onclick="clearSeats()"><i class="fas fa-eraser"></i> 清空座位</button>
  <button style="background: #ff4d4d;" onclick="resetScores()"><i class="fas fa-trash-alt"></i> 清除分數</button>
  <button class="save-btn" onclick="saveToCloud()"><i class="fas fa-cloud-upload-alt"></i> 儲存佈局</button>
</div>

<div class="grid" id="grid"></div>

<div class="overlay" id="overlay" onclick="closeModal()"></div>
<div class="modal" id="modal">
  <h3 id="m-name">姓名</h3>
  <input type="number" id="m-score" value="1" style="width:80px; font-size:20px; text-align:center;">
  <p>請輸入加分分值</p>
  <button onclick="applyScore()">確認加分</button>
</div>

<script>
  let students = []; // 原始學生資料
  let currentOrder = Array(30).fill(""); // 目前 30 個座位的學生 ID

  window.onload = () => {
    google.script.run.withSuccessHandler(data => {
      students = data;
      google.script.run.withSuccessHandler(saved => {
        if(saved && saved.length > 0) currentOrder = saved;
        render();
      }).getSavedSeating();
    }).getStudentData();
  };

  function render() {
    const grid = document.getElementById('grid');
    grid.innerHTML = '';
    
    // 找出目前哪些學生還沒被選 (用於下拉選單)
    const usedIds = currentOrder.filter(id => id !== "");

    for (let i = 0; i < 30; i++) {
      const slot = document.createElement('div');
      slot.className = 'seat-slot';
      const studentId = currentOrder[i];
      const student = students.find(s => s.id == studentId);

      if (student) {
        slot.innerHTML = `
          <div class="student-card" onclick="openModal('${student.id}', '${student.name}')">
            <img src="${student.img}">
            <span class="name-tag">${student.name}</span>
            <span class="score-tag">${student.score} 分</span>
          </div>
          <button style="position:absolute; bottom:5px; right:5px; padding:2px 5px; font-size:10px;" onclick="removeStudent(${i})">撤換</button>
        `;
      } else {
        // 顯示選單
        let options = `<option value="">--挑選學生--</option>`;
        students.forEach(s => {
          if (!usedIds.includes(s.id)) {
            options += `<option value="${s.id}">${s.id}. ${s.name}</option>`;
          }
        });
        slot.innerHTML = `<select onchange="assignStudent(${i}, this.value)">${options}</select>`;
      }
      grid.appendChild(slot);
    }
    updateRankings();
  }

  function assignStudent(index, id) {
    currentOrder[index] = id;
    render();
  }

  function removeStudent(index) {
    currentOrder[index] = "";
    render();
  }

  function randomShuffle() {
    let ids = students.map(s => s.id);
    // 洗牌
    for (let i = ids.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [ids[i], ids[j]] = [ids[j], ids[i]];
    }
    // 填入前 30 名
    currentOrder = Array(30).fill("").map((_, i) => ids[i] || "");
    render();
  }

  function clearSeats() {
    if(confirm("確定要清空目前座位重新安排嗎?")) {
      currentOrder = Array(30).fill("");
      render();
    }
  }

  function saveToCloud() {
    google.script.run.withSuccessHandler(() => alert("座位表已同步到雲端!")).saveSeating(currentOrder);
  }

  // --- 加分邏輯 ---
  let targetId = null;
  function openModal(id, name) {
    targetId = id;
    document.getElementById('m-name').innerText = name;
    document.getElementById('modal').style.display = 'block';
    document.getElementById('overlay').style.display = 'block';
  }

  function closeModal() {
    document.getElementById('modal').style.display = 'none';
    document.getElementById('overlay').style.display = 'none';
  }

  function applyScore() {
    const val = document.getElementById('m-score').value;
    google.script.run.withSuccessHandler(res => {
      const s = students.find(x => x.id == targetId);
      s.score = res.newScore;
      render();
      closeModal();
    }).updateScore(targetId, val);
  }

  function updateRankings() {
    const top3 = [...students].sort((a,b) => b.score - a.score).slice(0,3);
    document.getElementById('top-3').innerHTML = top3.map((s,i) => `<b>${i+1}.</b> ${s.name}(${s.score})`).join(' | ');
    
    const groups = {};
    students.forEach(s => { groups[s.group] = (groups[s.group] || 0) + s.score; });
    const gRank = Object.entries(groups).sort((a,b) => b[1]-a[1]);
    document.getElementById('group-rank').innerHTML = gRank.map(g => `第${g[0]}組: ${g[1]}分`).join(' / ');
  }
</script>/** script 區塊新增部分 **/

function resetScores() {
  // 第一次確認
  if (confirm("⚠️ 注意!這會將所有學生的個人與小組分數「歸零」,確定要執行嗎?")) {
    // 第二次確認,避免手滑
    const finalCheck = prompt("請輸入 'RESET' 以確認清除所有分數:");
    
    if (finalCheck === "RESET") {
      google.script.run.withSuccessHandler(msg => {
        alert(msg);
        // 更新網頁上的學生資料狀態
        students.forEach(s => s.score = 0);
        render(); // 重新渲染畫面
      }).resetAllScores();
    } else {
      alert("已取消操作。");
    }
  }
}
</body>
</html>
貼到 Apps Script 的 index.html 檔案

6. 部署

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

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

💬 評論區

老師們的交流與提問

載入評論中...