✨新增聊天功能
This commit is contained in:
parent
e6f98d4a2d
commit
c1bfaabcb2
@ -33,6 +33,7 @@
|
|||||||
"md-editor-v3": "^1.11.11",
|
"md-editor-v3": "^1.11.11",
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
"pinia": "^2.1.6",
|
"pinia": "^2.1.6",
|
||||||
|
"pinia-plugin-persistedstate": "^3.2.0",
|
||||||
"qrcodejs2-fixes": "^0.0.2",
|
"qrcodejs2-fixes": "^0.0.2",
|
||||||
"qs": "^6.11.0",
|
"qs": "^6.11.0",
|
||||||
"sortablejs": "^1.15.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 SvgIcon from '@/components/SvgIcon/index.vue'
|
||||||
import UploadImage from '@/components/ImageUpload.vue'
|
import UploadImage from '@/components/ImageUpload.vue'
|
||||||
import FileUpload from '@/components/FileUpload'
|
import UploadFile from '@/components/FileUpload'
|
||||||
import ImagePreview from '@/components/ImagePreview'
|
import ImagePreview from '@/components/ImagePreview'
|
||||||
import DictTag from '@/components/DictTag'
|
import DictTag from '@/components/DictTag'
|
||||||
import Dialog from '@/components/Dialog'
|
import Dialog from '@/components/Dialog'
|
||||||
@ -10,7 +10,7 @@ declare module '@vue/runtime-core' {
|
|||||||
UploadImage: typeof UploadImage,
|
UploadImage: typeof UploadImage,
|
||||||
DictTag: typeof DictTag,
|
DictTag: typeof DictTag,
|
||||||
ImagePreview: typeof ImagePreview,
|
ImagePreview: typeof ImagePreview,
|
||||||
FileUpload: typeof FileUpload,
|
UploadFile: typeof UploadFile,
|
||||||
ZrDialog: typeof Dialog
|
ZrDialog: typeof Dialog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -13,7 +13,7 @@
|
|||||||
<el-badge :is-dot="newsDot" class="item"> 通知 </el-badge>
|
<el-badge :is-dot="newsDot" class="item"> 通知 </el-badge>
|
||||||
</template>
|
</template>
|
||||||
<div class="content-box">
|
<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>
|
<el-icon :size="30" color="#409EFF"><bell /></el-icon>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="title">{{ item.noticeTitle }}</div>
|
<div class="title">{{ item.noticeTitle }}</div>
|
||||||
@ -23,22 +23,50 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-tab-pane>
|
</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>
|
</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>
|
</div>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
|
|
||||||
<el-dialog :title="info.noticeTitle" v-model="show" append-to-body>
|
<el-dialog draggable v-model="show" append-to-body>
|
||||||
<div v-html="info.noticeContent"></div>
|
<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>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup name="noticeIndex">
|
<script setup name="noticeIndex">
|
||||||
|
import msgList from '@/views/components/msgList.vue'
|
||||||
import useSocketStore from '@/store/modules/socket'
|
import useSocketStore from '@/store/modules/socket'
|
||||||
|
import useUserStore from '@/store/modules/user'
|
||||||
import { dayjs } from 'element-plus'
|
import { dayjs } from 'element-plus'
|
||||||
|
import { formatTime } from '@/utils/index'
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
const noticeType = ref('0')
|
const noticeType = ref('0')
|
||||||
// 小红点
|
// 小红点
|
||||||
@ -50,10 +78,17 @@ const noticeList = computed(() => {
|
|||||||
const noticeDot = computed(() => {
|
const noticeDot = computed(() => {
|
||||||
return useSocketStore().noticeDot
|
return useSocketStore().noticeDot
|
||||||
})
|
})
|
||||||
|
const chatList = computed(() => {
|
||||||
|
return useSocketStore().getSessionList(useUserStore().userId)
|
||||||
|
})
|
||||||
const info = ref({})
|
const info = ref({})
|
||||||
function handleDetails(item) {
|
function handleDetails(item, type) {
|
||||||
show.value = true
|
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() {
|
function onAllReadClick() {
|
||||||
@ -67,24 +102,24 @@ function onGoToGiteeClick() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.head-box {
|
// .head-box {
|
||||||
display: flex;
|
// display: flex;
|
||||||
border-bottom: 1px solid #ebeef5;
|
// border-bottom: 1px solid #ebeef5;
|
||||||
box-sizing: border-box;
|
// box-sizing: border-box;
|
||||||
color: #333333;
|
// color: #333333;
|
||||||
justify-content: space-between;
|
// justify-content: space-between;
|
||||||
height: 35px;
|
// height: 35px;
|
||||||
align-items: center;
|
// align-items: center;
|
||||||
.head-box-btn {
|
// .head-box-btn {
|
||||||
color: #1890ff;
|
// color: #1890ff;
|
||||||
font-size: 13px;
|
// font-size: 13px;
|
||||||
cursor: pointer;
|
// cursor: pointer;
|
||||||
opacity: 0.8;
|
// opacity: 0.8;
|
||||||
&:hover {
|
// &:hover {
|
||||||
opacity: 1;
|
// opacity: 1;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
.content-box {
|
.content-box {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
min-height: 60px;
|
min-height: 60px;
|
||||||
@ -102,6 +137,13 @@ function onGoToGiteeClick() {
|
|||||||
&:last-of-type {
|
&:last-of-type {
|
||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
.content {
|
||||||
|
margin-left: 8px;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
.icon {
|
.icon {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
@ -132,7 +174,7 @@ function onGoToGiteeClick() {
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: space-around;
|
||||||
border-top: 1px solid #ebeef5;
|
border-top: 1px solid #ebeef5;
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|||||||
@ -51,18 +51,19 @@ export default {
|
|||||||
})
|
})
|
||||||
// 接收聊天数据
|
// 接收聊天数据
|
||||||
connection.on('receiveChat', (data) => {
|
connection.on('receiveChat', (data) => {
|
||||||
const title = `来自${data.userName}的消息通知`
|
const { fromUser, message } = data
|
||||||
|
|
||||||
useSocketStore().setChat(data)
|
useSocketStore().setChat(data)
|
||||||
|
|
||||||
if (data.userid != useUserStore().userId) {
|
if (data.userid != useUserStore().userId) {
|
||||||
ElNotification({
|
ElNotification({
|
||||||
title: title,
|
title: fromUser.nickName,
|
||||||
message: data.message,
|
message: message,
|
||||||
type: 'success',
|
type: 'success',
|
||||||
duration: 3000
|
duration: 3000
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
webNotify({ title: title, body: data.message })
|
webNotify({ title: fromUser.nickName, body: message })
|
||||||
})
|
})
|
||||||
|
|
||||||
connection.on('onlineInfo', (data) => {
|
connection.on('onlineInfo', (data) => {
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
const store = createPinia()
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||||
|
|
||||||
|
const store = createPinia()
|
||||||
|
store.use(piniaPluginPersistedstate)
|
||||||
export default store
|
export default store
|
||||||
@ -1,4 +1,9 @@
|
|||||||
|
import useUserStore from './user'
|
||||||
|
import signalR from '@/signalr/signalr'
|
||||||
const useSocketStore = defineStore('socket', {
|
const useSocketStore = defineStore('socket', {
|
||||||
|
persist: {
|
||||||
|
paths: ['chatMessage', 'chatList', 'sessionList'] //存储指定key
|
||||||
|
},
|
||||||
state: () => ({
|
state: () => ({
|
||||||
onlineNum: 0,
|
onlineNum: 0,
|
||||||
onlineUsers: [],
|
onlineUsers: [],
|
||||||
@ -7,9 +12,24 @@ const useSocketStore = defineStore('socket', {
|
|||||||
//在线用户信息
|
//在线用户信息
|
||||||
onlineInfo: {},
|
onlineInfo: {},
|
||||||
// 聊天数据
|
// 聊天数据
|
||||||
chatList: [],
|
chatList: {},
|
||||||
leaveUser: {}
|
leaveUser: {},
|
||||||
|
sessionList: {},
|
||||||
|
newChat: 0
|
||||||
}),
|
}),
|
||||||
|
getters: {
|
||||||
|
/**
|
||||||
|
* 返回当前会话的消息
|
||||||
|
* @param {*} state
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getMessageList(state) {
|
||||||
|
return (conversationId) => state.chatList[conversationId]
|
||||||
|
},
|
||||||
|
getSessionList(state) {
|
||||||
|
return (userid) => state.sessionList[userid] || []
|
||||||
|
}
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
//更新在线人数
|
//更新在线人数
|
||||||
setOnlineUserNum(num) {
|
setOnlineUserNum(num) {
|
||||||
@ -32,7 +52,44 @@ const useSocketStore = defineStore('socket', {
|
|||||||
this.onlineInfo = data
|
this.onlineInfo = data
|
||||||
},
|
},
|
||||||
setChat(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: '消息内容不能为空'
|
inputErrorMessage: '消息内容不能为空'
|
||||||
})
|
})
|
||||||
.then(({ value }) => {
|
.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())
|
console.error(err.toString())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user