feat: 添加图标库,更新菜单与页面标签中的icon相关功能与样式

This commit is contained in:
ZhuRui
2026-07-28 17:40:35 +08:00
parent 202f4bbd44
commit da90d7c356
11 changed files with 146 additions and 59 deletions
+16 -18
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 {
margin-top: 8px;
font-size: 12px;
line-height: 1.5;
color: rgba(0, 0, 0, 0.45);
}
}
.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;
}
}
:global(.toggleConfirmModal),
:global(.deleteConfirmModal) {
.ant-modal-confirm-body {
padding: 24px !important;
+13 -1
View File
@@ -64,6 +64,10 @@
font-weight: 500;
}
.tabIcon {
color: @primary;
}
.tabBg {
&.tabBgActive {
.tabBgInner {
@@ -127,7 +131,7 @@
transition: fill 0.15s;
}
// 仅包裹文字 + 关闭按钮,通过 margin 收在 tab 内部
// 仅包裹图标 + 文字 + 关闭按钮,通过 margin 收在 tab 内部
.tabContent {
position: relative;
height: 28px;
@@ -143,6 +147,14 @@
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;
}
.tabTitle {
min-width: 0;
font-size: 13px;
+5 -1
View File
@@ -1,6 +1,7 @@
import { defineComponent, type PropType } from 'vue';
import { message } from 'ant-design-vue';
import type { TabView } from '@/types';
import { renderRouteIcon } from '@/utils/routeIcon';
import styles from './PageTabs.module.less';
/**
@@ -71,8 +72,11 @@ const PageTabs = defineComponent({
</svg>
</div>
{/* 标签内容:hover 背景包裹文字 + 关闭按钮 */}
{/* 标签内容:图标 + 文字 + 关闭按钮 */}
<div class={styles.tabContent}>
{tab.icon ? (
<span class={styles.tabIcon}>{renderRouteIcon(tab.icon, { size: 14 })}</span>
) : null}
<div class={styles.tabTitle}>{tab.title}</div>
<div class={styles.tabClose} onClick={(e: Event) => handleClose(tab.path, e)}>
+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',
},
],
},
+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) {
+5 -7
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.confirmContent}>
<span></span>
<strong>{record.roleName}</strong>
<span></span>
<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 } : {}),
});
}