Files
workspace/code/test-demo/src/components/TablePreview.tsx
T
2026-08-12 22:12:01 +08:00

163 lines
5.9 KiB
TypeScript

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>
);
}