本文將教授如何使用程式製作一個簡單的點名網頁。透過這個點名系統,你可以方便地管理你的學生出席狀況,並且隨時查詢歷史出席紀錄。我們將使用 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,就完成了!

Github Pages 設定
Github Pages 設定

好啦,現在你的網頁就完成了!你可以到網址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;

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

毛哥EM

一位喜歡把簡單的事,做得不簡單的高三生。