feat(permiss添加icons): 增加表格行拖拽排序功能

dev_na
chenhao 2026-07-03 15:52:17 +08:00
parent 37696b13a9
commit ae75ea4e2c
2 changed files with 104 additions and 39 deletions

View File

@ -478,7 +478,9 @@
"iconEmpty": "未找到匹配的图标",
"showIconLibrary": "显示图标库",
"hideIconLibrary": "收起图标库",
"iconLoadingMore": "已加载 {{current}} / {{total}} 个图标"
"iconLoadingMore": "已加载 {{current}} / {{total}} 个图标",
"commonIcons": "常用图标",
"allIcons": "全部图标"
},
"theme": {
"settings": "主题设置",

View File

@ -4,6 +4,7 @@ import { Button, Col, Drawer, Form, Input, InputNumber, Popconfirm, Row, Select,
import { DndContext, PointerSensor, useSensor, useSensors, DragEndEvent } from '@dnd-kit/core';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
import * as AntdIcons from "@ant-design/icons";
import { CSS } from '@dnd-kit/utilities';
import {
ApartmentOutlined,
@ -39,6 +40,7 @@ import "./index.less";
const { Text } = Typography;
type TreePermission = SysPermission & { key: number; children?: TreePermission[] };
type MenuIconComponent = React.ElementType<{ "aria-hidden"?: string | boolean }>;
const legacyIconAliases: Record<string, string> = {
dashboard: "DashboardOutlined",
@ -53,11 +55,30 @@ const legacyIconAliases: Record<string, string> = {
setting: "SettingOutlined"
};
const menuIconComponents: Record<string, React.ComponentType<any>> = {
const commonMenuIconNames = [
"DashboardOutlined",
"VideoCameraOutlined",
"UserOutlined",
"TeamOutlined",
"SafetyCertificateOutlined",
"SettingOutlined",
"DesktopOutlined",
"ShopOutlined",
"ApartmentOutlined",
"BookOutlined",
"FolderOutlined",
"MenuOutlined",
"ClusterOutlined"
];
const fallbackMenuIconComponents: Record<string, MenuIconComponent> = {
ApartmentOutlined,
BookOutlined,
ClusterOutlined,
DashboardOutlined,
DesktopOutlined,
FolderOutlined,
MenuOutlined,
SafetyCertificateOutlined,
SettingOutlined,
ShopOutlined,
@ -66,6 +87,13 @@ const menuIconComponents: Record<string, React.ComponentType<any>> = {
VideoCameraOutlined
};
const menuIconComponents = Object.entries(AntdIcons).reduce<Record<string, MenuIconComponent>>((icons, [name, component]) => {
if (name.endsWith("Outlined") && (typeof component === "function" || (typeof component === "object" && component !== null))) {
icons[name] = component as MenuIconComponent;
}
return icons;
}, {...fallbackMenuIconComponents});
interface RowProps extends React.HTMLAttributes<HTMLTableRowElement> {
'data-row-key': string;
}
@ -114,6 +142,7 @@ const DragHandle = () => {
);
};
const commonMenuIconOptions = commonMenuIconNames.filter((iconName) => menuIconComponents[iconName]);
const menuIconOptions = Object.keys(menuIconComponents).sort((left, right) => left.localeCompare(right));
function renderSelectableIcon(iconName?: string) {
@ -223,10 +252,18 @@ export default function Permissions() {
.map((permission) => ({ value: permission.permId, label: permission.name }));
}, [data, currentPermType]);
const filteredCommonIconOptions = useMemo(() => {
const keyword = iconSearchKeyword.trim().toLowerCase();
if (!keyword) return commonMenuIconOptions;
return commonMenuIconOptions.filter((iconName) => iconName.toLowerCase().includes(keyword));
}, [iconSearchKeyword]);
const filteredIconOptions = useMemo(() => {
const keyword = iconSearchKeyword.trim().toLowerCase();
if (!keyword) return menuIconOptions;
return menuIconOptions.filter((iconName) => iconName.toLowerCase().includes(keyword));
return menuIconOptions.filter((iconName) => {
const hitKeyword = keyword ? iconName.toLowerCase().includes(keyword) : true;
return hitKeyword && !commonMenuIconOptions.includes(iconName);
});
}, [iconSearchKeyword]);
const visibleIconOptions = useMemo(() => filteredIconOptions.slice(0, visibleIconCount), [filteredIconOptions, visibleIconCount]);
@ -254,6 +291,41 @@ export default function Permissions() {
setIconSearchKeyword("");
};
const renderIconPickerButton = (iconName: string) => {
const active = selectedIcon === iconName;
return (
<Tooltip key={iconName} title={iconName}>
<Button
data-icon-name={iconName}
type="text"
onClick={() => {
const nextValue = active ? undefined : iconName;
form.setFieldValue("icon", nextValue);
if (nextValue) {
setIconSearchKeyword("");
setIconPickerOpen(false);
}
}}
aria-label={iconName}
style={{
height: 44,
width: "100%",
borderRadius: 10,
border: active ? "1px solid #1677ff" : "1px solid transparent",
background: active ? "#e6f4ff" : "#fff",
color: active ? "#1677ff" : "inherit",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
boxShadow: active ? "0 0 0 2px rgba(22,119,255,0.12)" : "none"
}}
>
{renderSelectableIcon(iconName)}
</Button>
</Tooltip>
);
};
const onDragEnd = async ({ active, over }: DragEndEvent) => {
if (active.id !== over?.id) {
const activeId = Number(active.id);
@ -606,43 +678,34 @@ export default function Permissions() {
setVisibleIconCount((count) => Math.min(count + 120, filteredIconOptions.length));
}
}} style={{ maxHeight: 240, overflowY: "auto", border: "1px solid #f0f0f0", borderRadius: 8, padding: 12, background: "#fafafa" }}>
{filteredCommonIconOptions.length ? <>
<Text type="secondary" style={{
display: "block",
marginBottom: 8,
fontSize: 12
}}>{t("permissionsExt.commonIcons")}</Text>
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(44px, 1fr))",
gap: 10,
marginBottom: 12
}}>
{filteredCommonIconOptions.map(renderIconPickerButton)}
</div>
</> : null}
{visibleIconOptions.length ? <Text type="secondary" style={{
display: "block",
marginBottom: 8,
fontSize: 12
}}>{t("permissionsExt.allIcons")}</Text> : null}
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(44px, 1fr))", gap: 10 }}>
{visibleIconOptions.map((iconName) => {
const active = selectedIcon === iconName;
return (
<Tooltip key={iconName} title={iconName}>
<Button
data-icon-name={iconName}
type="text"
onClick={() => {
const nextValue = active ? undefined : iconName;
form.setFieldValue("icon", nextValue);
if (nextValue) {
setIconSearchKeyword("");
setIconPickerOpen(false);
}
}}
aria-label={iconName}
style={{
height: 44,
width: "100%",
borderRadius: 10,
border: active ? "1px solid #1677ff" : "1px solid transparent",
background: active ? "#e6f4ff" : "#fff",
color: active ? "#1677ff" : "inherit",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
boxShadow: active ? "0 0 0 2px rgba(22,119,255,0.12)" : "none"
}}
>
{renderSelectableIcon(iconName)}
</Button>
</Tooltip>
);
})}
{visibleIconOptions.map(renderIconPickerButton)}
</div>
{!filteredIconOptions.length ? <div style={{ padding: "16px 0", textAlign: "center", color: "#8c8c8c" }}>{t("permissionsExt.iconEmpty")}</div> : null}
{!filteredCommonIconOptions.length && !filteredIconOptions.length ? <div style={{
padding: "16px 0",
textAlign: "center",
color: "#8c8c8c"
}}>{t("permissionsExt.iconEmpty")}</div> : null}
{visibleIconOptions.length < filteredIconOptions.length ? <div style={{ paddingTop: 12, textAlign: "center", color: "#8c8c8c", fontSize: 12 }}>{t("permissionsExt.iconLoadingMore", { current: visibleIconOptions.length, total: filteredIconOptions.length })}</div> : null}
</div></div> : null}
</Space>