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

View File

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

View File

@@ -21,16 +21,7 @@ import { RouteRecordRaw } from 'vue-router';
* @description 各字段请查看 `/@/views/system/menu/component/addMenu.vue 下的 ruleForm` * @description 各字段请查看 `/@/views/system/menu/component/addMenu.vue 下的 ruleForm`
* @returns 返回路由菜单数据 * @returns 返回路由菜单数据
*/ */
export const dynamicRoutes: Array<RouteRecordRaw> = [ export const defaultDynamicRouteChildren: Array<RouteRecordRaw> = [
{
path: '/',
name: '/',
component: () => import('/@/layout/index.vue'),
redirect: '/home',
meta: {
isKeepAlive: true,
},
children: [
{ {
path: '/home', path: '/home',
name: 'home', name: 'home',
@@ -61,7 +52,18 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
icon: 'iconfont icon-diannao', icon: 'iconfont icon-diannao',
}, },
}, },
], ];
export const dynamicRoutes: Array<RouteRecordRaw> = [
{
path: '/',
name: '/',
component: () => import('/@/layout/index.vue'),
redirect: '/home',
meta: {
isKeepAlive: true,
},
children: [...defaultDynamicRouteChildren],
}, },
]; ];

View File

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

View File

@@ -6,18 +6,15 @@
<span>店铺维度统计</span> <span>店铺维度统计</span>
</div> </div>
</template> </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"> <div class="search-container">
<el-form :model="searchParams" :inline="true" class="search-form"> <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-form-item label="时间范围">
<el-date-picker <el-date-picker
v-model="searchParams.dateRange" v-model="searchParams.dateRange"
@@ -26,14 +23,61 @@
start-placeholder="开始日期" start-placeholder="开始日期"
end-placeholder="结束日期" end-placeholder="结束日期"
value-format="YYYY-MM-DD" 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>
<el-form-item label="时间粒度"> <el-form-item label="时间粒度">
<el-select v-model="searchParams.granularity" placeholder="选择时间粒度"> <el-select v-model="searchParams.granularity" placeholder="选择时间粒度">
<el-option label="小时" value="hour" />
<el-option label="日" value="day" /> <el-option label="日" value="day" />
<el-option label="周" value="week" /> <el-option label="周" value="week" />
<el-option label="月" value="month" /> <el-option label="月" value="month" />
<el-option label="季度" value="quarter" /> <el-option label="季度" value="quarter" />
<el-option label="年" value="year" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
@@ -42,6 +86,15 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </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"> <div class="stats-cards">
<el-card class="stats-card"> <el-card class="stats-card">
@@ -73,19 +126,85 @@
</div> </div>
</el-card> </el-card>
</div> </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> </el-card>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'; import { ref, reactive, onMounted, computed, watch } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
const searchParams = reactive({ const searchParams = reactive({
shopId: '',
dateRange: [], dateRange: [],
granularity: 'day', 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({ const statsData = reactive({
totalSales: 1258000, totalSales: 1258000,
orderCount: 5230, orderCount: 5230,
@@ -100,15 +219,47 @@ const statsData = reactive({
const salesChartRef = ref(); const salesChartRef = ref();
let salesChart: echarts.ECharts | null = null; 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 getMockSalesTrend = (shopId: string) => {
const dates = ['1月', '2月', '3月', '4月', '5月', '6月']; 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) => ({ return dates.map((date) => ({
date, date,
sales: 1000000 + Math.random() * 500000, sales: baseSales + Math.random() * 500000,
orders: 4000 + Math.random() * 2000, orders: baseOrders + Math.random() * 2000,
xx: 4000 + Math.random() * 2020,
ss: 4000 + Math.random() * 2010,
})); }));
}; };
@@ -123,17 +274,28 @@ const handleSearch = () => {
orderGrowth: 12.3 + Math.random() * 3, orderGrowth: 12.3 + Math.random() * 3,
refundRateChange: -0.5 + Math.random() * 1, refundRateChange: -0.5 + Math.random() * 1,
logisticsRateGrowth: 1.2 + Math.random() * 0.5, logisticsRateGrowth: 1.2 + Math.random() * 0.5,
salesTrend: getMockSalesTrend(), salesTrend: getMockSalesTrend(searchParams.shopId),
}; };
Object.assign(statsData, mockData); Object.assign(statsData, mockData);
initSalesChart(mockData.salesTrend); initSalesChart(mockData.salesTrend);
}; };
const handleReset = () => { const handleReset = () => {
searchParams.shopId = '';
searchParams.dateRange = []; searchParams.dateRange = [];
searchParams.granularity = 'day'; searchParams.granularity = 'day';
}; };
const handleShopClick = (shop: Shop) => {
searchParams.shopId = shop.id;
handleSearch();
};
const handleShopSelect = (shopId: string) => {
searchParams.shopId = shopId;
handleSearch();
};
const initSalesChart = (salesTrend: any[]) => { const initSalesChart = (salesTrend: any[]) => {
if (!salesChartRef.value) return; if (!salesChartRef.value) return;
@@ -200,6 +362,9 @@ const initSalesChart = (salesTrend: any[]) => {
}; };
onMounted(() => { onMounted(() => {
// 初始化店铺列表
shopList.value = getMockShopList();
// 初始化数据
handleSearch(); handleSearch();
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
@@ -271,4 +436,8 @@ onMounted(() => {
width: 100%; width: 100%;
height: 400px; height: 400px;
} }
.shop-list {
margin-top: 20px;
}
</style> </style>