Develop (#15)
* 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:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user