7167a43763
核心功能新增 工具套件扩展 JSON 工具(差异比较、格式化、转 YAML/TOML、压缩) Base64 转换器(文本/文件/图像编解码) Markdown ↔ HTML 双向转换 JWT 解析器 文本统计工具 二维码生成/解析(替换库以减小体积) 表单工具(已移除) 表单映射、智能填充、高亮定位功能已移除 架构与工程改进 技术栈升级 引入 @webext-core/messaging 重构消息通信 添加 @dnd-kit 支持功能列表拖拽排序 引入 i18next 实现中英文国际化 使用 MUI 主题系统替代硬编码样式,支持暗色模式 代码质量 统一组件导出为默认导出 提取公共组件(TextInputArea、SwitchButtonGroup、PageHeader、DecodeResultPaper) 使用 useStorageState 钩子统一管理存储状态 添加 PageErrorBoundary 页面级错误边界 实现组件懒加载优化首屏性能 关键重构 路由系统重构 — 支持独立标签页模式,合并路由与功能配置 状态管理优化 — 存储状态增加快照机制、验证器和防抖处理 剪贴板与打印工具 — 统一为 async/await 形式,修复重复触发问题 存储清理器 — 使用 TextEncoder 准确计算 UTF-8 字节数,支持 TB 单位 测试与 CI 测试覆盖 — 为新增工具、公共组件、边界场景补充单元测试 CI 精简 — 移除 Firefox 测试,仅保留 Chrome;修正类型检查命令
144 lines
4.3 KiB
TypeScript
144 lines
4.3 KiB
TypeScript
import { useCallback, useMemo, useState } from 'react';
|
|
import { Alert, alpha, Button, Paper, Stack, Typography } from '@mui/material';
|
|
import TextInputArea, { type ToolbarAction } from '@/components/TextInputArea';
|
|
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
|
|
import { useTranslation } from 'react-i18next';
|
|
import CopyButton from '@/components/CopyButton';
|
|
import { textToBase64, base64ToText } from '@/utils/base64Converter';
|
|
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
|
|
|
|
const IMAGE_DATA_URI_PATTERN = /^\s*data:image\//i;
|
|
|
|
const ERROR_MESSAGE_TO_I18N: Record<string, string> = {
|
|
'Invalid Base64 string': 'invalidBase64',
|
|
'Input appears to be binary data (e.g. an image). Please use the Image tab instead.':
|
|
'binaryDataDetected',
|
|
};
|
|
|
|
interface TextModeProps {
|
|
onSwitchToImageMode?: () => void;
|
|
}
|
|
|
|
export default function TextMode({ onSwitchToImageMode }: TextModeProps = {}) {
|
|
const { t } = useTranslation('base64Converter');
|
|
const [input, setInput] = useState('');
|
|
const [output, setOutput] = useState('');
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [direction, setDirection] = useState<'encode' | 'decode'>('encode');
|
|
|
|
const actionLabel = direction === 'encode' ? t('encode') : t('decode');
|
|
const placeholder =
|
|
direction === 'encode' ? t('textInputPlaceholder') : t('base64InputPlaceholder');
|
|
const outputLabel = direction === 'encode' ? t('base64Output') : t('textOutput');
|
|
|
|
const showImageHint = useMemo(
|
|
() => direction === 'decode' && IMAGE_DATA_URI_PATTERN.test(input),
|
|
[direction, input],
|
|
);
|
|
|
|
const handleDirectionChange = useCallback(
|
|
(value: 'encode' | 'decode') => {
|
|
if (value === direction) return;
|
|
setDirection(value);
|
|
setOutput('');
|
|
setError(null);
|
|
},
|
|
[direction],
|
|
);
|
|
|
|
const actions: ToolbarAction[] = useMemo(
|
|
() => [
|
|
{
|
|
key: 'convert',
|
|
label: actionLabel,
|
|
icon: <SwapHorizIcon />,
|
|
type: 'primary',
|
|
position: 'bottom',
|
|
disabled: (value: string) => !value.trim(),
|
|
onClick: (value: string) => {
|
|
setError(null);
|
|
try {
|
|
if (direction === 'encode') {
|
|
const result = textToBase64(value);
|
|
setOutput(result.output);
|
|
} else {
|
|
const decoded = base64ToText(value);
|
|
setOutput(decoded);
|
|
}
|
|
} catch (e) {
|
|
const message = e instanceof Error ? e.message : '';
|
|
const i18nKey = ERROR_MESSAGE_TO_I18N[message];
|
|
setError(i18nKey ? t(i18nKey) : message || t('conversionFailed'));
|
|
}
|
|
},
|
|
},
|
|
],
|
|
[direction, t, actionLabel],
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<SwitchButtonGroup
|
|
value={direction}
|
|
options={[
|
|
{ value: 'encode', label: t('encode') },
|
|
{ value: 'decode', label: t('decode') },
|
|
]}
|
|
onChange={handleDirectionChange}
|
|
size="small"
|
|
/>
|
|
|
|
<TextInputArea
|
|
placeholder={placeholder}
|
|
value={input}
|
|
onChange={(v) => {
|
|
setInput(v);
|
|
setError(null);
|
|
}}
|
|
actions={actions}
|
|
externalError={error || undefined}
|
|
onClear={() => setOutput('')}
|
|
/>
|
|
|
|
{showImageHint && (
|
|
<Alert
|
|
severity="info"
|
|
action={
|
|
<Button color="info" size="small" onClick={onSwitchToImageMode}>
|
|
{t('switchToImageMode')}
|
|
</Button>
|
|
}
|
|
>
|
|
{t('imageDataUriHint')}
|
|
</Alert>
|
|
)}
|
|
|
|
{output && (
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
p: 2,
|
|
borderRadius: 3,
|
|
bgcolor: (theme) => alpha(theme.palette.info.main, 0.04),
|
|
border: '1px solid',
|
|
borderColor: (theme) => alpha(theme.palette.info.main, 0.15),
|
|
}}
|
|
>
|
|
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}>
|
|
<Typography variant="caption" fontWeight={700} color="text.secondary">
|
|
{outputLabel}
|
|
</Typography>
|
|
<CopyButton text={output} />
|
|
</Stack>
|
|
<TextInputArea
|
|
readOnly
|
|
value={output.length > 2000 ? `${output.substring(0, 2000)}...` : output}
|
|
showClear={false}
|
|
showCount
|
|
/>
|
|
</Paper>
|
|
)}
|
|
</>
|
|
);
|
|
}
|