Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88aae37295 | |||
| 85e1696101 | |||
| 2ad04594fe | |||
| 126ee7b50a |
@@ -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",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
|
||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@@ -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)
|
||||
'@dnd-kit/core':
|
||||
specifier: ^6.3.1
|
||||
version: 6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
@@ -1893,6 +1896,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==}
|
||||
|
||||
@@ -13837,6 +13847,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':
|
||||
|
||||
0
src/pages/prod/extended/config.tsx
Normal file
0
src/pages/prod/extended/config.tsx
Normal file
0
src/pages/prod/extended/index.tsx
Normal file
0
src/pages/prod/extended/index.tsx
Normal file
@@ -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<DeptVO>[] = [
|
||||
{
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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 (
|
||||
// <ProFormText
|
||||
// formItemProps={{
|
||||
// style: {
|
||||
// marginBottom: 0,
|
||||
// },
|
||||
// }}
|
||||
// />
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: '菜单图标',
|
||||
dataIndex: 'icon',
|
||||
fieldProps: {
|
||||
placeholder: '请选择图标',
|
||||
},
|
||||
dependencies: ['type'],
|
||||
renderFormItem: (_schema, _config, form) => {
|
||||
const type = form.getFieldValue('type');
|
||||
if (type === 3) return null;
|
||||
|
||||
return <IconSelector />;
|
||||
},
|
||||
render: (_, record: MenuVO) => {
|
||||
if (!record.icon) return '—';
|
||||
// 保持原有渲染逻辑
|
||||
return (
|
||||
<ProFormText
|
||||
formItemProps={{
|
||||
style: {
|
||||
marginBottom: 0,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
{record.icon ? <i className={`anticon ${record.icon}`} /> : '—'}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
187
src/pages/system/menu/icon.tsx
Normal file
187
src/pages/system/menu/icon.tsx
Normal file
@@ -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<string, React.ReactNode> = {};
|
||||
|
||||
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<any>;
|
||||
iconMap[iconName] = <IconComponent theme="outline" size="16" />;
|
||||
}
|
||||
});
|
||||
|
||||
// 图标选项列表
|
||||
const iconOptions = Object.keys(iconMap).map((key) => ({
|
||||
label: (
|
||||
<span>
|
||||
{iconMap[key]} {key}
|
||||
</span>
|
||||
),
|
||||
value: key,
|
||||
}));
|
||||
|
||||
interface IconSelectorProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
const IconSelector: React.FC<IconSelectorProps> = ({ 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 (
|
||||
<>
|
||||
<div
|
||||
onClick={() => setOpen(true)}
|
||||
style={{
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: 6,
|
||||
padding: '4px 11px',
|
||||
cursor: 'pointer',
|
||||
minHeight: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{value ? (
|
||||
<span>
|
||||
{iconMap[value as keyof typeof iconMap]}
|
||||
<span style={{ marginLeft: 8 }}>{value}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ color: '#bfbfbf' }}>请选择图标</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
title="选择图标"
|
||||
open={open}
|
||||
onCancel={() => {
|
||||
setOpen(false);
|
||||
setCurrentPage(1);
|
||||
setSearchValue('');
|
||||
}}
|
||||
onOk={() => setOpen(false)}
|
||||
width={800}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<div style={{ padding: 16 }}>
|
||||
<Input.Search
|
||||
placeholder="搜索图标..."
|
||||
value={searchValue}
|
||||
onChange={(e) => {
|
||||
setSearchValue(e.target.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
{filteredIcons.length === 0 ? (
|
||||
<IconPark.Empty title="未找到相关图标" />
|
||||
) : (
|
||||
<>
|
||||
<List
|
||||
grid={{ gutter: 16, column: 8 }}
|
||||
dataSource={paginatedIcons}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
onClick={() => handleSelect(item.value)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center',
|
||||
border:
|
||||
value === item.value
|
||||
? '1px solid #1890ff'
|
||||
: '1px solid transparent',
|
||||
borderRadius: 6,
|
||||
padding: 8,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div style={{ fontSize: 20, marginBottom: 4 }}>
|
||||
{iconMap[item.value as keyof typeof iconMap]}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{item.value}
|
||||
</div>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: 16,
|
||||
}}
|
||||
>
|
||||
<Pagination
|
||||
current={currentPage}
|
||||
pageSize={pageSize}
|
||||
total={filteredIcons.length}
|
||||
onChange={setCurrentPage}
|
||||
showSizeChanger={false}
|
||||
size="small"
|
||||
/>
|
||||
<div>
|
||||
<a onClick={handleClear} style={{ marginRight: 16 }}>
|
||||
清除选择
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconSelector;
|
||||
@@ -55,6 +55,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 modal.confirm({
|
||||
@@ -127,6 +135,9 @@ const SystemMenu = () => {
|
||||
>
|
||||
<a>删除</a>
|
||||
</Popconfirm>,
|
||||
<a key="add" onClick={() => handleAddChild(record)}>
|
||||
添加
|
||||
</a>,
|
||||
],
|
||||
};
|
||||
const columns = [...baseMenuColumns, actionColumns];
|
||||
|
||||
@@ -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<UserVO>[] = [
|
||||
{
|
||||
@@ -99,6 +102,20 @@ export const baseTenantColumns: ProColumns<UserVO>[] = [
|
||||
];
|
||||
|
||||
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: '备注',
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
import * as IconPark from '@icon-park/react';
|
||||
import { Spin } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
// 动态构造 iconMap
|
||||
const iconMap: Record<string, React.ReactNode> = {};
|
||||
|
||||
// 获取所有图标名称并构建图标映射
|
||||
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<any>;
|
||||
iconMap[iconName] = <IconComponent theme="outline" size="16" />;
|
||||
}
|
||||
});
|
||||
|
||||
import type { MenuVO } from '@/services/system/menu';
|
||||
|
||||
export const loopMenuItem = (menus: MenuVO[], pId: number | string): any[] => {
|
||||
@@ -17,13 +37,18 @@ export const loopMenuItem = (menus: MenuVO[], pId: number | string): any[] => {
|
||||
const routeItem: any = {
|
||||
path: item.path,
|
||||
name: item.name,
|
||||
icon: '',
|
||||
id: item.id,
|
||||
icon: '',
|
||||
parentId: pId,
|
||||
hideInMenu: !item.visible,
|
||||
children: [],
|
||||
};
|
||||
|
||||
// 如果菜单项有图标且图标存在于图标映射中,则添加图标
|
||||
if (item.icon && iconMap[item.icon]) {
|
||||
routeItem.icon = iconMap[item.icon];
|
||||
}
|
||||
|
||||
// 只有当 Component 存在时才添加 element 属性
|
||||
if (Component) {
|
||||
routeItem.element = (
|
||||
|
||||
Reference in New Issue
Block a user