mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-25 02:00:09 +08:00
✨ feat(data-grid): 新增底部数据预览面板支持长数据字段完整查看与编辑
- 工具栏新增「数据预览」切换按钮,点击展开/收起底部面板 - 单击单元格自动更新面板内容,完整展示长文本和 JSON 数据 - 面板使用 Monaco Editor,JSON 数据自动语法高亮 - 编辑模式下支持直接修改并保存,只读模式下 Editor 设为 readOnly - 支持 JSON 一键格式化功能 - 通过 ref 追踪面板状态避免 mergedColumns 过度重渲染 - refs #271
This commit is contained in:
@@ -1112,6 +1112,14 @@ const DataGrid: React.FC<DataGridProps> = ({
|
|||||||
const cellEditorApplyRef = useRef<((val: string) => void) | null>(null);
|
const cellEditorApplyRef = useRef<((val: string) => void) | null>(null);
|
||||||
const [jsonEditorOpen, setJsonEditorOpen] = useState(false);
|
const [jsonEditorOpen, setJsonEditorOpen] = useState(false);
|
||||||
const [jsonEditorValue, setJsonEditorValue] = useState('');
|
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 [rowEditorOpen, setRowEditorOpen] = useState(false);
|
||||||
const [rowEditorRowKey, setRowEditorRowKey] = useState<string>('');
|
const [rowEditorRowKey, setRowEditorRowKey] = useState<string>('');
|
||||||
const rowEditorBaseRawRef = useRef<Record<string, any>>({});
|
const rowEditorBaseRawRef = useRef<Record<string, any>>({});
|
||||||
@@ -1420,6 +1428,34 @@ const DataGrid: React.FC<DataGridProps> = ({
|
|||||||
cellEditorApplyRef.current = null;
|
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) => {
|
const openCellEditor = useCallback((record: Item, dataIndex: string, title: React.ReactNode, onApplyValue?: (val: string) => void) => {
|
||||||
if (!record || !dataIndex) return;
|
if (!record || !dataIndex) return;
|
||||||
const raw = record?.[dataIndex];
|
const raw = record?.[dataIndex];
|
||||||
@@ -2818,6 +2854,14 @@ const DataGrid: React.FC<DataGridProps> = ({
|
|||||||
}
|
}
|
||||||
}, [addedRows]);
|
}, [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(() => {
|
const handleCellSetNull = useCallback(() => {
|
||||||
if (!cellContextMenu.record) return;
|
if (!cellContextMenu.record) return;
|
||||||
handleCellSave({ ...cellContextMenu.record, [cellContextMenu.dataIndex]: null });
|
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-row-key': rowKey === undefined || rowKey === null ? undefined : String(rowKey),
|
||||||
'data-col-name': dataIndex,
|
'data-col-name': dataIndex,
|
||||||
};
|
};
|
||||||
|
// 数据预览面板:单击单元格时更新聚焦信息
|
||||||
|
cellProps.onClick = () => {
|
||||||
|
if (dataPanelOpenRef.current) {
|
||||||
|
updateFocusedCell(record, dataIndex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (col.editable && enableInlineEditableCell) {
|
if (col.editable && enableInlineEditableCell) {
|
||||||
// 可编辑模式(非虚拟):传递给 EditableCell 的 props
|
// 可编辑模式(非虚拟):传递给 EditableCell 的 props
|
||||||
@@ -4613,6 +4663,24 @@ const DataGrid: React.FC<DataGridProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ marginLeft: 'auto' }} />
|
<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 }}>
|
<div style={{ flexShrink: 0 }}>
|
||||||
<Popover
|
<Popover
|
||||||
trigger="click"
|
trigger="click"
|
||||||
@@ -5142,6 +5210,75 @@ const DataGrid: React.FC<DataGridProps> = ({
|
|||||||
</div>
|
</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 定位 */}
|
{/* Cell Context Menu - 使用 Portal 渲染到 body,避免 backdropFilter 影响 fixed 定位 */}
|
||||||
{viewMode === 'table' && cellContextMenu.visible && createPortal(
|
{viewMode === 'table' && cellContextMenu.visible && createPortal(
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user