4 Commits

Author SHA1 Message Date
88aae37295 Merge branch 'wuxichen' of http://gitea.tashowz.com/tashow/tashow-manager into qianpw 2025-11-19 10:08:47 +08:00
85e1696101 feat: 菜单添加图标 2025-10-30 16:32:03 +08:00
2ad04594fe fix: 合并冲突 2025-10-30 16:27:19 +08:00
126ee7b50a feat: 系统管理 2025-10-30 16:12:02 +08:00
10 changed files with 340 additions and 38 deletions

View File

@@ -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
View File

@@ -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':

View File

View File

View 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,
},
],
},

View File

@@ -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 (
<ProFormText
formItemProps={{
style: {
marginBottom: 0,
return <IconSelector />;
},
}}
/>
render: (_, record: MenuVO) => {
if (!record.icon) return '—';
// 保持原有渲染逻辑
return (
<span>
{record.icon ? <i className={`anticon ${record.icon}`} /> : '—'}
</span>
);
},
},

View 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;

View File

@@ -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];

View File

@@ -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,
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;
},
],
},
formItemProps: {},
},
{
title: '备注',

View File

@@ -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 = (