0. 作品介紹
封面
1. AI 提示詞
把這段指令貼給 AI,讓它幫你生成程式碼。
我想要建立一個團購網頁
幫我用Google 試算表建立 【食品品項與點餐名單資料庫】
我希望有些資料我可以直接在試算表更改後,首頁可以直接呈現,並且送單之後會自動更新頁面
需求說明 【1.請列出各種品項的食品與價格2.建立40位學校成員的名單3.統計各種食品品項的數量4.統計每人應支付的金錢總額5.點錯的品項要可以刪除6.希望頁面呈現美味溫馨的風格】
要把它變成程式碼我要發佈
請完整敘述操作步驟 越詳細越好
作業環境
Google試算表
Google Apps Script
請給我後端gs完整程式碼、前端index完整程式碼
幫我用Google 試算表建立 【食品品項與點餐名單資料庫】
我希望有些資料我可以直接在試算表更改後,首頁可以直接呈現,並且送單之後會自動更新頁面
需求說明 【1.請列出各種品項的食品與價格2.建立40位學校成員的名單3.統計各種食品品項的數量4.統計每人應支付的金錢總額5.點錯的品項要可以刪除6.希望頁面呈現美味溫馨的風格】
要把它變成程式碼我要發佈
請完整敘述操作步驟 越詳細越好
作業環境
Google試算表
Google Apps Script
請給我後端gs完整程式碼、前端index完整程式碼
2. 資料表規則
請照下面規則建立分頁與欄位。
第1分頁: 菜單
A 欄: 品項
B 欄: 價格
第2分頁: 成員名單
A 欄: 姓名
第3分頁: 訂單紀錄
A 欄: 時間戳記
B 欄: 姓名
C 欄: 品項
D 欄: 金額
📄 解析結果(自動表格)
分頁:菜單
| A 欄 | B 欄 |
|---|---|
| 品項 | 價格 |
分頁:成員名單
| A 欄 |
|---|
| 姓名 |
分頁:訂單紀錄
| A 欄 | B 欄 | C 欄 | D 欄 |
|---|---|---|---|
| 時間戳記 | 姓名 | 品項 | 金額 |
3. 開啟 Apps Script
從試算表開 Apps Script,才能直接用 SpreadsheetApp 讀寫資料。
- 試算表上方選單:擴充功能 → Apps Script
- 刪掉預設的程式碼(或全部選取覆蓋)
-
保留/建立檔案:
- Code.gs
- index.html
4. 貼上後端(Code.gs)
把後端程式貼到 Apps Script 的 Code.gs。
// ========================================
// 午餐團購系統 - Google Apps Script 後端
// ========================================
/**
* 顯示前端網頁
*/
function doGet() {
return HtmlService.createHtmlOutputFromFile('index')
.setTitle('午餐團購系統')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
/**
* 取得所有資料(成員、菜單、訂單、統計)
*/
function getData() {
try {
var ss = SpreadsheetApp.getActiveSpreadsheet();
// 取得或建立工作表
var memberSheet = getOrCreateSheet(ss, '成員名單', ['姓名']);
var menuSheet = getOrCreateSheet(ss, '菜單', ['品項', '價格']);
var orderSheet = getOrCreateSheet(ss, '訂單記錄', ['姓名', '品項', '價格']);
// 讀取成員名單
var members = memberSheet.getRange('A2:A').getValues()
.flat()
.filter(function(v) { return v !== ''; });
// 讀取菜單
var menuData = menuSheet.getRange('A2:B').getValues();
var menu = [];
for (var i = 0; i < menuData.length; i++) {
if (menuData[i][0] !== '') {
menu.push([menuData[i][0], Number(menuData[i][1])]);
}
}
// 讀取所有訂單
var orders = [];
var orderData = orderSheet.getRange('A2:C').getValues();
for (var i = 0; i < orderData.length; i++) {
if (orderData[i][0] !== '') {
orders.push({
name: orderData[i][0],
item: orderData[i][1],
price: Number(orderData[i][2])
});
}
}
// 計算統計資料
var stats = calculateStats(orders);
return {
members: members,
menu: menu,
orders: orders,
stats: stats
};
} catch (error) {
Logger.log('getData 錯誤: ' + error.toString());
throw error;
}
}
/**
* 送出訂單
*/
function submitOrder(order) {
try {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var orderSheet = ss.getSheetByName('訂單記錄');
if (!orderSheet) {
throw new Error('找不到訂單記錄工作表');
}
// 驗證訂單資料
if (!order.name || !order.item || !order.price) {
throw new Error('訂單資料不完整');
}
// 新增訂單到工作表
orderSheet.appendRow([
order.name,
order.item,
Number(order.price)
]);
// 回傳更新後的資料
return getData();
} catch (error) {
Logger.log('submitOrder 錯誤: ' + error.toString());
throw error;
}
}
/**
* 刪除訂單
*/
function deleteOrder(index) {
try {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var orderSheet = ss.getSheetByName('訂單記錄');
if (!orderSheet) {
throw new Error('找不到訂單記錄工作表');
}
// 計算實際列號(+2 是因為:1. 有標題列 2. 索引從0開始但列從1開始)
var rowNumber = index + 2;
// 確認列號有效
var lastRow = orderSheet.getLastRow();
if (rowNumber <= 1 || rowNumber > lastRow) {
throw new Error('無效的訂單索引');
}
// 刪除該列
orderSheet.deleteRow(rowNumber);
// 回傳更新後的資料
return getData();
} catch (error) {
Logger.log('deleteOrder 錯誤: ' + error.toString());
throw error;
}
}
/**
* 計算統計資料
*/
function calculateStats(orders) {
var itemCounts = {}; // 品項統計
var personTotals = {}; // 個人金額統計
orders.forEach(function(order) {
// 品項數量統計
if (!itemCounts[order.item]) {
itemCounts[order.item] = 0;
}
itemCounts[order.item]++;
// 個人金額統計
if (!personTotals[order.name]) {
personTotals[order.name] = 0;
}
personTotals[order.name] += Number(order.price);
});
return {
itemCounts: itemCounts,
personTotals: personTotals
};
}
/**
* 取得或建立工作表
*/
function getOrCreateSheet(ss, sheetName, headers) {
var sheet = ss.getSheetByName(sheetName);
if (!sheet) {
// 建立新工作表
sheet = ss.insertSheet(sheetName);
// 設定標題列
if (headers && headers.length > 0) {
var headerRange = sheet.getRange(1, 1, 1, headers.length);
headerRange.setValues([headers]);
headerRange.setFontWeight('bold');
headerRange.setBackground('#ff6b9d');
headerRange.setFontColor('#ffffff');
}
Logger.log('已建立新工作表: ' + sheetName);
}
return sheet;
}
/**
* 初始化範例資料(首次使用時執行)
*/
function initializeSampleData() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
// 建立成員名單
var memberSheet = getOrCreateSheet(ss, '成員名單', ['姓名']);
memberSheet.getRange('A2:A6').setValues([
['小明'],
['小華'],
['小美'],
['小強'],
['小芳']
]);
// 建立菜單
var menuSheet = getOrCreateSheet(ss, '菜單', ['品項', '價格']);
menuSheet.getRange('A2:B11').setValues([
['雞腿便當', 80],
['排骨便當', 85],
['魚排便當', 90],
['素食便當', 75],
['焢肉便當', 80],
['咖哩雞便當', 85],
['宮保雞丁便當', 85],
['牛肉麵', 100],
['炒飯', 70],
['炒麵', 70]
]);
// 建立訂單記錄工作表
getOrCreateSheet(ss, '訂單記錄', ['姓名', '品項', '價格']);
SpreadsheetApp.getUi().alert('範例資料初始化完成!\n\n已建立:\n- 成員名單(5人)\n- 菜單(10項)\n- 訂單記錄(空白)');
}
/**
* 清空所有訂單
*/
function clearAllOrders() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var orderSheet = ss.getSheetByName('訂單記錄');
if (!orderSheet) {
SpreadsheetApp.getUi().alert('找不到訂單記錄工作表');
return;
}
var ui = SpreadsheetApp.getUi();
var response = ui.alert(
'確認清空',
'確定要清空所有訂單記錄嗎?此操作無法復原!',
ui.ButtonSet.YES_NO
);
if (response == ui.Button.YES) {
var lastRow = orderSheet.getLastRow();
if (lastRow > 1) {
orderSheet.deleteRows(2, lastRow - 1);
}
ui.alert('已清空所有訂單記錄!');
}
}
/**
* 建立自訂選單
*/
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('🍱 午餐團購')
.addItem('📱 開啟訂餐頁面', 'openWebApp')
.addSeparator()
.addItem('📊 初始化範例資料', 'initializeSampleData')
.addItem('🗑️ 清空所有訂單', 'clearAllOrders')
.addToUi();
}
/**
* 開啟網頁應用程式
*/
function openWebApp() {
var html = HtmlService.createHtmlOutputFromFile('index')
.setWidth(1000)
.setHeight(800);
SpreadsheetApp.getUi().showModalDialog(html, '🍱 午餐團購系統');
} 貼到 Apps Script 的 Code.gs 檔案
5. 貼上前端(index.html)
把前端程式貼到 Apps Script 的 index.html。
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" rel="stylesheet">
<style>
:root {
--primary-color: #ff6b9d;
--secondary-color: #ffd93d;
--success-color: #6bcf7f;
--danger-color: #ff4757;
--bg-gradient: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
--card-bg: #ffffff;
}
body {
font-family: 'Microsoft JhengHei', 'Comic Sans MS', sans-serif;
background: var(--bg-gradient);
color: #333;
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
.container {
max-width: 1000px;
width: 100%;
background: var(--card-bg);
padding: 40px;
border-radius: 30px;
box-shadow: 0 20px 60px rgba(255, 107, 157, 0.3);
position: relative;
overflow: hidden;
}
.container::before {
content: '🍕🍔🍟🌮🍜🍱🥗🍰';
position: absolute;
top: -20px;
left: 0;
right: 0;
font-size: 40px;
text-align: center;
opacity: 0.1;
letter-spacing: 20px;
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
h1 {
color: var(--primary-color);
text-align: center;
font-size: 2.5em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
animation: bounce 1s ease-in-out;
}
.subtitle {
text-align: center;
color: #ff8fab;
font-size: 1.1em;
margin-bottom: 30px;
font-weight: 500;
}
.form-group {
margin-bottom: 25px;
position: relative;
}
label {
display: block;
margin-bottom: 10px;
font-weight: bold;
color: #555;
font-size: 1.1em;
}
label::before {
content: '🌟 ';
font-size: 1.2em;
}
select, button {
width: 100%;
padding: 15px;
border-radius: 15px;
border: 3px solid #ffe0e9;
font-size: 16px;
transition: all 0.3s ease;
background: white;
}
select {
cursor: pointer;
font-weight: 500;
color: #555;
}
select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 4px rgba(255, 107, 157, 0.1);
}
button {
background: linear-gradient(135deg, var(--primary-color) 0%, #ff8fab 100%);
color: white;
border: none;
cursor: pointer;
font-weight: bold;
font-size: 1.2em;
margin-top: 10px;
box-shadow: 0 8px 20px rgba(255, 107, 157, 0.4);
position: relative;
overflow: hidden;
}
button::before {
content: '✨';
position: absolute;
left: 20px;
font-size: 1.5em;
}
button::after {
content: '✨';
position: absolute;
right: 20px;
font-size: 1.5em;
}
button:hover {
transform: translateY(-3px);
box-shadow: 0 12px 30px rgba(255, 107, 157, 0.5);
}
button:active {
transform: translateY(-1px);
}
button:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.stats-container {
margin-top: 40px;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
}
.stats-card {
background: linear-gradient(135deg, #fff5f7 0%, #ffe0e9 100%);
padding: 20px;
border-radius: 20px;
box-shadow: 0 8px 20px rgba(0,0,0,0.08);
border: 3px solid #ffc2d4;
position: relative;
overflow: hidden;
}
.stats-card.full-width {
grid-column: 1 / -1;
}
.stats-card::before {
content: '';
position: absolute;
top: -50px;
right: -50px;
width: 100px;
height: 100px;
background: rgba(255, 255, 255, 0.5);
border-radius: 50%;
}
.stats-card h3 {
margin-top: 0;
font-size: 1.3em;
color: var(--primary-color);
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.item-list {
list-style: none;
padding: 0;
font-size: 1em;
}
.item-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
margin-bottom: 8px;
background: white;
border-radius: 10px;
transition: all 0.3s ease;
border-left: 4px solid var(--secondary-color);
}
.item-list li:hover {
transform: translateX(5px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.item-list li span:first-child {
font-weight: 600;
color: #555;
flex: 1;
}
.item-list li span:nth-child(2) {
color: var(--primary-color);
font-weight: bold;
background: #ffe0e9;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.9em;
margin-right: 10px;
}
.order-list {
list-style: none;
padding: 0;
}
.order-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
margin-bottom: 10px;
background: white;
border-radius: 12px;
border-left: 4px solid var(--success-color);
transition: all 0.3s ease;
}
.order-item:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.order-info {
flex: 1;
display: flex;
align-items: center;
gap: 15px;
}
.order-person {
font-weight: bold;
color: #555;
min-width: 80px;
}
.order-item-name {
color: #666;
flex: 1;
}
.order-price {
color: var(--primary-color);
font-weight: bold;
background: #ffe0e9;
padding: 6px 12px;
border-radius: 20px;
min-width: 60px;
text-align: center;
}
.delete-btn {
background: linear-gradient(135deg, var(--danger-color) 0%, #ff6b81 100%);
color: white;
border: none;
padding: 8px 16px;
border-radius: 10px;
cursor: pointer;
font-size: 0.9em;
font-weight: bold;
transition: all 0.3s ease;
box-shadow: 0 4px 10px rgba(255, 71, 87, 0.3);
}
.delete-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(255, 71, 87, 0.4);
}
.delete-btn:active {
transform: translateY(0);
}
#loading {
text-align: center;
color: var(--primary-color);
font-size: 1.5em;
padding: 60px;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.empty-state {
text-align: center;
color: #999;
padding: 20px;
font-style: italic;
}
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0);
background: white;
padding: 40px;
border-radius: 30px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
z-index: 1000;
text-align: center;
opacity: 0;
transition: all 0.3s ease;
min-width: 300px;
}
.modal.show {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
}
.modal .emoji {
font-size: 80px;
animation: bounceIn 0.6s ease-out;
}
@keyframes bounceIn {
0% { transform: scale(0); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 999;
display: none;
}
.overlay.show {
display: block;
}
</style>
</head>
<body>
<div class="overlay" id="overlay"></div>
<div class="modal" id="successModal">
<div class="emoji">🎉</div>
<h2 style="color: var(--primary-color); margin: 20px 0 10px 0;">訂單成功!</h2>
<p style="color: #666; font-size: 1.1em;">美味即將送達~</p>
</div>
<div class="modal" id="deleteModal">
<div class="emoji">✅</div>
<h2 style="color: var(--success-color); margin: 20px 0 10px 0;">刪除成功!</h2>
<p style="color: #666; font-size: 1.1em;">訂單已移除</p>
</div>
<div class="container animate__animated animate__fadeIn">
<h1>🍽️ 超可愛午餐團購 🍽️</h1>
<p class="subtitle">讓我們一起享用美味的午餐吧!</p>
<div id="loading">🍕 載入美味菜單中... 🍔</div>
<div id="main-content" style="display:none;">
<div class="form-group">
<label>👤 你是誰呢?</label>
<select id="memberSelect">
<option value="">請選擇你的名字</option>
</select>
</div>
<div class="form-group">
<label>🍴 想吃什麼美食?</label>
<select id="menuSelect">
<option value="">請選擇想吃的餐點</option>
</select>
</div>
<button onclick="sendOrder()" id="submitBtn">送出我的美味訂單</button>
<div class="stats-container">
<div class="stats-card full-width animate__animated animate__fadeInUp">
<h3>📝 所有訂單明細</h3>
<ul id="orderList" class="order-list">
<li class="empty-state">尚無訂單資料</li>
</ul>
</div>
<div class="stats-card animate__animated animate__fadeInLeft">
<h3>📊 品項統計</h3>
<ul id="itemStats" class="item-list">
<li class="empty-state">尚無訂單資料</li>
</ul>
</div>
<div class="stats-card animate__animated animate__fadeInRight">
<h3>💰 每人應付金額</h3>
<ul id="personStats" class="item-list">
<li class="empty-state">尚無訂單資料</li>
</ul>
</div>
</div>
</div>
</div>
<script>
let menuData = [];
let allOrders = [];
// 初始化載入
window.onload = () => {
google.script.run.withSuccessHandler(renderPage).getData();
};
function renderPage(data) {
console.log('收到的資料:', data);
document.getElementById('loading').style.display = 'none';
document.getElementById('main-content').style.display = 'block';
const memberSelect = document.getElementById('memberSelect');
const menuSelect = document.getElementById('menuSelect');
// 渲染名單
if (data.members && data.members.length > 0) {
memberSelect.innerHTML = '<option value="">請選擇你的名字</option>' +
data.members.map(m => `<option value="${m}">${m}</option>`).join('');
} else {
memberSelect.innerHTML = '<option value="">無成員資料</option>';
}
// 渲染菜單
menuData = data.menu || [];
if (menuData.length > 0) {
menuSelect.innerHTML = '<option value="">請選擇想吃的餐點</option>' +
menuData.map(m => {
const itemName = m[0] || '';
const price = m[1] || 0;
return `<option value="${itemName}">${itemName} ($${price})</option>`;
}).join('');
} else {
menuSelect.innerHTML = '<option value="">無菜單資料</option>';
}
// 儲存所有訂單
allOrders = data.orders || [];
// 更新顯示
updateAllDisplays(data.stats);
}
function updateAllDisplays(stats) {
updateOrderList();
updateStatsDisplay(stats);
}
function updateOrderList() {
const orderList = document.getElementById('orderList');
if (allOrders && allOrders.length > 0) {
orderList.innerHTML = allOrders.map((order, index) => `
<li class="order-item animate__animated animate__fadeIn">
<div class="order-info">
<span class="order-person">👤 ${order.name}</span>
<span class="order-item-name">🍱 ${order.item}</span>
<span class="order-price">$${order.price}</span>
</div>
<button class="delete-btn" onclick="deleteOrder(${index})">🗑️ 刪除</button>
</li>
`).join('');
} else {
orderList.innerHTML = '<li class="empty-state">尚無訂單資料</li>';
}
}
function updateStatsDisplay(stats) {
const itemStats = document.getElementById('itemStats');
const personStats = document.getElementById('personStats');
// 品項統計
if (stats && stats.itemCounts && Object.keys(stats.itemCounts).length > 0) {
itemStats.innerHTML = Object.entries(stats.itemCounts)
.map(([name, count]) => `<li><span>🍱 ${name}</span><span>${count} 份</span></li>`)
.join('');
} else {
itemStats.innerHTML = '<li class="empty-state">尚無訂單資料</li>';
}
// 個人金額統計
if (stats && stats.personTotals && Object.keys(stats.personTotals).length > 0) {
personStats.innerHTML = Object.entries(stats.personTotals)
.map(([name, total]) => `<li><span>👤 ${name}</span><span>$${total}</span></li>`)
.join('');
} else {
personStats.innerHTML = '<li class="empty-state">尚無訂單資料</li>';
}
}
function sendOrder() {
const memberSelect = document.getElementById('memberSelect');
const menuSelect = document.getElementById('menuSelect');
const selectedName = memberSelect.value;
const selectedItem = menuSelect.value;
// 驗證是否已選擇
if (!selectedName) {
alert('請先選擇你的名字!😊');
return;
}
if (!selectedItem) {
alert('請先選擇想吃的餐點!🍴');
return;
}
const btn = document.getElementById('submitBtn');
btn.disabled = true;
btn.innerText = '🍳 處理中...';
// 找到對應的價格
const menuItem = menuData.find(m => m[0] === selectedItem);
const price = menuItem ? menuItem[1] : 0;
const order = {
name: selectedName,
item: selectedItem,
price: price
};
console.log('送出訂單:', order);
google.script.run
.withSuccessHandler((newData) => {
console.log('訂單成功回傳:', newData);
// 更新所有資料
allOrders = newData.orders || [];
updateAllDisplays(newData.stats);
// 顯示成功動畫
showModal('successModal');
// 恢復按鈕
btn.disabled = false;
btn.innerText = '送出我的美味訂單';
})
.withFailureHandler((error) => {
console.error('訂單失敗:', error);
alert('訂單送出失敗,請稍後再試!');
btn.disabled = false;
btn.innerText = '送出我的美味訂單';
})
.submitOrder(order);
}
function deleteOrder(index) {
if (!confirm('確定要刪除這個訂單嗎?🤔')) {
return;
}
console.log('刪除訂單索引:', index);
google.script.run
.withSuccessHandler((newData) => {
console.log('刪除成功回傳:', newData);
// 更新所有資料
allOrders = newData.orders || [];
updateAllDisplays(newData.stats);
// 顯示刪除成功動畫
showModal('deleteModal');
})
.withFailureHandler((error) => {
console.error('刪除失敗:', error);
alert('刪除失敗,請稍後再試!');
})
.deleteOrder(index);
}
function showModal(modalId) {
const overlay = document.getElementById('overlay');
const modal = document.getElementById(modalId);
overlay.classList.add('show');
modal.classList.add('show');
setTimeout(() => {
overlay.classList.remove('show');
modal.classList.remove('show');
}, 2000);
}
</script>
</body>
</html>
貼到 Apps Script 的 index.html 檔案
6. 部署
部署成 Web App 後,就會拿到可分享網址。
- Apps Script 右上:部署 → 新部署
- 類型選:Web app
- 執行身分:你
- 存取權限:任何人
- 完成後複製網址
💬 評論區
老師們的交流與提問
登入 後即可發表評論