2323
This commit is contained in:
parent
279c2074db
commit
c5b305d4ff
@ -1,5 +1,5 @@
|
|||||||
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||||
NODE_ENV = 'production'
|
NODE_ENV = 'production'
|
||||||
VITE_APP_BASE_API = 'http://scrm.1024tool.vip/'
|
VITE_APP_BASE_API = 'https://mini-chat.1024tool.vip/'
|
||||||
VITE_SERVE = "http://scrm.1024tool.vip/"
|
VITE_SERVE = "https://mini-chat.1024tool.vip/"
|
||||||
|
|
||||||
|
|||||||
12
components.d.ts
vendored
12
components.d.ts
vendored
@ -12,13 +12,10 @@ declare module 'vue' {
|
|||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElCard: typeof import('element-plus/es')['ElCard']
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
|
||||||
ElCol: typeof import('element-plus/es')['ElCol']
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
|
||||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
|
||||||
ElForm: typeof import('element-plus/es')['ElForm']
|
ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
ElHeader: typeof import('element-plus/es')['ElHeader']
|
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||||
@ -26,30 +23,21 @@ declare module 'vue' {
|
|||||||
ElImage: typeof import('element-plus/es')['ElImage']
|
ElImage: typeof import('element-plus/es')['ElImage']
|
||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||||
ElLink: typeof import('element-plus/es')['ElLink']
|
|
||||||
ElMain: typeof import('element-plus/es')['ElMain']
|
ElMain: typeof import('element-plus/es')['ElMain']
|
||||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
|
||||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
|
||||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
|
||||||
ElRow: typeof import('element-plus/es')['ElRow']
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
|
ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
|
||||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
|
||||||
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
|
|
||||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
ElTour: typeof import('element-plus/es')['ElTour']
|
|
||||||
ElTourStep: typeof import('element-plus/es')['ElTourStep']
|
|
||||||
IndexUser: typeof import('./src/components/TagClass/indexUser.vue')['default']
|
IndexUser: typeof import('./src/components/TagClass/indexUser.vue')['default']
|
||||||
KeyWords: typeof import('./src/components/KeyWords/index.vue')['default']
|
KeyWords: typeof import('./src/components/KeyWords/index.vue')['default']
|
||||||
MaterialPublic: typeof import('./src/components/MaterialPublic/index.vue')['default']
|
MaterialPublic: typeof import('./src/components/MaterialPublic/index.vue')['default']
|
||||||
|
|||||||
@ -31,7 +31,7 @@ const constantRoutes = [
|
|||||||
path: '/',
|
path: '/',
|
||||||
name: 'Layout',
|
name: 'Layout',
|
||||||
component: () => import('@/layout/index.vue'),
|
component: () => import('@/layout/index.vue'),
|
||||||
redirect: '/robot',
|
redirect: '/miniProgram',
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Layout',
|
title: 'Layout',
|
||||||
},
|
},
|
||||||
|
|||||||
398
src/views/chat/index copy.vue
Normal file
398
src/views/chat/index copy.vue
Normal file
@ -0,0 +1,398 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chat-page">
|
||||||
|
<div class="left-panel">
|
||||||
|
<UserCard @load="onLoadUserList" :cardlist="userList" height="calc(100vh - 120px)" @change="onSelectUser" />
|
||||||
|
</div>
|
||||||
|
<div class="right-panel">
|
||||||
|
<div class="chat-body" ref="chatBody" @scroll="handleScroll">
|
||||||
|
<ChatRecord :msgList="messages" :sendeInfo="sendeInfo" msgType="user" @retry="handleRetry" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-footer">
|
||||||
|
<div class="footer-left" style="">
|
||||||
|
<div class="upload-row" style="display:flex; align-items:center; gap:12px;margin-left: 6px;">
|
||||||
|
<el-icon style="font-size:24px;cursor:pointer" @click="triggerImageInput">
|
||||||
|
<svg class="icon" aria-hidden="true">
|
||||||
|
<use xlink:href="#icon-tupian1"></use>
|
||||||
|
</svg>
|
||||||
|
</el-icon>
|
||||||
|
<input ref="imageInput" type="file" accept="image/*" style="display:none"
|
||||||
|
@change="handleImageChange" />
|
||||||
|
<V3Emoji @click-emoji="onEmojiSelect" :recent="true"></V3Emoji>
|
||||||
|
</div>
|
||||||
|
<el-input type="textarea" v-model="draft" placeholder="输入消息..." rows="2" style="flex:1"
|
||||||
|
@keydown="handleInputKeydown" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, nextTick, watch } from 'vue'
|
||||||
|
import ChatRecord from '@/components/ChatRecord/index.vue'
|
||||||
|
import UserCard from '@/components/UserCard/index.vue'
|
||||||
|
import UserMessage from '@/components/UserMessage/index.vue'
|
||||||
|
import mihoutai from '@/assets/images/mihoutai.png'
|
||||||
|
import V3Emoji from "vue3-emoji";
|
||||||
|
import { getUserList, send_message, get_messages } from '@/api/chat'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { uploadFile } from '@/api/upload'
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
// mock data for now — integrate with store/api as needed
|
||||||
|
const userList = ref([
|
||||||
|
{ user_id: 'u1', user_name: '张三', user_avatar: '' },
|
||||||
|
{ user_id: 'u2', user_name: '李四', user_avatar: '' },
|
||||||
|
{ user_id: 'u3', user_name: '王五', user_avatar: '' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const activeUser = ref({})
|
||||||
|
const messages = ref([])
|
||||||
|
const draft = ref('')
|
||||||
|
const imageInput = ref(null)
|
||||||
|
const chatBody = ref(null)
|
||||||
|
|
||||||
|
const sendeInfo = reactive({
|
||||||
|
userInfo: { user_id: '', user_name: '', user_avatar: '' },
|
||||||
|
robotInfo: { wx_id: '', head_url: '' }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加消息分页查询参数
|
||||||
|
const msgQuery = reactive({
|
||||||
|
page: 1,
|
||||||
|
page_size: 100,
|
||||||
|
app_id: route.query.app_id,
|
||||||
|
user_id: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加加载状态
|
||||||
|
const loadingMore = ref(false)
|
||||||
|
const noMoreMessages = ref(false)
|
||||||
|
|
||||||
|
const onSelectUser = (id) => {
|
||||||
|
// id may be user_id or array when multiple; handle only single selection here
|
||||||
|
const u = userList.value.find((it) => it.user_id == id)
|
||||||
|
if (u) {
|
||||||
|
// mark selected and clear others
|
||||||
|
userList.value.forEach((it) => (it.active = it.user_id === id))
|
||||||
|
activeUser.value = u
|
||||||
|
sendeInfo.userInfo = { user_id: u.user_id, user_name: u.user_name, user_avatar: u.user_avatar }
|
||||||
|
// set robot identity used by ChatRecord to determine right-side messages
|
||||||
|
sendeInfo.robotInfo = { wx_id: 'robot', head_url: mihoutai }
|
||||||
|
// load messages for user (placeholder)
|
||||||
|
messages.value = []
|
||||||
|
// 重置分页参数
|
||||||
|
msgQuery.page = 1
|
||||||
|
msgQuery.user_id = u.user_id
|
||||||
|
noMoreMessages.value = false
|
||||||
|
// scroll after messages rendered
|
||||||
|
scrollToBottom()
|
||||||
|
getMessages()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改获取消息的方法
|
||||||
|
const getMessages = () => {
|
||||||
|
if (loadingMore.value || noMoreMessages.value) return
|
||||||
|
|
||||||
|
loadingMore.value = true
|
||||||
|
get_messages({ ...msgQuery }).then(res => {
|
||||||
|
if (res.list && res.list.length > 0) {
|
||||||
|
// 解析消息内容
|
||||||
|
const newMessages = res.list.map(item => {
|
||||||
|
item.content = JSON.parse(item.content)
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
|
||||||
|
if (msgQuery.page === 1) {
|
||||||
|
// 首次加载
|
||||||
|
messages.value = newMessages
|
||||||
|
} else {
|
||||||
|
// 加载更多,将新消息添加到列表开头
|
||||||
|
messages.value = [...newMessages, ...messages.value]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果返回的消息少于请求的数量,说明没有更多消息了
|
||||||
|
if (res.list.length < msgQuery.page_size) {
|
||||||
|
noMoreMessages.value = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 没有更多消息
|
||||||
|
noMoreMessages.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingMore.value = false
|
||||||
|
}).catch(() => {
|
||||||
|
loadingMore.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加滚动事件处理函数
|
||||||
|
const handleScroll = (e) => {
|
||||||
|
const { scrollTop } = e.target
|
||||||
|
|
||||||
|
// 当滚动到顶部时加载更多消息
|
||||||
|
if (scrollTop <= 0 && !loadingMore.value && !noMoreMessages.value) {
|
||||||
|
loadMoreMessages()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载更多消息
|
||||||
|
const loadMoreMessages = () => {
|
||||||
|
if (msgQuery.user_id) {
|
||||||
|
msgQuery.page++
|
||||||
|
getMessages()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const send = () => {
|
||||||
|
if (!activeUser.value.user_id) {
|
||||||
|
ElMessage({ type: 'warning', message: '请先选择一个联系人' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!draft.value) return
|
||||||
|
const content = draft.value
|
||||||
|
const id = Date.now() + Math.random()
|
||||||
|
const msg = {
|
||||||
|
_id: id,
|
||||||
|
_sending: true,
|
||||||
|
_failed: false,
|
||||||
|
sender_name: '机器人',
|
||||||
|
msg_type: 0,
|
||||||
|
content: { messages: content },
|
||||||
|
send_time: '刚刚',
|
||||||
|
"sender_id": "888888",
|
||||||
|
"sender_name": "平台",
|
||||||
|
"msg_type": 1
|
||||||
|
}
|
||||||
|
messages.value.push(msg)
|
||||||
|
draft.value = ''
|
||||||
|
// ensure scroll
|
||||||
|
scrollToBottom()
|
||||||
|
// send to server
|
||||||
|
send_message({
|
||||||
|
app_id: route.query.app_id,
|
||||||
|
content: JSON.stringify({
|
||||||
|
messages: content
|
||||||
|
}),
|
||||||
|
msg_type: 1,
|
||||||
|
to_user_id: activeUser.value.user_id
|
||||||
|
}).then(() => {
|
||||||
|
msg._sending = false
|
||||||
|
msg._failed = false
|
||||||
|
}).catch(() => {
|
||||||
|
msg._sending = false
|
||||||
|
msg._failed = true
|
||||||
|
ElMessage({ type: 'error', message: '消息发送失败' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInputKeydown = (e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggerImageInput = () => {
|
||||||
|
imageInput.value && imageInput.value.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImageChange = (e) => {
|
||||||
|
const file = e.target.files[0]
|
||||||
|
if (!file) return
|
||||||
|
// 直接用本地URL预览和发送
|
||||||
|
const url = URL.createObjectURL(file)
|
||||||
|
const id = Date.now() + Math.random()
|
||||||
|
const msg = {
|
||||||
|
_id: id,
|
||||||
|
_sending: true,
|
||||||
|
_failed: false,
|
||||||
|
sender_id: '888888',
|
||||||
|
sender_name: '平台',
|
||||||
|
msg_type: 2,
|
||||||
|
content: { messages: url },
|
||||||
|
send_time: '刚刚'
|
||||||
|
}
|
||||||
|
messages.value.push(msg)
|
||||||
|
// 清空input,允许重复选择同一图片
|
||||||
|
e.target.value = ''
|
||||||
|
// 图片可能增加高度,延迟滚动以保证图片渲染
|
||||||
|
setTimeout(() => scrollToBottom(), 100)
|
||||||
|
// 上传 file to server using FormData
|
||||||
|
const form = new FormData()
|
||||||
|
form.append('file', file)
|
||||||
|
form.append('app_id', route.query.app_id)
|
||||||
|
form.append('msg_type', 2)
|
||||||
|
form.append('to_user_id', activeUser.value.user_id)
|
||||||
|
uploadFile(form, import.meta.env.VITE_APP_BASE_API + 'admin/upload/image').then((resp) => {
|
||||||
|
send_message({
|
||||||
|
app_id: route.query.app_id,
|
||||||
|
content: JSON.stringify({ messages: import.meta.env.VITE_APP_BASE_API + resp.preview_image_url }),
|
||||||
|
msg_type: 2,
|
||||||
|
to_user_id: activeUser.value.user_id
|
||||||
|
}).then(() => {
|
||||||
|
msg._sending = false
|
||||||
|
msg._failed = false
|
||||||
|
}).catch(() => {
|
||||||
|
msg._sending = false
|
||||||
|
msg._failed = true
|
||||||
|
ElMessage({ type: 'error', message: '图片发送失败' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRetry = (id) => {
|
||||||
|
const msg = messages.value.find((m) => m._id === id)
|
||||||
|
if (!msg) return
|
||||||
|
msg._failed = false
|
||||||
|
msg._sending = true
|
||||||
|
if (msg.msg_type === 0) {
|
||||||
|
send_message({ app_id: route.query.app_id, content: msg.content.content, msg_type: 1, to_user_id: activeUser.value.user_id })
|
||||||
|
.then(() => { msg._sending = false })
|
||||||
|
.catch(() => { msg._sending = false; msg._failed = true; ElMessage({ type: 'error', message: '重发失败' }) })
|
||||||
|
} else if (msg.msg_type === 2) {
|
||||||
|
const form = new FormData()
|
||||||
|
form.append('file', msg.content.file)
|
||||||
|
form.append('app_id', route.query.app_id)
|
||||||
|
form.append('msg_type', 2)
|
||||||
|
form.append('to_user_id', activeUser.value.user_id)
|
||||||
|
send_message(form).then(() => { msg._sending = false }).catch(() => { msg._sending = false; msg._failed = true; ElMessage({ type: 'error', message: '重发失败' }) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollToBottom = async () => {
|
||||||
|
await nextTick()
|
||||||
|
const el = chatBody.value
|
||||||
|
if (!el) return
|
||||||
|
try {
|
||||||
|
el.scrollTop = el.scrollHeight
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当 messages 变化时自动滚动(仅在首次加载或发送新消息时)
|
||||||
|
// 移除原来的watcher,因为现在需要更精确地控制滚动行为
|
||||||
|
|
||||||
|
const onEmojiSelect = (emoji) => {
|
||||||
|
if (draft.value) {
|
||||||
|
draft.value += emoji
|
||||||
|
} else {
|
||||||
|
draft.value = emoji
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = reactive({
|
||||||
|
page: 1,
|
||||||
|
page_size: 20,
|
||||||
|
app_id: route.query.app_id
|
||||||
|
})
|
||||||
|
|
||||||
|
const getUsers = (append = false) => {
|
||||||
|
getUserList(params).then((res) => {
|
||||||
|
if (append) {
|
||||||
|
// append new items
|
||||||
|
userList.value = userList.value.concat(res.list || [])
|
||||||
|
} else {
|
||||||
|
userList.value = res.list || []
|
||||||
|
}
|
||||||
|
// if first load and no active user, select first
|
||||||
|
if (!append && userList.value.length && !activeUser.value.user_id) {
|
||||||
|
onSelectUser(userList.value[0].user_id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当 UserCard 触发 load(分页)时调用,加载下一页并滚动列表到底部以显示新数据
|
||||||
|
const onLoadUserList = async () => {
|
||||||
|
params.page = params.page + 1
|
||||||
|
await getUsers(true)
|
||||||
|
// 等待 DOM 更新后滚动
|
||||||
|
await nextTick()
|
||||||
|
const el = document.querySelector('.left-panel .infinite-list')
|
||||||
|
if (el) {
|
||||||
|
// 滚动到底部以展示新加载的用户
|
||||||
|
el.scrollTop = el.scrollHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendMessage = async (message) => {
|
||||||
|
send_message(message).then((res) => {
|
||||||
|
}).catch((err) => {
|
||||||
|
ElMessage({ type: 'error', message: '消息发送失败' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getUsers()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.chat-page {
|
||||||
|
display: flex;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-textarea__inner) {
|
||||||
|
background-color: transparent;
|
||||||
|
padding-left: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
width: 280px;
|
||||||
|
border-right: 1px solid var(--el-border-color);
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-header {
|
||||||
|
height: 60px;
|
||||||
|
line-height: 60px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
height: 64px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-bottom: 1px solid var(--el-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header .avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px;
|
||||||
|
overflow: auto;
|
||||||
|
background: #f5f7fa
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-footer {
|
||||||
|
height: 96px;
|
||||||
|
padding: 5px 5px;
|
||||||
|
border-top: 1px solid var(--el-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-footer .actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -5,12 +5,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="right-panel">
|
<div class="right-panel">
|
||||||
<div class="chat-body" ref="chatBody" @scroll="handleScroll">
|
<div class="chat-body" ref="chatBody" @scroll="handleScroll">
|
||||||
|
<!-- 加载更多提示 -->
|
||||||
|
<div v-if="loadingMore" class="loading-more">加载中...</div>
|
||||||
|
|
||||||
<ChatRecord :msgList="messages" :sendeInfo="sendeInfo" msgType="user" @retry="handleRetry" />
|
<ChatRecord :msgList="messages" :sendeInfo="sendeInfo" msgType="user" @retry="handleRetry" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chat-footer">
|
<div class="chat-footer">
|
||||||
<div class="footer-left" style="">
|
<div class="footer-left" style="">
|
||||||
<div class="upload-row" style="display:flex; align-items:center; gap:12px;margin-left: 6px;">
|
<div class="upload-row" style="display:flex; align-items:center; gap:12px;margin-left: 6px;position: relative; top: 10px;">
|
||||||
<el-icon style="font-size:24px;cursor:pointer" @click="triggerImageInput">
|
<el-icon style="font-size:24px;cursor:pointer" @click="triggerImageInput">
|
||||||
<svg class="icon" aria-hidden="true">
|
<svg class="icon" aria-hidden="true">
|
||||||
<use xlink:href="#icon-tupian1"></use>
|
<use xlink:href="#icon-tupian1"></use>
|
||||||
@ -23,13 +26,15 @@
|
|||||||
<el-input type="textarea" v-model="draft" placeholder="输入消息..." rows="2" style="flex:1"
|
<el-input type="textarea" v-model="draft" placeholder="输入消息..." rows="2" style="flex:1"
|
||||||
@keydown="handleInputKeydown" />
|
@keydown="handleInputKeydown" />
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 添加发送按钮 -->
|
||||||
|
<!-- <el-button type="primary" @click="send">发送</el-button> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted, nextTick, watch } from 'vue'
|
import { ref, reactive, onMounted, nextTick, watch, onUnmounted } from 'vue'
|
||||||
import ChatRecord from '@/components/ChatRecord/index.vue'
|
import ChatRecord from '@/components/ChatRecord/index.vue'
|
||||||
import UserCard from '@/components/UserCard/index.vue'
|
import UserCard from '@/components/UserCard/index.vue'
|
||||||
import UserMessage from '@/components/UserMessage/index.vue'
|
import UserMessage from '@/components/UserMessage/index.vue'
|
||||||
@ -38,9 +43,11 @@ import V3Emoji from "vue3-emoji";
|
|||||||
import { getUserList, send_message, get_messages } from '@/api/chat'
|
import { getUserList, send_message, get_messages } from '@/api/chat'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { uploadFile } from '@/api/upload'
|
import { uploadFile } from '@/api/upload'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
// mock data for now — integrate with store/api as needed
|
// 好友列表数据
|
||||||
const userList = ref([
|
const userList = ref([
|
||||||
{ user_id: 'u1', user_name: '张三', user_avatar: '' },
|
{ user_id: 'u1', user_name: '张三', user_avatar: '' },
|
||||||
{ user_id: 'u2', user_name: '李四', user_avatar: '' },
|
{ user_id: 'u2', user_name: '李四', user_avatar: '' },
|
||||||
@ -53,105 +60,177 @@ const draft = ref('')
|
|||||||
const imageInput = ref(null)
|
const imageInput = ref(null)
|
||||||
const chatBody = ref(null)
|
const chatBody = ref(null)
|
||||||
|
|
||||||
|
// 发送者信息
|
||||||
const sendeInfo = reactive({
|
const sendeInfo = reactive({
|
||||||
userInfo: { user_id: '', user_name: '', user_avatar: '' },
|
userInfo: { user_id: '', user_name: '', user_avatar: '' },
|
||||||
robotInfo: { wx_id: '', head_url: '' }
|
robotInfo: { wx_id: '', head_url: '' }
|
||||||
})
|
})
|
||||||
|
|
||||||
// 添加消息分页查询参数
|
// 消息分页查询参数
|
||||||
const msgQuery = reactive({
|
const msgQuery = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 100,
|
page_size: 20,
|
||||||
app_id: route.query.app_id,
|
app_id: route.query.app_id,
|
||||||
user_id: ''
|
user_id: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// 添加加载状态
|
// 状态变量
|
||||||
const loadingMore = ref(false)
|
const loadingMore = ref(false)
|
||||||
const noMoreMessages = ref(false)
|
const noMoreMessages = ref(false)
|
||||||
|
const isAutoScroll = ref(true) // 是否自动滚动到底部
|
||||||
|
const messageTimer = ref(null) // 定时器实例
|
||||||
|
|
||||||
|
// 选择用户
|
||||||
const onSelectUser = (id) => {
|
const onSelectUser = (id) => {
|
||||||
// id may be user_id or array when multiple; handle only single selection here
|
|
||||||
const u = userList.value.find((it) => it.user_id == id)
|
const u = userList.value.find((it) => it.user_id == id)
|
||||||
if (u) {
|
if (u) {
|
||||||
// mark selected and clear others
|
|
||||||
userList.value.forEach((it) => (it.active = it.user_id === id))
|
userList.value.forEach((it) => (it.active = it.user_id === id))
|
||||||
activeUser.value = u
|
activeUser.value = u
|
||||||
sendeInfo.userInfo = { user_id: u.user_id, user_name: u.user_name, user_avatar: u.user_avatar }
|
sendeInfo.userInfo = { user_id: u.user_id, user_name: u.user_name, user_avatar: u.user_avatar }
|
||||||
// set robot identity used by ChatRecord to determine right-side messages
|
|
||||||
sendeInfo.robotInfo = { wx_id: 'robot', head_url: mihoutai }
|
sendeInfo.robotInfo = { wx_id: 'robot', head_url: mihoutai }
|
||||||
// load messages for user (placeholder)
|
|
||||||
|
// 重置消息列表和分页
|
||||||
messages.value = []
|
messages.value = []
|
||||||
// 重置分页参数
|
|
||||||
msgQuery.page = 1
|
msgQuery.page = 1
|
||||||
msgQuery.user_id = u.user_id
|
msgQuery.user_id = u.user_id
|
||||||
noMoreMessages.value = false
|
noMoreMessages.value = false
|
||||||
// scroll after messages rendered
|
|
||||||
|
// 保持原有行为:先滚动到底部再加载(不 await),以免改变用户可感知的交互顺序
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
getMessages()
|
getMessages()
|
||||||
|
|
||||||
|
// 重启定时器
|
||||||
|
restartMessageTimer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改获取消息的方法
|
|
||||||
const getMessages = () => {
|
let msgTotal = 0
|
||||||
if (loadingMore.value || noMoreMessages.value) return
|
// 获取消息(区分加载历史和最新消息)
|
||||||
|
// isHistory: 是否为向上翻页加载历史
|
||||||
|
// pageOverride: 可选的临时 page 值(不会修改 msgQuery.page),用于定时拉取最新时使用 page=1
|
||||||
|
const getMessages = async (isHistory = true, pageOverride = null) => {
|
||||||
|
if (loadingMore.value) return
|
||||||
|
if (isHistory && noMoreMessages.value) return
|
||||||
|
if (!msgQuery.user_id) return
|
||||||
|
|
||||||
loadingMore.value = true
|
loadingMore.value = true
|
||||||
get_messages({ ...msgQuery }).then(res => {
|
const queryPage = pageOverride ?? msgQuery.page
|
||||||
if (res.list && res.list.length > 0) {
|
const query = { ...msgQuery, page: queryPage }
|
||||||
// 解析消息内容
|
|
||||||
const newMessages = res.list.map(item => {
|
try {
|
||||||
item.content = JSON.parse(item.content)
|
const res = await get_messages({ ...query })
|
||||||
|
msgTotal = res.total || 0
|
||||||
|
const list = res.list || []
|
||||||
|
|
||||||
|
if (list.length > 0) {
|
||||||
|
const newMessages = list.map(item => {
|
||||||
|
try { item.content = JSON.parse(item.content) } catch (e) { /* keep original */ }
|
||||||
return item
|
return item
|
||||||
})
|
})
|
||||||
|
|
||||||
if (msgQuery.page === 1) {
|
if (isHistory) {
|
||||||
// 首次加载
|
// 在向上加载历史时,需要在 prepend 前记录 scrollHeight 以便恢复视图位置
|
||||||
messages.value = newMessages
|
const el = chatBody.value
|
||||||
|
const prevScrollHeight = el ? el.scrollHeight : 0
|
||||||
|
|
||||||
|
if (queryPage === 1) {
|
||||||
|
// 第一次加载(第一页)直接替换
|
||||||
|
messages.value = newMessages
|
||||||
|
} else {
|
||||||
|
// 向上翻页,prepend
|
||||||
|
messages.value = [...newMessages, ...messages.value]
|
||||||
|
// 等待 DOM 更新后恢复滚动位置,避免跳动
|
||||||
|
await nextTick()
|
||||||
|
if (el) {
|
||||||
|
// 保持用户看到原来位置(新内容高度增加导致的偏移)
|
||||||
|
el.scrollTop = el.scrollHeight - prevScrollHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 加载更多,将新消息添加到列表开头
|
// 拉取最新消息(追加到末尾),避免重复
|
||||||
messages.value = [...newMessages, ...messages.value]
|
const existIds = new Set(messages.value.map(m => m.id || m._id))
|
||||||
|
const unique = newMessages.filter(m => !(existIds.has(m.id || m._id)))
|
||||||
|
if (unique.length) messages.value = [...messages.value, ...unique]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果返回的消息少于请求的数量,说明没有更多消息了
|
// 使用 total 判断是否还有更多历史消息(仅在已知 total 时判断)
|
||||||
if (res.list.length < msgQuery.page_size) {
|
if (msgTotal && isHistory) {
|
||||||
|
noMoreMessages.value = messages.value.length >= msgTotal
|
||||||
|
} else if (list.length < msgQuery.page_size && isHistory) {
|
||||||
|
// 如果后端没有 total,退回到 page_size 判断
|
||||||
noMoreMessages.value = true
|
noMoreMessages.value = true
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// 没有更多消息
|
// 非历史加载且处于自动滚动状态时滚到底部
|
||||||
|
if (!isHistory && isAutoScroll.value) {
|
||||||
|
await scrollToBottom()
|
||||||
|
}
|
||||||
|
} else if (isHistory) {
|
||||||
noMoreMessages.value = true
|
noMoreMessages.value = true
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// 可扩展错误处理
|
||||||
|
} finally {
|
||||||
|
loadingMore.value = false
|
||||||
|
|
||||||
loadingMore.value = false
|
// 如果是向上翻页(非第一页)并且 prepend 后视图仍然靠近顶部,自动继续翻页
|
||||||
}).catch(() => {
|
try {
|
||||||
loadingMore.value = false
|
if (isHistory && queryPage !== 1) {
|
||||||
})
|
await nextTick()
|
||||||
|
const el2 = chatBody.value
|
||||||
|
const nearTop = el2 ? el2.scrollTop <= 50 : false
|
||||||
|
if (nearTop && !noMoreMessages.value) {
|
||||||
|
// 继续加载下一页历史(递增 page)
|
||||||
|
msgQuery.page = msgQuery.page + 1
|
||||||
|
await getMessages(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略自动加载中的错误,保持用户可见性
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加滚动事件处理函数
|
// 滚动事件处理
|
||||||
const handleScroll = (e) => {
|
const handleScroll = (e) => {
|
||||||
const { scrollTop } = e.target
|
const { scrollTop, scrollHeight, clientHeight } = e.target
|
||||||
|
const isNearBottom = scrollHeight - scrollTop - clientHeight < 50 // 距离底部小于50px视为在底部
|
||||||
|
|
||||||
// 当滚动到顶部时加载更多消息
|
// 更新自动滚动状态
|
||||||
|
isAutoScroll.value = isNearBottom
|
||||||
|
|
||||||
|
// 控制定时器
|
||||||
|
if (isNearBottom && !messageTimer.value && msgQuery.user_id) {
|
||||||
|
// 在底部且无定时器,启动定时器
|
||||||
|
startMessageTimer()
|
||||||
|
} else if (!isNearBottom && messageTimer.value) {
|
||||||
|
// 不在底部且有定时器,停止定时器
|
||||||
|
stopMessageTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动到顶部时加载更多历史消息
|
||||||
if (scrollTop <= 0 && !loadingMore.value && !noMoreMessages.value) {
|
if (scrollTop <= 0 && !loadingMore.value && !noMoreMessages.value) {
|
||||||
loadMoreMessages()
|
loadMoreMessages()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载更多消息
|
// 加载更多历史消息
|
||||||
const loadMoreMessages = () => {
|
const loadMoreMessages = () => {
|
||||||
if (msgQuery.user_id) {
|
if (msgQuery.user_id && !loadingMore.value && !noMoreMessages.value) {
|
||||||
msgQuery.page++
|
msgQuery.page++
|
||||||
getMessages()
|
getMessages(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
const send = () => {
|
const send = () => {
|
||||||
if (!activeUser.value.user_id) {
|
if (!activeUser.value.user_id) {
|
||||||
ElMessage({ type: 'warning', message: '请先选择一个联系人' })
|
ElMessage({ type: 'warning', message: '请先选择一个联系人' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!draft.value) return
|
if (!draft.value) return
|
||||||
|
|
||||||
const content = draft.value
|
const content = draft.value
|
||||||
const id = Date.now() + Math.random()
|
const id = Date.now() + Math.random()
|
||||||
const msg = {
|
const msg = {
|
||||||
@ -168,9 +247,12 @@ const send = () => {
|
|||||||
}
|
}
|
||||||
messages.value.push(msg)
|
messages.value.push(msg)
|
||||||
draft.value = ''
|
draft.value = ''
|
||||||
// ensure scroll
|
|
||||||
|
// 发送后强制滚动到底部并设置自动滚动状态
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
// send to server
|
isAutoScroll.value = true
|
||||||
|
|
||||||
|
// 发送到服务器
|
||||||
send_message({
|
send_message({
|
||||||
app_id: route.query.app_id,
|
app_id: route.query.app_id,
|
||||||
content: JSON.stringify({
|
content: JSON.stringify({
|
||||||
@ -188,6 +270,7 @@ const send = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 键盘事件处理
|
||||||
const handleInputKeydown = (e) => {
|
const handleInputKeydown = (e) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@ -195,6 +278,7 @@ const handleInputKeydown = (e) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 图片上传相关
|
||||||
const triggerImageInput = () => {
|
const triggerImageInput = () => {
|
||||||
imageInput.value && imageInput.value.click()
|
imageInput.value && imageInput.value.click()
|
||||||
}
|
}
|
||||||
@ -202,7 +286,8 @@ const triggerImageInput = () => {
|
|||||||
const handleImageChange = (e) => {
|
const handleImageChange = (e) => {
|
||||||
const file = e.target.files[0]
|
const file = e.target.files[0]
|
||||||
if (!file) return
|
if (!file) return
|
||||||
// 直接用本地URL预览和发送
|
|
||||||
|
// 本地预览
|
||||||
const url = URL.createObjectURL(file)
|
const url = URL.createObjectURL(file)
|
||||||
const id = Date.now() + Math.random()
|
const id = Date.now() + Math.random()
|
||||||
const msg = {
|
const msg = {
|
||||||
@ -216,11 +301,13 @@ const handleImageChange = (e) => {
|
|||||||
send_time: '刚刚'
|
send_time: '刚刚'
|
||||||
}
|
}
|
||||||
messages.value.push(msg)
|
messages.value.push(msg)
|
||||||
// 清空input,允许重复选择同一图片
|
|
||||||
e.target.value = ''
|
e.target.value = ''
|
||||||
// 图片可能增加高度,延迟滚动以保证图片渲染
|
|
||||||
|
// 滚动到底部
|
||||||
setTimeout(() => scrollToBottom(), 100)
|
setTimeout(() => scrollToBottom(), 100)
|
||||||
// 上传 file to server using FormData
|
isAutoScroll.value = true
|
||||||
|
|
||||||
|
// 上传文件
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
form.append('file', file)
|
form.append('file', file)
|
||||||
form.append('app_id', route.query.app_id)
|
form.append('app_id', route.query.app_id)
|
||||||
@ -241,29 +328,46 @@ const handleImageChange = (e) => {
|
|||||||
ElMessage({ type: 'error', message: '图片发送失败' })
|
ElMessage({ type: 'error', message: '图片发送失败' })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重发消息
|
||||||
const handleRetry = (id) => {
|
const handleRetry = (id) => {
|
||||||
const msg = messages.value.find((m) => m._id === id)
|
const msg = messages.value.find((m) => m._id === id)
|
||||||
if (!msg) return
|
if (!msg) return
|
||||||
|
|
||||||
msg._failed = false
|
msg._failed = false
|
||||||
msg._sending = true
|
msg._sending = true
|
||||||
|
|
||||||
if (msg.msg_type === 0) {
|
if (msg.msg_type === 0) {
|
||||||
send_message({ app_id: route.query.app_id, content: msg.content.content, msg_type: 1, to_user_id: activeUser.value.user_id })
|
send_message({
|
||||||
.then(() => { msg._sending = false })
|
app_id: route.query.app_id,
|
||||||
.catch(() => { msg._sending = false; msg._failed = true; ElMessage({ type: 'error', message: '重发失败' }) })
|
content: msg.content.content,
|
||||||
|
msg_type: 1,
|
||||||
|
to_user_id: activeUser.value.user_id
|
||||||
|
}).then(() => {
|
||||||
|
msg._sending = false
|
||||||
|
}).catch(() => {
|
||||||
|
msg._sending = false;
|
||||||
|
msg._failed = true;
|
||||||
|
ElMessage({ type: 'error', message: '重发失败' })
|
||||||
|
})
|
||||||
} else if (msg.msg_type === 2) {
|
} else if (msg.msg_type === 2) {
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
form.append('file', msg.content.file)
|
form.append('file', msg.content.file)
|
||||||
form.append('app_id', route.query.app_id)
|
form.append('app_id', route.query.app_id)
|
||||||
form.append('msg_type', 2)
|
form.append('msg_type', 2)
|
||||||
form.append('to_user_id', activeUser.value.user_id)
|
form.append('to_user_id', activeUser.value.user_id)
|
||||||
send_message(form).then(() => { msg._sending = false }).catch(() => { msg._sending = false; msg._failed = true; ElMessage({ type: 'error', message: '重发失败' }) })
|
send_message(form).then(() => {
|
||||||
|
msg._sending = false
|
||||||
|
}).catch(() => {
|
||||||
|
msg._sending = false;
|
||||||
|
msg._failed = true;
|
||||||
|
ElMessage({ type: 'error', message: '重发失败' })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
const scrollToBottom = async () => {
|
const scrollToBottom = async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
const el = chatBody.value
|
const el = chatBody.value
|
||||||
@ -271,21 +375,16 @@ const scrollToBottom = async () => {
|
|||||||
try {
|
try {
|
||||||
el.scrollTop = el.scrollHeight
|
el.scrollTop = el.scrollHeight
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
// 忽略错误
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当 messages 变化时自动滚动(仅在首次加载或发送新消息时)
|
// 表情选择
|
||||||
// 移除原来的watcher,因为现在需要更精确地控制滚动行为
|
|
||||||
|
|
||||||
const onEmojiSelect = (emoji) => {
|
const onEmojiSelect = (emoji) => {
|
||||||
if (draft.value) {
|
draft.value = (draft.value || '') + emoji
|
||||||
draft.value += emoji
|
|
||||||
} else {
|
|
||||||
draft.value = emoji
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取用户列表
|
||||||
const params = reactive({
|
const params = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 20,
|
page_size: 20,
|
||||||
@ -295,47 +394,83 @@ const params = reactive({
|
|||||||
const getUsers = (append = false) => {
|
const getUsers = (append = false) => {
|
||||||
getUserList(params).then((res) => {
|
getUserList(params).then((res) => {
|
||||||
if (append) {
|
if (append) {
|
||||||
// append new items
|
|
||||||
userList.value = userList.value.concat(res.list || [])
|
userList.value = userList.value.concat(res.list || [])
|
||||||
} else {
|
} else {
|
||||||
userList.value = res.list || []
|
userList.value = res.list || []
|
||||||
}
|
}
|
||||||
// if first load and no active user, select first
|
// 首次加载且没有选中用户时,默认选中第一个
|
||||||
if (!append && userList.value.length && !activeUser.value.user_id) {
|
if (!append && userList.value.length && !activeUser.value.user_id) {
|
||||||
onSelectUser(userList.value[0].user_id)
|
onSelectUser(userList.value[0].user_id)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当 UserCard 触发 load(分页)时调用,加载下一页并滚动列表到底部以显示新数据
|
// 加载更多用户
|
||||||
const onLoadUserList = async () => {
|
const onLoadUserList = async () => {
|
||||||
params.page = params.page + 1
|
params.page = params.page + 1
|
||||||
await getUsers(true)
|
await getUsers(true)
|
||||||
// 等待 DOM 更新后滚动
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
const el = document.querySelector('.left-panel .infinite-list')
|
const el = document.querySelector('.left-panel .infinite-list')
|
||||||
if (el) {
|
if (el) {
|
||||||
// 滚动到底部以展示新加载的用户
|
|
||||||
el.scrollTop = el.scrollHeight
|
el.scrollTop = el.scrollHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendMessage = async (message) => {
|
// 定时器相关函数
|
||||||
send_message(message).then((res) => {
|
const startMessageTimer = () => {
|
||||||
}).catch((err) => {
|
// 清除现有定时器
|
||||||
ElMessage({ type: 'error', message: '消息发送失败' })
|
if (messageTimer.value) {
|
||||||
})
|
clearInterval(messageTimer.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每秒获取一次最新消息,不修改 msgQuery.page,使用 pageOverride=1
|
||||||
|
messageTimer.value = setInterval(() => {
|
||||||
|
if (!msgQuery.user_id) return
|
||||||
|
getMessages(false, 1)
|
||||||
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stopMessageTimer = () => {
|
||||||
|
if (messageTimer.value) {
|
||||||
|
clearInterval(messageTimer.value)
|
||||||
|
messageTimer.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const restartMessageTimer = () => {
|
||||||
|
stopMessageTimer()
|
||||||
|
if (msgQuery.user_id) {
|
||||||
|
startMessageTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getUsers()
|
getUsers()
|
||||||
|
|
||||||
|
// 监听窗口关闭,清理定时器
|
||||||
|
window.addEventListener('beforeunload', stopMessageTimer)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件卸载时清理定时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopMessageTimer()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听活跃用户变化,重启定时器
|
||||||
|
watch(activeUser, (newVal) => {
|
||||||
|
if (newVal.user_id) {
|
||||||
|
restartMessageTimer()
|
||||||
|
} else {
|
||||||
|
stopMessageTimer()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.chat-page {
|
.chat-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: calc(100vh - 60px);
|
height: calc(100vh - 64px);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-textarea__inner) {
|
:deep(.el-textarea__inner) {
|
||||||
@ -382,13 +517,37 @@ onMounted(() => {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background: #f5f7fa
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载更多样式
|
||||||
|
.loading-more {
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-footer {
|
.chat-footer {
|
||||||
height: 96px;
|
height: 96px;
|
||||||
padding: 5px 5px;
|
padding: 5px 15px;
|
||||||
border-top: 1px solid var(--el-border-color);
|
border-top: 1px solid var(--el-border-color);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-footer .footer-left {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-footer .el-button {
|
||||||
|
height: 40px;
|
||||||
|
align-self: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-footer .actions {
|
.chat-footer .actions {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user