@lvetechs/create-app 1.0.4 → 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.
- 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/hooks/useForm.ts +77 -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 +128 -0
- package/templates/react/src/stores/permission.ts +183 -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/auto-imports.d.ts +5 -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 +172 -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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
import { persist } from 'zustand/middleware'
|
|
3
|
+
import { useUserStore } from './user'
|
|
4
|
+
|
|
5
|
+
export type Permission = string
|
|
6
|
+
export type Role = string
|
|
7
|
+
|
|
8
|
+
export interface PermissionConfig {
|
|
9
|
+
permission: Permission
|
|
10
|
+
name: string
|
|
11
|
+
description?: string
|
|
12
|
+
module?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RoleConfig {
|
|
16
|
+
role: Role
|
|
17
|
+
name: string
|
|
18
|
+
description?: string
|
|
19
|
+
permissions: Permission[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface PermissionState {
|
|
23
|
+
allPermissions: PermissionConfig[]
|
|
24
|
+
allRoles: RoleConfig[]
|
|
25
|
+
|
|
26
|
+
// 计算相关
|
|
27
|
+
getUserPermissions: () => Permission[]
|
|
28
|
+
getPermissionsByModule: () => Record<string, PermissionConfig[]>
|
|
29
|
+
|
|
30
|
+
// 检查权限 / 角色
|
|
31
|
+
hasPermission: (permission: Permission) => boolean
|
|
32
|
+
hasAnyPermission: (permissions: Permission[]) => boolean
|
|
33
|
+
hasAllPermissions: (permissions: Permission[]) => boolean
|
|
34
|
+
hasRole: (role: Role) => boolean
|
|
35
|
+
hasAnyRole: (roles: Role[]) => boolean
|
|
36
|
+
|
|
37
|
+
// 查询配置
|
|
38
|
+
getRoleConfig: (role: Role) => RoleConfig | undefined
|
|
39
|
+
getPermissionConfig: (permission: Permission) => PermissionConfig | undefined
|
|
40
|
+
|
|
41
|
+
// 修改配置
|
|
42
|
+
addPermission: (permission: PermissionConfig) => void
|
|
43
|
+
addRole: (role: RoleConfig) => void
|
|
44
|
+
updateRolePermissions: (role: Role, permissions: Permission[]) => void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const usePermissionStore = create<PermissionState>()(
|
|
48
|
+
persist(
|
|
49
|
+
(set, get) => ({
|
|
50
|
+
allPermissions: [
|
|
51
|
+
{ permission: 'user:view', name: '查看用户', module: '用户管理' },
|
|
52
|
+
{ permission: 'user:create', name: '创建用户', module: '用户管理' },
|
|
53
|
+
{ permission: 'user:edit', name: '编辑用户', module: '用户管理' },
|
|
54
|
+
{ permission: 'user:delete', name: '删除用户', module: '用户管理' },
|
|
55
|
+
{ permission: 'role:view', name: '查看角色', module: '角色管理' },
|
|
56
|
+
{ permission: 'role:create', name: '创建角色', module: '角色管理' },
|
|
57
|
+
{ permission: 'role:edit', name: '编辑角色', module: '角色管理' },
|
|
58
|
+
{ permission: 'role:delete', name: '删除角色', module: '角色管理' },
|
|
59
|
+
{ permission: 'system:config', name: '系统配置', module: '系统管理' },
|
|
60
|
+
{ permission: 'system:log', name: '查看日志', module: '系统管理' }
|
|
61
|
+
],
|
|
62
|
+
|
|
63
|
+
allRoles: [
|
|
64
|
+
{
|
|
65
|
+
role: 'admin',
|
|
66
|
+
name: '超级管理员',
|
|
67
|
+
description: '拥有所有权限',
|
|
68
|
+
permissions: [
|
|
69
|
+
'user:view',
|
|
70
|
+
'user:create',
|
|
71
|
+
'user:edit',
|
|
72
|
+
'user:delete',
|
|
73
|
+
'role:view',
|
|
74
|
+
'role:create',
|
|
75
|
+
'role:edit',
|
|
76
|
+
'role:delete',
|
|
77
|
+
'system:config',
|
|
78
|
+
'system:log'
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
role: 'editor',
|
|
83
|
+
name: '编辑',
|
|
84
|
+
description: '可以查看和编辑内容',
|
|
85
|
+
permissions: ['user:view', 'user:create', 'user:edit', 'role:view']
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
role: 'viewer',
|
|
89
|
+
name: '只读',
|
|
90
|
+
description: '只能查看内容',
|
|
91
|
+
permissions: ['user:view', 'role:view']
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
|
|
95
|
+
getUserPermissions: () => {
|
|
96
|
+
const user = useUserStore.getState().userInfo
|
|
97
|
+
if (!user) return []
|
|
98
|
+
|
|
99
|
+
const roles = user.roles || []
|
|
100
|
+
const allRoles = get().allRoles
|
|
101
|
+
|
|
102
|
+
const permissions: Permission[] = []
|
|
103
|
+
roles.forEach((role) => {
|
|
104
|
+
const roleConfig = allRoles.find((r) => r.role === role)
|
|
105
|
+
if (roleConfig) {
|
|
106
|
+
permissions.push(...roleConfig.permissions)
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
return Array.from(new Set(permissions))
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
getPermissionsByModule: () => {
|
|
114
|
+
const grouped: Record<string, PermissionConfig[]> = {}
|
|
115
|
+
get().allPermissions.forEach((permission) => {
|
|
116
|
+
const module = permission.module || '其他'
|
|
117
|
+
if (!grouped[module]) grouped[module] = []
|
|
118
|
+
grouped[module].push(permission)
|
|
119
|
+
})
|
|
120
|
+
return grouped
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
hasPermission: (permission) => get().getUserPermissions().includes(permission),
|
|
124
|
+
|
|
125
|
+
hasAnyPermission: (permissions) => permissions.some((p) => get().hasPermission(p)),
|
|
126
|
+
|
|
127
|
+
hasAllPermissions: (permissions) => permissions.every((p) => get().hasPermission(p)),
|
|
128
|
+
|
|
129
|
+
hasRole: (role) => {
|
|
130
|
+
const user = useUserStore.getState().userInfo
|
|
131
|
+
return user?.roles?.includes(role) ?? false
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
hasAnyRole: (roles) => roles.some((r) => get().hasRole(r)),
|
|
135
|
+
|
|
136
|
+
getRoleConfig: (role) => get().allRoles.find((r) => r.role === role),
|
|
137
|
+
|
|
138
|
+
getPermissionConfig: (permission) =>
|
|
139
|
+
get().allPermissions.find((p) => p.permission === permission),
|
|
140
|
+
|
|
141
|
+
getPermissionsByModule: () => {
|
|
142
|
+
const grouped: Record<string, PermissionConfig[]> = {}
|
|
143
|
+
get().allPermissions.forEach((permission) => {
|
|
144
|
+
const module = permission.module || '其他'
|
|
145
|
+
if (!grouped[module]) grouped[module] = []
|
|
146
|
+
grouped[module].push(permission)
|
|
147
|
+
})
|
|
148
|
+
return grouped
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
addPermission: (permission) =>
|
|
152
|
+
set((state) => {
|
|
153
|
+
if (state.allPermissions.some((p) => p.permission === permission.permission)) {
|
|
154
|
+
return state
|
|
155
|
+
}
|
|
156
|
+
return { allPermissions: [...state.allPermissions, permission] }
|
|
157
|
+
}),
|
|
158
|
+
|
|
159
|
+
addRole: (role) =>
|
|
160
|
+
set((state) => {
|
|
161
|
+
if (state.allRoles.some((r) => r.role === role.role)) {
|
|
162
|
+
return state
|
|
163
|
+
}
|
|
164
|
+
return { allRoles: [...state.allRoles, role] }
|
|
165
|
+
}),
|
|
166
|
+
|
|
167
|
+
updateRolePermissions: (role, permissions) =>
|
|
168
|
+
set((state) => ({
|
|
169
|
+
allRoles: state.allRoles.map((r) =>
|
|
170
|
+
r.role === role ? { ...r, permissions } : r
|
|
171
|
+
)
|
|
172
|
+
}))
|
|
173
|
+
}),
|
|
174
|
+
{
|
|
175
|
+
name: 'permission-store',
|
|
176
|
+
partialize: (state) => ({
|
|
177
|
+
allPermissions: state.allPermissions,
|
|
178
|
+
allRoles: state.allRoles
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
|
|
@@ -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>
|