mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 18:19:17 +00:00
Revert "Merge pull request #424 from Wangnov/feat/i18n"
This reverts commit1d915d8327, reversing changes made to009f7c84f6.
This commit is contained in:
@@ -41,7 +41,7 @@
|
||||
ref="searchInput"
|
||||
v-model="searchQuery"
|
||||
class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
:placeholder="$t('common.accountSelector.searchPlaceholder')"
|
||||
placeholder="搜索账号名称..."
|
||||
style="padding-left: 40px; padding-right: 36px"
|
||||
type="text"
|
||||
@input="handleSearch"
|
||||
@@ -68,9 +68,7 @@
|
||||
:class="{ 'bg-blue-50 dark:bg-blue-900/20': !modelValue }"
|
||||
@click="selectAccount(null)"
|
||||
>
|
||||
<span class="text-gray-700 dark:text-gray-300">{{
|
||||
props.defaultOptionText || $t('common.accountSelector.useSharedPool')
|
||||
}}</span>
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ defaultOptionText }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 分组选项 -->
|
||||
@@ -78,7 +76,7 @@
|
||||
<div
|
||||
class="bg-gray-50 px-4 py-2 text-xs font-semibold text-gray-500 dark:bg-gray-700 dark:text-gray-400"
|
||||
>
|
||||
{{ $t('common.accountSelector.schedulingGroups') }}
|
||||
调度分组
|
||||
</div>
|
||||
<div
|
||||
v-for="group in filteredGroups"
|
||||
@@ -90,8 +88,7 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ group.name }}</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400"
|
||||
>{{ group.memberCount || 0
|
||||
}}{{ $t('common.accountSelector.membersUnit') }}</span
|
||||
>{{ group.memberCount || 0 }} 个成员</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,8 +101,10 @@
|
||||
>
|
||||
{{
|
||||
platform === 'claude'
|
||||
? $t('common.accountSelector.claudeOAuthAccounts')
|
||||
: $t('common.accountSelector.oauthAccounts')
|
||||
? 'Claude OAuth 专属账号'
|
||||
: platform === 'openai'
|
||||
? 'OpenAI 专属账号'
|
||||
: 'OAuth 专属账号'
|
||||
}}
|
||||
</div>
|
||||
<div
|
||||
@@ -143,7 +142,7 @@
|
||||
<div
|
||||
class="bg-gray-50 px-4 py-2 text-xs font-semibold text-gray-500 dark:bg-gray-700 dark:text-gray-400"
|
||||
>
|
||||
{{ $t('common.accountSelector.claudeConsoleAccounts') }}
|
||||
Claude Console 专属账号
|
||||
</div>
|
||||
<div
|
||||
v-for="account in filteredConsoleAccounts"
|
||||
@@ -177,13 +176,52 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OpenAI-Responses 账号(仅 OpenAI) -->
|
||||
<div v-if="platform === 'openai' && filteredOpenAIResponsesAccounts.length > 0">
|
||||
<div
|
||||
class="bg-gray-50 px-4 py-2 text-xs font-semibold text-gray-500 dark:bg-gray-700 dark:text-gray-400"
|
||||
>
|
||||
OpenAI-Responses 专属账号
|
||||
</div>
|
||||
<div
|
||||
v-for="account in filteredOpenAIResponsesAccounts"
|
||||
:key="account.id"
|
||||
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
:class="{
|
||||
'bg-blue-50 dark:bg-blue-900/20': modelValue === `responses:${account.id}`
|
||||
}"
|
||||
@click="selectAccount(`responses:${account.id}`)"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ account.name }}</span>
|
||||
<span
|
||||
class="ml-2 rounded-full px-2 py-0.5 text-xs"
|
||||
:class="
|
||||
account.isActive === 'true' || account.isActive === true
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
||||
: account.status === 'rate_limited'
|
||||
? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
|
||||
: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
||||
"
|
||||
>
|
||||
{{ getAccountStatusText(account) }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">
|
||||
{{ formatDate(account.createdAt) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 无搜索结果 -->
|
||||
<div
|
||||
v-if="searchQuery && !hasResults"
|
||||
class="px-4 py-8 text-center text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<i class="fas fa-search mb-2 text-2xl" />
|
||||
<p class="text-sm">{{ $t('common.accountSelector.noResultsFound') }}</p>
|
||||
<p class="text-sm">没有找到匹配的账号</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -194,9 +232,6 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t: $t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -206,7 +241,7 @@ const props = defineProps({
|
||||
platform: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: (value) => ['claude', 'gemini'].includes(value)
|
||||
validator: (value) => ['claude', 'gemini', 'openai', 'bedrock'].includes(value)
|
||||
},
|
||||
accounts: {
|
||||
type: Array,
|
||||
@@ -222,11 +257,11 @@ const props = defineProps({
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
default: '请选择账号'
|
||||
},
|
||||
defaultOptionText: {
|
||||
type: String,
|
||||
default: null
|
||||
default: '使用共享账号池'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -243,16 +278,13 @@ const lastDirection = ref('') // 记住上次的显示方向
|
||||
// 获取选中的标签
|
||||
const selectedLabel = computed(() => {
|
||||
// 如果没有选中值,显示默认选项文本
|
||||
if (!props.modelValue)
|
||||
return props.defaultOptionText || $t('common.accountSelector.useSharedPool')
|
||||
if (!props.modelValue) return props.defaultOptionText
|
||||
|
||||
// 分组
|
||||
if (props.modelValue.startsWith('group:')) {
|
||||
const groupId = props.modelValue.substring(6)
|
||||
const group = props.groups.find((g) => g.id === groupId)
|
||||
return group
|
||||
? `${group.name} (${group.memberCount || 0}${$t('common.accountSelector.membersUnit')})`
|
||||
: ''
|
||||
return group ? `${group.name} (${group.memberCount || 0} 个成员)` : ''
|
||||
}
|
||||
|
||||
// Console 账号
|
||||
@@ -264,6 +296,15 @@ const selectedLabel = computed(() => {
|
||||
return account ? `${account.name} (${getAccountStatusText(account)})` : ''
|
||||
}
|
||||
|
||||
// OpenAI-Responses 账号
|
||||
if (props.modelValue.startsWith('responses:')) {
|
||||
const accountId = props.modelValue.substring(10)
|
||||
const account = props.accounts.find(
|
||||
(a) => a.id === accountId && a.platform === 'openai-responses'
|
||||
)
|
||||
return account ? `${account.name} (${getAccountStatusText(account)})` : ''
|
||||
}
|
||||
|
||||
// OAuth 账号
|
||||
const account = props.accounts.find((a) => a.id === props.modelValue)
|
||||
return account ? `${account.name} (${getAccountStatusText(account)})` : ''
|
||||
@@ -271,26 +312,36 @@ const selectedLabel = computed(() => {
|
||||
|
||||
// 获取账户状态文本
|
||||
const getAccountStatusText = (account) => {
|
||||
if (!account) return $t('common.accountSelector.accountStatus.unknown')
|
||||
if (!account) return '未知'
|
||||
|
||||
// 处理 OpenAI-Responses 账号(isActive 可能是字符串)
|
||||
const isActive = account.isActive === 'true' || account.isActive === true
|
||||
|
||||
// 优先使用 isActive 判断
|
||||
if (account.isActive === false) {
|
||||
if (!isActive) {
|
||||
// 根据 status 提供更详细的状态信息
|
||||
switch (account.status) {
|
||||
case 'unauthorized':
|
||||
return $t('common.accountSelector.accountStatus.unauthorized')
|
||||
return '未授权'
|
||||
case 'error':
|
||||
return $t('common.accountSelector.accountStatus.tokenError')
|
||||
return 'Token错误'
|
||||
case 'created':
|
||||
return $t('common.accountSelector.accountStatus.pending')
|
||||
return '待验证'
|
||||
case 'rate_limited':
|
||||
return $t('common.accountSelector.accountStatus.rateLimited')
|
||||
return '限流中'
|
||||
case 'quota_exceeded':
|
||||
return '额度超限'
|
||||
default:
|
||||
return $t('common.accountSelector.accountStatus.error')
|
||||
return '异常'
|
||||
}
|
||||
}
|
||||
|
||||
return $t('common.accountSelector.accountStatus.active')
|
||||
// 对于激活的账号,如果是限流状态也要显示
|
||||
if (account.status === 'rate_limited') {
|
||||
return '限流中'
|
||||
}
|
||||
|
||||
return '正常'
|
||||
}
|
||||
|
||||
// 按创建时间倒序排序账号
|
||||
@@ -302,18 +353,42 @@ const sortedAccounts = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
// 过滤的分组
|
||||
// 过滤的分组(根据平台类型过滤)
|
||||
const filteredGroups = computed(() => {
|
||||
if (!searchQuery.value) return props.groups
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
return props.groups.filter((group) => group.name.toLowerCase().includes(query))
|
||||
// 只显示与当前平台匹配的分组
|
||||
let groups = props.groups.filter((group) => {
|
||||
// 如果分组有platform属性,则必须匹配当前平台
|
||||
// 如果没有platform属性,则认为是旧数据,根据平台判断
|
||||
if (group.platform) {
|
||||
return group.platform === props.platform
|
||||
}
|
||||
// 向后兼容:如果没有platform字段,通过其他方式判断
|
||||
return true
|
||||
})
|
||||
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
groups = groups.filter((group) => group.name.toLowerCase().includes(query))
|
||||
}
|
||||
|
||||
return groups
|
||||
})
|
||||
|
||||
// 过滤的 OAuth 账号
|
||||
const filteredOAuthAccounts = computed(() => {
|
||||
let accounts = sortedAccounts.value.filter((a) =>
|
||||
props.platform === 'claude' ? a.platform === 'claude-oauth' : a.platform !== 'claude-console'
|
||||
)
|
||||
let accounts = []
|
||||
|
||||
if (props.platform === 'claude') {
|
||||
accounts = sortedAccounts.value.filter((a) => a.platform === 'claude-oauth')
|
||||
} else if (props.platform === 'openai') {
|
||||
// 对于 OpenAI,只显示 openai 类型的账号
|
||||
accounts = sortedAccounts.value.filter((a) => a.platform === 'openai')
|
||||
} else {
|
||||
// 其他平台显示所有非特殊类型的账号
|
||||
accounts = sortedAccounts.value.filter(
|
||||
(a) => !['claude-oauth', 'claude-console', 'openai-responses'].includes(a.platform)
|
||||
)
|
||||
}
|
||||
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
@@ -337,12 +412,27 @@ const filteredConsoleAccounts = computed(() => {
|
||||
return accounts
|
||||
})
|
||||
|
||||
// 过滤的 OpenAI-Responses 账号
|
||||
const filteredOpenAIResponsesAccounts = computed(() => {
|
||||
if (props.platform !== 'openai') return []
|
||||
|
||||
let accounts = sortedAccounts.value.filter((a) => a.platform === 'openai-responses')
|
||||
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
accounts = accounts.filter((account) => account.name.toLowerCase().includes(query))
|
||||
}
|
||||
|
||||
return accounts
|
||||
})
|
||||
|
||||
// 是否有搜索结果
|
||||
const hasResults = computed(() => {
|
||||
return (
|
||||
filteredGroups.value.length > 0 ||
|
||||
filteredOAuthAccounts.value.length > 0 ||
|
||||
filteredConsoleAccounts.value.length > 0
|
||||
filteredConsoleAccounts.value.length > 0 ||
|
||||
filteredOpenAIResponsesAccounts.value.length > 0
|
||||
)
|
||||
})
|
||||
|
||||
@@ -354,12 +444,12 @@ const formatDate = (dateString) => {
|
||||
const diffInHours = (now - date) / (1000 * 60 * 60)
|
||||
|
||||
if (diffInHours < 24) {
|
||||
return $t('common.accountSelector.dateFormat.today')
|
||||
return '今天创建'
|
||||
} else if (diffInHours < 48) {
|
||||
return $t('common.accountSelector.dateFormat.yesterday')
|
||||
return '昨天创建'
|
||||
} else if (diffInHours < 168) {
|
||||
// 7天内
|
||||
return `${Math.floor(diffInHours / 24)}${$t('common.accountSelector.dateFormat.daysAgo')}`
|
||||
return `${Math.floor(diffInHours / 24)} 天前`
|
||||
} else {
|
||||
return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })
|
||||
}
|
||||
|
||||
@@ -49,26 +49,22 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
// 状态
|
||||
const isVisible = ref(false)
|
||||
const isProcessing = ref(false)
|
||||
const title = ref('')
|
||||
const message = ref('')
|
||||
const confirmText = ref(t('common.confirmDialog.confirm'))
|
||||
const cancelText = ref(t('common.confirmDialog.cancel'))
|
||||
const confirmText = ref('确认')
|
||||
const cancelText = ref('取消')
|
||||
let resolvePromise = null
|
||||
|
||||
// 显示确认对话框
|
||||
const showConfirm = (
|
||||
titleText,
|
||||
messageText,
|
||||
confirmTextParam = t('common.confirmDialog.confirm'),
|
||||
cancelTextParam = t('common.confirmDialog.cancel')
|
||||
confirmTextParam = '确认',
|
||||
cancelTextParam = '取消'
|
||||
) => {
|
||||
return new Promise((resolve) => {
|
||||
title.value = titleText
|
||||
|
||||
@@ -25,13 +25,13 @@
|
||||
class="flex-1 rounded-xl bg-gray-100 px-4 py-2.5 font-medium text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600"
|
||||
@click="$emit('cancel')"
|
||||
>
|
||||
{{ cancelLabel }}
|
||||
{{ cancelText }}
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 rounded-xl bg-gradient-to-r from-yellow-500 to-orange-500 px-4 py-2.5 font-medium text-white shadow-sm transition-colors hover:from-yellow-600 hover:to-orange-600"
|
||||
@click="$emit('confirm')"
|
||||
>
|
||||
{{ confirmLabel }}
|
||||
{{ confirmText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,12 +40,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
@@ -60,17 +55,14 @@ const props = defineProps({
|
||||
},
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '继续'
|
||||
},
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '取消'
|
||||
}
|
||||
})
|
||||
|
||||
const confirmLabel = computed(() => props.confirmText || t('common.confirmModal.continue'))
|
||||
const cancelLabel = computed(() => props.cancelText || t('common.confirmModal.cancel'))
|
||||
|
||||
defineEmits(['confirm', 'cancel'])
|
||||
</script>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<span
|
||||
class="select-none whitespace-nowrap text-sm font-medium text-gray-700 dark:text-gray-200"
|
||||
>
|
||||
{{ selectedLabel || placeholderText }}
|
||||
{{ selectedLabel || placeholder }}
|
||||
</span>
|
||||
<i
|
||||
:class="[
|
||||
@@ -65,9 +65,6 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -80,7 +77,7 @@ const props = defineProps({
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '请选择'
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
@@ -99,8 +96,6 @@ const triggerRef = ref(null)
|
||||
const dropdownRef = ref(null)
|
||||
const dropdownStyle = ref({})
|
||||
|
||||
const placeholderText = computed(() => props.placeholder || t('common.customDropdown.placeholder'))
|
||||
|
||||
const selectedLabel = computed(() => {
|
||||
const selected = props.options.find((opt) => opt.value === props.modelValue)
|
||||
return selected ? selected.label : ''
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
<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 { useI18n } from 'vue-i18n'
|
||||
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 { t } = useI18n()
|
||||
const localeStore = useLocaleStore()
|
||||
const showDropdown = ref(false)
|
||||
const dropdownTrigger = ref(null)
|
||||
|
||||
// 计算属性
|
||||
const currentLocaleInfo = computed(() => localeStore.getCurrentLocaleInfo(t))
|
||||
const supportedLocales = computed(() => localeStore.getSupportedLocales(t))
|
||||
|
||||
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>
|
||||
@@ -7,7 +7,7 @@
|
||||
<template v-if="!loading">
|
||||
<img
|
||||
v-if="logoSrc"
|
||||
:alt="$t('common.logoTitle.logoAlt')"
|
||||
alt="Logo"
|
||||
class="h-8 w-8 object-contain"
|
||||
:src="logoSrc"
|
||||
@error="handleLogoError"
|
||||
|
||||
@@ -69,7 +69,6 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// Props
|
||||
defineProps({
|
||||
@@ -89,37 +88,32 @@ defineProps({
|
||||
// Store
|
||||
const themeStore = useThemeStore()
|
||||
|
||||
// i18n
|
||||
const { t } = useI18n()
|
||||
|
||||
// 主题选项配置
|
||||
const themeOptions = computed(() => [
|
||||
const themeOptions = [
|
||||
{
|
||||
value: 'light',
|
||||
label: t('common.themeToggle.light.label'),
|
||||
shortLabel: t('common.themeToggle.light.shortLabel'),
|
||||
label: '浅色模式',
|
||||
shortLabel: '浅色',
|
||||
icon: 'fas fa-sun'
|
||||
},
|
||||
{
|
||||
value: 'dark',
|
||||
label: t('common.themeToggle.dark.label'),
|
||||
shortLabel: t('common.themeToggle.dark.shortLabel'),
|
||||
label: '深色模式',
|
||||
shortLabel: '深色',
|
||||
icon: 'fas fa-moon'
|
||||
},
|
||||
{
|
||||
value: 'auto',
|
||||
label: t('common.themeToggle.auto.label'),
|
||||
shortLabel: t('common.themeToggle.auto.shortLabel'),
|
||||
label: '跟随系统',
|
||||
shortLabel: '自动',
|
||||
icon: 'fas fa-circle-half-stroke'
|
||||
}
|
||||
])
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const themeTooltip = computed(() => {
|
||||
const current = themeOptions.value.find((opt) => opt.value === themeStore.themeMode)
|
||||
return current
|
||||
? `${t('common.themeToggle.clickToSwitch')} - ${current.label}`
|
||||
: t('common.themeToggle.toggleTheme')
|
||||
const current = themeOptions.find((opt) => opt.value === themeStore.themeMode)
|
||||
return current ? `点击切换主题 - ${current.label}` : '切换主题'
|
||||
})
|
||||
|
||||
// 方法
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
<i :class="getIconClass(toast.type)" />
|
||||
</div>
|
||||
<div class="toast-body">
|
||||
<div v-if="toast.title || getDefaultTitle(toast.type)" class="toast-title">
|
||||
{{ toast.title || getDefaultTitle(toast.type) }}
|
||||
<div v-if="toast.title" class="toast-title">
|
||||
{{ toast.title }}
|
||||
</div>
|
||||
<div class="toast-message">
|
||||
{{ toast.message }}
|
||||
@@ -35,9 +35,6 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 状态
|
||||
const toasts = ref([])
|
||||
@@ -54,11 +51,6 @@ const getIconClass = (type) => {
|
||||
return iconMap[type] || iconMap.info
|
||||
}
|
||||
|
||||
// 获取默认标题
|
||||
const getDefaultTitle = (type) => {
|
||||
return t(`common.toastNotification.defaultTitles.${type}`)
|
||||
}
|
||||
|
||||
// 添加Toast
|
||||
const addToast = (message, type = 'info', title = null, duration = 5000) => {
|
||||
const id = ++toastIdCounter
|
||||
|
||||
Reference in New Issue
Block a user