diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 244240f..aa2d055 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,12 +4,9 @@ on: push: branches: - main - - develop - - develop-* pull_request: branches: - main - - develop concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..f98f909 --- /dev/null +++ b/GEMINI.md @@ -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`. diff --git a/components/QrCodeToUrlSection.tsx b/components/QrCodeToUrlSection.tsx index f84ab2c..e1e2efa 100644 --- a/components/QrCodeToUrlSection.tsx +++ b/components/QrCodeToUrlSection.tsx @@ -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) => { + const fileInputRef = useRef(null); + + const handleFileChange = useCallback((file: File) => { + setQrCodeFile(file); + setParseError(''); + setParsedUrl(''); + }, []); + + const handleInputChange = (e: React.ChangeEvent) => { 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) => { + e.preventDefault(); + setDragging(true); + }; + + const handleDragLeave = () => { + setDragging(false); + }; + + const handleDrop = (e: React.DragEvent) => { + 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((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 ( - + + {qrCodeFile.name} diff --git a/components/QrCodeUploader.tsx b/components/QrCodeUploader.tsx index d35f77a..0a74985 100644 --- a/components/QrCodeUploader.tsx +++ b/components/QrCodeUploader.tsx @@ -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 = ({ setProgress(0); try { - // 模拟上传进度 const progressInterval = setInterval(() => { setProgress((prev) => { if (prev >= 90) { @@ -78,51 +77,20 @@ const QrCodeUploader: React.FC = ({ }); }, 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((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 = ({ }); } finally { setUploading(false); - // 延迟清除进度,让用户看到完成状态 setTimeout(() => setProgress(0), 500); } }, diff --git a/components/UrlEntryForm.tsx b/components/UrlEntryForm.tsx index 1d7c280..675e7f0 100644 --- a/components/UrlEntryForm.tsx +++ b/components/UrlEntryForm.tsx @@ -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, - }, - }} /> { 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)}`, }, }} > diff --git a/config/__tests__/routes.test.ts b/config/__tests__/routes.test.ts index b579c3d..7e4a5e6 100644 --- a/config/__tests__/routes.test.ts +++ b/config/__tests__/routes.test.ts @@ -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); }); }); }); diff --git a/config/pageTheme.ts b/config/pageTheme.ts index 30044f8..690dd23 100644 --- a/config/pageTheme.ts +++ b/config/pageTheme.ts @@ -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; diff --git a/config/routes.ts b/config/routes.ts index f84c90e..d332221 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -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: '表单识别', diff --git a/entrypoints/content.ts b/entrypoints/content.ts index 9018a3a..ba5a6a7 100644 --- a/entrypoints/content.ts +++ b/entrypoints/content.ts @@ -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: [''], 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: '未知操作' }); } diff --git a/entrypoints/popup/pages/DashboardPage.tsx b/entrypoints/popup/pages/DashboardPage.tsx index dfc6820..c3e1ce3 100644 --- a/entrypoints/popup/pages/DashboardPage.tsx +++ b/entrypoints/popup/pages/DashboardPage.tsx @@ -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 ( + } + onClick={() => navigateTo('formMapping')} + cardBackgroundColor={dashboardPageStyles.cardBackgroundColor} + /> + ); case 'formRecognizer': return ( ); + case 'formFill': + return ( + } + onClick={() => navigateTo('formFill')} + cardBackgroundColor={dashboardPageStyles.cardBackgroundColor} + /> + ); default: return null; } diff --git a/entrypoints/popup/pages/FormFillPage.tsx b/entrypoints/popup/pages/FormFillPage.tsx new file mode 100644 index 0000000..7a002b1 --- /dev/null +++ b/entrypoints/popup/pages/FormFillPage.tsx @@ -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([]); + const [previewData, setPreviewData] = useState>(new Map()); + const [injectResults, setInjectResults] = useState>(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(); + 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(); + + 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 = { + text: '文本', + select: '下拉框', + checkbox: '复选框', + radio: '单选框', + }; + return labels[type] || type; + }; + + const getStrategyLabel = (strategy: string) => { + const labels: Record = { + fixed: '固定值', + random: '随机', + sequence: '序列', + }; + return labels[strategy] || strategy; + }; + + return ( + + + } + /> + + {/* 操作区域 */} + + + + 填充控制 + + + + + + + + 点击"开始填充"后,将根据映射配置向网页表单注入数据。 + + + + {/* 字段列表 */} + + + 映射字段 ({entries.length}) + + + + + {entries.length === 0 ? ( + + + + ) : ( + entries.map((entry, index) => ( + + {index > 0 && } + + + + } + sx={{ py: 1.5 }} + > + + + {entry.label_display} + + + + } + secondary={ + + + {entry.fingerprint.selector} + + + + 预览: {previewData.get(entry.id) || '---'} + + {injectResults.has(entry.id) && + (injectResults.get(entry.id) ? ( + + ) : ( + + ))} + + + } + /> + + + )) + )} + + + {/* 统计信息 */} + {injectResults.size > 0 && ( + + + + + + {Array.from(injectResults.values()).filter(Boolean).length} + + + 成功注入 + + + + + + {Array.from(injectResults.values()).filter((v) => !v).length} + + + 注入失败 + + + + + + )} + + + + {/* 提示消息 */} + setShowSuccess(false)} + anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} + > + 填充完成! + + setShowError(false)} + anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} + > + {errorMessage} + + + ); +} diff --git a/entrypoints/popup/pages/FormMappingPage.tsx b/entrypoints/popup/pages/FormMappingPage.tsx new file mode 100644 index 0000000..0a1f3c6 --- /dev/null +++ b/entrypoints/popup/pages/FormMappingPage.tsx @@ -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([]); + const [isPicking, setIsPicking] = useState(false); + const [exportError, setExportError] = useState(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 ( + + + + } + /> + + + + + 状态控制 + + + + + 点击“开始拾取”后,直接在网页上点击想要映射的表单元素。 + + + + + + 已拾取字段 ({entries.length}) + + + + + + {entries.length === 0 ? ( + + + + ) : ( + entries.map((entry, index) => ( + + {index > 0 && } + deleteEntry(entry.id)} + sx={{ color: 'error.light' }} + > + + + } + sx={{ py: 1.5 }} + > + toggleSelection(entry.id)} + /> + + + + )) + )} + + + {entries.length > 0 && ( + + + + 映射配置导出 (JSON) + + + + +
{JSON.stringify(entries, null, 2)}
+
+
+ )} +
+
+
+ setExportError(null)} + anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} + > + setExportError(null)}> + {exportError} + + + setShowExportSuccess(false)} + anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} + > + setShowExportSuccess(false)}> + 配置导出成功! + + +
+ ); +} diff --git a/entrypoints/popup/pages/FormRecognizerPage.tsx b/entrypoints/popup/pages/FormRecognizerPage.tsx index ef78e5f..ee55284 100644 --- a/entrypoints/popup/pages/FormRecognizerPage.tsx +++ b/entrypoints/popup/pages/FormRecognizerPage.tsx @@ -339,6 +339,7 @@ const FormRecognizerPage = () => { title="表单测试数据填充器" subtitle="一键填充表单测试数据,提升开发和测试效率" icon={} + iconColor={formRecognizerPageStyles.primaryColor} sx={{ mb: 2.5 }} /> diff --git a/entrypoints/popup/pages/OpenUrlPage.tsx b/entrypoints/popup/pages/OpenUrlPage.tsx index e3cbfe6..be7fc67 100644 --- a/entrypoints/popup/pages/OpenUrlPage.tsx +++ b/entrypoints/popup/pages/OpenUrlPage.tsx @@ -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={} + iconColor={openUrlPageStyles.primaryColor} sx={{ mb: 2.5 }} /> diff --git a/entrypoints/popup/pages/QrCodePage.tsx b/entrypoints/popup/pages/QrCodePage.tsx index 59364d2..412a2f7 100644 --- a/entrypoints/popup/pages/QrCodePage.tsx +++ b/entrypoints/popup/pages/QrCodePage.tsx @@ -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 ( - + } + iconColor={qrCodePageStyles.primaryColor} sx={{ mb: 2.5 }} /> diff --git a/package-lock.json b/package-lock.json index a54cdb1..d9ed201 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/types/storage.d.ts b/types/storage.d.ts index 65299d6..4590f71 100644 --- a/types/storage.d.ts +++ b/types/storage.d.ts @@ -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; diff --git a/utils/formMapping/highlighter.ts b/utils/formMapping/highlighter.ts new file mode 100644 index 0000000..3ebe3b0 --- /dev/null +++ b/utils/formMapping/highlighter.ts @@ -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(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(); diff --git a/utils/formMapping/scanner.ts b/utils/formMapping/scanner.ts new file mode 100644 index 0000000..cf804c7 --- /dev/null +++ b/utils/formMapping/scanner.ts @@ -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(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; + } +} diff --git a/utils/formMapping/smartInjector.ts b/utils/formMapping/smartInjector.ts new file mode 100644 index 0000000..86452e8 --- /dev/null +++ b/utils/formMapping/smartInjector.ts @@ -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(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(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( + `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'); + } + }); + } +} diff --git a/utils/messages.ts b/utils/messages.ts index 1985f18..1d75985 100644 --- a/utils/messages.ts +++ b/utils/messages.ts @@ -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; } /** diff --git a/utils/qrCodeParser.ts b/utils/qrCodeParser.ts new file mode 100644 index 0000000..43ef0aa --- /dev/null +++ b/utils/qrCodeParser.ts @@ -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 { + 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((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 : '解析失败' }; + } +}