From 1d5bca773f9fa65b08fd409bd715ebcc90aac6fe Mon Sep 17 00:00:00 2001 From: Gsh <15170702455@163.com> Date: Sat, 24 Jan 2026 15:05:24 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=94=A8=E9=87=8F=E6=9F=A5=E7=9C=8B?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Yi.Ai.Vue3/pnpm-lock.yaml | 50 +- Yi.Ai.Vue3/src/api/model/index.ts | 33 + .../components/UsageStatistics.vue | 867 +++++++++++++++++- 3 files changed, 910 insertions(+), 40 deletions(-) diff --git a/Yi.Ai.Vue3/pnpm-lock.yaml b/Yi.Ai.Vue3/pnpm-lock.yaml index 72eff0bf..db99a03f 100644 --- a/Yi.Ai.Vue3/pnpm-lock.yaml +++ b/Yi.Ai.Vue3/pnpm-lock.yaml @@ -158,7 +158,7 @@ importers: devDependencies: '@antfu/eslint-config': specifier: ^4.16.2 - version: 4.17.0(@vue/compiler-sfc@3.5.17)(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.4)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0)) + version: 4.17.0(@vue/compiler-sfc@3.5.17)(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) '@changesets/cli': specifier: ^2.29.5 version: 2.29.5 @@ -188,7 +188,7 @@ importers: version: 8.6.14(react-dom@19.2.3(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.6.2)) '@storybook/experimental-addon-test': specifier: ^8.6.14 - version: 8.6.14(@vitest/browser@3.2.4(playwright@1.57.0)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0))(vitest@3.2.4))(@vitest/runner@3.2.4)(react-dom@19.2.3(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.6.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.4)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0)) + version: 8.6.14(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.3(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.6.2))(vitest@3.2.4) '@storybook/manager-api': specifier: ^8.6.14 version: 8.6.14(storybook@8.6.14(prettier@3.6.2)) @@ -227,7 +227,7 @@ importers: version: 3.2.4(playwright@1.57.0)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0))(vitest@3.2.4) '@vitest/coverage-v8': specifier: ^3.2.4 - version: 3.2.4(@vitest/browser@3.2.4(playwright@1.57.0)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0))(vitest@3.2.4))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.4)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0)) + version: 3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4) '@vue/tsconfig': specifier: ^0.7.0 version: 0.7.0(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3)) @@ -427,28 +427,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@ast-grep/napi-linux-arm64-musl@0.36.3': resolution: {integrity: sha512-2XRmNYuovZu0Pa4J3or4PKMkQZnXXfpVcCrPwWB/2ytX7XUo+TWLgYE8rPVnJOyw5zujkveFb0XUrro9mQgLzw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@ast-grep/napi-linux-x64-gnu@0.36.3': resolution: {integrity: sha512-mTwPRbBi1feGqR2b5TWC5gkEDeRi8wfk4euF5sKNihfMGHj6pdfINHQ3QvLVO4C7z0r/wgWLAvditFA0b997dg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@ast-grep/napi-linux-x64-musl@0.36.3': resolution: {integrity: sha512-tMGPrT+zuZzJK6n1cD1kOii7HYZE9gUXjwtVNE/uZqXEaWP6lmkfoTMbLjnxEe74VQbmaoDGh1/cjrDBnqC6Uw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@ast-grep/napi-win32-arm64-msvc@0.36.3': resolution: {integrity: sha512-7pFyr9+dyV+4cBJJ1I57gg6PDXP3GBQeVAsEEitzEruxx4Hb4cyNro54gGtlsS+6ty+N0t004tPQxYO2VrsPIg==} @@ -1245,35 +1241,30 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@napi-rs/canvas-linux-arm64-musl@0.1.84': resolution: {integrity: sha512-VyZq0EEw+OILnWk7G3ZgLLPaz1ERaPP++jLjeyLMbFOF+Tr4zHzWKiKDsEV/cT7btLPZbVoR3VX+T9/QubnURQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@napi-rs/canvas-linux-riscv64-gnu@0.1.84': resolution: {integrity: sha512-PSMTh8DiThvLRsbtc/a065I/ceZk17EXAATv9uNvHgkgo7wdEfTh2C3aveNkBMGByVO3tvnvD5v/YFtZL07cIg==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - libc: [glibc] '@napi-rs/canvas-linux-x64-gnu@0.1.84': resolution: {integrity: sha512-N1GY3noO1oqgEo3rYQIwY44kfM11vA0lDbN0orTOHfCSUZTUyiYCY0nZ197QMahZBm1aR/vYgsWpV74MMMDuNA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@napi-rs/canvas-linux-x64-musl@0.1.84': resolution: {integrity: sha512-vUZmua6ADqTWyHyei81aXIt9wp0yjeNwTH0KdhdeoBb6azHmFR8uKTukZMXfLCC3bnsW0t4lW7K78KNMknmtjg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@napi-rs/canvas-win32-x64-msvc@0.1.84': resolution: {integrity: sha512-YSs8ncurc1xzegUMNnQUTYrdrAuaXdPMOa+iYYyAxydOtg0ppV386hyYMsy00Yip1NlTgLCseRG4sHSnjQx6og==} @@ -1330,42 +1321,36 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [musl] '@parcel/watcher-win32-arm64@2.5.1': resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} @@ -1450,67 +1435,56 @@ packages: resolution: {integrity: sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.41.1': resolution: {integrity: sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.41.1': resolution: {integrity: sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.41.1': resolution: {integrity: sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.41.1': resolution: {integrity: sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.41.1': resolution: {integrity: sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.41.1': resolution: {integrity: sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.41.1': resolution: {integrity: sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.41.1': resolution: {integrity: sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.41.1': resolution: {integrity: sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.41.1': resolution: {integrity: sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.41.1': resolution: {integrity: sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==} @@ -6913,8 +6887,8 @@ packages: vue-component-type-helpers@2.2.12: resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==} - vue-component-type-helpers@3.1.8: - resolution: {integrity: sha512-oaowlmEM6BaYY+8o+9D9cuzxpWQWHqHTMKakMxXu0E+UCIOMTljyIPO15jcnaCwJtZu/zWDotK7mOIHvWD9mcw==} + vue-component-type-helpers@3.2.3: + resolution: {integrity: sha512-lpJTa8a+12Cgy/n5OdlQTzQhSWOCu+6zQoNFbl3KYxwAoB95mYIgMLKEYMvQykPJ2ucBDjJJISdIBHc1d9Hd3w==} vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} @@ -7133,7 +7107,7 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 - '@antfu/eslint-config@4.17.0(@vue/compiler-sfc@3.5.17)(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.4)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0))': + '@antfu/eslint-config@4.17.0(@vue/compiler-sfc@3.5.17)(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4)': dependencies: '@antfu/install-pkg': 1.1.0 '@clack/prompts': 0.11.0 @@ -7142,7 +7116,7 @@ snapshots: '@stylistic/eslint-plugin': 5.2.0(eslint@9.31.0(jiti@2.4.2)) '@typescript-eslint/eslint-plugin': 8.37.0(@typescript-eslint/parser@8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/parser': 8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) - '@vitest/eslint-plugin': 1.3.4(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.4)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0)) + '@vitest/eslint-plugin': 1.3.4(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4) ansis: 4.1.0 cac: 6.7.14 eslint: 9.31.0(jiti@2.4.2) @@ -8494,7 +8468,7 @@ snapshots: dependencies: type-fest: 2.19.0 - '@storybook/experimental-addon-test@8.6.14(@vitest/browser@3.2.4(playwright@1.57.0)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0))(vitest@3.2.4))(@vitest/runner@3.2.4)(react-dom@19.2.3(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.6.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.4)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0))': + '@storybook/experimental-addon-test@8.6.14(@vitest/browser@3.2.4)(@vitest/runner@3.2.4)(react-dom@19.2.3(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.6.2))(vitest@3.2.4)': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 1.6.0(react-dom@19.2.3(react@19.1.0))(react@19.1.0) @@ -8639,7 +8613,7 @@ snapshots: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.5.17(typescript@5.8.3) - vue-component-type-helpers: 3.1.8 + vue-component-type-helpers: 3.2.3 '@stylistic/eslint-plugin@5.2.0(eslint@9.31.0(jiti@2.4.2))': dependencies: @@ -9301,7 +9275,7 @@ snapshots: - utf-8-validate - vite - '@vitest/coverage-v8@3.2.4(@vitest/browser@3.2.4(playwright@1.57.0)(vite@6.3.5(@types/node@22.15.30)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0))(vitest@3.2.4))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.4)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0))': + '@vitest/coverage-v8@3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -9322,7 +9296,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/eslint-plugin@1.3.4(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.15.30)(@vitest/browser@3.2.4)(jiti@2.4.2)(sass-embedded@1.89.2)(sass@1.97.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.0))': + '@vitest/eslint-plugin@1.3.4(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4)': dependencies: '@typescript-eslint/utils': 8.33.1(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.31.0(jiti@2.4.2) @@ -14721,7 +14695,7 @@ snapshots: vue-component-type-helpers@2.2.12: {} - vue-component-type-helpers@3.1.8: {} + vue-component-type-helpers@3.2.3: {} vue-demi@0.14.10(vue@3.5.17(typescript@5.8.3)): dependencies: diff --git a/Yi.Ai.Vue3/src/api/model/index.ts b/Yi.Ai.Vue3/src/api/model/index.ts index 10055c14..453de779 100644 --- a/Yi.Ai.Vue3/src/api/model/index.ts +++ b/Yi.Ai.Vue3/src/api/model/index.ts @@ -211,3 +211,36 @@ export function getPremiumPackageTokenUsage() { "percentage": 0 } ] */ + +// 获取当前用户近24小时每小时Token消耗统计 +export function getLast24HoursTokenUsage() { + return get('/usage-statistics/last24Hours-token-usage').json(); +} +/* 返回数据 + [ + { + "hour": "2026-01-23T13:32:49.237Z", + "totalTokens": 0, + "modelBreakdown": [ + { + "modelId": "string", + "tokens": 0 + } + ] + } + ] +*/ + +// 获取当前用户今日各模型使用量统计 +export function getTodayModelUsage() { + return get('/usage-statistics/today-model-usage').json(); +} +/* 返回数据 + [ + { + "modelId": "string", + "usageCount": 0, + "totalTokens": 0 + } + ] +*/ diff --git a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/UsageStatistics.vue b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/UsageStatistics.vue index 7dad9735..a20af923 100644 --- a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/UsageStatistics.vue +++ b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/UsageStatistics.vue @@ -3,6 +3,7 @@ import { FullScreen, PieChart } from '@element-plus/icons-vue'; import { useElementSize } from '@vueuse/core'; import { BarChart, PieChart as EPieChart, LineChart } from 'echarts/charts'; import { + BrushComponent, GraphicComponent, GridComponent, LegendComponent, @@ -11,7 +12,7 @@ import { } from 'echarts/components'; import * as echarts from 'echarts/core'; import { CanvasRenderer } from 'echarts/renderers'; -import { getLast7DaysTokenUsage, getModelTokenUsage, getSelectableTokenInfo } from '@/api'; +import { getLast7DaysTokenUsage, getModelTokenUsage, getSelectableTokenInfo, getLast24HoursTokenUsage, getTodayModelUsage } from '@/api'; // 注册必要的组件 echarts.use([ @@ -23,6 +24,7 @@ echarts.use([ GridComponent, LegendComponent, GraphicComponent, + BrushComponent, CanvasRenderer, ]); @@ -30,9 +32,11 @@ echarts.use([ const lineChart = ref(null); const pieChart = ref(null); const barChart = ref(null); +const hourlyBarChart = ref(null); // 新增:近24小时每小时Token消耗柱状图 let lineChartInstance: any = null; let pieChartInstance: any = null; let barChartInstance: any = null; +let hourlyBarChartInstance: any = null; // 新增:近24小时柱状图实例 // 全屏状态 const isFullscreen = ref(false); @@ -47,6 +51,8 @@ const loading = ref(false); const totalTokens = ref(0); const usageData = ref([]); const modelUsageData = ref([]); +const hourlyUsageData = ref([]); // 新增:近24小时每小时Token消耗数据 +const todayModelUsageData = ref([]); // 新增:今日各模型使用量数据 // Token选择相关 const selectedTokenId = ref(''); // 空字符串表示查询全部 @@ -64,6 +70,74 @@ const selectedTokenName = computed(() => { return token?.name || '未知API密钥'; }); +// 计算属性:获取近24小时数据中所有唯一的模型ID(按总token使用量排序,只统计有数据的模型) +const hourlyModels = computed(() => { + const modelTokenMap = new Map(); + hourlyUsageData.value.forEach((hour) => { + hour.modelBreakdown?.forEach((model: any) => { + // 只统计有实际使用量的模型 + if (model.tokens > 0) { + const existing = modelTokenMap.get(model.modelId); + modelTokenMap.set(model.modelId, { + tokens: (existing?.tokens || 0) + model.tokens, + iconUrl: model.iconUrl || '', + }); + } + }); + }); + return Array.from(modelTokenMap.entries()) + .sort((a, b) => b[1].tokens - a[1].tokens) + .map(([modelId, data]) => ({ modelId, iconUrl: data.iconUrl })); +}); + +// 计算属性:模型颜色映射(保持一致性) +const modelColors = computed(() => { + const baseColors = [ + '#3a4de9', '#6a5acd', '#9370db', '#8a2be2', '#9932cc', + '#ba55d3', '#da70d6', '#ee82ee', '#dda0dd', '#ff00ff', + '#667eea', '#764ba2', '#f093fb', '#f5576c', '#4facfe', + '#00f2fe', '#43e97b', '#38f9d7', '#fa709a', '#fee140', + '#4361ee', '#3a0ca3', '#7209b7', '#f72585', '#4cc9f0', + ]; + const colorMap: Record = {}; + + // 先为近24小时数据的模型分配颜色 + hourlyModels.value.forEach(({ modelId }, index) => { + if (!colorMap[modelId]) { + colorMap[modelId] = baseColors[index % baseColors.length]; + } + }); + + // 为今日模型数据中没有颜色的模型分配颜色 + let colorIndex = hourlyModels.value.length; + todayModelUsageData.value.forEach((item: any) => { + if (!colorMap[item.modelId]) { + colorMap[item.modelId] = baseColors[colorIndex % baseColors.length]; + colorIndex++; + } + }); + + return colorMap; +}); + +// 计算属性:模型图标URL映射 +const modelIconUrls = computed(() => { + const iconMap: Record = {}; + hourlyModels.value.forEach(({ modelId, iconUrl }) => { + iconMap[modelId] = iconUrl; + }); + return iconMap; +}); + +// 计算属性:今日模型数据的图标映射 +const todayModelIcons = computed(() => { + const iconMap: Record = {}; + todayModelUsageData.value.forEach((item: any) => { + iconMap[item.modelId] = item.iconUrl || ''; + }); + return iconMap; +}); + // 获取可选择的Token列表 async function fetchTokenOptions() { try { @@ -93,13 +167,17 @@ async function fetchUsageData() { loading.value = true; try { const tokenId = selectedTokenId.value || undefined; - const [res, res2] = await Promise.all([ + const [res, res2, res3, res4] = await Promise.all([ getLast7DaysTokenUsage(tokenId), getModelTokenUsage(tokenId), + getLast24HoursTokenUsage(), + getTodayModelUsage(), ]); usageData.value = res.data || []; modelUsageData.value = res2.data || []; + hourlyUsageData.value = res3.data || []; + todayModelUsageData.value = res4.data || []; totalTokens.value = usageData.value.reduce((sum, item) => sum + item.tokens, 0); updateCharts(); @@ -124,6 +202,9 @@ function initCharts() { if (barChart.value) { barChartInstance = echarts.init(barChart.value); } + if (hourlyBarChart.value) { + hourlyBarChartInstance = echarts.init(hourlyBarChart.value); + } window.addEventListener('resize', resizeCharts); } @@ -133,6 +214,7 @@ function updateCharts() { updateLineChart(); updatePieChart(); updateBarChart(); + updateHourlyBarChart(); // 新增:更新近24小时柱状图 } // 更新折线图 @@ -512,11 +594,473 @@ function updateBarChart() { barChartInstance.setOption(option, true); } +// 更新近24小时每小时Token消耗柱状图 +function updateHourlyBarChart() { + if (!hourlyBarChartInstance) + return; + + // 空数据状态 + if (hourlyUsageData.value.length === 0 || hourlyModels.value.length === 0) { + const emptyOption = { + graphic: [ + { + type: 'group', + left: 'center', + top: 'center', + children: [ + { + type: 'rect', + shape: { + width: 160, + height: 160, + r: 12, + }, + style: { + fill: '#f5f7fa', + stroke: '#e9ecef', + lineWidth: 2, + }, + left: -80, + top: -80, + }, + { + type: 'text', + style: { + text: '📊', + fontSize: 48, + x: -24, + y: -40, + }, + }, + { + type: 'text', + style: { + text: '暂无数据', + fontSize: 18, + fontWeight: 'bold', + fill: '#909399', + x: -36, + y: 20, + }, + }, + { + type: 'text', + style: { + text: '近24小时暂无使用记录', + fontSize: 14, + fill: '#c0c4cc', + x: -80, + y: 50, + }, + }, + ], + }, + ], + }; + hourlyBarChartInstance.setOption(emptyOption, true); + return; + } + + const hours = hourlyUsageData.value.map(item => { + const date = new Date(item.hour); + return `${date.getHours().toString().padStart(2, '0')}:00`; + }); + const totalTokens = hourlyUsageData.value.map(item => item.totalTokens); + + const isMobile = window.innerWidth < 768; + + // 找出24小时中单小时模型数量最多的值 + const maxModelsPerHour = hourlyUsageData.value.reduce((max, hour) => { + const count = hour.modelBreakdown?.length || 0; + return Math.max(max, count); + }, 0); + + // 智能计算柱子宽度:基于最大模型数量动态计算 + // 柱子宽度 = 单元格宽度 / 最大模型数量 - 间距占比 + let barWidth: number; + let barGap: number | string; + let barCategoryGap: number | string; + + // 估算每个时间段的可用宽度(像素) + // 假设图表容器宽度,移动端约320-375px,桌面端约1000-1400px + const containerWidth = isMobile ? 350 : 1200; + const hourCount = hourlyUsageData.value.length; + const categoryWidth = containerWidth / hourCount; // 每个小时类别的宽度 + + // 柱子间距配置(像素) + const gapBetweenBars = isMobile ? 3 : 6; // 柱子之间的间距 + const gapBetweenCategories = isMobile ? 8 : 15; // 类别之间的间距 + + // 计算柱子宽度:可用宽度除以最大模型数 + // 可用宽度 = 类别宽度 - 类别间距 - (柱子数-1)*柱子间距 + const availableWidth = categoryWidth - gapBetweenCategories - ((maxModelsPerHour - 1) * gapBetweenBars); + barWidth = Math.max(4, Math.floor(availableWidth / maxModelsPerHour)); // 最小4px + + // 将间距转换为百分比以便ECharts自适应 + barGap = `${(gapBetweenBars / barWidth) * 100}%`; + barCategoryGap = `${(gapBetweenCategories / categoryWidth) * 100}%`; + + // 限制最大柱子宽度,避免太少模型时柱子过粗 + const maxBarWidth = isMobile ? 25 : 60; + barWidth = Math.min(barWidth, maxBarWidth); + + // 构建每个模型的数据系列(并排柱状图) + const series = hourlyModels.value.map(({ modelId }, index) => { + const data = hourlyUsageData.value.map(hour => { + const modelData = hour.modelBreakdown?.find((m: any) => m.modelId === modelId); + return modelData?.tokens || 0; + }); + + // 根据最大模型数量决定是否显示标签 + const showLabel = maxModelsPerHour <= 4 && !isMobile; + + return { + name: modelId, + type: 'bar', + barWidth, + barGap, + barCategoryGap, + data, + itemStyle: { + color: modelColors.value[modelId], + borderRadius: [4, 4, 0, 0], + borderWidth: 0, + }, + label: { + show: showLabel, + position: 'top', + formatter: (params: any) => { + if (params.value === 0) + return ''; + return params.value >= 1000 ? `${(params.value / 1000).toFixed(1)}k` : params.value.toString(); + }, + fontSize: isMobile ? 9 : 11, + color: '#666', + }, + emphasis: { + focus: 'series', + itemStyle: { + shadowBlur: 8, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.2)', + }, + }, + // 确保系列按总使用量排序的顺序 + seriesIndex: index, + }; + }); + + const option = { + graphic: [], + calculable: true, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow', + label: { + show: true, + }, + }, + formatter: (params: any) => { + if (!params || params.length === 0) + return ''; + const hour = params[0].axisValue; + + // 过滤出有数据的模型并按token使用量降序 + const activeParams = params + .filter((p: any) => p.value > 0) + .sort((a: any, b: any) => b.value - a.value); + + if (activeParams.length === 0) + return `
${hour}
暂无数据
`; + + // 计算总token + const totalTokens = activeParams.reduce((sum: number, p: any) => sum + p.value, 0); + + let result = `
+
${hour}
+
总计: ${totalTokens.toLocaleString()} tokens
+
`; + + // 限制显示的模型数量(最多显示前8个) + const maxDisplay = 8; + const displayParams = activeParams.slice(0, maxDisplay); + + displayParams.forEach((param: any, index: number) => { + const percent = ((param.value / totalTokens) * 100).toFixed(1); + result += `
+ + + ${param.seriesName} + + + ${param.value.toLocaleString()} + ${percent}% + +
`; + }); + + // 如果有更多模型,显示提示 + if (activeParams.length > maxDisplay) { + result += `
+ 还有 ${activeParams.length - maxDisplay} 个模型未显示 +
`; + } + + return result; + }, + confine: true, + backgroundColor: 'rgba(255, 255, 255, 0.98)', + borderColor: '#e9ecef', + borderWidth: 1, + borderRadius: 8, + padding: [12, 16], + textStyle: { + fontSize: 13, + color: '#333', + }, + position(point: any, params: any, dom: any, rect: any, size: any) { + if (isMobile) { + return ['50%', '10%']; + } + return null; + }, + }, + toolbox: { + show: !isMobile, + feature: { + dataZoom: { + yAxisIndex: 'none', + title: { + zoom: '区域缩放', + back: '还原缩放', + }, + }, + brush: { + type: ['lineX', 'clear'], + title: { + lineX: '横向选择', + clear: '清除选择', + }, + }, + restore: { + show: true, + title: '还原', + }, + saveAsImage: { + show: true, + title: '保存为图片', + pixelRatio: 2, + }, + }, + right: 15, + top: 5, + itemSize: 14, + iconStyle: { + borderColor: '#667eea', + borderWidth: 1.5, + }, + emphasis: { + iconStyle: { + borderColor: '#764ba2', + }, + }, + tooltip: { + show: true, + position: 'bottom', + formatter: (param: any) => { + return param.title; + }, + textStyle: { + fontSize: 11, + }, + }, + }, + // 区域选框缩放配置 + brush: { + id: 'brush', + xAxisIndex: 0, + link: ['x'], + transform: { + type: 'bar', + }, + throttleType: 'debounce', + throttleDelay: 300, + removeOnClick: true, + brushLink: 'all', + brushType: false, + inBrush: { + opacity: 1, + }, + outOfBrush: { + opacity: 0.3, + }, + }, + legend: { + show: true, + type: hourlyModels.value.length > 8 || isMobile ? 'scroll' : 'plain', + top: isMobile ? '0%' : '3%', + left: 'center', + itemWidth: 14, + itemHeight: 10, + itemGap: isMobile ? 8 : 10, + textStyle: { + fontSize: isMobile ? 10 : 12, + color: '#666', + }, + data: hourlyModels.value.map(({ modelId }) => ({ + name: modelId, + icon: 'rect', + })), + pageTextStyle: { + color: '#999', + }, + pageIconColor: '#667eea', + pageIconInactiveColor: '#ccc', + pageButtonItemGap: 5, + }, + grid: { + top: maxModelsPerHour > 8 ? '15%' : '12%', + left: '1%', + right: isMobile ? '8%' : '10%', + bottom: isMobile ? '15%' : '12%', + containLabel: true, + }, + dataZoom: [ + { + show: !isMobile, + start: hourlyUsageData.value.length > 12 ? 100 - Math.round((12 / hourlyUsageData.value.length) * 100) : 80, + end: 100, + xAxisIndex: [0], + bottom: '3%', + height: 18, + borderColor: 'transparent', + fillerColor: 'rgba(102, 126, 234, 0.2)', + handleStyle: { + color: '#667eea', + }, + textStyle: { + color: '#999', + fontSize: 11, + }, + }, + { + type: 'inside', + start: hourlyUsageData.value.length > 12 ? 100 - Math.round((12 / hourlyUsageData.value.length) * 100) : 80, + end: 100, + xAxisIndex: [0], + zoomOnMouseWheel: true, + moveOnMouseMove: true, + moveOnMouseWheel: false, + }, + { + show: !isMobile && maxModelsPerHour > 3, + yAxisIndex: [0], + filterMode: 'empty', + width: 28, + height: '70%', + showDataShadow: false, + left: '96%', + borderColor: 'transparent', + fillerColor: 'rgba(102, 126, 234, 0.15)', + handleStyle: { + color: '#667eea', + }, + }, + ], + // 区域选框缩放配置(配合calculable使用) + brush: { + id: 'brush', + xAxisIndex: 0, + link: ['x'], + throttleType: 'debounce', + throttleDelay: 300, + removeOnClick: true, + brushLink: 'all', + brushType: false, + inBrush: { + opacity: 1, + }, + outOfBrush: { + opacity: 0.15, + }, + // 选框样式 + brushStyle: { + borderWidth: 1, + color: 'rgba(102, 126, 234, 0.15)', + borderColor: '#667eea', + }, + }, + xAxis: { + type: 'category', + data: hours, + axisLine: { + lineStyle: { + color: '#e9ecef', + width: 1, + }, + }, + axisTick: { + alignWithLabel: true, + length: 4, + }, + axisLabel: { + interval: isMobile ? 3 : 0, + rotate: isMobile ? 45 : 0, + fontSize: isMobile ? 10 : 12, + color: '#666', + fontFamily: 'monospace', + }, + }, + yAxis: { + type: 'value', + name: 'Token用量', + nameTextStyle: { + fontSize: isMobile ? 11 : 12, + color: '#999', + padding: [0, 0, 0, 0], + }, + axisLine: { + show: true, + lineStyle: { + color: '#e9ecef', + width: 1, + }, + }, + axisTick: { + show: false, + }, + axisLabel: { + fontSize: isMobile ? 10 : 12, + color: '#666', + formatter: (value: number) => { + if (value >= 1000000) + return `${(value / 1000000).toFixed(1)}M`; + if (value >= 1000) + return `${(value / 1000).toFixed(0)}K`; + return value.toString(); + }, + }, + splitLine: { + lineStyle: { + color: '#f0f0f0', + type: 'dashed', + width: 1, + }, + }, + }, + series, + }; + + hourlyBarChartInstance.setOption(option, true); +} + // 调整图表大小 function resizeCharts() { lineChartInstance?.resize(); pieChartInstance?.resize(); barChartInstance?.resize(); + hourlyBarChartInstance?.resize(); } // 切换全屏 @@ -544,6 +1088,7 @@ onBeforeUnmount(() => { lineChartInstance?.dispose(); pieChartInstance?.dispose(); barChartInstance?.dispose(); + hourlyBarChartInstance?.dispose(); }); @@ -605,6 +1150,84 @@ onBeforeUnmount(() => { + + + +
+
+
+ + + + + +
+
+
📊
+
暂无数据
+
今日暂无模型使用记录
+
+
+
+
+
+ +
+ {{ item.modelId.charAt(0).toUpperCase() }} +
+
+
+
{{ item.modelId }}
+
+ + 使用 {{ item.usageCount }} 次 +
+
+
+ +
+
+ +