用 Google 試算表做資料庫,用 Gemini 寫程式,做一個學生打開連結就用得的網上測驗。不需要任何程式底子。
你完成之後會有甚麼?一條網址。學生打開,輸入姓名,答題,按交卷即時見到分數同正確答案;而全班的成績會自動回到你那份 Google 試算表裏。題目由你自己在試算表輸入,改題目不需要改程式。
今日做的東西,其實只有三個部分。明白了這三樣,之後你想做甚麼工具都是同一個道理。
| 部分 | 它是甚麼 | 負責甚麼 |
|---|---|---|
| Google 試算表 | 資料庫 | 存放題目、答案、成績 |
| Code.gs | 後端程式 | 去試算表拿題目、把成績寫回試算表 |
| index.html | 前端網頁 | 學生眼睛見到的畫面 |
按下面的按鈕,會直接彈出「建立副本」的視窗,按確定,副本就會存到你自己的 Google 雲端硬碟。之後你一直用這份副本,不要用原本那份。
複製之後請看一眼:試算表左下角的分頁名稱必須是「題庫」兩個字。程式靠這個名字去找你的資料,改了名就會找不到。
範本已經有兩條數學題示範格式。請你在下面加多三條,任何科目都可以,但今日建議先用加減乘除——因為我們現在要練的是流程,不是出題。
| 欄 | 填甚麼 | 例子 |
|---|---|---|
| A · 題目 | 題目全文 | 7 + 5 = ? |
| B–E · 選項A 至 選項D | 四個選項 | 10 / 11 / 12 / 13 |
| F · 正確答案 | 只填一個英文字母 A、B、C 或 D | C |
| G · 題解 | 答錯時給學生看的解釋,可留空 | 7 加 5 等於 12。 |
最常見的錯誤:F 欄填了完整的選項內容(例如填「12」)。F 欄只可以填 A、B、C、D 其中一個字母。
我要用 Google Apps Script 製作一個簡單的網上測驗系統,請幫我寫程式碼。 【資料來源】 我有一個 Google 試算表,裡面有一個工作表,名稱是「題庫」。 第一行是標題行,由 A 欄至 G 欄依次為: A 欄:題目 B 欄:選項A C 欄:選項B D 欄:選項C E 欄:選項D F 欄:正確答案(只會填 A、B、C 或 D 其中一個英文字母) G 欄:題解(可能留空) 第二行開始是題目資料,數量不固定。 【功能要求】 1. 學生打開網頁後,先輸入姓名,然後按「開始測驗」。 2. 系統從「題庫」工作表讀取全部題目,每題以單選按鈕顯示四個選項。 3. 學生按「交卷」後: - 即時批改,答對的題目顯示綠色,答錯的顯示紅色; - 答錯的題目要顯示正確答案,如果 G 欄有題解就一併顯示; - 在頁面底部顯示總分,例如「3 / 5」。 4. 交卷後,把成績寫入同一個試算表中名為「成績記錄」的工作表, 欄位依次為:時間、姓名、分數、總題數。 如果「成績記錄」工作表不存在,請自動建立並加上標題行。 【技術要求】 - 請給我兩個檔案:Code.gs(後端)和 index.html(前端)。 - 前端使用 google.script.run 呼叫後端函式。 - 不要使用任何外部程式庫或 CDN,所有 CSS 和 JavaScript 都寫在 index.html 之內。 - 介面簡潔清爽,適合中學生使用,全部文字使用繁體中文。 - 請在程式碼中加入中文註解,方便我理解每一段的作用。
I want to build a simple online quiz system with Google Apps Script. Please write the code for me. 【Data source】 I have a Google Sheet containing one worksheet named 題庫 (please use this exact name in the code). Row 1 is the header row. Columns A to G, in order, are: Column A: 題目 — the question text Column B: 選項A — option A Column C: 選項B — option B Column D: 選項C — option C Column E: 選項D — option D Column F: 正確答案 — the correct answer, holding only one letter: A, B, C or D Column G: 題解 — an explanation, which may be empty Question data starts at row 2. The number of questions is not fixed. 【Features】 1. When a student opens the page, they first type their name, then press "Start". 2. The system reads every question from the 題庫 worksheet and shows the four options as radio buttons. 3. After the student presses "Submit": - Mark immediately; correct questions turn green and wrong ones turn red. - For wrong questions, show the correct answer, along with the explanation from column G when it is not empty. - Show the total score at the bottom of the page, for example "3 / 5". 4. After submitting, write the result into a worksheet named 成績記錄 in the same spreadsheet, with these columns in order: time, name, score, total questions. If the 成績記錄 worksheet does not exist, create it automatically with a header row. 【Technical requirements】 - Give me two files: Code.gs (back end) and index.html (front end). - The front end calls the back-end functions through google.script.run. - Do not use any external library or CDN. All CSS and JavaScript must live inside index.html. - Keep the interface clean and simple, suitable for secondary school students. All interface text in English. - Add English comments to the code so I can follow what each part does.
這一段就是今日最值得學的一課。留意提示詞裏面,我們把工作表叫甚麼名、每一欄放甚麼,全部寫得清清楚楚。要 AI 幫你寫程式,你必須先講清楚你的資料是怎樣擺放的——你日後做任何工具,這一步都一樣。
Gemini 應該會給你兩段程式碼:一段是 Code.gs,一段是 index.html。等一等它生成完,先不要複製。
程式碼.gs(或 Code.gs)。把裏面原有的內容全部刪除,貼上 Gemini 給你的 Code.gs。index(不要打 .html,系統會自動加)。index.html。HTML 檔案的名一定要是 index。如果打錯,網頁打開會是一片空白。這是全場最常見的問題。
「前往(不安全)」是正常的,不用怕。因為這個程式是你自己剛剛寫的,未經 Google 審核,所以會出這個提示。你授權的對象是你自己的程式。
之後改了程式碼?要再做一次「部署 → 管理部署作業 → 編輯(鉛筆圖示)→ 版本揀『新版本』→ 部署」,網址不變。單純儲存是不會更新網頁的。
做到這一步,你已經有一個屬於自己的網上測驗系統。之後想換題目,只需要在試算表加減行數,完全不用碰程式碼。
| 你見到的情況 | 多數是這個原因 |
|---|---|
| 網頁一片空白 | HTML 檔案不是叫 index。回 Apps Script 改檔名,然後重新部署。 |
| 「找不到工作表」之類的錯誤 | 試算表的分頁名不是「題庫」。改回「題庫」兩個字。 |
| 題目出到,但全部答案都錯 | F 欄填了選項內容而不是 A/B/C/D 字母。 |
| 改了程式碼,但網頁沒有變 | 要重新部署(見步驟五最後一格),單純儲存不會更新。 |
| 學生說打不開 | 部署時「誰可以存取」不是揀「任何人」。重新編輯部署設定。 |
| 沒有「成績記錄」分頁 | 要成功交卷一次,程式才會自動建立這個分頁。 |
| 以上都不是,就是不成功 | 用下面的後備程式碼換掉兩個檔案,重新部署。 |
如果 Gemini 生成的版本試極都不成功,可以直接用下面這一套。它跟提示詞的要求完全一樣,已經測試過可以運作。請先試自己生成的版本——今日的重點是學識怎樣叫 AI 幫你寫,這裏只是保險。
// ===== 課堂小測驗 · 後端 =====
// 這個檔案負責:開啟網頁、從試算表讀題目、把成績寫回試算表。
// 當有人打開網址時,顯示 index.html
function doGet() {
return HtmlService.createHtmlOutputFromFile('index')
.setTitle('課堂小測驗');
}
// 從「題庫」工作表讀取所有題目
function getQuestions() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('題庫');
const data = sheet.getDataRange().getValues();
const questions = [];
for (let i = 1; i < data.length; i++) {
const row = data[i];
if (!row[0]) continue;
const letter = String(row[5]).toUpperCase().trim();
const options = { A: row[1], B: row[2], C: row[3], D: row[4] };
questions.push({
question: row[0],
options: [row[1], row[2], row[3], row[4]].filter(String),
answer: options[letter],
explanation: row[6] || ''
});
}
return questions;
}
// 把學生的成績寫入「成績記錄」工作表(第一次會自動建立)
function saveResult(name, score, total) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let sheet = ss.getSheetByName('成績記錄');
if (!sheet) {
sheet = ss.insertSheet('成績記錄');
sheet.appendRow(['時間', '姓名', '分數', '總題數']);
}
sheet.appendRow([new Date(), name, score, total]);
return '已儲存';
}
// ===== Class Quiz · Back end =====
// This file opens the page, reads questions from the Sheet,
// and writes results back to the Sheet.
// Serve index.html when someone opens the web app URL
function doGet() {
return HtmlService.createHtmlOutputFromFile('index')
.setTitle('Class Quiz');
}
// Read every question from the 題庫 worksheet
function getQuestions() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('題庫');
const data = sheet.getDataRange().getValues();
const questions = [];
for (let i = 1; i < data.length; i++) {
const row = data[i];
if (!row[0]) continue;
const letter = String(row[5]).toUpperCase().trim();
const options = { A: row[1], B: row[2], C: row[3], D: row[4] };
questions.push({
question: row[0],
options: [row[1], row[2], row[3], row[4]].filter(String),
answer: options[letter],
explanation: row[6] || ''
});
}
return questions;
}
// Write the result into the 成績記錄 worksheet (created automatically the first time)
function saveResult(name, score, total) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let sheet = ss.getSheetByName('成績記錄');
if (!sheet) {
sheet = ss.insertSheet('成績記錄');
sheet.appendRow(['Time', 'Name', 'Score', 'Total']);
}
sheet.appendRow([new Date(), name, score, total]);
return 'saved';
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<base target="_top">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700;900&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; }
body {
margin: 0; min-height: 100vh; padding: 1.5rem;
background: linear-gradient(160deg,#f6faff 0%,#e6f0fb 38%,#d2e4f7 72%,#c8dcf5 100%) fixed;
color: #102c4a;
font-family: "Noto Sans TC", -apple-system, "PingFang TC", "Microsoft JhengHei", sans-serif;
font-size: 17px; line-height: 1.75;
}
.wrap { max-width: 42rem; margin: 0 auto; }
.card {
background: rgba(255,255,255,.82); border: 1px solid rgba(24,88,176,.16);
border-radius: 20px; box-shadow: 0 6px 20px rgba(20,76,150,.08);
padding: 1.8rem 1.5rem; margin-bottom: 1.2rem;
}
h1 { font-size: 1.5rem; font-weight: 900; letter-spacing: -.02em; margin: 0 0 .2rem; }
.sub { color: #24628a; font-size: .95rem; margin: 0 0 1.3rem; }
label { display: block; font-size: .9rem; font-weight: 500; color: #24628a; margin-bottom: .3rem; }
input[type=text] {
width: 100%; padding: .7rem .85rem; font-size: 1rem; font-family: inherit; color: #102c4a;
background: #fff; border: 1px solid #aecdf0; border-radius: 14px; transition: all .25s;
}
input[type=text]:focus { outline: none; border-color: #2b7aea; box-shadow: 0 0 0 3px rgba(43,122,234,.18); }
button {
width: 100%; margin-top: 1rem; font-family: inherit; font-size: 1.02rem; font-weight: 700; color: #fff;
background: linear-gradient(120deg,#13539c,#1f66e2); box-shadow: 0 4px 14px rgba(19,83,156,.3);
border: 0; border-radius: 14px; padding: .8rem 2rem; cursor: pointer; transition: all .25s;
}
button:hover { filter: brightness(1.08); transform: translateY(-1px); }
button:disabled { background: #9fb3c8; box-shadow: none; cursor: not-allowed; transform: none; }
.q-num { font-size: .82rem; font-weight: 700; color: #2b7aea; letter-spacing: .04em; }
.q-text { font-weight: 700; font-size: 1.05rem; margin: .2rem 0 .9rem; }
.opt {
display: block; padding: .7rem .9rem; margin-bottom: .55rem; cursor: pointer;
background: #fff; border: 1px solid #d7e6f7; border-radius: 14px; transition: all .2s;
}
.opt:hover { border-color: #2b7aea; background: #f2f8ff; }
.opt input { margin-right: .55rem; }
.card.correct { border-color: #7fc39a; background: rgba(233,248,239,.9); }
.card.wrong { border-color: #f0adb9; background: rgba(253,238,240,.9); }
.fb { margin-top: .8rem; padding: .8rem 1rem; border-radius: 14px; font-size: .92rem; }
.fb.ok { background: #e3f5ea; color: #1c6b41; }
.fb.no { background: #fdecef; color: #a11d38; }
.fb b { font-weight: 700; }
.score-card { text-align: center; }
.score-num { font-size: 2.6rem; font-weight: 900; letter-spacing: -.03em; margin: .3rem 0; }
.saved { color: #24628a; font-size: .88rem; margin-top: .4rem; }
.hidden { display: none !important; }
</style>
</head>
<body>
<div class="wrap">
<!-- 第一頁:輸入姓名 -->
<div id="startCard" class="card">
<h1>課堂小測驗</h1>
<p class="sub">請輸入你的姓名,然後開始作答。</p>
<label for="nameInput">姓名</label>
<input type="text" id="nameInput" placeholder="例如:陳大文">
<button id="startBtn" onclick="startQuiz()">開始測驗</button>
</div>
<!-- 第二頁:題目 -->
<div id="quizArea" class="hidden">
<div id="questionList"></div>
<button id="submitBtn" onclick="submitQuiz()">交卷</button>
</div>
<!-- 第三頁:成績 -->
<div id="resultCard" class="card score-card hidden">
<p class="sub" style="margin:0">你的成績</p>
<div class="score-num" id="scoreText"></div>
<p class="saved" id="savedText"></p>
</div>
</div>
<script>
let questions = [];
let studentName = '';
// 按「開始測驗」:向後端拿題目
function startQuiz() {
const name = document.getElementById('nameInput').value.trim();
if (!name) { alert('請先輸入姓名'); return; }
studentName = name;
const btn = document.getElementById('startBtn');
btn.disabled = true;
btn.textContent = '載入題目中…';
google.script.run
.withSuccessHandler(showQuestions)
.withFailureHandler(function (err) {
alert('載入失敗:' + err.message);
btn.disabled = false;
btn.textContent = '開始測驗';
})
.getQuestions();
}
// 把題目畫出來
function showQuestions(list) {
if (!list || list.length === 0) {
alert('題庫沒有題目,請先在試算表輸入題目。');
const btn = document.getElementById('startBtn');
btn.disabled = false;
btn.textContent = '開始測驗';
return;
}
questions = list;
document.getElementById('startCard').classList.add('hidden');
document.getElementById('quizArea').classList.remove('hidden');
const box = document.getElementById('questionList');
box.innerHTML = '';
questions.forEach(function (q, i) {
const card = document.createElement('div');
card.className = 'card';
card.id = 'q' + i;
let html = '<div class="q-num">第 ' + (i + 1) + ' 題</div>';
html += '<div class="q-text">' + q.question + '</div>';
q.options.forEach(function (opt) {
html += '<label class="opt"><input type="radio" name="q' + i + '" value="' + opt + '">' + opt + '</label>';
});
html += '<div class="fb hidden" id="fb' + i + '"></div>';
card.innerHTML = html;
box.appendChild(card);
});
}
// 交卷:批改、顯示答案、把成績寫回試算表
function submitQuiz() {
let score = 0;
const btn = document.getElementById('submitBtn');
questions.forEach(function (q, i) {
const picked = document.querySelector('input[name="q' + i + '"]:checked');
const chosen = picked ? picked.value : '';
const card = document.getElementById('q' + i);
const fb = document.getElementById('fb' + i);
document.querySelectorAll('input[name="q' + i + '"]').forEach(function (r) { r.disabled = true; });
if (chosen === q.answer) {
score++;
card.classList.add('correct');
fb.className = 'fb ok';
fb.innerHTML = '<b>答對了!</b>' + (q.explanation ? ' ' + q.explanation : '');
} else {
card.classList.add('wrong');
fb.className = 'fb no';
fb.innerHTML = '<b>正確答案:</b>' + q.answer + (q.explanation ? '<br>' + q.explanation : '');
}
fb.classList.remove('hidden');
});
btn.disabled = true;
btn.textContent = '已交卷';
document.getElementById('scoreText').textContent = score + ' / ' + questions.length;
document.getElementById('resultCard').classList.remove('hidden');
document.getElementById('resultCard').scrollIntoView({ behavior: 'smooth' });
google.script.run
.withSuccessHandler(function () {
document.getElementById('savedText').textContent = '成績已記錄到試算表。';
})
.withFailureHandler(function () {
document.getElementById('savedText').textContent = '成績未能儲存,請通知老師。';
})
.saveResult(studentName, score, questions.length);
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<base target="_top">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700;900&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; }
body {
margin: 0; min-height: 100vh; padding: 1.5rem;
background: linear-gradient(160deg,#f6faff 0%,#e6f0fb 38%,#d2e4f7 72%,#c8dcf5 100%) fixed;
color: #102c4a;
font-family: "Noto Sans TC", -apple-system, "PingFang TC", "Microsoft JhengHei", sans-serif;
font-size: 17px; line-height: 1.75;
}
.wrap { max-width: 42rem; margin: 0 auto; }
.card {
background: rgba(255,255,255,.82); border: 1px solid rgba(24,88,176,.16);
border-radius: 20px; box-shadow: 0 6px 20px rgba(20,76,150,.08);
padding: 1.8rem 1.5rem; margin-bottom: 1.2rem;
}
h1 { font-size: 1.5rem; font-weight: 900; letter-spacing: -.02em; margin: 0 0 .2rem; }
.sub { color: #24628a; font-size: .95rem; margin: 0 0 1.3rem; }
label { display: block; font-size: .9rem; font-weight: 500; color: #24628a; margin-bottom: .3rem; }
input[type=text] {
width: 100%; padding: .7rem .85rem; font-size: 1rem; font-family: inherit; color: #102c4a;
background: #fff; border: 1px solid #aecdf0; border-radius: 14px; transition: all .25s;
}
input[type=text]:focus { outline: none; border-color: #2b7aea; box-shadow: 0 0 0 3px rgba(43,122,234,.18); }
button {
width: 100%; margin-top: 1rem; font-family: inherit; font-size: 1.02rem; font-weight: 700; color: #fff;
background: linear-gradient(120deg,#13539c,#1f66e2); box-shadow: 0 4px 14px rgba(19,83,156,.3);
border: 0; border-radius: 14px; padding: .8rem 2rem; cursor: pointer; transition: all .25s;
}
button:hover { filter: brightness(1.08); transform: translateY(-1px); }
button:disabled { background: #9fb3c8; box-shadow: none; cursor: not-allowed; transform: none; }
.q-num { font-size: .82rem; font-weight: 700; color: #2b7aea; letter-spacing: .04em; }
.q-text { font-weight: 700; font-size: 1.05rem; margin: .2rem 0 .9rem; }
.opt {
display: block; padding: .7rem .9rem; margin-bottom: .55rem; cursor: pointer;
background: #fff; border: 1px solid #d7e6f7; border-radius: 14px; transition: all .2s;
}
.opt:hover { border-color: #2b7aea; background: #f2f8ff; }
.opt input { margin-right: .55rem; }
.card.correct { border-color: #7fc39a; background: rgba(233,248,239,.9); }
.card.wrong { border-color: #f0adb9; background: rgba(253,238,240,.9); }
.fb { margin-top: .8rem; padding: .8rem 1rem; border-radius: 14px; font-size: .92rem; }
.fb.ok { background: #e3f5ea; color: #1c6b41; }
.fb.no { background: #fdecef; color: #a11d38; }
.fb b { font-weight: 700; }
.score-card { text-align: center; }
.score-num { font-size: 2.6rem; font-weight: 900; letter-spacing: -.03em; margin: .3rem 0; }
.saved { color: #24628a; font-size: .88rem; margin-top: .4rem; }
.hidden { display: none !important; }
</style>
</head>
<body>
<div class="wrap">
<!-- Screen 1: name entry -->
<div id="startCard" class="card">
<h1>Class Quiz</h1>
<p class="sub">Enter your name, then start the quiz.</p>
<label for="nameInput">Name</label>
<input type="text" id="nameInput" placeholder="e.g. Chan Tai Man">
<button id="startBtn" onclick="startQuiz()">Start quiz</button>
</div>
<!-- Screen 2: questions -->
<div id="quizArea" class="hidden">
<div id="questionList"></div>
<button id="submitBtn" onclick="submitQuiz()">Submit</button>
</div>
<!-- Screen 3: score -->
<div id="resultCard" class="card score-card hidden">
<p class="sub" style="margin:0">Your score</p>
<div class="score-num" id="scoreText"></div>
<p class="saved" id="savedText"></p>
</div>
</div>
<script>
let questions = [];
let studentName = '';
// "Start quiz": ask the back end for the questions
function startQuiz() {
const name = document.getElementById('nameInput').value.trim();
if (!name) { alert('Please enter your name first'); return; }
studentName = name;
const btn = document.getElementById('startBtn');
btn.disabled = true;
btn.textContent = 'Loading questions…';
google.script.run
.withSuccessHandler(showQuestions)
.withFailureHandler(function (err) {
alert('Could not load: ' + err.message);
btn.disabled = false;
btn.textContent = 'Start quiz';
})
.getQuestions();
}
// Render the questions
function showQuestions(list) {
if (!list || list.length === 0) {
alert('The question bank is empty. Please add questions to the Sheet first.');
const btn = document.getElementById('startBtn');
btn.disabled = false;
btn.textContent = 'Start quiz';
return;
}
questions = list;
document.getElementById('startCard').classList.add('hidden');
document.getElementById('quizArea').classList.remove('hidden');
const box = document.getElementById('questionList');
box.innerHTML = '';
questions.forEach(function (q, i) {
const card = document.createElement('div');
card.className = 'card';
card.id = 'q' + i;
let html = '<div class="q-num">Question ' + (i + 1) + '</div>';
html += '<div class="q-text">' + q.question + '</div>';
q.options.forEach(function (opt) {
html += '<label class="opt"><input type="radio" name="q' + i + '" value="' + opt + '">' + opt + '</label>';
});
html += '<div class="fb hidden" id="fb' + i + '"></div>';
card.innerHTML = html;
box.appendChild(card);
});
}
// Submit: mark, reveal answers, write the result back to the Sheet
function submitQuiz() {
let score = 0;
const btn = document.getElementById('submitBtn');
questions.forEach(function (q, i) {
const picked = document.querySelector('input[name="q' + i + '"]:checked');
const chosen = picked ? picked.value : '';
const card = document.getElementById('q' + i);
const fb = document.getElementById('fb' + i);
document.querySelectorAll('input[name="q' + i + '"]').forEach(function (r) { r.disabled = true; });
if (chosen === q.answer) {
score++;
card.classList.add('correct');
fb.className = 'fb ok';
fb.innerHTML = '<b>Correct!</b>' + (q.explanation ? ' ' + q.explanation : '');
} else {
card.classList.add('wrong');
fb.className = 'fb no';
fb.innerHTML = '<b>Correct answer:</b> ' + q.answer + (q.explanation ? '<br>' + q.explanation : '');
}
fb.classList.remove('hidden');
});
btn.disabled = true;
btn.textContent = 'Submitted';
document.getElementById('scoreText').textContent = score + ' / ' + questions.length;
document.getElementById('resultCard').classList.remove('hidden');
document.getElementById('resultCard').scrollIntoView({ behavior: 'smooth' });
google.script.run
.withSuccessHandler(function () {
document.getElementById('savedText').textContent = 'Your result has been saved to the Sheet.';
})
.withFailureHandler(function () {
document.getElementById('savedText').textContent = 'Result could not be saved. Please tell your teacher.';
})
.saveResult(studentName, score, questions.length);
}
</script>
</body>
</html>
同樣的三件套,加多幾張工作表、多幾個函式,就可以一路長出去:
MailApp 把成績同錯題分析電郵給學生。一個要知道的限制:今日這個簡化版,正確答案會隨題目一起送到學生的瀏覽器,適合自己溫習用。如果日後要拿來做正式評估,就要改成由伺服器批改(即是答案不下發到瀏覽器),否則學生有辦法看到答案。