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.
This commit is contained in:
@@ -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<ColumnConfig[]>([]);
|
||||
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<string | null>(null);
|
||||
const [filters, setFilters] = useState<Record<string, React.Key[]>>({});
|
||||
const [contextMenu, setContextMenu] = useState<{
|
||||
record: DataRow;
|
||||
x: number;
|
||||
y: number;
|
||||
} | null>(null);
|
||||
const [detailRecord, setDetailRecord] = useState<DataRow | null>(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 && (
|
||||
<>
|
||||
<div
|
||||
style={{ position: "fixed", inset: 0, zIndex: 1049 }}
|
||||
onClick={() => setContextMenu(null)}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
setContextMenu(null);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
left: contextMenu.x,
|
||||
top: contextMenu.y,
|
||||
zIndex: 1050,
|
||||
background: "#fff",
|
||||
borderRadius: 8,
|
||||
boxShadow: "0 6px 16px rgba(0,0,0,0.12)",
|
||||
border: "1px solid #f0f0f0",
|
||||
padding: 4,
|
||||
minWidth: 140,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
onContextMenu={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
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")
|
||||
}
|
||||
>
|
||||
查看完整记录
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<RecordDetailModal
|
||||
record={detailRecord}
|
||||
columns={columns}
|
||||
open={!!detailRecord}
|
||||
onClose={() => setDetailRecord(null)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
45
src/app/record-detail-modal.tsx
Normal file
45
src/app/record-detail-modal.tsx
Normal file
@@ -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 (
|
||||
<Modal
|
||||
title={`记录详情:${record ? pickRecordTitle(record) : ""}`}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width={900}
|
||||
>
|
||||
{record && columns.length > 0 ? (
|
||||
<Descriptions column={2} bordered size="small">
|
||||
{columns.map((col) => (
|
||||
<Descriptions.Item key={col} label={col}>
|
||||
<span style={VALUE_STYLE}>{formatCellValue(record[col])}</span>
|
||||
</Descriptions.Item>
|
||||
))}
|
||||
</Descriptions>
|
||||
) : (
|
||||
<Empty description="无数据" />
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user