+
+
+
+
@@ -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);
+ }
+ }
+}
diff --git a/src/components/user/UserCardModal.vue b/src/components/user/UserCardModal.vue
index d57efde..98a6852 100644
--- a/src/components/user/UserCardModal.vue
+++ b/src/components/user/UserCardModal.vue
@@ -177,14 +177,14 @@ const onAfterEnter = () => {
+
+
+
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+ 确认重新发送该视频消息吗?
+
+

-
-->
+
-
张三
- 工号:FL043
+ {{ state.nickname }}
+ 工号:{{ state.job_num }}
@@ -202,7 +202,7 @@ const onAfterEnter = () => {
手机号
- 江苏泰丰文化传播股份有限公司
+ {{ state.mobile }}
岗位
diff --git a/src/store/modules/dialogue.js b/src/store/modules/dialogue.js
index 1e942a9..398fb4f 100644
--- a/src/store/modules/dialogue.js
+++ b/src/store/modules/dialogue.js
@@ -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
+ }
}
}
})
diff --git a/src/store/modules/uploads.ts b/src/store/modules/uploads.ts
index 0451d7b..eedff56 100644
--- a/src/store/modules/uploads.ts
+++ b/src/store/modules/uploads.ts
@@ -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
- })
- }
}
})
diff --git a/src/types/chat.ts b/src/types/chat.ts
index ba84e30..c723195 100644
--- a/src/types/chat.ts
+++ b/src/types/chat.ts
@@ -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 {
diff --git a/src/utils/auth.js b/src/utils/auth.js
index 0ebab98..30d247c 100644
--- a/src/utils/auth.js
+++ b/src/utils/auth.js
@@ -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'
}
/**
diff --git a/src/views/message/inner/panel/PanelContent.vue b/src/views/message/inner/panel/PanelContent.vue
index 2578e5e..6871e72 100644
--- a/src/views/message/inner/panel/PanelContent.vue
+++ b/src/views/message/inner/panel/PanelContent.vue
@@ -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)"
/>
-
+
+ 已送达
{
:component="MoreThree"
@click="onContextMenu($event, item)"
/>
-
+ {
// 发送视频消息
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
+ }
+ )
}
// 发送投票消息