mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 创建语言切换组件
- 创建LanguageSwitch.vue组件,支持dropdown/button/icon三种显示模式 - 实现点击外部自动关闭下拉菜单功能 - 支持size属性控制组件大小(small/medium/large) - 集成locale store实现语言切换和状态同步 - 使用纯文字标识符显示:简/繁/EN,提供清晰的语言选择界面 - 下拉菜单显示完整语言名称:简体中文/繁體中文/English
This commit is contained in:
186
web/admin-spa/src/components/common/LanguageSwitch.vue
Normal file
186
web/admin-spa/src/components/common/LanguageSwitch.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<div class="language-switch" :class="containerClass">
|
||||
<!-- 下拉菜单模式 -->
|
||||
<div v-if="mode === 'dropdown'" class="relative">
|
||||
<button
|
||||
ref="dropdownTrigger"
|
||||
class="flex items-center gap-2 rounded-lg border border-gray-200 bg-white/80 px-3 py-2 text-sm font-medium text-gray-700 shadow-sm backdrop-blur-sm transition-all duration-200 hover:border-gray-300 hover:shadow-md dark:border-gray-600 dark:bg-gray-800/80 dark:text-gray-300 dark:hover:border-gray-500"
|
||||
@click="toggleDropdown"
|
||||
>
|
||||
<span>{{ currentLocaleInfo.flag }}</span>
|
||||
<i
|
||||
:class="[
|
||||
'fas fa-chevron-down text-xs transition-transform duration-200',
|
||||
showDropdown ? 'rotate-180' : ''
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<!-- 下拉选项 -->
|
||||
<transition
|
||||
enter-active-class="transition duration-200 ease-out"
|
||||
enter-from-class="transform scale-95 opacity-0"
|
||||
enter-to-class="transform scale-100 opacity-100"
|
||||
leave-active-class="transition duration-75 ease-in"
|
||||
leave-from-class="transform scale-100 opacity-100"
|
||||
leave-to-class="transform scale-95 opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="showDropdown"
|
||||
class="absolute right-0 top-full z-50 mt-2 w-40 rounded-lg border border-gray-200 bg-white py-2 shadow-xl dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<button
|
||||
v-for="locale in supportedLocales"
|
||||
:key="locale.code"
|
||||
class="flex w-full items-center gap-3 px-4 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
:class="{
|
||||
'bg-blue-50 text-blue-600 dark:bg-blue-900/20 dark:text-blue-400':
|
||||
locale.code === localeStore.currentLocale
|
||||
}"
|
||||
@click="switchLocale(locale.code)"
|
||||
>
|
||||
<span class="text-base font-medium">{{ locale.flag }}</span>
|
||||
<span class="flex-1 text-left">{{ locale.name }}</span>
|
||||
<i
|
||||
v-if="locale.code === localeStore.currentLocale"
|
||||
class="fas fa-check text-xs text-blue-600 dark:text-blue-400"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- 按钮模式 -->
|
||||
<div v-else-if="mode === 'button'" class="flex items-center gap-1">
|
||||
<button
|
||||
v-for="locale in supportedLocales"
|
||||
:key="locale.code"
|
||||
class="flex items-center gap-1 rounded-md px-2 py-1 text-sm transition-all duration-200"
|
||||
:class="[
|
||||
locale.code === localeStore.currentLocale
|
||||
? 'bg-blue-500 text-white shadow-sm'
|
||||
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700'
|
||||
]"
|
||||
@click="switchLocale(locale.code)"
|
||||
>
|
||||
<span>{{ locale.flag }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 图标模式 -->
|
||||
<div v-else-if="mode === 'icon'" class="relative">
|
||||
<button
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg text-gray-600 transition-all duration-200 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
||||
@click="toggleDropdown"
|
||||
>
|
||||
<span class="text-lg">{{ currentLocaleInfo.flag }}</span>
|
||||
</button>
|
||||
|
||||
<!-- 简化的下拉选项 -->
|
||||
<transition
|
||||
enter-active-class="transition duration-200 ease-out"
|
||||
enter-from-class="transform scale-95 opacity-0"
|
||||
enter-to-class="transform scale-100 opacity-100"
|
||||
leave-active-class="transition duration-75 ease-in"
|
||||
leave-from-class="transform scale-100 opacity-100"
|
||||
leave-to-class="transform scale-95 opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="showDropdown"
|
||||
class="absolute right-0 top-full z-50 mt-2 w-36 rounded-lg border border-gray-200 bg-white py-2 shadow-xl dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<button
|
||||
v-for="locale in supportedLocales"
|
||||
:key="locale.code"
|
||||
class="flex w-full items-center gap-2 px-3 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
:class="{
|
||||
'bg-blue-50 text-blue-600 dark:bg-blue-900/20 dark:text-blue-400':
|
||||
locale.code === localeStore.currentLocale
|
||||
}"
|
||||
@click="switchLocale(locale.code)"
|
||||
>
|
||||
<span class="font-medium">{{ locale.flag }}</span>
|
||||
<span>{{ locale.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useLocaleStore } from '@/stores/locale'
|
||||
|
||||
// 定义组件属性
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'dropdown', // dropdown | button | icon
|
||||
validator: (value) => ['dropdown', 'button', 'icon'].includes(value)
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium', // small | medium | large
|
||||
validator: (value) => ['small', 'medium', 'large'].includes(value)
|
||||
}
|
||||
})
|
||||
|
||||
// 发出事件
|
||||
const emit = defineEmits(['change'])
|
||||
|
||||
// 存储和响应式数据
|
||||
const localeStore = useLocaleStore()
|
||||
const showDropdown = ref(false)
|
||||
const dropdownTrigger = ref(null)
|
||||
|
||||
// 计算属性
|
||||
const currentLocaleInfo = computed(() => localeStore.getCurrentLocaleInfo())
|
||||
const supportedLocales = computed(() => localeStore.getSupportedLocales())
|
||||
|
||||
const containerClass = computed(() => {
|
||||
const classes = []
|
||||
|
||||
if (props.size === 'small') {
|
||||
classes.push('text-xs')
|
||||
} else if (props.size === 'large') {
|
||||
classes.push('text-base')
|
||||
}
|
||||
|
||||
return classes
|
||||
})
|
||||
|
||||
// 切换语言
|
||||
const switchLocale = (locale) => {
|
||||
localeStore.setLocale(locale)
|
||||
showDropdown.value = false
|
||||
emit('change', locale)
|
||||
}
|
||||
|
||||
// 切换下拉菜单显示
|
||||
const toggleDropdown = () => {
|
||||
showDropdown.value = !showDropdown.value
|
||||
}
|
||||
|
||||
// 点击外部关闭下拉菜单
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownTrigger.value && !dropdownTrigger.value.contains(event.target)) {
|
||||
showDropdown.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.language-switch {
|
||||
/* 自定义样式可以在这里添加 */
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user