Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2db05a922 |
@@ -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`。
|
||||
|
||||
@@ -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<string, any>; // 生成器参数
|
||||
unique: boolean; // 唯一性约束
|
||||
required: boolean; // 是否必填
|
||||
emptyRate?: number; // 选填字段的空值概率(0-100)
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
generatorId: string; // 生成器 ID,对应 lib/generators 中的 id
|
||||
params: Record<string, unknown>;
|
||||
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<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));
|
||||
}
|
||||
}
|
||||
const saved = ruleStorage.save({ name: '示例', fields });
|
||||
if (!saved) {
|
||||
// 达上限或 localStorage 不可用
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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 使用(历史设计稿,仅供参考)
|
||||
|
||||
<details>
|
||||
<summary>展开查看旧版设计示例(与当前实现不一致)</summary>
|
||||
|
||||
### 创建 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 = {}) {
|
||||
|
||||
---
|
||||
|
||||
</details>
|
||||
|
||||
## 错误提示机制
|
||||
|
||||
> 当前实现:`TestDataGenerator/index.tsx` 直接使用 `useGenerator` 的 `error`/`result` 与 `ResultPanel` 展示警告;下方示例引用未实现的 `useErrorHandler`,仅供对照。
|
||||
|
||||
### 错误类型分类
|
||||
|
||||
| 错误类型 | 严重程度 | 触发场景 | 提示方式 |
|
||||
|
||||
@@ -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 类型、文件大小、文件名输入和下载按钮 |
|
||||
|
||||
@@ -234,38 +234,29 @@ export default function FieldList({
|
||||
if (!ruleName.trim()) return;
|
||||
|
||||
const trimmedName = ruleName.trim();
|
||||
const existingRule = ruleStorage.getByName(trimmedName);
|
||||
|
||||
// 检查名称是否重复
|
||||
if (!overwrite && existingRule) {
|
||||
setShowConfirmOverwrite(true);
|
||||
return;
|
||||
if (!overwrite) {
|
||||
const existingRule = ruleStorage.getByName(trimmedName);
|
||||
if (existingRule) {
|
||||
setShowConfirmOverwrite(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const savedRule = ruleStorage.save(
|
||||
overwrite && existingRule
|
||||
? {
|
||||
id: existingRule.id,
|
||||
name: trimmedName,
|
||||
description: ruleDescription.trim(),
|
||||
fields: fields,
|
||||
}
|
||||
: {
|
||||
name: trimmedName,
|
||||
description: ruleDescription.trim(),
|
||||
fields: fields,
|
||||
},
|
||||
);
|
||||
const newRule = ruleStorage.save({
|
||||
name: trimmedName,
|
||||
description: ruleDescription.trim(),
|
||||
fields: fields,
|
||||
});
|
||||
|
||||
if (savedRule) {
|
||||
if (newRule) {
|
||||
setShowSaveDialog(false);
|
||||
setShowConfirmOverwrite(false);
|
||||
setRuleName('');
|
||||
setRuleDescription('');
|
||||
toast.success(overwrite ? '规则已覆盖' : '规则已保存');
|
||||
toast.success('规则已保存');
|
||||
onRuleSaved?.();
|
||||
} else {
|
||||
toast.error('规则保存失败');
|
||||
}
|
||||
},
|
||||
[ruleName, ruleDescription, fields, onRuleSaved],
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { FieldConfig } from '@/types/testDataGenerator';
|
||||
|
||||
vi.mock('@/utils/ruleStorage', () => ({
|
||||
getByName: vi.fn(),
|
||||
save: vi.fn(),
|
||||
update: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import FieldList from '../FieldList';
|
||||
import * as ruleStorage from '@/utils/ruleStorage';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const mockedRuleStorage = vi.mocked(ruleStorage);
|
||||
const mockedToast = vi.mocked(toast);
|
||||
|
||||
const mockFields: FieldConfig[] = [
|
||||
{
|
||||
id: 'field-1',
|
||||
name: 'username',
|
||||
generatorId: 'string',
|
||||
params: {},
|
||||
required: true,
|
||||
nullRate: 0,
|
||||
unique: false,
|
||||
},
|
||||
];
|
||||
|
||||
const defaultProps = {
|
||||
fields: mockFields,
|
||||
onUpdate: vi.fn(),
|
||||
onRemove: vi.fn(),
|
||||
onAdd: vi.fn(),
|
||||
onEdit: vi.fn(),
|
||||
onReorder: vi.fn(),
|
||||
};
|
||||
|
||||
describe('FieldList 规则保存', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockedRuleStorage.getByName.mockReturnValue(undefined);
|
||||
mockedRuleStorage.save.mockReturnValue({
|
||||
id: 'rule-1',
|
||||
name: 'My Rule',
|
||||
fields: mockFields,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
useCount: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('覆盖同名规则时应更新已有规则而非新建', async () => {
|
||||
const user = userEvent.setup();
|
||||
const existingRule = {
|
||||
id: 'existing-rule-id',
|
||||
name: 'My Rule',
|
||||
fields: mockFields,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
useCount: 0,
|
||||
};
|
||||
|
||||
mockedRuleStorage.getByName.mockReturnValue(existingRule);
|
||||
|
||||
render(<FieldList {...defaultProps} />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /保存规则/ }));
|
||||
await user.type(screen.getByPlaceholderText('规则名称'), 'My Rule');
|
||||
await user.click(screen.getByRole('button', { name: '确认' }));
|
||||
|
||||
expect(screen.getByText('已存在同名规则,是否覆盖保存?')).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '覆盖' }));
|
||||
|
||||
expect(mockedRuleStorage.save).toHaveBeenCalledWith({
|
||||
id: 'existing-rule-id',
|
||||
name: 'My Rule',
|
||||
description: '',
|
||||
fields: mockFields,
|
||||
});
|
||||
expect(mockedToast.success).toHaveBeenCalledWith('规则已覆盖');
|
||||
});
|
||||
});
|
||||
+3
-1
@@ -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 的注意事项
|
||||
|
||||
+18
-1
@@ -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 需满足以下任一条件:
|
||||
|
||||
@@ -48,27 +48,6 @@ describe('ruleStorage', () => {
|
||||
expect(updated).toBeNull();
|
||||
});
|
||||
|
||||
it('save 带 id 时应更新已有规则而非新建', () => {
|
||||
const first = ruleStorage.save({
|
||||
name: 'Test Rule',
|
||||
fields: [mockField],
|
||||
});
|
||||
expect(first).not.toBeNull();
|
||||
|
||||
const updatedField = { ...mockField, name: 'email' };
|
||||
const updated = ruleStorage.save({
|
||||
id: first!.id,
|
||||
name: 'Test Rule',
|
||||
description: 'Updated',
|
||||
fields: [updatedField],
|
||||
});
|
||||
|
||||
expect(updated).not.toBeNull();
|
||||
expect(ruleStorage.getCount()).toBe(1);
|
||||
expect(ruleStorage.getById(first!.id)?.fields[0].name).toBe('email');
|
||||
expect(ruleStorage.getById(first!.id)?.description).toBe('Updated');
|
||||
});
|
||||
|
||||
it('deleteRule 在 localStorage 写入失败时应返回 false', () => {
|
||||
const saved = ruleStorage.save({
|
||||
name: 'Test Rule',
|
||||
|
||||
Reference in New Issue
Block a user