feat(i18n): add language switcher component to navigation

This commit is contained in:
ghostubborn
2026-04-01 15:24:58 +08:00
parent 8f6110df0f
commit 3d5e5d024d
7 changed files with 171 additions and 0 deletions

View File

@@ -0,0 +1,153 @@
<template>
<div class="language-switcher" :class="{ dark: dark }" ref="switcherRef">
<button class="switcher-trigger" @click="toggleDropdown">
{{ currentLabel }}
<span class="caret">{{ open ? '▲' : '▼' }}</span>
</button>
<ul v-if="open" class="switcher-dropdown">
<li
v-for="loc in availableLocales"
:key="loc.key"
class="switcher-option"
:class="{ active: loc.key === locale }"
@click="switchLocale(loc.key)"
>
{{ loc.label }}
</li>
</ul>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { availableLocales } from '@/i18n/index.js'
const props = defineProps({
dark: {
type: Boolean,
default: false
}
})
const { locale } = useI18n()
const open = ref(false)
const switcherRef = ref(null)
const currentLabel = computed(() => {
const found = availableLocales.find(l => l.key === locale.value)
return found ? found.label : locale.value
})
const toggleDropdown = () => {
open.value = !open.value
}
const switchLocale = (key) => {
locale.value = key
localStorage.setItem('locale', key)
document.documentElement.lang = key
open.value = false
}
const onClickOutside = (e) => {
if (switcherRef.value && !switcherRef.value.contains(e.target)) {
open.value = false
}
}
onMounted(() => {
document.addEventListener('click', onClickOutside)
document.documentElement.lang = locale.value
})
onUnmounted(() => {
document.removeEventListener('click', onClickOutside)
})
</script>
<style scoped>
.language-switcher {
position: relative;
display: inline-block;
font-family: 'JetBrains Mono', monospace;
}
/* Light theme (default - for white header backgrounds) */
.switcher-trigger {
background: transparent;
color: #333;
border: 1px solid #CCC;
padding: 4px 12px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: border-color 0.2s, opacity 0.2s;
}
.switcher-trigger:hover {
border-color: #999;
}
.caret {
font-size: 0.6rem;
}
.switcher-dropdown {
position: absolute;
top: 100%;
right: 0;
margin-top: 4px;
background: #FFFFFF;
border: 1px solid #DDD;
list-style: none;
padding: 4px 0;
min-width: 100%;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.switcher-option {
padding: 6px 12px;
font-size: 0.8rem;
color: #333;
cursor: pointer;
white-space: nowrap;
transition: background 0.15s;
}
.switcher-option:hover {
background: #F0F0F0;
}
.switcher-option.active {
color: var(--orange, #FF4500);
}
/* Dark theme (for dark navbar backgrounds) */
.dark .switcher-trigger {
color: var(--white, #FFFFFF);
border-color: rgba(255, 255, 255, 0.3);
}
.dark .switcher-trigger:hover {
border-color: rgba(255, 255, 255, 0.6);
}
.dark .switcher-dropdown {
background: var(--black, #000000);
border-color: rgba(255, 255, 255, 0.2);
box-shadow: none;
}
.dark .switcher-option {
color: var(--white, #FFFFFF);
}
.dark .switcher-option:hover {
background: rgba(255, 255, 255, 0.1);
}
</style>

View File

@@ -4,6 +4,7 @@
<nav class="navbar"> <nav class="navbar">
<div class="nav-brand">MIROFISH</div> <div class="nav-brand">MIROFISH</div>
<div class="nav-links"> <div class="nav-links">
<LanguageSwitcher dark />
<a href="https://github.com/666ghj/MiroFish" target="_blank" class="github-link"> <a href="https://github.com/666ghj/MiroFish" target="_blank" class="github-link">
访问我们的Github主页 <span class="arrow"></span> 访问我们的Github主页 <span class="arrow"></span>
</a> </a>
@@ -210,6 +211,7 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import HistoryDatabase from '../components/HistoryDatabase.vue' import HistoryDatabase from '../components/HistoryDatabase.vue'
import LanguageSwitcher from '../components/LanguageSwitcher.vue'
const router = useRouter() const router = useRouter()
@@ -351,6 +353,7 @@ const startSimulation = () => {
.nav-links { .nav-links {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px;
} }
.github-link { .github-link {

View File

@@ -21,6 +21,8 @@
</div> </div>
<div class="header-right"> <div class="header-right">
<LanguageSwitcher />
<div class="step-divider"></div>
<div class="workflow-step"> <div class="workflow-step">
<span class="step-num">Step 5/5</span> <span class="step-num">Step 5/5</span>
<span class="step-name">深度互动</span> <span class="step-name">深度互动</span>
@@ -69,6 +71,7 @@ import Step5Interaction from '../components/Step5Interaction.vue'
import { getProject, getGraphData } from '../api/graph' import { getProject, getGraphData } from '../api/graph'
import { getSimulation } from '../api/simulation' import { getSimulation } from '../api/simulation'
import { getReport } from '../api/report' import { getReport } from '../api/report'
import LanguageSwitcher from '../components/LanguageSwitcher.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()

View File

@@ -21,6 +21,8 @@
</div> </div>
<div class="header-right"> <div class="header-right">
<LanguageSwitcher />
<div class="step-divider"></div>
<div class="workflow-step"> <div class="workflow-step">
<span class="step-num">Step {{ currentStep }}/5</span> <span class="step-num">Step {{ currentStep }}/5</span>
<span class="step-name">{{ stepNames[currentStep - 1] }}</span> <span class="step-name">{{ stepNames[currentStep - 1] }}</span>
@@ -82,6 +84,7 @@ import Step1GraphBuild from '../components/Step1GraphBuild.vue'
import Step2EnvSetup from '../components/Step2EnvSetup.vue' import Step2EnvSetup from '../components/Step2EnvSetup.vue'
import { generateOntology, getProject, buildGraph, getTaskStatus, getGraphData } from '../api/graph' import { generateOntology, getProject, buildGraph, getTaskStatus, getGraphData } from '../api/graph'
import { getPendingUpload, clearPendingUpload } from '../store/pendingUpload' import { getPendingUpload, clearPendingUpload } from '../store/pendingUpload'
import LanguageSwitcher from '../components/LanguageSwitcher.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()

View File

@@ -21,6 +21,8 @@
</div> </div>
<div class="header-right"> <div class="header-right">
<LanguageSwitcher />
<div class="step-divider"></div>
<div class="workflow-step"> <div class="workflow-step">
<span class="step-num">Step 4/5</span> <span class="step-num">Step 4/5</span>
<span class="step-name">报告生成</span> <span class="step-name">报告生成</span>
@@ -69,6 +71,7 @@ import Step4Report from '../components/Step4Report.vue'
import { getProject, getGraphData } from '../api/graph' import { getProject, getGraphData } from '../api/graph'
import { getSimulation } from '../api/simulation' import { getSimulation } from '../api/simulation'
import { getReport } from '../api/report' import { getReport } from '../api/report'
import LanguageSwitcher from '../components/LanguageSwitcher.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()

View File

@@ -21,6 +21,8 @@
</div> </div>
<div class="header-right"> <div class="header-right">
<LanguageSwitcher />
<div class="step-divider"></div>
<div class="workflow-step"> <div class="workflow-step">
<span class="step-num">Step 3/5</span> <span class="step-num">Step 3/5</span>
<span class="step-name">开始模拟</span> <span class="step-name">开始模拟</span>
@@ -73,6 +75,7 @@ import GraphPanel from '../components/GraphPanel.vue'
import Step3Simulation from '../components/Step3Simulation.vue' import Step3Simulation from '../components/Step3Simulation.vue'
import { getProject, getGraphData } from '../api/graph' import { getProject, getGraphData } from '../api/graph'
import { getSimulation, getSimulationConfig, stopSimulation, closeSimulationEnv, getEnvStatus } from '../api/simulation' import { getSimulation, getSimulationConfig, stopSimulation, closeSimulationEnv, getEnvStatus } from '../api/simulation'
import LanguageSwitcher from '../components/LanguageSwitcher.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()

View File

@@ -21,6 +21,8 @@
</div> </div>
<div class="header-right"> <div class="header-right">
<LanguageSwitcher />
<div class="step-divider"></div>
<div class="workflow-step"> <div class="workflow-step">
<span class="step-num">Step 2/5</span> <span class="step-num">Step 2/5</span>
<span class="step-name">环境搭建</span> <span class="step-name">环境搭建</span>
@@ -70,6 +72,7 @@ import GraphPanel from '../components/GraphPanel.vue'
import Step2EnvSetup from '../components/Step2EnvSetup.vue' import Step2EnvSetup from '../components/Step2EnvSetup.vue'
import { getProject, getGraphData } from '../api/graph' import { getProject, getGraphData } from '../api/graph'
import { getSimulation, stopSimulation, getEnvStatus, closeSimulationEnv } from '../api/simulation' import { getSimulation, stopSimulation, getEnvStatus, closeSimulationEnv } from '../api/simulation'
import LanguageSwitcher from '../components/LanguageSwitcher.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()