create-dwy 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/sync.js +2 -2
- package/src/utils.js +1 -1
- package/templates/claude-global/hooks/pre-git-commit-sensitive-check.sh +59 -0
- package/templates/claude-global/rules/backend-security.md +592 -0
- package/templates/claude-global/rules/server-security.md +209 -75
- package/templates/claude-global/rules/vue-code-style.md +706 -0
- package/templates/claude-global/settings.json +15 -0
- package/templates/claude-global/skills/core/SKILL.md +181 -0
- package/templates/claude-global/skills/danweiyuan-base/SKILL.md +279 -0
- package/templates/claude-global/skills/eui/SKILL.md +246 -0
- package/templates/claude-global/hooks/.gitkeep +0 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
# Vue 3 + TypeScript + Vite + Pinia 代码规范
|
|
2
|
+
|
|
3
|
+
所有 Vue / TypeScript 前端项目必须遵循以下规范。AI 生成或修改代码时,必须严格按照这些规则执行。
|
|
4
|
+
|
|
5
|
+
## 一、项目结构
|
|
6
|
+
|
|
7
|
+
### 强制规则
|
|
8
|
+
- 使用 `pnpm` 作为包管理器
|
|
9
|
+
- 使用 `Vite` 作为构建工具
|
|
10
|
+
- 必须有 `.node-version` 文件声明 Node 版本
|
|
11
|
+
- 必须有 `.npmrc` 配置安装源
|
|
12
|
+
|
|
13
|
+
### 标准目录结构
|
|
14
|
+
```
|
|
15
|
+
frontend/
|
|
16
|
+
├── src/
|
|
17
|
+
│ ├── views/ # 页面组件(一个路由对应一个文件)
|
|
18
|
+
│ ├── components/ # 可复用组件
|
|
19
|
+
│ ├── stores/ # Pinia 状态管理
|
|
20
|
+
│ ├── router/ # Vue Router 配置
|
|
21
|
+
│ │ └── index.ts
|
|
22
|
+
│ ├── api/ # API 请求模块(按业务域拆分)
|
|
23
|
+
│ ├── composables/ # 组合式函数(use* 开头)
|
|
24
|
+
│ ├── types/ # 共享类型定义
|
|
25
|
+
│ ├── utils/ # 工具函数
|
|
26
|
+
│ ├── assets/ # 静态资源
|
|
27
|
+
│ ├── App.vue # 根组件
|
|
28
|
+
│ ├── main.ts # 应用入口
|
|
29
|
+
│ └── style.css # 全局样式(Tailwind 入口)
|
|
30
|
+
├── index.html
|
|
31
|
+
├── package.json
|
|
32
|
+
├── vite.config.ts
|
|
33
|
+
├── tsconfig.json
|
|
34
|
+
├── tsconfig.app.json
|
|
35
|
+
└── .npmrc
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 二、组件规范
|
|
39
|
+
|
|
40
|
+
### 强制使用 Composition API + `<script setup>`
|
|
41
|
+
|
|
42
|
+
```vue
|
|
43
|
+
<!-- ✅ 正确 -->
|
|
44
|
+
<script setup lang="ts">
|
|
45
|
+
import { ref, computed, onMounted } from 'vue'
|
|
46
|
+
|
|
47
|
+
const count = ref(0)
|
|
48
|
+
const doubled = computed(() => count.value * 2)
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<!-- ❌ 禁止 Options API -->
|
|
52
|
+
<script lang="ts">
|
|
53
|
+
export default {
|
|
54
|
+
data() { return { count: 0 } },
|
|
55
|
+
computed: { doubled() { return this.count * 2 } }
|
|
56
|
+
}
|
|
57
|
+
</script>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 组件文件结构
|
|
61
|
+
|
|
62
|
+
```vue
|
|
63
|
+
<script setup lang="ts">
|
|
64
|
+
// 1. 类型导入
|
|
65
|
+
import type { UserInfo } from '@/types/user'
|
|
66
|
+
|
|
67
|
+
// 2. 组件/工具导入
|
|
68
|
+
import { ref, computed, onMounted } from 'vue'
|
|
69
|
+
import { useRouter } from 'vue-router'
|
|
70
|
+
import { useAuthStore } from '@/stores/auth'
|
|
71
|
+
|
|
72
|
+
// 3. Props/Emits 定义
|
|
73
|
+
const props = defineProps<{
|
|
74
|
+
userId: number
|
|
75
|
+
title?: string
|
|
76
|
+
}>()
|
|
77
|
+
|
|
78
|
+
const emit = defineEmits<{
|
|
79
|
+
submit: [data: UserInfo]
|
|
80
|
+
cancel: []
|
|
81
|
+
}>()
|
|
82
|
+
|
|
83
|
+
// 4. Store/Router 实例
|
|
84
|
+
const router = useRouter()
|
|
85
|
+
const authStore = useAuthStore()
|
|
86
|
+
|
|
87
|
+
// 5. 响应式状态
|
|
88
|
+
const loading = ref(false)
|
|
89
|
+
const formData = ref<UserInfo | null>(null)
|
|
90
|
+
|
|
91
|
+
// 6. 计算属性
|
|
92
|
+
const isValid = computed(() => !!formData.value?.name)
|
|
93
|
+
|
|
94
|
+
// 7. 方法
|
|
95
|
+
async function handleSubmit() {
|
|
96
|
+
loading.value = true
|
|
97
|
+
try {
|
|
98
|
+
emit('submit', formData.value!)
|
|
99
|
+
} finally {
|
|
100
|
+
loading.value = false
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 8. 生命周期
|
|
105
|
+
onMounted(() => {
|
|
106
|
+
// ...
|
|
107
|
+
})
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<template>
|
|
111
|
+
<!-- 模板 -->
|
|
112
|
+
</template>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 命名规范
|
|
116
|
+
|
|
117
|
+
| 类型 | 风格 | 示例 |
|
|
118
|
+
|------|------|------|
|
|
119
|
+
| 组件文件 | `PascalCase.vue` | `UserProfile.vue`、`LoginView.vue` |
|
|
120
|
+
| 页面组件 | `PascalCase` + View 后缀 | `DashboardView.vue`、`LoginView.vue` |
|
|
121
|
+
| 组合式函数 | `use` + PascalCase | `useAuth.ts`、`useDictionary.ts` |
|
|
122
|
+
| Store 文件 | `camelCase.ts` | `auth.ts`、`menu.ts` |
|
|
123
|
+
| API 文件 | `camelCase.ts` | `user.ts`、`exam.ts` |
|
|
124
|
+
| 类型文件 | `camelCase.ts` | `user.ts`、`common.ts` |
|
|
125
|
+
| 工具文件 | `camelCase.ts` | `request.ts`、`format.ts` |
|
|
126
|
+
| 变量/函数 | `camelCase` | `userName`、`handleSubmit` |
|
|
127
|
+
| 类型/接口 | `PascalCase` | `UserInfo`、`ExamListParams` |
|
|
128
|
+
| 常量 | `UPPER_SNAKE_CASE` | `MAX_PAGE_SIZE`、`API_TIMEOUT` |
|
|
129
|
+
| 事件处理函数 | `handle` + 动作 | `handleLogin`、`handleDelete` |
|
|
130
|
+
| 布尔变量 | `is/has/can/should` 前缀 | `isLoading`、`hasPermission` |
|
|
131
|
+
|
|
132
|
+
### 禁止的写法
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// ❌ 单字母变量(循环索引除外)
|
|
136
|
+
const d = getData()
|
|
137
|
+
|
|
138
|
+
// ✅ 有意义的名称
|
|
139
|
+
const userData = getData()
|
|
140
|
+
|
|
141
|
+
// ❌ 不带语义的布尔变量
|
|
142
|
+
const active = ref(true)
|
|
143
|
+
|
|
144
|
+
// ✅ 布尔变量带前缀
|
|
145
|
+
const isActive = ref(true)
|
|
146
|
+
|
|
147
|
+
// ❌ 在模板中使用复杂表达式
|
|
148
|
+
<div v-if="list.filter(i => i.status === 'active').length > 0">
|
|
149
|
+
|
|
150
|
+
// ✅ 用 computed 提取
|
|
151
|
+
const hasActiveItems = computed(() => list.value.some(i => i.status === 'active'))
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## 三、TypeScript 规范
|
|
155
|
+
|
|
156
|
+
### 强制规则
|
|
157
|
+
- 所有 `.ts` 和 `.vue` 文件必须使用 TypeScript
|
|
158
|
+
- **禁止**使用 `any`,除非与外部动态库交互且无法确定类型
|
|
159
|
+
- 使用 `any` 时必须附注释说明原因
|
|
160
|
+
- 类型导入使用 `import type { ... }`
|
|
161
|
+
|
|
162
|
+
### 类型标注
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// ✅ Props 使用泛型定义
|
|
166
|
+
const props = defineProps<{
|
|
167
|
+
title: string
|
|
168
|
+
count?: number
|
|
169
|
+
items: UserInfo[]
|
|
170
|
+
}>()
|
|
171
|
+
|
|
172
|
+
// ✅ Emits 使用泛型定义
|
|
173
|
+
const emit = defineEmits<{
|
|
174
|
+
change: [value: string]
|
|
175
|
+
submit: [data: FormData]
|
|
176
|
+
}>()
|
|
177
|
+
|
|
178
|
+
// ✅ ref 使用泛型
|
|
179
|
+
const user = ref<UserInfo | null>(null)
|
|
180
|
+
const list = ref<UserInfo[]>([])
|
|
181
|
+
|
|
182
|
+
// ✅ computed 返回类型由推导,复杂时显式标注
|
|
183
|
+
const filters = computed<FilterConfig[]>(() => [...])
|
|
184
|
+
|
|
185
|
+
// ❌ 不标注类型
|
|
186
|
+
const user = ref(null)
|
|
187
|
+
const list = ref([])
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 类型定义
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
// ✅ 共享类型放 types/ 目录
|
|
194
|
+
// types/user.ts
|
|
195
|
+
export interface UserInfo {
|
|
196
|
+
id: number
|
|
197
|
+
username: string
|
|
198
|
+
role: 'admin' | 'user'
|
|
199
|
+
email?: string
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface UserCreate {
|
|
203
|
+
username: string
|
|
204
|
+
password: string
|
|
205
|
+
role: 'admin' | 'user'
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ✅ 组件内部类型放 script setup 里
|
|
209
|
+
interface TableColumn {
|
|
210
|
+
key: string
|
|
211
|
+
label: string
|
|
212
|
+
sortable?: boolean
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### 现代语法(强制)
|
|
217
|
+
|
|
218
|
+
| 旧写法 | 新写法(强制) |
|
|
219
|
+
|--------|--------------|
|
|
220
|
+
| `Array<string>` | `string[]` |
|
|
221
|
+
| 枚举 `enum Status {}` | 联合类型 `type Status = 'active' \| 'inactive'` |
|
|
222
|
+
|
|
223
|
+
## 四、Pinia 状态管理
|
|
224
|
+
|
|
225
|
+
### 强制使用 Setup Store
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// ✅ Setup Store(强制)
|
|
229
|
+
export const useAuthStore = defineStore('auth', () => {
|
|
230
|
+
const token = ref<string | null>(localStorage.getItem('token'))
|
|
231
|
+
const user = ref<UserInfo | null>(null)
|
|
232
|
+
|
|
233
|
+
const isLoggedIn = computed(() => !!token.value)
|
|
234
|
+
|
|
235
|
+
async function login(username: string, password: string) {
|
|
236
|
+
const res = await apiLogin(username, password)
|
|
237
|
+
token.value = res.data.access_token
|
|
238
|
+
localStorage.setItem('token', token.value)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function logout() {
|
|
242
|
+
token.value = null
|
|
243
|
+
user.value = null
|
|
244
|
+
localStorage.removeItem('token')
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return { token, user, isLoggedIn, login, logout }
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// ❌ Options Store(禁止)
|
|
251
|
+
export const useAuthStore = defineStore('auth', {
|
|
252
|
+
state: () => ({ token: '' }),
|
|
253
|
+
actions: { login() {} }
|
|
254
|
+
})
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Store 设计原则
|
|
258
|
+
|
|
259
|
+
| 原则 | 说明 |
|
|
260
|
+
|------|------|
|
|
261
|
+
| 单一职责 | 一个 store 管理一个业务域(auth、menu、user) |
|
|
262
|
+
| 不直接修改 state | 通过方法修改,不在组件中直接 `store.xxx = yyy` |
|
|
263
|
+
| 异步操作放 store | API 调用封装在 store 方法中 |
|
|
264
|
+
| 解构用 `storeToRefs` | 解构响应式状态必须用 `storeToRefs`,方法直接解构 |
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
// ✅ 正确解构
|
|
268
|
+
const store = useReportStore()
|
|
269
|
+
const { report, loading } = storeToRefs(store)
|
|
270
|
+
const { setReport, reset } = store
|
|
271
|
+
|
|
272
|
+
// ❌ 错误解构(丢失响应性)
|
|
273
|
+
const { report, loading } = store
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## 五、路由规范
|
|
277
|
+
|
|
278
|
+
### 强制规则
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
282
|
+
|
|
283
|
+
const router = createRouter({
|
|
284
|
+
history: createWebHistory(),
|
|
285
|
+
routes: [
|
|
286
|
+
{
|
|
287
|
+
path: '/login',
|
|
288
|
+
name: 'Login',
|
|
289
|
+
component: () => import('@/views/LoginView.vue'),
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
path: '/dashboard',
|
|
293
|
+
name: 'Dashboard',
|
|
294
|
+
component: () => import('@/views/DashboardView.vue'),
|
|
295
|
+
meta: { requiresAuth: true },
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
})
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
| 规则 | 说明 |
|
|
302
|
+
|------|------|
|
|
303
|
+
| 懒加载 | 所有路由组件必须使用 `() => import()` 懒加载 |
|
|
304
|
+
| 路由守卫 | 认证检查放 `router.beforeEach`,不在组件中检查 |
|
|
305
|
+
| meta 类型 | 使用 `RouteMeta` 类型扩展声明 meta 字段 |
|
|
306
|
+
| 路径命名 | 使用 kebab-case:`/user-profile`,不用 camelCase |
|
|
307
|
+
|
|
308
|
+
### 路由守卫
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
// ✅ 全局前置守卫
|
|
312
|
+
router.beforeEach((to) => {
|
|
313
|
+
const authStore = useAuthStore()
|
|
314
|
+
|
|
315
|
+
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
|
|
316
|
+
return { name: 'Login' }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (to.meta.requiresAdmin && !authStore.isAdmin) {
|
|
320
|
+
return { name: 'Dashboard' }
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### 路由 Meta 类型声明
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
// types/router.d.ts
|
|
329
|
+
import 'vue-router'
|
|
330
|
+
|
|
331
|
+
declare module 'vue-router' {
|
|
332
|
+
interface RouteMeta {
|
|
333
|
+
requiresAuth?: boolean
|
|
334
|
+
requiresAdmin?: boolean
|
|
335
|
+
title?: string
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## 六、API 请求规范
|
|
341
|
+
|
|
342
|
+
### 强制规则
|
|
343
|
+
|
|
344
|
+
- API 文件按**业务域拆分**(`api/user.ts`、`api/exam.ts`)
|
|
345
|
+
- 每个 API 函数**独立导出**,不使用 default export
|
|
346
|
+
- 请求参数和响应使用**类型标注**
|
|
347
|
+
- **禁止**在组件中直接创建 axios 实例或调用 `fetch`
|
|
348
|
+
|
|
349
|
+
### API 文件结构
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
// api/user.ts
|
|
353
|
+
import request from '@/utils/request'
|
|
354
|
+
import type { UserInfo, UserCreate, UserUpdate } from '@/types/user'
|
|
355
|
+
|
|
356
|
+
export function getUsers(params?: { page?: number; page_size?: number }) {
|
|
357
|
+
return request.get<{ items: UserInfo[]; total: number }>('/users', { params })
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function getUser(id: number) {
|
|
361
|
+
return request.get<UserInfo>(`/users/${id}`)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function createUser(data: UserCreate) {
|
|
365
|
+
return request.post<UserInfo>('/users', data)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function updateUser(id: number, data: UserUpdate) {
|
|
369
|
+
return request.put<UserInfo>(`/users/${id}`, data)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function deleteUser(id: number) {
|
|
373
|
+
return request.delete(`/users/${id}`)
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### 请求实例配置
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
// utils/request.ts
|
|
381
|
+
import axios from 'axios'
|
|
382
|
+
|
|
383
|
+
const request = axios.create({
|
|
384
|
+
baseURL: '/api',
|
|
385
|
+
timeout: 30000,
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
// Token 注入
|
|
389
|
+
request.interceptors.request.use((config) => {
|
|
390
|
+
const token = localStorage.getItem('token')
|
|
391
|
+
if (token) {
|
|
392
|
+
config.headers.Authorization = `Bearer ${token}`
|
|
393
|
+
}
|
|
394
|
+
return config
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
// 401 处理
|
|
398
|
+
request.interceptors.response.use(
|
|
399
|
+
(response) => response,
|
|
400
|
+
(error) => {
|
|
401
|
+
if (error.response?.status === 401) {
|
|
402
|
+
localStorage.removeItem('token')
|
|
403
|
+
window.location.href = '/login'
|
|
404
|
+
}
|
|
405
|
+
return Promise.reject(error)
|
|
406
|
+
},
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
export default request
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### 禁止的写法
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
// ❌ 在组件中直接调用
|
|
416
|
+
const res = await axios.get('/api/users')
|
|
417
|
+
|
|
418
|
+
// ✅ 通过 API 模块调用
|
|
419
|
+
import { getUsers } from '@/api/user'
|
|
420
|
+
const res = await getUsers()
|
|
421
|
+
|
|
422
|
+
// ❌ API 函数使用 default export
|
|
423
|
+
export default { getUsers, createUser }
|
|
424
|
+
|
|
425
|
+
// ✅ 每个函数独立导出
|
|
426
|
+
export function getUsers() { ... }
|
|
427
|
+
export function createUser() { ... }
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## 七、组合式函数(Composables)
|
|
431
|
+
|
|
432
|
+
### 命名规范
|
|
433
|
+
|
|
434
|
+
- 文件名和函数名必须以 `use` 开头
|
|
435
|
+
- 返回值使用对象解构(不使用数组)
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
// composables/useList.ts
|
|
439
|
+
|
|
440
|
+
// ✅ 正确
|
|
441
|
+
export function useList<T>(fetchFn: () => Promise<T[]>) {
|
|
442
|
+
const items = ref<T[]>([])
|
|
443
|
+
const loading = ref(false)
|
|
444
|
+
|
|
445
|
+
async function refresh() {
|
|
446
|
+
loading.value = true
|
|
447
|
+
try {
|
|
448
|
+
items.value = await fetchFn()
|
|
449
|
+
} finally {
|
|
450
|
+
loading.value = false
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
onMounted(refresh)
|
|
455
|
+
|
|
456
|
+
return { items, loading, refresh }
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// 使用
|
|
460
|
+
const { items: users, loading, refresh } = useList(() => getUsers())
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### 禁止的写法
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
// ❌ 不以 use 开头
|
|
467
|
+
export function fetchList() { ... }
|
|
468
|
+
|
|
469
|
+
// ❌ 返回数组
|
|
470
|
+
export function useList() {
|
|
471
|
+
return [items, loading, refresh]
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
## 八、模板规范
|
|
476
|
+
|
|
477
|
+
### 指令使用
|
|
478
|
+
|
|
479
|
+
```vue
|
|
480
|
+
<!-- ✅ v-if/v-else 在同级元素上 -->
|
|
481
|
+
<div v-if="isLoading">加载中...</div>
|
|
482
|
+
<div v-else>{{ data }}</div>
|
|
483
|
+
|
|
484
|
+
<!-- ✅ v-for 必须带 key -->
|
|
485
|
+
<div v-for="item in list" :key="item.id">{{ item.name }}</div>
|
|
486
|
+
|
|
487
|
+
<!-- ❌ v-if 和 v-for 在同一元素上 -->
|
|
488
|
+
<div v-for="item in list" v-if="item.active" :key="item.id">
|
|
489
|
+
|
|
490
|
+
<!-- ✅ 用 computed 过滤 -->
|
|
491
|
+
<div v-for="item in activeList" :key="item.id">
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### 事件绑定
|
|
495
|
+
|
|
496
|
+
```vue
|
|
497
|
+
<!-- ✅ 方法引用(无参数时) -->
|
|
498
|
+
<button @click="handleSubmit">提交</button>
|
|
499
|
+
|
|
500
|
+
<!-- ✅ 箭头函数(需要传参时) -->
|
|
501
|
+
<button @click="() => handleDelete(item.id)">删除</button>
|
|
502
|
+
|
|
503
|
+
<!-- ❌ 模板中写复杂逻辑 -->
|
|
504
|
+
<button @click="loading = true; api.delete(id).then(() => refresh())">删除</button>
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### 组件使用
|
|
508
|
+
|
|
509
|
+
```vue
|
|
510
|
+
<!-- ✅ PascalCase 组件名 -->
|
|
511
|
+
<UserCard :user="user" @click="handleSelect" />
|
|
512
|
+
|
|
513
|
+
<!-- ❌ kebab-case 组件名 -->
|
|
514
|
+
<user-card :user="user" />
|
|
515
|
+
|
|
516
|
+
<!-- ✅ 布尔 prop 简写 -->
|
|
517
|
+
<EButton loading disabled>提交</EButton>
|
|
518
|
+
|
|
519
|
+
<!-- ✅ v-model 双向绑定 -->
|
|
520
|
+
<EInput v-model="form.name" placeholder="请输入" />
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
## 九、样式规范
|
|
524
|
+
|
|
525
|
+
### 强制使用 Tailwind CSS
|
|
526
|
+
|
|
527
|
+
```vue
|
|
528
|
+
<!-- ✅ 使用 Tailwind 工具类 -->
|
|
529
|
+
<div class="flex items-center gap-4 p-6 rounded-lg bg-card">
|
|
530
|
+
|
|
531
|
+
<!-- ❌ 使用内联样式 -->
|
|
532
|
+
<div style="display: flex; padding: 24px;">
|
|
533
|
+
|
|
534
|
+
<!-- ❌ 使用 scoped CSS(除非确实需要) -->
|
|
535
|
+
<style scoped>
|
|
536
|
+
.container { display: flex; }
|
|
537
|
+
</style>
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### 允许 scoped CSS 的场景
|
|
541
|
+
|
|
542
|
+
- 第三方组件样式覆盖
|
|
543
|
+
- 复杂动画定义
|
|
544
|
+
- 无法用 Tailwind 实现的样式
|
|
545
|
+
|
|
546
|
+
```vue
|
|
547
|
+
<!-- ✅ 覆盖第三方组件样式时可以用 scoped -->
|
|
548
|
+
<style scoped>
|
|
549
|
+
:deep(.el-input__inner) {
|
|
550
|
+
border-radius: 8px;
|
|
551
|
+
}
|
|
552
|
+
</style>
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### 响应式设计
|
|
556
|
+
|
|
557
|
+
```vue
|
|
558
|
+
<!-- ✅ Mobile-first 响应式 -->
|
|
559
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
560
|
+
|
|
561
|
+
<!-- ✅ 暗色模式使用 dark: 前缀 -->
|
|
562
|
+
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
## 十、Vite 配置规范
|
|
566
|
+
|
|
567
|
+
### 强制配置
|
|
568
|
+
|
|
569
|
+
```typescript
|
|
570
|
+
// vite.config.ts
|
|
571
|
+
import { fileURLToPath, URL } from 'node:url'
|
|
572
|
+
import { defineConfig } from 'vite'
|
|
573
|
+
import vue from '@vitejs/plugin-vue'
|
|
574
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
575
|
+
|
|
576
|
+
export default defineConfig({
|
|
577
|
+
plugins: [vue(), tailwindcss()],
|
|
578
|
+
resolve: {
|
|
579
|
+
alias: {
|
|
580
|
+
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
server: {
|
|
584
|
+
port: 5173,
|
|
585
|
+
proxy: {
|
|
586
|
+
'/api': {
|
|
587
|
+
target: 'http://127.0.0.1:8000',
|
|
588
|
+
changeOrigin: true,
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
},
|
|
592
|
+
})
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
| 规则 | 说明 |
|
|
596
|
+
|------|------|
|
|
597
|
+
| 路径别名 | 必须配置 `@` → `src/` |
|
|
598
|
+
| API 代理 | 开发环境必须配置 `/api` 代理到后端 |
|
|
599
|
+
| 端口 | 默认 5173,可在 `.env` 中覆盖 |
|
|
600
|
+
|
|
601
|
+
## 十一、错误处理
|
|
602
|
+
|
|
603
|
+
### 强制规则
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
// ✅ 异步操作必须有 try/catch
|
|
607
|
+
async function handleSubmit() {
|
|
608
|
+
loading.value = true
|
|
609
|
+
try {
|
|
610
|
+
await createUser(formData.value)
|
|
611
|
+
message.success('创建成功')
|
|
612
|
+
router.push('/users')
|
|
613
|
+
} catch (err) {
|
|
614
|
+
message.error('创建失败')
|
|
615
|
+
} finally {
|
|
616
|
+
loading.value = false
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ❌ 没有错误处理
|
|
621
|
+
async function handleSubmit() {
|
|
622
|
+
await createUser(formData.value)
|
|
623
|
+
router.push('/users')
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ❌ 空 catch
|
|
627
|
+
try { ... } catch {}
|
|
628
|
+
|
|
629
|
+
// ✅ 至少给用户反馈
|
|
630
|
+
try { ... } catch { message.error('操作失败') }
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Loading 状态
|
|
634
|
+
|
|
635
|
+
```typescript
|
|
636
|
+
// ✅ 所有异步操作必须有 loading 状态
|
|
637
|
+
const loading = ref(false)
|
|
638
|
+
|
|
639
|
+
async function fetchData() {
|
|
640
|
+
loading.value = true
|
|
641
|
+
try {
|
|
642
|
+
data.value = await getData()
|
|
643
|
+
} finally {
|
|
644
|
+
loading.value = false
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
## 十二、导入规范
|
|
650
|
+
|
|
651
|
+
### 导入顺序(按以下顺序排列,组间空行)
|
|
652
|
+
|
|
653
|
+
```typescript
|
|
654
|
+
// 1. Vue 核心
|
|
655
|
+
import { ref, computed, onMounted, watch } from 'vue'
|
|
656
|
+
import { useRouter, useRoute } from 'vue-router'
|
|
657
|
+
|
|
658
|
+
// 2. 第三方库
|
|
659
|
+
import { storeToRefs } from 'pinia'
|
|
660
|
+
import axios from 'axios'
|
|
661
|
+
|
|
662
|
+
// 3. 内部包(@danweiyuan/*)
|
|
663
|
+
import { EButton, EInput } from '@danweiyuan/eui'
|
|
664
|
+
|
|
665
|
+
// 4. 项目内模块
|
|
666
|
+
import { useAuthStore } from '@/stores/auth'
|
|
667
|
+
import { getUsers } from '@/api/user'
|
|
668
|
+
import type { UserInfo } from '@/types/user'
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
### 强制规则
|
|
672
|
+
|
|
673
|
+
```typescript
|
|
674
|
+
// ✅ 使用 @ 别名
|
|
675
|
+
import { useAuthStore } from '@/stores/auth'
|
|
676
|
+
|
|
677
|
+
// ❌ 使用相对路径(超过 2 层)
|
|
678
|
+
import { useAuthStore } from '../../../stores/auth'
|
|
679
|
+
|
|
680
|
+
// ✅ 类型导入使用 import type
|
|
681
|
+
import type { UserInfo } from '@/types/user'
|
|
682
|
+
|
|
683
|
+
// ❌ 类型和值混合导入
|
|
684
|
+
import { UserInfo, getUsers } from '@/api/user'
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
## 十三、常见反模式(禁止)
|
|
688
|
+
|
|
689
|
+
| 反模式 | 正确做法 |
|
|
690
|
+
|--------|---------|
|
|
691
|
+
| Options API | Composition API + `<script setup>` |
|
|
692
|
+
| Pinia Options Store | Setup Store(箭头函数) |
|
|
693
|
+
| `this.$refs` | `ref()` + 模板 ref |
|
|
694
|
+
| `this.$emit` | `defineEmits<>()` |
|
|
695
|
+
| `this.$router` | `useRouter()` |
|
|
696
|
+
| `this.$route` | `useRoute()` |
|
|
697
|
+
| Vuex | Pinia |
|
|
698
|
+
| Mixins | Composables(`use*`) |
|
|
699
|
+
| Event Bus | Pinia store 或 `provide/inject` |
|
|
700
|
+
| `v-if` + `v-for` 同元素 | computed 过滤后 `v-for` |
|
|
701
|
+
| scoped CSS 为主 | Tailwind CSS 为主 |
|
|
702
|
+
| 组件中直接调 axios | 通过 `api/` 模块调用 |
|
|
703
|
+
| `.value` 在模板中 | 模板自动解包,不需要 `.value` |
|
|
704
|
+
| `reactive()` 用于简单值 | `ref()` 用于所有场景 |
|
|
705
|
+
| 解构 store 不用 `storeToRefs` | 响应式值必须用 `storeToRefs` |
|
|
706
|
+
| 手写 CSS 为主 | Tailwind 工具类为主 |
|