Compare commits

..

1 Commits

Author SHA1 Message Date
LingandRX 36b6bfd20d Develop (#74)
* feat: 生成成功改为 toast 提示
* refactor: 数据预览改为显示示例数据,简化组件逻辑
* fix: 非必填字段预览显示 null
* fix: 修复空值率与必填状态的逻辑
* fix: 切换为非必填时默认空值率 100%
* feat: 字段编辑弹窗添加「完成」按钮 去除关闭按钮,添加取消按钮
* refactor: 删除未使用的 defaultNullRate 功能
* feat: 优化规则管理功能
* fix: 更新 features 测试断言以匹配新增的 testDataGenerator
* refactor: 将 formatFileSize 重命名为 formatBytes 并移至通用工具模块
* refactor: 使用 Label 组件替换原生 label 元素,并在 DecodeResultPaper 中引入 Input 组件
* refactor: 规范化页面组件目录结构
* refactor(JsonTools): 规范化页面组件目录结构
* refactor(StorageCleaner): 规范化页面组件目录结构
* refactor(Timestamp): 规范化页面组件目录结构
* refactor(i18n): 移除 chrome.i18n 国际化,统一使用中文硬编码
* refactor(AppRoot): 移除 RouterProvider 组件,直接渲染子组件
* fix(RouterProvider): 将 goBack 重命名为 goHome,修复跨窗同步 merge 不一致
* fix(ThemeModeProvider): 修复主题快照同步与首屏闪烁问题
* refactor: 将 UI 文案改为中文并优化相关逻辑
* refactor: 清理代码注释并优化组件结构
* feat(Dashboard): 重构仪表板功能组件和逻辑
* refactor: 更新功能键名称并移除 README 文件
* refactor: 重构 TopBar 组件并更新项目结构
* feat(TopBar): 添加搜索功能组件并优化结构
* refactor: 统一使用 Button 组件替换原有 button 元素
* feat: 添加 EmptyPlaceholder 组件并更新相关页面
* feat(TextStatistics): 引入 StatCard 组件以优化统计信息展示
* feat(Timestamp): 重构时间戳转换功能,新增 ConverterForm 组件
* feat(Jwt): 重构 JWT 组件,新增常量和结果视图
* refactor(RightClickRestorer): 用状态配置表简化页面 UI 结构
* fix(business): 修复 skewedRandom 函数中的数学计算错误
* refactor(ImageUploader): 移除预览 URL 相关逻辑并简化粘贴事件处理
* feat(TestDataGenerator): 更新规则编辑提示和警告显示逻辑
* refactor(CopyButton.test): 移除冗余测试用例
* refactor(RouterProvider, chromeStorage, useContextMenuData): 更新存储获取逻辑以移除默认值参数
* refactor(代码重复): 抽取共享工具函数与 UI 组件,消除多处重复实现
* fix(RouterProvider): 修复初始加载状态逻辑,确保在加载前不覆盖 chrome.storage
* feat(TopBar): 添加搜索框快捷键提示功能及相关测试
* refactor(StorageCleaner): 移动 isRestrictedUrl 函数到独立模块并更新相关引用
* fix(StorageCleaner): 修复 IndexedDB 清理竞态、部分成功计数与刷新后状态同步
* fix: 修复路由初始化竞态、存储状态静默覆盖与存储清理误删
* refactor: 统一中文文案并简化组件结构,提升代码可读性
* refactor: 更新 Worker 消息类型以支持生成任务 ID
* fix: 修复 ruleStorage 写入失败处理与 CopyButton 成功态样式
2026-06-27 17:58:59 +08:00
6 changed files with 123 additions and 169 deletions
-17
View File
@@ -29,20 +29,3 @@
- 导出工具:`src/utils/dataExporter.ts` - 导出工具:`src/utils/dataExporter.ts`
实现约束与任务完成状态记录在 [TASKS.md](./TASKS.md)。 实现约束与任务完成状态记录在 [TASKS.md](./TASKS.md)。
## 开发者注意事项(与源码同步)
以下行为以 `src/` 源码为准;设计文档中的旧版 class API、`metadata`/`options` 嵌套结构已废弃。
### Worker 任务 ID`generationId`
`useGenerator` 每次调用 `generate()` 递增 `generationIdRef`,经 `start` 消息传入 Worker。Worker 所有响应(`progress` / `complete` / `error`)均携带同一 `generationId`
- **取消**`cancel()` 先递增 ID 再发送 `cancel`,使进行中的 Worker 响应被主线程忽略;Worker 每生成 100 行让出事件循环以处理 cancel。
- **快速重试**:新任务 ID 大于旧响应时,旧消息被丢弃,避免 UI 状态错乱。
类型见 `WorkerRequestMessage` / `WorkerResponseMessage``src/types/testDataGenerator.ts`)。
### 规则存储写入失败
`ruleStorage.save()` / `update()``localStorage.setItem` 失败时返回 `null`(不部分提交)。页面层(如 `FieldList.tsx`)仅在返回值非空时 Toast 成功。详见 [rule-management.md § 存储机制](./rule-management.md#存储机制) 与 `src/utils/README.md`
+113 -54
View File
@@ -24,20 +24,25 @@
## 数据结构 ## 数据结构
> **与源码对齐**:类型定义见 `src/types/testDataGenerator.ts`。规则**不**持久化生成数量与导出格式(由页面 `GenerateOptions` 状态管理)。
### 规则模板 ### 规则模板
```typescript ```typescript
interface DataRule { interface DataRule {
id: string; id: string; // 唯一标识
name: string; name: string; // 规则名称
description?: string; description?: string; // 规则描述
fields: FieldConfig[]; fields: FieldConfig[]; // 字段配置
createdAt: number; options: {
updatedAt: number; total: number; // 生成数量
lastUsedAt?: number; format: 'json' | 'csv';
useCount: number; defaultEmptyRate: number; // 默认空值率(0-100
};
metadata: {
createdAt: number; // 创建时间
updatedAt: number; // 更新时间
lastUsedAt?: number; // 最后使用时间
useCount: number; // 使用次数
};
} }
``` ```
@@ -45,14 +50,13 @@ interface DataRule {
```typescript ```typescript
interface FieldConfig { interface FieldConfig {
id: string; id: string; // 字段唯一标识
name: string; name: string; // 字段名
description?: string; generator: string; // 生成器名称
generatorId: string; // 生成器 ID,对应 lib/generators 中的 id params: Record<string, any>; // 生成器参数
params: Record<string, unknown>; unique: boolean; // 唯一性约束
required: boolean; required: boolean; // 是否必填
nullRate: number; // 空值率 0-100,仅 required=false 时生效 emptyRate?: number; // 选填字段的空值概率(0-100)
unique: boolean;
} }
``` ```
@@ -106,9 +110,11 @@ interface FieldConfig {
1. 验证规则名称不为空 1. 验证规则名称不为空
2. 检查规则数量是否达到上限(20 条) 2. 检查规则数量是否达到上限(20 条)
3. 若达上限,`save()` 返回 `null`UI 应阻止或提示 3. 如果达到上限,显示提示"已达到最大规则数量"
4. 生成唯一 ID,写入 `createdAt`/`updatedAt``useCount` 初始为 0 4. 生成唯一 ID
5. 调用 `ruleStorage.save()`;仅当返回值非 `null` 时视为成功(`localStorage` 写入失败同样返回 `null` 5. 设置创建时间和更新时间
6. 初始化使用次数为 0
7. 保存到 localStorage
--- ---
@@ -161,13 +167,17 @@ interface FieldConfig {
- 支持按规则描述搜索 - 支持按规则描述搜索
- 搜索为模糊匹配,不区分大小写 - 搜索为模糊匹配,不区分大小写
**实现方式**`src/utils/ruleStorage.ts`: **实现方式**:
```typescript ```typescript
import * as ruleStorage from '@/utils/ruleStorage'; search(keyword: string): DataRule[] {
const rules = this.getAll();
const lowerKeyword = keyword.toLowerCase();
function searchRules(query: string): DataRule[] { return rules.filter(r =>
return ruleStorage.search(query); r.name.toLowerCase().includes(lowerKeyword) ||
r.description?.toLowerCase().includes(lowerKeyword)
);
} }
``` ```
@@ -214,7 +224,9 @@ function searchRules(query: string): DataRule[] {
1. 加载原规则配置到编辑器 1. 加载原规则配置到编辑器
2. 用户修改配置 2. 用户修改配置
3. 点击保存时调用 `ruleStorage.update()`;返回非 `null` 才更新 `updatedAt` 并提示成功 3. 点击保存时更新规则
4. 更新 metadata.updatedAt
5. 保存到 localStorage
--- ---
@@ -257,12 +269,18 @@ function searchRules(query: string): DataRule[] {
**实现方式**: **实现方式**:
```typescript ```typescript
function loadRule(ruleId: string): void { loadRule(ruleId: string): void {
const rule = ruleStorage.getById(ruleId); const rule = this.storage.getById(ruleId);
if (!rule) return;
setFields(rule.fields); // 应用规则配置
ruleStorage.recordUse(ruleId); this.setFields(rule.fields);
this.setOptions(rule.options);
// 记录使用
this.storage.recordUse(ruleId);
// 更新预览
this.updatePreview();
} }
``` ```
@@ -274,7 +292,7 @@ function loadRule(ruleId: string): void {
1. 点击"复制"按钮 1. 点击"复制"按钮
2. 创建规则的副本 2. 创建规则的副本
3. 名称添加「(副本)」后缀(默认 `duplicate(id, '(副本)')` 3. 名称添加"(副本)"后缀
4. 生成新的 ID 4. 生成新的 ID
5. 保存为新规则 5. 保存为新规则
@@ -309,20 +327,23 @@ function loadRule(ruleId: string): void {
└─────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────┘
``` ```
**导出格式**`exportRules()` 返回 `DataRule[]` 的 JSON 字符串,无外层包装): **导出格式**:
```json ```json
[ {
"version": "1.0",
"exportedAt": "2024-01-20T10:15:45.000Z",
"rules": [
{ {
"id": "rule_123456", "id": "rule_123456",
"name": "电商用户数据 - 测试用", "name": "电商用户数据 - 测试用",
"description": "用于测试用户注册功能", "description": "用于测试用户注册功能",
"fields": [], "fields": [...],
"createdAt": 1704067200000, "options": {...},
"updatedAt": 1704067200000, "metadata": {...}
"useCount": 0
} }
] ]
}
``` ```
--- ---
@@ -380,25 +401,63 @@ function loadRule(ruleId: string): void {
### 本地存储 ### 本地存储
使用 `localStorage`,键名 `testDataGenerator_rules`。API 为**命名导出函数**(见 `src/utils/ruleStorage.ts`): 使用 localStorage 存储规则数据:
| 函数 | 说明 |
| ---- | ---- |
| `getAll()` / `getById()` / `getByName()` | 读取 |
| `save()` / `update()` / `deleteRule()` / `duplicate()` | 写入;失败时返回 `null``false` |
| `recordUse()` | 递增 `useCount`、更新 `lastUsedAt` |
| `search()` / `getRecent()` | 搜索与最近使用 |
| `exportRules()` / `importRules()` | 导入导出 JSON 数组 |
| `clear()` | 清空全部规则 |
写入失败(如 `QuotaExceededError`)时,内部 `setAll()` 返回 `false``save`/`update` 返回 `null``deleteRule` 返回 `false`,并在控制台输出 `[ruleStorage] 保存规则失败`。调用方须检查返回值,避免误报成功。
```typescript ```typescript
import * as ruleStorage from '@/utils/ruleStorage'; class RuleStorage {
private readonly STORAGE_KEY = 'testDataGenerator_rules';
const saved = ruleStorage.save({ name: '示例', fields }); // 获取所有规则
if (!saved) { getAll(): DataRule[] {
// 达上限或 localStorage 不可用 const data = localStorage.getItem(this.STORAGE_KEY);
return data ? JSON.parse(data) : [];
}
// 保存规则
save(rule: DataRule): { success: boolean; message?: string } {
const rules = this.getAll();
// 规则数量限制:最多 20 条
const MAX_RULES = 20;
if (rules.length >= MAX_RULES) {
return {
success: false,
message: `已达到最大规则数量(${MAX_RULES}条),请删除一些规则后再保存`,
};
}
rules.push(rule);
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(rules));
return { success: true };
}
// 更新规则
update(id: string, updates: Partial<DataRule>): void {
const rules = this.getAll();
const index = rules.findIndex((r) => r.id === id);
if (index !== -1) {
rules[index] = { ...rules[index], ...updates };
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(rules));
}
}
// 删除规则
delete(id: string): void {
const rules = this.getAll();
const filtered = rules.filter((r) => r.id !== id);
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(filtered));
}
// 记录使用
recordUse(id: string): void {
const rules = this.getAll();
const rule = rules.find((r) => r.id === id);
if (rule) {
rule.metadata.lastUsedAt = Date.now();
rule.metadata.useCount++;
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(rules));
}
}
} }
``` ```
@@ -1,7 +1,5 @@
# 技术实现 # 技术实现
> **文档同步说明**:本文档含早期设计稿代码示例,部分类型/API 已与实现偏离。开发时请以 `src/types/testDataGenerator.ts`、`src/workers/generator.worker.ts`、`src/pages/TestDataGenerator/hooks/useGenerator.ts`、`src/utils/ruleStorage.ts` 为准。近期变更摘要见 [README § 开发者注意事项](./README.md#开发者注意事项与源码同步)。
## 技术栈 ## 技术栈
| 技术 | 用途 | 版本 | | 技术 | 用途 | 版本 |
@@ -1500,69 +1498,6 @@ export class DataExporter {
## Web Worker 使用 ## Web Worker 使用
### 消息协议
```typescript
// src/types/testDataGenerator.ts
type WorkerRequestMessage =
| { type: 'start'; payload: WorkerStartPayload }
| { type: 'cancel' };
type WorkerResponseMessage =
| { type: 'progress'; generationId: number; payload: GenerateProgress }
| { type: 'complete'; generationId: number; payload: GenerateResult }
| { type: 'error'; generationId: number; payload: { error: string } };
interface WorkerStartPayload {
generationId: number; // 任务 ID,用于忽略过期响应
fields: FieldConfig[];
count: number;
csvMode: boolean;
}
```
### useGenerator Hook
`src/pages/TestDataGenerator/hooks/useGenerator.ts` 负责 Worker 生命周期与 `generationId` 管理:
- Worker **复用**:同一 Hook 实例内只创建一次,出错后 `terminate` 并在下次重建
- **开始生成**`generate(fields, count, csvMode?)` 递增 `generationId` 并 post `start`
- **取消**`cancel()` 递增 ID(使旧响应失效)并 post `cancel`;取消完成的 `complete` 不写入 `error` 状态
- **响应过滤**`onmessage` 中若 `data.generationId !== generationIdRef.current` 则忽略
```typescript
const generate = (fields: FieldConfig[], count: number, csvMode = false) => {
if (isGenerating) return;
const generationId = ++generationIdRef.current;
worker.postMessage({ type: 'start', payload: { generationId, fields, count, csvMode } });
};
const cancel = () => {
if (workerRef.current && isGenerating) {
++generationIdRef.current;
worker.postMessage({ type: 'cancel' });
setIsGenerating(false);
}
};
```
### Worker 实现要点
`src/workers/generator.worker.ts`
-`field.generatorId` 查找生成器;选填字段按 `nullRate` 随机置 `null`
- 唯一性:≤1000 条随机+重试;>1000 条优先 `generateAtIndex`
-`YIELD_EVERY`100)行 `await setTimeout(0)`,以便处理 `cancel`
- 进度:每 1000 条或最后一行 post `progress`
---
## Web Worker 使用(历史设计稿,仅供参考)
<details>
<summary>展开查看旧版设计示例(与当前实现不一致)</summary>
### 创建 Worker ### 创建 Worker
```typescript ```typescript
@@ -1658,7 +1593,7 @@ export function useGenerator() {
} }
``` ```
### 错误处理(设计稿,`useErrorHandler.ts` 未实现) ### 错误处理
```typescript ```typescript
// src/pages/TestDataGenerator/hooks/useErrorHandler.ts // src/pages/TestDataGenerator/hooks/useErrorHandler.ts
@@ -1701,12 +1636,8 @@ export function useErrorHandler(options: ErrorHandlerOptions = {}) {
--- ---
</details>
## 错误提示机制 ## 错误提示机制
> 当前实现:`TestDataGenerator/index.tsx` 直接使用 `useGenerator` 的 `error`/`result` 与 `ResultPanel` 展示警告;下方示例引用未实现的 `useErrorHandler`,仅供对照。
### 错误类型分类 ### 错误类型分类
| 错误类型 | 严重程度 | 触发场景 | 提示方式 | | 错误类型 | 严重程度 | 触发场景 | 提示方式 |
+1 -1
View File
@@ -10,7 +10,7 @@
| `SwitchButtonGroup.tsx` | 通用切换按钮组,支持 `small/medium/large` 三种尺寸,用于页面子模式切换 | | `SwitchButtonGroup.tsx` | 通用切换按钮组,支持 `small/medium/large` 三种尺寸,用于页面子模式切换 |
| `EmptyPlaceholder.tsx` | 虚线边框空状态占位,统一工具页「暂无结果」提示样式 | | `EmptyPlaceholder.tsx` | 虚线边框空状态占位,统一工具页「暂无结果」提示样式 |
| `TextInputArea.tsx` | 增强文本输入区域,支持校验规则、工具栏操作、字符计数、清空 | | `TextInputArea.tsx` | 增强文本输入区域,支持校验规则、工具栏操作、字符计数、清空 |
| `CopyButton.tsx` | 一键复制按钮复制成功后 1.5s 内切换为 Check 图标并应用 `text-emerald-500`;空内容/失败时 `toast` 提示 | | `CopyButton.tsx` | 一键复制按钮,支持复制成功状态动画,封装 `copyTextToClipboard` `toast` 反馈 |
| `ImageUploader.tsx` | 图片上传组件,支持拖拽上传、文件选择和预览 | | `ImageUploader.tsx` | 图片上传组件,支持拖拽上传、文件选择和预览 |
| `QrCodePreview.tsx` | 二维码预览组件,展示生成的二维码图片,提供复制和下载操作 | | `QrCodePreview.tsx` | 二维码预览组件,展示生成的二维码图片,提供复制和下载操作 |
| `DecodeResultPaper.tsx` | Base64 解码结果展示面板,显示 MIME 类型、文件大小、文件名输入和下载按钮 | | `DecodeResultPaper.tsx` | Base64 解码结果展示面板,显示 MIME 类型、文件大小、文件名输入和下载按钮 |
+1 -3
View File
@@ -39,9 +39,7 @@
- `FieldConfig` — 字段配置(字段名、生成器、参数、必填、空值率、唯一性) - `FieldConfig` — 字段配置(字段名、生成器、参数、必填、空值率、唯一性)
- `DataRule` — 可保存/导入/导出的字段规则 - `DataRule` — 可保存/导入/导出的字段规则
- `GeneratorDefinition` / `GeneratorParam` — 内置生成器定义和参数 Schema - `GeneratorDefinition` / `GeneratorParam` — 内置生成器定义和参数 Schema
- `GenerateResult` / `GenerateProgress` — Worker 生成结果进度 - `GenerateResult` / `GenerateProgress` / `WorkerMessage` — Worker 生成结果进度和消息协议
- `WorkerRequestMessage` / `WorkerResponseMessage` — Worker 消息协议;每条响应携带 `generationId`,用于忽略过期任务(取消或快速重试时)
- `WorkerMessage` — 已废弃,请使用上述两种消息类型
- `ExportFile` — JSON/CSV 导出文件描述 - `ExportFile` — JSON/CSV 导出文件描述
## 修改 StorageSchema 的注意事项 ## 修改 StorageSchema 的注意事项
+1 -18
View File
@@ -24,7 +24,7 @@
| `textStatistics.ts` | 文本统计:使用 `Intl.Segmenter` 计算字符数/单词数/行数/字节大小 | | `textStatistics.ts` | 文本统计:使用 `Intl.Segmenter` 计算字符数/单词数/行数/字节大小 |
| `format.ts` | 通用格式化:`formatBytes` 将字节转为可读字符串(B/KB/MB/GB/TB | | `format.ts` | 通用格式化:`formatBytes` 将字节转为可读字符串(B/KB/MB/GB/TB |
| `dayjs.ts` | Day.js 初始化:扩展 UTC、Timezone、RelativeTime 插件,加载中文本地化 | | `dayjs.ts` | Day.js 初始化:扩展 UTC、Timezone、RelativeTime 插件,加载中文本地化 |
| `ruleStorage.ts` | 测试数据生成器规则存储:基于 `localStorage` 的 CRUD、搜索、导入/导出和数量限制(见下方说明) | | `ruleStorage.ts` | 测试数据生成器规则存储:基于 `localStorage` 的 CRUD、搜索、导入/导出和数量限制 |
| `dataExporter.ts` | 测试数据导出:JSON/CSV 转换、文件下载和复制到剪贴板 | | `dataExporter.ts` | 测试数据导出:JSON/CSV 转换、文件下载和复制到剪贴板 |
| `rightClickInjection.ts` | 右键恢复注入脚本:在页面上下文恢复 contextmenu/copy/paste 等事件默认行为 | | `rightClickInjection.ts` | 右键恢复注入脚本:在页面上下文恢复 contextmenu/copy/paste 等事件默认行为 |
@@ -36,23 +36,6 @@
| `useContextMenuData.ts` | 右键菜单数据 Hook:从 storage 读取待处理数据,匹配 featureKey 后消费并触发回调 | | `useContextMenuData.ts` | 右键菜单数据 Hook:从 storage 读取待处理数据,匹配 featureKey 后消费并触发回调 |
| `useDebounce.ts` | 防抖 Hook:对值进行延迟更新,避免频繁触发 | | `useDebounce.ts` | 防抖 Hook:对值进行延迟更新,避免频繁触发 |
### ruleStorage 写入失败处理
`ruleStorage.ts` 使用函数式导出(非 class),存储键为 `testDataGenerator_rules`,最多 `MAX_RULES = 20` 条。
写入经内部 `setAll()` 完成;`localStorage.setItem` 抛错(如配额超限)时返回 `false`,并 `console.error`**不会部分提交**
| 方法 | 写入失败返回值 | 常见失败原因 |
| ---- | -------------- | ------------ |
| `save()` | `null` | 达上限、规则不存在(更新时)、`setItem` 异常 |
| `update()` | `null` | 规则不存在、`setItem` 异常 |
| `deleteRule()` | `false` | 规则不存在、`setItem` 异常 |
| `duplicate()` | 仍可能返回对象但数据未持久化 | 见源码:`duplicate` 未检查 `setAll` 返回值(已知缺口) |
调用方应检查返回值后再展示成功 Toast。页面层参考 `FieldList.tsx``save`/`update` 返回非空才提示「规则已保存/已更新」。
规则仅持久化 `name``description``fields` 及时间戳/使用统计;生成数量与导出格式由页面状态管理,不写入规则。
### useStorageState 初始化防覆盖 ### useStorageState 初始化防覆盖
`useStorageState` 在挂载时从 storage 异步加载。写入 storage 需满足以下任一条件: `useStorageState` 在挂载时从 storage 异步加载。写入 storage 需满足以下任一条件: