20260812221200
This commit is contained in:
1 parent
87b2d3c482
commit
4bd730f649
47 files changed
+6286
-1881
No files matched your search
@@ -0,0 +1,162 @@
|
||||
import { useState, useCallback, useRef } from "react";
|
||||
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
|
||||
import type { DragEndEvent, Modifier } from "@dnd-kit/core";
|
||||
import { SortableContext, useSortable, horizontalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
const restrictToHorizontalAxis: Modifier = ({ transform }) => ({ ...transform, y: 0 });
|
||||
import type { ModuleField } from "../types";
|
||||
|
||||
interface Props {
|
||||
fields: ModuleField[];
|
||||
activeId: number | null;
|
||||
onActivate: (id: number) => void;
|
||||
onReorder: (activeId: number, overId: number) => void;
|
||||
onResize: (id: number, width: number) => void;
|
||||
}
|
||||
|
||||
function SortableColumn({
|
||||
field,
|
||||
active,
|
||||
isLast,
|
||||
onActivate,
|
||||
onResizeStart,
|
||||
}: {
|
||||
field: ModuleField;
|
||||
active: boolean;
|
||||
isLast: boolean;
|
||||
onActivate: (id: number) => void;
|
||||
onResizeStart: (e: React.MouseEvent, id: number) => void;
|
||||
}) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: field.id });
|
||||
const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.4 : 1 };
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={{ ...style, width: field.listW, flexShrink: 0, position: "relative" }}
|
||||
className={`px-3 py-2 border-r border-slate-200 text-xs font-medium cursor-pointer select-none ${
|
||||
active ? "bg-blue-50 text-blue-600" : "text-slate-600 hover:bg-slate-100"
|
||||
}`}
|
||||
onClick={() => onActivate(field.id)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="text-slate-300 hover:text-slate-500 cursor-grab active:cursor-grabbing shrink-0"
|
||||
title="拖拽排序"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 10h12M6 14h12" />
|
||||
</svg>
|
||||
</span>
|
||||
<span className="truncate">{field.label}</span>
|
||||
<span className="text-[10px] text-slate-300 ml-auto">{field.listW}</span>
|
||||
</div>
|
||||
{!isLast && (
|
||||
<div
|
||||
className="col-resize-handle"
|
||||
onMouseDown={(e) => onResizeStart(e, field.id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TablePreview({ fields, activeId, onActivate, onReorder, onResize }: Props) {
|
||||
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }));
|
||||
const [resizing, setResizing] = useState<{ id: number; startX: number; startW: number } | null>(null);
|
||||
const tableRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (over && active.id !== over.id) {
|
||||
onReorder(Number(active.id), Number(over.id));
|
||||
}
|
||||
},
|
||||
[onReorder]
|
||||
);
|
||||
|
||||
const handleResizeStart = useCallback(
|
||||
(e: React.MouseEvent, id: number) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const field = fields.find((f) => f.id === id);
|
||||
if (!field) return;
|
||||
setResizing({ id, startX: e.clientX, startW: field.listW });
|
||||
},
|
||||
[fields]
|
||||
);
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (!resizing) return;
|
||||
const dx = e.clientX - resizing.startX;
|
||||
const w = Math.max(40, Math.min(400, Math.round(resizing.startW + dx)));
|
||||
onResize(resizing.id, w);
|
||||
setResizing((prev) => (prev ? { ...prev } : null));
|
||||
},
|
||||
[resizing, onResize]
|
||||
);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
setResizing(null);
|
||||
}, []);
|
||||
|
||||
// Attach global listeners for resize
|
||||
if (resizing) {
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
}
|
||||
|
||||
const visible = fields.filter((f) => f.listOn).sort((a, b) => a.order - b.order);
|
||||
if (!visible.length) {
|
||||
return <div className="text-center text-xs text-slate-300 mt-16">暂无可见列 · 请开启字段的列表开关</div>;
|
||||
}
|
||||
|
||||
const totalW = visible.reduce((s, f) => s + f.listW, 0);
|
||||
|
||||
return (
|
||||
<div ref={tableRef}>
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd} modifiers={[restrictToHorizontalAxis]}>
|
||||
<SortableContext items={visible.map((f) => f.id)} strategy={horizontalListSortingStrategy}>
|
||||
<div className="overflow-x-auto border border-slate-200 rounded-lg bg-white shadow-sm">
|
||||
<div style={{ minWidth: Math.max(totalW, 400) }}>
|
||||
<div className="flex bg-slate-50 border-b border-slate-200">
|
||||
{visible.map((f, i) => (
|
||||
<SortableColumn
|
||||
key={f.id}
|
||||
field={f}
|
||||
active={f.id === activeId}
|
||||
isLast={i === visible.length - 1}
|
||||
onActivate={onActivate}
|
||||
onResizeStart={handleResizeStart}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{[0, 1].map((r) => (
|
||||
<div key={r} className="flex border-b border-slate-50 text-xs text-slate-300">
|
||||
{visible.map((f) => (
|
||||
<div
|
||||
key={f.id}
|
||||
className="shrink-0 px-3 py-2 border-r border-slate-50"
|
||||
style={{ width: f.listW, textAlign: f.listAlign }}
|
||||
>
|
||||
{f.type === "select" ? "—" : f.type === "date" ? "2024-08-12" : f.type === "number" ? "100.00" : "示例"}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
<div className="mt-2 text-[11px] text-slate-400 text-center">
|
||||
拖拽列头排序 · 拖拽右边缘调整宽度 · 点击列头选中字段
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user