Pārlūkot izejas kodu

Merge remote-tracking branch 'origin/Payment-Configuration' into Payment-Configuration

yys 2 dienas atpakaļ
vecāks
revīzija
fac1cc485a

+ 54 - 0
src/api/app/statistics/appStatistics.js

@@ -0,0 +1,54 @@
+import request from '@/utils/request'
+
+// App会员看课统计
+export function appWatchLogReport(query) {
+  return request({
+    url: '/app/statistics/appWatchLogReport',
+    method: 'get',
+    params: query
+  })
+}
+
+// App会员看课统计导出
+export function appWatchLogReportExport(query) {
+  return request({
+    url: '/app/statistics/appWatchLogReportExport',
+    method: 'get',
+    params: query
+  })
+}
+
+// APP看课统计(部门)
+export function appSalesWatchLogReport(query) {
+  return request({
+    url: '/app/statistics/appSalesWatchLogReport',
+    method: 'get',
+    params: query
+  })
+}
+
+// APP看课统计导出
+export function appSalesWatchLogReportExport(query) {
+  return request({
+    url: '/app/statistics/appSalesWatchLogReportExport',
+    method: 'get',
+    params: query
+  })
+}
+
+export function appWatchCourseStatistics(query) {
+  return request({
+    url: '/app/statistics/appWatchCourseStatistics',
+    method: 'get',
+    params: query
+  })
+}
+
+export function appWatchCourseStatisticsExport(query) {
+  return request({
+    url: '/app/statistics/appWatchCourseStatisticsExport',
+    method: 'get',
+    params: query
+  })
+}
+

+ 8 - 0
src/api/company/companyDept.js

@@ -81,3 +81,11 @@ export function allTreeselect() {
     method: 'get'
   })
 }
+
+export function selectDeptTree(query) {
+  return request({
+    url: '/company/companyDept/selectDeptTree',
+    method: 'get',
+    params: query
+  })
+}

+ 18 - 2
src/api/company/statistics.js

@@ -1,6 +1,6 @@
 import request from '@/utils/request'
 
- 
+
 export function voiceLogs(query) {
   return request({
     url: '/company/companyStatistics/voiceLogs',
@@ -30,7 +30,7 @@ export function exportMyVoiceLogs(query) {
     params: query
   })
 }
- 
+
 export function smsLogs(query) {
   return request({
     url: '/company/companyStatistics/smsLogs',
@@ -45,3 +45,19 @@ export function exportSmsLogs(query) {
     params: query
   })
 }
+
+export function redPacketStatisticsBySales(query) {
+  return request({
+    url: '/company/companyStatistics/redPacketStatisticsBySales',
+    method: 'get',
+    params: query
+  })
+}
+
+export function exportRedPacketStatisticsBySales(query) {
+  return request({
+    url: '/company/companyStatistics/exportRedPacketStatisticsBySales',
+    method: 'get',
+    params: query
+  })
+}

+ 339 - 0
src/views/app/statistics/appWatchCourseStatistics.vue

@@ -0,0 +1,339 @@
+
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="90px">
+      <el-form-item label="课程" prop="courseId">
+        <el-select filterable v-model="queryParams.courseId" placeholder="请选择课程"
+                   clearable size="small" @change="courseChange(queryParams.courseId)">
+          <el-option
+            v-for="dict in courseList"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="课程小节" prop="videoId">
+        <el-select filterable v-model="queryParams.videoId" placeholder="请选择课程小节"
+                   clearable size="small">
+          <el-option
+            v-for="dict in videoList"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="销售名称" prop="salesName">
+        <el-input v-model="queryParams.salesName" placeholder="请输入销售名称" clearable size="small" />
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="createTime"
+          size="small"          style="width: 240px"
+          value-format="yyyy-MM-dd"
+          type="date"
+          placeholder="选择日期"
+          :disabled-date="disabledDate"
+          @change="handleCreateTimeChange"></el-date-picker>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table
+      v-loading="loading"
+      border
+      :data="packageList"
+      show-summary
+      :summary-method="getSummaries"
+      height="600"
+    >
+      <el-table-column label="销售名称" align="center" prop="salesName" />
+      <el-table-column label="APP 会员数" align="center" prop="appUserCount" />
+      <el-table-column label="新注册 APP 会员数" align="center" prop="newAppUserCount" />
+      <el-table-column label="课程名称" align="center" prop="courseName" />
+      <el-table-column label="课程小节" align="center" prop="videoTitle" />
+      <el-table-column label="完课数" align="center" prop="finishedCount" />
+      <el-table-column label="完课率" align="center" prop="completionRate">
+        <template slot-scope="scope">
+          <span v-if="typeof scope.row.completionRate === 'number'">
+            {{ (scope.row.completionRate * 100).toFixed(2) }}%
+          </span>
+          <span v-else>
+            {{ scope.row.completionRate || '0.00%' }}
+          </span>
+        </template>
+      </el-table-column>
+      <el-table-column label="未看课数" align="center" prop="notWatchedCount" />
+      <el-table-column label="中断数" align="center" prop="interruptCount" />
+      <el-table-column label="看课中数" align="center" prop="watchingCount" />
+      <el-table-column label="答题数" align="center" prop="answeredCount" />
+      <el-table-column label="答题正确数" align="center" prop="correctCount" />
+      <el-table-column label="答题正确率" align="center" prop="correctRate">
+        <template slot-scope="scope">
+          <span v-if="typeof scope.row.correctRate === 'number'">
+            {{ (scope.row.correctRate * 100).toFixed(2) }}%
+          </span>
+          <span v-else>
+            {{ scope.row.correctRate || '0.00%' }}
+          </span>
+        </template>
+      </el-table-column>
+      <el-table-column label="红包个数" align="center" prop="redPacketCount" />
+      <el-table-column label="红包金额" align="center" prop="redPacketAmount" />
+    </el-table>
+  </div>
+</template>
+
+<script>import { courseList, videoList } from '@/api/course/courseRedPacketLog';
+import { appWatchCourseStatistics, appWatchCourseStatisticsExport } from "@/api/app/statistics/appStatistics";
+
+export default {
+  name: "appWatchCourseStatistics",
+  components: {},
+  data() {
+    return {
+      courseList: [],
+      videoList: [],
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      createTime: null,
+      // 表格数据
+      packageList: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 1000,
+        courseId: null,
+        videoId: null,
+        salesName: null,
+        sTime: null,
+        eTime: null
+      }
+    };
+  },
+  created() {
+    this.initDefaultDate();
+    this.getList();
+    courseList().then(response => {
+      this.courseList = response.list;
+    });
+  },
+  methods: {
+    /** 初始化默认日期为今天 */
+    initDefaultDate() {
+      const today = this.parseTime(new Date(), '{y}-{m}-{d}');
+      this.createTime = today;
+      this.queryParams.sTime = today;
+      this.queryParams.eTime = today;
+    },
+
+    /** 禁用日期选择 - 只能选择今天及之前的日期 */
+    disabledDate(time) {
+      return time.getTime() > Date.now();
+    },
+
+    /** 课程变更处理 */
+    courseChange(row) {
+      this.queryParams.videoId = null;
+      if (row === '') {
+        this.videoList = [];
+        return;
+      }
+      videoList(row).then(response => {
+        this.videoList = response.list;
+      });
+    },
+
+    /** 创建时间变更处理 */
+    handleCreateTimeChange(val) {
+      if (val) {
+        this.queryParams.sTime = val;
+        this.queryParams.eTime = val;
+      } else {
+        this.queryParams.sTime = null;
+        this.queryParams.eTime = null;
+      }
+    },
+
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.initDefaultDate();
+      this.queryParams.courseId = null;
+      this.queryParams.videoId = null;
+      this.queryParams.salesName = null;
+      this.videoList = [];
+      this.handleQuery();
+    },
+
+    /** 获取表格合计方法 */
+    getSummaries(param) {
+      const { columns, data } = param;
+      const sums = [];
+
+      columns.forEach((column, index) => {
+        if (index === 0) {
+          sums[index] = '合计';
+          return;
+        }
+
+        const values = data.map(item => Number(item[column.property]));
+
+        if (['appUserCount', 'newAppUserCount', 'finishedCount', 'notWatchedCount', 'interruptCount', 'watchingCount', 'answeredCount', 'correctCount', 'redPacketCount'].includes(column.property)) {
+          if (!values.every(value => isNaN(value))) {
+            sums[index] = values.reduce((prev, curr) => {
+              const value = Number(curr);
+              if (!isNaN(value)) {
+                return prev + value;
+              } else {
+                return prev;
+              }
+            }, 0);
+          } else {
+            sums[index] = 'N/A';
+          }
+        } else if (column.property === 'redPacketAmount') {
+          if (!values.every(value => isNaN(value))) {
+            const total = values.reduce((prev, curr) => {
+              const value = Number(curr);
+              if (!isNaN(value)) {
+                return prev + value;
+              } else {
+                return prev;
+              }
+            }, 0);
+            sums[index] = total.toFixed(2);
+          } else {
+            sums[index] = 'N/A';
+          }
+        } else if (column.property === 'completionRate') {
+          const totalFinished = data.reduce((sum, item) => sum + (Number(item.finishedCount) || 0), 0);
+          const totalCount = totalFinished + data.reduce((sum, item) => sum + (Number(item.notWatchedCount) || 0), 0);
+
+          if (totalCount > 0) {
+            const rate = (totalFinished / totalCount * 100).toFixed(2);
+            sums[index] = `${rate}%`;
+          } else {
+            sums[index] = '0.00%';
+          }
+        } else if (column.property === 'correctRate') {
+          const totalAnswered = data.reduce((sum, item) => sum + (Number(item.answeredCount) || 0), 0);
+          const totalCorrect = data.reduce((sum, item) => sum + (Number(item.correctCount) || 0), 0);
+
+          if (totalAnswered > 0) {
+            const rate = (totalCorrect / totalAnswered * 100).toFixed(2);
+            sums[index] = `${rate}%`;
+          } else {
+            sums[index] = '0.00%';
+          }
+        } else {
+          sums[index] = '';
+        }
+      });
+
+      return sums;
+    },
+
+    /** 查询列表 */
+    getList() {
+      this.loading = true;
+      appWatchCourseStatistics(this.queryParams).then(response => {
+        this.packageList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      }).catch(() => {
+        this.loading = false;
+      });
+    },
+
+    /** 导出按钮操作 */
+    handleExport() {
+      this.$confirm('是否确认导出 APP 看课统计数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        this.exportLoading = true;
+        return appWatchCourseStatisticsExport(this.queryParams);
+      }).then(response => {
+        this.download(response.msg);
+        this.exportLoading = false;
+      }).catch(() => {
+        this.exportLoading = false;
+      });
+    },
+
+    /** 时间格式化 */
+    parseTime(time, pattern) {
+      if (!time) return null;
+      const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}';
+      let date;
+      if (typeof time === 'string') {
+        date = new Date(time.replace(/-/g, '/'));
+      } else {
+        date = new Date(time);
+      }
+      const year = date.getFullYear();
+      const month = date.getMonth() + 1;
+      const day = date.getDate();
+      const hour = date.getHours();
+      const minute = date.getMinutes();
+      const second = date.getSeconds();
+
+      const obj = {
+        '{y}': year,
+        '{m}': month < 10 ? '0' + month : month,
+        '{d}': day < 10 ? '0' + day : day,
+        '{h}': hour < 10 ? '0' + hour : hour,
+        '{i}': minute < 10 ? '0' + minute : minute,
+        '{s}': second < 10 ? '0' + second : second
+      };
+
+      return format.replace(/\{(\w+)\}/g, (match, key) => obj[`{${key}}`] || match);
+    }
+  }
+};
+</script>
+
+<style scoped>.mb8 {
+  margin-bottom: 8px;
+}
+
+::v-deep .el-table .el-table__header th {
+  background-color: #f5f7fa;
+}
+
+::v-deep .el-table__body-wrapper {
+  overflow-y: auto;
+}
+</style>

+ 291 - 0
src/views/app/statistics/appWatchlogReport.vue

@@ -0,0 +1,291 @@
+<template>
+  <div class="app-container">
+
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="90px">
+      <el-form-item label="所属部门" prop="deptId">
+        <treeselect style="width:205.4px" v-model="queryParams.deptId" :options="deptTreeOptions" :show-count="true" placeholder="请选择所属部门" />
+      </el-form-item>
+      <el-form-item label="项目" prop="project">
+        <el-select filterable v-model="queryParams.project" placeholder="请选择项目"
+                   clearable size="small">
+          <el-option
+            v-for="item in projectList"
+            :key="item.dictValue"
+            :label="item.dictLabel"
+            :value="item.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="训练营" prop="trainingCampId">
+        <el-select filterable v-model="queryParams.trainingCampId" placeholder="请选择训练营"
+                   clearable size="small"  @change="handleCampChange">
+          <el-option
+            v-for="item in camps"
+            :key="item.dictValue"
+            :label="item.dictLabel"
+            :value="item.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <treeselect style="width: 220px" v-model="queryParams.periodId" :options="deptOptions"
+                    clearable :show-count="true" placeholder="请选择归属营期" value-consists-of="LEAF_PRIORITY"
+                    :normalizer="normalizer" />
+      </el-form-item>
+      <el-form-item>
+        <el-form-item label="看课时间" prop="createTime">
+          <el-date-picker v-model="createTime" size="small" style="width: 220px" value-format="yyyy-MM-dd"
+                          type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"
+                          @change="xdChange"></el-date-picker>
+        </el-form-item>
+        <el-form-item label="下单时间" prop="createTime">
+          <el-date-picker v-model="orderTime" size="small" style="width: 220px" value-format="yyyy-MM-dd"
+                          type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"
+                          @change="ydChange"></el-date-picker>
+        </el-form-item>
+      </el-form-item>
+      <el-form-item label="会员ID" prop="userId">
+        <el-input v-model="queryParams.userId" placeholder="请输入会员ID" clearable size="small" />
+      </el-form-item>
+      <el-form-item label="会员手机号" prop="userPhone">
+        <el-input v-model="queryParams.userPhone" placeholder="请输入会员手机号" clearable size="small" />
+      </el-form-item>
+      <el-form-item label="会员昵称" prop="nickName">
+        <el-input v-model="queryParams.nickName" placeholder="请输入会员昵称" clearable size="small" />
+      </el-form-item>
+
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" border :data="packageList">
+      <el-table-column label="会员id" align="center" prop="userId" />
+      <el-table-column label="会员昵称" align="center" prop="nickName" />
+      <el-table-column label="登录渠道" align="center" prop="loginChannel">
+        <template slot-scope="scope">
+          {{ scope.row.loginChannel ? 'APP' : '小程序' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="所属销售" align="center" prop="salesName" />
+      <el-table-column label="所属销售部门" align="center" prop="salesDept"/>
+      <el-table-column label="所属销售公司" align="center" prop="salesCompany"/>
+      <el-table-column label="训练营" align="center" prop="trainingCampName"/>
+      <el-table-column label="营期" align="center" prop="periodName"/>
+      <el-table-column label="小节名称" align="center" prop="videoTitle" />
+      <el-table-column label="公开课播放时长" align="center" prop="publicDuration" />
+      <el-table-column label="私域看课状态" align="center" prop="privateWatchStatus">
+        <template slot-scope="scope">
+          {{ formatPrivateWatchStatus(scope.row.privateWatchStatus) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="私域课播放时长" align="center" prop="privateWatchDuration" />
+      <el-table-column label="完课时间" align="center" prop="finishTime" />
+      <el-table-column label="答题状态" align="center" prop="answerStatus">
+        <template slot-scope="scope">
+          {{ scope.row.answerStatus ? scope.row.answerStatus : '未答题' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="红包金额" align="center" prop="redPacketAmount" />
+<!--      <el-table-column label="历史疗法订单数" align="center" prop="historyOrderCount" />-->
+    </el-table>
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
+                @pagination="getList" />
+  </div>
+</template>
+
+<script>
+import Treeselect from "@riophae/vue-treeselect";
+import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+import {getCampListLikeName} from "@/api/course/userCourseCamp";
+import {getPeriodListLikeName} from "@/api/course/userCoursePeriod";
+import { getCompanyList } from "@/api/company/company";
+import { selectDeptTree } from "@/api/company/companyDept";
+import { appWatchLogReport, appWatchLogReportExport } from "@/api/app/statistics/appStatistics";
+
+export default {
+  name: "AppWatchlogReport",
+  components: { Treeselect },
+  data() {
+    return {
+      normalizer: function(node) {
+        return {
+          id: node.id || node.dictValue,
+          label: node.label || node.dictLabel,
+          children: node.children
+        }
+      },
+      projectList: [],
+      companys: [],
+      camps: [],
+      deptOptions: [],
+      deptTreeOptions: [],
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 显示搜索条件
+      showSearch: true,
+      startTime: null,
+      endTime: null,
+      // 总条数
+      total: 0,
+      createTime: null,
+      orderTime: null,
+      // 表格数据
+      packageList: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        companyId: null,
+        project: null,
+        trainingCampId: null,
+        periodId: null,
+        sTime: null,
+        eTime: null,
+        orderSTime: null,
+        orderETime: null,
+        deptId: null,
+        userId: null,
+        userPhone: null,
+        nickName: null
+      }
+    };
+  },
+  created() {
+    this.getTreeselect(this.$store.state.user.user.companyId);
+
+    // getCampListLikeName({ "pageNum": 1, "pageSize": 100 }).then(response => {
+    //   this.camps = response.data.list
+    //   if (this.camps != null && this.camps.length > 0) {
+    //     this.companyId = this.camps[0].dictValue;
+    //   }
+    //   this.camps.push({ companyId: "-1", companyName: "无" })
+    // });
+
+    getCompanyList().then(response => {
+      this.companys = response.data;
+      if (this.companys != null && this.companys.length > 0) {
+        this.companyId = this.companys[0].companyId;
+      }
+      this.companys.push({ companyId: "-1", companyName: "无" })
+    });
+
+    this.getList();
+
+    this.getDicts("sys_course_project").then(e => {
+      this.projectList = e.data;
+    });
+  },
+  methods: {
+    // 格式化私域看课状态
+    formatPrivateWatchStatus(status) {
+      const statusMap = {
+        '1': '1看课中',
+        '2': '完课',
+        '3': '待看课',
+        '4': '看课中断',
+      };
+      return statusMap[status] || status;
+    },
+    /** 查询部门下拉树结构 */
+    getTreeselect(companyId) {
+      let queryParams = {};
+      if (companyId) {
+        queryParams.companyId = companyId;
+      }
+      selectDeptTree(queryParams).then(response => {
+        this.deptTreeOptions = response.data;
+      });
+    },
+    handleCampChange(val) {
+      if (val) {
+        getPeriodListLikeName({ campId: val }).then(response => {
+          this.deptOptions = response.data;
+        });
+      } else {
+        this.deptOptions = [];
+      }
+    },
+    xdChange(val) {
+      if (val) {
+        this.queryParams.sTime = val[0];
+        this.queryParams.eTime = val[1];
+      } else {
+        this.queryParams.sTime = null;
+        this.queryParams.eTime = null;
+      }
+    },
+    ydChange(val) {
+      if (val) {
+        this.queryParams.orderSTime = val[0];
+        this.queryParams.orderETime = val[1];
+      } else {
+        this.queryParams.orderSTime = null;
+        this.queryParams.orderETime = null;
+      }
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.createTime = null;
+      this.orderTime = null;
+      this.queryParams.sTime = null;
+      this.queryParams.eTime = null;
+      this.queryParams.orderSTime = null;
+      this.queryParams.orderETime = null;
+      this.queryParams.trainingCampId = null;
+      this.queryParams.periodId = null;
+      this.handleQuery();
+    },
+    /** 查询列表 */
+    getList() {
+      this.loading = true;
+      appWatchLogReport(this.queryParams).then(response => {
+        this.packageList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      }).catch(() => {
+        this.loading = false;
+      });
+    },
+
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出APP会员看课统计数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        this.exportLoading = true;
+        return appWatchLogReportExport(queryParams);
+      }).then(response => {
+        this.download(response.msg);
+        this.exportLoading = false;
+      }).catch(() => {});
+    }
+  }
+};
+</script>

+ 399 - 0
src/views/app/statistics/appWatchlogReportStatistics.vue

@@ -0,0 +1,399 @@
+<template>
+  <div class="app-container">
+
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="90px">
+      <el-form-item label="统计维度" prop="dimension">
+        <el-radio-group v-model="queryParams.dimension" @change="handleDimensionChange">
+          <el-radio-button label="sales">销售维度</el-radio-button>
+          <el-radio-button label="dept">部门维度</el-radio-button>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="所属部门" prop="deptId">
+        <treeselect style="width:205.4px" v-model="queryParams.deptId" :options="deptTreeOptions" :show-count="true" placeholder="请选择所属部门" />
+      </el-form-item>
+      <el-form-item label="项目" prop="project">
+        <el-select filterable v-model="queryParams.project" placeholder="请选择项目"
+                   clearable size="small">
+          <el-option
+            v-for="item in projectList"
+            :key="item.dictValue"
+            :label="item.dictLabel"
+            :value="item.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="训练营" prop="trainingCampId">
+        <el-select filterable v-model="queryParams.trainingCampId" placeholder="请选择训练营"
+                   clearable size="small"  @change="handleCampChange">
+          <el-option
+            v-for="item in camps"
+            :key="item.dictValue"
+            :label="item.dictLabel"
+            :value="item.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <treeselect style="width: 220px" v-model="queryParams.periodId" :options="deptOptions"
+                    clearable :show-count="true" placeholder="请选择归属营期" value-consists-of="LEAF_PRIORITY"
+                    :normalizer="normalizer" />
+      </el-form-item>
+      <el-form-item>
+        <el-form-item label="看课时间" prop="createTime">
+          <el-date-picker v-model="createTime" size="small" style="width: 220px" value-format="yyyy-MM-dd"
+                          type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"
+                          @change="xdChange"></el-date-picker>
+        </el-form-item>
+<!--        <el-form-item label="下单时间" prop="orderTime">-->
+<!--          <el-date-picker v-model="orderTime" size="small" style="width: 220px" value-format="yyyy-MM-dd"-->
+<!--                          type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"-->
+<!--                          @change="ydChange"></el-date-picker>-->
+<!--        </el-form-item>-->
+      </el-form-item>
+      <el-form-item label="会员ID" prop="userId">
+        <el-input v-model="queryParams.userId" placeholder="请输入会员ID" clearable size="small" />
+      </el-form-item>
+      <el-form-item label="会员手机号" prop="userPhone">
+        <el-input v-model="queryParams.userPhone" placeholder="请输入会员手机号" clearable size="small" />
+      </el-form-item>
+      <el-form-item label="会员昵称" prop="nickName">
+        <el-input v-model="queryParams.nickName" placeholder="请输入会员昵称" clearable size="small" />
+      </el-form-item>
+
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 销售维度表格 -->
+    <el-table v-if="queryParams.dimension === 'sales'" v-loading="loading" border :data="packageList">
+      <el-table-column label="销售" align="center" prop="salesName" />
+      <el-table-column label="APP会员数" align="center" prop="appUserCount" />
+      <el-table-column label="新注册APP会员数" align="center" prop="newAppUserCount" />
+      <el-table-column label="所属销售部门" align="center" prop="salesDept"/>
+      <el-table-column label="所属销售公司" align="center" prop="salesCompany"/>
+      <el-table-column label="训练营" align="center" prop="trainingCampName"/>
+      <el-table-column label="营期" align="center" prop="periodName"/>
+      <el-table-column label="课程小节" align="center" prop="videoTitle" />
+      <el-table-column label="完课数" align="center" prop="finishedCount" />
+      <el-table-column label="未完课数" align="center" prop="unfinishedCount" />
+      <el-table-column label="完课率" align="center" prop="completionRate" />
+      <el-table-column label="未看数" align="center" prop="notWatchedCount" />
+      <el-table-column label="未答题数" align="center" prop="notAnsweredCount" />
+      <el-table-column label="红包金额" align="center" prop="redPacketAmount" />
+<!--      <el-table-column label="历史疗法订单数" align="center" prop="historyOrderCount" />-->
+    </el-table>
+
+    <!-- 部门维度表格 -->
+    <el-table v-else v-loading="loading" border :data="packageList">
+      <el-table-column label="销售部门" align="center" prop="salesDept" />
+      <el-table-column label="销售数" align="center" prop="salesCount" />
+      <el-table-column label="APP会员数" align="center" prop="appUserCount" />
+      <el-table-column label="新注册APP会员数" align="center" prop="newAppUserCount" />
+      <el-table-column label="所属销售公司" align="center" prop="salesCompany"/>
+      <el-table-column label="训练营" align="center" prop="trainingCampName"/>
+      <el-table-column label="营期" align="center" prop="periodName"/>
+      <el-table-column label="课程小节" align="center" prop="videoTitle" />
+      <el-table-column label="完课数" align="center" prop="finishedCount" />
+      <el-table-column label="未完课数" align="center" prop="unfinishedCount" />
+      <el-table-column label="完课率" align="center" prop="completionRate" />
+      <el-table-column label="未看数" align="center" prop="notWatchedCount" />
+      <el-table-column label="未答题数" align="center" prop="notAnsweredCount" />
+      <el-table-column label="红包金额" align="center" prop="redPacketAmount" />
+<!--      <el-table-column label="历史疗法订单数" align="center" prop="historyOrderCount" />-->
+    </el-table>
+
+    <div class="total-summary">
+      <span class="total-title">总计:</span>
+      <span class="total-item">完课数: {{ calculatedTotalData.finishedCount }}</span>
+      <span class="total-item">未完课数: {{ calculatedTotalData.unfinishedCount }}</span>
+      <span class="total-item">完课率: {{ calculatedTotalData.completionRate }}</span>
+      <span class="total-item">未看数: {{ calculatedTotalData.notWatchedCount }}</span>
+      <span class="total-item">未答题数: {{ calculatedTotalData.notAnsweredCount }}</span>
+      <span class="total-item">红包金额: {{ calculatedTotalData.redPacketAmount }}</span>
+<!--      <span class="total-item">历史疗法订单数: {{ calculatedTotalData.historyOrderCount }}</span>-->
+    </div>
+
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
+                @pagination="getList" />
+  </div>
+</template>
+
+<script>
+import Treeselect from "@riophae/vue-treeselect";
+import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+import {getCampListLikeName} from "@/api/course/userCourseCamp";
+import {getPeriodListLikeName} from "@/api/course/userCoursePeriod";
+import { getCompanyList } from "@/api/company/company";
+import { selectDeptTree } from "@/api/company/companyDept";
+import { appSalesWatchLogReport, appSalesWatchLogReportExport } from "@/api/app/statistics/appStatistics";
+
+export default {
+  name: "AppWatchlogReportStatistics",
+  components: { Treeselect },
+  data() {
+    return {
+      normalizer: function(node) {
+        return {
+          id: node.id || node.dictValue,
+          label: node.label || node.dictLabel,
+          children: node.children
+        }
+      },
+      projectList: [],
+      companys: [],
+      camps: [],
+      deptOptions: [],
+      deptTreeOptions: [],
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      createTime: [],
+      // orderTime: null,
+      // 表格数据
+      packageList: [],
+      // 总计数据
+      calculatedTotalData: {
+        finishedCount: 0,
+        unfinishedCount: 0,
+        completionRate: 0,
+        notWatchedCount: 0,
+        notAnsweredCount: 0,
+        redPacketAmount: 0,
+        historyOrderCount: 0
+      },
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        companyId: null,
+        project: null,
+        trainingCampId: null,
+        periodId: null,
+        sTime: null,
+        eTime: null,
+        orderSTime: null,
+        orderETime: null,
+        deptId: null,
+        userId: null,
+        userPhone: null,
+        nickName: null,
+        dimension: 'sales'
+      }
+    };
+  },
+  created() {
+    // 设置默认看课时间为当天
+    const today = this.parseTime(new Date(), '{y}-{m}-{d}');
+    this.createTime = [today, today];
+    this.queryParams.sTime = today;
+    this.queryParams.eTime = today;
+
+    this.getTreeselect(this.$store.state.user.user.companyId);
+
+    getCampListLikeName({ "pageNum": 1, "pageSize": 100 }).then(response => {
+      this.camps = response.data.list
+      if (this.camps != null && this.camps.length > 0) {
+        this.companyId = this.camps[0].dictValue;
+      }
+      this.camps.push({ companyId: "-1", companyName: "无" })
+    });
+
+    getCompanyList().then(response => {
+      this.companys = response.data;
+      if (this.companys != null && this.companys.length > 0) {
+        this.companyId = this.companys[0].companyId;
+      }
+      this.companys.push({ companyId: "-1", companyName: "无" })
+    });
+
+    this.getList();
+
+    this.getDicts("sys_course_project").then(e => {
+      this.projectList = e.data;
+    });
+  },
+  methods: {
+    /** 维度切换处理 */
+    handleDimensionChange(val) {
+      this.queryParams.dimension = val;
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+
+    /** 查询部门下拉树结构 */
+    getTreeselect(companyId) {
+      let queryParams = {};
+      if (companyId) {
+        queryParams.companyId = companyId;
+      }
+      selectDeptTree(queryParams).then(response => {
+        this.deptTreeOptions = response.data;
+      });
+    },
+
+    /** 训练营变更处理 */
+    handleCampChange(val) {
+      this.queryParams.trainingCampId = val;
+      this.queryParams.periodId = null; // 清空已选择的营期
+
+      if (val) {
+        // 获取对应的营期数据
+        this.getPeriodByCamp(val);
+      } else {
+        // 如果清空训练营,也清空营期选项
+        this.deptOptions = [];
+      }
+    },
+
+    /** 根据训练营获取营期数据 */
+    getPeriodByCamp(campId) {
+      const param = { campId: campId };
+      getPeriodListLikeName(param).then((response) => {
+        this.deptOptions = response.data.list || [];
+      }).catch(error => {
+        console.error('获取营期数据失败:', error);
+        this.deptOptions = [];
+      });
+    },
+
+    xdChange(val) {
+      if (val) {
+        this.queryParams.sTime = val[0];
+        this.queryParams.eTime = val[1];
+      } else {
+        this.queryParams.sTime = null;
+        this.queryParams.eTime = null;
+      }
+    },
+
+    ydChange(val) {
+      if (val) {
+        this.queryParams.orderSTime = val[0];
+        this.queryParams.orderETime = val[1];
+      } else {
+        this.queryParams.orderSTime = null;
+        this.queryParams.orderETime = null;
+      }
+    },
+
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.createTime = null;
+      // this.orderTime = null;
+      this.queryParams.sTime = null;
+      this.queryParams.eTime = null;
+      this.queryParams.orderSTime = null;
+      this.queryParams.orderETime = null;
+      this.queryParams.trainingCampId = null;
+      this.queryParams.periodId = null;
+      this.handleQuery();
+    },
+
+    /** 计算总计 */
+    calculateTotals() {
+      // 重置总计数据
+      this.calculatedTotalData = {
+        finishedCount: 0,
+        unfinishedCount: 0,
+        completionRate: 0,
+        notWatchedCount: 0,
+        notAnsweredCount: 0,
+        redPacketAmount: 0,
+        historyOrderCount: 0
+      };
+      // 遍历当前页数据计算总和
+      this.packageList.forEach(item => {
+        this.calculatedTotalData.finishedCount += item.finishedCount || 0;
+        this.calculatedTotalData.unfinishedCount += item.unfinishedCount || 0;
+        this.calculatedTotalData.notWatchedCount += item.notWatchedCount || 0;
+        this.calculatedTotalData.notAnsweredCount += item.notAnsweredCount || 0;
+        this.calculatedTotalData.redPacketAmount += item.redPacketAmount || 0;
+        this.calculatedTotalData.historyOrderCount += item.historyOrderCount || 0;
+      });
+      // 计算完课率
+      const total = this.calculatedTotalData.finishedCount + this.calculatedTotalData.unfinishedCount;
+      this.calculatedTotalData.completionRate = total > 0
+        ? ((this.calculatedTotalData.finishedCount / total) * 100).toFixed(2) + '%'
+        : '0%';
+    },
+
+    /** 查询列表 */
+    getList() {
+      this.loading = true;
+      // this.queryParams={
+      //   pageNum: 1,
+      //   pageSize: 10,
+      // }
+      appSalesWatchLogReport(this.queryParams).then(response => {
+        this.packageList = response.rows;
+        this.total = response.total;
+        this.calculateTotals();
+        this.loading = false;
+      }).catch(() => {
+        this.loading = false;
+      });
+    },
+
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      const dimensionText = queryParams.dimension === 'sales' ? '销售维度' : '部门维度';
+      this.$confirm('是否确认导出' + dimensionText + 'APP看课统计数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        this.exportLoading = true;
+        return appSalesWatchLogReportExport(queryParams);
+      }).then(response => {
+        this.download(response.msg);
+        this.exportLoading = false;
+      }).catch(() => {});
+    }
+  }
+};
+</script>
+
+<style scoped>
+.total-summary {
+  margin-top: 15px;
+  padding: 10px;
+  background-color: #f5f7fa;
+  border-radius: 4px;
+}
+.total-title {
+  font-weight: bold;
+  margin-right: 10px;
+}
+.total-item {
+  margin-right: 20px;
+}
+</style>

+ 225 - 0
src/views/statistics/redPacket/index.vue

@@ -0,0 +1,225 @@
+<template>
+  <div class="app-container">
+    <el-form class="search-form" :inline="true">
+<!--      <el-form-item label="销售名称">-->
+<!--        <el-input-->
+<!--          v-model="userName"-->
+<!--          placeholder="请输入销售名称"-->
+<!--          clearable-->
+<!--          size="small"-->
+<!--          @keyup.enter.native="handleQuery"-->
+<!--        />-->
+<!--      </el-form-item>-->
+      <el-form-item label="创建时间">
+        <el-date-picker
+          v-model="daterangeCreateTime"
+          size="small"
+          style="width: 240px"
+          value-format="yyyy-MM-dd"
+          type="daterange"
+          range-separator="-"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+        ></el-date-picker>
+      </el-form-item>
+      <el-form-item>
+        <el-button
+          type="cyan"
+          icon="el-icon-search"
+          @click="handleQuery"
+          v-hasPermi="['company:statistics:redPacket']"
+        >搜索</el-button>
+        <el-button
+          type="warning"
+          icon="el-icon-download"
+          @click="handleExport"
+          v-hasPermi="['company:statistics:exportRedPacket']"
+        >导出</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-table
+      :data="statisticsList"
+      border
+      :summary-method="getSummaries"
+      show-summary
+      max-height="500"
+      style="width: 100%;"
+      v-loading="loading"
+    >
+      <el-table-column
+        prop="userName"
+        label="销售"
+        align="center"
+      />
+      <el-table-column
+        prop="userCount"
+        label="用户数"
+        align="center"
+      />
+      <el-table-column
+        prop="newUserCount"
+        label="新增用户数"
+        align="center"
+      />
+      <el-table-column
+        prop="finishCount"
+        label="完课数"
+        align="center"
+      />
+      <el-table-column
+        prop="redPacketAmount"
+        label="红包金额"
+        align="center"
+      />
+    </el-table>
+  </div>
+</template>
+
+<script>
+import { redPacketStatisticsBySales, exportRedPacketStatisticsBySales } from "@/api/company/statistics";
+
+export default {
+  name: 'RedPacketStatistics',
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 统计数据
+      statisticsList: [],
+      // 销售名称
+      userName: undefined,
+      // 创建时间范围
+      daterangeCreateTime: [],
+      // 查询参数
+      queryParams: {
+        userName: null,
+        startDate: null,
+        endDate: null
+      }
+    }
+  },
+  created() {
+    // 设置默认时间为今天
+    const today = this.formatDate(new Date());
+    this.daterangeCreateTime = [today, today];
+
+    this.getStatisticsList();
+  },
+  methods: {
+    /** 格式化日期 */
+    formatDate(date) {
+      const year = date.getFullYear();
+      const month = String(date.getMonth() + 1).padStart(2, '0');
+      const day = String(date.getDate()).padStart(2, '0');
+      return `${year}-${month}-${day}`;
+    },
+    /** 查询统计列表 */
+    getStatisticsList() {
+      this.loading = true;
+
+      // 处理时间参数
+      if (null != this.daterangeCreateTime && '' != this.daterangeCreateTime) {
+        this.queryParams.startDate = this.daterangeCreateTime[0] + ' 00:00:00';
+        this.queryParams.endDate = this.daterangeCreateTime[1] + ' 23:59:59';
+      } else {
+        this.queryParams.startDate = null;
+        this.queryParams.endDate = null;
+      }
+      this.queryParams.userName = this.userName;
+
+      redPacketStatisticsBySales(this.queryParams).then((response) => {
+        console.log('返回数据:', response);
+        // 根据返回的数据结构,可能是 list 或者 rows
+        this.statisticsList = response.list || response.rows || response.data || [];
+        this.loading = false;
+      }).catch(() => {
+        this.loading = false;
+      });
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getStatisticsList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.userName = null;
+      const today = this.formatDate(new Date());
+      this.daterangeCreateTime = [today, today];
+      this.handleQuery();
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      this.$confirm('是否确认导出所有红包统计数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        // 处理时间参数
+        if (null != this.daterangeCreateTime && '' != this.daterangeCreateTime) {
+          this.queryParams.startDate = this.daterangeCreateTime[0] + ' 00:00:00';
+          this.queryParams.endDate = this.daterangeCreateTime[1] + ' 23:59:59';
+        } else {
+          this.queryParams.startDate = null;
+          this.queryParams.endDate = null;
+        }
+        this.queryParams.userName = this.userName;
+
+        return exportRedPacketStatisticsBySales(this.queryParams);
+      }).then(response => {
+        this.download(response.msg);
+      }).catch(() => {});
+    },
+    /** 合计行 */
+    getSummaries(param) {
+      const { columns, data } = param;
+      const sums = [];
+      columns.forEach((column, index) => {
+        if (index === 0) {
+          sums[index] = '总计';
+          return;
+        }
+        const values = data.map(item => Number(item[column.property]));
+        if (!values.every(value => isNaN(value))) {
+          sums[index] = values.reduce((prev, curr) => {
+            const value = Number(curr);
+            if (!isNaN(value)) {
+              return prev + curr;
+            } else {
+              return prev;
+            }
+          }, 0);
+          // 红包金额保留两位小数
+          if (column.property === 'redPacketAmount') {
+            sums[index] = sums[index].toFixed(2);
+          } else {
+            sums[index] = Math.round(sums[index]);
+          }
+        } else {
+          sums[index] = '';
+        }
+      });
+      return sums;
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+  border: 1px solid #e6e6e6;
+  padding: 12px;
+  background-color: white;
+
+  .search-form {
+    margin: 20px 30px 0px 30px;
+  }
+
+  ::v-deep .el-table__footer-wrapper {
+    .cell {
+      font-weight: bold;
+    }
+  }
+}
+</style>