修复
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -78,13 +78,15 @@ public interface PointRecordMapper extends BaseMapperX<PointRecordDO> {
|
||||
"<if test='endTime != null'> AND create_time <= #{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);
|
||||
|
||||
/**
|
||||
* 按应用统计
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
// 窗口大小变化时重绘图表
|
||||
|
||||
Reference in New Issue
Block a user