2.26提交
This commit is contained in:
parent
451f28151c
commit
29bce3569f
2
.env
2
.env
|
|
@ -1,5 +1,5 @@
|
|||
# 开发环境配置
|
||||
VITE_DEV_API_BASE_URL=http://localhost:3000/api
|
||||
VITE_DEV_API_BASE_URL=http://192.168.10.10:8085/api/
|
||||
VITE_DEV_DEBUG=true
|
||||
VITE_DEV_REQUEST_TIMEOUT=5000
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"axios": "^1.13.4",
|
||||
"dayjs": "^1.11.19",
|
||||
"element-plus": "^2.13.2",
|
||||
"js-md5": "^0.8.3",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.27",
|
||||
"vue-router": "^5.0.1"
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ importers:
|
|||
element-plus:
|
||||
specifier: ^2.13.2
|
||||
version: 2.13.2(vue@3.5.27(typescript@5.9.3))
|
||||
js-md5:
|
||||
specifier: ^0.8.3
|
||||
version: 0.8.3
|
||||
pinia:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3))
|
||||
|
|
@ -1526,6 +1529,9 @@ packages:
|
|||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||
hasBin: true
|
||||
|
||||
js-md5@0.8.3:
|
||||
resolution: {integrity: sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==}
|
||||
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
|
|
@ -3631,6 +3637,8 @@ snapshots:
|
|||
|
||||
jiti@2.6.1: {}
|
||||
|
||||
js-md5@0.8.3: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-tokens@9.0.1: {}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ service.interceptors.request.use(
|
|||
|
||||
// 添加令牌到请求头
|
||||
if (token.value) {
|
||||
config.headers.Authorization = `Bearer ${token.value}`
|
||||
config.headers['token'] = token.value
|
||||
}
|
||||
|
||||
// 开发环境下打印请求信息
|
||||
|
|
@ -66,9 +66,9 @@ service.interceptors.response.use(
|
|||
// 提取响应数据
|
||||
const { data, headers } = response;
|
||||
// 检查响应状态码
|
||||
if (data.code !== 200) {
|
||||
console.error('业务错误:', data.message)
|
||||
return Promise.reject(new Error(data.message))
|
||||
if (data.code !== 200 && data.code !== 0) {
|
||||
console.error('业务错误:', data.msg)
|
||||
return Promise.reject(new Error(data.msg))
|
||||
}
|
||||
if (headers['new_token'] !== undefined) {
|
||||
useLocalStorage.set('token', headers['new_token'])
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
import { request } from './index';
|
||||
import type { ApiMenuItem } from '@/components/Aside/type';
|
||||
import { useLocalStorage } from '@/utils/storage';
|
||||
export const getMenuList = async (): Promise<any> => {
|
||||
if (!useLocalStorage.get('token').value) {
|
||||
console.log('token不存在');
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
// 使用API服务进行获取菜单列表请求
|
||||
const response = await request.get<ApiMenuItem[]>('menu/list');
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
// 统一错误处理
|
||||
const errMsg = error instanceof Error ? error.message : '获取菜单列表失败';
|
||||
ElNotification({
|
||||
type: 'error',
|
||||
message: errMsg,
|
||||
duration: 2000
|
||||
});
|
||||
throw new Error(errMsg); // 抛出错误让组件层捕获
|
||||
}
|
||||
};
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
<template>
|
||||
<el-aside class="aside-container">
|
||||
<!-- 侧边菜单(Element Plus默认深色主题) -->
|
||||
<el-menu class="el-menu-container" unique-opened :default-active="activeIndex" router :collapse="isCollapse">
|
||||
<!-- 软件名头部 -->
|
||||
<div class="logo" :class="isCollapse ? 'logo-collapse' : ''">
|
||||
|
|
@ -10,17 +9,17 @@
|
|||
<!-- 遍历asideList渲染菜单 -->
|
||||
<template v-for="menu in asideList" :key="menu.id">
|
||||
<!-- 1. 渲染折叠子菜单(el-sub-menu) -->
|
||||
<el-sub-menu v-if="menu.isSubMenu" :index="menu.path">
|
||||
<el-sub-menu v-if="menu.children && menu.children.length > 0" :index="menu.path">
|
||||
<template #title>
|
||||
<!-- 渲染菜单图标(如果有) -->
|
||||
<el-icon v-if="menu.icon">
|
||||
<component :is="menu.icon" />
|
||||
</el-icon>
|
||||
<span>{{ menu.label }}</span>
|
||||
<span>{{ menu.name }}</span>
|
||||
</template>
|
||||
<!-- 渲染子菜单列表 -->
|
||||
<el-menu-item v-for="child in menu.children" :key="child.id" :index="child.path">
|
||||
{{ child.label }}
|
||||
{{ child.name }}
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
|
|
@ -29,56 +28,75 @@
|
|||
<el-icon v-if="menu.icon">
|
||||
<component :is="menu.icon" />
|
||||
</el-icon>
|
||||
<span>{{ menu.label }}</span>
|
||||
<span>{{ menu.name }}</span>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { HomeFilled, Menu, Histogram, UserFilled, Promotion, HelpFilled, BellFilled } from '@element-plus/icons-vue'
|
||||
import type { MenuItem } from '@/components/Aside/type'
|
||||
import { getMenuList } from '@/api/menu'
|
||||
const route = useRoute()
|
||||
// element 图标导入
|
||||
import { HomeFilled, Menu, Histogram, UserFilled, Promotion, HelpFilled, BellFilled, Management, List } from '@element-plus/icons-vue'
|
||||
//ts 类型导入
|
||||
import type { RawMenuItem, MenuItem, IconMap } from '@/type/menu'
|
||||
// pinia 导入 获取用户菜单
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const activeIndex = ref(route.path) // 默认选中项
|
||||
let asideList = ref<MenuItem[]>([]) // 处理后的菜单列表
|
||||
defineProps({
|
||||
isCollapse: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
// 1. 定义path到图标名称的映射表(核心)
|
||||
const pathToIconMap = {
|
||||
"/": markRaw(HomeFilled), // 首页
|
||||
"/fund": markRaw(Menu), // 资金管理
|
||||
"/trade": markRaw(Histogram), // 交易统计
|
||||
"/account": markRaw(UserFilled), // 账户管理
|
||||
"/customized": markRaw(Promotion), // 个性化交易
|
||||
"/agent": markRaw(HelpFilled), // 代理管理
|
||||
"/notice": markRaw(BellFilled) // 通知管理
|
||||
|
||||
const route = useRoute()
|
||||
const activeIndex = ref(route.path)
|
||||
let asideList = ref<MenuItem[]>([]) // 现在使用适配后的 MenuItem 类型
|
||||
|
||||
// 1. 图标映射表(使用 IconMap 类型约束)
|
||||
const pathToIconMap: IconMap = {
|
||||
"/": markRaw(HomeFilled),
|
||||
"/fund": markRaw(Menu),
|
||||
"/trade": markRaw(Histogram),
|
||||
"/account": markRaw(UserFilled),
|
||||
"/customized": markRaw(Promotion),
|
||||
"/agent": markRaw(HelpFilled),
|
||||
"/notice": markRaw(BellFilled),
|
||||
"/permission": markRaw(Management),
|
||||
"/template": markRaw(List)
|
||||
}
|
||||
// 2. 为菜单数据补充图标字段的函数
|
||||
const addIconToMenu = (menuList: MenuItem[]) => {
|
||||
return menuList.map(menu => {
|
||||
// 为当前菜单项补充图标(根据path匹配)
|
||||
return {
|
||||
...menu,
|
||||
icon: pathToIconMap[menu.path as keyof typeof pathToIconMap] || "", // 匹配不到则为空
|
||||
// 如果需要给子菜单也加图标,可以在这里处理children
|
||||
children: menu.children ? menu.children.map(child => ({
|
||||
...child,
|
||||
// 子菜单图标可按需添加,示例:
|
||||
// icon: pathToIconMap[child.path] || ""
|
||||
})) : undefined
|
||||
|
||||
// 2. 递归转换原始菜单数据为适配 Element Plus 的格式
|
||||
const transformRawMenu = (rawMenu: RawMenuItem): MenuItem => {
|
||||
const isSubMenu = !!rawMenu.son && rawMenu.son.length > 0
|
||||
// 基础转换
|
||||
const menuItem: MenuItem = {
|
||||
id: rawMenu.id,
|
||||
name: rawMenu.name,
|
||||
path: rawMenu.path,
|
||||
index: rawMenu.path, // index 直接使用 path
|
||||
icon: pathToIconMap[rawMenu.path] || undefined,
|
||||
isSubMenu,
|
||||
raw: rawMenu // 保留原始数据
|
||||
}
|
||||
})
|
||||
// 递归处理子菜单
|
||||
if (isSubMenu) {
|
||||
menuItem.children = rawMenu.son!.map(child => transformRawMenu(child))
|
||||
}
|
||||
|
||||
return menuItem
|
||||
}
|
||||
|
||||
// 3. 批量转换菜单列表
|
||||
const transformMenuList = (rawList: RawMenuItem[]): MenuItem[] => {
|
||||
return rawList.map(menu => transformRawMenu(menu))
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 获取原始数据(icon字段为空字符串)
|
||||
const rawMenuList = await getMenuList()
|
||||
// 补充图标字段
|
||||
asideList.value = addIconToMenu(rawMenuList)
|
||||
// 获取原始数据(类型为 RawMenuItem[])
|
||||
// const rawMenuList = userStore.menus
|
||||
// 转换为适配 Element Plus 的格式
|
||||
// asideList.value = transformMenuList(rawMenuList)
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
// 后端返回的原始菜单类型(与后端约定)
|
||||
export interface ApiMenuItem {
|
||||
id: number; // 菜单ID,用于唯一标识
|
||||
path: string; // 路由path,如 "/fund/deposit"
|
||||
label: string; // 菜单名称
|
||||
isSubMenu: boolean; // 是否是子菜单容器
|
||||
icon?: string; // 图标名称字符串,如 "Menu" "HomeFilled"
|
||||
children?: ApiMenuItem[]; // 子菜单
|
||||
hidden?: boolean; // 是否隐藏(可选)
|
||||
}
|
||||
|
||||
// 前端处理后的菜单类型(增加图标组件)
|
||||
export interface MenuItem extends Omit<ApiMenuItem, 'icon'> {
|
||||
icon?: any; // Element Plus图标组件
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
<template>
|
||||
<section class="sticky">
|
||||
<el-header class="header">
|
||||
<div class="header-content">
|
||||
<div class="logo">
|
||||
|
|
@ -7,34 +8,62 @@
|
|||
<Expand />
|
||||
</el-icon>
|
||||
</div>
|
||||
<!-- <div class="logout" @click="handleLogout">
|
||||
退出登录
|
||||
</div> -->
|
||||
|
||||
<div class="profile">
|
||||
<!-- Tooltip 包裹整个 Dropdown,提示框指向头像 -->
|
||||
<el-dropdown @command="handleDropdownCommand" class="lang-dropdown">
|
||||
<span class="el-dropdown-lang">
|
||||
{{ langText }}
|
||||
<el-icon class="el-icon--right">
|
||||
<arrow-down />
|
||||
</el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="item in lang" :key="item.id" :command="item.text">{{ item.text
|
||||
}}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-tooltip :content="tooltip" placement="left-start">
|
||||
<el-dropdown placement="bottom-start">
|
||||
<el-icon size="24" class="avatar-icon">
|
||||
<el-icon size="24" class="avatar-icon" style="cursor: pointer;">
|
||||
<Avatar />
|
||||
</el-icon>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="handleChangePassword">修改密码</el-dropdown-item>
|
||||
<!-- <el-dropdown-item @click="handleChangePassword">修改密码</el-dropdown-item> -->
|
||||
<el-dropdown-item @click="handleLogout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
</el-header>
|
||||
</section>
|
||||
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ArrowDown } from '@element-plus/icons-vue'
|
||||
import { Expand, Avatar } from '@element-plus/icons-vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
import { useLocalStorage } from '@/utils/storage'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const tooltip = ref(`当前登录用户:${useLocalStorage.get('user').value?.username || ''}`)
|
||||
const lang = ref([
|
||||
{
|
||||
id: 'zh-CN',
|
||||
text: '中文'
|
||||
},
|
||||
{
|
||||
id: 'en-US',
|
||||
text: '英文'
|
||||
}
|
||||
])
|
||||
const langText = ref('中文')
|
||||
|
||||
// 接收折叠状态
|
||||
const props = defineProps<{
|
||||
|
|
@ -55,9 +84,14 @@ const handleLogout = () => {
|
|||
const handleChangePassword = () => {
|
||||
router.push('/user/change-password')
|
||||
}
|
||||
const handleDropdownCommand = (command: string) => {
|
||||
langText.value = command
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.header {
|
||||
height: 50px;
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -65,6 +99,9 @@ const handleChangePassword = () => {
|
|||
height: 100%;
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.el-icon--right {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
@ -75,6 +112,28 @@ const handleChangePassword = () => {
|
|||
}
|
||||
|
||||
.profile {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.lang-dropdown {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.el-dropdown-lang {
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-icon {
|
||||
cursor: pointer;
|
||||
|
||||
|
|
@ -88,5 +147,11 @@ const handleChangePassword = () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
height: 35px;
|
||||
border-top: 1px solid var(--el-menu-border-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<template>
|
||||
<div class="audit-container">
|
||||
<h1>资金审核</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
存款
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<template>
|
||||
<div class="detail-container">
|
||||
<h1>资金详情</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<template>
|
||||
<div class="recharge-container">
|
||||
<h1>资金充值</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<template>
|
||||
<div class="transfer-container">
|
||||
<h1>资金转账</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
取款
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<template>
|
||||
<div class="withdrawal-container">
|
||||
<h1>资金提现</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
首页
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -2,19 +2,12 @@
|
|||
<el-main>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="route-fade" mode="out-in" appear>
|
||||
<keep-alive :include="cachedViews">
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</el-main>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useCacheStore } from '@/stores/cache';
|
||||
// 绑定动态缓存列表
|
||||
const cacheStore = useCacheStore()
|
||||
const cachedViews = cacheStore.cachedViews
|
||||
</script>
|
||||
<script setup lang="ts"></script>
|
||||
<style scoped lang="scss">
|
||||
/* 过渡动画的 CSS 类(前缀和 Transition 的 name 一致) */
|
||||
/* 进入动画开始 */
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ import './styles/global.scss'
|
|||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
import { useUserStore } from './stores/user'
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
// ✅ 页面加载时优先执行initUser,恢复菜单
|
||||
const userStore = useUserStore();
|
||||
userStore.initUser();
|
||||
app.mount('#app')
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useLocalStorage, useSessionStorage } from '@/utils/storage'
|
||||
import { useCacheStore } from '@/stores/cache';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
|
|
@ -10,100 +9,19 @@ const routes = [
|
|||
component: () => import('@/views/Layout/index.vue'),
|
||||
meta: {
|
||||
title: '首页',
|
||||
// 显式配置为 none,表示无动画
|
||||
transition: 'none'
|
||||
},
|
||||
children: [
|
||||
// 修改密码
|
||||
{
|
||||
path: '/user/change-password',
|
||||
name: 'ChangePassword',
|
||||
component: () => import('@/views/User/ChangePassword.vue'),
|
||||
meta: {
|
||||
title: '修改密码',
|
||||
transition: 'fade'
|
||||
}
|
||||
},
|
||||
// 首页
|
||||
{
|
||||
path: '',
|
||||
name: 'Home',
|
||||
component: () => import('@/components/main/Home/index.vue'),
|
||||
meta: {
|
||||
title: '首页',
|
||||
transition: 'fade'
|
||||
}
|
||||
},
|
||||
// 存款
|
||||
{
|
||||
path: '/fund/deposit',
|
||||
name: 'Deposit',
|
||||
component: () => import('@/components/main/Fund/Deposit.vue'),
|
||||
meta: {
|
||||
title: '存款',
|
||||
transition: 'fade'
|
||||
}
|
||||
},
|
||||
// 取款
|
||||
{
|
||||
path: '/fund/withdraw',
|
||||
name: 'Withdraw',
|
||||
component: () => import('@/components/main/Fund/Withdraw.vue'),
|
||||
meta: {
|
||||
title: '取款',
|
||||
transition: 'fade'
|
||||
}
|
||||
},
|
||||
// 转账
|
||||
{
|
||||
path: '/fund/transfer',
|
||||
name: 'Transfer',
|
||||
component: () => import('@/components/main/Fund/Transfer.vue'),
|
||||
meta: {
|
||||
title: '转账',
|
||||
transition: 'fade'
|
||||
}
|
||||
},
|
||||
// 资金明细
|
||||
{
|
||||
path: '/fund/detail',
|
||||
name: 'Detail',
|
||||
component: () => import('@/components/main/Fund/Detail.vue'),
|
||||
meta: {
|
||||
title: '资金明细',
|
||||
transition: 'fade'
|
||||
}
|
||||
},
|
||||
// 审核记录
|
||||
{
|
||||
path: '/fund/audit',
|
||||
name: 'Audit',
|
||||
component: () => import('@/components/main/Fund/Audit.vue'),
|
||||
meta: {
|
||||
title: '审核记录',
|
||||
transition: 'fade'
|
||||
}
|
||||
},
|
||||
//提现记录
|
||||
{
|
||||
path: '/fund/withdrawal',
|
||||
name: 'Withdrawal',
|
||||
component: () => import('@/components/main/Fund/Withdrawal.vue'),
|
||||
meta: {
|
||||
title: '提现记录',
|
||||
transition: 'fade'
|
||||
}
|
||||
},
|
||||
// 充值记录
|
||||
{
|
||||
path: '/fund/recharge',
|
||||
name: 'Recharge',
|
||||
component: () => import('@/components/main/Fund/Recharge.vue'),
|
||||
meta: {
|
||||
title: '充值记录',
|
||||
transition: 'fade'
|
||||
}
|
||||
},
|
||||
// {
|
||||
// path: '/user/change-password',
|
||||
// name: 'ChangePassword',
|
||||
// component: () => import('@/views/User/ChangePassword.vue'),
|
||||
// meta: {
|
||||
// title: '修改密码',
|
||||
// transition: 'fade'
|
||||
// }
|
||||
// },
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -115,15 +33,26 @@ const routes = [
|
|||
transition: 'none'
|
||||
}
|
||||
},
|
||||
// {
|
||||
// path: '/reset-password',
|
||||
// name: 'ResetPassword',
|
||||
// component: () => import('@/views/User/ResetPassword.vue'),
|
||||
// meta: {
|
||||
// title: '重置密码',
|
||||
// transition: 'none'
|
||||
// }
|
||||
// },
|
||||
// 新增:404 页面(必须最后添加)
|
||||
{
|
||||
path: '/reset-password',
|
||||
name: 'ResetPassword',
|
||||
component: () => import('@/views/User/ResetPassword.vue'),
|
||||
meta: {
|
||||
title: '重置密码',
|
||||
transition: 'none'
|
||||
}
|
||||
path: '/404',
|
||||
name: '404',
|
||||
component: () => import('@/views/404/index.vue'),
|
||||
meta: { title: '页面不存在', transition: 'none' }
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: '/404',
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
|
|
@ -144,30 +73,22 @@ router.beforeEach((to, from, next) => {
|
|||
// 2. 获取 token(使用封装的 useLocalStorage.get,添加空值保护)
|
||||
const token = useLocalStorage.get<string>('token');
|
||||
|
||||
// 3. 修改密码缓存逻辑(提前执行,不影响 next 调用)
|
||||
const cacheStore = useCacheStore()
|
||||
// 进入修改密码页 → 缓存上一个业务页(仅在鉴权放行前记录)
|
||||
if (to.name === 'ChangePassword' && from.name && from.name !== 'Login') {
|
||||
cacheStore.addCache(String(from.name)) // 缓存原操作页
|
||||
useSessionStorage.set('backPath', from.fullPath) // 记录原页面完整地址
|
||||
}
|
||||
|
||||
// 4. 路由鉴权核心逻辑(唯一的 next 调用分支)
|
||||
if (token?.value) { // 空值保护:token 存在且有值
|
||||
// 4. 路由鉴权
|
||||
if (token?.value) {
|
||||
// 已登录:如果访问白名单页面,重定向到首页
|
||||
if (whiteList.includes(to.path)) {
|
||||
next('/'); // 唯一调用:重定向首页
|
||||
} else {
|
||||
// 已登录且访问非白名单页面,正常放行
|
||||
next(); // 唯一调用:正常放行
|
||||
next();
|
||||
}
|
||||
} else {
|
||||
// 未登录:如果访问白名单页面,正常放行
|
||||
if (whiteList.includes(to.path)) {
|
||||
next(); // 唯一调用:正常放行
|
||||
next();
|
||||
} else {
|
||||
// 未登录且访问非白名单页面,重定向到登录页
|
||||
next('/login'); // 唯一调用:重定向登录
|
||||
next('/login');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
//缓存组件,需要缓存的组件需要添加到cachedViews中
|
||||
export const useCacheStore = defineStore('cache', () => {
|
||||
// 动态缓存列表(绑定到keep-alive的include)
|
||||
const cachedViews = ref<string[]>([])
|
||||
|
||||
// 添加缓存(去重)
|
||||
const addCache = (name: string) => {
|
||||
if (name && !cachedViews.value.includes(name)) {
|
||||
cachedViews.value.push(name)
|
||||
}
|
||||
}
|
||||
|
||||
return { cachedViews, addCache }
|
||||
})
|
||||
|
|
@ -21,7 +21,7 @@ export const useUserStore = defineStore('user', () => {
|
|||
const token = ref<string>('');
|
||||
/** 是否加载中(登录/登出请求时) */
|
||||
const loading = ref<boolean>(false);
|
||||
// ========== 核心方法 ==========
|
||||
// ========== 方法 ==========
|
||||
/**
|
||||
* 用户登录
|
||||
* @param form 登录表单数据
|
||||
|
|
@ -31,7 +31,7 @@ export const useUserStore = defineStore('user', () => {
|
|||
try {
|
||||
loading.value = true;
|
||||
// 使用API服务进行登录请求
|
||||
const response = await request.post<LoginResponse>('user/login', form);
|
||||
const response = await request.post<LoginResponse>('/login', form);
|
||||
|
||||
// 从响应中获取数据
|
||||
const res = response.data;
|
||||
|
|
@ -43,7 +43,6 @@ export const useUserStore = defineStore('user', () => {
|
|||
// 更新响应式状态
|
||||
userInfo.value = res.data.user;
|
||||
token.value = res.data.user.token ?? '';
|
||||
|
||||
// 持久化到本地存储(防止页面刷新丢失)
|
||||
useLocalStorage.set('user', res.data.user);
|
||||
useLocalStorage.set('token', res.data.user.token);
|
||||
|
|
@ -64,7 +63,23 @@ export const useUserStore = defineStore('user', () => {
|
|||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
const logout = (): void => {
|
||||
const logout = async (): Promise<void> => {
|
||||
try {
|
||||
loading.value = true;
|
||||
// 使用API服务进行登出请求
|
||||
await request.post<LoginResponse>('/logout');
|
||||
} catch (error) {
|
||||
// 统一错误处理
|
||||
const errMsg = error instanceof Error ? error.message : '登出异常';
|
||||
ElNotification({
|
||||
type: 'error',
|
||||
message: errMsg,
|
||||
duration: 2000
|
||||
})
|
||||
throw new Error(errMsg); // 抛出错误让组件层捕获
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
// 清空响应式状态
|
||||
userInfo.value = {
|
||||
id: '',
|
||||
|
|
@ -79,6 +94,7 @@ export const useUserStore = defineStore('user', () => {
|
|||
useLocalStorage.remove('user');
|
||||
useLocalStorage.remove('token');
|
||||
useSessionStorage.remove('backPath');
|
||||
useLocalStorage.remove('menus');
|
||||
// 可选:登出后跳转到登录页(如果需要)
|
||||
// 使用 Vue Router 进行跳转
|
||||
// 显示登出成功通知
|
||||
|
|
@ -87,6 +103,7 @@ export const useUserStore = defineStore('user', () => {
|
|||
message: '退出登录',
|
||||
duration: 2000
|
||||
});
|
||||
// 跳转到登录页
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
|
|
@ -97,7 +114,7 @@ export const useUserStore = defineStore('user', () => {
|
|||
try {
|
||||
const savedUser = useLocalStorage.get('user');
|
||||
const savedToken = useLocalStorage.get('token');
|
||||
|
||||
const savedMenus = useLocalStorage.get('menus');
|
||||
if (savedUser.value && savedToken.value) {
|
||||
// 类型断言 + 验证,确保数据格式正确
|
||||
const parsedUser = savedUser.value as UserInfo;
|
||||
|
|
@ -187,6 +204,7 @@ export const useUserStore = defineStore('user', () => {
|
|||
});
|
||||
|
||||
// 修改成功后跳转到登录页
|
||||
useLocalStorage.remove('token');
|
||||
router.push('/login');
|
||||
} catch (error) {
|
||||
// 统一错误处理
|
||||
|
|
@ -203,11 +221,10 @@ export const useUserStore = defineStore('user', () => {
|
|||
};
|
||||
// ========== 暴露状态和方法 ==========
|
||||
return {
|
||||
// 状态
|
||||
// 状态和数据
|
||||
userInfo,
|
||||
token,
|
||||
loading,
|
||||
// 数据
|
||||
// 方法
|
||||
login,
|
||||
logout,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
* @template T - data 字段的类型,默认值为 any
|
||||
*/
|
||||
export interface ApiResponse<T = any> {
|
||||
msg: string;
|
||||
success: boolean;
|
||||
code: number;
|
||||
message: string;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
// 1. 后端返回的原始菜单数据类型(严格匹配接口返回)
|
||||
export interface RawMenuItem {
|
||||
id: number;
|
||||
name: string; // 菜单显示名称
|
||||
api: string; // 接口路径
|
||||
path: string; // 路由路径
|
||||
icon: string; // 后端返回的图标字段(为空字符串)
|
||||
parent_id: number; // 父菜单ID
|
||||
sort: number; // 排序序号
|
||||
type: number; // 菜单类型(1表示菜单)
|
||||
status: number; // 状态(1表示启用)
|
||||
son?: RawMenuItem[]; // 子菜单列表
|
||||
}
|
||||
|
||||
// 2. 前端适配 Element Plus 的菜单类型(增强版)
|
||||
export interface MenuItem {
|
||||
// 基础属性
|
||||
id: number;
|
||||
name: string;
|
||||
path: string;
|
||||
index: string; // 对应 el-menu-item / el-sub-menu 的 index 属性
|
||||
icon?: Component; // 图标组件或图标名称
|
||||
isSubMenu: boolean; // 是否是子菜单容器
|
||||
children?: MenuItem[]; // 子菜单列表(统一命名为children,更符合Vue习惯)
|
||||
// 原始数据保留(便于后续使用)
|
||||
raw?: RawMenuItem;
|
||||
}
|
||||
|
||||
// 4. 图标映射类型(约束图标映射表的类型)
|
||||
export type IconMap = Record<string, Component>;
|
||||
|
|
@ -20,8 +20,9 @@ export interface UserInfo {
|
|||
* 登录表单类型
|
||||
*/
|
||||
export interface LoginForm {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
login_type: number;
|
||||
remember?: boolean; // 可选:记住我
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
import md5 from 'js-md5';
|
||||
|
||||
// 你的加密函数(原样保留)
|
||||
export const encryptPassword = (password: string, salt = ''): string => {
|
||||
const encryptStr = salt ? password + salt : password;
|
||||
return md5(encryptStr);
|
||||
};
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<div class="not-found">
|
||||
<h1>404 - 页面不存在</h1>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.not-found {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -15,7 +15,7 @@ import Header from '@/components/Header/index.vue'
|
|||
import Main from '@/components/Main/index.vue'
|
||||
|
||||
import { useLocalStorage } from '@/utils/storage'
|
||||
// 控制 el-menu 原生折叠的核心状态
|
||||
// 控制 el-menu 原生折叠的状态
|
||||
const isCollapse = ref(useLocalStorage.get('isCollapse') ?? false)
|
||||
|
||||
// 切换折叠状态的方法
|
||||
|
|
@ -28,10 +28,5 @@ const toggleSidebar = () => {
|
|||
<style scoped lang="scss">
|
||||
.main-container {
|
||||
flex-direction: column;
|
||||
|
||||
.header {
|
||||
height: 50px;
|
||||
border-bottom: 1px solid var(--el-menu-border-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -24,10 +24,9 @@
|
|||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { FormRules, FormProps, FormInstance } from 'element-plus'
|
||||
import { useLocalStorage } from '@/utils/storage'
|
||||
import { useLocalStorage, useSessionStorage } from '@/utils/storage'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const router = useRouter()
|
||||
const labelPosition = ref<FormProps['labelPosition']>('left')
|
||||
const changePasswordFormRef = ref<FormInstance>()
|
||||
const error = ref('')
|
||||
|
|
@ -40,8 +39,8 @@ const changePasswordForm = ref({
|
|||
})
|
||||
|
||||
const formRules = reactive<FormRules>({
|
||||
currentPassword: [{ required: true, message: '请输入旧密码', trigger: 'blur' }],
|
||||
newPassword: [{ required: true, message: '请输入新密码', trigger: 'blur' }],
|
||||
currentPassword: [{ required: true, message: '请输入旧密码', trigger: 'blur' }, { min: 6, message: '旧密码不得少于6位', trigger: 'blur' }],
|
||||
newPassword: [{ required: true, message: '请输入新密码', trigger: 'blur' }, { min: 6, message: '新密码不得少于6位', trigger: 'blur' }],
|
||||
confirmPassword: [{ required: true, message: '请确认新密码', trigger: 'blur' }, {
|
||||
validator: (rule, value, callback) => {
|
||||
if (value !== changePasswordForm.value.newPassword) {
|
||||
|
|
@ -59,11 +58,8 @@ const handleChangePassword = async () => {
|
|||
if (valid) {
|
||||
try {
|
||||
error.value = ''
|
||||
useSessionStorage.set('changePassword', true);
|
||||
await useUserStore().changePassword(changePasswordForm.value)
|
||||
|
||||
// 密码修改成功后跳转到登录页
|
||||
useLocalStorage.remove('token');
|
||||
router.push('/login')
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : '密码修改失败'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<el-form :model="LoginForm" :rules="formRules" ref="loginFormRef" class="login-form" size="default">
|
||||
<!-- 用户名输入 -->
|
||||
<el-form-item label="用户名" prop="username" :label-position="labelPosition" label-width="65px">
|
||||
<el-input v-model="LoginForm.username" placeholder="请输入用户名" :prefix-icon="User" />
|
||||
<el-input v-model="LoginForm.email" placeholder="请输入用户名" :prefix-icon="User" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 密码输入 -->
|
||||
|
|
@ -47,6 +47,10 @@ import type { LoginForm as LoginFormType } from '@/type/user'
|
|||
// 业务导入
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useLocalStorage, useSessionStorage } from '@/utils/storage'
|
||||
|
||||
// 加密导入
|
||||
import { encryptPassword } from '@/utils/encrypt'
|
||||
|
||||
const router = useRouter()
|
||||
const error = ref('')
|
||||
const userStore = useUserStore()
|
||||
|
|
@ -55,13 +59,13 @@ const loginFormRef = ref<FormInstance>()
|
|||
const labelPosition = ref<FormProps['labelPosition']>('left')
|
||||
// 表单数据
|
||||
const LoginForm = reactive<LoginFormType>({
|
||||
username: 'admin',
|
||||
password: 'admin',
|
||||
remember: false
|
||||
email: '1241208486@qq.com',
|
||||
password: encryptPassword('888888'),
|
||||
login_type: 1,
|
||||
})
|
||||
// 表单校验规则(Element Plus 标准校验)
|
||||
const formRules = reactive<FormRules>({
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
email: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
|
|
@ -75,24 +79,18 @@ const handleLogin = async () => {
|
|||
await userStore.login(LoginForm)
|
||||
saveRememberedUsername()
|
||||
// 如果是登录或者退出登录重新登录,都是跳转首页,如果是从修改密码操作返回登录页,就跳转回修改密码操作前的页面
|
||||
// 1. 读取路由历史状态(加可选链,避免报错)
|
||||
const historyState = router.options.history.state;
|
||||
// 2. 读取回退路径(无 .value,加兜底为 '/')
|
||||
const isChangePassword = useSessionStorage.get('changePassword');
|
||||
const backPath = useSessionStorage.get('backPath') || '/';
|
||||
// 3. 清除 sessionStorage 记录(及时清理,避免残留)
|
||||
|
||||
// 2. 先清理 sessionStorage(避免残留,执行顺序关键)
|
||||
useSessionStorage.remove('changePassword');
|
||||
useSessionStorage.remove('backPath');
|
||||
|
||||
// 4. 严谨判断:是否从修改密码页跳登录
|
||||
const isFromChangePwd = historyState?.back === '/user/change-password'; // 可选链避免报错
|
||||
// 3. 重置菜单折叠状态(提前执行,避免跳转中断)
|
||||
useLocalStorage.set('isCollapse', false);
|
||||
const targetPath = isChangePassword ? backPath : '/';
|
||||
|
||||
// 5. 规范跳转(异步 + 兜底校验)
|
||||
const targetPath = isFromChangePwd ? backPath : '/';
|
||||
// 校验目标路径有效性,避免无效跳转
|
||||
if (typeof targetPath === 'string' && targetPath.startsWith('/')) {
|
||||
await router.push(targetPath); // 异步跳转,符合 Vue Router 规范
|
||||
} else {
|
||||
await router.push('/'); // 兜底跳首页
|
||||
}
|
||||
router.push(typeof targetPath === 'string' ? targetPath : targetPath.value || '/');
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : '登录失败'
|
||||
}
|
||||
|
|
@ -104,7 +102,7 @@ const handleLogin = async () => {
|
|||
// 记住账号
|
||||
const saveRememberedUsername = () => {
|
||||
if (LoginForm.remember) {
|
||||
useLocalStorage.set('rememberedUsername', LoginForm.username)
|
||||
useLocalStorage.set('rememberedUsername', LoginForm.email)
|
||||
} else {
|
||||
useLocalStorage.remove('rememberedUsername')
|
||||
}
|
||||
|
|
@ -113,7 +111,7 @@ const saveRememberedUsername = () => {
|
|||
const loadRememberedUsername = () => {
|
||||
const rememberedUsername = useLocalStorage.get('rememberedUsername')
|
||||
if (rememberedUsername.value) {
|
||||
LoginForm.username = rememberedUsername.value
|
||||
LoginForm.email = rememberedUsername.value
|
||||
LoginForm.remember = true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ const ResetPasswordForm = reactive({
|
|||
// 表单校验规则(Element Plus 标准校验)
|
||||
const formRules = reactive<FormRules>({
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
newPassword: [{ required: true, message: '请输入新密码', trigger: 'blur' }],
|
||||
newPassword: [{ required: true, message: '请输入新密码', trigger: 'blur' }, { min: 6, message: '新密码不得少于6位', trigger: 'blur' }],
|
||||
confirmPassword: [{
|
||||
required: true, message: '请确认新密码', trigger: 'blur'
|
||||
}, {
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<h1>Test</h1>
|
||||
<el-form-item width="100%">
|
||||
<el-button type="primary" class="login-btn" size="large" @click="handleLogout">
|
||||
退出登录
|
||||
</el-button>
|
||||
<el-button type="primary" class="login-btn" size="large" @click="hadnleList">
|
||||
获取信息
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
// 测试
|
||||
import { request } from '@/api/index'
|
||||
const userStore = useUserStore()
|
||||
const handleLogout = () => {
|
||||
userStore.logout()
|
||||
}
|
||||
const hadnleList = () => {
|
||||
request.get('/list').then(res => {
|
||||
console.log(res)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,395 +0,0 @@
|
|||
<template>
|
||||
<el-container class="app-container">
|
||||
<!-- 侧边栏 -->
|
||||
<el-aside width="200px" class="app-aside">
|
||||
<div class="logo">
|
||||
<h2>Vben Admin</h2>
|
||||
</div>
|
||||
<el-menu :default-active="activeMenu" class="app-menu" background-color="#001529" text-color="#bfcbd9"
|
||||
active-text-color="#409EFF">
|
||||
<el-sub-menu index="1">
|
||||
<template #title>
|
||||
<el-icon><data-analysis /></el-icon>
|
||||
<span>概览</span>
|
||||
</template>
|
||||
<el-menu-item index="1-1">分析页</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="2">
|
||||
<template #title>
|
||||
<el-icon>
|
||||
<monitor />
|
||||
</el-icon>
|
||||
<span>演示</span>
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="3">
|
||||
<template #title>
|
||||
<el-icon>
|
||||
<menu />
|
||||
</el-icon>
|
||||
<span>示例</span>
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="4">
|
||||
<template #title>
|
||||
<el-icon>
|
||||
<setting />
|
||||
</el-icon>
|
||||
<span>系统管理</span>
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
<el-menu-item index="5">
|
||||
<el-icon><folder-opened /></el-icon>
|
||||
<span>项目</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="6">
|
||||
<el-icon><info-filled /></el-icon>
|
||||
<span>关于</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<el-container class="app-main-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<el-header class="app-header">
|
||||
<div class="header-left">
|
||||
<el-icon class="toggle-icon" @click="toggleSidebar">
|
||||
<fold />
|
||||
</el-icon>
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item :to="{ path: '/' }">概览</el-breadcrumb-item>
|
||||
<el-breadcrumb-item>分析页</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-input placeholder="搜索" prefix-icon="Search" style="width: 200px; margin-right: 20px" />
|
||||
<el-icon class="header-icon"><moon-night /></el-icon>
|
||||
<el-icon class="header-icon">
|
||||
<avatar />
|
||||
</el-icon>
|
||||
<el-icon class="header-icon"><zoom-in /></el-icon>
|
||||
<el-icon class="header-icon">
|
||||
<bell />
|
||||
</el-icon>
|
||||
<el-avatar :size="32" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" />
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<!-- 内容主体 -->
|
||||
<el-main class="app-main">
|
||||
<el-tabs v-model="activeTab" class="content-tabs">
|
||||
<el-tab-pane label="分析页" name="analysis" />
|
||||
</el-tabs>
|
||||
|
||||
<!-- 数据卡片 -->
|
||||
<el-row :gutter="20" class="data-cards">
|
||||
<el-col :span="6">
|
||||
<el-card class="data-card">
|
||||
<div class="card-content">
|
||||
<div class="card-info">
|
||||
<div class="card-title">用户量</div>
|
||||
<div class="card-value">12,000</div>
|
||||
<div class="card-desc">日用户量</div>
|
||||
</div>
|
||||
<div class="card-icon blue">
|
||||
<el-icon>
|
||||
<user />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="data-card">
|
||||
<div class="card-content">
|
||||
<div class="card-info">
|
||||
<div class="card-title">访问量</div>
|
||||
<div class="card-value">25,000</div>
|
||||
<div class="card-desc">日访问量</div>
|
||||
</div>
|
||||
<div class="card-icon red">
|
||||
<el-icon>
|
||||
<view />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="data-card">
|
||||
<div class="card-content">
|
||||
<div class="card-info">
|
||||
<div class="card-title">下载量</div>
|
||||
<div class="card-value">6,000</div>
|
||||
<div class="card-desc">日下载量</div>
|
||||
</div>
|
||||
<div class="card-icon orange">
|
||||
<el-icon>
|
||||
<download />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="data-card">
|
||||
<div class="card-content">
|
||||
<div class="card-info">
|
||||
<div class="card-title">使用量</div>
|
||||
<div class="card-value">1,000</div>
|
||||
<div class="card-desc">日使用量</div>
|
||||
</div>
|
||||
<div class="card-icon yellow">
|
||||
<el-icon><data-analysis /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<div class="chart-header">
|
||||
<span>趋势分析</span>
|
||||
<el-radio-group v-model="chartRange" size="small">
|
||||
<el-radio-button value="today">今日</el-radio-button>
|
||||
<el-radio-button value="week">本周</el-radio-button>
|
||||
<el-radio-button value="month">本月</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 这里可以替换为你的图表组件,如 ECharts -->
|
||||
<div class="chart-placeholder">
|
||||
<div class="wave-chart"></div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部卡片 -->
|
||||
<el-row :gutter="20" class="bottom-cards">
|
||||
<el-col :span="8">
|
||||
<el-card class="bottom-card">
|
||||
<template #header>访问数量</template>
|
||||
<div class="radar-chart"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card class="bottom-card">
|
||||
<template #header>访问来源</template>
|
||||
<div class="donut-chart"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card class="bottom-card">
|
||||
<template #header>访问渠道</template>
|
||||
<div class="pie-chart"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import {
|
||||
Fold,
|
||||
MoonNight,
|
||||
Avatar,
|
||||
ZoomIn,
|
||||
Bell,
|
||||
User,
|
||||
View,
|
||||
Download,
|
||||
DataAnalysis,
|
||||
Monitor,
|
||||
Menu,
|
||||
Setting,
|
||||
FolderOpened,
|
||||
InfoFilled
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
const activeMenu = ref('1-1')
|
||||
const activeTab = ref('analysis')
|
||||
const chartRange = ref('today')
|
||||
|
||||
const toggleSidebar = () => {
|
||||
// 侧边栏折叠/展开逻辑
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.app-aside {
|
||||
background-color: #001529;
|
||||
color: #bfcbd9;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
border-bottom: 1px solid #0c2135;
|
||||
}
|
||||
|
||||
.app-menu {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.app-main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
background-color: #f0f2f5;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.content-tabs {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.data-cards {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.data-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 12px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.card-icon.blue {
|
||||
background-color: #409eff;
|
||||
}
|
||||
|
||||
.card-icon.red {
|
||||
background-color: #f56c6c;
|
||||
}
|
||||
|
||||
.card-icon.orange {
|
||||
background-color: #e6a23c;
|
||||
}
|
||||
|
||||
.card-icon.yellow {
|
||||
background-color: #e6a23c;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart-placeholder {
|
||||
height: 300px;
|
||||
background-color: #fafafa;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wave-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, rgba(64, 158, 255, 0.2) 0%, rgba(64, 158, 255, 0) 100%);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.bottom-cards {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.bottom-card {
|
||||
border-radius: 8px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.radar-chart,
|
||||
.donut-chart,
|
||||
.pie-chart {
|
||||
height: 200px;
|
||||
background-color: #fafafa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -75,4 +75,18 @@ export default defineConfig({
|
|||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
// 跨域代理核心配置
|
||||
server: {
|
||||
port: 5173, // 前端项目运行端口(可自定义)
|
||||
cors: true, // 允许跨域(基础配置)
|
||||
proxy: {
|
||||
// 匹配以 /api 开头的请求
|
||||
'/api': {
|
||||
target: 'http://192.168.10.10:8085', // 后端接口基础地址
|
||||
changeOrigin: true, // 开启跨域(关键)
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api') // 路径重写(根据后端实际情况调整)
|
||||
// 如果后端接口没有 /api 前缀,需要改为:rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue