feat: 添加移动端下拉菜单和主题色设置

- 新增 MobileMenu 组件,小屏幕使用汉堡菜单替代多个按钮
- 重构 LanguageSettings 为统一下拉框样式
- 新增设置页面,支持主题色切换
- 翻译页添加源语言选择器
- 更新 8 种语言的 i18n 翻译
This commit is contained in:
2026-03-10 13:44:52 +08:00
parent 6b9fba254d
commit abcae1b8d1
22 changed files with 877 additions and 177 deletions

View File

@@ -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>