Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
fb9d0878
Commit
fb9d0878
authored
Dec 28, 2025
by
shaw
Browse files
Merge PR #62: refactor(frontend): 前端界面优化与订阅状态管理增强
parents
fd51ff69
18c6686f
Changes
48
Hide whitespace changes
Inline
Side-by-side
backend/internal/pkg/gemini/models.go
View file @
fb9d0878
...
...
@@ -18,8 +18,10 @@ func DefaultModels() []Model {
methods
:=
[]
string
{
"generateContent"
,
"streamGenerateContent"
}
return
[]
Model
{
{
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-lite"
,
SupportedGenerationMethods
:
methods
},
{
Name
:
"models/gemini-1.5-pro"
,
SupportedGenerationMethods
:
methods
},
{
Name
:
"models/gemini-1.5-flash"
,
SupportedGenerationMethods
:
methods
},
{
Name
:
"models/gemini-1.5-flash-8b"
,
SupportedGenerationMethods
:
methods
},
...
...
backend/internal/pkg/geminicli/models.go
View file @
fb9d0878
...
...
@@ -11,11 +11,11 @@ type Model struct {
// DefaultModels is the curated Gemini model list used by the admin UI "test account" flow.
var
DefaultModels
=
[]
Model
{
{
ID
:
"gemini-3-pro"
,
Type
:
"model"
,
DisplayName
:
"Gemini 3 Pro"
,
CreatedAt
:
""
},
{
ID
:
"gemini-3-flash"
,
Type
:
"model"
,
DisplayName
:
"Gemini 3 Flash"
,
CreatedAt
:
""
},
{
ID
:
"gemini-3-pro
-preview
"
,
Type
:
"model"
,
DisplayName
:
"Gemini 3 Pro
Preview
"
,
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-flash"
,
Type
:
"model"
,
DisplayName
:
"Gemini 2.5 Flash"
,
CreatedAt
:
""
},
}
// DefaultTestModel is the default model to preselect in test flows.
const
DefaultTestModel
=
"gemini-
2.5
-pro"
const
DefaultTestModel
=
"gemini-
3
-pro
-preview
"
frontend/src/App.vue
View file @
fb9d0878
...
...
@@ -2,12 +2,14 @@
import
{
RouterView
,
useRouter
,
useRoute
}
from
'
vue-router
'
import
{
onMounted
,
watch
}
from
'
vue
'
import
Toast
from
'
@/components/common/Toast.vue
'
import
{
useAppStore
}
from
'
@/stores
'
import
{
useAppStore
,
useAuthStore
,
useSubscriptionStore
}
from
'
@/stores
'
import
{
getSetupStatus
}
from
'
@/api/setup
'
const
router
=
useRouter
()
const
route
=
useRoute
()
const
appStore
=
useAppStore
()
const
authStore
=
useAuthStore
()
const
subscriptionStore
=
useSubscriptionStore
()
/**
* Update favicon dynamically
...
...
@@ -46,6 +48,24 @@ watch(
{
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
()
=>
{
// Check if setup is needed
try
{
...
...
frontend/src/api/admin/accounts.ts
View file @
fb9d0878
...
...
@@ -30,6 +30,9 @@ export async function list(
type
?:
string
status
?:
string
search
?:
string
},
options
?:
{
signal
?:
AbortSignal
}
):
Promise
<
PaginatedResponse
<
Account
>>
{
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
Account
>>
(
'
/admin/accounts
'
,
{
...
...
@@ -37,7 +40,8 @@ export async function list(
page
,
page_size
:
pageSize
,
...
filters
}
},
signal
:
options
?.
signal
})
return
data
}
...
...
frontend/src/api/admin/groups.ts
View file @
fb9d0878
...
...
@@ -26,6 +26,9 @@ export async function list(
platform
?:
GroupPlatform
status
?:
'
active
'
|
'
inactive
'
is_exclusive
?:
boolean
},
options
?:
{
signal
?:
AbortSignal
}
):
Promise
<
PaginatedResponse
<
Group
>>
{
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
Group
>>
(
'
/admin/groups
'
,
{
...
...
@@ -33,7 +36,8 @@ export async function list(
page
,
page_size
:
pageSize
,
...
filters
}
},
signal
:
options
?.
signal
})
return
data
}
...
...
frontend/src/api/admin/proxies.ts
View file @
fb9d0878
...
...
@@ -20,6 +20,9 @@ export async function list(
protocol
?:
string
status
?:
'
active
'
|
'
inactive
'
search
?:
string
},
options
?:
{
signal
?:
AbortSignal
}
):
Promise
<
PaginatedResponse
<
Proxy
>>
{
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
Proxy
>>
(
'
/admin/proxies
'
,
{
...
...
@@ -27,7 +30,8 @@ export async function list(
page
,
page_size
:
pageSize
,
...
filters
}
},
signal
:
options
?.
signal
})
return
data
}
...
...
frontend/src/api/admin/redeem.ts
View file @
fb9d0878
...
...
@@ -25,6 +25,9 @@ export async function list(
type
?:
RedeemCodeType
status
?:
'
active
'
|
'
used
'
|
'
expired
'
|
'
unused
'
search
?:
string
},
options
?:
{
signal
?:
AbortSignal
}
):
Promise
<
PaginatedResponse
<
RedeemCode
>>
{
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
RedeemCode
>>
(
'
/admin/redeem-codes
'
,
{
...
...
@@ -32,7 +35,8 @@ export async function list(
page
,
page_size
:
pageSize
,
...
filters
}
},
signal
:
options
?.
signal
})
return
data
}
...
...
frontend/src/api/admin/subscriptions.ts
View file @
fb9d0878
...
...
@@ -27,6 +27,9 @@ export async function list(
status
?:
'
active
'
|
'
expired
'
|
'
revoked
'
user_id
?:
number
group_id
?:
number
},
options
?:
{
signal
?:
AbortSignal
}
):
Promise
<
PaginatedResponse
<
UserSubscription
>>
{
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
UserSubscription
>>
(
...
...
@@ -36,7 +39,8 @@ export async function list(
page
,
page_size
:
pageSize
,
...
filters
}
},
signal
:
options
?.
signal
}
)
return
data
...
...
frontend/src/api/admin/usage.ts
View file @
fb9d0878
...
...
@@ -41,9 +41,13 @@ export interface AdminUsageQueryParams extends UsageQueryParams {
* @param params - Query parameters for filtering and pagination
* @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
'
,
{
params
params
,
signal
:
options
?.
signal
})
return
data
}
...
...
frontend/src/api/admin/users.ts
View file @
fb9d0878
...
...
@@ -11,6 +11,7 @@ import type { User, UpdateUserRequest, PaginatedResponse } from '@/types'
* @param page - Page number (default: 1)
* @param pageSize - Items per page (default: 20)
* @param filters - Optional filters (status, role, search)
* @param options - Optional request options (signal)
* @returns Paginated list of users
*/
export
async
function
list
(
...
...
@@ -20,6 +21,9 @@ export async function list(
status
?:
'
active
'
|
'
disabled
'
role
?:
'
admin
'
|
'
user
'
search
?:
string
},
options
?:
{
signal
?:
AbortSignal
}
):
Promise
<
PaginatedResponse
<
User
>>
{
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
User
>>
(
'
/admin/users
'
,
{
...
...
@@ -27,7 +31,8 @@ export async function list(
page
,
page_size
:
pageSize
,
...
filters
}
},
signal
:
options
?.
signal
})
return
data
}
...
...
frontend/src/api/keys.ts
View file @
fb9d0878
...
...
@@ -10,14 +10,19 @@ import type { ApiKey, CreateApiKeyRequest, UpdateApiKeyRequest, PaginatedRespons
* List all API keys for current user
* @param page - Page number (default: 1)
* @param pageSize - Items per page (default: 10)
* @param options - Optional request options
* @returns Paginated list of API keys
*/
export
async
function
list
(
page
:
number
=
1
,
pageSize
:
number
=
10
pageSize
:
number
=
10
,
options
?:
{
signal
?:
AbortSignal
}
):
Promise
<
PaginatedResponse
<
ApiKey
>>
{
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
ApiKey
>>
(
'
/keys
'
,
{
params
:
{
page
,
page_size
:
pageSize
}
params
:
{
page
,
page_size
:
pageSize
},
signal
:
options
?.
signal
})
return
data
}
...
...
frontend/src/api/usage.ts
View file @
fb9d0878
...
...
@@ -90,8 +90,12 @@ export async function list(
* @param params - Query parameters for filtering and pagination
* @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
'
,
{
...
config
,
params
})
return
data
...
...
@@ -232,15 +236,22 @@ export interface BatchApiKeysUsageResponse {
/**
* Get batch usage stats for user's own API keys
* @param apiKeyIds - Array of API key IDs
* @param options - Optional request options
* @returns Usage stats map keyed by API key ID
*/
export
async
function
getDashboardApiKeysUsage
(
apiKeyIds
:
number
[]
apiKeyIds
:
number
[],
options
?:
{
signal
?:
AbortSignal
}
):
Promise
<
BatchApiKeysUsageResponse
>
{
const
{
data
}
=
await
apiClient
.
post
<
BatchApiKeysUsageResponse
>
(
'
/usage/dashboard/api-keys-usage
'
,
{
api_key_ids
:
apiKeyIds
},
{
signal
:
options
?.
signal
}
)
return
data
...
...
frontend/src/components/account/AccountStatsModal.vue
View file @
fb9d0878
<
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"
>
<!-- Account Info Header -->
<div
...
...
@@ -521,7 +526,7 @@
<
/button
>
<
/div
>
<
/template
>
<
/
Modal
>
<
/
BaseDialog
>
<
/template
>
<
script
setup
lang
=
"
ts
"
>
...
...
@@ -539,7 +544,7 @@ import {
Filler
}
from
'
chart.js
'
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
ModelDistributionChart
from
'
@/components/charts/ModelDistributionChart.vue
'
import
{
adminAPI
}
from
'
@/api/admin
'
...
...
frontend/src/components/account/AccountTestModal.vue
View file @
fb9d0878
<
template
>
<
Modal
<
BaseDialog
:show=
"show"
:title=
"t('admin.accounts.testAccountConnection')"
size=
"md
"
width=
"normal
"
@
close=
"handleClose"
>
<div
class=
"space-y-4"
>
...
...
@@ -273,13 +273,13 @@
</button>
</div>
</
template
>
</
Modal
>
</
BaseDialog
>
</template>
<
script
setup
lang=
"ts"
>
import
{
ref
,
watch
,
nextTick
}
from
'
vue
'
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
{
adminAPI
}
from
'
@/api/admin
'
import
type
{
Account
,
ClaudeModel
}
from
'
@/types
'
...
...
frontend/src/components/account/BulkEditAccountModal.vue
View file @
fb9d0878
<
template
>
<Modal
:show=
"show"
:title=
"t('admin.accounts.bulkEdit.title')"
size=
"lg"
@
close=
"handleClose"
>
<form
class=
"space-y-5"
@
submit.prevent=
"handleSubmit"
>
<BaseDialog
: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 -->
<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"
>
...
...
@@ -19,20 +24,30 @@
<!--
Base
URL
(
API
Key
only
)
-->
<
div
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
<
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
v
-
model
=
"
enableBaseUrl
"
id
=
"
bulk-edit-base-url-enabled
"
type
=
"
checkbox
"
aria
-
controls
=
"
bulk-edit-base-url
"
class
=
"
rounded border-gray-300 text-primary-600 focus:ring-primary-500
"
/>
<
/div
>
<
input
v
-
model
=
"
baseUrl
"
id
=
"
bulk-edit-base-url
"
type
=
"
text
"
:
disabled
=
"
!enableBaseUrl
"
class
=
"
input
"
:
class
=
"
!enableBaseUrl && 'cursor-not-allowed opacity-50'
"
:
placeholder
=
"
t('admin.accounts.bulkEdit.baseUrlPlaceholder')
"
aria
-
labelledby
=
"
bulk-edit-base-url-label
"
/>
<
p
class
=
"
input-hint
"
>
{{
t
(
'
admin.accounts.bulkEdit.baseUrlNotice
'
)
}}
...
...
@@ -42,15 +57,28 @@
<!--
Model
restriction
-->
<
div
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
<
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
v
-
model
=
"
enableModelRestriction
"
id
=
"
bulk-edit-model-restriction-enabled
"
type
=
"
checkbox
"
aria
-
controls
=
"
bulk-edit-model-restriction-body
"
class
=
"
rounded border-gray-300 text-primary-600 focus:ring-primary-500
"
/>
<
/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
-->
<
div
class
=
"
mb-4 flex gap-2
"
>
<
button
...
...
@@ -267,19 +295,27 @@
<
div
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
<
div
class
=
"
mb-3 flex items-center justify-between
"
>
<
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
"
>
{{
t
(
'
admin.accounts.customErrorCodesHint
'
)
}}
<
/p
>
<
/div
>
<
input
v
-
model
=
"
enableCustomErrorCodes
"
id
=
"
bulk-edit-custom-error-codes-enabled
"
type
=
"
checkbox
"
aria
-
controls
=
"
bulk-edit-custom-error-codes-body
"
class
=
"
rounded border-gray-300 text-primary-600 focus:ring-primary-500
"
/>
<
/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
"
>
<
p
class
=
"
text-xs text-amber-700 dark:text-amber-400
"
>
<
svg
...
...
@@ -321,11 +357,13 @@
<
div
class
=
"
flex items-center gap-2
"
>
<
input
v
-
model
=
"
customErrorCodeInput
"
id
=
"
bulk-edit-custom-error-code-input
"
type
=
"
number
"
min
=
"
100
"
max
=
"
599
"
class
=
"
input flex-1
"
:
placeholder
=
"
t('admin.accounts.enterErrorCode')
"
aria
-
labelledby
=
"
bulk-edit-custom-error-codes-label
"
@
keyup
.
enter
=
"
addCustomErrorCode
"
/>
<
button
type
=
"
button
"
class
=
"
btn btn-secondary px-3
"
@
click
=
"
addCustomErrorCode
"
>
...
...
@@ -374,20 +412,26 @@
<
div
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
<
div
class
=
"
flex items-center justify-between
"
>
<
div
class
=
"
flex-1 pr-4
"
>
<
label
class
=
"
input-label mb-0
"
>
{{
t
(
'
admin.accounts.interceptWarmupRequests
'
)
}}
<
/label
>
<
label
id
=
"
bulk-edit-intercept-warmup-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
"
>
{{
t
(
'
admin.accounts.interceptWarmupRequestsDesc
'
)
}}
<
/p
>
<
/div
>
<
input
v
-
model
=
"
enableInterceptWarmup
"
id
=
"
bulk-edit-intercept-warmup-enabled
"
type
=
"
checkbox
"
aria
-
controls
=
"
bulk-edit-intercept-warmup-body
"
class
=
"
rounded border-gray-300 text-primary-600 focus:ring-primary-500
"
/>
<
/div
>
<
div
v
-
if
=
"
enableInterceptWarmup
"
class
=
"
mt-3
"
>
<
div
v
-
if
=
"
enableInterceptWarmup
"
id
=
"
bulk-edit-intercept-warmup-body
"
class
=
"
mt-3
"
>
<
button
type
=
"
button
"
:
class
=
"
[
...
...
@@ -409,15 +453,27 @@
<!--
Proxy
-->
<
div
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
<
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
v
-
model
=
"
enableProxy
"
id
=
"
bulk-edit-proxy-enabled
"
type
=
"
checkbox
"
aria
-
controls
=
"
bulk-edit-proxy-body
"
class
=
"
rounded border-gray-300 text-primary-600 focus:ring-primary-500
"
/>
<
/div
>
<
div
:
class
=
"
!enableProxy && 'pointer-events-none opacity-50'
"
>
<
ProxySelector
v
-
model
=
"
proxyId
"
:
proxies
=
"
proxies
"
/>
<
div
id
=
"
bulk-edit-proxy-body
"
:
class
=
"
!enableProxy && 'pointer-events-none opacity-50'
"
>
<
ProxySelector
v
-
model
=
"
proxyId
"
:
proxies
=
"
proxies
"
aria
-
labelledby
=
"
bulk-edit-proxy-label
"
/>
<
/div
>
<
/div
>
...
...
@@ -425,38 +481,58 @@
<
div
class
=
"
grid grid-cols-2 gap-4 border-t border-gray-200 pt-4 dark:border-dark-600
"
>
<
div
>
<
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
v
-
model
=
"
enableConcurrency
"
id
=
"
bulk-edit-concurrency-enabled
"
type
=
"
checkbox
"
aria
-
controls
=
"
bulk-edit-concurrency
"
class
=
"
rounded border-gray-300 text-primary-600 focus:ring-primary-500
"
/>
<
/div
>
<
input
v
-
model
.
number
=
"
concurrency
"
id
=
"
bulk-edit-concurrency
"
type
=
"
number
"
min
=
"
1
"
:
disabled
=
"
!enableConcurrency
"
class
=
"
input
"
:
class
=
"
!enableConcurrency && 'cursor-not-allowed opacity-50'
"
aria
-
labelledby
=
"
bulk-edit-concurrency-label
"
/>
<
/div
>
<
div
>
<
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
v
-
model
=
"
enablePriority
"
id
=
"
bulk-edit-priority-enabled
"
type
=
"
checkbox
"
aria
-
controls
=
"
bulk-edit-priority
"
class
=
"
rounded border-gray-300 text-primary-600 focus:ring-primary-500
"
/>
<
/div
>
<
input
v
-
model
.
number
=
"
priority
"
id
=
"
bulk-edit-priority
"
type
=
"
number
"
min
=
"
1
"
:
disabled
=
"
!enablePriority
"
class
=
"
input
"
:
class
=
"
!enablePriority && 'cursor-not-allowed opacity-50'
"
aria
-
labelledby
=
"
bulk-edit-priority-label
"
/>
<
/div
>
<
/div
>
...
...
@@ -464,39 +540,69 @@
<!--
Status
-->
<
div
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
<
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
v
-
model
=
"
enableStatus
"
id
=
"
bulk-edit-status-enabled
"
type
=
"
checkbox
"
aria
-
controls
=
"
bulk-edit-status
"
class
=
"
rounded border-gray-300 text-primary-600 focus:ring-primary-500
"
/>
<
/div
>
<
div
:
class
=
"
!enableStatus && 'pointer-events-none opacity-50'
"
>
<
Select
v
-
model
=
"
status
"
:
options
=
"
statusOptions
"
/>
<
div
id
=
"
bulk-edit-status
"
:
class
=
"
!enableStatus && 'pointer-events-none opacity-50'
"
>
<
Select
v
-
model
=
"
status
"
:
options
=
"
statusOptions
"
aria
-
labelledby
=
"
bulk-edit-status-label
"
/>
<
/div
>
<
/div
>
<!--
Groups
-->
<
div
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
<
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
v
-
model
=
"
enableGroups
"
id
=
"
bulk-edit-groups-enabled
"
type
=
"
checkbox
"
aria
-
controls
=
"
bulk-edit-groups
"
class
=
"
rounded border-gray-300 text-primary-600 focus:ring-primary-500
"
/>
<
/div
>
<
div
:
class
=
"
!enableGroups && 'pointer-events-none opacity-50'
"
>
<
GroupSelector
v
-
model
=
"
groupIds
"
:
groups
=
"
groups
"
/>
<
div
id
=
"
bulk-edit-groups
"
:
class
=
"
!enableGroups && 'pointer-events-none opacity-50'
"
>
<
GroupSelector
v
-
model
=
"
groupIds
"
:
groups
=
"
groups
"
aria
-
labelledby
=
"
bulk-edit-groups-label
"
/>
<
/div
>
<
/div
>
<
/form
>
<!--
Action
buttons
--
>
<
div
class
=
"
flex justify-end gap-3
pt-4
"
>
<
template
#
footer
>
<
div
class
=
"
flex justify-end gap-3
"
>
<
button
type
=
"
button
"
class
=
"
btn btn-secondary
"
@
click
=
"
handleClose
"
>
{{
t
(
'
common.cancel
'
)
}}
<
/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
v
-
if
=
"
submitting
"
class
=
"
-ml-1 mr-2 h-4 w-4 animate-spin
"
...
...
@@ -522,8 +628,8 @@
}}
<
/button
>
<
/div
>
<
/
form
>
<
/
Modal
>
<
/
template
>
<
/
BaseDialog
>
<
/template
>
<
script
setup
lang
=
"
ts
"
>
...
...
@@ -532,7 +638,7 @@ import { useI18n } from 'vue-i18n'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
adminAPI
}
from
'
@/api/admin
'
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
ProxySelector
from
'
@/components/common/ProxySelector.vue
'
import
GroupSelector
from
'
@/components/common/GroupSelector.vue
'
...
...
frontend/src/components/account/CreateAccountModal.vue
View file @
fb9d0878
<
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 -->
<div
v-if=
"isOAuthFlow"
class=
"mb-6 flex items-center justify-center"
>
<div
class=
"flex items-center space-x-4"
>
...
...
@@ -34,7 +39,12 @@
</div>
<!-- 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>
<label
class=
"input-label"
>
{{
t
(
'
admin.accounts.accountName
'
)
}}
</label>
<input
...
...
@@ -963,11 +973,40 @@
<!--
Group
Selection
-->
<
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
"
>
{{
t
(
'
common.cancel
'
)
}}
<
/button
>
<
button
type
=
"
submit
"
:
disabled
=
"
submitting
"
class
=
"
btn btn-primary
"
>
<
button
type
=
"
submit
"
form
=
"
create-account-form
"
:
disabled
=
"
submitting
"
class
=
"
btn btn-primary
"
>
<
svg
v
-
if
=
"
submitting
"
class
=
"
-ml-1 mr-2 h-4 w-4 animate-spin
"
...
...
@@ -997,28 +1036,7 @@
}}
<
/button
>
<
/div
>
<
/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
class
=
"
flex justify-between gap-3 pt-4
"
>
<
div
v
-
else
class
=
"
flex justify-between gap-3
"
>
<
button
type
=
"
button
"
class
=
"
btn btn-secondary
"
@
click
=
"
goBackToBasicInfo
"
>
{{
t
(
'
common.back
'
)
}}
<
/button
>
...
...
@@ -1056,8 +1074,8 @@
}}
<
/button
>
<
/div
>
<
/
div
>
<
/
Modal
>
<
/
template
>
<
/
BaseDialog
>
<
/template
>
<
script
setup
lang
=
"
ts
"
>
...
...
@@ -1073,7 +1091,7 @@ import {
import
{
useOpenAIOAuth
}
from
'
@/composables/useOpenAIOAuth
'
import
{
useGeminiOAuth
}
from
'
@/composables/useGeminiOAuth
'
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
GroupSelector
from
'
@/components/common/GroupSelector.vue
'
import
OAuthAuthorizationFlow
from
'
./OAuthAuthorizationFlow.vue
'
...
...
frontend/src/components/account/EditAccountModal.vue
View file @
fb9d0878
<
template
>
<Modal
:show=
"show"
:title=
"t('admin.accounts.editAccount')"
size=
"xl"
@
close=
"handleClose"
>
<form
v-if=
"account"
@
submit.prevent=
"handleSubmit"
class=
"space-y-5"
>
<BaseDialog
: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>
<label
class=
"input-label"
>
{{
t
(
'
common.name
'
)
}}
</label>
<input
v-model=
"form.name"
type=
"text"
required
class=
"input"
/>
...
...
@@ -459,11 +469,19 @@
<!--
Group
Selection
-->
<
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
"
>
{{
t
(
'
common.cancel
'
)
}}
<
/button
>
<
button
type
=
"
submit
"
:
disabled
=
"
submitting
"
class
=
"
btn btn-primary
"
>
<
button
type
=
"
submit
"
form
=
"
edit-account-form
"
:
disabled
=
"
submitting
"
class
=
"
btn btn-primary
"
>
<
svg
v
-
if
=
"
submitting
"
class
=
"
-ml-1 mr-2 h-4 w-4 animate-spin
"
...
...
@@ -487,8 +505,8 @@
{{
submitting
?
t
(
'
admin.accounts.updating
'
)
:
t
(
'
common.update
'
)
}}
<
/button
>
<
/div
>
<
/
form
>
<
/
Modal
>
<
/
template
>
<
/
BaseDialog
>
<
/template
>
<
script
setup
lang
=
"
ts
"
>
...
...
@@ -497,7 +515,7 @@ import { useI18n } from 'vue-i18n'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
adminAPI
}
from
'
@/api/admin
'
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
ProxySelector
from
'
@/components/common/ProxySelector.vue
'
import
GroupSelector
from
'
@/components/common/GroupSelector.vue
'
...
...
frontend/src/components/account/OAuthAuthorizationFlow.vue
View file @
fb9d0878
<
template
>
<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 h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-blue-500"
>
...
...
frontend/src/components/account/ReAuthAccountModal.vue
View file @
fb9d0878
<
template
>
<
Modal
<
BaseDialog
:show=
"show"
:title=
"t('admin.accounts.reAuthorizeAccount')"
size=
"lg
"
width=
"wide
"
@
close=
"handleClose"
>
<div
v-if=
"account"
class=
"space-y-
5
"
>
<div
v-if=
"account"
class=
"space-y-
4
"
>
<!-- Account Info -->
<div
class=
"rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-dark-600 dark:bg-dark-700"
...
...
@@ -53,8 +53,8 @@
</div>
<!-- Add Method Selection (Claude only) -->
<
div
v-if=
"isAnthropic"
>
<l
abel
class=
"input-label"
>
{{
t
(
'
admin.accounts.oauth.authMethod
'
)
}}
</l
abel
>
<
fieldset
v-if=
"isAnthropic"
class=
"border-0 p-0"
>
<l
egend
class=
"input-label"
>
{{
t
(
'
admin.accounts.oauth.authMethod
'
)
}}
</l
egend
>
<div
class=
"mt-2 flex gap-4"
>
<label
class=
"flex cursor-pointer items-center"
>
<input
...
...
@@ -79,11 +79,11 @@
}}
</span>
</label>
</div>
</
div
>
</
fieldset
>
<!-- Gemini OAuth Type Selection -->
<
div
v-if=
"isGemini"
>
<l
abel
class=
"input-label"
>
{{
t
(
'
admin.accounts.oauth.gemini.oauthTypeLabel
'
)
}}
</l
abel
>
<
fieldset
v-if=
"isGemini"
class=
"border-0 p-0"
>
<l
egend
class=
"input-label"
>
{{
t
(
'
admin.accounts.oauth.gemini.oauthTypeLabel
'
)
}}
</l
egend
>
<div
class=
"mt-2 grid grid-cols-2 gap-3"
>
<button
type=
"button"
...
...
@@ -187,7 +187,7 @@
</div>
</button>
</div>
</
div
>
</
fieldset
>
<OAuthAuthorizationFlow
ref=
"oauthFlowRef"
...
...
@@ -207,7 +207,10 @@
@
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"
>
{{
t
(
'
common.cancel
'
)
}}
</button>
...
...
@@ -245,8 +248,8 @@
}}
</button>
</div>
</
div
>
</
Modal
>
</
template
>
</
BaseDialog
>
</template>
<
script
setup
lang=
"ts"
>
...
...
@@ -262,7 +265,7 @@ import {
import
{
useOpenAIOAuth
}
from
'
@/composables/useOpenAIOAuth
'
import
{
useGeminiOAuth
}
from
'
@/composables/useGeminiOAuth
'
import
type
{
Account
}
from
'
@/types
'
import
Modal
from
'
@/components/common/
Modal
.vue
'
import
BaseDialog
from
'
@/components/common/
BaseDialog
.vue
'
import
OAuthAuthorizationFlow
from
'
./OAuthAuthorizationFlow.vue
'
// Type for exposed OAuthAuthorizationFlow component
...
...
frontend/src/components/account/SyncFromCrsModal.vue
View file @
fb9d0878
<
template
>
<
Modal
<
BaseDialog
:show=
"show"
:title=
"t('admin.accounts.syncFromCrsTitle')"
size=
"lg
"
width=
"normal
"
close-on-click-outside
@
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"
>
{{
t
(
'
admin.accounts.syncFromCrsDesc
'
)
}}
</div>
...
...
@@ -84,25 +84,30 @@
<
/div
>
<
/div
>
<
/div
>
<
/
div
>
<
/
form
>
<
template
#
footer
>
<
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
'
)
}}
<
/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
'
)
}}
<
/button
>
<
/div
>
<
/template
>
<
/
Modal
>
<
/
BaseDialog
>
<
/template
>
<
script
setup
lang
=
"
ts
"
>
import
{
computed
,
reactive
,
ref
,
watch
}
from
'
vue
'
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
{
adminAPI
}
from
'
@/api/admin
'
...
...
Prev
1
2
3
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment