* docs: 添加组件文档注释和类型导入

refactor: 统一使用 SnackbarOptions 类型
style: 优化导入语句顺序和格式

* refactor: 简化假数据生成器中的faker导入和使用

Co-authored-by: Copilot <copilot@github.com>

* feat(form-recognizer): 增强表单识别功能并优化UI交互

- 新增字段类型偏好设置功能,支持按域名保存字段类型
- 重构FieldList组件,改进字段选择和类型修改体验
- 添加字段定位闪烁功能,便于在页面上快速找到对应字段
- 优化表单填充逻辑,支持单个字段覆盖默认填充模式
- 移除独立的侧边栏页面,统一使用主页面组件
- 改进useStorageState钩子,增加加载状态管理和防抖处理

* refactor: 移除未使用的组件文件

* refactor(页面头部): 提取通用 PageHeader 组件并替换各页面头部实现

重构各页面头部为统一的 PageHeader 组件,提高代码复用性和维护性

* style(组件): 调整自动刷新开关和存储选项网格的样式

优化自动刷新开关的文本内边距,重构存储选项网格的布局结构,调整间距和边框样式

* style(ui): 调整时间戳页面和结果视图的样式

- 为时区选择器添加圆角
- 优化结果视图的布局和对齐方式
- 调整结果项的内边距和文本样式

* ci(workflow): 移除Firefox测试以简化CI流程

仅保留Chrome浏览器的构建步骤,减少CI运行时间和资源消耗
This commit is contained in:
LingandRX
2026-04-28 08:56:47 +08:00
committed by GitHub
parent fb5f98dd3b
commit c039475119
29 changed files with 908 additions and 1317 deletions
+6
View File
@@ -2,6 +2,12 @@ import { Button as MuiButton, ButtonProps as MuiButtonProps } from '@mui/materia
export type ButtonProps = MuiButtonProps;
/**
* 按钮组件
* @param sx 自定义样式
* @param props 其他按钮属性
* @returns 按钮组件
*/
export function Button({ sx = [], ...props }: ButtonProps) {
return (
<MuiButton
+14 -3
View File
@@ -3,14 +3,24 @@ import { IconButton, Tooltip } from '@mui/material';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import CheckIcon from '@mui/icons-material/Check';
import { copyToClipboard } from '@/utils/clipboard';
import type { SnackbarOptions } from '@/components/GlobalSnackbar';
/**
* 复制按钮组件属性
* @param text 要复制的文本
* @param tooltip 提示信息
* @param size 按钮大小
* @param color 按钮颜色
* @param style 自定义样式
* @param showMessage 消息提示函数,用于显示复制成功或失败的消息
*/
interface CopyButtonProps {
text: string;
tooltip?: string;
size?: 'small' | 'medium' | 'large';
color?: 'primary' | 'secondary' | 'success' | 'error' | 'info' | 'warning' | string;
style?: React.CSSProperties;
showMessage?: (message: string, options?: { severity: 'success' | 'error' }) => void;
showMessage?: (message: string, options?: SnackbarOptions) => void;
}
/**
@@ -19,6 +29,8 @@ interface CopyButtonProps {
* @param tooltip 提示信息
* @param size 按钮大小
* @param color 按钮颜色
* @param style 自定义样式
* @param showMessage 消息提示函数,用于显示复制成功或失败的消息
* @returns 复制按钮组件
*/
export const CopyButton: React.FC<CopyButtonProps> = ({
@@ -56,8 +68,7 @@ export const CopyButton: React.FC<CopyButtonProps> = ({
'&:hover': {
bgcolor: copied
? 'success.main'
: typeof color === 'string' &&
!['primary', 'secondary', 'success', 'error', 'info', 'warning'].includes(color)
: !['primary', 'secondary', 'success', 'error', 'info', 'warning'].includes(color)
? color
: `${color}.main`,
color: '#fff',
-31
View File
@@ -1,31 +0,0 @@
import React from 'react';
import { Box, Typography, Paper } from '@mui/material';
const FeatureDescription: React.FC = () => {
return (
<Paper elevation={0} sx={{ borderRadius: 4, overflow: 'hidden' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider', px: 2, py: 1.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
</Typography>
</Box>
<Box sx={{ p: 2 }}>
<Typography variant="body2" sx={{ mb: 1 }}>
<strong></strong>
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
<strong></strong>
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
<strong></strong>
</Typography>
<Typography variant="body2">
<strong></strong>
</Typography>
</Box>
</Paper>
);
};
export default FeatureDescription;
+135 -58
View File
@@ -5,14 +5,19 @@ import {
Paper,
List,
ListItem,
ListItemText,
ListItemIcon,
Collapse,
Chip,
FormControl,
InputLabel,
Select,
MenuItem,
SelectChangeEvent,
Checkbox,
Button,
} from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import InputIcon from '@mui/icons-material/Input';
import { FieldType } from '@/utils/dummyDataGenerator';
// 字段数据接口
interface FieldData {
@@ -24,51 +29,57 @@ interface FieldData {
value: string;
isSelected: boolean;
generatedValue: string;
useInvalidData?: boolean;
}
// 字段类型显示名称映射
const FIELD_TYPE_NAMES: Record<string, string> = {
text: '文本',
email: '邮箱',
phone: '手机号',
number: '数字',
date: '日期',
textarea: '文本域',
radio: '单选框',
checkbox: '复选框',
select: '下拉框',
password: '密码',
name: '姓名',
id_card: '身份证号',
unknown: '未知',
};
// 字段类型颜色映射
const FIELD_TYPE_COLORS: Record<
string,
'default' | 'primary' | 'secondary' | 'error' | 'success' | 'warning'
> = {
email: 'primary',
phone: 'success',
number: 'secondary',
date: 'warning',
password: 'error',
name: 'primary',
id_card: 'secondary',
text: 'default',
textarea: 'default',
unknown: 'default',
[FieldType.TEXT]: '文本',
[FieldType.EMAIL]: '邮箱',
[FieldType.PHONE]: '手机号',
[FieldType.NUMBER]: '数字',
[FieldType.DATE]: '日期',
[FieldType.TEXTarea]: '文本域',
[FieldType.RADIO]: '单选框',
[FieldType.CHECKBOX]: '复选框',
[FieldType.SELECT]: '下拉框',
[FieldType.PASSWORD]: '密码',
[FieldType.NAME]: '姓名',
[FieldType.ID_CARD]: '身份证号',
[FieldType.UNKNOWN]: '未知',
};
interface FieldListProps {
fields: FieldData[];
showFields: boolean;
onToggleShowFields: () => void;
onFieldTypeChange: (fieldId: string, newType: string) => void;
onLocateField: (fieldId: string) => void;
onHoverField: (fieldId: string | null) => void;
onToggleFieldSelection: (fieldId: string) => void;
onToggleAllFields: () => void;
hoveredFieldId: string | null;
}
const FieldList: React.FC<FieldListProps> = ({ fields, showFields, onToggleShowFields }) => {
const FieldList: React.FC<FieldListProps> = ({
fields,
showFields,
onToggleShowFields,
onFieldTypeChange,
onHoverField,
onToggleFieldSelection,
onToggleAllFields,
hoveredFieldId,
}) => {
if (fields.length === 0) return null;
const handleTypeChange = (fieldId: string, event: SelectChangeEvent<string>) => {
onFieldTypeChange(fieldId, event.target.value);
};
const allSelected = fields.every((f) => f.isSelected);
const selectedCount = fields.filter((f) => f.isSelected).length;
return (
<Paper elevation={0} sx={{ borderRadius: 4, overflow: 'hidden', mb: 2 }}>
<Box
@@ -84,34 +95,100 @@ const FieldList: React.FC<FieldListProps> = ({ fields, showFields, onToggleShowF
}}
onClick={onToggleShowFields}
>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
({fields.length})
</Typography>
{showFields ? <ExpandLessIcon /> : <ExpandMoreIcon />}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
({fields.length})
</Typography>
<Typography
variant="caption"
sx={{
bgcolor: selectedCount > 0 ? 'primary.main' : 'grey.300',
color: selectedCount > 0 ? 'white' : 'text.secondary',
px: 1,
py: 0.25,
borderRadius: 1,
}}
>
{selectedCount}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Button
size="small"
onClick={(e) => {
e.stopPropagation();
onToggleAllFields();
}}
>
{allSelected ? '取消全选' : '全选'}
</Button>
{showFields ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</Box>
</Box>
<Collapse in={showFields}>
<List dense sx={{ maxHeight: 300, overflow: 'auto' }}>
<List dense sx={{ maxHeight: 400, overflow: 'auto' }}>
{fields.map((field, index) => (
<ListItem key={field.id} sx={{ py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 36 }}>
<InputIcon fontSize="small" color="action" />
<ListItem
key={field.id}
sx={{
py: 1,
px: 2,
bgcolor: hoveredFieldId === field.id ? '#e3f2fd' : 'transparent',
transition: 'background-color 0.2s ease',
}}
onMouseEnter={() => onHoverField(field.id)}
onMouseLeave={() => onHoverField(null)}
>
<ListItemIcon sx={{ minWidth: 40 }}>
<Checkbox
size="small"
checked={field.isSelected}
onChange={(e) => {
e.stopPropagation();
onToggleFieldSelection(field.id);
}}
/>
</ListItemIcon>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{field.label || field.name || field.placeholder || `字段 ${index + 1}`}
</Typography>
<Chip
label={FIELD_TYPE_NAMES[field.fieldType] || '未知'}
size="small"
color={FIELD_TYPE_COLORS[field.fieldType] || 'default'}
variant="outlined"
/>
</Box>
}
secondary={field.placeholder || field.name}
/>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<Typography
variant="body2"
sx={{
fontWeight: 600,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flex: 1,
opacity: field.isSelected ? 1 : 0.5,
}}
>
{field.label || field.name || field.placeholder || `字段 ${index + 1}`}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<FormControl size="small" sx={{ flex: 1, minWidth: 120 }}>
<InputLabel></InputLabel>
<Select
value={field.fieldType}
label="类型"
onChange={(e) => handleTypeChange(field.id, e)}
>
{Object.values(FieldType).map((type) => (
<MenuItem key={type} value={type}>
{FIELD_TYPE_NAMES[type] || type}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
{field.placeholder && (
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5 }}>
: {field.placeholder}
</Typography>
)}
</Box>
</ListItem>
))}
</List>
-79
View File
@@ -1,79 +0,0 @@
import React from 'react';
import { Button, Stack, CircularProgress } from '@mui/material';
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
import ClearAllIcon from '@mui/icons-material/ClearAll';
import { formRecognizerPageStyles } from '@/config/pageTheme';
interface MainActionsProps {
loading: boolean;
onFillValidData: () => void;
onFillInvalidData: () => void;
onClearAllFields: () => void;
}
const MainActions: React.FC<MainActionsProps> = ({
loading,
onFillValidData,
onFillInvalidData,
onClearAllFields,
}) => {
return (
<Stack spacing={2} sx={{ mb: 4 }}>
<Button
variant="contained"
startIcon={loading ? <CircularProgress size={16} color="inherit" /> : <AutoAwesomeIcon />}
onClick={onFillValidData}
disabled={loading}
fullWidth
sx={{
...formRecognizerPageStyles.buttonStyle,
bgcolor: formRecognizerPageStyles.validColor,
'&:hover': {
bgcolor: formRecognizerPageStyles.validDark,
},
}}
>
{loading ? '填充中...' : '一键填充(有效数据)'}
</Button>
<Button
variant="contained"
startIcon={loading ? <CircularProgress size={16} color="inherit" /> : <ErrorOutlineIcon />}
onClick={onFillInvalidData}
disabled={loading}
fullWidth
sx={{
...formRecognizerPageStyles.buttonStyle,
bgcolor: formRecognizerPageStyles.invalidColor,
'&:hover': {
bgcolor: formRecognizerPageStyles.invalidDark,
},
}}
>
{loading ? '填充中...' : '一键填充(异常数据)'}
</Button>
<Button
variant="outlined"
startIcon={loading ? <CircularProgress size={16} color="inherit" /> : <ClearAllIcon />}
onClick={onClearAllFields}
disabled={loading}
fullWidth
sx={{
...formRecognizerPageStyles.buttonStyle,
borderColor: formRecognizerPageStyles.clearColor,
color: formRecognizerPageStyles.clearColor,
'&:hover': {
borderColor: formRecognizerPageStyles.clearDark,
bgcolor: formRecognizerPageStyles.clearBg,
},
}}
>
{loading ? '清空中...' : '一键清空所有表单'}
</Button>
</Stack>
);
};
export default MainActions;
-80
View File
@@ -1,80 +0,0 @@
import React from 'react';
import {
Box,
Typography,
Paper,
List,
ListItem,
ListItemText,
Collapse,
Chip,
Divider,
} from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
interface OperationHistoryItem {
time: string;
type: string;
content: string;
result: string;
}
interface OperationHistoryProps {
history: OperationHistoryItem[];
showHistory: boolean;
onToggleShowHistory: () => void;
}
const OperationHistory: React.FC<OperationHistoryProps> = ({
history,
showHistory,
onToggleShowHistory,
}) => {
if (history.length === 0) return null;
return (
<Paper elevation={0} sx={{ borderRadius: 4, overflow: 'hidden', mb: 4 }}>
<Box
sx={{
borderBottom: 1,
borderColor: 'divider',
px: 2,
py: 1.5,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
cursor: 'pointer',
}}
onClick={onToggleShowHistory}
>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
({history.length})
</Typography>
{showHistory ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</Box>
<Collapse in={showHistory}>
<List dense sx={{ maxHeight: 300, overflow: 'auto' }}>
{history.map((item, index) => (
<Box key={index}>
{index > 0 && <Divider />}
<ListItem>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Chip label={item.type} size="small" color="primary" variant="outlined" />
<Typography variant="body2">{item.content}</Typography>
</Box>
}
secondary={`${item.time} · ${item.result}`}
/>
</ListItem>
</Box>
))}
</List>
</Collapse>
</Paper>
);
};
export default OperationHistory;
-34
View File
@@ -1,34 +0,0 @@
import React from 'react';
import { Box, Typography, Paper, FormControlLabel, Switch } from '@mui/material';
interface OptionsPanelProps {
includeHidden: boolean;
onIncludeHiddenChange: (checked: boolean) => void;
}
const OptionsPanel: React.FC<OptionsPanelProps> = ({ includeHidden, onIncludeHiddenChange }) => {
return (
<Paper elevation={0} sx={{ borderRadius: 4, overflow: 'hidden', mb: 4 }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider', px: 2, py: 1.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
</Typography>
</Box>
<Box sx={{ p: 2 }}>
<FormControlLabel
control={
<Switch
checked={includeHidden}
onChange={(e) => onIncludeHiddenChange(e.target.checked)}
color="primary"
/>
}
label="包含隐藏字段"
sx={{ width: '100%' }}
/>
</Box>
</Paper>
);
};
export default OptionsPanel;
+106
View File
@@ -0,0 +1,106 @@
import { Stack, Typography, Box, alpha, SxProps, Theme } from '@mui/material';
import { ReactNode } from 'react';
/**
* PageHeader 组件属性接口
*/
export interface PageHeaderProps {
/** 要显示的图标组件 */
icon: ReactNode;
/** 图标的颜色,默认为 '#1976d2'(蓝色) */
iconColor?: string;
/** 主标题文本 */
title: string;
/** 副标题文本(可选) */
subtitle?: string;
/** 在标题右侧显示的徽章/标签组件(可选) */
badge?: ReactNode;
/** 图标容器的自定义样式 */
iconSx?: SxProps<Theme>;
/** 标题文本的自定义样式 */
titleSx?: SxProps<Theme>;
/** 副标题文本的自定义样式 */
subtitleSx?: SxProps<Theme>;
/** 整个组件的自定义样式 */
sx?: SxProps<Theme>;
}
/**
* PageHeader - 通用页面标题栏组件
*
* 用于显示带图标的页面标题,支持自定义颜色、副标题、徽章等功能
*
* @example
* ```tsx
* <PageHeader
* icon={<AccessTimeIcon />}
* iconColor="#1976d2"
* title="时间戳转换"
* subtitle="Unix 毫秒数转换与格式化"
* />
* ```
*
* @example
* ```tsx
* <PageHeader
* icon={<StorageIcon />}
* iconColor={storageCleanerPageStyles.warningColor}
* title="存储清理"
* subtitle={domain}
* badge={<Badge>已占用 {size}</Badge>}
* />
* ```
*/
export default function PageHeader({
icon,
iconColor = '#1976d2',
title,
subtitle,
badge,
iconSx,
titleSx,
subtitleSx,
sx,
}: PageHeaderProps) {
return (
<Stack direction="row" spacing={1.5} alignItems="center" sx={{ mb: 2.5, ...sx }}>
{/* 图标容器 */}
<Box
sx={{
p: 1,
borderRadius: 2.5,
bgcolor: alpha(iconColor, 0.1),
color: iconColor,
display: 'flex',
...iconSx,
}}
>
{icon}
</Box>
{/* 标题区域 */}
<Box sx={{ flex: 1 }}>
{/* 标题行(含徽章) */}
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography
variant="subtitle1"
fontWeight={900}
sx={{ letterSpacing: '-0.5px', lineHeight: 1.2, ...titleSx }}
>
{title}
</Typography>
{badge}
</Stack>
{/* 副标题 */}
{subtitle && (
<Typography
variant="caption"
color="text.secondary"
sx={{ fontWeight: 600, ...subtitleSx }}
>
{subtitle}
</Typography>
)}
</Box>
</Stack>
);
}
+5 -5
View File
@@ -1,11 +1,11 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
Box,
Typography,
Paper,
CircularProgress,
Alert,
Box,
CircularProgress,
IconButton,
Paper,
Typography,
useMediaQuery,
useTheme,
} from '@mui/material';
-117
View File
@@ -1,117 +0,0 @@
import React from 'react';
import {
Box,
Typography,
Paper,
List,
ListItem,
ListItemText,
ListItemIcon,
Collapse,
Button,
Stack,
CircularProgress,
} from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import FolderIcon from '@mui/icons-material/Folder';
import DownloadIcon from '@mui/icons-material/Download';
import UploadIcon from '@mui/icons-material/Upload';
import { DataTemplate } from '@/utils/dataTemplate';
interface TemplateManagerProps {
templates: DataTemplate[];
showTemplates: boolean;
templateLoading: boolean;
onToggleShowTemplates: () => void;
onLoadTemplates: () => void;
onExportTemplates: () => void;
onImportTemplates: () => void;
}
const TemplateManager: React.FC<TemplateManagerProps> = ({
templates,
showTemplates,
templateLoading,
onToggleShowTemplates,
onLoadTemplates,
onExportTemplates,
onImportTemplates,
}) => {
const handleToggle = () => {
onToggleShowTemplates();
if (!showTemplates) onLoadTemplates();
};
return (
<Paper elevation={0} sx={{ borderRadius: 4, overflow: 'hidden', mb: 4 }}>
<Box
sx={{
borderBottom: 1,
borderColor: 'divider',
px: 2,
py: 1.5,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
cursor: 'pointer',
}}
onClick={handleToggle}
>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
({templates.length})
</Typography>
{showTemplates ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</Box>
<Collapse in={showTemplates}>
<Box sx={{ p: 2 }}>
<Stack direction="row" spacing={1} sx={{ mb: 2 }}>
<Button
size="small"
startIcon={<DownloadIcon />}
onClick={onExportTemplates}
variant="outlined"
>
</Button>
<Button
size="small"
startIcon={<UploadIcon />}
onClick={onImportTemplates}
variant="outlined"
>
</Button>
</Stack>
{templateLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 2 }}>
<CircularProgress size={20} />
</Box>
) : templates.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', py: 2 }}>
</Typography>
) : (
<List dense sx={{ maxHeight: 200, overflow: 'auto' }}>
{templates.map((template) => (
<ListItem key={template.id} sx={{ py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 36 }}>
<FolderIcon fontSize="small" color="primary" />
</ListItemIcon>
<ListItemText
primary={template.name}
secondary={`${template.fields.length} 个字段 · ${new Date(
template.updatedAt,
).toLocaleDateString('zh-CN')}`}
/>
</ListItem>
))}
</List>
)}
</Box>
</Collapse>
</Paper>
);
};
export default TemplateManager;