From 126ee7b50a6e4cc2cd0d806c235e2ec9dc0b190b Mon Sep 17 00:00:00 2001 From: qianpw <2233607957@qq.com> Date: Thu, 30 Oct 2025 16:12:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=B3=BB=E7=BB=9F=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 15 +++ src/components/DrawerForm/index.tsx | 4 +- src/pages/prod/extended/config.tsx | 0 src/pages/prod/extended/index.tsx | 0 src/pages/system/dept/config.tsx | 32 +++-- src/pages/system/menu/config.tsx | 51 +++++--- src/pages/system/menu/icon.tsx | 187 ++++++++++++++++++++++++++++ src/pages/system/menu/index.tsx | 11 ++ src/pages/system/user/config.tsx | 54 ++++++-- src/utils/menuUtils.tsx | 45 +++++-- 11 files changed, 351 insertions(+), 49 deletions(-) create mode 100644 src/pages/prod/extended/config.tsx create mode 100644 src/pages/prod/extended/index.tsx create mode 100644 src/pages/system/menu/icon.tsx diff --git a/package.json b/package.json index 7307a38..eb53f8e 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@ant-design/icons": "^5.6.1", "@ant-design/pro-components": "^2.8.9", "@ant-design/v5-patch-for-react-19": "^1.0.3", + "@icon-park/react": "^1.4.2", "@tinymce/tinymce-react": "^6.3.0", "antd": "^5.26.4", "antd-style": "^3.7.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3bd9a17..c9a2ca9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@ant-design/v5-patch-for-react-19': specifier: ^1.0.3 version: 1.0.3(antd@5.27.3(date-fns@2.30.0)(moment@2.30.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@icon-park/react': + specifier: ^1.4.2 + version: 1.4.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@tinymce/tinymce-react': specifier: ^6.3.0 version: 6.3.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(tinymce@8.1.2) @@ -1869,6 +1872,13 @@ packages: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead + '@icon-park/react@1.4.2': + resolution: {integrity: sha512-+MtQLjNiRuia3fC/NfpSCTIy5KH5b+NkMB9zYd7p3R4aAIK61AjK0OSraaICJdkKooU9jpzk8m0fY4g9A3JqhQ==} + engines: {node: '>= 8.0.0', npm: '>= 5.0.0'} + peerDependencies: + react: '>=16.9' + react-dom: '>=16.9' + '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -13794,6 +13804,11 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} + '@icon-park/react@1.4.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + '@iconify/types@2.0.0': {} '@iconify/utils@2.1.1': diff --git a/src/components/DrawerForm/index.tsx b/src/components/DrawerForm/index.tsx index a1124c0..a5a45d0 100644 --- a/src/components/DrawerForm/index.tsx +++ b/src/components/DrawerForm/index.tsx @@ -17,9 +17,9 @@ interface ConfigurableDrawerFormProps { width?: number | string; labelCol?: ColProps; wrapperCol?: ColProps; - footer: React.ReactNode; + footer?: React.ReactNode; children?: React.ReactNode; - bodyStyle: React.CSSProperties; + bodyStyle?: React.CSSProperties; } export interface ConfigurableDrawerFormRef { diff --git a/src/pages/prod/extended/config.tsx b/src/pages/prod/extended/config.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/prod/extended/index.tsx b/src/pages/prod/extended/index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/system/dept/config.tsx b/src/pages/system/dept/config.tsx index 52b524c..ffffce6 100644 --- a/src/pages/system/dept/config.tsx +++ b/src/pages/system/dept/config.tsx @@ -5,8 +5,9 @@ import type { import { Tag } from 'antd'; import dayjs from 'dayjs'; import { tenantStatus } from '@/constants'; -import type { DeptVO } from '@/services/system/dept'; +import { type DeptVO, getSimpleDeptList } from '@/services/system/dept'; import { getStatusLabel } from '@/utils/constant'; +import { handleTree } from '@/utils/tree'; export const baseDeptColumns: ProColumns[] = [ { @@ -84,11 +85,28 @@ export const formColumns = (_type: string): ProFormColumnsType[] => [ valueType: 'treeSelect', fieldProps: () => { return { - multiple: true, placeholder: '请选择上级部门', - options: [{ label: '11', value: 5016 }], }; }, + request: async () => { + // 调用getSimplePostList方法获取数据 + const res = await getSimpleDeptList(); + const deptTree = handleTree(res); + const formatToOptions = (nodes: Tree[]): any[] => + nodes.map((node) => ({ + label: node.name, + value: node.id, + children: node.children ? formatToOptions(node.children) : undefined, + })); + + return [ + { + label: '顶级部门', + value: 0, + children: formatToOptions(deptTree), + }, + ]; + }, }, { title: '部门名称', @@ -150,12 +168,12 @@ export const formColumns = (_type: string): ProFormColumnsType[] => [ fieldProps: { options: [ { - label: '正常', - value: 1, + label: '开启', + value: 0, }, { - label: '禁用', - value: 2, + label: '关闭', + value: 1, }, ], }, diff --git a/src/pages/system/menu/config.tsx b/src/pages/system/menu/config.tsx index 9ec61f1..e8e81d0 100644 --- a/src/pages/system/menu/config.tsx +++ b/src/pages/system/menu/config.tsx @@ -5,15 +5,12 @@ import { ProFormRadio, ProFormText, } from '@ant-design/pro-components'; -import { Switch } from 'antd'; +import { Select, Switch } from 'antd'; import { CommonStatusEnum } from '@/constants'; import { useMessage } from '@/hooks/antd/useMessage'; -import { - getSimpleMenusList, - type MenuVO, - updateMenu, -} from '@/services/system/menu'; +import { getMenuList, type MenuVO, updateMenu } from '@/services/system/menu'; import { handleTree } from '@/utils/tree'; +import IconSelector from './icon'; const handleStatus = async (record: MenuVO) => { const message = useMessage(); // 消息弹窗 @@ -86,7 +83,7 @@ export const formColumns = (_type: string): ProFormColumnsType[] => [ dataIndex: 'parentId', valueType: 'treeSelect', request: async () => { - const res = await getSimpleMenusList(); + const res = await getMenuList({}); console.log(res); const menu: Tree = { id: 0, name: '主类目', children: [] }; menu.children = handleTree(res); @@ -130,24 +127,44 @@ export const formColumns = (_type: string): ProFormColumnsType[] => [ ], }, }, + // { + // title: "菜单图标", + // dataIndex: "icon", + // fieldProps: { + // placeholder: "请选择图标", + // }, + // dependencies: ["type"], + // renderFormItem: (_schema, _config, form) => { + // const type = form.getFieldValue("type"); + // if (type === 3) return null; + // return ( + // + // ); + // }, + // }, { title: '菜单图标', dataIndex: 'icon', - fieldProps: { - placeholder: '请选择图标', - }, dependencies: ['type'], renderFormItem: (_schema, _config, form) => { const type = form.getFieldValue('type'); if (type === 3) return null; + + return ; + }, + render: (_, record: MenuVO) => { + if (!record.icon) return '—'; + // 保持原有渲染逻辑 return ( - + + {record.icon ? : '—'} + ); }, }, diff --git a/src/pages/system/menu/icon.tsx b/src/pages/system/menu/icon.tsx new file mode 100644 index 0000000..7b1224d --- /dev/null +++ b/src/pages/system/menu/icon.tsx @@ -0,0 +1,187 @@ +import * as IconPark from '@icon-park/react'; + +import { Input, List, Modal, Pagination } from 'antd'; +import { useState } from 'react'; + +const allIcons = Object.keys(IconPark); + +// 动态构造 iconMap +const iconMap: Record = {}; + +allIcons.forEach((iconName) => { + // 排除不需要的属性(如默认导出等) + if ( + iconName !== 'default' && + typeof IconPark[iconName as keyof typeof IconPark] === 'function' + ) { + const IconComponent = IconPark[ + iconName as keyof typeof IconPark + ] as React.ComponentType; + iconMap[iconName] = ; + } +}); + +// 图标选项列表 +const iconOptions = Object.keys(iconMap).map((key) => ({ + label: ( + + {iconMap[key]} {key} + + ), + value: key, +})); + +interface IconSelectorProps { + value?: string; + onChange?: (value: string) => void; +} + +const IconSelector: React.FC = ({ value, onChange }) => { + const [open, setOpen] = useState(false); + const [searchValue, setSearchValue] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + const pageSize = 48; + + // 过滤图标 + const filteredIcons = iconOptions.filter( + (option) => + option.value.toLowerCase().includes(searchValue.toLowerCase()) || + option.value.toLowerCase().includes(searchValue.toLowerCase()), + ); + + // 分页数据 + const paginatedIcons = filteredIcons.slice( + (currentPage - 1) * pageSize, + currentPage * pageSize, + ); + + const handleSelect = (iconName: string) => { + onChange?.(iconName); + setOpen(false); + setCurrentPage(1); + setSearchValue(''); + }; + + const handleClear = () => { + onChange?.(''); + setOpen(false); + }; + + return ( + <> +
setOpen(true)} + style={{ + border: '1px solid #d9d9d9', + borderRadius: 6, + padding: '4px 11px', + cursor: 'pointer', + minHeight: 32, + display: 'flex', + alignItems: 'center', + }} + > + {value ? ( + + {iconMap[value as keyof typeof iconMap]} + {value} + + ) : ( + 请选择图标 + )} +
+ + { + setOpen(false); + setCurrentPage(1); + setSearchValue(''); + }} + onOk={() => setOpen(false)} + width={800} + styles={{ body: { padding: 0 } }} + okText="确定" + cancelText="取消" + > +
+ { + setSearchValue(e.target.value); + setCurrentPage(1); + }} + style={{ marginBottom: 16 }} + /> + + {filteredIcons.length === 0 ? ( + + ) : ( + <> + ( + handleSelect(item.value)} + style={{ + cursor: 'pointer', + textAlign: 'center', + border: + value === item.value + ? '1px solid #1890ff' + : '1px solid transparent', + borderRadius: 6, + padding: 8, + }} + > +
+
+ {iconMap[item.value as keyof typeof iconMap]} +
+
+ {item.value} +
+
+
+ )} + /> + + + + )} +
+
+ + ); +}; + +export default IconSelector; diff --git a/src/pages/system/menu/index.tsx b/src/pages/system/menu/index.tsx index 9ea4eeb..f984bae 100644 --- a/src/pages/system/menu/index.tsx +++ b/src/pages/system/menu/index.tsx @@ -54,6 +54,14 @@ const SystemMenu = () => { configurableDrawerRef.current?.open({ type: 1 }); }; + const handleAddChild = (record: MenuVO) => { + setType('create'); + configurableDrawerRef.current?.open({ + type: 2, + parentId: record.id, + }); + }; + const handleReload = async () => { try { await message.confirm('即将更新缓存刷新浏览器!', '刷新菜单缓存'); @@ -121,6 +129,9 @@ const SystemMenu = () => { > 删除 , + handleAddChild(record)}> + 添加 + , ], }; const columns = [...baseMenuColumns, actionColumns]; diff --git a/src/pages/system/user/config.tsx b/src/pages/system/user/config.tsx index f22bb43..5829ce5 100644 --- a/src/pages/system/user/config.tsx +++ b/src/pages/system/user/config.tsx @@ -5,8 +5,11 @@ import type { } from '@ant-design/pro-components'; import { Modal, message, Switch } from 'antd'; import dayjs from 'dayjs'; +import { getSimpleDeptList } from '@/services/system/dept'; +import { getSimplePostList } from '@/services/system/post'; import { updateUserStatus } from '@/services/system/user'; import type { UserVO } from '@/services/system/user/index'; +import { handleTree } from '@/utils/tree'; export const baseTenantColumns: ProColumns[] = [ { @@ -99,6 +102,20 @@ export const baseTenantColumns: ProColumns[] = [ ]; export const formColumns = (type: string): ProFormColumnsType[] => [ + { + title: '用户账号', + dataIndex: 'username', + fieldProps: { + style: { display: 'none' }, + }, + formItemProps: { + style: { + display: 'none', + margin: 0, + padding: 0, + }, + }, + }, { title: '用户昵称', dataIndex: 'nickname', @@ -115,10 +132,20 @@ export const formColumns = (type: string): ProFormColumnsType[] => [ title: '归属部门', dataIndex: 'deptId', valueType: 'treeSelect', + fieldProps: { - multiple: true, placeholder: '请选择归属部门', - options: [{ lable: '11', value: 5016 }], + fieldNames: { + label: 'name', + value: 'id', + children: 'children', + }, + }, + request: async () => { + const res = await getSimpleDeptList(); + const data = handleTree(res); + console.log('data', data); + return data; }, }, { @@ -199,18 +226,19 @@ export const formColumns = (type: string): ProFormColumnsType[] => [ fieldProps: { mode: 'multiple', placeholder: '请选择岗位', - options: [ - { - label: '管理员', - value: 1, - }, - { - label: '普通用户', - value: 2, - }, - ], }, - formItemProps: {}, + request: async () => { + // 调用getSimplePostList方法获取数据 + const res = await getSimplePostList(); + + const data = + res.map((item: any) => ({ + label: item.label || item.name, + value: item.value || item.id, + })) || []; + + return data; + }, }, { title: '备注', diff --git a/src/utils/menuUtils.tsx b/src/utils/menuUtils.tsx index b76ecd6..c73d135 100644 --- a/src/utils/menuUtils.tsx +++ b/src/utils/menuUtils.tsx @@ -1,7 +1,26 @@ +import * as IconPark from '@icon-park/react'; import { Navigate } from '@umijs/max'; import { Spin } from 'antd'; import React from 'react'; +// 动态构造 iconMap +const iconMap: Record = {}; + +// 获取所有图标名称并构建图标映射 +const allIcons = Object.keys(IconPark); +allIcons.forEach((iconName) => { + // 排除不需要的属性(如默认导出等) + if ( + iconName !== 'default' && + typeof IconPark[iconName as keyof typeof IconPark] === 'function' + ) { + const IconComponent = IconPark[ + iconName as keyof typeof IconPark + ] as React.ComponentType; + iconMap[iconName] = ; + } +}); + export const loopMenuItem = (menus: any[], pId: number | string): any[] => { // console.log(menus, "menus"); return menus.flatMap((item) => { @@ -14,14 +33,24 @@ export const loopMenuItem = (menus: any[], pId: number | string): any[] => { return importComponent().catch(import404); }); } + + // 处理菜单图标 + const menuItem: any = { + path: item.path, + name: item.name, + id: item.menuID || item.id, + parentId: pId, + }; + + // 如果菜单项有图标且图标存在于图标映射中,则添加图标 + if (item.icon && iconMap[item.icon]) { + menuItem.icon = iconMap[item.icon]; + } + if (item.children && item.children.length > 0) { return [ { - path: item.path, - name: item.name, - // icon: item.icon, - id: item.id, - parentId: pId, + ...menuItem, children: [ { path: item.path, @@ -39,11 +68,7 @@ export const loopMenuItem = (menus: any[], pId: number | string): any[] => { } else { return [ { - path: item.path, - name: item.name, - // icon: item.icon, - id: item.menuID, - parentId: pId, + ...menuItem, element: ( }