This commit is contained in:
KevinLiao
2025-07-26 10:32:05 +08:00
9 changed files with 582 additions and 31 deletions

View File

@@ -112,6 +112,8 @@ const app = createApp({
apiKeys: [],
apiKeysLoading: false,
apiKeyStatsTimeRange: 'all', // API Key统计时间范围all, 7days, monthly
apiKeysSortBy: '', // 当前排序字段
apiKeysSortOrder: 'asc', // 排序顺序 'asc' 或 'desc'
showCreateApiKeyModal: false,
createApiKeyLoading: false,
apiKeyForm: {
@@ -199,6 +201,8 @@ const app = createApp({
// 账户
accounts: [],
accountsLoading: false,
accountSortBy: 'dailyTokens', // 默认按今日Token排序
accountsSortOrder: 'asc', // 排序顺序 'asc' 或 'desc'
showCreateAccountModal: false,
createAccountLoading: false,
accountForm: {
@@ -302,6 +306,83 @@ const app = createApp({
return `${window.location.protocol}//${window.location.host}/api/`;
},
// 排序后的账户列表
sortedAccounts() {
if (!this.accountsSortBy) {
return this.accounts;
}
return [...this.accounts].sort((a, b) => {
let aValue = a[this.accountsSortBy];
let bValue = b[this.accountsSortBy];
// 特殊处理状态字段
if (this.accountsSortBy === 'status') {
aValue = a.isActive ? 1 : 0;
bValue = b.isActive ? 1 : 0;
}
// 处理字符串比较
if (typeof aValue === 'string' && typeof bValue === 'string') {
aValue = aValue.toLowerCase();
bValue = bValue.toLowerCase();
}
// 排序
if (this.accountsSortOrder === 'asc') {
return aValue > bValue ? 1 : aValue < bValue ? -1 : 0;
} else {
return aValue < bValue ? 1 : aValue > bValue ? -1 : 0;
}
});
},
// 排序后的API Keys列表
sortedApiKeys() {
if (!this.apiKeysSortBy) {
return this.apiKeys;
}
return [...this.apiKeys].sort((a, b) => {
let aValue, bValue;
// 特殊处理不同字段
switch (this.apiKeysSortBy) {
case 'status':
aValue = a.isActive ? 1 : 0;
bValue = b.isActive ? 1 : 0;
break;
case 'cost':
// 计算费用,转换为数字比较
aValue = this.calculateApiKeyCostNumber(a.usage);
bValue = this.calculateApiKeyCostNumber(b.usage);
break;
case 'createdAt':
case 'expiresAt':
// 日期比较
aValue = a[this.apiKeysSortBy] ? new Date(a[this.apiKeysSortBy]).getTime() : 0;
bValue = b[this.apiKeysSortBy] ? new Date(b[this.apiKeysSortBy]).getTime() : 0;
break;
default:
aValue = a[this.apiKeysSortBy];
bValue = b[this.apiKeysSortBy];
// 处理字符串比较
if (typeof aValue === 'string' && typeof bValue === 'string') {
aValue = aValue.toLowerCase();
bValue = bValue.toLowerCase();
}
}
// 排序
if (this.apiKeysSortOrder === 'asc') {
return aValue > bValue ? 1 : aValue < bValue ? -1 : 0;
} else {
return aValue < bValue ? 1 : aValue > bValue ? -1 : 0;
}
});
},
// 获取专属账号列表
dedicatedAccounts() {
return this.accounts.filter(account =>
@@ -407,6 +488,30 @@ const app = createApp({
},
methods: {
// 账户列表排序
sortAccounts(field) {
if (this.accountsSortBy === field) {
// 如果点击的是当前排序字段,切换排序顺序
this.accountsSortOrder = this.accountsSortOrder === 'asc' ? 'desc' : 'asc';
} else {
// 如果点击的是新字段,设置为升序
this.accountsSortBy = field;
this.accountsSortOrder = 'asc';
}
},
// API Keys列表排序
sortApiKeys(field) {
if (this.apiKeysSortBy === field) {
// 如果点击的是当前排序字段,切换排序顺序
this.apiKeysSortOrder = this.apiKeysSortOrder === 'asc' ? 'desc' : 'asc';
} else {
// 如果点击的是新字段,设置为升序
this.apiKeysSortBy = field;
this.apiKeysSortOrder = 'asc';
}
},
// 从URL读取tab参数并设置activeTab
initializeTabFromUrl() {
const urlParams = new URLSearchParams(window.location.search);
@@ -1888,6 +1993,9 @@ const app = createApp({
account.boundApiKeysCount = this.apiKeys.filter(key => key.geminiAccountId === account.id).length;
}
});
// 加载完成后自动排序
this.sortAccounts();
} catch (error) {
console.error('Failed to load accounts:', error);
} finally {
@@ -1895,6 +2003,35 @@ const app = createApp({
}
},
// 账户排序
sortAccounts() {
if (!this.accounts || this.accounts.length === 0) return;
this.accounts.sort((a, b) => {
switch (this.accountSortBy) {
case 'name':
return a.name.localeCompare(b.name);
case 'dailyTokens':
const aTokens = (a.usage && a.usage.daily && a.usage.daily.allTokens) || 0;
const bTokens = (b.usage && b.usage.daily && b.usage.daily.allTokens) || 0;
return bTokens - aTokens; // 降序
case 'dailyRequests':
const aRequests = (a.usage && a.usage.daily && a.usage.daily.requests) || 0;
const bRequests = (b.usage && b.usage.daily && b.usage.daily.requests) || 0;
return bRequests - aRequests; // 降序
case 'totalTokens':
const aTotalTokens = (a.usage && a.usage.total && a.usage.total.allTokens) || 0;
const bTotalTokens = (b.usage && b.usage.total && b.usage.total.allTokens) || 0;
return bTotalTokens - aTotalTokens; // 降序
case 'lastUsed':
const aLastUsed = a.lastUsedAt ? new Date(a.lastUsedAt) : new Date(0);
const bLastUsed = b.lastUsedAt ? new Date(b.lastUsedAt) : new Date(0);
return bLastUsed - aLastUsed; // 降序(最近使用的在前)
default:
return 0;
}
});
},
async loadModelStats() {
this.modelStatsLoading = true;
@@ -3180,6 +3317,19 @@ const app = createApp({
// 如果没有后端费用数据,返回默认值
return '$0.000000';
},
// 计算API Key费用数值用于排序
calculateApiKeyCostNumber(usage) {
if (!usage || !usage.total) return 0;
// 使用后端返回的准确费用数据
if (usage.total.cost) {
return usage.total.cost;
}
// 如果没有后端费用数据返回0
return 0;
},
// 初始化日期筛选器
initializeDateFilter() {

View File

@@ -575,17 +575,40 @@
<table class="min-w-full">
<thead class="bg-gray-50/80 backdrop-blur-sm">
<tr>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">名称</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortApiKeys('name')">
名称
<i v-if="apiKeysSortBy === 'name'" :class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">API Key</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">状态</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">使用统计</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">创建时间</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">过期时间</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortApiKeys('status')">
状态
<i v-if="apiKeysSortBy === 'status'" :class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
使用统计
<span class="cursor-pointer hover:bg-gray-100 px-2 py-1 rounded" @click="sortApiKeys('cost')">
(费用
<i v-if="apiKeysSortBy === 'cost'" :class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>)
</span>
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortApiKeys('createdAt')">
创建时间
<i v-if="apiKeysSortBy === 'createdAt'" :class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortApiKeys('expiresAt')">
过期时间
<i v-if="apiKeysSortBy === 'expiresAt'" :class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200/50">
<template v-for="key in apiKeys" :key="key.id">
<template v-for="key in sortedApiKeys" :key="key.id">
<!-- API Key 主行 -->
<tr class="table-row">
<td class="px-6 py-4 whitespace-nowrap">
@@ -922,12 +945,21 @@
<h3 class="text-xl font-bold text-gray-900 mb-2">账户管理</h3>
<p class="text-gray-600">管理您的 Claude 和 Gemini 账户及代理配置</p>
</div>
<button
@click.stop="openCreateAccountModal"
class="btn btn-success px-6 py-3 flex items-center gap-2"
>
<i class="fas fa-plus"></i>添加账户
</button>
<div class="flex gap-2">
<select v-model="accountSortBy" @change="sortAccounts()" class="form-input px-3 py-2 text-sm">
<option value="name">按名称排序</option>
<option value="dailyTokens">按今日Token排序</option>
<option value="dailyRequests">按今日请求数排序</option>
<option value="totalTokens">按总Token排序</option>
<option value="lastUsed">按最后使用排序</option>
</select>
<button
@click.stop="openCreateAccountModal"
class="btn btn-success px-6 py-3 flex items-center gap-2"
>
<i class="fas fa-plus"></i>添加账户
</button>
</div>
</div>
<div v-if="accountsLoading" class="text-center py-12">
@@ -947,17 +979,34 @@
<table class="min-w-full">
<thead class="bg-gray-50/80 backdrop-blur-sm">
<tr>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">名称</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">平台</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">类型</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">状态</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortAccounts('name')">
名称
<i v-if="accountsSortBy === 'name'" :class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortAccounts('platform')">
平台
<i v-if="accountsSortBy === 'platform'" :class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortAccounts('accountType')">
类型
<i v-if="accountsSortBy === 'accountType'" :class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortAccounts('status')">
状态
<i v-if="accountsSortBy === 'status'" :class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">代理</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">今日使用</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">最后使用</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200/50">
<tr v-for="account in accounts" :key="account.id" class="table-row">
<tr v-for="account in sortedAccounts" :key="account.id" class="table-row">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="w-8 h-8 bg-gradient-to-br from-green-500 to-green-600 rounded-lg flex items-center justify-center mr-3">
@@ -1024,6 +1073,22 @@
</div>
<div v-else class="text-gray-400">无代理</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
<div v-if="account.usage && account.usage.daily" class="space-y-1">
<div class="flex items-center gap-2">
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
<span class="text-sm font-medium text-gray-900">{{ account.usage.daily.requests || 0 }} 次</span>
</div>
<div class="flex items-center gap-2">
<div class="w-2 h-2 bg-blue-500 rounded-full"></div>
<span class="text-xs text-gray-600">{{ formatNumber(account.usage.daily.allTokens || 0) }} tokens</span>
</div>
<div v-if="account.usage.averages && account.usage.averages.rpm > 0" class="text-xs text-gray-500">
平均 {{ account.usage.averages.rpm.toFixed(2) }} RPM
</div>
</div>
<div v-else class="text-gray-400 text-xs">暂无数据</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ account.lastUsedAt ? new Date(account.lastUsedAt).toLocaleDateString() : '从未使用' }}
</td>