iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0
Modern Web

微光採集瓶:用 Vue 3 打造每日美好記錄網頁系列 第 28 篇

【Day 28】月底清空機制:封存回憶與光珠歸零的流程設計

  • 分享至 

  • xImage
  •  

昨天(Day 27),我們完成了主畫面的減法設計,移除測試用的月度色卡按鈕,並重新整理頂部導覽與採集版面,讓使用者能更專注於每日的微光採集。

不過,當一個月份的光珠統計完成、月度色卡也封存至時光相簿後,瓶中的光珠該如何處理?我這才發現,自己還沒有設計好月度總結後的資料處理流程。當一個月份的回憶已經封存完成,瓶子裡的光珠該如何處理,才能讓採集流程持續循環?

如果直接清空光珠,系統原本的自動補漏機制可能會在重新載入時,將已經清除的日期重新補上白色光珠。此外,清空資料後,當天的採集狀態也可能受到影響。因此,今天的重點不只是刪除光珠,還要讓月卡封存、資料保存、自動補漏與每日採集限制能夠互相配合。


一、設計思路

在原本的設計中,月度色卡會根據當月光珠統計出採集總數、心情顏色分布與主要顏色,再保存至時光相簿。不過,先前的月卡與瓶中光珠之間仍有資料上的關聯,如果封存後直接清空光珠,就必須確保月卡已經獨立保存,不會因為原始資料被移除而失去內容。

因此,我將月底封存流程整理成三個重點:

  1. 建立獨立月卡: 將當月統計結果整理成獨立資料,保存至時光相簿。
  2. 清除當月光珠: 完成封存後,只移除屬於該月份的光珠,下個月資料重新紀錄。
  3. 記錄封存日期: 設定封存日期界線,避免自動補漏重新建立已封存日期的光珠,並維持每日只能手動採集一次的限制。

這樣一來,時光相簿負責保存過去的回憶,而瓶子則可以在每個月份結束後重新開始累積新的微光。

二、實作調整:月底封存與光珠清空

1. 建立獨立的月卡資料

首先,我在 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)
)

透過先建立獨立月卡、再保存至相簿的方式,即使之後清空瓶中的光珠,月卡仍能保留自己的統計資料,不必依賴原本的光珠陣列來顯示。

2. 計算封存月份的最後一天

接下來,我需要記錄這次封存的月份究竟到哪一天為止。

這裡不能直接使用 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 日。

3. 清空當月光珠,保留其他資料

完成月卡保存與封存日期更新後,才進行光珠清空:

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)
)

這樣可以讓程式碼中的封存步驟更加清楚:月卡先保存,封存邊界接著更新,最後清除當月光珠並同步保存結果。

4. 修改自動補漏機制,避免舊資料重新出現

完成清空後,另一個需要處理的問題是 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)
}

只有日期沒有落在封存邊界內,而且原本也沒有光珠時,系統才會自動建立新的白色光珠。

透過起點限制與迴圈內的再次檢查,能降低已封存日期被重新補回的可能性。

5. 維持每日只能採集一次的限制

最後,我也處理了清空光珠後的每日採集狀態。

原本的每日採集限制可以透過瓶中是否存在今天的手動光珠來判斷。但月底封存會將當月光珠清空,如果只依賴瓶中的資料判斷,今天的手動光珠就會消失,系統可能因此誤以為使用者還沒有採集。

所以,我在 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 上開啟,並實際檢查手機上的操作體驗與版面呈現,為專案的開發收尾做準備。


上一篇
【Day 27】減法設計:移除冗餘入口與優化採集版面
下一篇
【Day 29】用 ngrok 進行實機測試:連線除錯與手機版面微調
系列文
微光採集瓶:用 Vue 3 打造每日美好記錄網頁 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言