feat: 优化规则管理功能
- 移动保存按钮到字段配置区域 - 添加规则编辑功能,支持更新和另存为 - 编辑模式下显示编辑状态标识 - 保存时检查规则名称重复,支持覆盖保存 - 优化弹窗样式,统一风格 - 更换加载图标为 FolderOpen - 添加搜索防抖和错误处理 - 添加国际化文本
This commit is contained in:
@@ -1192,5 +1192,73 @@
|
|||||||
},
|
},
|
||||||
"testDataGenerator_totalRows": {
|
"testDataGenerator_totalRows": {
|
||||||
"message": "共 $ 条数据"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
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 { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Dialog, DialogContent } from '@/components/ui/dialog';
|
||||||
import { useI18n } from '@/utils/chromeI18n';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
|
import * as ruleStorage from '@/utils/ruleStorage';
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
closestCenter,
|
closestCenter,
|
||||||
@@ -26,7 +30,7 @@ import {
|
|||||||
useSortable,
|
useSortable,
|
||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import type { FieldConfig } from '@/types/testDataGenerator';
|
import type { FieldConfig, DataRule } from '@/types/testDataGenerator';
|
||||||
import FieldItem from './FieldItem';
|
import FieldItem from './FieldItem';
|
||||||
|
|
||||||
/** 最大字段数量 */
|
/** 最大字段数量 */
|
||||||
@@ -46,6 +50,8 @@ interface FieldListProps {
|
|||||||
onAdd: () => void;
|
onAdd: () => void;
|
||||||
onEdit: (index: number) => void;
|
onEdit: (index: number) => void;
|
||||||
onReorder: (oldIndex: number, newIndex: number) => void;
|
onReorder: (oldIndex: number, newIndex: number) => void;
|
||||||
|
editingRule?: DataRule | null;
|
||||||
|
onRuleSaved?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 可排序的字段项 */
|
/** 可排序的字段项 */
|
||||||
@@ -114,11 +120,17 @@ export default function FieldList({
|
|||||||
onAdd,
|
onAdd,
|
||||||
onEdit,
|
onEdit,
|
||||||
onReorder,
|
onReorder,
|
||||||
|
editingRule,
|
||||||
|
onRuleSaved,
|
||||||
}: FieldListProps) {
|
}: FieldListProps) {
|
||||||
const { t } = useI18n('testDataGenerator');
|
const { t } = useI18n('testDataGenerator');
|
||||||
const [scrollTop, setScrollTop] = useState(0);
|
const [scrollTop, setScrollTop] = useState(0);
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
const [activeId, setActiveId] = useState<string | null>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [showSaveDialog, setShowSaveDialog] = useState(false);
|
||||||
|
const [showConfirmOverwrite, setShowConfirmOverwrite] = useState(false);
|
||||||
|
const [ruleName, setRuleName] = useState('');
|
||||||
|
const [ruleDescription, setRuleDescription] = useState('');
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
@@ -205,22 +217,107 @@ export default function FieldList({
|
|||||||
|
|
||||||
const isMaxReached = fields.length >= MAX_FIELDS;
|
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 (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h3 className="text-sm font-medium text-foreground">
|
<h3 className="text-sm font-medium text-foreground">
|
||||||
{t('testDataGenerator_fields')} ({fields.length}/{MAX_FIELDS})
|
{t('testDataGenerator_fields')} ({fields.length}/{MAX_FIELDS})
|
||||||
</h3>
|
</h3>
|
||||||
<Button
|
<div className="flex items-center gap-2">
|
||||||
variant="outline"
|
{editingRule ? (
|
||||||
size="sm"
|
<>
|
||||||
onClick={onAdd}
|
<Button
|
||||||
disabled={isMaxReached}
|
variant="outline"
|
||||||
className="h-9 gap-1.5 px-3"
|
size="sm"
|
||||||
>
|
onClick={handleUpdateRule}
|
||||||
<Plus className="h-4 w-4" />
|
disabled={fields.length === 0}
|
||||||
{t('testDataGenerator_addField')}
|
className="h-8 gap-1.5 px-2.5"
|
||||||
</Button>
|
title={`${t('testDataGenerator_editing')}: ${editingRule.name}`}
|
||||||
|
>
|
||||||
|
<Save className="h-3.5 w-3.5" />
|
||||||
|
{t('testDataGenerator_updateRule')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowSaveDialog(true)}
|
||||||
|
disabled={fields.length === 0}
|
||||||
|
className="h-8 px-2"
|
||||||
|
>
|
||||||
|
{t('testDataGenerator_saveAs')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowSaveDialog(true)}
|
||||||
|
disabled={fields.length === 0}
|
||||||
|
className="h-8 gap-1.5 px-2.5"
|
||||||
|
>
|
||||||
|
<Save className="h-3.5 w-3.5" />
|
||||||
|
{t('testDataGenerator_saveRule')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={onAdd}
|
||||||
|
disabled={isMaxReached}
|
||||||
|
className="h-8 gap-1.5 px-2.5"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
{t('testDataGenerator_addField')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 min-h-0">
|
<div className="flex-1 min-h-0">
|
||||||
@@ -311,6 +408,59 @@ export default function FieldList({
|
|||||||
</DndContext>
|
</DndContext>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 保存规则对话框 */}
|
||||||
|
<Dialog open={showSaveDialog} onOpenChange={setShowSaveDialog}>
|
||||||
|
<DialogContent
|
||||||
|
showCloseButton={false}
|
||||||
|
className="w-[calc(100vw-4rem)] max-w-[420px] p-0 pt-6 flex flex-col"
|
||||||
|
>
|
||||||
|
<div className="flex-1 overflow-y-auto px-6 pt-2 pb-4 space-y-4">
|
||||||
|
<Input
|
||||||
|
value={ruleName}
|
||||||
|
onChange={(e) => setRuleName(e.target.value)}
|
||||||
|
placeholder={t('testDataGenerator_ruleNamePlaceholder')}
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={ruleDescription}
|
||||||
|
onChange={(e) => setRuleDescription(e.target.value)}
|
||||||
|
placeholder={t('testDataGenerator_ruleDescPlaceholder')}
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-2 px-6 py-2 border-t shrink-0">
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => setShowSaveDialog(false)}>
|
||||||
|
{t('testDataGenerator_cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" onClick={() => handleSave()} disabled={!ruleName.trim()}>
|
||||||
|
{t('testDataGenerator_confirm')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 覆盖确认对话框 */}
|
||||||
|
<Dialog open={showConfirmOverwrite} onOpenChange={setShowConfirmOverwrite}>
|
||||||
|
<DialogContent
|
||||||
|
showCloseButton={false}
|
||||||
|
className="w-[calc(100vw-4rem)] max-w-[420px] p-0 pt-6 flex flex-col"
|
||||||
|
>
|
||||||
|
<div className="flex-1 overflow-y-auto px-6 pb-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t('testDataGenerator_ruleNameDuplicate')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-2 px-6 py-2 border-t shrink-0">
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => setShowConfirmOverwrite(false)}>
|
||||||
|
{t('testDataGenerator_cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" onClick={() => handleSave(true)}>
|
||||||
|
{t('testDataGenerator_overwrite')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,143 +1,227 @@
|
|||||||
/**
|
/**
|
||||||
* 规则管理器组件
|
* 规则管理器组件
|
||||||
* 管理测试数据生成规则的保存、加载、删除等操作
|
* 管理测试数据生成规则的加载、删除等操作
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { Search, Save, Upload, Download, Trash2, Copy, Edit, Clock, Tag } from 'lucide-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 { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Dialog, DialogContent } from '@/components/ui/dialog';
|
||||||
import { useI18n } from '@/utils/chromeI18n';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import * as ruleStorage from '@/utils/ruleStorage';
|
import * as ruleStorage from '@/utils/ruleStorage';
|
||||||
import type { DataRule, FieldConfig } from '@/types/testDataGenerator';
|
import type { DataRule, FieldConfig } from '@/types/testDataGenerator';
|
||||||
|
|
||||||
interface RuleManagerProps {
|
interface RuleManagerProps {
|
||||||
currentFields: FieldConfig[];
|
|
||||||
onLoad: (fields: FieldConfig[]) => void;
|
onLoad: (fields: FieldConfig[]) => void;
|
||||||
|
onEdit?: (rule: DataRule) => void;
|
||||||
|
onRulesChanged?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RuleManager({ currentFields, onLoad }: RuleManagerProps) {
|
export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleManagerProps) {
|
||||||
const { t } = useI18n('testDataGenerator');
|
const { t, i18n } = useI18n('testDataGenerator');
|
||||||
const [rules, setRules] = useState<DataRule[]>(() => ruleStorage.getAll());
|
const [rules, setRules] = useState<DataRule[]>(() => ruleStorage.getAll());
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [showSaveDialog, setShowSaveDialog] = useState(false);
|
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('');
|
||||||
const [ruleName, setRuleName] = useState('');
|
const [ruleToDelete, setRuleToDelete] = useState<DataRule | null>(null);
|
||||||
const [ruleDescription, setRuleDescription] = useState('');
|
const [isImporting, setIsImporting] = useState(false);
|
||||||
|
const [isExporting, setIsExporting] = useState(false);
|
||||||
|
|
||||||
const loadRules = () => {
|
const loadRules = useCallback(() => {
|
||||||
setRules(ruleStorage.getAll());
|
setRules(ruleStorage.getAll());
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const filteredRules = searchQuery ? ruleStorage.search(searchQuery) : rules;
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setDebouncedSearchQuery(searchQuery);
|
||||||
|
}, 250);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [searchQuery]);
|
||||||
|
|
||||||
const handleSave = () => {
|
const filteredRules = useMemo(() => {
|
||||||
if (!ruleName.trim()) return;
|
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({
|
const handleLoad = useCallback(
|
||||||
name: ruleName.trim(),
|
(rule: DataRule) => {
|
||||||
description: ruleDescription.trim(),
|
onLoad(rule.fields);
|
||||||
fields: currentFields,
|
ruleStorage.recordUse(rule.id);
|
||||||
});
|
|
||||||
|
|
||||||
if (newRule) {
|
|
||||||
loadRules();
|
loadRules();
|
||||||
setShowSaveDialog(false);
|
toast.success(t('testDataGenerator_ruleLoaded', { name: rule.name }));
|
||||||
setRuleName('');
|
},
|
||||||
setRuleDescription('');
|
[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) => {
|
const handleImport = useCallback(() => {
|
||||||
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 input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
input.accept = '.json';
|
input.accept = '.json';
|
||||||
input.onchange = async (e) => {
|
input.onchange = async (e) => {
|
||||||
const file = (e.target as HTMLInputElement).files?.[0];
|
try {
|
||||||
if (!file) return;
|
setIsImporting(true);
|
||||||
|
const file = (e.target as HTMLInputElement).files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
const text = await file.text();
|
const text = await file.text();
|
||||||
const result = ruleStorage.importRules(text);
|
const result = ruleStorage.importRules(text);
|
||||||
|
|
||||||
if (result.errors.length > 0) {
|
if (result.success > 0) {
|
||||||
console.warn('[RuleManager] 导入警告:', result.errors);
|
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();
|
input.click();
|
||||||
};
|
}, [loadRules, t, onRulesChanged]);
|
||||||
|
|
||||||
const formatDate = (timestamp: number) => {
|
const formatDate = useCallback(
|
||||||
return new Date(timestamp).toLocaleDateString('zh-CN', {
|
(timestamp: number) => {
|
||||||
month: 'short',
|
return new Date(timestamp).toLocaleDateString(i18n.language || 'zh-CN', {
|
||||||
day: 'numeric',
|
month: 'short',
|
||||||
hour: '2-digit',
|
day: 'numeric',
|
||||||
minute: '2-digit',
|
hour: '2-digit',
|
||||||
});
|
minute: '2-digit',
|
||||||
};
|
});
|
||||||
|
},
|
||||||
|
[i18n.language],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{/* 删除确认对话框 */}
|
||||||
|
<Dialog open={!!ruleToDelete} onOpenChange={() => setRuleToDelete(null)}>
|
||||||
|
<DialogContent
|
||||||
|
showCloseButton={false}
|
||||||
|
className="w-[calc(100vw-4rem)] max-w-[420px] p-0 pt-6 flex flex-col"
|
||||||
|
>
|
||||||
|
<div className="flex-1 overflow-y-auto px-6 pb-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t('testDataGenerator_confirmDeleteDescription', { name: ruleToDelete?.name ?? '' })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-2 px-6 py-2 border-t shrink-0">
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => setRuleToDelete(null)}>
|
||||||
|
{t('testDataGenerator_cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" size="sm" onClick={handleDelete}>
|
||||||
|
{t('testDataGenerator_confirm')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* 工具栏 */}
|
{/* 工具栏 */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<div className="flex items-center gap-2">
|
<Button
|
||||||
<Button
|
variant="outline"
|
||||||
variant="outline"
|
size="sm"
|
||||||
size="sm"
|
onClick={handleImport}
|
||||||
onClick={() => setShowSaveDialog(true)}
|
disabled={isImporting}
|
||||||
disabled={currentFields.length === 0}
|
className="h-8 gap-1.5"
|
||||||
className="h-8 gap-1.5"
|
>
|
||||||
>
|
{isImporting ? (
|
||||||
<Save className="h-4 w-4" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
{t('testDataGenerator_saveRule')}
|
) : (
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="sm" onClick={handleImport} className="h-8 gap-1.5">
|
|
||||||
<Upload className="h-4 w-4" />
|
<Upload className="h-4 w-4" />
|
||||||
{t('testDataGenerator_import')}
|
)}
|
||||||
</Button>
|
{t('testDataGenerator_import')}
|
||||||
<Button
|
</Button>
|
||||||
variant="outline"
|
<Button
|
||||||
size="sm"
|
variant="outline"
|
||||||
onClick={handleExport}
|
size="sm"
|
||||||
disabled={rules.length === 0}
|
onClick={handleExport}
|
||||||
className="h-8 gap-1.5"
|
disabled={isExporting || rules.length === 0}
|
||||||
>
|
className="h-8 gap-1.5"
|
||||||
|
>
|
||||||
|
{isExporting ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
<Download className="h-4 w-4" />
|
<Download className="h-4 w-4" />
|
||||||
{t('testDataGenerator_export')}
|
)}
|
||||||
</Button>
|
{t('testDataGenerator_export')}
|
||||||
</div>
|
</Button>
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{t('testDataGenerator_ruleCount', { count: rules.length, max: 20 })}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 搜索框 */}
|
{/* 搜索框 */}
|
||||||
@@ -145,48 +229,23 @@ export default function RuleManager({ currentFields, onLoad }: RuleManagerProps)
|
|||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value.slice(0, 20))}
|
||||||
placeholder={t('testDataGenerator_searchRules')}
|
placeholder={t('testDataGenerator_searchRules')}
|
||||||
className="pl-9 h-9"
|
className="pl-9 pr-24 h-9"
|
||||||
|
maxLength={20}
|
||||||
/>
|
/>
|
||||||
|
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-muted-foreground pointer-events-none">
|
||||||
|
{t('testDataGenerator_ruleCount', { count: rules.length, max: 20 })}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 保存对话框 */}
|
|
||||||
{showSaveDialog && (
|
|
||||||
<div className="p-3 rounded-lg border bg-card space-y-3">
|
|
||||||
<h4 className="text-sm font-medium text-foreground">
|
|
||||||
{t('testDataGenerator_saveNewRule')}
|
|
||||||
</h4>
|
|
||||||
<Input
|
|
||||||
value={ruleName}
|
|
||||||
onChange={(e) => setRuleName(e.target.value)}
|
|
||||||
placeholder={t('testDataGenerator_ruleNamePlaceholder')}
|
|
||||||
className="h-9"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
value={ruleDescription}
|
|
||||||
onChange={(e) => setRuleDescription(e.target.value)}
|
|
||||||
placeholder={t('testDataGenerator_ruleDescPlaceholder')}
|
|
||||||
className="h-9"
|
|
||||||
/>
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
<Button variant="ghost" size="sm" onClick={() => setShowSaveDialog(false)}>
|
|
||||||
{t('testDataGenerator_cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" onClick={handleSave} disabled={!ruleName.trim()}>
|
|
||||||
{t('testDataGenerator_confirm')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 规则列表 */}
|
{/* 规则列表 */}
|
||||||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||||
{filteredRules.length === 0 ? (
|
{filteredRules.length === 0 ? (
|
||||||
<div className="text-center py-6">
|
<div className="text-center py-6">
|
||||||
<Tag className="h-8 w-8 text-muted-foreground/40 mx-auto mb-2" />
|
<Tag className="h-8 w-8 text-muted-foreground/40 mx-auto mb-2" />
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{searchQuery
|
{debouncedSearchQuery
|
||||||
? t('testDataGenerator_noSearchResults')
|
? t('testDataGenerator_noSearchResults')
|
||||||
: t('testDataGenerator_noRules')}
|
: t('testDataGenerator_noRules')}
|
||||||
</p>
|
</p>
|
||||||
@@ -225,6 +284,15 @@ export default function RuleManager({ currentFields, onLoad }: RuleManagerProps)
|
|||||||
className="h-7 w-7"
|
className="h-7 w-7"
|
||||||
onClick={() => handleLoad(rule)}
|
onClick={() => handleLoad(rule)}
|
||||||
title={t('testDataGenerator_load')}
|
title={t('testDataGenerator_load')}
|
||||||
|
>
|
||||||
|
<FolderOpen className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7"
|
||||||
|
onClick={() => handleEdit(rule)}
|
||||||
|
title={t('testDataGenerator_edit')}
|
||||||
>
|
>
|
||||||
<Edit className="h-3.5 w-3.5" />
|
<Edit className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -241,7 +309,7 @@ export default function RuleManager({ currentFields, onLoad }: RuleManagerProps)
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-7 w-7 text-destructive hover:text-destructive"
|
className="h-7 w-7 text-destructive hover:text-destructive"
|
||||||
onClick={() => handleDelete(rule.id)}
|
onClick={() => setRuleToDelete(rule)}
|
||||||
title={t('testDataGenerator_delete')}
|
title={t('testDataGenerator_delete')}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
|||||||
@@ -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(<RuleManager {...defaultProps} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('暂无保存的规则')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render rule list when rules exist', () => {
|
||||||
|
mockedRuleStorage.getAll.mockReturnValue([mockRule]);
|
||||||
|
|
||||||
|
render(<RuleManager {...defaultProps} />);
|
||||||
|
|
||||||
|
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(<RuleManager {...defaultProps} />);
|
||||||
|
|
||||||
|
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(<RuleManager {...defaultProps} />);
|
||||||
|
|
||||||
|
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(<RuleManager {...defaultProps} />);
|
||||||
|
|
||||||
|
// 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(<RuleManager {...defaultProps} />);
|
||||||
|
|
||||||
|
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(<RuleManager {...defaultProps} />);
|
||||||
|
|
||||||
|
// 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(<RuleManager {...defaultProps} />);
|
||||||
|
|
||||||
|
// 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(<RuleManager {...defaultProps} />);
|
||||||
|
|
||||||
|
// The component shows "{count} 字段" format
|
||||||
|
expect(screen.getByText(/字段/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,7 +7,7 @@ import { Settings, Database, Tag } from 'lucide-react';
|
|||||||
import { useI18n } from '@/utils/chromeI18n';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useGenerator } from './hooks/useGenerator';
|
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 { Dialog, DialogContent } from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -36,6 +36,7 @@ export default function TestDataGeneratorPage() {
|
|||||||
const [fields, setFields] = useState<FieldConfig[]>([]);
|
const [fields, setFields] = useState<FieldConfig[]>([]);
|
||||||
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
||||||
const [isEditorOpen, setIsEditorOpen] = useState(false);
|
const [isEditorOpen, setIsEditorOpen] = useState(false);
|
||||||
|
const [editingRule, setEditingRule] = useState<DataRule | null>(null);
|
||||||
|
|
||||||
// 生成选项
|
// 生成选项
|
||||||
const [count, setCount] = useState(100);
|
const [count, setCount] = useState(100);
|
||||||
@@ -123,11 +124,30 @@ export default function TestDataGeneratorPage() {
|
|||||||
(loadedFields: FieldConfig[]) => {
|
(loadedFields: FieldConfig[]) => {
|
||||||
setFields(loadedFields);
|
setFields(loadedFields);
|
||||||
setSelectedIndex(null);
|
setSelectedIndex(null);
|
||||||
|
setEditingRule(null);
|
||||||
clearResult();
|
clearResult();
|
||||||
},
|
},
|
||||||
[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(() => {
|
const handleGenerate = useCallback(() => {
|
||||||
if (fields.length === 0) return;
|
if (fields.length === 0) return;
|
||||||
@@ -188,6 +208,8 @@ export default function TestDataGeneratorPage() {
|
|||||||
onAdd={handleAddField}
|
onAdd={handleAddField}
|
||||||
onEdit={handleOpenEditor}
|
onEdit={handleOpenEditor}
|
||||||
onReorder={handleReorder}
|
onReorder={handleReorder}
|
||||||
|
editingRule={editingRule}
|
||||||
|
onRuleSaved={handleRuleSaved}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -195,7 +217,7 @@ export default function TestDataGeneratorPage() {
|
|||||||
{/* 规则管理标签页 */}
|
{/* 规则管理标签页 */}
|
||||||
{activeTab === 'rules' && (
|
{activeTab === 'rules' && (
|
||||||
<div className="p-4 rounded-xl border border-border bg-card shadow-sm">
|
<div className="p-4 rounded-xl border border-border bg-card shadow-sm">
|
||||||
<RuleManager currentFields={fields} onLoad={handleLoadRule} />
|
<RuleManager onLoad={handleLoadRule} onEdit={handleEditRule} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -264,7 +286,7 @@ export default function TestDataGeneratorPage() {
|
|||||||
<Dialog open={isEditorOpen} onOpenChange={setIsEditorOpen}>
|
<Dialog open={isEditorOpen} onOpenChange={setIsEditorOpen}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
className="w-[calc(100vw-4rem)] max-w-[420px] max-h-[calc(100vh-4rem)] p-0 pt-6 flex flex-col"
|
className="w-[calc(100vw-2rem)] max-w-[520px] max-h-[calc(100vh-2rem)] p-0 pt-6 flex flex-col"
|
||||||
>
|
>
|
||||||
<div className="flex-1 overflow-y-auto px-6 pb-4">
|
<div className="flex-1 overflow-y-auto px-6 pb-4">
|
||||||
{selectedField && selectedIndex !== null && (
|
{selectedField && selectedIndex !== null && (
|
||||||
|
|||||||
@@ -33,6 +33,14 @@ export function getById(id: string): DataRule | undefined {
|
|||||||
return rules.find((rule) => rule.id === id);
|
return rules.find((rule) => rule.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据名称获取规则
|
||||||
|
*/
|
||||||
|
export function getByName(name: string): DataRule | undefined {
|
||||||
|
const rules = getAll();
|
||||||
|
return rules.find((rule) => rule.name === name);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取规则数量
|
* 获取规则数量
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user