diff --git a/src/api/mts.js b/src/api/mts.js index dcd0922..29c832d 100644 --- a/src/api/mts.js +++ b/src/api/mts.js @@ -3,3 +3,7 @@ import request from '@/js/utils/request' export const mtsUploadService = (obj) => { return request.postForm('/mts/upload', obj) } + +export const mtsImageService = (obj) => { + return request.get('/mts/image', { params: obj }) +} diff --git a/src/js/utils/common.js b/src/js/utils/common.js index 4802344..ca2d4ca 100644 --- a/src/js/utils/common.js +++ b/src/js/utils/common.js @@ -201,3 +201,31 @@ export const jsonParseSafe = (str) => { } export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) + +export const base64ToFile = (base64Data, fileName) => { + let arr = base64Data.split(',') // 将 Base64 数据拆分成数据部分和前缀部分 + let bstr = atob(arr[1]) + let n = bstr.length + let u8arr = new Uint8Array(n) + while (n--) { + u8arr[n] = bstr.charCodeAt(n) + } + const mimeType = base64Data.match(/data:(.*?);/)[1] + switch (mimeType) { + case 'image/png': + fileName = fileName + '.png' + break + case 'image/jpeg': + fileName = fileName + '.jpg' + break + case 'image/gif': + fileName = fileName + '.gif' + break + case 'application/pdf': + fileName = fileName + '.pdf' + break + default: + fileName = fileName + '.dat' + } + return new File([u8arr], fileName, { type: mimeType }) +} diff --git a/src/js/utils/emojis.js b/src/js/utils/emojis.js index e712bbc..4301011 100644 --- a/src/js/utils/emojis.js +++ b/src/js/utils/emojis.js @@ -180,7 +180,7 @@ export const emojiTrans = (content) => { new Set(matches).forEach((item) => { const emoji = emojis[item] - content = content.replaceAll(item, emoji) + content = emoji ? content.replaceAll(item, emoji) : content }) return content diff --git a/src/stores/image.js b/src/stores/image.js new file mode 100644 index 0000000..1309257 --- /dev/null +++ b/src/stores/image.js @@ -0,0 +1,70 @@ +import { mtsImageService } from '@/api/mts' +import { defineStore } from 'pinia' +import { ref } from 'vue' + +const pattern = /\{[a-f0-9]+\}/g + +// image的缓存数据,不持久化存储 +export const imageStore = defineStore('anyim-image', () => { + /** + * { + * objectId_01: {objectId: objectId_01, originUrl: xxx, thumbUrl: xxx}, + * objectId_02: {objectId: objectId_02, originUrl: xxx, thumbUrl: xxx}, + * } + */ + const image = ref({}) + + const setImage = (obj) => { + image.value[obj.objectId] = obj + } + + const imageTrans = (content, maxWidth = 400, maxHeight = 300) => { + const matches = content.match(pattern) + if (!matches || matches.length === 0) { + return content + } + + new Set(matches).forEach((item) => { + let startIndex = item.indexOf('{') + let endIndex = item.indexOf('}') + const objectId = item.slice(startIndex + 1, endIndex) + const thumbUrl = image.value[objectId]?.thumbUrl + if (thumbUrl) { + const imageHtml = + `{${objectId}}` + content = content.replaceAll(item, imageHtml) + } + }) + + return content + } + + const getImageFromContent = async (content) => { + const imageIds = new Set() + const matches = content.match(pattern) + if (matches && matches.length > 0) { + matches.forEach((item) => { + let startIndex = item.indexOf('{') + let endIndex = item.indexOf('}') + const objectId = item.slice(startIndex + 1, endIndex) + if (!image.value[objectId]) { + imageIds.add(objectId) + } + }) + } + if (imageIds.size > 0) { + const res = await mtsImageService({ objectIds: [...imageIds].join(',') }) + res.data.data.forEach((item) => { + imageStore().setImage(item) // 缓存image数据 + }) + } + } + + return { + image, + setImage, + imageTrans, + getImageFromContent + } +}) diff --git a/src/stores/index.js b/src/stores/index.js index 0f3d714..e905ddc 100644 --- a/src/stores/index.js +++ b/src/stores/index.js @@ -12,3 +12,4 @@ export * from './message' export * from './search' export * from './userCard' export * from './groupCard' +export * from './image' diff --git a/src/stores/message.js b/src/stores/message.js index 733d504..4b3ae0c 100644 --- a/src/stores/message.js +++ b/src/stores/message.js @@ -6,6 +6,9 @@ import { msgQueryPartitionService } from '@/api/message' import { ElMessage } from 'element-plus' +import { MsgType } from '@/proto/msg' +import { mtsImageService } from '@/api/mts' +import { imageStore } from './image' // 消息功能相关需要缓存的数据,不持久化存储 export const messageStore = defineStore('anyim-message', () => { @@ -123,16 +126,41 @@ export const messageStore = defineStore('anyim-message', () => { const addMsgRecords = (sessionId, msgRecords) => { if (!msgRecords?.length) return + const imageIds = new Set() msgRecords.forEach((item) => { if (!msgRecordsList.value[sessionId]) { msgRecordsList.value[sessionId] = {} } msgRecordsList.value[sessionId][item.msgId] = item + + // 如果消息内容中含有图片,则查询图片的url + if (item.msgType === MsgType.CHAT || item.msgType === MsgType.GROUP_CHAT) { + const pattern = /\{[a-f0-9]+\}/g + const matches = item.content.match(pattern) + if (matches && matches.length > 0) { + matches.forEach((item) => { + let startIndex = item.indexOf('{') + let endIndex = item.indexOf('}') + const objectId = item.slice(startIndex + 1, endIndex) + if (!imageStore().image[objectId]) { + imageIds.add(objectId) + } + }) + } + } }) // 更新排序 msgIdSortArray.value[sessionId] = Object.keys(msgRecordsList.value[sessionId]).sort( (a, b) => a - b ) + + if (imageIds.size > 0) { + mtsImageService({ objectIds: [...imageIds].join(',') }).then((res) => { + res.data.data.forEach((item) => { + imageStore().setImage(item) // 缓存image数据 + }) + }) + } } /** diff --git a/src/views/message/MessageLayout.vue b/src/views/message/MessageLayout.vue index bbfe4fe..ab7911d 100644 --- a/src/views/message/MessageLayout.vue +++ b/src/views/message/MessageLayout.vue @@ -8,7 +8,6 @@ import { CirclePlus, LocationInformation, Clock, - Picture, FolderAdd, CreditCard, ArrowDownBold, @@ -1108,22 +1107,17 @@ const onSendEmoji = (key) => { - - - - + - + - + diff --git a/src/views/message/components/InputEditor.vue b/src/views/message/components/InputEditor.vue index e1a0f8d..e3d68e8 100644 --- a/src/views/message/components/InputEditor.vue +++ b/src/views/message/components/InputEditor.vue @@ -2,13 +2,18 @@ import { QuillEditor, Delta, Quill } from '@vueup/vue-quill' import '@vueup/vue-quill/dist/vue-quill.snow.css' import { onMounted, onUnmounted, ref, watch } from 'vue' -import { messageStore } from '@/stores' -import { ElMessage } from 'element-plus' -import { emojis } from '@/js/utils/emojis' +import { v4 as uuidv4 } from 'uuid' +import { messageStore, imageStore } from '@/stores' +import { ElMessage, ElLoading } from 'element-plus' +import { emojis, emojiTrans } from '@/js/utils/emojis' +import { base64ToFile } from '@/js/utils/common' +import { mtsUploadService } from '@/api/mts' +import { el_loading_options } from '@/const/commonConst' const props = defineProps(['sessionId', 'draft']) const emit = defineEmits(['sendMessage']) const messageData = messageStore() +const imageData = imageStore() const editorRef = ref() @@ -16,11 +21,11 @@ const getQuill = () => { return editorRef.value?.getQuill() } -onMounted(() => { +onMounted(async () => { // 给组件增加滚动条样式 document.querySelector('.ql-editor').classList.add('my-scrollbar') - getQuill().setText(props.draft) - getQuill().setSelection(getQuill().getLength(), 0, 'user') + await imageData.getImageFromContent(props.draft) + formatContent(props.draft) getQuill().on('composition-start', () => { // 当用户使用拼音输入法开始输入汉字时,这个事件就会被触发 getQuill().root.dataset.placeholder = '' @@ -41,30 +46,43 @@ onUnmounted(() => { } }) -const getContent = () => { +const getContent = async () => { const delta = getQuill().getContents() let content = '' - delta.ops.forEach((op) => { + 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 } else if (insert && insert.image) { const alt = op.attributes?.alt - // 表情 if (alt && alt.startsWith('[') && alt.endsWith(']')) { + // 表情 content = content + alt + } else if (alt && alt.startsWith('{') && alt.endsWith('}')) { + // 图片 + content = content + alt + } else if (insert.image.startsWith('data:') && insert.image.includes('base64')) { + // base64编码的图片 + const file = base64ToFile(insert.image, uuidv4()) // base64转file + el_loading_options.text = '图片上传中...' //上传中加一个loading效果 + const loadingInstance = ElLoading.service(el_loading_options) + const res = await mtsUploadService({ file: file }) //上传图片至服务端 + loadingInstance.close() + imageData.setImage(res.data.data) // 缓存image数据 + content = content + `{${res.data.data.objectId}}` } } - }) + } return content.trim() } // 监控session发生了切换 watch( () => props.sessionId, - (newValue, oldValue) => { - let content = getContent() + async (newValue, oldValue) => { + let content = await getContent() // 草稿若没发生变动,则不触发存储 if (oldValue && content !== messageData.sessionList[oldValue].draft) { messageData.updateSession({ @@ -72,14 +90,22 @@ watch( draft: content }) } - getQuill().setText(messageData.sessionList[newValue].draft || '') - getQuill().setSelection(getQuill().getLength(), 0, 'user') + formatContent(messageData.sessionList[newValue].draft || '') }, { deep: true } ) -const handleEnter = () => { - const content = getContent() +const formatContent = (content) => { + let html = emojiTrans(content) + html = imageData.imageTrans(html) + html = html.replace(/\n/g, '
') + getQuill().setText('') + getQuill().clipboard.dangerouslyPasteHTML(0, html) + getQuill().setSelection(getQuill().getLength(), 0, 'user') +} + +const handleEnter = async () => { + const content = await getContent() if (!content) { ElMessage.warning('请勿发送空内容') getQuill().setText('') diff --git a/src/views/message/components/MessageItem.vue b/src/views/message/components/MessageItem.vue index 3f78f84..fac6b6d 100644 --- a/src/views/message/components/MessageItem.vue +++ b/src/views/message/components/MessageItem.vue @@ -2,7 +2,7 @@ import { computed } from 'vue' import { WarningFilled } from '@element-plus/icons-vue' import { MsgType } from '@/proto/msg' -import { userStore, messageStore, groupStore, groupCardStore } from '@/stores' +import { userStore, messageStore, groupStore, groupCardStore, imageStore } from '@/stores' import { messageSysShowTime, messageBoxShowTime, jsonParseSafe } from '@/js/utils/common' import UserAvatarIcon from '@/components/common/UserAvatarIcon.vue' import { emojiTrans } from '@/js/utils/emojis' @@ -24,6 +24,7 @@ const userData = userStore() const messageData = messageStore() const groupData = groupStore() const groupCardData = groupCardStore() +const imageData = imageStore() const msg = computed(() => { return messageData.getMsg(props.sessionId, props.msgId) @@ -401,6 +402,12 @@ const onClickSystemMsg = (e) => { const onResendMsg = () => { emit('resendMsg', msg.value) } + +const formatContent = (content) => { + let html = emojiTrans(content) + html = imageData.imageTrans(html) + return html +}