Files
cpms_operation_platform/src/layouts/BasicLayout.tsx
T

242 lines
7.3 KiB
TypeScript
Raw Normal View History

2026-07-23 11:27:33 +08:00
import { computed, defineComponent, h, ref } from 'vue';
import type { CSSProperties } from 'vue';
2026-07-14 10:31:17 +08:00
import { useRouter, useRoute, RouterView } from 'vue-router';
import { Layout, Menu, Modal, Breadcrumb, Dropdown, message } from 'ant-design-vue';
2026-07-14 10:31:17 +08:00
import type { MenuProps } from 'ant-design-vue';
import {
DashboardOutlined,
EditOutlined,
2026-07-14 10:31:17 +08:00
InfoCircleOutlined,
LogoutOutlined,
2026-07-14 10:31:17 +08:00
MenuFoldOutlined,
MenuUnfoldOutlined,
2026-07-23 11:27:33 +08:00
SettingOutlined,
2026-07-25 09:39:56 +08:00
TransactionOutlined,
TrophyOutlined,
2026-07-14 10:31:17 +08:00
} from '@ant-design/icons-vue';
2026-07-23 11:27:33 +08:00
import { useMenuStore } from '@/stores/menuStore';
import { useUserStore } from '@/stores/userStore';
import { useState, useBreadcrumb, auth } from '@/hooks';
import { ChangePasswordModal } from '@/components';
import logoutSvg from '@/assets/img/icon/logout.svg';
2026-07-14 10:31:17 +08:00
import styles from './BasicLayout.module.less';
const { Header, Sider, Content, Footer } = Layout;
const headerStyle: CSSProperties = {
background: '#fff',
padding: '0 16px',
display: 'flex',
alignItems: 'center',
color: '#1a1a1a',
boxShadow: '0 1px 4px rgba(0, 21, 41, 0.08)',
};
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',
};
2026-07-14 10:31:17 +08:00
/**
2026-07-23 11:27:33 +08:00
* 图标名 → 组件映射
* 后端下发的 icon 字符串在此映射为 antd icon 组件
* 扩展新图标只需在此添加一行
*/
const ICON_MAP: Record<string, any> = {
DashboardOutlined,
InfoCircleOutlined,
SettingOutlined,
2026-07-25 09:39:56 +08:00
TransactionOutlined,
TrophyOutlined,
2026-07-23 11:27:33 +08:00
};
/**
* 将 menuStore 中的 menuItems 转为 antd Menu 的 items 格式
* 递归处理 iconName → icon VNode
*/
function resolveMenuItems(items: any[]): MenuProps['items'] {
return items.map((item) => {
const resolved: any = {
key: item.key,
label: item.label,
};
// iconName 字符串转为 VNode
if (item.iconName && ICON_MAP[item.iconName]) {
resolved.icon = () => h(ICON_MAP[item.iconName]);
}
if (item.children?.length) {
resolved.children = resolveMenuItems(item.children);
}
return resolved;
});
}
/**
* 基础布局:左侧导航(后端菜单驱动) + 顶部栏 + 内容区 + 页脚
2026-07-14 10:31:17 +08:00
*/
export default defineComponent({
name: 'BasicLayout',
setup() {
const route = useRoute();
const router = useRouter();
const collapsed = ref(false);
2026-07-23 11:27:33 +08:00
const { menuItems } = useMenuStore();
const { phone } = useUserStore();
const { breadcrumbs } = useBreadcrumb();
2026-07-23 11:27:33 +08:00
2026-07-14 10:31:17 +08:00
const selectedKeys = computed<string[]>(() => [route.path]);
2026-07-23 11:27:33 +08:00
const resolvedItems = computed(() => resolveMenuItems(menuItems.value));
2026-07-14 10:31:17 +08:00
const handleMenuClick: MenuProps['onClick'] = ({ key }) => {
router.push(key as string);
};
const toggleCollapsed = () => {
collapsed.value = !collapsed.value;
};
/** ===== 退出登录 ===== */
const handleLogout = () => {
Modal.confirm({
title: '提示',
content: '是否退出登录?',
okText: '确认',
cancelText: '取消',
onOk: () => {
const { clearUser } = useUserStore();
clearUser();
auth.logout();
},
});
};
/** ===== 修改密码弹窗 ===== */
const [changePwdVisible, setChangePwdVisible] = useState<boolean>(false);
const [changePwdSubmitting, setChangePwdSubmitting] = useState<boolean>(false);
const handleChangePassword = () => {
setChangePwdVisible(true);
};
const handleChangePwdSave = async (payload: { newPassword: string }) => {
if (changePwdSubmitting.value) return;
setChangePwdSubmitting(true);
try {
// TODO: 替换为真实 API 调用
console.log('修改密码:', payload);
message.success('修改成功');
setChangePwdVisible(false);
} catch (err: any) {
message.error(err?.msg || '修改失败');
} finally {
setChangePwdSubmitting(false);
}
};
/** ===== 头像下拉菜单:修改密码 / 退出登录 ===== */
const handleUserMenuClick: MenuProps['onClick'] = ({ key }) => {
if (key === 'changePassword') {
handleChangePassword();
} else if (key === 'logout') {
handleLogout();
}
};
2026-07-14 10:31:17 +08:00
return () => (
<Layout class={styles.container}>
<Sider style={siderStyle} collapsed={collapsed.value} trigger={null} collapsible>
2026-07-14 10:31:17 +08:00
<div class={styles.logo}>
<span class={styles.logoText}>{collapsed.value ? '6羽' : '六羽赛事运营平台'}</span>
2026-07-14 10:31:17 +08:00
</div>
<Menu
theme="light"
2026-07-14 10:31:17 +08:00
mode="inline"
selectedKeys={selectedKeys.value}
2026-07-23 11:27:33 +08:00
items={resolvedItems.value}
2026-07-14 10:31:17 +08:00
onClick={handleMenuClick}
/>
</Sider>
<Layout class={styles.main}>
<Header style={headerStyle}>
2026-07-14 10:31:17 +08:00
<span class={styles.trigger} onClick={toggleCollapsed}>
{collapsed.value ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</span>
{/* 面包屑:紧跟在折叠按钮之后 */}
<Breadcrumb separator=">" class={styles.breadcrumb}>
{breadcrumbs.value.map((item, index) => {
const isLast = index === breadcrumbs.value.length - 1;
return (
<Breadcrumb.Item key={item.path || index}>
{isLast || !item.path ? (
<span>{item.title}</span>
) : (
<a onClick={() => router.push(item.path as string)}>{item.title}</a>
)}
</Breadcrumb.Item>
);
})}
</Breadcrumb>
{/* 右侧: 账号信息 + 退出 */}
<div class={styles.headerRight}>
<span class={styles.userPhone}>{phone.value}</span>
<Dropdown trigger={['hover']} placement="bottomLeft">
{{
default: () => <img src={logoutSvg} class={styles.logoutIcon} />,
overlay: () => (
<Menu onClick={handleUserMenuClick}>
<Menu.Item key="changePassword">
<span class={styles.menuItem}>
<EditOutlined />
修改密码
</span>
</Menu.Item>
<Menu.Divider />
<Menu.Item key="logout">
<span class={`${styles.menuItem} ${styles.menuItemDanger}`}>
<LogoutOutlined />
退出登录
</span>
</Menu.Item>
</Menu>
),
}}
</Dropdown>
</div>
2026-07-14 10:31:17 +08:00
</Header>
<Content class={styles.content}>
<RouterView />
</Content>
<Footer style={footerStyle}>六个羽友赛事运营平台 ©{new Date().getFullYear()}</Footer>
2026-07-14 10:31:17 +08:00
</Layout>
{/* 修改密码弹窗 */}
2026-07-27 10:00:12 +08:00
{changePwdVisible.value && (
<ChangePasswordModal
visible={changePwdVisible.value}
submitting={changePwdSubmitting.value}
onClose={() => setChangePwdVisible(false)}
onSave={handleChangePwdSave}
/>
)}
2026-07-14 10:31:17 +08:00
</Layout>
);
},
});