@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
|
@@ -144,6 +144,7 @@ declare global {
|
|
|
144
144
|
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
|
|
145
145
|
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
|
|
146
146
|
const useCached: typeof import('@vueuse/core')['useCached']
|
|
147
|
+
const useCartStore: typeof import('./stores/cart')['useCartStore']
|
|
147
148
|
const useClipboard: typeof import('@vueuse/core')['useClipboard']
|
|
148
149
|
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
|
|
149
150
|
const useCloned: typeof import('@vueuse/core')['useCloned']
|
|
@@ -213,6 +214,7 @@ declare global {
|
|
|
213
214
|
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
|
|
214
215
|
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
|
|
215
216
|
const useNetwork: typeof import('@vueuse/core')['useNetwork']
|
|
217
|
+
const useNotificationStore: typeof import('./stores/notification')['useNotificationStore']
|
|
216
218
|
const useNow: typeof import('@vueuse/core')['useNow']
|
|
217
219
|
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
|
|
218
220
|
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
|
|
@@ -223,6 +225,7 @@ declare global {
|
|
|
223
225
|
const useParentElement: typeof import('@vueuse/core')['useParentElement']
|
|
224
226
|
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
|
|
225
227
|
const usePermission: typeof import('@vueuse/core')['usePermission']
|
|
228
|
+
const usePermissionStore: typeof import('./stores/permission')['usePermissionStore']
|
|
226
229
|
const usePointer: typeof import('@vueuse/core')['usePointer']
|
|
227
230
|
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
|
|
228
231
|
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
|
@@ -523,6 +526,7 @@ declare module 'vue' {
|
|
|
523
526
|
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
|
|
524
527
|
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
|
|
525
528
|
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
|
|
529
|
+
readonly useNotificationStore: UnwrapRef<typeof import('./stores/notification')['useNotificationStore']>
|
|
526
530
|
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
|
|
527
531
|
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
|
|
528
532
|
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
|
|
@@ -533,6 +537,7 @@ declare module 'vue' {
|
|
|
533
537
|
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
|
|
534
538
|
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
|
|
535
539
|
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
|
|
540
|
+
readonly usePermissionStore: UnwrapRef<typeof import('./stores/permission')['usePermissionStore']>
|
|
536
541
|
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
|
|
537
542
|
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
|
|
538
543
|
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
|
|
@@ -25,49 +25,19 @@ export const menuRoutes: MenuItem[] = [
|
|
|
25
25
|
{
|
|
26
26
|
path: '/home',
|
|
27
27
|
title: '首页',
|
|
28
|
-
icon: '
|
|
28
|
+
icon: ''
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
path: '/system',
|
|
32
32
|
title: '系统管理',
|
|
33
|
-
icon: '
|
|
33
|
+
icon: '',
|
|
34
34
|
children: [
|
|
35
35
|
{
|
|
36
36
|
path: '/system/user',
|
|
37
37
|
title: '用户管理',
|
|
38
|
-
icon: '
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
path: '/system/role',
|
|
42
|
-
title: '角色管理',
|
|
43
|
-
icon: '🔑'
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
path: '/system/menu',
|
|
47
|
-
title: '菜单管理',
|
|
48
|
-
icon: '📋'
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
path: '/system/log',
|
|
52
|
-
title: '日志管理',
|
|
53
|
-
icon: '📝',
|
|
54
|
-
children: [
|
|
55
|
-
{
|
|
56
|
-
path: '/system/log/operation',
|
|
57
|
-
title: '操作日志'
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
path: '/system/log/login',
|
|
61
|
-
title: '登录日志'
|
|
62
|
-
}
|
|
63
|
-
]
|
|
38
|
+
icon: ''
|
|
64
39
|
}
|
|
65
40
|
]
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
path: '/about',
|
|
69
|
-
title: '关于',
|
|
70
|
-
icon: 'ℹ️'
|
|
71
41
|
}
|
|
72
42
|
]
|
|
73
43
|
|
|
@@ -32,7 +32,7 @@ const layoutRoutes: RouteRecordRaw = {
|
|
|
32
32
|
path: 'home',
|
|
33
33
|
name: 'Home',
|
|
34
34
|
component: () => import('@/views/home/index.vue'),
|
|
35
|
-
meta: { title: '首页', icon: '
|
|
35
|
+
meta: { title: '首页', icon: '', requireAuth: true }
|
|
36
36
|
},
|
|
37
37
|
|
|
38
38
|
// ---------- 系统管理 (多级路由示例) ----------
|
|
@@ -40,58 +40,17 @@ const layoutRoutes: RouteRecordRaw = {
|
|
|
40
40
|
path: 'system',
|
|
41
41
|
name: 'System',
|
|
42
42
|
redirect: '/system/user',
|
|
43
|
-
meta: { title: '系统管理', icon: '
|
|
43
|
+
meta: { title: '系统管理', icon: '' },
|
|
44
44
|
children: [
|
|
45
45
|
{
|
|
46
46
|
path: 'user',
|
|
47
47
|
name: 'SystemUser',
|
|
48
48
|
component: () => import('@/views/system/user/index.vue'),
|
|
49
|
-
meta: { title: '用户管理', icon: '
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
path: 'role',
|
|
53
|
-
name: 'SystemRole',
|
|
54
|
-
component: () => import('@/views/system/role/index.vue'),
|
|
55
|
-
meta: { title: '角色管理', icon: '🔑', requireAuth: true }
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
path: 'menu',
|
|
59
|
-
name: 'SystemMenu',
|
|
60
|
-
component: () => import('@/views/system/menu/index.vue'),
|
|
61
|
-
meta: { title: '菜单管理', icon: '📋', requireAuth: true }
|
|
62
|
-
},
|
|
63
|
-
// 三级路由示例: 系统管理 > 日志管理 > 操作日志 / 登录日志
|
|
64
|
-
{
|
|
65
|
-
path: 'log',
|
|
66
|
-
name: 'SystemLog',
|
|
67
|
-
redirect: '/system/log/operation',
|
|
68
|
-
meta: { title: '日志管理', icon: '📝' },
|
|
69
|
-
children: [
|
|
70
|
-
{
|
|
71
|
-
path: 'operation',
|
|
72
|
-
name: 'OperationLog',
|
|
73
|
-
component: () => import('@/views/system/log/operation.vue'),
|
|
74
|
-
meta: { title: '操作日志', requireAuth: true }
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
path: 'login',
|
|
78
|
-
name: 'LoginLog',
|
|
79
|
-
component: () => import('@/views/system/log/login.vue'),
|
|
80
|
-
meta: { title: '登录日志', requireAuth: true }
|
|
81
|
-
}
|
|
82
|
-
]
|
|
49
|
+
meta: { title: '用户管理', icon: '', requireAuth: true }
|
|
83
50
|
}
|
|
84
51
|
]
|
|
85
52
|
},
|
|
86
53
|
|
|
87
|
-
// ---------- 关于页 ----------
|
|
88
|
-
{
|
|
89
|
-
path: 'about',
|
|
90
|
-
name: 'About',
|
|
91
|
-
component: () => import('@/views/about/index.vue'),
|
|
92
|
-
meta: { title: '关于', icon: 'ℹ️' }
|
|
93
|
-
},
|
|
94
|
-
|
|
95
54
|
// ---------- 详情页 (Layout 内但菜单隐藏) ----------
|
|
96
55
|
{
|
|
97
56
|
path: 'user/detail/:id',
|
|
@@ -102,49 +61,6 @@ const layoutRoutes: RouteRecordRaw = {
|
|
|
102
61
|
]
|
|
103
62
|
}
|
|
104
63
|
|
|
105
|
-
// ==================== 独立页面 (无 Layout) ====================
|
|
106
|
-
const standaloneRoutes: RouteRecordRaw[] = [
|
|
107
|
-
{
|
|
108
|
-
path: '/login',
|
|
109
|
-
name: 'Login',
|
|
110
|
-
component: () => import('@/views/login/index.vue'),
|
|
111
|
-
meta: { title: '登录' }
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
path: '/register',
|
|
115
|
-
name: 'Register',
|
|
116
|
-
component: () => import('@/views/register/index.vue'),
|
|
117
|
-
meta: { title: '注册' }
|
|
118
|
-
},
|
|
119
|
-
// 独立结果页 (不带侧边栏,全屏展示)
|
|
120
|
-
{
|
|
121
|
-
path: '/result',
|
|
122
|
-
name: 'Result',
|
|
123
|
-
redirect: '/result/success',
|
|
124
|
-
children: [
|
|
125
|
-
{
|
|
126
|
-
path: 'success',
|
|
127
|
-
name: 'ResultSuccess',
|
|
128
|
-
component: () => import('@/views/result/success.vue'),
|
|
129
|
-
meta: { title: '操作成功' }
|
|
130
|
-
},
|
|
131
|
-
{
|
|
132
|
-
path: 'fail',
|
|
133
|
-
name: 'ResultFail',
|
|
134
|
-
component: () => import('@/views/result/fail.vue'),
|
|
135
|
-
meta: { title: '操作失败' }
|
|
136
|
-
}
|
|
137
|
-
]
|
|
138
|
-
},
|
|
139
|
-
// 独立大屏页面示例
|
|
140
|
-
{
|
|
141
|
-
path: '/screen',
|
|
142
|
-
name: 'DataScreen',
|
|
143
|
-
component: () => import('@/views/screen/index.vue'),
|
|
144
|
-
meta: { title: '数据大屏' }
|
|
145
|
-
}
|
|
146
|
-
]
|
|
147
|
-
|
|
148
64
|
// ==================== 错误页 ====================
|
|
149
65
|
const errorRoutes: RouteRecordRaw[] = [
|
|
150
66
|
{
|
|
@@ -164,7 +80,6 @@ const errorRoutes: RouteRecordRaw[] = [
|
|
|
164
80
|
// ==================== 合并所有路由 ====================
|
|
165
81
|
const routes: RouteRecordRaw[] = [
|
|
166
82
|
layoutRoutes,
|
|
167
|
-
...standaloneRoutes,
|
|
168
83
|
...errorRoutes
|
|
169
84
|
]
|
|
170
85
|
|
|
@@ -1,33 +1,164 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
|
-
import { ref } from 'vue'
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface AppError {
|
|
5
|
+
id: string
|
|
6
|
+
message: string
|
|
7
|
+
code?: string
|
|
8
|
+
timestamp: number
|
|
9
|
+
type?: 'error' | 'warning' | 'info'
|
|
10
|
+
}
|
|
3
11
|
|
|
4
12
|
export const useAppStore = defineStore(
|
|
5
13
|
'app',
|
|
6
14
|
() => {
|
|
15
|
+
// 基础配置
|
|
7
16
|
const sidebarCollapsed = ref(false)
|
|
8
17
|
const theme = ref<'light' | 'dark'>('light')
|
|
9
18
|
const language = ref('zh-CN')
|
|
10
19
|
|
|
20
|
+
// 加载状态
|
|
21
|
+
const loading = ref(false)
|
|
22
|
+
const loadingText = ref('加载中...')
|
|
23
|
+
const loadingStack = ref<string[]>([]) // 支持多个并发加载任务
|
|
24
|
+
|
|
25
|
+
// 错误处理
|
|
26
|
+
const errors = ref<AppError[]>([])
|
|
27
|
+
const maxErrorCount = 10 // 最多保留的错误数量
|
|
28
|
+
|
|
29
|
+
// 计算属性:是否有加载任务
|
|
30
|
+
const isLoading = computed(() => loadingStack.value.length > 0)
|
|
31
|
+
|
|
32
|
+
// 计算属性:未处理的错误数量
|
|
33
|
+
const unhandledErrorCount = computed(() => {
|
|
34
|
+
return errors.value.filter((e) => e.type === 'error').length
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// 侧边栏控制
|
|
11
38
|
function toggleSidebar() {
|
|
12
39
|
sidebarCollapsed.value = !sidebarCollapsed.value
|
|
13
40
|
}
|
|
14
41
|
|
|
42
|
+
function setSidebarCollapsed(collapsed: boolean) {
|
|
43
|
+
sidebarCollapsed.value = collapsed
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 主题控制
|
|
15
47
|
function setTheme(val: 'light' | 'dark') {
|
|
16
48
|
theme.value = val
|
|
17
49
|
document.documentElement.setAttribute('data-theme', val)
|
|
18
50
|
}
|
|
19
51
|
|
|
52
|
+
function toggleTheme() {
|
|
53
|
+
setTheme(theme.value === 'light' ? 'dark' : 'light')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 语言控制
|
|
20
57
|
function setLanguage(val: string) {
|
|
21
58
|
language.value = val
|
|
22
59
|
}
|
|
23
60
|
|
|
61
|
+
// 加载状态管理
|
|
62
|
+
function startLoading(taskId?: string, text?: string) {
|
|
63
|
+
const id = taskId || `loading-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
64
|
+
loadingStack.value.push(id)
|
|
65
|
+
if (text) {
|
|
66
|
+
loadingText.value = text
|
|
67
|
+
}
|
|
68
|
+
loading.value = true
|
|
69
|
+
return id
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function stopLoading(taskId?: string) {
|
|
73
|
+
if (taskId) {
|
|
74
|
+
const index = loadingStack.value.indexOf(taskId)
|
|
75
|
+
if (index > -1) {
|
|
76
|
+
loadingStack.value.splice(index, 1)
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
loadingStack.value.pop()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (loadingStack.value.length === 0) {
|
|
83
|
+
loading.value = false
|
|
84
|
+
loadingText.value = '加载中...'
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 错误处理
|
|
89
|
+
function addError(
|
|
90
|
+
message: string,
|
|
91
|
+
code?: string,
|
|
92
|
+
type: AppError['type'] = 'error'
|
|
93
|
+
) {
|
|
94
|
+
const error: AppError = {
|
|
95
|
+
id: `error-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
96
|
+
message,
|
|
97
|
+
code,
|
|
98
|
+
timestamp: Date.now(),
|
|
99
|
+
type
|
|
100
|
+
}
|
|
101
|
+
errors.value.unshift(error)
|
|
102
|
+
|
|
103
|
+
// 限制错误数量
|
|
104
|
+
if (errors.value.length > maxErrorCount) {
|
|
105
|
+
errors.value = errors.value.slice(0, maxErrorCount)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return error.id
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function removeError(id: string) {
|
|
112
|
+
const index = errors.value.findIndex((e) => e.id === id)
|
|
113
|
+
if (index > -1) {
|
|
114
|
+
errors.value.splice(index, 1)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function clearErrors() {
|
|
119
|
+
errors.value = []
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function clearErrorsByType(type: AppError['type']) {
|
|
123
|
+
errors.value = errors.value.filter((e) => e.type !== type)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 获取指定类型的错误
|
|
127
|
+
function getErrorsByType(type: AppError['type']): AppError[] {
|
|
128
|
+
return errors.value.filter((e) => e.type === type)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 全局提示(简化版,实际项目中可以使用更完善的 toast 组件)
|
|
132
|
+
function showMessage(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info') {
|
|
133
|
+
addError(message, undefined, type)
|
|
134
|
+
}
|
|
135
|
+
|
|
24
136
|
return {
|
|
137
|
+
// 状态
|
|
25
138
|
sidebarCollapsed,
|
|
26
139
|
theme,
|
|
27
140
|
language,
|
|
141
|
+
loading,
|
|
142
|
+
loadingText,
|
|
143
|
+
loadingStack,
|
|
144
|
+
errors,
|
|
145
|
+
// 计算属性
|
|
146
|
+
isLoading,
|
|
147
|
+
unhandledErrorCount,
|
|
148
|
+
// 方法
|
|
28
149
|
toggleSidebar,
|
|
150
|
+
setSidebarCollapsed,
|
|
29
151
|
setTheme,
|
|
30
|
-
|
|
152
|
+
toggleTheme,
|
|
153
|
+
setLanguage,
|
|
154
|
+
startLoading,
|
|
155
|
+
stopLoading,
|
|
156
|
+
addError,
|
|
157
|
+
removeError,
|
|
158
|
+
clearErrors,
|
|
159
|
+
clearErrorsByType,
|
|
160
|
+
getErrorsByType,
|
|
161
|
+
showMessage
|
|
31
162
|
}
|
|
32
163
|
},
|
|
33
164
|
{
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
export type NotificationType = 'info' | 'success' | 'warning' | 'error'
|
|
5
|
+
|
|
6
|
+
export interface Notification {
|
|
7
|
+
id: string
|
|
8
|
+
title: string
|
|
9
|
+
content: string
|
|
10
|
+
type: NotificationType
|
|
11
|
+
read: boolean
|
|
12
|
+
timestamp: number
|
|
13
|
+
category?: string
|
|
14
|
+
actionUrl?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const useNotificationStore = defineStore(
|
|
18
|
+
'notification',
|
|
19
|
+
() => {
|
|
20
|
+
// 通知列表
|
|
21
|
+
const notifications = ref<Notification[]>([])
|
|
22
|
+
|
|
23
|
+
// 计算属性:未读数量
|
|
24
|
+
const unreadCount = computed(() => {
|
|
25
|
+
return notifications.value.filter((n) => !n.read).length
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// 计算属性:按类型分组的通知
|
|
29
|
+
const notificationsByType = computed(() => {
|
|
30
|
+
const grouped: Record<NotificationType, Notification[]> = {
|
|
31
|
+
info: [],
|
|
32
|
+
success: [],
|
|
33
|
+
warning: [],
|
|
34
|
+
error: []
|
|
35
|
+
}
|
|
36
|
+
notifications.value.forEach((notification) => {
|
|
37
|
+
grouped[notification.type].push(notification)
|
|
38
|
+
})
|
|
39
|
+
return grouped
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// 计算属性:按分类分组的通知
|
|
43
|
+
const notificationsByCategory = computed(() => {
|
|
44
|
+
const grouped: Record<string, Notification[]> = {}
|
|
45
|
+
notifications.value.forEach((notification) => {
|
|
46
|
+
const category = notification.category || '其他'
|
|
47
|
+
if (!grouped[category]) {
|
|
48
|
+
grouped[category] = []
|
|
49
|
+
}
|
|
50
|
+
grouped[category].push(notification)
|
|
51
|
+
})
|
|
52
|
+
return grouped
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// 添加通知
|
|
56
|
+
function addNotification(notification: Omit<Notification, 'id' | 'read' | 'timestamp'>) {
|
|
57
|
+
const newNotification: Notification = {
|
|
58
|
+
...notification,
|
|
59
|
+
id: `notif-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
60
|
+
read: false,
|
|
61
|
+
timestamp: Date.now()
|
|
62
|
+
}
|
|
63
|
+
notifications.value.unshift(newNotification) // 新通知添加到顶部
|
|
64
|
+
return newNotification.id
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 标记为已读
|
|
68
|
+
function markAsRead(id: string) {
|
|
69
|
+
const notification = notifications.value.find((n) => n.id === id)
|
|
70
|
+
if (notification) {
|
|
71
|
+
notification.read = true
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 批量标记为已读
|
|
76
|
+
function markAllAsRead() {
|
|
77
|
+
notifications.value.forEach((notification) => {
|
|
78
|
+
notification.read = true
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 标记分类为已读
|
|
83
|
+
function markCategoryAsRead(category: string) {
|
|
84
|
+
notifications.value.forEach((notification) => {
|
|
85
|
+
if (notification.category === category) {
|
|
86
|
+
notification.read = true
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 删除通知
|
|
92
|
+
function removeNotification(id: string) {
|
|
93
|
+
const index = notifications.value.findIndex((n) => n.id === id)
|
|
94
|
+
if (index > -1) {
|
|
95
|
+
notifications.value.splice(index, 1)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 批量删除已读通知
|
|
100
|
+
function removeReadNotifications() {
|
|
101
|
+
notifications.value = notifications.value.filter((n) => !n.read)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 清空所有通知
|
|
105
|
+
function clearAll() {
|
|
106
|
+
notifications.value = []
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 获取未读通知
|
|
110
|
+
function getUnreadNotifications(): Notification[] {
|
|
111
|
+
return notifications.value.filter((n) => !n.read)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 获取指定类型的通知
|
|
115
|
+
function getNotificationsByType(type: NotificationType): Notification[] {
|
|
116
|
+
return notifications.value.filter((n) => n.type === type)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 获取指定分类的通知
|
|
120
|
+
function getNotificationsByCategory(category: string): Notification[] {
|
|
121
|
+
return notifications.value.filter((n) => n.category === category)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 初始化示例数据
|
|
125
|
+
function initSampleData() {
|
|
126
|
+
if (notifications.value.length === 0) {
|
|
127
|
+
addNotification({
|
|
128
|
+
title: '系统通知',
|
|
129
|
+
content: '欢迎使用本系统!',
|
|
130
|
+
type: 'info',
|
|
131
|
+
category: '系统'
|
|
132
|
+
})
|
|
133
|
+
addNotification({
|
|
134
|
+
title: '订单提醒',
|
|
135
|
+
content: '您有新的订单待处理',
|
|
136
|
+
type: 'warning',
|
|
137
|
+
category: '订单'
|
|
138
|
+
})
|
|
139
|
+
addNotification({
|
|
140
|
+
title: '操作成功',
|
|
141
|
+
content: '数据保存成功',
|
|
142
|
+
type: 'success',
|
|
143
|
+
category: '操作'
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
notifications,
|
|
150
|
+
unreadCount,
|
|
151
|
+
notificationsByType,
|
|
152
|
+
notificationsByCategory,
|
|
153
|
+
addNotification,
|
|
154
|
+
markAsRead,
|
|
155
|
+
markAllAsRead,
|
|
156
|
+
markCategoryAsRead,
|
|
157
|
+
removeNotification,
|
|
158
|
+
removeReadNotifications,
|
|
159
|
+
clearAll,
|
|
160
|
+
getUnreadNotifications,
|
|
161
|
+
getNotificationsByType,
|
|
162
|
+
getNotificationsByCategory,
|
|
163
|
+
initSampleData
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
persist: {
|
|
168
|
+
key: 'notification-store',
|
|
169
|
+
paths: ['notifications']
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
)
|