From e263b63ced939d4dfb9adc4e8ff43b6867be8aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E9=9C=96=E9=93=83?= Date: Wed, 27 May 2026 16:56:27 +0800 Subject: [PATCH] =?UTF-8?q?fix(pages):=20=E4=BF=AE=E5=A4=8D=20TypeScript?= =?UTF-8?q?=20=E9=94=99=E8=AF=AF=E3=80=81=E7=A1=AC=E7=BC=96=E7=A0=81?= =?UTF-8?q?=E5=AD=97=E7=AC=A6=E4=B8=B2=EF=BC=8C=E7=A7=BB=E9=99=A4=E6=AD=BB?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 LiveClock.tsx CopyButton size 属性类型错误 (small → sm) - 修复 ToolCard.tsx 紫色 RGB 值拼写错误 (147,51,2 purple → 147,51,232) - 替换 4 处硬编码中文为 i18n 调用 (StorageCleaner, JsonTools) - MarkdownToHtml 预览链接色改为 CSS 变量以支持暗黑模式 - 移除 Base64Converter 中已被 Base64ConverterSection 替代的死代码 (FileMode.tsx, ImageMode.tsx 及其测试文件) - 更新 README.md 移除对已删除文件的引用 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- i18n/locales/en/jsonDiff.json | 1 + i18n/locales/en/jsonFormat.json | 1 + i18n/locales/en/storageCleaner.json | 1 + i18n/locales/zh/jsonDiff.json | 1 + i18n/locales/zh/jsonFormat.json | 1 + i18n/locales/zh/storageCleaner.json | 1 + pages/Base64Converter/FileMode.tsx | 220 --------------- pages/Base64Converter/ImageMode.tsx | 259 ------------------ .../__tests__/FileMode.test.tsx | 235 ---------------- .../__tests__/ImageMode.test.tsx | 182 ------------ pages/Dashboard/ToolCard.tsx | 2 +- pages/JsonTools/JsonConvertSection.tsx | 2 +- pages/JsonTools/JsonFormatSection.tsx | 2 +- pages/JsonTools/index.tsx | 4 +- pages/MarkdownToHtml/index.tsx | 4 +- pages/README.md | 14 +- pages/StorageCleaner/index.tsx | 2 +- pages/Timestamp/LiveClock.tsx | 2 +- 18 files changed, 21 insertions(+), 913 deletions(-) delete mode 100644 pages/Base64Converter/FileMode.tsx delete mode 100644 pages/Base64Converter/ImageMode.tsx delete mode 100644 pages/Base64Converter/__tests__/FileMode.test.tsx delete mode 100644 pages/Base64Converter/__tests__/ImageMode.test.tsx diff --git a/i18n/locales/en/jsonDiff.json b/i18n/locales/en/jsonDiff.json index 316a062..41cb13f 100644 --- a/i18n/locales/en/jsonDiff.json +++ b/i18n/locales/en/jsonDiff.json @@ -15,6 +15,7 @@ "diffCount": "{{count}} differences", "invalidJson": "Invalid JSON format", "emptyHint": "Enter JSON on both sides and click Compare", + "fixErrorHint": "Fix the JSON syntax errors above to enable live comparison", "added": "Added", "removed": "Removed", "modified": "Modified" diff --git a/i18n/locales/en/jsonFormat.json b/i18n/locales/en/jsonFormat.json index 9b8421b..05aeb97 100644 --- a/i18n/locales/en/jsonFormat.json +++ b/i18n/locales/en/jsonFormat.json @@ -14,6 +14,7 @@ "noContent": "Nothing to copy", "invalidJson": "Invalid JSON format", "emptyHint": "Enter JSON and click Format", + "fixErrorHint": "Fix the JSON syntax errors above to enable live formatting", "originalSize": "Original size", "formattedSize": "Formatted size", "diffMode": "Diff", diff --git a/i18n/locales/en/storageCleaner.json b/i18n/locales/en/storageCleaner.json index 17d0a95..6679b99 100644 --- a/i18n/locales/en/storageCleaner.json +++ b/i18n/locales/en/storageCleaner.json @@ -2,6 +2,7 @@ "pageTitle": "Storage Cleaner", "pageSubtitle": "Clear cache, cookies, and local storage", "loading": "Loading...", + "initializing": "Reading site data...", "occupied": "Occupied {{size}}", "cleaning": "Cleaning...", "cleanNow": "Clean Now", diff --git a/i18n/locales/zh/jsonDiff.json b/i18n/locales/zh/jsonDiff.json index 6fbe180..bc10e13 100644 --- a/i18n/locales/zh/jsonDiff.json +++ b/i18n/locales/zh/jsonDiff.json @@ -15,6 +15,7 @@ "diffCount": "{{count}} 处差异", "invalidJson": "无效的 JSON 格式", "emptyHint": "输入两侧 JSON 后点击比较", + "fixErrorHint": "请修正上方 JSON 的语法错误以开启实时流式比对", "added": "新增", "removed": "删除", "modified": "修改" diff --git a/i18n/locales/zh/jsonFormat.json b/i18n/locales/zh/jsonFormat.json index d5d2f95..2f0b584 100644 --- a/i18n/locales/zh/jsonFormat.json +++ b/i18n/locales/zh/jsonFormat.json @@ -14,6 +14,7 @@ "noContent": "无内容可复制", "invalidJson": "无效的 JSON 格式", "emptyHint": "输入 JSON 后点击格式化", + "fixErrorHint": "请修正上方 JSON 的语法错误以开启实时流式格式化", "originalSize": "原始大小", "formattedSize": "格式化后大小", "diffMode": "差异比较", diff --git a/i18n/locales/zh/storageCleaner.json b/i18n/locales/zh/storageCleaner.json index d17fb7b..6265097 100644 --- a/i18n/locales/zh/storageCleaner.json +++ b/i18n/locales/zh/storageCleaner.json @@ -2,6 +2,7 @@ "pageTitle": "存储清理", "pageSubtitle": "清理缓存、Cookies 及本地存储", "loading": "加载中...", + "initializing": "正在读取站点数据...", "occupied": "已占用 {{size}}", "cleaning": "正在清理...", "cleanNow": "立即清理", diff --git a/pages/Base64Converter/FileMode.tsx b/pages/Base64Converter/FileMode.tsx deleted file mode 100644 index d40fd41..0000000 --- a/pages/Base64Converter/FileMode.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import { Trash2, Upload } from 'lucide-react'; -import TextInputArea from '@/components/TextInputArea'; -import { useLazyTranslation } from '@/utils/useLazyTranslation'; -import CopyButton from '@/components/CopyButton'; -import DecodeResultPaper from '@/components/DecodeResultPaper'; -import { Button } from '@/components/ui/button'; -import { downloadBlob, formatFileSize, MAX_FILE_SIZE } from '@/utils/base64Converter'; -import { useStorageState } from '@/utils/useStorageState'; -import type { Base64ConvertDirection } from '@/types/storage'; -import SwitchButtonGroup from '@/components/SwitchButtonGroup'; -import { useBase64Converter } from './useBase64Converter'; // 💡 斩断重复代码 -import { cn } from '@/lib/utils'; - -const isValidDirection = (val: unknown): val is Base64ConvertDirection => - val === 'encode' || val === 'decode'; - -export default function FileMode() { - const { t } = useLazyTranslation('base64Converter'); - const [direction, setDirection] = useStorageState( - 'base64Converter/fileMode/direction', - 'encode', - isValidDirection, - ); - - const { - result, - info, - isLoading, - isDragging, - setIsDragging, - fileInputRef, - encodeError, - decodeInput, - setDecodeInput, - decoded, - decodeError, - decodedFileName, - setCustomFileName, - resetAll, - safeFileSelect, - } = useBase64Converter({ mode: 'file' }); - - const handleDownload = () => { - if (decoded) downloadBlob(decoded.blob, decodedFileName); - }; - - return ( -
-
- { - if (next && next !== direction) { - resetAll(); - setDirection(next); - } - }} - size="small" - /> -
- - {direction === 'encode' ? ( -
-
{ - e.preventDefault(); - setIsDragging(true); - }} - onDragLeave={() => setIsDragging(false)} - onDrop={(e) => { - e.preventDefault(); - setIsDragging(false); - const file = e.dataTransfer.files[0]; - if (file) safeFileSelect(file); - }} - onClick={() => fileInputRef.current?.click()} - className={cn( - 'flex flex-col items-center justify-center min-h-[190px] border-2 border-dashed rounded-2xl p-8 cursor-pointer', - isDragging - ? 'border-primary bg-primary/10' - : info - ? 'border-primary/60 bg-primary/5' - : 'border-border bg-muted/40 hover:border-primary/80 hover:bg-muted/70', - )} - > - { - const file = e.target.files?.[0]; - if (file) safeFileSelect(file); - }} - /> - {isLoading ? ( -
- ) : info ? ( -
- - - {info.name} - - - {formatFileSize(info.size)} · {info.type} - - - {t('clickOrDropToReplace')} - -
- ) : ( -
- - - {t('clickOrDropToFile')} - - - {t('maxFileSize', { max: `${MAX_FILE_SIZE / 1024 / 1024} MB` })} - -
- )} -
- - {encodeError && ( -
- {encodeError} -
- )} - - {result && ( -
-
- - {t('base64Output')} - -
- - -
-
- 2000 - ? `${result.output.substring(0, 2000)}...` - : result.output - } - showClear={false} - minRows={4} - /> -
-
- - {t('originalSize')}:{' '} - - {formatFileSize(result.originalBytes)} - - - | - - {t('encodedSize')}:{' '} - - {formatFileSize(result.outputBytes)} - - -
- -
-
- )} -
- ) : ( -
- - {decoded && ( - - )} -
- )} -
- ); -} diff --git a/pages/Base64Converter/ImageMode.tsx b/pages/Base64Converter/ImageMode.tsx deleted file mode 100644 index eafb0c3..0000000 --- a/pages/Base64Converter/ImageMode.tsx +++ /dev/null @@ -1,259 +0,0 @@ -import { Image as ImageIcon, Trash2 } from 'lucide-react'; -import TextInputArea from '@/components/TextInputArea'; -import { useLazyTranslation } from '@/utils/useLazyTranslation'; -import CopyButton from '@/components/CopyButton'; -import DecodeResultPaper from '@/components/DecodeResultPaper'; -import { Button } from '@/components/ui/button'; -import { downloadBlob, formatFileSize } from '@/utils/base64Converter'; -import { useStorageState } from '@/utils/useStorageState'; -import type { Base64ConvertDirection } from '@/types/storage'; -import SwitchButtonGroup from '@/components/SwitchButtonGroup'; -import { useBase64Converter } from './useBase64Converter'; // 💡 引入共享核心 -import { cn } from '@/lib/utils'; - -const isValidDirection = (val: unknown): val is Base64ConvertDirection => - val === 'encode' || val === 'decode'; - -export default function ImageMode() { - const { t } = useLazyTranslation('base64Converter'); - const [direction, setDirection] = useStorageState( - 'base64Converter/imageMode/direction', - 'encode', - isValidDirection, - ); - - // 消费完全托管的核心 Hook,消灭本地多余状态机 - const { - result, - info, - isLoading, - isDragging, - setIsDragging, - fileInputRef, - encodeError, - decodeInput, - setDecodeInput, - decoded, - decodeError, - decodedFileName, - setCustomFileName, - resetAll, - safeFileSelect, - } = useBase64Converter({ mode: 'image' }); - - const handleDirectionChange = (next: Base64ConvertDirection) => { - if (!next || next === direction) return; - resetAll(); - setDirection(next); - }; - - const handleDownload = () => { - if (decoded) downloadBlob(decoded.blob, decodedFileName); - }; - - return ( -
-
- -
- - {direction === 'encode' ? ( -
- {/* 图片拖拽投递箱终端 */} -
{ - e.preventDefault(); - setIsDragging(true); - }} - onDragLeave={() => setIsDragging(false)} - onDrop={(e) => { - e.preventDefault(); - setIsDragging(false); - const file = e.dataTransfer.files[0]; - if (file) safeFileSelect(file); - }} - onClick={() => fileInputRef.current?.click()} - className={cn( - 'flex flex-col items-center justify-center min-h-[190px] border-2 border-dashed rounded-2xl p-8 cursor-pointer', - isDragging - ? 'border-primary bg-primary/10' - : info - ? 'border-primary/60 bg-primary/5' - : 'border-border bg-muted/40 hover:border-primary/80 hover:bg-muted/70', - )} - > - { - const file = e.target.files?.[0]; - if (file) safeFileSelect(file); - }} - /> - {isLoading ? ( -
- ) : info ? ( -
- {result && ( -
- preview -
- )} - - {info.name} - - - {formatFileSize(info.size)} · {info.type} - - - {t('clickOrDropToReplace')} - -
- ) : ( -
- - - {t('clickOrDropToImage')} - - - {t('supportedFormats')} - -
- )} -
- - {encodeError && ( -
- {encodeError} -
- )} - - {result && ( -
-
- - {t('base64Output')} - -
- - -
-
- - 2000 - ? `${result.output.substring(0, 2000)}...` - : result.output - } - showClear={false} - minRows={4} - /> - -
-
- - {t('originalSize')}:{' '} - - {formatFileSize(result.originalBytes)} - - - | - - {t('encodedSize')}:{' '} - - {formatFileSize(result.outputBytes)} - - -
- - -
-
- )} - - {info && !result && ( -
- -
- )} -
- ) : ( -
- - {decoded && ( -
- -
- decoded preview -
-
-
- )} -
- )} -
- ); -} diff --git a/pages/Base64Converter/__tests__/FileMode.test.tsx b/pages/Base64Converter/__tests__/FileMode.test.tsx deleted file mode 100644 index 4af2760..0000000 --- a/pages/Base64Converter/__tests__/FileMode.test.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; -import FileMode from '../FileMode'; - -// Mock CopyButton -vi.mock('@/components/CopyButton', () => ({ - default: ({ text, tooltip }: { text: string; tooltip?: string }) => ( - - ), -})); - -beforeEach(() => { - localStorage.clear(); - vi.useFakeTimers({ shouldAdvanceTime: true }); -}); - -afterEach(() => { - vi.useRealTimers(); -}); - -// useStorageState's async loadState may overwrite user toggle if we click before the -// initial chrome.storage read settles. Flush pending microtasks first. -const waitForStorageReady = () => act(() => Promise.resolve()); - -describe('FileMode', () => { - it('应该渲染文件上传区域', async () => { - render(); - await waitForStorageReady(); - expect(screen.getByText('base64Converter:clickOrDropToFile')).toBeInTheDocument(); - expect(screen.getByText('base64Converter:maxFileSize')).toBeInTheDocument(); - }); - - it('应该处理有效的文件选择', async () => { - render(); - await waitForStorageReady(); - - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }); - - // 文件输入是隐藏的,直接触发 change 事件 - const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement; - fireEvent.change(hiddenInput, { target: { files: [file] } }); - - await waitFor(() => { - expect(screen.getByText('test.txt')).toBeInTheDocument(); - expect(screen.getByText('base64Converter:base64Output')).toBeInTheDocument(); - }); - }); - - it('应该拒绝超出大小限制的文件', async () => { - render(); - await waitForStorageReady(); - - // 创建一个超过 10MB 的文件 - const largeContent = new Uint8Array(11 * 1024 * 1024); - const file = new File([largeContent], 'large.bin', { type: 'application/octet-stream' }); - - const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement; - fireEvent.change(hiddenInput, { target: { files: [file] } }); - - await waitFor(() => { - expect(screen.getByRole('alert')).toHaveTextContent('base64Converter:fileSizeExceeded'); - }); - }); - - it('点击清除按钮应该清空文件状态', async () => { - render(); - await waitForStorageReady(); - - const file = new File(['test'], 'test.txt', { type: 'text/plain' }); - const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement; - fireEvent.change(hiddenInput, { target: { files: [file] } }); - - await waitFor(() => { - expect(screen.getByText('test.txt')).toBeInTheDocument(); - }); - - fireEvent.click(screen.getByText('base64Converter:clear')); - - await waitFor(() => { - expect(screen.queryByText('test.txt')).not.toBeInTheDocument(); - expect(screen.getByText('base64Converter:clickOrDropToFile')).toBeInTheDocument(); - }); - }); - - it('应该显示文件大小和类型信息', async () => { - render(); - await waitForStorageReady(); - - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }); - const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement; - fireEvent.change(hiddenInput, { target: { files: [file] } }); - - await waitFor(() => { - expect(screen.getByText('test.txt')).toBeInTheDocument(); - }); - // 文件类型显示在 caption 中,格式为 "size · type" - expect(screen.getByText(/test\.txt/)).toBeInTheDocument(); - }); - - it('应该显示原始大小和编码大小', async () => { - render(); - await waitForStorageReady(); - - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }); - const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement; - fireEvent.change(hiddenInput, { target: { files: [file] } }); - - await waitFor(() => { - expect(screen.getByText(/base64Converter:originalSize/)).toBeInTheDocument(); - expect(screen.getByText(/base64Converter:encodedSize/)).toBeInTheDocument(); - }); - }); - - it('应该提供复制按钮', async () => { - render(); - await waitForStorageReady(); - - const file = new File(['test'], 'test.txt', { type: 'text/plain' }); - const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement; - fireEvent.change(hiddenInput, { target: { files: [file] } }); - - await waitFor(() => { - const copyButtons = screen.getAllByTestId('copy-button'); - expect(copyButtons.length).toBeGreaterThanOrEqual(2); - }); - }); - - it('应该渲染 encode/decode 切换按钮', () => { - render(); - expect(screen.getByText('base64Converter:encode')).toBeInTheDocument(); - expect(screen.getByText('base64Converter:decode')).toBeInTheDocument(); - }); - - it('切到 decode 应该显示 Base64 输入框', async () => { - render(); - await waitForStorageReady(); - fireEvent.click(screen.getByText('base64Converter:decode')); - expect( - await screen.findByPlaceholderText('base64Converter:decodeBase64Placeholder'), - ).toBeInTheDocument(); - }); - - it('解码 PDF Base64 后应该显示 application/pdf 与默认文件名 decoded.pdf', async () => { - render(); - await waitForStorageReady(); - fireEvent.click(screen.getByText('base64Converter:decode')); - - const input = await screen.findByPlaceholderText('base64Converter:decodeBase64Placeholder'); - fireEvent.change(input, { target: { value: 'JVBERi0K' } }); - - act(() => { - vi.advanceTimersByTime(250); - }); - - await waitFor(() => { - expect(screen.getByText('base64Converter:decodedFileOutput')).toBeInTheDocument(); - expect(screen.getByText(/application\/pdf/)).toBeInTheDocument(); - expect(screen.getByDisplayValue('decoded.pdf')).toBeInTheDocument(); - }); - }); - - it('解码后的文件名应该可编辑', async () => { - render(); - await waitForStorageReady(); - fireEvent.click(screen.getByText('base64Converter:decode')); - - const input = await screen.findByPlaceholderText('base64Converter:decodeBase64Placeholder'); - fireEvent.change(input, { target: { value: 'JVBERi0K' } }); - - act(() => { - vi.advanceTimersByTime(250); - }); - - const filenameInput = (await screen.findByDisplayValue('decoded.pdf')) as HTMLInputElement; - fireEvent.change(filenameInput, { target: { value: 'my-report.pdf' } }); - expect(filenameInput.value).toBe('my-report.pdf'); - }); - - it('解码后应该显示下载按钮', async () => { - render(); - await waitForStorageReady(); - fireEvent.click(screen.getByText('base64Converter:decode')); - - const input = await screen.findByPlaceholderText('base64Converter:decodeBase64Placeholder'); - fireEvent.change(input, { target: { value: 'JVBERi0K' } }); - - act(() => { - vi.advanceTimersByTime(250); - }); - - expect(await screen.findByText('base64Converter:download')).toBeInTheDocument(); - }); - - it('解码非法 Base64 应该显示 invalidBase64 错误', async () => { - render(); - await waitForStorageReady(); - fireEvent.click(screen.getByText('base64Converter:decode')); - - const input = await screen.findByPlaceholderText('base64Converter:decodeBase64Placeholder'); - fireEvent.change(input, { target: { value: '!!!not base64' } }); - - act(() => { - vi.advanceTimersByTime(250); - }); - - await waitFor(() => { - expect(screen.getByText('base64Converter:invalidBase64')).toBeInTheDocument(); - }); - }); - - it('切换方向时应该清空解码状态', async () => { - render(); - await waitForStorageReady(); - fireEvent.click(screen.getByText('base64Converter:decode')); - - const input = await screen.findByPlaceholderText('base64Converter:decodeBase64Placeholder'); - fireEvent.change(input, { target: { value: 'JVBERi0K' } }); - - act(() => { - vi.advanceTimersByTime(250); - }); - - await waitFor(() => { - expect(screen.getByText('base64Converter:decodedFileOutput')).toBeInTheDocument(); - }); - - fireEvent.click(screen.getByText('base64Converter:encode')); - - await waitFor(() => { - expect(screen.queryByText('decodedFileOutput')).not.toBeInTheDocument(); - }); - }); -}); diff --git a/pages/Base64Converter/__tests__/ImageMode.test.tsx b/pages/Base64Converter/__tests__/ImageMode.test.tsx deleted file mode 100644 index 440dbe7..0000000 --- a/pages/Base64Converter/__tests__/ImageMode.test.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; -import ImageMode from '../ImageMode'; - -// Mock CopyButton -vi.mock('@/components/CopyButton', () => ({ - default: ({ text, tooltip }: { text: string; tooltip?: string }) => ( - - ), -})); - -beforeEach(() => { - localStorage.clear(); - vi.useFakeTimers({ shouldAdvanceTime: true }); -}); - -afterEach(() => { - vi.useRealTimers(); -}); - -const waitForStorageReady = () => act(() => Promise.resolve()); - -describe('ImageMode', () => { - it('应该渲染图像上传区域', async () => { - render(); - await waitForStorageReady(); - expect(screen.getByText('base64Converter:clickOrDropToImage')).toBeInTheDocument(); - expect(screen.getByText('base64Converter:supportedFormats')).toBeInTheDocument(); - }); - - it('应该接受有效的图像文件', async () => { - render(); - await waitForStorageReady(); - - const file = new File(['fake-image-data'], 'test.png', { type: 'image/png' }); - const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement; - fireEvent.change(hiddenInput, { target: { files: [file] } }); - - await waitFor(() => { - expect(screen.getByText('test.png')).toBeInTheDocument(); - expect(screen.getByText('base64Converter:base64Output')).toBeInTheDocument(); - }); - }); - - it('应该拒绝非图像文件', async () => { - render(); - await waitForStorageReady(); - - const file = new File(['not an image'], 'test.txt', { type: 'text/plain' }); - const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement; - fireEvent.change(hiddenInput, { target: { files: [file] } }); - - await waitFor(() => { - expect(screen.getByRole('alert')).toHaveTextContent('base64Converter:unsupportedImageType'); - }); - }); - - it('应该拒绝超出大小限制的图像', async () => { - render(); - await waitForStorageReady(); - - const largeContent = new Uint8Array(11 * 1024 * 1024); - const file = new File([largeContent], 'large.png', { type: 'image/png' }); - - const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement; - fireEvent.change(hiddenInput, { target: { files: [file] } }); - - await waitFor(() => { - expect(screen.getByRole('alert')).toHaveTextContent('base64Converter:fileSizeExceeded'); - }); - }); - - it('应该通过扩展名识别图像', async () => { - render(); - await waitForStorageReady(); - - // 没有 MIME 类型但有正确扩展名 - const file = new File(['fake'], 'test.jpg'); - const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement; - fireEvent.change(hiddenInput, { target: { files: [file] } }); - - await waitFor(() => { - expect(screen.getByText('test.jpg')).toBeInTheDocument(); - }); - }); - - it('点击清除按钮应该清空图像状态', async () => { - render(); - await waitForStorageReady(); - - const file = new File(['fake'], 'test.png', { type: 'image/png' }); - const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement; - fireEvent.change(hiddenInput, { target: { files: [file] } }); - - await waitFor(() => { - expect(screen.getByText('test.png')).toBeInTheDocument(); - }); - - fireEvent.click(screen.getByText('base64Converter:clear')); - - await waitFor(() => { - expect(screen.queryByText('test.png')).not.toBeInTheDocument(); - expect(screen.getByText('base64Converter:clickOrDropToImage')).toBeInTheDocument(); - }); - }); - - it('应该显示图像预览', async () => { - render(); - await waitForStorageReady(); - - const file = new File(['fake-image'], 'test.png', { type: 'image/png' }); - const hiddenInput = document.querySelector('input[type="file"]') as HTMLInputElement; - fireEvent.change(hiddenInput, { target: { files: [file] } }); - - await waitFor(() => { - const img = screen.getByAltText('preview'); - expect(img).toBeInTheDocument(); - expect(img.tagName.toLowerCase()).toBe('img'); - }); - }); - - it('应该渲染 encode/decode 切换按钮', async () => { - render(); - await waitForStorageReady(); - expect(screen.getByText('base64Converter:encode')).toBeInTheDocument(); - expect(screen.getByText('base64Converter:decode')).toBeInTheDocument(); - }); - - it('解码 PNG Base64 后应该显示图像预览', async () => { - render(); - await waitForStorageReady(); - fireEvent.click(screen.getByText('base64Converter:decode')); - - const input = await screen.findByPlaceholderText('base64Converter:decodeBase64Placeholder'); - fireEvent.change(input, { target: { value: 'iVBORw0KGgo=' } }); - - act(() => { - vi.advanceTimersByTime(250); - }); - - await waitFor(() => { - expect(screen.getByText('base64Converter:decodedImageOutput')).toBeInTheDocument(); - const img = screen.getByAltText('decoded preview'); - expect(img).toBeInTheDocument(); - expect(img.tagName.toLowerCase()).toBe('img'); - }); - }); - - it('解码后默认文件名应该为 decoded.png', async () => { - render(); - await waitForStorageReady(); - fireEvent.click(screen.getByText('base64Converter:decode')); - - const input = await screen.findByPlaceholderText('base64Converter:decodeBase64Placeholder'); - fireEvent.change(input, { target: { value: 'iVBORw0KGgo=' } }); - - act(() => { - vi.advanceTimersByTime(250); - }); - - expect(await screen.findByDisplayValue('decoded.png')).toBeInTheDocument(); - }); - - it('解码非法 Base64 应该显示 invalidBase64 错误', async () => { - render(); - await waitForStorageReady(); - fireEvent.click(screen.getByText('base64Converter:decode')); - - const input = await screen.findByPlaceholderText('base64Converter:decodeBase64Placeholder'); - fireEvent.change(input, { target: { value: '!!!not base64' } }); - - act(() => { - vi.advanceTimersByTime(250); - }); - - await waitFor(() => { - expect(screen.getByText('base64Converter:invalidBase64')).toBeInTheDocument(); - }); - }); -}); diff --git a/pages/Dashboard/ToolCard.tsx b/pages/Dashboard/ToolCard.tsx index 08b4819..e8f6379 100644 --- a/pages/Dashboard/ToolCard.tsx +++ b/pages/Dashboard/ToolCard.tsx @@ -10,7 +10,7 @@ const PALETTE_COLORS: Record = { success: '22, 163, 74', // green warning: '217, 119, 6', // amber (存储清理的橙色轴) error: '220, 38, 38', // red - secondary: '147, 51, 2 purple', + secondary: '147, 51, 232', info: '37, 99, 235', // blue }; diff --git a/pages/JsonTools/JsonConvertSection.tsx b/pages/JsonTools/JsonConvertSection.tsx index 8abd951..46eca77 100644 --- a/pages/JsonTools/JsonConvertSection.tsx +++ b/pages/JsonTools/JsonConvertSection.tsx @@ -133,7 +133,7 @@ export default function JsonConvertSection({ /* 5. 空状态提示容器:完美的中性虚线引导,不喧宾夺主 */

- {error ? '请修正上方 JSON 的语法错误以激活流式转换' : t(`jsonFormat:${pk}EmptyHint`)} + {error ? t('jsonFormat:fixErrorHint') : t(`jsonFormat:${pk}EmptyHint`)}

)} diff --git a/pages/JsonTools/JsonFormatSection.tsx b/pages/JsonTools/JsonFormatSection.tsx index bc4b091..bcd8076 100644 --- a/pages/JsonTools/JsonFormatSection.tsx +++ b/pages/JsonTools/JsonFormatSection.tsx @@ -160,7 +160,7 @@ export default function JsonFormatSection() { /* 空状态指示引导区 */

- {error ? '请修正上方 JSON 语法错误以开启实时流式格式化' : t('jsonFormat:emptyHint')} + {error ? t('jsonFormat:fixErrorHint') : t('jsonFormat:emptyHint')}

)} diff --git a/pages/JsonTools/index.tsx b/pages/JsonTools/index.tsx index c5f7079..0b58ff0 100644 --- a/pages/JsonTools/index.tsx +++ b/pages/JsonTools/index.tsx @@ -182,9 +182,7 @@ export default function Index() { ) : (

- {leftError || rightError - ? '请修正上方 JSON 的语法错误以开启实时流式比对' - : t('jsonDiff:emptyHint')} + {leftError || rightError ? t('jsonDiff:fixErrorHint') : t('jsonDiff:emptyHint')}

)} diff --git a/pages/MarkdownToHtml/index.tsx b/pages/MarkdownToHtml/index.tsx index a43eda3..cf9e6e5 100644 --- a/pages/MarkdownToHtml/index.tsx +++ b/pages/MarkdownToHtml/index.tsx @@ -37,7 +37,7 @@ const PREVIEW_STYLES = ` .markdown-body h1 { border-bottom: 1px solid var(--md-border); padding-bottom: 0.3em; font-size: 1.6em; } .markdown-body h2 { border-bottom: 1px solid var(--md-border); padding-bottom: 0.3em; font-size: 1.35em; } .markdown-body p { margin-top: 0; margin-bottom: 16px; } - .markdown-body a { color: #3b82f6; text-decoration: none; } + .markdown-body a { color: var(--md-link-color); text-decoration: none; } .markdown-body a:hover { text-decoration: underline; } .markdown-body code { background-color: var(--md-code-bg); @@ -110,6 +110,7 @@ export default function MarkdownToHtmlPage() { --md-pre-bg: rgba(255,255,255,0.04); --md-muted: #8b949e; --md-quote-line: rgba(255,255,255,0.25); + --md-link-color: #58a6ff; }` : `:root { --md-bg: #ffffff; @@ -119,6 +120,7 @@ export default function MarkdownToHtmlPage() { --md-pre-bg: rgba(128,128,128,0.03); --md-muted: #4b5563; --md-quote-line: rgba(128,128,128,0.3); + --md-link-color: #3b82f6; }`; // 💡 3. 核心大清洗:将全局基础树(html, body)与派生样式完全独立硬编码,杜绝任何语法踩踏 diff --git a/pages/README.md b/pages/README.md index 7cb5080..c3a15a2 100644 --- a/pages/README.md +++ b/pages/README.md @@ -106,14 +106,12 @@ JSON 工具集:差异比较、格式化、YAML/TOML/Minify 转换。 Base64 编解码工具,支持文本/文件/图片三种模式。 -| 文件 | 用途 | -| ---------------------------- | -------------------- | -| `index.tsx` | 页面入口,子模式切换 | -| `useBase64Converter.ts` | 业务逻辑 Hook | -| `TextMode.tsx` | 文本模式 | -| `FileMode.tsx` | 文件模式 | -| `ImageMode.tsx` | 图片模式 | -| `Base64ConverterSection.tsx` | 通用转换区域 | +| 文件 | 用途 | +| ---------------------------- | --------------------- | +| `index.tsx` | 页面入口,子模式切换 | +| `useBase64Converter.ts` | 业务逻辑 Hook | +| `TextMode.tsx` | 文本模式 | +| `Base64ConverterSection.tsx` | 文件/图片通用转换区域 | ### MarkdownToHtml/ diff --git a/pages/StorageCleaner/index.tsx b/pages/StorageCleaner/index.tsx index 8cf82af..02ddd2d 100644 --- a/pages/StorageCleaner/index.tsx +++ b/pages/StorageCleaner/index.tsx @@ -36,7 +36,7 @@ export default function Index() {
- 正在读取站点数据... + {t('storageCleaner:initializing')}
); diff --git a/pages/Timestamp/LiveClock.tsx b/pages/Timestamp/LiveClock.tsx index a809183..736e7c5 100644 --- a/pages/Timestamp/LiveClock.tsx +++ b/pages/Timestamp/LiveClock.tsx @@ -88,7 +88,7 @@ const LiveClock = React.memo(({ unit, onUseNow, className, ...props }: LiveClock