Jelajahi Sumber

企业会话记录增加按照内容搜索

cgp 15 jam lalu
induk
melakukan
5fa2616e12

+ 262 - 46
src/views/qw/conversationNew/ContentSearchTab.vue

@@ -1,7 +1,8 @@
 <template>
   <div class="content-search-tab">
+    <!-- 搜索表单 -->
     <div class="search-form">
-      <el-form :model="searchForm" ref="searchForm" label-width="100px">
+      <el-form :model="searchForm" inline>
         <el-form-item label="聊天内容" required>
           <el-input v-model="searchForm.queryWord" placeholder="请输入关键词(至少2个字符)" clearable />
         </el-form-item>
@@ -23,34 +24,106 @@
           />
         </el-form-item>
         <el-form-item>
-          <el-button type="primary" @click="doSearch" :loading="searching">查询</el-button>
+          <el-button type="primary" @click="handleSearch" :loading="searching">查询</el-button>
           <el-button @click="resetForm">重置</el-button>
         </el-form-item>
       </el-form>
     </div>
 
-    <div class="search-result">
-      <!-- 暂时显示空白,后续可展示搜索结果列表 -->
-      <div class="result-placeholder">
-        <i class="el-icon-search"></i>
-        <p>查询结果将在这里展示(后续版本完善)</p>
-      </div>
-      <!--
-        后续可加 ConversationPanel 展示会话详情,需传递员工ID和客户ID
-        但搜索接口返回的是消息列表,需要聚合按会话分组,暂不实现
-      -->
+    <!-- 搜索结果表格(无限滚动) -->
+    <div class="result-table-wrapper" ref="scrollContainer" @scroll="handleScroll">
+      <el-table
+        v-loading="tableLoading"
+        :data="messageList"
+        border
+        stripe
+        style="width: 100%"
+      >
+        <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">
+          <template slot-scope="scope">
+            {{ scope.row.receiverNames.join('、') }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="sendTimeStr" label="发送时间" width="180" />
+        <el-table-column prop="msgTypeName" label="消息类型" width="100" />
+        <el-table-column label="聊天内容" min-width="200">
+          <template slot-scope="scope">
+            <span v-if="scope.row.msgtype === 1">{{ scope.row.contentPreview || '[文本消息]' }}</span>
+            <span v-else>{{ scope.row.msgTypeName }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="120">
+          <template slot-scope="scope">
+            <el-button type="text" @click="openContextDrawer(scope.row)">查看上下文</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div v-if="loadingMore" class="loading-more">加载更多...</div>
+      <div v-if="!hasMore && messageList.length > 0" class="no-more">没有更多了</div>
+      <div v-if="searchedNoData" class="no-more">未找到相关消息</div>
     </div>
+
+    <!-- 抽屉:会话上下文 -->
+    <el-drawer
+      :visible.sync="drawerVisible"
+      title="会话上下文"
+      direction="rtl"
+      size="80%"
+      destroy-on-close
+    >
+      <div class="drawer-container">
+        <ConversationPanelPure
+          :corpId="corpId"
+          :customerId="currentCustomerId"
+          :staffUserId="currentStaffUserId"
+          @logout="handleLogout"
+        />
+      </div>
+    </el-drawer>
   </div>
 </template>
 
 <script>
 import { qwSearchMsg } from "@/api/qw/companySession";
-import ConversationPanelPure from './ConversationPanelPure.vue';
-import Pagination from "@/components/Pagination";
+import ConversationPanelPure from "./ConversationPanelPure.vue";
+
+const MSG_TYPE_MAP = {
+  0: '暂不支持',
+  1: '文本',
+  2: '图片',
+  3: '表情',
+  4: '链接',
+  5: '小程序',
+  6: '语音',
+  7: '视频',
+  8: '文件',
+  9: '名片',
+  10: '转发消息',
+  11: '视频号',
+  12: '日程',
+  13: '红包',
+  14: '地理位置',
+  15: '快速会议',
+  16: '待办',
+  17: '投票',
+  18: '在线文档',
+  19: '图文消息',
+  20: '图文混合消息',
+  21: '音频存档',
+  22: '音视频通话',
+  23: '微盘文件',
+  24: '同意会话存档',
+  25: '拒绝会话存档',
+  26: '群接龙',
+  27: 'markdown',
+  28: '笔记'
+};
 
 export default {
   name: "ContentSearchTab",
-  components: { ConversationPanelPure, Pagination },
+  components: { ConversationPanelPure },
   props: {
     corpId: { type: String, required: true },
   },
@@ -64,15 +137,25 @@ export default {
       },
       dateRange: [],
       searching: false,
+      tableLoading: false,
+      loadingMore: false,
+      messageList: [],
+      cursor: null,
+      hasMore: false,
+      searchedNoData: false,
+      drawerVisible: false,
+      currentStaffUserId: null,
+      currentCustomerId: null,
       pickerOptions: {
         disabledDate(time) {
-          // 只能选择当前日期到30天前
+          // 只能选择最近31天内的日期
           const today = new Date();
           today.setHours(0, 0, 0, 0);
-          const thirtyDaysAgo = new Date();
-          thirtyDaysAgo.setDate(today.getDate() - 30);
-          thirtyDaysAgo.setHours(0, 0, 0, 0);
-          return time.getTime() > today.getTime() || time.getTime() < thirtyDaysAgo.getTime();
+          const thirtyOneDaysAgo = new Date();
+          thirtyOneDaysAgo.setDate(today.getDate() - 31);
+          thirtyOneDaysAgo.setHours(0, 0, 0, 0);
+          // 禁止选择今天之后和31天前的日期
+          return time.getTime() > today.getTime() || time.getTime() < thirtyOneDaysAgo.getTime();
         },
       },
     };
@@ -80,16 +163,13 @@ export default {
   watch: {
     dateRange(val) {
       if (val && val.length === 2) {
-        this.searchForm.startTime = val[0];
-        this.searchForm.endTime = val[1];
-      } else {
         this.searchForm.startTime = null;
         this.searchForm.endTime = null;
       }
     },
   },
   methods: {
-    async doSearch() {
+    async handleSearch() {
       if (!this.searchForm.queryWord || this.searchForm.queryWord.length < 2) {
         this.$message.warning("聊天内容关键词至少2个字符");
         return;
@@ -98,31 +178,125 @@ export default {
         this.$message.warning("请先选择企微主体");
         return;
       }
-      this.searching = true;
+
+      // 计算当前时间和31天前的时间戳(秒)
+      const nowSec = Math.floor(Date.now() / 1000);
+      const thirtyOneDaysAgoSec = nowSec - 31 * 24 * 3600;
+
+      let startTime = null;
+      let endTime = null;
+
+      if (this.dateRange && this.dateRange.length === 2) {
+        // 用户选择了日期范围
+        startTime = Math.floor(this.dateRange[0] / 1000);
+        let rawEndTime = Math.floor(this.dateRange[1] / 1000);
+
+        // 强制 end_time 不能晚于当前时间
+        if (rawEndTime > nowSec) {
+          this.$message.warning("结束时间不能晚于当前时间,已自动调整");
+          rawEndTime = nowSec;
+        }
+        endTime = rawEndTime;
+
+        // 确保 start_time < end_time
+        if (startTime >= endTime) {
+          this.$message.error("开始时间必须早于结束时间,请重新选择");
+          return;
+        }
+      } else {
+        // 用户未选日期范围:默认查询31天前到当前时间
+        startTime = thirtyOneDaysAgoSec;
+        endTime = nowSec;
+        this.$message.info("未选择时间范围,默认查询最近31天内的消息");
+      }
+
+      // 保存到 searchForm 中,供后续滚动加载使用
+      this.searchForm.startTime = startTime;
+      this.searchForm.endTime = endTime;
+
+      // 重置状态
+      this.messageList = [];
+      this.cursor = null;
+      this.hasMore = false;
+      this.searchedNoData = false;
+      this.tableLoading = true;
+      await this.loadMoreMessages(true);
+      this.tableLoading = false;
+    },
+
+    async loadMoreMessages(reset = false) {
+      if (this.loadingMore) return;
+      if (!reset && !this.hasMore) return;
+      this.loadingMore = true;
       try {
         const params = {
           corpId: this.corpId,
           queryWord: this.searchForm.queryWord,
           chatType: this.searchForm.chatType || undefined,
-          startTime: this.searchForm.startTime ? Math.floor(this.searchForm.startTime / 1000) : undefined,
-          endTime: this.searchForm.endTime ? Math.floor(this.searchForm.endTime / 1000) : undefined,
-          limit: 50,
+          startTime: this.searchForm.startTime,
+          endTime: this.searchForm.endTime,
+          limit: 20,
+          cursor: reset ? null : this.cursor,
         };
-        // 仅调用接口验证,暂不处理返回结果
         const res = await qwSearchMsg(params);
-        if (res.code === 200) {
-          this.$message.success(`搜索完成,共 ${res.data?.data?.length || 0} 条消息`);
-          // 后续可在此处理结果展示,目前仅为演示
+        if (res.code === 200 && res.data) {
+          const newMessages = res.data.data || [];
+          if (reset && newMessages.length === 0) {
+            this.searchedNoData = true;
+          } else {
+            this.searchedNoData = false;
+          }
+          const enriched = this.enrichMessages(newMessages);
+          if (reset) {
+            this.messageList = enriched;
+          } else {
+            this.messageList = this.messageList.concat(enriched);
+          }
+          this.cursor = res.data.nextCursor || null;
+          this.hasMore = res.data.hasMore === 1;
         } else {
           this.$message.error(res.msg || "搜索失败");
+          if (reset) this.messageList = [];
         }
       } catch (e) {
         console.error("搜索异常", e);
         this.$message.error("搜索异常");
+        if (reset) this.messageList = [];
       } finally {
-        this.searching = false;
+        this.loadingMore = false;
       }
     },
+
+    enrichMessages(messages) {
+      return messages.map(msg => {
+        let senderTypeName = '';
+        if (msg.sender.type === 1) senderTypeName = '员工';
+        else if (msg.sender.type === 2) senderTypeName = '客户';
+        else if (msg.sender.type === 3) senderTypeName = '机器人';
+        else senderTypeName = '未知';
+
+        const senderName = 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 msgTypeName = MSG_TYPE_MAP[msg.msgtype] || '未知类型';
+        let contentPreview = '';
+        if (msg.msgtype === 1) {
+          contentPreview = '[文本消息,内容需后端解密]';
+        }
+        return {
+          ...msg,
+          senderTypeName,
+          senderName,
+          receiverNames,
+          msgTypeName,
+          contentPreview,
+        };
+      });
+    },
+
     resetForm() {
       this.searchForm = {
         queryWord: "",
@@ -131,6 +305,44 @@ export default {
         endTime: null,
       };
       this.dateRange = [];
+      this.messageList = [];
+      this.cursor = null;
+      this.hasMore = false;
+      this.searchedNoData = false;
+    },
+
+    handleScroll(e) {
+      const container = e.target;
+      if (container.scrollHeight - container.scrollTop - container.clientHeight < 10) {
+        if (this.hasMore && !this.loadingMore && !this.tableLoading) {
+          this.loadMoreMessages();
+        }
+      }
+    },
+
+    openContextDrawer(row) {
+      if (row.chatid) {
+        this.$message.info("暂不支持群聊上下文查看");
+        return;
+      }
+      let staffId = null, customerId = null;
+      if (row.sender.type === 1) staffId = row.sender.id;
+      if (row.sender.type === 2) customerId = row.sender.id;
+      for (let recv of (row.receiver_list || [])) {
+        if (recv.type === 1 && !staffId) staffId = recv.id;
+        if (recv.type === 2 && !customerId) customerId = recv.id;
+      }
+      if (staffId && customerId) {
+        this.currentStaffUserId = staffId;
+        this.currentCustomerId = customerId;
+        this.drawerVisible = true;
+      } else {
+        this.$message.warning("无法确定会话双方");
+      }
+    },
+
+    handleLogout() {
+      this.$message.warning("会话登录已失效,请刷新页面重试");
     },
   },
 };
@@ -139,29 +351,33 @@ export default {
 <style scoped>
 .content-search-tab {
   height: 100%;
+  display: flex;
+  flex-direction: column;
 }
+
 .search-form {
   background: #fff;
   padding: 16px;
   border-radius: 4px;
   margin-bottom: 16px;
 }
-.search-result {
+
+.result-table-wrapper {
+  flex: 1;
+  overflow-y: auto;
   background: #fff;
   border-radius: 4px;
-  padding: 24px;
-  min-height: 400px;
+  padding: 16px;
 }
-.result-placeholder {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  color: #c0c4cc;
-  padding: 60px 0;
+
+.loading-more, .no-more {
+  text-align: center;
+  padding: 12px;
+  color: #999;
+  font-size: 14px;
 }
-.result-placeholder i {
-  font-size: 64px;
-  margin-bottom: 16px;
+
+.drawer-container {
+  height: 100%;
 }
 </style>

+ 29 - 1
src/views/qw/conversationNew/EmployeeQueryTab.vue

@@ -371,5 +371,33 @@ export default {
 </script>
 
 <style scoped>
-/* 原有样式 */
+.employee-query-tab {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+.query-form {
+  background: #fff;
+  padding: 16px;
+  margin-bottom: 16px;
+  border-radius: 4px;
+}
+.drawer-container {
+  display: flex;
+  height: 100%;
+  gap: 16px;
+}
+.customer-list {
+  width: 360px;
+  flex-shrink: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+.conversation-panel {
+  flex: 1;
+  background: #f5f6f7;
+  border-radius: 8px;
+  overflow: hidden;
+}
 </style>