- 扩展 Icon.vue 组件,新增 60+ 图标路径 - 导航类: arrowRight, arrowLeft, arrowUp, arrowDown, chevronUp, externalLink - 状态类: checkCircle, xCircle, exclamationCircle, exclamationTriangle, infoCircle - 用户类: user, userCircle, userPlus, users - 文档类: document, clipboard, copy, inbox - 操作类: download, upload, filter, sort - 安全类: key, lock, shield - UI类: menu, calendar, home, terminal, gift, creditCard, mail - 数据类: chartBar, trendingUp, database, cube - 其他: bolt, sparkles, cloud, server, sun, moon, book 等 - 重构 56 个 Vue 组件,用 Icon 组件替换内联 SVG - 净减少约 2200 行代码 - 提升代码可维护性和一致性 - 统一图标样式和尺寸管理
92 lines
2.7 KiB
Vue
92 lines
2.7 KiB
Vue
<template>
|
|
<div class="relative" ref="dropdownRef">
|
|
<button
|
|
@click="toggleDropdown"
|
|
class="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-dark-700"
|
|
:title="currentLocale?.name"
|
|
>
|
|
<span class="text-base">{{ currentLocale?.flag }}</span>
|
|
<span class="hidden sm:inline">{{ currentLocale?.code.toUpperCase() }}</span>
|
|
<Icon
|
|
name="chevronDown"
|
|
size="xs"
|
|
class="text-gray-400 transition-transform duration-200"
|
|
:class="{ 'rotate-180': isOpen }"
|
|
/>
|
|
</button>
|
|
|
|
<transition name="dropdown">
|
|
<div
|
|
v-if="isOpen"
|
|
class="absolute right-0 z-50 mt-1 w-32 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-lg dark:border-dark-700 dark:bg-dark-800"
|
|
>
|
|
<button
|
|
v-for="locale in availableLocales"
|
|
:key="locale.code"
|
|
@click="selectLocale(locale.code)"
|
|
class="flex w-full items-center gap-2 px-3 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-dark-700"
|
|
:class="{
|
|
'bg-primary-50 text-primary-600 dark:bg-primary-900/20 dark:text-primary-400':
|
|
locale.code === currentLocaleCode
|
|
}"
|
|
>
|
|
<span class="text-base">{{ locale.flag }}</span>
|
|
<span>{{ locale.name }}</span>
|
|
<Icon v-if="locale.code === currentLocaleCode" name="check" size="sm" class="ml-auto text-primary-500" />
|
|
</button>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import Icon from '@/components/icons/Icon.vue'
|
|
import { setLocale, availableLocales } from '@/i18n'
|
|
|
|
const { locale } = useI18n()
|
|
|
|
const isOpen = ref(false)
|
|
const dropdownRef = ref<HTMLElement | null>(null)
|
|
|
|
const currentLocaleCode = computed(() => locale.value)
|
|
const currentLocale = computed(() => availableLocales.find((l) => l.code === locale.value))
|
|
|
|
function toggleDropdown() {
|
|
isOpen.value = !isOpen.value
|
|
}
|
|
|
|
function selectLocale(code: string) {
|
|
setLocale(code)
|
|
isOpen.value = false
|
|
}
|
|
|
|
function handleClickOutside(event: MouseEvent) {
|
|
if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
|
|
isOpen.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
document.addEventListener('click', handleClickOutside)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
document.removeEventListener('click', handleClickOutside)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.dropdown-enter-active,
|
|
.dropdown-leave-active {
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.dropdown-enter-from,
|
|
.dropdown-leave-to {
|
|
opacity: 0;
|
|
transform: scale(0.95) translateY(-4px);
|
|
}
|
|
</style>
|