3c9c9e740e
docs: 添加组件文档注释和类型导入 refactor: 统一使用 SnackbarOptions 类型 style: 优化导入语句顺序和格式 refactor: 简化假数据生成器中的faker导入和使用 feat(form-recognizer): 增强表单识别功能并优化UI交互 refactor: 移除未使用的组件文件 refactor(页面头部): 提取通用 PageHeader 组件 style(组件): 调整自动刷新开关和存储选项网格的样式 style(ui): 调整时间戳页面和结果视图的样式 ci(workflow): 移除Firefox测试以简化CI流程 refactor(qrcode): 重构二维码解析功能并提取为独立模块 style(页面样式): 统一页面头部图标颜色并优化表单样式 feat(formMapping): 添加表单映射功能 feat(表单映射): 添加配置导出功能及状态提示 feat(表单填充): 新增智能表单填充功能 ci: 精简触发CI的工作流分支 test: 更新路由测试以匹配新增的路由数量 test: 将测试文件中的描述和断言翻译为中文 test(PageHeader): 添加组件测试用例 refactor(theme): 重构主题样式并优化仪表盘卡片组件 refactor(Button): 统一按钮样式并移除重复样式定义 docs: 更新 README 文件中的目录结构说明 refactor: 优化代码类型声明和UI布局 refactor(消息通信): 使用 @webext-core/messaging 重构消息处理逻辑 fix: 修复依赖项缺失导致的潜在问题 refactor: 重构表单识别和消息处理逻辑 feat(侧边栏): 添加侧边栏状态变化通知功能 refactor: 优化高亮组件渲染逻辑并添加防抖处理 refactor: 统一组件导出方式为默认导出 refactor: 重构样式系统并迁移至 MUI 主题 docs: 更新项目文档以反映新增功能和技术栈细节 feat: 添加错误边界组件以捕获子组件错误 refactor(ui): 使用全局 snackbar 替换本地通知组件 refactor(snackbar): 重构全局消息提示为 SnackbarProvider 组件 refactor(Snackbar): 重构消息提示组件并优化样式 refactor: 重构路由和功能配置系统 refactor(GlobalSnackbar): 合并SnackbarProvider到GlobalSnackbar组件 feat(表单映射): 增强高亮器功能并优化元素定位 refactor: 移除表单映射相关功能 feat: 实现页面组件懒加载并优化加载状态显示 refactor: 移除未使用的数据生成和验证工具类 chore: 更新应用图标文件 refactor(qrcode): 替换二维码生成和解析库以减小体积 feat: 添加在标签页打开功能并优化错误处理 feat: 支持独立标签页模式并重构路由配置 feat(ui): 优化响应式布局和样式设计 refactor: 移除URL工具相关代码 refactor: 简化页面标题和存储选项网格的响应式布局 refactor: 优化样式和错误处理 refactor: 重构页面组件结构 style(DashboardPage): 优化仪表盘布局为响应式网格 refactor(ToolCard): 重构工具卡片组件并添加详细注释 feat: 新增文本统计功能 feat(jwt): 添加 JWT 解析工具功能 docs: 更新 README 以更清晰地描述项目功能和结构 docs: 更新AGENTS.md文档以反映项目最新状态 test: 更新特征计数测试以匹配新增工具 style: 统一导入语句顺序 refactor(pages): 重构工具页面组件结构并优化功能实现 feat(顶部栏): 添加搜索功能和历史记录 feat: 添加多语言支持(i18n) feat(顶部栏): 添加语言切换功能并优化国际化支持 feat(i18n): 增加语言归一化和同步功能 fix(剪贴板): 使用国际化文本替换剪贴板消息 test(顶部栏): 更新测试以匹配国际化标题 refactor: 重构组件目录结构 refactor(Dashboard): 优化卡片组件结构并移除无用属性 refactor(timestamp): 合并输入处理函数并优化样式结构 refactor(dashboard): 重构仪表盘卡片组件并优化布局 refactor(storageCleaner): 重构样式代码并优化国际化处理 refactor(StorageCleaner): 优化条件判断和清理未使用的代码 refactor(QrCode): 重构二维码页面样式和组件结构 refactor(ErrorBoundary): 移除冗余状态更新并调整错误背景色 feat(TopBar): 优化顶部导航栏功能并添加样式配置 chore: 在.gitignore中添加docs目录 feat(JsonDiff): 添加差异引擎类型定义 feat: 新增 JSON 差异比较工具功能 feat(主题): 添加暗色模式支持并重构主题系统
474 lines
17 KiB
TypeScript
474 lines
17 KiB
TypeScript
import { SyntheticEvent, useEffect, useMemo, useState } from 'react';
|
|
import {
|
|
alpha,
|
|
Box,
|
|
Button,
|
|
CircularProgress,
|
|
Divider,
|
|
IconButton,
|
|
Paper,
|
|
Stack,
|
|
Switch,
|
|
Tab,
|
|
Tabs,
|
|
Typography,
|
|
} from '@mui/material';
|
|
import SettingsIcon from '@mui/icons-material/Settings';
|
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
|
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
|
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
|
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
|
import type { PageType, StorageSchema } from '@/types/storage';
|
|
import { storageUtil } from '@/utils/chromeStorage';
|
|
import {
|
|
getDefaultPageOrder,
|
|
getDefaultVisibleFeatureKeys,
|
|
getFeatureByKey,
|
|
} from '@/config/features';
|
|
import GlobalSnackbar, { useSnackbarState } from '@/components/GlobalSnackbar';
|
|
import ErrorBoundary from '@/components/ErrorBoundary';
|
|
import PageHeader from '@/components/PageHeader';
|
|
import { useTheme, type PaletteColor, type Theme } from '@mui/material/styles';
|
|
import { useTranslation } from 'react-i18next';
|
|
import type { PaletteColorKey } from '@/config/features';
|
|
|
|
/** 安全地从 theme.palette 中取 PaletteColor */
|
|
const getPaletteColor = (theme: Theme, key: PaletteColorKey): PaletteColor =>
|
|
(theme.palette as unknown as Record<string, PaletteColor>)[key];
|
|
|
|
type WindowType = 'popup' | 'sidepanel' | 'tab';
|
|
|
|
/**
|
|
* Options 设置页面主组件
|
|
* 支持对不同窗口入口的功能显示和排序进行独立配置
|
|
*/
|
|
export default function App() {
|
|
const theme = useTheme();
|
|
const { t } = useTranslation(['features', 'common']);
|
|
// 从 URL 参数中初始化当前的 Tab 类型
|
|
const initialWindowType = useMemo(() => {
|
|
if (typeof window === 'undefined') return 'popup';
|
|
const params = new URLSearchParams(window.location.search);
|
|
const tab = params.get('tab');
|
|
if (tab === 'popup' || tab === 'sidepanel' || tab === 'tab') {
|
|
return tab as WindowType;
|
|
}
|
|
return 'popup';
|
|
}, []);
|
|
|
|
const [windowType, setWindowType] = useState<WindowType>(initialWindowType);
|
|
const [visiblePages, setVisiblePages] = useState<PageType[]>([]);
|
|
const [pageOrder, setPageOrder] = useState<PageType[]>([]);
|
|
const [isLoaded, setIsLoaded] = useState(false);
|
|
const { snackbarProps, showMessage } = useSnackbarState();
|
|
|
|
// 根据当前选择的窗口类型确定对应的 Storage Key
|
|
const configKeys = useMemo(() => {
|
|
switch (windowType) {
|
|
case 'sidepanel':
|
|
return {
|
|
visible: 'app/sidepanelVisiblePages' as keyof StorageSchema,
|
|
order: 'app/sidepanelPageOrder' as keyof StorageSchema,
|
|
};
|
|
case 'tab':
|
|
return {
|
|
visible: 'app/tabVisiblePages' as keyof StorageSchema,
|
|
order: 'app/tabPageOrder' as keyof StorageSchema,
|
|
};
|
|
case 'popup':
|
|
default:
|
|
return {
|
|
visible: 'app/popupVisiblePages' as keyof StorageSchema,
|
|
order: 'app/popupPageOrder' as keyof StorageSchema,
|
|
};
|
|
}
|
|
}, [windowType]);
|
|
|
|
// 当 windowType 改变时,同步更新 URL 参数
|
|
useEffect(() => {
|
|
const url = new URL(window.location.href);
|
|
url.searchParams.set('tab', windowType);
|
|
window.history.replaceState({}, '', url.toString());
|
|
}, [windowType]);
|
|
|
|
// 加载配置数据
|
|
useEffect(() => {
|
|
const loadConfig = async () => {
|
|
setIsLoaded(false);
|
|
try {
|
|
const [savedVisible, savedOrder] = await Promise.all([
|
|
storageUtil.get(configKeys.visible, getDefaultVisibleFeatureKeys()),
|
|
storageUtil.get(configKeys.order, getDefaultPageOrder()),
|
|
]);
|
|
setVisiblePages((savedVisible as PageType[]) ?? getDefaultVisibleFeatureKeys());
|
|
setPageOrder((savedOrder as PageType[]) ?? getDefaultPageOrder());
|
|
} catch (error) {
|
|
console.error('Failed to load config:', error);
|
|
setVisiblePages(getDefaultVisibleFeatureKeys());
|
|
setPageOrder(getDefaultPageOrder());
|
|
} finally {
|
|
setIsLoaded(true);
|
|
}
|
|
};
|
|
|
|
loadConfig().catch(console.error);
|
|
}, [configKeys]);
|
|
|
|
/**
|
|
* 切换页面可见性
|
|
*/
|
|
const handlePageToggle = async (page: PageType) => {
|
|
const isCurrentlyVisible = visiblePages.includes(page);
|
|
let newPages: PageType[];
|
|
|
|
if (isCurrentlyVisible) {
|
|
if (visiblePages.length <= 1) {
|
|
showToast('至少需要保留一个可见页面', 'warning');
|
|
return;
|
|
}
|
|
newPages = visiblePages.filter((p) => p !== page);
|
|
} else {
|
|
newPages = [...visiblePages, page];
|
|
}
|
|
|
|
try {
|
|
await storageUtil.set(configKeys.visible, newPages);
|
|
setVisiblePages(newPages);
|
|
const feature = getFeatureByKey(page);
|
|
const label = feature ? t(feature.labelKey) : page;
|
|
showToast(`已${isCurrentlyVisible ? '隐藏' : '显示'} ${label}`, 'success');
|
|
} catch (error) {
|
|
console.error('Failed to save config:', error);
|
|
showToast('保存失败', 'warning');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 调整页面显示顺序
|
|
*/
|
|
const handleMove = async (index: number, direction: 'up' | 'down') => {
|
|
if (direction === 'up' && index === 0) return;
|
|
if (direction === 'down' && index === pageOrder.length - 1) return;
|
|
|
|
const newOrder = [...pageOrder];
|
|
const swapIndex = direction === 'up' ? index - 1 : index + 1;
|
|
[newOrder[index], newOrder[swapIndex]] = [newOrder[swapIndex], newOrder[index]];
|
|
|
|
try {
|
|
await storageUtil.set(configKeys.order, newOrder);
|
|
setPageOrder(newOrder);
|
|
} catch (error) {
|
|
console.error('Failed to save order:', error);
|
|
showToast('排序保存失败', 'warning');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 恢复默认设置
|
|
*/
|
|
const handleRestoreDefaults = async () => {
|
|
try {
|
|
const defaults = getDefaultVisibleFeatureKeys();
|
|
const defaultOrder = getDefaultPageOrder();
|
|
|
|
await Promise.all([
|
|
storageUtil.set(configKeys.visible, defaults),
|
|
storageUtil.set(configKeys.order, defaultOrder),
|
|
]);
|
|
|
|
setVisiblePages(defaults);
|
|
setPageOrder(defaultOrder);
|
|
showToast('已恢复当前模式默认设置', 'success');
|
|
} catch (error) {
|
|
console.error('Failed to restore defaults:', error);
|
|
showToast('恢复失败', 'warning');
|
|
}
|
|
};
|
|
|
|
const handleWindowTypeChange = (_event: SyntheticEvent, newType: WindowType) => {
|
|
if (newType !== null) {
|
|
setWindowType(newType);
|
|
}
|
|
};
|
|
|
|
const showToast = (message: string, severity: 'success' | 'info' | 'warning') => {
|
|
showMessage(message, { severity });
|
|
};
|
|
|
|
if (!isLoaded) {
|
|
return (
|
|
<Box
|
|
sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}
|
|
>
|
|
<CircularProgress size={24} />
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box
|
|
className="app"
|
|
sx={{
|
|
minHeight: '100vh',
|
|
bgcolor: 'background.default',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
}}
|
|
>
|
|
<ErrorBoundary>
|
|
{/* 顶部标题与导航栏 */}
|
|
<Box
|
|
sx={{
|
|
width: '100%',
|
|
bgcolor: 'background.paper',
|
|
borderBottom: '1px solid',
|
|
borderColor: 'divider',
|
|
pt: { xs: 3, sm: 5 },
|
|
pb: 0,
|
|
px: { xs: 2, sm: 4 },
|
|
}}
|
|
>
|
|
<Box sx={{ maxWidth: 800, mx: 'auto' }}>
|
|
<PageHeader
|
|
icon={<SettingsIcon />}
|
|
iconColor={theme.palette.primary.main}
|
|
title="应用设置"
|
|
subtitle="针对不同窗口类型独立配置 Dashboard 中显示的功能及其排序"
|
|
sx={{ mb: 4 }}
|
|
/>
|
|
<Tabs
|
|
value={windowType}
|
|
onChange={handleWindowTypeChange}
|
|
indicatorColor="primary"
|
|
textColor="primary"
|
|
sx={{
|
|
'& .MuiTab-root': {
|
|
fontWeight: 700,
|
|
fontSize: '0.9rem',
|
|
textTransform: 'none',
|
|
minWidth: { xs: 100, sm: 140 },
|
|
letterSpacing: '0.3px',
|
|
},
|
|
'& .MuiTabs-indicator': {
|
|
height: 3,
|
|
borderRadius: '3px 3px 0 0',
|
|
},
|
|
}}
|
|
>
|
|
<Tab value="popup" label="Popup 窗口" />
|
|
<Tab value="sidepanel" label="侧边栏" />
|
|
<Tab value="tab" label="标签页" />
|
|
</Tabs>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* 主内容区域 */}
|
|
<Box sx={{ flex: 1, p: { xs: 2, sm: 4 } }}>
|
|
<Box sx={{ maxWidth: 800, mx: 'auto' }}>
|
|
<Stack direction="row" justifyContent="flex-end" sx={{ mb: 2.5 }}>
|
|
<Button
|
|
variant="outlined"
|
|
color="primary"
|
|
size="small"
|
|
onClick={handleRestoreDefaults}
|
|
startIcon={<RefreshIcon sx={{ fontSize: 18 }} />}
|
|
sx={{
|
|
borderRadius: 2.5,
|
|
fontWeight: 700,
|
|
textTransform: 'none',
|
|
borderColor: 'divider',
|
|
color: 'text.secondary',
|
|
'&:hover': {
|
|
borderColor: 'primary.main',
|
|
color: 'primary.main',
|
|
bgcolor: alpha(theme.palette.primary.main, 0.04),
|
|
},
|
|
}}
|
|
>
|
|
恢复当前模式默认
|
|
</Button>
|
|
</Stack>
|
|
|
|
{!isLoaded ? (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 12 }}>
|
|
<CircularProgress size={32} thickness={5} />
|
|
</Box>
|
|
) : (
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
borderRadius: 4,
|
|
border: '1px solid',
|
|
borderColor: 'divider',
|
|
overflow: 'hidden',
|
|
bgcolor: 'background.paper',
|
|
boxShadow: '0 4px 24px rgba(0,0,0,0.03)',
|
|
}}
|
|
>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
|
{pageOrder.map((key, index, array) => {
|
|
const feature = getFeatureByKey(key);
|
|
if (!feature) return null;
|
|
|
|
const isChecked = visiblePages.includes(key);
|
|
const isDisabled = isChecked && visiblePages.length === 1;
|
|
|
|
return (
|
|
<Box
|
|
key={key}
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
p: { xs: 2, sm: 2.5 },
|
|
borderBottom: index === array.length - 1 ? 'none' : '1px solid',
|
|
borderColor: 'divider',
|
|
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
'&:hover': {
|
|
bgcolor: alpha(
|
|
getPaletteColor(theme, feature.themeColorKey ?? 'primary').main,
|
|
0.02,
|
|
),
|
|
},
|
|
}}
|
|
>
|
|
<Stack
|
|
direction="row"
|
|
spacing={{ xs: 1.5, sm: 2.5 }}
|
|
alignItems="center"
|
|
sx={{ flex: 1, minWidth: 0 }}
|
|
>
|
|
{/* 拖拽/排序暗示图标 */}
|
|
<Box sx={{ color: 'text.disabled', display: 'flex' }}>
|
|
<DragIndicatorIcon fontSize="small" />
|
|
</Box>
|
|
|
|
{/* 功能图标容器 */}
|
|
<Box
|
|
sx={{
|
|
p: 1.2,
|
|
borderRadius: 2.5,
|
|
bgcolor: alpha(
|
|
getPaletteColor(theme, feature.themeColorKey ?? 'primary').main,
|
|
0.1,
|
|
),
|
|
color: getPaletteColor(theme, feature.themeColorKey ?? 'primary')
|
|
.main,
|
|
display: 'flex',
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
{feature.icon}
|
|
</Box>
|
|
|
|
{/* 文本信息 */}
|
|
<Box sx={{ minWidth: 0 }}>
|
|
<Typography
|
|
variant="subtitle2"
|
|
sx={{
|
|
fontWeight: 800,
|
|
color: 'text.primary',
|
|
fontSize: '0.95rem',
|
|
lineHeight: 1.2,
|
|
mb: 0.5,
|
|
}}
|
|
>
|
|
{t(feature.labelKey)}
|
|
</Typography>
|
|
<Typography
|
|
variant="caption"
|
|
color="text.secondary"
|
|
sx={{
|
|
fontWeight: 600,
|
|
display: 'block',
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap',
|
|
}}
|
|
>
|
|
{feature.descriptionKey ? t(feature.descriptionKey) : '暂无描述'}
|
|
</Typography>
|
|
</Box>
|
|
</Stack>
|
|
|
|
<Box sx={{ display: 'flex', alignItems: 'center', ml: { xs: 1, sm: 2 } }}>
|
|
{/* 移动操作按钮 */}
|
|
<Stack direction="row" sx={{ display: 'flex', mr: { xs: 0, sm: 1 } }}>
|
|
<IconButton
|
|
size="small"
|
|
onClick={() => handleMove(index, 'up')}
|
|
disabled={index === 0}
|
|
sx={{
|
|
color: 'text.disabled',
|
|
p: { xs: 0.5, sm: 1 },
|
|
'&:hover': {
|
|
color: 'primary.main',
|
|
bgcolor: alpha(theme.palette.primary.main, 0.08),
|
|
},
|
|
}}
|
|
>
|
|
<KeyboardArrowUpIcon fontSize="small" />
|
|
</IconButton>
|
|
<IconButton
|
|
size="small"
|
|
onClick={() => handleMove(index, 'down')}
|
|
disabled={index === array.length - 1}
|
|
sx={{
|
|
color: 'text.disabled',
|
|
p: { xs: 0.5, sm: 1 },
|
|
'&:hover': {
|
|
color: 'primary.main',
|
|
bgcolor: alpha(theme.palette.primary.main, 0.08),
|
|
},
|
|
}}
|
|
>
|
|
<KeyboardArrowDownIcon fontSize="small" />
|
|
</IconButton>
|
|
</Stack>
|
|
|
|
<Divider
|
|
orientation="vertical"
|
|
flexItem
|
|
sx={{
|
|
mx: { xs: 0.5, sm: 1 },
|
|
display: 'block',
|
|
height: 24,
|
|
alignSelf: 'center',
|
|
}}
|
|
/>
|
|
|
|
{/* 显示切换开关 */}
|
|
<Switch
|
|
color="primary"
|
|
checked={isChecked}
|
|
onChange={() => handlePageToggle(key)}
|
|
disabled={isDisabled}
|
|
sx={{
|
|
'& .MuiSwitch-switchBase.Mui-checked': {
|
|
color: getPaletteColor(theme, feature.themeColorKey ?? 'primary')
|
|
.main,
|
|
},
|
|
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': {
|
|
backgroundColor: getPaletteColor(
|
|
theme,
|
|
feature.themeColorKey ?? 'primary',
|
|
).main,
|
|
},
|
|
}}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
})}
|
|
</Box>
|
|
</Paper>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
</ErrorBoundary>
|
|
|
|
<GlobalSnackbar {...snackbarProps} />
|
|
</Box>
|
|
);
|
|
}
|