fix(frontend): 优化前端组件和国际化支持
- 添加 Accept-Language 请求头支持后端翻译 - 优化账户状态指示器和测试模态框 - 简化用户属性表单和配置模态框 - 新增多个国际化翻译条目 - 重构管理视图代码,提升可维护性
This commit is contained in:
@@ -504,7 +504,7 @@ const userTrendChartData = computed(() => {
|
||||
if (email && email.includes('@')) {
|
||||
return email.split('@')[0]
|
||||
}
|
||||
return `User #${userId}`
|
||||
return t('admin.redeem.userPrefix', { id: userId })
|
||||
}
|
||||
|
||||
// Group by user
|
||||
|
||||
@@ -88,15 +88,7 @@
|
||||
]"
|
||||
>
|
||||
<PlatformIcon :platform="value" size="xs" />
|
||||
{{
|
||||
value === 'anthropic'
|
||||
? 'Anthropic'
|
||||
: value === 'openai'
|
||||
? 'OpenAI'
|
||||
: value === 'antigravity'
|
||||
? 'Antigravity'
|
||||
: 'Gemini'
|
||||
}}
|
||||
{{ t('admin.groups.platforms.' + value) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -172,7 +164,7 @@
|
||||
|
||||
<template #cell-status="{ value }">
|
||||
<span :class="['badge', value === 'active' ? 'badge-success' : 'badge-danger']">
|
||||
{{ value }}
|
||||
{{ t('admin.groups.statuses.' + value) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
|
||||
<template #cell-status="{ value }">
|
||||
<span :class="['badge', value === 'active' ? 'badge-success' : 'badge-danger']">
|
||||
{{ value }}
|
||||
{{ t('admin.proxies.statuses.' + value) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -639,16 +639,16 @@ const statusOptions = computed(() => [
|
||||
])
|
||||
|
||||
// Form options
|
||||
const protocolSelectOptions = [
|
||||
{ value: 'http', label: 'HTTP' },
|
||||
{ value: 'https', label: 'HTTPS' },
|
||||
{ value: 'socks5', label: 'SOCKS5' },
|
||||
{ value: 'socks5h', label: 'SOCKS5H (服务端解析DNS)' }
|
||||
]
|
||||
const protocolSelectOptions = computed(() => [
|
||||
{ value: 'http', label: t('admin.proxies.protocols.http') },
|
||||
{ value: 'https', label: t('admin.proxies.protocols.https') },
|
||||
{ value: 'socks5', label: t('admin.proxies.protocols.socks5') },
|
||||
{ value: 'socks5h', label: t('admin.proxies.protocols.socks5h') }
|
||||
])
|
||||
|
||||
const editStatusOptions = computed(() => [
|
||||
{ value: 'active', label: t('common.active') },
|
||||
{ value: 'inactive', label: t('common.inactive') }
|
||||
{ value: 'active', label: t('admin.proxies.statuses.active') },
|
||||
{ value: 'inactive', label: t('admin.proxies.statuses.inactive') }
|
||||
])
|
||||
|
||||
const proxies = ref<Proxy[]>([])
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
: 'badge-primary'
|
||||
]"
|
||||
>
|
||||
{{ value }}
|
||||
{{ t('admin.redeem.types.' + value) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
<template v-if="row.type === 'balance'">${{ value.toFixed(2) }}</template>
|
||||
<template v-else-if="row.type === 'subscription'">
|
||||
{{ row.validity_days || 30 }}{{ t('admin.redeem.days') }}
|
||||
{{ row.validity_days || 30 }} {{ t('admin.redeem.days') }}
|
||||
<span v-if="row.group" class="ml-1 text-xs text-gray-500 dark:text-gray-400"
|
||||
>({{ row.group.name }})</span
|
||||
>
|
||||
@@ -140,7 +140,7 @@
|
||||
: 'badge-danger'
|
||||
]"
|
||||
>
|
||||
{{ value }}
|
||||
{{ t('admin.redeem.statuses.' + value) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{
|
||||
row.user?.email || `User #${row.user_id}`
|
||||
row.user?.email || t('admin.redeem.userPrefix', { id: row.user_id })
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -31,47 +31,29 @@
|
||||
</div>
|
||||
|
||||
<!-- Role Filter (visible when enabled) -->
|
||||
<div v-if="visibleFilters.has('role')" class="relative">
|
||||
<select
|
||||
<div v-if="visibleFilters.has('role')" class="w-32">
|
||||
<Select
|
||||
v-model="filters.role"
|
||||
:options="[
|
||||
{ value: '', label: t('admin.users.allRoles') },
|
||||
{ value: 'admin', label: t('admin.users.admin') },
|
||||
{ value: 'user', label: t('admin.users.user') }
|
||||
]"
|
||||
@change="applyFilter"
|
||||
class="input w-32 cursor-pointer appearance-none pr-8"
|
||||
>
|
||||
<option value="">{{ t('admin.users.allRoles') }}</option>
|
||||
<option value="admin">{{ t('admin.users.admin') }}</option>
|
||||
<option value="user">{{ t('admin.users.user') }}</option>
|
||||
</select>
|
||||
<svg
|
||||
class="pointer-events-none absolute right-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Status Filter (visible when enabled) -->
|
||||
<div v-if="visibleFilters.has('status')" class="relative">
|
||||
<select
|
||||
<div v-if="visibleFilters.has('status')" class="w-32">
|
||||
<Select
|
||||
v-model="filters.status"
|
||||
:options="[
|
||||
{ value: '', label: t('admin.users.allStatus') },
|
||||
{ value: 'active', label: t('common.active') },
|
||||
{ value: 'disabled', label: t('admin.users.disabled') }
|
||||
]"
|
||||
@change="applyFilter"
|
||||
class="input w-32 cursor-pointer appearance-none pr-8"
|
||||
>
|
||||
<option value="">{{ t('admin.users.allStatus') }}</option>
|
||||
<option value="active">{{ t('common.active') }}</option>
|
||||
<option value="disabled">{{ t('admin.users.disabled') }}</option>
|
||||
</select>
|
||||
<svg
|
||||
class="pointer-events-none absolute right-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Attribute Filters -->
|
||||
@@ -98,29 +80,16 @@
|
||||
/>
|
||||
<!-- Select/Multi-select type -->
|
||||
<template v-else-if="['select', 'multi_select'].includes(getAttributeDefinition(Number(attrId))?.type || '')">
|
||||
<select
|
||||
:value="value"
|
||||
@change="(e) => { updateAttributeFilter(Number(attrId), (e.target as HTMLSelectElement).value); applyFilter() }"
|
||||
class="input w-36 cursor-pointer appearance-none pr-8"
|
||||
>
|
||||
<option value="">{{ getAttributeDefinitionName(Number(attrId)) }}</option>
|
||||
<option
|
||||
v-for="opt in getAttributeDefinition(Number(attrId))?.options || []"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
<svg
|
||||
class="pointer-events-none absolute right-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
<div class="w-36">
|
||||
<Select
|
||||
:model-value="value"
|
||||
:options="[
|
||||
{ value: '', label: getAttributeDefinitionName(Number(attrId)) },
|
||||
...(getAttributeDefinition(Number(attrId))?.options || [])
|
||||
]"
|
||||
@update:model-value="(val) => { updateAttributeFilter(Number(attrId), String(val ?? '')); applyFilter() }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Fallback -->
|
||||
<input
|
||||
@@ -337,7 +306,7 @@
|
||||
|
||||
<template #cell-role="{ value }">
|
||||
<span :class="['badge', value === 'admin' ? 'badge-purple' : 'badge-gray']">
|
||||
{{ value }}
|
||||
{{ t('admin.users.roles.' + value) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -1419,6 +1388,7 @@ import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
|
||||
import EmptyState from '@/components/common/EmptyState.vue'
|
||||
import GroupBadge from '@/components/common/GroupBadge.vue'
|
||||
import Select from '@/components/common/Select.vue'
|
||||
import UserAttributesConfigModal from '@/components/user/UserAttributesConfigModal.vue'
|
||||
import UserAttributeForm from '@/components/user/UserAttributeForm.vue'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user