From 9a0c8ba850d481b789fe5a7e534fd3f175ef2022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E9=9C=96=E9=93=83?= Date: Sat, 6 Jun 2026 23:33:12 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=20features=20?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=96=AD=E8=A8=80=E4=BB=A5=E5=8C=B9=E9=85=8D?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=9A=84=20testDataGenerator=20fix:=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B5=8B=E8=AF=95=E6=95=B0=E6=8D=AE=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=99=A8=E5=A4=9A=E9=A1=B9=E4=BB=A3=E7=A0=81=E9=97=AE?= =?UTF-8?q?=E9=A2=98=20-=20useCallback=20=E9=97=AD=E5=8C=85=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=9A=E6=94=B9=E7=94=A8=E5=87=BD=E6=95=B0=E5=BC=8F?= =?UTF-8?q?=20setFields=20=E6=9B=B4=E6=96=B0=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E8=BF=87=E6=97=B6=E5=BC=95=E7=94=A8=20-=20Wo?= =?UTF-8?q?rker=20=E5=A4=8D=E7=94=A8=EF=BC=9A=E5=A4=8D=E7=94=A8=E5=B7=B2?= =?UTF-8?q?=E6=9C=89=20Worker=20=E8=80=8C=E9=9D=9E=E6=AF=8F=E6=AC=A1=20gen?= =?UTF-8?q?erate=20=E9=83=BD=E9=94=80=E6=AF=81=E9=87=8D=E5=BB=BA=20-=20?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=E5=A4=8D=E5=88=B6=E5=8F=8D=E9=A6=88=EF=BC=9A?= =?UTF-8?q?=E5=A4=8D=E5=88=B6=E6=93=8D=E4=BD=9C=E5=AE=8C=E6=88=90=E5=90=8E?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20toast=20=E6=88=90=E5=8A=9F/=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E6=8F=90=E7=A4=BA=20-=20=E5=AD=97=E6=AE=B5=E5=90=8D?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=EF=BC=9A=E6=B7=BB=E5=8A=A0=E7=A9=BA=E5=80=BC?= =?UTF-8?q?=E3=80=81=E9=9D=9E=E6=B3=95=E5=AD=97=E7=AC=A6=E3=80=81=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E5=90=8D=E7=A7=B0=E6=A0=A1=E9=AA=8C=20-=20i18n=20?= =?UTF-8?q?=E7=A1=AC=E7=BC=96=E7=A0=81=EF=BC=9AGeneratorConfig=20=E5=92=8C?= =?UTF-8?q?=20ruleStorage=20=E7=BB=9F=E4=B8=80=E4=BD=BF=E7=94=A8=20t()=20?= =?UTF-8?q?=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/_locales/zh_CN/messages.json | 40 ++++++++ src/config/__tests__/features.test.ts | 11 ++- .../components/ExportPanel.tsx | 15 ++- .../components/FieldEditor.tsx | 32 ++++++- .../components/GeneratorConfig.tsx | 14 ++- .../components/RuleManager.tsx | 2 +- .../components/__tests__/RuleManager.test.tsx | 3 +- .../TestDataGenerator/hooks/useGenerator.ts | 7 +- src/pages/TestDataGenerator/index.tsx | 91 +++++++++---------- src/utils/ruleStorage.ts | 6 +- 10 files changed, 154 insertions(+), 67 deletions(-) diff --git a/public/_locales/zh_CN/messages.json b/public/_locales/zh_CN/messages.json index 845a150..9717b5d 100644 --- a/public/_locales/zh_CN/messages.json +++ b/public/_locales/zh_CN/messages.json @@ -1260,5 +1260,45 @@ "testDataGenerator_saveAs": { "message": "另存为", "description": "Translation key: testDataGenerator_saveAs" + }, + "testDataGenerator_noGeneratorParams": { + "message": "此生成器无可配置参数", + "description": "Translation key: testDataGenerator_noGeneratorParams" + }, + "testDataGenerator_enabled": { + "message": "启用", + "description": "Translation key: testDataGenerator_enabled" + }, + "testDataGenerator_disabled": { + "message": "禁用", + "description": "Translation key: testDataGenerator_disabled" + }, + "testDataGenerator_commaSeparated": { + "message": "用逗号分隔多个值", + "description": "Translation key: testDataGenerator_commaSeparated" + }, + "testDataGenerator_copySuccess": { + "message": "已复制到剪贴板", + "description": "Translation key: testDataGenerator_copySuccess" + }, + "testDataGenerator_copyFailed": { + "message": "复制失败", + "description": "Translation key: testDataGenerator_copyFailed" + }, + "testDataGenerator_fieldNameEmpty": { + "message": "字段名称不能为空", + "description": "Translation key: testDataGenerator_fieldNameEmpty" + }, + "testDataGenerator_fieldNameDuplicate": { + "message": "字段名称已存在", + "description": "Translation key: testDataGenerator_fieldNameDuplicate" + }, + "testDataGenerator_fieldNameInvalid": { + "message": "字段名称只能包含字母、数字和下划线", + "description": "Translation key: testDataGenerator_fieldNameInvalid" + }, + "testDataGenerator_ruleCopySuffix": { + "message": "(副本)", + "description": "Translation key: testDataGenerator_ruleCopySuffix" } } diff --git a/src/config/__tests__/features.test.ts b/src/config/__tests__/features.test.ts index ea1feda..6ac2402 100644 --- a/src/config/__tests__/features.test.ts +++ b/src/config/__tests__/features.test.ts @@ -9,8 +9,8 @@ import { describe('features', () => { describe('FEATURES', () => { - it('should have 9 features defined', () => { - expect(FEATURES).toHaveLength(9); + it('should have 10 features defined', () => { + expect(FEATURES).toHaveLength(10); }); it('should have all required properties for each feature', () => { @@ -95,7 +95,7 @@ describe('features', () => { describe('getAllFeatureKeys', () => { it('should return all feature keys', () => { const allKeys = getAllFeatureKeys(); - expect(allKeys).toHaveLength(9); + expect(allKeys).toHaveLength(10); expect(allKeys).toContain('dashboard'); expect(allKeys).toContain('timestamp'); expect(allKeys).toContain('storageCleaner'); @@ -105,6 +105,7 @@ describe('features', () => { expect(allKeys).toContain('jsonDiff'); expect(allKeys).toContain('base64Converter'); expect(allKeys).toContain('rightClickRestorer'); + expect(allKeys).toContain('testDataGenerator'); }); }); @@ -121,9 +122,9 @@ describe('features', () => { expect(pageOrder).toContain('qrCode'); }); - it('should have 8 items in page order', () => { + it('should have 9 items in page order', () => { const pageOrder = getDefaultPageOrder(); - expect(pageOrder).toHaveLength(8); + expect(pageOrder).toHaveLength(9); }); }); }); diff --git a/src/pages/TestDataGenerator/components/ExportPanel.tsx b/src/pages/TestDataGenerator/components/ExportPanel.tsx index e80efee..f16a1cc 100644 --- a/src/pages/TestDataGenerator/components/ExportPanel.tsx +++ b/src/pages/TestDataGenerator/components/ExportPanel.tsx @@ -4,6 +4,7 @@ */ import { Copy, Download } from 'lucide-react'; +import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { useI18n } from '@/utils/chromeI18n'; import { DataExporter } from '@/utils/dataExporter'; @@ -22,12 +23,22 @@ export default function ExportPanel({ result }: ExportPanelProps) { const handleCopyJSON = async () => { const content = DataExporter.toJSON(result.data!); - await DataExporter.copyToClipboard(content); + const success = await DataExporter.copyToClipboard(content); + if (success) { + toast.success(t('testDataGenerator_copySuccess')); + } else { + toast.error(t('testDataGenerator_copyFailed')); + } }; const handleCopyCSV = async () => { const content = DataExporter.toCSV(result.data!); - await DataExporter.copyToClipboard(content); + const success = await DataExporter.copyToClipboard(content); + if (success) { + toast.success(t('testDataGenerator_copySuccess')); + } else { + toast.error(t('testDataGenerator_copyFailed')); + } }; const handleDownloadJSON = () => { diff --git a/src/pages/TestDataGenerator/components/FieldEditor.tsx b/src/pages/TestDataGenerator/components/FieldEditor.tsx index 4241354..3a5d740 100644 --- a/src/pages/TestDataGenerator/components/FieldEditor.tsx +++ b/src/pages/TestDataGenerator/components/FieldEditor.tsx @@ -3,6 +3,7 @@ * 编辑单个字段的详细配置 */ +import { useState, useCallback } from 'react'; import { useI18n } from '@/utils/chromeI18n'; import { Input } from '@/components/ui/input'; import { Switch } from '@/components/ui/switch'; @@ -15,14 +16,40 @@ import GeneratorConfig from './GeneratorConfig'; interface FieldEditorProps { field: FieldConfig; onChange: (field: FieldConfig) => void; + /** 所有字段名列表,用于检测重复 */ + allFieldNames?: string[]; } -export default function FieldEditor({ field, onChange }: FieldEditorProps) { +export default function FieldEditor({ field, onChange, allFieldNames = [] }: FieldEditorProps) { const { t } = useI18n('testDataGenerator'); const generator = getGeneratorById(field.generatorId); + const [nameError, setNameError] = useState(null); + + const validateFieldName = useCallback( + (name: string): string | null => { + const trimmed = name.trim(); + if (!trimmed) { + return t('testDataGenerator_fieldNameEmpty'); + } + if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) { + return t('testDataGenerator_fieldNameInvalid'); + } + const isDuplicate = allFieldNames.some( + (n, i) => n === trimmed && i !== allFieldNames.indexOf(field.name), + ); + if (isDuplicate) { + return t('testDataGenerator_fieldNameDuplicate'); + } + return null; + }, + [allFieldNames, field.name, t], + ); const handleNameChange = (name: string) => { onChange({ ...field, name }); + // 实时校验 + const error = validateFieldName(name); + setNameError(error); }; const handleDescriptionChange = (description: string) => { @@ -93,8 +120,9 @@ export default function FieldEditor({ field, onChange }: FieldEditorProps) { onChange={(e) => handleNameChange(e.target.value)} placeholder={t('testDataGenerator_fieldNamePlaceholder')} maxLength={20} - className="h-9" + className={`h-9 ${nameError ? 'border-destructive' : ''}`} /> + {nameError &&

{nameError}

}
diff --git a/src/pages/TestDataGenerator/components/GeneratorConfig.tsx b/src/pages/TestDataGenerator/components/GeneratorConfig.tsx index 770b124..f8e3392 100644 --- a/src/pages/TestDataGenerator/components/GeneratorConfig.tsx +++ b/src/pages/TestDataGenerator/components/GeneratorConfig.tsx @@ -12,6 +12,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { useI18n } from '@/utils/chromeI18n'; import type { GeneratorDefinition } from '@/types/testDataGenerator'; interface GeneratorConfigProps { @@ -21,12 +22,17 @@ interface GeneratorConfigProps { } export default function GeneratorConfig({ generator, params, onChange }: GeneratorConfigProps) { + const { t } = useI18n('testDataGenerator'); const handleParamChange = (key: string, value: unknown) => { onChange({ ...params, [key]: value }); }; if (generator.params.length === 0) { - return

此生成器无可配置参数

; + return ( +

+ {t('testDataGenerator_noGeneratorParams')} +

+ ); } return ( @@ -64,7 +70,9 @@ export default function GeneratorConfig({ generator, params, onChange }: Generat {param.type === 'boolean' && (
- {params[param.key] !== false ? '启用' : '禁用'} + {params[param.key] !== false + ? t('testDataGenerator_enabled') + : t('testDataGenerator_disabled')} )} diff --git a/src/pages/TestDataGenerator/components/RuleManager.tsx b/src/pages/TestDataGenerator/components/RuleManager.tsx index c59cf9e..6bd1d3b 100644 --- a/src/pages/TestDataGenerator/components/RuleManager.tsx +++ b/src/pages/TestDataGenerator/components/RuleManager.tsx @@ -80,7 +80,7 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana const handleDuplicate = useCallback( (id: string) => { - const result = ruleStorage.duplicate(id); + const result = ruleStorage.duplicate(id, t('testDataGenerator_ruleCopySuffix')); if (result) { loadRules(); toast.success(t('testDataGenerator_ruleDuplicated')); diff --git a/src/pages/TestDataGenerator/components/__tests__/RuleManager.test.tsx b/src/pages/TestDataGenerator/components/__tests__/RuleManager.test.tsx index 9811e4c..51e8cca 100644 --- a/src/pages/TestDataGenerator/components/__tests__/RuleManager.test.tsx +++ b/src/pages/TestDataGenerator/components/__tests__/RuleManager.test.tsx @@ -100,7 +100,6 @@ describe('RuleManager', () => { const deleteButton = screen.getByTitle('删除'); await user.click(deleteButton); - expect(screen.getByText('确认删除规则')).toBeInTheDocument(); expect(screen.getByText(/确定要删除规则/)).toBeInTheDocument(); }); @@ -133,7 +132,7 @@ describe('RuleManager', () => { await user.click(screen.getByTitle('复制')); - expect(mockedRuleStorage.duplicate).toHaveBeenCalledWith('rule-1'); + expect(mockedRuleStorage.duplicate).toHaveBeenCalledWith('rule-1', '(副本)'); expect(mockedToast.success).toHaveBeenCalled(); }); diff --git a/src/pages/TestDataGenerator/hooks/useGenerator.ts b/src/pages/TestDataGenerator/hooks/useGenerator.ts index a89eecd..7d38bd5 100644 --- a/src/pages/TestDataGenerator/hooks/useGenerator.ts +++ b/src/pages/TestDataGenerator/hooks/useGenerator.ts @@ -51,7 +51,7 @@ export function useGenerator(): UseGeneratorReturn { */ const getWorker = useCallback((): Worker => { if (workerRef.current) { - workerRef.current.terminate(); + return workerRef.current; } const worker = new Worker(new URL('@/workers/generator.worker.ts', import.meta.url), { @@ -82,8 +82,11 @@ export function useGenerator(): UseGeneratorReturn { worker.onerror = (err) => { console.error('[useGenerator] Worker 错误:', err); setIsGenerating(false); - setError('Worker 运行错误'); + setError(err.message || 'Worker 运行错误'); setProgress(null); + // Worker 出错后销毁,下次重新创建 + workerRef.current?.terminate(); + workerRef.current = null; }; workerRef.current = worker; diff --git a/src/pages/TestDataGenerator/index.tsx b/src/pages/TestDataGenerator/index.tsx index d42b910..d6a985f 100644 --- a/src/pages/TestDataGenerator/index.tsx +++ b/src/pages/TestDataGenerator/index.tsx @@ -60,64 +60,58 @@ export default function TestDataGeneratorPage() { // 添加新字段 const handleAddField = useCallback(() => { - if (fields.length >= MAX_FIELDS) return; - const newField: FieldConfig = { - id: generateId(), - name: `field${fields.length + 1}`, - generatorId: 'chineseName', - params: {}, - required: true, - nullRate: 0, - unique: false, - }; - setFields([...fields, newField]); - setSelectedIndex(fields.length); - }, [fields]); + setFields((prev) => { + if (prev.length >= MAX_FIELDS) return prev; + const newField: FieldConfig = { + id: generateId(), + name: `field${prev.length + 1}`, + generatorId: 'chineseName', + params: {}, + required: true, + nullRate: 0, + unique: false, + }; + setSelectedIndex(prev.length); + return [...prev, newField]; + }); + }, []); // 更新字段 - const handleUpdateField = useCallback( - (index: number, field: FieldConfig) => { - const newFields = [...fields]; + const handleUpdateField = useCallback((index: number, field: FieldConfig) => { + setFields((prev) => { + const newFields = [...prev]; newFields[index] = field; - setFields(newFields); - }, - [fields], - ); + return newFields; + }); + }, []); // 删除字段 - const handleRemoveField = useCallback( - (index: number) => { - const newFields = fields.filter((_, i) => i !== index); - setFields(newFields); - if (selectedIndex === index) { - setSelectedIndex(null); - } else if (selectedIndex !== null && selectedIndex > index) { - setSelectedIndex(selectedIndex - 1); - } - }, - [fields, selectedIndex], - ); + const handleRemoveField = useCallback((index: number) => { + setFields((prev) => prev.filter((_, i) => i !== index)); + setSelectedIndex((prev) => { + if (prev === index) return null; + if (prev !== null && prev > index) return prev - 1; + return prev; + }); + }, []); // 拖拽排序 - const handleReorder = useCallback( - (oldIndex: number, newIndex: number) => { - const newFields = [...fields]; + const handleReorder = useCallback((oldIndex: number, newIndex: number) => { + setFields((prev) => { + const newFields = [...prev]; const [moved] = newFields.splice(oldIndex, 1); newFields.splice(newIndex, 0, moved); - setFields(newFields); - // 同步更新选中索引 - if (selectedIndex === oldIndex) { - setSelectedIndex(newIndex); - } else if (selectedIndex !== null) { - if (oldIndex < selectedIndex && newIndex >= selectedIndex) { - setSelectedIndex(selectedIndex - 1); - } else if (oldIndex > selectedIndex && newIndex <= selectedIndex) { - setSelectedIndex(selectedIndex + 1); - } + return newFields; + }); + setSelectedIndex((prev) => { + if (prev === oldIndex) return newIndex; + if (prev !== null) { + if (oldIndex < prev && newIndex >= prev) return prev - 1; + if (oldIndex > prev && newIndex <= prev) return prev + 1; } - }, - [fields, selectedIndex], - ); + return prev; + }); + }, []); // 加载规则 const handleLoadRule = useCallback( @@ -293,6 +287,7 @@ export default function TestDataGeneratorPage() { handleUpdateField(selectedIndex, updatedField)} + allFieldNames={fields.map((f) => f.name)} /> )}
diff --git a/src/utils/ruleStorage.ts b/src/utils/ruleStorage.ts index fb4ed61..8253abf 100644 --- a/src/utils/ruleStorage.ts +++ b/src/utils/ruleStorage.ts @@ -138,8 +138,10 @@ export function deleteRule(id: string): boolean { /** * 复制规则 + * @param id 规则 ID + * @param copySuffix 复制后缀,默认为中文「(副本)」,可通过 i18n 传入 */ -export function duplicate(id: string): DataRule | null { +export function duplicate(id: string, copySuffix = '(副本)'): DataRule | null { const rule = getById(id); if (!rule) { console.warn('[ruleStorage] 规则不存在:', id); @@ -155,7 +157,7 @@ export function duplicate(id: string): DataRule | null { const newRule: DataRule = { ...rule, id: generateId(), - name: `${rule.name}(副本)`, + name: `${rule.name}${copySuffix}`, createdAt: now, updatedAt: now, useCount: 0,