数据持久化

im
dylanmay 2024-11-12 09:12:10 +08:00
parent 5feb3e6815
commit e5b90372a6
16 changed files with 222 additions and 29 deletions

View File

@ -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",

72
src/store/indexedDB.ts Normal file
View File

@ -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)
}
}

View File

@ -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

View File

@ -6,7 +6,7 @@
*/
import request from '@/config/axios'
import { MessageModelType } from '../types'
import { MessageModelType } from '../types/types'
export interface SendMsg {
clientMessageId: string

View File

@ -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' })

View File

@ -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()

View File

@ -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' })

View File

@ -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 || ''

View File

@ -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' })

View File

@ -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' })

View File

@ -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' })

View File

@ -1,4 +1,4 @@
import { ConversationType, MessageModelType } from '../types'
import { ConversationType, MessageModelType } from '../types/types'
export default class BaseConversation {
public id: string

View File

@ -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

View File

@ -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 {

View File

@ -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
)
}
}

View File

@ -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 => imsession => 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
}