Parcourir la source

feat: 新增完课模板功能、优化时间选择

scope: course, qw, system

新增完课模板相关页面及接口,优化SOP模板和系统设置的时间选择组件。
src/views/course/courseFinishTemp/index.vue: 新增完课模板列表页面。
src/api/course/courseFinishTemp.js: 新增完课模板相关API。
src/api/qw/sopTemp.js: 新增时间范围获取API。
src/views/qw/sopTemp/index.vue:  优化时间选择范围。
src/views/qw/sopTemp/updateSopTemp.vue:  优化时间选择范围。
src/views/system/config/config.vue:  优化时间选择范围。
src/views/course/userCourse/index.vue:  优化时间选择范围。
吴树波 il y a 1 mois
Parent
commit
b05154ec08

+ 53 - 0
src/api/course/courseFinishTemp.js

@@ -0,0 +1,53 @@
+import request from '@/utils/request'
+
+// 查询完课模板列表
+export function listCourseFinishTemp(query) {
+  return request({
+    url: '/course/courseFinishTemp/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询完课模板详细
+export function getCourseFinishTemp(id) {
+  return request({
+    url: '/course/courseFinishTemp/' + id,
+    method: 'get'
+  })
+}
+
+// 新增完课模板
+export function addCourseFinishTemp(data) {
+  return request({
+    url: '/course/courseFinishTemp',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改完课模板
+export function updateCourseFinishTemp(data) {
+  return request({
+    url: '/course/courseFinishTemp',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除完课模板
+export function delCourseFinishTemp(id) {
+  return request({
+    url: '/course/courseFinishTemp/' + id,
+    method: 'delete'
+  })
+}
+
+// 导出完课模板
+export function exportCourseFinishTemp(query) {
+  return request({
+    url: '/course/courseFinishTemp/export',
+    method: 'get',
+    params: query
+  })
+}

+ 8 - 0
src/api/qw/sopTemp.js

@@ -140,3 +140,11 @@ export function exportSopTemp(query) {
     params: query
   })
 }
+
+// 导出sop模板
+export function getSelectableRange() {
+  return request({
+    url: '/qw/sopTemp/getSelectableRange',
+    method: 'get'
+  })
+}

+ 1 - 1
src/views/company/CompanyRedPackageLogs/index.vue

@@ -5,7 +5,7 @@
         <el-select v-model="queryParams.status" clearable>
           <el-option label="待审核" :value="0" />
           <el-option label="已通过" :value="1" />
-          <el-option label="已解决" :value="2" />
+          <el-option label="已拒绝" :value="2" />
         </el-select>
       </el-form-item>
       <el-form-item>

+ 55 - 0
src/views/components/course/userCourseCatalogDetails.vue

@@ -45,6 +45,14 @@
           @click="openAdds"
         >批量添加</el-button>
       </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          size="mini"
+          @click="updateRedPageckeOpen"
+        >修改红包</el-button>
+      </el-col>
       <el-col :span="1.5">
         <el-button
           type="danger"
@@ -69,6 +77,7 @@
               {{ formatDuration(row.duration) }}
           </template>
       </el-table-column>
+      <el-table-column label="红包金额" align="center" prop="redPacketMoney" />
       <el-table-column label="排序" align="center" prop="courseSort" />
       <el-table-column label="上传时间" align="center" prop="createTime" />
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
@@ -286,6 +295,28 @@
         <el-button type="primary" @click="batchVideoSave">确 定</el-button>
       </div>
     </el-dialog>
+    <el-dialog title="视频库选择" :visible.sync="redData.open" width="900px" append-to-body>
+      <el-table border v-loading="redData.loading" :data="redData.list" height="600px">
+        <el-table-column label="小节名称" align="center" show-overflow-tooltip prop="title" />
+        <el-table-column label="视频文件名称" align="center" show-overflow-tooltip  prop="fileName" >
+        </el-table-column>
+        <el-table-column label="视频时长" align="center" prop="duration">
+          <template slot-scope="{ row }">
+            {{ formatDuration(row.duration) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="红包金额" align="center" prop="redPacketMoney">
+          <template slot-scope="scope">
+            <el-input class="el-input" v-model="scope.row.redPacketMoney" />
+          </template>
+        </el-table-column>
+        <el-table-column label="排序" align="center" prop="courseSort" />
+        <el-table-column label="上传时间" align="center" prop="createTime" />
+      </el-table>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="batchRedSave">确 定</el-button>
+      </div>
+    </el-dialog>
 
   </div>
 </template>
@@ -349,6 +380,18 @@ export default {
         courseName:null,
         userCourseVideoList:[],
         total: 0,
+        redData:{
+          queryParams: {
+            pageNum: 1,
+            pageSize: 99999,
+            courseId:null,
+          },
+          list: [],
+          open: false,
+          loading:true,
+          form:{
+            }
+        },
         queryParams: {
           pageNum: 1,
           pageSize: 10,
@@ -747,6 +790,18 @@ export default {
           this.getList();
         })
       },
+      updateRedPageckeOpen(){
+        this.redData.open = true;
+        this.redData.loading = true;
+        this.redData.queryParams.courseId = this.courseId;
+        getVideoListByCourseId(this.redData.queryParams).then(response => {
+          this.redData.list = response.rows;
+          this.redData.loading = false;
+        });
+      },
+      batchRedSave(){
+
+      },
     }
   }
 </script>

+ 916 - 0
src/views/course/courseFinishTemp/index.vue

@@ -0,0 +1,916 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="销售公司" prop="companyId">
+        <el-select filterable  v-model="queryParams.companyId" clearable placeholder="请选择公司名" size="small">
+          <el-option
+            v-for="item in companys"
+            :key="item.companyId"
+            :label="item.companyName"
+            :value="item.companyId"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="模板名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入模板名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
+          <el-option
+            v-for="dict in statusOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="课程" prop="courseId">
+        <el-input
+          v-model="queryParams.courseId"
+          placeholder="请输入课程id"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="小节" prop="videoId">
+        <el-input
+          v-model="queryParams.videoId"
+          placeholder="请输入小节视频id"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </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="primary"
+          plain
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['courseFinishTemp:course:add']"
+        >新增
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          plain
+          icon="el-icon-edit"
+          size="mini"
+          :disabled="single"
+          @click="handleUpdate"
+          v-hasPermi="['courseFinishTemp:course:edit']"
+        >修改
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          plain
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+          v-hasPermi="['courseFinishTemp:course:remove']"
+        >删除
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['courseFinishTemp:course:export']"
+        >导出
+        </el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table border v-loading="loading" :data="courseFinishTempList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center"/>
+      <el-table-column label="模板ID" align="center" prop="id"/>
+      <el-table-column label="销售公司" align="center" prop="status">
+        <template slot-scope="scope">
+          <el-tag v-for="item in companys" v-if="item.companyId == scope.row.companyId" >{{item.companyName}}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="模板名称" align="center" prop="name"/>
+      <el-table-column label="状态" align="center" prop="status">
+        <template slot-scope="scope">
+          <dict-tag :options="statusOptions" :value="scope.row.status"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="课程名称" align="center" prop="courseName"/>
+      <el-table-column label="小节名称" align="center" prop="videoName"/>
+      <el-table-column label="创建时间" align="center" prop="createTime"/>
+      <el-table-column label="修改时间" align="center" prop="updateTime"/>
+      <el-table-column label="全选销售" align="center" prop="isAllCompanyUser">
+        <template slot-scope="scope">
+          <dict-tag :options="allowSelect" :value="scope.row.isAllCompanyUser"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+            v-hasPermi="['courseFinishTemp:course:edit']"
+          >修改
+          </el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-s-promotion"
+            @click="handleSelectDetails(scope.row)"
+            v-hasPermi="['courseFinishTemp:course:query']"
+          >详情
+          </el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['courseFinishTemp:course:remove']"
+          >删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改完课模板对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="1000px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="模板名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入模板名称"/>
+        </el-form-item>
+        <el-form-item label="销售公司" prop="companyId">
+          <el-select filterable  v-model="form.companyId" placeholder="请选择公司" size="small" @change="initCompanyUserList">
+            <el-option
+              v-for="item in companys"
+              :key="item.companyId"
+              :label="item.companyName"
+              :value="item.companyId"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="类型">
+          <el-radio-group v-model="form.type">
+            <el-radio :label="1">个人消息</el-radio>
+            <el-radio :label="2">群发消息</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-radio-group v-model="form.status">
+            <el-radio
+              v-for="dict in statusOptions"
+              :key="dict.dictValue"
+              :label="parseInt(dict.dictValue)"
+            >{{ dict.dictLabel }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="选择课程">
+          <el-select v-model="form.courseId" placeholder="请选择课程" style=" margin-right: 10px;" size="mini"
+                     @change="courseChange()">
+            <el-option
+              v-for="dict in courseList"
+              :key="dict.dictValue"
+              :label="dict.dictLabel"
+              :value="parseInt(dict.dictValue)"
+            />
+          </el-select>
+          <el-select v-model="form.videoId" placeholder="请选择小节" size="mini" style=" margin-right: 10px;">
+            <el-option
+              v-for="dict in videoList"
+              :key="dict.dictValue"
+              :label="dict.dictLabel"
+              :value="parseInt(dict.dictValue)"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="规则" prop="setting">
+          <div v-for="(item, index) in setting" :key="index"
+               style="background-color: #fdfdfd; border: 1px solid #e6e6e6; margin-bottom: 20px;">
+            <el-row>
+              <el-col :span="22">
+                <el-form :model="item" label-width="70px">
+                  <el-form-item label="内容类别" style="margin: 2%">
+                    <el-radio-group v-model="item.contentType">
+                      <el-radio :label="item.dictValue" v-for="item in sysQwSopAiContentType">{{ item.dictLabel }}
+                      </el-radio>
+                    </el-radio-group>
+                  </el-form-item>
+                  <el-form-item label="内容" style="margin-bottom: 2%">
+                    <el-input v-if="item.contentType == 1 " v-model="item.value" type="textarea" :rows="3"
+                              placeholder="内容" style="width: 90%;margin-top: 10px;"/>
+
+                    <ImageUpload v-if="item.contentType == 2 " v-model="item.imgUrl" type="image" :num="1" :width="150"
+                                 :height="150"/>
+
+                    <div v-if="item.contentType == 3 ">
+                      <el-card class="box-card">
+                        <el-form-item label="链接标题:" label-width="100px">
+                          <el-input v-model="item.linkTitle" placeholder="请输入链接标题" style="width: 90%;"/>
+                        </el-form-item>
+                        <el-form-item label="链接描述:" label-width="100px">
+                          <el-input type="textarea" :rows="3" v-model="item.linkDescribe" placeholder="请输入链接描述"
+                                    style="width: 90%;margin-top: 1%;"/>
+                        </el-form-item>
+                        <el-form-item label="链接封面:" label-width="100px">
+                          <ImageUpload v-model="item.linkImageUrl" type="image" :num="1" :file-size="2" :width="150"
+                                       :height="150" style="margin-top: 1%;"/>
+                        </el-form-item>
+                        <el-form-item label="链接地址:" label-width="100px">
+                          <el-input v-model="item.linkUrl" placeholder="请输入链接地址" style="width: 90%;"/>
+                        </el-form-item>
+                      </el-card>
+                    </div>
+                    <div v-if="item.contentType == 4">
+
+                    </div>
+                    <div v-if="item.contentType == 5 ">
+
+                      <el-form-item label="上传文件:" prop="fileUrl" label-width="100px">
+                        <el-upload
+                          v-model="item.fileUrl"
+                          class="avatar-uploader"
+                          :action="uploadUrl"
+                          :show-file-list="false"
+                          :on-success="(res, file) => handleAvatarSuccessFile(res, file, item)"
+                          :before-upload="beforeAvatarUploadFile">
+                          <i class="el-icon-plus avatar-uploader-icon"></i>
+                        </el-upload>
+                        <el-link v-if="item.fileUrl" type="primary" :href="downloadUrl(item.fileUrl)" download>
+                          {{ item.fileUrl }}
+                        </el-link>
+                      </el-form-item>
+
+                    </div>
+
+                    <div v-if="item.contentType == 6 ">
+                      <el-form-item label="上传视频:" prop="videoUrl" label-width="100px">
+                        <el-upload
+                          v-model="item.videoUrl"
+                          class="avatar-uploader"
+                          :action="uploadUrl"
+                          :show-file-list="false"
+                          :on-success="(res, file) => handleAvatarSuccessVideo(res, file, item)"
+                          :before-upload="beforeAvatarUploadVideo">
+                          <i class="el-icon-plus avatar-uploader-icon"></i>
+                        </el-upload>
+                        <video v-if="item.videoUrl"
+                               :src="item.videoUrl"
+                               controls style="width: 200px;height: 100px">
+                        </video>
+                      </el-form-item>
+                    </div>
+                    <div v-if="item.contentType == 7 ">
+                      <el-input
+                        v-model="item.value"
+                        type="textarea" :rows="3" maxlength="66" show-word-limit
+                        placeholder="输入要转为语音的内容" style="width: 90%;margin-top: 10px;"
+                        @input="handleInputVideoText(item.value,item)"/>
+                    </div>
+                    <div v-if="item.contentType == 8">
+
+                    </div>
+
+                  </el-form-item>
+                </el-form>
+              </el-col>
+              <el-col :span="1" :offset="1">
+                <i class="el-icon-delete" @click="delSetList(index, 0)" style="margin-top: 20px;"
+                   v-if="setting.length>1 && formType==1"></i>
+              </el-col>
+            </el-row>
+          </div>
+          <el-link type="primary" class="el-icon-plus" :underline="false" @click='addSetList(0)' v-if="formType==1">
+            添加内容
+          </el-link>
+        </el-form-item>
+        <el-form-item label="群聊恭喜规则" prop="setting">
+          <div v-for="(item, index) in chatSetting" :key="index"
+               style="background-color: #fdfdfd; border: 1px solid #e6e6e6; margin-bottom: 20px;">
+            <el-row>
+              <el-col :span="22">
+                <el-form :model="item" label-width="70px">
+                  <el-form-item label="内容类别" style="margin: 2%">
+                    <el-radio-group v-model="item.contentType">
+                      <el-radio :label="item.dictValue" v-for="item in sysQwSopAiContentType">{{ item.dictLabel }}
+                      </el-radio>
+                    </el-radio-group>
+                  </el-form-item>
+                  <el-form-item label="内容" style="margin-bottom: 2%">
+                    <el-input v-if="item.contentType == 1 " v-model="item.value" type="textarea" :rows="3"
+                              placeholder="内容" style="width: 90%;margin-top: 10px;"/>
+
+                    <ImageUpload v-if="item.contentType == 2 " v-model="item.imgUrl" type="image" :num="1" :width="150"
+                                 :height="150"/>
+
+                    <div v-if="item.contentType == 3 ">
+                      <el-card class="box-card">
+                        <el-form-item label="链接标题:" label-width="100px">
+                          <el-input v-model="item.linkTitle" placeholder="请输入链接标题" style="width: 90%;"/>
+                        </el-form-item>
+                        <el-form-item label="链接描述:" label-width="100px">
+                          <el-input type="textarea" :rows="3" v-model="item.linkDescribe" placeholder="请输入链接描述"
+                                    style="width: 90%;margin-top: 1%;"/>
+                        </el-form-item>
+                        <el-form-item label="链接封面:" label-width="100px">
+                          <ImageUpload v-model="item.linkImageUrl" type="image" :num="1" :file-size="2" :width="150"
+                                       :height="150" style="margin-top: 1%;"/>
+                        </el-form-item>
+                        <el-form-item label="链接地址:" label-width="100px">
+                          <el-input v-model="item.linkUrl" placeholder="请输入链接地址" style="width: 90%;"/>
+                        </el-form-item>
+                      </el-card>
+                    </div>
+                    <div v-if="item.contentType == 4">
+
+                    </div>
+                    <div v-if="item.contentType == 5 ">
+
+                      <el-form-item label="上传文件:" prop="fileUrl" label-width="100px">
+                        <el-upload
+                          v-model="item.fileUrl"
+                          class="avatar-uploader"
+                          :action="uploadUrl"
+                          :show-file-list="false"
+                          :on-success="(res, file) => handleAvatarSuccessFile(res, file, item)"
+                          :before-upload="beforeAvatarUploadFile">
+                          <i class="el-icon-plus avatar-uploader-icon"></i>
+                        </el-upload>
+                        <el-link v-if="item.fileUrl" type="primary" :href="downloadUrl(item.fileUrl)" download>
+                          {{ item.fileUrl }}
+                        </el-link>
+                      </el-form-item>
+
+                    </div>
+
+                    <div v-if="item.contentType == 6 ">
+                      <el-form-item label="上传视频:" prop="videoUrl" label-width="100px">
+                        <el-upload
+                          v-model="item.videoUrl"
+                          class="avatar-uploader"
+                          :action="uploadUrl"
+                          :show-file-list="false"
+                          :on-success="(res, file) => handleAvatarSuccessVideo(res, file, item)"
+                          :before-upload="beforeAvatarUploadVideo">
+                          <i class="el-icon-plus avatar-uploader-icon"></i>
+                        </el-upload>
+                        <video v-if="item.videoUrl"
+                               :src="item.videoUrl"
+                               controls style="width: 200px;height: 100px">
+                        </video>
+                      </el-form-item>
+                    </div>
+                    <div v-if="item.contentType == 7 ">
+                      <el-input
+                        v-model="item.value"
+                        type="textarea" :rows="3" maxlength="66" show-word-limit
+                        placeholder="输入要转为语音的内容" style="width: 90%;margin-top: 10px;"
+                        @input="handleInputVideoText(item.value,item)"/>
+                    </div>
+                    <div v-if="item.contentType == 8">
+
+                    </div>
+
+                  </el-form-item>
+                </el-form>
+              </el-col>
+              <el-col :span="1" :offset="1">
+                <i class="el-icon-delete" @click="delSetList(index,1)" style="margin-top: 20px;"
+                   v-if="setting.length>1 && formType==1"></i>
+              </el-col>
+            </el-row>
+          </div>
+          <el-link type="primary" class="el-icon-plus" :underline="false" @click='addSetList(1)' v-if="formType==1">
+            添加内容
+          </el-link>
+        </el-form-item>
+        <el-form-item label="全选销售" prop="isAllCompanyUser">
+          <el-switch
+            v-model="form.isAllCompanyUser"
+            active-color="#13ce66"
+            inactive-color="#ff4949"
+            :active-value="1"
+            :inactive-value="2">
+          </el-switch>
+          <span v-if="form.isAllCompanyUser == '1'" style="margin-left: 10px;color: #13ce66">是</span>
+          <span v-else style="margin-left: 10px;color: #ff4949">否</span>
+        </el-form-item>
+        <el-form-item label="所属销售" prop="companyUserIds">
+          <el-select v-model="companyUserIds" remote multiple placeholder="请选择" filterable style="width: 100%;" v-loading="userLoading">
+            <el-option
+              v-for="dict in userList"
+              :key="dict.userId"
+              :label="dict.nickName"
+              :value="dict.userId.toString()">
+            </el-option>
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer" v-if="formType==1">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  listCourseFinishTemp,
+  getCourseFinishTemp,
+  addCourseFinishTemp,
+  updateCourseFinishTemp,
+  delCourseFinishTemp,
+  exportCourseFinishTemp
+} from "@/api/course/courseFinishTemp";
+import {getAllUserlist} from '@/api/company/companyUser'
+import {courseList, videoList} from '@/api/qw/sop'
+import ImageUpload from "@/views/qw/sop/ImageUpload.vue";
+import { getCompanyList } from "@/api/company/company";
+
+export default {
+  name: "CourseFinishTemp",
+  components: {ImageUpload},
+  data() {
+    return {
+      //上传语音的遮罩层
+      voiceLoading: false,
+      uploadUrl: process.env.VUE_APP_BASE_API + "/common/uploadOSS2",
+      uploadUrlByVoice: process.env.VUE_APP_BASE_API + "/common/uploadOSSByHOOKVoice",
+      companyUserIds: [],
+      userList: [],
+      // 状态字典
+      statusOptions: [],
+      allowSelect: [],
+      courseList: [],
+      videoList: [],
+      // 遮罩层
+      loading: true,
+      userLoading: false,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      companys: [],
+      formType: 1,
+      // 总条数
+      total: 0,
+      // 完课模板表格数据
+      courseFinishTempList: [],
+      //插件版
+      sysQwSopAiContentType: [],
+
+      sysFsSopWatchStatus: [],
+
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        name: null,
+        status: null,
+        setting: null,
+        chatSetting: null,
+        companyId: null,
+        courseId: null,
+        videoId: null,
+        companyUserIds: null,
+        isDel: null
+      },
+      // 模板表格数据
+      setting: [],
+      chatSetting: [],
+      // 表单参数
+      form: {
+        setting: null,
+        chatSetting: null,
+        videoIdSet: null,
+        courseIdSet: null,
+      },
+      // 表单校验
+      rules: {}
+    };
+  },
+  created() {
+    this.getList();
+
+    getCompanyList().then(response => {
+      this.companys = response.data;
+    });
+    this.getDicts("sys_company_status").then(response => {
+      this.statusOptions = response.data;
+    });
+
+    //复用一下
+    this.getDicts("sys_qw_allow_select").then(response => {
+      this.allowSelect = response.data;
+    });
+
+    this.getDicts("sys_fs_sop_watch_status").then(response => {
+      this.sysFsSopWatchStatus = response.data;
+    });
+
+    this.getDicts("sys_qwSopAi_contentType").then(response => {
+      this.sysQwSopAiContentType = response.data;
+    });
+    courseList().then(response => {
+      this.courseList = response.list;
+    });
+  },
+  methods: {
+    courseChange() {
+
+      videoList(this.form.courseId).then(response => {
+        this.videoList = response.list;
+      });
+    },
+    /** 查询完课模板列表 */
+    getList() {
+      this.loading = true;
+      listCourseFinishTemp(this.queryParams).then(response => {
+        this.courseFinishTempList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    handleAvatarSuccessFile(res, file, item) {
+      if (res.code === 200) {
+        // 使用 $set 确保响应式更新
+        this.$set(item, 'fileUrl', res.url);
+      } else {
+        this.msgError(res.msg);
+      }
+    },
+
+    beforeAvatarUploadFile(file) {
+      const isLt1M = file.size / 1024 / 1024 < 10;
+      if (!isLt1M) {
+        this.$message.error('上传大小不能超过 10MB!');
+      }
+      return isLt1M;
+    },
+    //下载文件
+    downloadUrl(materialUrl) {
+      // 直接返回文件 URL
+      return materialUrl;
+    },
+    handleInputVideoText(value, content) {
+      // 允许的字符:中文、英文(大小写)、数字和指定标点符号(,。!?)
+      const regex = /^[\u4e00-\u9fa5,。!?,!?]+$/;
+
+      // 删除不符合条件的字符
+      const filteredValue = value.split('').filter(char => regex.test(char)).join('');
+
+      this.$set(content, 'value', filteredValue);
+
+    },
+    handleAvatarSuccessVideo(res, file, item) {
+      if (res.code == 200) {
+        // 使用 $set 确保响应式更新
+        this.$set(item, 'videoUrl', res.url);
+      } else {
+        this.msgError(res.msg);
+      }
+    },
+
+    beforeAvatarUploadVideo(file) {
+      const isLt30M = file.size / 1024 / 1024 < 10;
+      const isMP4 = file.type === 'video/mp4';
+
+      if (!isMP4) {
+        this.$message.error('仅支持上传 MP4 格式的视频文件!');
+        return false;
+      }
+
+      if (!isLt30M) {
+        this.$message.error('上传大小不能超过 10MB!');
+        return false;
+      }
+
+      return true;
+    },
+
+    handleAvatarSuccessVoice(res, file, item) {
+      if (res.code == 200) {
+
+        // 创建 Audio 对象加载音频
+        const audio = new Audio(res.mp3Url);
+        audio.addEventListener('loadedmetadata', () => {
+          // 获取音频时长
+          this.$set(item, 'voiceDuration', Math.ceil(audio.duration));
+        });
+        // 使用 $set 确保响应式更新
+        this.$set(item, 'voiceUrl', res.silkUrl);
+        this.$set(item, 'mp3Url', res.mp3Url);
+      } else {
+        this.msgError(res.msg);
+      }
+      this.voiceLoading = false;
+    },
+    delSetList(index, type) {
+      if (type == 0) {
+        this.setting.splice(index, 1)
+      } else {
+        this.chatSetting.splice(index, 1)
+      }
+    },
+    beforeAvatarUploadVoice(file) {
+      return new Promise((resolve, reject) => {
+        const isLt10M = file.size / 1024 / 1024 < 10; // 假设语音文件大小限制为10MB
+        const isVoiceType = ['audio/mp3', 'audio/mpeg', 'audio/wav', 'audio/x-wav'].includes(file.type);
+
+        if (!isVoiceType) {
+          this.$message.error('仅支持上传 MP3, WAV, X-WAV 格式的语音文件!');
+          return reject(false); // 不允许继续上传
+        }
+
+        if (!isLt10M) {
+          this.$message.error('上传大小不能超过 10MB!');
+          return reject(false); // 不允许继续上传
+        }
+
+        // 使用 FileReader 读取文件
+        const reader = new FileReader();
+        reader.onload = (event) => {
+          const audio = new Audio(event.target.result);
+          audio.addEventListener('loadedmetadata', () => {
+            // 获取时长并保存
+            if (Math.ceil(audio.duration) > 30) {
+              this.$message.error('音频时长不能超过30秒!');
+              this.voiceLoading = false;
+              return reject(false); // 不允许继续上传
+            }
+            resolve(true); // 允许上传
+          });
+        };
+
+        reader.onerror = () => {
+          this.$message.error('无法读取音频文件!请上传正确的音频文件');
+          reject(false); // 不允许继续上传
+        };
+        this.voiceLoading = true;
+        reader.readAsDataURL(file); // 开始读取文件
+      });
+    },
+
+    addSetList(type) {
+      const newSetting = {
+        contentType: '1',
+        value: '',
+      };
+      // 将新设置项添加到 content.setting 数组中
+      if (type == 0) {
+        this.setting.push(newSetting);
+      } else {
+        this.chatSetting.push(newSetting);
+      }
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        name: null,
+        type: 1,
+        status: 1,
+        setting: [],
+        chatSetting: [],
+        companyId: null,
+        createBy: null,
+        createTime: null,
+        courseId: null,
+        videoId: null,
+        companyUserIds: null,
+        updateTime: null,
+        isDel: null,
+        isAllCompanyUser: null,
+      };
+      this.companyUserIds = []
+      this.setting = []
+      this.chatSetting = []
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length !== 1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加完课模板";
+    },
+
+    /**
+     * 查看完课模板
+     */
+    handleSelectDetails(row) {
+
+      this.reset();
+      const id = row.id || this.ids
+      getCourseFinishTemp(id).then(response => {
+        this.form = response.data;
+        this.initCompanyUserList();
+
+        this.setting = JSON.parse(this.form.setting)
+        this.chatSetting = JSON.parse(this.form.chatSetting)
+        if (response.data.companyUserIds != null) {
+          this.companyUserIds = this.form.companyUserIds.split(",");
+        }
+
+        videoList(this.form.courseId).then(response => {
+          this.videoList = response.list;
+        });
+
+        this.open = true;
+        this.title = "查看完课模板";
+        this.formType = 2;
+
+      });
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id || this.ids
+      getCourseFinishTemp(id).then(response => {
+        this.form = response.data;
+        this.setting = JSON.parse(this.form.setting)
+        this.chatSetting = JSON.parse(this.form.chatSetting)
+        if (response.data.companyUserIds != null) {
+          this.companyUserIds = this.form.companyUserIds.split(",");
+        }
+        videoList(this.form.courseId).then(response => {
+
+          this.videoList = response.list;
+        });
+        this.open = true;
+        this.title = "修改完课模板";
+        this.initCompanyUserList();
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+
+          if (this.form.isAllCompanyUser == null) {
+            this.form.isAllCompanyUser = 2;
+          }
+          this.form.companyUserIds = this.companyUserIds.toString()
+
+          this.form.setting = JSON.stringify(this.setting)
+          this.form.chatSetting = JSON.stringify(this.chatSetting)
+
+          if (this.setting.length <= 0) {
+            return this.$message("请添加规则")
+          }
+          for (let i = 0; i < this.setting.length; i++) {
+            if (this.setting[i].contentType == 1 && (this.setting[i].value == null || this.setting[i].value == "")) {
+              return this.$message.error("内容不能为空")
+            }
+            if (this.setting[i].contentType == 2 && (this.setting[i].imgUrl == null || this.setting[i].imgUrl == "")) {
+              return this.$message.error("图片不能为空")
+            }
+            if (this.setting[i].contentType == 3 && (this.setting[i].linkTitle == null || this.setting[i].linkTitle == "")) {
+              return this.$message.error("链接标题不能为空")
+            }
+            if (this.setting[i].contentType == 3 && (this.setting[i].linkDescribe == null || this.setting[i].linkDescribe == "")) {
+              return this.$message.error("链接描述不能为空")
+            }
+            if (this.setting[i].contentType == 3 && (this.setting[i].linkImageUrl == null || this.setting[i].linkImageUrl == "")) {
+              return this.$message.error("链接图片不能为空")
+            }
+            if (this.setting[i].contentType == 3 && this.setting[i].type == 1 && (this.setting[i].linkUrl == null || this.setting[i].linkUrl == "")) {
+              return this.$message.error("链接地址不能为空")
+            }
+            if (this.setting[i].contentType == 5 && (this.setting[i].fileUrl == null || this.setting[i].fileUrl == "")) {
+              return this.$message.error("文件不能为空")
+            }
+            if (this.setting[i].contentType == 6 && (this.setting[i].videoUrl == null || this.setting[i].videoUrl == "")) {
+              return this.$message.error("视频不能为空")
+            }
+            if (this.setting[i].contentType == 7 && (this.setting[i].value == null || this.setting[i].value == "")) {
+              return this.$message.error("语音不能为空")
+            }
+          }
+
+          if (this.form.id != null) {
+            updateCourseFinishTemp(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+              this.setting = [];
+              this.chatSetting = [];
+            });
+          } else {
+            addCourseFinishTemp(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.setting = [];
+              this.chatSetting = [];
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除完课模板编号为"' + ids + '"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function () {
+        return delCourseFinishTemp(ids);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(() => {
+      });
+    },
+    initCompanyUserList(){
+      this.form.companyUserIds = [];
+      this.userLoading = true;
+      getAllUserlist({companyId: this.form.companyId}).then(response => {
+        this.userLoading = false;
+        this.userList = response.data;
+      });
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有完课模板数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        this.exportLoading = true;
+        return exportCourseFinishTemp(queryParams);
+      }).then(response => {
+        this.download(response.msg);
+        this.exportLoading = false;
+      }).catch(() => {
+      });
+    }
+  }
+};
+</script>

+ 11 - 0
src/views/course/userCourse/index.vue

@@ -317,6 +317,10 @@
             <el-form-item label="发课时间" prop="sendTime">
               <el-time-picker
                 v-model="form.sendTime"
+                value-format="HH:mm"
+                format="HH:mm"
+                style="width: 100px"
+                :picker-options="{ selectableRange: startTimeRange }"
               >
               </el-time-picker>
             </el-form-item>
@@ -473,6 +477,8 @@ import {
   putOn,
   pullOff
 } from "@/api/course/userCourse";
+
+import {getSelectableRange} from "@/api/qw/sopTemp";
 import Treeselect from "@riophae/vue-treeselect";
 import "@riophae/vue-treeselect/dist/vue-treeselect.css";
 import Editor from '@/components/Editor/wang';
@@ -495,6 +501,7 @@ export default {
         talentId: null
       },
       talentList: [],
+      startTimeRange: [],
       show: {
         title: "目录管理",
         open: false
@@ -611,6 +618,10 @@ export default {
       this.categoryOptions = response.data;
     });
 
+
+    getSelectableRange().then(e => {
+      this.startTimeRange = e.data;
+    })
     // this.getTreeselect();
     this.getDicts("sys_spec_show").then(response => {
       this.specShowOptions = response.data;

+ 6 - 1
src/views/qw/sopTemp/index.vue

@@ -265,7 +265,7 @@
               v-model="form.timeList[index]"
               value-format="HH:mm"
               format="HH:mm"
-              :picker-options="{ selectableRange: '01:01:00 - 23:59:59' }"
+              :picker-options="{ selectableRange: startTimeRange }"
               placeholder="时间"
               style="width: 100px;height: 20px;margin-left: 10px;margin-top: 10px">
             </el-time-picker>
@@ -299,6 +299,7 @@ import {
   copyTemplate,
   delSopTemp,
   exportSopTemp,
+  getSelectableRange,
   getSopTemp,
   redList,
   listSopTemp,
@@ -346,6 +347,7 @@ export default {
       command: 0,
       // 状态字典
       statusOptions: [],
+      startTimeRange: [],
 
       shareOptions: {
         title: '分享模板',
@@ -400,6 +402,9 @@ export default {
     this.getDicts("sys_company_status").then(response => {
       this.statusOptions = response.data;
     });
+    getSelectableRange().then(e => {
+      this.startTimeRange = e.data;
+    })
 
     this.getDicts("sys_course_project").then(response => {
       this.projectOptions = response.data;

+ 6 - 1
src/views/qw/sopTemp/updateSopTemp.vue

@@ -100,7 +100,7 @@
                                       v-model="content.time"
                                       value-format="HH:mm"
                                       format="HH:mm"
-                                      :picker-options="{ selectableRange: '01:01:00 - 23:59:59' }"
+                                      :picker-options="{ selectableRange: startTimeRange }"
                                       placeholder="时间"
                                       style="width: 100px;height: 20px;">
                                     </el-time-picker>
@@ -668,6 +668,7 @@ import {
   sortDay,
   dayListFun,
   updateSopTemp,
+  getSelectableRange,
   exportSopTemp,
   addOrUpdateSetting,
   selectRulesInfo,
@@ -737,6 +738,7 @@ export default {
       open: false,
       // 状态字典
       statusOptions: [],
+      startTimeRange: [],
       videoNumOptions: {
         title: '选择视频号',
         open: false,
@@ -796,6 +798,9 @@ export default {
       this.courseList = response.list;
     });
 
+    getSelectableRange().then(e => {
+      this.startTimeRange = e.data;
+    })
     const id = this.$route.params && this.$route.params.id;
     this.id = id;
     this.formType = this.$route.params && this.$route.params.type;

+ 35 - 1
src/views/system/config/config.vue

@@ -388,6 +388,30 @@
              </el-tooltip>
            </el-form-item>
 
+
+           <el-form-item label="禁止发送时间段">
+             <el-row v-for="(item, index) in form18.disabledTimeList" style="margin-top: 10px">
+               <el-time-picker
+                 class="custom-input"
+                 v-model="item.startDisabledTime"
+                 value-format="HH:mm"
+                 format="HH:mm"
+                 placeholder="开始时间"
+                 style="width: 130px;height: 20px;">
+               </el-time-picker>
+               <el-time-picker
+                 class="custom-input"
+                 v-model="item.endDisabledTime"
+                 value-format="HH:mm"
+                 format="HH:mm"
+                 placeholder="结束时间"
+                 style="width: 130px;height: 20px;margin-left: 10px">
+               </el-time-picker>
+               <el-button type="danger" icon="el-icon-delete" circle style="margin-left: 20px" @click="removeDisabledTime(index)"></el-button>
+             </el-row>
+             <el-button @click="addDisabledTime" style="margin-top: 10px">添加时间段</el-button>
+           </el-form-item>
+
            <el-form-item label="红包模式">
              <el-radio-group v-model="form18.redPacketMode">
                <el-radio label="1">总公司</el-radio>
@@ -836,7 +860,17 @@ export default {
           this.msgSuccess("清理成功");
         }
       });
-    }
+    },
+    addDisabledTime(){
+      if(this.form18.disabledTimeList == null || this.form18.disabledTimeList == undefined){
+        this.form18.disabledTimeList = []
+      }
+      this.form18.disabledTimeList.push({startDisabledTime: "", endDisabledTime: ""});
+      this.$forceUpdate();
+    },
+    removeDisabledTime(index){
+      this.form18.disabledTimeList.splice(index, 1);
+    },
   }
 };
 </script>