"backend/internal/handler/vscode:/vscode.git/clone" did not exist on "b7e878de642a2e6753cca130d404d3475eb2cbc1"
Commit 3c341947 authored by yangjianbo's avatar yangjianbo
Browse files

Merge branch 'main' into test-dev

parents 3a7d3387 c01db6b1
...@@ -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 原始费用,不考虑倍率)
......
...@@ -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 {
......
...@@ -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)
......
...@@ -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
......
...@@ -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)
# ============================================================================= # =============================================================================
......
...@@ -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)
......
...@@ -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
} }
......
...@@ -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
} }
/** /**
......
...@@ -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'
......
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