0. 作品介紹

作者 楊翔紘 三民國中

本作品為「115學年度竹苗區免試入學積分小工具」,主要用途是協助國中學生依據竹苗區免試入學積分規則,進行個人化積分試算、結果儲存與後續查詢。 學生可透過計算機頁面自行輸入目前年級/試算階段、基本資料、均衡發展、多元學習表現、國中教育會考等資料,系統會依照積分規則自動計算目前可預估的免試入學積分,並產生積分明細與補分提醒。透過階段式試算設計,學生能依照目前已完成的學期資料進行檢核,避免誤填尚未發生或尚未結算的資料。 本工具不僅提供即時計算功能,也加入「儲存試算結果」與「個人結果查詢」機制。學生完成試算後,可輸入學號、生日與個人查詢密碼,將本次試算結果儲存至 Google 試算表。之後學生可於同一網頁中的「個人結果查詢」分頁,輸入相同的學號、生日與個人查詢密碼,查詢自己最近一次儲存的積分結果。


524 9 0
班級經營 辦公 作業
示範影片

1. AI 提示詞

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

※使用前請務必閱讀本說明,並按照說明流程設置!
教學連結:https://canva.link/vkdqc5gf77oxg6k

2. 資料表規則

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

第1分頁: saved_results A 欄: record_id B 欄: timestamp C 欄: student_id D 欄: birthday_hash E 欄: password_hash F 欄: name G 欄: class_name H 欄: seat_no I 欄: grade_stage J 欄: group1_score K 欄: group2_score L 欄: group3_score M 欄: total_score N 欄: report_text O 欄: input_json P 欄: result_json Q 欄: created_by_email

📄 解析結果(自動表格)

分頁:saved_results
A 欄 B 欄 C 欄 D 欄 E 欄 F 欄 G 欄 H 欄 I 欄 J 欄 K 欄 L 欄 M 欄 N 欄 O 欄 P 欄 Q 欄
record_id timestamp student_id birthday_hash password_hash name class_name seat_no grade_stage group1_score group2_score group3_score total_score report_text input_json result_json created_by_email

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 SHEET_NAME = 'saved_results';
const SECRET_SALT = '115_zhumiao_admission_score_tool_v2_change_before_real_use';

const HEADERS = [
  'record_id',
  'timestamp',
  'student_id',
  'birthday_hash',
  'password_hash',
  'name',
  'class_name',
  'seat_no',
  'grade_stage',
  'group1_score',
  'group2_score',
  'group3_score',
  'total_score',
  'report_text',
  'input_json',
  'result_json',
  'created_by_email'
];

function doGet(e) {
  const template = HtmlService.createTemplateFromFile('index');
  template.webAppUrl = getWebAppUrl_();
  return template.evaluate()
    .setTitle('115學年度竹苗區免試入學積分小工具')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

function getWebAppUrl_() {
  try {
    return ScriptApp.getService().getUrl() || '';
  } catch (err) {
    return '';
  }
}

/**
 * 第一次安裝或資料表異常時請執行這個函式。
 * 它會建立 saved_results 工作表,並確認欄位數足夠,避免「那些欄超出邊界」錯誤。
 */
function setupDatabase() {
  const sheet = getOrCreateDatabaseSheet_();
  return '資料表已建立或確認完成:' + SHEET_NAME;
}

function getOrCreateDatabaseSheet_() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName(SHEET_NAME);
  if (!sheet) {
    sheet = ss.insertSheet(SHEET_NAME);
  }

  ensureSheetHasColumns_(sheet, HEADERS.length);

  // 無論原本是否有不完整表頭,都統一修正為最新版欄位,避免舊版欄位不足造成寫入錯誤。
  sheet.getRange(1, 1, 1, HEADERS.length).setValues([HEADERS]);
  sheet.setFrozenRows(1);

  try {
    sheet.autoResizeColumns(1, HEADERS.length);
  } catch (err) {
    // autoResize 不是必要功能,避免因權限或環境問題中斷主流程。
  }
  return sheet;
}

function ensureSheetHasColumns_(sheet, requiredColumns) {
  const currentColumns = sheet.getMaxColumns();
  if (currentColumns < requiredColumns) {
    sheet.insertColumnsAfter(currentColumns, requiredColumns - currentColumns);
  }
}

function saveResult(payload) {
  payload = payload || {};
  const studentId = cleanText_(payload.studentId);
  const birthday = normalizeBirthday_(payload.birthday);
  const password = String(payload.password || '');

  if (!studentId) return { success: false, message: '請輸入學號。' };
  if (!birthday) return { success: false, message: '請輸入生日。' };
  if (password.length < 4) return { success: false, message: '查詢密碼至少需要 4 碼。' };
  if (!payload.reportText) return { success: false, message: '尚未產生試算報告,請先計算後再儲存。' };

  const sheet = getOrCreateDatabaseSheet_();
  ensureSheetHasColumns_(sheet, HEADERS.length);

  const now = new Date();
  const recordId = Utilities.getUuid();

  const row = [
    recordId,
    now,
    studentId,
    hashBirthday_(studentId, birthday),
    hashPassword_(studentId, birthday, password),
    cleanText_(payload.name),
    cleanText_(payload.className),
    cleanText_(payload.seatNo),
    cleanText_(payload.gradeStage),
    cleanText_(payload.group1Score),
    cleanText_(payload.group2Score),
    cleanText_(payload.group3Score),
    cleanText_(payload.totalScore),
    String(payload.reportText || ''),
    JSON.stringify(payload.inputJson || {}),
    JSON.stringify(payload.resultJson || {}),
    getUserEmailSafe_()
  ];

  const nextRow = Math.max(sheet.getLastRow() + 1, 2);
  sheet.getRange(nextRow, 1, 1, row.length).setValues([row]);

  return { success: true, message: '儲存成功', recordId: recordId };
}

function lookupResult(query) {
  query = query || {};
  const studentId = cleanText_(query.studentId);
  const birthday = normalizeBirthday_(query.birthday);
  const password = String(query.password || '');

  if (!studentId) return { success: false, message: '請輸入學號。' };
  if (!birthday) return { success: false, message: '請輸入生日。' };
  if (!password) return { success: false, message: '請輸入個人查詢密碼。' };

  const sheet = getOrCreateDatabaseSheet_();
  const values = sheet.getDataRange().getValues();
  if (values.length <= 1) return { success: false, message: '目前尚無已儲存的試算結果。' };

  const birthdayHash = hashBirthday_(studentId, birthday);
  const passwordHash = hashPassword_(studentId, birthday, password);
  const headers = values[0].map(String);

  // 從最後一筆往前找,所以若同一學生存了多次,會回傳最新一次。
  for (let i = values.length - 1; i >= 1; i--) {
    const row = values[i];
    const obj = rowToObject_(headers, row);
    if (
      cleanText_(obj.student_id) === studentId &&
      cleanText_(obj.birthday_hash) === birthdayHash &&
      cleanText_(obj.password_hash) === passwordHash
    ) {
      return {
        success: true,
        record: {
          recordId: cleanText_(obj.record_id),
          timestamp: formatDate_(obj.timestamp),
          studentId: cleanText_(obj.student_id),
          name: cleanText_(obj.name),
          className: cleanText_(obj.class_name),
          seatNo: cleanText_(obj.seat_no),
          gradeStage: cleanText_(obj.grade_stage),
          group1Score: cleanText_(obj.group1_score),
          group2Score: cleanText_(obj.group2_score),
          group3Score: cleanText_(obj.group3_score),
          totalScore: cleanText_(obj.total_score),
          reportText: String(obj.report_text || '')
        }
      };
    }
  }
  return { success: false, message: '查無符合的試算結果,請確認學號、生日與查詢密碼是否正確。' };
}

/**
 * 可用來測試後端是否能正常寫入與讀取。
 * 在 Apps Script 執行 testSaveAndLookup_,如果看到 success: true 就代表後端與資料表正常。
 */
function testSaveAndLookup_() {
  const payload = {
    studentId: 'TEST001',
    birthday: '2011-01-01',
    password: '1234',
    name: '測試學生',
    className: '301',
    seatNo: '01',
    gradeStage: '國三下/會考前',
    group1Score: '20',
    group2Score: '35',
    group3Score: '0',
    totalScore: '55 分',
    reportText: '這是一筆 Apps Script 後端測試資料。',
    inputJson: { demo: true },
    resultJson: { demo: true }
  };
  const save = saveResult(payload);
  const lookup = lookupResult({ studentId: 'TEST001', birthday: '2011-01-01', password: '1234' });
  return { save: save, lookup: lookup };
}

function rowToObject_(headers, row) {
  const obj = {};
  headers.forEach(function(header, index) {
    obj[header] = row[index];
  });
  return obj;
}

function cleanText_(value) {
  return String(value == null ? '' : value).trim();
}

function normalizeBirthday_(value) {
  return String(value || '').replace(/[^0-9]/g, '');
}

function hashBirthday_(studentId, birthday) {
  return sha256_(studentId + '|' + birthday + '|' + SECRET_SALT + '|birthday');
}

function hashPassword_(studentId, birthday, password) {
  return sha256_(studentId + '|' + birthday + '|' + password + '|' + SECRET_SALT + '|password');
}

function sha256_(text) {
  const bytes = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, text, Utilities.Charset.UTF_8);
  return bytes.map(function(b) {
    const v = b < 0 ? b + 256 : b;
    return ('0' + v.toString(16)).slice(-2);
  }).join('');
}

function formatDate_(value) {
  if (Object.prototype.toString.call(value) === '[object Date]') {
    return Utilities.formatDate(value, 'Asia/Taipei', 'yyyy/MM/dd HH:mm:ss');
  }
  return String(value || '');
}

function getUserEmailSafe_() {
  try {
    return Session.getActiveUser().getEmail() || '';
  } catch (err) {
    return '';
  }
}
貼到 Apps Script 的 Code.gs 檔案

5. 貼上前端(index.html)

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

index.html(前端)
<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>115學年度竹苗區免試入學積分小工具</title>
    <style>
        :root {
            --bg: #f7f3ec;
            --paper: #ffffff;
            --ink: #2f2a26;
            --muted: #766e66;
            --coffee: #6f4e37;
            --coffee-dark: #4f3625;
            --coffee-soft: #efe3d6;
            --blue: #2f6f9f;
            --blue-soft: #eaf4fb;
            --green: #2f8f5b;
            --green-soft: #e9f6ef;
            --orange-soft: #fff5e6;
            --line: #e2d8cc;
            --shadow: 0 12px 30px rgba(67, 47, 32, .10);
        }
        html, body {
            width: 100%;
            min-height: 100%;
            margin: 0;
            overflow-x: hidden;
            scroll-behavior: smooth;
        }
        body {
            font-family: 'Helvetica Neue', Arial, 'Noto Sans TC', sans-serif;
            background: linear-gradient(180deg, #f7f3ec 0%, #eef5f3 100%);
            color: var(--ink);
            line-height: 1.6;
            padding: 0;
            margin: 0;
        }
        .container {
            width: 100%;
            max-width: 1160px;
            margin: 0 auto;
            background: transparent;
            padding: 14px;
            border-radius: 0;
            box-shadow: none;
            box-sizing: border-box;
        }
        .hero {
            background: radial-gradient(circle at top left, #fff8ef 0%, #fff 44%, #eef7f4 100%);
            border: 1px solid var(--line);
            border-radius: 22px;
            padding: 22px 18px;
            margin-bottom: 14px;
            box-shadow: var(--shadow);
            overflow: hidden;
            position: relative;
        }
        .hero::after {
            content: '';
            position: absolute;
            width: 210px;
            height: 210px;
            right: -90px;
            top: -90px;
            background: rgba(111, 78, 55, .10);
            border-radius: 50%;
        }
        .hero-kicker {
            display: inline-flex;
            gap: 6px;
            align-items: center;
            padding: 5px 10px;
            border-radius: 999px;
            background: var(--coffee-soft);
            color: var(--coffee-dark);
            font-weight: 800;
            font-size: .86em;
            margin-bottom: 8px;
        }
        h1 {
            margin: 0;
            color: #263d50;
            font-size: clamp(1.65rem, 4vw, 2.5rem);
            letter-spacing: .02em;
            line-height: 1.25;
        }
        .hero-subtitle {
            margin: 8px 0 0 0;
            color: var(--muted);
            font-weight: 700;
            font-size: 1em;
        }
        .flow-cards {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 10px;
            margin-top: 18px;
        }
        .flow-card {
            background: rgba(255,255,255,.86);
            border: 1px solid var(--line);
            border-radius: 16px;
            padding: 12px;
            min-height: 78px;
            box-sizing: border-box;
        }
        .flow-card strong {
            display: block;
            color: var(--coffee-dark);
            font-size: .98em;
            margin-bottom: 3px;
        }
        .flow-card span {
            color: var(--muted);
            font-size: .85em;
            line-height: 1.35;
        }
        .section {
            background: var(--paper);
            padding: 16px;
            margin-bottom: 16px;
            border-radius: 18px;
            border: 1px solid var(--line);
            border-left: 7px solid var(--coffee);
            box-sizing: border-box;
            box-shadow: 0 6px 18px rgba(67, 47, 32, .07);
        }
        .section-title {
            font-weight: 900;
            font-size: 1.12em;
            margin-bottom: 10px;
            color: var(--blue);
            display: flex;
            align-items: center;
            gap: 8px;
            line-height: 1.35;
        }
        .section-title::before {
            content: '';
            width: 9px;
            height: 22px;
            background: currentColor;
            border-radius: 99px;
            opacity: .42;
        }
        .main-score-section-title {
            color: var(--coffee);
            font-weight: 900;
            font-size: 1.16em;
        }
        .section-helper {
            margin: 0 0 12px 0;
            padding: 10px 12px;
            background: var(--orange-soft);
            border: 1px dashed #d9a86b;
            border-radius: 12px;
            color: #74512f;
            font-size: .9em;
            font-weight: 700;
            line-height: 1.5;
        }
        .section-desc {
            font-size: 0.85em;
            color: #e74c3c;
            margin-top: -5px;
            margin-bottom: 12px;
            font-weight: bold;
            background: #fff;
            padding: 8px;
            border-radius: 4px;
            border: 1px dashed #e74c3c;
            line-height: 1.4;
        }
        label {
            display: block;
            margin-top: 12px;
            font-weight: bold;
            font-size: 0.95em;
        }
        select, input[type="text"], input[type="number"], input[type="date"] {
            width: 100%;
            padding: 10px;
            margin-top: 5px;
            border: 1px solid #ccc;
            border-radius: 5px;
            font-size: 0.95em;
            box-sizing: border-box;
            background: #fff;
        }
        select:disabled, input:disabled {
            background-color: #f0f0f0;
            color: #7f8c8d;
            cursor: not-allowed;
        }
        .grade-status-box {
            margin-top: 10px;
            padding: 10px;
            border-radius: 6px;
            background: #fff;
            border: 1px dashed #9b59b6;
            color: #6c3483;
            font-size: 0.9em;
            font-weight: bold;
            line-height: 1.5;
        }
        .field-locked-note {
            display: block;
            font-size: 0.76em;
            color: #999;
            font-weight: bold;
            margin-top: 3px;
        }
        small {
            display: block;
            margin-top: 4px;
            line-height: 1.4;
        }
        .query-section {
            background: var(--blue-soft);
            border: 1px dashed #7bb8df;
            border-left: 7px solid var(--blue);
            padding: 13px;
            border-radius: 16px;
            margin-bottom: 16px;
            font-size: 0.92em;
            font-weight: bold;
            box-sizing: border-box;
        }
        .query-section a {
            color: #2196f3;
            text-decoration: underline;
        }
        .query-section a:hover { color: #0b7dda; }
        .user-info-grid {
            display: flex;
            flex-direction: column;
            gap: 10px;
        }
        .table-responsive {
            width: 100%;
            overflow-x: auto;
            -webkit-overflow-scrolling: touch;
            margin-top: 8px;
            border-radius: 14px;
            border: 1px solid var(--line);
            background: #fff;
        }
        .table-hint {
            display: none;
            margin-top: 6px;
            color: var(--muted);
            font-size: .8em;
            font-weight: 700;
        }
        .calc-table {
            width: 100%;
            min-width: 620px;
            border-collapse: collapse;
            background: #fff;
            font-size: 0.9em;
        }
        .calc-table th, .calc-table td {
            border: 1px solid #ddd;
            padding: 8px 4px;
            text-align: center;
            vertical-align: middle;
        }
        .calc-table th {
            background-color: #3f4f5d;
            color: white;
            font-size: 0.95em;
            white-space: nowrap;
        }
        .calc-table td.left-align {
            text-align: left;
            font-weight: bold;
            background: #f9f9f9;
            padding-left: 8px;
            width: 120px;
        }
        .calc-table input[type="checkbox"] {
            transform: scale(1.15);
            cursor: pointer;
        }
        .calc-table input[type="number"] {
            width: 68px;
            padding: 5px;
            margin: 0;
            text-align: center;
            font-size: 0.9em;
        }
        .reward-table input[type="number"] { width: 72px; }
        .semester-cell {
            display: flex;
            flex-direction: column;
            gap: 5px;
            align-items: center;
            justify-content: center;
        }
        .exam-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
            gap: 10px;
        }
        .hour-text {
            display: block;
            font-size: 0.8em;
            color: #555;
            margin-top: 3px;
        }
        button {
            display: block;
            width: 100%;
            padding: 14px;
            background: var(--green);
            color: #fff;
            border: none;
            border-radius: 14px;
            font-size: 1.08em;
            cursor: pointer;
            font-weight: 900;
            margin-top: 18px;
            box-shadow: 0 8px 18px rgba(47, 143, 91, .20);
        }
        button:hover { filter: brightness(1.05); }
        .btn-primary-calc {
            font-size: 1.18em;
            padding: 16px;
            border-radius: 18px;
            background: linear-gradient(135deg, #2f8f5b 0%, #25764a 100%);
        }
        .action-btn-group {
            display: block;
            margin-top: 15px;
        }
        .btn-copy { background: #2980b9; margin: 0; font-size: 1.1em; padding: 12px; }
        .btn-copy:hover { background: #3498db; }
        .result-box {
            margin-top: 15px;
            display: none;
            box-sizing: border-box;
        }
        .report-card {
            background-color: #fff;
            color: #333;
            padding: 20px;
            border: 2px solid #2c3e50;
            border-radius: 8px;
            box-sizing: border-box;
        }
        .report-title {
            text-align: center;
            color: #2c3e50;
            border-bottom: 2px solid #2c3e50;
            padding-bottom: 10px;
            margin-top: 0;
            font-size: 1.4em;
        }
        .student-info-badge {
            background: #f8f9fa;
            padding: 10px;
            border-radius: 5px;
            margin: 15px 0;
            font-weight: bold;
            font-size: 0.95em;
            border-left: 5px solid #9b59b6;
        }
        .report-sub-title {
            color: #2980b9;
            border-bottom: 1px solid #ddd;
            padding-bottom: 3px;
            margin-top: 15px;
            margin-bottom: 8px;
            font-size: 1.1em;
        }
        .report-item-detail {
            font-size: 0.92em;
            margin: 5px 0;
            padding-left: 10px;
            line-height: 1.5;
        }
        .report-formula-desc {
            font-size: 0.85em;
            color: #7f8c8d;
            font-style: italic;
            display: block;
            margin-top: 1px;
            margin-bottom: 5px;
        }
        .report-total-badge {
            text-align: center;
            margin: 20px 0;
            padding: 12px;
            background-color: #fff3cd;
            border: 1px solid #ffeeba;
            border-radius: 6px;
        }
        .report-total-badge .score {
            font-size: 1.8em;
            font-weight: bold;
            color: #c0392b;
            display: block;
            margin-top: 5px;
        }
        .shortage-list {
            margin-top: 5px;
            padding-left: 20px;
            color: #c0392b;
            font-size: 0.9em;
        }
        .shortage-list li {
            margin-bottom: 4px;
            font-weight: bold;
        }
        .validation-warning {
            display: none;
            margin-top: 12px;
            padding: 10px;
            background: #fff3cd;
            border: 1px solid #ffeeba;
            border-radius: 6px;
            color: #856404;
            font-size: 0.9em;
            line-height: 1.5;
            box-sizing: border-box;
        }
        .validation-warning ul {
            margin: 6px 0 0 18px;
            padding: 0;
        }
        .is-auto-fixed {
            outline: 2px solid #f39c12;
            background-color: #fff8e1;
        }
        .muted-note {
            font-size:0.85em;
            color:#666;
            margin: 4px 0;
            line-height:1.5;
        }
        .blue-note {
            color:#2980b9;
            font-weight:bold;
        }
        .red-note {
            color:#c0392b;
            font-weight:bold;
        }

        .input-card-grid {
            display: grid;
            grid-template-columns: repeat(2, minmax(0, 1fr));
            gap: 12px;
            margin-top: 6px;
        }
        .input-card {
            background: #fbfaf7;
            border: 1px solid var(--line);
            border-radius: 14px;
            padding: 12px;
            box-sizing: border-box;
        }
        .input-card label { margin-top: 0; }
        .stage-panel {
            margin-top: 12px;
            padding: 14px;
            background: #fbf6ef;
            border: 1px solid #dec8b2;
            border-radius: 16px;
        }
        .stage-panel label { margin-top: 0; }
        .action-box {
            background: linear-gradient(135deg, #ffffff 0%, #eef8f2 100%);
            border: 1px solid #bfdec9;
            border-radius: 18px;
            padding: 14px;
            margin: 16px 0;
            box-shadow: 0 8px 18px rgba(47, 143, 91, .10);
        }
        .action-box-title {
            color: #1f6f45;
            font-weight: 900;
            margin-bottom: 4px;
            font-size: 1.06em;
        }
        .action-box-desc {
            color: var(--muted);
            font-size: .9em;
            font-weight: 700;
        }
        .save-steps {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 8px;
            margin: 10px 0 4px 0;
        }
        .save-step {
            background: #fff;
            border: 1px solid var(--line);
            border-radius: 12px;
            padding: 8px;
            color: var(--muted);
            font-size: .82em;
            font-weight: 800;
            text-align: center;
        }
        .top-tools {
            display: flex;
            gap: 8px;
            justify-content: center;
            flex-wrap: wrap;
            margin: 12px 0 2px 0;
        }
        .top-tools a {
            display: inline-block;
            padding: 9px 14px;
            border-radius: 999px;
            background: #ffffff;
            border: 1px solid var(--line);
            color: var(--coffee-dark);
            text-decoration: none;
            font-weight: 900;
            box-shadow: 0 4px 12px rgba(67,47,32,.06);
        }
        .top-tools a.secondary { color: var(--blue); }
        .muted-note { color: var(--muted); }
        .blue-note { color: var(--blue); font-weight: 900; }


        .site-footer {
            text-align: center;
            color: var(--muted);
            font-size: 0.86em;
            line-height: 1.7;
            margin: 22px 0 8px;
            padding: 14px 10px;
            border-top: 1px solid var(--line);
        }
        .site-footer strong {
            color: var(--coffee-dark);
            font-weight: 900;
        }

        @media (min-width: 480px) {
            body { padding: 0; }
            .container { padding: 20px; margin: 0 auto; }
            h2 { font-size: 1.75em; }
            .user-info-grid { flex-direction: row; }
            .user-info-grid > div { flex: 1; }
            .calc-table { font-size: 0.95em; }
            .calc-table td.left-align { width: 130px; padding-left: 10px; }
            .calc-table input[type="number"] { width: 72px; padding: 6px; }
            .reward-table input[type="number"] { width: 82px; }
        }

        .top-tools {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
            align-items: center;
            justify-content: center;
            margin: -5px 0 18px 0;
        }
        .top-tools a {
            display: inline-block;
            padding: 9px 14px;
            border-radius: 999px;
            background: #6b4f3a;
            color: #fff;
            text-decoration: none;
            font-weight: bold;
            font-size: 0.92em;
        }
        .top-tools a.secondary {
            background: #2980b9;
        }
        .save-panel {
            margin-top: 14px;
            padding: 16px;
            border-radius: 10px;
            background: #fffaf3;
            border: 1px solid #d7b98e;
            box-sizing: border-box;
        }
        .save-panel-title {
            font-weight: 800;
            color: #6b4f3a;
            font-size: 1.05em;
            margin-bottom: 6px;
        }
        .save-panel-desc {
            font-size: 0.88em;
            color: #555;
            line-height: 1.5;
            margin-bottom: 8px;
        }
        .save-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
            gap: 10px;
            align-items: end;
        }
        .btn-save-result {
            background: #6b4f3a;
            margin-top: 12px;
        }
        .btn-save-result:hover { background: #7a5a42; }
        .save-status {
            display: none;
            margin-top: 10px;
            padding: 10px;
            border-radius: 6px;
            font-size: 0.9em;
            line-height: 1.5;
        }
        .save-status.ok { display: block; background: #e8f5e9; color: #1b5e20; border: 1px solid #a5d6a7; }
        .save-status.err { display: block; background: #ffebee; color: #b71c1c; border: 1px solid #ef9a9a; }

    
    :root {
      --bg: #f7f3ec;
      --paper: #ffffff;
      --ink: #2f2a26;
      --muted: #766e66;
      --coffee: #6f4e37;
      --coffee-dark: #4f3625;
      --coffee-soft: #efe3d6;
      --blue: #2f6f9f;
      --blue-soft: #eaf4fb;
      --green: #2f8f5b;
      --green-soft: #e9f6ef;
      --red: #c0392b;
      --red-soft: #fff0ed;
      --orange-soft: #fff5e6;
      --line: #e2d8cc;
      --shadow: 0 12px 30px rgba(67, 47, 32, .10);
    }
    html, body {
      width: 100%;
      min-height: 100%;
      margin: 0;
      overflow-x: hidden;
      scroll-behavior: smooth;
    }
    body {
      font-family: 'Helvetica Neue', Arial, 'Noto Sans TC', sans-serif;
      background:
        radial-gradient(circle at top left, rgba(111, 78, 55, .10), transparent 32%),
        linear-gradient(180deg, #faf7f2 0%, var(--bg) 100%);
      color: var(--ink);
      line-height: 1.6;
    }
    .container {
      width: 100%;
      max-width: 1080px;
      margin: 0 auto;
      padding: 14px;
      box-sizing: border-box;
    }
    .hero {
      background: linear-gradient(135deg, #fffaf3 0%, #f1e3d3 100%);
      border: 1px solid var(--line);
      border-radius: 22px;
      padding: 24px 18px;
      box-shadow: var(--shadow);
      margin: 10px 0 16px;
      position: relative;
      overflow: hidden;
    }
    .hero::after {
      content: "";
      position: absolute;
      right: -50px;
      top: -50px;
      width: 170px;
      height: 170px;
      border-radius: 50%;
      background: rgba(47, 111, 159, .12);
    }
    .eyebrow {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      padding: 6px 12px;
      border-radius: 999px;
      background: rgba(111, 78, 55, .12);
      color: var(--coffee-dark);
      font-size: .9em;
      font-weight: 800;
      margin-bottom: 10px;
    }
    h1 {
      margin: 0;
      color: var(--coffee-dark);
      font-size: 1.7em;
      line-height: 1.25;
      letter-spacing: .02em;
    }
    .hero-subtitle {
      color: var(--muted);
      margin: 10px 0 0;
      font-size: .98em;
      max-width: 760px;
    }
    .top-actions {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
      margin-top: 18px;
      position: relative;
      z-index: 1;
    }
    .pill-link {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 6px;
      padding: 10px 14px;
      border-radius: 999px;
      color: #fff;
      background: var(--blue);
      text-decoration: none;
      font-weight: 800;
      box-shadow: 0 8px 18px rgba(47, 111, 159, .18);
    }
    .pill-link.secondary {
      color: var(--coffee-dark);
      background: #fff;
      border: 1px solid var(--line);
      box-shadow: none;
    }
    .steps {
      display: grid;
      grid-template-columns: 1fr;
      gap: 10px;
      margin-bottom: 16px;
    }
    .step-card {
      background: var(--paper);
      border: 1px solid var(--line);
      border-radius: 16px;
      padding: 14px;
      box-shadow: 0 6px 16px rgba(67, 47, 32, .06);
      display: flex;
      gap: 12px;
      align-items: flex-start;
    }
    .step-num {
      flex: 0 0 34px;
      width: 34px;
      height: 34px;
      border-radius: 50%;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      background: var(--coffee);
      color: #fff;
      font-weight: 900;
    }
    .step-card strong {
      display: block;
      color: var(--coffee-dark);
      margin-bottom: 2px;
    }
    .step-card span {
      display: block;
      color: var(--muted);
      font-size: .9em;
    }
    .section {
      background: var(--paper);
      border: 1px solid var(--line);
      border-radius: 18px;
      padding: 18px;
      box-shadow: var(--shadow);
      margin-bottom: 16px;
      box-sizing: border-box;
    }
    .section-title {
      display: flex;
      align-items: center;
      gap: 10px;
      font-weight: 900;
      color: var(--coffee-dark);
      font-size: 1.15em;
      margin-bottom: 8px;
    }
    .section-title .icon {
      width: 36px;
      height: 36px;
      border-radius: 12px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      background: var(--coffee-soft);
    }
    .section-desc {
      margin: 0 0 14px;
      color: var(--muted);
      font-size: .92em;
    }
    .form-grid {
      display: grid;
      grid-template-columns: 1fr;
      gap: 12px;
    }
    .field-card {
      background: #fffaf6;
      border: 1px solid #eaded0;
      border-radius: 14px;
      padding: 12px;
      box-sizing: border-box;
    }
    label {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 8px;
      font-weight: 900;
      color: var(--coffee-dark);
      margin-bottom: 6px;
      font-size: .95em;
    }
    .hint {
      display: block;
      color: var(--muted);
      font-size: .82em;
      font-weight: 500;
      margin-top: 6px;
    }
    input {
      width: 100%;
      padding: 12px;
      border: 1px solid #d7c9ba;
      border-radius: 10px;
      box-sizing: border-box;
      font-size: 1em;
      background: #fff;
      color: var(--ink);
    }
    input:focus {
      outline: 3px solid rgba(47, 111, 159, .15);
      border-color: var(--blue);
    }
    .primary-action {
      background: linear-gradient(135deg, var(--green) 0%, #2d9f67 100%);
      color: #fff;
      border: none;
      width: 100%;
      padding: 14px;
      border-radius: 12px;
      font-size: 1.08em;
      font-weight: 900;
      cursor: pointer;
      margin-top: 14px;
      box-shadow: 0 10px 22px rgba(47, 143, 91, .22);
    }
    .primary-action:hover { filter: brightness(1.03); }
    .notice {
      margin-top: 12px;
      padding: 12px;
      border-radius: 12px;
      background: var(--orange-soft);
      border: 1px dashed #d2a36b;
      color: #77512a;
      font-size: .9em;
    }
    .privacy-note {
      margin-top: 10px;
      padding: 12px;
      border-radius: 12px;
      background: var(--blue-soft);
      border: 1px solid #c7e0f1;
      color: #295b7e;
      font-size: .9em;
    }
    .status {
      display: none;
      margin-top: 12px;
      padding: 12px;
      border-radius: 12px;
      font-weight: 800;
      line-height: 1.5;
    }
    .status.ok {
      display: block;
      background: var(--green-soft);
      color: #1f6a43;
      border: 1px solid #bfe4ce;
    }
    .status.err {
      display: block;
      background: var(--red-soft);
      color: var(--red);
      border: 1px solid #f0b8ae;
    }
    /* 查詢頁報告卡片:限定 lookupPage,避免蓋掉計算機頁的試算報告 */
    #lookupPage .report-card {
      display: none;
      background: #fff;
      border: 2px solid var(--coffee-dark);
      border-radius: 18px;
      padding: 18px;
      margin-top: 16px;
      box-shadow: var(--shadow);
      overflow: hidden;
    }
    .report-header {
      display: grid;
      grid-template-columns: 1fr;
      gap: 10px;
      margin-bottom: 14px;
    }
    .badge {
      background: #f8f3ed;
      border-left: 6px solid var(--coffee);
      padding: 12px;
      border-radius: 12px;
      font-weight: 900;
      color: var(--coffee-dark);
      line-height: 1.55;
    }
    .mini-summary {
      display: grid;
      grid-template-columns: 1fr;
      gap: 10px;
      margin-bottom: 14px;
    }
    .score-chip {
      background: #fdfaf6;
      border: 1px solid var(--line);
      border-radius: 14px;
      padding: 12px;
    }
    .score-chip .label {
      color: var(--muted);
      font-size: .86em;
      font-weight: 800;
    }
    .score-chip .value {
      color: var(--coffee-dark);
      font-size: 1.25em;
      font-weight: 900;
      margin-top: 2px;
    }
    .report-title {
      color: var(--coffee-dark);
      font-size: 1.15em;
      font-weight: 900;
      border-bottom: 1px solid var(--line);
      padding-bottom: 8px;
      margin-bottom: 10px;
    }
    pre {
      white-space: pre-wrap;
      word-break: break-word;
      background: #fffdf9;
      border: 1px solid var(--line);
      padding: 14px;
      border-radius: 12px;
      font-family: inherit;
      font-size: .95em;
      line-height: 1.65;
      max-height: none;
      overflow: auto;
      box-sizing: border-box;
    }
    .footer-tip {
      text-align: center;
      color: var(--muted);
      font-size: .86em;
      line-height: 1.7;
      margin: 18px 0 8px;
      padding: 14px 10px;
      border-top: 1px solid var(--line);
    }
    .footer-tip strong {
      color: var(--coffee-dark);
      font-weight: 900;
    }
    @media (min-width: 640px) {
      .container { padding: 20px; }
      .hero { padding: 30px; }
      h1 { font-size: 2.1em; }
      .steps { grid-template-columns: repeat(3, 1fr); }
      .form-grid { grid-template-columns: repeat(3, 1fr); }
      .report-header { grid-template-columns: 1fr auto; align-items: center; }
      .mini-summary { grid-template-columns: repeat(4, 1fr); }
    }
    @media (max-width: 420px) {
      .hero { border-radius: 16px; padding: 20px 14px; }
      h1 { font-size: 1.45em; }
      .top-actions { flex-direction: column; }
      .pill-link { width: 100%; box-sizing: border-box; }
      .section { padding: 14px; border-radius: 16px; }
      .step-card { padding: 12px; }
      .mini-summary { grid-template-columns: 1fr 1fr; }
    }
  

        /* 單一 HTML 分頁切換 */
        .app-nav {
            width: 100%;
            max-width: 1080px;
            margin: 0 auto;
            padding: 12px 14px 0;
            box-sizing: border-box;
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
        }
        .app-nav button {
            flex: 1;
            min-width: 160px;
            border: 1px solid var(--line);
            background: rgba(255,255,255,.88);
            color: var(--coffee-dark);
            border-radius: 999px;
            padding: 11px 14px;
            font-size: .95em;
            font-weight: 900;
            cursor: pointer;
            box-shadow: 0 6px 16px rgba(67, 47, 32, .08);
            margin: 0;
        }
        .app-nav button.active {
            background: var(--coffee);
            color: #fff;
            border-color: var(--coffee);
        }
        .app-page[hidden] { display: none !important; }
        #lookupPage .top-actions { display:flex; gap:10px; flex-wrap:wrap; margin-top:14px; }

    
    /* 計算機頁試算報告:確保按下開始計算後可正常顯示 */
    #calculatorPage #resultBox .report-card {
      display: block;
    }

</style>
</head>
<body>

  <nav class="app-nav" aria-label="工具分頁">
    <button id="tabCalculator" class="active" type="button" onclick="showPage('calculator')">🧮 積分計算機</button>
    <button id="tabLookup" type="button" onclick="showPage('lookup')">🔎 個人結果查詢</button>
  </nav>

<main id="calculatorPage" class="app-page">


<div class="container">
    <div class="hero">
        <div class="hero-kicker">🎓 115 學年度|竹苗區免試入學</div>
        <h1>免試入學積分計算機</h1>
        <p class="hero-subtitle">看懂規則、掌握現況、規劃下一步。適合國一下、國二、國三學生依目前階段試算。</p>
        <div class="flow-cards" aria-label="操作流程">
            <div class="flow-card"><strong>① 選階段</strong><span>先選目前年級,系統只開放已完成學期。</span></div>
            <div class="flow-card"><strong>② 填資料</strong><span>依成績單、獎懲、服務時數輸入資料。</span></div>
            <div class="flow-card"><strong>③ 算分並儲存</strong><span>產生報告後,可用學號、生日與密碼保存查詢。</span></div>
        </div>
    </div>
    <div class="top-tools">
        <a href="javascript:void(0)" onclick="showPage('lookup')">🔎 查詢已儲存結果</a>
        <a class="secondary" href="javascript:void(0)" onclick="showPage('calculator')">🧮 回到計算機</a>
    </div>

    <div class="section" style="border-left-color: #8e44ad;">
        <div class="section-title" style="color: #8e44ad;">學生基本資料與試算階段</div>
        <p class="section-helper">先完成這一區,下面的成績、曠課、獎懲、服務學習欄位才會依照「已完成學期」自動開放。</p>
        <div class="stage-panel">
            <label>目前年級/試算階段</label>
            <select id="gradeStage">
                <option value="" selected>請先選擇年級後再開始填寫</option>
                <option value="1b">國一下(採計一上)</option>
                <option value="2a">國二上(採計一上、一下)</option>
                <option value="2b">國二下(採計一上至二上)</option>
                <option value="3a">國三上(採計一上至二下)</option>
                <option value="3b_pre">國三下/會考前(採計一上至三上)</option>
                <option value="3b_post">國三下/會考後或正式選填志願</option>
            </select>
            <div id="gradeNotice" class="grade-status-box">請先選擇目前年級/試算階段,系統會依剛開學時已完成學期自動開放欄位;尚未結算的學期不會列入計算。</div>
        </div>
        <div class="input-card-grid">
            <div class="input-card">
                <label>學生姓名</label>
                <input type="text" id="stu_name" placeholder="請輸入姓名">
            </div>
            <div class="input-card">
                <label>班級</label>
                <input type="text" id="stu_class" placeholder="例如:301">
            </div>
            <div class="input-card">
                <label>座號</label>
                <input type="number" id="stu_seat" placeholder="例如:05">
            </div>
            <div class="input-card">
                <label>資料用途</label>
                <div style="font-size:.9em; color:#766e66; font-weight:800; padding-top:8px;">基本資料只會用於報告顯示與儲存查詢,試算時請盡量與自己的資料一致。</div>
            </div>
        </div>
    </div>

    <div class="query-section">
        🔍 成績、獎懲與服務學習紀錄不確定?請先登入:<a href="https://pts.hc.edu.tw/" target="_blank" rel="noopener noreferrer">新竹市親師生平台</a> 進行查詢。<br>
        💡 請先選擇目前年級/試算階段;系統會依「剛開學時已完成的學期」開放欄位,尚未結算的學期會鎖定並排除於計算之外。
    </div>

    <div class="section">
        <div class="section-title main-score-section-title">一、均衡發展 (最高採計 30 分)</div>
        <p class="section-helper">這一區主要看「身分條件、就學區、志願序位、四個領域是否平均達 60 分」。請依目前資料填寫即可。</p>
        
        <label>1. 扶助弱勢</label>
        <select id="weak">
            <option value="0" selected>不符 (0分)</option>
            <option value="5">偏遠地區學生 或 經濟弱勢 (5分)</option>
            <option value="3">非山非市國中學生 (3分)</option>
        </select>

        <label>2. 就近入學 (竹苗區國中、共同/變更就學區學生)</label>
        <select id="local">
            <option value="5">符合 (5分)</option>
            <option value="0">不符合 (0分)</option>
        </select>

        <label>3. 志願序位積分</label>
        <select id="volunteer">
            <option value="0" selected>不採計:會考前積分試算 (0分)</option>
            <option value="10">第 1~5 志願序位積分 (10分)</option>
            <option value="9">第 6~10 志願序位積分 (9分)</option>
            <option value="8">第 11~15 志願序位積分 (8分)</option>
            <option value="7">第 16~20 志願序位積分 (7分)</option>
            <option value="6">第 21~25 志願序位積分 (6分)</option>
        </select>
        <small style="color: #e67e22; font-weight: bold;">*系統會依年級自動控制是否可選。國三會考後或正式選填志願時,才開放改選對應區間。</small>

        <label>4. 均衡學習 (請輸入已完成學期成績,系統依試算階段自動採計平均值)</label>
        <p class="muted-note">
            *各領域依「已採計學期」平均,平均達 60 分以上(含)即算該領域符合。四領域皆符合15分 / 三領域12分 / 二領域8分 / 一領域4分。<br>
            <span class="blue-note">💡【查詢路徑:親師生平台⮕畢業預警查詢】若各領域皆及格,成績輸入維持60分即可!</span>
        </p>
        
        <div class="table-hint">手機版表格可左右滑動查看完整欄位。</div>
        <div class="table-responsive">
            <table class="calc-table">
                <thead><tr><th>學習領域</th><th>一上</th><th>一下</th><th>二上</th><th>二下</th><th>三上</th></tr></thead>
                <tbody>
                    <tr>
                        <td class="left-align">健康與體育</td>
                        <td><div class="semester-cell"><input type="number" id="fit_1a" min="0" max="100" value="60"></div></td>
                        <td><div class="semester-cell"><input type="number" id="fit_1b" min="0" max="100" value="60"></div></td>
                        <td><div class="semester-cell"><input type="number" id="fit_2a" min="0" max="100" value="60"></div></td>
                        <td><div class="semester-cell"><input type="number" id="fit_2b" min="0" max="100" value="60"></div></td>
                        <td><div class="semester-cell"><input type="number" id="fit_3a" min="0" max="100" value="60"></div></td>
                    </tr>
                    <tr>
                        <td class="left-align">藝術</td>
                        <td><div class="semester-cell"><input type="number" id="art_1a" min="0" max="100" value="60"></div></td>
                        <td><div class="semester-cell"><input type="number" id="art_1b" min="0" max="100" value="60"></div></td>
                        <td><div class="semester-cell"><input type="number" id="art_2a" min="0" max="100" value="60"></div></td>
                        <td><div class="semester-cell"><input type="number" id="art_2b" min="0" max="100" value="60"></div></td>
                        <td><div class="semester-cell"><input type="number" id="art_3a" min="0" max="100" value="60"></div></td>
                    </tr>
                    <tr>
                        <td class="left-align">綜合活動</td>
                        <td><div class="semester-cell"><input type="number" id="mix_1a" min="0" max="100" value="60"></div></td>
                        <td><div class="semester-cell"><input type="number" id="mix_1b" min="0" max="100" value="60"></div></td>
                        <td><div class="semester-cell"><input type="number" id="mix_2a" min="0" max="100" value="60"></div></td>
                        <td><div class="semester-cell"><input type="number" id="mix_2b" min="0" max="100" value="60"></div></td>
                        <td><div class="semester-cell"><input type="number" id="mix_3a" min="0" max="100" value="60"></div></td>
                    </tr>
                    <tr>
                        <td class="left-align">科技</td>
                        <td><div class="semester-cell"><input type="number" id="tech_1a" min="0" max="100" value="60"></div></td>
                        <td><div class="semester-cell"><input type="number" id="tech_1b" min="0" max="100" value="60"></div></td>
                        <td><div class="semester-cell"><input type="number" id="tech_2a" min="0" max="100" value="60"></div></td>
                        <td><div class="semester-cell"><input type="number" id="tech_2b" min="0" max="100" value="60"></div></td>
                        <td><div class="semester-cell"><input type="number" id="tech_3a" min="0" max="100" value="60"></div></td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>

    <div class="section">
        <div class="section-title main-score-section-title">二、多元學習表現 (最高採計 40 分)</div>
        <p class="section-helper">這一區是最適合追蹤與補強的部分。系統會判斷是否仍可透過無曠課、服務學習、本土語言認證或獎懲紀錄補足到 40 分上限。</p>
        <div class="section-desc">※說明:多元學習表現原始總分 54 分,最高採計 40 分。【日常生活表現評量】、【無曠課紀錄的學期】、【功過相抵後的獎懲紀錄】採計國一上至國三下共六學期,國三下採計至當年度 4 月 20 日止;【服務學習】採計國一上至國三上五學期;【本土語言認證】屬於多元學習表現,最高 2 分。</div>
        
        <label>1. 日常生活表現評量</label>
        <select id="punish">
            <option value="10">功過相抵後或銷過後無懲罰紀錄 (10分)</option>
            <option value="0">否 (0分)</option>
        </select>

        <label>2. 無曠課紀錄的學期 (每學期 2 分,最高 12 分)</label>
        <div class="table-hint">手機版表格可左右滑動查看完整欄位。</div>
        <div class="table-responsive">
            <table class="calc-table" style="min-width: 420px;">
                <thead><tr><th>一上</th><th>一下</th><th>二上</th><th>二下</th><th>三上</th><th>三下</th></tr></thead>
                <tbody>
                    <tr>
                        <td><input type="checkbox" class="chk-absent" data-term="一上"></td>
                        <td><input type="checkbox" class="chk-absent" data-term="一下"></td>
                        <td><input type="checkbox" class="chk-absent" data-term="二上"></td>
                        <td><input type="checkbox" class="chk-absent" data-term="二下"></td>
                        <td><input type="checkbox" class="chk-absent" data-term="三上"></td>
                        <td><input type="checkbox" class="chk-absent" data-term="三下"></td>
                    </tr>
                </tbody>
            </table>
        </div>

        <label>3. 功過相抵後的獎懲紀錄 (分學期矩陣輸入,最高採計 20 分)</label>
        <div style="font-size:0.85em; color:#c0392b; font-weight:bold; background:#fff; border:1px dashed #c0392b; border-radius:4px; padding:8px; margin:5px 0 8px 0; line-height:1.4;">⚠️【查詢路徑:親師生平台⮕缺曠獎懲】輸入「功過相抵後」的累積獎勵次數!</div>
        
        <div class="table-hint">手機版表格可左右滑動查看完整欄位。</div>
        <div class="table-responsive">
            <table class="calc-table reward-table" style="min-width: 560px;">
                <thead>
                    <tr>
                        <th>學期別</th>
                        <th>大功數量 (每次4.5分)</th>
                        <th>小功數量 (每次1.5分)</th>
                        <th>嘉獎數量 (每次0.5分)</th>
                    </tr>
                </thead>
                <tbody>
                    <tr><td class="left-align">一上</td><td><input type="number" id="dg_1a" min="0" value="0"></td><td><input type="number" id="xg_1a" min="0" value="0"></td><td><input type="number" id="jj_1a" min="0" value="0"></td></tr>
                    <tr><td class="left-align">一下</td><td><input type="number" id="dg_1b" min="0" value="0"></td><td><input type="number" id="xg_1b" min="0" value="0"></td><td><input type="number" id="jj_1b" min="0" value="0"></td></tr>
                    <tr><td class="left-align">二上</td><td><input type="number" id="dg_2a" min="0" value="0"></td><td><input type="number" id="xg_2a" min="0" value="0"></td><td><input type="number" id="jj_2a" min="0" value="0"></td></tr>
                    <tr><td class="left-align">二下</td><td><input type="number" id="dg_2b" min="0" value="0"></td><td><input type="number" id="xg_2b" min="0" value="0"></td><td><input type="number" id="jj_2b" min="0" value="0"></td></tr>
                    <tr><td class="left-align">三上</td><td><input type="number" id="dg_3a" min="0" value="0"></td><td><input type="number" id="xg_3a" min="0" value="0"></td><td><input type="number" id="jj_3a" min="0" value="0"></td></tr>
                    <tr><td class="left-align">三下 (至4/20)</td><td><input type="number" id="dg_3b" min="0" value="0"></td><td><input type="number" id="xg_3b" min="0" value="0"></td><td><input type="number" id="jj_3b" min="0" value="0"></td></tr>
                </tbody>
            </table>
        </div>

        <label style="margin-top: 15px;">4. 服務學習時數 (前五學期,單期上限 2 分,總最高 10 分)</label>
        <div style="font-size:0.85em; color:#2980b9; font-weight:bold; background:#fff; border:1px dashed #2980b9; border-radius:4px; padding:8px; margin:5px 0 8px 0; line-height:1.4;">💡 請依實際服務學習時數輸入。每 3 小時折算 1 分,單一學期最高 2 分;前五學期總分最高 10 分。</div>
        <div class="table-responsive">
            <table class="calc-table" style="min-width: 420px;">
                <thead><tr><th>一上</th><th>一下</th><th>二上</th><th>二下</th><th>三上</th></tr></thead>
                <tbody>
                    <tr>
                        <td><input type="number" id="srv_1a" min="0" value="0"><br><span class="hour-text">小時</span></td>
                        <td><input type="number" id="srv_1b" min="0" value="0"><br><span class="hour-text">小時</span></td>
                        <td><input type="number" id="srv_2a" min="0" value="0"><br><span class="hour-text">小時</span></td>
                        <td><input type="number" id="srv_2b" min="0" value="0"><br><span class="hour-text">小時</span></td>
                        <td><input type="number" id="srv_3a" min="0" value="0"><br><span class="hour-text">小時</span></td>
                    </tr>
                </tbody>
            </table>
        </div>

        <label style="margin-top: 15px;">5. 本土語言認證 (最高 2 分)</label>
        <select id="localLang">
            <option value="0" selected>未取得或不採計 (0分)</option>
            <option value="2">已通過原住民族語、客語或閩南語初級以上認證 (2分)</option>
        </select>
        <small style="color: #666;">*本項屬於「多元學習表現」,獲得其中一種認證即採計 2 分,最多 2 分。</small>
    </div>

    <div class="section">
        <div class="section-title main-score-section-title">三、國中教育會考 (最高採計 30 分)</div>
        <p class="muted-note"><span class="blue-note">💡 系統會依年級自動控制:會考前維持不採計;國三會考後或正式選填階段才開放改選各科等級。</span></p>
        <div class="exam-grid">
            <div><label>國文科</label><select id="chinese" class="exam-select"><option value="0" selected>不採計:會考前積分試算 (0分)</option><option value="6">精熟 (6分)</option><option value="4">基礎 (4分)</option><option value="2">待加強 (2分)</option></select></div>
            <div><label>數學科</label><select id="math" class="exam-select"><option value="0" selected>不採計:會考前積分試算 (0分)</option><option value="6">精熟 (6分)</option><option value="4">基礎 (4分)</option><option value="2">待加強 (2分)</option></select></div>
            <div><label>英語科</label><select id="english" class="exam-select"><option value="0" selected>不採計:會考前積分試算 (0分)</option><option value="6">精熟 (6分)</option><option value="4">基礎 (4分)</option><option value="2">待加強 (2分)</option></select></div>
            <div><label>社會科</label><select id="social" class="exam-select"><option value="0" selected>不採計:會考前積分試算 (0分)</option><option value="6">精熟 (6分)</option><option value="4">基礎 (4分)</option><option value="2">待加強 (2分)</option></select></div>
            <div><label>自然科</label><select id="science" class="exam-select"><option value="0" selected>不採計:會考前積分試算 (0分)</option><option value="6">精熟 (6分)</option><option value="4">基礎 (4分)</option><option value="2">待加強 (2分)</option></select></div>
        </div>
        <label style="margin-top: 15px;">會考違規記點扣分</label>
        <select id="examViolation">
            <option value="0" selected>無違規記點 (不扣分)</option>
            <option value="1">記違規 1 點:扣會考總積分 0.3 分</option>
            <option value="2">記違規 2 點:扣會考總積分 0.6 分</option>
        </select>
        <small style="color:#666;">*違規記點僅扣國中教育會考總積分,不另行扣單科積分。寫作測驗級分與各科標示屬同分比序,不列入總積分計算。</small>
    </div>

    <div id="validationWarning" class="validation-warning" aria-live="polite"></div>

    <div class="action-box">
        <div class="action-box-title">準備好了嗎?</div>
        <div class="action-box-desc">確認資料輸入完畢後,點擊下方按鈕產生積分明細、補分策略與可儲存報告。</div>
        <button onclick="calculateScore()" class="btn-primary-calc">開始計算總積分</button>
    </div>

    <div id="resultBox" class="result-box">
        <div id="report-node" class="report-card">
            <h3 class="report-title">115學年度竹苗區免試入學積分試算報告</h3>
            <div id="studentDataBadge" class="student-info-badge"></div>
            <div class="report-sub-title">一、均衡發展積分明細 (得分:<span id="scoreGroup1">0</span> / 30 分)</div>
            <div id="detailGroup1"></div>
            <div class="report-sub-title">二、多元學習表現積分明細 (得分:<span id="scoreGroup2">0</span> / 40 分)</div>
            <div id="detailGroup2"></div>
            <div class="report-sub-title">三、國中教育會考積分明細 (得分:<span id="scoreGroup3">0</span> / 30 分)</div>
            <div id="detailGroup3"></div>
            <div id="shortageAlertArea"></div>
            <div class="report-total-badge">
                <strong>🌟 目前階段免試入學比序預估總積分 🌟</strong>
                <span id="reportTotalScore" class="score">0 分</span>
            </div>
        </div>
        <div class="action-btn-group">
            <button onclick="copyTextReport()" class="btn-copy">📋 複製精華報告</button>
        </div>
        <div class="save-panel">
            <div class="save-panel-title">💾 儲存我的試算結果</div>
            <div class="save-panel-desc">
                請先完成試算,再輸入學號、生日與個人查詢密碼。系統會將本次試算報告儲存;之後可到「查詢已儲存結果」頁面查詢。
            </div>
            <div class="save-steps">
                <div class="save-step">① 完成試算</div>
                <div class="save-step">② 設定查詢資料</div>
                <div class="save-step">③ 儲存並回查</div>
            </div>
            <div class="save-panel-desc"><strong>提醒:</strong>生日與查詢密碼後端會以雜湊方式儲存。</div>
            <div class="save-grid">
                <div>
                    <label>學號</label>
                    <input type="text" id="saveStudentId" placeholder="例如:1150001">
                </div>
                <div>
                    <label>生日</label>
                    <input type="date" id="saveBirthday">
                </div>
                <div>
                    <label>個人查詢密碼</label>
                    <input type="text" id="savePassword" placeholder="請自行設定,確認後再儲存" autocomplete="off">
                    <small style="color:#8a6d3b; font-weight:700;">請謹記查詢密碼,以免日後無法查詢。</small>
                </div>
            </div>
            <button onclick="saveCurrentResult()" class="btn-save-result">💾 儲存試算結果</button>
            <div id="saveStatus" class="save-status"></div>
        </div>
    </div>

    <div class="site-footer">
        <strong>竹苗區免試入學積分小工具|積分計算機頁</strong><br>
        新竹市立三民國中 楊翔紘老師 製作
    </div>
</div>


</main>
<main id="lookupPage" class="app-page" hidden>

  <div class="container">
    <section class="hero">
      <div class="eyebrow">🔐 個人試算結果查詢</div>
      <h1>我的免試入學積分查詢</h1>
      <p class="hero-subtitle">輸入你儲存試算結果時設定的「學號、生日、個人查詢密碼」,系統會找出最近一次儲存的積分報告。</p>
      <div class="top-actions">
        <a class="pill-link" href="javascript:void(0)" onclick="showPage('calculator')">🧮 回到積分計算機</a>
        <a class="pill-link secondary" href="#querySection">直接查詢 ↓</a>
      </div>
    </section>

    <section class="steps" aria-label="查詢步驟">
      <div class="step-card">
        <div class="step-num">1</div>
        <div><strong>準備查詢資料</strong><span>使用儲存時輸入的學號、生日與密碼。</span></div>
      </div>
      <div class="step-card">
        <div class="step-num">2</div>
        <div><strong>查詢最近紀錄</strong><span>系統會自動比對並取出最新一次試算。</span></div>
      </div>
      <div class="step-card">
        <div class="step-num">3</div>
        <div><strong>檢視積分報告</strong><span>確認分數明細與下一步補分提醒。</span></div>
      </div>
    </section>

    <section id="querySection" class="section">
      <div class="section-title"><span class="icon">🔎</span><span>查詢已儲存結果</span></div>
      <p class="section-desc">請輸入與儲存時完全相同的資料。若查不到,通常是學號、生日或個人查詢密碼其中一項不同。</p>

      <div class="form-grid">
        <div class="field-card">
          <label for="studentId">學號</label>
          <input type="text" id="studentId" placeholder="例如:S1150001" autocomplete="username">
          <span class="hint">請輸入你儲存試算結果時使用的學號。</span>
        </div>
        <div class="field-card">
          <label for="birthday">生日</label>
          <input type="date" id="birthday" autocomplete="bday">
          <span class="hint">請選擇儲存時輸入的生日。</span>
        </div>
        <div class="field-card">
          <label for="password">個人查詢密碼</label>
          <input type="text" id="password" placeholder="請輸入查詢密碼" autocomplete="off">
          <span class="hint">這不是 Google 密碼,是你自己在計算機頁設定的查詢密碼;文字會直接顯示,方便確認。</span>
        </div>
      </div>

      <button class="primary-action" onclick="lookupSavedResult()">查詢我的試算結果</button>

      <div class="notice">提醒:系統只會回傳與「學號、生日、查詢密碼」完全符合的最近一筆試算結果。</div>
      <div class="privacy-note">隱私說明:生日與查詢密碼不會以明碼儲存在試算表,後端會以雜湊形式保存。</div>
      <div id="status" class="status" aria-live="polite"></div>
    </section>

    <section id="reportCard" class="report-card">
      <div class="report-header">
        <div>
          <div class="report-title">📄 最近一次儲存的試算報告</div>
          <div id="recordBadge" class="badge"></div>
        </div>
      </div>

      <div class="mini-summary" id="summaryChips" style="display:none;">
        <div class="score-chip"><div class="label">均衡發展</div><div class="value" id="g1Chip">──</div></div>
        <div class="score-chip"><div class="label">多元學習表現</div><div class="value" id="g2Chip">──</div></div>
        <div class="score-chip"><div class="label">國中教育會考</div><div class="value" id="g3Chip">──</div></div>
        <div class="score-chip"><div class="label">目前總積分</div><div class="value" id="totalChip">──</div></div>
      </div>

      <pre id="reportText"></pre>
    </section>

    <div class="footer-tip"><strong>竹苗區免試入學積分小工具|個人結果查詢頁</strong><br>新竹市立三民國中 楊翔紘老師 製作</div>
  </div>

  
</main>
<script>

    function showPage(page) {
      var calcPage = document.getElementById('calculatorPage');
      var lookupPage = document.getElementById('lookupPage');
      var tabCalculator = document.getElementById('tabCalculator');
      var tabLookup = document.getElementById('tabLookup');
      var showLookup = page === 'lookup';
      if (calcPage) calcPage.hidden = showLookup;
      if (lookupPage) lookupPage.hidden = !showLookup;
      if (tabCalculator) tabCalculator.classList.toggle('active', !showLookup);
      if (tabLookup) tabLookup.classList.toggle('active', showLookup);
      window.scrollTo({ top: 0, behavior: 'smooth' });
    }


    let textReportContent = "";

    const fiveTermDefs = [
        { key: '1a', label: '一上', rank: 1 },
        { key: '1b', label: '一下', rank: 2 },
        { key: '2a', label: '二上', rank: 3 },
        { key: '2b', label: '二下', rank: 4 },
        { key: '3a', label: '三上', rank: 5 }
    ];
    const sixTermDefs = [
        { key: '1a', label: '一上', rank: 1 },
        { key: '1b', label: '一下', rank: 2 },
        { key: '2a', label: '二上', rank: 3 },
        { key: '2b', label: '二下', rank: 4 },
        { key: '3a', label: '三上', rank: 5 },
        { key: '3b', label: '三下', rank: 6 }
    ];
    const domainDefs = [
        { name: '健康與體育', prefix: 'fit' },
        { name: '藝術', prefix: 'art' },
        { name: '綜合活動', prefix: 'mix' },
        { name: '科技', prefix: 'tech' }
    ];
    const examDefs = [
        { id: 'chinese', label: '國文科' },
        { id: 'math', label: '數學科' },
        { id: 'english', label: '英語科' },
        { id: 'social', label: '社會科' },
        { id: 'science', label: '自然科' }
    ];

    const gradeStageConfigs = {
        '1b': { label: '國一下(採計一上)', termUntil: 1, serviceUntil: 1, examOpen: false, volunteerOpen: false },
        '2a': { label: '國二上(採計一上、一下)', termUntil: 2, serviceUntil: 2, examOpen: false, volunteerOpen: false },
        '2b': { label: '國二下(採計一上至二上)', termUntil: 3, serviceUntil: 3, examOpen: false, volunteerOpen: false },
        '3a': { label: '國三上(採計一上至二下)', termUntil: 4, serviceUntil: 4, examOpen: false, volunteerOpen: false },
        '3b_pre': { label: '國三下/會考前(採計一上至三上)', termUntil: 5, serviceUntil: 5, examOpen: false, volunteerOpen: false },
        '3b_post': { label: '國三下/會考後或正式選填志願', termUntil: 6, serviceUntil: 5, examOpen: true, volunteerOpen: true }
    };

    function getCurrentGradeConfig() {
        const stageEl = document.getElementById('gradeStage');
        const value = stageEl ? stageEl.value : '';
        return gradeStageConfigs[value] || null;
    }

    function getCurrentGradeLabel() {
        const config = getCurrentGradeConfig();
        return config ? config.label : '尚未選擇';
    }

    function escapeHTML(value) {
        return String(value).replace(/[&<>"']/g, function(ch) {
            return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ch];
        });
    }

    function formatNumber(value) {
        let num = Number(value) || 0;
        return Number.isInteger(num) ? String(num) : num.toFixed(1).replace(/\.0$/, "");
    }

    function getSelectedText(id) {
        const el = document.getElementById(id);
        if (!el || !el.options || el.selectedIndex < 0) return '';
        return el.options[el.selectedIndex].text;
    }

    function countChecked(className) {
        let checkboxes = document.querySelectorAll('.' + className);
        let count = 0;
        checkboxes.forEach(function(cb) { if (cb.checked) count++; });
        return count;
    }

    function calcSingleSemesterService(hours) {
        hours = Number(hours) || 0;
        return Math.min(2, Math.floor(hours / 3));
    }

    function markFixedField(el) {
        if (!el) return;
        el.classList.add('is-auto-fixed');
        setTimeout(function() { el.classList.remove('is-auto-fixed'); }, 2200);
    }

    function sanitizeNumber(id, label, options, warnings) {
        const el = document.getElementById(id);
        if (!el || el.disabled) return 0;
        const min = options.min ?? 0;
        const max = options.max ?? null;
        const defaultValue = options.defaultValue ?? 0;
        const integer = options.integer ?? false;

        let raw = String(el.value).trim();
        let num;
        let changed = false;

        if (raw === "") {
            num = defaultValue;
            changed = true;
            warnings.push(`${label} 空白,已自動改以 ${defaultValue} 計算。`);
        } else {
            num = Number(raw);
            if (!Number.isFinite(num)) {
                num = defaultValue;
                changed = true;
                warnings.push(`${label} 不是有效數字,已自動改以 ${defaultValue} 計算。`);
            }
        }
        if (integer && !Number.isInteger(num)) {
            num = Math.floor(num);
            changed = true;
            warnings.push(`${label} 應為整數,已自動無條件捨去為 ${num}。`);
        }
        if (num < min) { num = min; changed = true; warnings.push(`${label} 低於下限 ${min},已自動改為 ${min}。`); }
        if (max !== null && num > max) { num = max; changed = true; warnings.push(`${label} 超過上限 ${max},已自動改為 ${max}。`); }

        if (changed || String(el.value).trim() !== String(num)) {
            el.value = String(num);
            if (changed) markFixedField(el);
        }
        return num;
    }

    function sanitizeAllInputs() {
        const warnings = [];
        const seatEl = document.getElementById('stu_seat');
        if (seatEl && String(seatEl.value).trim() !== "") {
            sanitizeNumber('stu_seat', '座號', { min: 1, max: 99, defaultValue: 1, integer: true }, warnings);
        }

        domainDefs.forEach(function(domain) {
            fiveTermDefs.forEach(function(term) {
                const id = `${domain.prefix}_${term.key}`;
                sanitizeNumber(id, `${domain.name}${term.label}`, { min: 0, max: 100, defaultValue: 60 }, warnings);
            });
        });

        sixTermDefs.forEach(function(term) {
            ['dg', 'xg', 'jj'].forEach(function(type) {
                const labelMap = { dg: '大功', xg: '小功', jj: '嘉獎' };
                sanitizeNumber(`${type}_${term.key}`, `${term.label}${labelMap[type]}`, { min: 0, max: 99, defaultValue: 0, integer: true }, warnings);
            });
        });

        fiveTermDefs.forEach(function(term) {
            sanitizeNumber(`srv_${term.key}`, `服務學習${term.label}`, { min: 0, max: 999, defaultValue: 0 }, warnings);
        });

        showValidationWarnings(warnings);
        return warnings;
    }

    function showValidationWarnings(warnings) {
        const box = document.getElementById('validationWarning');
        if (!box) return;
        if (!warnings || warnings.length === 0) { box.style.display = 'none'; return; }
        const maxVisible = 6;
        const visibleItems = warnings.slice(0, maxVisible).map(function(msg) { return `<li>${escapeHTML(msg)}</li>`; }).join('');
        box.innerHTML = `⚠️ 系統已啟動輸入防呆,並自動修正以下欄位:<ul>${visibleItems}${warnings.length > maxVisible ? `<li>另有 ${warnings.length - maxVisible} 個欄位已一併修正。</li>` : ''}</ul>`;
        box.style.display = 'block';
    }

    function setNumberDisabled(id, disabled) {
        const el = document.getElementById(id);
        if (!el) return;
        el.disabled = disabled;
        if (disabled) el.classList.add('is-auto-fixed');
        else el.classList.remove('is-auto-fixed');
    }

    function updateGradeGating() {
        const config = getCurrentGradeConfig();
        const hasGrade = !!config;
        const notice = document.getElementById('gradeNotice');

        if (notice) {
            if (!hasGrade) {
                notice.innerHTML = '請先選擇目前年級/試算階段,系統會依剛開學時已完成學期自動開放欄位;尚未結算的學期不會列入計算。';
            } else {
                const termLabel = sixTermDefs.filter(function(term) { return term.rank <= config.termUntil; }).map(function(term) { return term.label; }).join('、') || '尚未開放';
                const serviceLabel = fiveTermDefs.filter(function(term) { return term.rank <= config.serviceUntil; }).map(function(term) { return term.label; }).join('、') || '尚未開放';
                notice.innerHTML = `目前階段:${config.label}。採計已完成學期:${termLabel}。服務學習開放:${serviceLabel}。${config.examOpen ? '會考與志願序位已開放。' : '會考與志願序位目前維持不採計。'}`;
            }
        }

        const volunteer = document.getElementById('volunteer');
        if (volunteer) {
            volunteer.disabled = !hasGrade || !config.volunteerOpen;
            if (volunteer.disabled) volunteer.value = '0';
        }
        examDefs.forEach(function(item) {
            const el = document.getElementById(item.id);
            if (!el) return;
            el.disabled = !hasGrade || !config.examOpen;
            if (el.disabled) el.value = '0';
        });

        const punish = document.getElementById('punish');
        if (punish) punish.disabled = !hasGrade;
        const localLang = document.getElementById('localLang');
        if (localLang) {
            localLang.disabled = !hasGrade;
            if (!hasGrade) localLang.value = '0';
        }
        const examViolation = document.getElementById('examViolation');
        if (examViolation) {
            examViolation.disabled = !hasGrade || !config.examOpen;
            if (examViolation.disabled) examViolation.value = '0';
        }
        domainDefs.forEach(function(domain) {
            fiveTermDefs.forEach(function(term) {
                const id = `${domain.prefix}_${term.key}`;
                const allowed = hasGrade && term.rank <= config.termUntil;
                setNumberDisabled(id, !allowed);
            });
        });

        document.querySelectorAll('.chk-absent').forEach(function(cb, index) {
            const term = sixTermDefs[index];
            const allowed = hasGrade && term && term.rank <= config.termUntil;
            cb.disabled = !allowed;
            if (!allowed) cb.checked = false;
        });

        sixTermDefs.forEach(function(term) {
            const allowed = hasGrade && term.rank <= config.termUntil;
            ['dg', 'xg', 'jj'].forEach(function(type) {
                setNumberDisabled(`${type}_${term.key}`, !allowed);
            });
        });

        fiveTermDefs.forEach(function(term) {
            const allowed = hasGrade && term.rank <= config.serviceUntil;
            setNumberDisabled(`srv_${term.key}`, !allowed);
        });
    }

    function setupGradeHandlers() {
        const gradeStage = document.getElementById('gradeStage');
        if (gradeStage) gradeStage.addEventListener('change', function() {
            updateGradeGating();
            document.getElementById('resultBox').style.display = 'none';
            textReportContent = '';
        });
        updateGradeGating();
    }

    function getDomainCalculation(domain) {
        let included = [];
        let excluded = [];
        let sum = 0;
        fiveTermDefs.forEach(function(term) {
            const id = `${domain.prefix}_${term.key}`;
            const el = document.getElementById(id);
            if (!el || el.disabled) {
                excluded.push(term.label);
            } else {
                const score = parseFloat(el.value) || 0;
                included.push({ label: term.label, score: score });
                sum += score;
            }
        });
        const count = included.length;
        const avg = count > 0 ? sum / count : 0;
        const passed = count > 0 && avg >= 60;
        const includedText = included.length ? included.map(function(item) { return `${item.label} ${formatNumber(item.score)}`; }).join('、') : '尚未採計任何學期';
        const excludedText = excluded.length ? excluded.join('、') : '無';
        return {
            name: domain.name,
            sum: sum,
            count: count,
            avg: avg,
            passed: passed,
            includedText: includedText,
            excludedText: excludedText,
            statusText: count === 0 ? '【未採計】' : (passed ? '【符合及格】' : '【不符合】')
        };
    }

    function getTermReward(prefix) {
        const dgEl = document.getElementById('dg_' + prefix);
        const xgEl = document.getElementById('xg_' + prefix);
        const jjEl = document.getElementById('jj_' + prefix);
        if (!dgEl || dgEl.disabled) return { dg: 0, xg: 0, jj: 0, score: 0, active: false };
        let dg = parseFloat(dgEl.value) || 0;
        let xg = parseFloat(xgEl.value) || 0;
        let jj = parseFloat(jjEl.value) || 0;
        return { dg: dg, xg: xg, jj: jj, score: (dg * 4.5) + (xg * 1.5) + (jj * 0.5), active: true };
    }

    function getExamLevelText(score) {
        if (score === 6) return '精熟';
        if (score === 4) return '基礎';
        if (score === 2) return '待加強';
        return '不採計';
    }

    function calculateScore() {
        const gradeConfig = getCurrentGradeConfig();
        if (!gradeConfig) {
            alert('請先選擇目前年級/試算階段,再開始計算。');
            const gradeEl = document.getElementById('gradeStage');
            if (gradeEl) gradeEl.focus();
            return;
        }
        updateGradeGating();
        sanitizeAllInputs();

        let name = document.getElementById('stu_name').value.trim();
        let sClass = document.getElementById('stu_class').value.trim();
        let seat = document.getElementById('stu_seat').value.trim();

        let safeName = escapeHTML(name);
        let safeClass = escapeHTML(sClass);
        let safeSeat = escapeHTML(seat);

        document.getElementById('studentDataBadge').innerHTML = `試算學生:${safeClass ? safeClass + ' 班 ' : '── 班 '}${safeSeat ? safeSeat + ' 號 ' : '── 號 '}${safeName ? '【' + safeName + '】' : '【一般學生】'}|目前階段:${escapeHTML(gradeConfig.label)}`;

        // 一、均衡發展 (最高採計 30 分)
        let weak = parseInt(document.getElementById('weak').value) || 0;
        let local = parseInt(document.getElementById('local').value) || 0;
        let volunteer = parseInt(document.getElementById('volunteer').value) || 0;
        let domainCalculations = domainDefs.map(getDomainCalculation);
        let passedDomains = domainCalculations.filter(function(item) { return item.passed; }).length;
        let domainScore = [0, 4, 8, 12, 15][passedDomains];
        let group1Raw = weak + local + volunteer + domainScore;
        let group1Score = Math.min(30, group1Raw);
        let domainDetailHTML = domainCalculations.map(function(item) {
            return `- ${item.name}:採計 ${item.includedText};排除 ${item.excludedText} → 平均 ${item.count ? item.avg.toFixed(1) : '未採計'} 分 ${item.statusText}`;
        }).join('<br>');

        document.getElementById('detailGroup1').innerHTML = `
            <div class="report-item-detail">• 扶助弱勢審查:${weak === 5 ? "符合偏遠/經濟弱勢 (+5分)" : (weak === 3 ? "符合非山非市 (+3分)" : "不符資格 (0分)")}</div>
            <div class="report-item-detail">• 就近入學審查:${local === 5 ? "符合竹苗就學區規範 (+5分)" : "不符 (0分)"}</div>
            <div class="report-item-detail">• 志願序位積分:${escapeHTML(getSelectedText('volunteer'))}</div>
            <div class="report-item-detail">• 均衡學習精算:共符合 <strong>${passedDomains}</strong> 個領域,核發 <strong>${domainScore}</strong> 分。</div>
            <span class="report-formula-desc">各領域有效學期平均精算過程:<br>${domainDetailHTML}<br>本大項原始加總 ${formatNumber(group1Raw)} 分,最高採計 30 分。</span>
        `;
        document.getElementById('scoreGroup1').innerText = formatNumber(group1Score);

        // 二、多元學習表現 (最高採計 40 分)
        let punish = parseInt(document.getElementById('punish').value) || 0;
        let localLang = parseInt(document.getElementById('localLang').value) || 0;
        let checkedAbsent = countChecked('chk-absent');
        let absentCheckedTerms = Array.from(document.querySelectorAll('.chk-absent')).map(function(cb) {
            return cb.checked ? cb.dataset.term : null;
        }).filter(Boolean);
        let absentTermText = absentCheckedTerms.length ? absentCheckedTerms.join('、') : '尚未勾選';
        let absentScore = Math.min(12, checkedAbsent * 2);
        let openedAbsentTerms = sixTermDefs.filter(function(term) { return term.rank <= gradeConfig.termUntil; }).length;
        let currentAbsentMax = Math.min(12, openedAbsentTerms * 2);
        let rewardByTerm = {};
        sixTermDefs.forEach(function(term) { rewardByTerm[term.key] = getTermReward(term.key); });
        let totalDG = sixTermDefs.reduce(function(sum, term) { return sum + rewardByTerm[term.key].dg; }, 0);
        let totalXG = sixTermDefs.reduce(function(sum, term) { return sum + rewardByTerm[term.key].xg; }, 0);
        let totalJJ = sixTermDefs.reduce(function(sum, term) { return sum + rewardByTerm[term.key].jj; }, 0);
        let rewardRaw = (totalDG * 4.5) + (totalXG * 1.5) + (totalJJ * 0.5);
        let rewardScore = Math.min(20, rewardRaw);
        let rewardTermDetailHTML = sixTermDefs.map(function(term) {
            let r = rewardByTerm[term.key];
            if (!r.active) return `- ${term.label}:尚未開放,不列入目前試算`;
            return `- ${term.label}:大功 ${formatNumber(r.dg)} | 小功 ${formatNumber(r.xg)} | 嘉獎 ${formatNumber(r.jj)} (換算得分:${formatNumber(r.score)} 分)`;
        }).join('<br>');

        let serviceByTerm = {};
        let serviceHoursTotal = 0;
        fiveTermDefs.forEach(function(term) {
            const el = document.getElementById('srv_' + term.key);
            const active = el && !el.disabled;
            const hours = active ? (parseFloat(el.value) || 0) : 0;
            serviceByTerm[term.key] = { hours: hours, score: active ? calcSingleSemesterService(hours) : 0, active: active };
            serviceHoursTotal += hours;
        });
        let serviceRaw = fiveTermDefs.reduce(function(sum, term) { return sum + serviceByTerm[term.key].score; }, 0);
        let serviceScore = Math.min(10, serviceRaw);
        let openedServiceTerms = fiveTermDefs.filter(function(term) { return term.rank <= gradeConfig.serviceUntil; }).length;
        let currentServiceMax = Math.min(10, openedServiceTerms * 2);
        let serviceTermText = fiveTermDefs.map(function(term) { return `${term.label} ${serviceByTerm[term.key].active ? formatNumber(serviceByTerm[term.key].score) + '分' : '未開放'}`; }).join(' | ');
        let group2Raw = punish + absentScore + rewardScore + serviceScore + localLang;
        let group2Score = Math.min(40, group2Raw);

        document.getElementById('detailGroup2').innerHTML = `
            <div class="report-item-detail">• 日常生活表現評量:<strong>${formatNumber(punish)}</strong> / 10 分</div>
            <div class="report-item-detail">• 無曠課紀錄學期:<strong>${formatNumber(absentScore)}</strong> / 12 分 <span class="report-formula-desc">目前已開放學期最高 ${formatNumber(currentAbsentMax)} 分;已勾選 ${checkedAbsent} 期:${escapeHTML(absentTermText)}</span></div>
            <div class="report-item-detail">• 功過相抵後的獎懲紀錄:<strong>${formatNumber(rewardScore)}</strong> / 20 分
                <span class="report-formula-desc">分學期累積明細:<br>${rewardTermDetailHTML}<br>總數累加:大功 ${formatNumber(totalDG)} | 小功 ${formatNumber(totalXG)} | 嘉獎 ${formatNumber(totalJJ)} → 原始加總為 ${formatNumber(rewardRaw)} 分,最高採計 20 分。</span>
            </div>
            <div class="report-item-detail">• 服務學習時數折算:<strong>${formatNumber(serviceScore)}</strong> / 10 分 <span class="report-formula-desc">目前已開放學期最高 ${formatNumber(currentServiceMax)} 分;分期限制精算:${serviceTermText},已開放學期總時數 ${formatNumber(serviceHoursTotal)} 小時,最終最高採計 10 分。</span></div>
            <div class="report-item-detail">• 本土語言認證:<strong>${formatNumber(localLang)}</strong> / 2 分 <span class="report-formula-desc">${localLang === 2 ? "已通過原住民族語、客語或閩南語初級以上認證" : "未取得或不採計"}</span></div>
            <span class="report-formula-desc">本大項原始加總 ${formatNumber(group2Raw)} 分,最高採計 40 分。</span>
        `;
        document.getElementById('scoreGroup2').innerText = formatNumber(group2Score);

        // 三、國中教育會考 (最高採計 30 分)
        let examScore = 0;
        let examReportItems = examDefs.map(function(item) {
            const score = parseInt(document.getElementById(item.id).value) || 0;
            examScore += score;
            return `${item.label}:${getExamLevelText(score)} ${score} 分`;
        });
        let examViolationPoints = parseInt(document.getElementById('examViolation').value) || 0;
        let examDeduction = examViolationPoints === 1 ? 0.3 : (examViolationPoints === 2 ? 0.6 : 0);
        let group3Score = Math.max(0, Math.min(30, examScore) - examDeduction);
        document.getElementById('detailGroup3').innerHTML = `
            <div class="report-item-detail">• 各科等級積分:<strong>${formatNumber(group3Score)}</strong> / 30 分
            <span class="report-formula-desc">${examReportItems.map(escapeHTML).join('<br>')}<br>違規記點:${examViolationPoints} 點,扣 ${formatNumber(examDeduction)} 分。寫作測驗級分與各科標示為同分比序項目,不列入總積分。</span></div>
        `;
        document.getElementById('scoreGroup3').innerText = formatNumber(group3Score);

        let shortageHTML = "";
        let shortageText = "";
        let shortageItems = [];

        if (gradeConfig.volunteerOpen && volunteer === 0) {
            shortageItems.push(`<li>志願序位積分:目前設定為不採計;正式選填志願後可改選第 1~25 志願序位區間。</li>`);
            shortageText += `• 志願序位積分:目前不採計,正式選填志願後再調整。\n`;
        }
        if (punish < 10) {
            shortageItems.push(`<li>日常生活表現評量:尚未拿滿,還差 ${formatNumber(10 - punish)} 分。</li>`);
            shortageText += `• 日常生活表現評量:還差 ${formatNumber(10 - punish)} 分。\n`;
        }
        if (absentScore < currentAbsentMax) {
            shortageItems.push(`<li>無曠課紀錄的學期:目前已開放學期最高 ${formatNumber(currentAbsentMax)} 分,現在取得 ${formatNumber(absentScore)} 分,還差 ${formatNumber(currentAbsentMax - absentScore)} 分。</li>`);
            shortageText += `• 無曠課紀錄的學期:目前已開放學期還差 ${formatNumber(currentAbsentMax - absentScore)} 分。\n`;
        }
        if (rewardScore < 20) {
            let diffPoints = 20 - rewardScore;
            let neededJiajiang = Math.ceil(diffPoints / 0.5);
            let neededXiaogong = Math.ceil(diffPoints / 1.5);
            shortageItems.push(`<li>功過相抵後的獎懲紀錄:目前 ${formatNumber(rewardScore)} 分,距離完整採計上限還差 <strong>${formatNumber(diffPoints)}</strong> 分;此項可在後續學期持續累積。</li>`);
            shortageText += `• 功過相抵後的獎懲紀錄:目前 ${formatNumber(rewardScore)} 分,距離完整採計上限還差 ${formatNumber(diffPoints)} 分。\n`;
        }
        if (serviceScore < currentServiceMax) {
            shortageItems.push(`<li>服務學習時數積分:目前已開放學期最高 ${formatNumber(currentServiceMax)} 分,現在取得 ${formatNumber(serviceScore)} 分,還差 ${formatNumber(currentServiceMax - serviceScore)} 分。</li>`);
            shortageText += `• 服務學習時數積分:目前已開放學期還差 ${formatNumber(currentServiceMax - serviceScore)} 分。\n`;
        }
        if (gradeConfig.examOpen && group3Score === 0) {
            shortageItems.push(`<li>國中教育會考:目前設定為不採計;取得會考成績後可改選精熟、基礎或待加強。</li>`);
            shortageText += `• 國中教育會考:目前不採計,取得成績後再調整。\n`;
        }

        const multiBuffer = Math.max(0, group2Raw - group2Score);
        const officialMultiMax = 40;
        const multiGap = Math.max(0, officialMultiMax - group2Score);
        let strategyHTML = '';
        let strategyText = '';
        if (group2Score >= officialMultiMax) {
            strategyHTML = `<div style="margin-top: 12px; color:#2c3e50; font-size:0.9em; background:#eefaf1; border:1px solid #c8e6c9; border-radius:6px; padding:10px; line-height:1.5;">✅ <strong>補分策略判讀:</strong>多元學習表現已達採計上限 40 分。即使單一項目未滿,例如獎懲未達 20 分,仍可能已由無曠課、服務學習或本土語認證等項目補足;報告重點可放在「維持無懲罰、無曠課與已取得分數」。${multiBuffer > 0 ? `目前原始分數比採計上限多 ${formatNumber(multiBuffer)} 分,可視為緩衝分數。` : ''}</div>`;
            strategyText = `多元學習表現已達採計上限 40 分;單項未滿不一定影響總分,請維持無懲罰、無曠課與既有分數。`;
        } else {
            let possibleIdeas = [];
            if (localLang === 0) possibleIdeas.push('本土語言認證可補 2 分');
            if (serviceScore < 10) possibleIdeas.push('服務學習每 3 小時 1 分、每學期最多 2 分');
            if (absentScore < 12) possibleIdeas.push('後續學期維持無曠課,每學期可增加 2 分');
            if (rewardScore < 20) possibleIdeas.push('獎懲紀錄可透過後續嘉獎、小功或大功累積');
            strategyHTML = `<div style="margin-top: 12px; color:#2c3e50; font-size:0.9em; background:#f8fbff; border:1px solid #bbdefb; border-radius:6px; padding:10px; line-height:1.5;">🧭 <strong>補分策略判讀:</strong>多元學習表現距離採計上限 40 分還差 ${formatNumber(multiGap)} 分。可優先看「可掌握、可累積」的項目:${possibleIdeas.length ? possibleIdeas.join(';') : '維持目前已取得項目,並等待後續學期成績或紀錄完成。'}。</div>`;
            strategyText = `多元學習表現距離採計上限 40 分還差 ${formatNumber(multiGap)} 分;可從無曠課、服務學習、本土語認證與獎懲紀錄中找可補分項目。`;
        }

        if (shortageItems.length > 0) {
            shortageHTML = `<div style="margin-top: 15px; color: #c0392b; font-weight: bold; font-size:0.95em; border-top: 1px dashed #ccc; padding-top: 10px;">⚠️ 積分提醒:</div><ul class="shortage-list">${shortageItems.join('')}</ul><div style="font-size:0.85em; color:#666; margin-top:5px;">※系統已依目前年級排除尚未發生學期;獎懲、會考與志願序位提醒屬於後續可持續累積或完成的方向。</div>`;
        } else {
            shortageHTML = `<div style="margin-top: 15px; color: #27ae60; font-weight: bold; font-size:0.95em; border-top: 1px dashed #ccc; padding-top: 10px;">🎉 太完美了!目前所有可輸入項目皆已達到滿分水準!</div>`;
            shortageText += `🎉 目前所有可輸入項目皆已達到滿分水準!\n`;
        }
        document.getElementById('shortageAlertArea').innerHTML = shortageHTML + strategyHTML;

        let totalScoreNow = group1Score + group2Score + group3Score;
        document.getElementById('reportTotalScore').innerText = formatNumber(totalScoreNow) + " 分";

        let cleanName = name || "一般學生";
        let weakLabel = weak === 5 ? "偏遠/經濟弱勢 5 分" : (weak === 3 ? "非山非市 3 分" : "不符合 0 分");
        let localLabel = local === 5 ? "符合 5 分" : "不符合 0 分";
        let localLangLabel = localLang === 2 ? "已通過 2 分" : "未取得或不採計 0 分";
        let shortageReportText = shortageText ? shortageText.trim() : "🎉 目前所有可輸入項目皆已達到滿分水準!";
        let strategyReportText = strategyText || "";
        let domainCopyText = domainCalculations.map(function(item) {
            return `  - ${item.name}:平均 ${item.count ? item.avg.toFixed(1) : '未採計'} 分,${item.passed ? '符合' : '不符合或未採計'}(採計:${item.includedText};排除:${item.excludedText})`;
        }).join('\n');
        let examCopyText = examReportItems.join(';') + `;違規記點 ${examViolationPoints} 點,扣 ${formatNumber(examDeduction)} 分`;

        textReportContent =
            `【115學年度竹苗區免試入學積分精華報告】\n` +
            `班級:${sClass || '──'} 座號:${seat || '──'} 姓名:${cleanName}\n` +
            `試算階段:${gradeConfig.label}(系統僅採計已完成學期)\n` +
            ` \n` +
            `一、均衡發展:${formatNumber(group1Score)} / 30 分\n` +
            `• 扶助弱勢:${weakLabel}\n` +
            `• 就近入學:${localLabel}\n` +
            `• 志願序位:${getSelectedText('volunteer')}\n` +
            `• 均衡學習:符合 ${passedDomains} / 4 個領域,取得 ${domainScore} 分\n${domainCopyText}\n` +
            ` \n` +
            `二、多元學習表現:${formatNumber(group2Score)} / 40 分\n` +
            `• 日常生活表現:${formatNumber(punish)} / 10 分\n` +
            `• 無曠課紀錄:${formatNumber(absentScore)} / 12 分(已符合 ${checkedAbsent} 個學期:${absentTermText})\n` +
            `• 獎懲紀錄:${formatNumber(rewardScore)} / 20 分(大功 ${formatNumber(totalDG)}、小功 ${formatNumber(totalXG)}、嘉獎 ${formatNumber(totalJJ)})\n` +
            `• 服務學習:${formatNumber(serviceScore)} / 10 分(前五學期共 ${formatNumber(serviceHoursTotal)} 小時)\n` +
            `• 本土語言認證:${localLangLabel}\n` +
            ` \n` +
            `三、國中教育會考:${formatNumber(group3Score)} / 30 分\n` +
            `• ${examCopyText}\n` +
            ` \n` +
            `四、積分提醒\n` +
            `${shortageReportText}\n` +
            `${strategyReportText ? "\n五、補分策略判讀\n" + strategyReportText + "\n" : ""}` +
            ` \n` +
            `★ 免試入學比序預估總積分:${formatNumber(totalScoreNow)} 分 ★`;

        document.getElementById('resultBox').style.display = 'block';
        document.getElementById('resultBox').scrollIntoView({ behavior: 'smooth' });
    }

    function copyTextReport() {
        if (!textReportContent) { alert("請先點擊「開始計算總積分」後再複製報告喔!"); return; }
        function fallbackCopy(text) {
            const textarea = document.createElement('textarea');
            textarea.value = text;
            textarea.setAttribute('readonly', '');
            textarea.style.position = 'fixed';
            textarea.style.left = '-9999px';
            document.body.appendChild(textarea);
            textarea.select();
            let ok = false;
            try { ok = document.execCommand('copy'); } catch (err) { console.error(err); }
            document.body.removeChild(textarea);
            if (ok) { alert("🎉 精華報告已成功複製到剪貼簿!已可直接貼上使用。"); }
            else { alert("複製失敗,請手動全選文字進行複製。"); }
        }
        if (navigator.clipboard && window.isSecureContext) {
            navigator.clipboard.writeText(textReportContent).then(function() {
                alert("🎉 精華報告已成功複製到剪貼簿!已可直接貼上使用。");
            }).catch(function() { fallbackCopy(textReportContent); });
        } else { fallbackCopy(textReportContent); }
    }

    document.addEventListener('DOMContentLoaded', setupGradeHandlers);


    function getCurrentResultForSaving() {
        const resultBox = document.getElementById('resultBox');
        if (!textReportContent || !resultBox || resultBox.style.display === 'none') {
            throw new Error('請先點擊「開始計算總積分」,產生試算報告後再儲存。');
        }
        const formData = {};
        document.querySelectorAll('input, select').forEach(function(el) {
            if (!el.id) return;
            if (el.id === 'savePassword' || el.id === 'saveBirthday') return;
            if (el.type === 'checkbox') formData[el.id || el.name] = el.checked;
            else formData[el.id] = el.value;
        });
        const resultData = {
            group1Score: document.getElementById('scoreGroup1') ? document.getElementById('scoreGroup1').innerText : '',
            group2Score: document.getElementById('scoreGroup2') ? document.getElementById('scoreGroup2').innerText : '',
            group3Score: document.getElementById('scoreGroup3') ? document.getElementById('scoreGroup3').innerText : '',
            totalScore: document.getElementById('reportTotalScore') ? document.getElementById('reportTotalScore').innerText : '',
            detailGroup1Text: document.getElementById('detailGroup1') ? document.getElementById('detailGroup1').innerText : '',
            detailGroup2Text: document.getElementById('detailGroup2') ? document.getElementById('detailGroup2').innerText : '',
            detailGroup3Text: document.getElementById('detailGroup3') ? document.getElementById('detailGroup3').innerText : '',
            shortageText: document.getElementById('shortageAlertArea') ? document.getElementById('shortageAlertArea').innerText : ''
        };
        return {
            studentId: document.getElementById('saveStudentId').value.trim(),
            birthday: document.getElementById('saveBirthday').value.trim(),
            password: document.getElementById('savePassword').value,
            name: document.getElementById('stu_name').value.trim(),
            className: document.getElementById('stu_class').value.trim(),
            seatNo: document.getElementById('stu_seat').value.trim(),
            gradeStage: (typeof getSelectedText === 'function' && document.getElementById('gradeStage')) ? getSelectedText('gradeStage') : (document.getElementById('gradeStage') ? document.getElementById('gradeStage').value : ''),
            group1Score: resultData.group1Score,
            group2Score: resultData.group2Score,
            group3Score: resultData.group3Score,
            totalScore: resultData.totalScore,
            reportText: textReportContent,
            inputJson: formData,
            resultJson: resultData
        };
    }

    function setSaveStatus(message, ok) {
        const box = document.getElementById('saveStatus');
        if (!box) return;
        box.className = 'save-status ' + (ok ? 'ok' : 'err');
        box.innerText = message;
    }

    function saveCurrentResult() {
        let payload;
        try {
            payload = getCurrentResultForSaving();
            if (!payload.studentId) throw new Error('請輸入學號。');
            if (!payload.birthday) throw new Error('請輸入生日。');
            if (!payload.password || payload.password.length < 4) throw new Error('請設定至少 4 碼的個人查詢密碼。');
        } catch (err) {
            setSaveStatus(err.message, false);
            return;
        }
        if (typeof google === 'undefined' || !google.script || !google.script.run) {
            setSaveStatus('目前不是在 Apps Script Web App 環境中,無法寫入 Google 試算表。請依 README 部署後再測試儲存功能。', false);
            return;
        }
        setSaveStatus('儲存中,請稍候……', true);
        google.script.run
            .withSuccessHandler(function(res) {
                if (res && res.success) {
                    setSaveStatus('✅ 儲存成功!你可以到「查詢已儲存結果」頁面,用學號、生日與查詢密碼查詢本次報告。儲存序號:' + res.recordId, true);
                } else {
                    setSaveStatus((res && res.message) ? res.message : '儲存失敗,請稍後再試。', false);
                }
            })
            .withFailureHandler(function(err) {
                setSaveStatus('儲存失敗:' + (err && err.message ? err.message : err), false);
            })
            .saveResult(payload);
    }




    function escapeText(value) {
      return String(value == null ? '' : value);
    }

    function setStatus(message, ok) {
      const box = document.getElementById('status');
      box.className = 'status ' + (ok ? 'ok' : 'err');
      box.innerText = message;
    }

    function getNumberFromRecord(record, keys) {
      for (var i = 0; i < keys.length; i++) {
        var value = record && record[keys[i]];
        if (value !== undefined && value !== null && value !== '') return value;
      }
      return null;
    }

    function showSummaryChips(record) {
      var g1 = getNumberFromRecord(record, ['group1Score', 'group1_score']);
      var g2 = getNumberFromRecord(record, ['group2Score', 'group2_score']);
      var g3 = getNumberFromRecord(record, ['group3Score', 'group3_score']);
      var total = getNumberFromRecord(record, ['totalScore', 'total_score']);
      if (g1 === null && g2 === null && g3 === null && total === null) {
        document.getElementById('summaryChips').style.display = 'none';
        return;
      }
      document.getElementById('g1Chip').innerText = g1 === null ? '──' : g1 + ' 分';
      document.getElementById('g2Chip').innerText = g2 === null ? '──' : g2 + ' 分';
      document.getElementById('g3Chip').innerText = g3 === null ? '──' : g3 + ' 分';
      document.getElementById('totalChip').innerText = total === null ? '──' : total + ' 分';
      document.getElementById('summaryChips').style.display = 'grid';
    }

    function lookupSavedResult() {
      const payload = {
        studentId: document.getElementById('studentId').value.trim(),
        birthday: document.getElementById('birthday').value.trim(),
        password: document.getElementById('password').value
      };
      if (!payload.studentId) return setStatus('請輸入學號。', false);
      if (!payload.birthday) return setStatus('請輸入生日。', false);
      if (!payload.password) return setStatus('請輸入個人查詢密碼。', false);
      if (typeof google === 'undefined' || !google.script || !google.script.run) {
        return setStatus('目前不是在 Apps Script Web App 環境中,無法查詢試算表資料。', false);
      }

      document.getElementById('reportCard').style.display = 'none';
      setStatus('查詢中,請稍候……', true);

      google.script.run
        .withSuccessHandler(function(res) {
          if (!res || !res.success) {
            document.getElementById('reportCard').style.display = 'none';
            setStatus(res && res.message ? res.message : '查詢失敗。', false);
            return;
          }
          const r = res.record || {};
          setStatus('✅ 查詢成功,以下為最近一次儲存的試算結果。', true);
          document.getElementById('recordBadge').innerText =
            '儲存時間:' + escapeText(r.timestamp || '──') + '\n' +
            '班級:' + escapeText(r.className || '──') + ' 座號:' + escapeText(r.seatNo || '──') + ' 姓名:' + escapeText(r.name || '──') + '\n' +
            '試算階段:' + escapeText(r.gradeStage || '──');
          showSummaryChips(r);
          document.getElementById('reportText').innerText = r.reportText || '此筆資料沒有報告內容。';
          document.getElementById('reportCard').style.display = 'block';
          document.getElementById('reportCard').scrollIntoView({ behavior: 'smooth' });
        })
        .withFailureHandler(function(err) {
          document.getElementById('reportCard').style.display = 'none';
          setStatus('查詢失敗:' + (err && err.message ? err.message : err), false);
        })
        .lookupResult(payload);
    }
  
</script>
</body>
</html>
貼到 Apps Script 的 index.html 檔案

6. 部署

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

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

💬 評論區

老師們的交流與提問

載入評論中...