fix: 增加对话token显示,token消耗统计

This commit is contained in:
Gsh
2025-08-10 00:56:44 +08:00
parent a9c3a1bcec
commit 3eb27c3d35
10 changed files with 545 additions and 37 deletions

View File

@@ -17,7 +17,7 @@ interface Props {
const props = withDefaults(defineProps<Props>(), {
title: '弹窗标题',
width: '800px',
width: '1000px',
defaultActive: '',
});
@@ -55,6 +55,7 @@ function handleConfirm() {
<template>
<el-dialog
v-model="visible"
:title="title"
:width="width"

View File

@@ -0,0 +1,426 @@
<script lang="ts" setup>
import { PieChart } from '@element-plus/icons-vue';
import { BarChart, PieChart as EPieChart, LineChart } from 'echarts/charts';
import {
GraphicComponent,
GridComponent,
LegendComponent,
TitleComponent,
TooltipComponent,
} from 'echarts/components';
// 按需引入 ECharts
import * as echarts from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
import { getLast7DaysTokenUsage, getModelTokenUsage } from '@/api';
// 注册必要的组件
echarts.use([
LineChart,
EPieChart,
BarChart,
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
GraphicComponent,
CanvasRenderer,
]);
// 图表引用
const lineChart = ref(null);
const pieChart = ref(null);
const barChart = ref(null);
let lineChartInstance: any = null;
let pieChartInstance: any = null;
let barChartInstance: any = null;
// 数据
const dateRange = ref([
new Date(new Date().setDate(new Date().getDate() - 30)),
new Date(),
]);
const loading = ref(false);
const totalTokens = ref(0);
const usageData = ref<any[]>([]);
const modelUsageData = ref<any[]>([]);
// 日期选择器选项
const pickerOptions = {
disabledDate(time: Date) {
return time > new Date();
},
shortcuts: [{
text: '最近一周',
onClick(picker: any) {
const end = new Date();
const start = new Date();
start.setDate(start.getDate() - 7);
picker.$emit('pick', [start, end]);
},
}, {
text: '最近一个月',
onClick(picker: any) {
const end = new Date();
const start = new Date();
start.setMonth(start.getMonth() - 1);
picker.$emit('pick', [start, end]);
},
}, {
text: '最近三个月',
onClick(picker: any) {
const end = new Date();
const start = new Date();
start.setMonth(start.getMonth() - 3);
picker.$emit('pick', [start, end]);
},
}],
};
// 获取用量数据
async function fetchUsageData() {
loading.value = true;
try {
const [res, res2] = await Promise.all([
getLast7DaysTokenUsage(),
getModelTokenUsage(),
]);
usageData.value = res.data || [];
modelUsageData.value = res2.data || [];
totalTokens.value = usageData.value.reduce((sum, item) => sum + item.tokens, 0);
updateCharts();
}
catch (error) {
console.error('获取用量数据失败:', error);
ElMessage.error('获取用量数据失败');
}
finally {
loading.value = false;
}
}
// 初始化图表
function initCharts() {
if (lineChart.value) {
lineChartInstance = echarts.init(lineChart.value);
}
if (pieChart.value) {
pieChartInstance = echarts.init(pieChart.value);
}
if (barChart.value) {
barChartInstance = echarts.init(barChart.value);
}
window.addEventListener('resize', resizeCharts);
}
// 更新图表数据
function updateCharts() {
updateLineChart();
updatePieChart();
updateBarChart();
}
// 更新折线图
function updateLineChart() {
if (!lineChartInstance)
return;
const dates = usageData.value.map((item) => {
const date = new Date(item.date);
return `${date.getMonth() + 1}${date.getDate()}`;
});
const tokens = usageData.value.map(item => item.tokens);
const option = {
tooltip: {
trigger: 'axis',
formatter: '{b}<br/>用量: {c} ',
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
data: dates,
axisLabel: {
rotate: 45,
},
},
yAxis: {
type: 'value',
name: 'Token用量',
axisLine: {
show: true,
},
},
series: [{
data: tokens,
type: 'line',
smooth: true,
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(58, 77, 233, 0.8)' },
{ offset: 1, color: 'rgba(58, 77, 233, 0.1)' },
]),
},
itemStyle: {
color: '#3a4de9',
},
lineStyle: {
width: 3,
},
symbolSize: 8,
}],
};
lineChartInstance.setOption(option);
}
// 更新饼图
function updatePieChart() {
if (!pieChartInstance || modelUsageData.value.length === 0)
return;
const data = modelUsageData.value.map(item => ({
name: item.model,
value: item.tokens,
}));
const option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} tokens ({d}%)',
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
data: data.map(item => item.name),
},
series: [
{
name: '模型用量',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2,
},
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data,
},
],
};
pieChartInstance.setOption(option);
}
// 更新柱状图
function updateBarChart() {
if (!barChartInstance || modelUsageData.value.length === 0)
return;
const models = modelUsageData.value.map(item => item.model);
const tokens = modelUsageData.value.map(item => item.tokens);
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
formatter: '{b}<br/>用量: {c} tokens',
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'value',
name: 'Token用量',
},
yAxis: {
type: 'category',
data: models,
axisLabel: {
interval: 0,
},
},
series: [
{
name: '用量',
type: 'bar',
data: tokens,
itemStyle: {
color(params: any) {
const colorList = [
'#3a4de9',
'#6a5acd',
'#9370db',
'#8a2be2',
'#9932cc',
'#ba55d3',
'#da70d6',
'#ee82ee',
'#dda0dd',
'#ff00ff',
];
return colorList[params.dataIndex % colorList.length];
},
borderRadius: [0, 4, 4, 0],
},
label: {
show: true,
position: 'right',
formatter: '{c} ',
},
},
],
};
barChartInstance.setOption(option);
}
// 调整图表大小
function resizeCharts() {
lineChartInstance?.resize();
pieChartInstance?.resize();
barChartInstance?.resize();
}
onMounted(() => {
initCharts();
fetchUsageData();
});
onBeforeUnmount(() => {
window.removeEventListener('resize', resizeCharts);
lineChartInstance?.dispose();
pieChartInstance?.dispose();
barChartInstance?.dispose();
});
</script>
<template>
<div class="usage-statistics">
<div class="header">
<h2>
<el-icon><PieChart /></el-icon>
Token用量统计
</h2>
</div>
<el-card v-loading="loading" class="chart-card">
<template #header>
<div class="card-header">
<span>近七天每日Token消耗量</span>
<el-tag type="primary">
总计: {{ totalTokens }} tokens
</el-tag>
</div>
</template>
<div class="chart-container">
<div ref="lineChart" class="chart" style="height: 400px;" />
</div>
</el-card>
<el-card v-loading="loading" class="chart-card">
<template #header>
<div class="card-header">
<span>各模型Token消耗占比</span>
</div>
</template>
<div class="chart-container">
<div ref="pieChart" class="chart" style="height: 400px;" />
</div>
</el-card>
<el-card v-loading="loading" class="chart-card">
<template #header>
<div class="card-header">
<span>各模型Token消耗量</span>
</div>
</template>
<div class="chart-container">
<div ref="barChart" class="chart" style="height: 400px;" />
</div>
</el-card>
</div>
</template>
<style scoped>
/* 样式保持不变 */
.usage-statistics {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header h2 {
display: flex;
align-items: center;
margin: 0;
font-size: 20px;
color: #333;
}
.header .el-icon {
margin-right: 8px;
color: #3a4de9;
}
.chart-card {
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.chart-container {
width: 100%;
}
.chart {
width: 100%;
}
@media (max-width: 768px) {
.header {
flex-direction: column;
align-items: flex-start;
}
}
</style>