Appearance
AI 協作:讓 AI 建 store
可選的 AI 協作
這一節是可選的延伸,不影響前面 Pinia、router 和持久化主線。重點不是讓 AI 代替設計 store,而是把需求、專案規則和驗證條件交代清楚,再把 AI 的產出當成需要人工審查的初稿。
本節採用通知中心,不重複書店 canonical 的購物車,也不重複第一章的購物車情境。它會有未讀數量、通知列表和標記已讀行為,正好能檢查 store 的 state、getter、action 與元件取用方式。
通知中心 store
假設應用程式右上角有一個通知鈴鐺。多個頁面都可能新增通知,導覽列顯示未讀數量,通知中心則可以逐筆標記已讀。這是跨元件共享的狀態,但它和本章書店範例是另一個獨立情境。
AI 協作:用 vue skill 加速
情境:應用程式右上角有通知鈴鐺。state 是通知陣列,getter 是未讀數量,action 是新增通知、標記單筆已讀和全部標記已讀;通知中心元件要顯示列表與未讀徽章。
做法:在安裝好 vue-skills 的 AI 工具中(安裝與觸發方式見第一章),下指令 use vue-pinia-best-practices,再給明確需求:
請為 Vue 3 應用建立通知中心。使用 Pinia Setup Store 與 Composition API:state 是通知陣列,getter 是未讀數量,action 有新增通知、標記單筆已讀、全部標記已讀;另外產生顯示列表與未讀徽章的通知中心元件,元件以
storeToRefs()取用 state 與 getter。提供src/stores/notification.js與src/components/NotificationCenter.vue。主線使用<script setup>,產出 JavaScript,不使用 TypeScript,不引入其他狀態管理套件。
AI 產出(節錄):store 骨架大致正確,元件卻留下本章的招牌問題;另一次產出甚至把 Option Store 的 state() 物件寫法混進 Setup Store 的檔案裡:
vue
<script setup>
import { useNotificationStore } from '../stores/notification.js'
const notificationStore = useNotificationStore()
const { unreadCount, notifications } = notificationStore // ❌ 直接解構,徽章不再更新
</script>工具驗證:這類問題建置工具幾乎抓不到,pnpm build 會照常通過,直接解構是完全合法的程式碼。所以要實際操作:按「產生通知」新增一筆,若未讀徽章不動,就是解構斷線;再開 Vue DevTools 的 Pinia 面板,確認 store 只有一份、每次 action 都留下紀錄。
人工驗證重點:
- state 與 getter 是否透過
storeToRefs()取得;action 直接解構即可。 - 檔案與命名是否符合慣例:
stores/notification.js對應useNotificationStore。 - 這個狀態該不該全域:通知跨頁面共享才成立,單一元件的暫態 UI 狀態不要塞進 store。
- getter 是否純推導、沒有副作用。
- 新增、標記已讀、全部已讀的行為在畫面與 DevTools 上一致。
可用的 skill 清單會更新;引用任何 skill 前,請以 vuejs-ai/skills 的 Available Skills 現行清單為準。
人工驗證
把提示詞拆成「要做什麼、不能做什麼、完成後怎麼驗」通常比一句「幫我做通知」可靠。特別是本書主線使用 JavaScript,提示詞必須明寫 JS-only,否則 AI 可能依照 Pinia 社群常見範例輸出 TypeScript。人工審查時也要把響應性、是否該全域、getter 是否純函式和命名一致性列成固定清單。
通知中心的修正版如下。它只保存通知本身的狀態,沒有把 toast 動畫、API 載入或元件 DOM 放進 store;元件使用 storeToRefs() 讀取 notifications 和 unreadCount,addNotification、markAsRead 和 markAllAsRead 則直接解構。
修正版完整範例:通知中心 store
src/stores/notification.js:
js
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
export const useNotificationStore = defineStore('notification', () => {
const notifications = ref([])
const unreadCount = computed(() => {
return notifications.value.filter((notification) => !notification.read).length
})
function addNotification(message, level = 'info') {
notifications.value.unshift({
id: crypto.randomUUID(),
message,
level,
read: false,
})
}
function markAsRead(id) {
const notification = notifications.value.find((item) => item.id === id)
if (notification) notification.read = true
}
function markAllAsRead() {
notifications.value.forEach((notification) => {
notification.read = true
})
}
return {
notifications,
unreadCount,
addNotification,
markAsRead,
markAllAsRead,
}
})src/components/NotificationCenter.vue:
vue
<script setup>
import { storeToRefs } from 'pinia'
import { useNotificationStore } from '../stores/notification.js'
const notificationStore = useNotificationStore()
const { notifications, unreadCount } = storeToRefs(notificationStore)
const { markAsRead, markAllAsRead } = notificationStore
</script>
<template>
<aside aria-label="通知中心">
<h2>通知({{ unreadCount }})</h2>
<button type="button" @click="markAllAsRead">
全部標記已讀
</button>
<ul>
<li v-for="notification in notifications" :key="notification.id">
<span :data-level="notification.level">
{{ notification.message }}
</span>
<button
v-if="!notification.read"
type="button"
@click="markAsRead(notification.id)"
>
標記已讀
</button>
</li>
</ul>
</aside>
</template>src/App.vue:
vue
<script setup>
import NotificationCenter from './components/NotificationCenter.vue'
import { useNotificationStore } from './stores/notification.js'
const notificationStore = useNotificationStore()
function receiveDemoNotification() {
notificationStore.addNotification('有一則新的讀書提醒。', 'info')
}
</script>
<template>
<main>
<button type="button" @click="receiveDemoNotification">
產生通知
</button>
<NotificationCenter />
</main>
</template>AI 可以協助產生 store 的初稿,但 state 範圍、命名、響應性和驗證責任仍然在開發者手上。看到直接解構、混用 store 風格或帶副作用的 getter 時,應該依照本章規則退回修改,而不是把能建置誤當成正確。
到這裡,Pinia 的定義、取用、router 整合、持久化和可選的 AI 協作都已串起來。第八章將再把這些 store 放進更完整的應用程式流程,處理新的畫面和資料互動情境。
