diff --git a/AGENTS.md b/AGENTS.md
index 34bd6ef..28d1b5e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -143,6 +143,7 @@ src/pages/FeatureName/
- 图标: 使用 `lucide-react` 图标库
- 格式: Prettier (`.prettierrc`: 100 字符宽, 单引号, 尾逗号 all, LF 换行)
- ESLint 使用 `typescript-eslint` 的 `projectService: true`(无需手动维护 project 路径)
+- **Git Commit**: 必须使用中文描述,遵循 Conventional Commits 规范(如 `fix(组件名): 描述`、`feat(功能名): 描述`)
## 关键外部库(非显而易见的)
diff --git a/public/README.md b/public/README.md
index b7b3f2f..8de0398 100644
--- a/public/README.md
+++ b/public/README.md
@@ -12,7 +12,6 @@
| `icon/48.png` | 48×48 图标(扩展管理页) |
| `icon/96.png` | 96×96 图标 |
| `icon/128.png` | 128×128 图标(Chrome Web Store) |
-| `wxt.svg` | WXT 框架标志 SVG 图标 |
## 注意事项
diff --git a/public/icon/128.png b/public/icon/128.png
index 4d9a614..8b7b566 100644
Binary files a/public/icon/128.png and b/public/icon/128.png differ
diff --git a/public/icon/16.png b/public/icon/16.png
index d6bc0e0..d3f0447 100644
Binary files a/public/icon/16.png and b/public/icon/16.png differ
diff --git a/public/icon/32.png b/public/icon/32.png
index 1533158..1de5c73 100644
Binary files a/public/icon/32.png and b/public/icon/32.png differ
diff --git a/public/icon/48.png b/public/icon/48.png
index d4ef488..86c73a9 100644
Binary files a/public/icon/48.png and b/public/icon/48.png differ
diff --git a/public/icon/96.png b/public/icon/96.png
index 1beb67c..a53fd4d 100644
Binary files a/public/icon/96.png and b/public/icon/96.png differ
diff --git a/public/wxt.svg b/public/wxt.svg
deleted file mode 100644
index d4f3acb..0000000
--- a/public/wxt.svg
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
-
-
diff --git a/src/components/__tests__/CopyButton.test.tsx b/src/components/__tests__/CopyButton.test.tsx
new file mode 100644
index 0000000..64af285
--- /dev/null
+++ b/src/components/__tests__/CopyButton.test.tsx
@@ -0,0 +1,194 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { act, render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+// unmock the globally-mocked component so we test the real implementation
+vi.unmock('@/components/CopyButton');
+
+vi.mock('@/utils/clipboard', () => ({
+ copyTextToClipboard: vi.fn(),
+}));
+
+vi.mock('sonner', () => ({
+ toast: {
+ success: vi.fn(),
+ error: vi.fn(),
+ },
+}));
+
+import { CopyButton } from '@/components/CopyButton';
+import { copyTextToClipboard } from '@/utils/clipboard';
+import { toast } from 'sonner';
+
+const mockedCopy = vi.mocked(copyTextToClipboard);
+const mockedToast = vi.mocked(toast);
+
+describe('CopyButton', () => {
+ beforeEach(() => {
+ vi.useFakeTimers({ shouldAdvanceTime: true });
+ vi.clearAllMocks();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it('复制成功时调用 copyTextToClipboard 并传入正确 text', async () => {
+ mockedCopy.mockResolvedValue(true);
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
+
+ render();
+
+ await user.click(screen.getByRole('button'));
+
+ expect(mockedCopy).toHaveBeenCalledWith('hello world');
+ expect(mockedToast.success).toHaveBeenCalled();
+ });
+
+ it('复制成功后图标切换为 Check,1.5 秒后恢复', async () => {
+ mockedCopy.mockResolvedValue(true);
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
+
+ render();
+
+ // 点击后复制成功,按钮获得 emerald 样式(说明切到了 Check 状态)
+ await user.click(screen.getByRole('button'));
+
+ await waitFor(() => {
+ expect(screen.getByRole('button').className).toContain('text-emerald');
+ });
+
+ // 1.5 秒后样式恢复
+ act(() => {
+ vi.advanceTimersByTime(1500);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByRole('button').className).not.toContain('text-emerald');
+ });
+ });
+
+ it('复制空文本时弹出 error toast', async () => {
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
+
+ render();
+
+ await user.click(screen.getByRole('button'));
+
+ expect(mockedCopy).not.toHaveBeenCalled();
+ expect(mockedToast.error).toHaveBeenCalled();
+ });
+
+ it('复制失败时弹出 error toast', async () => {
+ mockedCopy.mockResolvedValue(false);
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
+
+ render();
+
+ await user.click(screen.getByRole('button'));
+
+ expect(mockedCopy).toHaveBeenCalledWith('something');
+ expect(mockedToast.error).toHaveBeenCalled();
+ });
+
+ // ==================== 新增测试 ====================
+
+ it('初始渲染时显示 Copy 图标且无 emerald 样式', () => {
+ render();
+
+ const button = screen.getByRole('button');
+ expect(button.className).not.toContain('text-emerald');
+ // 通过 aria-label 确认按钮存在,图标由 lucide 渲染为 svg
+ expect(button).toHaveAttribute('aria-label');
+ });
+
+ it('自定义 tooltip 会覆盖默认 title 和 aria-label', () => {
+ render();
+
+ const button = screen.getByRole('button');
+ expect(button).toHaveAttribute('title', '自定义提示');
+ expect(button).toHaveAttribute('aria-label', '自定义提示');
+ });
+
+ it('className 被正确透传到按钮', () => {
+ render();
+
+ const button = screen.getByRole('button');
+ expect(button.className).toContain('my-custom-class');
+ });
+
+ it('点击事件阻止冒泡', async () => {
+ mockedCopy.mockResolvedValue(true);
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
+ const parentClick = vi.fn();
+
+ render(
+
+
+
,
+ );
+
+ await user.click(screen.getByRole('button'));
+
+ expect(mockedCopy).toHaveBeenCalled();
+ expect(parentClick).not.toHaveBeenCalled();
+ });
+
+ it('组件卸载时清除定时器,不触发状态更新警告', async () => {
+ mockedCopy.mockResolvedValue(true);
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
+
+ const { unmount } = render();
+
+ await user.click(screen.getByRole('button'));
+
+ // 在 1.5 秒超时到期前卸载组件
+ act(() => {
+ vi.advanceTimersByTime(500);
+ });
+
+ // 卸载不应抛出 "Can't perform a React state update on an unmounted component" 警告
+ expect(() => unmount()).not.toThrow();
+
+ // 前进剩余时间,确认没有异常
+ act(() => {
+ vi.advanceTimersByTime(2000);
+ });
+ });
+
+ it('快速连续点击不会创建多个重叠定时器', async () => {
+ mockedCopy.mockResolvedValue(true);
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
+
+ render();
+
+ const button = screen.getByRole('button');
+
+ // 快速点击 3 次
+ await user.click(button);
+ await user.click(button);
+ await user.click(button);
+
+ // copyTextToClipboard 应该被调用 3 次(每次点击都执行)
+ expect(mockedCopy).toHaveBeenCalledTimes(3);
+
+ // 但 setTimeout 相关的 clearTimeout + setTimeout 组合应正常工作
+ // advance 1.5 秒后,copied 状态应恢复为 false
+ act(() => {
+ vi.advanceTimersByTime(1500);
+ });
+
+ await waitFor(() => {
+ expect(button.className).not.toContain('text-emerald');
+ });
+ });
+
+ it('其他 button props 通过 ...props 透传', () => {
+ render();
+
+ const button = screen.getByRole('button');
+ expect(button).toHaveAttribute('data-testid', 'copy-btn');
+ expect(button).toBeDisabled();
+ expect(button).toHaveAttribute('id', 'copy-button-id');
+ });
+});
diff --git a/src/components/__tests__/ToolCard.test.tsx b/src/components/__tests__/ToolCard.test.tsx
deleted file mode 100644
index bf3da2e..0000000
--- a/src/components/__tests__/ToolCard.test.tsx
+++ /dev/null
@@ -1,118 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest';
-import { act, fireEvent, render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import ToolCard from '@/pages/Dashboard/ToolCard';
-import { Clock } from 'lucide-react';
-
-describe('ToolCard 组件', () => {
- beforeEach(() => {
- vi.clearAllMocks();
- });
-
- describe('渲染测试', () => {
- it('应渲染标题和描述', () => {
- render(
- {}}
- />,
- );
-
- expect(screen.getByText('测试工具')).toBeInTheDocument();
- expect(screen.getByText('这是一个测试工具')).toBeInTheDocument();
- });
-
- it('无描述时仅渲染标题', () => {
- render( {}} />);
-
- expect(screen.getByText('仅标题')).toBeInTheDocument();
- });
-
- it('应渲染图标', () => {
- const { container } = render(
- {}} />,
- );
-
- const svgElement = container.querySelector('svg');
- expect(svgElement).toBeInTheDocument();
- });
-
- it('提供快照内容时应渲染快照', () => {
- render(
- {}}
- snapshot={快照内容
}
- />,
- );
-
- expect(screen.getByTestId('snapshot')).toBeInTheDocument();
- });
-
- it('未提供快照时不渲染快照区域', () => {
- const { container } = render(
- {}}
- onNavigate={function (): void {
- throw new Error('Function not implemented.');
- }}
- />,
- );
-
- expect(container.querySelector('[data-testid="snapshot"]')).not.toBeInTheDocument();
- });
-
- it('应使用 CardActionArea 渲染,支持键盘聚焦', () => {
- render( {}} />);
-
- const button = screen.getByRole('button', { name: /可聚焦/ });
- expect(button).toBeInTheDocument();
- });
- });
-
- describe('交互测试', () => {
- it('点击时应调用 onClick', () => {
- const handleClick = vi.fn();
- render();
-
- const button = screen.getByRole('button', { name: /可点击/ });
- fireEvent.click(button);
-
- expect(handleClick).toHaveBeenCalledTimes(1);
- });
-
- it('按 Enter 键时应调用 onClick', async () => {
- const handleClick = vi.fn();
- render(
- ,
- );
-
- const button = screen.getByRole('button', { name: /键盘可触发/ });
- await act(async () => {
- button.focus();
- await userEvent.keyboard('{Enter}');
- });
-
- expect(handleClick).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('样式测试', () => {
- it('应应用自定义颜色代码', () => {
- const { container } = render(
- {}} />,
- );
-
- const svgElement = container.querySelector('svg');
- expect(svgElement).toBeInTheDocument();
- });
- });
-});
diff --git a/src/entrypoints/content/uiPopover.ts b/src/entrypoints/content/uiPopover.ts
index 1a6bafb..faf985e 100644
--- a/src/entrypoints/content/uiPopover.ts
+++ b/src/entrypoints/content/uiPopover.ts
@@ -161,7 +161,7 @@ function positionPopover(popover: HTMLElement, x: number, y: number): void {
let hideTimeout: ReturnType | null = null;
-export function showPopover(
+function showPopover(
x: number,
y: number,
contentHtml: string,
diff --git a/src/pages/Dashboard/ToolCard.tsx b/src/pages/Dashboard/ToolCard.tsx
deleted file mode 100644
index e8f6379..0000000
--- a/src/pages/Dashboard/ToolCard.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import type { ComponentType } from 'react';
-import React from 'react';
-import type { LucideProps } from 'lucide-react';
-import { ChevronRight } from 'lucide-react';
-import type { PaletteColorKey } from '@/config/features';
-import { cn } from '@/lib/utils';
-
-const PALETTE_COLORS: Record = {
- primary: '13, 148, 136', // teal
- success: '22, 163, 74', // green
- warning: '217, 119, 6', // amber (存储清理的橙色轴)
- error: '220, 38, 38', // red
- secondary: '147, 51, 232',
- info: '37, 99, 235', // blue
-};
-
-export interface ToolCardProps extends React.HTMLAttributes {
- title: string;
- description?: string;
- snapshot?: React.ReactNode;
- colorKey: PaletteColorKey;
- icon: ComponentType;
- onNavigate: () => void;
-}
-
-export default function ToolCard({
- title,
- description,
- snapshot,
- colorKey,
- icon: IconComponent,
- onNavigate,
- className,
- ...props
-}: ToolCardProps) {
- const rgbValues = PALETTE_COLORS[colorKey];
-
- return (
-
- {/* 上半部分:核心信息交互排版轴 */}
-
-
- {/* 左侧圆形图标容器 */}
-
-
-
-
- {/* 中间文字描述区:利用 flex-1 min-w-0 防御文本过长发生恶性撑开 */}
-
-
- {title}
-
- {description && (
-
- {description}
-
- )}
-
-
-
- {/* 右侧指示小箭头 */}
-
-
-
-
- {/* 覆盖整个上半部分的绝对定位隐形跳转层(A11y 无障碍标准合规) */}
-
-
-
- {/* 下半部分:未来的动态预览沙箱独立承载区 */}
- {snapshot != null && (
-
- {snapshot}
-
- )}
-
- );
-}
-
-ToolCard.displayName = 'ToolCard';
diff --git a/src/utils/__tests__/chromeTabs.test.ts b/src/utils/__tests__/chromeTabs.test.ts
index c992863..b9aabd6 100644
--- a/src/utils/__tests__/chromeTabs.test.ts
+++ b/src/utils/__tests__/chromeTabs.test.ts
@@ -1,101 +1,7 @@
import { describe, expect, it, vi } from 'vitest';
-import {
- getActiveTab,
- getActiveTabDomain,
- openExtensionPage,
- ensureContentScriptInjected,
-} from '@/utils/chromeTabs';
+import { openExtensionPage } from '@/utils/chromeTabs';
describe('chromeTabs', () => {
- describe('getActiveTab', () => {
- it('应该返回当前活动标签页', async () => {
- const mockTab = { id: 1, url: 'https://example.com', title: 'Example' } as chrome.tabs.Tab;
- (chrome.tabs.query as any).mockResolvedValue([mockTab]);
-
- const result = await getActiveTab();
-
- expect(result).toEqual(mockTab);
- expect(chrome.tabs.query).toHaveBeenCalledWith({ active: true, currentWindow: true });
- });
-
- it('当没有活动标签页时应返回 null', async () => {
- (chrome.tabs.query as any).mockResolvedValue([]);
-
- const result = await getActiveTab();
-
- expect(result).toBeNull();
- });
-
- it('当查询失败时应返回 null 并记录错误', async () => {
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
- (chrome.tabs.query as any).mockRejectedValue(new Error('Permission denied'));
-
- const result = await getActiveTab();
-
- expect(result).toBeNull();
- expect(consoleSpy).toHaveBeenCalledWith('获取活动标签页失败:', expect.any(Error));
- consoleSpy.mockRestore();
- });
- });
-
- describe('getActiveTabDomain', () => {
- it('应该返回当前活动标签页的域名', async () => {
- const mockTab = { id: 1, url: 'https://example.com/path?query=1' } as chrome.tabs.Tab;
- (chrome.tabs.query as any).mockResolvedValue([mockTab]);
-
- const result = await getActiveTabDomain();
-
- expect(result).toBe('example.com');
- });
-
- it('应该处理带有端口的 URL', async () => {
- const mockTab = { id: 1, url: 'https://example.com:8080/path' } as chrome.tabs.Tab;
- (chrome.tabs.query as any).mockResolvedValue([mockTab]);
-
- const result = await getActiveTabDomain();
-
- expect(result).toBe('example.com');
- });
-
- it('当标签页没有 URL 时应返回空字符串', async () => {
- const mockTab = { id: 1 } as chrome.tabs.Tab;
- (chrome.tabs.query as any).mockResolvedValue([mockTab]);
-
- const result = await getActiveTabDomain();
-
- expect(result).toBe('');
- });
-
- it('当没有活动标签页时应返回空字符串', async () => {
- (chrome.tabs.query as any).mockResolvedValue([]);
-
- const result = await getActiveTabDomain();
-
- expect(result).toBe('');
- });
-
- it('当 URL 解析失败时应返回空字符串并记录错误', async () => {
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
- const mockTab = { id: 1, url: 'not-a-valid-url' } as chrome.tabs.Tab;
- (chrome.tabs.query as any).mockResolvedValue([mockTab]);
-
- const result = await getActiveTabDomain();
-
- expect(result).toBe('');
- expect(consoleSpy).toHaveBeenCalledWith('解析域名失败:', expect.any(Error));
- consoleSpy.mockRestore();
- });
-
- it('应该处理 chrome-extension URL', async () => {
- const mockTab = { id: 1, url: 'chrome-extension://abc123/popup.html' } as chrome.tabs.Tab;
- (chrome.tabs.query as any).mockResolvedValue([mockTab]);
-
- const result = await getActiveTabDomain();
-
- expect(result).toBe('abc123');
- });
- });
-
describe('openExtensionPage', () => {
it('应该在新标签页中打开扩展页面', async () => {
await openExtensionPage('popup.html');
@@ -116,44 +22,4 @@ describe('chromeTabs', () => {
consoleSpy.mockRestore();
});
});
-
- describe('ensureContentScriptInjected', () => {
- it('当存在活动标签页时应返回 true', async () => {
- const mockTab = { id: 123, url: 'https://example.com' } as chrome.tabs.Tab;
- (chrome.tabs.query as any).mockResolvedValue([mockTab]);
-
- const result = await ensureContentScriptInjected();
-
- expect(result).toBe(true);
- });
-
- it('当没有活动标签页时应返回 false', async () => {
- (chrome.tabs.query as any).mockResolvedValue([]);
-
- const result = await ensureContentScriptInjected();
-
- expect(result).toBe(false);
- });
-
- it('当标签页没有 id 时应返回 false', async () => {
- const mockTab = { url: 'https://example.com' } as chrome.tabs.Tab;
- (chrome.tabs.query as any).mockResolvedValue([mockTab]);
-
- const result = await ensureContentScriptInjected();
-
- expect(result).toBe(false);
- });
-
- it('当整体操作失败时应返回 false 并记录错误', async () => {
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
- (chrome.tabs.query as any).mockRejectedValue(new Error('Query failed'));
-
- const result = await ensureContentScriptInjected();
-
- expect(result).toBe(false);
- // getActiveTab catches the error and logs "获取活动标签页失败"
- expect(consoleSpy).toHaveBeenCalledWith('获取活动标签页失败:', expect.any(Error));
- consoleSpy.mockRestore();
- });
- });
});
diff --git a/src/utils/__tests__/clipboard.test.ts b/src/utils/__tests__/clipboard.test.ts
index 568cb14..5aedb84 100644
--- a/src/utils/__tests__/clipboard.test.ts
+++ b/src/utils/__tests__/clipboard.test.ts
@@ -1,14 +1,5 @@
-import { describe, expect, it, vi, beforeAll } from 'vitest';
-import { copyTextToClipboard, copyImageToClipboard } from '@/utils/clipboard';
-
-// Mock ClipboardItem for test environment
-class MockClipboardItem {
- constructor(public items: Record) {}
-}
-
-beforeAll(() => {
- (globalThis as any).ClipboardItem = MockClipboardItem;
-});
+import { describe, expect, it, vi } from 'vitest';
+import { copyTextToClipboard } from '@/utils/clipboard';
describe('clipboard', () => {
describe('copyTextToClipboard', () => {
@@ -31,27 +22,4 @@ describe('clipboard', () => {
expect(result).toBe(false);
});
});
-
- describe('copyImageToClipboard', () => {
- it('复制成功时应返回 true', async () => {
- const write = vi.fn().mockResolvedValue(undefined);
- Object.assign(navigator, { clipboard: { write } });
-
- const blob = new Blob(['png data'], { type: 'image/png' });
- const result = await copyImageToClipboard(blob);
-
- expect(result).toBe(true);
- expect(write).toHaveBeenCalledTimes(1);
- });
-
- it('复制失败时应返回 false', async () => {
- const write = vi.fn().mockRejectedValue(new Error('Permission denied'));
- Object.assign(navigator, { clipboard: { write } });
-
- const blob = new Blob(['png data'], { type: 'image/png' });
- const result = await copyImageToClipboard(blob);
-
- expect(result).toBe(false);
- });
- });
});
diff --git a/src/utils/__tests__/useContextMenuData.test.ts b/src/utils/__tests__/useContextMenuData.test.ts
index e0c9b97..01b2818 100644
--- a/src/utils/__tests__/useContextMenuData.test.ts
+++ b/src/utils/__tests__/useContextMenuData.test.ts
@@ -1,10 +1,6 @@
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
-import {
- useContextMenuData,
- saveContextMenuData,
- clearContextMenuData,
-} from '@/utils/useContextMenuData';
+import { useContextMenuData, saveContextMenuData } from '@/utils/useContextMenuData';
describe('useContextMenuData', () => {
beforeEach(() => {
@@ -48,14 +44,6 @@ describe('useContextMenuData', () => {
});
});
- describe('clearContextMenuData', () => {
- it('应该从 storage 中删除数据', async () => {
- await clearContextMenuData();
-
- expect(chrome.storage.local.remove).toHaveBeenCalledWith(['contextMenu/pendingData']);
- });
- });
-
describe('useContextMenuData Hook', () => {
it('当 storage 中有匹配数据时应调用 onData 回调', async () => {
const mockData = {
diff --git a/src/utils/base64Converter.ts b/src/utils/base64Converter.ts
index 54ee1f4..494f3b5 100644
--- a/src/utils/base64Converter.ts
+++ b/src/utils/base64Converter.ts
@@ -8,7 +8,7 @@ import { formatBytes } from './format';
export const MAX_FILE_SIZE = 10 * 1024 * 1024;
/** 支持的图像 MIME 类型 */
-export const SUPPORTED_IMAGE_TYPES = [
+const SUPPORTED_IMAGE_TYPES = [
'image/png',
'image/jpeg',
'image/jpg',
@@ -20,7 +20,7 @@ export const SUPPORTED_IMAGE_TYPES = [
] as const;
/** 支持的图像文件扩展名 */
-export const SUPPORTED_IMAGE_EXTENSIONS = [
+const SUPPORTED_IMAGE_EXTENSIONS = [
'.png',
'.jpg',
'.jpeg',
@@ -34,7 +34,7 @@ export const SUPPORTED_IMAGE_EXTENSIONS = [
/**
* 文本转 Base64 编码结果
*/
-export interface TextToBase64Result {
+interface TextToBase64Result {
/** Base64 编码结果 */
output: string;
/** 原始字节数 */
@@ -226,7 +226,7 @@ export function formatFileSize(bytes: number): string {
/**
* Base64 解码为二进制后的产物
*/
-export interface Base64ToBlobResult {
+interface Base64ToBlobResult {
/** 解码后的 Blob */
blob: Blob;
/** 推断出的 MIME 类型 */
diff --git a/src/utils/chromeI18n.ts b/src/utils/chromeI18n.ts
index e172fd5..3442b96 100644
--- a/src/utils/chromeI18n.ts
+++ b/src/utils/chromeI18n.ts
@@ -66,11 +66,3 @@ export function useI18n(namespace?: string | string[]) {
isLoaded: true,
};
}
-
-/**
- * 预加载命名空间(无操作,兼容 useLazyTranslation)
- */
-export async function preloadNamespaces(_namespaces: string[]): Promise {
- // chrome.i18n 是同步的,无需预加载
- return Promise.resolve();
-}
diff --git a/src/utils/chromeTabs.ts b/src/utils/chromeTabs.ts
index bf892f5..451a085 100644
--- a/src/utils/chromeTabs.ts
+++ b/src/utils/chromeTabs.ts
@@ -2,35 +2,6 @@
* Chrome 标签页相关工具函数
*/
-/**
- * 获取当前活动的标签页
- */
-export async function getActiveTab(): Promise {
- try {
- const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
- return tab || null;
- } catch (error) {
- console.error('获取活动标签页失败:', error);
- return null;
- }
-}
-
-/**
- * 获取当前活动的标签页域名
- */
-export async function getActiveTabDomain(): Promise {
- const tab = await getActiveTab();
- if (tab?.url) {
- try {
- const url = new URL(tab.url);
- return url.hostname;
- } catch (e) {
- console.error('解析域名失败:', e);
- }
- }
- return '';
-}
-
/**
* 在新标签页中打开扩展页面
* @param page - 扩展页面路径(如 'popup.html')
@@ -50,28 +21,3 @@ export async function openExtensionPage(
console.error('打开扩展页面失败:', error);
}
}
-
-/**
- * 确保内容脚本已注入
- */
-export async function ensureContentScriptInjected(): Promise {
- try {
- const tab = await getActiveTab();
- if (!tab?.id) return false;
-
- try {
- return true;
- } catch (e) {
- console.log('内容脚本未注入,尝试注入...');
- console.error('注入内容脚本失败:', e);
- await chrome.scripting.executeScript({
- target: { tabId: tab.id },
- files: ['/content-scripts/content.js'],
- });
- return true;
- }
- } catch (error) {
- console.error('注入内容脚本失败:', error);
- return false;
- }
-}
diff --git a/src/utils/clipboard.ts b/src/utils/clipboard.ts
index 0ea3a3e..d6605a5 100644
--- a/src/utils/clipboard.ts
+++ b/src/utils/clipboard.ts
@@ -11,21 +11,3 @@ export async function copyTextToClipboard(text: string): Promise {
return false;
}
}
-
-/**
- * 复制图片到剪贴板
- * @param blob 要复制的图片
- * @returns Promise 是否复制成功
- */
-export async function copyImageToClipboard(blob: Blob): Promise {
- try {
- await navigator.clipboard.write([
- new ClipboardItem({
- 'image/png': blob,
- }),
- ]);
- return true;
- } catch {
- return false;
- }
-}
diff --git a/src/utils/contextMenu.ts b/src/utils/contextMenu.ts
index eac11b9..3269530 100644
--- a/src/utils/contextMenu.ts
+++ b/src/utils/contextMenu.ts
@@ -1,18 +1,18 @@
import type { PageType } from '@/types/storage';
-export interface ContextMenuItemConfig {
+interface ContextMenuItemConfig {
id: string;
title: string;
contexts: [`${chrome.contextMenus.ContextType}`, ...`${chrome.contextMenus.ContextType}`[]];
parentId?: string;
}
-export interface ContextMenuClickedInfo {
+interface ContextMenuClickedInfo {
featureKey: PageType;
payload: string;
}
-export interface ParseResult {
+interface ParseResult {
success: boolean;
data?: ContextMenuClickedInfo;
error?: string;
diff --git a/src/utils/jsonFormatter.ts b/src/utils/jsonFormatter.ts
index 9ea3b68..ff6c14b 100644
--- a/src/utils/jsonFormatter.ts
+++ b/src/utils/jsonFormatter.ts
@@ -71,7 +71,7 @@ export function formatJson(text: string, options: JsonFormatOptions): JsonFormat
/**
* JSON 压缩结果
*/
-export interface JsonMinifyResult {
+interface JsonMinifyResult {
/** 压缩后的 JSON 字符串 */
minified: string;
/** 原始输入的字节大小 */
diff --git a/src/utils/jsonToToml.ts b/src/utils/jsonToToml.ts
index e810a1e..78e24d3 100644
--- a/src/utils/jsonToToml.ts
+++ b/src/utils/jsonToToml.ts
@@ -1,7 +1,7 @@
/**
* JSON 转 TOML 转换结果
*/
-export interface JsonToTomlResult {
+interface JsonToTomlResult {
/** 转换后的 TOML 字符串 */
output: string;
/** 原始输入的字节大小 */
diff --git a/src/utils/jsonToYaml.ts b/src/utils/jsonToYaml.ts
index 546feb0..9920023 100644
--- a/src/utils/jsonToYaml.ts
+++ b/src/utils/jsonToYaml.ts
@@ -1,7 +1,7 @@
/**
* JSON 转 YAML 转换结果
*/
-export interface JsonToYamlResult {
+interface JsonToYamlResult {
/** 转换后的 YAML 字符串 */
output: string;
/** 原始输入的字节大小 */
diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts
index bb9f677..78d56bb 100644
--- a/src/utils/jwt.ts
+++ b/src/utils/jwt.ts
@@ -4,13 +4,13 @@
import { getMessage } from '@/utils/chromeI18n';
-export interface JwtHeader {
+interface JwtHeader {
alg: string;
typ?: string;
[key: string]: unknown;
}
-export interface JwtPayload {
+interface JwtPayload {
iss?: string;
sub?: string;
aud?: string | string[];
diff --git a/src/utils/qrCodeParser.ts b/src/utils/qrCodeParser.ts
index 946f8ba..63ef41c 100644
--- a/src/utils/qrCodeParser.ts
+++ b/src/utils/qrCodeParser.ts
@@ -1,6 +1,6 @@
import QrScanner from 'qr-scanner';
-export interface QrCodeParseResult {
+interface QrCodeParseResult {
success: boolean;
data?: string;
error?: string;
diff --git a/src/utils/storageCleaner.ts b/src/utils/storageCleaner.ts
index 1238e07..65ba10f 100644
--- a/src/utils/storageCleaner.ts
+++ b/src/utils/storageCleaner.ts
@@ -224,7 +224,7 @@ async function runCleanScript(
return { success: false, error: 'No result returned' };
}
-export async function injectClearLocalStorage(tabId: number): Promise {
+async function injectClearLocalStorage(tabId: number): Promise {
return runCleanScript(
tabId,
() => {
@@ -236,7 +236,7 @@ export async function injectClearLocalStorage(tabId: number): Promise {
+async function injectClearSessionStorage(tabId: number): Promise {
return runCleanScript(
tabId,
() => {
@@ -248,7 +248,7 @@ export async function injectClearSessionStorage(tabId: number): Promise {
+async function injectClearIndexedDB(tabId: number): Promise {
return runCleanScript(
tabId,
async () => {
@@ -292,7 +292,7 @@ export async function injectClearIndexedDB(tabId: number): Promise {
+async function injectClearCacheStorage(tabId: number): Promise {
return runCleanScript(
tabId,
async () => {
@@ -309,7 +309,7 @@ export async function injectClearCacheStorage(tabId: number): Promise {
+async function injectUnregisterServiceWorkers(tabId: number): Promise {
return runCleanScript(
tabId,
async () => {
@@ -381,10 +381,3 @@ export function formatCleaningResult(
return t('storageCleaner:cleanedSummary', { items: parts.join(', ') });
}
-
-export function isEmptyResult(result: CleaningResult): boolean {
- const values = Object.values(result).filter(
- (r): r is StorageCleanResult => r?.success === true && r.count > 0,
- );
- return values.length === 0;
-}
diff --git a/src/utils/useContextMenuData.ts b/src/utils/useContextMenuData.ts
index 8c2b1c1..ecc7ea2 100644
--- a/src/utils/useContextMenuData.ts
+++ b/src/utils/useContextMenuData.ts
@@ -90,10 +90,3 @@ export async function saveContextMenuData(
};
await storageUtil.set(STORAGE_KEY, pendingData);
}
-
-/**
- * 清除右键菜单待处理数据
- */
-export async function clearContextMenuData(): Promise {
- await storageUtil.remove(STORAGE_KEY);
-}
diff --git a/src/utils/useDebounce.ts b/src/utils/useDebounce.ts
deleted file mode 100644
index e9a70ac..0000000
--- a/src/utils/useDebounce.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { useState, useEffect } from 'react';
-
-/**
- * useDebounce Hook - 防抖值
- *
- * @param value - 需要防抖的值
- * @param delay - 延迟时间(毫秒)
- * @returns 防抖后的值
- */
-export function useDebounce(value: T, delay: number): T {
- const [debouncedValue, setDebouncedValue] = useState(value);
-
- useEffect(() => {
- const timer = setTimeout(() => {
- setDebouncedValue(value);
- }, delay);
-
- return () => {
- clearTimeout(timer);
- };
- }, [value, delay]);
-
- return debouncedValue;
-}
diff --git a/wxt.config.ts b/wxt.config.ts
index fd671cd..9bbbf6b 100644
--- a/wxt.config.ts
+++ b/wxt.config.ts
@@ -58,8 +58,20 @@ export default defineConfig({
'contextMenus',
],
host_permissions: [''],
+ icons: {
+ '16': 'icon/16.png',
+ '32': 'icon/32.png',
+ '48': 'icon/48.png',
+ '96': 'icon/96.png',
+ '128': 'icon/128.png',
+ },
action: {
default_title: '__MSG_appName__',
+ default_icon: {
+ '16': 'icon/16.png',
+ '32': 'icon/32.png',
+ '48': 'icon/48.png',
+ },
},
side_panel: {
default_path: 'entrypoints/sidepanel/index.html',