diff --git a/public/_locales/zh_CN/messages.json b/public/_locales/zh_CN/messages.json index 1fe5ccb..bd83827 100644 --- a/public/_locales/zh_CN/messages.json +++ b/public/_locales/zh_CN/messages.json @@ -1043,6 +1043,10 @@ "message": "配置字段后点击「生成数据」按钮", "description": "Translation key: testDataGenerator_noDataHint" }, + "testDataGenerator_sampleData": { + "message": "示例数据", + "description": "Translation key: testDataGenerator_sampleData" + }, "testDataGenerator_copied": { "message": "已复制", "description": "Translation key: testDataGenerator_copied" diff --git a/src/pages/TestDataGenerator/components/DataPreview.tsx b/src/pages/TestDataGenerator/components/DataPreview.tsx index ce162f9..3359c32 100644 --- a/src/pages/TestDataGenerator/components/DataPreview.tsx +++ b/src/pages/TestDataGenerator/components/DataPreview.tsx @@ -1,173 +1,108 @@ /** * 数据预览组件 - * 展示生成的数据,支持 JSON 和 CSV 格式切换 - * 大数据量使用虚拟列表优化性能 + * 展示一条示例数据,展示数据结构 */ -import { useState, useRef, useCallback, useMemo } from 'react'; -import { Copy, Check, FileJson, FileText } from 'lucide-react'; -import { Button } from '@/components/ui/button'; +import { useMemo } from 'react'; +import { FileJson, FileText } from 'lucide-react'; import { useI18n } from '@/utils/chromeI18n'; -import { DataExporter } from '@/utils/dataExporter'; -import type { GenerateResult } from '@/types/testDataGenerator'; +import { getGeneratorById } from '@/lib/generators'; +import type { FieldConfig } from '@/types/testDataGenerator'; interface DataPreviewProps { - result: GenerateResult | null; + fields: FieldConfig[]; } -type PreviewFormat = 'json' | 'csv'; +/** JSON 语法高亮渲染 */ +function JsonHighlight({ data }: { data: Record }) { + const formatted = useMemo(() => { + const lines: { indent: string; key?: string; value: string; isLast: boolean }[] = []; + const entries = Object.entries(data); -/** 虚拟列表配置 */ -const VIRTUAL_ROW_HEIGHT = 20; -const VIRTUAL_OVERSCAN = 5; + entries.forEach(([key, value], index) => { + const isLast = index === entries.length - 1; + const formattedValue = + value === null ? 'null' : typeof value === 'string' ? `"${value}"` : String(value); + lines.push({ indent: ' ', key, value: formattedValue, isLast }); + }); -export default function DataPreview({ result }: DataPreviewProps) { - const { t } = useI18n('testDataGenerator'); - const [format, setFormat] = useState('json'); - const [copied, setCopied] = useState(false); - const [scrollTop, setScrollTop] = useState(0); - const containerRef = useRef(null); + return lines; + }, [data]); - const data = result?.data; - const hasData = data && data.length > 0; - - // 将数据转换为预渲染的行文本(避免每次滚动重复计算) - const rows = useMemo(() => { - if (!hasData) return []; - if (format === 'json') { - return data.map((item) => JSON.stringify(item)); - } - // CSV 格式:先生成表头,再逐行 - const headers = Array.from(new Set(data.flatMap((row) => Object.keys(row)))); - const headerLine = headers.join(','); - const dataLines = data.map((row) => - headers - .map((h) => { - const val = String(row[h] ?? ''); - return val.includes(',') || val.includes('"') || val.includes('\n') - ? `"${val.replace(/"/g, '""')}"` - : val; - }) - .join(','), - ); - return [headerLine, ...dataLines]; - }, [data, hasData, format]); - - const totalRows = rows.length; - const isLargeDataset = totalRows > 100; - const containerHeight = 400; - - // 虚拟列表:计算可见范围 - const visibleStart = Math.max(0, Math.floor(scrollTop / VIRTUAL_ROW_HEIGHT) - VIRTUAL_OVERSCAN); - const visibleEnd = Math.min( - totalRows, - Math.ceil((scrollTop + containerHeight) / VIRTUAL_ROW_HEIGHT) + VIRTUAL_OVERSCAN, + return ( +
+      {'{'}
+      {formatted.map((line, i) => (
+        
+ {line.indent} + "{line.key}" + {': '} + {line.value} + {!line.isLast && ,} +
+ ))} + {'}'} +
); - const visibleRows = rows.slice(visibleStart, visibleEnd); - const totalHeight = totalRows * VIRTUAL_ROW_HEIGHT; +} - const handleScroll = useCallback(() => { - if (containerRef.current) { - setScrollTop(containerRef.current.scrollTop); +/** 根据值类型返回颜色类名 */ +function getValueColor(value: string): string { + if (value === 'null') return 'text-muted-foreground'; + if (value.startsWith('"')) return 'text-emerald-600 dark:text-emerald-400'; + if (/^\d+$/.test(value)) return 'text-amber-600 dark:text-amber-400'; + if (value === 'true' || value === 'false') return 'text-purple-600 dark:text-purple-400'; + return 'text-foreground'; +} + +export default function DataPreview({ fields }: DataPreviewProps) { + const { t } = useI18n('testDataGenerator'); + + // 生成一条示例数据 + const sampleData = useMemo(() => { + if (fields.length === 0) return null; + + const record: Record = {}; + for (const field of fields) { + const generator = getGeneratorById(field.generatorId); + if (generator) { + record[field.name] = generator.generate(field.params); + } else { + record[field.name] = null; + } } - }, []); + return record; + }, [fields]); - const handleCopy = async () => { - if (!hasData) return; - const fullContent = format === 'json' ? DataExporter.toJSON(data) : DataExporter.toCSV(data); - const success = await DataExporter.copyToClipboard(fullContent); - if (success) { - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } - }; - - if (!hasData) { + if (!sampleData) { return ( -
- -

{t('testDataGenerator_noData')}

-

{t('testDataGenerator_noDataHint')}

+
+ +

{t('testDataGenerator_noDataHint')}

); } return (
- {/* 工具栏 */} + {/* 示例标签 */}
- - - {isLargeDataset && ( - - {t('testDataGenerator_virtualMode', { count: totalRows })} - - )} -
- - -
- - {/* 虚拟列表预览 */} -
- {isLargeDataset ? ( - /* 虚拟滚动模式 */ -
-
- {visibleRows.map((row, i) => ( -
- {row} -
- ))} -
+
+
- ) : ( - /* 普通模式(小数据量) */ -
-            {rows.join('\n')}
-          
- )} + + {t('testDataGenerator_sampleData')} + +
+ + {Object.keys(sampleData).length} {t('testDataGenerator_fields')} +
- {/* 数据统计 */} -
- {t('testDataGenerator_totalRows', { count: data.length })} + {/* 示例数据展示 */} +
+
); diff --git a/src/pages/TestDataGenerator/index.tsx b/src/pages/TestDataGenerator/index.tsx index f8b0193..a3c2163 100644 --- a/src/pages/TestDataGenerator/index.tsx +++ b/src/pages/TestDataGenerator/index.tsx @@ -7,7 +7,6 @@ import { Settings, Database, Tag } from 'lucide-react'; import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; import { useGenerator } from './hooks/useGenerator'; -import { getGeneratorById } from '@/lib/generators'; import type { FieldConfig, GenerateResult } from '@/types/testDataGenerator'; import { Dialog, DialogContent } from '@/components/ui/dialog'; import { toast } from 'sonner'; @@ -45,61 +44,9 @@ export default function TestDataGeneratorPage() { // 当前标签页 const [activeTab, setActiveTab] = useState('fields'); - // 实时预览(防抖) - const [previewResult, setPreviewResult] = useState(null); - const debounceTimerRef = useRef | null>(null); + // 生成完成时显示 toast 提示(避免重复触发) const lastToastResultRef = useRef(null); - // 配置变更时生成预览数据(防抖 300ms) - useEffect(() => { - if (debounceTimerRef.current) { - clearTimeout(debounceTimerRef.current); - } - - // 无字段时不生成预览 - if (fields.length === 0) { - debounceTimerRef.current = setTimeout(() => setPreviewResult(null), 0); - return () => { - if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current); - }; - } - - debounceTimerRef.current = setTimeout(() => { - try { - const previewData: Record[] = []; - const sampleCount = Math.min(10, count); - for (let i = 0; i < sampleCount; i++) { - const record: Record = {}; - for (const field of fields) { - const generator = getGeneratorById(field.generatorId); - if (!generator) continue; - if (!field.required && Math.random() * 100 < field.nullRate) { - record[field.name] = null; - continue; - } - if (field.unique && generator.generateAtIndex) { - record[field.name] = generator.generateAtIndex(field.params, i); - } else { - record[field.name] = generator.generate(field.params); - } - } - previewData.push(record); - } - setPreviewResult({ - success: true, - data: previewData, - stats: { total: sampleCount, success: sampleCount, failed: 0, duration: 0 }, - }); - } catch { - setPreviewResult(null); - } - }, 300); - - return () => { - if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current); - }; - }, [fields, count]); - // 生成完成时显示 toast 提示(避免重复触发) useEffect(() => { if (result?.success && result.stats && result !== lastToastResultRef.current) { @@ -300,8 +247,8 @@ export default function TestDataGeneratorPage() { {t('testDataGenerator_dataPreview')} -
- +
+