diff --git a/Yi.Ai.Vue3/src/api/model/index.ts b/Yi.Ai.Vue3/src/api/model/index.ts index 453de779..c4f4627b 100644 --- a/Yi.Ai.Vue3/src/api/model/index.ts +++ b/Yi.Ai.Vue3/src/api/model/index.ts @@ -213,8 +213,12 @@ export function getPremiumPackageTokenUsage() { ] */ // 获取当前用户近24小时每小时Token消耗统计 -export function getLast24HoursTokenUsage() { - return get('/usage-statistics/last24Hours-token-usage').json(); +// tokenId: 可选,传入则查询该token的用量,不传则查询全部 +export function getLast24HoursTokenUsage(tokenId?: string) { + const url = tokenId + ? `/usage-statistics/last24Hours-token-usage?tokenId=${tokenId}` + : '/usage-statistics/last24Hours-token-usage'; + return get(url).json(); } /* 返回数据 [ @@ -232,8 +236,12 @@ export function getLast24HoursTokenUsage() { */ // 获取当前用户今日各模型使用量统计 -export function getTodayModelUsage() { - return get('/usage-statistics/today-model-usage').json(); +// tokenId: 可选,传入则查询该token的用量,不传则查询全部 +export function getTodayModelUsage(tokenId?: string) { + const url = tokenId + ? `/usage-statistics/today-model-usage?tokenId=${tokenId}` + : '/usage-statistics/today-model-usage'; + return get(url).json(); } /* 返回数据 [ diff --git a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/UsageStatistics.vue b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/UsageStatistics.vue index 47673081..87c691ba 100644 --- a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/UsageStatistics.vue +++ b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/UsageStatistics.vue @@ -50,6 +50,9 @@ const barContainerSize = useElementSize(barChart); const loading = ref(false); const totalTokens = ref(0); const usageData = ref([]); +const showLast7DaysChart = ref(false); // 是否显示近七天图表 +const loadingLast7Days = ref(false); // 近七天数据加载状态 +const currentPeriodView = ref('24h'); // 当前显示的时间范围:'24h' 或 '7d' const modelUsageData = ref([]); const hourlyUsageData = ref([]); // 新增:近24小时每小时Token消耗数据 const todayModelUsageData = ref([]); // 新增:今日各模型使用量数据 @@ -179,26 +182,65 @@ async function fetchTokenOptions() { // Token选择变化 function handleTokenChange() { + // 切换 token 时重置近七天数据状态 + showLast7DaysChart.value = false; + usageData.value = []; + totalTokens.value = 0; + // 如果当前是7天视图,切换回24小时视图 + if (currentPeriodView.value === '7d') { + currentPeriodView.value = '24h'; + } 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() { loading.value = true; try { const tokenId = selectedTokenId.value || undefined; - const [res, res2, res3, res4] = await Promise.all([ - getLast7DaysTokenUsage(tokenId), + const [res2, res3, res4] = await Promise.all([ getModelTokenUsage(tokenId), - getLast24HoursTokenUsage(), - getTodayModelUsage(), + getLast24HoursTokenUsage(tokenId), + getTodayModelUsage(tokenId), ]); - 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(); } @@ -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() { if (lineChart.value) { @@ -614,7 +689,7 @@ function updateBarChart() { barChartInstance.setOption(option, true); } -// 更新近24小时每小时Token消耗柱状图 +// 更新近24小时每小时Token消耗柱状图(堆叠柱状图) function updateHourlyBarChart() { if (!hourlyBarChartInstance) return; @@ -689,84 +764,36 @@ function updateHourlyBarChart() { 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); + // 堆叠柱状图强调样式 + const emphasisStyle = { + itemStyle: { + shadowBlur: 10, + 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 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, + stack: 'total', // 所有系列堆叠在一起 + emphasis: emphasisStyle, data, itemStyle: { color: modelColors.value[modelId], - borderRadius: [4, 4, 0, 0], + borderRadius: [0, 0, 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', + show: false, // 堆叠柱状图不显示标签 }, - emphasis: { - focus: 'series', - itemStyle: { - shadowBlur: 8, - shadowOffsetX: 0, - shadowColor: 'rgba(0, 0, 0, 0.2)', - }, - }, - // 确保系列按总使用量排序的顺序 - seriesIndex: index, }; }); @@ -847,21 +874,18 @@ function updateHourlyBarChart() { }, }, toolbox: { - show: true, // 强制显示工具箱 + show: true, feature: { - dataZoom: { - yAxisIndex: 'none', + magicType: { + type: ['stack', 'line'], title: { - zoom: '区域缩放', - back: '还原缩放', + stack: '切换为堆叠', + line: '切换为折线', }, }, - brush: { - type: ['lineX', 'clear'], - title: { - lineX: '横向选择', - clear: '清除选择', - }, + dataView: { + title: '数据视图', + readOnly: true, }, restore: { 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: { show: true, type: hourlyModels.value.length > 8 || isMobile ? 'scroll' : 'plain', @@ -920,89 +964,30 @@ function updateHourlyBarChart() { pageButtonItemGap: 5, }, grid: { - top: maxModelsPerHour > 8 ? '15%' : '12%', + top: '12%', left: '1%', - right: isMobile ? '8%' : '10%', - bottom: isMobile ? '15%' : '12%', + right: isMobile ? '8%' : '3%', + bottom: '10%', 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: { type: 'category', data: hours, + name: '时间', + nameTextStyle: { + fontSize: isMobile ? 11 : 12, + color: '#999', + padding: [0, 0, 0, 0], + }, axisLine: { + show: true, lineStyle: { color: '#e9ecef', width: 1, }, }, axisTick: { - alignWithLabel: true, - length: 4, + show: false, }, axisLabel: { interval: isMobile ? 3 : 0, @@ -1011,6 +996,12 @@ function updateHourlyBarChart() { color: '#666', fontFamily: 'monospace', }, + splitLine: { + show: false, + }, + splitArea: { + show: false, + }, }, yAxis: { type: 'value', @@ -1150,23 +1141,56 @@ onBeforeUnmount(() => { - - + + -
+ +
+ +
+
+
+
📊
+
该功能查询性能消耗较大
+ + 加载近七天数据 + +
+
+
+
@@ -1384,6 +1397,59 @@ onBeforeUnmount(() => { 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 { display: flex; justify-content: space-between; @@ -1643,6 +1709,40 @@ onBeforeUnmount(() => { 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 { height: 280px !important; @@ -1720,6 +1820,26 @@ onBeforeUnmount(() => { 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 { height: 250px !important;