feat: 添加图标库,更新菜单与页面标签中的icon相关功能与样式
This commit is contained in:
@@ -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": {
|
||||
|
||||
Generated
+12
@@ -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
|
||||
|
||||
@@ -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;
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)}>
|
||||
✕
|
||||
|
||||
@@ -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,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) {
|
||||
|
||||
@@ -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: '取消',
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 } : {}),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user