Commit 506cb21c authored by IanShaw027's avatar IanShaw027
Browse files

refactor(frontend): UI/UX改进和组件优化

- DataTable组件操作列自适应
- 优化各种Modal弹窗
- 统一API调用方式(AbortSignal)
- 添加全局订阅状态管理
- 优化各管理视图的交互和布局
- 修复国际化翻译问题
parent 9bbe468c
...@@ -18,8 +18,10 @@ func DefaultModels() []Model { ...@@ -18,8 +18,10 @@ func DefaultModels() []Model {
methods := []string{"generateContent", "streamGenerateContent"} methods := []string{"generateContent", "streamGenerateContent"}
return []Model{ return []Model{
{Name: "models/gemini-3-pro-preview", SupportedGenerationMethods: methods}, {Name: "models/gemini-3-pro-preview", SupportedGenerationMethods: methods},
{Name: "models/gemini-3-flash-preview", SupportedGenerationMethods: methods},
{Name: "models/gemini-2.5-pro", SupportedGenerationMethods: methods},
{Name: "models/gemini-2.5-flash", SupportedGenerationMethods: methods},
{Name: "models/gemini-2.0-flash", SupportedGenerationMethods: methods}, {Name: "models/gemini-2.0-flash", SupportedGenerationMethods: methods},
{Name: "models/gemini-2.0-flash-lite", SupportedGenerationMethods: methods},
{Name: "models/gemini-1.5-pro", SupportedGenerationMethods: methods}, {Name: "models/gemini-1.5-pro", SupportedGenerationMethods: methods},
{Name: "models/gemini-1.5-flash", SupportedGenerationMethods: methods}, {Name: "models/gemini-1.5-flash", SupportedGenerationMethods: methods},
{Name: "models/gemini-1.5-flash-8b", SupportedGenerationMethods: methods}, {Name: "models/gemini-1.5-flash-8b", SupportedGenerationMethods: methods},
......
...@@ -11,11 +11,11 @@ type Model struct { ...@@ -11,11 +11,11 @@ type Model struct {
// DefaultModels is the curated Gemini model list used by the admin UI "test account" flow. // DefaultModels is the curated Gemini model list used by the admin UI "test account" flow.
var DefaultModels = []Model{ var DefaultModels = []Model{
{ID: "gemini-3-pro", Type: "model", DisplayName: "Gemini 3 Pro", CreatedAt: ""}, {ID: "gemini-3-pro-preview", Type: "model", DisplayName: "Gemini 3 Pro Preview", CreatedAt: ""},
{ID: "gemini-3-flash", Type: "model", DisplayName: "Gemini 3 Flash", CreatedAt: ""}, {ID: "gemini-3-flash-preview", Type: "model", DisplayName: "Gemini 3 Flash Preview", CreatedAt: ""},
{ID: "gemini-2.5-pro", Type: "model", DisplayName: "Gemini 2.5 Pro", CreatedAt: ""}, {ID: "gemini-2.5-pro", Type: "model", DisplayName: "Gemini 2.5 Pro", CreatedAt: ""},
{ID: "gemini-2.5-flash", Type: "model", DisplayName: "Gemini 2.5 Flash", CreatedAt: ""}, {ID: "gemini-2.5-flash", Type: "model", DisplayName: "Gemini 2.5 Flash", CreatedAt: ""},
} }
// DefaultTestModel is the default model to preselect in test flows. // DefaultTestModel is the default model to preselect in test flows.
const DefaultTestModel = "gemini-2.5-pro" const DefaultTestModel = "gemini-3-pro-preview"
...@@ -2,12 +2,14 @@ ...@@ -2,12 +2,14 @@
import { RouterView, useRouter, useRoute } from 'vue-router' import { RouterView, useRouter, useRoute } from 'vue-router'
import { onMounted, watch } from 'vue' import { onMounted, watch } from 'vue'
import Toast from '@/components/common/Toast.vue' import Toast from '@/components/common/Toast.vue'
import { useAppStore } from '@/stores' import { useAppStore, useAuthStore, useSubscriptionStore } from '@/stores'
import { getSetupStatus } from '@/api/setup' import { getSetupStatus } from '@/api/setup'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const appStore = useAppStore() const appStore = useAppStore()
const authStore = useAuthStore()
const subscriptionStore = useSubscriptionStore()
/** /**
* Update favicon dynamically * Update favicon dynamically
...@@ -46,6 +48,24 @@ watch( ...@@ -46,6 +48,24 @@ watch(
{ immediate: true } { immediate: true }
) )
// Watch for authentication state and manage subscription data
watch(
() => authStore.isAuthenticated,
(isAuthenticated) => {
if (isAuthenticated) {
// User logged in: preload subscriptions and start polling
subscriptionStore.fetchActiveSubscriptions().catch((error) => {
console.error('Failed to preload subscriptions:', error)
})
subscriptionStore.startPolling()
} else {
// User logged out: clear data and stop polling
subscriptionStore.clear()
}
},
{ immediate: true }
)
onMounted(async () => { onMounted(async () => {
// Check if setup is needed // Check if setup is needed
try { try {
......
...@@ -30,6 +30,9 @@ export async function list( ...@@ -30,6 +30,9 @@ export async function list(
type?: string type?: string
status?: string status?: string
search?: string search?: string
},
options?: {
signal?: AbortSignal
} }
): Promise<PaginatedResponse<Account>> { ): Promise<PaginatedResponse<Account>> {
const { data } = await apiClient.get<PaginatedResponse<Account>>('/admin/accounts', { const { data } = await apiClient.get<PaginatedResponse<Account>>('/admin/accounts', {
...@@ -37,7 +40,8 @@ export async function list( ...@@ -37,7 +40,8 @@ export async function list(
page, page,
page_size: pageSize, page_size: pageSize,
...filters ...filters
} },
signal: options?.signal
}) })
return data return data
} }
......
...@@ -26,6 +26,9 @@ export async function list( ...@@ -26,6 +26,9 @@ export async function list(
platform?: GroupPlatform platform?: GroupPlatform
status?: 'active' | 'inactive' status?: 'active' | 'inactive'
is_exclusive?: boolean is_exclusive?: boolean
},
options?: {
signal?: AbortSignal
} }
): Promise<PaginatedResponse<Group>> { ): Promise<PaginatedResponse<Group>> {
const { data } = await apiClient.get<PaginatedResponse<Group>>('/admin/groups', { const { data } = await apiClient.get<PaginatedResponse<Group>>('/admin/groups', {
...@@ -33,7 +36,8 @@ export async function list( ...@@ -33,7 +36,8 @@ export async function list(
page, page,
page_size: pageSize, page_size: pageSize,
...filters ...filters
} },
signal: options?.signal
}) })
return data return data
} }
......
...@@ -20,6 +20,9 @@ export async function list( ...@@ -20,6 +20,9 @@ export async function list(
protocol?: string protocol?: string
status?: 'active' | 'inactive' status?: 'active' | 'inactive'
search?: string search?: string
},
options?: {
signal?: AbortSignal
} }
): Promise<PaginatedResponse<Proxy>> { ): Promise<PaginatedResponse<Proxy>> {
const { data } = await apiClient.get<PaginatedResponse<Proxy>>('/admin/proxies', { const { data } = await apiClient.get<PaginatedResponse<Proxy>>('/admin/proxies', {
...@@ -27,7 +30,8 @@ export async function list( ...@@ -27,7 +30,8 @@ export async function list(
page, page,
page_size: pageSize, page_size: pageSize,
...filters ...filters
} },
signal: options?.signal
}) })
return data return data
} }
......
...@@ -25,6 +25,9 @@ export async function list( ...@@ -25,6 +25,9 @@ export async function list(
type?: RedeemCodeType type?: RedeemCodeType
status?: 'active' | 'used' | 'expired' | 'unused' status?: 'active' | 'used' | 'expired' | 'unused'
search?: string search?: string
},
options?: {
signal?: AbortSignal
} }
): Promise<PaginatedResponse<RedeemCode>> { ): Promise<PaginatedResponse<RedeemCode>> {
const { data } = await apiClient.get<PaginatedResponse<RedeemCode>>('/admin/redeem-codes', { const { data } = await apiClient.get<PaginatedResponse<RedeemCode>>('/admin/redeem-codes', {
...@@ -32,7 +35,8 @@ export async function list( ...@@ -32,7 +35,8 @@ export async function list(
page, page,
page_size: pageSize, page_size: pageSize,
...filters ...filters
} },
signal: options?.signal
}) })
return data return data
} }
......
...@@ -27,6 +27,9 @@ export async function list( ...@@ -27,6 +27,9 @@ export async function list(
status?: 'active' | 'expired' | 'revoked' status?: 'active' | 'expired' | 'revoked'
user_id?: number user_id?: number
group_id?: number group_id?: number
},
options?: {
signal?: AbortSignal
} }
): Promise<PaginatedResponse<UserSubscription>> { ): Promise<PaginatedResponse<UserSubscription>> {
const { data } = await apiClient.get<PaginatedResponse<UserSubscription>>( const { data } = await apiClient.get<PaginatedResponse<UserSubscription>>(
...@@ -36,7 +39,8 @@ export async function list( ...@@ -36,7 +39,8 @@ export async function list(
page, page,
page_size: pageSize, page_size: pageSize,
...filters ...filters
} },
signal: options?.signal
} }
) )
return data return data
......
...@@ -41,9 +41,13 @@ export interface AdminUsageQueryParams extends UsageQueryParams { ...@@ -41,9 +41,13 @@ export interface AdminUsageQueryParams extends UsageQueryParams {
* @param params - Query parameters for filtering and pagination * @param params - Query parameters for filtering and pagination
* @returns Paginated list of usage logs * @returns Paginated list of usage logs
*/ */
export async function list(params: AdminUsageQueryParams): Promise<PaginatedResponse<UsageLog>> { export async function list(
params: AdminUsageQueryParams,
options?: { signal?: AbortSignal }
): Promise<PaginatedResponse<UsageLog>> {
const { data } = await apiClient.get<PaginatedResponse<UsageLog>>('/admin/usage', { const { data } = await apiClient.get<PaginatedResponse<UsageLog>>('/admin/usage', {
params params,
signal: options?.signal
}) })
return data return data
} }
......
...@@ -11,6 +11,7 @@ import type { User, UpdateUserRequest, PaginatedResponse } from '@/types' ...@@ -11,6 +11,7 @@ import type { User, UpdateUserRequest, PaginatedResponse } from '@/types'
* @param page - Page number (default: 1) * @param page - Page number (default: 1)
* @param pageSize - Items per page (default: 20) * @param pageSize - Items per page (default: 20)
* @param filters - Optional filters (status, role, search) * @param filters - Optional filters (status, role, search)
* @param options - Optional request options (signal)
* @returns Paginated list of users * @returns Paginated list of users
*/ */
export async function list( export async function list(
...@@ -20,6 +21,9 @@ export async function list( ...@@ -20,6 +21,9 @@ export async function list(
status?: 'active' | 'disabled' status?: 'active' | 'disabled'
role?: 'admin' | 'user' role?: 'admin' | 'user'
search?: string search?: string
},
options?: {
signal?: AbortSignal
} }
): Promise<PaginatedResponse<User>> { ): Promise<PaginatedResponse<User>> {
const { data } = await apiClient.get<PaginatedResponse<User>>('/admin/users', { const { data } = await apiClient.get<PaginatedResponse<User>>('/admin/users', {
...@@ -27,7 +31,8 @@ export async function list( ...@@ -27,7 +31,8 @@ export async function list(
page, page,
page_size: pageSize, page_size: pageSize,
...filters ...filters
} },
signal: options?.signal
}) })
return data return data
} }
......
...@@ -10,14 +10,19 @@ import type { ApiKey, CreateApiKeyRequest, UpdateApiKeyRequest, PaginatedRespons ...@@ -10,14 +10,19 @@ import type { ApiKey, CreateApiKeyRequest, UpdateApiKeyRequest, PaginatedRespons
* List all API keys for current user * List all API keys for current user
* @param page - Page number (default: 1) * @param page - Page number (default: 1)
* @param pageSize - Items per page (default: 10) * @param pageSize - Items per page (default: 10)
* @param options - Optional request options
* @returns Paginated list of API keys * @returns Paginated list of API keys
*/ */
export async function list( export async function list(
page: number = 1, page: number = 1,
pageSize: number = 10 pageSize: number = 10,
options?: {
signal?: AbortSignal
}
): Promise<PaginatedResponse<ApiKey>> { ): Promise<PaginatedResponse<ApiKey>> {
const { data } = await apiClient.get<PaginatedResponse<ApiKey>>('/keys', { const { data } = await apiClient.get<PaginatedResponse<ApiKey>>('/keys', {
params: { page, page_size: pageSize } params: { page, page_size: pageSize },
signal: options?.signal
}) })
return data return data
} }
......
...@@ -90,8 +90,12 @@ export async function list( ...@@ -90,8 +90,12 @@ export async function list(
* @param params - Query parameters for filtering and pagination * @param params - Query parameters for filtering and pagination
* @returns Paginated list of usage logs * @returns Paginated list of usage logs
*/ */
export async function query(params: UsageQueryParams): Promise<PaginatedResponse<UsageLog>> { export async function query(
params: UsageQueryParams,
config: { signal?: AbortSignal } = {}
): Promise<PaginatedResponse<UsageLog>> {
const { data } = await apiClient.get<PaginatedResponse<UsageLog>>('/usage', { const { data } = await apiClient.get<PaginatedResponse<UsageLog>>('/usage', {
...config,
params params
}) })
return data return data
...@@ -232,15 +236,22 @@ export interface BatchApiKeysUsageResponse { ...@@ -232,15 +236,22 @@ export interface BatchApiKeysUsageResponse {
/** /**
* Get batch usage stats for user's own API keys * Get batch usage stats for user's own API keys
* @param apiKeyIds - Array of API key IDs * @param apiKeyIds - Array of API key IDs
* @param options - Optional request options
* @returns Usage stats map keyed by API key ID * @returns Usage stats map keyed by API key ID
*/ */
export async function getDashboardApiKeysUsage( export async function getDashboardApiKeysUsage(
apiKeyIds: number[] apiKeyIds: number[],
options?: {
signal?: AbortSignal
}
): Promise<BatchApiKeysUsageResponse> { ): Promise<BatchApiKeysUsageResponse> {
const { data } = await apiClient.post<BatchApiKeysUsageResponse>( const { data } = await apiClient.post<BatchApiKeysUsageResponse>(
'/usage/dashboard/api-keys-usage', '/usage/dashboard/api-keys-usage',
{ {
api_key_ids: apiKeyIds api_key_ids: apiKeyIds
},
{
signal: options?.signal
} }
) )
return data return data
......
<template> <template>
<Modal :show="show" :title="t('admin.accounts.usageStatistics')" size="2xl" @close="handleClose"> <BaseDialog
:show="show"
:title="t('admin.accounts.usageStatistics')"
width="extra-wide"
@close="handleClose"
>
<div class="space-y-6"> <div class="space-y-6">
<!-- Account Info Header --> <!-- Account Info Header -->
<div <div
...@@ -521,7 +526,7 @@ ...@@ -521,7 +526,7 @@
</button> </button>
</div> </div>
</template> </template>
</Modal> </BaseDialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
...@@ -539,7 +544,7 @@ import { ...@@ -539,7 +544,7 @@ import {
Filler Filler
} from 'chart.js' } from 'chart.js'
import { Line } from 'vue-chartjs' import { Line } from 'vue-chartjs'
import Modal from '@/components/common/Modal.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
import LoadingSpinner from '@/components/common/LoadingSpinner.vue' import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
import ModelDistributionChart from '@/components/charts/ModelDistributionChart.vue' import ModelDistributionChart from '@/components/charts/ModelDistributionChart.vue'
import { adminAPI } from '@/api/admin' import { adminAPI } from '@/api/admin'
......
<template> <template>
<Modal <BaseDialog
:show="show" :show="show"
:title="t('admin.accounts.testAccountConnection')" :title="t('admin.accounts.testAccountConnection')"
size="md" width="normal"
@close="handleClose" @close="handleClose"
> >
<div class="space-y-4"> <div class="space-y-4">
...@@ -273,13 +273,13 @@ ...@@ -273,13 +273,13 @@
</button> </button>
</div> </div>
</template> </template>
</Modal> </BaseDialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, nextTick } from 'vue' import { ref, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import Modal from '@/components/common/Modal.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
import { useClipboard } from '@/composables/useClipboard' import { useClipboard } from '@/composables/useClipboard'
import { adminAPI } from '@/api/admin' import { adminAPI } from '@/api/admin'
import type { Account, ClaudeModel } from '@/types' import type { Account, ClaudeModel } from '@/types'
......
<template> <template>
<Modal :show="show" :title="t('admin.accounts.bulkEdit.title')" size="lg" @close="handleClose"> <BaseDialog
<form class="space-y-5" @submit.prevent="handleSubmit"> :show="show"
:title="t('admin.accounts.bulkEdit.title')"
width="wide"
@close="handleClose"
>
<form id="bulk-edit-account-form" class="space-y-5" @submit.prevent="handleSubmit">
<!-- Info --> <!-- Info -->
<div class="rounded-lg bg-blue-50 p-4 dark:bg-blue-900/20"> <div class="rounded-lg bg-blue-50 p-4 dark:bg-blue-900/20">
<p class="text-sm text-blue-700 dark:text-blue-400"> <p class="text-sm text-blue-700 dark:text-blue-400">
...@@ -19,20 +24,30 @@ ...@@ -19,20 +24,30 @@
<!-- Base URL (API Key only) --> <!-- Base URL (API Key only) -->
<div class="border-t border-gray-200 pt-4 dark:border-dark-600"> <div class="border-t border-gray-200 pt-4 dark:border-dark-600">
<div class="mb-3 flex items-center justify-between"> <div class="mb-3 flex items-center justify-between">
<label class="input-label mb-0">{{ t('admin.accounts.baseUrl') }}</label> <label
id="bulk-edit-base-url-label"
class="input-label mb-0"
for="bulk-edit-base-url-enabled"
>
{{ t('admin.accounts.baseUrl') }}
</label>
<input <input
v-model="enableBaseUrl" v-model="enableBaseUrl"
id="bulk-edit-base-url-enabled"
type="checkbox" type="checkbox"
aria-controls="bulk-edit-base-url"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/> />
</div> </div>
<input <input
v-model="baseUrl" v-model="baseUrl"
id="bulk-edit-base-url"
type="text" type="text"
:disabled="!enableBaseUrl" :disabled="!enableBaseUrl"
class="input" class="input"
:class="!enableBaseUrl && 'cursor-not-allowed opacity-50'" :class="!enableBaseUrl && 'cursor-not-allowed opacity-50'"
:placeholder="t('admin.accounts.bulkEdit.baseUrlPlaceholder')" :placeholder="t('admin.accounts.bulkEdit.baseUrlPlaceholder')"
aria-labelledby="bulk-edit-base-url-label"
/> />
<p class="input-hint"> <p class="input-hint">
{{ t('admin.accounts.bulkEdit.baseUrlNotice') }} {{ t('admin.accounts.bulkEdit.baseUrlNotice') }}
...@@ -42,15 +57,28 @@ ...@@ -42,15 +57,28 @@
<!-- Model restriction --> <!-- Model restriction -->
<div class="border-t border-gray-200 pt-4 dark:border-dark-600"> <div class="border-t border-gray-200 pt-4 dark:border-dark-600">
<div class="mb-3 flex items-center justify-between"> <div class="mb-3 flex items-center justify-between">
<label class="input-label mb-0">{{ t('admin.accounts.modelRestriction') }}</label> <label
id="bulk-edit-model-restriction-label"
class="input-label mb-0"
for="bulk-edit-model-restriction-enabled"
>
{{ t('admin.accounts.modelRestriction') }}
</label>
<input <input
v-model="enableModelRestriction" v-model="enableModelRestriction"
id="bulk-edit-model-restriction-enabled"
type="checkbox" type="checkbox"
aria-controls="bulk-edit-model-restriction-body"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/> />
</div> </div>
<div :class="!enableModelRestriction && 'pointer-events-none opacity-50'"> <div
id="bulk-edit-model-restriction-body"
:class="!enableModelRestriction && 'pointer-events-none opacity-50'"
role="group"
aria-labelledby="bulk-edit-model-restriction-label"
>
<!-- Mode Toggle --> <!-- Mode Toggle -->
<div class="mb-4 flex gap-2"> <div class="mb-4 flex gap-2">
<button <button
...@@ -267,19 +295,27 @@ ...@@ -267,19 +295,27 @@
<div class="border-t border-gray-200 pt-4 dark:border-dark-600"> <div class="border-t border-gray-200 pt-4 dark:border-dark-600">
<div class="mb-3 flex items-center justify-between"> <div class="mb-3 flex items-center justify-between">
<div> <div>
<label class="input-label mb-0">{{ t('admin.accounts.customErrorCodes') }}</label> <label
id="bulk-edit-custom-error-codes-label"
class="input-label mb-0"
for="bulk-edit-custom-error-codes-enabled"
>
{{ t('admin.accounts.customErrorCodes') }}
</label>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400"> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.customErrorCodesHint') }} {{ t('admin.accounts.customErrorCodesHint') }}
</p> </p>
</div> </div>
<input <input
v-model="enableCustomErrorCodes" v-model="enableCustomErrorCodes"
id="bulk-edit-custom-error-codes-enabled"
type="checkbox" type="checkbox"
aria-controls="bulk-edit-custom-error-codes-body"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/> />
</div> </div>
<div v-if="enableCustomErrorCodes" class="space-y-3"> <div v-if="enableCustomErrorCodes" id="bulk-edit-custom-error-codes-body" class="space-y-3">
<div class="rounded-lg bg-amber-50 p-3 dark:bg-amber-900/20"> <div class="rounded-lg bg-amber-50 p-3 dark:bg-amber-900/20">
<p class="text-xs text-amber-700 dark:text-amber-400"> <p class="text-xs text-amber-700 dark:text-amber-400">
<svg <svg
...@@ -321,11 +357,13 @@ ...@@ -321,11 +357,13 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<input <input
v-model="customErrorCodeInput" v-model="customErrorCodeInput"
id="bulk-edit-custom-error-code-input"
type="number" type="number"
min="100" min="100"
max="599" max="599"
class="input flex-1" class="input flex-1"
:placeholder="t('admin.accounts.enterErrorCode')" :placeholder="t('admin.accounts.enterErrorCode')"
aria-labelledby="bulk-edit-custom-error-codes-label"
@keyup.enter="addCustomErrorCode" @keyup.enter="addCustomErrorCode"
/> />
<button type="button" class="btn btn-secondary px-3" @click="addCustomErrorCode"> <button type="button" class="btn btn-secondary px-3" @click="addCustomErrorCode">
...@@ -374,20 +412,26 @@ ...@@ -374,20 +412,26 @@
<div class="border-t border-gray-200 pt-4 dark:border-dark-600"> <div class="border-t border-gray-200 pt-4 dark:border-dark-600">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex-1 pr-4"> <div class="flex-1 pr-4">
<label class="input-label mb-0">{{ <label
t('admin.accounts.interceptWarmupRequests') id="bulk-edit-intercept-warmup-label"
}}</label> class="input-label mb-0"
for="bulk-edit-intercept-warmup-enabled"
>
{{ t('admin.accounts.interceptWarmupRequests') }}
</label>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400"> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.interceptWarmupRequestsDesc') }} {{ t('admin.accounts.interceptWarmupRequestsDesc') }}
</p> </p>
</div> </div>
<input <input
v-model="enableInterceptWarmup" v-model="enableInterceptWarmup"
id="bulk-edit-intercept-warmup-enabled"
type="checkbox" type="checkbox"
aria-controls="bulk-edit-intercept-warmup-body"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/> />
</div> </div>
<div v-if="enableInterceptWarmup" class="mt-3"> <div v-if="enableInterceptWarmup" id="bulk-edit-intercept-warmup-body" class="mt-3">
<button <button
type="button" type="button"
:class="[ :class="[
...@@ -409,15 +453,27 @@ ...@@ -409,15 +453,27 @@
<!-- Proxy --> <!-- Proxy -->
<div class="border-t border-gray-200 pt-4 dark:border-dark-600"> <div class="border-t border-gray-200 pt-4 dark:border-dark-600">
<div class="mb-3 flex items-center justify-between"> <div class="mb-3 flex items-center justify-between">
<label class="input-label mb-0">{{ t('admin.accounts.proxy') }}</label> <label
id="bulk-edit-proxy-label"
class="input-label mb-0"
for="bulk-edit-proxy-enabled"
>
{{ t('admin.accounts.proxy') }}
</label>
<input <input
v-model="enableProxy" v-model="enableProxy"
id="bulk-edit-proxy-enabled"
type="checkbox" type="checkbox"
aria-controls="bulk-edit-proxy-body"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/> />
</div> </div>
<div :class="!enableProxy && 'pointer-events-none opacity-50'"> <div id="bulk-edit-proxy-body" :class="!enableProxy && 'pointer-events-none opacity-50'">
<ProxySelector v-model="proxyId" :proxies="proxies" /> <ProxySelector
v-model="proxyId"
:proxies="proxies"
aria-labelledby="bulk-edit-proxy-label"
/>
</div> </div>
</div> </div>
...@@ -425,38 +481,58 @@ ...@@ -425,38 +481,58 @@
<div class="grid grid-cols-2 gap-4 border-t border-gray-200 pt-4 dark:border-dark-600"> <div class="grid grid-cols-2 gap-4 border-t border-gray-200 pt-4 dark:border-dark-600">
<div> <div>
<div class="mb-3 flex items-center justify-between"> <div class="mb-3 flex items-center justify-between">
<label class="input-label mb-0">{{ t('admin.accounts.concurrency') }}</label> <label
id="bulk-edit-concurrency-label"
class="input-label mb-0"
for="bulk-edit-concurrency-enabled"
>
{{ t('admin.accounts.concurrency') }}
</label>
<input <input
v-model="enableConcurrency" v-model="enableConcurrency"
id="bulk-edit-concurrency-enabled"
type="checkbox" type="checkbox"
aria-controls="bulk-edit-concurrency"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/> />
</div> </div>
<input <input
v-model.number="concurrency" v-model.number="concurrency"
id="bulk-edit-concurrency"
type="number" type="number"
min="1" min="1"
:disabled="!enableConcurrency" :disabled="!enableConcurrency"
class="input" class="input"
:class="!enableConcurrency && 'cursor-not-allowed opacity-50'" :class="!enableConcurrency && 'cursor-not-allowed opacity-50'"
aria-labelledby="bulk-edit-concurrency-label"
/> />
</div> </div>
<div> <div>
<div class="mb-3 flex items-center justify-between"> <div class="mb-3 flex items-center justify-between">
<label class="input-label mb-0">{{ t('admin.accounts.priority') }}</label> <label
id="bulk-edit-priority-label"
class="input-label mb-0"
for="bulk-edit-priority-enabled"
>
{{ t('admin.accounts.priority') }}
</label>
<input <input
v-model="enablePriority" v-model="enablePriority"
id="bulk-edit-priority-enabled"
type="checkbox" type="checkbox"
aria-controls="bulk-edit-priority"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/> />
</div> </div>
<input <input
v-model.number="priority" v-model.number="priority"
id="bulk-edit-priority"
type="number" type="number"
min="1" min="1"
:disabled="!enablePriority" :disabled="!enablePriority"
class="input" class="input"
:class="!enablePriority && 'cursor-not-allowed opacity-50'" :class="!enablePriority && 'cursor-not-allowed opacity-50'"
aria-labelledby="bulk-edit-priority-label"
/> />
</div> </div>
</div> </div>
...@@ -464,39 +540,69 @@ ...@@ -464,39 +540,69 @@
<!-- Status --> <!-- Status -->
<div class="border-t border-gray-200 pt-4 dark:border-dark-600"> <div class="border-t border-gray-200 pt-4 dark:border-dark-600">
<div class="mb-3 flex items-center justify-between"> <div class="mb-3 flex items-center justify-between">
<label class="input-label mb-0">{{ t('common.status') }}</label> <label
id="bulk-edit-status-label"
class="input-label mb-0"
for="bulk-edit-status-enabled"
>
{{ t('common.status') }}
</label>
<input <input
v-model="enableStatus" v-model="enableStatus"
id="bulk-edit-status-enabled"
type="checkbox" type="checkbox"
aria-controls="bulk-edit-status"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/> />
</div> </div>
<div :class="!enableStatus && 'pointer-events-none opacity-50'"> <div id="bulk-edit-status" :class="!enableStatus && 'pointer-events-none opacity-50'">
<Select v-model="status" :options="statusOptions" /> <Select
v-model="status"
:options="statusOptions"
aria-labelledby="bulk-edit-status-label"
/>
</div> </div>
</div> </div>
<!-- Groups --> <!-- Groups -->
<div class="border-t border-gray-200 pt-4 dark:border-dark-600"> <div class="border-t border-gray-200 pt-4 dark:border-dark-600">
<div class="mb-3 flex items-center justify-between"> <div class="mb-3 flex items-center justify-between">
<label class="input-label mb-0">{{ t('nav.groups') }}</label> <label
id="bulk-edit-groups-label"
class="input-label mb-0"
for="bulk-edit-groups-enabled"
>
{{ t('nav.groups') }}
</label>
<input <input
v-model="enableGroups" v-model="enableGroups"
id="bulk-edit-groups-enabled"
type="checkbox" type="checkbox"
aria-controls="bulk-edit-groups"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/> />
</div> </div>
<div :class="!enableGroups && 'pointer-events-none opacity-50'"> <div id="bulk-edit-groups" :class="!enableGroups && 'pointer-events-none opacity-50'">
<GroupSelector v-model="groupIds" :groups="groups" /> <GroupSelector
v-model="groupIds"
:groups="groups"
aria-labelledby="bulk-edit-groups-label"
/>
</div> </div>
</div> </div>
</form>
<!-- Action buttons --> <template #footer>
<div class="flex justify-end gap-3 pt-4"> <div class="flex justify-end gap-3">
<button type="button" class="btn btn-secondary" @click="handleClose"> <button type="button" class="btn btn-secondary" @click="handleClose">
{{ t('common.cancel') }} {{ t('common.cancel') }}
</button> </button>
<button type="submit" :disabled="submitting" class="btn btn-primary"> <button
type="submit"
form="bulk-edit-account-form"
:disabled="submitting"
class="btn btn-primary"
>
<svg <svg
v-if="submitting" v-if="submitting"
class="-ml-1 mr-2 h-4 w-4 animate-spin" class="-ml-1 mr-2 h-4 w-4 animate-spin"
...@@ -522,8 +628,8 @@ ...@@ -522,8 +628,8 @@
}} }}
</button> </button>
</div> </div>
</form> </template>
</Modal> </BaseDialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
...@@ -532,7 +638,7 @@ import { useI18n } from 'vue-i18n' ...@@ -532,7 +638,7 @@ import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app' import { useAppStore } from '@/stores/app'
import { adminAPI } from '@/api/admin' import { adminAPI } from '@/api/admin'
import type { Proxy, Group } from '@/types' import type { Proxy, Group } from '@/types'
import Modal from '@/components/common/Modal.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
import Select from '@/components/common/Select.vue' import Select from '@/components/common/Select.vue'
import ProxySelector from '@/components/common/ProxySelector.vue' import ProxySelector from '@/components/common/ProxySelector.vue'
import GroupSelector from '@/components/common/GroupSelector.vue' import GroupSelector from '@/components/common/GroupSelector.vue'
......
<template> <template>
<Modal :show="show" :title="t('admin.accounts.createAccount')" size="xl" @close="handleClose"> <BaseDialog
:show="show"
:title="t('admin.accounts.createAccount')"
width="wide"
@close="handleClose"
>
<!-- Step Indicator for OAuth accounts --> <!-- Step Indicator for OAuth accounts -->
<div v-if="isOAuthFlow" class="mb-6 flex items-center justify-center"> <div v-if="isOAuthFlow" class="mb-6 flex items-center justify-center">
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
...@@ -34,7 +39,12 @@ ...@@ -34,7 +39,12 @@
</div> </div>
<!-- Step 1: Basic Info --> <!-- Step 1: Basic Info -->
<form v-if="step === 1" @submit.prevent="handleSubmit" class="space-y-5"> <form
v-if="step === 1"
id="create-account-form"
@submit.prevent="handleSubmit"
class="space-y-5"
>
<div> <div>
<label class="input-label">{{ t('admin.accounts.accountName') }}</label> <label class="input-label">{{ t('admin.accounts.accountName') }}</label>
<input <input
...@@ -963,11 +973,40 @@ ...@@ -963,11 +973,40 @@
<!-- Group Selection --> <!-- Group Selection -->
<GroupSelector v-model="form.group_ids" :groups="groups" :platform="form.platform" /> <GroupSelector v-model="form.group_ids" :groups="groups" :platform="form.platform" />
<div class="flex justify-end gap-3 pt-4"> </form>
<!-- Step 2: OAuth Authorization -->
<div v-else class="space-y-5">
<OAuthAuthorizationFlow
ref="oauthFlowRef"
:add-method="form.platform === 'anthropic' ? addMethod : 'oauth'"
:auth-url="currentAuthUrl"
:session-id="currentSessionId"
:loading="currentOAuthLoading"
:error="currentOAuthError"
:show-help="form.platform === 'anthropic'"
:show-proxy-warning="form.platform !== 'openai' && !!form.proxy_id"
:allow-multiple="form.platform === 'anthropic'"
:show-cookie-option="form.platform === 'anthropic'"
:platform="form.platform"
:show-project-id="geminiOAuthType === 'code_assist'"
@generate-url="handleGenerateUrl"
@cookie-auth="handleCookieAuth"
/>
</div>
<template #footer>
<div v-if="step === 1" class="flex justify-end gap-3">
<button @click="handleClose" type="button" class="btn btn-secondary"> <button @click="handleClose" type="button" class="btn btn-secondary">
{{ t('common.cancel') }} {{ t('common.cancel') }}
</button> </button>
<button type="submit" :disabled="submitting" class="btn btn-primary"> <button
type="submit"
form="create-account-form"
:disabled="submitting"
class="btn btn-primary"
>
<svg <svg
v-if="submitting" v-if="submitting"
class="-ml-1 mr-2 h-4 w-4 animate-spin" class="-ml-1 mr-2 h-4 w-4 animate-spin"
...@@ -997,28 +1036,7 @@ ...@@ -997,28 +1036,7 @@
}} }}
</button> </button>
</div> </div>
</form> <div v-else class="flex justify-between gap-3">
<!-- Step 2: OAuth Authorization -->
<div v-else class="space-y-5">
<OAuthAuthorizationFlow
ref="oauthFlowRef"
:add-method="form.platform === 'anthropic' ? addMethod : 'oauth'"
:auth-url="currentAuthUrl"
:session-id="currentSessionId"
:loading="currentOAuthLoading"
:error="currentOAuthError"
:show-help="form.platform === 'anthropic'"
:show-proxy-warning="form.platform !== 'openai' && !!form.proxy_id"
:allow-multiple="form.platform === 'anthropic'"
:show-cookie-option="form.platform === 'anthropic'"
:platform="form.platform"
:show-project-id="geminiOAuthType === 'code_assist'"
@generate-url="handleGenerateUrl"
@cookie-auth="handleCookieAuth"
/>
<div class="flex justify-between gap-3 pt-4">
<button type="button" class="btn btn-secondary" @click="goBackToBasicInfo"> <button type="button" class="btn btn-secondary" @click="goBackToBasicInfo">
{{ t('common.back') }} {{ t('common.back') }}
</button> </button>
...@@ -1056,8 +1074,8 @@ ...@@ -1056,8 +1074,8 @@
}} }}
</button> </button>
</div> </div>
</div> </template>
</Modal> </BaseDialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
...@@ -1073,7 +1091,7 @@ import { ...@@ -1073,7 +1091,7 @@ import {
import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth' import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth'
import { useGeminiOAuth } from '@/composables/useGeminiOAuth' import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
import type { Proxy, Group, AccountPlatform, AccountType } from '@/types' import type { Proxy, Group, AccountPlatform, AccountType } from '@/types'
import Modal from '@/components/common/Modal.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
import ProxySelector from '@/components/common/ProxySelector.vue' import ProxySelector from '@/components/common/ProxySelector.vue'
import GroupSelector from '@/components/common/GroupSelector.vue' import GroupSelector from '@/components/common/GroupSelector.vue'
import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue' import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue'
......
<template> <template>
<Modal :show="show" :title="t('admin.accounts.editAccount')" size="xl" @close="handleClose"> <BaseDialog
<form v-if="account" @submit.prevent="handleSubmit" class="space-y-5"> :show="show"
:title="t('admin.accounts.editAccount')"
width="wide"
@close="handleClose"
>
<form
v-if="account"
id="edit-account-form"
@submit.prevent="handleSubmit"
class="space-y-5"
>
<div> <div>
<label class="input-label">{{ t('common.name') }}</label> <label class="input-label">{{ t('common.name') }}</label>
<input v-model="form.name" type="text" required class="input" /> <input v-model="form.name" type="text" required class="input" />
...@@ -459,11 +469,19 @@ ...@@ -459,11 +469,19 @@
<!-- Group Selection --> <!-- Group Selection -->
<GroupSelector v-model="form.group_ids" :groups="groups" :platform="account?.platform" /> <GroupSelector v-model="form.group_ids" :groups="groups" :platform="account?.platform" />
<div class="flex justify-end gap-3 pt-4"> </form>
<template #footer>
<div v-if="account" class="flex justify-end gap-3">
<button @click="handleClose" type="button" class="btn btn-secondary"> <button @click="handleClose" type="button" class="btn btn-secondary">
{{ t('common.cancel') }} {{ t('common.cancel') }}
</button> </button>
<button type="submit" :disabled="submitting" class="btn btn-primary"> <button
type="submit"
form="edit-account-form"
:disabled="submitting"
class="btn btn-primary"
>
<svg <svg
v-if="submitting" v-if="submitting"
class="-ml-1 mr-2 h-4 w-4 animate-spin" class="-ml-1 mr-2 h-4 w-4 animate-spin"
...@@ -487,8 +505,8 @@ ...@@ -487,8 +505,8 @@
{{ submitting ? t('admin.accounts.updating') : t('common.update') }} {{ submitting ? t('admin.accounts.updating') : t('common.update') }}
</button> </button>
</div> </div>
</form> </template>
</Modal> </BaseDialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
...@@ -497,7 +515,7 @@ import { useI18n } from 'vue-i18n' ...@@ -497,7 +515,7 @@ import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app' import { useAppStore } from '@/stores/app'
import { adminAPI } from '@/api/admin' import { adminAPI } from '@/api/admin'
import type { Account, Proxy, Group } from '@/types' import type { Account, Proxy, Group } from '@/types'
import Modal from '@/components/common/Modal.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
import Select from '@/components/common/Select.vue' import Select from '@/components/common/Select.vue'
import ProxySelector from '@/components/common/ProxySelector.vue' import ProxySelector from '@/components/common/ProxySelector.vue'
import GroupSelector from '@/components/common/GroupSelector.vue' import GroupSelector from '@/components/common/GroupSelector.vue'
......
<template> <template>
<div <div
class="rounded-lg border border-blue-200 bg-blue-50 p-6 dark:border-blue-700 dark:bg-blue-900/30" class="rounded-lg border border-blue-200 bg-blue-50 p-4 dark:border-blue-700 dark:bg-blue-900/30"
> >
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">
<div class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-blue-500"> <div class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-blue-500">
......
<template> <template>
<Modal <BaseDialog
:show="show" :show="show"
:title="t('admin.accounts.reAuthorizeAccount')" :title="t('admin.accounts.reAuthorizeAccount')"
size="lg" width="wide"
@close="handleClose" @close="handleClose"
> >
<div v-if="account" class="space-y-5"> <div v-if="account" class="space-y-4">
<!-- Account Info --> <!-- Account Info -->
<div <div
class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-dark-600 dark:bg-dark-700" class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-dark-600 dark:bg-dark-700"
...@@ -53,8 +53,8 @@ ...@@ -53,8 +53,8 @@
</div> </div>
<!-- Add Method Selection (Claude only) --> <!-- Add Method Selection (Claude only) -->
<div v-if="isAnthropic"> <fieldset v-if="isAnthropic" class="border-0 p-0">
<label class="input-label">{{ t('admin.accounts.oauth.authMethod') }}</label> <legend class="input-label">{{ t('admin.accounts.oauth.authMethod') }}</legend>
<div class="mt-2 flex gap-4"> <div class="mt-2 flex gap-4">
<label class="flex cursor-pointer items-center"> <label class="flex cursor-pointer items-center">
<input <input
...@@ -79,11 +79,11 @@ ...@@ -79,11 +79,11 @@
}}</span> }}</span>
</label> </label>
</div> </div>
</div> </fieldset>
<!-- Gemini OAuth Type Selection --> <!-- Gemini OAuth Type Selection -->
<div v-if="isGemini"> <fieldset v-if="isGemini" class="border-0 p-0">
<label class="input-label">{{ t('admin.accounts.oauth.gemini.oauthTypeLabel') }}</label> <legend class="input-label">{{ t('admin.accounts.oauth.gemini.oauthTypeLabel') }}</legend>
<div class="mt-2 grid grid-cols-2 gap-3"> <div class="mt-2 grid grid-cols-2 gap-3">
<button <button
type="button" type="button"
...@@ -187,7 +187,7 @@ ...@@ -187,7 +187,7 @@
</div> </div>
</button> </button>
</div> </div>
</div> </fieldset>
<OAuthAuthorizationFlow <OAuthAuthorizationFlow
ref="oauthFlowRef" ref="oauthFlowRef"
...@@ -207,7 +207,10 @@ ...@@ -207,7 +207,10 @@
@cookie-auth="handleCookieAuth" @cookie-auth="handleCookieAuth"
/> />
<div class="flex justify-between gap-3 pt-4"> </div>
<template #footer>
<div v-if="account" class="flex justify-between gap-3">
<button type="button" class="btn btn-secondary" @click="handleClose"> <button type="button" class="btn btn-secondary" @click="handleClose">
{{ t('common.cancel') }} {{ t('common.cancel') }}
</button> </button>
...@@ -245,8 +248,8 @@ ...@@ -245,8 +248,8 @@
}} }}
</button> </button>
</div> </div>
</div> </template>
</Modal> </BaseDialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
...@@ -262,7 +265,7 @@ import { ...@@ -262,7 +265,7 @@ import {
import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth' import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth'
import { useGeminiOAuth } from '@/composables/useGeminiOAuth' import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
import type { Account } from '@/types' import type { Account } from '@/types'
import Modal from '@/components/common/Modal.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue' import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue'
// Type for exposed OAuthAuthorizationFlow component // Type for exposed OAuthAuthorizationFlow component
......
<template> <template>
<Modal <BaseDialog
:show="show" :show="show"
:title="t('admin.accounts.syncFromCrsTitle')" :title="t('admin.accounts.syncFromCrsTitle')"
size="lg" width="normal"
close-on-click-outside close-on-click-outside
@close="handleClose" @close="handleClose"
> >
<div class="space-y-4"> <form id="sync-from-crs-form" class="space-y-4" @submit.prevent="handleSync">
<div class="text-sm text-gray-600 dark:text-dark-300"> <div class="text-sm text-gray-600 dark:text-dark-300">
{{ t('admin.accounts.syncFromCrsDesc') }} {{ t('admin.accounts.syncFromCrsDesc') }}
</div> </div>
...@@ -84,25 +84,30 @@ ...@@ -84,25 +84,30 @@
</div> </div>
</div> </div>
</div> </div>
</div> </form>
<template #footer> <template #footer>
<div class="flex justify-end gap-3"> <div class="flex justify-end gap-3">
<button class="btn btn-secondary" :disabled="syncing" @click="handleClose"> <button class="btn btn-secondary" type="button" :disabled="syncing" @click="handleClose">
{{ t('common.cancel') }} {{ t('common.cancel') }}
</button> </button>
<button class="btn btn-primary" :disabled="syncing" @click="handleSync"> <button
class="btn btn-primary"
type="submit"
form="sync-from-crs-form"
:disabled="syncing"
>
{{ syncing ? t('admin.accounts.syncing') : t('admin.accounts.syncNow') }} {{ syncing ? t('admin.accounts.syncing') : t('admin.accounts.syncNow') }}
</button> </button>
</div> </div>
</template> </template>
</Modal> </BaseDialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue' import { computed, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import Modal from '@/components/common/Modal.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
import { useAppStore } from '@/stores/app' import { useAppStore } from '@/stores/app'
import { adminAPI } from '@/api/admin' import { adminAPI } from '@/api/admin'
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment