重写复现方法

This commit is contained in:
2025-09-01 18:18:44 +08:00
parent 7c11c1519b
commit 5d3d515b8e
41 changed files with 4249 additions and 5952 deletions

View File

@@ -0,0 +1,757 @@
<template>
<div class="app-container">
<a-layout style="min-height: 100%;">
<!-- 顶部导航栏 -->
<a-layout-header class="header">
<div class="logo">
<ApiOutlined />
<span>Capi系统</span>
</div>
<a-menu
theme="light"
mode="horizontal"
:selectedKeys="selectedKey"
class="menu"
@click="handleMenuClick"
>
<a-menu-item key="api">API管理</a-menu-item>
<a-menu-item key="data">数据管理</a-menu-item>
<a-menu-item key="system">系统管理</a-menu-item>
</a-menu>
<!-- 管理员区域下拉菜单 -->
<div class="admin">
<a-dropdown
placement="bottomRight"
:auto-adjust-overflow="true"
@visible-change="handleDropdownVisible"
>
<span class="admin-btn">
<UserOutlined /> 管理员
<DownOutlined class="ml-2" />
</span>
<template #overlay>
<a-menu class="logout-menu" @click="handleLogout" selected-keys="">
<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>
<!-- 标签栏始终显示 -->
<div class="tab-bar">
<a-tag
:class="['console-tab', activeTabKey === 'console' ? 'console-tab--active' : '']"
@click="switchToConsole"
>
<span>控制台</span>
</a-tag>
<a-button
size="small"
class="tab-nav-btn"
@click="switchTab('prev')"
:disabled="currentTabIndex === 0 || otherTabs.length === 0"
ghost
>
&lt;
</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
>
&gt;
</a-button>
<a-button
size="small"
class="close-all-btn"
@click="closeAllTabs"
:disabled="otherTabs.length === 0"
ghost
>
<CloseOutlined class="close-icon" /> 关闭所有
</a-button>
</div>
<!-- 内容区域统一通过component渲染含控制台 -->
<a-layout-content class="content">
<div class="home-content">
<component
:is="currentMainComponent"
:key="activeTabKey"
/>
</div>
</a-layout-content>
</a-layout>
<!-- 模块菜单弹窗80%宽度 -->
<a-modal
v-model:open="moduleModalVisible"
:title="currentModuleName"
:width="`80%`"
:centered="true"
:mask-closable="false"
@cancel="moduleModalVisible = false"
wrap-class-name="module-modal-wrapper"
>
<div class="module-menu-container">
<a-row :gutter="[{ xs: 16, sm: 24 }, { xs: 16, sm: 24 }]">
<a-col
v-for="menu in currentModuleMenus"
:key="menu.key"
:xs="12"
:sm="8"
:md="6"
:lg="4"
:xl="3"
>
<a-card
class="menu-card"
@click="handleMenuCardClick(menu)"
hoverable
bordered
>
<div class="menu-card-inner">
<component :is="menu.icon" class="menu-icon" />
<div class="menu-title">{{ menu.title }}</div>
</div>
</a-card>
</a-col>
</a-row>
</div>
</a-modal>
<!-- 退出确认对话框 -->
<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>
import { ref, computed, h } from 'vue';
import { Layout, Menu, Button, Tabs, Card, Tag, Dropdown, Modal, Row, Col } from 'ant-design-vue';
import {
ApiOutlined,
UserOutlined,
CloseOutlined,
DownOutlined,
LogoutOutlined,
ExclamationCircleOutlined,
BarChartOutlined,
DownloadOutlined,
SettingOutlined,
FormOutlined,
HistoryOutlined,
UserAddOutlined
} from '@ant-design/icons-vue';
// 导入Main组件控制台要展示的内容
import Main from './main.vue'; // 假设main.vue与当前文件在同一目录
// 模拟业务组件(实际项目中替换为真实组件)
const MockComponent = (props) => {
return h('div', { style: { padding: '20px' } }, [
h('h3', `这是${props.title}页面`),
h('p', '此处将展示该功能模块的具体内容')
]);
};
// 模块与菜单映射配置
const moduleMenusConfig = {
api: {
name: 'API管理',
menus: [
{
key: 'api-list',
title: 'API列表',
icon: ApiOutlined,
component: () => h(MockComponent, { title: 'API列表' }),
closable: true
},
{
key: 'api-add',
title: 'API创建',
icon: FormOutlined,
component: () => h(MockComponent, { title: 'API创建' }),
closable: true
},
{
key: 'api-history',
title: '调用记录',
icon: HistoryOutlined,
component: () => h(MockComponent, { title: 'API调用记录' }),
closable: true
}
]
},
data: {
name: '数据管理',
menus: [
{
key: 'data-analytics',
title: '数据分析',
icon: BarChartOutlined,
component: () => h(MockComponent, { title: '数据分析' }),
closable: true
},
{
key: 'data-export',
title: '数据导出',
icon: DownloadOutlined,
component: () => h(MockComponent, { title: '数据导出' }),
closable: true
}
]
},
system: {
name: '系统管理',
menus: [
{
key: 'system-settings',
title: '系统设置',
icon: SettingOutlined,
component: () => h(MockComponent, { title: '系统设置' }),
closable: true
},
{
key: 'user-manage',
title: '用户管理',
icon: UserOutlined,
component: () => h(MockComponent, { title: '用户管理' }),
closable: true
},
{
key: 'user-add',
title: '用户新增',
icon: UserAddOutlined,
component: () => h(MockComponent, { title: '用户新增' }),
closable: true
}
]
}
};
// 状态管理
const selectedKey = ref(['api']);
const moduleModalVisible = ref(false);
const currentModuleKey = ref('');
const activeTabKey = ref('console');
// 标签页数据初始包含控制台控制台组件使用Main.vue
const allTabs = ref([
{
key: 'console',
title: '控制台',
component: Main, // 这里使用导入的Main组件
closable: false // 控制台标签不可关闭
}
]);
// 计算属性
const otherTabs = computed(() =>
allTabs.value.filter(tab => tab.key !== 'console')
);
const currentTabIndex = computed(() =>
otherTabs.value.findIndex(tab => tab.key === activeTabKey.value)
);
const currentModuleName = computed(() =>
moduleMenusConfig[currentModuleKey.value]?.name || '功能菜单'
);
const currentModuleMenus = computed(() =>
moduleMenusConfig[currentModuleKey.value]?.menus || []
);
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 renderTabTitle = (tab) => {
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) => {
e.stopPropagation();
deleteTab(tab.key);
}
},
h(CloseOutlined, { size: 12 })
)
]);
};
const switchToConsole = () => {
activeTabKey.value = 'console';
};
const switchTab = (direction) => {
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) => {
// 禁止删除控制台标签
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;
}
};
const closeAllTabs = () => {
// 只保留控制台标签
allTabs.value = allTabs.value.filter(tab => tab.key === 'console');
activeTabKey.value = 'console';
};
const handleTabChange = (key) => {
activeTabKey.value = key;
const tabToModuleMap = {
'api-list': 'api',
'api-add': 'api',
'api-history': 'api',
'data-analytics': 'data',
'data-export': 'data',
'system-settings': 'system',
'user-manage': 'system',
'user-add': 'system'
};
selectedKey.value = [tabToModuleMap[key] || 'api'];
};
const handleMenuClick = ({ key }) => {
currentModuleKey.value = key;
moduleModalVisible.value = true;
selectedKey.value = [key];
};
const handleMenuCardClick = (menuItem) => {
moduleModalVisible.value = false;
// 检查标签是否已存在,不存在则添加
const isTabExists = allTabs.value.some(tab => tab.key === menuItem.key);
if (!isTabExists) {
allTabs.value.push({
key: menuItem.key,
title: menuItem.title,
component: menuItem.component,
closable: menuItem.closable
});
}
// 切换到该标签
activeTabKey.value = menuItem.key;
};
const handleDropdownVisible = (visible) => {
console.log('下拉菜单状态:', visible ? '显示' : '隐藏');
};
const logoutVisible = ref(false);
const handleLogout = () => {
logoutVisible.value = true;
};
const doLogout = () => {
localStorage.removeItem('token');
localStorage.removeItem('userInfo');
logoutVisible.value = false;
alert('退出登录成功,即将跳转到登录页');
};
</script>
<style scoped>
/* 样式内容与之前保持一致 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body,
.app-container {
height: 100vh;
overflow: hidden;
}
.app-container {
display: flex;
flex-direction: column;
}
/* 顶部导航 */
.header {
background: #fff;
border-bottom: 1px solid #e0e0e0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
height: 64px;
width: 100%;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.logo {
display: flex;
align-items: center;
font-size: 18px;
font-weight: 600;
color: #1890ff;
}
.logo span {
margin-left: 8px;
}
.menu {
flex: 1;
margin: 0 40px;
justify-content: center;
}
.admin {
display: flex;
align-items: center;
}
.admin-btn {
display: flex;
align-items: center;
padding: 8px 16px;
cursor: pointer;
border-radius: 4px;
transition: background 0.2s;
}
.admin-btn:hover {
background: #f5f9ff;
color: #1890ff;
}
.logout-menu {
width: 140px;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.logout-menu-item {
height: 40px;
display: flex;
align-items: center;
color: #666;
font-size: 14px;
}
.logout-menu-item:hover {
background-color: #e8f4ff !important;
color: #1890ff !important;
}
/* 标签栏:始终显示 */
.tab-bar {
background: #fff;
border-bottom: 1px solid #e0e0e0;
padding: 0 20px;
height: 44px;
margin-top: 2px;
display: flex;
align-items: center;
gap: 8px;
width: 100%;
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--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;
}
.ant-tabs-nav {
height: 100%;
margin: 0 !important;
}
.ant-tabs-nav-list {
height: 100%;
align-items: center;
}
.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;
}
.ant-tabs-tab:hover:not(.ant-tabs-tab-active) {
background: #f5f9ff;
color: #1890ff;
}
.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;
}
.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;
}
.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;
}
/* 内容区域 */
.content {
background: #f8f9fa;
padding: 16px;
flex-grow: 1;
overflow-y: auto;
width: 100%;
}
.home-content {
background: #fff;
padding: 24px;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
height: 100%;
overflow: auto;
}
/* 模块菜单弹窗 */
.module-modal-wrapper .ant-modal-content {
border-radius: 8px;
}
.module-menu-container {
padding: 24px 0;
}
.menu-card {
height: 160px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
border-color: #e8e8e8;
}
.menu-card:hover {
border-color: #1890ff;
box-shadow: 0 4px 16px rgba(24, 144, 255, 0.15);
transform: translateY(-2px);
}
.menu-card-inner {
text-align: center;
}
.menu-icon {
font-size: 32px;
color: #1890ff;
margin-bottom: 16px;
transition: color 0.3s;
}
.menu-card:hover .menu-icon {
color: #096dd9;
}
.menu-title {
font-size: 16px;
color: #333;
font-weight: 500;
}
/* 退出对话框 */
.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;
}
</style>