吴树波 4 днів тому
батько
коміт
2772411fec

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

@@ -167,6 +167,24 @@ export function getUserIdList(query) {
   })
 }
 
+/** 按项目查询课程下拉(完播统计用) */
+export function listCoursesByProject(project) {
+  return request({
+    url: '/course/courseWatchLog/listCoursesByProject',
+    method: 'get',
+    params: { project }
+  })
+}
+
+/** 按课程查询小节下拉(完播统计小节区间用) */
+export function listVideosByCourse(courseId) {
+  return request({
+    url: '/course/courseWatchLog/listVideosByCourse',
+    method: 'get',
+    params: { courseId }
+  })
+}
+
 // 导出用户ID列表
 export function exportUserIdList(query) {
   return request({

+ 318 - 37
src/views/course/sop/userIdList.vue

@@ -2,26 +2,6 @@
   <div class="app-container">
     <!-- 搜索区域 -->
     <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="100px">
-<!--      <el-form-item label="公司名称" prop="companyName">-->
-<!--        <el-input-->
-<!--          v-model="queryParams.companyName"-->
-<!--          placeholder="请输入公司名称"-->
-<!--          clearable-->
-<!--          size="small"-->
-<!--          style="width: 180px"-->
-<!--          @keyup.enter.native="handleQuery"-->
-<!--        />-->
-<!--      </el-form-item>-->
-<!--      <el-form-item label="小程序ID" prop="appId">-->
-<!--       <el-select v-model="queryParams.appId" placeholder="请选择所属小程序" clearable size="small">-->
-<!--          <el-option-->
-<!--            v-for="item in appMallOptions"-->
-<!--            :key="item.appId"-->
-<!--            :label="item.appName"-->
-<!--            :value="item.appId"-->
-<!--          />-->
-<!--        </el-select>-->
-<!--      </el-form-item>-->
       <el-form-item
         label="创建时间"
         prop="createTime"
@@ -37,6 +17,116 @@
           :clearable="false"
         />
       </el-form-item>
+      <el-form-item label="项目" prop="project">
+        <el-select
+          v-model="queryParams.project"
+          placeholder="请选择项目"
+          clearable
+          filterable
+          size="small"
+          style="width: 160px"
+          @change="handleProjectChange"
+        >
+          <el-option
+            v-for="dict in projectOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="parseInt(dict.dictValue, 10)"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="课程" prop="courseId">
+        <el-select
+          v-model="queryParams.courseId"
+          placeholder="请选择课程"
+          clearable
+          filterable
+          size="small"
+          style="width: 220px"
+          :disabled="!queryParams.project"
+          @change="handleCourseChange"
+        >
+          <el-option
+            v-for="item in courseOptions"
+            :key="item.courseId"
+            :label="formatCourseLabel(item)"
+            :value="item.courseId"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="小节" prop="videoIds">
+        <div class="video-filter-wrap">
+          <div class="video-filter">
+            <el-select
+              v-model="queryParams.videoIds"
+              placeholder="可不选;不选则按整课统计"
+              multiple
+              filterable
+              clearable
+              collapse-tags
+              size="small"
+              style="width: 320px"
+              :disabled="!queryParams.courseId"
+              :filter-method="filterVideoOption"
+              @visible-change="handleVideoSelectVisible"
+            >
+              <el-option
+                v-for="item in filteredVideoOptions"
+                :key="item.videoId"
+                :label="formatVideoLabel(item)"
+                :value="item.videoId"
+              />
+            </el-select>
+            <el-button
+              type="primary"
+              plain
+              size="mini"
+              :disabled="!queryParams.courseId || !videoOptions.length"
+              @click="selectAllVideos"
+            >全选</el-button>
+            <el-button
+              plain
+              size="mini"
+              :disabled="!queryParams.courseId || !queryParams.videoIds || !queryParams.videoIds.length"
+              @click="clearVideos"
+            >清空</el-button>
+            <span class="video-quick">
+              第
+              <el-input-number
+                v-model="quickFrom"
+                :min="1"
+                :max="videoOptions.length || 1"
+                :controls="false"
+                size="mini"
+                style="width: 56px"
+                :disabled="!queryParams.courseId"
+              />
+              -
+              <el-input-number
+                v-model="quickTo"
+                :min="1"
+                :max="videoOptions.length || 1"
+                :controls="false"
+                size="mini"
+                style="width: 56px"
+                :disabled="!queryParams.courseId"
+              />
+              节
+              <el-button
+                type="primary"
+                plain
+                size="mini"
+                :disabled="!queryParams.courseId || !videoOptions.length"
+                @click="applyQuickRange"
+              >按区间选择</el-button>
+            </span>
+          </div>
+          <div class="video-filter-tip">
+            <i class="el-icon-info"></i>
+            提示:不选小节=按整课统计;「全选 / 按区间选择」都会重新设定已选小节(覆盖上次选择);「清空」清除已选。
+          </div>
+        </div>
+      </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>
@@ -79,7 +169,7 @@
 </template>
 
 <script>
-import {getUserIdList, exportUserIdList } from "@/api/course/qw/courseWatchLog";
+import {getUserIdList, exportUserIdList, listCoursesByProject, listVideosByCourse } from "@/api/course/qw/courseWatchLog";
 import {getMiniProgramList } from "@/api/hisStore/storePayment";
 
 export default {
@@ -91,21 +181,33 @@ export default {
       total: 0,
       appRegisterList: [],
       appMallOptions:[],
+      projectOptions: [],
+      courseOptions: [],
+      videoOptions: [],
+      filteredVideoOptions: [],
+      // 快捷按课节序号勾选
+      quickFrom: 1,
+      quickTo: 1,
       queryParams: {
         pageNum: 1,
         pageSize: 10,
         companyName: null,
         appId: null,
-        createTime: this.getCurrentDate() // 设置默认值为当前时间
+        createTime: this.getCurrentDate(),
+        project: null,
+        courseId: null,
+        videoIds: []
       }
     };
   },
   created() {
+    this.getDicts('sys_course_project').then(res => {
+      this.projectOptions = res.data || [];
+    });
     this.getList();
     this.getMiniProgramList();
   },
   methods: {
-    /** 获取当前日期(YYYY-MM-DD格式) */
     getCurrentDate() {
       const now = new Date();
       const year = now.getFullYear();
@@ -114,7 +216,6 @@ export default {
       return `${year}-${month}-${day}`;
     },
 
-    // 获取小程序选项列表
     getMiniProgramList() {
       this.appMallOptions = []
       getMiniProgramList().then(response => {
@@ -130,16 +231,161 @@ export default {
       })
     },
 
-    /** 查询列表 */
+    formatCourseLabel(item) {
+      if (!item) {
+        return '';
+      }
+      return item.courseName || '';
+    },
+
+    formatVideoLabel(item) {
+      if (!item) {
+        return '';
+      }
+      const idx = this.videoOptions.findIndex(v => v.videoId === item.videoId);
+      const seq = idx >= 0 ? idx + 1 : (item.courseSort != null ? item.courseSort : '-');
+      const title = item.title || '未命名小节';
+      return `第${seq}节 · ${title}`;
+    },
+
+    filterVideoOption(query) {
+      const q = (query || '').trim().toLowerCase();
+      if (!q) {
+        this.filteredVideoOptions = this.videoOptions.slice();
+        return;
+      }
+      this.filteredVideoOptions = this.videoOptions.filter(item => {
+        const title = (item.title || '').toLowerCase();
+        const label = this.formatVideoLabel(item).toLowerCase();
+        return title.indexOf(q) !== -1 || label.indexOf(q) !== -1;
+      });
+    },
+
+    handleVideoSelectVisible(visible) {
+      if (visible) {
+        this.filteredVideoOptions = this.videoOptions.slice();
+      }
+    },
+
+    handleProjectChange() {
+      this.queryParams.courseId = null;
+      this.queryParams.videoIds = [];
+      this.courseOptions = [];
+      this.videoOptions = [];
+      this.filteredVideoOptions = [];
+      this.quickFrom = 1;
+      this.quickTo = 1;
+      if (this.queryParams.project != null) {
+        this.loadCourseOptions(this.queryParams.project);
+      }
+    },
+
+    handleCourseChange() {
+      this.queryParams.videoIds = [];
+      this.videoOptions = [];
+      this.filteredVideoOptions = [];
+      this.quickFrom = 1;
+      this.quickTo = 1;
+      if (this.queryParams.courseId != null) {
+        this.loadVideoOptions(this.queryParams.courseId);
+      }
+    },
+
+    loadCourseOptions(project) {
+      listCoursesByProject(project).then(response => {
+        this.courseOptions = response.data || [];
+      }).catch(() => {
+        this.courseOptions = [];
+        this.$message.error('加载课程列表失败');
+      });
+    },
+
+    loadVideoOptions(courseId) {
+      listVideosByCourse(courseId).then(response => {
+        this.videoOptions = response.data || [];
+        this.filteredVideoOptions = this.videoOptions.slice();
+        this.quickFrom = 1;
+        this.quickTo = this.videoOptions.length || 1;
+      }).catch(() => {
+        this.videoOptions = [];
+        this.filteredVideoOptions = [];
+        this.$message.error('加载小节列表失败');
+      });
+    },
+
+    selectAllVideos() {
+      this.queryParams.videoIds = this.videoOptions.map(v => v.videoId);
+      this.$message.success(`已重新选择全部小节,共${this.queryParams.videoIds.length}个`);
+    },
+
+    clearVideos() {
+      this.queryParams.videoIds = [];
+      this.$message.success('已清空小节选择');
+    },
+
+    /** 按课节序号重新选择(覆盖已选) */
+    applyQuickRange() {
+      if (!this.videoOptions.length) {
+        return;
+      }
+      let from = this.quickFrom == null ? 1 : this.quickFrom;
+      let to = this.quickTo == null ? this.videoOptions.length : this.quickTo;
+      if (from < 1) {
+        from = 1;
+      }
+      if (to > this.videoOptions.length) {
+        to = this.videoOptions.length;
+      }
+      if (from > to) {
+        this.$message.warning('起始节数不能大于结束节数');
+        return;
+      }
+      const ids = [];
+      for (let i = from - 1; i < to; i++) {
+        ids.push(this.videoOptions[i].videoId);
+      }
+      this.queryParams.videoIds = ids;
+      this.$message.success(`已重新选择第${from}-${to}节,共${ids.length}个小节`);
+    },
+
+    validateFilters() {
+      if (this.queryParams.courseId != null && this.queryParams.project == null) {
+        this.$message.warning('请先选择项目后再选择课程');
+        return false;
+      }
+      if (this.queryParams.videoIds && this.queryParams.videoIds.length > 0 && this.queryParams.courseId == null) {
+        this.$message.warning('请先选择课程后再选择小节');
+        return false;
+      }
+      return true;
+    },
+
+    /** 组装查询参数:小节以 videoIdList(逗号分隔)精确传 video_id */
+    buildQueryParams() {
+      const params = {
+        pageNum: this.queryParams.pageNum,
+        pageSize: this.queryParams.pageSize,
+        createTime: this.queryParams.createTime,
+        project: this.queryParams.project,
+        courseId: this.queryParams.courseId
+      };
+      if (this.queryParams.videoIds && this.queryParams.videoIds.length > 0) {
+        params.videoIdList = this.queryParams.videoIds.join(',');
+      }
+      return params;
+    },
+
     getList() {
-      // 验证创建时间是否为空
       if (!this.queryParams.createTime) {
         this.$message.warning('请选择创建时间')
         return
       }
+      if (!this.validateFilters()) {
+        return
+      }
 
       this.loading = true;
-      getUserIdList(this.queryParams).then(response => {
+      getUserIdList(this.buildQueryParams()).then(response => {
         this.appRegisterList = response.rows || [];
         this.total = response.total || 0;
         this.loading = false;
@@ -148,11 +394,12 @@ export default {
       });
     },
 
-    /** 搜索按钮操作 */
     handleQuery() {
-      // 验证表单
       this.$refs.queryForm.validate(valid => {
         if (valid) {
+          if (!this.validateFilters()) {
+            return
+          }
           this.queryParams.pageNum = 1;
           this.getList();
         } else {
@@ -161,31 +408,35 @@ export default {
       });
     },
 
-    /** 重置按钮操作 */
     resetQuery() {
-      // 重置表单
       this.$refs.queryForm.resetFields();
-      // 重置后恢复默认当前时间
       this.queryParams.createTime = this.getCurrentDate();
-      // 重置后立即查询
+      this.queryParams.project = null;
+      this.queryParams.courseId = null;
+      this.queryParams.videoIds = [];
+      this.courseOptions = [];
+      this.videoOptions = [];
+      this.filteredVideoOptions = [];
+      this.quickFrom = 1;
+      this.quickTo = 1;
       this.queryParams.pageNum = 1;
       this.getList();
     },
 
-    /** 导出按钮操作 */
     handleExport() {
-      // 验证创建时间是否为空
       if (!this.queryParams.createTime) {
         this.$message.warning('请选择创建时间');
         return;
       }
+      if (!this.validateFilters()) {
+        return;
+      }
 
       this.$confirm('是否确认导出数据?', '提示', {
         confirmButtonText: '确定',
         cancelButtonText: '取消',
         type: 'warning'
       }).then(() => {
-        // 显示全屏加载提示
         const loadingInstance = this.$loading({
           lock: true,
           text: '正在导出数据,请稍候...',
@@ -194,7 +445,7 @@ export default {
 
         this.exportLoading = true;
 
-        exportUserIdList(this.queryParams)
+        exportUserIdList(this.buildQueryParams())
           .then(response => {
             this.download(response.msg);
           })
@@ -217,4 +468,34 @@ export default {
 .app-container {
   padding: 20px;
 }
+.video-filter-wrap {
+  display: inline-flex;
+  flex-direction: column;
+  align-items: flex-start;
+}
+.video-filter {
+  display: inline-flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 4px 8px;
+}
+.video-quick {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  margin-left: 4px;
+  color: #606266;
+  font-size: 12px;
+}
+.video-filter-tip {
+  margin-top: 6px;
+  color: #909399;
+  font-size: 12px;
+  line-height: 1.5;
+  max-width: 760px;
+}
+.video-filter-tip .el-icon-info {
+  margin-right: 4px;
+  color: #909399;
+}
 </style>

+ 46 - 3
src/views/statistics/section/index.vue

@@ -42,6 +42,21 @@
         @dateRangeChange="dateRangeChange"
         ></date-range>
       </el-form-item>
+      <el-form-item label="项目" prop="project">
+        <el-select v-model="queryParams.project" placeholder="请选择项目" clearable filterable style="width: 160px">
+          <el-option
+            v-for="dict in projectOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="parseInt(dict.dictValue, 10)"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="课程区间" label-width="70px">
+        <el-input-number v-model="queryParams.courseStart" :min="1" :controls="true" placeholder="起" style="width: 110px" />
+        <span style="margin: 0 6px">-</span>
+        <el-input-number v-model="queryParams.courseEnd" :min="1" :controls="true" placeholder="止" style="width: 110px" />
+      </el-form-item>
       <el-form-item label="包含未发课" label-width="120px">
         <el-checkbox v-model="queryParams.includeSend0">
         </el-checkbox>
@@ -185,6 +200,8 @@ export default {
       // 日期范围
       dateRange: [],
       deptList: [],
+      // 项目字典
+      projectOptions: [],
       // 查询参数
       queryParams: {
         pageNum: 1,
@@ -194,6 +211,9 @@ export default {
         endDate: null,
         companyId: null,
         periodList: [],
+        project: null,
+        courseStart: undefined,
+        courseEnd: undefined,
         // 包含未发课数
         includeSend0: false
       },
@@ -209,9 +229,9 @@ export default {
   created() {
     this.getList();
 
-    // getCompanyList().then(response => {
-    //   this.companys = response.data;
-    // });
+    this.getDicts('sys_course_project').then(res => {
+      this.projectOptions = res.data || [];
+    });
 
     getSOPTaskData({}).then(response => {
       this.channelList = response.data;
@@ -230,6 +250,20 @@ export default {
         this.channelList = response.data;
       });
     },
+    /** 校验项目与课程区间 */
+    validateProjectCourseRange() {
+      const hasRange = this.queryParams.courseStart != null || this.queryParams.courseEnd != null;
+      if (hasRange && this.queryParams.project == null) {
+        this.$message.warning('请先选择项目后再按课程区间筛选');
+        return false;
+      }
+      if (this.queryParams.courseStart != null && this.queryParams.courseEnd != null
+        && this.queryParams.courseStart > this.queryParams.courseEnd) {
+        this.$message.warning('课程区间起始不能大于结束');
+        return false;
+      }
+      return true;
+    },
     getSummaries(param) {
       const { columns, data } = param;
       const sums = [];
@@ -307,6 +341,9 @@ export default {
     },
     /** 查询员工统计列表 */
     getList() {
+      if (!this.validateProjectCourseRange()) {
+        return;
+      }
       this.loading = true;
       this.queryParams.periodList = this.selectedMultipleTasks;
       if (this.dateRange && this.dateRange.length === 2) {
@@ -346,6 +383,9 @@ export default {
       this.dateRange = [];
       this.selectedMultipleTasks = [];
       this.selectedCompanyList = [];
+      this.queryParams.project = null;
+      this.queryParams.courseStart = undefined;
+      this.queryParams.courseEnd = undefined;
       this.resetForm("queryForm");
       this.handleQuery();
     },
@@ -357,6 +397,9 @@ export default {
     },
     /** 导出按钮操作 */
     handleExport() {
+      if (!this.validateProjectCourseRange()) {
+        return;
+      }
       this.queryParams.periodList = this.selectedMultipleTasks;
       if (this.dateRange && this.dateRange.length === 2) {
         this.queryParams.startDate = this.dateRange[0];