feat: enhance user API keys view and fix admin cost display

- Add deleted API keys display to user's My API Keys view
- Show deleted status with gray indicator and "Deleted" badge
- Display deletion date and hide delete button for deleted keys
- Fix cost calculation in admin deleted API keys tab
- Add getCostStats call to properly populate cost data
- Support includeDeleted parameter in user API keys endpoint

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Feng Yue
2025-08-14 15:25:22 +08:00
parent aff9966ed1
commit 4509f303e6
4 changed files with 41 additions and 8 deletions

View File

@@ -130,7 +130,8 @@ router.get('/profile', authenticateUser, async (req, res) => {
// 🔑 获取用户的API Keys // 🔑 获取用户的API Keys
router.get('/api-keys', authenticateUser, async (req, res) => { router.get('/api-keys', authenticateUser, async (req, res) => {
try { try {
const apiKeys = await apiKeyService.getUserApiKeys(req.user.id) const { includeDeleted = 'false' } = req.query
const apiKeys = await apiKeyService.getUserApiKeys(req.user.id, includeDeleted === 'true')
// 移除敏感信息并格式化usage数据 // 移除敏感信息并格式化usage数据
const safeApiKeys = apiKeys.map((key) => { const safeApiKeys = apiKeys.map((key) => {

View File

@@ -208,6 +208,14 @@ class ApiKeyService {
// 为每个key添加使用统计和当前并发数 // 为每个key添加使用统计和当前并发数
for (const key of apiKeys) { for (const key of apiKeys) {
key.usage = await redis.getUsageStats(key.id) key.usage = await redis.getUsageStats(key.id)
const costStats = await redis.getCostStats(key.id)
// Add cost information to usage object for frontend compatibility
if (key.usage && costStats) {
key.usage.total = key.usage.total || {}
key.usage.total.cost = costStats.total
key.usage.totalCost = costStats.total
}
key.totalCost = costStats ? costStats.total : 0
key.tokenLimit = parseInt(key.tokenLimit) key.tokenLimit = parseInt(key.tokenLimit)
key.concurrencyLimit = parseInt(key.concurrencyLimit || 0) key.concurrencyLimit = parseInt(key.concurrencyLimit || 0)
key.rateLimitWindow = parseInt(key.rateLimitWindow || 0) key.rateLimitWindow = parseInt(key.rateLimitWindow || 0)

View File

@@ -83,14 +83,27 @@
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<div <div
:class="['h-2 w-2 rounded-full', apiKey.isActive ? 'bg-green-400' : 'bg-red-400']" :class="[
'h-2 w-2 rounded-full',
apiKey.isDeleted === 'true' || apiKey.deletedAt
? 'bg-gray-400'
: apiKey.isActive
? 'bg-green-400'
: 'bg-red-400'
]"
></div> ></div>
</div> </div>
<div class="ml-4"> <div class="ml-4">
<div class="flex items-center"> <div class="flex items-center">
<p class="text-sm font-medium text-gray-900">{{ apiKey.name }}</p> <p class="text-sm font-medium text-gray-900">{{ apiKey.name }}</p>
<span <span
v-if="!apiKey.isActive" v-if="apiKey.isDeleted === 'true' || apiKey.deletedAt"
class="ml-2 inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800"
>
Deleted
</span>
<span
v-else-if="!apiKey.isActive"
class="ml-2 inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800" class="ml-2 inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800"
> >
Disabled Disabled
@@ -100,11 +113,17 @@
<p class="text-sm text-gray-500">{{ apiKey.description || 'No description' }}</p> <p class="text-sm text-gray-500">{{ apiKey.description || 'No description' }}</p>
<div class="mt-1 flex items-center space-x-4 text-xs text-gray-400"> <div class="mt-1 flex items-center space-x-4 text-xs text-gray-400">
<span>Created: {{ formatDate(apiKey.createdAt) }}</span> <span>Created: {{ formatDate(apiKey.createdAt) }}</span>
<span v-if="apiKey.lastUsedAt" <span v-if="apiKey.isDeleted === 'true' || apiKey.deletedAt"
>Deleted: {{ formatDate(apiKey.deletedAt) }}</span
>
<span v-else-if="apiKey.lastUsedAt"
>Last used: {{ formatDate(apiKey.lastUsedAt) }}</span >Last used: {{ formatDate(apiKey.lastUsedAt) }}</span
> >
<span v-else>Never used</span> <span v-else>Never used</span>
<span v-if="apiKey.expiresAt">Expires: {{ formatDate(apiKey.expiresAt) }}</span> <span
v-if="apiKey.expiresAt && !(apiKey.isDeleted === 'true' || apiKey.deletedAt)"
>Expires: {{ formatDate(apiKey.expiresAt) }}</span
>
</div> </div>
</div> </div>
</div> </div>
@@ -140,6 +159,7 @@
</button> </button>
<button <button
v-if="!(apiKey.isDeleted === 'true' || apiKey.deletedAt)"
class="inline-flex items-center rounded border border-transparent p-1 text-red-400 hover:text-red-600" class="inline-flex items-center rounded border border-transparent p-1 text-red-400 hover:text-red-600"
title="Delete API Key" title="Delete API Key"
@click="deleteApiKey(apiKey)" @click="deleteApiKey(apiKey)"
@@ -264,7 +284,7 @@ const formatDate = (dateString) => {
const loadApiKeys = async () => { const loadApiKeys = async () => {
loading.value = true loading.value = true
try { try {
apiKeys.value = await userStore.getUserApiKeys() apiKeys.value = await userStore.getUserApiKeys(true) // Include deleted keys
} catch (error) { } catch (error) {
console.error('Failed to load API keys:', error) console.error('Failed to load API keys:', error)
showToast('Failed to load API keys', 'error') showToast('Failed to load API keys', 'error')

View File

@@ -118,9 +118,13 @@ export const useUserStore = defineStore('user', {
}, },
// 🔑 获取用户API Keys // 🔑 获取用户API Keys
async getUserApiKeys() { async getUserApiKeys(includeDeleted = false) {
try { try {
const response = await axios.get(`${API_BASE}/api-keys`) const params = {}
if (includeDeleted) {
params.includeDeleted = 'true'
}
const response = await axios.get(`${API_BASE}/api-keys`, { params })
return response.data.success ? response.data.apiKeys : [] return response.data.success ? response.data.apiKeys : []
} catch (error) { } catch (error) {
console.error('Failed to fetch API keys:', error) console.error('Failed to fetch API keys:', error)