会话和消息处理

im
dylanmay 2024-10-26 19:45:08 +08:00
parent 90461a8cdf
commit f3968db2e0
14 changed files with 191 additions and 105 deletions

100
.vscode/settings.json vendored
View File

@ -1,5 +1,7 @@
{ {
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "./node_modules/typescript/lib",
"volar.tsPlugin": true,
"volar.tsPluginStatus": false,
"npm.packageManager": "pnpm", "npm.packageManager": "pnpm",
"editor.tabSize": 2, "editor.tabSize": 2,
"prettier.printWidth": 100, // "prettier.printWidth": 100, //
@ -62,7 +64,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"[typescript]": { "[typescript]": {
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"[typescriptreact]": { "[typescriptreact]": {
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint" "editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
@ -83,53 +85,74 @@
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit", "source.fixAll.eslint": "explicit"
"source.fixAll.stylelint": "explicit"
}, },
"[vue]": { "[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit"
}
}, },
"i18n-ally.localesPaths": ["src/locales"], "i18n-ally.localesPaths": ["src/locales"],
"i18n-ally.keystyle": "nested", "i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true, "i18n-ally.sortKeys": true,
"i18n-ally.namespace": false, "i18n-ally.namespace": true,
"i18n-ally.enabledParsers": ["ts"], "i18n-ally.enabledParsers": ["ts"],
"i18n-ally.sourceLanguage": "en", "i18n-ally.sourceLanguage": "en",
"i18n-ally.displayLanguage": "zh-CN", "i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.enabledFrameworks": ["vue", "react"], "i18n-ally.enabledFrameworks": ["vue", "react"],
"cSpell.words": [ "cSpell.words": [
"brotli",
"browserslist",
"codemirror",
"commitlint",
"cropperjs",
"echart",
"echarts",
"esnext",
"esno",
"iconify",
"INTLIFY",
"lintstagedrc",
"logicflow",
"nprogress",
"pinia",
"pnpm",
"qrcode",
"sider",
"sortablejs",
"stylelint",
"svgs",
"unocss",
"unplugin",
"unref",
"videojs",
"VITE",
"vitejs",
"vueuse",
"wangeditor",
"xingyu", "xingyu",
"yudao", "yudao",
"zxcvbn" "unocss",
"browserslist",
"esnext",
"unplugin",
"qrcode",
"sider",
"pinia",
"sider",
"nprogress",
"INTLIFY",
"stylelint",
"esno",
"vitejs",
"sortablejs",
"codemirror",
"iconify",
"commitlint",
"videojs",
"echarts",
"wangeditor",
"cropperjs",
"logicflow",
"vueuse",
"zxcvbn",
"lintstagedrc",
"brotli",
"sider",
"pnpm"
],
"vetur.format.scriptInitialIndent": true,
"vetur.format.styleInitialIndent": true,
"vetur.validation.script": false,
"MicroPython.executeButton": [
{
"text": "▶",
"tooltip": "运行",
"alignment": "left",
"command": "extension.executeFile",
"priority": 3.5
}
],
"MicroPython.syncButton": [
{
"text": "$(sync)",
"tooltip": "同步",
"alignment": "left",
"command": "extension.execute",
"priority": 4
}
], ],
// //
"explorer.fileNesting.enabled": true, "explorer.fileNesting.enabled": true,
@ -138,8 +161,7 @@
"*.ts": "$(capture).test.ts, $(capture).test.tsx", "*.ts": "$(capture).test.ts, $(capture).test.tsx",
"*.tsx": "$(capture).test.ts, $(capture).test.tsx", "*.tsx": "$(capture).test.ts, $(capture).test.tsx",
"*.env": "$(capture).env.*", "*.env": "$(capture).env.*",
"package.json": "pnpm-lock.yaml,yarn.lock,LICENSE,README*,CHANGELOG*,CNAME,.gitattributes,.eslintrc-auto-import.json,.gitignore,prettier.config.js,stylelint.config.js,commitlint.config.js,.stylelintignore,.prettierignore,.gitpod.yml,.eslintrc.js,.eslintignore" "package.json": "pnpm-lock.yaml,yarn.lock,LICENSE,README*,CHANGELOG*,CNAME,.gitattributes,.gitignore,prettier.config.js,stylelint.config.js,commitlint.config.js,.stylelintignore,.prettierignore,.gitpod.yml,.eslintrc.js,.eslintignore"
}, },
"terminal.integrated.scrollback": 10000, "terminal.integrated.scrollback": 10000
"nuxt.isNuxtApp": false
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<view class="flex h-full flex-1"> <view class="flex h-full flex-1">
<ToolSection @menuSelectChange="toolMenuSelectChange" /> <ToolSection @menu-select-change="toolMenuSelectChange" />
<Session v-if="bussinessType === MENU_LIST_ENUM.CONVERSATION" /> <Session v-if="bussinessType === MENU_LIST_ENUM.CONVERSATION" />
<Friends v-if="bussinessType === MENU_LIST_ENUM.FRIENDS" /> <Friends v-if="bussinessType === MENU_LIST_ENUM.FRIENDS" />
<view v-if="bussinessType === MENU_LIST_ENUM.CONVERSATION" class="flex w-full flex-col"> <view v-if="bussinessType === MENU_LIST_ENUM.CONVERSATION" class="flex w-full flex-col">
@ -22,7 +22,7 @@ import Session from '../components/Session/Index.vue'
import Friends from '../components/Friends/Index.vue' import Friends from '../components/Friends/Index.vue'
import ChatHeader from '../components/ChatHeader/Index.vue' import ChatHeader from '../components/ChatHeader/Index.vue'
import ChatMessage from '../components/ChatMessage/Index.vue' import ChatMessage from '../components/ChatMessage/Index.vue'
import InputSection from '../components/InputSection/index.vue' import InputSection from '../components/InputSection/Index.vue'
import FriendDetail from '../components/FriendDetail/Index.vue' import FriendDetail from '../components/FriendDetail/Index.vue'
import { MENU_LIST_ENUM } from '../types/index.d.ts' import { MENU_LIST_ENUM } from '../types/index.d.ts'

View File

@ -17,8 +17,7 @@ export interface SendMsg {
} }
export interface SessionMsgReq { export interface SessionMsgReq {
receiverId: number conversationNo: string
conversationType: number
sendTime: Date sendTime: Date
} }
@ -41,7 +40,7 @@ export default class MessageApi {
* @returns Promise<Array<MessageModelType>> * @returns Promise<Array<MessageModelType>>
*/ */
static getSessionMsg(params: SessionMsgReq): Promise<Array<MessageModelType>> { static getSessionMsg(params: SessionMsgReq): Promise<Array<MessageModelType>> {
return request.get({ url: '/im/message/list', params }) return request.get({ url: '/im/message/listByNo', params })
} }
/** /**

View File

@ -1,18 +1,12 @@
<template> <template>
<view <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 overflow-scroll" 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"
> ref="listBoxRef">
<template v-for="item in chatStore.currentSession?.msgList"> <template v-for="item in chatStore.currentSession?.msgList">
<TextMsg <TextMsg v-if="item.contentType === ContentType.TEXT" :key="item.clientMessageId" :message="item" class="py-1" />
v-if="item.contentType === ContentType.TEXT"
:key="item.clientMessageId"
:message="item"
/>
<ImageMsg <ImageMsg
v-if="item.contentType === ContentType.IMAGE" v-if="item.contentType === ContentType.IMAGE" :key="item.clientMessageId" :message="item"
:key="item.clientMessageId" class="py-1" />
:message="item"
/>
</template> </template>
</view> </view>
</template> </template>
@ -26,4 +20,31 @@ import { ContentType } from '../../types/index.d.ts'
defineOptions({ name: 'ChatMessage' }) defineOptions({ name: 'ChatMessage' })
const chatStore = useChatStore() const chatStore = useChatStore()
const listBoxRef = ref<HTMLElement | null>(null)
const msgListLength = computed(() => {
return chatStore.currentSession ? chatStore.currentSession.msgList.length : 0
})
const scrollToBottom = () => {
nextTick(() => {
if (listBoxRef.value) {
console.log("scrollToBottom");
listBoxRef.value.scrollTop = listBoxRef.value.scrollHeight;
}
});
};
watch(msgListLength, (newLength, oldLength) => {
if (newLength > oldLength) {
scrollToBottom()
}
})
watch(() => chatStore.currentSessionIndex, () => {
scrollToBottom()
})
</script> </script>

View File

@ -3,7 +3,7 @@
class="flex flex-col items-center w-full border-b-1 border-b-gray border-b-solid" class="flex flex-col items-center w-full border-b-1 border-b-gray border-b-solid"
style="height: 248px; min-height: 248px" style="height: 248px; min-height: 248px"
> >
<view class="flex p-2 w-full" style="height: 20px"> <view class="flex p-2 w-full" style="height: 32px">
<Icon icon="ep:apple" color="var(--top-header-text-color)" class="custom-hover" /> <Icon icon="ep:apple" color="var(--top-header-text-color)" class="custom-hover" />
<Icon icon="ep:folder" color="var(--top-header-text-color)" class="custom-hover" /> <Icon icon="ep:folder" color="var(--top-header-text-color)" class="custom-hover" />
<Icon icon="ep:chat-line-square" color="var(--top-header-text-color)" class="custom-hover" /> <Icon icon="ep:chat-line-square" color="var(--top-header-text-color)" class="custom-hover" />
@ -26,6 +26,7 @@ import TextMessage from '../../model/TextMessage'
import { useChatStoreWithOut } from '../../store/chatstore' import { useChatStoreWithOut } from '../../store/chatstore'
import { CONVERSATION_TYPE } from '../../types/index.d.ts' import { CONVERSATION_TYPE } from '../../types/index.d.ts'
import { SendStatus, MessageRole, ContentType } from '../../types/index.d.ts' import { SendStatus, MessageRole, ContentType } from '../../types/index.d.ts'
import { useUserStoreWithOut } from '../../../../store/modules/user';
defineOptions({ name: 'InputSection' }) defineOptions({ name: 'InputSection' })
@ -38,20 +39,23 @@ const onEnter = () => {
} }
const createTextMessage = (content: string): TextMessage => { const createTextMessage = (content: string): TextMessage => {
console.log('====>>>>', content)
const userStore = useUserStoreWithOut()
// account // account
const msg = new TextMessage( const msg = new TextMessage(
'', '',
'', userStore.user.avatar,
'', userStore.user.nickname,
new Date().getTime(), new Date().getTime(),
false, false,
content, content,
MessageRole.SELF, MessageRole.SELF,
SendStatus.SENDING, SendStatus.SENDING,
chatStore.currentSession?.id || '', chatStore.currentSession?.id || '',
chatStore.currentSession?.targetId, chatStore.currentSession ? chatStore.currentSession.targetId : 0,
chatStore.currentSession?.type || CONVERSATION_TYPE.SINGLE chatStore.currentSession?.type || CONVERSATION_TYPE.SINGLE,
chatStore.currentSession?.conversationNo || ''
) )
return msg return msg

View File

@ -4,7 +4,7 @@
:class="props.message.role === MessageRole.SELF ? 'flex-row-reverse' : 'flex-row'" :class="props.message.role === MessageRole.SELF ? 'flex-row-reverse' : 'flex-row'"
> >
<el-avatar shape="square" size="default" class="mx-2" :src="props.message.avatar" /> <el-avatar shape="square" size="default" class="mx-2" :src="props.message.avatar" />
<view class="flex flex-col"> <view class="flex flex-col" :class="props.message.role === MessageRole.SELF ? 'items-end' : 'items-start'">
<label class="text-xs text-gray-4 mb-1">{{ props.message.nickname }}</label> <label class="text-xs text-gray-4 mb-1">{{ props.message.nickname }}</label>
<view class="flex items-center"> <view class="flex items-center">
<el-icon v-if="props.message.sendStatus === SendStatus.SENDING" class="is-loading" <el-icon v-if="props.message.sendStatus === SendStatus.SENDING" class="is-loading"

View File

@ -1,5 +1,5 @@
<template> <template>
<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 items-center h-full py-2 b-1 b-gray b-solid" style="width: 258px">
<view class="flex flex-col w-full"> <view class="flex flex-col w-full">
<SessionItem <SessionItem
v-for="(item, index) in chatStore.sessionList" v-for="(item, index) in chatStore.sessionList"

View File

@ -1,40 +1,34 @@
<template> <template>
<view class="flex py-2 border-b-gray-3 border-b-solid items-center px-2" :class="bgColor()"> <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"> <el-avatar shape="square" size="default" class="mr-2" :src="props.conversation.avatar" />
{{ props.conversation.name || '' }}
</el-avatar>
<view class="flex flex-col flex-1 tems-end h-full"> <view class="flex flex-col flex-1 tems-end h-full">
<label <label class="text-black-c text-size-sm font-medium text-ellipsis text-nowrap" :class="namefontColor()">{{
class="text-black-c text-size-sm font-medium text-ellipsis text-nowrap" props.conversation.nickname || '' }}</label>
:class="namefontColor()" <label class="text-gray-f text-size-sm text-ellipsis text-nowrap mr-1" :class="timefontColor()">{{ lastMessage
>{{ props.conversation.name }}</label }}</label>
>
<label
class="text-gray-f text-size-sm text-ellipsis text-nowrap mr-1"
:class="timefontColor()"
>{{ props.conversation.description }}</label
>
</view> </view>
<view class="flex items-end h-full flex-col"> <view class="flex items-end h-full flex-col">
<label class="text-gray-f text-size-xs text-nowrap" :class="timefontColor()">{{ <label class="text-gray-f text-size-xs text-nowrap" :class="timefontColor()">{{
formatPast(new Date(props.conversation.updateTime), 'YYYY-MM-DD') formatPast(new Date(props.conversation.updateTime), 'YYYY/MM/DD')
}}</label> }}</label>
</view> </view>
</view> </view>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { PropType } from 'vue' import { PropType } from 'vue'
import { ConversationModelType } from '../../types' import { ContentType, ConversationModelType } from '../../types/index.d.ts'
import { formatPast } from '@/utils/formatTime' import { formatPast } from '@/utils/formatTime'
import { useChatStore } from '../../store/chatstore' import { useChatStore } from '../../store/chatstore'
import TextMessage from '../../model/TextMessage';
defineOptions({ name: 'SessionItem' }) defineOptions({ name: 'SessionItem' })
const props = defineProps({ const props = defineProps({
conversation: { conversation: {
type: Object as PropType<ConversationModelType>, type: Object as PropType<ConversationModelType>,
default: () => {} default: () => { }
}, },
index: Number index: Number
}) })
@ -51,6 +45,32 @@ const namefontColor = () => {
const timefontColor = () => { const timefontColor = () => {
return props.index === chatStore.currentSessionIndex ? 'text-white' : 'timeColor' return props.index === chatStore.currentSessionIndex ? 'text-white' : 'timeColor'
} }
/**
* TODO: 修改为后端计算否则在没有打开聊天窗口的时候没有数据
*/
const lastMessage = computed(() => {
if (props.conversation.msgList.length === 0) {
return props.conversation.lastMessageDescription
}
const lastIndex = props.conversation.msgList.length - 1
const lastMessage = props.conversation.msgList[lastIndex]
if (!lastMessage) {
return ''
}
if (lastMessage.contentType === ContentType.TEXT) {
return (lastMessage as TextMessage).content
} else if (lastMessage.contentType === ContentType.IMAGE) {
return '[图片]'
} else {
return '[其他]'
}
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -7,31 +7,39 @@ export default class BaseConversation {
public description: string public description: string
public createTime: number public createTime: number
public updateTime: number public updateTime: number
public unreadCount: number public unreadMessagesCount: number
public msgList: Array<MessageModelType> public msgList: Array<MessageModelType>
public type: ConversationType public type: ConversationType
public targetId: number public targetId: number
public senderId: number
public conversationNo: string
public lastMessageDescription: string
constructor( constructor(
id: string, id: string,
avatar: string, avatar: string,
name: string, name: string,
descrition: string, lastMessageDescription: string,
createTime: number, createTime: number,
updateTime: number, updateTime: number,
unreadCount: number, unreadMessagesCount: number,
msgList: Array<MessageModelType>, msgList: Array<MessageModelType>,
type: ConversationType, type: ConversationType,
targetId: number targetId: number,
senderId: number,
conversationNo: string
) { ) {
this.id = id this.id = id
this.avatar = avatar this.avatar = avatar
this.name = name this.name = name
this.description = descrition this.lastMessageDescription = lastMessageDescription
this.createTime = createTime this.createTime = createTime
this.updateTime = updateTime this.updateTime = updateTime
this.unreadCount = unreadCount this.unreadMessagesCount = unreadMessagesCount
this.msgList = msgList this.msgList = msgList
this.type = type this.type = type
this.targetId = targetId this.targetId = targetId
this.senderId = senderId
this.conversationNo = conversationNo
} }
} }

View File

@ -13,6 +13,7 @@ export default class BaseMessage {
clientMessageId: string clientMessageId: string
receiverId: number receiverId: number
conversationType: number conversationType: number
conversationNo: string
constructor( constructor(
id: string, id: string,
avatar: string, avatar: string,
@ -24,7 +25,8 @@ export default class BaseMessage {
contentType: ContentType, contentType: ContentType,
conversationId: string, conversationId: string,
receiverId: number, receiverId: number,
conversationType: number conversationType: number,
conversationNo: string
) { ) {
this.id = id this.id = id
this.avatar = avatar this.avatar = avatar
@ -38,6 +40,7 @@ export default class BaseMessage {
this.receiverId = receiverId this.receiverId = receiverId
this.clientMessageId = this.generateClientMessageId() this.clientMessageId = this.generateClientMessageId()
this.conversationType = conversationType this.conversationType = conversationType
this.conversationNo = conversationNo
} }
private generateClientMessageId() { private generateClientMessageId() {

View File

@ -6,25 +6,29 @@ export class ChatConversation extends BaseConversation {
id: string, id: string,
avatar: string, avatar: string,
name: string, name: string,
descrition: string, lastMessageDescription: string,
createTime: number, createTime: number,
updateTime: number, updateTime: number,
unreadCount: number, unreadMessagesCount: number,
msgList: Array<BaseMessage>, msgList: Array<BaseMessage>,
type: number, type: number,
targetId: number targetId: number,
senderId: number,
conversationNo: string
) { ) {
super( super(
id, id,
avatar, avatar,
name, name,
descrition, lastMessageDescription,
createTime, createTime,
updateTime, updateTime,
unreadCount, unreadMessagesCount,
msgList, msgList,
type, type,
targetId targetId,
senderId,
conversationNo
) )
} }
} }

View File

@ -15,7 +15,8 @@ export default class ImageMessage extends BaseMessage {
sendStatus: SendStatus, sendStatus: SendStatus,
conversationId: string, conversationId: string,
receiverId: number, receiverId: number,
conversationType: number conversationType: number,
conversationNo: string
) { ) {
super( super(
id, id,
@ -28,7 +29,8 @@ export default class ImageMessage extends BaseMessage {
ContentType.IMAGE, ContentType.IMAGE,
conversationId, conversationId,
receiverId, receiverId,
conversationType conversationType,
conversationNo
) )
this.content = content this.content = content
} }

View File

@ -15,7 +15,8 @@ export default class TextMessage extends BaseMessage {
sendStatus: SendStatus, sendStatus: SendStatus,
conversationId: string, conversationId: string,
receiverId: number, receiverId: number,
conversationType: number conversationType: number,
conversationNo: string
) { ) {
super( super(
id, id,
@ -28,7 +29,8 @@ export default class TextMessage extends BaseMessage {
ContentType.TEXT, ContentType.TEXT,
conversationId, conversationId,
receiverId, receiverId,
conversationType conversationType,
conversationNo
) )
this.content = content this.content = content
} }

View File

@ -11,12 +11,12 @@ interface ChatStoreModel {
sessionList: Array<ConversationModelType> sessionList: Array<ConversationModelType>
currentSession: ConversationModelType | null currentSession: ConversationModelType | null
currentSessionIndex: number currentSessionIndex: number
inputText: string inputText: string,
} }
export const useChatStore = defineStore('chatStore', { export const useChatStore = defineStore('chatStore', {
state: (): ChatStoreModel => ({ state: (): ChatStoreModel => ({
sessionList: reactive<Array<ConversationModelType>>([]), sessionList: [],
currentSession: null, currentSession: null,
currentSessionIndex: 0, currentSessionIndex: 0,
inputText: '' inputText: ''
@ -121,6 +121,10 @@ export const useChatStore = defineStore('chatStore', {
updateTime: item.lastReadTime, updateTime: item.lastReadTime,
name: item.targetId, name: item.targetId,
targetId: item.targetId, targetId: item.targetId,
senderId: item.userId,
conversationNo: item.no,
unreadMessagesCount: item.unreadMessagesCount,
description: item.lastMessageDescription,
msgList: [] msgList: []
})) }))
} catch (error) { } catch (error) {
@ -133,17 +137,14 @@ export const useChatStore = defineStore('chatStore', {
return return
} }
const receiverId = this.currentSession.targetId const userStore = useUserStoreWithOut()
const type = this.currentSession.type
try { try {
const res = await MessageApi.getSessionMsg({ const res = await MessageApi.getSessionMsg({
receiverId: receiverId, conversationNo: this.currentSession.conversationNo
conversationType: type, // sendTime: new Date().toISOString().slice(0, -1)
sendTime: new Date()
}) })
const userStore = useUserStoreWithOut()
this.currentSession.msgList = res.map((item) => { this.currentSession.msgList = res.map((item) => {
return { return {
@ -154,7 +155,7 @@ export const useChatStore = defineStore('chatStore', {
} }
}) })
} catch (error) { } catch (error) {
return error return error
} }
} }
} }