diff --git a/package.json b/package.json index 96538f3..5d6104e 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d9c0fb..e2757e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/assets/styles/pageLayout.module.less b/src/assets/styles/pageLayout.module.less index 43e2146..b5ba3fb 100644 --- a/src/assets/styles/pageLayout.module.less +++ b/src/assets/styles/pageLayout.module.less @@ -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; diff --git a/src/components/pageTabs/PageTabs.module.less b/src/components/pageTabs/PageTabs.module.less index 99311f7..10ad493 100644 --- a/src/components/pageTabs/PageTabs.module.less +++ b/src/components/pageTabs/PageTabs.module.less @@ -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; diff --git a/src/components/pageTabs/PageTabs.tsx b/src/components/pageTabs/PageTabs.tsx index 380c9d0..578c1ff 100644 --- a/src/components/pageTabs/PageTabs.tsx +++ b/src/components/pageTabs/PageTabs.tsx @@ -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({ - {/* 标签内容:hover 背景包裹文字 + 关闭按钮 */} + {/* 标签内容:图标 + 文字 + 关闭按钮 */}
+ {tab.icon ? ( + {renderRouteIcon(tab.icon, { size: 14 })} + ) : null}
{tab.title}
handleClose(tab.path, e)}> ✕ diff --git a/src/config/fallbackRoutes.ts b/src/config/fallbackRoutes.ts index be5d1c2..88d458b 100644 --- a/src/config/fallbackRoutes.ts +++ b/src/config/fallbackRoutes.ts @@ -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', }, ], }, diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx index dc95caf..52fc4ab 100644 --- a/src/layouts/BasicLayout.tsx +++ b/src/layouts/BasicLayout.tsx @@ -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 = { - 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) { diff --git a/src/pages/system/roles/index.tsx b/src/pages/system/roles/index.tsx index 501bbed..2153780 100644 --- a/src/pages/system/roles/index.tsx +++ b/src/pages/system/roles/index.tsx @@ -56,14 +56,12 @@ function confirmDelete({ record, onOk }: { record: any; onOk: (record: any) => v icon: null, class: 'deleteConfirmModal', content: ( - <> -
- 确定要删除角色 - {record.roleName} - 吗? -
+
+ 确定要删除角色 + {record.roleName} + 吗?
删除后该角色下的用户将失去对应的权限,请谨慎操作。
- +
), okText: '确认', cancelText: '取消', diff --git a/src/router/guard.ts b/src/router/guard.ts index b722907..87f1589 100644 --- a/src/router/guard.ts +++ b/src/router/guard.ts @@ -73,5 +73,6 @@ router.afterEach((to) => { path: to.path, name: componentName, title: to.meta.title, + icon: to.meta.icon as string | undefined, }); }); diff --git a/src/types/index.ts b/src/types/index.ts index f0b8112..6fda827 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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; diff --git a/src/utils/routeIcon.tsx b/src/utils/routeIcon.tsx new file mode 100644 index 0000000..400e0a0 --- /dev/null +++ b/src/utils/routeIcon.tsx @@ -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 = { + 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 } : {}), + }); +}