数据持久化
parent
5feb3e6815
commit
e5b90372a6
|
@ -50,6 +50,7 @@
|
|||
"element-plus": "2.8.4",
|
||||
"fast-xml-parser": "^4.3.2",
|
||||
"highlight.js": "^11.9.0",
|
||||
"idb": "^8.0.0",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"markdown-it": "^14.1.0",
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
import { ConversationModelType } from '@/views/chat/types/types'
|
||||
import { openDB, DBSchema, IDBPDatabase } from 'idb'
|
||||
|
||||
// Define your database schema
|
||||
interface MyDB extends DBSchema {
|
||||
Conversations: {
|
||||
key: string
|
||||
value: ConversationModelType
|
||||
}
|
||||
}
|
||||
|
||||
let dbPromise: Promise<IDBPDatabase<MyDB>>
|
||||
|
||||
export const initDB = () => {
|
||||
if (!dbPromise) {
|
||||
try {
|
||||
dbPromise = openDB<MyDB>('yudao-im-indexeddb', 1, {
|
||||
upgrade(db) {
|
||||
db.createObjectStore('Conversations', { keyPath: 'conversationNo' })
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
return dbPromise
|
||||
}
|
||||
|
||||
export const addConversation = async (conversation: ConversationModelType) => {
|
||||
|
||||
try {
|
||||
const db = await initDB()
|
||||
await db.put('Conversations', conversation)
|
||||
} catch (error) {
|
||||
console.error(conversation)
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const getConversation = async (conversationNo: string) => {
|
||||
|
||||
try {
|
||||
const db = await initDB()
|
||||
return await db.get('Conversations', conversationNo)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const deleteConversation = async (conversationNo: string) => {
|
||||
|
||||
try {
|
||||
const db = await initDB()
|
||||
await db.delete('Conversations', conversationNo)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const getAllConversations = async () => {
|
||||
|
||||
try {
|
||||
const db = await initDB()
|
||||
return await db.getAll('Conversations')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
}
|
|
@ -18,17 +18,22 @@
|
|||
*/
|
||||
|
||||
import ToolSection from '../components/ToolSection/Index.vue'
|
||||
import Session from '../components/Session/Index.vue'
|
||||
import Session from '../components/Conversation/index.vue'
|
||||
import Friends from '../components/Friends/Index.vue'
|
||||
import ChatHeader from '../components/ChatHeader/Index.vue' // TODO @dylan:为啥这个 index.vue 是大写哈?可以搞成小写哇?
|
||||
import ChatMessage from '../components/ChatMessage/Index.vue'
|
||||
import InputSection from '../components/InputSection/Index.vue'
|
||||
import ChatHeader from '../components/ChatHeader/index.vue'
|
||||
import ChatMessage from '../components/ChatMessage/index.vue'
|
||||
import InputSection from '../components/InputSection/index.vue'
|
||||
import FriendDetail from '../components/FriendDetail/Index.vue'
|
||||
import { MENU_LIST_ENUM } from '../types/index.d.ts'
|
||||
import { MENU_LIST_ENUM } from '../types/types'
|
||||
import { useWebSocketStore } from '../store/websocketStore'
|
||||
|
||||
defineOptions({ name: 'ChatPage' })
|
||||
|
||||
const bussinessType = ref(1)
|
||||
const webSocketStore = useWebSocketStore();
|
||||
onMounted(() => {
|
||||
webSocketStore.connect()
|
||||
})
|
||||
|
||||
const toolMenuSelectChange = (value) => {
|
||||
bussinessType.value = value
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import request from '@/config/axios'
|
||||
import { MessageModelType } from '../types'
|
||||
import { MessageModelType } from '../types/types'
|
||||
|
||||
export interface SendMsg {
|
||||
clientMessageId: string
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
import { useChatStore } from '../../store/chatstore'
|
||||
import TextMsg from '@/views/chat/components/Message/TextMsg.vue'
|
||||
import ImageMsg from '@/views/chat/components/Message/ImageMsg.vue'
|
||||
import { ContentType } from '../../types/index.d.ts'
|
||||
import { ContentType } from '../../types/types'
|
||||
|
||||
defineOptions({ name: 'ChatMessage' })
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<view class="flex flex-col w-full">
|
||||
<SessionItem
|
||||
v-for="(item, index) in chatStore.sessionList"
|
||||
:key="item.id"
|
||||
:key="item.id"
|
||||
:index="index"
|
||||
:conversation="item"
|
||||
@click="() => onSessionItemClick(index)"
|
||||
|
@ -13,17 +13,17 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import SessionItem from '../SessionItem/Index.vue'
|
||||
import SessionItem from '@/views/chat/components/ConversationItem/index.vue'
|
||||
import { useChatStoreWithOut } from '../../store/chatstore'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
defineOptions({ name: 'Session' })
|
||||
|
||||
const chatStore = useChatStoreWithOut()
|
||||
const { setCurrentConversation, setCurrentSessionIndex, getSession } = useChatStoreWithOut()
|
||||
const { setCurrentConversation, setCurrentSessionIndex, getConversationList } = useChatStoreWithOut()
|
||||
|
||||
onMounted(() => {
|
||||
getSession()
|
||||
getConversationList()
|
||||
// set default conversation
|
||||
nextTick(() => {
|
||||
setCurrentConversation()
|
|
@ -18,10 +18,11 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue'
|
||||
import { ContentType, ConversationModelType } from '../../types/index.d.ts'
|
||||
import { ContentType, ConversationModelType } from '../../types/types'
|
||||
|
||||
import { formatPast } from '@/utils/formatTime'
|
||||
import { useChatStore } from '../../store/chatstore'
|
||||
import TextMessage from '../../model/TextMessage';
|
||||
import { useChatStore } from '../../store/chatstore.js'
|
||||
import TextMessage from '../../model/TextMessage.js';
|
||||
|
||||
defineOptions({ name: 'SessionItem' })
|
||||
|
|
@ -24,18 +24,27 @@
|
|||
<script lang="ts" setup>
|
||||
import TextMessage from '../../model/TextMessage'
|
||||
import { useChatStoreWithOut } from '../../store/chatstore'
|
||||
import { CONVERSATION_TYPE } from '../../types/index.d.ts'
|
||||
import { SendStatus, MessageRole, ContentType } from '../../types/index.d.ts'
|
||||
import { CONVERSATION_TYPE } from '../../types/types'
|
||||
import { SendStatus, MessageRole, ContentType } from '../../types/types'
|
||||
import { useUserStoreWithOut } from '../../../../store/modules/user';
|
||||
import { ElNotification } from 'element-plus';
|
||||
|
||||
defineOptions({ name: 'InputSection' })
|
||||
|
||||
const chatStore = useChatStoreWithOut()
|
||||
const onEnter = () => {
|
||||
console.log('enter pressed')
|
||||
const msg = createTextMessage(chatStore.inputText)
|
||||
|
||||
if (!chatStore.inputText.trim()) {
|
||||
ElNotification({
|
||||
title: '温馨提示',
|
||||
message: '请输入内容',
|
||||
type: 'warning'
|
||||
})
|
||||
return
|
||||
}
|
||||
const msg = createTextMessage(chatStore.inputText.trim())
|
||||
chatStore.addMessageToCurrentSession(msg)
|
||||
chatStore.setInputText('')
|
||||
}
|
||||
|
||||
const createTextMessage = (content: string): TextMessage => {
|
||||
|
@ -53,6 +62,7 @@ const createTextMessage = (content: string): TextMessage => {
|
|||
MessageRole.SELF,
|
||||
SendStatus.SENDING,
|
||||
chatStore.currentSession?.id || '',
|
||||
userStore.user.id,
|
||||
chatStore.currentSession ? chatStore.currentSession.targetId : 0,
|
||||
chatStore.currentSession?.type || CONVERSATION_TYPE.SINGLE,
|
||||
chatStore.currentSession?.senderId || ''
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue'
|
||||
import { Loading } from '@element-plus/icons-vue'
|
||||
import { MessageModelType, MessageRole, SendStatus } from '../../types/index.d.ts'
|
||||
import { MessageModelType, MessageRole, SendStatus } from '../../types/types'
|
||||
|
||||
defineOptions({ name: 'BaseMessage' })
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
import { PropType } from 'vue'
|
||||
import { useChatStore } from '../../store/chatstore'
|
||||
import { onMounted } from 'vue'
|
||||
import { MessageModelType } from '../../types'
|
||||
import { MessageModelType } from '../../types/types'
|
||||
import BaseMesageLayout from './BaseMsg.vue'
|
||||
|
||||
defineOptions({ name: 'ImageMessage' })
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { MENU_LIST_ENUM } from '../../types/index.d.ts'
|
||||
import { MENU_LIST_ENUM } from '../../types/types'
|
||||
|
||||
defineOptions({ name: 'ToolSection' })
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ConversationType, MessageModelType } from '../types'
|
||||
import { ConversationType, MessageModelType } from '../types/types'
|
||||
|
||||
export default class BaseConversation {
|
||||
public id: string
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { MessageRole, ContentType, SendStatus } from '../types'
|
||||
import { MessageRole, ContentType, SendStatus } from '../types/types'
|
||||
|
||||
export default class BaseMessage {
|
||||
id?: string
|
||||
|
@ -11,6 +11,7 @@ export default class BaseMessage {
|
|||
contentType: ContentType
|
||||
conversationId: string
|
||||
clientMessageId: string
|
||||
senderId: number
|
||||
receiverId: number
|
||||
conversationType: number
|
||||
conversationUserId: number
|
||||
|
@ -24,6 +25,7 @@ export default class BaseMessage {
|
|||
sendStauts: SendStatus,
|
||||
contentType: ContentType,
|
||||
conversationId: string,
|
||||
senderId: number,
|
||||
receiverId: number,
|
||||
conversationType: number,
|
||||
conversationUserId: number
|
||||
|
@ -37,6 +39,7 @@ export default class BaseMessage {
|
|||
this.sendStatus = sendStauts
|
||||
this.contentType = contentType
|
||||
this.conversationId = conversationId
|
||||
this.senderId = senderId
|
||||
this.receiverId = receiverId
|
||||
this.clientMessageId = this.generateClientMessageId()
|
||||
this.conversationType = conversationType
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { MessageRole, ContentType, SendStatus } from '@/views/chat/types/index.d.ts'
|
||||
import { MessageRole, ContentType, SendStatus } from '@/views/chat/types/types'
|
||||
import BaseMessage from './BaseMessage'
|
||||
|
||||
export default class ImageMessage extends BaseMessage {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { MessageRole, ContentType, SendStatus } from '@/views/chat/types/index.d.ts'
|
||||
import { MessageRole, ContentType, SendStatus, ImMessageContent } from '@/views/chat/types/types'
|
||||
import BaseMessage from './BaseMessage'
|
||||
|
||||
export default class TextMessage extends BaseMessage {
|
||||
|
@ -14,6 +14,7 @@ export default class TextMessage extends BaseMessage {
|
|||
role: MessageRole,
|
||||
sendStatus: SendStatus,
|
||||
conversationId: string,
|
||||
senderId: number,
|
||||
receiverId: number,
|
||||
conversationType: number,
|
||||
conversationUserId: number
|
||||
|
@ -28,10 +29,34 @@ export default class TextMessage extends BaseMessage {
|
|||
sendStatus,
|
||||
ContentType.TEXT,
|
||||
conversationId,
|
||||
senderId,
|
||||
receiverId,
|
||||
conversationType,
|
||||
conversationUserId
|
||||
)
|
||||
this.content = content
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息转换
|
||||
* @param websocketMessage
|
||||
* @returns
|
||||
*/
|
||||
static fromWebsocket(websocketMessage: ImMessageContent): TextMessage {
|
||||
return new TextMessage(
|
||||
websocketMessage.id.toString(), // 服务端也应该返回一个clientMessageId
|
||||
websocketMessage.senderAvatar,
|
||||
websocketMessage.senderNickname,
|
||||
new Date().getTime(), // TODO: 是否合理
|
||||
false,
|
||||
websocketMessage.content,
|
||||
MessageRole.OTHER, // 可以去掉在使用的时候依据逻辑判断
|
||||
SendStatus.SUCCESS,
|
||||
'', // TODO: [dylan]
|
||||
websocketMessage.senderId,
|
||||
websocketMessage.receiverId,
|
||||
websocketMessage.conversationType,
|
||||
0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,11 +2,14 @@ import { store } from '@/store/index'
|
|||
import { defineStore } from 'pinia'
|
||||
import BaseConversation from '../model/BaseConversation'
|
||||
import BaseMessage from '../model/BaseMessage'
|
||||
import { ConversationModelType, MessageRole, ContentType, SendStatus } from '../types/index.d.ts'
|
||||
import { ConversationModelType, MessageRole, ContentType, SendStatus } from '../types/types'
|
||||
import SessionApi from '../api/sessionApi'
|
||||
import MessageApi, { SendMsg } from '../api/messageApi'
|
||||
import { useUserStoreWithOut } from '@/store/modules/user'
|
||||
import { useUserStore, useUserStoreWithOut } from '@/store/modules/user'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import { addConversation, getAllConversations } from '@/store/indexedDB'
|
||||
import { ChatConversation } from '../model/ChatConversation'
|
||||
import { generateConversationNo } from './websocketStore'
|
||||
|
||||
// TODO @dylan:是不是 chat => im;session => conversation;这样统一一点哈。
|
||||
interface ChatStoreModel {
|
||||
|
@ -81,6 +84,8 @@ export const useChatStore = defineStore('chatStore', {
|
|||
}
|
||||
|
||||
this.updateMsgToCurrentSession(updateMsg)
|
||||
} finally {
|
||||
this.setInputText('')
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -99,6 +104,42 @@ export const useChatStore = defineStore('chatStore', {
|
|||
this.sessionList.splice(conversationIndex, 1, msgConversation)
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加消息到会话
|
||||
* @param message
|
||||
*/
|
||||
addMessageToConversation<T extends BaseMessage>(message: T): void {
|
||||
// 无论是converstionNo1还是converstionNo2都都需要试一下
|
||||
const converstionNo1 = generateConversationNo(
|
||||
message.senderId,
|
||||
message.receiverId,
|
||||
message.conversationType
|
||||
)
|
||||
const converstionNo2 = generateConversationNo(
|
||||
message.receiverId,
|
||||
message.senderId,
|
||||
message.conversationType
|
||||
)
|
||||
|
||||
const conversationIndex = this.sessionList.findIndex(
|
||||
(item) => item.conversationNo === converstionNo1 || item.conversationNo === converstionNo2
|
||||
)
|
||||
|
||||
if (conversationIndex < 0) {
|
||||
console.log('conversation not exist')
|
||||
return
|
||||
}
|
||||
|
||||
const msgConversation = this.sessionList[conversationIndex]
|
||||
msgConversation.msgList.push(message)
|
||||
|
||||
// replace the old Conversation
|
||||
this.sessionList.splice(conversationIndex, 1, msgConversation)
|
||||
|
||||
// 更新消息到indexeddb
|
||||
addConversation(toRaw(msgConversation) as ChatConversation )
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新消息到当前会话
|
||||
* @param updatedMsg
|
||||
|
@ -112,13 +153,40 @@ export const useChatStore = defineStore('chatStore', {
|
|||
return item
|
||||
}
|
||||
})
|
||||
|
||||
const rawCurrentSesstion = toRaw(this.currentSession)
|
||||
rawCurrentSesstion.msgList =this.currentSession.msgList.map(item => toRaw(item))
|
||||
console.log("raw", rawCurrentSesstion)
|
||||
addConversation(rawCurrentSesstion as ChatConversation)
|
||||
}
|
||||
},
|
||||
|
||||
async getSession() {
|
||||
async getConversationList() {
|
||||
try {
|
||||
// 从数据库获取数据
|
||||
const _conversationList = await getAllConversations()
|
||||
|
||||
if (_conversationList) {
|
||||
// 加载到内存
|
||||
// TODO:[dylan]处理排序
|
||||
this.sessionList = _conversationList
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
} finally{
|
||||
|
||||
// 本地没有数据的时候才请求接口
|
||||
if (this.sessionList.length === 0) {
|
||||
this.getSessionFromServer()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async getSessionFromServer() {
|
||||
try {
|
||||
const res = await SessionApi.getSessionList()
|
||||
this.sessionList = res.map((item) => ({
|
||||
|
||||
this.sessionList = res.map((item) => ({
|
||||
...item,
|
||||
updateTime: item.lastReadTime,
|
||||
name: item.targetId,
|
||||
|
@ -129,6 +197,12 @@ export const useChatStore = defineStore('chatStore', {
|
|||
description: item.lastMessageDescription,
|
||||
msgList: []
|
||||
}))
|
||||
|
||||
// 同步到数据库
|
||||
this.sessionList.forEach((item) => {
|
||||
console.log(item)
|
||||
addConversation(toRaw(item) as ChatConversation)
|
||||
})
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
|
@ -157,6 +231,8 @@ export const useChatStore = defineStore('chatStore', {
|
|||
avatar: item.senderAvatar
|
||||
}
|
||||
})
|
||||
|
||||
addConversation(toRaw(this.currentSession) as ChatConversation)
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue