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 && (
-
-

-
- )}
-
- {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 && (
-
-
-
-

-
-
-
- )}
-
- )}
-
- );
-}
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