Quellcode durchsuchen

企微聊天 侧边栏客户详情、看课记录、课程管理

Long vor 1 Woche
Ursprung
Commit
5f387d43f9

+ 52 - 0
src/api/qw/im.js

@@ -91,6 +91,58 @@ export function exportMessage(query) {
   })
 }
 
+// 获取外部联系人详情
+export function getQwExternalContactDetails(query) {
+  return request({
+    url: '/qw/qwMsg/getQwExternalContactDetails',
+    method: 'get',
+    params: query
+  })
+}
+
+// 获取用户信息
+export function getQwUserInfo(query) {
+  return request({
+    url: '/qw/qwMsg/getQwUserInfo',
+    method: 'get',
+    params: query
+  })
+}
+
+// 修改用户信息
+export function updateQwUserInfo(data) {
+  return request({
+    url: '/qw/qwMsg/updateQwUserInfo',
+    method: 'post',
+    data: data
+  })
+}
 
+// 获取看课记录
+export function queryCourseWatchStatistics(data) {
+  return request({
+    url: '/qw/qwMsg/course/watch',
+    method: 'post',
+    data: data
+  })
+}
+
+// 获取课程列表
+export function getFsCourseListBySidebar(data) {
+  return request({
+    url: '/qw/qwMsg/getFsCourseListBySidebar',
+    method: 'post',
+    data: data
+  })
+}
+
+// 获取课程小节列表
+export function getFsCourseVideoListBySidebar(data) {
+  return request({
+    url: '/qw/qwMsg/getFsCourseVideoListBySidebar',
+    method: 'post',
+    data: data
+  })
+}
 
 

+ 0 - 9
src/api/users/user.js

@@ -88,12 +88,3 @@ export function enabledUsers(data) {
     data: data
   })
 }
-
-// 根据sessionId获取用户信息
-export function getUserInfoBySessionId(query) {
-  return request({
-    url: '/users/user/getUserInfoBySessionId',
-    method: 'get',
-    params: query
-  })
-}

BIN
src/assets/image/default-image.png


+ 1 - 2
src/components/LemonUI/components/message/voice.vue

@@ -29,8 +29,7 @@ export default {
         const {url, content} = JSON.parse(data.content)
         return <div class="lemon-message-voice-context">
             <audio block="true" showDuration='false' controls src={url}
-            controlslist='nodownload noplaybackrate'
-            oncontextmenu="return false"/>
+            controlslist='nodownload noplaybackrate'/>
             <div class="lemon-message-voice-context-text">
               <span>{content}</span>
               <span class="el-icon-check lemon-message-voice-context-text-transfer">转换完成</span>

+ 3 - 0
src/layout/index.vue

@@ -359,6 +359,9 @@ export default {
 .qw-im-content {
   width: 100%;
   height: 100%;
+  /* 可选:让内容区自动撑满弹窗 */
+  display: flex;
+  flex-direction: column;
 }
 
 </style>

+ 15 - 6
src/views/qw/qwChat/qq.vue

@@ -43,6 +43,11 @@
             />
           </div>
         </template>
+        <template #message-title>
+<!--          <div @click="openDrawer('right')" style="position: absolute;right: 14px;top: 10px;">-->
+<!--            <i class="el-icon-more" style="cursor: pointer"/>-->
+<!--          </div>-->
+        </template>
       </lemon-imui>
     </div>
 
@@ -65,7 +70,7 @@
     <el-drawer
       append-to-body
       :with-header="false"
-      size="75%"
+      size="35%"
       :title="detail.title" :visible.sync="detail.open">
       <userDetail ref="userDetail" />
     </el-drawer>
@@ -245,17 +250,17 @@ export default {
     openDrawer(position) {
       const IMUI = this.$refs.IMUI;
       const params = {
+        width: '30%',
         position,
         render: contact => {
           return (
             <div style="padding:15px">
               <h5>{contact.displayName}</h5>
-              <span style="cursor:pointer;" on-click={IMUI.closeDrawer}>关闭侧边栏</span>
             </div>
           );
         },
       };
-      IMUI.openDrawer(params);
+      IMUI.changeDrawer(params);
     },
     handlePullMessages(contact, next,instance) {
       const { IMUI } = this.$refs;
@@ -346,6 +351,8 @@ export default {
             conversation.lastContent = message.content;
             IMUI.topPopConversations(conversation);
             next();
+          } else {
+            next({status:'failed'})
           }
         });
       }
@@ -366,6 +373,8 @@ export default {
                 conversation.lastContent = message.content;
                 IMUI.topPopConversations(conversation);
                 next();
+              } else {
+                next({status:'failed'})
               }
             });
           }
@@ -400,7 +409,7 @@ export default {
     },
     handleMessageClick(e, key, message, instance) {
       if (key === 'avatar') {
-        this.qwUser.id !== message.fromUser.id && this.showDetail(message.toContactId)
+        this.qwUser.id !== message.fromUser.id && this.showDetail(message.extId)
         return
       }
 
@@ -506,10 +515,10 @@ export default {
       this.dialogVisible = true;
     },
     // 详情
-    showDetail(sessionId) {
+    showDetail(extId) {
       this.detail.open = true
       setTimeout(() => {
-        this.$refs.userDetail.getDetail(sessionId);
+        this.$refs.userDetail.getDetail(extId);
       }, 1);
     },
     // 搜索

+ 383 - 0
src/views/qw/qwChat/userDetail/courseManage.vue

@@ -0,0 +1,383 @@
+<template>
+  <div>
+    <!-- 顶部标题栏 -->
+    <div>
+      <div style="position: relative; display: flex; align-items: center; justify-content: center; height: 48px; background: #fff;">
+        <span style="position: absolute; left: 16px; cursor: pointer;" @click="handleBack">
+          <i class="el-icon-arrow-left" style="font-size: 20px; color: #333;"></i>
+        </span>
+        <span style="font-weight: bold; font-size: 16px;">课程管理</span>
+      </div>
+
+      <el-select
+        v-model="courseId"
+        placeholder="请点击选择课程"
+        class="courseSelect"
+        popper-class="courseSelectList"
+        @visible-change="handleSelectOpen"
+        @change="courseChange"
+        ref="courseSelect"
+      >
+        <el-option
+          v-for="item in courseList"
+          :key="item.courseId"
+          :label="item.courseName"
+          :value="item.courseId">
+        </el-option>
+        <div
+          v-if="courseQueryParams.hasNextPage"
+          class="load-more"
+          v-loading="courseQueryParams.loading"
+        >
+          <span v-if="!courseQueryParams.loading">加载更多</span>
+        </div>
+      </el-select>
+
+      <el-input
+        placeholder="请输入课程名称"
+        v-model="videoQueryParams.keyword"
+        class="videoSearch"
+        @keyup.enter.native="handleSearch"
+      >
+      </el-input>
+    </div>
+
+    <!-- 列表 -->
+    <div class="infinite-list-wrapper" style="overflow:auto">
+      <ul class="video-list" v-infinite-scroll="loadMoreVideo">
+        <li v-for="item in videoList" :key="item.videoId" class="video-item">
+          <div class="video-thumbnail">
+            <img :src="item.thumbnail || defaultImage" alt="" @error="handleImgError">
+            <div class="video-duration">{{formatDuration(item.duration)}}</div>
+          </div>
+          <div class="video-info">
+            <div class="video-title">{{item.title}}</div>
+            <div class="video-meta">
+              <span class="video-date">{{formatDate(item.createTime)}}</span>
+            </div>
+          </div>
+          <div class="video-actions">
+            <el-button type="primary" size="mini" round @click="handleShare(item)">分享课程</el-button>
+          </div>
+        </li>
+      </ul>
+      <div v-if="videoList.length === 0 && !videoQueryParams.loading" class="empty-tip">
+        <i class="el-icon-video-camera"></i>
+        <p>暂无视频</p>
+      </div>
+      <div v-if="videoQueryParams.loading" class="loading-more">
+        <i class="el-icon-loading"></i>
+        <span>加载中...</span>
+      </div>
+      <div v-if="!videoQueryParams.hasNextPage && videoList.length > 0" class="no-more">
+        <span>没有更多了</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import {getFsCourseListBySidebar, getFsCourseVideoListBySidebar} from "@/api/qw/im";
+
+export default {
+  name: "courseManage",
+  props: {
+    userId: {
+      type: Number,
+      default: () => null
+    }
+  },
+  data() {
+    return {
+      courseId: null,
+      courseQueryParams: {
+        extId: this.userId,
+        page: 1,
+        limit: 10,
+        hasNextPage: false,
+        loading: false,
+      },
+      courseList: [],
+      scrollEvent: null,
+      videoQueryParams: {
+        courseId: null,
+        keyword: '',
+        page: 1,
+        limit: 10,
+        hasNextPage: false,
+        loading: false,
+      },
+      videoList: [],
+      defaultImage: require('@/assets/image/default-image.png'),
+    }
+  },
+  created() {
+    this.getFsCourseListBySidebar()
+  },
+  methods: {
+    handleBack() {
+      this.$emit('back')
+    },
+    getFsCourseListBySidebar() {
+      this.courseQueryParams.loading = true;
+      getFsCourseListBySidebar(this.courseQueryParams).then(response => {
+        if (this.courseQueryParams.page === 1) {
+          this.courseList = response.data.list;
+        } else {
+          this.courseList = [...this.courseList, ...response.data.list];
+        }
+        this.courseQueryParams.hasNextPage = response.data.hasNextPage;
+        this.courseQueryParams.loading = false;
+      }).catch(() => {
+        this.courseQueryParams.loading = false;
+      });
+    },
+    loadMore() {
+      if (!this.courseQueryParams.hasNextPage || this.courseQueryParams.loading) return;
+
+      this.courseQueryParams.page += 1;
+      this.getFsCourseListBySidebar();
+    },
+    handleSelectOpen(visible) {
+      if (visible) {
+        // 下拉框打开时,添加滚动监听
+        this.$nextTick(() => {
+          const selectDropdown = document.querySelector('.el-select-dropdown.courseSelectList');
+          if (selectDropdown) {
+            const dropdownList = selectDropdown.querySelector('.el-select-dropdown__wrap');
+            if (dropdownList) {
+              // 移除之前可能存在的事件监听
+              if (this.scrollEvent) {
+                dropdownList.removeEventListener('scroll', this.scrollEvent);
+              }
+
+              // 添加新的滚动监听
+              this.scrollEvent = () => {
+                const { scrollTop, scrollHeight, clientHeight } = dropdownList;
+                // 当滚动到距离底部20px时,加载更多
+                if (scrollHeight - scrollTop - clientHeight < 20) {
+                  this.loadMore();
+                }
+              };
+
+              dropdownList.addEventListener('scroll', this.scrollEvent);
+            }
+          }
+        });
+      } else {
+        // 下拉框关闭时,移除滚动监听
+        if (this.scrollEvent) {
+          const selectDropdown = document.querySelector('.el-select-dropdown.courseSelectList');
+          if (selectDropdown) {
+            const dropdownList = selectDropdown.querySelector('.el-select-dropdown__wrap');
+            if (dropdownList) {
+              dropdownList.removeEventListener('scroll', this.scrollEvent);
+            }
+          }
+        }
+      }
+    },
+    courseChange(courseId) {
+      this.videoQueryParams.courseId = courseId;
+      this.handleSearch()
+    },
+    handleSearch() {
+      if (!this.videoQueryParams.courseId) {
+        return
+      }
+      this.videoQueryParams.page = 1;
+      this.getFsCourseVideoListBySidebar()
+    },
+    getFsCourseVideoListBySidebar() {
+      this.videoQueryParams.loading = true;
+      getFsCourseVideoListBySidebar(this.videoQueryParams).then(response => {
+        if (this.videoQueryParams.page === 1) {
+          this.videoList = response.data.list;
+        } else {
+          this.videoList = [...this.videoList, ...response.data.list];
+        }
+        this.videoQueryParams.hasNextPage = response.data.hasNextPage;
+        this.videoQueryParams.loading = false;
+        console.log(this.videoList)
+      }).catch(() => {
+        this.videoQueryParams.loading = false;
+      })
+    },
+    loadMoreVideo() {
+      if (!this.videoQueryParams.hasNextPage || this.videoQueryParams.loading) return;
+      this.videoQueryParams.page += 1;
+      this.getFsCourseVideoListBySidebar()
+    },
+    handleImgError(e) {
+      e.target.src = this.defaultImage;
+    },
+    formatDate(timestamp) {
+      if (!timestamp) return '';
+      const date = new Date(timestamp);
+      return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
+    },
+    formatDuration(seconds) {
+      if (!seconds) return '00:00';
+      const minutes = Math.floor(seconds / 60);
+      const remainingSeconds = seconds % 60;
+      return `${String(minutes).padStart(2, '0')}:${String(remainingSeconds).padStart(2, '0')}`;
+    },
+    handleShare(item) {
+      console.log('分享', item)
+    }
+  }
+}
+
+</script>
+
+<style scoped>
+.courseSelect {
+  width: 100%;
+}
+.courseSelectList li {
+  text-align: center;
+}
+::v-deep .courseSelect input {
+  border: unset;
+  text-align: center;
+}
+.load-more {
+  text-align: center;
+  height: 34px;
+  line-height: 34px;
+  color: #909399;
+  font-size: 12px;
+  cursor: pointer;
+  background-color: #f5f7fa;
+}
+.load-more:hover {
+  background-color: #e4e7ed;
+}
+.videoSearch {
+  padding: 10px;
+  border-radius: 8px;
+}
+::v-deep .videoSearch input {
+  border-radius: 22px;
+}
+
+.infinite-list-wrapper {
+  height: calc(100vh - 200px);
+  overflow-y: auto;
+  padding: 0 10px;
+  background-color: #f5f7fa;
+}
+
+.video-list {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+}
+
+.video-item {
+  display: flex;
+  padding: 15px;
+  margin-bottom: 10px;
+  background-color: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+  transition: transform 0.2s;
+}
+
+.video-item:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+.video-thumbnail {
+  position: relative;
+  flex: 0 0 120px;
+  height: 80px;
+  margin-right: 15px;
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.video-thumbnail img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.video-duration {
+  position: absolute;
+  bottom: 5px;
+  right: 5px;
+  background-color: rgba(0, 0, 0, 0.7);
+  color: #fff;
+  padding: 2px 4px;
+  border-radius: 2px;
+  font-size: 12px;
+}
+
+.video-info {
+  flex: 1;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-around;
+}
+
+.video-title {
+  font-size: 16px;
+  font-weight: 500;
+  color: #303133;
+  margin-bottom: 5px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.video-meta {
+  display: flex;
+  align-items: center;
+  font-size: 12px;
+  color: #909399;
+  margin-bottom: 5px;
+}
+
+.video-date {
+  margin-right: 15px;
+}
+
+.video-actions {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  gap: 8px;
+  margin-left: 10px;
+}
+
+.loading-more {
+  text-align: center;
+  padding: 15px 0;
+  color: #909399;
+}
+
+.no-more {
+  text-align: center;
+  padding: 15px 0;
+  color: #909399;
+  font-size: 13px;
+}
+
+.empty-tip {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 40px 0;
+  color: #909399;
+}
+
+.empty-tip i {
+  font-size: 48px;
+  margin-bottom: 10px;
+  color: #dcdfe6;
+}
+</style>

+ 341 - 108
src/views/qw/qwChat/userDetail/index.vue

@@ -1,148 +1,381 @@
 <template>
+  <div style="background-color: #f0f2f5; padding-bottom: 20px; min-height: 100%;">
+    <!-- 客户详情 -->
+    <div v-if="showDetail">
+      <div style="padding: 20px; background-color: #fff;">
+        客户详情
+      </div>
 
-  <div style="background-color: #f0f2f5; padding-bottom: 20px; min-height: 100%; " >
-    <div style="padding: 20px; background-color: #fff;">
-      用户详情
-    </div>
+      <!-- 用户头像 -->
+      <div style="padding: 20px 10px 0 10px;background-image: linear-gradient(to right, #e0edff, #dfe0fe)">
+        <div style="padding: 20px 10px 0 10px;background-image: linear-gradient(to right, #edf5ff, #ecedff);display: flex;border-radius: 8px;">
+          <div style="padding: 10px">
+            <el-avatar :size="50" :src="qwUserInfo.avatar"/>
+          </div>
+          <div style="padding: 10px;display: flex;flex-direction: column;align-items: flex-start;justify-content: center;">
+            <div>
+              <span style="font-size: 16px;">{{qwUserInfo.name}}</span>
+            </div>
+            <div>
+              <span style="font-size: 14px;color: #8c939d">备注:{{qwUserInfo.remark}}</span>
+            </div>
+          </div>
+        </div>
+      </div>
 
-    <div class="contentx">
-      <div class="desct">
-        基本信息
+      <!--  基本信息 -->
+      <div>
+        <div style="display: flex;justify-content: space-between;align-items: center;flex-direction: row;background: #FFF;padding: 10px">
+          <div>
+            基本信息
+          </div>
+          <div>
+            <el-button size="mini" round @click="updateQwDetail">修改用户信息</el-button>
+          </div>
+        </div>
+        <div style="background: #FFF; padding: 0 10px 10px 20px;color: #8c939d">
+          <el-row>
+            <span style="font-size: 14px;">性别:</span>
+            <span style="font-size: 14px;margin-left: 10px">{{qwUserDetail.sex || '未知'}}</span>
+          </el-row>
+          <el-row style="margin-top: 10px">
+            <span style="font-size: 14px;">年龄:</span>
+            <span style="font-size: 14px;margin-left: 10px">{{qwUserDetail.age || '未知'}}</span>
+          </el-row>
+          <el-row style="margin-top: 10px">
+            <span style="font-size: 14px;">行为习惯:</span>
+            <span style="font-size: 14px;margin-left: 10px">{{qwUserDetail.habits || '无'}}</span>
+          </el-row>
+          <el-row style="margin-top: 10px">
+            <span style="font-size: 14px;">患病时间:</span>
+            <span style="font-size: 14px;margin-left: 10px">{{qwUserDetail.illnessTime || '无'}}</span>
+          </el-row>
+          <el-row style="margin-top: 10px">
+            <span style="font-size: 14px;">疾病:</span>
+            <span style="font-size: 14px;margin-left: 10px">{{qwUserDetail.disease || '无'}}</span>
+          </el-row>
+          <el-row style="margin-top: 10px">
+            <span style="font-size: 14px;">家人的疾病:</span>
+            <span style="font-size: 14px;margin-left: 10px">{{qwUserDetail.familyDisease || '无'}}</span>
+          </el-row>
+          <el-row style="margin-top: 10px">
+            <span style="font-size: 14px;">是否线下就诊:</span>
+            <span style="font-size: 14px;margin-left: 10px">{{qwUserDetail.isLine || '无'}}</span>
+          </el-row>
+          <el-row style="margin-top: 10px">
+            <span style="font-size: 14px;">体质:</span>
+            <span style="font-size: 14px;margin-left: 10px">{{qwUserDetail.constitution || '无'}}</span>
+          </el-row>
+          <el-row style="margin-top: 10px">
+            <span style="font-size: 14px;">使用药品:</span>
+            <span style="font-size: 14px;margin-left: 10px">{{qwUserDetail.medicine || '无'}}</span>
+          </el-row>
+          <el-row style="margin-top: 10px">
+            <span style="font-size: 14px;">咨询产品:</span>
+            <span style="font-size: 14px;margin-left: 10px">{{qwUserDetail.consultProduct || '无'}}</span>
+          </el-row>
+          <el-row style="margin-top: 10px">
+            <span style="font-size: 14px;">是否已经购买产品:</span>
+            <span style="font-size: 14px;margin-left: 10px">{{qwUserDetail.isBuy || '无'}}</span>
+          </el-row>
+        </div>
       </div>
-      <el-descriptions title="" :column="3" border>
-
-        <el-descriptions-item label="会员id" >
-          <span>{{item.userId}}</span>
-        </el-descriptions-item>
-        <el-descriptions-item label="用户昵称" >
-          <span>{{item.nickname}}</span>
-        </el-descriptions-item>
-        <el-descriptions-item label="用户头像" >
-          <el-image v-if="item.avatar!=null"
-                    style="width: 50px;"
-                    :src="item.avatar">
-          </el-image>
-        </el-descriptions-item>
-        <el-descriptions-item label="手机号码" >
-          <span>{{item.phone}}</span>
-        </el-descriptions-item>
-        <el-descriptions-item label="用户积分" >
-          <span>{{item.integral}}</span>
-        </el-descriptions-item>
-        <el-descriptions-item label="状态" >
-             <span>
-                   <dict-tag :options="userOptions" :value="item.status"/>
-             </span>
-        </el-descriptions-item>
-
-        <el-descriptions-item label="上级昵称" >
-          <span>{{item.tuiName}}</span>
-        </el-descriptions-item>
-        <el-descriptions-item label="上级手机号码" >
-          <span>{{item.tuiPhone}}</span>
-        </el-descriptions-item>
-        <el-descriptions-item label="推广员关联时间" >
-          <span>{{item.tuiTime}}</span>
-        </el-descriptions-item>
-        <el-descriptions-item label="下级人数" >
-          <span>{{item.tuiUserCount}}</span>
-        </el-descriptions-item>
-        <el-descriptions-item label="最后一次登录ip" >
-          <span>{{item.lastIp}}</span>
-        </el-descriptions-item>
-        <el-descriptions-item label="余额" >
-          <span>{{item.balance}}</span>
-        </el-descriptions-item>
-        <el-descriptions-item label="创建时间" >
-          <span>{{item.createTime}}</span>
-        </el-descriptions-item>
-
-        <el-descriptions-item label="更新时间" >
-          <span>{{item.updateTime}}</span>
-        </el-descriptions-item>
-      </el-descriptions>
-    </div>
 
-    <div class="contentx" v-if="item.userId != null" >
-      <div class="desct">
-        用户订单
+      <!-- 看课记录 -->
+      <div style="background-color: #FFF">
+        <div style="padding: 10px;">
+          看课记录
+        </div>
+        <!-- 状态 -->
+        <div class="status-container">
+          <div
+            v-for="(item, index) in logTypeList"
+            :key="index"
+            class="status-item"
+          >
+            <div
+              class="status-color-block"
+              :style="{ backgroundColor: item.color }"
+            ></div>
+            <span class="status-text">{{ item.text }}</span>
+          </div>
+        </div>
+        <!-- 近七天看课记录 -->
+        <div>
+          <div class="section-title">
+            近七天看课记录
+          </div>
+          <div class="course-record-container">
+            <div
+              v-for="item in courseWatch7day"
+              :key="item.id"
+              class="course-record-item"
+            >
+              <span class="course-date">{{ item.date.split('-')[2] }}日</span>
+              <div
+                class="course-status-block"
+                :style="{ backgroundColor: getLogTypeColor(item.logType) }"
+                :title="getLogTypeText(item.logType)"
+              ></div>
+            </div>
+            <div v-if="courseWatch7day.length === 0" class="empty-record">
+              暂无看课记录
+            </div>
+          </div>
+        </div>
       </div>
-      <userStorerDetails  ref="userDetails" />
-    </div>
 
-    <div class="contentx" v-if="item.userId != null" >
-      <div class="desct">
-        用户看客记录
+      <!-- 按钮 -->
+      <div>
+        <div style="display: flex;justify-content: space-evenly;align-items: center;flex-direction: row;background: #FFF;padding: 10px 0 20px 0">
+          <div>
+            <el-button style="width: 150px;height: 35px;background-color: #1890ff;color: #fff;border: none;
+              border-radius: 22px;
+              font-size: 12px;
+              cursor: pointer;
+              transition: background 0.2s;"
+              @click="courseManage">课程管理</el-button>
+          </div>
+          <div>
+            <el-button style="width: 150px;height: 35px;background-color: #1890ff;color: #fff;border: none;
+              border-radius: 22px;
+              font-size: 12px;
+              cursor: pointer;
+              transition: background 0.2s;"
+              @click="remindCourseManage">催课管理</el-button>
+          </div>
+        </div>
       </div>
-      <userCourseWatchLog  ref="userWatchLog" />
     </div>
+
+    <!-- 修改用户信息 -->
+    <user-info-edit
+      v-if="showUpdate"
+      :userDetail="qwUserDetail"
+      @back="handleBack"
+      @save-success="handleSaveSuccess"
+    />
+
+    <!-- 课程管理 -->
+    <course-manage
+      v-if="showCourseManage"
+      :userId="qwUserInfo.id"
+      @back="handleBack"
+    />
+    <!-- 催课管理 -->
+    <remind-course-manage
+      v-if="showRemindCourseManage"
+      :user-id="qwUserDetail.id"
+      @back="handleBack"
+    />
+
+<!--    <div class="contentx" v-if="item.userId != null" >-->
+<!--      <div class="desct">-->
+<!--        用户订单-->
+<!--      </div>-->
+<!--      <userStorerDetails  ref="userDetails" />-->
+<!--    </div>-->
+
+<!--    <div class="contentx" v-if="item.userId != null" >-->
+<!--      <div class="desct">-->
+<!--        用户看客记录-->
+<!--      </div>-->
+<!--      <userCourseWatchLog  ref="userWatchLog" />-->
+<!--    </div>-->
   </div>
 </template>
 
 <script>
-import {getUserInfoBySessionId} from "@/api/users/user";
 import userStorerDetails from "@/views/qw/qwChat/userDetail/userStorerDetails.vue"
 import userCourseWatchLog from "@/views/qw/qwChat/userDetail/userCourseWatchLog.vue";
+import userInfoEdit from "@/views/qw/qwChat/userDetail/userInfoEdit.vue";
+import courseManage from "@/views/qw/qwChat/userDetail/courseManage.vue";
+import remindCourseManage from "@/views/qw/qwChat/userDetail/remindCourseManage.vue";
+import {getQwExternalContactDetails, getQwUserInfo, queryCourseWatchStatistics} from "@/api/qw/im";
 
 export default {
   name: "userDetail",
   components: {
     userStorerDetails,
-    userCourseWatchLog
+    userCourseWatchLog,
+    userInfoEdit,
+    courseManage,
+    remindCourseManage
   },
   data() {
+    // 定义颜色常量
+    const logTypeColors = {
+      notWatched: '#909399', // 未看课
+      watching: '#0bc6ff',   // 看课中
+      completed: '#67c23a',  // 完课
+      pending: '#f55a4f',    // 待看课
+      interrupted: '#ffd700' // 看课中断
+    };
+
     return {
-      userOptions: [],
-      item: {},
+      extId: null,
+      qwUserInfo: {},
+      qwUserDetail: {},
+      showDetail: true,
+      showUpdate: false,
+      courseWatch7day: [],
+      logTypeColors,
+      // 定义状态列表用于渲染
+      logTypeList: [
+        { type: 0, text: '未看课', color: logTypeColors.notWatched },
+        { type: 1, text: '看课中', color: logTypeColors.watching },
+        { type: 2, text: '完课', color: logTypeColors.completed },
+        { type: 3, text: '待看课', color: logTypeColors.pending },
+        { type: 4, text: '看课中断', color: logTypeColors.interrupted }
+      ],
+      showCourseManage: false,
+      showRemindCourseManage: false
     }
   },
-  created() {
-    this.getDicts("sys_user_status").then(response => {
-      this.userOptions = response.data;
-    });
-  },
   methods: {
-    getDetail(sessionId) {
-      this.item = {}
-      const params = {
-        sessionId: sessionId
+    getDetail(extId) {
+      this.extId = extId
+      this.getQwExternalContactDetails()
+      this.getQwUserInfo()
+      this.queryCourseWatchStatistics()
+    },
+    queryCourseWatchStatistics() {
+      const data = {
+        type: 0,
+        qwExternalContactId: this.extId
+      }
+      queryCourseWatchStatistics(data).then(response => {
+        this.courseWatch7day = response.data.data
+      })
+    },
+    getQwExternalContactDetails() {
+      const query = {
+        qwExternalContactId: this.extId
       }
-      getUserInfoBySessionId(params).then(response => {
-        this.item = response.data;
-        if (this.item.userId != null) {
-            setTimeout(() => {
-              this.$refs.userDetails.getUserDetails(this.item.userId);
-            }, 1);
-          setTimeout(() => {
-            this.$refs.userWatchLog.getUserWatchLog(this.item.userId);
-          }, 1);
-        }
+      getQwExternalContactDetails(query).then(response => {
+        this.qwUserInfo = response.data
       })
     },
+    getQwUserInfo() {
+      const query = {
+        qwExternalContactId: this.extId
+      }
+      getQwUserInfo(query).then(response => {
+        this.qwUserDetail = response.moreInfo
+      })
+    },
+    updateQwDetail() {
+      this.showDetail = false
+      this.showUpdate = true
+    },
+    handleBack() {
+      this.getQwUserInfo()
+      this.showDetail = true
+      this.showUpdate = false
+      this.showCourseManage = false
+      this.showRemindCourseManage = false
+    },
+    handleSaveSuccess() {
+      this.handleBack()
+    },
+    getLogTypeColor(logType) {
+      const type = parseInt(logType);
+      // 使用对象映射替代switch语句
+      const colorMap = {
+        0: this.logTypeColors.notWatched,
+        1: this.logTypeColors.watching,
+        2: this.logTypeColors.completed,
+        3: this.logTypeColors.pending,
+        4: this.logTypeColors.interrupted
+      };
+      return colorMap[type] || '';
+    },
+    getLogTypeText(logType) {
+      const type = parseInt(logType);
+      // 使用对象映射替代switch语句
+      const textMap = {
+        0: '未看课',
+        1: '看课中',
+        2: '完课',
+        3: '待看课',
+        4: '看课中断'
+      };
+      return textMap[type] || '';
+    },
+    courseManage() {
+      this.showDetail = false
+      this.showCourseManage = true
+    },
+    remindCourseManage() {
+      this.showDetail  = false
+      this.showRemindCourseManage = true
+    }
   }
 }
 </script>
-<style>
+<style scoped>
+.status-container {
+  display: flex;
+  gap: 20px;
+  padding: 10px 0 0 20px;
+  color: #8c939d;
+}
+
+.status-item {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+}
 
-.contentx{
-  height: 100%;
-  background-color: #fff;
-  padding: 0px 20px 20px;
+.status-color-block {
+  border-radius: 4px;
+  width: 20px;
+  height: 20px;
+}
 
+.status-text {
+  font-size: 12px;
+}
 
-  margin: 20px;
+.section-title {
+  padding: 10px;
+  font-size: 12px;
+  color: #8c939d;
 }
-.el-descriptions-item__label.is-bordered-label{
-  font-weight: normal;
+
+.course-record-container {
+  display: flex;
+  padding: 0 10px 15px 10px;
+  gap: 10px;
+  overflow-x: auto;
 }
-.el-descriptions-item__content {
-  max-width: 150px;
-  min-width: 100px;
+
+.course-record-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  min-width: 40px;
 }
-.desct{
-  padding-top: 20px;
-  padding-bottom: 20px;
-  color: #524b4a;
-  font-weight: bold;
+
+.course-date {
+  font-size: 12px;
+  color: #606266;
+  margin-bottom: 5px;
+}
+
+.course-status-block {
+  width: 20px;
+  height: 20px;
+  border-radius: 4px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.empty-record {
+  width: 100%;
+  text-align: center;
+  color: #909399;
+  font-size: 14px;
 }
 </style>

+ 28 - 0
src/views/qw/qwChat/userDetail/remindCourseManage.vue

@@ -0,0 +1,28 @@
+<template>
+  <div>
+    <!-- 顶部标题栏 -->
+    <div>
+      <div style="position: relative; display: flex; align-items: center; justify-content: center; height: 48px; background: #fff; border-bottom: 1px solid #eee;">
+        <span style="position: absolute; left: 16px; cursor: pointer;" @click="handleBack">
+          <i class="el-icon-arrow-left" style="font-size: 20px; color: #333;"></i>
+        </span>
+        <span style="font-weight: bold; font-size: 16px;">催课面板</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "remindCourseManage",
+  methods: {
+    handleBack() {
+      this.$emit("back");
+    }
+  }
+};
+</script>
+
+<style scoped>
+
+</style>

+ 250 - 0
src/views/qw/qwChat/userDetail/userInfoEdit.vue

@@ -0,0 +1,250 @@
+<template>
+  <div>
+    <!-- 顶部标题栏 -->
+    <div>
+      <div style="position: relative; display: flex; align-items: center; justify-content: center; height: 48px; background: #fff; border-bottom: 1px solid #eee;">
+        <span style="position: absolute; left: 16px; cursor: pointer;" @click="handleBack">
+          <i class="el-icon-arrow-left" style="font-size: 20px; color: #333;"></i>
+        </span>
+        <span style="font-weight: bold; font-size: 16px;">修改用户信息</span>
+      </div>
+    </div>
+    <!-- 表单内容 -->
+    <div class="user-form">
+      <div class="form-row">
+        <div class="form-label">姓名</div>
+        <div class="form-input">
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-label">性别</div>
+        <div class="form-input">
+          <input
+            v-model="formData.sex"
+            placeholder="请输入性别"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-label">年龄</div>
+        <div class="form-input">
+          <input
+            v-model="formData.age"
+            placeholder="请输入年龄"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-label">行为习惯</div>
+        <div class="form-input">
+          <input
+            v-model="formData.habits"
+            placeholder="请输入行为习惯"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-label">患病时间</div>
+        <div class="form-input">
+          <input
+            v-model="formData.illnessTime"
+            placeholder="请输入患病时间"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-label">疾病</div>
+        <div class="form-input">
+          <input
+            v-model="formData.disease"
+            placeholder="请输入疾病"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-label">家人的疾病</div>
+        <div class="form-input">
+          <input
+            v-model="formData.familyDisease"
+            placeholder="请输入家人的疾病"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-label">是否线下就诊</div>
+        <div class="form-input">
+          <input
+            v-model="formData.isLine"
+            placeholder="请输入是否线下就诊"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-label">体质</div>
+        <div class="form-input">
+          <input
+            v-model="formData.constitution"
+            placeholder="请输入体质"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-label">使用药品</div>
+        <div class="form-input">
+          <input
+            v-model="formData.medicine"
+            placeholder="请输入使用药品"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-label">咨询产品</div>
+        <div class="form-input">
+          <input
+            v-model="formData.consultProduct"
+            placeholder="请输入咨询产品"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-label">是否已经购买产品</div>
+        <div class="form-input">
+          <input
+            v-model="formData.isBuy"
+            placeholder="请输入是否已经购买产品"
+            class="input"
+            type="text"
+          />
+        </div>
+      </div>
+    </div>
+    <!-- 底部按钮 -->
+    <div class="user-edit-footer">
+      <button class="save-btn" v-loading="loading" :disabled="loading" @click="handleSave">保存修改</button>
+    </div>
+  </div>
+</template>
+
+<script>
+import {updateQwUserInfo} from "@/api/qw/im";
+
+export default {
+  name: "UserInfoEdit",
+  props: {
+    // 用户详情数据
+    userDetail: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  data() {
+    return {
+      // 表单数据
+      formData: {},
+      // 加载状态
+      loading: false
+    }
+  },
+  created() {
+    // 初始化表单数据
+    this.formData = Object.assign({}, this.userDetail);
+  },
+  methods: {
+    // 返回
+    handleBack() {
+      this.$emit('back');
+    },
+    // 保存修改
+    handleSave() {
+      this.loading = true;
+      updateQwUserInfo(this.formData).then(response => {
+        if (response.code === 200) {
+          this.loading = false;
+          this.$message.success("修改成功");
+          this.$emit('save-success');
+        } else {
+          this.$message.error(response.msg || "修改失败");
+          this.loading = false;
+        }
+      }).catch(() => {
+        this.loading = false;
+      });
+    }
+  }
+}
+</script>
+
+<style scoped>
+.user-form {
+  margin-top: 10px;
+  background: #fff;
+}
+.form-row {
+  display: flex;
+  align-items: center;
+  border-bottom: 1px solid #f2f2f2;
+  padding: 0 16px;
+  height: 44px;
+}
+.form-label {
+  flex: 0 0 150px;
+  color: #333;
+  font-size: 15px;
+}
+.form-input {
+  flex: 1;
+  text-align: right;
+}
+.input {
+  border: none;
+  outline: none;
+  background: transparent;
+  font-size: 15px;
+  color: #999;
+  width: 100%;
+  text-align: right;
+}
+.input::placeholder {
+  color: #ccc;
+}
+.user-edit-footer {
+  width: 100%;
+  padding: 10px;
+  background: #fff;
+  border-top: 1px solid #f2f2f2;
+}
+.save-btn {
+  width: 100%;
+  height: 35px;
+  background: #1890ff;
+  color: #fff;
+  border: none;
+  border-radius: 22px;
+  font-size: 12px;
+  cursor: pointer;
+  transition: background 0.2s;
+}
+.save-btn:hover {
+  background: #40a9ff;
+}
+</style>