✨新增聊天功能
This commit is contained in:
parent
e6f98d4a2d
commit
c1bfaabcb2
@ -33,6 +33,7 @@
|
||||
"md-editor-v3": "^1.11.11",
|
||||
"nprogress": "0.2.0",
|
||||
"pinia": "^2.1.6",
|
||||
"pinia-plugin-persistedstate": "^3.2.0",
|
||||
"qrcodejs2-fixes": "^0.0.2",
|
||||
"qs": "^6.11.0",
|
||||
"sortablejs": "^1.15.0",
|
||||
|
||||
4
src/components.d.ts
vendored
4
src/components.d.ts
vendored
@ -1,6 +1,6 @@
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import UploadImage from '@/components/ImageUpload.vue'
|
||||
import FileUpload from '@/components/FileUpload'
|
||||
import UploadFile from '@/components/FileUpload'
|
||||
import ImagePreview from '@/components/ImagePreview'
|
||||
import DictTag from '@/components/DictTag'
|
||||
import Dialog from '@/components/Dialog'
|
||||
@ -10,7 +10,7 @@ declare module '@vue/runtime-core' {
|
||||
UploadImage: typeof UploadImage,
|
||||
DictTag: typeof DictTag,
|
||||
ImagePreview: typeof ImagePreview,
|
||||
FileUpload: typeof FileUpload,
|
||||
UploadFile: typeof UploadFile,
|
||||
ZrDialog: typeof Dialog
|
||||
}
|
||||
}
|
||||
@ -13,7 +13,7 @@
|
||||
<el-badge :is-dot="newsDot" class="item"> 通知 </el-badge>
|
||||
</template>
|
||||
<div class="content-box">
|
||||
<div class="content-box-item" v-for="item in noticeList" @click="handleDetails(item)">
|
||||
<div class="content-box-item" v-for="item in noticeList" @click="handleDetails(item, 0)">
|
||||
<el-icon :size="30" color="#409EFF"><bell /></el-icon>
|
||||
<div class="content">
|
||||
<div class="title">{{ item.noticeTitle }}</div>
|
||||
@ -23,22 +23,50 @@
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="私信" name="1"> <div class="content-box"></div></el-tab-pane>
|
||||
<el-tab-pane name="1">
|
||||
<template #label>
|
||||
<el-badge :value="chatList.length"> 私信 </el-badge>
|
||||
</template>
|
||||
<div class="content-box">
|
||||
<div class="content-box-item" v-for="item in chatList" @click="handleDetails(item, 1)">
|
||||
<el-avatar :src="item.fromUser.avatar"></el-avatar>
|
||||
<div class="content">
|
||||
<div class="title">
|
||||
<span class="name">{{ item.fromUser.nickName }}</span>
|
||||
说:{{ item.message }}
|
||||
</div>
|
||||
<div class="content-box-time">{{ formatTime(item.chatTime) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<div class="foot-box" @click="onGoToGiteeClick" v-if="noticeList.length > 0">前往通知中心</div>
|
||||
<div class="foot-box">
|
||||
<div @click="onGoToGiteeClick" v-if="noticeList.length > 0">前往通知中心</div>
|
||||
<div>全部已读</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
|
||||
<el-dialog :title="info.noticeTitle" v-model="show" append-to-body>
|
||||
<div v-html="info.noticeContent"></div>
|
||||
<el-dialog draggable v-model="show" append-to-body>
|
||||
<template #header> {{ info.title }} </template>
|
||||
<template v-if="info">
|
||||
<div v-if="info.type == 0">
|
||||
<div v-html="info.item.noticeContent"></div>
|
||||
</div>
|
||||
<msgList v-if="info.type == 1" v-model="info.userId"> </msgList>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="noticeIndex">
|
||||
import msgList from '@/views/components/msgList.vue'
|
||||
import useSocketStore from '@/store/modules/socket'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import { dayjs } from 'element-plus'
|
||||
import { formatTime } from '@/utils/index'
|
||||
const { proxy } = getCurrentInstance()
|
||||
const noticeType = ref('0')
|
||||
// 小红点
|
||||
@ -50,10 +78,17 @@ const noticeList = computed(() => {
|
||||
const noticeDot = computed(() => {
|
||||
return useSocketStore().noticeDot
|
||||
})
|
||||
const chatList = computed(() => {
|
||||
return useSocketStore().getSessionList(useUserStore().userId)
|
||||
})
|
||||
const info = ref({})
|
||||
function handleDetails(item) {
|
||||
function handleDetails(item, type) {
|
||||
show.value = true
|
||||
info.value = item
|
||||
if (type == 0) {
|
||||
info.value = { type, item, title: item.noticeTitle }
|
||||
} else if (type == 1) {
|
||||
info.value = { type, title: item.fromUser.nickName, userId: item.userId }
|
||||
}
|
||||
}
|
||||
// 全部已读点击
|
||||
function onAllReadClick() {
|
||||
@ -67,24 +102,24 @@ function onGoToGiteeClick() {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.head-box {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
box-sizing: border-box;
|
||||
color: #333333;
|
||||
justify-content: space-between;
|
||||
height: 35px;
|
||||
align-items: center;
|
||||
.head-box-btn {
|
||||
color: #1890ff;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// .head-box {
|
||||
// display: flex;
|
||||
// border-bottom: 1px solid #ebeef5;
|
||||
// box-sizing: border-box;
|
||||
// color: #333333;
|
||||
// justify-content: space-between;
|
||||
// height: 35px;
|
||||
// align-items: center;
|
||||
// .head-box-btn {
|
||||
// color: #1890ff;
|
||||
// font-size: 13px;
|
||||
// cursor: pointer;
|
||||
// opacity: 0.8;
|
||||
// &:hover {
|
||||
// opacity: 1;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
.content-box {
|
||||
font-size: 13px;
|
||||
min-height: 60px;
|
||||
@ -102,6 +137,13 @@ function onGoToGiteeClick() {
|
||||
&:last-of-type {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
.content {
|
||||
margin-left: 8px;
|
||||
|
||||
.name {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
@ -132,7 +174,7 @@ function onGoToGiteeClick() {
|
||||
opacity: 0.8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: space-around;
|
||||
border-top: 1px solid #ebeef5;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
|
||||
@ -51,18 +51,19 @@ export default {
|
||||
})
|
||||
// 接收聊天数据
|
||||
connection.on('receiveChat', (data) => {
|
||||
const title = `来自${data.userName}的消息通知`
|
||||
const { fromUser, message } = data
|
||||
|
||||
useSocketStore().setChat(data)
|
||||
|
||||
if (data.userid != useUserStore().userId) {
|
||||
ElNotification({
|
||||
title: title,
|
||||
message: data.message,
|
||||
title: fromUser.nickName,
|
||||
message: message,
|
||||
type: 'success',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
webNotify({ title: title, body: data.message })
|
||||
webNotify({ title: fromUser.nickName, body: message })
|
||||
})
|
||||
|
||||
connection.on('onlineInfo', (data) => {
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
const store = createPinia()
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
|
||||
export default store
|
||||
const store = createPinia()
|
||||
store.use(piniaPluginPersistedstate)
|
||||
export default store
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
import useUserStore from './user'
|
||||
import signalR from '@/signalr/signalr'
|
||||
const useSocketStore = defineStore('socket', {
|
||||
persist: {
|
||||
paths: ['chatMessage', 'chatList', 'sessionList'] //存储指定key
|
||||
},
|
||||
state: () => ({
|
||||
onlineNum: 0,
|
||||
onlineUsers: [],
|
||||
@ -7,9 +12,24 @@ const useSocketStore = defineStore('socket', {
|
||||
//在线用户信息
|
||||
onlineInfo: {},
|
||||
// 聊天数据
|
||||
chatList: [],
|
||||
leaveUser: {}
|
||||
chatList: {},
|
||||
leaveUser: {},
|
||||
sessionList: {},
|
||||
newChat: 0
|
||||
}),
|
||||
getters: {
|
||||
/**
|
||||
* 返回当前会话的消息
|
||||
* @param {*} state
|
||||
* @returns
|
||||
*/
|
||||
getMessageList(state) {
|
||||
return (conversationId) => state.chatList[conversationId]
|
||||
},
|
||||
getSessionList(state) {
|
||||
return (userid) => state.sessionList[userid] || []
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
//更新在线人数
|
||||
setOnlineUserNum(num) {
|
||||
@ -32,7 +52,44 @@ const useSocketStore = defineStore('socket', {
|
||||
this.onlineInfo = data
|
||||
},
|
||||
setChat(data) {
|
||||
this.chatList.push(data)
|
||||
const userStore = useUserStore()
|
||||
var selfUserId = userStore.userId
|
||||
|
||||
var sessionId = data.toUserId
|
||||
if (data.userId != selfUserId) {
|
||||
sessionId = data.userId
|
||||
}
|
||||
var obj = this.chatList[sessionId]
|
||||
if (obj && obj.length >= 50) {
|
||||
this.chatList[sessionId].shift()
|
||||
}
|
||||
if (obj == null || obj == undefined) {
|
||||
this.chatList[sessionId] = []
|
||||
}
|
||||
// 判断消息是否是自己发的
|
||||
data.self = data.userId == selfUserId
|
||||
this.chatList[sessionId].push(data)
|
||||
if (selfUserId == data.userId) return
|
||||
|
||||
if (this.sessionList[selfUserId] == undefined) {
|
||||
this.sessionList[selfUserId] = []
|
||||
}
|
||||
var index = this.getSessionList(selfUserId).findIndex((x) => x.userId == data.userId)
|
||||
this.getSessionList(selfUserId).splice(index, 1)
|
||||
this.getSessionList(selfUserId).push(data)
|
||||
},
|
||||
sendChat(data) {
|
||||
// console.log(JSON.stringify(data))
|
||||
return new Promise((resolve, reject) => {
|
||||
signalR.SR.invoke('sendMessage', data.toUserId, data.message)
|
||||
.then(() => {
|
||||
resolve(true)
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(false)
|
||||
console.error(err.toString())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
179
src/views/components/msgList.vue
Normal file
179
src/views/components/msgList.vue
Normal file
@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<div style="height: 400px">
|
||||
<div class="message_content">
|
||||
<el-scrollbar height="100%" ref="scrollContainer">
|
||||
<template v-for="item in conversionMsgList">
|
||||
<div class="talk_item talk_primary" v-if="item.self">
|
||||
<div class="head">
|
||||
<el-avatar shape="square"> 我 </el-avatar>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="bubble">{{ item.message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="talk_item talk_other" v-else>
|
||||
<div class="head">
|
||||
<el-avatar :src="item.fromUser.avatar" shape="square" />
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="bubble">{{ item.message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<div class="talk_bottom">
|
||||
<div class="talk_area">
|
||||
<textarea class="textarea" placeholder="请输入聊天内容" @keyup.enter="handleSend" v-model="content"></textarea>
|
||||
|
||||
<div class="talk_btn">
|
||||
<el-button type="success" size="default" @click="handleSend">发送</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="msglist">
|
||||
import useSocketStore from '@/store/modules/socket'
|
||||
const socketStore = useSocketStore()
|
||||
const { proxy } = getCurrentInstance()
|
||||
const props = defineProps({
|
||||
modelValue: {}
|
||||
})
|
||||
const conversionMsgList = computed(() => {
|
||||
return socketStore.getMessageList(props.modelValue)
|
||||
})
|
||||
const content = ref('')
|
||||
function handleSend() {
|
||||
if (content.value.trim().length <= 0) {
|
||||
proxy.$modal.msgError('请输入聊天内容')
|
||||
return
|
||||
}
|
||||
var obj = {
|
||||
toUserId: props.modelValue,
|
||||
message: content.value
|
||||
}
|
||||
socketStore
|
||||
.sendChat(obj)
|
||||
.then(() => {
|
||||
content.value = ''
|
||||
scrollBottom()
|
||||
})
|
||||
.catch(() => {
|
||||
proxy.$modal.msgError('发送失败')
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 滚动聊天记录
|
||||
* @param {*} type
|
||||
*/
|
||||
function scrollBottom(type) {
|
||||
setTimeout(() => {
|
||||
const scrollWrapper = computed(() => proxy.$refs.scrollContainer.$refs.wrapRef)
|
||||
const height = scrollWrapper.value.scrollHeight
|
||||
|
||||
if (type == 1) {
|
||||
scrollWrapper.value.scrollTop = height
|
||||
} else {
|
||||
scrollWrapper.value.scrollTo({ top: height + 120, behavior: 'smooth' })
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
scrollBottom(1)
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.message_content {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
border-top: 1px solid #ddd;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.talk_item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: auto;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.head {
|
||||
display: block;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
img {
|
||||
border-radius: 0.3rem;
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
margin-right: 0.4rem;
|
||||
max-width: 70%;
|
||||
|
||||
.bubble {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
font-size: 1rem;
|
||||
margin-right: 0.2rem;
|
||||
border-radius: 0.4rem;
|
||||
background: var(--el-color-success);
|
||||
color: #fff;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.talk_primary {
|
||||
flex-direction: row-reverse;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.talk_item:after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.talk_bottom {
|
||||
border-top: 1px solid #ddd;
|
||||
|
||||
.talk_btn {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
padding: 0 10px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
.talk_area {
|
||||
position: relative;
|
||||
height: 100px;
|
||||
|
||||
.textarea {
|
||||
padding: 10px 0.2rem;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
resize: none;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
height: 100%;
|
||||
}
|
||||
.textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -111,7 +111,7 @@ function onChat(item) {
|
||||
inputErrorMessage: '消息内容不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
proxy.signalr.SR.invoke('sendMessage', item.connnectionId, item.userid, value).catch(function (err) {
|
||||
proxy.signalr.SR.invoke('sendMessage', item.userid, value).catch(function (err) {
|
||||
console.error(err.toString())
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user