Commit 706af292 authored by shaw's avatar shaw
Browse files

Merge branch 'fix/account-filters-menu-missing-features'

## Summary
- 修复账号管理页面组件拆分时遗漏的功能
- 统一所有内联 SVG 为 Icon 组件
- 修复 ProxySelector 选择"无代理"时发送错误值的问题

## Changes
- AccountTableFilters: 添加 Antigravity 平台选项、类型筛选器、inactive 状态
- AccountActionMenu: 恢复重置状态和清除限速按钮
- AccountsView: 修正 handleClearRateLimit 调用正确的 API
- ProxySelector: 修复选择"无代理"时发送 null 而不是 0

## Conflict Resolution
- ProxySelector.vue: 采用 PR 分支的正确逻辑(发送 null 而不是 0)
  这是正确的修复,因为后端使用 *int64 类型,nil 会触发 ClearProxyID()
parents 4d078a88 b6a41829
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"io" "io"
"log" "log"
mathrand "math/rand"
"net/http" "net/http"
"strings" "strings"
"sync/atomic" "sync/atomic"
...@@ -405,6 +406,14 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context, ...@@ -405,6 +406,14 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context,
// 重试循环 // 重试循环
var resp *http.Response var resp *http.Response
for attempt := 1; attempt <= antigravityMaxRetries; attempt++ { for attempt := 1; attempt <= antigravityMaxRetries; attempt++ {
// 检查 context 是否已取消(客户端断开连接)
select {
case <-ctx.Done():
log.Printf("%s status=context_canceled error=%v", prefix, ctx.Err())
return nil, ctx.Err()
default:
}
upstreamReq, err := antigravity.NewAPIRequest(ctx, action, accessToken, geminiBody) upstreamReq, err := antigravity.NewAPIRequest(ctx, action, accessToken, geminiBody)
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -414,7 +423,10 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context, ...@@ -414,7 +423,10 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context,
if err != nil { if err != nil {
if attempt < antigravityMaxRetries { if attempt < antigravityMaxRetries {
log.Printf("%s status=request_failed retry=%d/%d error=%v", prefix, attempt, antigravityMaxRetries, err) log.Printf("%s status=request_failed retry=%d/%d error=%v", prefix, attempt, antigravityMaxRetries, err)
sleepAntigravityBackoff(attempt) if !sleepAntigravityBackoffWithContext(ctx, attempt) {
log.Printf("%s status=context_canceled_during_backoff", prefix)
return nil, ctx.Err()
}
continue continue
} }
log.Printf("%s status=request_failed retries_exhausted error=%v", prefix, err) log.Printf("%s status=request_failed retries_exhausted error=%v", prefix, err)
...@@ -427,7 +439,10 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context, ...@@ -427,7 +439,10 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context,
if attempt < antigravityMaxRetries { if attempt < antigravityMaxRetries {
log.Printf("%s status=%d retry=%d/%d", prefix, resp.StatusCode, attempt, antigravityMaxRetries) log.Printf("%s status=%d retry=%d/%d", prefix, resp.StatusCode, attempt, antigravityMaxRetries)
sleepAntigravityBackoff(attempt) if !sleepAntigravityBackoffWithContext(ctx, attempt) {
log.Printf("%s status=context_canceled_during_backoff", prefix)
return nil, ctx.Err()
}
continue continue
} }
// 所有重试都失败,标记限流状态 // 所有重试都失败,标记限流状态
...@@ -901,6 +916,14 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co ...@@ -901,6 +916,14 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
// 重试循环 // 重试循环
var resp *http.Response var resp *http.Response
for attempt := 1; attempt <= antigravityMaxRetries; attempt++ { for attempt := 1; attempt <= antigravityMaxRetries; attempt++ {
// 检查 context 是否已取消(客户端断开连接)
select {
case <-ctx.Done():
log.Printf("%s status=context_canceled error=%v", prefix, ctx.Err())
return nil, ctx.Err()
default:
}
upstreamReq, err := antigravity.NewAPIRequest(ctx, upstreamAction, accessToken, wrappedBody) upstreamReq, err := antigravity.NewAPIRequest(ctx, upstreamAction, accessToken, wrappedBody)
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -910,7 +933,10 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co ...@@ -910,7 +933,10 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
if err != nil { if err != nil {
if attempt < antigravityMaxRetries { if attempt < antigravityMaxRetries {
log.Printf("%s status=request_failed retry=%d/%d error=%v", prefix, attempt, antigravityMaxRetries, err) log.Printf("%s status=request_failed retry=%d/%d error=%v", prefix, attempt, antigravityMaxRetries, err)
sleepAntigravityBackoff(attempt) if !sleepAntigravityBackoffWithContext(ctx, attempt) {
log.Printf("%s status=context_canceled_during_backoff", prefix)
return nil, ctx.Err()
}
continue continue
} }
log.Printf("%s status=request_failed retries_exhausted error=%v", prefix, err) log.Printf("%s status=request_failed retries_exhausted error=%v", prefix, err)
...@@ -923,7 +949,10 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co ...@@ -923,7 +949,10 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
if attempt < antigravityMaxRetries { if attempt < antigravityMaxRetries {
log.Printf("%s status=%d retry=%d/%d", prefix, resp.StatusCode, attempt, antigravityMaxRetries) log.Printf("%s status=%d retry=%d/%d", prefix, resp.StatusCode, attempt, antigravityMaxRetries)
sleepAntigravityBackoff(attempt) if !sleepAntigravityBackoffWithContext(ctx, attempt) {
log.Printf("%s status=context_canceled_during_backoff", prefix)
return nil, ctx.Err()
}
continue continue
} }
// 所有重试都失败,标记限流状态 // 所有重试都失败,标记限流状态
...@@ -1058,8 +1087,28 @@ func (s *AntigravityGatewayService) shouldFailoverUpstreamError(statusCode int) ...@@ -1058,8 +1087,28 @@ func (s *AntigravityGatewayService) shouldFailoverUpstreamError(statusCode int)
} }
} }
func sleepAntigravityBackoff(attempt int) { // sleepAntigravityBackoffWithContext 带 context 取消检查的退避等待
sleepGeminiBackoff(attempt) // 复用 Gemini 的退避逻辑 // 返回 true 表示正常完成等待,false 表示 context 已取消
func sleepAntigravityBackoffWithContext(ctx context.Context, attempt int) bool {
delay := geminiRetryBaseDelay * time.Duration(1<<uint(attempt-1))
if delay > geminiRetryMaxDelay {
delay = geminiRetryMaxDelay
}
// +/- 20% jitter
r := mathrand.New(mathrand.NewSource(time.Now().UnixNano()))
jitter := time.Duration(float64(delay) * 0.2 * (r.Float64()*2 - 1))
sleepFor := delay + jitter
if sleepFor < 0 {
sleepFor = 0
}
select {
case <-ctx.Done():
return false
case <-time.After(sleepFor):
return true
}
} }
func (s *AntigravityGatewayService) handleUpstreamError(ctx context.Context, prefix string, account *Account, statusCode int, headers http.Header, body []byte) { func (s *AntigravityGatewayService) handleUpstreamError(ctx context.Context, prefix string, account *Account, statusCode int, headers http.Header, body []byte) {
......
...@@ -15,14 +15,7 @@ ...@@ -15,14 +15,7 @@
<div <div
class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-primary-600" class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-primary-600"
> >
<svg class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="chartBar" size="md" class="text-white" :stroke-width="2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/>
</svg>
</div> </div>
<div> <div>
<div class="font-semibold text-gray-900 dark:text-gray-100">{{ account.name }}</div> <div class="font-semibold text-gray-900 dark:text-gray-100">{{ account.name }}</div>
...@@ -97,19 +90,7 @@ ...@@ -97,19 +90,7 @@
t('admin.accounts.stats.totalRequests') t('admin.accounts.stats.totalRequests')
}}</span> }}</span>
<div class="rounded-lg bg-blue-100 p-1.5 dark:bg-blue-900/30"> <div class="rounded-lg bg-blue-100 p-1.5 dark:bg-blue-900/30">
<svg <Icon name="bolt" size="sm" class="text-blue-600 dark:text-blue-400" :stroke-width="2" />
class="h-4 w-4 text-blue-600 dark:text-blue-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
</div> </div>
</div> </div>
<p class="text-2xl font-bold text-gray-900 dark:text-white"> <p class="text-2xl font-bold text-gray-900 dark:text-white">
...@@ -129,19 +110,12 @@ ...@@ -129,19 +110,12 @@
t('admin.accounts.stats.avgDailyCost') t('admin.accounts.stats.avgDailyCost')
}}</span> }}</span>
<div class="rounded-lg bg-amber-100 p-1.5 dark:bg-amber-900/30"> <div class="rounded-lg bg-amber-100 p-1.5 dark:bg-amber-900/30">
<svg <Icon
class="h-4 w-4 text-amber-600 dark:text-amber-400" name="calculator"
fill="none" size="sm"
viewBox="0 0 24 24" class="text-amber-600 dark:text-amber-400"
stroke="currentColor" :stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"
/> />
</svg>
</div> </div>
</div> </div>
<p class="text-2xl font-bold text-gray-900 dark:text-white"> <p class="text-2xl font-bold text-gray-900 dark:text-white">
...@@ -245,19 +219,12 @@ ...@@ -245,19 +219,12 @@
<div class="card p-4"> <div class="card p-4">
<div class="mb-3 flex items-center gap-2"> <div class="mb-3 flex items-center gap-2">
<div class="rounded-lg bg-orange-100 p-1.5 dark:bg-orange-900/30"> <div class="rounded-lg bg-orange-100 p-1.5 dark:bg-orange-900/30">
<svg <Icon
class="h-4 w-4 text-orange-600 dark:text-orange-400" name="fire"
fill="none" size="sm"
viewBox="0 0 24 24" class="text-orange-600 dark:text-orange-400"
stroke="currentColor" :stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17.657 18.657A8 8 0 016.343 7.343S7 9 9 10c0-2 .5-5 2.986-7C14 5 16.09 5.777 17.656 7.343A7.975 7.975 0 0120 13a7.975 7.975 0 01-2.343 5.657z"
/> />
</svg>
</div> </div>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ <span class="text-sm font-semibold text-gray-900 dark:text-white">{{
t('admin.accounts.stats.highestCostDay') t('admin.accounts.stats.highestCostDay')
...@@ -295,19 +262,12 @@ ...@@ -295,19 +262,12 @@
<div class="card p-4"> <div class="card p-4">
<div class="mb-3 flex items-center gap-2"> <div class="mb-3 flex items-center gap-2">
<div class="rounded-lg bg-indigo-100 p-1.5 dark:bg-indigo-900/30"> <div class="rounded-lg bg-indigo-100 p-1.5 dark:bg-indigo-900/30">
<svg <Icon
class="h-4 w-4 text-indigo-600 dark:text-indigo-400" name="trendingUp"
fill="none" size="sm"
viewBox="0 0 24 24" class="text-indigo-600 dark:text-indigo-400"
stroke="currentColor" :stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
/> />
</svg>
</div> </div>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ <span class="text-sm font-semibold text-gray-900 dark:text-white">{{
t('admin.accounts.stats.highestRequestDay') t('admin.accounts.stats.highestRequestDay')
...@@ -348,19 +308,7 @@ ...@@ -348,19 +308,7 @@
<div class="card p-4"> <div class="card p-4">
<div class="mb-3 flex items-center gap-2"> <div class="mb-3 flex items-center gap-2">
<div class="rounded-lg bg-teal-100 p-1.5 dark:bg-teal-900/30"> <div class="rounded-lg bg-teal-100 p-1.5 dark:bg-teal-900/30">
<svg <Icon name="cube" size="sm" class="text-teal-600 dark:text-teal-400" :stroke-width="2" />
class="h-4 w-4 text-teal-600 dark:text-teal-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
/>
</svg>
</div> </div>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ <span class="text-sm font-semibold text-gray-900 dark:text-white">{{
t('admin.accounts.stats.accumulatedTokens') t('admin.accounts.stats.accumulatedTokens')
...@@ -390,19 +338,7 @@ ...@@ -390,19 +338,7 @@
<div class="card p-4"> <div class="card p-4">
<div class="mb-3 flex items-center gap-2"> <div class="mb-3 flex items-center gap-2">
<div class="rounded-lg bg-rose-100 p-1.5 dark:bg-rose-900/30"> <div class="rounded-lg bg-rose-100 p-1.5 dark:bg-rose-900/30">
<svg <Icon name="bolt" size="sm" class="text-rose-600 dark:text-rose-400" :stroke-width="2" />
class="h-4 w-4 text-rose-600 dark:text-rose-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
</div> </div>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ <span class="text-sm font-semibold text-gray-900 dark:text-white">{{
t('admin.accounts.stats.performance') t('admin.accounts.stats.performance')
...@@ -432,19 +368,12 @@ ...@@ -432,19 +368,12 @@
<div class="card p-4"> <div class="card p-4">
<div class="mb-3 flex items-center gap-2"> <div class="mb-3 flex items-center gap-2">
<div class="rounded-lg bg-lime-100 p-1.5 dark:bg-lime-900/30"> <div class="rounded-lg bg-lime-100 p-1.5 dark:bg-lime-900/30">
<svg <Icon
class="h-4 w-4 text-lime-600 dark:text-lime-400" name="clipboard"
fill="none" size="sm"
viewBox="0 0 24 24" class="text-lime-600 dark:text-lime-400"
stroke="currentColor" :stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
/> />
</svg>
</div> </div>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ <span class="text-sm font-semibold text-gray-900 dark:text-white">{{
t('admin.accounts.stats.recentActivity') t('admin.accounts.stats.recentActivity')
...@@ -504,14 +433,7 @@ ...@@ -504,14 +433,7 @@
v-else-if="!loading" v-else-if="!loading"
class="flex flex-col items-center justify-center py-12 text-gray-500 dark:text-gray-400" class="flex flex-col items-center justify-center py-12 text-gray-500 dark:text-gray-400"
> >
<svg class="mb-4 h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="chartBar" size="xl" class="mb-4 h-12 w-12" :stroke-width="1.5" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/>
</svg>
<p class="text-sm">{{ t('admin.accounts.stats.noData') }}</p> <p class="text-sm">{{ t('admin.accounts.stats.noData') }}</p>
</div> </div>
</div> </div>
...@@ -547,6 +469,7 @@ import { Line } from 'vue-chartjs' ...@@ -547,6 +469,7 @@ import { Line } from 'vue-chartjs'
import BaseDialog from '@/components/common/BaseDialog.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 Icon from '@/components/icons/Icon.vue'
import { adminAPI } from '@/api/admin' import { adminAPI } from '@/api/admin'
import type { Account, AccountUsageStatsResponse } from '@/types' import type { Account, AccountUsageStatsResponse } from '@/types'
......
...@@ -48,13 +48,7 @@ ...@@ -48,13 +48,7 @@
<span <span
class="inline-flex items-center gap-1 rounded bg-amber-100 px-1.5 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-400" class="inline-flex items-center gap-1 rounded bg-amber-100 px-1.5 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-400"
> >
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <Icon name="exclamationTriangle" size="xs" :stroke-width="2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
429 429
</span> </span>
<!-- Tooltip --> <!-- Tooltip -->
...@@ -73,13 +67,7 @@ ...@@ -73,13 +67,7 @@
<span <span
class="inline-flex items-center gap-1 rounded bg-red-100 px-1.5 py-0.5 text-xs font-medium text-red-700 dark:bg-red-900/30 dark:text-red-400" class="inline-flex items-center gap-1 rounded bg-red-100 px-1.5 py-0.5 text-xs font-medium text-red-700 dark:bg-red-900/30 dark:text-red-400"
> >
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <Icon name="exclamationTriangle" size="xs" :stroke-width="2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
529 529
</span> </span>
<!-- Tooltip --> <!-- Tooltip -->
...@@ -100,6 +88,7 @@ import { computed } from 'vue' ...@@ -100,6 +88,7 @@ import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import type { Account } from '@/types' import type { Account } from '@/types'
import { formatTime } from '@/utils/format' import { formatTime } from '@/utils/format'
import Icon from '@/components/icons/Icon.vue'
const { t } = useI18n() const { t } = useI18n()
......
...@@ -15,14 +15,7 @@ ...@@ -15,14 +15,7 @@
<div <div
class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-primary-600" class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-primary-600"
> >
<svg class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="userCircle" size="md" class="text-white" :stroke-width="2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div> </div>
<div> <div>
<div class="font-semibold text-gray-900 dark:text-gray-100">{{ account.name }}</div> <div class="font-semibold text-gray-900 dark:text-gray-100">{{ account.name }}</div>
...@@ -70,14 +63,7 @@ ...@@ -70,14 +63,7 @@
> >
<!-- Status Line --> <!-- Status Line -->
<div v-if="status === 'idle'" class="flex items-center gap-2 text-gray-500"> <div v-if="status === 'idle'" class="flex items-center gap-2 text-gray-500">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="bolt" size="sm" :stroke-width="2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
<span>{{ t('admin.accounts.readyToTest') }}</span> <span>{{ t('admin.accounts.readyToTest') }}</span>
</div> </div>
<div v-else-if="status === 'connecting'" class="flex items-center gap-2 text-yellow-400"> <div v-else-if="status === 'connecting'" class="flex items-center gap-2 text-yellow-400">
...@@ -128,14 +114,7 @@ ...@@ -128,14 +114,7 @@
v-else-if="status === 'error'" v-else-if="status === 'error'"
class="mt-3 flex items-center gap-2 border-t border-gray-700 pt-3 text-red-400" class="mt-3 flex items-center gap-2 border-t border-gray-700 pt-3 text-red-400"
> >
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="xCircle" size="sm" :stroke-width="2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{{ errorMessage }}</span> <span>{{ errorMessage }}</span>
</div> </div>
</div> </div>
...@@ -147,14 +126,7 @@ ...@@ -147,14 +126,7 @@
class="absolute right-2 top-2 rounded-lg bg-gray-800/80 p-1.5 text-gray-400 opacity-0 transition-all hover:bg-gray-700 hover:text-white group-hover:opacity-100" class="absolute right-2 top-2 rounded-lg bg-gray-800/80 p-1.5 text-gray-400 opacity-0 transition-all hover:bg-gray-700 hover:text-white group-hover:opacity-100"
:title="t('admin.accounts.copyOutput')" :title="t('admin.accounts.copyOutput')"
> >
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="copy" size="sm" :stroke-width="2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</button> </button>
</div> </div>
...@@ -162,26 +134,12 @@ ...@@ -162,26 +134,12 @@
<div class="flex items-center justify-between px-1 text-xs text-gray-500 dark:text-gray-400"> <div class="flex items-center justify-between px-1 text-xs text-gray-500 dark:text-gray-400">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span class="flex items-center gap-1"> <span class="flex items-center gap-1">
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="cpu" size="sm" :stroke-width="2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"
/>
</svg>
{{ t('admin.accounts.testModel') }} {{ t('admin.accounts.testModel') }}
</span> </span>
</div> </div>
<span class="flex items-center gap-1"> <span class="flex items-center gap-1">
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="chatBubble" size="sm" :stroke-width="2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
/>
</svg>
{{ t('admin.accounts.testPrompt') }} {{ t('admin.accounts.testPrompt') }}
</span> </span>
</div> </div>
...@@ -278,6 +236,7 @@ import { ref, watch, nextTick } from 'vue' ...@@ -278,6 +236,7 @@ import { ref, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import BaseDialog from '@/components/common/BaseDialog.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
import Select from '@/components/common/Select.vue' import Select from '@/components/common/Select.vue'
import Icon from '@/components/icons/Icon.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'
......
...@@ -318,19 +318,7 @@ ...@@ -318,19 +318,7 @@
<div v-if="enableCustomErrorCodes" id="bulk-edit-custom-error-codes-body" 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 <Icon name="exclamationTriangle" size="sm" class="mr-1 inline" :stroke-width="2" />
class="mr-1 inline h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
{{ t('admin.accounts.customErrorCodesWarning') }} {{ t('admin.accounts.customErrorCodesWarning') }}
</p> </p>
</div> </div>
...@@ -391,14 +379,7 @@ ...@@ -391,14 +379,7 @@
class="hover:text-red-900 dark:hover:text-red-300" class="hover:text-red-900 dark:hover:text-red-300"
@click="removeErrorCode(code)" @click="removeErrorCode(code)"
> >
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="x" size="xs" class="h-3.5 w-3.5" :stroke-width="2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button> </button>
</span> </span>
<span v-if="selectedErrorCodes.length === 0" class="text-xs text-gray-400"> <span v-if="selectedErrorCodes.length === 0" class="text-xs text-gray-400">
...@@ -642,6 +623,7 @@ import BaseDialog from '@/components/common/BaseDialog.vue' ...@@ -642,6 +623,7 @@ 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'
import Icon from '@/components/icons/Icon.vue'
interface Props { interface Props {
show: boolean show: boolean
......
...@@ -81,19 +81,7 @@ ...@@ -81,19 +81,7 @@
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200' : 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
]" ]"
> >
<svg <Icon name="sparkles" size="sm" />
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456z"
/>
</svg>
Anthropic Anthropic
</button> </button>
<button <button
...@@ -156,19 +144,7 @@ ...@@ -156,19 +144,7 @@
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200' : 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
]" ]"
> >
<svg <Icon name="cloud" size="sm" />
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z"
/>
</svg>
Antigravity Antigravity
</button> </button>
</div> </div>
...@@ -196,19 +172,7 @@ ...@@ -196,19 +172,7 @@
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400' : 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
]" ]"
> >
<svg <Icon name="sparkles" size="sm" />
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456z"
/>
</svg>
</div> </div>
<div> <div>
<span class="block text-sm font-medium text-gray-900 dark:text-white">{{ <span class="block text-sm font-medium text-gray-900 dark:text-white">{{
...@@ -238,19 +202,7 @@ ...@@ -238,19 +202,7 @@
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400' : 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
]" ]"
> >
<svg <Icon name="key" size="sm" />
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
/>
</svg>
</div> </div>
<div> <div>
<span class="block text-sm font-medium text-gray-900 dark:text-white">{{ <span class="block text-sm font-medium text-gray-900 dark:text-white">{{
...@@ -286,19 +238,7 @@ ...@@ -286,19 +238,7 @@
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400' : 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
]" ]"
> >
<svg <Icon name="key" size="sm" />
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
/>
</svg>
</div> </div>
<div> <div>
<span class="block text-sm font-medium text-gray-900 dark:text-white">OAuth</span> <span class="block text-sm font-medium text-gray-900 dark:text-white">OAuth</span>
...@@ -324,19 +264,7 @@ ...@@ -324,19 +264,7 @@
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400' : 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
]" ]"
> >
<svg <Icon name="key" size="sm" />
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
/>
</svg>
</div> </div>
<div> <div>
<span class="block text-sm font-medium text-gray-900 dark:text-white">API Key</span> <span class="block text-sm font-medium text-gray-900 dark:text-white">API Key</span>
...@@ -380,19 +308,7 @@ ...@@ -380,19 +308,7 @@
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400' : 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
]" ]"
> >
<svg <Icon name="key" size="sm" />
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
/>
</svg>
</div> </div>
<div> <div>
<span class="block text-sm font-medium text-gray-900 dark:text-white"> <span class="block text-sm font-medium text-gray-900 dark:text-white">
...@@ -487,9 +403,7 @@ ...@@ -487,9 +403,7 @@
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400' : 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
]" ]"
> >
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"> <Icon name="user" size="sm" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
</svg>
</div> </div>
<div class="min-w-0"> <div class="min-w-0">
<span class="block text-sm font-medium text-gray-900 dark:text-white"> <span class="block text-sm font-medium text-gray-900 dark:text-white">
...@@ -532,9 +446,7 @@ ...@@ -532,9 +446,7 @@
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400' : 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
]" ]"
> >
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"> <Icon name="cloud" size="sm" />
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z" />
</svg>
</div> </div>
<div class="min-w-0"> <div class="min-w-0">
<span class="block text-sm font-medium text-gray-900 dark:text-white"> <span class="block text-sm font-medium text-gray-900 dark:text-white">
...@@ -710,19 +622,7 @@ ...@@ -710,19 +622,7 @@
class="flex items-center gap-3 rounded-lg border-2 border-purple-500 bg-purple-50 p-3 dark:bg-purple-900/20" class="flex items-center gap-3 rounded-lg border-2 border-purple-500 bg-purple-50 p-3 dark:bg-purple-900/20"
> >
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-purple-500 text-white"> <div class="flex h-8 w-8 items-center justify-center rounded-lg bg-purple-500 text-white">
<svg <Icon name="key" size="sm" />
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
/>
</svg>
</div> </div>
<div> <div>
<span class="block text-sm font-medium text-gray-900 dark:text-white">OAuth</span> <span class="block text-sm font-medium text-gray-900 dark:text-white">OAuth</span>
...@@ -1012,19 +912,7 @@ ...@@ -1012,19 +912,7 @@
<div v-if="customErrorCodesEnabled" class="space-y-3"> <div v-if="customErrorCodesEnabled" 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 <Icon name="exclamationTriangle" size="sm" class="mr-1 inline" :stroke-width="2" />
class="mr-1 inline h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
{{ t('admin.accounts.customErrorCodesWarning') }} {{ t('admin.accounts.customErrorCodesWarning') }}
</p> </p>
</div> </div>
...@@ -1083,14 +971,7 @@ ...@@ -1083,14 +971,7 @@
@click="removeErrorCode(code)" @click="removeErrorCode(code)"
class="hover:text-red-900 dark:hover:text-red-300" class="hover:text-red-900 dark:hover:text-red-300"
> >
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="x" size="sm" :stroke-width="2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button> </button>
</span> </span>
<span v-if="selectedErrorCodes.length === 0" class="text-xs text-gray-400"> <span v-if="selectedErrorCodes.length === 0" class="text-xs text-gray-400">
...@@ -1159,19 +1040,7 @@ ...@@ -1159,19 +1040,7 @@
<div v-if="tempUnschedEnabled" class="space-y-3"> <div v-if="tempUnschedEnabled" class="space-y-3">
<div class="rounded-lg bg-blue-50 p-3 dark:bg-blue-900/20"> <div class="rounded-lg bg-blue-50 p-3 dark:bg-blue-900/20">
<p class="text-xs text-blue-700 dark:text-blue-400"> <p class="text-xs text-blue-700 dark:text-blue-400">
<svg <Icon name="exclamationTriangle" size="sm" class="mr-1 inline" :stroke-width="2" />
class="mr-1 inline h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
{{ t('admin.accounts.tempUnschedulable.notice') }} {{ t('admin.accounts.tempUnschedulable.notice') }}
</p> </p>
</div> </div>
...@@ -1205,9 +1074,7 @@ ...@@ -1205,9 +1074,7 @@
@click="moveTempUnschedRule(index, -1)" @click="moveTempUnschedRule(index, -1)"
class="rounded p-1 text-gray-400 transition-colors hover:text-gray-600 disabled:cursor-not-allowed disabled:opacity-40 dark:hover:text-gray-200" class="rounded p-1 text-gray-400 transition-colors hover:text-gray-600 disabled:cursor-not-allowed disabled:opacity-40 dark:hover:text-gray-200"
> >
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="chevronUp" size="sm" :stroke-width="2" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
</svg>
</button> </button>
<button <button
type="button" type="button"
...@@ -1224,14 +1091,7 @@ ...@@ -1224,14 +1091,7 @@
@click="removeTempUnschedRule(index)" @click="removeTempUnschedRule(index)"
class="rounded p-1 text-red-500 transition-colors hover:text-red-600" class="rounded p-1 text-red-500 transition-colors hover:text-red-600"
> >
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="x" size="sm" :stroke-width="2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button> </button>
</div> </div>
</div> </div>
...@@ -1734,6 +1594,7 @@ import { useGeminiOAuth } from '@/composables/useGeminiOAuth' ...@@ -1734,6 +1594,7 @@ import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth' import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth'
import type { Proxy, Group, AccountPlatform, AccountType } from '@/types' import type { Proxy, Group, AccountPlatform, AccountType } from '@/types'
import BaseDialog from '@/components/common/BaseDialog.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
import Icon from '@/components/icons/Icon.vue'
import ProxySelector from '@/components/common/ProxySelector.vue' import ProxySelector from '@/components/common/ProxySelector.vue'
import GroupSelector from '@/components/common/GroupSelector.vue' import GroupSelector from '@/components/common/GroupSelector.vue'
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue' import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
......
...@@ -265,19 +265,7 @@ ...@@ -265,19 +265,7 @@
<div v-if="customErrorCodesEnabled" class="space-y-3"> <div v-if="customErrorCodesEnabled" 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 <Icon name="exclamationTriangle" size="sm" class="mr-1 inline" :stroke-width="2" />
class="mr-1 inline h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
{{ t('admin.accounts.customErrorCodesWarning') }} {{ t('admin.accounts.customErrorCodesWarning') }}
</p> </p>
</div> </div>
...@@ -336,14 +324,7 @@ ...@@ -336,14 +324,7 @@
@click="removeErrorCode(code)" @click="removeErrorCode(code)"
class="hover:text-red-900 dark:hover:text-red-300" class="hover:text-red-900 dark:hover:text-red-300"
> >
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="x" size="sm" :stroke-width="2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button> </button>
</span> </span>
<span v-if="selectedErrorCodes.length === 0" class="text-xs text-gray-400"> <span v-if="selectedErrorCodes.length === 0" class="text-xs text-gray-400">
...@@ -412,19 +393,7 @@ ...@@ -412,19 +393,7 @@
<div v-if="tempUnschedEnabled" class="space-y-3"> <div v-if="tempUnschedEnabled" class="space-y-3">
<div class="rounded-lg bg-blue-50 p-3 dark:bg-blue-900/20"> <div class="rounded-lg bg-blue-50 p-3 dark:bg-blue-900/20">
<p class="text-xs text-blue-700 dark:text-blue-400"> <p class="text-xs text-blue-700 dark:text-blue-400">
<svg <Icon name="exclamationTriangle" size="sm" class="mr-1 inline" :stroke-width="2" />
class="mr-1 inline h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
{{ t('admin.accounts.tempUnschedulable.notice') }} {{ t('admin.accounts.tempUnschedulable.notice') }}
</p> </p>
</div> </div>
...@@ -458,9 +427,7 @@ ...@@ -458,9 +427,7 @@
@click="moveTempUnschedRule(index, -1)" @click="moveTempUnschedRule(index, -1)"
class="rounded p-1 text-gray-400 transition-colors hover:text-gray-600 disabled:cursor-not-allowed disabled:opacity-40 dark:hover:text-gray-200" class="rounded p-1 text-gray-400 transition-colors hover:text-gray-600 disabled:cursor-not-allowed disabled:opacity-40 dark:hover:text-gray-200"
> >
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="chevronUp" size="sm" :stroke-width="2" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
</svg>
</button> </button>
<button <button
type="button" type="button"
...@@ -477,14 +444,7 @@ ...@@ -477,14 +444,7 @@
@click="removeTempUnschedRule(index)" @click="removeTempUnschedRule(index)"
class="rounded p-1 text-red-500 transition-colors hover:text-red-600" class="rounded p-1 text-red-500 transition-colors hover:text-red-600"
> >
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="x" size="sm" :stroke-width="2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button> </button>
</div> </div>
</div> </div>
...@@ -702,6 +662,7 @@ import { adminAPI } from '@/api/admin' ...@@ -702,6 +662,7 @@ import { adminAPI } from '@/api/admin'
import type { Account, Proxy, Group } from '@/types' import type { Account, Proxy, Group } from '@/types'
import BaseDialog from '@/components/common/BaseDialog.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
import Select from '@/components/common/Select.vue' import Select from '@/components/common/Select.vue'
import Icon from '@/components/icons/Icon.vue'
import ProxySelector from '@/components/common/ProxySelector.vue' import ProxySelector from '@/components/common/ProxySelector.vue'
import GroupSelector from '@/components/common/GroupSelector.vue' import GroupSelector from '@/components/common/GroupSelector.vue'
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue' import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
......
...@@ -21,9 +21,7 @@ ...@@ -21,9 +21,7 @@
@click.stop="removeModel(model)" @click.stop="removeModel(model)"
class="shrink-0 rounded-full hover:bg-gray-200 dark:hover:bg-dark-500" class="shrink-0 rounded-full hover:bg-gray-200 dark:hover:bg-dark-500"
> >
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="x" size="xs" class="h-3.5 w-3.5" :stroke-width="2" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</span> </span>
</div> </div>
...@@ -126,6 +124,7 @@ import { ref, computed } from 'vue' ...@@ -126,6 +124,7 @@ import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app' import { useAppStore } from '@/stores/app'
import ModelIcon from '@/components/common/ModelIcon.vue' import ModelIcon from '@/components/common/ModelIcon.vue'
import Icon from '@/components/icons/Icon.vue'
import { allModels, getModelsByPlatform } from '@/composables/useModelWhitelist' import { allModels, getModelsByPlatform } from '@/composables/useModelWhitelist'
const { t } = useI18n() const { t } = useI18n()
......
...@@ -4,19 +4,7 @@ ...@@ -4,19 +4,7 @@
> >
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">
<div class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-blue-500"> <div class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-blue-500">
<svg <Icon name="link" size="md" class="text-white" />
class="h-5 w-5 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244"
/>
</svg>
</div> </div>
<div class="flex-1"> <div class="flex-1">
<h4 class="mb-3 font-semibold text-blue-900 dark:text-blue-200">{{ oauthTitle }}</h4> <h4 class="mb-3 font-semibold text-blue-900 dark:text-blue-200">{{ oauthTitle }}</h4>
...@@ -66,19 +54,7 @@ ...@@ -66,19 +54,7 @@
<label <label
class="mb-2 flex items-center gap-2 text-sm font-semibold text-gray-700 dark:text-gray-300" class="mb-2 flex items-center gap-2 text-sm font-semibold text-gray-700 dark:text-gray-300"
> >
<svg <Icon name="key" size="sm" class="text-blue-500" />
class="h-4 w-4 text-blue-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
/>
</svg>
{{ t('admin.accounts.oauth.sessionKey') }} {{ t('admin.accounts.oauth.sessionKey') }}
<span <span
v-if="parsedKeyCount > 1 && allowMultiple" v-if="parsedKeyCount > 1 && allowMultiple"
...@@ -186,20 +162,7 @@ ...@@ -186,20 +162,7 @@
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path> ></path>
</svg> </svg>
<svg <Icon v-else name="sparkles" size="sm" class="mr-2" />
v-else
class="mr-2 h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"
/>
</svg>
{{ {{
loading loading
? t('admin.accounts.oauth.authorizing') ? t('admin.accounts.oauth.authorizing')
...@@ -281,20 +244,7 @@ ...@@ -281,20 +244,7 @@
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path> ></path>
</svg> </svg>
<svg <Icon v-else name="link" size="sm" class="mr-2" />
v-else
class="mr-2 h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244"
/>
</svg>
{{ loading ? t('admin.accounts.oauth.generating') : oauthGenerateAuthUrl }} {{ loading ? t('admin.accounts.oauth.generating') : oauthGenerateAuthUrl }}
</button> </button>
<div v-else class="space-y-3"> <div v-else class="space-y-3">
...@@ -325,20 +275,13 @@ ...@@ -325,20 +275,13 @@
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184" d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
/> />
</svg> </svg>
<svg <Icon
v-else v-else
class="h-4 w-4 text-green-500" name="check"
fill="none" size="sm"
viewBox="0 0 24 24" class="text-green-500"
stroke="currentColor" :stroke-width="2"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/> />
</svg>
</button> </button>
</div> </div>
<button <button
...@@ -346,19 +289,7 @@ ...@@ -346,19 +289,7 @@
class="text-xs text-blue-600 hover:text-blue-700 dark:text-blue-400" class="text-xs text-blue-600 hover:text-blue-700 dark:text-blue-400"
@click="handleRegenerate" @click="handleRegenerate"
> >
<svg <Icon name="refresh" size="xs" class="mr-1 inline" />
class="mr-1 inline h-3 w-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
{{ t('admin.accounts.oauth.regenerate') }} {{ t('admin.accounts.oauth.regenerate') }}
</button> </button>
</div> </div>
...@@ -427,19 +358,7 @@ ...@@ -427,19 +358,7 @@
></p> ></p>
<div> <div>
<label class="input-label"> <label class="input-label">
<svg <Icon name="key" size="sm" class="mr-1 inline text-blue-500" />
class="mr-1 inline h-4 w-4 text-blue-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
/>
</svg>
{{ oauthAuthCode }} {{ oauthAuthCode }}
</label> </label>
<textarea <textarea
...@@ -449,19 +368,7 @@ ...@@ -449,19 +368,7 @@
:placeholder="oauthAuthCodePlaceholder" :placeholder="oauthAuthCodePlaceholder"
></textarea> ></textarea>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400"> <p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
<svg <Icon name="infoCircle" size="xs" class="mr-1 inline" />
class="mr-1 inline h-3 w-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
/>
</svg>
{{ oauthAuthCodeHint }} {{ oauthAuthCodeHint }}
</p> </p>
...@@ -471,19 +378,12 @@ ...@@ -471,19 +378,12 @@
class="mt-3 rounded-lg border-2 border-amber-400 bg-amber-50 p-3 dark:border-amber-600 dark:bg-amber-900/30" class="mt-3 rounded-lg border-2 border-amber-400 bg-amber-50 p-3 dark:border-amber-600 dark:bg-amber-900/30"
> >
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<svg <Icon
class="h-5 w-5 flex-shrink-0 text-amber-600 dark:text-amber-400" name="exclamationTriangle"
fill="none" size="md"
viewBox="0 0 24 24" class="flex-shrink-0 text-amber-600 dark:text-amber-400"
stroke="currentColor" :stroke-width="2"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/> />
</svg>
<div class="text-sm text-amber-800 dark:text-amber-300"> <div class="text-sm text-amber-800 dark:text-amber-300">
<p class="font-semibold">{{ $t('admin.accounts.oauth.gemini.stateWarningTitle') }}</p> <p class="font-semibold">{{ $t('admin.accounts.oauth.gemini.stateWarningTitle') }}</p>
<p class="mt-1">{{ $t('admin.accounts.oauth.gemini.stateWarningDesc') }}</p> <p class="mt-1">{{ $t('admin.accounts.oauth.gemini.stateWarningDesc') }}</p>
...@@ -514,6 +414,7 @@ ...@@ -514,6 +414,7 @@
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useClipboard } from '@/composables/useClipboard' import { useClipboard } from '@/composables/useClipboard'
import Icon from '@/components/icons/Icon.vue'
import type { AddMethod, AuthInputMethod } from '@/composables/useAccountOAuth' import type { AddMethod, AuthInputMethod } from '@/composables/useAccountOAuth'
interface Props { interface Props {
......
...@@ -23,19 +23,7 @@ ...@@ -23,19 +23,7 @@
: 'from-orange-500 to-orange-600' : 'from-orange-500 to-orange-600'
]" ]"
> >
<svg <Icon name="sparkles" size="md" class="text-white" />
class="h-5 w-5 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"
/>
</svg>
</div> </div>
<div> <div>
<span class="block font-semibold text-gray-900 dark:text-white">{{ <span class="block font-semibold text-gray-900 dark:text-white">{{
...@@ -135,19 +123,7 @@ ...@@ -135,19 +123,7 @@
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400' : 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
]" ]"
> >
<svg <Icon name="cloud" size="sm" />
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z"
/>
</svg>
</div> </div>
<div class="min-w-0"> <div class="min-w-0">
<span class="block text-sm font-medium text-gray-900 dark:text-white"> <span class="block text-sm font-medium text-gray-900 dark:text-white">
...@@ -179,19 +155,7 @@ ...@@ -179,19 +155,7 @@
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400' : 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
]" ]"
> >
<svg <Icon name="sparkles" size="sm" />
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"
/>
</svg>
</div> </div>
<div class="min-w-0"> <div class="min-w-0">
<span class="block text-sm font-medium text-gray-900 dark:text-white"> <span class="block text-sm font-medium text-gray-900 dark:text-white">
...@@ -295,6 +259,7 @@ import { useGeminiOAuth } from '@/composables/useGeminiOAuth' ...@@ -295,6 +259,7 @@ import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth' import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth'
import type { Account } from '@/types' import type { Account } from '@/types'
import BaseDialog from '@/components/common/BaseDialog.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
import Icon from '@/components/icons/Icon.vue'
import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue' import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue'
// Type for exposed OAuthAuthorizationFlow component // Type for exposed OAuthAuthorizationFlow component
......
...@@ -3,12 +3,33 @@ ...@@ -3,12 +3,33 @@
<div v-if="show && position" class="action-menu-content fixed z-[9999] w-52 overflow-hidden rounded-xl bg-white shadow-lg ring-1 ring-black/5 dark:bg-dark-800" :style="{ top: position.top + 'px', left: position.left + 'px' }"> <div v-if="show && position" class="action-menu-content fixed z-[9999] w-52 overflow-hidden rounded-xl bg-white shadow-lg ring-1 ring-black/5 dark:bg-dark-800" :style="{ top: position.top + 'px', left: position.left + 'px' }">
<div class="py-1"> <div class="py-1">
<template v-if="account"> <template v-if="account">
<button @click="$emit('test', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm hover:bg-gray-100"><span class="text-green-500"></span> {{ t('admin.accounts.testConnection') }}</button> <button @click="$emit('test', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-dark-700">
<button @click="$emit('stats', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm hover:bg-gray-100"><span class="text-indigo-500">📊</span> {{ t('admin.accounts.viewStats') }}</button> <Icon name="play" size="sm" class="text-green-500" :stroke-width="2" />
{{ t('admin.accounts.testConnection') }}
</button>
<button @click="$emit('stats', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-dark-700">
<Icon name="chart" size="sm" class="text-indigo-500" />
{{ t('admin.accounts.viewStats') }}
</button>
<template v-if="account.type === 'oauth' || account.type === 'setup-token'"> <template v-if="account.type === 'oauth' || account.type === 'setup-token'">
<button @click="$emit('reauth', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm hover:bg-gray-100 text-blue-600">🔗 {{ t('admin.accounts.reAuthorize') }}</button> <button @click="$emit('reauth', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm text-blue-600 hover:bg-gray-100 dark:hover:bg-dark-700">
<button @click="$emit('refresh-token', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm hover:bg-gray-100 text-purple-600">🔄 {{ t('admin.accounts.refreshToken') }}</button> <Icon name="link" size="sm" />
{{ t('admin.accounts.reAuthorize') }}
</button>
<button @click="$emit('refresh-token', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm text-purple-600 hover:bg-gray-100 dark:hover:bg-dark-700">
<Icon name="refresh" size="sm" />
{{ t('admin.accounts.refreshToken') }}
</button>
</template> </template>
<div v-if="account.status === 'error' || isRateLimited || isOverloaded" class="my-1 border-t border-gray-100 dark:border-dark-700"></div>
<button v-if="account.status === 'error'" @click="$emit('reset-status', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm text-yellow-600 hover:bg-gray-100 dark:hover:bg-dark-700">
<Icon name="sync" size="sm" />
{{ t('admin.accounts.resetStatus') }}
</button>
<button v-if="isRateLimited || isOverloaded" @click="$emit('clear-rate-limit', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm text-amber-600 hover:bg-gray-100 dark:hover:bg-dark-700">
<Icon name="clock" size="sm" />
{{ t('admin.accounts.clearRateLimit') }}
</button>
</template> </template>
</div> </div>
</div> </div>
...@@ -16,6 +37,14 @@ ...@@ -16,6 +37,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
defineProps(['show', 'account', 'position']); defineEmits(['close', 'test', 'stats', 'reauth', 'refresh-token']); const { t } = useI18n() import { Icon } from '@/components/icons'
import type { Account } from '@/types'
const props = defineProps<{ show: boolean; account: Account | null; position: { top: number; left: number } | null }>()
defineEmits(['close', 'test', 'stats', 'reauth', 'refresh-token', 'reset-status', 'clear-rate-limit'])
const { t } = useI18n()
const isRateLimited = computed(() => props.account?.rate_limit_reset_at && new Date(props.account.rate_limit_reset_at) > new Date())
const isOverloaded = computed(() => props.account?.overload_until && new Date(props.account.overload_until) > new Date())
</script> </script>
...@@ -15,14 +15,7 @@ ...@@ -15,14 +15,7 @@
<div <div
class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-primary-600" class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-primary-600"
> >
<svg class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="chartBar" size="md" class="text-white" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/>
</svg>
</div> </div>
<div> <div>
<div class="font-semibold text-gray-900 dark:text-gray-100">{{ account.name }}</div> <div class="font-semibold text-gray-900 dark:text-gray-100">{{ account.name }}</div>
...@@ -60,19 +53,7 @@ ...@@ -60,19 +53,7 @@
t('admin.accounts.stats.totalCost') t('admin.accounts.stats.totalCost')
}}</span> }}</span>
<div class="rounded-lg bg-emerald-100 p-1.5 dark:bg-emerald-900/30"> <div class="rounded-lg bg-emerald-100 p-1.5 dark:bg-emerald-900/30">
<svg <Icon name="dollar" size="sm" class="text-emerald-600 dark:text-emerald-400" />
class="h-4 w-4 text-emerald-600 dark:text-emerald-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div> </div>
</div> </div>
<p class="text-2xl font-bold text-gray-900 dark:text-white"> <p class="text-2xl font-bold text-gray-900 dark:text-white">
...@@ -97,19 +78,7 @@ ...@@ -97,19 +78,7 @@
t('admin.accounts.stats.totalRequests') t('admin.accounts.stats.totalRequests')
}}</span> }}</span>
<div class="rounded-lg bg-blue-100 p-1.5 dark:bg-blue-900/30"> <div class="rounded-lg bg-blue-100 p-1.5 dark:bg-blue-900/30">
<svg <Icon name="bolt" size="sm" class="text-blue-600 dark:text-blue-400" />
class="h-4 w-4 text-blue-600 dark:text-blue-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
</div> </div>
</div> </div>
<p class="text-2xl font-bold text-gray-900 dark:text-white"> <p class="text-2xl font-bold text-gray-900 dark:text-white">
...@@ -129,19 +98,11 @@ ...@@ -129,19 +98,11 @@
t('admin.accounts.stats.avgDailyCost') t('admin.accounts.stats.avgDailyCost')
}}</span> }}</span>
<div class="rounded-lg bg-amber-100 p-1.5 dark:bg-amber-900/30"> <div class="rounded-lg bg-amber-100 p-1.5 dark:bg-amber-900/30">
<svg <Icon
class="h-4 w-4 text-amber-600 dark:text-amber-400" name="calculator"
fill="none" size="sm"
viewBox="0 0 24 24" class="text-amber-600 dark:text-amber-400"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"
/> />
</svg>
</div> </div>
</div> </div>
<p class="text-2xl font-bold text-gray-900 dark:text-white"> <p class="text-2xl font-bold text-gray-900 dark:text-white">
...@@ -195,19 +156,7 @@ ...@@ -195,19 +156,7 @@
<div class="card p-4"> <div class="card p-4">
<div class="mb-3 flex items-center gap-2"> <div class="mb-3 flex items-center gap-2">
<div class="rounded-lg bg-cyan-100 p-1.5 dark:bg-cyan-900/30"> <div class="rounded-lg bg-cyan-100 p-1.5 dark:bg-cyan-900/30">
<svg <Icon name="clock" size="sm" class="text-cyan-600 dark:text-cyan-400" />
class="h-4 w-4 text-cyan-600 dark:text-cyan-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div> </div>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ <span class="text-sm font-semibold text-gray-900 dark:text-white">{{
t('admin.accounts.stats.todayOverview') t('admin.accounts.stats.todayOverview')
...@@ -245,19 +194,7 @@ ...@@ -245,19 +194,7 @@
<div class="card p-4"> <div class="card p-4">
<div class="mb-3 flex items-center gap-2"> <div class="mb-3 flex items-center gap-2">
<div class="rounded-lg bg-orange-100 p-1.5 dark:bg-orange-900/30"> <div class="rounded-lg bg-orange-100 p-1.5 dark:bg-orange-900/30">
<svg <Icon name="fire" size="sm" class="text-orange-600 dark:text-orange-400" />
class="h-4 w-4 text-orange-600 dark:text-orange-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17.657 18.657A8 8 0 016.343 7.343S7 9 9 10c0-2 .5-5 2.986-7C14 5 16.09 5.777 17.656 7.343A7.975 7.975 0 0120 13a7.975 7.975 0 01-2.343 5.657z"
/>
</svg>
</div> </div>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ <span class="text-sm font-semibold text-gray-900 dark:text-white">{{
t('admin.accounts.stats.highestCostDay') t('admin.accounts.stats.highestCostDay')
...@@ -295,19 +232,11 @@ ...@@ -295,19 +232,11 @@
<div class="card p-4"> <div class="card p-4">
<div class="mb-3 flex items-center gap-2"> <div class="mb-3 flex items-center gap-2">
<div class="rounded-lg bg-indigo-100 p-1.5 dark:bg-indigo-900/30"> <div class="rounded-lg bg-indigo-100 p-1.5 dark:bg-indigo-900/30">
<svg <Icon
class="h-4 w-4 text-indigo-600 dark:text-indigo-400" name="trendingUp"
fill="none" size="sm"
viewBox="0 0 24 24" class="text-indigo-600 dark:text-indigo-400"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
/> />
</svg>
</div> </div>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ <span class="text-sm font-semibold text-gray-900 dark:text-white">{{
t('admin.accounts.stats.highestRequestDay') t('admin.accounts.stats.highestRequestDay')
...@@ -348,19 +277,7 @@ ...@@ -348,19 +277,7 @@
<div class="card p-4"> <div class="card p-4">
<div class="mb-3 flex items-center gap-2"> <div class="mb-3 flex items-center gap-2">
<div class="rounded-lg bg-teal-100 p-1.5 dark:bg-teal-900/30"> <div class="rounded-lg bg-teal-100 p-1.5 dark:bg-teal-900/30">
<svg <Icon name="cube" size="sm" class="text-teal-600 dark:text-teal-400" />
class="h-4 w-4 text-teal-600 dark:text-teal-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
/>
</svg>
</div> </div>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ <span class="text-sm font-semibold text-gray-900 dark:text-white">{{
t('admin.accounts.stats.accumulatedTokens') t('admin.accounts.stats.accumulatedTokens')
...@@ -390,19 +307,7 @@ ...@@ -390,19 +307,7 @@
<div class="card p-4"> <div class="card p-4">
<div class="mb-3 flex items-center gap-2"> <div class="mb-3 flex items-center gap-2">
<div class="rounded-lg bg-rose-100 p-1.5 dark:bg-rose-900/30"> <div class="rounded-lg bg-rose-100 p-1.5 dark:bg-rose-900/30">
<svg <Icon name="bolt" size="sm" class="text-rose-600 dark:text-rose-400" />
class="h-4 w-4 text-rose-600 dark:text-rose-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
</div> </div>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ <span class="text-sm font-semibold text-gray-900 dark:text-white">{{
t('admin.accounts.stats.performance') t('admin.accounts.stats.performance')
...@@ -432,19 +337,11 @@ ...@@ -432,19 +337,11 @@
<div class="card p-4"> <div class="card p-4">
<div class="mb-3 flex items-center gap-2"> <div class="mb-3 flex items-center gap-2">
<div class="rounded-lg bg-lime-100 p-1.5 dark:bg-lime-900/30"> <div class="rounded-lg bg-lime-100 p-1.5 dark:bg-lime-900/30">
<svg <Icon
class="h-4 w-4 text-lime-600 dark:text-lime-400" name="clipboard"
fill="none" size="sm"
viewBox="0 0 24 24" class="text-lime-600 dark:text-lime-400"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
/> />
</svg>
</div> </div>
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ <span class="text-sm font-semibold text-gray-900 dark:text-white">{{
t('admin.accounts.stats.recentActivity') t('admin.accounts.stats.recentActivity')
...@@ -504,14 +401,7 @@ ...@@ -504,14 +401,7 @@
v-else-if="!loading" v-else-if="!loading"
class="flex flex-col items-center justify-center py-12 text-gray-500 dark:text-gray-400" class="flex flex-col items-center justify-center py-12 text-gray-500 dark:text-gray-400"
> >
<svg class="mb-4 h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="chartBar" size="xl" class="mb-4 h-12 w-12" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/>
</svg>
<p class="text-sm">{{ t('admin.accounts.stats.noData') }}</p> <p class="text-sm">{{ t('admin.accounts.stats.noData') }}</p>
</div> </div>
</div> </div>
...@@ -547,6 +437,7 @@ import { Line } from 'vue-chartjs' ...@@ -547,6 +437,7 @@ import { Line } from 'vue-chartjs'
import BaseDialog from '@/components/common/BaseDialog.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 Icon from '@/components/icons/Icon.vue'
import { adminAPI } from '@/api/admin' import { adminAPI } from '@/api/admin'
import type { Account, AccountUsageStatsResponse } from '@/types' import type { Account, AccountUsageStatsResponse } from '@/types'
......
<template> <template>
<div class="flex max-w-full flex-wrap justify-end gap-3"> <div class="flex flex-wrap items-center gap-3">
<button @click="$emit('refresh')" :disabled="loading" class="btn btn-secondary flex-shrink-0"><svg :class="['h-5 w-5', loading ? 'animate-spin' : '']" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" /></svg></button> <button @click="$emit('refresh')" :disabled="loading" class="btn btn-secondary">
<button @click="$emit('sync')" class="btn btn-secondary flex-shrink-0">{{ t('admin.accounts.syncFromCrs') }}</button> <Icon name="refresh" size="md" :class="[loading ? 'animate-spin' : '']" />
<button @click="$emit('create')" class="btn btn-primary flex-shrink-0">{{ t('admin.accounts.createAccount') }}</button> </button>
<button @click="$emit('sync')" class="btn btn-secondary">{{ t('admin.accounts.syncFromCrs') }}</button>
<button @click="$emit('create')" class="btn btn-primary">{{ t('admin.accounts.createAccount') }}</button>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'; defineProps(['loading']); defineEmits(['refresh', 'sync', 'create']); const { t } = useI18n() import { useI18n } from 'vue-i18n'
import Icon from '@/components/icons/Icon.vue'
defineProps(['loading'])
defineEmits(['refresh', 'sync', 'create'])
const { t } = useI18n()
</script> </script>
<template> <template>
<div class="flex flex-wrap items-start gap-3"> <div class="flex flex-wrap items-center gap-3">
<div class="min-w-0 flex-1">
<SearchInput <SearchInput
:model-value="searchQuery" :model-value="searchQuery"
:placeholder="t('admin.accounts.searchAccounts')" :placeholder="t('admin.accounts.searchAccounts')"
class="w-full sm:w-64"
@update:model-value="$emit('update:searchQuery', $event)" @update:model-value="$emit('update:searchQuery', $event)"
@search="$emit('change')" @search="$emit('change')"
/> />
</div> <Select v-model="filters.platform" class="w-40" :options="pOpts" @change="$emit('change')" />
<div class="flex flex-wrap items-center gap-3"> <Select v-model="filters.type" class="w-40" :options="tOpts" @change="$emit('change')" />
<Select v-model="filters.platform" class="w-40 flex-shrink-0" :options="pOpts" @change="$emit('change')" /> <Select v-model="filters.status" class="w-40" :options="sOpts" @change="$emit('change')" />
<Select v-model="filters.status" class="w-40 flex-shrink-0" :options="sOpts" @change="$emit('change')" />
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { useI18n } from 'vue-i18n'; import Select from '@/components/common/Select.vue'; import SearchInput from '@/components/common/SearchInput.vue' import { computed } from 'vue'; import { useI18n } from 'vue-i18n'; import Select from '@/components/common/Select.vue'; import SearchInput from '@/components/common/SearchInput.vue'
defineProps(['searchQuery', 'filters']); defineEmits(['update:searchQuery', 'change']); const { t } = useI18n() defineProps(['searchQuery', 'filters']); defineEmits(['update:searchQuery', 'change']); const { t } = useI18n()
const pOpts = computed(() => [{ value: '', label: t('admin.accounts.allPlatforms') }, { value: 'openai', label: 'OpenAI' }, { value: 'anthropic', label: 'Anthropic' }, { value: 'gemini', label: 'Gemini' }]) const pOpts = computed(() => [{ value: '', label: t('admin.accounts.allPlatforms') }, { value: 'anthropic', label: 'Anthropic' }, { value: 'openai', label: 'OpenAI' }, { value: 'gemini', label: 'Gemini' }, { value: 'antigravity', label: 'Antigravity' }])
const sOpts = computed(() => [{ value: '', label: t('admin.accounts.allStatus') }, { value: 'active', label: t('admin.accounts.status.active') }, { value: 'error', label: t('admin.accounts.status.error') }]) const tOpts = computed(() => [{ value: '', label: t('admin.accounts.allTypes') }, { value: 'oauth', label: t('admin.accounts.oauthType') }, { value: 'setup-token', label: t('admin.accounts.setupToken') }, { value: 'apikey', label: t('admin.accounts.apiKey') }])
const sOpts = computed(() => [{ value: '', label: t('admin.accounts.allStatus') }, { value: 'active', label: t('admin.accounts.status.active') }, { value: 'inactive', label: t('admin.accounts.status.inactive') }, { value: 'error', label: t('admin.accounts.status.error') }])
</script> </script>
...@@ -15,14 +15,7 @@ ...@@ -15,14 +15,7 @@
<div <div
class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-primary-600" class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-primary-600"
> >
<svg class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="play" size="md" class="text-white" :stroke-width="2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div> </div>
<div> <div>
<div class="font-semibold text-gray-900 dark:text-gray-100">{{ account.name }}</div> <div class="font-semibold text-gray-900 dark:text-gray-100">{{ account.name }}</div>
...@@ -70,32 +63,11 @@ ...@@ -70,32 +63,11 @@
> >
<!-- Status Line --> <!-- Status Line -->
<div v-if="status === 'idle'" class="flex items-center gap-2 text-gray-500"> <div v-if="status === 'idle'" class="flex items-center gap-2 text-gray-500">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="play" size="sm" :stroke-width="2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
<span>{{ t('admin.accounts.readyToTest') }}</span> <span>{{ t('admin.accounts.readyToTest') }}</span>
</div> </div>
<div v-else-if="status === 'connecting'" class="flex items-center gap-2 text-yellow-400"> <div v-else-if="status === 'connecting'" class="flex items-center gap-2 text-yellow-400">
<svg class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24"> <Icon name="refresh" size="sm" class="animate-spin" :stroke-width="2" />
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<span>{{ t('admin.accounts.connectingToApi') }}</span> <span>{{ t('admin.accounts.connectingToApi') }}</span>
</div> </div>
...@@ -114,28 +86,14 @@ ...@@ -114,28 +86,14 @@
v-if="status === 'success'" v-if="status === 'success'"
class="mt-3 flex items-center gap-2 border-t border-gray-700 pt-3 text-green-400" class="mt-3 flex items-center gap-2 border-t border-gray-700 pt-3 text-green-400"
> >
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="check" size="sm" :stroke-width="2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{{ t('admin.accounts.testCompleted') }}</span> <span>{{ t('admin.accounts.testCompleted') }}</span>
</div> </div>
<div <div
v-else-if="status === 'error'" v-else-if="status === 'error'"
class="mt-3 flex items-center gap-2 border-t border-gray-700 pt-3 text-red-400" class="mt-3 flex items-center gap-2 border-t border-gray-700 pt-3 text-red-400"
> >
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="x" size="sm" :stroke-width="2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{{ errorMessage }}</span> <span>{{ errorMessage }}</span>
</div> </div>
</div> </div>
...@@ -147,14 +105,7 @@ ...@@ -147,14 +105,7 @@
class="absolute right-2 top-2 rounded-lg bg-gray-800/80 p-1.5 text-gray-400 opacity-0 transition-all hover:bg-gray-700 hover:text-white group-hover:opacity-100" class="absolute right-2 top-2 rounded-lg bg-gray-800/80 p-1.5 text-gray-400 opacity-0 transition-all hover:bg-gray-700 hover:text-white group-hover:opacity-100"
:title="t('admin.accounts.copyOutput')" :title="t('admin.accounts.copyOutput')"
> >
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="link" size="sm" :stroke-width="2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</button> </button>
</div> </div>
...@@ -162,26 +113,12 @@ ...@@ -162,26 +113,12 @@
<div class="flex items-center justify-between px-1 text-xs text-gray-500 dark:text-gray-400"> <div class="flex items-center justify-between px-1 text-xs text-gray-500 dark:text-gray-400">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span class="flex items-center gap-1"> <span class="flex items-center gap-1">
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="grid" size="sm" :stroke-width="2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"
/>
</svg>
{{ t('admin.accounts.testModel') }} {{ t('admin.accounts.testModel') }}
</span> </span>
</div> </div>
<span class="flex items-center gap-1"> <span class="flex items-center gap-1">
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Icon name="chat" size="sm" :stroke-width="2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
/>
</svg>
{{ t('admin.accounts.testPrompt') }} {{ t('admin.accounts.testPrompt') }}
</span> </span>
</div> </div>
...@@ -210,54 +147,15 @@ ...@@ -210,54 +147,15 @@
: 'bg-primary-500 text-white hover:bg-primary-600' : 'bg-primary-500 text-white hover:bg-primary-600'
]" ]"
> >
<svg <Icon
v-if="status === 'connecting'" v-if="status === 'connecting'"
class="h-4 w-4 animate-spin" name="refresh"
fill="none" size="sm"
viewBox="0 0 24 24" class="animate-spin"
> :stroke-width="2"
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<svg
v-else-if="status === 'idle'"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<svg v-else class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/> />
</svg> <Icon v-else-if="status === 'idle'" name="play" size="sm" :stroke-width="2" />
<Icon v-else name="refresh" size="sm" :stroke-width="2" />
<span> <span>
{{ {{
status === 'connecting' status === 'connecting'
...@@ -278,6 +176,7 @@ import { ref, watch, nextTick } from 'vue' ...@@ -278,6 +176,7 @@ import { ref, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import BaseDialog from '@/components/common/BaseDialog.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
import Select from '@/components/common/Select.vue' import Select from '@/components/common/Select.vue'
import { Icon } from '@/components/icons'
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'
......
...@@ -23,19 +23,7 @@ ...@@ -23,19 +23,7 @@
: 'from-orange-500 to-orange-600' : 'from-orange-500 to-orange-600'
]" ]"
> >
<svg <Icon name="sparkles" size="md" class="text-white" />
class="h-5 w-5 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"
/>
</svg>
</div> </div>
<div> <div>
<span class="block font-semibold text-gray-900 dark:text-white">{{ <span class="block font-semibold text-gray-900 dark:text-white">{{
...@@ -107,9 +95,7 @@ ...@@ -107,9 +95,7 @@
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400' : 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
]" ]"
> >
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"> <Icon name="user" size="sm" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
</svg>
</div> </div>
<div class="min-w-0"> <div class="min-w-0">
<span class="block text-sm font-medium text-gray-900 dark:text-white">Google One</span> <span class="block text-sm font-medium text-gray-900 dark:text-white">Google One</span>
...@@ -135,19 +121,7 @@ ...@@ -135,19 +121,7 @@
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400' : 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
]" ]"
> >
<svg <Icon name="cloud" size="sm" />
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z"
/>
</svg>
</div> </div>
<div class="min-w-0"> <div class="min-w-0">
<span class="block text-sm font-medium text-gray-900 dark:text-white"> <span class="block text-sm font-medium text-gray-900 dark:text-white">
...@@ -179,19 +153,7 @@ ...@@ -179,19 +153,7 @@
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400' : 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
]" ]"
> >
<svg <Icon name="sparkles" size="sm" />
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"
/>
</svg>
</div> </div>
<div class="min-w-0"> <div class="min-w-0">
<span class="block text-sm font-medium text-gray-900 dark:text-white"> <span class="block text-sm font-medium text-gray-900 dark:text-white">
...@@ -295,6 +257,7 @@ import { useGeminiOAuth } from '@/composables/useGeminiOAuth' ...@@ -295,6 +257,7 @@ import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth' import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth'
import type { Account } from '@/types' import type { Account } from '@/types'
import BaseDialog from '@/components/common/BaseDialog.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
import Icon from '@/components/icons/Icon.vue'
import OAuthAuthorizationFlow from '@/components/account/OAuthAuthorizationFlow.vue' import OAuthAuthorizationFlow from '@/components/account/OAuthAuthorizationFlow.vue'
// Type for exposed OAuthAuthorizationFlow component // Type for exposed OAuthAuthorizationFlow component
......
<template> <template>
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4"> <div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
<div class="card p-4 flex items-center gap-3"> <div class="card p-4 flex items-center gap-3">
<div class="rounded-lg bg-blue-100 p-2 dark:bg-blue-900/30 text-blue-600"><svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg></div> <div class="rounded-lg bg-blue-100 p-2 dark:bg-blue-900/30 text-blue-600">
<Icon name="document" size="md" />
</div>
<div><p class="text-xs font-medium text-gray-500">{{ t('usage.totalRequests') }}</p><p class="text-xl font-bold">{{ stats?.total_requests?.toLocaleString() || '0' }}</p></div> <div><p class="text-xs font-medium text-gray-500">{{ t('usage.totalRequests') }}</p><p class="text-xl font-bold">{{ stats?.total_requests?.toLocaleString() || '0' }}</p></div>
</div> </div>
<div class="card p-4 flex items-center gap-3"> <div class="card p-4 flex items-center gap-3">
...@@ -9,19 +11,36 @@ ...@@ -9,19 +11,36 @@
<div><p class="text-xs font-medium text-gray-500">{{ t('usage.totalTokens') }}</p><p class="text-xl font-bold">{{ formatTokens(stats?.total_tokens || 0) }}</p></div> <div><p class="text-xs font-medium text-gray-500">{{ t('usage.totalTokens') }}</p><p class="text-xl font-bold">{{ formatTokens(stats?.total_tokens || 0) }}</p></div>
</div> </div>
<div class="card p-4 flex items-center gap-3"> <div class="card p-4 flex items-center gap-3">
<div class="rounded-lg bg-green-100 p-2 dark:bg-green-900/30 text-green-600"><svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg></div> <div class="rounded-lg bg-green-100 p-2 dark:bg-green-900/30 text-green-600">
<Icon name="dollar" size="md" />
</div>
<div><p class="text-xs font-medium text-gray-500">{{ t('usage.totalCost') }}</p><p class="text-xl font-bold text-green-600">${{ (stats?.total_actual_cost || 0).toFixed(4) }}</p></div> <div><p class="text-xs font-medium text-gray-500">{{ t('usage.totalCost') }}</p><p class="text-xl font-bold text-green-600">${{ (stats?.total_actual_cost || 0).toFixed(4) }}</p></div>
</div> </div>
<div class="card p-4 flex items-center gap-3"> <div class="card p-4 flex items-center gap-3">
<div class="rounded-lg bg-purple-100 p-2 dark:bg-purple-900/30 text-purple-600"><svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" /></svg></div> <div class="rounded-lg bg-purple-100 p-2 dark:bg-purple-900/30 text-purple-600">
<Icon name="clock" size="md" />
</div>
<div><p class="text-xs font-medium text-gray-500">{{ t('usage.avgDuration') }}</p><p class="text-xl font-bold">{{ formatDuration(stats?.average_duration_ms || 0) }}</p></div> <div><p class="text-xs font-medium text-gray-500">{{ t('usage.avgDuration') }}</p><p class="text-xl font-bold">{{ formatDuration(stats?.average_duration_ms || 0) }}</p></div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'; import type { AdminUsageStatsResponse } from '@/api/admin/usage' import { useI18n } from 'vue-i18n'
defineProps<{ stats: AdminUsageStatsResponse | null }>(); const { t } = useI18n() import type { AdminUsageStatsResponse } from '@/api/admin/usage'
const formatDuration = (ms: number) => ms < 1000 ? `${ms.toFixed(0)}ms` : `${(ms/1000).toFixed(2)}s` import Icon from '@/components/icons/Icon.vue'
const formatTokens = (v: number) => { if (v >= 1e9) return (v/1e9).toFixed(2) + 'B'; if (v >= 1e6) return (v/1e6).toFixed(2) + 'M'; if (v >= 1e3) return (v/1e3).toFixed(2) + 'K'; return v.toLocaleString() }
defineProps<{ stats: AdminUsageStatsResponse | null }>()
const { t } = useI18n()
const formatDuration = (ms: number) =>
ms < 1000 ? `${ms.toFixed(0)}ms` : `${(ms / 1000).toFixed(2)}s`
const formatTokens = (value: number) => {
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'B'
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'M'
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'K'
return value.toLocaleString()
}
</script> </script>
<template> <template>
<div class="card overflow-hidden"><div class="overflow-auto"> <div class="card overflow-hidden">
<div class="overflow-auto">
<DataTable :columns="cols" :data="data" :loading="loading"> <DataTable :columns="cols" :data="data" :loading="loading">
<template #cell-user="{ row }"><div class="text-sm"><span class="font-medium text-gray-900 dark:text-white">{{ row.user?.email || '-' }}</span><span class="ml-1 text-xs text-gray-400">#{{ row.user_id }}</span></div></template> <template #cell-user="{ row }">
<template #cell-model="{ value }"><span class="font-medium">{{ value }}</span></template> <div class="text-sm">
<template #cell-tokens="{ row }"><div class="text-sm">In: {{ row.input_tokens.toLocaleString() }} / Out: {{ row.output_tokens.toLocaleString() }}</div></template> <span class="font-medium text-gray-900 dark:text-white">{{ row.user?.email || '-' }}</span>
<template #cell-cost="{ row }"><span class="font-medium text-green-600">${{ row.actual_cost.toFixed(6) }}</span></template> <span class="ml-1 text-gray-500 dark:text-gray-400">#{{ row.user_id }}</span>
<template #cell-created_at="{ value }"><span class="text-sm text-gray-500">{{ formatDateTime(value) }}</span></template> </div>
</template>
<template #cell-api_key="{ row }">
<span class="text-sm text-gray-900 dark:text-white">{{ row.api_key?.name || '-' }}</span>
</template>
<template #cell-account="{ row }">
<span class="text-sm text-gray-900 dark:text-white">{{ row.account?.name || '-' }}</span>
</template>
<template #cell-model="{ value }">
<span class="font-medium text-gray-900 dark:text-white">{{ value }}</span>
</template>
<template #cell-group="{ row }">
<span v-if="row.group" class="inline-flex items-center rounded px-2 py-0.5 text-xs font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200">
{{ row.group.name }}
</span>
<span v-else class="text-sm text-gray-400 dark:text-gray-500">-</span>
</template>
<template #cell-stream="{ row }">
<span class="inline-flex items-center rounded px-2 py-0.5 text-xs font-medium" :class="row.stream ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' : 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'">
{{ row.stream ? t('usage.stream') : t('usage.sync') }}
</span>
</template>
<template #cell-tokens="{ row }">
<div class="space-y-1 text-sm">
<div class="flex items-center gap-2">
<div class="inline-flex items-center gap-1">
<Icon name="arrowDown" size="sm" class="h-3.5 w-3.5 text-emerald-500" />
<span class="font-medium text-gray-900 dark:text-white">{{ row.input_tokens?.toLocaleString() || 0 }}</span>
</div>
<div class="inline-flex items-center gap-1">
<Icon name="arrowUp" size="sm" class="h-3.5 w-3.5 text-violet-500" />
<span class="font-medium text-gray-900 dark:text-white">{{ row.output_tokens?.toLocaleString() || 0 }}</span>
</div>
</div>
<div v-if="row.cache_read_tokens > 0 || row.cache_creation_tokens > 0" class="flex items-center gap-2">
<div v-if="row.cache_read_tokens > 0" class="inline-flex items-center gap-1">
<svg class="h-3.5 w-3.5 text-sky-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" /></svg>
<span class="font-medium text-sky-600 dark:text-sky-400">{{ formatCacheTokens(row.cache_read_tokens) }}</span>
</div>
<div v-if="row.cache_creation_tokens > 0" class="inline-flex items-center gap-1">
<svg class="h-3.5 w-3.5 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg>
<span class="font-medium text-amber-600 dark:text-amber-400">{{ formatCacheTokens(row.cache_creation_tokens) }}</span>
</div>
</div>
</div>
</template>
<template #cell-cost="{ row }">
<span class="font-medium text-green-600 dark:text-green-400">${{ row.actual_cost?.toFixed(6) || '0.000000' }}</span>
</template>
<template #cell-billing_type="{ row }">
<span class="inline-flex items-center rounded px-2 py-0.5 text-xs font-medium" :class="row.billing_type === 1 ? 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200' : 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200'">
{{ row.billing_type === 1 ? t('usage.subscription') : t('usage.balance') }}
</span>
</template>
<template #cell-first_token="{ row }">
<span v-if="row.first_token_ms != null" class="text-sm text-gray-600 dark:text-gray-400">{{ formatDuration(row.first_token_ms) }}</span>
<span v-else class="text-sm text-gray-400 dark:text-gray-500">-</span>
</template>
<template #cell-duration="{ row }">
<span class="text-sm text-gray-600 dark:text-gray-400">{{ formatDuration(row.duration_ms) }}</span>
</template>
<template #cell-created_at="{ value }">
<span class="text-sm text-gray-600 dark:text-gray-400">{{ formatDateTime(value) }}</span>
</template>
<template #cell-request_id="{ row }">
<div v-if="row.request_id" class="flex items-center gap-1.5 max-w-[120px]">
<span class="font-mono text-xs text-gray-500 dark:text-gray-400 truncate" :title="row.request_id">{{ row.request_id }}</span>
<button @click="copyRequestId(row.request_id)" class="flex-shrink-0 rounded p-0.5 transition-colors hover:bg-gray-100 dark:hover:bg-dark-700" :class="copiedRequestId === row.request_id ? 'text-green-500' : 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'" :title="copiedRequestId === row.request_id ? t('keys.copied') : t('keys.copyToClipboard')">
<svg v-if="copiedRequestId === row.request_id" class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" /></svg>
<Icon v-else name="copy" size="sm" class="h-3.5 w-3.5" />
</button>
</div>
<span v-else class="text-gray-400 dark:text-gray-500">-</span>
</template>
<template #empty><EmptyState :message="t('usage.noRecords')" /></template> <template #empty><EmptyState :message="t('usage.noRecords')" /></template>
</DataTable> </DataTable>
</div></div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { formatDateTime } from '@/utils/format'; import DataTable from '@/components/common/DataTable.vue'; import EmptyState from '@/components/common/EmptyState.vue' import { ref, computed } from 'vue'
defineProps(['data', 'loading']); const { t } = useI18n() import { useI18n } from 'vue-i18n'
import { formatDateTime } from '@/utils/format'
import { useAppStore } from '@/stores/app'
import DataTable from '@/components/common/DataTable.vue'
import EmptyState from '@/components/common/EmptyState.vue'
import Icon from '@/components/icons/Icon.vue'
defineProps(['data', 'loading'])
const { t } = useI18n()
const appStore = useAppStore()
const copiedRequestId = ref<string | null>(null)
const cols = computed(() => [ const cols = computed(() => [
{ key: 'user', label: t('admin.usage.user') }, { key: 'model', label: t('usage.model'), sortable: true }, { key: 'user', label: t('admin.usage.user'), sortable: false },
{ key: 'tokens', label: t('usage.tokens') }, { key: 'cost', label: t('usage.cost') }, { key: 'api_key', label: t('usage.apiKeyFilter'), sortable: false },
{ key: 'created_at', label: t('usage.time'), sortable: true } { key: 'account', label: t('admin.usage.account'), sortable: false },
{ key: 'model', label: t('usage.model'), sortable: true },
{ key: 'group', label: t('admin.usage.group'), sortable: false },
{ key: 'stream', label: t('usage.type'), sortable: false },
{ key: 'tokens', label: t('usage.tokens'), sortable: false },
{ key: 'cost', label: t('usage.cost'), sortable: false },
{ key: 'billing_type', label: t('usage.billingType'), sortable: false },
{ key: 'first_token', label: t('usage.firstToken'), sortable: false },
{ key: 'duration', label: t('usage.duration'), sortable: false },
{ key: 'created_at', label: t('usage.time'), sortable: true },
{ key: 'request_id', label: t('admin.usage.requestId'), sortable: false }
]) ])
const formatCacheTokens = (tokens: number): string => {
if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(1)}M`
if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}K`
return tokens.toString()
}
const formatDuration = (ms: number | null | undefined): string => {
if (ms == null) return '-'
if (ms < 1000) return `${ms}ms`
return `${(ms / 1000).toFixed(2)}s`
}
const copyRequestId = async (requestId: string) => {
try {
await navigator.clipboard.writeText(requestId)
copiedRequestId.value = requestId
appStore.showSuccess(t('admin.usage.requestIdCopied'))
setTimeout(() => { copiedRequestId.value = null }, 2000)
} catch {
appStore.showError(t('common.copyFailed'))
}
}
</script> </script>
...@@ -37,10 +37,21 @@ watch(() => props.show, (v) => { if(v) { form.amount = 0; form.notes = '' } }) ...@@ -37,10 +37,21 @@ watch(() => props.show, (v) => { if(v) { form.amount = 0; form.notes = '' } })
const calculateNewBalance = () => (props.user ? (props.operation === 'add' ? props.user.balance + form.amount : props.user.balance - form.amount) : 0) const calculateNewBalance = () => (props.user ? (props.operation === 'add' ? props.user.balance + form.amount : props.user.balance - form.amount) : 0)
const handleBalanceSubmit = async () => { const handleBalanceSubmit = async () => {
if (!props.user) return; submitting.value = true if (!props.user) return
if (!form.amount || form.amount <= 0) {
appStore.showError(t('admin.users.amountRequired'))
return
}
if (props.operation === 'subtract' && form.amount > props.user.balance) {
appStore.showError(t('admin.users.insufficientBalance'))
return
}
submitting.value = true
try { try {
await adminAPI.users.updateBalance(props.user.id, form.amount, props.operation, form.notes) await adminAPI.users.updateBalance(props.user.id, form.amount, props.operation, form.notes)
appStore.showSuccess(t('common.success')); emit('success'); emit('close') appStore.showSuccess(t('common.success')); emit('success'); emit('close')
} catch {} finally { submitting.value = false } } catch (e: any) {
appStore.showError(e.response?.data?.detail || t('common.error'))
} finally { submitting.value = false }
} }
</script> </script>
\ No newline at end of file
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
<input v-model="form.password" type="text" required class="input pr-10" :placeholder="t('admin.users.enterPassword')" /> <input v-model="form.password" type="text" required class="input pr-10" :placeholder="t('admin.users.enterPassword')" />
</div> </div>
<button type="button" @click="generateRandomPassword" class="btn btn-secondary px-3"> <button type="button" @click="generateRandomPassword" class="btn btn-secondary px-3">
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" /></svg> <Icon name="refresh" size="md" />
</button> </button>
</div> </div>
</div> </div>
...@@ -52,6 +52,7 @@ import { reactive, watch } from 'vue' ...@@ -52,6 +52,7 @@ import { reactive, watch } from 'vue'
import { useI18n } from 'vue-i18n'; import { adminAPI } from '@/api/admin' import { useI18n } from 'vue-i18n'; import { adminAPI } from '@/api/admin'
import { useForm } from '@/composables/useForm' import { useForm } from '@/composables/useForm'
import BaseDialog from '@/components/common/BaseDialog.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
import Icon from '@/components/icons/Icon.vue'
const props = defineProps<{ show: boolean }>() const props = defineProps<{ show: boolean }>()
const emit = defineEmits(['close', 'success']); const { t } = useI18n() const emit = defineEmits(['close', 'success']); const { t } = useI18n()
......
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