diff --git a/public/_locales/zh_CN/messages.json b/public/_locales/zh_CN/messages.json index 95682c4..845a150 100644 --- a/public/_locales/zh_CN/messages.json +++ b/public/_locales/zh_CN/messages.json @@ -1192,5 +1192,73 @@ }, "testDataGenerator_totalRows": { "message": "共 $ 条数据" + }, + "testDataGenerator_ruleSaved": { + "message": "规则已保存", + "description": "Translation key: testDataGenerator_ruleSaved" + }, + "testDataGenerator_ruleLoaded": { + "message": "已加载规则「{{name}}」", + "description": "Translation key: testDataGenerator_ruleLoaded" + }, + "testDataGenerator_ruleDeleted": { + "message": "规则已删除", + "description": "Translation key: testDataGenerator_ruleDeleted" + }, + "testDataGenerator_ruleDuplicated": { + "message": "规则已复制", + "description": "Translation key: testDataGenerator_ruleDuplicated" + }, + "testDataGenerator_exportSuccess": { + "message": "规则已导出", + "description": "Translation key: testDataGenerator_exportSuccess" + }, + "testDataGenerator_importSuccess": { + "message": "成功导入 {{count}} 条规则", + "description": "Translation key: testDataGenerator_importSuccess" + }, + "testDataGenerator_importFailed": { + "message": "{{count}} 条规则导入失败", + "description": "Translation key: testDataGenerator_importFailed" + }, + "testDataGenerator_confirmDeleteTitle": { + "message": "确认删除规则", + "description": "Translation key: testDataGenerator_confirmDeleteTitle" + }, + "testDataGenerator_confirmDeleteDescription": { + "message": "确定要删除规则「{{name}}」吗?此操作不可撤销。", + "description": "Translation key: testDataGenerator_confirmDeleteDescription" + }, + "testDataGenerator_ruleNameDuplicate": { + "message": "已存在同名规则,是否覆盖保存?", + "description": "Translation key: testDataGenerator_ruleNameDuplicate" + }, + "testDataGenerator_overwrite": { + "message": "覆盖", + "description": "Translation key: testDataGenerator_overwrite" + }, + "testDataGenerator_edit": { + "message": "编辑", + "description": "Translation key: testDataGenerator_edit" + }, + "testDataGenerator_ruleUpdated": { + "message": "规则已更新", + "description": "Translation key: testDataGenerator_ruleUpdated" + }, + "testDataGenerator_editing": { + "message": "编辑中", + "description": "Translation key: testDataGenerator_editing" + }, + "testDataGenerator_editingRule": { + "message": "正在编辑规则「{{name}}」", + "description": "Translation key: testDataGenerator_editingRule" + }, + "testDataGenerator_updateRule": { + "message": "更新规则", + "description": "Translation key: testDataGenerator_updateRule" + }, + "testDataGenerator_saveAs": { + "message": "另存为", + "description": "Translation key: testDataGenerator_saveAs" } } diff --git a/src/pages/TestDataGenerator/components/FieldList.tsx b/src/pages/TestDataGenerator/components/FieldList.tsx index a447a29..0536780 100644 --- a/src/pages/TestDataGenerator/components/FieldList.tsx +++ b/src/pages/TestDataGenerator/components/FieldList.tsx @@ -5,9 +5,13 @@ */ import { useState, useRef, useCallback, useEffect } from 'react'; -import { Plus, GripVertical, Trash2 } from 'lucide-react'; +import { Plus, GripVertical, Trash2, Save } from 'lucide-react'; +import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Dialog, DialogContent } from '@/components/ui/dialog'; import { useI18n } from '@/utils/chromeI18n'; +import * as ruleStorage from '@/utils/ruleStorage'; import { DndContext, closestCenter, @@ -26,7 +30,7 @@ import { useSortable, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; -import type { FieldConfig } from '@/types/testDataGenerator'; +import type { FieldConfig, DataRule } from '@/types/testDataGenerator'; import FieldItem from './FieldItem'; /** 最大字段数量 */ @@ -46,6 +50,8 @@ interface FieldListProps { onAdd: () => void; onEdit: (index: number) => void; onReorder: (oldIndex: number, newIndex: number) => void; + editingRule?: DataRule | null; + onRuleSaved?: () => void; } /** 可排序的字段项 */ @@ -114,11 +120,17 @@ export default function FieldList({ onAdd, onEdit, onReorder, + editingRule, + onRuleSaved, }: FieldListProps) { const { t } = useI18n('testDataGenerator'); const [scrollTop, setScrollTop] = useState(0); const [activeId, setActiveId] = useState(null); const containerRef = useRef(null); + const [showSaveDialog, setShowSaveDialog] = useState(false); + const [showConfirmOverwrite, setShowConfirmOverwrite] = useState(false); + const [ruleName, setRuleName] = useState(''); + const [ruleDescription, setRuleDescription] = useState(''); const sensors = useSensors( useSensor(PointerSensor, { @@ -205,22 +217,107 @@ export default function FieldList({ const isMaxReached = fields.length >= MAX_FIELDS; + // 编辑模式下,保存时更新原规则 + const handleUpdateRule = useCallback(() => { + if (!editingRule) return; + + const updated = ruleStorage.update(editingRule.id, { + fields: fields, + }); + + if (updated) { + toast.success(t('testDataGenerator_ruleUpdated')); + onRuleSaved?.(); + } + }, [editingRule, fields, t, onRuleSaved]); + + // 新建规则或另存为 + const handleSave = useCallback( + (overwrite = false) => { + if (!ruleName.trim()) return; + + const trimmedName = ruleName.trim(); + + // 检查名称是否重复 + if (!overwrite) { + const existingRule = ruleStorage.getByName(trimmedName); + if (existingRule) { + setShowConfirmOverwrite(true); + return; + } + } + + const newRule = ruleStorage.save({ + name: trimmedName, + description: ruleDescription.trim(), + fields: fields, + }); + + if (newRule) { + setShowSaveDialog(false); + setShowConfirmOverwrite(false); + setRuleName(''); + setRuleDescription(''); + toast.success(t('testDataGenerator_ruleSaved')); + onRuleSaved?.(); + } + }, + [ruleName, ruleDescription, fields, t, onRuleSaved], + ); + return (

{t('testDataGenerator_fields')} ({fields.length}/{MAX_FIELDS})

- +
+ {editingRule ? ( + <> + + + + ) : ( + + )} + +
@@ -311,6 +408,59 @@ export default function FieldList({ )}
+ + {/* 保存规则对话框 */} + + +
+ setRuleName(e.target.value)} + placeholder={t('testDataGenerator_ruleNamePlaceholder')} + className="h-9" + /> + setRuleDescription(e.target.value)} + placeholder={t('testDataGenerator_ruleDescPlaceholder')} + className="h-9" + /> +
+
+ + +
+
+
+ + {/* 覆盖确认对话框 */} + + +
+

+ {t('testDataGenerator_ruleNameDuplicate')} +

+
+
+ + +
+
+
); } diff --git a/src/pages/TestDataGenerator/components/RuleManager.tsx b/src/pages/TestDataGenerator/components/RuleManager.tsx index 820444e..c59cf9e 100644 --- a/src/pages/TestDataGenerator/components/RuleManager.tsx +++ b/src/pages/TestDataGenerator/components/RuleManager.tsx @@ -1,143 +1,227 @@ /** * 规则管理器组件 - * 管理测试数据生成规则的保存、加载、删除等操作 + * 管理测试数据生成规则的加载、删除等操作 */ -import { useState } from 'react'; -import { Search, Save, Upload, Download, Trash2, Copy, Edit, Clock, Tag } from 'lucide-react'; +import { useState, useEffect, useCallback, useMemo } from 'react'; +import { + Search, + Upload, + Download, + Trash2, + Copy, + Edit, + Clock, + Tag, + Loader2, + FolderOpen, +} from 'lucide-react'; +import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; +import { Dialog, DialogContent } from '@/components/ui/dialog'; import { useI18n } from '@/utils/chromeI18n'; import * as ruleStorage from '@/utils/ruleStorage'; import type { DataRule, FieldConfig } from '@/types/testDataGenerator'; interface RuleManagerProps { - currentFields: FieldConfig[]; onLoad: (fields: FieldConfig[]) => void; + onEdit?: (rule: DataRule) => void; + onRulesChanged?: () => void; } -export default function RuleManager({ currentFields, onLoad }: RuleManagerProps) { - const { t } = useI18n('testDataGenerator'); +export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleManagerProps) { + const { t, i18n } = useI18n('testDataGenerator'); const [rules, setRules] = useState(() => ruleStorage.getAll()); const [searchQuery, setSearchQuery] = useState(''); - const [showSaveDialog, setShowSaveDialog] = useState(false); - const [ruleName, setRuleName] = useState(''); - const [ruleDescription, setRuleDescription] = useState(''); + const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(''); + const [ruleToDelete, setRuleToDelete] = useState(null); + const [isImporting, setIsImporting] = useState(false); + const [isExporting, setIsExporting] = useState(false); - const loadRules = () => { + const loadRules = useCallback(() => { setRules(ruleStorage.getAll()); - }; + }, []); - const filteredRules = searchQuery ? ruleStorage.search(searchQuery) : rules; + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearchQuery(searchQuery); + }, 250); + return () => clearTimeout(timer); + }, [searchQuery]); - const handleSave = () => { - if (!ruleName.trim()) return; + const filteredRules = useMemo(() => { + if (!debouncedSearchQuery) return rules; + const query = debouncedSearchQuery.toLowerCase(); + return rules.filter( + (rule) => + rule.name.toLowerCase().includes(query) || rule.description?.toLowerCase().includes(query), + ); + }, [rules, debouncedSearchQuery]); - const newRule = ruleStorage.save({ - name: ruleName.trim(), - description: ruleDescription.trim(), - fields: currentFields, - }); - - if (newRule) { + const handleLoad = useCallback( + (rule: DataRule) => { + onLoad(rule.fields); + ruleStorage.recordUse(rule.id); loadRules(); - setShowSaveDialog(false); - setRuleName(''); - setRuleDescription(''); + toast.success(t('testDataGenerator_ruleLoaded', { name: rule.name })); + }, + [onLoad, loadRules, t], + ); + + const handleDelete = useCallback(() => { + if (!ruleToDelete) return; + ruleStorage.deleteRule(ruleToDelete.id); + loadRules(); + setRuleToDelete(null); + toast.success(t('testDataGenerator_ruleDeleted')); + onRulesChanged?.(); + }, [ruleToDelete, loadRules, t, onRulesChanged]); + + const handleDuplicate = useCallback( + (id: string) => { + const result = ruleStorage.duplicate(id); + if (result) { + loadRules(); + toast.success(t('testDataGenerator_ruleDuplicated')); + onRulesChanged?.(); + } + }, + [loadRules, t, onRulesChanged], + ); + + const handleEdit = useCallback( + (rule: DataRule) => { + onEdit?.(rule); + }, + [onEdit], + ); + + const handleExport = useCallback(() => { + try { + setIsExporting(true); + const json = ruleStorage.exportRules(); + const blob = new Blob([json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + try { + const link = document.createElement('a'); + link.href = url; + link.download = 'test-data-rules.json'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } finally { + URL.revokeObjectURL(url); + } + toast.success(t('testDataGenerator_exportSuccess')); + } catch (error) { + console.error('[RuleManager] 导出失败:', error); + toast.error(t('testDataGenerator_exportFailed')); + } finally { + setIsExporting(false); } - }; + }, [t]); - const handleLoad = (rule: DataRule) => { - onLoad(rule.fields); - ruleStorage.recordUse(rule.id); - loadRules(); - }; - - const handleDelete = (id: string) => { - ruleStorage.deleteRule(id); - loadRules(); - }; - - const handleDuplicate = (id: string) => { - ruleStorage.duplicate(id); - loadRules(); - }; - - const handleExport = () => { - const json = ruleStorage.exportRules(); - const blob = new Blob([json], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = 'test-data-rules.json'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - }; - - const handleImport = async () => { + const handleImport = useCallback(() => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = async (e) => { - const file = (e.target as HTMLInputElement).files?.[0]; - if (!file) return; + try { + setIsImporting(true); + const file = (e.target as HTMLInputElement).files?.[0]; + if (!file) return; - const text = await file.text(); - const result = ruleStorage.importRules(text); + const text = await file.text(); + const result = ruleStorage.importRules(text); - if (result.errors.length > 0) { - console.warn('[RuleManager] 导入警告:', result.errors); + if (result.success > 0) { + toast.success(t('testDataGenerator_importSuccess', { count: result.success })); + } + + if (result.failed > 0) { + toast.error(t('testDataGenerator_importFailed', { count: result.failed })); + console.warn('[RuleManager] 导入警告:', result.errors); + } + + loadRules(); + onRulesChanged?.(); + } catch (error) { + console.error('[RuleManager] 导入失败:', error); + toast.error(t('testDataGenerator_importFailed')); + } finally { + setIsImporting(false); } - - loadRules(); }; input.click(); - }; + }, [loadRules, t, onRulesChanged]); - const formatDate = (timestamp: number) => { - return new Date(timestamp).toLocaleDateString('zh-CN', { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - }; + const formatDate = useCallback( + (timestamp: number) => { + return new Date(timestamp).toLocaleDateString(i18n.language || 'zh-CN', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }, + [i18n.language], + ); return (
+ {/* 删除确认对话框 */} + setRuleToDelete(null)}> + +
+

+ {t('testDataGenerator_confirmDeleteDescription', { name: ruleToDelete?.name ?? '' })} +

+
+
+ + +
+
+
+ {/* 工具栏 */} -
-
- - - + -
- - {t('testDataGenerator_ruleCount', { count: rules.length, max: 20 })} - + )} + {t('testDataGenerator_export')} +
{/* 搜索框 */} @@ -145,48 +229,23 @@ export default function RuleManager({ currentFields, onLoad }: RuleManagerProps) setSearchQuery(e.target.value)} + onChange={(e) => setSearchQuery(e.target.value.slice(0, 20))} placeholder={t('testDataGenerator_searchRules')} - className="pl-9 h-9" + className="pl-9 pr-24 h-9" + maxLength={20} /> + + {t('testDataGenerator_ruleCount', { count: rules.length, max: 20 })} +
- {/* 保存对话框 */} - {showSaveDialog && ( -
-

- {t('testDataGenerator_saveNewRule')} -

- setRuleName(e.target.value)} - placeholder={t('testDataGenerator_ruleNamePlaceholder')} - className="h-9" - /> - setRuleDescription(e.target.value)} - placeholder={t('testDataGenerator_ruleDescPlaceholder')} - className="h-9" - /> -
- - -
-
- )} - {/* 规则列表 */}
{filteredRules.length === 0 ? (

- {searchQuery + {debouncedSearchQuery ? t('testDataGenerator_noSearchResults') : t('testDataGenerator_noRules')}

@@ -225,6 +284,15 @@ export default function RuleManager({ currentFields, onLoad }: RuleManagerProps) className="h-7 w-7" onClick={() => handleLoad(rule)} title={t('testDataGenerator_load')} + > + + + @@ -241,7 +309,7 @@ export default function RuleManager({ currentFields, onLoad }: RuleManagerProps) variant="ghost" size="icon" className="h-7 w-7 text-destructive hover:text-destructive" - onClick={() => handleDelete(rule.id)} + onClick={() => setRuleToDelete(rule)} title={t('testDataGenerator_delete')} > diff --git a/src/pages/TestDataGenerator/components/__tests__/RuleManager.test.tsx b/src/pages/TestDataGenerator/components/__tests__/RuleManager.test.tsx new file mode 100644 index 0000000..9811e4c --- /dev/null +++ b/src/pages/TestDataGenerator/components/__tests__/RuleManager.test.tsx @@ -0,0 +1,190 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +// Mock dependencies +vi.mock('@/utils/ruleStorage', () => ({ + getAll: vi.fn(() => []), + save: vi.fn(), + deleteRule: vi.fn(), + duplicate: vi.fn(), + recordUse: vi.fn(), + search: vi.fn(() => []), + exportRules: vi.fn(() => '[]'), + importRules: vi.fn(() => ({ success: 0, failed: 0, errors: [] })), +})); + +vi.mock('sonner', () => ({ + toast: { + success: vi.fn(), + error: vi.fn(), + }, +})); + +import RuleManager from '../RuleManager'; +import * as ruleStorage from '@/utils/ruleStorage'; +import { toast } from 'sonner'; +import type { DataRule, FieldConfig } from '@/types/testDataGenerator'; + +const mockedRuleStorage = vi.mocked(ruleStorage); +const mockedToast = vi.mocked(toast); + +const mockFields: FieldConfig[] = [ + { + id: 'field-1', + name: 'name', + generatorId: 'string', + params: {}, + required: true, + nullRate: 0, + unique: false, + }, +]; + +const mockRule: DataRule = { + id: 'rule-1', + name: 'Test Rule', + description: 'Test description', + fields: mockFields, + createdAt: Date.now(), + updatedAt: Date.now(), + useCount: 0, +}; + +describe('RuleManager', () => { + const defaultProps = { + onLoad: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockedRuleStorage.getAll.mockReturnValue([]); + }); + + it('should render empty state when no rules', () => { + render(); + + expect(screen.getByText('暂无保存的规则')).toBeInTheDocument(); + }); + + it('should render rule list when rules exist', () => { + mockedRuleStorage.getAll.mockReturnValue([mockRule]); + + render(); + + expect(screen.getByText('Test Rule')).toBeInTheDocument(); + expect(screen.getByText('Test description')).toBeInTheDocument(); + }); + + it('should load rule when load button clicked', async () => { + const user = userEvent.setup(); + mockedRuleStorage.getAll.mockReturnValue([mockRule]); + + render(); + + const loadButton = screen.getByTitle('加载'); + await user.click(loadButton); + + expect(defaultProps.onLoad).toHaveBeenCalledWith(mockFields); + expect(mockedRuleStorage.recordUse).toHaveBeenCalledWith('rule-1'); + // Note: t() mock doesn't handle placeholders, so we just check it was called + expect(mockedToast.success).toHaveBeenCalled(); + }); + + it('should show delete confirmation dialog', async () => { + const user = userEvent.setup(); + mockedRuleStorage.getAll.mockReturnValue([mockRule]); + + render(); + + const deleteButton = screen.getByTitle('删除'); + await user.click(deleteButton); + + expect(screen.getByText('确认删除规则')).toBeInTheDocument(); + expect(screen.getByText(/确定要删除规则/)).toBeInTheDocument(); + }); + + it('should delete rule after confirmation', async () => { + const user = userEvent.setup(); + mockedRuleStorage.getAll.mockReturnValue([mockRule]); + + render(); + + // Open delete dialog + await user.click(screen.getByTitle('删除')); + + // Confirm delete + await user.click(screen.getByText('确认')); + + expect(mockedRuleStorage.deleteRule).toHaveBeenCalledWith('rule-1'); + expect(mockedToast.success).toHaveBeenCalledWith('规则已删除'); + }); + + it('should duplicate rule when duplicate button clicked', async () => { + const user = userEvent.setup(); + mockedRuleStorage.getAll.mockReturnValue([mockRule]); + mockedRuleStorage.duplicate.mockReturnValue({ + ...mockRule, + id: 'rule-2', + name: 'Test Rule(副本)', + }); + + render(); + + await user.click(screen.getByTitle('复制')); + + expect(mockedRuleStorage.duplicate).toHaveBeenCalledWith('rule-1'); + expect(mockedToast.success).toHaveBeenCalled(); + }); + + it('should update search input value', async () => { + const rules = [mockRule, { ...mockRule, id: 'rule-2', name: 'Another Rule' }]; + mockedRuleStorage.getAll.mockReturnValue(rules); + + render(); + + // Initially both rules should be visible + expect(screen.getByText('Test Rule')).toBeInTheDocument(); + expect(screen.getByText('Another Rule')).toBeInTheDocument(); + + // Verify search input exists and is interactive + const searchInput = screen.getByPlaceholderText('搜索规则...'); + expect(searchInput).toBeInTheDocument(); + expect(searchInput).toHaveValue(''); + }); + + it('should call exportRules when export button clicked', async () => { + const user = userEvent.setup(); + mockedRuleStorage.getAll.mockReturnValue([mockRule]); + + render(); + + // Mock URL methods and body methods after render to avoid interfering with createRoot + const mockCreateObjectURL = vi.fn(() => 'blob:mock'); + const mockRevokeObjectURL = vi.fn(); + vi.spyOn(URL, 'createObjectURL').mockImplementation(mockCreateObjectURL); + vi.spyOn(URL, 'revokeObjectURL').mockImplementation(mockRevokeObjectURL); + vi.spyOn(document.body, 'appendChild').mockImplementation(() => null as unknown as Node); + vi.spyOn(document.body, 'removeChild').mockImplementation(() => null as unknown as Node); + + const buttons = screen.getAllByRole('button'); + const exportButton = buttons.find((btn) => btn.textContent?.includes('导出')); + expect(exportButton).toBeDefined(); + + await user.click(exportButton!); + + expect(mockedRuleStorage.exportRules).toHaveBeenCalled(); + expect(mockCreateObjectURL).toHaveBeenCalled(); + + vi.restoreAllMocks(); + }); + + it('should show field count for each rule', () => { + mockedRuleStorage.getAll.mockReturnValue([mockRule]); + + render(); + + // The component shows "{count} 字段" format + expect(screen.getByText(/字段/)).toBeInTheDocument(); + }); +}); diff --git a/src/pages/TestDataGenerator/index.tsx b/src/pages/TestDataGenerator/index.tsx index 5e9365c..d42b910 100644 --- a/src/pages/TestDataGenerator/index.tsx +++ b/src/pages/TestDataGenerator/index.tsx @@ -7,7 +7,7 @@ import { Settings, Database, Tag } from 'lucide-react'; import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; import { useGenerator } from './hooks/useGenerator'; -import type { FieldConfig, GenerateResult } from '@/types/testDataGenerator'; +import type { FieldConfig, GenerateResult, DataRule } from '@/types/testDataGenerator'; import { Dialog, DialogContent } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { toast } from 'sonner'; @@ -36,6 +36,7 @@ export default function TestDataGeneratorPage() { const [fields, setFields] = useState([]); const [selectedIndex, setSelectedIndex] = useState(null); const [isEditorOpen, setIsEditorOpen] = useState(false); + const [editingRule, setEditingRule] = useState(null); // 生成选项 const [count, setCount] = useState(100); @@ -123,11 +124,30 @@ export default function TestDataGeneratorPage() { (loadedFields: FieldConfig[]) => { setFields(loadedFields); setSelectedIndex(null); + setEditingRule(null); clearResult(); }, [clearResult], ); + // 编辑规则 + const handleEditRule = useCallback( + (rule: DataRule) => { + setFields(rule.fields); + setSelectedIndex(null); + setEditingRule(rule); + setActiveTab('fields'); + clearResult(); + toast.success(t('testDataGenerator_editingRule', { name: rule.name })); + }, + [clearResult, t], + ); + + // 保存规则成功后清除编辑状态 + const handleRuleSaved = useCallback(() => { + setEditingRule(null); + }, []); + // 开始生成 const handleGenerate = useCallback(() => { if (fields.length === 0) return; @@ -188,6 +208,8 @@ export default function TestDataGeneratorPage() { onAdd={handleAddField} onEdit={handleOpenEditor} onReorder={handleReorder} + editingRule={editingRule} + onRuleSaved={handleRuleSaved} />
)} @@ -195,7 +217,7 @@ export default function TestDataGeneratorPage() { {/* 规则管理标签页 */} {activeTab === 'rules' && (
- +
)} @@ -264,7 +286,7 @@ export default function TestDataGeneratorPage() {
{selectedField && selectedIndex !== null && ( diff --git a/src/utils/ruleStorage.ts b/src/utils/ruleStorage.ts index eef6c52..fb4ed61 100644 --- a/src/utils/ruleStorage.ts +++ b/src/utils/ruleStorage.ts @@ -33,6 +33,14 @@ export function getById(id: string): DataRule | undefined { return rules.find((rule) => rule.id === id); } +/** + * 根据名称获取规则 + */ +export function getByName(name: string): DataRule | undefined { + const rules = getAll(); + return rules.find((rule) => rule.name === name); +} + /** * 获取规则数量 */