新增拖拉排序
parent
5972182519
commit
658b7e6b59
|
|
@ -9,6 +9,10 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"axios": "^1.6.7",
|
||||
"classnames": "^2.5.1",
|
||||
|
|
@ -436,6 +440,73 @@
|
|||
"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": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz",
|
||||
|
|
@ -4575,6 +4646,12 @@
|
|||
"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": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"axios": "^1.6.7",
|
||||
"classnames": "^2.5.1",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ import React, { useEffect, useMemo, useRef, useState } from "react";
|
|||
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 * 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 { useDict } from "@/hooks/useDict";
|
||||
import { usePermission } from "@/hooks/usePermission";
|
||||
|
|
@ -26,6 +30,42 @@ const legacyIconAliases: Record<string, string> = {
|
|||
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)
|
||||
.filter((key) => /(?:Outlined|Filled|TwoTone)$/.test(key))
|
||||
.sort((left, right) => left.localeCompare(right));
|
||||
|
|
@ -82,6 +122,14 @@ export default function Permissions() {
|
|||
const iconGridRef = useRef<HTMLDivElement | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 1,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
|
@ -106,6 +154,21 @@ export default function Permissions() {
|
|||
}, [data, query]);
|
||||
|
||||
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 selectedIcon = Form.useWatch("icon", form);
|
||||
|
||||
|
|
@ -146,6 +209,46 @@ export default function Permissions() {
|
|||
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 = () => {
|
||||
setEditing(null);
|
||||
form.resetFields();
|
||||
|
|
@ -223,6 +326,21 @@ export default function Permissions() {
|
|||
};
|
||||
|
||||
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"),
|
||||
dataIndex: "name",
|
||||
|
|
@ -314,7 +432,29 @@ export default function Permissions() {
|
|||
</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" } }}>
|
||||
<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>
|
||||
|
||||
<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