Просмотр исходного кода

Merge remote-tracking branch 'origin/master'

三七 1 день назад
Родитель
Сommit
1deb8a0343

+ 3 - 3
src/api/qw/companySession.js

@@ -21,12 +21,12 @@ export function qwSignature({ url, corpid }) {
   });
 }
 
-// 获取会话记录
-export function qwConversations({ customerId, staffUserId, limit, cursor, corpid }) {
+// 获取会话记录(支持单聊和群聊)
+export function qwConversations({ customerId, staffUserId, limit, cursor, corpid, chatId }) {
   return request({
     url: '/weChatSpace/conversations',
     method: 'get',
-    params: { customerId, staffUserId, limit, cursor, corpid }
+    params: { customerId, staffUserId, limit, cursor, corpid, chatId }
   });
 }
 

+ 47 - 7
src/views/live/liveConfig/liveRedConf.vue

@@ -197,17 +197,41 @@
             placeholder="请输入口令,最多30字"
           />
         </el-form-item>
-        <el-form-item :label="showPasswordFields ? '持续时间' : '持续时间'" prop="duration">
-          <el-input v-model="form.duration" :placeholder="showPasswordFields ? '请输入持续时间(分)' : '请输入红包有效时间 单位:分'" />
+        <el-form-item :label="showPasswordFields ? '持续时间(分)' : '持续时间(分)'" prop="duration">
+          <el-input-number
+            v-model="form.duration"
+            :min="1"
+            :precision="0"
+            :step="1"
+            :controls="true"
+            style="width: 100%"
+            :placeholder="showPasswordFields ? '请输入持续时间(分)' : '请输入红包有效时间 单位:分'"
+          />
         </el-form-item>
         <el-form-item label="直播间ID" prop="liveId">
           <el-input v-model="form.liveId" placeholder="请输入直播间ID" :disabled="canLiveId"/>
         </el-form-item>
         <el-form-item :label="showPasswordFields ? '奖励积分' : '积分'" prop="redNum">
-          <el-input v-model="form.redNum" :placeholder="showPasswordFields ? '请输入奖励积分' : '请输入积分数量'" />
+          <el-input-number
+            v-model="form.redNum"
+            :min="1"
+            :precision="0"
+            :step="1"
+            :controls="true"
+            style="width: 100%"
+            :placeholder="showPasswordFields ? '请输入奖励积分' : '请输入积分数量'"
+          />
         </el-form-item>
         <el-form-item :label="showPasswordFields ? '奖励份数' : '中奖份量'" prop="totalLots">
-          <el-input v-model="form.totalLots" :placeholder="showPasswordFields ? '请输入奖励份数' : '请输入可中奖份量'" />
+          <el-input-number
+            v-model="form.totalLots"
+            :min="1"
+            :precision="0"
+            :step="1"
+            :controls="true"
+            style="width: 100%"
+            :placeholder="showPasswordFields ? '请输入奖励份数' : '请输入可中奖份量'"
+          />
         </el-form-item>
         <el-form-item :label="showPasswordFields ? '标题' : '红包标题'" prop="desc">
           <el-input v-model="form.desc" :placeholder="showPasswordFields ? '请输入标题' : '请输入红包描述'" />
@@ -246,6 +270,19 @@ export default {
         callback()
       }
     }
+    /** 正整数:禁止空、0、负数、小数 */
+    const validatePositiveInt = (label) => (rule, value, callback) => {
+      if (value === null || value === undefined || value === '') {
+        callback(new Error(label + '不能为空'))
+        return
+      }
+      const str = String(value).trim()
+      if (!/^[1-9]\d*$/.test(str)) {
+        callback(new Error(label + '须为正整数(不能为0、负数或小数)'))
+        return
+      }
+      callback()
+    }
     return {
       //字典
       redStatusOptions: [],
@@ -301,16 +338,19 @@ export default {
           { validator: validatePassword, trigger: "blur" }
         ],
         duration: [
-          { required: true, message: "有效时间 单位:分不能为空", trigger: "blur" }
+          { required: true, message: "持续时间不能为空", trigger: "blur" },
+          { validator: validatePositiveInt('持续时间'), trigger: ['blur', 'change'] }
         ],
         liveId: [
           { required: true, message: "直播间ID不能为空", trigger: "blur" }
         ],
         redNum: [
-          { required: true, message: "积分数量不能为空", trigger: "blur" }
+          { required: true, message: "奖励积分不能为空", trigger: "blur" },
+          { validator: validatePositiveInt('奖励积分'), trigger: ['blur', 'change'] }
         ],
         totalLots: [
-          { required: true, message: "份量不能为空", trigger: "blur" }
+          { required: true, message: "奖励份数不能为空", trigger: "blur" },
+          { validator: validatePositiveInt('奖励份数'), trigger: ['blur', 'change'] }
         ],
         desc: [
           { required: true, message: "描述不能为空", trigger: "blur" }

+ 18 - 6
src/views/qw/conversationNew/ContentSearchTab.vue

@@ -39,6 +39,7 @@
         stripe
         style="width: 100%"
       >
+        <el-table-column prop="chatTypeName" label="聊天类型" width="80" />
         <el-table-column prop="senderTypeName" label="发送人类型" width="100" />
         <el-table-column prop="senderName" label="发送人" min-width="150" />
         <el-table-column prop="receiverNames" label="接收人" min-width="200">
@@ -46,7 +47,7 @@
             {{ scope.row.receiverNames.join('、') }}
           </template>
         </el-table-column>
-        <el-table-column prop="sendTimeStr" label="发送时间" width="180" />
+        <el-table-column prop="send_time_str" label="发送时间" width="180" />
         <el-table-column prop="msgTypeName" label="消息类型" width="100" />
         <el-table-column label="聊天内容" min-width="200">
           <template slot-scope="scope">
@@ -78,6 +79,7 @@
           :corpId="corpId"
           :customerId="currentCustomerId"
           :staffUserId="currentStaffUserId"
+          :chatId="currentChatId"
           @logout="handleLogout"
         />
       </div>
@@ -146,6 +148,7 @@ export default {
       drawerVisible: false,
       currentStaffUserId: null,
       currentCustomerId: null,
+      currentChatId: null,
       pickerOptions: {
         disabledDate(time) {
           // 只能选择最近31天内的日期
@@ -275,12 +278,14 @@ export default {
         else if (msg.sender.type === 3) senderTypeName = '机器人';
         else senderTypeName = '未知';
 
-        const senderName = msg.sender.id;
+        // 优先使用后端返回的 name,查不到则用原始 ID 兜底
+        const senderName = msg.sender.name || msg.sender.id;
         const receiverNames = (msg.receiver_list || []).map(r => {
-          if (r.type === 1) return `员工:${r.id}`;
-          if (r.type === 2) return `客户:${r.id}`;
-          return r.id;
+          const label = r.type === 1 ? '员工' : r.type === 2 ? '客户' : '';
+          const name = r.name || r.id;
+          return label ? `${label}:${name}` : name;
         });
+        const chatTypeName = msg.chatType === 1 ? '单聊' : '群聊';
         const msgTypeName = MSG_TYPE_MAP[msg.msgtype] || '未知类型';
         let contentPreview = '';
         if (msg.msgtype === 1) {
@@ -288,6 +293,7 @@ export default {
         }
         return {
           ...msg,
+          chatTypeName,
           senderTypeName,
           senderName,
           receiverNames,
@@ -321,10 +327,15 @@ export default {
     },
 
     openContextDrawer(row) {
+      // 群聊:传 chatId 查看整个群的消息
       if (row.chatid) {
-        this.$message.info("暂不支持群聊上下文查看");
+        this.currentStaffUserId = null;
+        this.currentCustomerId = 'chat_' + row.chatid; // chat_ 前缀在 fetchConversations 中识别
+        this.currentChatId = row.chatid;
+        this.drawerVisible = true;
         return;
       }
+      // 单聊:传员工+客户
       let staffId = null, customerId = null;
       if (row.sender.type === 1) staffId = row.sender.id;
       if (row.sender.type === 2) customerId = row.sender.id;
@@ -335,6 +346,7 @@ export default {
       if (staffId && customerId) {
         this.currentStaffUserId = staffId;
         this.currentCustomerId = customerId;
+        this.currentChatId = null;
         this.drawerVisible = true;
       } else {
         this.$message.warning("无法确定会话双方");

+ 116 - 32
src/views/qw/conversationNew/ConversationPanelPure.vue

@@ -15,13 +15,13 @@
       <div class="login-tip">请扫码登录企微</div>
       <div id="login-container" ref="loginContainer" class="login-container"></div>
     </div>
-    <!-- 已登录但未选择员工 -->
-    <div v-else-if="!staffUserId" class="empty-tip">
+    <!-- 已登录但未选择员工(群聊不需要员工) -->
+    <div v-else-if="!staffUserId && !chatId" class="empty-tip">
       <i class="el-icon-info"></i>
       <p>请选择员工</p>
     </div>
-    <!-- 已登录但未选择客户 -->
-    <div v-else-if="!customerId" class="empty-tip">
+    <!-- 已登录但未选择客户(群聊不需要客户) -->
+    <div v-else-if="!customerId && !chatId" class="empty-tip">
       <i class="el-icon-info"></i>
       <p>请选择客户查看会话</p>
     </div>
@@ -42,7 +42,6 @@
 
 <script>
 import * as ww from '@wecom/jssdk';
-import defaultStaffAvatar from '@/assets/images/user.png';
 import { qwLogin, qwSignature, qwConversations, getQwSessionConfig } from '@/api/qw/companySession';
 
 // ========== 全局配置缓存 ==========
@@ -70,8 +69,8 @@ export default {
   props: {
     corpId: { type: String, default: null },
     customerId: { type: String, default: null },
-    customerAvatar: { type: String, default: '' },
-    staffUserId: { type: String, default: null }
+    staffUserId: { type: String, default: null },
+    chatId: { type: String, default: null }
   },
   data() {
     return {
@@ -83,7 +82,6 @@ export default {
       loadingMore: false,
       chatInstance: null,
       _sdkInited: false,
-      defaultStaffAvatar: defaultStaffAvatar,
       config: { corpid: '', agentid: '', domain: '' },
       configReady: false,
       configError: false,
@@ -97,7 +95,7 @@ export default {
   },
   watch: {
     customerId(newId, oldId) {
-      if (newId && newId !== oldId && this.isLoggedIn && this.staffUserId && this.configReady && !this.configError) {
+      if (newId && newId !== oldId && this.isLoggedIn && (this.staffUserId || this.chatId) && this.configReady && !this.configError) {
         this.resetAndReload();
       }
     },
@@ -106,6 +104,11 @@ export default {
         this.resetAndReload();
       }
     },
+    chatId(newVal, oldVal) {
+      if (newVal && newVal !== oldVal && this.isLoggedIn && this.configReady && !this.configError) {
+        this.resetAndReload();
+      }
+    },
     corpId(newId, oldId) {
       if (newId !== oldId) {
         this.destroyChat();
@@ -137,11 +140,6 @@ export default {
         }
       }
     },
-    customerAvatar(newAvatar) {
-      if (this.chatInstance && newAvatar) {
-        this.chatInstance.setData({ customerAvatar: newAvatar });
-      }
-    },
     isLoggedIn: {
       handler(newVal) {
         if (!newVal && this.configReady && !this.configError && this._sdkInited && !this._loginPanelCreated) {
@@ -264,7 +262,7 @@ export default {
           await this.handleLogin(code);
           this.storeLoginState(this.corpId);
           this.isLoggedIn = true;
-          if (this.customerId && this.staffUserId) await this.loadFirstPage();
+          if ((this.customerId && this.staffUserId) || this.chatId) await this.loadFirstPage();
         } catch (err) {
           this.clearLoginState(this.corpId);
           this.isLoggedIn = false;
@@ -273,7 +271,7 @@ export default {
       } else {
         if (this.checkLoginState(this.corpId)) {
           this.isLoggedIn = true;
-          if (this.customerId && this.staffUserId) await this.loadFirstPage();
+          if ((this.customerId && this.staffUserId) || this.chatId) await this.loadFirstPage();
         } else {
           this.isLoggedIn = false;
           this.safeEmitLogout();
@@ -316,7 +314,7 @@ export default {
           onLoginSuccess: async ({ code }) => {
             try {
               await this.handleLogin(code);
-              if (this.customerId && this.staffUserId) await this.loadFirstPage();
+              if ((this.customerId && this.staffUserId) || this.chatId) await this.loadFirstPage();
             } catch (e) {
               this.clearLoginState(this.corpId);
               this.isLoggedIn = false;
@@ -394,11 +392,13 @@ export default {
           staffUserId: this.staffUserId,
           limit: 100,
           cursor: this.cursor,
-          corpid: this.corpId
+          corpid: this.corpId,
+          chatId: this.chatId || undefined
         });
         const data = this._extractResponse(res);
         if (data.errcode !== 0) throw new Error(data.errmsg || '拉取失败');
-        this.msgList = data.data || [];
+        const rawList = data.data || [];
+        this.msgList = this.processMessages(rawList, false);
         this.cursor = data.next_cursor || '';
         this.hasMore = data.has_more === 1;
         this.isReady = true;
@@ -421,13 +421,15 @@ export default {
           staffUserId: this.staffUserId,
           limit: 50,
           cursor: this.cursor,
-          corpid: this.corpId
+          corpid: this.corpId,
+          chatId: this.chatId || undefined
         });
         const data = this._extractResponse(res);
         if (data.errcode !== 0) throw new Error(data.errmsg || '拉取更多失败');
         const newMessages = data.data || [];
         if (newMessages.length) {
-          this.msgList.push(...newMessages);
+          const processed = this.processMessages(newMessages, true);
+          this.msgList.push(...processed);
           this.cursor = data.next_cursor || '';
           this.hasMore = data.has_more === 1;
           if (this.chatInstance) this.chatInstance.setData({ msgList: this.msgList });
@@ -442,6 +444,27 @@ export default {
       }
     },
 
+    processMessages(messages, isAppend = false) {
+      const prevLastMsg = isAppend && this.msgList.length > 0
+        ? this.msgList[this.msgList.length - 1]
+        : null
+      let prevDate = prevLastMsg ? (prevLastMsg.dateStr || '') : ''
+
+      return messages.map(msg => {
+        const timeStr = msg.send_time_str || ''
+        const parts = timeStr.split(' ')
+        const dateStr = parts[0] || ''
+        const displayTime = parts[1] ? parts[1].substring(0, 5) : ''
+        const showDate = !!dateStr && dateStr !== prevDate
+        prevDate = dateStr
+        return {
+          ...msg,
+          dateStr,
+          displayTime,
+          showDate
+        }
+      })
+    },
     async renderOrUpdateChat() {
       if (this.msgList.length === 0 || this.configError) return;
       const container = document.getElementById('chat-container');
@@ -460,26 +483,87 @@ export default {
         template: `
           <scroll-view scroll-y="{{true}}" bindscrolltolower="onScrollToLower" style="height: 100%;">
             <view wx:for="{{data.msgList}}" wx:for-item="msg" wx:key="msgid" style="margin-bottom: 15px;">
-              <view style="text-align: center; margin-bottom: 8px;">
-                <text style="background:#e9e9e9; color:#333; padding:4px 12px; border-radius:16px; font-size:12px;">
-                  {{ msg.send_time_str }}
-                </text>
+              <!-- 日期分隔线:仅跨天时显示 -->
+              <view wx:if="{{msg.showDate}}" style="text-align: center; margin-bottom: 12px;">
+                <span style="background:#e9e9e9; color:#888; padding:3px 12px; border-radius:12px; font-size:11px;">{{ msg.dateStr }}</span>
+              </view>
+              <!-- 客户/机器人消息(左对齐) -->
+              <view wx:if="{{msg.sender.type == 2 || msg.sender.type == 3}}" style="display:flex; flex-direction:row; align-items:flex-start;">
+                <ww-open-data wx:if="{{msg.sender.type == 2}}" type="externalUserAvatar" openid="{{msg.sender.id}}" class="msg-avatar-left" />
+                <ww-open-data wx:if="{{msg.sender.type == 3}}" type="userAvatar" openid="{{msg.sender.id}}" class="msg-avatar-left" />
+                <view style="display:flex; flex-direction:column; max-width:70%; margin-left:10px;">
+                  <view style="display:flex; align-items:center; margin-bottom:4px;">
+                    <ww-open-data wx:if="{{msg.sender.type == 2}}" type="externalUserName" openid="{{msg.sender.id}}" class="sender-name" />
+                    <ww-open-data wx:if="{{msg.sender.type == 3}}" type="userName" openid="{{msg.sender.id}}" class="sender-name" />
+                    <span style="font-size:11px;color:#999;margin-left:8px;">{{ msg.displayTime }}</span>
+                  </view>
+                  <view class="msg-bubble msg-bubble-left">
+                    <ww-open-message message-id="{{msg.msgid}}" secret-key="{{msg.secretKey}}" open-type="viewMessage"/>
+                  </view>
+                </view>
               </view>
-              <view style="display:flex; flex-direction:row; {{msg.sender.type == 1 ? 'justify-content:flex-end;' : 'justify-content:flex-start;'}}">
-                <image wx:if="{{msg.sender.type == 2}}" src="{{data.customerAvatar}}" style="width:36px;height:36px;border-radius:4px;margin-right:10px;background:#fff;"></image>
-                <view style="max-width:70%;padding:10px 14px;border-radius:8px;word-break:break-all;background:{{msg.sender.type == 1 ? '#95ec69' : '#ffffff'}};box-shadow:0 1px 3px rgba(0,0,0,0.1);display:flex;align-items:center;">
+              <!-- 员工消息(右对齐) -->
+              <view wx:if="{{msg.sender.type == 1}}" style="display:flex; flex-direction:row; justify-content:flex-end; align-items:flex-start;">
+                <view style="display:flex; flex-direction:column; align-items:flex-end; max-width:70%; margin-right:10px;">
+                  <view style="display:flex; align-items:center; margin-bottom:4px;">
+                    <span style="font-size:11px;color:#999;margin-right:8px;">{{ msg.displayTime }}</span>
+                    <ww-open-data type="userName" openid="{{msg.sender.id}}" class="sender-name" />
+                  </view>
+                  <view class="msg-bubble msg-bubble-right">
+                    <ww-open-message message-id="{{msg.msgid}}" secret-key="{{msg.secretKey}}" open-type="viewMessage"/>
+                  </view>
+                </view>
+                <ww-open-data type="userAvatar" openid="{{msg.sender.id}}" class="msg-avatar-right" />
+              </view>
+              <!-- 未知类型消息(居中,无头像) -->
+              <view wx:if="{{msg.sender.type != 1 && msg.sender.type != 2 && msg.sender.type != 3}}" style="display:flex; justify-content:center;">
+                <view class="msg-bubble msg-bubble-left">
                   <ww-open-message message-id="{{msg.msgid}}" secret-key="{{msg.secretKey}}" open-type="viewMessage"/>
                 </view>
-                <image wx:if="{{msg.sender.type == 1}}" src="{{data.defaultStaffAvatar}}" style="width:36px;height:36px;border-radius:4px;margin-left:10px;background:#fff;"></image>
               </view>
             </view>
-            <view wx:if="{{data.hasMore && data.loadingMore}}" style="text-align:center;padding:10px;color:#999;">加载更多...</view>
+            <view wx:if="{{data.hasMore && data.loadingMore}}" style="text-align:center; padding:10px; color:#999;">加载更多...</view>
           </scroll-view>
         `,
+        style: `
+          .msg-avatar-left {
+            width: 36px;
+            height: 36px;
+            border-radius: 4px;
+            flex-shrink: 0;
+          }
+          .msg-avatar-right {
+            width: 36px;
+            height: 36px;
+            border-radius: 4px;
+            flex-shrink: 0;
+          }
+          .sender-name {
+            font-size: 12px;
+            color: #666;
+            max-width: 120px;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+          }
+          .msg-bubble {
+            padding: 10px 14px;
+            border-radius: 8px;
+            word-break: break-all;
+            display: flex;
+            align-items: center;
+          }
+          .msg-bubble-left {
+            background: #ffffff;
+            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+          }
+          .msg-bubble-right {
+            background: #95ec69;
+            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+          }
+        `,
         data: {
           msgList: this.msgList,
-          customerAvatar: this.customerAvatar,
-          defaultStaffAvatar: this.defaultStaffAvatar,
           hasMore: this.hasMore,
           loadingMore: this.loadingMore
         },

+ 8 - 6
src/views/qw/conversationNew/EmployeeQueryTab.vue

@@ -94,8 +94,7 @@
           <ConversationPanelPure
             :corpId="currentStaff && currentStaff.corpId"
             :customerId="selectedCustomerId"
-            :customerAvatar="selectedCustomerAvatar"
-            :staffUserId="currentStaff && currentStaff.qwUserId"
+            :staffUserId="currentStaffQwUserId"
             @logout="handleLogout"
           />
         </div>
@@ -149,14 +148,14 @@ export default {
       // 抽屉相关
       drawerVisible: false,
       currentStaff: null,
+      currentStaffQwUserId: '',  // 企业微信官方员工ID,从客户列表响应中提取
       customerList: [],
       customerTotal: 0,
       customerPageNum: 1,
       customerPageSize: 10,
       customerLoading: false,
       customerKeyword: "",
-      selectedCustomerId: null,
-      selectedCustomerAvatar: ""
+      selectedCustomerId: null
     };
   },
   computed: {
@@ -251,7 +250,7 @@ export default {
       this.currentStaff = { ...row, corpId: this.corp?.corpId };
       this.drawerVisible = true;
       this.selectedCustomerId = null;
-      this.selectedCustomerAvatar = "";
+      this.currentStaffQwUserId = '';
       this.customerPageNum = 1;
       this.loadCustomerList();
     },
@@ -271,6 +270,10 @@ export default {
         if (res.rows) {
           this.customerList = res.rows;
           this.customerTotal = res.total || 0;
+          // 从客户列表提取企业微信官方员工ID(用于会话查询,非SCRM内部ID)
+          if (!this.currentStaffQwUserId && res.rows.length > 0 && res.rows[0].qwUserId) {
+            this.currentStaffQwUserId = res.rows[0].qwUserId;
+          }
         } else {
           this.customerList = [];
           this.customerTotal = 0;
@@ -287,7 +290,6 @@ export default {
     // 选中客户
     handleCustomerClick(row) {
       this.selectedCustomerId = row.externalUserId;
-      this.selectedCustomerAvatar = row.avatar;
     },
 
     // 会话登录失效处理