Develop fill form (#13)

* feat(formRecognizer): 添加表单识别功能及相关组件

添加表单识别功能,包括以下内容:
1. 在路由配置中添加表单识别页面
2. 实现表单识别页面和侧边栏面板
3. 添加表单数据生成工具类
4. 实现与内容脚本的通信机制
5. 添加faker-js依赖用于生成测试数据
6. 支持不同入口点(popup/sidepanel)的组件渲染

* refactor(消息通信): 重构消息通信机制并集中管理消息协议

将分散的消息协议和通信逻辑集中到 utils/messages.ts 中
移除旧的 messages.tsx 文件并更新相关引用
添加消息动作枚举和类型定义,提高类型安全性
优化内容脚本注入失败时的处理逻辑

* feat(QR码): 添加粘贴图片功能并优化上传组件

添加全局粘贴事件监听,支持从剪贴板直接粘贴二维码图片进行解析。重构上传组件为独立组件QrCodeUploader,包含拖拽上传、预览、进度显示和错误处理功能。优化页面样式和用户体验。

- 在QrCodePage添加粘贴事件监听
- 创建QrCodeUploader组件整合上传功能
- 更新测试用例格式
- 调整多个页面的背景色样式

* feat(表单识别): 新增表单识别页面功能与模板管理

- 添加表单识别页面样式配置
- 实现表单字段扫描与展示功能
- 新增数据模板管理工具类
- 添加数据验证工具类
- 扩展表单识别页面功能,包括操作历史记录
- 支持模板的导入导出功能
- 优化表单填充操作的用户体验

* feat(消息系统): 添加标签页刷新功能

在消息系统中新增 RELOAD_TAB 动作类型和 tabId 字段,用于处理标签页刷新请求
修改 StorageCleanerPage 使用后台脚本发送刷新请求,确保弹窗关闭后仍能执行
在 background.ts 中添加标签页刷新处理逻辑,包括错误处理和响应返回

* refactor(theme): 重构主题颜色和样式配置

- 更新主题颜色以满足 WCAG AA 可访问性标准
- 提取全局样式配置到统一变量
- 使用语义化颜色变量替换硬编码值
- 为输入框样式创建统一配置

* feat: add URL entry management components and QR code generation feature

- Introduced `UrlEntryItem` and `UrlEntryList` components for displaying and managing URL entries.
- Added `UrlToQrCodeSection` component for generating QR codes from URLs with download and copy functionality.
- Implemented `AutoRefreshToggle`, `CleaningResult`, `DomainHeader`, `ErrorDisplay`, `OptionItem`, and `StorageOptionsGrid` components for enhanced user interface in storage cleaning.
- Created custom hooks `useStorageCleaner` and `useStorageState` for managing storage-related states and preferences.
- Added utility hook `useUrlPreferences` for handling URL entry preferences.

* feat: 新增时间戳转换器和相关组件,优化时间戳页面功能

* Refactor message handling and storage cleaning logic

* feat: 添加 GitHub Actions CI/CD 工作流,支持自动化构建与发布

* fix(background): 修复标签页刷新使用browser.tabs代替chrome.tabs
style(popup): 调整QR码页面样式顺序
refactor(storage): 重构存储清理逻辑并移除useSnackbar依赖
style(confirm): 优化存储清理确认对话框的UI样式
chore: 更新.gitignore忽略.trae目录

* chore: 移除 workbuddy 相关文件并更新 gitignore

* test: 更新 StorageCleanerConfirm 组件测试中的文本匹配
This commit is contained in:
LingandRX
2026-04-23 22:10:09 +08:00
committed by GitHub
parent 4850c92365
commit af0089c980
7 changed files with 145 additions and 70 deletions
+3
View File
@@ -25,3 +25,6 @@ stats-*.json
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
.trae/*
.workbuddy/*
+111 -38
View File
@@ -6,9 +6,11 @@ import {
Typography, Typography,
Box, Box,
Chip, Chip,
alpha,
} from '@mui/material'; } from '@mui/material';
import type { StorageCleanerOptions } from '@/types/storage'; import type { StorageCleanerOptions } from '@/types/storage';
import Button from '@/components/Button'; import Button from '@/components/Button';
import { THEME_COLORS } from '@/config/pageTheme';
export interface StorageCleanerConfirmProps { export interface StorageCleanerConfirmProps {
open: boolean; open: boolean;
@@ -17,6 +19,15 @@ export interface StorageCleanerConfirmProps {
options: StorageCleanerOptions; options: StorageCleanerOptions;
} }
const STORAGE_LABELS: Record<keyof StorageCleanerOptions, string> = {
localStorage: 'LocalStorage',
sessionStorage: 'Session Storage',
indexedDB: 'IndexedDB',
cookies: 'Cookies',
cacheStorage: 'Cache Storage',
serviceWorkers: 'Service Workers',
};
export function StorageCleanerConfirm({ export function StorageCleanerConfirm({
open, open,
onClose, onClose,
@@ -25,7 +36,7 @@ export function StorageCleanerConfirm({
}: StorageCleanerConfirmProps) { }: StorageCleanerConfirmProps) {
const selectedOptions = Object.entries(options) const selectedOptions = Object.entries(options)
.filter(([_, value]) => value) .filter(([_, value]) => value)
.map(([key, _]) => key); .map(([key, _]) => STORAGE_LABELS[key as keyof StorageCleanerOptions] || key);
return ( return (
<Dialog <Dialog
@@ -36,65 +47,127 @@ export function StorageCleanerConfirm({
slotProps={{ slotProps={{
paper: { paper: {
sx: { sx: {
borderRadius: 5, borderRadius: 6,
backgroundImage: 'none', backgroundImage: 'none',
boxShadow: '0 24px 48px -12px rgba(0,0,0,0.15)', boxShadow: `0 24px 64px -12px ${alpha(THEME_COLORS.black, 0.18)}`,
p: 1 p: 1.5,
bgcolor: 'background.paper',
}, },
}, },
}} }}
> >
<DialogTitle sx={{ textAlign: 'center', pt: 3, pb: 1, fontWeight: 900, letterSpacing: '-0.5px', fontSize: '1.25rem' }}> <DialogTitle
sx={{
textAlign: 'center',
pt: 4,
pb: 1,
fontWeight: 900,
letterSpacing: '-0.5px',
fontSize: '1.35rem',
color: 'text.primary',
}}
>
</DialogTitle> </DialogTitle>
<DialogContent sx={{ textAlign: 'center', pb: 2 }}> <DialogContent sx={{ textAlign: 'center', pb: 2 }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3, fontWeight: 500 }}> <Typography
variant="body2"
color="text.secondary"
sx={{ mb: 3.5, fontWeight: 500, fontSize: '0.9rem' }}
>
</Typography> </Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, justifyContent: 'center', mb: 3 }}> <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1.2, justifyContent: 'center', mb: 4 }}>
{selectedOptions.map((opt) => ( {selectedOptions.map((label) => (
<Chip <Chip
key={opt} key={label}
label={opt} label={label}
size="small" size="small"
sx={{ sx={{
bgcolor: 'grey.50', bgcolor: alpha(THEME_COLORS.warning, 0.04),
fontWeight: 600, fontWeight: 700,
color: 'text.secondary', color: THEME_COLORS.warning,
fontSize: '0.7rem', fontSize: '0.75rem',
border: '1px solid', border: '1px solid',
borderColor: 'grey.200' borderColor: alpha(THEME_COLORS.warning, 0.15),
}} borderRadius: 2.5,
height: 'auto',
'& .MuiChip-label': { px: 1.2, py: 0.6 },
}}
/> />
))} ))}
</Box> </Box>
<Typography variant="caption" sx={{ color: '#ff9800', fontWeight: 700, bgcolor: '#fff4e5', px: 1.5, py: 0.5, borderRadius: 2 }}> <Box
sx={{
</Typography> display: 'inline-flex',
alignItems: 'center',
gap: 1,
bgcolor: alpha(THEME_COLORS.error, 0.05),
color: THEME_COLORS.error,
px: 2,
py: 0.8,
borderRadius: 3,
border: '1px dashed',
borderColor: alpha(THEME_COLORS.error, 0.2),
}}
>
<Typography
variant="caption"
sx={{
fontWeight: 800,
display: 'flex',
alignItems: 'center',
gap: 0.5,
fontSize: '0.75rem',
}}
>
<span role="img" aria-label="warning">
</span>{' '}
</Typography>
</Box>
</DialogContent> </DialogContent>
<DialogActions sx={{ p: 2.5, gap: 1.5 }}> <DialogActions sx={{ p: 3, pt: 1, gap: 2 }}>
<Button <Button
variant="text" variant="text"
onClick={onClose} onClick={onClose}
fullWidth fullWidth
sx={{ fontWeight: 700, color: 'text.secondary', borderRadius: 3 }} sx={{
fontWeight: 700,
color: 'text.secondary',
borderRadius: 4,
fontSize: '0.95rem',
'&:hover': {
bgcolor: 'grey.100',
color: 'text.primary',
},
}}
> >
</Button> </Button>
<Button <Button
variant="contained" variant="contained"
onClick={onConfirm} onClick={onConfirm}
fullWidth fullWidth
sx={{ sx={{
bgcolor: '#ff9800', bgcolor: THEME_COLORS.warning,
'&:hover': { bgcolor: '#f57c00' },
fontWeight: 800, fontWeight: 800,
borderRadius: 3, borderRadius: 4,
boxShadow: 'none' fontSize: '0.95rem',
boxShadow: `0 8px 20px ${alpha(THEME_COLORS.warning, 0.25)}`,
'&:hover': {
bgcolor: THEME_COLORS.warningDark,
boxShadow: `0 12px 28px ${alpha(THEME_COLORS.warning, 0.35)}`,
transform: 'translateY(-2px)',
},
'&:active': {
transform: 'translateY(0)',
},
}} }}
> >
@@ -28,7 +28,7 @@ describe('StorageCleanerConfirm Component', () => {
onConfirm={mockOnConfirm} onConfirm={mockOnConfirm}
options={defaultOptions} options={defaultOptions}
{...props} {...props}
/> />,
); );
}; };
@@ -45,9 +45,9 @@ describe('StorageCleanerConfirm Component', () => {
it('should display selected options as chips', () => { it('should display selected options as chips', () => {
renderComponent(); renderComponent();
expect(screen.getByText('localStorage')).toBeInTheDocument(); expect(screen.getByText('LocalStorage')).toBeInTheDocument();
expect(screen.getByText('sessionStorage')).toBeInTheDocument(); expect(screen.getByText('Session Storage')).toBeInTheDocument();
expect(screen.getByText('cookies')).toBeInTheDocument(); expect(screen.getByText('Cookies')).toBeInTheDocument();
}); });
it('should display cancel and confirm buttons', () => { it('should display cancel and confirm buttons', () => {
@@ -88,10 +88,10 @@ describe('StorageCleanerConfirm Component', () => {
renderComponent({ options: partialOptions }); renderComponent({ options: partialOptions });
expect(screen.getByText('localStorage')).toBeInTheDocument(); expect(screen.getByText('LocalStorage')).toBeInTheDocument();
expect(screen.getByText('indexedDB')).toBeInTheDocument(); expect(screen.getByText('IndexedDB')).toBeInTheDocument();
expect(screen.queryByText('sessionStorage')).not.toBeInTheDocument(); expect(screen.queryByText('Session Storage')).not.toBeInTheDocument();
expect(screen.queryByText('cookies')).not.toBeInTheDocument(); expect(screen.queryByText('Cookies')).not.toBeInTheDocument();
}); });
it('should handle empty options', () => { it('should handle empty options', () => {
@@ -129,8 +129,8 @@ describe('StorageCleanerConfirm Component', () => {
renderComponent({ options: customOptions }); renderComponent({ options: customOptions });
expect(screen.getByText('sessionStorage')).toBeInTheDocument(); expect(screen.getByText('Session Storage')).toBeInTheDocument();
expect(screen.getByText('cookies')).toBeInTheDocument(); expect(screen.getByText('Cookies')).toBeInTheDocument();
}); });
}); });
}); });
+1 -1
View File
@@ -48,7 +48,7 @@ export default defineBackground(() => {
const delay = message.delay || 0; const delay = message.delay || 0;
const executeReload = () => { const executeReload = () => {
chrome.tabs browser.tabs
.reload(tabId) .reload(tabId)
.then(() => { .then(() => {
console.log('标签页刷新成功:', tabId); console.log('标签页刷新成功:', tabId);
+2 -2
View File
@@ -33,8 +33,8 @@ const QrCodePage = () => {
} }
return ( return (
<Box sx={{ bgcolor: dashboardPageStyles.backgroundColor, minHeight: '100%', pb: 3 }}> <Box sx={{ minHeight: '100%', pb: 3, bgcolor: dashboardPageStyles.backgroundColor }}>
<Container sx={{ py: 2, maxWidth: 400 }}> <Container sx={{ py: 2, maxWidth: 400, bgcolor: dashboardPageStyles.backgroundColor }}>
{/* Header */} {/* Header */}
<Stack direction="row" spacing={1.5} alignItems="center" sx={{ mb: 2.5 }}> <Stack direction="row" spacing={1.5} alignItems="center" sx={{ mb: 2.5 }}>
<Box <Box
@@ -11,7 +11,7 @@ import ErrorDisplay from './components/ErrorDisplay';
import CleaningResult from './components/CleaningResult'; import CleaningResult from './components/CleaningResult';
export default function StorageCleanerPage() { export default function StorageCleanerPage() {
const { snackbarProps } = useSnackbar(); const { snackbarProps, showMessage } = useSnackbar();
const { const {
domain, domain,
error, error,
@@ -30,7 +30,7 @@ export default function StorageCleanerPage() {
handleOptionChange, handleOptionChange,
handleSelectAll, handleSelectAll,
handleClean, handleClean,
} = useStorageCleaner(); } = useStorageCleaner({ showMessage });
if (isInitializing) { if (isInitializing) {
return ( return (
+15 -16
View File
@@ -1,5 +1,4 @@
import { useState, useEffect, useCallback, useRef } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
import { useSnackbar } from '@/components/GlobalSnackbar';
import { storageUtil } from '@/utils/chromeStorage'; import { storageUtil } from '@/utils/chromeStorage';
import type { import type {
StorageCleanerOptions, StorageCleanerOptions,
@@ -57,7 +56,16 @@ export interface UseStorageCleanerReturn {
handleClean: () => Promise<void>; handleClean: () => Promise<void>;
} }
export function useStorageCleaner(): UseStorageCleanerReturn { export interface UseStorageCleanerOptions {
showMessage: (
message: string,
options?: { severity?: 'success' | 'warning' | 'error' | 'info' },
) => void;
}
export function useStorageCleaner({
showMessage,
}: UseStorageCleanerOptions): UseStorageCleanerReturn {
const [domain, setDomain] = useState<string>(''); const [domain, setDomain] = useState<string>('');
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const [isInitializing, setIsInitializing] = useState<boolean>(true); const [isInitializing, setIsInitializing] = useState<boolean>(true);
@@ -67,7 +75,6 @@ export function useStorageCleaner(): UseStorageCleanerReturn {
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [result, setResult] = useState<CleaningResult | null>(null); const [result, setResult] = useState<CleaningResult | null>(null);
const [showConfirm, setShowConfirm] = useState<boolean>(false); const [showConfirm, setShowConfirm] = useState<boolean>(false);
const { showMessage } = useSnackbar();
const reloadTimeoutRef = useRef<NodeJS.Timeout | null>(null); const reloadTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const resultTimeoutRef = useRef<NodeJS.Timeout | null>(null); const resultTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const requestIdRef = useRef<number>(0); const requestIdRef = useRef<number>(0);
@@ -204,7 +211,7 @@ export function useStorageCleaner(): UseStorageCleanerReturn {
const handleClean = useCallback(async () => { const handleClean = useCallback(async () => {
const tab = await getCurrentTab(); const tab = await getCurrentTab();
if (!tab || !tab.id || !tab.url) { if (!tab || !tab.id || !tab.url) {
showMessage('无法获取当前标签页'); showMessage('无法获取当前标签页', { severity: 'warning' });
return; return;
} }
setLoading(true); setLoading(true);
@@ -212,19 +219,11 @@ export function useStorageCleaner(): UseStorageCleanerReturn {
const cleaningResult = await clearStorage(tab.id, tab.url, options); const cleaningResult = await clearStorage(tab.id, tab.url, options);
setResult(cleaningResult); setResult(cleaningResult);
// 5秒后自动清除结果提示 if (autoRefresh && cleaningResult.success) {
if (resultTimeoutRef.current) clearTimeout(resultTimeoutRef.current); showMessage('清理成功,即将刷新页面', { severity: 'success' });
resultTimeoutRef.current = setTimeout(() => { await chrome.runtime.sendMessage({ action: 'reloadTab', tabId: tab.id, delay: 1000 });
setResult(null);
}, 5000);
if (autoRefresh && cleaningResult.success && tab.id !== undefined) {
showMessage('清理成功,即将刷新页面');
// 立即发送刷新消息,并在后台处理延迟(或直接刷新)
// 这样即使弹窗关闭,后台也能收到指令
chrome.runtime.sendMessage({ action: 'reloadTab', tabId: tab.id, delay: 1000 });
} else { } else {
loadInfo(); await loadInfo();
} }
} catch (err) { } catch (err) {
showMessage(`清理失败: ${String(err)}`, { severity: 'error' }); showMessage(`清理失败: ${String(err)}`, { severity: 'error' });