import { computed, defineComponent, h, Transition, KeepAlive } from 'vue'; import type { CSSProperties } from 'vue'; import { useRouter, useRoute, RouterView } from 'vue-router'; import { Layout, Menu, Modal, Breadcrumb, Dropdown, message } from 'ant-design-vue'; import type { MenuProps } from 'ant-design-vue'; import { EditOutlined, LogoutOutlined, MenuFoldOutlined, MenuUnfoldOutlined, } from '@ant-design/icons-vue'; import { useMenuStore } from '@/stores/menuStore'; import { useUserStore } from '@/stores/userStore'; import { useTabsStore } from '@/stores/tabsStore'; import { useState, useBreadcrumb, auth } from '@/hooks'; import { ChangePasswordModal, PageTabs } from '@/components'; import { updateUserPwd } from '@/api/users'; import { renderRouteIcon } from '@/utils/routeIcon'; import logoutSvg from '@/assets/img/icon/logout.svg'; import styles from './BasicLayout.module.less'; const { Header, Sider, Content, Footer } = Layout; const headerStyle: CSSProperties = { background: '#fff', padding: '0', display: 'flex', flexDirection: 'column', justifyContent: 'center', color: '#1a1a1a', boxShadow: '0 1px 4px rgba(0, 21, 41, 0.08)', height: 'auto', }; const siderStyle: CSSProperties = { background: '#fff', boxShadow: '2px 0 8px rgba(0, 21, 41, 0.08)', }; const footerStyle: CSSProperties = { padding: '0 50px 16px 50px', textAlign: 'center', color: '#3a3a3a', userSelect: 'none', }; /** * 将 menuStore 中的 menuItems 转为 antd Menu 的 items 格式 * 递归处理 iconName → vue-icons-plus 图标 VNode */ function resolveMenuItems(items: any[]): MenuProps['items'] { return items.map((item) => { const resolved: any = { key: item.key, label: item.label, }; if (item.iconName) { resolved.icon = () => renderRouteIcon(item.iconName, { size: 16 }); } if (item.children?.length) { resolved.children = resolveMenuItems(item.children); } return resolved; }); } /** * 基础布局:左侧导航(后端菜单驱动) + 顶部栏 + 内容区 + 页脚 */ export default defineComponent({ name: 'BasicLayout', setup() { const route = useRoute(); const router = useRouter(); const [collapsed, setCollapsed] = useState(false); const { menuItems } = useMenuStore(); const { phone, nickname } = useUserStore(); const { tabs, cachedViews, removeTab, removeLeftTabs, removeRightTabs, removeOtherTabs, removeAllTabs, } = useTabsStore(); const { breadcrumbs } = useBreadcrumb(); const selectedKeys = computed(() => [route.path]); const resolvedItems = computed(() => resolveMenuItems(menuItems.value)); const handleMenuClick: MenuProps['onClick'] = ({ key }) => { router.push(key as string); }; const toggleCollapsed = () => { setCollapsed(!collapsed.value); }; const activeTabKey = computed(() => route.path); /** ===== 退出登录 ===== */ const handleLogout = () => { Modal.confirm({ title: '提示', content: '是否退出登录?', okText: '确认', cancelText: '取消', onOk: () => { const { clearUser } = useUserStore(); clearUser(); auth.logout(); }, }); }; /** ===== 修改密码弹窗 ===== */ const [changePwdVisible, setChangePwdVisible] = useState(false); const [changePwdSubmitting, setChangePwdSubmitting] = useState(false); const handleChangePassword = () => { setChangePwdVisible(true); }; const handleChangePwdSave = async (payload: { newPassword: string }) => { if (changePwdSubmitting.value) return; setChangePwdSubmitting(true); try { const res = await updateUserPwd({ password: payload.newPassword, confirmPassword: payload.newPassword, }); if (res.code == 200) { message.success('密码修改成功'); setChangePwdVisible(false); window.setTimeout(() => { auth.logout(); }, 1500); } else { message.error(res.msg || '修改失败'); } } catch (err: any) { console.error('修改密码失败:', err); } finally { setChangePwdSubmitting(false); } }; /** ===== 头像下拉菜单:修改密码 / 退出登录 ===== */ const handleUserMenuClick: MenuProps['onClick'] = ({ key }) => { if (key === 'changePassword') { handleChangePassword(); } else if (key === 'logout') { handleLogout(); } }; /** ===== PageTabs 事件 ===== */ const handleTabChange = (key: string) => { if (key !== route.path) { router.push(key); } }; const handleTabClose = (key: string) => { removeTab(key); }; const handleCloseLeftTabs = (key: string) => { removeLeftTabs(key); }; const handleCloseRightTabs = (key: string) => { removeRightTabs(key); }; const handleCloseOtherTabs = (key: string) => { removeOtherTabs(key); }; const handleCloseAllTabs = () => { removeAllTabs(); }; return () => (
{collapsed.value ? '6羽' : '六羽赛事运营平台'}
{/* ── 顶部行:折叠按钮 + 面包屑 + 右侧账号信息 ── 按需求把原来平铺在 Header 里的三块统一包到一个 row 容器内, 形成「行容器 + tabs 容器」的两层结构 */}
{collapsed.value ? : } {/* 面包屑:紧跟在折叠按钮之后 */} {breadcrumbs.value.map((item, index) => { const isLast = index === breadcrumbs.value.length - 1; return ( {isLast || !item.path ? ( {item.title} ) : ( router.push(item.path as string)}>{item.title} )} ); })} {/* 右侧: 账号信息 + 退出 */}
{nickname.value} {{ default: () => , overlay: () => ( 修改密码 退出登录 ), }}
{/* ── Tabs 行:Chrome 风格页面标签栏 ── 数据来自 tabsStore,点击切换路由,关闭按钮走 store.removeTab */}
{/* ── 路由出口 ── 用 缓存已访问过的页面, include 是组件 name 列表,所以页面组件需要在 defineComponent 中显式给出 name */}
( {Component ? h(Component, { key: route.path }) : null} ), }} />
六个羽友赛事运营平台 ©{new Date().getFullYear()}
{/* 修改密码弹窗 */} {changePwdVisible.value && ( setChangePwdVisible(false)} onSave={handleChangePwdSave} /> )} ); }, });