This commit is contained in:
左哥 2025-10-19 21:27:04 +08:00
parent 279c2074db
commit c5b305d4ff
5 changed files with 633 additions and 88 deletions

View File

@ -1,5 +1,5 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'production'
VITE_APP_BASE_API = 'http://scrm.1024tool.vip/'
VITE_SERVE = "http://scrm.1024tool.vip/"
VITE_APP_BASE_API = 'https://mini-chat.1024tool.vip/'
VITE_SERVE = "https://mini-chat.1024tool.vip/"

12
components.d.ts vendored
View File

@ -12,13 +12,10 @@ declare module 'vue' {
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader']
@ -26,30 +23,21 @@ declare module 'vue' {
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElLink: typeof import('element-plus/es')['ElLink']
ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
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']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
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']
ElTour: typeof import('element-plus/es')['ElTour']
ElTourStep: typeof import('element-plus/es')['ElTourStep']
IndexUser: typeof import('./src/components/TagClass/indexUser.vue')['default']
KeyWords: typeof import('./src/components/KeyWords/index.vue')['default']
MaterialPublic: typeof import('./src/components/MaterialPublic/index.vue')['default']

View File

@ -31,7 +31,7 @@ const constantRoutes = [
path: '/',
name: 'Layout',
component: () => import('@/layout/index.vue'),
redirect: '/robot',
redirect: '/miniProgram',
meta: {
title: 'Layout',
},

View 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>

View File

@ -5,12 +5,15 @@
</div>
<div class="right-panel">
<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" />
</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;">
<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">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-tupian1"></use>
@ -23,13 +26,15 @@
<el-input type="textarea" v-model="draft" placeholder="输入消息..." rows="2" style="flex:1"
@keydown="handleInputKeydown" />
</div>
<!-- 添加发送按钮 -->
<!-- <el-button type="primary" @click="send">发送</el-button> -->
</div>
</div>
</div>
</template>
<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 UserCard from '@/components/UserCard/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 { useRoute } from 'vue-router'
import { uploadFile } from '@/api/upload'
import { ElMessage } from 'element-plus'
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: '' },
@ -53,105 +60,177 @@ 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,
page_size: 20,
app_id: route.query.app_id,
user_id: ''
})
//
//
const loadingMore = ref(false)
const noMoreMessages = ref(false)
const isAutoScroll = ref(true) //
const messageTimer = ref(null) //
//
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
// await
scrollToBottom()
getMessages()
//
restartMessageTimer()
}
}
//
const getMessages = () => {
if (loadingMore.value || noMoreMessages.value) return
let msgTotal = 0
//
// 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
get_messages({ ...msgQuery }).then(res => {
if (res.list && res.list.length > 0) {
//
const newMessages = res.list.map(item => {
item.content = JSON.parse(item.content)
const queryPage = pageOverride ?? msgQuery.page
const query = { ...msgQuery, page: queryPage }
try {
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
})
if (msgQuery.page === 1) {
//
messages.value = newMessages
if (isHistory) {
// prepend scrollHeight 便
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 {
//
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]
}
//
if (res.list.length < msgQuery.page_size) {
// 使 total total
if (msgTotal && isHistory) {
noMoreMessages.value = messages.value.length >= msgTotal
} else if (list.length < msgQuery.page_size && isHistory) {
// total退 page_size
noMoreMessages.value = true
}
} else {
//
//
if (!isHistory && isAutoScroll.value) {
await scrollToBottom()
}
} else if (isHistory) {
noMoreMessages.value = true
}
} catch (err) {
//
} finally {
loadingMore.value = false
loadingMore.value = false
}).catch(() => {
loadingMore.value = false
})
// prepend
try {
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 { 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) {
loadMoreMessages()
}
}
//
//
const loadMoreMessages = () => {
if (msgQuery.user_id) {
if (msgQuery.user_id && !loadingMore.value && !noMoreMessages.value) {
msgQuery.page++
getMessages()
getMessages(true)
}
}
//
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 = {
@ -168,9 +247,12 @@ const send = () => {
}
messages.value.push(msg)
draft.value = ''
// ensure scroll
//
scrollToBottom()
// send to server
isAutoScroll.value = true
//
send_message({
app_id: route.query.app_id,
content: JSON.stringify({
@ -188,6 +270,7 @@ const send = () => {
})
}
//
const handleInputKeydown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
@ -195,6 +278,7 @@ const handleInputKeydown = (e) => {
}
}
//
const triggerImageInput = () => {
imageInput.value && imageInput.value.click()
}
@ -202,7 +286,8 @@ const triggerImageInput = () => {
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 = {
@ -216,11 +301,13 @@ const handleImageChange = (e) => {
send_time: '刚刚'
}
messages.value.push(msg)
// input
e.target.value = ''
//
//
setTimeout(() => scrollToBottom(), 100)
// file to server using FormData
isAutoScroll.value = true
//
const form = new FormData()
form.append('file', file)
form.append('app_id', route.query.app_id)
@ -241,29 +328,46 @@ const handleImageChange = (e) => {
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: '重发失败' }) })
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: '重发失败' }) })
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
@ -271,21 +375,16 @@ const scrollToBottom = async () => {
try {
el.scrollTop = el.scrollHeight
} catch (e) {
// ignore
//
}
}
// messages
// watcher
//
const onEmojiSelect = (emoji) => {
if (draft.value) {
draft.value += emoji
} else {
draft.value = emoji
}
draft.value = (draft.value || '') + emoji
}
//
const params = reactive({
page: 1,
page_size: 20,
@ -295,47 +394,83 @@ const params = reactive({
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: '消息发送失败' })
})
//
const startMessageTimer = () => {
//
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(() => {
getUsers()
//
window.addEventListener('beforeunload', stopMessageTimer)
})
//
onUnmounted(() => {
stopMessageTimer()
})
//
watch(activeUser, (newVal) => {
if (newVal.user_id) {
restartMessageTimer()
} else {
stopMessageTimer()
}
})
</script>
<style scoped lang="scss">
.chat-page {
display: flex;
height: calc(100vh - 60px);
height: calc(100vh - 64px);
}
:deep(.el-textarea__inner) {
@ -382,13 +517,37 @@ onMounted(() => {
flex: 1;
padding: 16px;
overflow: auto;
background: #f5f7fa
background: #f5f7fa;
}
//
.loading-more {
text-align: center;
padding: 10px;
color: #666;
font-size: 14px;
}
.chat-footer {
height: 96px;
padding: 5px 5px;
padding: 5px 15px;
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 {