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