自製免費點名系統
本文將教授如何使用程式製作一個簡單的點名網頁。透過這個點名系統,你可以方便地管理你的學生出席狀況,並且隨時查詢歷史出席紀錄。我們將使用 Google Apps Script, GitHub 和 Google Sheets 來建立這個點名系統。我敢保證即使你完全不會程式也可以在 5 分鐘內完成。本文會詳細說明從建立 Google Sheets 到部屬網頁的步驟,並提供完整的程式碼和演示網頁。
我在兩年前曾經寫過一篇【GAS】自製點名系統,出乎意料地幫助到許多人,所以我決定重新寫一次,比上次更容易製作和操作,也更好看一些 (我覺得啦)。
範例網頁
首先,我們先來看一下最終的成果。這是我們要製作的點名系統的演示網頁。你可以點擊這裡查看完整的演示網頁。他有以下幾個功能
- 點名:點擊學生姓名,即可完成點名
- 新增學生:輸入學生姓名點擊新增按鈕,即可新增學生
- 查詢歷史出席紀錄:輸入學生姓名點擊查詢按鈕,即可查詢歷史出席紀錄
好了,我們現在就開始製作這個點名系統吧 w
步驟一:建立 Google Sheets 文件
首先,我們需要建立一個 Google Sheets 文件,用於存儲學生的出席情況。在這個文件中,我們可以添加學生名稱、出席時間、剩餘課堂等信息。
請打開我建立的這個範例文件並建立副本
這樣 Google Sheet 就做好了。請複製這個文件的 ID,我們稍後會用到。ID 就是網址中的一長串字母和數字,比如說這個試算表:
1https://docs.google.com/spreadsheets/d/1m0F6pOejN-ldKFIrFwssmoEPB3EPDmSQJKEPr9T88-E/edit#gid=0
它的 ID 就是1m0F6pOejN-ldKFIrFwssmoEPB3EPDmSQJKEPr9T88-E
。
步驟二:建立 Google Apps Script
現在,我們需要建立一個 Google Apps Script,用於向 Google Sheets 文件中添加和讀取數據。請在網址輸入script.new,進入 Google Apps Script 編輯器。接著貼上我的這一串程式。請把第一行的雙引號裡面換成剛才複製的 ID。
1const id = "1m0F6pOejN-ldKFIrFwssmoEPB3EPDmSQJKEPr9T88-E";
2
3function doGet(e) {
4 let t = e.parameter,
5 a = SpreadsheetApp.openById(id).getSheets();
6 switch (t.type) {
7 case "call":
8 if (!t.time) return ContentService.createTextOutput(!1);
9 return (
10 a[0].appendRow([t.name, t.time, t.remain]),
11 ContentService.createTextOutput(!0)
12 );
13 case "list":
14 var r = a[1]
15 .getRange(2, 1, a[1].getLastRow() - 1, a[1].getLastColumn())
16 .getValues()
17 .filter((e) => "" !== e[0])
18 .map((e) => ({ name: e[0], left: e[2] }));
19 return ContentService.createTextOutput(
20 JSON.stringify(r)
21 ).setMimeType(ContentService.MimeType.JSON);
22 case "search":
23 var [n, ...r] = a[0].getDataRange().getValues();
24 let [u, i, p] = n,
25 s = n.indexOf(u),
26 m = n.indexOf(i),
27 c = n.indexOf(p),
28 l = r
29 .filter((e) => e[s] === t.name)
30 .map((e) => ({ time: e[m], left: e[c] }));
31 return ContentService.createTextOutput(
32 JSON.stringify(l)
33 ).setMimeType(ContentService.MimeType.JSON);
34 case "new":
35 let f = a[1].getLastRow() + 1;
36 return (
37 a[1].appendRow([
38 t.name,
39 `=COUNTIF('紀錄'!A:A,A${f})`,
40 `=D${f}-B${f}`
41 ]),
42 ContentService.createTextOutput(!0)
43 );
44 default:
45 return ContentService.createTextOutput("別亂撞我~");
46 }
47}
我們需要把它部屬成網頁,請點擊左上角的部屬,新增部屬作業,選擇部屬為網頁應用程式。執行身分選自己 (我),誰可以存取選所有人。接著點擊部屬,複製網頁應用程式網址。比如說:
1https://script.google.com/macros/s/AKfycbzxqGIMBbLkCka2aveltdVHYtdG-k_X98qzSd_V9MHDxWaOYXFwZgE3rRHDzCakzTxs/exec
步驟三:建立網頁
請你在任意一個網頁代管服務,比如說 Vercel,Github Pages, Gitlab Pages, Netlify 等等,建立一個網頁。接著在網頁中貼上以下程式碼。
如果你沒有使用過這些服務,可以參考以下教學:
使用 Github Pages 部屬網頁
請先註冊帳號,你可以參考以下影片:
部屬網頁有兩個辦法。選一個就可以了
用戶名.github.io
第一個是影片說明的方法,就是建立一個叫做用戶名.github.io
的倉庫,然後建立一個index.html
的檔案並貼上以下程式。
1<!doctype html>
2<html lang="zh-TW">
3 <head>
4 <meta charset="UTF-8" />
5 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7 <title>簡易點名系統</title>
8 <meta name="theme-color" content="3B4252" />
9 <style>
10 h1 {
11 /* 標題顏色 */
12 color: var(--nord7);
13 }
14
15 body {
16 /* 背景顏色 */
17 background-color: var(--nord0);
18 }
19
20 body {
21 /* 可選顏色 */
22 --nord0: #2e3440;
23 --nord1: #3b4252;
24 --nord2: #434c5e;
25 --nord3: #4c566a;
26 --nord4: #d8dee9;
27 --nord5: #e5e9f0;
28 --nord6: #eceff4;
29 --nord7: #8fbcbb;
30 --nord8: #88c0d0;
31 --nord9: #81a1c1;
32 --nord10: #5e81ac;
33 --nord11: #bf616a;
34 --nord12: #d08770;
35 --nord13: #ebcb8b;
36 --nord14: #a3be8c;
37 --nord15: #b48ead;
38 --black: #000;
39 --line: #4c566a;
40 }
41
42 main,
43 nav {
44 display: flex;
45 }
46
47 .call button,
48 button:hover {
49 background-color: var(--nord2);
50 }
51
52 button,
53 section > div {
54 background-color: var(--nord1);
55 box-shadow: rgba(0, 0, 0, 0.2) 0 0 0.5rem;
56 }
57
58 main,
59 section > div {
60 padding: 1rem;
61 width: 100%;
62 }
63
64 footer,
65 footer a {
66 color: var(--nord4);
67 }
68
69 .search button,
70 button,
71 input,
72 section > div {
73 box-shadow: rgba(0, 0, 0, 0.2) 0 0 0.5rem;
74 }
75
76 body,
77 button,
78 h2,
79 html {
80 text-align: center;
81 }
82
83 * {
84 padding: 0;
85 margin: 0;
86 box-sizing: border-box;
87 font-family: Arial, "微軟正黑體", Helvetica, sans-serif;
88 color: var(--nord6);
89 }
90
91 body,
92 html {
93 min-height: 100%;
94 }
95
96 main {
97 flex-direction: column;
98 height: 100vh;
99 height: 100dvh;
100 max-width: 500px;
101 margin: 0 auto;
102 }
103
104 nav {
105 justify-content: space-between;
106 }
107
108 button {
109 display: block;
110 height: 50px;
111 width: calc(1 / 3 * 100% - 1rem);
112 line-height: 50px;
113 border-radius: 1rem;
114 text-decoration: none;
115 border: none;
116 cursor: pointer;
117 transition: background-color 0.2s ease-in-out;
118 }
119
120 #call,
121 .search,
122 footer {
123 display: flex;
124 }
125
126 .call button {
127 width: calc(1 / 4 * 100% - 1rem);
128 margin: 0.5rem;
129 }
130
131 button:hover {
132 filter: brightness(1.2);
133 }
134
135 button:active {
136 background-color: var(--nord3);
137 filter: brightness(1.5);
138 }
139
140 section {
141 flex-grow: 1;
142 margin: 1rem 0;
143 position: relative;
144 }
145
146 footer {
147 justify-content: flex-end;
148 align-items: flex-end;
149 }
150
151 section > div {
152 border-radius: 1rem;
153 overflow-x: hidden;
154 overflow-y: auto;
155 position: absolute;
156 height: 100%;
157 transition: opacity 0.5s ease-in-out;
158 }
159
160 #add,
161 #history {
162 opacity: 0;
163 }
164
165 #call {
166 z-index: 2;
167 flex-wrap: wrap;
168 justify-content: space-between;
169 }
170
171 .search {
172 justify-content: center;
173 align-items: center;
174 }
175
176 .search button,
177 input {
178 height: 2rem;
179 width: 50%;
180 border-radius: 0.5rem;
181 border: transparent;
182 padding: 0 1rem;
183 background-color: var(--nord3);
184 color: var(--nord4);
185 }
186
187 h2,
188 table {
189 width: 100%;
190 }
191
192 input:focus {
193 outline: transparent;
194 }
195
196 .search button {
197 margin-left: 1rem;
198 width: auto;
199 line-height: 100%;
200 }
201
202 table {
203 border-collapse: collapse;
204 margin-top: 1rem;
205 }
206
207 tr {
208 border-bottom: 1px solid var(--line);
209 }
210
211 td {
212 padding: 0.5rem;
213 }
214
215 h2 {
216 margin-top: 2rem;
217 font-weight: 600;
218 }
219
220 #status {
221 margin: 0.5rem 0 1rem;
222 color: var(--nord13);
223 font-size: 1.3rem;
224 }
225 </style>
226 </head>
227
228 <body>
229 <main>
230 <h1>簡易點名系統</h1>
231 <h2 id="status">歡迎使用</h2>
232 <nav>
233 <button onclick="searchA()">查詢紀錄</button
234 ><button onclick="callA()">點名</button
235 ><button onclick="addA()">新增學生</button>
236 </nav>
237 <section>
238 <div id="history">
239 <div class="search">
240 <input type="text" /><button>搜尋</button>
241 </div>
242 <table>
243 <thead>
244 <tr>
245 <td>時間</td>
246 <td>剩下課堂</td>
247 </tr>
248 </thead>
249 <tbody></tbody>
250 </table>
251 </div>
252 <div id="call">
253 <h2>載入中</h2>
254 </div>
255 <div id="add">
256 <div class="search">
257 <input type="text" /><button>新增</button>
258 </div>
259 </div>
260 </section>
261 <footer>
262 <a href="edit-mr.github.io/">毛哥EM</a>製作 |
263 <a href="https://emtech.cc/post/roll-call">教學</a>
264 </footer>
265 </main>
266 <script>
267 //部屬連結放這裡
268 var url =
269 "https://script.google.com/macros/s/AKfycbzxqGIMBbLkCka2aveltdVHYtdG-k_X98qzSd_V9MHDxWaOYXFwZgE3rRHDzCakzTxs/exec";
270 const [history, call, add] = ["history", "call", "add"].map((t) =>
271 document.getElementById(t)
272 ),
273 searchA = () => {
274 (history.style.opacity = 1),
275 (history.style.zIndex = 2),
276 (call.style.opacity = add.style.opacity = 0),
277 (call.style.zIndex = add.style.zIndex = 1);
278 },
279 callA = () => {
280 (history.style.opacity = add.style.opacity = 0),
281 (history.style.zIndex = add.style.zIndex = 1),
282 (call.style.opacity = 1),
283 (call.style.zIndex = 2);
284 },
285 addA = () => {
286 (history.style.opacity = call.style.opacity = 0),
287 (history.style.zIndex = call.style.zIndex = 1),
288 (add.style.opacity = 1),
289 (add.style.zIndex = 2);
290 };
291 fetch(url + "?type=list")
292 .then((t) => t.json())
293 .then((t) => {
294 let e = document.getElementById("call");
295 (e.innerHTML = ""),
296 t.forEach((t, n) => {
297 let a = document.createElement("button");
298 (a.textContent = t.name),
299 (a.id = `student-${n + 1}`),
300 a.addEventListener("click", () => {
301 rollCall(t.name, t.left, n + 1);
302 }),
303 e.appendChild(a);
304 });
305 })
306 .catch((t) => console.error(t));
307 const status = document.getElementById("status");
308 function rollCall(t, e, n) {
309 status.innerHTML = `${t} 點名中...`;
310 var a = new Date(),
311 a = a
312 .toLocaleString("zh-TW", {
313 year: "numeric",
314 month: "2-digit",
315 day: "2-digit",
316 hour: "numeric",
317 minute: "numeric",
318 second: "numeric",
319 hour12: !0
320 })
321 .replace("-", "/")
322 .replace(" ", " ");
323 fetch(url + `?type=call&name=${t}&time=${a}&remain=${e}`)
324 .then((a) => {
325 a.ok
326 ? ((status.innerHTML = `${t} 已點名成功!剩餘課堂:${e - 1}`),
327 (document.getElementById(
328 "student-" + n
329 ).style.backgroundColor = "var(--nord14)"))
330 : ((status.innerHTML = `${t} 已點名失敗!剩餘課堂:${e}`),
331 (document.getElementById(
332 "student-" + n
333 ).style.backgroundColor = "var(--nord11)"));
334 })
335 .catch((t) => {
336 status.innerHTML = `發生錯誤:${t}`;
337 });
338 }
339 const searchBtn = document.querySelector("#history button"),
340 searchInput = document.querySelector("#history input"),
341 historyTableBody = document.querySelector("#history tbody");
342 searchBtn.addEventListener("click", () => {
343 status.innerHTML = "搜尋中...";
344 let t = searchInput.value,
345 e = `${e}?type=search&name=${encodeURIComponent(t)}`;
346 fetch(e)
347 .then((t) => t.json())
348 .then((t) => {
349 let e = document.querySelector("#history table tbody");
350 (e.innerHTML = ""),
351 t.forEach((t) => {
352 let n = document.createElement("tr"),
353 a = document.createElement("td"),
354 l = document.createElement("td");
355 var r = new Date(t.time);
356 (a.textContent = r
357 .toLocaleString("zh-TW", {
358 year: "numeric",
359 month: "2-digit",
360 day: "2-digit",
361 hour: "numeric",
362 minute: "numeric",
363 second: "numeric",
364 hour12: !0
365 })
366 .replace("-", "/")
367 .replace(" ", " ")),
368 (l.textContent = t.left),
369 n.appendChild(a),
370 n.appendChild(l),
371 e.appendChild(n),
372 (status.innerHTML = "搜尋完成");
373 });
374 })
375 .catch((t) => (status.innerHTML = t));
376 });
377 const addBtn = document.querySelector("#add button"),
378 addInput = document.querySelector("#add input");
379 addBtn.addEventListener("click", () => {
380 let t = addInput.value;
381 t &&
382 ((status.innerHTML = "新增中..."),
383 fetch(`${url}?type=new&name=${encodeURIComponent(t)}`)
384 .then((t) => (status.innerHTML = "新增成功"))
385 .catch((t) => {
386 status.innerHTML = t;
387 }));
388 });
389 </script>
390 </body>
391</html>
請把第 265 行的雙引號裡面換成剛才複製的網頁應用程式網址,然後按下儲存。這樣你的網頁就完成了!你可以到網址https://你的Github帳號.github.io/
來使用你的網頁。
Use this template
第二個方式也很簡單,請先到這個Github 倉庫並點擊右上角的 Fork,或是 Use this template。倉庫名稱 Repository name 會成為你的網址 (例如:https://你的 Github 帳號.github.io/倉庫名稱),然後點擊 Create repository from template。
請點擊檔案index.html
並點擊右上角的鉛筆按鈕編輯,
把第 265 行的雙引號裡面換成剛才複製的網頁應用程式網址,然後按下儲存。
然後再到你的倉庫裡面,點擊 Settings,然後點擊左邊的 Pages,把 Branch 改成 main,然後按下 Save,就完成了!
好啦,現在你的網頁就完成了!你可以到網址https://你的Github帳號.github.io/倉庫名稱
來使用你的網頁。
自訂
這樣你的網頁就建立完成且可以使用了。如果你想客製化顏色的話可以修改 CSS。比如說如果你想改標題你可以修改第 13 行
1color: var(--nord7);
你可以改成任何顏色,例如:color: red
,或是color: #ff0000
,或是color: rgb(255, 0, 0)
。
你可以 Google colorpicker 選取顏色,然後把 HEX 或是 RGB 的數字貼上去。
或是你可以使用預設的 Nord 顏色組。使用方式就是預設那樣,只要修改數字就好了。對應的顏色如下:
--nord0: #2E3440;
--nord1: #3B4252;
--nord2: #434C5E;
--nord3: #4C566A;
--nord4: #D8DEE9;
--nord5: #E5E9F0;
--nord6: #ECEFF4;
--nord7: #8FBCBB;
--nord8: #88C0D0;
--nord9: #81A1C1;
--nord10: #5E81AC;
--nord11: #BF616A;
--nord12: #D08770;
--nord13: #EBCB8B;
--nord14: #A3BE8C;
--nord15: #B48EAD;
希望你喜歡這個網頁!如果你覺得這篇文章有幫助到你歡迎在Instagram或Google 新聞追蹤毛哥EM 資訊密技。如果你有任何問題,歡迎直接到毛哥EM 資訊密技的 Instagram 私訊我,我很樂意協助解決你的問題。