chat-pc/src/views/message/inner/panel/PanelFooter.vue
Phoenix 4863b4c77c feat(chat): 实现聊天记录本地存储功能
添加Dexie.js作为本地数据库,实现聊天记录和会话的本地存储与同步
修改消息和会话相关store方法,支持本地数据库操作
优化消息加载逻辑,优先从本地加载再同步服务器数据
添加数据库工具函数,包括消息增删改查和会话管理功能
2025-06-30 16:00:06 +08:00

315 lines
7.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts" setup>
import { ref, onMounted, nextTick } from 'vue'
import {
useTalkStore,
useDialogueStore,
useSettingsStore,
useUploadsStore,
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 {scrollToBottom} from '@/utils/dom.ts'
import CustomEditor from '@/components/editor/CustomEditor.vue'
const userStore = useUserStore()
const talkStore = useTalkStore()
const editorStore = useEditorStore()
const settingsStore = useSettingsStore()
const uploadsStore = useUploadsStore()
const dialogueStore = useDialogueStore()
const props = defineProps({
uid: {
type: Number,
default: 0
},
talk_type: {
type: Number,
default: 0
},
receiver_id: {
type: Number,
default: 0
},
index_name: {
type: String,
default: ''
},
online: {
type: Boolean,
default: false
},
members: {
default: () => []
}
})
const isShowHistory = ref(false)
const onSendMessage = (data = {}, callBack: any) => {
let message = {
...data,
receiver: {
receiver_id: props.receiver_id,
talk_type: props.talk_type
}
}
ServePublishMessage(message)
.then(({ code, message, msg }) => {
if (code == 200) {
callBack(true)
} else {
window['$message'].warning(message || msg)
}
})
.catch(() => {
window['$message'].warning('网络繁忙,请稍后重试!')
})
}
// 发送文本消息
const onSendTextEvent = throttle((value: any) => {
let { data, callBack } = value
let message = {
type: 'text',
content: data.items[0].content,
quote_id: data.quoteId,
mentions: data.mentionUids
}
onSendMessage(message, (ok: boolean) => {
if (!ok) return
let el = document.getElementById('talk-session-list')
el?.scrollTo({ top: 0, behavior: 'smooth' })
callBack(true)
})
}, 1000)
// 发送图片消息
const onSendImageEvent = ({ data, callBack }) => {
onSendMessage({ type: 'image', ...data }, callBack)
}
// 发送视频消息
const onSendVideoEvent = async ({ data }) => {
// 获取视频首帧作为封面图
let videoPreview = null
try {
videoPreview = await getVideoImage(data)
} catch (error) {
console.error('获取视频封面失败:', error)
}
// 先创建一个带有上传ID的临时消息对象用于显示进度
const uploadId = `video-${Date.now()}-${Math.floor(Math.random() * 1000)}`
// 创建临时消息记录
const tempMessage = {
msg_id: uploadId,
insert_sequence: dialogueStore.records.length > 0
? dialogueStore.records[dialogueStore.records.length-1].sequence
: 0,
sequence: Date.now(),
talk_type: props.talk_type,
msg_type: 5, // 视频消息类型
user_id: props.uid,
receiver_id: props.receiver_id,
is_revoke: 0,
is_mark: 0,
is_read: 1,
content: '',
created_at: parseTime(new Date(), '{y}-{m}-{d} {h}:{i}'),
extra: {
url: videoPreview ? URL.createObjectURL(data) : '', // 使用本地视频URL作为预览
size: data.size,
is_uploading: true,
upload_id: uploadId,
percentage: 0
},
isCheck: false,
send_status: 1,
float: 'right' // 我发送的消息显示在右侧
}
// 使用新的方法添加上传任务
dialogueStore.addUploadTask(tempMessage)
nextTick(()=>{
scrollToBottom()
})
uploadsStore.initUploadFile(
data,
props.talk_type,
props.receiver_id,
uploadId,
async (percentage) => {
dialogueStore.updateUploadProgress(uploadId, percentage)
},
async () => {
dialogueStore.batchDelDialogueRecord([uploadId])
}
)
}
// 发送代码消息
const onSendCodeEvent = ({ data, callBack }) => {
onSendMessage({ type: 'code', code: data.code, lang: data.lang }, callBack)
}
// 发送文件消息
const onSendFileEvent = ({ data }) => {
const clientUploadId = `file-${Date.now()}-${Math.floor(Math.random() * 1000)}`
const tempMessage = {
msg_id: clientUploadId,
insert_sequence: dialogueStore.records.length > 0
? dialogueStore.records[dialogueStore.records.length-1].sequence
: 0,
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: '',
path:data.name,
size: data.size,
is_uploading: true,
upload_id: clientUploadId,
percentage: 0
},
float: 'right'
}
dialogueStore.addUploadTask(tempMessage)
nextTick(()=>{
scrollToBottom()
})
uploadsStore.initUploadFile(data, props.talk_type, props.receiver_id,clientUploadId,
async (percentage) => {
dialogueStore.updateUploadProgress(clientUploadId, percentage)
},
async () => {
// 上传完成后上传任务已经被removeUploadTask方法移除
// 不需要再次从records中删除
}
)
}
// 发送投票消息
const onSendVoteEvent = ({ data, callBack }) => {
let response = ServeSendVote({
receiver_id: props.receiver_id,
mode: data.mode,
anonymous: data.anonymous,
title: data.title,
options: data.options
})
response.then(({ code, message }) => {
if (code == 200) {
callBack(true)
} else {
window['$message'].warning(message)
}
})
response.catch(() => callBack(false))
}
// 发送表情消息
const onSendEmoticonEvent = ({ data, callBack }) => {
onSendMessage({ type: 'emoticon', emoticon_id: data }, callBack)
}
const onSendMixedEvent = ({ data, callBack }) => {
let message = {
type: 'mixed',
quote_id: data.quoteId,
items: data.items
}
onSendMessage(message, callBack)
}
const onKeyboardPush = throttle(() => {
ws.emit('im.message.keyboard', {
sender_id: props.uid,
receiver_id: props.receiver_id
})
}, 3000)
// 编辑器输入事件
const onInputEvent = ({ data }) => {
talkStore.updateItem({
index_name: props.index_name,
draft_text: data
})
// 判断对方是否在线和是否需要推送
// 3秒时间内推送一次
if (settingsStore.isKeyboard && props.online) {
onKeyboardPush()
}
}
// 注册事件
const evnets = {
text_event: onSendTextEvent,
image_event: onSendImageEvent,
video_event: onSendVideoEvent,
code_event: onSendCodeEvent,
file_event: onSendFileEvent,
input_event: onInputEvent,
vote_event: onSendVoteEvent,
emoticon_event: onSendEmoticonEvent,
history_event: () => {
isShowHistory.value = true
},
mixed_event: onSendMixedEvent
}
// 编辑器事件
const onEditorEvent = (msg: any) => {
evnets[msg.event] && evnets[msg.event](msg)
}
onMounted(() => {
editorStore.loadUserEmoticon()
})
</script>
<template>
<footer class="el-footer">
<MultiSelectFooter v-if="dialogueStore.isOpenMultiSelect" />
<!-- <Editor v-else @editor-event="onEditorEvent" :vote="talk_type == 2" :members="members" /> -->
<CustomEditor v-else @editor-event="onEditorEvent" :vote="talk_type == 2" :members="members" />
</footer>
<HistoryRecord
v-if="isShowHistory"
:talk-type="talk_type"
:receiver-id="receiver_id"
@close="isShowHistory = false"
/>
</template>
<style lang="less">
.el-footer {
height: inherit;
}
</style>