diff --git a/components/RouterContainer.tsx b/components/RouterContainer.tsx
index 0859fc9..2cd0e94 100644
--- a/components/RouterContainer.tsx
+++ b/components/RouterContainer.tsx
@@ -1,5 +1,5 @@
import { Box } from '@mui/material';
-import { ROUTES } from '@/config/routes';
+import { ROUTES, getEntryPointType } from '@/config/routes';
import { useRouter } from '@/providers/RouterProvider';
import { useMemo } from 'react';
@@ -10,11 +10,16 @@ export default function RouterContainer() {
return currentPage === 'dashboard' ? 'page-transition-dashboard' : 'page-transition-enter';
}, [currentPage]);
+ const entryPointType = useMemo(() => {
+ return getEntryPointType();
+ }, []);
+
if (!isLoaded) {
return
Loading...
;
}
- const currentRoute = ROUTES.find(route => route.key === currentPage);
+ const currentRoute = ROUTES.find((route) => route.key === currentPage);
+ const Component = currentRoute ? currentRoute.components[entryPointType] : null;
return (
- {currentRoute && }
+ {Component && }
);
}
diff --git a/config/routes.ts b/config/routes.ts
index f578e38..aa63a47 100644
--- a/config/routes.ts
+++ b/config/routes.ts
@@ -5,12 +5,18 @@ import StorageCleanerPage from '@/entrypoints/popup/pages/StorageCleanerPage';
import OpenUrlPage from '@/entrypoints/popup/pages/OpenUrlPage';
import OpenUrlViewerPage from '@/entrypoints/popup/pages/OpenUrlViewerPage';
import QrCodePage from '@/entrypoints/popup/pages/QrCodePage';
+import FormRecognizerPage from '@/entrypoints/popup/pages/FormRecognizerPage';
+import FormFillSidePanel from '@/entrypoints/sidepanel/pages/FormFillSidePanel';
export interface RouteConfig {
key: PageType;
label: string;
defaultVisible: boolean;
- component: React.ComponentType;
+ components: {
+ popup: React.ComponentType;
+ sidepanel: React.ComponentType;
+ detached: React.ComponentType;
+ };
}
export const ROUTES: RouteConfig[] = [
@@ -18,52 +24,99 @@ export const ROUTES: RouteConfig[] = [
key: 'dashboard',
label: 'Dashboard',
defaultVisible: true,
- component: DashboardPage,
+ components: {
+ popup: DashboardPage,
+ sidepanel: DashboardPage,
+ detached: DashboardPage,
+ },
},
{
key: 'timestamp',
label: '时间戳',
defaultVisible: true,
- component: TimestampPage,
+ components: {
+ popup: TimestampPage,
+ sidepanel: TimestampPage,
+ detached: TimestampPage,
+ },
},
{
key: 'storageCleaner',
label: '存储清理',
defaultVisible: true,
- component: StorageCleanerPage,
+ components: {
+ popup: StorageCleanerPage,
+ sidepanel: StorageCleanerPage,
+ detached: StorageCleanerPage,
+ },
},
{
key: 'openUrl',
label: 'Open Url',
defaultVisible: true,
- component: OpenUrlPage,
+ components: {
+ popup: OpenUrlPage,
+ sidepanel: OpenUrlPage,
+ detached: OpenUrlPage,
+ },
},
{
key: 'qrCode',
label: '二维码工具',
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',
label: '查看',
defaultVisible: false,
- component: OpenUrlViewerPage,
+ components: {
+ popup: OpenUrlViewerPage,
+ sidepanel: OpenUrlViewerPage,
+ detached: OpenUrlViewerPage,
+ },
},
];
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[] {
- return ROUTES.filter(route => route.defaultVisible).map(route => route.key);
+ return ROUTES.filter((route) => route.defaultVisible).map((route) => route.key);
}
export function getAllRouteKeys(): PageType[] {
- return ROUTES.map(route => route.key);
+ return ROUTES.map((route) => route.key);
}
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';
}
diff --git a/entrypoints/background.ts b/entrypoints/background.ts
index 916b555..dc78b04 100644
--- a/entrypoints/background.ts
+++ b/entrypoints/background.ts
@@ -1,5 +1,6 @@
import '../.wxt/types/imports.d.ts';
import { browser } from 'wxt/browser';
+import { type MessagePayload, type MessageResponse } from '@/utils/messages';
export default defineBackground(() => {
// 监听扩展图标点击事件,打开侧边栏
@@ -20,40 +21,6 @@ export default defineBackground(() => {
} else if (reason === 'update') {
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) => {
@@ -61,4 +28,25 @@ export default defineBackground(() => {
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: '处理消息失败' });
+ }
+ },
+ );
});
diff --git a/entrypoints/content.ts b/entrypoints/content.ts
index c354ff5..872609b 100644
--- a/entrypoints/content.ts
+++ b/entrypoints/content.ts
@@ -1,9 +1,117 @@
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({
matches: [''],
- runAt: 'document_start',
+ runAt: 'document_end',
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: '执行操作失败' });
+ }
+ },
+ );
},
});
diff --git a/entrypoints/popup/pages/DashboardPage.tsx b/entrypoints/popup/pages/DashboardPage.tsx
index 85fec59..dfc6820 100644
--- a/entrypoints/popup/pages/DashboardPage.tsx
+++ b/entrypoints/popup/pages/DashboardPage.tsx
@@ -5,6 +5,7 @@ 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 type { PageType } from '@/types/storage';
import { useEffect, useState } from 'react';
import dayjs from '@/utils/dayjs';
@@ -88,6 +89,18 @@ export default function DashboardPage() {
cardBackgroundColor={dashboardPageStyles.cardBackgroundColor}
/>
);
+ case 'formRecognizer':
+ return (
+ }
+ onClick={() => navigateTo('formRecognizer')}
+ cardBackgroundColor={dashboardPageStyles.cardBackgroundColor}
+ />
+ );
default:
return null;
}
diff --git a/entrypoints/popup/pages/FormRecognizerPage.tsx b/entrypoints/popup/pages/FormRecognizerPage.tsx
new file mode 100644
index 0000000..0c08ea6
--- /dev/null
+++ b/entrypoints/popup/pages/FormRecognizerPage.tsx
@@ -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 (
+
+
+
+
+ Dummy Data Generator
+
+
+ 一键填充表单测试数据,提升开发和测试效率
+
+
+
+ {/* 主要操作按钮 */}
+
+ :
+ }
+ onClick={handleFillValidData}
+ disabled={loading}
+ fullWidth
+ sx={{
+ py: 1.2,
+ borderRadius: 3,
+ bgcolor: '#4caf50',
+ fontWeight: 700,
+ '&:hover': {
+ bgcolor: '#388e3c',
+ },
+ }}
+ >
+ {loading ? '填充中...' : '一键填充(有效数据)'}
+
+
+ :
+ }
+ onClick={handleFillInvalidData}
+ disabled={loading}
+ fullWidth
+ sx={{
+ py: 1.2,
+ borderRadius: 3,
+ bgcolor: '#ff9800',
+ fontWeight: 700,
+ '&:hover': {
+ bgcolor: '#f57c00',
+ },
+ }}
+ >
+ {loading ? '填充中...' : '一键填充(异常数据)'}
+
+
+ : }
+ 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...' : '一键清空所有表单'}
+
+
+
+ {/* 选项设置 */}
+
+
+
+ 填充选项
+
+
+
+ setIncludeHidden(e.target.checked)}
+ color="primary"
+ />
+ }
+ label="包含隐藏字段"
+ sx={{ width: '100%' }}
+ />
+
+
+
+ {/* 功能说明 */}
+
+
+
+ 功能说明
+
+
+
+
+ 有效数据模式:生成符合格式要求的测试数据,适用于正常功能测试。
+
+
+ 异常数据模式:生成边界值或格式错误的数据,适用于异常场景测试。
+
+
+ 一键清空:快速清空当前页面所有表单字段的值。
+
+
+ 支持的字段类型:
+ 文本、邮箱、手机号、数字、日期、文本域、密码、身份证号等。
+
+
+
+
+
+
+
+ );
+};
+
+export default FormRecognizerPage;
diff --git a/entrypoints/sidepanel/pages/FormFillSidePanel.tsx b/entrypoints/sidepanel/pages/FormFillSidePanel.tsx
new file mode 100644
index 0000000..ccf00e1
--- /dev/null
+++ b/entrypoints/sidepanel/pages/FormFillSidePanel.tsx
@@ -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([]);
+ const [mode, setMode] = useState<'valid' | 'invalid'>('valid');
+ const [fillEmptyOnly, setFillEmptyOnly] = useState(false);
+ const [includeHidden] = useState(false);
+ const [hoveredFieldId, setHoveredFieldId] = useState(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.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 (
+
+
+
+ Dummy Data Pro
+
+
+ 智能表单填充助手
+
+
+
+
+
+ : }
+ onClick={handleScan}
+ disabled={scanning}
+ sx={{ flex: 1 }}
+ >
+ {scanning ? '扫描中...' : '扫描表单'}
+
+ }
+ onClick={handleRefreshAll}
+ disabled={fields.length === 0}
+ >
+ 刷新
+
+
+
+
+
+
+
+
+ setFillEmptyOnly(e.target.checked)}
+ size="small"
+ />
+ }
+ label="仅填充空字段"
+ sx={{ mb: 1 }}
+ />
+
+
+ : null}
+ onClick={handleFill}
+ disabled={loading || selectedCount === 0}
+ sx={{ flex: 1 }}
+ >
+ {loading ? '填充中...' : `确认填充 (${selectedCount})`}
+
+ }
+ onClick={handleClear}
+ disabled={loading}
+ >
+ 清空
+
+
+
+
+ {fields.length > 0 && (
+
+
+
+ 字段列表 ({fields.length})
+
+
+
+
+
+ {fields.map((field) => (
+ handleHoverField(field.id)}
+ onMouseLeave={() => handleHoverField(null)}
+ >
+
+ handleToggleSelect(field.id)}
+ sx={{ p: 0, mt: 0.5 }}
+ />
+
+
+
+ {field.label || field.placeholder || field.name || '未命名字段'}
+
+
+
+ {getFieldTypeLabel(field.fieldType)}
+
+
+
+ {
+ e.stopPropagation();
+ handleHoverField(field.id);
+ setTimeout(() => handleHoverField(null), 2000);
+ }}
+ sx={{ p: 0.5 }}
+ >
+
+
+
+
+
+ handleEditValue(field.id, e.target.value)}
+ placeholder="生成的数据..."
+ sx={{
+ '& .MuiInputBase-input': {
+ fontSize: '0.8rem',
+ fontFamily: 'monospace',
+ },
+ }}
+ />
+
+ {
+ e.stopPropagation();
+ handleRefreshField(field.id);
+ }}
+ sx={{ p: 0.5 }}
+ >
+
+
+
+
+ {field.value && (
+
+ 当前值: {field.value}
+
+ )}
+
+
+
+ ))}
+
+
+ )}
+
+ {fields.length === 0 && !scanning && (
+
+
+ 点击「扫描表单」按钮开始扫描当前页面的表单字段
+
+
+ )}
+
+
+
+ );
+};
+
+export default FormFillSidePanel;
diff --git a/package-lock.json b/package-lock.json
index f90a274..a54cdb1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,9 +8,11 @@
"name": "testing-tools",
"version": "1.0.0",
"hasInstallScript": true,
+ "license": "MIT",
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
+ "@faker-js/faker": "^10.4.0",
"@mui/icons-material": "^7.3.8",
"@mui/material": "^7.3.8",
"@webext-core/messaging": "^2.3.0",
@@ -1447,6 +1449,22 @@
"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": {
"version": "0.19.1",
"resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.1.tgz",
@@ -2065,9 +2083,6 @@
"arm"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2082,9 +2097,6 @@
"arm"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2099,9 +2111,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2116,9 +2125,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2133,9 +2139,6 @@
"loong64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2150,9 +2153,6 @@
"loong64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2167,9 +2167,6 @@
"ppc64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2184,9 +2181,6 @@
"ppc64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2201,9 +2195,6 @@
"riscv64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2218,9 +2209,6 @@
"riscv64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2235,9 +2223,6 @@
"s390x"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2252,9 +2237,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2269,9 +2251,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
diff --git a/package.json b/package.json
index ba392ca..eb4d527 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
+ "@faker-js/faker": "^10.4.0",
"@mui/icons-material": "^7.3.8",
"@mui/material": "^7.3.8",
"@webext-core/messaging": "^2.3.0",
diff --git a/providers/RouterProvider.tsx b/providers/RouterProvider.tsx
index 6edb2ff..77f5824 100644
--- a/providers/RouterProvider.tsx
+++ b/providers/RouterProvider.tsx
@@ -29,7 +29,7 @@ export function RouterProvider({
children,
defaultRoute = 'dashboard',
syncRoute = true,
- syncKey = 'app/currentRoute'
+ syncKey = 'app/currentRoute',
}: RouterProviderProps) {
const [currentPage, setCurrentPage] = useState(defaultRoute);
const [visiblePages, setVisiblePages] = useState(getDefaultVisibleRoutes());
@@ -124,7 +124,7 @@ export function RouterProvider({
syncNavigation,
goBack,
setVisiblePages,
- setPageOrder
+ setPageOrder,
}}
>
{children}
diff --git a/types/storage.d.ts b/types/storage.d.ts
index b3bb29d..816eca0 100644
--- a/types/storage.d.ts
+++ b/types/storage.d.ts
@@ -4,6 +4,7 @@ export type PageType =
| 'storageCleaner'
| 'openUrl'
| 'qrCode'
+ | 'formRecognizer'
| 'openUrlViewer';
export interface StorageSchema {
diff --git a/utils/dummyDataGenerator.ts b/utils/dummyDataGenerator.ts
new file mode 100644
index 0000000..d33d3b9
--- /dev/null
+++ b/utils/dummyDataGenerator.ts
@@ -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);
+ }
+ });
+}
diff --git a/utils/messages.ts b/utils/messages.ts
new file mode 100644
index 0000000..d0cf748
--- /dev/null
+++ b/utils/messages.ts
@@ -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[];
+ mode?: FillMode;
+ includeHidden?: boolean;
+ fieldId?: string;
+ fieldIds?: string[];
+}
+
+/**
+ * 消息响应接口
+ */
+export interface MessageResponse {
+ success: boolean;
+ message?: string;
+ fields?: Omit[];
+ totalCount?: number;
+ validCount?: number;
+ hasModal?: boolean;
+}
+
+/**
+ * 发送消息到内容脚本
+ */
+export async function sendMessageToContent(
+ action: MessageAction,
+ payload?: Omit,
+): Promise {
+ 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 {
+ 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;
+ }
+}
diff --git a/utils/messages.tsx b/utils/messages.tsx
deleted file mode 100644
index d6884db..0000000
--- a/utils/messages.tsx
+++ /dev/null
@@ -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();