昨天(Day 27),我們完成了主畫面的減法設計,移除測試用的月度色卡按鈕,並重新整理頂部導覽與採集版面,讓使用者能更專注於每日的微光採集。
不過,當一個月份的光珠統計完成、月度色卡也封存至時光相簿後,瓶中的光珠該如何處理?我這才發現,自己還沒有設計好月度總結後的資料處理流程。當一個月份的回憶已經封存完成,瓶子裡的光珠該如何處理,才能讓採集流程持續循環?
如果直接清空光珠,系統原本的自動補漏機制可能會在重新載入時,將已經清除的日期重新補上白色光珠。此外,清空資料後,當天的採集狀態也可能受到影響。因此,今天的重點不只是刪除光珠,還要讓月卡封存、資料保存、自動補漏與每日採集限制能夠互相配合。
在原本的設計中,月度色卡會根據當月光珠統計出採集總數、心情顏色分布與主要顏色,再保存至時光相簿。不過,先前的月卡與瓶中光珠之間仍有資料上的關聯,如果封存後直接清空光珠,就必須確保月卡已經獨立保存,不會因為原始資料被移除而失去內容。
因此,我將月底封存流程整理成三個重點:
這樣一來,時光相簿負責保存過去的回憶,而瓶子則可以在每個月份結束後重新開始累積新的微光。
首先,我在 sealMonthlyCard() 中建立 cardSnapshot,將當月的統計結果整理成獨立的物件:
const cardSnapshot = {
month: monthTarget,
totalGems: targetMonthGems.value.length,
stats: colorStats.value.map(item => ({ ...item })),
dominantColor: colorStats.value[0]?.color || '#ffffff',
sealedAt: new Date().toISOString()
}
這份資料包含月份、光珠總數、顏色統計、主要顏色與封存時間。其中,map(item => ({ ...item })) 會為每一筆顏色統計建立新的物件,避免月卡直接沿用原本的統計物件。
接著,我使用 findIndex() 檢查時光相簿中是否已經存在該月份的月卡。如果已經存在,就更新原本的資料;如果不存在,才新增一張月卡:
const existingIndex = archivedCards.value.findIndex(
card => card.month === monthTarget
)
if (existingIndex !== -1) {
archivedCards.value[existingIndex] = cardSnapshot
} else {
archivedCards.value.unshift(cardSnapshot)
}
archivedCards.value.sort((a, b) => {
return b.month.localeCompare(a.month)
})
這樣可以避免同一個月份重複產生多張月卡,也能讓相簿依月份由新到舊排列。
完成後,我再將相簿資料寫入 LocalStorage:
localStorage.setItem(
'store_archived_cards',
JSON.stringify(archivedCards.value)
)
透過先建立獨立月卡、再保存至相簿的方式,即使之後清空瓶中的光珠,月卡仍能保留自己的統計資料,不必依賴原本的光珠陣列來顯示。
接下來,我需要記錄這次封存的月份究竟到哪一天為止。
這裡不能直接使用 todayStr,因為使用者除了在月底當天封存,也可能在次月 1 號回顧上個月的色卡。例如,11 月 1 日封存 10 月份時,真正需要記錄的封存日期是 10 月 31 日,而不是 11 月 1 日。
因此,我另外建立 getMonthEndDate(),根據指定月份計算該月最後一天:
function getMonthEndDate(monthStr) {
const [year, month] = monthStr.split('-').map(Number)
return formatDate(new Date(year, month, 0))
}
這裡利用 JavaScript 的日期計算方式,取得指定月份的最後一天,再透過 formatDate() 統一轉換成 YYYY-MM-DD 格式。
接著,在封存流程中更新 lastArchivedDate:
const monthEndDate = getMonthEndDate(monthTarget)
if (
!lastArchivedDate.value ||
monthEndDate > lastArchivedDate.value
) {
lastArchivedDate.value = monthEndDate
localStorage.setItem(
'store_last_archived_date',
lastArchivedDate.value
)
}
這段程式碼不只記錄封存日期,也加入了防止日期倒退的判斷。只有在尚未有封存日期,或這次封存日期晚於原本紀錄時,才會更新邊界。
例如,當 10 月份已經封存至 10 月 31 日,之後即使重新開啟 9 月份的月卡,也不會讓封存日期倒退回 9 月 30 日。
完成月卡保存與封存日期更新後,才進行光珠清空:
gems.value = gems.value.filter(g => {
return !g.date || !g.date.startsWith(monthTarget)
})
這裡使用 filter() 篩選出不屬於目標月份的資料。
判斷條件 !g.date || !g.date.startsWith(monthTarget) 代表:如果光珠沒有日期,就保留;如果光珠的日期不屬於這次封存的月份,也保留。只有日期屬於目標月份的光珠才會被移除。
這樣可以避免清空某個月份時,連其他月份的資料也一起刪除。
雖然 gems 已經設定深度監聽,可以在資料變動時自動保存至 LocalStorage,但我仍在封存流程中明確寫入一次:
localStorage.setItem(
'store_gems',
JSON.stringify(gems.value)
)
這樣可以讓程式碼中的封存步驟更加清楚:月卡先保存,封存邊界接著更新,最後清除當月光珠並同步保存結果。
完成清空後,另一個需要處理的問題是 checkMissedDays()。
這個函式會檢查近期缺漏的日期,並自動建立白色光珠。如果沒有額外限制,已經清空的日期可能在重新載入頁面時再次被補上,讓月底清空失去意義。
因此,我將 lastArchivedDate 納入自動補漏的起點計算:
const boundaryDate = lastArchivedDate.value
? parseLocalDate(lastArchivedDate.value)
: new Date(0)
let loopDate = new Date(
Math.max(
firstGemDate.getTime(),
sevenDaysAgo.getTime(),
boundaryDate.getTime()
)
)
// 從起點的隔天開始檢查
loopDate = new Date(
loopDate.getTime() + 24 * 60 * 60 * 1000
)
這裡使用 Math.max() 比較三個日期,讓補漏起點不會早於最早一顆光珠的日期、最近七天的範圍,以及最後一次封存日期。
此外,在補漏迴圈中,我也再次檢查日期是否落在封存邊界內:
const isBeforeOrOnArchivedBoundary =
lastArchivedDate.value &&
checkDateStr <= lastArchivedDate.value
if (
!isBeforeOrOnArchivedBoundary &&
!existingDates.has(checkDateStr)
) {
const newGem = createHappyGem(
checkDateStr,
loopDate.getTime()
)
gems.value.push(newGem)
existingDates.add(checkDateStr)
}
只有日期沒有落在封存邊界內,而且原本也沒有光珠時,系統才會自動建立新的白色光珠。
透過起點限制與迴圈內的再次檢查,能降低已封存日期被重新補回的可能性。
最後,我也處理了清空光珠後的每日採集狀態。
原本的每日採集限制可以透過瓶中是否存在今天的手動光珠來判斷。但月底封存會將當月光珠清空,如果只依賴瓶中的資料判斷,今天的手動光珠就會消失,系統可能因此誤以為使用者還沒有採集。
所以,我在 hasCollectedToday 中增加了封存日期判斷:
const hasCollectedToday = computed(() => {
const hasManualGem = gems.value.some(
g => g.date === todayStr && !g.isAutoGenerated
)
const archivedToday = lastArchivedDate.value === todayStr
return hasManualGem || archivedToday
})
這段程式碼會同時檢查兩件事:瓶中是否存在今天的手動光珠,以及今天是否剛好就是最後封存日期。
只要其中一項成立,就會將今天視為已完成採集。如此一來,即使月底封存後瓶子已經清空,也不會因為原始光珠消失而讓當天再次採集。
為了確認月底封存與光珠清空機制能正常運作,我先在 VS Code 中將 const todayStr = getLogicalDate() 暫時改成 const todayStr = '2026-10-31',模擬目前日期為 10 月 31 日,讓系統進入月底情境。
接著,我在瀏覽器開發人員工具的 Console 中建立測試資料,模擬當月不同顏色的光珠,並包含自動補漏產生的白色光珠。最後,將測試資料寫入 store_gems,並移除原本的 store_last_archived_date,避免先前的封存紀錄影響測試。
透過這些準備,我可以在月底情境下觀察月度色卡是否能正確產生、封存後是否會清除當月光珠,以及重新載入頁面後,自動補漏機制是否會重新建立已封存的光珠。
點擊影片:測試月底清空和月卡
今天完成的重點,是讓月底封存從單純保存月卡,進一步整合成完整的資料處理流程。我先建立獨立的月卡快照,再將封存月份的最後一天記錄為日期界線,接著清除該月份的光珠,並同步調整自動補漏與每日採集限制。
這次實作讓我更清楚地理解到,前端功能之間往往存在連動關係。清空陣列看似只是一個簡單的操作,卻可能影響資料保存、日期判斷與後續功能。因此,在設計新功能時,除了考慮主要流程,也需要思考資料被新增、修改或移除之後,其他功能是否仍能正常運作。
明天(Day 29)將開始進行《微光採集瓶》的手機版測試,嘗試讓目前在電腦上執行的 Vue 網頁透過區域網路在 iPhone 上開啟,並實際檢查手機上的操作體驗與版面呈現,為專案的開發收尾做準備。