refactor(路由与用户管理): 优化路由处理和用户登出逻辑

- 修改用户登出时的重定向逻辑,确保用户显式返回登录页,避免保留重定向参数
- 引入默认动态路由子项,简化路由配置
- 更新后端路由初始化逻辑,确保动态路由的正确处理
- 增强代码可读性,修复部分代码风格问题
This commit is contained in:
2026-04-08 13:51:43 +08:00
parent c610c6b327
commit f89063af6f
5 changed files with 244 additions and 74 deletions

View File

@@ -180,8 +180,8 @@ export default defineComponent({
.then(async () => {
// 清除缓存/token等
Session.clear();
// 使用 reload 时,不需要调用 resetRoute() 重置路由
window.location.reload();
// 显式回到登录页,避免保留之前受保护页面的重定向参数
await router.replace('/login');
})
.catch(() => {});
} else if (path === 'wareHouse') {

View File

@@ -4,14 +4,12 @@ import { useUserInfo } from '/@/stores/userInfo';
import { useRequestOldRoutes } from '/@/stores/requestOldRoutes';
import { Session } from '/@/utils/storage';
import { NextLoading } from '/@/utils/loading';
import { dynamicRoutes, notFoundAndNoPower } from '/@/router/route';
import { dynamicRoutes, defaultDynamicRouteChildren, notFoundAndNoPower } from '/@/router/route';
import { formatTwoStageRoutes, formatFlatteningRoutes, router } from '/@/router/index';
import { useRoutesList } from '/@/stores/routesList';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import { getUserMenus } from '/@/api/system/menu/index';
const layouModules: any = import.meta.glob('../layout/routerView/*.{vue,tsx}');
const viewsModules: any = import.meta.glob('../views/**/*.{vue,tsx}');
@@ -43,11 +41,12 @@ export async function initBackEndControlRoutes() {
await useUserInfo().setPermissions();
// 获取路由菜单数据
await getBackEndControlRoutes();
let menuRoute = Session.get('userMenu')
let menuRoute = Session.get('userMenu');
// 存储接口原始路由未处理component根据需求选择使用
useRequestOldRoutes().setRequestOldRoutes(JSON.parse(JSON.stringify(menuRoute)));
// 处理路由component替换 dynamicRoutes/@/router/route第一个顶级 children 的路由
dynamicRoutes[0].children?.push(...await backEndComponent(menuRoute));
dynamicRoutes[0].children = [...defaultDynamicRouteChildren];
dynamicRoutes[0].children?.push(...(await backEndComponent(menuRoute)));
// 添加动态路由
await setAddRoute();
// 设置路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
@@ -104,10 +103,10 @@ export async function setAddRoute() {
* @returns 返回后端路由菜单数据
*/
export async function getBackEndControlRoutes() {
let menuRoute = Session.get('userMenu')
let permissions = Session.get('permissions')
let menuRoute = Session.get('userMenu');
let permissions = Session.get('permissions');
if (!menuRoute || !permissions) {
await refreshBackEndControlRoutes()
await refreshBackEndControlRoutes();
}
}
@@ -118,11 +117,11 @@ export async function getBackEndControlRoutes() {
*/
export async function refreshBackEndControlRoutes() {
// 获取路由
await getUserMenus().then((res:any)=>{
Session.set('userMenu',res.data.menuList)
Session.set('permissions',res.data.permissions)
})
await useUserInfo().setPermissions()
await getUserMenus().then((res: any) => {
Session.set('userMenu', res.data.menuList);
Session.set('permissions', res.data.permissions);
});
await useUserInfo().setPermissions();
}
/**
@@ -140,16 +139,16 @@ export function setBackEndControlRefreshRoutes() {
* @returns 返回处理成函数后的 component
*/
export function backEndComponent(routes: any) {
if (!routes) return;
if (!routes) return [];
return routes.map((item: any) => {
if(item.children&&item.children.length>0){
item.children.some((ci:any)=>{
if(!ci.meta.isHide){
item.redirect = ci
return true
if (item.children && item.children.length > 0) {
item.children.some((ci: any) => {
if (!ci.meta.isHide) {
item.redirect = ci;
return true;
}
return false
})
return false;
});
}
if (item.component) item.component = dynamicImport(dynamicViewsModules, item.component as string);
item.children && backEndComponent(item.children);

View File

@@ -21,6 +21,39 @@ import { RouteRecordRaw } from 'vue-router';
* @description 各字段请查看 `/@/views/system/menu/component/addMenu.vue 下的 ruleForm`
* @returns 返回路由菜单数据
*/
export const defaultDynamicRouteChildren: Array<RouteRecordRaw> = [
{
path: '/home',
name: 'home',
component: () => import('/@/views/home/index.vue'),
meta: {
title: 'message.router.home',
isLink: '',
isHide: true,
isKeepAlive: true,
isAffix: true,
isIframe: false,
roles: ['admin', 'common'],
icon: 'iconfont icon-shouye',
},
},
{
path: '/personal',
name: 'personals',
component: () => import('/@/views/system/personal/index.vue'),
meta: {
title: '个人中心',
isLink: '',
isHide: true,
isKeepAlive: true,
isAffix: false,
isIframe: false,
roles: ['admin'],
icon: 'iconfont icon-diannao',
},
},
];
export const dynamicRoutes: Array<RouteRecordRaw> = [
{
path: '/',
@@ -30,38 +63,7 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
meta: {
isKeepAlive: true,
},
children: [
{
path: '/home',
name: 'home',
component: () => import('/@/views/home/index.vue'),
meta: {
title: 'message.router.home',
isLink: '',
isHide: true,
isKeepAlive: true,
isAffix: true,
isIframe: false,
roles: ['admin', 'common'],
icon: 'iconfont icon-shouye',
},
},
{
path: '/personal',
name: 'personals',
component: () => import('/@/views/system/personal/index.vue'),
meta: {
title: '个人中心',
isLink: '',
isHide: true,
isKeepAlive: true,
isAffix: false,
isIframe: false,
roles: ['admin'],
icon: 'iconfont icon-diannao',
},
},
],
children: [...defaultDynamicRouteChildren],
},
];

View File

@@ -72,9 +72,9 @@ const performLogout = () => {
Session.clear();
localStorage.clear();
isHandlingTokenExpired = false;
// 跳转到后台管理登录页,确保完全刷新
// Hash 路由统一回登录页,避免跳到错误地址
setTimeout(() => {
window.location.href = '/login';
window.location.href = '/#/login';
}, 500);
};

View File

@@ -6,18 +6,15 @@
<span>店铺维度统计</span>
</div>
</template>
<!-- 销售趋势图表 -->
<div class="chart-container">
<el-card>
<template #header>
<div class="card-header">销售趋势</div>
</template>
<div ref="salesChartRef" class="chart"></div>
</el-card>
</div>
<!-- 搜索条件 -->
<div class="search-container">
<el-form :model="searchParams" :inline="true" class="search-form">
<el-form-item label="店铺">
<el-select v-model="searchParams.shopId" placeholder="选择店铺">
<el-option label="全部店铺" value="" />
<el-option v-for="shop in shopList" :key="shop.id" :label="shop.name" :value="shop.id" />
</el-select>
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker
v-model="searchParams.dateRange"
@@ -26,14 +23,61 @@
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
:shortcuts="[
{
text: '最近7天',
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
return [start, end];
},
},
{
text: '最近30天',
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
return [start, end];
},
},
{
text: '最近90天',
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
return [start, end];
},
},
{
text: '今年',
value: () => {
const end = new Date();
const start = new Date(new Date().getFullYear(), 0, 1);
return [start, end];
},
},
{
text: '去年',
value: () => {
const end = new Date(new Date().getFullYear() - 1, 11, 31);
const start = new Date(new Date().getFullYear() - 1, 0, 1);
return [start, end];
},
},
]"
/>
</el-form-item>
<el-form-item label="时间粒度">
<el-select v-model="searchParams.granularity" placeholder="选择时间粒度">
<el-option label="小时" value="hour" />
<el-option label="日" value="day" />
<el-option label="周" value="week" />
<el-option label="月" value="month" />
<el-option label="季度" value="quarter" />
<el-option label="年" value="year" />
</el-select>
</el-form-item>
<el-form-item>
@@ -42,6 +86,15 @@
</el-form-item>
</el-form>
</div>
<!-- 销售趋势图表 -->
<div class="chart-container">
<el-card>
<template #header>
<div class="card-header">销售趋势 - {{ selectedShopName }}</div>
</template>
<div ref="salesChartRef" class="chart"></div>
</el-card>
</div>
<!-- 核心指标 -->
<div class="stats-cards">
<el-card class="stats-card">
@@ -73,19 +126,85 @@
</div>
</el-card>
</div>
<!-- 店铺列表 -->
<div class="shop-list">
<el-card>
<template #header>
<div class="card-header">店铺列表</div>
</template>
<el-table :data="shopList" style="width: 100%" @row-click="handleShopClick">
<el-table-column prop="id" label="店铺ID" width="100" />
<el-table-column prop="name" label="店铺名称" />
<el-table-column prop="type" label="店铺类型">
<template #default="scope">
<el-tag size="small">{{ scope.row.type === 'physical' ? '线下店铺' : '线上店铺' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态">
<template #default="scope">
<el-tag size="small" :type="scope.row.status === 'active' ? 'success' : 'danger'">
{{ scope.row.status === 'active' ? '营业中' : '已关闭' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button type="primary" size="small" @click="handleShopSelect(scope.row.id)">查看详情</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { ref, reactive, onMounted, computed, watch } from 'vue';
import * as echarts from 'echarts';
const searchParams = reactive({
shopId: '',
dateRange: [],
granularity: 'day',
});
// 监听店铺选择变化,自动更新数据
watch(
() => searchParams.shopId,
() => {
handleSearch();
}
);
// 监听时间粒度变化,自动更新数据
watch(
() => searchParams.granularity,
() => {
handleSearch();
}
);
interface Shop {
id: string;
name: string;
type: string;
status: string;
}
const shopList = ref<Shop[]>([]);
// 模拟店铺列表数据
const getMockShopList = () => {
return [
{ id: '1', name: '店铺A', type: 'physical', status: 'active' },
{ id: '2', name: '店铺B', type: 'online', status: 'active' },
{ id: '3', name: '店铺C', type: 'physical', status: 'active' },
{ id: '4', name: '店铺D', type: 'online', status: 'active' },
{ id: '5', name: '店铺E', type: 'physical', status: 'active' },
];
};
const statsData = reactive({
totalSales: 1258000,
orderCount: 5230,
@@ -100,15 +219,47 @@ const statsData = reactive({
const salesChartRef = ref();
let salesChart: echarts.ECharts | null = null;
// 计算选中的店铺名称
const selectedShopName = computed(() => {
if (!searchParams.shopId) return '全部店铺';
const shop = shopList.value.find((s) => s.id === searchParams.shopId);
return shop ? shop.name : '未知店铺';
});
// 模拟销售趋势数据
const getMockSalesTrend = () => {
const dates = ['1月', '2月', '3月', '4月', '5月', '6月'];
const getMockSalesTrend = (shopId: string) => {
let dates: string[] = [];
const baseSales = shopId ? 800000 + parseInt(shopId) * 100000 : 1000000;
const baseOrders = shopId ? 3000 + parseInt(shopId) * 500 : 4000;
// 根据时间粒度生成不同的时间标签
switch (searchParams.granularity) {
case 'hour':
dates = ['0时', '4时', '8时', '12时', '16时', '20时'];
break;
case 'day':
dates = ['1日', '5日', '10日', '15日', '20日', '25日'];
break;
case 'week':
dates = ['第1周', '第2周', '第3周', '第4周', '第5周', '第6周'];
break;
case 'month':
dates = ['1月', '2月', '3月', '4月', '5月', '6月'];
break;
case 'quarter':
dates = ['Q1', 'Q2', 'Q3', 'Q4'];
break;
case 'year':
dates = ['2022年', '2023年', '2024年', '2025年', '2026年'];
break;
default:
dates = ['1月', '2月', '3月', '4月', '5月', '6月'];
}
return dates.map((date) => ({
date,
sales: 1000000 + Math.random() * 500000,
orders: 4000 + Math.random() * 2000,
xx: 4000 + Math.random() * 2020,
ss: 4000 + Math.random() * 2010,
sales: baseSales + Math.random() * 500000,
orders: baseOrders + Math.random() * 2000,
}));
};
@@ -123,17 +274,28 @@ const handleSearch = () => {
orderGrowth: 12.3 + Math.random() * 3,
refundRateChange: -0.5 + Math.random() * 1,
logisticsRateGrowth: 1.2 + Math.random() * 0.5,
salesTrend: getMockSalesTrend(),
salesTrend: getMockSalesTrend(searchParams.shopId),
};
Object.assign(statsData, mockData);
initSalesChart(mockData.salesTrend);
};
const handleReset = () => {
searchParams.shopId = '';
searchParams.dateRange = [];
searchParams.granularity = 'day';
};
const handleShopClick = (shop: Shop) => {
searchParams.shopId = shop.id;
handleSearch();
};
const handleShopSelect = (shopId: string) => {
searchParams.shopId = shopId;
handleSearch();
};
const initSalesChart = (salesTrend: any[]) => {
if (!salesChartRef.value) return;
@@ -200,6 +362,9 @@ const initSalesChart = (salesTrend: any[]) => {
};
onMounted(() => {
// 初始化店铺列表
shopList.value = getMockShopList();
// 初始化数据
handleSearch();
window.addEventListener('resize', () => {
@@ -271,4 +436,8 @@ onMounted(() => {
width: 100%;
height: 400px;
}
.shop-list {
margin-top: 20px;
}
</style>