【教學】不用寫程式!打造專屬 LINE 記帳管家,30 分鐘學會隱私 100% 自動記帳

更新 發佈閱讀 70 分鐘
覺得市面上的記帳 APP 太複雜、廣告太多,又不放心把個人財務資料交給第三方嗎?

本篇文章,將手把手教你如何「不寫任何一行程式」,只要透過「複製+貼上」,就能打造一個專屬於你個人的 LINE 記帳管家!不僅能透過 LINE 快速記帳、自動結算每月報表,所有資料還會乖乖躺在你自己的 Google 試算表裡,隱私度 100%!


📑 文章目錄

🌟 為什麼你該擁有這台專屬記帳機器人?(超強功能介紹)🎒 準備工具🛠️ 實戰教學步驟

  • 步驟一:建立專屬資料庫 (Google 試算表)
  • 步驟二:取得 LINE 機器人的「身體」 (LINE Developers)
  • 步驟三:喚醒機器人的「大腦」 (Google Apps Script)
  • 步驟四:讓大腦與身體連線 (部署與 Webhook)💎 進階解鎖:「每月 1 號自動推播」報表功能!🎉
  • 步驟五:大功告成,開始記帳!
  • 📖 實戰演練:小幫手使用說明 (指令大全)
  • 🛠️ 常見 Q&A:實作卡關除錯指南
  • 🤖 附錄:遇到 Bug 怎麼呼叫 AI 救援?
  • 🎉 結語:你的專屬財富管家,正式上線!
  • 20260412 update
  • Code in here


🌟 為什麼你該擁有這台專屬記帳機器人?(超強功能介紹)

在這個極簡的聊天視窗背後,其實隱藏著媲美付費 APP 的旗艦級功能,而且 「無廣告、免月租、資料 100% 歸你管」:

  • ⚡ 極速記帳,絕不廢話: 不用等 APP 載入、不用點擊繁瑣的下拉選單。像跟朋友聊天一樣打字,一秒就記好帳。🛡️ 防呆機制,隨時反悔: 記錯了?直接輸入「修改」;想刪掉?輸入「刪除」;不小心刪錯了?輸入「復原」一秒救回!容錯率 100%。⏳ 時光機補登功能: 昨天太累忘記記帳?直接開頭加上「昨天」或是指定日期「3/12」,帳務時間軸不混亂。🎯 主動式預算管家: 設定每月預算後,每記一筆帳都會幫你結算「本月還剩多少錢」。快超支時,還會跳出嚴厲的警告!💸📊 自動生成絕美圓餅圖: 想看結算?一句話,機器人立刻畫出精美的花費圓餅圖,你的錢都去哪了一目了然。⏰ 每月 1 號自動推播 (終極大絕招): 每個月的第一天,它會主動發送「上個月的結算報表」與「本月的預算提醒」給你,完全不用你自己動手查!


準備工具:

  1. 一個 Google 帳號 (Gmail)你的 LINE 帳號大約30分鐘的空閒時間

準備好了嗎?我們開始吧!🚀


步驟一:建立專屬資料庫 (Google 試算表)

機器人幫你記下的每一筆帳,都需要一個家。我們就用大家最熟悉的 Google 試算表來當資料庫。

  1. 打開你的 Google 雲端硬碟,新增一份空白的 Google 試算表。將檔名改為「我的 LINE 記帳本」(或任何你喜歡的名字)。在第一列 (A1 到 D1) 分別填上表頭:日期、分類、金額、說明。

(圖:設定好四個基本的欄位,這就是機器人寫入資料的地方)

步驟二:取得 LINE 機器人的「身體」(LINE Developers)

接下來,我們要去 LINE 的官方開發者後台,申請一個免費的機器人帳號。

  1. 進入 LINE Developers 官方網站,點擊右上角 Log In,用你平常的 LINE 帳號登入。點擊 Create a new provider (開發者名稱,填你的名字即可)。在 Provider 頁面中,選擇 Create a Messaging API channel。
  • Channel name: 給機器人取個名字(例如:專屬財管小幫手)。Channel description: 隨便填(例如:我的記帳機器人)。Category / Subcategory: 依照喜好選擇。勾選同意條款後,點擊 Create。
vocus|新世代的創作平台
vocus|新世代的創作平台

(圖:建立 Messaging API 頻道,這就是你的機器人本體)

4. 建立完成後,點擊進入你的機器人,切換到 Messaging API 頁籤。

5. 往下滑到底,找到 Channel access token (long-lived),點擊 Issue 按鈕。

⚠️ 重要: 將產生出來的這一長串亂碼(Token)複製下來,先貼到記事本裡備用。這把鑰匙千萬不能給別人!

vocus|新世代的創作平台
vocus|新世代的創作平台

(圖:取得並複製 Channel access token)

步驟三:喚醒機器人的「大腦」(Google Apps Script)

現在,我們要給這個機器人注入靈魂,也就是貼上幫你寫好的程式碼。

>>code在最後底下<<

  1. 回到剛才的 Google 試算表。點擊上方選單的 「擴充功能」 > 「Apps Script」。畫面會跳轉到一個寫程式的網頁。請將裡面的原始程式碼全部刪除(變成一片空白)。

(圖:從試算表進入 Apps Script 編輯器)

4. 將我準備好的「旗艦版記帳程式碼」全部複製並貼上。

5. ⚠️ 關鍵步驟: 找到程式碼的第 2 行 var CHANNEL_ACCESS_TOKEN = '請貼上您的_CHANNEL_ACCESS_TOKEN';,把你剛剛在步驟二複製的超長 Token,貼在單引號 ' ' 裡面。


步驟四:讓大腦與身體連線 (部署與 Webhook)

最後一步!我們要讓 Google 雲端跟 LINE 串接起來。

  1. 在 Apps Script 編輯器右上角,點擊藍色按鈕 「部署」 > 「新增部署作業」。左上角點擊齒輪圖示 ⚙️,選擇 「網頁應用程式 (Web app)」。在設定畫面中:
  • 執行身分:選擇「你自己的 Email」。誰可以存取:一定要改成「所有人 (Anyone)」。(這樣 LINE 才能把訊息傳進來)。

4. 點擊 「部署」。(如果跳出授權視窗,請點擊「核准存取權」> 選擇你的帳號 > 點擊左下角「進階」>「前往專案(不安全)」,最後點擊「允許」)。

vocus|新世代的創作平台

(圖:部署網頁應用程式,記得存取權限要設為「所有人」)

  1. 部署成功後,會看到一串 「網頁應用程式網址 (Web App URL)」,請把它複製下來。回到剛剛的 LINE Developers 後台 的 Messaging API 頁籤。找到 Webhook URL 的欄位,點擊 Edit,把網址貼上去並點擊 Update。將下方的 Use webhook 開關打開。
vocus|新世代的創作平台

(圖:將 Google 的網址貼回 LINE 後台並開啟 Webhook)

解鎖「每月 1 號自動推播」報表功能!

市面上的記帳 APP 通常要付費解鎖才能有「主動提醒」功能,但我們的專屬管家可是免費內建了這個終極大絕招!

只要經過簡單的設定,每個月的 1 號早上 8 點,你的 LINE 就會自動收到一份「三合一大禮包」:

  1. 上個月的花費總結與圓餅圖這個月的預算上限提醒防止你忘記的指令說明書

完全不用自己手動查,超級貼心!以下是設定步驟(大約只要 1 分鐘):

⚠️ 關鍵前置作業: 在設定之前,請一定要先去 LINE 裡面,隨便跟你的機器人說一句話(例如輸入 說明 或是記一筆帳)。這個動作是為了讓機器人「偷偷記住你的專屬 User ID」,這樣每個月 1 號它才知道要把報表傳給誰喔!

⏰ 設定自動推播 (觸發條件):

  1. 回到剛剛寫程式的 Google Apps Script (GAS) 編輯器 網頁。看看網頁最左側的選單,點擊一個 「時鐘圖示 ⏰」(滑鼠移過去會顯示「觸發條件」)。進入頁面後,點擊右下角藍色的 「新增觸發條件」 按鈕。這時會跳出一個設定視窗,請照著以下選項完美設定:
  • 選擇要執行的功能: 選擇 sendMonthlyPush(這就是我們寫好的發送主程式)選擇應執行的部署作業: 選擇 Head選取事件來源: 選擇 時間驅動選取時間型觸發條件的類型: 選擇 月計時器選取日期: 選擇 1日選取時間: 選擇 上午 8 點到 9 點(你也可以選半夜或任何你喜歡的時間)

5. 點擊右下角的 「儲存」。

(💡 溫馨提醒:點擊儲存時,Google 可能會再跳出一次「需要授權」的視窗,請跟剛才一樣,點選你的帳號 > 進階 > 前往專案 > 允許 即可!)

[建議讀者在此處放一張「觸發條件設定視窗」的截圖,讓大家對照著選]

🎉 步驟五:大功告成,開始記帳!

恭喜你完成了專屬記帳機器人的設定!

在 LINE Developers 的 Messaging API 頁籤上方,有一個 QR Code。用你的手機掃描它,把你的機器人加入好友。

試著對它輸入:「說明」 如果它馬上回覆了一長串精美的圖文秘笈,就代表設定完美成功啦!接下來,你可以試著輸入 飲食 150 麥當勞,然後打開你的 Google 試算表看看,是不是神奇地自動寫進去了呢?

快跟著這份秘笈,體驗一下「私人專屬財富管家」的威力吧!


📖 實戰演練:小幫手使用說明 (指令大全)

將機器人加為好友後,就可以直接開始記帳啦! (💡 偷偷說:如果以後忘記怎麼用,隨時對機器人輸入 「說明」,它就會把這份秘笈傳給你喔!)

📝 【基礎記帳】 (注意:文字與數字中間請加半形空白)

  • 今天花費: 飲食 150 麥當勞補登昨天: 昨天 交通 50 捷運指定日期: 3/12 服裝 500 買衣服 或 0312 服裝 500 買衣服

🛠️ 【修改與刪除】

  • 發現上一筆記錯了: 修改 飲食 100 麥當勞刪除上一筆紀錄: 直接打 刪除刪錯反悔了: 直接打 復原

💰 【預算與結算報表】

  • 設定每個月的預算上限: 設定預算 15000呼叫當月報表與圓餅圖: this month呼叫其他區間報表: this week (本週)、last month (上個月)、this year (今年)、total (歷史總結)


🛠️ 常見 Q&A:實作卡關除錯指南

恭喜你完成了專屬機器人!但在實作過程中,如果遇到機器人不理你,或是跳出奇怪的錯誤訊息,請不要慌張,99% 的問題都能在這裡找到解答:

🚨 Q1:為什麼我在 Apps Script 按「執行」,卻跳出一堆紅字錯誤?

A1:這是正常的!千萬不要在網頁上按「執行」測試。 因為 Apps Script 的 doPost 主程式是專門用來接收 LINE 傳來的真實對話。你在網頁上直接按執行,程式抓不到「你是誰」和「你說了什麼」(沒有 replyToken),就會報錯。 👉 解法: 請忽略網頁上的報錯,直接拿出手機,在你的 LINE 機器人聊天室裡打字測試(例如:輸入 說明)。

🚨 Q2:為什麼我在 LINE 裡面對機器人講話,它「已讀不回」?

A2:通常是「大腦」跟「身體」沒有連線成功,或者是權限沒開對。 請檢查以下三個最常見的失誤:

  1. 網址沒更新: 每次修改程式碼後,一定要重新點擊「部署」>「管理部署作業」>「編輯(鉛筆圖示)」> 版本選擇「建立新版本」 >「部署」。如果你選了舊版本,或者忘記把新網址貼到 LINE 後台的 Webhook,機器人就吃不到新指令。權限設錯了: 檢查部署時,「誰可以存取 (Who has access)」是不是設定成「所有人 (Anyone)」。如果設成「只有我自己」,LINE 官方的伺服器就進不來。Webhook 沒打開: 回到 LINE Developers 後台,確認 Webhook URL 下方的「Use webhook」開關有打開。

🚨 Q3:機器人回傳了 400 Bad Request,不讓我記帳怎麼辦?

A3:這代表你的 Token 貼錯了,LINE 拒絕承認這個身分。 👉 解法: 回到 Apps Script 程式碼的第 2 行。請確認你貼上的 CHANNEL_ACCESS_TOKEN:

  • 有沒有不小心複製到頭尾的「空白鍵」?字串前後的「單引號 '」有沒有不小心被刪掉?確保長得像這樣(沒有多餘空格):var CHANNEL_ACCESS_TOKEN = '你的超長Token字串';

🚨 Q4:機器人說「格式錯誤!紀錄失敗」,但我明明照著打了?

A4:請檢查「空格」和「分類名稱」。

  1. 機器人非常嚴格,指令中的空格必須是 「半形空格」,不能連在一起。例如:飲食 150 麥當勞 (正確) vs 飲食150麥當勞 (錯誤)。你輸入的分類,必須是程式碼裡有預設的。 👉 解法: 如果忘記可用分類或格式,隨時輸入 說明,機器人就會把秘笈傳給你核對囉!


附錄:遇到 Bug 怎麼呼叫 AI 救援?

💡 AI 除錯溝通的「黃金四要素」

要讓 AI 瞬間變成你的專屬工程師,提問時必須包含這四個關鍵:

  1. 背景 (Context): 你在做什麼專案?用什麼技術?目標 (Goal): 你原本預期發生什麼事?症狀 (Error): 實際發生了什麼事?(有沒有紅字錯誤訊息?)線索 (Code): 提供相關的程式碼或設定截圖。

🛠️ 萬用除錯 Prompt 模板 (可直接複製貼上)

建議讀者在遇到問題時,直接複製以下這段文字,填空後丟給 AI(例如 ChatGPT、Gemini 或 Claude):

【請幫我排除程式錯誤】

1. 我的專案背景: 我正在做一個 LINE 記帳機器人,使用的是 Google Apps Script (GAS) 搭配 LINE Messaging API。

2. 我剛剛做了什麼動作: [請填寫你剛剛做了什麼,例如:我在 LINE 裡面輸入了「飲食 150 麥當勞」 / 我剛剛在 GAS 貼上了新程式碼並按下儲存]

3. 遇到的問題 / 錯誤訊息: [請填寫具體症狀,例如:LINE 機器人已讀不回 / GAS 畫面跳出紅字寫 “SyntaxError: Unexpected token ‘}’ 行數:274”]

4. 相關程式碼 (如果有): [請將出問題的那段程式碼,或是全部程式碼貼在這裡]

請告訴我:

為什麼會發生這個錯誤?

我應該如何修改?(請直接提供修改後的程式碼)

📝 實際對照範例 (Bad vs. Good)

您可以把這個對照表放在文章裡,讓讀者秒懂差異:

  • ❌ NG 的提問(AI 要通靈): 「為什麼我按執行會報錯?246 行有問題,救命!」 (AI OS:我不知道你的 246 行寫了什麼,也不知道你在寫什麼語言啊…)✅ 神級的提問(AI 秒解答): 「我正在用 GAS 寫 LINE 機器人。我剛貼上程式碼,在網頁上按了『執行』測試。結果 GAS 跳出錯誤:『246 行 UrlFetchApp.fetch(url, options) 發生問題』。請幫我看看怎麼解決?附上我的程式碼:(貼上程式碼)」 (AI OS:收到!因為你不能在網頁上按執行,沒有 LINE 傳來的憑證所以報錯,請去手機上測試!)


🎉 結語:你的專屬財富管家,正式上線!

走到這一步,你已經成功掌握了打造專屬 LINE 機器人的技術!這不僅是一個記帳工具,更是你踏入自動化領域的第一步。未來,你還可以根據自己的需求,繼續為它擴充更多客製化功能。

如果你覺得這篇文章對你有幫助,歡迎在 Medium 上給我拍手(可以拍 50 下喔!👏),也歡迎分享給需要記帳卻又找不到順手工具的朋友們!有任何問題,也歡迎在底下留言交流。(ps:AI產出率100%


[20260412 update ] LINE 專屬記帳管家再進化:歷史明細、分類結算與圓餅圖一次滿足!📊

如果你也喜歡在 LINE 上用最簡單、直覺的方式記帳,那麼這次的更新你絕對不能錯過!

過去,我們的「專屬記帳管家」解決了快速記帳、動態分類與預算提醒的需求;但隨著記帳筆數越來越多,不少朋友敲碗:「我想知道這個月到底把錢花去哪了?」、「我想查昨天買那杯咖啡多少錢!」

為了解決這個痛點,這次我們為記帳管家迎來了史詩級的更新!以下是本次新增的三大亮點功能:

1. 🔍 隨查隨看的「歷史明細查詢」

現在,你不需要打開 Google 試算表,直接在 LINE 裡面就能調出過去的消費明細!無論是想看單日花費,還是整個月的總結,只要一句話就能搞定:

  • 👉 查本月: 查詢 本月👉 查單日: 查詢 3/12 或 查詢 昨天👉 查本周: 查詢 本周

機器人會立刻列出該區間內的所有消費明細與總金額,對帳變得超級方便。

2. 📊 視覺化再升級:專屬「分類結算與圓餅圖」

光看落落長的明細文字沒感覺?這次更新直接把視覺化圖表內建進去了! 當你輸入查詢指令後,機器人不只會給你明細列表,還會「自動加總」各分類的花費,並直接在 LINE 裡推播一張專屬的分類圓餅圖。 哪一個項目的花費比例最高?是「飲食」、「娛樂」還是「服裝」?看一眼圖表就清清楚楚,幫助你更精準地抓出財務漏洞!

3. 🚀 查詢容量加倍:支援最高 200 筆顯示

針對記帳特別勤勞、每天都有超多筆消費的重度使用者,我們也把單次查詢的顯示上限從 100 筆一口氣放寬到 200 筆! (溫馨小提醒:因為 LINE 官方有單則文字 5000 字元的限制,所以如果在備註欄位寫太多字,還是要稍微留意一下字數喔!)


💡 要如何更新我的記帳管家?

如果你已經是記帳管家的使用者,只需將 Apps Script 專案中的 queryHistory 函式替換為最新版本的程式碼,複製貼上並儲存後,請記得點擊編輯器右上角的 「部署」 > 「管理部署作業」,點選鉛筆圖示將版本設為 「新版本」 後儲存部署。這樣您的 LINE 機器人就會正式套用這套最新心血結晶囉!

第一次新建新建的使用者,上方所附的程式已是最新版本的,與此程式碼相同,不用更新舊有包含此功能。

快來更新你的 LINE 記帳管家,讓理財變得更聰明、更直覺吧!有任何想法、回饋或想新增的功能,也歡迎在底下留言告訴我喔!👇

/**

* ==========================================
* 🌟 LINE 專屬記帳管家 (ver.20260412) 🌟 *
========================================== *
請將下方的 '請貼上您的_CHANNEL_ACCESS_TOKEN'
* 替換成您在 LINE Developers 後台取得的 Token。
* (注意:請保留前後的單引號 ' ') */

var CHANNEL_ACCESS_TOKEN = '請貼上您的_CHANNEL_ACCESS_TOKEN';

function doPost(e) {

var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();

var msg = JSON.parse(e.postData.contents);

var event = msg.events[0];

var replyToken = event.replyToken;

var userId = event.source.userId;

var props = PropertiesService.getScriptProperties();

props.setProperty('USER_ID', userId);

// ==========================================

// 🏷️ 讀取或初始化「動態分類」清單

// ==========================================

var categoriesStr = props.getProperty('customCategories');

var validCategories = [];

if (categoriesStr) {

validCategories = JSON.parse(categoriesStr);

} else {

validCategories = ['飲食', '交通', '日常', '娛樂', '服裝', '保險', '其他', '學習', '電信費', '旅遊', '醫療', '稅'];

props.setProperty('customCategories', JSON.stringify(validCategories));

} // 📝 【專屬說明書內容】

var instructionText = "🎉 歡迎使用專屬記帳管家!\n\n📝 【記帳指令】(加半形空白)\n👉 今天:飲食 150 麥當勞\n👉 昨天:昨天 交通 50\n👉 指定:3/12 服裝 500\n\n🔍 【歷史明細查詢】(含圖表與分類結算)\n👉 查本月:查詢 本月\n👉 查上月:查詢 上月\n👉 查本周:查詢 本周\n👉 查單日:查詢 3/12\n\n🛠️ 【修改與刪除】\n👉 修改上一筆:修改 飲食 100\n👉 刪除上一筆:刪除\n👉 刪錯反悔:復原\n\n🏷️ 【自訂分類】\n👉 加分類:新增分類 寵物\n👉 刪分類:刪除分類 稅\n👉 查分類:我的分類\n\n💰 【預算與報表】\n👉 設預算:設定預算 15000\n👉 查圖表:本月花費、今年花費";

if (event.type === 'follow') { replyMessage(replyToken, [instructionText]); return; }

if (event.type !== 'message' || event.message.type !== 'text') { return; }

var userMessage = event.message.text.trim();

var userMessageLower = userMessage.toLowerCase();

// 🌟【呼叫說明書】支援「說明」、「說明書」、「使用說明」

var instructionKeywords = ["說明", "說明書", "使用說明"];

if (instructionKeywords.indexOf(userMessage) !== -1) {

replyMessage(replyToken, [instructionText]);

return;

} // 🌟【查詢歷史明細 + 產出圖表】

if (userMessage.indexOf("查詢 ") === 0) {

var queryRange = userMessage.substring(3).trim();

// queryHistory 現在會回傳陣列 (包含明細文字、結算文字、圖片)

var queryMessages = queryHistory(sheet, queryRange);

replyMessage(replyToken, queryMessages);

return;

} // 🌟【查詢我的分類】

if (userMessage === "我的分類" || userMessage === "分類列表") {

replyMessage(replyToken, ["🏷️ 【目前的記帳分類】\n" + validCategories.join("、")]);

return;

} // 🌟【新增自訂分類】

if (userMessage.indexOf("新增分類 ") === 0) {

var newCat = userMessage.substring(5).trim();

if (newCat) {

if (validCategories.indexOf(newCat) === -1) {

validCategories.push(newCat);

props.setProperty('customCategories', JSON.stringify(validCategories));

replyMessage(replyToken, ["✅ 成功新增分類:「" + newCat + "」!\n\n目前所有分類有:\n" + validCategories.join("、")]);

} else {

replyMessage(replyToken, ["⚠️ 分類「" + newCat + "」已經存在囉!"]);

} } return;

} // 🌟【刪除自訂分類】

if (userMessage.indexOf("刪除分類 ") === 0) {

var delCat = userMessage.substring(5).trim();

var catIndex = validCategories.indexOf(delCat);

if (catIndex !== -1) {

validCategories.splice(catIndex, 1);

props.setProperty('customCategories', JSON.stringify(validCategories));

replyMessage(replyToken, ["🗑️ 成功刪除分類:「" + delCat + "」!\n\n目前所有分類有:\n" + validCategories.join("、")]);

} else {

replyMessage(replyToken, ["⚠️ 找不到分類「" + delCat + "」,請檢查名稱是否有錯字喔!"]);

} return;

} // 🌟【設定每個月預算上限】

if (userMessage.indexOf("設定預算 ") === 0) {

var budget = parseFloat(userMessage.substring(5).trim());

if (!isNaN(budget)) {

props.setProperty('monthlyBudget', budget.toString());

replyMessage(replyToken, ["🎯 已成功設定「每月預算上限」為:$" + budget]);

} else { replyMessage(replyToken, ["❌ 預算設定失敗。\n👉 範例:設定預算 15000"]); }

return;

} // 🌟【查詢報表指令】(仍保留供快速查詢)

var reportKeywords = ["this week", "this month", "last month", "this year", "last year", "total", "本周花費", "本月花費", "上月花費", "今年花費", "去年花費", "全部結餘"];

var keywordMap = { "本周花費": "this week", "本月花費": "this month", "上月花費": "last month", "今年花費": "this year", "去年花費": "last year", "全部結餘": "total" };

var targetReportType = keywordMap[userMessage] || userMessageLower;

if (reportKeywords.indexOf(targetReportType) !== -1) {

var reportMessages = generateReportAndChart(sheet, targetReportType);

replyMessage(replyToken, reportMessages); return;

} // 🌟【復原剛剛刪除的紀錄】

if (userMessage === "復原") {

var lastDeletedStr = props.getProperty('lastDeletedRow');

if (lastDeletedStr) {

var restoreData = JSON.parse(lastDeletedStr); sheet.appendRow(restoreData); props.deleteProperty('lastDeletedRow');

replyMessage(replyToken, ["♻️ 已經為您「復原」剛剛刪掉的紀錄:\n\n📅 日期:" + restoreData[0] + "\n🏷️ 分類:" + restoreData[1] + "\n💰 支出:$" + restoreData[2] + "\n🗒️ 說明:" + (restoreData[3] || "無")]);

} else { replyMessage(replyToken, ["⚠️ 目前沒有可以復原的紀錄喔!"]); }

return;

} // 🌟【刪除最後一筆紀錄】

if (userMessage === "刪除") {

var lastRow = sheet.getLastRow();

if (lastRow > 1) {

var deletedData = sheet.getRange(lastRow, 1, 1, 4).getDisplayValues()[0];

props.setProperty('lastDeletedRow', JSON.stringify(deletedData)); sheet.deleteRow(lastRow);

replyMessage(replyToken, ["🗑️ 已成功刪除上一筆紀錄:\n" + deletedData[0] + " | " + deletedData[1] + " | $" + deletedData[2] + " | " + (deletedData[3] || "無說明")]);

} else { replyMessage(replyToken, ["⚠️ 表單目前是空的喔!"]); }

return;

} // 🌟【判斷是否為「修改」模式】

var isModify = false; var parseText = userMessage;

if (userMessage.indexOf("修改 ") === 0) { isModify = true; parseText = userMessage.substring(3).trim(); }

// ==========================================

// 🔍 開始解析記帳邏輯

// ==========================================

var splitMsg = parseText.split(" ");

var errorMsgText = "❌ 格式錯誤!紀錄失敗。\n\n👉 範例:飲食 150 麥當勞\n\n可用的分類有:" + validCategories.join("、");

var copyText = isModify ? "修改 飲食 150 麥當勞" : "飲食 150 麥當勞";

var targetDate = new Date(); var category = ""; var price = 0; var note = "";

if (validCategories.indexOf(splitMsg[0]) !== -1) {

if (splitMsg.length >= 2 && !isNaN(splitMsg[1])) {

category = splitMsg[0]; price = parseFloat(splitMsg[1]); note = splitMsg.slice(2).join(" ");

} else { replyMessage(replyToken, [errorMsgText, copyText]); return; }

} else if (splitMsg.length >= 3 && validCategories.indexOf(splitMsg[1]) !== -1) {

if (!isNaN(splitMsg[2])) {

var dateInput = splitMsg[0]; category = splitMsg[1]; price = parseFloat(splitMsg[2]); note = splitMsg.slice(3).join(" ");

if (dateInput === "昨天") { targetDate.setDate(targetDate.getDate() - 1); }

else if (dateInput === "前天") { targetDate.setDate(targetDate.getDate() - 2); }

else if (dateInput.indexOf("/") !== -1) {

var parts = dateInput.split("/");

if (parts.length === 2) { targetDate.setMonth(parseInt(parts[0]) - 1); targetDate.setDate(parseInt(parts[1])); }

else if (parts.length === 3) { targetDate.setFullYear(parseInt(parts[0])); targetDate.setMonth(parseInt(parts[1]) - 1); targetDate.setDate(parseInt(parts[2])); }

} else if (dateInput.length === 4 && !isNaN(dateInput)) {

targetDate.setMonth(parseInt(dateInput.substring(0, 2)) - 1); targetDate.setDate(parseInt(dateInput.substring(2, 4)));

} else { replyMessage(replyToken, ["❌ 日期格式看不懂!請使用:「昨天」、「3/12」或「0312」", copyText]); return; }

} else { replyMessage(replyToken, [errorMsgText, copyText]); return; }

} else { replyMessage(replyToken, [errorMsgText, copyText]); return; }

var formattedDate = Utilities.formatDate(targetDate, "Asia/Taipei", "yyyy/MM/dd");

// 🌟【計算本月花費,判斷預算】

var currentBudgetStr = props.getProperty('monthlyBudget');

var budgetWarning = "";

if (currentBudgetStr) {

var currentBudget = parseFloat(currentBudgetStr);

var currentTotal = calculateMonthTotal(sheet, targetDate) + price;

if (currentTotal > currentBudget) { budgetWarning = "\n\n⚠️ 【預算超支警告】\n本月累計 $" + currentTotal + ",已超出上限 ($" + currentBudget + ") 啦!💸"; }

else if (currentTotal > currentBudget * 0.8) { budgetWarning = "\n\n🔔 【預算快見底囉】\n本月累計 $" + currentTotal + ",距離上限只剩 $" + (currentBudget - currentTotal) + "!"; }

else { budgetWarning = "\n\n💡 (本月花費:$" + currentTotal + " / 上限 $" + currentBudget + ")"; }

} // 🌟【寫入 Google 試算表】

if (isModify) {

var lastRow = sheet.getLastRow();

if (lastRow > 1) {

var oldData = sheet.getRange(lastRow, 1, 1, 4).getDisplayValues()[0];

sheet.getRange(lastRow, 1, 1, 4).setValues([[formattedDate, category, price, note]]);

var modifyReplyText = "✏️ 已成功「修改」上一筆紀錄!\n\n【原本】" + oldData[0] + " | " + oldData[1] + " | $" + oldData[2] + " | " + (oldData[3] || "無") + "\n\n【更新為】\n📅 日期:" + formattedDate + "\n🏷️ 分類:" + category + "\n💰 支出:$" + price + "\n🗒️ 說明:" + note + budgetWarning;

replyMessage(replyToken, [modifyReplyText]);

} else { replyMessage(replyToken, ["⚠️ 表單目前是空的,沒有紀錄可以修改喔!"]); }

} else {

sheet.appendRow([formattedDate, category, price, note]);

var replyText = "✅ 已記錄:\n📅 日期:" + formattedDate + "\n🏷️ 分類:" + category + "\n💰 支出:$" + price + "\n🗒️ 說明:" + note + budgetWarning;

replyMessage(replyToken, [replyText]);

}}// ==========================================

// 🛠️ 輔助函式區塊

// ==========================================

// 🌟 查詢歷史紀錄明細 (包含分類結算與圓餅圖,支援上限 200 筆)

function queryHistory(sheet, rangeStr) {

var now = new Date(); var y = now.getFullYear(); var m = now.getMonth(); var d = now.getDate();

var startDate, endDate;

if (rangeStr === "本月" || rangeStr === "this month") {

startDate = new Date(y, m, 1); endDate = new Date(y, m + 1, 0, 23, 59, 59);

} else if (rangeStr === "上月" || rangeStr === "上個月" || rangeStr === "last month") {

startDate = new Date(y, m - 1, 1); endDate = new Date(y, m, 0, 23, 59, 59);

} else if (rangeStr === "本周" || rangeStr === "本週" || rangeStr === "this week") {

var day = now.getDay() || 7;

startDate = new Date(y, m, d - day + 1); startDate.setHours(0, 0, 0, 0); endDate = new Date(y, m, d - day + 7, 23, 59, 59);

} else if (rangeStr === "今天") {

startDate = new Date(y, m, d, 0, 0, 0); endDate = new Date(y, m, d, 23, 59, 59);

} else if (rangeStr === "昨天") {

startDate = new Date(y, m, d - 1, 0, 0, 0); endDate = new Date(y, m, d - 1, 23, 59, 59);

} else {

var targetDate = new Date(); var isValidDate = false;

if (rangeStr.indexOf("/") !== -1) {

var parts = rangeStr.split("/");

if (parts.length === 2) { targetDate.setMonth(parseInt(parts[0]) - 1); targetDate.setDate(parseInt(parts[1])); isValidDate = true; }

else if (parts.length === 3) { targetDate.setFullYear(parseInt(parts[0])); targetDate.setMonth(parseInt(parts[1]) - 1); targetDate.setDate(parseInt(parts[2])); isValidDate = true; }

} else if (rangeStr.length === 4 && !isNaN(rangeStr)) {

targetDate.setMonth(parseInt(rangeStr.substring(0, 2)) - 1); targetDate.setDate(parseInt(rangeStr.substring(2, 4))); isValidDate = true;

} else if (rangeStr.length === 8 && !isNaN(rangeStr)) {

targetDate.setFullYear(parseInt(rangeStr.substring(0,4))); targetDate.setMonth(parseInt(rangeStr.substring(4, 6)) - 1); targetDate.setDate(parseInt(rangeStr.substring(6, 8))); isValidDate = true;

} if (isValidDate) {

startDate = new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate(), 0, 0, 0);

endDate = new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate(), 23, 59, 59);

} else {

return ["❌ 看不懂您要查詢的時間範圍。\n👉 範例:查詢 本月、查詢 昨天、查詢 3/12"];

} } var data = sheet.getDataRange().getDisplayValues();

var results = [];

var totalQueryAmount = 0;

var categorySums = {};

for (var i = 1; i < data.length; i++) {

var rowDateStr = data[i][0];

if (!rowDateStr) continue;

var rowDate = new Date(rowDateStr);

if (rowDate >= startDate && rowDate <= endDate) {

results.push(data[i]);

var price = parseFloat(data[i][2]);

var cat = data[i][1];

if (!isNaN(price)) {

totalQueryAmount += price; if (!categorySums[cat]) categorySums[cat] = 0;

categorySums[cat] += price; } } } if (results.length === 0) {

return ["📭 找不到這個時間範圍的任何紀錄喔!"];

} if (results.length > 200) {

return ["⚠️ 哎呀!這段期間的紀錄共有 " + results.length + " 筆,超過了我們設定的顯示上限 (200筆)。\n請縮小查詢範圍(例如改成查詢單日:查詢 3/12)!"];

} // 📝 準備第一則訊息:歷史明細

var replyText = "🔍 【歷史明細查詢】\n共 " + results.length + " 筆,總計 $" + totalQueryAmount + "\n----------------------\n";

for (var j = 0; j < results.length; j++) {

var dStr = results[j][0].substring(5);

var cStr = results[j][1];

var pStr = results[j][2];

var nStr = results[j][3] ? " (" + results[j][3] + ")" : "";

replyText += dStr + " [" + cStr + "] $" + pStr + nStr + "\n";

} // 📊 準備第二則訊息:分類結算

var sortedCats = Object.keys(categorySums).map(function(k) { return { name: k, amount: categorySums[k] }; });

sortedCats.sort(function(a, b) { return b.amount - a.amount; });

var categoryReportText = "📊 【期間分類結算】\n----------------------\n";

var chartLabels = []; var chartData = [];

for (var k = 0; k < sortedCats.length; k++) {

categoryReportText += (k + 1) + ". " + sortedCats[k].name + ":$" + sortedCats[k].amount + "\n";

chartLabels.push(sortedCats[k].name); chartData.push(sortedCats[k].amount);

} var messagesToReply = [replyText];

// 🖼️ 準備第三則訊息:圖表 (如果有花費的話)

if (sortedCats.length > 0) {

var chartConfig = { type: 'outlabeledPie', data: { labels: chartLabels, datasets: [{ data: chartData, backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40', '#E7E9ED', '#8AC926', '#1982C4', '#6A4C93'] }] }, options: { plugins: { legend: { display: false }, outlabels: { text: '%l: %v', color: 'white', stretch: 35, font: { resizable: true, minSize: 12, maxSize: 18 } } } } };

var chartUrl = "https://quickchart.io/chart?c=" + encodeURIComponent(JSON.stringify(chartConfig)) + "&w=600&h=400";

var chartMsg = { 'type': 'image', 'originalContentUrl': chartUrl, 'previewImageUrl': chartUrl };

messagesToReply.push(categoryReportText);

messagesToReply.push(chartMsg);

} return messagesToReply;

}function calculateMonthTotal(sheet, targetDate) {

var data = sheet.getDataRange().getValues();

var y = targetDate.getFullYear(); var m = targetDate.getMonth();

var startDate = new Date(y, m, 1); var endDate = new Date(y, m + 1, 0, 23, 59, 59);

var totalAmount = 0;

for (var i = 1; i < data.length; i++) {

var rowDateStr = data[i][0]; if (!rowDateStr) continue;

var rowDate = new Date(rowDateStr);

if (rowDate >= startDate && rowDate <= endDate) {

var price = parseFloat(data[i][2]);

if (!isNaN(price)) { totalAmount += price; }

} } return totalAmount;

}function generateReportAndChart(sheet, type) {

var data = sheet.getDataRange().getValues();

var now = new Date(); var y = now.getFullYear(); var m = now.getMonth(); var d = now.getDate();

var startDate, endDate;

if (type === "this month") { startDate = new Date(y, m, 1); endDate = new Date(y, m + 1, 0, 23, 59, 59); }

else if (type === "last month") { startDate = new Date(y, m - 1, 1); endDate = new Date(y, m, 0, 23, 59, 59); }

else if (type === "this week") { var day = now.getDay() || 7; startDate = new Date(y, m, d - day + 1); startDate.setHours(0, 0, 0, 0); endDate = new Date(y, m, d - day + 7, 23, 59, 59); }

else if (type === "this year") { startDate = new Date(y, 0, 1); endDate = new Date(y, 11, 31, 23, 59, 59); }

else if (type === "last year") { startDate = new Date(y - 1, 0, 1); endDate = new Date(y - 1, 11, 31, 23, 59, 59); }

else if (type === "total") { startDate = new Date(2000, 0, 1); endDate = new Date(2100, 11, 31, 23, 59, 59); }

var categorySums = {}; var totalAmount = 0;

for (var i = 1; i < data.length; i++) {

var rowDateStr = data[i][0]; if (!rowDateStr) continue;

var rowDate = new Date(rowDateStr);

if (rowDate >= startDate && rowDate <= endDate) {

var cat = data[i][1]; var price = parseFloat(data[i][2]);

if (!isNaN(price)) { if (!categorySums[cat]) categorySums[cat] = 0; categorySums[cat] += price; totalAmount += price; }

} } var sortedCats = Object.keys(categorySums).map(function(k) { return { name: k, amount: categorySums[k] }; });

sortedCats.sort(function(a, b) { return b.amount - a.amount; });

var titleMap = { "this week": "本周花費", "this month": "本月花費", "last month": "上月花費", "this year": "今年花費", "last year": "去年花費", "total": "全部結餘" };

var reportTitle = "📊 【" + titleMap[type] + "結算】\n";

var periodText = "";

if (type === "total") { periodText = "歷史所有紀錄\n"; }

else if (type === "this year" || type === "last year") { periodText = Utilities.formatDate(startDate, "Asia/Taipei", "yyyy/MM/dd") + " ~ " + Utilities.formatDate(endDate, "Asia/Taipei", "yyyy/MM/dd") + "\n"; }

else { periodText = Utilities.formatDate(startDate, "Asia/Taipei", "MM/dd") + " ~ " + Utilities.formatDate(endDate, "Asia/Taipei", "MM/dd") + "\n"; }

var reportText = reportTitle + "🗓️ 期間:" + periodText + "💰 總計:$" + totalAmount + "\n----------------------\n";

var chartLabels = []; var chartData = [];

if (sortedCats.length === 0) { reportText += "這段期間沒有任何紀錄喔!🎉"; return [reportText]; }

else {

for (var j = 0; j < sortedCats.length; j++) {

reportText += (j + 1) + ". " + sortedCats[j].name + ":$" + sortedCats[j].amount + "\n";

chartLabels.push(sortedCats[j].name); chartData.push(sortedCats[j].amount);

} } var chartConfig = { type: 'outlabeledPie', data: { labels: chartLabels, datasets: [{ data: chartData, backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40', '#E7E9ED', '#8AC926', '#1982C4', '#6A4C93'] }] }, options: { plugins: { legend: { display: false }, outlabels: { text: '%l: %v', color: 'white', stretch: 35, font: { resizable: true, minSize: 12, maxSize: 18 } } } } };

var chartUrl = "https://quickchart.io/chart?c=" + encodeURIComponent(JSON.stringify(chartConfig)) + "&w=600&h=400";

return [ reportText, { 'type': 'image', 'originalContentUrl': chartUrl, 'previewImageUrl': chartUrl } ];

}function replyMessage(replyToken, messageArray) {

var messages = [];

for (var i = 0; i < messageArray.length; i++) {

if (typeof messageArray[i] === 'string') { messages.push({'type': 'text', 'text': messageArray[i]}); }

else { messages.push(messageArray[i]); }

} var url = 'https://api.line.me/v2/bot/message/reply';

var options = { 'headers': { 'Content-Type': 'application/json; charset=UTF-8', 'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN }, 'method': 'post', 'payload': JSON.stringify({ 'replyToken': replyToken, 'messages': messages }) };

UrlFetchApp.fetch(url, options);

}function sendMonthlyPush() {

var props = PropertiesService.getScriptProperties();

var userId = props.getProperty('USER_ID');

if (!userId) return;

var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();

var budgetStr = props.getProperty('monthlyBudget') || "未設定 (請輸入「設定預算 10000」)";

var instructionMsg = "📅 又是新的一個月囉!\n\n【指令提醒】\n👉 記帳:飲食 150\n👉 補登:昨天 交通 50\n👉 查明細:查詢 本月\n👉 看報表:本月花費";

var reportMessages = generateReportAndChart(sheet, "last month");

var budgetMsg = "💰 【這個月預算上限】\n目前設定為:$" + budgetStr + "\n\n祝您這個月也能好好守住錢包!💪";

var messages = [{'type': 'text', 'text': instructionMsg}];

for (var i = 0; i < reportMessages.length; i++) {

if (typeof reportMessages[i] === 'string') { messages.push({'type': 'text', 'text': reportMessages[i]}); }

else { messages.push(reportMessages[i]); }

} messages.push({'type': 'text', 'text': budgetMsg});

var url = 'https://api.line.me/v2/bot/message/push';

var options = { 'headers': { 'Content-Type': 'application/json; charset=UTF-8', 'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN }, 'method': 'post', 'payload': JSON.stringify({ 'to': userId, 'messages': messages }) };

UrlFetchApp.fetch(url, options);
留言
avatar-img
Wei Li的沙龍
1會員
1內容數
你可能也想看
Thumbnail
本篇文章雖然是Line機器人操作教學,但如何申請機器人請自己去搜尋一下,創建也自己創一下,有很多篇手把手教學 目標想達到的效果 Line機器人收到圖片後將圖片上傳至imgur雲端空間,回傳使用者圖片url 閱讀前需具備知識 Line機器人創建設置 Nodejs與express基礎配置 @line/b
Thumbnail
本篇文章雖然是Line機器人操作教學,但如何申請機器人請自己去搜尋一下,創建也自己創一下,有很多篇手把手教學 目標想達到的效果 Line機器人收到圖片後將圖片上傳至imgur雲端空間,回傳使用者圖片url 閱讀前需具備知識 Line機器人創建設置 Nodejs與express基礎配置 @line/b
Thumbnail
SD-LINE-BOT 是一款基於 Flask 的 LINE 機器人,使用 Stable Diffusion 生成圖片。用戶可透過 LINE 傳送提示字詞,機器人將自動生成並傳送圖片。服務可能因伺服器維護或其他因素而中斷。
Thumbnail
SD-LINE-BOT 是一款基於 Flask 的 LINE 機器人,使用 Stable Diffusion 生成圖片。用戶可透過 LINE 傳送提示字詞,機器人將自動生成並傳送圖片。服務可能因伺服器維護或其他因素而中斷。
Thumbnail
若說易卜生的《玩偶之家》為 19 世紀的女性,開啟了一扇離家的窄門,那麼《海妲.蓋柏樂》展現的便是門後的窒息世界。本篇文章由劇場演員 Amily 執筆,同為熟稔文本的演員,亦是深刻體察制度縫隙的當代女性,此文所看見的不僅僅是崩壞前夕的最後發聲,更是女人被迫置於冷酷的制度之下,步步陷入無以言說的困境。
Thumbnail
若說易卜生的《玩偶之家》為 19 世紀的女性,開啟了一扇離家的窄門,那麼《海妲.蓋柏樂》展現的便是門後的窒息世界。本篇文章由劇場演員 Amily 執筆,同為熟稔文本的演員,亦是深刻體察制度縫隙的當代女性,此文所看見的不僅僅是崩壞前夕的最後發聲,更是女人被迫置於冷酷的制度之下,步步陷入無以言說的困境。
Thumbnail
我的方格子寫得很隨興,一下寫設計工具文、一下寫日常體驗,而更隨便的日記類型文字,則是都放在舊部落格裡,持續寫文字讓我最開心的,就是有機會收到各式各樣的創作者來信,和我分享他們的設計與開發項目。
Thumbnail
我的方格子寫得很隨興,一下寫設計工具文、一下寫日常體驗,而更隨便的日記類型文字,則是都放在舊部落格裡,持續寫文字讓我最開心的,就是有機會收到各式各樣的創作者來信,和我分享他們的設計與開發項目。
Thumbnail
第一篇程式教學 製作LINE官方隨機回覆系統
Thumbnail
第一篇程式教學 製作LINE官方隨機回覆系統
Thumbnail
本文深度解析賽勒布倫尼科夫的舞臺作品《傳奇:帕拉贊諾夫的十段殘篇》,如何以十段殘篇,結合帕拉贊諾夫的電影美學、象徵意象與當代政治流亡抗爭,探討藝術在儀式消失的現代社會如何承接意義,並展現不羈的自由靈魂。
Thumbnail
本文深度解析賽勒布倫尼科夫的舞臺作品《傳奇:帕拉贊諾夫的十段殘篇》,如何以十段殘篇,結合帕拉贊諾夫的電影美學、象徵意象與當代政治流亡抗爭,探討藝術在儀式消失的現代社會如何承接意義,並展現不羈的自由靈魂。
Thumbnail
全新版本的《三便士歌劇》如何不落入「復刻經典」的巢臼,反而利用華麗的秀場視覺,引導觀眾在晚期資本主義的消費愉悅之中,而能驚覺「批判」本身亦可能被收編——而當絞繩升起,這場關於如何生存的黑色遊戲,又將帶領新時代的我們走向何種後現代的自我解構?
Thumbnail
全新版本的《三便士歌劇》如何不落入「復刻經典」的巢臼,反而利用華麗的秀場視覺,引導觀眾在晚期資本主義的消費愉悅之中,而能驚覺「批判」本身亦可能被收編——而當絞繩升起,這場關於如何生存的黑色遊戲,又將帶領新時代的我們走向何種後現代的自我解構?
Thumbnail
朋友說:「小精靈很適合當牌卡耶!」,我把這一年多畫的小精靈,變成牌卡了!!大家想到就能用line抽牌!
Thumbnail
朋友說:「小精靈很適合當牌卡耶!」,我把這一年多畫的小精靈,變成牌卡了!!大家想到就能用line抽牌!
Thumbnail
本文介紹如何使用 n8n 串接 LINE 與 LLM,打造具備記憶與思考能力的智慧聊天機器人。從 MQTT 觸發、JavaScript 預處理、LLM 回覆到 LINE 回傳,完整展示低程式化自動化流程,讓 LINE Bot 也能擁有 AI 大腦。
Thumbnail
本文介紹如何使用 n8n 串接 LINE 與 LLM,打造具備記憶與思考能力的智慧聊天機器人。從 MQTT 觸發、JavaScript 預處理、LLM 回覆到 LINE 回傳,完整展示低程式化自動化流程,讓 LINE Bot 也能擁有 AI 大腦。
Thumbnail
「記帳雞」(Checkchick)是LINE OA上有一款小有名氣的聊天機器人,只要加入好友即可開始簡易記帳,但除了功能豐富外,記帳機更是有溫度的聊天好朋友!
Thumbnail
「記帳雞」(Checkchick)是LINE OA上有一款小有名氣的聊天機器人,只要加入好友即可開始簡易記帳,但除了功能豐富外,記帳機更是有溫度的聊天好朋友!
Thumbnail
你還在用長輩圖或網路抓的聖誕卡嗎?今年來點不一樣的!本文將教你如何使用 LINE 平台上的 Moonshot 機器人,只需簡單指令,就能呼叫多種 AI 模型為你繪製獨一無二的聖誕賀卡,重點是:完全免費!
Thumbnail
你還在用長輩圖或網路抓的聖誕卡嗎?今年來點不一樣的!本文將教你如何使用 LINE 平台上的 Moonshot 機器人,只需簡單指令,就能呼叫多種 AI 模型為你繪製獨一無二的聖誕賀卡,重點是:完全免費!
Thumbnail
長期以來,西方美學以《維特魯威人》式的幾何比例定義「完美身體」,這種視覺標準無形中成為殖民擴張與種族分類的暴力工具。本文透過分析奈及利亞編舞家庫德斯.奧尼奎庫的舞作《轉轉生》,探討當代非洲舞蹈如何跳脫「標本式」的文化觀看。
Thumbnail
長期以來,西方美學以《維特魯威人》式的幾何比例定義「完美身體」,這種視覺標準無形中成為殖民擴張與種族分類的暴力工具。本文透過分析奈及利亞編舞家庫德斯.奧尼奎庫的舞作《轉轉生》,探討當代非洲舞蹈如何跳脫「標本式」的文化觀看。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News