Skip to content

AI 協作:讓 AI 建 store

重新認識 Vue.js: 008 天絕對看不完的 Vue.js 3.x 指南

可選的 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.jssrc/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() 讀取 notificationsunreadCountaddNotificationmarkAsReadmarkAllAsRead 則直接解構。

修正版完整範例:通知中心 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 放進更完整的應用程式流程,處理新的畫面和資料互動情境。