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": "未找到匹配的图标", "iconEmpty": "未找到匹配的图标",
"showIconLibrary": "显示图标库", "showIconLibrary": "显示图标库",
"hideIconLibrary": "收起图标库", "hideIconLibrary": "收起图标库",
"iconLoadingMore": "已加载 {{current}} / {{total}} 个图标" "iconLoadingMore": "已加载 {{current}} / {{total}} 个图标",
"commonIcons": "常用图标",
"allIcons": "全部图标"
}, },
"theme": { "theme": {
"settings": "主题设置", "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 { DndContext, PointerSensor, useSensor, useSensors, DragEndEvent } from '@dnd-kit/core';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
import * as AntdIcons from "@ant-design/icons";
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import { import {
ApartmentOutlined, ApartmentOutlined,
@ -39,6 +40,7 @@ import "./index.less";
const { Text } = Typography; const { Text } = Typography;
type TreePermission = SysPermission & { key: number; children?: TreePermission[] }; type TreePermission = SysPermission & { key: number; children?: TreePermission[] };
type MenuIconComponent = React.ElementType<{ "aria-hidden"?: string | boolean }>;
const legacyIconAliases: Record<string, string> = { const legacyIconAliases: Record<string, string> = {
dashboard: "DashboardOutlined", dashboard: "DashboardOutlined",
@ -53,11 +55,30 @@ const legacyIconAliases: Record<string, string> = {
setting: "SettingOutlined" 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, ApartmentOutlined,
BookOutlined, BookOutlined,
ClusterOutlined,
DashboardOutlined, DashboardOutlined,
DesktopOutlined, DesktopOutlined,
FolderOutlined,
MenuOutlined,
SafetyCertificateOutlined, SafetyCertificateOutlined,
SettingOutlined, SettingOutlined,
ShopOutlined, ShopOutlined,
@ -66,6 +87,13 @@ const menuIconComponents: Record<string, React.ComponentType<any>> = {
VideoCameraOutlined 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> { interface RowProps extends React.HTMLAttributes<HTMLTableRowElement> {
'data-row-key': string; '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)); const menuIconOptions = Object.keys(menuIconComponents).sort((left, right) => left.localeCompare(right));
function renderSelectableIcon(iconName?: string) { function renderSelectableIcon(iconName?: string) {
@ -223,10 +252,18 @@ export default function Permissions() {
.map((permission) => ({ value: permission.permId, label: permission.name })); .map((permission) => ({ value: permission.permId, label: permission.name }));
}, [data, currentPermType]); }, [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 filteredIconOptions = useMemo(() => {
const keyword = iconSearchKeyword.trim().toLowerCase(); const keyword = iconSearchKeyword.trim().toLowerCase();
if (!keyword) return menuIconOptions; return menuIconOptions.filter((iconName) => {
return menuIconOptions.filter((iconName) => iconName.toLowerCase().includes(keyword)); const hitKeyword = keyword ? iconName.toLowerCase().includes(keyword) : true;
return hitKeyword && !commonMenuIconOptions.includes(iconName);
});
}, [iconSearchKeyword]); }, [iconSearchKeyword]);
const visibleIconOptions = useMemo(() => filteredIconOptions.slice(0, visibleIconCount), [filteredIconOptions, visibleIconCount]); const visibleIconOptions = useMemo(() => filteredIconOptions.slice(0, visibleIconCount), [filteredIconOptions, visibleIconCount]);
@ -254,6 +291,41 @@ export default function Permissions() {
setIconSearchKeyword(""); 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) => { const onDragEnd = async ({ active, over }: DragEndEvent) => {
if (active.id !== over?.id) { if (active.id !== over?.id) {
const activeId = Number(active.id); const activeId = Number(active.id);
@ -606,43 +678,34 @@ export default function Permissions() {
setVisibleIconCount((count) => Math.min(count + 120, filteredIconOptions.length)); setVisibleIconCount((count) => Math.min(count + 120, filteredIconOptions.length));
} }
}} style={{ maxHeight: 240, overflowY: "auto", border: "1px solid #f0f0f0", borderRadius: 8, padding: 12, background: "#fafafa" }}> }} 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 }}> <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(44px, 1fr))", gap: 10 }}>
{visibleIconOptions.map((iconName) => { {visibleIconOptions.map(renderIconPickerButton)}
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>
);
})}
</div> </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} {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} </div></div> : null}
</Space> </Space>