✨ feat(im): 重构部分老代码。
parent
a28694074e
commit
505b3b5953
|
|
@ -51,7 +51,6 @@
|
||||||
"element-plus": "2.11.1",
|
"element-plus": "2.11.1",
|
||||||
"fast-xml-parser": "^4.3.2",
|
"fast-xml-parser": "^4.3.2",
|
||||||
"highlight.js": "^11.9.0",
|
"highlight.js": "^11.9.0",
|
||||||
"idb": "^8.0.0",
|
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"jsoneditor": "^10.1.3",
|
"jsoneditor": "^10.1.3",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
|
|
|
||||||
|
|
@ -86,9 +86,6 @@ importers:
|
||||||
highlight.js:
|
highlight.js:
|
||||||
specifier: ^11.9.0
|
specifier: ^11.9.0
|
||||||
version: 11.10.0
|
version: 11.10.0
|
||||||
idb:
|
|
||||||
specifier: ^8.0.0
|
|
||||||
version: 8.0.3
|
|
||||||
jsencrypt:
|
jsencrypt:
|
||||||
specifier: ^3.3.2
|
specifier: ^3.3.2
|
||||||
version: 3.3.2
|
version: 3.3.2
|
||||||
|
|
@ -3504,9 +3501,6 @@ packages:
|
||||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
idb@8.0.3:
|
|
||||||
resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==, tarball: https://registry.npmmirror.com/idb/-/idb-8.0.3.tgz}
|
|
||||||
|
|
||||||
ids@1.0.5:
|
ids@1.0.5:
|
||||||
resolution: {integrity: sha512-XQ0yom/4KWTL29sLG+tyuycy7UmeaM/79GRtSJq6IG9cJGIPeBz5kwDCguie3TwxaMNIc3WtPi0cTa1XYHicpw==}
|
resolution: {integrity: sha512-XQ0yom/4KWTL29sLG+tyuycy7UmeaM/79GRtSJq6IG9cJGIPeBz5kwDCguie3TwxaMNIc3WtPi0cTa1XYHicpw==}
|
||||||
|
|
||||||
|
|
@ -8903,8 +8897,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
|
|
||||||
idb@8.0.3: {}
|
|
||||||
|
|
||||||
ids@1.0.5: {}
|
ids@1.0.5: {}
|
||||||
|
|
||||||
ignore@5.3.2: {}
|
ignore@5.3.2: {}
|
||||||
|
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
|
|
||||||
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 {
|
|
||||||
const { wsCache } = useCache()
|
|
||||||
const user = wsCache.get(CACHE_KEY.USER).user
|
|
||||||
dbPromise = openDB<MyDB>('yudao-im-indexeddb-' + user.id, 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
<template>
|
|
||||||
<view class="flex h-full flex-1">
|
|
||||||
<ToolSection @menu-select-change="toolMenuSelectChange" />
|
|
||||||
<Session v-if="chatStore.bussinessType === MENU_LIST_ENUM.CONVERSATION" />
|
|
||||||
<view class="flex">
|
|
||||||
<Department v-if="chatStore.bussinessType === MENU_LIST_ENUM.FRIENDS" />
|
|
||||||
<Friends v-if="chatStore.bussinessType === MENU_LIST_ENUM.FRIENDS" />
|
|
||||||
</view>
|
|
||||||
<view v-if="chatStore.bussinessType === MENU_LIST_ENUM.CONVERSATION" class="flex w-full flex-col">
|
|
||||||
<ChatHeader />
|
|
||||||
<ChatMessage />
|
|
||||||
<InputSection />
|
|
||||||
</view>
|
|
||||||
<FriendDetail v-if="chatStore.bussinessType === MENU_LIST_ENUM.FRIENDS && useFriendStore.currentFriend" />
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
/**
|
|
||||||
* TODO: Replace me with comment, and tell the main subject of this page
|
|
||||||
*/
|
|
||||||
|
|
||||||
import ToolSection from '../components/ToolSection/Index.vue'
|
|
||||||
import Session from '../components/Conversation/index.vue'
|
|
||||||
import Friends from '../components/Friends/Index.vue'
|
|
||||||
import Department from '../components/Department/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/types'
|
|
||||||
import { useWebSocketStore } from '../store/websocketStore'
|
|
||||||
import { useFriendStoreWithOut } from '../store/friendstore'
|
|
||||||
import { useChatStore } from '../store/chatstore'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ChatPage' })
|
|
||||||
|
|
||||||
const webSocketStore = useWebSocketStore();
|
|
||||||
const useFriendStore = useFriendStoreWithOut()
|
|
||||||
const { resetFriendList } = useFriendStore
|
|
||||||
const chatStore = useChatStore()
|
|
||||||
const { setBussinessType } = useChatStore()
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
webSocketStore.connect()
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(() => chatStore.bussinessType, (newVal) => {
|
|
||||||
if (newVal !== MENU_LIST_ENUM.FRIENDS) {
|
|
||||||
resetFriendList()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const toolMenuSelectChange = (value) => {
|
|
||||||
setBussinessType(value)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
/*
|
|
||||||
* @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/types'
|
|
||||||
|
|
||||||
export interface SendMsg {
|
|
||||||
clientMessageId: string
|
|
||||||
receiverId: number
|
|
||||||
conversationType: number
|
|
||||||
contentType: number
|
|
||||||
content: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SessionMsgReq {
|
|
||||||
sendTime: string
|
|
||||||
receiverId: number
|
|
||||||
userId: number
|
|
||||||
conversationType: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 消息接口
|
|
||||||
*/
|
|
||||||
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 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
/*
|
|
||||||
* @Author: dylan.may@qq.com
|
|
||||||
* @Date: 2024-10-16 11:30:31
|
|
||||||
* @Last Modified by: dylan.may@qq.com
|
|
||||||
* @Last Modified time: 2024-11-28 17:32:26
|
|
||||||
*/
|
|
||||||
|
|
||||||
import request from '@/config/axios'
|
|
||||||
import { ChatConversation } from '../model/ChatConversation'
|
|
||||||
|
|
||||||
interface createConversationParam {
|
|
||||||
targetId: string,
|
|
||||||
type: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 会话接口
|
|
||||||
*/
|
|
||||||
export default class SessionApi {
|
|
||||||
/**
|
|
||||||
* 获取会话列表
|
|
||||||
* @returns Promise<Array<ChatConversation>>
|
|
||||||
*/
|
|
||||||
static getSessionList(): Promise<Array<ChatConversation>> {
|
|
||||||
return request.get({ url: '/im/conversation/list' })
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建会话
|
|
||||||
* @param data createConversationParam
|
|
||||||
* @returns Promise<ChatConversation>
|
|
||||||
*/
|
|
||||||
static createConversation(data: createConversationParam):Promise<ChatConversation> {
|
|
||||||
return request.post({
|
|
||||||
url: '/im/conversation/create',
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<template>
|
|
||||||
<view
|
|
||||||
class="flex items-center w-full border-b-1 border-b-gray border-b-solid"
|
|
||||||
style="height: 60px; min-height: 60px"
|
|
||||||
>
|
|
||||||
<label class="text-black text-size-xl font-medium mx-4">{{
|
|
||||||
chatStore.currentSession?.nickname || chatStore.currentSession?.name
|
|
||||||
}}</label>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { useChatStore } from '../../store/chatstore'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ChatHeader' })
|
|
||||||
|
|
||||||
const chatStore = useChatStore()
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
<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 overflow-scroll"
|
|
||||||
ref="listBoxRef"
|
|
||||||
>
|
|
||||||
<template v-for="item in chatStore.currentSession?.msgList">
|
|
||||||
<TextMsg
|
|
||||||
v-if="item.contentType === ContentType.TEXT"
|
|
||||||
:key="item.clientMessageId"
|
|
||||||
:message="item"
|
|
||||||
class="py-1"
|
|
||||||
/>
|
|
||||||
<ImageMsg
|
|
||||||
v-if="item.contentType === ContentType.IMAGE"
|
|
||||||
:key="item.clientMessageId"
|
|
||||||
:message="item"
|
|
||||||
class="py-1"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
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/types'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ChatMessage' })
|
|
||||||
|
|
||||||
const chatStore = useChatStore()
|
|
||||||
const listBoxRef = ref<HTMLElement | null>(null)
|
|
||||||
|
|
||||||
// TODO @dylan:msg 尽量使用 message 哈。非必要不缩写
|
|
||||||
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>
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
<template>
|
|
||||||
<view class="flex flex-col items-center h-full py-2 b-1 b-gray b-solid overflow-auto" style="width: 258px">
|
|
||||||
<view class="flex flex-col w-full">
|
|
||||||
<SessionItem
|
|
||||||
v-for="(item, index) in chatStore.sessionList"
|
|
||||||
:key="item.id"
|
|
||||||
:index="index"
|
|
||||||
:conversation="item"
|
|
||||||
@click="() => onSessionItemClick(index)"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
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, getConversationList } = useChatStoreWithOut()
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getConversationList()
|
|
||||||
// set default conversation
|
|
||||||
nextTick(() => {
|
|
||||||
setCurrentConversation()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const onSessionItemClick = (index: number) => {
|
|
||||||
setCurrentSessionIndex(index)
|
|
||||||
setCurrentConversation()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
<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" :src="props.conversation.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="namefontColor()">{{
|
|
||||||
props.conversation.nickname || '' }}</label>
|
|
||||||
<label class="text-gray-f text-size-sm text-ellipsis text-nowrap mr-1" :class="timefontColor()">{{ lastMessage
|
|
||||||
}}</label>
|
|
||||||
</view>
|
|
||||||
<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>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { PropType } from 'vue'
|
|
||||||
import { ContentType, ConversationModelType } from '../../types/types'
|
|
||||||
|
|
||||||
import { formatPast } from '@/utils/formatTime'
|
|
||||||
import { useChatStore } from '../../store/chatstore.js'
|
|
||||||
import TextMessage from '../../model/TextMessage.js';
|
|
||||||
|
|
||||||
defineOptions({ name: 'SessionItem' })
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
conversation: {
|
|
||||||
type: Object as PropType<ConversationModelType>,
|
|
||||||
default: () => { }
|
|
||||||
},
|
|
||||||
index: Number
|
|
||||||
})
|
|
||||||
|
|
||||||
const chatStore = useChatStore()
|
|
||||||
|
|
||||||
const bgColor = () => {
|
|
||||||
return props.index === chatStore.currentSessionIndex ? 'bg-blue' : 'bg-white'
|
|
||||||
}
|
|
||||||
|
|
||||||
const namefontColor = () => {
|
|
||||||
return props.index === chatStore.currentSessionIndex ? 'text-white' : 'nameColor'
|
|
||||||
}
|
|
||||||
const timefontColor = () => {
|
|
||||||
return props.index === chatStore.currentSessionIndex ? 'text-white' : 'timeColor'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: 修改为后端计算,否则在没有打开聊天窗口的时候,没有数据。
|
|
||||||
*/
|
|
||||||
const lastMessage = computed(() => {
|
|
||||||
|
|
||||||
if (!props.conversation.msgList || 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>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.timeColor {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nameColor {
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="tree-node" >
|
|
||||||
<div class="node-title custom-hover" @click.stop="onClick">
|
|
||||||
<ElAvatar class="m-2" shape="square" v-if="node.children.length > 0">{{ node.name.substring(0,1) }}</ElAvatar>
|
|
||||||
<span class="mx-1 p-1">{{ node.name }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="node.children && node.children.length > 0" class="children">
|
|
||||||
<TreeNode
|
|
||||||
v-for="child in node.children"
|
|
||||||
:key="child.id"
|
|
||||||
:node="child"
|
|
||||||
@node-click="$emit('node-click', $event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import TreeNode from './TreeNode.vue';
|
|
||||||
|
|
||||||
// 传递 `node` 数据作为属性
|
|
||||||
const props = defineProps({
|
|
||||||
node: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['node-click'])
|
|
||||||
|
|
||||||
const onClick = () => {
|
|
||||||
emit('node-click', props.node)
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.tree-node {
|
|
||||||
padding-left: 8px;
|
|
||||||
margin-left: 16px;
|
|
||||||
border-left: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-title {
|
|
||||||
margin: 4px 0;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.children {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="tree">
|
|
||||||
<TreeNode v-for="item in hierarchy" :key="item.id" :node="item" @node-click="$emit('tree-click', $event)"/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import TreeNode from './TreeNode.vue';
|
|
||||||
|
|
||||||
// 传递 `hierarchy` 数据作为组件属性
|
|
||||||
defineProps({
|
|
||||||
hierarchy: {
|
|
||||||
type: Array,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['tree-click'])
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.tree {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
<template>
|
|
||||||
<view class="flex flex-col items-left h-full py-2 b-1 b-gray b-solid" style="width: 248px; min-width: 248px">
|
|
||||||
<!-- <view v-for="item in departListState.list" class="w-full justify-left custom-hover border-b-gray border-1" :key="item.id" style="height: 70px;">
|
|
||||||
<ElAvatar shape="square">{{ item.name.substring(0,1) }}</ElAvatar>
|
|
||||||
<view class="text-size-sm ml-1">{{ item.name }}</view>
|
|
||||||
</view> -->
|
|
||||||
|
|
||||||
<el-skeleton animated :loading="state.loading" :throttle="{ leading: 500, initVal: true, trailing: 500 }"
|
|
||||||
>
|
|
||||||
<template #template>
|
|
||||||
<div v-for="item in 12" :key="item" class="flex flex-1 mx-2 my-3">
|
|
||||||
<el-skeleton-item animated variant="rect" style="width: 50px; height: 50px; max-width: 50px;" />
|
|
||||||
<div class="mx-2 flex flex-1 flex-col mt-2">
|
|
||||||
<el-skeleton-item animated variant="rect" style="height: 10px;" />
|
|
||||||
<el-skeleton-item animated variant="rect" style="height: 10px;" class="mt-3" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #default>
|
|
||||||
<div class="w-full h-full">
|
|
||||||
<TreeView :hierarchy="useFriendStore.departmentList" @tree-click="handleNodeClick" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-skeleton>
|
|
||||||
|
|
||||||
<view
|
|
||||||
v-if="useFriendStore.departmentList.length === 0 && !state.loading"
|
|
||||||
class="flex justify-center items-center h-full">No data</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { onMounted } from 'vue'
|
|
||||||
import TreeView from './components/TreeView.vue'
|
|
||||||
import { useFriendStoreWithOut } from '../../store/friendstore';
|
|
||||||
|
|
||||||
|
|
||||||
defineOptions({ name: 'Department' })
|
|
||||||
const useFriendStore = useFriendStoreWithOut()
|
|
||||||
const { fetchDepartment, setCurrentDepartmentId, fetchDeptUser } = useFriendStore
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
loading: true
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fetchDepartment()
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(() => useFriendStore.departmentList, () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
state.loading = false
|
|
||||||
}, 1000);
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleNodeClick = (data: Tree) => {
|
|
||||||
setCurrentDepartmentId(data.id)
|
|
||||||
fetchDeptUser(data.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
<template>
|
|
||||||
<view
|
|
||||||
class="flex flex-col items-center w-full border-b-1 border-b-gray border-b-solid flex-1 border-b-1 border-b-gray border-b-solid py-2"
|
|
||||||
>
|
|
||||||
<view class="flex mt-20" v-if="friendStore.currentFriend != null">
|
|
||||||
<el-image
|
|
||||||
style="width: 8rem; height: 8rem"
|
|
||||||
class="rounded"
|
|
||||||
:src="friendStore.currentFriend.avatar"
|
|
||||||
/>
|
|
||||||
<view class="flex flex-col ml-4 mt-10">
|
|
||||||
<label class="font-500 text-black font-size-5">{{ friendStore.currentFriend?.name || '无名' }}</label>
|
|
||||||
<label class="mt-2 text-size-sm">{{ friendStore.currentFriend?.description || '--人生若只如初见' }}</label>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view v-else class="mt-50 flex flex-col items-center">
|
|
||||||
<Icon icon="ep:coffee-cup" :size="64" />
|
|
||||||
<label>空空如也</label>
|
|
||||||
</view>
|
|
||||||
<el-button type="primary" class="mt-10" v-if="friendStore.currentFriend != null" @click="onSend"> 发送消息</el-button>
|
|
||||||
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { useChatStore } from '../../store/chatstore';
|
|
||||||
import { useFriendStore } from '../../store/friendstore'
|
|
||||||
import { CONVERSATION_TYPE } from '../../types/types';
|
|
||||||
|
|
||||||
defineOptions({ name: 'FriendDetail' })
|
|
||||||
|
|
||||||
const friendStore = useFriendStore()
|
|
||||||
const chatStore = useChatStore()
|
|
||||||
|
|
||||||
const onSend = () => {
|
|
||||||
const avatar = friendStore.currentFriend?.avatar || ''
|
|
||||||
const nickname = friendStore.currentFriend?.name || ''
|
|
||||||
chatStore.createConversation(friendStore.currentFriend?.id, CONVERSATION_TYPE.SINGLE, avatar, nickname)
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
<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" :src="friend.avatar" />
|
|
||||||
<label :class="fontColor()">{{ friend.name }}</label>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { PropType } from 'vue'
|
|
||||||
import { useFriendStore } from '../../store/friendstore'
|
|
||||||
|
|
||||||
import Friend from '../../model/Friend'
|
|
||||||
|
|
||||||
defineOptions({ name: 'FriendItem' })
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
friend: {
|
|
||||||
type: Object as PropType<Friend>,
|
|
||||||
default: () => {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const friendStore = useFriendStore()
|
|
||||||
|
|
||||||
const bgColor = () => {
|
|
||||||
return props.friend.id === friendStore.currentFriend?.id ? 'bg-blue' : 'bg-white'
|
|
||||||
}
|
|
||||||
|
|
||||||
const fontColor = () => {
|
|
||||||
return props.friend.id === friendStore.currentFriend?.id ? 'text-white' : 'text-black'
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
<template>
|
|
||||||
<view
|
|
||||||
class="flex flex-col items-center h-full py-2 b-1 b-gray b-solid b-l-white"
|
|
||||||
style="width: 248px; min-width: 248px"
|
|
||||||
>
|
|
||||||
<view class="flex flex-col w-full">
|
|
||||||
<FriendItem
|
|
||||||
v-for="(item, index) in friendStore.friendList"
|
|
||||||
:key="item.id"
|
|
||||||
:index="index"
|
|
||||||
:friend="item"
|
|
||||||
@click="() => onFriendClick(item)"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import FriendItem from '../FriendItem/Index.vue'
|
|
||||||
import { useFriendStore } from '../../store/friendstore'
|
|
||||||
import { onMounted } from 'vue'
|
|
||||||
import Friend from '../../model/Friend'
|
|
||||||
|
|
||||||
defineOptions({ name: 'Friends' })
|
|
||||||
|
|
||||||
const friendStore = useFriendStore()
|
|
||||||
onMounted(async () => {
|
|
||||||
// set default conversation
|
|
||||||
// await friendStore.fetchFriend()
|
|
||||||
})
|
|
||||||
|
|
||||||
const onFriendClick = (friend: Friend) => {
|
|
||||||
console.log('====>', friend)
|
|
||||||
friendStore.setCurrentFriend(friend)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
<template>
|
|
||||||
<view
|
|
||||||
class="flex flex-col items-center w-full border-b-1 border-b-gray border-b-solid"
|
|
||||||
style="height: 248px; min-height: 248px"
|
|
||||||
>
|
|
||||||
<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: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" />
|
|
||||||
</view>
|
|
||||||
<ElInput
|
|
||||||
type="textarea"
|
|
||||||
class="h-full"
|
|
||||||
clearable
|
|
||||||
v-model="chatStore.inputText"
|
|
||||||
input-style="border: none !important; box-shadow: none !important"
|
|
||||||
:autosize="{ minRows: 10, maxRows: 11 }"
|
|
||||||
placeholder="Press Enter to send"
|
|
||||||
@keydown.enter="onEnter"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import TextMessage from '../../model/TextMessage'
|
|
||||||
import { useChatStoreWithOut } from '../../store/chatstore'
|
|
||||||
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')
|
|
||||||
|
|
||||||
if (!chatStore.inputText.trim()) {
|
|
||||||
ElNotification({
|
|
||||||
title: '温馨提示',
|
|
||||||
message: '请输入内容',
|
|
||||||
type: 'warning'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const msg = createTextMessage(chatStore.inputText.trim())
|
|
||||||
chatStore.addMessageToCurrentSession(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
const createTextMessage = (content: string): TextMessage => {
|
|
||||||
|
|
||||||
const userStore = useUserStoreWithOut()
|
|
||||||
|
|
||||||
// 部分信息从account信息里面获取
|
|
||||||
const msg = new TextMessage(
|
|
||||||
'',
|
|
||||||
userStore.user.avatar,
|
|
||||||
userStore.user.nickname,
|
|
||||||
new Date().getTime(),
|
|
||||||
false,
|
|
||||||
content,
|
|
||||||
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 || ''
|
|
||||||
)
|
|
||||||
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
<template>
|
|
||||||
<view
|
|
||||||
class="flex w-full"
|
|
||||||
:class="props.message.role === MessageRole.SELF ? 'flex-row-reverse' : 'flex-row'"
|
|
||||||
>
|
|
||||||
<el-avatar shape="square" size="default" class="mx-2" :src="props.message.avatar" />
|
|
||||||
<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>
|
|
||||||
<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 { Loading } from '@element-plus/icons-vue'
|
|
||||||
import { MessageModelType, MessageRole, SendStatus } from '../../types/types'
|
|
||||||
|
|
||||||
defineOptions({ name: 'BaseMessage' })
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
message: {
|
|
||||||
type: Object as PropType<MessageModelType>,
|
|
||||||
default: () => {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<template>
|
|
||||||
<BaseMesageLayout>
|
|
||||||
<template #content>
|
|
||||||
<view>
|
|
||||||
<label>{{ props.message.content }}</label>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
</BaseMesageLayout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { PropType } from 'vue'
|
|
||||||
import { useChatStore } from '../../store/chatstore'
|
|
||||||
import { onMounted } from 'vue'
|
|
||||||
import { MessageModelType } from '../../types/types'
|
|
||||||
import BaseMesageLayout from './BaseMsg.vue'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ImageMessage' })
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
message: {
|
|
||||||
type: Object as PropType<MessageModelType>,
|
|
||||||
default: () => {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const { sessionList, setCurrentConversation, setCurrentSessionIndex } = useChatStore()
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<template>
|
|
||||||
<BaseMesageLayout :message="props.message">
|
|
||||||
<template #content>
|
|
||||||
<view class="p-3 bg-gray-2 rounded">
|
|
||||||
<label>{{ props.message.content }}</label>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
</BaseMesageLayout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { PropType } from 'vue'
|
|
||||||
import BaseMesageLayout from './BaseMsg.vue'
|
|
||||||
import TextMessage from '../../model/TextMessage'
|
|
||||||
|
|
||||||
defineOptions({ name: 'TextMessage' })
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
message: {
|
|
||||||
type: Object as PropType<TextMessage>,
|
|
||||||
default: () => {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
<template>
|
|
||||||
<view class="flex flex-col items-center bg-gray-2 h-full py-2" style="width: 80px; min-width: 80px">
|
|
||||||
<el-avatar shape="square" />
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex flex-col items-center px-3 py-3 mt-4 rounded-2 hover:bg-white"
|
|
||||||
:class="chatStore.bussinessType === MENU_LIST_ENUM.CONVERSATION ? 'bg-gray-3' : ''" style="width: 60px;"
|
|
||||||
@click="onConversatonClicked">
|
|
||||||
<icon icon="ep:chat-line-round" :size="24" color="#409EFF" />
|
|
||||||
<span class="text-xs mt-1 text-gray-5">会 话</span>
|
|
||||||
<span></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex flex-col items-center rounded-2 mt-4 p-3 hover:bg-white"
|
|
||||||
:class="chatStore.bussinessType === MENU_LIST_ENUM.FRIENDS ? 'bg-gray-3' : ''" style="width: 60px;" @click="onFriendsClicked">
|
|
||||||
<icon icon="ep:avatar" :size="24" color="#409EFF" />
|
|
||||||
<span class="text-xs mt-1 text-gray-5">联系人</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { useChatStore } from '../../store/chatstore';
|
|
||||||
import { MENU_LIST_ENUM } from '../../types/types'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ToolSection' })
|
|
||||||
|
|
||||||
const selectItem = ref(1)
|
|
||||||
const chatStore = useChatStore()
|
|
||||||
const { setBussinessType } = useChatStore()
|
|
||||||
|
|
||||||
const emit = defineEmits(['menuSelectChange'])
|
|
||||||
watch(
|
|
||||||
() => selectItem.value,
|
|
||||||
(newValue) => {
|
|
||||||
emit('menuSelectChange', newValue)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const onConversatonClicked = () => {
|
|
||||||
setBussinessType(MENU_LIST_ENUM.CONVERSATION)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onFriendsClicked = () => {
|
|
||||||
setBussinessType(MENU_LIST_ENUM.FRIENDS)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
import { ConversationType, MessageModelType } from '../types/types'
|
|
||||||
|
|
||||||
export default class BaseConversation {
|
|
||||||
public id: string
|
|
||||||
public avatar: string
|
|
||||||
public name: string
|
|
||||||
public description: string
|
|
||||||
public createTime: number
|
|
||||||
public updateTime: number
|
|
||||||
public unreadMessagesCount: number
|
|
||||||
public msgList: Array<MessageModelType>
|
|
||||||
public type: ConversationType
|
|
||||||
public targetId: number
|
|
||||||
public senderId: number
|
|
||||||
public conversationNo: string
|
|
||||||
public lastMessageDescription: string
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
id: string,
|
|
||||||
avatar: string,
|
|
||||||
name: string,
|
|
||||||
lastMessageDescription: string,
|
|
||||||
createTime: number,
|
|
||||||
updateTime: number,
|
|
||||||
unreadMessagesCount: number,
|
|
||||||
msgList: Array<MessageModelType>,
|
|
||||||
type: ConversationType,
|
|
||||||
targetId: number,
|
|
||||||
senderId: number,
|
|
||||||
conversationNo: string
|
|
||||||
) {
|
|
||||||
this.id = id
|
|
||||||
this.avatar = avatar
|
|
||||||
this.name = name
|
|
||||||
this.lastMessageDescription = lastMessageDescription
|
|
||||||
this.createTime = createTime
|
|
||||||
this.updateTime = updateTime
|
|
||||||
this.unreadMessagesCount = unreadMessagesCount
|
|
||||||
this.msgList = msgList
|
|
||||||
this.type = type
|
|
||||||
this.targetId = targetId
|
|
||||||
this.senderId = senderId
|
|
||||||
this.conversationNo = conversationNo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
import { MessageRole, ContentType, SendStatus } from '../types/types'
|
|
||||||
|
|
||||||
export default class BaseMessage {
|
|
||||||
id?: string
|
|
||||||
avatar?: string
|
|
||||||
nickname?: string
|
|
||||||
createTime: number
|
|
||||||
isRead: boolean
|
|
||||||
role: MessageRole
|
|
||||||
sendStatus: SendStatus
|
|
||||||
contentType: ContentType
|
|
||||||
conversationId: string
|
|
||||||
clientMessageId: string
|
|
||||||
senderId: number
|
|
||||||
receiverId: number
|
|
||||||
conversationType: number
|
|
||||||
conversationUserId: number
|
|
||||||
constructor(
|
|
||||||
id: string,
|
|
||||||
avatar: string,
|
|
||||||
nickname: string,
|
|
||||||
createTime: number,
|
|
||||||
isRead: boolean,
|
|
||||||
role: MessageRole,
|
|
||||||
sendStauts: SendStatus,
|
|
||||||
contentType: ContentType,
|
|
||||||
conversationId: string,
|
|
||||||
senderId: number,
|
|
||||||
receiverId: number,
|
|
||||||
conversationType: number,
|
|
||||||
conversationUserId: number
|
|
||||||
) {
|
|
||||||
this.id = id
|
|
||||||
this.avatar = avatar
|
|
||||||
this.nickname = nickname
|
|
||||||
this.createTime = createTime
|
|
||||||
this.isRead = isRead
|
|
||||||
this.role = role
|
|
||||||
this.sendStatus = sendStauts
|
|
||||||
this.contentType = contentType
|
|
||||||
this.conversationId = conversationId
|
|
||||||
this.senderId = senderId
|
|
||||||
this.receiverId = receiverId
|
|
||||||
this.clientMessageId = this.generateClientMessageId()
|
|
||||||
this.conversationType = conversationType
|
|
||||||
this.conversationUserId = conversationUserId
|
|
||||||
}
|
|
||||||
|
|
||||||
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}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
export interface BaseResponse<T> {
|
|
||||||
code: number // 0表示成功其他表示失败
|
|
||||||
message: string // 返回的信息,可以是成功或错误信息
|
|
||||||
data: T // 泛型数据,成功时返回数据,失败时为 null
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import BaseConversation from './BaseConversation'
|
|
||||||
import BaseMessage from './BaseMessage'
|
|
||||||
|
|
||||||
// TODO @dylan:这些 ts 类,是不是可以搞个 types.ts,然后放到 api/im 目录下?放在一个文件里
|
|
||||||
|
|
||||||
export class ChatConversation extends BaseConversation {
|
|
||||||
constructor(
|
|
||||||
id: string,
|
|
||||||
avatar: string,
|
|
||||||
name: string,
|
|
||||||
lastMessageDescription: string,
|
|
||||||
createTime: number,
|
|
||||||
updateTime: number,
|
|
||||||
unreadMessagesCount: number,
|
|
||||||
msgList: Array<BaseMessage>,
|
|
||||||
type: number,
|
|
||||||
targetId: number,
|
|
||||||
senderId: number,
|
|
||||||
conversationNo: string
|
|
||||||
) {
|
|
||||||
super(
|
|
||||||
id,
|
|
||||||
avatar,
|
|
||||||
name,
|
|
||||||
lastMessageDescription,
|
|
||||||
createTime,
|
|
||||||
updateTime,
|
|
||||||
unreadMessagesCount,
|
|
||||||
msgList,
|
|
||||||
type,
|
|
||||||
targetId,
|
|
||||||
senderId,
|
|
||||||
conversationNo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
export default class Friend {
|
|
||||||
public id: string
|
|
||||||
public avatar: string
|
|
||||||
public name: string
|
|
||||||
public description: string
|
|
||||||
public createTime: number
|
|
||||||
public deptId: number
|
|
||||||
public deptName: string
|
|
||||||
|
|
||||||
constructor(id, avatar, name, description, createTime, deptId, deptName) {
|
|
||||||
this.id = id
|
|
||||||
this.avatar = avatar
|
|
||||||
this.name = name
|
|
||||||
this.description = description
|
|
||||||
this.createTime = createTime
|
|
||||||
this.deptId = deptId
|
|
||||||
this.deptName = deptName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
import { MessageRole, ContentType, SendStatus } from '@/views/chat/types/types'
|
|
||||||
import BaseMessage from './BaseMessage'
|
|
||||||
|
|
||||||
export default class ImageMessage extends BaseMessage {
|
|
||||||
content: string
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
id: string,
|
|
||||||
avatar: string,
|
|
||||||
nickname: string,
|
|
||||||
createTime: number,
|
|
||||||
isRead: boolean,
|
|
||||||
content: string,
|
|
||||||
role: MessageRole,
|
|
||||||
sendStatus: SendStatus,
|
|
||||||
conversationId: string,
|
|
||||||
receiverId: number,
|
|
||||||
conversationType: number,
|
|
||||||
conversationUserId: number
|
|
||||||
) {
|
|
||||||
super(
|
|
||||||
id,
|
|
||||||
avatar,
|
|
||||||
nickname,
|
|
||||||
createTime,
|
|
||||||
isRead,
|
|
||||||
role,
|
|
||||||
sendStatus,
|
|
||||||
ContentType.IMAGE,
|
|
||||||
conversationId,
|
|
||||||
receiverId,
|
|
||||||
conversationType,
|
|
||||||
conversationUserId
|
|
||||||
)
|
|
||||||
this.content = content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
import { MessageRole, ContentType, SendStatus, ImMessageContent } from '@/views/chat/types/types'
|
|
||||||
import BaseMessage from './BaseMessage'
|
|
||||||
|
|
||||||
export default class TextMessage extends BaseMessage {
|
|
||||||
content: string
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
id: string,
|
|
||||||
avatar: string,
|
|
||||||
nickname: string,
|
|
||||||
createTime: number,
|
|
||||||
isRead: boolean,
|
|
||||||
content: string,
|
|
||||||
role: MessageRole,
|
|
||||||
sendStatus: SendStatus,
|
|
||||||
conversationId: string,
|
|
||||||
senderId: number,
|
|
||||||
receiverId: number,
|
|
||||||
conversationType: number,
|
|
||||||
conversationUserId: number
|
|
||||||
) {
|
|
||||||
super(
|
|
||||||
id,
|
|
||||||
avatar,
|
|
||||||
nickname,
|
|
||||||
createTime,
|
|
||||||
isRead,
|
|
||||||
role,
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,335 +0,0 @@
|
||||||
import { store } from '@/store/index'
|
|
||||||
import { defineStore } from 'pinia'
|
|
||||||
import BaseConversation from '../model/BaseConversation'
|
|
||||||
import BaseMessage from '../model/BaseMessage'
|
|
||||||
import { ConversationModelType, MessageRole, ContentType, SendStatus, MENU_LIST_ENUM, CONVERSATION_TYPE } from '../types/types'
|
|
||||||
import SessionApi from '../api/sessionApi'
|
|
||||||
import MessageApi, { SendMsg } from '../api/messageApi'
|
|
||||||
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 {
|
|
||||||
sessionList: Array<ConversationModelType>
|
|
||||||
currentSession: ConversationModelType | null
|
|
||||||
currentSessionIndex: number
|
|
||||||
inputText: string,
|
|
||||||
bussinessType: number // conversation 1, friends 2
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useChatStore = defineStore('chatStore', {
|
|
||||||
state: (): ChatStoreModel => ({
|
|
||||||
sessionList: [],
|
|
||||||
currentSession: null,
|
|
||||||
currentSessionIndex: 0,
|
|
||||||
inputText: '',
|
|
||||||
bussinessType: 1,
|
|
||||||
}),
|
|
||||||
|
|
||||||
getters: {
|
|
||||||
getSessionList(state: ChatStoreModel): Array<ConversationModelType> {
|
|
||||||
return state.sessionList
|
|
||||||
},
|
|
||||||
|
|
||||||
getCurrentSession(state: ChatStoreModel): ConversationModelType | null {
|
|
||||||
return state.currentSession
|
|
||||||
},
|
|
||||||
|
|
||||||
getCurrentSessionIndex(state: ChatStoreModel): number {
|
|
||||||
return state.currentSessionIndex
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
addSession(session: BaseConversation) {
|
|
||||||
this.sessionList.push(session)
|
|
||||||
},
|
|
||||||
|
|
||||||
setCurrentConversation() {
|
|
||||||
this.currentSession = this.sessionList[this.currentSessionIndex]
|
|
||||||
this.fetchSessionMsg()
|
|
||||||
},
|
|
||||||
|
|
||||||
setCurrentSessionIndex(index: number) {
|
|
||||||
this.currentSessionIndex = index
|
|
||||||
},
|
|
||||||
|
|
||||||
setInputText(content: string) {
|
|
||||||
this.inputText = content
|
|
||||||
},
|
|
||||||
|
|
||||||
setBussinessType(type: number) {
|
|
||||||
this.bussinessType = type
|
|
||||||
},
|
|
||||||
|
|
||||||
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)
|
|
||||||
} finally {
|
|
||||||
this.setInputText('')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addMessageToSesstion<T extends BaseMessage>(message: T): void {
|
|
||||||
// get the conversation from list
|
|
||||||
const conversationIndex = this.sessionList.findIndex((item) => {
|
|
||||||
return item.id === message.conversationId
|
|
||||||
})
|
|
||||||
|
|
||||||
const msgConversation = this.sessionList[conversationIndex]
|
|
||||||
|
|
||||||
// add the message
|
|
||||||
msgConversation?.msgList.push(message)
|
|
||||||
|
|
||||||
// replace the old Conversation
|
|
||||||
this.sessionList.splice(conversationIndex, 1, msgConversation)
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加消息到会话
|
|
||||||
* @param message
|
|
||||||
*/
|
|
||||||
addMessageToConversation<T extends BaseMessage>(message: T): void {
|
|
||||||
|
|
||||||
const converstionNo = generateConversationNo(
|
|
||||||
message.senderId,
|
|
||||||
message.receiverId,
|
|
||||||
message.conversationType
|
|
||||||
)
|
|
||||||
|
|
||||||
const conversationIndex = this.sessionList.findIndex(
|
|
||||||
(item) => item.conversationNo === converstionNo
|
|
||||||
)
|
|
||||||
|
|
||||||
if (conversationIndex < 0) {
|
|
||||||
console.log('conversation not exist, create it')
|
|
||||||
// Todo
|
|
||||||
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 )
|
|
||||||
|
|
||||||
// 更新当前会话
|
|
||||||
if (conversationIndex === this.currentSessionIndex) {
|
|
||||||
this.setCurrentConversation()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据消息创建会话
|
|
||||||
* @param BaseMessage
|
|
||||||
* @param msg
|
|
||||||
*/
|
|
||||||
createConversation(BaseMessage: msg) {
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成会话No
|
|
||||||
* @param id1
|
|
||||||
* @param id2
|
|
||||||
* @param conversationType
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
generateConversationNo(id1: string, id2: string, conversationType: CONVERSATION_TYPE): string {
|
|
||||||
const SINGLE_PREFIX = 's_'
|
|
||||||
const GROUP_PREFIX = 'g_'
|
|
||||||
const [smallId, largeId] = id1 < id2 ? [id1, id2] : [id2, id1];
|
|
||||||
|
|
||||||
if (conversationType === CONVERSATION_TYPE.SINGLE) {
|
|
||||||
return SINGLE_PREFIX + smallId + "_" + largeId
|
|
||||||
} else if (conversationType === CONVERSATION_TYPE.GROUP) {
|
|
||||||
return GROUP_PREFIX + smallId + "_" + largeId
|
|
||||||
}
|
|
||||||
|
|
||||||
return ''
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新消息到当前会话
|
|
||||||
* @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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const rawCurrentSesstion = toRaw(this.currentSession)
|
|
||||||
rawCurrentSesstion.msgList =this.currentSession.msgList.map(item => toRaw(item))
|
|
||||||
console.log("raw", rawCurrentSesstion)
|
|
||||||
addConversation(rawCurrentSesstion as ChatConversation)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
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) => ({
|
|
||||||
...item,
|
|
||||||
updateTime: item.lastReadTime,
|
|
||||||
name: item.targetId,
|
|
||||||
targetId: item.targetId,
|
|
||||||
senderId: item.userId,
|
|
||||||
conversationNo: item.no,
|
|
||||||
unreadMessagesCount: item.unreadMessagesCount,
|
|
||||||
description: item.lastMessageDescription,
|
|
||||||
msgList: []
|
|
||||||
}))
|
|
||||||
|
|
||||||
// 同步到数据库
|
|
||||||
this.sessionList.forEach((item) => {
|
|
||||||
console.log(item)
|
|
||||||
addConversation(toRaw(item) as ChatConversation)
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
return error
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchSessionMsg() {
|
|
||||||
if (!this.currentSession) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const userStore = useUserStoreWithOut()
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await MessageApi.getSessionMsg({
|
|
||||||
receiverId: this.currentSession.targetId,
|
|
||||||
userId: this.currentSession.senderId,
|
|
||||||
// sendTime: formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')
|
|
||||||
conversationType: this.currentSession.type
|
|
||||||
})
|
|
||||||
|
|
||||||
this.currentSession.msgList = res.map((item) => {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
role: item.senderId === userStore.user.id ? MessageRole.SELF : MessageRole.OTHER,
|
|
||||||
nickname: item.senderNickname,
|
|
||||||
avatar: item.senderAvatar
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
addConversation(toRaw(this.currentSession) as ChatConversation)
|
|
||||||
} catch (error) {
|
|
||||||
return error
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建会话
|
|
||||||
*/
|
|
||||||
async createConversation(targetId, type, avatar, nickname) {
|
|
||||||
try {
|
|
||||||
const param = {
|
|
||||||
targetId,
|
|
||||||
type
|
|
||||||
}
|
|
||||||
const res = await SessionApi.createConversation(param)
|
|
||||||
if (res) {
|
|
||||||
|
|
||||||
// 切换到聊天模式
|
|
||||||
this.bussinessType = MENU_LIST_ENUM.CONVERSATION
|
|
||||||
|
|
||||||
// 插入用户名和昵称
|
|
||||||
res.avatar = avatar;
|
|
||||||
res.nickname = nickname;
|
|
||||||
const localConversation = this.convertCoversationFromServer(res)
|
|
||||||
// 存入到数据库
|
|
||||||
addConversation(toRaw(localConversation) as ChatConversation)
|
|
||||||
// 从数据库同步到内存
|
|
||||||
await this.getConversationList()
|
|
||||||
// 设置当前的会话
|
|
||||||
const addIndex = this.sessionList.findIndex(item => item.conversationNo === localConversation.conversationNo)
|
|
||||||
this.setCurrentSessionIndex(addIndex)
|
|
||||||
this.setCurrentConversation()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
convertCoversationFromServer(item: any) {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
updateTime: item.updateTime,
|
|
||||||
targetId: item.targetId,
|
|
||||||
senderId: item.userId,
|
|
||||||
conversationNo: item.no,
|
|
||||||
unreadMessagesCount: item.unreadMessagesCount,
|
|
||||||
description: item.lastMessageDescription,
|
|
||||||
avatar: item.avatar,
|
|
||||||
name: item.name,
|
|
||||||
msgList: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useChatStoreWithOut = () => {
|
|
||||||
return useChatStore(store)
|
|
||||||
}
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
import { defineStore } from 'pinia'
|
|
||||||
import { store } from '@/store/index'
|
|
||||||
import BaseConversation from '../model/BaseConversation'
|
|
||||||
import Friend from '../model/Friend'
|
|
||||||
import { getAllUser, getDeptUser } from '@/api/system/user'
|
|
||||||
import * as DeptApi from '@/api/system/dept'
|
|
||||||
|
|
||||||
interface FriendStoreModel {
|
|
||||||
friendList: Array<Friend>
|
|
||||||
currentFriend: Friend | null,
|
|
||||||
selectedDepartmentId: number,
|
|
||||||
departmentList: DeptApi.DeptVO[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useFriendStore = defineStore('friendStore', {
|
|
||||||
state: (): FriendStoreModel => ({
|
|
||||||
friendList: [],
|
|
||||||
currentFriend: null,
|
|
||||||
selectedDepartmentId: 0,
|
|
||||||
departmentList: []
|
|
||||||
}),
|
|
||||||
|
|
||||||
getters: {
|
|
||||||
getFriendList(state: FriendStoreModel): Array<Friend> {
|
|
||||||
return state.friendList
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
addFriend(session: BaseConversation) {
|
|
||||||
this.friendList.push(session)
|
|
||||||
},
|
|
||||||
setCurrentFriend(friend: Friend) {
|
|
||||||
this.currentFriend = friend
|
|
||||||
},
|
|
||||||
setCurrentDepartmentId(id: number) {
|
|
||||||
this.selectedDepartmentId = id
|
|
||||||
},
|
|
||||||
resetFriendList() {
|
|
||||||
this.friendList = []
|
|
||||||
this.currentFriend = null
|
|
||||||
},
|
|
||||||
async fetchDepartment () {
|
|
||||||
try {
|
|
||||||
const result = await DeptApi.getSimpleDeptList()
|
|
||||||
this.departmentList = this.buildHierarchy(result)
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchFriend() {
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await getAllUser()
|
|
||||||
this.friendList = res
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchDeptUser(id) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await getDeptUser(id)
|
|
||||||
if (res) {
|
|
||||||
this.friendList = res.map(item => {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
name: item.nickname
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.friendList = []
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
buildHierarchy(data: DeptApi.DeptVO[]): DeptApi.DeptVO[] {
|
|
||||||
const map = new Map<number, DeptApi.DeptVO>();
|
|
||||||
|
|
||||||
// 初始化 map,确保每个 id 都有一条记录
|
|
||||||
data.forEach(item => map.set(item.id, { ...item, children: [] }));
|
|
||||||
|
|
||||||
const result: DeptApi.DeptVO[] = [];
|
|
||||||
|
|
||||||
data.forEach(item => {
|
|
||||||
if (item.parentId === 0) {
|
|
||||||
// 根节点
|
|
||||||
result.push(map.get(item.id)!);
|
|
||||||
} else {
|
|
||||||
// 子节点,放入父节点的 children 数组
|
|
||||||
const parent = map.get(item.parentId);
|
|
||||||
if (parent) {
|
|
||||||
parent.children!.push(map.get(item.id)!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export const useFriendStoreWithOut = () => {
|
|
||||||
return useFriendStore(store)
|
|
||||||
}
|
|
||||||
|
|
@ -1,207 +0,0 @@
|
||||||
import { defineStore } from 'pinia'
|
|
||||||
import { ref, onUnmounted } from 'vue'
|
|
||||||
import { getRefreshToken } from '@/utils/auth'
|
|
||||||
import { useChatStore } from './chatstore'
|
|
||||||
import {
|
|
||||||
ContentType,
|
|
||||||
ImMessageContent,
|
|
||||||
ImMessageReceiveResponse,
|
|
||||||
WEBSOCKET_MESSAGE_TYPE_ENUM
|
|
||||||
} from '../types/types'
|
|
||||||
import TextMessage from '../model/TextMessage'
|
|
||||||
import { debug } from 'console'
|
|
||||||
import { useUserStore } from '@/store/modules/user'
|
|
||||||
import BaseConversation from '../model/BaseConversation'
|
|
||||||
|
|
||||||
interface Message {
|
|
||||||
type: string
|
|
||||||
data: any
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ImConversationTypeEnum {
|
|
||||||
SINGLE = 1,
|
|
||||||
GROUP = 2
|
|
||||||
// Add other conversation types if needed
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateConversationNo(
|
|
||||||
fromUserId: number,
|
|
||||||
receiverId: number,
|
|
||||||
conversationType: number
|
|
||||||
): string | null {
|
|
||||||
if (conversationType === ImConversationTypeEnum.SINGLE) {
|
|
||||||
return `s_${fromUserId}_${receiverId}`
|
|
||||||
} else if (conversationType === ImConversationTypeEnum.GROUP) {
|
|
||||||
return `g_${receiverId}`
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useWebSocketStore = defineStore('webSocket', () => {
|
|
||||||
const socket = ref<WebSocket | null>(null)
|
|
||||||
const messages = ref<Message[]>([])
|
|
||||||
const isConnected = ref(false)
|
|
||||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
||||||
let heartbeatTimer: ReturnType<typeof setInterval> | null = null
|
|
||||||
|
|
||||||
// 初始化 WebSocket 连接
|
|
||||||
function connect() {
|
|
||||||
const refreshToken = getRefreshToken()
|
|
||||||
|
|
||||||
// 设置 WebSocket URL
|
|
||||||
if (refreshToken) {
|
|
||||||
console.log('refreshToken null')
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = `ws://localhost:48080/infra/ws?token=${refreshToken}`
|
|
||||||
socket.value = new WebSocket(url)
|
|
||||||
|
|
||||||
socket.value.onopen = () => {
|
|
||||||
isConnected.value = true
|
|
||||||
console.log('WebSocket connected')
|
|
||||||
startHeartbeat()
|
|
||||||
}
|
|
||||||
|
|
||||||
// {"type":"im-message-receive",
|
|
||||||
// "content":
|
|
||||||
// "{\
|
|
||||||
// "id\":239,\
|
|
||||||
// "conversationType\":1,\
|
|
||||||
// "senderId\":144,\
|
|
||||||
// "senderNickname\":\"dylan\",\
|
|
||||||
// "senderAvatar\":\"http://192.168.0.208:48083/admin-api/infra/file/4/get/c34f9521ce6a2e21148f16b73ab652e578b5bb572dbc259a5043f754c19c8a3f.png\",\
|
|
||||||
// "receiverId\":1,\
|
|
||||||
// "contentType\":101,\
|
|
||||||
// "content\":\"111\",\
|
|
||||||
// "sendTime\":1731139168438,\
|
|
||||||
// "sequence\":2}"
|
|
||||||
// }
|
|
||||||
socket.value.onmessage = (event) => {
|
|
||||||
|
|
||||||
if (event.data === 'pong') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const websoketMessage = JSON.parse(event.data) as ImMessageReceiveResponse
|
|
||||||
if (websoketMessage.type === WEBSOCKET_MESSAGE_TYPE_ENUM.IM_MESSAGE_RECEIVE.toString()) {
|
|
||||||
const socketChatMessage = JSON.parse(websoketMessage.content) as ImMessageContent
|
|
||||||
|
|
||||||
// 暂不处理自己发送的消息
|
|
||||||
if (socketChatMessage.senderId === useUserStore().user.id) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (socketChatMessage.contentType === ContentType.TEXT) {
|
|
||||||
const chatStore = useChatStore()
|
|
||||||
const localTextMessage = TextMessage.fromWebsocket(socketChatMessage)
|
|
||||||
chatStore.addMessageToConversation(localTextMessage)
|
|
||||||
} else if (socketChatMessage.contentType === ContentType.IMAGE) {
|
|
||||||
|
|
||||||
} else if (socketChatMessage.contentType === ContentType.AUDIO) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (websoketMessage.type === WEBSOCKET_MESSAGE_TYPE_ENUM.IM_CONVERSATION_ADD.toString()) {
|
|
||||||
const chatStore = useChatStore()
|
|
||||||
const conversation = JSON.parse(websoketMessage.content) as BaseConversation
|
|
||||||
chatStore.addSession(conversation)
|
|
||||||
// 同步到内存
|
|
||||||
chatStore.getConversationList()
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// TODO:[dylan]
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Received message:', websoketMessage)
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.info(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// messages.value.push(message)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.value.onclose = () => {
|
|
||||||
isConnected.value = false
|
|
||||||
console.log('WebSocket disconnected')
|
|
||||||
reconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.value.onerror = (error) => {
|
|
||||||
console.error('WebSocket error:', error)
|
|
||||||
isConnected.value = false
|
|
||||||
reconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送消息
|
|
||||||
function sendMessage(message: Message) {
|
|
||||||
if (socket.value && isConnected.value) {
|
|
||||||
socket.value.send(JSON.stringify(message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送心跳消息
|
|
||||||
*/
|
|
||||||
function sendHeartBeat() {
|
|
||||||
if (socket.value && isConnected.value) {
|
|
||||||
socket.value.send('ping')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 断开 WebSocket 连接
|
|
||||||
function disconnect() {
|
|
||||||
if (socket.value) {
|
|
||||||
socket.value.close()
|
|
||||||
socket.value = null
|
|
||||||
}
|
|
||||||
stopHeartbeat()
|
|
||||||
clearTimeout(reconnectTimer!)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自动重连逻辑
|
|
||||||
function reconnect() {
|
|
||||||
stopHeartbeat()
|
|
||||||
if (reconnectTimer) clearTimeout(reconnectTimer)
|
|
||||||
reconnectTimer = setTimeout(() => {
|
|
||||||
console.log('Reconnecting WebSocket...')
|
|
||||||
connect()
|
|
||||||
}, 3000) // 3秒后重连
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动心跳
|
|
||||||
function startHeartbeat() {
|
|
||||||
if (heartbeatTimer) clearInterval(heartbeatTimer)
|
|
||||||
heartbeatTimer = setInterval(() => {
|
|
||||||
if (socket.value && isConnected.value) {
|
|
||||||
sendHeartBeat()
|
|
||||||
console.log('Heartbeat sent')
|
|
||||||
}
|
|
||||||
}, 5000) // 每5秒发送一次心跳
|
|
||||||
}
|
|
||||||
|
|
||||||
// 停止心跳
|
|
||||||
function stopHeartbeat() {
|
|
||||||
if (heartbeatTimer) {
|
|
||||||
clearInterval(heartbeatTimer)
|
|
||||||
heartbeatTimer = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自动断开连接,清理资源
|
|
||||||
onUnmounted(() => {
|
|
||||||
disconnect()
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
connect,
|
|
||||||
disconnect,
|
|
||||||
sendMessage,
|
|
||||||
messages,
|
|
||||||
isConnected,
|
|
||||||
generateConversationNo
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
import BaseConversation from '../model/BaseConversation'
|
|
||||||
import BaseMessage from '../model/BaseMessage'
|
|
||||||
import { ChatConversation } from '../model/ChatConversation'
|
|
||||||
import ImageMessage from '../model/ImageMessage'
|
|
||||||
import TextMessage from '../model/TextMessage'
|
|
||||||
|
|
||||||
export enum MessageRole {
|
|
||||||
SELF = 1,
|
|
||||||
SYSTEM = 2,
|
|
||||||
OTHER = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum SendStatus {
|
|
||||||
FAILURE = 1,
|
|
||||||
SENDING = 2,
|
|
||||||
SUCCESS = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ContentType {
|
|
||||||
TEXT = 101,
|
|
||||||
IMAGE = 102,
|
|
||||||
AUDIO = 103,
|
|
||||||
SYSTEM = 1400
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enum MENU_LIST_ENUM {
|
|
||||||
CONVERSATION = 1,
|
|
||||||
FRIENDS = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enum CONVERSATION_TYPE {
|
|
||||||
SINGLE = 1,
|
|
||||||
GROUP = 3,
|
|
||||||
NOTIFICATION = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum WEBSOCKET_MESSAGE_TYPE_ENUM {
|
|
||||||
IM_MESSAGE_RECEIVE = 'im-message-receive',
|
|
||||||
IM_CONVERSATION_ADD = 'im-conversation-add'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export type MessageModelType = BaseMessage | TextMessage | ImageMessage
|
|
||||||
export type ConversationModelType = BaseConversation | ChatConversation
|
|
||||||
export type ConversationType = CONVERSATION_TYPE
|
|
||||||
|
|
||||||
export type ImMessageReceiveResponse = {
|
|
||||||
type: WEBSOCKET_MESSAGE_TYPE_ENUM
|
|
||||||
content: string
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ImMessageContent = {
|
|
||||||
id: number;
|
|
||||||
conversationType: number;
|
|
||||||
senderId: number;
|
|
||||||
senderNickname: string;
|
|
||||||
senderAvatar: string;
|
|
||||||
receiverId: number;
|
|
||||||
contentType: number;
|
|
||||||
content: string;
|
|
||||||
sendTime: number; // Use `Date` if you'd prefer the time to be a `Date` object
|
|
||||||
sequence: number;
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue