Develop (#17)
✨新功能 (Features) 智能表单引擎: 新增智能表单填充功能,内置模糊匹配引擎、Mock数据生成器与视觉反馈渲染器。 表单映射与导出: 实现表单映射页面(包含扫描器和高亮器),并支持将配置导出为 JSON 文件,附带 Snackbar 状态提示。 表单识别增强: 增加按域名保存字段类型偏好的功能;添加字段定位闪烁以辅助查找;优化填充逻辑(支持单字段覆盖默认模式);重构 FieldList 组件以提升操作体验。 ♻️ 代码重构 (Refactor) 通用组件提取: 提取并统一应用通用的 PageHeader 组件,移除独立的侧边栏页面及未使用的组件文件。 状态与逻辑优化: 改进 useStorageState 钩子(增加加载状态管理与防抖处理);将二维码解析功能重构为独立模块。 类型与依赖简化: 统一使用 SnackbarOptions 类型;简化假数据生成器中 faker 的导入与使用逻辑。 💄 样式与界面 (Style) UI 细节打磨: 统一各页面头部图标颜色,调整表单输入框与按钮交互样式;优化时间戳页面、结果视图布局(增加圆角、调整内边距/对齐方式);重构存储选项网格及自动刷新开关样式。 代码格式: 优化项目中导入语句的顺序与格式。 👷 持续集成 (CI) 流程提效: 移除 Firefox 测试步骤以减少资源消耗;收紧工作流触发条件,移除 develop 及其变体分支,仅保留 main 分支触发。 📝 文档 (Docs) 代码维护: 补充组件的文档注释与类型导入。
This commit is contained in:
@@ -4,12 +4,9 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
- develop-*
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Testing Tools Browser Extension - Gemini Instructions
|
||||
|
||||
This document provides essential context and instructions for AI agents working on the Testing Tools browser extension project.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Testing Tools** is a lightweight, feature-rich browser extension built with the [WXT (Web Extension Toolkit)](https://wxt.dev/) framework. It provides a suite of utilities for developers and testers, including timestamp conversion, storage management, URL shortcuts, and QR code tools.
|
||||
|
||||
### Tech Stack
|
||||
|
||||
- **Framework:** WXT (Web Extension Toolkit)
|
||||
- **Frontend:** React 19 + TypeScript
|
||||
- **UI Library:** Material UI (MUI) @7.x
|
||||
- **Date Handling:** dayjs (with UTC and timezone plugins)
|
||||
- **Messaging:** @webext-core/messaging
|
||||
- **Storage:** Type-safe Chrome Storage API wrapper
|
||||
- **Testing:** Vitest + Testing Library (jsdom)
|
||||
|
||||
### Architecture & Directory Structure
|
||||
|
||||
- `entrypoints/`: Extension entry points (popup, options, sidepanel, background, content).
|
||||
- `popup/`: Main UI shown when clicking the extension icon.
|
||||
- `options/`: Extension settings page.
|
||||
- `sidepanel/`: Browser side panel integration.
|
||||
- `background.ts`: Background script for lifecycle management and background tasks.
|
||||
- `content.ts`: Content script injected into web pages.
|
||||
- `components/`: Reusable React components.
|
||||
- `config/`: Application configuration, including routes and themes.
|
||||
- `providers/`: React Context providers (e.g., `RouterProvider`).
|
||||
- `utils/`: Utility functions and service abstractions.
|
||||
- `chromeStorage.ts`: Type-safe storage utility.
|
||||
- `types/`: Global TypeScript type definitions.
|
||||
- `public/`: Static assets (icons, etc.).
|
||||
|
||||
## Building and Running
|
||||
|
||||
### Development
|
||||
|
||||
- `npm run dev`: Start Chrome development mode with HMR.
|
||||
- `npm run dev:firefox`: Start Firefox development mode.
|
||||
- `npm run compile`: Run TypeScript type checking (`tsc --noEmit`).
|
||||
|
||||
### Production
|
||||
|
||||
- `npm run build`: Build production version for Chrome.
|
||||
- `npm run build:firefox`: Build production version for Firefox.
|
||||
- `npm run zip`: Package the extension for Chrome Web Store.
|
||||
- `npm run zip:firefox`: Package the extension for Firefox Add-ons.
|
||||
|
||||
### Testing & Linting
|
||||
|
||||
- `npm run test`: Run all tests once.
|
||||
- `npm run test:watch`: Run tests in watch mode.
|
||||
- `npm run test:coverage`: Run tests and generate coverage report.
|
||||
- `npm run lint`: Run ESLint checks.
|
||||
|
||||
## Development Conventions
|
||||
|
||||
### Coding Style
|
||||
|
||||
- **TypeScript:** Use strict typing. Prefer interfaces for object structures and types for unions/aliases.
|
||||
- **Components:** Functional components with Hooks. Use MUI components for consistent UI.
|
||||
- **Storage:** Always use `storageUtil` from `@/utils/chromeStorage.ts` for accessing `chrome.storage.local`. Ensure keys are defined in `StorageSchema` in `@/types/storage.d.ts`.
|
||||
- **Messaging:** Use `@webext-core/messaging` for communication between entry points. Define message types in `@/utils/messages.ts`.
|
||||
|
||||
### Testing Practices
|
||||
|
||||
- **Framework:** Vitest with `jsdom` environment.
|
||||
- **Location:** Place tests in `__tests__` directories adjacent to the files being tested.
|
||||
- **Naming:** Follow `*.test.ts` or `*.test.tsx` naming convention.
|
||||
- **Patterns:** Use `@testing-library/react` for component testing. Prefer `user-event` (v14+) for simulating interactions.
|
||||
|
||||
### CI/CD
|
||||
|
||||
- **GitHub Actions:** CI runs on push/PR to `main` and `develop` branches (lint, compile, test, build).
|
||||
- **Releases:** Automatic release to GitHub on pushing a `v*` tag.
|
||||
|
||||
## Key Considerations for AI Agents
|
||||
|
||||
- **Manifest Permissions:** When adding features that require new browser APIs, update `wxt.config.ts`.
|
||||
- **Browser Compatibility:** Ensure features work in both Chrome and Firefox.
|
||||
- **React 19:** Be aware of React 19 specific features and deprecations.
|
||||
- **WXT Modules:** The project uses `@wxt-dev/module-react`.
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
@@ -11,14 +11,16 @@ import {
|
||||
AccordionDetails,
|
||||
CircularProgress,
|
||||
InputAdornment,
|
||||
IconButton,
|
||||
} from '@mui/material';
|
||||
import LinkIcon from '@mui/icons-material/Link';
|
||||
import ImageIcon from '@mui/icons-material/Image';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import jsQR from 'jsqr';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import CopyButton from '@/components/CopyButton';
|
||||
import { qrCodePageStyles } from '@/config/pageTheme';
|
||||
import type { SnackbarOptions } from '@/components/GlobalSnackbar';
|
||||
import { parseQrCodeFromFile } from '@/utils/qrCodeParser';
|
||||
|
||||
interface QrCodeToUrlSectionProps {
|
||||
expanded: boolean;
|
||||
@@ -35,13 +37,37 @@ const QrCodeToUrlSection = ({
|
||||
const [parsedUrl, setParsedUrl] = useState('');
|
||||
const [parseError, setParseError] = useState('');
|
||||
const [parsing, setParsing] = useState(false);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFileChange = useCallback((file: File) => {
|
||||
setQrCodeFile(file);
|
||||
setParseError('');
|
||||
setParsedUrl('');
|
||||
}, []);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
const file = e.target.files[0];
|
||||
setQrCodeFile(file);
|
||||
setParseError('');
|
||||
setParsedUrl('');
|
||||
handleFileChange(e.target.files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = () => {
|
||||
setDragging(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setDragging(false);
|
||||
const droppedFile = e.dataTransfer.files?.[0];
|
||||
if (droppedFile) {
|
||||
handleFileChange(droppedFile);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -56,34 +82,16 @@ const QrCodeToUrlSection = ({
|
||||
setParseError('');
|
||||
setParsedUrl('');
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const result = await parseQrCodeFromFile(qrCodeFile);
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error('无法创建 canvas 上下文');
|
||||
}
|
||||
|
||||
const image = new Image();
|
||||
image.src = URL.createObjectURL(qrCodeFile);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
image.onload = () => {
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
ctx.drawImage(image, 0, 0);
|
||||
resolve();
|
||||
};
|
||||
image.onerror = () => reject(new Error('图片加载失败'));
|
||||
});
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const code = jsQR(imageData.data, imageData.width, imageData.height);
|
||||
|
||||
if (code) {
|
||||
setParsedUrl(code.data);
|
||||
if (result.success && result.data) {
|
||||
setParsedUrl(result.data);
|
||||
showMessage('二维码解析成功', { severity: 'success', autoHideDuration: 1000 });
|
||||
} else {
|
||||
showMessage('未检测到二维码', { severity: 'error', autoHideDuration: 1000 });
|
||||
showMessage(result.error || '未检测到二维码', {
|
||||
severity: 'error',
|
||||
autoHideDuration: 1000,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析二维码失败:', error);
|
||||
@@ -108,9 +116,7 @@ const QrCodeToUrlSection = ({
|
||||
const file = items[i].getAsFile();
|
||||
if (file) {
|
||||
try {
|
||||
setQrCodeFile(file);
|
||||
setParseError('');
|
||||
setParsedUrl('');
|
||||
handleFileChange(file);
|
||||
showMessage('图片粘贴成功', { severity: 'success', autoHideDuration: 1000 });
|
||||
} catch (error) {
|
||||
console.error('处理粘贴图片失败:', error);
|
||||
@@ -127,7 +133,7 @@ const QrCodeToUrlSection = ({
|
||||
return () => {
|
||||
document.removeEventListener('paste', handlePaste);
|
||||
};
|
||||
}, [expanded, showMessage]);
|
||||
}, [expanded, showMessage, handleFileChange]);
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
@@ -157,10 +163,18 @@ const QrCodeToUrlSection = ({
|
||||
justifyContent: 'center',
|
||||
minHeight: 200,
|
||||
border: '2px dashed',
|
||||
borderColor: qrCodeFile ? qrCodePageStyles.successColor : 'grey.200',
|
||||
borderColor: dragging
|
||||
? qrCodePageStyles.successColor
|
||||
: qrCodeFile
|
||||
? qrCodePageStyles.successColor
|
||||
: 'grey.200',
|
||||
borderRadius: 3,
|
||||
p: 4,
|
||||
bgcolor: qrCodeFile ? 'rgba(76, 175, 80, 0.05)' : 'grey.50',
|
||||
bgcolor: dragging
|
||||
? 'rgba(76, 175, 80, 0.1)'
|
||||
: qrCodeFile
|
||||
? 'rgba(76, 175, 80, 0.05)'
|
||||
: 'grey.50',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
'&:hover': {
|
||||
@@ -168,11 +182,15 @@ const QrCodeToUrlSection = ({
|
||||
bgcolor: 'rgba(76, 175, 80, 0.05)',
|
||||
},
|
||||
}}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileChange}
|
||||
onChange={handleInputChange}
|
||||
style={{
|
||||
display: 'none',
|
||||
}}
|
||||
@@ -195,8 +213,7 @@ const QrCodeToUrlSection = ({
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -212,33 +229,15 @@ const QrCodeToUrlSection = ({
|
||||
position: 'absolute',
|
||||
top: -8,
|
||||
right: -8,
|
||||
minWidth: '32px',
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '50%',
|
||||
bgcolor: 'rgba(244, 67, 54, 0.9)',
|
||||
color: 'white',
|
||||
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
'&:hover': {
|
||||
bgcolor: 'rgba(211, 47, 47, 0.95)',
|
||||
transform: 'scale(1.1)',
|
||||
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.3)',
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'scale(0.95)',
|
||||
},
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: 700,
|
||||
fontSize: '16px',
|
||||
lineHeight: 1,
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
<ClearIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
|
||||
{qrCodeFile.name}
|
||||
|
||||
@@ -13,9 +13,9 @@ import ImageIcon from '@mui/icons-material/Image';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
import jsQR from 'jsqr';
|
||||
import GlobalSnackbar, { useSnackbar } from './GlobalSnackbar';
|
||||
import CopyButton from './CopyButton';
|
||||
import { parseQrCodeFromFile } from '@/utils/qrCodeParser';
|
||||
|
||||
interface QrCodeUploaderProps {
|
||||
onQrCodeDetected?: (data: string) => void;
|
||||
@@ -67,7 +67,6 @@ const QrCodeUploader: React.FC<QrCodeUploaderProps> = ({
|
||||
setProgress(0);
|
||||
|
||||
try {
|
||||
// 模拟上传进度
|
||||
const progressInterval = setInterval(() => {
|
||||
setProgress((prev) => {
|
||||
if (prev >= 90) {
|
||||
@@ -78,51 +77,20 @@ const QrCodeUploader: React.FC<QrCodeUploaderProps> = ({
|
||||
});
|
||||
}, 200);
|
||||
|
||||
// 读取文件并解析二维码
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error('无法创建 canvas 上下文');
|
||||
}
|
||||
|
||||
const image = new Image();
|
||||
image.src = URL.createObjectURL(file);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
reject(new Error('图片加载超时'));
|
||||
}, timeout);
|
||||
|
||||
image.onload = () => {
|
||||
clearTimeout(timeoutId);
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
ctx.drawImage(image, 0, 0);
|
||||
resolve();
|
||||
};
|
||||
|
||||
image.onerror = () => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(new Error('图片加载失败'));
|
||||
};
|
||||
});
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const code = jsQR(imageData.data, imageData.width, imageData.height);
|
||||
const result = await parseQrCodeFromFile(file, timeout);
|
||||
|
||||
clearInterval(progressInterval);
|
||||
setProgress(100);
|
||||
|
||||
if (code) {
|
||||
setResult(code.data);
|
||||
if (result.success && result.data) {
|
||||
setResult(result.data);
|
||||
showMessage('二维码解析成功', { severity: 'success' });
|
||||
if (onQrCodeDetected) {
|
||||
onQrCodeDetected(code.data);
|
||||
onQrCodeDetected(result.data);
|
||||
}
|
||||
} else {
|
||||
setError('未检测到二维码');
|
||||
showMessage('未检测到二维码', { severity: 'error' });
|
||||
setError(result.error || '未检测到二维码');
|
||||
showMessage(result.error || '未检测到二维码', { severity: 'error' });
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '解析失败');
|
||||
@@ -131,7 +99,6 @@ const QrCodeUploader: React.FC<QrCodeUploaderProps> = ({
|
||||
});
|
||||
} finally {
|
||||
setUploading(false);
|
||||
// 延迟清除进度,让用户看到完成状态
|
||||
setTimeout(() => setProgress(0), 500);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { Box, TextField, Alert, Stack } from '@mui/material';
|
||||
import { Box, TextField, Alert, Stack, alpha } from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import Button from '@/components/Button';
|
||||
import type { OpenUrlEntry } from '@/types/storage';
|
||||
@@ -65,11 +65,6 @@ const UrlEntryForm = ({ onAddEntry, showMessage }: UrlEntryFormProps) => {
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
sx={openUrlPageStyles.INPUT_STYLE}
|
||||
slotProps={{
|
||||
inputLabel: {
|
||||
shrink: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="目标 URL"
|
||||
@@ -79,11 +74,6 @@ const UrlEntryForm = ({ onAddEntry, showMessage }: UrlEntryFormProps) => {
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
sx={openUrlPageStyles.INPUT_STYLE}
|
||||
slotProps={{
|
||||
inputLabel: {
|
||||
shrink: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{showMixedContentWarning && (
|
||||
@@ -111,8 +101,8 @@ const UrlEntryForm = ({ onAddEntry, showMessage }: UrlEntryFormProps) => {
|
||||
fontWeight: 800,
|
||||
boxShadow: 'none',
|
||||
'&:hover': {
|
||||
bgcolor: 'rgba(25, 118, 210, 0.85)',
|
||||
boxShadow: '0 8px 24px rgba(25, 118, 210, 0.2)',
|
||||
bgcolor: openUrlPageStyles.primaryDark,
|
||||
boxShadow: `0 8px 24px ${alpha(openUrlPageStyles.primaryColor, 0.2)}`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
|
||||
describe('routes', () => {
|
||||
describe('ROUTES', () => {
|
||||
it('should have 7 routes defined', () => {
|
||||
expect(ROUTES).toHaveLength(7);
|
||||
it('should have 9 routes defined', () => {
|
||||
expect(ROUTES).toHaveLength(9);
|
||||
});
|
||||
|
||||
it('should have all required properties for each route', () => {
|
||||
@@ -104,7 +104,7 @@ describe('routes', () => {
|
||||
describe('getAllRouteKeys', () => {
|
||||
it('should return all route keys', () => {
|
||||
const allKeys = getAllRouteKeys();
|
||||
expect(allKeys).toHaveLength(7);
|
||||
expect(allKeys).toHaveLength(9);
|
||||
expect(allKeys).toContain('dashboard');
|
||||
expect(allKeys).toContain('timestamp');
|
||||
expect(allKeys).toContain('storageCleaner');
|
||||
@@ -135,9 +135,9 @@ describe('routes', () => {
|
||||
expect(pageOrder).toContain('formRecognizer');
|
||||
});
|
||||
|
||||
it('should have 5 items in page order', () => {
|
||||
it('should have 7 items in page order', () => {
|
||||
const pageOrder = getDefaultPageOrder();
|
||||
expect(pageOrder).toHaveLength(5);
|
||||
expect(pageOrder).toHaveLength(7);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+14
-27
@@ -111,15 +111,13 @@ export const timestampPageStyles = {
|
||||
* 打开 URL 页面样式
|
||||
*/
|
||||
export const openUrlPageStyles = {
|
||||
primaryColor: THEME_COLORS.purple,
|
||||
primaryDark: THEME_COLORS.purpleDark,
|
||||
INPUT_STYLE: {
|
||||
'& .MuiOutlinedInput-root': {
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 3,
|
||||
borderRadius: 4,
|
||||
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
'& fieldset': {
|
||||
border: '1px solid',
|
||||
borderColor: 'grey.100',
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: 'grey.300',
|
||||
},
|
||||
@@ -129,14 +127,14 @@ export const openUrlPageStyles = {
|
||||
},
|
||||
'&.Mui-focused': {
|
||||
bgcolor: '#fff',
|
||||
boxShadow: (_theme: Theme) => `0 0 0 4px ${alpha(THEME_COLORS.purple, 0.1)}`,
|
||||
},
|
||||
},
|
||||
'& .MuiInputBase-input': {
|
||||
py: 1.2,
|
||||
py: '14px',
|
||||
px: 2,
|
||||
fontSize: '0.85rem',
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.4,
|
||||
},
|
||||
'& .MuiInputLabel-root': {
|
||||
fontSize: '0.85rem',
|
||||
@@ -176,26 +174,7 @@ export const qrCodePageStyles = {
|
||||
successDark: THEME_COLORS.successDark,
|
||||
white: THEME_COLORS.white,
|
||||
black: THEME_COLORS.black,
|
||||
INPUT_STYLE: {
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 3,
|
||||
'& fieldset': {
|
||||
borderColor: THEME_COLORS.success,
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: THEME_COLORS.success,
|
||||
},
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: THEME_COLORS.success,
|
||||
},
|
||||
},
|
||||
'& .MuiInputLabel-root': {
|
||||
fontSize: '0.85rem',
|
||||
fontWeight: 700,
|
||||
color: 'text.secondary',
|
||||
'&.Mui-focused': { color: THEME_COLORS.success },
|
||||
},
|
||||
},
|
||||
INPUT_STYLE: {},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
@@ -212,6 +191,7 @@ export const dashboardPageStyles = {
|
||||
* 使用语义化的颜色命名:valid(有效)、invalid(无效)、clear(清除)
|
||||
*/
|
||||
export const formRecognizerPageStyles = {
|
||||
primaryColor: '#ff5722',
|
||||
validColor: THEME_COLORS.success,
|
||||
validDark: THEME_COLORS.successDark,
|
||||
invalidColor: THEME_COLORS.warning,
|
||||
@@ -225,3 +205,10 @@ export const formRecognizerPageStyles = {
|
||||
fontWeight: 700,
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 表单映射页面样式
|
||||
*/
|
||||
export const formMappingPageStyles = {
|
||||
secondaryColor: THEME_COLORS.purple,
|
||||
} as const;
|
||||
|
||||
@@ -6,6 +6,8 @@ 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 FormMappingPage from '@/entrypoints/popup/pages/FormMappingPage';
|
||||
import FormFillPage from '@/entrypoints/popup/pages/FormFillPage';
|
||||
|
||||
export interface RouteConfig {
|
||||
key: PageType;
|
||||
@@ -69,6 +71,26 @@ export const ROUTES: RouteConfig[] = [
|
||||
detached: QrCodePage,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'formMapping',
|
||||
label: '表单映射',
|
||||
defaultVisible: true,
|
||||
components: {
|
||||
popup: FormMappingPage,
|
||||
sidepanel: FormMappingPage,
|
||||
detached: FormMappingPage,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'formFill',
|
||||
label: '智能填充',
|
||||
defaultVisible: true,
|
||||
components: {
|
||||
popup: FormFillPage,
|
||||
sidepanel: FormFillPage,
|
||||
detached: FormFillPage,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'formRecognizer',
|
||||
label: '表单识别',
|
||||
|
||||
@@ -12,6 +12,16 @@ import {
|
||||
} from '@/utils/dummyDataGenerator';
|
||||
import { MessageAction, type MessagePayload, type MessageResponse } from '@/utils/messages';
|
||||
|
||||
import { SmartDetector } from '@/utils/formMapping/scanner';
|
||||
import { highlighter } from '@/utils/formMapping/highlighter';
|
||||
import {
|
||||
FuzzyMatcher,
|
||||
SmartInjectionEngine,
|
||||
FeedbackRenderer,
|
||||
} from '@/utils/formMapping/smartInjector';
|
||||
import { storageUtil } from '@/utils/chromeStorage';
|
||||
import { FormMapEntry } from '@/types/storage';
|
||||
|
||||
// 存储当前扫描到的字段列表,用于高亮联动
|
||||
let currentFields: FormFieldInfo[] = [];
|
||||
|
||||
@@ -19,6 +29,53 @@ export default defineContentScript({
|
||||
matches: ['<all_urls>'],
|
||||
runAt: 'document_end',
|
||||
main() {
|
||||
// === 通用表单映射助手逻辑 ===
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (
|
||||
area === 'local' &&
|
||||
(changes['active_form_map'] || changes['app/formMapping/isPicking'])
|
||||
) {
|
||||
updateMappingUI();
|
||||
}
|
||||
});
|
||||
|
||||
async function updateMappingUI() {
|
||||
const entries = ((await storageUtil.get('active_form_map')) as FormMapEntry[]) || [];
|
||||
const isPicking = ((await storageUtil.get('app/formMapping/isPicking')) as boolean) || false;
|
||||
|
||||
if (entries.length > 0 || isPicking) {
|
||||
highlighter.show();
|
||||
highlighter.draw(entries);
|
||||
|
||||
if (isPicking) {
|
||||
highlighter.enablePicker(async (el) => {
|
||||
const fingerprint = SmartDetector.generateFingerprint(el);
|
||||
const label = SmartDetector.extractSemanticLabel(el);
|
||||
|
||||
const newEntry: FormMapEntry = {
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
label_display: label,
|
||||
fingerprint,
|
||||
action_logic: { type: 'text', strategy: 'fixed', value: '' },
|
||||
ui_state: { is_selected: true },
|
||||
};
|
||||
|
||||
const currentMap = ((await storageUtil.get('active_form_map')) as FormMapEntry[]) || [];
|
||||
await storageUtil.set('active_form_map', [...currentMap, newEntry]);
|
||||
await storageUtil.set('app/formMapping/isPicking', false);
|
||||
});
|
||||
} else {
|
||||
highlighter.disablePicker();
|
||||
}
|
||||
} else {
|
||||
highlighter.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// 初始加载映射 UI
|
||||
updateMappingUI();
|
||||
|
||||
// === 原有表单识别逻辑 ===
|
||||
// 监听来自 popup/sidepanel 的消息
|
||||
chrome.runtime.onMessage.addListener(
|
||||
(message: MessagePayload, _sender, sendResponse: (response: MessageResponse) => void) => {
|
||||
@@ -126,6 +183,38 @@ export default defineContentScript({
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'FORM_INJECT': {
|
||||
try {
|
||||
const injectData =
|
||||
(message.data as Array<{ entry: FormMapEntry; mockValue: string }>) || [];
|
||||
const results = injectData.map((item) => {
|
||||
const matchResult = FuzzyMatcher.findTargetElement(item.entry.fingerprint);
|
||||
if (matchResult.element) {
|
||||
const injectResult = SmartInjectionEngine.inject(
|
||||
matchResult.element,
|
||||
item.entry,
|
||||
item.mockValue,
|
||||
);
|
||||
if (injectResult.success) {
|
||||
FeedbackRenderer.renderSuccess(matchResult.element);
|
||||
} else {
|
||||
FeedbackRenderer.renderError(matchResult.element);
|
||||
}
|
||||
return { id: item.entry.id, success: injectResult.success };
|
||||
} else {
|
||||
return { id: item.entry.id, success: false };
|
||||
}
|
||||
});
|
||||
sendResponse({ success: true, results });
|
||||
} catch (error) {
|
||||
console.error('智能注入失败:', error);
|
||||
sendResponse({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '注入失败',
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
sendResponse({ success: false, message: '未知操作' });
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import StorageIcon from '@mui/icons-material/Storage';
|
||||
import LanguageIcon from '@mui/icons-material/Language';
|
||||
import QrCodeIcon from '@mui/icons-material/QrCode';
|
||||
import DescriptionIcon from '@mui/icons-material/Description';
|
||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
import type { PageType } from '@/types/storage';
|
||||
import { useEffect, useState } from 'react';
|
||||
import dayjs from '@/utils/dayjs';
|
||||
@@ -89,6 +90,18 @@ export default function DashboardPage() {
|
||||
cardBackgroundColor={dashboardPageStyles.cardBackgroundColor}
|
||||
/>
|
||||
);
|
||||
case 'formMapping':
|
||||
return (
|
||||
<ToolCard
|
||||
key={key}
|
||||
title="通用表单映射助手"
|
||||
description="智能识别表单指纹,自定义填充逻辑"
|
||||
colorCode="#3f51b5"
|
||||
icon={<AutoFixHighIcon sx={{ fontSize: 20 }} />}
|
||||
onClick={() => navigateTo('formMapping')}
|
||||
cardBackgroundColor={dashboardPageStyles.cardBackgroundColor}
|
||||
/>
|
||||
);
|
||||
case 'formRecognizer':
|
||||
return (
|
||||
<ToolCard
|
||||
@@ -101,6 +114,18 @@ export default function DashboardPage() {
|
||||
cardBackgroundColor={dashboardPageStyles.cardBackgroundColor}
|
||||
/>
|
||||
);
|
||||
case 'formFill':
|
||||
return (
|
||||
<ToolCard
|
||||
key={key}
|
||||
title="智能填充"
|
||||
description="根据表单指纹智能填充表单内容"
|
||||
colorCode="#2196f3"
|
||||
icon={<AutoFixHighIcon sx={{ fontSize: 20 }} />}
|
||||
onClick={() => navigateTo('formFill')}
|
||||
cardBackgroundColor={dashboardPageStyles.cardBackgroundColor}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Container,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
IconButton,
|
||||
Switch,
|
||||
Divider,
|
||||
Paper,
|
||||
Button,
|
||||
Chip,
|
||||
Snackbar,
|
||||
Alert,
|
||||
alpha,
|
||||
} from '@mui/material';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { storageUtil } from '@/utils/chromeStorage';
|
||||
import { FormMapEntry } from '@/types/storage';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import { globalStyles, formMappingPageStyles } from '@/config/pageTheme.ts';
|
||||
import { MockDataGenerator } from '@/utils/formMapping/smartInjector';
|
||||
|
||||
export default function FormFillPage() {
|
||||
const [entries, setEntries] = useState<FormMapEntry[]>([]);
|
||||
const [previewData, setPreviewData] = useState<Map<string, string>>(new Map());
|
||||
const [injectResults, setInjectResults] = useState<Map<string, boolean>>(new Map());
|
||||
const [isInjecting, setIsInjecting] = useState(false);
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
const [showError, setShowError] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const generatePreviewData = useCallback((items: FormMapEntry[]) => {
|
||||
const preview = new Map<string, string>();
|
||||
items.forEach((entry) => {
|
||||
const value = MockDataGenerator.generate(entry.action_logic, entry);
|
||||
preview.set(entry.id, value);
|
||||
});
|
||||
setPreviewData(preview);
|
||||
setInjectResults(new Map());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const loadEntries = async () => {
|
||||
const data = (await storageUtil.get('active_form_map')) as FormMapEntry[];
|
||||
setEntries(data || []);
|
||||
generatePreviewData(data || []);
|
||||
};
|
||||
|
||||
loadEntries();
|
||||
const listener = (changes: { [key: string]: chrome.storage.StorageChange }, area: string) => {
|
||||
if (area === 'local' && changes['active_form_map']) {
|
||||
loadEntries();
|
||||
}
|
||||
};
|
||||
chrome.storage.onChanged.addListener(listener);
|
||||
return () => chrome.storage.onChanged.removeListener(listener);
|
||||
}, [generatePreviewData]);
|
||||
|
||||
const refreshPreview = () => {
|
||||
generatePreviewData(entries);
|
||||
};
|
||||
|
||||
const injectAllFields = async () => {
|
||||
if (entries.length === 0) {
|
||||
setErrorMessage('没有可填充的字段');
|
||||
setShowError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsInjecting(true);
|
||||
const results = new Map<string, boolean>();
|
||||
|
||||
try {
|
||||
// 发送消息到 content script 执行注入
|
||||
const response = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
if (response.length === 0) {
|
||||
throw new Error('无法获取当前标签页');
|
||||
}
|
||||
|
||||
const tabId = response[0].id;
|
||||
if (!tabId) {
|
||||
throw new Error('标签页ID无效');
|
||||
}
|
||||
|
||||
// 准备注入数据
|
||||
const injectData = entries.map((entry) => ({
|
||||
entry,
|
||||
mockValue: previewData.get(entry.id) || '',
|
||||
}));
|
||||
|
||||
// 执行注入
|
||||
const result = await chrome.tabs.sendMessage(tabId, {
|
||||
type: 'FORM_INJECT',
|
||||
data: injectData,
|
||||
});
|
||||
|
||||
if (result && result.success) {
|
||||
result.results.forEach((r: { id: string; success: boolean }) => {
|
||||
results.set(r.id, r.success);
|
||||
});
|
||||
setInjectResults(results);
|
||||
setShowSuccess(true);
|
||||
} else {
|
||||
throw new Error(result?.error || '注入失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('注入失败:', error);
|
||||
setErrorMessage(
|
||||
error instanceof Error ? error.message : '注入失败,请确保已在网页中打开表单',
|
||||
);
|
||||
setShowError(true);
|
||||
} finally {
|
||||
setIsInjecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getFieldTypeLabel = (type: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
text: '文本',
|
||||
select: '下拉框',
|
||||
checkbox: '复选框',
|
||||
radio: '单选框',
|
||||
};
|
||||
return labels[type] || type;
|
||||
};
|
||||
|
||||
const getStrategyLabel = (strategy: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
fixed: '固定值',
|
||||
random: '随机',
|
||||
sequence: '序列',
|
||||
};
|
||||
return labels[strategy] || strategy;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ bgcolor: globalStyles.backgroundColor, minHeight: '100%', pb: 3 }}>
|
||||
<Container sx={{ py: 2, bgcolor: globalStyles.backgroundColor }}>
|
||||
<PageHeader
|
||||
title="智能表单填充"
|
||||
subtitle="基于指纹识别的精准数据注入"
|
||||
icon={<PlayArrowIcon />}
|
||||
/>
|
||||
<Container maxWidth="sm" sx={{ py: 2, px: 0 }}>
|
||||
{/* 操作区域 */}
|
||||
<Paper
|
||||
sx={{
|
||||
p: 2,
|
||||
mb: 2.5,
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 4,
|
||||
border: '1px solid',
|
||||
borderColor: 'grey.100',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
mb: 1.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" fontWeight={800} color="text.primary">
|
||||
填充控制
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={refreshPreview}
|
||||
size="small"
|
||||
startIcon={<RefreshIcon />}
|
||||
sx={{ borderRadius: 3 }}
|
||||
>
|
||||
刷新预览
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={injectAllFields}
|
||||
size="small"
|
||||
startIcon={<PlayArrowIcon />}
|
||||
disabled={isInjecting || entries.length === 0}
|
||||
sx={{
|
||||
borderRadius: 3,
|
||||
bgcolor: formMappingPageStyles.secondaryColor || '#9c27b0',
|
||||
boxShadow: `0 4px 12px ${alpha(formMappingPageStyles.secondaryColor || '#9c27b0', 0.2)}`,
|
||||
}}
|
||||
>
|
||||
{isInjecting ? '注入中...' : '开始填充'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
点击"开始填充"后,将根据映射配置向网页表单注入数据。
|
||||
</Typography>
|
||||
</Paper>
|
||||
|
||||
{/* 字段列表 */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
mb: 1.5,
|
||||
px: 0.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2" fontWeight={800} color="text.secondary">
|
||||
映射字段 ({entries.length})
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<List
|
||||
sx={{
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
border: '1px solid',
|
||||
borderColor: 'grey.100',
|
||||
}}
|
||||
>
|
||||
{entries.length === 0 ? (
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="暂无映射字段"
|
||||
secondary="请先在表单映射页面配置字段"
|
||||
slotProps={{
|
||||
primary: {
|
||||
align: 'center',
|
||||
color: 'text.secondary',
|
||||
},
|
||||
secondary: {
|
||||
align: 'center',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
) : (
|
||||
entries.map((entry, index) => (
|
||||
<Box key={entry.id}>
|
||||
{index > 0 && <Divider />}
|
||||
<ListItem
|
||||
secondaryAction={
|
||||
<IconButton edge="end" aria-label="preview">
|
||||
<VisibilityIcon fontSize="small" />
|
||||
</IconButton>
|
||||
}
|
||||
sx={{ py: 1.5 }}
|
||||
>
|
||||
<Switch
|
||||
edge="start"
|
||||
checked={entry.ui_state.is_selected}
|
||||
disabled
|
||||
sx={{ mr: 2 }}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<span style={{ fontWeight: 500 }}>{entry.label_display}</span>
|
||||
<Chip
|
||||
size="small"
|
||||
label={getFieldTypeLabel(entry.action_logic.type)}
|
||||
sx={{
|
||||
fontSize: '0.65rem',
|
||||
bgcolor: 'grey.100',
|
||||
color: 'grey.700',
|
||||
}}
|
||||
/>
|
||||
<Chip
|
||||
size="small"
|
||||
label={getStrategyLabel(entry.action_logic.strategy)}
|
||||
sx={{
|
||||
fontSize: '0.65rem',
|
||||
bgcolor: formMappingPageStyles.secondaryColor + '20',
|
||||
color: formMappingPageStyles.secondaryColor || '#9c27b0',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<Box>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.7rem',
|
||||
color: 'text.secondary',
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
{entry.fingerprint.selector}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.75rem',
|
||||
color: 'primary.main',
|
||||
fontStyle: 'italic',
|
||||
wordBreak: 'break-all',
|
||||
maxWidth: '250px',
|
||||
}}
|
||||
>
|
||||
预览: {previewData.get(entry.id) || '---'}
|
||||
</Typography>
|
||||
{injectResults.has(entry.id) &&
|
||||
(injectResults.get(entry.id) ? (
|
||||
<CheckCircleIcon sx={{ color: '#32CD32', fontSize: '1rem' }} />
|
||||
) : (
|
||||
<CancelIcon sx={{ color: '#FF4444', fontSize: '1rem' }} />
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</List>
|
||||
|
||||
{/* 统计信息 */}
|
||||
{injectResults.size > 0 && (
|
||||
<Box sx={{ mt: 4 }}>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: 'grey.50',
|
||||
borderRadius: 3,
|
||||
border: '1px solid',
|
||||
borderColor: 'grey.200',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-around' }}>
|
||||
<Box textAlign="center">
|
||||
<Typography variant="h5" fontWeight={800} color="primary.main">
|
||||
{Array.from(injectResults.values()).filter(Boolean).length}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
成功注入
|
||||
</Typography>
|
||||
</Box>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<Box textAlign="center">
|
||||
<Typography variant="h5" fontWeight={800} color="error.main">
|
||||
{Array.from(injectResults.values()).filter((v) => !v).length}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
注入失败
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
)}
|
||||
</Container>
|
||||
</Container>
|
||||
|
||||
{/* 提示消息 */}
|
||||
<Snackbar
|
||||
open={showSuccess}
|
||||
autoHideDuration={3000}
|
||||
onClose={() => setShowSuccess(false)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert severity="success">填充完成!</Alert>
|
||||
</Snackbar>
|
||||
<Snackbar
|
||||
open={showError}
|
||||
autoHideDuration={4000}
|
||||
onClose={() => setShowError(false)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert severity="error">{errorMessage}</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Container,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
IconButton,
|
||||
Switch,
|
||||
Divider,
|
||||
Paper,
|
||||
alpha,
|
||||
Snackbar,
|
||||
Alert,
|
||||
} from '@mui/material';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
|
||||
import FileDownloadIcon from '@mui/icons-material/FileDownload';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { storageUtil } from '@/utils/chromeStorage';
|
||||
import { FormMapEntry } from '@/types/storage';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import Button from '@/components/Button';
|
||||
import { globalStyles, formMappingPageStyles } from '@/config/pageTheme.ts';
|
||||
|
||||
export default function FormMappingPage() {
|
||||
const [entries, setEntries] = useState<FormMapEntry[]>([]);
|
||||
const [isPicking, setIsPicking] = useState(false);
|
||||
const [exportError, setExportError] = useState<string | null>(null);
|
||||
const [showExportSuccess, setShowExportSuccess] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
const data = (await storageUtil.get('active_form_map')) as FormMapEntry[];
|
||||
setEntries(data || []);
|
||||
const picking = (await storageUtil.get('app/formMapping/isPicking')) as boolean;
|
||||
setIsPicking(picking || false);
|
||||
};
|
||||
|
||||
loadData();
|
||||
|
||||
const listener = (changes: { [key: string]: chrome.storage.StorageChange }, area: string) => {
|
||||
if (area === 'local') {
|
||||
if (changes['active_form_map']) {
|
||||
setEntries((changes['active_form_map'].newValue as FormMapEntry[]) || []);
|
||||
}
|
||||
if (changes['app/formMapping/isPicking']) {
|
||||
setIsPicking((changes['app/formMapping/isPicking'].newValue as boolean) || false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
chrome.storage.onChanged.addListener(listener);
|
||||
return () => chrome.storage.onChanged.removeListener(listener);
|
||||
}, []);
|
||||
|
||||
const togglePicking = async () => {
|
||||
await storageUtil.set('app/formMapping/isPicking', !isPicking);
|
||||
};
|
||||
|
||||
const deleteEntry = async (id: string) => {
|
||||
const newEntries = entries.filter((e) => e.id !== id);
|
||||
await storageUtil.set('active_form_map', newEntries);
|
||||
};
|
||||
|
||||
const toggleSelection = async (id: string) => {
|
||||
const newEntries = entries.map((e) =>
|
||||
e.id === id ? { ...e, ui_state: { ...e.ui_state, is_selected: !e.ui_state.is_selected } } : e,
|
||||
);
|
||||
await storageUtil.set('active_form_map', newEntries);
|
||||
};
|
||||
|
||||
const clearAll = async () => {
|
||||
await storageUtil.set('active_form_map', []);
|
||||
await storageUtil.set('app/formMapping/isPicking', false);
|
||||
};
|
||||
|
||||
const exportConfig = () => {
|
||||
try {
|
||||
if (entries.length === 0) {
|
||||
setExportError('没有可导出的配置数据');
|
||||
return;
|
||||
}
|
||||
|
||||
const jsonStr = JSON.stringify(entries, null, 2);
|
||||
const blob = new Blob([jsonStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const date = new Date();
|
||||
const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
const filename = `form-mapping-config-${dateStr}.json`;
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
setShowExportSuccess(true);
|
||||
} catch (error) {
|
||||
console.error('导出配置失败:', error);
|
||||
setExportError(error instanceof Error ? error.message : '导出失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ bgcolor: globalStyles.backgroundColor, minHeight: '100%', pb: 3 }}>
|
||||
<Container sx={{ py: 2, bgcolor: globalStyles.backgroundColor }}>
|
||||
<PageHeader
|
||||
title="通用表单映射助手"
|
||||
subtitle="智能识别表单指纹,自定义填充逻辑"
|
||||
icon={<AutoFixHighIcon />}
|
||||
/>
|
||||
<Container maxWidth="sm" sx={{ py: 2, px: 0 }}>
|
||||
<Paper
|
||||
sx={{
|
||||
p: 2,
|
||||
mb: 2.5,
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 4,
|
||||
border: '1px solid',
|
||||
borderColor: 'grey.100',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
mb: 1.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" fontWeight={800} color="text.primary">
|
||||
状态控制
|
||||
</Typography>
|
||||
<Button
|
||||
variant={isPicking ? 'contained' : 'outlined'}
|
||||
onClick={togglePicking}
|
||||
size="small"
|
||||
startIcon={<AddCircleOutlineIcon />}
|
||||
sx={{
|
||||
borderRadius: 3,
|
||||
px: 2,
|
||||
fontWeight: 800,
|
||||
...(isPicking
|
||||
? {
|
||||
bgcolor: 'secondary.main',
|
||||
boxShadow: `0 4px 12px ${alpha(formMappingPageStyles.secondaryColor || '#9c27b0', 0.2)}`,
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{isPicking ? '正在拾取...' : '开始拾取'}
|
||||
</Button>
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
点击“开始拾取”后,直接在网页上点击想要映射的表单元素。
|
||||
</Typography>
|
||||
</Paper>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
mb: 1.5,
|
||||
px: 0.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2" fontWeight={800} color="text.secondary">
|
||||
已拾取字段 ({entries.length})
|
||||
</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={clearAll}
|
||||
sx={{ fontWeight: 700, fontSize: '0.75rem' }}
|
||||
>
|
||||
清空全部
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<List
|
||||
sx={{
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
border: '1px solid',
|
||||
borderColor: 'grey.100',
|
||||
}}
|
||||
>
|
||||
{entries.length === 0 ? (
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="暂无数据"
|
||||
secondary="点击上方按钮开始探测网页表单"
|
||||
slotProps={{
|
||||
primary: {
|
||||
align: 'center',
|
||||
color: 'text.secondary',
|
||||
},
|
||||
secondary: {
|
||||
align: 'center',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
) : (
|
||||
entries.map((entry, index) => (
|
||||
<Box key={entry.id}>
|
||||
{index > 0 && <Divider />}
|
||||
<ListItem
|
||||
secondaryAction={
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="delete"
|
||||
onClick={() => deleteEntry(entry.id)}
|
||||
sx={{ color: 'error.light' }}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
}
|
||||
sx={{ py: 1.5 }}
|
||||
>
|
||||
<Switch
|
||||
edge="start"
|
||||
checked={entry.ui_state.is_selected}
|
||||
onChange={() => toggleSelection(entry.id)}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={entry.label_display}
|
||||
secondary={entry.fingerprint.selector}
|
||||
slotProps={{
|
||||
primary: { fontWeight: 500 },
|
||||
secondary: {
|
||||
sx: {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.7rem',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: '200px',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</List>
|
||||
|
||||
{entries.length > 0 && (
|
||||
<Box sx={{ mt: 4 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
mb: 1.5,
|
||||
px: 0.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 800, color: 'text.secondary' }}>
|
||||
映射配置导出 (JSON)
|
||||
</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={exportConfig}
|
||||
startIcon={<FileDownloadIcon />}
|
||||
sx={{ fontWeight: 700, fontSize: '0.75rem', borderRadius: 3 }}
|
||||
>
|
||||
导出配置
|
||||
</Button>
|
||||
</Box>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: 'grey.50',
|
||||
borderRadius: 3,
|
||||
border: '1px solid',
|
||||
borderColor: 'grey.200',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.7rem',
|
||||
maxHeight: '180px',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<pre style={{ margin: 0 }}>{JSON.stringify(entries, null, 2)}</pre>
|
||||
</Paper>
|
||||
</Box>
|
||||
)}
|
||||
</Container>
|
||||
</Container>
|
||||
</Box>
|
||||
<Snackbar
|
||||
open={!!exportError}
|
||||
autoHideDuration={4000}
|
||||
onClose={() => setExportError(null)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert severity="error" onClose={() => setExportError(null)}>
|
||||
{exportError}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
<Snackbar
|
||||
open={showExportSuccess}
|
||||
autoHideDuration={3000}
|
||||
onClose={() => setShowExportSuccess(false)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert severity="success" onClose={() => setShowExportSuccess(false)}>
|
||||
配置导出成功!
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -339,6 +339,7 @@ const FormRecognizerPage = () => {
|
||||
title="表单测试数据填充器"
|
||||
subtitle="一键填充表单测试数据,提升开发和测试效率"
|
||||
icon={<InputIcon />}
|
||||
iconColor={formRecognizerPageStyles.primaryColor}
|
||||
sx={{ mb: 2.5 }}
|
||||
/>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import UrlEntryForm from '@/components/UrlEntryForm';
|
||||
import UrlEntryList from '@/components/UrlEntryList';
|
||||
import { useUrlPreferences } from '@/utils/useUrlPreferences';
|
||||
import type { OpenUrlEntry } from '@/types/storage';
|
||||
import { dashboardPageStyles } from '@/config/pageTheme';
|
||||
import { dashboardPageStyles, openUrlPageStyles } from '@/config/pageTheme';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
|
||||
export default function OpenUrlPage() {
|
||||
@@ -41,6 +41,7 @@ export default function OpenUrlPage() {
|
||||
title="URL 工具"
|
||||
subtitle="快速打开 URL 或复制链接"
|
||||
icon={<LanguageIcon />}
|
||||
iconColor={openUrlPageStyles.primaryColor}
|
||||
sx={{ mb: 2.5 }}
|
||||
/>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import GlobalSnackbar, { useSnackbar } from '@/components/GlobalSnackbar';
|
||||
import UrlToQrCodeSection from '@/components/UrlToQrCodeSection';
|
||||
import QrCodeToUrlSection from '@/components/QrCodeToUrlSection';
|
||||
import { useStorageState } from '@/utils/useStorageState';
|
||||
import { dashboardPageStyles } from '@/config/pageTheme';
|
||||
import { dashboardPageStyles, qrCodePageStyles } from '@/config/pageTheme';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
|
||||
const QrCodePage = () => {
|
||||
@@ -33,12 +33,13 @@ const QrCodePage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ minHeight: '100%', pb: 3, bgcolor: dashboardPageStyles.backgroundColor }}>
|
||||
<Box sx={{ minHeight: '100%', pb: 3 }}>
|
||||
<Container sx={{ py: 2, maxWidth: 400, bgcolor: dashboardPageStyles.backgroundColor }}>
|
||||
<PageHeader
|
||||
title="二维码工具"
|
||||
subtitle="生成和解析二维码"
|
||||
icon={<QrCodeIcon />}
|
||||
iconColor={qrCodePageStyles.primaryColor}
|
||||
sx={{ mb: 2.5 }}
|
||||
/>
|
||||
|
||||
|
||||
Generated
+150
-435
@@ -1891,44 +1891,6 @@
|
||||
"integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "2.0.5",
|
||||
"run-parallel": "^1.1.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.stat": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.walk": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
"fastq": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@pnpm/config.env-replace": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz",
|
||||
@@ -3039,9 +3001,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wxt-dev/browser": {
|
||||
"version": "0.1.32",
|
||||
"resolved": "https://registry.npmmirror.com/@wxt-dev/browser/-/browser-0.1.32.tgz",
|
||||
"integrity": "sha512-jvfSppeLzlH4sOkIvMBJoA1pKoI+U5gTkjDwMKdkTWh0P/fj+KDyze3lzo3S6372viCm8tXUKNez+VKyVz2ZDw==",
|
||||
"version": "0.1.40",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@wxt-dev/browser/-/browser-0.1.40.tgz",
|
||||
"integrity": "sha512-h2/v/Hpkj5sz//h84ProqBaAcTsDFRKp9b/JVHOK/r7LT0XLE+ZDs5YN1BnFLUEHdM7G3fUjTyBG84cayXQshQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3050,18 +3012,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wxt-dev/module-react": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmmirror.com/@wxt-dev/module-react/-/module-react-1.1.5.tgz",
|
||||
"integrity": "sha512-KgsUrsgH5rBT8MwiipnDEOHBXmLvTIdFICrI7KjngqSf9DpVRn92HsKmToxY0AYpkP19hHWta2oNYFTzmmm++g==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@wxt-dev/module-react/-/module-react-1.2.2.tgz",
|
||||
"integrity": "sha512-+lRLi1r9dAXpLySWSIWHLJ1h/nFzR20iQnx3RNrKyA6oJg4+ClOluVXozHjfPg9Okfy/umtffiOopGayASrg6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "^4.4.1 || ^5.0.0"
|
||||
"@vitejs/plugin-react": "^4.4.1 || ^5.0.0 || ^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/wxt-dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.4.19 || ^6.3.4 || ^7.0.0 || ^8.0.0-0",
|
||||
"wxt": ">=0.19.16"
|
||||
}
|
||||
},
|
||||
@@ -3706,7 +3668,7 @@
|
||||
},
|
||||
"node_modules/bundle-name": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/bundle-name/-/bundle-name-4.1.0.tgz",
|
||||
"integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
@@ -3749,36 +3711,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/c12/node_modules/chokidar": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz",
|
||||
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/c12/node_modules/readdirp": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-5.0.0.tgz",
|
||||
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/cac": {
|
||||
"version": "6.7.14",
|
||||
"resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz",
|
||||
@@ -3923,16 +3855,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/chokidar/-/chokidar-5.0.0.tgz",
|
||||
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
"readdirp": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
@@ -4000,9 +3932,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ci-info": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/ci-info/-/ci-info-4.3.1.tgz",
|
||||
"integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==",
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/ci-info/-/ci-info-4.4.0.tgz",
|
||||
"integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -4054,19 +3986,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-spinners": {
|
||||
"version": "2.9.2",
|
||||
"resolved": "https://registry.npmmirror.com/cli-spinners/-/cli-spinners-2.9.2.tgz",
|
||||
"integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-truncate": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-4.0.0.tgz",
|
||||
@@ -4642,9 +4561,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/default-browser": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/default-browser/-/default-browser-5.4.0.tgz",
|
||||
"integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==",
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/default-browser/-/default-browser-5.5.0.tgz",
|
||||
"integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4660,7 +4579,7 @@
|
||||
},
|
||||
"node_modules/default-browser-id": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/default-browser-id/-/default-browser-id-5.0.1.tgz",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/default-browser-id/-/default-browser-id-5.0.1.tgz",
|
||||
"integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
@@ -4691,7 +4610,7 @@
|
||||
},
|
||||
"node_modules/define-lazy-prop": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
|
||||
"integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
@@ -5623,23 +5542,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
"glob-parent": "^5.1.2",
|
||||
"merge2": "^1.3.0",
|
||||
"micromatch": "^4.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
@@ -5664,16 +5566,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz",
|
||||
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
|
||||
@@ -5706,9 +5598,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/filesize": {
|
||||
"version": "11.0.13",
|
||||
"resolved": "https://registry.npmmirror.com/filesize/-/filesize-11.0.13.tgz",
|
||||
"integrity": "sha512-mYJ/qXKvREuO0uH8LTQJ6v7GsUvVOguqxg2VTwQUkyTPXXRRWPdjuUPVqdBrJQhvci48OHlNGRnux+Slr2Rnvw==",
|
||||
"version": "11.0.17",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/filesize/-/filesize-11.0.17.tgz",
|
||||
"integrity": "sha512-oHLTvMLw6imZUl1se/RBQrFlyy50nXce4sU7yGR6Qc0JgCwqnfiFsAnEwotdGmfKLD7SArGUk2/5STU0k8LOBQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
@@ -6070,19 +5962,6 @@
|
||||
"giget": "dist/cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-to-regexp": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
|
||||
@@ -6306,9 +6185,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/hookable": {
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
|
||||
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/hookable/-/hookable-6.1.1.tgz",
|
||||
"integrity": "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -6673,7 +6552,7 @@
|
||||
},
|
||||
"node_modules/is-docker": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/is-docker/-/is-docker-3.0.0.tgz",
|
||||
"integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
@@ -6775,9 +6654,22 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-in-ssh": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/is-in-ssh/-/is-in-ssh-1.0.0.tgz",
|
||||
"integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-inside-container": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-inside-container/-/is-inside-container-1.0.0.tgz",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/is-inside-container/-/is-inside-container-1.0.0.tgz",
|
||||
"integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
@@ -6811,19 +6703,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-interactive": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-interactive/-/is-interactive-2.0.0.tgz",
|
||||
"integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-map": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz",
|
||||
@@ -7041,19 +6920,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-unicode-supported": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
||||
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-weakmap": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/is-weakmap/-/is-weakmap-2.0.2.tgz",
|
||||
@@ -7101,9 +6967,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/is-wsl": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-3.1.0.tgz",
|
||||
"integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/is-wsl/-/is-wsl-3.1.1.tgz",
|
||||
"integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -7537,19 +7403,6 @@
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lint-staged/node_modules/nano-spawn": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/nano-spawn/-/nano-spawn-2.0.0.tgz",
|
||||
"integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/nano-spawn?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/lint-staged/node_modules/slice-ansi": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-7.1.2.tgz",
|
||||
@@ -7643,36 +7496,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/log-symbols": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-6.0.0.tgz",
|
||||
"integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
"is-unicode-supported": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/log-symbols/node_modules/is-unicode-supported": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
|
||||
"integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/log-update": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/log-update/-/log-update-6.1.0.tgz",
|
||||
@@ -7777,15 +7600,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/magicast": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmmirror.com/magicast/-/magicast-0.3.5.tgz",
|
||||
"integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/magicast/-/magicast-0.5.2.tgz",
|
||||
"integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.25.4",
|
||||
"@babel/types": "^7.25.4",
|
||||
"source-map-js": "^1.2.0"
|
||||
"@babel/parser": "^7.29.0",
|
||||
"@babel/types": "^7.29.0",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
@@ -7822,16 +7645,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz",
|
||||
@@ -7905,45 +7718,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch/node_modules/brace-expansion": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
|
||||
@@ -8025,9 +7799,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nano-spawn": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/nano-spawn/-/nano-spawn-1.0.3.tgz",
|
||||
"integrity": "sha512-jtpsQDetTnvS2Ts1fiRdci5rx0VYws5jGyC+4IYOTnIQ/wwdf6JdomlHBwqC3bJYOvaKu0C2GSZ1A60anrYpaA==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/nano-spawn/-/nano-spawn-2.1.0.tgz",
|
||||
"integrity": "sha512-yTW+2okrElHiH4fsiz/+/zc0EDo9BDDoC3iKk8dpv1GeRc9nUWzUZHx6TofMWErchhUQR8hY9/Eu1Uja9x1nqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -8056,6 +7830,16 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/nanospinner": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/nanospinner/-/nanospinner-1.2.2.tgz",
|
||||
"integrity": "sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picocolors": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
@@ -8198,15 +7982,15 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nypm": {
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmmirror.com/nypm/-/nypm-0.6.4.tgz",
|
||||
"integrity": "sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw==",
|
||||
"version": "0.6.6",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/nypm/-/nypm-0.6.6.tgz",
|
||||
"integrity": "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"citty": "^0.2.0",
|
||||
"citty": "^0.2.2",
|
||||
"pathe": "^2.0.3",
|
||||
"tinyexec": "^1.0.2"
|
||||
"tinyexec": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"nypm": "dist/cli.mjs"
|
||||
@@ -8216,9 +8000,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nypm/node_modules/citty": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/citty/-/citty-0.2.0.tgz",
|
||||
"integrity": "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA==",
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/citty/-/citty-0.2.2.tgz",
|
||||
"integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -8386,19 +8170,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/open": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/open/-/open-10.2.0.tgz",
|
||||
"integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/open/-/open-11.0.0.tgz",
|
||||
"integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"default-browser": "^5.2.1",
|
||||
"default-browser": "^5.4.0",
|
||||
"define-lazy-prop": "^3.0.0",
|
||||
"is-in-ssh": "^1.0.0",
|
||||
"is-inside-container": "^1.0.0",
|
||||
"wsl-utils": "^0.1.0"
|
||||
"powershell-utils": "^0.1.0",
|
||||
"wsl-utils": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
@@ -8422,30 +8208,6 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ora": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/ora/-/ora-8.2.0.tgz",
|
||||
"integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
"cli-cursor": "^5.0.0",
|
||||
"cli-spinners": "^2.9.2",
|
||||
"is-interactive": "^2.0.0",
|
||||
"is-unicode-supported": "^2.0.0",
|
||||
"log-symbols": "^6.0.0",
|
||||
"stdin-discarder": "^0.2.2",
|
||||
"string-width": "^7.2.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/os-shim": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/os-shim/-/os-shim-0.1.3.tgz",
|
||||
@@ -8773,9 +8535,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.8",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/postcss/-/postcss-8.5.8.tgz",
|
||||
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
||||
"version": "8.5.12",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/postcss/-/postcss-8.5.12.tgz",
|
||||
"integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -8791,7 +8553,6 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -8801,6 +8562,19 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/powershell-utils": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/powershell-utils/-/powershell-utils-0.1.0.tgz",
|
||||
"integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -9212,27 +8986,6 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/quick-format-unescaped": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
|
||||
@@ -9356,13 +9109,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/readdirp/-/readdirp-5.0.0.tgz",
|
||||
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
@@ -9525,17 +9278,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz",
|
||||
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"iojs": ">=1.0.0",
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rfdc": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
|
||||
@@ -9597,7 +9339,7 @@
|
||||
},
|
||||
"node_modules/run-applescript": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/run-applescript/-/run-applescript-7.1.0.tgz",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/run-applescript/-/run-applescript-7.1.0.tgz",
|
||||
"integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
@@ -9608,30 +9350,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-array-concat": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
|
||||
@@ -10136,19 +9854,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stdin-discarder": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
|
||||
"integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/stop-iteration-iterator": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
||||
@@ -10480,9 +10185,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.0.2.tgz",
|
||||
"integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/tinyexec/-/tinyexec-1.1.2.tgz",
|
||||
"integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -12057,80 +11762,90 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wsl-utils": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.1.0.tgz",
|
||||
"integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==",
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/wsl-utils/-/wsl-utils-0.3.1.tgz",
|
||||
"integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-wsl": "^3.1.0"
|
||||
"is-wsl": "^3.1.0",
|
||||
"powershell-utils": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/wxt": {
|
||||
"version": "0.20.13",
|
||||
"resolved": "https://registry.npmmirror.com/wxt/-/wxt-0.20.13.tgz",
|
||||
"integrity": "sha512-FwQEk+0a4/pYha6rTKGl5iicU6kRYDBDiElJf55CFEfoJKqvGzBTZpphafurQfqU1X0hvAm9w5GEWC0thXI6wQ==",
|
||||
"version": "0.20.25",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/wxt/-/wxt-0.20.25.tgz",
|
||||
"integrity": "sha512-ca+8Yt0Auzn9tX0ZW2Kzocb9yM8F/RoOjcYQ0fHkwcSc7/IUkqV2+1JUNn1SMSNAS4Gr3YQHAn/pi3q+jIGRqw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@1natsu/wait-element": "^4.1.2",
|
||||
"@aklinker1/rollup-plugin-visualizer": "5.12.0",
|
||||
"@webext-core/fake-browser": "^1.3.2",
|
||||
"@webext-core/isolated-element": "^1.1.2",
|
||||
"@webext-core/fake-browser": "^1.3.4",
|
||||
"@webext-core/isolated-element": "^1.1.3",
|
||||
"@webext-core/match-patterns": "^1.0.3",
|
||||
"@wxt-dev/browser": "^0.1.32",
|
||||
"@wxt-dev/browser": "^0.1.40",
|
||||
"@wxt-dev/storage": "^1.0.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"c12": "^3.3.2",
|
||||
"cac": "^6.7.14",
|
||||
"chokidar": "^4.0.3",
|
||||
"ci-info": "^4.3.1",
|
||||
"c12": "^3.3.3",
|
||||
"cac": "^6.7.14 || ^7.0.0",
|
||||
"chokidar": "^5.0.0",
|
||||
"ci-info": "^4.4.0",
|
||||
"consola": "^3.4.2",
|
||||
"defu": "^6.1.4",
|
||||
"dotenv": "^17.2.3",
|
||||
"dotenv-expand": "^12.0.3",
|
||||
"esbuild": "^0.27.1",
|
||||
"fast-glob": "^3.3.3",
|
||||
"filesize": "^11.0.13",
|
||||
"fs-extra": "^11.3.2",
|
||||
"filesize": "^11.0.15",
|
||||
"get-port-please": "^3.2.0",
|
||||
"giget": "^1.2.3 || ^2.0.0",
|
||||
"hookable": "^5.5.3",
|
||||
"giget": "^1.2.3 || ^2.0.0 || ^3.0.0",
|
||||
"hookable": "^6.1.0",
|
||||
"import-meta-resolve": "^4.2.0",
|
||||
"is-wsl": "^3.1.0",
|
||||
"is-wsl": "^3.1.1",
|
||||
"json5": "^2.2.3",
|
||||
"jszip": "^3.10.1",
|
||||
"linkedom": "^0.18.12",
|
||||
"magicast": "^0.3.5",
|
||||
"minimatch": "^10.1.1",
|
||||
"nano-spawn": "^1.0.3",
|
||||
"magicast": "^0.5.2",
|
||||
"nano-spawn": "^2.0.0",
|
||||
"nanospinner": "^1.2.2",
|
||||
"normalize-path": "^3.0.0",
|
||||
"nypm": "^0.6.2",
|
||||
"nypm": "^0.6.5",
|
||||
"ohash": "^2.0.11",
|
||||
"open": "^10.2.0",
|
||||
"ora": "^8.2.0",
|
||||
"perfect-debounce": "^2.0.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"open": "^11.0.0",
|
||||
"perfect-debounce": "^2.1.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"prompts": "^2.4.2",
|
||||
"publish-browser-extension": "^2.3.0 || ^3.0.2",
|
||||
"publish-browser-extension": "^2.3.0 || ^3.0.2 || ^4.0.4",
|
||||
"scule": "^1.3.0",
|
||||
"unimport": "^3.13.1 || ^4.0.0 || ^5.0.0",
|
||||
"vite": "^5.4.19 || ^6.3.4 || ^7.0.0",
|
||||
"vite-node": "^3.2.4 || ^5.0.0",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"unimport": "^3.13.1 || ^4.0.0 || ^5.0.0 || ^6.0.0",
|
||||
"vite": "^5.4.19 || ^6.3.4 || ^7.0.0 || ^8.0.0-0",
|
||||
"vite-node": "^3.2.4 || ^5.0.0 || ^6.0.0",
|
||||
"web-ext-run": "^0.2.4"
|
||||
},
|
||||
"bin": {
|
||||
"wxt": "bin/wxt.mjs",
|
||||
"wxt-publish-extension": "bin/wxt-publish-extension.cjs"
|
||||
"wxt-publish-extension": "bin/wxt-publish-extension.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"bun": ">=1.2.0",
|
||||
"node": ">=20.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/wxt-dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"eslint": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xdg-basedir": {
|
||||
|
||||
Vendored
+22
@@ -5,8 +5,28 @@ export type PageType =
|
||||
| 'openUrl'
|
||||
| 'qrCode'
|
||||
| 'formRecognizer'
|
||||
| 'formMapping'
|
||||
| 'formFill'
|
||||
| 'openUrlViewer';
|
||||
|
||||
export interface FormMapEntry {
|
||||
id: string;
|
||||
label_display: string;
|
||||
fingerprint: {
|
||||
selector: string;
|
||||
name_attr: string;
|
||||
placeholder: string;
|
||||
};
|
||||
action_logic: {
|
||||
type: 'text' | 'select' | 'checkbox';
|
||||
strategy: 'fixed' | 'random' | 'sequence';
|
||||
value: string;
|
||||
};
|
||||
ui_state: {
|
||||
is_selected: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface StorageSchema {
|
||||
'app/currentRoute': PageType;
|
||||
'app/popupRoute': PageType;
|
||||
@@ -15,6 +35,8 @@ export interface StorageSchema {
|
||||
'app/pageOrder': PageType[];
|
||||
'app/lastRoute': string;
|
||||
'app/theme': string;
|
||||
'app/formMapping/isPicking': boolean;
|
||||
active_form_map: FormMapEntry[];
|
||||
'storageCleaner/preferences': StorageCleanerPreferences;
|
||||
'openUrl/preferences': OpenUrlPreferences;
|
||||
'openUrl/currentUrl': string;
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { FormMapEntry } from '@/types/storage';
|
||||
|
||||
/**
|
||||
* 可视化交互模块:负责在网页上绘制非破坏性的高亮遮罩
|
||||
*/
|
||||
export class VisualHighlighter {
|
||||
private canvas: HTMLCanvasElement | null = null;
|
||||
private ctx: CanvasRenderingContext2D | null = null;
|
||||
private isVisible = false;
|
||||
|
||||
constructor() {
|
||||
this.handleResize = this.handleResize.bind(this);
|
||||
}
|
||||
|
||||
public init() {
|
||||
if (this.canvas) return;
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.canvas.id = 'form-mapping-highlighter';
|
||||
Object.assign(this.canvas.style, {
|
||||
position: 'fixed',
|
||||
top: '0',
|
||||
left: '0',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
pointerEvents: 'none',
|
||||
zIndex: '2147483647',
|
||||
display: 'none',
|
||||
});
|
||||
document.body.appendChild(this.canvas);
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
window.addEventListener('scroll', this.handleResize);
|
||||
}
|
||||
|
||||
public show() {
|
||||
if (!this.canvas) this.init();
|
||||
this.isVisible = true;
|
||||
this.canvas!.style.display = 'block';
|
||||
this.handleResize();
|
||||
}
|
||||
|
||||
public hide() {
|
||||
this.isVisible = false;
|
||||
if (this.canvas) this.canvas.style.display = 'none';
|
||||
}
|
||||
|
||||
private handleResize() {
|
||||
if (!this.isVisible || !this.canvas || !this.ctx) return;
|
||||
this.canvas.width = window.innerWidth;
|
||||
this.canvas.height = window.innerHeight;
|
||||
this.draw();
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心渲染循环
|
||||
*/
|
||||
public draw(entries: FormMapEntry[] = []) {
|
||||
if (!this.ctx || !this.isVisible) return;
|
||||
this.ctx.clearRect(0, 0, this.canvas!.width, this.canvas!.height);
|
||||
|
||||
entries.forEach((entry) => {
|
||||
const el = document.querySelector<HTMLElement>(entry.fingerprint.selector);
|
||||
if (!el) return;
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
||||
// 检查元素是否在视口内
|
||||
if (
|
||||
rect.bottom < 0 ||
|
||||
rect.top > window.innerHeight ||
|
||||
rect.right < 0 ||
|
||||
rect.left > window.innerWidth
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置样式
|
||||
if (entry.ui_state.is_selected) {
|
||||
// 选中状态:亮黄色边框
|
||||
this.ctx!.strokeStyle = '#FFD700';
|
||||
this.ctx!.lineWidth = 3;
|
||||
this.ctx!.fillStyle = 'rgba(255, 215, 0, 0.2)';
|
||||
} else {
|
||||
// 未选中状态:浅蓝色半透明
|
||||
this.ctx!.strokeStyle = 'rgba(173, 216, 230, 0.8)';
|
||||
this.ctx!.lineWidth = 1;
|
||||
this.ctx!.fillStyle = 'rgba(173, 216, 230, 0.4)';
|
||||
}
|
||||
|
||||
// 绘制矩形
|
||||
this.ctx!.beginPath();
|
||||
this.ctx!.rect(rect.left, rect.top, rect.width, rect.height);
|
||||
this.ctx!.fill();
|
||||
this.ctx!.stroke();
|
||||
|
||||
// 如果被选中,绘制一个小标签
|
||||
if (entry.ui_state.is_selected) {
|
||||
this.ctx!.fillStyle = '#FFD700';
|
||||
this.ctx!.font = '12px sans-serif';
|
||||
this.ctx!.fillText(entry.label_display, rect.left, rect.top - 5);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启拾取模式:拦截点击事件
|
||||
*/
|
||||
public enablePicker(onPick: (element: HTMLElement) => void) {
|
||||
if (!this.canvas) this.init();
|
||||
this.canvas!.style.pointerEvents = 'auto';
|
||||
this.canvas!.style.cursor = 'crosshair';
|
||||
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 暂时禁用 canvas pointer-events 以便探测下方的真实元素
|
||||
this.canvas!.style.pointerEvents = 'none';
|
||||
const el = document.elementFromPoint(e.clientX, e.clientY) as HTMLElement;
|
||||
this.canvas!.style.pointerEvents = 'auto';
|
||||
|
||||
if (el) {
|
||||
onPick(el);
|
||||
}
|
||||
};
|
||||
|
||||
this.canvas!.addEventListener('click', handleClick, { capture: true, once: true });
|
||||
}
|
||||
|
||||
public disablePicker() {
|
||||
if (this.canvas) {
|
||||
this.canvas.style.pointerEvents = 'none';
|
||||
this.canvas.style.cursor = 'default';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const highlighter = new VisualHighlighter();
|
||||
@@ -0,0 +1,100 @@
|
||||
import { FormMapEntry } from '@/types/storage';
|
||||
|
||||
/**
|
||||
* 智能探测引擎:负责扫描 DOM 并生成唯一指纹
|
||||
*/
|
||||
export class SmartDetector {
|
||||
/**
|
||||
* 扫描页面中符合条件的表单元素
|
||||
*/
|
||||
public static scanFormElements(): HTMLElement[] {
|
||||
const selector =
|
||||
'input:not([type="hidden"]):not([type="submit"]):not([type="button"]), textarea, select, [contenteditable="true"]';
|
||||
const elements = Array.from(document.querySelectorAll<HTMLElement>(selector));
|
||||
return elements.filter((el) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
return rect.width > 0 && rect.height > 0 && window.getComputedStyle(el).display !== 'none';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成元素的唯一性指纹
|
||||
*/
|
||||
public static generateFingerprint(element: HTMLElement): FormMapEntry['fingerprint'] {
|
||||
return {
|
||||
selector: this.getUniqueSelector(element),
|
||||
name_attr: element.getAttribute('name') || element.getAttribute('id') || '',
|
||||
placeholder: element.getAttribute('placeholder') || '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取元素的语义标签 (核心算法)
|
||||
* 优先查找 label[for],其次在物理位置上方或左侧 50px 范围内寻找文本
|
||||
*/
|
||||
public static extractSemanticLabel(element: HTMLElement): string {
|
||||
// 1. 尝试查找关联的 label 元素
|
||||
if (element.id) {
|
||||
const label = document.querySelector(`label[for="${element.id}"]`);
|
||||
if (label?.textContent) return label.textContent.trim();
|
||||
}
|
||||
|
||||
// 2. 尝试向上查找父级中的 label
|
||||
const parentLabel = element.closest('label');
|
||||
if (parentLabel?.textContent) return parentLabel.textContent.trim();
|
||||
|
||||
// 3. 物理位置探测算法 (getBoundingClientRect)
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
// 探测左侧 50px
|
||||
const leftText = this.getTextNearby(rect.left - 25, rect.top + rect.height / 2);
|
||||
if (leftText) return leftText;
|
||||
|
||||
// 探测上方 50px
|
||||
const topText = this.getTextNearby(rect.left + rect.width / 2, rect.top - 25);
|
||||
if (topText) return topText;
|
||||
|
||||
// 4. 降级:使用 placeholder 或 name
|
||||
return element.getAttribute('placeholder') || element.getAttribute('name') || '未知字段';
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定坐标附近寻找最可能的文本节点
|
||||
*/
|
||||
private static getTextNearby(x: number, y: number): string | null {
|
||||
if (x < 0 || y < 0) return null;
|
||||
const el = document.elementFromPoint(x, y);
|
||||
if (!el) return null;
|
||||
|
||||
// 如果命中了文本容器
|
||||
const text = el.textContent?.trim();
|
||||
if (text && text.length < 30) return text; // 避免抓到太长的段落
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算元素的相对短且唯一的 CSS 选择器
|
||||
*/
|
||||
private static getUniqueSelector(el: HTMLElement): string {
|
||||
if (el.id) return `#${el.id}`;
|
||||
|
||||
let path = el.tagName.toLowerCase();
|
||||
|
||||
// 尝试添加类名以增加唯一性
|
||||
if (el.classList.length > 0) {
|
||||
path += `.${Array.from(el.classList).join('.')}`;
|
||||
}
|
||||
|
||||
// 如果当前路径在文档中不是唯一的,则增加 nth-child
|
||||
if (document.querySelectorAll(path).length > 1) {
|
||||
const parent = el.parentElement;
|
||||
if (parent) {
|
||||
const index = Array.from(parent.children).indexOf(el) + 1;
|
||||
path = `${this.getUniqueSelector(parent as HTMLElement)} > ${el.tagName.toLowerCase()}:nth-child(${index})`;
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,579 @@
|
||||
import { FormMapEntry } from '@/types/storage';
|
||||
|
||||
/**
|
||||
* 模糊匹配引擎结果接口
|
||||
*/
|
||||
export interface MatchResult {
|
||||
element: HTMLElement | null;
|
||||
score: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入结果接口
|
||||
*/
|
||||
export interface InjectResult {
|
||||
success: boolean;
|
||||
entry: FormMapEntry;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模糊匹配引擎
|
||||
* 根据 JSON 指纹在页面中精准定位目标 DOM 元素
|
||||
*/
|
||||
export class FuzzyMatcher {
|
||||
private static readonly MATCH_THRESHOLD = 75;
|
||||
private static readonly SCORE_SELECTOR = 50;
|
||||
private static readonly SCORE_NAME_ATTR = 25;
|
||||
private static readonly SCORE_PLACEHOLDER = 15;
|
||||
private static readonly SCORE_NEIGHBOR_TEXT = 10;
|
||||
|
||||
/**
|
||||
* 根据指纹查找目标元素
|
||||
* @param fingerprint - 表单字段指纹
|
||||
* @returns 匹配结果(包含元素和得分)
|
||||
*/
|
||||
public static findTargetElement(fingerprint: FormMapEntry['fingerprint']): MatchResult {
|
||||
const candidates: Array<{ element: HTMLElement; score: number }> = [];
|
||||
|
||||
// 1. 首先尝试精确选择器匹配
|
||||
if (fingerprint.selector) {
|
||||
const exactMatch = document.querySelector<HTMLElement>(fingerprint.selector);
|
||||
if (exactMatch) {
|
||||
const score = this.calculateScore(exactMatch, fingerprint);
|
||||
candidates.push({ element: exactMatch, score });
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 收集所有可能的候选元素
|
||||
const potentialElements = this.collectPotentialElements(fingerprint);
|
||||
for (const element of potentialElements) {
|
||||
const score = this.calculateScore(element, fingerprint);
|
||||
if (score > 0) {
|
||||
candidates.push({ element, score });
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 找到最高分的候选
|
||||
if (candidates.length === 0) {
|
||||
return { element: null, score: 0 };
|
||||
}
|
||||
|
||||
const bestMatch = candidates.reduce((prev, curr) => (curr.score > prev.score ? curr : prev));
|
||||
|
||||
return bestMatch.score >= this.MATCH_THRESHOLD
|
||||
? { element: bestMatch.element, score: bestMatch.score }
|
||||
: { element: null, score: bestMatch.score };
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算元素匹配得分
|
||||
*/
|
||||
private static calculateScore(
|
||||
element: HTMLElement,
|
||||
fingerprint: FormMapEntry['fingerprint'],
|
||||
): number {
|
||||
let score = 0;
|
||||
|
||||
// 选择器精确匹配
|
||||
if (fingerprint.selector) {
|
||||
const matched = document.querySelector(fingerprint.selector);
|
||||
if (matched === element) {
|
||||
score += this.SCORE_SELECTOR;
|
||||
}
|
||||
}
|
||||
|
||||
// name 或 id 属性匹配
|
||||
if (fingerprint.name_attr) {
|
||||
const elementName = element.getAttribute('name') || '';
|
||||
const elementId = element.getAttribute('id') || '';
|
||||
if (elementName === fingerprint.name_attr || elementId === fingerprint.name_attr) {
|
||||
score += this.SCORE_NAME_ATTR;
|
||||
}
|
||||
}
|
||||
|
||||
// placeholder 匹配
|
||||
if (fingerprint.placeholder) {
|
||||
const elementPlaceholder =
|
||||
'placeholder' in element && (element as HTMLInputElement).placeholder;
|
||||
if (elementPlaceholder === fingerprint.placeholder) {
|
||||
score += this.SCORE_PLACEHOLDER;
|
||||
}
|
||||
}
|
||||
|
||||
// 邻近文本(label)匹配
|
||||
if (fingerprint.name_attr || fingerprint.placeholder) {
|
||||
const neighborText = this.getNeighborText(element);
|
||||
const searchText = fingerprint.name_attr || fingerprint.placeholder || '';
|
||||
if (neighborText.includes(searchText)) {
|
||||
score += this.SCORE_NEIGHBOR_TEXT;
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集潜在的候选元素
|
||||
*/
|
||||
private static collectPotentialElements(
|
||||
_fingerprint: FormMapEntry['fingerprint'],
|
||||
): HTMLElement[] {
|
||||
const elements: HTMLElement[] = [];
|
||||
|
||||
// 获取所有表单元素
|
||||
const selectors = [
|
||||
'input:not([type="hidden"])',
|
||||
'textarea',
|
||||
'select',
|
||||
'[contenteditable="true"]',
|
||||
];
|
||||
|
||||
for (const selector of selectors) {
|
||||
const found = document.querySelectorAll<HTMLElement>(selector);
|
||||
found.forEach((el) => {
|
||||
if (this.isVisibleElement(el)) {
|
||||
elements.push(el);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元素附近的文本内容
|
||||
*/
|
||||
private static getNeighborText(element: HTMLElement): string {
|
||||
const texts: string[] = [];
|
||||
|
||||
// 查找关联的 label
|
||||
const id = element.getAttribute('id');
|
||||
if (id) {
|
||||
const label = document.querySelector(`label[for="${id}"]`);
|
||||
if (label) {
|
||||
texts.push(label.textContent || '');
|
||||
}
|
||||
}
|
||||
|
||||
// 查找父级 label
|
||||
const parentLabel = element.closest('label');
|
||||
if (parentLabel) {
|
||||
texts.push(parentLabel.textContent || '');
|
||||
}
|
||||
|
||||
// 查找相邻元素的文本
|
||||
const prevSibling = element.previousElementSibling;
|
||||
const nextSibling = element.nextElementSibling;
|
||||
if (prevSibling) {
|
||||
texts.push(prevSibling.textContent || '');
|
||||
}
|
||||
if (nextSibling) {
|
||||
texts.push(nextSibling.textContent || '');
|
||||
}
|
||||
|
||||
// 查找父级内的文本节点
|
||||
const parent = element.parentElement;
|
||||
if (parent) {
|
||||
const textNodes = parent.querySelectorAll('span, div, p');
|
||||
textNodes.forEach((node) => {
|
||||
texts.push(node.textContent || '');
|
||||
});
|
||||
}
|
||||
|
||||
return texts.join(' ').toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查元素是否可见
|
||||
*/
|
||||
private static isVisibleElement(element: HTMLElement): boolean {
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.width === 0 || rect.height === 0) return false;
|
||||
|
||||
const style = window.getComputedStyle(element);
|
||||
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能注入引擎
|
||||
* 突破 React/Vue 等现代框架的表单状态绑定
|
||||
*/
|
||||
export class SmartInjectionEngine {
|
||||
/**
|
||||
* 注入数据到目标元素
|
||||
* @param element - 目标 DOM 元素
|
||||
* @param entry - 表单映射条目
|
||||
* @returns 注入结果
|
||||
*/
|
||||
public static inject(element: HTMLElement, entry: FormMapEntry, mockValue: string): InjectResult {
|
||||
try {
|
||||
const { action_logic } = entry;
|
||||
|
||||
switch (action_logic.type) {
|
||||
case 'text':
|
||||
this.injectText(element as HTMLInputElement | HTMLTextAreaElement, mockValue);
|
||||
break;
|
||||
|
||||
case 'select':
|
||||
this.injectSelect(element as HTMLSelectElement, action_logic);
|
||||
break;
|
||||
|
||||
case 'checkbox':
|
||||
this.injectCheckbox(element as HTMLInputElement, action_logic);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.injectText(element as HTMLInputElement | HTMLTextAreaElement, mockValue);
|
||||
}
|
||||
|
||||
return { success: true, entry };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
entry,
|
||||
error: error instanceof Error ? error.message : '注入失败',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入文本类输入框
|
||||
*/
|
||||
private static injectText(element: HTMLInputElement | HTMLTextAreaElement, value: string): void {
|
||||
// 获取原生 setter
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
||||
element instanceof HTMLInputElement
|
||||
? window.HTMLInputElement.prototype
|
||||
: window.HTMLTextAreaElement.prototype,
|
||||
'value',
|
||||
)?.set;
|
||||
|
||||
if (nativeInputValueSetter) {
|
||||
nativeInputValueSetter.call(element, value);
|
||||
} else {
|
||||
element.value = value;
|
||||
}
|
||||
|
||||
// 连续触发事件
|
||||
element.dispatchEvent(new Event('focus', { bubbles: true }));
|
||||
element.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
element.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
element.dispatchEvent(new Event('blur', { bubbles: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入下拉框
|
||||
*/
|
||||
private static injectSelect(
|
||||
element: HTMLSelectElement,
|
||||
actionLogic: FormMapEntry['action_logic'],
|
||||
): void {
|
||||
if (actionLogic.strategy === 'random') {
|
||||
// 随机选择
|
||||
const options = Array.from(element.options).filter((opt) => !opt.disabled);
|
||||
if (options.length > 0) {
|
||||
const randomIndex = Math.floor(Math.random() * options.length);
|
||||
element.selectedIndex = randomIndex;
|
||||
}
|
||||
} else {
|
||||
// 使用固定值
|
||||
const value = actionLogic.value;
|
||||
const matchingOption = Array.from(element.options).find(
|
||||
(opt) => opt.value === value || opt.text === value,
|
||||
);
|
||||
if (matchingOption) {
|
||||
element.value = matchingOption.value;
|
||||
} else if (element.options.length > 0) {
|
||||
element.selectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
element.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入复选框/单选框
|
||||
*/
|
||||
private static injectCheckbox(
|
||||
element: HTMLInputElement,
|
||||
actionLogic: FormMapEntry['action_logic'],
|
||||
): void {
|
||||
if (actionLogic.strategy === 'random') {
|
||||
// 随机选择
|
||||
const isChecked = Math.random() > 0.5;
|
||||
if (element.type === 'checkbox') {
|
||||
element.checked = isChecked;
|
||||
} else if (element.type === 'radio') {
|
||||
// 对于单选框,找到同 name 的所有选项并随机选择一个
|
||||
const radioGroup = document.querySelectorAll<HTMLInputElement>(
|
||||
`input[type="radio"][name="${element.name}"]`,
|
||||
);
|
||||
if (radioGroup.length > 0) {
|
||||
const randomIndex = Math.floor(Math.random() * radioGroup.length);
|
||||
radioGroup[randomIndex].click();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 使用固定值
|
||||
const shouldCheck = actionLogic.value === 'true' || actionLogic.value === '1';
|
||||
element.checked = shouldCheck;
|
||||
if (shouldCheck) {
|
||||
element.click();
|
||||
}
|
||||
}
|
||||
|
||||
element.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock 数据生成器
|
||||
*/
|
||||
export class MockDataGenerator {
|
||||
/**
|
||||
* 根据策略生成随机数据
|
||||
*/
|
||||
public static generate(actionLogic: FormMapEntry['action_logic'], entry: FormMapEntry): string {
|
||||
const { strategy, value, type } = actionLogic;
|
||||
|
||||
if (strategy === 'fixed') {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 根据字段类型和策略生成数据
|
||||
switch (type) {
|
||||
case 'text':
|
||||
return this.generateText(entry);
|
||||
|
||||
case 'select':
|
||||
return this.generateSelectValue();
|
||||
|
||||
case 'checkbox':
|
||||
return this.generateBoolean();
|
||||
|
||||
default:
|
||||
return this.generateText(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文本数据
|
||||
*/
|
||||
private static generateText(entry: FormMapEntry): string {
|
||||
const { fingerprint, action_logic } = entry;
|
||||
const { strategy, value: pattern } = action_logic;
|
||||
|
||||
// 根据模式生成数据
|
||||
if (pattern) {
|
||||
return this.generateByPattern(pattern);
|
||||
}
|
||||
|
||||
// 根据指纹特征推断数据类型
|
||||
const name = fingerprint.name_attr.toLowerCase();
|
||||
const placeholder = fingerprint.placeholder.toLowerCase();
|
||||
|
||||
if (name.includes('phone') || placeholder.includes('phone')) {
|
||||
return this.generatePhoneNumber();
|
||||
}
|
||||
if (name.includes('email') || placeholder.includes('email')) {
|
||||
return this.generateEmail();
|
||||
}
|
||||
if (name.includes('name') || placeholder.includes('name')) {
|
||||
return this.generateName();
|
||||
}
|
||||
if (name.includes('id') || name.includes('card')) {
|
||||
return this.generateIdCard();
|
||||
}
|
||||
if (name.includes('date') || placeholder.includes('date')) {
|
||||
return this.generateDate();
|
||||
}
|
||||
if (name.includes('number') || placeholder.includes('number')) {
|
||||
return this.generateNumber();
|
||||
}
|
||||
|
||||
// 默认生成随机文本
|
||||
return strategy === 'random' ? this.generateRandomText() : '测试数据';
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模式生成数据
|
||||
*/
|
||||
private static generateByPattern(pattern: string): string {
|
||||
if (pattern.includes('phone') || pattern.includes('mobile')) {
|
||||
return this.generatePhoneNumber();
|
||||
}
|
||||
if (pattern.includes('email')) {
|
||||
return this.generateEmail();
|
||||
}
|
||||
if (pattern.includes('name')) {
|
||||
return this.generateName();
|
||||
}
|
||||
if (pattern.includes('date')) {
|
||||
return this.generateDate();
|
||||
}
|
||||
if (pattern.includes('idcard') || pattern.includes('身份证')) {
|
||||
return this.generateIdCard();
|
||||
}
|
||||
if (/^\d+$/.test(pattern)) {
|
||||
return this.generateNumber(pattern.length);
|
||||
}
|
||||
|
||||
return pattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成手机号
|
||||
*/
|
||||
private static generatePhoneNumber(): string {
|
||||
const prefix = '1' + ['3', '4', '5', '6', '7', '8', '9'][Math.floor(Math.random() * 7)];
|
||||
const suffix = Math.floor(Math.random() * 1000000000)
|
||||
.toString()
|
||||
.padStart(9, '0');
|
||||
return prefix + suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成邮箱
|
||||
*/
|
||||
private static generateEmail(): string {
|
||||
const names = ['test', 'user', 'admin', 'guest', 'demo'];
|
||||
const domains = ['example.com', 'test.com', 'gmail.com', 'outlook.com'];
|
||||
const name = names[Math.floor(Math.random() * names.length)];
|
||||
const domain = domains[Math.floor(Math.random() * domains.length)];
|
||||
const num = Math.floor(Math.random() * 1000);
|
||||
return `${name}${num}@${domain}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成姓名
|
||||
*/
|
||||
private static generateName(): string {
|
||||
const surnames = ['张', '李', '王', '刘', '陈', '杨', '赵', '黄'];
|
||||
const givenNames = ['伟', '芳', '强', '英', '华', '建', '明', '娜'];
|
||||
return (
|
||||
surnames[Math.floor(Math.random() * surnames.length)] +
|
||||
givenNames[Math.floor(Math.random() * givenNames.length)]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成身份证号
|
||||
*/
|
||||
private static generateIdCard(): string {
|
||||
const areaCodes = ['110101', '310101', '440101', '120101', '320101'];
|
||||
const areaCode = areaCodes[Math.floor(Math.random() * areaCodes.length)];
|
||||
const year = (1980 + Math.floor(Math.random() * 30)).toString();
|
||||
const month = String(1 + Math.floor(Math.random() * 12)).padStart(2, '0');
|
||||
const day = String(1 + Math.floor(Math.random() * 28)).padStart(2, '0');
|
||||
const random = Math.floor(Math.random() * 10000)
|
||||
.toString()
|
||||
.padStart(4, '0');
|
||||
return areaCode + year + month + day + random;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成日期
|
||||
*/
|
||||
private static generateDate(): string {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - Math.floor(Math.random() * 365));
|
||||
return date.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成数字
|
||||
*/
|
||||
private static generateNumber(length: number = 6): string {
|
||||
return Math.floor(Math.random() * Math.pow(10, length))
|
||||
.toString()
|
||||
.padStart(length, '0');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机文本
|
||||
*/
|
||||
private static generateRandomText(): string {
|
||||
const texts = ['测试内容', '示例文本', 'Lorem ipsum', '随机数据', 'Sample Text'];
|
||||
return texts[Math.floor(Math.random() * texts.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成下拉框值
|
||||
*/
|
||||
private static generateSelectValue(): string {
|
||||
return '选项' + (Math.floor(Math.random() * 5) + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成布尔值
|
||||
*/
|
||||
private static generateBoolean(): string {
|
||||
return Math.random() > 0.5 ? 'true' : 'false';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 视觉反馈渲染器
|
||||
*/
|
||||
export class FeedbackRenderer {
|
||||
private static readonly SUCCESS_COLOR = '#32CD32';
|
||||
private static readonly ERROR_COLOR = '#FF4444';
|
||||
private static readonly HIGHLIGHT_DURATION = 3000;
|
||||
|
||||
/**
|
||||
* 渲染成功反馈
|
||||
*/
|
||||
public static renderSuccess(element: HTMLElement): void {
|
||||
this.applyHighlight(element, this.SUCCESS_COLOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染失败反馈
|
||||
*/
|
||||
public static renderError(element: HTMLElement | null): void {
|
||||
if (!element) return;
|
||||
this.applyHighlight(element, this.ERROR_COLOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用高亮样式
|
||||
*/
|
||||
private static applyHighlight(element: HTMLElement, color: string): void {
|
||||
// 保存原始样式
|
||||
const originalStyle = element.getAttribute('style') || '';
|
||||
element.setAttribute('data-original-style', originalStyle);
|
||||
|
||||
// 应用高亮
|
||||
element.style.outline = `3px solid ${color}`;
|
||||
element.style.outlineOffset = '2px';
|
||||
element.style.transition = 'outline 0.3s ease';
|
||||
|
||||
// 自动移除高亮
|
||||
setTimeout(() => {
|
||||
const savedStyle = element.getAttribute('data-original-style');
|
||||
if (savedStyle) {
|
||||
element.setAttribute('style', savedStyle);
|
||||
element.removeAttribute('data-original-style');
|
||||
} else {
|
||||
element.style.outline = '';
|
||||
element.style.outlineOffset = '';
|
||||
}
|
||||
}, this.HIGHLIGHT_DURATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有高亮
|
||||
*/
|
||||
public static clearAllHighlights(): void {
|
||||
const highlightedElements = document.querySelectorAll('[data-original-style]');
|
||||
highlightedElements.forEach((element) => {
|
||||
const savedStyle = element.getAttribute('data-original-style');
|
||||
if (savedStyle) {
|
||||
element.setAttribute('style', savedStyle);
|
||||
element.removeAttribute('data-original-style');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ export interface MessagePayload {
|
||||
includeHidden?: boolean;
|
||||
fieldId?: string;
|
||||
fieldIds?: string[];
|
||||
data?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,6 +56,8 @@ export interface MessageResponse {
|
||||
totalCount?: number;
|
||||
validCount?: number;
|
||||
hasModal?: boolean;
|
||||
results?: unknown[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import jsQR from 'jsqr';
|
||||
|
||||
export interface QrCodeParseResult {
|
||||
success: boolean;
|
||||
data?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function parseQrCodeFromFile(
|
||||
file: File,
|
||||
timeout: number = 10000,
|
||||
): Promise<QrCodeParseResult> {
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
return { success: false, error: '无法创建 canvas 上下文' };
|
||||
}
|
||||
|
||||
const image = new Image();
|
||||
image.src = URL.createObjectURL(file);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
reject(new Error('图片加载超时'));
|
||||
}, timeout);
|
||||
|
||||
image.onload = () => {
|
||||
clearTimeout(timeoutId);
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
ctx.drawImage(image, 0, 0);
|
||||
resolve();
|
||||
};
|
||||
|
||||
image.onerror = () => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(new Error('图片加载失败'));
|
||||
};
|
||||
});
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const code = jsQR(imageData.data, imageData.width, imageData.height);
|
||||
|
||||
if (code) {
|
||||
return { success: true, data: code.data };
|
||||
} else {
|
||||
return { success: false, error: '未检测到二维码' };
|
||||
}
|
||||
} catch (err) {
|
||||
return { success: false, error: err instanceof Error ? err.message : '解析失败' };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user