☰
Fantastic-admin 框架 Pinia Store 生成指南:从交互式建模到持久化与自动导入的完整实践
2026/10/3 1:57:43 网站建设 项目流程
  • 前端
  • AI 技能

【免费下载链接】basic

⭐⭐⭐⭐⭐ 面向 AI 编程的管理系统框架,兼容PC、移动端。AI-oriented management system framework, compatible with PC and mobile device.

项目地址:https://gitcode.com/GitHub_Trending/ba/basic
点击查看免费下载

Store(状态仓库)是管理系统中承载"全局共享数据"的核心设施。本文围绕 fa-store-generator 技能 及其配套的 Store 模板与示例,系统讲解在 Fantastic-admin 这类 monorepo 管理框架中如何规范地创建 Pinia Store:包括创建前的交互式需求收集、命名规范、四种代码模板变体、持久化方案、自动导入机制,以及组件外部使用 Store 的正确姿势。读完本文,你将能独立为任意应用模块设计并落地一个可维护、可持久化、可被全局自动注入的业务 Store。

一、为什么需要 Store:技能触发的典型场景

在 Fantastic-admin 中,Store 解决的是"数据需要在多个页面、多个组件之间共享,并且刷新页面后仍然存在"这类问题。官方技能文档明确列出了应当启用 Store 生成流程的触发条件:

  • 多个页面需要共享数据;
  • 需要全局状态管理;
  • 数据需要持久化(刷新后还在);
  • 登录状态 / 用户信息需要缓存;
  • 购物车、通知、权限等全局数据;
  • 需要在组件外访问状态。

用户可能只是简单说一句"这个数据要全局共享"或"刷新后数据不能丢",此时就应当按本技能生成对应 Store。对应的触发关键词包括:创建 store、全局状态、状态管理、pinia、持久化、共享数据。从源码结构看,框架本身正是围绕这些场景组织 Store 的:登录与账号、主题设置、菜单、标签页、路由、页面保活各自都有独立的 Store 模块(见 apps/core/src/store/modules/app/),因此业务开发中新增 Store 是极其频繁的操作,规范化生成流程能显著减少样板代码与踩坑。

二、第一步:确认目标应用(monorepo 工作区)

本项目是 monorepo 架构,apps/目录下存放各应用(如core、core-naive-ui、example等),每个应用拥有独立的src/store/modules/目录。因此在执行任何文件读写操作之前,必须先确认目标应用:

  1. 执行ls apps/列出所有可用应用;
  2. 立即向用户提问,明确询问要在哪个应用中创建 Store,并停止等待回复;
  3. 收到用户明确回复后,才能继续后续步骤。

严格规则:如果用户没有在请求中明确说明目标应用(例如"在 example 应用中"、"apps/core"),则必须提问,不得自行猜测或默认选择任何应用。

确认后,后续所有文件路径均以该应用目录为根,例如apps/<app>/src/store/modules/。

项目 Store 概览

维度约定
位置apps/<app>/src/store/modules/(业务 store)
代码风格全部使用Composition API风格(defineStore+ setup 函数)
导入方式通过unplugin-auto-import自动导入,组件中无需手动 import
持久化pinia-plugin-persistedstatev4,配置persist: { pick: [...] }

其中"自动导入"机制已在 Vite 插件层落地,详见下文第五节。

三、交互式工作流:先收集信息,再生成代码

Store 的字段结构、持久化需求、异步 action 直接决定代码骨架——跳过信息收集会生成空壳,用户后续还要大量修改。因此官方技能要求按三步推进。

Step 1:收集基本信息

向用户提问(可合并为一次):

  1. Store 用途:管理什么数据?(例如:用户信息、购物车、通知列表)
  2. 存放位置:
    • apps/<app>/src/store/modules/— 业务 store(推荐)
    • apps/<app>/src/store/modules/app/— 框架级 store(仅框架内部使用)
  3. State 字段:需要哪些状态字段?请列出字段名、类型和初始值

Step 2:收集功能需求

根据 Step 1 的回答,继续询问:

  1. 持久化:是否需要持久化到 localStorage?如果是,哪些字段需要持久化?
  2. 异步 Action:是否有需要调用 API 的操作?如果有,请描述接口用途
  3. Computed:是否需要派生状态(computed)?例如:从列表中过滤、统计数量等

Step 3:确认并生成

汇总用户的回答,展示将要生成的内容摘要,确认后再写入文件。

这一工作流与真实框架 Store 的复杂度完全对应——例如 useAppRouteStore 同时包含ref状态、computed派生路由、异步 action(generateRoutesAtBack请求后端路由并构建匹配器);useAppAccountStore 则同时包含异步登录/登出、权限拉取与 localStorage 持久化。字段结构、异步链路、派生逻辑三者缺一不可。

四、命名规范

类型Store ID函数名文件名
业务 storecamelCaseuse<Name>Store<name>.ts
框架 storeapp<Name>useApp<Name>Store<name>.ts

示例:

  • 购物车 → ID:cart,函数:useCartStore,文件:apps/<app>/src/store/modules/cart.ts
  • 通知 → ID:notification,函数:useNotificationStore,文件:apps/<app>/src/store/modules/notification.ts

该规范在框架现有代码中得到了完整贯彻:useAppAccountStore、useAppSettingsStore、useAppMenuStore、useAppTabbarStore、useAppRouteStore、useAppKeepAliveStore均位于apps/core/src/store/modules/app/下,文件名与 ID 一一对应(见 apps/core/src/store/modules/app/)。

五、Store 模板与四种变体

5.1 基础模板

所有 Store 均采用defineStore+ setup 函数(Composition API)风格,结构统一为 State / Computed / Actions 三段式,最后统一return暴露:

import { defineStore } from 'pinia' export const use<Name>Store = defineStore('<id>', () => { // State const <field> = ref<<Type>>(<initialValue>) // Computed const <computed> = computed(() => ...) // Actions function <action>() { ... } return { <field>, <computed>, <action>, } })

5.2 变体一:纯状态 Store(无持久化、无异步)

适合购物车这类"会话内共享即可"的数据。注意addItem中对已存在条目做数量累加、total用reduce实时计算总价的写法,是典型的派生状态用法:

import { defineStore } from 'pinia' export const useCartStore = defineStore('cart', () => { const items = ref<CartItem[]>([]) const visible = ref(false) const total = computed(() => items.value.reduce((sum, item) => sum + item.price * item.quantity, 0), ) function addItem(item: CartItem) { const existing = items.value.find(i => i.id === item.id) if (existing) { existing.quantity++ } else { items.value.push({ ...item, quantity: 1 }) } } function removeItem(id: string) { items.value = items.value.filter(i => i.id !== id) } function clear() { items.value = [] } return { items, visible, total, addItem, removeItem, clear } })

框架中 useAppKeepAliveStore 就是典型的纯状态 Store:仅维护list: string[],提供add/remove/clean三个同步 action,没有任何持久化与异步逻辑。

5.3 变体二:带持久化的 Store

这是"刷新后数据不能丢"类需求的推荐写法,利用pinia-plugin-persistedstatev4 的第二参数persist配置:

import { defineStore } from 'pinia' export const useUserPreferenceStore = defineStore( 'userPreference', () => { const theme = ref<'light' | 'dark'>('light') const language = ref('zh-cn') const pageSize = ref(20) function setTheme(val: 'light' | 'dark') { theme.value = val } return { theme, language, pageSize, setTheme } }, { persist: { pick: ['theme', 'language', 'pageSize'], // 只持久化指定字段 }, }, )

持久化默认使用 localStorage,key 为 store ID。 使用pick只持久化部分字段,避免持久化临时状态。

需要特别说明的是:持久化插件的接入是全局性的。在 apps/core/src/store/index.ts 中,pinia实例被创建后立即pinia.use(piniaPluginPersistedstate)注册插件,再在 apps/core/src/main.ts 中通过app.use(pinia)挂载。这意味着任何 Store 只要声明了persist配置即可生效,无需额外改动入口文件。

值得对比的是框架现有的 useAppAccountStore:它采用了手动 localStorage 读写的方式持久化token/account/avatar(初始化时localStorage.getItem,登录时setItem,登出时removeItem)。两种方案各有适用场景——模板中的persist.pick适合"字段较多、部分需要落盘"的场景,手动 localStorage 则适合"敏感凭证需要精确控制读写时机"的场景。业务 Store 默认推荐使用persist.pick方案。

5.4 变体三:带异步 Action 的 Store

对接后端接口的标准模式:loading状态包裹请求过程、try/finally保证 loading 复位、computed 派生计数、action 内更新本地状态:

import { defineStore } from 'pinia' export const useNotificationStore = defineStore('notification', () => { const list = ref<Notification[]>([]) const loading = ref(false) const unreadCount = computed(() => list.value.filter(n => !n.read).length) async function fetchList() { loading.value = true try { const res = await api.notification.list() list.value = res.data } finally { loading.value = false } } async function markRead(id: string) { await api.notification.markRead(id) const item = list.value.find(n => n.id === id) if (item) { item.read = true } } return { list, loading, unreadCount, fetchList, markRead } })

这一模式在框架中随处可见。useAppAccountStore 的login、getPermissions、editPassword均为异步 action,且login成功后同步写入 localStorage 与响应式状态;useAppRouteStore 的generateRoutesAtBack则演示了"请求后端路由 → 格式化(formatBackRoutes按Layout/ 组件路径映射到import.meta.glob('@/views/**/*.vue'))→ 排序 → 构建createRouterMatcher"的完整异步链路。

5.5 变体四:带 TypeScript 接口定义的 Store

为复杂数据定义接口类型,并演示"已缓存则跳过请求"的典型优化:

import { defineStore } from 'pinia' interface DictionaryItem { label: string value: string | number } interface DictionaryState { [key: string]: DictionaryItem[] } export const useDictionaryStore = defineStore('dictionary', () => { const data = ref<DictionaryState>({}) function getItems(type: string): DictionaryItem[] { return data.value[type] ?? [] } async function fetchByType(type: string) { if (data.value[type]) return // 已缓存,跳过 const res = await api.dictionary.getByType(type) data.value[type] = res.data } return { data, getItems, fetchByType } })

框架级 Store 同样大量使用类型声明。例如 useAppMenuStore 引入MenuRecordMainRaw、MenuRecordRaw、RouteRecordMainRaw等类型约束菜单与路由结构;useAppRouteStore 使用RouterMatcher类型保存路由匹配器。可见"类型先行"是框架 Store 的一致实践。

六、自动导入机制:无需手动 import

Store 文件放入src/store/modules/后,unplugin-auto-import会自动扫描并全局注入,组件内直接const xxxStore = useXxxStore()即可,无需手动 import。该机制在 Vite 插件配置中显式声明:

// apps/core/vite/plugins.ts autoImport({ imports: [ 'vue', 'vue-router', 'pinia', FantasticAdminComponentsAutoImports, FantasticAdminComposablesAutoImports, ], dts: './src/types/auto-imports.d.ts', dirs: [ './src/store/modules/**/*', './src/composables/**/*', ], })

关键在dirs: ['./src/store/modules/**/*']这一项——它以 glob 形式扫描所有 Store 模块文件并注入导出的useXxxStore函数。这也是"Store ID 必须全局唯一(camelCase)"这条注意事项的根源:一旦同名,自动导入与 Pinia 实例解析都会产生歧义。

七、在组件外部使用 Store:必须传入 pinia 实例

在组件/composable 内可直接使用const xxxStore = useXxxStore();但在组件外(如路由守卫、路由配置、入口文件)调用时,由于此时还未建立组件上下文,必须显式传入全局 pinia 实例:

useXxxStore(pinia)

框架的 router/extensions.ts 中全部采用这一写法,例如:

const appSettingsStore = useAppSettingsStore(pinia) const appTabbarStore = useAppTabbarStore(pinia)

同样地,router/routes.ts 在计算首页标题时使用useAppSettingsStore(pinia).settings.app.home.title;router/index.ts 在创建路由历史模式时读取useAppSettingsStore(pinia).settings.app.routeMode。这些例子说明:在 Vue 应用正式挂载(app.use(pinia))之前,只要把pinia实例作为参数传入,就能安全地在模块顶层读取 Store 状态。

八、生成后的操作指引

Store 文件创建完成后,向用户交付以下信息:

  • 无需手动 import,unplugin-auto-import已自动处理;
  • 在任意组件/composable 中直接使用:const xxxStore = useXxxStore();
  • 如需在 store 外部(如路由守卫)使用,需传入 pinia 实例:useXxxStore(pinia)。

九、项目现有 Store 参考

模板文档给出了 4 个框架级 Store 参考;从当前仓库源码看,apps/core应用的store/modules/app/下实际存在 6 个框架 Store(完整清单见 apps/core/src/store/modules/app/):

Store文件用途
useAppAccountStoremodules/app/account.ts登录/登出、token、多账号、权限
useAppSettingsStoremodules/app/settings.ts主题、语言、布局配置、显示模式
useAppMenuStoremodules/app/menu.ts菜单生成与导航状态
useAppTabbarStoremodules/app/tabbar.ts标签栏管理
useAppRouteStoremodules/app/route.ts动态路由生成与匹配器
useAppKeepAliveStoremodules/app/keepAlive.ts页面保活列表管理

这 6 个 Store 是理解"复杂 Store 该如何设计"的最佳教材:useAppSettingsStore通过watch将状态变更实时同步到document.documentElement(主题色、圆角、灰度滤镜等副作用);useAppMenuStore由路由数据computed派生菜单树并按权限过滤;useAppTabbarStore在add/remove时联动useAppKeepAliveStore维护保活列表——即下面要讲的跨 Store 调用。

十、跨 Store 调用与注意事项

模板文档明确:跨 store 调用直接在 setup 函数内调用其他 store,例如:

const authStore = useAppAccountStore()

框架中的典型示例是 useAppAccountStore,它在 setup 顶部一次性获取多个依赖 Store:

const appSettingsStore = useAppSettingsStore() const appTabbarStore = useAppTabbarStore() const appRouteStore = useAppRouteStore() const appMenuStore = useAppMenuStore()

随后在logoutCleanStatus中联动清理:appSettingsStore.updateSettings({}, true)重置设置、appTabbarStore.clean()清空标签页、appRouteStore.removeRoutes()移除动态路由、appMenuStore.setActived(0)复位主菜单。这是"登出时全局状态复位"的最佳实践范本。

最后是模板文档列出的完整注意事项清单:

  • Store 文件放在src/store/modules/后,unplugin-auto-import会自动扫描并全局注入,无需手动 import;
  • Store ID 必须全局唯一(camelCase);
  • 避免在 store 中直接引用 DOM 或组件实例(但框架的useAppSettingsStore因主题系统需要会操作document与navigator,属特例);
  • 跨 store 调用:直接在 setup 函数内调用其他 store,如const authStore = useAppAccountStore()。

结语

从交互式需求收集,到命名规范、四种模板变体,再到自动导入与组件外使用,fa-store-generator 提供了一套端到端的 Store 生成方法论;而 store-patterns.md 与框架现有 6 个 Store 实现相互印证,构成了"模板可抄、范例可读、原理可查"的完整闭环。开发者在实际落地时,只需遵循"先确认应用 → 再收集需求 → 套用模板 → 按需持久化 → 组件外传 pinia"这条链路,即可稳定地产出高质量的状态管理模块。

  • 前端
  • AI 技能

【免费下载链接】basic

⭐⭐⭐⭐⭐ 面向 AI 编程的管理系统框架,兼容PC、移动端。AI-oriented management system framework, compatible with PC and mobile device.

项目地址:https://gitcode.com/GitHub_Trending/ba/basic
点击查看免费下载

相关推荐

上一篇:Runtipi Docker集成深度剖析:容器网络与数据卷管理
下一篇:Phaser CE入门指南:打造你的首款HTML5 2D游戏的完整教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询