本文將教授如何使用程式製作一個簡單的點名網頁。透過這個點名系統,你可以方便地管理你的學生出席狀況,並且隨時查詢歷史出席紀錄。我們將使用 Google Apps Script, GitHub 和 Google Sheets 來建立這個點名系統。我敢保證即使你完全不會程式也可以在 5 分鐘內完成。本文會詳細說明從建立 Google Sheets 到部屬網頁的步驟,並提供完整的程式碼和演示網頁。

我在兩年前曾經寫過一篇【Google Apps Script】自製點名系統,出乎意料地幫助到許多人,所以我決定重新寫一次,比上次更容易製作和操作,也更好看一些 (我覺得啦)。

範例網頁

首先,我們先來看一下最終的成果。這是我們要製作的點名系統的演示網頁。你可以點擊這裡查看完整的演示網頁。他有以下幾個功能

  • 點名:點擊學生姓名,即可完成點名
  • 新增學生:輸入學生姓名點擊新增按鈕,即可新增學生
  • 查詢歷史出席紀錄:輸入學生姓名點擊查詢按鈕,即可查詢歷史出席紀錄

好了,我們現在就開始製作這個點名系統吧 w

步驟一:建立 Google Sheets 文件

首先,我們需要建立一個 Google Sheets 文件,用於存儲學生的出席情況。在這個文件中,我們可以添加學生名稱、出席時間、剩餘課堂等信息。

請打開我建立的這個範例文件並建立副本

建立副本
建立副本

這樣 Google Sheet 就做好了。請複製這個文件的 ID,我們稍後會用到。ID 就是網址中的一長串字母和數字,比如說這個試算表:

1
2
https://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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const id = "1m0F6pOejN-ldKFIrFwssmoEPB3EPDmSQJKEPr9T88-E";

function doGet(e) {
    let t = e.parameter,
        a = SpreadsheetApp.openById(id).getSheets();
    switch (t.type) {
        case "call":
            if (!t.time) return ContentService.createTextOutput(!1);
            return (
                a[0].appendRow([t.name, t.time, t.remain]),
                ContentService.createTextOutput(!0)
            );
        case "list":
            var r = a[1]
                .getRange(2, 1, a[1].getLastRow() - 1, a[1].getLastColumn())
                .getValues()
                .filter((e) => "" !== e[0])
                .map((e) => ({ name: e[0], left: e[2] }));
            return ContentService.createTextOutput(
                JSON.stringify(r)
            ).setMimeType(ContentService.MimeType.JSON);
        case "search":
            var [n, ...r] = a[0].getDataRange().getValues();
            let [u, i, p] = n,
                s = n.indexOf(u),
                m = n.indexOf(i),
                c = n.indexOf(p),
                l = r
                    .filter((e) => e[s] === t.name)
                    .map((e) => ({ time: e[m], left: e[c] }));
            return ContentService.createTextOutput(
                JSON.stringify(l)
            ).setMimeType(ContentService.MimeType.JSON);
        case "new":
            let f = a[1].getLastRow() + 1;
            return (
                a[1].appendRow([
                    t.name,
                    `=COUNTIF('紀錄'!A:A,A${f})`,
                    `=D${f}-B${f}`
                ]),
                ContentService.createTextOutput(!0)
            );
        default:
            return ContentService.createTextOutput("別亂撞我~");
    }
}

我們需要把它部屬成網頁,請點擊左上角的部屬,新增部屬作業,選擇部屬為網頁應用程式。執行身分選自己 (我),誰可以存取選所有人。接著點擊部屬,複製網頁應用程式網址。比如說:

1
2
https://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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
<!doctype html>
<html lang="zh-TW">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>簡易點名系統</title>
        <meta name="theme-color" content="3B4252" />
        <style>
            h1 {
                /* 標題顏色 */
                color: var(--nord7);
            }

            body {
                /* 背景顏色 */
                background-color: var(--nord0);
            }

            body {
                /* 可選顏色 */
                --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;
                --black: #000;
                --line: #4c566a;
            }

            main,
            nav {
                display: flex;
            }

            .call button,
            button:hover {
                background-color: var(--nord2);
            }

            button,
            section > div {
                background-color: var(--nord1);
                box-shadow: rgba(0, 0, 0, 0.2) 0 0 0.5rem;
            }

            main,
            section > div {
                padding: 1rem;
                width: 100%;
            }

            footer,
            footer a {
                color: var(--nord4);
            }

            .search button,
            button,
            input,
            section > div {
                box-shadow: rgba(0, 0, 0, 0.2) 0 0 0.5rem;
            }

            body,
            button,
            h2,
            html {
                text-align: center;
            }

            * {
                padding: 0;
                margin: 0;
                box-sizing: border-box;
                font-family: Arial, "微軟正黑體", Helvetica, sans-serif;
                color: var(--nord6);
            }

            body,
            html {
                min-height: 100%;
            }

            main {
                flex-direction: column;
                height: 100vh;
                height: 100dvh;
                max-width: 500px;
                margin: 0 auto;
            }

            nav {
                justify-content: space-between;
            }

            button {
                display: block;
                height: 50px;
                width: calc(1 / 3 * 100% - 1rem);
                line-height: 50px;
                border-radius: 1rem;
                text-decoration: none;
                border: none;
                cursor: pointer;
                transition: background-color 0.2s ease-in-out;
            }

            #call,
            .search,
            footer {
                display: flex;
            }

            .call button {
                width: calc(1 / 4 * 100% - 1rem);
                margin: 0.5rem;
            }

            button:hover {
                filter: brightness(1.2);
            }

            button:active {
                background-color: var(--nord3);
                filter: brightness(1.5);
            }

            section {
                flex-grow: 1;
                margin: 1rem 0;
                position: relative;
            }

            footer {
                justify-content: flex-end;
                align-items: flex-end;
            }

            section > div {
                border-radius: 1rem;
                overflow-x: hidden;
                overflow-y: auto;
                position: absolute;
                height: 100%;
                transition: opacity 0.5s ease-in-out;
            }

            #add,
            #history {
                opacity: 0;
            }

            #call {
                z-index: 2;
                flex-wrap: wrap;
                justify-content: space-between;
            }

            .search {
                justify-content: center;
                align-items: center;
            }

            .search button,
            input {
                height: 2rem;
                width: 50%;
                border-radius: 0.5rem;
                border: transparent;
                padding: 0 1rem;
                background-color: var(--nord3);
                color: var(--nord4);
            }

            h2,
            table {
                width: 100%;
            }

            input:focus {
                outline: transparent;
            }

            .search button {
                margin-left: 1rem;
                width: auto;
                line-height: 100%;
            }

            table {
                border-collapse: collapse;
                margin-top: 1rem;
            }

            tr {
                border-bottom: 1px solid var(--line);
            }

            td {
                padding: 0.5rem;
            }

            h2 {
                margin-top: 2rem;
                font-weight: 600;
            }

            #status {
                margin: 0.5rem 0 1rem;
                color: var(--nord13);
                font-size: 1.3rem;
            }
        </style>
    </head>

    <body>
        <main>
            <h1>簡易點名系統</h1>
            <h2 id="status">歡迎使用</h2>
            <nav>
                <button onclick="searchA()">查詢紀錄</button
                ><button onclick="callA()">點名</button
                ><button onclick="addA()">新增學生</button>
            </nav>
            <section>
                <div id="history">
                    <div class="search">
                        <input type="text" /><button>搜尋</button>
                    </div>
                    <table>
                        <thead>
                            <tr>
                                <td>時間</td>
                                <td>剩下課堂</td>
                            </tr>
                        </thead>
                        <tbody></tbody>
                    </table>
                </div>
                <div id="call">
                    <h2>載入中</h2>
                </div>
                <div id="add">
                    <div class="search">
                        <input type="text" /><button>新增</button>
                    </div>
                </div>
            </section>
            <footer>
                <a href="edit-mr.github.io/">毛哥EM</a>製作 |
                <a href="https://emtech.cc/post/roll-call">教學</a>
            </footer>
        </main>
        <script>
            //部屬連結放這裡
            var url =
                "https://script.google.com/macros/s/AKfycbzxqGIMBbLkCka2aveltdVHYtdG-k_X98qzSd_V9MHDxWaOYXFwZgE3rRHDzCakzTxs/exec";
            const [history, call, add] = ["history", "call", "add"].map((t) =>
                    document.getElementById(t)
                ),
                searchA = () => {
                    (history.style.opacity = 1),
                        (history.style.zIndex = 2),
                        (call.style.opacity = add.style.opacity = 0),
                        (call.style.zIndex = add.style.zIndex = 1);
                },
                callA = () => {
                    (history.style.opacity = add.style.opacity = 0),
                        (history.style.zIndex = add.style.zIndex = 1),
                        (call.style.opacity = 1),
                        (call.style.zIndex = 2);
                },
                addA = () => {
                    (history.style.opacity = call.style.opacity = 0),
                        (history.style.zIndex = call.style.zIndex = 1),
                        (add.style.opacity = 1),
                        (add.style.zIndex = 2);
                };
            fetch(url + "?type=list")
                .then((t) => t.json())
                .then((t) => {
                    let e = document.getElementById("call");
                    (e.innerHTML = ""),
                        t.forEach((t, n) => {
                            let a = document.createElement("button");
                            (a.textContent = t.name),
                                (a.id = `student-${n + 1}`),
                                a.addEventListener("click", () => {
                                    rollCall(t.name, t.left, n + 1);
                                }),
                                e.appendChild(a);
                        });
                })
                .catch((t) => console.error(t));
            const status = document.getElementById("status");
            function rollCall(t, e, n) {
                status.innerHTML = `${t} 點名中...`;
                var a = new Date(),
                    a = a
                        .toLocaleString("zh-TW", {
                            year: "numeric",
                            month: "2-digit",
                            day: "2-digit",
                            hour: "numeric",
                            minute: "numeric",
                            second: "numeric",
                            hour12: !0
                        })
                        .replace("-", "/")
                        .replace(" ", " ");
                fetch(url + `?type=call&name=${t}&time=${a}&remain=${e}`)
                    .then((a) => {
                        a.ok
                            ? ((status.innerHTML = `${t} 已點名成功!剩餘課堂:${e - 1}`),
                              (document.getElementById(
                                  "student-" + n
                              ).style.backgroundColor = "var(--nord14)"))
                            : ((status.innerHTML = `${t} 已點名失敗!剩餘課堂:${e}`),
                              (document.getElementById(
                                  "student-" + n
                              ).style.backgroundColor = "var(--nord11)"));
                    })
                    .catch((t) => {
                        status.innerHTML = `發生錯誤:${t}`;
                    });
            }
            const searchBtn = document.querySelector("#history button"),
                searchInput = document.querySelector("#history input"),
                historyTableBody = document.querySelector("#history tbody");
            searchBtn.addEventListener("click", () => {
                status.innerHTML = "搜尋中...";
                let t = searchInput.value,
                    e = `${e}?type=search&name=${encodeURIComponent(t)}`;
                fetch(e)
                    .then((t) => t.json())
                    .then((t) => {
                        let e = document.querySelector("#history table tbody");
                        (e.innerHTML = ""),
                            t.forEach((t) => {
                                let n = document.createElement("tr"),
                                    a = document.createElement("td"),
                                    l = document.createElement("td");
                                var r = new Date(t.time);
                                (a.textContent = r
                                    .toLocaleString("zh-TW", {
                                        year: "numeric",
                                        month: "2-digit",
                                        day: "2-digit",
                                        hour: "numeric",
                                        minute: "numeric",
                                        second: "numeric",
                                        hour12: !0
                                    })
                                    .replace("-", "/")
                                    .replace(" ", " ")),
                                    (l.textContent = t.left),
                                    n.appendChild(a),
                                    n.appendChild(l),
                                    e.appendChild(n),
                                    (status.innerHTML = "搜尋完成");
                            });
                    })
                    .catch((t) => (status.innerHTML = t));
            });
            const addBtn = document.querySelector("#add button"),
                addInput = document.querySelector("#add input");
            addBtn.addEventListener("click", () => {
                let t = addInput.value;
                t &&
                    ((status.innerHTML = "新增中..."),
                    fetch(`${url}?type=new&name=${encodeURIComponent(t)}`)
                        .then((t) => (status.innerHTML = "新增成功"))
                        .catch((t) => {
                            status.innerHTML = t;
                        }));
            });
        </script>
    </body>
</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,就完成了!

Github Pages 設定
Github Pages 設定

好啦,現在你的網頁就完成了!你可以到網址https://你的Github帳號.github.io/倉庫名稱來使用你的網頁。

自訂

這樣你的網頁就建立完成且可以使用了。如果你想客製化顏色的話可以修改 CSS。比如說如果你想改標題你可以修改第 13 行

1
2
color: 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;

希望你喜歡這個網頁!如果你覺得這篇文章有幫助到你歡迎在InstagramGoogle 新聞追蹤毛哥EM 資訊密技。如果你有任何問題,歡迎直接到毛哥EM 資訊密技的 Instagram 私訊我,我很樂意協助解決你的問題。

毛哥EM

數位創作者,全端工程龍