From 55469708f0f9ef36dafcccfa72e66c85135f0541 Mon Sep 17 00:00:00 2001 From: Gsh <15170702455@163.com> Date: Sat, 29 Nov 2025 22:48:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=A4=9Atoken?= =?UTF-8?q?=E7=94=A8=E9=87=8F=E6=9F=A5=E7=9C=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Yi.Ai.Vue3/src/api/model/index.ts | 89 ++- .../components/APIKeyManagement.vue | 94 +-- .../components/PremiumPackageInfo.vue | 563 ++++++++++++++++++ .../components/UsageStatistics.vue | 199 +++++-- 4 files changed, 825 insertions(+), 120 deletions(-) diff --git a/Yi.Ai.Vue3/src/api/model/index.ts b/Yi.Ai.Vue3/src/api/model/index.ts index c3e9fff4..5e27a407 100644 --- a/Yi.Ai.Vue3/src/api/model/index.ts +++ b/Yi.Ai.Vue3/src/api/model/index.ts @@ -3,7 +3,6 @@ import { del, get, post, put } from '@/utils/request'; // 获取当前用户的模型列表 export function getModelList() { - // return get('/system/model/modelList'); return get('/ai-chat/model').json(); } // 申请ApiKey @@ -21,72 +20,36 @@ export function getRechargeLog() { } // 查询用户近7天token消耗 -export function getLast7DaysTokenUsage() { - return get('/usage-statistics/last7Days-token-usage').json(); +// tokenId: 可选,传入则查询该token的用量,不传则查询全部 +export function getLast7DaysTokenUsage(tokenId?: string) { + const url = tokenId + ? `/usage-statistics/last7Days-token-usage?tokenId=${tokenId}` + : '/usage-statistics/last7Days-token-usage'; + return get(url).json(); } // 查询用户token消耗各模型占比 -export function getModelTokenUsage() { - return get('/usage-statistics/model-token-usage').json(); +// tokenId: 可选,传入则查询该token的用量,不传则查询全部 +export function getModelTokenUsage(tokenId?: string) { + const url = tokenId + ? `/usage-statistics/model-token-usage?tokenId=${tokenId}` + : '/usage-statistics/model-token-usage'; + return get(url).json(); } -// 以下为新增接口 - // 获取当前用户得token列表 export function getTokenList() { return get('/token/list').json(); } -/* -返回数据: -[ - { - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "string", - "apiKey": "string", - "expireTime": "2025-11-29T07:34:23.850Z", - "premiumQuotaLimit": 0, - "premiumUsedQuota": 0, - "isDisabled": true, - "creationTime": "2025-11-29T07:34:23.850Z" - } -] */ // 创建token export function createToken(data: any) { return post('/token', data).json(); } -/* -data: -{ - "name": "string", - "expireTime": "2025-11-29T07:35:10.458Z", - "premiumQuotaLimit": 0 -} */ -/* -返回: -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "string", - "apiKey": "string", - "expireTime": "2025-11-29T07:35:10.459Z", - "premiumQuotaLimit": 0, - "premiumUsedQuota": 0, - "isDisabled": true, - "creationTime": "2025-11-29T07:35:10.459Z" -} */ // 编辑token export function editToken(data: any) { return put('/token', data).json(); } -/* -data: -{ - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "name": "string", - "expireTime": "2025-11-29T07:36:49.589Z", - "premiumQuotaLimit": 0 -} -*/ // 删除token export function deleteToken(id: string) { @@ -102,3 +65,31 @@ export function enableToken(id: string) { export function disableToken(id: string) { return post(`/token/${id}/disable`).json(); } + +// 新增接口2 +// 获取可选择的token信息 +export function getSelectableTokenInfo() { + return get('/token/select-list').json(); +} +/* +返回数据 + [ + { + "tokenId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "name": "string", + "isDisabled": true + } + ] */ +// 获取当前用户尊享包不同token用量占比(饼图) +export function getPremiumPackageTokenUsage() { + return get('/usage-statistics/premium-token-usage/by-token').json(); +} +/* 返回数据 + [ + { + "tokenId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "tokenName": "string", + "tokens": 0, + "percentage": 0 + } + ] */ diff --git a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/APIKeyManagement.vue b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/APIKeyManagement.vue index 69222db2..14fc776e 100644 --- a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/APIKeyManagement.vue +++ b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/APIKeyManagement.vue @@ -1,15 +1,15 @@ @@ -495,6 +794,270 @@ function onProductPackage() { font-size: 13px; } +/* Token用量占比卡片 */ +.token-usage-card { + margin-top: 24px; +} + +.token-icon { + background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%); + color: #0284c7; +} + +.token-usage-content { + display: flex; + flex-direction: column; + gap: 24px; +} + +.chart-container-wrapper { + width: 100%; + background: linear-gradient(135deg, #fafbfc 0%, #f5f6f8 100%); + border-radius: 12px; + padding: 20px; +} + +.token-pie-chart { + width: 100%; + height: 400px; +} + +/* Token统计列表 */ +.token-stats-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.token-stat-item { + padding: 16px; + background: linear-gradient(135deg, #fafbfc 0%, #ffffff 100%); + border-radius: 12px; + border: 1px solid #f0f2f5; + transition: all 0.3s; + + &:hover { + border-color: #667eea; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1); + transform: translateY(-2px); + } +} + +.token-stat-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +.token-rank { + flex-shrink: 0; +} + +.rank-badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 8px; + font-size: 14px; + font-weight: 700; + color: white; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + + &.rank-1 { + background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); + } + + &.rank-2 { + background: linear-gradient(135deg, #94a3b8 0%, #64748b 100%); + } + + &.rank-3 { + background: linear-gradient(135deg, #fb923c 0%, #ea580c 100%); + } +} + +.token-name { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + font-size: 16px; + font-weight: 600; + color: #1a1a1a; + + .el-icon { + color: #667eea; + font-size: 18px; + } +} + +.token-stat-data { + display: flex; + flex-direction: column; + gap: 12px; +} + +.stat-tokens { + display: flex; + align-items: baseline; + gap: 6px; + font-size: 14px; + + .label { + color: #909399; + } + + .value { + font-size: 20px; + font-weight: 700; + color: #667eea; + } + + .unit { + color: #909399; + font-size: 12px; + } +} + +.stat-percentage { + :deep(.el-progress__text) { + font-size: 14px !important; + font-weight: 700; + } +} + +/* 空状态 */ +.token-empty-state { + padding: 60px 20px; + + :deep(.el-empty__image) { + width: auto; + } +} + +.custom-empty-image { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 140px; + height: 140px; + margin: 0 auto; +} + +.empty-main-icon { + font-size: 80px; + color: #667eea; + position: relative; + z-index: 2; + animation: float 3s ease-in-out infinite; +} + +.empty-decoration { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; +} + +.decoration-circle { + position: absolute; + border-radius: 50%; + background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); + + &:nth-child(1) { + width: 60px; + height: 60px; + top: 10%; + left: 10%; + animation: pulse 2s ease-in-out infinite; + } + + &:nth-child(2) { + width: 40px; + height: 40px; + bottom: 20%; + right: 15%; + animation: pulse 2s ease-in-out infinite 0.5s; + } + + &:nth-child(3) { + width: 30px; + height: 30px; + top: 60%; + right: 5%; + animation: pulse 2s ease-in-out infinite 1s; + } +} + +@keyframes float { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); + } +} + +@keyframes pulse { + 0%, 100% { + transform: scale(1); + opacity: 0.6; + } + 50% { + transform: scale(1.2); + opacity: 0.3; + } +} + +.empty-description { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + margin-top: 20px; +} + +.empty-title { + margin: 0; + font-size: 18px; + font-weight: 600; + color: #303133; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.empty-text { + margin: 0; + font-size: 14px; + color: #606266; + line-height: 1.6; + max-width: 360px; +} + +.empty-tips { + display: flex; + align-items: center; + gap: 6px; + padding: 10px 16px; + background: linear-gradient(135deg, #f0f4ff 0%, #e8f0fe 100%); + border-radius: 8px; + font-size: 13px; + color: #667eea; + margin-top: 8px; + + .el-icon { + font-size: 16px; + flex-shrink: 0; + } +} + /* 警告卡片 */ .warning-card { border-radius: 12px; diff --git a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/UsageStatistics.vue b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/UsageStatistics.vue index d402143d..41ad2427 100644 --- a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/UsageStatistics.vue +++ b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/UsageStatistics.vue @@ -11,7 +11,7 @@ import { } from 'echarts/components'; import * as echarts from 'echarts/core'; import { CanvasRenderer } from 'echarts/renderers'; -import { getLast7DaysTokenUsage, getModelTokenUsage } from '@/api'; +import { getLast7DaysTokenUsage, getModelTokenUsage, getSelectableTokenInfo } from '@/api'; // 注册必要的组件 echarts.use([ @@ -48,16 +48,53 @@ const totalTokens = ref(0); const usageData = ref([]); const modelUsageData = ref([]); +// Token选择相关 +const selectedTokenId = ref(''); // 空字符串表示查询全部 +const tokenOptions = ref([]); +const tokenOptionsLoading = ref(false); + // 计算属性:是否有模型数据 const hasModelData = computed(() => modelUsageData.value.length > 0); +// 计算属性:当前选择的token名称 +const selectedTokenName = computed(() => { + if (!selectedTokenId.value) return '全部Token'; + const token = tokenOptions.value.find(t => t.tokenId === selectedTokenId.value); + return token?.name || '未知Token'; +}); + +// 获取可选择的Token列表 +async function fetchTokenOptions() { + try { + tokenOptionsLoading.value = true; + const res = await getSelectableTokenInfo(); + if (res.data) { + // 不再过滤禁用的token,全部显示 + tokenOptions.value = res.data; + } + } + catch (error) { + console.error('获取Token列表失败:', error); + ElMessage.error('获取Token列表失败'); + } + finally { + tokenOptionsLoading.value = false; + } +} + +// Token选择变化 +function handleTokenChange() { + fetchUsageData(); +} + // 获取用量数据 async function fetchUsageData() { loading.value = true; try { + const tokenId = selectedTokenId.value || undefined; const [res, res2] = await Promise.all([ - getLast7DaysTokenUsage(), - getModelTokenUsage(), + getLast7DaysTokenUsage(tokenId), + getModelTokenUsage(tokenId), ]); usageData.value = res.data || []; @@ -235,49 +272,47 @@ function updatePieChart() { formatter: '{a}
{b}: {c} tokens ({d}%)', }, legend: { - orient: isManyItems ? 'vertical' : 'horizontal', - right: isManyItems ? 10 : 'auto', - 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), + show: false, // 隐藏图例,使用标签线代替 }, series: [ { name: '模型用量', type: 'pie', radius: ['50%', '70%'], - center: isManyItems ? ['40%', '50%'] : ['50%', '50%'], - avoidLabelOverlap: false, + center: ['50%', '50%'], + avoidLabelOverlap: true, itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2, }, label: { - show: false, - position: 'center', + show: true, + position: 'outside', + formatter: '{b}: {d}%', + fontSize: 13, + fontWeight: 600, + color: '#333', }, emphasis: { label: { show: true, - fontSize: '18', + fontSize: 16, fontWeight: 'bold', }, + itemStyle: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.3)', + }, }, labelLine: { - show: false, + show: true, + length: 15, + length2: 10, + lineStyle: { + width: 1.5, + }, }, data, }, @@ -453,6 +488,7 @@ watch([pieContainerSize.width, barContainerSize.width], () => { onMounted(() => { initCharts(); + fetchTokenOptions(); fetchUsageData(); }); @@ -475,19 +511,54 @@ onBeforeUnmount(() => { Token用量统计 - +
+ + +
+ + 全部Token +
+
+ +
+ + + + {{ token.name }} + + 已禁用 + +
+
+
+ +