feat(permiss添加icons): 增加表格行拖拽排序功能
parent
37696b13a9
commit
ae75ea4e2c
|
|
@ -478,7 +478,9 @@
|
|||
"iconEmpty": "未找到匹配的图标",
|
||||
"showIconLibrary": "显示图标库",
|
||||
"hideIconLibrary": "收起图标库",
|
||||
"iconLoadingMore": "已加载 {{current}} / {{total}} 个图标"
|
||||
"iconLoadingMore": "已加载 {{current}} / {{total}} 个图标",
|
||||
"commonIcons": "常用图标",
|
||||
"allIcons": "全部图标"
|
||||
},
|
||||
"theme": {
|
||||
"settings": "主题设置",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue