mdk-skills 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.claude/.install.log +4 -0
  2. package/.claude/settings.json +64 -0
  3. package/.claude/settings.local.json +7 -0
  4. package/.claude/skills/agentation/.meta.json +6 -0
  5. package/.claude/skills/agentation/SKILL.md +107 -0
  6. package/.claude/skills/fe-biz-patterns/.meta.json +6 -0
  7. package/.claude/skills/fe-biz-patterns/SKILL.md +26 -0
  8. package/.claude/skills/fe-biz-patterns/references/infinite-scroll.md +292 -0
  9. package/.claude/skills/fe-biz-patterns/references/pinia-store.md +174 -0
  10. package/.claude/skills/fe-biz-patterns/references/service-layer.md +198 -0
  11. package/.claude/skills/fe-biz-patterns/references/tab-anchor.md +1125 -0
  12. package/.claude/skills/fe-biz-patterns/references/use-loading.md +114 -0
  13. package/.claude/skills/frontend-code-review/.meta.json +6 -0
  14. package/.claude/skills/frontend-code-review/SKILL.md +167 -0
  15. package/.claude/skills/frontend-code-review/references/checklist.md +298 -0
  16. package/.claude/skills/frontend-design/.meta.json +6 -0
  17. package/.claude/skills/frontend-design/LICENSE.txt +177 -0
  18. package/.claude/skills/frontend-design/SKILL.md +42 -0
  19. package/.claude/skills/moai-framework-electron/.meta.json +6 -0
  20. package/.claude/skills/moai-framework-electron/SKILL.md +328 -0
  21. package/.claude/skills/skill-creator/.meta.json +6 -0
  22. package/.claude/skills/skill-creator/SKILL.md +356 -0
  23. package/.claude/skills/skill-creator/references/output-patterns.md +82 -0
  24. package/.claude/skills/skill-creator/references/workflows.md +28 -0
  25. package/.claude/skills/skill-creator/scripts/init_skill.py +303 -0
  26. package/.claude/skills/skill-creator/scripts/package_skill.py +110 -0
  27. package/.claude/skills/skill-creator/scripts/quick_validate.py +95 -0
  28. package/.claude/skills/ui-ux-pro-max/.meta.json +6 -0
  29. package/.claude/skills/ui-ux-pro-max/SKILL.md +228 -0
  30. package/.claude/skills/ui-ux-pro-max/data/charts.csv +26 -0
  31. package/.claude/skills/ui-ux-pro-max/data/colors.csv +97 -0
  32. package/.claude/skills/ui-ux-pro-max/data/landing.csv +31 -0
  33. package/.claude/skills/ui-ux-pro-max/data/products.csv +97 -0
  34. package/.claude/skills/ui-ux-pro-max/data/prompts.csv +24 -0
  35. package/.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  36. package/.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  37. package/.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  38. package/.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  39. package/.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  40. package/.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  41. package/.claude/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  42. package/.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  43. package/.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  44. package/.claude/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  45. package/.claude/skills/ui-ux-pro-max/data/styles.csv +59 -0
  46. package/.claude/skills/ui-ux-pro-max/data/typography.csv +58 -0
  47. package/.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  48. package/.claude/skills/ui-ux-pro-max/scripts/core.py +238 -0
  49. package/.claude/skills/ui-ux-pro-max/scripts/search.py +61 -0
  50. package/.claude/skills/vue/.meta.json +6 -0
  51. package/.claude/skills/vue/SKILL.md +103 -0
  52. package/.claude/skills/vue/references/components.md +323 -0
  53. package/.claude/skills/vue/references/composables.md +358 -0
  54. package/.claude/skills/vue/references/directives.md +225 -0
  55. package/.claude/skills/vue/references/gotchas.md +438 -0
  56. package/.claude/skills/vue/references/provide-inject.md +174 -0
  57. package/.claude/skills/vue/references/reactivity.md +289 -0
  58. package/.claude/skills/vue/references/router.md +181 -0
  59. package/.claude/skills/vue/references/testing.md +294 -0
  60. package/.claude/skills/vue/references/typescript.md +172 -0
  61. package/.claude/skills/vue/references/utils-client.md +156 -0
  62. package/CLAUDE.md +131 -0
  63. package/package.json +23 -0
  64. package/scripts/cli.js +260 -0
  65. package/scripts/copy-skills.js +86 -0
  66. package/scripts/core.js +256 -0
@@ -0,0 +1,4 @@
1
+ [2026-05-09 20:45:26] [INFO] Install started (upgrade)
2
+ [2026-05-09 20:45:38] [INFO] Install started (upgrade)
3
+ [2026-05-09 20:46:37] [INFO] Install started (upgrade)
4
+ [2026-05-09 20:47:06] [INFO] Install started (upgrade)
@@ -0,0 +1,64 @@
1
+ {
2
+ "skills": {
3
+ "simplify": {
4
+ "enabled": true,
5
+ "description": "审查变更代码,确保复用性、质量和效率"
6
+ },
7
+ "loop": {
8
+ "enabled": true,
9
+ "description": "按固定间隔重复执行提示词或命令"
10
+ },
11
+ "claude-api": {
12
+ "enabled": true,
13
+ "description": "构建、调试和优化 Claude API / Anthropic SDK 应用"
14
+ },
15
+ "agentation": {
16
+ "enabled": true,
17
+ "description": "Agentation React 可视化反馈工具栏"
18
+ },
19
+ "frontend-code-review": {
20
+ "enabled": true,
21
+ "description": "前端代码全面审查"
22
+ },
23
+ "frontend-design": {
24
+ "enabled": true,
25
+ "description": "创意前端界面设计,落地页/品牌页/营销页"
26
+ },
27
+ "skill-creator": {
28
+ "enabled": true,
29
+ "description": "创建和更新技能指令"
30
+ },
31
+ "ui-ux-pro-max": {
32
+ "enabled": true,
33
+ "description": "UI/UX 设计智能系统,50 种样式,21 种配色方案"
34
+ },
35
+ "vue": {
36
+ "enabled": true,
37
+ "description": "Vue 3 组件/组合式函数开发,提供 Composition API 最佳实践"
38
+ },
39
+ "init": {
40
+ "enabled": true,
41
+ "description": "初始化 CLAUDE.md 项目文档"
42
+ },
43
+ "review": {
44
+ "enabled": true,
45
+ "description": "Pull Request 代码审查"
46
+ },
47
+ "security-review": {
48
+ "enabled": true,
49
+ "description": "待变更代码安全审查"
50
+ },
51
+ "fe-biz-patterns": {
52
+ "enabled": true,
53
+ "description": "前端业务模式库,loading/滚动加载/导入导出/批量操作/表单联动/大列表渲染/Service层封装/Pinia Store模式/分页数据管理等常见业务场景"
54
+ }
55
+ },
56
+ "always_apply_skills": [
57
+ "simplify",
58
+ "frontend-design",
59
+ "vue",
60
+ "frontend-code-review",
61
+ "ui-ux-pro-max",
62
+ "security-review"
63
+ ]
64
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "mcp__chrome-devtools__evaluate_script"
5
+ ]
6
+ }
7
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "agentation",
3
+ "version": "1.0.0",
4
+ "description": "Agentation React 可视化反馈工具栏",
5
+ "tags": ["react", "ui", "debug"]
6
+ }
@@ -0,0 +1,107 @@
1
+ ---
2
+ name: agentation
3
+ description: Add Agentation visual feedback toolbar to any React project
4
+ ---
5
+
6
+ # Agentation Setup
7
+
8
+ Set up the Agentation annotation toolbar in this project.
9
+
10
+ ## Requirements
11
+
12
+ - React 18+
13
+ - Zero dependencies beyond React
14
+
15
+ ## Steps
16
+
17
+ 1. **Check if already installed**
18
+ - Look for `agentation` in package.json dependencies
19
+ - If not found, run `npm install agentation` (or pnpm/yarn based on lockfile)
20
+
21
+ 2. **Check if already configured**
22
+ - Search for `<Agentation` or `import { Agentation }` in src/ or app/
23
+ - If found, report that Agentation is already set up and exit
24
+
25
+ 3. **Detect project type**
26
+ - **Next.js App Router**: has `app/layout.tsx` or `app/layout.js`
27
+ - **Next.js Pages Router**: has `pages/_app.tsx` or `pages/_app.js`
28
+ - **Vite + React**: has `vite.config.ts/js` and `src/main.tsx/jsx`
29
+ - **Create React App**: has `src/index.tsx/jsx` or `src/App.tsx/jsx`
30
+ - **Other React projects**: look for main entry file
31
+
32
+ 4. **Add the component**
33
+
34
+ **For Next.js App Router**, add to `app/layout.tsx`:
35
+ ```tsx
36
+ import { Agentation } from "agentation";
37
+
38
+ export default function RootLayout({ children }) {
39
+ return (
40
+ <html>
41
+ <body>
42
+ {children}
43
+ {process.env.NODE_ENV === "development" && <Agentation />}
44
+ </body>
45
+ </html>
46
+ );
47
+ }
48
+ ```
49
+
50
+ **For Next.js Pages Router**, add to `pages/_app.tsx`:
51
+ ```tsx
52
+ import { Agentation } from "agentation";
53
+
54
+ export default function App({ Component, pageProps }) {
55
+ return (
56
+ <>
57
+ <Component {...pageProps} />
58
+ {process.env.NODE_ENV === "development" && <Agentation />}
59
+ </>
60
+ );
61
+ }
62
+ ```
63
+
64
+ **For Vite + React**, add to `src/main.tsx`:
65
+ ```tsx
66
+ import { Agentation } from "agentation";
67
+
68
+ ReactDOM.createRoot(document.getElementById('root')!).render(
69
+ <React.StrictMode>
70
+ <App />
71
+ {import.meta.env.DEV && <Agentation />}
72
+ </React.StrictMode>
73
+ );
74
+ ```
75
+
76
+ **For Create React App**, add to `src/index.tsx`:
77
+ ```tsx
78
+ import { Agentation } from "agentation";
79
+
80
+ const root = ReactDOM.createRoot(document.getElementById('root'));
81
+ root.render(
82
+ <React.StrictMode>
83
+ <App />
84
+ {process.env.NODE_ENV === "development" && <Agentation />}
85
+ </React.StrictMode>
86
+ );
87
+ ```
88
+
89
+ **For other React projects**, add to the root component or main entry file:
90
+ ```tsx
91
+ import { Agentation } from "agentation";
92
+
93
+ // Add at the end of your root component
94
+ {process.env.NODE_ENV === "development" && <Agentation />}
95
+ ```
96
+
97
+ 5. **Confirm setup**
98
+ - Tell the user to run their dev server and look for the Agentation toolbar (floating button in bottom-right corner)
99
+ - The toolbar should appear in the bottom-right corner of the page
100
+
101
+ ## Notes
102
+
103
+ - The environment check ensures Agentation only loads in development
104
+ - For Vite projects, use `import.meta.env.DEV` instead of `process.env.NODE_ENV`
105
+ - For Next.js, use `process.env.NODE_ENV === "development"`
106
+ - No additional configuration needed — it works out of the box
107
+ - Compatible with any React 18+ project
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "fe-biz-patterns",
3
+ "version": "1.0.0",
4
+ "description": "小马自己的前端业务模式库,收录小马在前端业务中沉淀的方案和最佳实践",
5
+ "tags": ["frontend", "pattern", "vue", "react"]
6
+ }
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: fe-biz-patterns
3
+ description: 前端业务模式库,收录 XiaoMa 在实际业务中沉淀的前端方案和最佳实践。当用户要求实现包含以下特征的功能时使用:loading/加载/加载态、滚动加载/无限滚动/触底加载、数据导入导出、批量操作、表单联动、权限控制、大列表渲染、文件上传、实时搜索、拖拽排序、Service层封装/请求层/axios封装、Pinia Store模式/状态管理/分页数据管理、Tab 锚点/TabAnchor/带锚点滚动的 Tab 切换/吸顶 Tab 栏/浮动 Tab 栏/Tab 滑动指示器等常见前端业务场景。
4
+ ---
5
+
6
+ # 前端业务模式库
7
+
8
+ XiaoMa 实战沉淀的前端业务方案集合,覆盖日常开发中高频出现的业务场景。
9
+
10
+ 每个方案独立成篇,按需加载,避免上下文浪费。
11
+
12
+ ## 业务方案导航
13
+
14
+ | 方案 | 适用场景 | 参考文档 |
15
+ |------|---------|---------|
16
+ | 通用 loading 管理 | 表单提交、数据加载、异步操作的 loading 状态控制 | [use-loading.md](references/use-loading.md) |
17
+ | 滚动触底自动加载 | 列表滚动加载更多、无限滚动、分页拉取 | [infinite-scroll.md](references/infinite-scroll.md) |
18
+ | Service 层封装 | axios 实例封装、API 模块组织、拦截器管理、多 API 源隔离 | [service-layer.md](references/service-layer.md) |
19
+ | Pinia Store 模式 | Setup Store 格局、分页数据管理、storeToRefs 解构规范 | [pinia-store.md](references/pinia-store.md) |
20
+ | Tab 锚点滚动 | TabAnchor 锚点 Tab 组件:自动/手动模式、顶栏/侧栏布局、slide/scale 指示器、sticky 吸顶、float 浮动、IntersectionObserver 自动追踪、自定义 Tab 按钮 | [tab-anchor.md](references/tab-anchor.md) |
21
+
22
+ > **使用方式**:根据用户需求匹配上方方案,点击对应链接阅读完整实现文档,按文档中的模式生成代码。选择方案后**必须向用户确认**再执行。
23
+
24
+ ## 扩展指南
25
+
26
+ 新增业务方案时,在 `references/` 下创建对应的 `.md` 文件,并在上表补充一行。
@@ -0,0 +1,292 @@
1
+ # 滚动触底自动加载方案
2
+
3
+ ## 概述
4
+
5
+ 列表页滚动到底部时自动加载下一页数据,是 B 端中最常见的需求之一。本方案提供一套开箱即用的实现,覆盖主流业务场景。
6
+
7
+ ## 核心思路
8
+
9
+ 采用 **IntersectionObserver** 监听一个不可见的"哨兵元素",当该元素进入视口时触发加载。
10
+
11
+ ```
12
+ ┌──────────────────┐
13
+ │ 列表内容 │ ← 已加载的数据
14
+ │ ... │
15
+ │ ... │
16
+ │ ┌──────────┐ │
17
+ │ │ loading │ │ ← 加载状态提示
18
+ │ └──────────┘ │
19
+ │ ▲ sentinel ◄────│── IntersectionObserver 监听此元素
20
+ └──────────────────┘
21
+ ```
22
+
23
+ > 为什么不用 scroll 事件?IntersectionObserver 性能更好、不触发重排、且不依赖滚动容器的具体滚动逻辑。
24
+
25
+ ## 自定义 Hook
26
+
27
+ ```ts
28
+ import { ref, nextTick, onMounted, onUnmounted, type Ref } from 'vue'
29
+ import { useLoading } from '@/hooks/useLoading'
30
+
31
+ interface UseInfiniteScrollOptions<T> {
32
+ /** 分页请求函数,返回当前页的数据数组。返回空数组时视为加载完毕 */
33
+ fetcher: (page: number) => Promise<T[]>
34
+ /** 滚动容器,不传则默认使用 viewport */
35
+ root?: Ref<HTMLElement | null>
36
+ /** 触发加载的提前量,默认 '100px'(提前 100px 触发) */
37
+ rootMargin?: string
38
+ /** 阈值,默认 0 */
39
+ threshold?: number
40
+ /** 是否立即加载第一页,默认 true */
41
+ immediate?: boolean
42
+ }
43
+
44
+ export function useInfiniteScroll<T>(options: UseInfiniteScrollOptions<T>) {
45
+ const { fetcher, root, rootMargin = '100px', threshold = 0, immediate = true } = options
46
+
47
+ const data = ref<T[]>([]) as Ref<T[]>
48
+ const { loading, withLoading } = useLoading() // 详见 [use-loading.md](use-loading.md)
49
+ const error = ref<Error | null>(null)
50
+ const isFinished = ref(false)
51
+ const page = ref(0)
52
+ const sentinelRef = ref<HTMLElement | null>(null)
53
+
54
+ let observer: IntersectionObserver | null = null
55
+ let stopLoading = false // 防止竞态:卸载或刷新时中断
56
+
57
+ /** 加载下一页 */
58
+ async function loadMore() {
59
+ if (loading.value || isFinished.value) return
60
+
61
+ error.value = null
62
+ const currentPage = page.value + 1
63
+
64
+ try {
65
+ await withLoading(async () => {
66
+ const res = await fetcher(currentPage)
67
+ // 卸载后丢弃结果
68
+ if (stopLoading) return
69
+
70
+ if (res.length === 0) {
71
+ isFinished.value = true
72
+ } else {
73
+ data.value.push(...res)
74
+ page.value = currentPage
75
+ }
76
+ })
77
+ } catch (e) {
78
+ if (stopLoading) return
79
+ error.value = e as Error
80
+ }
81
+ }
82
+
83
+ /** 重置并重新加载 */
84
+ async function refresh() {
85
+ // 中断当前请求
86
+ stopLoading = true
87
+ await nextTick()
88
+ stopLoading = false
89
+
90
+ data.value = []
91
+ page.value = 0
92
+ isFinished.value = false
93
+ error.value = null
94
+
95
+ if (immediate) {
96
+ await loadMore()
97
+ }
98
+ }
99
+
100
+ /** 手动触发加载(对外暴露) */
101
+ function triggerLoadMore() {
102
+ loadMore()
103
+ }
104
+
105
+ function setupObserver() {
106
+ observer = new IntersectionObserver(
107
+ (entries) => {
108
+ if (entries[0]?.isIntersecting) {
109
+ loadMore()
110
+ }
111
+ },
112
+ {
113
+ root: root?.value ?? null,
114
+ rootMargin,
115
+ threshold,
116
+ }
117
+ )
118
+
119
+ if (sentinelRef.value) {
120
+ observer.observe(sentinelRef.value)
121
+ }
122
+ }
123
+
124
+ onMounted(() => {
125
+ setupObserver()
126
+ if (immediate) {
127
+ loadMore()
128
+ }
129
+ })
130
+
131
+ onUnmounted(() => {
132
+ stopLoading = true
133
+ observer?.disconnect()
134
+ })
135
+
136
+ return {
137
+ data,
138
+ loading,
139
+ error,
140
+ isFinished,
141
+ sentinelRef,
142
+ refresh,
143
+ loadMore: triggerLoadMore,
144
+ }
145
+ }
146
+ ```
147
+
148
+ ## 组件中使用
149
+
150
+ ```vue
151
+ <script setup lang="ts">
152
+ import { useInfiniteScroll } from '@/composables/useInfiniteScroll'
153
+
154
+ interface Item {
155
+ id: number
156
+ title: string
157
+ }
158
+
159
+ async function fetchList(page: number): Promise<Item[]> {
160
+ const res = await fetch(`/api/list?page=${page}&size=20`)
161
+ const json = await res.json()
162
+ return json.data // 假设接口返回 { data: Item[], total: number }
163
+ }
164
+
165
+ const { data, loading, error, isFinished, sentinelRef, refresh } = useInfiniteScroll<Item>({
166
+ fetcher: fetchList,
167
+ })
168
+ </script>
169
+
170
+ <template>
171
+ <div class="list-page">
172
+ <div class="list-header">
173
+ <button @click="refresh">刷新</button>
174
+ </div>
175
+
176
+ <div class="list-items">
177
+ <div v-for="item in data" :key="item.id" class="list-item">
178
+ {{ item.title }}
179
+ </div>
180
+ </div>
181
+
182
+ <!-- 加载状态 -->
183
+ <div v-if="loading && data.length === 0" class="status">首次加载中...</div>
184
+ <div v-if="loading && data.length > 0" class="status">加载更多...</div>
185
+
186
+ <!-- 错误状态 -->
187
+ <div v-if="error" class="status error">
188
+ 加载失败:{{ error.message }}
189
+ <button @click="loadMore">重试</button>
190
+ </div>
191
+
192
+ <!-- 加载完成 -->
193
+ <div v-if="isFinished && data.length > 0" class="status">没有更多了</div>
194
+
195
+ <!-- 空数据 -->
196
+ <div v-if="!loading && data.length === 0 && isFinished" class="status empty">
197
+ 暂无数据
198
+ </div>
199
+
200
+ <!-- 哨兵元素:当此元素进入视口时触发加载 -->
201
+ <div v-if="!isFinished" ref="sentinelRef" class="sentinel" />
202
+ </div>
203
+ </template>
204
+
205
+ <style scoped>
206
+ .sentinel {
207
+ height: 1px;
208
+ pointer-events: none;
209
+ }
210
+ .status {
211
+ padding: 16px;
212
+ text-align: center;
213
+ color: #999;
214
+ }
215
+ .status.error {
216
+ color: #e74c3c;
217
+ }
218
+ </style>
219
+ ```
220
+
221
+ ## 适配不同场景
222
+
223
+ ### 场景 1:指定滚动容器
224
+
225
+ 在弹窗或固定高度的容器内滚动时:
226
+
227
+ ```vue
228
+ <template>
229
+ <div ref="scrollContainer" class="scroll-container">
230
+ <div v-for="item in data" :key="item.id">{{ item.title }}</div>
231
+ <div ref="sentinelRef" />
232
+ </div>
233
+ </template>
234
+
235
+ <script setup lang="ts">
236
+ const scrollContainer = ref<HTMLElement | null>(null)
237
+
238
+ const { data, sentinelRef } = useInfiniteScroll<Item>({
239
+ fetcher: fetchList,
240
+ root: scrollContainer,
241
+ rootMargin: '50px',
242
+ })
243
+ </script>
244
+ ```
245
+
246
+ ### 场景 2:带搜索/筛选
247
+
248
+ ```vue
249
+ <script setup lang="ts">
250
+ const keyword = ref('')
251
+ const categoryId = ref('')
252
+
253
+ // 搜索条件变化时重置列表
254
+ watch([keyword, categoryId], () => {
255
+ refresh()
256
+ })
257
+
258
+ const { data, ...rest } = useInfiniteScroll({
259
+ fetcher: (page) => fetchList({ page, keyword: keyword.value, categoryId: categoryId.value }),
260
+ })
261
+ </script>
262
+ ```
263
+
264
+ ### 场景 3:手动触发(适用于"点击加载更多")
265
+
266
+ ```vue
267
+ <template>
268
+ <div v-for="item in data" :key="item.id">{{ item.title }}</div>
269
+ <button v-if="!isFinished" @click="loadMore" :disabled="loading">
270
+ {{ loading ? '加载中...' : '点击加载更多' }}
271
+ </button>
272
+ </template>
273
+ ```
274
+
275
+ ## 边界情况处理
276
+
277
+ | 场景 | 处理方式 |
278
+ |------|---------|
279
+ | 快速滚动到低部 | IntersectionObserver 自带节流,无需额外处理 |
280
+ | 组件卸载时请求未完成 | `stopLoading` 标志位丢弃回调结果,避免内存泄漏 |
281
+ | 请求失败 | 捕获异常,显示错误状态,提供重试按钮 |
282
+ | 接口返回空数组 | `isFinished` 置为 true,停止监听 |
283
+ | 搜索条件变化 | `refresh()` 重置全部状态并重新加载 |
284
+ | 上一次请求未完成时触发刷新 | `stopLoading` 中断旧请求 |
285
+ | 列表已有数据时 loading 态 | 区分首次加载(全屏 loading)和加载更多(底部 loading) |
286
+
287
+ ## 注意事项
288
+
289
+ 1. **分页从 1 开始**:绝大多数后端接口分页从 1 开始,按需调整 `page.value + 1` 的写法
290
+ 2. **key 绑定**:列表渲染务必绑定 `:key`,避免 Vue 的 diff 问题
291
+ 3. **容器 overflow**:如果容器内滚动,需要设置 `overflow-y: auto` 和固定高度
292
+ 4. **数据量过大**:列表超过 1000 条建议配合虚拟滚动,不要单纯依赖无限加载
@@ -0,0 +1,174 @@
1
+ # Pinia Store 封装方案
2
+
3
+ ## 概述
4
+
5
+ 基于 Pinia Setup Store(Composition API 风格)的状态管理方案。
6
+
7
+ 核心模式:
8
+ 1. **Setup Store 格局** — `defineStore` + Composition API,天然支持响应式
9
+ 2. **异步 action + loading 分离** — store 只管理数据,loading 状态交给 view 层的 `useLoading`
10
+ 3. **分页数据管理** — 首屏替换 / 翻页追加的通用模式
11
+
12
+ ## 目录结构
13
+
14
+ ```
15
+ src/stores/
16
+ home.ts -- 首页 store
17
+ city.ts -- 城市 store
18
+ ```
19
+
20
+ 每个文件对应一个业务域,与 service/modules 一一对应。
21
+
22
+ ## Setup Store 格局
23
+
24
+ ```ts
25
+ export const useHomeStore = defineStore('home', () => {
26
+ // --- 状态 ---
27
+ const xxx = ref<T>(...)
28
+
29
+ // --- 计算属性(可选) ---
30
+ const xxxName = computed(() => ...)
31
+
32
+ // --- 异步操作 ---
33
+ const fetchXxx = async () => {
34
+ const res = await getXxx()
35
+ xxx.value = res.data || []
36
+ }
37
+
38
+ // --- 同步操作 ---
39
+ const setXxx = (val: T) => { xxx.value = val }
40
+
41
+ return { xxx, xxxName, fetchXxx, setXxx }
42
+ })
43
+ ```
44
+
45
+ ### 命名规范
46
+
47
+ | 项目 | 规范 |
48
+ |------|------|
49
+ | Store 变量 | `use[Name]Store` |
50
+ | Store ID | 小写英文,与文件名一致 |
51
+ | 导出方式 | `export const`(统一使用命名导出) |
52
+ | 状态 | `ref()` 定义原始数据 |
53
+ | 派生 | `computed()` 定义计算属性 |
54
+ | 异步操作 | `fetch` / `load` 前缀 |
55
+ | 同步操作 | `set` / `select` 前缀,动词主导 |
56
+
57
+ ## 异步 Action 模式
58
+
59
+ ### 原则:Store 只管数据,不管 UI 状态
60
+
61
+ store 中的异步函数只负责获取数据、更新状态,不管理 loading/error:
62
+
63
+ ```ts
64
+ const fetchHotSuggests = async () => {
65
+ const res = await getHotSuggests()
66
+ hotSuggests.value = res.data || []
67
+ }
68
+ ```
69
+
70
+ View 层通过 `useLoading` 组合多个 action:
71
+
72
+ ```ts
73
+ // Home.vue
74
+ const { loading: initLoading, withLoading } = useLoading()
75
+
76
+ onMounted(() => {
77
+ withLoading(async () => {
78
+ await fetchHotSuggests()
79
+ await fetchCategories()
80
+ await fetchHouseList()
81
+ })
82
+ })
83
+ ```
84
+
85
+ 这样 store 保持纯数据逻辑,view 控制 UI 状态粒度(可以多个 action 共享一个 loading,也可以各自独立)。
86
+
87
+ ## 分页数据管理
88
+
89
+ ### 核心状态
90
+
91
+ ```ts
92
+ const list = ref<T[]>([]) // 列表数据
93
+ const currentPage = ref(1) // 当前页码
94
+ const hasMore = ref(true) // 是否还有更多
95
+ ```
96
+
97
+ ### 加载函数
98
+
99
+ ```ts
100
+ const fetchList = async (page: number = 1) => {
101
+ const res = await getList(page)
102
+ // 假设后端返回 { errcode: 0, data: [...] }
103
+ if (res.errcode === 0) {
104
+ if (page === 1) {
105
+ list.value = res.data || [] // 首页:替换
106
+ } else {
107
+ list.value.push(...(res.data || [])) // 翻页:追加
108
+ }
109
+ currentPage.value = page
110
+ hasMore.value = (res.data || []).length > 0
111
+ }
112
+ }
113
+ ```
114
+
115
+ ### View 层使用
116
+
117
+ ```vue
118
+ <script setup>
119
+ const { list, currentPage, hasMore } = storeToRefs(store)
120
+ const { fetchList } = store
121
+
122
+ // 首次加载
123
+ onMounted(() => fetchList())
124
+
125
+ // 加载更多
126
+ function onLoadMore() {
127
+ if (!hasMore.value) return
128
+ fetchList(currentPage.value + 1)
129
+ }
130
+ </script>
131
+
132
+ <template>
133
+ <div v-for="item in list" :key="item.id">{{ item.name }}</div>
134
+ <div v-if="hasMore" @click="onLoadMore">加载更多</div>
135
+ </template>
136
+ ```
137
+
138
+ ### 边界处理
139
+
140
+ | 场景 | 处理 |
141
+ |------|------|
142
+ | 空数据 | `res.data || []` 保底,避免 `.push(undefined)` |
143
+ | 已加载完毕 | `hasMore` 为 false 时拦截加载请求 |
144
+ | 搜索条件变化 | 重置 page = 1,重新 fetchList |
145
+ | 后端页码不从 1 开始 | 调整 `page === 1` 的判断逻辑 |
146
+
147
+ ## View 层消费 Store 的方式
148
+
149
+ ### 推荐:解构响应式状态 + 保留 action
150
+
151
+ ```ts
152
+ import { useHomeStore } from '@/stores/home'
153
+ import { storeToRefs } from 'pinia'
154
+
155
+ const store = useHomeStore()
156
+ // 响应式状态:必须用 storeToRefs 包裹,否则会丢失响应性
157
+ const { hotSuggests, houseList, hasMore } = storeToRefs(store)
158
+ // 非响应式方法/action:直接从 store 实例解构
159
+ const { fetchHotSuggests, fetchHouseList } = store
160
+ ```
161
+
162
+ ### storeToRefs 规则
163
+
164
+ | 类型 | 解构方式 | 原因 |
165
+ |------|---------|------|
166
+ | `ref` / `computed` | `storeToRefs(store)` | 保持响应性 |
167
+ | 函数(action) | `store.xxx` 或解构 | 本身就是普通函数,无响应式问题 |
168
+ | 普通值 | `store.xxx` | 非响应式属性 |
169
+
170
+ ## 注意事项
171
+
172
+ 1. **不要在 store 外直接修改 `storeToRefs` 的值**:`storeToRefs` 返回的是 ref,改 `xxx.value = ...` 会直接修改 store 状态。如果只是想取值不改值,用 `toRef(store, 'xxx')` 只读
173
+ 2. **避免 store 循环依赖**:Store A 调用 Store B 的 action 时,在 action 内部 `useXxxStore()`,不要在模块顶层调用
174
+ 3. **SSR 兼容**:`useStore()` 调用要在组件的 `setup` 或 `pinia` 实例已挂载后执行