2 Commits

Author SHA1 Message Date
ZhuRui a4dab891cf feat: 改进页面标签组件 2026-07-28 18:04:53 +08:00
ZhuRui da90d7c356 feat: 添加图标库,更新菜单与页面标签中的icon相关功能与样式 2026-07-28 17:40:35 +08:00
12 changed files with 490 additions and 239 deletions
+1
View File
@@ -27,6 +27,7 @@
"html2canvas": "^1.4.1",
"qs": "^6.15.0",
"vue": "^3.3.0",
"vue-icons-plus": "^0.1.8",
"vue-router": "^4.2.0"
},
"devDependencies": {
+12
View File
@@ -38,6 +38,9 @@ importers:
vue:
specifier: ^3.3.0
version: 3.5.40(typescript@5.9.3)
vue-icons-plus:
specifier: ^0.1.8
version: 0.1.8(vue@3.5.40(typescript@5.9.3))
vue-router:
specifier: ^4.2.0
version: 4.6.4(vue@3.5.40(typescript@5.9.3))
@@ -2188,6 +2191,11 @@ packages:
peerDependencies:
eslint: '>=6.0.0'
vue-icons-plus@0.1.8:
resolution: {integrity: sha512-Xc4hDsD/oP9waSUf44nSaFBhUPo+QkpKclx0S7//5BRACpXymctbit02epek0VRW6nb81pR486XmxPP/ofm2yQ==}
peerDependencies:
vue: '>=2.7.0'
vue-router@4.6.4:
resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==}
peerDependencies:
@@ -4404,6 +4412,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
vue-icons-plus@0.1.8(vue@3.5.40(typescript@5.9.3)):
dependencies:
vue: 3.5.40(typescript@5.9.3)
vue-router@4.6.4(vue@3.5.40(typescript@5.9.3)):
dependencies:
'@vue/devtools-api': 6.6.4
+11 -13
View File
@@ -52,6 +52,12 @@
}
}
// 表格内可点击数字链接(等宽数字,避免列宽抖动)
.tabularLink {
font-variant-numeric: tabular-nums;
cursor: pointer;
}
// ── 汇总 ──
.summary {
display: flex;
@@ -94,32 +100,24 @@
.confirmContent {
padding: 4px 0;
font-size: 14px;
line-height: 1.6;
color: rgba(0, 0, 0, 0.85);
strong {
margin: 0 4px;
font-weight: 600;
color: rgba(0, 0, 0, 0.88);
}
}
.confirmTip {
.confirmTip {
margin-top: 8px;
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
line-height: 1.5;
}
.tabularLink {
font-variant-numeric: tabular-nums;
cursor: pointer;
}
:global(.toggleConfirmModal) {
.ant-modal-confirm-body {
padding: 24px !important;
color: rgba(0, 0, 0, 0.45);
}
}
:global(.toggleConfirmModal),
:global(.deleteConfirmModal) {
.ant-modal-confirm-body {
padding: 24px !important;
+82 -20
View File
@@ -1,27 +1,74 @@
@primary: #1677ff;
.pageTabsMain {
display: flex;
.pageTabsWrap {
position: relative;
width: 100%;
height: 100%;
.navBtn {
position: absolute;
top: 50%;
z-index: 5;
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
border: none;
border-radius: 6px;
background: #fff;
color: rgba(0, 0, 0, 0.45);
box-shadow: 0 0 4px rgba(214, 214, 214, 0.9);
cursor: pointer;
transform: translateY(-50%);
transition:
color 0.15s,
background-color 0.15s;
&:hover {
color: rgba(0, 0, 0, 0.88);
background: #f5f5f5;
}
&:active {
color: rgba(0, 0, 0, 0.88);
background: #ebebeb;
}
}
.navBtnLeft {
left: 0;
}
.navBtnRight {
right: 0;
}
.pageTabs {
display: flex;
align-items: flex-start;
width: 100%;
height: 100%;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
&::-webkit-scrollbar {
height: 0;
display: none;
}
.tabItem {
position: relative;
display: flex;
flex-shrink: 0;
align-items: center;
height: 100%;
user-select: none;
transition: all 0.15s ease;
&:not(:first-child) {
margin-left: -9px;
margin-left: -10px;
}
&:global(:not(.dragging)) {
@@ -64,6 +111,10 @@
font-weight: 500;
}
.tabIcon {
color: @primary;
}
.tabBg {
&.tabBgActive {
.tabBgInner {
@@ -81,11 +132,11 @@
.divider {
position: absolute;
left: 7px;
left: 8px;
top: 50%;
z-index: 0;
width: 1px;
height: 16px;
height: 18px;
background-color: #e8e8e8;
transform: translateY(-50%);
transition: opacity 0.15s;
@@ -98,13 +149,13 @@
z-index: -1;
width: 100%;
height: 100%;
padding: 0 calc(7px - 1px);
padding: 0 calc(8px - 1px);
transition: opacity 0.15s;
}
.tabBgInner {
height: 100%;
border-radius: 7px 7px 0 0;
border-radius: 8px 8px 0 0;
transition:
background-color 0.15s,
margin 0.15s,
@@ -127,25 +178,35 @@
transition: fill 0.15s;
}
// 仅包裹文字 + 关闭按钮,通过 margin 收在 tab 内部
.tabContent {
position: relative;
height: 28px;
height: 34px;
z-index: 2;
display: inline-flex;
align-items: center;
gap: 4px;
gap: 6px;
width: fit-content;
max-width: 100%;
margin: 0 10px 0 10px;
padding: 2px 2px 2px 8px;
border-radius: 8px;
flex-shrink: 0;
margin: 0 12px;
padding: 4px 4px 4px 10px;
border-radius: 9px;
transition: background-color 0.35s;
}
.tabIcon {
display: inline-flex;
align-items: center;
flex-shrink: 0;
color: rgba(0, 0, 0, 0.45);
line-height: 1;
font-size: 16px;
}
.tabTitle {
min-width: 0;
font-size: 13px;
max-width: 160px;
font-size: 14px;
line-height: 1.4;
color: rgba(0, 0, 0, 0.65);
white-space: nowrap;
overflow: hidden;
@@ -154,14 +215,15 @@
.tabClose {
flex-shrink: 0;
width: 16px;
height: 16px;
line-height: 16px;
font-size: 10px;
width: 18px;
height: 18px;
line-height: 18px;
font-size: 11px;
text-align: center;
color: rgba(0, 0, 0, 0.45);
border-radius: 50%;
cursor: pointer;
transition: all 0.15s;
}
}
}
+144 -26
View File
@@ -1,16 +1,26 @@
import { defineComponent, type PropType } from 'vue';
import {
defineComponent,
nextTick,
onBeforeUnmount,
onMounted,
type PropType,
ref,
watch,
} from 'vue';
import { message } from 'ant-design-vue';
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
import type { TabView } from '@/types';
import { renderRouteIcon } from '@/utils/routeIcon';
import styles from './PageTabs.module.less';
const SCROLL_STEP = 240;
type TabKeyHandler = (key: string) => void;
/**
* Chrome 风格的页面标签栏组件
* 参考 VBEN tabs-chrome 的视觉设计:
* - 标签重叠排列(负 margin
* - 激活态:圆角背景 + SVG 底部曲线
* - hover 态:浅灰背景
* - 标签间分隔线
* - 关闭按钮
* - 无滚动条,滚轮横向滚动
* - 溢出时显示左右导航按钮
*/
const PageTabs = defineComponent({
name: 'PageTabs',
@@ -20,11 +30,73 @@ const PageTabs = defineComponent({
/** 当前激活 key */
activeKey: { type: String, required: true },
/** 关闭标签 */
onClose: { type: Function as PropType<(key: string) => void>, default: undefined },
onClose: { type: Function as PropType<TabKeyHandler>, default: undefined },
/** 切换标签 */
onChange: { type: Function as PropType<(key: string) => void>, default: undefined },
onChange: { type: Function as PropType<TabKeyHandler>, default: undefined },
},
setup(props) {
const scrollRef = ref<HTMLElement>();
const tabItemRefs = ref<Record<string, HTMLElement | undefined>>({});
const showNavLeft = ref(false);
const showNavRight = ref(false);
let resizeObserver: ResizeObserver | null = null;
const updateScrollState = () => {
const el = scrollRef.value;
if (!el) {
showNavLeft.value = false;
showNavRight.value = false;
return;
}
const { scrollLeft, scrollWidth, clientWidth } = el;
const canScroll = scrollWidth - clientWidth > 1;
showNavLeft.value = canScroll && scrollLeft > 1;
showNavRight.value = canScroll && scrollLeft + clientWidth < scrollWidth - 1;
};
const scrollByStep = (direction: -1 | 1) => {
const el = scrollRef.value;
if (!el) return;
el.scrollBy({ left: direction * SCROLL_STEP, behavior: 'smooth' });
};
const handleWheel = (e: WheelEvent) => {
const el = scrollRef.value;
if (!el || el.scrollWidth <= el.clientWidth) return;
const delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;
if (delta === 0) return;
e.preventDefault();
el.scrollLeft += delta;
updateScrollState();
};
const scrollActiveTabIntoView = () => {
nextTick(() => {
tabItemRefs.value[props.activeKey]?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest',
});
updateScrollState();
});
};
watch(() => props.activeKey, scrollActiveTabIntoView);
watch(() => props.tabs.map((t) => t.path).join(','), scrollActiveTabIntoView);
const setTabItemRef = (path: string, el: Element | null) => {
if (el) {
tabItemRefs.value[path] = el as HTMLElement;
} else {
delete tabItemRefs.value[path];
}
};
const handleClick = (key: string) => {
if (key !== props.activeKey && props.onChange) {
props.onChange(key);
@@ -42,37 +114,55 @@ const PageTabs = defineComponent({
}
};
return () => (
<div class={styles.pageTabsMain}>
{props.tabs.map((tab, i) => {
onMounted(() => {
const el = scrollRef.value;
if (!el) return;
el.addEventListener('wheel', handleWheel, { passive: false });
el.addEventListener('scroll', updateScrollState, { passive: true });
resizeObserver = new ResizeObserver(updateScrollState);
resizeObserver.observe(el);
updateScrollState();
});
onBeforeUnmount(() => {
const el = scrollRef.value;
if (el) {
el.removeEventListener('wheel', handleWheel);
el.removeEventListener('scroll', updateScrollState);
}
resizeObserver?.disconnect();
resizeObserver = null;
});
const renderTab = (tab: TabView, i: number) => {
const isActive = tab.path === props.activeKey;
const showDivider = i !== 0 && !isActive && props.tabs[i - 1]?.path !== props.activeKey;
return (
<div
key={tab.path}
ref={(el) => setTabItemRef(tab.path, el as Element | null)}
class={[styles.tabItem, isActive && styles.tabItemActive].filter(Boolean).join(' ')}
onClick={() => handleClick(tab.path)}
>
{/* 分隔线:第一个标签 / 激活标签 / 上一个就是激活的 → 不显示 */}
<div
v-show={i !== 0 && !isActive && props.tabs[i - 1]?.path !== props.activeKey}
class={styles.divider}
/>
<div v-show={showDivider} class={styles.divider} />
{/* Chrome 风格背景:激活态 */}
<div class={[styles.tabBg, isActive && styles.tabBgActive].filter(Boolean).join(' ')}>
<div class={styles.tabBgInner} />
{/* 左下曲线 */}
<svg class={styles.tabBgCurveBefore} height="7" width="7">
<path d="M 0 7 A 7 7 0 0 0 7 0 L 7 7 Z" />
<svg class={styles.tabBgCurveBefore} height="8" width="8">
<path d="M 0 8 A 8 8 0 0 0 8 0 L 8 8 Z" />
</svg>
{/* 右下曲线 */}
<svg class={styles.tabBgCurveAfter} height="7" width="7">
<path d="M 0 0 A 7 7 0 0 0 7 7 L 0 7 Z" />
<svg class={styles.tabBgCurveAfter} height="8" width="8">
<path d="M 0 0 A 8 8 0 0 0 8 8 L 0 8 Z" />
</svg>
</div>
{/* 标签内容:hover 背景包裹文字 + 关闭按钮 */}
<div class={styles.tabContent}>
{tab.icon ? (
<span class={styles.tabIcon}>{renderRouteIcon(tab.icon, { size: 16 })}</span>
) : null}
<div class={styles.tabTitle}>{tab.title}</div>
<div class={styles.tabClose} onClick={(e: Event) => handleClose(tab.path, e)}>
@@ -80,7 +170,35 @@ const PageTabs = defineComponent({
</div>
</div>
);
})}
};
return () => (
<div class={styles.pageTabsWrap}>
{showNavLeft.value && (
<button
type="button"
class={[styles.navBtn, styles.navBtnLeft].join(' ')}
aria-label="向左滚动标签"
onClick={() => scrollByStep(-1)}
>
<LeftOutlined />
</button>
)}
<div ref={scrollRef} class={styles.pageTabs}>
{props.tabs.map((tab, i) => renderTab(tab, i))}
</div>
{showNavRight.value && (
<button
type="button"
class={[styles.navBtn, styles.navBtnRight].join(' ')}
aria-label="向右滚动标签"
onClick={() => scrollByStep(1)}
>
<RightOutlined />
</button>
)}
</div>
);
},
+19 -8
View File
@@ -11,12 +11,11 @@ import type { MenuNode } from '@/types';
*
* ```json
* [
* { "id": "dashboard", "name": "工作台", "path": "dashboard", "component": "dashboard", "icon": "DashboardOutlined" },
* { "id": "dashboard", "name": "工作台", "path": "dashboard", "component": "dashboard", "icon": "AiOutlineDashboard" },
* {
* "id": "events", "name": "赛事管理", "path": "events", "icon": "TrophyOutlined",
* "id": "events", "name": "赛事管理", "path": "events", "icon": "AiOutlineTrophy",
* "children": [
* { "id": "events_list", "name": "赛事列表", "path": "events/list", "component": "events/list" },
* { "id": "events_orders", "name": "订单管理", "path": "events/orders", "component": "events/orders" }
* { "id": "events_list", "name": "赛事列表", "path": "events/list", "component": "events/list", "icon": "AiOutlineUnorderedList" }
* ]
* }
* ]
@@ -29,7 +28,7 @@ import type { MenuNode } from '@/types';
* | name | string | 是 | 菜单名称/路由 name |
* | path | string | 是 | 路由相对路径(不带 / 前缀) |
* | component | string | 否 | 组件路径(映射 src/pages/ 下目录,如 "events/list" |
* | icon | string | 否 | antd 图标名(如 DashboardOutlined |
* | icon | string | 否 | vue-icons-plus 图标名(如 AiOutlineTrophy,需在 routeIcon.tsx 注册 |
* | sort | number | 否 | 排序权重(越小越前) |
* | hideInMenu | boolean | 否 | 是否在菜单中隐藏 |
* | externalLink | string | 否 | 外链地址 |
@@ -47,7 +46,7 @@ export const FALLBACK_MENU_NODES: MenuNode[] = [
id: 'events',
name: '赛事管理',
path: 'events',
icon: 'TrophyOutlined',
icon: 'AiOutlineTrophy',
children: [
{
id: 'events_list',
@@ -55,6 +54,7 @@ export const FALLBACK_MENU_NODES: MenuNode[] = [
path: 'events/list',
component: 'events/list',
componentName: 'EventList',
icon: 'AiOutlineUnorderedList',
},
{
id: 'events_orders',
@@ -62,6 +62,7 @@ export const FALLBACK_MENU_NODES: MenuNode[] = [
path: 'events/orders',
component: 'events/orders',
componentName: 'EventOrders',
icon: 'AiOutlineShoppingCart',
},
{
id: 'events_users',
@@ -69,6 +70,7 @@ export const FALLBACK_MENU_NODES: MenuNode[] = [
path: 'events/users',
component: 'events/users',
componentName: 'EventUsers',
icon: 'AiOutlineTeam',
},
{
id: 'events_banner',
@@ -76,6 +78,7 @@ export const FALLBACK_MENU_NODES: MenuNode[] = [
path: 'events/banner',
component: 'events/banner',
componentName: 'EventBanner',
icon: 'AiOutlinePicture',
},
{
id: 'events_logs',
@@ -83,6 +86,7 @@ export const FALLBACK_MENU_NODES: MenuNode[] = [
path: 'events/logs',
component: 'events/logs',
componentName: 'EventLogs',
icon: 'AiOutlineHistory',
},
],
},
@@ -90,7 +94,7 @@ export const FALLBACK_MENU_NODES: MenuNode[] = [
id: 'finance',
name: '账务管理',
path: 'finance',
icon: 'TransactionOutlined',
icon: 'AiOutlineTransaction',
children: [
{
id: 'finance_withdraw',
@@ -98,6 +102,7 @@ export const FALLBACK_MENU_NODES: MenuNode[] = [
path: 'finance/withdraw',
component: 'finance/withdraw',
componentName: 'FinanceWithdraw',
icon: 'AiOutlineMoneyCollect',
},
{
id: 'finance_wallet',
@@ -105,6 +110,7 @@ export const FALLBACK_MENU_NODES: MenuNode[] = [
path: 'finance/wallet',
component: 'finance/wallet',
componentName: 'FinanceWallet',
icon: 'AiOutlineWallet',
},
{
id: 'finance_payments',
@@ -112,6 +118,7 @@ export const FALLBACK_MENU_NODES: MenuNode[] = [
path: 'finance/payments',
component: 'finance/payments',
componentName: 'FinancePayments',
icon: 'AiOutlinePayCircle',
},
{
id: 'finance_reports',
@@ -119,6 +126,7 @@ export const FALLBACK_MENU_NODES: MenuNode[] = [
path: 'finance/reports',
component: 'finance/reports',
componentName: 'FinanceReports',
icon: 'AiOutlineBarChart',
},
],
},
@@ -126,7 +134,7 @@ export const FALLBACK_MENU_NODES: MenuNode[] = [
id: 'system',
name: '系统管理',
path: 'system',
icon: 'SettingOutlined',
icon: 'AiOutlineSetting',
children: [
{
id: 'system_users',
@@ -134,6 +142,7 @@ export const FALLBACK_MENU_NODES: MenuNode[] = [
path: 'system/users',
component: 'system/users',
componentName: 'SystemUsers',
icon: 'AiOutlineUser',
},
{
id: 'system_roles',
@@ -141,6 +150,7 @@ export const FALLBACK_MENU_NODES: MenuNode[] = [
path: 'system/roles',
component: 'system/roles',
componentName: 'SystemRoles',
icon: 'AiOutlineSafetyCertificate',
},
{
id: 'system_logs',
@@ -148,6 +158,7 @@ export const FALLBACK_MENU_NODES: MenuNode[] = [
path: 'system/logs',
component: 'system/logs',
componentName: 'SystemLogs',
icon: 'AiOutlineAudit',
},
],
},
+2 -2
View File
@@ -129,8 +129,8 @@
.pageTabs {
width: 100%;
padding: 4px 16px 0 16px;
height: 42px;
height: 48px;
padding: 4px 8px 0;
background: #fff;
border-top: 1px solid #f0f0f0;
}
+4 -22
View File
@@ -4,21 +4,17 @@ 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 {
DashboardOutlined,
EditOutlined,
InfoCircleOutlined,
LogoutOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
SettingOutlined,
TransactionOutlined,
TrophyOutlined,
} 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 { renderRouteIcon } from '@/utils/routeIcon';
import logoutSvg from '@/assets/img/icon/logout.svg';
import styles from './BasicLayout.module.less';
@@ -47,22 +43,9 @@ const footerStyle: CSSProperties = {
userSelect: 'none',
};
/**
* 图标名 → 组件映射
* 后端下发的 icon 字符串在此映射为 antd icon 组件
* 扩展新图标只需在此添加一行
*/
const ICON_MAP: Record<string, any> = {
DashboardOutlined,
InfoCircleOutlined,
SettingOutlined,
TransactionOutlined,
TrophyOutlined,
};
/**
* 将 menuStore 中的 menuItems 转为 antd Menu 的 items 格式
* 递归处理 iconName → icon VNode
* 递归处理 iconName → vue-icons-plus 图标 VNode
*/
function resolveMenuItems(items: any[]): MenuProps['items'] {
return items.map((item) => {
@@ -71,9 +54,8 @@ function resolveMenuItems(items: any[]): MenuProps['items'] {
label: item.label,
};
// iconName 字符串转为 VNode
if (item.iconName && ICON_MAP[item.iconName]) {
resolved.icon = () => h(ICON_MAP[item.iconName]);
if (item.iconName) {
resolved.icon = () => renderRouteIcon(item.iconName, { size: 16 });
}
if (item.children?.length) {
+1 -3
View File
@@ -56,14 +56,12 @@ function confirmDelete({ record, onOk }: { record: any; onOk: (record: any) => v
icon: null,
class: 'deleteConfirmModal',
content: (
<>
<div class={pageStyles.confirmContent}>
<span></span>
<strong>{record.roleName}</strong>
<span></span>
</div>
<div class={pageStyles.confirmTip}></div>
</>
</div>
),
okText: '确认',
cancelText: '取消',
+1
View File
@@ -73,5 +73,6 @@ router.afterEach((to) => {
path: to.path,
name: componentName,
title: to.meta.title,
icon: to.meta.icon as string | undefined,
});
});
+6 -2
View File
@@ -28,6 +28,8 @@ export interface TabView {
path: string;
name: string;
title: string;
/** vue-icons-plus 图标名(如 AiOutlineTrophy */
icon?: string;
}
// ── 菜单 & 权限 ──
@@ -52,7 +54,7 @@ export interface MenuNode {
* 留空或省略 → 纯布局节点(自动重定向到第一个子路由)
*/
component?: string;
/** 菜单图标(antd icon 名,如 DashboardOutlined) */
/** 菜单图标vue-icons-plus 导出名,如 AiOutlineTrophy */
icon?: string;
/** 排序权重(越小越靠前) */
sort?: number;
@@ -79,8 +81,9 @@ export interface MenuNode {
*/
export interface MenuItemRaw {
key: string;
/** 解析后的图标 VNode(传给 antd Menu */
icon?: any;
/** antd icon 名称字符串,用于动态解析图标 */
/** vue-icons-plus 图标名(如 AiOutlineTrophy */
iconName?: string;
label: string;
children?: MenuItemRaw[];
@@ -97,6 +100,7 @@ export type PermissionCode = string;
declare module 'vue-router' {
interface RouteMeta {
title?: string;
/** vue-icons-plus 图标名(如 AiOutlineTrophy */
icon?: string;
/** 是否需要登录 */
requiresAuth?: boolean;
+64
View File
@@ -0,0 +1,64 @@
import { h, type VNode } from 'vue';
import type { IconType } from 'vue-icons-plus/lib';
import {
AiOutlineAudit,
AiOutlineBarChart,
AiOutlineHistory,
AiOutlineMoneyCollect,
AiOutlinePayCircle,
AiOutlinePicture,
AiOutlineSafetyCertificate,
AiOutlineSetting,
AiOutlineShoppingCart,
AiOutlineTeam,
AiOutlineTransaction,
AiOutlineTrophy,
AiOutlineUnorderedList,
AiOutlineUser,
AiOutlineWallet,
} from 'vue-icons-plus/ai';
/** 路由 / 菜单 / 标签页使用的图标名 → vue-icons-plus 组件 */
const ROUTE_ICON_MAP: Record<string, IconType> = {
AiOutlineTrophy,
AiOutlineUnorderedList,
AiOutlineShoppingCart,
AiOutlineTeam,
AiOutlinePicture,
AiOutlineHistory,
AiOutlineTransaction,
AiOutlineMoneyCollect,
AiOutlineWallet,
AiOutlinePayCircle,
AiOutlineBarChart,
AiOutlineSetting,
AiOutlineUser,
AiOutlineSafetyCertificate,
AiOutlineAudit,
};
export type RouteIconName = keyof typeof ROUTE_ICON_MAP;
export interface RouteIconOptions {
size?: number | string;
class?: string;
color?: string;
}
/** 将路由 meta.icon 字符串渲染为图标 VNode */
export function renderRouteIcon(iconName?: string, options: RouteIconOptions = {}): VNode | null {
if (!iconName) return null;
const Icon = ROUTE_ICON_MAP[iconName];
if (!Icon) {
console.warn(`[RouteIcon] 未注册的图标: ${iconName}`);
return null;
}
const { size = 14, class: className, color } = options;
return h(Icon, {
size,
class: className,
...(color ? { color } : {}),
});
}