Files
chunyu_prject_react/src/pages/CharsetConverter/CharsetConverterMobile.tsx
T
2026-08-05 23:59:22 +08:00

230 lines
6.7 KiB
TypeScript

import "./CharsetConverterMobile.css";
import React, { useState, useRef } from "react";
import { Button, Select } from "antd";
import { message } from "@/utils/message";
import {
ArrowLeftOutlined,
SwapOutlined,
CopyOutlined,
DeleteOutlined,
DownloadOutlined,
UploadOutlined,
} from "@ant-design/icons";
import { useNavigate } from "react-router-dom";
import ToolFavoriteButton from "@/components/ToolFavoriteButton/ToolFavoriteButton";
import {
CHAR_ENCODING_OPTIONS,
decodeBytes,
encodeText,
detectEncoding,
fileToBytes,
downloadBytes,
normalizeEncoding,
} from "@/utils/charEncoding";
const ENCODING_OPTIONS = CHAR_ENCODING_OPTIONS.map((opt) => ({
value: opt.value,
label: opt.label,
}));
const sourceOptions = [
{ value: "auto", label: "自动检测" },
...ENCODING_OPTIONS,
];
const CharsetConverterMobile: React.FC = () => {
const navigate = useNavigate();
const [inputText, setInputText] = useState("");
const [outputText, setOutputText] = useState("");
const [sourceEncoding, setSourceEncoding] = useState("auto");
const [targetEncoding, setTargetEncoding] = useState("utf-8");
const [fileInfo, setFileInfo] = useState<{ name: string; bytes: Uint8Array } | null>(null);
const [convertedBytes, setConvertedBytes] = useState<Uint8Array | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFile = async (file: File) => {
try {
const bytes = await fileToBytes(file);
setFileInfo({ name: file.name, bytes });
const detected = detectEncoding(bytes);
setInputText(decodeBytes(bytes, detected.encoding));
setSourceEncoding(detected.encoding);
setOutputText("");
setConvertedBytes(null);
message.success("文件已加载");
} catch (e: any) {
message.error(`读取文件失败: ${e.message}`);
}
};
const handleConvert = () => {
if (!fileInfo && !inputText.trim()) {
message.warning("请先上传文件或输入文本");
return;
}
try {
let srcBytes: Uint8Array;
let src: string;
if (fileInfo) {
srcBytes = fileInfo.bytes;
src = sourceEncoding === "auto" ? detectEncoding(fileInfo.bytes).encoding : sourceEncoding;
} else {
src = normalizeEncoding(sourceEncoding);
srcBytes = encodeText(inputText, src);
}
const decoded = decodeBytes(srcBytes, src);
const outBytes = encodeText(decoded, targetEncoding);
setOutputText(decoded);
setConvertedBytes(outBytes);
message.success("转换成功");
} catch (e: any) {
message.error(`转换错误: ${e.message}`);
}
};
const handleSwap = () => {
const tmp = sourceEncoding;
setSourceEncoding(targetEncoding);
setTargetEncoding(tmp);
const tmpText = inputText;
setInputText(outputText);
setOutputText(tmpText);
};
const handleClear = () => {
setInputText("");
setOutputText("");
setFileInfo(null);
setConvertedBytes(null);
setSourceEncoding("auto");
setTargetEncoding("utf-8");
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const handleCopy = () => {
if (!outputText) {
message.warning("没有可复制的内容");
return;
}
navigator.clipboard.writeText(outputText).then(() => {
message.success("已复制到剪贴板");
});
};
const handleDownload = () => {
if (!convertedBytes || convertedBytes.length === 0) {
message.warning("请先进行转换");
return;
}
const baseName = fileInfo?.name
? fileInfo.name.replace(/\.[^.]+$/, "")
: "converted";
const extMatch = fileInfo?.name?.match(/\.[^.]+$/);
const ext = extMatch ? extMatch[0] : ".txt";
downloadBytes(convertedBytes, `${baseName}.${targetEncoding}${ext}`, targetEncoding);
};
return (
<div className="cc-mobile-page">
<div className="cc-mobile-header">
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={() => navigate("/utility")}
/>
<div className="cc-mobile-header-title-row">
<span>编码转换</span>
<ToolFavoriteButton urlPath="/utility/charset-converter" variant="hero" />
</div>
<span />
</div>
<div className="cc-mobile-content">
<div className="cc-mobile-actions">
<button className="cc-mobile-action-btn" onClick={handleCopy}>
<CopyOutlined /> <span>复制</span>
</button>
<button className="cc-mobile-action-btn" onClick={handleDownload}>
<DownloadOutlined /> <span>下载</span>
</button>
<button className="cc-mobile-action-btn" onClick={handleSwap}>
<SwapOutlined /> <span>交换</span>
</button>
<button className="cc-mobile-action-btn cc-mobile-action-btn--danger" onClick={handleClear}>
<DeleteOutlined /> <span>清空</span>
</button>
</div>
<div className="cc-mobile-select-area">
<Select
value={sourceEncoding}
onChange={setSourceEncoding}
options={sourceOptions}
style={{ width: "100%" }}
size="large"
showSearch
optionFilterProp="label"
/>
<div className="cc-mobile-swap-row">
<SwapOutlined style={{ color: "#1677ff" }} />
</div>
<Select
value={targetEncoding}
onChange={setTargetEncoding}
options={ENCODING_OPTIONS}
style={{ width: "100%" }}
size="large"
showSearch
optionFilterProp="label"
/>
</div>
<input
type="file"
ref={fileInputRef}
style={{ display: "none" }}
accept=".txt,.csv,.json,.xml,.html,.htm,.md,.log,.ini,.conf"
onChange={(e) => {
const f = e.target.files?.[0];
if (f) handleFile(f);
}}
/>
<div className="cc-mobile-upload" onClick={() => fileInputRef.current?.click()}>
<UploadOutlined style={{ fontSize: 22, color: "#1677ff" }} />
<span>上传文本文件</span>
</div>
<textarea
className="cc-mobile-textarea"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="在此输入或粘贴文本..."
rows={8}
spellCheck={false}
/>
<Button
type="primary"
icon={<SwapOutlined />}
onClick={handleConvert}
block
>
转换
</Button>
<div className="cc-mobile-output">
{outputText || "结果将显示在这里..."}
</div>
</div>
</div>
);
};
export default CharsetConverterMobile;