mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
refactor: 移除仪表盘使用记录功能以避免与PR #753重叠
移除了仪表盘中的使用记录展示功能,避免与PR #753的API Key详细使用记录功能重叠: - 移除DashboardView.vue中的使用记录表格UI及相关函数 - 移除dashboard.js中的/dashboard/usage-records接口 - 保留核心账户管理功能(账户过滤、限流状态、统计模态框等) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -704,142 +704,4 @@ router.post('/cleanup', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 📊 获取最近的使用记录
|
|
||||||
router.get('/dashboard/usage-records', authenticateAdmin, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { limit = 100, offset = 0 } = req.query
|
|
||||||
const limitNum = Math.min(parseInt(limit) || 100, 500) // 最多500条
|
|
||||||
const offsetNum = Math.max(parseInt(offset) || 0, 0)
|
|
||||||
|
|
||||||
// 获取所有API Keys
|
|
||||||
const apiKeys = await apiKeyService.getAllApiKeys()
|
|
||||||
if (!apiKeys || apiKeys.length === 0) {
|
|
||||||
return res.json({ success: true, data: { records: [], total: 0 } })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 收集所有API Key的使用记录
|
|
||||||
const allRecords = []
|
|
||||||
for (const key of apiKeys) {
|
|
||||||
try {
|
|
||||||
const records = await redis.getUsageRecords(key.id, 100) // 每个key最多取100条
|
|
||||||
if (records && records.length > 0) {
|
|
||||||
// 为每条记录添加API Key信息
|
|
||||||
const enrichedRecords = records.map((record) => ({
|
|
||||||
...record,
|
|
||||||
apiKeyId: key.id,
|
|
||||||
apiKeyName: key.name || 'Unnamed Key'
|
|
||||||
}))
|
|
||||||
allRecords.push(...enrichedRecords)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Failed to get usage records for key ${key.id}:`, error)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按时间戳倒序排序(最新的在前)
|
|
||||||
allRecords.sort((a, b) => {
|
|
||||||
const timeA = new Date(a.timestamp).getTime()
|
|
||||||
const timeB = new Date(b.timestamp).getTime()
|
|
||||||
return timeB - timeA
|
|
||||||
})
|
|
||||||
|
|
||||||
// 分页
|
|
||||||
const paginatedRecords = allRecords.slice(offsetNum, offsetNum + limitNum)
|
|
||||||
|
|
||||||
// 获取账户名称映射
|
|
||||||
const accountIds = [...new Set(paginatedRecords.map((r) => r.accountId).filter(Boolean))]
|
|
||||||
const accountNameMap = {}
|
|
||||||
|
|
||||||
// 并发获取所有账户名称
|
|
||||||
await Promise.all(
|
|
||||||
accountIds.map(async (accountId) => {
|
|
||||||
try {
|
|
||||||
// 尝试从不同类型的账户中获取
|
|
||||||
const claudeAcc = await redis.getAccount(accountId)
|
|
||||||
if (claudeAcc && claudeAcc.name) {
|
|
||||||
accountNameMap[accountId] = claudeAcc.name
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const consoleAcc = await redis.getClaudeConsoleAccount(accountId)
|
|
||||||
if (consoleAcc && consoleAcc.name) {
|
|
||||||
accountNameMap[accountId] = consoleAcc.name
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const geminiAcc = await redis.getGeminiAccount(accountId)
|
|
||||||
if (geminiAcc && geminiAcc.name) {
|
|
||||||
accountNameMap[accountId] = geminiAcc.name
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const bedrockAcc = await redis.getBedrockAccount(accountId)
|
|
||||||
if (bedrockAcc && bedrockAcc.name) {
|
|
||||||
accountNameMap[accountId] = bedrockAcc.name
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const azureAcc = await redis.getAzureOpenaiAccount(accountId)
|
|
||||||
if (azureAcc && azureAcc.name) {
|
|
||||||
accountNameMap[accountId] = azureAcc.name
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const openaiResponsesAcc = await redis.getOpenaiResponsesAccount(accountId)
|
|
||||||
if (openaiResponsesAcc && openaiResponsesAcc.name) {
|
|
||||||
accountNameMap[accountId] = openaiResponsesAcc.name
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const droidAcc = await redis.getDroidAccount(accountId)
|
|
||||||
if (droidAcc && droidAcc.name) {
|
|
||||||
accountNameMap[accountId] = droidAcc.name
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const ccrAcc = await redis.getCcrAccount(accountId)
|
|
||||||
if (ccrAcc && ccrAcc.name) {
|
|
||||||
accountNameMap[accountId] = ccrAcc.name
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const openaiAcc = await redis.getOpenaiAccount(accountId)
|
|
||||||
if (openaiAcc && openaiAcc.name) {
|
|
||||||
accountNameMap[accountId] = openaiAcc.name
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 降级显示ID
|
|
||||||
accountNameMap[accountId] = accountId
|
|
||||||
} catch (error) {
|
|
||||||
accountNameMap[accountId] = accountId
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// 为记录添加账户名称
|
|
||||||
const enrichedRecords = paginatedRecords.map((record) => ({
|
|
||||||
...record,
|
|
||||||
accountName: record.accountId ? accountNameMap[record.accountId] || record.accountId : '-'
|
|
||||||
}))
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
records: enrichedRecords,
|
|
||||||
total: allRecords.length,
|
|
||||||
limit: limitNum,
|
|
||||||
offset: offsetNum
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('❌ Failed to get usage records:', error)
|
|
||||||
return res.status(500).json({
|
|
||||||
error: 'Failed to get usage records',
|
|
||||||
message: error.message
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|||||||
@@ -673,246 +673,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 最近使用记录 -->
|
|
||||||
<div class="mb-4 sm:mb-6 md:mb-8">
|
|
||||||
<div class="mb-4 flex items-center justify-between sm:mb-6">
|
|
||||||
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100 sm:text-xl">最近使用记录</h3>
|
|
||||||
<button
|
|
||||||
class="flex items-center gap-1 rounded-md border border-gray-300 bg-white px-3 py-1 text-sm font-medium text-blue-600 shadow-sm transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:hover:bg-gray-700 sm:gap-2"
|
|
||||||
:disabled="usageRecordsLoading"
|
|
||||||
@click="loadUsageRecords"
|
|
||||||
>
|
|
||||||
<i :class="['fas fa-sync-alt text-xs', { 'animate-spin': usageRecordsLoading }]"></i>
|
|
||||||
<span class="hidden sm:inline">{{ usageRecordsLoading ? '刷新中' : '刷新' }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card p-4 sm:p-6">
|
|
||||||
<div v-if="usageRecordsLoading" class="py-12 text-center">
|
|
||||||
<div class="loading-spinner mx-auto mb-4"></div>
|
|
||||||
<p class="text-gray-500 dark:text-gray-400">正在加载使用记录...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="usageRecords.length === 0" class="py-12 text-center">
|
|
||||||
<p class="text-gray-500 dark:text-gray-400">暂无使用记录</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="overflow-x-auto">
|
|
||||||
<table class="w-full" style="min-width: 1100px">
|
|
||||||
<thead class="bg-gray-50 dark:bg-gray-700/50">
|
|
||||||
<tr>
|
|
||||||
<th
|
|
||||||
class="min-w-[140px] border-b border-gray-200 px-3 py-3 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:border-gray-600 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
时间
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
class="min-w-[140px] border-b border-gray-200 px-3 py-3 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:border-gray-600 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
API Key
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
class="min-w-[140px] border-b border-gray-200 px-3 py-3 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:border-gray-600 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
账户
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
class="min-w-[180px] border-b border-gray-200 px-3 py-3 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:border-gray-600 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
模型
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
class="min-w-[90px] border-b border-gray-200 px-3 py-3 text-right text-xs font-bold uppercase tracking-wider text-gray-700 dark:border-gray-600 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
输入
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
class="min-w-[90px] border-b border-gray-200 px-3 py-3 text-right text-xs font-bold uppercase tracking-wider text-gray-700 dark:border-gray-600 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
输出
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
class="min-w-[90px] border-b border-gray-200 px-3 py-3 text-right text-xs font-bold uppercase tracking-wider text-gray-700 dark:border-gray-600 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
缓存创建
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
class="min-w-[90px] border-b border-gray-200 px-3 py-3 text-right text-xs font-bold uppercase tracking-wider text-gray-700 dark:border-gray-600 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
缓存读取
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
class="min-w-[100px] border-b border-gray-200 px-3 py-3 text-right text-xs font-bold uppercase tracking-wider text-gray-700 dark:border-gray-600 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
成本
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="divide-y divide-gray-200/50 dark:divide-gray-600/50">
|
|
||||||
<tr
|
|
||||||
v-for="(record, index) in usageRecords"
|
|
||||||
:key="index"
|
|
||||||
class="hover:bg-gray-50 dark:hover:bg-gray-700/30"
|
|
||||||
>
|
|
||||||
<td class="px-3 py-3 text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
{{ formatRecordTime(record.timestamp) }}
|
|
||||||
</td>
|
|
||||||
<td class="px-3 py-3 text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
<div class="truncate" :title="record.apiKeyName">
|
|
||||||
{{ record.apiKeyName }}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-3 py-3 text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
<div class="truncate" :title="record.accountName">
|
|
||||||
{{ record.accountName }}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-3 py-3 text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
<div class="truncate" :title="record.model">
|
|
||||||
{{ record.model }}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-3 py-3 text-right text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
{{ formatNumber(record.inputTokens) }}
|
|
||||||
</td>
|
|
||||||
<td class="px-3 py-3 text-right text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
{{ formatNumber(record.outputTokens) }}
|
|
||||||
</td>
|
|
||||||
<td class="px-3 py-3 text-right text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
{{ formatNumber(record.cacheCreateTokens) }}
|
|
||||||
</td>
|
|
||||||
<td class="px-3 py-3 text-right text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
{{ formatNumber(record.cacheReadTokens) }}
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
class="px-3 py-3 text-right text-sm font-medium text-gray-900 dark:text-gray-100"
|
|
||||||
>
|
|
||||||
${{ formatCost(record.cost) }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!-- 分页 -->
|
|
||||||
<div v-if="usageRecordsTotal > 0" class="mt-4 space-y-3">
|
|
||||||
<!-- 分页信息和每页数量选择 -->
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3 text-sm">
|
|
||||||
<div class="text-gray-600 dark:text-gray-400">
|
|
||||||
显示 {{ (usageRecordsCurrentPage - 1) * usageRecordsPageSize + 1 }} -
|
|
||||||
{{ Math.min(usageRecordsCurrentPage * usageRecordsPageSize, usageRecordsTotal) }}
|
|
||||||
条,共 {{ usageRecordsTotal }} 条
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-gray-600 dark:text-gray-400">每页显示:</span>
|
|
||||||
<select
|
|
||||||
v-model.number="usageRecordsPageSize"
|
|
||||||
class="rounded-lg border border-gray-300 bg-white px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300"
|
|
||||||
@change="handleUsageRecordsPageSizeChange(usageRecordsPageSize)"
|
|
||||||
>
|
|
||||||
<option v-for="size in usageRecordsPageSizeOptions" :key="size" :value="size">
|
|
||||||
{{ size }} 条
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 分页按钮 -->
|
|
||||||
<div class="flex justify-center gap-2">
|
|
||||||
<!-- 上一页 -->
|
|
||||||
<button
|
|
||||||
class="rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm text-gray-700 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
|
||||||
:disabled="usageRecordsCurrentPage === 1 || usageRecordsLoading"
|
|
||||||
@click="handleUsageRecordsPageChange(usageRecordsCurrentPage - 1)"
|
|
||||||
>
|
|
||||||
<i class="fas fa-chevron-left" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- 页码 -->
|
|
||||||
<template v-if="usageRecordsTotalPages <= 7">
|
|
||||||
<button
|
|
||||||
v-for="page in usageRecordsTotalPages"
|
|
||||||
:key="page"
|
|
||||||
class="min-w-[36px] rounded-lg border px-3 py-1.5 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
:class="
|
|
||||||
page === usageRecordsCurrentPage
|
|
||||||
? 'border-blue-500 bg-blue-500 text-white'
|
|
||||||
: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
|
||||||
"
|
|
||||||
:disabled="usageRecordsLoading"
|
|
||||||
@click="handleUsageRecordsPageChange(page)"
|
|
||||||
>
|
|
||||||
{{ page }}
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<!-- 第一页 -->
|
|
||||||
<button
|
|
||||||
class="min-w-[36px] rounded-lg border px-3 py-1.5 text-sm transition-colors"
|
|
||||||
:class="
|
|
||||||
1 === usageRecordsCurrentPage
|
|
||||||
? 'border-blue-500 bg-blue-500 text-white'
|
|
||||||
: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
|
||||||
"
|
|
||||||
@click="handleUsageRecordsPageChange(1)"
|
|
||||||
>
|
|
||||||
1
|
|
||||||
</button>
|
|
||||||
<!-- 省略号 -->
|
|
||||||
<span v-if="usageRecordsCurrentPage > 3" class="px-2 text-gray-500">...</span>
|
|
||||||
<!-- 当前页附近的页码 -->
|
|
||||||
<button
|
|
||||||
v-for="page in [
|
|
||||||
usageRecordsCurrentPage - 1,
|
|
||||||
usageRecordsCurrentPage,
|
|
||||||
usageRecordsCurrentPage + 1
|
|
||||||
].filter((p) => p > 1 && p < usageRecordsTotalPages)"
|
|
||||||
:key="page"
|
|
||||||
class="min-w-[36px] rounded-lg border px-3 py-1.5 text-sm transition-colors"
|
|
||||||
:class="
|
|
||||||
page === usageRecordsCurrentPage
|
|
||||||
? 'border-blue-500 bg-blue-500 text-white'
|
|
||||||
: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
|
||||||
"
|
|
||||||
@click="handleUsageRecordsPageChange(page)"
|
|
||||||
>
|
|
||||||
{{ page }}
|
|
||||||
</button>
|
|
||||||
<!-- 省略号 -->
|
|
||||||
<span
|
|
||||||
v-if="usageRecordsCurrentPage < usageRecordsTotalPages - 2"
|
|
||||||
class="px-2 text-gray-500"
|
|
||||||
>...</span
|
|
||||||
>
|
|
||||||
<!-- 最后一页 -->
|
|
||||||
<button
|
|
||||||
class="min-w-[36px] rounded-lg border px-3 py-1.5 text-sm transition-colors"
|
|
||||||
:class="
|
|
||||||
usageRecordsTotalPages === usageRecordsCurrentPage
|
|
||||||
? 'border-blue-500 bg-blue-500 text-white'
|
|
||||||
: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
|
||||||
"
|
|
||||||
@click="handleUsageRecordsPageChange(usageRecordsTotalPages)"
|
|
||||||
>
|
|
||||||
{{ usageRecordsTotalPages }}
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 下一页 -->
|
|
||||||
<button
|
|
||||||
class="rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm text-gray-700 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
|
||||||
:disabled="
|
|
||||||
usageRecordsCurrentPage === usageRecordsTotalPages || usageRecordsLoading
|
|
||||||
"
|
|
||||||
@click="handleUsageRecordsPageChange(usageRecordsCurrentPage + 1)"
|
|
||||||
>
|
|
||||||
<i class="fas fa-chevron-right" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -921,22 +681,12 @@ import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue'
|
|||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useDashboardStore } from '@/stores/dashboard'
|
import { useDashboardStore } from '@/stores/dashboard'
|
||||||
import { useThemeStore } from '@/stores/theme'
|
import { useThemeStore } from '@/stores/theme'
|
||||||
import { apiClient } from '@/config/api'
|
|
||||||
import { showToast } from '@/utils/toast'
|
|
||||||
import Chart from 'chart.js/auto'
|
import Chart from 'chart.js/auto'
|
||||||
|
|
||||||
const dashboardStore = useDashboardStore()
|
const dashboardStore = useDashboardStore()
|
||||||
const themeStore = useThemeStore()
|
const themeStore = useThemeStore()
|
||||||
const { isDarkMode } = storeToRefs(themeStore)
|
const { isDarkMode } = storeToRefs(themeStore)
|
||||||
|
|
||||||
// 使用记录相关
|
|
||||||
const usageRecords = ref([])
|
|
||||||
const usageRecordsLoading = ref(false)
|
|
||||||
const usageRecordsTotal = ref(0)
|
|
||||||
const usageRecordsCurrentPage = ref(1)
|
|
||||||
const usageRecordsPageSize = ref(20)
|
|
||||||
const usageRecordsPageSizeOptions = [10, 20, 50, 100]
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
dashboardData,
|
dashboardData,
|
||||||
costsData,
|
costsData,
|
||||||
@@ -1727,94 +1477,13 @@ watch(accountUsageTrendData, () => {
|
|||||||
nextTick(() => createAccountUsageTrendChart())
|
nextTick(() => createAccountUsageTrendChart())
|
||||||
})
|
})
|
||||||
|
|
||||||
// 加载使用记录
|
|
||||||
async function loadUsageRecords() {
|
|
||||||
if (usageRecordsLoading.value) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
usageRecordsLoading.value = true
|
|
||||||
const offset = (usageRecordsCurrentPage.value - 1) * usageRecordsPageSize.value
|
|
||||||
|
|
||||||
const response = await apiClient.get('/admin/dashboard/usage-records', {
|
|
||||||
params: {
|
|
||||||
limit: usageRecordsPageSize.value,
|
|
||||||
offset: offset
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.success && response.data) {
|
|
||||||
usageRecords.value = response.data.records || []
|
|
||||||
usageRecordsTotal.value = response.data.total || 0
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load usage records:', error)
|
|
||||||
showToast('加载使用记录失败', 'error')
|
|
||||||
} finally {
|
|
||||||
usageRecordsLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换页码
|
|
||||||
function handleUsageRecordsPageChange(page) {
|
|
||||||
usageRecordsCurrentPage.value = page
|
|
||||||
loadUsageRecords()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换每页数量
|
|
||||||
function handleUsageRecordsPageSizeChange(size) {
|
|
||||||
usageRecordsPageSize.value = size
|
|
||||||
usageRecordsCurrentPage.value = 1 // 重置到第一页
|
|
||||||
loadUsageRecords()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算总页数
|
|
||||||
const usageRecordsTotalPages = computed(() => {
|
|
||||||
return Math.ceil(usageRecordsTotal.value / usageRecordsPageSize.value) || 1
|
|
||||||
})
|
|
||||||
|
|
||||||
// 格式化记录时间
|
|
||||||
function formatRecordTime(timestamp) {
|
|
||||||
if (!timestamp) return '-'
|
|
||||||
const date = new Date(timestamp)
|
|
||||||
const now = new Date()
|
|
||||||
const diff = now - date
|
|
||||||
|
|
||||||
// 如果是今天
|
|
||||||
if (diff < 86400000 && date.getDate() === now.getDate()) {
|
|
||||||
return date.toLocaleTimeString('zh-CN', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果是昨天
|
|
||||||
if (diff < 172800000 && date.getDate() === now.getDate() - 1) {
|
|
||||||
return '昨天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 其他日期
|
|
||||||
return date.toLocaleString('zh-CN', {
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化成本
|
|
||||||
function formatCost(cost) {
|
|
||||||
if (!cost || cost === 0) return '0.000000'
|
|
||||||
return cost.toFixed(6)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 刷新所有数据
|
// 刷新所有数据
|
||||||
async function refreshAllData() {
|
async function refreshAllData() {
|
||||||
if (isRefreshing.value) return
|
if (isRefreshing.value) return
|
||||||
|
|
||||||
isRefreshing.value = true
|
isRefreshing.value = true
|
||||||
try {
|
try {
|
||||||
await Promise.all([loadDashboardData(), refreshChartsData(), loadUsageRecords()])
|
await Promise.all([loadDashboardData(), refreshChartsData()])
|
||||||
} finally {
|
} finally {
|
||||||
isRefreshing.value = false
|
isRefreshing.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user