This commit is contained in:
2026-03-08 16:35:17 +08:00
parent f76af4d01a
commit a125b5922f
8 changed files with 745 additions and 1671 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -96,7 +96,7 @@ public class AiUsageStatsServiceImpl implements AiUsageStatsService {
public PageResult<AiUsageUserStatsRespVO> getUserStatsPage(AiUsageStatsPageReqVO reqVO) {
// 查询用户统计数据
List<Map<String, Object>> userStatsList = pointRecordMapper.selectUserStats(
reqVO.getStartTime(), reqVO.getEndTime(), reqVO.getBizType(), reqVO.getUserId());
reqVO.getStartTime(), reqVO.getEndTime(), reqVO.getBizType(), reqVO.getUserId(), reqVO.getMobile());
// 计算总数
long total = userStatsList.size();

View File

@@ -34,6 +34,9 @@ public class AiUsageStatsPageReqVO extends SortablePageParam {
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "手机号")
private String mobile;
@Schema(description = "服务标识")
private String serviceCode;

View File

@@ -78,13 +78,15 @@ public interface PointRecordMapper extends BaseMapperX<PointRecordDO> {
"<if test='endTime != null'> AND create_time &lt;= #{endTime}</if>" +
"<if test='bizType != null and bizType != \"\"'> AND biz_type = #{bizType}</if>" +
"<if test='userId != null'> AND user_id = #{userId}</if>" +
"<if test='mobile != null and mobile != \"\"'> AND mobile LIKE CONCAT('%', #{mobile}, '%')</if>" +
"GROUP BY user_id, mobile " +
"ORDER BY consumePoints DESC" +
"</script>")
List<Map<String, Object>> selectUserStats(@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime,
@Param("bizType") String bizType,
@Param("userId") Long userId);
@Param("userId") Long userId,
@Param("mobile") String mobile);
/**
* 按应用统计

View File

@@ -68,6 +68,7 @@ export const getAiUsageUserStatsPage = (params: {
endTime?: string
bizType?: string
userId?: number
mobile?: string
}) => {
return request.get({ url: '/muye/ai-usage-stats/user-stats', params })
}

View File

@@ -0,0 +1,360 @@
<template>
<div class="app-stats-panel">
<!-- 筛选条件 -->
<el-form :model="queryParams" :inline="true" class="mb-16px">
<el-form-item label="应用">
<el-select
v-model="queryParams.serviceCode"
placeholder="全部应用"
clearable
filterable
class="!w-200px"
@change="handleQuery"
>
<el-option
v-for="app in appOptions"
:key="app.serviceCode"
:label="app.serviceName || app.serviceCode"
:value="app.serviceCode"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<Icon icon="ep:search" class="mr-4px" /> 查询
</el-button>
<el-button @click="handleReset">
<Icon icon="ep:refresh" class="mr-4px" /> 重置
</el-button>
</el-form-item>
</el-form>
<!-- 统计卡片 -->
<el-row :gutter="16" class="mb-16px">
<el-col :span="6">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-icon bg-blue">
<Icon icon="ep:menu" />
</div>
<div class="stat-content">
<div class="stat-label">应用数量</div>
<div class="stat-value">{{ summary.appCount?.toLocaleString() || 0 }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-icon bg-green">
<Icon icon="ep:phone" />
</div>
<div class="stat-content">
<div class="stat-label">总调用次数</div>
<div class="stat-value">{{ summary.totalCallCount?.toLocaleString() || 0 }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-icon bg-orange">
<Icon icon="ep:coin" />
</div>
<div class="stat-content">
<div class="stat-label">总消耗积分</div>
<div class="stat-value">{{ summary.totalConsumePoints?.toLocaleString() || 0 }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-icon bg-purple">
<Icon icon="ep:data-analysis" />
</div>
<div class="stat-content">
<div class="stat-label">平均积分/应用</div>
<div class="stat-value">{{ avgPointsPerApp }}</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 图表区域 -->
<el-row :gutter="16" class="mb-16px">
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>应用积分消耗排行 TOP10</span>
</div>
</template>
<div ref="rankChartRef" class="chart-container"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>平台占比分布</span>
</div>
</template>
<div ref="pieChartRef" class="chart-container"></div>
</el-card>
</el-col>
</el-row>
<!-- 数据表格 -->
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>应用详细统计</span>
</div>
</template>
<el-table v-loading="loading" :data="filteredDataList" :stripe="true">
<el-table-column label="服务标识" align="center" prop="serviceCode" width="150" />
<el-table-column label="服务名称" align="center" prop="serviceName" />
<el-table-column label="平台" align="center" prop="platform" width="100" />
<el-table-column label="调用次数" align="center" prop="callCount" sortable>
<template #default="{ row }">{{ row.callCount?.toLocaleString() }}</template>
</el-table-column>
<el-table-column label="消耗积分" align="center" prop="consumePoints" sortable>
<template #default="{ row }">{{ row.consumePoints?.toLocaleString() }}</template>
</el-table-column>
<el-table-column label="总Token" align="center" prop="totalTokens" sortable>
<template #default="{ row }">{{ row.totalTokens?.toLocaleString() || '-' }}</template>
</el-table-column>
<el-table-column label="平均积分/次" align="center" prop="avgPointsPerCall" sortable>
<template #default="{ row }">{{ row.avgPointsPerCall?.toFixed(2) }}</template>
</el-table-column>
<el-table-column label="平均Token/次" align="center" prop="avgTokensPerCall" sortable>
<template #default="{ row }">{{ row.avgTokensPerCall?.toFixed(2) }}</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import { getAiUsageAppStats, type AiUsageAppStats } from '@/api/muye/aiusagestats'
interface Props {
timeRange: string[]
bizType: string
}
const props = defineProps<Props>()
// 查询参数
const queryParams = reactive({
serviceCode: undefined as string | undefined
})
// 数据
const loading = ref(false)
const dataList = ref<AiUsageAppStats[]>([])
const appOptions = ref<{ serviceCode: string; serviceName: string }[]>([])
// 筛选后的数据
const filteredDataList = computed(() => {
if (!queryParams.serviceCode) return dataList.value
return dataList.value.filter(d => d.serviceCode === queryParams.serviceCode)
})
// 汇总数据
const summary = computed(() => {
const data = filteredDataList.value
const appCount = data.length
const totalCallCount = data.reduce((sum, item) => sum + (item.callCount || 0), 0)
const totalConsumePoints = data.reduce((sum, item) => sum + (item.consumePoints || 0), 0)
return { appCount, totalCallCount, totalConsumePoints }
})
const avgPointsPerApp = computed(() => {
if (summary.value.appCount === 0) return '0'
return (summary.value.totalConsumePoints / summary.value.appCount).toFixed(2)
})
// 图表
const rankChartRef = ref<HTMLElement>()
const pieChartRef = ref<HTMLElement>()
let rankChart: echarts.ECharts | null = null
let pieChart: echarts.ECharts | null = null
const initCharts = () => {
if (rankChartRef.value && !rankChart) {
rankChart = echarts.init(rankChartRef.value)
}
if (pieChartRef.value && !pieChart) {
pieChart = echarts.init(pieChartRef.value)
}
}
const drawCharts = () => {
initCharts()
const data = filteredDataList.value
// 积分消耗排行 TOP10
const topByPoints = [...data]
.sort((a, b) => (b.consumePoints || 0) - (a.consumePoints || 0))
.slice(0, 10)
if (rankChart && topByPoints.length > 0) {
rankChart.setOption({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: { left: 120, right: 20, top: 20, bottom: 20 },
xAxis: { type: 'value', name: '积分' },
yAxis: {
type: 'category',
data: topByPoints.map(d => d.serviceName || d.serviceCode).reverse(),
axisLabel: { width: 100, overflow: 'truncate' }
},
series: [{
type: 'bar',
data: topByPoints.map(d => d.consumePoints).reverse(),
itemStyle: { color: '#e6a23c' }
}]
})
}
// 平台占比饼图
const platformStats = new Map<string, number>()
data.forEach(item => {
const platform = item.platform || '未知'
platformStats.set(platform, (platformStats.get(platform) || 0) + (item.consumePoints || 0))
})
const pieData = Array.from(platformStats.entries())
.map(([name, value]) => ({ name, value }))
.sort((a, b) => b.value - a.value)
if (pieChart && pieData.length > 0) {
pieChart.setOption({
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: { orient: 'vertical', left: 'left', top: 'center' },
series: [{
type: 'pie',
radius: ['40%', '70%'],
center: ['60%', '50%'],
data: pieData,
emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' }},
label: { show: false }
}]
})
}
}
// 获取数据
const getDataList = async () => {
loading.value = true
try {
const [startTime, endTime] = props.timeRange || []
const res = await getAiUsageAppStats({
startTime,
endTime,
bizType: props.bizType
})
dataList.value = res || []
// 构建应用选项
appOptions.value = (res || []).map(item => ({
serviceCode: item.serviceCode,
serviceName: item.serviceName || item.serviceCode
}))
await nextTick()
drawCharts()
} finally {
loading.value = false
}
}
// 查询
const handleQuery = () => {
nextTick(() => drawCharts())
}
// 重置
const handleReset = () => {
queryParams.serviceCode = undefined
handleQuery()
}
// 监听外部参数变化
watch(() => [props.timeRange, props.bizType], () => {
getDataList()
}, { deep: true })
// 窗口大小变化
const handleResize = () => {
rankChart?.resize()
pieChart?.resize()
}
onMounted(() => {
getDataList()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
rankChart?.dispose()
pieChart?.dispose()
})
</script>
<style scoped lang="scss">
.app-stats-panel {
.stat-card {
display: flex;
align-items: center;
padding: 8px;
.stat-icon {
width: 48px;
height: 48px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: #fff;
margin-right: 16px;
&.bg-blue { background: linear-gradient(135deg, #409eff, #3375b9); }
&.bg-orange { background: linear-gradient(135deg, #e6a23c, #cf9236); }
&.bg-green { background: linear-gradient(135deg, #67c23a, #529b2e); }
&.bg-purple { background: linear-gradient(135deg, #909399, #73767a); }
}
.stat-content {
.stat-label {
font-size: 14px;
color: #909399;
margin-bottom: 4px;
}
.stat-value {
font-size: 24px;
font-weight: 600;
color: #303133;
}
}
}
.card-header {
font-size: 16px;
font-weight: 500;
}
.chart-container {
height: 280px;
}
}
</style>

View File

@@ -0,0 +1,370 @@
<template>
<div class="user-stats-panel">
<!-- 筛选条件 -->
<el-form :model="queryParams" :inline="true" class="mb-16px">
<el-form-item label="手机号">
<el-input
v-model="queryParams.mobile"
placeholder="请输入手机号"
clearable
class="!w-200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<Icon icon="ep:search" class="mr-4px" /> 查询
</el-button>
<el-button @click="handleReset">
<Icon icon="ep:refresh" class="mr-4px" /> 重置
</el-button>
</el-form-item>
</el-form>
<!-- 统计卡片 -->
<el-row :gutter="16" class="mb-16px">
<el-col :span="6">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-icon bg-blue">
<Icon icon="ep:user" />
</div>
<div class="stat-content">
<div class="stat-label">活跃用户数</div>
<div class="stat-value">{{ summary.activeUserCount?.toLocaleString() || 0 }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-icon bg-green">
<Icon icon="ep:phone" />
</div>
<div class="stat-content">
<div class="stat-label">总调用次数</div>
<div class="stat-value">{{ summary.totalCallCount?.toLocaleString() || 0 }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-icon bg-orange">
<Icon icon="ep:coin" />
</div>
<div class="stat-content">
<div class="stat-label">总消耗积分</div>
<div class="stat-value">{{ summary.totalConsumePoints?.toLocaleString() || 0 }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-icon bg-purple">
<Icon icon="ep:data-analysis" />
</div>
<div class="stat-content">
<div class="stat-label">平均积分/用户</div>
<div class="stat-value">{{ avgPointsPerUser }}</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 图表区域 -->
<el-row :gutter="16" class="mb-16px">
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>用户积分消耗排行 TOP10</span>
</div>
</template>
<div ref="rankChartRef" class="chart-container"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>用户调用次数排行 TOP10</span>
</div>
</template>
<div ref="callChartRef" class="chart-container"></div>
</el-card>
</el-col>
</el-row>
<!-- 数据表格 -->
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>用户详细统计</span>
</div>
</template>
<el-table v-loading="loading" :data="dataList" :stripe="true">
<el-table-column label="用户ID" align="center" prop="userId" width="100" />
<el-table-column label="手机号" align="center" prop="mobile" width="130" />
<el-table-column label="调用次数" align="center" prop="callCount" sortable>
<template #default="{ row }">{{ row.callCount?.toLocaleString() }}</template>
</el-table-column>
<el-table-column label="消耗积分" align="center" prop="consumePoints" sortable>
<template #default="{ row }">{{ row.consumePoints?.toLocaleString() }}</template>
</el-table-column>
<el-table-column label="输入Token" align="center" prop="inputTokens" sortable>
<template #default="{ row }">{{ row.inputTokens?.toLocaleString() || '-' }}</template>
</el-table-column>
<el-table-column label="输出Token" align="center" prop="outputTokens" sortable>
<template #default="{ row }">{{ row.outputTokens?.toLocaleString() || '-' }}</template>
</el-table-column>
<el-table-column label="总Token" align="center" prop="totalTokens" sortable>
<template #default="{ row }">{{ row.totalTokens?.toLocaleString() || '-' }}</template>
</el-table-column>
<el-table-column label="平均积分/次" align="center" prop="avgPointsPerCall" sortable>
<template #default="{ row }">{{ row.avgPointsPerCall?.toFixed(2) }}</template>
</el-table-column>
</el-table>
<Pagination
v-model:page="pageParams.pageNo"
v-model:limit="pageParams.pageSize"
:total="pageParams.total"
@pagination="getDataList"
/>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import { getAiUsageUserStatsPage, type AiUsageUserStats } from '@/api/muye/aiusagestats'
interface Props {
timeRange: string[]
bizType: string
}
const props = defineProps<Props>()
// 查询参数
const queryParams = reactive({
mobile: undefined as string | undefined
})
// 分页参数
const pageParams = reactive({
pageNo: 1,
pageSize: 10,
total: 0
})
// 数据
const loading = ref(false)
const dataList = ref<AiUsageUserStats[]>([])
const allUserData = ref<AiUsageUserStats[]>([])
// 汇总数据
const summary = computed(() => {
const activeUserCount = allUserData.value.length
const totalCallCount = allUserData.value.reduce((sum, item) => sum + (item.callCount || 0), 0)
const totalConsumePoints = allUserData.value.reduce((sum, item) => sum + (item.consumePoints || 0), 0)
return { activeUserCount, totalCallCount, totalConsumePoints }
})
const avgPointsPerUser = computed(() => {
if (summary.value.activeUserCount === 0) return '0'
return (summary.value.totalConsumePoints / summary.value.activeUserCount).toFixed(2)
})
// 图表
const rankChartRef = ref<HTMLElement>()
const callChartRef = ref<HTMLElement>()
let rankChart: echarts.ECharts | null = null
let callChart: echarts.ECharts | null = null
const initCharts = () => {
if (rankChartRef.value && !rankChart) {
rankChart = echarts.init(rankChartRef.value)
}
if (callChartRef.value && !callChart) {
callChart = echarts.init(callChartRef.value)
}
}
const drawCharts = () => {
initCharts()
// 积分消耗排行 TOP10
const topByPoints = [...allUserData.value]
.sort((a, b) => (b.consumePoints || 0) - (a.consumePoints || 0))
.slice(0, 10)
if (rankChart && topByPoints.length > 0) {
rankChart.setOption({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: { left: 100, right: 20, top: 20, bottom: 20 },
xAxis: { type: 'value', name: '积分' },
yAxis: {
type: 'category',
data: topByPoints.map(d => d.mobile || `用户${d.userId}`).reverse(),
axisLabel: { width: 80, overflow: 'truncate' }
},
series: [{
type: 'bar',
data: topByPoints.map(d => d.consumePoints).reverse(),
itemStyle: { color: '#e6a23c' }
}]
})
}
// 调用次数排行 TOP10
const topByCalls = [...allUserData.value]
.sort((a, b) => (b.callCount || 0) - (a.callCount || 0))
.slice(0, 10)
if (callChart && topByCalls.length > 0) {
callChart.setOption({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: { left: 100, right: 20, top: 20, bottom: 20 },
xAxis: { type: 'value', name: '次数' },
yAxis: {
type: 'category',
data: topByCalls.map(d => d.mobile || `用户${d.userId}`).reverse(),
axisLabel: { width: 80, overflow: 'truncate' }
},
series: [{
type: 'bar',
data: topByCalls.map(d => d.callCount).reverse(),
itemStyle: { color: '#409eff' }
}]
})
}
}
// 获取数据
const getDataList = async () => {
loading.value = true
try {
const [startTime, endTime] = props.timeRange || []
const res = await getAiUsageUserStatsPage({
pageNo: pageParams.pageNo,
pageSize: pageParams.pageSize,
startTime,
endTime,
bizType: props.bizType,
mobile: queryParams.mobile
})
dataList.value = res?.list || []
pageParams.total = res?.total || 0
} finally {
loading.value = false
}
}
// 获取全部数据用于图表
const getAllDataForChart = async () => {
const [startTime, endTime] = props.timeRange || []
const res = await getAiUsageUserStatsPage({
pageNo: 1,
pageSize: 100,
startTime,
endTime,
bizType: props.bizType,
mobile: queryParams.mobile
})
allUserData.value = res?.list || []
await nextTick()
drawCharts()
}
// 查询
const handleQuery = () => {
pageParams.pageNo = 1
getDataList()
getAllDataForChart()
}
// 重置
const handleReset = () => {
queryParams.mobile = undefined
handleQuery()
}
// 监听外部参数变化
watch(() => [props.timeRange, props.bizType], () => {
handleQuery()
}, { deep: true })
// 窗口大小变化
const handleResize = () => {
rankChart?.resize()
callChart?.resize()
}
onMounted(() => {
handleQuery()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
rankChart?.dispose()
callChart?.dispose()
})
</script>
<style scoped lang="scss">
.user-stats-panel {
.stat-card {
display: flex;
align-items: center;
padding: 8px;
.stat-icon {
width: 48px;
height: 48px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: #fff;
margin-right: 16px;
&.bg-blue { background: linear-gradient(135deg, #409eff, #3375b9); }
&.bg-orange { background: linear-gradient(135deg, #e6a23c, #cf9236); }
&.bg-green { background: linear-gradient(135deg, #67c23a, #529b2e); }
&.bg-purple { background: linear-gradient(135deg, #909399, #73767a); }
}
.stat-content {
.stat-label {
font-size: 14px;
color: #909399;
margin-bottom: 4px;
}
.stat-value {
font-size: 24px;
font-weight: 600;
color: #303133;
}
}
}
.card-header {
font-size: 16px;
font-weight: 500;
}
.chart-container {
height: 280px;
}
}
</style>

View File

@@ -29,7 +29,7 @@
<!-- Tab 切换 -->
<ContentWrap>
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
<el-tabs v-model="activeTab">
<!-- 概览 Tab -->
<el-tab-pane label="概览" name="overview">
<!-- 统计卡片 -->
@@ -136,58 +136,12 @@
<!-- 用户统计 Tab -->
<el-tab-pane label="用户统计" name="user">
<el-table v-loading="userLoading" :data="userStatsList" :stripe="true">
<el-table-column label="用户ID" align="center" prop="userId" />
<el-table-column label="手机号" align="center" prop="mobile" />
<el-table-column label="调用次数" align="center" prop="callCount">
<template #default="{ row }">{{ row.callCount?.toLocaleString() }}</template>
</el-table-column>
<el-table-column label="消耗积分" align="center" prop="consumePoints">
<template #default="{ row }">{{ row.consumePoints?.toLocaleString() }}</template>
</el-table-column>
<el-table-column label="输入Token" align="center" prop="inputTokens">
<template #default="{ row }">{{ row.inputTokens?.toLocaleString() || '-' }}</template>
</el-table-column>
<el-table-column label="输出Token" align="center" prop="outputTokens">
<template #default="{ row }">{{ row.outputTokens?.toLocaleString() || '-' }}</template>
</el-table-column>
<el-table-column label="总Token" align="center" prop="totalTokens">
<template #default="{ row }">{{ row.totalTokens?.toLocaleString() || '-' }}</template>
</el-table-column>
<el-table-column label="平均积分/次" align="center" prop="avgPointsPerCall">
<template #default="{ row }">{{ row.avgPointsPerCall?.toFixed(2) }}</template>
</el-table-column>
</el-table>
<Pagination
v-model:page="userPageParams.pageNo"
v-model:limit="userPageParams.pageSize"
:total="userPageParams.total"
@pagination="getUserStatsList"
/>
<UserStatsPanel v-if="activeTab === 'user'" :time-range="queryParams.timeRange" :biz-type="queryParams.bizType" />
</el-tab-pane>
<!-- 应用统计 Tab -->
<el-tab-pane label="应用统计" name="app">
<el-table v-loading="appLoading" :data="appStatsList" :stripe="true">
<el-table-column label="服务标识" align="center" prop="serviceCode" />
<el-table-column label="服务名称" align="center" prop="serviceName" />
<el-table-column label="平台" align="center" prop="platform" />
<el-table-column label="调用次数" align="center" prop="callCount">
<template #default="{ row }">{{ row.callCount?.toLocaleString() }}</template>
</el-table-column>
<el-table-column label="消耗积分" align="center" prop="consumePoints">
<template #default="{ row }">{{ row.consumePoints?.toLocaleString() }}</template>
</el-table-column>
<el-table-column label="总Token" align="center" prop="totalTokens">
<template #default="{ row }">{{ row.totalTokens?.toLocaleString() || '-' }}</template>
</el-table-column>
<el-table-column label="平均积分/次" align="center" prop="avgPointsPerCall">
<template #default="{ row }">{{ row.avgPointsPerCall?.toFixed(2) }}</template>
</el-table-column>
<el-table-column label="平均Token/次" align="center" prop="avgTokensPerCall">
<template #default="{ row }">{{ row.avgTokensPerCall?.toFixed(2) }}</template>
</el-table-column>
</el-table>
<AppStatsPanel v-if="activeTab === 'app'" :time-range="queryParams.timeRange" :biz-type="queryParams.bizType" />
</el-tab-pane>
</el-tabs>
</ContentWrap>
@@ -198,13 +152,11 @@ import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import {
getAiUsageOverview,
getAiUsageUserStatsPage,
getAiUsageAppStats,
getAiUsageTrend,
type AiUsageOverview,
type AiUsageUserStats,
type AiUsageAppStats
type AiUsageOverview
} from '@/api/muye/aiusagestats'
import UserStatsPanel from './components/UserStatsPanel.vue'
import AppStatsPanel from './components/AppStatsPanel.vue'
defineOptions({ name: 'AiUsageStats' })
@@ -254,19 +206,6 @@ const activeTab = ref('overview')
// 概览数据
const overviewData = ref<AiUsageOverview>({} as AiUsageOverview)
// 用户统计
const userLoading = ref(false)
const userStatsList = ref<AiUsageUserStats[]>([])
const userPageParams = reactive({
pageNo: 1,
pageSize: 10,
total: 0
})
// 应用统计
const appLoading = ref(false)
const appStatsList = ref<AiUsageAppStats[]>([])
// 图表
const trendChartRef = ref<HTMLElement>()
const pieChartRef = ref<HTMLElement>()
@@ -303,10 +242,8 @@ const getTrendData = async () => {
type: 'day'
})
// 确保 DOM 已渲染
await nextTick()
// 初始化图表
if (!initTrendChart()) {
console.warn('趋势图表容器未找到')
return
@@ -342,7 +279,6 @@ const initPieChart = () => {
// 绘制饼图
const drawPieChart = () => {
// 初始化图表
if (!initPieChart()) {
console.warn('饼图容器未找到')
return
@@ -367,61 +303,13 @@ const drawPieChart = () => {
}
}
// 获取用户统计列表
const getUserStatsList = async () => {
userLoading.value = true
try {
const [startTime, endTime] = queryParams.timeRange || []
const res = await getAiUsageUserStatsPage({
pageNo: userPageParams.pageNo,
pageSize: userPageParams.pageSize,
startTime,
endTime,
bizType: queryParams.bizType
})
userStatsList.value = res?.list || []
userPageParams.total = res?.total || 0
} finally {
userLoading.value = false
}
}
// 获取应用统计列表
const getAppStatsList = async () => {
appLoading.value = true
try {
const [startTime, endTime] = queryParams.timeRange || []
const res = await getAiUsageAppStats({
startTime,
endTime,
bizType: queryParams.bizType
})
appStatsList.value = res || []
} finally {
appLoading.value = false
}
}
// 查询
const handleQuery = async () => {
if (activeTab.value !== 'overview') return
await getOverviewData()
await getTrendData()
await nextTick()
drawPieChart()
if (activeTab.value === 'user') {
getUserStatsList()
} else if (activeTab.value === 'app') {
getAppStatsList()
}
}
// Tab 切换
const handleTabChange = (tab: string) => {
if (tab === 'user') {
getUserStatsList()
} else if (tab === 'app') {
getAppStatsList()
}
}
// 窗口大小变化时重绘图表