Browse Source

重粉2.0

xw 1 tuần trước cách đây
mục cha
commit
6413837096

+ 5 - 1
src/api/company/companyUser.js

@@ -209,7 +209,11 @@ export function uploadAvatar(data) {
   return request({
   return request({
     url: '/company/user/profile/avatar',
     url: '/company/user/profile/avatar',
     method: 'post',
     method: 'post',
-    data: data
+    data: data,
+    transformRequest: [function (data, headers) {
+      delete headers['Content-Type']
+      return data
+    }]
   })
   })
 }
 }
 
 

+ 4 - 2
src/api/qw/externalContact.js

@@ -16,7 +16,8 @@ export function listExternalContact(query) {
   })
   })
 }
 }
 
 
-// 查询重粉用户看课记录
+// 查询重粉用户看课记录 — GET /qw/externalContact/getWatchLogList
+// query: fsUserId(必填), projectId(可选, 1/28等价合并), courseId, pageNum, pageSize
 export function getWatchLogList(query) {
 export function getWatchLogList(query) {
   return request({
   return request({
     url: '/qw/externalContact/getWatchLogList',
     url: '/qw/externalContact/getWatchLogList',
@@ -25,7 +26,8 @@ export function getWatchLogList(query) {
   })
   })
 }
 }
 
 
-// 查询重粉看课历史综合信息(关联销售 + 课程进度)
+// 重粉看课历史 — GET /qw/externalContact/getRepeatCourseHistory?fsUserId=xxx
+// 返回 projectList、courseList(含 projectId)、salesList 等
 export function getRepeatCourseHistory(fsUserId) {
 export function getRepeatCourseHistory(fsUserId) {
   return request({
   return request({
     url: '/qw/externalContact/getRepeatCourseHistory',
     url: '/qw/externalContact/getRepeatCourseHistory',

+ 2 - 1
src/store/modules/user.js

@@ -1,5 +1,6 @@
 import { login, logout, getInfo,checkIsNeedCheck } from '@/api/login'
 import { login, logout, getInfo,checkIsNeedCheck } from '@/api/login'
 import { getToken, setToken, removeToken } from '@/utils/auth'
 import { getToken, setToken, removeToken } from '@/utils/auth'
+import { resolveAvatarUrl } from '@/utils/avatar'
 
 
 const user = {
 const user = {
   state: {
   state: {
@@ -65,7 +66,7 @@ const user = {
       return new Promise((resolve, reject) => {
       return new Promise((resolve, reject) => {
         getInfo().then(res => {
         getInfo().then(res => {
           const user = res.user
           const user = res.user
-          const avatar = user.avatar == "" ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar;
+          const avatar = resolveAvatarUrl(user.avatar)
           if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
           if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
             commit('SET_ROLES', res.roles)
             commit('SET_ROLES', res.roles)
             commit('SET_PERMISSIONS', res.permissions)
             commit('SET_PERMISSIONS', res.permissions)

+ 24 - 0
src/utils/avatar.js

@@ -0,0 +1,24 @@
+import { isExternal } from '@/utils/validate'
+
+const defaultAvatar = require('@/assets/images/profile.jpg')
+
+/**
+ * 将后端返回的头像路径转为可展示的完整地址
+ */
+export function resolveAvatarUrl(avatar) {
+  if (!avatar) {
+    return defaultAvatar
+  }
+  if (isExternal(avatar) || avatar.startsWith('data:') || avatar.startsWith('blob:')) {
+    return avatar
+  }
+  if (avatar.startsWith('/static/')) {
+    return avatar
+  }
+  const baseApi = process.env.VUE_APP_BASE_API || ''
+  if (baseApi && avatar.indexOf(baseApi) === 0) {
+    return avatar
+  }
+  const path = avatar.startsWith('/') ? avatar : '/' + avatar
+  return baseApi + path
+}

+ 4 - 1
src/views/company/companyUser/profile/index.vue

@@ -8,7 +8,7 @@
           </div>
           </div>
           <div>
           <div>
             <div class="text-center">
             <div class="text-center">
-              <userAvatar :user="user" />
+              <userAvatar :user="user" @avatar-updated="handleAvatarUpdated" />
             </div>
             </div>
             <ul class="list-group list-group-striped">
             <ul class="list-group list-group-striped">
               <li class="list-group-item">
               <li class="list-group-item">
@@ -117,6 +117,9 @@ export default {
         this.postGroup = response.postGroup;
         this.postGroup = response.postGroup;
       });
       });
     },
     },
+    handleAvatarUpdated(avatar) {
+      this.user = { ...this.user, avatar };
+    },
     openWechatBindDialog() {
     openWechatBindDialog() {
       getWechatBindQrcode().then(res => {
       getWechatBindQrcode().then(res => {
         console.log(res)
         console.log(res)

+ 58 - 13
src/views/company/companyUser/profile/userAvatar.vue

@@ -56,6 +56,7 @@
 import store from "@/store";
 import store from "@/store";
 import { VueCropper } from "vue-cropper";
 import { VueCropper } from "vue-cropper";
 import { uploadAvatar } from "@/api/company/companyUser";
 import { uploadAvatar } from "@/api/company/companyUser";
+import { resolveAvatarUrl } from "@/utils/avatar";
 
 
 export default {
 export default {
   components: { VueCropper },
   components: { VueCropper },
@@ -73,7 +74,7 @@ export default {
       // 弹出层标题
       // 弹出层标题
       title: "修改头像",
       title: "修改头像",
       options: {
       options: {
-        img: store.getters.avatar, //裁剪图片的地址
+        img: resolveAvatarUrl(store.getters.avatar),
         autoCrop: true, // 是否默认生成截图框
         autoCrop: true, // 是否默认生成截图框
         autoCropWidth: 200, // 默认生成截图框宽度
         autoCropWidth: 200, // 默认生成截图框宽度
         autoCropHeight: 200, // 默认生成截图框高度
         autoCropHeight: 200, // 默认生成截图框高度
@@ -82,7 +83,25 @@ export default {
       previews: {}
       previews: {}
     };
     };
   },
   },
+  watch: {
+    "user.avatar": {
+      handler(val) {
+        this.syncAvatarImg(val || store.getters.avatar);
+      },
+      immediate: true
+    },
+    "$store.state.user.avatar"(val) {
+      if (!this.user || !this.user.avatar) {
+        this.syncAvatarImg(val);
+      }
+    }
+  },
   methods: {
   methods: {
+    syncAvatarImg(avatar) {
+      if (avatar) {
+        this.options.img = resolveAvatarUrl(avatar);
+      }
+    },
     // 编辑头像
     // 编辑头像
     editCropper() {
     editCropper() {
       this.open = true;
       this.open = true;
@@ -109,29 +128,55 @@ export default {
     },
     },
     // 上传预处理
     // 上传预处理
     beforeUpload(file) {
     beforeUpload(file) {
-      if (file.type.indexOf("image/") == -1) {
-        this.msgError("文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。");
-      } else {
-        const reader = new FileReader();
-        reader.readAsDataURL(file);
-        reader.onload = () => {
-          this.options.img = reader.result;
-        };
+      const allowedTypes = ['image/bmp', 'image/gif', 'image/jpeg', 'image/png']
+      const allowedExt = /\.(bmp|gif|jpe?g|png)$/i
+      if (!allowedTypes.includes(file.type) && !allowedExt.test(file.name)) {
+        this.msgError("文件格式错误,仅支持 bmp/gif/jpg/jpeg/png 格式。");
+        return false;
       }
       }
+      const reader = new FileReader();
+      reader.readAsDataURL(file);
+      reader.onload = () => {
+        this.options.img = reader.result;
+      };
+      return false;
     },
     },
     // 上传图片
     // 上传图片
     uploadImg() {
     uploadImg() {
       this.$refs.cropper.getCropBlob(data => {
       this.$refs.cropper.getCropBlob(data => {
-        let formData = new FormData();
-        formData.append("avatarfile", data);
+        if (!data || data.size === 0) {
+          this.msgError("裁剪图片不能为空");
+          return;
+        }
+        const formData = new FormData();
+        const file = new File([data], "avatar.png", { type: data.type || "image/png" });
+        formData.append("avatarfile", file);
+        const loading = this.$loading({
+          lock: true,
+          text: "上传中...",
+          spinner: "el-icon-loading",
+          background: "rgba(0, 0, 0, 0.7)"
+        });
         uploadAvatar(formData).then(response => {
         uploadAvatar(formData).then(response => {
           if (response.code === 200) {
           if (response.code === 200) {
+            const displayUrl = resolveAvatarUrl(response.imgUrl);
             this.open = false;
             this.open = false;
-            this.options.img = process.env.VUE_APP_BASE_API + response.imgUrl;
-            store.commit('SET_AVATAR', this.options.img);
+            this.options.img = displayUrl;
+            store.commit("SET_AVATAR", displayUrl);
+            const currentUser = store.getters.user;
+            if (currentUser) {
+              store.commit("SET_USER", { ...currentUser, avatar: response.imgUrl });
+            }
+            this.$emit("avatar-updated", response.imgUrl);
             this.msgSuccess("修改成功");
             this.msgSuccess("修改成功");
+          } else {
+            this.msgError(response.msg || "上传失败");
           }
           }
           this.visible = false;
           this.visible = false;
+        }).catch(() => {
+          this.visible = false;
+        }).finally(() => {
+          loading.close();
         });
         });
       });
       });
     },
     },

+ 888 - 0
src/views/qw/externalContact/RepeatCourseHistoryDrawer.vue

@@ -0,0 +1,888 @@
+<template>
+  <el-drawer
+    :visible.sync="visible"
+    direction="rtl"
+    size="720px"
+    append-to-body
+    :with-header="false"
+    :wrapper-closable="false"
+    custom-class="repeat-course-history-drawer"
+    @closed="handleClosed"
+  >
+    <div class="rch-drawer" v-loading="loading">
+      <!-- 顶部客户信息区 -->
+      <div class="rch-hero">
+        <button type="button" class="rch-close" @click="visible = false">
+          <i class="el-icon-close"></i>
+        </button>
+        <div class="rch-hero-inner">
+          <div class="rch-avatar-wrap">
+            <img :src="currentRow.avatar || defaultAvatar" alt="" class="rch-avatar" />
+          </div>
+          <div class="rch-hero-info">
+            <div class="rch-name-row">
+              <h3 class="rch-name">{{ currentRow.name || '-' }}</h3>
+            </div>
+            <p v-if="currentRow.remark" class="rch-remark">
+              <i class="el-icon-edit-outline"></i>{{ currentRow.remark }}
+            </p>
+          </div>
+        </div>
+        <div class="rch-stats">
+          <div class="rch-stat">
+            <span class="rch-stat-num">{{ projectList.length }}</span>
+            <span class="rch-stat-label">看课项目</span>
+          </div>
+          <div class="rch-stat-divider"></div>
+          <div class="rch-stat">
+            <span class="rch-stat-num">{{ mockSales.length }}</span>
+            <span class="rch-stat-label">关联销售</span>
+          </div>
+          <div class="rch-stat-divider"></div>
+          <div class="rch-stat">
+            <span class="rch-stat-num">{{ filteredCourses.length }}</span>
+            <span class="rch-stat-label">课程进度</span>
+          </div>
+        </div>
+      </div>
+
+      <!-- 可滚动内容区 -->
+      <div class="rch-body">
+        <!-- 看课项目 -->
+        <section class="rch-panel">
+          <div class="rch-panel-head">
+            <span class="rch-panel-icon rch-panel-icon--project"><i class="el-icon-folder-opened"></i></span>
+            <span class="rch-panel-title">看课项目</span>
+          </div>
+          <div class="rch-project-chips" v-if="projectList.length">
+            <button
+              v-for="item in projectList"
+              :key="item.projectId"
+              type="button"
+              class="rch-chip"
+              :class="{
+                'rch-chip--repeat': item.isRepeat == 1,
+                'rch-chip--active': Number(selectedProjectId) === Number(item.projectId)
+              }"
+              @click="selectProject(item.projectId)"
+            >
+              {{ item.projectName }}
+              <i v-if="item.isRepeat == 1" class="el-icon-warning-outline rch-chip-warn"></i>
+            </button>
+          </div>
+          <div v-else class="rch-empty">暂无看课项目</div>
+          <p v-if="showEquivalentHint" class="rch-hint">
+            <i class="el-icon-info"></i>百膳食养与安康食养为同一项目,课程数据已合并展示
+          </p>
+        </section>
+
+        <!-- 关联销售 -->
+        <section class="rch-panel">
+          <div class="rch-panel-head">
+            <span class="rch-panel-icon rch-panel-icon--sales"><i class="el-icon-user"></i></span>
+            <span class="rch-panel-title">关联销售</span>
+            <span class="rch-panel-count">{{ mockSales.length }} 人</span>
+          </div>
+          <div class="rch-sales-grid" v-if="mockSales.length">
+            <div
+              v-for="(item, idx) in mockSales"
+              :key="idx"
+              class="rch-sales-item"
+              :class="{ 'rch-sales-item--locked': item.hasPermission === false }"
+            >
+              <div class="rch-sales-top">
+                <span class="rch-sales-name">{{ item.qwUserName }}</span>
+                <span
+                  v-if="item.status != null"
+                  class="rch-tag"
+                  :class="salesStatusClass(item.status)"
+                >{{ salesStatusLabel(item.status) }}</span>
+              </div>
+              <div class="rch-sales-meta">
+                <span><i class="el-icon-office-building"></i>{{ item.corpName || '-' }}</span>
+                <span><i class="el-icon-time"></i>{{ item.addTime || '-' }}</span>
+              </div>
+              <span v-if="item.hasPermission === false" class="rch-lock-tag">无权查看</span>
+            </div>
+          </div>
+          <div v-else class="rch-empty">暂无关联销售数据</div>
+        </section>
+
+        <!-- 课程学习进度 -->
+        <section class="rch-panel">
+          <div class="rch-panel-head">
+            <span class="rch-panel-icon rch-panel-icon--course"><i class="el-icon-reading"></i></span>
+            <span class="rch-panel-title">课程学习进度</span>
+            <span v-if="selectedProjectId" class="rch-panel-count">{{ filteredCourses.length }} 门</span>
+          </div>
+          <div class="rch-course-grid" v-if="filteredCourses.length">
+            <div
+              v-for="(course, idx) in filteredCourses"
+              :key="course.courseId || idx"
+              class="rch-course-card"
+              :class="{ 'rch-course-card--done': course.finished }"
+            >
+              <div class="rch-course-top">
+                <div class="rch-course-title">
+                  <span class="rch-course-idx">{{ idx + 1 }}</span>
+                  <span class="rch-course-name">{{ course.courseName }}</span>
+                </div>
+                <span
+                  class="rch-tag"
+                  :class="course.finished ? 'rch-tag--success' : 'rch-tag--warning'"
+                >{{ course.finished ? '已完课' : '学习中' }}</span>
+              </div>
+              <div class="rch-course-progress">
+                <div class="rch-progress-track">
+                  <div
+                    class="rch-progress-fill"
+                    :style="{ width: course.percentage + '%' }"
+                    :class="{ 'rch-progress-fill--done': course.finished }"
+                  ></div>
+                </div>
+                <span class="rch-progress-label">
+                  <strong>{{ course.watchedCount }}</strong>/{{ course.totalCount }} 节 · {{ course.percentage }}%
+                </span>
+              </div>
+              <div v-if="course.latestSection" class="rch-course-latest">
+                <i class="el-icon-video-play"></i>
+                <span>{{ course.latestSection }}</span>
+                <time v-if="course.latestTime">{{ course.latestTime }}</time>
+              </div>
+              <div v-if="course.qwUserName || course.corpName" class="rch-course-source">
+                <span v-if="course.qwUserName"><i class="el-icon-user"></i>{{ course.qwUserName }}</span>
+                <span v-if="course.corpName"><i class="el-icon-office-building"></i>{{ course.corpName }}</span>
+                <span v-if="course.hasPermission === false" class="rch-lock-tag rch-lock-tag--inline">无权查看</span>
+              </div>
+            </div>
+          </div>
+          <div v-else-if="selectedProjectId" class="rch-empty">该项目下暂无课程学习记录</div>
+          <div v-else class="rch-empty">请先选择看课项目</div>
+        </section>
+
+        <!-- 详细看课记录 -->
+        <section class="rch-panel rch-panel--detail">
+          <div class="rch-panel-head rch-panel-head--clickable" @click="detailExpanded = !detailExpanded">
+            <span class="rch-panel-icon rch-panel-icon--log"><i class="el-icon-document"></i></span>
+            <span class="rch-panel-title">详细看课记录</span>
+            <span v-if="detailExpanded && total" class="rch-panel-count">{{ total }} 条</span>
+            <i :class="detailExpanded ? 'el-icon-arrow-up' : 'el-icon-arrow-down'" class="rch-expand-icon"></i>
+          </div>
+          <el-collapse-transition>
+            <div v-show="detailExpanded" class="rch-detail-body">
+              <div class="rch-filter-bar">
+                <el-select
+                  v-model="queryParams.courseId"
+                  filterable
+                  placeholder="筛选课程"
+                  clearable
+                  size="small"
+                  class="rch-filter-select"
+                >
+                  <el-option
+                    v-for="c in filteredCourses"
+                    :key="c.courseId"
+                    :label="c.courseName"
+                    :value="c.courseId"
+                  />
+                </el-select>
+                <el-button type="primary" size="small" icon="el-icon-search" @click="handleQueryWatchLog">查询</el-button>
+              </div>
+              <el-table :data="list" size="small" class="rch-table" stripe>
+                <el-table-column label="企微主体" prop="corpName" min-width="110" show-overflow-tooltip>
+                  <template slot-scope="scope">
+                    <span :class="{ 'rch-no-perm': scope.row.hasPermission === false }">{{ scope.row.corpName }}</span>
+                  </template>
+                </el-table-column>
+                <el-table-column label="所属企微" prop="qwUserName" min-width="90" show-overflow-tooltip>
+                  <template slot-scope="scope">
+                    <span :class="{ 'rch-no-perm': scope.row.hasPermission === false }">{{ scope.row.qwUserName }}</span>
+                  </template>
+                </el-table-column>
+                <el-table-column label="项目" prop="projectName" min-width="90" show-overflow-tooltip />
+                <el-table-column label="课程" prop="courseName" min-width="110" show-overflow-tooltip />
+                <el-table-column label="小节" prop="videoName" min-width="90" show-overflow-tooltip />
+                <el-table-column label="记录时间" prop="createTime" min-width="140" show-overflow-tooltip />
+                <el-table-column label="完课" prop="logType" width="72" align="center">
+                  <template slot-scope="scope">
+                    <span
+                      class="rch-tag rch-tag--mini"
+                      :class="scope.row.logType == 2 ? 'rch-tag--success' : 'rch-tag--muted'"
+                    >{{ scope.row.logType == 2 ? '已完课' : '未完课' }}</span>
+                  </template>
+                </el-table-column>
+                <el-table-column label="完课时间" prop="finishTime" min-width="140" show-overflow-tooltip />
+              </el-table>
+              <pagination
+                v-show="total > 0"
+                :total="total"
+                :page.sync="queryParams.pageNum"
+                :limit.sync="queryParams.pageSize"
+                @pagination="fetchWatchLogList"
+              />
+            </div>
+          </el-collapse-transition>
+        </section>
+      </div>
+    </div>
+  </el-drawer>
+</template>
+
+<script>
+import { getRepeatCourseHistory, getWatchLogList } from '@/api/qw/externalContact'
+
+const SALES_STATUS = { 0: '正常', 1: '离职待接替', 2: '正在接替', 3: '流失', 4: '删除' }
+
+export default {
+  name: 'RepeatCourseHistoryDrawer',
+  data() {
+    return {
+      visible: false,
+      loading: false,
+      list: [],
+      total: 0,
+      detailExpanded: false,
+      currentRow: {},
+      projectList: [],
+      selectedProjectId: null,
+      mockSales: [],
+      mockCourses: [],
+      projectOptions: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        fsUserId: null,
+        projectId: null,
+        courseId: null,
+      },
+      defaultAvatar: require('@/assets/image/profile.jpg'),
+    }
+  },
+  computed: {
+    filteredCourses() {
+      const courses = this.mockCourses || []
+      const selectedId = this.selectedProjectId
+      if (selectedId == null || selectedId === '') return []
+      const matchIds = this.getMatchProjectIds(selectedId)
+      return courses.filter(c => matchIds.includes(Number(c.projectId)))
+    },
+    showEquivalentHint() {
+      const ids = new Set(this.projectList.map(p => Number(p.projectId)))
+      return ids.has(1) && ids.has(28)
+    },
+  },
+  created() {
+    this.getDicts('sys_course_project').then(res => {
+      this.projectOptions = res.data || []
+    })
+  },
+  methods: {
+    open(row) {
+      if (!row || !row.fsUserId) return
+      this.queryParams.fsUserId = row.fsUserId
+      this.visible = true
+      this.loading = true
+      this.detailExpanded = false
+      this.projectList = []
+      this.selectedProjectId = null
+      this.list = []
+      this.total = 0
+      this.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.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.projectList = this.buildDisplayProjectList(data.projectList)
+          this.mockSales = (data.salesList || []).map(s => ({
+            qwUserName: s.qwUserName,
+            corpName: s.corpName,
+            addTime: s.addTime,
+            status: s.status,
+            hasPermission: s.hasPermission,
+          }))
+          this.mockCourses = (data.courseList || []).map(c => ({
+            courseId: c.courseId,
+            courseName: c.courseName,
+            projectId: c.projectId != null ? Number(c.projectId) : null,
+            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,
+          }))
+          if (this.projectList.length > 0) {
+            this.selectedProjectId = this.projectList[0].projectId
+            this.queryParams.projectId = this.selectedProjectId
+          }
+          this.fetchWatchLogList()
+        }
+        this.loading = false
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    handleClosed() {
+      this.queryParams.courseId = null
+      this.queryParams.pageNum = 1
+    },
+    getMatchProjectIds(projectId) {
+      const pid = Number(projectId)
+      return pid === 1 || pid === 28 ? [1, 28] : [pid]
+    },
+    getProjectLabelById(projectId) {
+      const id = Number(projectId)
+      const found = this.projectOptions.find(o => Number(o.dictValue) === id)
+      return found ? found.dictLabel : `项目${id}`
+    },
+    buildDisplayProjectList(projectList) {
+      const equivalentMap = { 1: 28, 28: 1 }
+      const list = (projectList || []).map(p => ({
+        projectId: Number(p.projectId),
+        projectName: p.projectName || this.getProjectLabelById(p.projectId),
+        isRepeat: p.isRepeat,
+      }))
+      const idSet = new Set(list.map(p => p.projectId))
+      list.forEach(item => {
+        const eqId = equivalentMap[item.projectId]
+        if (eqId != null && !idSet.has(eqId)) {
+          list.push({
+            projectId: eqId,
+            projectName: this.getProjectLabelById(eqId),
+            isRepeat: item.isRepeat,
+          })
+          idSet.add(eqId)
+        }
+      })
+      return list.sort((a, b) => a.projectId - b.projectId)
+    },
+    selectProject(projectId) {
+      const pid = Number(projectId)
+      this.selectedProjectId = pid
+      this.queryParams.projectId = pid
+      this.queryParams.courseId = null
+      this.queryParams.pageNum = 1
+      this.detailExpanded = true
+      this.fetchWatchLogList()
+    },
+    handleQueryWatchLog() {
+      this.queryParams.pageNum = 1
+      this.fetchWatchLogList()
+    },
+    fetchWatchLogList() {
+      const params = {
+        pageNum: this.queryParams.pageNum,
+        pageSize: this.queryParams.pageSize,
+        fsUserId: this.queryParams.fsUserId,
+        courseId: this.queryParams.courseId || undefined,
+        projectId: this.queryParams.projectId || undefined,
+      }
+      getWatchLogList(params).then(e => {
+        this.list = e.rows || []
+        this.total = e.total || 0
+      })
+    },
+    salesStatusLabel(status) {
+      return SALES_STATUS[status] || '未知'
+    },
+    salesStatusClass(status) {
+      if (status === 0) return 'rch-tag--success'
+      if (status === 1 || status === 2) return 'rch-tag--warning'
+      return 'rch-tag--danger'
+    },
+  },
+}
+</script>
+
+<style>
+.repeat-course-history-drawer .el-drawer__body {
+  padding: 0;
+  overflow: hidden;
+  height: 100%;
+}
+</style>
+
+<style scoped>
+.rch-drawer {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  background: #f1f5f9;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', sans-serif;
+}
+
+/* ===== Hero ===== */
+.rch-hero {
+  position: relative;
+  flex-shrink: 0;
+  background: #fff;
+  padding: 20px 24px 0;
+  color: #1e293b;
+  border-bottom: 1px solid #e2e8f0;
+}
+.rch-close {
+  position: absolute;
+  top: 16px;
+  right: 16px;
+  width: 32px;
+  height: 32px;
+  border: none;
+  border-radius: 8px;
+  background: #f1f5f9;
+  color: #64748b;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 16px;
+  transition: background 0.2s, color 0.2s;
+  z-index: 2;
+}
+.rch-close:hover {
+  background: #e2e8f0;
+  color: #334155;
+}
+.rch-hero-inner {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  padding-right: 40px;
+  margin-bottom: 20px;
+}
+.rch-avatar-wrap {
+  position: relative;
+  flex-shrink: 0;
+}
+.rch-avatar {
+  width: 56px;
+  height: 56px;
+  border-radius: 16px;
+  object-fit: cover;
+  border: 3px solid #e2e8f0;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+.rch-name-row {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  flex-wrap: wrap;
+}
+.rch-name {
+  margin: 0;
+  font-size: 20px;
+  font-weight: 700;
+  letter-spacing: -0.02em;
+  line-height: 1.3;
+  color: #0f172a;
+}
+.rch-remark {
+  margin: 6px 0 0;
+  font-size: 13px;
+  color: #64748b;
+  line-height: 1.5;
+  display: flex;
+  align-items: flex-start;
+  gap: 4px;
+}
+.rch-remark i {
+  margin-top: 2px;
+  flex-shrink: 0;
+}
+.rch-stats {
+  display: flex;
+  align-items: center;
+  background: #f8fafc;
+  border: 1px solid #e2e8f0;
+  border-bottom: none;
+  border-radius: 12px 12px 0 0;
+  padding: 14px 0;
+}
+.rch-stat {
+  flex: 1;
+  text-align: center;
+}
+.rch-stat-num {
+  display: block;
+  font-size: 22px;
+  font-weight: 700;
+  line-height: 1.2;
+  color: #0f172a;
+}
+.rch-stat-label {
+  display: block;
+  font-size: 12px;
+  color: #64748b;
+  margin-top: 2px;
+}
+.rch-stat-divider {
+  width: 1px;
+  height: 32px;
+  background: #e2e8f0;
+}
+
+/* ===== Body ===== */
+.rch-body {
+  flex: 1;
+  overflow-y: auto;
+  padding: 16px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+.rch-body::-webkit-scrollbar {
+  width: 6px;
+}
+.rch-body::-webkit-scrollbar-thumb {
+  background: #cbd5e1;
+  border-radius: 3px;
+}
+
+/* ===== Panel ===== */
+.rch-panel {
+  background: #fff;
+  border-radius: 12px;
+  padding: 16px;
+  box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
+}
+.rch-panel-head {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 14px;
+}
+.rch-panel-head--clickable {
+  cursor: pointer;
+  user-select: none;
+  margin-bottom: 0;
+}
+.rch-panel-head--clickable + .rch-detail-body,
+.rch-panel--detail .rch-panel-head--clickable {
+  margin-bottom: 14px;
+}
+.rch-panel-icon {
+  width: 28px;
+  height: 28px;
+  border-radius: 8px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 14px;
+}
+.rch-panel-icon--project { background: #eef2ff; color: #6366f1; }
+.rch-panel-icon--sales { background: #ecfdf5; color: #10b981; }
+.rch-panel-icon--course { background: #fff7ed; color: #f59e0b; }
+.rch-panel-icon--log { background: #f0f9ff; color: #0ea5e9; }
+.rch-panel-title {
+  font-size: 15px;
+  font-weight: 600;
+  color: #1e293b;
+}
+.rch-panel-count {
+  margin-left: auto;
+  font-size: 12px;
+  color: #94a3b8;
+  font-weight: 500;
+}
+.rch-expand-icon {
+  margin-left: 4px;
+  color: #94a3b8;
+  font-size: 14px;
+}
+
+/* ===== Project chips ===== */
+.rch-project-chips {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+.rch-chip {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  padding: 6px 14px;
+  border-radius: 20px;
+  border: 1.5px solid #e2e8f0;
+  background: #f8fafc;
+  color: #475569;
+  font-size: 13px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s;
+  outline: none;
+}
+.rch-chip:hover {
+  border-color: #6366f1;
+  color: #6366f1;
+  background: #eef2ff;
+}
+.rch-chip--active {
+  background: #6366f1;
+  border-color: #6366f1;
+  color: #fff;
+  box-shadow: 0 2px 8px rgba(99, 102, 241, 0.35);
+}
+.rch-chip--repeat:not(.rch-chip--active) {
+  border-color: #fecaca;
+  color: #dc2626;
+  background: #fef2f2;
+}
+.rch-chip--repeat.rch-chip--active {
+  background: #dc2626;
+  border-color: #dc2626;
+  box-shadow: 0 2px 8px rgba(220, 38, 38, 0.35);
+}
+.rch-chip-warn {
+  font-size: 12px;
+}
+.rch-hint {
+  margin: 10px 0 0;
+  padding: 8px 12px;
+  background: #f0f9ff;
+  border-radius: 8px;
+  font-size: 12px;
+  color: #0369a1;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  line-height: 1.4;
+}
+
+/* ===== Sales ===== */
+.rch-sales-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+  gap: 10px;
+}
+.rch-sales-item {
+  position: relative;
+  padding: 12px 14px;
+  border-radius: 10px;
+  border: 1px solid #e2e8f0;
+  background: #fafbfc;
+  transition: box-shadow 0.2s, border-color 0.2s;
+}
+.rch-sales-item:hover {
+  border-color: #cbd5e1;
+  box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
+}
+.rch-sales-item--locked {
+  opacity: 0.72;
+}
+.rch-sales-top {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 8px;
+  margin-bottom: 8px;
+}
+.rch-sales-name {
+  font-weight: 600;
+  color: #1e293b;
+  font-size: 14px;
+}
+.rch-sales-meta {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  font-size: 12px;
+  color: #64748b;
+}
+.rch-sales-meta span {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+}
+.rch-sales-meta i {
+  color: #94a3b8;
+  width: 14px;
+  text-align: center;
+}
+
+/* ===== Tags ===== */
+.rch-tag {
+  display: inline-flex;
+  align-items: center;
+  padding: 2px 8px;
+  border-radius: 6px;
+  font-size: 11px;
+  font-weight: 600;
+  white-space: nowrap;
+}
+.rch-tag--mini {
+  padding: 1px 6px;
+  font-size: 11px;
+}
+.rch-tag--success { background: #dcfce7; color: #15803d; }
+.rch-tag--warning { background: #fef3c7; color: #b45309; }
+.rch-tag--danger { background: #fee2e2; color: #b91c1c; }
+.rch-tag--muted { background: #f1f5f9; color: #64748b; }
+.rch-lock-tag {
+  position: absolute;
+  top: 10px;
+  right: 10px;
+  font-size: 11px;
+  color: #94a3b8;
+  background: #f1f5f9;
+  padding: 1px 6px;
+  border-radius: 4px;
+}
+.rch-lock-tag--inline {
+  position: static;
+  margin-left: auto;
+}
+
+/* ===== Courses ===== */
+.rch-course-grid {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+.rch-course-card {
+  padding: 14px 16px;
+  border-radius: 10px;
+  border: 1px solid #e2e8f0;
+  background: #fff;
+  transition: box-shadow 0.2s;
+}
+.rch-course-card:hover {
+  box-shadow: 0 4px 12px rgba(15, 23, 42, 0.08);
+}
+.rch-course-card--done {
+  border-left: 3px solid #10b981;
+}
+.rch-course-top {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 10px;
+  margin-bottom: 10px;
+}
+.rch-course-title {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  min-width: 0;
+}
+.rch-course-idx {
+  flex-shrink: 0;
+  width: 22px;
+  height: 22px;
+  border-radius: 6px;
+  background: #6366f1;
+  color: #fff;
+  font-size: 11px;
+  font-weight: 700;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.rch-course-card--done .rch-course-idx {
+  background: #10b981;
+}
+.rch-course-name {
+  font-weight: 600;
+  color: #1e293b;
+  font-size: 14px;
+  line-height: 1.4;
+}
+.rch-course-progress {
+  margin-bottom: 8px;
+}
+.rch-progress-track {
+  height: 6px;
+  background: #e2e8f0;
+  border-radius: 3px;
+  overflow: hidden;
+  margin-bottom: 6px;
+}
+.rch-progress-fill {
+  height: 100%;
+  border-radius: 3px;
+  background: linear-gradient(90deg, #6366f1, #818cf8);
+  transition: width 0.4s ease;
+}
+.rch-progress-fill--done {
+  background: linear-gradient(90deg, #10b981, #34d399);
+}
+.rch-progress-label {
+  font-size: 12px;
+  color: #64748b;
+}
+.rch-progress-label strong {
+  color: #1e293b;
+  font-size: 13px;
+}
+.rch-course-latest {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 12px;
+  color: #6366f1;
+  margin-bottom: 6px;
+  flex-wrap: wrap;
+}
+.rch-course-latest i {
+  color: #818cf8;
+}
+.rch-course-latest time {
+  color: #94a3b8;
+  margin-left: auto;
+}
+.rch-course-source {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  font-size: 12px;
+  color: #94a3b8;
+  flex-wrap: wrap;
+}
+.rch-course-source span {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+}
+
+/* ===== Detail ===== */
+.rch-detail-body {
+  padding-top: 4px;
+}
+.rch-filter-bar {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  margin-bottom: 12px;
+}
+.rch-filter-select {
+  flex: 1;
+  max-width: 320px;
+}
+.rch-table {
+  border-radius: 8px;
+  overflow: hidden;
+}
+.rch-no-perm {
+  color: #cbd5e1;
+  font-style: italic;
+}
+
+/* ===== Empty ===== */
+.rch-empty {
+  text-align: center;
+  color: #94a3b8;
+  padding: 24px 0;
+  font-size: 13px;
+}
+</style>

+ 29 - 506
src/views/qw/externalContact/index.vue

@@ -83,8 +83,24 @@
           />
           />
         </el-select>
         </el-select>
       </el-form-item>
       </el-form-item>
-      <el-form-item label="是否重粉" prop="isRepeat">
-        <el-select v-model="queryParams.isRepeat" placeholder="重粉" clearable size="small">
+      <el-form-item label="项目归属" prop="projectId">
+        <el-select v-model="queryParams.projectId" placeholder="请选择项目" clearable size="small" @change="handleProjectChange">
+          <el-option
+            v-for="item in projectOptions"
+            :key="item.dictValue"
+            :label="item.dictLabel"
+            :value="item.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否重粉" prop="userRepeat">
+        <el-select
+          v-model="queryParams.userRepeat"
+          :placeholder="queryParams.projectId ? '重粉' : '请先选择项目'"
+          clearable
+          size="small"
+          :disabled="!queryParams.projectId"
+        >
           <el-option label="正常" :value="0"/>
           <el-option label="正常" :value="0"/>
           <el-option label="重粉" :value="1"/>
           <el-option label="重粉" :value="1"/>
         </el-select>
         </el-select>
@@ -517,7 +533,7 @@
             <el-button
             <el-button
               size="mini"
               size="mini"
               type="text"
               type="text"
-              @click="showLog(scope.row)"
+              @click="$refs.repeatCourseHistoryDrawer.open(scope.row)"
             >看课历史
             >看课历史
             </el-button>
             </el-button>
           </div>
           </div>
@@ -899,161 +915,7 @@
       <mycustomer ref="mycustomer"  @bindCustomerId="bindCustomerId"></mycustomer>
       <mycustomer ref="mycustomer"  @bindCustomerId="bindCustomerId"></mycustomer>
     </el-dialog>
     </el-dialog>
 
 
-    <!-- 重粉看课记录 - 卡片式弹窗   -->
-    <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/image/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="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 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>
-
-        <!-- 详细看课记录 -->
-        <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" ref="queryForm" :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">
-                <template slot-scope="scope">
-                  <span :class="{'no-permission': scope.row.hasPermission === false}">{{ scope.row.corpName }}</span>
-                </template>
-              </el-table-column>
-              <el-table-column label="所属企微" align="center" prop="qwUserName" min-width="100">
-                <template slot-scope="scope">
-                  <span :class="{'no-permission': scope.row.hasPermission === false}">{{ scope.row.qwUserName }}</span>
-                </template>
-              </el-table-column>
-              <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>
+    <RepeatCourseHistoryDrawer ref="repeatCourseHistoryDrawer" />
 
 
 <!--    设置一个课程sop-->
 <!--    设置一个课程sop-->
     <el-dialog :title="setSop.title" :visible.sync="setSop.open"  width="1200px" append-to-body>
     <el-dialog :title="setSop.title" :visible.sync="setSop.open"  width="1200px" append-to-body>
@@ -1134,9 +996,8 @@ import {
   setCustomerCourseSop,
   setCustomerCourseSop,
   getCustomerCourseSop,
   getCustomerCourseSop,
   setCustomerCourseSopList,
   setCustomerCourseSopList,
-  unBindUserId, updateExternalContactCall,updateExternalContactStatus,getWatchLogList,
-  updateFirstLoginRewardAddress,
-  getRepeatCourseHistory
+  unBindUserId, updateExternalContactCall,updateExternalContactStatus,
+  updateFirstLoginRewardAddress
 } from '@/api/qw/externalContact'
 } from '@/api/qw/externalContact'
 import {getMyQwUserList, getMyQwCompanyList, updateUser,getQwUserListLikeName} from "@/api/qw/user";
 import {getMyQwUserList, getMyQwCompanyList, updateUser,getQwUserListLikeName} from "@/api/qw/user";
 import {listTag, getTag, searchTags} from "@/api/qw/tag";
 import {listTag, getTag, searchTags} from "@/api/qw/tag";
@@ -1153,36 +1014,15 @@ import PaginationMore from "../../../components/PaginationMore/index.vue";
 import userDetails from '@/views/store/components/userDetails.vue';
 import userDetails from '@/views/store/components/userDetails.vue';
 import {courseList, videoList} from "@/api/course/courseRedPacketLog";
 import {courseList, videoList} from "@/api/course/courseRedPacketLog";
 import Collection from './collection.vue';
 import Collection from './collection.vue';
+import RepeatCourseHistoryDrawer from './RepeatCourseHistoryDrawer.vue';
 export default {
 export default {
   name: "ExternalContact",
   name: "ExternalContact",
-  components:{PaginationMore, mycustomer,customerDetails,SopDialog,selectUser,info,userDetails,collection},
+  components:{PaginationMore, mycustomer,customerDetails,SopDialog,selectUser,info,userDetails,collection,RepeatCourseHistoryDrawer},
   data() {
   data() {
     return {
     return {
       projectOptions: [],
       projectOptions: [],
       courseLists: [],
       courseLists: [],
       videoList: [],
       videoList: [],
-      //重粉记录的参数
-      log: {
-        open: false,
-        loading: true,
-        list: [],
-        total: 0,
-        detailExpanded: false,
-        currentRow: {},
-        // 模拟数据 - 关联销售
-        mockSales: [],
-        // 模拟数据 - 课程学习进度
-        mockCourses: [],
-        queryParams: {
-          pageNum: 1,
-          pageSize: 10,
-          externalUserId: null,
-          fsUserId: null,
-          projectId: null,
-          courseId: null,
-          videoId: null,
-        },
-      },
       statusDialog: {
       statusDialog: {
         open: false,
         open: false,
         title: "修改客户状态"
         title: "修改客户状态"
@@ -1401,7 +1241,8 @@ export default {
         wayId:null,
         wayId:null,
         levelType:null,
         levelType:null,
         companyUser:null,
         companyUser:null,
-        isRepeat: null
+        projectId: null,
+        userRepeat: null
       },
       },
       //选择的标签
       //选择的标签
       selectTags:[],
       selectTags:[],
@@ -1468,83 +1309,10 @@ export default {
 
 
   },
   },
   methods: {
   methods: {
-    /** 重粉查看操作 */
-    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();
-    },
-    handleQueryWatchLog() {
-      this.log.queryParams.pageNum = 1;
-      this.log.queryParams.pageSize = 10;
-      this.logList();
-    },
-    logList() {
-      // 只传 fsUserId 和 courseId,确保查询有效
-      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;
-      });
-    },
-    courseChange(row) {
-      this.log.queryParams.videoId = null;
-      if (row === '') {
-        this.videoList = [];
-        return
+    handleProjectChange() {
+      if (!this.queryParams.projectId) {
+        this.queryParams.userRepeat = null;
       }
       }
-      videoList(row).then(response => {
-        this.videoList = response.list
-      });
     },
     },
     handleMemberdetails(row){
     handleMemberdetails(row){
             this.show.open=true;
             this.show.open=true;
@@ -2935,248 +2703,3 @@ export default {
   margin-bottom: 20px;
   margin-bottom: 20px;
 }
 }
 </style>
 </style>
-
-<!-- 重粉看课历史弹窗样式(不能加 scoped,否则 custom-class 不生效) -->
-<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;
-  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;
-}
-.customer-info-card .customer-detail {
-  flex: 1;
-}
-.customer-info-card .customer-name-row {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  margin-bottom: 4px;
-}
-.customer-info-card .customer-name {
-  font-size: 16px;
-  font-weight: 600;
-  color: #303133;
-}
-.customer-info-card .customer-meta {
-  	color: #909399;
-  font-size: 12px;
-}
-
-/* 区块样式 */
-.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;
-}
-.section-title .el-tag {
-  margin-left: 4px;
-}
-
-/* 销售卡片 */
-.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;
-  transition: box-shadow 0.2s;
-}
-.sales-card:hover {
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
-}
-.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;
-  transition: box-shadow 0.2s;
-}
-.course-item:hover {
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
-}
-.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;
-  font-size: 12px;
-  font-weight: 600;
-}
-.course-name {
-  font-weight: 600;
-  color: #303133;
-  font-size: 14px;
-}
-.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-progress .progress-text b {
-  color: #303133;
-  font-size: 13px;
-}
-.course-latest {
-  display: flex;
-  align-items: center;
-  gap: 4px;
-  font-size: 12px;
-}
-.course-latest .latest-label {
-  color: #909399;
-}
-.course-latest .latest-section {
-  color: #409EFF;
-  font-weight: 500;
-}
-.course-latest .latest-time {
-  color: #c0c4cc;
-  margin-left: 8px;
-}
-.course-source {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-  font-size: 12px;
-  margin-top: 4px;
-}
-.course-source .source-item {
-  color: #909399;
-  display: inline-flex;
-  align-items: center;
-  gap: 4px;
-  line-height: 1;
-}
-.course-source .source-item i {
-  font-size: 13px;
-  line-height: 1;
-  vertical-align: middle;
-}
-.no-permission {
-  color: #c0c4cc;
-  font-style: italic;
-}
-
-/* 空提示 */
-.empty-tip {
-  text-align: center;
-  color: #c0c4cc;
-  padding: 20px 0;
-  font-size: 13px;
-}
-</style>

+ 29 - 507
src/views/qw/externalContact/myExternalContact.vue

@@ -85,8 +85,24 @@
           />
           />
         </el-select>
         </el-select>
       </el-form-item>
       </el-form-item>
-      <el-form-item label="是否重粉" prop="isRepeat">
-        <el-select v-model="queryParams.isRepeat" placeholder="重粉" clearable size="small">
+      <el-form-item label="项目归属" prop="projectId">
+        <el-select v-model="queryParams.projectId" placeholder="请选择项目" clearable size="small" @change="handleProjectChange">
+          <el-option
+            v-for="item in projectOptions"
+            :key="item.dictValue"
+            :label="item.dictLabel"
+            :value="item.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否重粉" prop="userRepeat">
+        <el-select
+          v-model="queryParams.userRepeat"
+          :placeholder="queryParams.projectId ? '重粉' : '请先选择项目'"
+          clearable
+          size="small"
+          :disabled="!queryParams.projectId"
+        >
           <el-option label="正常" :value="0"/>
           <el-option label="正常" :value="0"/>
           <el-option label="重粉" :value="1"/>
           <el-option label="重粉" :value="1"/>
         </el-select>
         </el-select>
@@ -477,7 +493,7 @@
             <el-button
             <el-button
               size="mini"
               size="mini"
               type="text"
               type="text"
-              @click="showLog(scope.row)"
+              @click="$refs.repeatCourseHistoryDrawer.open(scope.row)"
             >看课历史
             >看课历史
             </el-button>
             </el-button>
           </div>
           </div>
@@ -948,161 +964,7 @@
       </div>
       </div>
     </el-dialog>
     </el-dialog>
 
 
-    <!-- 重粉看课记录 - 卡片式弹窗   -->
-    <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/image/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="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 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>
-
-        <!-- 详细看课记录 -->
-        <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" ref="queryForm" :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">
-                <template slot-scope="scope">
-                  <span :class="{'no-permission': scope.row.hasPermission === false}">{{ scope.row.corpName }}</span>
-                </template>
-              </el-table-column>
-              <el-table-column label="所属企微" align="center" prop="qwUserName" min-width="100">
-                <template slot-scope="scope">
-                  <span :class="{'no-permission': scope.row.hasPermission === false}">{{ scope.row.qwUserName }}</span>
-                </template>
-              </el-table-column>
-              <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>
+    <RepeatCourseHistoryDrawer ref="repeatCourseHistoryDrawer" />
 
 
     <el-drawer
     <el-drawer
         :with-header="false"
         :with-header="false"
@@ -1196,9 +1058,8 @@ import {
   setCustomerCourseSop,
   setCustomerCourseSop,
   getCustomerCourseSop,
   getCustomerCourseSop,
   setCustomerCourseSopList,
   setCustomerCourseSopList,
-  syncMyExternalContact, unBindUserId, updateExternalContactCall,exportMyExternalContact,updateExternalContactStatus,getWatchLogList,
-  updateFirstLoginRewardAddress,
-  getRepeatCourseHistory
+  syncMyExternalContact, unBindUserId, updateExternalContactCall,exportMyExternalContact,updateExternalContactStatus,
+  updateFirstLoginRewardAddress
 } from '@/api/qw/externalContact'
 } from '@/api/qw/externalContact'
 import info from "@/views/qw/externalContact/info.vue";
 import info from "@/views/qw/externalContact/info.vue";
 import {getMyQwUserList, getMyQwCompanyList, handleInputAuthAppKey, updateUser} from "@/api/qw/user";
 import {getMyQwUserList, getMyQwCompanyList, handleInputAuthAppKey, updateUser} from "@/api/qw/user";
@@ -1214,12 +1075,13 @@ import {createLinkUrl} from "@/api/course/sopCourseLink";
 import {docList} from "@/api/doctor/doctor";
 import {docList} from "@/api/doctor/doctor";
 import PaginationMore from "../../../components/PaginationMore/index.vue";
 import PaginationMore from "../../../components/PaginationMore/index.vue";
 import Collection from './collection.vue';
 import Collection from './collection.vue';
+import RepeatCourseHistoryDrawer from './RepeatCourseHistoryDrawer.vue';
 import {courseList, videoList} from "@/api/course/courseRedPacketLog";
 import {courseList, videoList} from "@/api/course/courseRedPacketLog";
 import userDetails from '@/views/store/components/userDetails.vue';
 import userDetails from '@/views/store/components/userDetails.vue';
 import { updateFsUserRedStatus } from '@/api/user/fsUser'
 import { updateFsUserRedStatus } from '@/api/user/fsUser'
 export default {
 export default {
   name: "ExternalContact",
   name: "ExternalContact",
-  components:{PaginationMore, mycustomer,customerDetails,SopDialog,selectUser,info,Collection,userDetails},
+  components:{PaginationMore, mycustomer,customerDetails,SopDialog,selectUser,info,Collection,userDetails,RepeatCourseHistoryDrawer},
   data() {
   data() {
     return {
     return {
       member:{
       member:{
@@ -1229,28 +1091,6 @@ export default {
       projectOptions: [],
       projectOptions: [],
       courseLists: [],
       courseLists: [],
       videoList: [],
       videoList: [],
-      //重粉记录的参数
-      log: {
-        open: false,
-        loading: true,
-        list: [],
-        total: 0,
-        detailExpanded: false,
-        currentRow: {},
-        // 模拟数据 - 关联销售
-        mockSales: [],
-        // 模拟数据 - 课程学习进度
-        mockCourses: [],
-        queryParams: {
-          pageNum: 1,
-          pageSize: 10,
-          externalUserId: null,
-          fsUserId: null,
-          projectId: null,
-          courseId: null,
-          videoId: null,
-        },
-      },
       statusDialog: {
       statusDialog: {
         open: false,
         open: false,
         title: "修改客户状态"
         title: "修改客户状态"
@@ -1449,7 +1289,8 @@ export default {
         level:null,
         level:null,
         levelType:null,
         levelType:null,
         companyUser:null,
         companyUser:null,
-        isRepeat: null
+        projectId: null,
+        userRepeat: null
       },
       },
 
 
       queryTagParams:{
       queryTagParams:{
@@ -1521,84 +1362,10 @@ export default {
 
 
   },
   },
   methods: {
   methods: {
-    /** 重粉查看操作 */
-    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();
-    },
-    handleQueryWatchLog() {
-      this.log.queryParams.pageNum = 1;
-      this.log.queryParams.pageSize = 10;
-      this.logList();
-    },
-    logList() {
-      // 只传 fsUserId 和 courseId,确保查询有效
-      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;
-      });
-    },
-    courseChange(row) {
-      this.log.queryParams.videoId = null;
-      if (row === '') {
-        this.videoList = [];
-        return
+    handleProjectChange() {
+      if (!this.queryParams.projectId) {
+        this.queryParams.userRepeat = null;
       }
       }
-      videoList(row).then(response => {
-        this.videoList = response.list
-      });
     },
     },
 
 
     handleMemberdetails(row){
     handleMemberdetails(row){
@@ -3027,248 +2794,3 @@ export default {
   margin-bottom: 20px;
   margin-bottom: 20px;
 }
 }
 </style>
 </style>
-
-<!-- 重粉看课历史弹窗样式(不能加 scoped,否则 custom-class 不生效) -->
-<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;
-  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;
-}
-.customer-info-card .customer-detail {
-  flex: 1;
-}
-.customer-info-card .customer-name-row {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  margin-bottom: 4px;
-}
-.customer-info-card .customer-name {
-  font-size: 16px;
-  font-weight: 600;
-  color: #303133;
-}
-.customer-info-card .customer-meta {
-  	color: #909399;
-  font-size: 12px;
-}
-
-/* 区块样式 */
-.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;
-}
-.section-title .el-tag {
-  margin-left: 4px;
-}
-
-/* 销售卡片 */
-.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;
-  transition: box-shadow 0.2s;
-}
-.sales-card:hover {
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
-}
-.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;
-  transition: box-shadow 0.2s;
-}
-.course-item:hover {
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
-}
-.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;
-  font-size: 12px;
-  font-weight: 600;
-}
-.course-name {
-  font-weight: 600;
-  color: #303133;
-  font-size: 14px;
-}
-.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-progress .progress-text b {
-  color: #303133;
-  font-size: 13px;
-}
-.course-latest {
-  display: flex;
-  align-items: center;
-  gap: 4px;
-  font-size: 12px;
-}
-.course-latest .latest-label {
-  color: #909399;
-}
-.course-latest .latest-section {
-  color: #409EFF;
-  font-weight: 500;
-}
-.course-latest .latest-time {
-  color: #c0c4cc;
-  margin-left: 8px;
-}
-.course-source {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-  font-size: 12px;
-  margin-top: 4px;
-}
-.course-source .source-item {
-  color: #909399;
-  display: inline-flex;
-  align-items: center;
-  gap: 4px;
-  line-height: 1;
-}
-.course-source .source-item i {
-  font-size: 13px;
-  line-height: 1;
-  vertical-align: middle;
-}
-.no-permission {
-  color: #c0c4cc;
-  font-style: italic;
-}
-
-/* 空提示 */
-.empty-tip {
-  text-align: center;
-  color: #c0c4cc;
-  padding: 20px 0;
-  font-size: 13px;
-}
-</style>