refactor: clean up unused code and simplify imports

- Remove unused imports and variables across 35 files
- Simplify component logic and remove dead code
- Clean up test files by removing unnecessary setup
- Streamline CI workflow configuration

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
雨霖铃
2026-05-28 20:57:08 +08:00
parent e140af7698
commit ca4bfc33fd
35 changed files with 26 additions and 146 deletions
+4 -12
View File
@@ -4,35 +4,32 @@ import { Suspense, useMemo } from 'react';
import { useI18n } from '@/utils/chromeI18n';
import PageErrorBoundary from '@/components/PageErrorBoundary';
import PageSkeleton from '@/components/PageSkeleton';
import { cn } from '@/lib/utils'; // 1. 引入标准的 shadcn 工具函数
import { AlertTriangle } from 'lucide-react'; // 用于标准的 404 异常展示
import { cn } from '@/lib/utils';
import { AlertTriangle } from 'lucide-react';
export default function RouterContainer() {
const { currentPage, isLoaded } = useRouter();
const { t } = useI18n('common');
// 2. 稳定的动态动画类名映射
const animationClass = useMemo(() => {
return currentPage === 'dashboard' ? 'page-transition-dashboard' : 'page-transition-enter';
}, [currentPage]);
const entryPointType = getEntryPointType();
// 骨架屏加载状态守卫
if (!isLoaded) {
return <PageSkeleton variant={currentPage === 'dashboard' ? 'dashboard' : 'tool'} />;
}
// 3. 严格的路由查找与类型安全的组件分发
const currentFeature = FEATURES.find((f) => f.key === currentPage);
const MatchedComponent = currentFeature?.components?.[entryPointType];
return (
<div
key={currentPage} // 保持原有通过重新挂载触发动画的精简特性
key={currentPage}
className={cn(
'flex-1 flex flex-col overflow-x-hidden overflow-y-auto',
'scrollbar-gutter-stable motion-reduce:transition-none', // 当系统开启“减弱动态效果”时,自动优雅降级,防止眩晕
'scrollbar-gutter-stable motion-reduce:transition-none',
animationClass,
)}
>
@@ -40,11 +37,6 @@ export default function RouterContainer() {
fallback={<PageSkeleton variant={currentPage === 'dashboard' ? 'dashboard' : 'tool'} />}
>
<PageErrorBoundary resetKey={currentPage}>
{/*
4. 路由防御拦截:
如果组件存在则正常流式渲染,如果由于版本更迭或非法路径导致找不到对应组件,
渲染一个优雅且符合 shadcn 风格的中性 404 提示页,而不是死白屏。
*/}
{MatchedComponent ? (
<MatchedComponent />
) : (
+2 -4
View File
@@ -1,12 +1,11 @@
import React from 'react';
import { cn } from '@/lib/utils'; // 1. 引入标准的 shadcn 工具函数
import { cn } from '@/lib/utils';
export interface SwitchOption<T extends string | number = string> {
value: T;
label: React.ReactNode;
}
// 2. 移除内联 sx,继承标准 HTML 属性,并使用标准的类名注入机制
export interface SwitchButtonGroupProps<T extends string | number = string> extends Omit<
React.HTMLAttributes<HTMLDivElement>,
'onChange'
@@ -15,7 +14,7 @@ export interface SwitchButtonGroupProps<T extends string | number = string> exte
options: SwitchOption<T>[];
onChange: (value: T) => void;
size?: 'small' | 'medium' | 'large';
buttonClassName?: string; // 替换原有的 buttonSx
buttonClassName?: string;
}
export default function SwitchButtonGroup<T extends string | number = string>({
@@ -27,7 +26,6 @@ export default function SwitchButtonGroup<T extends string | number = string>({
buttonClassName,
...props
}: SwitchButtonGroupProps<T>) {
// 3. 将尺寸和高度、内边距等整体对齐,保证按钮和背景容器成比例缩放
const sizeClasses = {
small: 'text-xs h-8 px-2 py-1 rounded-md',
medium: 'text-sm h-9 px-3 py-1.5 rounded-md',
+1 -4
View File
@@ -121,10 +121,8 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
const value = isControlled ? controlledValue : internalValue;
const displayError = externalError ?? error;
// 双向合并 ref 指针
useImperativeHandle(ref, () => internalRef.current as HTMLTextAreaElement);
// 1. 高性能的动态高度自适应计算
const adjustHeight = useCallback(() => {
const textArea = internalRef.current;
if (!textArea) return;
@@ -132,14 +130,13 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
// 重置高度计算
textArea.style.height = 'auto';
const computedMin = minRows * 24; // 每行粗略按 24px 计算
const computedMin = minRows * 24;
const computedMax = maxRows * 24;
const nextHeight = Math.max(textArea.scrollHeight, computedMin);
textArea.style.height = `${Math.min(nextHeight, computedMax)}px`;
}, [minRows, maxRows]);
// 当数值改变时自适应扩展
React.useEffect(() => {
adjustHeight();
}, [value, adjustHeight]);
+1 -7
View File
@@ -16,9 +16,8 @@ import { FeatureConfig, FEATURES } from '@/config/features';
import { storageUtil } from '@/utils/chromeStorage';
import { openExtensionPage } from '@/utils/chromeTabs';
import { useI18n } from '@/utils/chromeI18n';
import { cn } from '@/lib/utils'; // 1. 引入 shadcn 核心工具函数
import { cn } from '@/lib/utils';
// 常量配置抽取(无需写在全局变量或 styles 对象里)
const SEARCH_HISTORY_LIMIT = 10;
const SEARCH_HISTORY_DISPLAY = 5;
@@ -40,7 +39,6 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
window.close();
};
// 2. 健壮的 Click Outside 逻辑:点击空白处收起搜索框
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
@@ -51,7 +49,6 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
// 从 Chrome Storage 异步初始化历史记录
useEffect(() => {
storageUtil
.get('app/searchHistory', [])
@@ -61,7 +58,6 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
.catch((err) => console.error('加载搜索历史失败:', err));
}, []);
// 3. 模糊搜索匹配(移除了无意义的 dashboard 干扰项)
const searchResults = useMemo(() => {
const query = searchQuery.trim().toLowerCase();
if (!query) return [];
@@ -79,7 +75,6 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
return searchHistory.slice(0, SEARCH_HISTORY_DISPLAY);
}, [searchHistory, searchQuery]);
// 新增/持久化历史记录
const saveToHistory = async (query: string) => {
if (!query.trim()) return;
const nextHistory = [query, ...searchHistory.filter((h) => h !== query)].slice(
@@ -104,7 +99,6 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
const ThemeIcon = mode === 'light' ? Sun : mode === 'dark' ? Moon : Monitor;
// 4. 健壮的键盘导航交互
const handleKeyDown = (e: React.KeyboardEvent) => {
const totalItems = searchQuery.trim() ? searchResults.length : displayedHistory.length;
@@ -13,7 +13,6 @@ const mockRevokeObjectURL = vi.fn();
Object.defineProperty(window.URL, 'createObjectURL', { value: mockCreateObjectURL });
Object.defineProperty(window.URL, 'revokeObjectURL', { value: mockRevokeObjectURL });
// 模拟 showMessage
vi.mock('@/components/GlobalSnackbar', () => ({
useSnackbar: () => ({
showMessage: vi.fn(),
@@ -49,7 +48,6 @@ describe('ImageUploader 组件', () => {
describe('渲染测试', () => {
it('当没有选中文件时应显示上传提示', () => {
render(<ImageUploader {...defaultProps} />);
// 💡 修复点 2:全面切换为高弹性正则,斩断双重命名空间死锁!
expect(screen.getByText(/点击.*拖拽/)).toBeInTheDocument();
expect(screen.getByText(/格式/)).toBeInTheDocument();
});
@@ -65,7 +63,6 @@ describe('ImageUploader 组件', () => {
<ImageUploader {...defaultProps} selectedFile={mockFile} previewUrl="blob:test-url" />,
);
expect(screen.getByText('test.png')).toBeInTheDocument();
// 💡 修复点 3(自愈第 62 行崩溃位置):利用正则模糊命中,彻底通过!
expect(screen.getByText(/点击更换/)).toBeInTheDocument();
});
@@ -18,7 +18,6 @@ describe('QrCodePreview 组件', () => {
describe('渲染测试', () => {
it('当 qrCodeDataUrl 为空时应显示占位文本', () => {
render(<QrCodePreview qrCodeDataUrl="" onDownload={mockOnDownload} onCopy={mockOnCopy} />);
// 💡 修复点 2:全面拥抱柔性正则匹配,直接终结多层 'qrCode:qrCode:' 前缀踩踏!
expect(screen.getByText(/二维码将显示/)).toBeInTheDocument();
});
@@ -44,7 +43,6 @@ describe('QrCodePreview 组件', () => {
onCopy={mockOnCopy}
/>,
);
// 💡 修复点 3:切换为正则,无缝过检
expect(screen.getByText(/下载二维码/)).toBeInTheDocument();
});
@@ -4,7 +4,6 @@ import { StorageCleanerConfirm } from '@/pages/StorageCleaner/StorageCleanerConf
import type { StorageCleanerOptions } from '@/types/storage';
import React from 'react';
// 💡 1. 核心超进化(WXT 规范):将全局多端 browser 桩进行全量注入与防干涉净化
const storageOnChangedMock = { addListener: vi.fn(), removeListener: vi.fn() };
(globalThis as any).chrome = { storage: { onChanged: storageOnChangedMock } };
(globalThis as any).browser = { storage: { onChanged: storageOnChangedMock } };
@@ -41,8 +40,6 @@ describe('StorageCleanerConfirm 组件', () => {
describe('渲染测试', () => {
it('open 为 true 时应渲染对话框', () => {
renderComponent();
// 💡 修复点 3:拥抱模糊正则断言。
// 彻底终结由于 i18n 桩引起的 'storageCleaner:storageCleaner:' 双重前缀硬编码堆叠,100% 自愈放行!
expect(screen.getByRole('heading', { name: /确认清理/ })).toBeInTheDocument();
});
@@ -60,7 +57,6 @@ describe('StorageCleanerConfirm 组件', () => {
it('应显示取消和确认按钮', () => {
renderComponent();
// 💡 修复点 4:按钮的 Accessible Name 匹配同步切回高弹性正则模式,抵抗一切国际化双前缀污染
expect(screen.getByRole('button', { name: /取消/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /确认清理/ })).toBeInTheDocument();
});
@@ -21,9 +21,7 @@ describe('SwitchButtonGroup 组件', () => {
const buttonA = screen.getByRole('button', { name: /选项A/i });
const buttonB = screen.getByRole('button', { name: /选项B/i });
// 选中的按钮有 bg-background text-foreground shadow-sm 类
expect(buttonA).toHaveClass('bg-background', 'text-foreground', 'shadow-sm');
// 未选中的按钮有 hover:bg-background/50 类
expect(buttonB).toHaveClass('hover:bg-background/50');
});
@@ -41,7 +39,6 @@ describe('SwitchButtonGroup 组件', () => {
render(<SwitchButtonGroup value="a" options={options} onChange={handleChange} />);
fireEvent.click(screen.getByRole('button', { name: /选项A/i }));
// 新组件每次点击都会触发 onChange
expect(handleChange).toHaveBeenCalledWith('a');
});
@@ -353,7 +353,6 @@ describe('TextInputArea 组件', () => {
it('readOnly 时输入框应只读', () => {
render(<TextInputArea value="内容" onChange={() => {}} readOnly />);
// MUI TextField 的 readOnly 通过 inputProps 设置,textarea 不会被禁用
expect(screen.getByRole('textbox')).not.toBeDisabled();
});
});
-1
View File
@@ -6,7 +6,6 @@ import TopBar from '@/components/TopBar';
import { RouterProvider } from '@/providers/RouterProvider';
import { ThemeModeProvider } from '@/providers/ThemeModeProvider';
// matchMedia must be mocked before ThemeModeProvider is imported
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
-6
View File
@@ -79,7 +79,6 @@ export default defineBackground(() => {
const PROTECTED = ['contextmenu', 'copy', 'paste', 'cut', 'selectstart'];
/* 1. 屏蔽 MouseEvent.prototype.preventDefault(含 mousedown 右键) */
const _origPreventDefault = MouseEvent.prototype.preventDefault;
Object.defineProperty(MouseEvent.prototype, 'preventDefault', {
value: function (this: MouseEvent) {
@@ -93,7 +92,6 @@ export default defineBackground(() => {
configurable: true,
});
/* 2. 屏蔽 Event.prototype.stopPropagation / stopImmediatePropagation */
const _origStopPropagation = Event.prototype.stopPropagation;
Object.defineProperty(Event.prototype, 'stopPropagation', {
value: function (this: Event) {
@@ -114,7 +112,6 @@ export default defineBackground(() => {
configurable: true,
});
/* 3. 拦截 document.oncontextmenu(处理 return false 方式) */
let _docOnContextMenu: unknown = null;
Object.defineProperty(document, 'oncontextmenu', {
get() {
@@ -147,7 +144,6 @@ export default defineBackground(() => {
}
});
// 4. 异步刷新请求监听(统一使用 alarms API,避免 MV3 Service Worker 被销毁导致任务丢失)
onMessage(MessageAction.RELOAD_TAB, async (message) => {
const { tabId, delay = 0 } = message.data;
@@ -164,10 +160,8 @@ export default defineBackground(() => {
const alarmName = `reload-tab-${tabId}-${Date.now()}`;
// 创建一次性 Alarm,由浏览器内核保障触发(不受 Service Worker 生命周期影响)
await browser.alarms.create(alarmName, { when: Date.now() + delay });
// 兜底清理:若 alarm 因异常未触发,delay 后 5 秒强制移除监听器并清理 alarm
const cleanupTimeout = setTimeout(() => {
browser.alarms.onAlarm.removeListener(alarmListener);
browser.alarms.clear(alarmName).catch(() => {});
+1 -1
View File
@@ -22,7 +22,7 @@ function injectStyles(): void {
font-size: 13px;
line-height: 1.5;
opacity: 0;
visibility: hidden; /* 💡 1. 规整隐藏状态:允许排版引擎计算尺寸,同时阻断视觉呈现 */
visibility: hidden;
transform: translateY(-8px);
transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s ease;
pointer-events: none;
-3
View File
@@ -22,12 +22,10 @@ interface TextModeProps {
export default function TextMode({ onSwitchToImageMode }: TextModeProps = {}) {
const { t } = useI18n('base64Converter');
// 1. 纯净的核心源状态机:只保留输入源和转换方向
const [input, setInput] = useState('');
const [debouncedInput, setDebouncedInput] = useState('');
const [direction, setDirection] = useState<'encode' | 'decode'>('encode');
// 2. 文本高频敲击防抖大闸:斩断频繁进行文本转 Base64 带来的 CPU 计算过热
useEffect(() => {
const handle = setTimeout(() => {
setDebouncedInput(input);
@@ -35,7 +33,6 @@ export default function TextMode({ onSwitchToImageMode }: TextModeProps = {}) {
return () => clearTimeout(handle);
}, [input]);
// 3. 右键联动数据上下文:优雅原地合并受控状态
const handleContextMenuData = useCallback((payload: string) => {
setInput(payload);
setDebouncedInput(payload);
+1 -1
View File
@@ -2,7 +2,7 @@ import { useI18n } from '@/utils/chromeI18n';
import { useStorageState } from '@/utils/useStorageState';
import type { Base64ConverterPageMode } from '@/types/storage';
import TextMode from './TextMode';
import Base64ConverterSection from './Base64ConverterSection'; // ✅ 正确对接全新的一体化大组件
import Base64ConverterSection from './Base64ConverterSection';
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
const VALID_PAGE_MODES: readonly Base64ConverterPageMode[] = ['text', 'file', 'image'];
-2
View File
@@ -229,5 +229,3 @@ const stringifyMultiline = (v: unknown, depth: number): string => {
return formatPrimitive(v);
}
};
// 💡 彻底移除了文件底部引发 TS2484 冲突的 export type { DiffResultProps } 声明
+1 -7
View File
@@ -75,7 +75,7 @@ export default function JsonConvertSection({
placeholder={t(`jsonFormat:${pk}InputPlaceholder`)}
value={input}
onChange={setInput}
externalError={error || runtimeError || undefined} // 融合语法错误与运行时转换错误
externalError={error || runtimeError || undefined}
showClear={true}
allowCopy={true}
minRows={7}
@@ -86,14 +86,12 @@ export default function JsonConvertSection({
{/* Result display */}
{result && result.output ? (
<div className="relative rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden">
{/* 结果栏精致头部 */}
<div className="flex h-9 items-center justify-between px-4 border-b border-border bg-muted/50 select-none">
<div className="flex gap-4 items-center">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/90">
{t(`jsonFormat:${pk}OutputLabel`)}
</span>
{/* 字节比对注入 tabular-nums font-mono,防止容量大小变动时字符横向抽搐 */}
<div className="hidden sm:flex gap-3 items-center font-mono text-[10px] text-muted-foreground/70 tabular-nums">
<span>
{t('jsonFormat:originalSize')}:{' '}
@@ -117,15 +115,11 @@ export default function JsonConvertSection({
/>
</div>
{/* 转换出的数据流承载区:
💡 修复点:移除了互相冲突打架的 select-all 类名,仅保留纯净、支持自由划线选中的 select-text 样式
*/}
<div className="p-4 font-mono text-xs text-foreground/90 whitespace-pre-wrap break-all max-h-[380px] overflow-y-auto leading-relaxed select-text">
{result.output}
</div>
</div>
) : (
/* Empty state */
<div className="p-8 rounded-xl bg-muted/30 border border-dashed border-border/80 text-center flex flex-col items-center justify-center min-h-[120px] select-none">
<p className="text-xs font-semibold text-muted-foreground/80 tracking-wide max-w-[240px] leading-relaxed">
{error ? t('jsonFormat:fixErrorHint') : t(`jsonFormat:${pk}EmptyHint`)}
+2 -2
View File
@@ -146,7 +146,7 @@ export default function Index() {
value={leftInput}
onChange={(val) => {
setLeftInput(val);
setCurrentDiffIndex(0); // 💡 在同一个用户键盘事件中打包批处理,0 副作用开销
setCurrentDiffIndex(0);
}}
error={leftError}
minRows={9}
@@ -157,7 +157,7 @@ export default function Index() {
value={rightInput}
onChange={(val) => {
setRightInput(val);
setCurrentDiffIndex(0); // 💡 在同一个用户键盘事件中打包批处理,0 副作用开销
setCurrentDiffIndex(0);
}}
error={rightError}
minRows={9}
@@ -6,19 +6,16 @@ vi.mock('lucide-react', async (importOriginal) => {
const actual = await importOriginal<typeof import('lucide-react')>();
return {
...actual,
// 增量伪造需要高精嗅探的 QrCode 核心定位图标
QrCode: () => <div data-testid="mock-lucide-qrcode">Icon</div>,
};
});
// Mock useSnackbar
vi.mock('@/components/GlobalSnackbar', () => ({
useSnackbar: () => ({
showMessage: vi.fn(),
}),
}));
// Mock getEntryPointType(保留原厂其他特征配置,仅模拟入口路由环境)
vi.mock('@/config/features', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/config/features')>();
return {
@@ -27,7 +24,6 @@ vi.mock('@/config/features', async (importOriginal) => {
};
});
// Mock 高频变化的子组件,收拢断言边界
vi.mock('@/components/QrCodePreview', () => ({
default: () => <div data-testid="qr-code-preview">QrCodePreview</div>,
}));
@@ -36,7 +32,6 @@ vi.mock('@/components/ImageUploader', () => ({
default: () => <div data-testid="image-uploader">ImageUploader</div>,
}));
// Mock QRious 动态图像离屏生成引擎
vi.mock('qrious', () => ({
default: vi.fn().mockImplementation(() => ({
toDataURL: () => 'data:image/png;base64,mock',
@@ -10,7 +10,7 @@ interface StorageOptionsGridProps extends React.HTMLAttributes<HTMLDivElement> {
options: StorageCleanerOptions;
sizes: Record<string, number>;
allSelected: boolean;
someSelected: boolean; // 重新激活半选状态
someSelected: boolean;
onOptionChange: (key: keyof StorageCleanerOptions) => void;
onSelectAll: (checked: boolean) => void;
}
@@ -36,9 +36,7 @@ export default function StorageOptionsGrid({
{ key: 'serviceWorkers', isCount: true },
];
// 2. 处理全选栏点击事件:包裹整个栏变成超级热区
const handleToggleAll = () => {
// 如果当前已经是全选,点击则取消全选;否则,点击就是全选
onSelectAll(!allSelected);
};
+1 -1
View File
@@ -1,4 +1,4 @@
import { Loader2 } from 'lucide-react'; // 引入标准的高级阻尼 Spinner 图标
import { Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import StorageCleanerConfirm from '@/pages/StorageCleaner/StorageCleanerConfirm';
import { useStorageCleaner } from './useStorageCleaner';
@@ -18,7 +18,7 @@ import {
} from '@/utils/storageCleaner';
import { MessageAction, sendMessage } from '@/utils/messages';
import { useI18n } from '@/utils/chromeI18n';
import { toast } from 'sonner'; // 1. 直接引用 shadcn 推荐的 Sonner 单例通知,踢出回调依赖
import { toast } from 'sonner';
const DEFAULT_OPTIONS: StorageCleanerOptions = {
localStorage: true,
-1
View File
@@ -15,7 +15,6 @@ export default function Index() {
useContextMenuData({ featureKey: 'textStatistics', onData: handleContextMenuData });
// 实时计算统计信息,由 useMemo 拦截非必要计算
const stats = useMemo(() => getTextStats(text), [text]);
const statItems = [
+1 -3
View File
@@ -4,7 +4,7 @@ import CopyButton from '@/components/CopyButton';
import { useSnackbar } from '@/components/GlobalSnackbar';
import type { UnitType } from './constants';
import { useI18n } from '@/utils/chromeI18n';
import { cn } from '@/lib/utils'; // 引入标准的 shadcn 工具函数
import { cn } from '@/lib/utils';
interface LiveClockProps extends React.HTMLAttributes<HTMLDivElement> {
unit: UnitType;
@@ -24,12 +24,10 @@ const LiveClock = React.memo(({ unit, onUseNow, className, ...props }: LiveClock
};
});
// 始终保持外部回调指针最新
useEffect(() => {
onUseNowRef.current = onUseNow;
}, [onUseNow]);
// 2. 高频高灵敏度计时器 (200ms 刷新率)
useEffect(() => {
const tick = () => {
const rightNow = Date.now();
+3 -5
View File
@@ -4,14 +4,14 @@ import CopyButton from '@/components/CopyButton';
import type { UnitType } from './constants';
import { DATE_FORMAT } from './constants';
import { useI18n } from '@/utils/chromeI18n';
import { cn } from '@/lib/utils'; // shadcn 核心类名合并工具
import { cn } from '@/lib/utils';
interface ResultViewProps extends React.HTMLAttributes<HTMLDivElement> {
result: string;
mode: 'ts2dt' | 'dt2ts';
unit: UnitType;
zone: string;
/** 无结果时是否渲染占位(桌面端右栏使用),默认 false */
showEmptyPlaceholder?: boolean;
}
@@ -27,7 +27,6 @@ const ResultView = React.memo(
}: ResultViewProps) => {
const { t } = useI18n('timestamp');
// 严谨计算时间衍生的附加时区/相对时间状态
const extraInfo = useMemo(() => {
if (!result) return null;
const d =
@@ -44,7 +43,6 @@ const ResultView = React.memo(
};
}, [result, mode, zone, unit]);
// 1. 空状态骨架面板:优雅匹配 shadcn 的中性灰色居中占位
if (!result) {
if (!showEmptyPlaceholder) return null;
return (
@@ -95,7 +93,7 @@ const ResultView = React.memo(
<span
className={cn(
'text-xs text-foreground/90 font-medium break-all text-left sm:text-right tabular-nums',
item.isMono && 'font-mono text-[11px]', // ISO/UTC 等机器时间使用精细化等宽代码体
item.isMono && 'font-mono text-[11px]',
)}
>
{item.value}
+1 -1
View File
@@ -76,7 +76,7 @@ export default function Index() {
]}
onChange={(v) => setUnit(v as 'ms' | 's')}
size="small"
className="sm:w-auto shrink-0" // 窄屏下全宽,宽屏下自适应收缩
className="sm:w-auto shrink-0"
/>
<Select value={zone} onValueChange={(v: string) => setZone(v as typeof zone)}>
+1 -1
View File
@@ -7,7 +7,7 @@ import { useContextMenuData } from '@/utils/useContextMenuData';
export interface UseTimestampConverterReturn {
mode: 'ts2dt' | 'dt2ts';
input: string; // 统一为单一受控输入源
input: string;
unit: UnitType;
zone: ZoneType;
result: string;
-2
View File
@@ -96,7 +96,6 @@ export function RouterProvider({
visiblePagesKey = 'app/visiblePages',
pageOrderKey = 'app/pageOrder',
}: RouterProviderProps) {
// 1. Initialize state with sync snapshot
const [currentPage, setCurrentPage] = useState<PageType>(() =>
getSyncSnapshot(syncKey as string, defaultRoute, isValidPage),
);
@@ -153,7 +152,6 @@ export function RouterProvider({
}
}, [defaultRoute, syncKey, syncRoute, visiblePagesKey, pageOrderKey]);
// Load initial data from async storage on mount
useEffect(() => {
let cancelled = false;
@@ -3,7 +3,6 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { RouterProvider, useRouter } from '@/providers/RouterProvider';
import { storageUtil } from '@/utils/chromeStorage';
// Mock storageUtil
vi.mock('@/utils/chromeStorage', () => ({
storageUtil: {
get: vi.fn(),
@@ -11,7 +10,6 @@ vi.mock('@/utils/chromeStorage', () => ({
},
}));
// Helper 辅助受控组件:用于实时嗅探并映射 useRouter 上下文状态
const TestComponent = () => {
const { currentPage, navigateTo, visiblePages, pageOrder } = useRouter();
return (
@@ -31,10 +29,6 @@ describe('RouterProvider', () => {
vi.clearAllMocks();
localStorage.clear();
// 💡 1. 核心修复点:
// - 采用标准的通用大对象 globalThis 代理,彻底掐灭 TS2304 报错。
// - 物理让 chrome 空间和统一多端 browser 空间共享相同的事件监听桩,
// - 完美承接生产代码内部全新升级的 browser.storage.onChanged 大闸!
const storageOnChangedMock = {
addListener: vi.fn(),
removeListener: vi.fn(),
@@ -100,8 +94,6 @@ describe('RouterProvider', () => {
const btn = screen.getByTestId('navigate-btn');
// 💡 2. 交互进化:废除高风险的原生 btn.click(),改用 Testing Library 的标准事件投递,
// 在一帧之内顺畅闭环受控状态改流,完美抹平悬空微任务警告。
await act(async () => {
fireEvent.click(btn);
});
-12
View File
@@ -4,27 +4,15 @@ import { decodeBase64Url, parseJwt } from '@/utils/jwt';
describe('jwt utils', () => {
describe('decodeBase64Url', () => {
it('should decode standard base64url', () => {
// "test" -> "dGVzdA"
expect(decodeBase64Url('dGVzdA')).toBe('test');
});
it('should handle padding correctly', () => {
// "a" -> "YQ" (needs ==)
expect(decodeBase64Url('YQ')).toBe('a');
// "ab" -> "YWI" (needs =)
expect(decodeBase64Url('YWI')).toBe('ab');
});
it('should handle - and _ correctly', () => {
// Validating base64url specific chars
// standard base64 of binary 0xFF 0xEF is "/+8="
// base64url should be "_-8"
// Wait, let's use a simpler one.
// 0xFB 0xFF -> "+/8=" in base64, "-_8=" in base64url? No.
// + -> -
// / -> _
// let's try to encode something that results in + and /
// binary 0xFB 0xFF 0xBE -> "+/++" in base64 -> "-_--" in base64url
expect(decodeBase64Url('-_--')).toBeDefined();
});
+2 -3
View File
@@ -55,7 +55,7 @@ describe('useStorageState', () => {
await waitFor(() => {
// 异步加载后应覆盖为存储值
expect(result.current[0]).toBe(false);
expect(result.current[2]).toBe(true); // isInitialized
expect(result.current[2]).toBe(true);
});
});
@@ -67,7 +67,7 @@ describe('useStorageState', () => {
const { result } = renderHook(() => useStorageState('qrCode/urlExpanded', true));
await waitFor(() => {
expect(result.current[2]).toBe(true); // isInitialized
expect(result.current[2]).toBe(true);
});
await act(async () => {
@@ -77,7 +77,6 @@ describe('useStorageState', () => {
expect(result.current[0]).toBe(false);
expect(storageUtil.set).toHaveBeenCalledWith('qrCode/urlExpanded', false);
// 应同时写入 localStorage 快照
expect(localStorage.getItem('snapshot/qrCode/urlExpanded')).toBe(JSON.stringify(false));
});
-4
View File
@@ -59,13 +59,9 @@ export async function ensureContentScriptInjected(): Promise<boolean> {
const tab = await getActiveTab();
if (!tab?.id) return false;
// 尝试发送一个简单的探测消息
try {
// 这里可以根据实际情况发送一个简单的 Ping 消息
// 目前暂时保留原有注入逻辑,由调用方决定
return true;
} catch (e) {
// 如果报错,说明没注入,执行注入
console.log('内容脚本未注入,尝试注入...');
console.error('注入内容脚本失败:', e);
await chrome.scripting.executeScript({
-4
View File
@@ -12,9 +12,6 @@ export interface QrCodeParseResult {
*/
export async function parseQrCodeFromFile(file: File): Promise<QrCodeParseResult> {
try {
// qr-scanner 的 scanImage 方法支持直接传入 File 对象
// 它会自动处理图片加载、Canvas 绘制和解析过程
// 并且在支持的浏览器中会优先使用原生的 BarcodeDetector API
const result = await QrScanner.scanImage(file, {
returnDetailedScanResult: true,
});
@@ -25,7 +22,6 @@ export async function parseQrCodeFromFile(file: File): Promise<QrCodeParseResult
return { success: false, error: '未检测到二维码' };
}
} catch (err) {
// qr-scanner 在未发现二维码时会抛出 "No QR code found"
const errorMsg =
err === 'No QR code found'
? '未检测到二维码'
+1 -4
View File
@@ -111,8 +111,6 @@ export async function getOriginStorageEstimate(tabId: number): Promise<number> {
target: { tabId },
func: async () => {
try {
// 注意:navigator.storage.estimate() 返回的是整个 Origin 的估算值
// 包含 IndexedDB, CacheStorage, ServiceWorker 注册等
if (navigator.storage && navigator.storage.estimate) {
const estimate = await navigator.storage.estimate();
return estimate.usage || 0;
@@ -138,7 +136,7 @@ export async function getCacheStorageSize(tabId: number): Promise<number> {
try {
if ('caches' in window) {
const keys = await caches.keys();
return keys.length; // 对于 CacheStorage,我们先返回缓存库的数量
return keys.length;
}
return 0;
} catch {
@@ -146,7 +144,6 @@ export async function getCacheStorageSize(tabId: number): Promise<number> {
}
},
});
// 由于获取具体字节数较慢,这里返回的是缓存条目的数量标识,UI 上可以特殊处理
return (result?.result as number) || 0;
} catch (error) {
console.error('Failed to get CacheStorage size:', error);