mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 增强API Keys Excel导出功能和样式美化
- 添加输入/输出Token列到Excel导出 - 使用xlsx-js-style库实现专业的Excel样式 - 彩色表头(蓝色/绿色区分) - 交替行背景色 - 正确的列对齐(日期右对齐,名称左对齐) - 费用列特殊样式(蓝色加粗) - 简化导出内容,仅包含用量数据 - Token数量使用K/M单位格式化 - 模型统计也包含输入/输出Token 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
98
web/admin-spa/package-lock.json
generated
98
web/admin-spa/package-lock.json
generated
@@ -16,7 +16,8 @@
|
|||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-router": "^4.2.5",
|
"vue-router": "^4.2.5",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5",
|
||||||
|
"xlsx-js-style": "^1.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.55.0",
|
"@playwright/test": "^1.55.0",
|
||||||
@@ -2348,6 +2349,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/exit-on-epipe": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/exsolve": {
|
"node_modules/exsolve": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.7.tgz",
|
"resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.7.tgz",
|
||||||
@@ -2423,6 +2433,12 @@
|
|||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fflate": {
|
||||||
|
"version": "0.3.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.3.11.tgz",
|
||||||
|
"integrity": "sha512-Rr5QlUeGN1mbOHlaqcSYMKVpPbgLy0AWT/W0EHxA6NGI12yO1jpoui2zBBvU2G824ltM6Ut8BFgfHSBGfkmS0A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||||
@@ -3857,6 +3873,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/printj": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"printj": "bin/printj.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
@@ -5348,6 +5376,74 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xlsx-js-style": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xlsx-js-style/-/xlsx-js-style-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-DDT4FXFSWfT4DXMSok/m3TvmP1gvO3dn0Eu/c+eXHW5Kzmp7IczNkxg/iEPnImbG9X0Vb8QhROda5eatSR/97Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.2.0",
|
||||||
|
"cfb": "^1.1.4",
|
||||||
|
"codepage": "~1.14.0",
|
||||||
|
"commander": "~2.17.1",
|
||||||
|
"crc-32": "~1.2.0",
|
||||||
|
"exit-on-epipe": "~1.0.1",
|
||||||
|
"fflate": "^0.3.8",
|
||||||
|
"ssf": "~0.11.2",
|
||||||
|
"wmf": "~1.0.1",
|
||||||
|
"word": "~0.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"xlsx": "bin/xlsx.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xlsx-js-style/node_modules/adler-32": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-/vUqU/UY4MVeFsg+SsK6c+/05RZXIHZMGJA+PX5JyWI0ZRcBpupnRuPLU/NXXoFwMYCPCoxIfElM2eS+DUXCqQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"exit-on-epipe": "~1.0.1",
|
||||||
|
"printj": "~1.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"adler32": "bin/adler32.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xlsx-js-style/node_modules/codepage": {
|
||||||
|
"version": "1.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.14.0.tgz",
|
||||||
|
"integrity": "sha512-iz3zJLhlrg37/gYRWgEPkaFTtzmnEv1h+r7NgZum2lFElYQPi0/5bnmuDfODHxfp0INEfnRqyfyeIJDbb7ahRw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "~2.14.1",
|
||||||
|
"exit-on-epipe": "~1.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"codepage": "bin/codepage.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xlsx-js-style/node_modules/codepage/node_modules/commander": {
|
||||||
|
"version": "2.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz",
|
||||||
|
"integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/xlsx-js-style/node_modules/commander": {
|
||||||
|
"version": "2.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
|
||||||
|
"integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/xml-name-validator": {
|
"node_modules/xml-name-validator": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-router": "^4.2.5",
|
"vue-router": "^4.2.5",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5",
|
||||||
|
"xlsx-js-style": "^1.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.55.0",
|
"@playwright/test": "^1.55.0",
|
||||||
|
|||||||
@@ -1766,7 +1766,7 @@ import { showToast } from '@/utils/toast'
|
|||||||
import { apiClient } from '@/config/api'
|
import { apiClient } from '@/config/api'
|
||||||
import { useClientsStore } from '@/stores/clients'
|
import { useClientsStore } from '@/stores/clients'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import * as XLSX from 'xlsx'
|
import * as XLSX from 'xlsx-js-style'
|
||||||
import CreateApiKeyModal from '@/components/apikeys/CreateApiKeyModal.vue'
|
import CreateApiKeyModal from '@/components/apikeys/CreateApiKeyModal.vue'
|
||||||
import EditApiKeyModal from '@/components/apikeys/EditApiKeyModal.vue'
|
import EditApiKeyModal from '@/components/apikeys/EditApiKeyModal.vue'
|
||||||
import RenewApiKeyModal from '@/components/apikeys/RenewApiKeyModal.vue'
|
import RenewApiKeyModal from '@/components/apikeys/RenewApiKeyModal.vue'
|
||||||
@@ -2538,6 +2538,102 @@ const getPeriodTokens = (key) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取日期范围内的输入token数量
|
||||||
|
const getPeriodInputTokens = (key) => {
|
||||||
|
// 根据全局日期筛选器返回对应的输入token数量
|
||||||
|
if (globalDateFilter.type === 'custom') {
|
||||||
|
// 自定义日期范围
|
||||||
|
if (key.usage) {
|
||||||
|
if (key.usage['custom'] && key.usage['custom'].inputTokens !== undefined) {
|
||||||
|
return key.usage['custom'].inputTokens
|
||||||
|
}
|
||||||
|
if (key.usage.total && key.usage.total.inputTokens !== undefined) {
|
||||||
|
return key.usage.total.inputTokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
} else if (globalDateFilter.preset === 'today') {
|
||||||
|
return key.usage?.daily?.inputTokens || 0
|
||||||
|
} else if (globalDateFilter.preset === '7days') {
|
||||||
|
// 使用 usage['7days'].inputTokens
|
||||||
|
if (key.usage && key.usage['7days'] && key.usage['7days'].inputTokens !== undefined) {
|
||||||
|
return key.usage['7days'].inputTokens
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
} else if (globalDateFilter.preset === '30days') {
|
||||||
|
// 使用 usage['30days'].inputTokens 或 usage.monthly.inputTokens
|
||||||
|
if (key.usage) {
|
||||||
|
if (key.usage['30days'] && key.usage['30days'].inputTokens !== undefined) {
|
||||||
|
return key.usage['30days'].inputTokens
|
||||||
|
}
|
||||||
|
if (key.usage.monthly && key.usage.monthly.inputTokens !== undefined) {
|
||||||
|
return key.usage.monthly.inputTokens
|
||||||
|
}
|
||||||
|
if (key.usage.total && key.usage.total.inputTokens !== undefined) {
|
||||||
|
return key.usage.total.inputTokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
} else if (globalDateFilter.preset === 'all') {
|
||||||
|
// 全部时间
|
||||||
|
if (key.usage && key.usage['all'] && key.usage['all'].inputTokens !== undefined) {
|
||||||
|
return key.usage['all'].inputTokens
|
||||||
|
}
|
||||||
|
return key.usage?.total?.inputTokens || 0
|
||||||
|
} else {
|
||||||
|
// 默认返回
|
||||||
|
return key.usage?.total?.inputTokens || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取日期范围内的输出token数量
|
||||||
|
const getPeriodOutputTokens = (key) => {
|
||||||
|
// 根据全局日期筛选器返回对应的输出token数量
|
||||||
|
if (globalDateFilter.type === 'custom') {
|
||||||
|
// 自定义日期范围
|
||||||
|
if (key.usage) {
|
||||||
|
if (key.usage['custom'] && key.usage['custom'].outputTokens !== undefined) {
|
||||||
|
return key.usage['custom'].outputTokens
|
||||||
|
}
|
||||||
|
if (key.usage.total && key.usage.total.outputTokens !== undefined) {
|
||||||
|
return key.usage.total.outputTokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
} else if (globalDateFilter.preset === 'today') {
|
||||||
|
return key.usage?.daily?.outputTokens || 0
|
||||||
|
} else if (globalDateFilter.preset === '7days') {
|
||||||
|
// 使用 usage['7days'].outputTokens
|
||||||
|
if (key.usage && key.usage['7days'] && key.usage['7days'].outputTokens !== undefined) {
|
||||||
|
return key.usage['7days'].outputTokens
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
} else if (globalDateFilter.preset === '30days') {
|
||||||
|
// 使用 usage['30days'].outputTokens 或 usage.monthly.outputTokens
|
||||||
|
if (key.usage) {
|
||||||
|
if (key.usage['30days'] && key.usage['30days'].outputTokens !== undefined) {
|
||||||
|
return key.usage['30days'].outputTokens
|
||||||
|
}
|
||||||
|
if (key.usage.monthly && key.usage.monthly.outputTokens !== undefined) {
|
||||||
|
return key.usage.monthly.outputTokens
|
||||||
|
}
|
||||||
|
if (key.usage.total && key.usage.total.outputTokens !== undefined) {
|
||||||
|
return key.usage.total.outputTokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
} else if (globalDateFilter.preset === 'all') {
|
||||||
|
// 全部时间
|
||||||
|
if (key.usage && key.usage['all'] && key.usage['all'].outputTokens !== undefined) {
|
||||||
|
return key.usage['all'].outputTokens
|
||||||
|
}
|
||||||
|
return key.usage?.total?.outputTokens || 0
|
||||||
|
} else {
|
||||||
|
// 默认返回
|
||||||
|
return key.usage?.total?.outputTokens || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 计算日期范围内的总费用(用于展开的详细统计)
|
// 计算日期范围内的总费用(用于展开的详细统计)
|
||||||
const calculatePeriodCost = (key) => {
|
const calculatePeriodCost = (key) => {
|
||||||
// 如果没有展开,使用缓存的费用数据
|
// 如果没有展开,使用缓存的费用数据
|
||||||
@@ -3255,74 +3351,57 @@ const clearSearch = () => {
|
|||||||
// 导出数据到Excel
|
// 导出数据到Excel
|
||||||
const exportToExcel = () => {
|
const exportToExcel = () => {
|
||||||
try {
|
try {
|
||||||
// 准备导出的数据 - 仅导出用量数据
|
// 准备导出的数据 - 简化版本
|
||||||
const exportData = sortedApiKeys.value.map((key) => {
|
const exportData = sortedApiKeys.value.map((key) => {
|
||||||
|
// 获取当前时间段的数据
|
||||||
|
const periodRequests = getPeriodRequests(key)
|
||||||
|
const periodCost = calculatePeriodCost(key)
|
||||||
|
const periodTokens = getPeriodTokens(key)
|
||||||
|
const periodInputTokens = getPeriodInputTokens(key)
|
||||||
|
const periodOutputTokens = getPeriodOutputTokens(key)
|
||||||
|
|
||||||
// 基础数据
|
// 基础数据
|
||||||
const baseData = {
|
const baseData = {
|
||||||
'API Key名称': key.name || '',
|
名称: key.name || '',
|
||||||
所有者: key.ownerDisplayName || '',
|
请求总数: periodRequests,
|
||||||
|
'总费用($)': periodCost.toFixed(4),
|
||||||
// 当前筛选时间段的统计
|
Token数: formatTokenCount(periodTokens),
|
||||||
请求总数: getPeriodRequests(key),
|
输入Token: formatTokenCount(periodInputTokens),
|
||||||
'总费用($)': calculatePeriodCost(key).toFixed(4),
|
输出Token: formatTokenCount(periodOutputTokens),
|
||||||
总Token数: getPeriodTokens(key),
|
最后使用时间: key.lastUsedAt ? formatDate(key.lastUsedAt) : '从未使用'
|
||||||
|
|
||||||
// 今日统计
|
|
||||||
今日请求数: 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 = {}
|
const modelStats = {}
|
||||||
|
|
||||||
// 处理每日模型统计
|
// 根据当前时间筛选条件获取对应的模型统计
|
||||||
if (key.usage?.daily?.models) {
|
let modelsData = null
|
||||||
Object.entries(key.usage.daily.models).forEach(([model, stats]) => {
|
|
||||||
const modelName = model.replace(/[:/]/g, '_') // 处理模型名中的特殊字符
|
if (globalDateFilter.preset === 'today') {
|
||||||
modelStats[`今日_${modelName}_请求数`] = stats.requests || 0
|
modelsData = key.usage?.daily?.models
|
||||||
modelStats[`今日_${modelName}_费用($)`] = (stats.cost || 0).toFixed(4)
|
} else if (globalDateFilter.preset === '7days') {
|
||||||
modelStats[`今日_${modelName}_输入Token`] = stats.inputTokens || 0
|
modelsData = key.usage?.weekly?.models
|
||||||
modelStats[`今日_${modelName}_输出Token`] = stats.outputTokens || 0
|
} else if (globalDateFilter.preset === '30days') {
|
||||||
modelStats[`今日_${modelName}_总Token`] = stats.totalTokens || 0
|
modelsData = key.usage?.monthly?.models
|
||||||
})
|
} else if (globalDateFilter.preset === 'all') {
|
||||||
|
modelsData = key.usage?.total?.models
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理总计模型统计
|
// 处理模型统计
|
||||||
if (key.usage?.total?.models) {
|
if (modelsData) {
|
||||||
Object.entries(key.usage.total.models).forEach(([model, stats]) => {
|
Object.entries(modelsData).forEach(([model, stats]) => {
|
||||||
const modelName = model.replace(/[:/]/g, '_')
|
// 简化模型名称,去掉前缀
|
||||||
modelStats[`累计_${modelName}_请求数`] = stats.requests || 0
|
let modelName = model
|
||||||
modelStats[`累计_${modelName}_费用($)`] = (stats.cost || 0).toFixed(4)
|
if (model.includes(':')) {
|
||||||
modelStats[`累计_${modelName}_输入Token`] = stats.inputTokens || 0
|
modelName = model.split(':').pop() // 取最后一部分
|
||||||
modelStats[`累计_${modelName}_输出Token`] = stats.outputTokens || 0
|
|
||||||
modelStats[`累计_${modelName}_总Token`] = stats.totalTokens || 0
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
modelName = modelName.replace(/[:/]/g, '_')
|
||||||
|
|
||||||
// 处理筛选时间段的模型统计
|
modelStats[`${modelName}_请求数`] = stats.requests || 0
|
||||||
if (globalDateFilter.preset === '7days' && key.usage?.weekly?.models) {
|
modelStats[`${modelName}_费用($)`] = (stats.cost || 0).toFixed(4)
|
||||||
Object.entries(key.usage.weekly.models).forEach(([model, stats]) => {
|
modelStats[`${modelName}_Token`] = formatTokenCount(stats.totalTokens || 0)
|
||||||
const modelName = model.replace(/[:/]/g, '_')
|
modelStats[`${modelName}_输入Token`] = formatTokenCount(stats.inputTokens || 0)
|
||||||
modelStats[`本周_${modelName}_请求数`] = stats.requests || 0
|
modelStats[`${modelName}_输出Token`] = formatTokenCount(stats.outputTokens || 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
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3330,21 +3409,98 @@ const exportToExcel = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 创建工作簿
|
// 创建工作簿
|
||||||
|
const wb = XLSX.utils.book_new()
|
||||||
const ws = XLSX.utils.json_to_sheet(exportData)
|
const ws = XLSX.utils.json_to_sheet(exportData)
|
||||||
|
|
||||||
// 动态设置列宽
|
// 获取工作表范围
|
||||||
|
const range = XLSX.utils.decode_range(ws['!ref'])
|
||||||
|
|
||||||
|
// 设置列宽
|
||||||
const headers = Object.keys(exportData[0] || {})
|
const headers = Object.keys(exportData[0] || {})
|
||||||
const colWidths = headers.map((header) => {
|
const columnWidths = headers.map((header) => {
|
||||||
if (header.includes('名称') || header.includes('所有者')) return { wch: 20 }
|
if (header === '名称') return { wch: 25 }
|
||||||
|
if (header === '最后使用时间') return { wch: 20 }
|
||||||
if (header.includes('费用')) return { wch: 15 }
|
if (header.includes('费用')) return { wch: 15 }
|
||||||
if (header.includes('Token')) return { wch: 15 }
|
if (header.includes('Token')) return { wch: 15 }
|
||||||
if (header.includes('请求')) return { wch: 12 }
|
if (header.includes('请求')) return { wch: 12 }
|
||||||
return { wch: 15 }
|
return { wch: 15 }
|
||||||
})
|
})
|
||||||
ws['!cols'] = colWidths
|
ws['!cols'] = columnWidths
|
||||||
|
|
||||||
|
// 应用样式到标题行
|
||||||
|
for (let C = range.s.c; C <= range.e.c; ++C) {
|
||||||
|
const cellAddress = XLSX.utils.encode_cell({ r: 0, c: C })
|
||||||
|
if (!ws[cellAddress]) continue
|
||||||
|
|
||||||
|
const header = headers[C]
|
||||||
|
const isModelColumn = header && header.includes('_')
|
||||||
|
|
||||||
|
ws[cellAddress].s = {
|
||||||
|
fill: {
|
||||||
|
fgColor: { rgb: isModelColumn ? '70AD47' : '4472C4' }
|
||||||
|
},
|
||||||
|
font: {
|
||||||
|
color: { rgb: 'FFFFFF' },
|
||||||
|
bold: true,
|
||||||
|
sz: 12
|
||||||
|
},
|
||||||
|
alignment: {
|
||||||
|
horizontal: 'center',
|
||||||
|
vertical: 'center'
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
top: { style: 'thin', color: { rgb: '2F5597' } },
|
||||||
|
bottom: { style: 'thin', color: { rgb: '2F5597' } },
|
||||||
|
left: { style: 'thin', color: { rgb: '2F5597' } },
|
||||||
|
right: { style: 'thin', color: { rgb: '2F5597' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用样式到数据行
|
||||||
|
for (let R = 1; R <= range.e.r; ++R) {
|
||||||
|
for (let C = range.s.c; C <= range.e.c; ++C) {
|
||||||
|
const cellAddress = XLSX.utils.encode_cell({ r: R, c: C })
|
||||||
|
if (!ws[cellAddress]) continue
|
||||||
|
|
||||||
|
const header = headers[C]
|
||||||
|
const value = ws[cellAddress].v
|
||||||
|
|
||||||
|
// 基础样式
|
||||||
|
const cellStyle = {
|
||||||
|
font: { sz: 11 },
|
||||||
|
border: {
|
||||||
|
top: { style: 'thin', color: { rgb: 'D3D3D3' } },
|
||||||
|
bottom: { style: 'thin', color: { rgb: 'D3D3D3' } },
|
||||||
|
left: { style: 'thin', color: { rgb: 'D3D3D3' } },
|
||||||
|
right: { style: 'thin', color: { rgb: 'D3D3D3' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 偶数行背景色
|
||||||
|
if (R % 2 === 0) {
|
||||||
|
cellStyle.fill = { fgColor: { rgb: 'F2F2F2' } }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据列类型设置对齐和特殊样式
|
||||||
|
if (header === '名称') {
|
||||||
|
cellStyle.alignment = { horizontal: 'left', vertical: 'center' }
|
||||||
|
} else if (header === '最后使用时间') {
|
||||||
|
cellStyle.alignment = { horizontal: 'right', vertical: 'center' }
|
||||||
|
if (value === '从未使用') {
|
||||||
|
cellStyle.font = { ...cellStyle.font, color: { rgb: '999999' }, italic: true }
|
||||||
|
}
|
||||||
|
} else if (header && header.includes('费用')) {
|
||||||
|
cellStyle.alignment = { horizontal: 'right', vertical: 'center' }
|
||||||
|
cellStyle.font = { ...cellStyle.font, color: { rgb: '0066CC' }, bold: true }
|
||||||
|
} else if (header && (header.includes('Token') || header.includes('请求'))) {
|
||||||
|
cellStyle.alignment = { horizontal: 'right', vertical: 'center' }
|
||||||
|
}
|
||||||
|
|
||||||
|
ws[cellAddress].s = cellStyle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 创建工作簿
|
|
||||||
const wb = XLSX.utils.book_new()
|
|
||||||
XLSX.utils.book_append_sheet(wb, ws, '用量统计')
|
XLSX.utils.book_append_sheet(wb, ws, '用量统计')
|
||||||
|
|
||||||
// 生成文件名(包含时间戳和筛选条件)
|
// 生成文件名(包含时间戳和筛选条件)
|
||||||
|
|||||||
Reference in New Issue
Block a user