Develop (#15)
* 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:
+2
-1
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user