mirror of
https://gitee.com/lijingbo-2021/open-anylink-web.git
synced 2025-12-30 11:02:25 +00:00
发送时立即渲染本地消息
This commit is contained in:
@@ -29,3 +29,11 @@ export const msgSendStatus = {
|
||||
OK: 'ok', // 发送成功
|
||||
FAILED: 'failed' // 发送失败
|
||||
}
|
||||
|
||||
// 消息中文件的上传状态
|
||||
export const msgFileUploadStatus = {
|
||||
UPLOAD_DEFAULT: 0, // 默认状态,不上传
|
||||
UPLOADING: 1, // 上传中
|
||||
UPLOAD_SUCCESS: 2, // 上传成功
|
||||
UPLOAD_FAILED: 3 // 上传失败
|
||||
}
|
||||
|
||||
89
src/models/message.js
Normal file
89
src/models/message.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import { msgSendStatus } from '@/const/msgConst'
|
||||
|
||||
/**
|
||||
* 消息渲染及缓存时用到实体类,收录所有可能用到的属性,目前作为参考用,并未实际调用
|
||||
*/
|
||||
class Message {
|
||||
/**
|
||||
* 会话内唯一消息Id
|
||||
*/
|
||||
msgId
|
||||
|
||||
/**
|
||||
* 消息序列号
|
||||
*/
|
||||
seq
|
||||
|
||||
/**
|
||||
* 消息所属的会话ID
|
||||
*/
|
||||
sessionId
|
||||
|
||||
/**
|
||||
* 消息发送ID
|
||||
*/
|
||||
fromId
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
msgType
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
content
|
||||
|
||||
/**
|
||||
* 消息状态:发送中,发送成功,发送失败
|
||||
*/
|
||||
status
|
||||
|
||||
/**
|
||||
* 接收消息的时间
|
||||
*/
|
||||
msgTime
|
||||
|
||||
/**
|
||||
* 消息发送的时间,发送消息时才需要填
|
||||
*/
|
||||
sendTime
|
||||
|
||||
/**
|
||||
* 消息中文件的上传状态
|
||||
*/
|
||||
uploadStatus
|
||||
|
||||
/**
|
||||
* 消息中文件的上传进度
|
||||
*/
|
||||
uploadProgress
|
||||
|
||||
constructor(
|
||||
sessionId,
|
||||
fromId,
|
||||
msgType,
|
||||
content,
|
||||
msgTime,
|
||||
sendTime = undefined,
|
||||
msgId = undefined,
|
||||
seq = undefined,
|
||||
status = msgSendStatus.PENDING,
|
||||
uploadStatus = undefined,
|
||||
uploadProgress = undefined
|
||||
) {
|
||||
this.msgId = msgId
|
||||
this.seq = seq
|
||||
this.sessionId = sessionId
|
||||
this.fromId = fromId
|
||||
this.msgType = msgType
|
||||
this.content = content
|
||||
this.status = status
|
||||
this.msgTime = msgTime
|
||||
this.sendTime = sendTime
|
||||
this.uploadStatus = uploadStatus
|
||||
this.uploadProgress = uploadProgress
|
||||
}
|
||||
}
|
||||
|
||||
export { Message }
|
||||
@@ -21,7 +21,15 @@ export const useImageStore = defineStore('anylink-image', () => {
|
||||
*/
|
||||
const imageInSession = ref({})
|
||||
|
||||
const setImage = (sessionId, obj) => {
|
||||
/**
|
||||
* 本地图片只是临时的,不用放进imageInSession
|
||||
* @param {*} obj
|
||||
*/
|
||||
const setLocalImage = (obj) => {
|
||||
image.value[obj.objectId] = obj
|
||||
}
|
||||
|
||||
const setServerImage = (sessionId, obj) => {
|
||||
image.value[obj.objectId] = obj
|
||||
if (!imageInSession.value[sessionId]) {
|
||||
imageInSession.value[sessionId] = []
|
||||
@@ -29,7 +37,7 @@ export const useImageStore = defineStore('anylink-image', () => {
|
||||
imageInSession.value[sessionId].push(obj.objectId)
|
||||
}
|
||||
|
||||
const imageTrans = (content, maxWidth = 400, maxHeight = 300) => {
|
||||
const imageTrans = (content, maxWidth = 360, maxHeight = 180) => {
|
||||
const matches = content.match(pattern)
|
||||
if (!matches || matches.length === 0) {
|
||||
return content
|
||||
@@ -68,7 +76,7 @@ export const useImageStore = defineStore('anylink-image', () => {
|
||||
if (imageIds.size > 0) {
|
||||
const res = await mtsImageService({ objectIds: [...imageIds].join(',') })
|
||||
res.data.data.forEach((item) => {
|
||||
setImage(sessionId, item) // 缓存image数据
|
||||
setServerImage(sessionId, item) // 缓存image数据
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -101,7 +109,7 @@ export const useImageStore = defineStore('anylink-image', () => {
|
||||
if (imageIds.size > 0) {
|
||||
const res = await mtsImageService({ objectIds: [...imageIds].join(',') })
|
||||
res.data.data.forEach((item) => {
|
||||
setImage(sessionId, item)
|
||||
setServerImage(sessionId, item)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -109,7 +117,8 @@ export const useImageStore = defineStore('anylink-image', () => {
|
||||
return {
|
||||
image,
|
||||
imageInSession,
|
||||
setImage,
|
||||
setLocalImage,
|
||||
setServerImage,
|
||||
imageTrans,
|
||||
loadImageInfoFromContent,
|
||||
preloadImage
|
||||
|
||||
@@ -117,7 +117,7 @@ export const useMessageStore = defineStore('anylink-message', () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话列表中加入新的消息数组
|
||||
* 对话列表中加入新的消息数组(预加载资源)
|
||||
* @param {*} sessionId 会话id
|
||||
* @param {*} msgRecords 新的消息数组
|
||||
*/
|
||||
@@ -128,6 +128,15 @@ export const useMessageStore = defineStore('anylink-message', () => {
|
||||
await useVideoStore().preloadVideo(sessionId, msgRecords)
|
||||
await useDocumentStore().preloadDocument(sessionId, msgRecords)
|
||||
|
||||
addMsgRecordsWithOutPreLoad(sessionId, msgRecords)
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话列表中加入新的消息数组
|
||||
* @param {*} sessionId 会话id
|
||||
* @param {*} msgRecords 新的消息数组
|
||||
*/
|
||||
const addMsgRecordsWithOutPreLoad = (sessionId, msgRecords) => {
|
||||
if (!msgRecords?.length) return
|
||||
msgRecords.forEach((item) => {
|
||||
if (!msgRecordsList.value[sessionId]) {
|
||||
@@ -272,6 +281,7 @@ export const useMessageStore = defineStore('anylink-message', () => {
|
||||
msgRecordsList,
|
||||
msgIdSortArray,
|
||||
addMsgRecords,
|
||||
addMsgRecordsWithOutPreLoad,
|
||||
removeMsgRecord,
|
||||
getMsg,
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ArrowDownBold,
|
||||
ArrowUp
|
||||
} from '@element-plus/icons-vue'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import DragLine from '@/components/common/DragLine.vue'
|
||||
import SearchBox from '@/components/search/SearchBox.vue'
|
||||
import AddButton from '@/components/common/AddButton.vue'
|
||||
@@ -478,7 +479,7 @@ const handleRead = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleSendMessage = (content, resendSeq = '') => {
|
||||
const handleSendMessage = (msg) => {
|
||||
if (isNotInGroup.value) {
|
||||
ElMessage.warning('您已离开该群或群已被解散')
|
||||
return
|
||||
@@ -490,14 +491,16 @@ const handleSendMessage = (content, resendSeq = '') => {
|
||||
|
||||
if (inputToolBarRef.value) inputToolBarRef.value.closeWindow()
|
||||
|
||||
const msg = {
|
||||
sessionId: selectedSessionId.value,
|
||||
fromId: myAccount.value,
|
||||
msgType: selectedSession.value.sessionType,
|
||||
content: content,
|
||||
status: msgSendStatus.PENDING,
|
||||
msgTime: new Date(),
|
||||
sendTime: new Date()
|
||||
if (typeof msg === 'string') {
|
||||
msg = {
|
||||
sessionId: selectedSessionId.value,
|
||||
fromId: myAccount.value,
|
||||
msgType: selectedSession.value.sessionType,
|
||||
content: msg,
|
||||
status: msgSendStatus.PENDING,
|
||||
msgTime: new Date(),
|
||||
sendTime: new Date()
|
||||
}
|
||||
}
|
||||
|
||||
const resendInterval = 2000 //2秒
|
||||
@@ -556,8 +559,8 @@ const handleSendMessage = (content, resendSeq = '') => {
|
||||
msg.sessionId,
|
||||
showId.value,
|
||||
selectedSession.value.sessionType,
|
||||
content,
|
||||
resendSeq,
|
||||
msg.content,
|
||||
msg.seq,
|
||||
before,
|
||||
after
|
||||
)
|
||||
@@ -567,8 +570,12 @@ const handleSendMessage = (content, resendSeq = '') => {
|
||||
locateSession(msg.sessionId)
|
||||
}
|
||||
|
||||
const handleResendMessage = ({ content, seq }) => {
|
||||
handleSendMessage(content, seq)
|
||||
const handleResendMessage = (msg) => {
|
||||
// 重发消息时更新这三个属性,其他不变
|
||||
msg.status = msgSendStatus.PENDING
|
||||
msg.msgTime = new Date()
|
||||
msg.sendTime = new Date()
|
||||
handleSendMessage(msg)
|
||||
}
|
||||
|
||||
const onLoadMore = async () => {
|
||||
@@ -982,24 +989,27 @@ const onSendEmoji = (key) => {
|
||||
inputEditorRef.value.addEmoji(key)
|
||||
}
|
||||
|
||||
const onSendImage = ({ objectId }) => {
|
||||
handleSendMessage(JSON.stringify({ type: msgContentType.IMAGE, value: objectId }))
|
||||
}
|
||||
|
||||
const onSendAudio = ({ objectId }) => {
|
||||
handleSendMessage(JSON.stringify({ type: msgContentType.AUDIO, value: objectId }))
|
||||
}
|
||||
|
||||
const onSendRecording = ({ objectId }) => {
|
||||
handleSendMessage(JSON.stringify({ type: msgContentType.RECORDING, value: objectId }))
|
||||
}
|
||||
|
||||
const onSendVideo = ({ objectId }) => {
|
||||
handleSendMessage(JSON.stringify({ type: msgContentType.VIDEO, value: objectId }))
|
||||
}
|
||||
|
||||
const onSendDocument = ({ objectId }) => {
|
||||
handleSendMessage(JSON.stringify({ type: msgContentType.DOCUMENT, value: objectId }))
|
||||
/**
|
||||
* 发送时先添加本地消息,可以立即渲染
|
||||
*/
|
||||
const handleLocalMsg = ({ content, contentType, objectId, fn }) => {
|
||||
const seq = uuidv4()
|
||||
const msg = {
|
||||
msgId: seq,
|
||||
seq: seq,
|
||||
sessionId: selectedSessionId.value,
|
||||
fromId: myAccount.value,
|
||||
msgType: selectedSession.value.sessionType,
|
||||
content:
|
||||
contentType === msgContentType.MIX
|
||||
? content
|
||||
: JSON.stringify({ type: contentType, value: objectId }),
|
||||
status: msgSendStatus.PENDING,
|
||||
msgTime: new Date(),
|
||||
sendTime: new Date()
|
||||
}
|
||||
messageData.addMsgRecordsWithOutPreLoad(msg.sessionId, [msg])
|
||||
fn(msg)
|
||||
}
|
||||
|
||||
const inputRecorderRef = ref(null)
|
||||
@@ -1193,7 +1203,8 @@ const onShowRecorder = () => {
|
||||
ref="inputRecorderRef"
|
||||
:sessionId="selectedSessionId"
|
||||
@exit="isShowRecorder = false"
|
||||
@sendRecording="onSendRecording"
|
||||
@saveLocalMsg="handleLocalMsg"
|
||||
@sendMessage="handleSendMessage"
|
||||
></InputRecorder>
|
||||
</el-container>
|
||||
<el-container v-else class="input-box-container">
|
||||
@@ -1203,11 +1214,9 @@ const onShowRecorder = () => {
|
||||
:sessionId="selectedSessionId"
|
||||
:isShowToolSet="!isNotInGroup"
|
||||
@sendEmoji="onSendEmoji"
|
||||
@sendImage="onSendImage"
|
||||
@sendAudio="onSendAudio"
|
||||
@sendVideo="onSendVideo"
|
||||
@sendDocument="onSendDocument"
|
||||
@showRecorder="onShowRecorder"
|
||||
@saveLocalMsg="handleLocalMsg"
|
||||
@sendMessage="handleSendMessage"
|
||||
></InputToolBar>
|
||||
</el-header>
|
||||
<el-main class="input-box-main">
|
||||
@@ -1230,6 +1239,7 @@ const onShowRecorder = () => {
|
||||
ref="inputEditorRef"
|
||||
:sessionId="selectedSessionId"
|
||||
:draft="selectedSession.draft || ''"
|
||||
@saveLocalMsg="handleLocalMsg"
|
||||
@sendMessage="handleSendMessage"
|
||||
></InputEditor>
|
||||
</el-main>
|
||||
|
||||
@@ -8,20 +8,16 @@ const emits = defineEmits(['load'])
|
||||
|
||||
const onLoad = (e) => {
|
||||
const img = e.target
|
||||
const ratio = img.naturalWidth / img.naturalHeight
|
||||
const maxRatio = 300 / 200 // 最大宽高比
|
||||
const maxWidth = 360
|
||||
const maxHeight = 180
|
||||
|
||||
// 如果图片尺寸在限制范围内,保持原始尺寸
|
||||
if (img.naturalWidth <= 300 && img.naturalHeight <= 200) {
|
||||
img.style.width = img.naturalWidth + 'px'
|
||||
img.style.height = img.naturalHeight + 'px'
|
||||
} else if (ratio > maxRatio) {
|
||||
if (img.naturalWidth / img.naturalHeight > maxWidth / maxHeight) {
|
||||
// 如果图片更宽,以宽度为基准
|
||||
img.style.width = '300px'
|
||||
img.style.width = maxWidth + 'px'
|
||||
img.style.height = 'auto'
|
||||
} else {
|
||||
// 如果图片更高,以高度为基准
|
||||
img.style.height = '200px'
|
||||
img.style.height = maxHeight + 'px'
|
||||
img.style.width = 'auto'
|
||||
}
|
||||
|
||||
@@ -63,8 +59,6 @@ const formatSize = computed(() => {
|
||||
position: relative;
|
||||
|
||||
.el-image {
|
||||
max-width: 300px;
|
||||
max-height: 200px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@ import '@vueup/vue-quill/dist/vue-quill.snow.css'
|
||||
import { onMounted, onUnmounted, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useMessageStore, useImageStore } from '@/stores'
|
||||
import { ElMessage, ElLoading } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { emojiTrans, getEmojiHtml } from '@/js/utils/emojis'
|
||||
import { base64ToFile } from '@/js/utils/common'
|
||||
import { mtsUploadService } from '@/api/mts'
|
||||
import { el_loading_options } from '@/const/commonConst'
|
||||
import { msgContentType, msgFileUploadStatus } from '@/const/msgConst'
|
||||
|
||||
const props = defineProps(['sessionId', 'draft'])
|
||||
const emit = defineEmits(['sendMessage'])
|
||||
const emit = defineEmits(['saveLocalMsg', 'sendMessage'])
|
||||
const messageData = useMessageStore()
|
||||
const imageData = useImageStore()
|
||||
|
||||
@@ -37,15 +37,36 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
onBeforeUnmount(async () => {
|
||||
let content = await getContent()
|
||||
// 草稿若没发生变动,则不触发存储
|
||||
const contentObj = parseContent()
|
||||
const draft = messageData.sessionList[props.sessionId]?.draft
|
||||
const content = contentObj.contentFromLocal.join('').trim()
|
||||
// 草稿若发生变动,则触发存储
|
||||
if (content && draft && content !== draft) {
|
||||
messageData.updateSession({
|
||||
sessionId: props.sessionId,
|
||||
draft: content
|
||||
})
|
||||
}
|
||||
|
||||
// 有图片需要上传,再保存一次draft
|
||||
if (contentObj.needUploadCount.value > 0) {
|
||||
const stopWatch = watch(
|
||||
() => contentObj.uploadedTotalCount.value,
|
||||
() => {
|
||||
if (contentObj.needUploadCount.value === contentObj.uploadedTotalCount.value) {
|
||||
// 满足第一个相等条件就停止监视
|
||||
stopWatch()
|
||||
if (contentObj.uploadSuccessCount.value === contentObj.needUploadCount.value) {
|
||||
// 满足第二个相等条件才保存草稿
|
||||
messageData.updateSession({
|
||||
sessionId: props.sessionId,
|
||||
draft: contentObj.contentFromServer.join('').trim()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -58,54 +79,114 @@ onUnmounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const getContent = async () => {
|
||||
const parseContent = (sessionId = props.sessionId) => {
|
||||
const delta = getQuill().getContents()
|
||||
let content = ''
|
||||
let contentFromLocal = new Array(delta.ops.length).fill('')
|
||||
let contentFromServer = new Array(delta.ops.length).fill('')
|
||||
let needUploadCount = ref(0) // 需要上传的图片个数
|
||||
let uploadedTotalCount = ref(0) // 已发上传请求的图片个数,包括上传成功和失败
|
||||
let uploadSuccessCount = ref(0) // 已经上传成功的图片个数
|
||||
for (let index = 0; index < delta.ops.length; index++) {
|
||||
const op = delta.ops[index]
|
||||
const insert = op.insert
|
||||
if (insert && typeof insert === 'string') {
|
||||
// 文本
|
||||
content = content + insert
|
||||
contentFromLocal[index] = insert
|
||||
contentFromServer[index] = insert
|
||||
} else if (insert && insert.image) {
|
||||
const alt = op.attributes?.alt
|
||||
if (alt && alt.startsWith('[') && alt.endsWith(']')) {
|
||||
// 表情
|
||||
content = content + alt
|
||||
// 表情id
|
||||
contentFromLocal[index] = alt
|
||||
contentFromServer[index] = alt
|
||||
} else if (alt && alt.startsWith('{') && alt.endsWith('}')) {
|
||||
// 图片
|
||||
content = content + alt
|
||||
// 图片id
|
||||
contentFromLocal[index] = alt
|
||||
contentFromServer[index] = alt
|
||||
} else if (insert.image.startsWith('data:') && insert.image.includes('base64')) {
|
||||
// base64编码的图片
|
||||
needUploadCount.value++
|
||||
const file = base64ToFile(insert.image, uuidv4()) // base64转file
|
||||
el_loading_options.text = '图片上传中...' //上传中加一个loading效果
|
||||
const loadingInstance = ElLoading.service(el_loading_options)
|
||||
try {
|
||||
const res = await mtsUploadService({ file: file, storeType: 1 }) //上传图片至服务端
|
||||
imageData.setImage(props.sessionId, res.data.data) // 缓存image数据
|
||||
content = content + `{${res.data.data.objectId}}`
|
||||
} finally {
|
||||
loadingInstance.close()
|
||||
}
|
||||
const tempObjectId = new Date().getTime()
|
||||
// 发送的时候设置本地缓存(非服务端数据),用于立即渲染
|
||||
const localSrc = URL.createObjectURL(file)
|
||||
imageData.setLocalImage({
|
||||
objectId: tempObjectId,
|
||||
originUrl: localSrc,
|
||||
thumbUrl: localSrc,
|
||||
fileName: file.name,
|
||||
size: file.size
|
||||
})
|
||||
contentFromLocal[index] = `{${tempObjectId}}`
|
||||
|
||||
//上传图片至服务端
|
||||
mtsUploadService({ file: file, storeType: 1 })
|
||||
.then((res) => {
|
||||
imageData.setServerImage(sessionId, res.data.data) // 缓存image数据
|
||||
uploadSuccessCount.value++
|
||||
contentFromServer[index] = `{${res.data.data.objectId}}`
|
||||
// TODO 这里要判断是最后一个上传的图片
|
||||
// 这里用异步有个问题,后面请求上传的图片传的块,在content中就会跑到前面去,图片的顺序会错乱
|
||||
// 可以把content设成一个数组,按照index下标给每个数组元素设置,防止乱序
|
||||
// 这样也可以watch每个元素如果都填满,就sendMessage
|
||||
})
|
||||
.finally(() => {
|
||||
uploadedTotalCount.value++
|
||||
})
|
||||
} else {
|
||||
// 当文本处理
|
||||
contentFromLocal[index] = insert
|
||||
contentFromServer[index] = insert
|
||||
}
|
||||
}
|
||||
}
|
||||
return content.trim()
|
||||
|
||||
return {
|
||||
needUploadCount: needUploadCount,
|
||||
uploadedTotalCount: uploadedTotalCount,
|
||||
uploadSuccessCount: uploadSuccessCount,
|
||||
contentFromLocal: contentFromLocal,
|
||||
contentFromServer: contentFromServer
|
||||
}
|
||||
}
|
||||
|
||||
// 监控session发生了切换
|
||||
watch(
|
||||
() => props.sessionId,
|
||||
async (newValue, oldValue) => {
|
||||
let content = await getContent()
|
||||
// 草稿若没发生变动,则不触发存储
|
||||
if (oldValue && content !== messageData.sessionList[oldValue].draft) {
|
||||
async (newSessionId, oldSessionId) => {
|
||||
const contentObj = parseContent(oldSessionId)
|
||||
const content = contentObj.contentFromLocal.join('').trim()
|
||||
// 草稿若发生变动,则触发存储
|
||||
if (oldSessionId && content !== messageData.sessionList[oldSessionId].draft) {
|
||||
messageData.updateSession({
|
||||
sessionId: oldValue,
|
||||
sessionId: oldSessionId,
|
||||
draft: content
|
||||
})
|
||||
}
|
||||
formatContent(messageData.sessionList[newValue].draft || '')
|
||||
|
||||
// 有图片需要上传,再保存一次draft
|
||||
if (contentObj.needUploadCount.value > 0) {
|
||||
const stopWatch = watch(
|
||||
() => contentObj.uploadedTotalCount.value,
|
||||
() => {
|
||||
if (contentObj.needUploadCount.value === contentObj.uploadedTotalCount.value) {
|
||||
// 满足第一个相等条件就停止监视
|
||||
stopWatch()
|
||||
if (contentObj.uploadSuccessCount.value === contentObj.needUploadCount.value) {
|
||||
// 满足第二个相等条件才保存草稿
|
||||
if (oldSessionId) {
|
||||
messageData.updateSession({
|
||||
sessionId: oldSessionId,
|
||||
draft: contentObj.contentFromServer.join('').trim()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
formatContent(messageData.sessionList[newSessionId].draft || '')
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
@@ -120,16 +201,56 @@ const formatContent = (content) => {
|
||||
}
|
||||
|
||||
const handleEnter = async () => {
|
||||
const content = await getContent()
|
||||
const contentObj = parseContent()
|
||||
const content = contentObj.contentFromLocal.join('').trim()
|
||||
if (!content) {
|
||||
ElMessage.warning('请勿发送空内容')
|
||||
getQuill().setText('')
|
||||
return
|
||||
} else if (content.length > 3000) {
|
||||
ElMessage.warning('发送内容请不要超过3000个字')
|
||||
} else {
|
||||
emit('sendMessage', content)
|
||||
getQuill().setText('')
|
||||
return
|
||||
}
|
||||
|
||||
if (contentObj.needUploadCount.value === 0) {
|
||||
emit('sendMessage', content)
|
||||
} else {
|
||||
// 发送的时候设置本地缓存(非服务端数据),用于立即渲染
|
||||
let msg = {}
|
||||
emit('saveLocalMsg', {
|
||||
contentType: msgContentType.MIX,
|
||||
content: content,
|
||||
fn: (result) => {
|
||||
msg = result
|
||||
}
|
||||
})
|
||||
|
||||
// 有图片需要上传
|
||||
if (contentObj.needUploadCount.value > 0) {
|
||||
msg.uploadStatus = msgFileUploadStatus.UPLOADING
|
||||
msg.uploadProgress = 0
|
||||
}
|
||||
// 监视图片上传结果,图片上传完后向服务器发送消息
|
||||
const stopWatch = watch(
|
||||
() => contentObj.uploadedTotalCount.value,
|
||||
() => {
|
||||
msg.uploadProgress = Math.floor(
|
||||
(contentObj.uploadSuccessCount.value / contentObj.needUploadCount.value) * 100
|
||||
)
|
||||
if (contentObj.uploadedTotalCount.value === contentObj.needUploadCount.value) {
|
||||
stopWatch()
|
||||
if (contentObj.uploadSuccessCount.value === contentObj.needUploadCount.value) {
|
||||
msg.uploadStatus = msgFileUploadStatus.UPLOAD_SUCCESS
|
||||
msg.content = contentObj.contentFromServer.join('').trim()
|
||||
emit('sendMessage', msg)
|
||||
}
|
||||
} else {
|
||||
msg.uploadStatus = msgFileUploadStatus.UPLOAD_FAILED
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
getQuill().setText('') // 编辑窗口置空
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
|
||||
import { Microphone } from '@element-plus/icons-vue'
|
||||
import { ElLoading, ElMessage } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAudioStore } from '@/stores'
|
||||
import { mtsUploadService } from '@/api/mts'
|
||||
import { el_loading_options } from '@/const/commonConst'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { msgContentType, msgFileUploadStatus } from '@/const/msgConst'
|
||||
|
||||
const props = defineProps(['sessionId'])
|
||||
const emit = defineEmits(['exit', 'sendRecording'])
|
||||
const emit = defineEmits(['exit', 'sendMessage', 'saveLocalMsg'])
|
||||
|
||||
const audioData = useAudioStore()
|
||||
const spaceDown = ref(false) // 空格键是否被按下
|
||||
@@ -144,18 +144,47 @@ const stopRecording = () => {
|
||||
}
|
||||
|
||||
const uploadRecord = () => {
|
||||
const loadingInstance = ElLoading.service(el_loading_options)
|
||||
const fileName = `${uuidv4()}.${fileSuffix}`
|
||||
const file = new File([recordBlob.value], fileName, { type: recordType })
|
||||
mtsUploadService({ file, storeType: 1, duration: Math.floor(recordDuration / 1000) })
|
||||
|
||||
// 发送的时候设置本地缓存(非服务端数据),用于立即渲染
|
||||
const duration = Math.floor(recordDuration / 1000)
|
||||
const localSrc = URL.createObjectURL(file)
|
||||
const tempObjectId = new Date().getTime()
|
||||
audioData.setAudio(props.sessionId, {
|
||||
objectId: tempObjectId,
|
||||
duration: duration,
|
||||
url: localSrc,
|
||||
fileName: file.name,
|
||||
size: file.size
|
||||
})
|
||||
let msg = {}
|
||||
emit('saveLocalMsg', {
|
||||
contentType: msgContentType.RECORDING,
|
||||
objectId: tempObjectId,
|
||||
fn: (result) => {
|
||||
msg = result
|
||||
}
|
||||
})
|
||||
msg.uploadStatus = msgFileUploadStatus.UPLOADING
|
||||
msg.uploadProgress = 0
|
||||
|
||||
mtsUploadService({ file, storeType: 1, duration: duration })
|
||||
.then((res) => {
|
||||
if (res.data.code === 0) {
|
||||
audioData.setAudio(props.sessionId, res.data.data) // 缓存audio的数据
|
||||
emit('sendRecording', res.data.data)
|
||||
audioData.setAudio(props.sessionId, res.data.data) // 缓存服务端响应的audio数据
|
||||
msg.uploadStatus = msgFileUploadStatus.UPLOAD_SUCCESS
|
||||
msg.uploadProgress = 100
|
||||
msg.content = JSON.stringify({
|
||||
type: msgContentType.RECORDING,
|
||||
value: res.data.data.objectId
|
||||
})
|
||||
emit('sendMessage', msg)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loadingInstance.close()
|
||||
.catch(() => {
|
||||
msg.uploadStatus = msgFileUploadStatus.UPLOAD_FAILED
|
||||
ElMessage.error('上传失败')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Clock, Microphone } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElLoading } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import EmojiIcon from '@/assets/svg/emoji.svg'
|
||||
import FileIcon from '@/assets/svg/file.svg'
|
||||
import ImageIcon from '@/assets/svg/image.svg'
|
||||
@@ -17,18 +17,11 @@ import {
|
||||
useVideoStore,
|
||||
useDocumentStore
|
||||
} from '@/stores'
|
||||
import { el_loading_options } from '@/const/commonConst'
|
||||
import { MsgType } from '@/proto/msg'
|
||||
import { msgContentType, msgFileUploadStatus } from '@/const/msgConst'
|
||||
|
||||
const props = defineProps(['sessionId', 'isShowToolSet'])
|
||||
const emit = defineEmits([
|
||||
'sendEmoji',
|
||||
'sendImage',
|
||||
'sendAudio',
|
||||
'sendVideo',
|
||||
'sendDocument',
|
||||
'showRecorder'
|
||||
])
|
||||
const emit = defineEmits(['sendEmoji', 'showRecorder', 'sendMessage', 'saveLocalMsg'])
|
||||
|
||||
const messageData = useMessageStore()
|
||||
const imageData = useImageStore()
|
||||
@@ -42,57 +35,110 @@ const onSelectedFile = (file) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (file.raw.type && file.raw.type.startsWith('image/')) {
|
||||
const loadingInstance = ElLoading.service(el_loading_options)
|
||||
mtsUploadService({ file: file.raw, storeType: 1 })
|
||||
.then((res) => {
|
||||
if (res.data.code === 0) {
|
||||
imageData.setImage(props.sessionId, res.data.data) // 缓存image数据
|
||||
emit('sendImage', res.data.data)
|
||||
}
|
||||
let contentType = msgContentType.DOCUMENT
|
||||
if (file.raw.type.startsWith('image/')) {
|
||||
contentType = msgContentType.IMAGE
|
||||
} else if (file.raw.type.startsWith('audio/')) {
|
||||
contentType = msgContentType.AUDIO
|
||||
} else if (file.raw.type.startsWith('video/')) {
|
||||
contentType = msgContentType.VIDEO
|
||||
}
|
||||
|
||||
setLocalData(contentType, file)
|
||||
let msg = {}
|
||||
emit('saveLocalMsg', {
|
||||
contentType: contentType,
|
||||
objectId: file.uid,
|
||||
fn: (result) => {
|
||||
msg = result
|
||||
}
|
||||
})
|
||||
msg.uploadStatus = msgFileUploadStatus.UPLOADING
|
||||
msg.uploadProgress = 0
|
||||
|
||||
mtsUploadService({ file: file.raw, storeType: 1 })
|
||||
.then((res) => {
|
||||
if (res.data.code === 0) {
|
||||
setStoreData(contentType, res.data.data)
|
||||
msg.uploadStatus = msgFileUploadStatus.UPLOAD_SUCCESS
|
||||
msg.uploadProgress = 100
|
||||
msg.content = JSON.stringify({ type: contentType, value: res.data.data.objectId })
|
||||
emit('sendMessage', msg)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
msg.uploadStatus = msgFileUploadStatus.UPLOAD_FAILED
|
||||
ElMessage.error('上传失败')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送的时候设置本地缓存(非服务端数据),用于立即渲染
|
||||
* @param contentType
|
||||
* @param file
|
||||
*/
|
||||
const setLocalData = (contentType, file) => {
|
||||
const localSrc = URL.createObjectURL(file.raw)
|
||||
switch (contentType) {
|
||||
case msgContentType.IMAGE:
|
||||
imageData.setLocalImage({
|
||||
objectId: file.uid,
|
||||
originUrl: localSrc,
|
||||
thumbUrl: localSrc,
|
||||
fileName: file.name,
|
||||
size: file.raw.size
|
||||
})
|
||||
.finally(() => {
|
||||
loadingInstance.close()
|
||||
break
|
||||
case msgContentType.AUDIO:
|
||||
audioData.setAudio(props.sessionId, {
|
||||
objectId: file.uid,
|
||||
url: localSrc,
|
||||
fileName: file.name,
|
||||
size: file.raw.size
|
||||
})
|
||||
} else if (file.raw.type && file.raw.type.startsWith('audio/')) {
|
||||
const loadingInstance = ElLoading.service(el_loading_options)
|
||||
mtsUploadService({ file: file.raw, storeType: 1 })
|
||||
.then((res) => {
|
||||
if (res.data.code === 0) {
|
||||
audioData.setAudio(props.sessionId, res.data.data) // 缓存audio的数据
|
||||
emit('sendAudio', res.data.data)
|
||||
}
|
||||
break
|
||||
case msgContentType.VIDEO:
|
||||
videoData.setVideo(props.sessionId, {
|
||||
objectId: file.uid,
|
||||
url: localSrc,
|
||||
fileName: file.name,
|
||||
size: file.raw.size
|
||||
})
|
||||
.finally(() => {
|
||||
loadingInstance.close()
|
||||
})
|
||||
} else if (file.raw.type && file.raw.type.startsWith('video/')) {
|
||||
const loadingInstance = ElLoading.service(el_loading_options)
|
||||
mtsUploadService({ file: file.raw, storeType: 1 })
|
||||
.then((res) => {
|
||||
if (res.data.code === 0) {
|
||||
videoData.setVideo(props.sessionId, res.data.data) // 缓存video的数据
|
||||
emit('sendVideo', res.data.data)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loadingInstance.close()
|
||||
})
|
||||
} else {
|
||||
const loadingInstance = ElLoading.service(el_loading_options)
|
||||
mtsUploadService({ file: file.raw, storeType: 1 })
|
||||
.then((res) => {
|
||||
if (res.data.code === 0) {
|
||||
documentData.setDocument(props.sessionId, res.data.data) // 缓存video的数据
|
||||
emit('sendDocument', res.data.data)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loadingInstance.close()
|
||||
break
|
||||
case msgContentType.DOCUMENT:
|
||||
default:
|
||||
documentData.setDocument(props.sessionId, {
|
||||
objectId: file.uid,
|
||||
documentType: file.raw.type,
|
||||
url: localSrc,
|
||||
fileName: file.name,
|
||||
size: file.raw.size
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务端响应数据回来后,设置store缓存
|
||||
* @param contentType
|
||||
* @param file
|
||||
*/
|
||||
const setStoreData = (contentType, data) => {
|
||||
switch (contentType) {
|
||||
case msgContentType.IMAGE:
|
||||
imageData.setServerImage(props.sessionId, data)
|
||||
break
|
||||
case msgContentType.AUDIO:
|
||||
audioData.setAudio(props.sessionId, data)
|
||||
break
|
||||
case msgContentType.VIDEO:
|
||||
videoData.setVideo(props.sessionId, data)
|
||||
break
|
||||
case msgContentType.DOCUMENT:
|
||||
default:
|
||||
documentData.setDocument(props.sessionId, data)
|
||||
}
|
||||
}
|
||||
|
||||
const onSendEmoji = (key) => {
|
||||
emit('sendEmoji', key)
|
||||
}
|
||||
|
||||
@@ -170,13 +170,14 @@ const renderImage = (content, ishowInfo = true) => {
|
||||
const imgId = content
|
||||
const url = imageData.image[imgId]?.thumbUrl
|
||||
if (url) {
|
||||
const imgIdList = imageData.imageInSession[props.sessionId].sort((a, b) => a - b)
|
||||
const srcList = imgIdList.map((item) => imageData.image[item].originUrl)
|
||||
const imgIdList = imageData.imageInSession[props.sessionId] || []
|
||||
const imgIdListSorted = imgIdList.includes(imgId) ? imgIdList.sort((a, b) => a - b) : [imgId]
|
||||
const srcList = imgIdListSorted.map((item) => imageData.image[item].originUrl)
|
||||
return h(ImageMsgBox, {
|
||||
url,
|
||||
imgId,
|
||||
srcList,
|
||||
initialIndex: imgIdList.indexOf(imgId),
|
||||
initialIndex: imgIdListSorted.indexOf(imgId),
|
||||
fileName: ishowInfo ? imageData.image[imgId].fileName : '',
|
||||
size: ishowInfo ? imageData.image[imgId].size : '',
|
||||
onLoad: () => {
|
||||
|
||||
Reference in New Issue
Block a user