0. 作品介紹

作者 Alex 數位實驗高中

研習報到系統


48 1 0
辦公
封面圖 封面

1. AI 提示詞

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

我想要建立一個 【研習報到表】

Google 試算表建立 【資料庫】

需求說明 【老師報到成功可指定位置、也可隨機】

要把它變成程式碼我要發佈

請給我完整程式碼

請完整敘述操作步驟 越詳細越好

作業環境

Google試算表

Google Apps Script

2. 資料表規則

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

第1分頁: 分頁1 A 欄: 報到代碼 B 欄: 姓名 C 欄: 預排座位 D 欄: 報到狀態 E 欄: 分配座位 F 欄: 報到時間

📄 解析結果(自動表格)

分頁:分頁1
A 欄 B 欄 C 欄 D 欄 E 欄 F 欄
報到代碼 姓名 預排座位 報到狀態 分配座位 報到時間

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 */

// 修改:配合 8x10 座位表,設定總座位數為 80
const TOTAL_SEATS_COUNT = 80; 

function doGet() {
  return HtmlService.createTemplateFromFile('index')
    .evaluate()
    .setTitle('研習報到系統 (含座位表)')
    .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

function processCheckIn(userKey) {
  var lock = LockService.getScriptLock();
  try {
    lock.waitLock(10000); 
  } catch (e) {
    return { status: 'error', message: '系統繁忙,請稍後再試' };
  }

  try {
    var ss = SpreadsheetApp.getActiveSpreadsheet();
    var sheet = ss.getSheetByName("名單");
    var data = sheet.getDataRange().getValues();
    
    var foundIndex = -1;
    var userData = null;

    // 1. 搜尋使用者 (A欄)
    for (var i = 1; i < data.length; i++) {
      if (String(data[i][0]).trim() == String(userKey).trim()) {
        foundIndex = i;
        userData = data[i];
        break;
      }
    }

    if (foundIndex === -1) {
      return { status: 'error', message: '找不到此報到代碼,請確認後重試。' };
    }

    // 2. 檢查是否已報到 (D欄)
    if (userData[3] === "已報到") {
      return { 
        status: 'success', 
        name: userData[1], 
        seat: userData[4], 
        message: '您已經完成報到了!' 
      };
    }

    // 3. 分配座位邏輯
    var assignedSeat = "";
    var preAssignedSeat = userData[2]; // C欄預排

    if (preAssignedSeat && String(preAssignedSeat).trim() !== "") {
      assignedSeat = preAssignedSeat;
    } else {
      // 找出已佔用座位
      var takenSeats = [];
      for (var j = 1; j < data.length; j++) {
        if (data[j][4]) takenSeats.push(String(data[j][4])); // E欄分配
        else if (data[j][2]) takenSeats.push(String(data[j][2])); // C欄預排
      }

      // 產生可用座位池 (1 ~ 80)
      var availableSeats = [];
      for (var k = 1; k <= TOTAL_SEATS_COUNT; k++) {
        // 如果座位號碼不在已佔用名單中
        if (takenSeats.indexOf(String(k)) === -1) {
          availableSeats.push(k);
        }
      }

      if (availableSeats.length === 0) {
        return { status: 'error', message: '抱歉,目前已無空位。' };
      }

      var randomIndex = Math.floor(Math.random() * availableSeats.length);
      assignedSeat = availableSeats[randomIndex];
    }

    // 4. 寫入資料庫
    var timestamp = new Date();
    sheet.getRange(foundIndex + 1, 4).setValue("已報到");
    sheet.getRange(foundIndex + 1, 5).setValue(assignedSeat);
    sheet.getRange(foundIndex + 1, 6).setValue(timestamp);

    return { 
      status: 'success', 
      name: userData[1], 
      seat: assignedSeat, 
      message: '報到成功!' 
    };

  } catch (err) {
    return { status: 'error', message: '系統錯誤: ' + err.toString() };
  } finally {
    lock.releaseLock();
  }
}
貼到 Apps Script 的 Code.gs 檔案

5. 貼上前端(index.html)

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

index.html(前端)
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <style>
      body { background-color: #f0f2f5; padding-bottom: 50px; }
      .main-container { max-width: 600px; margin: 30px auto; padding: 0 15px; }
      
      .card { border: none; border-radius: 15px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); overflow: hidden; }
      .card-header { background: #4285f4; color: white; text-align: center; padding: 20px; }
      
      .seat-big-display { font-size: 2.5rem; font-weight: 800; color: #4285f4; }
      
      /* --- 座位表樣式 --- */
      .stage-area {
        background-color: #6c757d; color: white; text-align: center;
        padding: 5px; margin-bottom: 15px; border-radius: 5px; font-weight: bold;
      }
      
      .seating-chart {
        display: grid;
        /* 設定 8 欄,每欄等寬 */
        grid-template-columns: repeat(8, 1fr); 
        gap: 6px; /* 格子間距 */
        justify-content: center;
        margin-top: 10px;
      }

      .seat-box {
        aspect-ratio: 1 / 1; /* 保持正方形 */
        background-color: #e9ecef;
        border: 1px solid #dee2e6;
        border-radius: 4px;
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 0.75rem;
        color: #666;
        cursor: default;
        transition: all 0.3s;
      }

      /* 自己的座位樣式 (被選中時) */
      .seat-box.my-seat {
        background-color: #dc3545 !important; /* 紅色高亮 */
        color: white !important;
        font-weight: bold;
        border-color: #dc3545;
        transform: scale(1.1);
        box-shadow: 0 0 10px rgba(220, 53, 69, 0.5);
        z-index: 10;
      }

      /* 手機版微調字體 */
      @media (max-width: 400px) {
        .seat-box { font-size: 0.6rem; }
      }

      .loader { display: none; width: 30px; height: 30px; border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto; }
      @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
      .hidden { display: none; }
    </style>
  </head>
  <body>

    <div class="main-container">
      <div class="card">
        <div class="card-header">
          <h3 class="mb-0">研習報到系統</h3>
        </div>
        <div class="card-body p-4">
          
          <div id="inputSection">
            <div class="mb-3 text-center">
              <label class="form-label text-muted">請輸入報到代碼 / 手機</label>
              <input type="text" class="form-control form-control-lg text-center" id="userKey" placeholder="例如: 1001" onkeypress="handleEnter(event)">
            </div>
            <button class="btn btn-primary w-100 btn-lg" onclick="submitCheckIn()">確認報到</button>
          </div>

          <div id="loadingSection" class="text-center mt-4 hidden">
            <div class="loader"></div>
            <p class="mt-2 text-muted">查詢座位中...</p>
          </div>

          <div id="resultSection" class="text-center mt-3 hidden">
            <div class="alert alert-success fw-bold" id="statusMessage">報到成功</div>
            <h4 id="userName">老師姓名</h4>
            <p class="text-muted mb-0">您的座位號碼</p>
            <div class="seat-big-display" id="seatNumber">--</div>
            
            <hr>
            
            <h6 class="text-start text-muted">座位位置圖 (紅框為您的位置)</h6>
            <div class="stage-area">講 台 (前方)</div>
            <div class="seating-chart" id="seatingGrid">
              </div>

            <button class="btn btn-outline-secondary w-100 mt-4" onclick="resetForm()">返回 / 下一位</button>
          </div>

          <div id="errorSection" class="alert alert-danger mt-3 hidden text-center"></div>

        </div>
      </div>
    </div>

    <script>
      // 初始化:產生 8x10 的格子
      document.addEventListener("DOMContentLoaded", function() {
        generateSeatingGrid(80); // 80個座位
      });

      function generateSeatingGrid(total) {
        const grid = document.getElementById('seatingGrid');
        grid.innerHTML = ''; // 清空
        
        for (let i = 1; i <= total; i++) {
          let div = document.createElement('div');
          div.className = 'seat-box';
          div.id = 'seat-' + i; // 設定 ID 以便後續搜尋
          div.innerText = i;
          grid.appendChild(div);
        }
      }

      function handleEnter(e) {
        if(e.key === 'Enter') submitCheckIn();
      }

      function submitCheckIn() {
        var key = document.getElementById('userKey').value;
        if (!key) { showError('請輸入代碼!'); return; }

        toggleSection('loading');
        
        google.script.run
          .withSuccessHandler(onSuccess)
          .withFailureHandler(onFailure)
          .processCheckIn(key);
      }

      function onSuccess(response) {
        if (response.status === 'success') {
          toggleSection('result');
          
          document.getElementById('userName').innerText = response.name + " 老師";
          document.getElementById('seatNumber').innerText = response.seat;
          document.getElementById('statusMessage').innerText = response.message;
          
          // --- 視覺化核心邏輯 ---
          highlightSeat(response.seat);

        } else {
          toggleSection('input');
          showError(response.message);
        }
      }

      function highlightSeat(seatNum) {
        // 先移除所有亮起狀態
        document.querySelectorAll('.seat-box').forEach(el => el.classList.remove('my-seat'));
        
        // 嘗試將回傳的座位轉為字串並去空白
        let targetId = 'seat-' + String(seatNum).trim();
        let targetDiv = document.getElementById(targetId);
        
        // 如果找到了對應的格子 (代表是 1~80 數字),就亮起
        if (targetDiv) {
          targetDiv.classList.add('my-seat');
        } else {
          // 如果座位是 "VIP-1" 等特殊文字,無法在地圖上顯示,就不亮起,但上方文字仍會顯示
          console.log("非標準數字座位,無法在地圖上標示");
        }
      }

      function onFailure(error) {
        toggleSection('input');
        showError('連線錯誤,請重試。');
      }

      function resetForm() {
        document.getElementById('userKey').value = '';
        toggleSection('input');
        // 清除亮起
        document.querySelectorAll('.seat-box').forEach(el => el.classList.remove('my-seat'));
      }

      function showError(msg) {
        var errDiv = document.getElementById('errorSection');
        errDiv.innerText = msg;
        errDiv.classList.remove('hidden');
      }

      function toggleSection(section) {
        document.getElementById('inputSection').classList.add('hidden');
        document.getElementById('loadingSection').classList.add('hidden');
        document.getElementById('resultSection').classList.add('hidden');
        document.getElementById('errorSection').classList.add('hidden');

        if(section === 'input') document.getElementById('inputSection').classList.remove('hidden');
        if(section === 'loading') document.getElementById('loadingSection').classList.remove('hidden');
        if(section === 'result') document.getElementById('resultSection').classList.remove('hidden');
      }
    </script>
  </body>
</html>
貼到 Apps Script 的 index.html 檔案

6. 部署

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

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

💬 評論區

老師們的交流與提問

載入評論中...