Compare commits
20 Commits
efd61b30f4
...
b956b4ef79
Author | SHA1 | Date | |
---|---|---|---|
|
b956b4ef79 | ||
|
99898555d4 | ||
|
57555751e4 | ||
|
f2b194f712 | ||
|
f010287bfa | ||
|
d62c26bee3 | ||
|
123bf8051f | ||
|
4863b4c77c | ||
|
df372ad14e | ||
|
8736155e64 | ||
|
435700cc4f | ||
|
871e33990a | ||
87de44f7f4 | |||
7886f260d4 | |||
32022fe61b | |||
24c94a04ad | |||
4d681f195e | |||
56098b5699 | |||
c110dc9ad6 | |||
982c2221e2 |
2
env/.env.test
vendored
2
env/.env.test
vendored
@ -7,6 +7,6 @@ VUE_APP_PREVIEW=false
|
||||
#VITE_SOCKET_API=ws://192.168.88.21:9504
|
||||
VITE_BASE_API=http://114.218.158.24:8503
|
||||
VITE_SOCKET_API=ws://114.218.158.24:8504
|
||||
VITE_EPR_BASEURL=http://114.218.158.24:9020
|
||||
VITE_EPR_BASEURL=http://172.16.100.93:8503
|
||||
VITE_PAGE_URL=http://172.16.100.93:9032
|
||||
VUE_APP_WEBSITE_NAME=""
|
@ -24,9 +24,11 @@
|
||||
"@vicons/ionicons5": "^0.13.0",
|
||||
"@vueup/vue-quill": "^1.2.0",
|
||||
"@vueuse/core": "^10.7.0",
|
||||
"@vueuse/rxjs": "^13.4.0",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"axios": "^1.6.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"dexie": "^4.0.11",
|
||||
"highlight.js": "^11.5.0",
|
||||
"js-audio-recorder": "^1.0.7",
|
||||
"lodash-es": "^4.17.21",
|
||||
@ -36,6 +38,7 @@
|
||||
"quill": "^1.3.7",
|
||||
"quill-image-uploader": "^1.3.0",
|
||||
"quill-mention": "^4.1.0",
|
||||
"rxjs": "^7.8.2",
|
||||
"sortablejs": "^1.15.6",
|
||||
"viewerjs": "^1.11.7",
|
||||
"vue": "^3.3.11",
|
||||
|
7385
pnpm-lock.yaml
Normal file
7385
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -32,7 +32,7 @@ class Read extends Base {
|
||||
|
||||
handle() {
|
||||
if (this.type == 'total') {
|
||||
console.error('====接收到了新版已读回执全量=====', this.resource)
|
||||
|
||||
const readList = this.resource.result
|
||||
if (readList.length > 0) {
|
||||
readList.forEach((item) => {
|
||||
|
@ -227,14 +227,13 @@ class Talk extends Base {
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
console.log('输出加载1')
|
||||
// 获取聊天面板元素节点
|
||||
const el = document.getElementById('imChatPanel')
|
||||
if (!el) return
|
||||
|
||||
// 判断的滚动条是否在底部
|
||||
const isBottom = isScrollAtBottom(el)
|
||||
|
||||
if (isBottom || record.user_id == this.getAccountId()) {
|
||||
scrollToBottom()
|
||||
} else {
|
||||
|
@ -130,19 +130,19 @@ export const useTalkRecord = (uid: number) => {
|
||||
cursor: loadConfig.cursor,
|
||||
limit: 30
|
||||
}
|
||||
|
||||
loadConfig.status = 0
|
||||
// 如果不是从本地数据库加载的,则设置加载状态为0(加载中)
|
||||
if (loadConfig.status !== 2 && loadConfig.status !== 3) {
|
||||
loadConfig.status = 0
|
||||
}
|
||||
|
||||
let scrollHeight = 0
|
||||
console.log('加载数据列表load')
|
||||
const el = document.getElementById('imChatPanel')
|
||||
if (el) {
|
||||
scrollHeight = el.scrollHeight
|
||||
}
|
||||
|
||||
const { data, code } = await ServeTalkRecords(request)
|
||||
if (code != 200) {
|
||||
return (loadConfig.status = 1)
|
||||
return (loadConfig.status = (loadConfig.status === 2 || loadConfig.status === 3) ? loadConfig.status : 1) // 如果已经从本地加载了数据,保持原状态
|
||||
}
|
||||
// 防止对话切换过快,数据渲染错误
|
||||
if (
|
||||
@ -154,20 +154,77 @@ export const useTalkRecord = (uid: number) => {
|
||||
|
||||
const items = (data.items || []).map((item: ITalkRecord) => formatTalkRecord(uid, item))
|
||||
|
||||
// 同步到本地数据库
|
||||
try {
|
||||
const { batchAddOrUpdateMessages } = await import('@/utils/db')
|
||||
await batchAddOrUpdateMessages(data.items || [], params.talk_type, params.receiver_id, true, 'sequence')
|
||||
console.log('聊天记录已同步到本地数据库')
|
||||
} catch (error) {
|
||||
console.error('同步聊天记录到本地数据库失败:', error)
|
||||
}
|
||||
|
||||
// 如果是从本地数据库加载的数据,且服务器返回的数据与本地数据相同,则不需要更新UI
|
||||
if ((loadConfig.status === 2 || loadConfig.status === 3) && request.cursor === 0) {
|
||||
try {
|
||||
// 获取最新的本地数据库消息进行比较
|
||||
const { getMessages } = await import('@/utils/db')
|
||||
const localMessages = await getMessages(
|
||||
params.talk_type,
|
||||
uid,
|
||||
params.receiver_id,
|
||||
items.length || 30, // 获取与服务器返回数量相同的消息
|
||||
0 // 从第一页开始
|
||||
)
|
||||
|
||||
// 格式化本地消息,确保与服务器消息结构一致
|
||||
const formattedLocalMessages = localMessages.map((item: ITalkRecord) => formatTalkRecord(uid, item))
|
||||
|
||||
|
||||
// 改进比较逻辑:检查消息数量和所有消息的ID是否匹配
|
||||
if (formattedLocalMessages.length === items.length && formattedLocalMessages.length > 0) {
|
||||
// 创建消息ID映射,用于快速查找
|
||||
const serverMsgMap = new Map()
|
||||
items.forEach(item => serverMsgMap.set(item.msg_id, item))
|
||||
|
||||
// 检查每条本地消息是否与服务器消息匹配
|
||||
const allMatch = formattedLocalMessages.every(localMsg => {
|
||||
const serverMsg = serverMsgMap.get(localMsg.msg_id)
|
||||
// 检查消息是否存在且关键状态是否一致(考虑撤回、已读等状态变化)
|
||||
return serverMsg &&
|
||||
serverMsg.is_revoke === localMsg.is_revoke &&
|
||||
serverMsg.is_read === localMsg.is_read &&
|
||||
(serverMsg.send_status === localMsg.send_status ||
|
||||
(!serverMsg.send_status && !localMsg.send_status)) &&
|
||||
serverMsg.content === localMsg.content
|
||||
})
|
||||
|
||||
if (allMatch) {
|
||||
console.log('本地数据与服务器数据一致,无需更新UI')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 数据不一致,需要更新UI
|
||||
console.log('本地数据与服务器数据不一致,更新UI')
|
||||
} catch (error) {
|
||||
console.error('比较本地数据和服务器数据时出错:', error)
|
||||
// 出错时默认更新UI
|
||||
}
|
||||
}
|
||||
|
||||
if (request.cursor == 0) {
|
||||
// 判断是否是初次加载
|
||||
dialogueStore.clearDialogueRecord()
|
||||
}
|
||||
|
||||
dialogueStore.unshiftDialogueRecord(items.reverse())
|
||||
|
||||
|
||||
loadConfig.status = items.length >= request.limit ? 1 : 2
|
||||
|
||||
loadConfig.cursor = data.cursor
|
||||
|
||||
nextTick(() => {
|
||||
const el = document.getElementById('imChatPanel')
|
||||
|
||||
if (el) {
|
||||
if (request.cursor == 0) {
|
||||
// el.scrollTop = el.scrollHeight
|
||||
@ -175,6 +232,12 @@ export const useTalkRecord = (uid: number) => {
|
||||
// setTimeout(() => {
|
||||
// el.scrollTop = el.scrollHeight + 1000
|
||||
// }, 500)
|
||||
console.log('滚动到底部')
|
||||
|
||||
// 在初次加载完成后恢复上传任务
|
||||
// 确保在所有聊天记录加载完成后再恢复上传任务
|
||||
dialogueStore.restoreUploadTasks()
|
||||
|
||||
scrollToBottom()
|
||||
} else {
|
||||
el.scrollTop = el.scrollHeight - scrollHeight
|
||||
@ -189,9 +252,7 @@ export const useTalkRecord = (uid: number) => {
|
||||
|
||||
// 获取当前消息的最小 sequence
|
||||
const getMinSequence = () => {
|
||||
console.error('records.value', records.value)
|
||||
if (!records.value.length) return 0
|
||||
console.error(Math.min(...records.value.map((item) => item.sequence)))
|
||||
return Math.min(...records.value.map((item) => item.sequence))
|
||||
}
|
||||
// 获取当前消息的最大 sequence
|
||||
@ -200,13 +261,56 @@ export const useTalkRecord = (uid: number) => {
|
||||
return Math.max(...records.value.map((item) => item.sequence))
|
||||
}
|
||||
|
||||
// 从本地数据库加载聊天记录
|
||||
const loadFromLocalDB = async (params: Params) => {
|
||||
try {
|
||||
// 导入 getMessages 函数
|
||||
const { getMessages } = await import('@/utils/db')
|
||||
// 从本地数据库获取聊天记录
|
||||
const localMessages = await getMessages(
|
||||
params.talk_type,
|
||||
uid,
|
||||
params.receiver_id,
|
||||
params.limit || 30,
|
||||
0 // 从第一页开始
|
||||
// 不传入 maxSequence 参数,获取最新的消息
|
||||
)
|
||||
// 如果有本地数据
|
||||
if (localMessages && localMessages.length > 0) {
|
||||
// 清空现有记录
|
||||
dialogueStore.clearDialogueRecord()
|
||||
|
||||
// 格式化并添加记录
|
||||
const formattedMessages = localMessages.map((item: ITalkRecord) => formatTalkRecord(uid, item))
|
||||
dialogueStore.unshiftDialogueRecord(formattedMessages)
|
||||
|
||||
// 设置加载状态为完成(3表示从本地数据库加载完成)
|
||||
loadConfig.status = 3
|
||||
|
||||
// 恢复上传任务
|
||||
dialogueStore.restoreUploadTasks()
|
||||
|
||||
// 滚动到底部
|
||||
nextTick(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('从本地数据库加载聊天记录失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载数据主入口,支持指定消息定位模式
|
||||
* @param params 原有参数
|
||||
* @param options 可选,{ specifiedMsg } 指定消息对象
|
||||
*/
|
||||
const onLoad = (params: Params, options?: LoadOptions) => {
|
||||
// 如果会话切换,重置所有状态
|
||||
const onLoad = async (params: Params, options?: LoadOptions) => {
|
||||
if (
|
||||
params.talk_type !== loadConfig.talk_type ||
|
||||
params.receiver_id !== loadConfig.receiver_id
|
||||
@ -221,7 +325,6 @@ export const useTalkRecord = (uid: number) => {
|
||||
// 新增:支持指定消息定位模式,参数以传入为准合并
|
||||
if (options?.specifiedMsg?.cursor !== undefined) {
|
||||
loadConfig.specialParams = { ...options.specifiedMsg } // 记录特殊参数,供分页加载用
|
||||
console.error('options', options)
|
||||
loadConfig.status = 0 // 复用主流程 loading 状态
|
||||
// 以 params 为基础,合并 specifiedMsg 的所有字段(只要有就覆盖)
|
||||
const contextParams = {
|
||||
@ -231,6 +334,7 @@ export const useTalkRecord = (uid: number) => {
|
||||
//msg_id是用来做定位的,不做参数,所以这里清空
|
||||
contextParams.msg_id = ''
|
||||
ServeTalkRecords(contextParams).then(({ data, code }) => {
|
||||
console.log('data',data)
|
||||
if (code !== 200) {
|
||||
loadConfig.status = 2
|
||||
return
|
||||
@ -322,6 +426,8 @@ export const useTalkRecord = (uid: number) => {
|
||||
})
|
||||
} else {
|
||||
// 其他情况滚动到底部
|
||||
// 在特殊参数模式下也需要恢复上传任务
|
||||
dialogueStore.restoreUploadTasks()
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
@ -331,14 +437,22 @@ export const useTalkRecord = (uid: number) => {
|
||||
}
|
||||
|
||||
loadConfig.specialParams = undefined // 普通模式清空
|
||||
|
||||
// 设置初始加载状态为0(加载中)
|
||||
loadConfig.status = 0
|
||||
|
||||
// 先从本地数据库加载数据
|
||||
const hasLocalData = await loadFromLocalDB(params)
|
||||
|
||||
// 无论是否有本地数据,都从服务器获取最新数据
|
||||
// 原有逻辑
|
||||
console.log('onLoad()执行load')
|
||||
load(params)
|
||||
}
|
||||
|
||||
// 向上加载更多(兼容特殊参数模式)
|
||||
const onRefreshLoad = () => {
|
||||
console.error('loadConfig.status', loadConfig.status)
|
||||
if (loadConfig.status == 1) {
|
||||
if (loadConfig.status == 1 || loadConfig.status == 3) {
|
||||
console.log('specialParams', loadConfig.specialParams)
|
||||
// 判断是否是特殊参数模式
|
||||
if (loadConfig.specialParams && typeof loadConfig.specialParams === 'object') {
|
||||
@ -369,6 +483,7 @@ export const useTalkRecord = (uid: number) => {
|
||||
} else {
|
||||
// 如果不匹配,重置为普通模式
|
||||
resetLoadConfig()
|
||||
console.log('load执行2')
|
||||
load({
|
||||
receiver_id: loadConfig.receiver_id,
|
||||
talk_type: loadConfig.talk_type,
|
||||
@ -377,6 +492,7 @@ export const useTalkRecord = (uid: number) => {
|
||||
}
|
||||
} else {
|
||||
// 原有逻辑
|
||||
console.log('load执行3')
|
||||
load({
|
||||
receiver_id: loadConfig.receiver_id,
|
||||
talk_type: loadConfig.talk_type,
|
||||
|
@ -8,11 +8,13 @@ import router from './router'
|
||||
import App from './App.vue'
|
||||
import * as plugins from './plugins'
|
||||
import request from "@/api/index.js";
|
||||
|
||||
if (window.__POWERED_BY_WUJIE__) {
|
||||
// eslint-disable-next-line
|
||||
window.__webpack_public_path__ = window.__WUJIE_PUBLIC_PATH__;
|
||||
}
|
||||
async function bootstrap() {
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
|
@ -15,7 +15,9 @@ export const useDialogueStore = defineStore('dialogue', {
|
||||
return {
|
||||
// 对话索引(聊天对话的唯一索引)
|
||||
index_name: '',
|
||||
|
||||
globalUploadList:[],
|
||||
// 添加一个映射,用于快速查找每个会话的上传任务
|
||||
uploadTaskMap: {}, // 格式: { "talk_type_receiver_id": [task1, task2, ...] }
|
||||
// 对话节点
|
||||
talk: {
|
||||
avatar:'',
|
||||
@ -129,8 +131,10 @@ export const useDialogueStore = defineStore('dialogue', {
|
||||
if (data.talk_type == 2) {
|
||||
this.updateGroupMembers()
|
||||
this.getGroupInfo()
|
||||
|
||||
}
|
||||
|
||||
// 注意:上传任务的恢复将在聊天记录加载完成后进行
|
||||
// 在useTalkRecord.ts的onLoad方法中,会在加载完聊天记录后调用restoreUploadTasks方法
|
||||
},
|
||||
|
||||
// 更新提及列表
|
||||
@ -171,10 +175,12 @@ export const useDialogueStore = defineStore('dialogue', {
|
||||
|
||||
// 数组头部压入对话记录
|
||||
unshiftDialogueRecord(records) {
|
||||
console.log('unshiftDialogueRecord')
|
||||
this.records.unshift(...records)
|
||||
},
|
||||
//数组尾部加入更多对话记录
|
||||
addDialogueRecordForLoadMore(records){
|
||||
console.log('addDialogueRecordForLoadMore')
|
||||
this.records.push(...records)
|
||||
},
|
||||
async getGroupInfo(){
|
||||
@ -186,24 +192,55 @@ export const useDialogueStore = defineStore('dialogue', {
|
||||
}
|
||||
},
|
||||
// 推送对话记录
|
||||
addDialogueRecord(record) {
|
||||
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)
|
||||
}
|
||||
},
|
||||
|
||||
// 更新对话记录
|
||||
updateDialogueRecord(params) {
|
||||
async updateDialogueRecord(params) {
|
||||
const { msg_id = '' } = params
|
||||
|
||||
const item = this.records.find((item) => item.msg_id === msg_id)
|
||||
|
||||
item && Object.assign(item, params)
|
||||
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)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 批量删除对话记录
|
||||
batchDelDialogueRecord(msgIds = []) {
|
||||
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)
|
||||
|
||||
@ -292,6 +329,16 @@ export const useDialogueStore = defineStore('dialogue', {
|
||||
|
||||
// 更新视频上传进度
|
||||
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
|
||||
)
|
||||
@ -301,6 +348,44 @@ export const useDialogueStore = defineStore('dialogue', {
|
||||
}
|
||||
},
|
||||
|
||||
// 添加上传任务
|
||||
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)
|
||||
}
|
||||
},
|
||||
|
||||
// 视频上传完成后更新消息
|
||||
completeUpload(uploadId, videoInfo) {
|
||||
const record = this.records.find(item =>
|
||||
@ -317,6 +402,135 @@ export const useDialogueStore = defineStore('dialogue', {
|
||||
// 更新会话信息
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -3,6 +3,7 @@ import { ServeGetTalkList, ServeCreateTalkList } from '@/api/chat'
|
||||
import { formatTalkItem, ttime, KEY_INDEX_NAME } from '@/utils/talk'
|
||||
import { useEditorDraftStore } from './editor-draft'
|
||||
import { ISession } from '@/types/chat'
|
||||
import { getConversations, addOrUpdateConversation, deleteConversation, getConversation } from '@/utils/db'
|
||||
|
||||
interface TalkStoreState {
|
||||
loadStatus: number
|
||||
@ -45,56 +46,126 @@ export const useTalkStore = defineStore('talk', {
|
||||
},
|
||||
|
||||
// 更新对话节点
|
||||
updateItem(params: any) {
|
||||
async updateItem(params: any) {
|
||||
const item = this.items.find((item) => item.index_name === params.index_name)
|
||||
|
||||
item && Object.assign(item, params)
|
||||
if (item) {
|
||||
Object.assign(item, params)
|
||||
|
||||
// 同步更新本地数据库
|
||||
try {
|
||||
await addOrUpdateConversation(JSON.parse(JSON.stringify(item)))
|
||||
} catch (error) {
|
||||
console.error('更新本地会话失败:', error)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 新增对话节点
|
||||
addItem(params: any) {
|
||||
async addItem(params: any) {
|
||||
this.items = [params, ...this.items]
|
||||
|
||||
// 同步添加到本地数据库
|
||||
try {
|
||||
await addOrUpdateConversation(JSON.parse(JSON.stringify(params)))
|
||||
} catch (error) {
|
||||
console.error('添加本地会话失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
// 移除对话节点
|
||||
delItem(index_name: string) {
|
||||
async delItem(index_name: string) {
|
||||
const i = this.items.findIndex((item) => item.index_name === index_name)
|
||||
|
||||
if (i >= 0) {
|
||||
const item = this.items[i]
|
||||
this.items.splice(i, 1)
|
||||
|
||||
// 同步从本地数据库删除
|
||||
try {
|
||||
// 从本地数据库中查找并删除会话
|
||||
const [talkType, receiverId] = index_name.split('_')
|
||||
const conversation = await getConversation(Number(talkType), Number(receiverId))
|
||||
|
||||
if (conversation && conversation.id) {
|
||||
await deleteConversation(conversation.id, false) // 不删除相关消息
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除本地会话失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
this.items = [...this.items]
|
||||
},
|
||||
|
||||
// 更新对话消息
|
||||
updateMessage(params: any) {
|
||||
async updateMessage(params: any) {
|
||||
const item = this.items.find((item) => item.index_name === params.index_name)
|
||||
|
||||
if (item) {
|
||||
item.unread_num++
|
||||
item.msg_text = params.msg_text
|
||||
item.updated_at = params.updated_at
|
||||
|
||||
// 同步更新本地数据库中的会话信息
|
||||
try {
|
||||
await addOrUpdateConversation(JSON.parse(JSON.stringify(item)))
|
||||
} catch (error) {
|
||||
console.error('更新本地会话消息失败:', error)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 更新联系人备注
|
||||
setRemark(params: any) {
|
||||
async setRemark(params: any) {
|
||||
const item = this.items.find((item) => item.index_name === `1_${params.user_id}`)
|
||||
|
||||
item && (item.remark = params.remark)
|
||||
if (item) {
|
||||
item.remark = params.remark
|
||||
|
||||
// 同步更新本地数据库
|
||||
try {
|
||||
await addOrUpdateConversation(JSON.parse(JSON.stringify(item)))
|
||||
} catch (error) {
|
||||
console.error('更新本地联系人备注失败:', error)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 加载会话列表
|
||||
loadTalkList() {
|
||||
async loadTalkList() {
|
||||
this.loadStatus = 2
|
||||
|
||||
const resp = ServeGetTalkList()
|
||||
try {
|
||||
// 先从本地数据库加载会话列表
|
||||
const localConversations = await getConversations()
|
||||
if (localConversations && localConversations.length > 0) {
|
||||
// 将本地会话列表转换为应用所需格式
|
||||
this.items = localConversations.map((item: any) => {
|
||||
// 确保本地存储的会话格式与应用一致
|
||||
const value = formatTalkItem(item)
|
||||
|
||||
resp.then(({ code, data }) => {
|
||||
if (code == 200) {
|
||||
const draft = useEditorDraftStore().items[value.index_name]
|
||||
if (draft) {
|
||||
value.draft_text = JSON.parse(draft).text || ''
|
||||
}
|
||||
|
||||
this.items = data.items.map((item: any) => {
|
||||
if (value.is_robot == 1) {
|
||||
value.is_online = 1
|
||||
}
|
||||
return value
|
||||
})
|
||||
|
||||
// 设置为加载完成状态,因为已从本地加载了数据,不需要等待服务器数据就可以显示
|
||||
this.loadStatus = 3
|
||||
}
|
||||
|
||||
// 从服务器获取最新会话列表
|
||||
const resp = await ServeGetTalkList()
|
||||
|
||||
if (resp.code == 200) {
|
||||
// 将服务器返回的会话列表转换为应用所需格式
|
||||
const serverItems = resp.data.items.map((item: any) => {
|
||||
const value = formatTalkItem(item)
|
||||
|
||||
const draft = useEditorDraftStore().items[value.index_name]
|
||||
@ -108,22 +179,40 @@ export const useTalkStore = defineStore('talk', {
|
||||
return value
|
||||
})
|
||||
|
||||
// 更新状态和本地数据库
|
||||
this.items = serverItems
|
||||
|
||||
// 将最新的会话列表保存到本地数据库
|
||||
for (const item of serverItems) {
|
||||
await addOrUpdateConversation(item)
|
||||
}
|
||||
|
||||
this.loadStatus = 3
|
||||
} else {
|
||||
this.loadStatus = 4
|
||||
// 如果服务器请求失败但本地有数据,保持使用本地数据
|
||||
if (this.items.length === 0) {
|
||||
this.loadStatus = 4
|
||||
} else {
|
||||
this.loadStatus = 3
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
resp.catch(() => {
|
||||
this.loadStatus = 4
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('加载会话列表失败:', error)
|
||||
|
||||
// 如果有本地数据,即使服务器请求失败也显示本地数据
|
||||
if (this.items.length === 0) {
|
||||
this.loadStatus = 4
|
||||
} else {
|
||||
this.loadStatus = 3
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
findTalkIndex(index_name: string) {
|
||||
return this.items.findIndex((item: ISession) => item.index_name === index_name)
|
||||
},
|
||||
|
||||
toTalk(talk_type: number, receiver_id: number, router: any) {
|
||||
async toTalk(talk_type: number, receiver_id: number, router: any) {
|
||||
const route = {
|
||||
path: '/message',
|
||||
query: {
|
||||
@ -136,13 +225,31 @@ export const useTalkStore = defineStore('talk', {
|
||||
return router.push(route)
|
||||
}
|
||||
|
||||
ServeCreateTalkList({
|
||||
talk_type,
|
||||
receiver_id
|
||||
}).then(({ code, data, message }) => {
|
||||
if (code == 200) {
|
||||
try {
|
||||
// 先检查本地数据库中是否有该会话
|
||||
const localConversation = await getConversation(talk_type, receiver_id)
|
||||
|
||||
if (localConversation) {
|
||||
// 如果本地有该会话,直接添加到列表中
|
||||
if (this.findTalkIndex(`${talk_type}_${receiver_id}`) === -1) {
|
||||
this.addItem(formatTalkItem(data))
|
||||
this.addItem(formatTalkItem(localConversation))
|
||||
}
|
||||
|
||||
sessionStorage.setItem(KEY_INDEX_NAME, `${talk_type}_${receiver_id}`)
|
||||
return router.push(route)
|
||||
}
|
||||
|
||||
// 如果本地没有,则从服务器创建
|
||||
const { code, data, message } = await ServeCreateTalkList({
|
||||
talk_type,
|
||||
receiver_id
|
||||
})
|
||||
|
||||
if (code == 200) {
|
||||
const formattedItem = formatTalkItem(data)
|
||||
|
||||
if (this.findTalkIndex(`${talk_type}_${receiver_id}`) === -1) {
|
||||
await this.addItem(formattedItem) // 使用 await 确保本地数据库同步更新
|
||||
}
|
||||
|
||||
sessionStorage.setItem(KEY_INDEX_NAME, `${talk_type}_${receiver_id}`)
|
||||
@ -150,7 +257,10 @@ export const useTalkStore = defineStore('talk', {
|
||||
} else {
|
||||
window['$message'].info(message)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('创建会话失败:', error)
|
||||
window['$message'].error('创建会话失败,请稍后再试')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1,7 +1,13 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ServeFindFileSplitInfo, ServeFileSubareaUpload } from '@/api/upload'
|
||||
import { ServeSendTalkFile } from '@/api/chat'
|
||||
import { uploadImg } from '@/api/upload'
|
||||
// import { message } from 'naive-ui'
|
||||
import {
|
||||
ServeSendTalkFile
|
||||
} from '@/api/chat'
|
||||
import {
|
||||
uploadImg,
|
||||
ServeFindFileSplitInfo,
|
||||
ServeFileSubareaUpload
|
||||
} from '@/api/upload'
|
||||
import {
|
||||
useDialogueStore
|
||||
} from '@/store'
|
||||
@ -140,12 +146,12 @@ export const useUploadsStore = defineStore('uploads', {
|
||||
this.triggerUpload(upload_id, clientUploadId)
|
||||
} else {
|
||||
message.error(res.message)
|
||||
onProgress(-1) // 通知上传失败
|
||||
this.handleUploadError(upload_id, clientUploadId)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("初始化分片上传失败:", error);
|
||||
message.error("初始化上传失败,请重试")
|
||||
onProgress(-1)
|
||||
this.handleUploadError(upload_id, clientUploadId)
|
||||
}
|
||||
},
|
||||
|
||||
@ -201,26 +207,20 @@ export const useUploadsStore = defineStore('uploads', {
|
||||
this.triggerUpload(uploadId, clientUploadId)
|
||||
}
|
||||
} else {
|
||||
updatedItem.onProgress(-1)
|
||||
// 上传失败处理
|
||||
console.error(`分片上传失败,错误码: ${res.code},错误信息: ${res.message || '未知错误'}`);
|
||||
updatedItem.status = 3
|
||||
|
||||
|
||||
this.handleUploadError(uploadId, clientUploadId || '')
|
||||
}
|
||||
} catch (error) {
|
||||
updatedItem.onProgress(-1)
|
||||
console.error("分片上传错误:", error);
|
||||
|
||||
// 获取最新的项目状态
|
||||
// 这里不应该重新定义变量,而是使用已有的updatedItem
|
||||
// const updatedItem = this.findItem(uploadId)
|
||||
if (!updatedItem) return
|
||||
|
||||
// 如果是暂停导致的错误,不改变状态
|
||||
if (updatedItem.is_paused) return
|
||||
|
||||
updatedItem.status = 3
|
||||
this.handleUploadError(uploadId, clientUploadId || '')
|
||||
}
|
||||
},
|
||||
|
||||
@ -244,6 +244,10 @@ export const useUploadsStore = defineStore('uploads', {
|
||||
talk_type: item.talk_type
|
||||
})
|
||||
|
||||
// 从DialogueStore中移除上传任务
|
||||
const dialogueStore = useDialogueStore()
|
||||
dialogueStore.removeUploadTask(clientUploadId)
|
||||
|
||||
if (item.onComplete) {
|
||||
item.onComplete(item)
|
||||
}
|
||||
@ -291,5 +295,21 @@ export const useUploadsStore = defineStore('uploads', {
|
||||
// 从上传列表中移除旧的上传项
|
||||
this.items = this.items.filter(i => i.client_upload_id !== clientUploadId)
|
||||
},
|
||||
|
||||
// 上传失败处理
|
||||
async handleUploadError(uploadId: string, clientUploadId: string) {
|
||||
const item = this.findItem(uploadId)
|
||||
if (!item) return
|
||||
|
||||
item.status = 3 // 设置为上传失败状态
|
||||
|
||||
// 从DialogueStore中移除上传任务
|
||||
const dialogueStore = useDialogueStore()
|
||||
dialogueStore.removeUploadTask(clientUploadId)
|
||||
|
||||
if (item.onProgress) {
|
||||
item.onProgress(-1) // 通知上传失败
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
381
src/utils/db.js
Normal file
381
src/utils/db.js
Normal file
@ -0,0 +1,381 @@
|
||||
|
||||
import Dexie from 'dexie';
|
||||
|
||||
export const db = new Dexie('chatHistory');
|
||||
|
||||
// 定义数据库表结构和索引
|
||||
// 版本3:优化了索引,提高了查询和排序性能
|
||||
db.version(4).stores({
|
||||
/**
|
||||
* 聊天记录表
|
||||
* - msg_id: 消息唯一ID (主键)
|
||||
* - sequence: 消息序列号,用于排序
|
||||
* - [talk_type+receiver_id]: 复合索引,用于快速查询会话消息
|
||||
* - created_at: 消息创建时间,用于排序
|
||||
* - [talk_type+receiver_id+sequence]: 复合索引,用于高效分页查询
|
||||
*/
|
||||
messages: 'msg_id, sequence, [talk_type+receiver_id], created_at, [talk_type+receiver_id+sequence]',
|
||||
|
||||
/**
|
||||
* 会话表
|
||||
* - ++id: 自增主键
|
||||
* - &index_name: 唯一索引 (talk_type + '_' + receiver_id)
|
||||
* - updated_at: 索引,用于排序
|
||||
* - is_top: 索引,用于置顶排序
|
||||
*/
|
||||
conversations: 'id, &index_name, talk_type, receiver_id, updated_at, unread_num, is_top',
|
||||
});
|
||||
|
||||
db.on('ready', () => {
|
||||
console.log(`数据库已就绪,版本: ${db.verno}`);
|
||||
});
|
||||
|
||||
/** 消息类型常量 */
|
||||
export const MessageType = {
|
||||
TEXT: 1, // 文本消息
|
||||
IMAGE: 2, // 图片消息
|
||||
FILE: 3, // 文件消息
|
||||
AUDIO: 4, // 语音消息
|
||||
VIDEO: 5, // 视频消息
|
||||
LOCATION: 6, // 位置消息
|
||||
CARD: 7, // 名片消息
|
||||
};
|
||||
|
||||
/** 会话类型常量 */
|
||||
export const TalkType = {
|
||||
PRIVATE: 1, // 私聊
|
||||
GROUP: 2, // 群聊
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成一个简单的UUID
|
||||
* @returns {string} UUID
|
||||
*/
|
||||
function generateUUID() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
// #region 消息操作
|
||||
|
||||
/**
|
||||
* 添加或更新一条聊天记录
|
||||
* @param {object} message - 消息对象
|
||||
* @returns {Promise<string>} 消息ID
|
||||
*/
|
||||
export async function addMessage(message) {
|
||||
try {
|
||||
if (!message.msg_id) {
|
||||
message.msg_id = generateUUID();
|
||||
}
|
||||
if (!message.created_at) {
|
||||
message.created_at = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
||||
}
|
||||
|
||||
// 使用 put 方法,如果主键已存在则更新,否则添加
|
||||
await db.messages.put(message);
|
||||
return message.msg_id;
|
||||
} catch (error) {
|
||||
console.error('添加或更新消息失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加或更新聊天记录
|
||||
* @param {Array<object>} messages - 消息对象数组
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function batchAddOrUpdateMessages(messages) {
|
||||
try {
|
||||
if (!Array.isArray(messages) || messages.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messagesToStore = messages.map(message => {
|
||||
if (!message.msg_id) {
|
||||
message.msg_id = generateUUID();
|
||||
}
|
||||
if (!message.created_at) {
|
||||
message.created_at = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
||||
}
|
||||
return message;
|
||||
});
|
||||
|
||||
await db.messages.bulkPut(messagesToStore);
|
||||
|
||||
// 更新最后一条消息到会话
|
||||
const latestMessage = messagesToStore[messagesToStore.length - 1];
|
||||
if (latestMessage) {
|
||||
await updateConversationLastMessage(latestMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量添加或更新消息失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定会话的聊天记录
|
||||
* @param {number} talkType - 会话类型 (1:私聊, 2:群聊)
|
||||
* @param {number} userId - 当前用户ID
|
||||
* @param {number} receiverId - 接收者ID (私聊为对方用户ID,群聊为群ID)
|
||||
* @param {number} [limit=30] - 限制返回的记录数量
|
||||
* @param {number|null} [maxSequence=null] - 最大sequence值,用于分页加载更早的消息
|
||||
* @returns {Promise<Array<object>>} 消息列表 (按sequence升序排列)
|
||||
*/
|
||||
export async function getMessages(talkType, userId, receiverId, limit = 30, maxSequence = null) {
|
||||
try {
|
||||
let collection;
|
||||
|
||||
if (maxSequence !== null) {
|
||||
// 加载更多:查询 sequence 小于 maxSequence 的消息
|
||||
collection = db.messages
|
||||
.where('[talk_type+receiver_id+sequence]')
|
||||
.between([talkType, receiverId, 0], [talkType, receiverId, maxSequence], true, false);
|
||||
} else {
|
||||
// 首次加载:查询指定会话的所有消息
|
||||
collection = db.messages.where({ '[talk_type+receiver_id]': [talkType, receiverId] });
|
||||
}
|
||||
|
||||
// 1. reverse() - 利用索引倒序排列,获取最新的消息
|
||||
// 2. limit() - 限制数量,实现分页
|
||||
// 3. toArray() - 执行查询
|
||||
const messages = await collection.reverse().limit(limit).toArray();
|
||||
|
||||
// 再次 reverse() - 将获取到的分页消息按时间正序排列,以便于在界面上显示
|
||||
return messages.reverse();
|
||||
} catch (error) {
|
||||
console.error('获取消息失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记指定会话的所有消息为已读
|
||||
* @param {number} talkType - 会话类型
|
||||
* @param {number} userId - 当前用户ID
|
||||
* @param {number} receiverId - 接收者ID
|
||||
* @returns {Promise<number>} 更新的消息数量
|
||||
*/
|
||||
export async function markMessagesAsRead(talkType, userId, receiverId) {
|
||||
try {
|
||||
let query;
|
||||
if (talkType === TalkType.PRIVATE) {
|
||||
// 私聊:只标记对方发给我的未读消息
|
||||
query = db.messages
|
||||
.where('[talk_type+receiver_id]')
|
||||
.equals([talkType, userId])
|
||||
.and(item => item.user_id === receiverId && item.is_read === 0);
|
||||
} else {
|
||||
// 群聊:标记群里所有非自己的未读消息
|
||||
query = db.messages
|
||||
.where('[talk_type+receiver_id]')
|
||||
.equals([talkType, receiverId])
|
||||
.and(item => item.user_id !== userId && item.is_read === 0);
|
||||
}
|
||||
return await query.modify({ is_read: 1 });
|
||||
} catch (error) {
|
||||
console.error('批量标记消息已读失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回消息
|
||||
* @param {string} msgId - 消息ID
|
||||
* @returns {Promise<number>} 更新记录数 (1或0)
|
||||
*/
|
||||
export async function revokeMessage(msgId) {
|
||||
try {
|
||||
return await db.messages.update(msgId, { is_revoke: 1 });
|
||||
} catch (error) {
|
||||
console.error('撤回消息失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除消息
|
||||
* @param {string} msgId - 消息ID
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function deleteMessage(msgId) {
|
||||
try {
|
||||
await db.messages.delete(msgId);
|
||||
} catch (error) {
|
||||
console.error('删除消息失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// #endregion 消息操作
|
||||
|
||||
// #region 会话操作
|
||||
|
||||
/**
|
||||
* 添加或更新会话
|
||||
* @param {object} conversation - 会话对象
|
||||
* @returns {Promise<number>} 会话ID
|
||||
*/
|
||||
export async function addOrUpdateConversation(conversation) {
|
||||
try {
|
||||
// put 方法会根据唯一索引 index_name 自动判断是添加还是更新
|
||||
return await db.conversations.put(conversation);
|
||||
} catch (error) {
|
||||
console.error('添加或更新会话失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有会话列表
|
||||
* @param {boolean} [includeEmpty=false] - 是否包含没有最后一条消息的会话
|
||||
* @returns {Promise<Array<object>>} 会话列表 (按置顶和更新时间排序)
|
||||
*/
|
||||
export async function getConversations(includeEmpty = false) {
|
||||
try {
|
||||
const filterFn = item => !includeEmpty ? (item.msg_text && item.msg_text.length > 0) : true;
|
||||
|
||||
// 分别查询置顶和非置顶会话,以利用索引并优化性能
|
||||
const topConversationsPromise = db.conversations
|
||||
.where('is_top')
|
||||
.equals(1)
|
||||
.sortBy('updated_at')
|
||||
.then(arr => arr.reverse().filter(filterFn));
|
||||
|
||||
const otherConversationsPromise = db.conversations
|
||||
.where('is_top')
|
||||
.notEqual(1)
|
||||
.sortBy('updated_at')
|
||||
.then(arr => arr.reverse().filter(filterFn));
|
||||
|
||||
const [topConversations, otherConversations] = await Promise.all([
|
||||
topConversationsPromise,
|
||||
otherConversationsPromise,
|
||||
]);
|
||||
|
||||
return [...topConversations, ...otherConversations];
|
||||
} catch (error) {
|
||||
console.error('获取会话列表失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定会话
|
||||
* @param {number} talkType - 会话类型
|
||||
* @param {number} receiverId - 接收者ID
|
||||
* @returns {Promise<object|undefined>} 会话对象
|
||||
*/
|
||||
export async function getConversation(talkType, receiverId) {
|
||||
try {
|
||||
const indexName = `${talkType}_${receiverId}`;
|
||||
return await db.conversations.get({ index_name: indexName });
|
||||
} catch (error) {
|
||||
console.error('获取会话失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话的未读消息数
|
||||
* @param {number} talkType - 会话类型
|
||||
* @param {number} receiverId - 接收者ID
|
||||
* @param {number|null} unreadNum - 未读消息数。如果为null,则自增1
|
||||
* @returns {Promise<number>} 更新的记录数
|
||||
*/
|
||||
export async function updateConversationUnreadNum(talkType, receiverId, unreadNum = null) {
|
||||
try {
|
||||
const indexName = `${talkType}_${receiverId}`;
|
||||
const conversation = await db.conversations.get({ index_name: indexName });
|
||||
|
||||
if (conversation) {
|
||||
const newUnreadNum = unreadNum === null ? (conversation.unread_num || 0) + 1 : unreadNum;
|
||||
return await db.conversations.update(conversation.id, { unread_num: newUnreadNum });
|
||||
}
|
||||
return 0;
|
||||
} catch (error) {
|
||||
console.error('更新会话未读数失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空会话的未读消息数
|
||||
* @param {number} talkType - 会话类型
|
||||
* @param {number} receiverId - 接收者ID
|
||||
* @returns {Promise<number>} 更新的记录数
|
||||
*/
|
||||
export function clearConversationUnreadNum(talkType, receiverId) {
|
||||
return updateConversationUnreadNum(talkType, receiverId, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话及其相关的消息
|
||||
* @param {number} conversationId - 会话ID
|
||||
* @param {boolean} [deleteMessages=false] - 是否同时删除相关的消息记录
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function deleteConversation(conversationId, deleteMessages = false) {
|
||||
try {
|
||||
await db.transaction('rw', db.conversations, db.messages, async () => {
|
||||
const conversation = await db.conversations.get(conversationId);
|
||||
if (!conversation) return;
|
||||
|
||||
// 删除会话
|
||||
await db.conversations.delete(conversationId);
|
||||
|
||||
// 如果需要,删除关联的消息
|
||||
if (deleteMessages) {
|
||||
const { talk_type, receiver_id } = conversation;
|
||||
await db.messages.where({ '[talk_type+receiver_id]': [talk_type, receiver_id] }).delete();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除会话失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话的最后一条消息摘要
|
||||
* @param {object} message - 消息对象
|
||||
* @returns {Promise<number>} 更新的记录数
|
||||
*/
|
||||
export async function updateConversationLastMessage(message) {
|
||||
try {
|
||||
const { talk_type, user_id, receiver_id, msg_type } = message;
|
||||
const targetReceiverId = talk_type === TalkType.PRIVATE ? (user_id === receiver_id ? user_id : receiver_id) : receiver_id;
|
||||
const indexName = `${talk_type}_${targetReceiverId}`;
|
||||
|
||||
const conversation = await db.conversations.get({ index_name: indexName });
|
||||
if (!conversation) return 0;
|
||||
|
||||
let msgText = '';
|
||||
switch (msg_type) {
|
||||
case MessageType.TEXT: msgText = message.content || ''; break;
|
||||
case MessageType.IMAGE: msgText = '[图片]'; break;
|
||||
case MessageType.FILE: msgText = '[文件]'; break;
|
||||
case MessageType.AUDIO: msgText = '[语音]'; break;
|
||||
case MessageType.VIDEO: msgText = '[视频]'; break;
|
||||
case MessageType.LOCATION: msgText = '[位置]'; break;
|
||||
case MessageType.CARD: msgText = '[名片]'; break;
|
||||
default: msgText = '[未知消息]';
|
||||
}
|
||||
|
||||
return await db.conversations.update(conversation.id, {
|
||||
msg_text: msgText,
|
||||
content: message.content || '',
|
||||
updated_at: message.created_at,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新会话最后消息失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// #endregion 会话操作
|
@ -54,7 +54,6 @@ request.interceptors.request.use((config) => {
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use((response) => {
|
||||
console.log('response.data.status',response.data.status)
|
||||
if(response.data.code !==200&&response.data.status!==0){
|
||||
window['$message'].warning(response.data.msg)
|
||||
}
|
||||
|
@ -145,8 +145,7 @@ watch(
|
||||
if (talkParams.type !== 2) {
|
||||
ServeCheckFriend({ receiver_id: newValue.receiver_id, talk_type: 1 }).then((res) => {
|
||||
if (res?.code === 200) {
|
||||
console.log(res, 'ress')
|
||||
isFriend.value = !res.data.is_friend
|
||||
isFriend.value = res.data.is_friend
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -537,7 +536,7 @@ const clearSelectedDateTime = () => {
|
||||
<main class="el-main relative">
|
||||
<div
|
||||
class="p-[15px] pt-[10px] w-[100%] z-99 absolute"
|
||||
v-if="isFriend && talkParams.type !== 2"
|
||||
v-if="!isFriend && talkParams.type !== 2"
|
||||
>
|
||||
<div
|
||||
class="bg-[#FFFFFF] w-[100%] p-[10px] text-[14px] flex justify-between"
|
||||
|
@ -519,6 +519,7 @@ const items = computed((): ISession[] => {
|
||||
|
||||
return [...topItems, ...normalItems]
|
||||
})
|
||||
setTimeout(()=>{console.log('items',items)},2000)
|
||||
watch(
|
||||
() => state.addressBookSearchNickName,
|
||||
(newValue, oldValue) => {
|
||||
@ -592,8 +593,8 @@ const indexName = computed(() => dialogueStore.index_name)
|
||||
|
||||
// 切换会话
|
||||
const onTabTalk = (item: ISession, follow = false) => {
|
||||
console.log('onTabTalk')
|
||||
|
||||
console.log('onTabTalk')
|
||||
console.log('item.index_name === indexName.value',item.index_name === indexName.value)
|
||||
if (item.index_name === indexName.value) return
|
||||
|
||||
searchKeyword.value = ''
|
||||
@ -638,7 +639,7 @@ const onReload = () => {
|
||||
// 初始化加载
|
||||
const onInitialize = () => {
|
||||
let index_name = getCacheIndexName()
|
||||
|
||||
console.log('index_name',index_name)
|
||||
index_name && onTabTalk(talkStore.findItem(index_name), true)
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { watch, onMounted, ref, nextTick, onUnmounted } from 'vue'
|
||||
import { NDropdown, NCheckbox, NPopover, NInfiniteScroll } from 'naive-ui'
|
||||
import { Loading, MoreThree, ToTop } from '@icon-park/vue-next'
|
||||
import { bus } from '@/utils/event-bus'
|
||||
import { useDialogueStore } from '@/store'
|
||||
import { useDialogueStore, useTalkStore } from '@/store'
|
||||
import { formatTime, parseTime } from '@/utils/datetime'
|
||||
import { clipboard, htmlDecode, clipboardImage } from '@/utils/common'
|
||||
import { downloadImage } from '@/utils/functions'
|
||||
@ -19,8 +19,11 @@ import RevokeMessage from '@/components/talk/message/RevokeMessage.vue'
|
||||
import { voiceToText, ServeMessageReadDetail } from '@/api/chat.js'
|
||||
import { confirmBox } from '@/components/confirm-box/service.js'
|
||||
import ws from '@/connect'
|
||||
import { useRouter } from 'vue-router'
|
||||
import avatarModule from '@/components/avatar-module/index.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 定义消息已读状态接口
|
||||
interface ReadStatus {
|
||||
msg_ids: string[]
|
||||
@ -86,11 +89,28 @@ const { dropdown, showDropdownMenu, closeDropdownMenu, isOneMonthBefore } = useM
|
||||
const { showUserInfoModal } = useInject()
|
||||
const dialogueStore = useDialogueStore()
|
||||
const userStore = useUserStore()
|
||||
const talkStore = useTalkStore()
|
||||
// const showUserInfoModal = (uid: number) => {
|
||||
// userStore.getUserInfo(uid)
|
||||
// }
|
||||
// 置底按钮
|
||||
const skipBottom = ref(false)
|
||||
const goToMessage = (result) => {
|
||||
const talk_type = props.talk_type
|
||||
const receiver_id = props.receiver_id
|
||||
dialogueStore.specifiedMsg = encodeURIComponent(
|
||||
JSON.stringify({
|
||||
talk_type,
|
||||
receiver_id,
|
||||
msg_id: result.msg_id,
|
||||
cursor: result.sequence - 15 > 0 ? result.sequence - 15 : 0,
|
||||
direction: 'down',
|
||||
sort_sequence: 'asc',
|
||||
create_time: result.created_at
|
||||
})
|
||||
)
|
||||
talkStore.toTalk(talk_type, receiver_id, router)
|
||||
}
|
||||
// 是否显示消息时间
|
||||
const isShowTalkTime = (index: number, datetime: string) => {
|
||||
if (datetime == undefined) {
|
||||
@ -330,15 +350,16 @@ const onContextMenuHandle = (key: string) => {
|
||||
}
|
||||
|
||||
const onRowClick = (item: ITalkRecord) => {
|
||||
if (dialogueStore.isOpenMultiSelect && isOneMonthBefore(item.created_at.split(' ')[0])) {
|
||||
if (dialogueStore.isOpenMultiSelect) {
|
||||
if (!isOneMonthBefore(item.created_at.split(' ')[0])) {
|
||||
return useMessage.info('只支持转发近一个月内的消息')
|
||||
}
|
||||
console.log('item.msg_type', item.msg_type)
|
||||
if (ForwardableMessageType.includes(item.msg_type)) {
|
||||
item.isCheck = !item.isCheck
|
||||
} else {
|
||||
useMessage.info('此类消息不支持转发')
|
||||
}
|
||||
} else {
|
||||
useMessage.info('只支持转发近一个月内的消息')
|
||||
}
|
||||
}
|
||||
|
||||
@ -351,6 +372,7 @@ let noRefreshTimer: number | null = null
|
||||
watch(
|
||||
() => props,
|
||||
async (newProps) => {
|
||||
console.log('监听props',newProps)
|
||||
await nextTick()
|
||||
// 生成当前会话的唯一标识
|
||||
const newSessionKey = `${newProps.talk_type}_${newProps.receiver_id}`
|
||||
@ -393,7 +415,7 @@ watch(
|
||||
}, 3000)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('fsd付大夫')
|
||||
onLoad(
|
||||
{
|
||||
receiver_id: newProps.receiver_id,
|
||||
@ -403,7 +425,7 @@ watch(
|
||||
specialParams ? { specifiedMsg: specialParams } : undefined
|
||||
)
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
{ deep: true,immediate:true }
|
||||
)
|
||||
|
||||
// onMounted(() => {
|
||||
@ -534,7 +556,6 @@ const checkVisibleOutElements = () => {
|
||||
})
|
||||
if (waitDoCheck.length > 0) {
|
||||
waitDoCheck.forEach((doCheckItem) => {
|
||||
console.error('====组装了新版已读回执参数,需要发送socket=====', doCheckItem)
|
||||
ws.emit('im.message.listen.read', doCheckItem)
|
||||
})
|
||||
}
|
||||
@ -594,7 +615,6 @@ watch(
|
||||
if (observer) {
|
||||
observer.disconnect()
|
||||
}
|
||||
|
||||
// 重新初始化观察者
|
||||
const options = {
|
||||
root: null,
|
||||
@ -602,7 +622,6 @@ watch(
|
||||
rootMargin: '50px 0px'
|
||||
}
|
||||
observer = new IntersectionObserver(handleIntersection, options)
|
||||
|
||||
// 重新观察所有消息元素
|
||||
const messageElements = document.querySelectorAll('.message-item')
|
||||
messageElements.forEach((el) => {
|
||||
@ -770,7 +789,7 @@ const onCustomSkipBottomEvent = () => {
|
||||
<div class="load-toolbar pointer">
|
||||
<span v-if="loadConfig.status == 0"> 正在加载数据中 ... </span>
|
||||
<span v-else-if="loadConfig.status == 1" @click="onRefreshLoad"> 查看更多消息 ... </span>
|
||||
<span v-else class="no-more"> 没有更多消息了 </span>
|
||||
<span v-else-if="loadConfig.status == 2 || loadConfig.status == 3" class="no-more"> 没有更多消息了 </span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@ -907,11 +926,11 @@ const onCustomSkipBottomEvent = () => {
|
||||
<n-icon class="more-tools pointer" :component="MoreThree" @click="onContextMenu($event, item)" />
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- @click="onJumpMessage(item.extra?.reply?.msg_id)" -->
|
||||
<div
|
||||
v-if="item.extra.reply"
|
||||
class="talk-reply pointer"
|
||||
@click="onJumpMessage(item.extra?.reply?.msg_id)"
|
||||
@click="goToMessage(item.extra?.reply)"
|
||||
>
|
||||
<n-icon :component="ToTop" size="14" class="icon-top" />
|
||||
<span class="ellipsis">
|
||||
@ -1056,6 +1075,7 @@ const onCustomSkipBottomEvent = () => {
|
||||
&.border {
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--im-primary-color);
|
||||
background-color: red;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -111,12 +111,15 @@ const onSendVideoEvent = async ({ data }) => {
|
||||
|
||||
// 先创建一个带有上传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,
|
||||
talk_type: props.talk_type,
|
||||
msg_type: 5, // 视频消息类型
|
||||
user_id: props.uid,
|
||||
receiver_id: props.receiver_id,
|
||||
@ -137,8 +140,8 @@ const onSendVideoEvent = async ({ data }) => {
|
||||
float: 'right' // 我发送的消息显示在右侧
|
||||
}
|
||||
|
||||
// 直接添加到对话记录中
|
||||
dialogueStore.addDialogueRecord(tempMessage)
|
||||
// 使用新的方法添加上传任务
|
||||
dialogueStore.addUploadTask(tempMessage)
|
||||
nextTick(()=>{
|
||||
scrollToBottom()
|
||||
})
|
||||
@ -151,8 +154,7 @@ const onSendVideoEvent = async ({ data }) => {
|
||||
dialogueStore.updateUploadProgress(uploadId, percentage)
|
||||
},
|
||||
async () => {
|
||||
dialogueStore.batchDelDialogueRecord([uploadId])
|
||||
|
||||
dialogueStore.batchDelDialogueRecord([uploadId])
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -164,13 +166,12 @@ const onSendCodeEvent = ({ data, callBack }) => {
|
||||
|
||||
// 发送文件消息
|
||||
const onSendFileEvent = ({ data }) => {
|
||||
let maxsize = 200 * 1024 * 1024
|
||||
if (data.size > maxsize) {
|
||||
return window['$message'].warning('上传文件不能超过100M!')
|
||||
}
|
||||
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,
|
||||
@ -192,7 +193,7 @@ const onSendFileEvent = ({ data }) => {
|
||||
},
|
||||
float: 'right'
|
||||
}
|
||||
dialogueStore.addDialogueRecord(tempMessage)
|
||||
dialogueStore.addUploadTask(tempMessage)
|
||||
nextTick(()=>{
|
||||
scrollToBottom()
|
||||
})
|
||||
@ -201,8 +202,8 @@ const onSendFileEvent = ({ data }) => {
|
||||
dialogueStore.updateUploadProgress(clientUploadId, percentage)
|
||||
},
|
||||
async () => {
|
||||
dialogueStore.batchDelDialogueRecord([clientUploadId])
|
||||
|
||||
// 上传完成后,上传任务已经被removeUploadTask方法移除
|
||||
// 不需要再次从records中删除
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -46,9 +46,9 @@ export default defineConfig(({ mode }) => {
|
||||
vueJsx({}),
|
||||
compressPlugin(),
|
||||
UnoCSS(),
|
||||
// vueDevTools({
|
||||
// launchEditor: 'trae',
|
||||
// })
|
||||
vueDevTools({
|
||||
launchEditor: 'trae',
|
||||
})
|
||||
],
|
||||
define: {
|
||||
__APP_ENV__: env.APP_ENV
|
||||
|
Loading…
Reference in New Issue
Block a user