feat: 优化Excel导出功能,专注用量数据统计

- 简化导出内容,仅包含用量相关数据
- 保留API Key名称和所有者信息
- 导出详细的分模型用量统计:
  * 今日各模型请求数、费用、输入/输出/总Token
  * 累计各模型请求数、费用、输入/输出/总Token
  * 根据时间筛选条件导出对应周期的模型统计
- 文件名包含时间筛选条件,便于识别数据范围
- 动态设置列宽,优化Excel显示效果
- 移除冗余的配置信息,专注核心用量数据

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Edric Li
2025-09-07 08:33:15 +08:00
parent 8dd58900d6
commit 8f43b9367b
3 changed files with 258 additions and 2 deletions

View File

@@ -168,6 +168,18 @@
<span>{{ showCheckboxes ? '取消选择' : '选择' }}</span>
</button>
<!-- 导出数据按钮 -->
<button
class="group relative flex items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-all duration-200 hover:border-gray-300 hover:shadow-md dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:hover:border-gray-500 sm:w-auto"
@click="exportToExcel"
>
<div
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-emerald-500 to-green-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
></div>
<i class="fas fa-file-excel relative text-emerald-500" />
<span class="relative">导出数据</span>
</button>
<!-- 批量编辑按钮 - 移到刷新按钮旁边 -->
<button
v-if="selectedApiKeys.length > 0"
@@ -1754,6 +1766,7 @@ import { showToast } from '@/utils/toast'
import { apiClient } from '@/config/api'
import { useClientsStore } from '@/stores/clients'
import { useAuthStore } from '@/stores/auth'
import * as XLSX from 'xlsx'
import CreateApiKeyModal from '@/components/apikeys/CreateApiKeyModal.vue'
import EditApiKeyModal from '@/components/apikeys/EditApiKeyModal.vue'
import RenewApiKeyModal from '@/components/apikeys/RenewApiKeyModal.vue'
@@ -3207,6 +3220,13 @@ const getWeeklyOpusCostProgressColor = (key) => {
return 'bg-green-500'
}
// 获取总费用进度 - 暂时不用
// const getTotalCostProgress = (key) => {
// if (!key.totalCostLimit || key.totalCostLimit === 0) return 0
// const percentage = ((key.totalCost || 0) / key.totalCostLimit) * 100
// return Math.min(percentage, 100)
// }
// 显示使用详情
const showUsageDetails = (apiKey) => {
selectedApiKeyForDetail.value = apiKey
@@ -3232,6 +3252,137 @@ const clearSearch = () => {
currentPage.value = 1
}
// 导出数据到Excel
const exportToExcel = () => {
try {
// 准备导出的数据 - 仅导出用量数据
const exportData = sortedApiKeys.value.map((key) => {
// 基础数据
const baseData = {
'API Key名称': key.name || '',
所有者: key.ownerDisplayName || '',
// 当前筛选时间段的统计
请求总数: getPeriodRequests(key),
'总费用($)': calculatePeriodCost(key).toFixed(4),
总Token数: getPeriodTokens(key),
// 今日统计
今日请求数: key.usage?.daily?.requests || 0,
'今日费用($)': (key.dailyCost || 0).toFixed(4),
今日Token数: key.usage?.daily?.totalTokens || 0,
// 累计总统计
累计总请求数: key.usage?.total?.requests || 0,
'累计总费用($)': (key.totalCost || 0).toFixed(4),
累计总Token数: key.usage?.total?.totalTokens || 0
}
// 添加分模型统计(如果有)
const modelStats = {}
// 处理每日模型统计
if (key.usage?.daily?.models) {
Object.entries(key.usage.daily.models).forEach(([model, stats]) => {
const modelName = model.replace(/[:/]/g, '_') // 处理模型名中的特殊字符
modelStats[`今日_${modelName}_请求数`] = stats.requests || 0
modelStats[`今日_${modelName}_费用($)`] = (stats.cost || 0).toFixed(4)
modelStats[`今日_${modelName}_输入Token`] = stats.inputTokens || 0
modelStats[`今日_${modelName}_输出Token`] = stats.outputTokens || 0
modelStats[`今日_${modelName}_总Token`] = stats.totalTokens || 0
})
}
// 处理总计模型统计
if (key.usage?.total?.models) {
Object.entries(key.usage.total.models).forEach(([model, stats]) => {
const modelName = model.replace(/[:/]/g, '_')
modelStats[`累计_${modelName}_请求数`] = stats.requests || 0
modelStats[`累计_${modelName}_费用($)`] = (stats.cost || 0).toFixed(4)
modelStats[`累计_${modelName}_输入Token`] = stats.inputTokens || 0
modelStats[`累计_${modelName}_输出Token`] = stats.outputTokens || 0
modelStats[`累计_${modelName}_总Token`] = stats.totalTokens || 0
})
}
// 处理筛选时间段的模型统计
if (globalDateFilter.preset === '7days' && key.usage?.weekly?.models) {
Object.entries(key.usage.weekly.models).forEach(([model, stats]) => {
const modelName = model.replace(/[:/]/g, '_')
modelStats[`本周_${modelName}_请求数`] = stats.requests || 0
modelStats[`本周_${modelName}_费用($)`] = (stats.cost || 0).toFixed(4)
modelStats[`本周_${modelName}_输入Token`] = stats.inputTokens || 0
modelStats[`本周_${modelName}_输出Token`] = stats.outputTokens || 0
modelStats[`本周_${modelName}_总Token`] = stats.totalTokens || 0
})
} else if (globalDateFilter.preset === '30days' && key.usage?.monthly?.models) {
Object.entries(key.usage.monthly.models).forEach(([model, stats]) => {
const modelName = model.replace(/[:/]/g, '_')
modelStats[`本月_${modelName}_请求数`] = stats.requests || 0
modelStats[`本月_${modelName}_费用($)`] = (stats.cost || 0).toFixed(4)
modelStats[`本月_${modelName}_输入Token`] = stats.inputTokens || 0
modelStats[`本月_${modelName}_输出Token`] = stats.outputTokens || 0
modelStats[`本月_${modelName}_总Token`] = stats.totalTokens || 0
})
}
return { ...baseData, ...modelStats }
})
// 创建工作簿
const ws = XLSX.utils.json_to_sheet(exportData)
// 动态设置列宽
const headers = Object.keys(exportData[0] || {})
const colWidths = headers.map((header) => {
if (header.includes('名称') || header.includes('所有者')) return { wch: 20 }
if (header.includes('费用')) return { wch: 15 }
if (header.includes('Token')) return { wch: 15 }
if (header.includes('请求')) return { wch: 12 }
return { wch: 15 }
})
ws['!cols'] = colWidths
// 创建工作簿
const wb = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(wb, ws, '用量统计')
// 生成文件名(包含时间戳和筛选条件)
const now = new Date()
const timestamp =
now.getFullYear() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0') +
'_' +
String(now.getHours()).padStart(2, '0') +
String(now.getMinutes()).padStart(2, '0') +
String(now.getSeconds()).padStart(2, '0')
let timeRangeLabel = ''
if (globalDateFilter.type === 'preset') {
const presetLabels = {
today: '今日',
'7days': '最近7天',
'30days': '最近30天',
all: '全部时间'
}
timeRangeLabel = presetLabels[globalDateFilter.preset] || globalDateFilter.preset
} else {
timeRangeLabel = '自定义时间'
}
const filename = `API_Keys_用量统计_${timeRangeLabel}_${timestamp}.xlsx`
// 导出文件
XLSX.writeFile(wb, filename)
showToast(`成功导出 ${exportData.length} 条API Key用量数据`, 'success')
} catch (error) {
console.error('导出失败:', error)
showToast('导出失败,请重试', 'error')
}
}
// 监听筛选条件变化,重置页码和选中状态
// 监听筛选条件变化(不包括搜索),清空选中状态
watch([selectedTagFilter, apiKeyStatsTimeRange], () => {