* 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运行时间和资源消耗

* refactor(qrcode): 重构二维码解析功能并提取为独立模块

* style(页面样式): 统一页面头部图标颜色并优化表单样式

更新各页面头部组件的图标颜色配置,移除冗余的表单标签属性,优化输入框样式和按钮悬停效果

* feat(formMapping): 添加表单映射功能

- 创建 FormMappingPage 页面组件
- 实现表单扫描器 scanner.ts
- 实现高亮器 highlighter.ts
- 更新路由配置 routes.ts
- 更新内容脚本 content.ts
- 更新类型定义 storage.d.ts
- 添加 formMappingPageStyles 样式配置

* feat(表单映射): 添加配置导出功能及状态提示

添加配置导出为JSON文件的功能,包含导出按钮和错误处理。新增Snackbar组件用于显示导出成功和错误状态。优化页面布局结构,将状态提示移至全局容器外。

* feat(表单填充): 新增智能表单填充功能

添加智能表单填充功能,包括:
1. 新增表单填充页面和路由配置
2. 实现模糊匹配引擎和智能注入引擎
3. 添加Mock数据生成器和视觉反馈渲染器
4. 扩展消息接口支持填充操作

* ci: 精简触发CI的工作流分支

移除对develop分支及其变体的触发,仅保留main分支的触发

* test: 更新路由测试以匹配新增的路由数量

* test: 将测试文件中的描述和断言翻译为中文

* test(PageHeader): 添加组件测试用例验证渲染逻辑和样式

* refactor(theme): 重构主题样式并优化仪表盘卡片组件

将页面样式配置集中管理,移除各页面中硬编码的背景色
新增 DashboardCard 组件封装通用卡片逻辑
添加 dashboardCards 配置文件统一管理卡片数据

* refactor(Button): 统一按钮样式并移除重复样式定义

将按钮样式统一封装到 Button 组件中,移除各页面重复的样式定义
更新组件文档说明,提供更清晰的用法示例

* docs: 更新 README 文件中的目录结构说明

添加新组件和配置文件的说明,保持文档与代码同步

* refactor: 优化代码类型声明和UI布局

修复类型声明从any改为never以提高类型安全性
调整TimestampPage页面布局间距
移除DashboardPage中不必要的实时时钟状态
更新ESLint配置以使用推荐配置
添加选项页面打开的错误处理

* refactor(消息通信): 使用 @webext-core/messaging 重构消息处理逻辑

将原有的 chrome.runtime.onMessage 和 chrome.tabs.sendMessage 替换为类型安全的 @webext-core/messaging 实现
添加 ProtocolMap 类型定义确保消息类型安全
更新相关页面和后台脚本使用新的消息通信方式

* fix: 修复依赖项缺失导致的潜在问题

修复 useStorageState 中缺少 defaultValue 依赖的问题
重构 RouterProvider 的初始化逻辑,使用 useCallback 优化性能

* refactor: 重构表单识别和消息处理逻辑

将表单映射UI逻辑提取到独立文件
将消息处理器提取到独立文件
将表单识别页面逻辑提取到自定义hook
优化代码结构和可维护性

* feat(侧边栏): 添加侧边栏状态变化通知功能

- 在消息类型中新增侧边栏状态变化枚举和字段
- 侧边栏打开和关闭时发送状态通知
- 替换轮询检查方式为消息监听机制

* refactor: 优化高亮组件渲染逻辑并添加防抖处理

重构高亮组件渲染逻辑,使用 requestAnimationFrame 进行节流优化
在 useStorageCleaner 中添加防抖处理以避免频繁加载

* refactor: 统一组件导出方式为默认导出

* refactor: 重构样式系统并迁移至 MUI 主题

删除冗余的 CSS 文件,统一使用 MUI 主题管理样式
新增 useActiveTabDomain 和 useSidePanelState 自定义 Hook
优化各入口点的主题集成和布局处理

* docs: 更新项目文档以反映新增功能和技术栈细节

更新 AGENTS.md 文档,详细描述新增的功能模块(存储管理、URL 管理、二维码生成、表单工具套件等)和更新的技术栈信息

* feat: 添加错误边界组件以捕获子组件错误

在 popup、sidepanel 和 options 入口点添加 ErrorBoundary 组件,用于捕获并处理子组件中的 JavaScript 错误。当错误发生时,显示友好的错误界面并提供刷新功能。

* refactor(ui): 使用全局 snackbar 替换本地通知组件

重构表单页面中的通知系统,移除本地 Snackbar 和 Alert 组件,改用全局 GlobalSnackbar 组件统一管理通知
标准化主题配置中的 CSS 属性命名,移除带前缀的样式属性

* refactor(snackbar): 重构全局消息提示为 SnackbarProvider 组件

将原本分散在各页面的 GlobalSnackbar 组件重构为集中管理的 SnackbarProvider,通过 Context 提供统一的消息提示功能。主要变更包括:

1. 创建新的 SnackbarProvider 组件作为全局消息提示容器
2. 提供 useSnackbar hook 供子组件调用
3. 移除各页面中独立的 GlobalSnackbar 实例
4. 在 App 根组件中统一集成 SnackbarProvider
5. 优化消息提示样式和交互行为

* refactor(Snackbar): 重构消息提示组件并优化样式

重构 Snackbar 组件,将状态管理逻辑提取到 GlobalSnackbar 中复用
优化消息提示样式,调整阴影效果和最小尺寸
统一各页面使用 Snackbar 的方式,移除冗余配置

* refactor: 重构路由和功能配置系统

- 将路由配置和仪表盘卡片配置合并为统一的 features 配置
- 重构 RouterProvider 增加数据校验和本地快照功能
- 优化表单字段识别和填充逻辑,提取公共方法
- 更新相关组件和文档以适配新的配置系统
- 删除废弃的 dashboardCards 和 routes 配置文件

* refactor(GlobalSnackbar): 合并SnackbarProvider到GlobalSnackbar组件

重构Snackbar相关代码,将SnackbarProvider合并到GlobalSnackbar组件中,简化代码结构
更新相关组件引用路径,从SnackbarProvider改为GlobalSnackbar
修复StorageCleanerPage禁用状态逻辑问题

* feat(表单映射): 增强高亮器功能并优化元素定位

- 新增高亮器闪烁功能,支持自定义持续时间
- 改进元素定位算法,支持 Shadow DOM 穿透
- 优化拾取模式,支持连续拾取和悬停预览
- 增强错误处理和信息反馈
- 添加测试用例确保核心功能稳定性

* refactor: 移除表单映射相关功能

删除表单映射相关的UI、逻辑和测试代码,包括表单识别、智能填充和高亮功能
简化消息处理逻辑和配置文件,保留核心功能

* feat: 实现页面组件懒加载并优化加载状态显示

refactor: 移除未使用的数据生成和验证工具类

chore: 更新应用图标文件

* refactor(qrcode): 替换二维码生成和解析库以减小体积

使用 qr-scanner 替代 jsqr 进行二维码解析,使用 qrious 替代 qrcode 进行二维码生成
新的库体积更小且性能更好,同时移除了不再使用的 @faker-js/faker 依赖

* feat: 添加在标签页打开功能并优化错误处理

修复测试中的加载状态检查
将控制台日志改为错误捕获
添加全局消息提示组件

* feat: 支持独立标签页模式并重构路由配置

重构路由系统以支持独立标签页模式,替换原有的独立窗口模式
- 新增标签页路由、可见页面列表和页面顺序的存储键
- 修改 RouterProvider 以支持不同窗口类型的独立配置
- 更新选项页面以支持为不同窗口类型单独配置
- 添加相关单元测试

* feat(ui): 优化响应式布局和样式设计

重构选项页面布局,增强移动端适配性
更新主题配置和组件样式,提升用户体验

* refactor: 移除URL工具相关代码

移除不再需要的URL工具功能,包括组件、页面、样式和类型定义

* refactor: 简化页面标题和存储选项网格的响应式布局

* refactor: 优化样式和错误处理

- 简化 chrome.runtime.openOptionsPage 的错误处理
- 拆分 html, body, #root 的样式定义,使用 minHeight 替代 height

* refactor: 重构页面组件结构
- 将页面组件从 entrypoints/popup/pages 移动到 pages 目录
- 更新文档中的路径引用

* style(DashboardPage): 优化仪表盘布局为响应式网格

refactor(ToolCard): 重构工具卡片组件并添加详细注释

* feat: 新增文本统计功能

添加文本统计工具页面及相关功能,包括:
- 文本统计工具页面组件
- 文本统计计算工具函数
- 相关类型定义和样式配置
- 单元测试覆盖核心功能

* feat(jwt): 添加 JWT 解析工具功能

新增 JWT 解析工具页面,包含以下内容:
- 添加 JWT 类型定义和页面样式配置
- 实现 JWT 解析核心功能及测试用例
- 创建 JWT 解析页面组件
- 在功能配置中注册 JWT 工具

* docs: 更新 README 以更清晰地描述项目功能和结构

- 重构功能描述部分,使用更简洁的列表形式展示
- 更新技术栈和项目结构说明
- 优化开发流程部分的描述
- 移除冗余的权限和依赖说明

* docs: 更新AGENTS.md文档以反映项目最新状态

- 重写项目概述和核心命令部分,使其更简洁准确
- 重构目录结构描述,采用更清晰的树状格式
- 移除过时的功能模块描述,聚焦当前核心功能
- 更新AI代理开发准则和权限管理说明
- 优化文档整体结构和可读性

* test: 更新特征计数测试以匹配新增 textStatistics 和 jwt 工具

* style: 统一导入语句顺序

* refactor(pages): 重构工具页面组件结构并优化功能实现

- 将各工具页面组件迁移至独立目录并重构为模块化结构
- 新增存储清理工具相关组件及状态管理逻辑
- 实现时间戳转换工具的自定义hook和UI组件
- 优化JWT解析工具的代码结构和样式
- 更新文本统计工具的性能计算逻辑
- 调整配置文件中的页面引用路径
- 删除无用文件和过时文档

* feat(顶部栏): 添加搜索功能和历史记录

实现顶部栏搜索框功能,支持模糊搜索工具并显示搜索结果列表
添加搜索历史记录功能,自动保存最近搜索并显示
支持键盘导航和选择搜索结果

* feat: 添加多语言支持(i18n)

实现应用的多语言功能,支持中英文切换
添加i18next和相关依赖
创建语言资源文件
更新组件以使用翻译功能
重构文本统计工具页面
完善测试和构建配置

* feat(顶部栏): 添加语言切换功能并优化国际化支持
feat(i18n): 增加语言归一化和同步功能
fix(剪贴板): 使用国际化文本替换剪贴板消息
test(顶部栏): 更新测试以匹配国际化标题

* refactor: 重构组件目录结构,将组件移动到对应功能页面目录

将多个组件从通用组件目录移动到各自功能页面目录下,包括 ToolCard、StorageCleanerConfirm、UrlToQrCodeSection 和 QrCodeToUrlSection。同时新增 Dashboard 页面相关组件 DashboardCard 和 ToolCard,并优化了组件导入路径。

重构旨在提高代码组织性,使组件与其所属功能页面更紧密关联,便于维护和理解代码结构。所有组件功能保持不变,仅调整文件位置和导入路径。

* refactor(Dashboard): 优化卡片组件结构并移除无用属性

重构 DashboardCard 组件接口,直接传递属性而非配置对象
移除 ToolCard 中无用的 hasAI 属性
使用 useMemo 优化可见性判断性能
统一使用 alpha 函数处理颜色透明度

* refactor(timestamp): 合并输入处理函数并优化样式结构

重构时间戳转换器的输入处理逻辑,将 setTsInput 和 setDtInput 合并为统一的 setInput 函数
将组件样式提取到 pageTheme 配置中统一管理
使用 ToggleButtonGroup 替换自定义模式切换器实现
移除不必要的 showMessage 属性传递,改用 useSnackbar 钩子

* refactor(dashboard): 重构仪表盘卡片组件并优化布局

- 删除冗余的 DashboardCard 组件,直接使用 ToolCard
- 将网格布局样式移至主题配置
- 使用 CardActionArea 增强可访问性
- 简化点击事件处理逻辑
- 更新相关测试用例

* refactor(storageCleaner): 重构样式代码并优化国际化处理

将分散的样式代码集中到pageTheme.ts中统一管理
优化组件中的国际化文本引用
修复useStorageCleaner中的状态同步问题
更新测试用例以匹配国际化文本

* refactor(StorageCleaner): 优化条件判断和清理未使用的代码

简化禁用按钮的条件判断逻辑
移除未使用的 resultTimeoutRef 及相关清理代码

* refactor(QrCode): 重构二维码页面样式和组件结构

将内联样式提取到主题配置文件中
移除未使用的全局消息提示依赖
优化组件结构和代码复用
删除未使用的QrCodeUploader组件

* refactor(ErrorBoundary): 移除冗余状态更新并调整错误背景色

移除 componentDidCatch 中的冗余 setState 调用
将错误背景色从 'error.shortest' 改为 rgba 格式

* feat(TopBar): 优化顶部导航栏功能并添加样式配置

- 新增 openExtensionPage 工具函数用于打开扩展页面
- 将顶部导航栏样式配置提取到 pageTheme 中统一管理
- 改进搜索历史功能,增加错误处理和配置化限制
- 优化键盘导航和搜索结果的交互逻辑

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: yll <mtmtchb@outlook.com>
This commit is contained in:
LingandRX
2026-05-06 23:11:27 +08:00
committed by GitHub
parent 9d1d8a362a
commit ff964e3608
88 changed files with 2952 additions and 2572 deletions
@@ -0,0 +1,26 @@
import { Box, Switch, Typography } from '@mui/material';
import { storageCleanerPageStyles } from '@/config/pageTheme';
import { useTranslation } from 'react-i18next';
interface AutoRefreshToggleProps {
autoRefresh: boolean;
onChange: (checked: boolean) => void;
}
export default function AutoRefreshToggle({ autoRefresh, onChange }: AutoRefreshToggleProps) {
const { t } = useTranslation(['storageCleaner']);
return (
<Box sx={storageCleanerPageStyles.AUTO_REFRESH_CONTAINER}>
<Typography variant="body2" fontWeight={700} sx={{ fontSize: '0.8rem', px: 1.2 }}>
{t('storageCleaner:autoRefresh')}
</Typography>
<Switch
size="small"
checked={autoRefresh}
onChange={(e) => onChange(e.target.checked)}
color="warning"
sx={storageCleanerPageStyles.AUTO_REFRESH_SWITCH}
/>
</Box>
);
}
+27
View File
@@ -0,0 +1,27 @@
import { Alert, Box } from '@mui/material';
import type { CleaningResult } from '@/types/storage';
import { formatCleaningResult } from '@/utils/storageCleaner';
import { storageCleanerPageStyles } from '@/config/pageTheme';
import { useTranslation } from 'react-i18next';
interface CleaningResultProps {
result: CleaningResult | null;
}
export default function CleaningResult({ result }: CleaningResultProps) {
const { t } = useTranslation(['storageCleaner']);
if (!result) return null;
return (
<Box sx={{ mt: 3, animation: 'fadeIn 0.3s ease-in-out' }}>
<Alert
severity={result.success ? 'success' : 'error'}
sx={storageCleanerPageStyles.CLEANING_RESULT_ALERT}
>
{result.success
? formatCleaningResult(result, t)
: result.error || t('storageCleaner:partialFailure')}
</Alert>
</Box>
);
}
+62
View File
@@ -0,0 +1,62 @@
import { Box } from '@mui/material';
import StorageIcon from '@mui/icons-material/Storage';
import PageHeader from '@/components/PageHeader';
import { formatSize } from '@/utils/storageCleaner';
import { storageCleanerPageStyles } from '@/config/pageTheme';
import { useTranslation } from 'react-i18next';
/**
* DomainHeader 组件属性接口
*/
interface DomainHeaderProps {
/** 当前域名 */
domain: string;
/** 已占用的存储大小(字节) */
totalSize: number;
}
/**
* DomainHeader - 存储清理页面标题栏组件
*
* 使用 PageHeader 组件构建,显示域名和已占用存储空间大小
*
* @example
* ```tsx
* <DomainHeader
* domain="example.com"
* totalSize={1048576}
* />
* ```
*/
export default function DomainHeader({ domain, totalSize }: DomainHeaderProps) {
const { t } = useTranslation(['storageCleaner']);
return (
<PageHeader
icon={<StorageIcon sx={{ fontSize: 22 }} />}
iconColor={storageCleanerPageStyles.warningColor}
title={t('storageCleaner:pageTitle')}
subtitle={domain || t('storageCleaner:loading')}
badge={
totalSize > 0 ? (
<Box sx={storageCleanerPageStyles.DOMAIN_HEADER_BADGE}>
{t('storageCleaner:occupied', { size: formatSize(totalSize) })}
</Box>
) : null
}
iconSx={storageCleanerPageStyles.DOMAIN_HEADER_ICON}
titleSx={{
fontSize: '1rem',
}}
subtitleSx={{
display: 'block',
maxWidth: 240,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
mt: 0.3,
fontSize: '0.75rem',
}}
sx={{ mb: 3 }}
/>
);
}
+44
View File
@@ -0,0 +1,44 @@
import { Box, Container, Typography } from '@mui/material';
import WarningIcon from '@mui/icons-material/Warning';
import { storageCleanerPageStyles } from '@/config/pageTheme';
import { useTranslation } from 'react-i18next';
interface ErrorDisplayProps {
error: string;
}
export default function ErrorDisplay({ error }: ErrorDisplayProps) {
const { t } = useTranslation(['storageCleaner']);
return (
<Container sx={storageCleanerPageStyles.ERROR_DISPLAY_CONTAINER}>
<Box sx={{ width: '100%', maxWidth: 320 }}>
<Box sx={storageCleanerPageStyles.ERROR_DISPLAY_BOX}>
<WarningIcon sx={{ fontSize: 36, color: 'error.main', mb: 2 }} />
<Typography
variant="body1"
color="error.main"
sx={{
fontSize: '0.9rem',
fontWeight: 700,
lineHeight: 1.4,
mb: 3,
}}
>
{error}
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{
fontSize: '0.75rem',
fontWeight: 500,
lineHeight: 1.4,
}}
>
{t('storageCleaner:errorStandardOnly')}
</Typography>
</Box>
</Box>
</Container>
);
}
+51
View File
@@ -0,0 +1,51 @@
import { Box, Checkbox, Typography } from '@mui/material';
import { formatSize } from '@/utils/storageCleaner';
import { storageCleanerPageStyles } from '@/config/pageTheme';
import { useTranslation } from 'react-i18next';
interface OptionItemProps {
labelKey: string;
checked: boolean;
size?: number;
isCount?: boolean;
onChange: () => void;
}
export default function OptionItem({
labelKey,
checked,
size,
isCount = false,
onChange,
}: OptionItemProps) {
const { t } = useTranslation(['storageCleaner']);
return (
<Box sx={storageCleanerPageStyles.OPTION_ITEM(checked)}>
<Box sx={{ flex: 1, minWidth: 0, mr: 1.5 }}>
<Typography
variant="body2"
fontWeight={700}
sx={storageCleanerPageStyles.OPTION_ITEM_LABEL(checked)}
>
{t(labelKey)}
</Typography>
{size !== undefined && size > 0 ? (
<Typography variant="caption" sx={storageCleanerPageStyles.OPTION_ITEM_SIZE}>
{isCount ? `${size} ${t('storageCleaner:countUnit')}` : formatSize(size)}
</Typography>
) : (
<Typography variant="caption" sx={storageCleanerPageStyles.OPTION_ITEM_NO_DATA}>
{t('storageCleaner:noData')}
</Typography>
)}
</Box>
<Checkbox
size="small"
checked={checked}
onChange={onChange}
color="warning"
sx={storageCleanerPageStyles.OPTION_ITEM_CHECKBOX}
/>
</Box>
);
}
@@ -0,0 +1,102 @@
import {
Box,
Chip,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Typography,
} from '@mui/material';
import type { StorageCleanerOptions } from '@/types/storage';
import Button from '@/components/Button';
import { storageCleanerPageStyles } from '@/config/pageTheme';
import { useTranslation } from 'react-i18next';
export interface StorageCleanerConfirmProps {
open: boolean;
onClose: () => void;
onConfirm: () => void;
options: StorageCleanerOptions;
}
export function StorageCleanerConfirm({
open,
onClose,
onConfirm,
options,
}: StorageCleanerConfirmProps) {
const { t } = useTranslation(['storageCleaner']);
const selectedOptions = Object.entries(options)
.filter(([_, value]) => value)
.map(([key, _]) => t(`storageCleaner:options.${key as keyof StorageCleanerOptions}`));
return (
<Dialog
open={open}
onClose={onClose}
fullWidth
maxWidth="xs"
slotProps={{
paper: {
sx: storageCleanerPageStyles.CONFIRM_DIALOG_PAPER,
},
}}
>
<DialogTitle sx={storageCleanerPageStyles.CONFIRM_DIALOG_TITLE}>
{t('storageCleaner:confirmTitle')}
</DialogTitle>
<DialogContent sx={storageCleanerPageStyles.CONFIRM_DIALOG_CONTENT}>
<Typography
variant="body2"
color="text.secondary"
sx={storageCleanerPageStyles.CONFIRM_DIALOG_DESC}
>
{t('storageCleaner:confirmDesc')}
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1.2, justifyContent: 'center', mb: 4 }}>
{selectedOptions.map((label) => (
<Chip
key={label}
label={label}
size="small"
sx={storageCleanerPageStyles.CONFIRM_DIALOG_CHIP}
/>
))}
</Box>
<Box sx={storageCleanerPageStyles.CONFIRM_DIALOG_WARNING_BOX}>
<Typography variant="caption" sx={storageCleanerPageStyles.CONFIRM_DIALOG_WARNING_TEXT}>
<span role="img" aria-label="warning">
</span>{' '}
{t('storageCleaner:irreversible')}
</Typography>
</Box>
</DialogContent>
<DialogActions sx={{ p: 3, pt: 1, gap: 2 }}>
<Button
variant="text"
onClick={onClose}
fullWidth
sx={storageCleanerPageStyles.CONFIRM_DIALOG_CANCEL}
>
{t('common:buttons.cancel')}
</Button>
<Button
variant="contained"
onClick={onConfirm}
fullWidth
sx={storageCleanerPageStyles.CONFIRM_DIALOG_CONFIRM}
>
{t('storageCleaner:confirmAction')}
</Button>
</DialogActions>
</Dialog>
);
}
export default StorageCleanerConfirm;
@@ -0,0 +1,72 @@
import { Box, Checkbox, Divider, Grid, Typography } from '@mui/material';
import type { StorageCleanerOptions } from '@/types/storage';
import OptionItem from './OptionItem';
import { storageCleanerPageStyles } from '@/config/pageTheme';
import { useTranslation } from 'react-i18next';
interface StorageOptionsGridProps {
options: StorageCleanerOptions;
sizes: Record<string, number>;
allSelected: boolean;
someSelected: boolean;
onOptionChange: (key: keyof StorageCleanerOptions) => void;
onSelectAll: (checked: boolean) => void;
}
export default function StorageOptionsGrid({
options,
sizes,
allSelected,
someSelected,
onOptionChange,
onSelectAll,
}: StorageOptionsGridProps) {
const { t } = useTranslation(['storageCleaner']);
const optionKeys: { key: keyof StorageCleanerOptions; isCount?: boolean }[] = [
{ key: 'localStorage' },
{ key: 'sessionStorage' },
{ key: 'indexedDB' },
{ key: 'cookies' },
{ key: 'cacheStorage', isCount: true },
{ key: 'serviceWorkers', isCount: true },
];
return (
<Box sx={storageCleanerPageStyles.OPTIONS_GRID_CONTAINER}>
<Box sx={{ p: 1.2 }}>
<Grid container spacing={1.5}>
{optionKeys.map(({ key, isCount }) => (
<Grid size={6} key={key}>
<OptionItem
labelKey={`storageCleaner:options.${key}`}
checked={options[key]}
size={sizes[key]}
isCount={isCount}
onChange={() => onOptionChange(key)}
/>
</Grid>
))}
</Grid>
</Box>
<Divider sx={{ mx: 0, borderColor: 'grey.100' }} />
<Box sx={storageCleanerPageStyles.OPTIONS_GRID_FOOTER}>
<Typography
variant="body2"
fontWeight={700}
sx={{ color: 'text.secondary', fontSize: '0.7rem', px: 0 }}
>
{t('storageCleaner:selectAll')}
</Typography>
<Checkbox
size="small"
checked={allSelected}
indeterminate={someSelected}
onChange={(e) => onSelectAll(e.target.checked)}
color="warning"
sx={storageCleanerPageStyles.OPTIONS_GRID_CHECKBOX}
/>
</Box>
</Box>
);
}
+88
View File
@@ -0,0 +1,88 @@
import { Box, CircularProgress, Container } from '@mui/material';
import Button from '@/components/Button';
import { useSnackbar } from '@/components/GlobalSnackbar';
import StorageCleanerConfirm from '@/pages/StorageCleaner/StorageCleanerConfirm';
import { storageCleanerPageStyles } from '@/config/pageTheme';
import { useStorageCleaner } from './useStorageCleaner';
import DomainHeader from './DomainHeader';
import StorageOptionsGrid from './StorageOptionsGrid';
import AutoRefreshToggle from './AutoRefreshToggle';
import ErrorDisplay from './ErrorDisplay';
import CleaningResult from './CleaningResult';
import { useTranslation } from 'react-i18next';
export default function Index() {
const { showMessage } = useSnackbar();
const { t } = useTranslation(['storageCleaner']);
const {
domain,
error,
isInitializing,
options,
sizes,
autoRefresh,
loading,
result,
showConfirm,
setShowConfirm,
totalSize,
allSelected,
someSelected,
handleAutoRefreshChange,
handleOptionChange,
handleSelectAll,
handleClean,
} = useStorageCleaner({ showMessage });
const isDisabled = !(someSelected || allSelected) || loading;
if (isInitializing) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<CircularProgress size={24} color="warning" />
</Box>
);
}
if (error) {
return <ErrorDisplay error={error} />;
}
return (
<Box>
<Container sx={{ py: 2 }}>
<DomainHeader domain={domain} totalSize={totalSize} />
<StorageOptionsGrid
options={options}
sizes={sizes}
allSelected={allSelected}
someSelected={someSelected}
onOptionChange={handleOptionChange}
onSelectAll={handleSelectAll}
/>
<AutoRefreshToggle autoRefresh={autoRefresh} onChange={handleAutoRefreshChange} />
<Button
variant="contained"
onClick={() => setShowConfirm(true)}
sx={storageCleanerPageStyles.CONFIRM_DIALOG_CONFIRM}
disabled={isDisabled}
fullWidth
>
{loading ? t('storageCleaner:cleaning') : t('storageCleaner:cleanNow')}
</Button>
<CleaningResult result={result} />
</Container>
<StorageCleanerConfirm
open={showConfirm}
onClose={() => setShowConfirm(false)}
onConfirm={handleClean}
options={options}
/>
</Box>
);
}
+281
View File
@@ -0,0 +1,281 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { storageUtil } from '@/utils/chromeStorage';
import type { SnackbarOptions } from '@/components/GlobalSnackbar';
import type {
CleaningResult,
StorageCleanerOptions,
StorageCleanerPreferences,
} from '@/types/storage';
import {
clearStorage,
getCacheStorageSize,
getCookieSize,
getCurrentTab,
getIndexedDBSize,
getLocalStorageSize,
getServiceWorkerCount,
getSessionStorageSize,
isRestrictedUrl,
} from '@/utils/storageCleaner';
import { MessageAction, sendMessage } from '@/utils/messages';
import { useTranslation } from 'react-i18next';
const DEFAULT_OPTIONS: StorageCleanerOptions = {
localStorage: true,
sessionStorage: true,
indexedDB: true,
cookies: true,
cacheStorage: true,
serviceWorkers: true,
};
const DEFAULT_PREFERENCES: StorageCleanerPreferences = {
autoRefresh: true,
selectedTypes: DEFAULT_OPTIONS,
};
export interface UseStorageCleanerReturn {
// State
domain: string;
error: string;
isInitializing: boolean;
options: StorageCleanerOptions;
sizes: Record<string, number>;
autoRefresh: boolean;
loading: boolean;
result: CleaningResult | null;
showConfirm: boolean;
setShowConfirm: (show: boolean) => void;
// Computed
totalSize: number;
allSelected: boolean;
someSelected: boolean;
// Handlers
handleAutoRefreshChange: (checked: boolean) => Promise<void>;
handleOptionChange: (key: keyof StorageCleanerOptions) => Promise<void>;
handleSelectAll: (checked: boolean) => Promise<void>;
handleClean: () => Promise<void>;
}
export interface UseStorageCleanerOptions {
showMessage: (message: string, options?: SnackbarOptions) => void;
}
export function useStorageCleaner({
showMessage,
}: UseStorageCleanerOptions): UseStorageCleanerReturn {
const { t } = useTranslation(['storageCleaner', 'common']);
const [domain, setDomain] = useState<string>('');
const [error, setError] = useState<string>('');
const [isInitializing, setIsInitializing] = useState<boolean>(true);
const [options, setOptions] = useState<StorageCleanerOptions>(DEFAULT_OPTIONS);
const [sizes, setSizes] = useState<Record<string, number>>({});
const [autoRefresh, setAutoRefresh] = useState<boolean>(true);
const [loading, setLoading] = useState<boolean>(false);
const [result, setResult] = useState<CleaningResult | null>(null);
const [showConfirm, setShowConfirm] = useState<boolean>(false);
const requestIdRef = useRef<number>(0);
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const loadingRef = useRef(loading);
useEffect(() => {
loadingRef.current = loading;
}, [loading]);
useEffect(() => {
return () => {
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
};
}, []);
const loadInfo = useCallback(async () => {
const currentRequestId = ++requestIdRef.current;
try {
const tab = await getCurrentTab();
if (currentRequestId !== requestIdRef.current) return;
if (!tab || !tab.url) {
setError(t('storageCleaner:errorNoTab'));
return;
}
if (isRestrictedUrl(tab.url)) {
setError(t('storageCleaner:errorRestricted'));
return;
}
setError('');
const url = tab.url;
const tabId = tab.id!;
setDomain(new URL(url).hostname);
const [savedPrefs, cSize, lsSize, ssSize, idbSize, cacheCount, swCount] = await Promise.all([
storageUtil.get('storageCleaner/preferences', DEFAULT_PREFERENCES),
getCookieSize(url),
getLocalStorageSize(tabId),
getSessionStorageSize(tabId),
getIndexedDBSize(tabId),
getCacheStorageSize(tabId),
getServiceWorkerCount(tabId),
]);
if (currentRequestId !== requestIdRef.current) return;
if (savedPrefs) {
setAutoRefresh(savedPrefs.autoRefresh ?? DEFAULT_PREFERENCES.autoRefresh);
setOptions(savedPrefs.selectedTypes ?? DEFAULT_PREFERENCES.selectedTypes);
}
setSizes({
cookies: cSize,
localStorage: lsSize,
sessionStorage: ssSize,
indexedDB: idbSize,
cacheStorage: cacheCount,
serviceWorkers: swCount,
});
} finally {
if (currentRequestId === requestIdRef.current) {
setIsInitializing(false);
}
}
}, [t]);
const loadInfoRef = useRef(loadInfo);
loadInfoRef.current = loadInfo;
const debouncedLoadInfo = useCallback(() => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
debounceTimerRef.current = setTimeout(() => {
loadInfoRef.current().catch(console.error);
}, 300);
}, []);
useEffect(() => {
// 首次加载不防抖
loadInfoRef.current().catch(console.error);
const handleTabChange = () => debouncedLoadInfo();
const handleTabUpdated = (_tabId: number, changeInfo: { status?: string; url?: string }) => {
if (changeInfo.status === 'complete' || changeInfo.url) {
debouncedLoadInfo();
}
};
chrome.tabs.onActivated.addListener(handleTabChange);
chrome.tabs.onUpdated.addListener(handleTabUpdated);
chrome.windows.onFocusChanged.addListener(handleTabChange);
return () => {
chrome.tabs.onActivated.removeListener(handleTabChange);
chrome.tabs.onUpdated.removeListener(handleTabUpdated);
chrome.windows.onFocusChanged.removeListener(handleTabChange);
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
};
}, [debouncedLoadInfo]);
const handleAutoRefreshChange = useCallback(
async (checked: boolean) => {
setAutoRefresh(checked);
await storageUtil.set('storageCleaner/preferences', {
autoRefresh: checked,
selectedTypes: options,
});
},
[options],
);
const handleOptionChange = useCallback(
async (key: keyof StorageCleanerOptions) => {
const newOptions = { ...options, [key]: !options[key] };
setOptions(newOptions);
await storageUtil.set('storageCleaner/preferences', {
autoRefresh,
selectedTypes: newOptions,
});
},
[options, autoRefresh],
);
const handleSelectAll = useCallback(
async (checked: boolean) => {
const newOptions = {
localStorage: checked,
sessionStorage: checked,
indexedDB: checked,
cookies: checked,
cacheStorage: checked,
serviceWorkers: checked,
};
setOptions(newOptions);
await storageUtil.set('storageCleaner/preferences', {
autoRefresh,
selectedTypes: newOptions,
});
},
[autoRefresh],
);
const handleClean = useCallback(async () => {
if (loadingRef.current) {
return;
}
const tab = await getCurrentTab();
if (!tab || !tab.id || !tab.url) {
showMessage(t('storageCleaner:errorNoTab'), { severity: 'warning' });
return;
}
setLoading(true);
try {
const cleaningResult = await clearStorage(tab.id, tab.url, options);
setResult(cleaningResult);
if (autoRefresh && cleaningResult.success) {
showMessage(t('storageCleaner:cleanSuccessReload'), { severity: 'success' });
await sendMessage(MessageAction.RELOAD_TAB, { tabId: tab.id, delay: 1000 });
} else {
await loadInfo();
}
} catch (err) {
showMessage(`${t('common:messages.copyError')}: ${String(err)}`, { severity: 'error' });
} finally {
setLoading(false);
setShowConfirm(false);
}
}, [options, autoRefresh, showMessage, loadInfo, t]);
const totalSize =
(sizes.cookies || 0) +
(sizes.localStorage || 0) +
(sizes.sessionStorage || 0) +
(sizes.indexedDB || 0);
const allSelected = Object.values(options).every(Boolean);
const someSelected = Object.values(options).some(Boolean) && !allSelected;
return {
domain,
error,
isInitializing,
options,
sizes,
autoRefresh,
loading,
result,
showConfirm,
setShowConfirm,
totalSize,
allSelected,
someSelected,
handleAutoRefreshChange,
handleOptionChange,
handleSelectAll,
handleClean,
};
}