- 前端
- AI 技能
【免费下载链接】basic
⭐⭐⭐⭐⭐ 面向 AI 编程的管理系统框架,兼容PC、移动端。AI-oriented management system framework, compatible with PC and mobile device.
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/目录。因此在执行任何文件读写操作之前,必须先确认目标应用:
- 执行
ls apps/列出所有可用应用; - 立即向用户提问,明确询问要在哪个应用中创建 Store,并停止等待回复;
- 收到用户明确回复后,才能继续后续步骤。
严格规则:如果用户没有在请求中明确说明目标应用(例如"在 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:收集基本信息
向用户提问(可合并为一次):
- Store 用途:管理什么数据?(例如:用户信息、购物车、通知列表)
- 存放位置:
apps/<app>/src/store/modules/— 业务 store(推荐)apps/<app>/src/store/modules/app/— 框架级 store(仅框架内部使用)
- State 字段:需要哪些状态字段?请列出字段名、类型和初始值
Step 2:收集功能需求
根据 Step 1 的回答,继续询问:
- 持久化:是否需要持久化到 localStorage?如果是,哪些字段需要持久化?
- 异步 Action:是否有需要调用 API 的操作?如果有,请描述接口用途
- Computed:是否需要派生状态(computed)?例如:从列表中过滤、统计数量等
Step 3:确认并生成
汇总用户的回答,展示将要生成的内容摘要,确认后再写入文件。
这一工作流与真实框架 Store 的复杂度完全对应——例如 useAppRouteStore 同时包含ref状态、computed派生路由、异步 action(generateRoutesAtBack请求后端路由并构建匹配器);useAppAccountStore 则同时包含异步登录/登出、权限拉取与 localStorage 持久化。字段结构、异步链路、派生逻辑三者缺一不可。
四、命名规范
| 类型 | Store ID | 函数名 | 文件名 |
|---|---|---|---|
| 业务 store | camelCase | use<Name>Store | <name>.ts |
| 框架 store | app<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 | 文件 | 用途 |
|---|---|---|
useAppAccountStore | modules/app/account.ts | 登录/登出、token、多账号、权限 |
useAppSettingsStore | modules/app/settings.ts | 主题、语言、布局配置、显示模式 |
useAppMenuStore | modules/app/menu.ts | 菜单生成与导航状态 |
useAppTabbarStore | modules/app/tabbar.ts | 标签栏管理 |
useAppRouteStore | modules/app/route.ts | 动态路由生成与匹配器 |
useAppKeepAliveStore | modules/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.
相关推荐
Redwood 框架中的 TypeScript 支持:从零接入、自动类型生成到严格模式的完整指南
Redwood 框架中的 TypeScript 支持:从零接入、自动类型生成到严格模式的完整指南 Redwood 框架内置了完整的 TypeScript 支持,
后端前端Web框架开发工具Fantastic-admin CRUD 页面生成模板全解析:从占位符到完整业务模块的代码生成实践
Fantastic admin CRUD 页面生成模板全解析:从占位符到完整业务模块的代码生成实践 本文以 Fantastic admin 框架(GitHub_
前端AI 技能Fantastic-admin CRUD 页面生成器:从零生成标准业务模块的完整指南
Fantastic admin CRUD 页面生成器:从零生成标准业务模块的完整指南 导读 本文围绕 Fantastic admin 框架内置的 fa crud
前端AI 技能
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考