重写复现方法
This commit is contained in:
@@ -1,21 +1,4 @@
|
||||
<script setup>
|
||||
// This starter template is using Vue 3 <script setup> SFCs
|
||||
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img alt="Vue logo" src="./assets/logo.png" />
|
||||
<HelloWorld msg="Hello Vue 3 + Vite" />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
margin-top: 60px;
|
||||
}
|
||||
</style>
|
||||
<!-- 所有页面都会在这里渲染 -->
|
||||
<router-view />
|
||||
</template>
|
||||
14
capi-ui/src/api/auth.ts
Normal file
14
capi-ui/src/api/auth.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export interface LoginParams {
|
||||
account: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface LoginResult {
|
||||
token: string
|
||||
}
|
||||
|
||||
/** 登录接口 */
|
||||
export const apiLogin = (data: LoginParams) =>
|
||||
request.post<LoginResult>('/Sys/login/userLogin', data)
|
||||
BIN
capi-ui/src/assets/imges/backImg.jpg
Normal file
BIN
capi-ui/src/assets/imges/backImg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
42
capi-ui/src/composables/useRequest.ts
Normal file
42
capi-ui/src/composables/useRequest.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// src/composables/useRequest.ts
|
||||
import { ref } from 'vue'
|
||||
import service from '@/utils/request'
|
||||
|
||||
interface UseRequestOptions<T> {
|
||||
url: string
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
params?: any
|
||||
data?: any
|
||||
immediate?: boolean
|
||||
onSuccess?: (data: T) => void
|
||||
onError?: (error: any) => void
|
||||
}
|
||||
|
||||
export function useRequest<T = any>(options: UseRequestOptions<T>) {
|
||||
const { url, method = 'GET', params, data, immediate = true, onSuccess, onError } = options
|
||||
|
||||
const loading = ref(false)
|
||||
const response = ref<T | null>(null)
|
||||
const error = ref<any>(null)
|
||||
|
||||
const run = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const res = await service({ url, method, params, data })
|
||||
response.value = res
|
||||
onSuccess?.(res)
|
||||
} catch (err) {
|
||||
error.value = err
|
||||
onError?.(err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
run()
|
||||
}
|
||||
|
||||
return { loading, response, error, run }
|
||||
}
|
||||
@@ -1,4 +1,14 @@
|
||||
// src/main.ts
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router' // 新增
|
||||
import { createPinia } from 'pinia'
|
||||
import Antd from 'ant-design-vue'
|
||||
import 'ant-design-vue/dist/reset.css'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia()) // 注册Pinia
|
||||
app.use(router) // 注册路由
|
||||
app.use(Antd) // 注册Ant Design Vue
|
||||
app.mount('#app')
|
||||
26
capi-ui/src/router/index.ts
Normal file
26
capi-ui/src/router/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/login'
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/login/Login.vue')
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/sys/Dashboard.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
33
capi-ui/src/utils/request.ts
Normal file
33
capi-ui/src/utils/request.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
// src/utils/request.ts
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
|
||||
// 创建 axios 实例
|
||||
const service: AxiosInstance = axios.create({
|
||||
baseURL: import.meta.env.VITE_BASE_API, // 从环境变量读取
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
// 可添加 token
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
(response: AxiosResponse) => response.data,
|
||||
(error) => {
|
||||
// 统一错误处理
|
||||
console.error('Request error:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default service
|
||||
123
capi-ui/src/views/login/Login.vue
Normal file
123
capi-ui/src/views/login/Login.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div class="login-wrapper">
|
||||
<a-card class="login-card" :bordered="false">
|
||||
<div class="login-header">
|
||||
<img class="logo" src="@/assets/logo.png" alt="logo" />
|
||||
<h1>cApi管理系统</h1>
|
||||
<p>安全、高效的一站式管理解决方案,为您的业务保驾护航</p>
|
||||
</div>
|
||||
|
||||
<a-form ref="formRef" :model="form" :rules="rules" size="large" @finish="handleSubmit">
|
||||
<a-form-item name="account">
|
||||
<a-input v-model:value="form.account" placeholder="请输入用户名" allow-clear>
|
||||
<template #prefix><UserOutlined /></template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="password">
|
||||
<a-input-password v-model:value="form.password" placeholder="请输入密码" allow-clear>
|
||||
<template #prefix><LockOutlined /></template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit" :loading="loading" block>
|
||||
登录
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
import type { LoginParams, LoginResult } from '@/api/auth'
|
||||
import { apiLogin } from '@/api/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const formRef = ref()
|
||||
const loading = ref(false)
|
||||
|
||||
const form = reactive<LoginParams>({ account: '', password: '' })
|
||||
|
||||
const rules = {
|
||||
account: [{ required: true, message: '请输入用户名' }],
|
||||
password: [{ required: true, message: '请输入密码' }]
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// 防止重复提交
|
||||
if (loading.value) return;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
// 更清晰的变量命名,API返回通常包含code、data、msg等字段
|
||||
const response: LoginResult = await apiLogin(form);
|
||||
// 检查请求是否成功(通常200为成功状态码)
|
||||
if (response.code === 200 && response.data) {
|
||||
localStorage.setItem('userInfo', JSON.stringify(response.data));
|
||||
message.success(response.msg || '登录成功');
|
||||
// 延迟跳转提升用户体验
|
||||
setTimeout(() => {
|
||||
router.replace('/dashboard');
|
||||
}, 800);
|
||||
return; // 成功后终止函数,避免执行后续错误提示
|
||||
}
|
||||
|
||||
// 处理业务错误(如账号密码错误)
|
||||
message.error(response.msg || '登录失败,请检查账号密码');
|
||||
} catch (error) {
|
||||
// 处理网络错误或异常情况
|
||||
console.error('登录请求异常:', error);
|
||||
message.error('网络异常,请检查网络连接后重试');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.login-wrapper {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 200px;
|
||||
|
||||
/* 渐变与背景图片融合设置 */
|
||||
background:
|
||||
linear-gradient(120deg, rgba(79, 172, 254, 0.7) 0%, rgba(0, 242, 254, 0.7) 50%, rgba(122, 90, 248, 0.7) 100%),
|
||||
url('@/assets/imges/backImg.jpg') center center no-repeat;
|
||||
|
||||
/* 背景融合模式 - 使渐变与图片颜色叠加融合 */
|
||||
background-blend-mode: overlay;
|
||||
/* 确保背景完全覆盖容器 */
|
||||
background-size: cover;
|
||||
}
|
||||
.login-text{
|
||||
text-align: center;
|
||||
color: white;
|
||||
width: 800px;
|
||||
padding: 32px 48px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.login-card {
|
||||
width: 520px;
|
||||
padding: 32px 48px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
.logo { width: 64px; height: 64px; margin-bottom: 12px; }
|
||||
h1 { font-size: 24px; font-weight: 600; margin: 0; }
|
||||
p { font-size: 14px; color: #8c8c8c; margin: 8px 0 0; }
|
||||
}
|
||||
|
||||
</style>
|
||||
757
capi-ui/src/views/sys/Dashboard.vue
Normal file
757
capi-ui/src/views/sys/Dashboard.vue
Normal 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
|
||||
>
|
||||
<
|
||||
</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>
|
||||
|
||||
<!-- 内容区域:统一通过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>
|
||||
9
capi-ui/src/views/sys/main.vue
Normal file
9
capi-ui/src/views/sys/main.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
sddddddddddddd
|
||||
</template>
|
||||
|
||||
<script>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
Reference in New Issue
Block a user