@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,270 @@
|
|
|
1
|
+
# Vue 组件模式示例
|
|
2
|
+
|
|
3
|
+
Vue 3 中不直接存在 Pipe 的概念,通过 computed 或工具函数实现数据转换。
|
|
4
|
+
|
|
5
|
+
## 数据转换(替代 Pipe)
|
|
6
|
+
|
|
7
|
+
```vue
|
|
8
|
+
<script setup lang="ts">
|
|
9
|
+
// ✅ 方案1: 纯工具函数(无状态转换)
|
|
10
|
+
function formatDate(date: string | Date, style: 'short' | 'long' = 'short'): string {
|
|
11
|
+
const d = typeof date === 'string' ? new Date(date) : date
|
|
12
|
+
if (style === 'short') {
|
|
13
|
+
return d.toLocaleDateString('zh-CN')
|
|
14
|
+
}
|
|
15
|
+
return d.toLocaleDateString('zh-CN', {
|
|
16
|
+
year: 'numeric', month: 'long', day: 'numeric',
|
|
17
|
+
hour: '2-digit', minute: '2-digit',
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatCurrency(amount: number, currency = 'CNY'): string {
|
|
22
|
+
return new Intl.NumberFormat('zh-CN', {
|
|
23
|
+
style: 'currency',
|
|
24
|
+
currency,
|
|
25
|
+
}).format(amount)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ✅ 方案2: computed 属性(有状态转换 — 如搜索/过滤)
|
|
29
|
+
const props = defineProps<{ users: User[] }>()
|
|
30
|
+
const search = ref('')
|
|
31
|
+
|
|
32
|
+
const filteredUsers = computed(() => {
|
|
33
|
+
if (!search.value.trim()) return props.users
|
|
34
|
+
const q = search.value.toLowerCase()
|
|
35
|
+
return props.users.filter(
|
|
36
|
+
u => u.username.toLowerCase().includes(q) || u.email.toLowerCase().includes(q)
|
|
37
|
+
)
|
|
38
|
+
})
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<a-table
|
|
43
|
+
:data-source="filteredUsers"
|
|
44
|
+
:columns="[
|
|
45
|
+
{ title: '创建时间', dataIndex: 'createdAt', customRender: ({ text }) => formatDate(text) },
|
|
46
|
+
{ title: '金额', dataIndex: 'amount', customRender: ({ text }) => formatCurrency(text) },
|
|
47
|
+
]"
|
|
48
|
+
/>
|
|
49
|
+
</template>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 插槽模式(Slots)
|
|
53
|
+
|
|
54
|
+
### 命名插槽 + 默认内容
|
|
55
|
+
|
|
56
|
+
```vue
|
|
57
|
+
<!-- components/Card.vue -->
|
|
58
|
+
<template>
|
|
59
|
+
<div class="card">
|
|
60
|
+
<header v-if="$slots.header">
|
|
61
|
+
<slot name="header" />
|
|
62
|
+
</header>
|
|
63
|
+
<main>
|
|
64
|
+
<slot />
|
|
65
|
+
</main>
|
|
66
|
+
<footer v-if="$slots.footer">
|
|
67
|
+
<slot name="footer" />
|
|
68
|
+
</footer>
|
|
69
|
+
</div>
|
|
70
|
+
</template>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 作用域插槽
|
|
74
|
+
|
|
75
|
+
```vue
|
|
76
|
+
<!-- components/DataList.vue -->
|
|
77
|
+
<script setup lang="ts">
|
|
78
|
+
interface Item {
|
|
79
|
+
id: number
|
|
80
|
+
name: string
|
|
81
|
+
email: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
defineProps<{ items: Item[] }>()
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<template>
|
|
88
|
+
<ul>
|
|
89
|
+
<li v-for="item in items" :key="item.id">
|
|
90
|
+
<slot name="item" :item="item">
|
|
91
|
+
<!-- 默认内容:显示名称 -->
|
|
92
|
+
{{ item.name }}
|
|
93
|
+
</slot>
|
|
94
|
+
</li>
|
|
95
|
+
</ul>
|
|
96
|
+
</template>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```vue
|
|
100
|
+
<!-- 使用作用域插槽 -->
|
|
101
|
+
<DataList :items="users">
|
|
102
|
+
<template #item="{ item }">
|
|
103
|
+
<strong>{{ item.name }}</strong>
|
|
104
|
+
<span class="text-gray-500">({{ item.email }})</span>
|
|
105
|
+
</template>
|
|
106
|
+
</DataList>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## 动态组件
|
|
110
|
+
|
|
111
|
+
```vue
|
|
112
|
+
<script setup lang="ts">
|
|
113
|
+
import { shallowRef } from 'vue'
|
|
114
|
+
import UserList from '@/views/UserList.vue'
|
|
115
|
+
import UserDetail from '@/views/UserDetail.vue'
|
|
116
|
+
import Settings from '@/views/Settings.vue'
|
|
117
|
+
|
|
118
|
+
const currentView = shallowRef(UserList)
|
|
119
|
+
|
|
120
|
+
function switchView(view: 'list' | 'detail' | 'settings') {
|
|
121
|
+
const views = { list: UserList, detail: UserDetail, settings: Settings }
|
|
122
|
+
currentView.value = views[view]
|
|
123
|
+
}
|
|
124
|
+
</script>
|
|
125
|
+
|
|
126
|
+
<template>
|
|
127
|
+
<KeepAlive>
|
|
128
|
+
<component :is="currentView" />
|
|
129
|
+
</KeepAlive>
|
|
130
|
+
</template>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## KeepAlive 缓存组件
|
|
134
|
+
|
|
135
|
+
```vue
|
|
136
|
+
<template>
|
|
137
|
+
<!-- 缓存已激活的视图,避免切换时重新渲染 -->
|
|
138
|
+
<KeepAlive :include="['UserList', 'Dashboard']" :max="10">
|
|
139
|
+
<RouterView />
|
|
140
|
+
</KeepAlive>
|
|
141
|
+
</template>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Transition 动画
|
|
145
|
+
|
|
146
|
+
```vue
|
|
147
|
+
<template>
|
|
148
|
+
<a-button @click="show = !show">
|
|
149
|
+
{{ show ? '隐藏' : '显示' }}
|
|
150
|
+
</a-button>
|
|
151
|
+
|
|
152
|
+
<Transition name="fade" mode="out-in">
|
|
153
|
+
<div v-if="show" key="content">
|
|
154
|
+
内容区域
|
|
155
|
+
</div>
|
|
156
|
+
</Transition>
|
|
157
|
+
</template>
|
|
158
|
+
|
|
159
|
+
<style scoped>
|
|
160
|
+
.fade-enter-active,
|
|
161
|
+
.fade-leave-active {
|
|
162
|
+
transition: opacity 0.3s ease;
|
|
163
|
+
}
|
|
164
|
+
.fade-enter-from,
|
|
165
|
+
.fade-leave-to {
|
|
166
|
+
opacity: 0;
|
|
167
|
+
}
|
|
168
|
+
</style>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## 异步组件
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// components/AsyncWidget.ts
|
|
175
|
+
import { defineAsyncComponent } from 'vue'
|
|
176
|
+
import { Spin } from 'ant-design-vue'
|
|
177
|
+
|
|
178
|
+
export default defineAsyncComponent({
|
|
179
|
+
loader: () => import('./HeavyWidget.vue'),
|
|
180
|
+
loadingComponent: Spin,
|
|
181
|
+
delay: 200,
|
|
182
|
+
timeout: 8000,
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// 路由中的异步组件 — 通过动态 import 实现
|
|
186
|
+
// router/index.ts
|
|
187
|
+
const routes = [
|
|
188
|
+
{
|
|
189
|
+
path: '/dashboard',
|
|
190
|
+
component: () => import('@/views/Dashboard.vue'),
|
|
191
|
+
},
|
|
192
|
+
]
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Provide/Inject 模式
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// injection-keys.ts
|
|
199
|
+
import type { InjectionKey, Ref } from 'vue'
|
|
200
|
+
|
|
201
|
+
export const ThemeKey: InjectionKey<{
|
|
202
|
+
theme: Ref<'light' | 'dark'>
|
|
203
|
+
toggleTheme: () => void
|
|
204
|
+
}> = Symbol('theme')
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
```vue
|
|
208
|
+
<!-- 祖先组件:provide -->
|
|
209
|
+
<script setup lang="ts">
|
|
210
|
+
import { provide, ref } from 'vue'
|
|
211
|
+
import { ThemeKey } from '@/injection-keys'
|
|
212
|
+
|
|
213
|
+
const theme = ref<'light' | 'dark'>('light')
|
|
214
|
+
|
|
215
|
+
function toggleTheme() {
|
|
216
|
+
theme.value = theme.value === 'light' ? 'dark' : 'light'
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
provide(ThemeKey, { theme, toggleTheme })
|
|
220
|
+
</script>
|
|
221
|
+
|
|
222
|
+
<template>
|
|
223
|
+
<slot />
|
|
224
|
+
</template>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
```vue
|
|
228
|
+
<!-- 后代组件:inject -->
|
|
229
|
+
<script setup lang="ts">
|
|
230
|
+
import { inject } from 'vue'
|
|
231
|
+
import { ThemeKey } from '@/injection-keys'
|
|
232
|
+
|
|
233
|
+
const { theme, toggleTheme } = inject(ThemeKey)!
|
|
234
|
+
</script>
|
|
235
|
+
|
|
236
|
+
<template>
|
|
237
|
+
<div :class="theme">
|
|
238
|
+
当前主题: {{ theme }}
|
|
239
|
+
<a-button @click="toggleTheme">切换主题</a-button>
|
|
240
|
+
</div>
|
|
241
|
+
</template>
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## 权限控制(导航守卫 + 组件封装)
|
|
245
|
+
|
|
246
|
+
```vue
|
|
247
|
+
<!-- components/AuthGuard.vue -->
|
|
248
|
+
<script setup lang="ts">
|
|
249
|
+
import { useAuthStore } from '@/stores/auth'
|
|
250
|
+
import { useRouter } from 'vue-router'
|
|
251
|
+
|
|
252
|
+
const auth = useAuthStore()
|
|
253
|
+
const router = useRouter()
|
|
254
|
+
|
|
255
|
+
if (!auth.isAuthenticated) {
|
|
256
|
+
router.replace({ name: 'login', query: { redirect: router.currentRoute.value.fullPath } })
|
|
257
|
+
}
|
|
258
|
+
</script>
|
|
259
|
+
|
|
260
|
+
<template>
|
|
261
|
+
<slot v-if="auth.isAuthenticated" />
|
|
262
|
+
</template>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
```vue
|
|
266
|
+
<!-- 使用 -->
|
|
267
|
+
<AuthGuard>
|
|
268
|
+
<Dashboard />
|
|
269
|
+
</AuthGuard>
|
|
270
|
+
```
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Vue 实体类型定义示例
|
|
2
|
+
|
|
3
|
+
## 基础实体层次
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
// shared/types/entity.ts
|
|
7
|
+
export interface ImmutableEntity {
|
|
8
|
+
readonly id: string
|
|
9
|
+
readonly createdAt: string
|
|
10
|
+
readonly updatedAt: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Entity extends ImmutableEntity {
|
|
14
|
+
version: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Auditable<T> extends Entity {
|
|
18
|
+
createdBy: T
|
|
19
|
+
updatedBy: T
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ImmutableAuditable<T> extends ImmutableEntity {
|
|
23
|
+
createdBy: T
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## API 响应类型
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// shared/types/api.ts
|
|
31
|
+
export interface PageResponse<T> {
|
|
32
|
+
content: T[]
|
|
33
|
+
totalElements: number
|
|
34
|
+
totalPages: number
|
|
35
|
+
number: number
|
|
36
|
+
size: number
|
|
37
|
+
first: boolean
|
|
38
|
+
last: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ApiError {
|
|
42
|
+
code: string
|
|
43
|
+
message: string
|
|
44
|
+
details?: Record<string, string[]>
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 业务实体示例
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// shared/types/user.ts
|
|
52
|
+
export interface User extends Auditable<string> {
|
|
53
|
+
username: string
|
|
54
|
+
email: string
|
|
55
|
+
displayName: string
|
|
56
|
+
avatar?: string
|
|
57
|
+
role: UserRole
|
|
58
|
+
active: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export enum UserRole {
|
|
62
|
+
Admin = 'ADMIN',
|
|
63
|
+
Editor = 'EDITOR',
|
|
64
|
+
Viewer = 'VIEWER',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface CreateUserRequest {
|
|
68
|
+
username: string
|
|
69
|
+
email: string
|
|
70
|
+
displayName: string
|
|
71
|
+
role: UserRole
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface UpdateUserRequest {
|
|
75
|
+
email?: string
|
|
76
|
+
displayName?: string
|
|
77
|
+
role?: UserRole
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 组件 Props 类型(Vue 方式)
|
|
82
|
+
|
|
83
|
+
```vue
|
|
84
|
+
<!-- components/UserAvatar.vue -->
|
|
85
|
+
<script setup lang="ts">
|
|
86
|
+
import type { User } from '@/shared/types/user'
|
|
87
|
+
|
|
88
|
+
interface UserAvatarProps {
|
|
89
|
+
user: Pick<User, 'username' | 'avatar'>
|
|
90
|
+
size?: 'sm' | 'md' | 'lg'
|
|
91
|
+
onClick?: () => void
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const props = withDefaults(defineProps<UserAvatarProps>(), {
|
|
95
|
+
size: 'md',
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const emit = defineEmits<{
|
|
99
|
+
click: [user: User]
|
|
100
|
+
}>()
|
|
101
|
+
|
|
102
|
+
function handleClick() {
|
|
103
|
+
if (props.onClick) {
|
|
104
|
+
props.onClick()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<template>
|
|
110
|
+
<a-avatar
|
|
111
|
+
:src="user.avatar"
|
|
112
|
+
:alt="user.username"
|
|
113
|
+
:size="size === 'sm' ? 24 : size === 'md' ? 32 : 48"
|
|
114
|
+
:style="{ cursor: onClick ? 'pointer' : 'default' }"
|
|
115
|
+
@click="handleClick"
|
|
116
|
+
/>
|
|
117
|
+
</template>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```vue
|
|
121
|
+
<!-- views/UserListPage.vue — 页面组件:默认导出 -->
|
|
122
|
+
<script setup lang="ts">
|
|
123
|
+
import { useUsers } from '@/composables/useUsers'
|
|
124
|
+
|
|
125
|
+
const { users, loading } = useUsers()
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
<template>
|
|
129
|
+
<UserTable :users="users" :loading="loading" />
|
|
130
|
+
</template>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Discriminated Union 类型
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// 用于多状态组件
|
|
137
|
+
export type NotificationType = 'success' | 'error' | 'warning' | 'info'
|
|
138
|
+
|
|
139
|
+
export interface NotificationConfig {
|
|
140
|
+
type: NotificationType
|
|
141
|
+
title: string
|
|
142
|
+
message: string
|
|
143
|
+
duration?: number
|
|
144
|
+
}
|
|
145
|
+
```
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Vue 表单处理示例(Ant Design Vue)
|
|
2
|
+
|
|
3
|
+
## 创建用户表单
|
|
4
|
+
|
|
5
|
+
```vue
|
|
6
|
+
<!-- components/CreateUserForm.vue -->
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import { reactive, ref } from 'vue'
|
|
9
|
+
import { message } from 'ant-design-vue'
|
|
10
|
+
import type { UserRole } from '@/shared/types/user'
|
|
11
|
+
|
|
12
|
+
interface FormState {
|
|
13
|
+
username: string
|
|
14
|
+
email: string
|
|
15
|
+
displayName: string
|
|
16
|
+
role: UserRole | undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits<{
|
|
20
|
+
success: [userId: string]
|
|
21
|
+
}>()
|
|
22
|
+
|
|
23
|
+
const formState = reactive<FormState>({
|
|
24
|
+
username: '',
|
|
25
|
+
email: '',
|
|
26
|
+
displayName: '',
|
|
27
|
+
role: undefined,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const loading = ref(false)
|
|
31
|
+
const formRef = ref()
|
|
32
|
+
|
|
33
|
+
// 校验规则
|
|
34
|
+
const rules = {
|
|
35
|
+
username: [
|
|
36
|
+
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
|
37
|
+
{ min: 3, max: 20, message: '用户名长度 3-20 个字符', trigger: 'blur' },
|
|
38
|
+
],
|
|
39
|
+
email: [
|
|
40
|
+
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
|
41
|
+
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' },
|
|
42
|
+
],
|
|
43
|
+
displayName: [
|
|
44
|
+
{ required: true, message: '请输入显示名称', trigger: 'blur' },
|
|
45
|
+
],
|
|
46
|
+
role: [
|
|
47
|
+
{ required: true, message: '请选择角色', trigger: 'change' },
|
|
48
|
+
],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function handleSubmit() {
|
|
52
|
+
try {
|
|
53
|
+
await formRef.value?.validate()
|
|
54
|
+
loading.value = true
|
|
55
|
+
|
|
56
|
+
const res = await fetch('/api/v1/users', {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: { 'Content-Type': 'application/json' },
|
|
59
|
+
body: JSON.stringify(formState),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
if (!res.ok) throw new Error('创建失败')
|
|
63
|
+
const user = await res.json()
|
|
64
|
+
|
|
65
|
+
message.success('用户创建成功')
|
|
66
|
+
emit('success', user.id)
|
|
67
|
+
formRef.value?.resetFields()
|
|
68
|
+
} catch (e) {
|
|
69
|
+
if (e instanceof Error) {
|
|
70
|
+
message.error(e.message)
|
|
71
|
+
}
|
|
72
|
+
} finally {
|
|
73
|
+
loading.value = false
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function handleReset() {
|
|
78
|
+
formRef.value?.resetFields()
|
|
79
|
+
}
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<template>
|
|
83
|
+
<a-form
|
|
84
|
+
ref="formRef"
|
|
85
|
+
:model="formState"
|
|
86
|
+
:rules="rules"
|
|
87
|
+
:label-col="{ span: 6 }"
|
|
88
|
+
:wrapper-col="{ span: 16 }"
|
|
89
|
+
@finish="handleSubmit"
|
|
90
|
+
>
|
|
91
|
+
<a-form-item label="用户名" name="username">
|
|
92
|
+
<a-input v-model:value="formState.username" placeholder="请输入用户名" />
|
|
93
|
+
</a-form-item>
|
|
94
|
+
|
|
95
|
+
<a-form-item label="邮箱" name="email">
|
|
96
|
+
<a-input v-model:value="formState.email" placeholder="请输入邮箱" />
|
|
97
|
+
</a-form-item>
|
|
98
|
+
|
|
99
|
+
<a-form-item label="显示名称" name="displayName">
|
|
100
|
+
<a-input v-model:value="formState.displayName" placeholder="请输入显示名称" />
|
|
101
|
+
</a-form-item>
|
|
102
|
+
|
|
103
|
+
<a-form-item label="角色" name="role">
|
|
104
|
+
<a-select v-model:value="formState.role" placeholder="请选择角色">
|
|
105
|
+
<a-select-option value="ADMIN">管理员</a-select-option>
|
|
106
|
+
<a-select-option value="EDITOR">编辑</a-select-option>
|
|
107
|
+
<a-select-option value="VIEWER">查看者</a-select-option>
|
|
108
|
+
</a-select>
|
|
109
|
+
</a-form-item>
|
|
110
|
+
|
|
111
|
+
<a-form-item :wrapper-col="{ offset: 6, span: 16 }">
|
|
112
|
+
<a-button type="primary" html-type="submit" :loading="loading">
|
|
113
|
+
{{ loading ? '创建中...' : '创建' }}
|
|
114
|
+
</a-button>
|
|
115
|
+
<a-button style="margin-left: 8px" @click="handleReset">
|
|
116
|
+
重置
|
|
117
|
+
</a-button>
|
|
118
|
+
</a-form-item>
|
|
119
|
+
</a-form>
|
|
120
|
+
</template>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## 编辑用户信息 Modal
|
|
124
|
+
|
|
125
|
+
```vue
|
|
126
|
+
<!-- components/EditUserModal.vue -->
|
|
127
|
+
<script setup lang="ts">
|
|
128
|
+
import { reactive, ref, watch } from 'vue'
|
|
129
|
+
import { message, Modal } from 'ant-design-vue'
|
|
130
|
+
import type { User } from '@/shared/types/user'
|
|
131
|
+
|
|
132
|
+
interface EditForm {
|
|
133
|
+
email: string
|
|
134
|
+
displayName: string
|
|
135
|
+
active: boolean
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const props = defineProps<{
|
|
139
|
+
visible: boolean
|
|
140
|
+
user: User | null
|
|
141
|
+
}>()
|
|
142
|
+
|
|
143
|
+
const emit = defineEmits<{
|
|
144
|
+
close: []
|
|
145
|
+
saved: []
|
|
146
|
+
}>()
|
|
147
|
+
|
|
148
|
+
const formState = reactive<EditForm>({
|
|
149
|
+
email: '',
|
|
150
|
+
displayName: '',
|
|
151
|
+
active: true,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const formRef = ref()
|
|
155
|
+
const loading = ref(false)
|
|
156
|
+
|
|
157
|
+
// 监听 user 变化填充表单
|
|
158
|
+
watch(() => props.user, (user) => {
|
|
159
|
+
if (user) {
|
|
160
|
+
formState.email = user.email
|
|
161
|
+
formState.displayName = user.displayName
|
|
162
|
+
formState.active = user.active
|
|
163
|
+
}
|
|
164
|
+
}, { immediate: true })
|
|
165
|
+
|
|
166
|
+
const rules = {
|
|
167
|
+
email: [
|
|
168
|
+
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
|
169
|
+
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' },
|
|
170
|
+
],
|
|
171
|
+
displayName: [
|
|
172
|
+
{ required: true, message: '请输入显示名称', trigger: 'blur' },
|
|
173
|
+
],
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function handleOk() {
|
|
177
|
+
try {
|
|
178
|
+
await formRef.value?.validate()
|
|
179
|
+
loading.value = true
|
|
180
|
+
|
|
181
|
+
const res = await fetch(`/api/v1/users/${props.user!.id}`, {
|
|
182
|
+
method: 'PUT',
|
|
183
|
+
headers: { 'Content-Type': 'application/json' },
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
email: formState.email,
|
|
186
|
+
displayName: formState.displayName,
|
|
187
|
+
}),
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
if (!res.ok) throw new Error('更新失败')
|
|
191
|
+
message.success('更新成功')
|
|
192
|
+
emit('saved')
|
|
193
|
+
} catch (e) {
|
|
194
|
+
if (e instanceof Error) {
|
|
195
|
+
message.error(e.message)
|
|
196
|
+
}
|
|
197
|
+
} finally {
|
|
198
|
+
loading.value = false
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function handleCancel() {
|
|
203
|
+
emit('close')
|
|
204
|
+
}
|
|
205
|
+
</script>
|
|
206
|
+
|
|
207
|
+
<template>
|
|
208
|
+
<a-modal
|
|
209
|
+
:visible="visible"
|
|
210
|
+
title="编辑用户"
|
|
211
|
+
:confirm-loading="loading"
|
|
212
|
+
destroy-on-close
|
|
213
|
+
@ok="handleOk"
|
|
214
|
+
@cancel="handleCancel"
|
|
215
|
+
>
|
|
216
|
+
<a-form ref="formRef" :model="formState" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
|
|
217
|
+
<a-form-item label="邮箱" name="email">
|
|
218
|
+
<a-input v-model:value="formState.email" />
|
|
219
|
+
</a-form-item>
|
|
220
|
+
|
|
221
|
+
<a-form-item label="显示名称" name="displayName">
|
|
222
|
+
<a-input v-model:value="formState.displayName" />
|
|
223
|
+
</a-form-item>
|
|
224
|
+
|
|
225
|
+
<a-form-item label="状态" name="active">
|
|
226
|
+
<a-switch v-model:checked="formState.active" />
|
|
227
|
+
</a-form-item>
|
|
228
|
+
</a-form>
|
|
229
|
+
</a-modal>
|
|
230
|
+
</template>
|
|
231
|
+
```
|