0. 作品介紹
封面
1. AI 提示詞
把這段指令貼給 AI,讓它幫你生成程式碼。
我想要建立一個 【研習活動網頁】
幫我用 Google 試算表建立 【活動資料庫】
我希望有些資料我可以直接在試算表更改,首頁可以直接呈現
需求說明 【
1. 首頁自動把「活動資料庫」裡的每一個場次,排成一張一張好看的表格。
2. 活動可以分成好幾個大區塊(例如不同主題的工作坊),每個區塊自動用不同顏色。
3. 表格欄位要有:場次、日期、時間、報名;其中「日期」和「時間」要分成兩個欄位。
4. 表格每一列的底色要一深一淺交錯(斑馬紋),看起來比較清楚。
5. 每一列最右邊有一個「報名」按鈕,點下去會打開那一場的報名網址。
6. 網頁最上方要有一排「快速連結」,點了可以直接跳到對應的區塊。
7. 同一主題若分好幾梯、或分上午/下午、線上/實體,要能放在同一個大區塊裡再分成小表。
8. 網頁的標題、小標題、日期範圍、最下面的備註,我想直接在試算表改,不用動程式。
9. 用手機打開也要能正常閱讀(表格會自動變成一張張卡片)。
10. 整體風格希望簡潔專業、配色柔和(米白底、圓角卡片、每個區塊一種主色)。
】
要把它變成程式碼我要發佈
請完整敘述操作步驟 越詳細越好
作業環境
Google 試算表
Google Apps Script
我看不懂程式碼,請給我後端 gs 完整程式碼、前端 index 完整程式碼
幫我用 Google 試算表建立 【活動資料庫】
我希望有些資料我可以直接在試算表更改,首頁可以直接呈現
需求說明 【
1. 首頁自動把「活動資料庫」裡的每一個場次,排成一張一張好看的表格。
2. 活動可以分成好幾個大區塊(例如不同主題的工作坊),每個區塊自動用不同顏色。
3. 表格欄位要有:場次、日期、時間、報名;其中「日期」和「時間」要分成兩個欄位。
4. 表格每一列的底色要一深一淺交錯(斑馬紋),看起來比較清楚。
5. 每一列最右邊有一個「報名」按鈕,點下去會打開那一場的報名網址。
6. 網頁最上方要有一排「快速連結」,點了可以直接跳到對應的區塊。
7. 同一主題若分好幾梯、或分上午/下午、線上/實體,要能放在同一個大區塊裡再分成小表。
8. 網頁的標題、小標題、日期範圍、最下面的備註,我想直接在試算表改,不用動程式。
9. 用手機打開也要能正常閱讀(表格會自動變成一張張卡片)。
10. 整體風格希望簡潔專業、配色柔和(米白底、圓角卡片、每個區塊一種主色)。
】
要把它變成程式碼我要發佈
請完整敘述操作步驟 越詳細越好
作業環境
Google 試算表
Google Apps Script
我看不懂程式碼,請給我後端 gs 完整程式碼、前端 index 完整程式碼
2. 資料表規則
請照下面規則建立分頁與欄位。
第1分頁: 頁面設定
A 欄: 項目
B 欄: 內容
第2分頁: 課程資料
A 欄: 區塊
B 欄: 區塊附註
C 欄: 子群組
D 欄: 場次
E 欄: 日期
F 欄: 時間
G 欄: 報名網址
📄 解析結果(自動表格)
分頁:頁面設定
| A 欄 | B 欄 |
|---|---|
| 項目 | 內容 |
分頁:課程資料
| A 欄 | B 欄 | C 欄 | D 欄 | E 欄 | F 欄 | G 欄 |
|---|---|---|---|---|---|---|
| 區塊 | 區塊附註 | 子群組 | 場次 | 日期 | 時間 | 報名網址 |
3. 開啟 Apps Script
從試算表開 Apps Script,才能直接用 SpreadsheetApp 讀寫資料。
- 試算表上方選單:擴充功能 → Apps Script
- 刪掉預設的程式碼(或全部選取覆蓋)
-
保留/建立檔案:
- Code.gs
- index.html
4. 貼上後端(Code.gs)
把後端程式貼到 Apps Script 的 Code.gs。
// Code.gs — 後端:讀取試算表,把課程資料回傳給前端渲染
function doGet() {
return HtmlService.createHtmlOutputFromFile('index')
.setTitle('課程表')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
.addMetaTag('viewport', 'width=device-width, initial-scale=1');
}
function getScheduleData() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var settings = readSettings_(ss);
var sections = readCourses_(ss);
return { meta: settings.meta, notes: settings.notes, sections: sections };
}
function readSettings_(ss) {
var meta = { eyebrow: '', title: '課程表', subtitle: '', pill: '', footer: '' };
var notes = [];
var sh = ss.getSheetByName('頁面設定');
if (sh) {
var last = sh.getLastRow();
if (last >= 2) {
var vals = sh.getRange(2, 1, last - 1, 2).getValues();
vals.forEach(function (r) {
var k = String(r[0]).trim();
var v = String(r[1]).trim();
if (!k) return;
if (k === '小標籤') meta.eyebrow = v;
else if (k === '標題') meta.title = v;
else if (k === '副標') meta.subtitle = v;
else if (k === '年度膠囊' || k === '日期範圍') meta.pill = v;
else if (k === '頁尾') meta.footer = v;
else if (k === '備註') { if (v) notes.push(v); }
});
}
}
return { meta: meta, notes: notes };
}
function readCourses_(ss) {
var sh = ss.getSheetByName('課程資料');
if (!sh) return [];
var last = sh.getLastRow();
if (last < 2) return [];
var vals = sh.getRange(2, 1, last - 1, 7).getValues();
var order = [];
var map = {};
vals.forEach(function (r) {
var sec = String(r[0]).trim();
if (!sec) return;
if (!map[sec]) {
map[sec] = { title: sec, note: '', groupsOrder: [], groups: {} };
order.push(sec);
}
var s = map[sec];
if (!s.note) {
var nt = String(r[1]).trim();
if (nt) s.note = nt;
}
var gt = String(r[2]).trim();
if (!s.groups[gt]) {
s.groups[gt] = { title: gt, rows: [] };
s.groupsOrder.push(gt);
}
s.groups[gt].rows.push({
sess: cell_(r[3]),
date: cell_(r[4]),
time: cell_(r[5]),
url: String(r[6]).trim()
});
});
return order.map(function (sec, i) {
var s = map[sec];
return {
id: 'sec-' + (i + 1),
title: s.title,
note: s.note,
groups: s.groupsOrder.map(function (gt) { return s.groups[gt]; })
};
});
}
function cell_(v) {
if (v instanceof Date) {
var tz = Session.getScriptTimeZone();
var y = v.getFullYear();
if (y < 1900) return Utilities.formatDate(v, tz, 'HH:mm');
return Utilities.formatDate(v, tz, 'M月d日');
}
return String(v).trim();
} 貼到 Apps Script 的 Code.gs 檔案
5. 貼上前端(index.html)
把前端程式貼到 Apps Script 的 index.html。
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<base target="_top">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
/* ===== 樣式鎖定:請勿修改本 <style> 內的任何規則 =====
版面固定:置中單欄 920px、米白底 #faf8f3、字型 Noto Sans TC。
配色為六色循環 s1..s6(綠 / 紫 / 橘 / 藍 / 玫瑰 / 青),每個主區塊用一組色;
由下方 render() 依「區塊出現順序」自動指派,超過 6 個循環回 s1。
表格欄位固定為「場次|日期|時間|報名」,日期與時間必為兩欄;
資料列淺 / 深交錯(斑馬紋);報名為連外按鈕;≤720px 自動轉為卡片式。 */
:root{
--ink:#1f2430;
--muted:#6b7280;
--line:#e7e3da;
--paper:#faf8f3;
--card:#ffffff;
}
*{box-sizing:border-box;}
html{scroll-behavior:smooth;}
body{
margin:0;
background:var(--paper);
color:var(--ink);
font-family:"Noto Sans TC","PingFang TC","Microsoft JhengHei",system-ui,sans-serif;
line-height:1.6;
-webkit-font-smoothing:antialiased;
}
.wrap{max-width:920px;margin:0 auto;padding:40px 20px 64px;}
header{margin-bottom:28px;}
.eyebrow{font-size:13px;letter-spacing:.18em;color:var(--muted);text-transform:uppercase;margin-bottom:10px;}
h1{font-size:30px;line-height:1.3;margin:0 0 8px;font-weight:700;letter-spacing:.01em;}
.sub{color:var(--muted);font-size:15px;margin:0;}
.year-pill{display:inline-block;margin-top:14px;padding:4px 12px;border-radius:999px;background:#fff;border:1px solid var(--line);font-size:13px;color:var(--muted);}
.quicknav{display:flex;flex-wrap:wrap;gap:10px;margin-top:24px;padding-top:22px;border-top:1px dashed var(--line);}
.quicknav .label{width:100%;font-size:12px;letter-spacing:.1em;color:var(--muted);margin-bottom:2px;}
.tag{display:inline-flex;align-items:center;gap:8px;text-decoration:none;padding:9px 16px;border-radius:999px;font-size:14px;font-weight:600;border:1.5px solid;transition:background .15s ease,color .15s ease,transform .08s ease;}
.tag:hover{transform:translateY(-1px);}
.tag .n{width:8px;height:8px;border-radius:50%;}
.tag.s1{color:#0a5a48;border-color:#0f8a6e;background:#e3f4ef;}
.tag.s1 .n{background:#0f8a6e;} .tag.s1:hover{background:#0f8a6e;color:#fff;} .tag.s1:hover .n{background:#fff;}
.tag.s2{color:#473690;border-color:#6d54c7;background:#eeeafb;}
.tag.s2 .n{background:#6d54c7;} .tag.s2:hover{background:#6d54c7;color:#fff;} .tag.s2:hover .n{background:#fff;}
.tag.s3{color:#8a4712;border-color:#c2681f;background:#fbeee0;}
.tag.s3 .n{background:#c2681f;} .tag.s3:hover{background:#c2681f;color:#fff;} .tag.s3:hover .n{background:#fff;}
.tag.s4{color:#1f4d80;border-color:#2f6fb0;background:#e6eef7;}
.tag.s4 .n{background:#2f6fb0;} .tag.s4:hover{background:#2f6fb0;color:#fff;} .tag.s4:hover .n{background:#fff;}
.tag.s5{color:#8a2f4d;border-color:#c0466e;background:#fbe7ee;}
.tag.s5 .n{background:#c0466e;} .tag.s5:hover{background:#c0466e;color:#fff;} .tag.s5:hover .n{background:#fff;}
.tag.s6{color:#0a5a5a;border-color:#128a8a;background:#e0f4f4;}
.tag.s6 .n{background:#128a8a;} .tag.s6:hover{background:#128a8a;color:#fff;} .tag.s6:hover .n{background:#fff;}
.batch{margin-top:44px;scroll-margin-top:20px;}
.batch.s1{--acc:#0f8a6e;--acc-soft:#e3f4ef;--acc-zebra:#cfeae2;--acc-ink:#0a5a48;}
.batch.s2{--acc:#6d54c7;--acc-soft:#eeeafb;--acc-zebra:#ded6f5;--acc-ink:#473690;}
.batch.s3{--acc:#c2681f;--acc-soft:#fbeee0;--acc-zebra:#f6dfc6;--acc-ink:#8a4712;}
.batch.s4{--acc:#2f6fb0;--acc-soft:#e6eef7;--acc-zebra:#cfe0f0;--acc-ink:#1f4d80;}
.batch.s5{--acc:#c0466e;--acc-soft:#fbe7ee;--acc-zebra:#f6d0dd;--acc-ink:#8a2f4d;}
.batch.s6{--acc:#128a8a;--acc-soft:#e0f4f4;--acc-zebra:#c7ebeb;--acc-ink:#0a5a5a;}
.batch-head{display:flex;align-items:baseline;gap:10px;margin-bottom:6px;}
.dot{width:11px;height:11px;border-radius:50%;background:var(--acc);align-self:center;}
.batch-title{font-size:19px;font-weight:700;margin:0;}
.batch-note{color:var(--muted);font-size:14px;margin:0 0 16px 21px;}
.subhead{font-size:14px;font-weight:700;color:var(--acc-ink);margin:22px 0 10px;display:flex;align-items:center;gap:8px;}
.subhead::before{content:"";width:6px;height:16px;border-radius:3px;background:var(--acc);}
table{width:100%;border-collapse:separate;border-spacing:0;background:var(--card);border:1px solid var(--line);border-radius:14px;overflow:hidden;margin-bottom:4px;}
thead th{text-align:left;font-size:12px;letter-spacing:.06em;color:#fff;font-weight:600;padding:13px 16px;background:var(--acc);}
tbody td{padding:14px 16px;font-size:15px;vertical-align:middle;}
tbody tr:nth-child(odd) td{background:var(--acc-soft);}
tbody tr:nth-child(even) td{background:var(--acc-zebra);}
.sess{display:inline-flex;align-items:center;justify-content:center;min-width:64px;padding:5px 10px;border-radius:8px;background:#fff;font-weight:700;font-size:14px;color:var(--acc-ink);}
.col-sess{width:96px;}
.date{font-weight:600;white-space:nowrap;color:var(--acc-ink);}
.time{color:var(--muted);font-size:14px;white-space:nowrap;}
.col-date,.col-time{width:130px;}
.btn{display:inline-flex;align-items:center;gap:6px;text-decoration:none;padding:8px 14px;border-radius:9px;font-size:14px;font-weight:600;white-space:nowrap;background:var(--acc);color:#fff;transition:transform .08s ease,filter .15s ease;}
.btn:hover{filter:brightness(1.08);transform:translateY(-1px);}
.btn .arrow{font-size:13px;}
.col-reg{width:120px;text-align:right;}
.notes{margin-top:44px;background:#fff;border:1px solid var(--line);border-radius:14px;padding:20px 22px;}
.notes h3{margin:0 0 10px;font-size:15px;font-weight:700;}
.notes ul{margin:0;padding-left:18px;color:var(--muted);font-size:14px;}
.notes li{margin-bottom:6px;}
footer{margin-top:32px;text-align:center;color:var(--muted);font-size:13px;}
.state{padding:60px 0;text-align:center;color:var(--muted);font-size:15px;}
@media (max-width:720px){
h1{font-size:24px;}
thead{display:none;}
table,tbody,tr,td{display:block;width:100%;}
table{border:none;background:transparent;padding:0;margin-bottom:0;}
tbody tr{border-radius:14px;overflow:hidden;margin-bottom:12px;border:1px solid var(--line);}
tbody td{border:none;padding:9px 16px;}
.col-sess{padding-top:14px;}
.col-reg{text-align:left;padding-bottom:14px;}
.time{display:inline;}
}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="eyebrow" id="eyebrow"></div>
<h1 id="title">課程表</h1>
<p class="sub" id="sub"></p>
<span class="year-pill" id="pill" style="display:none"></span>
<nav class="quicknav" id="quicknav" style="display:none"></nav>
</header>
<div id="state" class="state">載入中…</div>
<div id="sections"></div>
<div id="notes"></div>
<footer id="footer">點選各場次「報名」按鈕即可前往報名頁面</footer>
</div>
<script>
function esc(s){
return String(s==null?'':s).replace(/[&<>"]/g,function(c){
return {'&':'&','<':'<','>':'>','"':'"'}[c];
});
}
window.onload = function(){
google.script.run
.withSuccessHandler(render)
.withFailureHandler(showError)
.getScheduleData();
};
function showError(err){
document.getElementById('state').textContent = '讀取資料時發生錯誤:' + (err && err.message ? err.message : err);
}
function render(data){
document.getElementById('state').style.display = 'none';
var m = data.meta || {};
document.getElementById('eyebrow').textContent = m.eyebrow || '';
document.getElementById('title').textContent = m.title || '課程表';
document.title = m.title || '課程表';
document.getElementById('sub').textContent = m.subtitle || '';
if(m.pill){ var p=document.getElementById('pill'); p.textContent=m.pill; p.style.display='inline-block'; }
if(m.footer){ document.getElementById('footer').textContent = m.footer; }
var sections = data.sections || [];
if(sections.length){
var nav = document.getElementById('quicknav');
var tags = '<span class="label">快速連結</span>';
sections.forEach(function(sec,i){
// 依區塊出現順序指派顏色,超過 6 色循環回 s1
var s = 's' + ((i % 6) + 1);
tags += '<a class="tag '+s+'" href="#'+esc(sec.id)+'"><span class="n"></span>'+esc(sec.title)+'</a>';
});
nav.innerHTML = tags;
nav.style.display = 'flex';
}
var html = '';
sections.forEach(function(sec,i){
var s = 's' + ((i % 6) + 1);
html += '<section class="batch '+s+'" id="'+esc(sec.id)+'">';
html += '<div class="batch-head"><span class="dot"></span><h2 class="batch-title">'+esc(sec.title)+'</h2></div>';
if(sec.note){ html += '<p class="batch-note">'+esc(sec.note)+'</p>'; }
(sec.groups||[]).forEach(function(g){
if(g.title){ html += '<div class="subhead">'+esc(g.title)+'</div>'; }
// 欄位固定:場次|日期|時間|報名(日期與時間分兩欄)
html += '<table><thead><tr>'
+ '<th class="col-sess">場次</th><th class="col-date">日期</th>'
+ '<th class="col-time">時間</th><th class="col-reg">報名</th>'
+ '</tr></thead><tbody>';
(g.rows||[]).forEach(function(r){
html += '<tr>'
+ '<td><span class="sess">'+esc(r.sess)+'</span></td>'
+ '<td><span class="date">'+esc(r.date)+'</span></td>'
+ '<td><span class="time">'+esc(r.time)+'</span></td>'
+ '<td class="col-reg">'
+ (r.url ? '<a class="btn" href="'+esc(r.url)+'" target="_blank" rel="noopener">報名 <span class="arrow">↗</span></a>' : '')
+ '</td></tr>';
});
html += '</tbody></table>';
});
html += '</section>';
});
document.getElementById('sections').innerHTML = html;
var notes = data.notes || [];
if(notes.length){
var n = '<div class="notes"><h3>備註</h3><ul>';
notes.forEach(function(x){ n += '<li>'+esc(x)+'</li>'; });
n += '</ul></div>';
document.getElementById('notes').innerHTML = n;
}
}
</script>
</body>
</html>
貼到 Apps Script 的 index.html 檔案
6. 部署
部署成 Web App 後,就會拿到可分享網址。
- Apps Script 右上:部署 → 新部署
- 類型選:Web app
- 執行身分:你
- 存取權限:任何人
- 完成後複製網址
💬 評論區
老師們的交流與提問
登入 後即可發表評論