fix(frontend): 优化前端组件和国际化支持

- 添加 Accept-Language 请求头支持后端翻译
- 优化账户状态指示器和测试模态框
- 简化用户属性表单和配置模态框
- 新增多个国际化翻译条目
- 重构管理视图代码,提升可维护性
This commit is contained in:
IanShaw027
2026-01-04 20:49:34 +08:00
parent 4df712624e
commit 6c036d7b59
14 changed files with 189 additions and 142 deletions

View File

@@ -52,7 +52,7 @@
<div
class="pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 -translate-x-1/2 whitespace-nowrap rounded bg-gray-900 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
>
Rate limited until {{ formatTime(account.rate_limit_reset_at) }}
{{ t('admin.accounts.statuses.rateLimitedUntil', { time: formatTime(account.rate_limit_reset_at) }) }}
<div
class="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"
></div>
@@ -77,7 +77,7 @@
<div
class="pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 -translate-x-1/2 whitespace-nowrap rounded bg-gray-900 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
>
Overloaded until {{ formatTime(account.overload_until) }}
{{ t('admin.accounts.statuses.overloadedUntil', { time: formatTime(account.overload_until) }) }}
<div
class="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"
></div>
@@ -96,9 +96,12 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { Account } from '@/types'
import { formatTime } from '@/utils/format'
const { t } = useI18n()
const props = defineProps<{
account: Account
}>()
@@ -140,12 +143,12 @@ const statusClass = computed(() => {
// Computed: status text
const statusText = computed(() => {
if (!props.account.schedulable) {
return 'Paused'
return t('admin.accounts.statuses.paused')
}
if (isRateLimited.value || isOverloaded.value) {
return 'Limited'
return t('admin.accounts.statuses.limited')
}
return props.account.status
return t(`admin.accounts.statuses.${props.account.status}`)
})
// Computed: tier display

View File

@@ -48,21 +48,18 @@
</span>
</div>
<!-- Model Selection -->
<div class="space-y-1.5">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('admin.accounts.selectTestModel') }}
</label>
<select
<Select
v-model="selectedModelId"
:options="availableModels"
:disabled="loadingModels || status === 'connecting'"
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-primary-500 focus:ring-2 focus:ring-primary-500 disabled:cursor-not-allowed disabled:opacity-50 dark:border-dark-500 dark:bg-dark-700 dark:text-gray-100"
>
<option v-if="loadingModels" value="">{{ t('common.loading') }}...</option>
<option v-for="model in availableModels" :key="model.id" :value="model.id">
{{ model.display_name }} ({{ model.id }})
</option>
</select>
value-key="id"
label-key="display_name"
:placeholder="loadingModels ? t('common.loading') + '...' : t('admin.accounts.selectTestModel')"
/>
</div>
<!-- Terminal Output -->
@@ -280,6 +277,7 @@
import { ref, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseDialog from '@/components/common/BaseDialog.vue'
import Select from '@/components/common/Select.vue'
import { useClipboard } from '@/composables/useClipboard'
import { adminAPI } from '@/api/admin'
import type { Account, ClaudeModel } from '@/types'

View File

@@ -52,18 +52,12 @@
/>
<!-- Select -->
<select
<Select
v-else-if="attr.type === 'select'"
v-model="localValues[attr.id]"
:required="attr.required"
class="input"
:options="attr.options || []"
@change="emitChange"
>
<option value="">{{ t('common.selectOption') }}</option>
<option v-for="opt in attr.options" :key="opt.value" :value="opt.value">
{{ opt.label }}
</option>
</select>
/>
<!-- Multi-Select (Checkboxes) -->
<div v-else-if="attr.type === 'multi_select'" class="space-y-2">
@@ -102,6 +96,7 @@ import { ref, watch, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { adminAPI } from '@/api/admin'
import type { UserAttributeDefinition, UserAttributeValuesMap } from '@/types'
import Select from '@/components/common/Select.vue'
const { t } = useI18n()

View File

@@ -142,11 +142,10 @@
<!-- Type -->
<div>
<label class="input-label">{{ t('admin.users.attributes.type') }}</label>
<select v-model="form.type" class="input" required>
<option v-for="type in attributeTypes" :key="type" :value="type">
{{ t(`admin.users.attributes.types.${type}`) }}
</option>
</select>
<Select
v-model="form.type"
:options="attributeTypes.map(type => ({ value: type, label: t(`admin.users.attributes.types.${type}`) }))"
/>
</div>
<!-- Options (for select/multi_select) -->
@@ -257,6 +256,7 @@ import { adminAPI } from '@/api/admin'
import type { UserAttributeDefinition, UserAttributeType, UserAttributeOption } from '@/types'
import BaseDialog from '@/components/common/BaseDialog.vue'
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
import Select from '@/components/common/Select.vue'
const { t } = useI18n()
const appStore = useAppStore()