重写复现方法
This commit is contained in:
45
capi-ui/src/api/menu.ts
Normal file
45
capi-ui/src/api/menu.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 菜单模块接口定义
|
||||
*/
|
||||
export interface MenuItem {
|
||||
menuId: string
|
||||
menuName: string
|
||||
menuCode: string
|
||||
moduleCode: string
|
||||
cicon: string
|
||||
chref: string
|
||||
ftenantId: string
|
||||
fflowId: string | null
|
||||
fflowTaskId: string | null
|
||||
fflowState: string | null
|
||||
}
|
||||
|
||||
export interface ModuleItem {
|
||||
moduleName: string
|
||||
moduleCode: string
|
||||
icon: string
|
||||
href: string
|
||||
menus: MenuItem[]
|
||||
}
|
||||
|
||||
export interface MenuResponse {
|
||||
code: number
|
||||
message: string
|
||||
result: ModuleItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模块菜单数据
|
||||
*/
|
||||
export const getModuleMenus = async (): Promise<ModuleItem[]> => {
|
||||
const response = await request.get<MenuResponse>('/Sys/login/getModules')
|
||||
|
||||
if (response.code !== 200) {
|
||||
console.error('获取菜单数据失败:', response.message)
|
||||
throw new Error(`获取菜单失败: ${response.message}`)
|
||||
}
|
||||
|
||||
return response.result || []
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
msg: String
|
||||
})
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<p>
|
||||
Welcome:
|
||||
<a href="https://hx.dcloud.net.cn/" target="_blank">HBuilderX</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://vitejs.dev/guide/features.html" target="_blank">
|
||||
Vite Documentation
|
||||
</a>
|
||||
|
|
||||
<a href="https://v3.vuejs.org/" target="_blank">Vue 3 Documentation</a>
|
||||
</p>
|
||||
|
||||
<button type="button" @click="count++">count is: {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test hot module replacement.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
652
capi-ui/src/components/Layout/AppLayout.vue
Normal file
652
capi-ui/src/components/Layout/AppLayout.vue
Normal file
@@ -0,0 +1,652 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<a-layout style="min-height: 100vh;">
|
||||
<!-- 顶部导航栏 -->
|
||||
<a-layout-header class="header">
|
||||
<div class="logo">
|
||||
<img src="/my.png" class="logo-img" alt="系统Logo" />
|
||||
<span class="system-name">cApi</span>
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
<div class="search-box">
|
||||
<SearchOutlined class="search-icon" />
|
||||
<input type="text" placeholder="搜索" class="search-input" />
|
||||
</div>
|
||||
|
||||
<div class="action-icons">
|
||||
<a-tooltip title="收藏">
|
||||
<StarOutlined class="action-icon" />
|
||||
</a-tooltip>
|
||||
<a-tooltip title="通知">
|
||||
<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>
|
||||
|
||||
<a-dropdown
|
||||
placement="bottomRight"
|
||||
:auto-adjust-overflow="true"
|
||||
@visible-change="handleDropdownVisible"
|
||||
>
|
||||
<div class="user-avatar">
|
||||
<img src="https://picsum.photos/id/1005/200/200" alt="用户头像" class="avatar-img" />
|
||||
</div>
|
||||
<template #overlay>
|
||||
<a-menu class="user-menu" @click="handleUserMenuClick">
|
||||
<a-menu-item key="profile">个人资料</a-menu-item>
|
||||
<a-menu-item key="settings">账户设置</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="logout" class="logout-menu-item">
|
||||
<LogoutOutlined class="mr-2" /> 退出登录
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</a-layout-header>
|
||||
|
||||
<!-- 中间主体布局 -->
|
||||
<a-layout style="flex: 1; height: calc(100vh - 64px);">
|
||||
<!-- 左侧菜单:折叠时悬浮子菜单,展开时点击展开 -->
|
||||
<a-layout-sider
|
||||
class="side-menu-container"
|
||||
width="200"
|
||||
:collapsed="collapsed"
|
||||
:trigger="null"
|
||||
collapsible
|
||||
:class="{ 'sider-collapsed': collapsed }"
|
||||
>
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="menu-loading">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
|
||||
<!-- 菜单内容 -->
|
||||
<div v-else class="menu-scroll-container">
|
||||
<!-- 父级菜单列表:根据折叠状态切换交互 -->
|
||||
<div
|
||||
v-for="(module, moduleKey) in moduleMenusConfig"
|
||||
:key="module.moduleCode"
|
||||
class="parent-menu-item"
|
||||
>
|
||||
<!-- 折叠状态:使用Dropdown实现悬浮子菜单 -->
|
||||
<a-dropdown
|
||||
v-if="collapsed"
|
||||
placement="right"
|
||||
:auto-adjust-overflow="true"
|
||||
:open="collapsedMenuVisible[module.moduleCode]"
|
||||
@openChange="(visible) => handleCollapsedMenuVisible(visible, module.moduleCode)"
|
||||
>
|
||||
<!-- 折叠时的父级菜单触发按钮 -->
|
||||
<div class="parent-menu-header collapsed-mode" @click.stop>
|
||||
<component :is="getIconComponent(module.icon)" class="parent-menu-icon" />
|
||||
<!-- 折叠时显示模块名称tooltip -->
|
||||
<a-tooltip :title="module.moduleName" placement="right">
|
||||
<span class="collapsed-tooltip-placeholder" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- 折叠时的悬浮子菜单内容 -->
|
||||
<template #overlay>
|
||||
<a-menu
|
||||
class="collapsed-sub-menu"
|
||||
@click="(e) => handleCollapsedSubMenuClick(e, module.menus)"
|
||||
>
|
||||
<a-menu-item
|
||||
v-for="menu in module.menus"
|
||||
:key="menu.menuCode"
|
||||
:selected="selectedKey === menu.menuCode"
|
||||
>
|
||||
<component :is="getIconComponent(menu.cicon)" class="sub-menu-icon" />
|
||||
<span>{{ menu.menuName }}</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
|
||||
<!-- 展开状态:原点击展开子菜单逻辑 -->
|
||||
<div v-else>
|
||||
<!-- 父级菜单标题(点击展开/关闭子菜单) -->
|
||||
<div
|
||||
class="parent-menu-header"
|
||||
@click="toggleSubMenu(module.moduleCode)"
|
||||
>
|
||||
<component :is="getIconComponent(module.icon)" class="parent-menu-icon" />
|
||||
<span class="parent-menu-title">{{ module.moduleName }}</span>
|
||||
<component
|
||||
:is="isSubMenuOpen(module.moduleCode) ? CaretUpOutlined : CaretDownOutlined"
|
||||
class="parent-menu-caret"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 子菜单列表(根据状态显示/隐藏) -->
|
||||
<div
|
||||
class="sub-menu-list"
|
||||
:class="{ 'sub-menu-visible': isSubMenuOpen(module.moduleCode) }"
|
||||
>
|
||||
<div
|
||||
v-for="menu in module.menus"
|
||||
:key="menu.menuCode"
|
||||
class="sub-menu-item"
|
||||
:class="{ 'sub-menu-item-active': selectedKey === menu.menuCode }"
|
||||
@click="handleSubMenuItemClick(menu)"
|
||||
>
|
||||
<component :is="getIconComponent(menu.cicon)" class="sub-menu-icon" />
|
||||
<span>{{ menu.menuName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 折叠/展开按钮 -->
|
||||
<div class="collapse-trigger" @click="collapsed = !collapsed">
|
||||
<component :is="collapsed ? MenuUnfoldOutlined : MenuFoldOutlined" />
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<a-layout-content class="right-content">
|
||||
<div class="tab-bar">
|
||||
<a-tag
|
||||
:class="['console-tab', activeTabKey === 'console' ? 'console-tab--active' : '']"
|
||||
@click="switchToConsole"
|
||||
>
|
||||
<HomeOutlined class="console-tab-icon" />
|
||||
<span>控制台</span>
|
||||
</a-tag>
|
||||
|
||||
<a-button
|
||||
size="small"
|
||||
class="tab-nav-btn"
|
||||
@click="switchTab('prev')"
|
||||
:disabled="currentTabIndex === 0 || otherTabs.length === 0"
|
||||
ghost
|
||||
>
|
||||
<
|
||||
</a-button>
|
||||
|
||||
<a-tabs
|
||||
v-model:activeKey="activeTabKey"
|
||||
@change="handleTabChange"
|
||||
class="google-tabs"
|
||||
:animated="false"
|
||||
hide-add
|
||||
>
|
||||
<a-tab-pane
|
||||
v-for="tab in otherTabs"
|
||||
:key="tab.key"
|
||||
:tab="renderTabTitle(tab)"
|
||||
/>
|
||||
</a-tabs>
|
||||
|
||||
<a-button
|
||||
size="small"
|
||||
class="tab-nav-btn"
|
||||
@click="switchTab('next')"
|
||||
:disabled="currentTabIndex === otherTabs.length - 1 || otherTabs.length === 0"
|
||||
ghost
|
||||
>
|
||||
>
|
||||
</a-button>
|
||||
|
||||
<a-button
|
||||
size="small"
|
||||
class="close-all-btn"
|
||||
@click="closeAllTabs"
|
||||
:disabled="otherTabs.length === 0"
|
||||
ghost
|
||||
>
|
||||
<CloseOutlined class="close-icon" /> 关闭所有
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-layout-content class="main-content">
|
||||
<div class="home-content">
|
||||
<!-- 加载状态显示 -->
|
||||
<div v-if="contentLoading" class="content-loading">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
|
||||
<!-- 组件内容显示 -->
|
||||
<component
|
||||
v-else
|
||||
:is="currentMainComponent"
|
||||
:key="activeTabKey"
|
||||
/>
|
||||
|
||||
<!-- 组件加载失败提示 -->
|
||||
<div v-if="componentError" class="component-error">
|
||||
<ExclamationCircleOutlined class="error-icon" />
|
||||
<p>组件加载失败,请检查路径是否正确</p>
|
||||
<p class="error-path">{{ errorPath }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a-layout-content>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
|
||||
<!-- 退出确认对话框 -->
|
||||
<a-modal
|
||||
v-model:open="logoutVisible"
|
||||
:centered="true"
|
||||
:closable="false"
|
||||
:mask-closable="false"
|
||||
:footer="null"
|
||||
:width="420"
|
||||
wrap-class-name="logout-modal-wrapper"
|
||||
>
|
||||
<div class="logout-body">
|
||||
<p class="logout-title">
|
||||
<ExclamationCircleOutlined style="color:#faad14; margin-right:8px;" />
|
||||
温馨提示
|
||||
</p>
|
||||
<p class="logout-desc">是否确认退出系统?</p>
|
||||
|
||||
<div class="logout-actions">
|
||||
<a-button size="large" @click="logoutVisible = false">取消</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="large"
|
||||
style="margin-left: 16px"
|
||||
@click="doLogout"
|
||||
>
|
||||
确定
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, h, reactive, watch, onMounted, defineAsyncComponent } from 'vue';
|
||||
import { Layout, Menu, Button, Tabs, Card, Tag, Dropdown, Modal, Tooltip, Spin } from 'ant-design-vue';
|
||||
import { getModuleMenus, ModuleItem, MenuItem } from '@/api/menu'
|
||||
|
||||
import {
|
||||
ApiOutlined,
|
||||
UserOutlined,
|
||||
CloseOutlined,
|
||||
LinkOutlined,
|
||||
LogoutOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
BarChartOutlined,
|
||||
DownloadOutlined,
|
||||
SettingOutlined,
|
||||
FormOutlined,
|
||||
HistoryOutlined,
|
||||
HomeOutlined,
|
||||
DatabaseOutlined,
|
||||
SearchOutlined,
|
||||
StarOutlined,
|
||||
BellOutlined,
|
||||
QuestionCircleOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
CaretUpOutlined,
|
||||
CaretDownOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
|
||||
// 导入控制台组件
|
||||
import Main from './Main.vue';
|
||||
|
||||
// 状态管理
|
||||
const collapsed = ref(false); // 菜单折叠状态
|
||||
const activeTabKey = ref('console'); // 当前激活的标签页
|
||||
const logoutVisible = ref(false); // 退出确认框显示状态
|
||||
const selectedKey = ref(''); // 当前选中的子菜单项
|
||||
const expandedParentMenus = ref<string[]>([]); // 展开状态下的子菜单展开记录
|
||||
const loading = ref(true); // 菜单加载状态
|
||||
const contentLoading = ref(false); // 内容区加载状态
|
||||
const componentError = ref(false); // 组件加载错误状态
|
||||
const errorPath = ref(''); // 错误路径记录
|
||||
|
||||
// 状态定义
|
||||
const moduleMenusConfig = ref<ModuleItem[]>([])
|
||||
// 初始化折叠状态下的菜单显示状态
|
||||
const collapsedMenuVisible = reactive<Record<string, boolean>>({})
|
||||
|
||||
// 标签页数据类型定义
|
||||
interface TabItem {
|
||||
key: string;
|
||||
title: string;
|
||||
component: any;
|
||||
closable: boolean;
|
||||
chref: string; // 存储组件路径
|
||||
}
|
||||
|
||||
// 标签页数据
|
||||
const allTabs = ref<TabItem[]>([
|
||||
{ key: 'console', title: '控制台', component: Main, closable: false, chref: '' }
|
||||
]);
|
||||
|
||||
// 计算属性
|
||||
const otherTabs = computed(() => allTabs.value.filter(tab => tab.key !== 'console'));
|
||||
const currentTabIndex = computed(() => otherTabs.value.findIndex(tab => tab.key === activeTabKey.value));
|
||||
const currentMainComponent = computed(() => {
|
||||
try {
|
||||
const targetTab = allTabs.value.find(tab => tab.key === activeTabKey.value);
|
||||
return targetTab ? targetTab.component : null;
|
||||
} catch (error) {
|
||||
console.error('组件加载失败:', error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// 从接口获取菜单数据
|
||||
const fetchMenuData = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const modules = await getModuleMenus()
|
||||
|
||||
moduleMenusConfig.value = modules
|
||||
// 初始化折叠状态下的菜单显示状态
|
||||
modules.forEach(module => {
|
||||
collapsedMenuVisible[module.moduleCode] = false
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('菜单接口请求失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 页面加载时获取菜单数据
|
||||
onMounted(() => {
|
||||
fetchMenuData();
|
||||
});
|
||||
|
||||
// 根据图标名称获取图标组件
|
||||
const getIconComponent = (iconName: string) => {
|
||||
const iconMap: Record<string, any> = {
|
||||
ApiOutlined,
|
||||
UserOutlined,
|
||||
BarChartOutlined,
|
||||
DownloadOutlined,
|
||||
SettingOutlined,
|
||||
FormOutlined,
|
||||
HistoryOutlined,
|
||||
HomeOutlined,
|
||||
DatabaseOutlined,
|
||||
LinkOutlined
|
||||
};
|
||||
|
||||
return iconMap[iconName] || ApiOutlined;
|
||||
};
|
||||
|
||||
// 根据chref路径获取组件
|
||||
const getComponentByPath = async (chref: string) => {
|
||||
try {
|
||||
// 显示加载状态
|
||||
contentLoading.value = true;
|
||||
componentError.value = false;
|
||||
|
||||
// 添加@vite-ignore注释告诉Vite忽略动态导入分析
|
||||
const component = defineAsyncComponent(() =>
|
||||
import(/* @vite-ignore */ `@/views/${chref}.vue`)
|
||||
);
|
||||
|
||||
return component;
|
||||
} catch (error) {
|
||||
console.error(`加载组件失败: @/views/${chref}.vue`, error);
|
||||
componentError.value = true;
|
||||
errorPath.value = `@/views/${chref}.vue`;
|
||||
return null;
|
||||
} finally {
|
||||
contentLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 检查展开状态下子菜单是否打开
|
||||
const isSubMenuOpen = (moduleCode: string) => {
|
||||
return expandedParentMenus.value.includes(moduleCode);
|
||||
};
|
||||
|
||||
// 【优化核心】展开状态:切换子菜单展开/关闭,点击当前父级时关闭其他父级
|
||||
const toggleSubMenu = (moduleCode: string) => {
|
||||
if (isSubMenuOpen(moduleCode)) {
|
||||
// 当前父级已展开,点击则关闭
|
||||
expandedParentMenus.value = [];
|
||||
} else {
|
||||
// 当前父级未展开,点击则打开当前并关闭其他所有父级
|
||||
expandedParentMenus.value = [moduleCode];
|
||||
}
|
||||
};
|
||||
|
||||
// 折叠状态:控制悬浮菜单显示/隐藏
|
||||
const handleCollapsedMenuVisible = (visible: boolean, moduleCode: string) => {
|
||||
// 关闭其他所有悬浮菜单
|
||||
Object.keys(collapsedMenuVisible).forEach(key => {
|
||||
if (key !== moduleCode) collapsedMenuVisible[key] = false;
|
||||
});
|
||||
// 更新当前菜单状态
|
||||
collapsedMenuVisible[moduleCode] = visible;
|
||||
};
|
||||
|
||||
// 标签页标题渲染
|
||||
const renderTabTitle = (tab: TabItem) => {
|
||||
return h('div', { class: 'tab-title-container' }, [
|
||||
h('span', { class: 'tab-title-text' }, tab.title),
|
||||
tab.closable && h(
|
||||
'span',
|
||||
{
|
||||
class: 'tab-close-btn',
|
||||
onClick: (e: Event) => {
|
||||
e.stopPropagation();
|
||||
deleteTab(tab.key);
|
||||
}
|
||||
},
|
||||
h(CloseOutlined, { size: 12 })
|
||||
)
|
||||
]);
|
||||
};
|
||||
|
||||
// 切换到控制台
|
||||
const switchToConsole = () => {
|
||||
activeTabKey.value = 'console';
|
||||
selectedKey.value = '';
|
||||
componentError.value = false;
|
||||
// 关闭所有展开的子菜单
|
||||
expandedParentMenus.value = [];
|
||||
// 关闭所有折叠状态的悬浮菜单
|
||||
Object.keys(collapsedMenuVisible).forEach(key => collapsedMenuVisible[key] = false);
|
||||
};
|
||||
|
||||
// 标签页切换(上一个/下一个)
|
||||
const switchTab = (direction: 'prev' | 'next') => {
|
||||
componentError.value = false;
|
||||
if (direction === 'prev' && currentTabIndex.value > 0) {
|
||||
activeTabKey.value = otherTabs.value[currentTabIndex.value - 1].key;
|
||||
} else if (direction === 'next' && currentTabIndex.value < otherTabs.value.length - 1) {
|
||||
activeTabKey.value = otherTabs.value[currentTabIndex.value + 1].key;
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭单个标签页
|
||||
const deleteTab = (key: string) => {
|
||||
if (key === 'console') return;
|
||||
const tabIndex = allTabs.value.findIndex(tab => tab.key === key);
|
||||
if (tabIndex === -1) return;
|
||||
|
||||
allTabs.value = allTabs.value.filter(tab => tab.key !== key);
|
||||
|
||||
if (key === activeTabKey.value) {
|
||||
const targetTab = allTabs.value[tabIndex - 1] || allTabs.value.find(tab => tab.key === 'console');
|
||||
activeTabKey.value = targetTab.key;
|
||||
componentError.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭所有标签页
|
||||
const closeAllTabs = () => {
|
||||
allTabs.value = allTabs.value.filter(tab => tab.key === 'console');
|
||||
activeTabKey.value = 'console';
|
||||
selectedKey.value = '';
|
||||
expandedParentMenus.value = [];
|
||||
Object.keys(collapsedMenuVisible).forEach(key => collapsedMenuVisible[key] = false);
|
||||
componentError.value = false;
|
||||
};
|
||||
|
||||
// 展开状态:子菜单项点击
|
||||
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) => {
|
||||
activeTabKey.value = key;
|
||||
componentError.value = false;
|
||||
|
||||
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];
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 用户菜单相关方法
|
||||
const handleDropdownVisible = (visible: boolean) => {
|
||||
console.log('用户下拉菜单状态:', visible ? '显示' : '隐藏');
|
||||
};
|
||||
|
||||
const handleUserMenuClick = ({ key }: { key: string }) => {
|
||||
if (key === 'logout') {
|
||||
handleLogout();
|
||||
} else {
|
||||
console.log('用户菜单点击:', key);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
logoutVisible.value = true;
|
||||
};
|
||||
|
||||
const doLogout = () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('userInfo');
|
||||
logoutVisible.value = false;
|
||||
alert('退出登录成功,即将跳转到登录页');
|
||||
// 实际项目中替换为:router.push('/login');
|
||||
};
|
||||
|
||||
// 监听折叠状态变化,重置菜单状态
|
||||
watch(collapsed, (newVal) => {
|
||||
if (newVal) {
|
||||
// 切换到折叠状态:关闭所有展开的子菜单
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
@import "styles/app-layout.css";
|
||||
</style>
|
||||
9
capi-ui/src/components/Layout/Main.vue
Normal file
9
capi-ui/src/components/Layout/Main.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
jjjjjjjjj
|
||||
</template>
|
||||
|
||||
<script>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
511
capi-ui/src/components/Layout/styles/app-layout.css
Normal file
511
capi-ui/src/components/Layout/styles/app-layout.css
Normal file
@@ -0,0 +1,511 @@
|
||||
/* 基础样式 */
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body, .app-container { height: 100vh; overflow: hidden; }
|
||||
.app-container { display: flex; flex-direction: column; }
|
||||
|
||||
/* 顶部导航栏样式 */
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
height: 64px;
|
||||
width: 100%;
|
||||
background: linear-gradient(90deg, #e6f7ff 0%, #f0f9ff 100%);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 52px;
|
||||
border-right: 0px solid rgba(24, 144, 255, 0.1);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
width: 65px;
|
||||
height: 65px;
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.system-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.header-actions { display: flex; align-items: center; gap: 16px; }
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 4px;
|
||||
padding: 0 12px;
|
||||
height: 36px;
|
||||
width: 240px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.search-icon { color: #8c8c8c; margin-right: 8px; }
|
||||
.search-input {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: #333;
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
}
|
||||
.search-input::placeholder { color: #b3b3b3; }
|
||||
|
||||
.action-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding-right: 16px;
|
||||
border-right: 1px solid rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
color: #1890ff;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.action-icon:hover { color: #096dd9; transform: scale(1.1); }
|
||||
|
||||
.user-avatar { cursor: pointer; }
|
||||
.avatar-img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.user-avatar:hover .avatar-img {
|
||||
border-color: #1890ff;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
.user-menu { width: 160px; border-radius: 6px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); }
|
||||
.logout-menu-item { color: #f5222d !important; }
|
||||
|
||||
/* 左侧菜单样式 - 核心区域 */
|
||||
.side-menu-container {
|
||||
background: #fff;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 10; /* 确保菜单在内容上方 */
|
||||
}
|
||||
|
||||
/* 折叠状态的侧边栏 */
|
||||
.sider-collapsed {
|
||||
width: 64px !important;
|
||||
}
|
||||
|
||||
/* 菜单加载状态 */
|
||||
.menu-loading {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 菜单滚动容器 */
|
||||
.menu-scroll-container {
|
||||
flex: 1;
|
||||
padding: 16px 0;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 60px; /* 为底部按钮留出空间 */
|
||||
}
|
||||
|
||||
/* 父级菜单项容器 */
|
||||
.parent-menu-item {
|
||||
margin-bottom: 4px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 父级菜单标题基础样式 */
|
||||
.parent-menu-header {
|
||||
height: 44px;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
transition: all 0.2s ease;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
/* 展开状态父菜单交互 */
|
||||
.parent-menu-header:hover {
|
||||
background-color: #f5f9ff;
|
||||
border-left-color: #1890ff;
|
||||
}
|
||||
|
||||
/* 折叠状态父菜单样式 */
|
||||
.parent-menu-header.collapsed-mode {
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border-left: none;
|
||||
height: 48px; /* 增大点击区域 */
|
||||
width: 100%;
|
||||
}
|
||||
.parent-menu-header.collapsed-mode:hover {
|
||||
background-color: #f5f9ff;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
/* 折叠状态tooltip占位元素 */
|
||||
.collapsed-tooltip-placeholder {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* 父级菜单图标 */
|
||||
.parent-menu-icon {
|
||||
color: #1890ff;
|
||||
font-size: 16px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
/* 折叠状态图标 */
|
||||
.parent-menu-header.collapsed-mode .parent-menu-icon {
|
||||
margin-right: 0;
|
||||
font-size: 20px; /* 折叠时图标放大 */
|
||||
}
|
||||
|
||||
/* 父级菜单文字 */
|
||||
.parent-menu-title {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 父级菜单箭头 */
|
||||
.parent-menu-caret {
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* 展开状态子菜单列表容器 */
|
||||
.sub-menu-list {
|
||||
background-color: #fafafa;
|
||||
overflow: hidden;
|
||||
max-height: 0;
|
||||
transition: max-height 0.3s ease-out;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
/* 展开状态子菜单显示 */
|
||||
.sub-menu-visible {
|
||||
max-height: 500px;
|
||||
transition: max-height 0.3s ease-in;
|
||||
}
|
||||
|
||||
/* 展开状态子菜单项样式 */
|
||||
.sub-menu-item {
|
||||
height: 38px;
|
||||
padding: 0 16px 0 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
border-left: 2px solid transparent;
|
||||
}
|
||||
|
||||
/* 子菜单图标 */
|
||||
.sub-menu-icon {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* 展开状态子菜单项交互 */
|
||||
.sub-menu-item:hover {
|
||||
background-color: #f0f7ff;
|
||||
color: #1890ff;
|
||||
padding-left: 42px;
|
||||
border-left-color: #1890ff;
|
||||
}
|
||||
.sub-menu-item:hover .sub-menu-icon {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 子菜单项选中样式 */
|
||||
.sub-menu-item-active {
|
||||
background-color: #e6f4ff;
|
||||
color: #1890ff;
|
||||
font-weight: 500;
|
||||
padding-left: 42px;
|
||||
border-left-color: #1890ff;
|
||||
}
|
||||
.sub-menu-item-active .sub-menu-icon {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 折叠状态:悬浮子菜单样式 */
|
||||
:deep(.collapsed-sub-menu) {
|
||||
min-width: 180px !important;
|
||||
border-radius: 6px !important;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15) !important;
|
||||
padding: 6px 0 !important;
|
||||
z-index: 999 !important; /* 确保悬浮菜单在最上层 */
|
||||
}
|
||||
:deep(.collapsed-sub-menu .ant-menu-item) {
|
||||
height: 40px !important;
|
||||
line-height: 40px !important;
|
||||
padding: 0 16px !important;
|
||||
transition: all 0.2s ease !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 8px !important;
|
||||
}
|
||||
:deep(.collapsed-sub-menu .ant-menu-item:hover) {
|
||||
background-color: #f5f9ff !important;
|
||||
color: #1890ff !important;
|
||||
}
|
||||
:deep(.collapsed-sub-menu .ant-menu-item-selected) {
|
||||
background-color: #e6f4ff !important;
|
||||
color: #1890ff !important;
|
||||
}
|
||||
:deep(.collapsed-sub-menu .ant-menu-item .anticon) {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
/* 折叠/展开按钮 */
|
||||
.collapse-trigger {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
right: -18px;
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 50%;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
z-index: 11; /* 确保按钮在菜单上方 */
|
||||
color: #1890ff;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.collapse-trigger:hover {
|
||||
color: #096dd9;
|
||||
box-shadow: 0 2px 4px rgba(24, 144, 255, 0.15);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 右侧内容区样式 */
|
||||
.right-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
padding: 0 20px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.console-tab {
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #666;
|
||||
background: transparent;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.console-tab-icon { margin-right: 6px; font-size: 14px; }
|
||||
.console-tab--active {
|
||||
background: #e8f4ff !important;
|
||||
color: #1890ff;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 1px 2px rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
.console-tab:hover:not(.console-tab--active) {
|
||||
background: #f5f9ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.tab-nav-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.tab-nav-btn:hover { background: #f5f9ff; color: #1890ff; }
|
||||
.tab-nav-btn:disabled { color: #ccc !important; cursor: not-allowed; background: transparent !important; }
|
||||
|
||||
.google-tabs { flex: 1; overflow: hidden; }
|
||||
:deep(.ant-tabs-nav) { height: 100%; margin: 0 !important; }
|
||||
:deep(.ant-tabs-nav-list) { height: 100%; align-items: center; }
|
||||
:deep(.ant-tabs-tab) {
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
margin: 0 2px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
background: transparent;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
:deep(.ant-tabs-tab:hover:not(.ant-tabs-tab-active)) { background: #f5f9ff; color: #1890ff; }
|
||||
:deep(.ant-tabs-tab.ant-tabs-tab-active) {
|
||||
background: #e8f4ff !important;
|
||||
color: #1890ff !important;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 1px 2px rgba(24, 144, 255, 0.1);
|
||||
border-bottom: 2px solid #1890ff;
|
||||
}
|
||||
:deep(.ant-tabs-ink-bar) { display: none; }
|
||||
|
||||
.tab-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 100%;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.tab-title-text { white-space: nowrap; }
|
||||
.tab-close-btn {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.tab-title-container:hover .tab-close-btn { display: inline-flex; }
|
||||
:deep(.ant-tabs-tab-active) .tab-close-btn { color: #1890ff; }
|
||||
.tab-close-btn:hover { background: #d1eaff; color: #1890ff; }
|
||||
|
||||
.close-all-btn {
|
||||
color: #666;
|
||||
margin-left: 2px;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.close-all-btn:hover { color: #1890ff; background: #f5f9ff; }
|
||||
.close-all-btn:disabled { color: #ccc !important; cursor: not-allowed; background: transparent !important; }
|
||||
.close-icon { margin-right: 4px; font-size: 12px; }
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.home-content {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
min-height: calc(100vh - 64px - 44px - 32px);
|
||||
}
|
||||
|
||||
/* 内容加载状态 */
|
||||
.content-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
/* 组件加载错误样式 */
|
||||
.component-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
color: #f5222d;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.error-path {
|
||||
font-family: monospace;
|
||||
margin-top: 8px;
|
||||
color: #8c8c8c;
|
||||
background: #f5f5f5;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
word-break: break-all;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
/* 退出对话框样式 */
|
||||
:deep(.logout-modal-wrapper .ant-modal) { width: 480px !important; }
|
||||
.logout-body { text-align: center; padding: 8px 0 4px; }
|
||||
.logout-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
text-align: left;
|
||||
color: #333;
|
||||
}
|
||||
.logout-desc {
|
||||
font-size: 16px;
|
||||
color: #555;
|
||||
margin-bottom: 32px;
|
||||
text-align: left;
|
||||
}
|
||||
.logout-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
@@ -1,26 +1,30 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
// router/index.ts
|
||||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/login'
|
||||
},
|
||||
/* 1. 固定路由 */
|
||||
const constantRoutes: RouteRecordRaw[] = [
|
||||
{ path: '/', redirect: '/login' },
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/login/Login.vue')
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/sys/Dashboard.vue'),
|
||||
path: '/index',
|
||||
name: 'Index',
|
||||
component: () => import('@/views/sys/index.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/biz/data/index', // 路由PATH,与chref对应
|
||||
name: 'DataIndex',
|
||||
component: () => import('@/biz/data/index.vue') // 对应的组件
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes
|
||||
routes: [...constantRoutes]
|
||||
})
|
||||
|
||||
export default router
|
||||
162
capi-ui/src/views/biz/data/index.vue
Normal file
162
capi-ui/src/views/biz/data/index.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<!-- 根容器:100%填充父容器(home-content),确保自适应 -->
|
||||
<div class="empty-page-container">
|
||||
<!-- 空白页核心内容区:垂直居中 -->
|
||||
<div class="empty-content">
|
||||
<!-- 空状态图标 -->
|
||||
<div class="empty-icon">
|
||||
<FileTextOutlined />
|
||||
</div>
|
||||
|
||||
<!-- 标题与描述 -->
|
||||
<h2 class="empty-title">这是一个空白页面</h2>
|
||||
<p class="empty-desc">
|
||||
你可以在这里扩展功能,比如添加数据展示、表单提交、图表分析等内容<br>
|
||||
页面已适配父容器尺寸,支持高度/宽度自适应
|
||||
</p>
|
||||
|
||||
<!-- 操作按钮:提供基础交互入口 -->
|
||||
<div class="empty-actions">
|
||||
<a-button type="primary" @click="handleCreate">
|
||||
<PlusOutlined class="mr-2" /> 创建内容
|
||||
</a-button>
|
||||
<a-button @click="handleGuide" style="margin-left: 12px;">
|
||||
<QuestionCircleOutlined class="mr-2" /> 查看使用指南
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 辅助提示:展示页面基础信息 -->
|
||||
<div class="empty-meta">
|
||||
<span class="meta-item">页面路径:@/views/EmptyTemplatePage.vue</span>
|
||||
<span class="meta-item">适配场景:列表页、详情页、表单页等初始模板</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
// 引入必要组件(与原项目组件库一致)
|
||||
import { Button } from 'ant-design-vue';
|
||||
import { FileTextOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
// 页面基础状态(可根据需求扩展)
|
||||
const pageName = ref('空白模板页面');
|
||||
|
||||
// 按钮交互事件(示例:可根据业务逻辑修改)
|
||||
const handleCreate = () => {
|
||||
console.log(`[${pageName.value}] 触发「创建内容」操作`);
|
||||
// 实际场景可扩展:打开创建弹窗、跳转表单页面等
|
||||
alert('可在此处实现「创建内容」的业务逻辑');
|
||||
};
|
||||
|
||||
const handleGuide = () => {
|
||||
console.log(`[${pageName.value}] 查看使用指南`);
|
||||
// 实际场景可扩展:打开帮助文档、显示引导弹窗等
|
||||
alert('使用指南:\n1. 此页面已适配父容器自适应尺寸\n2. 可在 empty-content 内添加业务组件\n3. 样式可参考原项目规范调整');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 根容器:100%填充父容器,确保自适应 */
|
||||
.empty-page-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 空白页内容区:居中展示,控制宽高比例 */
|
||||
.empty-content {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
padding: 48px 24px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* 空状态图标:突出显示,统一风格 */
|
||||
.empty-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background-color: #e8f4ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.empty-icon :deep(svg) {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
/* 标题样式:与原项目标题层级一致 */
|
||||
.empty-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 描述文本:浅色、易读 */
|
||||
.empty-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 操作按钮区:间距控制 */
|
||||
.empty-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* 辅助信息:弱化显示,底部对齐 */
|
||||
.empty-meta {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 响应式:小屏幕适配 */
|
||||
@media (max-width: 768px) {
|
||||
.empty-content {
|
||||
padding: 32px 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.empty-icon :deep(svg) {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.empty-meta {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
12
capi-ui/src/views/sys/index.vue
Normal file
12
capi-ui/src/views/sys/index.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<!-- src/views/sys/index.vue -->
|
||||
<template>
|
||||
<AppLayout />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppLayout from '@/components/Layout/AppLayout.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 可选:局部样式覆盖 */
|
||||
</style>
|
||||
@@ -1,4 +1,6 @@
|
||||
<template>
|
||||
|
||||
ffffffffffffffffffff
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -59,4 +59,5 @@ public class ApiModule implements Serializable {
|
||||
*/
|
||||
@TableField("f_flow_state")
|
||||
private Integer fFlowState;
|
||||
|
||||
}
|
||||
|
||||
31
src/main/java/com/mini/capi/sys/domain/ModuleMenuDTO.java
Normal file
31
src/main/java/com/mini/capi/sys/domain/ModuleMenuDTO.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.mini.capi.sys.domain;
|
||||
|
||||
import com.mini.capi.biz.domain.ApiMenus;
|
||||
import com.mini.capi.biz.domain.ApiModule;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class ModuleMenuDTO implements Serializable {
|
||||
|
||||
private String moduleName;
|
||||
private String moduleCode;
|
||||
private String icon;
|
||||
private String href;
|
||||
|
||||
List<ApiMenus> menus = new ArrayList<>();
|
||||
|
||||
public ModuleMenuDTO() {
|
||||
}
|
||||
|
||||
public ModuleMenuDTO(String moduleName,String moduleCode,String icon,String href,List<ApiMenus> menus){
|
||||
this.moduleName = moduleName;
|
||||
this.moduleCode = moduleCode;
|
||||
this.icon = icon;
|
||||
this.href = href;
|
||||
this.menus = menus;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.mini.capi.sys.pageController;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.mini.capi.biz.domain.ApiMenus;
|
||||
import com.mini.capi.biz.domain.ApiModule;
|
||||
@@ -8,13 +7,16 @@ import com.mini.capi.biz.domain.ApiUser;
|
||||
import com.mini.capi.biz.service.ApiMenusService;
|
||||
import com.mini.capi.biz.service.ApiModuleService;
|
||||
import com.mini.capi.biz.service.ApiUserService;
|
||||
import com.mini.capi.model.ApiResult;
|
||||
import com.mini.capi.model.auth.Result;
|
||||
import com.mini.capi.sys.domain.ModuleMenuDTO;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import lombok.Data;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -98,20 +100,21 @@ public class loginPageController {
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/modules")
|
||||
public List<ApiModule> modules() {
|
||||
return moduleService.list();
|
||||
@GetMapping("/getModules")
|
||||
public ApiResult<List<ModuleMenuDTO>> getModules() {
|
||||
List<ApiModule> apiModules = moduleService.list();
|
||||
List<ModuleMenuDTO> menuDTOList = new ArrayList<>();
|
||||
for (ApiModule apiModule : apiModules) {
|
||||
ModuleMenuDTO menuDTO = new ModuleMenuDTO(apiModule.getModuleName(), apiModule.getModuleCode(), apiModule.getCIcon(), apiModule.getCHref(), getMenus(apiModule.getModuleCode()));
|
||||
menuDTOList.add(menuDTO);
|
||||
}
|
||||
return ApiResult.success(menuDTOList);
|
||||
}
|
||||
|
||||
@GetMapping("/menus")
|
||||
public List<ApiMenus> menus(@RequestParam String moduleCode) {
|
||||
|
||||
public List<ApiMenus> getMenus(String moduleCode) {
|
||||
QueryWrapper<ApiMenus> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("module_code", moduleCode);
|
||||
return menusService.list(queryWrapper);
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
public ApiUser me(HttpSession session) {
|
||||
return (ApiUser) session.getAttribute("Authorization");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user