2.26提交

This commit is contained in:
李远 2026-02-26 11:03:50 +08:00
parent 451f28151c
commit 29bce3569f
33 changed files with 317 additions and 753 deletions

2
.env
View File

@ -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

View File

@ -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"

View File

@ -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: {}

View File

@ -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'])

View File

@ -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); // 抛出错误让组件层捕获
}
};

View File

@ -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">

View File

@ -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图标组件
}

View File

@ -1,40 +1,69 @@
<template>
<el-header class="header">
<div class="header-content">
<div class="logo">
<el-icon class="el-icon--right" size="24" @click="handleToggleCollapse"
:class="{ 'collapse': isCollapse }">
<Expand />
</el-icon>
</div>
<!-- <div class="logout" @click="handleLogout">
退出登录
</div> -->
<div class="profile">
<el-dropdown placement="bottom-start">
<el-icon size="24" class="avatar-icon">
<Avatar />
<section class="sticky">
<el-header class="header">
<div class="header-content">
<div class="logo">
<el-icon class="el-icon--right" size="24" @click="handleToggleCollapse"
:class="{ 'collapse': isCollapse }">
<Expand />
</el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleChangePassword">修改密码</el-dropdown-item>
<el-dropdown-item @click="handleLogout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</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" style="cursor: pointer;">
<Avatar />
</el-icon>
<template #dropdown>
<el-dropdown-menu>
<!-- <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>
</div>
</el-header>
</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>

View File

@ -1,5 +0,0 @@
<template>
<div class="audit-container">
<h1>资金审核</h1>
</div>
</template>

View File

@ -1,5 +0,0 @@
<template>
<div>
存款
</div>
</template>

View File

@ -1,5 +0,0 @@
<template>
<div class="detail-container">
<h1>资金详情</h1>
</div>
</template>

View File

@ -1,5 +0,0 @@
<template>
<div class="recharge-container">
<h1>资金充值</h1>
</div>
</template>

View File

@ -1,5 +0,0 @@
<template>
<div class="transfer-container">
<h1>资金转账</h1>
</div>
</template>

View File

@ -1,5 +0,0 @@
<template>
<div>
取款
</div>
</template>

View File

@ -1,5 +0,0 @@
<template>
<div class="withdrawal-container">
<h1>资金提现</h1>
</div>
</template>

View File

@ -1,5 +0,0 @@
<template>
<div>
首页
</div>
</template>

View File

@ -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>
<component :is="Component" />
</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 一致) */
/* 进入动画开始 */

View File

@ -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')

View File

@ -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');
}
}
});

View File

@ -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 }
})

View File

@ -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,

View File

@ -3,6 +3,7 @@
* @template T - data any
*/
export interface ApiResponse<T = any> {
msg: string;
success: boolean;
code: number;
message: string;

30
src/type/menu.ts Normal file
View File

@ -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>;

View File

@ -20,8 +20,9 @@ export interface UserInfo {
*
*/
export interface LoginForm {
username: string;
email: string;
password: string;
login_type: number;
remember?: boolean; // 可选:记住我
}

7
src/utils/encrypt.ts Normal file
View File

@ -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);
};

13
src/views/404/index.vue Normal file
View File

@ -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>

View File

@ -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>

View File

@ -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 : '密码修改失败'
}

View File

@ -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
}
}

View File

@ -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'
}, {

View File

@ -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>

View File

@ -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>

View File

@ -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/, '')
}
}
}
})