一般記帳軟體會有哪一些功能?
記帳軟體的功能因應用途和複雜程度可能有所不同,但以下是一般記帳軟體常見的功能:
選擇記帳軟體時,可根據個人的財務管理需求,優先考慮以上功能是否齊全和適合自己的使用情境。
我想用google試算表加google app script寫一個手機介面的記帳網頁工具,要有基本記帳功能 收入與支出記錄:用於記錄日常的收入與支出項目。 分類管理:可以依據支出/收入類別(如餐飲、交通、娛樂等)進行分類。 及 預算管理 預算設定:為特定類別設定預算。 超支提醒:當支出接近或超過預算時提醒用戶。 以上的功能。 我會先建立一個試算表,把所有資料都儲存在同一個試算表中,手機介面要好看一點,給我完整.gs及.html。
Thought about Google試算表記帳工具 花的時間為 11 秒
以下範例示範如何在同一個 Google 試算表中,使用 Google Apps Script + HTML 網頁,製作一個具備「基本記帳功能」與「預算管理」的簡易手機介面。包含:
為了讓結構清晰,建議在 同一個試算表 中建立以下三個工作表(Sheet):
日期, 類型(收入/支出), 類別, 金額, 備註類別, 預算金額類別名稱, 類型(收入/支出)例如:
Transactions
A B C D E 2025/01/01 支出 餐飲 200 中餐 2025/01/02 收入 薪資 50000 公司
Budgets
A B 餐飲 5000 交通 2000 娛樂 3000
Categories
A B 餐飲 支出 交通 支出 娛樂 支出 薪資 收入
Code.gs(放 Apps Script 後端程式碼)Index.html(放前端網頁程式碼)以下示例程式中,請依照你自己的需求修改欄位或工作表名稱。
/************************************************
* 全域設定
************************************************/
// 試算表ID(若同一個專案,可使用 SpreadsheetApp.getActiveSpreadsheet() 取得)
var SPREADSHEET_ID = SpreadsheetApp.getActiveSpreadsheet().getId();
// 設定試算表中各工作表名稱
var SHEET_TRANSACTIONS = 'Transactions';
var SHEET_BUDGETS = 'Budgets';
var SHEET_CATEGORIES = 'Categories';
/************************************************
* 網頁入口
************************************************/
function doGet(e) {
return HtmlService
.createTemplateFromFile('Index')
.evaluate()
// 手機介面自適應
.addMetaTag('viewport', 'width=device-width, initial-scale=1.0')
.setTitle('簡易記帳工具');
}
/************************************************
* 輔助函式 - 讀取 HTML 文件
************************************************/
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
/************************************************
* 1. 取得分類清單
************************************************/
function getCategories() {
var ss = SpreadsheetApp.openById(SPREADSHEET_ID);
var sheet = ss.getSheetByName(SHEET_CATEGORIES);
var data = sheet.getDataRange().getValues(); // 讀取所有
// 假設結構:
// A欄:類別名稱
// B欄:類型(收入/支出)
// 只取第一列以外的資料
var categories = [];
for (var i = 1; i < data.length; i++) {
var row = data[i];
categories.push({
name: row[0],
type: row[1]
});
}
return categories;
}
/************************************************
* 2. 取得預算設定
************************************************/
function getBudgets() {
var ss = SpreadsheetApp.openById(SPREADSHEET_ID);
var sheet = ss.getSheetByName(SHEET_BUDGETS);
var data = sheet.getDataRange().getValues(); // 讀取所有
// 結構:
// A欄:類別
// B欄:預算金額
// 只取第一列以外的資料
var budgets = {};
for (var i = 1; i < data.length; i++) {
var row = data[i];
var category = row[0];
var budget = row[1];
budgets[category] = budget;
}
return budgets;
}
/************************************************
* 3. 寫入交易紀錄
************************************************/
function saveTransaction(transaction) {
// transaction 物件包含以下欄位:
// { date, type, category, amount, note }
var ss = SpreadsheetApp.openById(SPREADSHEET_ID);
var sheet = ss.getSheetByName(SHEET_TRANSACTIONS);
// 寫入時,依照交易表格式調整
// A欄:日期
// B欄:類型(收入/支出)
// C欄:類別
// D欄:金額
// E欄:備註
sheet.appendRow([
transaction.date,
transaction.type,
transaction.category,
parseFloat(transaction.amount),
transaction.note
]);
return '交易已儲存';
}
/************************************************
* 4. 取得本月支出總額 (依類別)
************************************************/
function getMonthlySpending() {
var ss = SpreadsheetApp.openById(SPREADSHEET_ID);
var sheet = ss.getSheetByName(SHEET_TRANSACTIONS);
var data = sheet.getDataRange().getValues();
// 取得當下年月,用於判斷「本月」
var now = new Date();
var currentMonth = now.getMonth();
var currentYear = now.getFullYear();
// 結構:
// A: 日期
// B: 類型
// C: 類別
// D: 金額
// E: 備註
// 從第二列開始讀取
var spendingByCategory = {};
for (var i = 1; i < data.length; i++) {
var row = data[i];
var date = new Date(row[0]);
var type = row[1];
var category = row[2];
var amount = parseFloat(row[3]) || 0;
// 只統計支出 + 當月
if (type === '支出' &&
date.getMonth() === currentMonth &&
date.getFullYear() === currentYear) {
if (!spendingByCategory[category]) {
spendingByCategory[category] = 0;
}
spendingByCategory[category] += amount;
}
}
return spendingByCategory;
}
/************************************************
* 5. 結合預算 & 支出,回傳給前端
************************************************/
function getDashboardData() {
var categories = getCategories(); // 全部分類
var budgets = getBudgets(); // 預算表
var spendingByCategory = getMonthlySpending(); // 本月支出
// 前端可直接使用的結構
var dashboardData = [];
categories.forEach(function(cat) {
// 只關心「支出」類型需要預算,收入類型通常不預算
if (cat.type === '支出') {
var categoryName = cat.name;
var budget = budgets[categoryName] ? budgets[categoryName] : 0;
var spent = spendingByCategory[categoryName] ? spendingByCategory[categoryName] : 0;
// 計算超支或接近超支
// 例如當 spent >= 0.8 * budget 就算「接近超支」
var warningLevel = 'normal';
if (budget > 0) {
if (spent >= budget) {
warningLevel = 'over'; // 超支
} else if (spent >= 0.8 * budget) {
warningLevel = 'near'; // 接近超支
}
}
dashboardData.push({
category: categoryName,
budget: budget,
spent: spent,
warningLevel: warningLevel
});
}
});
return dashboardData;
}
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
/* 簡單的行動裝置風格調整 */
body {
font-family: sans-serif;
margin: 10px;
}
h1 {
text-align: center;
}
.container {
max-width: 400px;
margin: 0 auto;
}
.input-field {
margin-bottom: 10px;
}
label {
display: block;
margin-bottom: 4px;
font-weight: bold;
}
input, select {
width: 100%;
padding: 8px;
font-size: 16px;
box-sizing: border-box;
}
button {
width: 100%;
padding: 10px;
font-size: 16px;
background: #4CAF50;
color: #fff;
border: none;
cursor: pointer;
}
button:hover {
background: #45a049;
}
.budget-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.budget-table th, .budget-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.warning-normal {
background-color: #ffffff;
}
.warning-near {
background-color: #fff7e6; /* 淡黃色 */
}
.warning-over {
background-color: #ffdddd; /* 淡紅色 */
}
.message {
margin-top: 10px;
text-align: center;
}
</style>
</head>
<body>
<h1>簡易記帳工具</h1>
<div class="container">
<!-- 輸入表單 -->
<div class="input-field">
<label for="date">日期</label>
<input type="date" id="date" />
</div>
<div class="input-field">
<label for="type">類型</label>
<select id="type">
<option value="支出" selected>支出</option>
<option value="收入">收入</option>
</select>
</div>
<div class="input-field">
<label for="category">類別</label>
<select id="category"></select>
</div>
<div class="input-field">
<label for="amount">金額</label>
<input type="number" id="amount" step="0.01" />
</div>
<div class="input-field">
<label for="note">備註</label>
<input type="text" id="note" />
</div>
<button onclick="submitForm()">新增交易</button>
<div class="message" id="msgBox"></div>
<!-- 預算表 -->
<table class="budget-table" id="budgetTable">
<thead>
<tr>
<th>類別</th>
<th>預算</th>
<th>本月已用</th>
</tr>
</thead>
<tbody id="budgetTableBody"></tbody>
</table>
</div>
<script>
// 前端初始化:載入分類、載入預算使用狀況
document.addEventListener('DOMContentLoaded', function() {
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
refreshDashboard();
});
// 填充分類下拉選單
function populateCategorySelect(categories) {
var categorySelect = document.getElementById('category');
categorySelect.innerHTML = '';
// 預設先顯示支出類別
categories.forEach(function(cat) {
// 也可以只顯示支出: if (cat.type === '支出') {...}
var option = document.createElement('option');
option.value = cat.name;
option.text = cat.name + ' (' + cat.type + ')';
categorySelect.appendChild(option);
});
}
// 提交表單
function submitForm() {
var date = document.getElementById('date').value;
var type = document.getElementById('type').value;
var category = document.getElementById('category').value;
var amount = document.getElementById('amount').value;
var note = document.getElementById('note').value;
if (!date || !type || !category || !amount) {
showMessage('請填寫完整資訊', true);
return;
}
var transaction = {
date: date,
type: type,
category: category,
amount: amount,
note: note
};
google.script.run.withSuccessHandler(function(res){
showMessage(res, false);
// 清空輸入
document.getElementById('date').value = '';
document.getElementById('amount').value = '';
document.getElementById('note').value = '';
// 刷新預算表
refreshDashboard();
}).withFailureHandler(function(err){
showMessage('寫入失敗: ' + err.message, true);
}).saveTransaction(transaction);
}
// 刷新預算與支出狀況
function refreshDashboard() {
google.script.run.withSuccessHandler(function(dashboardData){
populateBudgetTable(dashboardData);
}).getDashboardData();
}
// 顯示預算表
function populateBudgetTable(data) {
var tbody = document.getElementById('budgetTableBody');
tbody.innerHTML = '';
data.forEach(function(item){
var tr = document.createElement('tr');
// 依照 warningLevel 套用不同背景
if (item.warningLevel === 'over') {
tr.className = 'warning-over';
} else if (item.warningLevel === 'near') {
tr.className = 'warning-near';
} else {
tr.className = 'warning-normal';
}
var tdCategory = document.createElement('td');
tdCategory.textContent = item.category;
var tdBudget = document.createElement('td');
tdBudget.textContent = item.budget || 0;
var tdSpent = document.createElement('td');
tdSpent.textContent = item.spent;
tr.appendChild(tdCategory);
tr.appendChild(tdBudget);
tr.appendChild(tdSpent);
tbody.appendChild(tr);
});
}
// 提示訊息
function showMessage(msg, isError) {
var msgBox = document.getElementById('msgBox');
msgBox.style.color = isError ? 'red' : 'green';
msgBox.textContent = msg;
}
</script>
</body>
</html>
建立 Sheets:
Transactions (A欄:日期、B欄:類型、C欄:類別、D欄:金額、E欄:備註)Budgets (A欄:類別、B欄:預算金額)Categories (A欄:類別名稱、B欄:類型(收入/支出))複製程式碼
Code.gs 中貼上 Apps Script 後端程式碼Index.html 中貼上前端 HTML 程式碼部署為網頁應用程式
功能驗證
注意:
- 若你想要在手機上使用得更方便,可以將網頁應用程式網址「加入主畫面捷徑」,就像安裝成 App。
- 此範例的「接近超支」閾值為
0.8 * 預算,可自行在程式中修改。- 如果想要記錄「收入」類型,但不需要預算,程式中目前僅在
getDashboardData()中針對支出類型做統計。你也可以擴充更多功能,例如對收入類型也做不同分析。
這樣,就完成了以 Google 試算表 + Google Apps Script 打造的 手機介面記帳工具,並同時支援 預算管理 與 超支提醒。可以依照自己的需求再進一步優化介面或邏輯。祝開發順利!
讓gas程式直接在同一個試算表中新增需要的工作表來記錄資料,網頁介面會自動抓取當下日期及時間(可再手動修改)來讓使用者新增支出或收入,並預設幾個常用的記帳類別
Thought about automated sheet creation and categories 花的時間為 12 秒
以下示範一個完整的 Google Apps Script 專案(含 .gs 與 .html),能在同一個試算表中:
請在「Apps Script 編輯器」中建立一個名為 Code.gs 的檔案,將下列程式貼上。
/************************************************
* 全域設定
************************************************/
// 如果你想指定特定試算表,請填寫其ID;
// 若在同一個試算表專案中,可直接用 SpreadsheetApp.getActiveSpreadsheet()
var SPREADSHEET_ID = SpreadsheetApp.getActiveSpreadsheet().getId();
// 預計要建立的工作表名稱
var SHEET_TRANSACTIONS = 'Transactions'; // 交易紀錄
var SHEET_BUDGETS = 'Budgets'; // 預算設定
var SHEET_CATEGORIES = 'Categories'; // 分類清單
/************************************************
* 網頁入口
************************************************/
function doGet() {
// 先確保必需的工作表存在,沒有就自動建立
setupSpreadsheet();
// 回傳網頁
return HtmlService
.createTemplateFromFile('Index')
.evaluate()
.addMetaTag('viewport', 'width=device-width, initial-scale=1.0')
.setTitle('簡易記帳工具');
}
/************************************************
* 一次載入前端 HTML
************************************************/
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
/************************************************
* 0. 自動建立所需的工作表 (若尚未存在)
************************************************/
function setupSpreadsheet() {
var ss = SpreadsheetApp.openById(SPREADSHEET_ID);
// 1) 建立 Transactions (交易紀錄)
var sheetTx = ss.getSheetByName(SHEET_TRANSACTIONS);
if (!sheetTx) {
sheetTx = ss.insertSheet(SHEET_TRANSACTIONS);
// 設定欄位標題
sheetTx.appendRow(['日期時間', '類型(收入/支出)', '類別', '金額', '備註']);
}
// 2) 建立 Budgets (預算設定)
var sheetBd = ss.getSheetByName(SHEET_BUDGETS);
if (!sheetBd) {
sheetBd = ss.insertSheet(SHEET_BUDGETS);
// 設定欄位標題
sheetBd.appendRow(['類別', '預算金額']);
// 可以視需求預先放一些常用的預算設定 (範例)
sheetBd.appendRow(['飲食', 5000]);
sheetBd.appendRow(['交通', 2000]);
sheetBd.appendRow(['娛樂', 3000]);
}
// 3) 建立 Categories (分類清單)
var sheetCat = ss.getSheetByName(SHEET_CATEGORIES);
if (!sheetCat) {
sheetCat = ss.insertSheet(SHEET_CATEGORIES);
// 設定欄位標題
sheetCat.appendRow(['類別名稱', '類型(收入/支出)']);
// 常用的記帳類別 (預設)
var defaultCategories = [
['飲食', '支出'],
['交通', '支出'],
['娛樂', '支出'],
['房租', '支出'],
['薪資', '收入'],
];
sheetCat.getRange(2, 1, defaultCategories.length, 2).setValues(defaultCategories);
}
}
/************************************************
* 1. 取得分類清單
************************************************/
function getCategories() {
var ss = SpreadsheetApp.openById(SPREADSHEET_ID);
var sheet = ss.getSheetByName(SHEET_CATEGORIES);
var data = sheet.getDataRange().getValues(); // 全部資料
// 假設第一列是標題,所以從第二列開始
var categories = [];
for (var i = 1; i < data.length; i++) {
var row = data[i];
var name = row[0];
var type = row[1];
if (name) {
categories.push({
name: name,
type: type
});
}
}
return categories;
}
/************************************************
* 2. 取得預算設定
************************************************/
function getBudgets() {
var ss = SpreadsheetApp.openById(SPREADSHEET_ID);
var sheet = ss.getSheetByName(SHEET_BUDGETS);
var data = sheet.getDataRange().getValues();
// A欄: 類別, B欄: 預算金額
var budgets = {};
for (var i = 1; i < data.length; i++) {
var row = data[i];
var category = row[0];
var budget = row[1];
if (category) {
budgets[category] = budget;
}
}
return budgets;
}
/************************************************
* 3. 寫入交易紀錄
************************************************/
function saveTransaction(transaction) {
// transaction = { dateTime, type, category, amount, note }
var ss = SpreadsheetApp.openById(SPREADSHEET_ID);
var sheet = ss.getSheetByName(SHEET_TRANSACTIONS);
// 寫入 (日期時間, 類型, 類別, 金額, 備註)
sheet.appendRow([
transaction.dateTime,
transaction.type,
transaction.category,
parseFloat(transaction.amount),
transaction.note
]);
return '交易已儲存!';
}
/************************************************
* 4. 取得本月支出總額 (依類別)
************************************************/
function getMonthlySpending() {
var ss = SpreadsheetApp.openById(SPREADSHEET_ID);
var sheet = ss.getSheetByName(SHEET_TRANSACTIONS);
var data = sheet.getDataRange().getValues();
// 取得當前年月,用於判斷「本月」
var now = new Date();
var currentMonth = now.getMonth();
var currentYear = now.getFullYear();
// 結構:
// A: 日期時間
// B: 類型
// C: 類別
// D: 金額
// E: 備註
var spendingByCategory = {};
// 從第二列開始 (第一列是標題)
for (var i = 1; i < data.length; i++) {
var row = data[i];
var dateTime = row[0];
var type = row[1];
var category = row[2];
var amount = parseFloat(row[3]) || 0;
var txDate = new Date(dateTime);
// 僅計算「支出」+「本月」
if (type === '支出' &&
txDate.getMonth() === currentMonth &&
txDate.getFullYear() === currentYear) {
if (!spendingByCategory[category]) {
spendingByCategory[category] = 0;
}
spendingByCategory[category] += amount;
}
}
return spendingByCategory;
}
/************************************************
* 5. 彙整預算 & 支出,回傳給前端
************************************************/
function getDashboardData() {
var budgets = getBudgets();
var categories = getCategories();
var spendingByCategory = getMonthlySpending();
var dashboardData = [];
categories.forEach(function(cat) {
if (cat.type === '支出') {
var categoryName = cat.name;
var budget = budgets[categoryName] ? budgets[categoryName] : 0;
var spent = spendingByCategory[categoryName] ? spendingByCategory[categoryName] : 0;
// 超支 / 接近超支 (可自訂閾值)
var warningLevel = 'normal';
if (budget > 0) {
if (spent >= budget) {
warningLevel = 'over'; // 超支
} else if (spent >= 0.8 * budget) {
warningLevel = 'near'; // 接近超支
}
}
dashboardData.push({
category: categoryName,
budget: budget,
spent: spent,
warningLevel: warningLevel
});
}
});
return dashboardData;
}
請在同一個專案中建立 Index.html。以下為簡易範例,帶有手機友善介面,並自動載入「現在日期與時間」,供使用者新增交易紀錄。
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
/* 手機友善基礎樣式 */
body {
font-family: sans-serif;
margin: 10px;
}
h1 {
text-align: center;
margin-bottom: 0;
}
.sub-title {
text-align: center;
color: #666;
margin-top: 4px;
margin-bottom: 20px;
}
.container {
max-width: 400px;
margin: 0 auto;
}
.input-field {
margin-bottom: 12px;
}
label {
display: block;
font-weight: bold;
margin-bottom: 4px;
}
input, select {
width: 100%;
box-sizing: border-box;
padding: 8px;
font-size: 16px;
}
button {
width: 100%;
padding: 10px;
font-size: 16px;
background: #4CAF50;
color: #fff;
border: none;
cursor: pointer;
margin-top: 10px;
}
button:hover {
background: #45a049;
}
/* 預算表 */
.budget-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.budget-table th, .budget-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.warning-normal {
background-color: #ffffff;
}
.warning-near {
background-color: #fff8db; /* 淡黃色 */
}
.warning-over {
background-color: #ffdcdc; /* 淡紅色 */
}
.message {
margin-top: 8px;
text-align: center;
}
</style>
</head>
<body>
<h1>簡易記帳工具</h1>
<div class="sub-title">Google Apps Script + 試算表</div>
<div class="container">
<!-- 輸入表單 -->
<div class="input-field">
<label for="dateTime">日期與時間</label>
<!-- 預設使用 datetime-local,自動帶入現在時間 (可手動修改) -->
<input type="datetime-local" id="dateTime" />
</div>
<div class="input-field">
<label for="type">類型</label>
<select id="type">
<option value="支出" selected>支出</option>
<option value="收入">收入</option>
</select>
</div>
<div class="input-field">
<label for="category">類別</label>
<select id="category"></select>
</div>
<div class="input-field">
<label for="amount">金額</label>
<input type="number" id="amount" step="0.01" />
</div>
<div class="input-field">
<label for="note">備註</label>
<input type="text" id="note" />
</div>
<button onclick="submitForm()">新增交易</button>
<div class="message" id="msgBox"></div>
<!-- 預算表 -->
<table class="budget-table" id="budgetTable">
<thead>
<tr>
<th>類別</th>
<th>預算</th>
<th>本月已用</th>
</tr>
</thead>
<tbody id="budgetTableBody"></tbody>
</table>
</div>
<script>
/********************************************
* 前端初始化:載入「分類清單」與「預算表」
********************************************/
document.addEventListener('DOMContentLoaded', function() {
// 1) 自動帶入現在日期與時間 (可手動修改)
setDefaultDateTime();
// 2) 載入分類 (for 下拉選單)
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
// 3) 載入預算/支出狀況
refreshDashboard();
});
// 將 category 選單填入
function populateCategorySelect(categories) {
var categorySelect = document.getElementById('category');
categorySelect.innerHTML = '';
// 預設只顯示「支出」和「收入」全部?也可以判斷
categories.forEach(function(cat) {
var option = document.createElement('option');
option.value = cat.name;
option.text = cat.name + ' (' + cat.type + ')';
categorySelect.appendChild(option);
});
}
// 設定「現在時間」到 datetime-local input
function setDefaultDateTime() {
var now = new Date();
// 將目前時間轉成 ISO 8601,再截取 yyyy-mm-ddThh:mm 方便給 datetime-local
var localISO = new Date(now.getTime() - now.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16); // 'YYYY-MM-DDTHH:MM'
document.getElementById('dateTime').value = localISO;
}
// 提交表單:新增交易紀錄
function submitForm() {
var dateTime = document.getElementById('dateTime').value;
var type = document.getElementById('type').value;
var category = document.getElementById('category').value;
var amount = document.getElementById('amount').value;
var note = document.getElementById('note').value;
// 簡易驗證
if (!dateTime || !type || !category || !amount) {
showMessage('請填寫完整資訊', true);
return;
}
var transaction = {
dateTime: dateTime,
type: type,
category: category,
amount: amount,
note: note
};
google.script.run
.withSuccessHandler(function(msg) {
showMessage(msg, false);
// 新增成功後,清空部份欄位
setDefaultDateTime();
document.getElementById('amount').value = '';
document.getElementById('note').value = '';
// 刷新預算表
refreshDashboard();
})
.withFailureHandler(function(err) {
showMessage('寫入失敗: ' + err.message, true);
})
.saveTransaction(transaction);
}
// 刷新預算表
function refreshDashboard() {
google.script.run
.withSuccessHandler(function(dashboardData){
populateBudgetTable(dashboardData);
})
.getDashboardData();
}
// 將預算與支出狀況顯示在表格上
function populateBudgetTable(data) {
var tbody = document.getElementById('budgetTableBody');
tbody.innerHTML = '';
data.forEach(function(item){
var tr = document.createElement('tr');
// 根據 warningLevel 使用不同顯示顏色
if (item.warningLevel === 'over') {
tr.className = 'warning-over';
} else if (item.warningLevel === 'near') {
tr.className = 'warning-near';
} else {
tr.className = 'warning-normal';
}
// 類別
var tdCat = document.createElement('td');
tdCat.textContent = item.category;
tr.appendChild(tdCat);
// 預算
var tdBudget = document.createElement('td');
tdBudget.textContent = item.budget || 0;
tr.appendChild(tdBudget);
// 已用
var tdSpent = document.createElement('td');
tdSpent.textContent = item.spent;
tr.appendChild(tdSpent);
tbody.appendChild(tr);
});
}
// 顯示訊息
function showMessage(msg, isError) {
var msgBox = document.getElementById('msgBox');
msgBox.style.color = isError ? 'red' : 'green';
msgBox.textContent = msg;
}
</script>
</body>
</html>
在試算表中建立 Apps Script 專案
Code.gs 和 Index.html,分別貼上上方程式碼部署為網頁應用程式
首次使用
setupSpreadsheet() 會檢查並自動建立 Transactions, Budgets, Categories 這三個工作表(如果尚未存在)Categories 中預設一些常見類別(飲食、交通、娛樂、房租、薪資...),在 Budgets 也自動填入範例預算操作流程
後續擴充
Budgets 工作表中自行修改或增加類別的預算Categories 工作表中增修自訂分類(收入或支出)以上程式碼即能實現以下需求:
如有需要,可再根據實際需求進一步客製化、優化介面或功能。祝開發順利!
web端加上個一查看該月支出的按鈕,按下會在新圖層用表格列出該月的支出列表及總計,並可按類別排序
Thought about web interface enhancements 花的時間為 26 秒
以下範例示範如何在既有的「簡易記帳工具」網頁上,增加一個「查看該月支出」按鈕,點擊後會在「浮層 (Modal)」或「區塊」中列出本月所有支出明細及總計,並提供按類別排序功能。
後端 (Code.gs):
getThisMonthExpenses() 函式,回傳「本月支出」的交易清單 (含日期時間、類型、類別、金額、備註)。前端 (Index.html):
viewMonthlyExpenses()。viewMonthlyExpenses() 會透過 google.script.run.getThisMonthExpenses() 取得資料後,顯示在「浮層 (Modal)」中的一個表格。提示:
- 若不想用浮層 (Modal) 方式,也可直接在頁面上切換一個顯示區塊 (div) 與表格。
- 以下範例用最簡單的浮層呈現方式(CSS + JS)。可依需求自行美化。
請在既有的 Code.gs 中,新增下列函式 getThisMonthExpenses()。其餘原有程式碼(如 setupSpreadsheet()、saveTransaction() 等)維持不動即可。
/**
* 回傳「本月支出」的所有交易明細
* @return {Array} expenses 本月支出的交易陣列,每筆包含:
* { dateTime, type, category, amount, note }
*/
function getThisMonthExpenses() {
var ss = SpreadsheetApp.openById(SPREADSHEET_ID);
var sheet = ss.getSheetByName(SHEET_TRANSACTIONS);
var data = sheet.getDataRange().getValues();
// 取得當前年、月 (供篩選)
var now = new Date();
var currentYear = now.getFullYear();
var currentMonth = now.getMonth(); // 0 ~ 11
// A: 日期時間, B: 類型, C: 類別, D: 金額, E: 備註
var expenses = [];
for(var i=1; i < data.length; i++){
var row = data[i];
var dateTime = new Date(row[0]);
var type = row[1];
var category = row[2];
var amount = parseFloat(row[3]) || 0;
var note = row[4] || '';
// 篩選「本年、本月」且「類型為支出」的交易
if(type === '支出' &&
dateTime.getFullYear() === currentYear &&
dateTime.getMonth() === currentMonth) {
expenses.push({
dateTime: row[0], // 直接保留原字串或 row[0].toString()
type: type,
category: category,
amount: amount,
note: note
});
}
}
return expenses;
}
在你既有的 Index.html 中,做以下幾項更新/新增:
viewMonthlyExpenses(),按鈕觸發 → 呼叫 google.script.run.getThisMonthExpenses() → 成功後把資料存到全域變數並渲染表格renderMonthlyExpenses() 用於顯示/刷新表格sortExpensesByCategory() 用於對本月支出資料以「類別」排序 (升冪)closeMonthlyExpensesModal() 關閉浮層以下為更新後的範例完整碼。請特別留意:
Index.html,尤其 CSS、JS 片段要確保別與原程式衝突。Index.html,請在 <body> 裡合適位置插入「查看該月支出」按鈕、Modal 區塊,以及在 <script> 中增加新函式即可。<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
/* ========== 既有基礎樣式 (可依需求保留/修改) ========== */
body {
font-family: sans-serif;
margin: 10px;
}
h1 {
text-align: center;
margin-bottom: 0;
}
.sub-title {
text-align: center;
color: #666;
margin-top: 4px;
margin-bottom: 20px;
}
.container {
max-width: 400px;
margin: 0 auto;
}
.input-field {
margin-bottom: 12px;
}
label {
display: block;
font-weight: bold;
margin-bottom: 4px;
}
input, select {
width: 100%;
box-sizing: border-box;
padding: 8px;
font-size: 16px;
}
button {
width: 100%;
padding: 10px;
font-size: 16px;
background: #4CAF50;
color: #fff;
border: none;
cursor: pointer;
margin-top: 10px;
}
button:hover {
background: #45a049;
}
/* ========== 預算表 ========== */
.budget-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.budget-table th, .budget-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.warning-normal {
background-color: #ffffff;
}
.warning-near {
background-color: #fff8db; /* 淡黃色 */
}
.warning-over {
background-color: #ffdcdc; /* 淡紅色 */
}
.message {
margin-top: 8px;
text-align: center;
}
/* ========== 浮層 (Modal) 樣式 ========== */
.modal-overlay {
position: fixed;
z-index: 999; /* 確保浮層在最上層 */
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景 */
display: none; /* 初始隱藏 */
justify-content: center;
align-items: center;
}
.modal-content {
background: #fff;
padding: 20px;
border-radius: 8px;
width: 90%;
max-width: 600px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.modal-header h2 {
margin: 0;
}
.close-btn {
background: #ccc;
color: #000;
border: none;
font-size: 16px;
padding: 5px 10px;
cursor: pointer;
}
.close-btn:hover {
background: #aaa;
}
.expense-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
}
.expense-table th, .expense-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.sort-btn {
background: #2196F3;
margin-bottom: 10px;
}
.sort-btn:hover {
background: #0b7dda;
}
</style>
</head>
<body>
<h1>簡易記帳工具</h1>
<div class="sub-title">Google Apps Script + 試算表</div>
<div class="container">
<!-- ========== 既有:新增交易表單 ========== -->
<div class="input-field">
<label for="dateTime">日期與時間</label>
<input type="datetime-local" id="dateTime" />
</div>
<div class="input-field">
<label for="type">類型</label>
<select id="type">
<option value="支出" selected>支出</option>
<option value="收入">收入</option>
</select>
</div>
<div class="input-field">
<label for="category">類別</label>
<select id="category"></select>
</div>
<div class="input-field">
<label for="amount">金額</label>
<input type="number" id="amount" step="0.01" />
</div>
<div class="input-field">
<label for="note">備註</label>
<input type="text" id="note" />
</div>
<button onclick="submitForm()">新增交易</button>
<div class="message" id="msgBox"></div>
<!-- ========== 新增:查看「該月支出」按鈕 ========== -->
<button style="background:#f44336; margin-top:20px;" onclick="viewMonthlyExpenses()">
查看該月支出
</button>
<!-- ========== 預算表 ========== -->
<table class="budget-table" id="budgetTable">
<thead>
<tr>
<th>類別</th>
<th>預算</th>
<th>本月已用</th>
</tr>
</thead>
<tbody id="budgetTableBody"></tbody>
</table>
</div>
<!-- ========== 浮層 (Modal) 內容 ========== -->
<div class="modal-overlay" id="monthlyExpensesModal">
<div class="modal-content">
<div class="modal-header">
<h2>本月支出列表</h2>
<button class="close-btn" onclick="closeMonthlyExpensesModal()">X</button>
</div>
<!-- 排序按鈕 -->
<button class="sort-btn" onclick="sortExpensesByCategory()">
按「類別」排序
</button>
<!-- 顯示表格 -->
<table class="expense-table" id="expenseTable">
<thead>
<tr>
<th>日期時間</th>
<th>類別</th>
<th>金額</th>
<th>備註</th>
</tr>
</thead>
<tbody id="expenseTableBody"></tbody>
</table>
<!-- 總計 -->
<div id="totalAmount" style="text-align:right; font-weight:bold;"></div>
</div>
</div>
<script>
/********************************************
* 既有:載入「分類清單」與「預算表」
********************************************/
document.addEventListener('DOMContentLoaded', function() {
setDefaultDateTime();
// 載入分類
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
// 載入預算
refreshDashboard();
});
// 帶入「現在時間」到 datetime-local
function setDefaultDateTime() {
var now = new Date();
var localISO = new Date(now.getTime() - now.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16); // 'YYYY-MM-DDTHH:MM'
document.getElementById('dateTime').value = localISO;
}
// 填充分類下拉
function populateCategorySelect(categories) {
var categorySelect = document.getElementById('category');
categorySelect.innerHTML = '';
categories.forEach(function(cat) {
var option = document.createElement('option');
option.value = cat.name;
option.text = cat.name + ' (' + cat.type + ')';
categorySelect.appendChild(option);
});
}
// 新增交易
function submitForm() {
var dateTime = document.getElementById('dateTime').value;
var type = document.getElementById('type').value;
var category = document.getElementById('category').value;
var amount = document.getElementById('amount').value;
var note = document.getElementById('note').value;
if (!dateTime || !type || !category || !amount) {
showMessage('請填寫完整資訊', true);
return;
}
var transaction = {
dateTime: dateTime,
type: type,
category: category,
amount: amount,
note: note
};
google.script.run
.withSuccessHandler(function(msg) {
showMessage(msg, false);
setDefaultDateTime();
document.getElementById('amount').value = '';
document.getElementById('note').value = '';
// 刷新預算
refreshDashboard();
})
.withFailureHandler(function(err) {
showMessage('寫入失敗: ' + err.message, true);
})
.saveTransaction(transaction);
}
// 顯示/隱藏訊息
function showMessage(msg, isError) {
var msgBox = document.getElementById('msgBox');
msgBox.style.color = isError ? 'red' : 'green';
msgBox.textContent = msg;
}
// 刷新預算表
function refreshDashboard() {
google.script.run
.withSuccessHandler(function(dashboardData){
populateBudgetTable(dashboardData);
})
.getDashboardData();
}
function populateBudgetTable(data) {
var tbody = document.getElementById('budgetTableBody');
tbody.innerHTML = '';
data.forEach(function(item){
var tr = document.createElement('tr');
if (item.warningLevel === 'over') {
tr.className = 'warning-over';
} else if (item.warningLevel === 'near') {
tr.className = 'warning-near';
} else {
tr.className = 'warning-normal';
}
var tdCat = document.createElement('td');
tdCat.textContent = item.category;
var tdBudget = document.createElement('td');
tdBudget.textContent = item.budget || 0;
var tdSpent = document.createElement('td');
tdSpent.textContent = item.spent;
tr.appendChild(tdCat);
tr.appendChild(tdBudget);
tr.appendChild(tdSpent);
tbody.appendChild(tr);
});
}
/********************************************
* 新增:查看「該月支出」按鈕的功能
********************************************/
var monthlyExpenses = []; // 用於儲存本月支出資料
// 1) 取得本月支出 -> 顯示浮層
function viewMonthlyExpenses() {
google.script.run
.withSuccessHandler(function(expenses){
monthlyExpenses = expenses;
renderMonthlyExpenses();
document.getElementById('monthlyExpensesModal').style.display = 'flex';
})
.withFailureHandler(function(err){
alert('取得本月支出失敗:' + err.message);
})
.getThisMonthExpenses();
}
// 2) 渲染本月支出的表格
function renderMonthlyExpenses() {
var tbody = document.getElementById('expenseTableBody');
tbody.innerHTML = '';
// 計算總計
var total = 0;
monthlyExpenses.forEach(function(item){
total += parseFloat(item.amount) || 0;
var tr = document.createElement('tr');
var tdDateTime = document.createElement('td');
tdDateTime.textContent = item.dateTime; // 直接顯示字串
var tdCategory = document.createElement('td');
tdCategory.textContent = item.category;
var tdAmount = document.createElement('td');
tdAmount.textContent = item.amount;
var tdNote = document.createElement('td');
tdNote.textContent = item.note;
tr.appendChild(tdDateTime);
tr.appendChild(tdCategory);
tr.appendChild(tdAmount);
tr.appendChild(tdNote);
tbody.appendChild(tr);
});
// 顯示總計
document.getElementById('totalAmount').textContent = '總計:' + total;
}
// 3) 按類別排序 (升冪) 後重新渲染
function sortExpensesByCategory() {
monthlyExpenses.sort(function(a, b){
return a.category.localeCompare(b.category, 'zh-TW');
});
renderMonthlyExpenses();
}
// 4) 關閉浮層
function closeMonthlyExpensesModal() {
document.getElementById('monthlyExpensesModal').style.display = 'none';
}
</script>
</body>
</html>
「查看該月支出」按鈕
viewMonthlyExpenses() → 從後端 getThisMonthExpenses() 取得本月支出交易清單 → 顯示在浮層中。getThisMonthExpenses() (後端)
Transactions 試算表中,所有「本年度、本月、且類型為支出」的紀錄。{ dateTime, type, category, amount, note } 陣列給前端。浮層 (Modal) 顯示
Index.html 中使用 .modal-overlay + .modal-content 做出簡單浮層效果。closeMonthlyExpensesModal())。本月支出列表
<table> 顯示所有支出紀錄,包含:日期時間、類別、金額、備註。總計。按類別排序
sortExpensesByCategory() → 前端對 monthlyExpenses 依 category 升冪排序 → 呼叫 renderMonthlyExpenses() 重新渲染表格。你也可以進一步擴充「依日期排序」、「依金額排序」等更多功能,只要在前端對陣列排序後重繪表格即可。
以上示例即可達到:
你可根據實際需求進一步調整版面或邏輯。祝開發順利!
給我完整的.gs
思考 花的時間為 2 秒
以下是完整的 Code.gs 程式碼,整合了先前所有功能,包括:
Transactions, Budgets, Categories),並填入初始欄位標題與預設常用的記帳類別。Code.gs 程式碼/************************************************
* 全域設定
************************************************/
// 取得當前試算表
var ss = SpreadsheetApp.getActiveSpreadsheet();
// 預計要建立的工作表名稱
var SHEET_TRANSACTIONS = 'Transactions'; // 交易紀錄
var SHEET_BUDGETS = 'Budgets'; // 預算設定
var SHEET_CATEGORIES = 'Categories'; // 分類清單
/************************************************
* 網頁入口
************************************************/
function doGet() {
// 先確保必需的工作表存在,沒有就自動建立
setupSpreadsheet();
// 回傳網頁
return HtmlService
.createTemplateFromFile('Index')
.evaluate()
.addMetaTag('viewport', 'width=device-width, initial-scale=1.0')
.setTitle('簡易記帳工具');
}
/************************************************
* 一次載入前端 HTML
************************************************/
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
/************************************************
* 0. 自動建立所需的工作表 (若尚未存在)
************************************************/
function setupSpreadsheet() {
// 1) 建立 Transactions (交易紀錄)
var sheetTx = ss.getSheetByName(SHEET_TRANSACTIONS);
if (!sheetTx) {
sheetTx = ss.insertSheet(SHEET_TRANSACTIONS);
// 設定欄位標題
sheetTx.appendRow(['日期時間', '類型(收入/支出)', '類別', '金額', '備註']);
}
// 2) 建立 Budgets (預算設定)
var sheetBd = ss.getSheetByName(SHEET_BUDGETS);
if (!sheetBd) {
sheetBd = ss.insertSheet(SHEET_BUDGETS);
// 設定欄位標題
sheetBd.appendRow(['類別', '預算金額']);
// 預設預算設定 (可根據需求修改或新增)
var defaultBudgets = [
['飲食', 5000],
['交通', 2000],
['娛樂', 3000],
['房租', 10000],
['薪資', 0] // 收入類別預算可設為0或不設定
];
sheetBd.getRange(2, 1, defaultBudgets.length, 2).setValues(defaultBudgets);
}
// 3) 建立 Categories (分類清單)
var sheetCat = ss.getSheetByName(SHEET_CATEGORIES);
if (!sheetCat) {
sheetCat = ss.insertSheet(SHEET_CATEGORIES);
// 設定欄位標題
sheetCat.appendRow(['類別名稱', '類型(收入/支出)']);
// 常用的記帳類別 (預設)
var defaultCategories = [
['飲食', '支出'],
['交通', '支出'],
['娛樂', '支出'],
['房租', '支出'],
['薪資', '收入'],
['其他收入', '收入'],
['其他支出', '支出']
];
sheetCat.getRange(2, 1, defaultCategories.length, 2).setValues(defaultCategories);
}
}
/************************************************
* 1. 取得分類清單
************************************************/
function getCategories() {
var sheet = ss.getSheetByName(SHEET_CATEGORIES);
var data = sheet.getDataRange().getValues(); // 全部資料
// 假設第一列是標題,所以從第二列開始
var categories = [];
for (var i = 1; i < data.length; i++) {
var row = data[i];
var name = row[0];
var type = row[1];
if (name) {
categories.push({
name: name,
type: type
});
}
}
return categories;
}
/************************************************
* 2. 取得預算設定
************************************************/
function getBudgets() {
var sheet = ss.getSheetByName(SHEET_BUDGETS);
var data = sheet.getDataRange().getValues();
// A欄: 類別, B欄: 預算金額
var budgets = {};
for (var i = 1; i < data.length; i++) {
var row = data[i];
var category = row[0];
var budget = row[1];
if (category) {
budgets[category] = budget;
}
}
return budgets;
}
/************************************************
* 3. 寫入交易紀錄
************************************************/
function saveTransaction(transaction) {
// transaction = { dateTime, type, category, amount, note }
var sheet = ss.getSheetByName(SHEET_TRANSACTIONS);
// 寫入 (日期時間, 類型, 類別, 金額, 備註)
sheet.appendRow([
transaction.dateTime,
transaction.type,
transaction.category,
parseFloat(transaction.amount),
transaction.note
]);
return '交易已儲存!';
}
/************************************************
* 4. 取得本月支出總額 (依類別)
************************************************/
function getMonthlySpending() {
var sheet = ss.getSheetByName(SHEET_TRANSACTIONS);
var data = sheet.getDataRange().getValues();
// 取得當前年月,用於判斷「本月」
var now = new Date();
var currentMonth = now.getMonth();
var currentYear = now.getFullYear();
// 結構:
// A: 日期時間
// B: 類型
// C: 類別
// D: 金額
// E: 備註
var spendingByCategory = {};
// 從第二列開始 (第一列是標題)
for (var i = 1; i < data.length; i++) {
var row = data[i];
var dateTime = row[0];
var type = row[1];
var category = row[2];
var amount = parseFloat(row[3]) || 0;
var txDate = new Date(dateTime);
// 僅計算「支出」+「本月」
if (type === '支出' &&
txDate.getMonth() === currentMonth &&
txDate.getFullYear() === currentYear) {
if (!spendingByCategory[category]) {
spendingByCategory[category] = 0;
}
spendingByCategory[category] += amount;
}
}
return spendingByCategory;
}
/************************************************
* 5. 彙整預算 & 支出,回傳給前端
************************************************/
function getDashboardData() {
var budgets = getBudgets();
var categories = getCategories();
var spendingByCategory = getMonthlySpending();
var dashboardData = [];
categories.forEach(function(cat) {
if (cat.type === '支出') {
var categoryName = cat.name;
var budget = budgets[categoryName] ? budgets[categoryName] : 0;
var spent = spendingByCategory[categoryName] ? spendingByCategory[categoryName] : 0;
// 超支 / 接近超支 (可自訂閾值)
var warningLevel = 'normal';
if (budget > 0) {
if (spent >= budget) {
warningLevel = 'over'; // 超支
} else if (spent >= 0.8 * budget) {
warningLevel = 'near'; // 接近超支
}
}
dashboardData.push({
category: categoryName,
budget: budget,
spent: spent,
warningLevel: warningLevel
});
}
});
return dashboardData;
}
/************************************************
* 6. 取得本月所有支出明細
************************************************/
/**
* 回傳「本月支出」的所有交易明細
* @return {Array} expenses 本月支出的交易陣列,每筆包含:
* { dateTime, type, category, amount, note }
*/
function getThisMonthExpenses() {
var sheet = ss.getSheetByName(SHEET_TRANSACTIONS);
var data = sheet.getDataRange().getValues();
// 取得當前年月,用於判斷「本月」
var now = new Date();
var currentYear = now.getFullYear();
var currentMonth = now.getMonth(); // 0 ~ 11
// A: 日期時間, B: 類型, C: 類別, D: 金額, E: 備註
var expenses = [];
for(var i=1; i < data.length; i++){
var row = data[i];
var dateTime = row[0];
var type = row[1];
var category = row[2];
var amount = parseFloat(row[3]) || 0;
var note = row[4] || '';
var txDate = new Date(dateTime);
// 篩選「本年、本月」且「類型為支出」的交易
if(type === '支出' &&
txDate.getFullYear() === currentYear &&
txDate.getMonth() === currentMonth) {
expenses.push({
dateTime: row[0], // 保留原字串格式
type: type,
category: category,
amount: amount,
note: note
});
}
}
return expenses;
}
全域設定:
ss)。Transactions, Budgets, Categories。doGet() 函式:
setupSpreadsheet() 確保所需的工作表存在,若不存在則自動建立。Index.html,並設定 viewport 以適應手機介面。setupSpreadsheet() 函式:
Transactions:用於記錄交易紀錄,包含 日期時間, 類型(收入/支出), 類別, 金額, 備註。Budgets:用於設定預算,包含 類別, 預算金額,並預設一些常用預算。Categories:用於管理分類,包含 類別名稱, 類型(收入/支出),並預設一些常用分類。基本功能函式:
getCategories():從 Categories 工作表讀取所有分類資料,回傳一個物件陣列,每個物件包含 name 與 type。getBudgets():從 Budgets 工作表讀取預算設定,回傳一個鍵值對物件,鍵為類別名稱,值為預算金額。saveTransaction(transaction):接收一個交易物件,將其寫入 Transactions 工作表,並回傳成功訊息。getMonthlySpending():計算本月每個支出類別的總支出,回傳一個鍵值對物件,鍵為類別名稱,值為總支出金額。getDashboardData():整合分類、預算與支出資料,回傳一個包含 category, budget, spent, warningLevel 的陣列,用於前端顯示預算表。新增功能函式:
getThisMonthExpenses():取得本月所有支出的交易明細,回傳一個包含 dateTime, type, category, amount, note 的陣列,用於前端顯示支出列表。建立或開啟你的 Google 試算表。
開啟 Apps Script 編輯器:
新增或更新 Code.gs:
Code.gs,請將上述完整程式碼貼上,並覆蓋原有內容。.gs 檔案,命名為 Code.gs,並貼上上述程式碼。建立 Index.html:
Index.html 並包含前端的 HTML、CSS、JavaScript。先前的回覆中已提供了更新後的 Index.html,請確保其內容與需求一致。部署為網頁應用程式:
取得應用程式網址:
首次使用:
setupSpreadsheet() 會自動檢查並建立所需的工作表,並填入預設的分類與預算。新增交易:
Transactions 工作表中。查看本月支出:
編輯與刪除交易:
多用戶支援:
更豐富的報表分析:
通知功能:
自訂預算提醒閾值:
多語言支援:
以上程式碼與說明應能滿足你目前的需求,並提供一個穩固的基礎供你進一步開發與擴充。若有任何疑問或需要進一步的協助,歡迎隨時提問。祝開發順利!
userCodeAppPanel:137 Uncaught TypeError: Cannot read properties of null (reading 'forEach') at renderMonthlyExpenses (userCodeAppPanel:137:25) at userCodeAppPanel:121:13 at Hh (3036894486-mae_html_user_bin_i18n_mae_html_user__zh_tw.js:146:320) at 3036894486-mae_html_user_bin_i18n_mae_html_user__zh_tw.js:35:276 at ff.N (3036894486-mae_html_user_bin_i18n_mae_html_user__zh_tw.js:100:374) at Ad (3036894486-mae_html_user_bin_i18n_mae_html_user__zh_tw.js:63:477) at a (3036894486-mae_html_user_bin_i18n_mae_html_user__zh_tw.js:61:52)Understand this errorAI 157046241-warden_bin_i18n_warden__zh_tw.js:123 Net state changed from BUSY to IDLE
思考 花的時間為 12 秒
抱歉你遇到了這個錯誤。根據錯誤訊息:
Uncaught TypeError: Cannot read properties of null (reading 'forEach')
at renderMonthlyExpenses (userCodeAppPanel:137:25)
...
這表示在前端的 renderMonthlyExpenses 函式中,嘗試對 null 執行 forEach,也就是說,monthlyExpenses 變數為 null 或未正確初始化。這通常是因為後端 getThisMonthExpenses 函式未正確回傳預期的陣列,或者前端未正確接收資料所導致的。
以下是完整且修正後的 Code.gs 以及更新後的 Index.html,這些修正將確保:
getThisMonthExpenses 函式始終回傳一個陣列,即使當月沒有支出紀錄。null 或未定義的情況下執行 forEach。Code.gs/************************************************
* 全域設定
************************************************/
const SHEET_TRANSACTIONS = 'Transactions'; // 交易紀錄
const SHEET_BUDGETS = 'Budgets'; // 預算設定
const SHEET_CATEGORIES = 'Categories'; // 分類清單
/************************************************
* 網頁入口
************************************************/
function doGet() {
// 先確保必需的工作表存在,沒有就自動建立
setupSpreadsheet();
// 回傳網頁
return HtmlService
.createTemplateFromFile('Index')
.evaluate()
.addMetaTag('viewport', 'width=device-width, initial-scale=1.0')
.setTitle('簡易記帳工具');
}
/************************************************
* 一次載入前端 HTML
************************************************/
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
/************************************************
* 0. 自動建立所需的工作表 (若尚未存在)
************************************************/
function setupSpreadsheet() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
// 1) 建立 Transactions (交易紀錄)
let sheetTx = ss.getSheetByName(SHEET_TRANSACTIONS);
if (!sheetTx) {
sheetTx = ss.insertSheet(SHEET_TRANSACTIONS);
// 設定欄位標題
sheetTx.appendRow(['日期時間', '類型(收入/支出)', '類別', '金額', '備註']);
}
// 2) 建立 Budgets (預算設定)
let sheetBd = ss.getSheetByName(SHEET_BUDGETS);
if (!sheetBd) {
sheetBd = ss.insertSheet(SHEET_BUDGETS);
// 設定欄位標題
sheetBd.appendRow(['類別', '預算金額']);
// 預設預算設定 (可根據需求修改或新增)
const defaultBudgets = [
['飲食', 5000],
['交通', 2000],
['娛樂', 3000],
['房租', 10000],
['薪資', 0] // 收入類別預算可設為0或不設定
];
sheetBd.getRange(2, 1, defaultBudgets.length, 2).setValues(defaultBudgets);
}
// 3) 建立 Categories (分類清單)
let sheetCat = ss.getSheetByName(SHEET_CATEGORIES);
if (!sheetCat) {
sheetCat = ss.insertSheet(SHEET_CATEGORIES);
// 設定欄位標題
sheetCat.appendRow(['類別名稱', '類型(收入/支出)']);
// 常用的記帳類別 (預設)
const defaultCategories = [
['飲食', '支出'],
['交通', '支出'],
['娛樂', '支出'],
['房租', '支出'],
['薪資', '收入'],
['其他收入', '收入'],
['其他支出', '支出']
];
sheetCat.getRange(2, 1, defaultCategories.length, 2).setValues(defaultCategories);
}
}
/************************************************
* 1. 取得分類清單
************************************************/
function getCategories() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_CATEGORIES);
const data = sheet.getDataRange().getValues(); // 全部資料
// 假設第一列是標題,所以從第二列開始
const categories = [];
for (let i = 1; i < data.length; i++) {
const row = data[i];
const name = row[0];
const type = row[1];
if (name && type) {
categories.push({
name: name,
type: type
});
}
}
return categories;
}
/************************************************
* 2. 取得預算設定
************************************************/
function getBudgets() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_BUDGETS);
const data = sheet.getDataRange().getValues();
// A欄: 類別, B欄: 預算金額
const budgets = {};
for (let i = 1; i < data.length; i++) {
const row = data[i];
const category = row[0];
const budget = parseFloat(row[1]) || 0;
if (category) {
budgets[category] = budget;
}
}
return budgets;
}
/************************************************
* 3. 寫入交易紀錄
************************************************/
function saveTransaction(transaction) {
// transaction = { dateTime, type, category, amount, note }
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_TRANSACTIONS);
// 檢查必要欄位
if (!transaction.dateTime || !transaction.type || !transaction.category || !transaction.amount) {
throw new Error('缺少必要的交易資訊');
}
// 檢查金額是否為數字
const amount = parseFloat(transaction.amount);
if (isNaN(amount)) {
throw new Error('金額必須為數字');
}
// 寫入 (日期時間, 類型, 類別, 金額, 備註)
sheet.appendRow([
transaction.dateTime,
transaction.type,
transaction.category,
amount,
transaction.note || ''
]);
return '交易已儲存!';
}
/************************************************
* 4. 取得本月支出總額 (依類別)
************************************************/
function getMonthlySpending() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_TRANSACTIONS);
const data = sheet.getDataRange().getValues();
// 取得當前年月,用於判斷「本月」
const now = new Date();
const currentMonth = now.getMonth();
const currentYear = now.getFullYear();
// 結構:
// A: 日期時間
// B: 類型
// C: 類別
// D: 金額
// E: 備註
const spendingByCategory = {};
// 從第二列開始 (第一列是標題)
for (let i = 1; i < data.length; i++) {
const row = data[i];
const dateTime = new Date(row[0]);
const type = row[1];
const category = row[2];
const amount = parseFloat(row[3]) || 0;
// 僅計算「支出」+「本月」
if (type === '支出' &&
dateTime.getMonth() === currentMonth &&
dateTime.getFullYear() === currentYear) {
if (!spendingByCategory[category]) {
spendingByCategory[category] = 0;
}
spendingByCategory[category] += amount;
}
}
return spendingByCategory;
}
/************************************************
* 5. 彙整預算 & 支出,回傳給前端
************************************************/
function getDashboardData() {
const budgets = getBudgets();
const categories = getCategories();
const spendingByCategory = getMonthlySpending();
const dashboardData = [];
categories.forEach(function(cat) {
if (cat.type === '支出') {
const categoryName = cat.name;
const budget = budgets[categoryName] || 0;
const spent = spendingByCategory[categoryName] || 0;
// 超支 / 接近超支 (可自訂閾值)
let warningLevel = 'normal';
if (budget > 0) {
if (spent >= budget) {
warningLevel = 'over'; // 超支
} else if (spent >= 0.8 * budget) {
warningLevel = 'near'; // 接近超支
}
}
dashboardData.push({
category: categoryName,
budget: budget,
spent: spent,
warningLevel: warningLevel
});
}
});
return dashboardData;
}
/************************************************
* 6. 取得本月所有支出明細
************************************************/
/**
* 回傳「本月支出」的所有交易明細
* @return {Array} expenses 本月支出的交易陣列,每筆包含:
* { dateTime, type, category, amount, note }
*/
function getThisMonthExpenses() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_TRANSACTIONS);
const data = sheet.getDataRange().getValues();
// 取得當前年月,用於判斷「本月」
const now = new Date();
const currentYear = now.getFullYear();
const currentMonth = now.getMonth(); // 0 ~ 11
// A: 日期時間, B: 類型, C: 類別, D: 金額, E: 備註
const expenses = [];
for(let i=1; i < data.length; i++){
const row = data[i];
const dateTimeStr = row[0];
const dateTime = new Date(dateTimeStr);
const type = row[1];
const category = row[2];
const amount = parseFloat(row[3]) || 0;
const note = row[4] || '';
// 篩選「本年、本月」且「類型為支出」的交易
if(type === '支出' &&
dateTime.getFullYear() === currentYear &&
dateTime.getMonth() === currentMonth) {
expenses.push({
dateTime: Utilities.formatDate(dateTime, ss.getSpreadsheetTimeZone(), "yyyy/MM/dd HH:mm"),
type: type,
category: category,
amount: amount,
note: note
});
}
}
return expenses;
}
Index.html為了確保前端能正確處理後端回傳的資料,我們將增加一些防護措施,並確保在渲染支出列表前確認 monthlyExpenses 是一個有效的陣列。
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
/* 手機友善基礎樣式 */
body {
font-family: sans-serif;
margin: 10px;
}
h1 {
text-align: center;
margin-bottom: 0;
}
.sub-title {
text-align: center;
color: #666;
margin-top: 4px;
margin-bottom: 20px;
}
.container {
max-width: 400px;
margin: 0 auto;
}
.input-field {
margin-bottom: 12px;
}
label {
display: block;
font-weight: bold;
margin-bottom: 4px;
}
input, select {
width: 100%;
box-sizing: border-box;
padding: 8px;
font-size: 16px;
}
button {
width: 100%;
padding: 10px;
font-size: 16px;
background: #4CAF50;
color: #fff;
border: none;
cursor: pointer;
margin-top: 10px;
}
button:hover {
background: #45a049;
}
/* 預算表 */
.budget-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.budget-table th, .budget-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.warning-normal {
background-color: #ffffff;
}
.warning-near {
background-color: #fff8db; /* 淡黃色 */
}
.warning-over {
background-color: #ffdcdc; /* 淡紅色 */
}
.message {
margin-top: 8px;
text-align: center;
}
/* 浮層 (Modal) 樣式 */
.modal-overlay {
position: fixed;
z-index: 999; /* 確保浮層在最上層 */
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景 */
display: none; /* 初始隱藏 */
justify-content: center;
align-items: center;
}
.modal-content {
background: #fff;
padding: 20px;
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.modal-header h2 {
margin: 0;
}
.close-btn {
background: #ccc;
color: #000;
border: none;
font-size: 16px;
padding: 5px 10px;
cursor: pointer;
border-radius: 4px;
}
.close-btn:hover {
background: #aaa;
}
.expense-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
}
.expense-table th, .expense-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.sort-btn {
background: #2196F3;
color: #fff;
padding: 8px 12px;
border: none;
cursor: pointer;
border-radius: 4px;
margin-bottom: 10px;
}
.sort-btn:hover {
background: #0b7dda;
}
/* 滾動條樣式(選用) */
.modal-content::-webkit-scrollbar {
width: 8px;
}
.modal-content::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.2);
border-radius: 4px;
}
</style>
</head>
<body>
<h1>簡易記帳工具</h1>
<div class="sub-title">Google Apps Script + 試算表</div>
<div class="container">
<!-- 新增交易表單 -->
<div class="input-field">
<label for="dateTime">日期與時間</label>
<input type="datetime-local" id="dateTime" />
</div>
<div class="input-field">
<label for="type">類型</label>
<select id="type" onchange="filterCategoryOptions()">
<option value="支出" selected>支出</option>
<option value="收入">收入</option>
</select>
</div>
<div class="input-field">
<label for="category">類別</label>
<select id="category"></select>
</div>
<div class="input-field">
<label for="amount">金額</label>
<input type="number" id="amount" step="0.01" />
</div>
<div class="input-field">
<label for="note">備註</label>
<input type="text" id="note" />
</div>
<button onclick="submitForm()">新增交易</button>
<div class="message" id="msgBox"></div>
<!-- 查看該月支出按鈕 -->
<button style="background:#f44336; margin-top:20px;" onclick="viewMonthlyExpenses()">
查看該月支出
</button>
<!-- 預算表 -->
<table class="budget-table" id="budgetTable">
<thead>
<tr>
<th>類別</th>
<th>預算</th>
<th>本月已用</th>
</tr>
</thead>
<tbody id="budgetTableBody"></tbody>
</table>
</div>
<!-- 浮層 (Modal) 內容 -->
<div class="modal-overlay" id="monthlyExpensesModal">
<div class="modal-content">
<div class="modal-header">
<h2>本月支出列表</h2>
<button class="close-btn" onclick="closeMonthlyExpensesModal()">X</button>
</div>
<!-- 排序按鈕 -->
<button class="sort-btn" onclick="sortExpensesByCategory()">
按「類別」排序
</button>
<!-- 支出表格 -->
<table class="expense-table" id="expenseTable">
<thead>
<tr>
<th>日期時間</th>
<th>類別</th>
<th>金額</th>
<th>備註</th>
</tr>
</thead>
<tbody id="expenseTableBody"></tbody>
</table>
<!-- 總計 -->
<div id="totalAmount" style="text-align:right; font-weight:bold;"></div>
</div>
</div>
<script>
/********************************************
* 前端初始化:載入「分類清單」與「預算表」
********************************************/
document.addEventListener('DOMContentLoaded', function() {
setDefaultDateTime();
// 載入分類
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
// 載入預算
refreshDashboard();
});
// 設定「現在時間」到 datetime-local
function setDefaultDateTime() {
const now = new Date();
const localISO = new Date(now.getTime() - now.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16); // 'YYYY-MM-DDTHH:MM'
document.getElementById('dateTime').value = localISO;
}
// 填充分類下拉
function populateCategorySelect(categories) {
const categorySelect = document.getElementById('category');
categorySelect.innerHTML = '';
// 根據選擇的類型篩選類別
const selectedType = document.getElementById('type').value;
categories.forEach(function(cat) {
if (cat.type === selectedType) {
const option = document.createElement('option');
option.value = cat.name;
option.text = cat.name;
categorySelect.appendChild(option);
}
});
}
// 根據「類型」篩選「類別」選項
function filterCategoryOptions() {
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
}
// 新增交易
function submitForm() {
const dateTime = document.getElementById('dateTime').value;
const type = document.getElementById('type').value;
const category = document.getElementById('category').value;
const amount = document.getElementById('amount').value;
const note = document.getElementById('note').value;
// 簡易驗證
if (!dateTime || !type || !category || !amount) {
showMessage('請填寫完整資訊', true);
return;
}
const transaction = {
dateTime: dateTime,
type: type,
category: category,
amount: amount,
note: note
};
google.script.run
.withSuccessHandler(function(msg) {
showMessage(msg, false);
setDefaultDateTime();
document.getElementById('amount').value = '';
document.getElementById('note').value = '';
// 刷新預算
refreshDashboard();
})
.withFailureHandler(function(err) {
showMessage('寫入失敗: ' + err.message, true);
})
.saveTransaction(transaction);
}
// 顯示訊息
function showMessage(msg, isError) {
const msgBox = document.getElementById('msgBox');
msgBox.style.color = isError ? 'red' : 'green';
msgBox.textContent = msg;
}
// 刷新預算表
function refreshDashboard() {
google.script.run
.withSuccessHandler(function(dashboardData){
populateBudgetTable(dashboardData);
})
.getDashboardData();
}
function populateBudgetTable(data) {
const tbody = document.getElementById('budgetTableBody');
tbody.innerHTML = '';
data.forEach(function(item){
const tr = document.createElement('tr');
if (item.warningLevel === 'over') {
tr.className = 'warning-over';
} else if (item.warningLevel === 'near') {
tr.className = 'warning-near';
} else {
tr.className = 'warning-normal';
}
const tdCat = document.createElement('td');
tdCat.textContent = item.category;
const tdBudget = document.createElement('td');
tdBudget.textContent = item.budget || 0;
const tdSpent = document.createElement('td');
tdSpent.textContent = item.spent;
tr.appendChild(tdCat);
tr.appendChild(tdBudget);
tr.appendChild(tdSpent);
tbody.appendChild(tr);
});
}
/********************************************
* 新增:查看「該月支出」按鈕的功能
********************************************/
let monthlyExpenses = []; // 用於儲存本月支出資料
// 1) 取得本月支出 -> 顯示浮層
function viewMonthlyExpenses() {
google.script.run
.withSuccessHandler(function(expenses){
if (Array.isArray(expenses)) {
monthlyExpenses = expenses;
renderMonthlyExpenses();
document.getElementById('monthlyExpensesModal').style.display = 'flex';
} else {
alert('取得的支出資料格式錯誤。');
}
})
.withFailureHandler(function(err){
alert('取得本月支出失敗:' + err.message);
})
.getThisMonthExpenses();
}
// 2) 渲染本月支出的表格
function renderMonthlyExpenses() {
const tbody = document.getElementById('expenseTableBody');
tbody.innerHTML = '';
if (!Array.isArray(monthlyExpenses) || monthlyExpenses.length === 0) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 4;
td.textContent = '本月尚無支出紀錄。';
tr.appendChild(td);
tbody.appendChild(tr);
document.getElementById('totalAmount').textContent = '總計:0';
return;
}
// 計算總計
let total = 0;
monthlyExpenses.forEach(function(item){
total += parseFloat(item.amount) || 0;
const tr = document.createElement('tr');
const tdDateTime = document.createElement('td');
tdDateTime.textContent = item.dateTime; // 格式化後的日期時間
const tdCategory = document.createElement('td');
tdCategory.textContent = item.category;
const tdAmount = document.createElement('td');
tdAmount.textContent = item.amount;
const tdNote = document.createElement('td');
tdNote.textContent = item.note;
tr.appendChild(tdDateTime);
tr.appendChild(tdCategory);
tr.appendChild(tdAmount);
tr.appendChild(tdNote);
tbody.appendChild(tr);
});
// 顯示總計
document.getElementById('totalAmount').textContent = '總計:' + total;
}
// 3) 按類別排序 (升冪) 後重新渲染
function sortExpensesByCategory() {
if (!Array.isArray(monthlyExpenses)) return;
monthlyExpenses.sort(function(a, b){
return a.category.localeCompare(b.category, 'zh-TW');
});
renderMonthlyExpenses();
}
// 4) 關閉浮層
function closeMonthlyExpensesModal() {
document.getElementById('monthlyExpensesModal').style.display = 'none';
}
</script>
</body>
</html>
後端 Code.gs 修正:
getThisMonthExpenses 始終回傳陣列:即使當月無支出紀錄,也會回傳空陣列,避免前端接收到 null。Utilities.formatDate 將日期格式化為更易讀的格式(yyyy/MM/dd HH:mm)。saveTransaction 函式中,檢查必要欄位是否存在,並確保金額為有效數字。const 和 let:替換了 var 以提升程式碼的可讀性與安全性。前端 Index.html 修正:
viewMonthlyExpenses 函式中,加入檢查確保 expenses 是一個陣列。renderMonthlyExpenses 中處理空陣列:如果 monthlyExpenses 為空,顯示「本月尚無支出紀錄」。錯誤處理:
monthlyExpenses 執行 forEach 前,確認其為有效陣列。其他建議:
Transactions 工作表中的「日期時間」欄位為有效的日期格式,否則 new Date(dateTimeStr) 可能會產生 Invalid Date。更新 Code.gs:
Code.gs 替換為上述提供的完整程式碼。更新 Index.html:
Index.html 替換為上述提供的完整 HTML 內容。重新部署應用程式:
測試功能:
Transactions 工作表。調整與優化:
Transactions 工作表沒有資料:
日期時間格式錯誤:
datetime-local 欄位正確輸入,並在 Transactions 工作表中顯示為有效的日期格式。授權問題:
資料未正確更新:
浮層無法顯示或關閉:
.modal-overlay 是否被正確設置為 display: flex 以顯示,並確保 closeMonthlyExpensesModal 函式正確運作。透過以上修正與說明,應該可以解決你遇到的錯誤並正常使用「查看該月支出」功能。如果問題仍然存在,建議進一步檢查 Transactions 工作表中的資料格式,或在 Apps Script 中加入日誌 (Logger.log) 來協助除錯。
若有任何其他問題,歡迎隨時詢問!
類別把房租改為網購;web介面中類型及類別改為在同一列,分二欄呈現;金額及備註改為在同列,分二欄呈現
思考 花的時間為 5 秒
了解,你希望進行以下幾項修改:
以下提供完整的 Code.gs 和更新後的 Index.html,以滿足你的需求。
Code.gs在 Code.gs 中,我們將「房租」類別改為「網購」,並更新預算設定。以下是更新後的完整程式碼:
/************************************************
* 全域設定
************************************************/
const SHEET_TRANSACTIONS = 'Transactions'; // 交易紀錄
const SHEET_BUDGETS = 'Budgets'; // 預算設定
const SHEET_CATEGORIES = 'Categories'; // 分類清單
/************************************************
* 網頁入口
************************************************/
function doGet() {
// 先確保必需的工作表存在,沒有就自動建立
setupSpreadsheet();
// 回傳網頁
return HtmlService
.createTemplateFromFile('Index')
.evaluate()
.addMetaTag('viewport', 'width=device-width, initial-scale=1.0')
.setTitle('簡易記帳工具');
}
/************************************************
* 一次載入前端 HTML
************************************************/
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
/************************************************
* 0. 自動建立所需的工作表 (若尚未存在)
************************************************/
function setupSpreadsheet() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
// 1) 建立 Transactions (交易紀錄)
let sheetTx = ss.getSheetByName(SHEET_TRANSACTIONS);
if (!sheetTx) {
sheetTx = ss.insertSheet(SHEET_TRANSACTIONS);
// 設定欄位標題
sheetTx.appendRow(['日期時間', '類型(收入/支出)', '類別', '金額', '備註']);
}
// 2) 建立 Budgets (預算設定)
let sheetBd = ss.getSheetByName(SHEET_BUDGETS);
if (!sheetBd) {
sheetBd = ss.insertSheet(SHEET_BUDGETS);
// 設定欄位標題
sheetBd.appendRow(['類別', '預算金額']);
// 預設預算設定 (可根據需求修改或新增)
const defaultBudgets = [
['飲食', 5000],
['交通', 2000],
['娛樂', 3000],
['網購', 4000], // 將「房租」改為「網購」
['薪資', 0] // 收入類別預算可設為0或不設定
];
sheetBd.getRange(2, 1, defaultBudgets.length, 2).setValues(defaultBudgets);
}
// 3) 建立 Categories (分類清單)
let sheetCat = ss.getSheetByName(SHEET_CATEGORIES);
if (!sheetCat) {
sheetCat = ss.insertSheet(SHEET_CATEGORIES);
// 設定欄位標題
sheetCat.appendRow(['類別名稱', '類型(收入/支出)']);
// 常用的記帳類別 (預設)
const defaultCategories = [
['飲食', '支出'],
['交通', '支出'],
['娛樂', '支出'],
['網購', '支出'], // 將「房租」改為「網購」
['薪資', '收入'],
['其他收入', '收入'],
['其他支出', '支出']
];
sheetCat.getRange(2, 1, defaultCategories.length, 2).setValues(defaultCategories);
}
}
/************************************************
* 1. 取得分類清單
************************************************/
function getCategories() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_CATEGORIES);
const data = sheet.getDataRange().getValues(); // 全部資料
// 假設第一列是標題,所以從第二列開始
const categories = [];
for (let i = 1; i < data.length; i++) {
const row = data[i];
const name = row[0];
const type = row[1];
if (name && type) {
categories.push({
name: name,
type: type
});
}
}
return categories;
}
/************************************************
* 2. 取得預算設定
************************************************/
function getBudgets() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_BUDGETS);
const data = sheet.getDataRange().getValues();
// A欄: 類別, B欄: 預算金額
const budgets = {};
for (let i = 1; i < data.length; i++) {
const row = data[i];
const category = row[0];
const budget = parseFloat(row[1]) || 0;
if (category) {
budgets[category] = budget;
}
}
return budgets;
}
/************************************************
* 3. 寫入交易紀錄
************************************************/
function saveTransaction(transaction) {
// transaction = { dateTime, type, category, amount, note }
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_TRANSACTIONS);
// 檢查必要欄位
if (!transaction.dateTime || !transaction.type || !transaction.category || !transaction.amount) {
throw new Error('缺少必要的交易資訊');
}
// 檢查金額是否為數字
const amount = parseFloat(transaction.amount);
if (isNaN(amount)) {
throw new Error('金額必須為數字');
}
// 寫入 (日期時間, 類型, 類別, 金額, 備註)
sheet.appendRow([
transaction.dateTime,
transaction.type,
transaction.category,
amount,
transaction.note || ''
]);
return '交易已儲存!';
}
/************************************************
* 4. 取得本月支出總額 (依類別)
************************************************/
function getMonthlySpending() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_TRANSACTIONS);
const data = sheet.getDataRange().getValues();
// 取得當前年月,用於判斷「本月」
const now = new Date();
const currentMonth = now.getMonth();
const currentYear = now.getFullYear();
// 結構:
// A: 日期時間
// B: 類型
// C: 類別
// D: 金額
// E: 備註
const spendingByCategory = {};
// 從第二列開始 (第一列是標題)
for (let i = 1; i < data.length; i++) {
const row = data[i];
const dateTime = new Date(row[0]);
const type = row[1];
const category = row[2];
const amount = parseFloat(row[3]) || 0;
// 僅計算「支出」+「本月」
if (type === '支出' &&
dateTime.getMonth() === currentMonth &&
dateTime.getFullYear() === currentYear) {
if (!spendingByCategory[category]) {
spendingByCategory[category] = 0;
}
spendingByCategory[category] += amount;
}
}
return spendingByCategory;
}
/************************************************
* 5. 彙整預算 & 支出,回傳給前端
************************************************/
function getDashboardData() {
const budgets = getBudgets();
const categories = getCategories();
const spendingByCategory = getMonthlySpending();
const dashboardData = [];
categories.forEach(function(cat) {
if (cat.type === '支出') {
const categoryName = cat.name;
const budget = budgets[categoryName] || 0;
const spent = spendingByCategory[categoryName] || 0;
// 超支 / 接近超支 (可自訂閾值)
let warningLevel = 'normal';
if (budget > 0) {
if (spent >= budget) {
warningLevel = 'over'; // 超支
} else if (spent >= 0.8 * budget) {
warningLevel = 'near'; // 接近超支
}
}
dashboardData.push({
category: categoryName,
budget: budget,
spent: spent,
warningLevel: warningLevel
});
}
});
return dashboardData;
}
/************************************************
* 6. 取得本月所有支出明細
************************************************/
/**
* 回傳「本月支出」的所有交易明細
* @return {Array} expenses 本月支出的交易陣列,每筆包含:
* { dateTime, type, category, amount, note }
*/
function getThisMonthExpenses() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_TRANSACTIONS);
const data = sheet.getDataRange().getValues();
// 取得當前年月,用於判斷「本月」
const now = new Date();
const currentYear = now.getFullYear();
const currentMonth = now.getMonth(); // 0 ~ 11
// A: 日期時間, B: 類型, C: 類別, D: 金額, E: 備註
const expenses = [];
for(let i=1; i < data.length; i++){
const row = data[i];
const dateTimeStr = row[0];
const dateTime = new Date(dateTimeStr);
const type = row[1];
const category = row[2];
const amount = parseFloat(row[3]) || 0;
const note = row[4] || '';
// 篩選「本年、本月」且「類型為支出」的交易
if(type === '支出' &&
dateTime.getFullYear() === currentYear &&
dateTime.getMonth() === currentMonth) {
expenses.push({
dateTime: Utilities.formatDate(dateTime, ss.getSpreadsheetTimeZone(), "yyyy/MM/dd HH:mm"),
type: type,
category: category,
amount: amount,
note: note
});
}
}
return expenses;
}
將「房租」類別改為「網購」:
setupSpreadsheet() 函式中,defaultBudgets 和 defaultCategories 陣列中將「房租」改為「網購」,並設定相應的預算金額(此例中設為 4000,可根據需要調整)。資料驗證與錯誤處理:
saveTransaction 函式中,增加了對必要欄位的檢查,確保不會因為缺少資料而導致前端錯誤。日期時間格式化:
Utilities.formatDate 將日期時間格式化為 yyyy/MM/dd HH:mm,提升可讀性。Index.html接下來,我們將調整網頁介面的表單佈局,使「類型」與「類別」在同一列呈現,「金額」與「備註」也在同一列呈現。以下是更新後的完整 Index.html:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
/* 手機友善基礎樣式 */
body {
font-family: sans-serif;
margin: 10px;
}
h1 {
text-align: center;
margin-bottom: 0;
}
.sub-title {
text-align: center;
color: #666;
margin-top: 4px;
margin-bottom: 20px;
}
.container {
max-width: 500px;
margin: 0 auto;
}
.input-row {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
.input-row .input-field {
flex: 0 0 48%;
}
label {
display: block;
font-weight: bold;
margin-bottom: 4px;
}
input, select {
width: 100%;
box-sizing: border-box;
padding: 8px;
font-size: 16px;
}
button {
width: 100%;
padding: 10px;
font-size: 16px;
background: #4CAF50;
color: #fff;
border: none;
cursor: pointer;
margin-top: 10px;
border-radius: 4px;
}
button:hover {
background: #45a049;
}
/* 預算表 */
.budget-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.budget-table th, .budget-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.warning-normal {
background-color: #ffffff;
}
.warning-near {
background-color: #fff8db; /* 淡黃色 */
}
.warning-over {
background-color: #ffdcdc; /* 淡紅色 */
}
.message {
margin-top: 8px;
text-align: center;
}
/* 浮層 (Modal) 樣式 */
.modal-overlay {
position: fixed;
z-index: 999; /* 確保浮層在最上層 */
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景 */
display: none; /* 初始隱藏 */
justify-content: center;
align-items: center;
}
.modal-content {
background: #fff;
padding: 20px;
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.modal-header h2 {
margin: 0;
}
.close-btn {
background: #ccc;
color: #000;
border: none;
font-size: 16px;
padding: 5px 10px;
cursor: pointer;
border-radius: 4px;
}
.close-btn:hover {
background: #aaa;
}
.expense-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
}
.expense-table th, .expense-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.sort-btn {
background: #2196F3;
color: #fff;
padding: 8px 12px;
border: none;
cursor: pointer;
border-radius: 4px;
margin-bottom: 10px;
}
.sort-btn:hover {
background: #0b7dda;
}
/* 滾動條樣式(選用) */
.modal-content::-webkit-scrollbar {
width: 8px;
}
.modal-content::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.2);
border-radius: 4px;
}
</style>
</head>
<body>
<h1>簡易記帳工具</h1>
<div class="sub-title">Google Apps Script + 試算表</div>
<div class="container">
<!-- 新增交易表單 -->
<div class="input-row">
<!-- 類型 -->
<div class="input-field">
<label for="type">類型</label>
<select id="type" onchange="filterCategoryOptions()">
<option value="支出" selected>支出</option>
<option value="收入">收入</option>
</select>
</div>
<!-- 類別 -->
<div class="input-field">
<label for="category">類別</label>
<select id="category"></select>
</div>
</div>
<div class="input-row">
<!-- 金額 -->
<div class="input-field">
<label for="amount">金額</label>
<input type="number" id="amount" step="0.01" />
</div>
<!-- 備註 -->
<div class="input-field">
<label for="note">備註</label>
<input type="text" id="note" />
</div>
</div>
<!-- 日期與時間 -->
<div class="input-field">
<label for="dateTime">日期與時間</label>
<input type="datetime-local" id="dateTime" />
</div>
<button onclick="submitForm()">新增交易</button>
<div class="message" id="msgBox"></div>
<!-- 查看該月支出按鈕 -->
<button style="background:#f44336; margin-top:20px;" onclick="viewMonthlyExpenses()">
查看該月支出
</button>
<!-- 預算表 -->
<table class="budget-table" id="budgetTable">
<thead>
<tr>
<th>類別</th>
<th>預算</th>
<th>本月已用</th>
</tr>
</thead>
<tbody id="budgetTableBody"></tbody>
</table>
</div>
<!-- 浮層 (Modal) 內容 -->
<div class="modal-overlay" id="monthlyExpensesModal">
<div class="modal-content">
<div class="modal-header">
<h2>本月支出列表</h2>
<button class="close-btn" onclick="closeMonthlyExpensesModal()">X</button>
</div>
<!-- 排序按鈕 -->
<button class="sort-btn" onclick="sortExpensesByCategory()">
按「類別」排序
</button>
<!-- 支出表格 -->
<table class="expense-table" id="expenseTable">
<thead>
<tr>
<th>日期時間</th>
<th>類別</th>
<th>金額</th>
<th>備註</th>
</tr>
</thead>
<tbody id="expenseTableBody"></tbody>
</table>
<!-- 總計 -->
<div id="totalAmount" style="text-align:right; font-weight:bold;"></div>
</div>
</div>
<script>
/********************************************
* 前端初始化:載入「分類清單」與「預算表」
********************************************/
document.addEventListener('DOMContentLoaded', function() {
setDefaultDateTime();
// 載入分類
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
// 載入預算
refreshDashboard();
});
// 設定「現在時間」到 datetime-local
function setDefaultDateTime() {
const now = new Date();
const localISO = new Date(now.getTime() - now.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16); // 'YYYY-MM-DDTHH:MM'
document.getElementById('dateTime').value = localISO;
}
// 填充分類下拉
function populateCategorySelect(categories) {
const categorySelect = document.getElementById('category');
categorySelect.innerHTML = '';
// 根據選擇的類型篩選類別
const selectedType = document.getElementById('type').value;
categories.forEach(function(cat) {
if (cat.type === selectedType) {
const option = document.createElement('option');
option.value = cat.name;
option.text = cat.name;
categorySelect.appendChild(option);
}
});
}
// 根據「類型」篩選「類別」選項
function filterCategoryOptions() {
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
}
// 新增交易
function submitForm() {
const dateTime = document.getElementById('dateTime').value;
const type = document.getElementById('type').value;
const category = document.getElementById('category').value;
const amount = document.getElementById('amount').value;
const note = document.getElementById('note').value;
// 簡易驗證
if (!dateTime || !type || !category || !amount) {
showMessage('請填寫完整資訊', true);
return;
}
const transaction = {
dateTime: dateTime,
type: type,
category: category,
amount: amount,
note: note
};
google.script.run
.withSuccessHandler(function(msg) {
showMessage(msg, false);
setDefaultDateTime();
document.getElementById('amount').value = '';
document.getElementById('note').value = '';
// 刷新預算
refreshDashboard();
})
.withFailureHandler(function(err) {
showMessage('寫入失敗: ' + err.message, true);
})
.saveTransaction(transaction);
}
// 顯示訊息
function showMessage(msg, isError) {
const msgBox = document.getElementById('msgBox');
msgBox.style.color = isError ? 'red' : 'green';
msgBox.textContent = msg;
}
// 刷新預算表
function refreshDashboard() {
google.script.run
.withSuccessHandler(function(dashboardData){
populateBudgetTable(dashboardData);
})
.getDashboardData();
}
function populateBudgetTable(data) {
const tbody = document.getElementById('budgetTableBody');
tbody.innerHTML = '';
data.forEach(function(item){
const tr = document.createElement('tr');
if (item.warningLevel === 'over') {
tr.className = 'warning-over';
} else if (item.warningLevel === 'near') {
tr.className = 'warning-near';
} else {
tr.className = 'warning-normal';
}
const tdCat = document.createElement('td');
tdCat.textContent = item.category;
const tdBudget = document.createElement('td');
tdBudget.textContent = item.budget || 0;
const tdSpent = document.createElement('td');
tdSpent.textContent = item.spent;
tr.appendChild(tdCat);
tr.appendChild(tdBudget);
tr.appendChild(tdSpent);
tbody.appendChild(tr);
});
}
/********************************************
* 新增:查看「該月支出」按鈕的功能
********************************************/
let monthlyExpenses = []; // 用於儲存本月支出資料
// 1) 取得本月支出 -> 顯示浮層
function viewMonthlyExpenses() {
google.script.run
.withSuccessHandler(function(expenses){
if (Array.isArray(expenses)) {
monthlyExpenses = expenses;
renderMonthlyExpenses();
document.getElementById('monthlyExpensesModal').style.display = 'flex';
} else {
alert('取得的支出資料格式錯誤。');
}
})
.withFailureHandler(function(err){
alert('取得本月支出失敗:' + err.message);
})
.getThisMonthExpenses();
}
// 2) 渲染本月支出的表格
function renderMonthlyExpenses() {
const tbody = document.getElementById('expenseTableBody');
tbody.innerHTML = '';
if (!Array.isArray(monthlyExpenses) || monthlyExpenses.length === 0) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 4;
td.textContent = '本月尚無支出紀錄。';
tr.appendChild(td);
tbody.appendChild(tr);
document.getElementById('totalAmount').textContent = '總計:0';
return;
}
// 計算總計
let total = 0;
monthlyExpenses.forEach(function(item){
total += parseFloat(item.amount) || 0;
const tr = document.createElement('tr');
const tdDateTime = document.createElement('td');
tdDateTime.textContent = item.dateTime; // 格式化後的日期時間
const tdCategory = document.createElement('td');
tdCategory.textContent = item.category;
const tdAmount = document.createElement('td');
tdAmount.textContent = item.amount;
const tdNote = document.createElement('td');
tdNote.textContent = item.note;
tr.appendChild(tdDateTime);
tr.appendChild(tdCategory);
tr.appendChild(tdAmount);
tr.appendChild(tdNote);
tbody.appendChild(tr);
});
// 顯示總計
document.getElementById('totalAmount').textContent = '總計:' + total;
}
// 3) 按類別排序 (升冪) 後重新渲染
function sortExpensesByCategory() {
if (!Array.isArray(monthlyExpenses)) return;
monthlyExpenses.sort(function(a, b){
return a.category.localeCompare(b.category, 'zh-TW');
});
renderMonthlyExpenses();
}
// 4) 關閉浮層
function closeMonthlyExpensesModal() {
document.getElementById('monthlyExpensesModal').style.display = 'none';
}
</script>
</body>
</html>
將「類型」與「類別」放在同一列,分為兩欄呈現:
.input-row 容器搭配 flex 佈局,將「類型」與「類別」各自包裹在 .input-field 中,並設定 flex: 0 0 48% 以使兩欄佔據約一半的寬度。將「金額」與「備註」放在同一列,分為兩欄呈現:
.input-row 容器來排列「金額」與「備註」欄位,使其並排顯示。將「日期與時間」放在單獨一列,因為這個欄位需要更寬的空間。
調整了網頁容器的最大寬度:
.container 的 max-width 從 400px 增加至 500px,以提供更寬敞的表單佈局空間。增強用戶體驗:
border-radius) 以提升美觀度。動態篩選類別選項:
filterCategoryOptions() 函式重新載入對應類型的類別選項,確保使用者僅能選擇符合類型的分類。更新 Code.gs:
Code.gs 替換為上述提供的完整程式碼。更新 Index.html:
Index.html 替換為上述提供的完整 HTML 內容。重新部署應用程式:
測試功能:
Transactions 工作表中。修正與優化:
交易無法正確寫入 Transactions 工作表:
Code.gs 中的 saveTransaction 函式是否正確執行,並且沒有被其他錯誤中斷。浮層無法顯示或渲染支出紀錄:
getThisMonthExpenses 函式正確回傳資料,即使當月沒有支出紀錄也應回傳空陣列。renderMonthlyExpenses 函式中。.modal-overlay 的 display 屬性。類別選單無法正確篩選:
filterCategoryOptions 函式正確呼叫 getCategories 並根據選擇的類型更新「類別」選單。Categories 工作表中類別的「類型」欄位是否正確填寫(「收入」或「支出」)。預算表未正確更新:
refreshDashboard 函式被呼叫並且 getDashboardData 函式正確回傳資料。Budgets 工作表中的預算金額是否正確設定,並且與 Categories 中的「支出」類別一致。日期時間顯示錯誤:
datetime-local 欄位正確選擇日期與時間。Transactions 工作表中的「日期時間」欄位為有效的日期格式,避免 Invalid Date 問題。透過以上的修改,你的記帳工具將具備以下功能:
請按照上述步驟進行更新與部署,並在測試過程中確認所有功能正常運作。如有任何進一步的問題或需求,歡迎隨時提問!
調整查看該月支出的圖層中,上方的標題"本月支出列表"及"X"關閉按鈕分別為一半寬度
思考 花的時間為 5 秒
了解,我將協助你調整「查看該月支出」的浮層(Modal)中的標題「本月支出列表」及關閉按鈕「X」各自佔據一半的寬度。這將涉及調整 CSS 和 HTML 結構,以確保這兩個元素在同一行上均分可用空間。
以下是完整的 Index.html,已根據你的需求進行調整:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
/* 手機友善基礎樣式 */
body {
font-family: sans-serif;
margin: 10px;
}
h1 {
text-align: center;
margin-bottom: 0;
}
.sub-title {
text-align: center;
color: #666;
margin-top: 4px;
margin-bottom: 20px;
}
.container {
max-width: 500px;
margin: 0 auto;
}
.input-row {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
.input-row .input-field {
flex: 0 0 48%;
}
label {
display: block;
font-weight: bold;
margin-bottom: 4px;
}
input, select {
width: 100%;
box-sizing: border-box;
padding: 8px;
font-size: 16px;
}
button {
width: 100%;
padding: 10px;
font-size: 16px;
background: #4CAF50;
color: #fff;
border: none;
cursor: pointer;
margin-top: 10px;
border-radius: 4px;
}
button:hover {
background: #45a049;
}
/* 預算表 */
.budget-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.budget-table th, .budget-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.warning-normal {
background-color: #ffffff;
}
.warning-near {
background-color: #fff8db; /* 淡黃色 */
}
.warning-over {
background-color: #ffdcdc; /* 淡紅色 */
}
.message {
margin-top: 8px;
text-align: center;
}
/* 浮層 (Modal) 樣式 */
.modal-overlay {
position: fixed;
z-index: 999; /* 確保浮層在最上層 */
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景 */
display: none; /* 初始隱藏 */
justify-content: center;
align-items: center;
}
.modal-content {
background: #fff;
padding: 20px;
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
}
/* 調整後的模態框標題 */
.modal-header {
display: flex;
width: 100%;
margin-bottom: 10px;
}
.modal-header h2 {
flex: 1;
text-align: left;
margin: 0;
font-size: 20px;
}
.modal-header .close-btn {
flex: 1;
text-align: right;
background: #ccc;
color: #000;
border: none;
font-size: 20px;
padding: 5px 10px;
cursor: pointer;
border-radius: 4px;
}
.modal-header .close-btn:hover {
background: #aaa;
}
.expense-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
}
.expense-table th, .expense-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.sort-btn {
background: #2196F3;
color: #fff;
padding: 8px 12px;
border: none;
cursor: pointer;
border-radius: 4px;
margin-bottom: 10px;
}
.sort-btn:hover {
background: #0b7dda;
}
/* 滾動條樣式(選用) */
.modal-content::-webkit-scrollbar {
width: 8px;
}
.modal-content::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.2);
border-radius: 4px;
}
</style>
</head>
<body>
<h1>簡易記帳工具</h1>
<div class="sub-title">Google Apps Script + 試算表</div>
<div class="container">
<!-- 新增交易表單 -->
<div class="input-row">
<!-- 類型 -->
<div class="input-field">
<label for="type">類型</label>
<select id="type" onchange="filterCategoryOptions()">
<option value="支出" selected>支出</option>
<option value="收入">收入</option>
</select>
</div>
<!-- 類別 -->
<div class="input-field">
<label for="category">類別</label>
<select id="category"></select>
</div>
</div>
<div class="input-row">
<!-- 金額 -->
<div class="input-field">
<label for="amount">金額</label>
<input type="number" id="amount" step="0.01" />
</div>
<!-- 備註 -->
<div class="input-field">
<label for="note">備註</label>
<input type="text" id="note" />
</div>
</div>
<!-- 日期與時間 -->
<div class="input-field">
<label for="dateTime">日期與時間</label>
<input type="datetime-local" id="dateTime" />
</div>
<button onclick="submitForm()">新增交易</button>
<div class="message" id="msgBox"></div>
<!-- 查看該月支出按鈕 -->
<button style="background:#f44336; margin-top:20px;" onclick="viewMonthlyExpenses()">
查看該月支出
</button>
<!-- 預算表 -->
<table class="budget-table" id="budgetTable">
<thead>
<tr>
<th>類別</th>
<th>預算</th>
<th>本月已用</th>
</tr>
</thead>
<tbody id="budgetTableBody"></tbody>
</table>
</div>
<!-- 浮層 (Modal) 內容 -->
<div class="modal-overlay" id="monthlyExpensesModal">
<div class="modal-content">
<div class="modal-header">
<h2>本月支出列表</h2>
<button class="close-btn" onclick="closeMonthlyExpensesModal()">X</button>
</div>
<!-- 排序按鈕 -->
<button class="sort-btn" onclick="sortExpensesByCategory()">
按「類別」排序
</button>
<!-- 支出表格 -->
<table class="expense-table" id="expenseTable">
<thead>
<tr>
<th>日期時間</th>
<th>類別</th>
<th>金額</th>
<th>備註</th>
</tr>
</thead>
<tbody id="expenseTableBody"></tbody>
</table>
<!-- 總計 -->
<div id="totalAmount" style="text-align:right; font-weight:bold;"></div>
</div>
</div>
<script>
/********************************************
* 前端初始化:載入「分類清單」與「預算表」
********************************************/
document.addEventListener('DOMContentLoaded', function() {
setDefaultDateTime();
// 載入分類
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
// 載入預算
refreshDashboard();
});
// 設定「現在時間」到 datetime-local
function setDefaultDateTime() {
const now = new Date();
const localISO = new Date(now.getTime() - now.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16); // 'YYYY-MM-DDTHH:MM'
document.getElementById('dateTime').value = localISO;
}
// 填充分類下拉
function populateCategorySelect(categories) {
const categorySelect = document.getElementById('category');
categorySelect.innerHTML = '';
// 根據選擇的類型篩選類別
const selectedType = document.getElementById('type').value;
categories.forEach(function(cat) {
if (cat.type === selectedType) {
const option = document.createElement('option');
option.value = cat.name;
option.text = cat.name;
categorySelect.appendChild(option);
}
});
}
// 根據「類型」篩選「類別」選項
function filterCategoryOptions() {
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
}
// 新增交易
function submitForm() {
const dateTime = document.getElementById('dateTime').value;
const type = document.getElementById('type').value;
const category = document.getElementById('category').value;
const amount = document.getElementById('amount').value;
const note = document.getElementById('note').value;
// 簡易驗證
if (!dateTime || !type || !category || !amount) {
showMessage('請填寫完整資訊', true);
return;
}
const transaction = {
dateTime: dateTime,
type: type,
category: category,
amount: amount,
note: note
};
google.script.run
.withSuccessHandler(function(msg) {
showMessage(msg, false);
setDefaultDateTime();
document.getElementById('amount').value = '';
document.getElementById('note').value = '';
// 刷新預算
refreshDashboard();
})
.withFailureHandler(function(err) {
showMessage('寫入失敗: ' + err.message, true);
})
.saveTransaction(transaction);
}
// 顯示訊息
function showMessage(msg, isError) {
const msgBox = document.getElementById('msgBox');
msgBox.style.color = isError ? 'red' : 'green';
msgBox.textContent = msg;
}
// 刷新預算表
function refreshDashboard() {
google.script.run
.withSuccessHandler(function(dashboardData){
populateBudgetTable(dashboardData);
})
.getDashboardData();
}
function populateBudgetTable(data) {
const tbody = document.getElementById('budgetTableBody');
tbody.innerHTML = '';
data.forEach(function(item){
const tr = document.createElement('tr');
if (item.warningLevel === 'over') {
tr.className = 'warning-over';
} else if (item.warningLevel === 'near') {
tr.className = 'warning-near';
} else {
tr.className = 'warning-normal';
}
const tdCat = document.createElement('td');
tdCat.textContent = item.category;
const tdBudget = document.createElement('td');
tdBudget.textContent = item.budget || 0;
const tdSpent = document.createElement('td');
tdSpent.textContent = item.spent;
tr.appendChild(tdCat);
tr.appendChild(tdBudget);
tr.appendChild(tdSpent);
tbody.appendChild(tr);
});
}
/********************************************
* 新增:查看「該月支出」按鈕的功能
********************************************/
let monthlyExpenses = []; // 用於儲存本月支出資料
// 1) 取得本月支出 -> 顯示浮層
function viewMonthlyExpenses() {
google.script.run
.withSuccessHandler(function(expenses){
if (Array.isArray(expenses)) {
monthlyExpenses = expenses;
renderMonthlyExpenses();
document.getElementById('monthlyExpensesModal').style.display = 'flex';
} else {
alert('取得的支出資料格式錯誤。');
}
})
.withFailureHandler(function(err){
alert('取得本月支出失敗:' + err.message);
})
.getThisMonthExpenses();
}
// 2) 渲染本月支出的表格
function renderMonthlyExpenses() {
const tbody = document.getElementById('expenseTableBody');
tbody.innerHTML = '';
if (!Array.isArray(monthlyExpenses) || monthlyExpenses.length === 0) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 4;
td.textContent = '本月尚無支出紀錄。';
tr.appendChild(td);
tbody.appendChild(tr);
document.getElementById('totalAmount').textContent = '總計:0';
return;
}
// 計算總計
let total = 0;
monthlyExpenses.forEach(function(item){
total += parseFloat(item.amount) || 0;
const tr = document.createElement('tr');
const tdDateTime = document.createElement('td');
tdDateTime.textContent = item.dateTime; // 格式化後的日期時間
const tdCategory = document.createElement('td');
tdCategory.textContent = item.category;
const tdAmount = document.createElement('td');
tdAmount.textContent = item.amount;
const tdNote = document.createElement('td');
tdNote.textContent = item.note;
tr.appendChild(tdDateTime);
tr.appendChild(tdCategory);
tr.appendChild(tdAmount);
tr.appendChild(tdNote);
tbody.appendChild(tr);
});
// 顯示總計
document.getElementById('totalAmount').textContent = '總計:' + total;
}
// 3) 按類別排序 (升冪) 後重新渲染
function sortExpensesByCategory() {
if (!Array.isArray(monthlyExpenses)) return;
monthlyExpenses.sort(function(a, b){
return a.category.localeCompare(b.category, 'zh-TW');
});
renderMonthlyExpenses();
}
// 4) 關閉浮層
function closeMonthlyExpensesModal() {
document.getElementById('monthlyExpensesModal').style.display = 'none';
}
</script>
</body>
</html>
修改類別名稱:
Code.gs 中,將預設預算和分類清單中的「房租」改為「網購」。這已在上一次的 Code.gs 中完成,無需再次修改。調整表單佈局:
.input-row 容器與 flex 佈局,將「類型」與「類別」放在同一列,並分為兩欄呈現。.input-row 中,分為兩欄呈現。調整浮層標題與關閉按鈕的佈局:
.modal-header 中,移除 justify-content: space-between;,並將 flex: 1 分配給標題和關閉按鈕,以確保它們各自佔據一半的寬度。text-align 以確保標題左對齊,關閉按鈕右對齊。.modal-header 中包含 <h2> 和 <button class="close-btn">X</button> 兩個元素,並正確套用 CSS 類別。動態篩選類別選項:
filterCategoryOptions 函式會重新載入對應類型的類別選項,確保使用者僅能選擇符合類型的分類。防錯處理:
viewMonthlyExpenses 函式中,新增了檢查確保接收到的 expenses 是一個有效的陣列。renderMonthlyExpenses 函式中,處理當本月無支出紀錄時,顯示相應的提示訊息,避免對 null 執行 forEach。更新 Code.gs:
Code.gs 中已將「房租」類別改為「網購」,並且 setupSpreadsheet 函式中反映了這一變更。更新 Index.html:
Index.html 內容替換你現有的 Index.html。重新部署應用程式:
測試表單佈局:
測試浮層佈局:
驗證功能正常:
浮層標題與關閉按鈕未正確分佈:
.modal-header 的 flex 設定是否正確。.modal-header h2 和 .modal-header .close-btn 的 flex 屬性設為 1,使其各自佔據一半的寬度。新增交易後,預算表未更新:
submitForm 函式中,refreshDashboard 函式是否被正確呼叫。Code.gs 中的 getDashboardData 函式是否正確計算預算與支出。查看本月支出浮層無法正確顯示支出紀錄:
getThisMonthExpenses 函式是否正確回傳支出紀錄。renderMonthlyExpenses 函式,確保它能正確處理接收到的資料。類別選單未正確更新:
filterCategoryOptions 函式是否被正確觸發。Categories 工作表中類別的「類型」欄位是否正確填寫(「收入」或「支出」)。透過以上的調整,你的記帳工具應該能夠符合以下需求:
請按照上述步驟進行更新與部署,並在使用過程中進行測試以確保所有功能正常運作。如有任何進一步的問題或需求,歡迎隨時提出!
增加"搜尋/篩選功能:在浮層中增加搜尋框,能只顯示特定關鍵字或日期範圍的支出"及"匯出功能:前端將當前表格下載為 CSV"
思考 花的時間為 6 秒
了解,你希望在「查看該月支出」的浮層中增加以下功能:
搜尋/篩選功能:
匯出功能:
以下是完整的 Code.gs 和更新後的 Index.html,包括上述功能的實作。
Code.gsCode.gs 的部分不需要做額外的修改,因為搜尋/篩選功能將在前端處理,而匯出功能也可以在前端完成。然而,確保 getThisMonthExpenses 函式回傳所有本月支出紀錄,以便前端進行過濾和匯出。
/************************************************
* 全域設定
************************************************/
const SHEET_TRANSACTIONS = 'Transactions'; // 交易紀錄
const SHEET_BUDGETS = 'Budgets'; // 預算設定
const SHEET_CATEGORIES = 'Categories'; // 分類清單
/************************************************
* 網頁入口
************************************************/
function doGet() {
// 先確保必需的工作表存在,沒有就自動建立
setupSpreadsheet();
// 回傳網頁
return HtmlService
.createTemplateFromFile('Index')
.evaluate()
.addMetaTag('viewport', 'width=device-width, initial-scale=1.0')
.setTitle('簡易記帳工具');
}
/************************************************
* 一次載入前端 HTML
************************************************/
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
/************************************************
* 0. 自動建立所需的工作表 (若尚未存在)
************************************************/
function setupSpreadsheet() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
// 1) 建立 Transactions (交易紀錄)
let sheetTx = ss.getSheetByName(SHEET_TRANSACTIONS);
if (!sheetTx) {
sheetTx = ss.insertSheet(SHEET_TRANSACTIONS);
// 設定欄位標題
sheetTx.appendRow(['日期時間', '類型(收入/支出)', '類別', '金額', '備註']);
}
// 2) 建立 Budgets (預算設定)
let sheetBd = ss.getSheetByName(SHEET_BUDGETS);
if (!sheetBd) {
sheetBd = ss.insertSheet(SHEET_BUDGETS);
// 設定欄位標題
sheetBd.appendRow(['類別', '預算金額']);
// 預設預算設定 (可根據需求修改或新增)
const defaultBudgets = [
['飲食', 5000],
['交通', 2000],
['娛樂', 3000],
['網購', 4000], // 將「房租」改為「網購」
['薪資', 0] // 收入類別預算可設為0或不設定
];
sheetBd.getRange(2, 1, defaultBudgets.length, 2).setValues(defaultBudgets);
}
// 3) 建立 Categories (分類清單)
let sheetCat = ss.getSheetByName(SHEET_CATEGORIES);
if (!sheetCat) {
sheetCat = ss.insertSheet(SHEET_CATEGORIES);
// 設定欄位標題
sheetCat.appendRow(['類別名稱', '類型(收入/支出)']);
// 常用的記帳類別 (預設)
const defaultCategories = [
['飲食', '支出'],
['交通', '支出'],
['娛樂', '支出'],
['網購', '支出'], // 將「房租」改為「網購」
['薪資', '收入'],
['其他收入', '收入'],
['其他支出', '支出']
];
sheetCat.getRange(2, 1, defaultCategories.length, 2).setValues(defaultCategories);
}
}
/************************************************
* 1. 取得分類清單
************************************************/
function getCategories() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_CATEGORIES);
const data = sheet.getDataRange().getValues(); // 全部資料
// 假設第一列是標題,所以從第二列開始
const categories = [];
for (let i = 1; i < data.length; i++) {
const row = data[i];
const name = row[0];
const type = row[1];
if (name && type) {
categories.push({
name: name,
type: type
});
}
}
return categories;
}
/************************************************
* 2. 取得預算設定
************************************************/
function getBudgets() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_BUDGETS);
const data = sheet.getDataRange().getValues();
// A欄: 類別, B欄: 預算金額
const budgets = {};
for (let i = 1; i < data.length; i++) {
const row = data[i];
const category = row[0];
const budget = parseFloat(row[1]) || 0;
if (category) {
budgets[category] = budget;
}
}
return budgets;
}
/************************************************
* 3. 寫入交易紀錄
************************************************/
function saveTransaction(transaction) {
// transaction = { dateTime, type, category, amount, note }
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_TRANSACTIONS);
// 檢查必要欄位
if (!transaction.dateTime || !transaction.type || !transaction.category || !transaction.amount) {
throw new Error('缺少必要的交易資訊');
}
// 檢查金額是否為數字
const amount = parseFloat(transaction.amount);
if (isNaN(amount)) {
throw new Error('金額必須為數字');
}
// 寫入 (日期時間, 類型, 類別, 金額, 備註)
sheet.appendRow([
transaction.dateTime,
transaction.type,
transaction.category,
amount,
transaction.note || ''
]);
return '交易已儲存!';
}
/************************************************
* 4. 取得本月支出總額 (依類別)
************************************************/
function getMonthlySpending() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_TRANSACTIONS);
const data = sheet.getDataRange().getValues();
// 取得當前年月,用於判斷「本月」
const now = new Date();
const currentMonth = now.getMonth();
const currentYear = now.getFullYear();
// 結構:
// A: 日期時間
// B: 類型
// C: 類別
// D: 金額
// E: 備註
const spendingByCategory = {};
// 從第二列開始 (第一列是標題)
for (let i = 1; i < data.length; i++) {
const row = data[i];
const dateTime = new Date(row[0]);
const type = row[1];
const category = row[2];
const amount = parseFloat(row[3]) || 0;
// 僅計算「支出」+「本月」
if (type === '支出' &&
dateTime.getMonth() === currentMonth &&
dateTime.getFullYear() === currentYear) {
if (!spendingByCategory[category]) {
spendingByCategory[category] = 0;
}
spendingByCategory[category] += amount;
}
}
return spendingByCategory;
}
/************************************************
* 5. 彙整預算 & 支出,回傳給前端
************************************************/
function getDashboardData() {
const budgets = getBudgets();
const categories = getCategories();
const spendingByCategory = getMonthlySpending();
const dashboardData = [];
categories.forEach(function(cat) {
if (cat.type === '支出') {
const categoryName = cat.name;
const budget = budgets[categoryName] || 0;
const spent = spendingByCategory[categoryName] || 0;
// 超支 / 接近超支 (可自訂閾值)
let warningLevel = 'normal';
if (budget > 0) {
if (spent >= budget) {
warningLevel = 'over'; // 超支
} else if (spent >= 0.8 * budget) {
warningLevel = 'near'; // 接近超支
}
}
dashboardData.push({
category: categoryName,
budget: budget,
spent: spent,
warningLevel: warningLevel
});
}
});
return dashboardData;
}
/************************************************
* 6. 取得本月所有支出明細
************************************************/
/**
* 回傳「本月支出」的所有交易明細
* @return {Array} expenses 本月支出的交易陣列,每筆包含:
* { dateTime, type, category, amount, note }
*/
function getThisMonthExpenses() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_TRANSACTIONS);
const data = sheet.getDataRange().getValues();
// 取得當前年月,用於判斷「本月」
const now = new Date();
const currentYear = now.getFullYear();
const currentMonth = now.getMonth(); // 0 ~ 11
// A: 日期時間, B: 類型, C: 類別, D: 金額, E: 備註
const expenses = [];
for(let i=1; i < data.length; i++){
const row = data[i];
const dateTimeStr = row[0];
const dateTime = new Date(dateTimeStr);
const type = row[1];
const category = row[2];
const amount = parseFloat(row[3]) || 0;
const note = row[4] || '';
// 篩選「本年、本月」且「類型為支出」的交易
if(type === '支出' &&
dateTime.getFullYear() === currentYear &&
dateTime.getMonth() === currentMonth) {
expenses.push({
dateTime: Utilities.formatDate(dateTime, ss.getSpreadsheetTimeZone(), "yyyy/MM/dd HH:mm"),
type: type,
category: category,
amount: amount,
note: note
});
}
}
return expenses;
}
Index.html以下是更新後的 Index.html,包括搜尋/篩選功能和匯出功能的實作:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
/* 手機友善基礎樣式 */
body {
font-family: sans-serif;
margin: 10px;
}
h1 {
text-align: center;
margin-bottom: 0;
}
.sub-title {
text-align: center;
color: #666;
margin-top: 4px;
margin-bottom: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.input-row {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
.input-row .input-field {
flex: 0 0 48%;
}
label {
display: block;
font-weight: bold;
margin-bottom: 4px;
}
input, select {
width: 100%;
box-sizing: border-box;
padding: 8px;
font-size: 16px;
}
button {
width: 100%;
padding: 10px;
font-size: 16px;
background: #4CAF50;
color: #fff;
border: none;
cursor: pointer;
margin-top: 10px;
border-radius: 4px;
}
button:hover {
background: #45a049;
}
/* 預算表 */
.budget-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.budget-table th, .budget-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.warning-normal {
background-color: #ffffff;
}
.warning-near {
background-color: #fff8db; /* 淡黃色 */
}
.warning-over {
background-color: #ffdcdc; /* 淡紅色 */
}
.message {
margin-top: 8px;
text-align: center;
}
/* 浮層 (Modal) 樣式 */
.modal-overlay {
position: fixed;
z-index: 999; /* 確保浮層在最上層 */
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景 */
display: none; /* 初始隱藏 */
justify-content: center;
align-items: center;
}
.modal-content {
background: #fff;
padding: 20px;
border-radius: 8px;
width: 95%;
max-width: 800px;
max-height: 90vh;
overflow-y: auto;
}
/* 調整後的模態框標題 */
.modal-header {
display: flex;
width: 100%;
margin-bottom: 10px;
}
.modal-header h2 {
flex: 1;
text-align: left;
margin: 0;
font-size: 20px;
}
.modal-header .close-btn {
flex: 1;
text-align: right;
background: #ccc;
color: #000;
border: none;
font-size: 20px;
padding: 5px 10px;
cursor: pointer;
border-radius: 4px;
}
.modal-header .close-btn:hover {
background: #aaa;
}
.expense-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
}
.expense-table th, .expense-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.sort-btn, .filter-btn, .export-btn {
background: #2196F3;
color: #fff;
padding: 8px 12px;
border: none;
cursor: pointer;
border-radius: 4px;
margin-right: 10px;
margin-bottom: 10px;
}
.sort-btn:hover, .filter-btn:hover, .export-btn:hover {
background: #0b7dda;
}
/* 滾動條樣式(選用) */
.modal-content::-webkit-scrollbar {
width: 8px;
}
.modal-content::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.2);
border-radius: 4px;
}
</style>
</head>
<body>
<h1>簡易記帳工具</h1>
<div class="sub-title">Google Apps Script + 試算表</div>
<div class="container">
<!-- 新增交易表單 -->
<div class="input-row">
<!-- 類型 -->
<div class="input-field">
<label for="type">類型</label>
<select id="type" onchange="filterCategoryOptions()">
<option value="支出" selected>支出</option>
<option value="收入">收入</option>
</select>
</div>
<!-- 類別 -->
<div class="input-field">
<label for="category">類別</label>
<select id="category"></select>
</div>
</div>
<div class="input-row">
<!-- 金額 -->
<div class="input-field">
<label for="amount">金額</label>
<input type="number" id="amount" step="0.01" />
</div>
<!-- 備註 -->
<div class="input-field">
<label for="note">備註</label>
<input type="text" id="note" />
</div>
</div>
<!-- 日期與時間 -->
<div class="input-field">
<label for="dateTime">日期與時間</label>
<input type="datetime-local" id="dateTime" />
</div>
<button onclick="submitForm()">新增交易</button>
<div class="message" id="msgBox"></div>
<!-- 查看該月支出按鈕 -->
<button style="background:#f44336; margin-top:20px;" onclick="viewMonthlyExpenses()">
查看該月支出
</button>
<!-- 預算表 -->
<table class="budget-table" id="budgetTable">
<thead>
<tr>
<th>類別</th>
<th>預算</th>
<th>本月已用</th>
</tr>
</thead>
<tbody id="budgetTableBody"></tbody>
</table>
</div>
<!-- 浮層 (Modal) 內容 -->
<div class="modal-overlay" id="monthlyExpensesModal">
<div class="modal-content">
<div class="modal-header">
<h2>本月支出列表</h2>
<button class="close-btn" onclick="closeMonthlyExpensesModal()">X</button>
</div>
<!-- 搜尋/篩選區域 -->
<div style="margin-bottom: 15px;">
<input type="text" id="searchKeyword" placeholder="搜尋關鍵字(類別或備註)" style="width: 48%; padding: 8px; margin-right: 4%;">
<input type="date" id="filterStartDate" placeholder="開始日期" style="width: 48%; padding: 8px;">
<input type="date" id="filterEndDate" placeholder="結束日期" style="width: 48%; padding: 8px; margin-top: 10px;">
<button class="filter-btn" onclick="applyFilters()" style="width: 48%; margin-top: 10px;">套用篩選</button>
<button class="filter-btn" onclick="resetFilters()" style="width: 48%; margin-top: 10px; background:#555;">重置篩選</button>
</div>
<!-- 排序及匯出按鈕 -->
<div style="margin-bottom: 15px;">
<button class="sort-btn" onclick="sortExpensesByCategory()">
按「類別」排序
</button>
<button class="export-btn" onclick="exportToCSV()">
匯出為 CSV
</button>
</div>
<!-- 支出表格 -->
<table class="expense-table" id="expenseTable">
<thead>
<tr>
<th>日期時間</th>
<th>類別</th>
<th>金額</th>
<th>備註</th>
</tr>
</thead>
<tbody id="expenseTableBody"></tbody>
</table>
<!-- 總計 -->
<div id="totalAmount" style="text-align:right; font-weight:bold;"></div>
</div>
</div>
<script>
/********************************************
* 前端初始化:載入「分類清單」與「預算表」
********************************************/
document.addEventListener('DOMContentLoaded', function() {
setDefaultDateTime();
// 載入分類
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
// 載入預算
refreshDashboard();
});
// 設定「現在時間」到 datetime-local
function setDefaultDateTime() {
const now = new Date();
const localISO = new Date(now.getTime() - now.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16); // 'YYYY-MM-DDTHH:MM'
document.getElementById('dateTime').value = localISO;
}
// 填充分類下拉
function populateCategorySelect(categories) {
const categorySelect = document.getElementById('category');
categorySelect.innerHTML = '';
// 根據選擇的類型篩選類別
const selectedType = document.getElementById('type').value;
categories.forEach(function(cat) {
if (cat.type === selectedType) {
const option = document.createElement('option');
option.value = cat.name;
option.text = cat.name;
categorySelect.appendChild(option);
}
});
}
// 根據「類型」篩選「類別」選項
function filterCategoryOptions() {
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
}
// 新增交易
function submitForm() {
const dateTime = document.getElementById('dateTime').value;
const type = document.getElementById('type').value;
const category = document.getElementById('category').value;
const amount = document.getElementById('amount').value;
const note = document.getElementById('note').value;
// 簡易驗證
if (!dateTime || !type || !category || !amount) {
showMessage('請填寫完整資訊', true);
return;
}
const transaction = {
dateTime: dateTime,
type: type,
category: category,
amount: amount,
note: note
};
google.script.run
.withSuccessHandler(function(msg) {
showMessage(msg, false);
setDefaultDateTime();
document.getElementById('amount').value = '';
document.getElementById('note').value = '';
// 刷新預算
refreshDashboard();
})
.withFailureHandler(function(err) {
showMessage('寫入失敗: ' + err.message, true);
})
.saveTransaction(transaction);
}
// 顯示訊息
function showMessage(msg, isError) {
const msgBox = document.getElementById('msgBox');
msgBox.style.color = isError ? 'red' : 'green';
msgBox.textContent = msg;
}
// 刷新預算表
function refreshDashboard() {
google.script.run
.withSuccessHandler(function(dashboardData){
populateBudgetTable(dashboardData);
})
.getDashboardData();
}
function populateBudgetTable(data) {
const tbody = document.getElementById('budgetTableBody');
tbody.innerHTML = '';
data.forEach(function(item){
const tr = document.createElement('tr');
if (item.warningLevel === 'over') {
tr.className = 'warning-over';
} else if (item.warningLevel === 'near') {
tr.className = 'warning-near';
} else {
tr.className = 'warning-normal';
}
const tdCat = document.createElement('td');
tdCat.textContent = item.category;
const tdBudget = document.createElement('td');
tdBudget.textContent = item.budget || 0;
const tdSpent = document.createElement('td');
tdSpent.textContent = item.spent;
tr.appendChild(tdCat);
tr.appendChild(tdBudget);
tr.appendChild(tdSpent);
tbody.appendChild(tr);
});
}
/********************************************
* 新增:查看「該月支出」按鈕的功能
********************************************/
let monthlyExpenses = []; // 用於儲存本月支出資料
let filteredExpenses = []; // 用於儲存過濾後的支出資料
// 1) 取得本月支出 -> 顯示浮層
function viewMonthlyExpenses() {
google.script.run
.withSuccessHandler(function(expenses){
if (Array.isArray(expenses)) {
monthlyExpenses = expenses;
filteredExpenses = expenses; // 初始化為全部
renderMonthlyExpenses();
document.getElementById('monthlyExpensesModal').style.display = 'flex';
} else {
alert('取得的支出資料格式錯誤。');
}
})
.withFailureHandler(function(err){
alert('取得本月支出失敗:' + err.message);
})
.getThisMonthExpenses();
}
// 2) 渲染本月支出的表格
function renderMonthlyExpenses() {
const tbody = document.getElementById('expenseTableBody');
tbody.innerHTML = '';
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 4;
td.textContent = '本月尚無符合條件的支出紀錄。';
tr.appendChild(td);
tbody.appendChild(tr);
document.getElementById('totalAmount').textContent = '總計:0';
return;
}
// 計算總計
let total = 0;
filteredExpenses.forEach(function(item){
total += parseFloat(item.amount) || 0;
const tr = document.createElement('tr');
const tdDateTime = document.createElement('td');
tdDateTime.textContent = item.dateTime; // 格式化後的日期時間
const tdCategory = document.createElement('td');
tdCategory.textContent = item.category;
const tdAmount = document.createElement('td');
tdAmount.textContent = item.amount;
const tdNote = document.createElement('td');
tdNote.textContent = item.note;
tr.appendChild(tdDateTime);
tr.appendChild(tdCategory);
tr.appendChild(tdAmount);
tr.appendChild(tdNote);
tbody.appendChild(tr);
});
// 顯示總計
document.getElementById('totalAmount').textContent = '總計:' + total;
}
// 3) 按類別排序 (升冪) 後重新渲染
function sortExpensesByCategory() {
if (!Array.isArray(filteredExpenses)) return;
filteredExpenses.sort(function(a, b){
return a.category.localeCompare(b.category, 'zh-TW');
});
renderMonthlyExpenses();
}
// 4) 關閉浮層
function closeMonthlyExpensesModal() {
document.getElementById('monthlyExpensesModal').style.display = 'none';
}
// 5) 套用篩選
function applyFilters() {
const keyword = document.getElementById('searchKeyword').value.trim().toLowerCase();
const startDate = document.getElementById('filterStartDate').value;
const endDate = document.getElementById('filterEndDate').value;
filteredExpenses = monthlyExpenses.filter(function(expense){
let matchesKeyword = true;
let matchesStartDate = true;
let matchesEndDate = true;
// 檢查關鍵字(類別或備註)
if (keyword) {
const categoryMatch = expense.category.toLowerCase().includes(keyword);
const noteMatch = expense.note.toLowerCase().includes(keyword);
matchesKeyword = categoryMatch || noteMatch;
}
// 檢查開始日期
if (startDate) {
const expenseDate = new Date(expense.dateTime);
const start = new Date(startDate);
matchesStartDate = expenseDate >= start;
}
// 檢查結束日期
if (endDate) {
const expenseDate = new Date(expense.dateTime);
const end = new Date(endDate);
// 將結束日期設為當天結束
end.setHours(23,59,59,999);
matchesEndDate = expenseDate <= end;
}
return matchesKeyword && matchesStartDate && matchesEndDate;
});
renderMonthlyExpenses();
}
// 6) 重置篩選
function resetFilters() {
document.getElementById('searchKeyword').value = '';
document.getElementById('filterStartDate').value = '';
document.getElementById('filterEndDate').value = '';
filteredExpenses = monthlyExpenses;
renderMonthlyExpenses();
}
// 7) 匯出為 CSV
function exportToCSV() {
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
alert('沒有可匯出的資料。');
return;
}
// 構建 CSV 資料
let csvContent = "data:text/csv;charset=utf-8,";
csvContent += "日期時間,類別,金額,備註\n"; // 標題
filteredExpenses.forEach(function(item){
const row = `"${item.dateTime}","${item.category}","${item.amount}","${item.note}"`;
csvContent += row + "\n";
});
// 觸發下載
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
const now = new Date();
const timestamp = now.getFullYear() + "-" +
String(now.getMonth()+1).padStart(2, '0') + "-" +
String(now.getDate()).padStart(2, '0') + "_" +
String(now.getHours()).padStart(2, '0') + "-" +
String(now.getMinutes()).padStart(2, '0') + "-" +
String(now.getSeconds()).padStart(2, '0');
link.setAttribute("download", `本月支出_${timestamp}.csv`);
document.body.appendChild(link); // Required for FF
link.click(); // This will download the data file named "本月支出_timestamp.csv"
document.body.removeChild(link);
}
</script>
</body>
</html>
在浮層中加入了搜尋和篩選的輸入欄位:
applyFilters() 函式:
filter 方法,根據關鍵字和日期範圍過濾 monthlyExpenses 陣列,將結果存入 filteredExpenses 陣列。renderMonthlyExpenses() 重新渲染表格,顯示過濾後的結果。resetFilters() 函式:
filteredExpenses 重置為全部的 monthlyExpenses 資料。renderMonthlyExpenses() 重新渲染表格,顯示全部支出紀錄。在浮層中加入了一個「匯出為 CSV」的按鈕,讓使用者可以下載當前顯示的支出表格資料。
exportToCSV() 函式:filteredExpenses 陣列中有資料)。encodeURI 將 CSV 資料編碼為 URI 格式。<a> 元素,設定其 href 為編碼後的 CSV 資料,並設定 download 屬性為欲下載的檔名。<a> 元素,保持 DOM 清潔。viewMonthlyExpenses() 函式中,確保接收到的資料是有效的陣列,並在 renderMonthlyExpenses() 中處理無資料的情況,避免 JavaScript 錯誤。更新 Code.gs:
Code.gs 中已將「房租」類別改為「網購」,並且 setupSpreadsheet 函式中反映了這一變更。getThisMonthExpenses 函式能正確回傳所有本月支出紀錄。更新 Index.html:
Index.html 替換為上述提供的完整 HTML 內容。重新部署應用程式:
測試功能:
Transactions 工作表中。修正與優化:
搜尋/篩選功能無法正常運作:
monthlyExpenses 陣列。applyFilters 或 renderMonthlyExpenses 函式中。monthlyExpenses 和 filteredExpenses 陣列中的每個物件包含 dateTime、category、amount 和 note 屬性。匯出功能無法下載 CSV 檔案:
exportToCSV 函式被正確呼叫:exportToCSV 函式。filteredExpenses 陣列中的資料正確,並且所有欄位都被正確地轉換為 CSV 格式。浮層無法正確顯示或關閉:
.modal-overlay 的 display 屬性在開啟和關閉時被正確設置。viewMonthlyExpenses 和 closeMonthlyExpensesModal 函式被正確呼叫,並且修改了 .modal-overlay 的 display 屬性。類別選單未正確更新:
filterCategoryOptions 函式被正確呼叫:filterCategoryOptions 函式被觸發,並且重新載入「類別」選單。Categories 工作表中的資料:新增交易後,預算表未更新:
submitForm 函式中 refreshDashboard 被正確呼叫:refreshDashboard 函式被呼叫以更新預算表。getDashboardData 函式:getDashboardData 函式能正確計算每個類別的預算和支出,並正確設定 warningLevel。日期時間顯示錯誤:
datetime-local 欄位正確選擇日期與時間。getThisMonthExpenses 函式的日期格式化:Utilities.formatDate 將日期格式化為 yyyy/MM/dd HH:mm,以便前端能正確顯示。透過以上的修改,你的記帳工具將具備以下新增功能:
搜尋/篩選功能:
匯出功能:
表單佈局與介面優化:
防錯處理與使用者體驗提升:
請按照上述步驟進行更新與部署,並在使用過程中進行測試以確保所有功能正常運作。如有任何進一步的問題或需求,歡迎隨時提出!
本月支出列表的日期篩選,預設列出該月的1日到最後一日,並讓使用者可以手動選擇日期範圍來查看支出
思考 花的時間為 8 秒
了解,我將協助你在「查看該月支出」的浮層中增加以下功能:
預設日期範圍:
搜尋/篩選功能:
匯出功能:
以下是完整的 Index.html,已根據你的需求進行調整:
Index.html<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
/* 手機友善基礎樣式 */
body {
font-family: sans-serif;
margin: 10px;
}
h1 {
text-align: center;
margin-bottom: 0;
}
.sub-title {
text-align: center;
color: #666;
margin-top: 4px;
margin-bottom: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.input-row {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
.input-row .input-field {
flex: 0 0 48%;
}
label {
display: block;
font-weight: bold;
margin-bottom: 4px;
}
input, select {
width: 100%;
box-sizing: border-box;
padding: 8px;
font-size: 16px;
}
button {
width: 100%;
padding: 10px;
font-size: 16px;
background: #4CAF50;
color: #fff;
border: none;
cursor: pointer;
margin-top: 10px;
border-radius: 4px;
}
button:hover {
background: #45a049;
}
/* 預算表 */
.budget-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.budget-table th, .budget-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.warning-normal {
background-color: #ffffff;
}
.warning-near {
background-color: #fff8db; /* 淡黃色 */
}
.warning-over {
background-color: #ffdcdc; /* 淡紅色 */
}
.message {
margin-top: 8px;
text-align: center;
}
/* 浮層 (Modal) 樣式 */
.modal-overlay {
position: fixed;
z-index: 999; /* 確保浮層在最上層 */
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景 */
display: none; /* 初始隱藏 */
justify-content: center;
align-items: center;
}
.modal-content {
background: #fff;
padding: 20px;
border-radius: 8px;
width: 95%;
max-width: 800px;
max-height: 90vh;
overflow-y: auto;
}
/* 調整後的模態框標題 */
.modal-header {
display: flex;
width: 100%;
margin-bottom: 10px;
}
.modal-header h2 {
flex: 1;
text-align: left;
margin: 0;
font-size: 20px;
}
.modal-header .close-btn {
flex: 1;
text-align: right;
background: #ccc;
color: #000;
border: none;
font-size: 20px;
padding: 5px 10px;
cursor: pointer;
border-radius: 4px;
}
.modal-header .close-btn:hover {
background: #aaa;
}
.expense-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
}
.expense-table th, .expense-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.sort-btn, .filter-btn, .export-btn {
background: #2196F3;
color: #fff;
padding: 8px 12px;
border: none;
cursor: pointer;
border-radius: 4px;
margin-right: 10px;
margin-bottom: 10px;
}
.sort-btn:hover, .filter-btn:hover, .export-btn:hover {
background: #0b7dda;
}
/* 滾動條樣式(選用) */
.modal-content::-webkit-scrollbar {
width: 8px;
}
.modal-content::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.2);
border-radius: 4px;
}
</style>
</head>
<body>
<h1>簡易記帳工具</h1>
<div class="sub-title">Google Apps Script + 試算表</div>
<div class="container">
<!-- 新增交易表單 -->
<div class="input-row">
<!-- 類型 -->
<div class="input-field">
<label for="type">類型</label>
<select id="type" onchange="filterCategoryOptions()">
<option value="支出" selected>支出</option>
<option value="收入">收入</option>
</select>
</div>
<!-- 類別 -->
<div class="input-field">
<label for="category">類別</label>
<select id="category"></select>
</div>
</div>
<div class="input-row">
<!-- 金額 -->
<div class="input-field">
<label for="amount">金額</label>
<input type="number" id="amount" step="0.01" />
</div>
<!-- 備註 -->
<div class="input-field">
<label for="note">備註</label>
<input type="text" id="note" />
</div>
</div>
<!-- 日期與時間 -->
<div class="input-field">
<label for="dateTime">日期與時間</label>
<input type="datetime-local" id="dateTime" />
</div>
<button onclick="submitForm()">新增交易</button>
<div class="message" id="msgBox"></div>
<!-- 查看該月支出按鈕 -->
<button style="background:#f44336; margin-top:20px;" onclick="viewMonthlyExpenses()">
查看該月支出
</button>
<!-- 預算表 -->
<table class="budget-table" id="budgetTable">
<thead>
<tr>
<th>類別</th>
<th>預算</th>
<th>本月已用</th>
</tr>
</thead>
<tbody id="budgetTableBody"></tbody>
</table>
</div>
<!-- 浮層 (Modal) 內容 -->
<div class="modal-overlay" id="monthlyExpensesModal">
<div class="modal-content">
<div class="modal-header">
<h2>本月支出列表</h2>
<button class="close-btn" onclick="closeMonthlyExpensesModal()">X</button>
</div>
<!-- 搜尋/篩選區域 -->
<div style="margin-bottom: 15px;">
<input type="text" id="searchKeyword" placeholder="搜尋關鍵字(類別或備註)" style="width: 48%; padding: 8px; margin-right: 4%;">
<input type="date" id="filterStartDate" placeholder="開始日期" style="width: 48%; padding: 8px;">
<input type="date" id="filterEndDate" placeholder="結束日期" style="width: 48%; padding: 8px; margin-top: 10px;">
<button class="filter-btn" onclick="applyFilters()" style="width: 48%; margin-top: 10px;">套用篩選</button>
<button class="filter-btn" onclick="resetFilters()" style="width: 48%; margin-top: 10px; background:#555;">重置篩選</button>
</div>
<!-- 排序及匯出按鈕 -->
<div style="margin-bottom: 15px;">
<button class="sort-btn" onclick="sortExpensesByCategory()">
按「類別」排序
</button>
<button class="export-btn" onclick="exportToCSV()">
匯出為 CSV
</button>
</div>
<!-- 支出表格 -->
<table class="expense-table" id="expenseTable">
<thead>
<tr>
<th>日期時間</th>
<th>類別</th>
<th>金額</th>
<th>備註</th>
</tr>
</thead>
<tbody id="expenseTableBody"></tbody>
</table>
<!-- 總計 -->
<div id="totalAmount" style="text-align:right; font-weight:bold;"></div>
</div>
</div>
<script>
/********************************************
* 前端初始化:載入「分類清單」與「預算表」
********************************************/
document.addEventListener('DOMContentLoaded', function() {
setDefaultDateTime();
// 載入分類
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
// 載入預算
refreshDashboard();
});
// 設定「現在時間」到 datetime-local
function setDefaultDateTime() {
const now = new Date();
const localISO = new Date(now.getTime() - now.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16); // 'YYYY-MM-DDTHH:MM'
document.getElementById('dateTime').value = localISO;
}
// 填充分類下拉
function populateCategorySelect(categories) {
const categorySelect = document.getElementById('category');
categorySelect.innerHTML = '';
// 根據選擇的類型篩選類別
const selectedType = document.getElementById('type').value;
categories.forEach(function(cat) {
if (cat.type === selectedType) {
const option = document.createElement('option');
option.value = cat.name;
option.text = cat.name;
categorySelect.appendChild(option);
}
});
}
// 根據「類型」篩選「類別」選項
function filterCategoryOptions() {
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
}
// 新增交易
function submitForm() {
const dateTime = document.getElementById('dateTime').value;
const type = document.getElementById('type').value;
const category = document.getElementById('category').value;
const amount = document.getElementById('amount').value;
const note = document.getElementById('note').value;
// 簡易驗證
if (!dateTime || !type || !category || !amount) {
showMessage('請填寫完整資訊', true);
return;
}
const transaction = {
dateTime: dateTime,
type: type,
category: category,
amount: amount,
note: note
};
google.script.run
.withSuccessHandler(function(msg) {
showMessage(msg, false);
setDefaultDateTime();
document.getElementById('amount').value = '';
document.getElementById('note').value = '';
// 刷新預算
refreshDashboard();
})
.withFailureHandler(function(err) {
showMessage('寫入失敗: ' + err.message, true);
})
.saveTransaction(transaction);
}
// 顯示訊息
function showMessage(msg, isError) {
const msgBox = document.getElementById('msgBox');
msgBox.style.color = isError ? 'red' : 'green';
msgBox.textContent = msg;
}
// 刷新預算表
function refreshDashboard() {
google.script.run
.withSuccessHandler(function(dashboardData){
populateBudgetTable(dashboardData);
})
.getDashboardData();
}
function populateBudgetTable(data) {
const tbody = document.getElementById('budgetTableBody');
tbody.innerHTML = '';
data.forEach(function(item){
const tr = document.createElement('tr');
if (item.warningLevel === 'over') {
tr.className = 'warning-over';
} else if (item.warningLevel === 'near') {
tr.className = 'warning-near';
} else {
tr.className = 'warning-normal';
}
const tdCat = document.createElement('td');
tdCat.textContent = item.category;
const tdBudget = document.createElement('td');
tdBudget.textContent = item.budget || 0;
const tdSpent = document.createElement('td');
tdSpent.textContent = item.spent;
tr.appendChild(tdCat);
tr.appendChild(tdBudget);
tr.appendChild(tdSpent);
tbody.appendChild(tr);
});
}
/********************************************
* 新增:查看「該月支出」按鈕的功能
********************************************/
let monthlyExpenses = []; // 用於儲存本月支出資料
let filteredExpenses = []; // 用於儲存過濾後的支出資料
// 1) 取得本月支出 -> 顯示浮層
function viewMonthlyExpenses() {
google.script.run
.withSuccessHandler(function(expenses){
if (Array.isArray(expenses)) {
monthlyExpenses = expenses;
filteredExpenses = expenses; // 初始化為全部
setDefaultFilterDates(); // 設定預設日期範圍
applyFilters(); // 套用初始篩選
renderMonthlyExpenses();
document.getElementById('monthlyExpensesModal').style.display = 'flex';
} else {
alert('取得的支出資料格式錯誤。');
}
})
.withFailureHandler(function(err){
alert('取得本月支出失敗:' + err.message);
})
.getThisMonthExpenses();
}
// 2) 設定預設日期範圍為當月的1日到最後一日
function setDefaultFilterDates() {
const now = new Date();
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0);
const startDateInput = document.getElementById('filterStartDate');
const endDateInput = document.getElementById('filterEndDate');
startDateInput.value = firstDay.toISOString().slice(0, 10); // 'YYYY-MM-DD'
endDateInput.value = lastDay.toISOString().slice(0, 10); // 'YYYY-MM-DD'
}
// 3) 渲染本月支出的表格
function renderMonthlyExpenses() {
const tbody = document.getElementById('expenseTableBody');
tbody.innerHTML = '';
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 4;
td.textContent = '本月尚無符合條件的支出紀錄。';
tr.appendChild(td);
tbody.appendChild(tr);
document.getElementById('totalAmount').textContent = '總計:0';
return;
}
// 計算總計
let total = 0;
filteredExpenses.forEach(function(item){
total += parseFloat(item.amount) || 0;
const tr = document.createElement('tr');
const tdDateTime = document.createElement('td');
tdDateTime.textContent = item.dateTime; // 格式化後的日期時間
const tdCategory = document.createElement('td');
tdCategory.textContent = item.category;
const tdAmount = document.createElement('td');
tdAmount.textContent = item.amount;
const tdNote = document.createElement('td');
tdNote.textContent = item.note;
tr.appendChild(tdDateTime);
tr.appendChild(tdCategory);
tr.appendChild(tdAmount);
tr.appendChild(tdNote);
tbody.appendChild(tr);
});
// 顯示總計
document.getElementById('totalAmount').textContent = '總計:' + total;
}
// 4) 按類別排序 (升冪) 後重新渲染
function sortExpensesByCategory() {
if (!Array.isArray(filteredExpenses)) return;
filteredExpenses.sort(function(a, b){
return a.category.localeCompare(b.category, 'zh-TW');
});
renderMonthlyExpenses();
}
// 5) 關閉浮層
function closeMonthlyExpensesModal() {
document.getElementById('monthlyExpensesModal').style.display = 'none';
}
// 6) 套用篩選
function applyFilters() {
const keyword = document.getElementById('searchKeyword').value.trim().toLowerCase();
const startDate = document.getElementById('filterStartDate').value;
const endDate = document.getElementById('filterEndDate').value;
filteredExpenses = monthlyExpenses.filter(function(expense){
let matchesKeyword = true;
let matchesStartDate = true;
let matchesEndDate = true;
// 檢查關鍵字(類別或備註)
if (keyword) {
const categoryMatch = expense.category.toLowerCase().includes(keyword);
const noteMatch = expense.note.toLowerCase().includes(keyword);
matchesKeyword = categoryMatch || noteMatch;
}
// 檢查開始日期
if (startDate) {
const expenseDate = new Date(expense.dateTime);
const start = new Date(startDate);
matchesStartDate = expenseDate >= start;
}
// 檢查結束日期
if (endDate) {
const expenseDate = new Date(expense.dateTime);
const end = new Date(endDate);
// 將結束日期設為當天結束
end.setHours(23,59,59,999);
matchesEndDate = expenseDate <= end;
}
return matchesKeyword && matchesStartDate && matchesEndDate;
});
renderMonthlyExpenses();
}
// 7) 重置篩選
function resetFilters() {
document.getElementById('searchKeyword').value = '';
document.getElementById('filterStartDate').value = '';
document.getElementById('filterEndDate').value = '';
filteredExpenses = monthlyExpenses;
renderMonthlyExpenses();
}
// 8) 匯出為 CSV
function exportToCSV() {
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
alert('沒有可匯出的資料。');
return;
}
// 構建 CSV 資料
let csvContent = "data:text/csv;charset=utf-8,";
csvContent += "日期時間,類別,金額,備註\n"; // 標題
filteredExpenses.forEach(function(item){
const row = `"${item.dateTime}","${item.category}","${item.amount}","${item.note}"`;
csvContent += row + "\n";
});
// 觸發下載
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
const now = new Date();
const timestamp = now.getFullYear() + "-" +
String(now.getMonth()+1).padStart(2, '0') + "-" +
String(now.getDate()).padStart(2, '0') + "_" +
String(now.getHours()).padStart(2, '0') + "-" +
String(now.getMinutes()).padStart(2, '0') + "-" +
String(now.getSeconds()).padStart(2, '0');
link.setAttribute("download", `本月支出_${timestamp}.csv`);
document.body.appendChild(link); // Required for FF
link.click(); // This will download the data file named "本月支出_timestamp.csv"
document.body.removeChild(link);
}
</script>
</body>
</html>
函式 setDefaultFilterDates():
Date 物件計算當月的第一天和最後一天,並設置到相應的日期輸入框中。修改 viewMonthlyExpenses() 函式:
setDefaultFilterDates() 來設置預設日期範圍。applyFilters() 來根據這些預設日期範圍進行初始篩選,確保顯示的是當月的全部支出紀錄。搜尋框與日期範圍輸入:
searchKeyword:用於輸入關鍵字(例如,類別或備註中的文字)。filterStartDate:選擇篩選的起始日期。filterEndDate:選擇篩選的結束日期。套用篩選與重置篩選按鈕:
套用篩選 按鈕會呼叫 applyFilters() 函式,根據輸入的關鍵字和日期範圍進行過濾。重置篩選 按鈕會呼叫 resetFilters() 函式,清空所有篩選條件並顯示全部支出紀錄。函式 applyFilters():
filter 方法,根據這些條件過濾 monthlyExpenses 陣列,將符合條件的項目存入 filteredExpenses 陣列。renderMonthlyExpenses() 重新渲染表格,顯示過濾後的支出紀錄。函式 resetFilters():
filteredExpenses 重置為 monthlyExpenses,即顯示全部支出紀錄。renderMonthlyExpenses() 重新渲染表格。匯出為 CSV 按鈕:
exportToCSV() 函式。函式 exportToCSV():
filteredExpenses 陣列中是否有資料可匯出。encodeURI 將 CSV 資料編碼為 URI 格式。<a> 元素,設置其 href 為編碼後的 URI,並設定 download 屬性為欲下載的檔名(包含時間戳記以避免覆蓋)。<a> 元素,保持 DOM 清潔。調整浮層標題與關閉按鈕的佈局:
改進使用者體驗:
更新 Code.gs:
Code.gs 中已經包含前述的所有功能,特別是 getThisMonthExpenses 函式能夠正確回傳當月的支出紀錄。Code.gs,確認是否需要進一步調整以配合新的前端功能。更新 Index.html:
Index.html 替換為上述提供的完整 HTML 內容。重新部署應用程式:
測試功能:
Transactions 工作表中。修正與優化:
預設日期範圍未正確設定:
setDefaultFilterDates() 函式是否在 viewMonthlyExpenses() 中被正確呼叫。Date 物件的相關方法。搜尋/篩選功能無法正常運作:
monthlyExpenses 陣列。applyFilters 或 renderMonthlyExpenses 函式中。monthlyExpenses 和 filteredExpenses 陣列中的每個物件包含 dateTime、category、amount 和 note 屬性。匯出功能無法下載 CSV 檔案:
exportToCSV 函式被正確呼叫:exportToCSV 函式。filteredExpenses 陣列中的資料正確,並且所有欄位都被正確地轉換為 CSV 格式。浮層無法正確顯示或關閉:
.modal-overlay 的 display 屬性在開啟和關閉時被正確設置。viewMonthlyExpenses 和 closeMonthlyExpensesModal 函式被正確呼叫,並且修改了 .modal-overlay 的 display 屬性。類別選單未正確更新:
filterCategoryOptions 函式被正確呼叫:filterCategoryOptions 函式被觸發,並且重新載入「類別」選單。Categories 工作表中的資料:新增交易後,預算表未更新:
submitForm 函式中 refreshDashboard 被正確呼叫:refreshDashboard 函式被呼叫以更新預算表。getDashboardData 函式:getDashboardData 函式能正確計算每個類別的預算和支出,並正確設定 warningLevel。日期時間顯示錯誤:
datetime-local 欄位正確選擇日期與時間。getThisMonthExpenses 函式的日期格式化:Utilities.formatDate 將日期格式化為 yyyy/MM/dd HH:mm,以便前端能正確顯示。透過以上的修改,你的記帳工具將具備以下新增功能:
預設日期範圍設定:
搜尋/篩選功能:
匯出功能:
表單佈局與介面優化:
防錯處理與使用者體驗提升:
請按照上述步驟進行更新與部署,並在使用過程中進行測試以確保所有功能正常運作。如有任何進一步的問題或需求,歡迎隨時提出!
查看該月支出改為查看月出支,圖層中改為預設呈現該月支出,並提供切按月份的功能,以查看不同月份支出
思考 花的時間為 11 秒
了解,我將協助你進一步優化「查看月出支」的功能,具體包括:
更改按鈕標題:
預設日期範圍:
月份切換功能:
搜尋/篩選功能:
匯出功能:
為了實現上述功能,我們需要對後端的 Code.gs 以及前端的 Index.html 進行相應的修改。
Code.gs主要修改點:
getExpenses 函式:允許根據指定的月份和年份來取得支出紀錄。getThisMonthExpenses 函式:讓其呼叫 getExpenses 函式並傳遞當前月份和年份。以下是更新後的完整 Code.gs:
/************************************************
* 全域設定
************************************************/
const SHEET_TRANSACTIONS = 'Transactions'; // 交易紀錄
const SHEET_BUDGETS = 'Budgets'; // 預算設定
const SHEET_CATEGORIES = 'Categories'; // 分類清單
/************************************************
* 網頁入口
************************************************/
function doGet() {
// 先確保必需的工作表存在,沒有就自動建立
setupSpreadsheet();
// 回傳網頁
return HtmlService
.createTemplateFromFile('Index')
.evaluate()
.addMetaTag('viewport', 'width=device-width, initial-scale=1.0')
.setTitle('簡易記帳工具');
}
/************************************************
* 一次載入前端 HTML
************************************************/
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
/************************************************
* 0. 自動建立所需的工作表 (若尚未存在)
************************************************/
function setupSpreadsheet() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
// 1) 建立 Transactions (交易紀錄)
let sheetTx = ss.getSheetByName(SHEET_TRANSACTIONS);
if (!sheetTx) {
sheetTx = ss.insertSheet(SHEET_TRANSACTIONS);
// 設定欄位標題
sheetTx.appendRow(['日期時間', '類型(收入/支出)', '類別', '金額', '備註']);
}
// 2) 建立 Budgets (預算設定)
let sheetBd = ss.getSheetByName(SHEET_BUDGETS);
if (!sheetBd) {
sheetBd = ss.insertSheet(SHEET_BUDGETS);
// 設定欄位標題
sheetBd.appendRow(['類別', '預算金額']);
// 預設預算設定 (可根據需求修改或新增)
const defaultBudgets = [
['飲食', 5000],
['交通', 2000],
['娛樂', 3000],
['網購', 4000], // 將「房租」改為「網購」
['薪資', 0] // 收入類別預算可設為0或不設定
];
sheetBd.getRange(2, 1, defaultBudgets.length, 2).setValues(defaultBudgets);
}
// 3) 建立 Categories (分類清單)
let sheetCat = ss.getSheetByName(SHEET_CATEGORIES);
if (!sheetCat) {
sheetCat = ss.insertSheet(SHEET_CATEGORIES);
// 設定欄位標題
sheetCat.appendRow(['類別名稱', '類型(收入/支出)']);
// 常用的記帳類別 (預設)
const defaultCategories = [
['飲食', '支出'],
['交通', '支出'],
['娛樂', '支出'],
['網購', '支出'], // 將「房租」改為「網購」
['薪資', '收入'],
['其他收入', '收入'],
['其他支出', '支出']
];
sheetCat.getRange(2, 1, defaultCategories.length, 2).setValues(defaultCategories);
}
}
/************************************************
* 1. 取得分類清單
************************************************/
function getCategories() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_CATEGORIES);
const data = sheet.getDataRange().getValues(); // 全部資料
// 假設第一列是標題,所以從第二列開始
const categories = [];
for (let i = 1; i < data.length; i++) {
const row = data[i];
const name = row[0];
const type = row[1];
if (name && type) {
categories.push({
name: name,
type: type
});
}
}
return categories;
}
/************************************************
* 2. 取得預算設定
************************************************/
function getBudgets() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_BUDGETS);
const data = sheet.getDataRange().getValues();
// A欄: 類別, B欄: 預算金額
const budgets = {};
for (let i = 1; i < data.length; i++) {
const row = data[i];
const category = row[0];
const budget = parseFloat(row[1]) || 0;
if (category) {
budgets[category] = budget;
}
}
return budgets;
}
/************************************************
* 3. 寫入交易紀錄
************************************************/
function saveTransaction(transaction) {
// transaction = { dateTime, type, category, amount, note }
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_TRANSACTIONS);
// 檢查必要欄位
if (!transaction.dateTime || !transaction.type || !transaction.category || !transaction.amount) {
throw new Error('缺少必要的交易資訊');
}
// 檢查金額是否為數字
const amount = parseFloat(transaction.amount);
if (isNaN(amount)) {
throw new Error('金額必須為數字');
}
// 寫入 (日期時間, 類型, 類別, 金額, 備註)
sheet.appendRow([
transaction.dateTime,
transaction.type,
transaction.category,
amount,
transaction.note || ''
]);
return '交易已儲存!';
}
/************************************************
* 4. 取得本月支出總額 (依類別)
************************************************/
function getMonthlySpending() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_TRANSACTIONS);
const data = sheet.getDataRange().getValues();
// 取得當前年月,用於判斷「本月」
const now = new Date();
const currentMonth = now.getMonth();
const currentYear = now.getFullYear();
// 結構:
// A: 日期時間
// B: 類型
// C: 類別
// D: 金額
// E: 備註
const spendingByCategory = {};
// 從第二列開始 (第一列是標題)
for (let i = 1; i < data.length; i++) {
const row = data[i];
const dateTime = new Date(row[0]);
const type = row[1];
const category = row[2];
const amount = parseFloat(row[3]) || 0;
// 僅計算「支出」+「本月」
if (type === '支出' &&
dateTime.getMonth() === currentMonth &&
dateTime.getFullYear() === currentYear) {
if (!spendingByCategory[category]) {
spendingByCategory[category] = 0;
}
spendingByCategory[category] += amount;
}
}
return spendingByCategory;
}
/************************************************
* 5. 彙整預算 & 支出,回傳給前端
************************************************/
function getDashboardData() {
const budgets = getBudgets();
const categories = getCategories();
const spendingByCategory = getMonthlySpending();
const dashboardData = [];
categories.forEach(function(cat) {
if (cat.type === '支出') {
const categoryName = cat.name;
const budget = budgets[categoryName] || 0;
const spent = spendingByCategory[categoryName] || 0;
// 超支 / 接近超支 (可自訂閾值)
let warningLevel = 'normal';
if (budget > 0) {
if (spent >= budget) {
warningLevel = 'over'; // 超支
} else if (spent >= 0.8 * budget) {
warningLevel = 'near'; // 接近超支
}
}
dashboardData.push({
category: categoryName,
budget: budget,
spent: spent,
warningLevel: warningLevel
});
}
});
return dashboardData;
}
/************************************************
* 6. 取得指定月份的所有支出明細
************************************************/
/**
* 回傳指定月份的所有支出明細
* @param {number} month - 月份 (0-11)
* @param {number} year - 年份 (e.g., 2023)
* @return {Array} expenses - 支出紀錄陣列,每筆包含:
* { dateTime, type, category, amount, note }
*/
function getExpenses(month, year) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_TRANSACTIONS);
const data = sheet.getDataRange().getValues();
// 如果沒有提供 month 和 year,預設為當前月
if (typeof month === 'undefined' || typeof year === 'undefined') {
const now = new Date();
month = now.getMonth(); // 0 ~ 11
year = now.getFullYear();
}
// A: 日期時間, B: 類型, C: 類別, D: 金額, E: 備註
const expenses = [];
for(let i=1; i < data.length; i++){
const row = data[i];
const dateTimeStr = row[0];
const dateTime = new Date(dateTimeStr);
const type = row[1];
const category = row[2];
const amount = parseFloat(row[3]) || 0;
const note = row[4] || '';
// 篩選「指定年月」且「類型為支出」的交易
if(type === '支出' &&
dateTime.getFullYear() === year &&
dateTime.getMonth() === month) {
expenses.push({
dateTime: Utilities.formatDate(dateTime, ss.getSpreadsheetTimeZone(), "yyyy/MM/dd HH:mm"),
type: type,
category: category,
amount: amount,
note: note
});
}
}
return expenses;
}
新增 getExpenses 函式:
month(月份,0-11)和 year(年份,例如 2023)。Transactions 工作表中的所有紀錄,並回傳符合指定月份和類型為「支出」的支出紀錄。調整 getThisMonthExpenses 函式:
getThisMonthExpenses 會呼叫 getExpenses 並傳遞當前月份和年份,以取得當前月的支出紀錄。getExpenses 函式能夠被其他功能調用以取得不同月份的支出紀錄。/**
* 回傳「本月支出」的所有交易明細
* @return {Array} expenses 本月支出的交易陣列,每筆包含:
* { dateTime, type, category, amount, note }
*/
function getThisMonthExpenses() {
const now = new Date();
const month = now.getMonth(); // 0 ~ 11
const year = now.getFullYear();
return getExpenses(month, year);
}
Index.html主要修改點:
更改按鈕標題:
新增月份選擇功能:
調整預設日期範圍:
調整 JavaScript 功能:
viewMonthlyExpenses 函式,根據選擇的月份和年份來取得支出紀錄。applyFilters 函式以考慮選擇的月份和年份。以下是更新後的完整 Index.html:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
/* 手機友善基礎樣式 */
body {
font-family: sans-serif;
margin: 10px;
}
h1 {
text-align: center;
margin-bottom: 0;
}
.sub-title {
text-align: center;
color: #666;
margin-top: 4px;
margin-bottom: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.input-row {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
.input-row .input-field {
flex: 0 0 48%;
}
label {
display: block;
font-weight: bold;
margin-bottom: 4px;
}
input, select {
width: 100%;
box-sizing: border-box;
padding: 8px;
font-size: 16px;
}
button {
width: 100%;
padding: 10px;
font-size: 16px;
background: #4CAF50;
color: #fff;
border: none;
cursor: pointer;
margin-top: 10px;
border-radius: 4px;
}
button:hover {
background: #45a049;
}
/* 預算表 */
.budget-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.budget-table th, .budget-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.warning-normal {
background-color: #ffffff;
}
.warning-near {
background-color: #fff8db; /* 淡黃色 */
}
.warning-over {
background-color: #ffdcdc; /* 淡紅色 */
}
.message {
margin-top: 8px;
text-align: center;
}
/* 浮層 (Modal) 樣式 */
.modal-overlay {
position: fixed;
z-index: 999; /* 確保浮層在最上層 */
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景 */
display: none; /* 初始隱藏 */
justify-content: center;
align-items: center;
}
.modal-content {
background: #fff;
padding: 20px;
border-radius: 8px;
width: 95%;
max-width: 800px;
max-height: 90vh;
overflow-y: auto;
}
/* 調整後的模態框標題 */
.modal-header {
display: flex;
width: 100%;
margin-bottom: 10px;
}
.modal-header h2 {
flex: 1;
text-align: left;
margin: 0;
font-size: 20px;
}
.modal-header .close-btn {
flex: 1;
text-align: right;
background: #ccc;
color: #000;
border: none;
font-size: 20px;
padding: 5px 10px;
cursor: pointer;
border-radius: 4px;
}
.modal-header .close-btn:hover {
background: #aaa;
}
.expense-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
}
.expense-table th, .expense-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.sort-btn, .filter-btn, .export-btn {
background: #2196F3;
color: #fff;
padding: 8px 12px;
border: none;
cursor: pointer;
border-radius: 4px;
margin-right: 10px;
margin-bottom: 10px;
}
.sort-btn:hover, .filter-btn:hover, .export-btn:hover {
background: #0b7dda;
}
/* 滾動條樣式(選用) */
.modal-content::-webkit-scrollbar {
width: 8px;
}
.modal-content::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.2);
border-radius: 4px;
}
</style>
</head>
<body>
<h1>簡易記帳工具</h1>
<div class="sub-title">Google Apps Script + 試算表</div>
<div class="container">
<!-- 新增交易表單 -->
<div class="input-row">
<!-- 類型 -->
<div class="input-field">
<label for="type">類型</label>
<select id="type" onchange="filterCategoryOptions()">
<option value="支出" selected>支出</option>
<option value="收入">收入</option>
</select>
</div>
<!-- 類別 -->
<div class="input-field">
<label for="category">類別</label>
<select id="category"></select>
</div>
</div>
<div class="input-row">
<!-- 金額 -->
<div class="input-field">
<label for="amount">金額</label>
<input type="number" id="amount" step="0.01" />
</div>
<!-- 備註 -->
<div class="input-field">
<label for="note">備註</label>
<input type="text" id="note" />
</div>
</div>
<!-- 日期與時間 -->
<div class="input-field">
<label for="dateTime">日期與時間</label>
<input type="datetime-local" id="dateTime" />
</div>
<button onclick="submitForm()">新增交易</button>
<div class="message" id="msgBox"></div>
<!-- 查看月出支按鈕 -->
<button style="background:#f44336; margin-top:20px;" onclick="viewMonthlyExpenses()">
查看月出支
</button>
<!-- 預算表 -->
<table class="budget-table" id="budgetTable">
<thead>
<tr>
<th>類別</th>
<th>預算</th>
<th>本月已用</th>
</tr>
</thead>
<tbody id="budgetTableBody"></tbody>
</table>
</div>
<!-- 浮層 (Modal) 內容 -->
<div class="modal-overlay" id="monthlyExpensesModal">
<div class="modal-content">
<div class="modal-header">
<h2>本月支出列表</h2>
<button class="close-btn" onclick="closeMonthlyExpensesModal()">X</button>
</div>
<!-- 月份選擇區域 -->
<div style="margin-bottom: 15px; display: flex; justify-content: space-between;">
<div class="input-field" style="flex: 0 0 48%;">
<label for="selectMonth">選擇月份</label>
<select id="selectMonth" onchange="viewMonthlyExpenses()">
<!-- 月份選項由 JavaScript 動態生成 -->
</select>
</div>
<div class="input-field" style="flex: 0 0 48%;">
<label for="selectYear">選擇年份</label>
<select id="selectYear" onchange="viewMonthlyExpenses()">
<!-- 年份選項由 JavaScript 動態生成 -->
</select>
</div>
</div>
<!-- 搜尋/篩選區域 -->
<div style="margin-bottom: 15px;">
<input type="text" id="searchKeyword" placeholder="搜尋關鍵字(類別或備註)" style="width: 48%; padding: 8px; margin-right: 4%;">
<input type="date" id="filterStartDate" placeholder="開始日期" style="width: 48%; padding: 8px;">
<input type="date" id="filterEndDate" placeholder="結束日期" style="width: 48%; padding: 8px; margin-top: 10px;">
<button class="filter-btn" onclick="applyFilters()" style="width: 48%; margin-top: 10px;">套用篩選</button>
<button class="filter-btn" onclick="resetFilters()" style="width: 48%; margin-top: 10px; background:#555;">重置篩選</button>
</div>
<!-- 排序及匯出按鈕 -->
<div style="margin-bottom: 15px;">
<button class="sort-btn" onclick="sortExpensesByCategory()">
按「類別」排序
</button>
<button class="export-btn" onclick="exportToCSV()">
匯出為 CSV
</button>
</div>
<!-- 支出表格 -->
<table class="expense-table" id="expenseTable">
<thead>
<tr>
<th>日期時間</th>
<th>類別</th>
<th>金額</th>
<th>備註</th>
</tr>
</thead>
<tbody id="expenseTableBody"></tbody>
</table>
<!-- 總計 -->
<div id="totalAmount" style="text-align:right; font-weight:bold;"></div>
</div>
</div>
<script>
/********************************************
* 前端初始化:載入「分類清單」與「預算表」
********************************************/
document.addEventListener('DOMContentLoaded', function() {
setDefaultDateTime();
populateMonthYearSelectors();
// 載入分類
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
// 載入預算
refreshDashboard();
});
// 設定「現在時間」到 datetime-local
function setDefaultDateTime() {
const now = new Date();
const localISO = new Date(now.getTime() - now.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16); // 'YYYY-MM-DDTHH:MM'
document.getElementById('dateTime').value = localISO;
}
// 填充分類下拉
function populateCategorySelect(categories) {
const categorySelect = document.getElementById('category');
categorySelect.innerHTML = '';
// 根據選擇的類型篩選類別
const selectedType = document.getElementById('type').value;
categories.forEach(function(cat) {
if (cat.type === selectedType) {
const option = document.createElement('option');
option.value = cat.name;
option.text = cat.name;
categorySelect.appendChild(option);
}
});
}
// 根據「類型」篩選「類別」選項
function filterCategoryOptions() {
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
}
// 新增交易
function submitForm() {
const dateTime = document.getElementById('dateTime').value;
const type = document.getElementById('type').value;
const category = document.getElementById('category').value;
const amount = document.getElementById('amount').value;
const note = document.getElementById('note').value;
// 簡易驗證
if (!dateTime || !type || !category || !amount) {
showMessage('請填寫完整資訊', true);
return;
}
const transaction = {
dateTime: dateTime,
type: type,
category: category,
amount: amount,
note: note
};
google.script.run
.withSuccessHandler(function(msg) {
showMessage(msg, false);
setDefaultDateTime();
document.getElementById('amount').value = '';
document.getElementById('note').value = '';
// 刷新預算
refreshDashboard();
})
.withFailureHandler(function(err) {
showMessage('寫入失敗: ' + err.message, true);
})
.saveTransaction(transaction);
}
// 顯示訊息
function showMessage(msg, isError) {
const msgBox = document.getElementById('msgBox');
msgBox.style.color = isError ? 'red' : 'green';
msgBox.textContent = msg;
}
// 刷新預算表
function refreshDashboard() {
google.script.run
.withSuccessHandler(function(dashboardData){
populateBudgetTable(dashboardData);
})
.getDashboardData();
}
function populateBudgetTable(data) {
const tbody = document.getElementById('budgetTableBody');
tbody.innerHTML = '';
data.forEach(function(item){
const tr = document.createElement('tr');
if (item.warningLevel === 'over') {
tr.className = 'warning-over';
} else if (item.warningLevel === 'near') {
tr.className = 'warning-near';
} else {
tr.className = 'warning-normal';
}
const tdCat = document.createElement('td');
tdCat.textContent = item.category;
const tdBudget = document.createElement('td');
tdBudget.textContent = item.budget || 0;
const tdSpent = document.createElement('td');
tdSpent.textContent = item.spent;
tr.appendChild(tdCat);
tr.appendChild(tdBudget);
tr.appendChild(tdSpent);
tbody.appendChild(tr);
});
}
/********************************************
* 新增:查看「月出支」按鈕的功能
********************************************/
let monthlyExpenses = []; // 用於儲存本月支出資料
let filteredExpenses = []; // 用於儲存過濾後的支出資料
// 1) 填充月份和年份選擇器
function populateMonthYearSelectors() {
const selectMonth = document.getElementById('selectMonth');
const selectYear = document.getElementById('selectYear');
// 填充月份選項
const months = [
'一月', '二月', '三月', '四月', '五月', '六月',
'七月', '八月', '九月', '十月', '十一月', '十二月'
];
months.forEach(function(month, index) {
const option = document.createElement('option');
option.value = index; // 0-11
option.text = month;
selectMonth.appendChild(option);
});
// 填充年份選項(例如,從 2020 到當前年份 + 5)
const currentYear = new Date().getFullYear();
for(let year = currentYear - 5; year <= currentYear + 5; year++) {
const option = document.createElement('option');
option.value = year;
option.text = year;
selectYear.appendChild(option);
}
}
// 2) 取得指定月份支出 -> 顯示浮層
function viewMonthlyExpenses() {
const selectMonth = document.getElementById('selectMonth').value;
const selectYear = document.getElementById('selectYear').value;
google.script.run
.withSuccessHandler(function(expenses){
if (Array.isArray(expenses)) {
monthlyExpenses = expenses;
filteredExpenses = expenses; // 初始化為全部
setDefaultFilterDates(selectMonth, selectYear); // 設定預設日期範圍
applyFilters(); // 套用初始篩選
renderMonthlyExpenses();
document.getElementById('monthlyExpensesModal').style.display = 'flex';
} else {
alert('取得的支出資料格式錯誤。');
}
})
.withFailureHandler(function(err){
alert('取得支出失敗:' + err.message);
})
.getExpenses(parseInt(selectMonth), parseInt(selectYear));
}
// 3) 設定預設日期範圍為選擇的月份的1日到最後一日
function setDefaultFilterDates(month, year) {
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const startDateInput = document.getElementById('filterStartDate');
const endDateInput = document.getElementById('filterEndDate');
startDateInput.value = firstDay.toISOString().slice(0, 10); // 'YYYY-MM-DD'
endDateInput.value = lastDay.toISOString().slice(0, 10); // 'YYYY-MM-DD'
}
// 4) 渲染本月支出的表格
function renderMonthlyExpenses() {
const tbody = document.getElementById('expenseTableBody');
tbody.innerHTML = '';
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 4;
td.textContent = '本月尚無符合條件的支出紀錄。';
tr.appendChild(td);
tbody.appendChild(tr);
document.getElementById('totalAmount').textContent = '總計:0';
return;
}
// 計算總計
let total = 0;
filteredExpenses.forEach(function(item){
total += parseFloat(item.amount) || 0;
const tr = document.createElement('tr');
const tdDateTime = document.createElement('td');
tdDateTime.textContent = item.dateTime; // 格式化後的日期時間
const tdCategory = document.createElement('td');
tdCategory.textContent = item.category;
const tdAmount = document.createElement('td');
tdAmount.textContent = item.amount;
const tdNote = document.createElement('td');
tdNote.textContent = item.note;
tr.appendChild(tdDateTime);
tr.appendChild(tdCategory);
tr.appendChild(tdAmount);
tr.appendChild(tdNote);
tbody.appendChild(tr);
});
// 顯示總計
document.getElementById('totalAmount').textContent = '總計:' + total;
}
// 5) 按類別排序 (升冪) 後重新渲染
function sortExpensesByCategory() {
if (!Array.isArray(filteredExpenses)) return;
filteredExpenses.sort(function(a, b){
return a.category.localeCompare(b.category, 'zh-TW');
});
renderMonthlyExpenses();
}
// 6) 關閉浮層
function closeMonthlyExpensesModal() {
document.getElementById('monthlyExpensesModal').style.display = 'none';
}
// 7) 套用篩選
function applyFilters() {
const keyword = document.getElementById('searchKeyword').value.trim().toLowerCase();
const startDate = document.getElementById('filterStartDate').value;
const endDate = document.getElementById('filterEndDate').value;
filteredExpenses = monthlyExpenses.filter(function(expense){
let matchesKeyword = true;
let matchesStartDate = true;
let matchesEndDate = true;
// 檢查關鍵字(類別或備註)
if (keyword) {
const categoryMatch = expense.category.toLowerCase().includes(keyword);
const noteMatch = expense.note.toLowerCase().includes(keyword);
matchesKeyword = categoryMatch || noteMatch;
}
// 檢查開始日期
if (startDate) {
const expenseDate = new Date(expense.dateTime);
const start = new Date(startDate);
matchesStartDate = expenseDate >= start;
}
// 檢查結束日期
if (endDate) {
const expenseDate = new Date(expense.dateTime);
const end = new Date(endDate);
// 將結束日期設為當天結束
end.setHours(23,59,59,999);
matchesEndDate = expenseDate <= end;
}
return matchesKeyword && matchesStartDate && matchesEndDate;
});
renderMonthlyExpenses();
}
// 8) 重置篩選
function resetFilters() {
document.getElementById('searchKeyword').value = '';
document.getElementById('filterStartDate').value = '';
document.getElementById('filterEndDate').value = '';
filteredExpenses = monthlyExpenses;
renderMonthlyExpenses();
}
// 9) 匯出為 CSV
function exportToCSV() {
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
alert('沒有可匯出的資料。');
return;
}
// 構建 CSV 資料
let csvContent = "data:text/csv;charset=utf-8,";
csvContent += "日期時間,類別,金額,備註\n"; // 標題
filteredExpenses.forEach(function(item){
const row = `"${item.dateTime}","${item.category}","${item.amount}","${item.note}"`;
csvContent += row + "\n";
});
// 觸發下載
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
const now = new Date();
const timestamp = now.getFullYear() + "-" +
String(now.getMonth()+1).padStart(2, '0') + "-" +
String(now.getDate()).padStart(2, '0') + "_" +
String(now.getHours()).padStart(2, '0') + "-" +
String(now.getMinutes()).padStart(2, '0') + "-" +
String(now.getSeconds()).padStart(2, '0');
link.setAttribute("download", `本月支出_${timestamp}.csv`);
document.body.appendChild(link); // Required for FF
link.click(); // This will download the data file named "本月支出_timestamp.csv"
document.body.removeChild(link);
}
</script>
</body>
</html>
更改按鈕標題:
新增月份和年份選擇器:
selectMonth),一個用於選擇年份(selectYear)。預設選擇當前月份和年份:
viewMonthlyExpenses 函式會取得當前選擇的月份和年份(預設為當前月份和年份)。動態載入支出紀錄:
viewMonthlyExpenses 函式,使其根據選擇的月份和年份呼叫後端的 getExpenses 函式,取得相應月份的支出紀錄。monthlyExpenses 和 filteredExpenses 陣列,以便後續的篩選和渲染。套用篩選功能:
applyFilters 函式現在會根據使用者選擇的月份和年份,結合關鍵字搜尋和日期範圍來過濾支出紀錄。匯出功能:
exportToCSV 函式保持不變,仍然可以將當前顯示的支出紀錄匯出為 CSV 檔案。其他調整:
更新 Code.gs:
Code.gs 替換為上述提供的完整程式碼。更新 Index.html:
Index.html 替換為上述提供的完整 HTML 內容。重新部署應用程式:
測試功能:
Transactions 工作表中。修正與優化:
月份和年份選擇器無法正常填充或顯示:
populateMonthYearSelectors 函式被正確呼叫:DOMContentLoaded 事件中呼叫了 populateMonthYearSelectors 函式。populateMonthYearSelectors 或相關函式中。切換月份和年份後,支出紀錄未正確顯示:
viewMonthlyExpenses 函式被正確呼叫:viewMonthlyExpenses 函式被觸發並且呼叫了後端的 getExpenses 函式。getExpenses 函式能正確回傳指定月份和年份的支出紀錄。套用篩選功能無法正常運作:
monthlyExpenses 陣列。applyFilters 或 renderMonthlyExpenses 函式中。monthlyExpenses 和 filteredExpenses 陣列中的每個物件包含 dateTime、category、amount 和 note 屬性。匯出功能無法下載 CSV 檔案:
exportToCSV 函式被正確呼叫:exportToCSV 函式。filteredExpenses 陣列中的資料正確,並且所有欄位都被正確地轉換為 CSV 格式。浮層無法正確顯示或關閉:
.modal-overlay 的 display 屬性在開啟和關閉時被正確設置。viewMonthlyExpenses 和 closeMonthlyExpensesModal 函式被正確呼叫,並且修改了 .modal-overlay 的 display 屬性。日期時間顯示錯誤:
datetime-local 欄位正確選擇日期與時間。getExpenses 函式的日期格式化:Utilities.formatDate 將日期格式化為 yyyy/MM/dd HH:mm,以便前端能正確顯示。類別選單未正確更新:
filterCategoryOptions 函式被正確呼叫:filterCategoryOptions 函式被觸發,並且重新載入「類別」選單。Categories 工作表中的資料:新增交易後,預算表未更新:
submitForm 函式中 refreshDashboard 被正確呼叫:refreshDashboard 函式被呼叫以更新預算表。getDashboardData 函式:getDashboardData 函式能正確計算每個類別的預算和支出,並正確設定 warningLevel。透過以上的修改,你的記帳工具將具備以下新增功能:
更改按鈕標題:
預設日期範圍設定:
月份切換功能:
搜尋/篩選功能:
匯出功能:
表單佈局與介面優化:
防錯處理與使用者體驗提升:
請按照上述步驟進行更新與部署,並在使用過程中進行測試以確保所有功能正常運作。如有任何進一步的問題或需求,歡迎隨時提出!
查看月出支圖層,移除日期篩選的功能,預設呈現出該月份的支出
思考 花的時間為 11 秒
了解,我將協助你更新「查看月出支」的功能,具體包括:
更改按鈕標題:
移除日期篩選功能:
新增月份切換功能:
保留關鍵字搜尋功能:
保留匯出功能:
以下是更新後的 Index.html,已根據你的需求進行調整:
Index.html<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
/* 手機友善基礎樣式 */
body {
font-family: sans-serif;
margin: 10px;
}
h1 {
text-align: center;
margin-bottom: 0;
}
.sub-title {
text-align: center;
color: #666;
margin-top: 4px;
margin-bottom: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.input-row {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
.input-row .input-field {
flex: 0 0 48%;
}
label {
display: block;
font-weight: bold;
margin-bottom: 4px;
}
input, select {
width: 100%;
box-sizing: border-box;
padding: 8px;
font-size: 16px;
}
button {
width: 100%;
padding: 10px;
font-size: 16px;
background: #4CAF50;
color: #fff;
border: none;
cursor: pointer;
margin-top: 10px;
border-radius: 4px;
}
button:hover {
background: #45a049;
}
/* 預算表 */
.budget-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.budget-table th, .budget-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.warning-normal {
background-color: #ffffff;
}
.warning-near {
background-color: #fff8db; /* 淡黃色 */
}
.warning-over {
background-color: #ffdcdc; /* 淡紅色 */
}
.message {
margin-top: 8px;
text-align: center;
}
/* 浮層 (Modal) 樣式 */
.modal-overlay {
position: fixed;
z-index: 999; /* 確保浮層在最上層 */
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景 */
display: none; /* 初始隱藏 */
justify-content: center;
align-items: center;
}
.modal-content {
background: #fff;
padding: 20px;
border-radius: 8px;
width: 95%;
max-width: 800px;
max-height: 90vh;
overflow-y: auto;
}
/* 調整後的模態框標題 */
.modal-header {
display: flex;
width: 100%;
margin-bottom: 10px;
}
.modal-header h2 {
flex: 1;
text-align: left;
margin: 0;
font-size: 20px;
}
.modal-header .close-btn {
flex: 1;
text-align: right;
background: #ccc;
color: #000;
border: none;
font-size: 20px;
padding: 5px 10px;
cursor: pointer;
border-radius: 4px;
}
.modal-header .close-btn:hover {
background: #aaa;
}
.expense-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
}
.expense-table th, .expense-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.sort-btn, .filter-btn, .export-btn {
background: #2196F3;
color: #fff;
padding: 8px 12px;
border: none;
cursor: pointer;
border-radius: 4px;
margin-right: 10px;
margin-bottom: 10px;
}
.sort-btn:hover, .filter-btn:hover, .export-btn:hover {
background: #0b7dda;
}
/* 滾動條樣式(選用) */
.modal-content::-webkit-scrollbar {
width: 8px;
}
.modal-content::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.2);
border-radius: 4px;
}
</style>
</head>
<body>
<h1>簡易記帳工具</h1>
<div class="sub-title">Google Apps Script + 試算表</div>
<div class="container">
<!-- 新增交易表單 -->
<div class="input-row">
<!-- 類型 -->
<div class="input-field">
<label for="type">類型</label>
<select id="type" onchange="filterCategoryOptions()">
<option value="支出" selected>支出</option>
<option value="收入">收入</option>
</select>
</div>
<!-- 類別 -->
<div class="input-field">
<label for="category">類別</label>
<select id="category"></select>
</div>
</div>
<div class="input-row">
<!-- 金額 -->
<div class="input-field">
<label for="amount">金額</label>
<input type="number" id="amount" step="0.01" />
</div>
<!-- 備註 -->
<div class="input-field">
<label for="note">備註</label>
<input type="text" id="note" />
</div>
</div>
<!-- 日期與時間 -->
<div class="input-field">
<label for="dateTime">日期與時間</label>
<input type="datetime-local" id="dateTime" />
</div>
<button onclick="submitForm()">新增交易</button>
<div class="message" id="msgBox"></div>
<!-- 查看月出支按鈕 -->
<button style="background:#f44336; margin-top:20px;" onclick="viewMonthlyExpenses()">
查看月出支
</button>
<!-- 預算表 -->
<table class="budget-table" id="budgetTable">
<thead>
<tr>
<th>類別</th>
<th>預算</th>
<th>本月已用</th>
</tr>
</thead>
<tbody id="budgetTableBody"></tbody>
</table>
</div>
<!-- 浮層 (Modal) 內容 -->
<div class="modal-overlay" id="monthlyExpensesModal">
<div class="modal-content">
<div class="modal-header">
<h2>本月支出列表</h2>
<button class="close-btn" onclick="closeMonthlyExpensesModal()">X</button>
</div>
<!-- 月份和年份選擇區域 -->
<div style="margin-bottom: 15px; display: flex; justify-content: space-between;">
<div class="input-field" style="flex: 0 0 48%;">
<label for="selectMonth">選擇月份</label>
<select id="selectMonth" onchange="viewMonthlyExpenses()">
<!-- 月份選項由 JavaScript 動態生成 -->
</select>
</div>
<div class="input-field" style="flex: 0 0 48%;">
<label for="selectYear">選擇年份</label>
<select id="selectYear" onchange="viewMonthlyExpenses()">
<!-- 年份選項由 JavaScript 動態生成 -->
</select>
</div>
</div>
<!-- 搜尋區域(移除日期篩選) -->
<div style="margin-bottom: 15px;">
<input type="text" id="searchKeyword" placeholder="搜尋關鍵字(類別或備註)" style="width: 48%; padding: 8px; margin-right: 4%;">
<button class="filter-btn" onclick="applyFilters()" style="width: 48%; margin-top: 10px;">套用篩選</button>
<button class="filter-btn" onclick="resetFilters()" style="width: 48%; margin-top: 10px; background:#555;">重置篩選</button>
</div>
<!-- 排序及匯出按鈕 -->
<div style="margin-bottom: 15px;">
<button class="sort-btn" onclick="sortExpensesByCategory()">
按「類別」排序
</button>
<button class="export-btn" onclick="exportToCSV()">
匯出為 CSV
</button>
</div>
<!-- 支出表格 -->
<table class="expense-table" id="expenseTable">
<thead>
<tr>
<th>日期時間</th>
<th>類別</th>
<th>金額</th>
<th>備註</th>
</tr>
</thead>
<tbody id="expenseTableBody"></tbody>
</table>
<!-- 總計 -->
<div id="totalAmount" style="text-align:right; font-weight:bold;"></div>
</div>
</div>
<script>
/********************************************
* 前端初始化:載入「分類清單」與「預算表」
********************************************/
document.addEventListener('DOMContentLoaded', function() {
setDefaultDateTime();
populateMonthYearSelectors();
// 載入分類
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
// 載入預算
refreshDashboard();
});
// 設定「現在時間」到 datetime-local
function setDefaultDateTime() {
const now = new Date();
const localISO = new Date(now.getTime() - now.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16); // 'YYYY-MM-DDTHH:MM'
document.getElementById('dateTime').value = localISO;
}
// 填充分類下拉
function populateCategorySelect(categories) {
const categorySelect = document.getElementById('category');
categorySelect.innerHTML = '';
// 根據選擇的類型篩選類別
const selectedType = document.getElementById('type').value;
categories.forEach(function(cat) {
if (cat.type === selectedType) {
const option = document.createElement('option');
option.value = cat.name;
option.text = cat.name;
categorySelect.appendChild(option);
}
});
}
// 根據「類型」篩選「類別」選項
function filterCategoryOptions() {
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
}
// 新增交易
function submitForm() {
const dateTime = document.getElementById('dateTime').value;
const type = document.getElementById('type').value;
const category = document.getElementById('category').value;
const amount = document.getElementById('amount').value;
const note = document.getElementById('note').value;
// 簡易驗證
if (!dateTime || !type || !category || !amount) {
showMessage('請填寫完整資訊', true);
return;
}
const transaction = {
dateTime: dateTime,
type: type,
category: category,
amount: amount,
note: note
};
google.script.run
.withSuccessHandler(function(msg) {
showMessage(msg, false);
setDefaultDateTime();
document.getElementById('amount').value = '';
document.getElementById('note').value = '';
// 刷新預算
refreshDashboard();
})
.withFailureHandler(function(err) {
showMessage('寫入失敗: ' + err.message, true);
})
.saveTransaction(transaction);
}
// 顯示訊息
function showMessage(msg, isError) {
const msgBox = document.getElementById('msgBox');
msgBox.style.color = isError ? 'red' : 'green';
msgBox.textContent = msg;
}
// 刷新預算表
function refreshDashboard() {
google.script.run
.withSuccessHandler(function(dashboardData){
populateBudgetTable(dashboardData);
})
.getDashboardData();
}
function populateBudgetTable(data) {
const tbody = document.getElementById('budgetTableBody');
tbody.innerHTML = '';
data.forEach(function(item){
const tr = document.createElement('tr');
if (item.warningLevel === 'over') {
tr.className = 'warning-over';
} else if (item.warningLevel === 'near') {
tr.className = 'warning-near';
} else {
tr.className = 'warning-normal';
}
const tdCat = document.createElement('td');
tdCat.textContent = item.category;
const tdBudget = document.createElement('td');
tdBudget.textContent = item.budget || 0;
const tdSpent = document.createElement('td');
tdSpent.textContent = item.spent;
tr.appendChild(tdCat);
tr.appendChild(tdBudget);
tr.appendChild(tdSpent);
tbody.appendChild(tr);
});
}
/********************************************
* 新增:查看「月出支」按鈕的功能
********************************************/
let monthlyExpenses = []; // 用於儲存當月支出資料
let filteredExpenses = []; // 用於儲存過濾後的支出資料
// 1) 填充月份和年份選擇器
function populateMonthYearSelectors() {
const selectMonth = document.getElementById('selectMonth');
const selectYear = document.getElementById('selectYear');
// 填充月份選項
const months = [
'一月', '二月', '三月', '四月', '五月', '六月',
'七月', '八月', '九月', '十月', '十一月', '十二月'
];
months.forEach(function(month, index) {
const option = document.createElement('option');
option.value = index; // 0-11
option.text = month;
selectMonth.appendChild(option);
});
// 填充年份選項(例如,從 2020 到當前年份 + 5)
const currentYear = new Date().getFullYear();
for(let year = currentYear - 5; year <= currentYear + 5; year++) {
const option = document.createElement('option');
option.value = year;
option.text = year;
selectYear.appendChild(option);
}
// 設定預設選擇為當前年月
selectMonth.value = new Date().getMonth(); // 0-11
selectYear.value = currentYear;
}
// 2) 取得指定月份支出 -> 顯示浮層
function viewMonthlyExpenses() {
const selectMonth = document.getElementById('selectMonth').value;
const selectYear = document.getElementById('selectYear').value;
google.script.run
.withSuccessHandler(function(expenses){
if (Array.isArray(expenses)) {
monthlyExpenses = expenses;
filteredExpenses = expenses; // 初始化為全部
applyFilters(); // 套用初始篩選(僅關鍵字)
renderMonthlyExpenses();
document.getElementById('monthlyExpensesModal').style.display = 'flex';
} else {
alert('取得的支出資料格式錯誤。');
}
})
.withFailureHandler(function(err){
alert('取得支出失敗:' + err.message);
})
.getExpenses(parseInt(selectMonth), parseInt(selectYear));
}
// 3) 渲染當月支出的表格
function renderMonthlyExpenses() {
const tbody = document.getElementById('expenseTableBody');
tbody.innerHTML = '';
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 4;
td.textContent = '本月尚無符合條件的支出紀錄。';
tr.appendChild(td);
tbody.appendChild(tr);
document.getElementById('totalAmount').textContent = '總計:0';
return;
}
// 計算總計
let total = 0;
filteredExpenses.forEach(function(item){
total += parseFloat(item.amount) || 0;
const tr = document.createElement('tr');
const tdDateTime = document.createElement('td');
tdDateTime.textContent = item.dateTime; // 格式化後的日期時間
const tdCategory = document.createElement('td');
tdCategory.textContent = item.category;
const tdAmount = document.createElement('td');
tdAmount.textContent = item.amount;
const tdNote = document.createElement('td');
tdNote.textContent = item.note;
tr.appendChild(tdDateTime);
tr.appendChild(tdCategory);
tr.appendChild(tdAmount);
tr.appendChild(tdNote);
tbody.appendChild(tr);
});
// 顯示總計
document.getElementById('totalAmount').textContent = '總計:' + total;
}
// 4) 按類別排序 (升冪) 後重新渲染
function sortExpensesByCategory() {
if (!Array.isArray(filteredExpenses)) return;
filteredExpenses.sort(function(a, b){
return a.category.localeCompare(b.category, 'zh-TW');
});
renderMonthlyExpenses();
}
// 5) 關閉浮層
function closeMonthlyExpensesModal() {
document.getElementById('monthlyExpensesModal').style.display = 'none';
}
// 6) 套用篩選(僅關鍵字)
function applyFilters() {
const keyword = document.getElementById('searchKeyword').value.trim().toLowerCase();
filteredExpenses = monthlyExpenses.filter(function(expense){
let matchesKeyword = true;
// 檢查關鍵字(類別或備註)
if (keyword) {
const categoryMatch = expense.category.toLowerCase().includes(keyword);
const noteMatch = expense.note.toLowerCase().includes(keyword);
matchesKeyword = categoryMatch || noteMatch;
}
return matchesKeyword;
});
renderMonthlyExpenses();
}
// 7) 重置篩選(僅關鍵字)
function resetFilters() {
document.getElementById('searchKeyword').value = '';
filteredExpenses = monthlyExpenses;
renderMonthlyExpenses();
}
// 8) 匯出為 CSV
function exportToCSV() {
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
alert('沒有可匯出的資料。');
return;
}
// 構建 CSV 資料
let csvContent = "data:text/csv;charset=utf-8,";
csvContent += "日期時間,類別,金額,備註\n"; // 標題
filteredExpenses.forEach(function(item){
const row = `"${item.dateTime}","${item.category}","${item.amount}","${item.note}"`;
csvContent += row + "\n";
});
// 觸發下載
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
const now = new Date();
const timestamp = now.getFullYear() + "-" +
String(now.getMonth()+1).padStart(2, '0') + "-" +
String(now.getDate()).padStart(2, '0') + "_" +
String(now.getHours()).padStart(2, '0') + "-" +
String(now.getMinutes()).padStart(2, '0') + "-" +
String(now.getSeconds()).padStart(2, '0');
link.setAttribute("download", `本月支出_${timestamp}.csv`);
document.body.appendChild(link); // Required for FF
link.click(); // This will download the data file named "本月支出_timestamp.csv"
document.body.removeChild(link);
}
</script>
</body>
</html>
filterStartDate 和 filterEndDate)。applyFilters 和 resetFilters 函式,使其僅處理關鍵字篩選。selectMonth 和 selectYear,分別用於選擇月份和年份。populateMonthYearSelectors 函式動態填充月份(從一月到十二月)和年份(從當前年份減5年到當前年份加5年)的選項。viewMonthlyExpenses 函式以更新顯示的支出紀錄。searchKeyword),允許使用者根據類別或備註進行過濾。exportToCSV 函式將匯出 filteredExpenses 陣列中的資料。populateMonthYearSelectors 函式:viewMonthlyExpenses 函式:getExpenses 函式以取得該月的支出紀錄。monthlyExpenses 和 filteredExpenses 陣列,並呼叫 applyFilters 進行初始篩選(僅關鍵字)。applyFilters 和 resetFilters 函式:renderMonthlyExpenses 函式:filteredExpenses 陣列中的支出紀錄到表格中,並計算總計。sortExpensesByCategory 函式:exportToCSV 函式:filteredExpenses 資料匯出為 CSV 檔案,並自動下載。更新 Code.gs:
Code.gs 已包含之前步驟中的所有功能,特別是 getExpenses(month, year) 函式。Code.gs,確認是否需要進一步調整以配合新的前端功能。更新 Index.html:
Index.html 替換為上述提供的完整 HTML 內容。重新部署應用程式:
測試功能:
Transactions 工作表中。修正與優化:
月份和年份選擇器無法正常填充或顯示:
populateMonthYearSelectors 函式被正確呼叫:DOMContentLoaded 事件中呼叫了 populateMonthYearSelectors 函式。populateMonthYearSelectors 或相關函式中。切換月份和年份後,支出紀錄未正確顯示:
viewMonthlyExpenses 函式被正確呼叫:viewMonthlyExpenses 函式被觸發並且呼叫了後端的 getExpenses 函式。getExpenses 函式能正確回傳指定月份和年份的支出紀錄。套用篩選功能無法正常運作:
monthlyExpenses 陣列。applyFilters 或 renderMonthlyExpenses 函式中。monthlyExpenses 和 filteredExpenses 陣列中的每個物件包含 dateTime、category、amount 和 note 屬性。匯出功能無法下載 CSV 檔案:
exportToCSV 函式被正確呼叫:exportToCSV 函式。filteredExpenses 陣列中的資料正確,並且所有欄位都被正確地轉換為 CSV 格式。浮層無法正確顯示或關閉:
.modal-overlay 的 display 屬性在開啟和關閉時被正確設置。viewMonthlyExpenses 和 closeMonthlyExpensesModal 函式被正確呼叫,並且修改了 .modal-overlay 的 display 屬性。日期時間顯示錯誤:
datetime-local 欄位正確選擇日期與時間。getExpenses 函式的日期格式化:Utilities.formatDate 將日期格式化為 yyyy/MM/dd HH:mm,以便前端能正確顯示。類別選單未正確更新:
filterCategoryOptions 函式被正確呼叫:filterCategoryOptions 函式被觸發,並且重新載入「類別」選單。Categories 工作表中的資料:新增交易後,預算表未更新:
submitForm 函式中 refreshDashboard 被正確呼叫:refreshDashboard 函式被呼叫以更新預算表。getDashboardData 函式:getDashboardData 函式能正確計算每個類別的預算和支出,並正確設定 warningLevel。透過以上的修改,你的記帳工具將具備以下新增功能:
更改按鈕標題:
移除日期篩選功能:
新增月份切換功能:
保留關鍵字搜尋功能:
保留匯出功能:
表單佈局與介面優化:
防錯處理與使用者體驗提升:
請按照上述步驟進行更新與部署,並在使用過程中進行測試以確保所有功能正常運作。如有任何進一步的問題或需求,歡迎隨時提出!
查看月出支圖層,把"搜尋"框改到"X"(關閉)按鈕的位置,並加上送出按鈕,關閉改為點擊圖層外的區域即關閉,移除套用篩選及重置篩選的按鈕
思考 花的時間為 5 秒
了解,我將協助你對「查看月出支」的浮層進行以下修改:
將「搜尋」框移至浮層標題區域,並新增「送出」按鈕:
移除「X」關閉按鈕:
實現點擊浮層外部區域即關閉浮層的功能:
.modal-overlay 區域)時,浮層將自動關閉。移除「套用篩選」及「重置篩選」按鈕:
以下是更新後的 Index.html,已根據你的需求進行調整:
Index.html<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
/* 手機友善基礎樣式 */
body {
font-family: sans-serif;
margin: 10px;
}
h1 {
text-align: center;
margin-bottom: 0;
}
.sub-title {
text-align: center;
color: #666;
margin-top: 4px;
margin-bottom: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.input-row {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
.input-row .input-field {
flex: 0 0 48%;
}
label {
display: block;
font-weight: bold;
margin-bottom: 4px;
}
input, select {
width: 100%;
box-sizing: border-box;
padding: 8px;
font-size: 16px;
}
button {
width: 100%;
padding: 10px;
font-size: 16px;
background: #4CAF50;
color: #fff;
border: none;
cursor: pointer;
margin-top: 10px;
border-radius: 4px;
}
button:hover {
background: #45a049;
}
/* 預算表 */
.budget-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.budget-table th, .budget-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.warning-normal {
background-color: #ffffff;
}
.warning-near {
background-color: #fff8db; /* 淡黃色 */
}
.warning-over {
background-color: #ffdcdc; /* 淡紅色 */
}
.message {
margin-top: 8px;
text-align: center;
}
/* 浮層 (Modal) 樣式 */
.modal-overlay {
position: fixed;
z-index: 999; /* 確保浮層在最上層 */
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景 */
display: none; /* 初始隱藏 */
justify-content: center;
align-items: center;
}
.modal-content {
background: #fff;
padding: 20px;
border-radius: 8px;
width: 95%;
max-width: 800px;
max-height: 90vh;
overflow-y: auto;
position: relative;
}
/* 調整後的模態框標題 */
.modal-header {
display: flex;
width: 100%;
align-items: center;
margin-bottom: 15px;
}
.modal-header h2 {
flex: 1;
text-align: left;
margin: 0;
font-size: 20px;
}
/* 搜尋框和送出按鈕 */
.search-container {
display: flex;
align-items: center;
flex: 1;
margin-left: 10px;
}
.search-container input[type="text"] {
flex: 1;
padding: 6px;
font-size: 14px;
}
.search-container button {
padding: 6px 12px;
margin-left: 5px;
font-size: 14px;
background: #2196F3;
}
.search-container button:hover {
background: #0b7dda;
}
.expense-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
}
.expense-table th, .expense-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.sort-btn, .export-btn {
background: #2196F3;
color: #fff;
padding: 8px 12px;
border: none;
cursor: pointer;
border-radius: 4px;
margin-right: 10px;
margin-bottom: 10px;
}
.sort-btn:hover, .export-btn:hover {
background: #0b7dda;
}
/* 滾動條樣式(選用) */
.modal-content::-webkit-scrollbar {
width: 8px;
}
.modal-content::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.2);
border-radius: 4px;
}
</style>
</head>
<body>
<h1>簡易記帳工具</h1>
<div class="sub-title">Google Apps Script + 試算表</div>
<div class="container">
<!-- 新增交易表單 -->
<div class="input-row">
<!-- 類型 -->
<div class="input-field">
<label for="type">類型</label>
<select id="type" onchange="filterCategoryOptions()">
<option value="支出" selected>支出</option>
<option value="收入">收入</option>
</select>
</div>
<!-- 類別 -->
<div class="input-field">
<label for="category">類別</label>
<select id="category"></select>
</div>
</div>
<div class="input-row">
<!-- 金額 -->
<div class="input-field">
<label for="amount">金額</label>
<input type="number" id="amount" step="0.01" />
</div>
<!-- 備註 -->
<div class="input-field">
<label for="note">備註</label>
<input type="text" id="note" />
</div>
</div>
<!-- 日期與時間 -->
<div class="input-field">
<label for="dateTime">日期與時間</label>
<input type="datetime-local" id="dateTime" />
</div>
<button onclick="submitForm()">新增交易</button>
<div class="message" id="msgBox"></div>
<!-- 查看月出支按鈕 -->
<button style="background:#f44336; margin-top:20px;" onclick="viewMonthlyExpenses()">
查看月出支
</button>
<!-- 預算表 -->
<table class="budget-table" id="budgetTable">
<thead>
<tr>
<th>類別</th>
<th>預算</th>
<th>本月已用</th>
</tr>
</thead>
<tbody id="budgetTableBody"></tbody>
</table>
</div>
<!-- 浮層 (Modal) 內容 -->
<div class="modal-overlay" id="monthlyExpensesModal">
<div class="modal-content">
<div class="modal-header">
<h2>本月支出列表</h2>
<div class="search-container">
<input type="text" id="searchKeyword" placeholder="搜尋關鍵字(類別或備註)" />
<button onclick="applyFilters()">送出</button>
</div>
</div>
<!-- 月份和年份選擇區域 -->
<div style="margin-bottom: 15px; display: flex; justify-content: space-between;">
<div class="input-field" style="flex: 0 0 48%;">
<label for="selectMonth">選擇月份</label>
<select id="selectMonth" onchange="viewMonthlyExpenses()">
<!-- 月份選項由 JavaScript 動態生成 -->
</select>
</div>
<div class="input-field" style="flex: 0 0 48%;">
<label for="selectYear">選擇年份</label>
<select id="selectYear" onchange="viewMonthlyExpenses()">
<!-- 年份選項由 JavaScript 動態生成 -->
</select>
</div>
</div>
<!-- 排序及匯出按鈕 -->
<div style="margin-bottom: 15px;">
<button class="sort-btn" onclick="sortExpensesByCategory()">
按「類別」排序
</button>
<button class="export-btn" onclick="exportToCSV()">
匯出為 CSV
</button>
</div>
<!-- 支出表格 -->
<table class="expense-table" id="expenseTable">
<thead>
<tr>
<th>日期時間</th>
<th>類別</th>
<th>金額</th>
<th>備註</th>
</tr>
</thead>
<tbody id="expenseTableBody"></tbody>
</table>
<!-- 總計 -->
<div id="totalAmount" style="text-align:right; font-weight:bold;"></div>
</div>
</div>
<script>
/********************************************
* 前端初始化:載入「分類清單」與「預算表」
********************************************/
document.addEventListener('DOMContentLoaded', function() {
setDefaultDateTime();
populateMonthYearSelectors();
// 載入分類
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
// 載入預算
refreshDashboard();
});
// 設定「現在時間」到 datetime-local
function setDefaultDateTime() {
const now = new Date();
const localISO = new Date(now.getTime() - now.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16); // 'YYYY-MM-DDTHH:MM'
document.getElementById('dateTime').value = localISO;
}
// 填充分類下拉
function populateCategorySelect(categories) {
const categorySelect = document.getElementById('category');
categorySelect.innerHTML = '';
// 根據選擇的類型篩選類別
const selectedType = document.getElementById('type').value;
categories.forEach(function(cat) {
if (cat.type === selectedType) {
const option = document.createElement('option');
option.value = cat.name;
option.text = cat.name;
categorySelect.appendChild(option);
}
});
}
// 根據「類型」篩選「類別」選項
function filterCategoryOptions() {
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
}
// 新增交易
function submitForm() {
const dateTime = document.getElementById('dateTime').value;
const type = document.getElementById('type').value;
const category = document.getElementById('category').value;
const amount = document.getElementById('amount').value;
const note = document.getElementById('note').value;
// 簡易驗證
if (!dateTime || !type || !category || !amount) {
showMessage('請填寫完整資訊', true);
return;
}
const transaction = {
dateTime: dateTime,
type: type,
category: category,
amount: amount,
note: note
};
google.script.run
.withSuccessHandler(function(msg) {
showMessage(msg, false);
setDefaultDateTime();
document.getElementById('amount').value = '';
document.getElementById('note').value = '';
// 刷新預算
refreshDashboard();
})
.withFailureHandler(function(err) {
showMessage('寫入失敗: ' + err.message, true);
})
.saveTransaction(transaction);
}
// 顯示訊息
function showMessage(msg, isError) {
const msgBox = document.getElementById('msgBox');
msgBox.style.color = isError ? 'red' : 'green';
msgBox.textContent = msg;
}
// 刷新預算表
function refreshDashboard() {
google.script.run
.withSuccessHandler(function(dashboardData){
populateBudgetTable(dashboardData);
})
.getDashboardData();
}
function populateBudgetTable(data) {
const tbody = document.getElementById('budgetTableBody');
tbody.innerHTML = '';
data.forEach(function(item){
const tr = document.createElement('tr');
if (item.warningLevel === 'over') {
tr.className = 'warning-over';
} else if (item.warningLevel === 'near') {
tr.className = 'warning-near';
} else {
tr.className = 'warning-normal';
}
const tdCat = document.createElement('td');
tdCat.textContent = item.category;
const tdBudget = document.createElement('td');
tdBudget.textContent = item.budget || 0;
const tdSpent = document.createElement('td');
tdSpent.textContent = item.spent;
tr.appendChild(tdCat);
tr.appendChild(tdBudget);
tr.appendChild(tdSpent);
tbody.appendChild(tr);
});
}
/********************************************
* 新增:查看「月出支」按鈕的功能
********************************************/
let monthlyExpenses = []; // 用於儲存當月支出資料
let filteredExpenses = []; // 用於儲存過濾後的支出資料
// 1) 填充月份和年份選擇器
function populateMonthYearSelectors() {
const selectMonth = document.getElementById('selectMonth');
const selectYear = document.getElementById('selectYear');
// 填充月份選項
const months = [
'一月', '二月', '三月', '四月', '五月', '六月',
'七月', '八月', '九月', '十月', '十一月', '十二月'
];
months.forEach(function(month, index) {
const option = document.createElement('option');
option.value = index; // 0-11
option.text = month;
selectMonth.appendChild(option);
});
// 填充年份選項(例如,從 2020 到當前年份 + 5)
const currentYear = new Date().getFullYear();
for(let year = currentYear - 5; year <= currentYear + 5; year++) {
const option = document.createElement('option');
option.value = year;
option.text = year;
selectYear.appendChild(option);
}
// 設定預設選擇為當前年月
selectMonth.value = new Date().getMonth(); // 0-11
selectYear.value = currentYear;
}
// 2) 取得指定月份支出 -> 顯示浮層
function viewMonthlyExpenses() {
const selectMonth = document.getElementById('selectMonth').value;
const selectYear = document.getElementById('selectYear').value;
google.script.run
.withSuccessHandler(function(expenses){
if (Array.isArray(expenses)) {
monthlyExpenses = expenses;
filteredExpenses = expenses; // 初始化為全部
applyFilters(); // 套用初始篩選(僅關鍵字)
renderMonthlyExpenses();
document.getElementById('monthlyExpensesModal').style.display = 'flex';
} else {
alert('取得的支出資料格式錯誤。');
}
})
.withFailureHandler(function(err){
alert('取得支出失敗:' + err.message);
})
.getExpenses(parseInt(selectMonth), parseInt(selectYear));
}
// 3) 渲染當月支出的表格
function renderMonthlyExpenses() {
const tbody = document.getElementById('expenseTableBody');
tbody.innerHTML = '';
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 4;
td.textContent = '本月尚無符合條件的支出紀錄。';
tr.appendChild(td);
tbody.appendChild(tr);
document.getElementById('totalAmount').textContent = '總計:0';
return;
}
// 計算總計
let total = 0;
filteredExpenses.forEach(function(item){
total += parseFloat(item.amount) || 0;
const tr = document.createElement('tr');
const tdDateTime = document.createElement('td');
tdDateTime.textContent = item.dateTime; // 格式化後的日期時間
const tdCategory = document.createElement('td');
tdCategory.textContent = item.category;
const tdAmount = document.createElement('td');
tdAmount.textContent = item.amount;
const tdNote = document.createElement('td');
tdNote.textContent = item.note;
tr.appendChild(tdDateTime);
tr.appendChild(tdCategory);
tr.appendChild(tdAmount);
tr.appendChild(tdNote);
tbody.appendChild(tr);
});
// 顯示總計
document.getElementById('totalAmount').textContent = '總計:' + total;
}
// 4) 按類別排序 (升冪) 後重新渲染
function sortExpensesByCategory() {
if (!Array.isArray(filteredExpenses)) return;
filteredExpenses.sort(function(a, b){
return a.category.localeCompare(b.category, 'zh-TW');
});
renderMonthlyExpenses();
}
// 5) 關閉浮層
function closeMonthlyExpensesModal() {
document.getElementById('monthlyExpensesModal').style.display = 'none';
}
// 6) 套用篩選(僅關鍵字)
function applyFilters() {
const keyword = document.getElementById('searchKeyword').value.trim().toLowerCase();
filteredExpenses = monthlyExpenses.filter(function(expense){
let matchesKeyword = true;
// 檢查關鍵字(類別或備註)
if (keyword) {
const categoryMatch = expense.category.toLowerCase().includes(keyword);
const noteMatch = expense.note.toLowerCase().includes(keyword);
matchesKeyword = categoryMatch || noteMatch;
}
return matchesKeyword;
});
renderMonthlyExpenses();
}
// 7) 重置篩選(僅關鍵字)
function resetFilters() {
document.getElementById('searchKeyword').value = '';
filteredExpenses = monthlyExpenses;
renderMonthlyExpenses();
}
// 8) 匯出為 CSV
function exportToCSV() {
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
alert('沒有可匯出的資料。');
return;
}
// 構建 CSV 資料
let csvContent = "data:text/csv;charset=utf-8,";
csvContent += "日期時間,類別,金額,備註\n"; // 標題
filteredExpenses.forEach(function(item){
const row = `"${item.dateTime}","${item.category}","${item.amount}","${item.note}"`;
csvContent += row + "\n";
});
// 觸發下載
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
const now = new Date();
const timestamp = now.getFullYear() + "-" +
String(now.getMonth()+1).padStart(2, '0') + "-" +
String(now.getDate()).padStart(2, '0') + "_" +
String(now.getHours()).padStart(2, '0') + "-" +
String(now.getMinutes()).padStart(2, '0') + "-" +
String(now.getSeconds()).padStart(2, '0');
link.setAttribute("download", `本月支出_${timestamp}.csv`);
document.body.appendChild(link); // Required for FF
link.click(); // This will download the data file named "本月支出_timestamp.csv"
document.body.removeChild(link);
}
/********************************************
* 新增:點擊浮層外部關閉浮層
********************************************/
window.onclick = function(event) {
const modal = document.getElementById('monthlyExpensesModal');
if (event.target === modal) {
modal.style.display = 'none';
}
}
</script>
</body>
</html>
HTML 結構調整:
.modal-header 中新增一個 .search-container,其中包含一個搜尋輸入框和一個「送出」按鈕。CSS 調整:
.search-container 和其中的元素添加了適當的樣式,使其在標題區域內整齊排列。移除「X」按鈕:
.modal-header 中的關閉按鈕 (<button class="close-btn" ...>X</button>) 移除。新增點擊浮層外部關閉功能:
<script> 標籤內新增了一個全域的 window.onclick 事件監聽器。.modal-overlay 區域(即浮層背景)時,浮層會被關閉。HTML 調整:
JavaScript 調整:
applyFilters 和 resetFilters 函式,使其僅處理關鍵字篩選,不再處理日期篩選。整體樣式優化:
防錯處理:
更新 Code.gs:
Code.gs 已包含前述步驟中的所有功能,特別是 getExpenses(month, year) 函式。更新 Index.html:
Index.html 替換為上述提供的完整 HTML 內容。重新部署應用程式:
測試功能:
Transactions 工作表中。修正與優化:
搜尋功能無法正常運作:
applyFilters 函式被正確呼叫。applyFilters 或 renderMonthlyExpenses 函式中。monthlyExpenses 和 filteredExpenses 陣列中的每個物件包含 dateTime、category、amount 和 note 屬性。點擊浮層外部無法關閉浮層:
window.onclick 事件監聽器是否正確設置:<script> 標籤內新增的 window.onclick 函式能夠正確識別點擊事件。.modal-overlay 的 display 屬性在開啟和關閉時被正確設置。匯出功能無法下載 CSV 檔案:
exportToCSV 函式被正確呼叫:exportToCSV 函式。filteredExpenses 陣列中的資料正確,並且所有欄位都被正確地轉換為 CSV 格式。月份和年份選擇器無法正常填充或顯示:
populateMonthYearSelectors 函式被正確呼叫:DOMContentLoaded 事件中呼叫了 populateMonthYearSelectors 函式。populateMonthYearSelectors 或相關函式中。切換月份和年份後,支出紀錄未正確顯示:
viewMonthlyExpenses 函式被正確呼叫:viewMonthlyExpenses 函式被觸發並且呼叫了後端的 getExpenses 函式。getExpenses 函式能正確回傳指定月份和年份的支出紀錄。按類別排序功能無法正常運作:
sortExpensesByCategory 函式被正確呼叫:sortExpensesByCategory 函式。filteredExpenses 陣列中的資料能夠正確排序,並且 renderMonthlyExpenses 函式能夠正確重新渲染表格。新增交易後,預算表未更新:
submitForm 函式中 refreshDashboard 被正確呼叫:refreshDashboard 函式被呼叫以更新預算表。getDashboardData 函式:getDashboardData 函式能正確計算每個類別的預算和支出,並正確設定 warningLevel。日期時間顯示錯誤:
datetime-local 欄位正確選擇日期與時間。getExpenses 函式的日期格式化:Utilities.formatDate 將日期格式化為 yyyy/MM/dd HH:mm,以便前端能正確顯示。類別選單未正確更新:
filterCategoryOptions 函式被正確呼叫:filterCategoryOptions 函式被觸發,並且重新載入「類別」選單。Categories 工作表中的資料:透過以上的修改,你的記帳工具將具備以下新增功能:
搜尋框位置調整與送出按鈕:
移除「X」關閉按鈕並實現點擊浮層外部關閉功能:
移除「套用篩選」及「重置篩選」按鈕:
保留並優化其他功能:
整體介面優化:
防錯處理與使用者體驗提升:
請按照上述步驟進行更新與部署,並在使用過程中進行測試以確保所有功能正常運作。如有任何進一步的問題或需求,歡迎隨時提出!
查看月支出圖層的本月支出列表文字佔寬度為一半,"搜尋框"及"送出"按鈕,寬度改為相同,各為四分之一
思考 花的時間為 8 秒
了解,我將協助你調整「查看月出支」浮層的布局,使得「本月支出列表」標題佔據一半的寬度,而「搜尋框」及「送出」按鈕各佔四分之一的寬度。同時,移除「套用篩選」及「重置篩選」按鈕,並調整關閉浮層的方式為點擊浮層外部區域即可關閉。
以下是更新後的 Index.html,已根據你的需求進行調整:
Index.html<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
/* 手機友善基礎樣式 */
body {
font-family: sans-serif;
margin: 10px;
}
h1 {
text-align: center;
margin-bottom: 0;
}
.sub-title {
text-align: center;
color: #666;
margin-top: 4px;
margin-bottom: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.input-row {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
.input-row .input-field {
flex: 0 0 48%;
}
label {
display: block;
font-weight: bold;
margin-bottom: 4px;
}
input, select {
width: 100%;
box-sizing: border-box;
padding: 8px;
font-size: 16px;
}
button {
width: 100%;
padding: 10px;
font-size: 16px;
background: #4CAF50;
color: #fff;
border: none;
cursor: pointer;
margin-top: 10px;
border-radius: 4px;
}
button:hover {
background: #45a049;
}
/* 預算表 */
.budget-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.budget-table th, .budget-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.warning-normal {
background-color: #ffffff;
}
.warning-near {
background-color: #fff8db; /* 淡黃色 */
}
.warning-over {
background-color: #ffdcdc; /* 淡紅色 */
}
.message {
margin-top: 8px;
text-align: center;
}
/* 浮層 (Modal) 樣式 */
.modal-overlay {
position: fixed;
z-index: 999; /* 確保浮層在最上層 */
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景 */
display: none; /* 初始隱藏 */
justify-content: center;
align-items: center;
}
.modal-content {
background: #fff;
padding: 20px;
border-radius: 8px;
width: 95%;
max-width: 800px;
max-height: 90vh;
overflow-y: auto;
position: relative;
}
/* 調整後的模態框標題 */
.modal-header {
display: flex;
width: 100%;
align-items: center;
margin-bottom: 15px;
}
.modal-header h2 {
flex: 0 0 50%; /* 佔據一半寬度 */
text-align: left;
margin: 0;
font-size: 20px;
}
/* 搜尋框和送出按鈕調整 */
.search-container {
display: flex;
align-items: center;
flex: 0 0 50%; /* 佔據剩餘的一半寬度 */
}
.search-container input[type="text"] {
flex: 0 0 50%; /* 佔據四分之一總寬度 */
padding: 6px;
font-size: 14px;
}
.search-container button {
flex: 0 0 50%; /* 佔據四分之一總寬度 */
padding: 6px 12px;
margin-left: 5px;
font-size: 14px;
background: #2196F3;
border-radius: 4px;
}
.search-container button:hover {
background: #0b7dda;
}
.expense-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
}
.expense-table th, .expense-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.sort-btn, .export-btn {
background: #2196F3;
color: #fff;
padding: 8px 12px;
border: none;
cursor: pointer;
border-radius: 4px;
margin-right: 10px;
margin-bottom: 10px;
}
.sort-btn:hover, .export-btn:hover {
background: #0b7dda;
}
/* 滾動條樣式(選用) */
.modal-content::-webkit-scrollbar {
width: 8px;
}
.modal-content::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.2);
border-radius: 4px;
}
</style>
</head>
<body>
<h1>簡易記帳工具</h1>
<div class="sub-title">Google Apps Script + 試算表</div>
<div class="container">
<!-- 新增交易表單 -->
<div class="input-row">
<!-- 類型 -->
<div class="input-field">
<label for="type">類型</label>
<select id="type" onchange="filterCategoryOptions()">
<option value="支出" selected>支出</option>
<option value="收入">收入</option>
</select>
</div>
<!-- 類別 -->
<div class="input-field">
<label for="category">類別</label>
<select id="category"></select>
</div>
</div>
<div class="input-row">
<!-- 金額 -->
<div class="input-field">
<label for="amount">金額</label>
<input type="number" id="amount" step="0.01" />
</div>
<!-- 備註 -->
<div class="input-field">
<label for="note">備註</label>
<input type="text" id="note" />
</div>
</div>
<!-- 日期與時間 -->
<div class="input-field">
<label for="dateTime">日期與時間</label>
<input type="datetime-local" id="dateTime" />
</div>
<button onclick="submitForm()">新增交易</button>
<div class="message" id="msgBox"></div>
<!-- 查看月出支按鈕 -->
<button style="background:#f44336; margin-top:20px;" onclick="viewMonthlyExpenses()">
查看月出支
</button>
<!-- 預算表 -->
<table class="budget-table" id="budgetTable">
<thead>
<tr>
<th>類別</th>
<th>預算</th>
<th>本月已用</th>
</tr>
</thead>
<tbody id="budgetTableBody"></tbody>
</table>
</div>
<!-- 浮層 (Modal) 內容 -->
<div class="modal-overlay" id="monthlyExpensesModal">
<div class="modal-content">
<div class="modal-header">
<h2>本月支出列表</h2>
<div class="search-container">
<input type="text" id="searchKeyword" placeholder="搜尋關鍵字(類別或備註)" />
<button onclick="applyFilters()">送出</button>
</div>
</div>
<!-- 月份和年份選擇區域 -->
<div style="margin-bottom: 15px; display: flex; justify-content: space-between;">
<div class="input-field" style="flex: 0 0 48%;">
<label for="selectMonth">選擇月份</label>
<select id="selectMonth" onchange="viewMonthlyExpenses()">
<!-- 月份選項由 JavaScript 動態生成 -->
</select>
</div>
<div class="input-field" style="flex: 0 0 48%;">
<label for="selectYear">選擇年份</label>
<select id="selectYear" onchange="viewMonthlyExpenses()">
<!-- 年份選項由 JavaScript 動態生成 -->
</select>
</div>
</div>
<!-- 排序及匯出按鈕 -->
<div style="margin-bottom: 15px;">
<button class="sort-btn" onclick="sortExpensesByCategory()">
按「類別」排序
</button>
<button class="export-btn" onclick="exportToCSV()">
匯出為 CSV
</button>
</div>
<!-- 支出表格 -->
<table class="expense-table" id="expenseTable">
<thead>
<tr>
<th>日期時間</th>
<th>類別</th>
<th>金額</th>
<th>備註</th>
</tr>
</thead>
<tbody id="expenseTableBody"></tbody>
</table>
<!-- 總計 -->
<div id="totalAmount" style="text-align:right; font-weight:bold;"></div>
</div>
</div>
<script>
/********************************************
* 前端初始化:載入「分類清單」與「預算表」
********************************************/
document.addEventListener('DOMContentLoaded', function() {
setDefaultDateTime();
populateMonthYearSelectors();
// 載入分類
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
// 載入預算
refreshDashboard();
});
// 設定「現在時間」到 datetime-local
function setDefaultDateTime() {
const now = new Date();
const localISO = new Date(now.getTime() - now.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16); // 'YYYY-MM-DDTHH:MM'
document.getElementById('dateTime').value = localISO;
}
// 填充分類下拉
function populateCategorySelect(categories) {
const categorySelect = document.getElementById('category');
categorySelect.innerHTML = '';
// 根據選擇的類型篩選類別
const selectedType = document.getElementById('type').value;
categories.forEach(function(cat) {
if (cat.type === selectedType) {
const option = document.createElement('option');
option.value = cat.name;
option.text = cat.name;
categorySelect.appendChild(option);
}
});
}
// 根據「類型」篩選「類別」選項
function filterCategoryOptions() {
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
}
// 新增交易
function submitForm() {
const dateTime = document.getElementById('dateTime').value;
const type = document.getElementById('type').value;
const category = document.getElementById('category').value;
const amount = document.getElementById('amount').value;
const note = document.getElementById('note').value;
// 簡易驗證
if (!dateTime || !type || !category || !amount) {
showMessage('請填寫完整資訊', true);
return;
}
const transaction = {
dateTime: dateTime,
type: type,
category: category,
amount: amount,
note: note
};
google.script.run
.withSuccessHandler(function(msg) {
showMessage(msg, false);
setDefaultDateTime();
document.getElementById('amount').value = '';
document.getElementById('note').value = '';
// 刷新預算
refreshDashboard();
})
.withFailureHandler(function(err) {
showMessage('寫入失敗: ' + err.message, true);
})
.saveTransaction(transaction);
}
// 顯示訊息
function showMessage(msg, isError) {
const msgBox = document.getElementById('msgBox');
msgBox.style.color = isError ? 'red' : 'green';
msgBox.textContent = msg;
}
// 刷新預算表
function refreshDashboard() {
google.script.run
.withSuccessHandler(function(dashboardData){
populateBudgetTable(dashboardData);
})
.getDashboardData();
}
function populateBudgetTable(data) {
const tbody = document.getElementById('budgetTableBody');
tbody.innerHTML = '';
data.forEach(function(item){
const tr = document.createElement('tr');
if (item.warningLevel === 'over') {
tr.className = 'warning-over';
} else if (item.warningLevel === 'near') {
tr.className = 'warning-near';
} else {
tr.className = 'warning-normal';
}
const tdCat = document.createElement('td');
tdCat.textContent = item.category;
const tdBudget = document.createElement('td');
tdBudget.textContent = item.budget || 0;
const tdSpent = document.createElement('td');
tdSpent.textContent = item.spent;
tr.appendChild(tdCat);
tr.appendChild(tdBudget);
tr.appendChild(tdSpent);
tbody.appendChild(tr);
});
}
/********************************************
* 新增:查看「月出支」按鈕的功能
********************************************/
let monthlyExpenses = []; // 用於儲存當月支出資料
let filteredExpenses = []; // 用於儲存過濾後的支出資料
// 1) 填充月份和年份選擇器
function populateMonthYearSelectors() {
const selectMonth = document.getElementById('selectMonth');
const selectYear = document.getElementById('selectYear');
// 填充月份選項
const months = [
'一月', '二月', '三月', '四月', '五月', '六月',
'七月', '八月', '九月', '十月', '十一月', '十二月'
];
months.forEach(function(month, index) {
const option = document.createElement('option');
option.value = index; // 0-11
option.text = month;
selectMonth.appendChild(option);
});
// 填充年份選項(例如,從 2020 到當前年份 + 5)
const currentYear = new Date().getFullYear();
for(let year = currentYear - 5; year <= currentYear + 5; year++) {
const option = document.createElement('option');
option.value = year;
option.text = year;
selectYear.appendChild(option);
}
// 設定預設選擇為當前年月
selectMonth.value = new Date().getMonth(); // 0-11
selectYear.value = currentYear;
}
// 2) 取得指定月份支出 -> 顯示浮層
function viewMonthlyExpenses() {
const selectMonth = document.getElementById('selectMonth').value;
const selectYear = document.getElementById('selectYear').value;
google.script.run
.withSuccessHandler(function(expenses){
if (Array.isArray(expenses)) {
monthlyExpenses = expenses;
filteredExpenses = expenses; // 初始化為全部
applyFilters(); // 套用初始篩選(僅關鍵字)
renderMonthlyExpenses();
document.getElementById('monthlyExpensesModal').style.display = 'flex';
} else {
alert('取得的支出資料格式錯誤。');
}
})
.withFailureHandler(function(err){
alert('取得支出失敗:' + err.message);
})
.getExpenses(parseInt(selectMonth), parseInt(selectYear));
}
// 3) 渲染當月支出的表格
function renderMonthlyExpenses() {
const tbody = document.getElementById('expenseTableBody');
tbody.innerHTML = '';
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 4;
td.textContent = '本月尚無符合條件的支出紀錄。';
tr.appendChild(td);
tbody.appendChild(tr);
document.getElementById('totalAmount').textContent = '總計:0';
return;
}
// 計算總計
let total = 0;
filteredExpenses.forEach(function(item){
total += parseFloat(item.amount) || 0;
const tr = document.createElement('tr');
const tdDateTime = document.createElement('td');
tdDateTime.textContent = item.dateTime; // 格式化後的日期時間
const tdCategory = document.createElement('td');
tdCategory.textContent = item.category;
const tdAmount = document.createElement('td');
tdAmount.textContent = item.amount;
const tdNote = document.createElement('td');
tdNote.textContent = item.note;
tr.appendChild(tdDateTime);
tr.appendChild(tdCategory);
tr.appendChild(tdAmount);
tr.appendChild(tdNote);
tbody.appendChild(tr);
});
// 顯示總計
document.getElementById('totalAmount').textContent = '總計:' + total;
}
// 4) 按類別排序 (升冪) 後重新渲染
function sortExpensesByCategory() {
if (!Array.isArray(filteredExpenses)) return;
filteredExpenses.sort(function(a, b){
return a.category.localeCompare(b.category, 'zh-TW');
});
renderMonthlyExpenses();
}
// 5) 關閉浮層
function closeMonthlyExpensesModal() {
document.getElementById('monthlyExpensesModal').style.display = 'none';
}
// 6) 套用篩選(僅關鍵字)
function applyFilters() {
const keyword = document.getElementById('searchKeyword').value.trim().toLowerCase();
filteredExpenses = monthlyExpenses.filter(function(expense){
let matchesKeyword = true;
// 檢查關鍵字(類別或備註)
if (keyword) {
const categoryMatch = expense.category.toLowerCase().includes(keyword);
const noteMatch = expense.note.toLowerCase().includes(keyword);
matchesKeyword = categoryMatch || noteMatch;
}
return matchesKeyword;
});
renderMonthlyExpenses();
}
// 7) 匯出為 CSV
function exportToCSV() {
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
alert('沒有可匯出的資料。');
return;
}
// 構建 CSV 資料
let csvContent = "data:text/csv;charset=utf-8,";
csvContent += "日期時間,類別,金額,備註\n"; // 標題
filteredExpenses.forEach(function(item){
const row = `"${item.dateTime}","${item.category}","${item.amount}","${item.note}"`;
csvContent += row + "\n";
});
// 觸發下載
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
const now = new Date();
const timestamp = now.getFullYear() + "-" +
String(now.getMonth()+1).padStart(2, '0') + "-" +
String(now.getDate()).padStart(2, '0') + "_" +
String(now.getHours()).padStart(2, '0') + "-" +
String(now.getMinutes()).padStart(2, '0') + "-" +
String(now.getSeconds()).padStart(2, '0');
link.setAttribute("download", `本月支出_${timestamp}.csv`);
document.body.appendChild(link); // Required for FF
link.click(); // This will download the data file named "本月支出_timestamp.csv"
document.body.removeChild(link);
}
/********************************************
* 新增:點擊浮層外部關閉浮層
********************************************/
window.onclick = function(event) {
const modal = document.getElementById('monthlyExpensesModal');
if (event.target === modal) {
modal.style.display = 'none';
}
}
</script>
</body>
</html>
HTML 結構調整:
.modal-header 中,將標題 <h2> 設定為佔據一半的寬度 (flex: 0 0 50%)。.search-container,包含一個搜尋輸入框和一個「送出」按鈕,佔據剩餘的半寬度 (flex: 0 0 50%)。.search-container 中,將搜尋輸入框和送出按鈕各自設定為佔據四分之一總寬度 (flex: 0 0 50%),即每個佔據總寬度的 25%。CSS 調整:
.modal-header 以使用 flex 佈局,確保標題和搜尋容器能夠正確佈局。.search-container 中,使用 flex 佈局,使搜尋輸入框和送出按鈕能夠並排顯示且具有相同的寬度。border-radius) 以提升美觀度。HTML 結構調整:
<div> 區塊。JavaScript 調整:
applyFilters 函式,使其僅根據關鍵字進行過濾,不再處理日期篩選。HTML 結構調整:
JavaScript 調整:
window.onclick 事件監聽器,當使用者點擊 .modal-overlay(浮層背景)時,自動關閉浮層。filteredExpenses 陣列進行排序,並重新渲染表格。filteredExpenses 資料匯出為 CSV 檔案。樣式調整:
防錯處理:
更新 Code.gs:
Code.gs 已包含前述步驟中的所有功能,特別是 getExpenses(month, year) 函式。Code.gs,確認是否需要進一步調整以配合新的前端功能。更新 Index.html:
Index.html 替換為上述提供的完整 HTML 內容。重新部署應用程式:
測試功能:
Transactions 工作表中。修正與優化:
搜尋功能無法正常運作:
applyFilters 函式被正確呼叫。applyFilters 或 renderMonthlyExpenses 函式中。monthlyExpenses 和 filteredExpenses 陣列中的每個物件包含 dateTime、category、amount 和 note 屬性。點擊浮層外部無法關閉浮層:
window.onclick 事件監聽器是否正確設置:<script> 標籤內新增的 window.onclick 函式能夠正確識別點擊事件。.modal-overlay 的 display 屬性在開啟和關閉時被正確設置。匯出功能無法下載 CSV 檔案:
exportToCSV 函式被正確呼叫:exportToCSV 函式。filteredExpenses 陣列中的資料正確,並且所有欄位都被正確地轉換為 CSV 格式。月份和年份選擇器無法正常填充或顯示:
populateMonthYearSelectors 函式被正確呼叫:DOMContentLoaded 事件中呼叫了 populateMonthYearSelectors 函式。populateMonthYearSelectors 或相關函式中。切換月份和年份後,支出紀錄未正確顯示:
viewMonthlyExpenses 函式被正確呼叫:viewMonthlyExpenses 函式被觸發並且呼叫了後端的 getExpenses 函式。getExpenses 函式能正確回傳指定月份和年份的支出紀錄。按類別排序功能無法正常運作:
sortExpensesByCategory 函式被正確呼叫:sortExpensesByCategory 函式。filteredExpenses 陣列中的資料能夠正確排序,並且 renderMonthlyExpenses 函式能夠正確重新渲染表格。新增交易後,預算表未更新:
submitForm 函式中 refreshDashboard 被正確呼叫:refreshDashboard 函式被呼叫以更新預算表。getDashboardData 函式:getDashboardData 函式能正確計算每個類別的預算和支出,並正確設定 warningLevel。日期時間顯示錯誤:
datetime-local 欄位正確選擇日期與時間。getExpenses 函式的日期格式化:Utilities.formatDate 將日期格式化為 yyyy/MM/dd HH:mm,以便前端能正確顯示。類別選單未正確更新:
filterCategoryOptions 函式被正確呼叫:filterCategoryOptions 函式被觸發,並且重新載入「類別」選單。Categories 工作表中的資料:透過以上的修改,你的記帳工具將具備以下新增功能:
搜尋框位置調整與送出按鈕:
移除「X」關閉按鈕並實現點擊浮層外部關閉功能:
移除「套用篩選」及「重置篩選」按鈕:
保留並優化其他功能:
整體介面優化:
防錯處理與使用者體驗提升:
請按照上述步驟進行更新與部署,並在使用過程中進行測試以確保所有功能正常運作。如有任何進一步的問題或需求,歡迎隨時提出!
本月支出列表圖層最上方加上"X"(關閉)按鈕,寬度為100%,按下把圖層關閉
思考 花的時間為 14 秒
了解,我將協助你在「本月支出列表」的浮層中新增一個全寬的「X」關閉按鈕,並確保其功能正常。以下是更新後的完整 Index.html,包含所需的修改:
Index.html<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
/* 手機友善基礎樣式 */
body {
font-family: sans-serif;
margin: 10px;
}
h1 {
text-align: center;
margin-bottom: 0;
}
.sub-title {
text-align: center;
color: #666;
margin-top: 4px;
margin-bottom: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.input-row {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
.input-row .input-field {
flex: 0 0 48%;
}
label {
display: block;
font-weight: bold;
margin-bottom: 4px;
}
input, select {
width: 100%;
box-sizing: border-box;
padding: 8px;
font-size: 16px;
}
button {
width: 100%;
padding: 10px;
font-size: 16px;
background: #4CAF50;
color: #fff;
border: none;
cursor: pointer;
margin-top: 10px;
border-radius: 4px;
}
button:hover {
background: #45a049;
}
/* 預算表 */
.budget-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.budget-table th, .budget-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.warning-normal {
background-color: #ffffff;
}
.warning-near {
background-color: #fff8db; /* 淡黃色 */
}
.warning-over {
background-color: #ffdcdc; /* 淡紅色 */
}
.message {
margin-top: 8px;
text-align: center;
}
/* 浮層 (Modal) 樣式 */
.modal-overlay {
position: fixed;
z-index: 999; /* 確保浮層在最上層 */
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景 */
display: none; /* 初始隱藏 */
justify-content: center;
align-items: center;
}
.modal-content {
background: #fff;
padding: 20px;
border-radius: 8px;
width: 95%;
max-width: 800px;
max-height: 90vh;
overflow-y: auto;
position: relative;
}
/* 新增的全寬關閉按鈕 */
.modal-close-btn {
width: 100%;
background: #f44336;
color: #fff;
border: none;
padding: 10px 0;
font-size: 16px;
cursor: pointer;
border-radius: 4px;
margin-bottom: 15px;
}
.modal-close-btn:hover {
background: #da190b;
}
/* 調整後的模態框標題 */
.modal-header {
display: flex;
width: 100%;
align-items: center;
margin-bottom: 15px;
}
.modal-header h2 {
flex: 0 0 50%; /* 佔據一半寬度 */
text-align: left;
margin: 0;
font-size: 20px;
}
/* 搜尋框和送出按鈕調整 */
.search-container {
display: flex;
align-items: center;
flex: 0 0 50%; /* 佔據剩餘的一半寬度 */
}
.search-container input[type="text"] {
flex: 0 0 50%; /* 佔據四分之一總寬度 */
padding: 6px;
font-size: 14px;
}
.search-container button {
flex: 0 0 50%; /* 佔據四分之一總寬度 */
padding: 6px 12px;
margin-left: 5px;
font-size: 14px;
background: #2196F3;
border-radius: 4px;
}
.search-container button:hover {
background: #0b7dda;
}
.expense-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
}
.expense-table th, .expense-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.sort-btn, .export-btn {
background: #2196F3;
color: #fff;
padding: 8px 12px;
border: none;
cursor: pointer;
border-radius: 4px;
margin-right: 10px;
margin-bottom: 10px;
}
.sort-btn:hover, .export-btn:hover {
background: #0b7dda;
}
/* 滾動條樣式(選用) */
.modal-content::-webkit-scrollbar {
width: 8px;
}
.modal-content::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.2);
border-radius: 4px;
}
</style>
</head>
<body>
<h1>簡易記帳工具</h1>
<div class="sub-title">Google Apps Script + 試算表</div>
<div class="container">
<!-- 新增交易表單 -->
<div class="input-row">
<!-- 類型 -->
<div class="input-field">
<label for="type">類型</label>
<select id="type" onchange="filterCategoryOptions()">
<option value="支出" selected>支出</option>
<option value="收入">收入</option>
</select>
</div>
<!-- 類別 -->
<div class="input-field">
<label for="category">類別</label>
<select id="category"></select>
</div>
</div>
<div class="input-row">
<!-- 金額 -->
<div class="input-field">
<label for="amount">金額</label>
<input type="number" id="amount" step="0.01" />
</div>
<!-- 備註 -->
<div class="input-field">
<label for="note">備註</label>
<input type="text" id="note" />
</div>
</div>
<!-- 日期與時間 -->
<div class="input-field">
<label for="dateTime">日期與時間</label>
<input type="datetime-local" id="dateTime" />
</div>
<button onclick="submitForm()">新增交易</button>
<div class="message" id="msgBox"></div>
<!-- 查看月出支按鈕 -->
<button style="background:#f44336; margin-top:20px;" onclick="viewMonthlyExpenses()">
查看月出支
</button>
<!-- 預算表 -->
<table class="budget-table" id="budgetTable">
<thead>
<tr>
<th>類別</th>
<th>預算</th>
<th>本月已用</th>
</tr>
</thead>
<tbody id="budgetTableBody"></tbody>
</table>
</div>
<!-- 浮層 (Modal) 內容 -->
<div class="modal-overlay" id="monthlyExpensesModal">
<div class="modal-content">
<!-- 新增的全寬關閉按鈕 -->
<button class="modal-close-btn" onclick="closeMonthlyExpensesModal()">X</button>
<div class="modal-header">
<h2>本月支出列表</h2>
<div class="search-container">
<input type="text" id="searchKeyword" placeholder="搜尋關鍵字(類別或備註)" />
<button onclick="applyFilters()">送出</button>
</div>
</div>
<!-- 月份和年份選擇區域 -->
<div style="margin-bottom: 15px; display: flex; justify-content: space-between;">
<div class="input-field" style="flex: 0 0 48%;">
<label for="selectMonth">選擇月份</label>
<select id="selectMonth" onchange="viewMonthlyExpenses()">
<!-- 月份選項由 JavaScript 動態生成 -->
</select>
</div>
<div class="input-field" style="flex: 0 0 48%;">
<label for="selectYear">選擇年份</label>
<select id="selectYear" onchange="viewMonthlyExpenses()">
<!-- 年份選項由 JavaScript 動態生成 -->
</select>
</div>
</div>
<!-- 排序及匯出按鈕 -->
<div style="margin-bottom: 15px;">
<button class="sort-btn" onclick="sortExpensesByCategory()">
按「類別」排序
</button>
<button class="export-btn" onclick="exportToCSV()">
匯出為 CSV
</button>
</div>
<!-- 支出表格 -->
<table class="expense-table" id="expenseTable">
<thead>
<tr>
<th>日期時間</th>
<th>類別</th>
<th>金額</th>
<th>備註</th>
</tr>
</thead>
<tbody id="expenseTableBody"></tbody>
</table>
<!-- 總計 -->
<div id="totalAmount" style="text-align:right; font-weight:bold;"></div>
</div>
</div>
<script>
/********************************************
* 前端初始化:載入「分類清單」與「預算表」
********************************************/
document.addEventListener('DOMContentLoaded', function() {
setDefaultDateTime();
populateMonthYearSelectors();
// 載入分類
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
// 載入預算
refreshDashboard();
});
// 設定「現在時間」到 datetime-local
function setDefaultDateTime() {
const now = new Date();
const localISO = new Date(now.getTime() - now.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16); // 'YYYY-MM-DDTHH:MM'
document.getElementById('dateTime').value = localISO;
}
// 填充分類下拉
function populateCategorySelect(categories) {
const categorySelect = document.getElementById('category');
categorySelect.innerHTML = '';
// 根據選擇的類型篩選類別
const selectedType = document.getElementById('type').value;
categories.forEach(function(cat) {
if (cat.type === selectedType) {
const option = document.createElement('option');
option.value = cat.name;
option.text = cat.name;
categorySelect.appendChild(option);
}
});
}
// 根據「類型」篩選「類別」選項
function filterCategoryOptions() {
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
}
// 新增交易
function submitForm() {
const dateTime = document.getElementById('dateTime').value;
const type = document.getElementById('type').value;
const category = document.getElementById('category').value;
const amount = document.getElementById('amount').value;
const note = document.getElementById('note').value;
// 簡易驗證
if (!dateTime || !type || !category || !amount) {
showMessage('請填寫完整資訊', true);
return;
}
const transaction = {
dateTime: dateTime,
type: type,
category: category,
amount: amount,
note: note
};
google.script.run
.withSuccessHandler(function(msg) {
showMessage(msg, false);
setDefaultDateTime();
document.getElementById('amount').value = '';
document.getElementById('note').value = '';
// 刷新預算
refreshDashboard();
})
.withFailureHandler(function(err) {
showMessage('寫入失敗: ' + err.message, true);
})
.saveTransaction(transaction);
}
// 顯示訊息
function showMessage(msg, isError) {
const msgBox = document.getElementById('msgBox');
msgBox.style.color = isError ? 'red' : 'green';
msgBox.textContent = msg;
}
// 刷新預算表
function refreshDashboard() {
google.script.run
.withSuccessHandler(function(dashboardData){
populateBudgetTable(dashboardData);
})
.getDashboardData();
}
function populateBudgetTable(data) {
const tbody = document.getElementById('budgetTableBody');
tbody.innerHTML = '';
data.forEach(function(item){
const tr = document.createElement('tr');
if (item.warningLevel === 'over') {
tr.className = 'warning-over';
} else if (item.warningLevel === 'near') {
tr.className = 'warning-near';
} else {
tr.className = 'warning-normal';
}
const tdCat = document.createElement('td');
tdCat.textContent = item.category;
const tdBudget = document.createElement('td');
tdBudget.textContent = item.budget || 0;
const tdSpent = document.createElement('td');
tdSpent.textContent = item.spent;
tr.appendChild(tdCat);
tr.appendChild(tdBudget);
tr.appendChild(tdSpent);
tbody.appendChild(tr);
});
}
/********************************************
* 新增:查看「月出支」按鈕的功能
********************************************/
let monthlyExpenses = []; // 用於儲存當月支出資料
let filteredExpenses = []; // 用於儲存過濾後的支出資料
// 1) 填充月份和年份選擇器
function populateMonthYearSelectors() {
const selectMonth = document.getElementById('selectMonth');
const selectYear = document.getElementById('selectYear');
// 填充月份選項
const months = [
'一月', '二月', '三月', '四月', '五月', '六月',
'七月', '八月', '九月', '十月', '十一月', '十二月'
];
months.forEach(function(month, index) {
const option = document.createElement('option');
option.value = index; // 0-11
option.text = month;
selectMonth.appendChild(option);
});
// 填充年份選項(例如,從 2020 到當前年份 + 5)
const currentYear = new Date().getFullYear();
for(let year = currentYear - 5; year <= currentYear + 5; year++) {
const option = document.createElement('option');
option.value = year;
option.text = year;
selectYear.appendChild(option);
}
// 設定預設選擇為當前年月
selectMonth.value = new Date().getMonth(); // 0-11
selectYear.value = currentYear;
}
// 2) 取得指定月份支出 -> 顯示浮層
function viewMonthlyExpenses() {
const selectMonth = document.getElementById('selectMonth').value;
const selectYear = document.getElementById('selectYear').value;
google.script.run
.withSuccessHandler(function(expenses){
if (Array.isArray(expenses)) {
monthlyExpenses = expenses;
filteredExpenses = expenses; // 初始化為全部
applyFilters(); // 套用初始篩選(僅關鍵字)
renderMonthlyExpenses();
document.getElementById('monthlyExpensesModal').style.display = 'flex';
} else {
alert('取得的支出資料格式錯誤。');
}
})
.withFailureHandler(function(err){
alert('取得支出失敗:' + err.message);
})
.getExpenses(parseInt(selectMonth), parseInt(selectYear));
}
// 3) 渲染當月支出的表格
function renderMonthlyExpenses() {
const tbody = document.getElementById('expenseTableBody');
tbody.innerHTML = '';
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 4;
td.textContent = '本月尚無符合條件的支出紀錄。';
tr.appendChild(td);
tbody.appendChild(tr);
document.getElementById('totalAmount').textContent = '總計:0';
return;
}
// 計算總計
let total = 0;
filteredExpenses.forEach(function(item){
total += parseFloat(item.amount) || 0;
const tr = document.createElement('tr');
const tdDateTime = document.createElement('td');
tdDateTime.textContent = item.dateTime; // 格式化後的日期時間
const tdCategory = document.createElement('td');
tdCategory.textContent = item.category;
const tdAmount = document.createElement('td');
tdAmount.textContent = item.amount;
const tdNote = document.createElement('td');
tdNote.textContent = item.note;
tr.appendChild(tdDateTime);
tr.appendChild(tdCategory);
tr.appendChild(tdAmount);
tr.appendChild(tdNote);
tbody.appendChild(tr);
});
// 顯示總計
document.getElementById('totalAmount').textContent = '總計:' + total;
}
// 4) 按類別排序 (升冪) 後重新渲染
function sortExpensesByCategory() {
if (!Array.isArray(filteredExpenses)) return;
filteredExpenses.sort(function(a, b){
return a.category.localeCompare(b.category, 'zh-TW');
});
renderMonthlyExpenses();
}
// 5) 關閉浮層
function closeMonthlyExpensesModal() {
document.getElementById('monthlyExpensesModal').style.display = 'none';
}
// 6) 套用篩選(僅關鍵字)
function applyFilters() {
const keyword = document.getElementById('searchKeyword').value.trim().toLowerCase();
filteredExpenses = monthlyExpenses.filter(function(expense){
let matchesKeyword = true;
// 檢查關鍵字(類別或備註)
if (keyword) {
const categoryMatch = expense.category.toLowerCase().includes(keyword);
const noteMatch = expense.note.toLowerCase().includes(keyword);
matchesKeyword = categoryMatch || noteMatch;
}
return matchesKeyword;
});
renderMonthlyExpenses();
}
// 7) 匯出為 CSV
function exportToCSV() {
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
alert('沒有可匯出的資料。');
return;
}
// 构建 CSV 資料
let csvContent = "data:text/csv;charset=utf-8,";
csvContent += "日期時間,類別,金額,備註\n"; // 標題
filteredExpenses.forEach(function(item){
const row = `"${item.dateTime}","${item.category}","${item.amount}","${item.note}"`;
csvContent += row + "\n";
});
// 觸發下載
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
const now = new Date();
const timestamp = now.getFullYear() + "-" +
String(now.getMonth()+1).padStart(2, '0') + "-" +
String(now.getDate()).padStart(2, '0') + "_" +
String(now.getHours()).padStart(2, '0') + "-" +
String(now.getMinutes()).padStart(2, '0') + "-" +
String(now.getSeconds()).padStart(2, '0');
link.setAttribute("download", `本月支出_${timestamp}.csv`);
document.body.appendChild(link); // Required for FF
link.click(); // This will download the data file named "本月支出_timestamp.csv"
document.body.removeChild(link);
}
/********************************************
* 新增:點擊浮層外部關閉浮層
********************************************/
window.onclick = function(event) {
const modal = document.getElementById('monthlyExpensesModal');
if (event.target === modal) {
modal.style.display = 'none';
}
}
</script>
</body>
</html>
HTML 結構調整:
.modal-content 的最上方新增一個 <button> 元素,類別為 modal-close-btn,內容為「X」。onclick 屬性設置為 closeMonthlyExpensesModal(),以觸發關閉浮層的功能。<!-- 新增的全寬關閉按鈕 -->
<button class="modal-close-btn" onclick="closeMonthlyExpensesModal()">X</button>
CSS 調整:
.modal-close-btn 類別,使其佔據浮層的全部寬度(100%),並具有明顯的視覺效果。/* 新增的全寬關閉按鈕 */
.modal-close-btn {
width: 100%;
background: #f44336;
color: #fff;
border: none;
padding: 10px 0;
font-size: 16px;
cursor: pointer;
border-radius: 4px;
margin-bottom: 15px;
}
.modal-close-btn:hover {
background: #da190b;
}
HTML 結構:
.modal-header 內的 .search-container 中,並保持原有的佈局比例。CSS 調整:
.search-container 設定為 flex: 0 0 50%,以配合 .modal-header 中標題佔據的 50% 寬度。flex: 0 0 50%,即佔據各自的 25% 寬度(相對於總寬度的 50%)。/* 搜尋框和送出按鈕調整 */
.search-container {
display: flex;
align-items: center;
flex: 0 0 50%; /* 佔據剩餘的一半寬度 */
}
.search-container input[type="text"] {
flex: 0 0 50%; /* 佔據四分之一總寬度 */
padding: 6px;
font-size: 14px;
}
.search-container button {
flex: 0 0 50%; /* 佔據四分之一總寬度 */
padding: 6px 12px;
margin-left: 5px;
font-size: 14px;
background: #2196F3;
border-radius: 4px;
}
.search-container button:hover {
background: #0b7dda;
}
JavaScript 調整:
window.onclick 事件監聽器,用於監測使用者是否點擊了浮層外部區域(即 .modal-overlay)。/********************************************
* 新增:點擊浮層外部關閉浮層
********************************************/
window.onclick = function(event) {
const modal = document.getElementById('monthlyExpensesModal');
if (event.target === modal) {
modal.style.display = 'none';
}
}
HTML 結構:
CSS 調整:
更新 Code.gs:
Code.gs 已包含之前步驟中的所有功能,特別是 getExpenses(month, year) 函式。Code.gs,確認是否需要進一步調整以配合新的前端功能。更新 Index.html:
Index.html 替換為上述提供的完整 HTML 內容。重新部署應用程式:
測試功能:
Transactions 工作表中。修正與優化:
「X」關閉按鈕無法正常關閉浮層:
確認 closeMonthlyExpensesModal 函式是否正確定義:
<script> 中存在 closeMonthlyExpensesModal 函式,且其功能為隱藏浮層。// 5) 關閉浮層
function closeMonthlyExpensesModal() {
document.getElementById('monthlyExpensesModal').style.display = 'none';
}
檢查按鈕的 onclick 屬性是否正確指向函式:
onclick="closeMonthlyExpensesModal()" 沒有拼寫錯誤。搜尋功能無法正常運作:
applyFilters 函式被正確呼叫。applyFilters 或 renderMonthlyExpenses 函式中。monthlyExpenses 和 filteredExpenses 陣列中的每個物件包含 dateTime、category、amount 和 note 屬性。點擊浮層外部無法關閉浮層:
window.onclick 事件監聽器是否正確設置:<script> 標籤內新增的 window.onclick 函式能夠正確識別點擊事件。.modal-overlay 的 display 屬性在開啟和關閉時被正確設置。匯出功能無法下載 CSV 檔案:
exportToCSV 函式被正確呼叫:exportToCSV 函式。filteredExpenses 陣列中的資料正確,並且所有欄位都被正確地轉換為 CSV 格式。月份和年份選擇器無法正常填充或顯示:
populateMonthYearSelectors 函式被正確呼叫:DOMContentLoaded 事件中呼叫了 populateMonthYearSelectors 函式。populateMonthYearSelectors 或相關函式中。切換月份和年份後,支出紀錄未正確顯示:
viewMonthlyExpenses 函式被正確呼叫:viewMonthlyExpenses 函式被觸發並且呼叫了後端的 getExpenses 函式。getExpenses 函式能正確回傳指定月份和年份的支出紀錄。按類別排序功能無法正常運作:
sortExpensesByCategory 函式被正確呼叫:sortExpensesByCategory 函式。filteredExpenses 陣列中的資料能夠正確排序,並且 renderMonthlyExpenses 函式能夠正確重新渲染表格。新增交易後,預算表未更新:
submitForm 函式中 refreshDashboard 被正確呼叫:refreshDashboard 函式被呼叫以更新預算表。getDashboardData 函式:getDashboardData 函式能正確計算每個類別的預算和支出,並正確設定 warningLevel。日期時間顯示錯誤:
datetime-local 欄位正確選擇日期與時間。getExpenses 函式的日期格式化:Utilities.formatDate 將日期格式化為 yyyy/MM/dd HH:mm,以便前端能正確顯示。類別選單未正確更新:
* **確認 `filterCategoryOptions` 函式被正確呼叫**:
* 當「類型」改變時,確保 `filterCategoryOptions` 函式被觸發,並且重新載入「類別」選單。
* **檢查 `Categories` 工作表中的資料**:
* 確保所有類別的「類型」欄位正確填寫(「收入」或「支出」)。
透過以上的修改,你的記帳工具將具備以下新增功能:
新增全寬「X」關閉按鈕:
搜尋框與送出按鈕位置與寬度調整:
移除不必要的篩選按鈕:
保留並優化其他功能:
整體介面優化:
防錯處理與使用者體驗提升:
請按照上述步驟進行更新與部署,並在使用過程中進行測試以確保所有功能正常運作。如有任何進一步的問題或需求,歡迎隨時提出!
不改動其他介面及功能,每次開啟網頁,網頁背景圖都從以下網址中隨機設定https://picsum.photos/200/150?random=6139,並設為30%的透明度
思考 花的時間為 13 秒
了解,我將協助你在現有的 Index.html 中新增每次開啟網頁時隨機設定背景圖片的功能,同時將背景圖片設為 30% 的透明度。為了不改動其他介面及功能,我將僅添加必要的 HTML、CSS 和 JavaScript 代碼。
以下是更新後的完整 Index.html,包含新增的背景圖片功能:
Index.html<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
/* 手機友善基礎樣式 */
body {
font-family: sans-serif;
margin: 10px;
position: relative; /* 為了確保背景層在後 */
z-index: 1; /* 確保內容在背景層之上 */
}
h1 {
text-align: center;
margin-bottom: 0;
}
.sub-title {
text-align: center;
color: #666;
margin-top: 4px;
margin-bottom: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.input-row {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
.input-row .input-field {
flex: 0 0 48%;
}
label {
display: block;
font-weight: bold;
margin-bottom: 4px;
}
input, select {
width: 100%;
box-sizing: border-box;
padding: 8px;
font-size: 16px;
}
button {
width: 100%;
padding: 10px;
font-size: 16px;
background: #4CAF50;
color: #fff;
border: none;
cursor: pointer;
margin-top: 10px;
border-radius: 4px;
}
button:hover {
background: #45a049;
}
/* 預算表 */
.budget-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.budget-table th, .budget-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.warning-normal {
background-color: #ffffff;
}
.warning-near {
background-color: #fff8db; /* 淡黃色 */
}
.warning-over {
background-color: #ffdcdc; /* 淡紅色 */
}
.message {
margin-top: 8px;
text-align: center;
}
/* 浮層 (Modal) 樣式 */
.modal-overlay {
position: fixed;
z-index: 999; /* 確保浮層在最上層 */
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景 */
display: none; /* 初始隱藏 */
justify-content: center;
align-items: center;
}
.modal-content {
background: #fff;
padding: 20px;
border-radius: 8px;
width: 95%;
max-width: 800px;
max-height: 90vh;
overflow-y: auto;
position: relative;
z-index: 1000; /* 確保內容在背景層之上 */
}
/* 新增的全寬關閉按鈕 */
.modal-close-btn {
width: 100%;
background: #f44336;
color: #fff;
border: none;
padding: 10px 0;
font-size: 16px;
cursor: pointer;
border-radius: 4px;
margin-bottom: 15px;
}
.modal-close-btn:hover {
background: #da190b;
}
/* 調整後的模態框標題 */
.modal-header {
display: flex;
width: 100%;
align-items: center;
margin-bottom: 15px;
}
.modal-header h2 {
flex: 0 0 50%; /* 佔據一半寬度 */
text-align: left;
margin: 0;
font-size: 20px;
}
/* 搜尋框和送出按鈕調整 */
.search-container {
display: flex;
align-items: center;
flex: 0 0 50%; /* 佔據剩餘的一半寬度 */
}
.search-container input[type="text"] {
flex: 0 0 50%; /* 佔據四分之一總寬度 */
padding: 6px;
font-size: 14px;
}
.search-container button {
flex: 0 0 50%; /* 佔據四分之一總寬度 */
padding: 6px 12px;
margin-left: 5px;
font-size: 14px;
background: #2196F3;
border-radius: 4px;
}
.search-container button:hover {
background: #0b7dda;
}
.expense-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
}
.expense-table th, .expense-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.sort-btn, .export-btn {
background: #2196F3;
color: #fff;
padding: 8px 12px;
border: none;
cursor: pointer;
border-radius: 4px;
margin-right: 10px;
margin-bottom: 10px;
}
.sort-btn:hover, .export-btn:hover {
background: #0b7dda;
}
/* 背景圖片樣式 */
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
opacity: 0.3; /* 設為30%的透明度 */
z-index: 0; /* 確保在所有內容後面 */
}
/* 滾動條樣式(選用) */
.modal-content::-webkit-scrollbar {
width: 8px;
}
.modal-content::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.2);
border-radius: 4px;
}
</style>
</head>
<body>
<!-- 背景圖片容器 -->
<div id="background"></div>
<h1>簡易記帳工具</h1>
<div class="sub-title">Google Apps Script + 試算表</div>
<div class="container">
<!-- 新增交易表單 -->
<div class="input-row">
<!-- 類型 -->
<div class="input-field">
<label for="type">類型</label>
<select id="type" onchange="filterCategoryOptions()">
<option value="支出" selected>支出</option>
<option value="收入">收入</option>
</select>
</div>
<!-- 類別 -->
<div class="input-field">
<label for="category">類別</label>
<select id="category"></select>
</div>
</div>
<div class="input-row">
<!-- 金額 -->
<div class="input-field">
<label for="amount">金額</label>
<input type="number" id="amount" step="0.01" />
</div>
<!-- 備註 -->
<div class="input-field">
<label for="note">備註</label>
<input type="text" id="note" />
</div>
</div>
<!-- 日期與時間 -->
<div class="input-field">
<label for="dateTime">日期與時間</label>
<input type="datetime-local" id="dateTime" />
</div>
<button onclick="submitForm()">新增交易</button>
<div class="message" id="msgBox"></div>
<!-- 查看月出支按鈕 -->
<button style="background:#f44336; margin-top:20px;" onclick="viewMonthlyExpenses()">
查看月出支
</button>
<!-- 預算表 -->
<table class="budget-table" id="budgetTable">
<thead>
<tr>
<th>類別</th>
<th>預算</th>
<th>本月已用</th>
</tr>
</thead>
<tbody id="budgetTableBody"></tbody>
</table>
</div>
<!-- 浮層 (Modal) 內容 -->
<div class="modal-overlay" id="monthlyExpensesModal">
<div class="modal-content">
<!-- 新增的全寬關閉按鈕 -->
<button class="modal-close-btn" onclick="closeMonthlyExpensesModal()">X</button>
<div class="modal-header">
<h2>本月支出列表</h2>
<div class="search-container">
<input type="text" id="searchKeyword" placeholder="搜尋關鍵字(類別或備註)" />
<button onclick="applyFilters()">送出</button>
</div>
</div>
<!-- 月份和年份選擇區域 -->
<div style="margin-bottom: 15px; display: flex; justify-content: space-between;">
<div class="input-field" style="flex: 0 0 48%;">
<label for="selectMonth">選擇月份</label>
<select id="selectMonth" onchange="viewMonthlyExpenses()">
<!-- 月份選項由 JavaScript 動態生成 -->
</select>
</div>
<div class="input-field" style="flex: 0 0 48%;">
<label for="selectYear">選擇年份</label>
<select id="selectYear" onchange="viewMonthlyExpenses()">
<!-- 年份選項由 JavaScript 動態生成 -->
</select>
</div>
</div>
<!-- 排序及匯出按鈕 -->
<div style="margin-bottom: 15px;">
<button class="sort-btn" onclick="sortExpensesByCategory()">
按「類別」排序
</button>
<button class="export-btn" onclick="exportToCSV()">
匯出為 CSV
</button>
</div>
<!-- 支出表格 -->
<table class="expense-table" id="expenseTable">
<thead>
<tr>
<th>日期時間</th>
<th>類別</th>
<th>金額</th>
<th>備註</th>
</tr>
</thead>
<tbody id="expenseTableBody"></tbody>
</table>
<!-- 總計 -->
<div id="totalAmount" style="text-align:right; font-weight:bold;"></div>
</div>
</div>
<script>
/********************************************
* 前端初始化:載入「分類清單」與「預算表」
********************************************/
document.addEventListener('DOMContentLoaded', function() {
setRandomBackground(); // 設定隨機背景圖片
setDefaultDateTime();
populateMonthYearSelectors();
// 載入分類
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
// 載入預算
refreshDashboard();
});
// 設定「現在時間」到 datetime-local
function setDefaultDateTime() {
const now = new Date();
const localISO = new Date(now.getTime() - now.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16); // 'YYYY-MM-DDTHH:MM'
document.getElementById('dateTime').value = localISO;
}
// 填充分類下拉
function populateCategorySelect(categories) {
const categorySelect = document.getElementById('category');
categorySelect.innerHTML = '';
// 根據選擇的類型篩選類別
const selectedType = document.getElementById('type').value;
categories.forEach(function(cat) {
if (cat.type === selectedType) {
const option = document.createElement('option');
option.value = cat.name;
option.text = cat.name;
categorySelect.appendChild(option);
}
});
}
// 根據「類型」篩選「類別」選項
function filterCategoryOptions() {
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
}
// 新增交易
function submitForm() {
const dateTime = document.getElementById('dateTime').value;
const type = document.getElementById('type').value;
const category = document.getElementById('category').value;
const amount = document.getElementById('amount').value;
const note = document.getElementById('note').value;
// 簡易驗證
if (!dateTime || !type || !category || !amount) {
showMessage('請填寫完整資訊', true);
return;
}
const transaction = {
dateTime: dateTime,
type: type,
category: category,
amount: amount,
note: note
};
google.script.run
.withSuccessHandler(function(msg) {
showMessage(msg, false);
setDefaultDateTime();
document.getElementById('amount').value = '';
document.getElementById('note').value = '';
// 刷新預算
refreshDashboard();
})
.withFailureHandler(function(err) {
showMessage('寫入失敗: ' + err.message, true);
})
.saveTransaction(transaction);
}
// 顯示訊息
function showMessage(msg, isError) {
const msgBox = document.getElementById('msgBox');
msgBox.style.color = isError ? 'red' : 'green';
msgBox.textContent = msg;
}
// 刷新預算表
function refreshDashboard() {
google.script.run
.withSuccessHandler(function(dashboardData){
populateBudgetTable(dashboardData);
})
.getDashboardData();
}
function populateBudgetTable(data) {
const tbody = document.getElementById('budgetTableBody');
tbody.innerHTML = '';
data.forEach(function(item){
const tr = document.createElement('tr');
if (item.warningLevel === 'over') {
tr.className = 'warning-over';
} else if (item.warningLevel === 'near') {
tr.className = 'warning-near';
} else {
tr.className = 'warning-normal';
}
const tdCat = document.createElement('td');
tdCat.textContent = item.category;
const tdBudget = document.createElement('td');
tdBudget.textContent = item.budget || 0;
const tdSpent = document.createElement('td');
tdSpent.textContent = item.spent;
tr.appendChild(tdCat);
tr.appendChild(tdBudget);
tr.appendChild(tdSpent);
tbody.appendChild(tr);
});
}
/********************************************
* 新增:查看「月出支」按鈕的功能
********************************************/
let monthlyExpenses = []; // 用於儲存當月支出資料
let filteredExpenses = []; // 用於儲存過濾後的支出資料
// 1) 填充月份和年份選擇器
function populateMonthYearSelectors() {
const selectMonth = document.getElementById('selectMonth');
const selectYear = document.getElementById('selectYear');
// 填充月份選項
const months = [
'一月', '二月', '三月', '四月', '五月', '六月',
'七月', '八月', '九月', '十月', '十一月', '十二月'
];
months.forEach(function(month, index) {
const option = document.createElement('option');
option.value = index; // 0-11
option.text = month;
selectMonth.appendChild(option);
});
// 填充年份選項(例如,從 2020 到當前年份 + 5)
const currentYear = new Date().getFullYear();
for(let year = currentYear - 5; year <= currentYear + 5; year++) {
const option = document.createElement('option');
option.value = year;
option.text = year;
selectYear.appendChild(option);
}
// 設定預設選擇為當前年月
selectMonth.value = new Date().getMonth(); // 0-11
selectYear.value = currentYear;
}
// 2) 取得指定月份支出 -> 顯示浮層
function viewMonthlyExpenses() {
const selectMonth = document.getElementById('selectMonth').value;
const selectYear = document.getElementById('selectYear').value;
google.script.run
.withSuccessHandler(function(expenses){
if (Array.isArray(expenses)) {
monthlyExpenses = expenses;
filteredExpenses = expenses; // 初始化為全部
applyFilters(); // 套用初始篩選(僅關鍵字)
renderMonthlyExpenses();
document.getElementById('monthlyExpensesModal').style.display = 'flex';
} else {
alert('取得的支出資料格式錯誤。');
}
})
.withFailureHandler(function(err){
alert('取得支出失敗:' + err.message);
})
.getExpenses(parseInt(selectMonth), parseInt(selectYear));
}
// 3) 渲染當月支出的表格
function renderMonthlyExpenses() {
const tbody = document.getElementById('expenseTableBody');
tbody.innerHTML = '';
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 4;
td.textContent = '本月尚無符合條件的支出紀錄。';
tr.appendChild(td);
tbody.appendChild(tr);
document.getElementById('totalAmount').textContent = '總計:0';
return;
}
// 計算總計
let total = 0;
filteredExpenses.forEach(function(item){
total += parseFloat(item.amount) || 0;
const tr = document.createElement('tr');
const tdDateTime = document.createElement('td');
tdDateTime.textContent = item.dateTime; // 格式化後的日期時間
const tdCategory = document.createElement('td');
tdCategory.textContent = item.category;
const tdAmount = document.createElement('td');
tdAmount.textContent = item.amount;
const tdNote = document.createElement('td');
tdNote.textContent = item.note;
tr.appendChild(tdDateTime);
tr.appendChild(tdCategory);
tr.appendChild(tdAmount);
tr.appendChild(tdNote);
tbody.appendChild(tr);
});
// 顯示總計
document.getElementById('totalAmount').textContent = '總計:' + total;
}
// 4) 按類別排序 (升冪) 後重新渲染
function sortExpensesByCategory() {
if (!Array.isArray(filteredExpenses)) return;
filteredExpenses.sort(function(a, b){
return a.category.localeCompare(b.category, 'zh-TW');
});
renderMonthlyExpenses();
}
// 5) 關閉浮層
function closeMonthlyExpensesModal() {
document.getElementById('monthlyExpensesModal').style.display = 'none';
}
// 6) 套用篩選(僅關鍵字)
function applyFilters() {
const keyword = document.getElementById('searchKeyword').value.trim().toLowerCase();
filteredExpenses = monthlyExpenses.filter(function(expense){
let matchesKeyword = true;
// 檢查關鍵字(類別或備註)
if (keyword) {
const categoryMatch = expense.category.toLowerCase().includes(keyword);
const noteMatch = expense.note.toLowerCase().includes(keyword);
matchesKeyword = categoryMatch || noteMatch;
}
return matchesKeyword;
});
renderMonthlyExpenses();
}
// 7) 匯出為 CSV
function exportToCSV() {
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
alert('沒有可匯出的資料。');
return;
}
// 構建 CSV 資料
let csvContent = "data:text/csv;charset=utf-8,";
csvContent += "日期時間,類別,金額,備註\n"; // 標題
filteredExpenses.forEach(function(item){
const row = `"${item.dateTime}","${item.category}","${item.amount}","${item.note}"`;
csvContent += row + "\n";
});
// 觸發下載
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
const now = new Date();
const timestamp = now.getFullYear() + "-" +
String(now.getMonth()+1).padStart(2, '0') + "-" +
String(now.getDate()).padStart(2, '0') + "_" +
String(now.getHours()).padStart(2, '0') + "-" +
String(now.getMinutes()).padStart(2, '0') + "-" +
String(now.getSeconds()).padStart(2, '0');
link.setAttribute("download", `本月支出_${timestamp}.csv`);
document.body.appendChild(link); // Required for FF
link.click(); // This will download the data file named "本月支出_timestamp.csv"
document.body.removeChild(link);
}
/********************************************
* 新增:點擊浮層外部關閉浮層
********************************************/
window.onclick = function(event) {
const modal = document.getElementById('monthlyExpensesModal');
if (event.target === modal) {
modal.style.display = 'none';
}
}
/********************************************
* 新增:設定隨機背景圖片
********************************************/
function setRandomBackground() {
const bgDiv = document.getElementById('background');
const randomNum = Math.floor(Math.random() * 10000); // 生成隨機數以獲取不同圖片
bgDiv.style.backgroundImage = `url('https://picsum.photos/200/150?random=${randomNum}')`;
}
</script>
</body>
</html>
在 <body> 標籤的最上方新增一個 <div> 元素,id 為 background,用於顯示背景圖片。
<!-- 背景圖片容器 -->
<div id="background"></div>
CSS 調整:
為 #background 設定固定定位,覆蓋整個頁面,並設置背景圖片的相關屬性。
設定 opacity: 0.3 以達到 30% 的透明度。
設定 z-index: 0 確保背景層位於所有內容之下。
/* 背景圖片樣式 */
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
opacity: 0.3; /* 設為30%的透明度 */
z-index: 0; /* 確保在所有內容後面 */
}
調整 body 樣式:
設定 position: relative 和 z-index: 1,確保內容層位於背景層之上。
body {
font-family: sans-serif;
margin: 10px;
position: relative; /* 為了確保背景層在後 */
z-index: 1; /* 確保內容在背景層之上 */
}
新增一個 setRandomBackground 函式,用於在頁面載入時設定隨機背景圖片。
在 DOMContentLoaded 事件中呼叫 setRandomBackground 以確保每次開啟網頁時背景圖片都隨機更換。
/********************************************
* 新增:設定隨機背景圖片
********************************************/
function setRandomBackground() {
const bgDiv = document.getElementById('background');
const randomNum = Math.floor(Math.random() * 10000); // 生成隨機數以獲取不同圖片
bgDiv.style.backgroundImage = `url('https://picsum.photos/200/150?random=${randomNum}')`;
}
在 DOMContentLoaded 事件中呼叫:
document.addEventListener('DOMContentLoaded', function() {
setRandomBackground(); // 設定隨機背景圖片
setDefaultDateTime();
populateMonthYearSelectors();
// 載入分類
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
// 載入預算
refreshDashboard();
});
確保背景層的 z-index 為 0,而內容層的 z-index 為 1,使得內容層覆蓋在背景層之上。
/* 調整層級 */
#background {
z-index: 0; /* 背景層 */
}
body {
z-index: 1; /* 內容層 */
}
更新 Code.gs:
Code.gs 已包含之前步驟中的所有功能,特別是 getExpenses(month, year) 函式。Code.gs,確認是否需要進一步調整以配合新的前端功能。更新 Index.html:
Index.html 替換為上述提供的完整 HTML 內容。重新部署應用程式:
測試功能:
Transactions 工作表中。修正與優化:
背景圖片無法顯示或顯示不正確:
<body> 標籤內新增了 <div id="background"></div>。setRandomBackground 函式中。setRandomBackground 函式在 DOMContentLoaded 事件中被正確呼叫。https://picsum.photos/200/150?random=${randomNum} 的格式正確,且隨機數字生成沒有問題。背景圖片透明度影響到其他內容:
z-index 設定:z-index 設定為 0,而內容層的 z-index 設定為 1。opacity 設定:opacity: 0.3,而不影響到其他內容層。背景圖片尺寸不符合預期:
調整圖片尺寸:
https://picsum.photos/1920/1080?random=${randomNum} 以獲得更大尺寸的圖片。function setRandomBackground() {
const bgDiv = document.getElementById('background');
const randomNum = Math.floor(Math.random() * 10000); // 生成隨機數以獲取不同圖片
bgDiv.style.backgroundImage = `url('https://picsum.photos/1920/1080?random=${randomNum}')`;
}
背景圖片載入速度慢或影響頁面性能:
其他功能受到背景圖片的影響:
z-index 和 position 屬性來控制各層級的顯示順序。透過以上的修改,你的記帳工具將具備以下新增功能:
隨機背景圖片功能:
新增全寬「X」關閉按鈕:
搜尋框與送出按鈕位置與寬度調整:
移除不必要的篩選按鈕:
保留並優化其他功能:
整體介面優化:
防錯處理與使用者體驗提升:
請按照上述步驟進行更新與部署,並在使用過程中進行測試以確保所有功能正常運作。如有任何進一步的問題或需求,歡迎隨時提出!
bgDiv.style.backgroundImage = url('https://picsum.photos/1920/1080?random= 其中的網址改為https://picsum.photos/400/900?random= 並讓圖層在最下層,不要影響按鈕及輸入框
思考 花的時間為 9 秒
了解,我將協助你根據需求更新 Index.html,具體修改如下:
更改背景圖片的網址:
https://picsum.photos/400/900?random= 取代原先的 https://picsum.photos/1920/1080?random=。確保背景圖層在最下層,不影響按鈕及輸入框:
#background) 設定為 z-index: 0,而其他內容層 (body 及 .modal-content) 設定為更高的 z-index,以避免背景圖層遮蔽其他元素。以下是更新後的完整 Index.html:
Index.html<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
/* 手機友善基礎樣式 */
body {
font-family: sans-serif;
margin: 10px;
position: relative; /* 為了確保背景層在後 */
z-index: 1; /* 確保內容在背景層之上 */
}
h1 {
text-align: center;
margin-bottom: 0;
}
.sub-title {
text-align: center;
color: #666;
margin-top: 4px;
margin-bottom: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.input-row {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
.input-row .input-field {
flex: 0 0 48%;
}
label {
display: block;
font-weight: bold;
margin-bottom: 4px;
}
input, select {
width: 100%;
box-sizing: border-box;
padding: 8px;
font-size: 16px;
}
button {
width: 100%;
padding: 10px;
font-size: 16px;
background: #4CAF50;
color: #fff;
border: none;
cursor: pointer;
margin-top: 10px;
border-radius: 4px;
}
button:hover {
background: #45a049;
}
/* 預算表 */
.budget-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.budget-table th, .budget-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.warning-normal {
background-color: #ffffff;
}
.warning-near {
background-color: #fff8db; /* 淡黃色 */
}
.warning-over {
background-color: #ffdcdc; /* 淡紅色 */
}
.message {
margin-top: 8px;
text-align: center;
}
/* 浮層 (Modal) 樣式 */
.modal-overlay {
position: fixed;
z-index: 999; /* 確保浮層在最上層 */
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景 */
display: none; /* 初始隱藏 */
justify-content: center;
align-items: center;
}
.modal-content {
background: #fff;
padding: 20px;
border-radius: 8px;
width: 95%;
max-width: 800px;
max-height: 90vh;
overflow-y: auto;
position: relative;
z-index: 1000; /* 確保內容在背景層之上 */
}
/* 新增的全寬關閉按鈕 */
.modal-close-btn {
width: 100%;
background: #f44336;
color: #fff;
border: none;
padding: 10px 0;
font-size: 16px;
cursor: pointer;
border-radius: 4px;
margin-bottom: 15px;
}
.modal-close-btn:hover {
background: #da190b;
}
/* 調整後的模態框標題 */
.modal-header {
display: flex;
width: 100%;
align-items: center;
margin-bottom: 15px;
}
.modal-header h2 {
flex: 0 0 50%; /* 佔據一半寬度 */
text-align: left;
margin: 0;
font-size: 20px;
}
/* 搜尋框和送出按鈕調整 */
.search-container {
display: flex;
align-items: center;
flex: 0 0 50%; /* 佔據剩餘的一半寬度 */
}
.search-container input[type="text"] {
flex: 0 0 50%; /* 佔據四分之一總寬度 */
padding: 6px;
font-size: 14px;
}
.search-container button {
flex: 0 0 50%; /* 佔據四分之一總寬度 */
padding: 6px 12px;
margin-left: 5px;
font-size: 14px;
background: #2196F3;
border-radius: 4px;
}
.search-container button:hover {
background: #0b7dda;
}
.expense-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
}
.expense-table th, .expense-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.sort-btn, .export-btn {
background: #2196F3;
color: #fff;
padding: 8px 12px;
border: none;
cursor: pointer;
border-radius: 4px;
margin-right: 10px;
margin-bottom: 10px;
}
.sort-btn:hover, .export-btn:hover {
background: #0b7dda;
}
/* 背景圖片樣式 */
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
opacity: 0.3; /* 設為30%的透明度 */
z-index: 0; /* 確保在所有內容後面 */
}
/* 滾動條樣式(選用) */
.modal-content::-webkit-scrollbar {
width: 8px;
}
.modal-content::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.2);
border-radius: 4px;
}
</style>
</head>
<body>
<!-- 背景圖片容器 -->
<div id="background"></div>
<h1>簡易記帳工具</h1>
<div class="sub-title">Google Apps Script + 試算表</div>
<div class="container">
<!-- 新增交易表單 -->
<div class="input-row">
<!-- 類型 -->
<div class="input-field">
<label for="type">類型</label>
<select id="type" onchange="filterCategoryOptions()">
<option value="支出" selected>支出</option>
<option value="收入">收入</option>
</select>
</div>
<!-- 類別 -->
<div class="input-field">
<label for="category">類別</label>
<select id="category"></select>
</div>
</div>
<div class="input-row">
<!-- 金額 -->
<div class="input-field">
<label for="amount">金額</label>
<input type="number" id="amount" step="0.01" />
</div>
<!-- 備註 -->
<div class="input-field">
<label for="note">備註</label>
<input type="text" id="note" />
</div>
</div>
<!-- 日期與時間 -->
<div class="input-field">
<label for="dateTime">日期與時間</label>
<input type="datetime-local" id="dateTime" />
</div>
<button onclick="submitForm()">新增交易</button>
<div class="message" id="msgBox"></div>
<!-- 查看月出支按鈕 -->
<button style="background:#f44336; margin-top:20px;" onclick="viewMonthlyExpenses()">
查看月出支
</button>
<!-- 預算表 -->
<table class="budget-table" id="budgetTable">
<thead>
<tr>
<th>類別</th>
<th>預算</th>
<th>本月已用</th>
</tr>
</thead>
<tbody id="budgetTableBody"></tbody>
</table>
</div>
<!-- 浮層 (Modal) 內容 -->
<div class="modal-overlay" id="monthlyExpensesModal">
<div class="modal-content">
<!-- 新增的全寬關閉按鈕 -->
<button class="modal-close-btn" onclick="closeMonthlyExpensesModal()">X</button>
<div class="modal-header">
<h2>本月支出列表</h2>
<div class="search-container">
<input type="text" id="searchKeyword" placeholder="搜尋關鍵字(類別或備註)" />
<button onclick="applyFilters()">送出</button>
</div>
</div>
<!-- 月份和年份選擇區域 -->
<div style="margin-bottom: 15px; display: flex; justify-content: space-between;">
<div class="input-field" style="flex: 0 0 48%;">
<label for="selectMonth">選擇月份</label>
<select id="selectMonth" onchange="viewMonthlyExpenses()">
<!-- 月份選項由 JavaScript 動態生成 -->
</select>
</div>
<div class="input-field" style="flex: 0 0 48%;">
<label for="selectYear">選擇年份</label>
<select id="selectYear" onchange="viewMonthlyExpenses()">
<!-- 年份選項由 JavaScript 動態生成 -->
</select>
</div>
</div>
<!-- 排序及匯出按鈕 -->
<div style="margin-bottom: 15px;">
<button class="sort-btn" onclick="sortExpensesByCategory()">
按「類別」排序
</button>
<button class="export-btn" onclick="exportToCSV()">
匯出為 CSV
</button>
</div>
<!-- 支出表格 -->
<table class="expense-table" id="expenseTable">
<thead>
<tr>
<th>日期時間</th>
<th>類別</th>
<th>金額</th>
<th>備註</th>
</tr>
</thead>
<tbody id="expenseTableBody"></tbody>
</table>
<!-- 總計 -->
<div id="totalAmount" style="text-align:right; font-weight:bold;"></div>
</div>
</div>
<script>
/********************************************
* 前端初始化:載入「分類清單」與「預算表」
********************************************/
document.addEventListener('DOMContentLoaded', function() {
setRandomBackground(); // 設定隨機背景圖片
setDefaultDateTime();
populateMonthYearSelectors();
// 載入分類
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
// 載入預算
refreshDashboard();
});
// 設定隨機背景圖片
function setRandomBackground() {
const bgDiv = document.getElementById('background');
const randomNum = Math.floor(Math.random() * 10000); // 生成隨機數以獲取不同圖片
bgDiv.style.backgroundImage = `url('https://picsum.photos/400/900?random=${randomNum}')`;
}
// 設定「現在時間」到 datetime-local
function setDefaultDateTime() {
const now = new Date();
const localISO = new Date(now.getTime() - now.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16); // 'YYYY-MM-DDTHH:MM'
document.getElementById('dateTime').value = localISO;
}
// 填充分類下拉
function populateCategorySelect(categories) {
const categorySelect = document.getElementById('category');
categorySelect.innerHTML = '';
// 根據選擇的類型篩選類別
const selectedType = document.getElementById('type').value;
categories.forEach(function(cat) {
if (cat.type === selectedType) {
const option = document.createElement('option');
option.value = cat.name;
option.text = cat.name;
categorySelect.appendChild(option);
}
});
}
// 根據「類型」篩選「類別」選項
function filterCategoryOptions() {
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
}
// 新增交易
function submitForm() {
const dateTime = document.getElementById('dateTime').value;
const type = document.getElementById('type').value;
const category = document.getElementById('category').value;
const amount = document.getElementById('amount').value;
const note = document.getElementById('note').value;
// 簡易驗證
if (!dateTime || !type || !category || !amount) {
showMessage('請填寫完整資訊', true);
return;
}
const transaction = {
dateTime: dateTime,
type: type,
category: category,
amount: amount,
note: note
};
google.script.run
.withSuccessHandler(function(msg) {
showMessage(msg, false);
setDefaultDateTime();
document.getElementById('amount').value = '';
document.getElementById('note').value = '';
// 刷新預算
refreshDashboard();
})
.withFailureHandler(function(err) {
showMessage('寫入失敗: ' + err.message, true);
})
.saveTransaction(transaction);
}
// 顯示訊息
function showMessage(msg, isError) {
const msgBox = document.getElementById('msgBox');
msgBox.style.color = isError ? 'red' : 'green';
msgBox.textContent = msg;
}
// 刷新預算表
function refreshDashboard() {
google.script.run
.withSuccessHandler(function(dashboardData){
populateBudgetTable(dashboardData);
})
.getDashboardData();
}
function populateBudgetTable(data) {
const tbody = document.getElementById('budgetTableBody');
tbody.innerHTML = '';
data.forEach(function(item){
const tr = document.createElement('tr');
if (item.warningLevel === 'over') {
tr.className = 'warning-over';
} else if (item.warningLevel === 'near') {
tr.className = 'warning-near';
} else {
tr.className = 'warning-normal';
}
const tdCat = document.createElement('td');
tdCat.textContent = item.category;
const tdBudget = document.createElement('td');
tdBudget.textContent = item.budget || 0;
const tdSpent = document.createElement('td');
tdSpent.textContent = item.spent;
tr.appendChild(tdCat);
tr.appendChild(tdBudget);
tr.appendChild(tdSpent);
tbody.appendChild(tr);
});
}
/********************************************
* 新增:查看「月出支」按鈕的功能
********************************************/
let monthlyExpenses = []; // 用於儲存當月支出資料
let filteredExpenses = []; // 用於儲存過濾後的支出資料
// 1) 填充月份和年份選擇器
function populateMonthYearSelectors() {
const selectMonth = document.getElementById('selectMonth');
const selectYear = document.getElementById('selectYear');
// 填充月份選項
const months = [
'一月', '二月', '三月', '四月', '五月', '六月',
'七月', '八月', '九月', '十月', '十一月', '十二月'
];
months.forEach(function(month, index) {
const option = document.createElement('option');
option.value = index; // 0-11
option.text = month;
selectMonth.appendChild(option);
});
// 填充年份選項(例如,從 2020 到當前年份 + 5)
const currentYear = new Date().getFullYear();
for(let year = currentYear - 5; year <= currentYear + 5; year++) {
const option = document.createElement('option');
option.value = year;
option.text = year;
selectYear.appendChild(option);
}
// 設定預設選擇為當前年月
selectMonth.value = new Date().getMonth(); // 0-11
selectYear.value = currentYear;
}
// 2) 取得指定月份支出 -> 顯示浮層
function viewMonthlyExpenses() {
const selectMonth = document.getElementById('selectMonth').value;
const selectYear = document.getElementById('selectYear').value;
google.script.run
.withSuccessHandler(function(expenses){
if (Array.isArray(expenses)) {
monthlyExpenses = expenses;
filteredExpenses = expenses; // 初始化為全部
applyFilters(); // 套用初始篩選(僅關鍵字)
renderMonthlyExpenses();
document.getElementById('monthlyExpensesModal').style.display = 'flex';
} else {
alert('取得的支出資料格式錯誤。');
}
})
.withFailureHandler(function(err){
alert('取得支出失敗:' + err.message);
})
.getExpenses(parseInt(selectMonth), parseInt(selectYear));
}
// 3) 渲染當月支出的表格
function renderMonthlyExpenses() {
const tbody = document.getElementById('expenseTableBody');
tbody.innerHTML = '';
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 4;
td.textContent = '本月尚無符合條件的支出紀錄。';
tr.appendChild(td);
tbody.appendChild(tr);
document.getElementById('totalAmount').textContent = '總計:0';
return;
}
// 計算總計
let total = 0;
filteredExpenses.forEach(function(item){
total += parseFloat(item.amount) || 0;
const tr = document.createElement('tr');
const tdDateTime = document.createElement('td');
tdDateTime.textContent = item.dateTime; // 格式化後的日期時間
const tdCategory = document.createElement('td');
tdCategory.textContent = item.category;
const tdAmount = document.createElement('td');
tdAmount.textContent = item.amount;
const tdNote = document.createElement('td');
tdNote.textContent = item.note;
tr.appendChild(tdDateTime);
tr.appendChild(tdCategory);
tr.appendChild(tdAmount);
tr.appendChild(tdNote);
tbody.appendChild(tr);
});
// 顯示總計
document.getElementById('totalAmount').textContent = '總計:' + total;
}
// 4) 按類別排序 (升冪) 後重新渲染
function sortExpensesByCategory() {
if (!Array.isArray(filteredExpenses)) return;
filteredExpenses.sort(function(a, b){
return a.category.localeCompare(b.category, 'zh-TW');
});
renderMonthlyExpenses();
}
// 5) 關閉浮層
function closeMonthlyExpensesModal() {
document.getElementById('monthlyExpensesModal').style.display = 'none';
}
// 6) 套用篩選(僅關鍵字)
function applyFilters() {
const keyword = document.getElementById('searchKeyword').value.trim().toLowerCase();
filteredExpenses = monthlyExpenses.filter(function(expense){
let matchesKeyword = true;
// 檢查關鍵字(類別或備註)
if (keyword) {
const categoryMatch = expense.category.toLowerCase().includes(keyword);
const noteMatch = expense.note.toLowerCase().includes(keyword);
matchesKeyword = categoryMatch || noteMatch;
}
return matchesKeyword;
});
renderMonthlyExpenses();
}
// 7) 匯出為 CSV
function exportToCSV() {
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
alert('沒有可匯出的資料。');
return;
}
// 构建 CSV 資料
let csvContent = "data:text/csv;charset=utf-8,";
csvContent += "日期時間,類別,金額,備註\n"; // 標題
filteredExpenses.forEach(function(item){
const row = `"${item.dateTime}","${item.category}","${item.amount}","${item.note}"`;
csvContent += row + "\n";
});
// 觸發下載
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
const now = new Date();
const timestamp = now.getFullYear() + "-" +
String(now.getMonth()+1).padStart(2, '0') + "-" +
String(now.getDate()).padStart(2, '0') + "_" +
String(now.getHours()).padStart(2, '0') + "-" +
String(now.getMinutes()).padStart(2, '0') + "-" +
String(now.getSeconds()).padStart(2, '0');
link.setAttribute("download", `本月支出_${timestamp}.csv`);
document.body.appendChild(link); // Required for FF
link.click(); // This will download the data file named "本月支出_timestamp.csv"
document.body.removeChild(link);
}
/********************************************
* 新增:點擊浮層外部關閉浮層
********************************************/
window.onclick = function(event) {
const modal = document.getElementById('monthlyExpensesModal');
if (event.target === modal) {
modal.style.display = 'none';
}
}
</script>
</body>
</html>
JavaScript 函式調整:
setRandomBackground 函式中,將背景圖片的網址從 https://picsum.photos/1920/1080?random=${randomNum} 改為 https://picsum.photos/400/900?random=${randomNum}。// 設定隨機背景圖片
function setRandomBackground() {
const bgDiv = document.getElementById('background');
const randomNum = Math.floor(Math.random() * 10000); // 生成隨機數以獲取不同圖片
bgDiv.style.backgroundImage = `url('https://picsum.photos/400/900?random=${randomNum}')`;
}
CSS 調整:
#background 的 z-index: 0,確保其位於所有內容之下。body 和 .modal-content 擁有更高的 z-index,如 z-index: 1 和 z-index: 1000,以確保內容層覆蓋在背景層之上。/* 背景圖片樣式 */
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
opacity: 0.3; /* 設為30%的透明度 */
z-index: 0; /* 確保在所有內容後面 */
}
/* 調整層級 */
body {
z-index: 1; /* 內容層 */
}
.modal-content {
z-index: 1000; /* 確保內容在背景層之上 */
}
HTML 結構新增:
<body> 標籤的最上方新增一個 <div> 元素,id 為 background,用於顯示背景圖片。<!-- 背景圖片容器 -->
<div id="background"></div>
z-index.modal-content 設定為 z-index: 1000,以確保其位於背景層之上,避免被背景圖片遮蔽。更新 Code.gs:
Code.gs 已包含之前步驟中的所有功能,特別是 getExpenses(month, year) 函式。Code.gs,確認是否需要進一步調整以配合新的前端功能。更新 Index.html:
Index.html 替換為上述提供的完整 HTML 內容。重新部署應用程式:
測試功能:
Transactions 工作表中。修正與優化:
背景圖片無法顯示或顯示不正確:
<body> 標籤內新增了 <div id="background"></div>。setRandomBackground 函式中。setRandomBackground 函式在 DOMContentLoaded 事件中被正確呼叫。https://picsum.photos/400/900?random=${randomNum} 的格式正確,且隨機數字生成沒有問題。背景圖片透明度影響到其他內容:
z-index 設定:z-index 設定為 0,而內容層的 z-index 設定為更高的值(如 1 和 1000)。opacity 設定:opacity: 0.3,而不影響到其他內容層。背景圖片尺寸不符合預期:
https://picsum.photos/800/1600?random=${randomNum} 以獲得更大尺寸的圖片。背景圖片載入速度慢或影響頁面性能:
其他功能受到背景圖片的影響:
z-index 和 position 屬性來控制各層級的顯示順序。透過以上的修改,你的記帳工具將具備以下新增功能:
隨機背景圖片功能:
https://picsum.photos/400/900?random=${randomNum} 隨機選擇一張圖片顯示。新增全寬「X」關閉按鈕:
搜尋框與送出按鈕位置與寬度調整:
移除不必要的篩選按鈕:
保留並優化其他功能:
整體介面優化:
防錯處理與使用者體驗提升:
請按照上述步驟進行更新與部署,並在使用過程中進行測試以確保所有功能正常運作。如有任何進一步的問題或需求,歡迎隨時提出!
背景圖片的透明度改為20%,所有內容都可以正常操作,背景圖片都在文字、按鈕、表格及輸入框的下面
思考 花的時間為 4 秒
了解,我將協助你根據最新需求更新 Index.html。具體修改如下:
為了達到這些目標,我將對背景圖片的 CSS 進行以下調整:
opacity 為 0.2,即20%的透明度。pointer-events: none,確保背景層不會攔截任何滑鼠事件,避免影響頁面上的互動元素。以下是更新後的完整 Index.html:
Index.html<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
/* 手機友善基礎樣式 */
body {
font-family: sans-serif;
margin: 10px;
position: relative; /* 為了確保背景層在後 */
z-index: 1; /* 確保內容在背景層之上 */
}
h1 {
text-align: center;
margin-bottom: 0;
}
.sub-title {
text-align: center;
color: #666;
margin-top: 4px;
margin-bottom: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.input-row {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
.input-row .input-field {
flex: 0 0 48%;
}
label {
display: block;
font-weight: bold;
margin-bottom: 4px;
}
input, select {
width: 100%;
box-sizing: border-box;
padding: 8px;
font-size: 16px;
}
button {
width: 100%;
padding: 10px;
font-size: 16px;
background: #4CAF50;
color: #fff;
border: none;
cursor: pointer;
margin-top: 10px;
border-radius: 4px;
}
button:hover {
background: #45a049;
}
/* 預算表 */
.budget-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.budget-table th, .budget-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.warning-normal {
background-color: #ffffff;
}
.warning-near {
background-color: #fff8db; /* 淡黃色 */
}
.warning-over {
background-color: #ffdcdc; /* 淡紅色 */
}
.message {
margin-top: 8px;
text-align: center;
}
/* 浮層 (Modal) 樣式 */
.modal-overlay {
position: fixed;
z-index: 999; /* 確保浮層在最上層 */
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景 */
display: none; /* 初始隱藏 */
justify-content: center;
align-items: center;
}
.modal-content {
background: #fff;
padding: 20px;
border-radius: 8px;
width: 95%;
max-width: 800px;
max-height: 90vh;
overflow-y: auto;
position: relative;
z-index: 1000; /* 確保內容在背景層之上 */
}
/* 新增的全寬關閉按鈕 */
.modal-close-btn {
width: 100%;
background: #f44336;
color: #fff;
border: none;
padding: 10px 0;
font-size: 16px;
cursor: pointer;
border-radius: 4px;
margin-bottom: 15px;
}
.modal-close-btn:hover {
background: #da190b;
}
/* 調整後的模態框標題 */
.modal-header {
display: flex;
width: 100%;
align-items: center;
margin-bottom: 15px;
}
.modal-header h2 {
flex: 0 0 50%; /* 佔據一半寬度 */
text-align: left;
margin: 0;
font-size: 20px;
}
/* 搜尋框和送出按鈕調整 */
.search-container {
display: flex;
align-items: center;
flex: 0 0 50%; /* 佔據剩餘的一半寬度 */
}
.search-container input[type="text"] {
flex: 0 0 50%; /* 佔據四分之一總寬度 */
padding: 6px;
font-size: 14px;
}
.search-container button {
flex: 0 0 50%; /* 佔據四分之一總寬度 */
padding: 6px 12px;
margin-left: 5px;
font-size: 14px;
background: #2196F3;
border-radius: 4px;
}
.search-container button:hover {
background: #0b7dda;
}
.expense-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
}
.expense-table th, .expense-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.sort-btn, .export-btn {
background: #2196F3;
color: #fff;
padding: 8px 12px;
border: none;
cursor: pointer;
border-radius: 4px;
margin-right: 10px;
margin-bottom: 10px;
}
.sort-btn:hover, .export-btn:hover {
background: #0b7dda;
}
/* 背景圖片樣式 */
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
opacity: 0.2; /* 設為20%的透明度 */
z-index: 0; /* 確保在所有內容後面 */
pointer-events: none; /* 確保不會攔截滑鼠事件 */
}
/* 滾動條樣式(選用) */
.modal-content::-webkit-scrollbar {
width: 8px;
}
.modal-content::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.2);
border-radius: 4px;
}
</style>
</head>
<body>
<!-- 背景圖片容器 -->
<div id="background"></div>
<h1>簡易記帳工具</h1>
<div class="sub-title">Google Apps Script + 試算表</div>
<div class="container">
<!-- 新增交易表單 -->
<div class="input-row">
<!-- 類型 -->
<div class="input-field">
<label for="type">類型</label>
<select id="type" onchange="filterCategoryOptions()">
<option value="支出" selected>支出</option>
<option value="收入">收入</option>
</select>
</div>
<!-- 類別 -->
<div class="input-field">
<label for="category">類別</label>
<select id="category"></select>
</div>
</div>
<div class="input-row">
<!-- 金額 -->
<div class="input-field">
<label for="amount">金額</label>
<input type="number" id="amount" step="0.01" />
</div>
<!-- 備註 -->
<div class="input-field">
<label for="note">備註</label>
<input type="text" id="note" />
</div>
</div>
<!-- 日期與時間 -->
<div class="input-field">
<label for="dateTime">日期與時間</label>
<input type="datetime-local" id="dateTime" />
</div>
<button onclick="submitForm()">新增交易</button>
<div class="message" id="msgBox"></div>
<!-- 查看月出支按鈕 -->
<button style="background:#f44336; margin-top:20px;" onclick="viewMonthlyExpenses()">
查看月出支
</button>
<!-- 預算表 -->
<table class="budget-table" id="budgetTable">
<thead>
<tr>
<th>類別</th>
<th>預算</th>
<th>本月已用</th>
</tr>
</thead>
<tbody id="budgetTableBody"></tbody>
</table>
</div>
<!-- 浮層 (Modal) 內容 -->
<div class="modal-overlay" id="monthlyExpensesModal">
<div class="modal-content">
<!-- 新增的全寬關閉按鈕 -->
<button class="modal-close-btn" onclick="closeMonthlyExpensesModal()">X</button>
<div class="modal-header">
<h2>本月支出列表</h2>
<div class="search-container">
<input type="text" id="searchKeyword" placeholder="搜尋關鍵字(類別或備註)" />
<button onclick="applyFilters()">送出</button>
</div>
</div>
<!-- 月份和年份選擇區域 -->
<div style="margin-bottom: 15px; display: flex; justify-content: space-between;">
<div class="input-field" style="flex: 0 0 48%;">
<label for="selectMonth">選擇月份</label>
<select id="selectMonth" onchange="viewMonthlyExpenses()">
<!-- 月份選項由 JavaScript 動態生成 -->
</select>
</div>
<div class="input-field" style="flex: 0 0 48%;">
<label for="selectYear">選擇年份</label>
<select id="selectYear" onchange="viewMonthlyExpenses()">
<!-- 年份選項由 JavaScript 動態生成 -->
</select>
</div>
</div>
<!-- 排序及匯出按鈕 -->
<div style="margin-bottom: 15px;">
<button class="sort-btn" onclick="sortExpensesByCategory()">
按「類別」排序
</button>
<button class="export-btn" onclick="exportToCSV()">
匯出為 CSV
</button>
</div>
<!-- 支出表格 -->
<table class="expense-table" id="expenseTable">
<thead>
<tr>
<th>日期時間</th>
<th>類別</th>
<th>金額</th>
<th>備註</th>
</tr>
</thead>
<tbody id="expenseTableBody"></tbody>
</table>
<!-- 總計 -->
<div id="totalAmount" style="text-align:right; font-weight:bold;"></div>
</div>
</div>
<script>
/********************************************
* 前端初始化:載入「分類清單」與「預算表」
********************************************/
document.addEventListener('DOMContentLoaded', function() {
setRandomBackground(); // 設定隨機背景圖片
setDefaultDateTime();
populateMonthYearSelectors();
// 載入分類
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
// 載入預算
refreshDashboard();
});
// 設定隨機背景圖片
function setRandomBackground() {
const bgDiv = document.getElementById('background');
const randomNum = Math.floor(Math.random() * 10000); // 生成隨機數以獲取不同圖片
bgDiv.style.backgroundImage = `url('https://picsum.photos/400/900?random=${randomNum}')`;
}
// 設定「現在時間」到 datetime-local
function setDefaultDateTime() {
const now = new Date();
const localISO = new Date(now.getTime() - now.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16); // 'YYYY-MM-DDTHH:MM'
document.getElementById('dateTime').value = localISO;
}
// 填充分類下拉
function populateCategorySelect(categories) {
const categorySelect = document.getElementById('category');
categorySelect.innerHTML = '';
// 根據選擇的類型篩選類別
const selectedType = document.getElementById('type').value;
categories.forEach(function(cat) {
if (cat.type === selectedType) {
const option = document.createElement('option');
option.value = cat.name;
option.text = cat.name;
categorySelect.appendChild(option);
}
});
}
// 根據「類型」篩選「類別」選項
function filterCategoryOptions() {
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
}
// 新增交易
function submitForm() {
const dateTime = document.getElementById('dateTime').value;
const type = document.getElementById('type').value;
const category = document.getElementById('category').value;
const amount = document.getElementById('amount').value;
const note = document.getElementById('note').value;
// 簡易驗證
if (!dateTime || !type || !category || !amount) {
showMessage('請填寫完整資訊', true);
return;
}
const transaction = {
dateTime: dateTime,
type: type,
category: category,
amount: amount,
note: note
};
google.script.run
.withSuccessHandler(function(msg) {
showMessage(msg, false);
setDefaultDateTime();
document.getElementById('amount').value = '';
document.getElementById('note').value = '';
// 刷新預算
refreshDashboard();
})
.withFailureHandler(function(err) {
showMessage('寫入失敗: ' + err.message, true);
})
.saveTransaction(transaction);
}
// 顯示訊息
function showMessage(msg, isError) {
const msgBox = document.getElementById('msgBox');
msgBox.style.color = isError ? 'red' : 'green';
msgBox.textContent = msg;
}
// 刷新預算表
function refreshDashboard() {
google.script.run
.withSuccessHandler(function(dashboardData){
populateBudgetTable(dashboardData);
})
.getDashboardData();
}
function populateBudgetTable(data) {
const tbody = document.getElementById('budgetTableBody');
tbody.innerHTML = '';
data.forEach(function(item){
const tr = document.createElement('tr');
if (item.warningLevel === 'over') {
tr.className = 'warning-over';
} else if (item.warningLevel === 'near') {
tr.className = 'warning-near';
} else {
tr.className = 'warning-normal';
}
const tdCat = document.createElement('td');
tdCat.textContent = item.category;
const tdBudget = document.createElement('td');
tdBudget.textContent = item.budget || 0;
const tdSpent = document.createElement('td');
tdSpent.textContent = item.spent;
tr.appendChild(tdCat);
tr.appendChild(tdBudget);
tr.appendChild(tdSpent);
tbody.appendChild(tr);
});
}
/********************************************
* 新增:查看「月出支」按鈕的功能
********************************************/
let monthlyExpenses = []; // 用於儲存當月支出資料
let filteredExpenses = []; // 用於儲存過濾後的支出資料
// 1) 填充月份和年份選擇器
function populateMonthYearSelectors() {
const selectMonth = document.getElementById('selectMonth');
const selectYear = document.getElementById('selectYear');
// 填充月份選項
const months = [
'一月', '二月', '三月', '四月', '五月', '六月',
'七月', '八月', '九月', '十月', '十一月', '十二月'
];
months.forEach(function(month, index) {
const option = document.createElement('option');
option.value = index; // 0-11
option.text = month;
selectMonth.appendChild(option);
});
// 填充年份選項(例如,從 2020 到當前年份 + 5)
const currentYear = new Date().getFullYear();
for(let year = currentYear - 5; year <= currentYear + 5; year++) {
const option = document.createElement('option');
option.value = year;
option.text = year;
selectYear.appendChild(option);
}
// 設定預設選擇為當前年月
selectMonth.value = new Date().getMonth(); // 0-11
selectYear.value = currentYear;
}
// 2) 取得指定月份支出 -> 顯示浮層
function viewMonthlyExpenses() {
const selectMonth = document.getElementById('selectMonth').value;
const selectYear = document.getElementById('selectYear').value;
google.script.run
.withSuccessHandler(function(expenses){
if (Array.isArray(expenses)) {
monthlyExpenses = expenses;
filteredExpenses = expenses; // 初始化為全部
applyFilters(); // 套用初始篩選(僅關鍵字)
renderMonthlyExpenses();
document.getElementById('monthlyExpensesModal').style.display = 'flex';
} else {
alert('取得的支出資料格式錯誤。');
}
})
.withFailureHandler(function(err){
alert('取得支出失敗:' + err.message);
})
.getExpenses(parseInt(selectMonth), parseInt(selectYear));
}
// 3) 渲染當月支出的表格
function renderMonthlyExpenses() {
const tbody = document.getElementById('expenseTableBody');
tbody.innerHTML = '';
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 4;
td.textContent = '本月尚無符合條件的支出紀錄。';
tr.appendChild(td);
tbody.appendChild(tr);
document.getElementById('totalAmount').textContent = '總計:0';
return;
}
// 計算總計
let total = 0;
filteredExpenses.forEach(function(item){
total += parseFloat(item.amount) || 0;
const tr = document.createElement('tr');
const tdDateTime = document.createElement('td');
tdDateTime.textContent = item.dateTime; // 格式化後的日期時間
const tdCategory = document.createElement('td');
tdCategory.textContent = item.category;
const tdAmount = document.createElement('td');
tdAmount.textContent = item.amount;
const tdNote = document.createElement('td');
tdNote.textContent = item.note;
tr.appendChild(tdDateTime);
tr.appendChild(tdCategory);
tr.appendChild(tdAmount);
tr.appendChild(tdNote);
tbody.appendChild(tr);
});
// 顯示總計
document.getElementById('totalAmount').textContent = '總計:' + total;
}
// 4) 按類別排序 (升冪) 後重新渲染
function sortExpensesByCategory() {
if (!Array.isArray(filteredExpenses)) return;
filteredExpenses.sort(function(a, b){
return a.category.localeCompare(b.category, 'zh-TW');
});
renderMonthlyExpenses();
}
// 5) 關閉浮層
function closeMonthlyExpensesModal() {
document.getElementById('monthlyExpensesModal').style.display = 'none';
}
// 6) 套用篩選(僅關鍵字)
function applyFilters() {
const keyword = document.getElementById('searchKeyword').value.trim().toLowerCase();
filteredExpenses = monthlyExpenses.filter(function(expense){
let matchesKeyword = true;
// 檢查關鍵字(類別或備註)
if (keyword) {
const categoryMatch = expense.category.toLowerCase().includes(keyword);
const noteMatch = expense.note.toLowerCase().includes(keyword);
matchesKeyword = categoryMatch || noteMatch;
}
return matchesKeyword;
});
renderMonthlyExpenses();
}
// 7) 匯出為 CSV
function exportToCSV() {
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
alert('沒有可匯出的資料。');
return;
}
// 構建 CSV 資料
let csvContent = "data:text/csv;charset=utf-8,";
csvContent += "日期時間,類別,金額,備註\n"; // 標題
filteredExpenses.forEach(function(item){
const row = `"${item.dateTime}","${item.category}","${item.amount}","${item.note}"`;
csvContent += row + "\n";
});
// 觸發下載
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
const now = new Date();
const timestamp = now.getFullYear() + "-" +
String(now.getMonth()+1).padStart(2, '0') + "-" +
String(now.getDate()).padStart(2, '0') + "_" +
String(now.getHours()).padStart(2, '0') + "-" +
String(now.getMinutes()).padStart(2, '0') + "-" +
String(now.getSeconds()).padStart(2, '0');
link.setAttribute("download", `本月支出_${timestamp}.csv`);
document.body.appendChild(link); // Required for FF
link.click(); // This will download the data file named "本月支出_timestamp.csv"
document.body.removeChild(link);
}
/********************************************
* 新增:點擊浮層外部關閉浮層
********************************************/
window.onclick = function(event) {
const modal = document.getElementById('monthlyExpensesModal');
if (event.target === modal) {
modal.style.display = 'none';
}
}
</script>
</body>
</html>
將 #background 的 opacity 從 0.3 改為 0.2,以達到20%的透明度。
/* 背景圖片樣式 */
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
opacity: 0.2; /* 設為20%的透明度 */
z-index: 0; /* 確保在所有內容後面 */
pointer-events: none; /* 確保不會攔截滑鼠事件 */
}
保持 #background 的 z-index: 0,確保其位於所有內容之下。
設定 body 的 z-index: 1 和 position: relative,確保內容層位於背景層之上。
設定 .modal-content 的 z-index: 1000,確保浮層內容位於所有其他內容之上。
添加 pointer-events: none 到 #background,確保背景層不會攔截任何滑鼠事件,避免影響頁面上的互動元素。
/* 背景圖片樣式 */
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
opacity: 0.2; /* 設為20%的透明度 */
z-index: 0; /* 確保在所有內容後面 */
pointer-events: none; /* 確保不會攔截滑鼠事件 */
}
/* 調整層級 */
body {
z-index: 1; /* 內容層 */
}
.modal-content {
z-index: 1000; /* 確保內容在背景層之上 */
}
新增背景圖片容器:
在 <body> 標籤的最上方新增一個 <div> 元素,id 為 background,用於顯示背景圖片。
<!-- 背景圖片容器 -->
<div id="background"></div>
JavaScript 調整:
更新 setRandomBackground 函式,將背景圖片的網址從 https://picsum.photos/400/900?random=${randomNum} 改為 https://picsum.photos/400/900?random=${randomNum},符合你的最新需求。
// 設定隨機背景圖片
function setRandomBackground() {
const bgDiv = document.getElementById('background');
const randomNum = Math.floor(Math.random() * 10000); // 生成隨機數以獲取不同圖片
bgDiv.style.backgroundImage = `url('https://picsum.photos/400/900?random=${randomNum}')`;
}
確保不改動其他介面及功能:
更新 Code.gs:
Code.gs 已包含之前步驟中的所有功能,特別是 getExpenses(month, year) 函式。Code.gs,確認是否需要進一步調整以配合新的前端功能。更新 Index.html:
Index.html 替換為上述提供的完整 HTML 內容。重新部署應用程式:
測試功能:
Transactions 工作表中。修正與優化:
背景圖片無法顯示或顯示不正確:
<body> 標籤內新增了 <div id="background"></div>。setRandomBackground 函式中。setRandomBackground 函式在 DOMContentLoaded 事件中被正確呼叫。https://picsum.photos/400/900?random=${randomNum} 的格式正確,且隨機數字生成沒有問題。背景圖片透明度影響到其他內容:
z-index 設定:z-index 設定為 0,而內容層的 z-index 設定為更高的值(如 1 和 1000)。opacity 設定:opacity: 0.2,而不影響到其他內容層。pointer-events 設定:pointer-events: none 已經添加到 #background,防止背景層攔截滑鼠事件。背景圖片尺寸不符合預期:
https://picsum.photos/800/1600?random=${randomNum} 以獲得更大尺寸的圖片。背景圖片載入速度慢或影響頁面性能:
其他功能受到背景圖片的影響:
z-index 和 position 屬性來控制各層級的顯示順序。z-index:body 和 .modal-content 的 z-index 設定高於 #background,以確保內容層在背景層之上。透過以上的修改,你的記帳工具將具備以下新增功能:
隨機背景圖片功能:
https://picsum.photos/400/900?random=${randomNum} 隨機選擇一張圖片顯示。新增全寬「X」關閉按鈕:
搜尋框與送出按鈕位置與寬度調整:
移除不必要的篩選按鈕:
保留並優化其他功能:
整體介面優化:
防錯處理與使用者體驗提升:
請按照上述步驟進行更新與部署,並在使用過程中進行測試以確保所有功能正常運作。如有任何進一步的問題或需求,歡迎隨時提出!
類別 預算 本月已用 的表格標題欄背景改為淡藍色,背景圖片的z-index值改為-100
思考 花的時間為 4 秒
了解,我將協助你根據最新需求更新 Index.html。具體修改如下:
z-index 值改為 -100,以確保背景圖片位於所有內容的最下層,並不影響按鈕及輸入框的操作。以下是更新後的完整 Index.html,包含上述修改:
Index.html<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
/* 手機友善基礎樣式 */
body {
font-family: sans-serif;
margin: 10px;
position: relative; /* 為了確保背景層在後 */
z-index: 1; /* 確保內容在背景層之上 */
}
h1 {
text-align: center;
margin-bottom: 0;
}
.sub-title {
text-align: center;
color: #666;
margin-top: 4px;
margin-bottom: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.input-row {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
.input-row .input-field {
flex: 0 0 48%;
}
label {
display: block;
font-weight: bold;
margin-bottom: 4px;
}
input, select {
width: 100%;
box-sizing: border-box;
padding: 8px;
font-size: 16px;
}
button {
width: 100%;
padding: 10px;
font-size: 16px;
background: #4CAF50;
color: #fff;
border: none;
cursor: pointer;
margin-top: 10px;
border-radius: 4px;
}
button:hover {
background: #45a049;
}
/* 預算表 */
.budget-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.budget-table th, .budget-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
/* 將表格標題欄背景改為淡藍色 */
.budget-table th {
background-color: #add8e6; /* 淡藍色 */
}
.warning-normal {
background-color: #ffffff;
}
.warning-near {
background-color: #fff8db; /* 淡黃色 */
}
.warning-over {
background-color: #ffdcdc; /* 淡紅色 */
}
.message {
margin-top: 8px;
text-align: center;
}
/* 浮層 (Modal) 樣式 */
.modal-overlay {
position: fixed;
z-index: 999; /* 確保浮層在最上層 */
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景 */
display: none; /* 初始隱藏 */
justify-content: center;
align-items: center;
}
.modal-content {
background: #fff;
padding: 20px;
border-radius: 8px;
width: 95%;
max-width: 800px;
max-height: 90vh;
overflow-y: auto;
position: relative;
z-index: 1000; /* 確保內容在背景層之上 */
}
/* 新增的全寬關閉按鈕 */
.modal-close-btn {
width: 100%;
background: #f44336;
color: #fff;
border: none;
padding: 10px 0;
font-size: 16px;
cursor: pointer;
border-radius: 4px;
margin-bottom: 15px;
}
.modal-close-btn:hover {
background: #da190b;
}
/* 調整後的模態框標題 */
.modal-header {
display: flex;
width: 100%;
align-items: center;
margin-bottom: 15px;
}
.modal-header h2 {
flex: 0 0 50%; /* 佔據一半寬度 */
text-align: left;
margin: 0;
font-size: 20px;
}
/* 搜尋框和送出按鈕調整 */
.search-container {
display: flex;
align-items: center;
flex: 0 0 50%; /* 佔據剩餘的一半寬度 */
}
.search-container input[type="text"] {
flex: 0 0 50%; /* 佔據四分之一總寬度 */
padding: 6px;
font-size: 14px;
}
.search-container button {
flex: 0 0 50%; /* 佔據四分之一總寬度 */
padding: 6px 12px;
margin-left: 5px;
font-size: 14px;
background: #2196F3;
border-radius: 4px;
}
.search-container button:hover {
background: #0b7dda;
}
.expense-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
}
.expense-table th, .expense-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.sort-btn, .export-btn {
background: #2196F3;
color: #fff;
padding: 8px 12px;
border: none;
cursor: pointer;
border-radius: 4px;
margin-right: 10px;
margin-bottom: 10px;
}
.sort-btn:hover, .export-btn:hover {
background: #0b7dda;
}
/* 背景圖片樣式 */
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
opacity: 0.2; /* 設為20%的透明度 */
z-index: -100; /* 確保在所有內容後面 */
pointer-events: none; /* 確保不會攔截滑鼠事件 */
}
/* 滾動條樣式(選用) */
.modal-content::-webkit-scrollbar {
width: 8px;
}
.modal-content::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.2);
border-radius: 4px;
}
</style>
</head>
<body>
<!-- 背景圖片容器 -->
<div id="background"></div>
<h1>簡易記帳工具</h1>
<div class="sub-title">Google Apps Script + 試算表</div>
<div class="container">
<!-- 新增交易表單 -->
<div class="input-row">
<!-- 類型 -->
<div class="input-field">
<label for="type">類型</label>
<select id="type" onchange="filterCategoryOptions()">
<option value="支出" selected>支出</option>
<option value="收入">收入</option>
</select>
</div>
<!-- 類別 -->
<div class="input-field">
<label for="category">類別</label>
<select id="category"></select>
</div>
</div>
<div class="input-row">
<!-- 金額 -->
<div class="input-field">
<label for="amount">金額</label>
<input type="number" id="amount" step="0.01" />
</div>
<!-- 備註 -->
<div class="input-field">
<label for="note">備註</label>
<input type="text" id="note" />
</div>
</div>
<!-- 日期與時間 -->
<div class="input-field">
<label for="dateTime">日期與時間</label>
<input type="datetime-local" id="dateTime" />
</div>
<button onclick="submitForm()">新增交易</button>
<div class="message" id="msgBox"></div>
<!-- 查看月出支按鈕 -->
<button style="background:#f44336; margin-top:20px;" onclick="viewMonthlyExpenses()">
查看月出支
</button>
<!-- 預算表 -->
<table class="budget-table" id="budgetTable">
<thead>
<tr>
<th>類別</th>
<th>預算</th>
<th>本月已用</th>
</tr>
</thead>
<tbody id="budgetTableBody"></tbody>
</table>
</div>
<!-- 浮層 (Modal) 內容 -->
<div class="modal-overlay" id="monthlyExpensesModal">
<div class="modal-content">
<!-- 新增的全寬關閉按鈕 -->
<button class="modal-close-btn" onclick="closeMonthlyExpensesModal()">X</button>
<div class="modal-header">
<h2>本月支出列表</h2>
<div class="search-container">
<input type="text" id="searchKeyword" placeholder="搜尋關鍵字(類別或備註)" />
<button onclick="applyFilters()">送出</button>
</div>
</div>
<!-- 月份和年份選擇區域 -->
<div style="margin-bottom: 15px; display: flex; justify-content: space-between;">
<div class="input-field" style="flex: 0 0 48%;">
<label for="selectMonth">選擇月份</label>
<select id="selectMonth" onchange="viewMonthlyExpenses()">
<!-- 月份選項由 JavaScript 動態生成 -->
</select>
</div>
<div class="input-field" style="flex: 0 0 48%;">
<label for="selectYear">選擇年份</label>
<select id="selectYear" onchange="viewMonthlyExpenses()">
<!-- 年份選項由 JavaScript 動態生成 -->
</select>
</div>
</div>
<!-- 排序及匯出按鈕 -->
<div style="margin-bottom: 15px;">
<button class="sort-btn" onclick="sortExpensesByCategory()">
按「類別」排序
</button>
<button class="export-btn" onclick="exportToCSV()">
匯出為 CSV
</button>
</div>
<!-- 支出表格 -->
<table class="expense-table" id="expenseTable">
<thead>
<tr>
<th>日期時間</th>
<th>類別</th>
<th>金額</th>
<th>備註</th>
</tr>
</thead>
<tbody id="expenseTableBody"></tbody>
</table>
<!-- 總計 -->
<div id="totalAmount" style="text-align:right; font-weight:bold;"></div>
</div>
</div>
<script>
/********************************************
* 前端初始化:載入「分類清單」與「預算表」
********************************************/
document.addEventListener('DOMContentLoaded', function() {
setRandomBackground(); // 設定隨機背景圖片
setDefaultDateTime();
populateMonthYearSelectors();
// 載入分類
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
// 載入預算
refreshDashboard();
});
// 設定隨機背景圖片
function setRandomBackground() {
const bgDiv = document.getElementById('background');
const randomNum = Math.floor(Math.random() * 10000); // 生成隨機數以獲取不同圖片
bgDiv.style.backgroundImage = `url('https://picsum.photos/400/900?random=${randomNum}')`;
}
// 設定「現在時間」到 datetime-local
function setDefaultDateTime() {
const now = new Date();
const localISO = new Date(now.getTime() - now.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16); // 'YYYY-MM-DDTHH:MM'
document.getElementById('dateTime').value = localISO;
}
// 填充分類下拉
function populateCategorySelect(categories) {
const categorySelect = document.getElementById('category');
categorySelect.innerHTML = '';
// 根據選擇的類型篩選類別
const selectedType = document.getElementById('type').value;
categories.forEach(function(cat) {
if (cat.type === selectedType) {
const option = document.createElement('option');
option.value = cat.name;
option.text = cat.name;
categorySelect.appendChild(option);
}
});
}
// 根據「類型」篩選「類別」選項
function filterCategoryOptions() {
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
}
// 新增交易
function submitForm() {
const dateTime = document.getElementById('dateTime').value;
const type = document.getElementById('type').value;
const category = document.getElementById('category').value;
const amount = document.getElementById('amount').value;
const note = document.getElementById('note').value;
// 簡易驗證
if (!dateTime || !type || !category || !amount) {
showMessage('請填寫完整資訊', true);
return;
}
const transaction = {
dateTime: dateTime,
type: type,
category: category,
amount: amount,
note: note
};
google.script.run
.withSuccessHandler(function(msg) {
showMessage(msg, false);
setDefaultDateTime();
document.getElementById('amount').value = '';
document.getElementById('note').value = '';
// 刷新預算
refreshDashboard();
})
.withFailureHandler(function(err) {
showMessage('寫入失敗: ' + err.message, true);
})
.saveTransaction(transaction);
}
// 顯示訊息
function showMessage(msg, isError) {
const msgBox = document.getElementById('msgBox');
msgBox.style.color = isError ? 'red' : 'green';
msgBox.textContent = msg;
}
// 刷新預算表
function refreshDashboard() {
google.script.run
.withSuccessHandler(function(dashboardData){
populateBudgetTable(dashboardData);
})
.getDashboardData();
}
function populateBudgetTable(data) {
const tbody = document.getElementById('budgetTableBody');
tbody.innerHTML = '';
data.forEach(function(item){
const tr = document.createElement('tr');
if (item.warningLevel === 'over') {
tr.className = 'warning-over';
} else if (item.warningLevel === 'near') {
tr.className = 'warning-near';
} else {
tr.className = 'warning-normal';
}
const tdCat = document.createElement('td');
tdCat.textContent = item.category;
const tdBudget = document.createElement('td');
tdBudget.textContent = item.budget || 0;
const tdSpent = document.createElement('td');
tdSpent.textContent = item.spent;
tr.appendChild(tdCat);
tr.appendChild(tdBudget);
tr.appendChild(tdSpent);
tbody.appendChild(tr);
});
}
/********************************************
* 新增:查看「月出支」按鈕的功能
********************************************/
let monthlyExpenses = []; // 用於儲存當月支出資料
let filteredExpenses = []; // 用於儲存過濾後的支出資料
// 1) 填充月份和年份選擇器
function populateMonthYearSelectors() {
const selectMonth = document.getElementById('selectMonth');
const selectYear = document.getElementById('selectYear');
// 填充月份選項
const months = [
'一月', '二月', '三月', '四月', '五月', '六月',
'七月', '八月', '九月', '十月', '十一月', '十二月'
];
months.forEach(function(month, index) {
const option = document.createElement('option');
option.value = index; // 0-11
option.text = month;
selectMonth.appendChild(option);
});
// 填充年份選項(例如,從 2020 到當前年份 + 5)
const currentYear = new Date().getFullYear();
for(let year = currentYear - 5; year <= currentYear + 5; year++) {
const option = document.createElement('option');
option.value = year;
option.text = year;
selectYear.appendChild(option);
}
// 設定預設選擇為當前年月
selectMonth.value = new Date().getMonth(); // 0-11
selectYear.value = currentYear;
}
// 2) 取得指定月份支出 -> 顯示浮層
function viewMonthlyExpenses() {
const selectMonth = document.getElementById('selectMonth').value;
const selectYear = document.getElementById('selectYear').value;
google.script.run
.withSuccessHandler(function(expenses){
if (Array.isArray(expenses)) {
monthlyExpenses = expenses;
filteredExpenses = expenses; // 初始化為全部
applyFilters(); // 套用初始篩選(僅關鍵字)
renderMonthlyExpenses();
document.getElementById('monthlyExpensesModal').style.display = 'flex';
} else {
alert('取得的支出資料格式錯誤。');
}
})
.withFailureHandler(function(err){
alert('取得支出失敗:' + err.message);
})
.getExpenses(parseInt(selectMonth), parseInt(selectYear));
}
// 3) 渲染當月支出的表格
function renderMonthlyExpenses() {
const tbody = document.getElementById('expenseTableBody');
tbody.innerHTML = '';
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 4;
td.textContent = '本月尚無符合條件的支出紀錄。';
tr.appendChild(td);
tbody.appendChild(tr);
document.getElementById('totalAmount').textContent = '總計:0';
return;
}
// 計算總計
let total = 0;
filteredExpenses.forEach(function(item){
total += parseFloat(item.amount) || 0;
const tr = document.createElement('tr');
const tdDateTime = document.createElement('td');
tdDateTime.textContent = item.dateTime; // 格式化後的日期時間
const tdCategory = document.createElement('td');
tdCategory.textContent = item.category;
const tdAmount = document.createElement('td');
tdAmount.textContent = item.amount;
const tdNote = document.createElement('td');
tdNote.textContent = item.note;
tr.appendChild(tdDateTime);
tr.appendChild(tdCategory);
tr.appendChild(tdAmount);
tr.appendChild(tdNote);
tbody.appendChild(tr);
});
// 顯示總計
document.getElementById('totalAmount').textContent = '總計:' + total;
}
// 4) 按類別排序 (升冪) 後重新渲染
function sortExpensesByCategory() {
if (!Array.isArray(filteredExpenses)) return;
filteredExpenses.sort(function(a, b){
return a.category.localeCompare(b.category, 'zh-TW');
});
renderMonthlyExpenses();
}
// 5) 關閉浮層
function closeMonthlyExpensesModal() {
document.getElementById('monthlyExpensesModal').style.display = 'none';
}
// 6) 套用篩選(僅關鍵字)
function applyFilters() {
const keyword = document.getElementById('searchKeyword').value.trim().toLowerCase();
filteredExpenses = monthlyExpenses.filter(function(expense){
let matchesKeyword = true;
// 檢查關鍵字(類別或備註)
if (keyword) {
const categoryMatch = expense.category.toLowerCase().includes(keyword);
const noteMatch = expense.note.toLowerCase().includes(keyword);
matchesKeyword = categoryMatch || noteMatch;
}
return matchesKeyword;
});
renderMonthlyExpenses();
}
// 7) 匯出為 CSV
function exportToCSV() {
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
alert('沒有可匯出的資料。');
return;
}
// 構建 CSV 資料
let csvContent = "data:text/csv;charset=utf-8,";
csvContent += "日期時間,類別,金額,備註\n"; // 標題
filteredExpenses.forEach(function(item){
const row = `"${item.dateTime}","${item.category}","${item.amount}","${item.note}"`;
csvContent += row + "\n";
});
// 觸發下載
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
const now = new Date();
const timestamp = now.getFullYear() + "-" +
String(now.getMonth()+1).padStart(2, '0') + "-" +
String(now.getDate()).padStart(2, '0') + "_" +
String(now.getHours()).padStart(2, '0') + "-" +
String(now.getMinutes()).padStart(2, '0') + "-" +
String(now.getSeconds()).padStart(2, '0');
link.setAttribute("download", `本月支出_${timestamp}.csv`);
document.body.appendChild(link); // Required for FF
link.click(); // This will download the data file named "本月支出_timestamp.csv"
document.body.removeChild(link);
}
/********************************************
* 新增:點擊浮層外部關閉浮層
********************************************/
window.onclick = function(event) {
const modal = document.getElementById('monthlyExpensesModal');
if (event.target === modal) {
modal.style.display = 'none';
}
}
</script>
</body>
</html>
CSS 調整:
#background 的 opacity 從 0.2 保持不變,因為你已經要求設為20%。z-index 從 0 改為 -100,確保背景圖片位於最下層。pointer-events: none,確保背景層不會攔截任何滑鼠事件,避免影響頁面上的互動元素。/* 背景圖片樣式 */
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
opacity: 0.2; /* 設為20%的透明度 */
z-index: -100; /* 確保在所有內容後面 */
pointer-events: none; /* 確保不會攔截滑鼠事件 */
}
CSS 調整:
.budget-table th 添加 background-color: #add8e6;,這是淡藍色的十六進位色碼。/* 將表格標題欄背景改為淡藍色 */
.budget-table th {
background-color: #add8e6; /* 淡藍色 */
}
CSS 調整:
body 的 z-index 為 1,而 .modal-content 的 z-index 為 1000,這樣所有內容都會位於背景圖片之上。/* 調整層級 */
body {
z-index: 1; /* 內容層 */
}
.modal-content {
z-index: 1000; /* 確保內容在背景層之上 */
}
更新 Code.gs:
Code.gs 已包含之前步驟中的所有功能,特別是 getExpenses(month, year) 函式。Code.gs,確認是否需要進一步調整以配合新的前端功能。更新 Index.html:
Index.html 替換為上述提供的完整 HTML 內容。重新部署應用程式:
測試功能:
Transactions 工作表中。修正與優化:
背景圖片無法顯示或顯示不正確:
<body> 標籤內新增了 <div id="background"></div>。setRandomBackground 函式中。setRandomBackground 函式在 DOMContentLoaded 事件中被正確呼叫。https://picsum.photos/400/900?random=${randomNum} 的格式正確,且隨機數字生成沒有問題。背景圖片透明度影響到其他內容:
z-index 設定:z-index 設定為 -100,而內容層的 z-index 設定為更高的值(如 1 和 1000)。opacity 設定:opacity: 0.2,而不影響到其他內容層。pointer-events 設定:pointer-events: none 已經添加到 #background,防止背景層攔截滑鼠事件。背景圖片尺寸不符合預期:
https://picsum.photos/800/1600?random=${randomNum} 以獲得更大尺寸的圖片。背景圖片載入速度慢或影響頁面性能:
其他功能受到背景圖片的影響:
z-index 和 position 屬性來控制各層級的顯示順序。z-index:body 和 .modal-content 的 z-index 設定高於 #background,以確保內容層在背景層之上。透過以上的修改,你的記帳工具將具備以下新增功能:
隨機背景圖片功能:
https://picsum.photos/400/900?random=${randomNum} 隨機選擇一張圖片顯示。新增全寬「X」關閉按鈕:
搜尋框與送出按鈕位置與寬度調整:
移除不必要的篩選按鈕:
保留並優化其他功能:
整體介面優化:
防錯處理與使用者體驗提升:
請按照上述步驟進行更新與部署,並在使用過程中進行測試以確保所有功能正常運作。如有任何進一步的問題或需求,歡迎隨時提出!
按下"新增交易"下方呈現"送出中....勿重複點擊"文字提示,直到出現"交易已儲存!","交易已儲存!"文字只呈現3秒,即自動消失
思考 花的時間為 8 秒
了解,我將協助你根據最新需求更新 Index.html。具體修改如下:
為「新增交易」按鈕添加 id:
id="submitBtn" 以便在 JavaScript 中方便地控制按鈕的狀態(啟用/禁用)。修改 submitForm() 函式:
以下是更新後的完整 Index.html:
Index.html<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
/* 手機友善基礎樣式 */
body {
font-family: sans-serif;
margin: 10px;
position: relative; /* 為了確保背景層在後 */
z-index: 1; /* 確保內容在背景層之上 */
}
h1 {
text-align: center;
margin-bottom: 0;
}
.sub-title {
text-align: center;
color: #666;
margin-top: 4px;
margin-bottom: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.input-row {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
.input-row .input-field {
flex: 0 0 48%;
}
label {
display: block;
font-weight: bold;
margin-bottom: 4px;
}
input, select {
width: 100%;
box-sizing: border-box;
padding: 8px;
font-size: 16px;
}
button {
width: 100%;
padding: 10px;
font-size: 16px;
background: #4CAF50;
color: #fff;
border: none;
cursor: pointer;
margin-top: 10px;
border-radius: 4px;
}
button:hover {
background: #45a049;
}
/* 預算表 */
.budget-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.budget-table th, .budget-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
/* 將表格標題欄背景改為淡藍色 */
.budget-table th {
background-color: #add8e6; /* 淡藍色 */
}
.warning-normal {
background-color: #ffffff;
}
.warning-near {
background-color: #fff8db; /* 淡黃色 */
}
.warning-over {
background-color: #ffdcdc; /* 淡紅色 */
}
.message {
margin-top: 8px;
text-align: center;
}
/* 浮層 (Modal) 樣式 */
.modal-overlay {
position: fixed;
z-index: 999; /* 確保浮層在最上層 */
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景 */
display: none; /* 初始隱藏 */
justify-content: center;
align-items: center;
}
.modal-content {
background: #fff;
padding: 20px;
border-radius: 8px;
width: 95%;
max-width: 800px;
max-height: 90vh;
overflow-y: auto;
position: relative;
z-index: 1000; /* 確保內容在背景層之上 */
}
/* 新增的全寬關閉按鈕 */
.modal-close-btn {
width: 100%;
background: #f44336;
color: #fff;
border: none;
padding: 10px 0;
font-size: 16px;
cursor: pointer;
border-radius: 4px;
margin-bottom: 15px;
}
.modal-close-btn:hover {
background: #da190b;
}
/* 調整後的模態框標題 */
.modal-header {
display: flex;
width: 100%;
align-items: center;
margin-bottom: 15px;
}
.modal-header h2 {
flex: 0 0 50%; /* 佔據一半寬度 */
text-align: left;
margin: 0;
font-size: 20px;
}
/* 搜尋框和送出按鈕調整 */
.search-container {
display: flex;
align-items: center;
flex: 0 0 50%; /* 佔據剩餘的一半寬度 */
}
.search-container input[type="text"] {
flex: 0 0 50%; /* 佔據四分之一總寬度 */
padding: 6px;
font-size: 14px;
}
.search-container button {
flex: 0 0 50%; /* 佔據四分之一總寬度 */
padding: 6px 12px;
margin-left: 5px;
font-size: 14px;
background: #2196F3;
border-radius: 4px;
}
.search-container button:hover {
background: #0b7dda;
}
.expense-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
}
.expense-table th, .expense-table td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
.sort-btn, .export-btn {
background: #2196F3;
color: #fff;
padding: 8px 12px;
border: none;
cursor: pointer;
border-radius: 4px;
margin-right: 10px;
margin-bottom: 10px;
}
.sort-btn:hover, .export-btn:hover {
background: #0b7dda;
}
/* 背景圖片樣式 */
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
opacity: 0.2; /* 設為20%的透明度 */
z-index: -100; /* 確保在所有內容後面 */
pointer-events: none; /* 確保不會攔截滑鼠事件 */
}
/* 滾動條樣式(選用) */
.modal-content::-webkit-scrollbar {
width: 8px;
}
.modal-content::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.2);
border-radius: 4px;
}
</style>
</head>
<body>
<!-- 背景圖片容器 -->
<div id="background"></div>
<h1>簡易記帳工具</h1>
<div class="sub-title">Google Apps Script + 試算表</div>
<div class="container">
<!-- 新增交易表單 -->
<div class="input-row">
<!-- 類型 -->
<div class="input-field">
<label for="type">類型</label>
<select id="type" onchange="filterCategoryOptions()">
<option value="支出" selected>支出</option>
<option value="收入">收入</option>
</select>
</div>
<!-- 類別 -->
<div class="input-field">
<label for="category">類別</label>
<select id="category"></select>
</div>
</div>
<div class="input-row">
<!-- 金額 -->
<div class="input-field">
<label for="amount">金額</label>
<input type="number" id="amount" step="0.01" />
</div>
<!-- 備註 -->
<div class="input-field">
<label for="note">備註</label>
<input type="text" id="note" />
</div>
</div>
<!-- 日期與時間 -->
<div class="input-field">
<label for="dateTime">日期與時間</label>
<input type="datetime-local" id="dateTime" />
</div>
<!-- 為按鈕添加 id="submitBtn" -->
<button id="submitBtn" onclick="submitForm()">新增交易</button>
<div class="message" id="msgBox"></div>
<!-- 查看月出支按鈕 -->
<button style="background:#f44336; margin-top:20px;" onclick="viewMonthlyExpenses()">
查看月出支
</button>
<!-- 預算表 -->
<table class="budget-table" id="budgetTable">
<thead>
<tr>
<th>類別</th>
<th>預算</th>
<th>本月已用</th>
</tr>
</thead>
<tbody id="budgetTableBody"></tbody>
</table>
</div>
<!-- 浮層 (Modal) 內容 -->
<div class="modal-overlay" id="monthlyExpensesModal">
<div class="modal-content">
<!-- 新增的全寬關閉按鈕 -->
<button class="modal-close-btn" onclick="closeMonthlyExpensesModal()">X</button>
<div class="modal-header">
<h2>本月支出列表</h2>
<div class="search-container">
<input type="text" id="searchKeyword" placeholder="搜尋關鍵字(類別或備註)" />
<button onclick="applyFilters()">送出</button>
</div>
</div>
<!-- 月份和年份選擇區域 -->
<div style="margin-bottom: 15px; display: flex; justify-content: space-between;">
<div class="input-field" style="flex: 0 0 48%;">
<label for="selectMonth">選擇月份</label>
<select id="selectMonth" onchange="viewMonthlyExpenses()">
<!-- 月份選項由 JavaScript 動態生成 -->
</select>
</div>
<div class="input-field" style="flex: 0 0 48%;">
<label for="selectYear">選擇年份</label>
<select id="selectYear" onchange="viewMonthlyExpenses()">
<!-- 年份選項由 JavaScript 動態生成 -->
</select>
</div>
</div>
<!-- 排序及匯出按鈕 -->
<div style="margin-bottom: 15px;">
<button class="sort-btn" onclick="sortExpensesByCategory()">
按「類別」排序
</button>
<button class="export-btn" onclick="exportToCSV()">
匯出為 CSV
</button>
</div>
<!-- 支出表格 -->
<table class="expense-table" id="expenseTable">
<thead>
<tr>
<th>日期時間</th>
<th>類別</th>
<th>金額</th>
<th>備註</th>
</tr>
</thead>
<tbody id="expenseTableBody"></tbody>
</table>
<!-- 總計 -->
<div id="totalAmount" style="text-align:right; font-weight:bold;"></div>
</div>
</div>
<script>
/********************************************
* 前端初始化:載入「分類清單」與「預算表」
********************************************/
document.addEventListener('DOMContentLoaded', function() {
setRandomBackground(); // 設定隨機背景圖片
setDefaultDateTime();
populateMonthYearSelectors();
// 載入分類
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
// 載入預算
refreshDashboard();
});
// 設定隨機背景圖片
function setRandomBackground() {
const bgDiv = document.getElementById('background');
const randomNum = Math.floor(Math.random() * 10000); // 生成隨機數以獲取不同圖片
bgDiv.style.backgroundImage = `url('https://picsum.photos/400/900?random=${randomNum}')`;
}
// 設定「現在時間」到 datetime-local
function setDefaultDateTime() {
const now = new Date();
const localISO = new Date(now.getTime() - now.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16); // 'YYYY-MM-DDTHH:MM'
document.getElementById('dateTime').value = localISO;
}
// 填充分類下拉
function populateCategorySelect(categories) {
const categorySelect = document.getElementById('category');
categorySelect.innerHTML = '';
// 根據選擇的類型篩選類別
const selectedType = document.getElementById('type').value;
categories.forEach(function(cat) {
if (cat.type === selectedType) {
const option = document.createElement('option');
option.value = cat.name;
option.text = cat.name;
categorySelect.appendChild(option);
}
});
}
// 根據「類型」篩選「類別」選項
function filterCategoryOptions() {
google.script.run.withSuccessHandler(function(categories){
populateCategorySelect(categories);
}).getCategories();
}
// 新增交易
function submitForm() {
const submitBtn = document.getElementById('submitBtn');
const msgBox = document.getElementById('msgBox');
const dateTime = document.getElementById('dateTime').value;
const type = document.getElementById('type').value;
const category = document.getElementById('category').value;
const amount = document.getElementById('amount').value;
const note = document.getElementById('note').value;
// 簡易驗證
if (!dateTime || !type || !category || !amount) {
showMessage('請填寫完整資訊', true);
return;
}
const transaction = {
dateTime: dateTime,
type: type,
category: category,
amount: amount,
note: note
};
// 禁用按鈕並顯示「送出中....勿重複點擊」
submitBtn.disabled = true;
showMessage('送出中....勿重複點擊', false);
google.script.run
.withSuccessHandler(function(msg) {
// 顯示「交易已儲存!」訊息
showMessage('交易已儲存!', false);
setDefaultDateTime();
document.getElementById('amount').value = '';
document.getElementById('note').value = '';
// 刷新預算
refreshDashboard();
// 設定3秒後清除訊息並啟用按鈕
setTimeout(function(){
showMessage('', false);
submitBtn.disabled = false;
}, 3000);
})
.withFailureHandler(function(err) {
// 顯示錯誤訊息並啟用按鈕
showMessage('寫入失敗: ' + err.message, true);
submitBtn.disabled = false;
})
.saveTransaction(transaction);
}
// 顯示訊息
function showMessage(msg, isError) {
const msgBox = document.getElementById('msgBox');
msgBox.style.color = isError ? 'red' : 'green';
msgBox.textContent = msg;
}
// 刷新預算表
function refreshDashboard() {
google.script.run
.withSuccessHandler(function(dashboardData){
populateBudgetTable(dashboardData);
})
.getDashboardData();
}
function populateBudgetTable(data) {
const tbody = document.getElementById('budgetTableBody');
tbody.innerHTML = '';
data.forEach(function(item){
const tr = document.createElement('tr');
if (item.warningLevel === 'over') {
tr.className = 'warning-over';
} else if (item.warningLevel === 'near') {
tr.className = 'warning-near';
} else {
tr.className = 'warning-normal';
}
const tdCat = document.createElement('td');
tdCat.textContent = item.category;
const tdBudget = document.createElement('td');
tdBudget.textContent = item.budget || 0;
const tdSpent = document.createElement('td');
tdSpent.textContent = item.spent;
tr.appendChild(tdCat);
tr.appendChild(tdBudget);
tr.appendChild(tdSpent);
tbody.appendChild(tr);
});
}
/********************************************
* 新增:查看「月出支」按鈕的功能
********************************************/
let monthlyExpenses = []; // 用於儲存當月支出資料
let filteredExpenses = []; // 用於儲存過濾後的支出資料
// 1) 填充月份和年份選擇器
function populateMonthYearSelectors() {
const selectMonth = document.getElementById('selectMonth');
const selectYear = document.getElementById('selectYear');
// 填充月份選項
const months = [
'一月', '二月', '三月', '四月', '五月', '六月',
'七月', '八月', '九月', '十月', '十一月', '十二月'
];
months.forEach(function(month, index) {
const option = document.createElement('option');
option.value = index; // 0-11
option.text = month;
selectMonth.appendChild(option);
});
// 填充年份選項(例如,從 2020 到當前年份 + 5)
const currentYear = new Date().getFullYear();
for(let year = currentYear - 5; year <= currentYear + 5; year++) {
const option = document.createElement('option');
option.value = year;
option.text = year;
selectYear.appendChild(option);
}
// 設定預設選擇為當前年月
selectMonth.value = new Date().getMonth(); // 0-11
selectYear.value = currentYear;
}
// 2) 取得指定月份支出 -> 顯示浮層
function viewMonthlyExpenses() {
const selectMonth = document.getElementById('selectMonth').value;
const selectYear = document.getElementById('selectYear').value;
google.script.run
.withSuccessHandler(function(expenses){
if (Array.isArray(expenses)) {
monthlyExpenses = expenses;
filteredExpenses = expenses; // 初始化為全部
applyFilters(); // 套用初始篩選(僅關鍵字)
renderMonthlyExpenses();
document.getElementById('monthlyExpensesModal').style.display = 'flex';
} else {
alert('取得的支出資料格式錯誤。');
}
})
.withFailureHandler(function(err){
alert('取得支出失敗:' + err.message);
})
.getExpenses(parseInt(selectMonth), parseInt(selectYear));
}
// 3) 渲染當月支出的表格
function renderMonthlyExpenses() {
const tbody = document.getElementById('expenseTableBody');
tbody.innerHTML = '';
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 4;
td.textContent = '本月尚無符合條件的支出紀錄。';
tr.appendChild(td);
tbody.appendChild(tr);
document.getElementById('totalAmount').textContent = '總計:0';
return;
}
// 計算總計
let total = 0;
filteredExpenses.forEach(function(item){
total += parseFloat(item.amount) || 0;
const tr = document.createElement('tr');
const tdDateTime = document.createElement('td');
tdDateTime.textContent = item.dateTime; // 格式化後的日期時間
const tdCategory = document.createElement('td');
tdCategory.textContent = item.category;
const tdAmount = document.createElement('td');
tdAmount.textContent = item.amount;
const tdNote = document.createElement('td');
tdNote.textContent = item.note;
tr.appendChild(tdDateTime);
tr.appendChild(tdCategory);
tr.appendChild(tdAmount);
tr.appendChild(tdNote);
tbody.appendChild(tr);
});
// 顯示總計
document.getElementById('totalAmount').textContent = '總計:' + total;
}
// 4) 按類別排序 (升冪) 後重新渲染
function sortExpensesByCategory() {
if (!Array.isArray(filteredExpenses)) return;
filteredExpenses.sort(function(a, b){
return a.category.localeCompare(b.category, 'zh-TW');
});
renderMonthlyExpenses();
}
// 5) 關閉浮層
function closeMonthlyExpensesModal() {
document.getElementById('monthlyExpensesModal').style.display = 'none';
}
// 6) 套用篩選(僅關鍵字)
function applyFilters() {
const keyword = document.getElementById('searchKeyword').value.trim().toLowerCase();
filteredExpenses = monthlyExpenses.filter(function(expense){
let matchesKeyword = true;
// 檢查關鍵字(類別或備註)
if (keyword) {
const categoryMatch = expense.category.toLowerCase().includes(keyword);
const noteMatch = expense.note.toLowerCase().includes(keyword);
matchesKeyword = categoryMatch || noteMatch;
}
return matchesKeyword;
});
renderMonthlyExpenses();
}
// 7) 匯出為 CSV
function exportToCSV() {
if (!Array.isArray(filteredExpenses) || filteredExpenses.length === 0) {
alert('沒有可匯出的資料。');
return;
}
// 構建 CSV 資料
let csvContent = "data:text/csv;charset=utf-8,";
csvContent += "日期時間,類別,金額,備註\n"; // 標題
filteredExpenses.forEach(function(item){
const row = `"${item.dateTime}","${item.category}","${item.amount}","${item.note}"`;
csvContent += row + "\n";
});
// 觸發下載
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
const now = new Date();
const timestamp = now.getFullYear() + "-" +
String(now.getMonth()+1).padStart(2, '0') + "-" +
String(now.getDate()).padStart(2, '0') + "_" +
String(now.getHours()).padStart(2, '0') + "-" +
String(now.getMinutes()).padStart(2, '0') + "-" +
String(now.getSeconds()).padStart(2, '0');
link.setAttribute("download", `本月支出_${timestamp}.csv`);
document.body.appendChild(link); // Required for FF
link.click(); // This will download the data file named "本月支出_timestamp.csv"
document.body.removeChild(link);
}
/********************************************
* 新增:點擊浮層外部關閉浮層
********************************************/
window.onclick = function(event) {
const modal = document.getElementById('monthlyExpensesModal');
if (event.target === modal) {
modal.style.display = 'none';
}
}
</script>
</body>
</html>
idHTML 調整:
id="submitBtn",以便在 JavaScript 中控制按鈕的啟用和禁用狀態。<!-- 為按鈕添加 id="submitBtn" -->
<button id="submitBtn" onclick="submitForm()">新增交易</button>
submitForm() 函式JavaScript 調整:
submitForm() 函式中,首先禁用「新增交易」按鈕,並在 msgBox 顯示「送出中....勿重複點擊」。msgBox 設為「交易已儲存!」,並使用 setTimeout 在3秒後清除訊息並重新啟用按鈕。// 新增交易
function submitForm() {
const submitBtn = document.getElementById('submitBtn');
const msgBox = document.getElementById('msgBox');
const dateTime = document.getElementById('dateTime').value;
const type = document.getElementById('type').value;
const category = document.getElementById('category').value;
const amount = document.getElementById('amount').value;
const note = document.getElementById('note').value;
// 簡易驗證
if (!dateTime || !type || !category || !amount) {
showMessage('請填寫完整資訊', true);
return;
}
const transaction = {
dateTime: dateTime,
type: type,
category: category,
amount: amount,
note: note
};
// 禁用按鈕並顯示「送出中....勿重複點擊」
submitBtn.disabled = true;
showMessage('送出中....勿重複點擊', false);
google.script.run
.withSuccessHandler(function(msg) {
// 顯示「交易已儲存!」訊息
showMessage('交易已儲存!', false);
setDefaultDateTime();
document.getElementById('amount').value = '';
document.getElementById('note').value = '';
// 刷新預算
refreshDashboard();
// 設定3秒後清除訊息並啟用按鈕
setTimeout(function(){
showMessage('', false);
submitBtn.disabled = false;
}, 3000);
})
.withFailureHandler(function(err) {
// 顯示錯誤訊息並啟用按鈕
showMessage('寫入失敗: ' + err.message, true);
submitBtn.disabled = false;
})
.saveTransaction(transaction);
}
CSS 調整:
#background 的 z-index 從 -100 改為 -100(保持不變),並確保其他內容層的 z-index 高於背景層。pointer-events: none 已被添加到 #background,以防止背景層攔截滑鼠事件,確保所有互動元素正常工作。/* 背景圖片樣式 */
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
opacity: 0.2; /* 設為20%的透明度 */
z-index: -100; /* 確保在所有內容後面 */
pointer-events: none; /* 確保不會攔截滑鼠事件 */
}
/* 調整層級 */
body {
z-index: 1; /* 內容層 */
}
.modal-content {
z-index: 1000; /* 確保內容在背景層之上 */
}
更新 Code.gs:
Code.gs 已包含之前步驟中的所有功能,特別是 getExpenses(month, year) 函式。Code.gs,確認是否需要進一步調整以配合新的前端功能。更新 Index.html:
Index.html 替換為上述提供的完整 HTML 內容。重新部署應用程式:
測試功能:
Transactions 工作表中。修正與優化:
訊息提示無法正常顯示或按鈕無法禁用/啟用:
確認按鈕 id 是否正確:
id="submitBtn"。<button id="submitBtn" onclick="submitForm()">新增交易</button>
檢查 JavaScript 函式是否正確執行:
submitForm() 函式中。submitForm() 函式中正確獲取按鈕和訊息框元素。訊息未能在3秒後自動消失:
確認 setTimeout 是否正確設置:
setTimeout 的時間設定為3000毫秒(3秒)。setTimeout(function(){
showMessage('', false);
submitBtn.disabled = false;
}, 3000);
檢查訊息清除邏輯是否正確:
showMessage('', false); 能正確清除訊息。背景圖片遮蔽內容或無法正常顯示:
確認 z-index 設定:
#background 的 z-index 設定為 -100,而 body 和 .modal-content 的 z-index 高於 #background。/* 背景圖片樣式 */
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
opacity: 0.2; /* 設為20%的透明度 */
z-index: -100; /* 確保在所有內容後面 */
pointer-events: none; /* 確保不會攔截滑鼠事件 */
}
/* 調整層級 */
body {
z-index: 1; /* 內容層 */
}
.modal-content {
z-index: 1000; /* 確保內容在背景層之上 */
}
確認 pointer-events: none 已正確應用:
pointer-events: none; 已經添加到 #background,以防止背景層攔截滑鼠事件。按鈕無法正確禁用或啟用:
submitBtn 的獲取是否正確:submitForm() 函式中,正確使用 document.getElementById('submitBtn') 獲取按鈕元素。交易儲存後預算表未刷新:
refreshDashboard() 函式是否正確執行:refreshDashboard(),並且該函式能正確獲取並顯示預算數據。Code.gs 中相關函式的實現:getDashboardData() 函式正確實現並返回所需的數據。透過以上的修改,你的記帳工具將具備以下新增功能:
新增「送出中....勿重複點擊」與「交易已儲存!」訊息提示:
調整背景圖片層級與透明度:
調整預算表標題欄背景顏色:
保留並優化其他功能:
整體介面優化:
防錯處理與使用者體驗提升:
請按照上述步驟進行更新與部署,並在使用過程中進行測試以確保所有功能正常運作。如有任何進一步的問題或需求,歡迎隨時提出!
謝謝
不客气!😊 如果还有其他问题或需要进一步调整,随时可以找我!祝你开发顺利,工具使用愉快!🚀✨
Powered by ChatGPT Exporter