@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.
Files changed (47) hide show
  1. package/package.json +1 -1
  2. package/templates/react/.env +3 -3
  3. package/templates/react/.env.development +3 -3
  4. package/templates/react/.env.production +3 -3
  5. package/templates/react/package.json +1 -1
  6. package/templates/react/pnpm-lock.yaml +5 -5
  7. package/templates/react/src/hooks/useForm.ts +77 -0
  8. package/templates/react/src/layouts/menuConfig.ts +3 -33
  9. package/templates/react/src/router/index.tsx +0 -39
  10. package/templates/react/src/stores/app.ts +141 -3
  11. package/templates/react/src/stores/notification.ts +128 -0
  12. package/templates/react/src/stores/permission.ts +183 -0
  13. package/templates/react/src/stores/user.ts +151 -4
  14. package/templates/react/src/views/home/index.tsx +167 -6
  15. package/templates/react/src/views/system/user/index.tsx +171 -5
  16. package/templates/vue/.env +2 -2
  17. package/templates/vue/.env.development +2 -2
  18. package/templates/vue/.env.production +2 -2
  19. package/templates/vue/pnpm-lock.yaml +3307 -3307
  20. package/templates/vue/src/auto-imports.d.ts +5 -0
  21. package/templates/vue/src/layouts/menuConfig.ts +3 -33
  22. package/templates/vue/src/router/index.ts +3 -88
  23. package/templates/vue/src/stores/app.ts +133 -2
  24. package/templates/vue/src/stores/notification.ts +172 -0
  25. package/templates/vue/src/stores/permission.ts +184 -0
  26. package/templates/vue/src/stores/user.ts +109 -2
  27. package/templates/vue/src/views/home/index.vue +7 -7
  28. package/templates/react/src/views/about/index.tsx +0 -40
  29. package/templates/react/src/views/login/index.tsx +0 -138
  30. package/templates/react/src/views/register/index.tsx +0 -143
  31. package/templates/react/src/views/result/fail.tsx +0 -39
  32. package/templates/react/src/views/result/success.tsx +0 -35
  33. package/templates/react/src/views/screen/index.tsx +0 -120
  34. package/templates/react/src/views/system/log/login.tsx +0 -51
  35. package/templates/react/src/views/system/log/operation.tsx +0 -47
  36. package/templates/react/src/views/system/menu/index.tsx +0 -62
  37. package/templates/react/src/views/system/role/index.tsx +0 -63
  38. package/templates/vue/src/views/about/index.vue +0 -67
  39. package/templates/vue/src/views/login/index.vue +0 -153
  40. package/templates/vue/src/views/register/index.vue +0 -169
  41. package/templates/vue/src/views/result/fail.vue +0 -92
  42. package/templates/vue/src/views/result/success.vue +0 -92
  43. package/templates/vue/src/views/screen/index.vue +0 -150
  44. package/templates/vue/src/views/system/log/login.vue +0 -51
  45. package/templates/vue/src/views/system/log/operation.vue +0 -47
  46. package/templates/vue/src/views/system/menu/index.vue +0 -58
  47. 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) => 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,13 +1,159 @@
1
- import { useState } from 'react'
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
- export default function Home() {
5
- const [count, setCount] = useState(0)
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 }}>👋 欢迎使用 {import.meta.env.VITE_APP_TITLE}</h1>
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>