feat(data-grid): 新增底部数据预览面板支持长数据字段完整查看与编辑

- 工具栏新增「数据预览」切换按钮,点击展开/收起底部面板
- 单击单元格自动更新面板内容,完整展示长文本和 JSON 数据
- 面板使用 Monaco Editor,JSON 数据自动语法高亮
- 编辑模式下支持直接修改并保存,只读模式下 Editor 设为 readOnly
- 支持 JSON 一键格式化功能
- 通过 ref 追踪面板状态避免 mergedColumns 过度重渲染
- refs #271
This commit is contained in:
Syngnat
2026-03-20 15:37:17 +08:00
parent 84579b83c9
commit 17e4e3ad1c

View File

@@ -1112,6 +1112,14 @@ const DataGrid: React.FC<DataGridProps> = ({
const cellEditorApplyRef = useRef<((val: string) => void) | null>(null);
const [jsonEditorOpen, setJsonEditorOpen] = useState(false);
const [jsonEditorValue, setJsonEditorValue] = useState('');
// --- Data Preview Panel State ---
const [dataPanelOpen, setDataPanelOpen] = useState(false);
const dataPanelOpenRef = useRef(false);
const [focusedCellInfo, setFocusedCellInfo] = useState<{ record: Item; dataIndex: string; title: string } | null>(null);
const [dataPanelValue, setDataPanelValue] = useState('');
const [dataPanelIsJson, setDataPanelIsJson] = useState(false);
const dataPanelDirtyRef = useRef(false);
const [rowEditorOpen, setRowEditorOpen] = useState(false);
const [rowEditorRowKey, setRowEditorRowKey] = useState<string>('');
const rowEditorBaseRawRef = useRef<Record<string, any>>({});
@@ -1420,6 +1428,34 @@ const DataGrid: React.FC<DataGridProps> = ({
cellEditorApplyRef.current = null;
}, []);
// --- Data Preview Panel Helpers ---
const updateFocusedCell = useCallback((record: Item, dataIndex: string) => {
if (!record || !dataIndex) return;
const raw = record?.[dataIndex];
const text = toEditableText(raw);
const isJson = looksLikeJsonText(text);
setFocusedCellInfo({ record, dataIndex, title: dataIndex });
// 仅在面板未被用户手动编辑时自动同步值
if (!dataPanelDirtyRef.current) {
setDataPanelValue(text);
setDataPanelIsJson(isJson);
}
}, []);
const handleDataPanelFormatJson = useCallback(() => {
if (!dataPanelIsJson) return;
try {
const obj = JSON.parse(dataPanelValue);
setDataPanelValue(JSON.stringify(obj, null, 2));
dataPanelDirtyRef.current = true;
} catch (e: any) {
void message.error('JSON 格式无效:' + (e?.message || String(e)));
}
}, [dataPanelIsJson, dataPanelValue]);
// 同步 ref 用于 onCell 闭包
useEffect(() => { dataPanelOpenRef.current = dataPanelOpen; }, [dataPanelOpen]);
const openCellEditor = useCallback((record: Item, dataIndex: string, title: React.ReactNode, onApplyValue?: (val: string) => void) => {
if (!record || !dataIndex) return;
const raw = record?.[dataIndex];
@@ -2818,6 +2854,14 @@ const DataGrid: React.FC<DataGridProps> = ({
}
}, [addedRows]);
const handleDataPanelSave = useCallback(() => {
if (!focusedCellInfo) return;
const nextRow: any = { ...focusedCellInfo.record, [focusedCellInfo.dataIndex]: dataPanelValue };
handleCellSave(nextRow);
dataPanelDirtyRef.current = false;
void message.success('已保存');
}, [focusedCellInfo, dataPanelValue, handleCellSave]);
const handleCellSetNull = useCallback(() => {
if (!cellContextMenu.record) return;
handleCellSave({ ...cellContextMenu.record, [cellContextMenu.dataIndex]: null });
@@ -3228,6 +3272,12 @@ const DataGrid: React.FC<DataGridProps> = ({
'data-row-key': rowKey === undefined || rowKey === null ? undefined : String(rowKey),
'data-col-name': dataIndex,
};
// 数据预览面板:单击单元格时更新聚焦信息
cellProps.onClick = () => {
if (dataPanelOpenRef.current) {
updateFocusedCell(record, dataIndex);
}
};
if (col.editable && enableInlineEditableCell) {
// 可编辑模式(非虚拟):传递给 EditableCell 的 props
@@ -4613,6 +4663,24 @@ const DataGrid: React.FC<DataGridProps> = ({
)}
<div style={{ marginLeft: 'auto' }} />
<div style={{ flexShrink: 0 }}>
<Button
icon={<EditOutlined />}
type={dataPanelOpen ? 'primary' : 'default'}
onClick={() => {
const next = !dataPanelOpen;
setDataPanelOpen(next);
if (!next) {
setFocusedCellInfo(null);
setDataPanelValue('');
setDataPanelIsJson(false);
dataPanelDirtyRef.current = false;
}
}}
>
</Button>
</div>
<div style={{ flexShrink: 0 }}>
<Popover
trigger="click"
@@ -5142,6 +5210,75 @@ const DataGrid: React.FC<DataGridProps> = ({
</div>
)}
{/* Data Preview Panel */}
{dataPanelOpen && viewMode === 'table' && (
<div style={{
height: 200,
borderTop: darkMode ? '1px solid rgba(255,255,255,0.12)' : '1px solid rgba(0,0,0,0.12)',
display: 'flex',
flexDirection: 'column',
background: darkMode ? 'rgba(0,0,0,0.15)' : 'rgba(255,255,255,0.6)',
flexShrink: 0,
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '4px 10px',
fontSize: 12,
borderBottom: darkMode ? '1px solid rgba(255,255,255,0.06)' : '1px solid rgba(0,0,0,0.06)',
flexShrink: 0,
}}>
<span style={{ color: darkMode ? '#aaa' : '#666', fontWeight: 500 }}>
{focusedCellInfo ? focusedCellInfo.dataIndex : '点击单元格查看数据'}
</span>
{focusedCellInfo && (() => {
const meta = columnMetaMap[focusedCellInfo.dataIndex] || columnMetaMapByLowerName[focusedCellInfo.dataIndex.toLowerCase()];
return meta?.type ? <span style={{ color: '#888', fontSize: 11 }}>({meta.type})</span> : null;
})()}
<div style={{ flex: 1 }} />
{dataPanelIsJson && (
<Button size="small" onClick={handleDataPanelFormatJson}> JSON</Button>
)}
{canModifyData && focusedCellInfo && (
<Button size="small" type="primary" onClick={handleDataPanelSave}></Button>
)}
</div>
<div style={{ flex: 1, minHeight: 0 }}>
{focusedCellInfo ? (
<Editor
height="100%"
language={dataPanelIsJson ? 'json' : 'plaintext'}
theme={darkMode ? 'transparent-dark' : 'transparent-light'}
value={dataPanelValue}
onChange={(val) => {
setDataPanelValue(val || '');
dataPanelDirtyRef.current = true;
}}
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
wordWrap: 'on',
fontSize: 13,
tabSize: 2,
automaticLayout: true,
readOnly: !canModifyData,
lineNumbers: 'off',
glyphMargin: false,
folding: false,
lineDecorationsWidth: 4,
padding: { top: 6, bottom: 6 },
}}
/>
) : (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: '#999', fontSize: 13 }}>
</div>
)}
</div>
</div>
)}
{/* Cell Context Menu - 使用 Portal 渲染到 body避免 backdropFilter 影响 fixed 定位 */}
{viewMode === 'table' && cellContextMenu.visible && createPortal(
<div