使用LeanCloud平台的即时通讯

news2024/11/25 12:37:47

LeanCloud 是领先的 Serverless 云服务,为产品开发提供强有力的后端支持,旨在帮助开发者降低研发、运营维护等阶段投入的精力和成本。 LeanCloud 整合了各项服务,让开发者能够聚焦在核心业务上,为客户创造更多价值。

*即时通讯

LeanCloud地址

效果图

在这里插入图片描述

安装

npm install leancloud-realtime leancloud-realtime-plugin-typed-messages leancloud-storage --save

简单封装ChatComponent.vue

<template>
    <div class="chat">
        <div class="userList">
            <ul>
                <li v-for="item in userList" :class="{ active: item === adverse }" :key="item" @click="selectUser(item)">{{ item }}</li>
            </ul>
        </div>
        <div class="instant-messaging">
            <h3>{{ adverse }}</h3>
            <ul v-show="newMessage.length !== 0" ref="scrollContainer" id="scrollContainer" class="scroll-container">
                <li v-for="item in newMessage" :key="item" class="message-item"
                    :style="`justify-content: ${item.from !== creator ? 'flex-start' : 'flex-end'};`">
                    <div v-if="item.from !== creator" class="message-content">
                        <div class="user">
                            <img :src="'https://img0.baidu.com/it/u=2226630510,461838410&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=521'"
                                alt="" />
                        </div>
                        <div class="message image" :style="'padding: 6px;'" v-if="item.content._lctype === -2">
                            <img :src="item.content._lcfile.url" alt="" />
                        </div>
                        <div class="message" v-if="item.content._lctype === -1">{{ item.content._lctext }}</div>
                    </div>
                    <div v-else class="message-content">
                        <div class="message image" :style="'padding: 6px;'" v-if="item.content._lctype === -2">
                            <img :src="item.content._lcfile.url" alt="" />
                        </div>
                        <div class="message" v-if="item.content._lctype === -1">{{ item.content._lctext }}</div>
                        <div class="user">
                            <img :src="'https://img1.baidu.com/it/u=3622150954,2575811681&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=500'"
                                alt="" />
                        </div>
                    </div>
                </li>
            </ul>
            <ul v-show="newMessage.length === 0">
                <li>暂无消息</li>
            </ul>
            <div class="message-operation">
                <!-- <el-button type="primary" @click="moreMessage">更多</el-button> -->
                <el-upload v-model:file-list="fileList" ref="uploadRef" class="upload-demo" multiple :limit="3"
                    :auto-upload="false" :show-file-list="false" :accept="'image/*'">
                    <el-button :icon="Picture" />
                </el-upload>
                <el-input class="message-input" v-model="messageValue" @keyup.enter="sendMessage" style="width: 240px"
                    placeholder="" />
                <el-button type="primary" @click="sendMessage">发送</el-button>
            </div>
        </div>
    </div>
</template>

<script setup>
import * as IM from 'leancloud-realtime'
import AV from 'leancloud-storage'
import initPlugin from 'leancloud-realtime-plugin-typed-messages'
import { onMounted, ref, watch, onBeforeUnmount } from 'vue'
import { ElMessage } from 'element-plus'
import { Picture } from '@element-plus/icons-vue'
import axios from 'axios'

const props = defineProps({
    // 自己的 id
    ownId: {
        type: String,
        default: ''
    },
    // 对方的 id
    adverseId: {
        type: String,
        default: ''
    },
    // 聊天室名称
    chatsName: {
        type: String,
        default: ''
    },
})

const { Realtime, TextMessage, Event } = IM
const { TypedMessagesPlugin, ImageMessage } = initPlugin(AV, IM)
let realtime;

const newMessage = ref([])
const creator = ref('')
const adverse = ref('')
const messageValue = ref('')
const fileList = ref([])
const uploadRef = ref(null)
const scrollContainer = ref(null)

// 监听文件上传
watch(
    () => fileList.value.length,
    () => {
        console.log(fileList.value)
        fileList.value.length !== 0 && sendMessage()
    }
)

// 发送消息
const sendMessage = () => {
    console.log(messageValue.value === '' && fileList.value.length === 0)
    if (messageValue.value === '' && fileList.value.length === 0) {
        ElMessage({
            message: '不能发送空消息!',
            type: 'warning'
        })
        return
    }
    /**
     * 创建一个 IMClient 实例
     */
    // 1、Tom 用自己的名字作为 clientId 来登录即时通讯服务
    realtime
        .createIMClient(props.ownId)
        .then(function (own) {
            // 成功登录
            console.log('登录成功', own)
            // 创建与 Jerry 之间的对话
            own.createConversation({
                // tom 是一个 IMClient 实例
                // 指定对话的成员除了当前用户 Tom(SDK 会默认把当前用户当做对话成员)之外,还有 Jerry
                members: [props.adverseId],
                // 对话名称
                name: props.chatsName,
                unique: true
            }).then(function (conversation) {
                // 创建成功
                console.log('创建成功', conversation)
                // 发送一条文本消息
                if (messageValue.value !== '') {
                    conversation
                        .send(new TextMessage(messageValue.value))
                        .then(function (message) {
                            console.log('发送成功!')
                            createUser_Jerry()
                            messageValue.value = ''
                        })
                        .catch(console.error)
                }

                if (fileList.value.length !== 0) {
                    var file = new AV.File('avatar.jpg', fileList.value[0].raw)
                    file
                        .save()
                        .then(function () {
                            var message = new ImageMessage(file)
                            message.setText('本地图片')
                            message.setAttributes({ location: '武汉' })
                            return conversation.send(message)
                        })
                        .then(function () {
                            console.log('发送成功')
                            createUser_Jerry()
                            fileList.value = []
                        })
                        .catch(console.error.bind(console))
                }
            })
        })
        .catch(console.error)
}

const createUser_Jerry = () => {
    // Jerry 登录
    realtime
        .createIMClient(props.ownId)
        .then(function (own) {
            // 成功登录
            console.log('登录成功', own)
            creator.value = own.id
            // 确保 Jerry 已加入对话
            own.createConversation({
                members: [props.adverseId],
                name: props.chatsName,
                unique: true
            }).then(function (conversation) {
                // 创建成功
                console.log('创建成功', conversation)
                adverse.value = conversation.members.find((item) => item !== conversation.creator)
                // 当前用户被添加至某个对话
                own.on(Event.INVITED, function invitedEventHandler(payload, conversation) {
                    console.log(payload.invitedBy, conversation.id)
                })
                // 当前用户收到了某一条消息,可以通过响应 Event.MESSAGE 这一事件来处理。
                own.on(Event.MESSAGE, function (message, conversation) {
                    // newMessage.value.push(message.text)
                    console.log('收到新消息:' + message.text, conversation)
                    getChatRecord(conversation)
                })

                getChatRecord(conversation)
            })
        })
        .catch(console.error)
}

let messageIterator
// 获取聊天记录
const getChatRecord = (conversation) => {
    // conversation
    //     .queryMessages({
    //         limit: 10, // limit 取值范围 1~100,默认 20
    //     })
    //     .then(function (messages) {
    //         // 最新的十条消息,按时间增序排列
    //         newMessage.value = messages
    //     })
    //     .catch(console.error.bind(console));

    // JS SDK 通过迭代器隐藏了翻页的实现细节,开发者通过不断的调用 next 方法即可获得后续数据。
    // 创建一个迭代器,每次获取 10 条历史消息
    messageIterator = conversation.createMessagesIterator({ limit: 50 })
    // 第一次调用 next 方法,获得前 10 条消息,还有更多消息,done 为 false
    messageIterator
        .next()
        .then(function (result) {
            // result: {
            //   value: [message1, ..., message10],
            //   done: false,
            // }
            console.log(result)
            newMessage.value = result.value
            scrollContainer.value.scrollTop = scrollContainer.value.scrollHeight
        })
        .catch(console.error.bind(console))
}

// 更多消息
const moreMessage = () => {
    // 第二次调用 next 方法,获得第 11~20 条消息,还有更多消息,done 为 false
    // 迭代器内部会记录起始消息的数据,无需开发者显示指定
    messageIterator
        .next()
        .then(function (result) {
            // result: {
            //   value: [message21, ..., message30],
            //   done: true,
            // }
            if (result.value.length === 0) {
                ElMessage.warning('没有更多消息了')
                return
            }
            //   newMessage.value = [...result.value, ...newMessage.value]
            newMessage.value = result.value.concat(newMessage.value)
        })
        .catch(console.error.bind(console))
}

let isScrollLocked = true
// 滚动事件
const handleScroll = () => {
    if (!isScrollLocked && isNearTop(scrollContainer.value)) {
        isScrollLocked = true // 锁定滚动条位置
        const ulContainer = document.getElementById('scrollContainer')
        console.log(ulContainer.scrollHeight)
        setTimeout(() => {
            moreMessage()
            isScrollLocked = false
        }, 1000)
    } else {
        setTimeout(() => {
            isScrollLocked = false
        }, 1000);
    }
}

/* 节流 */
const throttle = (func, delay) => {
    let time = null
    return function () {
        let args = Array.from(arguments)
        if (time === null) {
            time = setTimeout(() => {
                func(...args)
                clearTimeout(time)
                time = null
            }, delay)
        }
    }
}

// 是否到达顶部
const isNearTop = (scrollContainer) => {
    const threshold = 50 // 可调整的阈值,距离底部多少像素时触发加载
    return scrollContainer.scrollTop <= threshold
}

// 延迟一段时间后再获取滚动高度
const checkAndSetScrollTop = () => {
    const ulContainer = document.getElementById('scrollContainer')

    // 确保滚动高度不为零
    if (ulContainer.scrollHeight !== 0) {
        // 将滚动条设置到最底部
        ulContainer.scrollTop = ulContainer.scrollHeight
    } else {
        // 如果滚动高度仍为0,再延迟几秒重新检查
        setTimeout(checkAndSetScrollTop, 500) // 这里的延迟时间可以根据实际情况调整
    }
}

const userList = ref([])
// 获取会话
const getConversation = () => {
    axios({
        method: 'get',
        url: `/1.2/rtm/conversations?limit=10&where={"c": "${props.ownId}"}`,
        headers: {
            'X-LC-Id': 'NLblIbY4gEKenESSb7Q3vY9Y-gzGzoHsz',
            'X-LC-Key': 'l1JvznaAgLvkGumBdiXDYQ6q,master',
            'Content-Type': 'application/json'
        }
    })
        .then(function (response) {
            console.log(response.data);
            userList.value = response.data.results.map(item => {
                return item.m.find(v => v !== item.c)
            })
            console.log(userList.value);
        });
}
const emit = defineEmits(['selectUser'])
// 选择用户
const selectUser = (user) => {
    emit('selectUser', user)
}

// 监听props.adverseId
watch(() => props.adverseId,
    () => {
        createUser_Jerry()
        getConversation()
        getUnreader()
    }
)

// 获取未读消息
const getUnreader = () => {
    axios({
        method: 'get',
        url: `/1.2/rtm/clients/${props.ownId}/unread-count`,
        headers: {
            'X-LC-Id': 'NLblIbY4gEKenESSb7Q3vY9Y-gzGzoHsz',
            'X-LC-Key': 'l1JvznaAgLvkGumBdiXDYQ6q,master',
            'Content-Type': 'application/json'
        }
    })
        .then(function (response) {
            console.log(response.data);
            if (response.data.unread > 0) {
                ElMessage.warning('有新消息')
            }
        });
}

onMounted(() => {
    realtime = new Realtime({
        appId: 'NLblIbY4gEKenESSb7Q3vY9Y-gzGzoHsz',
        appKey: 'pVpG3Q8DWAnWDWUquOb5cBu0',
        server: 'https://nlbliby4.lc-cn-n1-shared.com',
        // 初始化即时通讯服务时需要指定富媒体消息插件
        plugins: [TypedMessagesPlugin]
    })
    localStorage.setItem('debug', 'LC*')

    AV.init({
        appId: 'NLblIbY4gEKenESSb7Q3vY9Y-gzGzoHsz',
        appKey: 'pVpG3Q8DWAnWDWUquOb5cBu0',
        serverURL: 'https://nlbliby4.lc-cn-n1-shared.com'
    })
    createUser_Jerry()

    getConversation()
    getUnreader()

    checkAndSetScrollTop() // 延迟一段时间后再获取滚动高度
    scrollContainer.value.addEventListener('scroll', throttle(handleScroll, 1000))
})

onBeforeUnmount(() => {
    scrollContainer.value.removeEventListener('scroll', handleScroll)
})
</script>

<style lang="scss" scoped>
* {

    ul,
    li {
        list-style: none;
        padding: 0;
        margin: 0;
    }
}

.chat {
    display: flex;

    .userList {
        border: 1px solid #e3e3e3;
        width: 16%;

        ul {
            li {
                line-height: 36px;
                background: #e3e3e3;
                padding: 0 10px;
                margin-bottom: 1px;
                cursor: pointer;
            }
            li:hover{
                background: #cacaca;
            }
            .active{
                background: #c1c1c1;
            }
        }
    }

    .instant-messaging {
        flex: 1;
        background: #f1f1f1;
        padding: 20px;

        h3 {
            text-align: center;
        }

        .scroll-container {
            height: 580px;
            overflow-y: auto;

            &::-webkit-scrollbar {
                /*滚动条整体样式*/
                width: 10px;
                /*高宽分别对应横竖滚动条的尺寸*/
                height: 10px;
            }

            &::-webkit-scrollbar-thumb {
                /*滚动条里面小方块*/
                border-radius: 10px;
                background: #ccc;
                box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
            }

            .message-item {
                display: flex;
                margin-bottom: 10px;

                .message-content {
                    display: flex;
                    align-items: center;

                    .user {
                        width: 36px;
                        height: 36px;
                        margin-right: 10px;
                        border-radius: 50px;

                        img {
                            width: 100%;
                            height: 100%;
                        }
                    }

                    .message {
                        line-height: 36px;
                        background: #fff;
                        padding: 0 6px;
                        margin-right: 10px;
                        display: flex;
                        justify-content: center;
                    }

                    .image {
                        max-width: 100px;
                        height: 52px;

                        img {
                            width: 100%;
                            height: 100%;
                        }
                    }
                }
            }
        }

        .message-operation {
            display: flex;
            justify-content: flex-end;

            .message-input {
                margin: 0 20px;
            }
        }
    }
}
</style>

优化后的封装

<template>
    <div class="chat">
        <div class="userList">
            <ul>
                <li v-for="item in userList" :class="{ active: item === adverse }" :key="item"
                    @click="selectUser(item)">{{ item }}</li>
            </ul>
        </div>
        <div class="instant-messaging">
            <h3>{{ adverse }}</h3>
            <el-scrollbar v-show="newMessage.length !== 0" class="scroll-container" ref="scrollbarRef"
                @scroll="handleScrollVal">
                <div v-for="item in newMessage" :key="item" class="message-item"
                    :style="`justify-content: ${item.from !== creator ? 'flex-start' : 'flex-end'};`">
                    <div v-if="item.from !== creator" class="message-content">
                        <div class="user">
                            <img :src="'https://img0.baidu.com/it/u=2226630510,461838410&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=521'"
                                alt="" />
                        </div>
                        <div class="message image" :style="'padding: 6px;'" v-if="item.content._lctype === -2">
                            <img :src="item.content._lcfile.url" alt="" />
                        </div>
                        <div class="message" v-if="item.content._lctype === -1">{{ item.content._lctext }}</div>
                    </div>
                    <div v-else class="message-content">
                        <div class="message image" :style="'padding: 6px;'" v-if="item.content._lctype === -2">
                            <img :src="item.content._lcfile.url" alt="" />
                        </div>
                        <div class="message" v-if="item.content._lctype === -1">{{ item.content._lctext }}</div>
                        <div class="user">
                            <img :src="'https://img1.baidu.com/it/u=3622150954,2575811681&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=500'"
                                alt="" />
                        </div>
                    </div>
                </div>
            </el-scrollbar>
            <ul v-show="newMessage.length === 0">
                <li>暂无消息</li>
            </ul>
            <div class="message-operation">
                <!-- <el-button type="primary" @click="moreMessage">更多</el-button> -->
                <el-upload v-model:file-list="fileList" ref="uploadRef" class="upload-demo" multiple :limit="3"
                    :auto-upload="false" :show-file-list="false" :accept="'image/*'">
                    <el-button :icon="Picture" />
                </el-upload>
                <el-input class="message-input" v-model="messageValue" @keyup.enter="sendMessage" style="width: 240px"
                    placeholder="" />
                <el-button type="primary" @click="sendMessage">发送</el-button>
            </div>
        </div>
    </div>
</template>

<script setup>
import * as IM from 'leancloud-realtime'
import AV from 'leancloud-storage'
import initPlugin from 'leancloud-realtime-plugin-typed-messages'
import { onMounted, ref, watch, onBeforeUnmount } from 'vue'
import { ElMessage } from 'element-plus'
import { Picture } from '@element-plus/icons-vue'
import axios from 'axios'
import _ from 'lodash'

const props = defineProps({
    // 自己的 id
    ownId: {
        type: String,
        default: ''
    },
    // 对方的 id
    adverseId: {
        type: String,
        default: ''
    },
    // 聊天室名称
    chatsName: {
        type: String,
        default: ''
    },
})

const { Realtime, TextMessage, Event } = IM
const { TypedMessagesPlugin, ImageMessage } = initPlugin(AV, IM)
let realtime;

const newMessage = ref([])
const creator = ref('')
const adverse = ref('')
const messageValue = ref('')
const fileList = ref([])
const uploadRef = ref(null)
const scrollContainer = ref(null)

// 监听文件上传
watch(
    () => fileList.value.length,
    () => {
        console.log(fileList.value)
        fileList.value.length !== 0 && sendMessage()
    }
)

// 发送消息
const sendMessage = () => {
    if (messageValue.value === '' && fileList.value.length === 0) {
        ElMessage({
            message: '不能发送空消息!',
            type: 'warning'
        })
        return
    }
    /**
     * 创建一个 IMClient 实例
     */
    // 1、Tom 用自己的名字作为 clientId 来登录即时通讯服务
    realtime
        .createIMClient(props.ownId)
        .then(function (own) {
            // 成功登录
            console.log('登录成功', own)
            // 创建与 Jerry 之间的对话
            own.createConversation({
                // tom 是一个 IMClient 实例
                // 指定对话的成员除了当前用户 Tom(SDK 会默认把当前用户当做对话成员)之外,还有 Jerry
                members: [props.adverseId],
                // 对话名称
                name: props.chatsName,
                unique: true
            }).then(function (conversation) {
                // 创建成功
                console.log('创建成功', conversation)
                // 发送一条文本消息
                if (messageValue.value !== '') {
                    conversation
                        .send(new TextMessage(messageValue.value))
                        .then(function (message) {
                            console.log('发送成功!')
                            createUser_Jerry()
                            messageValue.value = ''
                        })
                        .catch(console.error)
                }

                if (fileList.value.length !== 0) {
                    var file = new AV.File('avatar.jpg', fileList.value[0].raw)
                    file
                        .save()
                        .then(function () {
                            var message = new ImageMessage(file)
                            message.setText('本地图片')
                            message.setAttributes({ location: '武汉' })
                            return conversation.send(message)
                        })
                        .then(function () {
                            console.log('发送成功')
                            createUser_Jerry()
                            fileList.value = []
                        })
                        .catch(console.error.bind(console))
                }
            })
        })
        .catch(console.error)
}

const createUser_Jerry = () => {
    // Jerry 登录
    realtime
        .createIMClient(props.ownId)
        .then(function (own) {
            // 成功登录
            console.log('登录成功', own)
            creator.value = own.id
            // 确保 Jerry 已加入对话
            own.createConversation({
                members: [props.adverseId],
                name: props.chatsName,
                unique: true
            }).then(function (conversation) {
                // 创建成功
                console.log('创建成功', conversation)
                adverse.value = conversation.members.find((item) => item !== conversation.creator)
                // 当前用户被添加至某个对话
                own.on(Event.INVITED, function invitedEventHandler(payload, conversation) {
                    console.log(payload.invitedBy, conversation.id)
                })
                // 当前用户收到了某一条消息,可以通过响应 Event.MESSAGE 这一事件来处理。
                own.on(Event.MESSAGE, function (message, conversation) {
                    // newMessage.value.push(message.text)
                    console.log('收到新消息:' + message.text, conversation)
                    getChatRecord(conversation)
                })

                getChatRecord(conversation)
            })
        })
        .catch(console.error)
}

let messageIterator
// 获取聊天记录
const getChatRecord = (conversation) => {
    // JS SDK 通过迭代器隐藏了翻页的实现细节,开发者通过不断的调用 next 方法即可获得后续数据。
    // 创建一个迭代器,每次获取 10 条历史消息
    messageIterator = conversation.createMessagesIterator({ limit: 20 })
    // 第一次调用 next 方法,获得前 10 条消息,还有更多消息,done 为 false
    messageIterator
        .next()
        .then(function (result) {
            newMessage.value = result.value
            scrollContainer.value.scrollTop = scrollContainer.value.scrollHeight
        })
        .catch(console.error.bind(console))
}

// 更多消息
const moreMessage = () => {
    messageIterator
        .next()
        .then(function (result) {
            if (result.value.length === 0) {
                ElMessage.warning('没有更多消息了')
                return
            }
            newMessage.value = result.value.concat(newMessage.value)
        })
        .catch(console.error.bind(console))
}

const userList = ref([]) // 用户列表
// 获取会话
const getConversation = () => {
    axios({
        method: 'get',
        url: `/1.2/rtm/conversations?limit=10&where={"c": "${props.ownId}"}`,
        headers: {
            'X-LC-Id': 'NLblIbY4gEKenESSb7Q3vY9Y-gzGzoHsz',
            'X-LC-Key': 'l1JvznaAgLvkGumBdiXDYQ6q,master',
            'Content-Type': 'application/json'
        }
    })
        .then(function (response) {
            console.log(response.data);
            userList.value = response.data.results.map(item => {
                return item.m.find(v => v !== item.c)
            })
            console.log(userList.value);
        });
}
const emit = defineEmits(['selectUser'])
// 选择用户
const selectUser = (user) => {
    emit('selectUser', user)
}

// 监听props.adverseId
watch(() => props.adverseId,
    () => {
        createUser_Jerry()
        getConversation()
        getUnreader()
    }
)

// 获取未读消息
const getUnreader = () => {
    axios({
        method: 'get',
        url: `/1.2/rtm/clients/${props.ownId}/unread-count`,
        headers: {
            'X-LC-Id': 'NLblIbY4gEKenESSb7Q3vY9Y-gzGzoHsz',
            'X-LC-Key': 'l1JvznaAgLvkGumBdiXDYQ6q,master',
            'Content-Type': 'application/json'
        }
    })
        .then(function (response) {
            console.log(response.data);
            if (response.data.unread > 0) {
                ElMessage.warning('有新消息')
            }
        });
}

const scrollbarRef = ref(null) // 滚动组件实例
// 在滚动事件绑定时创建节流函数
const throttledMoreMessage = _.throttle(moreMessage, 1000, { 'trailing': false });
// 滚动条滚动事件
const handleScrollVal = (val) => {
    if (val.scrollTop < 52) {
        throttledMoreMessage()
    }
}

onMounted(() => {
    realtime = new Realtime({
        appId: 'NLblIbY4gEKenESSb7Q3vY9Y-gzGzoHsz',
        appKey: 'pVpG3Q8DWAnWDWUquOb5cBu0',
        server: 'https://nlbliby4.lc-cn-n1-shared.com',
        // 初始化即时通讯服务时需要指定富媒体消息插件
        plugins: [TypedMessagesPlugin]
    })
    localStorage.setItem('debug', 'LC*')

    AV.init({
        appId: 'NLblIbY4gEKenESSb7Q3vY9Y-gzGzoHsz',
        appKey: 'pVpG3Q8DWAnWDWUquOb5cBu0',
        serverURL: 'https://nlbliby4.lc-cn-n1-shared.com'
    })
    createUser_Jerry()

    getConversation()
    getUnreader()

    setTimeout(() => {
        scrollbarRef.value.setScrollTop(1000)
    }, 500)
})
</script>

<style lang="scss" scoped>
* {

    ul,
    li {
        list-style: none;
        padding: 0;
        margin: 0;
    }
}

.chat {
    display: flex;

    .userList {
        border: 1px solid #e3e3e3;
        width: 16%;

        ul {
            li {
                line-height: 36px;
                background: #e3e3e3;
                padding: 0 10px;
                margin-bottom: 1px;
                cursor: pointer;
            }

            li:hover {
                background: #cacaca;
            }

            .active {
                background: #c1c1c1;
            }
        }
    }

    .instant-messaging {
        flex: 1;
        background: #f1f1f1;
        padding: 20px;

        h3 {
            text-align: center;
        }

        .scroll-container {
            height: 580px;
            overflow-y: auto;

            .message-item {
                display: flex;
                margin-bottom: 10px;

                .message-content {
                    display: flex;
                    align-items: center;

                    .user {
                        width: 36px;
                        height: 36px;
                        margin-right: 10px;
                        border-radius: 50px;

                        img {
                            width: 100%;
                            height: 100%;
                        }
                    }

                    .message {
                        line-height: 36px;
                        background: #fff;
                        padding: 0 6px;
                        margin-right: 10px;
                        display: flex;
                        justify-content: center;
                    }

                    .image {
                        max-width: 100px;
                        height: 52px;

                        img {
                            width: 100%;
                            height: 100%;
                        }
                    }
                }
            }
        }

        .message-operation {
            display: flex;
            justify-content: flex-end;

            .message-input {
                margin: 0 20px;
            }
        }
    }
}
</style>

使用组件

<template>
    <div>
        <ChatComponent :ownId="ownId" :adverseId="adverseId" :chatsName="chatsName" @selectUser="selectUser" />
    </div>
</template>

<script setup>
import ChatComponent from '@/components/ChatComponent.vue'
import { ref } from 'vue'

// const ownId = ref('goto_w')
// const adverseId = ref('tom')
// const chatsName = ref('goto_w  & tom')

// const ownId = ref('红红')
// const adverseId = ref('熊熊')
// const chatsName = ref('红红  & 熊熊')

const ownId = ref('goto_w')
const adverseId = ref('M')
const chatsName = ref('goto_w & M')

const selectUser = (user) => {
    adverseId.value = user
}

</script>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1795844.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

15、matlab绘图汇总(图例、标题、坐标轴、线条格式、颜色和散点格式设置)

1、plot()函数默认格式画图 代码&#xff1a; x0:0.1:20;%绘图默认格式 ysin(x); plot(x,y) 2、X轴和Y轴显示范围/axis()函数 代码&#xff1a; x0:0.1:20;%绘图默认格式 ysin(x); plot(x,y) axis([0 21 -1.1 1.1])%设置范围 3、网格显示/grid on函数 代码&#xff1a; …

设备上CCD功能增加(从接线到程序)

今天终于完成了一个上面交给我的一个小项目&#xff0c;给设备增加一个CCD拍照功能&#xff0c;首先先说明一下本次使用基恩士的CCD相机&#xff0c;控制器&#xff0c;还有软件&#xff08;三菱程序与基恩士程序&#xff09;。如果对你有帮助&#xff0c;欢迎评论收藏&#xf…

VMware Workstation中WinXP联网问题

我一直以为我的虚拟机上的XP没有联网 因为 蒙了半天&#xff0c;发现是因为这个网址打不开&#xff0c;不是没有网 太傻了 不如在cmd命令行中通过ping baidu.com来判断是否联网

Dubbo 自定义 Filter 编码实例

Dubbo的Filter机制为我们做应用的扩展设计提供了很多可能性&#xff0c;这里的Filter也是“责任链”机制的一种实现场景&#xff0c;作为Java码农&#xff0c;我们也经常接触到很多责任链的实现场景&#xff0c;如Tomcat进入Servlet前的filter&#xff0c;如Spring Aop代理的链…

语言模型解构——Tokenizer

1. 认识Tokenizer 1.1 为什么要有tokenizer&#xff1f; 计算机是无法理解人类语言的&#xff0c;它只会进行0和1的二进制计算。但是呢&#xff0c;大语言模型就是通过二进制计算&#xff0c;让你感觉计算机理解了人类语言。 举个例子&#xff1a;单1&#xff0c;双2&#x…

龙迅LT8712X TYPE-C或者DP转HDMI加VGA输出,内置MCU,只是IIS以及4K60HZ分辨率

龙迅LT8712X描述&#xff1a; LT8712X是一种高性能的Type-C/DP1.2到HDMI2.0和VGA转换器&#xff0c;设计用于将USB Type-C源或DP1.2源连接到HDMI2.0和VGA接收器。LT8712X集成了一个DP1.2兼容的接收器&#xff0c;一个HDMI2.0兼容的发射机和一个高速三角机窝视频DAC。此外&…

【论文速读】LM的文本生成方法,Top-p,温度,《The Curious Case of Neural Text Degeneration》

论文链接&#xff1a;https://arxiv.org/abs/1904.09751 https://ar5iv.labs.arxiv.org/html/1904.09751 这篇文章&#xff0c;描述的是语言模型的文本生成的核采样的方法&#xff0c;就是现在熟知的top-p 大概看看&#xff0c;还有几个地方比较有趣&#xff0c;值得记录一下。…

kotlin1.8.10问题导致gson报错TypeToken type argument must not contain a type variable

书接上回&#xff0c;https://blog.csdn.net/jzlhll123/article/details/139302991。 之前我发现gson报错后&#xff1a; gson在2.11.0给我的kotlin项目代码报错了。 IllegalArgumentException: TypeToken type argument must not contain a type variable 上次解释原因是因为&…

WALT算法简介

WALT(Windows-Assist Load Tracing)算法是由Qcom开发&#xff0c; 通过把时间划分为窗口&#xff0c;对 task运行时间和CPU负载进行跟踪计算的方法。为任务调度、迁移、负载均衡及CPU调频 提供输入。 WALT相对PELT算法&#xff0c;更能及时反映负载变化&#xff0c; 更适用于…

PasteCode系列系统说明

定义 PasteCode系列是指项目是基于PasteTemplate构建的五层以上项目&#xff0c;包括不仅限于 Domain EntityFrameworkCore Application.Contracts Application HttpApi.Host 熟悉ABP vNext就很好理解了&#xff0c;因为PasteTemplate就是基于ABP的框架精简而来&#xff01;在…

CVE-2022-4230

CVE-2022-4230 漏洞介绍 WP Statistics WordPress 插件13.2.9之前的版本不会转义参数&#xff0c;这可能允许经过身份验证的用户执行 SQL 注入攻击。默认情况下&#xff0c;具有管理选项功能 (admin) 的用户可以使用受影响的功能&#xff0c;但是该插件有一个设置允许低权限用…

绘画参数配置及使用

绘画参数配置及使用 路径&#xff1a;站点后台-功能-AI绘画 进入参数配置 接口选择&#xff1a;多种接口自主选择&#xff08;需自己准备key&#xff09;&#xff0c;对应接口的key对话和绘画通用 存储空间&#xff1a; 位置在超管后台-存储空间 自主选择存储&#xff08;需…

Python画图(多图展示在一个平面)

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

Go实战 | 使用Go-Fiber采用分层架构搭建一个简单的Web服务

前言 &#x1f4e2;博客主页&#xff1a;程序源⠀-CSDN博客 &#x1f4e2;欢迎点赞&#x1f44d;收藏⭐留言&#x1f4dd;如有错误敬请指正&#xff01; 一、环境准备、示例介绍 Go语言安装&#xff0c;GoLand编辑器 这个示例实现了一个简单的待办事项&#xff08;todo&#xf…

应用解析 | 面向智能网联汽车的产教融合解决方案

背景介绍 随着科技的飞速发展&#xff0c;智能网联汽车已成为汽车产业的新宠&#xff0c;引领着未来出行的潮流。然而&#xff0c;行业的高速发展也带来了对高素质技术技能人才的迫切需求。为满足这一需求&#xff0c;推动教育链、人才链与产业链、创新链的深度融合&#xff0…

【Java】static 修饰变量

static 一种java内置关键字&#xff0c;静态关键字&#xff0c;可以修饰成员变量、成员方法。 static 成员变量 1.static 成员变量2.类变量图解3.类变量的访问4.类变量的内存原理5.类变量的应用 1.static 成员变量 成员变量按照有无static修饰&#xff0c;可以分为 类变量…

AGP8+ android.useNewApkCreator‘ is deprecated 打包失败

问题 新建一个项目&#xff0c;默认使用最新版的 AGP 和 Gradle&#xff0c;打包构建立马失败&#xff01; 错误日志 Caused by: com.android.builder.errors.EvalIssueException: The option android.useNewApkCreator is deprecated. An exception occurred applying plu…

STM32-16-ADC

STM32-01-认识单片机 STM32-02-基础知识 STM32-03-HAL库 STM32-04-时钟树 STM32-05-SYSTEM文件夹 STM32-06-GPIO STM32-07-外部中断 STM32-08-串口 STM32-09-IWDG和WWDG STM32-10-定时器 STM32-11-电容触摸按键 STM32-12-OLED模块 STM32-13-MPU STM32-14-FSMC_LCD STM32-15-DMA…

关于2024中国海洋装备博览会(福州)的参展通知

2024中国航洋装备博览会 2024世界航海装备大会 2024中国船舶供应链大会 2024中国航洋装备博览会2024世界航海装备大会 时间地点、规模、主题、定位 1.时间&#xff1a;2024年11月15日至18日 2.地点&#xff1a;福州海峡国际会展中心、冠城大通游艇码头 3.规模&#xff1…

Seed-TTS语音编辑有多强?对比实测结果让你惊叹!

GLM-4-9B 开源系列模型 前言 就在最近&#xff0c;ByteDance的研究人员最近推出了一系列名为Seed-TTS的大规模自回归文本转语音(TTS)模型,能够合成几乎与人类语音无法区分的高质量语音。那么Seed-TTS的表现究竟有多强呢?让我们一起来感受下Seed-TTS带来的惊喜吧! 介绍Seed-TTS…