|
|
@@ -309,11 +309,259 @@
|
|
|
<el-button type="primary" @click="confirmAutoRemark">确 定</el-button>
|
|
|
</div>
|
|
|
</el-dialog>
|
|
|
+
|
|
|
+ <!-- 详情侧边栏 -->
|
|
|
+ <el-drawer
|
|
|
+ title="直播间详情"
|
|
|
+ :visible.sync="detailDrawerVisible"
|
|
|
+ direction="rtl"
|
|
|
+ size="60%"
|
|
|
+ :before-close="closeDetailDrawer"
|
|
|
+ >
|
|
|
+ <div v-if="!showUserDetail" class="detail-content">
|
|
|
+ <el-card shadow="never" style="margin-bottom: 20px;">
|
|
|
+ <div slot="header">
|
|
|
+ <span>总体数据</span>
|
|
|
+ <el-button style="float: right; padding: 3px 0" type="text" @click="handleViewUserDetail">查看用户详情</el-button>
|
|
|
+ </div>
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">视频时长:</span>
|
|
|
+ <span class="detail-value">{{ formatDuration(detailData.videoDuration || 0) }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">累计观看人数:</span>
|
|
|
+ <span class="detail-value">{{ detailData.totalViewers || 0 }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">累计完课人数:</span>
|
|
|
+ <span class="detail-value">{{ detailData.totalCompletedCourses || 0 }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">到课完课率:</span>
|
|
|
+ <span class="detail-value">{{ formatPercent(detailData.totalCompletionRate) }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-card shadow="never" style="margin-bottom: 20px;">
|
|
|
+ <div slot="header">直播数据</div>
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">直播观看人数:</span>
|
|
|
+ <span class="detail-value">{{ detailData.liveViewers || 0 }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">>20分钟人数(直播):</span>
|
|
|
+ <span class="detail-value">{{ detailData.liveOver20Minutes || 0 }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">>30分钟人数(直播):</span>
|
|
|
+ <span class="detail-value">{{ detailData.liveOver30Minutes || 0 }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">到课完课率直播(>20分钟):</span>
|
|
|
+ <span class="detail-value">{{ formatPercent(detailData.liveCompletionRate20) }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">到课完课率直播(>30分钟):</span>
|
|
|
+ <span class="detail-value">{{ formatPercent(detailData.liveCompletionRate30) }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-card shadow="never" style="margin-bottom: 20px;">
|
|
|
+ <div slot="header">回放数据</div>
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">回放观看人数:</span>
|
|
|
+ <span class="detail-value">{{ detailData.playbackViewers || 0 }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">>20分钟人数(回放):</span>
|
|
|
+ <span class="detail-value">{{ detailData.playbackOver20Minutes || 0 }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">>30分钟人数(回放):</span>
|
|
|
+ <span class="detail-value">{{ detailData.playbackOver30Minutes || 0 }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">到课完课率回放(>20分钟):</span>
|
|
|
+ <span class="detail-value">{{ formatPercent(detailData.playbackCompletionRate20) }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">到课完课率回放(>30分钟):</span>
|
|
|
+ <span class="detail-value">{{ formatPercent(detailData.playbackCompletionRate30) }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-card shadow="never" style="margin-bottom: 20px;">
|
|
|
+ <div slot="header">时长统计</div>
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">直播峰值:</span>
|
|
|
+ <span class="detail-value">{{ detailData.livePeak || 0 }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">直播平均时长:</span>
|
|
|
+ <span class="detail-value">{{ formatDuration(detailData.liveAvgDuration || 0) }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">回放平均时长:</span>
|
|
|
+ <span class="detail-value">{{ formatDuration(detailData.playbackAvgDuration || 0) }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">回放完播率:</span>
|
|
|
+ <span class="detail-value">{{ formatPercent(detailData.playbackFinishRate) }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-card shadow="never" style="margin-bottom: 20px;">
|
|
|
+ <div slot="header">订单数据</div>
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">GMV:</span>
|
|
|
+ <span class="detail-value">{{ formatMoney(detailData.gmv) }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">付费人数:</span>
|
|
|
+ <span class="detail-value">{{ detailData.paidUsers || 0 }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">付费单数:</span>
|
|
|
+ <span class="detail-value">{{ detailData.paidOrders || 0 }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">峰值转化率:</span>
|
|
|
+ <span class="detail-value">{{ formatPercent(detailData.peakConversionRate) }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">总到课转化率:</span>
|
|
|
+ <span class="detail-value">{{ formatPercent(detailData.totalViewerConversionRate) }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">30min完课转化率:</span>
|
|
|
+ <span class="detail-value">{{ formatPercent(detailData.completion30MinConversionRate) }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">峰值R值:</span>
|
|
|
+ <span class="detail-value">{{ formatMoney(detailData.peakRValue) }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="detail-item">
|
|
|
+ <span class="detail-label">完课R值:</span>
|
|
|
+ <span class="detail-value">{{ formatMoney(detailData.completionRValue) }}</span>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-card shadow="never" v-if="detailData.productSalesList && detailData.productSalesList.length > 0">
|
|
|
+ <div slot="header">单品销量统计</div>
|
|
|
+ <el-table :data="detailData.productSalesList" border>
|
|
|
+ <el-table-column prop="productName" label="商品名称" min-width="200"></el-table-column>
|
|
|
+ <el-table-column prop="salesCount" label="销量" width="100" align="center"></el-table-column>
|
|
|
+ <el-table-column prop="salesAmount" label="销售额" width="150" align="center">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ {{ formatMoney(scope.row.salesAmount) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 用户详情列表 -->
|
|
|
+ <div v-else class="user-detail-content">
|
|
|
+ <div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
|
|
|
+ <el-button type="text" icon="el-icon-arrow-left" @click="showUserDetail = false">返回</el-button>
|
|
|
+ <el-button type="primary" size="small" icon="el-icon-download" :loading="userDetailExportLoading" @click="handleExportUserDetail">导出用户详情</el-button>
|
|
|
+ </div>
|
|
|
+ <el-table
|
|
|
+ :data="userDetailList"
|
|
|
+ v-loading="userDetailLoading"
|
|
|
+ border
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ <el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
|
|
|
+ <el-table-column prop="userName" label="用户名称" min-width="120"></el-table-column>
|
|
|
+ <el-table-column prop="liveWatchDuration" label="直播观看时长" width="140" align="center">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ {{ formatDuration(scope.row.liveWatchDuration || 0) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="playbackWatchDuration" label="回放观看时长" width="140" align="center">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ {{ formatDuration(scope.row.playbackWatchDuration || 0) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="orderCount" label="订单数" width="100" align="center"></el-table-column>
|
|
|
+ <el-table-column prop="orderAmount" label="订单金额" width="120" align="center">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ {{ formatMoney(scope.row.orderAmount) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="companyName" label="分公司" min-width="150"></el-table-column>
|
|
|
+ <el-table-column prop="salesName" label="销售" min-width="120"></el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </div>
|
|
|
+ </el-drawer>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import { listLiveData, exportLiveData, autoTagAndRemark, dashboardData } from "@/api/live/liveData";
|
|
|
+import { listLiveData, exportLiveData, autoTagAndRemark, dashboardData, getLiveDataDetailBySql, getLiveUserDetailListBySql, exportLiveUserDetail } from "@/api/live/liveData";
|
|
|
import { batchUpdateExternalContactNotes } from "@/api/qw/externalContact";
|
|
|
import { addTag } from "@/api/qw/externalContact";
|
|
|
|
|
|
@@ -377,7 +625,15 @@ export default {
|
|
|
rules: [],
|
|
|
remarkFormat: 'time',
|
|
|
position: 1
|
|
|
- }
|
|
|
+ },
|
|
|
+ // 详情侧边栏
|
|
|
+ detailDrawerVisible: false,
|
|
|
+ currentLiveId: null,
|
|
|
+ detailData: {},
|
|
|
+ userDetailList: [],
|
|
|
+ userDetailLoading: false,
|
|
|
+ showUserDetail: false,
|
|
|
+ userDetailExportLoading: false
|
|
|
};
|
|
|
},
|
|
|
created() {
|
|
|
@@ -582,8 +838,9 @@ export default {
|
|
|
},
|
|
|
/** 查看详情 */
|
|
|
handleViewDetail(row) {
|
|
|
- // 可以跳转到详情页面或打开详情弹窗
|
|
|
- this.$message.info('查看详情功能待实现');
|
|
|
+ this.currentLiveId = row.liveId;
|
|
|
+ this.detailDrawerVisible = true;
|
|
|
+ this.loadDetailData();
|
|
|
},
|
|
|
/** 格式化时长 */
|
|
|
formatDuration(seconds) {
|
|
|
@@ -609,6 +866,75 @@ export default {
|
|
|
// this.getStatistics();
|
|
|
this.getList();
|
|
|
this.$message.success('设置成功');
|
|
|
+ },
|
|
|
+ /** 加载详情数据 */
|
|
|
+ loadDetailData() {
|
|
|
+ if (!this.currentLiveId) return;
|
|
|
+ // 使用SQL方式获取详情数据
|
|
|
+ getLiveDataDetailBySql(this.currentLiveId).then(response => {
|
|
|
+ if (response.code === 200) {
|
|
|
+ this.detailData = response.data || {};
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 查看用户详情 */
|
|
|
+ handleViewUserDetail() {
|
|
|
+ if (!this.currentLiveId) return;
|
|
|
+ this.showUserDetail = true;
|
|
|
+ this.userDetailLoading = true;
|
|
|
+ getLiveUserDetailListBySql(this.currentLiveId).then(response => {
|
|
|
+ if (response.code === 200) {
|
|
|
+ this.userDetailList = response.data || [];
|
|
|
+ }
|
|
|
+ this.userDetailLoading = false;
|
|
|
+ }).catch(() => {
|
|
|
+ this.userDetailLoading = false;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 关闭详情侧边栏 */
|
|
|
+ closeDetailDrawer() {
|
|
|
+ this.detailDrawerVisible = false;
|
|
|
+ this.showUserDetail = false;
|
|
|
+ this.detailData = {};
|
|
|
+ this.userDetailList = [];
|
|
|
+ this.currentLiveId = null;
|
|
|
+ },
|
|
|
+ /** 格式化百分比 */
|
|
|
+ formatPercent(value) {
|
|
|
+ if (value == null || value === undefined) return '0%';
|
|
|
+ return (typeof value === 'number' ? value : parseFloat(value)).toFixed(2) + '%';
|
|
|
+ },
|
|
|
+ /** 格式化金额 */
|
|
|
+ formatMoney(value) {
|
|
|
+ if (value == null || value === undefined) return '¥0.00';
|
|
|
+ return '¥' + (typeof value === 'number' ? value : parseFloat(value)).toFixed(2);
|
|
|
+ },
|
|
|
+ /** 导出用户详情数据 */
|
|
|
+ handleExportUserDetail() {
|
|
|
+ if (!this.currentLiveId) {
|
|
|
+ this.$message.warning('无法获取直播间ID');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!this.userDetailList || this.userDetailList.length === 0) {
|
|
|
+ this.$message.warning('暂无用户详情数据可导出');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.$confirm('是否确认导出当前直播间的用户详情数据?', "提示", {
|
|
|
+ confirmButtonText: "确定",
|
|
|
+ cancelButtonText: "取消",
|
|
|
+ type: "warning"
|
|
|
+ }).then(() => {
|
|
|
+ this.userDetailExportLoading = true;
|
|
|
+ return exportLiveUserDetail(this.currentLiveId);
|
|
|
+ }).then(response => {
|
|
|
+ if (response.code === 200) {
|
|
|
+ this.download(response.msg);
|
|
|
+ this.$message.success('导出成功');
|
|
|
+ }
|
|
|
+ this.userDetailExportLoading = false;
|
|
|
+ }).catch(() => {
|
|
|
+ this.userDetailExportLoading = false;
|
|
|
+ });
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
@@ -656,4 +982,28 @@ export default {
|
|
|
padding-left: 10px;
|
|
|
padding-top: 30px;
|
|
|
}
|
|
|
+
|
|
|
+.detail-content {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-item {
|
|
|
+ margin-bottom: 15px;
|
|
|
+ line-height: 32px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-label {
|
|
|
+ color: #606266;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-value {
|
|
|
+ color: #303133;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.user-detail-content {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
</style>
|