|
|
@@ -0,0 +1,632 @@
|
|
|
+<template>
|
|
|
+ <div class="course-statistics-container">
|
|
|
+ <!-- 筛选条件 -->
|
|
|
+ <el-form :inline="true" :model="queryParams" class="demo-form-inline" style="margin-bottom: 20px;">
|
|
|
+ <el-form-item label="小节名称">
|
|
|
+ <el-input
|
|
|
+ v-model="queryParams.videoName"
|
|
|
+ placeholder="请输入小节名称关键字"
|
|
|
+ clearable
|
|
|
+ size="small"
|
|
|
+ style="width: 300px;"
|
|
|
+ @keyup.enter.native="handleQuery"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" size="small" @click="handleQuery">查询</el-button>
|
|
|
+ <el-button size="small" @click="resetQuery">重置</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <!-- 数据表格 -->
|
|
|
+ <el-table
|
|
|
+ v-loading="loading"
|
|
|
+ :data="list"
|
|
|
+ border
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ <el-table-column label="课程名称" align="center" prop="courseName" width="200" />
|
|
|
+ <el-table-column label="小节" align="center" prop="videoName" width="210" />
|
|
|
+<!-- <el-table-column label="小节状态" align="center" prop="videoStatus" width="120">-->
|
|
|
+<!-- <template slot-scope="scope">-->
|
|
|
+<!-- <el-tag :type="scope.row.videoStatus === '已开课' ? 'success' : scope.row.videoStatus === '已结束' ? 'info' : 'warning'">-->
|
|
|
+<!-- {{ scope.row.videoStatus }}-->
|
|
|
+<!-- </el-tag>-->
|
|
|
+<!-- </template>-->
|
|
|
+<!-- </el-table-column>-->
|
|
|
+ <el-table-column label="开课状态" align="center" prop="openStatus" width="120">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-tag :type="scope.row.openStatus === '已开课' ? 'success' : scope.row.openStatus === '已结束' ? 'info' : 'warning'">
|
|
|
+ {{ scope.row.openStatus }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="营期时间" align="center" prop="periodDate" width="120" />
|
|
|
+ <el-table-column label="开始时间" align="center" prop="startTime" width="180" />
|
|
|
+ <el-table-column label="结束时间" align="center" prop="endTime" width="180" />
|
|
|
+ <el-table-column label="操作" align="center" fixed="right">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-button
|
|
|
+ type="text"
|
|
|
+ size="small"
|
|
|
+ @click="handleViewDetail(scope.row)"
|
|
|
+ >
|
|
|
+ 查看详情
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <!-- 分页 -->
|
|
|
+ <pagination
|
|
|
+ v-show="total > 0"
|
|
|
+ :total="total"
|
|
|
+ :page.sync="queryParams.pageNum"
|
|
|
+ :limit.sync="queryParams.pageSize"
|
|
|
+ @pagination="getList"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 课程详情抽屉 -->
|
|
|
+ <el-drawer
|
|
|
+ title="课程小结详情"
|
|
|
+ :visible.sync="detailDialog.visible"
|
|
|
+ direction="rtl"
|
|
|
+ size="60%"
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ append-to-body
|
|
|
+ class="course-detail-drawer"
|
|
|
+ >
|
|
|
+ <div v-loading="detailDialog.loading" style="padding: 20px;">
|
|
|
+ <!-- 第一块:总体数据 -->
|
|
|
+ <el-card class="detail-card" shadow="never">
|
|
|
+ <div slot="header" class="card-header">
|
|
|
+ <span>总体数据</span>
|
|
|
+ <el-button type="primary" size="small" @click="handleViewUserDetail">查看用户详情</el-button>
|
|
|
+ </div>
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">视频时长</div>
|
|
|
+ <div class="stat-value">{{ formatDuration(detailDialog.data.videoDuration) }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">累计观看人数</div>
|
|
|
+ <div class="stat-value">{{ detailDialog.data.totalWatchCount || 0 }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">累计完课人数</div>
|
|
|
+ <div class="stat-value">{{ detailDialog.data.totalCompleteCount || 0 }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">到课完课率</div>
|
|
|
+ <div class="stat-value">{{ detailDialog.data.completeRate || '0%' }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 第二块:首次点播数据 -->
|
|
|
+ <el-card class="detail-card" shadow="never">
|
|
|
+ <div slot="header" class="card-header">
|
|
|
+ <span>首次点播数据</span>
|
|
|
+ </div>
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">观看人数</div>
|
|
|
+ <div class="stat-value">{{ detailDialog.data.firstWatchCount || 0 }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">>=20分钟人数(首次)</div>
|
|
|
+ <div class="stat-value">{{ detailDialog.data.firstWatch20MinCount || 0 }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">>=30分钟人数(首次)</div>
|
|
|
+ <div class="stat-value">{{ detailDialog.data.firstWatch30MinCount || 0 }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">到课完课率首次(>=20分钟)</div>
|
|
|
+ <div class="stat-value">{{ detailDialog.data.firstCompleteRate20Min || '0%' }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">到课完课率首次(>=30分钟)</div>
|
|
|
+ <div class="stat-value">{{ detailDialog.data.firstCompleteRate30Min || '0%' }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 第三块:第2-n次观看数据 -->
|
|
|
+ <el-card class="detail-card" shadow="never">
|
|
|
+ <div slot="header" class="card-header">
|
|
|
+ <span>第2-n次观看数据</span>
|
|
|
+ </div>
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">观看人数</div>
|
|
|
+ <div class="stat-value">{{ detailDialog.data.repeatWatchCount || 0 }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">>=20分钟人数</div>
|
|
|
+ <div class="stat-value">{{ detailDialog.data.repeatWatch20MinCount || 0 }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">>=30分钟人数</div>
|
|
|
+ <div class="stat-value">{{ detailDialog.data.repeatWatch30MinCount || 0 }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">到课完课率2-n次(>=20分钟)</div>
|
|
|
+ <div class="stat-value">{{ detailDialog.data.repeatCompleteRate20Min || '0%' }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">到课完课率2-n次(>=30分钟)</div>
|
|
|
+ <div class="stat-value">{{ detailDialog.data.repeatCompleteRate30Min || '0%' }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 第四块:订单数据 -->
|
|
|
+ <el-card class="detail-card" shadow="never">
|
|
|
+ <div slot="header" class="card-header">
|
|
|
+ <span>订单数据</span>
|
|
|
+ </div>
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">GMV</div>
|
|
|
+ <div class="stat-value">¥{{ detailDialog.data.gmv || '0.00' }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">付费人数</div>
|
|
|
+ <div class="stat-value">{{ detailDialog.data.paidUserCount || 0 }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">付费单数</div>
|
|
|
+ <div class="stat-value">{{ detailDialog.data.paidOrderCount || 0 }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">总付费转换率</div>
|
|
|
+ <div class="stat-value">{{ detailDialog.data.totalPaidConversionRate || '0%' }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">20min付费转化率</div>
|
|
|
+ <div class="stat-value">{{ detailDialog.data.paidConversionRate20Min || '0%' }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">完课R值</div>
|
|
|
+ <div class="stat-value">{{ detailDialog.data.completeRValue || '0.00' }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">领红包人数</div>
|
|
|
+ <div class="stat-value">{{ detailDialog.data.redPacketUserCount || 0 }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">答题人数</div>
|
|
|
+ <div class="stat-value">{{ detailDialog.data.answerUserCount || 0 }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 第五块:单品销量统计 -->
|
|
|
+ <el-card class="detail-card" shadow="never">
|
|
|
+ <div slot="header" class="card-header">
|
|
|
+ <span>单品销量统计</span>
|
|
|
+ </div>
|
|
|
+ <el-table
|
|
|
+ :data="detailDialog.data.productList || []"
|
|
|
+ border
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ <el-table-column label="商品名称" align="center" prop="productName" />
|
|
|
+ <el-table-column label="销量" align="center" prop="salesCount" />
|
|
|
+ <el-table-column label="销售额" align="center" prop="salesAmount">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ ¥{{ scope.row.salesAmount || '0.00' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+ </el-drawer>
|
|
|
+
|
|
|
+ <!-- 用户详情抽屉 -->
|
|
|
+ <el-drawer
|
|
|
+ title="用户看课数据"
|
|
|
+ :visible.sync="userDetailDialog.visible"
|
|
|
+ direction="rtl"
|
|
|
+ size="60%"
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ append-to-body
|
|
|
+ >
|
|
|
+ <div v-loading="userDetailDialog.loading" style="padding: 20px;">
|
|
|
+ <div style="margin-bottom: 20px; text-align: right;">
|
|
|
+ <el-button type="primary" size="small" @click="handleExportUserDetail">导出用户详情</el-button>
|
|
|
+ </div>
|
|
|
+ <el-table
|
|
|
+ :data="userDetailDialog.list"
|
|
|
+ border
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ <el-table-column label="用户名称" align="center" prop="userName" width="150" />
|
|
|
+ <el-table-column label="观看时长" align="center" prop="watchDuration" width="120">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ {{ formatDuration(scope.row.watchDuration) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="第2-n次观看时长" align="center" prop="repeatWatchDuration" width="150">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ {{ formatDuration(scope.row.repeatWatchDuration) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="订单数" align="center" prop="orderCount" width="100" />
|
|
|
+ <el-table-column label="订单金额" align="center" prop="orderAmount" width="120">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ ¥{{ scope.row.orderAmount || '0.00' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="分公司名称" align="center" prop="companyName" width="150" />
|
|
|
+ <el-table-column label="销售名称" align="center" prop="salesName" />
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <!-- 分页 -->
|
|
|
+ <pagination
|
|
|
+ v-show="userDetailDialog.total > 0"
|
|
|
+ :total="userDetailDialog.total"
|
|
|
+ :page.sync="userDetailDialog.queryParams.pageNum"
|
|
|
+ :limit.sync="userDetailDialog.queryParams.pageSize"
|
|
|
+ @pagination="getUserDetailList"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-drawer>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { getDays } from "@/api/course/userCoursePeriod";
|
|
|
+import { getCourseStatisticsDetail, getCourseStatisticsUserDetailList, exportCourseStatisticsUserDetail } from "@/api/course/courseWatchLog";
|
|
|
+import { download } from "@/utils/common";
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: "CourseStatistics",
|
|
|
+ props: {
|
|
|
+ periodId: {
|
|
|
+ type: [String, Number],
|
|
|
+ default: null
|
|
|
+ },
|
|
|
+ active: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ loading: false,
|
|
|
+ list: [],
|
|
|
+ total: 0,
|
|
|
+ queryParams: {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ videoName: null,
|
|
|
+ periodId: null
|
|
|
+ },
|
|
|
+ // 详情弹窗
|
|
|
+ detailDialog: {
|
|
|
+ visible: false,
|
|
|
+ loading: false,
|
|
|
+ data: {}
|
|
|
+ },
|
|
|
+ // 用户详情弹窗
|
|
|
+ userDetailDialog: {
|
|
|
+ visible: false,
|
|
|
+ loading: false,
|
|
|
+ list: [],
|
|
|
+ total: 0,
|
|
|
+ queryParams: {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ videoId: null,
|
|
|
+ periodId: null
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ active(newVal) {
|
|
|
+ if (newVal && this.periodId) {
|
|
|
+ this.queryParams.periodId = this.periodId;
|
|
|
+ this.getList();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ periodId(newVal) {
|
|
|
+ if (newVal && this.active) {
|
|
|
+ this.queryParams.periodId = newVal;
|
|
|
+ this.getList();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ if (this.active && this.periodId) {
|
|
|
+ this.queryParams.periodId = this.periodId;
|
|
|
+ this.getList();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ /** 查询列表 */
|
|
|
+ getList() {
|
|
|
+ if (!this.queryParams.periodId) {
|
|
|
+ this.loading = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.loading = true;
|
|
|
+ getDays(this.queryParams).then(response => {
|
|
|
+ if (response.code === 200) {
|
|
|
+ // 映射字段并格式化数据
|
|
|
+ this.list = (response.rows || []).map(item => {
|
|
|
+ return {
|
|
|
+ ...item,
|
|
|
+ // 映射字段
|
|
|
+ periodDate: item.dayDate,
|
|
|
+ startTime: item.startDateTime,
|
|
|
+ endTime: item.endDateTime,
|
|
|
+ // 格式化开课状态
|
|
|
+ openStatus: this.formatCourseStatus(item.status),
|
|
|
+ // 小节状态(可以根据需要设置,这里先使用开课状态)
|
|
|
+ videoStatus: this.formatCourseStatus(item.status)
|
|
|
+ };
|
|
|
+ });
|
|
|
+ this.total = response.total || 0;
|
|
|
+ } else {
|
|
|
+ this.$message.error(response.msg || '查询失败');
|
|
|
+ this.list = [];
|
|
|
+ this.total = 0;
|
|
|
+ }
|
|
|
+ this.loading = false;
|
|
|
+ }).catch(error => {
|
|
|
+ this.$message.error('查询失败:' + (error.message || '未知错误'));
|
|
|
+ this.loading = false;
|
|
|
+ this.list = [];
|
|
|
+ this.total = 0;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 格式化课程状态 */
|
|
|
+ formatCourseStatus(status) {
|
|
|
+ const statusMap = {
|
|
|
+ 0: '未开始',
|
|
|
+ 1: '已开课',
|
|
|
+ 2: '已结束'
|
|
|
+ };
|
|
|
+ return statusMap[status] || '未知状态';
|
|
|
+ },
|
|
|
+ /** 搜索按钮操作 */
|
|
|
+ handleQuery() {
|
|
|
+ this.queryParams.pageNum = 1;
|
|
|
+ this.getList();
|
|
|
+ },
|
|
|
+ /** 重置按钮操作 */
|
|
|
+ resetQuery() {
|
|
|
+ this.queryParams.videoName = null;
|
|
|
+ this.queryParams.pageNum = 1;
|
|
|
+ this.getList();
|
|
|
+ },
|
|
|
+ /** 查看详情 */
|
|
|
+ handleViewDetail(row) {
|
|
|
+ this.detailDialog.visible = true;
|
|
|
+ this.detailDialog.loading = true;
|
|
|
+
|
|
|
+ // 调用API获取总体数据
|
|
|
+ const videoId = row.videoId || row.id;
|
|
|
+ const periodId = this.queryParams.periodId;
|
|
|
+
|
|
|
+ if (!videoId || !periodId) {
|
|
|
+ this.$message.error('视频ID或营期ID不能为空');
|
|
|
+ this.detailDialog.loading = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ getCourseStatisticsDetail(videoId, periodId).then(response => {
|
|
|
+ if (response.code === 200 && response.data) {
|
|
|
+ const data = response.data;
|
|
|
+ // 设置总体数据
|
|
|
+ this.detailDialog.data = {
|
|
|
+ videoDuration: data.videoDuration || 0,
|
|
|
+ totalWatchCount: data.totalWatchCount || 0,
|
|
|
+ totalCompleteCount: data.totalCompleteCount || 0,
|
|
|
+ completeRate: data.completeRate != null ? Number(data.completeRate).toFixed(2) + '%' : '0%',
|
|
|
+ // 首次点播数据(接口返回)
|
|
|
+ firstWatchCount: data.firstWatchCount ?? 0,
|
|
|
+ firstWatch20MinCount: data.firstWatch20MinCount ?? 0,
|
|
|
+ firstWatch30MinCount: data.firstWatch30MinCount ?? 0,
|
|
|
+ firstCompleteRate20Min: data.firstCompleteRate20Min != null ? Number(data.firstCompleteRate20Min).toFixed(2) + '%' : '0%',
|
|
|
+ firstCompleteRate30Min: data.firstCompleteRate30Min != null ? Number(data.firstCompleteRate30Min).toFixed(2) + '%' : '0%',
|
|
|
+ // 第2-n次观看数据(接口返回)
|
|
|
+ repeatWatchCount: data.repeatWatchCount ?? 0,
|
|
|
+ repeatWatch20MinCount: data.repeatWatch20MinCount ?? 0,
|
|
|
+ repeatWatch30MinCount: data.repeatWatch30MinCount ?? 0,
|
|
|
+ repeatCompleteRate20Min: data.repeatCompleteRate20Min != null ? Number(data.repeatCompleteRate20Min).toFixed(2) + '%' : '0%',
|
|
|
+ repeatCompleteRate30Min: data.repeatCompleteRate30Min != null ? Number(data.repeatCompleteRate30Min).toFixed(2) + '%' : '0%',
|
|
|
+ // 订单数据(接口返回)
|
|
|
+ gmv: data.gmv != null ? Number(data.gmv).toFixed(2) : '0.00',
|
|
|
+ paidUserCount: data.paidUserCount ?? 0,
|
|
|
+ paidOrderCount: data.paidOrderCount ?? 0,
|
|
|
+ totalPaidConversionRate: data.totalPaidConversionRate != null ? Number(data.totalPaidConversionRate).toFixed(2) + '%' : '0%',
|
|
|
+ paidConversionRate20Min: data.paidConversionRate20Min != null ? Number(data.paidConversionRate20Min).toFixed(2) + '%' : '0%',
|
|
|
+ completeRValue: data.completeRValue != null ? Number(data.completeRValue).toFixed(2) : '0.00',
|
|
|
+ redPacketUserCount: data.redPacketUserCount ?? 0,
|
|
|
+ answerUserCount: data.answerUserCount ?? 0,
|
|
|
+ productList: (data.productList || []).map(p => ({
|
|
|
+ productName: p.productName || '未知商品',
|
|
|
+ salesCount: p.salesCount ?? 0,
|
|
|
+ salesAmount: p.salesAmount != null ? Number(p.salesAmount).toFixed(2) : '0.00'
|
|
|
+ })),
|
|
|
+ videoId: videoId,
|
|
|
+ id: row.id
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ this.$message.error(response.msg || '获取数据失败');
|
|
|
+ }
|
|
|
+ this.detailDialog.loading = false;
|
|
|
+ }).catch(error => {
|
|
|
+ this.$message.error('获取数据失败:' + (error.message || '未知错误'));
|
|
|
+ this.detailDialog.loading = false;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 查看用户详情 */
|
|
|
+ handleViewUserDetail() {
|
|
|
+ this.userDetailDialog.visible = true;
|
|
|
+ this.userDetailDialog.queryParams.videoId = this.detailDialog.data.videoId || this.detailDialog.data.id;
|
|
|
+ this.userDetailDialog.queryParams.periodId = this.queryParams.periodId;
|
|
|
+ this.userDetailDialog.queryParams.pageNum = 1;
|
|
|
+ this.getUserDetailList();
|
|
|
+ },
|
|
|
+ /** 获取用户详情列表 */
|
|
|
+ getUserDetailList() {
|
|
|
+ const videoId = this.userDetailDialog.queryParams.videoId;
|
|
|
+ const periodId = this.userDetailDialog.queryParams.periodId;
|
|
|
+ if (!videoId || !periodId) {
|
|
|
+ this.$message.error('视频ID或营期ID不能为空');
|
|
|
+ this.userDetailDialog.loading = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.userDetailDialog.loading = true;
|
|
|
+ getCourseStatisticsUserDetailList(this.userDetailDialog.queryParams).then(response => {
|
|
|
+ if (response.code === 200 && response.data) {
|
|
|
+ const d = response.data;
|
|
|
+ this.userDetailDialog.list = d.list || d.rows || [];
|
|
|
+ this.userDetailDialog.total = d.total ?? 0;
|
|
|
+ } else {
|
|
|
+ this.userDetailDialog.list = [];
|
|
|
+ this.userDetailDialog.total = 0;
|
|
|
+ }
|
|
|
+ this.userDetailDialog.loading = false;
|
|
|
+ }).catch(() => {
|
|
|
+ this.userDetailDialog.list = [];
|
|
|
+ this.userDetailDialog.total = 0;
|
|
|
+ this.userDetailDialog.loading = false;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 导出用户详情(按创建时间倒序,最多50000条) */
|
|
|
+ handleExportUserDetail() {
|
|
|
+ const videoId = this.userDetailDialog.queryParams.videoId;
|
|
|
+ const periodId = this.userDetailDialog.queryParams.periodId;
|
|
|
+ if (!videoId || !periodId) {
|
|
|
+ this.$message.error('请先查看用户详情');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.$confirm('是否确认导出用户看课数据?(按创建时间倒序,最多50000条)', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }).then(() => {
|
|
|
+ exportCourseStatisticsUserDetail(videoId, periodId).then(response => {
|
|
|
+ if (response.code === 200 && response.msg) {
|
|
|
+ download(response.msg);
|
|
|
+ this.$message.success('导出成功');
|
|
|
+ } else {
|
|
|
+ this.$message.error(response.msg || '导出失败');
|
|
|
+ }
|
|
|
+ }).catch(() => {
|
|
|
+ this.$message.error('导出失败');
|
|
|
+ });
|
|
|
+ }).catch(() => {});
|
|
|
+ },
|
|
|
+ /** 格式化时长 */
|
|
|
+ formatDuration(seconds) {
|
|
|
+ if (!seconds) return '0秒';
|
|
|
+ const hours = Math.floor(seconds / 3600);
|
|
|
+ const minutes = Math.floor((seconds % 3600) / 60);
|
|
|
+ const secs = seconds % 60;
|
|
|
+ if (hours > 0) {
|
|
|
+ return `${hours}小时${minutes}分钟${secs}秒`;
|
|
|
+ } else if (minutes > 0) {
|
|
|
+ return `${minutes}分钟${secs}秒`;
|
|
|
+ } else {
|
|
|
+ return `${secs}秒`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.course-statistics-container {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-card {
|
|
|
+ margin-bottom: 20px;
|
|
|
+
|
|
|
+ .card-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-item {
|
|
|
+ text-align: center;
|
|
|
+ padding: 20px;
|
|
|
+ border: 1px solid #EBEEF5;
|
|
|
+ border-radius: 4px;
|
|
|
+ background-color: #F5F7FA;
|
|
|
+ margin-top: 10px;
|
|
|
+
|
|
|
+ .stat-label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #606266;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-value {
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #303133;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.course-detail-drawer {
|
|
|
+ ::v-deep .el-drawer__body {
|
|
|
+ overflow-y: auto;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|