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:
+22
-34
@@ -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: '处理消息失败' });
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
+110
-2
@@ -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: ['<all_urls>'],
|
||||
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: '执行操作失败' });
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<ToolCard
|
||||
key={key}
|
||||
title="表单识别"
|
||||
description="识别标签页中的表单内容"
|
||||
colorCode="#ff5722"
|
||||
icon={<DescriptionIcon sx={{ fontSize: 20 }} />}
|
||||
onClick={() => navigateTo('formRecognizer')}
|
||||
cardBackgroundColor={dashboardPageStyles.cardBackgroundColor}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
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;
|
||||
Reference in New Issue
Block a user