sync from local backup
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user