0. 作品介紹

作者 新竹教師測試員 教育處

適合負責彙整、影印各班或各年級考卷的人員使用


144 3 0
辦公
示範影片

1. AI 提示詞

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

1.我想要建立一個 【期末考卷影印需求填寫網頁】

2.Google 試算表建立 【114-1課程資料】和【回覆資料庫】

3.需求說明 【老師選擇姓名之後,從114-1課程資料的試算表調出他的資料、選擇印製份數(修課人數, +1, +2)、限制只能上傳pdf檔、不強迫老師每個上傳欄位都需要上傳檔案】

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

5.老師上傳的檔案請存在雲端硬碟資料夾,資料夾名稱為「期末影印」

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

作業環境

Google試算表

Google Apps Script

Google雲端硬碟

2. 資料表規則

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

資料表規則圖
第1分頁: 114-1課程資料 A 欄: 科號 B 欄: 科目 C 欄: 教師姓名 D 欄: 修課人數 第2分頁: 回覆資料庫 A 欄: 時間戳記 B 欄: 教師 C 欄: 科號 D 欄: 科目 E 欄: 修課人數 F 欄: 份數選項 G 欄: 最終份數 H 欄: 是否裝訂 I 欄: 紙張尺寸 J 欄: 頁數 K 欄: 檔案連結

📄 解析結果(自動表格)

分頁:114-1課程資料
A 欄 B 欄 C 欄 D 欄
科號 科目 教師姓名 修課人數
分頁:回覆資料庫
A 欄 B 欄 C 欄 D 欄 E 欄 F 欄 G 欄 H 欄 I 欄 J 欄 K 欄
時間戳記 教師 科號 科目 修課人數 份數選項 最終份數 是否裝訂 紙張尺寸 頁數 檔案連結

3. 開啟 Apps Script

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

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

4. 貼上後端(Code.gs)

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

Code.gs(後端)
// ===== 基本設定 =====
const DATA_SHEET_NAME = '114-1課程資料';
const RESPONSE_SHEET_NAME = '回覆資料庫';
const TARGET_FOLDER_ID = '請改成自己的雲端硬碟';

// 允許的 MIME types
const ALLOWED_TYPES = [
  'application/pdf',
  'application/msword',
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
];

// ===== Web App 入口 =====
function doGet() {
  return HtmlService.createHtmlOutputFromFile('Index')
    .setTitle('期末考卷影印需求系統')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

// ===== 教師名單 =====
function getTeacherNames() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(DATA_SHEET_NAME);
  const values = sheet.getDataRange().getValues();
  return [...new Set(values.slice(1).map(r => r[2]).filter(String))].sort();
}

// ===== 教師課程 =====
function getCoursesByTeacher(teacher) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(DATA_SHEET_NAME);
  return sheet.getDataRange().getValues().slice(1)
    .filter(r => r[2] === teacher)
    .map(r => ({
      id: r[0],
      name: r[1],
      teacher: r[2],
      students: r[3]
    }));
}

// ===== 表單處理 =====
function processForm(formObject) {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    let sheet = ss.getSheetByName(RESPONSE_SHEET_NAME);

    if (!sheet) {
      sheet = ss.insertSheet(RESPONSE_SHEET_NAME);
      sheet.appendRow([
        '時間戳記','教師','科號','科目','修課人數',
        '份數選項','最終份數','是否裝訂','紙張尺寸',
        '頁數','檔案連結'
      ]);
    }

    const folder = DriveApp.getFolderById(TARGET_FOLDER_ID);
    const rows = [];
    const time = new Date();

    for (const key in formObject) {
      if (!key.startsWith('course_')) continue;

      const id = key.replace('course_', '');
      const course = JSON.parse(formObject[key]);
      let fileUrl = '未上傳';

      const fileObj = formObject['file_' + id];
      if (fileObj && fileObj.data) {

        // ===== 後端檔案格式檢查 =====
        if (!ALLOWED_TYPES.includes(fileObj.type)) {
          throw new Error(`不支援的檔案格式:${fileObj.name}`);
        }

        const blob = Utilities.newBlob(
          Utilities.base64Decode(fileObj.data),
          fileObj.type,
          fileObj.name
        );

        const fileName =
          `[${course.name}]${course.teacher}_${course.finalQuantity}份_${fileObj.name}`;

        fileUrl = folder.createFile(blob).setName(fileName).getUrl();
      }

      rows.push([
        time,
        course.teacher,
        course.id,
        course.name,
        course.students,
        course.chosenQuantity,
        course.finalQuantity,
        course.binding || '否',
        course.paperSize || '',
        course.pageCount || '',
        fileUrl
      ]);
    }

    if (rows.length) {
      sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, rows[0].length)
        .setValues(rows);
    }

    return { success: true, message: '已成功提交需求' };

  } catch (e) {
    return { success: false, message: '錯誤:' + e.message };
  }
}
貼到 Apps Script 的 Code.gs 檔案

5. 貼上前端(index.html)

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

index.html(前端)
<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <style>
    body { font-family:"Microsoft JhengHei"; background:#f4f7f6; padding:20px; }
    .card { background:#fff; padding:25px; max-width:850px; margin:auto; border-radius:8px; }
    .course-item { background:#fafafa; margin:15px 0; padding:15px; border-left:5px solid #27ae60; }
    label { font-weight:bold; margin-top:10px; display:block; }
    select,input { width:100%; padding:8px; margin-top:5px; }
    small { color:#666; display:block; margin-top:4px; }
    button { width:100%; padding:15px; background:#27ae60; color:white; border:none; margin-top:20px; }
    .sub-fields { display:none; }
    #status { margin-top:15px; font-weight:bold; text-align:center; }
  </style>
</head>

<body>
<div class="card">
  <h2>期末考卷影印需求填寫</h2>

  <label>教師姓名</label>
  <select id="teacherSelect" onchange="loadCourses()"></select>

  <div id="courseList"></div>
  <button id="submitBtn" onclick="submitForm()" style="display:none;">確認提交</button>
  <div id="status"></div>
</div>

<script>
const teacherSelect = document.getElementById('teacherSelect');
const courseList = document.getElementById('courseList');
const submitBtn = document.getElementById('submitBtn');
const status = document.getElementById('status');

// 載入教師
google.script.run.withSuccessHandler(list=>{
  teacherSelect.innerHTML =
    '<option value="">請選擇</option>' +
    list.map(t=>`<option>${t}</option>`).join('');
}).getTeacherNames();

// 載入課程
function loadCourses(){
  const teacher = teacherSelect.value;
  if(!teacher) return;

  google.script.run.withSuccessHandler(courses=>{
    let html='';
    courses.forEach(c=>{
      html+=`
      <div class="course-item">
        <b>${c.id} ${c.name}(${c.students}人)</b>

        <label>影印份數</label>
        <select class="opt" data-id="${c.id}" data-base="${c.students}"
          onchange="toggleOptions('${c.id}')">
          <option value="none">不需要影印</option>
          <option value="base">修課人數 (${c.students})</option>
          <option value="base+1">修課人數 + 1 (${c.students+1})</option>
          <option value="base+2">修課人數 + 2 (${c.students+2})</option>
        </select>

        <div class="sub-fields" id="sub_${c.id}">
          <label>是否裝訂</label>
          <select id="bind_${c.id}">
            <option value="否">否</option>
            <option value="是">是</option>
          </select>

          <label>紙張尺寸</label>
          <select id="paper_${c.id}">
            <option value="A4">A4</option>
            <option value="A3">A3</option>
          </select>

          <label>頁數</label>
          <input type="number" id="page_${c.id}" min="1">

          <label>檔案上傳</label>
          <input type="file" id="file_${c.id}"
            accept=".pdf,.doc,.docx"
            onchange="validateFile('${c.id}')">
          <small>建議上傳 PDF 檔,以避免格式跑掉</small>
        </div>
      </div>`;
    });
    courseList.innerHTML = html;
    submitBtn.style.display = 'block';
  }).getCoursesByTeacher(teacher);
}

// 顯示 / 隱藏
function toggleOptions(id){
  const val = document.querySelector(`.opt[data-id="${id}"]`).value;
  document.getElementById('sub_'+id).style.display =
    (val === 'none') ? 'none' : 'block';
}

// ===== 前端檔案格式檢查 =====
function validateFile(id){
  const fileInput = document.getElementById('file_' + id);
  const file = fileInput.files[0];
  if(!file) return;

  const allowed = ['pdf','doc','docx'];
  const ext = file.name.split('.').pop().toLowerCase();

  if (!allowed.includes(ext)) {
    alert('僅允許上傳 PDF 或 Word 檔(.doc / .docx)');
    fileInput.value = '';
  }
}

// 送出
async function submitForm(){
  try {
    submitBtn.disabled=true;
    status.innerText='提交中...';

    const teacher = teacherSelect.value;
    const formObj = {};
    const opts = document.querySelectorAll('.opt');

    for (const opt of opts){
      const id = opt.dataset.id;
      const base = Number(opt.dataset.base);
      const val = opt.value;

      let qty = 0;
      if (val === 'base') qty = base;
      if (val === 'base+1') qty = base+1;
      if (val === 'base+2') qty = base+2;

      const page = document.getElementById('page_'+id)?.value || '';
      const file = document.getElementById('file_'+id)?.files[0];

      if (val !== 'none' && !file) {
        alert('請上傳檔案');
        submitBtn.disabled=false;
        return;
      }

      formObj['course_'+id] = JSON.stringify({
        id: id,
        name: document.querySelector(`[data-id="${id}"]`).closest('.course-item')
              .querySelector('b').innerText,
        teacher: teacher,
        students: base,
        chosenQuantity:
          val==='none'?'不需要影印':
          val==='base'?`修課人數 (${base})`:
          val==='base+1'?`修課人數 + 1 (${base+1})`:
          `修課人數 + 2 (${base+2})`,
        finalQuantity: qty,
        binding: document.getElementById('bind_'+id)?.value || '',
        paperSize: document.getElementById('paper_'+id)?.value || '',
        pageCount: page
      });

      if (file){
        const b64 = await toBase64(file);
        formObj['file_'+id]={ name:file.name, type:file.type, data:b64 };
      }
    }

    google.script.run.withSuccessHandler(res=>{
      status.innerText=res.message;
      submitBtn.disabled=false;
    }).processForm(formObj);

  } catch(e){
    alert(e.message);
    submitBtn.disabled=false;
    status.innerText='';
  }
}

// File → Base64
function toBase64(file){
  return new Promise(resolve=>{
    const r=new FileReader();
    r.onload=e=>resolve(e.target.result.split(',')[1]);
    r.readAsDataURL(file);
  });
}
</script>
</body>
</html>
貼到 Apps Script 的 index.html 檔案

6. 部署

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

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

💬 評論區

老師們的交流與提問

載入評論中...