fix(frontend): 优化前端组件和国际化支持
- 添加 Accept-Language 请求头支持后端翻译 - 优化账户状态指示器和测试模态框 - 简化用户属性表单和配置模态框 - 新增多个国际化翻译条目 - 重构管理视图代码,提升可维护性
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
|
||||
import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios'
|
||||
import type { ApiResponse } from '@/types'
|
||||
import { getLocale } from '@/i18n'
|
||||
|
||||
// ==================== Axios Instance Configuration ====================
|
||||
|
||||
@@ -27,6 +28,12 @@ apiClient.interceptors.request.use(
|
||||
if (token && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
// Attach locale for backend translations
|
||||
if (config.headers) {
|
||||
config.headers['Accept-Language'] = getLocale()
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -47,6 +47,7 @@ export default {
|
||||
description: 'Configure your Sub2API instance',
|
||||
database: {
|
||||
title: 'Database Configuration',
|
||||
description: 'Connect to your PostgreSQL database',
|
||||
host: 'Host',
|
||||
port: 'Port',
|
||||
username: 'Username',
|
||||
@@ -63,6 +64,7 @@ export default {
|
||||
},
|
||||
redis: {
|
||||
title: 'Redis Configuration',
|
||||
description: 'Connect to your Redis server',
|
||||
host: 'Host',
|
||||
port: 'Port',
|
||||
password: 'Password (optional)',
|
||||
@@ -71,6 +73,7 @@ export default {
|
||||
},
|
||||
admin: {
|
||||
title: 'Admin Account',
|
||||
description: 'Create your administrator account',
|
||||
email: 'Email',
|
||||
password: 'Password',
|
||||
confirmPassword: 'Confirm Password',
|
||||
@@ -80,9 +83,21 @@ export default {
|
||||
},
|
||||
ready: {
|
||||
title: 'Ready to Install',
|
||||
description: 'Review your configuration and complete setup',
|
||||
database: 'Database',
|
||||
redis: 'Redis',
|
||||
adminEmail: 'Admin Email'
|
||||
},
|
||||
status: {
|
||||
testing: 'Testing...',
|
||||
success: 'Connection Successful',
|
||||
testConnection: 'Test Connection',
|
||||
installing: 'Installing...',
|
||||
completeInstallation: 'Complete Installation',
|
||||
completed: 'Installation completed!',
|
||||
redirecting: 'Redirecting to login page...',
|
||||
restarting: 'Service is restarting, please wait...',
|
||||
timeout: 'Service restart is taking longer than expected. Please refresh the page manually.'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -790,6 +805,18 @@ export default {
|
||||
failedToCreate: 'Failed to create group',
|
||||
failedToUpdate: 'Failed to update group',
|
||||
failedToDelete: 'Failed to delete group',
|
||||
platforms: {
|
||||
all: 'All Platforms',
|
||||
anthropic: 'Anthropic',
|
||||
openai: 'OpenAI',
|
||||
gemini: 'Gemini',
|
||||
antigravity: 'Antigravity'
|
||||
},
|
||||
statuses: {
|
||||
active: 'Active',
|
||||
inactive: 'Inactive',
|
||||
error: 'Error'
|
||||
},
|
||||
deleteConfirm:
|
||||
"Are you sure you want to delete '{name}'? All associated API keys will no longer belong to any group.",
|
||||
deleteConfirmSubscription:
|
||||
@@ -954,6 +981,16 @@ export default {
|
||||
tokenRefreshed: 'Token refreshed successfully',
|
||||
accountDeleted: 'Account deleted successfully',
|
||||
rateLimitCleared: 'Rate limit cleared successfully',
|
||||
statuses: {
|
||||
active: 'Active',
|
||||
inactive: 'Inactive',
|
||||
error: 'Error',
|
||||
cooldown: 'Cooldown',
|
||||
paused: 'Paused',
|
||||
limited: 'Limited',
|
||||
rateLimitedUntil: 'Rate limited until {time}',
|
||||
overloadedUntil: 'Overloaded until {time}'
|
||||
},
|
||||
bulkActions: {
|
||||
selected: '{count} account(s) selected',
|
||||
selectCurrentPage: 'Select this page',
|
||||
@@ -1396,6 +1433,17 @@ export default {
|
||||
searchProxies: 'Search proxies...',
|
||||
allProtocols: 'All Protocols',
|
||||
allStatus: 'All Status',
|
||||
protocols: {
|
||||
http: 'HTTP',
|
||||
https: 'HTTPS',
|
||||
socks5: 'SOCKS5',
|
||||
socks5h: 'SOCKS5H (Remote DNS)'
|
||||
},
|
||||
statuses: {
|
||||
active: 'Active',
|
||||
inactive: 'Inactive',
|
||||
error: 'Error'
|
||||
},
|
||||
columns: {
|
||||
name: 'Name',
|
||||
protocol: 'Protocol',
|
||||
|
||||
@@ -44,6 +44,7 @@ export default {
|
||||
description: '配置您的 Sub2API 实例',
|
||||
database: {
|
||||
title: '数据库配置',
|
||||
description: '连接到您的 PostgreSQL 数据库',
|
||||
host: '主机',
|
||||
port: '端口',
|
||||
username: '用户名',
|
||||
@@ -60,6 +61,7 @@ export default {
|
||||
},
|
||||
redis: {
|
||||
title: 'Redis 配置',
|
||||
description: '连接到您的 Redis 服务器',
|
||||
host: '主机',
|
||||
port: '端口',
|
||||
password: '密码(可选)',
|
||||
@@ -68,6 +70,7 @@ export default {
|
||||
},
|
||||
admin: {
|
||||
title: '管理员账户',
|
||||
description: '创建您的管理员账户',
|
||||
email: '邮箱',
|
||||
password: '密码',
|
||||
confirmPassword: '确认密码',
|
||||
@@ -77,9 +80,21 @@ export default {
|
||||
},
|
||||
ready: {
|
||||
title: '准备安装',
|
||||
description: '检查您的配置并完成安装',
|
||||
database: '数据库',
|
||||
redis: 'Redis',
|
||||
adminEmail: '管理员邮箱'
|
||||
},
|
||||
status: {
|
||||
testing: '测试中...',
|
||||
success: '连接成功',
|
||||
testConnection: '测试连接',
|
||||
installing: '安装中...',
|
||||
completeInstallation: '完成安装',
|
||||
completed: '安装完成!',
|
||||
redirecting: '正在跳转到登录页面...',
|
||||
restarting: '服务正在重启,请稍候...',
|
||||
timeout: '服务重启时间超出预期,请手动刷新页面。'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -848,8 +863,15 @@ export default {
|
||||
rateMultiplierHint: '1.0 = 标准费率,0.5 = 半价,2.0 = 双倍',
|
||||
platforms: {
|
||||
all: '全部平台',
|
||||
claude: 'Claude',
|
||||
openai: 'OpenAI'
|
||||
anthropic: 'Anthropic',
|
||||
openai: 'OpenAI',
|
||||
gemini: 'Gemini',
|
||||
antigravity: 'Antigravity'
|
||||
},
|
||||
statuses: {
|
||||
active: '正常',
|
||||
inactive: '停用',
|
||||
error: '错误'
|
||||
},
|
||||
saving: '保存中...',
|
||||
noGroups: '暂无分组',
|
||||
@@ -1054,7 +1076,11 @@ export default {
|
||||
active: '正常',
|
||||
inactive: '停用',
|
||||
error: '错误',
|
||||
cooldown: '冷却中'
|
||||
cooldown: '冷却中',
|
||||
paused: '暂停',
|
||||
limited: '限流',
|
||||
rateLimitedUntil: '限流中,重置时间:{time}',
|
||||
overloadedUntil: '负载过重,重置时间:{time}'
|
||||
},
|
||||
usageWindow: {
|
||||
statsTitle: '5小时窗口用量统计',
|
||||
@@ -1520,7 +1546,8 @@ export default {
|
||||
protocols: {
|
||||
http: 'HTTP',
|
||||
https: 'HTTPS',
|
||||
socks5: 'SOCKS5'
|
||||
socks5: 'SOCKS5',
|
||||
socks5h: 'SOCKS5H (服务端解析 DNS)'
|
||||
},
|
||||
statuses: {
|
||||
active: '正常',
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
{{ t('setup.database.title') }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-dark-400">
|
||||
Connect to your PostgreSQL database
|
||||
{{ t('setup.database.description') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -145,12 +145,15 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('setup.database.sslMode') }}</label>
|
||||
<select v-model="formData.database.sslmode" class="input">
|
||||
<option value="disable">{{ t('setup.database.ssl.disable') }}</option>
|
||||
<option value="require">{{ t('setup.database.ssl.require') }}</option>
|
||||
<option value="verify-ca">{{ t('setup.database.ssl.verifyCa') }}</option>
|
||||
<option value="verify-full">{{ t('setup.database.ssl.verifyFull') }}</option>
|
||||
</select>
|
||||
<Select
|
||||
v-model="formData.database.sslmode"
|
||||
:options="[
|
||||
{ value: 'disable', label: t('setup.database.ssl.disable') },
|
||||
{ value: 'require', label: t('setup.database.ssl.require') },
|
||||
{ value: 'verify-ca', label: t('setup.database.ssl.verifyCa') },
|
||||
{ value: 'verify-full', label: t('setup.database.ssl.verifyFull') }
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -190,7 +193,11 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
{{
|
||||
testingDb ? 'Testing...' : dbConnected ? 'Connection Successful' : 'Test Connection'
|
||||
testingDb
|
||||
? t('setup.status.testing')
|
||||
: dbConnected
|
||||
? t('setup.status.success')
|
||||
: t('setup.status.testConnection')
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
@@ -202,7 +209,7 @@
|
||||
{{ t('setup.redis.title') }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-dark-400">
|
||||
Connect to your Redis server
|
||||
{{ t('setup.redis.description') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -285,10 +292,10 @@
|
||||
</svg>
|
||||
{{
|
||||
testingRedis
|
||||
? 'Testing...'
|
||||
? t('setup.status.testing')
|
||||
: redisConnected
|
||||
? 'Connection Successful'
|
||||
: 'Test Connection'
|
||||
? t('setup.status.success')
|
||||
: t('setup.status.testConnection')
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
@@ -300,7 +307,7 @@
|
||||
{{ t('setup.admin.title') }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-dark-400">
|
||||
Create your administrator account
|
||||
{{ t('setup.admin.description') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -348,7 +355,7 @@
|
||||
{{ t('setup.ready.title') }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-dark-400">
|
||||
Review your configuration and complete setup
|
||||
{{ t('setup.ready.description') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -447,13 +454,13 @@
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-green-700 dark:text-green-400">
|
||||
Installation completed!
|
||||
{{ t('setup.status.completed') }}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-green-600 dark:text-green-500">
|
||||
{{
|
||||
serviceReady
|
||||
? 'Redirecting to login page...'
|
||||
: 'Service is restarting, please wait...'
|
||||
? t('setup.status.redirecting')
|
||||
: t('setup.status.restarting')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
@@ -480,7 +487,7 @@
|
||||
d="M15.75 19.5L8.25 12l7.5-7.5"
|
||||
/>
|
||||
</svg>
|
||||
Previous
|
||||
{{ t('common.back') }}
|
||||
</button>
|
||||
<div v-else></div>
|
||||
|
||||
@@ -490,7 +497,7 @@
|
||||
:disabled="!canProceed"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Next
|
||||
{{ t('common.next') }}
|
||||
<svg
|
||||
class="ml-2 h-4 w-4"
|
||||
fill="none"
|
||||
@@ -528,7 +535,7 @@
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
{{ installing ? 'Installing...' : 'Complete Installation' }}
|
||||
{{ installing ? t('setup.status.installing') : t('setup.status.completeInstallation') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -540,15 +547,16 @@
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { testDatabase, testRedis, install, type InstallRequest } from '@/api/setup'
|
||||
import Select from '@/components/common/Select.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const steps = [
|
||||
{ id: 'database', title: 'Database' },
|
||||
{ id: 'redis', title: 'Redis' },
|
||||
{ id: 'admin', title: 'Admin' },
|
||||
{ id: 'complete', title: 'Complete' }
|
||||
]
|
||||
const steps = computed(() => [
|
||||
{ id: 'database', title: t('setup.database.title') },
|
||||
{ id: 'redis', title: t('setup.redis.title') },
|
||||
{ id: 'admin', title: t('setup.admin.title') },
|
||||
{ id: 'complete', title: t('setup.ready.title') }
|
||||
])
|
||||
|
||||
const currentStep = ref(0)
|
||||
const errorMessage = ref('')
|
||||
@@ -710,7 +718,6 @@ async function waitForServiceRestart() {
|
||||
|
||||
// If we reach here, service didn't restart in time
|
||||
// Show a message to refresh manually
|
||||
errorMessage.value =
|
||||
'Service restart is taking longer than expected. Please refresh the page manually.'
|
||||
errorMessage.value = t('setup.status.timeout')
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user