单聊对接

im
dylanmay 2024-10-19 16:06:29 +08:00
parent aaba03d001
commit 90619542c8
19 changed files with 354 additions and 137 deletions

View File

@ -61,7 +61,7 @@ export function getWeek(dateTime: Date): number {
* @description param 3 60 * 60* 24 * 1000 * 3
* @returns
*/
export function formatPast(param: string | Date, format = 'YYYY-mm-dd HH:MM:SS'): string {
export function formatPast(param: string | Date, format = 'YYYY-MM-DD HH:mm:ss'): string {
// 传入格式处理、存储转换值
let t: any, s: number
// 获取js 时间戳

View File

@ -0,0 +1,58 @@
/*
* @Author: dylan.may@qq.com
* @Date: 2024-10-16 11:30:31
* @Last Modified by: dylan.may@qq.com
* @Last Modified time: 2024-10-16 16:01:25
*/
import request from '@/config/axios'
import { MessageModelType } from '../types'
export interface SendMsg {
clientMessageId: string
receiverId: number
conversationType: number
contentType: number
content: string
}
export interface SessionMsgReq {
receiverId: number
conversationType: number
sendTime: Date
}
/**
*
*/
export default class MessageApi {
/**
*
* @param data SendMsg
* @returns Promise<{ id: number; sendTime: number }>
*/
static send(data: SendMsg): Promise<{ id: number; sendTime: number }> {
return request.post({ url: '/im/message/send', data })
}
/**
*
* @param data SessionMsgReq
* @returns Promise<Array<MessageModelType>>
*/
static getSessionMsg(params: SessionMsgReq): Promise<Array<MessageModelType>> {
return request.get({ url: '/im/message/list', params })
}
/**
*
* @param data { sequence: number; size: number }
* @returns Promise<Array<MessageModelType>>
*/
static getMessageForAllSession(params: {
sequence: number
size: number
}): Promise<Array<MessageModelType>> {
return request.get({ url: '/im/message/pull', params })
}
}

View File

@ -0,0 +1,22 @@
/*
* @Author: dylan.may@qq.com
* @Date: 2024-10-16 11:30:31
* @Last Modified by: dylan.may@qq.com
* @Last Modified time: 2024-10-16 16:01:25
*/
import request from '@/config/axios'
import { ChatConversation } from '../model/ChatConversation'
/**
*
*/
export default class SessionApi {
/**
*
* @returns Promise<Array<ChatConversation>>
*/
static getSessionList(): Promise<Array<ChatConversation>> {
return request.get({ url: '/im/conversation/list' })
}
}

View File

@ -1,7 +1,7 @@
<template>
<view
class="flex items-center w-full border-b-1 border-b-gray border-b-solid"
style="height: 60px"
style="height: 60px; min-height: 60px"
>
<label class="text-black text-size-xl font-medium mx-4">{{
chatStore.currentSession?.name

View File

@ -1,19 +1,27 @@
<template>
<view
class="flex flex-col items-start w-full border-b-1 border-b-gray border-b-solid flex-1 border-b-1 border-b-gray border-b-solid py-2"
class="flex flex-col items-start w-full border-b-1 border-b-gray border-b-solid flex-1 border-b-1 border-b-gray border-b-solid py-2 overflow-scroll"
>
<template v-for="item in chatStore.currentSession?.msgList">
<TextMessage v-if="item.messageType === MessageType.TEXT" :key="item.id" :message="item" />
<ImageMessage v-if="item.messageType === MessageType.IMAGE" :key="item.id" :message="item" />
<TextMsg
v-if="item.contentType === ContentType.TEXT"
:key="item.clientMessageId"
:message="item"
/>
<ImageMsg
v-if="item.contentType === ContentType.IMAGE"
:key="item.clientMessageId"
:message="item"
/>
</template>
</view>
</template>
<script lang="ts" setup>
import { useChatStore } from '../../store/chatstore'
import TextMessage from '@/views/chat/components/Message/TextMessage.vue'
import ImageMessage from '@/views/chat/components/Message/ImageMessage.vue'
import { MessageType } from '../../types/index.d.ts'
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'
defineOptions({ name: 'ChatMessage' })

View File

@ -1,7 +1,7 @@
<template>
<view
class="flex flex-col items-center w-full border-b-1 border-b-gray border-b-solid"
style="height: 248px"
style="height: 248px; min-height: 248px"
>
<view class="flex p-2 w-full" style="height: 20px">
<Icon icon="ep:apple" color="var(--top-header-text-color)" class="custom-hover" />
@ -23,12 +23,13 @@
<script lang="ts" setup>
import TextMessage from '../../model/TextMessage'
import { useChatStore } from '../../store/chatstore'
import { SendStatus, MessageRole, MessageType } from '../../types/index.d.ts'
import { useChatStoreWithOut } from '../../store/chatstore'
import { CONVERSATION_TYPE } from '../../types/index.d.ts'
import { SendStatus, MessageRole, ContentType } from '../../types/index.d.ts'
defineOptions({ name: 'InputSection' })
const chatStore = useChatStore()
const chatStore = useChatStoreWithOut()
const onEnter = () => {
console.log('enter pressed')
const msg = createTextMessage(chatStore.inputText)
@ -38,19 +39,19 @@ const onEnter = () => {
const createTextMessage = (content: string): TextMessage => {
console.log('====>>>>', content)
const _localId = `${new Date().getTime()}`
// account
const msg = new TextMessage(
_localId,
'https://img0.baidu.com/it/u=1121635512,1294972039&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=889',
'Dylan May',
'',
'',
'',
new Date().getTime(),
false,
content,
MessageRole.SELF,
SendStatus.SUCCESS,
MessageType.TEXT,
chatStore.currentSession?.id || ''
SendStatus.SENDING,
chatStore.currentSession?.id || '',
chatStore.currentSession?.targetId,
chatStore.currentSession?.type || CONVERSATION_TYPE.SINGLE
)
return msg

View File

@ -6,14 +6,20 @@
<el-avatar shape="square" size="default" class="mx-2" :src="props.message.avatar" />
<view class="flex flex-col">
<label class="text-xs text-gray-4 mb-1">{{ props.message.nickname }}</label>
<slot name="content"></slot>
<view class="flex items-center">
<el-icon v-if="props.message.sendStatus === SendStatus.SENDING" class="is-loading"
><Loading
/></el-icon>
<slot name="content"></slot>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { PropType } from 'vue'
import { MessageModelType, MessageRole } from '../../types/index.d.ts'
import { Loading } from '@element-plus/icons-vue'
import { MessageModelType, MessageRole, SendStatus } from '../../types/index.d.ts'
defineOptions({ name: 'BaseMessage' })

View File

@ -13,7 +13,7 @@ import { PropType } from 'vue'
import { useChatStore } from '../../store/chatstore'
import { onMounted } from 'vue'
import { MessageModelType } from '../../types'
import BaseMesageLayout from '../Message/BaseMessage.vue'
import BaseMesageLayout from './BaseMsg.vue'
defineOptions({ name: 'ImageMessage' })

View File

@ -10,18 +10,15 @@
<script lang="ts" setup>
import { PropType } from 'vue'
import { useChatStore } from '../../store/chatstore'
import BaseMesageLayout from '../Message/BaseMessage.vue'
import { MessageModelType } from '../../types/index'
import BaseMesageLayout from './BaseMsg.vue'
import TextMessage from '../../model/TextMessage'
defineOptions({ name: 'TextMessage' })
const props = defineProps({
message: {
type: Object as PropType<MessageModelType>,
type: Object as PropType<TextMessage>,
default: () => {}
}
})
const { sessionList, setCurrentConversation, setCurrentSessionIndex } = useChatStore()
</script>

View File

@ -2,7 +2,7 @@
<view class="flex flex-col items-center h-full py-2 b-1 b-gray b-solid" style="width: 248px">
<view class="flex flex-col w-full">
<SessionItem
v-for="(item, index) in sessionList"
v-for="(item, index) in chatStore.sessionList"
:key="item.id"
:index="index"
:conversation="item"
@ -14,16 +14,20 @@
<script lang="ts" setup>
import SessionItem from '../SessionItem/Index.vue'
import { useChatStore } from '../../store/chatstore'
import { useChatStoreWithOut } from '../../store/chatstore'
import { onMounted } from 'vue'
defineOptions({ name: 'Session' })
const { sessionList, setCurrentConversation, setCurrentSessionIndex } = useChatStore()
const chatStore = useChatStoreWithOut()
const { setCurrentConversation, setCurrentSessionIndex, getSession } = useChatStoreWithOut()
onMounted(() => {
getSession()
// set default conversation
setCurrentConversation()
nextTick(() => {
setCurrentConversation()
})
})
const onSessionItemClick = (index: number) => {

View File

@ -1,19 +1,23 @@
<template>
<view class="flex py-2 border-b-gray-3 border-b-solid items-center px-2" :class="bgColor()">
<el-avatar shape="square" size="default" class="mr-2" />
<view class="flex flex-col flex-1">
<el-avatar shape="square" size="default" class="mr-2">
{{ props.conversation.name || '' }}
</el-avatar>
<view class="flex flex-col flex-1 tems-end h-full">
<label
class="text-black-c text-size-sm font-medium text-ellipsis text-nowrap"
:class="fontColor()"
:class="namefontColor()"
>{{ props.conversation.name }}</label
>
<label class="text-gray-f text-size-sm text-ellipsis text-nowrap mr-1" :class="fontColor()">{{
props.conversation.description
}}</label>
<label
class="text-gray-f text-size-sm text-ellipsis text-nowrap mr-1"
:class="timefontColor()"
>{{ props.conversation.description }}</label
>
</view>
<view class="flex items-end h-full">
<label class="text-gray-f text-size-sm text-nowrap" :class="fontColor()">{{
formatPast(new Date(props.conversation.updateTime))
<view class="flex items-end h-full flex-col">
<label class="text-gray-f text-size-xs text-nowrap" :class="timefontColor()">{{
formatPast(new Date(props.conversation.updateTime), 'YYYY-MM-DD')
}}</label>
</view>
</view>
@ -41,7 +45,20 @@ const bgColor = () => {
return props.index === chatStore.currentSessionIndex ? 'bg-blue' : 'bg-white'
}
const fontColor = () => {
return props.index === chatStore.currentSessionIndex ? 'text-white' : 'text-gray-f'
const namefontColor = () => {
return props.index === chatStore.currentSessionIndex ? 'text-white' : 'nameColor'
}
const timefontColor = () => {
return props.index === chatStore.currentSessionIndex ? 'text-white' : 'timeColor'
}
</script>
<style lang="scss" scoped>
.timeColor {
color: #999;
}
.nameColor {
color: black;
}
</style>

View File

@ -1,4 +1,4 @@
import { MessageModelType } from '../types'
import { ConversationType, MessageModelType } from '../types'
export default class BaseConversation {
public id: string
@ -9,7 +9,8 @@ export default class BaseConversation {
public updateTime: number
public unreadCount: number
public msgList: Array<MessageModelType>
public type: ConversationType
public targetId: number
constructor(
id: string,
avatar: string,
@ -18,7 +19,9 @@ export default class BaseConversation {
createTime: number,
updateTime: number,
unreadCount: number,
msgList: Array<MessageModelType>
msgList: Array<MessageModelType>,
type: ConversationType,
targetId: number
) {
this.id = id
this.avatar = avatar
@ -28,5 +31,7 @@ export default class BaseConversation {
this.updateTime = updateTime
this.unreadCount = unreadCount
this.msgList = msgList
this.type = type
this.targetId = targetId
}
}

View File

@ -1,16 +1,18 @@
import { MessageRole, MessageType, SendStatus } from '../types'
import { MessageRole, ContentType, SendStatus } from '../types'
export default class BaseMessage {
id: string
avatar: string
nickname: string
id?: string
avatar?: string
nickname?: string
createTime: number
isRead: boolean
role: MessageRole
sendStatus: SendStatus
messageType: MessageType
contentType: ContentType
conversationId: string
clientMessageId: string
receiverId: number
conversationType: number
constructor(
id: string,
avatar: string,
@ -19,8 +21,10 @@ export default class BaseMessage {
isRead: boolean,
role: MessageRole,
sendStauts: SendStatus,
messageType: MessageType,
conversationId: string
contentType: ContentType,
conversationId: string,
receiverId: number,
conversationType: number
) {
this.id = id
this.avatar = avatar
@ -29,7 +33,21 @@ export default class BaseMessage {
this.isRead = isRead
this.role = role
this.sendStatus = sendStauts
this.messageType = messageType
this.contentType = contentType
this.conversationId = conversationId
this.receiverId = receiverId
this.clientMessageId = this.generateClientMessageId()
this.conversationType = conversationType
}
private generateClientMessageId() {
const timestamp = Date.now().toString() // 获取当前时间戳
const randomPart = 'xxxx-xxxx-4xxx-yxxx-xxxx'.replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0,
v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
return `${timestamp}-${randomPart}`
}
}

View File

@ -0,0 +1,5 @@
export interface BaseResponse<T> {
code: number // 0表示成功其他表示失败
message: string // 返回的信息,可以是成功或错误信息
data: T // 泛型数据,成功时返回数据,失败时为 null
}

View File

@ -10,8 +10,21 @@ export class ChatConversation extends BaseConversation {
createTime: number,
updateTime: number,
unreadCount: number,
msgList: Array<BaseMessage>
msgList: Array<BaseMessage>,
type: number,
targetId: number
) {
super(id, avatar, name, descrition, createTime, updateTime, unreadCount, msgList)
super(
id,
avatar,
name,
descrition,
createTime,
updateTime,
unreadCount,
msgList,
type,
targetId
)
}
}

View File

@ -1,4 +1,4 @@
import { MessageRole, MessageType, SendStatus } from '../types'
import { MessageRole, ContentType, SendStatus } from '@/views/chat/types/index.d.ts'
import BaseMessage from './BaseMessage'
export default class ImageMessage extends BaseMessage {
@ -13,10 +13,23 @@ export default class ImageMessage extends BaseMessage {
content: string,
role: MessageRole,
sendStatus: SendStatus,
messageType: MessageType,
conversationId: string
conversationId: string,
receiverId: number,
conversationType: number
) {
super(id, avatar, nickname, createTime, isRead, role, sendStatus, messageType, conversationId)
super(
id,
avatar,
nickname,
createTime,
isRead,
role,
sendStatus,
ContentType.IMAGE,
conversationId,
receiverId,
conversationType
)
this.content = content
}
}

View File

@ -1,4 +1,4 @@
import { MessageRole, MessageType, SendStatus } from '../types'
import { MessageRole, ContentType, SendStatus } from '@/views/chat/types/index.d.ts'
import BaseMessage from './BaseMessage'
export default class TextMessage extends BaseMessage {
@ -13,10 +13,23 @@ export default class TextMessage extends BaseMessage {
content: string,
role: MessageRole,
sendStatus: SendStatus,
messageType: MessageType,
conversationId: string
conversationId: string,
receiverId: number,
conversationType: number
) {
super(id, avatar, nickname, createTime, isRead, role, sendStatus, messageType, conversationId)
super(
id,
avatar,
nickname,
createTime,
isRead,
role,
sendStatus,
ContentType.TEXT,
conversationId,
receiverId,
conversationType
)
this.content = content
}
}

View File

@ -1,7 +1,11 @@
import { store } from '@/store/index'
import { defineStore } from 'pinia'
import BaseConversation from '../model/BaseConversation'
import BaseMessage from '../model/BaseMessage'
import { ConversationModelType, MessageRole, MessageType, SendStatus } from '../types/index.d.ts'
import { ConversationModelType, MessageRole, ContentType, SendStatus } from '../types/index.d.ts'
import SessionApi from '../api/sessionApi'
import MessageApi, { SendMsg } from '../api/messageApi'
import { useUserStoreWithOut } from '@/store/modules/user'
interface ChatStoreModel {
sessionList: Array<ConversationModelType>
@ -12,71 +16,7 @@ interface ChatStoreModel {
export const useChatStore = defineStore('chatStore', {
state: (): ChatStoreModel => ({
sessionList: [
{
id: '11111',
name: '张三',
avatar:
'https://img.zcool.cn/community/019fb65925bc32a801216a3ef77f7b.png@1280w_1l_2o_100sh.png',
description: 'sss',
createTime: 1693970987760,
updateTime: 1693970987760,
unreadCount: 1,
msgList: [
{
avatar:
'https://img0.baidu.com/it/u=1121635512,1294972039&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=889',
nickname: 'Dylan May',
id: '222221111',
isRead: false,
messageType: MessageType.TEXT,
sendStatus: SendStatus.SUCCESS,
role: MessageRole.SELF,
createTime: 1693970987760,
conversationId: '11111',
content: 'hello MUSK'
},
{
avatar:
'https://img0.baidu.com/it/u=4211304696,1059959254&fm=253&fmt=auto&app=120&f=JPEG?w=800&h=1174',
nickname: 'Elon Musk',
id: '2222222222',
isRead: false,
messageType: MessageType.TEXT,
sendStatus: SendStatus.SUCCESS,
role: MessageRole.OTHER,
createTime: 1693970987760,
conversationId: '11111',
content: 'hello DYLAN'
}
]
},
{
id: '22222',
name: '搞笑的一家人',
avatar:
'https://img.zcool.cn/community/019fb65925bc32a801216a3ef77f7b.png@1280w_1l_2o_100sh.png',
description: '今天晚上吃啥',
createTime: 1693970987760,
updateTime: 1693970987760,
unreadCount: 1,
msgList: [
{
avatar:
'https://img.zcool.cn/community/019fb65925bc32a801216a3ef77f7b.png@1280w_1l_2o_100sh.png',
nickname: '小艳',
id: '22222',
isRead: false,
messageType: MessageType.TEXT,
sendStatus: SendStatus.SUCCESS,
role: MessageRole.OTHER,
createTime: 1693970987760,
conversationId: '22222',
content: 'what your name'
}
]
}
],
sessionList: reactive<Array<ConversationModelType>>([]),
currentSession: null,
currentSessionIndex: 0,
inputText: ''
@ -103,6 +43,7 @@ export const useChatStore = defineStore('chatStore', {
setCurrentConversation() {
this.currentSession = this.sessionList[this.currentSessionIndex]
this.fetchSessionMsg()
},
setCurrentSessionIndex(index: number) {
@ -113,8 +54,32 @@ export const useChatStore = defineStore('chatStore', {
this.inputText = content
},
addMessageToCurrentSession<T extends BaseMessage>(message: T): void {
async addMessageToCurrentSession<T extends BaseMessage>(message: T): Promise<void> {
this.currentSession?.msgList.push(message)
try {
const res = await MessageApi.send(message as unknown as SendMsg)
console.log(res)
if (res.id) {
// 更新发送状态
const updateMsg = {
...message,
id: res.id,
sendTime: res.sendTime,
sendStatus: SendStatus.SUCCESS
}
this.updateMsgToCurrentSession(updateMsg)
}
} catch (error) {
console.log(error)
const updateMsg = {
...message,
sendStatus: SendStatus.SUCCESS
}
this.updateMsgToCurrentSession(updateMsg)
}
},
addMessageToSesstion<T extends BaseMessage>(message: T): void {
@ -130,6 +95,71 @@ export const useChatStore = defineStore('chatStore', {
// replace the old Conversation
this.sessionList.splice(conversationIndex, 1, msgConversation)
},
/**
*
* @param updatedMsg
*/
updateMsgToCurrentSession<T extends BaseMessage>(updatedMsg: T): void {
if (this.currentSession) {
this.currentSession.msgList = this.currentSession?.msgList.map((item) => {
if (item.clientMessageId === updatedMsg.clientMessageId) {
return updatedMsg
} else {
return item
}
})
}
},
async getSession() {
try {
const res = await SessionApi.getSessionList()
this.sessionList = res.map((item) => ({
...item,
updateTime: item.lastReadTime,
name: item.targetId,
targetId: item.targetId,
msgList: []
}))
} catch (error) {
return error
}
},
async fetchSessionMsg() {
if (!this.currentSession) {
return
}
const receiverId = this.currentSession.targetId
const type = this.currentSession.type
try {
const res = await MessageApi.getSessionMsg({
receiverId: receiverId,
conversationType: type,
sendTime: new Date()
})
const userStore = useUserStoreWithOut()
this.currentSession.msgList = res.map((item) => {
return {
...item,
role: item.senderId === userStore.user.id ? MessageRole.SELF : MessageRole.OTHER,
nickname: item.senderNickname,
avatar: item.senderAvatar
}
})
} catch (error) {
return error
}
}
}
})
export const useChatStoreWithOut = () => {
return useChatStore(store)
}

View File

@ -16,11 +16,11 @@ export enum SendStatus {
SUCCESS = 3
}
export enum MessageType {
TEXT = 1,
IMAGE = 2,
AUDIO = 3,
SYSTEM = 4
export enum ContentType {
TEXT = 101,
IMAGE = 102,
AUDIO = 103,
SYSTEM = 1400
}
export const enum MENU_LIST_ENUM {
@ -28,5 +28,12 @@ export const enum MENU_LIST_ENUM {
FRIENDS = 2
}
export const enum CONVERSATION_TYPE {
SINGLE = 1,
GROUP = 3,
NOTIFICATION = 4
}
export type MessageModelType = BaseMessage | TextMessage | ImageMessage
export type ConversationModelType = BaseConversation | ChatConversation
export type ConversationType = CONVERSATION_TYPE