befly-admin 3.0.1
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 +4 -0
- package/.env.production +4 -0
- package/LICENSE +201 -0
- package/README.md +143 -0
- package/index.html +13 -0
- package/libs/auto-routes-template.js +104 -0
- package/libs/autoRouter.ts +67 -0
- package/libs/icons.ts +543 -0
- package/package.json +42 -0
- package/public/logo.svg +106 -0
- package/src/App.vue +17 -0
- package/src/api/auth.ts +60 -0
- package/src/components/Icon.vue +41 -0
- package/src/env.d.ts +9 -0
- package/src/layouts/0.vue +207 -0
- package/src/layouts/1.vue +22 -0
- package/src/layouts/2.vue +166 -0
- package/src/main.ts +19 -0
- package/src/plugins/http.ts +94 -0
- package/src/plugins/router.ts +47 -0
- package/src/plugins/store.ts +19 -0
- package/src/styles/index.scss +198 -0
- package/src/styles/mixins.scss +98 -0
- package/src/styles/variables.scss +75 -0
- package/src/types/env.d.ts +23 -0
- package/src/util.ts +28 -0
- package/src/views/403/403.vue +33 -0
- package/src/views/admin/components/edit.vue +147 -0
- package/src/views/admin/components/role.vue +135 -0
- package/src/views/admin/index.vue +169 -0
- package/src/views/dict/components/edit.vue +156 -0
- package/src/views/dict/index.vue +159 -0
- package/src/views/index/components/AddonList.vue +125 -0
- package/src/views/index/components/EnvironmentInfo.vue +97 -0
- package/src/views/index/components/OperationLogs.vue +112 -0
- package/src/views/index/components/PerformanceMetrics.vue +148 -0
- package/src/views/index/components/QuickActions.vue +27 -0
- package/src/views/index/components/ServiceStatus.vue +193 -0
- package/src/views/index/components/SystemNotifications.vue +136 -0
- package/src/views/index/components/SystemOverview.vue +188 -0
- package/src/views/index/components/SystemResources.vue +104 -0
- package/src/views/index/components/UserInfo.vue +136 -0
- package/src/views/index/index.vue +62 -0
- package/src/views/login/index_1.vue +694 -0
- package/src/views/menu/components/edit.vue +150 -0
- package/src/views/menu/index.vue +168 -0
- package/src/views/news/detail/detail_2.vue +26 -0
- package/src/views/news/detail/index.vue +26 -0
- package/src/views/news/news.vue +26 -0
- package/src/views/role/components/api.vue +280 -0
- package/src/views/role/components/edit.vue +129 -0
- package/src/views/role/components/menu.vue +143 -0
- package/src/views/role/index.vue +179 -0
- package/src/views/user/user.vue +320 -0
- package/temp/router.js +71 -0
- package/tsconfig.json +34 -0
- package/vite.config.ts +100 -0
package/src/api/auth.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// 登录表单类型
|
|
2
|
+
export interface LoginForm {
|
|
3
|
+
email?: string;
|
|
4
|
+
password?: string;
|
|
5
|
+
phone?: string;
|
|
6
|
+
code?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// 注册表单类型
|
|
10
|
+
export interface RegisterForm {
|
|
11
|
+
name: string;
|
|
12
|
+
email: string;
|
|
13
|
+
password: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 登录响应类型
|
|
17
|
+
export interface LoginResponse {
|
|
18
|
+
token: string;
|
|
19
|
+
userInfo?: {
|
|
20
|
+
id: number;
|
|
21
|
+
name: string;
|
|
22
|
+
email: string;
|
|
23
|
+
[key: string]: any;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 用户登录
|
|
29
|
+
*/
|
|
30
|
+
export const loginApi = (data: LoginForm) => {
|
|
31
|
+
return $Http<LoginResponse>('/addon/admin/login', data);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 用户注册
|
|
36
|
+
*/
|
|
37
|
+
export const registerApi = (data: RegisterForm) => {
|
|
38
|
+
return $Http('/addon/admin/register', data);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 发送短信验证码
|
|
43
|
+
*/
|
|
44
|
+
export const sendSmsCodeApi = (phone: string) => {
|
|
45
|
+
return $Http('/addon/admin/sendSmsCode', { phone });
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 获取用户信息
|
|
50
|
+
*/
|
|
51
|
+
export function getUserInfo() {
|
|
52
|
+
return $Http('/addon/admin/adminInfo');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 退出登录
|
|
57
|
+
*/
|
|
58
|
+
export const logoutApi = () => {
|
|
59
|
+
return $Http('/addon/admin/logout');
|
|
60
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component :is="iconComponent" :size="size" :color="color" :stroke-width="strokeWidth" style="margin-right: 8px; vertical-align: middle" v-bind="$attrs" />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup>
|
|
6
|
+
import * as LucideIcons from 'lucide-vue-next';
|
|
7
|
+
import { computed, markRaw } from 'vue';
|
|
8
|
+
|
|
9
|
+
const props = defineProps({
|
|
10
|
+
name: {
|
|
11
|
+
type: String,
|
|
12
|
+
required: true
|
|
13
|
+
},
|
|
14
|
+
size: {
|
|
15
|
+
type: Number,
|
|
16
|
+
default: 16
|
|
17
|
+
},
|
|
18
|
+
color: {
|
|
19
|
+
type: String,
|
|
20
|
+
default: 'currentColor'
|
|
21
|
+
},
|
|
22
|
+
strokeWidth: {
|
|
23
|
+
type: Number,
|
|
24
|
+
default: 2
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// 动态获取图标组件
|
|
29
|
+
const iconComponent = computed(() => {
|
|
30
|
+
const iconName = props.name;
|
|
31
|
+
const icon = LucideIcons[iconName];
|
|
32
|
+
|
|
33
|
+
if (!icon) {
|
|
34
|
+
console.warn(`Icon "${props.name}" not found in lucide-vue-next`);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 使用 markRaw 避免响应式包装
|
|
39
|
+
return markRaw(icon);
|
|
40
|
+
});
|
|
41
|
+
</script>
|
package/src/env.d.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="layout-0-wrapper">
|
|
3
|
+
<!-- 顶部导航栏 -->
|
|
4
|
+
<div class="layout-header">
|
|
5
|
+
<div class="logo">
|
|
6
|
+
<h2>野蜂飞舞后台管理</h2>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="header-right">
|
|
9
|
+
<tiny-dropdown title="管理员" trigger="click" border type="info" @item-click="$Method.handleUserMenu">
|
|
10
|
+
<template #dropdown>
|
|
11
|
+
<tiny-dropdown-menu>
|
|
12
|
+
<tiny-dropdown-item :item-data="{ value: 'clearCache' }">刷新缓存</tiny-dropdown-item>
|
|
13
|
+
<tiny-dropdown-item :item-data="{ value: 'logout' }" divided>退出登录</tiny-dropdown-item>
|
|
14
|
+
</tiny-dropdown-menu>
|
|
15
|
+
</template>
|
|
16
|
+
</tiny-dropdown>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<!-- 菜单栏 -->
|
|
21
|
+
<div class="layout-menu">
|
|
22
|
+
<tiny-tree-menu :data="$Data.userMenus" :props="{ label: 'name' }" node-key="id" :node-height="40" :show-filter="false" :default-expanded-keys="$Data.expandedKeys" :default-expanded-keys-highlight="$Data.currentNodeKey" style="height: 100%" only-check-children width-adapt @node-click="$Method.onMenuClick">
|
|
23
|
+
<template #default="{ data }">
|
|
24
|
+
<span class="menu-item">
|
|
25
|
+
<Icon :name="data.icon || 'Squircle'" :size="16" style="margin-right: 8px; vertical-align: middle" />
|
|
26
|
+
<span>{{ data.name }}</span>
|
|
27
|
+
</span>
|
|
28
|
+
</template>
|
|
29
|
+
</tiny-tree-menu>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- 内容区域 -->
|
|
33
|
+
<div class="layout-main">
|
|
34
|
+
<RouterView />
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup>
|
|
40
|
+
import { arrayToTree } from '../util';
|
|
41
|
+
|
|
42
|
+
const router = useRouter();
|
|
43
|
+
const route = useRoute();
|
|
44
|
+
|
|
45
|
+
// 响应式数据
|
|
46
|
+
const $Data = $ref({
|
|
47
|
+
userMenus: [],
|
|
48
|
+
expandedKeys: [],
|
|
49
|
+
currentNodeKey: 0
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// 方法
|
|
53
|
+
const $Method = {
|
|
54
|
+
// 获取用户菜单权限
|
|
55
|
+
async fetchUserMenus() {
|
|
56
|
+
try {
|
|
57
|
+
const { data } = await $Http('/addon/admin/menuAll');
|
|
58
|
+
// 将一维数组转换为树形结构(最多2级)
|
|
59
|
+
$Data.userMenus = arrayToTree(data);
|
|
60
|
+
$Method.setActiveMenu();
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('获取用户菜单失败:', error);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// 设置当前激活的菜单(2级菜单专用)
|
|
67
|
+
setActiveMenu() {
|
|
68
|
+
const currentPath = route.path;
|
|
69
|
+
|
|
70
|
+
// 遍历父级菜单
|
|
71
|
+
for (const parent of $Data.userMenus) {
|
|
72
|
+
// 检查父级菜单
|
|
73
|
+
if (parent.path === currentPath) {
|
|
74
|
+
$Data.currentNodeKey = parent.id;
|
|
75
|
+
$Data.expandedKeys = [parent.id];
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 检查子级菜单
|
|
80
|
+
if (parent.children?.length) {
|
|
81
|
+
for (const child of parent.children) {
|
|
82
|
+
if (child.path === currentPath) {
|
|
83
|
+
nextTick(() => {
|
|
84
|
+
$Data.currentNodeKey = child.id;
|
|
85
|
+
$Data.expandedKeys = [parent.id];
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
// 处理菜单点击
|
|
95
|
+
onMenuClick(data) {
|
|
96
|
+
if (data.path) {
|
|
97
|
+
router.push(data.path);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
// 处理用户菜单点击
|
|
102
|
+
handleUserMenu(data) {
|
|
103
|
+
const value = data.itemData?.value || data.value;
|
|
104
|
+
if (value === 'logout') {
|
|
105
|
+
localStorage.removeItem('token');
|
|
106
|
+
router.push('/login');
|
|
107
|
+
Modal.message({ message: '退出成功', status: 'success' });
|
|
108
|
+
} else if (value === 'clearCache') {
|
|
109
|
+
console.log('刷新缓存');
|
|
110
|
+
Modal.message({ message: '缓存刷新成功', status: 'success' });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
$Method.fetchUserMenus();
|
|
116
|
+
</script>
|
|
117
|
+
|
|
118
|
+
<style scoped lang="scss">
|
|
119
|
+
.layout-0-wrapper {
|
|
120
|
+
$menu-width: 240px;
|
|
121
|
+
$head-height: 64px;
|
|
122
|
+
$gap: 16px;
|
|
123
|
+
position: absolute;
|
|
124
|
+
top: 0;
|
|
125
|
+
left: 0;
|
|
126
|
+
height: 100vh;
|
|
127
|
+
width: 100vw;
|
|
128
|
+
background: #f5f7fa;
|
|
129
|
+
overflow: hidden;
|
|
130
|
+
|
|
131
|
+
.layout-header {
|
|
132
|
+
position: absolute;
|
|
133
|
+
top: $gap;
|
|
134
|
+
left: $gap;
|
|
135
|
+
right: $gap;
|
|
136
|
+
height: $head-height;
|
|
137
|
+
display: flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
justify-content: space-between;
|
|
140
|
+
padding: 0 15px;
|
|
141
|
+
background: #ffffff;
|
|
142
|
+
border-radius: 8px;
|
|
143
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
|
144
|
+
border: 1px solid #e8eaed;
|
|
145
|
+
z-index: 100;
|
|
146
|
+
|
|
147
|
+
.logo {
|
|
148
|
+
h2 {
|
|
149
|
+
margin: 0;
|
|
150
|
+
font-size: 22px;
|
|
151
|
+
font-weight: 700;
|
|
152
|
+
color: #1f2329;
|
|
153
|
+
letter-spacing: 0.5px;
|
|
154
|
+
background: linear-gradient(135deg, #0052d9 0%, #0084f4 100%);
|
|
155
|
+
-webkit-background-clip: text;
|
|
156
|
+
-webkit-text-fill-color: transparent;
|
|
157
|
+
background-clip: text;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.header-right {
|
|
162
|
+
display: flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
gap: 16px;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.layout-menu {
|
|
169
|
+
position: absolute;
|
|
170
|
+
top: calc($head-height + $gap * 2);
|
|
171
|
+
left: $gap;
|
|
172
|
+
bottom: $gap;
|
|
173
|
+
width: $menu-width;
|
|
174
|
+
background: #ffffff;
|
|
175
|
+
border-radius: 8px;
|
|
176
|
+
z-index: 99;
|
|
177
|
+
padding: 16px 12px;
|
|
178
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
|
179
|
+
border: 1px solid #e8eaed;
|
|
180
|
+
|
|
181
|
+
.tiny-tree-menu:before {
|
|
182
|
+
display: none;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.menu-item {
|
|
186
|
+
display: flex;
|
|
187
|
+
align-items: center;
|
|
188
|
+
width: 100%;
|
|
189
|
+
padding: 2px 0;
|
|
190
|
+
transition: all 0.2s ease;
|
|
191
|
+
|
|
192
|
+
&:hover {
|
|
193
|
+
color: #0052d9;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.layout-main {
|
|
199
|
+
position: absolute;
|
|
200
|
+
top: calc($head-height + $gap * 2);
|
|
201
|
+
left: calc($menu-width + $gap * 2);
|
|
202
|
+
right: $gap;
|
|
203
|
+
bottom: $gap;
|
|
204
|
+
background: #f5f7fa;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="layout-container">
|
|
3
|
+
<RouterView />
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup>
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
const route = useRoute();
|
|
10
|
+
|
|
11
|
+
// 响应式数据
|
|
12
|
+
const $Data = $ref({});
|
|
13
|
+
|
|
14
|
+
// 方法
|
|
15
|
+
const $Method = {};
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<style scoped lang="scss">
|
|
19
|
+
.layout-container {
|
|
20
|
+
height: 100vh;
|
|
21
|
+
}
|
|
22
|
+
</style>
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="layout-container">
|
|
3
|
+
<RouterView />
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup>
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
const route = useRoute();
|
|
10
|
+
|
|
11
|
+
// 响应式数据
|
|
12
|
+
const $Data = $ref({
|
|
13
|
+
collapsed: localStorage.getItem('sidebar-collapsed') === 'true',
|
|
14
|
+
expandedKeys: [],
|
|
15
|
+
menuItems: []
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// 方法
|
|
19
|
+
const $Method = {
|
|
20
|
+
// 切换折叠状态
|
|
21
|
+
toggleCollapse() {
|
|
22
|
+
$Data.collapsed = !$Data.collapsed;
|
|
23
|
+
localStorage.setItem('sidebar-collapsed', String($Data.collapsed));
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// 当前激活菜单
|
|
28
|
+
const activeMenu = computed(() => route.name);
|
|
29
|
+
const currentTitle = computed(() => route.meta.title || '');
|
|
30
|
+
|
|
31
|
+
// 图标映射(根据路由名称首段匹配)
|
|
32
|
+
const iconMap = {
|
|
33
|
+
index: DashboardIcon,
|
|
34
|
+
user: UserIcon,
|
|
35
|
+
news: FileIcon,
|
|
36
|
+
system: SettingIcon,
|
|
37
|
+
default: AppIcon
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// 从路由构建菜单结构
|
|
41
|
+
function buildMenuFromRoutes() {
|
|
42
|
+
const routes = router.getRoutes();
|
|
43
|
+
// 过滤掉布局路由和登录页
|
|
44
|
+
const pageRoutes = routes.filter((r) => r.name && r.name !== 'layout0' && r.name !== 'login' && !String(r.name).startsWith('layout'));
|
|
45
|
+
const menuTree = {};
|
|
46
|
+
for (const r of pageRoutes) {
|
|
47
|
+
const name = String(r.name);
|
|
48
|
+
const segments = name.split('-');
|
|
49
|
+
const firstSeg = segments[0];
|
|
50
|
+
// 一级菜单
|
|
51
|
+
if (!menuTree[firstSeg]) {
|
|
52
|
+
menuTree[firstSeg] = {
|
|
53
|
+
value: segments.length === 1 ? name : firstSeg,
|
|
54
|
+
label: firstSeg.charAt(0).toUpperCase() + firstSeg.slice(1),
|
|
55
|
+
icon: iconMap[firstSeg] || iconMap.default,
|
|
56
|
+
children: []
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// 多级路由加入子菜单
|
|
60
|
+
if (segments.length > 1) {
|
|
61
|
+
menuTree[firstSeg].children.push({
|
|
62
|
+
value: name,
|
|
63
|
+
label: segments.slice(1).join(' / ')
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
$Data.menuItems = Object.values(menuTree).sort((a, b) => {
|
|
68
|
+
if (a.value === 'index') return -1;
|
|
69
|
+
if (b.value === 'index') return 1;
|
|
70
|
+
return a.label.localeCompare(b.label);
|
|
71
|
+
});
|
|
72
|
+
// 默认展开所有包含子菜单的项
|
|
73
|
+
$Data.expandedKeys = $Data.menuItems.filter((m) => m.children && m.children.length).map((m) => m.value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 组件挂载时构建菜单
|
|
77
|
+
onMounted(() => {
|
|
78
|
+
buildMenuFromRoutes();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const userMenuOptions = [
|
|
82
|
+
{ content: '个人中心', value: 'profile' },
|
|
83
|
+
{ content: '退出登录', value: 'logout' }
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const handleMenuChange = (value) => {
|
|
87
|
+
router.push({ name: value });
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleUserMenu = (data) => {
|
|
91
|
+
if (data.value === 'logout') {
|
|
92
|
+
localStorage.removeItem('token');
|
|
93
|
+
router.push('/login');
|
|
94
|
+
MessagePlugin.success('退出成功');
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<style scoped lang="scss">
|
|
100
|
+
.layout-container {
|
|
101
|
+
height: 100vh;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.sidebar {
|
|
105
|
+
transition: width 0.3s ease;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.logo {
|
|
109
|
+
height: 64px;
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
justify-content: center;
|
|
113
|
+
border-bottom: 1px solid $border-color;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.logo h2 {
|
|
117
|
+
margin: 0;
|
|
118
|
+
font-size: 20px;
|
|
119
|
+
font-weight: 600;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.logo-short {
|
|
123
|
+
font-size: 24px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.header {
|
|
127
|
+
display: flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
justify-content: space-between;
|
|
130
|
+
padding: 0 24px;
|
|
131
|
+
background: $bg-color-container;
|
|
132
|
+
border-bottom: 1px solid $border-color;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.header-left {
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
gap: 16px;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.header-left h3 {
|
|
142
|
+
margin: 0;
|
|
143
|
+
font-size: 18px;
|
|
144
|
+
font-weight: 500;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.collapse-btn {
|
|
148
|
+
font-size: 20px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.header-right {
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
gap: 16px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.ml-2 {
|
|
158
|
+
margin-left: 8px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.content {
|
|
162
|
+
padding: 24px;
|
|
163
|
+
background: $bg-color-page;
|
|
164
|
+
overflow-y: auto;
|
|
165
|
+
}
|
|
166
|
+
</style>
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createApp } from 'vue';
|
|
2
|
+
import { createPinia } from 'pinia';
|
|
3
|
+
import App from './App.vue';
|
|
4
|
+
|
|
5
|
+
// 引入全局样式
|
|
6
|
+
import './styles/index.scss';
|
|
7
|
+
|
|
8
|
+
// 引入路由实例
|
|
9
|
+
import { router } from './plugins/router';
|
|
10
|
+
|
|
11
|
+
const app = createApp(App);
|
|
12
|
+
|
|
13
|
+
// 安装基础插件
|
|
14
|
+
app.use(createPinia());
|
|
15
|
+
|
|
16
|
+
// 使用路由
|
|
17
|
+
app.use(router);
|
|
18
|
+
|
|
19
|
+
app.mount('#app');
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios';
|
|
2
|
+
import { Modal } from '@opentiny/vue';
|
|
3
|
+
|
|
4
|
+
// API 响应格式
|
|
5
|
+
interface ApiResponse<T = any> {
|
|
6
|
+
code: 0 | 1;
|
|
7
|
+
msg: string;
|
|
8
|
+
data: T;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 创建 axios 实例
|
|
12
|
+
const request: AxiosInstance = axios.create({
|
|
13
|
+
baseURL: import.meta.env.VITE_API_BASE_URL,
|
|
14
|
+
timeout: 10000,
|
|
15
|
+
headers: {
|
|
16
|
+
'Content-Type': 'application/json'
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// 请求拦截器
|
|
21
|
+
request.interceptors.request.use(
|
|
22
|
+
(config) => {
|
|
23
|
+
// 添加 token
|
|
24
|
+
const token = localStorage.getItem('token');
|
|
25
|
+
if (token) {
|
|
26
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
27
|
+
// 开发环境下打印 token 信息
|
|
28
|
+
if (import.meta.env.DEV) {
|
|
29
|
+
console.log('[HTTP] 请求携带 token:', token.substring(0, 20) + '...');
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
if (import.meta.env.DEV) {
|
|
33
|
+
console.log('[HTTP] 请求未携带 token');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return config;
|
|
37
|
+
},
|
|
38
|
+
(error) => {
|
|
39
|
+
console.error('[Request] 请求错误:', error);
|
|
40
|
+
return Promise.reject(error);
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// 响应拦截器
|
|
45
|
+
request.interceptors.response.use(
|
|
46
|
+
(response: AxiosResponse<ApiResponse>) => {
|
|
47
|
+
const res = response.data;
|
|
48
|
+
|
|
49
|
+
// 如果code不是0,说明业务失败
|
|
50
|
+
if (res.code !== 0) {
|
|
51
|
+
Modal.message({
|
|
52
|
+
message: res.msg || '请求失败',
|
|
53
|
+
status: 'error'
|
|
54
|
+
});
|
|
55
|
+
return Promise.reject(res.data);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return res;
|
|
59
|
+
},
|
|
60
|
+
(error) => {
|
|
61
|
+
Modal.message({ message: '网络连接失败', status: 'error' });
|
|
62
|
+
return Promise.reject(error);
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 统一的 HTTP 请求方法(仅支持 GET 和 POST)
|
|
68
|
+
* @param url - 请求路径
|
|
69
|
+
* @param data - 请求数据,默认为空对象
|
|
70
|
+
* @param method - 请求方法,默认为 'post',可选 'get' | 'post'
|
|
71
|
+
* @param config - axios 请求配置
|
|
72
|
+
* @returns Promise<ApiResponse<T>>
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* // POST 请求(默认)
|
|
76
|
+
* await $Http('/addon/admin/login', { email, password });
|
|
77
|
+
*
|
|
78
|
+
* // GET 请求
|
|
79
|
+
* await $Http('/addon/admin/info', {}, 'get');
|
|
80
|
+
*/
|
|
81
|
+
export function $Http<T = any>(url: string, data: any = {}, method: 'get' | 'post' = 'post', config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
|
82
|
+
const methodLower = method.toLowerCase() as 'get' | 'post';
|
|
83
|
+
|
|
84
|
+
// GET 请求将 data 作为 params
|
|
85
|
+
if (methodLower === 'get') {
|
|
86
|
+
return request.get(url, {
|
|
87
|
+
...config,
|
|
88
|
+
params: data
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// POST 请求将 data 作为 body
|
|
93
|
+
return request.post(url, data, config);
|
|
94
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createRouter, createWebHashHistory } from 'vue-router';
|
|
2
|
+
import autoRoutes from 'virtual:auto-routes';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 创建并导出路由实例
|
|
6
|
+
* 可直接在 main.ts 中使用 app.use(router)
|
|
7
|
+
*/
|
|
8
|
+
export const router = createRouter({
|
|
9
|
+
history: createWebHashHistory(import.meta.env.BASE_URL),
|
|
10
|
+
routes: autoRoutes
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// 路由守卫 - 基础验证
|
|
14
|
+
router.beforeEach(async (to, from, next) => {
|
|
15
|
+
// 设置页面标题
|
|
16
|
+
const titlePrefix = 'Befly Admin';
|
|
17
|
+
if (to.meta?.title) {
|
|
18
|
+
document.title = `${titlePrefix} - ${to.meta.title}`;
|
|
19
|
+
} else {
|
|
20
|
+
document.title = titlePrefix;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const token = localStorage.getItem('token');
|
|
24
|
+
|
|
25
|
+
// 判断是否为公开路由:使用 layout0 的需要登录,其他 layout 为公开路由
|
|
26
|
+
const isProtectedRoute = to.matched.some((record) => record.name === 'layout0');
|
|
27
|
+
|
|
28
|
+
// 1. 未登录且访问受保护路由 → 跳转登录
|
|
29
|
+
if (!token && isProtectedRoute) {
|
|
30
|
+
return next('/login');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 2. 已登录访问登录页 → 跳转首页
|
|
34
|
+
if (token && to.path === '/login') {
|
|
35
|
+
return next('/');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
next();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// 路由就绪后处理
|
|
42
|
+
router.afterEach((to) => {
|
|
43
|
+
// 可以在这里添加页面访问统计等
|
|
44
|
+
if (import.meta.env.DEV) {
|
|
45
|
+
console.log(`[Router] 导航到: ${to.path}`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineStore } from 'pinia';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 全局状态管理
|
|
5
|
+
* 集中管理所有全局数据,避免分散到多个 store 文件
|
|
6
|
+
*/
|
|
7
|
+
export const useGlobal = defineStore('global', () => {
|
|
8
|
+
// ==================== 全局状态 ====================
|
|
9
|
+
|
|
10
|
+
// 可以在这里添加全局状态,例如:
|
|
11
|
+
// - 用户信息
|
|
12
|
+
// - 主题配置
|
|
13
|
+
// - 应用设置
|
|
14
|
+
// - 等等
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
// 暂时没有状态和方法
|
|
18
|
+
};
|
|
19
|
+
});
|