develop (#68)
* docs(AGENTS.md): 添加 Git Commit 必须使用中文描述的规范 * docs: 更新测试数据生成器功能设计文档 * refactor: 删除死代码 * docs: 统一测试数据生成器文档,移除 SQL/TypeScript 导出,修复接口命名不一致 * feat: 实现测试数据生成器功能 * docs: 更新 README 和 AGENTS 文档,补充测试数据生成器模块说明
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 数据导出工具
|
||||
* 实现 JSON 和 CSV 格式的数据导出
|
||||
*/
|
||||
|
||||
import type { ExportFile } from '@/types/testDataGenerator';
|
||||
|
||||
/**
|
||||
* 数据导出器
|
||||
*/
|
||||
export class DataExporter {
|
||||
/**
|
||||
* 将数据转换为 JSON 字符串
|
||||
*/
|
||||
static toJSON(data: Record<string, unknown>[], pretty = true): string {
|
||||
return JSON.stringify(data, null, pretty ? 2 : undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数据转换为 CSV 字符串
|
||||
*/
|
||||
static toCSV(data: Record<string, unknown>[]): string {
|
||||
if (data.length === 0) return '';
|
||||
|
||||
// 获取所有列名
|
||||
const headers = Array.from(new Set(data.flatMap((row) => Object.keys(row))));
|
||||
|
||||
// 构建 CSV 内容
|
||||
const rows = [
|
||||
headers.map((h) => this.escapeCSV(h)).join(','),
|
||||
...data.map((row) => headers.map((h) => this.escapeCSV(String(row[h] ?? ''))).join(',')),
|
||||
];
|
||||
|
||||
return rows.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义 CSV 字段
|
||||
*/
|
||||
private static escapeCSV(value: string): string {
|
||||
// 如果包含逗号、引号、换行符,需要用引号包裹
|
||||
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
||||
return `"${value.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据格式导出数据
|
||||
*/
|
||||
static exportByFormat(data: Record<string, unknown>[], format: 'json' | 'csv'): string {
|
||||
if (format === 'csv') {
|
||||
return this.toCSV(data);
|
||||
}
|
||||
return this.toJSON(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出为多个文件
|
||||
*/
|
||||
static toMultipleFiles(
|
||||
data: Record<string, unknown>[],
|
||||
formats: ('json' | 'csv')[],
|
||||
filename = 'test-data',
|
||||
): ExportFile[] {
|
||||
return formats.map((format) => ({
|
||||
filename,
|
||||
content: this.exportByFormat(data, format),
|
||||
mimeType: this.getMimeType(format),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出为单个文件
|
||||
*/
|
||||
static toSingleFile(
|
||||
data: Record<string, unknown>[],
|
||||
format: 'json' | 'csv',
|
||||
filename = 'test-data',
|
||||
): ExportFile {
|
||||
return {
|
||||
filename,
|
||||
content: this.exportByFormat(data, format),
|
||||
mimeType: this.getMimeType(format),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MIME 类型
|
||||
*/
|
||||
private static getMimeType(format: string): string {
|
||||
const mimeTypes: Record<string, string> = {
|
||||
json: 'application/json',
|
||||
csv: 'text/csv',
|
||||
};
|
||||
return mimeTypes[format] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
*/
|
||||
static download(file: ExportFile): void {
|
||||
const blob = new Blob([file.content], { type: file.mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${file.filename}.${file.mimeType === 'application/json' ? 'json' : 'csv'}`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载多个文件
|
||||
*/
|
||||
static downloadMultiple(files: ExportFile[]): void {
|
||||
files.forEach((file) => this.download(file));
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制到剪贴板
|
||||
*/
|
||||
static async copyToClipboard(content: string): Promise<boolean> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[DataExporter] 复制失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* 规则存储工具
|
||||
* 实现测试数据生成器规则的持久化存储和管理
|
||||
*/
|
||||
|
||||
import type { DataRule, FieldConfig } from '@/types/testDataGenerator';
|
||||
|
||||
/** 存储键名 */
|
||||
const STORAGE_KEY = 'testDataGenerator_rules';
|
||||
|
||||
/** 最大规则数量 */
|
||||
const MAX_RULES = 20;
|
||||
|
||||
/**
|
||||
* 获取所有规则
|
||||
*/
|
||||
export function getAll(): DataRule[] {
|
||||
try {
|
||||
const data = localStorage.getItem(STORAGE_KEY);
|
||||
if (!data) return [];
|
||||
return JSON.parse(data) as DataRule[];
|
||||
} catch (error) {
|
||||
console.error('[ruleStorage] 获取规则列表失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 获取规则
|
||||
*/
|
||||
export function getById(id: string): DataRule | undefined {
|
||||
const rules = getAll();
|
||||
return rules.find((rule) => rule.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据名称获取规则
|
||||
*/
|
||||
export function getByName(name: string): DataRule | undefined {
|
||||
const rules = getAll();
|
||||
return rules.find((rule) => rule.name === name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取规则数量
|
||||
*/
|
||||
export function getCount(): number {
|
||||
return getAll().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否达到最大数量限制
|
||||
*/
|
||||
export function isMaxReached(): boolean {
|
||||
return getCount() >= MAX_RULES;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存规则(新增或更新)
|
||||
*/
|
||||
export function save(
|
||||
rule: Omit<DataRule, 'id' | 'createdAt' | 'updatedAt' | 'useCount'> & { id?: string },
|
||||
): DataRule | null {
|
||||
const rules = getAll();
|
||||
const now = Date.now();
|
||||
|
||||
// 新增规则
|
||||
if (!rule.id) {
|
||||
if (isMaxReached()) {
|
||||
console.warn('[ruleStorage] 规则数量已达上限');
|
||||
return null;
|
||||
}
|
||||
const newRule: DataRule = {
|
||||
...rule,
|
||||
id: generateId(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
useCount: 0,
|
||||
};
|
||||
rules.unshift(newRule);
|
||||
setAll(rules);
|
||||
return newRule;
|
||||
}
|
||||
|
||||
// 更新规则
|
||||
const index = rules.findIndex((r) => r.id === rule.id);
|
||||
if (index === -1) {
|
||||
console.warn('[ruleStorage] 规则不存在:', rule.id);
|
||||
return null;
|
||||
}
|
||||
const updatedRule: DataRule = {
|
||||
...rules[index],
|
||||
...rule,
|
||||
id: rule.id,
|
||||
updatedAt: now,
|
||||
};
|
||||
rules[index] = updatedRule;
|
||||
setAll(rules);
|
||||
return updatedRule;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新规则
|
||||
*/
|
||||
export function update(id: string, updates: Partial<DataRule>): DataRule | null {
|
||||
const rules = getAll();
|
||||
const index = rules.findIndex((r) => r.id === id);
|
||||
if (index === -1) {
|
||||
console.warn('[ruleStorage] 规则不存在:', id);
|
||||
return null;
|
||||
}
|
||||
|
||||
const updatedRule: DataRule = {
|
||||
...rules[index],
|
||||
...updates,
|
||||
id: rules[index].id, // 确保 ID 不被覆盖
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
rules[index] = updatedRule;
|
||||
setAll(rules);
|
||||
return updatedRule;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除规则
|
||||
*/
|
||||
export function deleteRule(id: string): boolean {
|
||||
const rules = getAll();
|
||||
const index = rules.findIndex((r) => r.id === id);
|
||||
if (index === -1) {
|
||||
console.warn('[ruleStorage] 规则不存在:', id);
|
||||
return false;
|
||||
}
|
||||
rules.splice(index, 1);
|
||||
setAll(rules);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制规则
|
||||
* @param id 规则 ID
|
||||
* @param copySuffix 复制后缀,默认为中文「(副本)」,可通过 i18n 传入
|
||||
*/
|
||||
export function duplicate(id: string, copySuffix = '(副本)'): DataRule | null {
|
||||
const rule = getById(id);
|
||||
if (!rule) {
|
||||
console.warn('[ruleStorage] 规则不存在:', id);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isMaxReached()) {
|
||||
console.warn('[ruleStorage] 规则数量已达上限');
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const newRule: DataRule = {
|
||||
...rule,
|
||||
id: generateId(),
|
||||
name: `${rule.name}${copySuffix}`,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
useCount: 0,
|
||||
};
|
||||
|
||||
const rules = getAll();
|
||||
rules.unshift(newRule);
|
||||
setAll(rules);
|
||||
return newRule;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录规则使用
|
||||
*/
|
||||
export function recordUse(id: string): void {
|
||||
const rules = getAll();
|
||||
const index = rules.findIndex((r) => r.id === id);
|
||||
if (index === -1) return;
|
||||
|
||||
rules[index].useCount += 1;
|
||||
rules[index].lastUsedAt = Date.now();
|
||||
setAll(rules);
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索规则
|
||||
*/
|
||||
export function search(query: string): DataRule[] {
|
||||
const rules = getAll();
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return rules.filter(
|
||||
(rule) =>
|
||||
rule.name.toLowerCase().includes(lowerQuery) ||
|
||||
rule.description?.toLowerCase().includes(lowerQuery),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近使用的规则
|
||||
*/
|
||||
export function getRecent(count = 5): DataRule[] {
|
||||
const rules = getAll();
|
||||
return [...rules]
|
||||
.filter((rule) => rule.lastUsedAt)
|
||||
.sort((a, b) => (b.lastUsedAt || 0) - (a.lastUsedAt || 0))
|
||||
.slice(0, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出规则(JSON 字符串)
|
||||
*/
|
||||
export function exportRules(ids?: string[]): string {
|
||||
const rules = getAll();
|
||||
const exportData = ids ? rules.filter((r) => ids.includes(r.id)) : rules;
|
||||
return JSON.stringify(exportData, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入规则
|
||||
*/
|
||||
export function importRules(jsonString: string): {
|
||||
success: number;
|
||||
failed: number;
|
||||
errors: string[];
|
||||
} {
|
||||
const result = { success: 0, failed: 0, errors: [] as string[] };
|
||||
|
||||
try {
|
||||
const data = JSON.parse(jsonString);
|
||||
if (!Array.isArray(data)) {
|
||||
result.failed = 1;
|
||||
result.errors.push('导入数据格式错误:不是数组');
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const item of data) {
|
||||
const validation = validateRule(item);
|
||||
if (!validation.valid) {
|
||||
result.failed++;
|
||||
result.errors.push(`规则 "${item.name || '未知'}": ${validation.error}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const saved = save(item as Omit<DataRule, 'id' | 'createdAt' | 'updatedAt' | 'useCount'>);
|
||||
if (saved) {
|
||||
result.success++;
|
||||
} else {
|
||||
result.failed++;
|
||||
result.errors.push(`规则 "${item.name}" 保存失败`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
result.failed = 1;
|
||||
result.errors.push(`导入失败: ${error}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有规则
|
||||
*/
|
||||
export function clear(): void {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存所有规则到存储
|
||||
*/
|
||||
function setAll(rules: DataRule[]): void {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(rules));
|
||||
} catch (error) {
|
||||
console.error('[ruleStorage] 保存规则失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一 ID
|
||||
*/
|
||||
export function generateId(): string {
|
||||
return `rule_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证规则格式
|
||||
*/
|
||||
function validateRule(rule: unknown): { valid: boolean; error?: string } {
|
||||
if (!rule || typeof rule !== 'object') {
|
||||
return { valid: false, error: '规则格式无效' };
|
||||
}
|
||||
|
||||
const obj = rule as Record<string, unknown>;
|
||||
|
||||
if (!obj.name || typeof obj.name !== 'string') {
|
||||
return { valid: false, error: '缺少规则名称' };
|
||||
}
|
||||
|
||||
if (!obj.fields || !Array.isArray(obj.fields)) {
|
||||
return { valid: false, error: '缺少字段配置' };
|
||||
}
|
||||
|
||||
for (const field of obj.fields) {
|
||||
const fieldValidation = validateField(field);
|
||||
if (!fieldValidation.valid) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `字段 "${(field as FieldConfig).name || '未知'}": ${fieldValidation.error}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证字段配置
|
||||
*/
|
||||
function validateField(field: unknown): { valid: boolean; error?: string } {
|
||||
if (!field || typeof field !== 'object') {
|
||||
return { valid: false, error: '字段格式无效' };
|
||||
}
|
||||
|
||||
const obj = field as Record<string, unknown>;
|
||||
|
||||
if (!obj.name || typeof obj.name !== 'string') {
|
||||
return { valid: false, error: '缺少字段名称' };
|
||||
}
|
||||
|
||||
if (!obj.generatorId || typeof obj.generatorId !== 'string') {
|
||||
return { valid: false, error: '缺少生成器 ID' };
|
||||
}
|
||||
|
||||
if (typeof obj.required !== 'boolean') {
|
||||
return { valid: false, error: '缺少必填标识' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
Reference in New Issue
Block a user