mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
给API Keys和账号管理列表增加关键字段排序
This commit is contained in:
118
web/admin/app.js
118
web/admin/app.js
@@ -112,6 +112,8 @@ const app = createApp({
|
|||||||
apiKeys: [],
|
apiKeys: [],
|
||||||
apiKeysLoading: false,
|
apiKeysLoading: false,
|
||||||
apiKeyStatsTimeRange: 'all', // API Key统计时间范围:all, 7days, monthly
|
apiKeyStatsTimeRange: 'all', // API Key统计时间范围:all, 7days, monthly
|
||||||
|
apiKeysSortBy: '', // 当前排序字段
|
||||||
|
apiKeysSortOrder: 'asc', // 排序顺序 'asc' 或 'desc'
|
||||||
showCreateApiKeyModal: false,
|
showCreateApiKeyModal: false,
|
||||||
createApiKeyLoading: false,
|
createApiKeyLoading: false,
|
||||||
apiKeyForm: {
|
apiKeyForm: {
|
||||||
@@ -192,6 +194,8 @@ const app = createApp({
|
|||||||
// 账户
|
// 账户
|
||||||
accounts: [],
|
accounts: [],
|
||||||
accountsLoading: false,
|
accountsLoading: false,
|
||||||
|
accountsSortBy: '', // 当前排序字段
|
||||||
|
accountsSortOrder: 'asc', // 排序顺序 'asc' 或 'desc'
|
||||||
showCreateAccountModal: false,
|
showCreateAccountModal: false,
|
||||||
createAccountLoading: false,
|
createAccountLoading: false,
|
||||||
accountForm: {
|
accountForm: {
|
||||||
@@ -295,6 +299,83 @@ const app = createApp({
|
|||||||
return `${window.location.protocol}//${window.location.host}/api/`;
|
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() {
|
dedicatedAccounts() {
|
||||||
return this.accounts.filter(account =>
|
return this.accounts.filter(account =>
|
||||||
@@ -399,6 +480,30 @@ const app = createApp({
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
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
|
// 从URL读取tab参数并设置activeTab
|
||||||
initializeTabFromUrl() {
|
initializeTabFromUrl() {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
@@ -3150,6 +3255,19 @@ const app = createApp({
|
|||||||
// 如果没有后端费用数据,返回默认值
|
// 如果没有后端费用数据,返回默认值
|
||||||
return '$0.000000';
|
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() {
|
initializeDateFilter() {
|
||||||
|
|||||||
@@ -575,17 +575,40 @@
|
|||||||
<table class="min-w-full">
|
<table class="min-w-full">
|
||||||
<thead class="bg-gray-50/80 backdrop-blur-sm">
|
<thead class="bg-gray-50/80 backdrop-blur-sm">
|
||||||
<tr>
|
<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">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 cursor-pointer hover:bg-gray-100" @click="sortApiKeys('status')">
|
||||||
<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>
|
<i v-if="apiKeysSortBy === 'status'" :class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">过期时间</th>
|
<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>
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200/50">
|
<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 主行 -->
|
<!-- API Key 主行 -->
|
||||||
<tr class="table-row">
|
<tr class="table-row">
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
@@ -947,17 +970,33 @@
|
|||||||
<table class="min-w-full">
|
<table class="min-w-full">
|
||||||
<thead class="bg-gray-50/80 backdrop-blur-sm">
|
<thead class="bg-gray-50/80 backdrop-blur-sm">
|
||||||
<tr>
|
<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="sortAccounts('name')">
|
||||||
<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>
|
<i v-if="accountsSortBy === 'name'" :class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">状态</th>
|
<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>
|
||||||
<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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200/50">
|
<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">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<div class="flex items-center">
|
<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">
|
<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">
|
||||||
|
|||||||
Reference in New Issue
Block a user