|
@@ -0,0 +1,896 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="app-container qw-msg-audit-page">
|
|
|
|
|
+ <div class="audit-shell">
|
|
|
|
|
+ <!-- 左:主体 + 员工 -->
|
|
|
|
|
+ <aside class="audit-left">
|
|
|
|
|
+ <div class="aside-block aside-corp">
|
|
|
|
|
+ <div class="block-label">企微主体</div>
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="corpId"
|
|
|
|
|
+ filterable
|
|
|
|
|
+ clearable
|
|
|
|
|
+ placeholder="请选择企微主体"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ class="corp-select"
|
|
|
|
|
+ :loading="corpLoading"
|
|
|
|
|
+ @change="handleCorpChange"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="item in corpList"
|
|
|
|
|
+ :key="item.dictValue"
|
|
|
|
|
+ :label="item.dictLabel"
|
|
|
|
|
+ :value="item.dictValue"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-loading="userLoading" class="aside-block aside-users">
|
|
|
|
|
+ <div class="block-label">员工</div>
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="userFilter"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ placeholder="筛选当前页"
|
|
|
|
|
+ prefix-icon="el-icon-search"
|
|
|
|
|
+ class="user-search"
|
|
|
|
|
+ />
|
|
|
|
|
+ <el-scrollbar class="user-scroll">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="u in filteredUsers"
|
|
|
|
|
+ :key="userRowKey(u)"
|
|
|
|
|
+ class="user-item"
|
|
|
|
|
+ :class="{ active: selectedUser && selectedUser.qwUserId === u.qwUserId }"
|
|
|
|
|
+ @click="selectUser(u)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="user-name">{{ displayUserName(u) }}</span>
|
|
|
|
|
+ <span class="user-id text-muted">{{ u.qwUserId }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-empty
|
|
|
|
|
+ v-if="!userLoading && filteredUsers.length === 0"
|
|
|
|
|
+ :image-size="72"
|
|
|
|
|
+ description="暂无员工"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-scrollbar>
|
|
|
|
|
+ <pagination
|
|
|
|
|
+ v-show="userTotal > 0"
|
|
|
|
|
+ small
|
|
|
|
|
+ class="user-pagination"
|
|
|
|
|
+ layout="prev, pager, next"
|
|
|
|
|
+ :total="userTotal"
|
|
|
|
|
+ :page.sync="userQuery.pageNum"
|
|
|
|
|
+ :limit.sync="userQuery.pageSize"
|
|
|
|
|
+ @pagination="loadUserList"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </aside>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 中:会话列表 -->
|
|
|
|
|
+ <aside class="audit-conv">
|
|
|
|
|
+ <div class="conv-head">
|
|
|
|
|
+ <span class="conv-head-title">会话</span>
|
|
|
|
|
+ <el-radio-group
|
|
|
|
|
+ v-model="chatScope"
|
|
|
|
|
+ size="mini"
|
|
|
|
|
+ :disabled="!selectedUser"
|
|
|
|
|
+ @change="handleChatScopeChange"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-radio-button label="single">单聊</el-radio-button>
|
|
|
|
|
+ <el-radio-button label="group">群聊</el-radio-button>
|
|
|
|
|
+ </el-radio-group>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-loading="convLoading" class="conv-body">
|
|
|
|
|
+ <el-scrollbar class="conv-scroll">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(c, idx) in conversationList"
|
|
|
|
|
+ :key="convKey(c, idx)"
|
|
|
|
|
+ class="conv-item"
|
|
|
|
|
+ :class="{ active: isSelectedConv(c) }"
|
|
|
|
|
+ @click="selectConversation(c)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="conv-item-main">
|
|
|
|
|
+ <el-avatar :src="convAvatar(c)" :size="40" shape="square" class="conv-avatar" />
|
|
|
|
|
+ <div class="conv-text">
|
|
|
|
|
+ <div class="conv-title">{{ convTitle(c) }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-empty
|
|
|
|
|
+ v-if="!convLoading && selectedUser && conversationList.length === 0"
|
|
|
|
|
+ :image-size="56"
|
|
|
|
|
+ description="暂无会话"
|
|
|
|
|
+ />
|
|
|
|
|
+ <div v-if="!selectedUser" class="conv-placeholder text-muted">请先选择左边的员工</div>
|
|
|
|
|
+ </el-scrollbar>
|
|
|
|
|
+ <pagination
|
|
|
|
|
+ v-show="conversationTotal > 0"
|
|
|
|
|
+ small
|
|
|
|
|
+ class="conv-pagination"
|
|
|
|
|
+ layout="prev, pager, next"
|
|
|
|
|
+ :total="conversationTotal"
|
|
|
|
|
+ :page.sync="conversationQuery.pageNum"
|
|
|
|
|
+ :limit.sync="conversationQuery.pageSize"
|
|
|
|
|
+ @pagination="loadConversationList"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </aside>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 右:聊天记录 -->
|
|
|
|
|
+ <section class="audit-chat">
|
|
|
|
|
+ <div class="chat-toolbar">
|
|
|
|
|
+ <div class="toolbar-title">
|
|
|
|
|
+ <template v-if="selectedUser && selectedConversation">
|
|
|
|
|
+ <el-avatar :src="convAvatar(selectedConversation)" :size="36" shape="square" class="toolbar-avatar" />
|
|
|
|
|
+ <span class="toolbar-name">{{ convTitle(selectedConversation) }}</span>
|
|
|
|
|
+ <span class="text-muted toolbar-sub">· {{ displayUserName(selectedUser) }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <span v-else-if="selectedUser" class="text-muted">请选择会话</span>
|
|
|
|
|
+ <span v-else class="text-muted">请先选择员工和会话</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-loading="msgLoading" class="chat-panel">
|
|
|
|
|
+ <template v-if="selectedUser && selectedConversation">
|
|
|
|
|
+ <el-scrollbar ref="msgScroll" class="msg-scroll">
|
|
|
|
|
+ <div class="msg-list">
|
|
|
|
|
+ <div v-if="msgLoadingMore" class="msg-load-tip">加载更早的消息…</div>
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="msg in messages"
|
|
|
|
|
+ :key="msg.id || msg.msgId"
|
|
|
|
|
+ class="msg-row"
|
|
|
|
|
+ :class="{ 'is-self': isSelfMessage(msg) }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="msg-meta">
|
|
|
|
|
+ <span class="from">{{ msg.fromUser }}</span>
|
|
|
|
|
+ <span class="time">{{ formatMsgTime(msg.msgTime) }}</span>
|
|
|
|
|
+ <span v-if="chatScope === 'group' && msg.roomId" class="room-tag">群 {{ shortRoom(msg.roomId) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="bubble">
|
|
|
|
|
+ <div v-if="msg.textContent" class="text">{{ msg.textContent }}</div>
|
|
|
|
|
+ <div v-else-if="msg.mediaOssUrl" class="media">
|
|
|
|
|
+ <a :href="msg.mediaOssUrl" target="_blank" rel="noopener">查看媒体</a>
|
|
|
|
|
+ <span v-if="msg.mediaFileName" class="file-name">{{ msg.mediaFileName }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-else class="fallback text-muted">
|
|
|
|
|
+ [{{ msgTypeLabel(msg.msgType) }}]
|
|
|
|
|
+ <template v-if="msg.mediaFileName">{{ msg.mediaFileName }}</template>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+<!-- <div class="msg-extra text-muted">-->
|
|
|
|
|
+<!-- <span v-if="msg.toList">to: {{ formatToList(msg.toList) }}</span>-->
|
|
|
|
|
+<!-- <span v-if="msg.fromUserRole != null && msg.fromUserRole !== ''"> · 角色 {{ msg.fromUserRole }}</span>-->
|
|
|
|
|
+<!-- </div>-->
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-empty v-if="!msgLoading && messages.length === 0" description="暂无消息" />
|
|
|
|
|
+ </el-scrollbar>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <el-empty v-else-if="!selectedUser" description="在左侧选择员工和会话" />
|
|
|
|
|
+ <el-empty v-else description="选择会话后查看消息记录" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+import { getMyQwCompanyList } from '@/api/qw/user'
|
|
|
|
|
+import { qwList } from '@/api/qw/qwUser'
|
|
|
|
|
+import { listQwMsgAuditConversation, listQwMsgAuditMessage } from '@/api/qw/qwMsgAuditMessage'
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ name: 'QwMsgAuditMessage',
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ corpId: null,
|
|
|
|
|
+ corpList: [],
|
|
|
|
|
+ corpLoading: false,
|
|
|
|
|
+ userList: [],
|
|
|
|
|
+ userTotal: 0,
|
|
|
|
|
+ userQuery: {
|
|
|
|
|
+ pageNum: 1,
|
|
|
|
|
+ pageSize: 20
|
|
|
|
|
+ },
|
|
|
|
|
+ userLoading: false,
|
|
|
|
|
+ userFilter: '',
|
|
|
|
|
+ selectedUser: null,
|
|
|
|
|
+ chatScope: 'single',
|
|
|
|
|
+ conversationList: [],
|
|
|
|
|
+ conversationTotal: 0,
|
|
|
|
|
+ conversationQuery: {
|
|
|
|
|
+ pageNum: 1,
|
|
|
|
|
+ pageSize: 15
|
|
|
|
|
+ },
|
|
|
|
|
+ convLoading: false,
|
|
|
|
|
+ selectedConversation: null,
|
|
|
|
|
+ messages: [],
|
|
|
|
|
+ msgPageSize: 15,
|
|
|
|
|
+ msgPageNum: 1,
|
|
|
|
|
+ msgTotal: 0,
|
|
|
|
|
+ msgHasMore: false,
|
|
|
|
|
+ msgLoadingMore: false,
|
|
|
|
|
+ msgLoading: false,
|
|
|
|
|
+ _msgScrollWrap: null,
|
|
|
|
|
+ _onMsgScroll: null
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ computed: {
|
|
|
|
|
+ filteredUsers() {
|
|
|
|
|
+ const q = (this.userFilter || '').toLowerCase()
|
|
|
|
|
+ if (!q) return this.userList
|
|
|
|
|
+ return this.userList.filter((u) => {
|
|
|
|
|
+ const name = (u.qwUserName || u.nickName || '').toLowerCase()
|
|
|
|
|
+ const id = u.qwUserId != null ? String(u.qwUserId).toLowerCase() : ''
|
|
|
|
|
+ return name.includes(q) || id.includes(q)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ created() {
|
|
|
|
|
+ this.loadCorpList()
|
|
|
|
|
+ },
|
|
|
|
|
+ // 离开页面时去掉滚动监听,避免内存泄漏
|
|
|
|
|
+ beforeDestroy() {
|
|
|
|
|
+ this.unbindMsgScroll()
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ // 气泡里显示时间
|
|
|
|
|
+ formatMsgTime(t) {
|
|
|
|
|
+ if (!t) return ''
|
|
|
|
|
+ return this.parseTime(t)
|
|
|
|
|
+ },
|
|
|
|
|
+ // 群聊里 roomId 太长时缩略展示
|
|
|
|
|
+ shortRoom(roomId) {
|
|
|
|
|
+ if (!roomId) return ''
|
|
|
|
|
+ const s = String(roomId)
|
|
|
|
|
+ return s.length > 12 ? s.slice(0, 10) + '…' : s
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // toList 可能是对象,转成可读字符串
|
|
|
|
|
+ formatToList(toList) {
|
|
|
|
|
+ if (toList == null) return ''
|
|
|
|
|
+ if (typeof toList === 'string') return toList
|
|
|
|
|
+ try {
|
|
|
|
|
+ return JSON.stringify(toList)
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ return String(toList)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ msgTypeLabel(type) {
|
|
|
|
|
+ if (type == null || type === '') return '未知类型'
|
|
|
|
|
+ return String(type)
|
|
|
|
|
+ },
|
|
|
|
|
+ // 是否当前员工自己发的(用于绿气泡靠右)
|
|
|
|
|
+ isSelfMessage(msg) {
|
|
|
|
|
+ if (!this.selectedUser || !msg.fromUser) return false
|
|
|
|
|
+ return String(msg.fromUser) === String(this.selectedUser.qwUserId)
|
|
|
|
|
+ },
|
|
|
|
|
+ displayUserName(u) {
|
|
|
|
|
+ if (!u) return ''
|
|
|
|
|
+ return u.qwUserName || ''
|
|
|
|
|
+ },
|
|
|
|
|
+ userRowKey(u) {
|
|
|
|
|
+ return u.qwUserId
|
|
|
|
|
+ },
|
|
|
|
|
+ convKey(c, idx) {
|
|
|
|
|
+ if (c && c.conversationKey != null) return String(c.conversationKey)
|
|
|
|
|
+ return 'conv-' + idx
|
|
|
|
|
+ },
|
|
|
|
|
+ convTitle(c) {
|
|
|
|
|
+ if (!c) return ''
|
|
|
|
|
+ if (c.name) return c.name
|
|
|
|
|
+ if (this.chatScope === 'group') {
|
|
|
|
|
+ return c.roomName || '群聊'
|
|
|
|
|
+ }
|
|
|
|
|
+ return c.title || '—'
|
|
|
|
|
+ },
|
|
|
|
|
+ // 接口头像链接字段 Avatar(兼容 avatar)
|
|
|
|
|
+ convAvatar(c) {
|
|
|
|
|
+ if (!c) return ''
|
|
|
|
|
+ return c.Avatar || c.avatar || ''
|
|
|
|
|
+ },
|
|
|
|
|
+ isSelectedConv(c) {
|
|
|
|
|
+ if (!c || !this.selectedConversation) return false
|
|
|
|
|
+ const a = c.conversationKey
|
|
|
|
|
+ const b = this.selectedConversation.conversationKey
|
|
|
|
|
+ if (a == null || b == null) return false
|
|
|
|
|
+ return String(a) === String(b)
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // full=true:换主体、员工被踢出当前页等,清空中间会话列表 + 右侧消息;full=false:只清空右侧消息与分页状态
|
|
|
|
|
+ resetSession(full) {
|
|
|
|
|
+ if (full) {
|
|
|
|
|
+ this.selectedConversation = null
|
|
|
|
|
+ this.conversationList = []
|
|
|
|
|
+ this.conversationTotal = 0
|
|
|
|
|
+ this.conversationQuery.pageNum = 1
|
|
|
|
|
+ }
|
|
|
|
|
+ this.messages = []
|
|
|
|
|
+ this.msgPageNum = 1
|
|
|
|
|
+ this.msgTotal = 0
|
|
|
|
|
+ this.msgHasMore = false
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 拉主体下拉,默认选中第一项并顺带请求员工列表
|
|
|
|
|
+ async loadCorpList() {
|
|
|
|
|
+ this.corpLoading = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getMyQwCompanyList()
|
|
|
|
|
+ this.corpList = res.data || []
|
|
|
|
|
+ if (this.corpList.length > 0) {
|
|
|
|
|
+ this.corpId = this.corpList[0].dictValue
|
|
|
|
|
+ await this.handleCorpChange()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.corpId = null
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ this.corpList = []
|
|
|
|
|
+ this.corpId = null
|
|
|
|
|
+ this.$message.warning('获取企微主体失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.corpLoading = false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 左侧员工分页(依赖当前 corpId)
|
|
|
|
|
+ async loadUserList() {
|
|
|
|
|
+ if (!this.corpId) {
|
|
|
|
|
+ this.userList = []
|
|
|
|
|
+ this.userTotal = 0
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ this.userLoading = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await qwList({
|
|
|
|
|
+ corpId: this.corpId,
|
|
|
|
|
+ pageNum: this.userQuery.pageNum,
|
|
|
|
|
+ pageSize: this.userQuery.pageSize
|
|
|
|
|
+ })
|
|
|
|
|
+ this.userList = res.rows || []
|
|
|
|
|
+ this.userTotal = res.total || 0
|
|
|
|
|
+ if (
|
|
|
|
|
+ this.selectedUser &&
|
|
|
|
|
+ !this.userList.some((u) => String(u.qwUserId) === String(this.selectedUser.qwUserId))
|
|
|
|
|
+ ) {
|
|
|
|
|
+ this.selectedUser = null
|
|
|
|
|
+ this.resetSession(true)
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ this.userList = []
|
|
|
|
|
+ this.userTotal = 0
|
|
|
|
|
+ this.$message.warning('获取员工列表失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.userLoading = false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 换主体:清空选人、会话、消息,再拉员工
|
|
|
|
|
+ async handleCorpChange() {
|
|
|
|
|
+ this.selectedUser = null
|
|
|
|
|
+ this.resetSession(true)
|
|
|
|
|
+ this.unbindMsgScroll()
|
|
|
|
|
+ this.userQuery.pageNum = 1
|
|
|
|
|
+ if (!this.corpId) {
|
|
|
|
|
+ this.userList = []
|
|
|
|
|
+ this.userTotal = 0
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ await this.loadUserList()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 点某个员工:拉中间会话列表
|
|
|
|
|
+ async selectUser(u) {
|
|
|
|
|
+ this.selectedUser = u
|
|
|
|
|
+ this.selectedConversation = null
|
|
|
|
|
+ this.resetSession(false)
|
|
|
|
|
+ this.conversationQuery.pageNum = 1
|
|
|
|
|
+ this.unbindMsgScroll()
|
|
|
|
|
+ await this.loadConversationList()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 单聊 / 群聊切换:重新拉会话列表,右侧清空
|
|
|
|
|
+ async handleChatScopeChange() {
|
|
|
|
|
+ if (!this.selectedUser) return
|
|
|
|
|
+ this.selectedConversation = null
|
|
|
|
|
+ this.resetSession(false)
|
|
|
|
|
+ this.conversationQuery.pageNum = 1
|
|
|
|
|
+ this.unbindMsgScroll()
|
|
|
|
|
+ await this.loadConversationList()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 中间栏会话分页
|
|
|
|
|
+ async loadConversationList() {
|
|
|
|
|
+ if (!this.selectedUser || !this.corpId) {
|
|
|
|
|
+ this.conversationList = []
|
|
|
|
|
+ this.conversationTotal = 0
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ this.convLoading = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await listQwMsgAuditConversation({
|
|
|
|
|
+ corpId: this.corpId,
|
|
|
|
|
+ qwUserId: this.selectedUser.qwUserId,
|
|
|
|
|
+ chatScope: this.chatScope,
|
|
|
|
|
+ pageNum: this.conversationQuery.pageNum,
|
|
|
|
|
+ pageSize: this.conversationQuery.pageSize
|
|
|
|
|
+ })
|
|
|
|
|
+ const rows = res.rows || res.data
|
|
|
|
|
+ this.conversationList = Array.isArray(rows) ? rows : []
|
|
|
|
|
+ this.conversationTotal = res.total != null ? res.total : 0
|
|
|
|
|
+ if (this.selectedConversation && !this.conversationList.some((c) => this.isSelectedConv(c))) {
|
|
|
|
|
+ this.selectedConversation = null
|
|
|
|
|
+ this.resetSession(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ this.conversationList = []
|
|
|
|
|
+ this.conversationTotal = 0
|
|
|
|
|
+ this.$message.warning('获取会话列表失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.convLoading = false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ async selectConversation(c) {
|
|
|
|
|
+ if (!c || c.conversationKey == null || c.conversationKey === '') {
|
|
|
|
|
+ this.$message.warning('会话参数有误,无法加载消息')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ this.selectedConversation = c
|
|
|
|
|
+ this.resetSession(false)
|
|
|
|
|
+ this.unbindMsgScroll()
|
|
|
|
|
+ this.msgLoading = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.fetchMessagePage(1, true)
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ this.scrollMsgBottom()
|
|
|
|
|
+ this.bindMsgScroll()
|
|
|
|
|
+ })
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.msgLoading = false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 消息列表按时间降序时,reverse 后展示为「上旧下新」
|
|
|
|
|
+ async fetchMessagePage(pageNum, replace) {
|
|
|
|
|
+ const key = this.selectedConversation && this.selectedConversation.conversationKey
|
|
|
|
|
+ if (key == null || key === '') {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const res = await listQwMsgAuditMessage({
|
|
|
|
|
+ conversationKey: key,
|
|
|
|
|
+ pageNum,
|
|
|
|
|
+ pageSize: this.msgPageSize
|
|
|
|
|
+ })
|
|
|
|
|
+ const raw = res.rows || res.data
|
|
|
|
|
+ const rows = Array.isArray(raw) ? raw : []
|
|
|
|
|
+ const asc = rows.slice().reverse()
|
|
|
|
|
+ const total = res.total != null ? Number(res.total) : null
|
|
|
|
|
+
|
|
|
|
|
+ if (replace) {
|
|
|
|
|
+ this.messages = asc
|
|
|
|
|
+ this.msgPageNum = pageNum
|
|
|
|
|
+ this.msgTotal = total != null ? total : 0
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.messages = [...asc, ...this.messages]
|
|
|
|
|
+ this.msgPageNum = pageNum
|
|
|
|
|
+ }
|
|
|
|
|
+ if (total != null) {
|
|
|
|
|
+ this.msgHasMore = this.messages.length < total
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.msgHasMore = rows.length >= this.msgPageSize
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ async loadOlderMessages() {
|
|
|
|
|
+ if (!this.selectedConversation || !this.msgHasMore) return
|
|
|
|
|
+ const key = this.selectedConversation.conversationKey
|
|
|
|
|
+ if (key == null || key === '') return
|
|
|
|
|
+ if (this.msgLoading || this.msgLoadingMore) return
|
|
|
|
|
+
|
|
|
|
|
+ const nextPage = this.msgPageNum + 1
|
|
|
|
|
+ const scrollbar = this.$refs.msgScroll
|
|
|
|
|
+ const wrap = scrollbar && scrollbar.wrap
|
|
|
|
|
+ const oldScrollHeight = wrap ? wrap.scrollHeight : 0
|
|
|
|
|
+ const oldScrollTop = wrap ? wrap.scrollTop : 0
|
|
|
|
|
+
|
|
|
|
|
+ this.msgLoadingMore = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.fetchMessagePage(nextPage, false)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.msgLoadingMore = false
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ if (wrap) {
|
|
|
|
|
+ const delta = wrap.scrollHeight - oldScrollHeight
|
|
|
|
|
+ wrap.scrollTop = oldScrollTop + delta
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ bindMsgScroll() {
|
|
|
|
|
+ this.unbindMsgScroll()
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ const sb = this.$refs.msgScroll
|
|
|
|
|
+ if (!sb || !sb.wrap) return
|
|
|
|
|
+ this._msgScrollWrap = sb.wrap
|
|
|
|
|
+ // 距顶部很近时尝试加载更早消息
|
|
|
|
|
+ this._onMsgScroll = () => {
|
|
|
|
|
+ const wrap = this._msgScrollWrap
|
|
|
|
|
+ if (!wrap || wrap.scrollTop > 100) return
|
|
|
|
|
+ this.loadOlderMessages()
|
|
|
|
|
+ }
|
|
|
|
|
+ sb.wrap.addEventListener('scroll', this._onMsgScroll, { passive: true })
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 移除消息滚动容器上的scroll事件监听
|
|
|
|
|
+ unbindMsgScroll() {
|
|
|
|
|
+ if (this._msgScrollWrap && this._onMsgScroll) {
|
|
|
|
|
+ this._msgScrollWrap.removeEventListener('scroll', this._onMsgScroll)
|
|
|
|
|
+ }
|
|
|
|
|
+ this._msgScrollWrap = null
|
|
|
|
|
+ this._onMsgScroll = null
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ scrollMsgBottom() {
|
|
|
|
|
+ const ref = this.$refs.msgScroll
|
|
|
|
|
+ if (ref && ref.wrap) {
|
|
|
|
|
+ ref.wrap.scrollTop = ref.wrap.scrollHeight
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+.qw-msg-audit-page {
|
|
|
|
|
+ // 整页占满主内容区高度,避免外层再出一条纵向滚动条;只让内部列表滚动
|
|
|
|
|
+ height: calc(100vh - 120px);
|
|
|
|
|
+ max-height: calc(100vh - 120px);
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+
|
|
|
|
|
+ .audit-shell {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .audit-left {
|
|
|
|
|
+ width: 260px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ border-right: 1px solid #ebeef5;
|
|
|
|
|
+ background: #fafafa;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .audit-conv {
|
|
|
|
|
+ width: 220px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ border-right: 1px solid #ebeef5;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .conv-head {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ padding: 10px 8px;
|
|
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+
|
|
|
|
|
+ .conv-head-title {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ::v-deep .el-radio-group {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ::v-deep .el-radio-button {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+
|
|
|
|
|
+ .el-radio-button__inner {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ padding: 6px 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .conv-body {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .conv-scroll {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-height: 160px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .conv-placeholder {
|
|
|
|
|
+ padding: 24px 12px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .conv-item {
|
|
|
|
|
+ padding: 8px 10px;
|
|
|
|
|
+ margin: 0 6px 6px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ border: 1px solid transparent;
|
|
|
|
|
+ transition: background 0.15s;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ background: #f5f7fa;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.active {
|
|
|
|
|
+ border-color: #c2e7b0;
|
|
|
|
|
+ background: #f0f9eb;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .conv-item-main {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .conv-avatar {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .conv-text {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .conv-title {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .conv-pagination {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ padding: 4px;
|
|
|
|
|
+
|
|
|
|
|
+ ::v-deep .pagination-container {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .aside-block {
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .aside-corp {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .aside-users {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .block-label {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .corp-select {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .user-search {
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .user-pagination {
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+ padding: 0 4px;
|
|
|
|
|
+
|
|
|
|
|
+ ::v-deep .pagination-container {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .user-scroll {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-height: 200px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .user-item {
|
|
|
|
|
+ padding: 8px 10px;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border: 1px solid transparent;
|
|
|
|
|
+ transition: background 0.15s;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ background: #f0f9eb;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.active {
|
|
|
|
|
+ border-color: #67c23a;
|
|
|
|
|
+ background: #f0f9eb;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .user-name {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .user-id {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .text-muted {
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .audit-chat {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .chat-toolbar {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ padding: 10px 16px;
|
|
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .toolbar-title {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .toolbar-avatar {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .toolbar-name {
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .toolbar-sub {
|
|
|
|
|
+ font-weight: normal;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .chat-panel {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ background: #ededed;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // flex 子项里 el-scrollbar 用 height:0 + flex:1 才能拿到确定高度,只在此处滚动
|
|
|
|
|
+ .msg-scroll {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ height: 0;
|
|
|
|
|
+
|
|
|
|
|
+ ::v-deep .el-scrollbar__wrap {
|
|
|
|
|
+ overflow-x: hidden;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .msg-load-tip {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ padding: 8px 0 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .msg-list {
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ max-width: 880px;
|
|
|
|
|
+ margin: 0 auto;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .msg-row {
|
|
|
|
|
+ margin-bottom: 14px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+
|
|
|
|
|
+ &.is-self {
|
|
|
|
|
+ align-items: flex-end;
|
|
|
|
|
+
|
|
|
|
|
+ .bubble {
|
|
|
|
|
+ background: #95ec69;
|
|
|
|
|
+ color: #000;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .msg-meta {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+
|
|
|
|
|
+ .from {
|
|
|
|
|
+ margin-right: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .room-tag {
|
|
|
|
|
+ margin-left: 8px;
|
|
|
|
|
+ color: #67c23a;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .bubble {
|
|
|
|
|
+ max-width: 75%;
|
|
|
|
|
+ padding: 8px 12px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ word-break: break-word;
|
|
|
|
|
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
|
|
|
|
+
|
|
|
|
|
+ .text {
|
|
|
|
|
+ line-height: 1.5;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .media {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+
|
|
|
|
|
+ .file-name {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .msg-extra {
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ max-width: 75%;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|