@deepstorm/cli 0.1.0 → 0.2.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/dist/config-schema.json +1 -1
- package/dist/registry.json +65 -5
- package/dist/skills/reef-gen-frontend/variants/react/steps.md +31 -0
- package/dist/skills/reef-gen-frontend/variants/vue/steps.md +34 -0
- package/dist/skills/reef-style-frontend/fragments/test/vitest-react/examples/testing.md +283 -0
- package/dist/skills/reef-style-frontend/fragments/test/vitest-react/quick-reference.md +152 -0
- package/dist/skills/reef-style-frontend/fragments/test/vitest-vue/examples/testing.md +457 -0
- package/dist/skills/reef-style-frontend/fragments/test/vitest-vue/quick-reference.md +186 -0
- package/dist/skills/reef-style-frontend/fragments/ui-lib/antd/examples/ui-components.md +257 -0
- package/dist/skills/reef-style-frontend/fragments/ui-lib/antd/quick-reference.md +166 -0
- package/dist/skills/reef-style-frontend/fragments/ui-lib/antd-vue/examples/ui-components.md +353 -0
- package/dist/skills/reef-style-frontend/fragments/ui-lib/antd-vue/quick-reference.md +267 -0
- package/dist/skills/reef-style-frontend/variants/react/examples/code-wrapping.md +124 -0
- package/dist/skills/reef-style-frontend/variants/react/examples/component-types-pipes.md +195 -0
- package/dist/skills/reef-style-frontend/variants/react/examples/entity-types.md +127 -0
- package/dist/skills/reef-style-frontend/variants/react/examples/forms-layer.md +251 -0
- package/dist/skills/reef-style-frontend/variants/react/examples/service-routing.md +226 -0
- package/dist/skills/reef-style-frontend/variants/react/quick-reference.md +118 -0
- package/dist/skills/reef-style-frontend/variants/vue/examples/code-wrapping.md +162 -0
- package/dist/skills/reef-style-frontend/variants/vue/examples/component-types-pipes.md +270 -0
- package/dist/skills/reef-style-frontend/variants/vue/examples/entity-types.md +145 -0
- package/dist/skills/reef-style-frontend/variants/vue/examples/forms-layer.md +231 -0
- package/dist/skills/reef-style-frontend/variants/vue/examples/service-routing.md +224 -0
- package/dist/skills/reef-style-frontend/variants/vue/quick-reference.md +145 -0
- package/package.json +3 -1
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# React 数据获取层 + 路由配置示例
|
|
2
|
+
|
|
3
|
+
## 自定义 Hook(数据获取层)
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// hooks/useUsers.ts
|
|
7
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
8
|
+
import type { User, PageResponse } from '../shared/types/user'
|
|
9
|
+
|
|
10
|
+
interface UseUsersOptions {
|
|
11
|
+
page?: number
|
|
12
|
+
size?: number
|
|
13
|
+
search?: string
|
|
14
|
+
role?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface UseUsersResult {
|
|
18
|
+
users: User[]
|
|
19
|
+
total: number
|
|
20
|
+
loading: boolean
|
|
21
|
+
error: Error | null
|
|
22
|
+
refresh: () => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 使用 URLSearchParams 构建查询参数
|
|
26
|
+
function buildQuery(params: UseUsersOptions): string {
|
|
27
|
+
const searchParams = new URLSearchParams()
|
|
28
|
+
if (params.page !== undefined) searchParams.set('page', String(params.page))
|
|
29
|
+
if (params.size !== undefined) searchParams.set('size', String(params.size))
|
|
30
|
+
if (params.search) searchParams.set('search', params.search)
|
|
31
|
+
if (params.role) searchParams.set('role', params.role)
|
|
32
|
+
const qs = searchParams.toString()
|
|
33
|
+
return qs ? `?${qs}` : ''
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useUsers(options: UseUsersOptions = {}): UseUsersResult {
|
|
37
|
+
const [users, setUsers] = useState<User[]>([])
|
|
38
|
+
const [total, setTotal] = useState(0)
|
|
39
|
+
const [loading, setLoading] = useState(true)
|
|
40
|
+
const [error, setError] = useState<Error | null>(null)
|
|
41
|
+
const [refreshKey, setRefreshKey] = useState(0)
|
|
42
|
+
|
|
43
|
+
const refresh = useCallback(() => setRefreshKey(k => k + 1), [])
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
let cancelled = false
|
|
47
|
+
setLoading(true)
|
|
48
|
+
setError(null)
|
|
49
|
+
|
|
50
|
+
fetch(`/api/v1/users${buildQuery(options)}`)
|
|
51
|
+
.then(res => {
|
|
52
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
53
|
+
return res.json() as Promise<PageResponse<User>>
|
|
54
|
+
})
|
|
55
|
+
.then(data => {
|
|
56
|
+
if (!cancelled) {
|
|
57
|
+
setUsers(data.content)
|
|
58
|
+
setTotal(data.totalElements)
|
|
59
|
+
setLoading(false)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
.catch(err => {
|
|
63
|
+
if (!cancelled) {
|
|
64
|
+
setError(err)
|
|
65
|
+
setLoading(false)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
return () => { cancelled = true }
|
|
70
|
+
}, [options.page, options.size, options.search, options.role, refreshKey])
|
|
71
|
+
|
|
72
|
+
return { users, total, loading, error, refresh }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 写操作 Hook
|
|
76
|
+
export function useCreateUser() {
|
|
77
|
+
const [loading, setLoading] = useState(false)
|
|
78
|
+
|
|
79
|
+
const create = async (data: CreateUserRequest): Promise<User> => {
|
|
80
|
+
setLoading(true)
|
|
81
|
+
const res = await fetch('/api/v1/users', {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { 'Content-Type': 'application/json' },
|
|
84
|
+
body: JSON.stringify(data),
|
|
85
|
+
})
|
|
86
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
87
|
+
const user = await res.json()
|
|
88
|
+
setLoading(false)
|
|
89
|
+
return user
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { create, loading }
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Service 层抽取(可选 — 复杂项目使用)
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
// services/userService.ts
|
|
100
|
+
// 将 API 调用统一到 Service 层,Hook 只负责状态管理
|
|
101
|
+
import type { User, CreateUserRequest, UpdateUserRequest, PageResponse } from '../shared/types/user'
|
|
102
|
+
|
|
103
|
+
const BASE_URL = '/api/v1/users'
|
|
104
|
+
|
|
105
|
+
async function handleResponse<T>(res: Response): Promise<T> {
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
const error = await res.json().catch(() => ({ message: `HTTP ${res.status}` }))
|
|
108
|
+
throw new Error(error.message)
|
|
109
|
+
}
|
|
110
|
+
return res.json()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const userService = {
|
|
114
|
+
list(params?: Record<string, string | number>): Promise<PageResponse<User>> {
|
|
115
|
+
const qs = params ? '?' + new URLSearchParams(
|
|
116
|
+
Object.entries(params).map(([k, v]) => [k, String(v)])
|
|
117
|
+
).toString() : ''
|
|
118
|
+
return fetch(`${BASE_URL}${qs}`).then(handleResponse)
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
getById(id: string): Promise<User> {
|
|
122
|
+
return fetch(`${BASE_URL}/${id}`).then(handleResponse)
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
create(data: CreateUserRequest): Promise<User> {
|
|
126
|
+
return fetch(BASE_URL, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: { 'Content-Type': 'application/json' },
|
|
129
|
+
body: JSON.stringify(data),
|
|
130
|
+
}).then(handleResponse)
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
update(id: string, data: UpdateUserRequest): Promise<User> {
|
|
134
|
+
return fetch(`${BASE_URL}/${id}`, {
|
|
135
|
+
method: 'PUT',
|
|
136
|
+
headers: { 'Content-Type': 'application/json' },
|
|
137
|
+
body: JSON.stringify(data),
|
|
138
|
+
}).then(handleResponse)
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
delete(id: string): Promise<void> {
|
|
142
|
+
return fetch(`${BASE_URL}/${id}`, { method: 'DELETE' }).then(handleResponse)
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## 路由配置
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
// routes/index.tsx
|
|
151
|
+
import { createBrowserRouter, Navigate } from 'react-router-dom'
|
|
152
|
+
import { lazy, Suspense } from 'react'
|
|
153
|
+
import { Spin } from 'antd'
|
|
154
|
+
|
|
155
|
+
// 懒加载页面组件
|
|
156
|
+
const Dashboard = lazy(() => import('../pages/Dashboard'))
|
|
157
|
+
const UserList = lazy(() => import('../pages/UserList'))
|
|
158
|
+
const UserDetail = lazy(() => import('../pages/UserDetail'))
|
|
159
|
+
const CreateUser = lazy(() => import('../pages/CreateUser'))
|
|
160
|
+
const Settings = lazy(() => import('../pages/Settings'))
|
|
161
|
+
const NotFound = lazy(() => import('../pages/NotFound'))
|
|
162
|
+
|
|
163
|
+
// 通用懒加载包装
|
|
164
|
+
const LazyLoad: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
165
|
+
<Suspense fallback={<Spin style={{ display: 'flex', justifyContent: 'center', marginTop: 120 }} />}>
|
|
166
|
+
{children}
|
|
167
|
+
</Suspense>
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
export const router = createBrowserRouter([
|
|
171
|
+
{
|
|
172
|
+
path: '/',
|
|
173
|
+
element: <LazyLoad><Dashboard /></LazyLoad>,
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
path: '/users',
|
|
177
|
+
element: <LazyLoad><UserList /></LazyLoad>,
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
path: '/users/new',
|
|
181
|
+
element: <LazyLoad><CreateUser /></LazyLoad>,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
path: '/users/:id',
|
|
185
|
+
element: <LazyLoad><UserDetail /></LazyLoad>,
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
path: '/settings',
|
|
189
|
+
element: <LazyLoad><Settings /></LazyLoad>,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
path: '/404',
|
|
193
|
+
element: <LazyLoad><NotFound /></LazyLoad>,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
path: '*',
|
|
197
|
+
element: <Navigate to="/404" replace />,
|
|
198
|
+
},
|
|
199
|
+
])
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## 主应用入口
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
// App.tsx
|
|
206
|
+
import { RouterProvider } from 'react-router-dom'
|
|
207
|
+
import { ConfigProvider } from 'antd'
|
|
208
|
+
import { router } from './routes'
|
|
209
|
+
|
|
210
|
+
const App: React.FC = () => {
|
|
211
|
+
return (
|
|
212
|
+
<ConfigProvider
|
|
213
|
+
theme={{
|
|
214
|
+
token: {
|
|
215
|
+
colorPrimary: '#1677ff',
|
|
216
|
+
borderRadius: 6,
|
|
217
|
+
},
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
220
|
+
<RouterProvider router={router} />
|
|
221
|
+
</ConfigProvider>
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export default App
|
|
226
|
+
```
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# 前端编码快速参考 (React)
|
|
2
|
+
|
|
3
|
+
按需加载。仅当你需要编写对应组件类型时阅读相关章节。
|
|
4
|
+
|
|
5
|
+
> **完整示例代码见 `examples/` 目录。** 已安装子维度的规范,通过该维度的 `{value}.md` 文件阅读。
|
|
6
|
+
|
|
7
|
+
## 速查
|
|
8
|
+
|
|
9
|
+
| 场景 | 决策 |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| 新建组件 | 函数组件 + TypeScript + `interface` Props |
|
|
12
|
+
| 新建表单 | Ant Design `Form` + `useState` / `useReducer` |
|
|
13
|
+
| 读数据 | 自定义 Hook(`useUsers`)封装 fetch/axios |
|
|
14
|
+
| 写数据 | 自定义 Hook + `useState` loading/error 三态 |
|
|
15
|
+
| Hook 职责 | 只负责业务逻辑和数据获取,不管理 UI 渲染 |
|
|
16
|
+
| 路由 | `React.lazy()` + `Suspense` 懒加载 |
|
|
17
|
+
|
|
18
|
+
## 新建组件
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
interface UserTableProps {
|
|
22
|
+
users: User[]
|
|
23
|
+
loading?: boolean
|
|
24
|
+
onSelect?: (user: User) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const UserTable: React.FC<UserTableProps> = ({ users, loading, onSelect }) => {
|
|
28
|
+
return (
|
|
29
|
+
<Table
|
|
30
|
+
dataSource={users}
|
|
31
|
+
loading={loading}
|
|
32
|
+
rowKey="id"
|
|
33
|
+
columns={columns}
|
|
34
|
+
onRow={(record) => ({
|
|
35
|
+
onClick: () => onSelect?.(record),
|
|
36
|
+
})}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
| 规则 | 要求 |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| 函数组件 | 禁止 Class 组件,使用 `React.FC` 或显式 Props 类型 |
|
|
45
|
+
| `interface` Props | 使用 `interface` 定义 Props,使用 `type` 定义联合类型 |
|
|
46
|
+
| 状态提升 | 组件间共享状态提升到最近公共父组件 |
|
|
47
|
+
| 默认导出 | 页面级组件使用 `export default`,通用组件使用具名导出 |
|
|
48
|
+
|
|
49
|
+
## Hooks 规则
|
|
50
|
+
|
|
51
|
+
- **顶层调用**:禁止在条件、循环、嵌套函数中调用 Hooks
|
|
52
|
+
- **依赖数组**:`useEffect` / `useCallback` / `useMemo` 的依赖数组必须穷举闭包内所有响应值
|
|
53
|
+
- **自定义 Hook**:以 `use` 开头命名,返回值为对象或元组
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
// ✅ 正确:自定义 Hook 分离数据获取逻辑
|
|
57
|
+
function useUsers() {
|
|
58
|
+
const [users, setUsers] = useState<User[]>([])
|
|
59
|
+
const [loading, setLoading] = useState(true)
|
|
60
|
+
const [error, setError] = useState<Error | null>(null)
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
fetch('/api/v1/users')
|
|
64
|
+
.then(res => res.json())
|
|
65
|
+
.then(data => { setUsers(data); setLoading(false) })
|
|
66
|
+
.catch(err => { setError(err); setLoading(false) })
|
|
67
|
+
}, [])
|
|
68
|
+
|
|
69
|
+
return { users, loading, error }
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Ant Design 组件
|
|
74
|
+
|
|
75
|
+
- **按需导入**:`import { Button, Table } from 'antd'`(Ant Design 5.x 支持 Tree Shaking,无需额外配置)
|
|
76
|
+
- **Form**:`Form.Item` 的 `name` 必须与数据模型字段一致
|
|
77
|
+
- **Table**:列定义使用 `columns` 数组,`dataSource` 需要唯一 `rowKey`
|
|
78
|
+
- **Modal**:关闭时通过 `afterClose` 或 `destroyOnClose` 重置内部状态
|
|
79
|
+
- **布局**:使用 `Row` / `Col` 栅格系统或 Flex 布局
|
|
80
|
+
|
|
81
|
+
## 错误处理
|
|
82
|
+
|
|
83
|
+
- 全局错误边界(Error Boundary)捕获组件渲染异常
|
|
84
|
+
- API 错误在 Hook 层捕获并暴露错误状态,UI 层通过 Ant Design 组件展示反馈
|
|
85
|
+
- 表单校验使用 Ant Design Form 内置校验规则,非必要不自定义 `validator`
|
|
86
|
+
|
|
87
|
+
## 路由
|
|
88
|
+
|
|
89
|
+
- 所有页面组件使用 `React.lazy()` + `Suspense` 懒加载
|
|
90
|
+
- 路由配置集中定义在路由文件中,不在组件内嵌路由
|
|
91
|
+
- 权限通过 Route 组件的 wrapper 或 middleware 模式控制
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
const UserList = React.lazy(() => import('./pages/UserList'))
|
|
95
|
+
const UserDetail = React.lazy(() => import('./pages/UserDetail'))
|
|
96
|
+
|
|
97
|
+
const routes = [
|
|
98
|
+
{ path: '/users', element: <UserList /> },
|
|
99
|
+
{ path: '/users/:id', element: <UserDetail /> },
|
|
100
|
+
]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 代码风格
|
|
104
|
+
|
|
105
|
+
- 控制流语句必须使用大括号,禁止无大括号的早期返回
|
|
106
|
+
- 代码折行规则(90 列限制)详见 `examples/code-wrapping.md`
|
|
107
|
+
- JSX 属性超过 3 个时换行排列
|
|
108
|
+
- 自闭合标签:无子元素时使用 `<Component />` 而非 `<Component></Component>`
|
|
109
|
+
|
|
110
|
+
## 常见坑
|
|
111
|
+
|
|
112
|
+
| 场景 | 问题 | 正确做法 |
|
|
113
|
+
|------|------|---------|
|
|
114
|
+
| `useEffect` 依赖遗漏 | 闭包内使用了 state 或 props 但未加入依赖数组 | 穷举所有依赖,或使用 `useCallback` 稳定引用 |
|
|
115
|
+
| 列表 `key` | 使用 index 作为 key 导致渲染异常 | 使用唯一 ID(`rowKey` / 数据 id) |
|
|
116
|
+
| 表单受控/非受控混淆 | 组件同时接收 `value` 和 `defaultValue` | 受控组件用 `value` + `onChange`,非受控用 `defaultValue` |
|
|
117
|
+
| 状态更新异步 | 连续 `setState` 导致只生效最后一次 | 使用函数形式 `setCount(prev => prev + 1)` |
|
|
118
|
+
| `useCallback` 滥用 | 对所有函数都用 `useCallback` | 仅对传给子组件或 `useEffect` 依赖的函数使用 |
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Vue 模板/脚本代码折行规则
|
|
2
|
+
|
|
3
|
+
遵循 90 列限制,以下是 Vue 专有的折行场景。
|
|
4
|
+
|
|
5
|
+
## 模板属性换行
|
|
6
|
+
|
|
7
|
+
单个属性保持在行内;3 个及以上属性每个占一行:
|
|
8
|
+
|
|
9
|
+
```vue
|
|
10
|
+
<template>
|
|
11
|
+
<!-- ✅ 单属性行内 -->
|
|
12
|
+
<a-button type="primary">提交</a-button>
|
|
13
|
+
|
|
14
|
+
<!-- ✅ 2 个属性行内 -->
|
|
15
|
+
<a-button type="primary" :loading="submitting">提交</a-button>
|
|
16
|
+
|
|
17
|
+
<!-- ✅ 3+ 属性换行排列 -->
|
|
18
|
+
<a-table
|
|
19
|
+
:data-source="users"
|
|
20
|
+
:columns="columns"
|
|
21
|
+
row-key="id"
|
|
22
|
+
:loading="loading"
|
|
23
|
+
:pagination="{ pageSize: 20 }"
|
|
24
|
+
@change="handlePageChange"
|
|
25
|
+
/>
|
|
26
|
+
</template>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 指令换行
|
|
30
|
+
|
|
31
|
+
```vue
|
|
32
|
+
<template>
|
|
33
|
+
<!-- ✅ 短指令行内 -->
|
|
34
|
+
<div v-if="loading">加载中...</div>
|
|
35
|
+
|
|
36
|
+
<!-- ✅ 多指令或长表达式适当换行 -->
|
|
37
|
+
<a-form-item
|
|
38
|
+
v-if="!isReadonly"
|
|
39
|
+
label="邮箱"
|
|
40
|
+
name="email"
|
|
41
|
+
:rules="[
|
|
42
|
+
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
|
43
|
+
{ type: 'email', message: '无效的邮箱格式', trigger: 'blur' },
|
|
44
|
+
]"
|
|
45
|
+
>
|
|
46
|
+
<a-input v-model:value="formState.email" />
|
|
47
|
+
</a-form-item>
|
|
48
|
+
</template>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 自闭合标签
|
|
52
|
+
|
|
53
|
+
无子元素时使用自闭合标签,属性换行规则同上:
|
|
54
|
+
|
|
55
|
+
```vue
|
|
56
|
+
<template>
|
|
57
|
+
<!-- ✅ 无属性 -->
|
|
58
|
+
<a-divider />
|
|
59
|
+
|
|
60
|
+
<!-- ✅ 多属性 -->
|
|
61
|
+
<a-date-picker
|
|
62
|
+
v-model:value="date"
|
|
63
|
+
format="YYYY-MM-DD"
|
|
64
|
+
/>
|
|
65
|
+
</template>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 条件渲染换行
|
|
69
|
+
|
|
70
|
+
```vue
|
|
71
|
+
<template>
|
|
72
|
+
<!-- ✅ 简单的 v-if/v-else 保持紧凑 -->
|
|
73
|
+
<div>{{ loading ? '加载中...' : users.length + ' 条记录' }}</div>
|
|
74
|
+
|
|
75
|
+
<!-- ✅ 复杂条件使用多个 v-if 换行 -->
|
|
76
|
+
<a-alert
|
|
77
|
+
v-if="error"
|
|
78
|
+
type="error"
|
|
79
|
+
:message="error.message"
|
|
80
|
+
closable
|
|
81
|
+
@close="clearError"
|
|
82
|
+
/>
|
|
83
|
+
<a-table v-else-if="!loading" :data-source="users" />
|
|
84
|
+
</template>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Script setup 声明
|
|
88
|
+
|
|
89
|
+
```vue
|
|
90
|
+
<script setup lang="ts">
|
|
91
|
+
// ✅ ref/computed 连续排列,按逻辑分组
|
|
92
|
+
const users = ref<User[]>([])
|
|
93
|
+
const loading = ref(true)
|
|
94
|
+
const search = ref('')
|
|
95
|
+
|
|
96
|
+
const filteredUsers = computed(() =>
|
|
97
|
+
users.value.filter(u => u.username.includes(search.value))
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
// ✅ watch 短依赖行内
|
|
101
|
+
watch(search, () => {
|
|
102
|
+
fetchUsers(search.value)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// ✅ 多源 watch 换行
|
|
106
|
+
watch(
|
|
107
|
+
[search, role, status],
|
|
108
|
+
([newSearch, newRole, newStatus]) => {
|
|
109
|
+
fetchFilteredUsers({ search: newSearch, role: newRole, status: newStatus })
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
</script>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## 事件处理函数换行
|
|
116
|
+
|
|
117
|
+
```vue
|
|
118
|
+
<template>
|
|
119
|
+
<!-- ✅ 短表达式行内 -->
|
|
120
|
+
<a-button @click="isOpen = true">打开</a-button>
|
|
121
|
+
|
|
122
|
+
<!-- ✅ 多行函数体:提取到 script setup 中 -->
|
|
123
|
+
<a-modal
|
|
124
|
+
title="确认删除"
|
|
125
|
+
:open="isOpen"
|
|
126
|
+
@ok="handleDelete"
|
|
127
|
+
@cancel="isOpen = false"
|
|
128
|
+
>
|
|
129
|
+
<p>确定要删除该用户吗?</p>
|
|
130
|
+
</a-modal>
|
|
131
|
+
</template>
|
|
132
|
+
|
|
133
|
+
<script setup lang="ts">
|
|
134
|
+
async function handleDelete() {
|
|
135
|
+
await deleteUser(id)
|
|
136
|
+
message.success('删除成功')
|
|
137
|
+
isOpen.value = false
|
|
138
|
+
onDeleted?.()
|
|
139
|
+
}
|
|
140
|
+
</script>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Props / Emits 类型定义换行
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// ✅ 每个属性一行
|
|
147
|
+
interface UserFormProps {
|
|
148
|
+
user?: User
|
|
149
|
+
onSubmit: (values: CreateUserRequest) => Promise<void>
|
|
150
|
+
onCancel: () => void
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ✅ emits 命名元组每个事件一行
|
|
154
|
+
const emit = defineEmits<{
|
|
155
|
+
success: [userId: string]
|
|
156
|
+
close: []
|
|
157
|
+
error: [message: string]
|
|
158
|
+
}>()
|
|
159
|
+
|
|
160
|
+
// ✅ 联合类型换行
|
|
161
|
+
type UserStatus = 'active' | 'inactive' | 'locked' | 'pending'
|
|
162
|
+
```
|