feat(i18n): add language switcher component to navigation
This commit is contained in:
153
frontend/src/components/LanguageSwitcher.vue
Normal file
153
frontend/src/components/LanguageSwitcher.vue
Normal 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>
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user