Develop fill form (#10)
* feat(formRecognizer): 添加表单识别功能及相关组件 添加表单识别功能,包括以下内容: 1. 在路由配置中添加表单识别页面 2. 实现表单识别页面和侧边栏面板 3. 添加表单数据生成工具类 4. 实现与内容脚本的通信机制 5. 添加faker-js依赖用于生成测试数据 6. 支持不同入口点(popup/sidepanel)的组件渲染 * refactor(消息通信): 重构消息通信机制并集中管理消息协议 将分散的消息协议和通信逻辑集中到 utils/messages.ts 中 移除旧的 messages.tsx 文件并更新相关引用 添加消息动作枚举和类型定义,提高类型安全性 优化内容脚本注入失败时的处理逻辑
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import { ROUTES } from '@/config/routes';
|
import { ROUTES, getEntryPointType } from '@/config/routes';
|
||||||
import { useRouter } from '@/providers/RouterProvider';
|
import { useRouter } from '@/providers/RouterProvider';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
@@ -10,11 +10,16 @@ export default function RouterContainer() {
|
|||||||
return currentPage === 'dashboard' ? 'page-transition-dashboard' : 'page-transition-enter';
|
return currentPage === 'dashboard' ? 'page-transition-dashboard' : 'page-transition-enter';
|
||||||
}, [currentPage]);
|
}, [currentPage]);
|
||||||
|
|
||||||
|
const entryPointType = useMemo(() => {
|
||||||
|
return getEntryPointType();
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!isLoaded) {
|
if (!isLoaded) {
|
||||||
return <div className="app">Loading...</div>;
|
return <div className="app">Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentRoute = ROUTES.find(route => route.key === currentPage);
|
const currentRoute = ROUTES.find((route) => route.key === currentPage);
|
||||||
|
const Component = currentRoute ? currentRoute.components[entryPointType] : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -29,7 +34,7 @@ export default function RouterContainer() {
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{currentRoute && <currentRoute.component />}
|
{Component && <Component />}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+64
-11
@@ -5,12 +5,18 @@ import StorageCleanerPage from '@/entrypoints/popup/pages/StorageCleanerPage';
|
|||||||
import OpenUrlPage from '@/entrypoints/popup/pages/OpenUrlPage';
|
import OpenUrlPage from '@/entrypoints/popup/pages/OpenUrlPage';
|
||||||
import OpenUrlViewerPage from '@/entrypoints/popup/pages/OpenUrlViewerPage';
|
import OpenUrlViewerPage from '@/entrypoints/popup/pages/OpenUrlViewerPage';
|
||||||
import QrCodePage from '@/entrypoints/popup/pages/QrCodePage';
|
import QrCodePage from '@/entrypoints/popup/pages/QrCodePage';
|
||||||
|
import FormRecognizerPage from '@/entrypoints/popup/pages/FormRecognizerPage';
|
||||||
|
import FormFillSidePanel from '@/entrypoints/sidepanel/pages/FormFillSidePanel';
|
||||||
|
|
||||||
export interface RouteConfig {
|
export interface RouteConfig {
|
||||||
key: PageType;
|
key: PageType;
|
||||||
label: string;
|
label: string;
|
||||||
defaultVisible: boolean;
|
defaultVisible: boolean;
|
||||||
component: React.ComponentType;
|
components: {
|
||||||
|
popup: React.ComponentType;
|
||||||
|
sidepanel: React.ComponentType;
|
||||||
|
detached: React.ComponentType;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ROUTES: RouteConfig[] = [
|
export const ROUTES: RouteConfig[] = [
|
||||||
@@ -18,52 +24,99 @@ export const ROUTES: RouteConfig[] = [
|
|||||||
key: 'dashboard',
|
key: 'dashboard',
|
||||||
label: 'Dashboard',
|
label: 'Dashboard',
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
component: DashboardPage,
|
components: {
|
||||||
|
popup: DashboardPage,
|
||||||
|
sidepanel: DashboardPage,
|
||||||
|
detached: DashboardPage,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'timestamp',
|
key: 'timestamp',
|
||||||
label: '时间戳',
|
label: '时间戳',
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
component: TimestampPage,
|
components: {
|
||||||
|
popup: TimestampPage,
|
||||||
|
sidepanel: TimestampPage,
|
||||||
|
detached: TimestampPage,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'storageCleaner',
|
key: 'storageCleaner',
|
||||||
label: '存储清理',
|
label: '存储清理',
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
component: StorageCleanerPage,
|
components: {
|
||||||
|
popup: StorageCleanerPage,
|
||||||
|
sidepanel: StorageCleanerPage,
|
||||||
|
detached: StorageCleanerPage,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'openUrl',
|
key: 'openUrl',
|
||||||
label: 'Open Url',
|
label: 'Open Url',
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
component: OpenUrlPage,
|
components: {
|
||||||
|
popup: OpenUrlPage,
|
||||||
|
sidepanel: OpenUrlPage,
|
||||||
|
detached: OpenUrlPage,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'qrCode',
|
key: 'qrCode',
|
||||||
label: '二维码工具',
|
label: '二维码工具',
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
component: QrCodePage,
|
components: {
|
||||||
|
popup: QrCodePage,
|
||||||
|
sidepanel: QrCodePage,
|
||||||
|
detached: QrCodePage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'formRecognizer',
|
||||||
|
label: '表单识别',
|
||||||
|
defaultVisible: true,
|
||||||
|
components: {
|
||||||
|
popup: FormRecognizerPage,
|
||||||
|
sidepanel: FormFillSidePanel,
|
||||||
|
detached: FormRecognizerPage,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'openUrlViewer',
|
key: 'openUrlViewer',
|
||||||
label: '查看',
|
label: '查看',
|
||||||
defaultVisible: false,
|
defaultVisible: false,
|
||||||
component: OpenUrlViewerPage,
|
components: {
|
||||||
|
popup: OpenUrlViewerPage,
|
||||||
|
sidepanel: OpenUrlViewerPage,
|
||||||
|
detached: OpenUrlViewerPage,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getRouteByKey(key: PageType): RouteConfig | undefined {
|
export function getRouteByKey(key: PageType): RouteConfig | undefined {
|
||||||
return ROUTES.find(route => route.key === key);
|
return ROUTES.find((route) => route.key === key);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaultVisibleRoutes(): PageType[] {
|
export function getDefaultVisibleRoutes(): PageType[] {
|
||||||
return ROUTES.filter(route => route.defaultVisible).map(route => route.key);
|
return ROUTES.filter((route) => route.defaultVisible).map((route) => route.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllRouteKeys(): PageType[] {
|
export function getAllRouteKeys(): PageType[] {
|
||||||
return ROUTES.map(route => route.key);
|
return ROUTES.map((route) => route.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaultPageOrder(): PageType[] {
|
export function getDefaultPageOrder(): PageType[] {
|
||||||
return ROUTES.filter(route => route.key !== 'dashboard' && route.key !== 'openUrlViewer').map(route => route.key);
|
return ROUTES.filter((route) => route.key !== 'dashboard' && route.key !== 'openUrlViewer').map(
|
||||||
|
(route) => route.key,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEntryPointType(): 'popup' | 'sidepanel' | 'detached' {
|
||||||
|
const pathname = window.location.pathname;
|
||||||
|
if (pathname.includes('sidepanel')) {
|
||||||
|
return 'sidepanel';
|
||||||
|
}
|
||||||
|
if (new URLSearchParams(window.location.search).get('mode') === 'detached') {
|
||||||
|
return 'detached';
|
||||||
|
}
|
||||||
|
return 'popup';
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-34
@@ -1,5 +1,6 @@
|
|||||||
import '../.wxt/types/imports.d.ts';
|
import '../.wxt/types/imports.d.ts';
|
||||||
import { browser } from 'wxt/browser';
|
import { browser } from 'wxt/browser';
|
||||||
|
import { type MessagePayload, type MessageResponse } from '@/utils/messages';
|
||||||
|
|
||||||
export default defineBackground(() => {
|
export default defineBackground(() => {
|
||||||
// 监听扩展图标点击事件,打开侧边栏
|
// 监听扩展图标点击事件,打开侧边栏
|
||||||
@@ -20,40 +21,6 @@ export default defineBackground(() => {
|
|||||||
} else if (reason === 'update') {
|
} else if (reason === 'update') {
|
||||||
console.log('Extension updated to a new version');
|
console.log('Extension updated to a new version');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取所有标签页
|
|
||||||
const tabs = await browser.tabs.query({});
|
|
||||||
|
|
||||||
// 过滤不合法或受限制的 URL
|
|
||||||
const targetTabs = tabs.filter((tab) => {
|
|
||||||
if (!tab.id || !tab.url) return false;
|
|
||||||
const restrictedProtocols = [
|
|
||||||
'chrome:',
|
|
||||||
'chrome-extension:',
|
|
||||||
'about:',
|
|
||||||
'edge:',
|
|
||||||
'view-source:',
|
|
||||||
];
|
|
||||||
return !restrictedProtocols.some((protocol) => tab.url!.startsWith(protocol));
|
|
||||||
});
|
|
||||||
|
|
||||||
const results = await Promise.allSettled(
|
|
||||||
targetTabs.map((tab) =>
|
|
||||||
browser.scripting
|
|
||||||
.executeScript({
|
|
||||||
target: { tabId: tab.id! },
|
|
||||||
files: ['/content-scripts/content.js'],
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.warn(`Failed to inject script into tab ${tab.id}:`, err.message);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const successCount = results.filter((r) => r.status === 'fulfilled').length;
|
|
||||||
console.log(
|
|
||||||
`Successfully injected content script into ${successCount}/${targetTabs.length} tabs.`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
|
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
|
||||||
@@ -61,4 +28,25 @@ export default defineBackground(() => {
|
|||||||
console.log('加载完成的 Tab ID:', tabId);
|
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);
|
||||||
|
sendResponse({ success: true, message: '消息已收到' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理消息失败:', error);
|
||||||
|
sendResponse({ success: false, message: '处理消息失败' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
+110
-2
@@ -1,9 +1,117 @@
|
|||||||
import '../.wxt/types/imports.d.ts';
|
import '../.wxt/types/imports.d.ts';
|
||||||
|
import {
|
||||||
|
fillAllFields,
|
||||||
|
clearAllFields,
|
||||||
|
fillSelectedFields,
|
||||||
|
scanFormFields,
|
||||||
|
highlightField,
|
||||||
|
unhighlightField,
|
||||||
|
FillMode,
|
||||||
|
type FormFieldInfo,
|
||||||
|
} from '@/utils/dummyDataGenerator';
|
||||||
|
import { MessageAction, type MessagePayload, type MessageResponse } from '@/utils/messages';
|
||||||
|
|
||||||
|
// 存储当前扫描到的字段列表,用于高亮联动
|
||||||
|
let currentFields: FormFieldInfo[] = [];
|
||||||
|
|
||||||
export default defineContentScript({
|
export default defineContentScript({
|
||||||
matches: ['<all_urls>'],
|
matches: ['<all_urls>'],
|
||||||
runAt: 'document_start',
|
runAt: 'document_end',
|
||||||
main() {
|
main() {
|
||||||
// Content script placeholder
|
// 监听来自 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 fieldIds = (message.fields || []).filter((f) => f.isSelected).map((f) => f.id);
|
||||||
|
const fieldsToFill = currentFields.filter((f) => fieldIds.includes(f.id));
|
||||||
|
fieldsToFill.forEach((f) => (f.isSelected = true));
|
||||||
|
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;
|
||||||
|
default:
|
||||||
|
sendResponse({ success: false, message: '未知操作' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('执行操作失败:', error);
|
||||||
|
sendResponse({ success: false, message: '执行操作失败' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
|||||||
import StorageIcon from '@mui/icons-material/Storage';
|
import StorageIcon from '@mui/icons-material/Storage';
|
||||||
import LanguageIcon from '@mui/icons-material/Language';
|
import LanguageIcon from '@mui/icons-material/Language';
|
||||||
import QrCodeIcon from '@mui/icons-material/QrCode';
|
import QrCodeIcon from '@mui/icons-material/QrCode';
|
||||||
|
import DescriptionIcon from '@mui/icons-material/Description';
|
||||||
import type { PageType } from '@/types/storage';
|
import type { PageType } from '@/types/storage';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import dayjs from '@/utils/dayjs';
|
import dayjs from '@/utils/dayjs';
|
||||||
@@ -88,6 +89,18 @@ export default function DashboardPage() {
|
|||||||
cardBackgroundColor={dashboardPageStyles.cardBackgroundColor}
|
cardBackgroundColor={dashboardPageStyles.cardBackgroundColor}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case 'formRecognizer':
|
||||||
|
return (
|
||||||
|
<ToolCard
|
||||||
|
key={key}
|
||||||
|
title="表单识别"
|
||||||
|
description="识别标签页中的表单内容"
|
||||||
|
colorCode="#ff5722"
|
||||||
|
icon={<DescriptionIcon sx={{ fontSize: 20 }} />}
|
||||||
|
onClick={() => navigateTo('formRecognizer')}
|
||||||
|
cardBackgroundColor={dashboardPageStyles.cardBackgroundColor}
|
||||||
|
/>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Container,
|
||||||
|
Button,
|
||||||
|
Paper,
|
||||||
|
CircularProgress,
|
||||||
|
Stack,
|
||||||
|
Switch,
|
||||||
|
FormControlLabel,
|
||||||
|
} from '@mui/material';
|
||||||
|
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
||||||
|
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||||
|
import ClearAllIcon from '@mui/icons-material/ClearAll';
|
||||||
|
import GlobalSnackbar, { useSnackbar } from '@/components/GlobalSnackbar';
|
||||||
|
import { dashboardPageStyles } from '@/config/pageTheme';
|
||||||
|
|
||||||
|
const FormRecognizerPage = () => {
|
||||||
|
const { snackbarProps, showMessage } = useSnackbar({ autoHideDuration: 1500 });
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [includeHidden, setIncludeHidden] = useState(false);
|
||||||
|
|
||||||
|
interface MessagePayload {
|
||||||
|
includeHidden?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendMessageToContent = async (action: string, payload?: MessagePayload) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
|
if (!tab.id) {
|
||||||
|
showMessage('无法获取当前标签页', { severity: 'error' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await chrome.tabs.sendMessage(tab.id, { action, ...payload });
|
||||||
|
if (response.success) {
|
||||||
|
showMessage(response.message, { severity: 'success' });
|
||||||
|
} else {
|
||||||
|
showMessage(response.message, { severity: 'error' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发送消息失败:', error);
|
||||||
|
showMessage('请确保当前页面已加载完成', { severity: 'error' });
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFillValidData = () => {
|
||||||
|
sendMessageToContent('fillValidData', { includeHidden });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFillInvalidData = () => {
|
||||||
|
sendMessageToContent('fillInvalidData', { includeHidden });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearAllFields = () => {
|
||||||
|
sendMessageToContent('clearAllFields');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ bgcolor: dashboardPageStyles.backgroundColor, minHeight: '100%', pb: 4 }}>
|
||||||
|
<Container maxWidth="sm" sx={{ py: 3, px: 2 }}>
|
||||||
|
<Box sx={{ mb: 4 }}>
|
||||||
|
<Typography variant="h6" component="h1" sx={{ fontWeight: 700, mb: 1 }}>
|
||||||
|
Dummy Data Generator
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
一键填充表单测试数据,提升开发和测试效率
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 主要操作按钮 */}
|
||||||
|
<Stack spacing={2} sx={{ mb: 4 }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={
|
||||||
|
loading ? <CircularProgress size={16} color="inherit" /> : <AutoAwesomeIcon />
|
||||||
|
}
|
||||||
|
onClick={handleFillValidData}
|
||||||
|
disabled={loading}
|
||||||
|
fullWidth
|
||||||
|
sx={{
|
||||||
|
py: 1.2,
|
||||||
|
borderRadius: 3,
|
||||||
|
bgcolor: '#4caf50',
|
||||||
|
fontWeight: 700,
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: '#388e3c',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? '填充中...' : '一键填充(有效数据)'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={
|
||||||
|
loading ? <CircularProgress size={16} color="inherit" /> : <ErrorOutlineIcon />
|
||||||
|
}
|
||||||
|
onClick={handleFillInvalidData}
|
||||||
|
disabled={loading}
|
||||||
|
fullWidth
|
||||||
|
sx={{
|
||||||
|
py: 1.2,
|
||||||
|
borderRadius: 3,
|
||||||
|
bgcolor: '#ff9800',
|
||||||
|
fontWeight: 700,
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: '#f57c00',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? '填充中...' : '一键填充(异常数据)'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={loading ? <CircularProgress size={16} color="inherit" /> : <ClearAllIcon />}
|
||||||
|
onClick={handleClearAllFields}
|
||||||
|
disabled={loading}
|
||||||
|
fullWidth
|
||||||
|
sx={{
|
||||||
|
py: 1.2,
|
||||||
|
borderRadius: 3,
|
||||||
|
borderColor: '#f44336',
|
||||||
|
color: '#f44336',
|
||||||
|
fontWeight: 700,
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: '#d32f2f',
|
||||||
|
bgcolor: 'rgba(244, 67, 54, 0.05)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? '清空ing...' : '一键清空所有表单'}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* 选项设置 */}
|
||||||
|
<Paper elevation={0} sx={{ borderRadius: 4, overflow: 'hidden', mb: 4 }}>
|
||||||
|
<Box sx={{ borderBottom: 1, borderColor: 'divider', px: 2, py: 1.5 }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
|
||||||
|
填充选项
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={includeHidden}
|
||||||
|
onChange={(e) => setIncludeHidden(e.target.checked)}
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="包含隐藏字段"
|
||||||
|
sx={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* 功能说明 */}
|
||||||
|
<Paper elevation={0} sx={{ borderRadius: 4, overflow: 'hidden' }}>
|
||||||
|
<Box sx={{ borderBottom: 1, borderColor: 'divider', px: 2, py: 1.5 }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
|
||||||
|
功能说明
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||||
|
<strong>有效数据模式:</strong>生成符合格式要求的测试数据,适用于正常功能测试。
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||||
|
<strong>异常数据模式:</strong>生成边界值或格式错误的数据,适用于异常场景测试。
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||||
|
<strong>一键清空:</strong>快速清空当前页面所有表单字段的值。
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
<strong>支持的字段类型:</strong>
|
||||||
|
文本、邮箱、手机号、数字、日期、文本域、密码、身份证号等。
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<GlobalSnackbar {...snackbarProps} />
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormRecognizerPage;
|
||||||
@@ -0,0 +1,477 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Paper,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
TextField,
|
||||||
|
IconButton,
|
||||||
|
CircularProgress,
|
||||||
|
Switch,
|
||||||
|
FormControlLabel,
|
||||||
|
Tooltip,
|
||||||
|
Alert,
|
||||||
|
} from '@mui/material';
|
||||||
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
|
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||||
|
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||||
|
import HighlightAltIcon from '@mui/icons-material/HighlightAlt';
|
||||||
|
import GlobalSnackbar, { useSnackbar } from '@/components/GlobalSnackbar';
|
||||||
|
import { FieldType, FillMode } from '@/utils/dummyDataGenerator';
|
||||||
|
import { MessageAction, sendMessageToContent, injectContentScript } from '@/utils/messages';
|
||||||
|
|
||||||
|
interface FieldData {
|
||||||
|
id: string;
|
||||||
|
fieldType: FieldType;
|
||||||
|
label: string | null;
|
||||||
|
placeholder: string;
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
isSelected: boolean;
|
||||||
|
generatedValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormFillSidePanel = () => {
|
||||||
|
const { snackbarProps, showMessage } = useSnackbar({ autoHideDuration: 1500 });
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [scanning, setScanning] = useState(false);
|
||||||
|
const [fields, setFields] = useState<FieldData[]>([]);
|
||||||
|
const [mode, setMode] = useState<'valid' | 'invalid'>('valid');
|
||||||
|
const [fillEmptyOnly, setFillEmptyOnly] = useState(false);
|
||||||
|
const [includeHidden] = useState(false);
|
||||||
|
const [hoveredFieldId, setHoveredFieldId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleScan = 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) {
|
||||||
|
setFields(response.fields || []);
|
||||||
|
showMessage(`扫描完成,发现 ${response.totalCount} 个可填充字段`, { severity: 'success' });
|
||||||
|
} else {
|
||||||
|
showMessage(response.message || '扫描失败', { severity: 'error' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('扫描失败:', error);
|
||||||
|
showMessage('扫描失败,请确保页面已加载', { severity: 'error' });
|
||||||
|
} finally {
|
||||||
|
setScanning(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefreshAll = async () => {
|
||||||
|
const updatedFields = fields.map((field) => ({
|
||||||
|
...field,
|
||||||
|
generatedValue: generateRandomValue(field.fieldType, mode),
|
||||||
|
}));
|
||||||
|
setFields(updatedFields);
|
||||||
|
showMessage('已刷新所有数据', { severity: 'success' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefreshField = (fieldId: string) => {
|
||||||
|
setFields((prevFields) =>
|
||||||
|
prevFields.map((field) =>
|
||||||
|
field.id === fieldId
|
||||||
|
? { ...field, generatedValue: generateRandomValue(field.fieldType, mode) }
|
||||||
|
: field,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleSelect = (fieldId: string) => {
|
||||||
|
setFields((prevFields) =>
|
||||||
|
prevFields.map((field) =>
|
||||||
|
field.id === fieldId ? { ...field, isSelected: !field.isSelected } : field,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectAll = () => {
|
||||||
|
const allSelected = fields.every((f) => f.isSelected);
|
||||||
|
setFields((prevFields) => prevFields.map((field) => ({ ...field, isSelected: !allSelected })));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditValue = (fieldId: string, newValue: string) => {
|
||||||
|
setFields((prevFields) =>
|
||||||
|
prevFields.map((field) =>
|
||||||
|
field.id === fieldId ? { ...field, generatedValue: newValue } : field,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFill = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const selectedFields = fields.filter((f) => f.isSelected);
|
||||||
|
const response = await sendMessageToContent(MessageAction.FILL_SELECTED_FIELDS, {
|
||||||
|
fields: selectedFields,
|
||||||
|
mode: mode === 'valid' ? FillMode.VALID : FillMode.INVALID,
|
||||||
|
includeHidden,
|
||||||
|
});
|
||||||
|
if (response.success) {
|
||||||
|
showMessage(response.message || '填充成功', { severity: 'success' });
|
||||||
|
} else {
|
||||||
|
showMessage(response.message || '填充失败', { severity: 'error' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('填充失败:', error);
|
||||||
|
showMessage('填充失败', { severity: 'error' });
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClear = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const 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);
|
||||||
|
showMessage('清空失败', { severity: 'error' });
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHoverField = async (fieldId: string | null) => {
|
||||||
|
setHoveredFieldId(fieldId);
|
||||||
|
if (fieldId) {
|
||||||
|
await sendMessageToContent(MessageAction.HIGHLIGHT_FIELD, { fieldId });
|
||||||
|
} else {
|
||||||
|
await sendMessageToContent(MessageAction.UNHIGHLIGHT_ALL_FIELDS);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateRandomValue = (fieldType: FieldType, fillMode: 'valid' | 'invalid'): string => {
|
||||||
|
const chineseNames = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十'];
|
||||||
|
const englishNames = ['John Doe', 'Jane Smith', 'Bob Wilson', 'Alice Brown'];
|
||||||
|
const domains = ['example.com', 'test.com', 'demo.com'];
|
||||||
|
const specialChars = '!@#$%^&*()';
|
||||||
|
|
||||||
|
switch (fieldType) {
|
||||||
|
case FieldType.NAME:
|
||||||
|
return Math.random() > 0.5
|
||||||
|
? chineseNames[Math.floor(Math.random() * chineseNames.length)]
|
||||||
|
: englishNames[Math.floor(Math.random() * englishNames.length)];
|
||||||
|
case FieldType.EMAIL: {
|
||||||
|
const emailPrefix = Math.random().toString(36).substr(2, 8);
|
||||||
|
const emailDomain = domains[Math.floor(Math.random() * domains.length)];
|
||||||
|
return fillMode === 'valid' ? `${emailPrefix}@${emailDomain}` : `${emailPrefix}example.com`;
|
||||||
|
}
|
||||||
|
case FieldType.PHONE: {
|
||||||
|
const phonePrefixes = ['130', '131', '132', '135', '136', '137', '138', '139'];
|
||||||
|
const phonePrefix = phonePrefixes[Math.floor(Math.random() * phonePrefixes.length)];
|
||||||
|
const phoneSuffix = Math.floor(Math.random() * 100000000)
|
||||||
|
.toString()
|
||||||
|
.padStart(8, '0');
|
||||||
|
return fillMode === 'valid'
|
||||||
|
? phonePrefix + phoneSuffix
|
||||||
|
: phonePrefix + Math.floor(Math.random() * 10000000).toString();
|
||||||
|
}
|
||||||
|
case FieldType.NUMBER:
|
||||||
|
return fillMode === 'valid'
|
||||||
|
? String(Math.floor(Math.random() * 10000))
|
||||||
|
: String(-Math.floor(Math.random() * 10000));
|
||||||
|
case FieldType.DATE: {
|
||||||
|
const days = Math.floor(Math.random() * 365);
|
||||||
|
const date = new Date();
|
||||||
|
date.setDate(date.getDate() + days);
|
||||||
|
return date.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
case FieldType.TEXTarea:
|
||||||
|
return fillMode === 'valid'
|
||||||
|
? '这是一段测试文本,用于填充表单输入区域。'.repeat(3)
|
||||||
|
: specialChars.repeat(100);
|
||||||
|
case FieldType.PASSWORD:
|
||||||
|
return 'Test@123';
|
||||||
|
case FieldType.ID_CARD: {
|
||||||
|
const areaCode = '110101';
|
||||||
|
const year = 1990 + Math.floor(Math.random() * 30);
|
||||||
|
const month = String(1 + Math.floor(Math.random() * 12)).padStart(2, '0');
|
||||||
|
const day = String(1 + Math.floor(Math.random() * 28)).padStart(2, '0');
|
||||||
|
const random = Math.floor(Math.random() * 10000)
|
||||||
|
.toString()
|
||||||
|
.padStart(4, '0');
|
||||||
|
return areaCode + year + month + day + random;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fillMode === 'valid' ? '测试数据' : specialChars.repeat(50);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFieldTypeLabel = (fieldType: FieldType): string => {
|
||||||
|
const labels: Record<FieldType, string> = {
|
||||||
|
[FieldType.TEXT]: '文本',
|
||||||
|
[FieldType.EMAIL]: '邮箱',
|
||||||
|
[FieldType.PHONE]: '手机',
|
||||||
|
[FieldType.NUMBER]: '数字',
|
||||||
|
[FieldType.DATE]: '日期',
|
||||||
|
[FieldType.TEXTarea]: '文本域',
|
||||||
|
[FieldType.RADIO]: '单选',
|
||||||
|
[FieldType.CHECKBOX]: '多选',
|
||||||
|
[FieldType.SELECT]: '下拉',
|
||||||
|
[FieldType.PASSWORD]: '密码',
|
||||||
|
[FieldType.NAME]: '姓名',
|
||||||
|
[FieldType.ID_CARD]: '身份证',
|
||||||
|
[FieldType.UNKNOWN]: '未知',
|
||||||
|
};
|
||||||
|
return labels[fieldType] || '未知';
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedCount = fields.filter((f) => f.isSelected).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column', bgcolor: '#f5f5f5' }}>
|
||||||
|
<Box sx={{ p: 2, bgcolor: 'white', borderBottom: '1px solid #e0e0e0' }}>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1 }}>
|
||||||
|
Dummy Data Pro
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
智能表单填充助手
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ p: 2, bgcolor: 'white', mb: 1 }}>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
startIcon={scanning ? <CircularProgress size={16} color="inherit" /> : <RefreshIcon />}
|
||||||
|
onClick={handleScan}
|
||||||
|
disabled={scanning}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
>
|
||||||
|
{scanning ? '扫描中...' : '扫描表单'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
startIcon={<AutoFixHighIcon />}
|
||||||
|
onClick={handleRefreshAll}
|
||||||
|
disabled={fields.length === 0}
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="success"
|
||||||
|
size="small"
|
||||||
|
onClick={() => setMode('valid')}
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
bgcolor: mode === 'valid' ? '#4caf50' : '#e0e0e0',
|
||||||
|
color: mode === 'valid' ? 'white' : 'text.primary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
有效数据
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="warning"
|
||||||
|
size="small"
|
||||||
|
onClick={() => setMode('invalid')}
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
bgcolor: mode === 'invalid' ? '#ff9800' : '#e0e0e0',
|
||||||
|
color: mode === 'invalid' ? 'white' : 'text.primary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
异常数据
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={fillEmptyOnly}
|
||||||
|
onChange={(e) => setFillEmptyOnly(e.target.checked)}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="仅填充空字段"
|
||||||
|
sx={{ mb: 1 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
startIcon={loading ? <CircularProgress size={16} color="inherit" /> : null}
|
||||||
|
onClick={handleFill}
|
||||||
|
disabled={loading || selectedCount === 0}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
>
|
||||||
|
{loading ? '填充中...' : `确认填充 (${selectedCount})`}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
size="small"
|
||||||
|
startIcon={<DeleteOutlineIcon />}
|
||||||
|
onClick={handleClear}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
清空
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{fields.length > 0 && (
|
||||||
|
<Box sx={{ flex: 1, overflow: 'auto', px: 2, pb: 2 }}>
|
||||||
|
<Box
|
||||||
|
sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}
|
||||||
|
>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
|
||||||
|
字段列表 ({fields.length})
|
||||||
|
</Typography>
|
||||||
|
<Button size="small" onClick={handleSelectAll}>
|
||||||
|
{fields.every((f) => f.isSelected) ? '取消全选' : '全选'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
|
{fields.map((field) => (
|
||||||
|
<Paper
|
||||||
|
key={field.id}
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
p: 1.5,
|
||||||
|
borderRadius: 2,
|
||||||
|
bgcolor: hoveredFieldId === field.id ? '#e3f2fd' : 'white',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: hoveredFieldId === field.id ? '#2196f3' : '#e0e0e0',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => handleHoverField(field.id)}
|
||||||
|
onMouseLeave={() => handleHoverField(null)}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1 }}>
|
||||||
|
<Checkbox
|
||||||
|
size="small"
|
||||||
|
checked={field.isSelected}
|
||||||
|
onChange={() => handleToggleSelect(field.id)}
|
||||||
|
sx={{ p: 0, mt: 0.5 }}
|
||||||
|
/>
|
||||||
|
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
fontWeight: 600,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
maxWidth: '60%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{field.label || field.placeholder || field.name || '未命名字段'}
|
||||||
|
</Typography>
|
||||||
|
<Tooltip title="字段类型">
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
px: 0.5,
|
||||||
|
py: 0.25,
|
||||||
|
bgcolor: '#e0e0e0',
|
||||||
|
borderRadius: 1,
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getFieldTypeLabel(field.fieldType)}
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="高亮显示">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleHoverField(field.id);
|
||||||
|
setTimeout(() => handleHoverField(null), 2000);
|
||||||
|
}}
|
||||||
|
sx={{ p: 0.5 }}
|
||||||
|
>
|
||||||
|
<HighlightAltIcon sx={{ fontSize: 16 }} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
value={field.generatedValue}
|
||||||
|
onChange={(e) => handleEditValue(field.id, e.target.value)}
|
||||||
|
placeholder="生成的数据..."
|
||||||
|
sx={{
|
||||||
|
'& .MuiInputBase-input': {
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip title="刷新此项">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleRefreshField(field.id);
|
||||||
|
}}
|
||||||
|
sx={{ p: 0.5 }}
|
||||||
|
>
|
||||||
|
<RefreshIcon sx={{ fontSize: 18 }} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
{field.value && (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ mt: 0.5, display: 'block' }}
|
||||||
|
>
|
||||||
|
当前值: {field.value}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fields.length === 0 && !scanning && (
|
||||||
|
<Box
|
||||||
|
sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', p: 3 }}
|
||||||
|
>
|
||||||
|
<Alert severity="info" sx={{ width: '100%' }}>
|
||||||
|
点击「扫描表单」按钮开始扫描当前页面的表单字段
|
||||||
|
</Alert>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<GlobalSnackbar {...snackbarProps} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormFillSidePanel;
|
||||||
Generated
+18
-39
@@ -8,9 +8,11 @@
|
|||||||
"name": "testing-tools",
|
"name": "testing-tools",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@faker-js/faker": "^10.4.0",
|
||||||
"@mui/icons-material": "^7.3.8",
|
"@mui/icons-material": "^7.3.8",
|
||||||
"@mui/material": "^7.3.8",
|
"@mui/material": "^7.3.8",
|
||||||
"@webext-core/messaging": "^2.3.0",
|
"@webext-core/messaging": "^2.3.0",
|
||||||
@@ -1447,6 +1449,22 @@
|
|||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@faker-js/faker": {
|
||||||
|
"version": "10.4.0",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/@faker-js/faker/-/faker-10.4.0.tgz",
|
||||||
|
"integrity": "sha512-sDBWI3yLy8EcDzgobvJTWq1MJYzAkQdpjXuPukga9wXonhpMRvd1Izuo2Qgwey2OiEoRIBr35RMU9HJRoOHzpw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fakerjs"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0",
|
||||||
|
"npm": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
@@ -2065,9 +2083,6 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2082,9 +2097,6 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2099,9 +2111,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2116,9 +2125,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2133,9 +2139,6 @@
|
|||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2150,9 +2153,6 @@
|
|||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2167,9 +2167,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2184,9 +2181,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2201,9 +2195,6 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2218,9 +2209,6 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2235,9 +2223,6 @@
|
|||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2252,9 +2237,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2269,9 +2251,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@faker-js/faker": "^10.4.0",
|
||||||
"@mui/icons-material": "^7.3.8",
|
"@mui/icons-material": "^7.3.8",
|
||||||
"@mui/material": "^7.3.8",
|
"@mui/material": "^7.3.8",
|
||||||
"@webext-core/messaging": "^2.3.0",
|
"@webext-core/messaging": "^2.3.0",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function RouterProvider({
|
|||||||
children,
|
children,
|
||||||
defaultRoute = 'dashboard',
|
defaultRoute = 'dashboard',
|
||||||
syncRoute = true,
|
syncRoute = true,
|
||||||
syncKey = 'app/currentRoute'
|
syncKey = 'app/currentRoute',
|
||||||
}: RouterProviderProps) {
|
}: RouterProviderProps) {
|
||||||
const [currentPage, setCurrentPage] = useState<PageType>(defaultRoute);
|
const [currentPage, setCurrentPage] = useState<PageType>(defaultRoute);
|
||||||
const [visiblePages, setVisiblePages] = useState<PageType[]>(getDefaultVisibleRoutes());
|
const [visiblePages, setVisiblePages] = useState<PageType[]>(getDefaultVisibleRoutes());
|
||||||
@@ -124,7 +124,7 @@ export function RouterProvider({
|
|||||||
syncNavigation,
|
syncNavigation,
|
||||||
goBack,
|
goBack,
|
||||||
setVisiblePages,
|
setVisiblePages,
|
||||||
setPageOrder
|
setPageOrder,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
Vendored
+1
@@ -4,6 +4,7 @@ export type PageType =
|
|||||||
| 'storageCleaner'
|
| 'storageCleaner'
|
||||||
| 'openUrl'
|
| 'openUrl'
|
||||||
| 'qrCode'
|
| 'qrCode'
|
||||||
|
| 'formRecognizer'
|
||||||
| 'openUrlViewer';
|
| 'openUrlViewer';
|
||||||
|
|
||||||
export interface StorageSchema {
|
export interface StorageSchema {
|
||||||
|
|||||||
@@ -0,0 +1,984 @@
|
|||||||
|
import { faker, fakerZH_CN } from '@faker-js/faker';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单字段信息接口
|
||||||
|
*/
|
||||||
|
export interface FormFieldInfo {
|
||||||
|
id: string;
|
||||||
|
element: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
||||||
|
fieldType: FieldType;
|
||||||
|
label: string | null;
|
||||||
|
placeholder: string;
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
isSelected: boolean;
|
||||||
|
generatedValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描结果接口
|
||||||
|
*/
|
||||||
|
export interface ScanResult {
|
||||||
|
fields: FormFieldInfo[];
|
||||||
|
totalCount: number;
|
||||||
|
validCount: number;
|
||||||
|
modalContainer: HTMLElement | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据生成器工具类
|
||||||
|
* 用于生成各种类型的测试数据
|
||||||
|
*/
|
||||||
|
export class DummyDataGenerator {
|
||||||
|
/**
|
||||||
|
* 生成随机中文姓名
|
||||||
|
*/
|
||||||
|
static generateChineseName(): string {
|
||||||
|
return fakerZH_CN.person.fullName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机英文姓名
|
||||||
|
*/
|
||||||
|
static generateEnglishName(): string {
|
||||||
|
return faker.person.fullName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机手机号
|
||||||
|
*/
|
||||||
|
static generatePhoneNumber(): string {
|
||||||
|
return fakerZH_CN.phone.number();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成有效邮箱
|
||||||
|
*/
|
||||||
|
static generateValidEmail(): string {
|
||||||
|
return faker.internet.email();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成无效邮箱
|
||||||
|
*/
|
||||||
|
static generateInvalidEmail(): string {
|
||||||
|
const invalidEmails = [
|
||||||
|
'testexample.com', // 缺失 @
|
||||||
|
'test@@example.com', // 多个 @
|
||||||
|
'test@', // 缺失域名
|
||||||
|
'test@.com', // 域名为空
|
||||||
|
'test@example', // 缺失顶级域名
|
||||||
|
];
|
||||||
|
|
||||||
|
return invalidEmails[Math.floor(Math.random() * invalidEmails.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成短文本
|
||||||
|
*/
|
||||||
|
static generateShortText(): string {
|
||||||
|
return faker.lorem.sentence({ min: 3, max: 6 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成长文本
|
||||||
|
*/
|
||||||
|
static generateLongText(): string {
|
||||||
|
return faker.lorem.paragraphs(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成边界测试文本
|
||||||
|
*/
|
||||||
|
static generateBoundaryText(): string {
|
||||||
|
const specialChars = '!@#$%^&*()_+[]{}|;:,.<>?';
|
||||||
|
const emoji = '😀😃😄😁😆😅😂🤣';
|
||||||
|
let text = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
text += specialChars + emoji + '测试文本';
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机数字
|
||||||
|
*/
|
||||||
|
static generateNumber(): number {
|
||||||
|
return faker.number.int(10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机浮点数
|
||||||
|
*/
|
||||||
|
static generateFloat(): number {
|
||||||
|
return faker.number.float({ max: 10000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机负数
|
||||||
|
*/
|
||||||
|
static generateNegativeNumber(): number {
|
||||||
|
return -faker.number.int(10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机日期
|
||||||
|
*/
|
||||||
|
static generateDate(): string {
|
||||||
|
return faker.date.recent({ days: 365 }).toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成过去的日期
|
||||||
|
*/
|
||||||
|
static generatePastDate(): string {
|
||||||
|
return faker.date.past({ years: 1 }).toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成未来的日期
|
||||||
|
*/
|
||||||
|
static generateFutureDate(): string {
|
||||||
|
return faker.date.future({ years: 1 }).toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机身份证号
|
||||||
|
*/
|
||||||
|
static generateIdCard(): string {
|
||||||
|
const areaCodes = [
|
||||||
|
'110101',
|
||||||
|
'110102',
|
||||||
|
'110103',
|
||||||
|
'110104',
|
||||||
|
'110105',
|
||||||
|
'310101',
|
||||||
|
'310102',
|
||||||
|
'310103',
|
||||||
|
'310104',
|
||||||
|
'310105',
|
||||||
|
'440101',
|
||||||
|
'440102',
|
||||||
|
'440103',
|
||||||
|
'440104',
|
||||||
|
'440105',
|
||||||
|
];
|
||||||
|
const areaCode = areaCodes[Math.floor(Math.random() * areaCodes.length)];
|
||||||
|
const year = (1950 + Math.floor(Math.random() * 50)).toString();
|
||||||
|
const month = String(1 + Math.floor(Math.random() * 12)).padStart(2, '0');
|
||||||
|
const day = String(1 + Math.floor(Math.random() * 28)).padStart(2, '0');
|
||||||
|
const random = Math.floor(Math.random() * 10000)
|
||||||
|
.toString()
|
||||||
|
.padStart(4, '0');
|
||||||
|
|
||||||
|
return areaCode + year + month + day + random;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单字段类型
|
||||||
|
*/
|
||||||
|
export enum FieldType {
|
||||||
|
TEXT = 'text',
|
||||||
|
EMAIL = 'email',
|
||||||
|
PHONE = 'phone',
|
||||||
|
NUMBER = 'number',
|
||||||
|
DATE = 'date',
|
||||||
|
TEXTarea = 'textarea',
|
||||||
|
RADIO = 'radio',
|
||||||
|
CHECKBOX = 'checkbox',
|
||||||
|
SELECT = 'select',
|
||||||
|
PASSWORD = 'password',
|
||||||
|
NAME = 'name',
|
||||||
|
ID_CARD = 'id_card',
|
||||||
|
UNKNOWN = 'unknown',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 填充模式
|
||||||
|
*/
|
||||||
|
export enum FillMode {
|
||||||
|
VALID = 'valid',
|
||||||
|
INVALID = 'invalid',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 识别表单字段类型
|
||||||
|
*/
|
||||||
|
export function recognizeFieldType(
|
||||||
|
element: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement,
|
||||||
|
): FieldType {
|
||||||
|
// 基于 type 属性识别
|
||||||
|
if (element instanceof HTMLInputElement) {
|
||||||
|
switch (element.type) {
|
||||||
|
case 'email':
|
||||||
|
return FieldType.EMAIL;
|
||||||
|
case 'tel':
|
||||||
|
return FieldType.PHONE;
|
||||||
|
case 'number':
|
||||||
|
return FieldType.NUMBER;
|
||||||
|
case 'date':
|
||||||
|
return FieldType.DATE;
|
||||||
|
case 'password':
|
||||||
|
return FieldType.PASSWORD;
|
||||||
|
case 'radio':
|
||||||
|
return FieldType.RADIO;
|
||||||
|
case 'checkbox':
|
||||||
|
return FieldType.CHECKBOX;
|
||||||
|
case 'text':
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于 name 或 id 识别
|
||||||
|
const name = (element.name || element.id || '').toLowerCase();
|
||||||
|
if (name.includes('email') || name.includes('mail')) {
|
||||||
|
return FieldType.EMAIL;
|
||||||
|
}
|
||||||
|
if (name.includes('phone') || name.includes('tel') || name.includes('mobile')) {
|
||||||
|
return FieldType.PHONE;
|
||||||
|
}
|
||||||
|
if (name.includes('name') || name.includes('user') || name.includes('username')) {
|
||||||
|
return FieldType.NAME;
|
||||||
|
}
|
||||||
|
if (name.includes('id') || name.includes('card') || name.includes('identity')) {
|
||||||
|
return FieldType.ID_CARD;
|
||||||
|
}
|
||||||
|
if (name.includes('password') || name.includes('pass')) {
|
||||||
|
return FieldType.PASSWORD;
|
||||||
|
}
|
||||||
|
if (name.includes('number') || name.includes('num')) {
|
||||||
|
return FieldType.NUMBER;
|
||||||
|
}
|
||||||
|
if (name.includes('date') || name.includes('time')) {
|
||||||
|
return FieldType.DATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于 placeholder 识别
|
||||||
|
if ('placeholder' in element) {
|
||||||
|
const placeholder = (element.placeholder || '').toLowerCase();
|
||||||
|
if (placeholder.includes('email') || placeholder.includes('mail')) {
|
||||||
|
return FieldType.EMAIL;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
placeholder.includes('phone') ||
|
||||||
|
placeholder.includes('tel') ||
|
||||||
|
placeholder.includes('mobile')
|
||||||
|
) {
|
||||||
|
return FieldType.PHONE;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
placeholder.includes('name') ||
|
||||||
|
placeholder.includes('user') ||
|
||||||
|
placeholder.includes('username')
|
||||||
|
) {
|
||||||
|
return FieldType.NAME;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
placeholder.includes('id') ||
|
||||||
|
placeholder.includes('card') ||
|
||||||
|
placeholder.includes('identity')
|
||||||
|
) {
|
||||||
|
return FieldType.ID_CARD;
|
||||||
|
}
|
||||||
|
if (placeholder.includes('password') || placeholder.includes('pass')) {
|
||||||
|
return FieldType.PASSWORD;
|
||||||
|
}
|
||||||
|
if (placeholder.includes('number') || placeholder.includes('num')) {
|
||||||
|
return FieldType.NUMBER;
|
||||||
|
}
|
||||||
|
if (placeholder.includes('date') || placeholder.includes('time')) {
|
||||||
|
return FieldType.DATE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于标签识别
|
||||||
|
const label = getFieldLabel(element);
|
||||||
|
if (label) {
|
||||||
|
const labelText = label.toLowerCase();
|
||||||
|
if (labelText.includes('邮箱') || labelText.includes('email') || labelText.includes('mail')) {
|
||||||
|
return FieldType.EMAIL;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
labelText.includes('手机') ||
|
||||||
|
labelText.includes('电话') ||
|
||||||
|
labelText.includes('tel') ||
|
||||||
|
labelText.includes('mobile')
|
||||||
|
) {
|
||||||
|
return FieldType.PHONE;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
labelText.includes('姓名') ||
|
||||||
|
labelText.includes('名字') ||
|
||||||
|
labelText.includes('name') ||
|
||||||
|
labelText.includes('user') ||
|
||||||
|
labelText.includes('username')
|
||||||
|
) {
|
||||||
|
return FieldType.NAME;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
labelText.includes('身份证') ||
|
||||||
|
labelText.includes('id') ||
|
||||||
|
labelText.includes('card') ||
|
||||||
|
labelText.includes('identity')
|
||||||
|
) {
|
||||||
|
return FieldType.ID_CARD;
|
||||||
|
}
|
||||||
|
if (labelText.includes('密码') || labelText.includes('pass')) {
|
||||||
|
return FieldType.PASSWORD;
|
||||||
|
}
|
||||||
|
if (labelText.includes('数字') || labelText.includes('number') || labelText.includes('num')) {
|
||||||
|
return FieldType.NUMBER;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
labelText.includes('日期') ||
|
||||||
|
labelText.includes('时间') ||
|
||||||
|
labelText.includes('date') ||
|
||||||
|
labelText.includes('time')
|
||||||
|
) {
|
||||||
|
return FieldType.DATE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于元素类型识别
|
||||||
|
if (element instanceof HTMLTextAreaElement) {
|
||||||
|
return FieldType.TEXTarea;
|
||||||
|
}
|
||||||
|
if (element instanceof HTMLSelectElement) {
|
||||||
|
return FieldType.SELECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FieldType.TEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取字段的标签
|
||||||
|
*/
|
||||||
|
function getFieldLabel(element: HTMLElement): string | null {
|
||||||
|
// 查找相邻的 label 元素
|
||||||
|
const labels = document.querySelectorAll('label');
|
||||||
|
for (const label of labels) {
|
||||||
|
const forAttr = label.getAttribute('for');
|
||||||
|
if (forAttr === element.id) {
|
||||||
|
return label.textContent || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找父元素中的 label
|
||||||
|
let parent = element.parentElement;
|
||||||
|
while (parent) {
|
||||||
|
if (parent.tagName === 'LABEL') {
|
||||||
|
return parent.textContent || null;
|
||||||
|
}
|
||||||
|
parent = parent.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 填充表单字段
|
||||||
|
*/
|
||||||
|
export function fillField(
|
||||||
|
element: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement,
|
||||||
|
mode: FillMode,
|
||||||
|
): void {
|
||||||
|
const fieldType = recognizeFieldType(element);
|
||||||
|
let value: string | number | boolean = '';
|
||||||
|
|
||||||
|
switch (fieldType) {
|
||||||
|
case FieldType.NAME:
|
||||||
|
value =
|
||||||
|
Math.random() > 0.5
|
||||||
|
? DummyDataGenerator.generateChineseName()
|
||||||
|
: DummyDataGenerator.generateEnglishName();
|
||||||
|
break;
|
||||||
|
case FieldType.EMAIL:
|
||||||
|
value =
|
||||||
|
mode === FillMode.VALID
|
||||||
|
? DummyDataGenerator.generateValidEmail()
|
||||||
|
: DummyDataGenerator.generateInvalidEmail();
|
||||||
|
break;
|
||||||
|
case FieldType.PHONE:
|
||||||
|
value = DummyDataGenerator.generatePhoneNumber();
|
||||||
|
break;
|
||||||
|
case FieldType.NUMBER:
|
||||||
|
value =
|
||||||
|
mode === FillMode.VALID
|
||||||
|
? DummyDataGenerator.generateNumber()
|
||||||
|
: DummyDataGenerator.generateNegativeNumber();
|
||||||
|
break;
|
||||||
|
case FieldType.DATE:
|
||||||
|
value = DummyDataGenerator.generateDate();
|
||||||
|
break;
|
||||||
|
case FieldType.TEXTarea:
|
||||||
|
value =
|
||||||
|
mode === FillMode.VALID
|
||||||
|
? DummyDataGenerator.generateLongText()
|
||||||
|
: DummyDataGenerator.generateBoundaryText();
|
||||||
|
break;
|
||||||
|
case FieldType.PASSWORD:
|
||||||
|
value = 'password123';
|
||||||
|
break;
|
||||||
|
case FieldType.ID_CARD:
|
||||||
|
value = DummyDataGenerator.generateIdCard();
|
||||||
|
break;
|
||||||
|
case FieldType.TEXT:
|
||||||
|
default:
|
||||||
|
value =
|
||||||
|
mode === FillMode.VALID
|
||||||
|
? DummyDataGenerator.generateShortText()
|
||||||
|
: DummyDataGenerator.generateBoundaryText();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充值
|
||||||
|
if (element instanceof HTMLInputElement) {
|
||||||
|
if (element.type === 'checkbox' || element.type === 'radio') {
|
||||||
|
element.checked = Math.random() > 0.5;
|
||||||
|
} else {
|
||||||
|
element.value = String(value);
|
||||||
|
}
|
||||||
|
} else if (element instanceof HTMLTextAreaElement) {
|
||||||
|
element.value = String(value);
|
||||||
|
} else if (element instanceof HTMLSelectElement) {
|
||||||
|
// 随机选择一个非禁用的选项
|
||||||
|
const options = Array.from(element.options).filter((option) => !option.disabled);
|
||||||
|
if (options.length > 0) {
|
||||||
|
const randomIndex = Math.floor(Math.random() * options.length);
|
||||||
|
element.selectedIndex = randomIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发事件,确保前端框架能够监听到变化
|
||||||
|
triggerEvents(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发事件
|
||||||
|
*/
|
||||||
|
function triggerEvents(element: HTMLElement): void {
|
||||||
|
// 触发 input 事件
|
||||||
|
const inputEvent = new Event('input', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
element.dispatchEvent(inputEvent);
|
||||||
|
|
||||||
|
// 触发 change 事件
|
||||||
|
const changeEvent = new Event('change', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
element.dispatchEvent(changeEvent);
|
||||||
|
|
||||||
|
// 触发 blur 事件
|
||||||
|
const blurEvent = new Event('blur', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
element.dispatchEvent(blurEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断元素是否真正可见且允许输入
|
||||||
|
*/
|
||||||
|
function isElementVisible(element: HTMLElement): boolean {
|
||||||
|
// 1. 排除隐藏域、禁用和只读状态
|
||||||
|
if (element instanceof HTMLInputElement) {
|
||||||
|
if (element.type === 'hidden' || element.disabled || element.readOnly) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (element instanceof HTMLTextAreaElement) {
|
||||||
|
if (element.disabled || element.readOnly) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (element instanceof HTMLSelectElement) {
|
||||||
|
if (element.disabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查空间尺寸 (能有效过滤大部分 display: none 或未渲染完毕的组件)
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
if (rect.width === 0 || rect.height === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查计算样式 (兜底检查 css 隐藏手段)
|
||||||
|
const style = window.getComputedStyle(element);
|
||||||
|
if (
|
||||||
|
style.display === 'none' ||
|
||||||
|
style.visibility === 'hidden' ||
|
||||||
|
style.opacity === '0' ||
|
||||||
|
style.visibility === 'collapse'
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Z轴穿透验证 (Raycasting)
|
||||||
|
* 通过 document.elementFromPoint(x, y) 向元素中心点发射坐标射线
|
||||||
|
* 如果获取到的顶层元素不是输入框本身或其子元素,则判定为"视觉遮挡"
|
||||||
|
*/
|
||||||
|
function isElementNotObscured(element: HTMLElement): boolean {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
|
||||||
|
// 计算元素中心点坐标
|
||||||
|
const centerX = rect.left + rect.width / 2;
|
||||||
|
const centerY = rect.top + rect.height / 2;
|
||||||
|
|
||||||
|
// 向元素中心点发射坐标射线,获取最顶层的元素
|
||||||
|
const topElement = document.elementFromPoint(centerX, centerY);
|
||||||
|
|
||||||
|
if (!topElement) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查获取到的顶层元素是否是输入框本身或其子元素
|
||||||
|
return element.contains(topElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断元素是否在视口范围内
|
||||||
|
*/
|
||||||
|
function isElementInViewport(element: HTMLElement): boolean {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
|
||||||
|
return (
|
||||||
|
rect.top >= 0 &&
|
||||||
|
rect.left >= 0 &&
|
||||||
|
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||||
|
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断元素是否真正可见且允许输入(漏斗式检测)
|
||||||
|
*/
|
||||||
|
function isElementValidForFill(element: HTMLElement): boolean {
|
||||||
|
// 1. 基础过滤:排除隐藏域、禁用和只读状态
|
||||||
|
if (element instanceof HTMLInputElement) {
|
||||||
|
if (element.type === 'hidden' || element.disabled || element.readOnly) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (element instanceof HTMLTextAreaElement) {
|
||||||
|
if (element.disabled || element.readOnly) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (element instanceof HTMLSelectElement) {
|
||||||
|
if (element.disabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 空间尺寸检测:排除宽高为0的元素
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
if (rect.width === 0 || rect.height === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. CSS样式检测:排除通过CSS隐藏的元素
|
||||||
|
const style = window.getComputedStyle(element);
|
||||||
|
if (
|
||||||
|
style.display === 'none' ||
|
||||||
|
style.visibility === 'hidden' ||
|
||||||
|
style.opacity === '0' ||
|
||||||
|
style.visibility === 'collapse'
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 视口检测:只处理当前屏幕滚动范围内的元素
|
||||||
|
if (!isElementInViewport(element)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Z轴穿透验证(最后一步,最耗时,放最后)
|
||||||
|
if (!isElementNotObscured(element)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找最上层的弹窗容器
|
||||||
|
*/
|
||||||
|
function findActiveModalContainer(): HTMLElement | null {
|
||||||
|
const modalSelectors = [
|
||||||
|
'.ant-modal-content',
|
||||||
|
'.el-dialog',
|
||||||
|
'[role="dialog"]',
|
||||||
|
'.MuiDialog-content',
|
||||||
|
'.modal-content',
|
||||||
|
'.dialog-content',
|
||||||
|
'.popup-content',
|
||||||
|
];
|
||||||
|
|
||||||
|
let topModal: HTMLElement | null = null;
|
||||||
|
let highestZIndex = 0;
|
||||||
|
|
||||||
|
modalSelectors.forEach((selector) => {
|
||||||
|
const modals = document.querySelectorAll(selector);
|
||||||
|
modals.forEach((modal) => {
|
||||||
|
if (modal instanceof HTMLElement) {
|
||||||
|
const style = window.getComputedStyle(modal);
|
||||||
|
const zIndex = parseInt(style.zIndex, 10) || 0;
|
||||||
|
if (zIndex > highestZIndex && isElementVisible(modal)) {
|
||||||
|
highestZIndex = zIndex;
|
||||||
|
topModal = modal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return topModal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描页面中所有可见的表单字段
|
||||||
|
*/
|
||||||
|
export function scanFormFields(): ScanResult {
|
||||||
|
const inputs = document.querySelectorAll('input, textarea, select');
|
||||||
|
const fields: FormFieldInfo[] = [];
|
||||||
|
const modalContainer = findActiveModalContainer();
|
||||||
|
|
||||||
|
inputs.forEach((input) => {
|
||||||
|
if (
|
||||||
|
input instanceof HTMLInputElement ||
|
||||||
|
input instanceof HTMLTextAreaElement ||
|
||||||
|
input instanceof HTMLSelectElement
|
||||||
|
) {
|
||||||
|
if (isElementValidForFill(input)) {
|
||||||
|
const fieldType = recognizeFieldType(input);
|
||||||
|
const label = getFieldLabel(input);
|
||||||
|
const placeholder = 'placeholder' in input ? input.placeholder : '';
|
||||||
|
const name = input.name || input.id || '';
|
||||||
|
|
||||||
|
fields.push({
|
||||||
|
id: `field-${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
element: input,
|
||||||
|
fieldType,
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
name,
|
||||||
|
value: input.value,
|
||||||
|
isSelected: true,
|
||||||
|
generatedValue: generateValueByFieldType(fieldType, FillMode.VALID),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
fields,
|
||||||
|
totalCount: fields.length,
|
||||||
|
validCount: fields.filter((f) => f.isSelected).length,
|
||||||
|
modalContainer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据字段类型生成对应的值
|
||||||
|
*/
|
||||||
|
export function generateValueByFieldType(fieldType: FieldType, mode: FillMode): string {
|
||||||
|
switch (fieldType) {
|
||||||
|
case FieldType.NAME:
|
||||||
|
return Math.random() > 0.5
|
||||||
|
? DummyDataGenerator.generateChineseName()
|
||||||
|
: DummyDataGenerator.generateEnglishName();
|
||||||
|
case FieldType.EMAIL:
|
||||||
|
return mode === FillMode.VALID
|
||||||
|
? DummyDataGenerator.generateValidEmail()
|
||||||
|
: DummyDataGenerator.generateInvalidEmail();
|
||||||
|
case FieldType.PHONE:
|
||||||
|
return DummyDataGenerator.generatePhoneNumber();
|
||||||
|
case FieldType.NUMBER:
|
||||||
|
return String(
|
||||||
|
mode === FillMode.VALID
|
||||||
|
? DummyDataGenerator.generateNumber()
|
||||||
|
: DummyDataGenerator.generateNegativeNumber(),
|
||||||
|
);
|
||||||
|
case FieldType.DATE:
|
||||||
|
return DummyDataGenerator.generateDate();
|
||||||
|
case FieldType.TEXTarea:
|
||||||
|
return mode === FillMode.VALID
|
||||||
|
? DummyDataGenerator.generateLongText()
|
||||||
|
: DummyDataGenerator.generateBoundaryText();
|
||||||
|
case FieldType.PASSWORD:
|
||||||
|
return 'password123';
|
||||||
|
case FieldType.ID_CARD:
|
||||||
|
return DummyDataGenerator.generateIdCard();
|
||||||
|
case FieldType.TEXT:
|
||||||
|
default:
|
||||||
|
return mode === FillMode.VALID
|
||||||
|
? DummyDataGenerator.generateShortText()
|
||||||
|
: DummyDataGenerator.generateBoundaryText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 框架级数据注入器
|
||||||
|
* 破解 React/Vue 的 input setter 劫持
|
||||||
|
*/
|
||||||
|
function setInputValue(
|
||||||
|
element: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement,
|
||||||
|
value: string,
|
||||||
|
): void {
|
||||||
|
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
||||||
|
element instanceof HTMLInputElement
|
||||||
|
? window.HTMLInputElement.prototype
|
||||||
|
: element instanceof HTMLTextAreaElement
|
||||||
|
? window.HTMLTextAreaElement.prototype
|
||||||
|
: window.HTMLSelectElement.prototype,
|
||||||
|
'value',
|
||||||
|
)?.set;
|
||||||
|
|
||||||
|
if (nativeInputValueSetter) {
|
||||||
|
nativeInputValueSetter.call(element, value);
|
||||||
|
} else {
|
||||||
|
element.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 填充指定字段(使用框架级注入)
|
||||||
|
*/
|
||||||
|
export function fillFieldWithInjector(
|
||||||
|
element: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement,
|
||||||
|
value: string,
|
||||||
|
): void {
|
||||||
|
if (element instanceof HTMLInputElement) {
|
||||||
|
if (element.type === 'checkbox' || element.type === 'radio') {
|
||||||
|
element.checked = value === 'true' || value === '1';
|
||||||
|
} else {
|
||||||
|
setInputValue(element, value);
|
||||||
|
}
|
||||||
|
} else if (element instanceof HTMLTextAreaElement) {
|
||||||
|
setInputValue(element, value);
|
||||||
|
} else if (element instanceof HTMLSelectElement) {
|
||||||
|
// 查找匹配的选项
|
||||||
|
const options = Array.from(element.options);
|
||||||
|
const matchingOption = options.find((opt) => opt.value === value || opt.text === value);
|
||||||
|
if (matchingOption) {
|
||||||
|
element.value = matchingOption.value;
|
||||||
|
} else if (options.length > 0) {
|
||||||
|
element.selectedIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerEvents(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量填充选中的字段
|
||||||
|
*/
|
||||||
|
export function fillSelectedFields(fields: FormFieldInfo[], mode: FillMode): number {
|
||||||
|
let filledCount = 0;
|
||||||
|
|
||||||
|
fields.forEach((field) => {
|
||||||
|
if (field.isSelected) {
|
||||||
|
const value = field.generatedValue || generateValueByFieldType(field.fieldType, mode);
|
||||||
|
fillFieldWithInjector(field.element, value);
|
||||||
|
filledCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return filledCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 高亮指定字段
|
||||||
|
*/
|
||||||
|
export function highlightField(element: HTMLElement): void {
|
||||||
|
const originalStyle =
|
||||||
|
element.getAttribute('data-original-style') || element.getAttribute('style') || '';
|
||||||
|
element.setAttribute('data-original-style', originalStyle);
|
||||||
|
|
||||||
|
element.style.outline = '3px solid #2196f3';
|
||||||
|
element.style.outlineOffset = '2px';
|
||||||
|
element.style.transition = 'outline 0.2s ease-in-out';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消高亮指定字段
|
||||||
|
*/
|
||||||
|
export function unhighlightField(element: HTMLElement): void {
|
||||||
|
const originalStyle = element.getAttribute('data-original-style') || '';
|
||||||
|
if (originalStyle) {
|
||||||
|
element.setAttribute('style', originalStyle);
|
||||||
|
element.removeAttribute('data-original-style');
|
||||||
|
} else {
|
||||||
|
element.style.outline = '';
|
||||||
|
element.style.outlineOffset = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 高亮所有指定字段
|
||||||
|
*/
|
||||||
|
export function highlightAllFields(fieldIds: string[], fields: FormFieldInfo[]): void {
|
||||||
|
fieldIds.forEach((id) => {
|
||||||
|
const field = fields.find((f) => f.id === id);
|
||||||
|
if (field) {
|
||||||
|
highlightField(field.element);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消高亮所有字段
|
||||||
|
*/
|
||||||
|
export function unhighlightAllFields(fields: FormFieldInfo[]): void {
|
||||||
|
fields.forEach((field) => {
|
||||||
|
unhighlightField(field.element);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 填充所有表单字段
|
||||||
|
*/
|
||||||
|
export function fillAllFields(mode: FillMode, includeHidden: boolean = false): void {
|
||||||
|
const inputs = document.querySelectorAll('input, textarea, select');
|
||||||
|
|
||||||
|
inputs.forEach((element) => {
|
||||||
|
if (
|
||||||
|
element instanceof HTMLInputElement ||
|
||||||
|
element instanceof HTMLTextAreaElement ||
|
||||||
|
element instanceof HTMLSelectElement
|
||||||
|
) {
|
||||||
|
if (!isElementValidForFill(element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!includeHidden) {
|
||||||
|
if (element instanceof HTMLInputElement && element.type === 'hidden') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldType = recognizeFieldType(element);
|
||||||
|
const value = generateValueByFieldType(fieldType, mode);
|
||||||
|
fillFieldWithInjector(element, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 填充指定容器内的表单字段
|
||||||
|
*/
|
||||||
|
export function fillFieldsInContainer(
|
||||||
|
mode: FillMode,
|
||||||
|
container: HTMLElement,
|
||||||
|
includeHidden: boolean = false,
|
||||||
|
): void {
|
||||||
|
const inputs = container.querySelectorAll('input, textarea, select');
|
||||||
|
|
||||||
|
inputs.forEach((element) => {
|
||||||
|
if (
|
||||||
|
element instanceof HTMLInputElement ||
|
||||||
|
element instanceof HTMLTextAreaElement ||
|
||||||
|
element instanceof HTMLSelectElement
|
||||||
|
) {
|
||||||
|
if (!isElementValidForFill(element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!includeHidden) {
|
||||||
|
if (element instanceof HTMLInputElement && element.type === 'hidden') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldType = recognizeFieldType(element);
|
||||||
|
const value = generateValueByFieldType(fieldType, mode);
|
||||||
|
fillFieldWithInjector(element, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 填充弹窗内的表单字段
|
||||||
|
*/
|
||||||
|
export function fillFieldsInActiveModal(mode: FillMode, includeHidden: boolean = false): boolean {
|
||||||
|
const modal = findActiveModalContainer();
|
||||||
|
if (modal) {
|
||||||
|
fillFieldsInContainer(mode, modal, includeHidden);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有表单字段
|
||||||
|
*/
|
||||||
|
export function clearAllFields(): void {
|
||||||
|
const inputs = document.querySelectorAll('input, textarea, select');
|
||||||
|
|
||||||
|
inputs.forEach((element) => {
|
||||||
|
if (
|
||||||
|
element instanceof HTMLInputElement ||
|
||||||
|
element instanceof HTMLTextAreaElement ||
|
||||||
|
element instanceof HTMLSelectElement
|
||||||
|
) {
|
||||||
|
if (!isElementValidForFill(element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element instanceof HTMLInputElement) {
|
||||||
|
if (element.type === 'checkbox' || element.type === 'radio') {
|
||||||
|
element.checked = false;
|
||||||
|
} else {
|
||||||
|
setInputValue(element, '');
|
||||||
|
}
|
||||||
|
} else if (element instanceof HTMLTextAreaElement) {
|
||||||
|
setInputValue(element, '');
|
||||||
|
} else if (element instanceof HTMLSelectElement) {
|
||||||
|
element.selectedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerEvents(element);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空指定容器内的表单字段
|
||||||
|
*/
|
||||||
|
export function clearFieldsInContainer(container: HTMLElement): void {
|
||||||
|
const inputs = container.querySelectorAll('input, textarea, select');
|
||||||
|
|
||||||
|
inputs.forEach((element) => {
|
||||||
|
if (
|
||||||
|
element instanceof HTMLInputElement ||
|
||||||
|
element instanceof HTMLTextAreaElement ||
|
||||||
|
element instanceof HTMLSelectElement
|
||||||
|
) {
|
||||||
|
if (!isElementValidForFill(element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element instanceof HTMLInputElement) {
|
||||||
|
if (element.type === 'checkbox' || element.type === 'radio') {
|
||||||
|
element.checked = false;
|
||||||
|
} else {
|
||||||
|
setInputValue(element, '');
|
||||||
|
}
|
||||||
|
} else if (element instanceof HTMLTextAreaElement) {
|
||||||
|
setInputValue(element, '');
|
||||||
|
} else if (element instanceof HTMLSelectElement) {
|
||||||
|
element.selectedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerEvents(element);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { FormFieldInfo, FillMode } from './dummyDataGenerator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息动作类型
|
||||||
|
*/
|
||||||
|
export enum MessageAction {
|
||||||
|
// 表单相关操作
|
||||||
|
SCAN_FORM_FIELDS = 'scanFormFields',
|
||||||
|
FILL_VALID_DATA = 'fillValidData',
|
||||||
|
FILL_INVALID_DATA = 'fillInvalidData',
|
||||||
|
FILL_SELECTED_FIELDS = 'fillSelectedFields',
|
||||||
|
CLEAR_ALL_FIELDS = 'clearAllFields',
|
||||||
|
|
||||||
|
// 字段高亮操作
|
||||||
|
HIGHLIGHT_FIELD = 'highlightField',
|
||||||
|
UNHIGHLIGHT_FIELD = 'unhighlightField',
|
||||||
|
HIGHLIGHT_ALL_FIELDS = 'highlightAllFields',
|
||||||
|
UNHIGHLIGHT_ALL_FIELDS = 'unhighlightAllFields',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息载荷接口
|
||||||
|
*/
|
||||||
|
export interface MessagePayload {
|
||||||
|
action: MessageAction;
|
||||||
|
fields?: Omit<FormFieldInfo, 'element'>[];
|
||||||
|
mode?: FillMode;
|
||||||
|
includeHidden?: boolean;
|
||||||
|
fieldId?: string;
|
||||||
|
fieldIds?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息响应接口
|
||||||
|
*/
|
||||||
|
export interface MessageResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
fields?: Omit<FormFieldInfo, 'element'>[];
|
||||||
|
totalCount?: number;
|
||||||
|
validCount?: number;
|
||||||
|
hasModal?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息到内容脚本
|
||||||
|
*/
|
||||||
|
export async function sendMessageToContent(
|
||||||
|
action: MessageAction,
|
||||||
|
payload?: Omit<MessagePayload, 'action'>,
|
||||||
|
): Promise<MessageResponse> {
|
||||||
|
try {
|
||||||
|
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
|
if (!tab?.id) {
|
||||||
|
return { success: false, message: '无法获取当前标签页' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.tabs.sendMessage(tab.id!, { action, ...payload }, (response) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
console.error('消息发送失败:', chrome.runtime.lastError);
|
||||||
|
resolve({ success: false, message: '无法连接到页面,请确保页面已加载' });
|
||||||
|
} else {
|
||||||
|
resolve((response as MessageResponse) || { success: false, message: '未收到响应' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取标签页失败:', error);
|
||||||
|
return { success: false, message: '无法获取当前标签页' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注入内容脚本
|
||||||
|
*/
|
||||||
|
export async function injectContentScript(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
|
if (!tab?.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试注入内容脚本
|
||||||
|
await chrome.scripting.executeScript({
|
||||||
|
target: { tabId: tab.id },
|
||||||
|
files: ['/content-scripts/content.js'],
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('注入内容脚本失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { defineExtensionMessaging } from '@webext-core/messaging';
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
||||||
interface ProtocolMap {
|
|
||||||
// Placeholder - 扩展消息协议
|
|
||||||
}
|
|
||||||
|
|
||||||
export const { sendMessage, onMessage } = defineExtensionMessaging<ProtocolMap>();
|
|
||||||
Reference in New Issue
Block a user