From b2db05a9227388dfe5544dd56f3df310070cd1a8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 29 Jun 2026 16:08:10 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=90=8C=E6=AD=A5=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=94=9F=E6=88=90=E5=99=A8=20Worker=20?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=20ID=20=E4=B8=8E=20ruleStorage=20=E5=86=99?= =?UTF-8?q?=E5=85=A5=E5=A4=B1=E8=B4=A5=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: LingandRX --- docs/test-data-generator/README.md | 17 ++ docs/test-data-generator/rule-management.md | 179 ++++++------------ .../technical-implementation.md | 71 ++++++- src/components/README.md | 2 +- src/types/README.md | 4 +- src/utils/README.md | 19 +- 6 files changed, 169 insertions(+), 123 deletions(-) diff --git a/docs/test-data-generator/README.md b/docs/test-data-generator/README.md index 540cdd0..c356c42 100644 --- a/docs/test-data-generator/README.md +++ b/docs/test-data-generator/README.md @@ -29,3 +29,20 @@ - 导出工具:`src/utils/dataExporter.ts` 实现约束与任务完成状态记录在 [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`。 diff --git a/docs/test-data-generator/rule-management.md b/docs/test-data-generator/rule-management.md index 91a4dc1..a930001 100644 --- a/docs/test-data-generator/rule-management.md +++ b/docs/test-data-generator/rule-management.md @@ -24,25 +24,20 @@ ## 数据结构 +> **与源码对齐**:类型定义见 `src/types/testDataGenerator.ts`。规则**不**持久化生成数量与导出格式(由页面 `GenerateOptions` 状态管理)。 + ### 规则模板 ```typescript interface DataRule { - id: string; // 唯一标识 - name: string; // 规则名称 - description?: string; // 规则描述 - fields: FieldConfig[]; // 字段配置 - options: { - total: number; // 生成数量 - format: 'json' | 'csv'; - defaultEmptyRate: number; // 默认空值率(0-100) - }; - metadata: { - createdAt: number; // 创建时间 - updatedAt: number; // 更新时间 - lastUsedAt?: number; // 最后使用时间 - useCount: number; // 使用次数 - }; + id: string; + name: string; + description?: string; + fields: FieldConfig[]; + createdAt: number; + updatedAt: number; + lastUsedAt?: number; + useCount: number; } ``` @@ -50,13 +45,14 @@ interface DataRule { ```typescript interface FieldConfig { - id: string; // 字段唯一标识 - name: string; // 字段名 - generator: string; // 生成器名称 - params: Record; // 生成器参数 - unique: boolean; // 唯一性约束 - required: boolean; // 是否必填 - emptyRate?: number; // 选填字段的空值概率(0-100) + id: string; + name: string; + description?: string; + generatorId: string; // 生成器 ID,对应 lib/generators 中的 id + params: Record; + required: boolean; + nullRate: number; // 空值率 0-100,仅 required=false 时生效 + unique: boolean; } ``` @@ -110,11 +106,9 @@ interface FieldConfig { 1. 验证规则名称不为空 2. 检查规则数量是否达到上限(20 条) -3. 如果达到上限,显示提示"已达到最大规则数量" -4. 生成唯一 ID -5. 设置创建时间和更新时间 -6. 初始化使用次数为 0 -7. 保存到 localStorage +3. 若达上限,`save()` 返回 `null`,UI 应阻止或提示 +4. 生成唯一 ID,写入 `createdAt`/`updatedAt`,`useCount` 初始为 0 +5. 调用 `ruleStorage.save()`;仅当返回值非 `null` 时视为成功(`localStorage` 写入失败同样返回 `null`) --- @@ -167,17 +161,13 @@ interface FieldConfig { - 支持按规则描述搜索 - 搜索为模糊匹配,不区分大小写 -**实现方式**: +**实现方式**(`src/utils/ruleStorage.ts`): ```typescript -search(keyword: string): DataRule[] { - const rules = this.getAll(); - const lowerKeyword = keyword.toLowerCase(); +import * as ruleStorage from '@/utils/ruleStorage'; - return rules.filter(r => - r.name.toLowerCase().includes(lowerKeyword) || - r.description?.toLowerCase().includes(lowerKeyword) - ); +function searchRules(query: string): DataRule[] { + return ruleStorage.search(query); } ``` @@ -224,9 +214,7 @@ search(keyword: string): DataRule[] { 1. 加载原规则配置到编辑器 2. 用户修改配置 -3. 点击保存时更新规则 -4. 更新 metadata.updatedAt -5. 保存到 localStorage +3. 点击保存时调用 `ruleStorage.update()`;返回非 `null` 才更新 `updatedAt` 并提示成功 --- @@ -269,18 +257,12 @@ search(keyword: string): DataRule[] { **实现方式**: ```typescript -loadRule(ruleId: string): void { - const rule = this.storage.getById(ruleId); +function loadRule(ruleId: string): void { + const rule = ruleStorage.getById(ruleId); + if (!rule) return; - // 应用规则配置 - this.setFields(rule.fields); - this.setOptions(rule.options); - - // 记录使用 - this.storage.recordUse(ruleId); - - // 更新预览 - this.updatePreview(); + setFields(rule.fields); + ruleStorage.recordUse(ruleId); } ``` @@ -292,7 +274,7 @@ loadRule(ruleId: string): void { 1. 点击"复制"按钮 2. 创建规则的副本 -3. 名称添加"(副本)"后缀 +3. 名称添加「(副本)」后缀(默认 `duplicate(id, '(副本)')`) 4. 生成新的 ID 5. 保存为新规则 @@ -327,23 +309,20 @@ loadRule(ruleId: string): void { └─────────────────────────────────────────────────────────────────────┘ ``` -**导出格式**: +**导出格式**(`exportRules()` 返回 `DataRule[]` 的 JSON 字符串,无外层包装): ```json -{ - "version": "1.0", - "exportedAt": "2024-01-20T10:15:45.000Z", - "rules": [ - { - "id": "rule_123456", - "name": "电商用户数据 - 测试用", - "description": "用于测试用户注册功能", - "fields": [...], - "options": {...}, - "metadata": {...} - } - ] -} +[ + { + "id": "rule_123456", + "name": "电商用户数据 - 测试用", + "description": "用于测试用户注册功能", + "fields": [], + "createdAt": 1704067200000, + "updatedAt": 1704067200000, + "useCount": 0 + } +] ``` --- @@ -401,63 +380,25 @@ loadRule(ruleId: string): void { ### 本地存储 -使用 localStorage 存储规则数据: +使用 `localStorage`,键名 `testDataGenerator_rules`。API 为**命名导出函数**(见 `src/utils/ruleStorage.ts`): + +| 函数 | 说明 | +| ---- | ---- | +| `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 -class RuleStorage { - private readonly STORAGE_KEY = 'testDataGenerator_rules'; +import * as ruleStorage from '@/utils/ruleStorage'; - // 获取所有规则 - getAll(): DataRule[] { - 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): 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)); - } - } +const saved = ruleStorage.save({ name: '示例', fields }); +if (!saved) { + // 达上限或 localStorage 不可用 } ``` diff --git a/docs/test-data-generator/technical-implementation.md b/docs/test-data-generator/technical-implementation.md index 2531468..f8f4df7 100644 --- a/docs/test-data-generator/technical-implementation.md +++ b/docs/test-data-generator/technical-implementation.md @@ -1,5 +1,7 @@ # 技术实现 +> **文档同步说明**:本文档含早期设计稿代码示例,部分类型/API 已与实现偏离。开发时请以 `src/types/testDataGenerator.ts`、`src/workers/generator.worker.ts`、`src/pages/TestDataGenerator/hooks/useGenerator.ts`、`src/utils/ruleStorage.ts` 为准。近期变更摘要见 [README § 开发者注意事项](./README.md#开发者注意事项与源码同步)。 + ## 技术栈 | 技术 | 用途 | 版本 | @@ -1498,6 +1500,69 @@ export class DataExporter { ## 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 使用(历史设计稿,仅供参考) + +
+展开查看旧版设计示例(与当前实现不一致) + ### 创建 Worker ```typescript @@ -1593,7 +1658,7 @@ export function useGenerator() { } ``` -### 错误处理 +### 错误处理(设计稿,`useErrorHandler.ts` 未实现) ```typescript // src/pages/TestDataGenerator/hooks/useErrorHandler.ts @@ -1636,8 +1701,12 @@ export function useErrorHandler(options: ErrorHandlerOptions = {}) { --- +
+ ## 错误提示机制 +> 当前实现:`TestDataGenerator/index.tsx` 直接使用 `useGenerator` 的 `error`/`result` 与 `ResultPanel` 展示警告;下方示例引用未实现的 `useErrorHandler`,仅供对照。 + ### 错误类型分类 | 错误类型 | 严重程度 | 触发场景 | 提示方式 | diff --git a/src/components/README.md b/src/components/README.md index c8759a0..f08076a 100644 --- a/src/components/README.md +++ b/src/components/README.md @@ -10,7 +10,7 @@ | `SwitchButtonGroup.tsx` | 通用切换按钮组,支持 `small/medium/large` 三种尺寸,用于页面子模式切换 | | `EmptyPlaceholder.tsx` | 虚线边框空状态占位,统一工具页「暂无结果」提示样式 | | `TextInputArea.tsx` | 增强文本输入区域,支持校验规则、工具栏操作、字符计数、清空 | -| `CopyButton.tsx` | 一键复制按钮,支持复制成功状态动画,封装 `copyTextToClipboard` 和 `toast` 反馈 | +| `CopyButton.tsx` | 一键复制按钮:复制成功后 1.5s 内切换为 Check 图标并应用 `text-emerald-500`;空内容/失败时 `toast` 提示 | | `ImageUploader.tsx` | 图片上传组件,支持拖拽上传、文件选择和预览 | | `QrCodePreview.tsx` | 二维码预览组件,展示生成的二维码图片,提供复制和下载操作 | | `DecodeResultPaper.tsx` | Base64 解码结果展示面板,显示 MIME 类型、文件大小、文件名输入和下载按钮 | diff --git a/src/types/README.md b/src/types/README.md index b91d268..756a3f3 100644 --- a/src/types/README.md +++ b/src/types/README.md @@ -39,7 +39,9 @@ - `FieldConfig` — 字段配置(字段名、生成器、参数、必填、空值率、唯一性) - `DataRule` — 可保存/导入/导出的字段规则 - `GeneratorDefinition` / `GeneratorParam` — 内置生成器定义和参数 Schema -- `GenerateResult` / `GenerateProgress` / `WorkerMessage` — Worker 生成结果、进度和消息协议 +- `GenerateResult` / `GenerateProgress` — Worker 生成结果与进度 +- `WorkerRequestMessage` / `WorkerResponseMessage` — Worker 消息协议;每条响应携带 `generationId`,用于忽略过期任务(取消或快速重试时) +- `WorkerMessage` — 已废弃,请使用上述两种消息类型 - `ExportFile` — JSON/CSV 导出文件描述 ## 修改 StorageSchema 的注意事项 diff --git a/src/utils/README.md b/src/utils/README.md index 0cbdcd7..47c55f5 100644 --- a/src/utils/README.md +++ b/src/utils/README.md @@ -24,7 +24,7 @@ | `textStatistics.ts` | 文本统计:使用 `Intl.Segmenter` 计算字符数/单词数/行数/字节大小 | | `format.ts` | 通用格式化:`formatBytes` 将字节转为可读字符串(B/KB/MB/GB/TB) | | `dayjs.ts` | Day.js 初始化:扩展 UTC、Timezone、RelativeTime 插件,加载中文本地化 | -| `ruleStorage.ts` | 测试数据生成器规则存储:基于 `localStorage` 的 CRUD、搜索、导入/导出和数量限制 | +| `ruleStorage.ts` | 测试数据生成器规则存储:基于 `localStorage` 的 CRUD、搜索、导入/导出和数量限制(见下方说明) | | `dataExporter.ts` | 测试数据导出:JSON/CSV 转换、文件下载和复制到剪贴板 | | `rightClickInjection.ts` | 右键恢复注入脚本:在页面上下文恢复 contextmenu/copy/paste 等事件默认行为 | @@ -36,6 +36,23 @@ | `useContextMenuData.ts` | 右键菜单数据 Hook:从 storage 读取待处理数据,匹配 featureKey 后消费并触发回调 | | `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` 在挂载时从 storage 异步加载。写入 storage 需满足以下任一条件: