create-dwy 0.8.0 → 0.9.1
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/package.json +1 -1
- package/src/create.js +50 -3
- package/src/utils.js +8 -0
- package/templates/claude-global/CLAUDE.md +81 -6
- package/templates/claude-global/rules/dwy-postgres.md +89 -0
- package/templates/claude-global/rules/dwy-python-backend.md +492 -0
- package/templates/claude-global/rules/dwy-python-core.md +94 -634
- package/templates/claude-global/rules/dwy-python-testing.md +115 -13
- package/templates/claude-global/rules/dwy-redis.md +88 -0
- package/templates/claude-global/rules/dwy-vue-core.md +341 -645
- package/templates/claude-global/rules/dwy-vue-pinia.md +47 -34
- package/templates/claude-global/rules/dwy-vue-testing.md +120 -55
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +1 -1
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +1 -1
- package/templates/claude-global/skills/dwy-dolphindb/SKILL.md +22 -15
- package/templates/claude-global/skills/dwy-dolphindb/evals/evals.json +23 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/dolphindb-rules.md +100 -1
- package/templates/claude-global/skills/dwy-ekit/SKILL.md +142 -276
- package/templates/claude-global/skills/dwy-eui/SKILL.md +264 -182
- package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +117 -129
- package/templates/claude-global/skills/dwy-git-commit/SKILL.md +125 -0
- package/templates/claude-global/skills/dwy-publish/SKILL.md +1 -1
- package/templates/claude-global/{rules/dwy-tech-stack.md → skills/dwy-tech-stack/SKILL.md} +4 -3
- package/templates/project/fullstack-monorepo/CLAUDE.md.hbs +81 -0
- package/templates/project/fullstack-monorepo/_env.example.hbs +24 -0
- package/templates/project/fullstack-monorepo/_gitignore +37 -0
- package/templates/project/fullstack-monorepo/backend/.python-version +1 -0
- package/templates/project/fullstack-monorepo/backend/CLAUDE.md.hbs +68 -0
- package/templates/project/fullstack-monorepo/backend/Dockerfile.dev +23 -0
- package/templates/project/fullstack-monorepo/backend/alembic/env.py.hbs +68 -0
- package/templates/project/fullstack-monorepo/backend/alembic/script.py.mako +26 -0
- package/templates/project/fullstack-monorepo/backend/alembic.ini.hbs +43 -0
- package/templates/project/fullstack-monorepo/backend/packages/dwy-shared/pyproject.toml +16 -0
- package/templates/project/fullstack-monorepo/backend/packages/dwy-shared/src/dwy_shared/__init__.py +12 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/pyproject.toml.hbs +20 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/__init__.py.hbs +40 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/config.py +9 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/models.py.hbs +17 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/protocols.py +14 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/router.py.hbs +22 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/schemas.py +21 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/service.py +11 -0
- package/templates/project/fullstack-monorepo/backend/pyproject.toml.hbs +84 -0
- package/templates/project/fullstack-monorepo/backend/src/app/__init__.py +3 -0
- package/templates/project/fullstack-monorepo/backend/src/app/config.py.hbs +21 -0
- package/templates/project/fullstack-monorepo/backend/src/app/exceptions.py +26 -0
- package/templates/project/fullstack-monorepo/backend/src/app/factories.py +16 -0
- package/templates/project/fullstack-monorepo/backend/src/app/infra/__init__.py +1 -0
- package/templates/project/fullstack-monorepo/backend/src/app/main.py.hbs +65 -0
- package/templates/project/fullstack-monorepo/backend/src/app/middleware/__init__.py +1 -0
- package/templates/project/fullstack-monorepo/backend/tests/__init__.py +0 -0
- package/templates/project/fullstack-monorepo/backend/tests/conftest.py.hbs +42 -0
- package/templates/project/fullstack-monorepo/dev.sh.hbs +83 -0
- package/templates/project/fullstack-monorepo/docker-compose.dev.yml.hbs +36 -0
- package/templates/project/fullstack-monorepo/frontend/CLAUDE.md.hbs +35 -0
- package/templates/project/fullstack-monorepo/frontend/Dockerfile +18 -0
- package/templates/project/fullstack-monorepo/frontend/_npmrc +3 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/index.html.hbs +13 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/package.json.hbs +31 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/App.vue.hbs +10 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/api/client.ts.hbs +11 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/features/{{initialDomain}}/.gitkeep +0 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/index.css +12 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/main.ts +17 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/router/index.ts.hbs +24 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/stores/.gitkeep +0 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/types/.gitkeep +0 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/views/HomeView.vue.hbs +36 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/tsconfig.json +7 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/vite.config.ts.hbs +27 -0
- package/templates/project/fullstack-monorepo/frontend/nginx.conf +11 -0
- package/templates/project/fullstack-monorepo/frontend/package.json.hbs +13 -0
- package/templates/project/fullstack-monorepo/frontend/packages/core/package.json.hbs +15 -0
- package/templates/project/fullstack-monorepo/frontend/packages/core/src/index.ts +3 -0
- package/templates/project/fullstack-monorepo/frontend/packages/features/{{initialDomain}}/package.json.hbs +14 -0
- package/templates/project/fullstack-monorepo/frontend/packages/features/{{initialDomain}}/src/index.ts.hbs +16 -0
- package/templates/project/fullstack-monorepo/frontend/packages/features/{{initialDomain}}/src/pages/IndexPage.vue.hbs +9 -0
- package/templates/project/fullstack-monorepo/frontend/packages/ui/package.json.hbs +15 -0
- package/templates/project/fullstack-monorepo/frontend/packages/ui/src/index.ts +3 -0
- package/templates/project/fullstack-monorepo/frontend/pnpm-workspace.yaml.hbs +23 -0
- package/templates/project/fullstack-monorepo/frontend/tsconfig.base.json.hbs +23 -0
- package/templates/claude-global/rules/dwy-api-design.md +0 -110
- package/templates/claude-global/rules/dwy-git-commit-convention.md +0 -82
- package/templates/claude-global/rules/dwy-git-security.md +0 -53
- package/templates/claude-global/rules/dwy-payload-limits.md +0 -186
- package/templates/claude-global/rules/dwy-prefer-open-source.md +0 -32
- package/templates/claude-global/rules/dwy-project-structure.md +0 -181
- package/templates/claude-global/rules/dwy-python-fastapi.md +0 -272
- package/templates/claude-global/rules/dwy-python-orm.md +0 -61
- package/templates/claude-global/rules/dwy-python-security.md +0 -636
- package/templates/claude-global/rules/dwy-server-security.md +0 -518
- package/templates/claude-global/rules/dwy-vue-api.md +0 -146
- package/templates/claude-global/skills/dwy-ekit/references/data.md +0 -135
- package/templates/claude-global/skills/dwy-ekit/references/form.md +0 -68
- package/templates/claude-global/skills/dwy-ekit/references/request.md +0 -183
- package/templates/claude-global/skills/dwy-ekit/references/ui.md +0 -109
- package/templates/claude-global/skills/dwy-vue-core/SKILL.md +0 -890
- /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 = '...'`。
|