mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
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:
106
web/admin-spa/package-lock.json
generated
106
web/admin-spa/package-lock.json
generated
@@ -15,7 +15,8 @@
|
||||
"element-plus": "^2.4.4",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.5"
|
||||
"vue-router": "^4.2.5",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.55.0",
|
||||
@@ -1366,6 +1367,15 @@
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/adler-32": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
|
||||
@@ -1643,6 +1653,19 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/cfb": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"crc-32": "~1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -1710,6 +1733,15 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/codepage": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -1766,6 +1798,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"crc32": "bin/crc32.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -2497,6 +2541,15 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/frac": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.7.tgz",
|
||||
@@ -4083,6 +4136,18 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ssf": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"frac": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz",
|
||||
@@ -5126,6 +5191,24 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wmf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/word": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
@@ -5244,6 +5327,27 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/xlsx": {
|
||||
"version": "0.18.5",
|
||||
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"cfb": "~1.2.1",
|
||||
"codepage": "~1.15.0",
|
||||
"crc-32": "~1.2.1",
|
||||
"ssf": "~0.11.2",
|
||||
"wmf": "~1.0.1",
|
||||
"word": "~0.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"xlsx": "bin/xlsx.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-name-validator": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"element-plus": "^2.4.4",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.5"
|
||||
"vue-router": "^4.2.5",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.55.0",
|
||||
|
||||
@@ -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], () => {
|
||||
|
||||
Reference in New Issue
Block a user