@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.
- 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/App.tsx +7 -1
- package/templates/react/src/api/notification.ts +43 -0
- package/templates/react/src/components/NotificationButton/index.tsx +219 -0
- package/templates/react/src/components/Toast/index.tsx +150 -0
- package/templates/react/src/hooks/useForm.ts +77 -0
- package/templates/react/src/layouts/DefaultLayout.tsx +2 -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 +146 -0
- package/templates/react/src/stores/permission.ts +173 -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/App.vue +2 -0
- package/templates/vue/src/api/notification.ts +43 -0
- package/templates/vue/src/auto-imports.d.ts +5 -0
- package/templates/vue/src/components/NotificationButton/index.vue +242 -0
- package/templates/vue/src/components/Toast/index.vue +126 -0
- package/templates/vue/src/components.d.ts +2 -0
- package/templates/vue/src/layouts/DefaultLayout.vue +2 -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 +189 -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
|
@@ -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,189 @@
|
|
|
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
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 从 API 获取消息
|
|
137
|
+
async function fetchNotifications() {
|
|
138
|
+
try {
|
|
139
|
+
// 动态导入避免循环依赖
|
|
140
|
+
const { getNotificationsApi } = await import('@/api/notification')
|
|
141
|
+
const response = await getNotificationsApi({ unreadOnly: false, pageSize: 20 })
|
|
142
|
+
if (response.data) {
|
|
143
|
+
// 合并新消息(实际项目中需要去重逻辑)
|
|
144
|
+
const existingIds = new Set(notifications.value.map((n) => n.id))
|
|
145
|
+
response.data.list.forEach((item) => {
|
|
146
|
+
if (!existingIds.has(item.id)) {
|
|
147
|
+
addNotification({
|
|
148
|
+
title: item.title,
|
|
149
|
+
content: item.content,
|
|
150
|
+
type: item.type,
|
|
151
|
+
category: item.category,
|
|
152
|
+
actionUrl: item.actionUrl
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error('获取消息失败:', error)
|
|
159
|
+
// 如果 API 失败,使用示例数据
|
|
160
|
+
initSampleData()
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
notifications,
|
|
166
|
+
unreadCount,
|
|
167
|
+
notificationsByType,
|
|
168
|
+
notificationsByCategory,
|
|
169
|
+
addNotification,
|
|
170
|
+
markAsRead,
|
|
171
|
+
markAllAsRead,
|
|
172
|
+
markCategoryAsRead,
|
|
173
|
+
removeNotification,
|
|
174
|
+
removeReadNotifications,
|
|
175
|
+
clearAll,
|
|
176
|
+
getUnreadNotifications,
|
|
177
|
+
getNotificationsByType,
|
|
178
|
+
getNotificationsByCategory,
|
|
179
|
+
initSampleData,
|
|
180
|
+
fetchNotifications
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
persist: {
|
|
185
|
+
key: 'notification-store',
|
|
186
|
+
paths: ['notifications']
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { useUserStore } from './user'
|
|
4
|
+
|
|
5
|
+
export type Permission = string
|
|
6
|
+
export type Role = string
|
|
7
|
+
|
|
8
|
+
export interface PermissionConfig {
|
|
9
|
+
// 权限标识
|
|
10
|
+
permission: Permission
|
|
11
|
+
// 权限名称
|
|
12
|
+
name: string
|
|
13
|
+
// 权限描述
|
|
14
|
+
description?: string
|
|
15
|
+
// 所属模块
|
|
16
|
+
module?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RoleConfig {
|
|
20
|
+
// 角色标识
|
|
21
|
+
role: Role
|
|
22
|
+
// 角色名称
|
|
23
|
+
name: string
|
|
24
|
+
// 角色描述
|
|
25
|
+
description?: string
|
|
26
|
+
// 角色拥有的权限列表
|
|
27
|
+
permissions: Permission[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const usePermissionStore = defineStore(
|
|
31
|
+
'permission',
|
|
32
|
+
() => {
|
|
33
|
+
// 所有权限配置
|
|
34
|
+
const allPermissions = ref<PermissionConfig[]>([
|
|
35
|
+
{ permission: 'user:view', name: '查看用户', module: '用户管理' },
|
|
36
|
+
{ permission: 'user:create', name: '创建用户', module: '用户管理' },
|
|
37
|
+
{ permission: 'user:edit', name: '编辑用户', module: '用户管理' },
|
|
38
|
+
{ permission: 'user:delete', name: '删除用户', module: '用户管理' },
|
|
39
|
+
{ permission: 'role:view', name: '查看角色', module: '角色管理' },
|
|
40
|
+
{ permission: 'role:create', name: '创建角色', module: '角色管理' },
|
|
41
|
+
{ permission: 'role:edit', name: '编辑角色', module: '角色管理' },
|
|
42
|
+
{ permission: 'role:delete', name: '删除角色', module: '角色管理' },
|
|
43
|
+
{ permission: 'system:config', name: '系统配置', module: '系统管理' },
|
|
44
|
+
{ permission: 'system:log', name: '查看日志', module: '系统管理' }
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
// 所有角色配置
|
|
48
|
+
const allRoles = ref<RoleConfig[]>([
|
|
49
|
+
{
|
|
50
|
+
role: 'admin',
|
|
51
|
+
name: '超级管理员',
|
|
52
|
+
description: '拥有所有权限',
|
|
53
|
+
permissions: ['user:view', 'user:create', 'user:edit', 'user:delete', 'role:view', 'role:create', 'role:edit', 'role:delete', 'system:config', 'system:log']
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
role: 'editor',
|
|
57
|
+
name: '编辑',
|
|
58
|
+
description: '可以查看和编辑内容',
|
|
59
|
+
permissions: ['user:view', 'user:create', 'user:edit', 'role:view']
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
role: 'viewer',
|
|
63
|
+
name: '只读',
|
|
64
|
+
description: '只能查看内容',
|
|
65
|
+
permissions: ['user:view', 'role:view']
|
|
66
|
+
}
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
// 当前用户拥有的权限(从 user store 获取)
|
|
70
|
+
const userPermissions = computed(() => {
|
|
71
|
+
const userStore = useUserStore()
|
|
72
|
+
if (!userStore.userInfo) return []
|
|
73
|
+
|
|
74
|
+
const userRoles = userStore.userInfo.roles || []
|
|
75
|
+
const permissions: Permission[] = []
|
|
76
|
+
|
|
77
|
+
// 根据用户角色获取所有权限
|
|
78
|
+
userRoles.forEach((role) => {
|
|
79
|
+
const roleConfig = allRoles.value.find((r) => r.role === role)
|
|
80
|
+
if (roleConfig) {
|
|
81
|
+
permissions.push(...roleConfig.permissions)
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// 去重
|
|
86
|
+
return Array.from(new Set(permissions))
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// 检查是否有指定权限
|
|
90
|
+
function hasPermission(permission: Permission): boolean {
|
|
91
|
+
return userPermissions.value.includes(permission)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 检查是否有任意一个权限
|
|
95
|
+
function hasAnyPermission(permissions: Permission[]): boolean {
|
|
96
|
+
return permissions.some((p) => hasPermission(p))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 检查是否有所有权限
|
|
100
|
+
function hasAllPermissions(permissions: Permission[]): boolean {
|
|
101
|
+
return permissions.every((p) => hasPermission(p))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 检查是否有指定角色
|
|
105
|
+
function hasRole(role: Role): boolean {
|
|
106
|
+
const userStore = useUserStore()
|
|
107
|
+
if (!userStore.userInfo) return false
|
|
108
|
+
return userStore.userInfo.roles?.includes(role) || false
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 检查是否有任意一个角色
|
|
112
|
+
function hasAnyRole(roles: Role[]): boolean {
|
|
113
|
+
return roles.some((r) => hasRole(r))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 获取角色配置
|
|
117
|
+
function getRoleConfig(role: Role): RoleConfig | undefined {
|
|
118
|
+
return allRoles.value.find((r) => r.role === role)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 获取权限配置
|
|
122
|
+
function getPermissionConfig(permission: Permission): PermissionConfig | undefined {
|
|
123
|
+
return allPermissions.value.find((p) => p.permission === permission)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 按模块分组权限
|
|
127
|
+
const permissionsByModule = computed(() => {
|
|
128
|
+
const grouped: Record<string, PermissionConfig[]> = {}
|
|
129
|
+
allPermissions.value.forEach((permission) => {
|
|
130
|
+
const module = permission.module || '其他'
|
|
131
|
+
if (!grouped[module]) {
|
|
132
|
+
grouped[module] = []
|
|
133
|
+
}
|
|
134
|
+
grouped[module].push(permission)
|
|
135
|
+
})
|
|
136
|
+
return grouped
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// 添加权限配置
|
|
140
|
+
function addPermission(permission: PermissionConfig) {
|
|
141
|
+
if (!allPermissions.value.find((p) => p.permission === permission.permission)) {
|
|
142
|
+
allPermissions.value.push(permission)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 添加角色配置
|
|
147
|
+
function addRole(role: RoleConfig) {
|
|
148
|
+
if (!allRoles.value.find((r) => r.role === role.role)) {
|
|
149
|
+
allRoles.value.push(role)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 更新角色权限
|
|
154
|
+
function updateRolePermissions(role: Role, permissions: Permission[]) {
|
|
155
|
+
const roleConfig = allRoles.value.find((r) => r.role === role)
|
|
156
|
+
if (roleConfig) {
|
|
157
|
+
roleConfig.permissions = permissions
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
allPermissions,
|
|
163
|
+
allRoles,
|
|
164
|
+
userPermissions,
|
|
165
|
+
permissionsByModule,
|
|
166
|
+
hasPermission,
|
|
167
|
+
hasAnyPermission,
|
|
168
|
+
hasAllPermissions,
|
|
169
|
+
hasRole,
|
|
170
|
+
hasAnyRole,
|
|
171
|
+
getRoleConfig,
|
|
172
|
+
getPermissionConfig,
|
|
173
|
+
addPermission,
|
|
174
|
+
addRole,
|
|
175
|
+
updateRolePermissions
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
persist: {
|
|
180
|
+
key: 'permission-store',
|
|
181
|
+
paths: ['allPermissions', 'allRoles']
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { ref, computed } from 'vue'
|
|
3
|
+
import { usePermissionStore } from './permission'
|
|
3
4
|
|
|
4
5
|
export interface UserInfo {
|
|
5
6
|
id: number
|
|
@@ -7,6 +8,10 @@ export interface UserInfo {
|
|
|
7
8
|
nickname: string
|
|
8
9
|
avatar: string
|
|
9
10
|
roles: string[]
|
|
11
|
+
email?: string
|
|
12
|
+
phone?: string
|
|
13
|
+
department?: string
|
|
14
|
+
lastLoginTime?: number
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
export const useUserStore = defineStore(
|
|
@@ -14,35 +19,137 @@ export const useUserStore = defineStore(
|
|
|
14
19
|
() => {
|
|
15
20
|
const token = ref<string>('')
|
|
16
21
|
const userInfo = ref<UserInfo | null>(null)
|
|
22
|
+
const loginHistory = ref<Array<{ time: number; ip: string; location?: string }>>([])
|
|
17
23
|
|
|
18
24
|
const isLoggedIn = computed(() => !!token.value)
|
|
19
25
|
|
|
26
|
+
// 计算属性:用户显示名称
|
|
27
|
+
const displayName = computed(() => {
|
|
28
|
+
return userInfo.value?.nickname || userInfo.value?.username || '未登录'
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// 计算属性:用户头像
|
|
32
|
+
const avatar = computed(() => {
|
|
33
|
+
return userInfo.value?.avatar || '/default-avatar.png'
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// 计算属性:是否是管理员
|
|
37
|
+
const isAdmin = computed(() => {
|
|
38
|
+
return userInfo.value?.roles?.includes('admin') || false
|
|
39
|
+
})
|
|
40
|
+
|
|
20
41
|
function setToken(val: string) {
|
|
21
42
|
token.value = val
|
|
22
43
|
}
|
|
23
44
|
|
|
24
45
|
function setUserInfo(info: UserInfo) {
|
|
25
46
|
userInfo.value = info
|
|
47
|
+
// 更新最后登录时间
|
|
48
|
+
if (userInfo.value) {
|
|
49
|
+
userInfo.value.lastLoginTime = Date.now()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 更新用户信息(部分更新)
|
|
54
|
+
function updateUserInfo(updates: Partial<UserInfo>) {
|
|
55
|
+
if (userInfo.value) {
|
|
56
|
+
userInfo.value = { ...userInfo.value, ...updates }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 添加登录历史记录
|
|
61
|
+
function addLoginHistory(ip: string, location?: string) {
|
|
62
|
+
loginHistory.value.unshift({
|
|
63
|
+
time: Date.now(),
|
|
64
|
+
ip,
|
|
65
|
+
location
|
|
66
|
+
})
|
|
67
|
+
// 只保留最近 10 条记录
|
|
68
|
+
if (loginHistory.value.length > 10) {
|
|
69
|
+
loginHistory.value = loginHistory.value.slice(0, 10)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 检查用户是否有指定权限(通过 permission store)
|
|
74
|
+
function hasPermission(permission: string): boolean {
|
|
75
|
+
const permissionStore = usePermissionStore()
|
|
76
|
+
return permissionStore.hasPermission(permission)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 检查用户是否有指定角色
|
|
80
|
+
function hasRole(role: string): boolean {
|
|
81
|
+
return userInfo.value?.roles?.includes(role) || false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 添加角色
|
|
85
|
+
function addRole(role: string) {
|
|
86
|
+
if (userInfo.value && !userInfo.value.roles.includes(role)) {
|
|
87
|
+
userInfo.value.roles.push(role)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 移除角色
|
|
92
|
+
function removeRole(role: string) {
|
|
93
|
+
if (userInfo.value) {
|
|
94
|
+
userInfo.value.roles = userInfo.value.roles.filter((r) => r !== role)
|
|
95
|
+
}
|
|
26
96
|
}
|
|
27
97
|
|
|
28
98
|
function logout() {
|
|
29
99
|
token.value = ''
|
|
30
100
|
userInfo.value = null
|
|
101
|
+
// 清空登录历史(可选,根据需求决定)
|
|
102
|
+
// loginHistory.value = []
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 模拟登录
|
|
106
|
+
async function login(username: string, _password: string) {
|
|
107
|
+
// 这里应该是实际的 API 调用
|
|
108
|
+
// 模拟异步登录
|
|
109
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
110
|
+
|
|
111
|
+
// 模拟登录成功
|
|
112
|
+
const mockUserInfo: UserInfo = {
|
|
113
|
+
id: 1,
|
|
114
|
+
username,
|
|
115
|
+
nickname: username === 'admin' ? '管理员' : '普通用户',
|
|
116
|
+
avatar: '/avatar.png',
|
|
117
|
+
roles: username === 'admin' ? ['admin'] : ['viewer'],
|
|
118
|
+
email: `${username}@example.com`,
|
|
119
|
+
lastLoginTime: Date.now()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
setToken('mock-token-' + Date.now())
|
|
123
|
+
setUserInfo(mockUserInfo)
|
|
124
|
+
addLoginHistory('192.168.1.100', '本地')
|
|
125
|
+
|
|
126
|
+
return mockUserInfo
|
|
31
127
|
}
|
|
32
128
|
|
|
33
129
|
return {
|
|
34
130
|
token,
|
|
35
131
|
userInfo,
|
|
132
|
+
loginHistory,
|
|
36
133
|
isLoggedIn,
|
|
134
|
+
displayName,
|
|
135
|
+
avatar,
|
|
136
|
+
isAdmin,
|
|
37
137
|
setToken,
|
|
38
138
|
setUserInfo,
|
|
39
|
-
|
|
139
|
+
updateUserInfo,
|
|
140
|
+
addLoginHistory,
|
|
141
|
+
hasPermission,
|
|
142
|
+
hasRole,
|
|
143
|
+
addRole,
|
|
144
|
+
removeRole,
|
|
145
|
+
logout,
|
|
146
|
+
login
|
|
40
147
|
}
|
|
41
148
|
},
|
|
42
149
|
{
|
|
43
150
|
persist: {
|
|
44
151
|
key: 'user-store',
|
|
45
|
-
paths: ['token']
|
|
152
|
+
paths: ['token', 'userInfo', 'loginHistory']
|
|
46
153
|
}
|
|
47
154
|
}
|
|
48
155
|
)
|