create-dwy 0.7.1 → 0.9.0

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 (96) hide show
  1. package/package.json +1 -1
  2. package/src/create.js +50 -3
  3. package/src/sync.js +228 -130
  4. package/src/utils.js +4 -0
  5. package/templates/claude-global/CLAUDE.md +81 -6
  6. package/templates/claude-global/rules/dwy-postgres.md +89 -0
  7. package/templates/claude-global/rules/dwy-python-backend.md +492 -0
  8. package/templates/claude-global/rules/dwy-python-core.md +94 -634
  9. package/templates/claude-global/rules/dwy-python-testing.md +115 -13
  10. package/templates/claude-global/rules/dwy-redis.md +88 -0
  11. package/templates/claude-global/rules/dwy-vue-core.md +341 -645
  12. package/templates/claude-global/rules/dwy-vue-pinia.md +47 -34
  13. package/templates/claude-global/rules/dwy-vue-testing.md +120 -55
  14. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +1 -1
  15. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +1 -1
  16. package/templates/claude-global/skills/dwy-dolphindb/SKILL.md +22 -15
  17. package/templates/claude-global/skills/dwy-dolphindb/evals/evals.json +23 -0
  18. package/templates/claude-global/skills/dwy-dolphindb/references/dolphindb-rules.md +100 -1
  19. package/templates/claude-global/skills/dwy-ekit/SKILL.md +142 -276
  20. package/templates/claude-global/skills/dwy-eui/SKILL.md +264 -182
  21. package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +117 -129
  22. package/templates/claude-global/skills/dwy-git-commit/SKILL.md +125 -0
  23. package/templates/claude-global/skills/dwy-publish/SKILL.md +1 -1
  24. package/templates/claude-global/{rules/dwy-tech-stack.md → skills/dwy-tech-stack/SKILL.md} +4 -3
  25. package/templates/project/fullstack-monorepo/CLAUDE.md.hbs +81 -0
  26. package/templates/project/fullstack-monorepo/backend/.python-version +1 -0
  27. package/templates/project/fullstack-monorepo/backend/CLAUDE.md.hbs +68 -0
  28. package/templates/project/fullstack-monorepo/backend/Dockerfile.dev +23 -0
  29. package/templates/project/fullstack-monorepo/backend/alembic/env.py.hbs +68 -0
  30. package/templates/project/fullstack-monorepo/backend/alembic/script.py.mako +26 -0
  31. package/templates/project/fullstack-monorepo/backend/alembic.ini.hbs +43 -0
  32. package/templates/project/fullstack-monorepo/backend/packages/dwy-shared/pyproject.toml +16 -0
  33. package/templates/project/fullstack-monorepo/backend/packages/dwy-shared/src/dwy_shared/__init__.py +12 -0
  34. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/pyproject.toml.hbs +20 -0
  35. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/__init__.py.hbs +40 -0
  36. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/config.py +9 -0
  37. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/models.py.hbs +17 -0
  38. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/protocols.py +14 -0
  39. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/router.py.hbs +22 -0
  40. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/schemas.py +21 -0
  41. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/service.py +11 -0
  42. package/templates/project/fullstack-monorepo/backend/pyproject.toml.hbs +84 -0
  43. package/templates/project/fullstack-monorepo/backend/src/app/__init__.py +3 -0
  44. package/templates/project/fullstack-monorepo/backend/src/app/config.py.hbs +21 -0
  45. package/templates/project/fullstack-monorepo/backend/src/app/exceptions.py +26 -0
  46. package/templates/project/fullstack-monorepo/backend/src/app/factories.py +16 -0
  47. package/templates/project/fullstack-monorepo/backend/src/app/infra/__init__.py +1 -0
  48. package/templates/project/fullstack-monorepo/backend/src/app/main.py.hbs +65 -0
  49. package/templates/project/fullstack-monorepo/backend/src/app/middleware/__init__.py +1 -0
  50. package/templates/project/fullstack-monorepo/backend/tests/__init__.py +0 -0
  51. package/templates/project/fullstack-monorepo/backend/tests/conftest.py.hbs +42 -0
  52. package/templates/project/fullstack-monorepo/dev.sh.hbs +83 -0
  53. package/templates/project/fullstack-monorepo/docker-compose.dev.yml.hbs +36 -0
  54. package/templates/project/fullstack-monorepo/frontend/CLAUDE.md.hbs +35 -0
  55. package/templates/project/fullstack-monorepo/frontend/Dockerfile +18 -0
  56. package/templates/project/fullstack-monorepo/frontend/apps/web/index.html.hbs +13 -0
  57. package/templates/project/fullstack-monorepo/frontend/apps/web/package.json.hbs +31 -0
  58. package/templates/project/fullstack-monorepo/frontend/apps/web/src/App.vue.hbs +10 -0
  59. package/templates/project/fullstack-monorepo/frontend/apps/web/src/api/client.ts.hbs +11 -0
  60. package/templates/project/fullstack-monorepo/frontend/apps/web/src/features/{{initialDomain}}/.gitkeep +0 -0
  61. package/templates/project/fullstack-monorepo/frontend/apps/web/src/index.css +12 -0
  62. package/templates/project/fullstack-monorepo/frontend/apps/web/src/main.ts +17 -0
  63. package/templates/project/fullstack-monorepo/frontend/apps/web/src/router/index.ts.hbs +24 -0
  64. package/templates/project/fullstack-monorepo/frontend/apps/web/src/stores/.gitkeep +0 -0
  65. package/templates/project/fullstack-monorepo/frontend/apps/web/src/types/.gitkeep +0 -0
  66. package/templates/project/fullstack-monorepo/frontend/apps/web/src/views/HomeView.vue.hbs +36 -0
  67. package/templates/project/fullstack-monorepo/frontend/apps/web/tsconfig.json +7 -0
  68. package/templates/project/fullstack-monorepo/frontend/apps/web/vite.config.ts.hbs +27 -0
  69. package/templates/project/fullstack-monorepo/frontend/nginx.conf +11 -0
  70. package/templates/project/fullstack-monorepo/frontend/package.json.hbs +13 -0
  71. package/templates/project/fullstack-monorepo/frontend/packages/core/package.json.hbs +15 -0
  72. package/templates/project/fullstack-monorepo/frontend/packages/core/src/index.ts +3 -0
  73. package/templates/project/fullstack-monorepo/frontend/packages/features/{{initialDomain}}/package.json.hbs +14 -0
  74. package/templates/project/fullstack-monorepo/frontend/packages/features/{{initialDomain}}/src/index.ts.hbs +16 -0
  75. package/templates/project/fullstack-monorepo/frontend/packages/features/{{initialDomain}}/src/pages/IndexPage.vue.hbs +9 -0
  76. package/templates/project/fullstack-monorepo/frontend/packages/ui/package.json.hbs +15 -0
  77. package/templates/project/fullstack-monorepo/frontend/packages/ui/src/index.ts +3 -0
  78. package/templates/project/fullstack-monorepo/frontend/pnpm-workspace.yaml.hbs +23 -0
  79. package/templates/project/fullstack-monorepo/frontend/tsconfig.base.json.hbs +23 -0
  80. package/templates/claude-global/rules/dwy-api-design.md +0 -110
  81. package/templates/claude-global/rules/dwy-git-commit-convention.md +0 -82
  82. package/templates/claude-global/rules/dwy-git-security.md +0 -53
  83. package/templates/claude-global/rules/dwy-payload-limits.md +0 -186
  84. package/templates/claude-global/rules/dwy-prefer-open-source.md +0 -32
  85. package/templates/claude-global/rules/dwy-project-structure.md +0 -181
  86. package/templates/claude-global/rules/dwy-python-fastapi.md +0 -272
  87. package/templates/claude-global/rules/dwy-python-orm.md +0 -61
  88. package/templates/claude-global/rules/dwy-python-security.md +0 -636
  89. package/templates/claude-global/rules/dwy-server-security.md +0 -518
  90. package/templates/claude-global/rules/dwy-vue-api.md +0 -146
  91. package/templates/claude-global/skills/dwy-ekit/references/data.md +0 -135
  92. package/templates/claude-global/skills/dwy-ekit/references/form.md +0 -68
  93. package/templates/claude-global/skills/dwy-ekit/references/request.md +0 -183
  94. package/templates/claude-global/skills/dwy-ekit/references/ui.md +0 -109
  95. package/templates/claude-global/skills/dwy-vue-core/SKILL.md +0 -890
  96. /package/templates/{claude-global/commands → project/fullstack-monorepo/backend/alembic/versions}/.gitkeep +0 -0
@@ -1,135 +0,0 @@
1
- # 数据与查询字符串(storage / cookie / date / qs)
2
-
3
- > 何时读这份:当用户使用本模块的 API 或问法涉及本模块功能时读取(localStorage 读写、Cookie 读写、日期/时间/时区格式化、查询字符串序列化/解析)。
4
-
5
- ## storage — localStorage
6
-
7
- ```ts
8
- import { useStorage, storage } from '@dwydev/ekit'
9
- ```
10
-
11
- ### useStorage(key, defaultValue) — 直接再导出 @vueuse/core
12
-
13
- 跨标签页同步、SSR-safe、自动 JSON 序列化。**API 与 VueUse 一致**(不再是 ekit 自写)。
14
-
15
- ```ts
16
- const token = useStorage<string>('token', '')
17
- const config = useStorage('config', { theme: 'light' }, localStorage, { mergeDefaults: true })
18
- ```
19
-
20
- ### storage — 静态对象(非组件场景)
21
-
22
- | 方法 | 签名 |
23
- |------|------|
24
- | `get<T>(key, defaultValue?)` | 返回 `T \| undefined`,JSON 解析失败按字符串返回 |
25
- | `set(key, value)` | 自动 `JSON.stringify` |
26
- | `remove(key)` | 删除 |
27
- | `clear()` | 清空全部(慎用) |
28
-
29
- ---
30
-
31
- ## cookie — Cookie 读写
32
-
33
- ```ts
34
- import { useCookie, cookie } from '@dwydev/ekit'
35
- import type { CookieOptions } from '@dwydev/ekit' // 旧名 CookieAttributes 已废弃
36
- ```
37
-
38
- ### CookieOptions
39
-
40
- ```ts
41
- interface CookieOptions {
42
- expires?: number | Date // 天数或 Date;不传 = 会话级
43
- path?: string // 默认 '/'
44
- domain?: string
45
- secure?: boolean // 仅 HTTPS
46
- sameSite?: 'strict' | 'lax' | 'none'
47
- }
48
- ```
49
-
50
- ### cookie(静态)
51
-
52
- ```ts
53
- cookie.get<User>('user')
54
- cookie.set('user', { id: 1, name: 'Alice' }, { expires: 7, secure: true })
55
- cookie.remove('user', { path: '/' }) // 删除时 path/domain 必须与写入一致
56
- ```
57
-
58
- ### useCookie(key, defaultValue?) → Ref
59
-
60
- ```ts
61
- const lang = useCookie('lang', 'zh-CN')
62
- lang.value = 'en-US' // 自动写入 cookie
63
- lang.value = undefined // 自动删除
64
- ```
65
-
66
- ---
67
-
68
- ## date — 日期 / 时间 / 时区
69
-
70
- ```ts
71
- import {
72
- now, formatTimestamp, formatInTimezone,
73
- formatDate, formatDateTime, formatTime, formatRelativeTime, formatBy,
74
- } from '@dwydev/ekit'
75
- ```
76
-
77
- 底层是 dayjs(含 utc + timezone + relativeTime + zh-cn locale 插件),**不导出 dayjs 实例**。所有函数对外用 `string | number | Date`。
78
-
79
- ### 时间戳工具(v0.6.0 新增)
80
-
81
- | 函数 | 签名 | 说明 |
82
- |------|------|------|
83
- | `now()` | `(): number` | 当前 UTC Unix 毫秒戳,等价于 `Date.now()` |
84
- | `formatTimestamp(ts, fmt?)` | `(ts: number, fmt?: string): string` | 把毫秒戳格式化为本地时区字符串。默认 `'YYYY-MM-DD HH:mm:ss'` |
85
- | `formatInTimezone(tz, ts?, fmt?)` | `(tz: string, ts?: number, fmt?: string): string` | 在指定 IANA 时区下格式化(如 `'Asia/Shanghai'` / `'America/New_York'` / `'UTC'`),ts 不传用当前时间 |
86
-
87
- ```ts
88
- const ts = now() // 1745318445000
89
- formatTimestamp(ts) // '2026-04-22 18:40:45'
90
- formatTimestamp(ts, 'YYYY/MM/DD') // '2026/04/22'
91
- formatInTimezone('America/New_York', ts) // '2026-04-22 06:40:45'
92
- formatInTimezone('UTC', ts, 'HH:mm') // '10:40'
93
- ```
94
-
95
- ### 普通格式化
96
-
97
- | 函数 | 输出 | 输入 |
98
- |------|------|------|
99
- | `formatDate(input)` | `YYYY-MM-DD` | `string \| number \| Date` |
100
- | `formatDateTime(input)` | `YYYY-MM-DD HH:mm:ss` | 同上 |
101
- | `formatTime(input)` | `HH:mm` | 同上 |
102
- | `formatRelativeTime(input)` | `刚刚` / `N 分钟前` / `N 小时前` / `N 天前`;> 30 天退化为 `YYYY-MM-DD` | 同上 |
103
- | `formatBy(input, template)` | 自定义 dayjs 模板 | `formatBy(ts, 'YYYY年MM月DD日')` → `2026年04月22日` |
104
-
105
- ---
106
-
107
- ## qs — 查询字符串
108
-
109
- ```ts
110
- import { stringify, parse } from '@dwydev/ekit'
111
- import type { StringifyOptions, ParseOptions } from '@dwydev/ekit' // 旧名 IStringifyOptions / IParseOptions 已废弃
112
- ```
113
-
114
- ```ts
115
- stringify({ ids: [1, 2], name: 'a' }) // 'ids=1&ids=2&name=a' 默认 arrayFormat: 'repeat'
116
- stringify({ ids: [1, 2] }, { arrayFormat: 'brackets' }) // 'ids[]=1&ids[]=2'
117
- stringify({ a: 1 }, { addQueryPrefix: true }) // '?a=1'
118
-
119
- parse('?a=1&b=2') // { a: '1', b: '2' } 默认 ignoreQueryPrefix: true
120
- parse('a.b=1', { allowDots: true }) // { a: { b: '1' } }
121
- ```
122
-
123
- ### StringifyOptions / ParseOptions
124
-
125
- ```ts
126
- interface StringifyOptions {
127
- arrayFormat?: 'repeat' | 'brackets' | 'indices' | 'comma'
128
- addQueryPrefix?: boolean
129
- skipNulls?: boolean
130
- }
131
- interface ParseOptions {
132
- ignoreQueryPrefix?: boolean
133
- allowDots?: boolean
134
- }
135
- ```
@@ -1,68 +0,0 @@
1
- # 表单与脱敏(validators / masking)
2
-
3
- > 何时读这份:当用户使用本模块的 API 或问法涉及本模块功能时读取(表单校验、zod schema、vee-validate 集成、PII 数据脱敏)。
4
-
5
- ## validators — 表单校验
6
-
7
- ```ts
8
- import {
9
- isPhone, isEmail, isIdCard, isUrl, isRequired, minLength, maxLength,
10
- // 也可用 zod schema 配合 vee-validate
11
- phoneSchema, emailSchema, idCardSchema, urlSchema,
12
- requiredSchema, minLengthSchema, maxLengthSchema,
13
- } from '@dwydev/ekit'
14
- ```
15
-
16
- ### 布尔校验函数
17
-
18
- | 函数 | 签名 | 规则 |
19
- |------|------|------|
20
- | `isPhone(value)` | `string => boolean` | 中国手机号 1[3-9] 开头 11 位 |
21
- | `isEmail(value)` | `string => boolean` | 基础邮箱 |
22
- | `isIdCard(value)` | `string => boolean` | 18 位身份证(末位允许 X/x) |
23
- | `isUrl(value)` | `string => boolean` | 合法 URL |
24
- | `isRequired(value)` | `any => boolean` | 非空(0 / false 视为有效) |
25
- | `minLength(value, n)` | `(string, number) => boolean` | 最小长度 |
26
- | `maxLength(value, n)` | `(string, number) => boolean` | 最大长度 |
27
-
28
- ### Zod schema(配合 vee-validate)
29
-
30
- ```ts
31
- import { useForm } from 'vee-validate'
32
- import { toTypedSchema } from '@vee-validate/zod'
33
- import { z } from 'zod'
34
- import { phoneSchema, emailSchema } from '@dwydev/ekit'
35
-
36
- const schema = toTypedSchema(z.object({
37
- phone: phoneSchema,
38
- email: emailSchema,
39
- }))
40
- const { defineField, errors } = useForm({ validationSchema: schema })
41
- ```
42
-
43
- > `minLengthSchema(n)` / `maxLengthSchema(n)` 是工厂函数,调用后返回 schema。
44
-
45
- ---
46
-
47
- ## masking — PII 脱敏
48
-
49
- ```ts
50
- import {
51
- maskPhone, maskEmail, maskIdCard, maskBankCard,
52
- maskName, maskAddress, maskIp, maskLicensePlate, maskText,
53
- } from '@dwydev/ekit'
54
- ```
55
-
56
- 纯函数,无外部依赖。**输入不符合格式时原样返回,不抛异常**。
57
-
58
- | 函数 | 规则 | 示例 |
59
- |------|------|------|
60
- | `maskPhone(phone)` | 前 3 后 4 | `'138****5678'` |
61
- | `maskEmail(email)` | 首字符 + `***` + @域名 | `'z***@gmail.com'` |
62
- | `maskIdCard(id)` | 前 3 后 4(18 位) | `'420***********1234'` |
63
- | `maskBankCard(card)` | 前 4 后 4(13~19 位) | `'6222********1234'` |
64
- | `maskName(name)` | 2 字 → 姓+`*`;3+ 字 → 姓+`*`×(n-2)+末字 | `'张*'` / `'张*明'` |
65
- | `maskAddress(addr)` | 保留省/市/区/县/镇/旗前缀 + `****` | `'浙江省杭州市西湖区****'` |
66
- | `maskIp(ip)` | IPv4 末段替换 `*` | `'192.168.1.*'` |
67
- | `maskLicensePlate(plate)` | 前 2 后 1,中间 `***` | `'浙A***8'` |
68
- | `maskText(text, start=1, end=1, ch='*')` | 通用:保留首尾 N 位 | `maskText('1234567890', 2, 3)` → `'12*****890'` |
@@ -1,183 +0,0 @@
1
- # request — HTTP 客户端(防腐层)
2
-
3
- > 何时读这份:当用户使用本模块的 API 或问法涉及本模块功能时读取(HTTP 请求、createRequest、内置插件、refreshToken、dwyeapi 响应契约对接、业务错误捕获)。
4
-
5
- ```ts
6
- import {
7
- createRequest, tokenPlugin, headerPlugin, unwrapPlugin, refreshTokenPlugin,
8
- SUCCESS_CODE, extractValidationErrors, isApiBusinessError,
9
- } from '@dwydev/ekit'
10
- import type {
11
- HttpClient, HttpConfig, HttpResponse, HttpError, HttpPlugin,
12
- HttpMethod, HttpResponseType, CreateRequestOptions,
13
- ApiResponse, PageData, ValidationErrorData, ValidationFieldError,
14
- CommonBusinessCode, BusinessCode, ApiBusinessError,
15
- } from '@dwydev/ekit'
16
- ```
17
-
18
- ## createRequest(options?) → HttpClient
19
-
20
- 底层是 axios,但**返回 `HttpClient` 而不是 `AxiosInstance`**。
21
-
22
- ```ts
23
- const http: HttpClient = createRequest({
24
- baseURL: '/api', // 默认 '/api'
25
- timeout: 30000, // 默认 30000
26
- headers: { 'X-Client': 'web' }, // 共享默认请求头
27
- plugins: [
28
- tokenPlugin({ getToken: () => localStorage.getItem('token') }),
29
- unwrapPlugin(),
30
- ],
31
- })
32
-
33
- // HttpClient 接口
34
- const res = await http.get<User>('/users/1') // res: HttpResponse<User>
35
- const res = await http.post<User>('/users', { ... })
36
- const res = await http.request({ method: 'PUT', url: '/users/1', data })
37
- // 还有 put / delete / patch / head
38
- ```
39
-
40
- ## HttpConfig / HttpResponse / HttpError
41
-
42
- ```ts
43
- interface HttpConfig {
44
- url?: string
45
- method?: HttpMethod // 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'
46
- baseURL?: string
47
- headers?: Record<string, string>
48
- params?: Record<string, any>
49
- data?: any
50
- timeout?: number
51
- responseType?: HttpResponseType // 'json' | 'blob' | 'text' | 'arraybuffer'
52
- extra?: Record<string, unknown> // 逃生舱:透传 axios 特有配置
53
- }
54
-
55
- interface HttpResponse<T = any> {
56
- data: T
57
- status: number
58
- statusText: string
59
- headers: Record<string, string>
60
- config: HttpConfig
61
- }
62
-
63
- interface HttpError<T = any> extends Error {
64
- config?: HttpConfig
65
- response?: HttpResponse<T>
66
- code?: string
67
- businessCode?: string // v0.7.0 新增:后端业务错误码(如 "NOT_FOUND")
68
- apiResponse?: ApiResponse<unknown> // v0.7.0 新增:原始 dwyeapi 响应信封
69
- }
70
- ```
71
-
72
- ## HttpPlugin 接口
73
-
74
- ```ts
75
- interface HttpPlugin {
76
- onRequest?: (config: HttpConfig) => HttpConfig | Promise<HttpConfig>
77
- onResponse?: (response: HttpResponse) => HttpResponse | Promise<HttpResponse>
78
- // 返回非 undefined 视为已恢复,终止后续插件
79
- onResponseError?: (error: HttpError) => any
80
- }
81
- ```
82
-
83
- > 旧版的 `RequestPlugin` 已重命名为 `HttpPlugin`,所有钩子参数类型从 axios 类型改为 ekit 自有契约。
84
-
85
- ## 内置插件
86
-
87
- | 插件 | 用途 | 参数 |
88
- |------|------|------|
89
- | `tokenPlugin` | 注入 `Authorization: Bearer <token>` | `{ getToken: () => string \| null }` |
90
- | `headerPlugin` | 注入自定义动态 header | `{ name: string, getValue: () => string \| null }` |
91
- | `unwrapPlugin` | 解包 dwyeapi `{ code, message, data, timestamp }`,code !== `"SUCCESS"` 时 reject(附带 `businessCode` / `apiResponse`);非该格式原样透传 | 无 |
92
- | `refreshTokenPlugin` | 401 自动刷新并重放 | 见下 |
93
-
94
- ## refreshTokenPlugin(带 retry 注入)
95
-
96
- 新版需要传 `retry`,因为插件不再持有 client 引用:
97
-
98
- ```ts
99
- let http: HttpClient
100
- http = createRequest({
101
- plugins: [
102
- tokenPlugin({ getToken: () => localStorage.getItem('token') }),
103
- refreshTokenPlugin({
104
- getRefreshToken: () => localStorage.getItem('refreshToken'),
105
- refreshFn: async (refreshToken) => {
106
- const res = await http.post<{ token: string }>('/auth/refresh', { refreshToken })
107
- localStorage.setItem('token', res.data.token)
108
- return res.data.token
109
- },
110
- onRefreshFail: () => {
111
- localStorage.clear()
112
- window.location.href = '/login'
113
- },
114
- retry: (cfg) => http.request(cfg), // 必填:重放请求
115
- isLoginUrl: (url) => url.includes('/auth/login'), // 可选
116
- }),
117
- unwrapPlugin(),
118
- ],
119
- })
120
- ```
121
-
122
- ## 响应契约对齐 dwyeapi v0.7.0
123
-
124
- 所有接口统一信封 `{ code, message, data, timestamp }`,`code === "SUCCESS"` 表示业务成功。unwrapPlugin 自动按此契约解包,业务代码拿到的 `response.data` 就是纯业务数据或 `PageData<T>`。
125
-
126
- ```ts
127
- import { SUCCESS_CODE } from '@dwydev/ekit'
128
- import type { ApiResponse, PageData } from '@dwydev/ekit'
129
-
130
- // 单体响应
131
- const res = await http.get<User>('/users/1')
132
- // res.data: User(unwrap 后是 User 不是 ApiResponse<User>)
133
-
134
- // 分页响应
135
- const list = await http.get<PageData<User>>('/users?page=1&page_size=20')
136
- list.data.items // User[]
137
- list.data.total // number
138
- list.data.page // number
139
- list.data.page_size // number(保持 snake_case 对齐后端 JSON)
140
- ```
141
-
142
- **内置业务错误码(`CommonBusinessCode`):**
143
-
144
- | code | HTTP | 含义 |
145
- |------|------|------|
146
- | `SUCCESS` | 200 | 业务成功,unwrap 自动解包 data |
147
- | `NOT_FOUND` | 404 | 资源不存在 |
148
- | `BUSINESS_ERROR` | 422 | 业务规则不允许(基础码;业务常自定义为 `INSUFFICIENT_BALANCE` 等) |
149
- | `PERMISSION_DENIED` | 403 | 无权限 |
150
- | `AUTHENTICATION_FAILED` | 401 | 认证失败 |
151
- | `VALIDATION_ERROR` | 422 | 请求参数校验失败,`data.errors` 为字段错误数组 |
152
- | `INTERNAL_ERROR` | 500 | 服务器错误 |
153
- | `HTTP_{status_code}` | 原状态 | 第三方 HTTPException 透传(如 OAuth2 的 `HTTP_401`) |
154
-
155
- `BusinessCode = CommonBusinessCode | (string & {})`:既自动补全常见码,又允许业务 `BusinessError(code="CUSTOM")` 自定义。
156
-
157
- ## 捕获业务错误:isApiBusinessError + extractValidationErrors
158
-
159
- unwrapPlugin 失败时抛 `HttpError`,附带 `businessCode`(业务错误码)和 `apiResponse`(原始信封)。业务 catch 直接按 code 分支:
160
-
161
- ```ts
162
- import { isApiBusinessError, extractValidationErrors } from '@dwydev/ekit'
163
-
164
- try {
165
- await http.post('/orders', body)
166
- } catch (err) {
167
- if (!isApiBusinessError(err)) {
168
- message.error('网络异常')
169
- return
170
- }
171
- switch (err.businessCode) {
172
- case 'INSUFFICIENT_BALANCE': showRecharge(); break
173
- case 'NOT_FOUND': message.warning('订单不存在'); break
174
- case 'VALIDATION_ERROR':
175
- // err.apiResponse.data.errors: [{ field: "body.email", message: "..." }, ...]
176
- form.setErrors(extractValidationErrors(err))
177
- break
178
- default: message.error(err.message)
179
- }
180
- }
181
- ```
182
-
183
- `extractValidationErrors` 自动剥离 `body. / query. / path. / header. / cookie.` 前缀,返回 `{ field: message }` 扁平对象,与 vee-validate 的 `form.setErrors` 直接兼容。嵌套字段路径(如 `body.items.0.name`)保留为 `items.0.name`。输入可以是 HttpError、完整 ApiResponse、或裸 `ValidationErrorData`,非校验错误时返回空对象。
@@ -1,109 +0,0 @@
1
- # UI 交互(copy / file / hooks)
2
-
3
- > 何时读这份:当用户使用本模块的 API 或问法涉及本模块功能时读取(剪贴板、文件下载/Blob/文件大小、Vue Composables 工具)。
4
-
5
- ## copy — 剪贴板
6
-
7
- ```ts
8
- import { copyText, useClipboard } from '@dwydev/ekit'
9
- ```
10
-
11
- ### copyText(text)
12
-
13
- 非组件场景一次性复制:
14
-
15
- ```ts
16
- await copyText('hello')
17
- ```
18
-
19
- ### useClipboard — 直接再导出 @vueuse/core
20
-
21
- ```ts
22
- const { text, copy, copied, isSupported } = useClipboard({ source: '', copiedDuring: 1500 })
23
- copy('hello')
24
- // copied: ComputedRef<boolean>,1500ms 后自动复位
25
- // isSupported: ComputedRef<boolean>,注意是 computed 不是 ref
26
- ```
27
-
28
- > 旧版 ekit 自写的 `useClipboard` 返回结构略有不同,迁移时需对照新签名。
29
-
30
- ---
31
-
32
- ## file — 文件下载 / Blob / 文件大小
33
-
34
- ```ts
35
- import { downloadFile, saveBlob, formatFileSize } from '@dwydev/ekit'
36
- import type { DownloadOptions, FileRequester } from '@dwydev/ekit' // requester 不是 requestInstance
37
- ```
38
-
39
- ### downloadFile(url, options?)
40
-
41
- ```ts
42
- await downloadFile('/api/export.xlsx') // GET,自动从 Content-Disposition 解析文件名
43
- await downloadFile('/api/export', {
44
- filename: '报表.xlsx',
45
- method: 'POST',
46
- data: { dateRange: [start, end] },
47
- headers: { Authorization: `Bearer ${token}` },
48
- })
49
-
50
- // 用自己的 HTTP 客户端发请求(不传则动态 import axios)
51
- await downloadFile('/api/export', { requester: myAxiosInstance })
52
- ```
53
-
54
- ### FileRequester 契约
55
-
56
- 任何满足以下结构的对象都可以作为 requester(axios 实例天然满足):
57
-
58
- ```ts
59
- interface FileRequester {
60
- request(config: {
61
- url: string
62
- method?: 'GET' | 'POST'
63
- data?: any
64
- headers?: Record<string, string>
65
- responseType: 'blob'
66
- }): Promise<{
67
- data: Blob | ArrayBuffer
68
- headers?: Record<string, string | undefined>
69
- }>
70
- }
71
- ```
72
-
73
- > 旧版字段 `requestInstance: AxiosInstance` 已改名 `requester: FileRequester`,类型不再泄露 axios。
74
-
75
- ### 其他
76
-
77
- ```ts
78
- saveBlob(blob, 'data.json') // 直接保存已有 Blob
79
- formatFileSize(1234567) // '1.18 MB'
80
- formatFileSize(1234567, 0) // '1 MB'
81
- ```
82
-
83
- ---
84
-
85
- ## hooks — Vue Composables
86
-
87
- **全部再导出 `@vueuse/core`**,ekit 不再自写。从 `@dwydev/ekit` 主入口导入即可:
88
-
89
- ```ts
90
- import {
91
- // src/hooks/index.ts
92
- useDebounce, useClickOutside, useEventListener,
93
- // src/hooks/vueuse.ts
94
- useThrottle, useWindowSize, useMediaQuery, useIntersectionObserver, useResizeObserver,
95
- } from '@dwydev/ekit'
96
- ```
97
-
98
- | ekit 名 | VueUse 实现 | 说明 |
99
- |---------|-------------|------|
100
- | `useDebounce(value, delay=300)` | `refDebounced` | 默认 300ms(VueUse 默认 200ms) |
101
- | `useClickOutside(target, handler)` | `onClickOutside` | 点击元素外部触发 |
102
- | `useEventListener(target, event, handler, options?)` | 同名 | 自动绑/解绑,支持 `Ref<EventTarget>` |
103
- | `useThrottle(fn, ms)` | `useThrottleFn` | 节流函数 |
104
- | `useWindowSize()` | 同名 | `{ width, height }` |
105
- | `useMediaQuery(query)` | 同名 | `useMediaQuery('(max-width: 768px)')` |
106
- | `useIntersectionObserver(target, cb)` | 同名 | 监听元素出现/消失 |
107
- | `useResizeObserver(target, cb)` | 同名 | 监听元素尺寸变化 |
108
-
109
- > 旧版 ekit 自写的 `useDebounce` 返回普通 `Ref<T>`,新版返回 VueUse 的 `Readonly<Ref<T>>`,**不能** `debouncedSearch.value = '...'`。