create-dwy 0.5.1 → 0.6.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/bin/index.js +6 -15
- package/package.json +3 -3
- package/src/create.js +61 -37
- package/src/sync.js +206 -61
- package/templates/claude-global/CLAUDE.md +92 -0
- package/templates/claude-global/rules/dwy-android-core.md +610 -0
- package/templates/claude-global/rules/dwy-api-design.md +110 -0
- package/templates/claude-global/rules/dwy-db-migration.md +46 -0
- package/templates/claude-global/rules/dwy-flutter-core.md +564 -0
- package/templates/claude-global/rules/dwy-flutter-network.md +222 -0
- package/templates/claude-global/rules/dwy-flutter-router.md +97 -0
- package/templates/claude-global/rules/dwy-flutter-testing.md +94 -0
- package/templates/claude-global/rules/dwy-git-commit-convention.md +82 -0
- package/templates/claude-global/rules/dwy-git-security.md +53 -0
- package/templates/claude-global/rules/dwy-payload-limits.md +178 -0
- package/templates/claude-global/rules/{prefer-open-source.md → dwy-prefer-open-source.md} +4 -0
- package/templates/claude-global/rules/dwy-project-structure.md +181 -0
- package/templates/claude-global/rules/{python-code-style.md → dwy-python-core.md} +292 -358
- package/templates/claude-global/rules/dwy-python-fastapi.md +272 -0
- package/templates/claude-global/rules/dwy-python-orm.md +61 -0
- package/templates/claude-global/rules/{backend-security.md → dwy-python-security.md} +99 -72
- package/templates/claude-global/rules/dwy-python-testing.md +53 -0
- package/templates/claude-global/rules/{server-security.md → dwy-server-security.md} +1 -2
- package/templates/claude-global/rules/dwy-tdd-development.md +70 -0
- package/templates/claude-global/rules/dwy-tech-stack.md +60 -0
- package/templates/claude-global/rules/dwy-vue-api.md +146 -0
- package/templates/claude-global/rules/{vue-code-style.md → dwy-vue-core.md} +338 -152
- package/templates/claude-global/rules/dwy-vue-pinia.md +75 -0
- package/templates/claude-global/rules/dwy-vue-testing.md +102 -0
- package/templates/claude-global/skills/dwy-deploy-audit/SKILL.md +481 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-data.md +117 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +74 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-runtime.md +184 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_base.sh +39 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_capacity.sh +278 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_db.sh +122 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_docker.sh +249 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_env.sh +64 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_https.sh +60 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_logs.sh +209 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +92 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_resilience.sh +157 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_secrets.sh +327 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_services.sh +77 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_ssh.sh +56 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/run_all.sh +105 -0
- package/templates/claude-global/skills/dwy-docker/SKILL.md +187 -0
- package/templates/claude-global/skills/dwy-docker/references/ask-templates.md +142 -0
- package/templates/claude-global/skills/dwy-docker/references/templates.md +479 -0
- package/templates/claude-global/skills/dwy-docker/references/version-rules.md +76 -0
- package/templates/claude-global/skills/dwy-docker/scripts/query_dockerhub.py +204 -0
- package/templates/claude-global/skills/dwy-docker/scripts/scan_images.sh +185 -0
- package/templates/claude-global/skills/dwy-eapi/SKILL.md +433 -0
- package/templates/claude-global/skills/dwy-eapi/references/tasks-integration-guide.md +387 -0
- package/templates/claude-global/skills/dwy-ekit/SKILL.md +85 -0
- package/templates/claude-global/skills/dwy-ekit/references/data.md +135 -0
- package/templates/claude-global/skills/dwy-ekit/references/form.md +68 -0
- package/templates/claude-global/skills/dwy-ekit/references/request.md +183 -0
- package/templates/claude-global/skills/dwy-ekit/references/ui.md +109 -0
- package/templates/claude-global/skills/{dwy-frontend-eui → dwy-eui}/SKILL.md +129 -15
- package/templates/claude-global/skills/dwy-eui/references/eui-design-guide.md +257 -0
- package/templates/claude-global/skills/dwy-eui/references/eui-integration-guide.md +302 -0
- package/templates/claude-global/skills/dwy-eui/references/eui-landing-design-guide.md +437 -0
- package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +214 -0
- package/templates/claude-global/skills/dwy-mirror-source/SKILL.md +275 -0
- package/templates/claude-global/skills/dwy-mirror-source/references/dockerfile-snippets.md +228 -0
- package/templates/claude-global/skills/dwy-mirror-source/references/mirror-providers.md +150 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/__pycache__/check_mirrors.cpython-313.pyc +0 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/apply_mirrors.py +666 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/check_mirrors.py +868 -0
- package/templates/claude-global/skills/dwy-pentest/SKILL.md +344 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__init__.py +1 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/__init__.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/config.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/executor.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/severity.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/validator.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/config.py +109 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/executor.py +157 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/severity.py +96 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/validator.py +113 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_auth.py +289 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_recon.py +217 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_report.py +343 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_scan.py +205 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_ssl.py +208 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_vuln.py +205 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_web.py +330 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pyproject.toml +14 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/setup_tools.sh +97 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/uv.lock +415 -0
- package/templates/claude-global/skills/{dwy-release → dwy-publish}/SKILL.md +42 -18
- package/templates/claude-global/skills/dwy-vue-core/SKILL.md +890 -0
- package/templates/project/backend/.dwy.hbs +5 -0
- package/templates/project/backend/.python-version +1 -0
- package/templates/project/backend/CLAUDE.md.hbs +40 -0
- package/templates/project/{web/backend → backend}/alembic/script.py.mako +2 -0
- package/templates/project/{web/backend → backend}/alembic.ini.hbs +0 -1
- package/templates/project/backend/deploy.sh.hbs +18 -0
- package/templates/project/{web → backend}/docker-compose.prod.yml.hbs +1 -1
- package/templates/project/{web/backend → backend}/pyproject.toml.hbs +4 -5
- package/templates/project/{web/backend → backend}/src/app/config.py.hbs +1 -1
- package/templates/project/backend/src/app/database.py +10 -0
- package/templates/project/backend/src/app/dependencies.py +5 -0
- package/templates/project/{web/backend → backend}/src/app/main.py.hbs +3 -3
- package/templates/project/backend/src/app/models/base.py +5 -0
- package/templates/project/web/CLAUDE.md.hbs +29 -36
- package/templates/project/web/{frontend → apps/web}/index.html.hbs +1 -0
- package/templates/project/web/{frontend → apps/web}/package.json.hbs +4 -3
- package/templates/project/web/apps/web/src/App.vue +9 -0
- package/templates/project/web/apps/web/src/api/client.ts +5 -0
- package/templates/project/web/apps/web/src/composables/.gitkeep +0 -0
- package/templates/project/web/apps/web/src/layouts/DefaultLayout.vue +8 -0
- package/templates/project/web/{frontend → apps/web}/src/router/index.ts +1 -1
- package/templates/project/web/{frontend → apps/web}/src/stores/auth.ts +4 -2
- package/templates/project/web/apps/web/src/style.css +2 -0
- package/templates/project/web/apps/web/src/types/router.d.ts +8 -0
- package/templates/project/web/apps/web/src/views/HomeView.vue +8 -0
- package/templates/project/web/apps/web/tsconfig.app.json +18 -0
- package/templates/project/web/apps/web/tsconfig.json +6 -0
- package/templates/project/web/{frontend → apps/web}/vite.config.ts.hbs +7 -1
- package/templates/project/web/package.json.hbs +16 -0
- package/templates/project/web/packages/.gitkeep +0 -0
- package/templates/project/web/pnpm-workspace.yaml +3 -0
- package/templates/claude-global/hooks/post-sync-claude-global.sh +0 -72
- package/templates/claude-global/rules/agent-team-execution.md +0 -138
- package/templates/claude-global/rules/agent-team-patterns.md +0 -157
- package/templates/claude-global/rules/git-commit-convention.md +0 -106
- package/templates/claude-global/rules/git-security.md +0 -144
- package/templates/claude-global/skills/dwy-backend-base/SKILL.md +0 -279
- package/templates/claude-global/skills/dwy-frontend-core/SKILL.md +0 -181
- package/templates/project/web/.claude/commands/deploy.md +0 -4
- package/templates/project/web/backend/src/app/models/base.py +0 -5
- package/templates/project/web/deploy.sh.hbs +0 -113
- package/templates/project/web/frontend/src/App.vue +0 -3
- package/templates/project/web/frontend/src/api/client.ts +0 -13
- package/templates/project/web/frontend/src/style.css +0 -1
- package/templates/project/web/frontend/src/views/HomeView.vue +0 -5
- package/templates/project/web/frontend/tsconfig.app.json +0 -14
- package/templates/project/web/frontend/tsconfig.json +0 -4
- /package/templates/{project/web/backend → claude-global/skills/dwy-pentest/scripts}/.python-version +0 -0
- /package/templates/project/{web/backend → backend}/Dockerfile +0 -0
- /package/templates/project/{web/backend → backend}/alembic/env.py.hbs +0 -0
- /package/templates/{claude-global/skills → project/backend/alembic/versions}/.gitkeep +0 -0
- /package/templates/project/{web → backend}/docker-compose.dev.yml.hbs +0 -0
- /package/templates/project/{web/backend → backend}/src/app/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/models/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/routers/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/routers/health.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/schemas/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/services/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/tests/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/tests/conftest.py.hbs +0 -0
- /package/templates/project/{web/backend/alembic/versions → mobile}/.gitkeep +0 -0
- /package/templates/project/web/{frontend → apps/web}/src/components/.gitkeep +0 -0
- /package/templates/project/web/{frontend → apps/web}/src/main.ts +0 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Vue 前端网络层(ekit createRequest + 拦截器插件)
|
|
3
|
+
paths:
|
|
4
|
+
- "**/api/**/*.ts"
|
|
5
|
+
- "**/api/**/*.vue"
|
|
6
|
+
- "**/api/client.ts"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Vue 前端网络层规范
|
|
10
|
+
|
|
11
|
+
## 六、API 请求规范
|
|
12
|
+
|
|
13
|
+
### 强制规则
|
|
14
|
+
|
|
15
|
+
- API 文件按**业务域拆分**(`api/user.ts`、`api/exam.ts`)
|
|
16
|
+
- 每个 API 函数**独立导出**,不使用 default export
|
|
17
|
+
- 请求参数和响应使用**类型标注**
|
|
18
|
+
- **禁止**在组件中直接创建 axios 实例或调用 `fetch`
|
|
19
|
+
|
|
20
|
+
### API 文件结构
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// api/user.ts
|
|
24
|
+
import request from '@/utils/request'
|
|
25
|
+
import type { UserInfo, UserCreate, UserUpdate } from '@/types/user'
|
|
26
|
+
|
|
27
|
+
export function getUsers(params?: { page?: number; page_size?: number }) {
|
|
28
|
+
return request.get<{ items: UserInfo[]; total: number }>('/users', { params })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getUser(id: number) {
|
|
32
|
+
return request.get<UserInfo>(`/users/${id}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createUser(data: UserCreate) {
|
|
36
|
+
return request.post<UserInfo>('/users', data)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function updateUser(id: number, data: UserUpdate) {
|
|
40
|
+
return request.put<UserInfo>(`/users/${id}`, data)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function deleteUser(id: number) {
|
|
44
|
+
return request.delete(`/users/${id}`)
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 请求实例配置
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// utils/request.ts
|
|
52
|
+
import axios from 'axios'
|
|
53
|
+
|
|
54
|
+
const request = axios.create({
|
|
55
|
+
baseURL: '/api',
|
|
56
|
+
timeout: 30000,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Token 注入
|
|
60
|
+
request.interceptors.request.use((config) => {
|
|
61
|
+
const token = localStorage.getItem('token')
|
|
62
|
+
if (token) {
|
|
63
|
+
config.headers.Authorization = `Bearer ${token}`
|
|
64
|
+
}
|
|
65
|
+
return config
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// 401 处理
|
|
69
|
+
request.interceptors.response.use(
|
|
70
|
+
(response) => response,
|
|
71
|
+
(error) => {
|
|
72
|
+
if (error.response?.status === 401) {
|
|
73
|
+
localStorage.removeItem('token')
|
|
74
|
+
window.location.href = '/login'
|
|
75
|
+
}
|
|
76
|
+
return Promise.reject(error)
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
export default request
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 禁止的写法
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// ❌ 在组件中直接调用
|
|
87
|
+
const res = await axios.get('/api/users')
|
|
88
|
+
|
|
89
|
+
// ✅ 通过 API 模块调用
|
|
90
|
+
import { getUsers } from '@/api/user'
|
|
91
|
+
const res = await getUsers()
|
|
92
|
+
|
|
93
|
+
// ❌ API 函数使用 default export
|
|
94
|
+
export default { getUsers, createUser }
|
|
95
|
+
|
|
96
|
+
// ✅ 每个函数独立导出
|
|
97
|
+
export function getUsers() { ... }
|
|
98
|
+
export function createUser() { ... }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 网络层规范(ekit 统一方案)
|
|
104
|
+
|
|
105
|
+
### Vue 前端
|
|
106
|
+
|
|
107
|
+
**唯一方式:** ekit `createRequest()` + 插件。
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// api/client.ts
|
|
111
|
+
import { createRequest } from '@dwydev/ekit'
|
|
112
|
+
|
|
113
|
+
export const request = createRequest({
|
|
114
|
+
baseURL: import.meta.env.VITE_API_BASE_URL,
|
|
115
|
+
plugins: [
|
|
116
|
+
tokenPlugin(), // 注入 Authorization header
|
|
117
|
+
refreshTokenPlugin(), // 401 自动刷新
|
|
118
|
+
unwrapPlugin(), // 解包响应 → data
|
|
119
|
+
],
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// api/user.ts — 按业务域拆分
|
|
123
|
+
export function getUsers(params: { page: number; page_size: number }) {
|
|
124
|
+
return request.get('/users', { params })
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function createUser(data: UserCreate) {
|
|
128
|
+
return request.post('/users', data)
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**禁止:** `axios.create()`、`fetch()`、`new XMLHttpRequest()`
|
|
133
|
+
|
|
134
|
+
### 响应解包
|
|
135
|
+
|
|
136
|
+
前端的解包逻辑必须与后端 eapi response 格式对齐:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
后端返回: { code: "SUCCESS", message: "success", data: {...}, timestamp: ... }
|
|
140
|
+
↓ unwrap
|
|
141
|
+
前端拿到: {...} (直接是 data 的内容)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
分页响应解包后拿到 `{ items, total, page, page_size }`(保持 snake_case 对齐后端 JSON)。
|
|
145
|
+
|
|
146
|
+
业务失败时(`code !== "SUCCESS"`),unwrapPlugin 抛出的 HttpError 携带 `businessCode` 和 `apiResponse`。业务 catch 中用 `isApiBusinessError(err)` 做类型守卫后按 `err.businessCode` 分支(如 `NOT_FOUND` / `VALIDATION_ERROR` / `PERMISSION_DENIED`);`VALIDATION_ERROR` 场景可用 `extractValidationErrors(err)` 直接给 vee-validate 的 `setErrors` 回填。
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
+
description: Vue 3 + TS + Vite + Tailwind 基础风格(组件/TS/模板/样式/反模式/错误处理)
|
|
2
3
|
paths:
|
|
3
4
|
- "**/*.vue"
|
|
4
|
-
- "**/*.ts"
|
|
5
|
-
- "**/*.tsx"
|
|
6
5
|
- "**/vite.config.*"
|
|
7
|
-
- "**/tsconfig*.json"
|
|
8
6
|
---
|
|
9
7
|
|
|
10
|
-
# Vue 3
|
|
11
|
-
|
|
12
|
-
所有 Vue / TypeScript 前端项目必须遵循以下规范。AI 生成或修改代码时,必须严格按照这些规则执行。
|
|
8
|
+
# Vue 3 基础风格规范
|
|
13
9
|
|
|
14
10
|
## 一、项目结构
|
|
15
11
|
|
|
@@ -36,9 +32,16 @@ frontend/
|
|
|
36
32
|
│ ├── App.vue # 根组件
|
|
37
33
|
│ ├── main.ts # 应用入口
|
|
38
34
|
│ └── style.css # 全局样式(Tailwind 入口)
|
|
35
|
+
├── tests/ # 测试文件(独立目录,禁止放在 src/ 内)
|
|
36
|
+
│ ├── utils/ # 对应 src/utils/
|
|
37
|
+
│ ├── stores/ # 对应 src/stores/
|
|
38
|
+
│ ├── api/ # 对应 src/api/
|
|
39
|
+
│ ├── router/ # 对应 src/router/
|
|
40
|
+
│ └── components/ # 对应 src/components/
|
|
39
41
|
├── index.html
|
|
40
42
|
├── package.json
|
|
41
43
|
├── vite.config.ts
|
|
44
|
+
├── vitest.config.ts # 测试配置(独立文件)
|
|
42
45
|
├── tsconfig.json
|
|
43
46
|
├── tsconfig.app.json
|
|
44
47
|
└── .npmrc
|
|
@@ -138,6 +141,37 @@ onMounted(() => {
|
|
|
138
141
|
| 事件处理函数 | `handle` + 动作 | `handleLogin`、`handleDelete` |
|
|
139
142
|
| 布尔变量 | `is/has/can/should` 前缀 | `isLoading`、`hasPermission` |
|
|
140
143
|
|
|
144
|
+
### 禁止魔法字符串
|
|
145
|
+
|
|
146
|
+
同一个字符串字面量在文件内出现 2 次及以上时,**必须**提取为常量。
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
// ❌ 魔法字符串散落多处,改一处漏一处
|
|
150
|
+
storage.set('access_token', token)
|
|
151
|
+
// ...
|
|
152
|
+
const t = storage.get('access_token')
|
|
153
|
+
|
|
154
|
+
// ✅ 提取为常量
|
|
155
|
+
const ACCESS_TOKEN_KEY = 'access_token'
|
|
156
|
+
storage.set(ACCESS_TOKEN_KEY, token)
|
|
157
|
+
const t = storage.get(ACCESS_TOKEN_KEY)
|
|
158
|
+
|
|
159
|
+
// ❌ 事件名、路由路径、storage key 等重复字面量
|
|
160
|
+
emit('update:modelValue', value)
|
|
161
|
+
router.push('/dashboard')
|
|
162
|
+
localStorage.getItem('theme')
|
|
163
|
+
|
|
164
|
+
// ✅ 跨文件共享的 key 放到专门的常量文件
|
|
165
|
+
// constants/storage-keys.ts
|
|
166
|
+
export const STORAGE_KEYS = {
|
|
167
|
+
ACCESS_TOKEN: 'access_token',
|
|
168
|
+
REFRESH_TOKEN: 'refresh_token',
|
|
169
|
+
THEME: 'theme',
|
|
170
|
+
} as const
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**判断标准:** 同一字符串在同一文件出现 ≥ 2 次 → 提取为常量。跨文件使用的 key → 提取到 `constants/` 目录共享。
|
|
174
|
+
|
|
141
175
|
### 禁止的写法
|
|
142
176
|
|
|
143
177
|
```typescript
|
|
@@ -229,59 +263,6 @@ interface TableColumn {
|
|
|
229
263
|
| `Array<string>` | `string[]` |
|
|
230
264
|
| 枚举 `enum Status {}` | 联合类型 `type Status = 'active' \| 'inactive'` |
|
|
231
265
|
|
|
232
|
-
## 四、Pinia 状态管理
|
|
233
|
-
|
|
234
|
-
### 强制使用 Setup Store
|
|
235
|
-
|
|
236
|
-
```typescript
|
|
237
|
-
// ✅ Setup Store(强制)
|
|
238
|
-
export const useAuthStore = defineStore('auth', () => {
|
|
239
|
-
const token = ref<string | null>(localStorage.getItem('token'))
|
|
240
|
-
const user = ref<UserInfo | null>(null)
|
|
241
|
-
|
|
242
|
-
const isLoggedIn = computed(() => !!token.value)
|
|
243
|
-
|
|
244
|
-
async function login(username: string, password: string) {
|
|
245
|
-
const res = await apiLogin(username, password)
|
|
246
|
-
token.value = res.data.access_token
|
|
247
|
-
localStorage.setItem('token', token.value)
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function logout() {
|
|
251
|
-
token.value = null
|
|
252
|
-
user.value = null
|
|
253
|
-
localStorage.removeItem('token')
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return { token, user, isLoggedIn, login, logout }
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
// ❌ Options Store(禁止)
|
|
260
|
-
export const useAuthStore = defineStore('auth', {
|
|
261
|
-
state: () => ({ token: '' }),
|
|
262
|
-
actions: { login() {} }
|
|
263
|
-
})
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
### Store 设计原则
|
|
267
|
-
|
|
268
|
-
| 原则 | 说明 |
|
|
269
|
-
|------|------|
|
|
270
|
-
| 单一职责 | 一个 store 管理一个业务域(auth、menu、user) |
|
|
271
|
-
| 不直接修改 state | 通过方法修改,不在组件中直接 `store.xxx = yyy` |
|
|
272
|
-
| 异步操作放 store | API 调用封装在 store 方法中 |
|
|
273
|
-
| 解构用 `storeToRefs` | 解构响应式状态必须用 `storeToRefs`,方法直接解构 |
|
|
274
|
-
|
|
275
|
-
```typescript
|
|
276
|
-
// ✅ 正确解构
|
|
277
|
-
const store = useReportStore()
|
|
278
|
-
const { report, loading } = storeToRefs(store)
|
|
279
|
-
const { setReport, reset } = store
|
|
280
|
-
|
|
281
|
-
// ❌ 错误解构(丢失响应性)
|
|
282
|
-
const { report, loading } = store
|
|
283
|
-
```
|
|
284
|
-
|
|
285
266
|
## 五、路由规范
|
|
286
267
|
|
|
287
268
|
### 强制规则
|
|
@@ -346,96 +327,6 @@ declare module 'vue-router' {
|
|
|
346
327
|
}
|
|
347
328
|
```
|
|
348
329
|
|
|
349
|
-
## 六、API 请求规范
|
|
350
|
-
|
|
351
|
-
### 强制规则
|
|
352
|
-
|
|
353
|
-
- API 文件按**业务域拆分**(`api/user.ts`、`api/exam.ts`)
|
|
354
|
-
- 每个 API 函数**独立导出**,不使用 default export
|
|
355
|
-
- 请求参数和响应使用**类型标注**
|
|
356
|
-
- **禁止**在组件中直接创建 axios 实例或调用 `fetch`
|
|
357
|
-
|
|
358
|
-
### API 文件结构
|
|
359
|
-
|
|
360
|
-
```typescript
|
|
361
|
-
// api/user.ts
|
|
362
|
-
import request from '@/utils/request'
|
|
363
|
-
import type { UserInfo, UserCreate, UserUpdate } from '@/types/user'
|
|
364
|
-
|
|
365
|
-
export function getUsers(params?: { page?: number; page_size?: number }) {
|
|
366
|
-
return request.get<{ items: UserInfo[]; total: number }>('/users', { params })
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
export function getUser(id: number) {
|
|
370
|
-
return request.get<UserInfo>(`/users/${id}`)
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
export function createUser(data: UserCreate) {
|
|
374
|
-
return request.post<UserInfo>('/users', data)
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
export function updateUser(id: number, data: UserUpdate) {
|
|
378
|
-
return request.put<UserInfo>(`/users/${id}`, data)
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
export function deleteUser(id: number) {
|
|
382
|
-
return request.delete(`/users/${id}`)
|
|
383
|
-
}
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
### 请求实例配置
|
|
387
|
-
|
|
388
|
-
```typescript
|
|
389
|
-
// utils/request.ts
|
|
390
|
-
import axios from 'axios'
|
|
391
|
-
|
|
392
|
-
const request = axios.create({
|
|
393
|
-
baseURL: '/api',
|
|
394
|
-
timeout: 30000,
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
// Token 注入
|
|
398
|
-
request.interceptors.request.use((config) => {
|
|
399
|
-
const token = localStorage.getItem('token')
|
|
400
|
-
if (token) {
|
|
401
|
-
config.headers.Authorization = `Bearer ${token}`
|
|
402
|
-
}
|
|
403
|
-
return config
|
|
404
|
-
})
|
|
405
|
-
|
|
406
|
-
// 401 处理
|
|
407
|
-
request.interceptors.response.use(
|
|
408
|
-
(response) => response,
|
|
409
|
-
(error) => {
|
|
410
|
-
if (error.response?.status === 401) {
|
|
411
|
-
localStorage.removeItem('token')
|
|
412
|
-
window.location.href = '/login'
|
|
413
|
-
}
|
|
414
|
-
return Promise.reject(error)
|
|
415
|
-
},
|
|
416
|
-
)
|
|
417
|
-
|
|
418
|
-
export default request
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
### 禁止的写法
|
|
422
|
-
|
|
423
|
-
```typescript
|
|
424
|
-
// ❌ 在组件中直接调用
|
|
425
|
-
const res = await axios.get('/api/users')
|
|
426
|
-
|
|
427
|
-
// ✅ 通过 API 模块调用
|
|
428
|
-
import { getUsers } from '@/api/user'
|
|
429
|
-
const res = await getUsers()
|
|
430
|
-
|
|
431
|
-
// ❌ API 函数使用 default export
|
|
432
|
-
export default { getUsers, createUser }
|
|
433
|
-
|
|
434
|
-
// ✅ 每个函数独立导出
|
|
435
|
-
export function getUsers() { ... }
|
|
436
|
-
export function createUser() { ... }
|
|
437
|
-
```
|
|
438
|
-
|
|
439
330
|
## 七、组合式函数(Composables)
|
|
440
331
|
|
|
441
332
|
### 命名规范
|
|
@@ -668,8 +559,8 @@ import { useRouter, useRoute } from 'vue-router'
|
|
|
668
559
|
import { storeToRefs } from 'pinia'
|
|
669
560
|
import axios from 'axios'
|
|
670
561
|
|
|
671
|
-
// 3. 内部包(@
|
|
672
|
-
import { EButton, EInput } from '@
|
|
562
|
+
// 3. 内部包(@dwydev/*)
|
|
563
|
+
import { EButton, EInput } from '@dwydev/eui'
|
|
673
564
|
|
|
674
565
|
// 4. 项目内模块
|
|
675
566
|
import { useAuthStore } from '@/stores/auth'
|
|
@@ -693,7 +584,193 @@ import type { UserInfo } from '@/types/user'
|
|
|
693
584
|
import { UserInfo, getUsers } from '@/api/user'
|
|
694
585
|
```
|
|
695
586
|
|
|
696
|
-
##
|
|
587
|
+
## 十三、注释规范
|
|
588
|
+
|
|
589
|
+
### 强制规则
|
|
590
|
+
|
|
591
|
+
所有 `.ts` 和 `.vue` 文件必须遵循以下注释规则:
|
|
592
|
+
|
|
593
|
+
1. **文件顶部必须有概述注释** — 说明文件的功能和职责
|
|
594
|
+
2. **所有导出的方法/函数必须有中文注释** — 说明用途、参数、返回值
|
|
595
|
+
3. **所有导出的类必须有中文注释** — 说明类的职责
|
|
596
|
+
4. **所有导出的接口/类型必须有中文注释** — 说明类型含义,每个字段也需注释
|
|
597
|
+
5. **复杂的内部方法必须有中文注释** — 简单的 getter/setter 可省略
|
|
598
|
+
6. **注释使用中文** — 与代码保持一致的语言风格
|
|
599
|
+
|
|
600
|
+
### 文件顶部注释
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
603
|
+
/**
|
|
604
|
+
* 用户认证相关的 API 请求模块
|
|
605
|
+
* 提供登录、登出、刷新 token、获取用户信息等接口
|
|
606
|
+
*/
|
|
607
|
+
import { request } from '@/utils/request'
|
|
608
|
+
import type { LoginParams, UserInfo } from '@/types/user'
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
```vue
|
|
612
|
+
<!--
|
|
613
|
+
用户信息卡片组件
|
|
614
|
+
展示用户头像、昵称、角色标签,支持点击跳转到用户详情页
|
|
615
|
+
-->
|
|
616
|
+
<script setup lang="ts">
|
|
617
|
+
// ...
|
|
618
|
+
</script>
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
### 方法/函数注释
|
|
622
|
+
|
|
623
|
+
```typescript
|
|
624
|
+
/**
|
|
625
|
+
* 用户登录
|
|
626
|
+
* @param params 登录参数(用户名、密码)
|
|
627
|
+
* @returns 登录成功后返回用户信息和 token
|
|
628
|
+
*/
|
|
629
|
+
export async function login(params: LoginParams): Promise<LoginResult> {
|
|
630
|
+
return request.post('/auth/login', params)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* 格式化金额显示
|
|
635
|
+
* @param value 金额数值(单位:分)
|
|
636
|
+
* @param currency 货币符号,默认 ¥
|
|
637
|
+
* @returns 格式化后的金额字符串,如 ¥1,234.56
|
|
638
|
+
*/
|
|
639
|
+
export function formatMoney(value: number, currency = '¥'): string {
|
|
640
|
+
return `${currency}${(value / 100).toLocaleString()}`
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### 类注释
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
/**
|
|
648
|
+
* HTTP 请求客户端
|
|
649
|
+
* 封装 axios,提供拦截器、错误处理、token 自动刷新等能力
|
|
650
|
+
*/
|
|
651
|
+
export class RequestClient {
|
|
652
|
+
// ...
|
|
653
|
+
}
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### 接口/类型注释
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
/**
|
|
660
|
+
* 用户基础信息
|
|
661
|
+
*/
|
|
662
|
+
export interface UserInfo {
|
|
663
|
+
/** 用户 ID */
|
|
664
|
+
id: number
|
|
665
|
+
/** 用户名(登录账号) */
|
|
666
|
+
username: string
|
|
667
|
+
/** 用户角色:admin=管理员,user=普通用户 */
|
|
668
|
+
role: 'admin' | 'user'
|
|
669
|
+
/** 邮箱地址(可选) */
|
|
670
|
+
email?: string
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* 分页查询参数
|
|
675
|
+
*/
|
|
676
|
+
export interface PageParams {
|
|
677
|
+
/** 当前页码,从 1 开始 */
|
|
678
|
+
page: number
|
|
679
|
+
/** 每页条数,默认 20 */
|
|
680
|
+
pageSize: number
|
|
681
|
+
}
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### 组件 Props/Emits 注释
|
|
685
|
+
|
|
686
|
+
```vue
|
|
687
|
+
<script setup lang="ts">
|
|
688
|
+
/**
|
|
689
|
+
* 数据表格组件的 Props 定义
|
|
690
|
+
*/
|
|
691
|
+
interface Props {
|
|
692
|
+
/** 表格数据源 */
|
|
693
|
+
data: Record<string, unknown>[]
|
|
694
|
+
/** 列配置 */
|
|
695
|
+
columns: TableColumn[]
|
|
696
|
+
/** 是否显示加载中 */
|
|
697
|
+
loading?: boolean
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const props = defineProps<Props>()
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* 组件事件定义
|
|
704
|
+
*/
|
|
705
|
+
const emit = defineEmits<{
|
|
706
|
+
/** 行点击事件,参数为当前行数据 */
|
|
707
|
+
rowClick: [row: Record<string, unknown>]
|
|
708
|
+
/** 选中行变化事件,参数为选中的行数组 */
|
|
709
|
+
selectionChange: [rows: Record<string, unknown>[]]
|
|
710
|
+
}>()
|
|
711
|
+
</script>
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### 组合式函数注释
|
|
715
|
+
|
|
716
|
+
```typescript
|
|
717
|
+
/**
|
|
718
|
+
* 列表数据管理 composable
|
|
719
|
+
* 封装列表加载、刷新、loading 状态
|
|
720
|
+
* @param fetchFn 数据获取函数
|
|
721
|
+
* @returns 列表数据、加载状态、刷新方法
|
|
722
|
+
*/
|
|
723
|
+
export function useList<T>(fetchFn: () => Promise<T[]>) {
|
|
724
|
+
// ...
|
|
725
|
+
}
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
### 可省略注释的场景
|
|
729
|
+
|
|
730
|
+
- 一目了然的 getter/setter(如 `get name() { return this._name }`)
|
|
731
|
+
- 测试文件中的 `describe` / `it` 块(用例名本身就是描述)
|
|
732
|
+
- 组件内部的简单辅助函数(< 5 行且逻辑清晰)
|
|
733
|
+
- 私有变量的简单赋值
|
|
734
|
+
|
|
735
|
+
### 禁止的写法
|
|
736
|
+
|
|
737
|
+
```typescript
|
|
738
|
+
// ❌ 无文件头注释
|
|
739
|
+
import { ref } from 'vue'
|
|
740
|
+
export function login() { ... }
|
|
741
|
+
|
|
742
|
+
// ❌ 方法无注释
|
|
743
|
+
export async function deleteUser(id: number) {
|
|
744
|
+
return request.delete(`/users/${id}`)
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// ❌ 接口字段无说明
|
|
748
|
+
export interface UserInfo {
|
|
749
|
+
id: number
|
|
750
|
+
username: string
|
|
751
|
+
role: 'admin' | 'user'
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// ❌ 英文注释(与代码风格不一致)
|
|
755
|
+
/** Get user info by id */
|
|
756
|
+
export async function getUser(id: number) { ... }
|
|
757
|
+
|
|
758
|
+
// ❌ 废话注释(只复述代码,不说明 why)
|
|
759
|
+
/** 设置 count 为 0 */
|
|
760
|
+
count.value = 0
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
### 注释质量要求
|
|
764
|
+
|
|
765
|
+
| 要求 | 说明 |
|
|
766
|
+
|------|------|
|
|
767
|
+
| 说明 why 而非 what | 代码已经说明 what,注释要解释为什么这样写 |
|
|
768
|
+
| 语言一致性 | 全部使用中文,不混用中英文 |
|
|
769
|
+
| 描述业务含义 | 避免"获取数据"这种泛泛说法,要写"获取用户列表" |
|
|
770
|
+
| 标注边界条件 | 参数范围、特殊取值、空值处理要说明 |
|
|
771
|
+
| 及时更新 | 代码修改后必须同步更新注释,禁止留下过时注释 |
|
|
772
|
+
|
|
773
|
+
## 十四、常见反模式(禁止)
|
|
697
774
|
|
|
698
775
|
| 反模式 | 正确做法 |
|
|
699
776
|
|--------|---------|
|
|
@@ -713,6 +790,67 @@ import { UserInfo, getUsers } from '@/api/user'
|
|
|
713
790
|
| `reactive()` 用于简单值 | `ref()` 用于所有场景 |
|
|
714
791
|
| 解构 store 不用 `storeToRefs` | 响应式值必须用 `storeToRefs` |
|
|
715
792
|
| 手写 CSS 为主 | Tailwind 工具类为主 |
|
|
793
|
+
| 测试文件放在 `src/` 内 | 放在独立 `tests/` 目录 |
|
|
794
|
+
| 测试中用相对路径回溯 `../../src/` | 用 `@` 别名导入 |
|
|
795
|
+
|
|
796
|
+
## 十五、代码简约原则
|
|
797
|
+
|
|
798
|
+
### 核心思想
|
|
799
|
+
|
|
800
|
+
代码只写**必要的逻辑**,不写"以防万一"的冗余代码。信任 TypeScript 类型系统和框架保证,只在系统边界(用户输入、外部 API)做校验。
|
|
801
|
+
|
|
802
|
+
### 禁止的冗余模式
|
|
803
|
+
|
|
804
|
+
```typescript
|
|
805
|
+
// ❌ 不必要的 fallback — 类型已保证
|
|
806
|
+
const name = props.title ?? '' // props.title 声明为 string,不可能是 null
|
|
807
|
+
const items = data.value || [] // ref<Item[]>([]) 初始就是数组
|
|
808
|
+
|
|
809
|
+
// ✅ 直接使用
|
|
810
|
+
const name = props.title
|
|
811
|
+
const items = data.value
|
|
812
|
+
|
|
813
|
+
// ❌ 不必要的可选链 — 值一定存在
|
|
814
|
+
const id = user?.id // user 来自必传 prop,不可能是 undefined
|
|
815
|
+
|
|
816
|
+
// ✅ 直接访问
|
|
817
|
+
const id = user.id
|
|
818
|
+
|
|
819
|
+
// ❌ 不必要的条件渲染守卫
|
|
820
|
+
<div v-if="list"> // list 是 ref<Item[]>([]),永远为真值
|
|
821
|
+
<div v-for="item in list" ...>
|
|
822
|
+
|
|
823
|
+
// ✅ 直接渲染
|
|
824
|
+
<div v-for="item in list" ...>
|
|
825
|
+
|
|
826
|
+
// ❌ 不必要的空函数 fallback
|
|
827
|
+
const onClick = props.onSubmit ?? (() => {})
|
|
828
|
+
|
|
829
|
+
// ✅ 条件调用
|
|
830
|
+
props.onSubmit?.()
|
|
831
|
+
|
|
832
|
+
// ❌ 不必要的 try-catch 吞异常
|
|
833
|
+
try {
|
|
834
|
+
await api.getUsers()
|
|
835
|
+
} catch {
|
|
836
|
+
// 空 catch,bug 永远不会被发现
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// ✅ 让异常暴露或给用户反馈
|
|
840
|
+
try {
|
|
841
|
+
await api.getUsers()
|
|
842
|
+
} catch {
|
|
843
|
+
toast.error('加载失败')
|
|
844
|
+
}
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
### 判断标准
|
|
848
|
+
|
|
849
|
+
写每一行防御代码前问自己:**这个情况在当前上下文下真的会发生吗?**
|
|
850
|
+
|
|
851
|
+
- 如果**会** → 写防御,加注释说明触发条件
|
|
852
|
+
- 如果**不会** → 不写,信任上游保证
|
|
853
|
+
- 如果**不确定** → 查看类型定义和调用链确认,不要"以防万一"
|
|
716
854
|
|
|
717
855
|
## 代码自检(写代码时强制执行)
|
|
718
856
|
|
|
@@ -734,5 +872,53 @@ import { UserInfo, getUsers } from '@/api/user'
|
|
|
734
872
|
| 12 | 无 `v-if` + `v-for` 同元素 | ✓ |
|
|
735
873
|
| 13 | 模板无复杂表达式,已提取为 `computed` | ✓ |
|
|
736
874
|
| 14 | 枚举用联合类型 `type X = 'a' | 'b'`,不用 `enum` | ✓ |
|
|
875
|
+
| 15 | 测试文件在 `tests/` 目录,不在 `src/` 内 | ✓ |
|
|
876
|
+
| 16 | `tsconfig.app.json` 的 include 不包含 `tests/` | ✓ |
|
|
877
|
+
| 17 | 文件顶部有中文概述注释,说明文件职责 | ✓ |
|
|
878
|
+
| 18 | 所有导出的方法/函数有中文注释(含参数、返回值) | ✓ |
|
|
879
|
+
| 19 | 所有导出的类/接口/类型有中文注释,接口字段逐个注释 | ✓ |
|
|
880
|
+
| 20 | Props/Emits 定义有中文注释说明每个字段/事件 | ✓ |
|
|
737
881
|
|
|
738
882
|
**不执行自检就提交代码 = 违规。**
|
|
883
|
+
|
|
884
|
+
---
|
|
885
|
+
|
|
886
|
+
## 错误处理(跨栈约束)
|
|
887
|
+
|
|
888
|
+
### 前端(Vue)
|
|
889
|
+
|
|
890
|
+
通过 ekit request 插件统一处理:
|
|
891
|
+
|
|
892
|
+
```typescript
|
|
893
|
+
// 在 api/client.ts 配置拦截器后,业务代码不需要 try/catch
|
|
894
|
+
// 401 → refreshToken 插件自动刷新,失败则跳登录
|
|
895
|
+
// 422 → unwrap 插件抛出错误,页面级 catch 显示 message
|
|
896
|
+
// 其他错误 → 全局错误提示
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
**禁止:** 每个 API 调用都包 try/catch,应在页面级或 store 级统一处理。
|
|
900
|
+
|
|
901
|
+
---
|
|
902
|
+
|
|
903
|
+
## 环境变量管理
|
|
904
|
+
|
|
905
|
+
### 前端(Vite)
|
|
906
|
+
|
|
907
|
+
```bash
|
|
908
|
+
# .env.development
|
|
909
|
+
VITE_API_BASE_URL=http://localhost:8000/api
|
|
910
|
+
|
|
911
|
+
# .env.production
|
|
912
|
+
VITE_API_BASE_URL=/api
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
代码中通过 `import.meta.env.VITE_API_BASE_URL` 访问。**只有 `VITE_` 前缀的变量会暴露给客户端。**
|
|
916
|
+
|
|
917
|
+
### 强制规则
|
|
918
|
+
|
|
919
|
+
| 规则 | 说明 |
|
|
920
|
+
|------|------|
|
|
921
|
+
| 每个项目必须有 `.env.example` | 列出所有变量名 + 注释说明,值用占位符 |
|
|
922
|
+
| `.env` 文件禁止提交 git | 已在 git-security.md 约束 |
|
|
923
|
+
| 禁止硬编码 | 数据库连接、密钥、API 地址等必须走环境变量 |
|
|
924
|
+
| 禁止运行时读取 `.env` 文件 | 后端用 BaseSettings(启动时加载),前端用 Vite 编译注入 |
|