新增拖拉排序
parent
5972182519
commit
658b7e6b59
|
|
@ -9,6 +9,10 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^6.1.0",
|
"@ant-design/icons": "^6.1.0",
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"antd": "^5.13.2",
|
"antd": "^5.13.2",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
|
|
@ -436,6 +440,73 @@
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dnd-kit/accessibility": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/core": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/accessibility": "^3.1.1",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/modifiers": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz",
|
||||||
|
"integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.0",
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/sortable": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.0",
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/utilities": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emotion/hash": {
|
"node_modules/@emotion/hash": {
|
||||||
"version": "0.8.0",
|
"version": "0.8.0",
|
||||||
"resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz",
|
||||||
|
|
@ -4575,6 +4646,12 @@
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^6.1.0",
|
"@ant-design/icons": "^6.1.0",
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"antd": "^5.13.2",
|
"antd": "^5.13.2",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,11 @@ import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button, Card, Col, Drawer, Form, Input, InputNumber, Popconfirm, Row, Select, Space, Table, Tag, Tooltip, Typography, App } from 'antd';
|
import { Button, Card, Col, Drawer, Form, Input, InputNumber, Popconfirm, Row, Select, Space, Table, Tag, Tooltip, Typography, App } from 'antd';
|
||||||
import * as AntIcons from "@ant-design/icons";
|
import * as AntIcons from "@ant-design/icons";
|
||||||
import { CheckSquareOutlined, ClusterOutlined, DeleteOutlined, EditOutlined, FolderOutlined, InfoCircleOutlined, MenuOutlined, PlusOutlined, ReloadOutlined, SearchOutlined } from "@ant-design/icons";
|
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 { CSS } from '@dnd-kit/utilities';
|
||||||
|
import { CheckSquareOutlined, ClusterOutlined, DeleteOutlined, EditOutlined, FolderOutlined, InfoCircleOutlined, MenuOutlined, PlusOutlined, ReloadOutlined, SearchOutlined, HolderOutlined } from "@ant-design/icons";
|
||||||
import { createPermission, deletePermission, listMyPermissions, updatePermission } from "@/api";
|
import { createPermission, deletePermission, listMyPermissions, updatePermission } from "@/api";
|
||||||
import { useDict } from "@/hooks/useDict";
|
import { useDict } from "@/hooks/useDict";
|
||||||
import { usePermission } from "@/hooks/usePermission";
|
import { usePermission } from "@/hooks/usePermission";
|
||||||
|
|
@ -26,6 +30,42 @@ const legacyIconAliases: Record<string, string> = {
|
||||||
setting: "SettingOutlined"
|
setting: "SettingOutlined"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface RowProps extends React.HTMLAttributes<HTMLTableRowElement> {
|
||||||
|
'data-row-key': string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DragContext = React.createContext<any>({});
|
||||||
|
|
||||||
|
const DraggableRow = ({ children, ...props }: RowProps) => {
|
||||||
|
const id = props['data-row-key']?.toString();
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
setActivatorNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({
|
||||||
|
id: id || 'empty-id',
|
||||||
|
});
|
||||||
|
|
||||||
|
const style: React.CSSProperties = {
|
||||||
|
...props.style,
|
||||||
|
transform: CSS.Transform.toString(transform && { ...transform, scaleY: 1 }),
|
||||||
|
transition,
|
||||||
|
...(isDragging ? { position: 'relative', zIndex: 9999, background: '#fafafa', boxShadow: '0 5px 15px rgba(0,0,0,0.15)' } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DragContext.Provider value={{ setActivatorNodeRef, listeners }}>
|
||||||
|
<tr {...props} ref={setNodeRef} style={style} {...attributes}>
|
||||||
|
{children}
|
||||||
|
</tr>
|
||||||
|
</DragContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const menuIconOptions = Object.keys(AntIcons)
|
const menuIconOptions = Object.keys(AntIcons)
|
||||||
.filter((key) => /(?:Outlined|Filled|TwoTone)$/.test(key))
|
.filter((key) => /(?:Outlined|Filled|TwoTone)$/.test(key))
|
||||||
.sort((left, right) => left.localeCompare(right));
|
.sort((left, right) => left.localeCompare(right));
|
||||||
|
|
@ -82,6 +122,14 @@ export default function Permissions() {
|
||||||
const iconGridRef = useRef<HTMLDivElement | null>(null);
|
const iconGridRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
distance: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -106,6 +154,21 @@ export default function Permissions() {
|
||||||
}, [data, query]);
|
}, [data, query]);
|
||||||
|
|
||||||
const treeData = useMemo(() => buildTree(filtered), [filtered]);
|
const treeData = useMemo(() => buildTree(filtered), [filtered]);
|
||||||
|
|
||||||
|
const flatVisualKeys = useMemo(() => {
|
||||||
|
const keys: string[] = [];
|
||||||
|
const traverse = (nodes: TreePermission[]) => {
|
||||||
|
nodes.forEach(node => {
|
||||||
|
keys.push(node.permId.toString());
|
||||||
|
if (node.children && node.children.length > 0) {
|
||||||
|
traverse(node.children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
traverse(treeData);
|
||||||
|
return keys;
|
||||||
|
}, [treeData]);
|
||||||
|
|
||||||
const currentPermType = Form.useWatch("permType", form);
|
const currentPermType = Form.useWatch("permType", form);
|
||||||
const selectedIcon = Form.useWatch("icon", form);
|
const selectedIcon = Form.useWatch("icon", form);
|
||||||
|
|
||||||
|
|
@ -146,6 +209,46 @@ export default function Permissions() {
|
||||||
setIconSearchKeyword("");
|
setIconSearchKeyword("");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDragEnd = async ({ active, over }: DragEndEvent) => {
|
||||||
|
if (active.id !== over?.id) {
|
||||||
|
const activeId = Number(active.id);
|
||||||
|
const overId = Number(over?.id);
|
||||||
|
const activeNode = data.find(n => n.permId === activeId);
|
||||||
|
const overNode = data.find(n => n.permId === overId);
|
||||||
|
if (!activeNode || !overNode) return;
|
||||||
|
if (activeNode.parentId !== overNode.parentId) {
|
||||||
|
message.warning("只能在同级节点之间排序");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const siblings = data
|
||||||
|
.filter(n => n.parentId === activeNode.parentId)
|
||||||
|
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
|
||||||
|
|
||||||
|
const activeIndex = siblings.findIndex(n => n.permId === activeId);
|
||||||
|
const overIndex = siblings.findIndex(n => n.permId === overId);
|
||||||
|
|
||||||
|
const newSiblings = [...siblings];
|
||||||
|
const [removed] = newSiblings.splice(activeIndex, 1);
|
||||||
|
newSiblings.splice(overIndex, 0, removed);
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const updates = newSiblings.map((node, index) => {
|
||||||
|
if (node.sortOrder !== index) {
|
||||||
|
return updatePermission(node.permId, { sortOrder: index });
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
await Promise.all(updates);
|
||||||
|
message.success(t("common.success"));
|
||||||
|
load();
|
||||||
|
} catch (e) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditing(null);
|
setEditing(null);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
|
|
@ -223,6 +326,21 @@ export default function Permissions() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
|
{
|
||||||
|
key: 'sortHandle',
|
||||||
|
width: 40,
|
||||||
|
align: 'center' as const,
|
||||||
|
render: () => {
|
||||||
|
const { setActivatorNodeRef, listeners } = React.useContext(DragContext);
|
||||||
|
return (
|
||||||
|
<HolderOutlined
|
||||||
|
ref={setActivatorNodeRef}
|
||||||
|
style={{ touchAction: 'none', cursor: 'move', color: '#999' }}
|
||||||
|
{...listeners}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t("permissions.permName"),
|
title: t("permissions.permName"),
|
||||||
dataIndex: "name",
|
dataIndex: "name",
|
||||||
|
|
@ -314,7 +432,29 @@ export default function Permissions() {
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="app-page__content-card permissions-content-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}>
|
<Card className="app-page__content-card permissions-content-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}>
|
||||||
<Table className="permissions-table-full" rowKey="permId" loading={loading} dataSource={treeData} columns={columns} pagination={false} size="middle" scroll={{ x: 'max-content', y: '100%' }} expandable={{ defaultExpandAllRows: false, rowExpandable: (record) => record.permType !== "button" && !!record.children?.length }} />
|
<DndContext sensors={sensors} modifiers={[restrictToVerticalAxis]} onDragEnd={onDragEnd}>
|
||||||
|
<SortableContext
|
||||||
|
items={flatVisualKeys}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
className="permissions-table-full"
|
||||||
|
rowKey="permId"
|
||||||
|
loading={loading}
|
||||||
|
dataSource={treeData}
|
||||||
|
columns={columns}
|
||||||
|
pagination={false}
|
||||||
|
size="middle"
|
||||||
|
scroll={{ x: 'max-content', y: '100%' }}
|
||||||
|
expandable={{ defaultExpandAllRows: false, rowExpandable: (record) => record.permType !== "button" && !!record.children?.length, expandIconColumnIndex: 1 }}
|
||||||
|
components={{
|
||||||
|
body: {
|
||||||
|
row: DraggableRow,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Drawer title={<Space><ClusterOutlined aria-hidden="true" /><span>{editing ? t("permissions.drawerTitleEdit") : t("permissions.drawerTitleCreate")}</span></Space>} open={open} onClose={() => setOpen(false)} width={520} destroyOnHidden forceRender footer={<div className="app-page__drawer-footer"><Button onClick={() => setOpen(false)}>{t("common.cancel")}</Button><Button type="primary" loading={saving} onClick={submit}>{t("common.save")}</Button></div>}>
|
<Drawer title={<Space><ClusterOutlined aria-hidden="true" /><span>{editing ? t("permissions.drawerTitleEdit") : t("permissions.drawerTitleCreate")}</span></Space>} open={open} onClose={() => setOpen(false)} width={520} destroyOnHidden forceRender footer={<div className="app-page__drawer-footer"><Button onClick={() => setOpen(false)}>{t("common.cancel")}</Button><Button type="primary" loading={saving} onClick={submit}>{t("common.save")}</Button></div>}>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue