更新组件和API,添加NProgress和NTag支持,优化上传功能,增强编辑器功能,调整样式和结构,提升用户体验。

This commit is contained in:
Phoenix 2025-05-14 11:50:52 +08:00
parent 651baafd0f
commit 661472a70a
15 changed files with 999 additions and 301 deletions

2
components.d.ts vendored
View File

@ -57,6 +57,8 @@ declare module 'vue' {
NoticeEditor: typeof import('./src/components/group/manage/NoticeEditor.vue')['default']
NoticeTab: typeof import('./src/components/group/manage/NoticeTab.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']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

View File

@ -21,6 +21,9 @@ export const ServeFileSubareaUpload = (data = {}, options = {}) => {
}
// 上传图片文件或者视频
export const uploadImg = (data) => {
return post('/upload/img', data,{baseURL:import.meta.env.VITE_EPR_BASEURL})
export const uploadImg = (data, signal) => {
return post('/upload/img', data, {
baseURL: import.meta.env.VITE_EPR_BASEURL,
signal: signal
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

View File

@ -1,81 +1,117 @@
<script lang="ts" setup>
// Quill
import '@vueup/vue-quill/dist/vue-quill.snow.css'
//
import 'quill-image-uploader/dist/quill.imageUploader.min.css'
//
import '@/assets/css/editor-mention.less'
// Vue
import { reactive, watch, ref, markRaw, computed, onMounted, onUnmounted } from 'vue'
// Naive UI
import { NPopover } from 'naive-ui'
//
import {
Voice as IconVoice,
SourceCode,
Local,
SmilingFace,
Pic,
FolderUpload,
Ranking,
History
Voice as IconVoice, //
SourceCode, //
Local, //
SmilingFace, //
Pic, //
FolderUpload, //
Ranking, //
History //
} from '@icon-park/vue-next'
// Quill
import { QuillEditor, Quill } from '@vueup/vue-quill'
//
import ImageUploader from 'quill-image-uploader'
//
import EmojiBlot from './formats/emoji'
//
import QuoteBlot from './formats/quote'
//
import 'quill-mention'
//
import { useDialogueStore, useEditorDraftStore } from '@/store'
//
import { deltaToMessage, deltaToString, isEmptyDelta } from './util'
//
import { getImageInfo } from '@/utils/functions'
//
import { EditorConst } from '@/constant/event-bus'
//
import { emitCall } from '@/utils/common'
//
import { defAvatar } from '@/constant/default'
import MeEditorVote from './MeEditorVote.vue'
import MeEditorEmoticon from './MeEditorEmoticon.vue'
import MeEditorCode from './MeEditorCode.vue'
import MeEditorRecorder from './MeEditorRecorder.vue'
//
import MeEditorVote from './MeEditorVote.vue' //
import MeEditorEmoticon from './MeEditorEmoticon.vue' //
import MeEditorCode from './MeEditorCode.vue' //
import MeEditorRecorder from './MeEditorRecorder.vue' //
// API
import { ServeUploadImage } from '@/api/upload'
import { uploadImg } from '@/api/upload'
// 线
import { useEventBus } from '@/hooks'
Quill.register('formats/emoji', EmojiBlot)
Quill.register('formats/quote', QuoteBlot)
Quill.register('modules/imageUploader', ImageUploader)
// Quill
Quill.register('formats/emoji', EmojiBlot) //
Quill.register('formats/quote', QuoteBlot) //
Quill.register('modules/imageUploader', ImageUploader) //
//
const emit = defineEmits(['editor-event'])
//
const dialogueStore = useDialogueStore()
// 稿
const editorDraftStore = useEditorDraftStore()
// props
const props = defineProps({
vote: {
type: Boolean,
default: false
default: false //
},
members: {
default: () => []
default: () => [] // @
}
})
//
const editor = ref()
// Quill
const getQuill = () => {
return editor.value?.getQuill()
}
//
const getQuillSelectionIndex = () => {
let quill = getQuill()
return (quill.getSelection() || {}).index || quill.getLength()
}
//
const indexName = computed(() => dialogueStore.index_name)
//
const isShowEditorVote = ref(false)
//
const isShowEditorCode = ref(false)
//
const isShowEditorRecorder = ref(false)
// DOM
const fileImageRef = ref()
// DOM
const uploadFileRef = ref()
//
const emoticonRef = ref()
//
const editorOption = {
debug: false,
modules: {
toolbar: false,
toolbar: false, //
clipboard: {
//
//
matchers: [[Node.ELEMENT_NODE, onClipboardMatcher]]
},
@ -83,19 +119,22 @@ const editorOption = {
bindings: {
enter: {
key: 13,
handler: onSendMessage
handler: onSendMessage // Enter
}
}
},
//
imageUploader: {
upload: onEditorUpload
},
// @
mention: {
allowedChars: /^[\u4e00-\u9fa5]*$/,
mentionDenotationChars: ['@'],
positioningStrategy: 'fixed',
allowedChars: /^[\u4e00-\u9fa5]*$/, //
mentionDenotationChars: ['@'], // @
positioningStrategy: 'fixed', //
// @
renderItem: (data: any) => {
const el = document.createElement('div')
el.className = 'ed-member-item'
@ -103,6 +142,7 @@ const editorOption = {
el.innerHTML += `<span class="nickname">${data.nickname}</span>`
return el
},
//
source: function (searchTerm: string, renderList: any) {
if (!props.members.length) {
return renderList([])
@ -123,66 +163,73 @@ const editorOption = {
}
},
placeholder: '按Enter发送 / Shift+Enter 换行',
theme: 'snow'
theme: 'snow' // 使snow
}
//
const navs = reactive([
{
title: '图片',
icon: markRaw(Pic),
show: true,
click: () => {
fileImageRef.value.click()
fileImageRef.value.click() //
}
},
{
title: '件',
title: '件',
icon: markRaw(FolderUpload),
show: true,
click: () => {
uploadFileRef.value.click()
uploadFileRef.value.click() //
}
},
{
title: '代码',
icon: markRaw(SourceCode),
show: true,
click: () => {
isShowEditorCode.value = true
}
},
{
title: '语音消息',
icon: markRaw(IconVoice),
show: true,
click: () => {
isShowEditorRecorder.value = true
}
},
{
title: '地理位置',
icon: markRaw(Local),
show: true,
click: () => {}
},
{
title: '群投票',
icon: markRaw(Ranking),
show: computed(() => props.vote),
click: () => {
isShowEditorVote.value = true
}
},
{
title: '历史记录',
icon: markRaw(History),
show: true,
click: () => {
emit('editor-event', emitCall('history_event'))
}
}
//
// {
// title: '',
// icon: markRaw(SourceCode),
// show: true,
// click: () => {
// isShowEditorCode.value = true
// }
// },
// {
// title: '',
// icon: markRaw(IconVoice),
// show: true,
// click: () => {
// isShowEditorRecorder.value = true
// }
// },
// {
// title: '',
// icon: markRaw(Local),
// show: true,
// click: () => {}
// },
// {
// title: '',
// icon: markRaw(Ranking),
// show: computed(() => props.vote),
// click: () => {
// isShowEditorVote.value = true
// }
// },
// {
// title: '',
// icon: markRaw(History),
// show: true,
// click: () => {
// emit('editor-event', emitCall('history_event'))
// }
// }
])
/**
* 上传图片函数
* @param file 文件对象
* @returns Promise成功时返回图片URL
*/
function onUploadImage(file: File) {
return new Promise((resolve) => {
let image = new Image()
@ -190,35 +237,44 @@ function onUploadImage(file: File) {
image.onload = () => {
const form = new FormData()
form.append('file', file)
form.append("source", "fonchain-chat");
// form.append('width', image.width.toString())
// form.append('height', image.height.toString())
form.append("source", "fonchain-chat"); //
// URL
form.append("urlParam", `width=${image.width}&height=${image.height}`);
// API
uploadImg(form).then(({ code, data, message }) => {
if (code == 0) {
resolve(data.ori_url)
resolve(data.ori_url) // URL
} else {
resolve('')
window['$message'].error(message)
window['$message'].error(message) //
}
})
}
})
}
/**
* 编辑器上传处理函数
* @param file 要上传的文件
* @returns Promise
*/
function onEditorUpload(file: File) {
async function fn(file: File, resolve: Function, reject: Function) {
if (file.type.indexOf('image/') === 0) {
// 使
return resolve(await onUploadImage(file))
}
reject()
//
if (file.type.indexOf('video/') === 0) {
//
let fn = emitCall('video_event', file, () => {})
emit('editor-event', fn)
} else {
//
let fn = emitCall('file_event', file, () => {})
emit('editor-event', fn)
}
@ -229,29 +285,40 @@ function onEditorUpload(file: File) {
})
}
/**
* 投票事件处理
* @param data 投票数据
*/
function onVoteEvent(data: any) {
const msg = emitCall('vote_event', data, (ok: boolean) => {
if (ok) {
isShowEditorVote.value = false
isShowEditorVote.value = false //
}
})
emit('editor-event', msg)
}
/**
* 表情事件处理
* @param data 表情数据
*/
function onEmoticonEvent(data: any) {
emoticonRef.value.setShow(false)
emoticonRef.value.setShow(false) //
if (data.type == 1) {
//
const quill = getQuill()
let index = getQuillSelectionIndex()
//
if (index == 1 && quill.getLength() == 1 && quill.getText(0, 1) == '\n') {
quill.deleteText(0, 1)
index = 0
}
if (data.img) {
//
quill.insertEmbed(index, 'emoji', {
alt: data.value,
src: data.img,
@ -259,40 +326,54 @@ function onEmoticonEvent(data: any) {
height: '24px'
})
} else {
//
quill.insertText(index, data.value)
}
//
quill.setSelection(index + 1, 0, 'user')
} else {
//
let fn = emitCall('emoticon_event', data.value, () => {})
emit('editor-event', fn)
}
}
/**
* 代码事件处理
* @param data 代码数据
*/
function onCodeEvent(data: any) {
const msg = emitCall('code_event', data, (ok: boolean) => {
isShowEditorCode.value = false
isShowEditorCode.value = false //
})
emit('editor-event', msg)
}
/**
* 文件上传处理
* @param e 上传事件对象
*/
async function onUploadFile(e: any) {
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) {
console.log("进入图片")
//
const quill = getQuill()
let index = getQuillSelectionIndex()
//
if (index == 1 && quill.getLength() == 1 && quill.getText(0, 1) == '\n') {
quill.deleteText(0, 1)
index = 0
}
//
let src = await onUploadImage(file)
if (src) {
quill.insertEmbed(index, 'image', src)
@ -304,29 +385,41 @@ async function onUploadFile(e: any) {
if (file.type.indexOf('video/') === 0) {
console.log("进入视频")
//
let fn = emitCall('video_event', file, () => {})
emit('editor-event', fn)
} else {
console.log("进入其他")
//
let fn = emitCall('file_event', file, () => {})
emit('editor-event', fn)
}
}
/**
* 录音事件处理
* @param file 录音文件
*/
function onRecorderEvent(file: any) {
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) {
const ops: any[] = []
Delta.ops.forEach((op) => {
//
//
if (op.insert && typeof op.insert === 'string') {
ops.push({
insert: op.insert, //
attributes: {} //
attributes: {} //
})
} else {
ops.push(op)
@ -337,12 +430,16 @@ function onClipboardMatcher(node: any, Delta) {
return Delta
}
/**
* 发送消息处理
* 根据编辑器内容类型发送不同类型的消息
*/
function onSendMessage() {
var delta = getQuill().getContents()
let data = deltaToMessage(delta)
let data = deltaToMessage(delta) // Delta
if (data.items.length === 0) {
return
return //
}
switch (data.msgType) {
@ -351,60 +448,72 @@ function onSendMessage() {
return window['$message'].info('发送内容超长,请分条发送')
}
//
emit(
'editor-event',
emitCall('text_event', data, (ok: any) => {
ok && getQuill().setContents([], Quill.sources.USER)
ok && getQuill().setContents([], Quill.sources.USER) //
})
)
break
case 3: //
//
emit(
'editor-event',
emitCall(
'image_event',
{ ...getImageInfo(data.items[0].content), url: data.items[0].content, size: 10000 },
(ok: any) => {
ok && getQuill().setContents([])
ok && getQuill().setContents([]) //
}
)
)
break
case 12: //
case 12: //
//
emit(
'editor-event',
emitCall('mixed_event', data, (ok: any) => {
ok && getQuill().setContents([])
ok && getQuill().setContents([]) //
})
)
break
}
}
/**
* 编辑器内容改变时的处理
* 保存草稿并触发输入事件
*/
function onEditorChange() {
let delta = getQuill().getContents()
let text = deltaToString(delta)
let text = deltaToString(delta) // Delta
if (!isEmptyDelta(delta)) {
// 稿store
editorDraftStore.items[indexName.value || ''] = JSON.stringify({
text: text,
ops: delta.ops
})
} else {
// editorDraftStore.items
// 稿
delete editorDraftStore.items[indexName.value || '']
}
//
emit('editor-event', emitCall('input_event', text))
}
/**
* 加载编辑器草稿内容
* 当切换聊天对象时加载对应的草稿
*/
function loadEditorDraftText() {
if (!editor.value) return
//
// DOM
setTimeout(() => {
hideMentionDom()
hideMentionDom() // @
const quill = getQuill()
@ -415,33 +524,47 @@ function loadEditorDraftText() {
if (draft) {
quill.setContents(JSON.parse(draft)?.ops || [])
} else {
quill.setContents([])
quill.setContents([]) // 稿
}
//
const index = getQuillSelectionIndex()
quill.setSelection(index, 0, 'user')
}, 0)
}
/**
* 处理@成员事件
* @param data @成员数据
*/
function onSubscribeMention(data: any) {
const mention = getQuill().getModule('mention')
// @
mention.insertItem({ id: data?.id, denotationChar: '@', value: data.value }, true)
}
/**
* 处理引用事件
* @param data 引用数据
*/
function onSubscribeQuote(data: any) {
//
const delta = getQuill().getContents()
if (delta.ops?.some((item: any) => item.insert.quote)) {
return
return //
}
const quill = getQuill()
const index = getQuillSelectionIndex()
//
quill.insertEmbed(0, 'quote', data)
quill.setSelection(index + 1, 0, 'user')
quill.setSelection(index + 1, 0, 'user') //
}
/**
* 隐藏@成员DOM元素
*/
function hideMentionDom() {
let el = document.querySelector('.ql-mention-list-container')
if (el) {
@ -449,27 +572,34 @@ function hideMentionDom() {
}
}
// 稿
watch(indexName, loadEditorDraftText, { immediate: true })
//
onMounted(() => {
loadEditorDraftText()
})
//
onUnmounted(() => {
hideMentionDom()
})
// 线
useEventBus([
{ name: EditorConst.Mention, event: onSubscribeMention },
{ name: EditorConst.Quote, event: onSubscribeQuote }
{ name: EditorConst.Mention, event: onSubscribeMention }, // @
{ name: EditorConst.Quote, event: onSubscribeQuote } //
])
</script>
<template>
<!-- 编辑器容器 -->
<section class="el-container editor">
<section class="el-container is-vertical">
<!-- 工具栏区域 -->
<header class="el-header toolbar bdr-t">
<div class="tools">
<!-- 表情选择器弹出框 -->
<n-popover
placement="top-start"
trigger="click"
@ -489,6 +619,7 @@ useEventBus([
<MeEditorEmoticon @on-select="onEmoticonEvent" />
</n-popover>
<!-- 工具栏其他功能按钮 -->
<div
class="item pointer"
v-for="nav in navs"
@ -502,6 +633,7 @@ useEventBus([
</div>
</header>
<!-- 编辑器主体区域 -->
<main class="el-main height100">
<QuillEditor
ref="editor"
@ -514,11 +646,13 @@ useEventBus([
</section>
</section>
<!-- 隐藏的文件上传表单 -->
<form enctype="multipart/form-data" style="display: none">
<input type="file" ref="fileImageRef" accept="image/*" @change="onUploadFile" />
<input type="file" ref="uploadFileRef" @change="onUploadFile" />
</form>
<!-- 条件渲染的功能组件 -->
<MeEditorVote v-if="isShowEditorVote" @close="isShowEditorVote = false" @submit="onVoteEvent" />
<MeEditorCode
@ -536,7 +670,7 @@ useEventBus([
<style lang="less" scoped>
.editor {
--tip-bg-color: rgb(241 241 241 / 90%);
--tip-bg-color: rgb(241 241 241 / 90%); /* 提示背景颜色 */
height: 100%;
@ -559,7 +693,7 @@ useEventBus([
user-select: none;
.tip-title {
display: none;
display: none; /* 默认隐藏提示文字 */
position: absolute;
top: 40px;
left: 0px;
@ -577,7 +711,7 @@ useEventBus([
&:hover {
.tip-title {
display: block;
display: block; /* 悬停时显示提示文字 */
}
}
}
@ -585,6 +719,7 @@ useEventBus([
}
}
/* 暗色模式样式调整 */
html[theme-mode='dark'] {
.editor {
--tip-bg-color: #48484d;
@ -593,13 +728,16 @@ html[theme-mode='dark'] {
</style>
<style lang="less">
/* 全局编辑器样式 */
#editor {
overflow: hidden;
}
/* 编辑器主体区域样式 */
.ql-editor {
padding: 8px;
/* 滚动条样式 */
&::-webkit-scrollbar {
width: 3px;
height: 3px;
@ -611,6 +749,7 @@ html[theme-mode='dark'] {
background-color: transparent;
}
/* 悬停时显示滚动条 */
&:hover {
&::-webkit-scrollbar-thumb {
background-color: var(--im-scrollbar-thumb);
@ -618,6 +757,7 @@ html[theme-mode='dark'] {
}
}
/* 编辑器占位符样式 */
.ql-editor.ql-blank::before {
font-family:
PingFang SC,
@ -626,6 +766,7 @@ html[theme-mode='dark'] {
left: 8px;
}
/* 编辑器中图片样式 */
.ql-snow .ql-editor img {
max-width: 100px;
border-radius: 3px;
@ -633,6 +774,7 @@ html[theme-mode='dark'] {
margin: 0px 2px;
}
/* 图片上传中样式 */
.image-uploading {
display: flex;
width: 100px;
@ -646,15 +788,18 @@ html[theme-mode='dark'] {
}
}
/* 表情符号样式 */
.ed-emoji {
background-color: unset !important;
}
/* 编辑器占位符样式 */
.ql-editor.ql-blank::before {
font-style: unset;
color: #b8b3b3;
}
/* 引用卡片样式 */
.quote-card-content {
display: flex;
background-color: #f6f6f6;
@ -691,6 +836,7 @@ html[theme-mode='dark'] {
}
}
/* 暗色模式下的样式调整 */
html[theme-mode='dark'] {
.ql-editor.ql-blank::before {
color: #57575a;

View File

@ -1,118 +1,142 @@
<script lang="ts" setup>
<script setup>
import { fileFormatSize } from '@/utils/strings'
import { download, getFileNameSuffix } from '@/utils/functions'
import { ITalkRecordExtraFile, ITalkRecord } from '@/types/chat'
import { ref, computed } from 'vue'
defineProps<{
extra: ITalkRecordExtraFile
data: ITalkRecord
maxWidth?: Boolean
}>()
//
const props = defineProps({
//
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>
<template>
<section class="file-message">
<div class="main">
<div class="ext">{{ getFileNameSuffix(extra.name) }}</div>
<div class="file-box">
<p class="info">
<span class="name">{{ extra.name }}</span>
<span class="size">({{ fileFormatSize(extra.size) }})</span>
</p>
<p class="notice">文件已成功发送, 文件助手永久保存</p>
<div class="w-243px bg-#fff rounded-8px shadow-md px-14px pointer">
<!-- 文件头部信息 -->
<div class="flex py-14px pr-5px justify-between w-full" style="border-bottom: 1px solid #EEEEEE;">
<!-- 文件名 -->
<div class="text-#1A1A1A text-14px">{{ extra.name }}</div>
<!-- 文件图标区域 -->
<div class="relative">
<img class="w-47.91px h-47.91px" src="@/assets/image/file-paper-line@2x.png" alt="文件图标">
<!-- 文件扩展名显示 - 非上传状态 -->
<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 class="footer">
<a @click="download(data.msg_id)">下载</a>
<a>在线预览</a>
</div>
</section>
<!-- 文件大小信息 -->
<div class="text-#747474 text-12px pt-5px pb-11px">{{ fileFormatSize(extra.size) }}</div>
</div>
</template>
<style lang="less" scoped>
.file-message {
width: 250px;
min-height: 85px;
padding: 10px;
border-radius: 10px;
border: 1px solid var(--im-message-border-color);
.circle-progress-container {
width: 20px;
height: 20px;
position: relative;
cursor: pointer;
}
.main {
height: 45px;
display: flex;
flex-direction: row;
margin-top: 5px;
.circle-progress {
transform: rotate(-90deg);
transform-origin: center;
}
.ext {
display: flex;
justify-content: center;
align-items: center;
width: 45px;
height: 45px;
color: #ffffff;
background: #49a4ff;
border-radius: 5px;
font-size: 12px;
}
.progress-circle {
transition: stroke-dashoffset 0.3s ease;
}
.file-box {
flex: 1 1;
height: 45px;
margin-left: 10px;
overflow: hidden;
.pause-icon, .play-icon {
transform-origin: center;
}
.info {
display: flex;
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;
}
}
}
.transform-rotate-90 {
transform: rotate(90deg);
}
</style>

View File

@ -47,7 +47,7 @@ const img = (src: string, width = 200) => {
min-width: 30px;
min-height: 30px;
max-width:240px;
max-height:300px
height:149px
&.left {
background: var(--im-message-right-bg-color);
}

View File

@ -1,11 +1,17 @@
<script lang="ts" setup>
import 'xgplayer/dist/index.min.css'
import { ref, nextTick } from 'vue'
import { NImage, NModal, NCard } from 'naive-ui'
import { Play, Close } from '@icon-park/vue-next'
import { ref, nextTick, watch } from 'vue'
import { NImage, NModal, NCard, NProgress, NPopconfirm } from 'naive-ui'
import { Play, Close, Pause, Right, Attention } from '@icon-park/vue-next'
import { getImageInfo } from '@/utils/functions'
import {PauseOutline} from '@vicons/ionicons5'
import Player from 'xgplayer'
import { ITalkRecordExtraVideo, ITalkRecord } from '@/types/chat'
import { useUploadsStore } from '@/store'
// @ts-ignore
const message = window.$message
const uploadsStore = useUploadsStore()
const props = defineProps<{
extra: ITalkRecordExtraVideo
@ -40,8 +46,43 @@ const img = (src: string, width = 200) => {
}
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() {
//
if (props.extra.is_uploading) {
return
}
open.value = true
await nextTick()
@ -54,18 +95,102 @@ async function onPlay() {
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>
<template>
<section
class="im-message-video"
:class="{ left: data.float === 'left' }"
:style="img(extra.cover, 350)"
@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">
<n-icon :component="Play" size="36" />
</div>
<!-- <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>
<n-modal v-model:show="open">
@ -92,23 +217,24 @@ async function onPlay() {
min-height: 30px;
display: inline-flex;
position: relative;
height:149px;
&.left {
background: var(--im-message-right-bg-color);
}
:deep(.n-image img) {
video {
width: 100%;
height: 100%;
border-radius: 5px;
object-fit: cover;
background-color: #333; /* 添加背景色,避免默认显示为灰色 */
}
.btn-video {
width: 30px;
height: 20px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
position: absolute;
left: calc(50% - 15px);
top: calc(50% - 10px);
cursor: pointer;
color: #ffffff;
}
@ -134,4 +260,54 @@ async function onPlay() {
align-items: 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>

View File

@ -177,14 +177,14 @@ const onAfterEnter = () => {
<img class="w-20px h-20px" src="@/assets/image/close.png" alt="">
</div>
<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>
</div>
<div>
<div class="text-#000 text-16px mb-5px">张三</div>
<div class="text-#ACACAC text-12px">工号FL043</div>
<div class="text-#000 text-16px mb-5px">{{ state.nickname }}</div>
<div class="text-#ACACAC text-12px">工号{{ state.job_num }}</div>
</div>
</div>
<div class="bg-#fff rounded-4px mb-20px">
@ -202,7 +202,7 @@ const onAfterEnter = () => {
</div>
<div class="flex px-15px py-9px">
<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 class="flex px-15px py-9px">
<div class="text-#000 text-12px w-84px">岗位</div>

View File

@ -226,6 +226,30 @@ export const useDialogueStore = defineStore('dialogue', {
useEditorStore().loadUserEmoticon()
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
}
}
}
})

View File

@ -1,10 +1,34 @@
import { defineStore } from 'pinia'
import { ServeFindFileSplitInfo, ServeFileSubareaUpload } from '@/api/upload'
import { ServeSendTalkFile } from '@/api/chat'
import { uploadImg } from '@/api/upload'
import {
useDialogueStore
} from '@/store'
// @ts-ignore
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) {
const splitNum = Math.ceil(file.size / eachSize) // 分片总数
@ -31,7 +55,8 @@ export const useUploadsStore = defineStore('uploads', {
state: () => {
return {
isShow: false,
items: []
items: [] as UploadItem[],
dialogueStore: useDialogueStore()
}
},
getters: {
@ -45,81 +70,282 @@ export const useUploadsStore = defineStore('uploads', {
close() {
this.isShow = false
},
// 获取分片文件数组索引
findItem(uploadId: string): UploadItem | undefined {
return this.items.find((item) => item.upload_id === uploadId)
},
// 初始化上传
initUploadFile(file: File, talkType: number, receiverId: number, username: string) {
ServeFindFileSplitInfo({
file_name: file.name,
file_size: file.size
}).then((res) => {
// 通过客户端ID查找上传项
findItemByClientId(clientUploadId: string): UploadItem | undefined {
return this.items.find((item) => item.client_upload_id === clientUploadId)
},
// 暂停文件上传
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) {
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
this.items.unshift({
file: file,
talk_type: talkType,
receiver_id: receiverId,
upload_id: upload_id,
client_upload_id: uploadId, // 客户端生成的上传ID用于前端标识
uploadIndex: 0,
percentage: 0,
status: 0, // 文件上传状态 0:等待上传 1:上传中 2:上传完成 3:网络异常
files: fileSlice(file, upload_id, split_size),
files: fileChunks,
avatar: '',
username: username
username: username,
is_paused: false,
onProgress: onProgress,
onComplete: onComplete,
})
this.triggerUpload(upload_id)
this.isShow = true
this.isShow = false // 不显示上传管理抽屉
// 开始上传分片
this.triggerUpload(upload_id, uploadId)
} else {
message.error(res.message)
onProgress(-1) // 通知上传失败
}
})
} catch (error) {
console.error("初始化分片上传失败:", error);
message.error("初始化上传失败,请重试")
onProgress(-1)
}
},
// 获取分片文件数组索引
findItem(uploadId: string): any {
return this.items.find((item: any) => item.upload_id === uploadId)
},
// 触发上传
triggerUpload(uploadId: string) {
const item = this.findItem(uploadId)
const form = item.files[item.uploadIndex]
item.status = 1
ServeFileSubareaUpload(form)
.then((res) => {
if (res.code == 200) {
item.uploadIndex++
if (item.uploadIndex === item.files.length) {
item.status = 2
item.percentage = 100
this.sendUploadMessage(item)
} else {
const percentage = (item.uploadIndex / item.files.length) * 100
item.percentage = percentage.toFixed(1)
this.triggerUpload(uploadId)
// 触发分片上传
async triggerUpload(uploadId: string, clientUploadId?: string) {
const currentItem = this.findItem(uploadId)
if (!currentItem) return
// 如果已暂停,不继续上传
if (currentItem.is_paused) return
// 如果已上传完成,不继续上传
if (currentItem.uploadIndex >= currentItem.files.length) {
if (clientUploadId) {
this.completeUpload(currentItem, clientUploadId)
}
return
}
// 获取当前要上传的分片
const form = currentItem.files[currentItem.uploadIndex]
// 更新状态为上传中
currentItem.status = 1
// 上传当前分片
try {
const res = await ServeFileSubareaUpload(form)
// 获取最新的项目状态,确保仍然存在且没有被暂停
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 {
item.status = 3
// 继续上传下一个分片
this.triggerUpload(uploadId, clientUploadId)
}
})
.catch(() => {
item.status = 3
})
} else {
// 上传失败处理
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
})
}
}
})

View File

@ -65,6 +65,7 @@ export interface ITalkRecordExtraFile {
name: string
path: string
size: number
percentage: number
}
export interface ITalkRecordExtraForward {
@ -90,6 +91,9 @@ export interface ITalkRecordExtraVideo {
url: string
duration: number
size: number
is_uploading?: boolean
upload_id?: string
percentage?: number
}
export interface ITalkRecordExtraMixed {

View File

@ -18,7 +18,7 @@ export function isLoggedIn() {
*/
export function getAccessToken() {
// return storage.get(AccessToken) || ''
return JSON.parse(localStorage.getItem('token'))||'79b5c732d96d2b27a48a99dfd4a5566c43aaa5796242e854ebe3ffc198d6876b9628e7b764d9af65ab5dbb2d517ced88170491b74b048c0ba827c0d3741462cb89dc59ed46653a449af837a8262941caaef1334d640773710f8cd96473bacfb190cba595a5d6a9c87d70f0999a3ebb41147213b31b4bdccffca66a56acf3baab5af0154f0dce360079f37709f78e13711036899344bddb0fb4cf0f2890287cb62c3fcbe33368caa5e213624577be8b8420ab75b1f50775ee16142a4321c5d56995f37354a66a969da98d95ba6e65d142ed097e04b411c1ebad2f62866d0ec7e1838420530a9941dbbcd00490199f8b891bd3a81a1ac4e73e2aed60deeaec60792c525cc0c96e8f4a666eca6ee7a10716507b402cde5759bbcda1fa681fbe4dcdfe05abbc2b1644c68dc74ebaf8d9c9cc4eb61afaf3de52fa357dbfdfe17acf14'
return JSON.parse(localStorage.getItem('token'))||'79b5c732d96d2b27a48a99dfd4a5566c43aaa5796242e854ebe3ffc198d6876b9628e7b764d9af65ab5dbb2d517ced88170491b74b048c0ba827c0d3741462cb89dc59ed46653a449af837a8262941caaef1334d640773710f8cd96473bacfb190cba595a5d6a9c87d70f0999a3ebb41147213b31b4bdccffca66a56acf3baab5af0154f0dce360079f37709f78e13711036899344bddb0fb4cf0f2890287cb62c3fcbe33368caa5e213624577be8b8420ab75b1f50775ee16142a4321c5d56995f37354a66a969da98d95ba6e65d142ed097e04b411c1ebad2f62866d0ec7e1838420530a9941dbbcd00490199f8b89574a563986daa80674dd774ef18032ee6016a202902c95452e1e81931358d4d3cb7f0db0c6fc66f406f57e411cb1e2aeb77318f7c36b2b61f48c4c645d27920f05c204fe133ab9bfa481e9c1ae2e384c'
}
/**

View File

@ -47,7 +47,9 @@ watch(() => records, (newValue, oldValue) => {
//
const skipBottom = ref(false)
setTimeout(()=>{
console.log(records.value,'records.value');
},1000)
//
const isShowTalkTime = (index: number, datetime: string) => {
if (datetime == undefined) {
@ -368,7 +370,7 @@ onMounted(() => {
@contextmenu.prevent="onContextMenu($event, item)"
/>
<div class="talk-tools">
<!-- <div class="talk-tools">
<template v-if="talk_type == 1 && item.float == 'right'">
<loading
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>
</template>
<n-icon
@ -388,7 +390,7 @@ onMounted(() => {
:component="MoreThree"
@click="onContextMenu($event, item)"
/>
</div>
</div> -->
</div>
<div

View File

@ -5,16 +5,18 @@ import {
useDialogueStore,
useSettingsStore,
useUploadsStore,
useEditorStore
useEditorStore,
useUserStore
} from '@/store'
import ws from '@/connect'
import { ServePublishMessage, ServeSendVote } from '@/api/chat'
import { throttle, getVideoImage } from '@/utils/common'
import { parseTime } from '@/utils/datetime'
import Editor from '@/components/editor/Editor.vue'
import MultiSelectFooter from './MultiSelectFooter.vue'
import HistoryRecord from '@/components/talk/HistoryRecord.vue'
import { uploadImg } from '@/api/upload'
const userStore = useUserStore()
const talkStore = useTalkStore()
const editorStore = useEditorStore()
const settingsStore = useSettingsStore()
@ -98,26 +100,80 @@ const onSendImageEvent = ({ data, callBack }) => {
//
const onSendVideoEvent = async ({ data }) => {
let resp = await getVideoImage(data)
const form = new FormData()
form.append('file', data)
form.append("source", "fonchain-chat");
form.append("type", "video");
form.append("urlParam", `width=${resp.width}&height=${resp.height}`);
console.log(form.get('file'));
let video = await uploadImg(form)
if (video.code != 0) return
let message = {
type: 'video',
url: video.data.ori_url,
cover: video.data.cover_url,
duration: parseInt(resp.duration),
size: data.size
console.log('onSendVideoEvent')
//
// let resp = await getVideoImage(data)
// ID
const uploadId = `video-${Date.now()}-${Math.floor(Math.random() * 1000)}`
//
const tempMessage = {
msg_id: uploadId,
sequence: Date.now(),
talk_type: props.talk_type,
msg_type: 5, //
user_id: props.uid,
receiver_id: props.receiver_id,
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) {
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
}
)
}
//