Commit 7b9c3f16 authored by shaw's avatar shaw
Browse files

feat: 账号管理新增使用统计功能

- 新增账号统计弹窗,展示30天使用数据
- 显示总费用、请求数、日均费用、日均请求等汇总指标
- 显示今日概览、最高费用日、最高请求日
- 包含费用与请求趋势图(双Y轴)
- 复用模型分布图组件展示模型使用分布
- 显示实际扣费和标准计费(标准计费以较淡颜色显示)
parent d9e27df9
...@@ -83,7 +83,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { ...@@ -83,7 +83,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
accountUsageService := service.NewAccountUsageService(accountRepository, usageLogRepository, claudeUsageFetcher) accountUsageService := service.NewAccountUsageService(accountRepository, usageLogRepository, claudeUsageFetcher)
httpUpstream := repository.NewHTTPUpstream(configConfig) httpUpstream := repository.NewHTTPUpstream(configConfig)
accountTestService := service.NewAccountTestService(accountRepository, oAuthService, openAIOAuthService, httpUpstream) accountTestService := service.NewAccountTestService(accountRepository, oAuthService, openAIOAuthService, httpUpstream)
accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, rateLimitService, accountUsageService, accountTestService) accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, rateLimitService, accountUsageService, accountTestService, usageLogRepository)
oAuthHandler := admin.NewOAuthHandler(oAuthService) oAuthHandler := admin.NewOAuthHandler(oAuthService)
openAIOAuthHandler := admin.NewOpenAIOAuthHandler(openAIOAuthService, adminService) openAIOAuthHandler := admin.NewOpenAIOAuthHandler(openAIOAuthService, adminService)
proxyHandler := admin.NewProxyHandler(adminService) proxyHandler := admin.NewProxyHandler(adminService)
......
...@@ -6,6 +6,8 @@ import ( ...@@ -6,6 +6,8 @@ import (
"sub2api/internal/pkg/claude" "sub2api/internal/pkg/claude"
"sub2api/internal/pkg/openai" "sub2api/internal/pkg/openai"
"sub2api/internal/pkg/response" "sub2api/internal/pkg/response"
"sub2api/internal/pkg/timezone"
"sub2api/internal/repository"
"sub2api/internal/service" "sub2api/internal/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
...@@ -31,10 +33,11 @@ type AccountHandler struct { ...@@ -31,10 +33,11 @@ type AccountHandler struct {
rateLimitService *service.RateLimitService rateLimitService *service.RateLimitService
accountUsageService *service.AccountUsageService accountUsageService *service.AccountUsageService
accountTestService *service.AccountTestService accountTestService *service.AccountTestService
usageLogRepo *repository.UsageLogRepository
} }
// NewAccountHandler creates a new admin account handler // NewAccountHandler creates a new admin account handler
func NewAccountHandler(adminService service.AdminService, oauthService *service.OAuthService, openaiOAuthService *service.OpenAIOAuthService, rateLimitService *service.RateLimitService, accountUsageService *service.AccountUsageService, accountTestService *service.AccountTestService) *AccountHandler { func NewAccountHandler(adminService service.AdminService, oauthService *service.OAuthService, openaiOAuthService *service.OpenAIOAuthService, rateLimitService *service.RateLimitService, accountUsageService *service.AccountUsageService, accountTestService *service.AccountTestService, usageLogRepo *repository.UsageLogRepository) *AccountHandler {
return &AccountHandler{ return &AccountHandler{
adminService: adminService, adminService: adminService,
oauthService: oauthService, oauthService: oauthService,
...@@ -42,6 +45,7 @@ func NewAccountHandler(adminService service.AdminService, oauthService *service. ...@@ -42,6 +45,7 @@ func NewAccountHandler(adminService service.AdminService, oauthService *service.
rateLimitService: rateLimitService, rateLimitService: rateLimitService,
accountUsageService: accountUsageService, accountUsageService: accountUsageService,
accountTestService: accountTestService, accountTestService: accountTestService,
usageLogRepo: usageLogRepo,
} }
} }
...@@ -297,15 +301,26 @@ func (h *AccountHandler) GetStats(c *gin.Context) { ...@@ -297,15 +301,26 @@ func (h *AccountHandler) GetStats(c *gin.Context) {
return return
} }
// Return mock data for now // Parse days parameter (default 30)
_ = accountID days := 30
response.Success(c, gin.H{ if daysStr := c.Query("days"); daysStr != "" {
"total_requests": 0, if d, err := strconv.Atoi(daysStr); err == nil && d > 0 && d <= 90 {
"successful_requests": 0, days = d
"failed_requests": 0, }
"total_tokens": 0, }
"average_response_time": 0,
}) // Calculate time range
now := timezone.Now()
endTime := timezone.StartOfDay(now.AddDate(0, 0, 1))
startTime := timezone.StartOfDay(now.AddDate(0, 0, -days+1))
stats, err := h.usageLogRepo.GetAccountUsageStats(c.Request.Context(), accountID, startTime, endTime)
if err != nil {
response.InternalError(c, "Failed to get account stats: "+err.Error())
return
}
response.Success(c, stats)
} }
// ClearError handles clearing account error // ClearError handles clearing account error
......
...@@ -175,7 +175,7 @@ func (h *DashboardHandler) GetModelStats(c *gin.Context) { ...@@ -175,7 +175,7 @@ func (h *DashboardHandler) GetModelStats(c *gin.Context) {
} }
} }
stats, err := h.usageRepo.GetModelStatsWithFilters(c.Request.Context(), startTime, endTime, userID, apiKeyID) stats, err := h.usageRepo.GetModelStatsWithFilters(c.Request.Context(), startTime, endTime, userID, apiKeyID, 0)
if err != nil { if err != nil {
response.Error(c, 500, "Failed to get model statistics") response.Error(c, 500, "Failed to get model statistics")
return return
......
...@@ -937,7 +937,7 @@ func (r *UsageLogRepository) GetUsageTrendWithFilters(ctx context.Context, start ...@@ -937,7 +937,7 @@ func (r *UsageLogRepository) GetUsageTrendWithFilters(ctx context.Context, start
} }
// GetModelStatsWithFilters returns model statistics with optional user/api_key filters // GetModelStatsWithFilters returns model statistics with optional user/api_key filters
func (r *UsageLogRepository) GetModelStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID int64) ([]ModelStat, error) { func (r *UsageLogRepository) GetModelStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID int64) ([]ModelStat, error) {
var results []ModelStat var results []ModelStat
db := r.db.WithContext(ctx).Model(&model.UsageLog{}). db := r.db.WithContext(ctx).Model(&model.UsageLog{}).
...@@ -958,6 +958,9 @@ func (r *UsageLogRepository) GetModelStatsWithFilters(ctx context.Context, start ...@@ -958,6 +958,9 @@ func (r *UsageLogRepository) GetModelStatsWithFilters(ctx context.Context, start
if apiKeyID > 0 { if apiKeyID > 0 {
db = db.Where("api_key_id = ?", apiKeyID) db = db.Where("api_key_id = ?", apiKeyID)
} }
if accountID > 0 {
db = db.Where("account_id = ?", accountID)
}
err := db.Group("model").Order("total_tokens DESC").Scan(&results).Error err := db.Group("model").Order("total_tokens DESC").Scan(&results).Error
if err != nil { if err != nil {
...@@ -1007,3 +1010,209 @@ func (r *UsageLogRepository) GetGlobalStats(ctx context.Context, startTime, endT ...@@ -1007,3 +1010,209 @@ func (r *UsageLogRepository) GetGlobalStats(ctx context.Context, startTime, endT
AverageDurationMs: stats.AverageDurationMs, AverageDurationMs: stats.AverageDurationMs,
}, nil }, nil
} }
// AccountUsageHistory represents daily usage history for an account
type AccountUsageHistory struct {
Date string `json:"date"`
Label string `json:"label"`
Requests int64 `json:"requests"`
Tokens int64 `json:"tokens"`
Cost float64 `json:"cost"`
ActualCost float64 `json:"actual_cost"`
}
// AccountUsageSummary represents summary statistics for an account
type AccountUsageSummary struct {
Days int `json:"days"`
ActualDaysUsed int `json:"actual_days_used"`
TotalCost float64 `json:"total_cost"`
TotalStandardCost float64 `json:"total_standard_cost"`
TotalRequests int64 `json:"total_requests"`
TotalTokens int64 `json:"total_tokens"`
AvgDailyCost float64 `json:"avg_daily_cost"`
AvgDailyRequests float64 `json:"avg_daily_requests"`
AvgDailyTokens float64 `json:"avg_daily_tokens"`
AvgDurationMs float64 `json:"avg_duration_ms"`
Today *struct {
Date string `json:"date"`
Cost float64 `json:"cost"`
Requests int64 `json:"requests"`
Tokens int64 `json:"tokens"`
} `json:"today"`
HighestCostDay *struct {
Date string `json:"date"`
Label string `json:"label"`
Cost float64 `json:"cost"`
Requests int64 `json:"requests"`
} `json:"highest_cost_day"`
HighestRequestDay *struct {
Date string `json:"date"`
Label string `json:"label"`
Requests int64 `json:"requests"`
Cost float64 `json:"cost"`
} `json:"highest_request_day"`
}
// AccountUsageStatsResponse represents the full usage statistics response for an account
type AccountUsageStatsResponse struct {
History []AccountUsageHistory `json:"history"`
Summary AccountUsageSummary `json:"summary"`
Models []ModelStat `json:"models"`
}
// GetAccountUsageStats returns comprehensive usage statistics for an account over a time range
func (r *UsageLogRepository) GetAccountUsageStats(ctx context.Context, accountID int64, startTime, endTime time.Time) (*AccountUsageStatsResponse, error) {
daysCount := int(endTime.Sub(startTime).Hours()/24) + 1
if daysCount <= 0 {
daysCount = 30
}
// Get daily history
var historyResults []struct {
Date string `gorm:"column:date"`
Requests int64 `gorm:"column:requests"`
Tokens int64 `gorm:"column:tokens"`
Cost float64 `gorm:"column:cost"`
ActualCost float64 `gorm:"column:actual_cost"`
}
err := r.db.WithContext(ctx).Model(&model.UsageLog{}).
Select(`
TO_CHAR(created_at, 'YYYY-MM-DD') as date,
COUNT(*) as requests,
COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as tokens,
COALESCE(SUM(total_cost), 0) as cost,
COALESCE(SUM(actual_cost), 0) as actual_cost
`).
Where("account_id = ? AND created_at >= ? AND created_at < ?", accountID, startTime, endTime).
Group("date").
Order("date ASC").
Scan(&historyResults).Error
if err != nil {
return nil, err
}
// Build history with labels
history := make([]AccountUsageHistory, 0, len(historyResults))
for _, h := range historyResults {
// Parse date to get label (MM/DD)
t, _ := time.Parse("2006-01-02", h.Date)
label := t.Format("01/02")
history = append(history, AccountUsageHistory{
Date: h.Date,
Label: label,
Requests: h.Requests,
Tokens: h.Tokens,
Cost: h.Cost,
ActualCost: h.ActualCost,
})
}
// Calculate summary
var totalActualCost, totalStandardCost float64
var totalRequests, totalTokens int64
var highestCostDay, highestRequestDay *AccountUsageHistory
for i := range history {
h := &history[i]
totalActualCost += h.ActualCost
totalStandardCost += h.Cost
totalRequests += h.Requests
totalTokens += h.Tokens
if highestCostDay == nil || h.ActualCost > highestCostDay.ActualCost {
highestCostDay = h
}
if highestRequestDay == nil || h.Requests > highestRequestDay.Requests {
highestRequestDay = h
}
}
actualDaysUsed := len(history)
if actualDaysUsed == 0 {
actualDaysUsed = 1
}
// Get average duration
var avgDuration struct {
AvgDurationMs float64 `gorm:"column:avg_duration_ms"`
}
r.db.WithContext(ctx).Model(&model.UsageLog{}).
Select("COALESCE(AVG(duration_ms), 0) as avg_duration_ms").
Where("account_id = ? AND created_at >= ? AND created_at < ?", accountID, startTime, endTime).
Scan(&avgDuration)
summary := AccountUsageSummary{
Days: daysCount,
ActualDaysUsed: actualDaysUsed,
TotalCost: totalActualCost,
TotalStandardCost: totalStandardCost,
TotalRequests: totalRequests,
TotalTokens: totalTokens,
AvgDailyCost: totalActualCost / float64(actualDaysUsed),
AvgDailyRequests: float64(totalRequests) / float64(actualDaysUsed),
AvgDailyTokens: float64(totalTokens) / float64(actualDaysUsed),
AvgDurationMs: avgDuration.AvgDurationMs,
}
// Set today's stats
todayStr := timezone.Now().Format("2006-01-02")
for i := range history {
if history[i].Date == todayStr {
summary.Today = &struct {
Date string `json:"date"`
Cost float64 `json:"cost"`
Requests int64 `json:"requests"`
Tokens int64 `json:"tokens"`
}{
Date: history[i].Date,
Cost: history[i].ActualCost,
Requests: history[i].Requests,
Tokens: history[i].Tokens,
}
break
}
}
// Set highest cost day
if highestCostDay != nil {
summary.HighestCostDay = &struct {
Date string `json:"date"`
Label string `json:"label"`
Cost float64 `json:"cost"`
Requests int64 `json:"requests"`
}{
Date: highestCostDay.Date,
Label: highestCostDay.Label,
Cost: highestCostDay.ActualCost,
Requests: highestCostDay.Requests,
}
}
// Set highest request day
if highestRequestDay != nil {
summary.HighestRequestDay = &struct {
Date string `json:"date"`
Label string `json:"label"`
Requests int64 `json:"requests"`
Cost float64 `json:"cost"`
}{
Date: highestRequestDay.Date,
Label: highestRequestDay.Label,
Requests: highestRequestDay.Requests,
Cost: highestRequestDay.ActualCost,
}
}
// Get model statistics using the unified method
models, err := r.GetModelStatsWithFilters(ctx, startTime, endTime, 0, 0, accountID)
if err != nil {
models = []ModelStat{}
}
return &AccountUsageStatsResponse{
History: history,
Summary: summary,
Models: models,
}, nil
}
...@@ -12,6 +12,7 @@ import type { ...@@ -12,6 +12,7 @@ import type {
AccountUsageInfo, AccountUsageInfo,
WindowStats, WindowStats,
ClaudeModel, ClaudeModel,
AccountUsageStatsResponse,
} from '@/types'; } from '@/types';
/** /**
...@@ -126,27 +127,12 @@ export async function refreshCredentials(id: number): Promise<Account> { ...@@ -126,27 +127,12 @@ export async function refreshCredentials(id: number): Promise<Account> {
/** /**
* Get account usage statistics * Get account usage statistics
* @param id - Account ID * @param id - Account ID
* @param period - Time period * @param days - Number of days (default: 30)
* @returns Account usage statistics * @returns Account usage statistics with history, summary, and models
*/ */
export async function getStats( export async function getStats(id: number, days: number = 30): Promise<AccountUsageStatsResponse> {
id: number, const { data } = await apiClient.get<AccountUsageStatsResponse>(`/admin/accounts/${id}/stats`, {
period: string = 'month' params: { days },
): Promise<{
total_requests: number;
successful_requests: number;
failed_requests: number;
total_tokens: number;
average_response_time: number;
}> {
const { data } = await apiClient.get<{
total_requests: number;
successful_requests: number;
failed_requests: number;
total_tokens: number;
average_response_time: number;
}>(`/admin/accounts/${id}/stats`, {
params: { period },
}); });
return data; return data;
} }
......
This diff is collapsed.
...@@ -5,3 +5,6 @@ export { default as OAuthAuthorizationFlow } from './OAuthAuthorizationFlow.vue' ...@@ -5,3 +5,6 @@ export { default as OAuthAuthorizationFlow } from './OAuthAuthorizationFlow.vue'
export { default as AccountStatusIndicator } from './AccountStatusIndicator.vue' export { default as AccountStatusIndicator } from './AccountStatusIndicator.vue'
export { default as AccountUsageCell } from './AccountUsageCell.vue' export { default as AccountUsageCell } from './AccountUsageCell.vue'
export { default as UsageProgressBar } from './UsageProgressBar.vue' export { default as UsageProgressBar } from './UsageProgressBar.vue'
export { default as AccountStatsModal } from './AccountStatsModal.vue'
export { default as AccountTestModal } from './AccountTestModal.vue'
export { default as AccountTodayStatsCell } from './AccountTodayStatsCell.vue'
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, watch, onMounted, onUnmounted } from 'vue' import { computed, watch, onMounted, onUnmounted } from 'vue'
type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full' type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full'
interface Props { interface Props {
show: boolean show: boolean
...@@ -80,6 +80,7 @@ const sizeClasses = computed(() => { ...@@ -80,6 +80,7 @@ const sizeClasses = computed(() => {
md: 'max-w-md', md: 'max-w-md',
lg: 'max-w-lg', lg: 'max-w-lg',
xl: 'max-w-xl', xl: 'max-w-xl',
'2xl': 'max-w-5xl',
full: 'max-w-4xl' full: 'max-w-4xl'
} }
return sizes[props.size] return sizes[props.size]
......
...@@ -825,6 +825,39 @@ export default { ...@@ -825,6 +825,39 @@ export default {
selectTestModel: 'Select Test Model', selectTestModel: 'Select Test Model',
testModel: 'claude-sonnet-4-5-20250929', testModel: 'claude-sonnet-4-5-20250929',
testPrompt: 'Prompt: "hi"', testPrompt: 'Prompt: "hi"',
// Stats Modal
viewStats: 'View Stats',
usageStatistics: 'Usage Statistics',
last30DaysUsage: 'Last 30 days usage statistics (based on actual usage days)',
stats: {
totalCost: '30-Day Total Cost',
accumulatedCost: 'Accumulated cost',
standardCost: 'Standard',
totalRequests: '30-Day Total Requests',
totalCalls: 'Total API calls',
avgDailyCost: 'Daily Avg Cost',
basedOnActualDays: 'Based on {days} actual usage days',
avgDailyRequests: 'Daily Avg Requests',
avgDailyUsage: 'Average daily usage',
todayOverview: 'Today Overview',
cost: 'Cost',
requests: 'Requests',
highestCostDay: 'Highest Cost Day',
highestRequestDay: 'Highest Request Day',
date: 'Date',
accumulatedTokens: 'Accumulated Tokens',
totalTokens: '30-Day Total',
dailyAvgTokens: 'Daily Average',
performance: 'Performance',
avgResponseTime: 'Avg Response',
daysActive: 'Days Active',
recentActivity: 'Recent Activity',
todayRequests: 'Today Requests',
todayTokens: 'Today Tokens',
todayCost: 'Today Cost',
usageTrend: '30-Day Cost & Request Trend',
noData: 'No usage data available for this account',
},
}, },
// Proxies // Proxies
......
...@@ -918,6 +918,39 @@ export default { ...@@ -918,6 +918,39 @@ export default {
selectTestModel: '选择测试模型', selectTestModel: '选择测试模型',
testModel: 'claude-sonnet-4-5-20250929', testModel: 'claude-sonnet-4-5-20250929',
testPrompt: '提示词:"hi"', testPrompt: '提示词:"hi"',
// Stats Modal
viewStats: '查看统计',
usageStatistics: '使用统计',
last30DaysUsage: '近30天使用统计(日均基于实际使用天数)',
stats: {
totalCost: '30天总费用',
accumulatedCost: '累计成本',
standardCost: '标准计费',
totalRequests: '30天总请求',
totalCalls: '累计调用次数',
avgDailyCost: '日均费用',
basedOnActualDays: '基于 {days} 天实际使用',
avgDailyRequests: '日均请求',
avgDailyUsage: '平均每日调用',
todayOverview: '今日概览',
cost: '费用',
requests: '请求',
highestCostDay: '最高费用日',
highestRequestDay: '最高请求日',
date: '日期',
accumulatedTokens: '累计 Token',
totalTokens: '30天总计',
dailyAvgTokens: '日均 Token',
performance: '性能',
avgResponseTime: '平均响应',
daysActive: '活跃天数',
recentActivity: '最近统计',
todayRequests: '今日请求',
todayTokens: '今日 Token',
todayCost: '今日费用',
usageTrend: '30天费用与请求趋势',
noData: '该账号暂无使用数据',
},
}, },
// Proxies Management // Proxies Management
......
...@@ -645,3 +645,51 @@ export interface UsageQueryParams { ...@@ -645,3 +645,51 @@ export interface UsageQueryParams {
start_date?: string; start_date?: string;
end_date?: string; end_date?: string;
} }
// ==================== Account Usage Statistics ====================
export interface AccountUsageHistory {
date: string;
label: string;
requests: number;
tokens: number;
cost: number;
actual_cost: number;
}
export interface AccountUsageSummary {
days: number;
actual_days_used: number;
total_cost: number;
total_standard_cost: number;
total_requests: number;
total_tokens: number;
avg_daily_cost: number;
avg_daily_requests: number;
avg_daily_tokens: number;
avg_duration_ms: number;
today: {
date: string;
cost: number;
requests: number;
tokens: number;
} | null;
highest_cost_day: {
date: string;
label: string;
cost: number;
requests: number;
} | null;
highest_request_day: {
date: string;
label: string;
requests: number;
cost: number;
} | null;
}
export interface AccountUsageStatsResponse {
history: AccountUsageHistory[];
summary: AccountUsageSummary;
models: ModelStat[];
}
...@@ -186,6 +186,16 @@ ...@@ -186,6 +186,16 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 010 1.972l-11.54 6.347a1.125 1.125 0 01-1.667-.986V5.653z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 010 1.972l-11.54 6.347a1.125 1.125 0 01-1.667-.986V5.653z" />
</svg> </svg>
</button> </button>
<!-- View Stats button -->
<button
@click="handleViewStats(row)"
class="p-2 rounded-lg hover:bg-indigo-50 dark:hover:bg-indigo-900/20 text-gray-500 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
:title="t('admin.accounts.viewStats')"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" />
</svg>
</button>
<button <button
v-if="row.type === 'oauth' || row.type === 'setup-token'" v-if="row.type === 'oauth' || row.type === 'setup-token'"
@click="handleReAuth(row)" @click="handleReAuth(row)"
...@@ -284,6 +294,13 @@ ...@@ -284,6 +294,13 @@
@close="closeTestModal" @close="closeTestModal"
/> />
<!-- Account Stats Modal -->
<AccountStatsModal
:show="showStatsModal"
:account="statsAccount"
@close="closeStatsModal"
/>
<!-- Delete Confirmation Dialog --> <!-- Delete Confirmation Dialog -->
<ConfirmDialog <ConfirmDialog
:show="showDeleteDialog" :show="showDeleteDialog"
...@@ -311,7 +328,7 @@ import Pagination from '@/components/common/Pagination.vue' ...@@ -311,7 +328,7 @@ import Pagination from '@/components/common/Pagination.vue'
import ConfirmDialog from '@/components/common/ConfirmDialog.vue' import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
import EmptyState from '@/components/common/EmptyState.vue' import EmptyState from '@/components/common/EmptyState.vue'
import Select from '@/components/common/Select.vue' import Select from '@/components/common/Select.vue'
import { CreateAccountModal, EditAccountModal, ReAuthAccountModal } from '@/components/account' import { CreateAccountModal, EditAccountModal, ReAuthAccountModal, AccountStatsModal } from '@/components/account'
import AccountStatusIndicator from '@/components/account/AccountStatusIndicator.vue' import AccountStatusIndicator from '@/components/account/AccountStatusIndicator.vue'
import AccountUsageCell from '@/components/account/AccountUsageCell.vue' import AccountUsageCell from '@/components/account/AccountUsageCell.vue'
import AccountTodayStatsCell from '@/components/account/AccountTodayStatsCell.vue' import AccountTodayStatsCell from '@/components/account/AccountTodayStatsCell.vue'
...@@ -382,10 +399,12 @@ const showEditModal = ref(false) ...@@ -382,10 +399,12 @@ const showEditModal = ref(false)
const showReAuthModal = ref(false) const showReAuthModal = ref(false)
const showDeleteDialog = ref(false) const showDeleteDialog = ref(false)
const showTestModal = ref(false) const showTestModal = ref(false)
const showStatsModal = ref(false)
const editingAccount = ref<Account | null>(null) const editingAccount = ref<Account | null>(null)
const reAuthAccount = ref<Account | null>(null) const reAuthAccount = ref<Account | null>(null)
const deletingAccount = ref<Account | null>(null) const deletingAccount = ref<Account | null>(null)
const testingAccount = ref<Account | null>(null) const testingAccount = ref<Account | null>(null)
const statsAccount = ref<Account | null>(null)
const togglingSchedulable = ref<number | null>(null) const togglingSchedulable = ref<number | null>(null)
// Rate limit / Overload helpers // Rate limit / Overload helpers
...@@ -574,6 +593,17 @@ const closeTestModal = () => { ...@@ -574,6 +593,17 @@ const closeTestModal = () => {
testingAccount.value = null testingAccount.value = null
} }
// Stats modal
const handleViewStats = (account: Account) => {
statsAccount.value = account
showStatsModal.value = true
}
const closeStatsModal = () => {
showStatsModal.value = false
statsAccount.value = null
}
// Initialize // Initialize
onMounted(() => { onMounted(() => {
loadAccounts() loadAccounts()
......
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