8d36f21f1b
* 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运行时间和资源消耗 * refactor(qrcode): 重构二维码解析功能并提取为独立模块 * style(页面样式): 统一页面头部图标颜色并优化表单样式 更新各页面头部组件的图标颜色配置,移除冗余的表单标签属性,优化输入框样式和按钮悬停效果 * feat(formMapping): 添加表单映射功能 - 创建 FormMappingPage 页面组件 - 实现表单扫描器 scanner.ts - 实现高亮器 highlighter.ts - 更新路由配置 routes.ts - 更新内容脚本 content.ts - 更新类型定义 storage.d.ts - 添加 formMappingPageStyles 样式配置 * feat(表单映射): 添加配置导出功能及状态提示 添加配置导出为JSON文件的功能,包含导出按钮和错误处理。新增Snackbar组件用于显示导出成功和错误状态。优化页面布局结构,将状态提示移至全局容器外。 * feat(表单填充): 新增智能表单填充功能 添加智能表单填充功能,包括: 1. 新增表单填充页面和路由配置 2. 实现模糊匹配引擎和智能注入引擎 3. 添加Mock数据生成器和视觉反馈渲染器 4. 扩展消息接口支持填充操作 * ci: 精简触发CI的工作流分支 移除对develop分支及其变体的触发,仅保留main分支的触发 * test: 更新路由测试以匹配新增的路由数量 * test: 将测试文件中的描述和断言翻译为中文 * test(PageHeader): 添加组件测试用例验证渲染逻辑和样式 * refactor(theme): 重构主题样式并优化仪表盘卡片组件 将页面样式配置集中管理,移除各页面中硬编码的背景色 新增 DashboardCard 组件封装通用卡片逻辑 添加 dashboardCards 配置文件统一管理卡片数据 * refactor(Button): 统一按钮样式并移除重复样式定义 将按钮样式统一封装到 Button 组件中,移除各页面重复的样式定义 更新组件文档说明,提供更清晰的用法示例 * docs: 更新 README 文件中的目录结构说明 添加新组件和配置文件的说明,保持文档与代码同步 * refactor: 优化代码类型声明和UI布局 修复类型声明从any改为never以提高类型安全性 调整TimestampPage页面布局间距 移除DashboardPage中不必要的实时时钟状态 更新ESLint配置以使用推荐配置 添加选项页面打开的错误处理 * refactor(消息通信): 使用 @webext-core/messaging 重构消息处理逻辑 将原有的 chrome.runtime.onMessage 和 chrome.tabs.sendMessage 替换为类型安全的 @webext-core/messaging 实现 添加 ProtocolMap 类型定义确保消息类型安全 更新相关页面和后台脚本使用新的消息通信方式 * fix: 修复依赖项缺失导致的潜在问题 修复 useStorageState 中缺少 defaultValue 依赖的问题 重构 RouterProvider 的初始化逻辑,使用 useCallback 优化性能 * refactor: 重构表单识别和消息处理逻辑 将表单映射UI逻辑提取到独立文件 将消息处理器提取到独立文件 将表单识别页面逻辑提取到自定义hook 优化代码结构和可维护性 * feat(侧边栏): 添加侧边栏状态变化通知功能 - 在消息类型中新增侧边栏状态变化枚举和字段 - 侧边栏打开和关闭时发送状态通知 - 替换轮询检查方式为消息监听机制 * refactor: 优化高亮组件渲染逻辑并添加防抖处理 重构高亮组件渲染逻辑,使用 requestAnimationFrame 进行节流优化 在 useStorageCleaner 中添加防抖处理以避免频繁加载 * refactor: 统一组件导出方式为默认导出 * refactor: 重构样式系统并迁移至 MUI 主题 删除冗余的 CSS 文件,统一使用 MUI 主题管理样式 新增 useActiveTabDomain 和 useSidePanelState 自定义 Hook 优化各入口点的主题集成和布局处理 * docs: 更新项目文档以反映新增功能和技术栈细节 更新 AGENTS.md 文档,详细描述新增的功能模块(存储管理、URL 管理、二维码生成、表单工具套件等)和更新的技术栈信息 * feat: 添加错误边界组件以捕获子组件错误 在 popup、sidepanel 和 options 入口点添加 ErrorBoundary 组件,用于捕获并处理子组件中的 JavaScript 错误。当错误发生时,显示友好的错误界面并提供刷新功能。 * refactor(ui): 使用全局 snackbar 替换本地通知组件 重构表单页面中的通知系统,移除本地 Snackbar 和 Alert 组件,改用全局 GlobalSnackbar 组件统一管理通知 标准化主题配置中的 CSS 属性命名,移除带前缀的样式属性 * refactor(snackbar): 重构全局消息提示为 SnackbarProvider 组件 将原本分散在各页面的 GlobalSnackbar 组件重构为集中管理的 SnackbarProvider,通过 Context 提供统一的消息提示功能。主要变更包括: 1. 创建新的 SnackbarProvider 组件作为全局消息提示容器 2. 提供 useSnackbar hook 供子组件调用 3. 移除各页面中独立的 GlobalSnackbar 实例 4. 在 App 根组件中统一集成 SnackbarProvider 5. 优化消息提示样式和交互行为 * refactor(Snackbar): 重构消息提示组件并优化样式 重构 Snackbar 组件,将状态管理逻辑提取到 GlobalSnackbar 中复用 优化消息提示样式,调整阴影效果和最小尺寸 统一各页面使用 Snackbar 的方式,移除冗余配置 * refactor: 重构路由和功能配置系统 - 将路由配置和仪表盘卡片配置合并为统一的 features 配置 - 重构 RouterProvider 增加数据校验和本地快照功能 - 优化表单字段识别和填充逻辑,提取公共方法 - 更新相关组件和文档以适配新的配置系统 - 删除废弃的 dashboardCards 和 routes 配置文件
580 lines
16 KiB
TypeScript
580 lines
16 KiB
TypeScript
import { FormMapEntry } from '@/types/storage';
|
|
|
|
/**
|
|
* 模糊匹配引擎结果接口
|
|
*/
|
|
export interface MatchResult {
|
|
element: HTMLElement | null;
|
|
score: number;
|
|
}
|
|
|
|
/**
|
|
* 注入结果接口
|
|
*/
|
|
export interface InjectResult {
|
|
success: boolean;
|
|
entry: FormMapEntry;
|
|
error?: string;
|
|
}
|
|
|
|
/**
|
|
* 模糊匹配引擎
|
|
* 根据 JSON 指纹在页面中精准定位目标 DOM 元素
|
|
*/
|
|
export class FuzzyMatcher {
|
|
private static readonly MATCH_THRESHOLD = 75;
|
|
private static readonly SCORE_SELECTOR = 50;
|
|
private static readonly SCORE_NAME_ATTR = 25;
|
|
private static readonly SCORE_PLACEHOLDER = 15;
|
|
private static readonly SCORE_NEIGHBOR_TEXT = 10;
|
|
|
|
/**
|
|
* 根据指纹查找目标元素
|
|
* @param fingerprint - 表单字段指纹
|
|
* @returns 匹配结果(包含元素和得分)
|
|
*/
|
|
public static findTargetElement(fingerprint: FormMapEntry['fingerprint']): MatchResult {
|
|
const candidates: Array<{ element: HTMLElement; score: number }> = [];
|
|
|
|
// 1. 首先尝试精确选择器匹配
|
|
if (fingerprint.selector) {
|
|
const exactMatch = document.querySelector<HTMLElement>(fingerprint.selector);
|
|
if (exactMatch) {
|
|
const score = this.calculateScore(exactMatch, fingerprint);
|
|
candidates.push({ element: exactMatch, score });
|
|
}
|
|
}
|
|
|
|
// 2. 收集所有可能的候选元素
|
|
const potentialElements = this.collectPotentialElements(fingerprint);
|
|
for (const element of potentialElements) {
|
|
const score = this.calculateScore(element, fingerprint);
|
|
if (score > 0) {
|
|
candidates.push({ element, score });
|
|
}
|
|
}
|
|
|
|
// 3. 找到最高分的候选
|
|
if (candidates.length === 0) {
|
|
return { element: null, score: 0 };
|
|
}
|
|
|
|
const bestMatch = candidates.reduce((prev, curr) => (curr.score > prev.score ? curr : prev));
|
|
|
|
return bestMatch.score >= this.MATCH_THRESHOLD
|
|
? { element: bestMatch.element, score: bestMatch.score }
|
|
: { element: null, score: bestMatch.score };
|
|
}
|
|
|
|
/**
|
|
* 计算元素匹配得分
|
|
*/
|
|
private static calculateScore(
|
|
element: HTMLElement,
|
|
fingerprint: FormMapEntry['fingerprint'],
|
|
): number {
|
|
let score = 0;
|
|
|
|
// 选择器精确匹配
|
|
if (fingerprint.selector) {
|
|
const matched = document.querySelector(fingerprint.selector);
|
|
if (matched === element) {
|
|
score += this.SCORE_SELECTOR;
|
|
}
|
|
}
|
|
|
|
// name 或 id 属性匹配
|
|
if (fingerprint.name_attr) {
|
|
const elementName = element.getAttribute('name') || '';
|
|
const elementId = element.getAttribute('id') || '';
|
|
if (elementName === fingerprint.name_attr || elementId === fingerprint.name_attr) {
|
|
score += this.SCORE_NAME_ATTR;
|
|
}
|
|
}
|
|
|
|
// placeholder 匹配
|
|
if (fingerprint.placeholder) {
|
|
const elementPlaceholder =
|
|
'placeholder' in element && (element as HTMLInputElement).placeholder;
|
|
if (elementPlaceholder === fingerprint.placeholder) {
|
|
score += this.SCORE_PLACEHOLDER;
|
|
}
|
|
}
|
|
|
|
// 邻近文本(label)匹配
|
|
if (fingerprint.name_attr || fingerprint.placeholder) {
|
|
const neighborText = this.getNeighborText(element);
|
|
const searchText = fingerprint.name_attr || fingerprint.placeholder || '';
|
|
if (neighborText.includes(searchText)) {
|
|
score += this.SCORE_NEIGHBOR_TEXT;
|
|
}
|
|
}
|
|
|
|
return score;
|
|
}
|
|
|
|
/**
|
|
* 收集潜在的候选元素
|
|
*/
|
|
private static collectPotentialElements(
|
|
_fingerprint: FormMapEntry['fingerprint'],
|
|
): HTMLElement[] {
|
|
const elements: HTMLElement[] = [];
|
|
|
|
// 获取所有表单元素
|
|
const selectors = [
|
|
'input:not([type="hidden"])',
|
|
'textarea',
|
|
'select',
|
|
'[contenteditable="true"]',
|
|
];
|
|
|
|
for (const selector of selectors) {
|
|
const found = document.querySelectorAll<HTMLElement>(selector);
|
|
found.forEach((el) => {
|
|
if (this.isVisibleElement(el)) {
|
|
elements.push(el);
|
|
}
|
|
});
|
|
}
|
|
|
|
return elements;
|
|
}
|
|
|
|
/**
|
|
* 获取元素附近的文本内容
|
|
*/
|
|
private static getNeighborText(element: HTMLElement): string {
|
|
const texts: string[] = [];
|
|
|
|
// 查找关联的 label
|
|
const id = element.getAttribute('id');
|
|
if (id) {
|
|
const label = document.querySelector(`label[for="${id}"]`);
|
|
if (label) {
|
|
texts.push(label.textContent || '');
|
|
}
|
|
}
|
|
|
|
// 查找父级 label
|
|
const parentLabel = element.closest('label');
|
|
if (parentLabel) {
|
|
texts.push(parentLabel.textContent || '');
|
|
}
|
|
|
|
// 查找相邻元素的文本
|
|
const prevSibling = element.previousElementSibling;
|
|
const nextSibling = element.nextElementSibling;
|
|
if (prevSibling) {
|
|
texts.push(prevSibling.textContent || '');
|
|
}
|
|
if (nextSibling) {
|
|
texts.push(nextSibling.textContent || '');
|
|
}
|
|
|
|
// 查找父级内的文本节点
|
|
const parent = element.parentElement;
|
|
if (parent) {
|
|
const textNodes = parent.querySelectorAll('span, div, p');
|
|
textNodes.forEach((node) => {
|
|
texts.push(node.textContent || '');
|
|
});
|
|
}
|
|
|
|
return texts.join(' ').toLowerCase();
|
|
}
|
|
|
|
/**
|
|
* 检查元素是否可见
|
|
*/
|
|
private static isVisibleElement(element: HTMLElement): boolean {
|
|
const rect = element.getBoundingClientRect();
|
|
if (rect.width === 0 || rect.height === 0) return false;
|
|
|
|
const style = window.getComputedStyle(element);
|
|
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 智能注入引擎
|
|
* 突破 React/Vue 等现代框架的表单状态绑定
|
|
*/
|
|
export class SmartInjectionEngine {
|
|
/**
|
|
* 注入数据到目标元素
|
|
* @param element - 目标 DOM 元素
|
|
* @param entry - 表单映射条目
|
|
* @param mockValue - mock数据
|
|
* @returns 注入结果
|
|
*/
|
|
public static inject(element: HTMLElement, entry: FormMapEntry, mockValue: string): InjectResult {
|
|
try {
|
|
const { action_logic } = entry;
|
|
|
|
switch (action_logic.type) {
|
|
case 'text':
|
|
this.injectText(element as HTMLInputElement | HTMLTextAreaElement, mockValue);
|
|
break;
|
|
|
|
case 'select':
|
|
this.injectSelect(element as HTMLSelectElement, action_logic);
|
|
break;
|
|
|
|
case 'checkbox':
|
|
this.injectCheckbox(element as HTMLInputElement, action_logic);
|
|
break;
|
|
|
|
default:
|
|
this.injectText(element as HTMLInputElement | HTMLTextAreaElement, mockValue);
|
|
}
|
|
|
|
return { success: true, entry };
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
entry,
|
|
error: error instanceof Error ? error.message : '注入失败',
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 注入文本类输入框
|
|
*/
|
|
private static injectText(element: HTMLInputElement | HTMLTextAreaElement, value: string): void {
|
|
// 获取原生 setter
|
|
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
element instanceof HTMLInputElement
|
|
? window.HTMLInputElement.prototype
|
|
: window.HTMLTextAreaElement.prototype,
|
|
'value',
|
|
)?.set;
|
|
|
|
if (nativeInputValueSetter) {
|
|
nativeInputValueSetter.call(element, value);
|
|
} else {
|
|
element.value = value;
|
|
}
|
|
|
|
// 连续触发事件
|
|
element.dispatchEvent(new Event('focus', { bubbles: true }));
|
|
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
element.dispatchEvent(new Event('blur', { bubbles: true }));
|
|
}
|
|
|
|
/**
|
|
* 注入下拉框
|
|
*/
|
|
private static injectSelect(
|
|
element: HTMLSelectElement,
|
|
actionLogic: FormMapEntry['action_logic'],
|
|
): void {
|
|
if (actionLogic.strategy === 'random') {
|
|
// 随机选择
|
|
const options = Array.from(element.options).filter((opt) => !opt.disabled);
|
|
if (options.length > 0) {
|
|
element.selectedIndex = Math.floor(Math.random() * options.length);
|
|
}
|
|
} else {
|
|
// 使用固定值
|
|
const value = actionLogic.value;
|
|
const matchingOption = Array.from(element.options).find(
|
|
(opt) => opt.value === value || opt.text === value,
|
|
);
|
|
if (matchingOption) {
|
|
element.value = matchingOption.value;
|
|
} else if (element.options.length > 0) {
|
|
element.selectedIndex = 0;
|
|
}
|
|
}
|
|
|
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
|
|
/**
|
|
* 注入复选框/单选框
|
|
*/
|
|
private static injectCheckbox(
|
|
element: HTMLInputElement,
|
|
actionLogic: FormMapEntry['action_logic'],
|
|
): void {
|
|
if (actionLogic.strategy === 'random') {
|
|
// 随机选择
|
|
const isChecked = Math.random() > 0.5;
|
|
if (element.type === 'checkbox') {
|
|
element.checked = isChecked;
|
|
} else if (element.type === 'radio') {
|
|
// 对于单选框,找到同 name 的所有选项并随机选择一个
|
|
const radioGroup = document.querySelectorAll<HTMLInputElement>(
|
|
`input[type="radio"][name="${element.name}"]`,
|
|
);
|
|
if (radioGroup.length > 0) {
|
|
const randomIndex = Math.floor(Math.random() * radioGroup.length);
|
|
radioGroup[randomIndex].click();
|
|
}
|
|
}
|
|
} else {
|
|
// 使用固定值
|
|
const shouldCheck = actionLogic.value === 'true' || actionLogic.value === '1';
|
|
element.checked = shouldCheck;
|
|
if (shouldCheck) {
|
|
element.click();
|
|
}
|
|
}
|
|
|
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mock 数据生成器
|
|
*/
|
|
export class MockDataGenerator {
|
|
/**
|
|
* 根据策略生成随机数据
|
|
*/
|
|
public static generate(actionLogic: FormMapEntry['action_logic'], entry: FormMapEntry): string {
|
|
const { strategy, value, type } = actionLogic;
|
|
|
|
if (strategy === 'fixed') {
|
|
return value;
|
|
}
|
|
|
|
// 根据字段类型和策略生成数据
|
|
switch (type) {
|
|
case 'text':
|
|
return this.generateText(entry);
|
|
|
|
case 'select':
|
|
return this.generateSelectValue();
|
|
|
|
case 'checkbox':
|
|
return this.generateBoolean();
|
|
|
|
default:
|
|
return this.generateText(entry);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 生成文本数据
|
|
*/
|
|
private static generateText(entry: FormMapEntry): string {
|
|
const { fingerprint, action_logic } = entry;
|
|
const { strategy, value: pattern } = action_logic;
|
|
|
|
// 根据模式生成数据
|
|
if (pattern) {
|
|
return this.generateByPattern(pattern);
|
|
}
|
|
|
|
// 根据指纹特征推断数据类型
|
|
const name = fingerprint.name_attr.toLowerCase();
|
|
const placeholder = fingerprint.placeholder.toLowerCase();
|
|
|
|
if (name.includes('phone') || placeholder.includes('phone')) {
|
|
return this.generatePhoneNumber();
|
|
}
|
|
if (name.includes('email') || placeholder.includes('email')) {
|
|
return this.generateEmail();
|
|
}
|
|
if (name.includes('name') || placeholder.includes('name')) {
|
|
return this.generateName();
|
|
}
|
|
if (name.includes('id') || name.includes('card')) {
|
|
return this.generateIdCard();
|
|
}
|
|
if (name.includes('date') || placeholder.includes('date')) {
|
|
return this.generateDate();
|
|
}
|
|
if (name.includes('number') || placeholder.includes('number')) {
|
|
return this.generateNumber();
|
|
}
|
|
|
|
// 默认生成随机文本
|
|
return strategy === 'random' ? this.generateRandomText() : '测试数据';
|
|
}
|
|
|
|
/**
|
|
* 根据模式生成数据
|
|
*/
|
|
private static generateByPattern(pattern: string): string {
|
|
if (pattern.includes('phone') || pattern.includes('mobile')) {
|
|
return this.generatePhoneNumber();
|
|
}
|
|
if (pattern.includes('email')) {
|
|
return this.generateEmail();
|
|
}
|
|
if (pattern.includes('name')) {
|
|
return this.generateName();
|
|
}
|
|
if (pattern.includes('date')) {
|
|
return this.generateDate();
|
|
}
|
|
if (pattern.includes('idcard') || pattern.includes('身份证')) {
|
|
return this.generateIdCard();
|
|
}
|
|
if (/^\d+$/.test(pattern)) {
|
|
return this.generateNumber(pattern.length);
|
|
}
|
|
|
|
return pattern;
|
|
}
|
|
|
|
/**
|
|
* 生成手机号
|
|
*/
|
|
private static generatePhoneNumber(): string {
|
|
const prefix = '1' + ['3', '4', '5', '6', '7', '8', '9'][Math.floor(Math.random() * 7)];
|
|
const suffix = Math.floor(Math.random() * 1000000000)
|
|
.toString()
|
|
.padStart(9, '0');
|
|
return prefix + suffix;
|
|
}
|
|
|
|
/**
|
|
* 生成邮箱
|
|
*/
|
|
private static generateEmail(): string {
|
|
const names = ['test', 'user', 'admin', 'guest', 'demo'];
|
|
const domains = ['example.com', 'test.com', 'gmail.com', 'outlook.com'];
|
|
const name = names[Math.floor(Math.random() * names.length)];
|
|
const domain = domains[Math.floor(Math.random() * domains.length)];
|
|
const num = Math.floor(Math.random() * 1000);
|
|
return `${name}${num}@${domain}`;
|
|
}
|
|
|
|
/**
|
|
* 生成姓名
|
|
*/
|
|
private static generateName(): string {
|
|
const surnames = ['张', '李', '王', '刘', '陈', '杨', '赵', '黄'];
|
|
const givenNames = ['伟', '芳', '强', '英', '华', '建', '明', '娜'];
|
|
return (
|
|
surnames[Math.floor(Math.random() * surnames.length)] +
|
|
givenNames[Math.floor(Math.random() * givenNames.length)]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 生成身份证号
|
|
*/
|
|
private static generateIdCard(): string {
|
|
const areaCodes = ['110101', '310101', '440101', '120101', '320101'];
|
|
const areaCode = areaCodes[Math.floor(Math.random() * areaCodes.length)];
|
|
const year = (1980 + Math.floor(Math.random() * 30)).toString();
|
|
const month = String(1 + Math.floor(Math.random() * 12)).padStart(2, '0');
|
|
const day = String(1 + Math.floor(Math.random() * 28)).padStart(2, '0');
|
|
const random = Math.floor(Math.random() * 10000)
|
|
.toString()
|
|
.padStart(4, '0');
|
|
return areaCode + year + month + day + random;
|
|
}
|
|
|
|
/**
|
|
* 生成日期
|
|
*/
|
|
private static generateDate(): string {
|
|
const date = new Date();
|
|
date.setDate(date.getDate() - Math.floor(Math.random() * 365));
|
|
return date.toISOString().split('T')[0];
|
|
}
|
|
|
|
/**
|
|
* 生成数字
|
|
*/
|
|
private static generateNumber(length: number = 6): string {
|
|
return Math.floor(Math.random() * Math.pow(10, length))
|
|
.toString()
|
|
.padStart(length, '0');
|
|
}
|
|
|
|
/**
|
|
* 生成随机文本
|
|
*/
|
|
private static generateRandomText(): string {
|
|
const texts = ['测试内容', '示例文本', 'Lorem ipsum', '随机数据', 'Sample Text'];
|
|
return texts[Math.floor(Math.random() * texts.length)];
|
|
}
|
|
|
|
/**
|
|
* 生成下拉框值
|
|
*/
|
|
private static generateSelectValue(): string {
|
|
return '选项' + (Math.floor(Math.random() * 5) + 1);
|
|
}
|
|
|
|
/**
|
|
* 生成布尔值
|
|
*/
|
|
private static generateBoolean(): string {
|
|
return Math.random() > 0.5 ? 'true' : 'false';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 视觉反馈渲染器
|
|
*/
|
|
export class FeedbackRenderer {
|
|
private static readonly SUCCESS_COLOR = '#32CD32';
|
|
private static readonly ERROR_COLOR = '#FF4444';
|
|
private static readonly HIGHLIGHT_DURATION = 3000;
|
|
|
|
/**
|
|
* 渲染成功反馈
|
|
*/
|
|
public static renderSuccess(element: HTMLElement): void {
|
|
this.applyHighlight(element, this.SUCCESS_COLOR);
|
|
}
|
|
|
|
/**
|
|
* 渲染失败反馈
|
|
*/
|
|
public static renderError(element: HTMLElement | null): void {
|
|
if (!element) return;
|
|
this.applyHighlight(element, this.ERROR_COLOR);
|
|
}
|
|
|
|
/**
|
|
* 应用高亮样式
|
|
*/
|
|
private static applyHighlight(element: HTMLElement, color: string): void {
|
|
// 保存原始样式
|
|
const originalStyle = element.getAttribute('style') || '';
|
|
element.setAttribute('data-original-style', originalStyle);
|
|
|
|
// 应用高亮
|
|
element.style.outline = `3px solid ${color}`;
|
|
element.style.outlineOffset = '2px';
|
|
element.style.transition = 'outline 0.3s ease';
|
|
|
|
// 自动移除高亮
|
|
setTimeout(() => {
|
|
const savedStyle = element.getAttribute('data-original-style');
|
|
if (savedStyle) {
|
|
element.setAttribute('style', savedStyle);
|
|
element.removeAttribute('data-original-style');
|
|
} else {
|
|
element.style.outline = '';
|
|
element.style.outlineOffset = '';
|
|
}
|
|
}, this.HIGHLIGHT_DURATION);
|
|
}
|
|
|
|
/**
|
|
* 清除所有高亮
|
|
*/
|
|
public static clearAllHighlights(): void {
|
|
const highlightedElements = document.querySelectorAll('[data-original-style]');
|
|
highlightedElements.forEach((element) => {
|
|
const savedStyle = element.getAttribute('data-original-style');
|
|
if (savedStyle) {
|
|
element.setAttribute('style', savedStyle);
|
|
element.removeAttribute('data-original-style');
|
|
}
|
|
});
|
|
}
|
|
}
|