@lvetechs/create-app 1.0.3 → 1.0.5

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.
Files changed (59) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +18 -4
  3. package/templates/react/.env +3 -0
  4. package/templates/react/.env.development +3 -0
  5. package/templates/react/.env.production +3 -0
  6. package/templates/react/index.html +1 -1
  7. package/templates/react/package.json +45 -45
  8. package/templates/react/pnpm-lock.yaml +166 -5
  9. package/templates/react/src/hooks/useForm.ts +77 -0
  10. package/templates/react/src/layouts/DefaultLayout.tsx +6 -1
  11. package/templates/react/src/layouts/menuConfig.ts +3 -33
  12. package/templates/react/src/main.tsx +3 -0
  13. package/templates/react/src/router/index.tsx +0 -39
  14. package/templates/react/src/stores/app.ts +141 -3
  15. package/templates/react/src/stores/notification.ts +128 -0
  16. package/templates/react/src/stores/permission.ts +183 -0
  17. package/templates/react/src/stores/user.ts +151 -4
  18. package/templates/react/src/views/home/index.tsx +205 -43
  19. package/templates/react/src/views/system/user/index.tsx +171 -5
  20. package/templates/react/tsconfig.json +1 -1
  21. package/templates/react/vite.config.ts +7 -3
  22. package/templates/vue/.env +2 -2
  23. package/templates/vue/.env.development +2 -2
  24. package/templates/vue/.env.production +2 -2
  25. package/templates/vue/index.html +1 -1
  26. package/templates/vue/package.json +0 -1
  27. package/templates/vue/pnpm-lock.yaml +3307 -3307
  28. package/templates/vue/src/auto-imports.d.ts +5 -0
  29. package/templates/vue/src/layouts/DefaultLayout.vue +2 -3
  30. package/templates/vue/src/layouts/menuConfig.ts +3 -33
  31. package/templates/vue/src/router/index.ts +4 -89
  32. package/templates/vue/src/stores/app.ts +133 -2
  33. package/templates/vue/src/stores/notification.ts +172 -0
  34. package/templates/vue/src/stores/permission.ts +184 -0
  35. package/templates/vue/src/stores/user.ts +109 -2
  36. package/templates/vue/src/views/home/index.vue +8 -8
  37. package/templates/react/_gitignore +0 -25
  38. package/templates/react/src/views/about/index.tsx +0 -40
  39. package/templates/react/src/views/login/index.tsx +0 -138
  40. package/templates/react/src/views/register/index.tsx +0 -143
  41. package/templates/react/src/views/result/fail.tsx +0 -39
  42. package/templates/react/src/views/result/success.tsx +0 -35
  43. package/templates/react/src/views/screen/index.tsx +0 -120
  44. package/templates/react/src/views/system/log/login.tsx +0 -51
  45. package/templates/react/src/views/system/log/operation.tsx +0 -47
  46. package/templates/react/src/views/system/menu/index.tsx +0 -62
  47. package/templates/react/src/views/system/role/index.tsx +0 -63
  48. package/templates/react/tsconfig.node.json +0 -11
  49. package/templates/vue/_gitignore +0 -25
  50. package/templates/vue/src/views/about/index.vue +0 -67
  51. package/templates/vue/src/views/login/index.vue +0 -153
  52. package/templates/vue/src/views/register/index.vue +0 -169
  53. package/templates/vue/src/views/result/fail.vue +0 -92
  54. package/templates/vue/src/views/result/success.vue +0 -92
  55. package/templates/vue/src/views/screen/index.vue +0 -150
  56. package/templates/vue/src/views/system/log/login.vue +0 -51
  57. package/templates/vue/src/views/system/log/operation.vue +0 -47
  58. package/templates/vue/src/views/system/menu/index.vue +0 -58
  59. 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) => set({ userInfo: 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
- logout: () => set({ token: '', userInfo: null, isLoggedIn: false })
174
+ return mockUserInfo
175
+ }
33
176
  }),
34
177
  {
35
178
  name: 'user-store',
36
- partialize: (state) => ({ token: state.token })
179
+ partialize: (state) => ({
180
+ token: state.token,
181
+ userInfo: state.userInfo,
182
+ loginHistory: state.loginHistory
183
+ })
37
184
  }
38
185
  )
39
186
  )
@@ -1,56 +1,218 @@
1
- import { useState } from 'react'
1
+ import { createContext, useContext, useReducer } from 'react'
2
2
  import '@/styles/page-common.scss'
3
+ import { Button, Card, Container, Input, Tabs } from '@lvetechs/ui-lib'
4
+ import { useForm } from '@/hooks/useForm'
3
5
 
4
- export default function Home() {
5
- const [count, setCount] = useState(0)
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
+ }
6
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
+ }
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 }}>👋 欢迎使用 {{PROJECT_NAME}}</h1>
156
+ <h1 style={{ fontSize: 28, marginBottom: 12 }}>欢迎使用 {import.meta.env.VITE_APP_TITLE}</h1>
157
+ <p style={{ color: 'var(--text-color-secondary)', fontSize: 15, marginBottom: 24 }}>
158
+ 该模板创建自 @lvetechs/create-app react模板
159
+ </p>
11
160
  <p style={{ color: 'var(--text-color-secondary)', fontSize: 15, marginBottom: 24 }}>
12
- 该模板集成了 React 18 + Vite + TypeScript + React Router + Zustand + Axios + TailwindCSS
161
+ 该模板集成了 React 18 + Vite + TypeScript + React Router + Zustand + Axios + TailwindCSS + @lvetechs/ui-lib
13
162
  等常用工具和最佳实践。
14
163
  </p>
15
-
16
- <div
17
- style={{
18
- display: 'grid',
19
- gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))',
20
- gap: 16,
21
- marginBottom: 24
22
- }}
23
- >
24
- {[
25
- { icon: '⚡', title: 'Vite', desc: '下一代前端构建工具,极速开发体验' },
26
- { icon: '🦾', title: 'TypeScript', desc: '强类型语言支持,提高代码质量' },
27
- { icon: '🐻', title: 'Zustand', desc: '轻量灵活的状态管理方案' },
28
- { icon: '🧭', title: 'React Router', desc: '声明式路由,支持懒加载' },
29
- { icon: '📡', title: 'Axios', desc: '请求封装,拦截器 + 类型安全' },
30
- { icon: '🎨', title: 'TailwindCSS', desc: '实用优先的 CSS 框架' }
31
- ].map((item) => (
32
- <div
33
- key={item.title}
34
- style={{
35
- padding: 16,
36
- border: '1px solid var(--border-color-light)',
37
- borderRadius: 'var(--border-radius)',
38
- transition: 'all 0.2s'
39
- }}
40
- >
41
- <h3 style={{ fontSize: 16, marginBottom: 6 }}>
42
- {item.icon} {item.title}
43
- </h3>
44
- <p style={{ fontSize: 13, color: 'var(--text-color-secondary)' }}>{item.desc}</p>
164
+ <Tabs items={[
165
+ {
166
+ label: '@lvetechs/ui-lib', key: '@lvetechs/ui-lib', children: <div className="feature-card">
167
+ @lvetechs/ui-lib - 现代化 React UI 组件库,支持可视化设计器 + 解析渲染器
168
+ </div>
169
+ },
170
+ {
171
+ label: 'Vite', key: 'Vite', children: <div className="feature-card">
172
+ 下一代前端构建工具,极速开发体验
173
+ </div>
174
+ },
175
+ {
176
+ label: 'TypeScript', key: 'TypeScript', children: <div className="feature-card">
177
+ 强类型语言支持,提高代码质量
45
178
  </div>
46
- ))}
47
- </div>
48
-
49
- <div style={{ textAlign: 'center' }}>
50
- <button className="btn-primary" onClick={() => setCount((c) => c + 1)}>
51
- 计数器: {count}
52
- </button>
53
- </div>
179
+ },
180
+ {
181
+ label: 'Zustand', key: 'Zustand', children: <Container direction="row" justify="start" align="center" gap="md" padding="md" >
182
+ 轻量灵活的状态管理方案
183
+ </Container>
184
+ },
185
+ {
186
+ label: 'React Router', key: 'React Router', children: <div className="feature-card">
187
+ 声明式路由,支持懒加载
188
+ </div>
189
+ },
190
+ {
191
+ label: 'Axios', key: 'Axios', children: <div className="feature-card">
192
+ 请求封装,拦截器
193
+ </div>
194
+ },
195
+ {
196
+ label: 'TailwindCSS', key: 'TailwindCSS', children: <div className="feature-card">
197
+ 实用优先的 CSS 框架
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
+ )
214
+ }
215
+ ]} defaultValue="@lvetechs/ui-lib" />
54
216
  </div>
55
217
  </div>
56
218
  )
@@ -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
- <button className="btn-primary">+ 新增用户</button>
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
- <button className="btn-link" onClick={() => navigate(`/user/detail/${user.id}`)}>
209
+ <Button style={{ margin: '0 5px' }} type="button" onClick={() => navigate(`/user/detail/${user.id}`)}>
44
210
  详情
45
- </button>
46
- <button className="btn-link">编辑</button>
47
- <button className="btn-link danger">删除</button>
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
  ))}
@@ -22,6 +22,6 @@
22
22
  "types": ["vite/client"]
23
23
  },
24
24
  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts"],
25
- "references": [{ "path": "./tsconfig.node.json" }]
25
+ "references": []
26
26
  }
27
27
 
@@ -1,6 +1,11 @@
1
+ /*
2
+ * @Author: linpeng
3
+ * @Date: 2026-02-11 10:54:48
4
+ * @Description:
5
+ */
1
6
  import { defineConfig } from 'vite'
2
7
  import react from '@vitejs/plugin-react'
3
- import path from 'node:path'
8
+ import path from 'path'
4
9
 
5
10
  // https://vitejs.dev/config/
6
11
  export default defineConfig({
@@ -16,8 +21,7 @@ export default defineConfig({
16
21
  proxy: {
17
22
  '/api': {
18
23
  target: 'http://localhost:8080',
19
- changeOrigin: true,
20
- rewrite: (p) => p.replace(/^\/api/, '')
24
+ changeOrigin: true
21
25
  }
22
26
  }
23
27
  },
@@ -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