From a4afa3a8f5d995b55580576f5534734eb93bf42e Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Wed, 24 Jun 2026 11:49:02 +0800 Subject: [PATCH] Add right-click context menu to view full record in modal Right-click a data row shows a custom menu; choosing 'view full record' opens a Modal displaying all fields (incl. hidden columns) fully wrapped, so long-content fields are completely visible. Inline table keeps ellipsis. Also fixes a pre-existing setState-in-effect lint error in ColumnSettingsModal (render-phase reset) that would fail next build under eslint-plugin-react-hooks v7. --- src/app/page.tsx | 75 +++++++++++++++++++++++++++++++-- src/app/record-detail-modal.tsx | 45 ++++++++++++++++++++ 2 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 src/app/record-detail-modal.tsx diff --git a/src/app/page.tsx b/src/app/page.tsx index 3bc2a0f..20f7588 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -40,6 +40,7 @@ import { import { CSS } from "@dnd-kit/utilities"; import { nextLockedRow, type DataRow } from "./table-filters"; import { buildColumnFilterProps } from "./table-filter-ui"; +import { RecordDetailModal } from "./record-detail-modal"; const { Title, Text } = Typography; @@ -241,15 +242,17 @@ function ColumnSettingsModal({ }) { const [local, setLocal] = useState([]); const [search, setSearch] = useState(""); + const [prevOpen, setPrevOpen] = useState(open); const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 4 } })); - // Sync when modal opens - useEffect(() => { + // Reset local copy whenever the modal opens (render-phase, no setState-in-effect). + if (open !== prevOpen) { + setPrevOpen(open); if (open) { setLocal(config.map((c) => ({ ...c }))); setSearch(""); } - }, [open, config]); + } const filtered = search ? local.filter((c) => c.key.toLowerCase().includes(search.toLowerCase())) @@ -501,6 +504,12 @@ export default function Home() { const [exporting, setExporting] = useState(false); const [lockedRowKey, setLockedRowKey] = useState(null); const [filters, setFilters] = useState>({}); + const [contextMenu, setContextMenu] = useState<{ + record: DataRow; + x: number; + y: number; + } | null>(null); + const [detailRecord, setDetailRecord] = useState(null); // Persist config useEffect(() => { @@ -524,6 +533,8 @@ export default function Home() { setSearched(true); setLockedRowKey(null); setFilters({}); + setContextMenu(null); + setDetailRecord(null); } catch { setError("网络请求失败"); } finally { @@ -538,6 +549,8 @@ export default function Home() { setError(null); setLockedRowKey(null); setFilters({}); + setContextMenu(null); + setDetailRecord(null); }, []); const handleExport = useCallback(async () => { @@ -657,6 +670,10 @@ export default function Home() { onRow={(record) => ({ onClick: () => setLockedRowKey((prev) => nextLockedRow(prev, String(record.ID))), + onContextMenu: (e) => { + e.preventDefault(); + setContextMenu({ record, x: e.clientX, y: e.clientY }); + }, })} rowClassName={(record) => String(record.ID) === lockedRowKey ? "row-locked" : "" @@ -692,6 +709,58 @@ export default function Home() { onApply={handleApplyConfig} onCancel={() => setModalOpen(false)} /> + + {contextMenu && ( + <> +
setContextMenu(null)} + onContextMenu={(e) => { + e.preventDefault(); + setContextMenu(null); + }} + /> +
e.stopPropagation()} + > +
{ + setDetailRecord(contextMenu.record); + setContextMenu(null); + }} + style={{ padding: "6px 14px", cursor: "pointer", fontSize: 14 }} + onMouseEnter={(e) => + (e.currentTarget.style.background = "#f0f7ff") + } + onMouseLeave={(e) => + (e.currentTarget.style.background = "transparent") + } + > + 查看完整记录 +
+
+ + )} + + setDetailRecord(null)} + />
); } diff --git a/src/app/record-detail-modal.tsx b/src/app/record-detail-modal.tsx new file mode 100644 index 0000000..c0fa880 --- /dev/null +++ b/src/app/record-detail-modal.tsx @@ -0,0 +1,45 @@ +"use client"; + +import type { CSSProperties } from "react"; +import { Modal, Descriptions, Empty } from "antd"; +import type { DataRow } from "./table-filters"; +import { pickRecordTitle, formatCellValue } from "./record-detail"; + +const VALUE_STYLE: CSSProperties = { + whiteSpace: "pre-wrap", + wordBreak: "break-all", +}; + +export function RecordDetailModal({ + record, + columns, + open, + onClose, +}: { + record: DataRow | null; + columns: string[]; + open: boolean; + onClose: () => void; +}) { + return ( + + {record && columns.length > 0 ? ( + + {columns.map((col) => ( + + {formatCellValue(record[col])} + + ))} + + ) : ( + + )} + + ); +}