Files
new-school-iot-admin/.agents/skills/v3-upsert-store/SKILL.md
T
2026-10-26 18:54:09 +08:00

251 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: v3-upsert-store
description: 创建或更新全局状态(Store)。当用户提到以下任何场景时都应触发:新建状态管理、新增 Pinia Store、给 Store 加字段。即使用户没有明确说 Store,只要意图是和状态管理器有关就应该使用此 Skill。使用时需提供 Store 名称、State 字段和 Actions 描述。
metadata:
author: pany
version: "2026.06.01"
---
# 创建或更新 Pinia Store
根据用户提供的 Store 名称、State 字段和 Actions 描述,生成或更新 Pinia Store 文件。
本 Skill 定义的是项目默认 Store 模式,当用户的实际需求与本 Skill 约定冲突时,以用户需求为准。
## 输入要求
用户需提供:
1. **Store 名称**(如 `counter`、`notification`)— 用于文件名和命名
2. **State 字段列表** — 每个字段需明确:
- 字段名(英文,camelCase)
- 类型(string / number / boolean / array / object / 自定义类型等)
- 初始值
- 是否需要持久化到 localStorage
3. **Actions 描述** — 需要哪些操作(如 reset、toggle、increment、fetch 等)
如果用户信息不完整,主动询问补全后再生成。
## 操作模式
### 创建新 Store
当 `src/pinia/stores/<store名>.ts` 不存在时,生成完整的 Store 文件。
### 更新已有 Store
当文件已存在时,在已有代码中追加新的 State 字段和 Actions,保留原有代码不动。追加时:
- 新 State 声明插入到已有 State 声明之后
- 新 Actions 插入到已有 Actions 之后(如有 `// #region` 分组则放在对应区域)
- 更新 `return` 语句,追加新导出的字段和方法
## 生成文件
### Store 文件
路径:`src/pinia/stores/<store名>.ts`
### 持久化相关(仅在用户指定持久化字段时生成)
- 在 `src/common/constants/cache-key.ts` 的 `CacheKey` 类中追加新的 Key
- 在 `src/common/utils/local-storage.ts` 中追加对应的 get/set 函数
## 代码规范
### Store 结构
完整的 Store 文件结构如下(以一个带持久化和辅助函数的 Store 为例):
```ts
import type { XxxType } from "@@/types/xxx"
import { getXxx, setXxx } from "@@/utils/local-storage"
import { pinia } from "@/pinia"
interface Sidebar {
opened: boolean
withoutAnimation: boolean
}
/** 辅助函数的用途描述 */
function helperFunction(param: string) {
// ...
}
export const useXxxStore = defineStore("xxx", () => {
// Token
const token = ref<string>(getToken() || "")
// 侧边栏状态
const sidebar: Sidebar = reactive({
opened: true,
withoutAnimation: false
})
// 设置 Token
const setToken = (value: string) => {
token.value = value
}
// 切换侧边栏
const toggleSidebar = (withoutAnimation: boolean) => {
sidebar.opened = !sidebar.opened
sidebar.withoutAnimation = withoutAnimation
}
return { token, sidebar, setToken, toggleSidebar }
})
/**
* @description 在 SPA 应用中可用于在 pinia 实例被激活前使用 store
* @description 在 SSR 应用中可用于在 setup 外使用 store
*/
export function useXxxStoreOutside() {
return useXxxStore(pinia)
}
```
### 关键规则
1. **必须使用 Setup Store 语法**(`defineStore("id", () => { ... })`),不使用 Options API
2. **必须导出 `useXxxStoreOutside`** 函数,且 JSDoc 注释原样保留(见上方模板)
3. **`defineStore` 第一个参数**(Store ID):单词用小写(如 `"user"`),多词用 kebab-case(如 `"tags-view"`)
4. **State 类型标注**:
- `ref` 用泛型:`ref<string>("")`、`ref<number>(0)`、`ref<string[]>([])`
- `reactive` 在变量上标注类型:`const sidebar: Sidebar = reactive({...})`
5. **Actions 统一使用箭头函数**:`const xxx = (param: Type) => { ... }`
6. **注释风格**:
- Store 内部的 State 和 Actions 用 `//` 单行注释,描述具体用途(如 `// 切换侧边栏`、`// 设置 Token`)
- 不要使用 `/** */` JSDoc 注释 Store 内部成员
- 不要使用泛化的分区标题(如 `// state`、`// actions`),每条注释直接描述对应内容
- 模块级辅助函数(Store 外部)使用 `/** */` JSDoc 注释
7. **`// #region` / `// #endregion`** 仅在同一类操作有多组变体时使用(如 tags-view 的 add/del/delOthers/delAll),普通 Store 不需要
8. **自动导入**:`defineStore`、`ref`、`reactive`、`watch`、`watchEffect`、`computed` 无需手动 import
9. **Vue 类型导入是允许的**:如 `import type { Ref } from "vue"`
### 文件结构顺序
```
1. import type 语句(类型导入)
2. import 语句(运行时导入,Pinia 实例必须导入)
3. interface / type 声明(Store 需要的本地类型)
4. 模块级辅助函数(不需要响应式访问的纯函数,用 /** */ 注释)
5. export const useXxxStore = defineStore(...)
6. export function useXxxStoreOutside()
```
### 持久化模式
当字段需要持久化时,遵循项目已有模式:
**1. CacheKey 常量**(`src/common/constants/cache-key.ts`):
```ts
static readonly XXX_DATA = `${SYSTEM_NAME}-xxx-data-key`
```
命名规则:大写下划线 + `-key` 后缀。
**2. localStorage 工具函数**(`src/common/utils/local-storage.ts`):
简单字符串值:
```ts
// #region Xxx 描述
export function getXxx() {
return localStorage.getItem(CacheKey.XXX_DATA)
}
export function setXxx(value: string) {
localStorage.setItem(CacheKey.XXX_DATA, value)
}
// #endregion
```
对象/数组值:
```ts
// #region Xxx 描述
export function getXxx() {
const json = localStorage.getItem(CacheKey.XXX_DATA)
return json ? (JSON.parse(json) as XxxType) : null
}
export function setXxx(data: XxxType) {
localStorage.setItem(CacheKey.XXX_DATA, JSON.stringify(data))
}
// #endregion
```
**3. Store 中使用**:
```ts
import { getXxx, setXxx } from "@@/utils/local-storage"
// Xxx 数据
const data = ref<XxxType>(getXxx() ?? defaultValue)
// 监听变化并持久化
watch(data, (newVal) => {
setXxx(newVal)
})
```
当需要同时监听多个持久化字段时,用 `watchEffect` 代替多个 `watch`:
```ts
watchEffect(() => {
setVisitedViews(visitedViews.value)
setCachedViews(cachedViews.value)
})
```
### 类型定义
- 简单类型直接在 Store 文件中用 `interface` 或 `type` 声明(定义在 Store 函数之前)
- 需要被外部引用的类型加 `export`(如 `export type TagView = Partial<RouteLocationNormalizedGeneric>`)
- 复杂、共享类型抽离到 `types` 目录或就近放置
## 命名约定
Store 名为 `notification` 时:
| 位置 | 命名 |
|------|------|
| 文件 | `src/pinia/stores/notification.ts` |
| Store ID | `"notification"` |
| Composable | `useNotificationStore` |
| Outside | `useNotificationStoreOutside` |
| CacheKey(如需) | `NOTIFICATION_DATA` |
| localStorage 函数(如需) | `getNotificationData` / `setNotificationData` |
多词 Store 名使用 kebab-case 文件名和 Store ID(如 `user-preference` → `"user-preference"` → `useUserPreferenceStore`)。
## 导入规范
```ts
// 类型导入
import type { Ref } from "vue"
import type { RouteRecordRaw } from "vue-router"
// 运行时导入:常量和工具函数
import { SIDEBAR_CLOSED, SIDEBAR_OPENED } from "@@/constants/app-key"
import { getSidebarStatus, setSidebarStatus } from "@@/utils/local-storage"
// 运行时导入:Pinia 实例(必需)
import { pinia } from "@/pinia"
// 运行时导入:其他 Store(当需要跨 Store 交互时)
import { useSettingsStore } from "./settings"
```
注意导入顺序:类型导入在前,运行时导入在后;`@@` 路径在前,`@` 路径在后。
## 设计决策指引
**ref vs reactive** — 当 State 是一个有结构的对象且字段间关联紧密时用 `reactive`(如 sidebar 的 opened + withoutAnimation),否则每个独立状态用 `ref`。大多数情况下用 `ref`。
**watch vs watchEffect** — 监听单个字段变化时用 `watch`;监听多个字段且都需要持久化时用 `watchEffect`(自动追踪依赖,代码更简洁)。
**辅助函数放在 Store 内还是外** — 不依赖响应式状态的纯逻辑函数放在 Store 外部(如权限过滤、格式化),需要访问 Store 内 `ref` / `reactive` 的函数放在内部作为 Action。
**是否需要 Reset Action** — 如果 Store 管理的是临时状态(如表单数据、UI 状态),通常需要一个 Reset 方法恢复初始值。如果是用户身份等持久数据,通常不需要通用 Reset。
**是否需要 Outside 函数** — 始终生成。这是项目约定,确保在路由守卫、Axios 拦截器等 setup 外场景可用。