iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0
Modern Web

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

【Day 15】時間的設置:當日編輯撤回與歷史過期鎖定

  • 分享至 

  • xImage
  •  

在昨天的實作中,我們透過 Vue 3 的 watch 深度監聽與 localStorage,成功讓掉進玻璃瓶裡的微光可以在網頁重新整理後依然保存下來。

但隨著系統功能越做越多,新的使用情境問題浮現了出來:

  1. 輸入手滑怎麼辦? 如果今天採集時不小心打錯字,或是臨時想更換更貼切的詞彙,應該允許修改或撤回。
  2. 不可逆的回憶:如果連上週、好幾天前的心情都可以隨意塗改,日記的真實感與儀式感就會大打折扣。應該要限制只有當天可以修改採集的文字內容。

所以今天的目標就是:替每顆微光打上「日期印記」,並實作「當日可修訂/撤回,過期後不可修改」的規則!

一、資料結構升級:給光珠一個「生日」

在以前的實作中,每顆光珠的物件結構只有 id、color、note 以及隨機偏移座標:

// 過去的光珠資料結構
{
  id: Date.now(),
  color: '#ffd166',
  note: '今天買到了好吃的蛋糕',
  offsetX: 2,
  offsetY: -1
}

為了追蹤這顆光珠到底屬於哪一天,我們需要為它新增一個 date 欄位(格式為標準的 YYYY-MM-DD)。

在 <script setup> 中,我們先定義今天的日期字串:

// 取得當前日期的標準字串 (例如 "2026-09-29")
const todayStr = new Date().toISOString().split('T')[0]

💡為什麼是用 'T' 來做分隔?

初次看到 new Date().toISOString().split('T')[0] 時,可能會好奇中間為何憑空出現英文字母 T。

這是源自於國際標準組織制定的時間格式 ISO 8601。當我們執行 toISOString() 時,會得到一段標準字串,例如:

"2026-09-29T14:15:30.123Z"

在這串標準格式中,字母 T 代表「Time」,專門用來作為「前半段日期(Date)」與「後半段時間(Time)」的明確分界線。

因此,使用 .split('T') 就像拿剪刀從 T 的位置剪開,將字串切成 ["2026-09-29", "14:15:30.123Z"] 兩個元素的陣列;最後加上 [0] 取第 1 個元素,便能以最簡潔的一行語法取得標準的 YYYY-MM-DD 日期格式。

註:toISOString() 預設採用 UTC 世界協調時間,若在接近午夜或清晨(特別是台灣時區 UTC+8)執行,可能會因時區換算而產生日期位移。若是對時區要求極為嚴格的情境,使用本機的 getFullYear()、getMonth()、getDate() 自行組裝會更穩定!

接著,修改「放入微光」的 addGem() 函式,在建立新光珠時把 date: todayStr 寫入:

function addGem() {
  if (todayNote.value.trim() === '') {
    alert('請先寫下一件今天的美好小事喔!')
    return
  }

  const randomOffsetX = Math.floor(Math.random() * 9) - 4
  const randomOffsetY = Math.floor(Math.random() * 7) - 3

  const newGem = {
    id: Date.now(),
    date: todayStr, // 新增日期標籤
    color: selectedColor.value,
    note: todayNote.value,
    offsetX: randomOffsetX,
    offsetY: randomOffsetY
  }

二、修改與撤回

當使用者點開瓶身中的某顆光珠時,會彈出卡片顯示內容。
我們在卡片中提供兩個限當日可執行的操作:

  1. 修改內容:使用原生 prompt 彈窗直接編輯文字。
  2. 撤回微光:透過陣列的 filter 移除該光珠。

在 <script setup> 中補上這兩個函式:

// 修改當日文字
function editGem() {
  const newNote = prompt('修改你的美好小事:', activeGem.value.note)
  if (newNote && newNote.trim() !== '') {
    activeGem.value.note = newNote.trim()
  }
}

// 撤回當日微光
function deleteGem() {
  if (confirm('確定要撤回這道微光嗎?')) {
    // 透過 filter 排除當前選中的光珠 ID
    gems.value = gems.value.filter(g => g.id !== activeGem.value.id)
    closeGem() // 關閉卡片
  }
}

💡這裡有個 Vue 3 的隱藏亮點:

注意看!我們在 editGem() 與 deleteGem() 中,完全沒有手動寫 localStorage.setItem!

因為昨天我們已經建立了深度監聽:

watch(gems, (newVal) => {
  localStorage.setItem('store_gems', JSON.stringify(newVal))
}, { deep: true })

只要我們修改了物件裡面的屬性(activeGem.value.note = ...)或是改變了陣列內容(gems.value = ...),Vue 的響應式系統就會自動捕捉變化,並在背景自動完成同步!

三、樣板改造:時間的界線(過期鎖定)

打開原本的彈出預覽卡片,我們利用 v-if 與 v-else 來做狀態劃分:

  • 如果選中的光珠 activeGem.date === todayStr,顯示「編輯」與「撤回」按鈕。
  • 如果是過期的歷史光珠,隱藏操作按鈕,改為顯示鎖定提示。
<!-- 彈出預覽卡片 -->
<div v-if="activeGem" class="modal-backdrop" @click="closeGem">
  <div class="gem-card" :style="{ borderColor: activeGem.color }" @click.stop>
    <!-- 光點顏色標籤 -->
    <div class="gem-badge" :style="{ backgroundColor: activeGem.color }"></div>

    <!-- 顯示採集日期 -->
    <div class="gem-date">{{ activeGem.date || '未知時間' }}</div>

    <!-- 記錄的內文 -->
    <p class="gem-text">{{ activeGem.note }}</p>

    <!-- 情況 A:如果是今天採集的光珠,允許編輯與撤回 -->
    <div v-if="activeGem.date === todayStr" class="gem-actions">
      <button class="btn-sub edit" @click="editGem">編輯</button>
      <button class="btn-sub delete" @click="deleteGem">撤回</button>
    </div>

    <!-- 情況 B:過期光珠,無法撤回 -->
    <div v-else class="locked-hint">
      昔日回憶已無法撤回
    </div>

    <button class="btn-close" @click="closeGem">收起微光</button>
  </div>
</div>

四、為時間印記加上樣式

在 <style scoped> 尾部補上幾行CSS:

/* 日期標籤文字 */
.gem-date {
  font-size: 0.75rem;
  color: #8b949e;
  margin-bottom: 8px;
  letter-spacing: 1px;
}

/* 鎖定提示文字 */
.locked-hint {
  font-size: 0.75rem;
  color: #6e7681;
  margin-bottom: 16px;
  font-style: italic;
}

/* 操作按鈕群組 */
.gem-actions {
  display: flex;
  justify-content: center;
  gap: 10px;
  margin-bottom: 16px;
}

.btn-sub {
  border: none;
  border-radius: 12px;
  padding: 4px 12px;
  font-size: 0.75rem;
  cursor: pointer;
  transition: opacity 0.2s;
}

.btn-sub.edit {
  background: rgba(255, 255, 255, 0.15);
  color: #c9d1d9;
}

.btn-sub.delete {
  background: rgba(248, 81, 73, 0.2);
  color: #f85149;
}

.btn-sub:hover {
  opacity: 0.8;
}

💡開發者測試技巧:如何重置或偽造「歷史光珠」?

在導入了日期過期鎖定後,我們可能會遇到兩個測試上的需求:

  1. 想清空前幾天留下來的舊測試資料。
  2. 想驗證「過期鎖定」到底有沒有成功生效(總不能真的等到明天才測吧!)。

這時瀏覽器的開發人員工具就是最好的工具:

  • 清空所有光珠:

    按下 F12 打開開發者工具,切換至 Application 分頁 -> 左側展開 Local Storage -> 點擊本機網址。找到 store_gems 項目,按右鍵點擊 Delete,再重新整理網頁,就能得到一個空瓶子。

  • 偽造一顆「昨天」的微光來測試鎖定:

    直接在 Local Storage 的 store_gems 內容點兩下進行編輯,找到其中一顆光珠,手動將其 "date" 改成昨天的日期(例如 "2026-09-28"),按 Enter 儲存並重新整理網頁。

    此時回到畫面點開該顆光珠,你就會看見編輯與撤回按鈕消失,替換成 昔日回憶已無法撤回,用來驗證時光鎖定邏輯!

五、成果展示

  1. 驗證「過期鎖定」:展示點開手動改成昨天的光珠,確認沒有編輯與撤回按鈕,只有唯讀提示文字。
    影片
  2. 驗證「編輯內容和撤回光珠」:展示點擊今日光珠,修改文字與使用撤回按鈕讓光珠從瓶中消失。
    影片

成果回顧

今天我們完成了採集瓶從純粹的圖形動態,邁向具備真實時間序列的日記應用:

  1. 日期維度:每顆光珠都擁有了專屬的誕生日期。
  2. 編輯修改:當天手滑或打錯字,能隨時打開彈窗進行編輯與撤銷。
  3. 時光鎖定:過期的微光將永遠鎖在瓶子裡,化作唯讀的珍貴回憶。

明日預告

以現實生活來說,現代人的作息真的會剛好在半夜 12:00(00:00)準時結束一天嗎?

明天(Day 16),我們將更改系統的 0 點分界,打造更符合現代人作息的時間判定。


上一篇
【Day 14】別讓記憶消失:用 LocalStorage 實現資料持久化儲存
下一篇
【Day 16】重新定義「今天」:睡前才算一天結束的時間邏輯
系列文
微光採集瓶:用 Vue 3 打造每日美好記錄網頁 共 19 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言