feat: 完成意心ai3.4版本发布

This commit is contained in:
ccnetcore
2026-01-24 17:55:42 +08:00
parent 21b7ef4d74
commit 1ada6360d4
2 changed files with 319 additions and 191 deletions

View File

@@ -213,8 +213,12 @@ export function getPremiumPackageTokenUsage() {
] */ ] */
// 获取当前用户近24小时每小时Token消耗统计 // 获取当前用户近24小时每小时Token消耗统计
export function getLast24HoursTokenUsage() { // tokenId: 可选传入则查询该token的用量不传则查询全部
return get<any>('/usage-statistics/last24Hours-token-usage').json(); export function getLast24HoursTokenUsage(tokenId?: string) {
const url = tokenId
? `/usage-statistics/last24Hours-token-usage?tokenId=${tokenId}`
: '/usage-statistics/last24Hours-token-usage';
return get<any>(url).json();
} }
/* 返回数据 /* 返回数据
[ [
@@ -232,8 +236,12 @@ export function getLast24HoursTokenUsage() {
*/ */
// 获取当前用户今日各模型使用量统计 // 获取当前用户今日各模型使用量统计
export function getTodayModelUsage() { // tokenId: 可选传入则查询该token的用量不传则查询全部
return get<any>('/usage-statistics/today-model-usage').json(); export function getTodayModelUsage(tokenId?: string) {
const url = tokenId
? `/usage-statistics/today-model-usage?tokenId=${tokenId}`
: '/usage-statistics/today-model-usage';
return get<any>(url).json();
} }
/* 返回数据 /* 返回数据
[ [

View File

@@ -50,6 +50,9 @@ const barContainerSize = useElementSize(barChart);
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 showLast7DaysChart = ref(false); // 是否显示近七天图表
const loadingLast7Days = ref(false); // 近七天数据加载状态
const currentPeriodView = ref('24h'); // 当前显示的时间范围:'24h' 或 '7d'
const modelUsageData = ref<any[]>([]); const modelUsageData = ref<any[]>([]);
const hourlyUsageData = ref<any[]>([]); // 新增近24小时每小时Token消耗数据 const hourlyUsageData = ref<any[]>([]); // 新增近24小时每小时Token消耗数据
const todayModelUsageData = ref<any[]>([]); // 新增:今日各模型使用量数据 const todayModelUsageData = ref<any[]>([]); // 新增:今日各模型使用量数据
@@ -179,26 +182,65 @@ async function fetchTokenOptions() {
// Token选择变化 // Token选择变化
function handleTokenChange() { function handleTokenChange() {
// 切换 token 时重置近七天数据状态
showLast7DaysChart.value = false;
usageData.value = [];
totalTokens.value = 0;
// 如果当前是7天视图切换回24小时视图
if (currentPeriodView.value === '7d') {
currentPeriodView.value = '24h';
}
fetchUsageData(); fetchUsageData();
} }
// 获取用量数据 // 切换时间范围视图24小时/7天
function togglePeriodView() {
if (currentPeriodView.value === '24h') {
// 切换到7天视图
currentPeriodView.value = '7d';
if (!showLast7DaysChart.value) {
fetchLast7DaysData();
} else {
// 使用 nextTick 确保 DOM 更新后再渲染图表
nextTick(() => {
// 确保 lineChart 实例已初始化
if (!lineChartInstance && lineChart.value) {
lineChartInstance = echarts.init(lineChart.value);
}
updateLineChart();
// 延迟调用 resize 确保容器尺寸正确
setTimeout(() => {
lineChartInstance?.resize();
}, 100);
});
}
} else {
// 切换回24小时视图
currentPeriodView.value = '24h';
// 使用 nextTick 确保 DOM 更新后再渲染图表
nextTick(() => {
updateHourlyBarChart();
setTimeout(() => {
hourlyBarChartInstance?.resize();
}, 100);
});
}
}
// 获取用量数据(不包含近七天数据)
async function fetchUsageData() { async function fetchUsageData() {
loading.value = true; loading.value = true;
try { try {
const tokenId = selectedTokenId.value || undefined; const tokenId = selectedTokenId.value || undefined;
const [res, res2, res3, res4] = await Promise.all([ const [res2, res3, res4] = await Promise.all([
getLast7DaysTokenUsage(tokenId),
getModelTokenUsage(tokenId), getModelTokenUsage(tokenId),
getLast24HoursTokenUsage(), getLast24HoursTokenUsage(tokenId),
getTodayModelUsage(), getTodayModelUsage(tokenId),
]); ]);
usageData.value = res.data || [];
modelUsageData.value = res2.data || []; modelUsageData.value = res2.data || [];
hourlyUsageData.value = res3.data || []; hourlyUsageData.value = res3.data || [];
todayModelUsageData.value = res4.data || []; todayModelUsageData.value = res4.data || [];
totalTokens.value = usageData.value.reduce((sum, item) => sum + item.tokens, 0);
updateCharts(); updateCharts();
} }
@@ -211,6 +253,39 @@ async function fetchUsageData() {
} }
} }
// 单独加载近七天数据
async function fetchLast7DaysData() {
loadingLast7Days.value = true;
try {
const tokenId = selectedTokenId.value || undefined;
const res = await getLast7DaysTokenUsage(tokenId);
usageData.value = res.data || [];
totalTokens.value = usageData.value.reduce((sum, item) => sum + item.tokens, 0);
showLast7DaysChart.value = true;
// 使用 nextTick 确保 DOM 更新后再渲染图表
nextTick(() => {
// 确保 lineChart 实例已初始化
if (!lineChartInstance && lineChart.value) {
lineChartInstance = echarts.init(lineChart.value);
}
updateLineChart();
// 延迟调用 resize 确保容器尺寸正确
setTimeout(() => {
lineChartInstance?.resize();
}, 100);
});
}
catch (error) {
console.error('获取近七天数据失败:', error);
ElMessage.error('获取近七天数据失败');
}
finally {
loadingLast7Days.value = false;
}
}
// 初始化图表 // 初始化图表
function initCharts() { function initCharts() {
if (lineChart.value) { if (lineChart.value) {
@@ -614,7 +689,7 @@ function updateBarChart() {
barChartInstance.setOption(option, true); barChartInstance.setOption(option, true);
} }
// 更新近24小时每小时Token消耗柱状图 // 更新近24小时每小时Token消耗柱状图(堆叠柱状图)
function updateHourlyBarChart() { function updateHourlyBarChart() {
if (!hourlyBarChartInstance) if (!hourlyBarChartInstance)
return; return;
@@ -689,84 +764,36 @@ function updateHourlyBarChart() {
const isMobile = window.innerWidth < 768; const isMobile = window.innerWidth < 768;
// 找出24小时中单小时模型数量最多的值 // 堆叠柱状图强调样式
const maxModelsPerHour = hourlyUsageData.value.reduce((max, hour) => { const emphasisStyle = {
const count = hour.modelBreakdown?.length || 0; itemStyle: {
return Math.max(max, count); shadowBlur: 10,
}, 0); shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.3)',
},
};
// 智能计算柱子宽度:基于最大模型数量动态计算 // 构建每个模型的数据系列(堆叠柱状图)
// 柱子宽度 = 单元格宽度 / 最大模型数量 - 间距占比
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 series = hourlyModels.value.map(({ modelId }, index) => {
const data = hourlyUsageData.value.map((hour) => { const data = hourlyUsageData.value.map((hour) => {
const modelData = hour.modelBreakdown?.find((m: any) => m.modelId === modelId); const modelData = hour.modelBreakdown?.find((m: any) => m.modelId === modelId);
return modelData?.tokens || 0; return modelData?.tokens || 0;
}); });
// 根据最大模型数量决定是否显示标签
const showLabel = maxModelsPerHour <= 4 && !isMobile;
return { return {
name: modelId, name: modelId,
type: 'bar', type: 'bar',
barWidth, stack: 'total', // 所有系列堆叠在一起
barGap, emphasis: emphasisStyle,
barCategoryGap,
data, data,
itemStyle: { itemStyle: {
color: modelColors.value[modelId], color: modelColors.value[modelId],
borderRadius: [4, 4, 0, 0], borderRadius: [0, 0, 0, 0], // 堆叠柱状图不需要圆角
borderWidth: 0, borderWidth: 0,
}, },
label: { label: {
show: showLabel, show: false, // 堆叠柱状图不显示标签
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,
}; };
}); });
@@ -847,21 +874,18 @@ function updateHourlyBarChart() {
}, },
}, },
toolbox: { toolbox: {
show: true, // 强制显示工具箱 show: true,
feature: { feature: {
dataZoom: { magicType: {
yAxisIndex: 'none', type: ['stack', 'line'],
title: { title: {
zoom: '区域缩放', stack: '切换为堆叠',
back: '还原缩放', line: '切换为折线',
}, },
}, },
brush: { dataView: {
type: ['lineX', 'clear'], title: '数据视图',
title: { readOnly: true,
lineX: '横向选择',
clear: '清除选择',
},
}, },
restore: { restore: {
show: true, show: true,
@@ -896,6 +920,26 @@ function updateHourlyBarChart() {
}, },
}, },
}, },
brush: {
toolbox: ['lineX', 'keep', 'clear'],
xAxisIndex: 0,
brushLink: 'all',
throttleType: 'debounce',
throttleDelay: 300,
removeOnClick: true,
brushType: false,
inBrush: {
opacity: 1,
},
outOfBrush: {
opacity: 0.15,
},
brushStyle: {
borderWidth: 1,
color: 'rgba(102, 126, 234, 0.15)',
borderColor: '#667eea',
},
},
legend: { legend: {
show: true, show: true,
type: hourlyModels.value.length > 8 || isMobile ? 'scroll' : 'plain', type: hourlyModels.value.length > 8 || isMobile ? 'scroll' : 'plain',
@@ -920,89 +964,30 @@ function updateHourlyBarChart() {
pageButtonItemGap: 5, pageButtonItemGap: 5,
}, },
grid: { grid: {
top: maxModelsPerHour > 8 ? '15%' : '12%', top: '12%',
left: '1%', left: '1%',
right: isMobile ? '8%' : '10%', right: isMobile ? '8%' : '3%',
bottom: isMobile ? '15%' : '12%', bottom: '10%',
containLabel: true, containLabel: true,
}, },
dataZoom: [
{
show: true, // 强制显示滑动条
start: hourlyUsageData.value.length > 12 ? 100 - Math.round((12 / hourlyUsageData.value.length) * 100) : 80,
end: 100,
xAxisIndex: [0],
bottom: '3%',
height: 20,
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: true, // 强制显示Y轴缩放条
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: { xAxis: {
type: 'category', type: 'category',
data: hours, data: hours,
name: '时间',
nameTextStyle: {
fontSize: isMobile ? 11 : 12,
color: '#999',
padding: [0, 0, 0, 0],
},
axisLine: { axisLine: {
show: true,
lineStyle: { lineStyle: {
color: '#e9ecef', color: '#e9ecef',
width: 1, width: 1,
}, },
}, },
axisTick: { axisTick: {
alignWithLabel: true, show: false,
length: 4,
}, },
axisLabel: { axisLabel: {
interval: isMobile ? 3 : 0, interval: isMobile ? 3 : 0,
@@ -1011,6 +996,12 @@ function updateHourlyBarChart() {
color: '#666', color: '#666',
fontFamily: 'monospace', fontFamily: 'monospace',
}, },
splitLine: {
show: false,
},
splitArea: {
show: false,
},
}, },
yAxis: { yAxis: {
type: 'value', type: 'value',
@@ -1150,23 +1141,56 @@ onBeforeUnmount(() => {
</div> </div>
</div> </div>
<!-- 近24小时每小时Token消耗柱状图 --> <!-- 时间范围图表可切换24小时/7 -->
<el-card v-loading="loading" class="chart-card"> <el-card v-loading="currentPeriodView === '7d' ? loadingLast7Days : loading" class="chart-card">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span class="card-title"> 近24小时每小时Token消耗</span> <span class="card-title">
{{ currentPeriodView === '24h' ? '⏰ 近24小时每小时Token消耗' : '📊 近七天每日Token消耗量' }}{{ selectedTokenId ? ` (${selectedTokenName})` : '' }}
</span>
<!-- 切换按钮 -->
<div class="period-toggle-group">
<el-button
:type="currentPeriodView === '24h' ? 'primary' : 'default'"
size="small"
@click="togglePeriodView"
>
近24小时
</el-button>
<el-button
:type="currentPeriodView === '7d' ? 'primary' : 'default'"
size="small"
@click="togglePeriodView"
>
近7天
</el-button>
</div>
</div> </div>
</template> </template>
<div class="chart-container"> <!-- 24小时视图 -->
<div v-show="currentPeriodView === '24h'" class="chart-container">
<div ref="hourlyBarChart" class="chart hourly-bar-chart" /> <div ref="hourlyBarChart" class="chart hourly-bar-chart" />
</div> </div>
<!-- 7天视图 -->
<div v-show="currentPeriodView === '7d'" class="chart-container">
<div v-if="!showLast7DaysChart" class="load-chart-inline">
<div class="load-inline-content">
<div class="load-inline-icon">📊</div>
<div class="load-inline-text">该功能查询性能消耗较大</div>
<el-button type="primary" @click="fetchLast7DaysData" :loading="loadingLast7Days">
加载近七天数据
</el-button>
</div>
</div>
<div v-else ref="lineChart" class="chart line-chart" />
</div>
</el-card> </el-card>
<!-- 今日各模型使用量卡片列表 --> <!-- 今日各模型使用量卡片列表 -->
<el-card v-loading="loading" class="chart-card today-model-card"> <el-card v-loading="loading" class="chart-card today-model-card">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span class="card-title">📋 今日各模型使用量统计(凌晨零点至现在)</span> <span class="card-title">📋 今日各模型使用量统计(凌晨零点至现在){{ selectedTokenId ? ` (${selectedTokenName})` : '' }}</span>
<el-tag v-if="todayModelUsageData.length > 0" type="success" effect="plain"> <el-tag v-if="todayModelUsageData.length > 0" type="success" effect="plain">
{{ todayModelUsageData.length }} 个模型 {{ todayModelUsageData.length }} 个模型
</el-tag> </el-tag>
@@ -1236,41 +1260,30 @@ onBeforeUnmount(() => {
</div> </div>
</el-card> </el-card>
<el-card v-loading="loading" class="chart-card"> <!-- 各模型Token消耗占比和总Token消耗量并排显示 -->
<template #header> <div class="charts-row">
<div class="card-header"> <el-card v-loading="loading" class="chart-card half-width">
<span class="card-title">📊 近七天每日Token消耗量{{ selectedTokenId ? ` (${selectedTokenName})` : '' }}</span> <template #header>
<el-tag type="primary" size="large" effect="dark"> <div class="card-header">
近七日总计: {{ totalTokens }} tokens <span class="card-title">🥧 各模型总Token消耗占比{{ selectedTokenId ? ` (${selectedTokenName})` : '' }}</span>
</el-tag> </div>
</template>
<div class="chart-container">
<div ref="pieChart" class="chart pie-chart" />
</div> </div>
</template> </el-card>
<div class="chart-container">
<div ref="lineChart" class="chart line-chart" />
</div>
</el-card>
<el-card v-loading="loading" class="chart-card"> <el-card v-loading="loading" class="chart-card half-width">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span class="card-title">🥧 各模型Token消耗占比{{ selectedTokenId ? ` (${selectedTokenName})` : '' }}</span> <span class="card-title">📈 各模型Token消耗{{ selectedTokenId ? ` (${selectedTokenName})` : '' }}</span>
</div>
</template>
<div class="chart-container">
<div ref="barChart" class="chart bar-chart" />
</div> </div>
</template> </el-card>
<div class="chart-container"> </div>
<div ref="pieChart" class="chart pie-chart" />
</div>
</el-card>
<el-card v-loading="loading" class="chart-card">
<template #header>
<div class="card-header">
<span class="card-title">📈 各模型总Token消耗量{{ selectedTokenId ? ` (${selectedTokenName})` : '' }}</span>
</div>
</template>
<div class="chart-container">
<div ref="barChart" class="chart bar-chart" />
</div>
</el-card>
</div> </div>
</template> </template>
@@ -1384,6 +1397,59 @@ onBeforeUnmount(() => {
transform: translateY(-4px); transform: translateY(-4px);
} }
/* 图表行布局:两个图表并排显示 */
.charts-row {
display: flex;
gap: 20px;
margin-bottom: 30px;
}
.charts-row .chart-card {
flex: 1;
margin-bottom: 0;
}
.charts-row .half-width {
width: 50%;
}
/* 时间范围切换按钮组 */
.period-toggle-group {
display: flex;
gap: 8px;
align-items: center;
}
.period-toggle-group .el-button {
padding: 6px 16px;
font-size: 13px;
}
/* 内联加载提示 */
.load-chart-inline {
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
.load-inline-content {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.load-inline-icon {
font-size: 48px;
}
.load-inline-text {
font-size: 14px;
color: #999;
}
.card-header { .card-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -1643,6 +1709,40 @@ onBeforeUnmount(() => {
overflow: hidden; overflow: hidden;
} }
/* 移动端图表行垂直排列 */
.charts-row {
flex-direction: column;
gap: 16px;
}
.charts-row .half-width {
width: 100%;
}
/* 移动端切换按钮组 */
.period-toggle-group {
flex-direction: column;
gap: 4px;
}
.period-toggle-group .el-button {
padding: 4px 12px;
font-size: 12px;
}
/* 移动端内联加载提示 */
.load-chart-inline {
min-height: 280px;
}
.load-inline-icon {
font-size: 40px;
}
.load-inline-text {
font-size: 13px;
}
/* 移动端图表高度优化 */ /* 移动端图表高度优化 */
.line-chart { .line-chart {
height: 280px !important; height: 280px !important;
@@ -1720,6 +1820,26 @@ onBeforeUnmount(() => {
font-size: 13px; font-size: 13px;
} }
/* 超小屏幕图表行垂直排列 */
.charts-row {
flex-direction: column;
gap: 12px;
}
.charts-row .half-width {
width: 100%;
}
/* 超小屏幕切换按钮组 */
.period-toggle-group .el-button {
padding: 4px 10px;
font-size: 11px;
}
.load-chart-inline {
min-height: 240px;
}
/* 超小屏幕图表高度 */ /* 超小屏幕图表高度 */
.line-chart { .line-chart {
height: 250px !important; height: 250px !important;