@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.
Files changed (58) 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/App.tsx +7 -1
  8. package/templates/react/src/api/notification.ts +43 -0
  9. package/templates/react/src/components/NotificationButton/index.tsx +219 -0
  10. package/templates/react/src/components/Toast/index.tsx +150 -0
  11. package/templates/react/src/hooks/useForm.ts +77 -0
  12. package/templates/react/src/layouts/DefaultLayout.tsx +2 -0
  13. package/templates/react/src/layouts/menuConfig.ts +3 -33
  14. package/templates/react/src/router/index.tsx +0 -39
  15. package/templates/react/src/stores/app.ts +141 -3
  16. package/templates/react/src/stores/notification.ts +146 -0
  17. package/templates/react/src/stores/permission.ts +173 -0
  18. package/templates/react/src/stores/user.ts +151 -4
  19. package/templates/react/src/views/home/index.tsx +167 -6
  20. package/templates/react/src/views/system/user/index.tsx +171 -5
  21. package/templates/vue/.env +2 -2
  22. package/templates/vue/.env.development +2 -2
  23. package/templates/vue/.env.production +2 -2
  24. package/templates/vue/pnpm-lock.yaml +3307 -3307
  25. package/templates/vue/src/App.vue +2 -0
  26. package/templates/vue/src/api/notification.ts +43 -0
  27. package/templates/vue/src/auto-imports.d.ts +5 -0
  28. package/templates/vue/src/components/NotificationButton/index.vue +242 -0
  29. package/templates/vue/src/components/Toast/index.vue +126 -0
  30. package/templates/vue/src/components.d.ts +2 -0
  31. package/templates/vue/src/layouts/DefaultLayout.vue +2 -0
  32. package/templates/vue/src/layouts/menuConfig.ts +3 -33
  33. package/templates/vue/src/router/index.ts +3 -88
  34. package/templates/vue/src/stores/app.ts +133 -2
  35. package/templates/vue/src/stores/notification.ts +189 -0
  36. package/templates/vue/src/stores/permission.ts +184 -0
  37. package/templates/vue/src/stores/user.ts +109 -2
  38. package/templates/vue/src/views/home/index.vue +7 -7
  39. package/templates/react/src/views/about/index.tsx +0 -40
  40. package/templates/react/src/views/login/index.tsx +0 -138
  41. package/templates/react/src/views/register/index.tsx +0 -143
  42. package/templates/react/src/views/result/fail.tsx +0 -39
  43. package/templates/react/src/views/result/success.tsx +0 -35
  44. package/templates/react/src/views/screen/index.tsx +0 -120
  45. package/templates/react/src/views/system/log/login.tsx +0 -51
  46. package/templates/react/src/views/system/log/operation.tsx +0 -47
  47. package/templates/react/src/views/system/menu/index.tsx +0 -62
  48. package/templates/react/src/views/system/role/index.tsx +0 -63
  49. package/templates/vue/src/views/about/index.vue +0 -67
  50. package/templates/vue/src/views/login/index.vue +0 -153
  51. package/templates/vue/src/views/register/index.vue +0 -169
  52. package/templates/vue/src/views/result/fail.vue +0 -92
  53. package/templates/vue/src/views/result/success.vue +0 -92
  54. package/templates/vue/src/views/screen/index.vue +0 -150
  55. package/templates/vue/src/views/system/log/login.vue +0 -51
  56. package/templates/vue/src/views/system/log/operation.vue +0 -47
  57. package/templates/vue/src/views/system/menu/index.vue +0 -58
  58. package/templates/vue/src/views/system/role/index.vue +0 -59
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvetechs/create-app",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "快速创建 Vue 3 / React 18 项目的脚手架工具",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,3 +1,3 @@
1
- # 基础环境变量
2
- VITE_APP_TITLE=My React App
3
- VITE_APP_BASE_API=/api
1
+ # 基础环境变量
2
+ VITE_APP_TITLE=My React App
3
+ VITE_APP_BASE_API=/api
@@ -1,3 +1,3 @@
1
- # 开发环境
2
- VITE_APP_TITLE=My React App (Dev)
3
- VITE_APP_BASE_API=/api
1
+ # 开发环境
2
+ VITE_APP_TITLE=My React App (Dev)
3
+ VITE_APP_BASE_API=/api
@@ -1,3 +1,3 @@
1
- # 生产环境
2
- VITE_APP_TITLE=My React App
3
- VITE_APP_BASE_API=https://api.example.com
1
+ # 生产环境
2
+ VITE_APP_TITLE=My React App
3
+ VITE_APP_BASE_API=https://api.example.com
@@ -20,7 +20,7 @@
20
20
  "zustand": "^4.5.2"
21
21
  },
22
22
  "devDependencies": {
23
- "@lvetechs/ui-lib": "^1.1.6",
23
+ "@lvetechs/ui-lib": "^1.1.7",
24
24
  "@types/node": "^25.2.3",
25
25
  "@types/nprogress": "^0.2.3",
26
26
  "@types/react": "^18.3.1",
@@ -34,8 +34,8 @@ importers:
34
34
  version: 4.5.7(@types/react@18.3.28)(react@18.3.1)
35
35
  devDependencies:
36
36
  '@lvetechs/ui-lib':
37
- specifier: ^1.1.6
38
- version: 1.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vue@3.5.28(typescript@5.9.3))
37
+ specifier: ^1.1.7
38
+ version: 1.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vue@3.5.28(typescript@5.9.3))
39
39
  '@types/node':
40
40
  specifier: ^25.2.3
41
41
  version: 25.2.3
@@ -371,8 +371,8 @@ packages:
371
371
  '@jridgewell/trace-mapping@0.3.31':
372
372
  resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
373
373
 
374
- '@lvetechs/ui-lib@1.1.6':
375
- resolution: {integrity: sha512-bFRfKcmhVN3UuA3skrEiwZs+u6ij0PhvT/QfPU1edfSjP55TjXaH6USysHLoRZnMibvAcrWCveTzmEppLCh6ew==}
374
+ '@lvetechs/ui-lib@1.1.7':
375
+ resolution: {integrity: sha512-BH3QmLcLxK9A7C5mAs/pLovO9ZJ2MBlxjJhdMCElxMn935OQ0TXhrVmHI39asST5xAmTNeSyDMXLVuS3eNuu6w==}
376
376
  peerDependencies:
377
377
  react: ^18.0.0
378
378
  react-dom: ^18.0.0
@@ -2005,7 +2005,7 @@ snapshots:
2005
2005
  '@jridgewell/resolve-uri': 3.1.2
2006
2006
  '@jridgewell/sourcemap-codec': 1.5.5
2007
2007
 
2008
- '@lvetechs/ui-lib@1.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vue@3.5.28(typescript@5.9.3))':
2008
+ '@lvetechs/ui-lib@1.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vue@3.5.28(typescript@5.9.3))':
2009
2009
  dependencies:
2010
2010
  lucide-react: 0.563.0(react@18.3.1)
2011
2011
  react: 18.3.1
@@ -1,9 +1,15 @@
1
1
  import { useRoutes } from 'react-router-dom'
2
2
  import { routes } from './router'
3
+ import { ToastProvider } from '@/components/Toast'
3
4
 
4
5
  function App() {
5
6
  const element = useRoutes(routes)
6
- return <>{element}</>
7
+ return (
8
+ <>
9
+ {element}
10
+ <ToastProvider />
11
+ </>
12
+ )
7
13
  }
8
14
 
9
15
  export default App
@@ -0,0 +1,43 @@
1
+ import { http } from '@/utils/request'
2
+ import type { NotificationItem } from '@/stores/notification'
3
+
4
+ /** 获取通知列表参数 */
5
+ export interface GetNotificationsParams {
6
+ page?: number
7
+ pageSize?: number
8
+ type?: NotificationItem['type']
9
+ category?: string
10
+ unreadOnly?: boolean
11
+ }
12
+
13
+ /** 获取通知列表响应 */
14
+ export interface GetNotificationsResult {
15
+ list: NotificationItem[]
16
+ total: number
17
+ unreadCount: number
18
+ }
19
+
20
+ /** 获取通知列表 */
21
+ export function getNotificationsApi(params?: GetNotificationsParams) {
22
+ return http.get<GetNotificationsResult>('/notification/list', { params })
23
+ }
24
+
25
+ /** 标记通知为已读 */
26
+ export function markNotificationReadApi(id: string) {
27
+ return http.post(`/notification/${id}/read`)
28
+ }
29
+
30
+ /** 批量标记为已读 */
31
+ export function markAllReadApi() {
32
+ return http.post('/notification/read-all')
33
+ }
34
+
35
+ /** 删除通知 */
36
+ export function deleteNotificationApi(id: string) {
37
+ return http.delete(`/notification/${id}`)
38
+ }
39
+
40
+ /** 获取未读数量 */
41
+ export function getUnreadCountApi() {
42
+ return http.get<{ count: number }>('/notification/unread-count')
43
+ }
@@ -0,0 +1,219 @@
1
+ import { useState, useEffect, useRef } from 'react'
2
+ import { useNotificationStore } from '@/stores/notification'
3
+ import { getNotificationsApi, markNotificationReadApi, markAllReadApi } from '@/api/notification'
4
+ import { toast } from '@/components/Toast'
5
+ import type { NotificationItem } from '@/stores/notification'
6
+
7
+ export default function NotificationButton() {
8
+ const [showDropdown, setShowDropdown] = useState(false)
9
+ const dropdownRef = useRef<HTMLDivElement>(null)
10
+ const { notifications, addNotification, markAsRead, markAllAsRead, getUnreadCount } =
11
+ useNotificationStore()
12
+
13
+ const unreadCount = getUnreadCount()
14
+
15
+ // 点击外部关闭下拉菜单
16
+ useEffect(() => {
17
+ function handleClickOutside(event: MouseEvent) {
18
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
19
+ setShowDropdown(false)
20
+ }
21
+ }
22
+
23
+ if (showDropdown) {
24
+ document.addEventListener('mousedown', handleClickOutside)
25
+ return () => document.removeEventListener('mousedown', handleClickOutside)
26
+ }
27
+ }, [showDropdown])
28
+
29
+ // 获取最新消息
30
+ const fetchNotifications = async () => {
31
+ try {
32
+ const response = await getNotificationsApi({ unreadOnly: false, pageSize: 20 })
33
+ if (response.data) {
34
+ // 将新消息添加到 store(实际项目中可能需要去重逻辑)
35
+ response.data.list.forEach((item) => {
36
+ // 检查是否已存在
37
+ const exists = notifications.find((n) => n.id === item.id)
38
+ if (!exists) {
39
+ addNotification({
40
+ title: item.title,
41
+ content: item.content,
42
+ type: item.type,
43
+ category: item.category,
44
+ actionUrl: item.actionUrl
45
+ })
46
+ }
47
+ })
48
+ toast.success(`获取到 ${response.data.list.length} 条消息`)
49
+ }
50
+ } catch (error) {
51
+ console.error('获取消息失败:', error)
52
+ toast.error('获取消息失败')
53
+ }
54
+ }
55
+
56
+ // 标记为已读
57
+ const handleMarkAsRead = async (id: string) => {
58
+ try {
59
+ await markNotificationReadApi(id)
60
+ markAsRead(id)
61
+ toast.success('已标记为已读')
62
+ } catch (error) {
63
+ console.error('标记已读失败:', error)
64
+ toast.error('标记已读失败')
65
+ }
66
+ }
67
+
68
+ // 全部标记为已读
69
+ const handleMarkAllRead = async () => {
70
+ try {
71
+ await markAllReadApi()
72
+ markAllAsRead()
73
+ toast.success('已全部标记为已读')
74
+ } catch (error) {
75
+ console.error('全部标记已读失败:', error)
76
+ toast.error('全部标记已读失败')
77
+ }
78
+ }
79
+
80
+ // 格式化时间
81
+ const formatTime = (timestamp: number) => {
82
+ const now = Date.now()
83
+ const diff = now - timestamp
84
+ const minutes = Math.floor(diff / 60000)
85
+ const hours = Math.floor(diff / 3600000)
86
+ const days = Math.floor(diff / 86400000)
87
+
88
+ if (minutes < 1) return '刚刚'
89
+ if (minutes < 60) return `${minutes}分钟前`
90
+ if (hours < 24) return `${hours}小时前`
91
+ if (days < 7) return `${days}天前`
92
+ return new Date(timestamp).toLocaleDateString()
93
+ }
94
+
95
+ const typeIcons = {
96
+ info: 'ℹ️',
97
+ success: '✓',
98
+ warning: '⚠️',
99
+ error: '✕'
100
+ }
101
+
102
+ const typeColors = {
103
+ info: 'text-blue-500',
104
+ success: 'text-green-500',
105
+ warning: 'text-yellow-500',
106
+ error: 'text-red-500'
107
+ }
108
+
109
+ return (
110
+ <div className="relative" ref={dropdownRef}>
111
+ <button
112
+ className="relative flex items-center justify-center w-10 h-10 rounded-lg border border-slate-200 hover:border-indigo-500 hover:text-indigo-500 transition-colors"
113
+ onClick={() => {
114
+ setShowDropdown(!showDropdown)
115
+ // if (!showDropdown) {
116
+ // fetchNotifications()
117
+ // }
118
+ }}
119
+ title="消息通知"
120
+ >
121
+ <span className="text-lg">🔔</span>
122
+ {unreadCount > 0 && (
123
+ <span className="absolute -top-1 -right-1 flex items-center justify-center min-w-[18px] h-[18px] px-1 text-xs font-bold text-white bg-red-500 rounded-full">
124
+ {unreadCount > 99 ? '99+' : unreadCount}
125
+ </span>
126
+ )}
127
+ </button>
128
+
129
+ {showDropdown && (
130
+ <div className="absolute right-0 top-12 w-80 bg-white rounded-lg shadow-lg border border-slate-200 z-50 max-h-[500px] flex flex-col">
131
+ {/* 头部 */}
132
+ <div className="flex items-center justify-between p-4 border-b border-slate-200">
133
+ <h3 className="font-semibold text-slate-700">消息通知</h3>
134
+ <div className="flex gap-2">
135
+ {/* <button
136
+ className="text-xs text-indigo-600 hover:text-indigo-700"
137
+ onClick={fetchNotifications}
138
+ >
139
+ 刷新
140
+ </button> */}
141
+ {unreadCount > 0 && (
142
+ <button
143
+ className="text-xs text-indigo-600 hover:text-indigo-700"
144
+ onClick={handleMarkAllRead}
145
+ >
146
+ 全部已读
147
+ </button>
148
+ )}
149
+ </div>
150
+ </div>
151
+
152
+ {/* 消息列表 */}
153
+ <div className="flex-1 overflow-y-auto">
154
+ {notifications.length === 0 ? (
155
+ <div className="flex flex-col items-center justify-center py-8 text-slate-400">
156
+ <span className="text-4xl mb-2"></span>
157
+ <p className="text-sm">暂无消息</p>
158
+ </div>
159
+ ) : (
160
+ <div className="divide-y divide-slate-100">
161
+ {notifications.map((item: NotificationItem) => (
162
+ <div
163
+ key={item.id}
164
+ className={`p-3 hover:bg-slate-50 cursor-pointer transition-colors ${
165
+ !item.read ? 'bg-blue-50' : ''
166
+ }`}
167
+ onClick={() => !item.read && handleMarkAsRead(item.id)}
168
+ >
169
+ <div className="flex items-start gap-2">
170
+ <span className={`text-lg ${typeColors[item.type]}`}>
171
+ {typeIcons[item.type]}
172
+ </span>
173
+ <div className="flex-1 min-w-0">
174
+ <div className="flex items-center justify-between mb-1">
175
+ <h4 className="font-medium text-sm text-slate-700 truncate">
176
+ {item.title}
177
+ </h4>
178
+ {!item.read && (
179
+ <span className="flex-shrink-0 w-2 h-2 bg-blue-500 rounded-full ml-2"></span>
180
+ )}
181
+ </div>
182
+ <p className="text-xs text-slate-500 line-clamp-2 mb-1">
183
+ {item.content}
184
+ </p>
185
+ <div className="flex items-center justify-between">
186
+ <span className="text-xs text-slate-400">{formatTime(item.timestamp)}</span>
187
+ {item.category && (
188
+ <span className="text-xs text-slate-400 bg-slate-100 px-2 py-0.5 rounded">
189
+ {item.category}
190
+ </span>
191
+ )}
192
+ </div>
193
+ </div>
194
+ </div>
195
+ </div>
196
+ ))}
197
+ </div>
198
+ )}
199
+ </div>
200
+
201
+ {/* 底部 */}
202
+ {notifications.length > 0 && (
203
+ <div className="p-2 border-t border-slate-200 text-center">
204
+ <button
205
+ className="text-xs text-slate-500 hover:text-indigo-600"
206
+ onClick={() => {
207
+ // 可以跳转到消息中心页面
208
+ console.log('查看全部消息')
209
+ }}
210
+ >
211
+ 查看全部消息
212
+ </button>
213
+ </div>
214
+ )}
215
+ </div>
216
+ )}
217
+ </div>
218
+ )
219
+ }
@@ -0,0 +1,150 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+
4
+ export type ToastType = 'success' | 'error' | 'warning' | 'info'
5
+
6
+ export interface ToastItem {
7
+ id: string
8
+ message: string
9
+ type: ToastType
10
+ duration?: number
11
+ }
12
+
13
+ interface ToastProps {
14
+ toast: ToastItem
15
+ onClose: (id: string) => void
16
+ }
17
+
18
+ function ToastItem({ toast, onClose }: ToastProps) {
19
+ useEffect(() => {
20
+ if (toast.duration && toast.duration > 0) {
21
+ const timer = setTimeout(() => {
22
+ onClose(toast.id)
23
+ }, toast.duration)
24
+ return () => clearTimeout(timer)
25
+ }
26
+ }, [toast.id, toast.duration, onClose])
27
+
28
+ const icons = {
29
+ success: '✓',
30
+ error: '✕',
31
+ warning: '⚠',
32
+ info: 'ℹ'
33
+ }
34
+
35
+ const colors = {
36
+ success: 'bg-green-500',
37
+ error: 'bg-red-500',
38
+ warning: 'bg-yellow-500',
39
+ info: 'bg-blue-500'
40
+ }
41
+
42
+ return (
43
+ <div
44
+ className={`flex items-center gap-3 rounded-lg ${colors[toast.type]} px-4 py-3 text-white shadow-lg transition-all animate-in slide-in-from-top-5`}
45
+ onClick={() => onClose(toast.id)}
46
+ >
47
+ <span className="text-lg">{icons[toast.type]}</span>
48
+ <span className="flex-1 text-sm">{toast.message}</span>
49
+ <button
50
+ className="ml-2 text-white opacity-70 hover:opacity-100"
51
+ onClick={(e) => {
52
+ e.stopPropagation()
53
+ onClose(toast.id)
54
+ }}
55
+ >
56
+
57
+ </button>
58
+ </div>
59
+ )
60
+ }
61
+
62
+ interface ToastContainerProps {
63
+ toasts: ToastItem[]
64
+ onClose: (id: string) => void
65
+ }
66
+
67
+ function ToastContainer({ toasts, onClose }: ToastContainerProps) {
68
+ if (toasts.length === 0) return null
69
+
70
+ return createPortal(
71
+ <div className="fixed top-4 right-4 z-[9999] flex flex-col gap-2">
72
+ {toasts.map((toast) => (
73
+ <ToastItem key={toast.id} toast={toast} onClose={onClose} />
74
+ ))}
75
+ </div>,
76
+ document.body
77
+ )
78
+ }
79
+
80
+ // Toast 管理器
81
+ class ToastManager {
82
+ private toasts: ToastItem[] = []
83
+ private listeners: Array<(toasts: ToastItem[]) => void> = []
84
+
85
+ private notify() {
86
+ this.listeners.forEach((listener) => listener([...this.toasts]))
87
+ }
88
+
89
+ subscribe(listener: (toasts: ToastItem[]) => void) {
90
+ this.listeners.push(listener)
91
+ return () => {
92
+ this.listeners = this.listeners.filter((l) => l !== listener)
93
+ }
94
+ }
95
+
96
+ show(message: string, type: ToastType = 'info', duration = 3000) {
97
+ const id = `toast-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
98
+ const toast: ToastItem = { id, message, type, duration }
99
+ this.toasts.push(toast)
100
+ this.notify()
101
+ return id
102
+ }
103
+
104
+ close(id: string) {
105
+ this.toasts = this.toasts.filter((t) => t.id !== id)
106
+ this.notify()
107
+ }
108
+
109
+ success(message: string, duration?: number) {
110
+ return this.show(message, 'success', duration)
111
+ }
112
+
113
+ error(message: string, duration?: number) {
114
+ return this.show(message, 'error', duration)
115
+ }
116
+
117
+ warning(message: string, duration?: number) {
118
+ return this.show(message, 'warning', duration)
119
+ }
120
+
121
+ info(message: string, duration?: number) {
122
+ return this.show(message, 'info', duration)
123
+ }
124
+
125
+ getToasts() {
126
+ return [...this.toasts]
127
+ }
128
+ }
129
+
130
+ export const toastManager = new ToastManager()
131
+
132
+ // Toast 容器组件(需要在 App 中引入)
133
+ export function ToastProvider() {
134
+ const [toasts, setToasts] = useState<ToastItem[]>([])
135
+
136
+ useEffect(() => {
137
+ const unsubscribe = toastManager.subscribe(setToasts)
138
+ return unsubscribe
139
+ }, [])
140
+
141
+ return <ToastContainer toasts={toasts} onClose={(id) => toastManager.close(id)} />
142
+ }
143
+
144
+ // 便捷方法
145
+ export const toast = {
146
+ success: (message: string, duration?: number) => toastManager.success(message, duration),
147
+ error: (message: string, duration?: number) => toastManager.error(message, duration),
148
+ warning: (message: string, duration?: number) => toastManager.warning(message, duration),
149
+ info: (message: string, duration?: number) => toastManager.info(message, duration)
150
+ }
@@ -0,0 +1,77 @@
1
+ import { useState, useCallback, ChangeEvent } from 'react'
2
+
3
+ export type FormErrors<T> = Partial<Record<keyof T, string>>
4
+
5
+ export interface UseFormOptions<T> {
6
+ /** 初始表单值 */
7
+ initialValues: T
8
+ /** 校验函数(可选) */
9
+ validate?: (values: T) => FormErrors<T>
10
+ /** 提交成功回调(可选) */
11
+ onSubmit?: (values: T) => void | Promise<void>
12
+ }
13
+
14
+ /**
15
+ * 通用表单 Hook 示例
16
+ *
17
+ * - 管理表单值 values
18
+ * - 简单错误提示 errors
19
+ * - 提供 handleChange / handleSubmit / reset
20
+ */
21
+ export function useForm<T extends Record<string, any>>({
22
+ initialValues,
23
+ validate,
24
+ onSubmit
25
+ }: UseFormOptions<T>) {
26
+ const [values, setValues] = useState<T>(initialValues)
27
+ const [errors, setErrors] = useState<FormErrors<T>>({})
28
+ const [submitting, setSubmitting] = useState(false)
29
+
30
+ const handleChange = useCallback(
31
+ (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
32
+ const { name, value } = e.target
33
+ setValues((prev) => ({
34
+ ...prev,
35
+ [name]: value
36
+ }))
37
+ },
38
+ []
39
+ )
40
+
41
+ const reset = useCallback(() => {
42
+ setValues(initialValues)
43
+ setErrors({})
44
+ }, [initialValues])
45
+
46
+ const handleSubmit = useCallback(
47
+ async () => {
48
+ if (validate) {
49
+ const nextErrors = validate(values)
50
+ setErrors(nextErrors)
51
+ const hasError = Object.values(nextErrors).some(Boolean)
52
+ if (hasError) return
53
+ }
54
+
55
+ if (!onSubmit) return
56
+ try {
57
+ setSubmitting(true)
58
+ await onSubmit(values)
59
+ } finally {
60
+ setSubmitting(false)
61
+ }
62
+ },
63
+ [onSubmit, validate, values]
64
+ )
65
+
66
+ return {
67
+ values,
68
+ errors,
69
+ submitting,
70
+ handleChange,
71
+ handleSubmit,
72
+ reset,
73
+ setValues,
74
+ setErrors
75
+ }
76
+ }
77
+
@@ -7,6 +7,7 @@ import { Outlet } from 'react-router-dom'
7
7
  import { useAppStore } from '@/stores/app'
8
8
  import SidebarMenu from './components/SidebarMenu'
9
9
  import { menuRoutes } from './menuConfig'
10
+ import NotificationButton from '@/components/NotificationButton'
10
11
 
11
12
  export default function DefaultLayout() {
12
13
  const { sidebarCollapsed, toggleSidebar, theme, setTheme } = useAppStore()
@@ -24,6 +25,7 @@ export default function DefaultLayout() {
24
25
  </button>
25
26
  </div>
26
27
  <div className="header-right">
28
+ <NotificationButton />
27
29
  <button
28
30
  className="theme-btn"
29
31
  onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
@@ -24,49 +24,19 @@ export const menuRoutes: MenuItem[] = [
24
24
  {
25
25
  path: '/home',
26
26
  title: '首页',
27
- icon: '🏠'
27
+ icon: ''
28
28
  },
29
29
  {
30
30
  path: '/system',
31
31
  title: '系统管理',
32
- icon: '⚙️',
32
+ icon: '',
33
33
  children: [
34
34
  {
35
35
  path: '/system/user',
36
36
  title: '用户管理',
37
- icon: '👤'
37
+ icon: ''
38
38
  },
39
- {
40
- path: '/system/role',
41
- title: '角色管理',
42
- icon: '🔑'
43
- },
44
- {
45
- path: '/system/menu',
46
- title: '菜单管理',
47
- icon: '📋'
48
- },
49
- {
50
- path: '/system/log',
51
- title: '日志管理',
52
- icon: '📝',
53
- children: [
54
- {
55
- path: '/system/log/operation',
56
- title: '操作日志'
57
- },
58
- {
59
- path: '/system/log/login',
60
- title: '登录日志'
61
- }
62
- ]
63
- }
64
39
  ]
65
- },
66
- {
67
- path: '/about',
68
- title: '关于',
69
- icon: 'ℹ️'
70
40
  }
71
41
  ]
72
42