mdk-skills 2.2.1 → 2.2.2
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.
- package/.claude/.install.log +22 -0
- package/.claude/backups/{20260510.145155 → 20260510.145547}/.install.log +8 -0
- package/.claude/backups/20260510.145547/skills/test1/.meta.json +6 -0
- package/.claude/backups/20260510.145547/skills/test2/.meta.json +6 -0
- package/.claude/backups/20260510.145651/.install.log +19 -0
- package/.claude/backups/{20260510.145155 → 20260510.145651}/settings.json +1 -7
- package/.claude/backups/20260510.145651/skills/test1/.meta.json +6 -0
- package/.claude/backups/20260510.145651/skills/test2/.meta.json +6 -0
- package/.claude/backups/20260510.150310/.install.log +30 -0
- package/.claude/backups/20260510.150310/profiles.json +67 -0
- package/.claude/backups/20260510.150310/settings.json +29 -0
- package/.claude/backups/20260510.150310/skills/frontend-code-review/.meta.json +6 -0
- package/.claude/backups/20260510.150310/skills/frontend-code-review/SKILL.md +167 -0
- package/.claude/backups/20260510.150310/skills/frontend-code-review/references/checklist.md +298 -0
- package/.claude/backups/20260510.150310/skills/frontend-design/.meta.json +6 -0
- package/.claude/backups/20260510.150310/skills/frontend-design/LICENSE.txt +177 -0
- package/.claude/backups/20260510.150310/skills/frontend-design/SKILL.md +42 -0
- package/.claude/backups/20260510.150310/skills/skill-creator/.meta.json +6 -0
- package/.claude/backups/20260510.150310/skills/skill-creator/SKILL.md +356 -0
- package/.claude/backups/20260510.150310/skills/skill-creator/references/output-patterns.md +82 -0
- package/.claude/backups/20260510.150310/skills/skill-creator/references/workflows.md +28 -0
- package/.claude/backups/20260510.150310/skills/skill-creator/scripts/init_skill.py +303 -0
- package/.claude/backups/20260510.150310/skills/skill-creator/scripts/package_skill.py +110 -0
- package/.claude/backups/20260510.150310/skills/skill-creator/scripts/quick_validate.py +95 -0
- package/.claude/backups/20260510.150310/skills/test1/.meta.json +6 -0
- package/.claude/backups/20260510.150310/skills/test2/.meta.json +6 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/.meta.json +6 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/SKILL.md +228 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/data/prompts.csv +24 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/data/styles.csv +59 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/scripts/core.py +238 -0
- package/.claude/backups/20260510.150310/skills/ui-ux-pro-max/scripts/search.py +61 -0
- package/.claude/backups/20260510.150310/skills/v3-fe-biz-patterns/.meta.json +6 -0
- package/.claude/backups/20260510.150310/skills/v3-fe-biz-patterns/SKILL.md +26 -0
- package/.claude/backups/20260510.150310/skills/v3-fe-biz-patterns/references/infinite-scroll.md +292 -0
- package/.claude/backups/20260510.150310/skills/v3-fe-biz-patterns/references/pinia-store.md +174 -0
- package/.claude/backups/20260510.150310/skills/v3-fe-biz-patterns/references/service-layer.md +198 -0
- package/.claude/backups/20260510.150310/skills/v3-fe-biz-patterns/references/tab-anchor.md +1125 -0
- package/.claude/backups/20260510.150310/skills/v3-fe-biz-patterns/references/use-loading.md +114 -0
- package/.claude/backups/20260510.150310/skills/vue/.meta.json +6 -0
- package/.claude/backups/20260510.150310/skills/vue/SKILL.md +103 -0
- package/.claude/backups/20260510.150310/skills/vue/references/components.md +323 -0
- package/.claude/backups/20260510.150310/skills/vue/references/composables.md +358 -0
- package/.claude/backups/20260510.150310/skills/vue/references/directives.md +225 -0
- package/.claude/backups/20260510.150310/skills/vue/references/gotchas.md +438 -0
- package/.claude/backups/20260510.150310/skills/vue/references/provide-inject.md +174 -0
- package/.claude/backups/20260510.150310/skills/vue/references/reactivity.md +289 -0
- package/.claude/backups/20260510.150310/skills/vue/references/router.md +181 -0
- package/.claude/backups/20260510.150310/skills/vue/references/testing.md +294 -0
- package/.claude/backups/20260510.150310/skills/vue/references/typescript.md +172 -0
- package/.claude/backups/20260510.150310/skills/vue/references/utils-client.md +156 -0
- package/.claude/backups/CLAUDE.md.20260510.145651 +131 -0
- package/.claude/backups/CLAUDE.md.20260510.150310 +131 -0
- package/.claude/settings.json +1 -7
- package/.claude/skills/test1/.meta.json +6 -0
- package/.claude/skills/test2/.meta.json +6 -0
- package/package.json +1 -1
- package/scripts/cli.js +1 -1
- package/scripts/web-ui/server.js +2 -1
- package/scripts/web-ui/src/api/skills.js +5 -1
- package/scripts/web-ui/src/views/Dashboard.vue +12 -1
- package/.claude/backups/20260510.144501/.install.log +0 -1
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/profiles.json +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/settings.json +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/frontend-code-review/.meta.json +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/frontend-code-review/SKILL.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/frontend-code-review/references/checklist.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/frontend-design/.meta.json +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/frontend-design/LICENSE.txt +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/frontend-design/SKILL.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/skill-creator/.meta.json +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/skill-creator/SKILL.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/skill-creator/references/output-patterns.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/skill-creator/references/workflows.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/skill-creator/scripts/init_skill.py +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/skill-creator/scripts/package_skill.py +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/skill-creator/scripts/quick_validate.py +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/.meta.json +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/SKILL.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/data/charts.csv +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/data/colors.csv +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/data/landing.csv +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/data/products.csv +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/data/prompts.csv +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/data/stacks/flutter.csv +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/data/stacks/nextjs.csv +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/data/stacks/react-native.csv +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/data/stacks/react.csv +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/data/stacks/svelte.csv +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/data/stacks/swiftui.csv +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/data/stacks/vue.csv +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/data/styles.csv +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/data/typography.csv +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/data/ux-guidelines.csv +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/scripts/core.py +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/ui-ux-pro-max/scripts/search.py +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/v3-fe-biz-patterns/.meta.json +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/v3-fe-biz-patterns/SKILL.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/v3-fe-biz-patterns/references/infinite-scroll.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/v3-fe-biz-patterns/references/pinia-store.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/v3-fe-biz-patterns/references/service-layer.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/v3-fe-biz-patterns/references/tab-anchor.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/v3-fe-biz-patterns/references/use-loading.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/vue/.meta.json +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/vue/SKILL.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/vue/references/components.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/vue/references/composables.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/vue/references/directives.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/vue/references/gotchas.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/vue/references/provide-inject.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/vue/references/reactivity.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/vue/references/router.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/vue/references/testing.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/vue/references/typescript.md +0 -0
- /package/.claude/backups/{20260510.144501 → 20260510.145547}/skills/vue/references/utils-client.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/profiles.json +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/frontend-code-review/.meta.json +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/frontend-code-review/SKILL.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/frontend-code-review/references/checklist.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/frontend-design/.meta.json +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/frontend-design/LICENSE.txt +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/frontend-design/SKILL.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/skill-creator/.meta.json +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/skill-creator/SKILL.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/skill-creator/references/output-patterns.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/skill-creator/references/workflows.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/skill-creator/scripts/init_skill.py +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/skill-creator/scripts/package_skill.py +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/skill-creator/scripts/quick_validate.py +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/.meta.json +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/SKILL.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/data/charts.csv +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/data/colors.csv +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/data/landing.csv +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/data/products.csv +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/data/prompts.csv +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/data/stacks/flutter.csv +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/data/stacks/nextjs.csv +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/data/stacks/react-native.csv +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/data/stacks/react.csv +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/data/stacks/svelte.csv +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/data/stacks/swiftui.csv +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/data/stacks/vue.csv +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/data/styles.csv +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/data/typography.csv +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/data/ux-guidelines.csv +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/scripts/core.py +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/ui-ux-pro-max/scripts/search.py +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/v3-fe-biz-patterns/.meta.json +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/v3-fe-biz-patterns/SKILL.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/v3-fe-biz-patterns/references/infinite-scroll.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/v3-fe-biz-patterns/references/pinia-store.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/v3-fe-biz-patterns/references/service-layer.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/v3-fe-biz-patterns/references/tab-anchor.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/v3-fe-biz-patterns/references/use-loading.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/vue/.meta.json +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/vue/SKILL.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/vue/references/components.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/vue/references/composables.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/vue/references/directives.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/vue/references/gotchas.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/vue/references/provide-inject.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/vue/references/reactivity.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/vue/references/router.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/vue/references/testing.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/vue/references/typescript.md +0 -0
- /package/.claude/backups/{20260510.145155 → 20260510.145651}/skills/vue/references/utils-client.md +0 -0
package/.claude/backups/20260510.150310/skills/v3-fe-biz-patterns/references/infinite-scroll.md
ADDED
|
@@ -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` 实例已挂载后执行
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Service 层封装方案
|
|
2
|
+
|
|
3
|
+
## 概述
|
|
4
|
+
|
|
5
|
+
基于 axios 的请求层封装,核心模式:
|
|
6
|
+
|
|
7
|
+
1. **Request 类 + 单例** — 封装 axios 实例,统一配置和拦截器
|
|
8
|
+
2. **按业务域拆分模块** — `modules/[domain].ts`,类型与函数共置
|
|
9
|
+
3. **config 集中管理** — baseURL、超时、状态码映射统一维护
|
|
10
|
+
|
|
11
|
+
## 目录结构
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
src/service/
|
|
15
|
+
index.ts -- 统一导出
|
|
16
|
+
request/
|
|
17
|
+
config.ts -- 配置集中管理
|
|
18
|
+
index.ts -- Request 类(axios 封装)
|
|
19
|
+
modules/
|
|
20
|
+
home.ts -- 首页 API
|
|
21
|
+
city.ts -- 城市 API
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Request 类封装
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
class Request {
|
|
28
|
+
private instance: AxiosInstance
|
|
29
|
+
|
|
30
|
+
constructor() {
|
|
31
|
+
this.instance = axios.create({
|
|
32
|
+
baseURL: config.baseURL,
|
|
33
|
+
timeout: config.timeout,
|
|
34
|
+
headers: config.headers,
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public get<T>(url, params?, options?): Promise<T>
|
|
39
|
+
public post<T>(url, data?, options?): Promise<T>
|
|
40
|
+
public put<T>(url, data?, options?): Promise<T>
|
|
41
|
+
public delete<T>(url, params?, options?): Promise<T>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const request = new Request()
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
关键设计点:
|
|
48
|
+
|
|
49
|
+
- **单例模式**:整个应用共享一个 Request 实例,避免重复创建
|
|
50
|
+
- **泛型方法**:`get<T>` 返回 `Promise<T>`,调用方通过类型参数控制返回值类型
|
|
51
|
+
- **RequestOptions**:扩展 `AxiosRequestConfig`,预留 `showLoading`、`showError` 等业务字段
|
|
52
|
+
|
|
53
|
+
## 拦截器(可选启用)
|
|
54
|
+
|
|
55
|
+
拦截器默认不激活,需要时显式调用 `request.enableInterceptors()`:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
class Request {
|
|
59
|
+
/** 启用拦截器:token 注入 + 统一错误处理 */
|
|
60
|
+
enableInterceptors() {
|
|
61
|
+
// 请求拦截器:自动注入 token
|
|
62
|
+
this.instance.interceptors.request.use((config) => {
|
|
63
|
+
const token = localStorage.getItem('token')
|
|
64
|
+
if (token) {
|
|
65
|
+
config.headers.Authorization = `Bearer ${token}`
|
|
66
|
+
}
|
|
67
|
+
return config
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// 响应拦截器:解包数据 + 统一错误提示
|
|
71
|
+
this.instance.interceptors.response.use(
|
|
72
|
+
(response) => {
|
|
73
|
+
const { code, message, data } = response.data
|
|
74
|
+
if (code === 0) return data // 成功:直接返回业务数据
|
|
75
|
+
console.error('请求失败:', message)
|
|
76
|
+
return Promise.reject(new Error(message))
|
|
77
|
+
},
|
|
78
|
+
(error) => {
|
|
79
|
+
if (error.response) {
|
|
80
|
+
console.error(statusCodeMap[error.response.status] || '网络请求失败')
|
|
81
|
+
} else {
|
|
82
|
+
console.error('网络连接失败')
|
|
83
|
+
}
|
|
84
|
+
return Promise.reject(error)
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
启用后,API 函数的写法会简化——不再需要手动解 `res.data`,因为响应拦截器已经解了一层。
|
|
92
|
+
|
|
93
|
+
## Config 集中管理
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// src/service/request/config.ts
|
|
97
|
+
export interface RequestConfig {
|
|
98
|
+
baseURL: string
|
|
99
|
+
timeout: number
|
|
100
|
+
headers?: Record<string, string>
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const config: RequestConfig = {
|
|
104
|
+
baseURL: 'http://xxx/api',
|
|
105
|
+
timeout: 10000,
|
|
106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const statusCodeMap: Record<number, string> = {
|
|
110
|
+
400: '请求参数错误',
|
|
111
|
+
401: '未授权,请登录',
|
|
112
|
+
403: '拒绝访问',
|
|
113
|
+
404: '请求地址不存在',
|
|
114
|
+
500: '服务器内部错误',
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
多 API 源时,在 config 中添加对应配置对象,Request 类中用单独 instance 隔离:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
export const mapConfig = {
|
|
122
|
+
baseURL: '/api/map',
|
|
123
|
+
key: 'xxx',
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// request/index.ts
|
|
127
|
+
class Request {
|
|
128
|
+
private mapInstance: AxiosInstance | null = null
|
|
129
|
+
|
|
130
|
+
public mapGet<T>(url, params?): Promise<T> {
|
|
131
|
+
if (!this.mapInstance) {
|
|
132
|
+
this.mapInstance = axios.create({ baseURL: mapConfig.baseURL, timeout: config.timeout })
|
|
133
|
+
}
|
|
134
|
+
return this.mapInstance.get(url, { params })
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## API 模块组织
|
|
140
|
+
|
|
141
|
+
### 按业务域拆分
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// src/service/modules/home.ts
|
|
145
|
+
import { request } from '../request'
|
|
146
|
+
|
|
147
|
+
// 类型定义与 API 函数共置
|
|
148
|
+
export interface HouseListItem { /* ... */ }
|
|
149
|
+
|
|
150
|
+
export const getHotSuggests = async () => {
|
|
151
|
+
const res = await request.get('/home/hotSuggests')
|
|
152
|
+
return res.data
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const getHouseList = async (page = 1) => {
|
|
156
|
+
const res = await request.get('/home/houselist', { page })
|
|
157
|
+
return res.data
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 规则
|
|
162
|
+
|
|
163
|
+
| 原则 | 说明 |
|
|
164
|
+
|------|------|
|
|
165
|
+
| 一域一文件 | 每个业务模块一个文件,命名与后端资源对应 |
|
|
166
|
+
| 类型共置 | 接口响应类型定义在 API 函数同一文件,就近维护 |
|
|
167
|
+
| 函数即接口 | 每个 API 导出一个 async 函数,参数即业务参数 |
|
|
168
|
+
| 统一导出 | `service/index.ts` 只导出 request 实例和核心类型,modules 由消费方按需 import |
|
|
169
|
+
|
|
170
|
+
## View 层消费方式
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { getHotSuggests } from '@/service/modules/home'
|
|
174
|
+
|
|
175
|
+
onMounted(async () => {
|
|
176
|
+
const res = await getHotSuggests()
|
|
177
|
+
hotSuggests.value = res.data || []
|
|
178
|
+
})
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
或通过 store 间接调用:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
// store 中调用 service
|
|
185
|
+
const fetchHotSuggests = async () => {
|
|
186
|
+
const res = await getHotSuggests()
|
|
187
|
+
hotSuggests.value = res.data || []
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// view 中调用 store action
|
|
191
|
+
onMounted(() => store.fetchHotSuggests())
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## 注意事项
|
|
195
|
+
|
|
196
|
+
1. **拦截器启用时机**:`enableInterceptors()` 应在应用初始化时(如 `main.ts`)调用一次,避免重复注册
|
|
197
|
+
2. **响应拦截器副作用**:启用后 API 函数返回的是解包后的业务数据,而非完整 AxiosResponse,与之配套的 store 逻辑需要同步调整
|
|
198
|
+
3. **泛型参数**:`request.get<T>` 的 `T` 只是类型标注,不会在运行时做校验,后端返回格式异常仍需前端容错
|