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
3c341947
Commit
3c341947
authored
Dec 29, 2025
by
yangjianbo
Browse files
Merge branch 'main' into test-dev
parents
3a7d3387
c01db6b1
Changes
78
Hide whitespace changes
Inline
Side-by-side
backend/internal/service/gateway_service.go
View file @
3c341947
...
@@ -313,7 +313,10 @@ func (s *GatewayService) SelectAccountForModelWithExclusions(ctx context.Context
...
@@ -313,7 +313,10 @@ func (s *GatewayService) SelectAccountForModelWithExclusions(ctx context.Context
// 2. 获取可调度账号列表(排除限流和过载的账号,仅限 Anthropic 平台)
// 2. 获取可调度账号列表(排除限流和过载的账号,仅限 Anthropic 平台)
var
accounts
[]
Account
var
accounts
[]
Account
var
err
error
var
err
error
if
groupID
!=
nil
{
if
s
.
cfg
.
RunMode
==
config
.
RunModeSimple
{
// 简易模式:忽略 groupID,查询所有可用账号
accounts
,
err
=
s
.
accountRepo
.
ListSchedulableByPlatform
(
ctx
,
PlatformAnthropic
)
}
else
if
groupID
!=
nil
{
accounts
,
err
=
s
.
accountRepo
.
ListSchedulableByGroupIDAndPlatform
(
ctx
,
*
groupID
,
PlatformAnthropic
)
accounts
,
err
=
s
.
accountRepo
.
ListSchedulableByGroupIDAndPlatform
(
ctx
,
*
groupID
,
PlatformAnthropic
)
}
else
{
}
else
{
accounts
,
err
=
s
.
accountRepo
.
ListSchedulableByPlatform
(
ctx
,
PlatformAnthropic
)
accounts
,
err
=
s
.
accountRepo
.
ListSchedulableByPlatform
(
ctx
,
PlatformAnthropic
)
...
@@ -1065,6 +1068,12 @@ func (s *GatewayService) RecordUsage(ctx context.Context, input *RecordUsageInpu
...
@@ -1065,6 +1068,12 @@ func (s *GatewayService) RecordUsage(ctx context.Context, input *RecordUsageInpu
log
.
Printf
(
"Create usage log failed: %v"
,
err
)
log
.
Printf
(
"Create usage log failed: %v"
,
err
)
}
}
if
s
.
cfg
!=
nil
&&
s
.
cfg
.
RunMode
==
config
.
RunModeSimple
{
log
.
Printf
(
"[SIMPLE MODE] Usage recorded (not billed): user=%d, tokens=%d"
,
usageLog
.
UserID
,
usageLog
.
TotalTokens
())
s
.
deferredService
.
ScheduleLastUsedUpdate
(
account
.
ID
)
return
nil
}
// 根据计费类型执行扣费
// 根据计费类型执行扣费
if
isSubscriptionBilling
{
if
isSubscriptionBilling
{
// 订阅模式:更新订阅用量(使用 TotalCost 原始费用,不考虑倍率)
// 订阅模式:更新订阅用量(使用 TotalCost 原始费用,不考虑倍率)
...
...
backend/internal/service/openai_gateway_service.go
View file @
3c341947
...
@@ -10,6 +10,7 @@ import (
...
@@ -10,6 +10,7 @@ import (
"errors"
"errors"
"fmt"
"fmt"
"io"
"io"
"log"
"net/http"
"net/http"
"regexp"
"regexp"
"strconv"
"strconv"
...
@@ -155,7 +156,10 @@ func (s *OpenAIGatewayService) SelectAccountForModelWithExclusions(ctx context.C
...
@@ -155,7 +156,10 @@ func (s *OpenAIGatewayService) SelectAccountForModelWithExclusions(ctx context.C
// 2. Get schedulable OpenAI accounts
// 2. Get schedulable OpenAI accounts
var
accounts
[]
Account
var
accounts
[]
Account
var
err
error
var
err
error
if
groupID
!=
nil
{
// 简易模式:忽略分组限制,查询所有可用账号
if
s
.
cfg
.
RunMode
==
config
.
RunModeSimple
{
accounts
,
err
=
s
.
accountRepo
.
ListSchedulableByPlatform
(
ctx
,
PlatformOpenAI
)
}
else
if
groupID
!=
nil
{
accounts
,
err
=
s
.
accountRepo
.
ListSchedulableByGroupIDAndPlatform
(
ctx
,
*
groupID
,
PlatformOpenAI
)
accounts
,
err
=
s
.
accountRepo
.
ListSchedulableByGroupIDAndPlatform
(
ctx
,
*
groupID
,
PlatformOpenAI
)
}
else
{
}
else
{
accounts
,
err
=
s
.
accountRepo
.
ListSchedulableByPlatform
(
ctx
,
PlatformOpenAI
)
accounts
,
err
=
s
.
accountRepo
.
ListSchedulableByPlatform
(
ctx
,
PlatformOpenAI
)
...
@@ -754,6 +758,12 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
...
@@ -754,6 +758,12 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
_
=
s
.
usageLogRepo
.
Create
(
ctx
,
usageLog
)
_
=
s
.
usageLogRepo
.
Create
(
ctx
,
usageLog
)
if
s
.
cfg
!=
nil
&&
s
.
cfg
.
RunMode
==
config
.
RunModeSimple
{
log
.
Printf
(
"[SIMPLE MODE] Usage recorded (not billed): user=%d, tokens=%d"
,
usageLog
.
UserID
,
usageLog
.
TotalTokens
())
s
.
deferredService
.
ScheduleLastUsedUpdate
(
account
.
ID
)
return
nil
}
// Deduct based on billing type
// Deduct based on billing type
if
isSubscriptionBilling
{
if
isSubscriptionBilling
{
if
cost
.
TotalCost
>
0
{
if
cost
.
TotalCost
>
0
{
...
...
backend/internal/service/user_service.go
View file @
3c341947
...
@@ -164,6 +164,14 @@ func (s *UserService) UpdateBalance(ctx context.Context, userID int64, amount fl
...
@@ -164,6 +164,14 @@ func (s *UserService) UpdateBalance(ctx context.Context, userID int64, amount fl
return
nil
return
nil
}
}
// UpdateConcurrency 更新用户并发数(管理员功能)
func
(
s
*
UserService
)
UpdateConcurrency
(
ctx
context
.
Context
,
userID
int64
,
concurrency
int
)
error
{
if
err
:=
s
.
userRepo
.
UpdateConcurrency
(
ctx
,
userID
,
concurrency
);
err
!=
nil
{
return
fmt
.
Errorf
(
"update concurrency: %w"
,
err
)
}
return
nil
}
// UpdateStatus 更新用户状态(管理员功能)
// UpdateStatus 更新用户状态(管理员功能)
func
(
s
*
UserService
)
UpdateStatus
(
ctx
context
.
Context
,
userID
int64
,
status
string
)
error
{
func
(
s
*
UserService
)
UpdateStatus
(
ctx
context
.
Context
,
userID
int64
,
status
string
)
error
{
user
,
err
:=
s
.
userRepo
.
GetByID
(
ctx
,
userID
)
user
,
err
:=
s
.
userRepo
.
GetByID
(
ctx
,
userID
)
...
...
deploy/.env.example
View file @
3c341947
...
@@ -20,6 +20,10 @@ SERVER_PORT=8080
...
@@ -20,6 +20,10 @@ SERVER_PORT=8080
# Server mode: release or debug
# Server mode: release or debug
SERVER_MODE=release
SERVER_MODE=release
# 运行模式: standard (默认) 或 simple (内部自用)
# standard: 完整 SaaS 功能,包含计费/余额校验;simple: 隐藏 SaaS 功能并跳过计费/余额校验
RUN_MODE=standard
# Timezone
# Timezone
TZ=Asia/Shanghai
TZ=Asia/Shanghai
...
...
deploy/config.example.yaml
View file @
3c341947
...
@@ -13,6 +13,14 @@ server:
...
@@ -13,6 +13,14 @@ server:
# Mode: "debug" for development, "release" for production
# Mode: "debug" for development, "release" for production
mode
:
"
release"
mode
:
"
release"
# =============================================================================
# Run Mode Configuration
# =============================================================================
# Run mode: "standard" (default) or "simple" (for internal use)
# - standard: Full SaaS features with billing/balance checks
# - simple: Hides SaaS features and skips billing/balance checks
run_mode
:
"
standard"
# =============================================================================
# =============================================================================
# Database Configuration (PostgreSQL)
# Database Configuration (PostgreSQL)
# =============================================================================
# =============================================================================
...
...
deploy/docker-compose.yml
View file @
3c341947
...
@@ -36,6 +36,7 @@ services:
...
@@ -36,6 +36,7 @@ services:
-
SERVER_HOST=0.0.0.0
-
SERVER_HOST=0.0.0.0
-
SERVER_PORT=8080
-
SERVER_PORT=8080
-
SERVER_MODE=${SERVER_MODE:-release}
-
SERVER_MODE=${SERVER_MODE:-release}
-
RUN_MODE=${RUN_MODE:-standard}
# =======================================================================
# =======================================================================
# Database Configuration (PostgreSQL)
# Database Configuration (PostgreSQL)
...
...
frontend/src/App.vue
View file @
3c341947
...
@@ -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
{
...
...
frontend/src/api/admin/accounts.ts
View file @
3c341947
...
@@ -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
}
}
...
...
frontend/src/api/admin/groups.ts
View file @
3c341947
...
@@ -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
}
}
...
...
frontend/src/api/admin/proxies.ts
View file @
3c341947
...
@@ -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
}
}
...
...
frontend/src/api/admin/redeem.ts
View file @
3c341947
...
@@ -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
}
}
...
...
frontend/src/api/admin/subscriptions.ts
View file @
3c341947
...
@@ -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
...
...
frontend/src/api/admin/usage.ts
View file @
3c341947
...
@@ -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
}
}
...
...
frontend/src/api/admin/users.ts
View file @
3c341947
...
@@ -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
}
}
...
...
frontend/src/api/auth.ts
View file @
3c341947
...
@@ -8,7 +8,7 @@ import type {
...
@@ -8,7 +8,7 @@ import type {
LoginRequest
,
LoginRequest
,
RegisterRequest
,
RegisterRequest
,
AuthResponse
,
AuthResponse
,
User
,
CurrentUserResponse
,
SendVerifyCodeRequest
,
SendVerifyCodeRequest
,
SendVerifyCodeResponse
,
SendVerifyCodeResponse
,
PublicSettings
PublicSettings
...
@@ -70,9 +70,8 @@ export async function register(userData: RegisterRequest): Promise<AuthResponse>
...
@@ -70,9 +70,8 @@ export async function register(userData: RegisterRequest): Promise<AuthResponse>
* Get current authenticated user
* Get current authenticated user
* @returns User profile data
* @returns User profile data
*/
*/
export
async
function
getCurrentUser
():
Promise
<
User
>
{
export
async
function
getCurrentUser
()
{
const
{
data
}
=
await
apiClient
.
get
<
User
>
(
'
/auth/me
'
)
return
apiClient
.
get
<
CurrentUserResponse
>
(
'
/auth/me
'
)
return
data
}
}
/**
/**
...
...
frontend/src/api/keys.ts
View file @
3c341947
...
@@ -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
}
}
...
...
frontend/src/api/usage.ts
View file @
3c341947
...
@@ -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
...
...
frontend/src/components/account/AccountStatsModal.vue
View file @
3c341947
<
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
'
...
...
frontend/src/components/account/AccountTestModal.vue
View file @
3c341947
<
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
'
...
...
frontend/src/components/account/BulkEditAccountModal.vue
View file @
3c341947
<
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
'
...
...
Prev
1
2
3
4
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