xw 12 часов назад
Родитель
Сommit
a7b99b4b52

+ 18 - 0
src/api/qw/externalContact.js

@@ -9,6 +9,24 @@ export function listExternalContact(query) {
   })
 }
 
+// 查询重粉用户看课记录
+export function getWatchLogList(query) {
+  return request({
+    url: '/qw/externalContact/getWatchLogList',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询重粉看课历史综合信息
+export function getRepeatCourseHistory(fsUserId) {
+  return request({
+    url: '/qw/externalContact/getRepeatCourseHistory',
+    method: 'get',
+    params: { fsUserId }
+  })
+}
+
 // 查询企业微信客户列表
 export function getRepeat(query) {
   return request({

+ 8 - 0
src/views/his/company/index.vue

@@ -474,6 +474,12 @@
             />
           </el-select>
         </el-form-item>
+        <el-form-item label="是否强制评级">
+          <el-radio-group v-model="form.level">
+            <el-radio :label="1">开</el-radio>
+            <el-radio :label="0">关</el-radio>
+          </el-radio-group>
+        </el-form-item>
         <el-form-item label="看课休息开关" >
           <el-radio-group v-model="form.isOpenRestReminder">
             <el-radio :label="1" >开</el-radio>
@@ -1200,6 +1206,7 @@ export default {
         userId: null,
         remark: null,
         isOpenRestReminder: null,
+        level: 0,
         linkName: null,
         limitUserCount: null,
         maxPadNum: -1,
@@ -1278,6 +1285,7 @@ export default {
         if (this.form.companyType != null) {
           this.form.companyType = String(this.form.companyType)
         }
+        this.form.level = this.form.level != null ? Number(this.form.level) : 0
       })
     },
     getAppList(companyId) {

+ 419 - 798
src/views/qw/externalContact/index.vue

@@ -1,47 +1,6 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px" style="border: 1px solid transparent">
-      <el-form-item label="销售公司" prop="companyId">
-        <el-select v-model="queryParams.companyId" placeholder="销售公司"  size="small" @change="getAllUserlist(queryParams.companyId)">
-          <el-option
-              v-for="dict in qwCompanyList"
-              :key="dict.companyId"
-              :label="dict.companyName"
-              :value="dict.companyId"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="销售账号" prop="companyUserName">
-        <el-select v-model="queryParams.companyUserName" placeholder="销售账号"  size="small" filterable clearable  >
-          <el-option
-              v-for="dict in companyUserNameList"
-              :key="dict.userId"
-              :label="dict.userName"
-              :value="dict.userName"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="企微主体" prop="corpId">
-        <el-select v-model="queryParams.corpId" placeholder="企微主体" size="small" clearable>
-          <el-option
-            v-for="corp in corpList"
-            :key="corp.corpId"
-            :label="corp.corpName"
-            :value="corp.corpId"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="企微微信" prop="companyUserName">
-        <el-select v-model="queryParams.qwUserId" placeholder="企微微信"  size="small" clearable>
-          <el-option
-              v-for="dict in qwUserList"
-              :key="dict.id"
-              :label="dict.qwUserName"
-              :value="dict.id"
-          />
-        </el-select>
-      </el-form-item>
-
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
       <el-form-item label="客户会员id" prop="fsUserId">
         <el-input
           v-model="queryParams.fsUserId"
@@ -62,179 +21,6 @@
         />
       </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="qwUserName">
-        <el-input
-            v-model="queryParams.qwUserName"
-            placeholder="请输入销售企微昵称"
-            clearable
-            size="small"
-            @keyup.enter.native="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="用户类别" prop="type">
-        <el-select v-model="queryParams.type" placeholder="请选择用户类别" clearable size="small">
-          <el-option
-              v-for="dict in typeOptions"
-              :key="dict.dictValue"
-              :label="dict.dictLabel"
-              :value="dict.dictValue"
-          />
-        </el-select>
-      </el-form-item>
-
-      <el-form-item label="性别" prop="gender">
-        <el-select v-model="queryParams.gender" placeholder="状态" clearable size="small">
-          <el-option
-              v-for="dict in genderOptions"
-              :key="dict.dictValue"
-              :label="dict.dictLabel"
-              :value="dict.dictValue"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="客户等级" prop="level">
-        <el-select v-model="queryParams.level" placeholder="客户等级" clearable size="small">
-          <el-option
-              v-for="dict in ratingType"
-              :key="dict.dictValue"
-              :label="dict.dictLabel"
-              :value="dict.dictValue"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="等级升降" prop="levelType">
-        <el-select v-model="queryParams.levelType" placeholder="等级升降" clearable size="small">
-          <el-option
-              v-for="dict in ratingUpFall"
-              :key="dict.dictValue"
-              :label="dict.dictLabel"
-              :value="dict.dictValue"
-          />
-        </el-select>
-      </el-form-item>
-
-
-      <el-form-item label="电话号码" prop="remarkMobiles">
-        <el-input
-            v-model="queryParams.remarkMobiles"
-            placeholder="请输入备注电话号码"
-            clearable
-            size="small"
-            @keyup.enter.native="handleQuery"
-        />
-      </el-form-item>
-
-      <el-form-item label="来源" prop="addWay">
-
-        <el-select v-model="queryParams.addWay" placeholder="来源" clearable size="small">
-          <el-option
-              v-for="dict in addWayOptions"
-              :key="dict.dictValue"
-              :label="dict.dictLabel"
-              :value="dict.dictValue"
-          />
-        </el-select>
-      </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="addWay">
-
-        <el-select v-model="queryParams.transferStatus" placeholder="转接状态" clearable size="small">
-          <el-option
-              v-for="dict in transferStatusOptions"
-              :key="dict.dictValue"
-              :label="dict.dictLabel"
-              :value="dict.dictValue"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="是否绑定会员" prop="isBindMini">
-        <el-select v-model="queryParams.isBindMini" placeholder="是否绑定会员" clearable size="small" @change="handleQuery" >
-          <el-option
-              v-for="dict in isBindMiniOptions"
-              :key="dict.dictValue"
-              :label="dict.dictLabel"
-              :value="dict.dictValue"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="标签" prop="tagIds">
-        <div @click="hangleChangeTags()" style="cursor: pointer; border: 1px solid #e6e6e6; background-color: white; overflow: hidden; flex-grow: 1;width: 250px">
-          <div style="min-height: 35px; max-height: 200px; overflow-y: auto;">
-            <el-tag type="success"
-                    closable
-                    :disable-transitions="false"
-                    v-for="list in this.selectTags"
-                    :key="list.tagId"
-                    @close="handleCloseTags(list)"
-                    style="margin: 3px;"
-            >{{list.name}}
-            </el-tag>
-          </div>
-        </div>
-
-
-      </el-form-item>
-      <el-form-item label="备注" prop="remark">
-        <el-input
-            v-model="queryParams.remark"
-            placeholder="请输入备注"
-            clearable
-            size="small"
-            @keyup.enter.native="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="所属销售" prop="companyUser">
-        <el-input
-            v-model="queryParams.companyUser"
-            placeholder="请输入昵称或者手机号"
-            clearable
-            size="small"
-            @keyup.enter.native="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="添加时间">
-        <el-date-picker v-model="daterange" size="small" style="width: 220px" value-format="yyyy-MM-dd" type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
-      </el-form-item>
-
-
-
-
-      <el-form-item label="流失时间" prop="lossTime">
-        <el-date-picker clearable size="small"
-                        v-model="queryParams.lossTime"
-                        type="date"
-                        value-format="yyyy-MM-dd"
-                        placeholder="选择流失时间">
-        </el-date-picker>
-      </el-form-item>
-      <el-form-item label="删除时间" prop="delTime">
-        <el-date-picker clearable size="small"
-                        v-model="queryParams.delTime"
-                        type="date"
-                        value-format="yyyy-MM-dd"
-                        placeholder="选择删除时间">
-        </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>
@@ -342,59 +128,24 @@
       </template>
     </el-table-column>
     <el-table-column label="企业id" align="center" prop="corpId" />
-    <el-table-column label="是否绑定会员" width="100px" align="center" fixed="right">
+    <el-table-column label="重粉看课历史" width="140px" align="center" fixed="right">
       <template slot-scope="scope">
-        <el-tag v-if="scope.row.fsUserId" >已绑定</el-tag>
-        <el-tag v-else type="info"> 未绑定</el-tag>
+        <div v-if="scope.row.fsUserId">
+          <el-tag type="success" v-if="scope.row.isRepeat == 0" size="mini">正常</el-tag>
+          <el-tag type="danger" v-if="scope.row.isRepeat == 1" size="mini">重粉</el-tag>
+          <el-button
+            size="mini"
+            type="text"
+            @click="showLog(scope.row)"
+          >看课历史
+          </el-button>
+        </div>
       </template>
     </el-table-column>
-    <el-table-column label="修改" align="center" class-name="small-padding fixed-width" width="120px" fixed="right">
+    <el-table-column label="是否绑定会员" width="100px" align="center" fixed="right">
       <template slot-scope="scope">
-<!--        <el-button-->
-<!--            v-if="scope.row.status==0||scope.row.status==2"-->
-<!--            size="mini"-->
-<!--            type="text"-->
-<!--            icon="el-icon-edit"-->
-<!--            @click="handleUpdate(scope.row)"-->
-<!--            v-hasPermi="['qw:externalContact:edit']"-->
-<!--        >修改备注</el-button>-->
-<!--        <el-button-->
-<!--            size="mini"-->
-<!--            type="text"-->
-<!--            icon="el-icon-user-solid"-->
-<!--            @click="handleAppellation(scope.row)"-->
-<!--        >修改客户称呼</el-button>-->
-<!--      </template>-->
-<!--    </el-table-column>-->
-<!--    <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120px" fixed="right">-->
-<!--      <template slot-scope="scope">-->
-<!--        <el-button-->
-<!--            size="mini"-->
-<!--            type="text"-->
-<!--            icon="el-icon-edit-outline"-->
-<!--            @click="handleUpdateUser(scope.row)"-->
-<!--        >-->
-<!--          <span v-if="scope.row.fsUserId">换绑会员</span>-->
-<!--          <span v-else>绑定会员</span>-->
-<!--        </el-button>-->
-
-<!--        <el-button-->
-<!--            v-if="scope.row.fsUserId"-->
-<!--            size="mini"-->
-<!--            type="text"-->
-<!--            icon="el-icon-thumb"-->
-<!--            @click="handleUnBindUserId(scope.row)"-->
-<!--            v-hasPermi="['qw:externalContact:unBindUserId']"-->
-<!--        >-->
-<!--          <span>解除会员绑定</span>-->
-<!--        </el-button>-->
-        <el-button
-            size="mini"
-            type="text"
-            @click="handledetails(scope.row)"
-            v-hasPermi="['qw:externalContact:getUserInfo']"
-        >AI获取用户信息
-        </el-button>
+        <el-tag v-if="scope.row.fsUserId" >已绑定</el-tag>
+        <el-tag v-else type="info"> 未绑定</el-tag>
       </template>
     </el-table-column>
     </el-table>
@@ -405,190 +156,199 @@
         :limit.sync="queryParams.pageSize"
         @pagination="getList"
     />
-    <!--  搜索标签   -->
-    <el-dialog :title="changeTagDialog.title" :visible.sync="changeTagDialog.open" style="width:100%;height: 100%" append-to-body>
 
-      <div>搜索标签:
-        <el-input v-model="queryTagParams.name" placeholder="请输入标签名称" clearable size="small" style="width: 200px;margin-right: 10px" />
-        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleSearchTags(queryTagParams.name)">搜索</el-button>
-        <el-button type="primary" icon="el-icon-plus" size="mini" @click="cancelSearchTags">重置</el-button>
-      </div>
-      <div v-for="item in tagGroupList" :key="item.id"  >
-        <div style="font-size: 20px;margin-top: 20px;margin-bottom: 20px;">
-          <span class="name-background">{{ item.name }}</span>
+    <!-- 重粉看课记录 -->
+    <el-dialog
+      title="重粉看课历史"
+      :visible.sync="log.open"
+      width="900px"
+      append-to-body
+      custom-class="repeat-course-history-dialog"
+      :close-on-click-modal="false"
+    >
+      <div class="repeat-history-content" v-loading="log.loading">
+        <div class="customer-info-card">
+          <div class="customer-avatar">
+            <img :src="log.currentRow.avatar || require('@/assets/images/profile.jpg')" alt="" />
+          </div>
+          <div class="customer-detail">
+            <div class="customer-name-row">
+              <span class="customer-name">{{ log.currentRow.name || '-' }}</span>
+              <el-tag v-if="log.currentRow.isRepeat == 0" type="success" size="small">正常</el-tag>
+              <el-tag v-if="log.currentRow.isRepeat == 1" type="danger" size="small">重粉</el-tag>
+            </div>
+            <div class="customer-meta">
+              <span v-if="log.currentRow.remark">备注:{{ log.currentRow.remark }}</span>
+            </div>
+          </div>
         </div>
-        <div class="tag-container">
-          <a
-              v-for="tagItem in item.tag"
-              class="tag-box"
-              @click="tagSelection(tagItem)"
-              :class="{ 'tag-selected': tagItem.isSelected }"
-          >
-            {{ tagItem.name }}
-          </a>
+
+        <div class="section-block">
+          <div class="section-title">
+            <i class="el-icon-user"></i>
+            <span>关联销售</span>
+            <el-tag size="mini" type="info">{{ log.mockSales.length }}人</el-tag>
+          </div>
+          <div class="sales-cards" v-if="log.mockSales.length > 0">
+            <div class="sales-card" v-for="(item, idx) in log.mockSales" :key="idx">
+              <div class="sales-card-top">
+                <span class="sales-name">{{ item.qwUserName }}</span>
+                <el-tag size="mini" v-if="item.status != null" :type="item.status === 0 ? 'success' : item.status === 1 ? 'warning' : item.status === 2 ? '' : 'danger'">{{ {0:'正常',1:'离职待接替',2:'正在接替',3:'流失',4:'删除'}[item.status] || '未知' }}</el-tag>
+                <el-tag size="mini" type="info" v-if="item.hasPermission === false">无权查看</el-tag>
+              </div>
+              <div class="sales-card-body">
+                <div class="sales-card-row">
+                  <span class="label">所属主体</span>
+                  <span class="value">{{ item.corpName }}</span>
+                </div>
+                <div class="sales-card-row">
+                  <span class="label">添加时间</span>
+                  <span class="value">{{ item.addTime }}</span>
+                </div>
+              </div>
+            </div>
+          </div>
+          <div class="empty-tip" v-else>暂无关联销售数据</div>
         </div>
-      </div>
 
-      <pagination
-          v-show="tagTotal>0"
-          :total="tagTotal"
-          :page.sync="queryTagParams.pageNum"
-          :limit.sync="queryTagParams.pageSize"
-          @pagination="getPageListTagGroup"
-      />
-      <div slot="footer" class="dialog-footer">
-        <el-button type="primary" @click="tagSubmitForm()">确 定</el-button>
-        <el-button @click="tagCancel()">取消</el-button>
-      </div>
-    </el-dialog>
+        <div class="section-block">
+          <div class="section-title">
+            <i class="el-icon-reading"></i>
+            <span>课程学习进度</span>
+            <el-tag size="mini" type="info">{{ log.mockCourses.length }}门</el-tag>
+          </div>
+          <div class="course-list" v-if="log.mockCourses.length > 0">
+            <div class="course-item" v-for="(course, idx) in log.mockCourses" :key="idx">
+              <div class="course-header">
+                <div class="course-name-wrap">
+                  <span class="course-index">{{ idx + 1 }}</span>
+                  <span class="course-name">{{ course.courseName }}</span>
+                </div>
+                <div class="course-status">
+                  <el-tag v-if="course.finished" type="success" size="mini" effect="plain">已完课</el-tag>
+                  <el-tag v-else type="warning" size="mini" effect="plain">学习中</el-tag>
+                </div>
+              </div>
+              <div class="course-progress">
+                <el-progress
+                  :percentage="course.percentage"
+                  :stroke-width="10"
+                  :color="course.finished ? '#67C23A' : '#409EFF'"
+                ></el-progress>
+                <span class="progress-text">已学 <b>{{ course.watchedCount }}</b>/{{ course.totalCount }}节</span>
+              </div>
+              <div class="course-latest">
+                <span class="latest-label">最新学习:</span>
+                <span class="latest-section">{{ course.latestSection }}</span>
+                <span class="latest-time" v-if="course.latestTime">{{ course.latestTime }}</span>
+              </div>
+              <div class="course-source" v-if="course.qwUserName || course.corpName">
+                <span class="source-item" v-if="course.qwUserName"><i class="el-icon-user"></i> {{ course.qwUserName }}</span>
+                <span class="source-item" v-if="course.corpName"><i class="el-icon-office-building"></i> {{ course.corpName }}</span>
+                <el-tag size="mini" type="info" v-if="course.hasPermission === false" style="margin-left: 4px;">无权查看</el-tag>
+              </div>
+            </div>
+          </div>
+          <div class="empty-tip" v-else>暂无课程学习记录</div>
+        </div>
 
-    <el-dialog :title="info.title" :visible.sync="info.open"   width="1100px" append-to-body>
-      <externalContactInfo  ref="Details" />
+        <div class="section-block">
+          <div class="section-title" @click="log.detailExpanded = !log.detailExpanded" style="cursor: pointer;">
+            <i class="el-icon-document"></i>
+            <span>详细看课记录</span>
+            <i :class="log.detailExpanded ? 'el-icon-arrow-up' : 'el-icon-arrow-down'" style="margin-left: 4px;"></i>
+          </div>
+          <div v-show="log.detailExpanded">
+            <el-form :model="log.queryParams" :inline="true" label-width="80px" size="small">
+              <el-form-item label="课程" prop="courseId">
+                <el-select filterable v-model="log.queryParams.courseId" placeholder="请选择课程" clearable>
+                  <el-option
+                    v-for="c in log.mockCourses"
+                    :key="c.courseId"
+                    :label="c.courseName"
+                    :value="c.courseId"
+                  />
+                </el-select>
+              </el-form-item>
+              <el-form-item>
+                <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQueryWatchLog">搜索</el-button>
+              </el-form-item>
+            </el-form>
+            <el-table :data="log.list" size="small" border>
+              <el-table-column label="所属企微主体" align="center" prop="corpName" min-width="120" />
+              <el-table-column label="所属企微" align="center" prop="qwUserName" min-width="100" />
+              <el-table-column label="项目" align="center" prop="projectName" min-width="100"/>
+              <el-table-column label="课程" align="center" prop="courseName" min-width="120"/>
+              <el-table-column label="小节" align="center" prop="videoName" min-width="100"/>
+              <el-table-column label="记录时间" align="center" prop="createTime" min-width="140"/>
+              <el-table-column label="是否完课" align="center" prop="logType" width="80">
+                <template slot-scope="scope">
+                  <el-tag v-if="scope.row.logType == 2" type="success" size="mini">已完课</el-tag>
+                  <el-tag v-else type="info" size="mini">未完课</el-tag>
+                </template>
+              </el-table-column>
+              <el-table-column label="完课时间" align="center" prop="finishTime" min-width="140"/>
+            </el-table>
+            <pagination
+              v-show="log.total>0"
+              :total="log.total"
+              :page.sync="log.queryParams.pageNum"
+              :limit.sync="log.queryParams.pageSize"
+              @pagination="logList"
+            />
+          </div>
+        </div>
+      </div>
     </el-dialog>
 
   </div>
 </template>
 
 <script>
-import { listExternalContact, getExternalContact, delExternalContact, addExternalContact, updateExternalContact, exportExternalContact, exportUnionId } from "@/api/qw/externalContact";
-import {getCompanyList} from "@/api/company/company";
-import {getAllUserlist} from "@/api/company/companyUser";
-import {getQwUserInfo} from "@/api/qw/qwUser";
-import { allListTagGroup} from "@/api/qw/tagGroup";
-import {searchTags,} from "@/api/qw/tag";
-import { allCorp } from "@/api/qw/qwCompany";
+import { listExternalContact, getWatchLogList, getRepeatCourseHistory } from "@/api/qw/externalContact";
 import PaginationMore from '@/components/PaginationMore/index.vue'
-import externalContactInfo from "../externalContact/info";
 
 export default {
   name: "ExternalContact",
-  components: { PaginationMore,externalContactInfo },
+  components: { PaginationMore },
   data() {
     return {
-      daterange: [],
-      // 遮罩层
       loading: false,
-      // 导出遮罩层
-      exportLoading: false,
-      exportUnionidLoading: false,
-      //等级状态
       ratingUpFall: [],
-      // 性别字典
       genderOptions: [],
-      // 用户类别字典
       typeOptions: [],
-      //状态
-      statusOptions:[],
-      //客户等级
+      statusOptions: [],
       ratingType: [],
-      // 来源字典
       addWayOptions: [],
-      //转接状态
-      transferStatusOptions:[],
-      // 选中数组
+      transferStatusOptions: [],
       ids: [],
-      // 非单个禁用
       single: true,
-      // 非多个禁用
       multiple: true,
-      // 显示搜索条件
       showSearch: true,
-      // 总条数
       total: 0,
-      // 企业微信客户表格数据
       externalContactList: [],
-      //销售员工数据
-      companyUserNameList:[],
-      //企微用户信息数据
-      qwUserList:[],
-      // 弹出层标题
-      title: "",
-      // 是否显示弹出层
-      open: false,
-      info:{
-        title:"用户信息",
-        open:false,
+      log: {
+        open: false,
+        loading: false,
+        list: [],
+        total: 0,
+        detailExpanded: false,
+        currentRow: {},
+        mockSales: [],
+        mockCourses: [],
+        queryParams: {
+          pageNum: 1,
+          pageSize: 10,
+          fsUserId: null,
+          courseId: null,
+        },
       },
-      //标签
-      tagGroupList: [],
-      // 查询参数
       queryParams: {
-        extId:null,
-        companyUser:null,
         pageNum: 1,
         pageSize: 10,
-        qwUserName:null,
-        userId: null,
-        externalUserId: null,
-        name: null,
-        avatar: null,
-        type: null,
-        gender: null,
-        description: null,
-        tagIds: null,
-        remarkMobiles: null,
-        remarkCorpName: null,
-        addWay: null,
-        operUserid: null,
-        corpId: null,
-        companyId: null,
-        companyUserId: null,
-        qwUserId: null,
-        customerId: null,
-        transferStatus: null,
-        status: null,
-        stageStatus: null,
-        transferTime: null,
-        transferNum: null,
-        lossTime: null,
-        delTime: null,
-        state: null,
-        wayId: null,
         fsUserId: null,
-        openId: null,
-        unionid: null,
-        uploadAddWxStatus: null,
-        uploadRegisterStatus: null,
-        uploadFinishedStatus: null,
-        welcomeStatus: null,
-        isInteract: null,
-        level: null,
-        companyUserName:null,
-        levelType: null,
-        firstTime: null,
-        lastWatchTime: null,
-        isRepeat: null,
-        commentStatus: null,
-        isBindMini:null,
-        sTime:null,
-        eTime:null,
+        extId: null,
       },
-      //标签
-      changeTagDialog:{
-        title:"",
-        open:false,
-      },
-      selectTags:[],
-      tagTotal:0,
-      queryTagParams:{
-        pageNum: 1,
-        pageSize: 10,
-        total:0,
-        name:null,
-        corpId:null,
-      },
-      isBindMiniOptions:[
-        {dictLabel:"已绑定",dictValue:'isBindMini'},
-        {dictLabel:"未绑定",dictValue:'noBindMini'},
-      ],
-      qwCompanyList:[],
-      // 企微主体列表
-      corpList: [],
-      // 表单参数
-      form: {},
-      // 表单校验
-      rules: {
-      }
     };
   },
   created() {
@@ -626,279 +386,99 @@ export default {
     this.getDicts("sys_qw_transfer_status").then(response => {
       this.transferStatusOptions = response.data;
     });
-
-    // 企微主体列表
-    allCorp().then(response => {
-      this.corpList = response.data || [];
-    });
-
-    //获取企业
-    getCompanyList().then(response => {
-      this.qwCompanyList = response.data;
-      if(this.qwCompanyList!=null){
-        this.queryParams.companyId=this.qwCompanyList[0].companyId;
-        this.getAllUserlist(this.queryParams.companyId);
-      }
-    });
   },
   methods: {
-    handledetails(row){
-      this.info.open=true;
-      setTimeout(() => {
-        this.$refs.Details.getDetails(row.id);
-      }, 1);
+    showLog(row) {
+      this.log.queryParams.fsUserId = row.fsUserId;
+      this.log.open = true;
+      this.log.loading = true;
+      this.log.detailExpanded = false;
+      this.log.currentRow = {
+        name: row.name,
+        avatar: row.avatar,
+        remark: row.remark,
+        isRepeat: row.isRepeat,
+      };
+      getRepeatCourseHistory(row.fsUserId).then(res => {
+        const data = res.data;
+        if (data) {
+          this.log.currentRow = {
+            name: data.name || row.name,
+            avatar: data.avatar || row.avatar,
+            remark: data.remark || row.remark,
+            isRepeat: data.userRepeat != null ? data.userRepeat : row.isRepeat,
+          };
+          this.log.mockSales = (data.salesList || []).map(s => ({
+            qwUserName: s.qwUserName,
+            corpName: s.corpName,
+            addTime: s.addTime,
+            status: s.status,
+            hasPermission: s.hasPermission,
+          }));
+          this.log.mockCourses = (data.courseList || []).map(c => ({
+            courseId: c.courseId,
+            courseName: c.courseName,
+            watchedCount: c.watchedCount,
+            totalCount: c.totalCount,
+            percentage: c.percentage,
+            latestSection: c.latestSection,
+            latestTime: c.latestTime,
+            finished: c.finished,
+            qwUserName: c.qwUserName,
+            corpName: c.corpName,
+            hasPermission: c.hasPermission,
+          }));
+        }
+        this.log.loading = false;
+      }).catch(() => {
+        this.log.loading = false;
+      });
+      this.logList();
     },
-
-    /** 获取销售账号列表 **/
-    getAllUserlist(companyId){
-      if(companyId){
-        this.getList();
-        getAllUserlist({companyId}).then(response => {
-          this.companyUserNameList=response.data;
-        });
-        //企业微信
-        getQwUserInfo({companyId}).then(response => {
-          this.qwUserList=response.data;
-        })
-      }
+    handleQueryWatchLog() {
+      this.log.queryParams.pageNum = 1;
+      this.logList();
     },
-    tagSelection(row){
-      row.isSelected= !row.isSelected;
-      this.$forceUpdate();
+    logList() {
+      const params = {
+        pageNum: this.log.queryParams.pageNum,
+        pageSize: this.log.queryParams.pageSize,
+        fsUserId: this.log.queryParams.fsUserId,
+        courseId: this.log.queryParams.courseId || undefined,
+      };
+      getWatchLogList(params).then(e => {
+        this.log.list = e.rows;
+        this.log.total = e.total;
+      });
     },
+
     /** 查询企业微信客户列表 */
     getList() {
       this.loading = true;
-      if(this.daterange!=null){
-        this.queryParams.sTime=this.daterange[0];
-        this.queryParams.eTime=this.daterange[1];
-      }else{
-        this.queryParams.sTime=null;
-        this.queryParams.eTime=null;
-      }
       listExternalContact(this.queryParams).then(response => {
         this.externalContactList = response.rows;
         this.total = response.total;
         this.loading = false;
       });
     },
-    // 取消按钮
-    cancel() {
-      this.open = false;
-      this.reset();
-    },
-    // 表单重置
-    reset() {
-      this.form = {
-        id: null,
-        extId:null,
-        userId: null,
-        externalUserId: null,
-        name: null,
-        avatar: null,
-        type: null,
-        gender: null,
-        remark: null,
-        description: null,
-        tagIds: null,
-        remarkMobiles: null,
-        remarkCorpName: null,
-        addWay: null,
-        operUserid: null,
-        corpId: null,
-        companyId: null,
-        companyUserId: null,
-        qwUserId: null,
-        customerId: null,
-        transferStatus: 0,
-        status: 0,
-        stageStatus: "0",
-        createBy: null,
-        updateBy: null,
-        updateTime: null,
-        transferTime: null,
-        transferNum: null,
-        lossTime: null,
-        delTime: null,
-        state: null,
-        wayId: null,
-        fsUserId: null,
-        openId: null,
-        unionid: null,
-        uploadAddWxStatus: 0,
-        uploadRegisterStatus: 0,
-        uploadFinishedStatus: 0,
-        welcomeStatus: 0,
-        isInteract: null,
-        level: null,
-        levelType: null,
-        firstTime: null,
-        lastWatchTime: null,
-        isRepeat: null,
-        commentStatus: 0
-      };
-      this.resetForm("form");
-    },
     /** 搜索按钮操作 */
     handleQuery() {
-      //验证是否选择企业
-      if(!this.queryParams.companyId){
+      if (!this.queryParams.fsUserId && !this.queryParams.extId) {
         return this.$message.warning({
-          message: "请先选择企业!",
+          message: "请至少输入客户会员id或企微客户id",
           duration: 3000
-        })
-      }
-
-      if (this.selectTags!=null && this.selectTags.length>0){
-        // 确保 this.form.tags 是数组
-        if (!this.queryParams.tagIds) {
-          this.queryParams.tagIds = []; // 如果未定义,初始化
-        } else {
-          this.queryParams.tagIds = []; // 清空已有数据
-        }
-
-        // 遍历并添加 tagId
-        this.selectTags.forEach(tag => {
-          if (tag.tagId) { // 确保 tagId 存在
-            this.queryParams.tagIds.push(tag.tagId);
-          }
         });
-        this.queryParams.tagIds=this.queryParams.tagIds.join(",");
-      }else {
-        this.queryParams.tagIds=null;
       }
-
       this.queryParams.pageNum = 1;
       this.getList();
     },
     /** 重置按钮操作 */
     resetQuery() {
       this.resetForm("queryForm");
-      this.queryParams.corpId = null;
-      this.selectTags=[];
-      this.queryParams.qwUserId = null;
-      this.queryParams.sTime=null;
-      this.queryParams.eTime=null;
-      this.externalContactList=[];
-      this.daterange=[];
-      if(this.qwCompanyList!=null){
-        this.queryParams.companyId=this.qwCompanyList[0].companyId;
-        this.getAllUserlist(this.queryParams.companyId);
-      }
-    },
-    handleSearchTags(name){
-
-      searchTags({name:name,corpId:this.queryParams.corpId}).then(response => {
-        this.tagGroupList = response.rows;
-      });
-
-    },
-    cancelSearchTags(){
-      this.resetSearchQueryTag()
-
-      this.getPageListTagGroup();
-    },
-    resetSearchQueryTag(){
-
-      this.queryTagParams= {
-        pageNum: 1,
-        pageSize: 10,
-        total:0,
-        name:null,
-      };
-    },
-    //确定选择标签
-    tagSubmitForm(){
-
-
-      for (let i = 0; i < this.tagGroupList.length; i++) {
-        for (let x = 0; x < this.tagGroupList[i].tag.length; x++) {
-          if (this.tagGroupList[i].tag[x].isSelected === true) {
-
-            if (!this.selectTags) {
-              this.selectTags = [];
-            }
-            // 检查当前 tag 是否已经存在于 tagListFormIndex[index] 中
-            let tagExists = this.selectTags.some(
-                tag => tag.id === this.tagGroupList[i].tag[x].id
-            );
-
-            // 如果 tag 不存在于 tagListFormIndex[index] 中,则新增
-            if (!tagExists) {
-              this.selectTags.push(this.tagGroupList[i].tag[x]);
-            }
-          }
-        }
-      }
-      if (!this.selectTags || this.selectTags.length === 0) {
-        return this.$message('请选择标签');
-      }
-
-      this.changeTagDialog.open = false;
-    },
-    getPageListTagGroup(){
-      this.queryTagParams.corpId=this.queryParams.corpId
-      allListTagGroup(this.queryTagParams).then(response => {
-        this.tagGroupList = response.rows;
-        this.tagTotal = response.total;
-      });
-    },
-    //取消选择标签
-    tagCancel(){
-      this.changeTagDialog.open = false;
-    },
-    //删除一些选择的标签
-    handleCloseTags(list){
-      const ls = this.selectTags.findIndex(t => t.tagId === list.tagId);
-      if (ls !== -1) {
-        this.selectTags.splice(ls, 1);
-        this.selectTags = [...this.selectTags];
-      }
-
-      if (this.selectTags!=null && this.selectTags.length>0){
-        // 确保 this.form.tags 是数组
-        if (!this.queryParams.tagIds) {
-          this.queryParams.tagIds = []; // 如果未定义,初始化
-        } else {
-          this.queryParams.tagIds = []; // 清空已有数据
-        }
-
-        // 遍历并添加 tagId
-        this.selectTags.forEach(tag => {
-          if (tag.tagId) { // 确保 tagId 存在
-            this.queryParams.tagIds.push(tag.tagId);
-          }
-        });
-        this.queryParams.tagIds=this.queryParams.tagIds.join(",");
-      }else {
-        this.queryParams.tagIds=null;
-      }
-
-    },
-    //搜索的标签
-    hangleChangeTags(){
-
-      this.changeTagDialog.title="搜索的标签"
-      this.changeTagDialog.open=true;
-
-      // 获取 tagListFormIndex 中的所有 tagId,用于快速查找
-      const selectedTagIds = new Set(
-          (this.selectTags || []).map(tagItem => tagItem?.tagId)
-      );
-
-      this.queryTagParams.name=null;
-
-      this.getPageListTagGroup();
-
-      setTimeout(() => {
-        for (let i = 0; i < this.tagGroupList.length; i++) {
-          for (let x = 0; x < this.tagGroupList[i].tag.length; x++) {
-            this.tagGroupList[i].tag[x].isSelected = selectedTagIds.has(this.tagGroupList[i].tag[x].tagId);
-          }
-        }
-      }, 200);
-
-
+      this.queryParams.fsUserId = null;
+      this.queryParams.extId = null;
+      this.externalContactList = [];
+      this.total = 0;
     },
     // 多选框选中数据
     handleSelectionChange(selection) {
@@ -906,159 +486,200 @@ export default {
       this.single = selection.length!==1
       this.multiple = !selection.length
     },
-    /** 新增按钮操作 */
-    handleAdd() {
-      this.reset();
-      this.open = true;
-      this.title = "添加企业微信客户";
-    },
-    /** 修改按钮操作 */
-    handleUpdate(row) {
-      this.reset();
-      const id = row.id || this.ids
-      getExternalContact(id).then(response => {
-        this.form = response.data;
-        this.open = true;
-        this.title = "修改企业微信客户";
-      });
-    },
-    /** 提交按钮 */
-    submitForm() {
-      this.$refs["form"].validate(valid => {
-        if (valid) {
-          if (this.form.id != null) {
-            updateExternalContact(this.form).then(response => {
-              this.msgSuccess("修改成功");
-              this.open = false;
-              this.getList();
-            });
-          } else {
-            addExternalContact(this.form).then(response => {
-              this.msgSuccess("新增成功");
-              this.open = false;
-              this.getList();
-            });
-          }
-        }
-      });
-    },
-    /** 删除按钮操作 */
-    handleDelete(row) {
-      const ids = row.id || this.ids;
-      this.$confirm('是否确认删除企业微信客户编号为"' + ids + '"的数据项?', "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(function() {
-          return delExternalContact(ids);
-        }).then(() => {
-          this.getList();
-          this.msgSuccess("删除成功");
-        }).catch(() => {});
-    },
-    /** 导出按钮操作 */
-    handleExport() {
-      //验证是否选择企业
-      if(!this.queryParams.companyId){
-        return this.$message.warning({
-          message: "请先选择企业!",
-          duration: 3000
-        })
-      }
-      const queryParams = this.queryParams;
-      this.$confirm('是否确认导出所有企业微信客户数据项?', "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(() => {
-          this.exportLoading = true;
-          return exportExternalContact(queryParams);
-        }).then(response => {
-          this.download(response.msg);
-          this.exportLoading = false;
-        }).catch(() => {});
-    },
-    /** 导出Unionid按钮操作 */
-    handleExportUnionId() {
-      //验证是否选择企业
-      if(!this.queryParams.companyId){
-        return this.$message.warning({
-          message: "请先选择企业!",
-          duration: 3000
-        })
-      }
-      const queryParams = this.queryParams;
-      this.$confirm('是否确认导出所有企业微信客户unionid数据项?', "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(() => {
-          this.exportUnionidLoading = true;
-          return exportUnionId(queryParams);
-        }).then(response => {
-          this.download(response.msg);
-          this.exportUnionidLoading = false;
-        }).catch(() => {});
-    },
   }
 };
 </script>
 
 <style scoped>
-/* CSS 样式 */
-.tag-container {
+</style>
+
+<style>
+.repeat-course-history-dialog {
+  border-radius: 8px;
+}
+.repeat-course-history-dialog .el-dialog__header {
+  border-bottom: 1px solid #ebeef5;
+  padding-bottom: 15px;
+}
+.repeat-course-history-dialog .el-dialog__body {
+  padding: 16px 20px;
+  max-height: 70vh;
+  overflow-y: auto;
+}
+.repeat-history-content {
+  font-size: 14px;
+  color: #303133;
+}
+.customer-info-card {
   display: flex;
-  flex-wrap: wrap; /* 超出宽度时自动换行 */
-  gap: 8px; /* 设置标签之间的间距 */
+  align-items: center;
+  padding: 12px 16px;
+  background: #f5f7fa;
+  border-radius: 8px;
+  margin-bottom: 20px;
+}
+.customer-info-card .customer-avatar {
+  width: 48px;
+  height: 48px;
+  border-radius: 50%;
+  overflow: hidden;
+  margin-right: 14px;
+  flex-shrink: 0;
+  border: 2px solid #e4e7ed;
+}
+.customer-info-card .customer-avatar img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
 }
-.name-background {
-  display: inline-block;
-  background-color: #abece6; /* 背景颜色 */
-  padding: 4px 8px; /* 调整内边距,让背景包裹文字 */
-  border-radius: 4px; /* 可选:设置圆角 */
+.customer-info-card .customer-detail {
+  flex: 1;
 }
-/* CSS 样式 */
-.tag-container {
+.customer-info-card .customer-name-row {
   display: flex;
-  flex-wrap: wrap; /* 超出宽度时自动换行 */
-  gap: 8px; /* 设置标签之间的间距 */
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 4px;
 }
-.name-background {
-  display: inline-block;
-  background-color: #abece6; /* 背景颜色 */
-  padding: 4px 8px; /* 调整内边距,让背景包裹文字 */
-  border-radius: 4px; /* 可选:设置圆角 */
+.customer-info-card .customer-name {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
 }
-.tag-box {
-  padding: 8px 12px;
-  border: 1px solid #989797;
-  border-radius: 4px;
-  cursor: pointer;
-  display: inline-block;
+.customer-info-card .customer-meta {
+  color: #909399;
+  font-size: 12px;
 }
-
-.tag-selected {
-  background-color: #00bc98;
+.section-block {
+  margin-bottom: 20px;
+}
+.section-title {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 15px;
+  font-weight: 600;
+  color: #303133;
+  padding-bottom: 10px;
+  border-bottom: 1px solid #ebeef5;
+  margin-bottom: 12px;
+}
+.section-title i:first-child {
+  color: #409EFF;
+  font-size: 16px;
+}
+.sales-cards {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+}
+.sales-card {
+  flex: 1;
+  min-width: 220px;
+  max-width: 280px;
+  border: 1px solid #e4e7ed;
+  border-radius: 6px;
+  padding: 12px;
+  background: #fff;
+}
+.sales-card-top {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 8px;
+}
+.sales-card-top .sales-name {
+  font-weight: 600;
+  color: #303133;
+  font-size: 14px;
+}
+.sales-card-body {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+.sales-card-row {
+  display: flex;
+  align-items: center;
+  font-size: 12px;
+}
+.sales-card-row .label {
+  color: #909399;
+  width: 60px;
+  flex-shrink: 0;
+}
+.sales-card-row .value {
+  color: #606266;
+}
+.course-list {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+.course-item {
+  border: 1px solid #e4e7ed;
+  border-radius: 6px;
+  padding: 12px 14px;
+  background: #fff;
+}
+.course-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 8px;
+}
+.course-name-wrap {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+.course-index {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 22px;
+  height: 22px;
+  border-radius: 50%;
+  background: #409EFF;
   color: #fff;
-  border-color: #00bc98;
+  font-size: 12px;
+  font-weight: 600;
 }
-
-.el-tag + .el-tag {
-  margin-left: 10px;
+.course-name {
+  font-weight: 600;
+  color: #303133;
+  font-size: 14px;
 }
-
-
-
-.button-new-tag {
-  margin-left: 10px;
-  height: 32px;
-  line-height: 30px;
-  padding-top: 0;
-  padding-bottom: 0;
+.course-progress {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  margin-bottom: 6px;
+}
+.course-progress .el-progress {
+  flex: 1;
+}
+.course-progress .progress-text {
+  font-size: 12px;
+  color: #909399;
+  white-space: nowrap;
+}
+.course-latest {
+  font-size: 12px;
+  color: #606266;
+  margin-bottom: 4px;
+}
+.course-source {
+  font-size: 12px;
+  color: #909399;
+}
+.course-source .source-item {
+  margin-right: 12px;
 }
-.input-new-tag {
-  width: 90px;
-  margin-left: 10px;
-  vertical-align: bottom;
+.empty-tip {
+  color: #909399;
+  font-size: 13px;
+  text-align: center;
+  padding: 20px 0;
 }
 </style>

+ 398 - 5
src/views/system/config/config.vue

@@ -1495,6 +1495,125 @@
             >
             </el-switch>
           </el-form-item>
+
+          <div class="course-repeat-limit-panel">
+            <div class="course-repeat-limit-header">
+              <div class="course-repeat-limit-header__brand">
+                <span class="course-repeat-limit-header__icon">
+                  <i class="el-icon-s-grid" />
+                </span>
+                <div class="course-repeat-limit-header__text">
+                  <div class="course-repeat-limit-header__title">按项目看课重粉限制</div>
+                  <div class="course-repeat-limit-header__desc">
+                    同一项目内,限制客户仅在首次看课的销售处继续观看
+                  </div>
+                </div>
+              </div>
+              <el-tag
+                :type="form18.projectRepeatLimitEnabled ? 'success' : 'info'"
+                effect="plain"
+                size="medium"
+                class="course-repeat-limit-header__tag"
+              >
+                {{ form18.projectRepeatLimitEnabled ? '限制已开启' : '未限制看课' }}
+              </el-tag>
+            </div>
+
+            <el-alert
+              class="course-repeat-limit-alert"
+              type="info"
+              :closable="false"
+              show-icon
+              title="仅控制客户能否看课;关闭「开启限制」则不限制看课。"
+            />
+
+            <div class="course-repeat-limit-body">
+              <div class="course-repeat-limit-settings">
+                <el-form-item
+                  prop="projectRepeatLimitEnabled"
+                  label-width="0"
+                  class="course-repeat-limit-form-item"
+                >
+                  <div class="course-repeat-limit-row">
+                    <div class="course-repeat-limit-row__label">
+                      <span class="course-repeat-limit-row__name">开启限制</span>
+                      <span class="course-repeat-limit-row__hint">关闭时不限制;开启后按生效时间绑定首次看课销售</span>
+                    </div>
+                    <el-switch
+                      v-model="form18.projectRepeatLimitEnabled"
+                      active-color="#409EFF"
+                      inactive-color="#DCDFE6"
+                      @change="onProjectRepeatLimitEnabledChange"
+                    />
+                  </div>
+                </el-form-item>
+
+                <el-form-item
+                  prop="projectRepeatLimitEffectiveTime"
+                  label-width="0"
+                  class="course-repeat-limit-form-item"
+                  :rules="rulesProjectRepeatLimitEffectiveTime"
+                >
+                  <div
+                    class="course-repeat-limit-row"
+                    :class="{ 'is-disabled': !form18.projectRepeatLimitEnabled }"
+                  >
+                    <div class="course-repeat-limit-row__label">
+                      <span class="course-repeat-limit-row__name">
+                        生效时间
+                        <span v-if="form18.projectRepeatLimitEnabled" class="course-repeat-limit-row__required">*</span>
+                      </span>
+                      <span class="course-repeat-limit-row__hint">
+                        {{ form18.projectRepeatLimitEnabled ? '到达该时间后开始执行重粉限制' : '开启限制后可配置生效时间' }}
+                      </span>
+                    </div>
+                    <el-date-picker
+                      v-model="form18.projectRepeatLimitEffectiveTime"
+                      type="datetime"
+                      value-format="yyyy-MM-dd HH:mm:ss"
+                      format="yyyy-MM-dd HH:mm:ss"
+                      placeholder="请选择生效时间"
+                      prefix-icon="el-icon-time"
+                      :disabled="!form18.projectRepeatLimitEnabled"
+                      class="course-repeat-limit-datetime"
+                    />
+                  </div>
+                </el-form-item>
+
+                <el-form-item
+                  prop="pilotCourseSkipRepeatLimit"
+                  label-width="0"
+                  class="course-repeat-limit-form-item course-repeat-limit-form-item--last"
+                >
+                  <div class="course-repeat-limit-row">
+                    <div class="course-repeat-limit-row__label">
+                      <span class="course-repeat-limit-row__name">先导课豁免</span>
+                      <span class="course-repeat-limit-row__hint">is_first=1 或标题含「先导课」的课程不受限制</span>
+                    </div>
+                    <el-switch
+                      v-model="form18.pilotCourseSkipRepeatLimit"
+                      active-color="#409EFF"
+                      inactive-color="#DCDFE6"
+                      :disabled="!form18.projectRepeatLimitEnabled"
+                    />
+                  </div>
+                </el-form-item>
+              </div>
+
+              <div class="course-repeat-limit-rules">
+                <div class="course-repeat-limit-rules__title">
+                  <i class="el-icon-document" />
+                  规则说明
+                </div>
+                <ul class="course-repeat-limit-rules__list">
+                  <li><strong>生效前</strong>:客户可在同一项目下多个销售处看课</li>
+                  <li><strong>生效后</strong>:同一项目内仅能在首次进入看课的销售下看课(看 1 秒即绑定)</li>
+                  <li><strong>先导课</strong>:开启豁免后不受上述限制</li>
+                </ul>
+              </div>
+            </div>
+          </div>
+
           <el-form-item label="是否允许用户暂停" prop="isAllowUserPause" label-width="120">
             <el-switch
               v-model="form18.isAllowUserPause"
@@ -2665,6 +2784,9 @@ export default {
       },
       form18: {
         viewCommentNum: 200,
+        projectRepeatLimitEnabled: false,
+        projectRepeatLimitEffectiveTime: '',
+        pilotCourseSkipRepeatLimit: true,
       },
       form19: {},
       form20: {
@@ -2815,6 +2937,26 @@ export default {
           trigger: 'blur'
         }
       ],
+      rulesProjectRepeatLimitEffectiveTime: [
+        {
+          validator: (rule, value, callback) => {
+            if (!this.form18.projectRepeatLimitEnabled) {
+              callback()
+              return
+            }
+            if (!value) {
+              callback(new Error('开启限制时请填写生效时间'))
+              return
+            }
+            if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(value)) {
+              callback(new Error('生效时间格式应为 yyyy-MM-dd HH:mm:ss'))
+              return
+            }
+            callback()
+          },
+          trigger: 'change'
+        }
+      ],
       rules20: {
         levelDay: [{ required: true, message: '请输入评级天数', trigger: 'blur' }],
         aLevelMin: [{ required: true, message: '请输入A级最小值', trigger: 'blur' }],
@@ -3102,7 +3244,9 @@ export default {
           console.log(this.form16)
         }
         if (key == 'course.config') {
-          this.form18 = JSON.parse(response.data.configValue)
+          const parsed = JSON.parse(response.data.configValue)
+          this.form18 = parsed
+          this.applyCourseConfigDefaults(this.form18)
         }
         if (key == 'redPacket.config') {
           this.form19 = JSON.parse(response.data.configValue)
@@ -3370,12 +3514,33 @@ export default {
         }
       })
     },
+    onProjectRepeatLimitEnabledChange() {
+      if (!this.form18.projectRepeatLimitEnabled && this.$refs.form18) {
+        this.$refs.form18.clearValidate('projectRepeatLimitEffectiveTime')
+      }
+    },
+    applyCourseConfigDefaults(form) {
+      if (form.projectRepeatLimitEnabled === undefined) {
+        this.$set(form, 'projectRepeatLimitEnabled', false)
+      }
+      if (form.projectRepeatLimitEffectiveTime === undefined || form.projectRepeatLimitEffectiveTime === null) {
+        this.$set(form, 'projectRepeatLimitEffectiveTime', '')
+      }
+      if (form.pilotCourseSkipRepeatLimit === undefined) {
+        this.$set(form, 'pilotCourseSkipRepeatLimit', true)
+      }
+    },
     submitForm18() {
-      var param = { configId: this.configId, configValue: JSON.stringify(this.form18) }
-      updateConfigByKey(param).then(response => {
-        if (response.code === 200) {
-          this.msgSuccess('修改成功')
+      this.$refs['form18'].validate((valid) => {
+        if (!valid) {
+          return
         }
+        var param = { configId: this.configId, configValue: JSON.stringify(this.form18) }
+        updateConfigByKey(param).then(response => {
+          if (response.code === 200) {
+            this.msgSuccess('修改成功')
+          }
+        })
       })
     },
     submitForm19() {
@@ -3697,6 +3862,234 @@ export default {
   justify-content: flex-end;
 }
 
+/* 按项目看课重粉限制 — 配置卡片 */
+.course-repeat-limit-panel {
+  margin: 20px 0 28px;
+  padding: 0;
+  border-radius: 10px;
+  border: 1px solid #e4e7ed;
+  background: linear-gradient(180deg, #fafbfc 0%, #fff 48px);
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
+  overflow: hidden;
+}
+
+.course-repeat-limit-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 18px 22px;
+  border-bottom: 1px solid #ebeef5;
+  background: linear-gradient(90deg, rgba(64, 158, 255, 0.06) 0%, transparent 60%);
+}
+
+.course-repeat-limit-header__brand {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  min-width: 0;
+}
+
+.course-repeat-limit-header__icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 42px;
+  height: 42px;
+  border-radius: 10px;
+  background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
+  color: #fff;
+  font-size: 20px;
+  flex-shrink: 0;
+  box-shadow: 0 4px 10px rgba(64, 158, 255, 0.35);
+}
+
+.course-repeat-limit-header__title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+  line-height: 1.4;
+  letter-spacing: 0.02em;
+}
+
+.course-repeat-limit-header__desc {
+  margin-top: 4px;
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.5;
+}
+
+.course-repeat-limit-header__tag {
+  flex-shrink: 0;
+  margin-left: 16px;
+  font-weight: 500;
+}
+
+.course-repeat-limit-alert {
+  margin: 0;
+  border-radius: 0;
+  border-left: none;
+  border-right: none;
+}
+
+.course-repeat-limit-alert >>> .el-alert__title {
+  font-size: 13px;
+  line-height: 1.6;
+}
+
+.course-repeat-limit-body {
+  display: flex;
+  gap: 0;
+  padding: 0;
+}
+
+.course-repeat-limit-settings {
+  flex: 1;
+  min-width: 0;
+  padding: 8px 22px 12px;
+  border-right: 1px dashed #e4e7ed;
+}
+
+.course-repeat-limit-form-item {
+  margin-bottom: 0;
+}
+
+.course-repeat-limit-form-item >>> .el-form-item__content {
+  margin-left: 0 !important;
+  line-height: normal;
+}
+
+.course-repeat-limit-form-item >>> .el-form-item__error {
+  padding-left: 0;
+  margin-top: 4px;
+}
+
+.course-repeat-limit-form-item--last .course-repeat-limit-row {
+  border-bottom: none;
+}
+
+.course-repeat-limit-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 20px;
+  padding: 16px 0;
+  border-bottom: 1px solid #f0f2f5;
+  transition: opacity 0.2s ease;
+}
+
+.course-repeat-limit-row.is-disabled {
+  opacity: 0.72;
+}
+
+.course-repeat-limit-row__label {
+  flex: 1;
+  min-width: 0;
+}
+
+.course-repeat-limit-row__name {
+  display: block;
+  font-size: 14px;
+  font-weight: 500;
+  color: #303133;
+}
+
+.course-repeat-limit-row__required {
+  color: #f56c6c;
+  margin-left: 2px;
+}
+
+.course-repeat-limit-row__hint {
+  display: block;
+  margin-top: 4px;
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.5;
+}
+
+.course-repeat-limit-datetime {
+  width: 240px;
+  flex-shrink: 0;
+}
+
+.course-repeat-limit-rules {
+  width: 300px;
+  flex-shrink: 0;
+  padding: 18px 20px;
+  background: #f8fafc;
+}
+
+.course-repeat-limit-rules__title {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 13px;
+  font-weight: 600;
+  color: #606266;
+  margin-bottom: 12px;
+}
+
+.course-repeat-limit-rules__title i {
+  color: #409eff;
+  font-size: 15px;
+}
+
+.course-repeat-limit-rules__list {
+  margin: 0;
+  padding: 0 0 0 18px;
+  list-style: none;
+}
+
+.course-repeat-limit-rules__list li {
+  position: relative;
+  font-size: 12px;
+  color: #606266;
+  line-height: 1.7;
+  margin-bottom: 10px;
+  padding-left: 10px;
+}
+
+.course-repeat-limit-rules__list li::before {
+  content: '';
+  position: absolute;
+  left: -8px;
+  top: 8px;
+  width: 5px;
+  height: 5px;
+  border-radius: 50%;
+  background: #409eff;
+  opacity: 0.65;
+}
+
+.course-repeat-limit-rules__list li strong {
+  color: #303133;
+  font-weight: 600;
+}
+
+@media (max-width: 960px) {
+  .course-repeat-limit-body {
+    flex-direction: column;
+  }
+
+  .course-repeat-limit-settings {
+    border-right: none;
+    border-bottom: 1px dashed #e4e7ed;
+  }
+
+  .course-repeat-limit-rules {
+    width: 100%;
+  }
+
+  .course-repeat-limit-row {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 12px;
+  }
+
+  .course-repeat-limit-datetime {
+    width: 100%;
+  }
+}
+