@lvetechs/create-app 1.0.4 → 1.0.6
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/templates/react/.env +3 -3
- package/templates/react/.env.development +3 -3
- package/templates/react/.env.production +3 -3
- package/templates/react/package.json +1 -1
- package/templates/react/pnpm-lock.yaml +5 -5
- package/templates/react/src/App.tsx +7 -1
- package/templates/react/src/api/notification.ts +43 -0
- package/templates/react/src/components/NotificationButton/index.tsx +219 -0
- package/templates/react/src/components/Toast/index.tsx +150 -0
- package/templates/react/src/hooks/useForm.ts +77 -0
- package/templates/react/src/layouts/DefaultLayout.tsx +2 -0
- package/templates/react/src/layouts/menuConfig.ts +3 -33
- package/templates/react/src/router/index.tsx +0 -39
- package/templates/react/src/stores/app.ts +141 -3
- package/templates/react/src/stores/notification.ts +146 -0
- package/templates/react/src/stores/permission.ts +173 -0
- package/templates/react/src/stores/user.ts +151 -4
- package/templates/react/src/views/home/index.tsx +167 -6
- package/templates/react/src/views/system/user/index.tsx +171 -5
- package/templates/vue/.env +2 -2
- package/templates/vue/.env.development +2 -2
- package/templates/vue/.env.production +2 -2
- package/templates/vue/pnpm-lock.yaml +3307 -3307
- package/templates/vue/src/App.vue +2 -0
- package/templates/vue/src/api/notification.ts +43 -0
- package/templates/vue/src/auto-imports.d.ts +5 -0
- package/templates/vue/src/components/NotificationButton/index.vue +242 -0
- package/templates/vue/src/components/Toast/index.vue +126 -0
- package/templates/vue/src/components.d.ts +2 -0
- package/templates/vue/src/layouts/DefaultLayout.vue +2 -0
- package/templates/vue/src/layouts/menuConfig.ts +3 -33
- package/templates/vue/src/router/index.ts +3 -88
- package/templates/vue/src/stores/app.ts +133 -2
- package/templates/vue/src/stores/notification.ts +189 -0
- package/templates/vue/src/stores/permission.ts +184 -0
- package/templates/vue/src/stores/user.ts +109 -2
- package/templates/vue/src/views/home/index.vue +7 -7
- package/templates/react/src/views/about/index.tsx +0 -40
- package/templates/react/src/views/login/index.tsx +0 -138
- package/templates/react/src/views/register/index.tsx +0 -143
- package/templates/react/src/views/result/fail.tsx +0 -39
- package/templates/react/src/views/result/success.tsx +0 -35
- package/templates/react/src/views/screen/index.tsx +0 -120
- package/templates/react/src/views/system/log/login.tsx +0 -51
- package/templates/react/src/views/system/log/operation.tsx +0 -47
- package/templates/react/src/views/system/menu/index.tsx +0 -62
- package/templates/react/src/views/system/role/index.tsx +0 -63
- package/templates/vue/src/views/about/index.vue +0 -67
- package/templates/vue/src/views/login/index.vue +0 -153
- package/templates/vue/src/views/register/index.vue +0 -169
- package/templates/vue/src/views/result/fail.vue +0 -92
- package/templates/vue/src/views/result/success.vue +0 -92
- package/templates/vue/src/views/screen/index.vue +0 -150
- package/templates/vue/src/views/system/log/login.vue +0 -51
- package/templates/vue/src/views/system/log/operation.vue +0 -47
- package/templates/vue/src/views/system/menu/index.vue +0 -58
- package/templates/vue/src/views/system/role/index.vue +0 -59
|
@@ -7,33 +7,180 @@ export interface UserInfo {
|
|
|
7
7
|
nickname: string
|
|
8
8
|
avatar: string
|
|
9
9
|
roles: string[]
|
|
10
|
+
email?: string
|
|
11
|
+
phone?: string
|
|
12
|
+
department?: string
|
|
13
|
+
lastLoginTime?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface LoginHistoryItem {
|
|
17
|
+
time: number
|
|
18
|
+
ip: string
|
|
19
|
+
location?: string
|
|
10
20
|
}
|
|
11
21
|
|
|
12
22
|
interface UserState {
|
|
13
23
|
token: string
|
|
14
24
|
userInfo: UserInfo | null
|
|
25
|
+
/** 是否已登录(由 token 推导,也会被显式维护一份,方便订阅) */
|
|
15
26
|
isLoggedIn: boolean
|
|
27
|
+
/** 登录历史(最近若干次登录) */
|
|
28
|
+
loginHistory: LoginHistoryItem[]
|
|
29
|
+
|
|
30
|
+
// ========== 计算相关(通过方法计算,而不是状态字段) ==========
|
|
31
|
+
/** 用户展示名称 */
|
|
32
|
+
getDisplayName: () => string
|
|
33
|
+
/** 用户头像地址 */
|
|
34
|
+
getAvatar: () => string
|
|
35
|
+
/** 是否管理员 */
|
|
36
|
+
getIsAdmin: () => boolean
|
|
37
|
+
|
|
38
|
+
// ========== 基础方法 ==========
|
|
16
39
|
setToken: (val: string) => void
|
|
17
40
|
setUserInfo: (info: UserInfo) => void
|
|
41
|
+
updateUserInfo: (updates: Partial<UserInfo>) => void
|
|
42
|
+
|
|
43
|
+
addLoginHistory: (ip: string, location?: string) => void
|
|
44
|
+
|
|
45
|
+
hasRole: (role: string) => boolean
|
|
46
|
+
addRole: (role: string) => void
|
|
47
|
+
removeRole: (role: string) => void
|
|
48
|
+
|
|
18
49
|
logout: () => void
|
|
50
|
+
|
|
51
|
+
/** 模拟登录流程(实际项目中可替换为真实接口) */
|
|
52
|
+
login: (username: string, password: string) => Promise<UserInfo>
|
|
19
53
|
}
|
|
20
54
|
|
|
21
55
|
export const useUserStore = create<UserState>()(
|
|
22
56
|
persist(
|
|
23
|
-
(set) => ({
|
|
57
|
+
(set, get) => ({
|
|
24
58
|
token: '',
|
|
25
59
|
userInfo: null,
|
|
26
60
|
isLoggedIn: false,
|
|
61
|
+
loginHistory: [],
|
|
62
|
+
|
|
63
|
+
// 计算相关
|
|
64
|
+
getDisplayName: () => {
|
|
65
|
+
const user = get().userInfo
|
|
66
|
+
return user?.nickname || user?.username || '未登录'
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
getAvatar: () => {
|
|
70
|
+
const user = get().userInfo
|
|
71
|
+
return user?.avatar || '/default-avatar.png'
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
getIsAdmin: () => {
|
|
75
|
+
const user = get().userInfo
|
|
76
|
+
return user?.roles?.includes('admin') ?? false
|
|
77
|
+
},
|
|
27
78
|
|
|
79
|
+
// 基础方法
|
|
28
80
|
setToken: (val) => set({ token: val, isLoggedIn: !!val }),
|
|
29
81
|
|
|
30
|
-
setUserInfo: (info) =>
|
|
82
|
+
setUserInfo: (info) =>
|
|
83
|
+
set(() => ({
|
|
84
|
+
userInfo: {
|
|
85
|
+
...info,
|
|
86
|
+
lastLoginTime: info.lastLoginTime ?? Date.now()
|
|
87
|
+
}
|
|
88
|
+
})),
|
|
89
|
+
|
|
90
|
+
updateUserInfo: (updates) =>
|
|
91
|
+
set((state) =>
|
|
92
|
+
state.userInfo
|
|
93
|
+
? {
|
|
94
|
+
userInfo: {
|
|
95
|
+
...state.userInfo,
|
|
96
|
+
...updates
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
: state
|
|
100
|
+
),
|
|
101
|
+
|
|
102
|
+
addLoginHistory: (ip, location) =>
|
|
103
|
+
set((state) => {
|
|
104
|
+
const next: LoginHistoryItem[] = [
|
|
105
|
+
{
|
|
106
|
+
time: Date.now(),
|
|
107
|
+
ip,
|
|
108
|
+
location
|
|
109
|
+
},
|
|
110
|
+
...state.loginHistory
|
|
111
|
+
]
|
|
112
|
+
// 只保留最近 10 条
|
|
113
|
+
if (next.length > 10) {
|
|
114
|
+
next.length = 10
|
|
115
|
+
}
|
|
116
|
+
return { loginHistory: next }
|
|
117
|
+
}),
|
|
118
|
+
|
|
119
|
+
hasRole: (role) => {
|
|
120
|
+
const user = get().userInfo
|
|
121
|
+
return user?.roles?.includes(role) ?? false
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
addRole: (role) =>
|
|
125
|
+
set((state) => {
|
|
126
|
+
const user = state.userInfo
|
|
127
|
+
if (!user) return state
|
|
128
|
+
if (user.roles.includes(role)) return state
|
|
129
|
+
return {
|
|
130
|
+
userInfo: {
|
|
131
|
+
...user,
|
|
132
|
+
roles: [...user.roles, role]
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}),
|
|
136
|
+
|
|
137
|
+
removeRole: (role) =>
|
|
138
|
+
set((state) => {
|
|
139
|
+
const user = state.userInfo
|
|
140
|
+
if (!user) return state
|
|
141
|
+
return {
|
|
142
|
+
userInfo: {
|
|
143
|
+
...user,
|
|
144
|
+
roles: user.roles.filter((r) => r !== role)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}),
|
|
148
|
+
|
|
149
|
+
logout: () => set({ token: '', userInfo: null, isLoggedIn: false }),
|
|
150
|
+
|
|
151
|
+
// 模拟登录(可在实际项目中替换成真正的 API 调用)
|
|
152
|
+
login: async (username, _password) => {
|
|
153
|
+
// 模拟异步请求
|
|
154
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
155
|
+
|
|
156
|
+
const mockUserInfo: UserInfo = {
|
|
157
|
+
id: 1,
|
|
158
|
+
username,
|
|
159
|
+
nickname: username === 'admin' ? '管理员' : '普通用户',
|
|
160
|
+
avatar: '/avatar.png',
|
|
161
|
+
roles: username === 'admin' ? ['admin'] : ['viewer'],
|
|
162
|
+
email: `${username}@example.com`,
|
|
163
|
+
lastLoginTime: Date.now()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
set({
|
|
167
|
+
token: `mock-token-${Date.now()}`,
|
|
168
|
+
userInfo: mockUserInfo,
|
|
169
|
+
isLoggedIn: true
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
get().addLoginHistory('192.168.1.100', '本地')
|
|
31
173
|
|
|
32
|
-
|
|
174
|
+
return mockUserInfo
|
|
175
|
+
}
|
|
33
176
|
}),
|
|
34
177
|
{
|
|
35
178
|
name: 'user-store',
|
|
36
|
-
partialize: (state) => ({
|
|
179
|
+
partialize: (state) => ({
|
|
180
|
+
token: state.token,
|
|
181
|
+
userInfo: state.userInfo,
|
|
182
|
+
loginHistory: state.loginHistory
|
|
183
|
+
})
|
|
37
184
|
}
|
|
38
185
|
)
|
|
39
186
|
)
|
|
@@ -1,13 +1,159 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createContext, useContext, useReducer } from 'react'
|
|
2
2
|
import '@/styles/page-common.scss'
|
|
3
|
-
import { Button, Container, Input, Tabs } from '@lvetechs/ui-lib'
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { Button, Card, Container, Input, Tabs } from '@lvetechs/ui-lib'
|
|
4
|
+
import { useForm } from '@/hooks/useForm'
|
|
5
|
+
|
|
6
|
+
// ================ useContext + useReducer 示例 ================
|
|
7
|
+
|
|
8
|
+
interface CounterState {
|
|
9
|
+
count: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type CounterAction = { type: 'increment' } | { type: 'decrement' } | { type: 'reset' }
|
|
13
|
+
|
|
14
|
+
const CounterContext = createContext<
|
|
15
|
+
| {
|
|
16
|
+
state: CounterState
|
|
17
|
+
dispatch: React.Dispatch<CounterAction>
|
|
18
|
+
}
|
|
19
|
+
| undefined
|
|
20
|
+
>(undefined)
|
|
21
|
+
|
|
22
|
+
function counterReducer(state: CounterState, action: CounterAction): CounterState {
|
|
23
|
+
switch (action.type) {
|
|
24
|
+
case 'increment':
|
|
25
|
+
return { count: state.count + 1 }
|
|
26
|
+
case 'decrement':
|
|
27
|
+
return { count: state.count - 1 }
|
|
28
|
+
case 'reset':
|
|
29
|
+
return { count: 0 }
|
|
30
|
+
default:
|
|
31
|
+
return state
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function useCounter() {
|
|
36
|
+
const ctx = useContext(CounterContext)
|
|
37
|
+
if (!ctx) {
|
|
38
|
+
throw new Error('useCounter 必须在 <CounterProvider> 内使用')
|
|
39
|
+
}
|
|
40
|
+
return ctx
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function CounterProvider({ children }: { children: React.ReactNode }) {
|
|
44
|
+
const [state, dispatch] = useReducer(counterReducer, { count: 0 })
|
|
45
|
+
|
|
46
|
+
return <CounterContext.Provider value={{ state, dispatch }}>{children}</CounterContext.Provider>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function CounterSection() {
|
|
50
|
+
const {
|
|
51
|
+
state: { count },
|
|
52
|
+
dispatch
|
|
53
|
+
} = useCounter()
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Card padding="md">
|
|
57
|
+
<h3 className="mb-2 text-lg font-semibold">useContext + useReducer 计数器示例</h3>
|
|
58
|
+
<p className="mb-4 text-3xl font-bold text-indigo-600">{count}</p>
|
|
59
|
+
<Container direction="row" gap="sm">
|
|
60
|
+
<Button onClick={() => dispatch({ type: 'increment' })}>+1</Button>
|
|
61
|
+
<Button onClick={() => dispatch({ type: 'decrement' })}>-1</Button>
|
|
62
|
+
<Button onClick={() => dispatch({ type: 'reset' })}>重置</Button>
|
|
63
|
+
</Container>
|
|
64
|
+
</Card>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ================ 表单 Hook 示例(useForm) ================
|
|
69
|
+
|
|
70
|
+
interface ProfileFormValues {
|
|
71
|
+
name: string
|
|
72
|
+
email: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function validateProfile(values: ProfileFormValues) {
|
|
76
|
+
const errors: Partial<Record<keyof ProfileFormValues, string>> = {}
|
|
77
|
+
if (!values.name.trim()) {
|
|
78
|
+
errors.name = '请输入姓名'
|
|
79
|
+
}
|
|
80
|
+
if (!values.email.trim()) {
|
|
81
|
+
errors.email = '请输入邮箱'
|
|
82
|
+
} else if (!/^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/.test(values.email)) {
|
|
83
|
+
errors.email = '邮箱格式不正确'
|
|
84
|
+
}
|
|
85
|
+
return errors
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function ProfileFormSection() {
|
|
89
|
+
const { values, errors, submitting, handleChange, handleSubmit, reset } = useForm<ProfileFormValues>(
|
|
90
|
+
{
|
|
91
|
+
initialValues: {
|
|
92
|
+
name: '',
|
|
93
|
+
email: ''
|
|
94
|
+
},
|
|
95
|
+
validate: validateProfile,
|
|
96
|
+
onSubmit: async (formValues) => {
|
|
97
|
+
// eslint-disable-next-line no-console
|
|
98
|
+
console.log('提交的表单数据', formValues)
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, 800))
|
|
100
|
+
alert('提交成功(请在控制台查看提交数据)')
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<Card padding="md">
|
|
107
|
+
<h3 className="mb-2 text-lg font-semibold">useForm 表单示例</h3>
|
|
108
|
+
<p className="mb-4 text-sm text-slate-500">
|
|
109
|
+
演示如何用自定义 Hook 管理表单值、校验和提交状态。
|
|
110
|
+
</p>
|
|
111
|
+
<form className="space-y-4" onSubmit={handleSubmit}>
|
|
112
|
+
<div>
|
|
113
|
+
<label className="mb-1 block text-sm font-medium text-slate-700" htmlFor="name">
|
|
114
|
+
姓名
|
|
115
|
+
</label>
|
|
116
|
+
<Input
|
|
117
|
+
id="name"
|
|
118
|
+
name="name"
|
|
119
|
+
placeholder="请输入姓名"
|
|
120
|
+
value={values.name}
|
|
121
|
+
onChange={handleChange}
|
|
122
|
+
/>
|
|
123
|
+
{errors.name && <p className="mt-1 text-xs text-rose-500">{errors.name}</p>}
|
|
124
|
+
</div>
|
|
125
|
+
<div>
|
|
126
|
+
<label className="mb-1 block text-sm font-medium text-slate-700" htmlFor="email">
|
|
127
|
+
邮箱
|
|
128
|
+
</label>
|
|
129
|
+
<Input
|
|
130
|
+
id="email"
|
|
131
|
+
name="email"
|
|
132
|
+
type="email"
|
|
133
|
+
placeholder="请输入邮箱"
|
|
134
|
+
value={values.email}
|
|
135
|
+
onChange={handleChange}
|
|
136
|
+
/>
|
|
137
|
+
{errors.email && <p className="mt-1 text-xs text-rose-500">{errors.email}</p>}
|
|
138
|
+
</div>
|
|
139
|
+
<Container direction="row" gap="sm">
|
|
140
|
+
<Button type="submit" disabled={submitting}>
|
|
141
|
+
{submitting ? '提交中...' : '提交'}
|
|
142
|
+
</Button>
|
|
143
|
+
<Button type="button" onClick={reset}>
|
|
144
|
+
重置
|
|
145
|
+
</Button>
|
|
146
|
+
</Container>
|
|
147
|
+
</form>
|
|
148
|
+
</Card>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
6
151
|
|
|
152
|
+
export default function Home() {
|
|
7
153
|
return (
|
|
8
154
|
<div className="home-page" style={{ maxWidth: 900, margin: '0 auto' }}>
|
|
9
155
|
<div className="page-card">
|
|
10
|
-
<h1 style={{ fontSize: 28, marginBottom: 12 }}
|
|
156
|
+
<h1 style={{ fontSize: 28, marginBottom: 12 }}>欢迎使用 {import.meta.env.VITE_APP_TITLE}</h1>
|
|
11
157
|
<p style={{ color: 'var(--text-color-secondary)', fontSize: 15, marginBottom: 24 }}>
|
|
12
158
|
该模板创建自 @lvetechs/create-app react模板
|
|
13
159
|
</p>
|
|
@@ -48,8 +194,23 @@ export default function Home() {
|
|
|
48
194
|
},
|
|
49
195
|
{
|
|
50
196
|
label: 'TailwindCSS', key: 'TailwindCSS', children: <div className="feature-card">
|
|
51
|
-
实用优先的
|
|
197
|
+
实用优先的 CSS 框架
|
|
52
198
|
</div>
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
label: 'React Hooks', key: 'React Hooks', children: (
|
|
202
|
+
<CounterProvider>
|
|
203
|
+
<div className="space-y-4">
|
|
204
|
+
<p className="text-sm text-slate-500 mb-4">
|
|
205
|
+
演示 useContext、useReducer 和自定义 useForm Hook 的实际应用
|
|
206
|
+
</p>
|
|
207
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
208
|
+
<CounterSection />
|
|
209
|
+
<ProfileFormSection />
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</CounterProvider>
|
|
213
|
+
)
|
|
53
214
|
}
|
|
54
215
|
]} defaultValue="@lvetechs/ui-lib" />
|
|
55
216
|
</div>
|
|
@@ -1,5 +1,39 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
1
2
|
import { useNavigate } from 'react-router-dom'
|
|
2
3
|
import '@/styles/page-common.scss'
|
|
4
|
+
import { Button, Input, Dialog } from '@lvetechs/ui-lib'
|
|
5
|
+
import { useForm } from '@/hooks/useForm'
|
|
6
|
+
|
|
7
|
+
// 如果 @lvetechs/ui-lib 没有 Select,自己实现一个简单的 Select
|
|
8
|
+
interface SelectProps {
|
|
9
|
+
name?: string
|
|
10
|
+
value: string
|
|
11
|
+
onChange: (value: string) => void
|
|
12
|
+
options: Array<{ label: string; value: string }>
|
|
13
|
+
placeholder?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function Select({ name, value, onChange, options, placeholder }: SelectProps) {
|
|
17
|
+
return (
|
|
18
|
+
<select
|
|
19
|
+
name={name}
|
|
20
|
+
className="w-full rounded border border-slate-200 px-3 py-2 text-sm outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500"
|
|
21
|
+
value={value}
|
|
22
|
+
onChange={(e) => onChange(e.target.value)}
|
|
23
|
+
>
|
|
24
|
+
{placeholder && (
|
|
25
|
+
<option value="" disabled>
|
|
26
|
+
{placeholder}
|
|
27
|
+
</option>
|
|
28
|
+
)}
|
|
29
|
+
{options.map((option) => (
|
|
30
|
+
<option key={option.value} value={option.value}>
|
|
31
|
+
{option.label}
|
|
32
|
+
</option>
|
|
33
|
+
))}
|
|
34
|
+
</select>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
3
37
|
|
|
4
38
|
const users = [
|
|
5
39
|
{ id: 1, username: 'admin', nickname: '管理员', role: '超级管理员', status: '启用' },
|
|
@@ -7,15 +41,147 @@ const users = [
|
|
|
7
41
|
{ id: 3, username: 'viewer', nickname: '访客', role: '只读', status: '禁用' }
|
|
8
42
|
]
|
|
9
43
|
|
|
44
|
+
interface UserFormValues {
|
|
45
|
+
username: string
|
|
46
|
+
nickname: string
|
|
47
|
+
email: string
|
|
48
|
+
role: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function validateUserForm(values: UserFormValues) {
|
|
52
|
+
const errors: Partial<Record<keyof UserFormValues, string>> = {}
|
|
53
|
+
if (!values.username.trim()) {
|
|
54
|
+
errors.username = '请输入用户名'
|
|
55
|
+
}
|
|
56
|
+
if (!values.nickname.trim()) {
|
|
57
|
+
errors.nickname = '请输入昵称'
|
|
58
|
+
}
|
|
59
|
+
if (!values.email.trim()) {
|
|
60
|
+
errors.email = '请输入邮箱'
|
|
61
|
+
} else if (!/^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/.test(values.email)) {
|
|
62
|
+
errors.email = '邮箱格式不正确'
|
|
63
|
+
}
|
|
64
|
+
if (!values.role.trim()) {
|
|
65
|
+
errors.role = '请选择角色'
|
|
66
|
+
}
|
|
67
|
+
return errors
|
|
68
|
+
}
|
|
69
|
+
|
|
10
70
|
export default function SystemUser() {
|
|
11
71
|
const navigate = useNavigate()
|
|
72
|
+
const [showForm, setShowForm] = useState(false)
|
|
73
|
+
|
|
74
|
+
const { values, errors, submitting, handleChange, handleSubmit, reset } = useForm<UserFormValues>({
|
|
75
|
+
initialValues: {
|
|
76
|
+
username: '',
|
|
77
|
+
nickname: '',
|
|
78
|
+
email: '',
|
|
79
|
+
role: ''
|
|
80
|
+
},
|
|
81
|
+
validate: validateUserForm,
|
|
82
|
+
onSubmit: async (formValues) => {
|
|
83
|
+
// eslint-disable-next-line no-console
|
|
84
|
+
console.log('新增用户', formValues)
|
|
85
|
+
await new Promise((resolve) => setTimeout(resolve, 800))
|
|
86
|
+
alert(`用户 ${formValues.username} 创建成功!`)
|
|
87
|
+
setShowForm(false)
|
|
88
|
+
reset()
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const handleOpenForm = () => {
|
|
93
|
+
setShowForm(true)
|
|
94
|
+
reset()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const handleCloseForm = () => {
|
|
98
|
+
setShowForm(false)
|
|
99
|
+
reset()
|
|
100
|
+
}
|
|
12
101
|
|
|
13
102
|
return (
|
|
14
103
|
<div className="page-container">
|
|
15
104
|
<div className="page-header">
|
|
16
105
|
<h2>用户管理</h2>
|
|
17
|
-
<
|
|
106
|
+
<Button onClick={handleOpenForm}>+ 新增用户</Button>
|
|
18
107
|
</div>
|
|
108
|
+
|
|
109
|
+
{/* 新增用户表单弹窗(使用 @lvetechs/ui-lib 的 Dialog 作为容器) */}
|
|
110
|
+
{showForm && (
|
|
111
|
+
<Dialog
|
|
112
|
+
visible={showForm}
|
|
113
|
+
maskClosable={false}
|
|
114
|
+
onClose={handleCloseForm}
|
|
115
|
+
onOk={handleSubmit}
|
|
116
|
+
okText={submitting ? '提交中...' : '提交'}
|
|
117
|
+
cancelText='取消'
|
|
118
|
+
onCancel={handleCloseForm}
|
|
119
|
+
>
|
|
120
|
+
<div className="space-y-4">
|
|
121
|
+
<label className="mb-1 block text-sm font-medium text-slate-700" htmlFor="username">
|
|
122
|
+
用户名 <span className="text-rose-500">*</span>
|
|
123
|
+
</label>
|
|
124
|
+
<Input
|
|
125
|
+
id="username"
|
|
126
|
+
name="username"
|
|
127
|
+
placeholder="请输入用户名"
|
|
128
|
+
value={values.username}
|
|
129
|
+
onChange={handleChange}
|
|
130
|
+
/>
|
|
131
|
+
{errors.username && (
|
|
132
|
+
<p className="mt-1 text-xs text-rose-500">{errors.username}</p>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
<div>
|
|
136
|
+
<label className="mb-1 block text-sm font-medium text-slate-700" htmlFor="nickname">
|
|
137
|
+
昵称 <span className="text-rose-500">*</span>
|
|
138
|
+
</label>
|
|
139
|
+
<Input
|
|
140
|
+
id="nickname"
|
|
141
|
+
name="nickname"
|
|
142
|
+
placeholder="请输入昵称"
|
|
143
|
+
value={values.nickname}
|
|
144
|
+
onChange={handleChange}
|
|
145
|
+
/>
|
|
146
|
+
{errors.nickname && (
|
|
147
|
+
<p className="mt-1 text-xs text-rose-500">{errors.nickname}</p>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
<div>
|
|
151
|
+
<label className="mb-1 block text-sm font-medium text-slate-700" htmlFor="email">
|
|
152
|
+
邮箱 <span className="text-rose-500">*</span>
|
|
153
|
+
</label>
|
|
154
|
+
<Input
|
|
155
|
+
id="email"
|
|
156
|
+
name="email"
|
|
157
|
+
type="email"
|
|
158
|
+
placeholder="请输入邮箱"
|
|
159
|
+
value={values.email}
|
|
160
|
+
onChange={handleChange}
|
|
161
|
+
/>
|
|
162
|
+
{errors.email && <p className="mt-1 text-xs text-rose-500">{errors.email}</p>}
|
|
163
|
+
</div>
|
|
164
|
+
<div>
|
|
165
|
+
<label className="mb-1 block text-sm font-medium text-slate-700" htmlFor="role">
|
|
166
|
+
角色 <span className="text-rose-500">*</span>
|
|
167
|
+
</label>
|
|
168
|
+
<Select
|
|
169
|
+
name="role"
|
|
170
|
+
value={values.role}
|
|
171
|
+
onChange={(value) => {
|
|
172
|
+
handleChange({ target: { name: 'role', value } } as any)
|
|
173
|
+
}}
|
|
174
|
+
placeholder="请选择角色"
|
|
175
|
+
options={[
|
|
176
|
+
{ label: '超级管理员', value: '超级管理员' },
|
|
177
|
+
{ label: '编辑', value: '编辑' },
|
|
178
|
+
{ label: '只读', value: '只读' }
|
|
179
|
+
]}
|
|
180
|
+
/>
|
|
181
|
+
{errors.role && <p className="mt-1 text-xs text-rose-500">{errors.role}</p>}
|
|
182
|
+
</div>
|
|
183
|
+
</Dialog>
|
|
184
|
+
)}
|
|
19
185
|
<table className="data-table">
|
|
20
186
|
<thead>
|
|
21
187
|
<tr>
|
|
@@ -40,11 +206,11 @@ export default function SystemUser() {
|
|
|
40
206
|
</span>
|
|
41
207
|
</td>
|
|
42
208
|
<td>
|
|
43
|
-
<
|
|
209
|
+
<Button style={{ margin: '0 5px' }} type="button" onClick={() => navigate(`/user/detail/${user.id}`)}>
|
|
44
210
|
详情
|
|
45
|
-
</
|
|
46
|
-
<
|
|
47
|
-
<
|
|
211
|
+
</Button>
|
|
212
|
+
<Button style={{ margin: '0 5px' }} type="button">编辑</Button>
|
|
213
|
+
<Button style={{ margin: '0 5px' }} type="button">删除</Button>
|
|
48
214
|
</td>
|
|
49
215
|
</tr>
|
|
50
216
|
))}
|
package/templates/vue/.env
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
# 基础环境变量
|
|
2
|
-
VITE_APP_TITLE=My Vue App
|
|
1
|
+
# 基础环境变量
|
|
2
|
+
VITE_APP_TITLE=My Vue App
|
|
3
3
|
VITE_APP_BASE_API=/api
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
# 开发环境
|
|
2
|
-
VITE_APP_TITLE=My Vue App (Dev)
|
|
1
|
+
# 开发环境
|
|
2
|
+
VITE_APP_TITLE=My Vue App (Dev)
|
|
3
3
|
VITE_APP_BASE_API=/api
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
# 生产环境
|
|
2
|
-
VITE_APP_TITLE=My Vue App
|
|
1
|
+
# 生产环境
|
|
2
|
+
VITE_APP_TITLE=My Vue App
|
|
3
3
|
VITE_APP_BASE_API=https://api.example.com
|