mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:10:52 +00:00
feat: management of deleted keys
This commit is contained in:
@@ -5,12 +5,7 @@ const userService = require('../services/userService')
|
|||||||
const apiKeyService = require('../services/apiKeyService')
|
const apiKeyService = require('../services/apiKeyService')
|
||||||
const logger = require('../utils/logger')
|
const logger = require('../utils/logger')
|
||||||
const config = require('../../config/config')
|
const config = require('../../config/config')
|
||||||
const {
|
const { authenticateUser, authenticateUserOrAdmin, requireAdmin } = require('../middleware/auth')
|
||||||
authenticateUser,
|
|
||||||
authenticateUserOrAdmin,
|
|
||||||
requireAdmin,
|
|
||||||
requireRole
|
|
||||||
} = require('../middleware/auth')
|
|
||||||
|
|
||||||
// 🔐 用户登录端点
|
// 🔐 用户登录端点
|
||||||
router.post('/login', async (req, res) => {
|
router.post('/login', async (req, res) => {
|
||||||
@@ -253,7 +248,6 @@ router.post('/api-keys', authenticateUser, async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
// 🗑️ 删除API Key
|
// 🗑️ 删除API Key
|
||||||
router.delete('/api-keys/:keyId', authenticateUser, async (req, res) => {
|
router.delete('/api-keys/:keyId', authenticateUser, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -313,7 +307,7 @@ router.get('/usage-stats', authenticateUser, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取使用统计
|
// 获取使用统计
|
||||||
const stats = await apiKeyService.getUsageStats(apiKeyIds, { period, model })
|
const stats = await apiKeyService.getAggregatedUsageStats(apiKeyIds, { period, model })
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -584,7 +578,7 @@ router.get('/:userId/usage-stats', authenticateUserOrAdmin, requireAdmin, async
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取使用统计
|
// 获取使用统计
|
||||||
const stats = await apiKeyService.getUsageStats(apiKeyIds, { period, model })
|
const stats = await apiKeyService.getAggregatedUsageStats(apiKeyIds, { period, model })
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -536,7 +536,7 @@ class ApiKeyService {
|
|||||||
createdAt: key.createdAt,
|
createdAt: key.createdAt,
|
||||||
lastUsedAt: key.lastUsedAt,
|
lastUsedAt: key.lastUsedAt,
|
||||||
expiresAt: key.expiresAt,
|
expiresAt: key.expiresAt,
|
||||||
usage: usage,
|
usage,
|
||||||
dailyCost,
|
dailyCost,
|
||||||
totalCost: costStats.total,
|
totalCost: costStats.total,
|
||||||
dailyCostLimit: parseFloat(key.dailyCostLimit || 0),
|
dailyCostLimit: parseFloat(key.dailyCostLimit || 0),
|
||||||
@@ -628,8 +628,8 @@ class ApiKeyService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🗑️ 删除API Key
|
// 🗑️ 硬删除API Key (完全移除)
|
||||||
async deleteApiKey(keyId) {
|
async hardDeleteApiKey(keyId) {
|
||||||
try {
|
try {
|
||||||
const keyData = await redis.getApiKey(keyId)
|
const keyData = await redis.getApiKey(keyId)
|
||||||
if (!keyData) {
|
if (!keyData) {
|
||||||
@@ -669,14 +669,14 @@ class ApiKeyService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 📊 获取使用统计(支持多个API Key)
|
// 📊 获取聚合使用统计(支持多个API Key)
|
||||||
async getUsageStats(keyIds, options = {}) {
|
async getAggregatedUsageStats(keyIds, options = {}) {
|
||||||
try {
|
try {
|
||||||
if (!Array.isArray(keyIds)) {
|
if (!Array.isArray(keyIds)) {
|
||||||
keyIds = [keyIds]
|
keyIds = [keyIds]
|
||||||
}
|
}
|
||||||
|
|
||||||
const { period = 'week', model } = options
|
const { period: _period = 'week', model: _model } = options
|
||||||
const stats = {
|
const stats = {
|
||||||
totalRequests: 0,
|
totalRequests: 0,
|
||||||
totalInputTokens: 0,
|
totalInputTokens: 0,
|
||||||
|
|||||||
@@ -6,6 +6,50 @@
|
|||||||
<h3 class="mb-1 text-lg font-bold text-gray-900 sm:mb-2 sm:text-xl">API Keys 管理</h3>
|
<h3 class="mb-1 text-lg font-bold text-gray-900 sm:mb-2 sm:text-xl">API Keys 管理</h3>
|
||||||
<p class="text-sm text-gray-600 sm:text-base">管理和监控您的 API 密钥</p>
|
<p class="text-sm text-gray-600 sm:text-base">管理和监控您的 API 密钥</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Navigation -->
|
||||||
|
<div class="border-b border-gray-200">
|
||||||
|
<nav aria-label="Tabs" class="-mb-px flex space-x-8">
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
'whitespace-nowrap border-b-2 px-1 py-2 text-sm font-medium',
|
||||||
|
activeTab === 'active'
|
||||||
|
? 'border-blue-500 text-blue-600'
|
||||||
|
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'
|
||||||
|
]"
|
||||||
|
@click="activeTab = 'active'"
|
||||||
|
>
|
||||||
|
活跃 API Keys
|
||||||
|
<span
|
||||||
|
v-if="apiKeys.length > 0"
|
||||||
|
class="ml-2 rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-900"
|
||||||
|
>
|
||||||
|
{{ apiKeys.length }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
'whitespace-nowrap border-b-2 px-1 py-2 text-sm font-medium',
|
||||||
|
activeTab === 'deleted'
|
||||||
|
? 'border-blue-500 text-blue-600'
|
||||||
|
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'
|
||||||
|
]"
|
||||||
|
@click="loadDeletedApiKeys"
|
||||||
|
>
|
||||||
|
已删除 API Keys
|
||||||
|
<span
|
||||||
|
v-if="deletedApiKeys.length > 0"
|
||||||
|
class="ml-2 rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-900"
|
||||||
|
>
|
||||||
|
{{ deletedApiKeys.length }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Content -->
|
||||||
|
<!-- 活跃 API Keys Tab Panel -->
|
||||||
|
<div v-if="activeTab === 'active'" class="tab-panel">
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<!-- 筛选器组 -->
|
<!-- 筛选器组 -->
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:gap-3">
|
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:gap-3">
|
||||||
@@ -98,7 +142,6 @@
|
|||||||
<span>创建新 Key</span>
|
<span>创建新 Key</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="apiKeysLoading" class="py-12 text-center">
|
<div v-if="apiKeysLoading" class="py-12 text-center">
|
||||||
<div class="loading-spinner mx-auto mb-4" />
|
<div class="loading-spinner mx-auto mb-4" />
|
||||||
@@ -224,7 +267,10 @@
|
|||||||
<i class="fas fa-key text-xs text-white" />
|
<i class="fas fa-key text-xs text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="truncate text-sm font-semibold text-gray-900" :title="key.name">
|
<div
|
||||||
|
class="truncate text-sm font-semibold text-gray-900"
|
||||||
|
:title="key.name"
|
||||||
|
>
|
||||||
{{ key.name }}
|
{{ key.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="truncate text-xs text-gray-500" :title="key.id">
|
<div class="truncate text-xs text-gray-500" :title="key.id">
|
||||||
@@ -297,7 +343,9 @@
|
|||||||
>
|
>
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="!key.tags || key.tags.length === 0" class="text-xs text-gray-400"
|
<span
|
||||||
|
v-if="!key.tags || key.tags.length === 0"
|
||||||
|
class="text-xs text-gray-400"
|
||||||
>无标签</span
|
>无标签</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -421,7 +469,12 @@
|
|||||||
title="编辑过期时间"
|
title="编辑过期时间"
|
||||||
@click.stop="startEditExpiry(key)"
|
@click.stop="startEditExpiry(key)"
|
||||||
>
|
>
|
||||||
<svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
|
class="h-3 w-3"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
|
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
@@ -586,10 +639,14 @@
|
|||||||
<span class="text-xs">刷新</span>
|
<span class="text-xs">刷新</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-400">尝试调整时间范围或点击刷新重新加载数据</p>
|
<p class="text-xs text-gray-400">
|
||||||
|
尝试调整时间范围或点击刷新重新加载数据
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0"
|
v-else-if="
|
||||||
|
apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0
|
||||||
|
"
|
||||||
class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"
|
class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -602,7 +659,8 @@
|
|||||||
<span class="mb-1 block text-sm font-semibold text-gray-800">{{
|
<span class="mb-1 block text-sm font-semibold text-gray-800">{{
|
||||||
stat.model
|
stat.model
|
||||||
}}</span>
|
}}</span>
|
||||||
<span class="rounded-full bg-blue-50 px-2 py-1 text-xs text-gray-500"
|
<span
|
||||||
|
class="rounded-full bg-blue-50 px-2 py-1 text-xs text-gray-500"
|
||||||
>{{ stat.requests }} 次请求</span
|
>{{ stat.requests }} 次请求</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -628,7 +686,9 @@
|
|||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 border-t border-gray-100 pt-2">
|
<div class="mt-2 border-t border-gray-100 pt-2">
|
||||||
<div class="flex items-center justify-between text-xs text-gray-500">
|
<div
|
||||||
|
class="flex items-center justify-between text-xs text-gray-500"
|
||||||
|
>
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<i class="fas fa-arrow-down mr-1 text-green-500" />
|
<i class="fas fa-arrow-down mr-1 text-green-500" />
|
||||||
输入:
|
输入:
|
||||||
@@ -637,7 +697,9 @@
|
|||||||
formatTokenCount(stat.inputTokens)
|
formatTokenCount(stat.inputTokens)
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between text-xs text-gray-500">
|
<div
|
||||||
|
class="flex items-center justify-between text-xs text-gray-500"
|
||||||
|
>
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<i class="fas fa-arrow-up mr-1 text-blue-500" />
|
<i class="fas fa-arrow-up mr-1 text-blue-500" />
|
||||||
输出:
|
输出:
|
||||||
@@ -713,7 +775,10 @@
|
|||||||
<span class="text-gray-600">
|
<span class="text-gray-600">
|
||||||
总请求:
|
总请求:
|
||||||
<span class="font-semibold text-gray-800">{{
|
<span class="font-semibold text-gray-800">{{
|
||||||
apiKeyModelStats[key.id].reduce((sum, stat) => sum + stat.requests, 0)
|
apiKeyModelStats[key.id].reduce(
|
||||||
|
(sum, stat) => sum + stat.requests,
|
||||||
|
0
|
||||||
|
)
|
||||||
}}</span>
|
}}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="text-gray-600">
|
<span class="text-gray-600">
|
||||||
@@ -809,7 +874,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- OpenAI 绑定 -->
|
<!-- OpenAI 绑定 -->
|
||||||
<div v-if="key.openaiAccountId" class="flex flex-wrap items-center gap-1 text-xs">
|
<div v-if="key.openaiAccountId" class="flex flex-wrap items-center gap-1 text-xs">
|
||||||
<span class="inline-flex items-center rounded bg-gray-100 px-2 py-0.5 text-gray-700">
|
<span
|
||||||
|
class="inline-flex items-center rounded bg-gray-100 px-2 py-0.5 text-gray-700"
|
||||||
|
>
|
||||||
<i class="fa-openai mr-1" />
|
<i class="fa-openai mr-1" />
|
||||||
OpenAI
|
OpenAI
|
||||||
</span>
|
</span>
|
||||||
@@ -886,7 +953,9 @@
|
|||||||
|
|
||||||
<!-- 移动端时间窗口限制 -->
|
<!-- 移动端时间窗口限制 -->
|
||||||
<WindowCountdown
|
<WindowCountdown
|
||||||
v-if="key.rateLimitWindow > 0 && (key.rateLimitRequests > 0 || key.tokenLimit > 0)"
|
v-if="
|
||||||
|
key.rateLimitWindow > 0 && (key.rateLimitRequests > 0 || key.tokenLimit > 0)
|
||||||
|
"
|
||||||
:current-requests="key.currentWindowRequests"
|
:current-requests="key.currentWindowRequests"
|
||||||
:current-tokens="key.currentWindowTokens"
|
:current-tokens="key.currentWindowTokens"
|
||||||
:rate-limit-window="key.rateLimitWindow"
|
:rate-limit-window="key.rateLimitWindow"
|
||||||
@@ -1056,7 +1125,9 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- 最后一页 -->
|
<!-- 最后一页 -->
|
||||||
<span v-if="currentPage < totalPages - 3" class="hidden px-2 text-gray-500 sm:inline"
|
<span
|
||||||
|
v-if="currentPage < totalPages - 3"
|
||||||
|
class="hidden px-2 text-gray-500 sm:inline"
|
||||||
>...</span
|
>...</span
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -1080,6 +1151,146 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 已删除 API Keys Tab Panel -->
|
||||||
|
<div v-else-if="activeTab === 'deleted'" class="tab-panel">
|
||||||
|
<div v-if="deletedApiKeysLoading" class="py-12 text-center">
|
||||||
|
<div class="loading-spinner mx-auto mb-4" />
|
||||||
|
<p class="text-gray-500">正在加载已删除的 API Keys...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="deletedApiKeys.length === 0" class="py-12 text-center">
|
||||||
|
<div
|
||||||
|
class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-gray-100"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash text-xl text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<p class="text-lg text-gray-500">暂无已删除的 API Keys</p>
|
||||||
|
<p class="mt-2 text-sm text-gray-400">已删除的 API Keys 会出现在这里</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已删除的 API Keys 表格 -->
|
||||||
|
<div v-else class="table-container">
|
||||||
|
<table class="w-full table-fixed">
|
||||||
|
<thead class="bg-gray-50/80 backdrop-blur-sm">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
class="w-[20%] min-w-[150px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700"
|
||||||
|
>
|
||||||
|
名称
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="w-[15%] min-w-[120px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700"
|
||||||
|
>
|
||||||
|
创建者
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="w-[15%] min-w-[120px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700"
|
||||||
|
>
|
||||||
|
创建时间
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="w-[15%] min-w-[120px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700"
|
||||||
|
>
|
||||||
|
删除者
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="w-[15%] min-w-[120px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700"
|
||||||
|
>
|
||||||
|
删除时间
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="w-[20%] min-w-[150px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700"
|
||||||
|
>
|
||||||
|
使用统计
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200/50">
|
||||||
|
<tr v-for="key in deletedApiKeys" :key="key.id" class="table-row">
|
||||||
|
<td class="px-3 py-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div
|
||||||
|
class="mr-2 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-red-500 to-red-600"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash text-xs text-white" />
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<div class="truncate text-sm font-semibold text-gray-900" :title="key.name">
|
||||||
|
{{ key.name }}
|
||||||
|
</div>
|
||||||
|
<div class="truncate text-xs text-gray-500" :title="key.id">
|
||||||
|
{{ key.id }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-4">
|
||||||
|
<div class="text-sm">
|
||||||
|
<span v-if="key.createdBy === 'admin'" class="text-blue-600">
|
||||||
|
<i class="fas fa-user-shield mr-1" />
|
||||||
|
管理员
|
||||||
|
</span>
|
||||||
|
<span v-else-if="key.userUsername" class="text-green-600">
|
||||||
|
<i class="fas fa-user mr-1" />
|
||||||
|
{{ key.userUsername }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-gray-500">
|
||||||
|
<i class="fas fa-question-circle mr-1" />
|
||||||
|
未知
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||||
|
{{ formatDate(key.createdAt) }}
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-4">
|
||||||
|
<div class="text-sm">
|
||||||
|
<span v-if="key.deletedByType === 'admin'" class="text-blue-600">
|
||||||
|
<i class="fas fa-user-shield mr-1" />
|
||||||
|
{{ key.deletedBy }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="key.deletedByType === 'user'" class="text-green-600">
|
||||||
|
<i class="fas fa-user mr-1" />
|
||||||
|
{{ key.deletedBy }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-gray-500">
|
||||||
|
<i class="fas fa-cog mr-1" />
|
||||||
|
{{ key.deletedBy }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||||
|
{{ formatDate(key.deletedAt) }}
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-4">
|
||||||
|
<div class="text-sm">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-gray-600">请求</span>
|
||||||
|
<span class="font-semibold text-gray-900">
|
||||||
|
{{ formatNumber(key.usage?.total?.requests || 0) }}次
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-gray-600">费用</span>
|
||||||
|
<span class="font-semibold text-green-600">
|
||||||
|
${{ (key.usage?.total?.cost || 0).toFixed(4) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="key.lastUsedAt" class="flex items-center justify-between">
|
||||||
|
<span class="text-gray-600">最后使用</span>
|
||||||
|
<span class="font-medium text-gray-700">
|
||||||
|
{{ formatLastUsed(key.lastUsedAt) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-xs text-gray-400">从未使用</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 模态框组件 -->
|
<!-- 模态框组件 -->
|
||||||
<CreateApiKeyModal
|
<CreateApiKeyModal
|
||||||
v-if="showCreateApiKeyModal"
|
v-if="showCreateApiKeyModal"
|
||||||
@@ -1132,6 +1343,8 @@
|
|||||||
@close="showUsageDetailModal = false"
|
@close="showUsageDetailModal = false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -1154,6 +1367,11 @@ const clientsStore = useClientsStore()
|
|||||||
const apiKeys = ref([])
|
const apiKeys = ref([])
|
||||||
const apiKeysLoading = ref(false)
|
const apiKeysLoading = ref(false)
|
||||||
const apiKeyStatsTimeRange = ref('today')
|
const apiKeyStatsTimeRange = ref('today')
|
||||||
|
|
||||||
|
// Tab management
|
||||||
|
const activeTab = ref('active')
|
||||||
|
const deletedApiKeys = ref([])
|
||||||
|
const deletedApiKeysLoading = ref(false)
|
||||||
const apiKeysSortBy = ref('')
|
const apiKeysSortBy = ref('')
|
||||||
const apiKeysSortOrder = ref('asc')
|
const apiKeysSortOrder = ref('asc')
|
||||||
const expandedApiKeys = ref({})
|
const expandedApiKeys = ref({})
|
||||||
@@ -1376,6 +1594,22 @@ const loadApiKeys = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载已删除的API Keys
|
||||||
|
const loadDeletedApiKeys = async () => {
|
||||||
|
activeTab.value = 'deleted'
|
||||||
|
deletedApiKeysLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = await apiClient.get('/admin/api-keys/deleted')
|
||||||
|
if (data.success) {
|
||||||
|
deletedApiKeys.value = data.apiKeys || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showToast('加载已删除的 API Keys 失败', 'error')
|
||||||
|
} finally {
|
||||||
|
deletedApiKeysLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 排序API Keys
|
// 排序API Keys
|
||||||
const sortApiKeys = (field) => {
|
const sortApiKeys = (field) => {
|
||||||
if (apiKeysSortBy.value === field) {
|
if (apiKeysSortBy.value === field) {
|
||||||
|
|||||||
Reference in New Issue
Block a user