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,122 +1,170 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Vue 3 + TS + Vite + Tailwind
|
|
2
|
+
description: Vue 3 + TS + Vite + Tailwind 应用层规范(强制使用 eui/ekit、业务聚合结构、组件 / TS / 路由 / API / 模板 / 样式 / 错误处理 / 注释)
|
|
3
3
|
paths:
|
|
4
4
|
- "**/*.vue"
|
|
5
5
|
- "**/vite.config.*"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# Vue 3
|
|
8
|
+
# Vue 3 应用层开发规范
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
适用于基于 Vue 3 + TypeScript + Vite + Tailwind CSS 的前端项目。基础库(组件 / 工具)规范见 `dwy-eui` / `dwy-ekit` skill;状态管理见 `dwy-vue-pinia` rule;测试见 `dwy-vue-testing` rule。
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 零、优先使用 eui / ekit 基础库(强制)
|
|
15
|
+
|
|
16
|
+
`@dwydev/eui` 与 `@dwydev/ekit` 是团队内部 npm 基础库,分别覆盖 UI 组件层与通用工具层。**凡是 eui / ekit 已提供的能力,禁止自行实现,也禁止引入其他库重复造轮子。**
|
|
17
|
+
|
|
18
|
+
### 必须复用的能力域
|
|
19
|
+
|
|
20
|
+
| 能力域 | 归属 |
|
|
21
|
+
|--------|------|
|
|
22
|
+
| 通用 UI 组件(Button / Input / Select / Dialog / Drawer / Form / Table 等) | eui |
|
|
23
|
+
| 主题 / 设计 tokens / 暗色模式 | eui |
|
|
24
|
+
| 表单校验(vee-validate + zod 集成) | eui + ekit |
|
|
25
|
+
| 中后台 / 落地页设计规范 | eui(references) |
|
|
26
|
+
| HTTP 客户端(拦截器 / token / 刷新 / 解包) | ekit |
|
|
27
|
+
| 本地存储 / Cookie | ekit(storage / cookie) |
|
|
28
|
+
| 日期 / 时间格式化 | ekit(date) |
|
|
29
|
+
| 数据校验 schema | ekit(validators) |
|
|
30
|
+
| PII 数据脱敏 | ekit(masking) |
|
|
31
|
+
| 剪贴板 / 文件下载 / qs 序列化 | ekit |
|
|
32
|
+
| VueUse 再导出(debounce / throttle / clickOutside / mediaQuery 等) | ekit(hooks) |
|
|
33
|
+
|
|
34
|
+
### 查阅方式(关键)
|
|
35
|
+
|
|
36
|
+
**在编写任何 Vue 代码前,以及调用任何 eui / ekit 提供的 API 之前,必须先用 `Skill` 工具加载对应 skill 查阅当前最新接口。** 本文件只规定约束与边界,**不固化**组件 props、函数签名 —— 这些会随版本演进,固化在 rule 里会与实际包不同步。
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
Skill 工具调用:
|
|
40
|
+
- skill="dwy-eui" 组件库 API 与设计规范
|
|
41
|
+
- skill="dwy-ekit" 工具库 API 与契约
|
|
42
|
+
```
|
|
11
43
|
|
|
12
44
|
### 强制规则
|
|
13
|
-
- 使用 `pnpm` 作为包管理器
|
|
14
|
-
- 使用 `Vite` 作为构建工具
|
|
15
|
-
- 必须有 `.node-version` 文件声明 Node 版本
|
|
16
|
-
- 必须有 `.npmrc` 配置安装源
|
|
17
45
|
|
|
18
|
-
|
|
46
|
+
- **禁止**业务代码出现 `axios` / `fetch` / `new XMLHttpRequest`,HTTP 走 ekit `createRequest`
|
|
47
|
+
- **禁止**直接 `localStorage.getItem` / `setItem` / `removeItem`,走 ekit storage / `useStorage`
|
|
48
|
+
- **禁止**直接 `document.cookie` 或单独引入 `js-cookie`,走 ekit cookie
|
|
49
|
+
- **禁止**业务代码 `new Date()` / `toLocaleString` / 直接 `import dayjs`,走 ekit date
|
|
50
|
+
- **禁止**手写 `navigator.clipboard` / `document.addEventListener('click', ...)` 监听,走 ekit copy / hooks
|
|
51
|
+
- **禁止**自定义已有 UI 组件(Button / Input / Select / Dialog / Form / Table 等),走 eui
|
|
52
|
+
- **禁止**自定义已有日期 / 时间 / 验证 / 脱敏 / 下载 工具函数,走 ekit
|
|
53
|
+
- 涉及任一能力域,**先查 skill 拿到当前 API,再写代码**
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 一、项目结构与业务聚合(强制)
|
|
58
|
+
|
|
59
|
+
### 核心原则
|
|
60
|
+
|
|
61
|
+
**按功能模块(feature / domain)聚合,禁止按技术层散落。** 同一业务功能的 view / components / store / api / types 必须集中在同一个功能目录下;**禁止**全局存在 `views/` / `components/` / `stores/` / `api/` 等顶级"技术层"目录把不同功能的同类文件混放。
|
|
62
|
+
|
|
63
|
+
### 为什么
|
|
64
|
+
|
|
65
|
+
- **跨项目迁移**:复制单个功能目录即可携带该功能全部代码;技术层散落需在 5+ 目录里翻找
|
|
66
|
+
- **可读性**:阅读功能时所有相关代码在同一目录,无需跨目录跳转
|
|
67
|
+
- **变更影响域清晰**:一次改动 diff 集中,code review 更高效
|
|
68
|
+
- **删除友好**:删功能时整个目录删掉即可,不会留下孤儿文件
|
|
69
|
+
|
|
70
|
+
### 标准结构(参考)
|
|
71
|
+
|
|
19
72
|
```
|
|
20
|
-
|
|
21
|
-
├──
|
|
22
|
-
│ ├──
|
|
23
|
-
│ ├──
|
|
24
|
-
│ ├──
|
|
25
|
-
│ ├──
|
|
26
|
-
│ │
|
|
27
|
-
│ ├──
|
|
28
|
-
│
|
|
29
|
-
│ ├──
|
|
30
|
-
│
|
|
31
|
-
│
|
|
32
|
-
│
|
|
33
|
-
│
|
|
34
|
-
│
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
│ ├──
|
|
38
|
-
│ ├──
|
|
39
|
-
│
|
|
40
|
-
|
|
41
|
-
├──
|
|
42
|
-
├──
|
|
43
|
-
|
|
44
|
-
├──
|
|
45
|
-
|
|
46
|
-
├── tsconfig.app.json
|
|
47
|
-
└── .npmrc
|
|
73
|
+
src/
|
|
74
|
+
├── modules/ # 业务模块根目录
|
|
75
|
+
│ ├── users/ # 用户功能
|
|
76
|
+
│ │ ├── api.ts # 该业务的 API(用 ekit createRequest 客户端)
|
|
77
|
+
│ │ ├── store.ts # 该业务的 Pinia store
|
|
78
|
+
│ │ ├── types.ts # 该业务的类型
|
|
79
|
+
│ │ ├── views/ # 该业务的页面组件
|
|
80
|
+
│ │ │ ├── UserListView.vue
|
|
81
|
+
│ │ │ └── UserDetailView.vue
|
|
82
|
+
│ │ ├── components/ # 该业务的内部组件
|
|
83
|
+
│ │ │ └── UserCard.vue
|
|
84
|
+
│ │ └── route.ts # 该业务的路由片段
|
|
85
|
+
│ └── orders/
|
|
86
|
+
│ ├── api.ts
|
|
87
|
+
│ ├── store.ts
|
|
88
|
+
│ └── views/
|
|
89
|
+
├── shared/ # 跨业务真正复用:UI 子组件 / composables / 常量
|
|
90
|
+
│ ├── components/
|
|
91
|
+
│ ├── composables/
|
|
92
|
+
│ └── constants/
|
|
93
|
+
├── core/ # 全局基础设施:HTTP client / 路由根 / 主题 / 启动
|
|
94
|
+
│ ├── http.ts # ekit createRequest 实例(全局唯一)
|
|
95
|
+
│ ├── router.ts
|
|
96
|
+
│ └── main.ts
|
|
97
|
+
├── App.vue
|
|
98
|
+
└── style.css
|
|
48
99
|
```
|
|
49
100
|
|
|
101
|
+
> 测试目录与 `src/` 并列、镜像业务聚合结构(详见 `dwy-vue-testing` rule)。
|
|
102
|
+
|
|
103
|
+
### 强制规则
|
|
104
|
+
|
|
105
|
+
- **禁止**顶级 `views/` / `components/` / `stores/` / `api/` / `types/` 把不同功能的同类文件混放
|
|
106
|
+
- 一个功能目录内文件名固定:`api.ts` / `store.ts` / `types.ts` / `route.ts`(单数、不带功能前缀)
|
|
107
|
+
- 跨业务真正共享的代码放 `shared/` 或 `core/`,**禁止**为"可能复用"把单一功能逻辑提前抽离
|
|
108
|
+
- 单文件超过约 400 行时再考虑拆分,**禁止**未到规模就预拆分
|
|
109
|
+
- 模块间引用通过明确 import,**禁止**循环依赖
|
|
110
|
+
|
|
111
|
+
### 例外
|
|
112
|
+
|
|
113
|
+
- 小型项目(< 3 个业务模块)允许扁平结构(`src/views/` + `src/components/`),但**不得**长大后保持扁平
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
50
117
|
## 二、组件规范
|
|
51
118
|
|
|
52
119
|
### 强制使用 Composition API + `<script setup>`
|
|
53
120
|
|
|
54
121
|
```vue
|
|
55
|
-
<!--
|
|
122
|
+
<!-- 正例 -->
|
|
56
123
|
<script setup lang="ts">
|
|
57
|
-
import { ref, computed
|
|
124
|
+
import { ref, computed } from 'vue'
|
|
58
125
|
|
|
59
126
|
const count = ref(0)
|
|
60
127
|
const doubled = computed(() => count.value * 2)
|
|
61
128
|
</script>
|
|
62
129
|
|
|
63
|
-
<!--
|
|
130
|
+
<!-- 反例:禁止 Options API -->
|
|
64
131
|
<script lang="ts">
|
|
65
132
|
export default {
|
|
66
133
|
data() { return { count: 0 } },
|
|
67
|
-
computed: { doubled() { return this.count * 2 } }
|
|
68
134
|
}
|
|
69
135
|
</script>
|
|
70
136
|
```
|
|
71
137
|
|
|
72
|
-
###
|
|
138
|
+
### 组件文件块顺序
|
|
73
139
|
|
|
74
140
|
```vue
|
|
75
141
|
<script setup lang="ts">
|
|
76
142
|
// 1. 类型导入
|
|
77
|
-
import type { UserInfo } from '@/types
|
|
143
|
+
import type { UserInfo } from '@/modules/users/types'
|
|
78
144
|
|
|
79
|
-
// 2.
|
|
145
|
+
// 2. 组件 / 工具导入
|
|
80
146
|
import { ref, computed, onMounted } from 'vue'
|
|
81
147
|
import { useRouter } from 'vue-router'
|
|
82
|
-
import {
|
|
83
|
-
|
|
84
|
-
// 3. Props/Emits 定义
|
|
85
|
-
const props = defineProps<{
|
|
86
|
-
userId: number
|
|
87
|
-
title?: string
|
|
88
|
-
}>()
|
|
148
|
+
import { EButton, EInput } from '@dwydev/eui'
|
|
89
149
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}>()
|
|
150
|
+
// 3. Props / Emits
|
|
151
|
+
const props = defineProps<{ userId: number; title?: string }>()
|
|
152
|
+
const emit = defineEmits<{ submit: [data: UserInfo]; cancel: [] }>()
|
|
94
153
|
|
|
95
|
-
// 4. Store/Router
|
|
154
|
+
// 4. Store / Router
|
|
96
155
|
const router = useRouter()
|
|
97
|
-
const authStore = useAuthStore()
|
|
98
156
|
|
|
99
157
|
// 5. 响应式状态
|
|
100
158
|
const loading = ref(false)
|
|
101
|
-
const formData = ref<UserInfo | null>(null)
|
|
102
159
|
|
|
103
160
|
// 6. 计算属性
|
|
104
|
-
const isValid = computed(() => !!
|
|
161
|
+
const isValid = computed(() => !!props.title)
|
|
105
162
|
|
|
106
163
|
// 7. 方法
|
|
107
|
-
async function handleSubmit() {
|
|
108
|
-
loading.value = true
|
|
109
|
-
try {
|
|
110
|
-
emit('submit', formData.value!)
|
|
111
|
-
} finally {
|
|
112
|
-
loading.value = false
|
|
113
|
-
}
|
|
114
|
-
}
|
|
164
|
+
async function handleSubmit() { /* ... */ }
|
|
115
165
|
|
|
116
166
|
// 8. 生命周期
|
|
117
|
-
onMounted(() => {
|
|
118
|
-
// ...
|
|
119
|
-
})
|
|
167
|
+
onMounted(() => { /* ... */ })
|
|
120
168
|
</script>
|
|
121
169
|
|
|
122
170
|
<template>
|
|
@@ -128,194 +176,101 @@ onMounted(() => {
|
|
|
128
176
|
|
|
129
177
|
| 类型 | 风格 | 示例 |
|
|
130
178
|
|------|------|------|
|
|
131
|
-
| 组件文件 | `PascalCase.vue` | `UserProfile.vue
|
|
132
|
-
| 页面组件 | `PascalCase` + View 后缀 | `DashboardView.vue
|
|
133
|
-
| 组合式函数 | `use` + PascalCase | `
|
|
179
|
+
| 组件文件 | `PascalCase.vue` | `UserProfile.vue` |
|
|
180
|
+
| 页面组件 | `PascalCase` + View 后缀 | `DashboardView.vue` |
|
|
181
|
+
| 组合式函数 | `use` + PascalCase | `useDictionary.ts` |
|
|
134
182
|
| Store 文件 | `camelCase.ts` | `auth.ts`、`menu.ts` |
|
|
135
|
-
|
|
|
136
|
-
|
|
|
137
|
-
|
|
|
138
|
-
|
|
|
139
|
-
|
|
|
140
|
-
|
|
|
141
|
-
|
|
|
142
|
-
| 布尔变量 | `is/has/can/should` 前缀 | `isLoading`、`hasPermission` |
|
|
143
|
-
|
|
144
|
-
### 禁止魔法字符串
|
|
183
|
+
| 类型文件 | `camelCase.ts` | `user.ts` |
|
|
184
|
+
| 工具文件 | `camelCase.ts` | `format.ts` |
|
|
185
|
+
| 变量 / 函数 | `camelCase` | `userName` / `handleSubmit` |
|
|
186
|
+
| 类型 / 接口 | `PascalCase` | `UserInfo` |
|
|
187
|
+
| 常量 | `UPPER_SNAKE_CASE` | `MAX_PAGE_SIZE` |
|
|
188
|
+
| 事件处理函数 | `handle` + 动作 | `handleLogin` |
|
|
189
|
+
| 布尔变量 | `is/has/can/should` 前缀 | `isLoading` / `hasPermission` |
|
|
145
190
|
|
|
146
|
-
|
|
191
|
+
### 魔法字符串提取常量
|
|
147
192
|
|
|
148
|
-
|
|
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/` 目录共享。
|
|
193
|
+
同一字符串字面量在文件内出现 ≥ 2 次时,**必须**提取为常量。跨文件复用的 key 放 `shared/constants/`。
|
|
174
194
|
|
|
175
195
|
### 禁止的写法
|
|
176
196
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// ✅ 有意义的名称
|
|
182
|
-
const userData = getData()
|
|
183
|
-
|
|
184
|
-
// ❌ 不带语义的布尔变量
|
|
185
|
-
const active = ref(true)
|
|
186
|
-
|
|
187
|
-
// ✅ 布尔变量带前缀
|
|
188
|
-
const isActive = ref(true)
|
|
189
|
-
|
|
190
|
-
// ❌ 在模板中使用复杂表达式
|
|
191
|
-
<div v-if="list.filter(i => i.status === 'active').length > 0">
|
|
197
|
+
- 单字母变量(循环索引除外)
|
|
198
|
+
- 不带语义的布尔变量(`const active = ref(true)` → `const isActive = ref(true)`)
|
|
199
|
+
- 模板中复杂表达式(提取为 `computed`)
|
|
192
200
|
|
|
193
|
-
|
|
194
|
-
const hasActiveItems = computed(() => list.value.some(i => i.status === 'active'))
|
|
195
|
-
```
|
|
201
|
+
---
|
|
196
202
|
|
|
197
203
|
## 三、TypeScript 规范
|
|
198
204
|
|
|
199
205
|
### 强制规则
|
|
200
|
-
|
|
201
|
-
-
|
|
202
|
-
-
|
|
203
|
-
-
|
|
206
|
+
|
|
207
|
+
- 所有 `.ts` / `.vue` 必须用 TypeScript
|
|
208
|
+
- **禁止** `any`,除非与外部动态库交互且无法确定类型;使用时必须附中文注释说明原因
|
|
209
|
+
- 类型导入用 `import type`
|
|
210
|
+
- 联合类型代替 `enum`:`type Status = 'active' | 'inactive'`
|
|
211
|
+
- 现代语法:`string[]` 而非 `Array<string>`
|
|
204
212
|
|
|
205
213
|
### 类型标注
|
|
206
214
|
|
|
207
215
|
```typescript
|
|
208
|
-
//
|
|
209
|
-
const props = defineProps<{
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// ✅ Emits 使用泛型定义
|
|
216
|
-
const emit = defineEmits<{
|
|
217
|
-
change: [value: string]
|
|
218
|
-
submit: [data: FormData]
|
|
219
|
-
}>()
|
|
220
|
-
|
|
221
|
-
// ✅ ref 使用泛型
|
|
216
|
+
// Props 泛型
|
|
217
|
+
const props = defineProps<{ title: string; items: UserInfo[] }>()
|
|
218
|
+
|
|
219
|
+
// Emits 泛型
|
|
220
|
+
const emit = defineEmits<{ change: [value: string] }>()
|
|
221
|
+
|
|
222
|
+
// ref 泛型(复杂类型)
|
|
222
223
|
const user = ref<UserInfo | null>(null)
|
|
223
224
|
const list = ref<UserInfo[]>([])
|
|
224
225
|
|
|
225
|
-
//
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
// ❌ 不标注类型
|
|
229
|
-
const user = ref(null)
|
|
230
|
-
const list = ref([])
|
|
226
|
+
// 反例
|
|
227
|
+
const user = ref(null) // 类型推为 Ref<null>
|
|
228
|
+
const list = ref([]) // 类型推为 Ref<never[]>
|
|
231
229
|
```
|
|
232
230
|
|
|
233
|
-
###
|
|
234
|
-
|
|
235
|
-
```typescript
|
|
236
|
-
// ✅ 共享类型放 types/ 目录
|
|
237
|
-
// types/user.ts
|
|
238
|
-
export interface UserInfo {
|
|
239
|
-
id: number
|
|
240
|
-
username: string
|
|
241
|
-
role: 'admin' | 'user'
|
|
242
|
-
email?: string
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
export interface UserCreate {
|
|
246
|
-
username: string
|
|
247
|
-
password: string
|
|
248
|
-
role: 'admin' | 'user'
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// ✅ 组件内部类型放 script setup 里
|
|
252
|
-
interface TableColumn {
|
|
253
|
-
key: string
|
|
254
|
-
label: string
|
|
255
|
-
sortable?: boolean
|
|
256
|
-
}
|
|
257
|
-
```
|
|
231
|
+
### 类型组织
|
|
258
232
|
|
|
259
|
-
|
|
233
|
+
- 跨业务共享类型放 `shared/types/`
|
|
234
|
+
- 业务内部类型放该业务的 `types.ts`
|
|
235
|
+
- 组件内部一次性类型直接在 `<script setup>` 中声明
|
|
260
236
|
|
|
261
|
-
|
|
262
|
-
|--------|--------------|
|
|
263
|
-
| `Array<string>` | `string[]` |
|
|
264
|
-
| 枚举 `enum Status {}` | 联合类型 `type Status = 'active' \| 'inactive'` |
|
|
237
|
+
---
|
|
265
238
|
|
|
266
|
-
##
|
|
239
|
+
## 四、路由规范
|
|
267
240
|
|
|
268
241
|
### 强制规则
|
|
269
242
|
|
|
243
|
+
- 路由组件**必须** `() => import()` 懒加载
|
|
244
|
+
- 认证守卫在 `router.beforeEach`,**禁止**在组件中检查
|
|
245
|
+
- meta 字段必须通过 `RouteMeta` 类型扩展声明
|
|
246
|
+
- 路径用 **kebab-case**:`/user-profile`,**禁止** `camelCase`
|
|
247
|
+
|
|
270
248
|
```typescript
|
|
249
|
+
// core/router.ts
|
|
271
250
|
import { createRouter, createWebHistory } from 'vue-router'
|
|
272
251
|
|
|
273
252
|
const router = createRouter({
|
|
274
253
|
history: createWebHistory(),
|
|
275
254
|
routes: [
|
|
276
|
-
{
|
|
277
|
-
path: '/login',
|
|
278
|
-
name: 'Login',
|
|
279
|
-
component: () => import('@/views/LoginView.vue'),
|
|
280
|
-
},
|
|
281
255
|
{
|
|
282
256
|
path: '/dashboard',
|
|
283
257
|
name: 'Dashboard',
|
|
284
|
-
component: () => import('@/views/DashboardView.vue'),
|
|
258
|
+
component: () => import('@/modules/dashboard/views/DashboardView.vue'),
|
|
285
259
|
meta: { requiresAuth: true },
|
|
286
260
|
},
|
|
287
261
|
],
|
|
288
262
|
})
|
|
289
|
-
```
|
|
290
263
|
|
|
291
|
-
| 规则 | 说明 |
|
|
292
|
-
|------|------|
|
|
293
|
-
| 懒加载 | 所有路由组件必须使用 `() => import()` 懒加载 |
|
|
294
|
-
| 路由守卫 | 认证检查放 `router.beforeEach`,不在组件中检查 |
|
|
295
|
-
| meta 类型 | 使用 `RouteMeta` 类型扩展声明 meta 字段 |
|
|
296
|
-
| 路径命名 | 使用 kebab-case:`/user-profile`,不用 camelCase |
|
|
297
|
-
|
|
298
|
-
### 路由守卫
|
|
299
|
-
|
|
300
|
-
```typescript
|
|
301
|
-
// ✅ 全局前置守卫
|
|
302
264
|
router.beforeEach((to) => {
|
|
303
265
|
const authStore = useAuthStore()
|
|
304
|
-
|
|
305
266
|
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
|
|
306
267
|
return { name: 'Login' }
|
|
307
268
|
}
|
|
308
|
-
|
|
309
|
-
if (to.meta.requiresAdmin && !authStore.isAdmin) {
|
|
310
|
-
return { name: 'Dashboard' }
|
|
311
|
-
}
|
|
312
269
|
})
|
|
313
270
|
```
|
|
314
271
|
|
|
315
|
-
### 路由 Meta 类型声明
|
|
316
|
-
|
|
317
272
|
```typescript
|
|
318
|
-
// types/router.d.ts
|
|
273
|
+
// shared/types/router.d.ts
|
|
319
274
|
import 'vue-router'
|
|
320
275
|
|
|
321
276
|
declare module 'vue-router' {
|
|
@@ -327,144 +282,130 @@ declare module 'vue-router' {
|
|
|
327
282
|
}
|
|
328
283
|
```
|
|
329
284
|
|
|
330
|
-
|
|
285
|
+
---
|
|
331
286
|
|
|
332
|
-
|
|
287
|
+
## 五、组合式函数(Composables)
|
|
333
288
|
|
|
334
|
-
-
|
|
335
|
-
-
|
|
289
|
+
- 文件名与函数名以 `use` 开头
|
|
290
|
+
- 返回**对象**(非数组),便于解构与按需取
|
|
336
291
|
|
|
337
292
|
```typescript
|
|
338
|
-
// composables/useList.ts
|
|
339
|
-
|
|
340
|
-
// ✅ 正确
|
|
341
293
|
export function useList<T>(fetchFn: () => Promise<T[]>) {
|
|
342
294
|
const items = ref<T[]>([])
|
|
343
295
|
const loading = ref(false)
|
|
344
|
-
|
|
345
296
|
async function refresh() {
|
|
346
297
|
loading.value = true
|
|
347
|
-
try {
|
|
348
|
-
items.value = await fetchFn()
|
|
349
|
-
} finally {
|
|
350
|
-
loading.value = false
|
|
351
|
-
}
|
|
298
|
+
try { items.value = await fetchFn() } finally { loading.value = false }
|
|
352
299
|
}
|
|
353
|
-
|
|
354
300
|
onMounted(refresh)
|
|
355
|
-
|
|
356
301
|
return { items, loading, refresh }
|
|
357
302
|
}
|
|
358
|
-
|
|
359
|
-
// 使用
|
|
360
|
-
const { items: users, loading, refresh } = useList(() => getUsers())
|
|
361
303
|
```
|
|
362
304
|
|
|
363
|
-
|
|
305
|
+
通用 hooks(debounce / throttle / clickOutside / mediaQuery 等)**禁止**自己造,走 ekit hooks(具体 API 查 `dwy-ekit` skill)。
|
|
364
306
|
|
|
365
|
-
|
|
366
|
-
// ❌ 不以 use 开头
|
|
367
|
-
export function fetchList() { ... }
|
|
307
|
+
---
|
|
368
308
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
```
|
|
309
|
+
## 六、API 请求规范
|
|
310
|
+
|
|
311
|
+
### 强制规则
|
|
374
312
|
|
|
375
|
-
|
|
313
|
+
- HTTP 客户端**必须**用 ekit `createRequest` 创建,全局唯一实例放 `core/http.ts`(具体参数与插件查 `dwy-ekit` skill)
|
|
314
|
+
- **禁止**业务代码 `axios.create()` / `fetch()` / `new XMLHttpRequest()`
|
|
315
|
+
- 每个业务模块的 API 函数集中在该业务目录的 `api.ts`,按业务域隔离
|
|
316
|
+
- 每个 API 函数**独立导出**,**禁止** `default export`
|
|
317
|
+
- 请求参数 / 响应**必须**类型标注
|
|
318
|
+
- 响应解包契约与 dwyeapi 对齐(`{ code, message, data }`),具体类型与守卫查 `dwy-ekit` skill
|
|
376
319
|
|
|
377
|
-
###
|
|
320
|
+
### 示例
|
|
378
321
|
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
<div v-else>{{ data }}</div>
|
|
322
|
+
```typescript
|
|
323
|
+
// core/http.ts —— 全局唯一 HTTP 客户端(具体 plugins 查 dwy-ekit skill)
|
|
324
|
+
import { createRequest } from '@dwydev/ekit'
|
|
383
325
|
|
|
384
|
-
|
|
385
|
-
|
|
326
|
+
export const request = createRequest({
|
|
327
|
+
baseURL: import.meta.env.VITE_API_BASE_URL,
|
|
328
|
+
plugins: [
|
|
329
|
+
/* tokenPlugin / refreshTokenPlugin / unwrapPlugin —— 具体用法查 dwy-ekit skill */
|
|
330
|
+
],
|
|
331
|
+
})
|
|
386
332
|
|
|
387
|
-
|
|
388
|
-
|
|
333
|
+
// modules/users/api.ts —— 该业务的 API
|
|
334
|
+
import { request } from '@/core/http'
|
|
335
|
+
import type { UserInfo, UserCreate } from './types'
|
|
389
336
|
|
|
390
|
-
|
|
391
|
-
<
|
|
392
|
-
|
|
337
|
+
export function getUsers(params: { page: number; page_size: number }) {
|
|
338
|
+
return request.get<{ items: UserInfo[]; total: number }>('/users', { params })
|
|
339
|
+
}
|
|
393
340
|
|
|
394
|
-
|
|
341
|
+
export function createUser(data: UserCreate) {
|
|
342
|
+
return request.post<UserInfo>('/users', data)
|
|
343
|
+
}
|
|
344
|
+
```
|
|
395
345
|
|
|
396
|
-
|
|
397
|
-
<!-- ✅ 方法引用(无参数时) -->
|
|
398
|
-
<button @click="handleSubmit">提交</button>
|
|
346
|
+
### 错误处理契约
|
|
399
347
|
|
|
400
|
-
|
|
401
|
-
|
|
348
|
+
- 401 / 422 / 业务错误的统一处理由 ekit request 插件完成(具体 API 查 `dwy-ekit` skill)
|
|
349
|
+
- 业务侧 catch 用 ekit 提供的 `isApiBusinessError` 类型守卫做分支(`NOT_FOUND` / `VALIDATION_ERROR` / `PERMISSION_DENIED` 等)
|
|
350
|
+
- **禁止**每个 API 调用都包 `try/catch`,只在页面级或 store 级统一处理
|
|
402
351
|
|
|
403
|
-
|
|
404
|
-
<button @click="loading = true; api.delete(id).then(() => refresh())">删除</button>
|
|
405
|
-
```
|
|
352
|
+
---
|
|
406
353
|
|
|
407
|
-
|
|
354
|
+
## 七、模板规范
|
|
408
355
|
|
|
409
356
|
```vue
|
|
410
|
-
<!--
|
|
411
|
-
<
|
|
357
|
+
<!-- v-if / v-for 不在同元素,v-for 必须带 :key -->
|
|
358
|
+
<div v-for="item in activeList" :key="item.id">{{ item.name }}</div>
|
|
412
359
|
|
|
413
|
-
<!--
|
|
414
|
-
<
|
|
360
|
+
<!-- 事件:方法引用 / 传参用箭头函数 -->
|
|
361
|
+
<button @click="handleSubmit">提交</button>
|
|
362
|
+
<button @click="() => handleDelete(item.id)">删除</button>
|
|
415
363
|
|
|
416
|
-
<!--
|
|
417
|
-
<
|
|
364
|
+
<!-- 组件 PascalCase -->
|
|
365
|
+
<UserCard :user="user" @click="handleSelect" />
|
|
418
366
|
|
|
419
|
-
<!--
|
|
367
|
+
<!-- 双向绑定优先 v-model -->
|
|
420
368
|
<EInput v-model="form.name" placeholder="请输入" />
|
|
421
369
|
```
|
|
422
370
|
|
|
423
|
-
|
|
371
|
+
**禁止**:
|
|
372
|
+
- `v-if` 与 `v-for` 同元素
|
|
373
|
+
- 模板中复杂表达式 / 内联多语句
|
|
374
|
+
- `kebab-case` 组件名 / 模板中写 `.value`
|
|
424
375
|
|
|
425
|
-
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## 八、样式规范
|
|
379
|
+
|
|
380
|
+
### 强制使用 Tailwind CSS 4
|
|
426
381
|
|
|
427
382
|
```vue
|
|
428
|
-
<!--
|
|
383
|
+
<!-- 正例 -->
|
|
429
384
|
<div class="flex items-center gap-4 p-6 rounded-lg bg-card">
|
|
430
385
|
|
|
431
|
-
<!--
|
|
386
|
+
<!-- 反例 -->
|
|
432
387
|
<div style="display: flex; padding: 24px;">
|
|
433
|
-
|
|
434
|
-
<!-- ❌ 使用 scoped CSS(除非确实需要) -->
|
|
435
|
-
<style scoped>
|
|
436
|
-
.container { display: flex; }
|
|
437
|
-
</style>
|
|
438
388
|
```
|
|
439
389
|
|
|
440
390
|
### 允许 scoped CSS 的场景
|
|
441
391
|
|
|
442
|
-
-
|
|
392
|
+
- 第三方 / eui 内部样式覆盖(`:deep(...)`)
|
|
443
393
|
- 复杂动画定义
|
|
444
|
-
- 无法用 Tailwind
|
|
445
|
-
|
|
446
|
-
```vue
|
|
447
|
-
<!-- ✅ 覆盖第三方组件样式时可以用 scoped -->
|
|
448
|
-
<style scoped>
|
|
449
|
-
:deep(.el-input__inner) {
|
|
450
|
-
border-radius: 8px;
|
|
451
|
-
}
|
|
452
|
-
</style>
|
|
453
|
-
```
|
|
394
|
+
- 无法用 Tailwind 表达的样式
|
|
454
395
|
|
|
455
|
-
###
|
|
396
|
+
### 响应式与暗色
|
|
456
397
|
|
|
457
398
|
```vue
|
|
458
|
-
<!--
|
|
399
|
+
<!-- mobile-first -->
|
|
459
400
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
460
401
|
|
|
461
|
-
<!--
|
|
402
|
+
<!-- 暗色用 dark: 前缀 -->
|
|
462
403
|
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
|
463
404
|
```
|
|
464
405
|
|
|
465
|
-
|
|
406
|
+
---
|
|
466
407
|
|
|
467
|
-
|
|
408
|
+
## 九、Vite 配置
|
|
468
409
|
|
|
469
410
|
```typescript
|
|
470
411
|
// vite.config.ts
|
|
@@ -476,17 +417,12 @@ import tailwindcss from '@tailwindcss/vite'
|
|
|
476
417
|
export default defineConfig({
|
|
477
418
|
plugins: [vue(), tailwindcss()],
|
|
478
419
|
resolve: {
|
|
479
|
-
alias: {
|
|
480
|
-
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
|
481
|
-
},
|
|
420
|
+
alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },
|
|
482
421
|
},
|
|
483
422
|
server: {
|
|
484
423
|
port: 5173,
|
|
485
424
|
proxy: {
|
|
486
|
-
'/api': {
|
|
487
|
-
target: 'http://127.0.0.1:8000',
|
|
488
|
-
changeOrigin: true,
|
|
489
|
-
},
|
|
425
|
+
'/api': { target: 'http://127.0.0.1:8000', changeOrigin: true },
|
|
490
426
|
},
|
|
491
427
|
},
|
|
492
428
|
})
|
|
@@ -494,431 +430,191 @@ export default defineConfig({
|
|
|
494
430
|
|
|
495
431
|
| 规则 | 说明 |
|
|
496
432
|
|------|------|
|
|
497
|
-
| 路径别名 |
|
|
498
|
-
| API 代理 |
|
|
499
|
-
| 端口 | 默认 5173,可在 `.env`
|
|
433
|
+
| 路径别名 | 必须配 `@` → `src/` |
|
|
434
|
+
| API 代理 | 开发环境必须配 `/api` 代理到后端 |
|
|
435
|
+
| 端口 | 默认 5173,可在 `.env` 覆盖 |
|
|
500
436
|
|
|
501
|
-
|
|
437
|
+
---
|
|
502
438
|
|
|
503
|
-
|
|
439
|
+
## 十、错误处理
|
|
504
440
|
|
|
505
441
|
```typescript
|
|
506
|
-
//
|
|
442
|
+
// 异步操作必须有 try / catch 与 loading
|
|
507
443
|
async function handleSubmit() {
|
|
508
444
|
loading.value = true
|
|
509
445
|
try {
|
|
510
446
|
await createUser(formData.value)
|
|
511
447
|
message.success('创建成功')
|
|
512
|
-
|
|
513
|
-
} catch (err) {
|
|
448
|
+
} catch {
|
|
514
449
|
message.error('创建失败')
|
|
515
450
|
} finally {
|
|
516
451
|
loading.value = false
|
|
517
452
|
}
|
|
518
453
|
}
|
|
454
|
+
```
|
|
519
455
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
router.push('/users')
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// ❌ 空 catch
|
|
527
|
-
try { ... } catch {}
|
|
456
|
+
- 401 / 422 / 业务错误的拦截走 ekit request 插件(查 `dwy-ekit` skill),**禁止**每个 API 都包 try-catch
|
|
457
|
+
- **禁止**空 catch(`try { ... } catch {}` 吞异常)
|
|
458
|
+
- 异步操作**必须**有 loading 状态
|
|
528
459
|
|
|
529
|
-
|
|
530
|
-
try { ... } catch { message.error('操作失败') }
|
|
531
|
-
```
|
|
460
|
+
---
|
|
532
461
|
|
|
533
|
-
|
|
462
|
+
## 十一、环境变量
|
|
534
463
|
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
|
|
464
|
+
```bash
|
|
465
|
+
# .env.development
|
|
466
|
+
VITE_API_BASE_URL=http://localhost:8000/api
|
|
538
467
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
try {
|
|
542
|
-
data.value = await getData()
|
|
543
|
-
} finally {
|
|
544
|
-
loading.value = false
|
|
545
|
-
}
|
|
546
|
-
}
|
|
468
|
+
# .env.production
|
|
469
|
+
VITE_API_BASE_URL=/api
|
|
547
470
|
```
|
|
548
471
|
|
|
472
|
+
| 规则 | 说明 |
|
|
473
|
+
|------|------|
|
|
474
|
+
| `.env.example` | 必须存在,列出所有变量名 + 注释 |
|
|
475
|
+
| `.env` | **禁止**提交 git |
|
|
476
|
+
| 命名 | 前端变量必须 `VITE_` 前缀才暴露给客户端 |
|
|
477
|
+
| 硬编码 | **禁止**硬编码 API 地址 / 密钥 |
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
549
481
|
## 十二、导入规范
|
|
550
482
|
|
|
551
|
-
###
|
|
483
|
+
### 顺序(组间空行)
|
|
552
484
|
|
|
553
485
|
```typescript
|
|
554
|
-
// 1. Vue 核心
|
|
555
|
-
import { ref, computed
|
|
556
|
-
import { useRouter
|
|
486
|
+
// 1. Vue / vue-router 核心
|
|
487
|
+
import { ref, computed } from 'vue'
|
|
488
|
+
import { useRouter } from 'vue-router'
|
|
557
489
|
|
|
558
|
-
// 2.
|
|
490
|
+
// 2. 第三方
|
|
559
491
|
import { storeToRefs } from 'pinia'
|
|
560
|
-
import axios from 'axios'
|
|
561
492
|
|
|
562
|
-
// 3.
|
|
493
|
+
// 3. @dwydev/* 内部包
|
|
563
494
|
import { EButton, EInput } from '@dwydev/eui'
|
|
495
|
+
import { request } from '@dwydev/ekit'
|
|
564
496
|
|
|
565
|
-
// 4.
|
|
566
|
-
import { useAuthStore } from '@/
|
|
567
|
-
import {
|
|
568
|
-
import type { UserInfo } from '@/types/user'
|
|
497
|
+
// 4. 项目内(@ 别名)
|
|
498
|
+
import { useAuthStore } from '@/modules/users/store'
|
|
499
|
+
import type { UserInfo } from '@/modules/users/types'
|
|
569
500
|
```
|
|
570
501
|
|
|
571
502
|
### 强制规则
|
|
572
503
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
import { useAuthStore } from '@/stores/auth'
|
|
576
|
-
|
|
577
|
-
// ❌ 使用相对路径(超过 2 层)
|
|
578
|
-
import { useAuthStore } from '../../../stores/auth'
|
|
579
|
-
|
|
580
|
-
// ✅ 类型导入使用 import type
|
|
581
|
-
import type { UserInfo } from '@/types/user'
|
|
504
|
+
- **必须**用 `@` 别名,**禁止**超过 2 层的相对路径回溯
|
|
505
|
+
- 类型用 `import type`,**禁止**与值混合导入
|
|
582
506
|
|
|
583
|
-
|
|
584
|
-
import { UserInfo, getUsers } from '@/api/user'
|
|
585
|
-
```
|
|
507
|
+
---
|
|
586
508
|
|
|
587
509
|
## 十三、注释规范
|
|
588
510
|
|
|
589
511
|
### 强制规则
|
|
590
512
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
-
```
|
|
513
|
+
1. 文件顶部**必须**有中文概述注释,说明文件职责
|
|
514
|
+
2. 所有**导出**的方法 / 函数 / 类 / 接口 / 类型**必须**有中文注释
|
|
515
|
+
3. 接口字段**必须**逐个 `/** */` 注释
|
|
516
|
+
4. 注释说明 **why**(动机 / 边界 / 约束),不复述代码做了什么
|
|
517
|
+
5. 全部使用**中文**,**禁止**混用中英文
|
|
518
|
+
6. 代码改了,注释**必须**同步更新
|
|
655
519
|
|
|
656
|
-
###
|
|
520
|
+
### 示例
|
|
657
521
|
|
|
658
522
|
```typescript
|
|
659
523
|
/**
|
|
660
524
|
* 用户基础信息
|
|
661
525
|
*/
|
|
662
526
|
export interface UserInfo {
|
|
663
|
-
/** 用户
|
|
664
|
-
|
|
527
|
+
/** 用户 UUID(对外标识,禁止暴露内部自增 id) */
|
|
528
|
+
uuid: string
|
|
665
529
|
/** 用户名(登录账号) */
|
|
666
530
|
username: string
|
|
667
|
-
/**
|
|
531
|
+
/** 角色:admin=管理员,user=普通用户 */
|
|
668
532
|
role: 'admin' | 'user'
|
|
669
|
-
/** 邮箱地址(可选) */
|
|
670
|
-
email?: string
|
|
671
533
|
}
|
|
672
534
|
|
|
673
535
|
/**
|
|
674
|
-
*
|
|
675
|
-
|
|
676
|
-
|
|
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 列表数据、加载状态、刷新方法
|
|
536
|
+
* 格式化金额显示(输入单位为分,输出元)
|
|
537
|
+
* @param value 金额数值,单位:分
|
|
538
|
+
* @param currency 货币符号,默认 ¥
|
|
722
539
|
*/
|
|
723
|
-
export function
|
|
724
|
-
|
|
540
|
+
export function formatMoney(value: number, currency = '¥'): string {
|
|
541
|
+
return `${currency}${(value / 100).toLocaleString()}`
|
|
725
542
|
}
|
|
726
543
|
```
|
|
727
544
|
|
|
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
|
-
}
|
|
545
|
+
### 可省略
|
|
753
546
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
547
|
+
- 一目了然的 getter / setter
|
|
548
|
+
- 测试 `describe` / `it` 块(用例名即描述)
|
|
549
|
+
- 组件内部 < 5 行的简单辅助函数
|
|
757
550
|
|
|
758
|
-
|
|
759
|
-
/** 设置 count 为 0 */
|
|
760
|
-
count.value = 0
|
|
761
|
-
```
|
|
551
|
+
---
|
|
762
552
|
|
|
763
|
-
|
|
553
|
+
## 十四、代码简约原则
|
|
764
554
|
|
|
765
|
-
|
|
766
|
-
|------|------|
|
|
767
|
-
| 说明 why 而非 what | 代码已经说明 what,注释要解释为什么这样写 |
|
|
768
|
-
| 语言一致性 | 全部使用中文,不混用中英文 |
|
|
769
|
-
| 描述业务含义 | 避免"获取数据"这种泛泛说法,要写"获取用户列表" |
|
|
770
|
-
| 标注边界条件 | 参数范围、特殊取值、空值处理要说明 |
|
|
771
|
-
| 及时更新 | 代码修改后必须同步更新注释,禁止留下过时注释 |
|
|
772
|
-
|
|
773
|
-
## 十四、常见反模式(禁止)
|
|
774
|
-
|
|
775
|
-
| 反模式 | 正确做法 |
|
|
776
|
-
|--------|---------|
|
|
777
|
-
| Options API | Composition API + `<script setup>` |
|
|
778
|
-
| Pinia Options Store | Setup Store(箭头函数) |
|
|
779
|
-
| `this.$refs` | `ref()` + 模板 ref |
|
|
780
|
-
| `this.$emit` | `defineEmits<>()` |
|
|
781
|
-
| `this.$router` | `useRouter()` |
|
|
782
|
-
| `this.$route` | `useRoute()` |
|
|
783
|
-
| Vuex | Pinia |
|
|
784
|
-
| Mixins | Composables(`use*`) |
|
|
785
|
-
| Event Bus | Pinia store 或 `provide/inject` |
|
|
786
|
-
| `v-if` + `v-for` 同元素 | computed 过滤后 `v-for` |
|
|
787
|
-
| scoped CSS 为主 | Tailwind CSS 为主 |
|
|
788
|
-
| 组件中直接调 axios | 通过 `api/` 模块调用 |
|
|
789
|
-
| `.value` 在模板中 | 模板自动解包,不需要 `.value` |
|
|
790
|
-
| `reactive()` 用于简单值 | `ref()` 用于所有场景 |
|
|
791
|
-
| 解构 store 不用 `storeToRefs` | 响应式值必须用 `storeToRefs` |
|
|
792
|
-
| 手写 CSS 为主 | Tailwind 工具类为主 |
|
|
793
|
-
| 测试文件放在 `src/` 内 | 放在独立 `tests/` 目录 |
|
|
794
|
-
| 测试中用相对路径回溯 `../../src/` | 用 `@` 别名导入 |
|
|
795
|
-
|
|
796
|
-
## 十五、代码简约原则
|
|
797
|
-
|
|
798
|
-
### 核心思想
|
|
799
|
-
|
|
800
|
-
代码只写**必要的逻辑**,不写"以防万一"的冗余代码。信任 TypeScript 类型系统和框架保证,只在系统边界(用户输入、外部 API)做校验。
|
|
555
|
+
代码只写**必要的逻辑**,不写"以防万一"的冗余代码。信任 TypeScript 类型系统与框架保证,只在系统边界(用户输入、外部 API)做校验。
|
|
801
556
|
|
|
802
557
|
### 禁止的冗余模式
|
|
803
558
|
|
|
804
559
|
```typescript
|
|
805
|
-
//
|
|
806
|
-
const name = props.title ?? '' // props.title
|
|
807
|
-
const items = data.value || [] // ref<Item[]>([]) 初始就是数组
|
|
560
|
+
// 反例:类型已保证非空,多余 fallback
|
|
561
|
+
const name = props.title ?? '' // props.title: string
|
|
808
562
|
|
|
809
|
-
//
|
|
563
|
+
// 正例
|
|
810
564
|
const name = props.title
|
|
811
|
-
const items = data.value
|
|
812
565
|
|
|
813
|
-
//
|
|
814
|
-
const id = user?.id // user 来自必传 prop
|
|
566
|
+
// 反例:值一定存在
|
|
567
|
+
const id = user?.id // user 来自必传 prop
|
|
815
568
|
|
|
816
|
-
//
|
|
569
|
+
// 正例
|
|
817
570
|
const id = user.id
|
|
818
571
|
|
|
819
|
-
//
|
|
820
|
-
|
|
821
|
-
<div v-for="item in list" ...>
|
|
822
|
-
|
|
823
|
-
// ✅ 直接渲染
|
|
824
|
-
<div v-for="item in list" ...>
|
|
825
|
-
|
|
826
|
-
// ❌ 不必要的空函数 fallback
|
|
827
|
-
const onClick = props.onSubmit ?? (() => {})
|
|
572
|
+
// 反例:空 catch 吞异常
|
|
573
|
+
try { await api.getUsers() } catch {}
|
|
828
574
|
|
|
829
|
-
//
|
|
830
|
-
|
|
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
|
-
}
|
|
575
|
+
// 正例:让异常暴露或给用户反馈
|
|
576
|
+
try { await api.getUsers() } catch { toast.error('加载失败') }
|
|
845
577
|
```
|
|
846
578
|
|
|
847
579
|
### 判断标准
|
|
848
580
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
- 如果**会** → 写防御,加注释说明触发条件
|
|
852
|
-
- 如果**不会** → 不写,信任上游保证
|
|
853
|
-
- 如果**不确定** → 查看类型定义和调用链确认,不要"以防万一"
|
|
854
|
-
|
|
855
|
-
## 代码自检(写代码时强制执行)
|
|
856
|
-
|
|
857
|
-
**每次生成或修改 Vue/TypeScript 代码后,必须逐条验证以下清单。任何一条未通过 → STOP,立即修正后再继续。**
|
|
858
|
-
|
|
859
|
-
| # | 检查项 | 违规即 STOP |
|
|
860
|
-
|---|--------|------------|
|
|
861
|
-
| 1 | 使用 `<script setup lang="ts">`,不是 Options API | ✓ |
|
|
862
|
-
| 2 | Props 用 `defineProps<{}>()`,Emits 用 `defineEmits<{}>()` | ✓ |
|
|
863
|
-
| 3 | 无 `any` 类型(除非有注释说明原因) | ✓ |
|
|
864
|
-
| 4 | `ref()` 复杂类型有泛型标注(`ref<T>` 而非 `ref(null)`) | ✓ |
|
|
865
|
-
| 5 | Pinia 使用 Setup Store,不是 Options Store | ✓ |
|
|
866
|
-
| 6 | Store 解构用 `storeToRefs`(响应式值),方法直接解构 | ✓ |
|
|
867
|
-
| 7 | 类型导入用 `import type { ... }` | ✓ |
|
|
868
|
-
| 8 | 路由组件使用 `() => import()` 懒加载 | ✓ |
|
|
869
|
-
| 9 | API 调用通过 `api/` 模块,未在组件中直接 `axios.get` | ✓ |
|
|
870
|
-
| 10 | 布尔变量有 `is/has/can/should` 前缀 | ✓ |
|
|
871
|
-
| 11 | 样式以 Tailwind 为主,非必要不写 scoped CSS | ✓ |
|
|
872
|
-
| 12 | 无 `v-if` + `v-for` 同元素 | ✓ |
|
|
873
|
-
| 13 | 模板无复杂表达式,已提取为 `computed` | ✓ |
|
|
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 定义有中文注释说明每个字段/事件 | ✓ |
|
|
881
|
-
|
|
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
|
-
```
|
|
581
|
+
写每行防御代码前自问:**这个情况在当前上下文真的会发生吗?**
|
|
898
582
|
|
|
899
|
-
|
|
583
|
+
- 会 → 写防御,注释说明触发条件
|
|
584
|
+
- 不会 → 不写,信任上游保证
|
|
585
|
+
- 不确定 → 查类型定义与调用链确认
|
|
900
586
|
|
|
901
587
|
---
|
|
902
588
|
|
|
903
|
-
##
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
|
920
|
-
|
|
921
|
-
|
|
|
922
|
-
|
|
|
923
|
-
|
|
|
924
|
-
|
|
|
589
|
+
## 十五、违规检测清单
|
|
590
|
+
|
|
591
|
+
AI 编写或审查 Vue 代码时,**必须**检查以下违规模式:
|
|
592
|
+
|
|
593
|
+
| 严重程度 | 动作 |
|
|
594
|
+
|---------|------|
|
|
595
|
+
| **致命** | **立即 STOP**,必须修正后才能继续 |
|
|
596
|
+
| **高** | 必须修正后才能继续,向用户说明 |
|
|
597
|
+
| **中** | 提示风险,建议修正 |
|
|
598
|
+
|
|
599
|
+
| 检查项 | 违规模式 | 严重程度 |
|
|
600
|
+
|--------|---------|---------|
|
|
601
|
+
| 未查 skill | 使用 eui / ekit API 前未先查 `dwy-eui` / `dwy-ekit` skill | 高 |
|
|
602
|
+
| ekit 重复造轮子 | 自造 HTTP / storage / cookie / date / 校验 / 脱敏 等 ekit 已提供能力 | 高 |
|
|
603
|
+
| eui 重复造轮子 | 自造已有 UI 组件(Button / Input / Dialog 等) | 高 |
|
|
604
|
+
| 组件直调 axios | 组件内 `axios.get` / `fetch` / `new XMLHttpRequest` | **致命 → STOP** |
|
|
605
|
+
| 直接 localStorage | `localStorage.getItem` / `setItem` 出现在业务代码 | 高 |
|
|
606
|
+
| 直接 new Date | `new Date()` / `toLocaleString` / 直接 `import dayjs` | 高 |
|
|
607
|
+
| 技术层散落 | 顶级 `views/` / `components/` / `stores/` / `api/` 把不同业务混放 | 高 |
|
|
608
|
+
| Options API | `<script>` 中 `export default { data, methods, ... }` | **致命 → STOP** |
|
|
609
|
+
| Options Store | `defineStore(id, { state, actions })` | **致命 → STOP** |
|
|
610
|
+
| any 无注释 | 使用 `any` 但未注释说明原因 | 高 |
|
|
611
|
+
| 旧 axios 例子 | 业务里出现 `axios.create()` / `interceptors.request.use(...)` | **致命 → STOP** |
|
|
612
|
+
| `v-if` + `v-for` 同元素 | 同一元素既 `v-if` 又 `v-for` | 高 |
|
|
613
|
+
| 模板复杂表达式 | 模板中 `.filter(...).length > 0` 等多语句 | 中 |
|
|
614
|
+
| 解构 store 丢响应性 | `const { x } = store`(应用 `storeToRefs`) | 高 |
|
|
615
|
+
| 注释缺失 | 导出函数 / 类型 / 接口字段无中文注释 | 中 |
|
|
616
|
+
| 注释为英文 | 项目中文环境下写英文注释 | 中 |
|
|
617
|
+
| 硬编码 API 地址 | URL 直接写在代码里,未走 `import.meta.env` | 高 |
|
|
618
|
+
| 相对路径回溯 | `../../../` 跨层导入 | 中 |
|
|
619
|
+
| scoped 替代 Tailwind | 大段 scoped CSS 实现可用 Tailwind 表达的样式 | 中 |
|
|
620
|
+
| 测试与源码混放 | 测试文件放 `src/` 内(详见 `dwy-vue-testing`) | 高 |
|