fix: 优化token图表,增加全屏显示
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { PieChart } from '@element-plus/icons-vue';
|
import { FullScreen, PieChart } from '@element-plus/icons-vue';
|
||||||
|
import { useElementSize } from '@vueuse/core';
|
||||||
import { BarChart, PieChart as EPieChart, LineChart } from 'echarts/charts';
|
import { BarChart, PieChart as EPieChart, LineChart } from 'echarts/charts';
|
||||||
import {
|
import {
|
||||||
GraphicComponent,
|
GraphicComponent,
|
||||||
@@ -8,7 +9,6 @@ import {
|
|||||||
TitleComponent,
|
TitleComponent,
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
} from 'echarts/components';
|
} from 'echarts/components';
|
||||||
// 按需引入 ECharts
|
|
||||||
import * as echarts from 'echarts/core';
|
import * as echarts from 'echarts/core';
|
||||||
import { CanvasRenderer } from 'echarts/renderers';
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
import { getLast7DaysTokenUsage, getModelTokenUsage } from '@/api';
|
import { getLast7DaysTokenUsage, getModelTokenUsage } from '@/api';
|
||||||
@@ -34,48 +34,20 @@ let lineChartInstance: any = null;
|
|||||||
let pieChartInstance: any = null;
|
let pieChartInstance: any = null;
|
||||||
let barChartInstance: any = null;
|
let barChartInstance: any = null;
|
||||||
|
|
||||||
|
// 全屏状态
|
||||||
|
const isFullscreen = ref(false);
|
||||||
|
const usageStatisticsRef = ref<HTMLElement>();
|
||||||
|
|
||||||
|
// 容器尺寸监听
|
||||||
|
const pieContainerSize = useElementSize(pieChart);
|
||||||
|
const barContainerSize = useElementSize(barChart);
|
||||||
|
|
||||||
// 数据
|
// 数据
|
||||||
const dateRange = ref([
|
|
||||||
new Date(new Date().setDate(new Date().getDate() - 30)),
|
|
||||||
new Date(),
|
|
||||||
]);
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const totalTokens = ref(0);
|
const totalTokens = ref(0);
|
||||||
const usageData = ref<any[]>([]);
|
const usageData = ref<any[]>([]);
|
||||||
const modelUsageData = 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() {
|
async function fetchUsageData() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@@ -149,7 +121,7 @@ function updateLineChart() {
|
|||||||
type: 'category',
|
type: 'category',
|
||||||
data: dates,
|
data: dates,
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
rotate: 45,
|
// rotate: 45,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
@@ -182,7 +154,7 @@ function updateLineChart() {
|
|||||||
lineChartInstance.setOption(option);
|
lineChartInstance.setOption(option);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新饼图
|
// 更新饼图 - 动态适应数据量
|
||||||
function updatePieChart() {
|
function updatePieChart() {
|
||||||
if (!pieChartInstance || modelUsageData.value.length === 0)
|
if (!pieChartInstance || modelUsageData.value.length === 0)
|
||||||
return;
|
return;
|
||||||
@@ -192,15 +164,31 @@ function updatePieChart() {
|
|||||||
value: item.tokens,
|
value: item.tokens,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const isManyItems = data.length > 8;
|
||||||
|
const isSmallContainer = pieContainerSize.width.value < 600;
|
||||||
|
|
||||||
const option = {
|
const option = {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'item',
|
trigger: 'item',
|
||||||
formatter: '{a} <br/>{b}: {c} tokens ({d}%)',
|
formatter: '{a} <br/>{b}: {c} tokens ({d}%)',
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
orient: 'vertical',
|
orient: isManyItems ? 'vertical' : 'horizontal',
|
||||||
right: 10,
|
right: isManyItems ? 10 : 'auto',
|
||||||
top: 'center',
|
bottom: isManyItems ? 0 : 10,
|
||||||
|
type: isManyItems ? 'scroll' : 'plain',
|
||||||
|
pageIconColor: '#3a4de9',
|
||||||
|
pageIconInactiveColor: '#ccc',
|
||||||
|
pageTextStyle: { color: '#333' },
|
||||||
|
itemGap: isSmallContainer ? 5 : 10,
|
||||||
|
itemWidth: isSmallContainer ? 15 : 25,
|
||||||
|
itemHeight: isSmallContainer ? 10 : 14,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: isSmallContainer ? 10 : 12,
|
||||||
|
},
|
||||||
|
formatter(name: string) {
|
||||||
|
return name.length > 15 ? `${name.substring(0, 12)}...` : name;
|
||||||
|
},
|
||||||
data: data.map(item => item.name),
|
data: data.map(item => item.name),
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
@@ -208,6 +196,7 @@ function updatePieChart() {
|
|||||||
name: '模型用量',
|
name: '模型用量',
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
radius: ['50%', '70%'],
|
radius: ['50%', '70%'],
|
||||||
|
center: isManyItems ? ['40%', '50%'] : ['50%', '50%'],
|
||||||
avoidLabelOverlap: false,
|
avoidLabelOverlap: false,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
@@ -236,7 +225,7 @@ function updatePieChart() {
|
|||||||
pieChartInstance.setOption(option);
|
pieChartInstance.setOption(option);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新柱状图
|
// 更新柱状图 - 动态适应数据量
|
||||||
function updateBarChart() {
|
function updateBarChart() {
|
||||||
if (!barChartInstance || modelUsageData.value.length === 0)
|
if (!barChartInstance || modelUsageData.value.length === 0)
|
||||||
return;
|
return;
|
||||||
@@ -244,6 +233,9 @@ function updateBarChart() {
|
|||||||
const models = modelUsageData.value.map(item => item.model);
|
const models = modelUsageData.value.map(item => item.model);
|
||||||
const tokens = modelUsageData.value.map(item => item.tokens);
|
const tokens = modelUsageData.value.map(item => item.tokens);
|
||||||
|
|
||||||
|
const isManyItems = models.length > 10;
|
||||||
|
const isSmallContainer = barContainerSize.width.value < 600;
|
||||||
|
|
||||||
const option = {
|
const option = {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
@@ -256,6 +248,7 @@ function updateBarChart() {
|
|||||||
left: '3%',
|
left: '3%',
|
||||||
right: '4%',
|
right: '4%',
|
||||||
bottom: '3%',
|
bottom: '3%',
|
||||||
|
top: '3%',
|
||||||
containLabel: true,
|
containLabel: true,
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
@@ -267,12 +260,20 @@ function updateBarChart() {
|
|||||||
data: models,
|
data: models,
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
interval: 0,
|
interval: 0,
|
||||||
|
fontSize: Math.max(10, 14 - Math.floor(models.length / 10)),
|
||||||
|
formatter(value: string) {
|
||||||
|
if (isSmallContainer && value.length > 10) {
|
||||||
|
return `${value.substring(0, 8)}...`;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
name: '用量',
|
name: '用量',
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
|
barWidth: isManyItems ? '60%' : '80%',
|
||||||
data: tokens,
|
data: tokens,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color(params: any) {
|
color(params: any) {
|
||||||
@@ -311,6 +312,20 @@ function resizeCharts() {
|
|||||||
barChartInstance?.resize();
|
barChartInstance?.resize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换全屏
|
||||||
|
function toggleFullscreen() {
|
||||||
|
isFullscreen.value = !isFullscreen.value;
|
||||||
|
nextTick(() => {
|
||||||
|
resizeCharts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听容器大小变化
|
||||||
|
watch([pieContainerSize.width, barContainerSize.width], () => {
|
||||||
|
updatePieChart();
|
||||||
|
updateBarChart();
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initCharts();
|
initCharts();
|
||||||
fetchUsageData();
|
fetchUsageData();
|
||||||
@@ -325,12 +340,23 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="usage-statistics">
|
<div
|
||||||
|
ref="usageStatisticsRef"
|
||||||
|
class="usage-statistics"
|
||||||
|
:class="{ 'fullscreen-mode': isFullscreen }"
|
||||||
|
>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h2>
|
<h2>
|
||||||
<el-icon><PieChart /></el-icon>
|
<el-icon><PieChart /></el-icon>
|
||||||
Token用量统计
|
Token用量统计
|
||||||
</h2>
|
</h2>
|
||||||
|
<el-button
|
||||||
|
:icon="FullScreen"
|
||||||
|
circle
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
@click="toggleFullscreen"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-card v-loading="loading" class="chart-card">
|
<el-card v-loading="loading" class="chart-card">
|
||||||
@@ -354,7 +380,7 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<div ref="pieChart" class="chart" style="height: 400px;" />
|
<div ref="pieChart" class="chart" style="height: 450px;" />
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
@@ -365,16 +391,29 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<div ref="barChart" class="chart" style="height: 400px;" />
|
<div ref="barChart" class="chart" style="height: 500px;" />
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 样式保持不变 */
|
|
||||||
.usage-statistics {
|
.usage-statistics {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-statistics.fullscreen-mode {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 2000;
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
@@ -421,6 +460,19 @@ onBeforeUnmount(() => {
|
|||||||
.header {
|
.header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
height: 350px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-statistics.fullscreen-mode {
|
||||||
|
padding: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user