Refactor and enhance form recognition, mapping, and UI components (#18)
* 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 配置文件
This commit is contained in:
+15
-61
@@ -1,6 +1,6 @@
|
||||
import '../.wxt/types/imports.d.ts';
|
||||
import { browser } from 'wxt/browser';
|
||||
import { type MessagePayload, type MessageResponse } from '@/utils/messages';
|
||||
import { MessageAction, onMessage } from '@/utils/messages';
|
||||
|
||||
export default defineBackground(() => {
|
||||
// 监听扩展图标点击事件,打开侧边栏
|
||||
@@ -14,66 +14,20 @@ export default defineBackground(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// 监听扩展安装或更新事件
|
||||
browser.runtime.onInstalled.addListener(async ({ reason }) => {
|
||||
if (reason === 'install') {
|
||||
console.log('Extension installed for the first time');
|
||||
} else if (reason === 'update') {
|
||||
console.log('Extension updated to a new version');
|
||||
// 使用 @webext-core/messaging 处理消息
|
||||
onMessage(MessageAction.RELOAD_TAB, async (message) => {
|
||||
const { tabId, delay = 0 } = message.data;
|
||||
|
||||
const executeReload = () => {
|
||||
browser.tabs.reload(tabId);
|
||||
};
|
||||
|
||||
if (delay > 0) {
|
||||
setTimeout(executeReload, delay);
|
||||
} else {
|
||||
executeReload();
|
||||
}
|
||||
|
||||
return { success: true, message: '刷新请求已接收' };
|
||||
});
|
||||
|
||||
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
|
||||
if (changeInfo.status === 'complete') {
|
||||
console.log('加载完成的 Tab ID:', tabId);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听来自 popup/sidepanel 的消息
|
||||
chrome.runtime.onMessage.addListener(
|
||||
(message: MessagePayload, sender, sendResponse: (response: MessageResponse) => void) => {
|
||||
try {
|
||||
// 处理跨标签页的消息转发
|
||||
if (sender.tab) {
|
||||
// 从内容脚本或弹出页面发送的消息
|
||||
console.log('收到来自标签页的消息:', message.action);
|
||||
sendResponse({ success: true, message: '消息已收到' });
|
||||
} else {
|
||||
// 从扩展其他部分发送的消息
|
||||
console.log('收到来自扩展的消息:', message.action);
|
||||
|
||||
// 处理刷新标签页请求
|
||||
if (message.action === 'reloadTab' && message.tabId !== undefined) {
|
||||
const tabId = message.tabId;
|
||||
const delay = message.delay || 0;
|
||||
|
||||
const executeReload = () => {
|
||||
browser.tabs
|
||||
.reload(tabId)
|
||||
.then(() => {
|
||||
console.log('标签页刷新成功:', tabId);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('刷新标签页失败:', err.message);
|
||||
});
|
||||
};
|
||||
|
||||
if (delay > 0) {
|
||||
setTimeout(executeReload, delay);
|
||||
} else {
|
||||
executeReload();
|
||||
}
|
||||
|
||||
sendResponse({ success: true, message: '刷新请求已接收' });
|
||||
return;
|
||||
}
|
||||
|
||||
sendResponse({ success: true, message: '消息已收到' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理消息失败:', error);
|
||||
sendResponse({ success: false, message: '处理消息失败' });
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
+6
-219
@@ -1,228 +1,15 @@
|
||||
import '../.wxt/types/imports.d.ts';
|
||||
import {
|
||||
fillAllFields,
|
||||
clearAllFields,
|
||||
fillSelectedFields,
|
||||
scanFormFields,
|
||||
highlightField,
|
||||
unhighlightField,
|
||||
flashField,
|
||||
FillMode,
|
||||
type FormFieldInfo,
|
||||
} from '@/utils/dummyDataGenerator';
|
||||
import { MessageAction, type MessagePayload, type MessageResponse } from '@/utils/messages';
|
||||
|
||||
import { SmartDetector } from '@/utils/formMapping/scanner';
|
||||
import { highlighter } from '@/utils/formMapping/highlighter';
|
||||
import {
|
||||
FuzzyMatcher,
|
||||
SmartInjectionEngine,
|
||||
FeedbackRenderer,
|
||||
} from '@/utils/formMapping/smartInjector';
|
||||
import { storageUtil } from '@/utils/chromeStorage';
|
||||
import { FormMapEntry } from '@/types/storage';
|
||||
|
||||
// 存储当前扫描到的字段列表,用于高亮联动
|
||||
let currentFields: FormFieldInfo[] = [];
|
||||
import { initFormMappingHelper } from '@/utils/formMapping/ui';
|
||||
import { initMessageHandler } from './content/messageHandler';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['<all_urls>'],
|
||||
runAt: 'document_end',
|
||||
main() {
|
||||
// === 通用表单映射助手逻辑 ===
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (
|
||||
area === 'local' &&
|
||||
(changes['active_form_map'] || changes['app/formMapping/isPicking'])
|
||||
) {
|
||||
updateMappingUI();
|
||||
}
|
||||
});
|
||||
// 初始化表单映射助手逻辑 (UI, Picker, Highlighter)
|
||||
initFormMappingHelper();
|
||||
|
||||
async function updateMappingUI() {
|
||||
const entries = ((await storageUtil.get('active_form_map')) as FormMapEntry[]) || [];
|
||||
const isPicking = ((await storageUtil.get('app/formMapping/isPicking')) as boolean) || false;
|
||||
|
||||
if (entries.length > 0 || isPicking) {
|
||||
highlighter.show();
|
||||
highlighter.draw(entries);
|
||||
|
||||
if (isPicking) {
|
||||
highlighter.enablePicker(async (el) => {
|
||||
const fingerprint = SmartDetector.generateFingerprint(el);
|
||||
const label = SmartDetector.extractSemanticLabel(el);
|
||||
|
||||
const newEntry: FormMapEntry = {
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
label_display: label,
|
||||
fingerprint,
|
||||
action_logic: { type: 'text', strategy: 'fixed', value: '' },
|
||||
ui_state: { is_selected: true },
|
||||
};
|
||||
|
||||
const currentMap = ((await storageUtil.get('active_form_map')) as FormMapEntry[]) || [];
|
||||
await storageUtil.set('active_form_map', [...currentMap, newEntry]);
|
||||
await storageUtil.set('app/formMapping/isPicking', false);
|
||||
});
|
||||
} else {
|
||||
highlighter.disablePicker();
|
||||
}
|
||||
} else {
|
||||
highlighter.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// 初始加载映射 UI
|
||||
updateMappingUI();
|
||||
|
||||
// === 原有表单识别逻辑 ===
|
||||
// 监听来自 popup/sidepanel 的消息
|
||||
chrome.runtime.onMessage.addListener(
|
||||
(message: MessagePayload, _sender, sendResponse: (response: MessageResponse) => void) => {
|
||||
try {
|
||||
switch (message.action) {
|
||||
case MessageAction.SCAN_FORM_FIELDS: {
|
||||
const result = scanFormFields();
|
||||
currentFields = result.fields;
|
||||
sendResponse({
|
||||
success: true,
|
||||
fields: result.fields.map((f) => ({
|
||||
id: f.id,
|
||||
fieldType: f.fieldType,
|
||||
label: f.label,
|
||||
placeholder: f.placeholder,
|
||||
name: f.name,
|
||||
value: f.value,
|
||||
isSelected: f.isSelected,
|
||||
generatedValue: f.generatedValue,
|
||||
})),
|
||||
totalCount: result.totalCount,
|
||||
validCount: result.validCount,
|
||||
hasModal: !!result.modalContainer,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case MessageAction.FILL_VALID_DATA:
|
||||
fillAllFields(FillMode.VALID, message.includeHidden || false);
|
||||
sendResponse({ success: true, message: '已填充有效数据' });
|
||||
break;
|
||||
case MessageAction.FILL_INVALID_DATA:
|
||||
fillAllFields(FillMode.INVALID, message.includeHidden || false);
|
||||
sendResponse({ success: true, message: '已填充无效数据' });
|
||||
break;
|
||||
case MessageAction.FILL_SELECTED_FIELDS: {
|
||||
// 使用之前扫描时存储的字段,因为它们包含element属性
|
||||
const incomingFields = message.fields || [];
|
||||
const fieldsToFill = currentFields.map((field) => {
|
||||
const incomingField = incomingFields.find((f) => f.id === field.id);
|
||||
if (incomingField) {
|
||||
return {
|
||||
...field,
|
||||
fieldType: incomingField.fieldType,
|
||||
isSelected: incomingField.isSelected,
|
||||
useInvalidData: (incomingField as { useInvalidData?: boolean }).useInvalidData,
|
||||
};
|
||||
}
|
||||
return field;
|
||||
});
|
||||
const count = fillSelectedFields(fieldsToFill, message.mode || FillMode.VALID);
|
||||
sendResponse({ success: true, message: `已填充 ${count} 个字段` });
|
||||
break;
|
||||
}
|
||||
case MessageAction.CLEAR_ALL_FIELDS:
|
||||
clearAllFields();
|
||||
sendResponse({ success: true, message: '已清空所有字段' });
|
||||
break;
|
||||
case MessageAction.HIGHLIGHT_FIELD: {
|
||||
const fieldId = message.fieldId;
|
||||
const field = currentFields.find((f) => f.id === fieldId);
|
||||
if (field) {
|
||||
highlightField(field.element);
|
||||
sendResponse({ success: true });
|
||||
} else {
|
||||
sendResponse({ success: false, message: '未找到字段' });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageAction.UNHIGHLIGHT_FIELD: {
|
||||
const fieldId = message.fieldId;
|
||||
const field = currentFields.find((f) => f.id === fieldId);
|
||||
if (field) {
|
||||
unhighlightField(field.element);
|
||||
sendResponse({ success: true });
|
||||
} else {
|
||||
sendResponse({ success: false, message: '未找到字段' });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageAction.HIGHLIGHT_ALL_FIELDS: {
|
||||
const fieldIds = message.fieldIds || [];
|
||||
fieldIds.forEach((id) => {
|
||||
const field = currentFields.find((f) => f.id === id);
|
||||
if (field) {
|
||||
highlightField(field.element);
|
||||
}
|
||||
});
|
||||
sendResponse({ success: true });
|
||||
break;
|
||||
}
|
||||
case MessageAction.UNHIGHLIGHT_ALL_FIELDS:
|
||||
currentFields.forEach((field) => {
|
||||
unhighlightField(field.element);
|
||||
});
|
||||
sendResponse({ success: true });
|
||||
break;
|
||||
case MessageAction.FLASH_FIELD: {
|
||||
const fieldId = message.fieldId;
|
||||
const field = currentFields.find((f) => f.id === fieldId);
|
||||
if (field) {
|
||||
flashField(field.element);
|
||||
sendResponse({ success: true });
|
||||
} else {
|
||||
sendResponse({ success: false, message: '未找到字段' });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'FORM_INJECT': {
|
||||
try {
|
||||
const injectData =
|
||||
(message.data as Array<{ entry: FormMapEntry; mockValue: string }>) || [];
|
||||
const results = injectData.map((item) => {
|
||||
const matchResult = FuzzyMatcher.findTargetElement(item.entry.fingerprint);
|
||||
if (matchResult.element) {
|
||||
const injectResult = SmartInjectionEngine.inject(
|
||||
matchResult.element,
|
||||
item.entry,
|
||||
item.mockValue,
|
||||
);
|
||||
if (injectResult.success) {
|
||||
FeedbackRenderer.renderSuccess(matchResult.element);
|
||||
} else {
|
||||
FeedbackRenderer.renderError(matchResult.element);
|
||||
}
|
||||
return { id: item.entry.id, success: injectResult.success };
|
||||
} else {
|
||||
return { id: item.entry.id, success: false };
|
||||
}
|
||||
});
|
||||
sendResponse({ success: true, results });
|
||||
} catch (error) {
|
||||
console.error('智能注入失败:', error);
|
||||
sendResponse({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '注入失败',
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
sendResponse({ success: false, message: '未知操作' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('执行操作失败:', error);
|
||||
sendResponse({ success: false, message: '执行操作失败' });
|
||||
}
|
||||
},
|
||||
);
|
||||
// 初始化消息处理器 (Scan, Fill, Clear, Highlight, Flash, Inject)
|
||||
initMessageHandler();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import {
|
||||
fillAllFields,
|
||||
clearAllFields,
|
||||
fillSelectedFields,
|
||||
scanFormFields,
|
||||
highlightField,
|
||||
unhighlightField,
|
||||
flashField,
|
||||
FillMode,
|
||||
type FormFieldInfo,
|
||||
} from '@/utils/dummyDataGenerator';
|
||||
import { MessageAction, onMessage } from '@/utils/messages';
|
||||
import {
|
||||
FuzzyMatcher,
|
||||
SmartInjectionEngine,
|
||||
FeedbackRenderer,
|
||||
} from '@/utils/formMapping/smartInjector';
|
||||
import { FormMapEntry } from '@/types/storage';
|
||||
|
||||
// 存储当前扫描到的字段列表,用于高亮联动
|
||||
let currentFields: FormFieldInfo[] = [];
|
||||
|
||||
/**
|
||||
* 初始化消息处理器
|
||||
*/
|
||||
export function initMessageHandler() {
|
||||
onMessage(MessageAction.SCAN_FORM_FIELDS, async () => {
|
||||
const result = scanFormFields();
|
||||
currentFields = result.fields;
|
||||
return {
|
||||
success: true,
|
||||
fields: result.fields.map((f) => ({
|
||||
id: f.id,
|
||||
fieldType: f.fieldType,
|
||||
label: f.label,
|
||||
placeholder: f.placeholder,
|
||||
name: f.name,
|
||||
value: f.value,
|
||||
isSelected: f.isSelected,
|
||||
generatedValue: f.generatedValue,
|
||||
})),
|
||||
totalCount: result.totalCount,
|
||||
validCount: result.validCount,
|
||||
hasModal: !!result.modalContainer,
|
||||
};
|
||||
});
|
||||
|
||||
onMessage(MessageAction.FILL_VALID_DATA, async (message) => {
|
||||
fillAllFields(FillMode.VALID, message.data.includeHidden || false);
|
||||
return { success: true, message: '已填充有效数据' };
|
||||
});
|
||||
|
||||
onMessage(MessageAction.FILL_INVALID_DATA, async (message) => {
|
||||
fillAllFields(FillMode.INVALID, message.data.includeHidden || false);
|
||||
return { success: true, message: '已填充无效数据' };
|
||||
});
|
||||
|
||||
onMessage(MessageAction.FILL_SELECTED_FIELDS, async (message) => {
|
||||
const { fields: incomingFields, mode } = message.data;
|
||||
const fieldsToFill = currentFields.map((field) => {
|
||||
const incomingField = incomingFields.find((f) => f.id === field.id);
|
||||
if (incomingField) {
|
||||
return {
|
||||
...field,
|
||||
fieldType: incomingField.fieldType,
|
||||
isSelected: incomingField.isSelected,
|
||||
useInvalidData: incomingField.useInvalidData,
|
||||
};
|
||||
}
|
||||
return field;
|
||||
});
|
||||
const count = fillSelectedFields(fieldsToFill, mode || FillMode.VALID);
|
||||
return { success: true, message: `已填充 ${count} 个字段` };
|
||||
});
|
||||
|
||||
onMessage(MessageAction.CLEAR_ALL_FIELDS, async () => {
|
||||
clearAllFields();
|
||||
return { success: true, message: '已清空所有字段' };
|
||||
});
|
||||
|
||||
onMessage(MessageAction.HIGHLIGHT_FIELD, async (message) => {
|
||||
const { fieldId } = message.data;
|
||||
const field = currentFields.find((f) => f.id === fieldId);
|
||||
if (field) {
|
||||
highlightField(field.element);
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, message: '未找到字段' };
|
||||
});
|
||||
|
||||
onMessage(MessageAction.UNHIGHLIGHT_FIELD, async (message) => {
|
||||
const { fieldId } = message.data;
|
||||
const field = currentFields.find((f) => f.id === fieldId);
|
||||
if (field) {
|
||||
unhighlightField(field.element);
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, message: '未找到字段' };
|
||||
});
|
||||
|
||||
onMessage(MessageAction.HIGHLIGHT_ALL_FIELDS, async (message) => {
|
||||
const { fieldIds } = message.data;
|
||||
fieldIds.forEach((id) => {
|
||||
const field = currentFields.find((f) => f.id === id);
|
||||
if (field) {
|
||||
highlightField(field.element);
|
||||
}
|
||||
});
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
onMessage(MessageAction.UNHIGHLIGHT_ALL_FIELDS, async () => {
|
||||
currentFields.forEach((field) => {
|
||||
unhighlightField(field.element);
|
||||
});
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
onMessage(MessageAction.FLASH_FIELD, async (message) => {
|
||||
const { fieldId } = message.data;
|
||||
const field = currentFields.find((f) => f.id === fieldId);
|
||||
if (field) {
|
||||
flashField(field.element);
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, message: '未找到字段' };
|
||||
});
|
||||
|
||||
onMessage(MessageAction.FORM_INJECT, async (message) => {
|
||||
try {
|
||||
const injectData =
|
||||
(message.data.data as Array<{ entry: FormMapEntry; mockValue: string }>) || [];
|
||||
const results = injectData.map((item) => {
|
||||
const matchResult = FuzzyMatcher.findTargetElement(item.entry.fingerprint);
|
||||
if (matchResult.element) {
|
||||
const injectResult = SmartInjectionEngine.inject(
|
||||
matchResult.element,
|
||||
item.entry,
|
||||
item.mockValue,
|
||||
);
|
||||
if (injectResult.success) {
|
||||
FeedbackRenderer.renderSuccess(matchResult.element);
|
||||
} else {
|
||||
FeedbackRenderer.renderError(matchResult.element);
|
||||
}
|
||||
return { id: item.entry.id, success: injectResult.success };
|
||||
} else {
|
||||
return { id: item.entry.id, success: false };
|
||||
}
|
||||
});
|
||||
return { success: true, results };
|
||||
} catch (error) {
|
||||
console.error('智能注入失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '注入失败',
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
.app {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell',
|
||||
'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
+106
-91
@@ -14,30 +14,35 @@ import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import type { PageType } from '@/types/storage';
|
||||
import { storageUtil } from '@/utils/chromeStorage';
|
||||
import { getRouteByKey, getDefaultPageOrder, getDefaultVisibleRoutes } from '@/config/routes';
|
||||
import GlobalSnackbar, { useSnackbar } from '@/components/GlobalSnackbar';
|
||||
import {
|
||||
getFeatureByKey,
|
||||
getDefaultPageOrder,
|
||||
getDefaultVisibleFeatureKeys,
|
||||
} from '@/config/features';
|
||||
import GlobalSnackbar, { useSnackbarState } from '@/components/GlobalSnackbar';
|
||||
import ErrorBoundary from '@/components/ErrorBoundary';
|
||||
|
||||
function App() {
|
||||
export default function App() {
|
||||
const [visiblePages, setVisiblePages] = useState<PageType[]>([]);
|
||||
const [pageOrder, setPageOrder] = useState<PageType[]>([]);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const { snackbarProps, showMessage } = useSnackbar();
|
||||
const { snackbarProps, showMessage } = useSnackbarState();
|
||||
|
||||
useEffect(() => {
|
||||
loadConfig();
|
||||
loadConfig().catch(console.error);
|
||||
}, []);
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const [savedVisible, savedOrder] = await Promise.all([
|
||||
storageUtil.get('app/visiblePages', getDefaultVisibleRoutes()),
|
||||
storageUtil.get('app/visiblePages', getDefaultVisibleFeatureKeys()),
|
||||
storageUtil.get('app/pageOrder', getDefaultPageOrder()),
|
||||
]);
|
||||
setVisiblePages(savedVisible ?? getDefaultVisibleRoutes());
|
||||
setVisiblePages(savedVisible ?? getDefaultVisibleFeatureKeys());
|
||||
setPageOrder(savedOrder && savedOrder.length > 0 ? savedOrder : getDefaultPageOrder());
|
||||
} catch (error) {
|
||||
console.error('Failed to load config:', error);
|
||||
setVisiblePages(getDefaultVisibleRoutes());
|
||||
setVisiblePages(getDefaultVisibleFeatureKeys());
|
||||
setPageOrder(getDefaultPageOrder());
|
||||
} finally {
|
||||
setIsLoaded(true);
|
||||
@@ -61,8 +66,8 @@ function App() {
|
||||
try {
|
||||
await storageUtil.set('app/visiblePages', newPages);
|
||||
setVisiblePages(newPages);
|
||||
const route = getRouteByKey(page);
|
||||
showToast(`已${isCurrentlyVisible ? '隐藏' : '显示'} ${route?.label || page}`, 'success');
|
||||
const feature = getFeatureByKey(page);
|
||||
showToast(`已${isCurrentlyVisible ? '隐藏' : '显示'} ${feature?.label || page}`, 'success');
|
||||
} catch (error) {
|
||||
console.error('Failed to save config:', error);
|
||||
showToast('保存失败', 'warning');
|
||||
@@ -88,8 +93,8 @@ function App() {
|
||||
|
||||
const handleRestoreDefaults = async () => {
|
||||
try {
|
||||
const { getDefaultVisibleRoutes } = await import('@/config/routes');
|
||||
const defaults = getDefaultVisibleRoutes();
|
||||
const { getDefaultVisibleFeatureKeys } = await import('@/config/features');
|
||||
const defaults = getDefaultVisibleFeatureKeys();
|
||||
const defaultOrder = getDefaultPageOrder();
|
||||
|
||||
await Promise.all([
|
||||
@@ -121,92 +126,102 @@ function App() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 4, maxWidth: 600, mx: 'auto', minHeight: '100vh', bgcolor: 'grey.50' }}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" sx={{ mb: 4 }}>
|
||||
<Button
|
||||
variant="text"
|
||||
size="small"
|
||||
onClick={handleRestoreDefaults}
|
||||
startIcon={<RefreshIcon sx={{ fontSize: 16 }} />}
|
||||
sx={{ color: 'text.secondary', fontWeight: 600 }}
|
||||
>
|
||||
恢复默认
|
||||
</Button>
|
||||
</Stack>
|
||||
<Box
|
||||
className="app"
|
||||
sx={{ p: 4, minHeight: '100vh', bgcolor: 'grey.50', display: 'block', overflowY: 'auto' }}
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<Box sx={{ maxWidth: 600, mx: 'auto' }}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="flex-start"
|
||||
sx={{ mb: 4 }}
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
size="small"
|
||||
onClick={handleRestoreDefaults}
|
||||
startIcon={<RefreshIcon sx={{ fontSize: 16 }} />}
|
||||
sx={{ color: 'text.secondary', fontWeight: 600 }}
|
||||
>
|
||||
恢复默认
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
borderRadius: 4,
|
||||
border: '1px solid',
|
||||
borderColor: 'grey.200',
|
||||
overflow: 'hidden',
|
||||
bgcolor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{pageOrder.map((key, index, array) => {
|
||||
const route = getRouteByKey(key);
|
||||
if (!route) return null;
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
borderRadius: 4,
|
||||
border: '1px solid',
|
||||
borderColor: 'grey.200',
|
||||
overflow: 'hidden',
|
||||
bgcolor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{pageOrder.map((key, index, array) => {
|
||||
const feature = getFeatureByKey(key);
|
||||
if (!feature) return null;
|
||||
|
||||
const isChecked = visiblePages.includes(key);
|
||||
const isDisabled = isChecked && visiblePages.length === 1;
|
||||
const isChecked = visiblePages.includes(key);
|
||||
const isDisabled = isChecked && visiblePages.length === 1;
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={key}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
p: 2.5,
|
||||
borderBottom: index === array.length - 1 ? 'none' : '1px solid',
|
||||
borderColor: 'grey.100',
|
||||
transition: 'all 0.2s',
|
||||
'&:hover': { bgcolor: 'grey.50' },
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="body1" sx={{ fontWeight: 700, color: 'text.primary' }}>
|
||||
{route.label}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{isChecked ? '已在 Dashboard 启用' : '已在 Dashboard 隐藏'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleMove(index, 'up')}
|
||||
disabled={index === 0}
|
||||
sx={{ color: 'text.secondary' }}
|
||||
return (
|
||||
<Box
|
||||
key={key}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
p: 2.5,
|
||||
borderBottom: index === array.length - 1 ? 'none' : '1px solid',
|
||||
borderColor: 'grey.100',
|
||||
transition: 'all 0.2s',
|
||||
'&:hover': { bgcolor: 'grey.50' },
|
||||
}}
|
||||
>
|
||||
<KeyboardArrowUpIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleMove(index, 'down')}
|
||||
disabled={index === array.length - 1}
|
||||
sx={{ color: 'text.secondary' }}
|
||||
>
|
||||
<KeyboardArrowDownIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={isChecked}
|
||||
onChange={() => handlePageToggle(key)}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
<Box>
|
||||
<Typography variant="body1" sx={{ fontWeight: 700, color: 'text.primary' }}>
|
||||
{feature.label}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{isChecked ? '已在 Dashboard 启用' : '已在 Dashboard 隐藏'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleMove(index, 'up')}
|
||||
disabled={index === 0}
|
||||
sx={{ color: 'text.secondary' }}
|
||||
>
|
||||
<KeyboardArrowUpIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleMove(index, 'down')}
|
||||
disabled={index === array.length - 1}
|
||||
sx={{ color: 'text.secondary' }}
|
||||
>
|
||||
<KeyboardArrowDownIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={isChecked}
|
||||
onChange={() => handlePageToggle(key)}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Paper>
|
||||
</ErrorBoundary>
|
||||
|
||||
<GlobalSnackbar {...snackbarProps} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import theme from '@/config/theme';
|
||||
import App from './App';
|
||||
import './App.css';
|
||||
import '@mui/material/styles';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
/* 极简滚动条变量 */
|
||||
:root {
|
||||
/* 统一圆角变量 */
|
||||
--radius: 8px;
|
||||
/* 统一背景颜色变量 */
|
||||
--bg-color: #fafafa;
|
||||
/* 统一文字颜色变量 */
|
||||
--text-color: #333333;
|
||||
/* 统一按钮颜色变量 */
|
||||
--btn-bg: #e0e0e0;
|
||||
/* 统一按钮文字颜色变量 */
|
||||
--btn-text: #333333;
|
||||
--border: 1px solid #e0e0e0;
|
||||
|
||||
/* 滚动条变量 */
|
||||
--sb-width: 6px;
|
||||
--sb-thumb-color: rgba(0, 0, 0, 0.1);
|
||||
--sb-thumb-hover: rgba(0, 0, 0, 0.2);
|
||||
--sb-track-color: transparent;
|
||||
}
|
||||
|
||||
/* 默认 Popup 尺寸 */
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 400px;
|
||||
height: 600px;
|
||||
overflow: hidden; /* 禁用外层滚动 */
|
||||
}
|
||||
|
||||
/* 独立窗口全屏自适应 */
|
||||
@media screen and (min-width: 401px), screen and (min-height: 601px) {
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
min-width: 400px;
|
||||
min-height: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 全局极简滚动条定制 */
|
||||
*::-webkit-scrollbar {
|
||||
width: var(--sb-width);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: var(--sb-track-color);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: var(--sb-thumb-color);
|
||||
border-radius: 10px;
|
||||
background-clip: content-box;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--sb-thumb-hover);
|
||||
}
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
overflow: hidden; /* 内部由 flex 子项控制滚动 */
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(30px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.page-transition-enter {
|
||||
animation: slideInRight 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) forwards;
|
||||
}
|
||||
|
||||
.page-transition-dashboard {
|
||||
animation: fadeIn 0.3s ease-out forwards;
|
||||
}
|
||||
+32
-13
@@ -1,24 +1,43 @@
|
||||
import './App.css';
|
||||
import RouterProvider from '@/providers/RouterProvider';
|
||||
import TopBar from '@/components/TopBar';
|
||||
import RouterContainer from '@/components/RouterContainer';
|
||||
import ErrorBoundary from '@/components/ErrorBoundary';
|
||||
import { globalStyles } from '@/config/pageTheme';
|
||||
import { SnackbarProvider } from '@/components/GlobalSnackbar';
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
function App() {
|
||||
export default function App() {
|
||||
// 打开Chrome扩展选项页面,需确保manifest中已配置options_page或options_ui
|
||||
const handleOpenOptions = () => {
|
||||
chrome.runtime.openOptionsPage();
|
||||
chrome.runtime.openOptionsPage().catch((r) => console.error(r));
|
||||
};
|
||||
|
||||
return (
|
||||
<RouterProvider defaultRoute="dashboard" syncRoute={true} syncKey="app/popupRoute">
|
||||
<div
|
||||
className="app"
|
||||
style={{ display: 'flex', flexDirection: 'column', height: '100vh', overflow: 'hidden' }}
|
||||
>
|
||||
<TopBar onOpenOptions={handleOpenOptions} />
|
||||
<RouterContainer />
|
||||
</div>
|
||||
<RouterProvider syncKey="app/popupRoute">
|
||||
<SnackbarProvider initialOptions={{ autoHideDuration: 1500000 }}>
|
||||
<Box
|
||||
className="app"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '400px',
|
||||
height: '600px',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: globalStyles.backgroundColor,
|
||||
'@media screen and (min-width: 401px), screen and (min-height: 601px)': {
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
minWidth: '400px',
|
||||
minHeight: '600px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TopBar onOpenOptions={handleOpenOptions} />
|
||||
<ErrorBoundary>
|
||||
<RouterContainer />
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
</SnackbarProvider>
|
||||
</RouterProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import theme from '@/config/theme';
|
||||
import App from './App.tsx';
|
||||
import './style.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -1,143 +1,49 @@
|
||||
import { Box, Typography, Container } from '@mui/material';
|
||||
import { Box } from '@mui/material';
|
||||
import { useRouter } from '@/providers/RouterProvider';
|
||||
import ToolCard from '@/components/ToolCard';
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import StorageIcon from '@mui/icons-material/Storage';
|
||||
import LanguageIcon from '@mui/icons-material/Language';
|
||||
import QrCodeIcon from '@mui/icons-material/QrCode';
|
||||
import DescriptionIcon from '@mui/icons-material/Description';
|
||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
import DashboardCard from '@/components/DashboardCard';
|
||||
import { getFeatureByKey } from '@/config/features';
|
||||
import type { PageType } from '@/types/storage';
|
||||
import { useEffect, useState } from 'react';
|
||||
import dayjs from '@/utils/dayjs';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { dashboardPageStyles } from '@/config/pageTheme';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { navigateTo, visiblePages, pageOrder } = useRouter();
|
||||
const [now, setNow] = useState(dayjs());
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setNow(dayjs()), 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
const isVisible = (key: string) => visiblePages.includes(key as PageType);
|
||||
|
||||
const renderCard = (key: PageType) => {
|
||||
switch (key) {
|
||||
case 'timestamp':
|
||||
return (
|
||||
<ToolCard
|
||||
key={key}
|
||||
title="时间戳"
|
||||
description="Unix 毫秒数转换与格式化"
|
||||
colorCode="#2196f3"
|
||||
icon={<AccessTimeIcon sx={{ fontSize: 20 }} />}
|
||||
onClick={() => navigateTo('timestamp')}
|
||||
cardBackgroundColor={dashboardPageStyles.cardBackgroundColor}
|
||||
snapshot={
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: 600,
|
||||
color: dashboardPageStyles.primaryColor,
|
||||
fontSize: '0.85rem',
|
||||
}}
|
||||
>
|
||||
{now.valueOf()}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '0.7rem', color: 'text.secondary' }}>
|
||||
{now.format('HH:mm:ss')}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
);
|
||||
case 'storageCleaner':
|
||||
return (
|
||||
<ToolCard
|
||||
key={key}
|
||||
title="存储管理"
|
||||
description="清理缓存、Cookies 及本地存储"
|
||||
colorCode="#ff9800"
|
||||
icon={<StorageIcon sx={{ fontSize: 20 }} />}
|
||||
onClick={() => navigateTo('storageCleaner')}
|
||||
cardBackgroundColor={dashboardPageStyles.cardBackgroundColor}
|
||||
/>
|
||||
);
|
||||
case 'openUrl':
|
||||
return (
|
||||
<ToolCard
|
||||
key={key}
|
||||
title="URL 工具"
|
||||
description="快速打开 URL 或复制链接"
|
||||
colorCode="#9c27b0"
|
||||
icon={<LanguageIcon sx={{ fontSize: 20 }} />}
|
||||
onClick={() => navigateTo('openUrl')}
|
||||
cardBackgroundColor={dashboardPageStyles.cardBackgroundColor}
|
||||
/>
|
||||
);
|
||||
case 'qrCode':
|
||||
return (
|
||||
<ToolCard
|
||||
key={key}
|
||||
title="二维码工具"
|
||||
description="URL 转二维码与二维码解析"
|
||||
colorCode="#4caf50"
|
||||
icon={<QrCodeIcon sx={{ fontSize: 20 }} />}
|
||||
onClick={() => navigateTo('qrCode')}
|
||||
cardBackgroundColor={dashboardPageStyles.cardBackgroundColor}
|
||||
/>
|
||||
);
|
||||
case 'formMapping':
|
||||
return (
|
||||
<ToolCard
|
||||
key={key}
|
||||
title="通用表单映射助手"
|
||||
description="智能识别表单指纹,自定义填充逻辑"
|
||||
colorCode="#3f51b5"
|
||||
icon={<AutoFixHighIcon sx={{ fontSize: 20 }} />}
|
||||
onClick={() => navigateTo('formMapping')}
|
||||
cardBackgroundColor={dashboardPageStyles.cardBackgroundColor}
|
||||
/>
|
||||
);
|
||||
case 'formRecognizer':
|
||||
return (
|
||||
<ToolCard
|
||||
key={key}
|
||||
title="表单识别"
|
||||
description="识别标签页中的表单内容"
|
||||
colorCode="#ff5722"
|
||||
icon={<DescriptionIcon sx={{ fontSize: 20 }} />}
|
||||
onClick={() => navigateTo('formRecognizer')}
|
||||
cardBackgroundColor={dashboardPageStyles.cardBackgroundColor}
|
||||
/>
|
||||
);
|
||||
case 'formFill':
|
||||
return (
|
||||
<ToolCard
|
||||
key={key}
|
||||
title="智能填充"
|
||||
description="根据表单指纹智能填充表单内容"
|
||||
colorCode="#2196f3"
|
||||
icon={<AutoFixHighIcon sx={{ fontSize: 20 }} />}
|
||||
onClick={() => navigateTo('formFill')}
|
||||
cardBackgroundColor={dashboardPageStyles.cardBackgroundColor}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const handleCardClick = useCallback(
|
||||
(page: PageType) => {
|
||||
navigateTo(page);
|
||||
},
|
||||
[navigateTo],
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ bgcolor: dashboardPageStyles.backgroundColor, minHeight: '100%', pb: 4 }}>
|
||||
<Container maxWidth="sm" sx={{ py: 3, px: 2 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{pageOrder.map((key) => (isVisible(key) ? renderCard(key) : null))}
|
||||
</Box>
|
||||
</Container>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, p: 2 }}>
|
||||
{pageOrder.map((key) => {
|
||||
if (!isVisible(key)) return null;
|
||||
|
||||
const feature = getFeatureByKey(key);
|
||||
if (!feature || !feature.icon || !feature.themeColor) return null;
|
||||
|
||||
// 适配 DashboardCard 组件,将 themeColor 映射到 colorCode
|
||||
const cardConfig = {
|
||||
title: feature.label,
|
||||
description: feature.description,
|
||||
colorCode: feature.themeColor,
|
||||
icon: feature.icon,
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
key={key}
|
||||
config={cardConfig}
|
||||
onClick={() => handleCardClick(key)}
|
||||
cardBackgroundColor={dashboardPageStyles.cardBackgroundColor}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,11 +9,7 @@ import {
|
||||
Switch,
|
||||
Divider,
|
||||
Paper,
|
||||
Button,
|
||||
Chip,
|
||||
Snackbar,
|
||||
Alert,
|
||||
alpha,
|
||||
} from '@mui/material';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
@@ -24,17 +20,18 @@ import { useEffect, useState, useCallback } from 'react';
|
||||
import { storageUtil } from '@/utils/chromeStorage';
|
||||
import { FormMapEntry } from '@/types/storage';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import { globalStyles, formMappingPageStyles } from '@/config/pageTheme.ts';
|
||||
import { formMappingPageStyles } from '@/config/pageTheme.ts';
|
||||
import { MockDataGenerator } from '@/utils/formMapping/smartInjector';
|
||||
import { Button } from '@/components/Button';
|
||||
import { FormInjectResult, MessageAction, sendMessage } from '@/utils/messages';
|
||||
import { useSnackbar as useGlobalSnackbar } from '@/components/SnackbarProvider';
|
||||
|
||||
export default function FormFillPage() {
|
||||
const [entries, setEntries] = useState<FormMapEntry[]>([]);
|
||||
const [previewData, setPreviewData] = useState<Map<string, string>>(new Map());
|
||||
const [injectResults, setInjectResults] = useState<Map<string, boolean>>(new Map());
|
||||
const [isInjecting, setIsInjecting] = useState(false);
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
const [showError, setShowError] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const { showMessage } = useGlobalSnackbar({ autoHideDuration: 3000 });
|
||||
|
||||
const generatePreviewData = useCallback((items: FormMapEntry[]) => {
|
||||
const preview = new Map<string, string>();
|
||||
@@ -53,10 +50,10 @@ export default function FormFillPage() {
|
||||
generatePreviewData(data || []);
|
||||
};
|
||||
|
||||
loadEntries();
|
||||
loadEntries().catch((r) => console.error(r));
|
||||
const listener = (changes: { [key: string]: chrome.storage.StorageChange }, area: string) => {
|
||||
if (area === 'local' && changes['active_form_map']) {
|
||||
loadEntries();
|
||||
loadEntries().catch((r) => console.error(r));
|
||||
}
|
||||
};
|
||||
chrome.storage.onChanged.addListener(listener);
|
||||
@@ -69,8 +66,7 @@ export default function FormFillPage() {
|
||||
|
||||
const injectAllFields = async () => {
|
||||
if (entries.length === 0) {
|
||||
setErrorMessage('没有可填充的字段');
|
||||
setShowError(true);
|
||||
showMessage('没有可填充的字段', { severity: 'error' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -95,27 +91,24 @@ export default function FormFillPage() {
|
||||
mockValue: previewData.get(entry.id) || '',
|
||||
}));
|
||||
|
||||
// 执行注入
|
||||
const result = await chrome.tabs.sendMessage(tabId, {
|
||||
type: 'FORM_INJECT',
|
||||
data: injectData,
|
||||
});
|
||||
// 执行注入 (使用 type-safe sendMessage)
|
||||
const result = await sendMessage(MessageAction.FORM_INJECT, { data: injectData }, tabId);
|
||||
|
||||
if (result && result.success) {
|
||||
result.results.forEach((r: { id: string; success: boolean }) => {
|
||||
result.results?.forEach((r: FormInjectResult) => {
|
||||
results.set(r.id, r.success);
|
||||
});
|
||||
setInjectResults(results);
|
||||
setShowSuccess(true);
|
||||
showMessage('填充成功!', { severity: 'success' });
|
||||
} else {
|
||||
throw new Error(result?.error || '注入失败');
|
||||
throw new Error(result?.error || result?.message || '注入失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('注入失败:', error);
|
||||
setErrorMessage(
|
||||
error instanceof Error ? error.message : '注入失败,请确保已在网页中打开表单',
|
||||
);
|
||||
setShowError(true);
|
||||
showMessage(error instanceof Error ? error.message : '注入失败,请确保已在网页中打开表单');
|
||||
showMessage(error instanceof Error ? error.message : '注入失败,请确保已在网页中打开表单', {
|
||||
severity: 'error',
|
||||
});
|
||||
} finally {
|
||||
setIsInjecting(false);
|
||||
}
|
||||
@@ -141,8 +134,8 @@ export default function FormFillPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ bgcolor: globalStyles.backgroundColor, minHeight: '100%', pb: 3 }}>
|
||||
<Container sx={{ py: 2, bgcolor: globalStyles.backgroundColor }}>
|
||||
<Box>
|
||||
<Container sx={{ py: 2 }}>
|
||||
<PageHeader
|
||||
title="智能表单填充"
|
||||
subtitle="基于指纹识别的精准数据注入"
|
||||
@@ -177,7 +170,6 @@ export default function FormFillPage() {
|
||||
onClick={refreshPreview}
|
||||
size="small"
|
||||
startIcon={<RefreshIcon />}
|
||||
sx={{ borderRadius: 3 }}
|
||||
>
|
||||
刷新预览
|
||||
</Button>
|
||||
@@ -188,9 +180,7 @@ export default function FormFillPage() {
|
||||
startIcon={<PlayArrowIcon />}
|
||||
disabled={isInjecting || entries.length === 0}
|
||||
sx={{
|
||||
borderRadius: 3,
|
||||
bgcolor: formMappingPageStyles.secondaryColor || '#9c27b0',
|
||||
boxShadow: `0 4px 12px ${alpha(formMappingPageStyles.secondaryColor || '#9c27b0', 0.2)}`,
|
||||
}}
|
||||
>
|
||||
{isInjecting ? '注入中...' : '开始填充'}
|
||||
@@ -261,6 +251,14 @@ export default function FormFillPage() {
|
||||
sx={{ mr: 2 }}
|
||||
/>
|
||||
<ListItemText
|
||||
slotProps={{
|
||||
primary: {
|
||||
component: 'div',
|
||||
},
|
||||
secondary: {
|
||||
component: 'div',
|
||||
},
|
||||
}}
|
||||
primary={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<span style={{ fontWeight: 500 }}>{entry.label_display}</span>
|
||||
@@ -361,24 +359,6 @@ export default function FormFillPage() {
|
||||
)}
|
||||
</Container>
|
||||
</Container>
|
||||
|
||||
{/* 提示消息 */}
|
||||
<Snackbar
|
||||
open={showSuccess}
|
||||
autoHideDuration={3000}
|
||||
onClose={() => setShowSuccess(false)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert severity="success">填充完成!</Alert>
|
||||
</Snackbar>
|
||||
<Snackbar
|
||||
open={showError}
|
||||
autoHideDuration={4000}
|
||||
onClose={() => setShowError(false)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert severity="error">{errorMessage}</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,9 +9,6 @@ import {
|
||||
Switch,
|
||||
Divider,
|
||||
Paper,
|
||||
alpha,
|
||||
Snackbar,
|
||||
Alert,
|
||||
} from '@mui/material';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
@@ -22,13 +19,12 @@ import { storageUtil } from '@/utils/chromeStorage';
|
||||
import { FormMapEntry } from '@/types/storage';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import Button from '@/components/Button';
|
||||
import { globalStyles, formMappingPageStyles } from '@/config/pageTheme.ts';
|
||||
import { useSnackbar as useGlobalSnackbar } from '@/components/SnackbarProvider';
|
||||
|
||||
export default function FormMappingPage() {
|
||||
const [entries, setEntries] = useState<FormMapEntry[]>([]);
|
||||
const [isPicking, setIsPicking] = useState(false);
|
||||
const [exportError, setExportError] = useState<string | null>(null);
|
||||
const [showExportSuccess, setShowExportSuccess] = useState(false);
|
||||
const { showMessage } = useGlobalSnackbar({ autoHideDuration: 3000 });
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
@@ -38,7 +34,7 @@ export default function FormMappingPage() {
|
||||
setIsPicking(picking || false);
|
||||
};
|
||||
|
||||
loadData();
|
||||
loadData().catch((r) => console.error(r));
|
||||
|
||||
const listener = (changes: { [key: string]: chrome.storage.StorageChange }, area: string) => {
|
||||
if (area === 'local') {
|
||||
@@ -79,7 +75,7 @@ export default function FormMappingPage() {
|
||||
const exportConfig = () => {
|
||||
try {
|
||||
if (entries.length === 0) {
|
||||
setExportError('没有可导出的配置数据');
|
||||
showMessage('没有可导出的配置数据', { severity: 'warning' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -99,17 +95,19 @@ export default function FormMappingPage() {
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
setShowExportSuccess(true);
|
||||
showMessage('配置导出成功!', { severity: 'success' });
|
||||
} catch (error) {
|
||||
console.error('导出配置失败:', error);
|
||||
setExportError(error instanceof Error ? error.message : '导出失败,请重试');
|
||||
showMessage(error instanceof Error ? error.message : '导出失败,请重试', {
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ bgcolor: globalStyles.backgroundColor, minHeight: '100%', pb: 3 }}>
|
||||
<Container sx={{ py: 2, bgcolor: globalStyles.backgroundColor }}>
|
||||
<Box>
|
||||
<Container sx={{ py: 2 }}>
|
||||
<PageHeader
|
||||
title="通用表单映射助手"
|
||||
subtitle="智能识别表单指纹,自定义填充逻辑"
|
||||
@@ -142,17 +140,6 @@ export default function FormMappingPage() {
|
||||
onClick={togglePicking}
|
||||
size="small"
|
||||
startIcon={<AddCircleOutlineIcon />}
|
||||
sx={{
|
||||
borderRadius: 3,
|
||||
px: 2,
|
||||
fontWeight: 800,
|
||||
...(isPicking
|
||||
? {
|
||||
bgcolor: 'secondary.main',
|
||||
boxShadow: `0 4px 12px ${alpha(formMappingPageStyles.secondaryColor || '#9c27b0', 0.2)}`,
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{isPicking ? '正在拾取...' : '开始拾取'}
|
||||
</Button>
|
||||
@@ -174,12 +161,7 @@ export default function FormMappingPage() {
|
||||
<Typography variant="subtitle2" fontWeight={800} color="text.secondary">
|
||||
已拾取字段 ({entries.length})
|
||||
</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={clearAll}
|
||||
sx={{ fontWeight: 700, fontSize: '0.75rem' }}
|
||||
>
|
||||
<Button size="small" color="error" onClick={clearAll}>
|
||||
清空全部
|
||||
</Button>
|
||||
</Box>
|
||||
@@ -273,7 +255,6 @@ export default function FormMappingPage() {
|
||||
variant="outlined"
|
||||
onClick={exportConfig}
|
||||
startIcon={<FileDownloadIcon />}
|
||||
sx={{ fontWeight: 700, fontSize: '0.75rem', borderRadius: 3 }}
|
||||
>
|
||||
导出配置
|
||||
</Button>
|
||||
@@ -299,26 +280,6 @@ export default function FormMappingPage() {
|
||||
</Container>
|
||||
</Container>
|
||||
</Box>
|
||||
<Snackbar
|
||||
open={!!exportError}
|
||||
autoHideDuration={4000}
|
||||
onClose={() => setExportError(null)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert severity="error" onClose={() => setExportError(null)}>
|
||||
{exportError}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
<Snackbar
|
||||
open={showExportSuccess}
|
||||
autoHideDuration={3000}
|
||||
onClose={() => setShowExportSuccess(false)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert severity="success" onClose={() => setShowExportSuccess(false)}>
|
||||
配置导出成功!
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,339 +1,39 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Button,
|
||||
CircularProgress,
|
||||
FormControlLabel,
|
||||
Switch,
|
||||
alpha,
|
||||
} from '@mui/material';
|
||||
import { Box, Container, CircularProgress, FormControlLabel, Switch } from '@mui/material';
|
||||
import Button from '@/components/Button';
|
||||
import InputIcon from '@mui/icons-material/Input';
|
||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
||||
import GlobalSnackbar, { useSnackbar } from '@/components/GlobalSnackbar';
|
||||
import { dashboardPageStyles, formRecognizerPageStyles } from '@/config/pageTheme';
|
||||
import { MessageAction, sendMessageToContent, injectContentScript } from '@/utils/messages';
|
||||
import { FillMode } from '@/utils/dummyDataGenerator';
|
||||
import { formRecognizerPageStyles } from '@/config/pageTheme';
|
||||
import FieldList from '@/components/FieldList';
|
||||
import { useStorageState } from '@/utils/useStorageState';
|
||||
import { FieldTypePreferences } from '@/types/storage';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import { useFormRecognizer } from './hooks/useFormRecognizer';
|
||||
|
||||
// 字段数据接口
|
||||
interface FieldData {
|
||||
id: string;
|
||||
fieldType: string;
|
||||
label: string | null;
|
||||
placeholder: string;
|
||||
name: string;
|
||||
value: string;
|
||||
isSelected: boolean;
|
||||
generatedValue: string;
|
||||
useInvalidData?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_FIELD_TYPE_PREFERENCES: FieldTypePreferences = {};
|
||||
|
||||
const FormRecognizerPage = () => {
|
||||
const { snackbarProps, showMessage } = useSnackbar({ autoHideDuration: 1500 });
|
||||
const [fillLoading, setFillLoading] = useState(false);
|
||||
const [clearLoading, setClearLoading] = useState(false);
|
||||
const [includeHidden, setIncludeHidden] = useState(false);
|
||||
const isProcessingRef = useRef(false);
|
||||
const [fields, setFields] = useState<FieldData[]>([]);
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [showFields, setShowFields] = useState(false);
|
||||
const [hoveredFieldId, setHoveredFieldId] = useState<string | null>(null);
|
||||
const [currentDomain, setCurrentDomain] = useState<string>('');
|
||||
const [sidePanelOpen, setSidePanelOpen] = useState(false);
|
||||
|
||||
const [fieldTypePreferences, setFieldTypePreferences] = useStorageState(
|
||||
'formRecognizer/fieldTypePreferences',
|
||||
DEFAULT_FIELD_TYPE_PREFERENCES,
|
||||
);
|
||||
|
||||
// 获取当前域名
|
||||
useEffect(() => {
|
||||
const getActiveTab = async () => {
|
||||
try {
|
||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
if (tab?.url) {
|
||||
const url = new URL(tab.url);
|
||||
setCurrentDomain(url.hostname);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取标签页信息失败:', error);
|
||||
}
|
||||
};
|
||||
getActiveTab();
|
||||
}, []);
|
||||
|
||||
// 检测侧边栏状态
|
||||
useEffect(() => {
|
||||
console.log('开始检测侧边栏状态');
|
||||
const checkSidePanelState = async () => {
|
||||
try {
|
||||
// 检查 chrome.runtime.getContexts 是否可用
|
||||
if (typeof chrome.runtime.getContexts === 'function') {
|
||||
const contexts = await chrome.runtime.getContexts({
|
||||
contextTypes: ['SIDE_PANEL'],
|
||||
});
|
||||
console.log('侧边栏上下文:', contexts);
|
||||
setSidePanelOpen(contexts.length > 0);
|
||||
} else {
|
||||
console.log('chrome.runtime.getContexts 不可用');
|
||||
setSidePanelOpen(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检测侧边栏状态失败:', error);
|
||||
setSidePanelOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始检查
|
||||
checkSidePanelState();
|
||||
|
||||
// 定期检查侧边栏状态(每 500ms)
|
||||
const interval = setInterval(checkSidePanelState, 500);
|
||||
|
||||
console.log('侧边栏状态检测已启动');
|
||||
return () => {
|
||||
console.log('清理侧边栏状态检测');
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 生成字段标识符
|
||||
const getFieldIdentifier = (field: Pick<FieldData, 'label' | 'name' | 'placeholder'>): string => {
|
||||
return field.label || field.name || field.placeholder || 'unknown';
|
||||
};
|
||||
|
||||
// 应用保存的类型偏好
|
||||
const applySavedPreferences = (fields: FieldData[]): FieldData[] => {
|
||||
if (!currentDomain || !(fieldTypePreferences as FieldTypePreferences)[currentDomain]) {
|
||||
return fields;
|
||||
}
|
||||
const prefs = (fieldTypePreferences as FieldTypePreferences)[currentDomain];
|
||||
return fields.map((field) => {
|
||||
const identifier = getFieldIdentifier(field);
|
||||
if (prefs && prefs[identifier]) {
|
||||
return { ...field, fieldType: prefs[identifier] };
|
||||
}
|
||||
return field;
|
||||
});
|
||||
};
|
||||
|
||||
// 保存类型偏好
|
||||
const saveTypePreference = (field: FieldData, newType: string) => {
|
||||
if (!currentDomain) return;
|
||||
const identifier = getFieldIdentifier(field);
|
||||
setFieldTypePreferences((prev) => {
|
||||
const prevPrefs = prev as FieldTypePreferences;
|
||||
const currentDomainPrefs = prevPrefs[currentDomain] || {};
|
||||
return {
|
||||
...prevPrefs,
|
||||
[currentDomain]: {
|
||||
...currentDomainPrefs,
|
||||
[identifier]: newType,
|
||||
},
|
||||
} as FieldTypePreferences;
|
||||
});
|
||||
};
|
||||
|
||||
// 扫描表单字段
|
||||
const handleScanFields = async () => {
|
||||
setScanning(true);
|
||||
try {
|
||||
let response = await sendMessageToContent(MessageAction.SCAN_FORM_FIELDS);
|
||||
|
||||
if (!response.success && response.message && response.message.includes('无法连接')) {
|
||||
showMessage('正在注入内容脚本...', { severity: 'info' });
|
||||
const injected = await injectContentScript();
|
||||
if (injected) {
|
||||
response = await sendMessageToContent(MessageAction.SCAN_FORM_FIELDS);
|
||||
}
|
||||
}
|
||||
|
||||
if (response.success && response.fields) {
|
||||
const fieldsWithPreferences = applySavedPreferences(response.fields as FieldData[]);
|
||||
setFields(fieldsWithPreferences);
|
||||
setShowFields(true);
|
||||
showMessage(`扫描完成,发现 ${response.totalCount} 个可填充字段`, { severity: 'success' });
|
||||
} else {
|
||||
showMessage(response.message || '扫描失败', { severity: 'error' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('扫描失败:', error);
|
||||
showMessage('扫描失败,请确保页面已加载', { severity: 'error' });
|
||||
} finally {
|
||||
setScanning(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新字段类型
|
||||
const handleFieldTypeChange = (fieldId: string, newType: string) => {
|
||||
setFields((prev) =>
|
||||
prev.map((field) => {
|
||||
if (field.id === fieldId) {
|
||||
const updatedField = {
|
||||
...field,
|
||||
fieldType: newType,
|
||||
// 清空旧的 generatedValue,保持界面一致性
|
||||
generatedValue: '',
|
||||
};
|
||||
saveTypePreference(field, newType);
|
||||
return updatedField;
|
||||
}
|
||||
return field;
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
// 切换单个字段的选中状态
|
||||
const handleToggleFieldSelection = (fieldId: string) => {
|
||||
setFields((prev) =>
|
||||
prev.map((field) =>
|
||||
field.id === fieldId ? { ...field, isSelected: !field.isSelected } : field,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
// 全选/取消全选
|
||||
const handleToggleAllFields = () => {
|
||||
setFields((prev) => {
|
||||
const allSelected = prev.every((f) => f.isSelected);
|
||||
return prev.map((f) => ({ ...f, isSelected: !allSelected }));
|
||||
});
|
||||
};
|
||||
|
||||
// 定位字段(闪烁)
|
||||
const handleLocateField = async (fieldId: string) => {
|
||||
try {
|
||||
const response = await sendMessageToContent(MessageAction.FLASH_FIELD, { fieldId });
|
||||
if (!response.success) {
|
||||
showMessage(response.message || '定位字段失败', { severity: 'error' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('定位字段失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 悬停高亮
|
||||
const handleHoverField = async (fieldId: string | null) => {
|
||||
setHoveredFieldId(fieldId);
|
||||
try {
|
||||
if (fieldId) {
|
||||
await sendMessageToContent(MessageAction.HIGHLIGHT_FIELD, { fieldId });
|
||||
} else {
|
||||
await sendMessageToContent(MessageAction.UNHIGHLIGHT_ALL_FIELDS);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('高亮字段失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 填充选中字段
|
||||
const handleFillSelectedFields = async () => {
|
||||
if (isProcessingRef.current) {
|
||||
showMessage('操作进行中,请稍候...', { severity: 'warning' });
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedCount = fields.filter((f) => f.isSelected).length;
|
||||
if (selectedCount === 0) {
|
||||
showMessage('请先选择要填充的字段', { severity: 'warning' });
|
||||
return;
|
||||
}
|
||||
|
||||
setFillLoading(true);
|
||||
isProcessingRef.current = true;
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const messageFields = fields as any;
|
||||
let response = await sendMessageToContent(MessageAction.FILL_SELECTED_FIELDS, {
|
||||
fields: messageFields,
|
||||
mode: FillMode.VALID,
|
||||
includeHidden,
|
||||
});
|
||||
|
||||
if (!response.success && response.message && response.message.includes('无法连接')) {
|
||||
showMessage('正在注入内容脚本...', { severity: 'info' });
|
||||
const injected = await injectContentScript();
|
||||
if (injected) {
|
||||
response = await sendMessageToContent(MessageAction.FILL_SELECTED_FIELDS, {
|
||||
fields: messageFields,
|
||||
mode: FillMode.VALID,
|
||||
includeHidden,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
showMessage(response.message || '填充成功', { severity: 'success' });
|
||||
} else {
|
||||
showMessage(response.message || '填充失败', { severity: 'error' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('填充失败:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
showMessage(`填充失败:${errorMessage},请确保当前页面已加载完成`, { severity: 'error' });
|
||||
} finally {
|
||||
setFillLoading(false);
|
||||
isProcessingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 清空所有字段
|
||||
const handleClearAllFields = async () => {
|
||||
if (isProcessingRef.current) {
|
||||
showMessage('操作进行中,请稍候...', { severity: 'warning' });
|
||||
return;
|
||||
}
|
||||
|
||||
setClearLoading(true);
|
||||
isProcessingRef.current = true;
|
||||
try {
|
||||
let response = await sendMessageToContent(MessageAction.CLEAR_ALL_FIELDS);
|
||||
|
||||
if (!response.success && response.message && response.message.includes('无法连接')) {
|
||||
showMessage('正在注入内容脚本...', { severity: 'info' });
|
||||
const injected = await injectContentScript();
|
||||
if (injected) {
|
||||
response = await sendMessageToContent(MessageAction.CLEAR_ALL_FIELDS);
|
||||
}
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
showMessage(response.message || '清空成功', { severity: 'success' });
|
||||
} else {
|
||||
showMessage(response.message || '清空失败', { severity: 'error' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('清空失败:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
showMessage(`清空失败:${errorMessage},请确保当前页面已加载完成`, { severity: 'error' });
|
||||
} finally {
|
||||
setClearLoading(false);
|
||||
isProcessingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 打开侧边栏
|
||||
const handleOpenSidePanel = async () => {
|
||||
try {
|
||||
await chrome.sidePanel.open({ windowId: chrome.windows.WINDOW_ID_CURRENT });
|
||||
showMessage('侧边栏已打开', { severity: 'success' });
|
||||
} catch (error) {
|
||||
console.error('打开侧边栏失败:', error);
|
||||
showMessage('打开侧边栏失败', { severity: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
const selectedCount = fields.filter((f) => f.isSelected).length;
|
||||
export default function FormRecognizerPage() {
|
||||
const {
|
||||
fillLoading,
|
||||
clearLoading,
|
||||
includeHidden,
|
||||
setIncludeHidden,
|
||||
fields,
|
||||
scanning,
|
||||
showFields,
|
||||
setShowFields,
|
||||
hoveredFieldId,
|
||||
sidePanelOpen,
|
||||
handleScanFields,
|
||||
handleFieldTypeChange,
|
||||
handleToggleFieldSelection,
|
||||
handleToggleAllFields,
|
||||
handleLocateField,
|
||||
handleHoverField,
|
||||
handleFillSelectedFields,
|
||||
handleClearAllFields,
|
||||
handleOpenSidePanel,
|
||||
selectedCount,
|
||||
} = useFormRecognizer();
|
||||
|
||||
return (
|
||||
<Box sx={{ bgcolor: dashboardPageStyles.backgroundColor, minHeight: '100%', pb: 4 }}>
|
||||
<Container maxWidth="sm" sx={{ py: 3, px: 2, bgcolor: '#f5f5f5' }}>
|
||||
<Box>
|
||||
<Container maxWidth="sm" sx={{ py: 3, px: 2 }}>
|
||||
{/* Header */}
|
||||
<PageHeader
|
||||
title="表单测试数据填充器"
|
||||
@@ -366,16 +66,6 @@ const FormRecognizerPage = () => {
|
||||
onClick={handleScanFields}
|
||||
disabled={scanning}
|
||||
fullWidth
|
||||
sx={{
|
||||
...formRecognizerPageStyles.buttonStyle,
|
||||
mb: 2,
|
||||
borderColor: formRecognizerPageStyles.validColor,
|
||||
color: formRecognizerPageStyles.validColor,
|
||||
'&:hover': {
|
||||
borderColor: formRecognizerPageStyles.validDark,
|
||||
bgcolor: 'rgba(76, 175, 80, 0.05)',
|
||||
},
|
||||
}}
|
||||
startIcon={scanning ? <CircularProgress size={16} color="inherit" /> : <InputIcon />}
|
||||
>
|
||||
{scanning ? '扫描中...' : '扫描表单字段'}
|
||||
@@ -403,27 +93,6 @@ const FormRecognizerPage = () => {
|
||||
onClick={handleFillSelectedFields}
|
||||
disabled={fillLoading || selectedCount === 0}
|
||||
fullWidth
|
||||
sx={{
|
||||
py: 1.6,
|
||||
borderRadius: 4,
|
||||
bgcolor: formRecognizerPageStyles.validColor,
|
||||
fontWeight: 700,
|
||||
fontSize: '1rem',
|
||||
textTransform: 'none',
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
'&:hover': {
|
||||
bgcolor: formRecognizerPageStyles.validDark,
|
||||
boxShadow: `0 8px 24px ${alpha(formRecognizerPageStyles.validColor, 0.3)}`,
|
||||
transform: 'translateY(-1px)',
|
||||
},
|
||||
'&:disabled': {
|
||||
bgcolor: '#e0e0e0',
|
||||
color: '#9e9e9e',
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'translateY(0)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
填充选中字段
|
||||
</Button>
|
||||
@@ -435,28 +104,6 @@ const FormRecognizerPage = () => {
|
||||
onClick={handleClearAllFields}
|
||||
disabled={clearLoading}
|
||||
fullWidth
|
||||
sx={{
|
||||
py: 1.6,
|
||||
borderRadius: 4,
|
||||
borderColor: formRecognizerPageStyles.clearColor,
|
||||
color: formRecognizerPageStyles.clearColor,
|
||||
fontWeight: 600,
|
||||
fontSize: '1rem',
|
||||
textTransform: 'none',
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
'&:hover': {
|
||||
borderColor: formRecognizerPageStyles.clearDark,
|
||||
bgcolor: formRecognizerPageStyles.clearBg,
|
||||
transform: 'translateY(-1px)',
|
||||
},
|
||||
'&:disabled': {
|
||||
borderColor: '#e0e0e0',
|
||||
color: '#9e9e9e',
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'translateY(0)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
清空所有字段
|
||||
</Button>
|
||||
@@ -474,11 +121,7 @@ const FormRecognizerPage = () => {
|
||||
label="包含隐藏字段"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<GlobalSnackbar {...snackbarProps} />
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormRecognizerPage;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Box, Typography, Container } from '@mui/material';
|
||||
import LanguageIcon from '@mui/icons-material/Language';
|
||||
import GlobalSnackbar, { useSnackbar } from '@/components/GlobalSnackbar';
|
||||
import { useSnackbar as useGlobalSnackbar } from '@/components/SnackbarProvider';
|
||||
import UrlEntryForm from '@/components/UrlEntryForm';
|
||||
import UrlEntryList from '@/components/UrlEntryList';
|
||||
import { useUrlPreferences } from '@/utils/useUrlPreferences';
|
||||
import type { OpenUrlEntry } from '@/types/storage';
|
||||
import { dashboardPageStyles, openUrlPageStyles } from '@/config/pageTheme';
|
||||
import { openUrlPageStyles } from '@/config/pageTheme';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
|
||||
export default function OpenUrlPage() {
|
||||
const { entries, setEntries, isLoaded } = useUrlPreferences();
|
||||
const { snackbarProps, showMessage } = useSnackbar();
|
||||
const { showMessage } = useGlobalSnackbar();
|
||||
|
||||
const handleAddEntry = (entry: OpenUrlEntry) => {
|
||||
setEntries([...entries, entry]);
|
||||
@@ -25,7 +25,7 @@ export default function OpenUrlPage() {
|
||||
|
||||
if (!isLoaded) {
|
||||
return (
|
||||
<Box sx={{ bgcolor: dashboardPageStyles.backgroundColor, minHeight: '100%', pb: 3 }}>
|
||||
<Box sx={{ minHeight: '100%', pb: 3 }}>
|
||||
<Container sx={{ py: 2 }}>
|
||||
<Typography>加载中...</Typography>
|
||||
</Container>
|
||||
@@ -34,7 +34,7 @@ export default function OpenUrlPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ bgcolor: dashboardPageStyles.backgroundColor, minHeight: '100%', pb: 3 }}>
|
||||
<Box>
|
||||
<Container sx={{ py: 2 }}>
|
||||
{/* Header */}
|
||||
<PageHeader
|
||||
@@ -64,7 +64,6 @@ export default function OpenUrlPage() {
|
||||
/>
|
||||
</Box>
|
||||
</Container>
|
||||
<GlobalSnackbar {...snackbarProps} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Box, Typography, CircularProgress, Alert } from '@mui/material';
|
||||
import { storageUtil } from '@/utils/chromeStorage';
|
||||
import { dashboardPageStyles } from '@/config/pageTheme';
|
||||
|
||||
// 只允许 HTTP/HTTPS 协议,阻止危险协议
|
||||
const ALLOWED_PROTOCOLS = ['http:', 'https:'];
|
||||
@@ -112,7 +111,6 @@ export default function OpenUrlViewerPage() {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
bgcolor: dashboardPageStyles.backgroundColor,
|
||||
}}
|
||||
>
|
||||
{/* 加载状态指示器 */}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Box, Stack, Container, CircularProgress } from '@mui/material';
|
||||
import QrCodeIcon from '@mui/icons-material/QrCode';
|
||||
import GlobalSnackbar, { useSnackbar } from '@/components/GlobalSnackbar';
|
||||
import { useSnackbar as useGlobalSnackbar } from '@/components/SnackbarProvider';
|
||||
import UrlToQrCodeSection from '@/components/UrlToQrCodeSection';
|
||||
import QrCodeToUrlSection from '@/components/QrCodeToUrlSection';
|
||||
import { useStorageState } from '@/utils/useStorageState';
|
||||
import { dashboardPageStyles, qrCodePageStyles } from '@/config/pageTheme';
|
||||
import { qrCodePageStyles } from '@/config/pageTheme';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
|
||||
const QrCodePage = () => {
|
||||
const { snackbarProps, showMessage } = useSnackbar({ autoHideDuration: 1500 });
|
||||
export default function QrCodePage() {
|
||||
const { showMessage } = useGlobalSnackbar();
|
||||
|
||||
// 使用自定义钩子管理展开状态
|
||||
const [urlExpanded, setUrlExpanded, urlInitialized] = useStorageState('qrCode/urlExpanded', true);
|
||||
@@ -33,8 +33,8 @@ const QrCodePage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ minHeight: '100%', pb: 3 }}>
|
||||
<Container sx={{ py: 2, maxWidth: 400, bgcolor: dashboardPageStyles.backgroundColor }}>
|
||||
<Box>
|
||||
<Container sx={{ py: 2, maxWidth: 400 }}>
|
||||
<PageHeader
|
||||
title="二维码工具"
|
||||
subtitle="生成和解析二维码"
|
||||
@@ -56,10 +56,7 @@ const QrCodePage = () => {
|
||||
showMessage={showMessage}
|
||||
/>
|
||||
</Stack>
|
||||
<GlobalSnackbar {...snackbarProps} />
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default QrCodePage;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, Container, CircularProgress } from '@mui/material';
|
||||
import Button from '@/components/Button';
|
||||
import GlobalSnackbar, { useSnackbar } from '@/components/GlobalSnackbar';
|
||||
import { useSnackbar as useGlobalSnackbar } from '@/components/SnackbarProvider';
|
||||
import StorageCleanerConfirm from '@/components/StorageCleanerConfirm';
|
||||
import { storageCleanerPageStyles } from '@/config/pageTheme';
|
||||
import { useStorageCleaner } from './useStorageCleaner';
|
||||
@@ -11,7 +11,7 @@ import ErrorDisplay from './components/ErrorDisplay';
|
||||
import CleaningResult from './components/CleaningResult';
|
||||
|
||||
export default function StorageCleanerPage() {
|
||||
const { snackbarProps, showMessage } = useSnackbar();
|
||||
const { showMessage } = useGlobalSnackbar();
|
||||
const {
|
||||
domain,
|
||||
error,
|
||||
@@ -45,7 +45,7 @@ export default function StorageCleanerPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ bgcolor: '#f5f5f5', minHeight: '100%', pb: 2 }}>
|
||||
<Box>
|
||||
<Container sx={{ py: 2 }}>
|
||||
<DomainHeader domain={domain} totalSize={totalSize} />
|
||||
|
||||
@@ -64,20 +64,9 @@ export default function StorageCleanerPage() {
|
||||
variant="contained"
|
||||
onClick={() => setShowConfirm(true)}
|
||||
sx={{
|
||||
py: 1.3,
|
||||
borderRadius: 4,
|
||||
bgcolor: storageCleanerPageStyles.warningColor,
|
||||
fontWeight: 800,
|
||||
fontSize: '0.85rem',
|
||||
boxShadow: '0 4px 12px rgba(255, 152, 0, 0.25)',
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
'&:hover': {
|
||||
bgcolor: storageCleanerPageStyles.warningDark,
|
||||
boxShadow: '0 8px 20px rgba(255, 152, 0, 0.35)',
|
||||
transform: 'translateY(-2px)',
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'translateY(0)',
|
||||
},
|
||||
}}
|
||||
disabled={loading}
|
||||
@@ -95,7 +84,6 @@ export default function StorageCleanerPage() {
|
||||
onConfirm={handleClean}
|
||||
options={options}
|
||||
/>
|
||||
<GlobalSnackbar {...snackbarProps} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { TextField, Select, MenuItem, Stack, Box, Container, alpha } from '@mui/material';
|
||||
import GlobalSnackbar, { useSnackbar } from '@/components/GlobalSnackbar';
|
||||
import { TextField, Select, MenuItem, Stack, Box, Container } from '@mui/material';
|
||||
import { useSnackbar } from '@/components/SnackbarProvider';
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import Button from '@/components/Button';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import { ZONES, globalStyles, timestampPageStyles } from '@/config/pageTheme';
|
||||
import { ZONES, timestampPageStyles } from '@/config/pageTheme';
|
||||
import LiveClock from './components/LiveClock';
|
||||
import ResultView from './components/ResultView';
|
||||
import { useTimestampConverter } from './hooks/useTimestampConverter';
|
||||
|
||||
export default function TimestampPage() {
|
||||
const { snackbarProps, showMessage } = useSnackbar({ autoHideDuration: 1500 });
|
||||
const { showMessage } = useSnackbar();
|
||||
const {
|
||||
mode,
|
||||
tsInput,
|
||||
@@ -28,14 +28,13 @@ export default function TimestampPage() {
|
||||
} = useTimestampConverter();
|
||||
|
||||
return (
|
||||
<Box sx={{ bgcolor: globalStyles.backgroundColor, minHeight: '100%', pb: 3 }}>
|
||||
<Container sx={{ py: 2, bgcolor: globalStyles.backgroundColor }}>
|
||||
<Box>
|
||||
<Container sx={{ p: 2 }}>
|
||||
{/* Header */}
|
||||
<PageHeader
|
||||
title="时间戳转换"
|
||||
subtitle="Unix 毫秒数转换与格式化"
|
||||
icon={<AccessTimeIcon />}
|
||||
sx={{ mb: 2.5 }}
|
||||
/>
|
||||
|
||||
{/* Live Clock Card */}
|
||||
@@ -66,7 +65,7 @@ export default function TimestampPage() {
|
||||
width: 'calc(50% - 5px)',
|
||||
bgcolor: '#fff',
|
||||
borderRadius: 3.5,
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.05)',
|
||||
transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
transform: mode === 'ts2dt' ? 'translateX(0)' : 'translateX(100%)',
|
||||
top: 5,
|
||||
@@ -171,31 +170,13 @@ export default function TimestampPage() {
|
||||
</Stack>
|
||||
|
||||
{/* Main Action */}
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
onClick={convert}
|
||||
sx={{
|
||||
py: 1.4,
|
||||
borderRadius: 4,
|
||||
bgcolor: 'primary.main',
|
||||
fontWeight: 800,
|
||||
fontSize: '0.9rem',
|
||||
boxShadow: 'none',
|
||||
'&:hover': {
|
||||
bgcolor: 'primary.dark',
|
||||
boxShadow: `0 8px 24px ${alpha(timestampPageStyles.primaryColor, 0.2)}`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button fullWidth variant="contained" onClick={convert}>
|
||||
立即转换
|
||||
</Button>
|
||||
|
||||
{/* Result View */}
|
||||
<ResultView result={result} mode={mode} unit={unit} zone={zone} showMessage={showMessage} />
|
||||
</Container>
|
||||
|
||||
<GlobalSnackbar {...snackbarProps} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getActiveTabDomain } from '@/utils/chromeTabs';
|
||||
|
||||
/**
|
||||
* 自动获取并维护当前活动标签页域名的 Hook
|
||||
*/
|
||||
export function useActiveTabDomain() {
|
||||
const [domain, setDomain] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
getActiveTabDomain().then(setDomain);
|
||||
|
||||
// 如果需要实时同步(如切换标签页),可以监听 chrome.tabs.onActivated
|
||||
const handleActivated = () => getActiveTabDomain().then(setDomain);
|
||||
const handleUpdated = (_: number, changeInfo: { url?: string }) => {
|
||||
if (changeInfo.url) getActiveTabDomain().then(setDomain);
|
||||
};
|
||||
|
||||
chrome.tabs.onActivated.addListener(handleActivated);
|
||||
chrome.tabs.onUpdated.addListener(handleUpdated);
|
||||
|
||||
return () => {
|
||||
chrome.tabs.onActivated.removeListener(handleActivated);
|
||||
chrome.tabs.onUpdated.removeListener(handleUpdated);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return domain;
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
import { useSnackbar as useGlobalSnackbar } from '@/components/SnackbarProvider';
|
||||
import { MessageAction, sendMessageToContent, injectContentScript } from '@/utils/messages';
|
||||
import { FillMode } from '@/utils/dummyDataGenerator';
|
||||
import { useStorageState } from '@/utils/useStorageState';
|
||||
import { FieldTypePreferences } from '@/types/storage';
|
||||
import { useActiveTabDomain } from './useActiveTabDomain';
|
||||
import { useSidePanelState } from './useSidePanelState';
|
||||
|
||||
// 字段数据接口
|
||||
export interface FieldData {
|
||||
id: string;
|
||||
fieldType: string;
|
||||
label: string | null;
|
||||
placeholder: string;
|
||||
name: string;
|
||||
value: string;
|
||||
isSelected: boolean;
|
||||
generatedValue: string;
|
||||
useInvalidData?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_FIELD_TYPE_PREFERENCES: FieldTypePreferences = {};
|
||||
|
||||
export function useFormRecognizer() {
|
||||
const { showMessage } = useGlobalSnackbar({ autoHideDuration: 1500 });
|
||||
const [fillLoading, setFillLoading] = useState(false);
|
||||
const [clearLoading, setClearLoading] = useState(false);
|
||||
const [includeHidden, setIncludeHidden] = useState(false);
|
||||
const isProcessingRef = useRef(false);
|
||||
const [fields, setFields] = useState<FieldData[]>([]);
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [showFields, setShowFields] = useState(false);
|
||||
const [hoveredFieldId, setHoveredFieldId] = useState<string | null>(null);
|
||||
|
||||
const currentDomain = useActiveTabDomain();
|
||||
const { sidePanelOpen, handleOpenSidePanel } = useSidePanelState();
|
||||
|
||||
const [fieldTypePreferences, setFieldTypePreferences] = useStorageState(
|
||||
'formRecognizer/fieldTypePreferences',
|
||||
DEFAULT_FIELD_TYPE_PREFERENCES,
|
||||
);
|
||||
|
||||
// 生成字段标识符
|
||||
const getFieldIdentifier = useCallback(
|
||||
(field: Pick<FieldData, 'label' | 'name' | 'placeholder'>): string => {
|
||||
return field.label || field.name || field.placeholder || 'unknown';
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// 应用保存的类型偏好
|
||||
const applySavedPreferences = useCallback(
|
||||
(fields: FieldData[], domain: string, preferences: FieldTypePreferences): FieldData[] => {
|
||||
if (!domain || !preferences[domain]) {
|
||||
return fields;
|
||||
}
|
||||
const prefs = preferences[domain];
|
||||
return fields.map((field) => {
|
||||
const identifier = getFieldIdentifier(field);
|
||||
if (prefs && prefs[identifier]) {
|
||||
return { ...field, fieldType: prefs[identifier] };
|
||||
}
|
||||
return field;
|
||||
});
|
||||
},
|
||||
[getFieldIdentifier],
|
||||
);
|
||||
|
||||
// 保存类型偏好
|
||||
const saveTypePreference = useCallback(
|
||||
(field: FieldData, newType: string) => {
|
||||
if (!currentDomain) return;
|
||||
const identifier = getFieldIdentifier(field);
|
||||
setFieldTypePreferences((prev) => {
|
||||
const prevPrefs = prev as FieldTypePreferences;
|
||||
const currentDomainPrefs = prevPrefs[currentDomain] || {};
|
||||
return {
|
||||
...prevPrefs,
|
||||
[currentDomain]: {
|
||||
...currentDomainPrefs,
|
||||
[identifier]: newType,
|
||||
},
|
||||
} as FieldTypePreferences;
|
||||
});
|
||||
},
|
||||
[currentDomain, getFieldIdentifier, setFieldTypePreferences],
|
||||
);
|
||||
|
||||
// 扫描表单字段
|
||||
const handleScanFields = async () => {
|
||||
setScanning(true);
|
||||
try {
|
||||
let response = await sendMessageToContent(MessageAction.SCAN_FORM_FIELDS);
|
||||
|
||||
if (!response.success && response.message && response.message.includes('无法连接')) {
|
||||
showMessage('正在注入内容脚本...', { severity: 'info' });
|
||||
const injected = await injectContentScript();
|
||||
if (injected) {
|
||||
response = await sendMessageToContent(MessageAction.SCAN_FORM_FIELDS);
|
||||
}
|
||||
}
|
||||
|
||||
if (response.success && response.fields) {
|
||||
const fieldsWithPreferences = applySavedPreferences(
|
||||
response.fields as FieldData[],
|
||||
currentDomain,
|
||||
fieldTypePreferences,
|
||||
);
|
||||
setFields(fieldsWithPreferences);
|
||||
setShowFields(true);
|
||||
showMessage(`扫描完成,发现 ${response.totalCount} 个可填充字段`, { severity: 'success' });
|
||||
} else {
|
||||
showMessage(response.message || '扫描失败', { severity: 'error' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('扫描失败:', error);
|
||||
showMessage('扫描失败,请确保页面已加载', { severity: 'error' });
|
||||
} finally {
|
||||
setScanning(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新字段类型
|
||||
const handleFieldTypeChange = (fieldId: string, newType: string) => {
|
||||
setFields((prev) =>
|
||||
prev.map((field) => {
|
||||
if (field.id === fieldId) {
|
||||
const updatedField = {
|
||||
...field,
|
||||
fieldType: newType,
|
||||
generatedValue: '',
|
||||
};
|
||||
saveTypePreference(field, newType);
|
||||
return updatedField;
|
||||
}
|
||||
return field;
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
// 切换单个字段的选中状态
|
||||
const handleToggleFieldSelection = (fieldId: string) => {
|
||||
setFields((prev) =>
|
||||
prev.map((field) =>
|
||||
field.id === fieldId ? { ...field, isSelected: !field.isSelected } : field,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
// 全选/取消全选
|
||||
const handleToggleAllFields = () => {
|
||||
setFields((prev) => {
|
||||
const allSelected = prev.every((f) => f.isSelected);
|
||||
return prev.map((f) => ({ ...f, isSelected: !allSelected }));
|
||||
});
|
||||
};
|
||||
|
||||
// 定位字段(闪烁)
|
||||
const handleLocateField = async (fieldId: string) => {
|
||||
try {
|
||||
const response = await sendMessageToContent(MessageAction.FLASH_FIELD, { fieldId });
|
||||
if (!response.success) {
|
||||
showMessage(response.message || '定位字段失败', { severity: 'error' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('定位字段失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 悬停高亮
|
||||
const handleHoverField = async (fieldId: string | null) => {
|
||||
setHoveredFieldId(fieldId);
|
||||
try {
|
||||
if (fieldId) {
|
||||
await sendMessageToContent(MessageAction.HIGHLIGHT_FIELD, { fieldId });
|
||||
} else {
|
||||
await sendMessageToContent(MessageAction.UNHIGHLIGHT_ALL_FIELDS);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('高亮字段失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 填充选中字段
|
||||
const handleFillSelectedFields = async () => {
|
||||
if (isProcessingRef.current) {
|
||||
showMessage('操作进行中,请稍候...', { severity: 'warning' });
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedCount = fields.filter((f) => f.isSelected).length;
|
||||
if (selectedCount === 0) {
|
||||
showMessage('请先选择要填充的字段', { severity: 'warning' });
|
||||
return;
|
||||
}
|
||||
|
||||
setFillLoading(true);
|
||||
isProcessingRef.current = true;
|
||||
try {
|
||||
const messageFields = fields as MessageFieldData[];
|
||||
let response = await sendMessageToContent(MessageAction.FILL_SELECTED_FIELDS, {
|
||||
fields: messageFields,
|
||||
mode: FillMode.VALID,
|
||||
includeHidden,
|
||||
});
|
||||
|
||||
if (!response.success && response.message && response.message.includes('无法连接')) {
|
||||
showMessage('正在注入内容脚本...', { severity: 'info' });
|
||||
const injected = await injectContentScript();
|
||||
if (injected) {
|
||||
response = await sendMessageToContent(MessageAction.FILL_SELECTED_FIELDS, {
|
||||
fields: messageFields,
|
||||
mode: FillMode.VALID,
|
||||
includeHidden,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
showMessage(response.message || '填充成功', { severity: 'success' });
|
||||
} else {
|
||||
showMessage(response.message || '填充失败', { severity: 'error' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('填充失败:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
showMessage(`填充失败:${errorMessage},请确保当前页面已加载完成`, { severity: 'error' });
|
||||
} finally {
|
||||
setFillLoading(false);
|
||||
isProcessingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 清空所有字段
|
||||
const handleClearAllFields = async () => {
|
||||
if (isProcessingRef.current) {
|
||||
showMessage('操作进行中,请稍候...', { severity: 'warning' });
|
||||
return;
|
||||
}
|
||||
|
||||
setClearLoading(true);
|
||||
isProcessingRef.current = true;
|
||||
try {
|
||||
let response = await sendMessageToContent(MessageAction.CLEAR_ALL_FIELDS);
|
||||
|
||||
if (!response.success && response.message && response.message.includes('无法连接')) {
|
||||
showMessage('正在注入内容脚本...', { severity: 'info' });
|
||||
const injected = await injectContentScript();
|
||||
if (injected) {
|
||||
response = await sendMessageToContent(MessageAction.CLEAR_ALL_FIELDS);
|
||||
}
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
showMessage(response.message || '清空成功', { severity: 'success' });
|
||||
} else {
|
||||
showMessage(response.message || '清空失败', { severity: 'error' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('清空失败:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
showMessage(`清空失败:${errorMessage},请确保当前页面已加载完成`, { severity: 'error' });
|
||||
} finally {
|
||||
setClearLoading(false);
|
||||
isProcessingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
fillLoading,
|
||||
clearLoading,
|
||||
includeHidden,
|
||||
setIncludeHidden,
|
||||
fields,
|
||||
scanning,
|
||||
showFields,
|
||||
setShowFields,
|
||||
hoveredFieldId,
|
||||
sidePanelOpen,
|
||||
handleScanFields,
|
||||
handleFieldTypeChange,
|
||||
handleToggleFieldSelection,
|
||||
handleToggleAllFields,
|
||||
handleLocateField,
|
||||
handleHoverField,
|
||||
handleFillSelectedFields,
|
||||
handleClearAllFields,
|
||||
handleOpenSidePanel,
|
||||
selectedCount: fields.filter((f) => f.isSelected).length,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { MessageAction, onMessage } from '@/utils/messages';
|
||||
import { useSnackbar } from '@/components/SnackbarProvider';
|
||||
|
||||
/**
|
||||
* 管理侧边栏状态检测与开启逻辑的 Hook
|
||||
*/
|
||||
export function useSidePanelState() {
|
||||
const [sidePanelOpen, setSidePanelOpen] = useState(false);
|
||||
const { showMessage } = useSnackbar();
|
||||
|
||||
const checkSidePanelState = useCallback(async () => {
|
||||
try {
|
||||
if (typeof chrome.runtime.getContexts === 'function') {
|
||||
const contexts = await chrome.runtime.getContexts({
|
||||
contextTypes: ['SIDE_PANEL'],
|
||||
});
|
||||
setSidePanelOpen(contexts.length > 0);
|
||||
} else {
|
||||
setSidePanelOpen(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检测侧边栏状态失败:', error);
|
||||
setSidePanelOpen(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 使用 requestAnimationFrame 避免同步调用 setState
|
||||
requestAnimationFrame(() => {
|
||||
checkSidePanelState().catch(console.error);
|
||||
});
|
||||
|
||||
// 监听侧边栏状态变化消息
|
||||
const removeListener = onMessage(MessageAction.SIDE_PANEL_STATE_CHANGED, (message) => {
|
||||
setSidePanelOpen(message.data.isOpen);
|
||||
});
|
||||
|
||||
return () => {
|
||||
removeListener();
|
||||
};
|
||||
}, [checkSidePanelState]);
|
||||
|
||||
const handleOpenSidePanel = useCallback(async () => {
|
||||
try {
|
||||
await chrome.sidePanel.open({ windowId: chrome.windows.WINDOW_ID_CURRENT });
|
||||
setSidePanelOpen(true);
|
||||
showMessage('侧边栏已打开', { severity: 'success' });
|
||||
} catch (error) {
|
||||
console.error('打开侧边栏失败:', error);
|
||||
showMessage('打开侧边栏失败', { severity: 'error' });
|
||||
}
|
||||
}, [showMessage]);
|
||||
|
||||
return {
|
||||
sidePanelOpen,
|
||||
handleOpenSidePanel,
|
||||
checkSidePanelState,
|
||||
};
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
getCacheStorageSize,
|
||||
getServiceWorkerCount,
|
||||
} from '@/utils/storageCleaner';
|
||||
import { MessageAction, sendMessage } from '@/utils/messages';
|
||||
|
||||
const DEFAULT_OPTIONS: StorageCleanerOptions = {
|
||||
localStorage: true,
|
||||
@@ -73,16 +74,15 @@ export function useStorageCleaner({
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [result, setResult] = useState<CleaningResult | null>(null);
|
||||
const [showConfirm, setShowConfirm] = useState<boolean>(false);
|
||||
const reloadTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const resultTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const requestIdRef = useRef<number>(0);
|
||||
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const reloadTimeout = reloadTimeoutRef.current;
|
||||
const resultTimeout = resultTimeoutRef.current;
|
||||
return () => {
|
||||
if (reloadTimeout) clearTimeout(reloadTimeout);
|
||||
if (resultTimeout) clearTimeout(resultTimeout);
|
||||
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -141,13 +141,23 @@ export function useStorageCleaner({
|
||||
const loadInfoRef = useRef(loadInfo);
|
||||
loadInfoRef.current = loadInfo;
|
||||
|
||||
useEffect(() => {
|
||||
loadInfoRef.current();
|
||||
const debouncedLoadInfo = useCallback(() => {
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
debounceTimerRef.current = setTimeout(() => {
|
||||
loadInfoRef.current().then((r) => console.info(r));
|
||||
}, 300);
|
||||
}, []);
|
||||
|
||||
const handleTabChange = () => loadInfoRef.current();
|
||||
useEffect(() => {
|
||||
// 首次加载不防抖
|
||||
loadInfoRef.current().then((r) => console.info(r));
|
||||
|
||||
const handleTabChange = () => debouncedLoadInfo();
|
||||
const handleTabUpdated = (_tabId: number, changeInfo: { status?: string; url?: string }) => {
|
||||
if (changeInfo.status === 'complete' || changeInfo.url) {
|
||||
loadInfoRef.current();
|
||||
debouncedLoadInfo();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -159,8 +169,11 @@ export function useStorageCleaner({
|
||||
chrome.tabs.onActivated.removeListener(handleTabChange);
|
||||
chrome.tabs.onUpdated.removeListener(handleTabUpdated);
|
||||
chrome.windows.onFocusChanged.removeListener(handleTabChange);
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, [debouncedLoadInfo]);
|
||||
|
||||
const handleAutoRefreshChange = useCallback(
|
||||
async (checked: boolean) => {
|
||||
@@ -219,7 +232,7 @@ export function useStorageCleaner({
|
||||
|
||||
if (autoRefresh && cleaningResult.success) {
|
||||
showMessage('清理成功,即将刷新页面', { severity: 'success' });
|
||||
await chrome.runtime.sendMessage({ action: 'reloadTab', tabId: tab.id, delay: 1000 });
|
||||
await sendMessage(MessageAction.RELOAD_TAB, { tabId: tab.id, delay: 1000 });
|
||||
} else {
|
||||
await loadInfo();
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-size: inherit; /* 先全部重置为继承大小 */
|
||||
font-weight: inherit;
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
/* 极简滚动条变量 */
|
||||
:root {
|
||||
/* 统一圆角变量 */
|
||||
--radius: 8px;
|
||||
/* 统一背景颜色变量 */
|
||||
--bg-color: #fafafa;
|
||||
/* 统一文字颜色变量 */
|
||||
--text-color: #333333;
|
||||
/* 统一按钮颜色变量 */
|
||||
--btn-bg: #e0e0e0;
|
||||
/* 统一按钮文字颜色变量 */
|
||||
--btn-text: #333333;
|
||||
--border: 1px solid #e0e0e0;
|
||||
|
||||
/* 滚动条变量 */
|
||||
--sb-width: 6px;
|
||||
--sb-thumb-color: rgba(0, 0, 0, 0.1);
|
||||
--sb-thumb-hover: rgba(0, 0, 0, 0.2);
|
||||
--sb-track-color: transparent;
|
||||
}
|
||||
|
||||
/* 适配侧边栏 - 使用百分比而非固定尺寸 */
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden; /* 禁用外层滚动 */
|
||||
}
|
||||
|
||||
/* 全局极简滚动条定制 */
|
||||
*::-webkit-scrollbar {
|
||||
width: var(--sb-width);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: var(--sb-track-color);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: var(--sb-thumb-color);
|
||||
border-radius: 10px;
|
||||
background-clip: content-box;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--sb-thumb-hover);
|
||||
}
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
overflow: hidden; /* 内部由 flex 子项控制滚动 */
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(30px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.page-transition-enter {
|
||||
animation: slideInRight 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) forwards;
|
||||
}
|
||||
|
||||
.page-transition-dashboard {
|
||||
animation: fadeIn 0.3s ease-out forwards;
|
||||
}
|
||||
@@ -1,21 +1,42 @@
|
||||
import './App.css';
|
||||
import { useEffect } from 'react';
|
||||
import RouterProvider from '@/providers/RouterProvider';
|
||||
import TopBar from '@/components/TopBar';
|
||||
import RouterContainer from '@/components/RouterContainer';
|
||||
import ErrorBoundary from '@/components/ErrorBoundary';
|
||||
import { MessageAction, sendMessage } from '@/utils/messages';
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
function App() {
|
||||
export default function App() {
|
||||
const handleOpenOptions = () => {
|
||||
chrome.runtime.openOptionsPage();
|
||||
};
|
||||
|
||||
// 通知侧边栏已打开
|
||||
useEffect(() => {
|
||||
sendMessage(MessageAction.SIDE_PANEL_STATE_CHANGED, { isOpen: true });
|
||||
return () => {
|
||||
// 尝试在关闭时通知,虽然在某些情况下可能无法成功发送
|
||||
sendMessage(MessageAction.SIDE_PANEL_STATE_CHANGED, { isOpen: false });
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<RouterProvider defaultRoute="dashboard" syncKey="app/sidepanelRoute">
|
||||
<div className="app" style={{ display: 'flex', flexDirection: 'column', height: '100vh', overflow: 'hidden' }}>
|
||||
<Box
|
||||
className="app"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100vh',
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<TopBar onOpenOptions={handleOpenOptions} />
|
||||
<RouterContainer />
|
||||
</div>
|
||||
<ErrorBoundary>
|
||||
<RouterContainer />
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
</RouterProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import theme from '@/config/theme';
|
||||
import App from './App.tsx';
|
||||
import './style.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Ubuntu', 'Cantarell', 'Fira Sans',
|
||||
'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit; /* 先全部重置为继承大小 */
|
||||
font-weight: inherit;
|
||||
}
|
||||
Reference in New Issue
Block a user