* docs: 添加组件文档注释和类型导入

refactor: 统一使用 SnackbarOptions 类型
style: 优化导入语句顺序和格式

* refactor: 简化假数据生成器中的faker导入和使用

Co-authored-by: Copilot <copilot@github.com>

* feat(form-recognizer): 增强表单识别功能并优化UI交互

- 新增字段类型偏好设置功能,支持按域名保存字段类型
- 重构FieldList组件,改进字段选择和类型修改体验
- 添加字段定位闪烁功能,便于在页面上快速找到对应字段
- 优化表单填充逻辑,支持单个字段覆盖默认填充模式
- 移除独立的侧边栏页面,统一使用主页面组件
- 改进useStorageState钩子,增加加载状态管理和防抖处理

* refactor: 移除未使用的组件文件

* refactor(页面头部): 提取通用 PageHeader 组件并替换各页面头部实现

重构各页面头部为统一的 PageHeader 组件,提高代码复用性和维护性

* style(组件): 调整自动刷新开关和存储选项网格的样式

优化自动刷新开关的文本内边距,重构存储选项网格的布局结构,调整间距和边框样式

* style(ui): 调整时间戳页面和结果视图的样式

- 为时区选择器添加圆角
- 优化结果视图的布局和对齐方式
- 调整结果项的内边距和文本样式

* ci(workflow): 移除Firefox测试以简化CI流程

仅保留Chrome浏览器的构建步骤,减少CI运行时间和资源消耗
This commit is contained in:
LingandRX
2026-04-28 08:56:47 +08:00
committed by GitHub
parent fb5f98dd3b
commit c039475119
29 changed files with 908 additions and 1317 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
import type { SnackbarOptions } from '@/components/GlobalSnackbar';
import { useSnackbar } from '@/components/GlobalSnackbar';
/**
@@ -8,7 +9,7 @@ import { useSnackbar } from '@/components/GlobalSnackbar';
*/
export const copyToClipboard = async (
text: string,
showMessage?: (message: string, options?: { severity: 'success' | 'error' }) => void,
showMessage?: (message: string, options?: SnackbarOptions) => void,
): Promise<boolean> => {
try {
await navigator.clipboard.writeText(text);
+57 -16
View File
@@ -1,4 +1,4 @@
import { faker, fakerZH_CN } from '@faker-js/faker';
import { fakerZH_CN as faker } from '@faker-js/faker';
/**
* 表单字段信息接口
@@ -34,7 +34,7 @@ export class DummyDataGenerator {
* 生成随机中文姓名
*/
static generateChineseName(): string {
return fakerZH_CN.person.fullName();
return faker.person.fullName();
}
/**
@@ -45,17 +45,20 @@ export class DummyDataGenerator {
}
/**
* 生成随机手机号
* 生成随机手机号(中国格式)
*/
static generatePhoneNumber(): string {
return fakerZH_CN.phone.number();
const prefix =
'1' + faker.string.numeric({ length: 1, allowLeadingZeros: false, exclude: ['0', '1', '2'] });
const suffix = faker.string.numeric({ length: 9, allowLeadingZeros: true });
return prefix + suffix;
}
/**
* 生成有效邮箱
*/
static generateValidEmail(): string {
return fakerZH_CN.internet.email();
return faker.internet.email();
}
/**
@@ -77,14 +80,14 @@ export class DummyDataGenerator {
* 生成短文本
*/
static generateShortText(): string {
return fakerZH_CN.lorem.sentence({ min: 3, max: 6 });
return faker.lorem.sentence({ min: 3, max: 6 });
}
/**
* 生成长文本
*/
static generateLongText(): string {
return fakerZH_CN.lorem.paragraphs(5);
return faker.lorem.paragraphs(5);
}
/**
@@ -106,42 +109,42 @@ export class DummyDataGenerator {
* 生成随机数字
*/
static generateNumber(): number {
return fakerZH_CN.number.int(10000);
return faker.number.int(10000);
}
/**
* 生成随机浮点数
*/
static generateFloat(): number {
return fakerZH_CN.number.float({ max: 10000 });
return faker.number.float({ max: 10000 });
}
/**
* 生成随机负数
*/
static generateNegativeNumber(): number {
return -fakerZH_CN.number.int(10000);
return -faker.number.int(10000);
}
/**
* 生成随机日期
*/
static generateDate(): string {
return fakerZH_CN.date.recent({ days: 365 }).toISOString().split('T')[0];
return faker.date.recent({ days: 365 }).toISOString().split('T')[0];
}
/**
* 生成过去的日期
*/
static generatePastDate(): string {
return fakerZH_CN.date.past({ years: 1 }).toISOString().split('T')[0];
return faker.date.past({ years: 1 }).toISOString().split('T')[0];
}
/**
* 生成未来的日期
*/
static generateFutureDate(): string {
return fakerZH_CN.date.future({ years: 1 }).toISOString().split('T')[0];
return faker.date.future({ years: 1 }).toISOString().split('T')[0];
}
/**
@@ -779,14 +782,23 @@ export function fillFieldWithInjector(
}
/**
* 批量填充选中的字段
* 批量填充选中的字段(支持单字段模式覆盖)
*/
export function fillSelectedFields(fields: FormFieldInfo[], mode: FillMode): number {
export function fillSelectedFields(
fields: Array<FormFieldInfo & { useInvalidData?: boolean }>,
defaultMode: FillMode,
): number {
let filledCount = 0;
fields.forEach((field) => {
if (field.isSelected) {
const value = field.generatedValue || generateValueByFieldType(field.fieldType, mode);
const mode = field.useInvalidData
? FillMode.INVALID
: field.useInvalidData === false
? FillMode.VALID
: defaultMode;
// 始终根据当前 fieldType 重新生成值,确保类型变更生效
const value = generateValueByFieldType(field.fieldType, mode);
fillFieldWithInjector(field.element, value);
filledCount++;
}
@@ -795,6 +807,35 @@ export function fillSelectedFields(fields: FormFieldInfo[], mode: FillMode): num
return filledCount;
}
/**
* 闪烁字段(用于定位)
*/
export function flashField(element: HTMLElement): void {
let flashCount = 0;
const maxFlashes = 4;
const originalStyle =
element.getAttribute('data-original-style') || element.getAttribute('style') || '';
element.setAttribute('data-original-style', originalStyle);
const flash = () => {
if (flashCount >= maxFlashes) {
unhighlightField(element);
return;
}
if (flashCount % 2 === 0) {
element.style.outline = '3px solid #4caf50';
element.style.outlineOffset = '2px';
element.style.transition = 'outline 0.3s ease-in-out';
} else {
element.style.outline = '';
}
flashCount++;
setTimeout(flash, 300);
};
flash();
}
/**
* 高亮指定字段
*/
+11 -1
View File
@@ -1,5 +1,12 @@
import { FormFieldInfo, FillMode } from './dummyDataGenerator';
/**
* 字段数据接口(用于消息传递)
*/
interface MessageFieldData extends Omit<FormFieldInfo, 'element'> {
useInvalidData?: boolean;
}
/**
* 消息动作类型
*/
@@ -19,6 +26,9 @@ export enum MessageAction {
UNHIGHLIGHT_FIELD = 'unhighlightField',
HIGHLIGHT_ALL_FIELDS = 'highlightAllFields',
UNHIGHLIGHT_ALL_FIELDS = 'unhighlightAllFields',
// 字段定位/闪烁
FLASH_FIELD = 'flashField',
}
/**
@@ -28,7 +38,7 @@ export interface MessagePayload {
action: MessageAction | string;
tabId?: number;
delay?: number;
fields?: Omit<FormFieldInfo, 'element'>[];
fields?: MessageFieldData[];
mode?: FillMode;
includeHidden?: boolean;
fieldId?: string;
+15 -6
View File
@@ -1,28 +1,37 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { storageUtil } from '@/utils/chromeStorage';
import type { StorageSchema } from '@/types/storage';
export const useStorageState = (
key: 'qrCode/urlExpanded' | 'qrCode/qrExpanded',
defaultValue: boolean,
export const useStorageState = <K extends keyof StorageSchema>(
key: K,
defaultValue: StorageSchema[K],
) => {
const [value, setValue] = useState(defaultValue);
const [isInitialized, setIsInitialized] = useState(false);
const hasLoadedFromStorage = useRef(false);
// Only load from storage once on mount
useEffect(() => {
if (hasLoadedFromStorage.current) return;
const loadState = async () => {
try {
const savedValue = await storageUtil.get(key, defaultValue);
setValue(savedValue ?? defaultValue);
if (savedValue !== undefined) {
setValue(savedValue);
}
} catch (error) {
console.error(`加载状态失败 (${key}):`, error);
} finally {
setIsInitialized(true);
hasLoadedFromStorage.current = true;
}
};
loadState();
}, [key, defaultValue]);
}, [key]); // eslint-disable-line react-hooks/exhaustive-deps -- defaultValue intentionally excluded to prevent infinite loops
// Save to storage when value changes (after initial load)
useEffect(() => {
if (!isInitialized) return;