xto-fronted 0.1.1 → 0.1.3
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/.env.development +3 -4
- package/.env.production +3 -4
- package/bin/cli.js +104 -0
- package/dist/{403-MQkNUulz.js → 403-DM5wfQkM.js} +6 -6
- package/dist/{404-BOFYLq4X.js → 404-BurAu5LC.js} +7 -7
- package/dist/api/auth.d.ts +9 -8
- package/dist/api/menu.d.ts +3 -0
- package/dist/api/user.d.ts +2 -12
- package/dist/composables/index.d.ts +8 -0
- package/dist/composables/useApp.d.ts +64 -0
- package/dist/composables/useAuth.d.ts +19 -4
- package/dist/composables/useMenu.d.ts +34 -0
- package/dist/config/index.d.ts +11 -0
- package/dist/index-BNiEld34.js +15 -0
- package/dist/index-Be9RiEfo.js +98 -0
- package/dist/index-BqRv1bdN.js +1185 -0
- package/dist/index-CQLVXvNJ.js +15 -0
- package/dist/index-CyiE8n2V.js +15 -0
- package/dist/index-xauR1bOL.js +15 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.es.js +50 -66
- package/dist/index.umd.js +1 -1
- package/dist/stores/auth.d.ts +60 -23
- package/dist/stores/menu.d.ts +40 -29
- package/dist/stores/user.d.ts +63 -84
- package/dist/style.css +1 -1
- package/dist/utils/auth.d.ts +15 -7
- package/dist/utils/permission.d.ts +1 -6
- package/dist/views/system/menu/index.vue.d.ts +1 -3
- package/dist/views/system/role/index.vue.d.ts +1 -3
- package/dist/views/system/user/index.vue.d.ts +1 -3
- package/package.json +27 -19
- package/src/api/auth.ts +34 -25
- package/src/api/menu.ts +13 -0
- package/src/api/user.ts +11 -45
- package/src/components/Layout/Header.vue +334 -389
- package/src/components/Layout/Sidebar.vue +212 -296
- package/src/components/Layout/Tabs.vue +19 -133
- package/src/composables/index.ts +9 -0
- package/src/composables/useApp.ts +170 -0
- package/src/composables/useAuth.ts +69 -44
- package/src/composables/useMenu.ts +141 -0
- package/src/config/index.ts +19 -0
- package/src/directives/permission.ts +40 -37
- package/src/index.ts +9 -4
- package/src/router/index.ts +70 -80
- package/src/stores/auth.ts +44 -31
- package/src/stores/menu.ts +157 -79
- package/src/stores/user.ts +40 -72
- package/src/types/api.d.ts +102 -83
- package/src/types/xto.d.ts +148 -148
- package/src/utils/auth.ts +85 -61
- package/src/utils/permission.ts +29 -41
- package/src/utils/request.ts +125 -125
- package/src/utils/storage.ts +10 -1
- package/src/views/dashboard/index.vue +31 -283
- package/src/views/login/index.vue +140 -247
- package/src/views/system/menu/index.vue +31 -380
- package/src/views/system/role/index.vue +31 -303
- package/src/views/system/user/index.vue +31 -326
- package/vite.config.ts +3 -3
- package/dist/index-BJxYdNPy.js +0 -475
- package/dist/index-BvnIIBR1.js +0 -142
- package/dist/index-CEvAq6KE.js +0 -372
- package/dist/index-DPkqej__.js +0 -345
- package/dist/index-pq9Z5K62.js +0 -184
- package/dist/index-vVfjShJR.js +0 -1183
package/src/utils/permission.ts
CHANGED
|
@@ -1,42 +1,30 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 权限工具函数
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
*
|
|
9
|
-
* @param permission 权限标识
|
|
10
|
-
*/
|
|
11
|
-
export function hasPermission(permission: string | string[]): boolean {
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (Array.isArray(role)) {
|
|
31
|
-
return role.some(r => roles.includes(r))
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return roles.includes(role)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* 检查是否是管理员
|
|
39
|
-
*/
|
|
40
|
-
export function isAdmin(): boolean {
|
|
41
|
-
return hasRole('admin')
|
|
1
|
+
/**
|
|
2
|
+
* 权限工具函数
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useMenuStore } from '@/stores/menu'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 检查是否有权限(基于菜单按钮权限)
|
|
9
|
+
* @param permission 权限标识
|
|
10
|
+
*/
|
|
11
|
+
export function hasPermission(permission: string | string[]): boolean {
|
|
12
|
+
const menuStore = useMenuStore()
|
|
13
|
+
const currentPath = window.location.pathname
|
|
14
|
+
const btnList = menuStore.menuBtnListMap[currentPath] || []
|
|
15
|
+
const btnCodes = btnList.map(item => item.code)
|
|
16
|
+
|
|
17
|
+
if (Array.isArray(permission)) {
|
|
18
|
+
return permission.some(p => btnCodes.includes(p))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return btnCodes.includes(permission)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 检查是否是管理员
|
|
26
|
+
*/
|
|
27
|
+
export function isAdmin(): boolean {
|
|
28
|
+
// 暂时返回 true,后续可根据实际需求实现
|
|
29
|
+
return true
|
|
42
30
|
}
|
package/src/utils/request.ts
CHANGED
|
@@ -1,126 +1,126 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Axios 请求封装
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
|
|
6
|
-
import { getToken, clearToken } from './auth'
|
|
7
|
-
import { Message } from '@xto/feedback'
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
break
|
|
81
|
-
case 404:
|
|
82
|
-
Message.error('请求资源不存在')
|
|
83
|
-
break
|
|
84
|
-
case 500:
|
|
85
|
-
Message.error('服务器错误')
|
|
86
|
-
break
|
|
87
|
-
default:
|
|
88
|
-
Message.error(response.data?.message || '请求失败')
|
|
89
|
-
}
|
|
90
|
-
} else {
|
|
91
|
-
Message.error('网络连接失败')
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return Promise.reject(error)
|
|
95
|
-
}
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
return instance
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const request = createRequest()
|
|
102
|
-
|
|
103
|
-
// 请求方法
|
|
104
|
-
export const http = {
|
|
105
|
-
get<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
|
106
|
-
return request.get(url, config).then((res) => res.data)
|
|
107
|
-
},
|
|
108
|
-
|
|
109
|
-
post<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
|
110
|
-
return request.post(url, data, config).then((res) => res.data)
|
|
111
|
-
},
|
|
112
|
-
|
|
113
|
-
put<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
|
114
|
-
return request.put(url, data, config).then((res) => res.data)
|
|
115
|
-
},
|
|
116
|
-
|
|
117
|
-
patch<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
|
118
|
-
return request.patch(url, data, config).then((res) => res.data)
|
|
119
|
-
},
|
|
120
|
-
|
|
121
|
-
delete<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
|
122
|
-
return request.delete(url, config).then((res) => res.data)
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Axios 请求封装
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
|
|
6
|
+
import { getToken, getTokenType, clearToken } from './auth'
|
|
7
|
+
import { Message } from '@xto/feedback'
|
|
8
|
+
import config from '@/config'
|
|
9
|
+
|
|
10
|
+
// 响应数据接口
|
|
11
|
+
export interface ApiResponse<T = unknown> {
|
|
12
|
+
code: number
|
|
13
|
+
data: T
|
|
14
|
+
message: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 分页参数
|
|
18
|
+
export interface PageParams {
|
|
19
|
+
page: number
|
|
20
|
+
pageSize: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 分页响应
|
|
24
|
+
export interface PageResponse<T> {
|
|
25
|
+
list: T[]
|
|
26
|
+
total: number
|
|
27
|
+
page: number
|
|
28
|
+
pageSize: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 创建 axios 实例
|
|
32
|
+
const createRequest = (): AxiosInstance => {
|
|
33
|
+
const instance = axios.create({
|
|
34
|
+
baseURL: config.baseUrl,
|
|
35
|
+
timeout: 15000,
|
|
36
|
+
headers: {
|
|
37
|
+
'Content-Type': 'application/json'
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// 请求拦截器
|
|
42
|
+
instance.interceptors.request.use(
|
|
43
|
+
(config: InternalAxiosRequestConfig) => {
|
|
44
|
+
const token = getToken()
|
|
45
|
+
if (token) {
|
|
46
|
+
const tokenType = getTokenType()
|
|
47
|
+
config.headers.Authorization = `${tokenType} ${token}`
|
|
48
|
+
}
|
|
49
|
+
return config
|
|
50
|
+
},
|
|
51
|
+
(error) => {
|
|
52
|
+
return Promise.reject(error)
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// 响应拦截器
|
|
57
|
+
instance.interceptors.response.use(
|
|
58
|
+
(response: AxiosResponse<ApiResponse>) => {
|
|
59
|
+
const { data } = response
|
|
60
|
+
|
|
61
|
+
// 成功
|
|
62
|
+
if (data.code === 200 || data.code === 0) {
|
|
63
|
+
return response
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 业务错误
|
|
67
|
+
Message.error(data.message || '请求失败')
|
|
68
|
+
return Promise.reject(new Error(data.message || '请求失败'))
|
|
69
|
+
},
|
|
70
|
+
(error) => {
|
|
71
|
+
const { response } = error
|
|
72
|
+
|
|
73
|
+
if (response) {
|
|
74
|
+
switch (response.status) {
|
|
75
|
+
case 401:
|
|
76
|
+
case 403:
|
|
77
|
+
Message.error('登录已过期,请重新登录')
|
|
78
|
+
clearToken()
|
|
79
|
+
window.location.href = '/login'
|
|
80
|
+
break
|
|
81
|
+
case 404:
|
|
82
|
+
Message.error('请求资源不存在')
|
|
83
|
+
break
|
|
84
|
+
case 500:
|
|
85
|
+
Message.error('服务器错误')
|
|
86
|
+
break
|
|
87
|
+
default:
|
|
88
|
+
Message.error(response.data?.message || '请求失败')
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
Message.error('网络连接失败')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return Promise.reject(error)
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return instance
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const request = createRequest()
|
|
102
|
+
|
|
103
|
+
// 请求方法
|
|
104
|
+
export const http = {
|
|
105
|
+
get<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
|
106
|
+
return request.get(url, config).then((res) => res.data)
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
post<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
|
110
|
+
return request.post(url, data, config).then((res) => res.data)
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
put<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
|
114
|
+
return request.put(url, data, config).then((res) => res.data)
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
patch<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
|
118
|
+
return request.patch(url, data, config).then((res) => res.data)
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
delete<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
|
122
|
+
return request.delete(url, config).then((res) => res.data)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
126
|
export default request
|
package/src/utils/storage.ts
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
* 本地存储封装
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import config from '@/config'
|
|
6
|
+
|
|
7
|
+
// toboyu-cloud 格式的 key 前缀
|
|
8
|
+
const getPrefix = (): string => {
|
|
9
|
+
return `tooyu-cloud:${config.appId}:`
|
|
10
|
+
}
|
|
6
11
|
|
|
7
12
|
interface StorageWrapper {
|
|
8
13
|
get<T>(key: string): T | null
|
|
@@ -14,6 +19,7 @@ interface StorageWrapper {
|
|
|
14
19
|
const createStorage = (storage: globalThis.Storage): StorageWrapper => {
|
|
15
20
|
return {
|
|
16
21
|
get<T>(key: string): T | null {
|
|
22
|
+
const prefix = getPrefix()
|
|
17
23
|
const value = storage.getItem(prefix + key)
|
|
18
24
|
if (!value) return null
|
|
19
25
|
try {
|
|
@@ -23,6 +29,7 @@ const createStorage = (storage: globalThis.Storage): StorageWrapper => {
|
|
|
23
29
|
}
|
|
24
30
|
},
|
|
25
31
|
set(key: string, value: unknown): void {
|
|
32
|
+
const prefix = getPrefix()
|
|
26
33
|
if (value === null || value === undefined) {
|
|
27
34
|
storage.removeItem(prefix + key)
|
|
28
35
|
return
|
|
@@ -31,9 +38,11 @@ const createStorage = (storage: globalThis.Storage): StorageWrapper => {
|
|
|
31
38
|
storage.setItem(prefix + key, stringValue)
|
|
32
39
|
},
|
|
33
40
|
remove(key: string): void {
|
|
41
|
+
const prefix = getPrefix()
|
|
34
42
|
storage.removeItem(prefix + key)
|
|
35
43
|
},
|
|
36
44
|
clear(): void {
|
|
45
|
+
const prefix = getPrefix()
|
|
37
46
|
const keys = Object.keys(storage)
|
|
38
47
|
keys.forEach(key => {
|
|
39
48
|
if (key.startsWith(prefix)) {
|
|
@@ -1,284 +1,32 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
<Card v-for="stat in stats" :key="stat.title" class="stat-card">
|
|
33
|
-
<div class="stat-card__content">
|
|
34
|
-
<div class="stat-card__icon" :style="{ backgroundColor: stat.color + '20' }">
|
|
35
|
-
{{ stat.icon }}
|
|
36
|
-
</div>
|
|
37
|
-
<div class="stat-card__info">
|
|
38
|
-
<div class="stat-card__title">{{ stat.title }}</div>
|
|
39
|
-
<div class="stat-card__value" :style="{ color: stat.color }">
|
|
40
|
-
{{ stat.value.toLocaleString() }}
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|
|
43
|
-
</div>
|
|
44
|
-
</Card>
|
|
45
|
-
</div>
|
|
46
|
-
|
|
47
|
-
<!-- 主体内容 -->
|
|
48
|
-
<div class="dashboard__main">
|
|
49
|
-
<!-- 快捷入口 -->
|
|
50
|
-
<Card class="dashboard__quick">
|
|
51
|
-
<template #header>
|
|
52
|
-
<span class="card-title">快捷入口</span>
|
|
53
|
-
</template>
|
|
54
|
-
<div class="quick-links">
|
|
55
|
-
<router-link
|
|
56
|
-
v-for="link in quickLinks"
|
|
57
|
-
:key="link.path"
|
|
58
|
-
:to="link.path"
|
|
59
|
-
class="quick-link"
|
|
60
|
-
>
|
|
61
|
-
<span class="quick-link__icon">{{ link.icon }}</span>
|
|
62
|
-
<span class="quick-link__title">{{ link.title }}</span>
|
|
63
|
-
</router-link>
|
|
64
|
-
</div>
|
|
65
|
-
</Card>
|
|
66
|
-
|
|
67
|
-
<!-- 最近活动 -->
|
|
68
|
-
<Card class="dashboard__activity">
|
|
69
|
-
<template #header>
|
|
70
|
-
<span class="card-title">最近活动</span>
|
|
71
|
-
</template>
|
|
72
|
-
<div class="activity-list">
|
|
73
|
-
<div v-for="(activity, index) in activities" :key="index" class="activity-item">
|
|
74
|
-
<Tag :type="activity.type as any" size="small">{{ activity.user }}</Tag>
|
|
75
|
-
<span class="activity-item__action">{{ activity.action }}</span>
|
|
76
|
-
<span class="activity-item__time">{{ activity.time }}</span>
|
|
77
|
-
</div>
|
|
78
|
-
</div>
|
|
79
|
-
</Card>
|
|
80
|
-
</div>
|
|
81
|
-
|
|
82
|
-
<!-- 系统信息 -->
|
|
83
|
-
<Card class="dashboard__system">
|
|
84
|
-
<template #header>
|
|
85
|
-
<span class="card-title">系统信息</span>
|
|
86
|
-
</template>
|
|
87
|
-
<div class="system-info">
|
|
88
|
-
<div class="system-info__item">
|
|
89
|
-
<span class="system-info__label">系统版本</span>
|
|
90
|
-
<span class="system-info__value">v1.0.0</span>
|
|
91
|
-
</div>
|
|
92
|
-
<div class="system-info__item">
|
|
93
|
-
<span class="system-info__label">Vue 版本</span>
|
|
94
|
-
<span class="system-info__value">3.4.21</span>
|
|
95
|
-
</div>
|
|
96
|
-
<div class="system-info__item">
|
|
97
|
-
<span class="system-info__label">构建工具</span>
|
|
98
|
-
<span class="system-info__value">Vite 5</span>
|
|
99
|
-
</div>
|
|
100
|
-
<div class="system-info__item">
|
|
101
|
-
<span class="system-info__label">UI 组件库</span>
|
|
102
|
-
<span class="system-info__value">xto</span>
|
|
103
|
-
</div>
|
|
104
|
-
<div class="system-info__item">
|
|
105
|
-
<span class="system-info__label">服务器状态</span>
|
|
106
|
-
<Progress :percentage="75" status="success" />
|
|
107
|
-
</div>
|
|
108
|
-
<div class="system-info__item">
|
|
109
|
-
<span class="system-info__label">内存使用</span>
|
|
110
|
-
<Progress :percentage="45" />
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
</Card>
|
|
114
|
-
</div>
|
|
115
|
-
</template>
|
|
116
|
-
|
|
117
|
-
<style lang="scss" scoped>
|
|
118
|
-
.dashboard {
|
|
119
|
-
padding: 20px;
|
|
120
|
-
|
|
121
|
-
&__stats {
|
|
122
|
-
display: grid;
|
|
123
|
-
grid-template-columns: repeat(4, 1fr);
|
|
124
|
-
gap: 20px;
|
|
125
|
-
margin-bottom: 20px;
|
|
126
|
-
|
|
127
|
-
@media (max-width: 1200px) {
|
|
128
|
-
grid-template-columns: repeat(2, 1fr);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
@media (max-width: 768px) {
|
|
132
|
-
grid-template-columns: 1fr;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
&__main {
|
|
137
|
-
display: grid;
|
|
138
|
-
grid-template-columns: repeat(2, 1fr);
|
|
139
|
-
gap: 20px;
|
|
140
|
-
margin-bottom: 20px;
|
|
141
|
-
|
|
142
|
-
@media (max-width: 992px) {
|
|
143
|
-
grid-template-columns: 1fr;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
&__quick,
|
|
148
|
-
&__activity {
|
|
149
|
-
min-height: 300px;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
.card-title {
|
|
154
|
-
font-size: 16px;
|
|
155
|
-
font-weight: 500;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
.stat-card {
|
|
159
|
-
&__content {
|
|
160
|
-
display: flex;
|
|
161
|
-
align-items: center;
|
|
162
|
-
gap: 15px;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
&__icon {
|
|
166
|
-
width: 50px;
|
|
167
|
-
height: 50px;
|
|
168
|
-
display: flex;
|
|
169
|
-
align-items: center;
|
|
170
|
-
justify-content: center;
|
|
171
|
-
font-size: 24px;
|
|
172
|
-
border-radius: var(--border-radius-base);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
&__info {
|
|
176
|
-
flex: 1;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
&__title {
|
|
180
|
-
font-size: 14px;
|
|
181
|
-
color: var(--color-text-secondary);
|
|
182
|
-
margin-bottom: 5px;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
&__value {
|
|
186
|
-
font-size: 24px;
|
|
187
|
-
font-weight: 600;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
.quick-links {
|
|
192
|
-
display: grid;
|
|
193
|
-
grid-template-columns: repeat(4, 1fr);
|
|
194
|
-
gap: 15px;
|
|
195
|
-
|
|
196
|
-
@media (max-width: 768px) {
|
|
197
|
-
grid-template-columns: repeat(2, 1fr);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
.quick-link {
|
|
202
|
-
display: flex;
|
|
203
|
-
flex-direction: column;
|
|
204
|
-
align-items: center;
|
|
205
|
-
justify-content: center;
|
|
206
|
-
padding: 20px;
|
|
207
|
-
border-radius: var(--border-radius-base);
|
|
208
|
-
background-color: var(--color-fill);
|
|
209
|
-
text-decoration: none;
|
|
210
|
-
transition: all var(--transition-duration-fast);
|
|
211
|
-
|
|
212
|
-
&:hover {
|
|
213
|
-
background-color: var(--color-primary-light-9);
|
|
214
|
-
transform: translateY(-2px);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
&__icon {
|
|
218
|
-
font-size: 28px;
|
|
219
|
-
margin-bottom: 10px;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
&__title {
|
|
223
|
-
font-size: 14px;
|
|
224
|
-
color: var(--color-text-primary);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
.activity-list {
|
|
229
|
-
display: flex;
|
|
230
|
-
flex-direction: column;
|
|
231
|
-
gap: 15px;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
.activity-item {
|
|
235
|
-
display: flex;
|
|
236
|
-
align-items: center;
|
|
237
|
-
gap: 10px;
|
|
238
|
-
padding-bottom: 15px;
|
|
239
|
-
border-bottom: 1px solid var(--color-border-lighter);
|
|
240
|
-
|
|
241
|
-
&:last-child {
|
|
242
|
-
border-bottom: none;
|
|
243
|
-
padding-bottom: 0;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
&__action {
|
|
247
|
-
flex: 1;
|
|
248
|
-
font-size: 14px;
|
|
249
|
-
color: var(--color-text-regular);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
&__time {
|
|
253
|
-
font-size: 12px;
|
|
254
|
-
color: var(--color-text-placeholder);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
.system-info {
|
|
259
|
-
display: grid;
|
|
260
|
-
grid-template-columns: repeat(2, 1fr);
|
|
261
|
-
gap: 20px;
|
|
262
|
-
|
|
263
|
-
@media (max-width: 768px) {
|
|
264
|
-
grid-template-columns: 1fr;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
&__item {
|
|
268
|
-
display: flex;
|
|
269
|
-
align-items: center;
|
|
270
|
-
gap: 10px;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
&__label {
|
|
274
|
-
font-size: 14px;
|
|
275
|
-
color: var(--color-text-secondary);
|
|
276
|
-
min-width: 80px;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
&__value {
|
|
280
|
-
font-size: 14px;
|
|
281
|
-
color: var(--color-text-primary);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
</script>
|
|
3
|
+
|
|
4
|
+
<template>
|
|
5
|
+
<div class="dashboard">
|
|
6
|
+
<div class="dashboard__placeholder">
|
|
7
|
+
<h2>首页</h2>
|
|
8
|
+
<p>功能开发中...</p>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<style lang="scss" scoped>
|
|
14
|
+
.dashboard {
|
|
15
|
+
padding: 20px;
|
|
16
|
+
height: 100%;
|
|
17
|
+
|
|
18
|
+
&__placeholder {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
height: 100%;
|
|
24
|
+
color: var(--color-text-secondary);
|
|
25
|
+
|
|
26
|
+
h2 {
|
|
27
|
+
margin-bottom: 10px;
|
|
28
|
+
color: var(--color-text-primary);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
284
32
|
</style>
|