| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586 |
- <template>
- <div class="content-search-tab">
- <!-- 搜索表单 -->
- <div class="search-form">
- <el-form :model="searchForm" inline>
- <el-form-item label="聊天内容" required>
- <el-input v-model="searchForm.queryWord" placeholder="请输入关键词(至少2个字符)" clearable />
- </el-form-item>
- <el-form-item label="聊天类型">
- <el-select v-model="searchForm.chatType" placeholder="请选择" clearable @change="onChatTypeChange">
- <el-option label="全部" :value="null" />
- <el-option label="单聊" :value="1" />
- <el-option label="群聊" :value="2" />
- </el-select>
- </el-form-item>
- <!-- 单聊条件:员工 + 客户 -->
- <el-form-item v-if="searchForm.chatType === 1" label="员工">
- <el-select
- v-model="searchForm.staffUserId"
- filterable
- remote
- clearable
- reserve-keyword
- placeholder="请输入员工名称搜索"
- :remote-method="searchStaff"
- :loading="staffSearchLoading"
- @change="onStaffChange"
- >
- <el-option
- v-for="item in staffOptions"
- :key="item.qwUserId"
- :label="item.qwUserName"
- :value="item.qwUserId"
- />
- </el-select>
- </el-form-item>
- <el-form-item v-if="searchForm.chatType === 1" label="客户">
- <el-select
- v-model="searchForm.customerId"
- filterable
- clearable
- placeholder="请先选择员工,再搜索客户"
- :loading="customerPreLoading"
- :filter-method="filterCustomerLocal"
- @visible-change="onCustomerDropdownVisible"
- >
- <el-option
- v-for="item in filteredCustomerOptions"
- :key="item.externalUserId"
- :label="item.name"
- :value="item.externalUserId"
- />
- </el-select>
- <span v-if="customerNoMatch" style="color:#E6A23C;font-size:12px;">未匹配到客户</span>
- </el-form-item>
- <!-- 群聊条件:群聊 -->
- <el-form-item v-if="searchForm.chatType === 2" label="群聊">
- <el-select
- v-model="searchForm.chatId"
- filterable
- remote
- clearable
- reserve-keyword
- placeholder="请输入群聊名称搜索"
- :remote-method="searchGroupChat"
- :loading="groupChatSearchLoading"
- >
- <el-option
- v-for="item in groupChatOptions"
- :key="item.chatId"
- :label="item.name"
- :value="item.chatId"
- />
- </el-select>
- </el-form-item>
- <el-form-item label="发送时间">
- <el-date-picker
- v-model="dateRange"
- type="daterange"
- range-separator="至"
- start-placeholder="开始日期"
- end-placeholder="结束日期"
- :picker-options="pickerOptions"
- value-format="timestamp"
- />
- </el-form-item>
- <el-form-item>
- <el-button type="primary" @click="handleSearch" :loading="searching">查询</el-button>
- <el-button @click="resetForm">重置</el-button>
- </el-form-item>
- </el-form>
- </div>
- <!-- 搜索结果表格(无限滚动) -->
- <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="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">
- <template slot-scope="scope">
- {{ scope.row.receiverNames.join('、') }}
- </template>
- </el-table-column>
- <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">
- <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"
- :chatId="currentChatId"
- @logout="handleLogout"
- />
- </div>
- </el-drawer>
- </div>
- </template>
- <script>
- import { qwSearchMsg, qwStaffOptions, qwCustomerOptions, qwGroupChatOptions } from "@/api/qw/companySession";
- 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 },
- props: {
- corpId: { type: String, required: true },
- },
- data() {
- return {
- searchForm: {
- queryWord: "",
- chatType: null,
- staffUserId: null,
- customerId: null,
- chatId: null,
- startTime: null,
- endTime: null,
- },
- // 下拉选项
- staffOptions: [],
- customerOptions: [], // 员工的所有客户(预加载后本地过滤)
- filteredCustomerOptions: [], // 本地过滤后的展示列表
- customerNoMatch: false, // 输入关键字后无匹配
- groupChatOptions: [],
- staffSearchLoading: false,
- customerPreLoading: false, // 预加载员工客户时的 loading
- customerSearchLoading: false,
- groupChatSearchLoading: false,
- dateRange: [],
- searching: false,
- tableLoading: false,
- loadingMore: false,
- messageList: [],
- cursor: null,
- hasMore: false,
- searchedNoData: false,
- drawerVisible: false,
- currentStaffUserId: null,
- currentCustomerId: null,
- currentChatId: null,
- pickerOptions: {
- disabledDate(time) {
- // 只能选择最近31天内的日期
- const today = new Date();
- today.setHours(0, 0, 0, 0);
- 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();
- },
- },
- };
- },
- watch: {
- dateRange(val) {
- if (val && val.length === 2) {
- this.searchForm.startTime = null;
- this.searchForm.endTime = null;
- }
- },
- },
- methods: {
- async handleSearch() {
- if (!this.searchForm.queryWord || this.searchForm.queryWord.length < 2) {
- this.$message.warning("聊天内容关键词至少2个字符");
- return;
- }
- if (!this.corpId) {
- this.$message.warning("请先选择企微主体");
- return;
- }
- // 计算当前时间和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,
- staffUserId: this.searchForm.staffUserId || undefined,
- customerId: this.searchForm.customerId || undefined,
- chatId: this.searchForm.chatId || undefined,
- startTime: this.searchForm.startTime,
- endTime: this.searchForm.endTime,
- limit: 50,
- cursor: reset ? null : this.cursor,
- };
- const res = await qwSearchMsg(params);
- 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.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 = '未知';
- // 优先使用后端返回的 name,查不到则用原始 ID 兜底
- const senderName = msg.sender.name || msg.sender.id;
- const receiverNames = (msg.receiver_list || []).map(r => {
- 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) {
- contentPreview = '[文本消息,内容需后端解密]';
- }
- return {
- ...msg,
- chatTypeName,
- senderTypeName,
- senderName,
- receiverNames,
- msgTypeName,
- contentPreview,
- };
- });
- },
- resetForm() {
- this.searchForm = {
- queryWord: "",
- chatType: null,
- staffUserId: null,
- customerId: null,
- chatId: null,
- startTime: null,
- endTime: null,
- };
- this.staffOptions = [];
- this.customerOptions = [];
- this.filteredCustomerOptions = [];
- this.customerNoMatch = false;
- this.groupChatOptions = [];
- this.dateRange = [];
- this.messageList = [];
- this.cursor = null;
- this.hasMore = false;
- this.searchedNoData = false;
- },
- onChatTypeChange(val) {
- this.searchForm.staffUserId = null;
- this.searchForm.customerId = null;
- this.searchForm.chatId = null;
- this.staffOptions = [];
- this.customerOptions = [];
- this.filteredCustomerOptions = [];
- this.customerNoMatch = false;
- this.groupChatOptions = [];
- },
- async onStaffChange(staffUserId) {
- // 切换员工时清空已选客户,预加载该员工全部客户
- this.searchForm.customerId = null;
- this.customerOptions = [];
- this.filteredCustomerOptions = [];
- this.customerNoMatch = false;
- if (!staffUserId || !this.corpId) return;
- this.customerPreLoading = true;
- try {
- const res = await qwCustomerOptions({ corpId: this.corpId, qwUserId: staffUserId });
- if (res.code === 200) {
- this.customerOptions = res.data || [];
- this.filteredCustomerOptions = this.customerOptions;
- }
- } catch (e) {
- console.error('预加载客户失败', e);
- } finally {
- this.customerPreLoading = false;
- }
- },
- filterCustomerLocal(keyword) {
- // 本地过滤,不调后端
- if (!keyword) {
- this.filteredCustomerOptions = this.customerOptions;
- this.customerNoMatch = false;
- } else {
- const kw = keyword.toLowerCase();
- this.filteredCustomerOptions = this.customerOptions.filter(
- c => c.name && c.name.toLowerCase().includes(kw)
- );
- this.customerNoMatch = this.filteredCustomerOptions.length === 0;
- }
- },
- onCustomerDropdownVisible(visible) {
- // 下拉打开时重置为全量
- if (visible) {
- this.filteredCustomerOptions = this.customerOptions;
- this.customerNoMatch = false;
- }
- },
- async searchStaff(keyword) {
- if (!this.corpId) return;
- this.staffSearchLoading = true;
- try {
- const res = await qwStaffOptions({ corpId: this.corpId, keyword });
- if (res.code === 200) {
- this.staffOptions = res.data || [];
- }
- } catch (e) {
- console.error('搜索员工失败', e);
- } finally {
- this.staffSearchLoading = false;
- }
- },
- async searchCustomer(keyword) {
- // 兼容旧调用:本地已预加载则本地过滤,否则调远端
- if (this.customerOptions.length > 0) {
- this.filterCustomerLocal(keyword);
- } else if (this.corpId) {
- this.customerSearchLoading = true;
- try {
- const res = await qwCustomerOptions({ corpId: this.corpId, keyword });
- if (res.code === 200) {
- this.customerOptions = res.data || [];
- this.filteredCustomerOptions = this.customerOptions;
- }
- } catch (e) {
- console.error('搜索客户失败', e);
- } finally {
- this.customerSearchLoading = false;
- }
- }
- },
- async searchGroupChat(keyword) {
- if (!this.corpId) return;
- this.groupChatSearchLoading = true;
- try {
- const res = await qwGroupChatOptions({ corpId: this.corpId, keyword });
- if (res.code === 200) {
- this.groupChatOptions = res.data || [];
- }
- } catch (e) {
- console.error('搜索群聊失败', e);
- } finally {
- this.groupChatSearchLoading = 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) {
- // 群聊:传 chatId 查看整个群的消息
- if (row.chatid) {
- 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;
- 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.currentChatId = null;
- this.drawerVisible = true;
- } else {
- this.$message.warning("无法确定会话双方");
- }
- },
- handleLogout() {
- this.$message.warning("会话登录已失效,请刷新页面重试");
- },
- },
- };
- </script>
- <style scoped>
- .content-search-tab {
- height: 100%;
- display: flex;
- flex-direction: column;
- }
- .search-form {
- background: #fff;
- padding: 16px;
- border-radius: 4px;
- margin-bottom: 16px;
- }
- .result-table-wrapper {
- flex: 1;
- overflow-y: auto;
- background: #fff;
- border-radius: 4px;
- padding: 16px;
- }
- .loading-more, .no-more {
- text-align: center;
- padding: 12px;
- color: #999;
- font-size: 14px;
- }
- .drawer-container {
- height: 100%;
- }
- </style>
|