重写复现方法

This commit is contained in:
2025-09-02 17:59:01 +08:00
parent c555bbba1d
commit de83878345
3 changed files with 244 additions and 369 deletions

View File

@@ -1,34 +1,23 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<a-layout style="min-height: 100vh;"> <a-layout style="min-height: 100vh;">
<!-- 顶部导航栏 --> <!-- 顶部导航栏无修改 -->
<a-layout-header class="header"> <a-layout-header class="header">
<div class="logo"> <div class="logo">
<img src="/my.png" class="logo-img" alt="系统Logo" /> <img src="/my.png" class="logo-img" alt="系统Logo" />
<span class="system-name">cApi</span> <span class="system-name">cApi</span>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<div class="search-box"> <div class="search-box">
<SearchOutlined class="search-icon" /> <SearchOutlined class="search-icon" />
<input type="text" placeholder="搜索" class="search-input" /> <input type="text" placeholder="搜索" class="search-input" />
</div> </div>
<div class="action-icons"> <div class="action-icons">
<a-tooltip title="收藏"> <a-tooltip title="收藏"><StarOutlined class="action-icon" /></a-tooltip>
<StarOutlined class="action-icon" /> <a-tooltip title="通知"><BellOutlined class="action-icon" /></a-tooltip>
</a-tooltip> <a-tooltip title="帮助"><QuestionCircleOutlined class="action-icon" /></a-tooltip>
<a-tooltip title="通知"> <a-tooltip title="设置"><SettingOutlined class="action-icon" /></a-tooltip>
<BellOutlined class="action-icon" />
</a-tooltip>
<a-tooltip title="帮助">
<QuestionCircleOutlined class="action-icon" />
</a-tooltip>
<a-tooltip title="设置">
<SettingOutlined class="action-icon" />
</a-tooltip>
</div> </div>
<a-dropdown <a-dropdown
placement="bottomRight" placement="bottomRight"
:auto-adjust-overflow="true" :auto-adjust-overflow="true"
@@ -51,9 +40,9 @@
</div> </div>
</a-layout-header> </a-layout-header>
<!-- 中间主体布局 --> <!-- 中间主体布局菜单逻辑优化 -->
<a-layout style="flex: 1; height: calc(100vh - 64px);"> <a-layout style="flex: 1; height: calc(100vh - 64px);">
<!-- 左侧菜单折叠时悬浮子菜单展开时点击展开 --> <!-- 左侧菜单状态与路由同步优化 -->
<a-layout-sider <a-layout-sider
class="side-menu-container" class="side-menu-container"
width="200" width="200"
@@ -62,20 +51,14 @@
collapsible collapsible
:class="{ 'sider-collapsed': collapsed }" :class="{ 'sider-collapsed': collapsed }"
> >
<!-- 加载状态 --> <div v-if="loading" class="menu-loading"><a-spin size="large" /></div>
<div v-if="loading" class="menu-loading">
<a-spin size="large" />
</div>
<!-- 菜单内容 -->
<div v-else class="menu-scroll-container"> <div v-else class="menu-scroll-container">
<!-- 父级菜单列表根据折叠状态切换交互 -->
<div <div
v-for="(module, moduleKey) in moduleMenusConfig" v-for="(module, moduleKey) in moduleMenusConfig"
:key="module.moduleCode" :key="module.moduleCode"
class="parent-menu-item" class="parent-menu-item"
> >
<!-- 折叠状态使用Dropdown实现悬浮子菜单 --> <!-- 折叠状态悬浮子菜单 -->
<a-dropdown <a-dropdown
v-if="collapsed" v-if="collapsed"
placement="right" placement="right"
@@ -83,16 +66,12 @@
:open="collapsedMenuVisible[module.moduleCode]" :open="collapsedMenuVisible[module.moduleCode]"
@openChange="(visible) => handleCollapsedMenuVisible(visible, module.moduleCode)" @openChange="(visible) => handleCollapsedMenuVisible(visible, module.moduleCode)"
> >
<!-- 折叠时的父级菜单触发按钮 -->
<div class="parent-menu-header collapsed-mode" @click.stop> <div class="parent-menu-header collapsed-mode" @click.stop>
<component :is="getIconComponent(module.icon)" class="parent-menu-icon" /> <component :is="getIconComponent(module.icon)" class="parent-menu-icon" />
<!-- 折叠时显示模块名称tooltip -->
<a-tooltip :title="module.moduleName" placement="right"> <a-tooltip :title="module.moduleName" placement="right">
<span class="collapsed-tooltip-placeholder" /> <span class="collapsed-tooltip-placeholder" />
</a-tooltip> </a-tooltip>
</div> </div>
<!-- 折叠时的悬浮子菜单内容 -->
<template #overlay> <template #overlay>
<a-menu <a-menu
class="collapsed-sub-menu" class="collapsed-sub-menu"
@@ -110,9 +89,8 @@
</template> </template>
</a-dropdown> </a-dropdown>
<!-- 展开状态点击展开子菜单逻辑 --> <!-- 展开状态点击展开子菜单 -->
<div v-else> <div v-else>
<!-- 父级菜单标题点击展开/关闭子菜单 -->
<div <div
class="parent-menu-header" class="parent-menu-header"
@click="toggleSubMenu(module.moduleCode)" @click="toggleSubMenu(module.moduleCode)"
@@ -124,8 +102,6 @@
class="parent-menu-caret" class="parent-menu-caret"
/> />
</div> </div>
<!-- 子菜单列表根据状态显示/隐藏 -->
<div <div
class="sub-menu-list" class="sub-menu-list"
:class="{ 'sub-menu-visible': isSubMenuOpen(module.moduleCode) }" :class="{ 'sub-menu-visible': isSubMenuOpen(module.moduleCode) }"
@@ -144,24 +120,22 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 折叠/展开按钮 -->
<div class="collapse-trigger" @click="collapsed = !collapsed"> <div class="collapse-trigger" @click="collapsed = !collapsed">
<component :is="collapsed ? MenuUnfoldOutlined : MenuFoldOutlined" /> <component :is="collapsed ? MenuUnfoldOutlined : MenuFoldOutlined" />
</div> </div>
</a-layout-sider> </a-layout-sider>
<!-- 右侧内容区 --> <!-- 右侧内容区核心修改用RouterView渲染路由组件 -->
<a-layout-content class="right-content"> <a-layout-content class="right-content">
<!-- 标签页与路由绑定 -->
<div class="tab-bar"> <div class="tab-bar">
<a-tag <a-tag
:class="['console-tab', activeTabKey === 'console' ? 'console-tab--active' : '']" :class="['console-tab', $route.path === '/console' ? 'console-tab--active' : '']"
@click="switchToConsole" @click="switchToConsole"
> >
<HomeOutlined class="console-tab-icon" /> <HomeOutlined class="console-tab-icon" />
<span>控制台</span> <span>控制台</span>
</a-tag> </a-tag>
<a-button <a-button
size="small" size="small"
class="tab-nav-btn" class="tab-nav-btn"
@@ -171,7 +145,6 @@
> >
&lt; &lt;
</a-button> </a-button>
<a-tabs <a-tabs
v-model:activeKey="activeTabKey" v-model:activeKey="activeTabKey"
@change="handleTabChange" @change="handleTabChange"
@@ -185,7 +158,6 @@
:tab="renderTabTitle(tab)" :tab="renderTabTitle(tab)"
/> />
</a-tabs> </a-tabs>
<a-button <a-button
size="small" size="small"
class="tab-nav-btn" class="tab-nav-btn"
@@ -195,7 +167,6 @@
> >
&gt; &gt;
</a-button> </a-button>
<a-button <a-button
size="small" size="small"
class="close-all-btn" class="close-all-btn"
@@ -207,25 +178,25 @@
</a-button> </a-button>
</div> </div>
<!-- 内容区核心RouterView渲染当前路由组件 + 加载/错误状态 -->
<a-layout-content class="main-content"> <a-layout-content class="main-content">
<div class="home-content"> <div class="home-content">
<!-- 加载状态显示 --> <!-- 加载状态路由切换时显示 -->
<div v-if="contentLoading" class="content-loading"> <div v-if="contentLoading" class="content-loading">
<a-spin size="large" /> <a-spin size="large" />
</div> </div>
<!-- 组件内容显示 --> <!-- 路由组件渲染容器 -->
<component <RouterView
v-else v-else
:is="currentMainComponent" @error="handleRouterViewError"
:key="activeTabKey"
/> />
<!-- 组件加载失败提示 --> <!-- 组件加载失败提示 -->
<div v-if="componentError" class="component-error"> <div v-if="componentError" class="component-error">
<ExclamationCircleOutlined class="error-icon" /> <ExclamationCircleOutlined class="error-icon" />
<p>组件加载失败请检查路径是否正确</p> <p>组件加载失败请检查路由配置或组件路</p>
<p class="error-path">{{ errorPath }}</p> <p class="error-path">失败路由{{ errorPath }}</p>
</div> </div>
</div> </div>
</a-layout-content> </a-layout-content>
@@ -233,7 +204,7 @@
</a-layout> </a-layout>
</a-layout> </a-layout>
<!-- 退出确认对话框 --> <!-- 退出确认对话框无修改 -->
<a-modal <a-modal
v-model:open="logoutVisible" v-model:open="logoutVisible"
:centered="true" :centered="true"
@@ -249,17 +220,9 @@
温馨提示 温馨提示
</p> </p>
<p class="logout-desc">是否确认退出系统</p> <p class="logout-desc">是否确认退出系统</p>
<div class="logout-actions"> <div class="logout-actions">
<a-button size="large" @click="logoutVisible = false">取消</a-button> <a-button size="large" @click="logoutVisible = false">取消</a-button>
<a-button <a-button type="primary" size="large" style="margin-left: 16px" @click="doLogout">确定</a-button>
type="primary"
size="large"
style="margin-left: 16px"
@click="doLogout"
>
确定
</a-button>
</div> </div>
</div> </div>
</a-modal> </a-modal>
@@ -267,91 +230,77 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, h, reactive, watch, onMounted, defineAsyncComponent } from 'vue'; import { ref, computed, h, reactive, watch, onMounted } from 'vue';
import { Layout, Menu, Button, Tabs, Card, Tag, Dropdown, Modal, Tooltip, Spin } from 'ant-design-vue'; import { useRouter, useRoute, RouterView } from 'vue-router'; // 引入Vue Router核心API
import { getModuleMenus, ModuleItem, MenuItem } from '@/api/menu' import { Layout, Menu, Button, Tabs, Tag, Dropdown, Modal, Tooltip, Spin } from 'ant-design-vue';
import { getModuleMenus, ModuleItem, MenuItem } from '@/api/menu';
// 图标导入(无修改)
import { import {
ApiOutlined, ApiOutlined, UserOutlined, CloseOutlined, LinkOutlined, LogoutOutlined,
UserOutlined, ExclamationCircleOutlined, BarChartOutlined, DownloadOutlined, SettingOutlined,
CloseOutlined, FormOutlined, HistoryOutlined, HomeOutlined, DatabaseOutlined, SearchOutlined,
LinkOutlined, StarOutlined, BellOutlined, QuestionCircleOutlined, MenuFoldOutlined,
LogoutOutlined, MenuUnfoldOutlined, CaretUpOutlined, CaretDownOutlined
ExclamationCircleOutlined,
BarChartOutlined,
DownloadOutlined,
SettingOutlined,
FormOutlined,
HistoryOutlined,
HomeOutlined,
DatabaseOutlined,
SearchOutlined,
StarOutlined,
BellOutlined,
QuestionCircleOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
CaretUpOutlined,
CaretDownOutlined
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
// 导入控制台组件 // 1. 初始化路由实例
import Main from './Main.vue'; const router = useRouter();
const route = useRoute();
// 状态管理 // 2. 状态管理移除原component相关状态新增路由关联状态
const collapsed = ref(false); // 菜单折叠状态 const collapsed = ref(false);
const activeTabKey = ref('console'); // 当前激活的标签页 const logoutVisible = ref(false);
const logoutVisible = ref(false); // 退出确认框显示状态 const selectedKey = ref(''); // 选中的菜单Code
const selectedKey = ref(''); // 当前选中的子菜单项 const expandedParentMenus = ref<string[]>([]); // 展开的父菜单Code
const expandedParentMenus = ref<string[]>([]); // 展开状态下的子菜单展开记录
const loading = ref(true); // 菜单加载状态 const loading = ref(true); // 菜单加载状态
const contentLoading = ref(false); // 内容区加载状态 const contentLoading = ref(false); // 路由切换加载状态
const componentError = ref(false); // 组件加载错误状态 const componentError = ref(false); // 组件加载错误
const errorPath = ref(''); // 错误路径记录 const errorPath = ref(''); // 错误路由路
// 状态定义 // 菜单配置与折叠状态
const moduleMenusConfig = ref<ModuleItem[]>([]) const moduleMenusConfig = ref<ModuleItem[]>([]);
// 初始化折叠状态下的菜单显示状态 const collapsedMenuVisible = reactive<Record<string, boolean>>({});
const collapsedMenuVisible = reactive<Record<string, boolean>>({})
// 标签页数据类型定义 // 3. 标签页数据结构修改关联路由Path移除component
interface TabItem { interface TabItem {
key: string; key: string; // 建议用menuCode唯一
title: string; title: string; // 菜单名称
component: any; path: string; // 路由Path原chref
closable: boolean; closable: boolean; // 是否可关闭
chref: string; // 存储组件路径
} }
// 标签页数据 // 初始化标签页:控制台对应路由/console
const allTabs = ref<TabItem[]>([ const allTabs = ref<TabItem[]>([
{ key: 'console', title: '控制台', component: Main, closable: false, chref: '' } { key: 'console', title: '控制台', path: '/console', closable: false }
]); ]);
// 计算属性 // 4. 计算属性(标签页过滤与索引)
const otherTabs = computed(() => allTabs.value.filter(tab => tab.key !== 'console')); const otherTabs = computed(() => allTabs.value.filter(tab => tab.key !== 'console'));
const currentTabIndex = computed(() => otherTabs.value.findIndex(tab => tab.key === activeTabKey.value)); const activeTabKey = computed({
const currentMainComponent = computed(() => { get() { // 从当前路由同步activeTabKey
try { const matchedTab = allTabs.value.find(tab => tab.path === route.path);
const targetTab = allTabs.value.find(tab => tab.key === activeTabKey.value); return matchedTab ? matchedTab.key : 'console';
return targetTab ? targetTab.component : null; },
} catch (error) { set(key) { // 避免手动修改(实际通过路由切换触发)
console.error('组件加载失败:', error); const matchedTab = allTabs.value.find(tab => tab.key === key);
return null; if (matchedTab) router.push(matchedTab.path);
} }
}); });
const currentTabIndex = computed(() => {
return otherTabs.value.findIndex(tab => tab.path === route.path);
});
// 从接口获取菜单数据 // 5. 菜单数据加载(无修改)
const fetchMenuData = async () => { const fetchMenuData = async () => {
try { try {
loading.value = true; loading.value = true;
const modules = await getModuleMenus() const modules = await getModuleMenus();
moduleMenusConfig.value = modules;
moduleMenusConfig.value = modules // 初始化折叠菜单状态
// 初始化折叠状态下的菜单显示状态 modules.forEach(module => collapsedMenuVisible[module.moduleCode] = false);
modules.forEach(module => { // 路由初始化后同步菜单状态
collapsedMenuVisible[module.moduleCode] = false syncMenuStateWithRoute();
})
} catch (error) { } catch (error) {
console.error('菜单接口请求失败:', error); console.error('菜单接口请求失败:', error);
} finally { } finally {
@@ -359,80 +308,101 @@ const fetchMenuData = async () => {
} }
}; };
// 页面加载时获取菜单数据
onMounted(() => { onMounted(() => {
fetchMenuData(); fetchMenuData();
// 监听路由变化,同步菜单和标签页状态
router.beforeEach((to, from, next) => {
contentLoading.value = true; // 路由开始切换,显示加载
componentError.value = false; // 重置错误状态
next();
});
router.afterEach(() => {
contentLoading.value = false; // 路由切换完成,隐藏加载
syncMenuStateWithRoute(); // 同步菜单状态
});
}); });
// 根据图标名称获取图标组件 // 6. 核心工具函数
// 6.1 图标组件获取(无修改)
const getIconComponent = (iconName: string) => { const getIconComponent = (iconName: string) => {
const iconMap: Record<string, any> = { const iconMap: Record<string, any> = {
ApiOutlined, ApiOutlined, UserOutlined, BarChartOutlined, DownloadOutlined,
UserOutlined, SettingOutlined, FormOutlined, HistoryOutlined, HomeOutlined,
BarChartOutlined, DatabaseOutlined, LinkOutlined
DownloadOutlined,
SettingOutlined,
FormOutlined,
HistoryOutlined,
HomeOutlined,
DatabaseOutlined,
LinkOutlined
}; };
return iconMap[iconName] || ApiOutlined; return iconMap[iconName] || ApiOutlined;
}; };
// 根据chref路径获取组件 // 6.2 同步菜单状态与当前路由(关键:路由变 -> 菜单变)
const getComponentByPath = async (chref: string) => { const syncMenuStateWithRoute = () => {
try { if (route.path === '/console') { // 控制台路由
// 显示加载状态 selectedKey.value = '';
contentLoading.value = true; expandedParentMenus.value = [];
componentError.value = false; return;
}
// 添加@vite-ignore注释告诉Vite忽略动态导入分析
const component = defineAsyncComponent(() => // 非控制台路由:找到对应菜单,更新选中和展开状态
import(/* @vite-ignore */ `@/views/${chref}.vue`) for (const module of moduleMenusConfig.value) {
); const matchedMenu = module.menus.find(menu => menu.chref === route.path);
if (matchedMenu) {
return component; selectedKey.value = matchedMenu.menuCode;
} catch (error) { expandedParentMenus.value = [module.moduleCode]; // 展开当前菜单的父级
console.error(`加载组件失败: @/views/${chref}.vue`, error); break;
componentError.value = true; }
errorPath.value = `@/views/${chref}.vue`;
return null;
} finally {
contentLoading.value = false;
} }
}; };
// 6.3 检查父菜单是否展开(无修改)
// 检查展开状态下子菜单是否打开
const isSubMenuOpen = (moduleCode: string) => { const isSubMenuOpen = (moduleCode: string) => {
return expandedParentMenus.value.includes(moduleCode); return expandedParentMenus.value.includes(moduleCode);
}; };
// 【优化核心】展开状态:切换子菜单展开/关闭,点击当前父级时关闭其他父级 // 7. 菜单交互逻辑(修改为路由跳转)
// 7.1 展开状态:切换父菜单展开/关闭
const toggleSubMenu = (moduleCode: string) => { const toggleSubMenu = (moduleCode: string) => {
if (isSubMenuOpen(moduleCode)) { expandedParentMenus.value = isSubMenuOpen(moduleCode) ? [] : [moduleCode];
// 当前父级已展开,点击则关闭
expandedParentMenus.value = [];
} else {
// 当前父级未展开,点击则打开当前并关闭其他所有父级
expandedParentMenus.value = [moduleCode];
}
}; };
// 折叠状态:控制悬浮菜单显示/隐藏 // 7.2 折叠状态:控制悬浮菜单显示
const handleCollapsedMenuVisible = (visible: boolean, moduleCode: string) => { const handleCollapsedMenuVisible = (visible: boolean, moduleCode: string) => {
// 关闭其他所有悬浮菜单
Object.keys(collapsedMenuVisible).forEach(key => { Object.keys(collapsedMenuVisible).forEach(key => {
if (key !== moduleCode) collapsedMenuVisible[key] = false; if (key !== moduleCode) collapsedMenuVisible[key] = false;
}); });
// 更新当前菜单状态
collapsedMenuVisible[moduleCode] = visible; collapsedMenuVisible[moduleCode] = visible;
}; };
// 标签页标题渲染 // 7.3 展开状态:子菜单点击(核心:跳转路由 + 添加标签页)
const handleSubMenuItemClick = (menu: MenuItem) => {
const targetPath = menu.chref; // 菜单的chref即路由Path
if (!targetPath) return;
// 步骤1跳转路由
router.push(targetPath);
// 步骤2添加标签页不存在则新增
const tabExists = allTabs.value.some(tab => tab.path === targetPath);
if (!tabExists) {
allTabs.value.push({
key: menu.menuCode,
title: menu.menuName,
path: targetPath,
closable: true
});
}
// 步骤3关闭折叠菜单若存在
Object.keys(collapsedMenuVisible).forEach(key => collapsedMenuVisible[key] = false);
};
// 7.4 折叠状态:悬浮子菜单点击(同展开状态逻辑)
const handleCollapsedSubMenuClick = (e: { key: string }, menus: MenuItem[]) => {
const menuCode = e.key;
const menu = menus.find(item => item.menuCode === menuCode);
if (menu) handleSubMenuItemClick(menu);
};
// 8. 标签页交互逻辑(与路由绑定)
// 8.1 渲染标签标题(无修改)
const renderTabTitle = (tab: TabItem) => { const renderTabTitle = (tab: TabItem) => {
return h('div', { class: 'tab-title-container' }, [ return h('div', { class: 'tab-title-container' }, [
h('span', { class: 'tab-title-text' }, tab.title), h('span', { class: 'tab-title-text' }, tab.title),
@@ -442,7 +412,7 @@ const renderTabTitle = (tab: TabItem) => {
class: 'tab-close-btn', class: 'tab-close-btn',
onClick: (e: Event) => { onClick: (e: Event) => {
e.stopPropagation(); e.stopPropagation();
deleteTab(tab.key); deleteTab(tab);
} }
}, },
h(CloseOutlined, { size: 12 }) h(CloseOutlined, { size: 12 })
@@ -450,203 +420,94 @@ const renderTabTitle = (tab: TabItem) => {
]); ]);
}; };
// 切换到控制台 // 8.2 切换到控制台(跳转路由)
const switchToConsole = () => { const switchToConsole = () => {
activeTabKey.value = 'console'; router.push('/console');
selectedKey.value = '';
componentError.value = false;
// 关闭所有展开的子菜单
expandedParentMenus.value = [];
// 关闭所有折叠状态的悬浮菜单
Object.keys(collapsedMenuVisible).forEach(key => collapsedMenuVisible[key] = false); Object.keys(collapsedMenuVisible).forEach(key => collapsedMenuVisible[key] = false);
}; };
// 标签页切换(上一个/下一个) // 8.3 标签页切换(上一个/下一个)
const switchTab = (direction: 'prev' | 'next') => { const switchTab = (direction: 'prev' | 'next') => {
componentError.value = false; const tabs = otherTabs.value;
if (direction === 'prev' && currentTabIndex.value > 0) { if (tabs.length === 0) return;
activeTabKey.value = otherTabs.value[currentTabIndex.value - 1].key;
} else if (direction === 'next' && currentTabIndex.value < otherTabs.value.length - 1) { const currentIdx = currentTabIndex.value;
activeTabKey.value = otherTabs.value[currentTabIndex.value + 1].key; let targetIdx = direction === 'prev' ? currentIdx - 1 : currentIdx + 1;
} targetIdx = Math.max(0, Math.min(targetIdx, tabs.length - 1));
router.push(tabs[targetIdx].path); // 跳转目标路由
}; };
// 关闭单个标签页 // 8.4 关闭单个标签页
const deleteTab = (key: string) => { const deleteTab = (tab: TabItem) => {
if (key === 'console') return; if (!tab.closable) return;
const tabIndex = allTabs.value.findIndex(tab => tab.key === key);
const tabIndex = allTabs.value.findIndex(item => item.key === tab.key);
if (tabIndex === -1) return; if (tabIndex === -1) return;
allTabs.value = allTabs.value.filter(tab => tab.key !== key); // 若关闭的是当前激活标签,跳转到前一个标签或控制台
const isActive = tab.path === route.path;
if (key === activeTabKey.value) { allTabs.value = allTabs.value.filter(item => item.key !== tab.key);
const targetTab = allTabs.value[tabIndex - 1] || allTabs.value.find(tab => tab.key === 'console');
activeTabKey.value = targetTab.key; if (isActive) {
componentError.value = false; const tabs = otherTabs.value;
if (tabs.length > 0) {
const targetTab = tabs[Math.min(tabIndex - 1, tabs.length - 1)];
router.push(targetTab.path);
} else {
router.push('/console');
}
} }
}; };
// 关闭所有标签页 // 8.5 关闭所有标签页
const closeAllTabs = () => { const closeAllTabs = () => {
allTabs.value = allTabs.value.filter(tab => tab.key === 'console'); allTabs.value = allTabs.value.filter(tab => tab.key === 'console');
activeTabKey.value = 'console'; router.push('/console');
selectedKey.value = '';
expandedParentMenus.value = [];
Object.keys(collapsedMenuVisible).forEach(key => collapsedMenuVisible[key] = false); Object.keys(collapsedMenuVisible).forEach(key => collapsedMenuVisible[key] = false);
componentError.value = false;
}; };
// 展开状态:子菜单项点击 // 8.6 标签页切换事件(路由已同步,无需额外处理)
const handleSubMenuItemClick = async (menu: MenuItem) => {
selectedKey.value = menu.menuCode;
componentError.value = false;
try {
// 检查标签页是否已存在
const existingTab = allTabs.value.find(tab => tab.key === menu.menuCode);
if (existingTab) {
// 标签页已存在,直接切换
activeTabKey.value = menu.menuCode;
return;
}
// 动态加载组件
const component = await getComponentByPath(menu.chref);
if (component) {
// 添加新标签页
allTabs.value.push({
key: menu.menuCode,
title: menu.menuName,
component: component,
closable: true,
chref: menu.chref
});
activeTabKey.value = menu.menuCode;
}
} catch (error) {
console.error('处理菜单点击失败:', error);
}
};
// 折叠状态:悬浮子菜单项点击
const handleCollapsedSubMenuClick = async (e: { key: string }, menus: MenuItem[]) => {
const menuCode = e.key;
const menu = menus.find(item => item.menuCode === menuCode);
if (menu) {
selectedKey.value = menuCode;
componentError.value = false;
try {
// 检查标签页是否已存在
const existingTab = allTabs.value.find(tab => tab.key === menuCode);
if (existingTab) {
// 标签页已存在,直接切换
activeTabKey.value = menuCode;
// 关闭所有悬浮菜单
Object.keys(collapsedMenuVisible).forEach(key => collapsedMenuVisible[key] = false);
return;
}
// 动态加载组件
const component = await getComponentByPath(menu.chref);
if (component) {
// 添加新标签页
allTabs.value.push({
key: menu.menuCode,
title: menu.menuName,
component: component,
closable: true,
chref: menu.chref
});
activeTabKey.value = menu.menuCode;
}
} catch (error) {
console.error('处理菜单点击失败:', error);
} finally {
// 点击后关闭所有悬浮菜单
Object.keys(collapsedMenuVisible).forEach(key => collapsedMenuVisible[key] = false);
}
}
};
// 标签页切换时同步菜单状态
const handleTabChange = (key: string) => { const handleTabChange = (key: string) => {
activeTabKey.value = key; const matchedTab = allTabs.value.find(tab => tab.key === key);
componentError.value = false; if (matchedTab) router.push(matchedTab.path);
if (key === 'console') {
selectedKey.value = '';
expandedParentMenus.value = [];
Object.keys(collapsedMenuVisible).forEach(k => collapsedMenuVisible[k] = false);
return;
}
// 同步选中状态
selectedKey.value = key;
// 展开状态:同步展开对应父菜单,关闭其他父菜单
if (!collapsed.value) {
moduleMenusConfig.value.forEach(module => {
const hasKey = module.menus.some(menu => menu.menuCode === key);
if (hasKey) {
// 只展开当前子菜单所属的父级
expandedParentMenus.value = [module.moduleCode];
}
});
}
}; };
// 用户菜单相关方法 // 9. 错误处理RouterView组件加载失败
const handleDropdownVisible = (visible: boolean) => { const handleRouterViewError = (err: Error) => {
console.log('用户下拉菜单状态:', visible ? '显示' : '隐藏'); componentError.value = true;
errorPath.value = route.path;
console.error('路由组件加载失败:', err);
};
// 10. 用户菜单与退出登录(优化退出跳转)
const handleDropdownVisible = (visible: boolean) => {
console.log('用户下拉菜单状态:', visible ? '显示' : '隐藏');
}; };
const handleUserMenuClick = ({ key }: { key: string }) => { const handleUserMenuClick = ({ key }: { key: string }) => {
if (key === 'logout') { if (key === 'logout') handleLogout();
handleLogout(); else console.log('用户菜单点击:', key);
} else {
console.log('用户菜单点击:', key);
}
}; };
const handleLogout = () => { const handleLogout = () => {
logoutVisible.value = true; logoutVisible.value = true;
}; };
const doLogout = () => { const doLogout = () => {
localStorage.removeItem('token'); localStorage.removeItem('token');
localStorage.removeItem('userInfo'); localStorage.removeItem('userInfo');
logoutVisible.value = false; logoutVisible.value = false;
alert('退出登录成功,即将跳转到登录页'); router.push('/login'); // 用路由跳转替代alert
// 实际项目中替换为router.push('/login');
}; };
// 监听折叠状态变化,重置菜单状态 // 11. 监听菜单折叠状态变化
watch(collapsed, (newVal) => { watch(collapsed, (newVal) => {
if (newVal) { if (newVal) expandedParentMenus.value = []; // 折叠时关闭所有父菜单
// 切换到折叠状态:关闭所有展开的子菜单 else syncMenuStateWithRoute(); // 展开时同步路由状态
expandedParentMenus.value = [];
} else {
// 切换到展开状态:关闭所有悬浮菜单
Object.keys(collapsedMenuVisible).forEach(key => collapsedMenuVisible[key] = false);
// 如果有选中的子菜单,展开对应父菜单,关闭其他父菜单
if (selectedKey.value) {
moduleMenusConfig.value.forEach(module => {
const hasKey = module.menus.some(menu => menu.menuCode === selectedKey.value);
if (hasKey) {
expandedParentMenus.value = [module.moduleCode];
}
});
}
}
}); });
</script> </script>
<style scoped> <style scoped>
@import "styles/app-layout.css"; @import "styles/app-layout.css";
</style> </style>

View File

@@ -1,53 +1,59 @@
// router/index.ts // router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
/* 1. 固定路由 */ // 1. 以相对路径写 glob相对于当前文件
const constantRoutes: RouteRecordRaw[] = [ const bizModules = import.meta.glob('../views/biz/**/*.vue')
// 2. 根据模块生成子路由
function buildBizRoutes() {
return Object.entries(bizModules).map(([filePath, asyncComp]) => {
const routePath = filePath
.replace(/^..\views\/biz/, '')
.replace(/\.vue$/, '')
.replace(/\/index$/i, '') || '/'
const routeName = routePath
.split('/')
.filter(Boolean)
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
.join('') || 'BizRoot'
return {
path: routePath,
name: routeName,
component: asyncComp,
meta: { requiresAuth: true }
}
})
}
// 3. 固定路由
const routes = [
{ path: '/', redirect: '/login' }, { path: '/', redirect: '/login' },
{ {
path: '/login', path: '/login',
name: 'Login', name: 'Login',
component: () => import('@/views/login/Login.vue') component: () => import('../views/login/Login.vue')
}, },
{ {
path: '/index', path: '/index',
name: 'Index', name: 'Index',
component: () => import('@/views/sys/index.vue'), component: () => import('../views/sys/index.vue'),
meta: { requiresAuth: true } meta: { requiresAuth: true },
children: buildBizRoutes()
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('../views/sys/NotFound.vue'),
meta: { hidden: true }
} }
] ]
/* 2. 自动扫描 views/biz 下的所有 .vue 文件 */ // 4. 创建路由实例
const bizModules = import.meta.glob('../views/biz/**/*.vue')
const bizRoutes: RouteRecordRaw[] = Object.entries(bizModules).map(
([filePath, asyncComp]) => {
// filePath 形如 ../views/biz/order/List.vue
// 去掉前缀和扩展名,得到 biz/order/List
const routePath = filePath
.replace('../views/biz/', '')
.replace('.vue', '')
.split('/')
.join('/')
// 路由名:把路径转成 PascalCase例如 OrderList
const routeName = routePath
.split('/')
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
.join('')
return {
path: `/biz/${routePath}`.replace(/\/+/g, '/'), // 防止双斜杠
name: routeName || 'BizIndex',
component: asyncComp
}
}
)
/* 3. 合并路由 */
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes: [...constantRoutes, ...bizRoutes] routes
}) })
export default router export default router

View File

@@ -0,0 +1,8 @@
<template>
</template>
<script>
</script>
<style>
</style>