Files
srdb/webui/static/js/components/RowDetailModal.js
bourdon 30c3e74bd2 feat: 完善 WebUI basePath 支持并简化示例代码
主要改动:

1. WebUI basePath 逻辑完善
   - NewWebUI 支持可变参数 basePath
   - 新增 path() 辅助方法统一路径处理
   - handleTableAPI 正确处理 basePath 前缀
   - handleIndex 根据 basePath 替换占位符

2. 简化示例代码
   - 删除反向代理实现(111行)
   - 直接使用带 basePath 的 WebUI
   - 代码量减少 33%,架构更清晰

3. 前端优化
   - 新增 api.js 统一 API 服务层
   - 所有组件使用统一的 API 调用
   - 支持通过 window.API_BASE 配置 basePath

4. 修复 .gitignore
   - 使用通用模式支持 commands 目录
   - 无需为新示例项目修改配置
2025-10-14 22:23:30 +08:00

223 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { html } from 'htm/preact';
import { useState, useEffect, useRef } from 'preact/hooks';
import { getRowBySeq } from '~/utils/api.js';
const styles = {
overlay: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.5)',
backdropFilter: 'blur(4px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 10000,
padding: '20px'
},
modal: {
background: 'var(--bg-elevated)',
borderRadius: 'var(--radius-lg)',
boxShadow: 'var(--shadow-xl)',
maxWidth: '800px',
width: '100%',
maxHeight: '90vh',
display: 'flex',
flexDirection: 'column',
border: '1px solid var(--border-color)'
},
header: {
padding: '20px',
borderBottom: '1px solid var(--border-color)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
},
title: {
fontSize: '18px',
fontWeight: 600,
color: 'var(--text-primary)',
margin: 0
},
closeButton: {
background: 'none',
border: 'none',
fontSize: '24px',
color: 'var(--text-secondary)',
cursor: 'pointer',
padding: '4px 8px',
borderRadius: 'var(--radius-sm)',
transition: 'var(--transition)',
lineHeight: 1
},
content: {
padding: '20px',
overflowY: 'auto',
flex: 1
},
fieldGroup: {
marginBottom: '16px',
padding: '12px',
background: 'var(--bg-surface)',
borderRadius: 'var(--radius-md)',
border: '1px solid var(--border-color)'
},
fieldLabel: {
fontSize: '12px',
fontWeight: 600,
color: 'var(--text-secondary)',
textTransform: 'uppercase',
marginBottom: '4px',
display: 'flex',
alignItems: 'center',
gap: '6px'
},
fieldValue: {
fontSize: '14px',
color: 'var(--text-primary)',
wordBreak: 'break-word',
fontFamily: '"Courier New", monospace',
whiteSpace: 'pre-wrap',
lineHeight: 1.5
},
metaTag: {
display: 'inline-block',
padding: '2px 6px',
fontSize: '10px',
fontWeight: 600,
borderRadius: 'var(--radius-sm)',
background: 'var(--primary-bg)',
color: 'var(--primary)',
marginLeft: '6px'
}
};
export function RowDetailModal({ tableName, seq, onClose }) {
const overlayRef = useRef(null);
const [rowData, setRowData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchRowData();
}, [tableName, seq]);
// ESC 键关闭
useEffect(() => {
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [onClose]);
const fetchRowData = async () => {
try {
setLoading(true);
const data = await getRowBySeq(tableName, seq);
setRowData(data);
} catch (error) {
console.error('Failed to fetch row data:', error);
} finally {
setLoading(false);
}
};
const handleOverlayClick = (e) => {
if (e.target === overlayRef.current) {
onClose();
}
};
const formatTime = (nanoTime) => {
if (!nanoTime) return '';
const date = new Date(nanoTime / 1000000);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
};
const formatValue = (value, key) => {
if (value === null) return 'null';
if (value === undefined) return 'undefined';
if (key === '_time') {
return formatTime(value);
}
if (typeof value === 'object') {
try {
return JSON.stringify(value, null, 2);
} catch (e) {
return '[Object]';
}
}
return String(value);
};
const renderField = (key, value) => {
const isMeta = key === '_seq' || key === '_time';
return html`
<div key=${key} style=${styles.fieldGroup}>
<div style=${styles.fieldLabel}>
${key}
${isMeta && html`<span style=${styles.metaTag}>系统字段</span>`}
</div>
<div style=${styles.fieldValue}>
${formatValue(value, key)}
</div>
</div>
`;
};
return html`
<div
ref=${overlayRef}
style=${styles.overlay}
onClick=${handleOverlayClick}
>
<div style=${styles.modal}>
<div style=${styles.header}>
<h3 style=${styles.title}>记录详情 - ${tableName}</h3>
<button
style=${styles.closeButton}
onClick=${onClose}
onMouseEnter=${(e) => e.target.style.background = 'var(--bg-hover)'}
onMouseLeave=${(e) => e.target.style.background = 'none'}
>
×
</button>
</div>
<div style=${styles.content}>
${loading && html`
<div class="loading" style=${{ textAlign: 'center', padding: '40px' }}>
<p>加载中...</p>
</div>
`}
${!loading && rowData && html`
<div>
${Object.entries(rowData).map(([key, value]) => renderField(key, value))}
</div>
`}
${!loading && !rowData && html`
<div class="empty" style=${{ textAlign: 'center', padding: '40px' }}>
<p>未找到数据</p>
</div>
`}
</div>
</div>
</div>
`;
}