Merge remote-tracking branch 'origin/ai-hub' into ai-hub
This commit is contained in:
@@ -5,6 +5,8 @@
|
|||||||
"ComputedRef": true,
|
"ComputedRef": true,
|
||||||
"DirectiveBinding": true,
|
"DirectiveBinding": true,
|
||||||
"EffectScope": true,
|
"EffectScope": true,
|
||||||
|
"ElMessage": true,
|
||||||
|
"ElMessageBox": true,
|
||||||
"ExtractDefaultPropTypes": true,
|
"ExtractDefaultPropTypes": true,
|
||||||
"ExtractPropTypes": true,
|
"ExtractPropTypes": true,
|
||||||
"ExtractPublicPropTypes": true,
|
"ExtractPublicPropTypes": true,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"@vueuse/core": "^13.5.0",
|
"@vueuse/core": "^13.5.0",
|
||||||
"@vueuse/integrations": "^13.5.0",
|
"@vueuse/integrations": "^13.5.0",
|
||||||
"driver.js": "^1.3.6",
|
"driver.js": "^1.3.6",
|
||||||
|
"echarts": "^6.0.0",
|
||||||
"element-plus": "^2.10.4",
|
"element-plus": "^2.10.4",
|
||||||
"fingerprintjs": "^0.5.3",
|
"fingerprintjs": "^0.5.3",
|
||||||
"hook-fetch": "^2.0.4-beta.1",
|
"hook-fetch": "^2.0.4-beta.1",
|
||||||
|
|||||||
23
Yi.Ai.Vue3/pnpm-lock.yaml
generated
23
Yi.Ai.Vue3/pnpm-lock.yaml
generated
@@ -35,6 +35,9 @@ importers:
|
|||||||
driver.js:
|
driver.js:
|
||||||
specifier: ^1.3.6
|
specifier: ^1.3.6
|
||||||
version: 1.3.6
|
version: 1.3.6
|
||||||
|
echarts:
|
||||||
|
specifier: ^6.0.0
|
||||||
|
version: 6.0.0
|
||||||
element-plus:
|
element-plus:
|
||||||
specifier: ^2.10.4
|
specifier: ^2.10.4
|
||||||
version: 2.10.4(vue@3.5.17(typescript@5.8.3))
|
version: 2.10.4(vue@3.5.17(typescript@5.8.3))
|
||||||
@@ -1919,6 +1922,9 @@ packages:
|
|||||||
duplexer@0.1.2:
|
duplexer@0.1.2:
|
||||||
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
|
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
|
||||||
|
|
||||||
|
echarts@6.0.0:
|
||||||
|
resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==}
|
||||||
|
|
||||||
electron-to-chromium@1.5.165:
|
electron-to-chromium@1.5.165:
|
||||||
resolution: {integrity: sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==}
|
resolution: {integrity: sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==}
|
||||||
|
|
||||||
@@ -4450,6 +4456,9 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.0.0'
|
typescript: '>=4.0.0'
|
||||||
|
|
||||||
|
tslib@2.3.0:
|
||||||
|
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
|
||||||
|
|
||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
@@ -4853,6 +4862,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==}
|
resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==}
|
||||||
engines: {node: '>=12.20'}
|
engines: {node: '>=12.20'}
|
||||||
|
|
||||||
|
zrender@6.0.0:
|
||||||
|
resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==}
|
||||||
|
|
||||||
zwitch@2.0.4:
|
zwitch@2.0.4:
|
||||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||||
|
|
||||||
@@ -6789,6 +6801,11 @@ snapshots:
|
|||||||
|
|
||||||
duplexer@0.1.2: {}
|
duplexer@0.1.2: {}
|
||||||
|
|
||||||
|
echarts@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.3.0
|
||||||
|
zrender: 6.0.0
|
||||||
|
|
||||||
electron-to-chromium@1.5.165: {}
|
electron-to-chromium@1.5.165: {}
|
||||||
|
|
||||||
element-plus@2.10.4(vue@3.5.17(typescript@5.8.3)):
|
element-plus@2.10.4(vue@3.5.17(typescript@5.8.3)):
|
||||||
@@ -9819,6 +9836,8 @@ snapshots:
|
|||||||
picomatch: 4.0.2
|
picomatch: 4.0.2
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
|
|
||||||
|
tslib@2.3.0: {}
|
||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
@@ -10359,4 +10378,8 @@ snapshots:
|
|||||||
|
|
||||||
yocto-queue@1.2.1: {}
|
yocto-queue@1.2.1: {}
|
||||||
|
|
||||||
|
zrender@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.3.0
|
||||||
|
|
||||||
zwitch@2.0.4: {}
|
zwitch@2.0.4: {}
|
||||||
|
|||||||
@@ -19,3 +19,12 @@ export function getApiKey() {
|
|||||||
export function getRechargeLog() {
|
export function getRechargeLog() {
|
||||||
return get<any>('/recharge/account').json();
|
return get<any>('/recharge/account').json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询用户近7天token消耗
|
||||||
|
export function getLast7DaysTokenUsage() {
|
||||||
|
return get<any>('/usage-statistics/last7Days-token-usage').json();
|
||||||
|
}
|
||||||
|
// 查询用户token消耗各模型占比
|
||||||
|
export function getModelTokenUsage() {
|
||||||
|
return get<any>('/usage-statistics/model-token-usage').json();
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ interface Props {
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
title: '弹窗标题',
|
title: '弹窗标题',
|
||||||
width: '800px',
|
width: '1000px',
|
||||||
defaultActive: '',
|
defaultActive: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,6 +55,7 @@ function handleConfirm() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
|
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
:title="title"
|
:title="title"
|
||||||
:width="width"
|
:width="width"
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -62,6 +62,7 @@ const navItems = [
|
|||||||
// { name: 'permission', label: '权限管理', icon: 'Key' },
|
// { name: 'permission', label: '权限管理', icon: 'Key' },
|
||||||
{ name: 'apiKey', label: 'API密钥', icon: 'Key' },
|
{ name: 'apiKey', label: 'API密钥', icon: 'Key' },
|
||||||
{ name: 'rechargeLog', label: '充值记录', icon: 'Document' },
|
{ name: 'rechargeLog', label: '充值记录', icon: 'Document' },
|
||||||
|
{ name: 'usageStatistics', label: '用量统计', icon: 'Histogram' },
|
||||||
];
|
];
|
||||||
function openDialog() {
|
function openDialog() {
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
@@ -259,6 +260,10 @@ function openVipGuide() {
|
|||||||
<template #user>
|
<template #user>
|
||||||
<user-management />
|
<user-management />
|
||||||
</template>
|
</template>
|
||||||
|
<!-- 用量统计 -->
|
||||||
|
<template #usageStatistics>
|
||||||
|
<usage-statistics />
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 角色管理内容 -->
|
<!-- 角色管理内容 -->
|
||||||
<template #role>
|
<template #role>
|
||||||
|
|||||||
@@ -111,52 +111,55 @@ watch(
|
|||||||
// 封装数据处理逻辑
|
// 封装数据处理逻辑
|
||||||
function handleDataChunk(chunk: AnyObject) {
|
function handleDataChunk(chunk: AnyObject) {
|
||||||
try {
|
try {
|
||||||
const reasoningChunk = chunk.choices?.[0].delta.reasoning_content;
|
// 安全获取 delta 和 content
|
||||||
if (reasoningChunk) {
|
const delta = chunk.choices?.[0]?.delta;
|
||||||
// 开始思考链状态
|
const reasoningChunk = delta?.reasoning_content;
|
||||||
bubbleItems.value[bubbleItems.value.length - 1].thinkingStatus = 'thinking';
|
const parsedChunk = delta?.content;
|
||||||
bubbleItems.value[bubbleItems.value.length - 1].loading = true;
|
|
||||||
bubbleItems.value[bubbleItems.value.length - 1].thinlCollapse = true;
|
// usage 处理(可以移动到 startSSE 里也可以写这里)
|
||||||
if (bubbleItems.value.length) {
|
if (chunk.usage) {
|
||||||
bubbleItems.value[bubbleItems.value.length - 1].reasoning_content += reasoningChunk;
|
const { prompt_tokens, completion_tokens, total_tokens } = chunk.usage;
|
||||||
}
|
const latest = bubbleItems.value[bubbleItems.value.length - 1];
|
||||||
|
latest.tokenUsage = {
|
||||||
|
prompt: prompt_tokens,
|
||||||
|
completion: completion_tokens,
|
||||||
|
total: total_tokens,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reasoningChunk) {
|
||||||
|
const latest = bubbleItems.value[bubbleItems.value.length - 1];
|
||||||
|
latest.thinkingStatus = 'thinking';
|
||||||
|
latest.loading = true;
|
||||||
|
latest.thinlCollapse = true;
|
||||||
|
latest.reasoning_content += reasoningChunk;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 另一种思考中形式,content中有 <think></think> 的格式
|
|
||||||
// 一开始匹配到 <think> 开始,匹配到 </think> 结束,并处理标签中的内容为思考内容
|
|
||||||
const parsedChunk = chunk.choices?.[0].delta.content;
|
|
||||||
if (parsedChunk) {
|
if (parsedChunk) {
|
||||||
const thinkStart = parsedChunk.includes('<think>');
|
const thinkStart = parsedChunk.includes('<think>');
|
||||||
const thinkEnd = parsedChunk.includes('</think>');
|
const thinkEnd = parsedChunk.includes('</think>');
|
||||||
if (thinkStart) {
|
|
||||||
|
if (thinkStart)
|
||||||
isThinking = true;
|
isThinking = true;
|
||||||
}
|
if (thinkEnd)
|
||||||
if (thinkEnd) {
|
|
||||||
isThinking = false;
|
isThinking = false;
|
||||||
}
|
|
||||||
|
const latest = bubbleItems.value[bubbleItems.value.length - 1];
|
||||||
|
|
||||||
if (isThinking) {
|
if (isThinking) {
|
||||||
// 开始思考链状态
|
latest.thinkingStatus = 'thinking';
|
||||||
bubbleItems.value[bubbleItems.value.length - 1].thinkingStatus = 'thinking';
|
latest.loading = true;
|
||||||
bubbleItems.value[bubbleItems.value.length - 1].loading = true;
|
latest.thinlCollapse = true;
|
||||||
bubbleItems.value[bubbleItems.value.length - 1].thinlCollapse = true;
|
latest.reasoning_content += parsedChunk.replace('<think>', '').replace('</think>', '');
|
||||||
if (bubbleItems.value.length) {
|
|
||||||
bubbleItems.value[bubbleItems.value.length - 1].reasoning_content += parsedChunk
|
|
||||||
.replace('<think>', '')
|
|
||||||
.replace('</think>', '');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// 结束 思考链状态
|
latest.thinkingStatus = 'end';
|
||||||
bubbleItems.value[bubbleItems.value.length - 1].thinkingStatus = 'end';
|
latest.loading = false;
|
||||||
bubbleItems.value[bubbleItems.value.length - 1].loading = false;
|
latest.content += parsedChunk;
|
||||||
if (bubbleItems.value.length) {
|
|
||||||
bubbleItems.value[bubbleItems.value.length - 1].content += parsedChunk;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
// 这里如果使用了中断,会有报错,可以忽略不管
|
|
||||||
console.error('解析数据时出错:', err);
|
console.error('解析数据时出错:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,8 +311,9 @@ function copy(item: any) {
|
|||||||
<div class="footer-wrapper">
|
<div class="footer-wrapper">
|
||||||
<div class="footer-container">
|
<div class="footer-container">
|
||||||
<div class="footer-time">
|
<div class="footer-time">
|
||||||
{{ item.creationTime }}
|
<span v-if="item.creationTime "> {{ item.creationTime }}</span>
|
||||||
|
<span v-if="((item.role === 'ai' || item.role === 'assistant') && item?.tokenUsage?.total) " class="footer-token">
|
||||||
|
{{ ((item.role === 'ai' || item.role === 'assistant') && item?.tokenUsage?.total) ? `token:${item?.tokenUsage?.total}` : '' }}</span>
|
||||||
<el-button icon="DocumentCopy" size="small" circle @click="copy(item)" />
|
<el-button icon="DocumentCopy" size="small" circle @click="copy(item)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -421,4 +425,41 @@ function copy(item: any) {
|
|||||||
margin-bottom: 22px;
|
margin-bottom: 22px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
.footer-time {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 3px;
|
||||||
|
.footer-token {
|
||||||
|
background: rgba(1, 183, 86, 0.53);
|
||||||
|
padding: 0 4px;
|
||||||
|
margin: 0 2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-container {
|
||||||
|
:deep(.el-button + .el-button) {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
padding: 12px;
|
||||||
|
background: linear-gradient(to right, #fdfcfb 0%, #ffd1ab 100%);
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container span {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { useUserStore } from '@/stores/index.js';
|
|||||||
// 判断是否是 VIP 用户
|
// 判断是否是 VIP 用户
|
||||||
export function isUserVip(): boolean {
|
export function isUserVip(): boolean {
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
console.log('isUserVip----', userStore);
|
|
||||||
|
|
||||||
const userRoles = userStore.userInfo?.roles ?? [];
|
const userRoles = userStore.userInfo?.roles ?? [];
|
||||||
return userRoles.some((role: any) => role.roleCode === 'YiXinAi-Vip');
|
return userRoles.some((role: any) => role.roleCode === 'YiXinAi-Vip');
|
||||||
}
|
}
|
||||||
|
|||||||
2
Yi.Ai.Vue3/types/components.d.ts
vendored
2
Yi.Ai.Vue3/types/components.d.ts
vendored
@@ -34,6 +34,7 @@ declare module 'vue' {
|
|||||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
FilesSelect: typeof import('./../src/components/FilesSelect/index.vue')['default']
|
FilesSelect: typeof import('./../src/components/FilesSelect/index.vue')['default']
|
||||||
IconSelect: typeof import('./../src/components/IconSelect/index.vue')['default']
|
IconSelect: typeof import('./../src/components/IconSelect/index.vue')['default']
|
||||||
@@ -49,6 +50,7 @@ declare module 'vue' {
|
|||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
SvgIcon: typeof import('./../src/components/SvgIcon/index.vue')['default']
|
SvgIcon: typeof import('./../src/components/SvgIcon/index.vue')['default']
|
||||||
|
UsageStatistics: typeof import('./../src/components/userPersonalCenter/components/UsageStatistics.vue')['default']
|
||||||
UserManagement: typeof import('./../src/components/userPersonalCenter/components/UserManagement.vue')['default']
|
UserManagement: typeof import('./../src/components/userPersonalCenter/components/UserManagement.vue')['default']
|
||||||
VerificationCode: typeof import('./../src/components/LoginDialog/components/FormLogin/VerificationCode.vue')['default']
|
VerificationCode: typeof import('./../src/components/LoginDialog/components/FormLogin/VerificationCode.vue')['default']
|
||||||
WelecomeText: typeof import('./../src/components/WelecomeText/index.vue')['default']
|
WelecomeText: typeof import('./../src/components/WelecomeText/index.vue')['default']
|
||||||
|
|||||||
Reference in New Issue
Block a user