feat: 添加移动端下拉菜单和主题色设置
- 新增 MobileMenu 组件,小屏幕使用汉堡菜单替代多个按钮 - 重构 LanguageSettings 为统一下拉框样式 - 新增设置页面,支持主题色切换 - 翻译页添加源语言选择器 - 更新 8 种语言的 i18n 翻译
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { LightButton, PrimaryButton, IconClick } from "@/design-system/base/button";
|
||||
import { Select } from "@/design-system/base/select";
|
||||
import { IMAGES } from "@/config/images";
|
||||
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
||||
import { useTranslations } from "next-intl";
|
||||
@@ -10,16 +11,45 @@ import { toast } from "sonner";
|
||||
import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts";
|
||||
import { TSharedTranslationResult } from "@/shared/translator-type";
|
||||
|
||||
const SOURCE_LANGUAGES = [
|
||||
{ value: "Auto", labelKey: "auto" },
|
||||
{ value: "Chinese", labelKey: "chinese" },
|
||||
{ value: "English", labelKey: "english" },
|
||||
{ value: "Japanese", labelKey: "japanese" },
|
||||
{ value: "Korean", labelKey: "korean" },
|
||||
{ value: "French", labelKey: "french" },
|
||||
{ value: "German", labelKey: "german" },
|
||||
{ value: "Italian", labelKey: "italian" },
|
||||
{ value: "Spanish", labelKey: "spanish" },
|
||||
{ value: "Portuguese", labelKey: "portuguese" },
|
||||
{ value: "Russian", labelKey: "russian" },
|
||||
] as const;
|
||||
|
||||
const TARGET_LANGUAGES = [
|
||||
{ value: "Chinese", labelKey: "chinese" },
|
||||
{ value: "English", labelKey: "english" },
|
||||
{ value: "Japanese", labelKey: "japanese" },
|
||||
{ value: "Korean", labelKey: "korean" },
|
||||
{ value: "French", labelKey: "french" },
|
||||
{ value: "German", labelKey: "german" },
|
||||
{ value: "Italian", labelKey: "italian" },
|
||||
{ value: "Spanish", labelKey: "spanish" },
|
||||
{ value: "Portuguese", labelKey: "portuguese" },
|
||||
{ value: "Russian", labelKey: "russian" },
|
||||
] as const;
|
||||
|
||||
export default function TranslatorPage() {
|
||||
const t = useTranslations("translator");
|
||||
|
||||
const taref = useRef<HTMLTextAreaElement>(null);
|
||||
const [sourceLanguage, setSourceLanguage] = useState<string>("Auto");
|
||||
const [targetLanguage, setTargetLanguage] = useState<string>("Chinese");
|
||||
const [translationResult, setTranslationResult] = useState<TSharedTranslationResult | null>(null);
|
||||
const [needIpa, setNeedIpa] = useState(true);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [lastTranslation, setLastTranslation] = useState<{
|
||||
sourceText: string;
|
||||
sourceLanguage: string;
|
||||
targetLanguage: string;
|
||||
} | null>(null);
|
||||
const { load, play } = useAudioPlayer();
|
||||
@@ -63,9 +93,10 @@ export default function TranslatorPage() {
|
||||
const sourceText = taref.current.value;
|
||||
|
||||
// 判断是否需要强制重新翻译
|
||||
// 只有当源文本和目标语言都与上次相同时,才强制重新翻译
|
||||
// 只有当源文本、源语言和目标语言都与上次相同时,才强制重新翻译
|
||||
const forceRetranslate =
|
||||
lastTranslation?.sourceText === sourceText &&
|
||||
lastTranslation?.sourceLanguage === sourceLanguage &&
|
||||
lastTranslation?.targetLanguage === targetLanguage;
|
||||
|
||||
try {
|
||||
@@ -74,12 +105,14 @@ export default function TranslatorPage() {
|
||||
targetLanguage,
|
||||
forceRetranslate,
|
||||
needIpa,
|
||||
sourceLanguage: sourceLanguage === "Auto" ? undefined : sourceLanguage,
|
||||
});
|
||||
|
||||
if (result.success && result.data) {
|
||||
setTranslationResult(result.data);
|
||||
setLastTranslation({
|
||||
sourceText,
|
||||
sourceLanguage,
|
||||
targetLanguage,
|
||||
});
|
||||
} else {
|
||||
@@ -132,11 +165,47 @@ export default function TranslatorPage() {
|
||||
></IconClick>
|
||||
</div>
|
||||
</div>
|
||||
<div className="option1 w-full flex flex-row justify-between items-center">
|
||||
<span>{t("detectLanguage")}</span>
|
||||
<div className="option1 w-full flex gap-1 items-center overflow-x-auto">
|
||||
<span className="shrink-0">{t("sourceLanguage")}</span>
|
||||
<LightButton
|
||||
selected={sourceLanguage === "Auto"}
|
||||
onClick={() => setSourceLanguage("Auto")}
|
||||
className="shrink-0 hidden lg:inline-flex"
|
||||
>
|
||||
{t("auto")}
|
||||
</LightButton>
|
||||
<LightButton
|
||||
selected={sourceLanguage === "Chinese"}
|
||||
onClick={() => setSourceLanguage("Chinese")}
|
||||
className="shrink-0 hidden lg:inline-flex"
|
||||
>
|
||||
{t("chinese")}
|
||||
</LightButton>
|
||||
<LightButton
|
||||
selected={sourceLanguage === "English"}
|
||||
onClick={() => setSourceLanguage("English")}
|
||||
className="shrink-0 hidden xl:inline-flex"
|
||||
>
|
||||
{t("english")}
|
||||
</LightButton>
|
||||
<Select
|
||||
value={sourceLanguage}
|
||||
onChange={(e) => setSourceLanguage(e.target.value)}
|
||||
variant="light"
|
||||
size="sm"
|
||||
className="w-auto min-w-[100px] shrink-0"
|
||||
>
|
||||
{SOURCE_LANGUAGES.map((lang) => (
|
||||
<option key={lang.value} value={lang.value}>
|
||||
{t(lang.labelKey)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<div className="flex-1"></div>
|
||||
<LightButton
|
||||
selected={needIpa}
|
||||
onClick={() => setNeedIpa((prev) => !prev)}
|
||||
className="shrink-0"
|
||||
>
|
||||
{t("generateIPA")}
|
||||
</LightButton>
|
||||
@@ -172,37 +241,42 @@ export default function TranslatorPage() {
|
||||
></IconClick>
|
||||
</div>
|
||||
</div>
|
||||
<div className="option2 w-full flex gap-1 items-center flex-wrap">
|
||||
<span>{t("translateInto")}</span>
|
||||
<div className="option2 w-full flex gap-1 items-center overflow-x-auto">
|
||||
<span className="shrink-0">{t("translateInto")}</span>
|
||||
<LightButton
|
||||
selected={targetLanguage === "Chinese"}
|
||||
onClick={() => setTargetLanguage("Chinese")}
|
||||
className="shrink-0 hidden lg:inline-flex"
|
||||
>
|
||||
{t("chinese")}
|
||||
</LightButton>
|
||||
<LightButton
|
||||
selected={targetLanguage === "English"}
|
||||
onClick={() => setTargetLanguage("English")}
|
||||
className="shrink-0 hidden lg:inline-flex"
|
||||
>
|
||||
{t("english")}
|
||||
</LightButton>
|
||||
<LightButton
|
||||
selected={targetLanguage === "Italian"}
|
||||
onClick={() => setTargetLanguage("Italian")}
|
||||
selected={targetLanguage === "Japanese"}
|
||||
onClick={() => setTargetLanguage("Japanese")}
|
||||
className="shrink-0 hidden xl:inline-flex"
|
||||
>
|
||||
{t("italian")}
|
||||
{t("japanese")}
|
||||
</LightButton>
|
||||
<LightButton
|
||||
selected={!["Chinese", "English", "Italian"].includes(targetLanguage)}
|
||||
onClick={() => {
|
||||
const newLang = prompt(t("enterLanguage"));
|
||||
if (newLang) {
|
||||
setTargetLanguage(newLang);
|
||||
}
|
||||
}}
|
||||
<Select
|
||||
value={targetLanguage}
|
||||
onChange={(e) => setTargetLanguage(e.target.value)}
|
||||
variant="light"
|
||||
size="sm"
|
||||
className="w-auto min-w-[100px] shrink-0"
|
||||
>
|
||||
{t("other")}
|
||||
</LightButton>
|
||||
{TARGET_LANGUAGES.map((lang) => (
|
||||
<option key={lang.value} value={lang.value}>
|
||||
{t(lang.labelKey)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user