230 lines
6.7 KiB
TypeScript
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;
|