Jelajahi Sumber

企微看课统计和会员看课统计)加一个上线客户完播率和流失人数&进线时间后面需要加一个沟通内容

lxb 2 hari lalu
induk
melakukan
09ce1819c2

+ 17 - 0
src/api/course/courseRedPacketLog.js

@@ -85,3 +85,20 @@ export function exportCourseRedPacketLog(query) {
     params: query
   })
 }
+
+//会员流失人数统计
+export function courseLost(query) {
+  return request({
+    url: '/course/courseRedPacketLog/courseLost',
+    method: 'get',
+    params: query
+  })
+}
+//企微流失人数统计
+export function qwCourseLost(query) {
+  return request({
+    url: '/qw/course/courseWatchLog/courseLost',
+    method: 'post',
+    data: query
+  })
+}

+ 9 - 0
src/api/course/qw/courseWatchLog.js

@@ -126,3 +126,12 @@ export function listBytrainingCampId(query) {
     params: query
   })
 }
+
+
+export function courseLost(query) {
+  return request({
+    url: '/qw/course/courseWatchLog/statisticsList',
+    method: 'get',
+    params: query
+  })
+}

+ 8 - 1
src/api/course/userCourseCamp.js

@@ -8,7 +8,14 @@ export function listCamp(query) {
     params: query
   })
 }
-
+//训练营列表(不分页 不带参)
+export function shortListCamp(query) {
+  return request({
+    url: '/course/trainingCamp/shortList',
+    method: 'get',
+    params: query
+  })
+}
 // 新增训练营
 export function addCamp(data) {
   return request({

+ 443 - 0
src/views/course/courseLost/index.vue

@@ -0,0 +1,443 @@
+<template>
+  <div class="app-container">
+    <el-card class="search-card" shadow="never">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="68px">
+        <el-form-item label="课程" prop="courseId">
+          <el-select
+            filterable
+            v-model="queryParams.courseId"
+            placeholder="请选择课程"
+            clearable
+            size="small"
+            style="width: 200px"
+          >
+            <el-option
+              v-for="dict in campData"
+              :key="dict.trainingCampId"
+              :label="dict.trainingCampName"
+              :value="dict.trainingCampId"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item class="search-buttons">
+          <el-button type="primary" icon="el-icon-search" size="small" @click="handleQuery">查询</el-button>
+          <el-button icon="el-icon-refresh" size="small" @click="resetQuery">重置</el-button>
+          <el-button v-show="false"
+            type="warning"
+            icon="el-icon-download"
+            size="small"
+            :loading="exportLoading"
+            @click="handleExport"
+            v-hasPermi="['qw:user:export']"
+          >导出数据</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- 统计结果展示区域 -->
+    <el-row :gutter="20" class="statistics-row">
+      <el-col :span="24">
+        <el-card class="statistics-card" shadow="hover">
+          <div slot="header" class="clearfix">
+            <span class="card-title">
+              <i class="el-icon-data-analysis"></i>
+              详情
+            </span>
+          </div>
+
+          <!-- 加载状态 -->
+          <div v-if="loading" class="loading-content">
+            <el-skeleton :rows="5" animated />
+          </div>
+
+          <!-- 数据展示 -->
+          <div v-else-if="statisticsResult" class="data-content">
+            <div class="data-display">
+              <div class="data-value">
+                {{ formatValue(statisticsResult.lostData) }}
+              </div>
+            </div>
+          </div>
+          <!-- 无数据状态 -->
+          <div v-else class="empty-content">
+            <el-empty description="暂无统计数据">
+              <el-button type="primary" size="small" @click="resetQuery">重新查询</el-button>
+            </el-empty>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { statisticsList, statisticsExport } from "@/api/course/qw/courseWatchLog";
+import { shortListCamp } from '@/api/course/userCourseCamp'
+import { courseLost, videoList } from '@/api/course/courseRedPacketLog'
+
+export default {
+  name: "CourseLost",
+  data() {
+    return {
+      // 查询相关
+      createTime: null,
+      courseLists: [],
+      videoList: [],
+
+      // 状态相关
+      loading: true,
+      exportLoading: false,
+      infoDialogVisible: false,
+
+      // 数据相关
+      statisticsResult: null,
+      //课程数据
+      campData: null,
+      showSearch: true,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        courseId: null,
+        sTime: null,
+        eTime: null,
+      }
+    };
+  },
+  created() {
+    shortListCamp().then(response => {
+      this.campData = response.data;
+    })
+    this.getStatisticsData();
+  },
+  methods: {
+    // 格式化显示值
+    formatValue(value) {
+      if (value === null || value === undefined || value === '') {
+        return '暂无数据';
+      }
+
+      // 如果是数字,添加千位分隔符
+      if (typeof value === 'number' || !isNaN(value)) {
+        const num = Number(value);
+        return num.toLocaleString('zh-CN');
+      }
+
+      return value;
+    },
+
+    // 获取当前时间
+    getCurrentTime() {
+      const now = new Date();
+      const year = now.getFullYear();
+      const month = String(now.getMonth() + 1).padStart(2, '0');
+      const day = String(now.getDate()).padStart(2, '0');
+      const hours = String(now.getHours()).padStart(2, '0');
+      const minutes = String(now.getMinutes()).padStart(2, '0');
+      const seconds = String(now.getSeconds()).padStart(2, '0');
+      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+    },
+
+    // 获取统计数据
+    getStatisticsData() {
+      this.loading = true;
+      courseLost(this.queryParams)
+        .then(response => {
+          this.handleResponse(response);
+          this.updateTime = this.getCurrentTime();
+        })
+        .catch(error => {
+          console.error('获取统计数据失败:', error);
+          this.$message.error('获取统计数据失败');
+          this.statisticsResult = null;
+        })
+        .finally(() => {
+          this.loading = false;
+        });
+    },
+
+    // 处理接口响应
+    handleResponse(response) {
+      // 根据实际接口返回结构调整
+      if (response.data && response.data.lostData !== undefined) {
+        this.statisticsResult = response.data;
+      } else if (response.rows && response.rows.length > 0) {
+        this.statisticsResult = { lostData: response.rows[0].lostData };
+      } else if (response.total !== undefined) {
+        this.statisticsResult = { lostData: response.total };
+      } else if (typeof response === 'object') {
+        this.statisticsResult = { lostData: response.data };;
+      } else {
+        this.statisticsResult = { lostData: response.data };
+      }
+      // 如果没有mockData字段,创建默认结构
+      if (!this.statisticsResult || this.statisticsResult.lostData === undefined) {
+        this.statisticsResult = { lostData: '暂无数据' };
+      }
+    },
+
+    // 刷新数据
+    refreshData() {
+      this.getStatisticsData();
+      this.$message.success('数据已刷新');
+    },
+
+    // 显示字段信息
+    showFieldInfo() {
+      this.infoDialogVisible = true;
+    },
+
+    // 查询
+    handleQuery() {
+/*       if (!this.queryParams.sTime || !this.queryParams.eTime) {
+        this.$message.warning("请选择时间范围");
+        return;
+      } */
+      this.getStatisticsData();
+    },
+
+    // 重置查询
+    resetQuery() {
+      this.$refs.queryForm.resetFields();
+      this.createTime = null;
+      this.queryParams.sTime = null;
+      this.queryParams.eTime = null;
+      this.queryParams.courseId = null;
+      this.videoList = [];
+      this.getStatisticsData();
+    },
+
+    // 导出数据
+    handleExport() {
+      if (!this.queryParams.sTime || !this.queryParams.eTime) {
+        this.$message.warning("请选择时间范围");
+        return;
+      }
+
+      this.$confirm('确定要导出当前统计数据吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+        .then(() => {
+          this.exportLoading = true;
+          return statisticsExport(this.queryParams);
+        })
+        .then(response => {
+          this.download(response.msg);
+          this.$message.success('导出成功');
+        })
+        .catch(() => {
+          // 用户取消导出
+        })
+        .finally(() => {
+          this.exportLoading = false;
+        });
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+  padding: 20px;
+}
+
+// 搜索卡片样式
+.search-card {
+  margin-bottom: 20px;
+
+  ::v-deep .el-card__body {
+    padding: 20px;
+  }
+
+  .search-buttons {
+    margin-left: 10px;
+  }
+}
+
+// 统计卡片样式
+.statistics-row {
+  margin-top: 20px;
+}
+
+.statistics-card {
+  min-height: 400px;
+
+  ::v-deep .el-card__header {
+    padding: 18px 20px;
+    background-color: #f5f7fa;
+    border-bottom: 1px solid #ebeef5;
+  }
+
+  .card-title {
+    font-size: 16px;
+    font-weight: 500;
+    color: #303133;
+
+    i {
+      margin-right: 8px;
+      color: #409eff;
+    }
+  }
+
+  .card-actions {
+    float: right;
+    margin-top: -6px;
+
+    .el-button {
+      padding: 0;
+      margin-left: 16px;
+    }
+  }
+}
+
+// 数据内容区域
+.loading-content {
+  padding: 40px 20px;
+}
+
+.data-content {
+  padding: 0px 20px;
+  text-align: center;
+}
+
+.data-header {
+  margin-bottom: 30px;
+
+  .data-title {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 12px;
+    margin-bottom: 8px;
+
+    .title-text {
+      font-size: 18px;
+      font-weight: 500;
+      color: #606266;
+    }
+  }
+
+  .data-subtitle {
+    font-size: 14px;
+    color: #909399;
+  }
+}
+
+.data-display {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 20px;
+  margin: 40px 0;
+  padding: 40px;
+  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+  border-radius: 12px;
+
+  .data-value {
+    font-size: 64px;
+    font-weight: 700;
+    color: #409eff;
+    text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
+  }
+
+  .data-unit {
+    .el-tag {
+      font-size: 14px;
+      padding: 8px 16px;
+    }
+  }
+}
+
+.data-footer {
+  margin-top: 40px;
+  padding-top: 20px;
+  border-top: 1px solid #ebeef5;
+
+  .data-meta {
+    display: flex;
+    justify-content: center;
+    gap: 30px;
+    flex-wrap: wrap;
+
+    .meta-item {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+      font-size: 14px;
+      color: #909399;
+
+      i {
+        color: #409eff;
+      }
+    }
+  }
+}
+
+// 空状态
+.empty-content {
+  padding: 80px 20px;
+}
+
+// 字段信息对话框样式
+.field-info {
+  .info-item {
+    display: flex;
+    margin-bottom: 16px;
+    padding-bottom: 16px;
+    border-bottom: 1px solid #ebeef5;
+
+    &:last-child {
+      margin-bottom: 0;
+      padding-bottom: 0;
+      border-bottom: none;
+    }
+
+    .info-label {
+      width: 100px;
+      font-weight: 500;
+      color: #303133;
+      flex-shrink: 0;
+    }
+
+    .info-value {
+      flex: 1;
+      color: #606266;
+      word-break: break-word;
+    }
+  }
+}
+
+// 响应式设计
+@media (max-width: 768px) {
+  .search-card ::v-deep .el-card__body {
+    padding: 15px;
+  }
+
+  .data-display {
+    flex-direction: column;
+    padding: 30px 20px;
+
+    .data-value {
+      font-size: 48px;
+    }
+  }
+
+  .data-footer .data-meta {
+    flex-direction: column;
+    gap: 12px;
+    align-items: flex-start;
+  }
+
+  .search-buttons {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 10px;
+
+    .el-button {
+      margin-left: 0 !important;
+    }
+  }
+}
+</style>

+ 452 - 0
src/views/course/courseLost/qw/index.vue

@@ -0,0 +1,452 @@
+<template>
+  <div class="app-container">
+    <el-card class="search-card" shadow="never">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="68px">
+        <el-form-item label="SOP任务" prop="channel" label-width="70px">
+          <date-range
+            v-model="selectedMultipleTasks"
+            :raw-data="channelList"
+            placeholder="请选择多个任务"
+            :parentSelectable="true"
+            :multiple="true"
+            component-width="300px"
+            :max-display-tags="3"
+            :check-strictly="false"
+            :return-leaf-only="false"
+            :show-node-detail="true"
+            @change="handleMultiChange"
+            @dateRangeChange="dateRangeChange"
+          ></date-range>
+        </el-form-item>
+        <el-form-item class="search-buttons">
+          <el-button type="primary" icon="el-icon-search" size="small" @click="handleQuery">查询</el-button>
+          <el-button icon="el-icon-refresh" size="small" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- 统计结果展示区域 -->
+    <el-row :gutter="20" class="statistics-row">
+      <el-col :span="24">
+        <el-card class="statistics-card" shadow="hover">
+          <div slot="header" class="clearfix">
+            <span class="card-title">
+              <i class="el-icon-data-analysis"></i>
+              详情
+            </span>
+          </div>
+
+          <!-- 加载状态 -->
+          <div v-if="loading" class="loading-content">
+            <el-skeleton :rows="5" animated />
+          </div>
+
+          <!-- 数据展示 -->
+          <div v-else-if="statisticsResult" class="data-content">
+            <div class="data-display">
+              <div class="data-value">
+                {{ formatValue(statisticsResult.lostData) }}
+              </div>
+            </div>
+          </div>
+          <!-- 无数据状态 -->
+          <div v-else class="empty-content">
+            <el-empty description="暂无统计数据">
+              <el-button type="primary" size="small" @click="resetQuery">重新查询</el-button>
+            </el-empty>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { shortListCamp } from '@/api/course/userCourseCamp'
+import { qwCourseLost } from '@/api/course/courseRedPacketLog'
+import {
+  getSOPTaskData,
+} from '@/api/system/employeeStats'
+import SelectTree from '@/components/TreeSelect/index.vue'
+import dateRange from '@/components/TreeSelect/dateRange.vue'
+export default {
+  name: "CourseLostQw",
+  components: {SelectTree,dateRange},
+  data() {
+    return {
+      // 查询相关
+      createTime: null,
+      courseLists: [],
+      videoList: [],
+      selectedMultipleTasks: [],
+      // 选中数组
+      ids: [],
+      // 状态相关
+      loading: true,
+      exportLoading: false,
+      infoDialogVisible: false,
+      // 数据相关
+      statisticsResult: null,
+      //课程数据
+      campData: null,
+      showSearch: true,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        periodList: []
+      },
+      // 频道列表
+      channelList: [],
+    };
+  },
+  created() {
+    getSOPTaskData({}).then(response => {
+      this.channelList = response.data;
+    });
+  },
+  methods: {
+
+    // 格式化显示值
+    formatValue(value) {
+      if (value === null || value === undefined || value === '') {
+        return '暂无数据';
+      }
+
+      // 如果是数字,添加千位分隔符
+      if (typeof value === 'number' || !isNaN(value)) {
+        const num = Number(value);
+        return num.toLocaleString('zh-CN');
+      }
+
+      return value;
+    },
+
+    // 获取当前时间
+    getCurrentTime() {
+      const now = new Date();
+      const year = now.getFullYear();
+      const month = String(now.getMonth() + 1).padStart(2, '0');
+      const day = String(now.getDate()).padStart(2, '0');
+      const hours = String(now.getHours()).padStart(2, '0');
+      const minutes = String(now.getMinutes()).padStart(2, '0');
+      const seconds = String(now.getSeconds()).padStart(2, '0');
+      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+    },
+
+    // 获取统计数据
+    getStatisticsData() {
+      this.loading = true;
+      qwCourseLost(this.queryParams)
+        .then(response => {
+          this.handleResponse(response);
+          this.updateTime = this.getCurrentTime();
+        })
+        .catch(error => {
+          console.error('获取统计数据失败:', error);
+          this.$message.error('获取统计数据失败');
+          this.statisticsResult = null;
+        })
+        .finally(() => {
+          this.loading = false;
+        });
+    },
+    handleMultiChange(e){
+
+    },
+    dateRangeChange(e){
+      getSOPTaskData({
+        startDate: e[0],
+        endDate: e[1]
+      }).then(response => {
+        this.channelList = response.data;
+      });
+    },
+    // 处理接口响应
+    handleResponse(response) {
+      // 根据实际接口返回结构调整
+      if (response.data && response.data.lostData !== undefined) {
+        this.statisticsResult = response.data;
+      } else if (response.rows && response.rows.length > 0) {
+        this.statisticsResult = { lostData: response.rows[0].lostData };
+      } else if (response.total !== undefined) {
+        this.statisticsResult = { lostData: response.total };
+      } else if (typeof response === 'object') {
+        this.statisticsResult = { lostData: response.data };
+        ;
+      } else {
+        this.statisticsResult = { lostData: response.data };
+      }
+      // 如果没有mockData字段,创建默认结构
+      if (!this.statisticsResult || this.statisticsResult.lostData === undefined) {
+        this.statisticsResult = { lostData: '暂无数据' };
+      }
+    },
+
+    // 刷新数据
+    refreshData() {
+      this.getStatisticsData();
+      this.$message.success('数据已刷新');
+    },
+
+    // 查询
+    handleQuery() {
+      /*       if (!this.queryParams.sTime || !this.queryParams.eTime) {
+              this.$message.warning("请选择时间范围");
+              return;
+            } */
+      this.queryParams.periodList = this.selectedMultipleTasks;
+      this.getStatisticsData();
+    },
+
+    // 重置查询
+    resetQuery() {
+      this.$refs.queryForm.resetFields();
+      this.createTime = null;
+      this.queryParams.sTime = null;
+      this.queryParams.eTime = null;
+      this.queryParams.courseId = null;
+      this.videoList = [];
+      this.selectedMultipleTasks = [];
+      this.getStatisticsData();
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+  padding: 20px;
+}
+
+// 搜索卡片样式
+.search-card {
+  margin-bottom: 20px;
+
+  ::v-deep .el-card__body {
+    padding: 20px;
+  }
+
+  .search-buttons {
+    margin-left: 10px;
+  }
+}
+
+// 统计卡片样式
+.statistics-row {
+  margin-top: 20px;
+}
+
+.statistics-card {
+  min-height: 400px;
+
+  ::v-deep .el-card__header {
+    padding: 18px 20px;
+    background-color: #f5f7fa;
+    border-bottom: 1px solid #ebeef5;
+  }
+
+  .card-title {
+    font-size: 16px;
+    font-weight: 500;
+    color: #303133;
+
+    i {
+      margin-right: 8px;
+      color: #409eff;
+    }
+  }
+
+  .card-actions {
+    float: right;
+    margin-top: -6px;
+
+    .el-button {
+      padding: 0;
+      margin-left: 16px;
+    }
+  }
+}
+
+// 数据内容区域
+.loading-content {
+  padding: 40px 20px;
+}
+
+.data-content {
+  padding: 0px 20px;
+  text-align: center;
+}
+
+.data-header {
+  margin-bottom: 30px;
+
+  .data-title {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 12px;
+    margin-bottom: 8px;
+
+    .title-text {
+      font-size: 18px;
+      font-weight: 500;
+      color: #606266;
+    }
+  }
+
+  .data-subtitle {
+    font-size: 14px;
+    color: #909399;
+  }
+}
+
+.data-display {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 20px;
+  margin: 40px 0;
+  padding: 40px;
+  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+  border-radius: 12px;
+
+  .data-value {
+    font-size: 64px;
+    font-weight: 700;
+    color: #409eff;
+    text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
+  }
+
+  .data-unit {
+    .el-tag {
+      font-size: 14px;
+      padding: 8px 16px;
+    }
+  }
+}
+
+.data-footer {
+  margin-top: 40px;
+  padding-top: 20px;
+  border-top: 1px solid #ebeef5;
+
+  .data-meta {
+    display: flex;
+    justify-content: center;
+    gap: 30px;
+    flex-wrap: wrap;
+
+    .meta-item {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+      font-size: 14px;
+      color: #909399;
+
+      i {
+        color: #409eff;
+      }
+    }
+  }
+}
+
+// 空状态
+.empty-content {
+  padding: 80px 20px;
+}
+
+// 字段信息对话框样式
+.field-info {
+  .info-item {
+    display: flex;
+    margin-bottom: 16px;
+    padding-bottom: 16px;
+    border-bottom: 1px solid #ebeef5;
+
+    &:last-child {
+      margin-bottom: 0;
+      padding-bottom: 0;
+      border-bottom: none;
+    }
+
+    .info-label {
+      width: 100px;
+      font-weight: 500;
+      color: #303133;
+      flex-shrink: 0;
+    }
+
+    .info-value {
+      flex: 1;
+      color: #606266;
+      word-break: break-word;
+    }
+  }
+}
+.stats-card {
+  text-align: center;
+  margin-bottom: 16px;
+
+  .stats-number {
+    font-size: 24px;
+    font-weight: bold;
+    color: #409EFF;
+
+    &.online {
+      color: #67C23A;
+    }
+
+    &.offline {
+      color: #F56C6C;
+    }
+
+    &.rate {
+      color: #E6A23C;
+    }
+  }
+}
+
+.mb8 {
+  margin-bottom: 8px;
+}
+
+::v-deep .el-table .el-table__header th {
+  background-color: #f5f7fa;
+}
+
+::v-deep .el-alert {
+  margin-bottom: 16px;
+}
+// 响应式设计
+@media (max-width: 768px) {
+  .search-card ::v-deep .el-card__body {
+    padding: 15px;
+  }
+
+  .data-display {
+    flex-direction: column;
+    padding: 30px 20px;
+
+    .data-value {
+      font-size: 48px;
+    }
+  }
+
+  .data-footer .data-meta {
+    flex-direction: column;
+    gap: 12px;
+    align-items: flex-start;
+  }
+
+  .search-buttons {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 10px;
+
+    .el-button {
+      margin-left: 0 !important;
+    }
+  }
+}
+</style>

+ 163 - 0
src/views/course/courseWatchLog/index.vue

@@ -376,6 +376,11 @@
       <el-table-column label="完课时间" align="center" prop="finishTime" />
       <el-table-column label="营期时间" align="center" prop="campPeriodTime" />
       <el-table-column label="进线时间" align="center" prop="qecCreateTime" />
+      <el-table-column label="沟通内容" show-overflow-tooltip align="center" prop="interflowContent" >
+        <template slot-scope="scope">
+          <el-button @click="showInterflowContentFun(scope.row)" type="text" size="small">详情</el-button>
+        </template>
+      </el-table-column>
       <el-table-column label="是否领奖" align="center" prop="rewardType" >
         <template slot-scope="scope">
           <el-tag
@@ -391,6 +396,7 @@
         label="操作"
         width="100">
         <template slot-scope="scope">
+          <el-button @click="editInterflowContentFun(scope.row)" type="text" size="small">修改沟通内容</el-button>
           <el-button @click="openAnswerLogFun(scope.row)" type="text" size="small">答题记录</el-button>
           <el-button @click="openRedLogFun(scope.row)" type="text" size="small">红包记录</el-button>
         </template>
@@ -601,7 +607,55 @@
         <el-button @click="resultDialogVisible = false">关闭</el-button>
       </span>
     </el-dialog>
+    <!-- 修改沟通内容弹窗 -->
+    <el-dialog
+      title="修改沟通内容"
+      :visible.sync="editContentDialogVisible"
+      width="500px"
+      append-to-body
+    >
+      <el-form
+        ref="editContentForm"
+        :model="editContentForm"
+        :rules="editContentRules"
+        label-width="80px"
+      >
+        <el-form-item label="沟通内容" prop="interflowContent">
+          <el-input
+            v-model="editContentForm.interflowContent"
+            type="textarea"
+            :rows="6"
+            placeholder="请输入沟通内容"
+            maxlength="500"
+            show-word-limit
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="editContentDialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="submitEditContent" :loading="editContentLoading">确 定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 沟通内容详情弹窗 -->
+    <el-dialog
+      title="沟通内容详情"
+      :visible.sync="showContentDialogVisible"
+      width="500px"
+      append-to-body
+    >
+      <div class="content-detail">
+        <div class="content-section">
+          <div class="content-text">
+            {{ showContentDetail || '暂无沟通内容' }}
+          </div>
+        </div>
+      </div>
 
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="showContentDialogVisible = false">关闭</el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
@@ -816,6 +870,21 @@ export default {
         pageSize: 10
       },
       qwUserOptionsLoading: false,
+
+      editContentDialogVisible: false, // 弹窗显示状态
+      editContentLoading: false, // 提交加载状态
+      editContentForm: {
+        logId: null,
+        interflowContent: ''
+      },
+      editContentRules: {
+        interflowContent: [
+          { required: true, message: '沟通内容不能为空', trigger: 'blur' },
+          { max: 500, message: '长度不能超过500个字符', trigger: 'blur' }
+        ]
+      },
+      showContentDialogVisible: false, // 详情弹窗显示状态
+      showContentDetail: '' // 沟通内容详情
     };
   },
   created() {
@@ -1409,7 +1478,66 @@ export default {
           this.exportLoading = false;
         }).catch(() => {});
     },
+    /**
+     * 修改沟通内容
+     * @param row 当前行数据
+     */
+    editInterflowContentFun(row) {
+      this.editContentForm = {
+        logId: row.logId,
+        interflowContent: row.interflowContent || ''
+      };
+
+      this.editContentDialogVisible = true;
 
+      // 清空表单验证
+      this.$nextTick(() => {
+        if (this.$refs.editContentForm) {
+          this.$refs.editContentForm.clearValidate();
+        }
+      });
+    },
+    /**
+     * 查看沟通内容详情
+     * @param row 当前行数据
+     */
+    showInterflowContentFun(row) {
+      console.log('查看沟通内容详情:', row.logId, row.interflowContent);
+
+      // 设置沟通内容详情
+      this.showContentDetail = row.interflowContent || '暂无沟通内容';
+
+      // 打开弹窗
+      this.showContentDialogVisible = true;
+    },
+    /**
+     * 提交修改沟通内容
+     */
+    submitEditContent() {
+      this.$refs.editContentForm.validate(valid => {
+        if (!valid) return;
+
+        this.editContentLoading = true;
+
+        // 调用后端API更新沟通内容
+        updateCourseWatchLog({
+          logId: this.editContentForm.logId,
+          interflowContent: this.editContentForm.interflowContent
+        })
+          .then(response => {
+            this.$message.success('修改成功');
+            this.editContentDialogVisible = false;
+            this.getList(); // 刷新列表
+          })
+          .catch(error => {
+            console.error('修改失败:', error);
+            this.$message.error('修改失败');
+          })
+          .finally(() => {
+            this.editContentLoading = false;
+          });
+      });
+    },
     openAnswerLogFun(row) {
       this.openAnswerLog = true;
       this.answerLogQueryParams.watchLogId = row.logId;
@@ -1779,4 +1907,39 @@ export default {
   background: rgba(0, 0, 0, 0.2);
   border-radius: 3px;
 }
+
+.content-detail {
+  padding: 10px;
+}
+
+.content-section {
+  margin-top: 10px;
+}
+
+.content-text {
+  padding: 12px;
+  background-color: #f5f7fa;
+  border-radius: 4px;
+  border: 1px solid #ebeef5;
+  line-height: 1.6;
+  min-height: 100px;
+  max-height: 300px;
+  overflow-y: auto;
+  white-space: pre-wrap; /* 保留换行和空格 */
+  word-break: break-word; /* 自动换行 */
+}
+
+/* 美化滚动条 */
+.content-text::-webkit-scrollbar {
+  width: 6px;
+}
+
+.content-text::-webkit-scrollbar-thumb {
+  background: #c0c4cc;
+  border-radius: 3px;
+}
+
+.content-text::-webkit-scrollbar-track {
+  background: #f5f7fa;
+}
 </style>

+ 141 - 4
src/views/course/courseWatchLog/qw/statistics.vue

@@ -70,6 +70,8 @@
       <el-table-column label="上线率" align="center" prop="onLineRate" />
       <el-table-column label="完课率" align="center" prop="finishedRate" />
 <!--      <el-table-column label="消耗红包金额" align="center" prop="redAmount" />-->
+      <el-table-column label="上线客户完播率" align="center" prop="onLineCompleteRate" />
+<!--      <el-table-column label="流失人数" align="center" prop="onLineNum" />-->
     </el-table>
 
     <pagination
@@ -159,7 +161,7 @@ export default {
     });
   },
   methods: {
-     getSummaries(param) {
+     bfGetSummaries(param) {//以前的代码备份
       let totalNum = 0;
       const { columns, data } = param;
       const sums = [];
@@ -180,7 +182,7 @@ export default {
               return prev;
             }
           }, 0);
-          if(index > 3 && totalNum != 0){
+          if(index > 3 && totalNum !== 0){
               sums[index] = sums[index] + "("+ ((sums[index] / totalNum) * 100).toFixed(2)  + "%)"
           }
           if( column.property=="videoName"){
@@ -196,12 +198,13 @@ export default {
             column.property === "noUserWaitNumber" ||
             column.property === "onLineRate" ||
             column.property === "finishedRate" ||
-            column.property === "redAmount"
+            column.property === "redAmount" ||
+            column.property === "onLineCompleteRate"
           ) {
           }
         } else {
           sums[index] = "";
-          if(index === 12 || index === 13){
+          if(index === 12 || index === 13 || index===14){
             //  let numbers = data.map(item => {
             //   return parseFloat(item[column.property].replace('%', '')) || 0; // 处理空值或无效值
             // });
@@ -210,15 +213,149 @@ export default {
             if(index === 12 && !!sums[9]){
               let sumsNum = sums[6] + sums[7] + sums[8];
               sums[index] = (sumsNum * 100 / sums[9]).toFixed(2) + '%';
+
+
             } else if(index === 13 && !!sums[9]){
               sums[index] =  (sums[7] * 100 / sums[9]).toFixed(2) + '%';
             }
+            else if(index === 14 && !!sums[9]){
+              sums[index] =  (sums[7] * 100 / sums[9]).toFixed(2) + '%';
+            }
           }
         }
       });
       console.log(sums);
       return sums;
     },
+    getSummaries(param) {
+      const { columns, data } = param;
+      const sums = [];
+
+      // 计算各种总数
+      let totalSendNumber = 0;
+      let totalType1 = 0;
+      let totalType2 = 0;
+      let totalType3 = 0;
+      let totalType4 = 0;
+      let totalIsUserWaitNumber = 0;
+      let totalNoUserWaitNumber = 0;
+      let totalOnLineNum = 0;
+
+      // 百分比直接累加(不是平均)
+      let totalOnLineRate = 0;     // 上线率合计
+      let totalFinishedRate = 0;   // 完课率合计
+      let totalOnLineCompleteRate = 0; // 上线客户完播率合计
+
+      // 遍历数据
+      data.forEach(item => {
+        // 数字字段累加
+        totalSendNumber += Number(item.sendNumber) || 0;
+        totalType1 += Number(item.type1) || 0;
+        totalType2 += Number(item.type2) || 0;
+        totalType3 += Number(item.type3) || 0;
+        totalType4 += Number(item.type4) || 0;
+        totalIsUserWaitNumber += Number(item.isUserWaitNumber) || 0;
+        totalNoUserWaitNumber += Number(item.noUserWaitNumber) || 0;
+
+        // 计算本行的上线人数
+        const itemOnLineNum = (Number(item.type1) || 0) + (Number(item.type2) || 0) + (Number(item.type4) || 0);
+        totalOnLineNum += itemOnLineNum;
+
+        // 提取百分比字段的值(去除%号)
+        const parsePercentage = (value) => {
+          if (!value && value !== 0) return 0;
+          if (typeof value === 'string') {
+            // 处理"5.00%"格式
+            const num = parseFloat(value.replace('%', ''));
+            return isNaN(num) ? 0 : num;
+          }
+          return Number(value) || 0;
+        };
+
+        // 累加上线率
+        totalOnLineRate += parsePercentage(item.onLineRate);
+
+        // 累加完课率
+        totalFinishedRate += parsePercentage(item.finishedRate);
+
+        // 累加上线客户完播率
+        totalOnLineCompleteRate += parsePercentage(item.onLineCompleteRate);
+      });
+
+      columns.forEach((column, index) => {
+        if (index === 0) {
+          sums[index] = "总计";
+          return;
+        }
+
+        const property = column.property;
+
+        // 处理特殊字段
+        if (property === "videoName" || property === "courseName" ||
+          property === "qwUserName" || property === "createTime") {
+          sums[index] = "";
+          return;
+        }
+
+        // 处理百分比字段 - 直接累加的结果
+        if (property === "onLineRate") {
+          sums[index] = totalOnLineRate.toFixed(2) + '%';
+          return;
+        } else if (property === "finishedRate") {
+          sums[index] = totalFinishedRate.toFixed(2) + '%';
+          return;
+        } else if (property === "onLineCompleteRate") {
+          sums[index] = totalOnLineCompleteRate.toFixed(2) + '%';
+          return;
+        }
+
+        // 处理普通数字字段
+        switch (property) {
+          case "type1":
+            sums[index] = totalType1;
+            break;
+          case "type2":
+            sums[index] = totalType2;
+            break;
+          case "type3":
+            sums[index] = totalType3;
+            break;
+          case "type4":
+            sums[index] = totalType4;
+            break;
+          case "sendNumber":
+            sums[index] = totalSendNumber;
+            break;
+          case "isUserWaitNumber":
+            sums[index] = totalIsUserWaitNumber;
+            break;
+          case "noUserWaitNumber":
+            sums[index] = totalNoUserWaitNumber;
+            break;
+          case "redAmount":
+            // 如果需要计算红包金额总和
+            const redAmounts = data.map(item => Number(item.redAmount) || 0);
+            sums[index] = redAmounts.reduce((sum, curr) => sum + curr, 0);
+            break;
+          default:
+            sums[index] = "";
+        }
+      });
+
+      console.log("统计结果:", {
+        发课数: totalSendNumber,
+        看课中: totalType1,
+        已完课: totalType2,
+        待看课: totalType3,
+        看课中断: totalType4,
+        上线人数: totalOnLineNum,
+        上线率合计: totalOnLineRate.toFixed(2) + '%',
+        完课率合计: totalFinishedRate.toFixed(2) + '%',
+        上线完播率合计: totalOnLineCompleteRate.toFixed(2) + '%'
+      });
+
+      return sums;
+    },
     courseChange(row){
       this.queryParams.videoId=null;
       if(row === ''){