feat: 完成布局/仪表板/用户相关组件国际化与语言切换优化(TabBar/MainLayout/AppHeader、UsageTrend/ModelDistribution、User*、Common 组件、i18n/locale 增强)

This commit is contained in:
Wangnov
2025-09-10 18:13:27 +08:00
parent 022724336b
commit d5b9f809b0
20 changed files with 410 additions and 300 deletions

View File

@@ -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="搜索账号名称..."
:placeholder="$t('common.accountSelector.searchPlaceholder')"
style="padding-left: 40px; padding-right: 36px"
type="text"
@input="handleSearch"
@@ -68,7 +68,9 @@
:class="{ 'bg-blue-50 dark:bg-blue-900/20': !modelValue }"
@click="selectAccount(null)"
>
<span class="text-gray-700 dark:text-gray-300">{{ defaultOptionText }}</span>
<span class="text-gray-700 dark:text-gray-300">{{
props.defaultOptionText || $t('common.accountSelector.useSharedPool')
}}</span>
</div>
<!-- 分组选项 -->
@@ -76,7 +78,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"
@@ -88,7 +90,8 @@
<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 }} 个成员</span
>{{ group.memberCount || 0
}}{{ $t('common.accountSelector.membersUnit') }}</span
>
</div>
</div>
@@ -101,10 +104,8 @@
>
{{
platform === 'claude'
? 'Claude OAuth 专属账号'
: platform === 'openai'
? 'OpenAI 专属账号'
: 'OAuth 专属账号'
? $t('common.accountSelector.claudeOAuthAccounts')
: $t('common.accountSelector.oauthAccounts')
}}
</div>
<div
@@ -142,7 +143,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"
>
Claude Console 专属账号
{{ $t('common.accountSelector.claudeConsoleAccounts') }}
</div>
<div
v-for="account in filteredConsoleAccounts"
@@ -176,52 +177,13 @@
</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">没有找到匹配的账号</p>
<p class="text-sm">{{ $t('common.accountSelector.noResultsFound') }}</p>
</div>
</div>
</div>
@@ -232,6 +194,9 @@
<script setup>
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
const { t: $t } = useI18n()
const props = defineProps({
modelValue: {
@@ -241,7 +206,7 @@ const props = defineProps({
platform: {
type: String,
required: true,
validator: (value) => ['claude', 'gemini', 'openai', 'bedrock'].includes(value)
validator: (value) => ['claude', 'gemini'].includes(value)
},
accounts: {
type: Array,
@@ -257,11 +222,11 @@ const props = defineProps({
},
placeholder: {
type: String,
default: '请选择账号'
default: null
},
defaultOptionText: {
type: String,
default: '使用共享账号池'
default: null
}
})
@@ -278,13 +243,16 @@ const lastDirection = ref('') // 记住上次的显示方向
// 获取选中的标签
const selectedLabel = computed(() => {
// 如果没有选中值,显示默认选项文本
if (!props.modelValue) return props.defaultOptionText
if (!props.modelValue)
return props.defaultOptionText || $t('common.accountSelector.useSharedPool')
// 分组
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} 个成员)` : ''
return group
? `${group.name} (${group.memberCount || 0}${$t('common.accountSelector.membersUnit')})`
: ''
}
// Console 账号
@@ -296,15 +264,6 @@ 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)})` : ''
@@ -312,36 +271,26 @@ const selectedLabel = computed(() => {
// 获取账户状态文本
const getAccountStatusText = (account) => {
if (!account) return '未知'
// 处理 OpenAI-Responses 账号isActive 可能是字符串)
const isActive = account.isActive === 'true' || account.isActive === true
if (!account) return $t('common.accountSelector.accountStatus.unknown')
// 优先使用 isActive 判断
if (!isActive) {
if (account.isActive === false) {
// 根据 status 提供更详细的状态信息
switch (account.status) {
case 'unauthorized':
return '未授权'
return $t('common.accountSelector.accountStatus.unauthorized')
case 'error':
return 'Token错误'
return $t('common.accountSelector.accountStatus.tokenError')
case 'created':
return '待验证'
return $t('common.accountSelector.accountStatus.pending')
case 'rate_limited':
return '限流中'
case 'quota_exceeded':
return '额度超限'
return $t('common.accountSelector.accountStatus.rateLimited')
default:
return '异常'
return $t('common.accountSelector.accountStatus.error')
}
}
// 对于激活的账号,如果是限流状态也要显示
if (account.status === 'rate_limited') {
return '限流中'
}
return '正常'
return $t('common.accountSelector.accountStatus.active')
}
// 按创建时间倒序排序账号
@@ -353,42 +302,18 @@ const sortedAccounts = computed(() => {
})
})
// 过滤的分组(根据平台类型过滤)
// 过滤的分组
const filteredGroups = computed(() => {
// 只显示与当前平台匹配的分组
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
if (!searchQuery.value) return props.groups
const query = searchQuery.value.toLowerCase()
return props.groups.filter((group) => group.name.toLowerCase().includes(query))
})
// 过滤的 OAuth 账号
const filteredOAuthAccounts = computed(() => {
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)
)
}
let accounts = sortedAccounts.value.filter((a) =>
props.platform === 'claude' ? a.platform === 'claude-oauth' : a.platform !== 'claude-console'
)
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
@@ -412,27 +337,12 @@ 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 ||
filteredOpenAIResponsesAccounts.value.length > 0
filteredConsoleAccounts.value.length > 0
)
})
@@ -444,12 +354,12 @@ const formatDate = (dateString) => {
const diffInHours = (now - date) / (1000 * 60 * 60)
if (diffInHours < 24) {
return '今天创建'
return $t('common.accountSelector.dateFormat.today')
} else if (diffInHours < 48) {
return '昨天创建'
return $t('common.accountSelector.dateFormat.yesterday')
} else if (diffInHours < 168) {
// 7天内
return `${Math.floor(diffInHours / 24)} 天前`
return `${Math.floor(diffInHours / 24)}${$t('common.accountSelector.dateFormat.daysAgo')}`
} else {
return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })
}

View File

@@ -49,22 +49,26 @@
<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('确认')
const cancelText = ref('取消')
const confirmText = ref(t('common.confirmDialog.confirm'))
const cancelText = ref(t('common.confirmDialog.cancel'))
let resolvePromise = null
// 显示确认对话框
const showConfirm = (
titleText,
messageText,
confirmTextParam = '确认',
cancelTextParam = '取消'
confirmTextParam = t('common.confirmDialog.confirm'),
cancelTextParam = t('common.confirmDialog.cancel')
) => {
return new Promise((resolve) => {
title.value = titleText

View File

@@ -40,6 +40,10 @@
</template>
<script setup>
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
defineProps({
show: {
type: Boolean,
@@ -55,11 +59,11 @@ defineProps({
},
confirmText: {
type: String,
default: '继续'
default: () => t('common.confirmModal.continue')
},
cancelText: {
type: String,
default: '取消'
default: () => t('common.confirmModal.cancel')
}
})

View File

@@ -65,6 +65,9 @@
<script setup>
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({
modelValue: {
@@ -77,7 +80,7 @@ const props = defineProps({
},
placeholder: {
type: String,
default: '请选择'
default: () => t('common.customDropdown.placeholder')
},
icon: {
type: String,

View File

@@ -110,6 +110,7 @@
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useLocaleStore } from '@/stores/locale'
// 定义组件属性
@@ -130,13 +131,14 @@ const props = defineProps({
const emit = defineEmits(['change'])
// 存储和响应式数据
const { t } = useI18n()
const localeStore = useLocaleStore()
const showDropdown = ref(false)
const dropdownTrigger = ref(null)
// 计算属性
const currentLocaleInfo = computed(() => localeStore.getCurrentLocaleInfo())
const supportedLocales = computed(() => localeStore.getSupportedLocales())
const currentLocaleInfo = computed(() => localeStore.getCurrentLocaleInfo(t))
const supportedLocales = computed(() => localeStore.getSupportedLocales(t))
const containerClass = computed(() => {
const classes = []

View File

@@ -7,7 +7,7 @@
<template v-if="!loading">
<img
v-if="logoSrc"
alt="Logo"
:alt="$t('common.logoTitle.logoAlt')"
class="h-8 w-8 object-contain"
:src="logoSrc"
@error="handleLogoError"

View File

@@ -69,6 +69,7 @@
<script setup>
import { computed } from 'vue'
import { useThemeStore } from '@/stores/theme'
import { useI18n } from 'vue-i18n'
// Props
defineProps({
@@ -88,32 +89,37 @@ defineProps({
// Store
const themeStore = useThemeStore()
// i18n
const { t } = useI18n()
// 主题选项配置
const themeOptions = [
const themeOptions = computed(() => [
{
value: 'light',
label: '浅色模式',
shortLabel: '浅色',
label: t('common.themeToggle.light.label'),
shortLabel: t('common.themeToggle.light.shortLabel'),
icon: 'fas fa-sun'
},
{
value: 'dark',
label: '深色模式',
shortLabel: '深色',
label: t('common.themeToggle.dark.label'),
shortLabel: t('common.themeToggle.dark.shortLabel'),
icon: 'fas fa-moon'
},
{
value: 'auto',
label: '跟随系统',
shortLabel: '自动',
label: t('common.themeToggle.auto.label'),
shortLabel: t('common.themeToggle.auto.shortLabel'),
icon: 'fas fa-circle-half-stroke'
}
]
])
// 计算属性
const themeTooltip = computed(() => {
const current = themeOptions.find((opt) => opt.value === themeStore.themeMode)
return current ? `点击切换主题 - ${current.label}` : '切换主题'
const current = themeOptions.value.find((opt) => opt.value === themeStore.themeMode)
return current
? `${t('common.themeToggle.clickToSwitch')} - ${current.label}`
: t('common.themeToggle.toggleTheme')
})
// 方法

View File

@@ -12,8 +12,8 @@
<i :class="getIconClass(toast.type)" />
</div>
<div class="toast-body">
<div v-if="toast.title" class="toast-title">
{{ toast.title }}
<div v-if="toast.title || getDefaultTitle(toast.type)" class="toast-title">
{{ toast.title || getDefaultTitle(toast.type) }}
</div>
<div class="toast-message">
{{ toast.message }}
@@ -35,6 +35,9 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
// 状态
const toasts = ref([])
@@ -51,6 +54,11 @@ 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