204 lines
6.5 KiB
JavaScript
204 lines
6.5 KiB
JavaScript
|
|
import { html } from 'htm/preact';
|
|||
|
|
import { useState, useEffect } from 'preact/hooks';
|
|||
|
|
import { RowDetailModal } from './RowDetailModal.js';
|
|||
|
|
import { Pagination } from './Pagination.js';
|
|||
|
|
import { TableRow } from './TableRow.js';
|
|||
|
|
import { useCellPopover } from '../hooks/useCellPopover.js';
|
|||
|
|
import { useTooltip } from '../hooks/useTooltip.js';
|
|||
|
|
|
|||
|
|
const styles = {
|
|||
|
|
container: {
|
|||
|
|
display: 'flex',
|
|||
|
|
flexDirection: 'column',
|
|||
|
|
gap: '12px',
|
|||
|
|
position: 'relative'
|
|||
|
|
},
|
|||
|
|
loadingBar: {
|
|||
|
|
position: 'absolute',
|
|||
|
|
top: 0,
|
|||
|
|
left: 0,
|
|||
|
|
right: 0,
|
|||
|
|
height: '3px',
|
|||
|
|
background: 'var(--primary)',
|
|||
|
|
zIndex: 100,
|
|||
|
|
animation: 'loading-slide 1.5s ease-in-out infinite'
|
|||
|
|
},
|
|||
|
|
loadingOverlay: {
|
|||
|
|
position: 'fixed',
|
|||
|
|
top: '16px',
|
|||
|
|
left: '50%',
|
|||
|
|
transform: 'translateX(-50%)',
|
|||
|
|
padding: '10px 20px',
|
|||
|
|
background: 'var(--bg-elevated)',
|
|||
|
|
border: '1px solid var(--border-color)',
|
|||
|
|
borderRadius: 'var(--radius-md)',
|
|||
|
|
boxShadow: 'var(--shadow-lg)',
|
|||
|
|
zIndex: 999,
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
gap: '10px',
|
|||
|
|
fontSize: '14px',
|
|||
|
|
color: 'var(--text-primary)',
|
|||
|
|
fontWeight: 500
|
|||
|
|
},
|
|||
|
|
tableWrapper: {
|
|||
|
|
overflowX: 'auto',
|
|||
|
|
background: 'var(--bg-surface)',
|
|||
|
|
border: '1px solid var(--border-color)',
|
|||
|
|
borderRadius: 'var(--radius-md)'
|
|||
|
|
},
|
|||
|
|
table: {
|
|||
|
|
width: '100%',
|
|||
|
|
borderCollapse: 'collapse',
|
|||
|
|
fontSize: '13px'
|
|||
|
|
},
|
|||
|
|
th: {
|
|||
|
|
background: 'var(--bg-elevated)',
|
|||
|
|
color: 'var(--text-secondary)',
|
|||
|
|
fontWeight: 600,
|
|||
|
|
textAlign: 'left',
|
|||
|
|
padding: '12px',
|
|||
|
|
borderBottom: '1px solid var(--border-color)',
|
|||
|
|
position: 'sticky',
|
|||
|
|
top: 0,
|
|||
|
|
zIndex: 1
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export function DataTable({ schema, tableName, totalRows, selectedColumns = [] }) {
|
|||
|
|
const [page, setPage] = useState(0);
|
|||
|
|
const [pageSize, setPageSize] = useState(20);
|
|||
|
|
const [data, setData] = useState([]);
|
|||
|
|
const [loading, setLoading] = useState(false);
|
|||
|
|
const [selectedSeq, setSelectedSeq] = useState(null);
|
|||
|
|
|
|||
|
|
const { showPopover, hidePopover } = useCellPopover();
|
|||
|
|
const { showTooltip, hideTooltip } = useTooltip();
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
fetchData();
|
|||
|
|
}, [tableName, page, pageSize]);
|
|||
|
|
|
|||
|
|
const fetchData = async () => {
|
|||
|
|
try {
|
|||
|
|
setLoading(true);
|
|||
|
|
const offset = page * pageSize;
|
|||
|
|
const response = await fetch(`/api/tables/${tableName}/data?limit=${pageSize}&offset=${offset}`);
|
|||
|
|
if (response.ok) {
|
|||
|
|
const result = await response.json();
|
|||
|
|
setData(result.data || []);
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Failed to fetch data:', error);
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const getColumns = () => {
|
|||
|
|
let columns = [];
|
|||
|
|
|
|||
|
|
if (selectedColumns && selectedColumns.length > 0) {
|
|||
|
|
// 使用选中的列
|
|||
|
|
columns = [...selectedColumns];
|
|||
|
|
} else if (schema && schema.fields) {
|
|||
|
|
// 没有选择时,显示所有字段
|
|||
|
|
columns = schema.fields.map(f => f.name);
|
|||
|
|
} else {
|
|||
|
|
return ['_seq', '_time'];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 过滤掉 _seq 和 _time(它们会被固定放到特定位置)
|
|||
|
|
const filtered = columns.filter(c => c !== '_seq' && c !== '_time');
|
|||
|
|
|
|||
|
|
// _seq 在开头,其他字段在中间,_time 在倒数第二(Actions 列之前)
|
|||
|
|
return ['_seq', ...filtered, '_time'];
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleViewDetail = (seq) => {
|
|||
|
|
setSelectedSeq(seq);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handlePageSizeChange = (newPageSize) => {
|
|||
|
|
setPageSize(newPageSize);
|
|||
|
|
setPage(0);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const getFieldComment = (fieldName) => {
|
|||
|
|
if (!schema || !schema.fields) return '';
|
|||
|
|
const field = schema.fields.find(f => f.name === fieldName);
|
|||
|
|
return field?.comment || '';
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const columns = getColumns();
|
|||
|
|
|
|||
|
|
if (!data || data.length === 0) {
|
|||
|
|
return html`<div class="empty"><p>暂无数据</p></div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return html`
|
|||
|
|
<div style=${styles.container}>
|
|||
|
|
${loading && html`
|
|||
|
|
<div style=${styles.loadingOverlay}>
|
|||
|
|
<span style=${{ fontSize: '16px' }}>⏳</span>
|
|||
|
|
<span>加载中...</span>
|
|||
|
|
</div>
|
|||
|
|
`}
|
|||
|
|
|
|||
|
|
<div style=${styles.tableWrapper}>
|
|||
|
|
<table style=${styles.table}>
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
${columns.map(col => {
|
|||
|
|
const comment = getFieldComment(col);
|
|||
|
|
return html`
|
|||
|
|
<th
|
|||
|
|
key=${col}
|
|||
|
|
style=${styles.th}
|
|||
|
|
onMouseEnter=${(e) => comment && showTooltip(e.currentTarget, comment)}
|
|||
|
|
onMouseLeave=${hideTooltip}
|
|||
|
|
>
|
|||
|
|
${col}
|
|||
|
|
</th>
|
|||
|
|
`;
|
|||
|
|
})}
|
|||
|
|
<th style=${{ ...styles.th, textAlign: 'center' }}>操作</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
${data.map((row, idx) => html`
|
|||
|
|
<${TableRow}
|
|||
|
|
key=${row._seq || idx}
|
|||
|
|
row=${row}
|
|||
|
|
columns=${columns}
|
|||
|
|
onViewDetail=${handleViewDetail}
|
|||
|
|
onShowPopover=${showPopover}
|
|||
|
|
onHidePopover=${hidePopover}
|
|||
|
|
/>
|
|||
|
|
`)}
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 分页控件 -->
|
|||
|
|
<${Pagination}
|
|||
|
|
page=${page}
|
|||
|
|
pageSize=${pageSize}
|
|||
|
|
totalRows=${totalRows}
|
|||
|
|
onPageChange=${setPage}
|
|||
|
|
onPageSizeChange=${handlePageSizeChange}
|
|||
|
|
onJumpToPage=${setPage}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<!-- 详情模态框 -->
|
|||
|
|
${selectedSeq !== null && html`
|
|||
|
|
<${RowDetailModal}
|
|||
|
|
tableName=${tableName}
|
|||
|
|
seq=${selectedSeq}
|
|||
|
|
onClose=${() => setSelectedSeq(null)}
|
|||
|
|
/>
|
|||
|
|
`}
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
}
|