|
@@ -1,7 +1,8 @@
|
|
|
<template>
|
|
<template>
|
|
|
<div class="content-search-tab">
|
|
<div class="content-search-tab">
|
|
|
|
|
+ <!-- 搜索表单 -->
|
|
|
<div class="search-form">
|
|
<div class="search-form">
|
|
|
- <el-form :model="searchForm" ref="searchForm" label-width="100px">
|
|
|
|
|
|
|
+ <el-form :model="searchForm" inline>
|
|
|
<el-form-item label="聊天内容" required>
|
|
<el-form-item label="聊天内容" required>
|
|
|
<el-input v-model="searchForm.queryWord" placeholder="请输入关键词(至少2个字符)" clearable />
|
|
<el-input v-model="searchForm.queryWord" placeholder="请输入关键词(至少2个字符)" clearable />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
@@ -23,34 +24,106 @@
|
|
|
/>
|
|
/>
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
<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-button @click="resetForm">重置</el-button>
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
</el-form>
|
|
</el-form>
|
|
|
</div>
|
|
</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>
|
|
</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>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
|
import { qwSearchMsg } from "@/api/qw/companySession";
|
|
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 {
|
|
export default {
|
|
|
name: "ContentSearchTab",
|
|
name: "ContentSearchTab",
|
|
|
- components: { ConversationPanelPure, Pagination },
|
|
|
|
|
|
|
+ components: { ConversationPanelPure },
|
|
|
props: {
|
|
props: {
|
|
|
corpId: { type: String, required: true },
|
|
corpId: { type: String, required: true },
|
|
|
},
|
|
},
|
|
@@ -64,15 +137,25 @@ export default {
|
|
|
},
|
|
},
|
|
|
dateRange: [],
|
|
dateRange: [],
|
|
|
searching: false,
|
|
searching: false,
|
|
|
|
|
+ tableLoading: false,
|
|
|
|
|
+ loadingMore: false,
|
|
|
|
|
+ messageList: [],
|
|
|
|
|
+ cursor: null,
|
|
|
|
|
+ hasMore: false,
|
|
|
|
|
+ searchedNoData: false,
|
|
|
|
|
+ drawerVisible: false,
|
|
|
|
|
+ currentStaffUserId: null,
|
|
|
|
|
+ currentCustomerId: null,
|
|
|
pickerOptions: {
|
|
pickerOptions: {
|
|
|
disabledDate(time) {
|
|
disabledDate(time) {
|
|
|
- // 只能选择当前日期到30天前
|
|
|
|
|
|
|
+ // 只能选择最近31天内的日期
|
|
|
const today = new Date();
|
|
const today = new Date();
|
|
|
today.setHours(0, 0, 0, 0);
|
|
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: {
|
|
watch: {
|
|
|
dateRange(val) {
|
|
dateRange(val) {
|
|
|
if (val && val.length === 2) {
|
|
if (val && val.length === 2) {
|
|
|
- this.searchForm.startTime = val[0];
|
|
|
|
|
- this.searchForm.endTime = val[1];
|
|
|
|
|
- } else {
|
|
|
|
|
this.searchForm.startTime = null;
|
|
this.searchForm.startTime = null;
|
|
|
this.searchForm.endTime = null;
|
|
this.searchForm.endTime = null;
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
methods: {
|
|
methods: {
|
|
|
- async doSearch() {
|
|
|
|
|
|
|
+ async handleSearch() {
|
|
|
if (!this.searchForm.queryWord || this.searchForm.queryWord.length < 2) {
|
|
if (!this.searchForm.queryWord || this.searchForm.queryWord.length < 2) {
|
|
|
this.$message.warning("聊天内容关键词至少2个字符");
|
|
this.$message.warning("聊天内容关键词至少2个字符");
|
|
|
return;
|
|
return;
|
|
@@ -98,31 +178,125 @@ export default {
|
|
|
this.$message.warning("请先选择企微主体");
|
|
this.$message.warning("请先选择企微主体");
|
|
|
return;
|
|
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 {
|
|
try {
|
|
|
const params = {
|
|
const params = {
|
|
|
corpId: this.corpId,
|
|
corpId: this.corpId,
|
|
|
queryWord: this.searchForm.queryWord,
|
|
queryWord: this.searchForm.queryWord,
|
|
|
chatType: this.searchForm.chatType || undefined,
|
|
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);
|
|
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 {
|
|
} else {
|
|
|
this.$message.error(res.msg || "搜索失败");
|
|
this.$message.error(res.msg || "搜索失败");
|
|
|
|
|
+ if (reset) this.messageList = [];
|
|
|
}
|
|
}
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
console.error("搜索异常", e);
|
|
console.error("搜索异常", e);
|
|
|
this.$message.error("搜索异常");
|
|
this.$message.error("搜索异常");
|
|
|
|
|
+ if (reset) this.messageList = [];
|
|
|
} finally {
|
|
} 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() {
|
|
resetForm() {
|
|
|
this.searchForm = {
|
|
this.searchForm = {
|
|
|
queryWord: "",
|
|
queryWord: "",
|
|
@@ -131,6 +305,44 @@ export default {
|
|
|
endTime: null,
|
|
endTime: null,
|
|
|
};
|
|
};
|
|
|
this.dateRange = [];
|
|
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>
|
|
<style scoped>
|
|
|
.content-search-tab {
|
|
.content-search-tab {
|
|
|
height: 100%;
|
|
height: 100%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
.search-form {
|
|
.search-form {
|
|
|
background: #fff;
|
|
background: #fff;
|
|
|
padding: 16px;
|
|
padding: 16px;
|
|
|
border-radius: 4px;
|
|
border-radius: 4px;
|
|
|
margin-bottom: 16px;
|
|
margin-bottom: 16px;
|
|
|
}
|
|
}
|
|
|
-.search-result {
|
|
|
|
|
|
|
+
|
|
|
|
|
+.result-table-wrapper {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
background: #fff;
|
|
background: #fff;
|
|
|
border-radius: 4px;
|
|
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>
|
|
</style>
|