0. 作品介紹

作者 彭若雯 新竹國小

收集老師們使用數位平台的成果照片,並順便呈現成果照片區


64 2 0
封面圖 封面

1. AI 提示詞

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

我想要建立一個 【教師軟體成果收集與展示平台】
幫我用Google 試算表建立 【教師軟體成果資料庫】
我希望有些資料我可以直接在試算表更改首頁可以直接呈現
需求說明 【
1.資料與名單管理:
我有一個名單,包含「教師姓名」與「需繳交的軟體項目」。
後端需與 Google 試算表連動,即時讀取名單設定。
照片需儲存在 Google 雲端硬碟,且自動建立資料夾階層:第一層為「軟體名稱」、第二層為「教師姓名」、第三層存放照片。
2.使用者介面 :
必須支援手機版操作,方便老師使用手機拍照後直接上傳。
老師選擇自己的姓名後,系統自動列出該位老師需繳交的軟體項目。
每個項目需顯示「未繳交」或「已完成」的狀態燈號。
支援一次選取多張照片進行批次上傳。
3.彈性擴充:
若教師不在名單內,可選擇「其他」並手動輸入姓名。
若軟體不在清單內,可選擇「其他」並手動輸入軟體名稱。
上傳成功後,介面需即時更新為「已完成」狀態,不需重新整理網頁。
4.成果展示區:
在網頁最下方顯示所有上傳的照片,並依「軟體名稱」分類。
展示區僅顯示照片,不顯示教師姓名與上傳時間。
每個軟體類別只需隨機顯示 3~4 張照片即可。
點擊照片後,需彈出視窗 (Modal/Lightbox) 顯示大圖】
要把它變成程式碼我要發佈
請完整敘述操作步驟 越詳細越好
作業環境
Google試算表
Google Apps Script
我看不懂程式碼,請給我後端gs完整程式碼、前端index完整程式碼
補充:我有使用雲端硬碟回收照片或PDF或文件【雲端硬碟連結】

2. 資料表規則

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

第1分頁: 名單設定 A 欄: 教師姓名 B 欄: 需繳交軟體 第2分頁: 繳交紀錄 A 欄: 時間戳記 B 欄: 教師姓名 C 欄: 軟體名稱 D 欄: 圖片連結 E 欄: 檔案ID

📄 解析結果(自動表格)

分頁:名單設定
A 欄 B 欄
教師姓名 需繳交軟體
分頁:繳交紀錄
A 欄 B 欄 C 欄 D 欄 E 欄
時間戳記 教師姓名 軟體名稱 圖片連結 檔案ID

3. 開啟 Apps Script

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

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

4. 貼上後端(Code.gs)

把後端程式貼到 Apps Script 的 Code.gs。

Code.gs(後端)
/**
 * 設定區:請將下方的 ID 換成你第一步建立的 Google Drive 資料夾 ID
 * 這將作為「根目錄」,程式會在其下自動建立「軟體資料夾」與「教師資料夾」
 */
const UPLOAD_FOLDER_ID = "你的資料夾ID貼在這裡"; // 例如 "1abcDEfg..."

/* -------------------------------------------------- */

// 1. 取得網頁介面
function doGet() {
  return HtmlService.createTemplateFromFile('index')
    .evaluate()
    .setTitle('教師軟體成果收集平台')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
    .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

// 2. 取得所有初始資料 (名單、軟體清單、目前繳交狀態、成果展照片)
function getInitialData() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const configSheet = ss.getSheetByName("名單設定");
  const recordSheet = ss.getSheetByName("繳交紀錄");
  
  // 讀取名單設定 (假設 A欄是姓名, B欄是需繳交軟體)
  const lastRow = configSheet.getLastRow();
  const configData = lastRow > 1 ? configSheet.getRange(2, 1, lastRow - 1, 2).getValues() : [];
  
  // 整理老師名單與需求
  let teachers = {};
  let allSoftwareSet = new Set(); 

  configData.forEach(row => {
    let name = row[0];
    let softStr = row[1];
    if (name && softStr) {
      let softwareList = softStr.toString().split(/[,,]/).map(s => s.trim()).filter(s => s !== "");
      teachers[name] = softwareList;
      softwareList.forEach(s => allSoftwareSet.add(s));
    }
  });

  // 讀取繳交紀錄
  let records = [];
  if (recordSheet.getLastRow() > 1) {
    records = recordSheet.getRange(2, 1, recordSheet.getLastRow() - 1, 5).getValues();
  }

  let completedMap = {};
  let galleryData = {}; 

  records.forEach(row => {
    let rName = row[1];
    let rSoft = row[2];
    let rUrl = row[3];
    
    // 標記完成狀態
    completedMap[`${rName}_${rSoft}`] = true;

    // 收集成果展照片
    if (rUrl) {
      if (!galleryData[rSoft]) {
        galleryData[rSoft] = [];
      }
      galleryData[rSoft].unshift({
        teacher: rName,
        url: rUrl,
        time: new Date(row[0]).toLocaleDateString()
      });
    }
  });

  return {
    teachers: teachers, 
    completedMap: completedMap, 
    allSoftware: Array.from(allSoftwareSet), 
    gallery: galleryData 
  };
}

// 3. 輔助函式:取得或建立資料夾
function getOrCreateFolder(parentFolder, folderName) {
  const folders = parentFolder.getFoldersByName(folderName);
  if (folders.hasNext()) {
    return folders.next();
  } else {
    const newFolder = parentFolder.createFolder(folderName);
    newFolder.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
    return newFolder;
  }
}

// 4. 處理檔案上傳
function uploadPhoto(data) {
  try {
    // A. 將 Base64 轉為 Blob
    const contentType = data.fileData.substring(5, data.fileData.indexOf(';'));
    const bytes = Utilities.base64Decode(data.fileData.substring(data.fileData.indexOf('base64,') + 7));
    const blob = Utilities.newBlob(bytes, contentType, data.fileName);
    
    // B. 處理資料夾階層
    const rootFolder = DriveApp.getFolderById(UPLOAD_FOLDER_ID);
    const softwareFolder = getOrCreateFolder(rootFolder, data.softwareName);
    const teacherFolder = getOrCreateFolder(softwareFolder, data.teacherName);
    
    // C. 存入最終資料夾
    const file = teacherFolder.createFile(blob);
    file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
    
    const displayUrl = `https://drive.google.com/uc?export=view&id=${file.getId()}`;

    // D. 寫入試算表
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const recordSheet = ss.getSheetByName("繳交紀錄");
    
    recordSheet.appendRow([
      new Date(),
      data.teacherName,
      data.softwareName,
      displayUrl,
      file.getId()
    ]);

    // [更新] 回傳 fileName 讓前端知道哪個檔案傳完了
    return { success: true, url: displayUrl, fileName: data.fileName };

  } catch (e) {
    return { success: false, error: e.toString() };
  }
}
貼到 Apps Script 的 Code.gs 檔案

5. 貼上前端(index.html)

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

index.html(前端)
<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <!-- 使用 Bootstrap 5 讓介面美觀 -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
  <style>
    body { background-color: #f8f9fa; font-family: "Microsoft JhengHei", sans-serif; }
    .container { max-width: 900px; padding-top: 30px; padding-bottom: 50px; }
    .card { border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); border: none; margin-bottom: 20px; }
    .header-box { background: linear-gradient(135deg, #0d6efd, #0dcaf0); color: white; padding: 30px; border-radius: 12px; margin-bottom: 30px; text-align: center; }
    .status-badge { font-size: 0.9em; padding: 6px 12px; border-radius: 20px; }
    .gallery-img { width: 100%; height: 200px; object-fit: cover; border-radius: 8px; cursor: pointer; transition: transform 0.2s; }
    .gallery-img:hover { transform: scale(1.03); }
    .loading-overlay { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(255,255,255,0.8); z-index: 9999; display: flex; justify-content: center; align-items: center; flex-direction: column; }
    .software-section-title { border-left: 5px solid #0d6efd; padding-left: 10px; margin-top: 30px; margin-bottom: 15px; }
    
    /* [新增] Modal 圖片樣式 */
    #modalImage {
      max-height: 80vh; /* 限制高度,避免超出螢幕 */
      width: auto;
      max-width: 100%;
      display: block;
      margin: 0 auto;
      object-fit: contain;
    }
  </style>
</head>
<body>

  <!-- Loading 動畫 -->
  <div id="loader" class="loading-overlay">
    <div class="spinner-border text-primary" role="status" style="width: 3rem; height: 3rem;"></div>
    <div class="mt-3 fw-bold">系統載入中,請稍候...</div>
  </div>

  <div class="container">
    <div class="header-box">
      <h1><i class="fas fa-camera"></i> 教師軟體成果收集平台</h1>
      <p class="mb-0">請選擇您的姓名並上傳指定軟體的使用照片</p>
    </div>

    <!-- 步驟 1: 選擇身份 -->
    <div class="card p-4">
      <h4 class="mb-3"><i class="fas fa-user-circle"></i> 步驟 1:請選擇教師姓名</h4>
      <select id="teacherSelect" class="form-select form-select-lg" onchange="handleTeacherChange()">
        <option value="" selected disabled>-- 請選擇 --</option>
        <!-- 選單由 JavaScript 動態產生 -->
      </select>

      <!-- 若選擇其他,顯示手動輸入框 -->
      <div id="otherTeacherInputDiv" class="mt-3" style="display: none;">
        <label class="form-label">請輸入您的姓名:</label>
        <input type="text" id="customTeacherName" class="form-control" placeholder="例如:張代課">
        <div class="mt-2 text-muted small">選擇「其他」的老師,您可以從下方自由選擇要繳交的軟體。</div>
      </div>
    </div>

    <!-- 步驟 2: 任務列表 (動態顯示) -->
    <div id="taskSection" class="card p-4" style="display: none;">
      <h4 class="mb-3"><i class="fas fa-list-check"></i> 步驟 2:上傳軟體使用照片</h4>
      <p class="text-muted small"><i class="fas fa-info-circle"></i> 支援多張照片同時選取上傳。</p>
      <div id="taskList">
        <!-- 任務卡片會插在這裡 -->
      </div>
      
      <!-- 給「其他」老師或是想額外上傳的按鈕 -->
      <div id="extraUploadSection" class="mt-4 pt-3 border-top">
        <h5><i class="fas fa-plus-circle"></i> 上傳其他軟體照片</h5>
        <div class="row g-2 align-items-center">
          <div class="col-md-6">
            <select id="extraSoftwareSelect" class="form-select" onchange="handleExtraSoftwareChange()">
              <option value="" selected>-- 選擇軟體 --</option>
            </select>
            <!-- 自訂軟體名稱輸入框 -->
            <input type="text" id="customSoftwareInput" class="form-control mt-2" placeholder="請輸入軟體名稱 (例如: T++)" style="display: none;">
          </div>
          <div class="col-md-2">
            <button class="btn btn-outline-primary w-100" onclick="addManualTask()">新增上傳</button>
          </div>
        </div>
      </div>
    </div>

    <!-- 步驟 3: 成果展示區 -->
    <div class="card p-4 mt-5">
      <h4 class="mb-4 text-center"><i class="fas fa-images"></i> 成果展示區</h4>
      <p class="text-center text-muted">目前所有老師繳交的成果將顯示於此</p>
      <div id="galleryContainer">
        <!-- 照片會分類插在這裡 -->
      </div>
    </div>
  </div>

  <!-- [新增] 圖片預覽 Modal (燈箱) -->
  <div class="modal fade" id="imagePreviewModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered modal-lg">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title">成果預覽</h5>
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
        </div>
        <div class="modal-body text-center bg-dark">
          <img id="modalImage" src="" alt="預覽圖片">
        </div>
      </div>
    </div>
  </div>

  <!-- JavaScript 邏輯 -->
  <!-- 引入 Bootstrap JS 以支援 Modal -->
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
  <script>
    let globalData = {}; // 儲存後端傳來的資料
    let imageModal; // Modal 實例

    window.onload = function() {
      // 初始化 Modal
      imageModal = new bootstrap.Modal(document.getElementById('imagePreviewModal'));

      // [FIX] 檢查 google 是否定義,若無則載入模擬資料 (防止在預覽環境報錯)
      if (typeof google === 'undefined' || typeof google.script === 'undefined') {
        console.warn('未偵測到 Google Apps Script 環境,載入模擬資料供預覽。');
        loadMockData();
      } else {
        google.script.run.withSuccessHandler(initPage).getInitialData();
      }
    };

    // [NEW] 模擬資料函式
    function loadMockData() {
      const mockData = {
        teachers: {
          "王大明 (預覽)": ["Canva", "Word"],
          "林小美 (預覽)": ["Excel"]
        },
        completedMap: {},
        allSoftware: ["Canva", "Word", "Excel", "PowerPoint"],
        gallery: {
          "Canva": [
            { url: "https://via.placeholder.com/600x400?text=Preview+Image", teacher: "預覽", time: "2023/1/1" }
          ]
        }
      };
      // 模擬一點延遲增加真實感
      setTimeout(() => initPage(mockData), 500);
    }

    function initPage(data) {
      globalData = data;
      const select = document.getElementById('teacherSelect');
      const extraSelect = document.getElementById('extraSoftwareSelect');

      // 1. 填入老師選單
      Object.keys(data.teachers).forEach(name => {
        let option = document.createElement('option');
        option.value = name;
        option.text = name;
        select.add(option);
      });
      
      let otherOpt = document.createElement('option');
      otherOpt.value = "Other";
      otherOpt.text = "其他 (名單外教師)";
      select.add(otherOpt);

      // 2. 填入所有軟體選單
      data.allSoftware.forEach(soft => {
        let opt = document.createElement('option');
        opt.value = soft;
        opt.text = soft;
        extraSelect.add(opt);
      });
      
      let customSoftOpt = document.createElement('option');
      customSoftOpt.value = "_CUSTOM_";
      customSoftOpt.text = "其他 (自行輸入)";
      extraSelect.add(customSoftOpt);

      renderGallery();
      document.getElementById('loader').style.display = 'none';
    }

    function handleTeacherChange() {
      const selectedName = document.getElementById('teacherSelect').value;
      const otherInputDiv = document.getElementById('otherTeacherInputDiv');
      const taskSection = document.getElementById('taskSection');
      const taskList = document.getElementById('taskList');

      taskList.innerHTML = ''; 
      taskSection.style.display = 'block';

      if (selectedName === 'Other') {
        otherInputDiv.style.display = 'block';
        taskList.innerHTML = '<p class="text-muted">請使用下方「上傳其他軟體照片」區塊來進行繳交。</p>';
      } else {
        otherInputDiv.style.display = 'none';
        const softwares = globalData.teachers[selectedName] || [];
        
        if(softwares.length === 0) {
            taskList.innerHTML = '<div class="alert alert-warning">此教師目前沒有設定需繳交的軟體。</div>';
        }

        softwares.forEach(soft => {
          createTaskRow(selectedName, soft);
        });
      }
    }
    
    function handleExtraSoftwareChange() {
      const select = document.getElementById('extraSoftwareSelect');
      const input = document.getElementById('customSoftwareInput');
      if (select.value === '_CUSTOM_') {
        input.style.display = 'block';
        input.focus();
      } else {
        input.style.display = 'none';
      }
    }

    // 建立單一任務列 (UI)
    function createTaskRow(teacherName, softwareName) {
      const taskList = document.getElementById('taskList');
      const isCompleted = globalData.completedMap[`${teacherName}_${softwareName}`];

      const div = document.createElement('div');
      div.className = 'card p-3 mb-2 shadow-sm';
      div.style.borderLeft = isCompleted ? '5px solid #198754' : '5px solid #dc3545';
      
      let statusHtml = isCompleted 
        ? '<span class="badge bg-success status-badge"><i class="fas fa-check"></i> 已完成</span>'
        : '<span class="badge bg-danger status-badge"><i class="fas fa-times"></i> 未繳交</span>';

      div.innerHTML = `
        <div class="row align-items-center">
          <div class="col-md-4">
            <h5 class="mb-0 fw-bold">${softwareName}</h5>
          </div>
          <div class="col-md-3">
            ${statusHtml}
          </div>
          <div class="col-md-5 text-end">
            <input type="file" class="form-control d-none" id="file_${softwareName}" accept="image/*" multiple onchange="uploadFile(this, '${softwareName}')">
            <button class="btn btn-outline-primary" onclick="document.getElementById('file_${softwareName}').click()">
              <i class="fas fa-cloud-upload-alt"></i> ${isCompleted ? '繼續上傳' : '上傳照片'}
            </button>
          </div>
        </div>
      `;
      taskList.appendChild(div);
    }

    function addManualTask() {
      let teacherName = getCurrentTeacherName();
      if (!teacherName) {
        Swal.fire('提示', '請先輸入或選擇教師姓名', 'warning');
        return;
      }

      const select = document.getElementById('extraSoftwareSelect');
      let softName = select.value;
      
      if (softName === '_CUSTOM_') {
        const input = document.getElementById('customSoftwareInput');
        softName = input.value.trim();
        if (!softName) {
           Swal.fire('提示', '請輸入軟體名稱', 'warning');
           return;
        }
      } else if (!softName) {
        Swal.fire('提示', '請選擇要新增的軟體', 'warning');
        return;
      }

      createTaskRow(teacherName, softName);
      if (select.value === '_CUSTOM_') {
        document.getElementById('customSoftwareInput').value = '';
      }
    }

    function getCurrentTeacherName() {
      const select = document.getElementById('teacherSelect');
      if (select.value === 'Other') {
        return document.getElementById('customTeacherName').value.trim();
      }
      return select.value;
    }

    async function uploadFile(inputElement, softwareName) {
      const files = inputElement.files;
      if (files.length === 0) return;

      const teacherName = getCurrentTeacherName();
      if (!teacherName) {
        Swal.fire('錯誤', '請先填寫姓名', 'error');
        inputElement.value = ''; 
        return;
      }

      let successCount = 0;
      let failCount = 0;

      Swal.fire({
        title: '準備上傳中...',
        html: `共有 ${files.length} 張照片,請勿關閉視窗。`,
        allowOutsideClick: false,
        didOpen: () => { Swal.showLoading(); }
      });

      for (let i = 0; i < files.length; i++) {
        const file = files[i];
        
        Swal.update({
            html: `正在上傳第 <b>${i + 1}</b> / <b>${files.length}</b> 張照片<br><small class="text-muted">${file.name}</small>`
        });

        try {
            const result = await uploadSingleFile(file, teacherName, softwareName);
            
            if (result.success) {
                successCount++;
                updateGalleryLocal(softwareName, teacherName, result.url);
                globalData.completedMap[`${teacherName}_${softwareName}`] = true;
            } else {
                failCount++;
                console.error("Upload failed for " + file.name + ": " + result.error);
            }
        } catch (err) {
            failCount++;
            console.error("System error for " + file.name + ": " + err);
        }
      }

      inputElement.value = ''; 
      handleTeacherChange(); 
      
      if (failCount === 0) {
        Swal.fire('全部完成', `成功上傳 ${successCount} 張照片!`, 'success');
      } else {
        Swal.fire('完成但有錯誤', `成功: ${successCount} 張<br>失敗: ${failCount} 張`, 'warning');
      }
    }

    function uploadSingleFile(file, teacherName, softwareName) {
        return new Promise((resolve, reject) => {
            // [FIX] 增加對預覽環境的判斷,如果是預覽則回傳假成功
            if (typeof google === 'undefined' || typeof google.script === 'undefined') {
                console.log("模擬環境:假裝上傳成功");
                setTimeout(() => {
                    resolve({ 
                        success: true, 
                        url: "https://via.placeholder.com/600x400?text=Simulated+Upload", 
                        fileName: file.name 
                    });
                }, 800);
                return;
            }

            const reader = new FileReader();
            reader.onload = function(e) {
                const fileData = e.target.result;
                google.script.run
                  .withSuccessHandler(resolve)
                  .withFailureHandler(reject)
                  .uploadPhoto({
                    fileData: fileData,
                    fileName: file.name,
                    mimeType: file.type,
                    teacherName: teacherName,
                    softwareName: softwareName
                  });
            };
            reader.readAsDataURL(file);
        });
    }

    // [新增] 開啟圖片預覽視窗
    function openImageModal(url) {
        const modalImg = document.getElementById('modalImage');
        // 先顯示載入中或預設圖,避免上一張殘留
        modalImg.src = "";
        modalImg.src = url;
        imageModal.show();
    }

    // [核心修改] 渲染成果展示區
    function renderGallery() {
      const container = document.getElementById('galleryContainer');
      container.innerHTML = '';
      
      const gallery = globalData.gallery;
      if (Object.keys(gallery).length === 0) {
        container.innerHTML = '<div class="text-center p-5 text-muted">目前尚無任何照片</div>';
        return;
      }

      for (const [soft, allImages] of Object.entries(gallery)) {
        // 1. 隨機選取 4 張照片邏輯
        let displayImages = [...allImages]; // 複製一份,不影響原始資料
        
        if (displayImages.length > 4) {
             // 簡單的隨機排序後取前4張
             displayImages = displayImages.sort(() => 0.5 - Math.random()).slice(0, 4);
        }

        let section = document.createElement('div');
        section.className = 'mb-5';
        
        let imgHtml = '';
        displayImages.forEach(img => {
          // 2. 轉換為 Thumbnail 連結
          let thumbUrl = img.url;
          if (thumbUrl.includes('drive.google.com') && thumbUrl.includes('id=')) {
             // 預覽圖使用 w600 (縮圖)
             thumbUrl = thumbUrl.replace('uc?export=view&', 'thumbnail?sz=w600&');
          }

          // 3. [關鍵修改] 產生高畫質大圖連結 (sz=s2000 代表長邊 2000px),並綁定 Modal 事件
          let largeUrl = thumbUrl.replace('sz=w600', 'sz=s2000');
          // 如果原始網址沒有經過 replace (例如模擬資料),直接用原網址
          if(largeUrl === thumbUrl && !thumbUrl.includes('thumbnail?')) {
             largeUrl = thumbUrl; 
          }

          // 移除 window.open,改用 openImageModal
          imgHtml += `
            <div class="col-6 col-md-4 col-lg-3 mb-3">
              <div class="card h-100">
                <img src="${thumbUrl}" class="gallery-img" onclick="openImageModal('${largeUrl}')" referrerpolicy="no-referrer" loading="lazy">
              </div>
            </div>
          `;
        });

        section.innerHTML = `
          <h5 class="software-section-title">${soft}</h5>
          <div class="row">
            ${imgHtml}
          </div>
        `;
        container.appendChild(section);
      }
    }

    function updateGalleryLocal(soft, teacher, url) {
      if (!globalData.gallery[soft]) {
        globalData.gallery[soft] = [];
      }
      globalData.gallery[soft].unshift({
        teacher: teacher,
        url: url,
        time: new Date().toLocaleDateString()
      });
      renderGallery();
    }
  </script>
</body>
</html>
貼到 Apps Script 的 index.html 檔案

6. 部署

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

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

💬 評論區

老師們的交流與提問

載入評論中...