540 lines
15 KiB
JavaScript
540 lines
15 KiB
JavaScript
import { defineStore } from 'pinia'
|
||
import {
|
||
ServeRemoveRecords,
|
||
ServeRevokeRecords,
|
||
ServePublishMessage,
|
||
ServeCollectEmoticon
|
||
} from '@/api/chat'
|
||
import { ServeGetGroupMembers,ServeGroupDetail } from '@/api/group.js'
|
||
import { useEditorStore } from './editor'
|
||
// 键盘消息事件定时器
|
||
let keyboardTimeout = null
|
||
|
||
export const useDialogueStore = defineStore('dialogue', {
|
||
state: () => {
|
||
return {
|
||
// 对话索引(聊天对话的唯一索引)
|
||
index_name: '',
|
||
globalUploadList:[],
|
||
// 添加一个映射,用于快速查找每个会话的上传任务
|
||
uploadTaskMap: {}, // 格式: { "talk_type_receiver_id": [task1, task2, ...] }
|
||
// 对话节点
|
||
talk: {
|
||
avatar:'',
|
||
username: '',
|
||
talk_type: 0, // 对话来源[1:私聊;2:群聊]
|
||
receiver_id: 0,
|
||
group_type:0
|
||
},
|
||
|
||
// 好友是否正在输入文字
|
||
keyboard: false,
|
||
|
||
// 对方是否在线
|
||
online: false,
|
||
|
||
// 聊天记录
|
||
records: [],
|
||
|
||
// 查询指定消息上下文的消息信息
|
||
specifiedMsg: '',
|
||
|
||
// 是否不刷新聊天记录
|
||
noRefreshRecords: false,
|
||
|
||
// 是否是手动切换会话
|
||
isManualSwitch: false,
|
||
|
||
// 新消息提示
|
||
unreadBubble: 0,
|
||
|
||
// 是否开启多选操作模式
|
||
isOpenMultiSelect: false,
|
||
|
||
// 是否显示编辑器
|
||
isShowEditor: false,
|
||
|
||
//是否已被解散
|
||
isDismiss: false,
|
||
|
||
//是否退群/移出群
|
||
isQuit: false,
|
||
|
||
// 是否显示会话列表
|
||
isShowSessionList: true,
|
||
groupInfo: {} ,
|
||
// 群成员列表
|
||
members: [],
|
||
// 群成员列表按字母分组
|
||
membersByAlphabet: {},
|
||
|
||
// 对话记录
|
||
items: {
|
||
'1_1': {
|
||
talk_type: 1, // 对话类型
|
||
receiver_id: 0, // 接收者ID
|
||
read_sequence: 0, // 当前已读的最后一条记录
|
||
records: []
|
||
}
|
||
}
|
||
}
|
||
},
|
||
getters: {
|
||
// 多选列表
|
||
selectItems: (state) => state.records.filter((item) => item.isCheck),
|
||
// 当前对话是否是群聊
|
||
isGroupTalk: (state) => state.talk.talk_type === 2
|
||
},
|
||
actions: {
|
||
// 更新在线状态
|
||
setOnlineStatus(status) {
|
||
this.online = status
|
||
},
|
||
|
||
// 更新群解散状态
|
||
updateDismiss(isDismiss) {
|
||
this.isDismiss = isDismiss
|
||
},
|
||
|
||
// 更新群成员退出状态
|
||
updateQuit(isQuit) {
|
||
this.isQuit = isQuit
|
||
},
|
||
|
||
// 更新对话信息
|
||
setDialogue(data = {}) {
|
||
this.online = data.is_online == 1
|
||
this.talk = {
|
||
username: data.remark || data.name,
|
||
talk_type: data.talk_type,
|
||
receiver_id: data.receiver_id,
|
||
avatar:data.avatar,
|
||
group_type:data.group_type
|
||
}
|
||
|
||
this.index_name = `${data.talk_type}_${data.receiver_id}`
|
||
this.records = []
|
||
this.unreadBubble = 0
|
||
this.isShowEditor = data?.is_robot === 0
|
||
|
||
this.isDismiss = data?.is_dismiss === 1 ? true : false
|
||
this.isQuit = data?.is_quit === 1 ? true : false
|
||
|
||
// 只在手动切换会话时清空 specifiedMsg
|
||
// if (this.isManualSwitch) {
|
||
// this.specifiedMsg = ''
|
||
// this.isManualSwitch = false
|
||
// }
|
||
|
||
this.members = []
|
||
this.membersByAlphabet = []
|
||
if (data.talk_type == 2) {
|
||
this.updateGroupMembers()
|
||
this.getGroupInfo()
|
||
}
|
||
|
||
// 注意:上传任务的恢复将在聊天记录加载完成后进行
|
||
// 在useTalkRecord.ts的onLoad方法中,会在加载完聊天记录后调用restoreUploadTasks方法
|
||
},
|
||
|
||
// 更新提及列表
|
||
async updateGroupMembers() {
|
||
let { code, data } = await ServeGetGroupMembers({
|
||
group_id: this.talk.receiver_id
|
||
})
|
||
|
||
if (code != 200) return
|
||
|
||
this.members = data.items.map((o) => ({
|
||
id: o.user_id,
|
||
nickname: o.nickname,
|
||
avatar: o.avatar,
|
||
gender: o.gender,
|
||
leader: o.leader,
|
||
remark: o.remark,
|
||
online: false,
|
||
value: o.nickname
|
||
}))
|
||
const groupMap = {};
|
||
data.sortItems.forEach(member => {
|
||
const alpha = (member.key || member.nickname?.[0] || '#').toUpperCase();
|
||
if (!groupMap[alpha]) groupMap[alpha] = [];
|
||
groupMap[alpha].push(member);
|
||
});
|
||
const alphabets = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
||
this.membersByAlphabet = alphabets.map(alpha => ({
|
||
alphabet: alpha,
|
||
members: groupMap[alpha] || []
|
||
})).filter(group => group.members.length > 0);
|
||
},
|
||
|
||
// 清空对话记录
|
||
clearDialogueRecord() {
|
||
this.records = []
|
||
},
|
||
|
||
// 数组头部压入对话记录
|
||
unshiftDialogueRecord(records) {
|
||
console.log('unshiftDialogueRecord')
|
||
this.records.unshift(...records)
|
||
},
|
||
//数组尾部加入更多对话记录
|
||
addDialogueRecordForLoadMore(records){
|
||
console.log('addDialogueRecordForLoadMore')
|
||
this.records.push(...records)
|
||
},
|
||
async getGroupInfo(){
|
||
const { code, data } = await ServeGroupDetail({
|
||
group_id: this.talk.receiver_id
|
||
})
|
||
if(code == 200){
|
||
this.groupInfo = data
|
||
}
|
||
},
|
||
// 推送对话记录
|
||
async addDialogueRecord(record) {
|
||
// TOOD 需要通过 sequence 排序,保证消息一致性
|
||
// this.records.splice(index, 0, record)
|
||
this.records.push(record)
|
||
|
||
// 同步到本地数据库
|
||
try {
|
||
const { addMessage } = await import('@/utils/db')
|
||
await addMessage(record)
|
||
} catch (error) {
|
||
console.error('同步消息到本地数据库失败:', error)
|
||
}
|
||
},
|
||
|
||
// 更新对话记录
|
||
async updateDialogueRecord(params) {
|
||
const { msg_id = '' } = params
|
||
|
||
const item = this.records.find((item) => item.msg_id === msg_id)
|
||
|
||
if (item) {
|
||
Object.assign(item, params)
|
||
|
||
// 同步到本地数据库
|
||
try {
|
||
// 如果是撤回消息
|
||
if (params.is_revoke === 1) {
|
||
const { revokeMessage } = await import('@/utils/db')
|
||
await revokeMessage(msg_id)
|
||
}
|
||
} catch (error) {
|
||
console.error('同步消息更新到本地数据库失败:', error)
|
||
}
|
||
}
|
||
},
|
||
|
||
// 批量删除对话记录
|
||
async batchDelDialogueRecord(msgIds = []) {
|
||
// 同步到本地数据库
|
||
try {
|
||
const { deleteMessage } = await import('@/utils/db')
|
||
for (const msgid of msgIds) {
|
||
await deleteMessage(msgid)
|
||
}
|
||
} catch (error) {
|
||
console.error('同步消息删除到本地数据库失败:', error)
|
||
}
|
||
|
||
// 从内存中删除
|
||
msgIds.forEach((msgid) => {
|
||
const index = this.records.findIndex((item) => item.msg_id === msgid)
|
||
|
||
if (index >= 0) this.records.splice(index, 1)
|
||
})
|
||
},
|
||
|
||
// 自增好友键盘输入事件
|
||
triggerKeyboard() {
|
||
this.keyboard = true
|
||
|
||
clearTimeout(keyboardTimeout)
|
||
|
||
keyboardTimeout = setTimeout(() => (this.keyboard = false), 2000)
|
||
},
|
||
|
||
setUnreadBubble(value) {
|
||
if (value === 0) {
|
||
this.unreadBubble = 0
|
||
} else {
|
||
this.unreadBubble++
|
||
}
|
||
},
|
||
|
||
// 关闭多选模式
|
||
closeMultiSelect() {
|
||
this.isOpenMultiSelect = false
|
||
|
||
for (const item of this.selectItems) {
|
||
if (item.isCheck) {
|
||
item.isCheck = false
|
||
}
|
||
}
|
||
},
|
||
|
||
// 删除聊天记录
|
||
ApiDeleteRecord(msgIds = []) {
|
||
ServeRemoveRecords({
|
||
talk_type: this.talk.talk_type,
|
||
receiver_id: this.talk.receiver_id,
|
||
msg_ids: msgIds
|
||
}).then((res) => {
|
||
if (res.code == 200) {
|
||
this.batchDelDialogueRecord(msgIds)
|
||
}
|
||
})
|
||
},
|
||
|
||
// 撤销聊天记录
|
||
ApiRevokeRecord(msg_id = '') {
|
||
ServeRevokeRecords({ msg_id }).then((res) => {
|
||
if (res.code == 200) {
|
||
this.updateDialogueRecord({ msg_id, is_revoke: 1 })
|
||
} else {
|
||
window['$message'].warning(res.message)
|
||
}
|
||
})
|
||
},
|
||
|
||
// 转发聊天记录
|
||
ApiForwardRecord(options) {
|
||
let data = {
|
||
type: 'forward',
|
||
receiver: {
|
||
talk_type: this.talk.talk_type,
|
||
receiver_id: this.talk.receiver_id
|
||
},
|
||
...options
|
||
}
|
||
|
||
ServePublishMessage(data).then((res) => {
|
||
if (res.code == 200) {
|
||
this.closeMultiSelect()
|
||
}
|
||
})
|
||
},
|
||
|
||
ApiCollectImage(options) {
|
||
const { msg_id } = options
|
||
|
||
ServeCollectEmoticon({ msg_id }).then(() => {
|
||
useEditorStore().loadUserEmoticon()
|
||
window['$message'] && window['$message'].success('收藏成功')
|
||
})
|
||
},
|
||
|
||
// 更新视频上传进度
|
||
updateUploadProgress(uploadId, percentage) {
|
||
// 更新全局列表中的进度
|
||
const globalTask = this.globalUploadList.find(item =>
|
||
item.extra && item.extra.is_uploading && item.extra.upload_id === uploadId
|
||
)
|
||
|
||
if (globalTask) {
|
||
globalTask.extra.percentage = percentage
|
||
}
|
||
|
||
// 更新当前会话记录中的进度
|
||
const record = this.records.find(item =>
|
||
item.extra && item.extra.is_uploading && item.extra.upload_id === uploadId
|
||
)
|
||
|
||
if (record) {
|
||
record.extra.percentage = percentage
|
||
}
|
||
},
|
||
|
||
// 添加上传任务
|
||
addUploadTask(task) {
|
||
// 添加到全局列表
|
||
this.globalUploadList.push(task)
|
||
|
||
// 添加到会话映射
|
||
const sessionKey = `${task.talk_type}_${task.receiver_id}`
|
||
if (!this.uploadTaskMap[sessionKey]) {
|
||
this.uploadTaskMap[sessionKey] = []
|
||
}
|
||
this.uploadTaskMap[sessionKey].push(task)
|
||
|
||
// 同时添加到当前会话记录
|
||
this.addDialogueRecord(task)
|
||
},
|
||
|
||
// 上传完成后移除任务
|
||
removeUploadTask(uploadId) {
|
||
// 从全局列表中找到任务
|
||
const taskIndex = this.globalUploadList.findIndex(item => item.msg_id === uploadId)
|
||
|
||
if (taskIndex >= 0) {
|
||
const task = this.globalUploadList[taskIndex]
|
||
const sessionKey = `${task.talk_type}_${task.receiver_id}`
|
||
|
||
// 从会话映射中移除
|
||
if (this.uploadTaskMap[sessionKey]) {
|
||
const mapIndex = this.uploadTaskMap[sessionKey].findIndex(item => item.msg_id === uploadId)
|
||
if (mapIndex >= 0) {
|
||
this.uploadTaskMap[sessionKey].splice(mapIndex, 1)
|
||
}
|
||
}
|
||
|
||
// 从全局列表中移除
|
||
this.globalUploadList.splice(taskIndex, 1)
|
||
// 移除消息记录
|
||
|
||
this.batchDelDialogueRecord([uploadId])
|
||
}
|
||
},
|
||
|
||
// 视频上传完成后更新消息
|
||
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
|
||
}
|
||
},
|
||
|
||
// 更新会话信息
|
||
updateDialogueTalk(params){
|
||
Object.assign(this.talk, params)
|
||
},
|
||
|
||
// 根据 insert_sequence 将任务插入到 records 数组的正确位置(使用优化的二分查找)
|
||
insertTaskAtCorrectPosition(task) {
|
||
const len = this.records.length
|
||
|
||
// 快速路径:如果数组为空或任务应该插入到末尾
|
||
if (len === 0) {
|
||
this.records.push(task)
|
||
return
|
||
}
|
||
|
||
// 快速路径:检查是否应该插入到开头或末尾(避免二分查找的开销)
|
||
if (task.insert_sequence < this.records[0].sequence) {
|
||
this.records.unshift(task)
|
||
return
|
||
}
|
||
|
||
if (task.insert_sequence >= this.records[len - 1].sequence) {
|
||
this.records.push(task)
|
||
return
|
||
}
|
||
|
||
// 使用优化的二分查找算法找到插入位置
|
||
let low = 0
|
||
let high = len - 1
|
||
|
||
// 二分查找优化:使用位运算加速计算中点
|
||
while (low <= high) {
|
||
const mid = (low + high) >>> 1 // 无符号右移代替 Math.floor((low + high) / 2)
|
||
if (this.records[mid].sequence <= task.insert_sequence) {
|
||
low = mid + 1
|
||
} else {
|
||
high = mid - 1
|
||
}
|
||
}
|
||
|
||
// 在找到的位置插入任务
|
||
this.records.splice(low, 0, task)
|
||
},
|
||
|
||
// 恢复当前会话的上传任务
|
||
restoreUploadTasks() {
|
||
// 获取当前会话的sessionKey
|
||
const sessionKey = `${this.talk.talk_type}_${this.talk.receiver_id}`
|
||
|
||
// 检查是否有需要恢复的上传任务
|
||
if (!this.uploadTaskMap[sessionKey] || this.uploadTaskMap[sessionKey].length === 0) {
|
||
return
|
||
}
|
||
|
||
// 性能优化:缓存数组长度和本地变量,减少属性查找
|
||
const tasks = this.uploadTaskMap[sessionKey]
|
||
const tasksLength = tasks.length
|
||
|
||
// 如果只有一个任务,直接处理
|
||
if (tasksLength === 1) {
|
||
this.insertTaskAtCorrectPosition(tasks[0])
|
||
return
|
||
}
|
||
|
||
// 性能优化:对于少量任务,避免创建新数组和排序开销
|
||
if (tasksLength <= 10) {
|
||
// 找出最小的 insert_sequence
|
||
let minIndex = 0
|
||
for (let i = 1; i < tasksLength; i++) {
|
||
if (tasks[i].insert_sequence < tasks[minIndex].insert_sequence) {
|
||
minIndex = i
|
||
}
|
||
}
|
||
|
||
// 按顺序插入任务
|
||
let inserted = 0
|
||
let currentMin = tasks[minIndex]
|
||
this.insertTaskAtCorrectPosition(currentMin)
|
||
inserted++
|
||
|
||
while (inserted < tasksLength) {
|
||
minIndex = -1
|
||
let minSequence = Infinity
|
||
|
||
// 找出剩余任务中 insert_sequence 最小的
|
||
for (let i = 0; i < tasksLength; i++) {
|
||
const task = tasks[i]
|
||
if (task !== currentMin && task.insert_sequence < minSequence) {
|
||
minIndex = i
|
||
minSequence = task.insert_sequence
|
||
}
|
||
}
|
||
|
||
if (minIndex !== -1) {
|
||
currentMin = tasks[minIndex]
|
||
this.insertTaskAtCorrectPosition(currentMin)
|
||
inserted++
|
||
}
|
||
}
|
||
} else {
|
||
// 对于大量任务,使用排序后批量处理
|
||
// 创建一个新数组并排序,避免修改原数组
|
||
const sortedTasks = [...tasks].sort((a, b) => a.insert_sequence - b.insert_sequence)
|
||
|
||
// 性能优化:使用 requestAnimationFrame 进行批处理,更好地配合浏览器渲染周期
|
||
const batchSize = 50 // 每批处理的任务数量
|
||
const totalBatches = Math.ceil(sortedTasks.length / batchSize)
|
||
|
||
const processBatch = (batchIndex) => {
|
||
const startIndex = batchIndex * batchSize
|
||
const endIndex = Math.min(startIndex + batchSize, sortedTasks.length)
|
||
|
||
// 处理当前批次的任务
|
||
for (let i = startIndex; i < endIndex; i++) {
|
||
this.insertTaskAtCorrectPosition(sortedTasks[i])
|
||
}
|
||
|
||
// 如果还有更多批次,安排下一个批次
|
||
if (batchIndex < totalBatches - 1) {
|
||
// 使用 requestAnimationFrame 配合浏览器渲染周期
|
||
// 如果不支持,回退到 setTimeout
|
||
if (typeof requestAnimationFrame !== 'undefined') {
|
||
requestAnimationFrame(() => processBatch(batchIndex + 1))
|
||
} else {
|
||
setTimeout(() => processBatch(batchIndex + 1), 0)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 开始处理第一批
|
||
processBatch(0)
|
||
}
|
||
}
|
||
}
|
||
})
|