更新组件和API,添加NProgress和NTag支持,优化上传功能,增强编辑器功能,调整样式和结构,提升用户体验。
This commit is contained in:
parent
651baafd0f
commit
661472a70a
2
components.d.ts
vendored
2
components.d.ts
vendored
@ -57,6 +57,8 @@ declare module 'vue' {
|
|||||||
NoticeEditor: typeof import('./src/components/group/manage/NoticeEditor.vue')['default']
|
NoticeEditor: typeof import('./src/components/group/manage/NoticeEditor.vue')['default']
|
||||||
NoticeTab: typeof import('./src/components/group/manage/NoticeTab.vue')['default']
|
NoticeTab: typeof import('./src/components/group/manage/NoticeTab.vue')['default']
|
||||||
NotificationApi: typeof import('./src/components/common/NotificationApi.vue')['default']
|
NotificationApi: typeof import('./src/components/common/NotificationApi.vue')['default']
|
||||||
|
NProgress: typeof import('naive-ui')['NProgress']
|
||||||
|
NTag: typeof import('naive-ui')['NTag']
|
||||||
RevokeMessage: typeof import('./src/components/talk/message/RevokeMessage.vue')['default']
|
RevokeMessage: typeof import('./src/components/talk/message/RevokeMessage.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
@ -21,6 +21,9 @@ export const ServeFileSubareaUpload = (data = {}, options = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 上传图片文件或者视频
|
// 上传图片文件或者视频
|
||||||
export const uploadImg = (data) => {
|
export const uploadImg = (data, signal) => {
|
||||||
return post('/upload/img', data,{baseURL:import.meta.env.VITE_EPR_BASEURL})
|
return post('/upload/img', data, {
|
||||||
|
baseURL: import.meta.env.VITE_EPR_BASEURL,
|
||||||
|
signal: signal
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
BIN
src/assets/image/file-paper-line@2x.png
Normal file
BIN
src/assets/image/file-paper-line@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/image/file@2x.png
Normal file
BIN
src/assets/image/file@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 607 B |
@ -1,81 +1,117 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
// 引入Quill编辑器的样式文件
|
||||||
import '@vueup/vue-quill/dist/vue-quill.snow.css'
|
import '@vueup/vue-quill/dist/vue-quill.snow.css'
|
||||||
|
// 引入图片上传插件的样式
|
||||||
import 'quill-image-uploader/dist/quill.imageUploader.min.css'
|
import 'quill-image-uploader/dist/quill.imageUploader.min.css'
|
||||||
|
// 引入自定义的提及功能样式
|
||||||
import '@/assets/css/editor-mention.less'
|
import '@/assets/css/editor-mention.less'
|
||||||
|
// 引入Vue核心功能
|
||||||
import { reactive, watch, ref, markRaw, computed, onMounted, onUnmounted } from 'vue'
|
import { reactive, watch, ref, markRaw, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
// 引入Naive UI的弹出框组件
|
||||||
import { NPopover } from 'naive-ui'
|
import { NPopover } from 'naive-ui'
|
||||||
|
// 引入图标组件
|
||||||
import {
|
import {
|
||||||
Voice as IconVoice,
|
Voice as IconVoice, // 语音图标
|
||||||
SourceCode,
|
SourceCode, // 代码图标
|
||||||
Local,
|
Local, // 地理位置图标
|
||||||
SmilingFace,
|
SmilingFace, // 表情图标
|
||||||
Pic,
|
Pic, // 图片图标
|
||||||
FolderUpload,
|
FolderUpload, // 文件上传图标
|
||||||
Ranking,
|
Ranking, // 排名图标(用于投票)
|
||||||
History
|
History // 历史记录图标
|
||||||
} from '@icon-park/vue-next'
|
} from '@icon-park/vue-next'
|
||||||
|
// 引入Quill编辑器及其核心实例
|
||||||
import { QuillEditor, Quill } from '@vueup/vue-quill'
|
import { QuillEditor, Quill } from '@vueup/vue-quill'
|
||||||
|
// 引入图片上传插件
|
||||||
import ImageUploader from 'quill-image-uploader'
|
import ImageUploader from 'quill-image-uploader'
|
||||||
|
// 引入自定义表情符号格式
|
||||||
import EmojiBlot from './formats/emoji'
|
import EmojiBlot from './formats/emoji'
|
||||||
|
// 引入自定义引用格式
|
||||||
import QuoteBlot from './formats/quote'
|
import QuoteBlot from './formats/quote'
|
||||||
|
// 引入提及功能
|
||||||
import 'quill-mention'
|
import 'quill-mention'
|
||||||
|
// 引入状态管理
|
||||||
import { useDialogueStore, useEditorDraftStore } from '@/store'
|
import { useDialogueStore, useEditorDraftStore } from '@/store'
|
||||||
|
// 引入编辑器工具函数
|
||||||
import { deltaToMessage, deltaToString, isEmptyDelta } from './util'
|
import { deltaToMessage, deltaToString, isEmptyDelta } from './util'
|
||||||
|
// 引入获取图片信息的工具函数
|
||||||
import { getImageInfo } from '@/utils/functions'
|
import { getImageInfo } from '@/utils/functions'
|
||||||
|
// 引入编辑器常量定义
|
||||||
import { EditorConst } from '@/constant/event-bus'
|
import { EditorConst } from '@/constant/event-bus'
|
||||||
|
// 引入事件调用工具
|
||||||
import { emitCall } from '@/utils/common'
|
import { emitCall } from '@/utils/common'
|
||||||
|
// 引入默认头像常量
|
||||||
import { defAvatar } from '@/constant/default'
|
import { defAvatar } from '@/constant/default'
|
||||||
import MeEditorVote from './MeEditorVote.vue'
|
// 引入编辑器各子组件
|
||||||
import MeEditorEmoticon from './MeEditorEmoticon.vue'
|
import MeEditorVote from './MeEditorVote.vue' // 投票组件
|
||||||
import MeEditorCode from './MeEditorCode.vue'
|
import MeEditorEmoticon from './MeEditorEmoticon.vue' // 表情组件
|
||||||
import MeEditorRecorder from './MeEditorRecorder.vue'
|
import MeEditorCode from './MeEditorCode.vue' // 代码编辑组件
|
||||||
|
import MeEditorRecorder from './MeEditorRecorder.vue' // 录音组件
|
||||||
|
// 引入上传API
|
||||||
import { ServeUploadImage } from '@/api/upload'
|
import { ServeUploadImage } from '@/api/upload'
|
||||||
import { uploadImg } from '@/api/upload'
|
import { uploadImg } from '@/api/upload'
|
||||||
|
// 引入事件总线钩子
|
||||||
import { useEventBus } from '@/hooks'
|
import { useEventBus } from '@/hooks'
|
||||||
|
|
||||||
Quill.register('formats/emoji', EmojiBlot)
|
// 注册Quill编辑器的自定义格式
|
||||||
Quill.register('formats/quote', QuoteBlot)
|
Quill.register('formats/emoji', EmojiBlot) // 注册表情格式
|
||||||
Quill.register('modules/imageUploader', ImageUploader)
|
Quill.register('formats/quote', QuoteBlot) // 注册引用格式
|
||||||
|
Quill.register('modules/imageUploader', ImageUploader) // 注册图片上传模块
|
||||||
|
|
||||||
|
// 定义组件的事件
|
||||||
const emit = defineEmits(['editor-event'])
|
const emit = defineEmits(['editor-event'])
|
||||||
|
// 获取对话状态管理
|
||||||
const dialogueStore = useDialogueStore()
|
const dialogueStore = useDialogueStore()
|
||||||
|
// 获取编辑器草稿状态管理
|
||||||
const editorDraftStore = useEditorDraftStore()
|
const editorDraftStore = useEditorDraftStore()
|
||||||
|
// 定义组件props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
vote: {
|
vote: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false // 是否显示投票功能
|
||||||
},
|
},
|
||||||
members: {
|
members: {
|
||||||
default: () => []
|
default: () => [] // 聊天成员列表,用于@功能
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 编辑器引用
|
||||||
const editor = ref()
|
const editor = ref()
|
||||||
|
|
||||||
|
// 获取Quill编辑器实例
|
||||||
const getQuill = () => {
|
const getQuill = () => {
|
||||||
return editor.value?.getQuill()
|
return editor.value?.getQuill()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取当前编辑器光标位置
|
||||||
const getQuillSelectionIndex = () => {
|
const getQuillSelectionIndex = () => {
|
||||||
let quill = getQuill()
|
let quill = getQuill()
|
||||||
|
|
||||||
return (quill.getSelection() || {}).index || quill.getLength()
|
return (quill.getSelection() || {}).index || quill.getLength()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算当前对话索引名称(标识当前聊天)
|
||||||
const indexName = computed(() => dialogueStore.index_name)
|
const indexName = computed(() => dialogueStore.index_name)
|
||||||
|
// 控制是否显示编辑器的投票界面
|
||||||
const isShowEditorVote = ref(false)
|
const isShowEditorVote = ref(false)
|
||||||
|
// 控制是否显示编辑器的代码界面
|
||||||
const isShowEditorCode = ref(false)
|
const isShowEditorCode = ref(false)
|
||||||
|
// 控制是否显示录音界面
|
||||||
const isShowEditorRecorder = ref(false)
|
const isShowEditorRecorder = ref(false)
|
||||||
|
// 图片文件上传DOM引用
|
||||||
const fileImageRef = ref()
|
const fileImageRef = ref()
|
||||||
|
// 文件上传DOM引用
|
||||||
const uploadFileRef = ref()
|
const uploadFileRef = ref()
|
||||||
|
// 表情面板引用
|
||||||
const emoticonRef = ref()
|
const emoticonRef = ref()
|
||||||
|
|
||||||
|
// 编辑器配置选项
|
||||||
const editorOption = {
|
const editorOption = {
|
||||||
debug: false,
|
debug: false,
|
||||||
modules: {
|
modules: {
|
||||||
toolbar: false,
|
toolbar: false, // 禁用默认工具栏
|
||||||
clipboard: {
|
clipboard: {
|
||||||
// 粘贴版,处理粘贴时候的自带样式
|
// 粘贴处理,去除粘贴时的自带样式
|
||||||
matchers: [[Node.ELEMENT_NODE, onClipboardMatcher]]
|
matchers: [[Node.ELEMENT_NODE, onClipboardMatcher]]
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -83,19 +119,22 @@ const editorOption = {
|
|||||||
bindings: {
|
bindings: {
|
||||||
enter: {
|
enter: {
|
||||||
key: 13,
|
key: 13,
|
||||||
handler: onSendMessage
|
handler: onSendMessage // 按Enter键发送消息
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 图片上传配置
|
||||||
imageUploader: {
|
imageUploader: {
|
||||||
upload: onEditorUpload
|
upload: onEditorUpload
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// @功能配置
|
||||||
mention: {
|
mention: {
|
||||||
allowedChars: /^[\u4e00-\u9fa5]*$/,
|
allowedChars: /^[\u4e00-\u9fa5]*$/, // 允许中文字符
|
||||||
mentionDenotationChars: ['@'],
|
mentionDenotationChars: ['@'], // @符号触发
|
||||||
positioningStrategy: 'fixed',
|
positioningStrategy: 'fixed', // 定位策略
|
||||||
|
// 渲染@项目的函数
|
||||||
renderItem: (data: any) => {
|
renderItem: (data: any) => {
|
||||||
const el = document.createElement('div')
|
const el = document.createElement('div')
|
||||||
el.className = 'ed-member-item'
|
el.className = 'ed-member-item'
|
||||||
@ -103,6 +142,7 @@ const editorOption = {
|
|||||||
el.innerHTML += `<span class="nickname">${data.nickname}</span>`
|
el.innerHTML += `<span class="nickname">${data.nickname}</span>`
|
||||||
return el
|
return el
|
||||||
},
|
},
|
||||||
|
// 数据源函数,过滤匹配的用户
|
||||||
source: function (searchTerm: string, renderList: any) {
|
source: function (searchTerm: string, renderList: any) {
|
||||||
if (!props.members.length) {
|
if (!props.members.length) {
|
||||||
return renderList([])
|
return renderList([])
|
||||||
@ -123,66 +163,73 @@ const editorOption = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
placeholder: '按Enter发送 / Shift+Enter 换行',
|
placeholder: '按Enter发送 / Shift+Enter 换行',
|
||||||
theme: 'snow'
|
theme: 'snow' // 使用snow主题
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 底部工具栏配置
|
||||||
const navs = reactive([
|
const navs = reactive([
|
||||||
{
|
{
|
||||||
title: '图片',
|
title: '图片',
|
||||||
icon: markRaw(Pic),
|
icon: markRaw(Pic),
|
||||||
show: true,
|
show: true,
|
||||||
click: () => {
|
click: () => {
|
||||||
fileImageRef.value.click()
|
fileImageRef.value.click() // 触发图片上传
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '附件',
|
title: '文件',
|
||||||
icon: markRaw(FolderUpload),
|
icon: markRaw(FolderUpload),
|
||||||
show: true,
|
show: true,
|
||||||
click: () => {
|
click: () => {
|
||||||
uploadFileRef.value.click()
|
uploadFileRef.value.click() // 触发文件上传
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
// 以下功能已被注释掉,但保留代码
|
||||||
title: '代码',
|
// {
|
||||||
icon: markRaw(SourceCode),
|
// title: '代码',
|
||||||
show: true,
|
// icon: markRaw(SourceCode),
|
||||||
click: () => {
|
// show: true,
|
||||||
isShowEditorCode.value = true
|
// click: () => {
|
||||||
}
|
// isShowEditorCode.value = true
|
||||||
},
|
// }
|
||||||
{
|
// },
|
||||||
title: '语音消息',
|
// {
|
||||||
icon: markRaw(IconVoice),
|
// title: '语音消息',
|
||||||
show: true,
|
// icon: markRaw(IconVoice),
|
||||||
click: () => {
|
// show: true,
|
||||||
isShowEditorRecorder.value = true
|
// click: () => {
|
||||||
}
|
// isShowEditorRecorder.value = true
|
||||||
},
|
// }
|
||||||
{
|
// },
|
||||||
title: '地理位置',
|
// {
|
||||||
icon: markRaw(Local),
|
// title: '地理位置',
|
||||||
show: true,
|
// icon: markRaw(Local),
|
||||||
click: () => {}
|
// show: true,
|
||||||
},
|
// click: () => {}
|
||||||
{
|
// },
|
||||||
title: '群投票',
|
// {
|
||||||
icon: markRaw(Ranking),
|
// title: '群投票',
|
||||||
show: computed(() => props.vote),
|
// icon: markRaw(Ranking),
|
||||||
click: () => {
|
// show: computed(() => props.vote),
|
||||||
isShowEditorVote.value = true
|
// click: () => {
|
||||||
}
|
// isShowEditorVote.value = true
|
||||||
},
|
// }
|
||||||
{
|
// },
|
||||||
title: '历史记录',
|
// {
|
||||||
icon: markRaw(History),
|
// title: '历史记录',
|
||||||
show: true,
|
// icon: markRaw(History),
|
||||||
click: () => {
|
// show: true,
|
||||||
emit('editor-event', emitCall('history_event'))
|
// click: () => {
|
||||||
}
|
// emit('editor-event', emitCall('history_event'))
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
])
|
])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传图片函数
|
||||||
|
* @param file 文件对象
|
||||||
|
* @returns Promise,成功时返回图片URL
|
||||||
|
*/
|
||||||
function onUploadImage(file: File) {
|
function onUploadImage(file: File) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
let image = new Image()
|
let image = new Image()
|
||||||
@ -190,35 +237,44 @@ function onUploadImage(file: File) {
|
|||||||
image.onload = () => {
|
image.onload = () => {
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
form.append('file', file)
|
form.append('file', file)
|
||||||
form.append("source", "fonchain-chat");
|
form.append("source", "fonchain-chat"); // 图片来源标识
|
||||||
// form.append('width', image.width.toString())
|
// 添加图片尺寸信息作为URL参数
|
||||||
// form.append('height', image.height.toString())
|
|
||||||
form.append("urlParam", `width=${image.width}&height=${image.height}`);
|
form.append("urlParam", `width=${image.width}&height=${image.height}`);
|
||||||
|
|
||||||
|
// 调用上传API
|
||||||
uploadImg(form).then(({ code, data, message }) => {
|
uploadImg(form).then(({ code, data, message }) => {
|
||||||
if (code == 0) {
|
if (code == 0) {
|
||||||
resolve(data.ori_url)
|
resolve(data.ori_url) // 返回原始图片URL
|
||||||
} else {
|
} else {
|
||||||
resolve('')
|
resolve('')
|
||||||
window['$message'].error(message)
|
window['$message'].error(message) // 显示错误信息
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑器上传处理函数
|
||||||
|
* @param file 要上传的文件
|
||||||
|
* @returns Promise
|
||||||
|
*/
|
||||||
function onEditorUpload(file: File) {
|
function onEditorUpload(file: File) {
|
||||||
async function fn(file: File, resolve: Function, reject: Function) {
|
async function fn(file: File, resolve: Function, reject: Function) {
|
||||||
if (file.type.indexOf('image/') === 0) {
|
if (file.type.indexOf('image/') === 0) {
|
||||||
|
// 如果是图片,使用图片上传处理
|
||||||
return resolve(await onUploadImage(file))
|
return resolve(await onUploadImage(file))
|
||||||
}
|
}
|
||||||
|
|
||||||
reject()
|
reject()
|
||||||
|
|
||||||
|
// 非图片文件的处理
|
||||||
if (file.type.indexOf('video/') === 0) {
|
if (file.type.indexOf('video/') === 0) {
|
||||||
|
// 视频文件
|
||||||
let fn = emitCall('video_event', file, () => {})
|
let fn = emitCall('video_event', file, () => {})
|
||||||
emit('editor-event', fn)
|
emit('editor-event', fn)
|
||||||
} else {
|
} else {
|
||||||
|
// 其他文件
|
||||||
let fn = emitCall('file_event', file, () => {})
|
let fn = emitCall('file_event', file, () => {})
|
||||||
emit('editor-event', fn)
|
emit('editor-event', fn)
|
||||||
}
|
}
|
||||||
@ -229,29 +285,40 @@ function onEditorUpload(file: File) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 投票事件处理
|
||||||
|
* @param data 投票数据
|
||||||
|
*/
|
||||||
function onVoteEvent(data: any) {
|
function onVoteEvent(data: any) {
|
||||||
const msg = emitCall('vote_event', data, (ok: boolean) => {
|
const msg = emitCall('vote_event', data, (ok: boolean) => {
|
||||||
if (ok) {
|
if (ok) {
|
||||||
isShowEditorVote.value = false
|
isShowEditorVote.value = false // 成功后关闭投票界面
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
emit('editor-event', msg)
|
emit('editor-event', msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表情事件处理
|
||||||
|
* @param data 表情数据
|
||||||
|
*/
|
||||||
function onEmoticonEvent(data: any) {
|
function onEmoticonEvent(data: any) {
|
||||||
emoticonRef.value.setShow(false)
|
emoticonRef.value.setShow(false) // 关闭表情面板
|
||||||
|
|
||||||
if (data.type == 1) {
|
if (data.type == 1) {
|
||||||
|
// 插入文本表情
|
||||||
const quill = getQuill()
|
const quill = getQuill()
|
||||||
let index = getQuillSelectionIndex()
|
let index = getQuillSelectionIndex()
|
||||||
|
|
||||||
|
// 删除编辑器中多余的换行符
|
||||||
if (index == 1 && quill.getLength() == 1 && quill.getText(0, 1) == '\n') {
|
if (index == 1 && quill.getLength() == 1 && quill.getText(0, 1) == '\n') {
|
||||||
quill.deleteText(0, 1)
|
quill.deleteText(0, 1)
|
||||||
index = 0
|
index = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.img) {
|
if (data.img) {
|
||||||
|
// 插入图片表情
|
||||||
quill.insertEmbed(index, 'emoji', {
|
quill.insertEmbed(index, 'emoji', {
|
||||||
alt: data.value,
|
alt: data.value,
|
||||||
src: data.img,
|
src: data.img,
|
||||||
@ -259,40 +326,54 @@ function onEmoticonEvent(data: any) {
|
|||||||
height: '24px'
|
height: '24px'
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
// 插入文本表情
|
||||||
quill.insertText(index, data.value)
|
quill.insertText(index, data.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置光标位置
|
||||||
quill.setSelection(index + 1, 0, 'user')
|
quill.setSelection(index + 1, 0, 'user')
|
||||||
} else {
|
} else {
|
||||||
|
// 发送整个表情包
|
||||||
let fn = emitCall('emoticon_event', data.value, () => {})
|
let fn = emitCall('emoticon_event', data.value, () => {})
|
||||||
emit('editor-event', fn)
|
emit('editor-event', fn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代码事件处理
|
||||||
|
* @param data 代码数据
|
||||||
|
*/
|
||||||
function onCodeEvent(data: any) {
|
function onCodeEvent(data: any) {
|
||||||
const msg = emitCall('code_event', data, (ok: boolean) => {
|
const msg = emitCall('code_event', data, (ok: boolean) => {
|
||||||
isShowEditorCode.value = false
|
isShowEditorCode.value = false // 成功后关闭代码界面
|
||||||
})
|
})
|
||||||
|
|
||||||
emit('editor-event', msg)
|
emit('editor-event', msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传处理
|
||||||
|
* @param e 上传事件对象
|
||||||
|
*/
|
||||||
async function onUploadFile(e: any) {
|
async function onUploadFile(e: any) {
|
||||||
let file = e.target.files[0]
|
let file = e.target.files[0]
|
||||||
|
|
||||||
e.target.value = null
|
e.target.value = null // 清空input,允许再次选择相同文件
|
||||||
|
|
||||||
console.log("文件类型"+file.type)
|
console.log("文件类型"+file.type)
|
||||||
if (file.type.indexOf('image/') === 0) {
|
if (file.type.indexOf('image/') === 0) {
|
||||||
console.log("进入图片")
|
console.log("进入图片")
|
||||||
|
// 处理图片文件
|
||||||
const quill = getQuill()
|
const quill = getQuill()
|
||||||
let index = getQuillSelectionIndex()
|
let index = getQuillSelectionIndex()
|
||||||
|
|
||||||
|
// 删除编辑器中多余的换行符
|
||||||
if (index == 1 && quill.getLength() == 1 && quill.getText(0, 1) == '\n') {
|
if (index == 1 && quill.getLength() == 1 && quill.getText(0, 1) == '\n') {
|
||||||
quill.deleteText(0, 1)
|
quill.deleteText(0, 1)
|
||||||
index = 0
|
index = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 上传图片并插入到编辑器中
|
||||||
let src = await onUploadImage(file)
|
let src = await onUploadImage(file)
|
||||||
if (src) {
|
if (src) {
|
||||||
quill.insertEmbed(index, 'image', src)
|
quill.insertEmbed(index, 'image', src)
|
||||||
@ -304,29 +385,41 @@ async function onUploadFile(e: any) {
|
|||||||
|
|
||||||
if (file.type.indexOf('video/') === 0) {
|
if (file.type.indexOf('video/') === 0) {
|
||||||
console.log("进入视频")
|
console.log("进入视频")
|
||||||
|
// 处理视频文件
|
||||||
let fn = emitCall('video_event', file, () => {})
|
let fn = emitCall('video_event', file, () => {})
|
||||||
emit('editor-event', fn)
|
emit('editor-event', fn)
|
||||||
} else {
|
} else {
|
||||||
console.log("进入其他")
|
console.log("进入其他")
|
||||||
|
// 处理其他类型文件
|
||||||
let fn = emitCall('file_event', file, () => {})
|
let fn = emitCall('file_event', file, () => {})
|
||||||
emit('editor-event', fn)
|
emit('editor-event', fn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 录音事件处理
|
||||||
|
* @param file 录音文件
|
||||||
|
*/
|
||||||
function onRecorderEvent(file: any) {
|
function onRecorderEvent(file: any) {
|
||||||
emit('editor-event', emitCall('file_event', file))
|
emit('editor-event', emitCall('file_event', file))
|
||||||
isShowEditorRecorder.value = false
|
isShowEditorRecorder.value = false // 关闭录音界面
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 粘贴内容处理,移除粘贴内容中的样式
|
||||||
|
* @param node DOM节点
|
||||||
|
* @param Delta Quill Delta对象
|
||||||
|
* @returns 处理后的Delta
|
||||||
|
*/
|
||||||
function onClipboardMatcher(node: any, Delta) {
|
function onClipboardMatcher(node: any, Delta) {
|
||||||
const ops: any[] = []
|
const ops: any[] = []
|
||||||
|
|
||||||
Delta.ops.forEach((op) => {
|
Delta.ops.forEach((op) => {
|
||||||
// 如果粘贴了图片,这里会是一个对象,所以可以这样处理
|
// 处理粘贴内容
|
||||||
if (op.insert && typeof op.insert === 'string') {
|
if (op.insert && typeof op.insert === 'string') {
|
||||||
ops.push({
|
ops.push({
|
||||||
insert: op.insert, // 文字内容
|
insert: op.insert, // 文字内容
|
||||||
attributes: {} //文字样式(包括背景色和文字颜色等)
|
attributes: {} // 移除所有样式
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
ops.push(op)
|
ops.push(op)
|
||||||
@ -337,12 +430,16 @@ function onClipboardMatcher(node: any, Delta) {
|
|||||||
return Delta
|
return Delta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息处理
|
||||||
|
* 根据编辑器内容类型发送不同类型的消息
|
||||||
|
*/
|
||||||
function onSendMessage() {
|
function onSendMessage() {
|
||||||
var delta = getQuill().getContents()
|
var delta = getQuill().getContents()
|
||||||
let data = deltaToMessage(delta)
|
let data = deltaToMessage(delta) // 转换Delta为消息格式
|
||||||
|
|
||||||
if (data.items.length === 0) {
|
if (data.items.length === 0) {
|
||||||
return
|
return // 没有内容不发送
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (data.msgType) {
|
switch (data.msgType) {
|
||||||
@ -351,60 +448,72 @@ function onSendMessage() {
|
|||||||
return window['$message'].info('发送内容超长,请分条发送')
|
return window['$message'].info('发送内容超长,请分条发送')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 发送文本消息
|
||||||
emit(
|
emit(
|
||||||
'editor-event',
|
'editor-event',
|
||||||
emitCall('text_event', data, (ok: any) => {
|
emitCall('text_event', data, (ok: any) => {
|
||||||
ok && getQuill().setContents([], Quill.sources.USER)
|
ok && getQuill().setContents([], Quill.sources.USER) // 成功发送后清空编辑器
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
case 3: // 图片消息
|
case 3: // 图片消息
|
||||||
|
// 发送图片消息
|
||||||
emit(
|
emit(
|
||||||
'editor-event',
|
'editor-event',
|
||||||
emitCall(
|
emitCall(
|
||||||
'image_event',
|
'image_event',
|
||||||
{ ...getImageInfo(data.items[0].content), url: data.items[0].content, size: 10000 },
|
{ ...getImageInfo(data.items[0].content), url: data.items[0].content, size: 10000 },
|
||||||
(ok: any) => {
|
(ok: any) => {
|
||||||
ok && getQuill().setContents([])
|
ok && getQuill().setContents([]) // 成功发送后清空编辑器
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
case 12: // 图文消息
|
case 12: // 图文混合消息
|
||||||
|
// 发送混合消息
|
||||||
emit(
|
emit(
|
||||||
'editor-event',
|
'editor-event',
|
||||||
emitCall('mixed_event', data, (ok: any) => {
|
emitCall('mixed_event', data, (ok: any) => {
|
||||||
ok && getQuill().setContents([])
|
ok && getQuill().setContents([]) // 成功发送后清空编辑器
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑器内容改变时的处理
|
||||||
|
* 保存草稿并触发输入事件
|
||||||
|
*/
|
||||||
function onEditorChange() {
|
function onEditorChange() {
|
||||||
let delta = getQuill().getContents()
|
let delta = getQuill().getContents()
|
||||||
|
let text = deltaToString(delta) // 将Delta转为纯文本
|
||||||
let text = deltaToString(delta)
|
|
||||||
|
|
||||||
if (!isEmptyDelta(delta)) {
|
if (!isEmptyDelta(delta)) {
|
||||||
|
// 保存草稿到store
|
||||||
editorDraftStore.items[indexName.value || ''] = JSON.stringify({
|
editorDraftStore.items[indexName.value || ''] = JSON.stringify({
|
||||||
text: text,
|
text: text,
|
||||||
ops: delta.ops
|
ops: delta.ops
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// 删除 editorDraftStore.items 下的元素
|
// 编辑器为空时删除对应草稿
|
||||||
delete editorDraftStore.items[indexName.value || '']
|
delete editorDraftStore.items[indexName.value || '']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 触发输入事件
|
||||||
emit('editor-event', emitCall('input_event', text))
|
emit('editor-event', emitCall('input_event', text))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载编辑器草稿内容
|
||||||
|
* 当切换聊天对象时,加载对应的草稿
|
||||||
|
*/
|
||||||
function loadEditorDraftText() {
|
function loadEditorDraftText() {
|
||||||
if (!editor.value) return
|
if (!editor.value) return
|
||||||
|
|
||||||
// 这里延迟处理,不然会有问题
|
// 延迟处理,确保DOM已渲染
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
hideMentionDom()
|
hideMentionDom() // 隐藏@菜单
|
||||||
|
|
||||||
const quill = getQuill()
|
const quill = getQuill()
|
||||||
|
|
||||||
@ -415,33 +524,47 @@ function loadEditorDraftText() {
|
|||||||
if (draft) {
|
if (draft) {
|
||||||
quill.setContents(JSON.parse(draft)?.ops || [])
|
quill.setContents(JSON.parse(draft)?.ops || [])
|
||||||
} else {
|
} else {
|
||||||
quill.setContents([])
|
quill.setContents([]) // 没有草稿则清空编辑器
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置光标位置到末尾
|
||||||
const index = getQuillSelectionIndex()
|
const index = getQuillSelectionIndex()
|
||||||
quill.setSelection(index, 0, 'user')
|
quill.setSelection(index, 0, 'user')
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理@成员事件
|
||||||
|
* @param data @成员数据
|
||||||
|
*/
|
||||||
function onSubscribeMention(data: any) {
|
function onSubscribeMention(data: any) {
|
||||||
const mention = getQuill().getModule('mention')
|
const mention = getQuill().getModule('mention')
|
||||||
|
// 插入@项
|
||||||
mention.insertItem({ id: data?.id, denotationChar: '@', value: data.value }, true)
|
mention.insertItem({ id: data?.id, denotationChar: '@', value: data.value }, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理引用事件
|
||||||
|
* @param data 引用数据
|
||||||
|
*/
|
||||||
function onSubscribeQuote(data: any) {
|
function onSubscribeQuote(data: any) {
|
||||||
|
// 检查是否已有引用内容
|
||||||
const delta = getQuill().getContents()
|
const delta = getQuill().getContents()
|
||||||
if (delta.ops?.some((item: any) => item.insert.quote)) {
|
if (delta.ops?.some((item: any) => item.insert.quote)) {
|
||||||
return
|
return // 已有引用则不再添加
|
||||||
}
|
}
|
||||||
|
|
||||||
const quill = getQuill()
|
const quill = getQuill()
|
||||||
const index = getQuillSelectionIndex()
|
const index = getQuillSelectionIndex()
|
||||||
|
|
||||||
|
// 在编辑器开头插入引用
|
||||||
quill.insertEmbed(0, 'quote', data)
|
quill.insertEmbed(0, 'quote', data)
|
||||||
quill.setSelection(index + 1, 0, 'user')
|
quill.setSelection(index + 1, 0, 'user') // 设置光标到引用后
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏@成员DOM元素
|
||||||
|
*/
|
||||||
function hideMentionDom() {
|
function hideMentionDom() {
|
||||||
let el = document.querySelector('.ql-mention-list-container')
|
let el = document.querySelector('.ql-mention-list-container')
|
||||||
if (el) {
|
if (el) {
|
||||||
@ -449,27 +572,34 @@ function hideMentionDom() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 监听聊天索引变化,切换聊天时加载对应草稿
|
||||||
watch(indexName, loadEditorDraftText, { immediate: true })
|
watch(indexName, loadEditorDraftText, { immediate: true })
|
||||||
|
|
||||||
|
// 组件挂载时初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadEditorDraftText()
|
loadEditorDraftText()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 组件卸载时清理
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
hideMentionDom()
|
hideMentionDom()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 订阅编辑器相关事件总线事件
|
||||||
useEventBus([
|
useEventBus([
|
||||||
{ name: EditorConst.Mention, event: onSubscribeMention },
|
{ name: EditorConst.Mention, event: onSubscribeMention }, // @成员事件
|
||||||
{ name: EditorConst.Quote, event: onSubscribeQuote }
|
{ name: EditorConst.Quote, event: onSubscribeQuote } // 引用事件
|
||||||
])
|
])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- 编辑器容器 -->
|
||||||
<section class="el-container editor">
|
<section class="el-container editor">
|
||||||
<section class="el-container is-vertical">
|
<section class="el-container is-vertical">
|
||||||
|
<!-- 工具栏区域 -->
|
||||||
<header class="el-header toolbar bdr-t">
|
<header class="el-header toolbar bdr-t">
|
||||||
<div class="tools">
|
<div class="tools">
|
||||||
|
<!-- 表情选择器弹出框 -->
|
||||||
<n-popover
|
<n-popover
|
||||||
placement="top-start"
|
placement="top-start"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
@ -489,6 +619,7 @@ useEventBus([
|
|||||||
<MeEditorEmoticon @on-select="onEmoticonEvent" />
|
<MeEditorEmoticon @on-select="onEmoticonEvent" />
|
||||||
</n-popover>
|
</n-popover>
|
||||||
|
|
||||||
|
<!-- 工具栏其他功能按钮 -->
|
||||||
<div
|
<div
|
||||||
class="item pointer"
|
class="item pointer"
|
||||||
v-for="nav in navs"
|
v-for="nav in navs"
|
||||||
@ -502,6 +633,7 @@ useEventBus([
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- 编辑器主体区域 -->
|
||||||
<main class="el-main height100">
|
<main class="el-main height100">
|
||||||
<QuillEditor
|
<QuillEditor
|
||||||
ref="editor"
|
ref="editor"
|
||||||
@ -514,11 +646,13 @@ useEventBus([
|
|||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- 隐藏的文件上传表单 -->
|
||||||
<form enctype="multipart/form-data" style="display: none">
|
<form enctype="multipart/form-data" style="display: none">
|
||||||
<input type="file" ref="fileImageRef" accept="image/*" @change="onUploadFile" />
|
<input type="file" ref="fileImageRef" accept="image/*" @change="onUploadFile" />
|
||||||
<input type="file" ref="uploadFileRef" @change="onUploadFile" />
|
<input type="file" ref="uploadFileRef" @change="onUploadFile" />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- 条件渲染的功能组件 -->
|
||||||
<MeEditorVote v-if="isShowEditorVote" @close="isShowEditorVote = false" @submit="onVoteEvent" />
|
<MeEditorVote v-if="isShowEditorVote" @close="isShowEditorVote = false" @submit="onVoteEvent" />
|
||||||
|
|
||||||
<MeEditorCode
|
<MeEditorCode
|
||||||
@ -536,7 +670,7 @@ useEventBus([
|
|||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.editor {
|
.editor {
|
||||||
--tip-bg-color: rgb(241 241 241 / 90%);
|
--tip-bg-color: rgb(241 241 241 / 90%); /* 提示背景颜色 */
|
||||||
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
@ -559,7 +693,7 @@ useEventBus([
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
.tip-title {
|
.tip-title {
|
||||||
display: none;
|
display: none; /* 默认隐藏提示文字 */
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 40px;
|
top: 40px;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
@ -577,7 +711,7 @@ useEventBus([
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
.tip-title {
|
.tip-title {
|
||||||
display: block;
|
display: block; /* 悬停时显示提示文字 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -585,6 +719,7 @@ useEventBus([
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 暗色模式样式调整 */
|
||||||
html[theme-mode='dark'] {
|
html[theme-mode='dark'] {
|
||||||
.editor {
|
.editor {
|
||||||
--tip-bg-color: #48484d;
|
--tip-bg-color: #48484d;
|
||||||
@ -593,13 +728,16 @@ html[theme-mode='dark'] {
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
|
/* 全局编辑器样式 */
|
||||||
#editor {
|
#editor {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 编辑器主体区域样式 */
|
||||||
.ql-editor {
|
.ql-editor {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 3px;
|
width: 3px;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
@ -611,6 +749,7 @@ html[theme-mode='dark'] {
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 悬停时显示滚动条 */
|
||||||
&:hover {
|
&:hover {
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background-color: var(--im-scrollbar-thumb);
|
background-color: var(--im-scrollbar-thumb);
|
||||||
@ -618,6 +757,7 @@ html[theme-mode='dark'] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 编辑器占位符样式 */
|
||||||
.ql-editor.ql-blank::before {
|
.ql-editor.ql-blank::before {
|
||||||
font-family:
|
font-family:
|
||||||
PingFang SC,
|
PingFang SC,
|
||||||
@ -626,6 +766,7 @@ html[theme-mode='dark'] {
|
|||||||
left: 8px;
|
left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 编辑器中图片样式 */
|
||||||
.ql-snow .ql-editor img {
|
.ql-snow .ql-editor img {
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
@ -633,6 +774,7 @@ html[theme-mode='dark'] {
|
|||||||
margin: 0px 2px;
|
margin: 0px 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 图片上传中样式 */
|
||||||
.image-uploading {
|
.image-uploading {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
@ -646,15 +788,18 @@ html[theme-mode='dark'] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 表情符号样式 */
|
||||||
.ed-emoji {
|
.ed-emoji {
|
||||||
background-color: unset !important;
|
background-color: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 编辑器占位符样式 */
|
||||||
.ql-editor.ql-blank::before {
|
.ql-editor.ql-blank::before {
|
||||||
font-style: unset;
|
font-style: unset;
|
||||||
color: #b8b3b3;
|
color: #b8b3b3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 引用卡片样式 */
|
||||||
.quote-card-content {
|
.quote-card-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: #f6f6f6;
|
background-color: #f6f6f6;
|
||||||
@ -691,6 +836,7 @@ html[theme-mode='dark'] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 暗色模式下的样式调整 */
|
||||||
html[theme-mode='dark'] {
|
html[theme-mode='dark'] {
|
||||||
.ql-editor.ql-blank::before {
|
.ql-editor.ql-blank::before {
|
||||||
color: #57575a;
|
color: #57575a;
|
||||||
|
@ -1,118 +1,142 @@
|
|||||||
<script lang="ts" setup>
|
<script setup>
|
||||||
import { fileFormatSize } from '@/utils/strings'
|
import { fileFormatSize } from '@/utils/strings'
|
||||||
import { download, getFileNameSuffix } from '@/utils/functions'
|
import { download, getFileNameSuffix } from '@/utils/functions'
|
||||||
import { ITalkRecordExtraFile, ITalkRecord } from '@/types/chat'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
defineProps<{
|
// 定义组件属性
|
||||||
extra: ITalkRecordExtraFile
|
const props = defineProps({
|
||||||
data: ITalkRecord
|
// 文件的额外信息
|
||||||
maxWidth?: Boolean
|
extra: {
|
||||||
}>()
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
// 聊天记录数据
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
// 是否使用最大宽度
|
||||||
|
maxWidth: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 控制文件上传时的播放状态
|
||||||
|
const isPlaying = ref(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换播放状态
|
||||||
|
* 在上传过程中可以暂停/继续
|
||||||
|
*/
|
||||||
|
const togglePlay = () => {
|
||||||
|
isPlaying.value = !isPlaying.value
|
||||||
|
console.log('播放状态:', isPlaying.value ? '播放中' : '暂停')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从文件URL中提取并返回大写的文件扩展名
|
||||||
|
* @param {string} url - 文件的URL或名称
|
||||||
|
* @returns {string} 大写的文件扩展名
|
||||||
|
*/
|
||||||
|
function getFileExtensionUpperCase(url) {
|
||||||
|
// 从URL提取文件名
|
||||||
|
const fileName = url.split('/').pop()
|
||||||
|
// 提取扩展名并转换为大写
|
||||||
|
return fileName.split('.').pop().toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算SVG圆环进度条的参数
|
||||||
|
const radius = 9 // 圆环半径
|
||||||
|
const circumference = computed(() => 2 * Math.PI * radius) // 计算圆周长
|
||||||
|
// 根据上传百分比计算描边偏移量
|
||||||
|
const strokeDashoffset = computed(() =>
|
||||||
|
circumference.value * (1 - props.extra.percentage / 100)
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="file-message">
|
<div class="w-243px bg-#fff rounded-8px shadow-md px-14px pointer">
|
||||||
<div class="main">
|
<!-- 文件头部信息 -->
|
||||||
<div class="ext">{{ getFileNameSuffix(extra.name) }}</div>
|
<div class="flex py-14px pr-5px justify-between w-full" style="border-bottom: 1px solid #EEEEEE;">
|
||||||
<div class="file-box">
|
<!-- 文件名 -->
|
||||||
<p class="info">
|
<div class="text-#1A1A1A text-14px">{{ extra.name }}</div>
|
||||||
<span class="name">{{ extra.name }}</span>
|
<!-- 文件图标区域 -->
|
||||||
<span class="size">({{ fileFormatSize(extra.size) }})</span>
|
<div class="relative">
|
||||||
</p>
|
<img class="w-47.91px h-47.91px" src="@/assets/image/file-paper-line@2x.png" alt="文件图标">
|
||||||
<p class="notice">文件已成功发送, 文件助手永久保存</p>
|
<!-- 文件扩展名显示 - 非上传状态 -->
|
||||||
|
<div v-if="!extra.is_uploading" class="absolute top-11px left-16px text-#DE4E4E text-10px font-bold">
|
||||||
|
{{ getFileExtensionUpperCase(extra.name) }}
|
||||||
|
</div>
|
||||||
|
<!-- 上传进度圆环 - 上传状态 -->
|
||||||
|
<div v-else class="absolute top-9px left-16px w-20px h-20px">
|
||||||
|
<div class="circle-progress-container" @click="togglePlay">
|
||||||
|
<svg class="circle-progress" width="20" height="20" viewBox="0 0 20 20">
|
||||||
|
<!-- 底色圆环 -->
|
||||||
|
<circle
|
||||||
|
cx="10"
|
||||||
|
cy="10"
|
||||||
|
r="9"
|
||||||
|
fill="transparent"
|
||||||
|
stroke="#EEEEEE"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
<!-- 进度圆环 -->
|
||||||
|
<circle
|
||||||
|
cx="10"
|
||||||
|
cy="10"
|
||||||
|
r="9"
|
||||||
|
fill="transparent"
|
||||||
|
stroke="#D54C4B"
|
||||||
|
stroke-width="2"
|
||||||
|
:stroke-dasharray="circumference"
|
||||||
|
:stroke-dashoffset="strokeDashoffset"
|
||||||
|
transform="rotate(-90 10 10)"
|
||||||
|
class="progress-circle"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 暂停图标 - 播放中显示 -->
|
||||||
|
<g v-if="isPlaying" class="pause-icon transform-rotate-90">
|
||||||
|
<rect x="7" y="5" width="2" height="10" fill="#D54C4B" />
|
||||||
|
<rect x="11" y="5" width="2" height="10" fill="#D54C4B" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- 播放图标 - 暂停时显示 -->
|
||||||
|
<g v-else class="play-icon">
|
||||||
|
<rect x="6" y="6" width="8" height="8" fill="#D54C4B" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<!-- 文件大小信息 -->
|
||||||
<a @click="download(data.msg_id)">下载</a>
|
<div class="text-#747474 text-12px pt-5px pb-11px">{{ fileFormatSize(extra.size) }}</div>
|
||||||
<a>在线预览</a>
|
</div>
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.file-message {
|
.circle-progress-container {
|
||||||
width: 250px;
|
width: 20px;
|
||||||
min-height: 85px;
|
height: 20px;
|
||||||
padding: 10px;
|
position: relative;
|
||||||
border-radius: 10px;
|
cursor: pointer;
|
||||||
border: 1px solid var(--im-message-border-color);
|
}
|
||||||
|
|
||||||
.main {
|
.circle-progress {
|
||||||
height: 45px;
|
transform: rotate(-90deg);
|
||||||
display: flex;
|
transform-origin: center;
|
||||||
flex-direction: row;
|
}
|
||||||
margin-top: 5px;
|
|
||||||
|
|
||||||
.ext {
|
.progress-circle {
|
||||||
display: flex;
|
transition: stroke-dashoffset 0.3s ease;
|
||||||
justify-content: center;
|
}
|
||||||
align-items: center;
|
|
||||||
width: 45px;
|
|
||||||
height: 45px;
|
|
||||||
color: #ffffff;
|
|
||||||
background: #49a4ff;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-box {
|
.pause-icon, .play-icon {
|
||||||
flex: 1 1;
|
transform-origin: center;
|
||||||
height: 45px;
|
}
|
||||||
margin-left: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.info {
|
.transform-rotate-90 {
|
||||||
display: flex;
|
transform: rotate(90deg);
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
overflow: hidden;
|
|
||||||
height: 24px;
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
.name {
|
|
||||||
flex: 1 auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.size {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #cac6c6;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.notice {
|
|
||||||
height: 25px;
|
|
||||||
line-height: 25px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #929191;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
height: 30px;
|
|
||||||
line-height: 37px;
|
|
||||||
text-align: right;
|
|
||||||
font-size: 12px;
|
|
||||||
border-top: 1px solid var(--border-color);
|
|
||||||
margin-top: 10px;
|
|
||||||
|
|
||||||
a {
|
|
||||||
margin: 0 3px;
|
|
||||||
user-select: none;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--im-text-color);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: royalblue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -47,7 +47,7 @@ const img = (src: string, width = 200) => {
|
|||||||
min-width: 30px;
|
min-width: 30px;
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
max-width:240px;
|
max-width:240px;
|
||||||
max-height:300px
|
height:149px
|
||||||
&.left {
|
&.left {
|
||||||
background: var(--im-message-right-bg-color);
|
background: var(--im-message-right-bg-color);
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import 'xgplayer/dist/index.min.css'
|
import 'xgplayer/dist/index.min.css'
|
||||||
import { ref, nextTick } from 'vue'
|
import { ref, nextTick, watch } from 'vue'
|
||||||
import { NImage, NModal, NCard } from 'naive-ui'
|
import { NImage, NModal, NCard, NProgress, NPopconfirm } from 'naive-ui'
|
||||||
import { Play, Close } from '@icon-park/vue-next'
|
import { Play, Close, Pause, Right, Attention } from '@icon-park/vue-next'
|
||||||
import { getImageInfo } from '@/utils/functions'
|
import { getImageInfo } from '@/utils/functions'
|
||||||
|
import {PauseOutline} from '@vicons/ionicons5'
|
||||||
import Player from 'xgplayer'
|
import Player from 'xgplayer'
|
||||||
import { ITalkRecordExtraVideo, ITalkRecord } from '@/types/chat'
|
import { ITalkRecordExtraVideo, ITalkRecord } from '@/types/chat'
|
||||||
|
import { useUploadsStore } from '@/store'
|
||||||
|
// @ts-ignore
|
||||||
|
const message = window.$message
|
||||||
|
|
||||||
|
const uploadsStore = useUploadsStore()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
extra: ITalkRecordExtraVideo
|
extra: ITalkRecordExtraVideo
|
||||||
@ -40,8 +46,43 @@ const img = (src: string, width = 200) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const open = ref(false)
|
const open = ref(false)
|
||||||
|
const isPaused = ref(false)
|
||||||
|
const uploadFailed = ref(false)
|
||||||
|
|
||||||
|
// 查找上传项并检查状态
|
||||||
|
const updatePauseStatus = () => {
|
||||||
|
if (props.extra.is_uploading && props.extra.upload_id) {
|
||||||
|
// 使用新的查找方法
|
||||||
|
const item = uploadsStore.findItemByClientId(props.extra.upload_id)
|
||||||
|
|
||||||
|
if (item && item.is_paused !== undefined) {
|
||||||
|
isPaused.value = item.is_paused
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化时检查状态
|
||||||
|
updatePauseStatus()
|
||||||
|
|
||||||
|
// 监听关键道具变化
|
||||||
|
watch(() => props.extra.percentage, (newVal: number | undefined) => {
|
||||||
|
// 确保进度更新时 UI 也实时更新
|
||||||
|
// 检测上传失败状态 (-1表示上传失败)
|
||||||
|
if (newVal === -1) {
|
||||||
|
uploadFailed.value = true
|
||||||
|
// 显示上传失败提示
|
||||||
|
message.error('视频发送失败,请点击红色感叹号重试')
|
||||||
|
} else if (newVal !== undefined && newVal > 0) {
|
||||||
|
uploadFailed.value = false
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
async function onPlay() {
|
async function onPlay() {
|
||||||
|
// 如果视频正在上传,不执行播放操作
|
||||||
|
if (props.extra.is_uploading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
open.value = true
|
open.value = true
|
||||||
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
@ -54,18 +95,102 @@ async function onPlay() {
|
|||||||
lang: 'zh-cn'
|
lang: 'zh-cn'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 暂停上传
|
||||||
|
function pauseUpload(e) {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (props.extra.is_uploading && props.extra.upload_id) {
|
||||||
|
uploadsStore.pauseVideoUpload(props.extra.upload_id)
|
||||||
|
isPaused.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 继续上传
|
||||||
|
function resumeUpload(e) {
|
||||||
|
console.log('resumeUpload')
|
||||||
|
e.stopPropagation()
|
||||||
|
if (props.extra.is_uploading && props.extra.upload_id) {
|
||||||
|
uploadsStore.resumeVideoUpload(props.extra.upload_id)
|
||||||
|
isPaused.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新上传视频
|
||||||
|
function retryUpload(e) {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (props.extra.upload_id) {
|
||||||
|
// 重置失败状态
|
||||||
|
uploadFailed.value = false
|
||||||
|
|
||||||
|
// 恢复上传
|
||||||
|
uploadsStore.resumeVideoUpload(props.extra.upload_id)
|
||||||
|
message.success('正在重新上传视频...')
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<section
|
<section
|
||||||
class="im-message-video"
|
class="im-message-video"
|
||||||
:class="{ left: data.float === 'left' }"
|
:class="{ left: data.float === 'left' }"
|
||||||
:style="img(extra.cover, 350)"
|
|
||||||
@click="onPlay"
|
@click="onPlay"
|
||||||
>
|
>
|
||||||
<n-image :src="extra.cover" preview-disabled />
|
|
||||||
|
<!-- <n-image :src="extra.cover" preview-disabled /> -->
|
||||||
|
<video :src="props.extra.url" :controls="false"></video>
|
||||||
|
|
||||||
|
<!-- 上传进度显示 -->
|
||||||
|
<div v-if="extra.is_uploading && !uploadFailed" class="upload-progress">
|
||||||
|
<n-progress
|
||||||
|
|
||||||
|
type="circle"
|
||||||
|
:percentage="Math.round(extra.percentage || 0)"
|
||||||
|
:show-indicator="false"
|
||||||
|
:stroke-width="6"
|
||||||
|
color="#fff"
|
||||||
|
rail-color="#E3E3E3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 暂停/继续按钮移到圆圈内部 -->
|
||||||
|
<div class="upload-control" @click.stop>
|
||||||
|
<n-icon
|
||||||
|
v-if="!isPaused"
|
||||||
|
class="control-btn"
|
||||||
|
:component="PauseOutline"
|
||||||
|
size="20"
|
||||||
|
@click="pauseUpload"
|
||||||
|
/>
|
||||||
|
<div v-else class="w-15px h-15px bg-#fff rounded-4px" @click="resumeUpload" >
|
||||||
|
|
||||||
<div class="btn-video">
|
</div>
|
||||||
<n-icon :component="Play" size="36" />
|
<!-- <n-icon
|
||||||
|
v-else
|
||||||
|
class="control-btn"
|
||||||
|
:component="Right"
|
||||||
|
size="20"
|
||||||
|
@click="resumeUpload"
|
||||||
|
/> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 上传失败显示 -->
|
||||||
|
<div v-if="uploadFailed" class="upload-failed" @click.stop>
|
||||||
|
<n-popconfirm
|
||||||
|
placement="right"
|
||||||
|
@positive-click="retryUpload"
|
||||||
|
positive-text="重新发送"
|
||||||
|
negative-text="取消"
|
||||||
|
>
|
||||||
|
<template #trigger>
|
||||||
|
<div class="failed-icon">
|
||||||
|
<n-icon :component="Attention" size="22" color="#ff4d4f" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
确认重新发送该视频消息吗?
|
||||||
|
</n-popconfirm>
|
||||||
|
</div>
|
||||||
|
<!-- 播放按钮,仅在视频不是上传状态且未失败时显示 -->
|
||||||
|
<div v-if="!extra.is_uploading && !uploadFailed" class="btn-video">
|
||||||
|
<n-icon :component="Play" size="40" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n-modal v-model:show="open">
|
<n-modal v-model:show="open">
|
||||||
@ -92,23 +217,24 @@ async function onPlay() {
|
|||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
height:149px;
|
||||||
&.left {
|
&.left {
|
||||||
background: var(--im-message-right-bg-color);
|
background: var(--im-message-right-bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.n-image img) {
|
video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
object-fit: cover;
|
||||||
|
background-color: #333; /* 添加背景色,避免默认显示为灰色 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-video {
|
.btn-video {
|
||||||
width: 30px;
|
left: 50%;
|
||||||
height: 20px;
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: calc(50% - 15px);
|
|
||||||
top: calc(50% - 10px);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
@ -134,4 +260,54 @@ async function onPlay() {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-progress {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.upload-control {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
color: white;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上传失败样式 */
|
||||||
|
.upload-failed {
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
bottom: 10px;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
.failed-icon {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -177,14 +177,14 @@ const onAfterEnter = () => {
|
|||||||
<img class="w-20px h-20px" src="@/assets/image/close.png" alt="">
|
<img class="w-20px h-20px" src="@/assets/image/close.png" alt="">
|
||||||
</div>
|
</div>
|
||||||
<div class="flex py-10px bg-#fff px-16px rounded-4px items-center mb-10px">
|
<div class="flex py-10px bg-#fff px-16px rounded-4px items-center mb-10px">
|
||||||
<div class="w-59px h-59px bg-#46299D rounded-8px mr-12px overflow-hidden">
|
<div class="w-59px h-59px rounded-8px mr-12px overflow-hidden">
|
||||||
<n-image width="59" :src="state.avatar" >
|
<n-image width="59" :src="state.avatar" >
|
||||||
|
|
||||||
</n-image>
|
</n-image>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-#000 text-16px mb-5px">张三</div>
|
<div class="text-#000 text-16px mb-5px">{{ state.nickname }}</div>
|
||||||
<div class="text-#ACACAC text-12px">工号:FL043</div>
|
<div class="text-#ACACAC text-12px">工号:{{ state.job_num }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-#fff rounded-4px mb-20px">
|
<div class="bg-#fff rounded-4px mb-20px">
|
||||||
@ -202,7 +202,7 @@ const onAfterEnter = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex px-15px py-9px">
|
<div class="flex px-15px py-9px">
|
||||||
<div class="text-#000 text-12px w-84px">手机号</div>
|
<div class="text-#000 text-12px w-84px">手机号</div>
|
||||||
<div class="text-#747474 text-12px">江苏泰丰文化传播股份有限公司</div>
|
<div class="text-#747474 text-12px">{{ state.mobile }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex px-15px py-9px">
|
<div class="flex px-15px py-9px">
|
||||||
<div class="text-#000 text-12px w-84px">岗位</div>
|
<div class="text-#000 text-12px w-84px">岗位</div>
|
||||||
|
@ -226,6 +226,30 @@ export const useDialogueStore = defineStore('dialogue', {
|
|||||||
useEditorStore().loadUserEmoticon()
|
useEditorStore().loadUserEmoticon()
|
||||||
window['$message'] && window['$message'].success('收藏成功')
|
window['$message'] && window['$message'].success('收藏成功')
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新视频上传进度
|
||||||
|
updateUploadProgress(uploadId, percentage) {
|
||||||
|
const record = this.records.find(item =>
|
||||||
|
item.extra && item.extra.is_uploading && item.extra.upload_id === uploadId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (record) {
|
||||||
|
record.extra.percentage = percentage
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 视频上传完成后更新消息
|
||||||
|
completeUpload(uploadId, videoInfo) {
|
||||||
|
const record = this.records.find(item =>
|
||||||
|
item.extra && item.extra.is_uploading && item.extra.upload_id === uploadId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (record) {
|
||||||
|
record.extra.is_uploading = false
|
||||||
|
record.extra.url = videoInfo.url
|
||||||
|
// record.extra.cover = videoInfo.cover
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,10 +1,34 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ServeFindFileSplitInfo, ServeFileSubareaUpload } from '@/api/upload'
|
import { ServeFindFileSplitInfo, ServeFileSubareaUpload } from '@/api/upload'
|
||||||
import { ServeSendTalkFile } from '@/api/chat'
|
import { ServeSendTalkFile } from '@/api/chat'
|
||||||
|
import { uploadImg } from '@/api/upload'
|
||||||
|
import {
|
||||||
|
useDialogueStore
|
||||||
|
} from '@/store'
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const message = window.$message
|
const message = window.$message
|
||||||
|
|
||||||
|
// 定义上传项接口
|
||||||
|
interface UploadItem {
|
||||||
|
file: File;
|
||||||
|
talk_type: number;
|
||||||
|
receiver_id: number;
|
||||||
|
upload_id: string;
|
||||||
|
client_upload_id?: string; // 上传时的客户端ID
|
||||||
|
uploadIndex: number;
|
||||||
|
percentage: number;
|
||||||
|
status: number; // 文件上传状态 0:等待上传 1:上传中 2:上传完成 3:网络异常
|
||||||
|
files: FormData[];
|
||||||
|
avatar: string;
|
||||||
|
username: string;
|
||||||
|
is_paused?: boolean; // 是否暂停上传
|
||||||
|
form?: FormData;
|
||||||
|
progress_interval?: any;
|
||||||
|
upload_controller?: AbortController;
|
||||||
|
onProgress?: (percentage: number) => void;
|
||||||
|
onComplete?: (data: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
// 处理拆分上传文件
|
// 处理拆分上传文件
|
||||||
function fileSlice(file: File, uploadId: string, eachSize: number) {
|
function fileSlice(file: File, uploadId: string, eachSize: number) {
|
||||||
const splitNum = Math.ceil(file.size / eachSize) // 分片总数
|
const splitNum = Math.ceil(file.size / eachSize) // 分片总数
|
||||||
@ -31,7 +55,8 @@ export const useUploadsStore = defineStore('uploads', {
|
|||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
isShow: false,
|
isShow: false,
|
||||||
items: []
|
items: [] as UploadItem[],
|
||||||
|
dialogueStore: useDialogueStore()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
@ -45,81 +70,282 @@ export const useUploadsStore = defineStore('uploads', {
|
|||||||
close() {
|
close() {
|
||||||
this.isShow = false
|
this.isShow = false
|
||||||
},
|
},
|
||||||
|
// 获取分片文件数组索引
|
||||||
|
findItem(uploadId: string): UploadItem | undefined {
|
||||||
|
return this.items.find((item) => item.upload_id === uploadId)
|
||||||
|
},
|
||||||
|
|
||||||
// 初始化上传
|
// 通过客户端ID查找上传项
|
||||||
initUploadFile(file: File, talkType: number, receiverId: number, username: string) {
|
findItemByClientId(clientUploadId: string): UploadItem | undefined {
|
||||||
ServeFindFileSplitInfo({
|
return this.items.find((item) => item.client_upload_id === clientUploadId)
|
||||||
file_name: file.name,
|
},
|
||||||
file_size: file.size
|
|
||||||
}).then((res) => {
|
// 暂停文件上传
|
||||||
|
pauseUpload(uploadId: string) {
|
||||||
|
const item = this.findItem(uploadId)
|
||||||
|
if (!item) return
|
||||||
|
|
||||||
|
item.is_paused = true
|
||||||
|
console.log(`暂停上传: ${uploadId}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 恢复文件上传
|
||||||
|
resumeUpload(uploadId: string) {
|
||||||
|
const item = this.findItem(uploadId)
|
||||||
|
if (!item) return
|
||||||
|
|
||||||
|
item.is_paused = false
|
||||||
|
console.log(`恢复上传: ${uploadId}`)
|
||||||
|
|
||||||
|
// 继续上传
|
||||||
|
this.triggerUpload(uploadId)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 发送上传消息
|
||||||
|
async sendUploadMessage(item: any) {
|
||||||
|
try {
|
||||||
|
await ServeSendTalkFile({
|
||||||
|
upload_id: item.upload_id,
|
||||||
|
receiver_id: item.receiver_id,
|
||||||
|
talk_type: item.talk_type
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("发送上传消息失败:", error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 初始化视频上传(使用分片上传方式)
|
||||||
|
async initUploadFile(
|
||||||
|
file: File,
|
||||||
|
talkType: number,
|
||||||
|
receiverId: number,
|
||||||
|
username: string,
|
||||||
|
uploadId: string,
|
||||||
|
onProgress: (percentage: number) => void,
|
||||||
|
onComplete: (data: any) => void
|
||||||
|
) {
|
||||||
|
// 使用分片上传机制,先获取分片信息
|
||||||
|
try {
|
||||||
|
const res = await ServeFindFileSplitInfo({
|
||||||
|
file_name: file.name,
|
||||||
|
file_size: file.size
|
||||||
|
})
|
||||||
|
|
||||||
if (res.code == 200) {
|
if (res.code == 200) {
|
||||||
const { upload_id, split_size } = res.data
|
const { upload_id, split_size } = res.data
|
||||||
|
|
||||||
|
// 使用较小的分片大小,以获得更细粒度的进度控制
|
||||||
|
// 将分片大小减半,增加分片数量
|
||||||
|
const actualSplitSize = Math.min(split_size, 512 * 1024); // 使用更小的分片,如512KB
|
||||||
|
|
||||||
|
// 创建分片数组
|
||||||
|
const fileChunks = fileSlice(file, upload_id, actualSplitSize)
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.items.unshift({
|
this.items.unshift({
|
||||||
file: file,
|
file: file,
|
||||||
talk_type: talkType,
|
talk_type: talkType,
|
||||||
receiver_id: receiverId,
|
receiver_id: receiverId,
|
||||||
upload_id: upload_id,
|
upload_id: upload_id,
|
||||||
|
client_upload_id: uploadId, // 客户端生成的上传ID,用于前端标识
|
||||||
uploadIndex: 0,
|
uploadIndex: 0,
|
||||||
percentage: 0,
|
percentage: 0,
|
||||||
status: 0, // 文件上传状态 0:等待上传 1:上传中 2:上传完成 3:网络异常
|
status: 0, // 文件上传状态 0:等待上传 1:上传中 2:上传完成 3:网络异常
|
||||||
files: fileSlice(file, upload_id, split_size),
|
files: fileChunks,
|
||||||
avatar: '',
|
avatar: '',
|
||||||
username: username
|
username: username,
|
||||||
|
is_paused: false,
|
||||||
|
onProgress: onProgress,
|
||||||
|
onComplete: onComplete,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.triggerUpload(upload_id)
|
this.isShow = false // 不显示上传管理抽屉
|
||||||
this.isShow = true
|
|
||||||
|
// 开始上传分片
|
||||||
|
this.triggerUpload(upload_id, uploadId)
|
||||||
} else {
|
} else {
|
||||||
message.error(res.message)
|
message.error(res.message)
|
||||||
|
onProgress(-1) // 通知上传失败
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
|
console.error("初始化分片上传失败:", error);
|
||||||
|
message.error("初始化上传失败,请重试")
|
||||||
|
onProgress(-1)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取分片文件数组索引
|
// 触发分片上传
|
||||||
findItem(uploadId: string): any {
|
async triggerUpload(uploadId: string, clientUploadId?: string) {
|
||||||
return this.items.find((item: any) => item.upload_id === uploadId)
|
const currentItem = this.findItem(uploadId)
|
||||||
},
|
if (!currentItem) return
|
||||||
|
|
||||||
// 触发上传
|
// 如果已暂停,不继续上传
|
||||||
triggerUpload(uploadId: string) {
|
if (currentItem.is_paused) return
|
||||||
const item = this.findItem(uploadId)
|
|
||||||
|
// 如果已上传完成,不继续上传
|
||||||
const form = item.files[item.uploadIndex]
|
if (currentItem.uploadIndex >= currentItem.files.length) {
|
||||||
|
if (clientUploadId) {
|
||||||
item.status = 1
|
this.completeUpload(currentItem, clientUploadId)
|
||||||
|
}
|
||||||
ServeFileSubareaUpload(form)
|
return
|
||||||
.then((res) => {
|
}
|
||||||
if (res.code == 200) {
|
|
||||||
item.uploadIndex++
|
// 获取当前要上传的分片
|
||||||
|
const form = currentItem.files[currentItem.uploadIndex]
|
||||||
if (item.uploadIndex === item.files.length) {
|
|
||||||
item.status = 2
|
// 更新状态为上传中
|
||||||
item.percentage = 100
|
currentItem.status = 1
|
||||||
this.sendUploadMessage(item)
|
|
||||||
} else {
|
// 上传当前分片
|
||||||
const percentage = (item.uploadIndex / item.files.length) * 100
|
try {
|
||||||
item.percentage = percentage.toFixed(1)
|
const res = await ServeFileSubareaUpload(form)
|
||||||
this.triggerUpload(uploadId)
|
|
||||||
|
// 获取最新的项目状态,确保仍然存在且没有被暂停
|
||||||
|
const updatedItem = this.findItem(uploadId)
|
||||||
|
if (!updatedItem || updatedItem.is_paused) return
|
||||||
|
|
||||||
|
if (res.code == 200) {
|
||||||
|
// 当前分片上传成功,增加索引
|
||||||
|
updatedItem.uploadIndex++
|
||||||
|
|
||||||
|
// 计算上传进度
|
||||||
|
const percentage = (updatedItem.uploadIndex / updatedItem.files.length) * 100
|
||||||
|
updatedItem.percentage = parseFloat(percentage.toFixed(1))
|
||||||
|
|
||||||
|
// 回调进度
|
||||||
|
if (updatedItem.onProgress) {
|
||||||
|
updatedItem.onProgress(updatedItem.percentage)
|
||||||
|
}
|
||||||
|
// if (clientUploadId) {
|
||||||
|
// this.dialogueStore.updateUploadProgress(clientUploadId, percentage)
|
||||||
|
// }
|
||||||
|
// 检查是否全部上传完成
|
||||||
|
if (updatedItem.uploadIndex === updatedItem.files.length) {
|
||||||
|
// 所有分片上传完成
|
||||||
|
if (clientUploadId) {
|
||||||
|
this.completeUpload(updatedItem, clientUploadId)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
item.status = 3
|
// 继续上传下一个分片
|
||||||
|
this.triggerUpload(uploadId, clientUploadId)
|
||||||
}
|
}
|
||||||
})
|
} else {
|
||||||
.catch(() => {
|
// 上传失败处理
|
||||||
item.status = 3
|
console.error(`分片上传失败,错误码: ${res.code},错误信息: ${res.message || '未知错误'}`);
|
||||||
})
|
updatedItem.status = 3
|
||||||
|
|
||||||
|
// 尝试重试当前分片
|
||||||
|
this.retryUpload(uploadId, clientUploadId, res.message || '上传失败,请重试')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("分片上传错误:", error);
|
||||||
|
|
||||||
|
// 获取最新的项目状态
|
||||||
|
const updatedItem = this.findItem(uploadId)
|
||||||
|
if (!updatedItem) return
|
||||||
|
|
||||||
|
// 如果是暂停导致的错误,不改变状态
|
||||||
|
if (updatedItem.is_paused) return
|
||||||
|
|
||||||
|
updatedItem.status = 3
|
||||||
|
|
||||||
|
// 尝试重试当前分片
|
||||||
|
this.retryUpload(uploadId, clientUploadId, '网络错误,正在重试')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 重试上传
|
||||||
|
retryUpload(uploadId: string, clientUploadId?: string, errorMessage?: string) {
|
||||||
|
const item = this.findItem(uploadId)
|
||||||
|
if (!item) return
|
||||||
|
|
||||||
|
// 如果有暂停/恢复按钮,先告知用户上传出错
|
||||||
|
if (item.onProgress) {
|
||||||
|
item.onProgress(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误提示
|
||||||
|
message.warning(errorMessage)
|
||||||
|
|
||||||
|
// 创建一个5秒后自动重试的机制
|
||||||
|
setTimeout(() => {
|
||||||
|
const currentItem = this.findItem(uploadId)
|
||||||
|
if (!currentItem) return
|
||||||
|
|
||||||
|
// 如果用户没有手动暂停,则自动重试
|
||||||
|
if (!currentItem.is_paused) {
|
||||||
|
console.log('正在重试上传分片...');
|
||||||
|
this.triggerUpload(uploadId, clientUploadId)
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 完成上传
|
||||||
|
async completeUpload(item: UploadItem, clientUploadId: string) {
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
item.status = 2
|
||||||
|
item.percentage = 100
|
||||||
|
if (item.onProgress) {
|
||||||
|
item.onProgress(100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最终URL并回调
|
||||||
|
try {
|
||||||
|
await ServeSendTalkFile({
|
||||||
|
upload_id: item.upload_id,
|
||||||
|
receiver_id: item.receiver_id,
|
||||||
|
talk_type: item.talk_type
|
||||||
|
})
|
||||||
|
|
||||||
|
if (item.onComplete) {
|
||||||
|
item.onComplete(item)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("发送文件消息失败:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 暂停视频上传
|
||||||
|
pauseVideoUpload(clientUploadId: string) {
|
||||||
|
const item = this.findItemByClientId(clientUploadId)
|
||||||
|
if (!item) return
|
||||||
|
|
||||||
|
item.is_paused = true
|
||||||
|
},
|
||||||
|
|
||||||
|
// 恢复视频上传
|
||||||
|
resumeVideoUpload(clientUploadId: string) {
|
||||||
|
const item = this.findItemByClientId(clientUploadId)
|
||||||
|
if (!item) return
|
||||||
|
|
||||||
|
item.is_paused = false
|
||||||
|
|
||||||
|
// 继续上传
|
||||||
|
if (item.upload_id) {
|
||||||
|
this.triggerUpload(item.upload_id, clientUploadId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 重试文件上传
|
||||||
|
retryCommonUpload(uploadId: string, errorMessage: string) {
|
||||||
|
const item = this.findItem(uploadId)
|
||||||
|
if (!item) return
|
||||||
|
|
||||||
|
// 显示错误提示
|
||||||
|
message.warning(errorMessage)
|
||||||
|
|
||||||
|
// 创建一个5秒后自动重试的机制
|
||||||
|
setTimeout(() => {
|
||||||
|
const currentItem = this.findItem(uploadId)
|
||||||
|
if (!currentItem) return
|
||||||
|
|
||||||
|
// 如果用户没有手动暂停,则自动重试
|
||||||
|
if (!currentItem.is_paused) {
|
||||||
|
console.log('正在重试上传分片...');
|
||||||
|
this.triggerUpload(uploadId)
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 发送上传消息
|
|
||||||
sendUploadMessage(item: any) {
|
|
||||||
ServeSendTalkFile({
|
|
||||||
upload_id: item.upload_id,
|
|
||||||
receiver_id: item.receiver_id,
|
|
||||||
talk_type: item.talk_type
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -65,6 +65,7 @@ export interface ITalkRecordExtraFile {
|
|||||||
name: string
|
name: string
|
||||||
path: string
|
path: string
|
||||||
size: number
|
size: number
|
||||||
|
percentage: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITalkRecordExtraForward {
|
export interface ITalkRecordExtraForward {
|
||||||
@ -90,6 +91,9 @@ export interface ITalkRecordExtraVideo {
|
|||||||
url: string
|
url: string
|
||||||
duration: number
|
duration: number
|
||||||
size: number
|
size: number
|
||||||
|
is_uploading?: boolean
|
||||||
|
upload_id?: string
|
||||||
|
percentage?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITalkRecordExtraMixed {
|
export interface ITalkRecordExtraMixed {
|
||||||
|
@ -18,7 +18,7 @@ export function isLoggedIn() {
|
|||||||
*/
|
*/
|
||||||
export function getAccessToken() {
|
export function getAccessToken() {
|
||||||
// return storage.get(AccessToken) || ''
|
// return storage.get(AccessToken) || ''
|
||||||
return JSON.parse(localStorage.getItem('token'))||'79b5c732d96d2b27a48a99dfd4a5566c43aaa5796242e854ebe3ffc198d6876b9628e7b764d9af65ab5dbb2d517ced88170491b74b048c0ba827c0d3741462cb89dc59ed46653a449af837a8262941caaef1334d640773710f8cd96473bacfb190cba595a5d6a9c87d70f0999a3ebb41147213b31b4bdccffca66a56acf3baab5af0154f0dce360079f37709f78e13711036899344bddb0fb4cf0f2890287cb62c3fcbe33368caa5e213624577be8b8420ab75b1f50775ee16142a4321c5d56995f37354a66a969da98d95ba6e65d142ed097e04b411c1ebad2f62866d0ec7e1838420530a9941dbbcd00490199f8b891bd3a81a1ac4e73e2aed60deeaec60792c525cc0c96e8f4a666eca6ee7a10716507b402cde5759bbcda1fa681fbe4dcdfe05abbc2b1644c68dc74ebaf8d9c9cc4eb61afaf3de52fa357dbfdfe17acf14'
|
return JSON.parse(localStorage.getItem('token'))||'79b5c732d96d2b27a48a99dfd4a5566c43aaa5796242e854ebe3ffc198d6876b9628e7b764d9af65ab5dbb2d517ced88170491b74b048c0ba827c0d3741462cb89dc59ed46653a449af837a8262941caaef1334d640773710f8cd96473bacfb190cba595a5d6a9c87d70f0999a3ebb41147213b31b4bdccffca66a56acf3baab5af0154f0dce360079f37709f78e13711036899344bddb0fb4cf0f2890287cb62c3fcbe33368caa5e213624577be8b8420ab75b1f50775ee16142a4321c5d56995f37354a66a969da98d95ba6e65d142ed097e04b411c1ebad2f62866d0ec7e1838420530a9941dbbcd00490199f8b89574a563986daa80674dd774ef18032ee6016a202902c95452e1e81931358d4d3cb7f0db0c6fc66f406f57e411cb1e2aeb77318f7c36b2b61f48c4c645d27920f05c204fe133ab9bfa481e9c1ae2e384c'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,7 +47,9 @@ watch(() => records, (newValue, oldValue) => {
|
|||||||
|
|
||||||
// 置底按钮
|
// 置底按钮
|
||||||
const skipBottom = ref(false)
|
const skipBottom = ref(false)
|
||||||
|
setTimeout(()=>{
|
||||||
|
console.log(records.value,'records.value');
|
||||||
|
},1000)
|
||||||
// 是否显示消息时间
|
// 是否显示消息时间
|
||||||
const isShowTalkTime = (index: number, datetime: string) => {
|
const isShowTalkTime = (index: number, datetime: string) => {
|
||||||
if (datetime == undefined) {
|
if (datetime == undefined) {
|
||||||
@ -368,7 +370,7 @@ onMounted(() => {
|
|||||||
@contextmenu.prevent="onContextMenu($event, item)"
|
@contextmenu.prevent="onContextMenu($event, item)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="talk-tools">
|
<!-- <div class="talk-tools">
|
||||||
<template v-if="talk_type == 1 && item.float == 'right'">
|
<template v-if="talk_type == 1 && item.float == 'right'">
|
||||||
<loading
|
<loading
|
||||||
theme="outline"
|
theme="outline"
|
||||||
@ -380,7 +382,7 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<span v-show="item.send_status == 1"> 正在发送... </span>
|
<span v-show="item.send_status == 1"> 正在发送... </span>
|
||||||
<!-- <span v-show="item.send_status != 1"> 已送达 </span> -->
|
<span v-show="item.send_status != 1"> 已送达 </span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<n-icon
|
<n-icon
|
||||||
@ -388,7 +390,7 @@ onMounted(() => {
|
|||||||
:component="MoreThree"
|
:component="MoreThree"
|
||||||
@click="onContextMenu($event, item)"
|
@click="onContextMenu($event, item)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -5,16 +5,18 @@ import {
|
|||||||
useDialogueStore,
|
useDialogueStore,
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
useUploadsStore,
|
useUploadsStore,
|
||||||
useEditorStore
|
useEditorStore,
|
||||||
|
useUserStore
|
||||||
} from '@/store'
|
} from '@/store'
|
||||||
import ws from '@/connect'
|
import ws from '@/connect'
|
||||||
import { ServePublishMessage, ServeSendVote } from '@/api/chat'
|
import { ServePublishMessage, ServeSendVote } from '@/api/chat'
|
||||||
import { throttle, getVideoImage } from '@/utils/common'
|
import { throttle, getVideoImage } from '@/utils/common'
|
||||||
|
import { parseTime } from '@/utils/datetime'
|
||||||
import Editor from '@/components/editor/Editor.vue'
|
import Editor from '@/components/editor/Editor.vue'
|
||||||
import MultiSelectFooter from './MultiSelectFooter.vue'
|
import MultiSelectFooter from './MultiSelectFooter.vue'
|
||||||
import HistoryRecord from '@/components/talk/HistoryRecord.vue'
|
import HistoryRecord from '@/components/talk/HistoryRecord.vue'
|
||||||
import { uploadImg } from '@/api/upload'
|
import { uploadImg } from '@/api/upload'
|
||||||
|
const userStore = useUserStore()
|
||||||
const talkStore = useTalkStore()
|
const talkStore = useTalkStore()
|
||||||
const editorStore = useEditorStore()
|
const editorStore = useEditorStore()
|
||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
@ -98,26 +100,80 @@ const onSendImageEvent = ({ data, callBack }) => {
|
|||||||
|
|
||||||
// 发送视频消息
|
// 发送视频消息
|
||||||
const onSendVideoEvent = async ({ data }) => {
|
const onSendVideoEvent = async ({ data }) => {
|
||||||
let resp = await getVideoImage(data)
|
console.log('onSendVideoEvent')
|
||||||
const form = new FormData()
|
|
||||||
form.append('file', data)
|
// 获取视频首帧作为封面图
|
||||||
form.append("source", "fonchain-chat");
|
// let resp = await getVideoImage(data)
|
||||||
form.append("type", "video");
|
|
||||||
form.append("urlParam", `width=${resp.width}&height=${resp.height}`);
|
// 先创建一个带有上传ID的临时消息对象,用于显示进度
|
||||||
|
const uploadId = `video-${Date.now()}-${Math.floor(Math.random() * 1000)}`
|
||||||
console.log(form.get('file'));
|
|
||||||
let video = await uploadImg(form)
|
// 创建临时消息记录
|
||||||
if (video.code != 0) return
|
const tempMessage = {
|
||||||
|
msg_id: uploadId,
|
||||||
let message = {
|
sequence: Date.now(),
|
||||||
type: 'video',
|
talk_type: props.talk_type,
|
||||||
url: video.data.ori_url,
|
msg_type: 5, // 视频消息类型
|
||||||
cover: video.data.cover_url,
|
user_id: props.uid,
|
||||||
duration: parseInt(resp.duration),
|
receiver_id: props.receiver_id,
|
||||||
size: data.size
|
nickname: '我', // 本地显示
|
||||||
|
avatar: userStore.avatar, // 本地显示可能不需要
|
||||||
|
is_revoke: 0,
|
||||||
|
is_mark: 0,
|
||||||
|
is_read: 1,
|
||||||
|
content: '',
|
||||||
|
created_at: parseTime(new Date(), '{y}-{m}-{d} {h}:{i}'),
|
||||||
|
extra: {
|
||||||
|
url: '', // 上传完成后会更新
|
||||||
|
size: data.size,
|
||||||
|
is_uploading: true,
|
||||||
|
upload_id: uploadId,
|
||||||
|
percentage: 0
|
||||||
|
},
|
||||||
|
isCheck: false,
|
||||||
|
send_status: 1,
|
||||||
|
float: 'right' // 我发送的消息显示在右侧
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 直接添加到对话记录中
|
||||||
|
dialogueStore.addDialogueRecord(tempMessage)
|
||||||
|
uploadsStore.initUploadFile(
|
||||||
|
data,
|
||||||
|
props.talk_type,
|
||||||
|
props.receiver_id,
|
||||||
|
dialogueStore.talk.username,
|
||||||
|
uploadId,
|
||||||
|
async (percentage) => {
|
||||||
|
console.log('percentage', percentage)
|
||||||
|
// 更新消息进度的回调
|
||||||
|
dialogueStore.updateUploadProgress(uploadId, percentage)
|
||||||
|
},
|
||||||
|
async (videoData) => {
|
||||||
|
console.log('videoData', videoData)
|
||||||
|
// 上传完成后的回调
|
||||||
|
if (videoData.code != 0) return
|
||||||
|
|
||||||
|
// 更新临时消息为最终消息
|
||||||
|
dialogueStore.completeUpload(uploadId, {
|
||||||
|
url: videoData.data.ori_url,
|
||||||
|
cover: videoData.data.cover_url
|
||||||
|
})
|
||||||
|
|
||||||
|
// 上传成功后,发送正式消息给服务端
|
||||||
|
let finalMessage = {
|
||||||
|
type: 'video',
|
||||||
|
url: videoData.data.ori_url,
|
||||||
|
|
||||||
onSendMessage(message, () => {})
|
size: data.size
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送真实消息到服务端
|
||||||
|
onSendMessage(finalMessage, () => {
|
||||||
|
// 上传成功且消息发送成功后,删除临时消息
|
||||||
|
dialogueStore.batchDelDialogueRecord([uploadId])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送代码消息
|
// 发送代码消息
|
||||||
@ -131,8 +187,43 @@ const onSendFileEvent = ({ data }) => {
|
|||||||
if (data.size > maxsize) {
|
if (data.size > maxsize) {
|
||||||
return window['$message'].warning('上传文件不能超过100M!')
|
return window['$message'].warning('上传文件不能超过100M!')
|
||||||
}
|
}
|
||||||
|
const uploadId = `file-${Date.now()}-${Math.floor(Math.random() * 1000)}`
|
||||||
|
|
||||||
uploadsStore.initUploadFile(data, props.talk_type, props.receiver_id, dialogueStore.talk.username)
|
const tempMessage = {
|
||||||
|
msg_id: uploadId,
|
||||||
|
sequence: Date.now(),
|
||||||
|
talk_type: props.talk_type,
|
||||||
|
msg_type: 6,
|
||||||
|
user_id: props.uid,
|
||||||
|
receiver_id: props.receiver_id,
|
||||||
|
nickname: dialogueStore.talk.username,
|
||||||
|
avatar: userStore.avatar,
|
||||||
|
is_revoke: 0,
|
||||||
|
is_read: 0,
|
||||||
|
created_at: parseTime(new Date(), '{y}-{m}-{d} {h}:{i}'),
|
||||||
|
extra: {
|
||||||
|
name: data.name,
|
||||||
|
url: '',
|
||||||
|
size: data.size,
|
||||||
|
is_uploading: true,
|
||||||
|
upload_id: uploadId,
|
||||||
|
percentage: 0
|
||||||
|
},
|
||||||
|
erp_user_id: 4692,
|
||||||
|
float: 'right'
|
||||||
|
}
|
||||||
|
dialogueStore.addDialogueRecord(tempMessage)
|
||||||
|
|
||||||
|
uploadsStore.initUploadFile(data, props.talk_type, props.receiver_id, dialogueStore.talk.username,uploadId,
|
||||||
|
async (percentage) => {
|
||||||
|
dialogueStore.updateUploadProgress(uploadId, percentage)
|
||||||
|
},
|
||||||
|
async (data) => {
|
||||||
|
console.log('data', data)
|
||||||
|
// 上传完成后的回调
|
||||||
|
if (data.code != 0) return
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送投票消息
|
// 发送投票消息
|
||||||
|
Loading…
Reference in New Issue
Block a user