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',