WorkerMan实现Web通讯(使用Vue实现前端页面逻辑)

 需要使用到的扩展 https://github.com/tangbc/vue-virtual-scroll-list

最终效果

对话框

主页面 Main.vue

<template>
    <div>
        <!---chat message-->
        <el-dialog id="chat" top="8vh" :width="dialogWidth" center :show-close="false" :close-on-press-escape="false" :close-on-click-modal="false" :visible.sync="visible">
            <div slot="title" style="display: flex;align-items: center;position: relative;">
                <el-avatar :src="chat.img" :size="50" fit="fill" :alt="chat.title"/>
                <div>
                    <div v-html="chat.title" style="margin-left: 10px;text-align: left;color: #fff"/>
                    <div slot="title" v-html="chat.desc" style="margin-left: 10px;color: #fff"/>
                </div>
            </div>
            <el-row :gutter="24">
                <el-col :xl="{'span':18}" :lg="{'span':16}" :md="{'span':16}" :sm="{'span':16}" :xs="{'span':14}" id="leftBox">
                    <el-card>
                        <div class="main">
                            <div class="list-container">
                                <virtual-list v-show="!!chat.messageLists.length" class="stream scroll-touch" :class="{ overflow: overflow }" ref="vsl"
                                              :data-key="'uid'"
                                              :data-sources="chat.messageLists"
                                              :data-component="messageComponent"
                                              :estimate-size="70"
                                              :item-class="'stream-item'"
                                              :item-class-add="addItemClass"
                                              @resized="onItemRendered"
                                              @totop="onTotop">
                                    <div slot="header" v-show="overflow" class="header">
                                        <div class="spinner" v-show="!finished"></div>
                                        <div class="finished" v-show="finished">No more data</div>
                                    </div>
                                </virtual-list>
                                <div class="empty" v-show="!chat.messageLists.length">
                                    <div class="wrapper">
                                        <div class="icon"></div>
                                        <div class="tips">No chats</div>
                                    </div>
                                </div>
                            </div>
                        </div>
                        <div class="input-msg">
                            <emotion @clickEmotion="getEmotion" v-show="showEmotion" :height="300"/>
                            <div>
                                <el-tooltip effect="dark" content="房间名称" placement="top-start">
                                    <el-menu :default-active="chat.room_id"   background-color="#409EFF"
                                             text-color="#fff"
                                             active-text-color="#ffd04b" mode="horizontal" style="margin-bottom:10px;">
                                        <el-menu-item @click="setRoomID(room)" v-for="(room,index) in oauthConfig" :key="index" :index="room.id.toString()">
                                            {{room.name}}
                                        </el-menu-item>
                                    </el-menu>
                                </el-tooltip>
                                <el-tooltip effect="dark" content="发送表情" placement="top-start">
                                    <i @click="showEmotion = !showEmotion" class="el-icon-picture-outline-round icon"/>
                                </el-tooltip>
                                <el-upload :action="cgi.uploadUrl"
                                           :data="fileData"
                                           :headers="headers"
                                           :show-file-list="false"
                                           :on-success="uploadSuccess"
                                           :before-upload="beforeUpload" style="float: left">
                                    <el-tooltip effect="dark" content="发送文件和图片" placement="top-start">
                                        <i  @click="showEmotion = false" class="el-icon-picture-outline icon"/>
                                    </el-tooltip>
                                </el-upload>
                            </div>
                            <div contentEditable="true" ref="message" id="content" @focus="showEmotion = false" @keydown="setMsg">

                            </div>
                        </div>
                        <div class="input-button" style="text-align: right">
                            <el-tooltip effect="dark" content="Shift + Enter 快捷发送" placement="top-start">
                                <el-button type="primary" round plain size="medium" @click="sendMsg">发 送</el-button>
                            </el-tooltip>
                        </div>
                    </el-card>
                </el-col>
                <el-col :xl="{'span':6}" :lg="{'span':8}" :md="{'span':8}" :sm="{'span':8}" :xs="{'span':10}" id="rightBox">
                    <el-card>
                        <div style="background: #fff;min-height: 60px">
                            群公告:
                            <el-carousel tyle="cursor: pointer" :interval="4000" arrow="never" direction="vertical" indicator-position="none" height="100px">
                                <el-carousel-item v-for="(item,index) in groupAnnouncementConfig" :key="index">
                                    <div style="cursor: pointer;margin-top: 20px" v-html="item.name"/>
                                </el-carousel-item>
                            </el-carousel>
                        </div>
                        <el-divider/>
                        <div style="margin-bottom: 10px">
                            在线人数({{chat.userTotal}}/{{chat.userOnline}})
                        </div>
                        <el-autocomplete placeholder="搜索" v-model="chat.users" clearable :fetch-suggestions="querySearch" @clear="clearSearch" style="width: 100%"/>
                        <div class="user-list">
                            <el-menu style="width: 100%;">
                                <el-menu-item :users="user" v-contextmenu:contextmenu @click="sendUser(user,index)" v-for="(user,index) in chat.client_list_part" :key="index" :index="index.toString()">
                                    <el-avatar :size="30" :src="user.client_img" style="cursor: pointer"/>
                                    <span slot="title" style="font-size: 14px" v-html="user.client_name.replace(chat.users,'<b style=color:#0e82fc;font-weight:300>'+chat.users+'</b>')"/>
                                    <!--未读消息数-->
                                    <el-badge v-if="user.total" type="danger" :value="user.total" style="top: 10px;right: 15px"/>
                                    <!--在线-->
                                    <el-badge v-else-if="user.online" type="success" is-dot style="top: 12px;right: 10px"/>
                                    <!--离线-->
                                    <el-badge v-else-if="!user.online" type="info" is-dot style="top: 12px;right: 10px"/>
                                </el-menu-item>
                            </el-menu>
                        </div>
                        <!--右键菜单-->
                        <v-contextmenu ref="contextmenu" @contextmenu="menuRightChange">
                            <v-contextmenu-item @click="userInfoVisible = true"><i class="el-icon-postcard"/> 查看资料</v-contextmenu-item>
                            <v-contextmenu-item divider></v-contextmenu-item>
                            <v-contextmenu-item @click="sendToMessage"><i class="el-icon-chat-line-square"/> 发送消息</v-contextmenu-item>
                        </v-contextmenu>
                    </el-card>
                </el-col>
            </el-row>
        </el-dialog>
        <!---chat message-->
        <!--userInfo Start-->
        <UserInfo :user-info="targetUsers" :dialog-width="dialogWidth" :user-info-visible="userInfoVisible" :closeDialog="closeDialog"/>
        <!--userInfo End-->
    </div>
</template>

<script>
    import { mapGetters, mapActions } from 'vuex'
    import func from '../../api/func'
    import emotion from '../emotion/Index'
    import Push from 'push.js'
    import Item from './Item'
    import VirtualList from 'vue-virtual-scroll-list'
    import UserInfo from './UserInfo'
    export default {
        props: {
            chatVisible: {
                type: Boolean,
                default: () => true
            },
            dialogWidth: {
                type: String,
                default: () => '50%'
            },
            currentPage: {
                type: Number,
                default: () => 1
            },
            sizeLimit: {
                type: Number,
                default: () => 19
            }
        },
        name: 'chatRoom',
        data () {
            return {
                cgi: {uploadUrl: process.env.API_ROOT + 'v1/common/upload'},
                fileData: {},
                headers: {},
                inputMsg: '',
                emotionLists: [],
                showEmotion: false,
                chat: {},
                messageComponent: Item,
                overflow: false,
                finished: false,
                visible: this.chatVisible,
                targetUsers: {},
                userInfoVisible: false,
                userInfoDialogWidth: this.dialogWidth
            }
        },
        components: {
            UserInfo,
            emotion,
            VirtualList
        },
        computed: {
            ...mapGetters(['oauthConfig', 'groupAnnouncementConfig', 'userInfo'])
        },
        watch: {
            inputMsg: function () {
                this.inputMsg = this.$refs.message.innerHTML
            },
            chatVisible: function () {
                this.visible = this.chatVisible
                if (!this.visible) {
                    this.userInfoVisible = false
                }
            }
        },
        methods: {
            ...mapActions(['getOauthConfig', 'saveWeather']),
            /**
             * todo:获取目标用户的信息
             * @param item
             */
            menuRightChange (item) {
                if (item.data.attrs && item.data.attrs.users) {
                    this.targetUsers = item.data.attrs.users
                }
            },
            /**
             * todo:弹框关闭回调
             */
            closeDialog: function () {
                this.userInfoVisible = false
            },
            /**
             * todo:发送消息
             */
            sendToMessage: function () {
                this.sendUser(this.targetUsers, this.targetUsers.uid)
            },
            /**
             * todo:添加class
             * @param index
             */
            addItemClass: function (index) {
                if (this.chat.messageLists.length > 0) {
                    if (this.chat.messageLists[index].to_client_name === 'all') {
                        return this.userInfo.username === this.chat.messageLists[index].from_client_name ? 'creator' : ''
                    } else {
                        return this.userInfo.username === this.chat.messageLists[index].to_client_name ? '' : 'creator'
                    }
                }
            },
            /**
             * todo:记录读取
             */
            onItemRendered: function () {
                if (!this.$refs.vsl) {
                    return
                }
                if (this.$refs.vsl) {
                    const vsl = this.$refs.vsl
                    if (vsl) {
                        this.overflow = vsl.getScrollSize() > vsl.getClientSize()
                    }
                }
            },
            /**
             * todo:向上滚动获取数据
             */
            onTotop: function () {
                setTimeout(() => {
                    if (this.chat.currentPage <= Math.ceil(this.chat.messageTotal / this.chat.sizeLimit)) {
                        this.chat.currentPage++
                        // 获取聊天记录
                        let str = {
                            type: 'history',
                            from_client_name: this.userInfo.username,
                            from_client_id: this.userInfo.uuid,
                            to_client_name: this.chat.to_client_name,
                            to_client_id: this.chat.to_client_id,
                            room_id: this.chat.to_client_name === 'all' ? this.chat.room_id : '',
                            uid: this.userInfo.uuid,
                            page: this.chat.currentPage,
                            limit: this.chat.sizeLimit,
                            source: 'user'
                        }
                        this.userInfo.websocketServer.send(JSON.stringify(str))
                    }
                    this.finished = true
                }, 500)
            },
            /**
             * todo:workerMan-chat链接
             * @param ws
             */
            connect: function (ws) {
                let __this = this
                // 连接建立时发送登录信息
                ws.onopen = function () {
                    let str = {
                        type: 'login',
                        from_client_id: __this.userInfo.uuid,
                        client_name: __this.userInfo.username,
                        room_id: __this.chat.room_id,
                        client_img: __this.userInfo.avatar_url,
                        uid: __this.userInfo.uuid,
                        adcode: __this.userInfo.adcode
                    }
                    ws.send(JSON.stringify(str))
                }
                ws.onmessage = function (response) {
                    let data = JSON.parse(response.data)
                    switch (data['type']) {
                    // 心跳
                    case 'ping':
                        ws.send('{"type":"pong"}')
                        break
                    // 登陆
                    case 'login':
                        __this.chat.client_list = data.client_list
                        __this.chat.client_list_part = data.client_list
                        __this.setUsersLists()
                        __this.saveWeather(data.weather)
                        console.log(data)
                        break
                    // 发送消息
                    case 'say':
                        __this.say(data)
                        __this.chat.client_list = data.client_list
                        __this.chat.client_list_part = data.client_list
                        __this.setUsersLists()
                        __this.$refs.vsl.scrollToBottom()
                        console.log(data)
                        break
                    // 聊天记录
                    case 'history':
                        __this.chat.messageTotal = data.total
                        __this.chat.client_list = data.client_list
                        __this.chat.client_list_part = data.client_list
                        if (__this.chat.currentPage > 1) {
                            __this.chat.messageLists = __this.chat.messageLists.concat(data.message)
                        } else {
                            __this.chat.messageLists = data.message
                            __this.$refs.vsl.scrollToBottom()
                        }
                        __this.chat.messageLists.sort(function (a, b) {
                            return a.time > b.time ? 1 : -1
                        })
                        __this.setUsersLists()
                        console.log(data)
                        break
                    // 消息删除/消息撤回
                    case 'srem':
                    case 'recall':
                        __this.chat.messageLists = data.message
                        __this.chat.client_list = data.client_list
                        __this.chat.client_list_part = data.client_list
                        __this.setUsersLists()
                        __this.removeMessageList(data)
                        console.log(data)
                        break
                    case 'logout':
                        console.log(data)
                        break
                    }
                }
                ws.onclose = function () {
                    console.log('连接关闭,定时重连')
                    __this.connect(ws)
                }
                ws.onerror = function () {
                    console.log('出现错误')
                }
            },
            /**
             * todo:用户搜索
             * @param queryString
             * @param cb (回调函数 cb(result))
             */
            querySearch: function (queryString, cb) {
                let result = []
                for (let i in this.chat.client_list) {
                    if (this.chat.client_list[i].client_name.indexOf(queryString) >= 0) {
                        result.push(this.chat.client_list[i])
                    }
                }
                this.chat.client_list_part = result
            },
            /**
             * todo:清空搜索内容
             */
            clearSearch: function () {
                this.chat.client_list_part = this.chat.client_list
            },
            /**
             * todo:设置未读消息数
             */
            setUsersLists: function () {
                // 展示单个用户未读消息数
                let unreadMessage = []
                this.chat.userOnline = 0
                this.chat.userTotal = 0
                for (let i in this.chat.client_list) {
                    if (this.userInfo.username === this.chat.client_list[i]['client_name']) {
                        unreadMessage = this.chat.client_list[i]['unread']
                        this.chat.msgCount = this.chat.client_list[i]['unreadCount']
                        this.$emit('setMsgCount', this.chat.msgCount)
                    }
                    if (this.chat.client_list[i]['online']) {
                        this.chat.userOnline++
                    }
                    this.chat.userTotal++
                }
                if (unreadMessage.length > 0) {
                    for (let i in unreadMessage) {
                        for (let j in this.chat.client_list) {
                            if (unreadMessage[i]['form'] === j) {
                                this.chat.client_list[j]['total'] = parseInt(unreadMessage[i]['total'])
                            }
                        }
                    }
                }
            },
            /**
             * TODO:设置群聊房间号
             * @param room
             */
            setRoomID: function (room) {
                if (this.chat.room_id !== room.id.toString()) {
                    this.showEmotion = false
                    this.finished = false
                    this.chat.room_id = room.id.toString()
                    this.chat.title = room.name
                    this.chat.desc = room.value
                    this.chat.currentPage = 1
                    this.chat.img = 'https://cdn.pixabay.com/photo/2016/12/13/21/20/alien-1905155_960_720.png'
                    // 加入房间
                    let login = {
                        type: 'login',
                        from_client_id: this.userInfo.uuid,
                        client_name: this.userInfo.username,
                        room_id: this.chat.room_id,
                        client_img: this.userInfo.avatar_url,
                        uid: this.userInfo.uuid,
                        adcode: this.userInfo.adcode
                    }
                    this.userInfo.websocketServer.send(JSON.stringify(login))
                }
                // 群聊时,接收方为所有用户
                this.chat.to_client_id = 'all'
                this.chat.to_client_name = 'all'
                // 获取聊天记录
                let str = {
                    type: 'history',
                    from_client_name: this.userInfo.username,
                    from_client_id: this.chat.from_client_id,
                    to_client_name: this.chat.to_client_name,
                    to_client_id: this.chat.to_client_id,
                    room_id: this.chat.to_client_name === 'all' ? this.chat.room_id : '',
                    uid: this.userInfo.uuid,
                    page: this.chat.currentPage,
                    limit: this.chat.sizeLimit,
                    source: 'room'
                }
                console.log(this.userInfo)
                this.userInfo.websocketServer.send(JSON.stringify(str))
            },
            /**
             * TODO:设置发送给谁
             * @param user
             * @param clientId
             */
            sendUser: function (user, clientId) {
                this.finished = false
                this.chat.to_client_name = user.client_name
                this.chat.to_client_id = clientId === '0' ? 'all' : clientId
                this.chat.from_client_id = user.uid
                this.chat.uid = this.chat.to_client_id
                this.chat.title = user.client_name
                if (this.chat.users) {
                    this.chat.users = user.client_name
                }
                this.chat.img = user.client_img
                this.chat.desc = user.centerInfo.desc
                this.chat.room_id = ''
                user.total = 0
                this.chat.currentPage = 1
                // 获取聊天记录
                let str = {
                    type: 'history',
                    from_client_name: this.userInfo.username,
                    from_client_id: this.userInfo.uuid,
                    to_client_name: this.chat.to_client_name,
                    to_client_id: this.chat.to_client_id,
                    room_id: this.chat.to_client_name === 'all' ? this.chat.room_id : '',
                    uid: this.userInfo.uuid,
                    page: this.chat.currentPage,
                    limit: this.chat.sizeLimit,
                    source: 'user'
                }
                this.userInfo.websocketServer.send(JSON.stringify(str))
            },
            /**
             * TODO:获取发送内容
             */
            setMsg: function () {
                this.inputMsg = this.$refs.message.innerHTML
            },
            /**
             * TODO:发送表情
             * @param emotion
             */
            getEmotion: function (emotion) {
                this.chat.msg_type = 'text'
                this.inputMsg += "<img src='" + emotion.icon + "' width='50px' height='50px' style='object-fit: contain;' alt='" + emotion.emoji + "' title='" + emotion.title + "'>"
                this.$refs.message.innerHTML = this.inputMsg
            },
            /**
             * TODO:图片上传成功
             * @param response
             */
            uploadSuccess: function (response) {
                if (response && response.code === 200) {
                    switch (this.chat.msg_type) {
                    case 'img':
                        this.inputMsg += "<img src='" + response.item.src + "' width='100px' height='100px' style='object-fit: contain;' alt='" + this.chat.from_client_name + "'>"
                        break
                    case 'video':
                        this.inputMsg += "<video src='" + response.item.src + "' width='200px' height='200px' controls='controls'>"
                        break
                    }
                    this.$refs.message.innerHTML = this.inputMsg
                    return
                }
                this.$message({type: 'warning', message: response.msg})
            },
            /**
             * TODO:图片上传前
             * @param file
             */
            beforeUpload: function (file) {
                let ext = file.name.split('.')[1]
                switch (ext.toLocaleLowerCase()) {
                case 'jpg':
                case 'gif':
                case 'png':
                case 'jpeg':
                    this.chat.msg_type = 'img'
                    if (file.size > 2 * 1024 * 1024) {
                        this.$message({type: 'warning', message: 'upload image size error'})
                    }
                    break
                case 'mp4':
                    this.chat.msg_type = 'video'
                    if (file.size > 5 * 1024 * 1024) {
                        this.$message({type: 'warning', message: 'upload video size error'})
                    }
                    break
                default:
                    this.$message({type: 'warning', message: 'Unsupported file format'})
                    break
                }
            },
            /**
             * TODO:发送消息
             * @param data
             */
            say: function (data) {
                if (this.userInfo.username !== data['from_client_name']) {
                    if (data['to_client_id'] !== 'all') {
                        this.chat.title = data['from_client_name']
                        this.chat.to_client_name = data['from_client_name']
                        this.chat.to_client_id = data['from_client_id']
                    }
                    this.chat.messageLists.push(data)
                    this.pushMessage(data['content'])
                }
                if (this.chatVisible && this.userInfo.username === data['from_client_name']) {
                    if (data['to_client_id'] !== 'all') {
                        this.chat.title = data['to_client_name']
                        this.chat.to_client_name = data['to_client_name']
                        this.chat.to_client_id = data['to_client_id']
                    }
                }
            },
            /**
             * TODO:消息发送
             */
            sendMsg: function () {
                this.showEmotion = false
                this.inputMsg = this.$refs.message !== undefined ? this.$refs.message.innerHTML : ''
                if (this.inputMsg.trim() !== '') {
                    // 发送消息
                    let str = {
                        type: 'say',
                        to_client_id: this.chat.to_client_id,
                        to_client_name: this.chat.to_client_name,
                        from_client_name: this.userInfo.username,
                        from_client_id: this.userInfo.uuid,
                        msg_type: this.chat.msg_type,
                        content: this.inputMsg,
                        client_img: this.userInfo.avatar_url,
                        room_id: this.chat.to_client_name === 'all' ? this.chat.room_id : '',
                        uid: this.chat.uid,
                        time: func.setTime(func.getTimestamp() * 1000)
                    }
                    this.userInfo.websocketServer.send(JSON.stringify(str))
                    this.chat.messageLists.push(str)
                    this.$refs.vsl.scrollToBottom()
                    this.$refs.message.innerHTML = ''
                    this.inputMsg = ''
                    return
                }
                if (this.chatVisible) {
                    this.$refs.message.focus()
                    this.$message({type: 'info', message: 'Please enter message'})
                }
            },
            /**
             * todo:推送弹框消息
             * @param message
             */
            pushMessage: function (message) {
                Push.create('你有未读消息', {
                    body: message,
                    requireInteraction: true,
                    icon: 'https://www.fanglonger.com/favicon.ico',
                    timeout: 60000
                })
            },
            /**
             * todo:删除消息
             */
            removeMessageList: function (targetMessage) {
                if (this.chat.messageLists.length > 0) {
                    let i = 0
                    this.chat.messageLists.map((item, index) => {
                        if (this.compareJson(item, targetMessage)) {
                            i = index
                        }
                    })
                    let __this = this
                    let messageLists = []
                    Object.keys(__this.chat.messageLists).forEach(function (message) {
                        messageLists.push(__this.chat.messageLists[message])
                    })
                    messageLists.splice(i, 1)
                    this.chat.messageLists = messageLists
                }
            },
            /**
             * todo:json字符串比较
             * @param jsonA
             * @param jsonB
             * @param field
             * @returns {boolean}
             */
            compareJson: function (jsonA, jsonB, field = ['type']) {
                let a = Object.keys(jsonA)
                let b = Object.keys(jsonB)
                if (a.length !== b.length) {
                    return false
                }
                let total = a.length
                let num = field.length
                for (let i in jsonA) {
                    if (!field.includes(i) && jsonA[i] === jsonB[i]) {
                        num++
                    }
                }
                return num === total
            }
        },
        /**
         * todo:生命周期
         */
        created () {
            // 初始化聊天系统参数
            this.chat = {
                to_client_name: 'all',
                to_client_id: 'all',
                from_client_id: this.userInfo.uuid,
                from_client_name: this.userInfo.username,
                uid: this.userInfo.uuid,
                room_id: this.userInfo.room_id,
                title: this.userInfo.room_name,
                msgCount: 0,
                client_list: [],
                client_list_part: [],
                img: 'https://cdn.pixabay.com/photo/2016/12/13/21/20/alien-1905155_960_720.png',
                desc: '',
                messageLists: [],
                msg_type: 'text',
                users: '',
                userTotal: 0,
                userOnline: 0,
                currentPage: this.currentPage,
                sizeLimit: this.sizeLimit,
                messageTotal: 0
            }
            // 浏览器消息推送
            try {
                Push.Permission.request()
            } catch (e) {
                // eslint-disable-next-line no-unused-expressions
                e ? this.$message.error(JSON.stringify(e)) : ''
            }
            // 图片上传参数
            this.fileData.token = this.userInfo.token
            this.fileData.rand = false
            this.headers.Authorization = `${func.setPassword(func.setRandom(32), func.setRandom(12))}${this.userInfo.token}${func.setPassword(func.setRandom(32), func.setRandom(12))}`
            // 客服系统初始化
            this.connect(this.userInfo.websocketServer)
            this.getOauthConfig('RoomLists')
            this.getOauthConfig('GroupAnnouncementConfig')
            let __this = this
            // 键盘事件
            document.onkeydown = function (e) {
                if (e.code === 'Enter' && e.shiftKey) {
                    e.preventDefault()
                    __this.sendMsg()
                    return false
                }
            }
            // 撤回消息
            this.$on('recallMessage', (newMessage, oldMessage) => {
                // 消息撤回
                newMessage.type = 'recall'
                newMessage['recall_message'] = JSON.parse(JSON.stringify(oldMessage))
                this.removeMessageList(oldMessage)
                // 消息推送
                this.userInfo.websocketServer.send(JSON.stringify(newMessage))
                this.chat.messageLists.push(newMessage)
            })
            // 删除消息
            this.$on('deleteMessage', (newMessage, oldMessage) => {
                // 删除消息
                newMessage.type = 'srem'
                this.removeMessageList(oldMessage)
                this.userInfo.websocketServer.send(JSON.stringify(newMessage))
            })
        }
    }
</script>
<style>
.user-list{
    min-height:355px;
    max-height: 355px;
    overflow: hidden;
    overflow-y: auto;
    padding: 0 !important;
}
#content{
    border: 1px solid #eee;
    height: 95px;
    border-radius: 10px;
    -moz-border-radius:10px;
    -webkit-border-radius:10px;
    padding:10px;
    overflow: scroll;
    overflow-x: hidden;
}
.input-msg{
    min-height: 100px;
    margin-bottom: 10px;
    margin-top: 10px;
}
.input-msg .icon{
    font-size: 25px;
    margin:0 10px 10px 0;
    color: #000;
    cursor: pointer;
}
#leftBox {
    padding-left: 0 !important;
    padding-right: 2px !important;
}
#rightBox {
    padding: 0 !important;
}
#rightBox .el-input__inner{
    margin-bottom: 10px;
}
.el-autocomplete-suggestion {
    display: none;
}
#chat .el-dialog__header {
    background: #409EFF;
}
#chat .el-dialog--center .el-dialog__body {
    padding: 5px 15px 5px !important;
}
#chat .el-card {
    border-radius: 0 !important;
    -webkit-border-radius:  0 !important;
    -moz-border-radius:  0 !important;
}
</style>
<style lang="less">
#chat {
    .stream, .empty {
        position: relative;
        width: 100%;
        height: 320px;
        overflow-y: auto;
        border: 1px solid #eee;
        display: flex;
        flex-direction: column-reverse;

        &.overflow {
            flex-direction: column;
        }

        .stream-item {
            display: flex;
            align-items: center;
            padding: 1em;

            &.creator {
                flex-direction: row-reverse;
            }
        }
    }

    .empty {
        .wrapper {
            position: absolute;
            left: 0;
            top: 0;
            bottom: 0;
            right: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            flex-direction: column;
            color: #bfbfbf;
        }

        .icon {
            width: 70px;
            height: 70px;
            background-size: cover;
            background-image: url("data:image/svg+xml,%3Csvg t='1587779561352' class='icon' viewBox='0 0 1024 1024' version='1.1' xmlns='http://www.w3.org/2000/svg' p-id='1970' width='200' height='200'%3E%3Cpath d='M915.90919 428.28025l40.909735 6.785543 1.77339-10.488889 8.708336-12.93766-103.649662-69.574588-7.776104-5.219885-7.837502-2.918469L552.61093 223.932142l-6.92676-2.579755-6.962575 2.308579-347.57209 115.214045-8.804527 2.14792-5.704932 4.046153L53.550637 432.126858l9.832949 13.854543 4.493338 11.238972 39.739073-16.353456L75.734892 485.09114l96.368838 54.05001 0.107447 199.534476-2.349511 4.28356 2.352581 1.310855 0.001023 2.709715 4.859681 0 281.95872 157.194158 7.696286 4.260024 2.344395 1.277086 2.635013 1.726318 9.41851-3.080152 8.748245-3.419889 388.69979-159.572322 0.031722-9.44307L878.607632 546.373879l92.490507-35.759435L915.90919 428.28025zM561.218982 260.057921l245.843174 91.538833-245.843174 92.115977L561.218982 260.057921zM121.002888 475.127208l40.684608-56.45171 5.983271-8.358365 23.439852-32.558533 262.998903 123.622552L411.290297 637.993552 121.002888 475.127208zM203.051579 726.133985l-0.100284-169.629375 226.371704 126.989229 28.646434-91.38636 0.893346 276.69688L203.051579 726.133985zM473.818399 476.500485 225.704508 359.870184l304.674071-100.980879 0 196.376553L473.818399 476.500485zM847.766207 724.616422 489.771743 871.571379l-0.955768-301.186643 45.281299 109.166305 313.667909-121.215736L847.765183 724.616422zM551.061644 639.889739l-57.241702-137.940652 347.098299-130.082684 83.064834 123.892705L551.061644 639.889739z' p-id='1971' fill='%23bfbfbf'%3E%3C/path%3E%3C/svg%3E");
        }
    }

    .header {
        padding: .5em;
        .finished {
            font-size: 14px;
            text-align: center;
            color: #bfbfbf;
        }

        .spinner {
            font-size: 10px;
            margin: 0 auto;
            text-indent: -9999em;
            width: 15px;
            height: 15px;
            border-radius: 50%;
            background: #ffffff;
            background: linear-gradient(to right, #ccc 10%, rgba(255, 255, 255, 0) 42%);
            position: relative;
            animation: load3 1.4s infinite linear;
            transform: translateZ(0);
        }

        .spinner:before {
            width: 50%;
            height: 50%;
            background: #ccc;
            border-radius: 100% 0 0 0;
            position: absolute;
            top: 0;
            left: 0;
            content: '';
        }

        .spinner:after {
            background: #ffffff;
            width: 75%;
            height: 75%;
            border-radius: 50%;
            content: '';
            margin: auto;
            position: absolute;
            top: 0;
            left: 0;
            bottom: 0;
            right: 0;
        }

        @-webkit-keyframes load3 {
            0% {
                transform: rotate(0deg);
            }
            100% {
                transform: rotate(360deg);
            }
        }
        @keyframes load3 {
            0% {
                transform: rotate(0deg);
            }
            100% {
                transform: rotate(360deg);
            }
        }
    }
}
</style>
右鍵查看用戶信息
用戶信息彈框

用户信息组件 UserInfo.vue

<template>
    <div :id="id">
        <el-dialog :visible.sync="visible" :title="userInfo.client_name" :width="userInfoDialogWidth" center id="userInfo" @close="closeDialog" :show-close="false" :style="transformStyle">
            <div slot="title" style="display: flex;align-items: center;position: relative;">
                <el-avatar :src="userInfo.client_img" :size="50" fit="fill" :alt="userInfo.client_name"></el-avatar>
                <div>
                    <div v-html="userInfo.client_name" style="margin-left: 10px;text-align: left;color: #fff"></div>
                    <div slot="title" v-html="userInfo.desc" style="margin-left: 10px;color:#fff"></div>
                </div>
            </div>
            <el-row :gutter="24">
                <el-col :xl="{'span':22,'offset':1}" :lg="{'span':22,'offset':1}" :md="{'span':22,'offset':1}" :sm="{'span':22,'offset':1}" :xs="{'span':22,'offset':1}" class="userInfo">
                    <el-form :model="userInfo" label-width="56px" label-position="left">
                        <el-form-item label="账号:">
                            <span v-html="userInfo.uid"></span>
                        </el-form-item>
                        <el-form-item label="简介:">
                            <span v-html="userInfo.desc"></span>
                        </el-form-item>
                        <el-form-item label="户籍:">
                            <span v-html="userInfo.local"></span>
                        </el-form-item>
                        <el-form-item label="住址:">
                            <span v-html="userInfo.ip_address"></span>
                        </el-form-item>
                        <el-form-item label="标签:">
                            <el-tag style="margin-right: 5px" effect="plain" type="success" v-for="(tags,index) in userInfo.tags" :key="index">{{tags}}</el-tag>
                        </el-form-item>
                    </el-form>
                </el-col>
            </el-row>
        </el-dialog>
    </div>
</template>

<script>
    export default {
        name: 'UserInfo',
        props: {
            userInfoVisible: {
                type: Boolean,
                default: () => false
            },
            userInfo: {
                type: [Object, Array],
                default: () => {}
            },
            closeDialog: {
                type: Function | Boolean
            },
            dialogWidth: {
                type: String,
                default: () => '40%'
            },
            id: {
                type: String,
                default: () => 'userInfo'
            }
        },
        data () {
            return {
                visible: this.userInfoVisible,
                userInfoDialogWidth: this.dialogWidth,
                randomNum: 0,
                transformStyle: {}
            }
        },
        watch: {
            userInfoVisible: function () {
                this.visible = this.userInfoVisible
                this.userInfoDialogWidth = (parseInt(this.dialogWidth.replace('%', '')) - 15) + '%'
                this.getUserInfo()
            }
        },
        created () {
            this.userInfoDialogWidth = (parseInt(this.dialogWidth.replace('%', '')) - 15) + '%'
            this.randomNum = Math.ceil(Math.random() * 5 | 0)
            this.transformStyle = { transform: 'rotate(' + this.randomNum + 'deg)' }
        },
        methods: {
            /**
             * todo:获取用户信息
             */
            getUserInfo: function () {
                this.userInfo.desc = this.userInfo.centerInfo.desc || ''
                this.userInfo.tags = JSON.parse(this.userInfo.centerInfo.tags) || ''
                this.userInfo.ip_address = this.setLocal(JSON.parse(this.userInfo.centerInfo.ip_address)) || ''
                this.userInfo.local = this.setLocal(JSON.parse(this.userInfo.centerInfo.local)) || ''
            },
            /**
             * TODO:设置地址显示
             */
            setLocal: function (item) {
                if (item.length > 0) {
                    let str = item.join(',')
                    return str.replace(new RegExp(/,/g), ' / ')
                }
                return item
            }
        }
    }
</script>

<style lang="less">
#userInfo {
    .el-dialog__body{
        position: relative;
        overflow: hidden;
        background-size: contain;
        background-position: right;
        background-image: url('../../assets/default.jpg');
        background-repeat: no-repeat;
        padding-left: 0 !important;
    }
    .el-form-item {
        margin-bottom: 0!important;
    }
    .el-form-item__content{
        margin-left: 0 !important;
    }
    .el-dialog__header{
        background: #409EFF;
    }
    .clearfix {
        text-align: center;
        color: #000000;
        font-size: 20px;
    }
    .clearfix:before, .clearfix:after {
        display: table;
        content: "";
    }
    .clearfix:after {
        clear: both
    }
}
</style>
消息列表

消息列表组件 Item.vue

<template>
    <div ref="messageLists">
        <!--群聊模板信息展示-->
        <div ref="message" v-contextmenu:contextmenu :source="source" class="item" v-if="source.to_client_name === 'all'" :class="source.from_client_name === userInfo.username ? 'creator' : '' ">
            <div class="avatar">
                <el-avatar :src="source.client_img" />
            </div>
            <div class="body">
                <div class="name" v-if="source.from_client_name !== userInfo.username">{{source.username || source.from_client_name}} {{source.time}}</div>
                <div class="content">
                    <div class="text"><span v-html="source.from_client_name === userInfo.username ? unescape(source.content.replace('%ACCOUNT%','你')) : unescape(source.content.replace('%ACCOUNT%',source.from_client_name))"/></div>
                </div>
            </div>
        </div>
        <!--私聊模板信息展示-->
        <div ref="message" v-contextmenu:contextmenu :source="source" class="item" v-else  :class="source.to_client_name === userInfo.username ? '' : 'creator' ">
            <div class="avatar">
                <el-avatar :src="source.client_img" />
            </div>
            <div class="body">
                <div class="name" v-if="source.to_client_name === userInfo.username">{{source.username || source.from_client_name}} {{source.time}}</div>
                <div class="content">
                    <div class="text"><span v-html="source.from_client_name === userInfo.username ? unescape(source.content.replace('%ACCOUNT%','你')) : unescape(source.content.replace('%ACCOUNT%',source.from_client_name))"/></div>
                </div>
            </div>
        </div>
        <!--右键菜单-->
        <v-contextmenu ref="contextmenu" theme="bright" @contextmenu="menuRightChange" v-if="source.from_client_name === userInfo.username && showContextMenu">
            <v-contextmenu-item @click="setMessages('recall')">消息撤回</v-contextmenu-item>
            <v-contextmenu-item divider/>
            <v-contextmenu-item @click="setMessages('delete')">消息删除</v-contextmenu-item>
        </v-contextmenu>
        <v-contextmenu ref="contextmenu" v-else/>
    </div>
</template>

<script>
    import {mapGetters} from 'vuex'
    import func from '../../api/func'
    import mixins from '../../api/dispatch'
    export default {
        name: 'ChatRoomItem',
        props: {
            source: {
                type: Object,
                default: () => {}
            }
        },
        mixins: [mixins],
        data () {
            return {
                targetMessage: {},
                showContextMenu: true
            }
        },
        computed: {
            ...mapGetters(['userInfo'])
        },
        methods: {
            /**
             * TODO:字符串标签转换
             * @param html
             */
            unescape: function (html) {
                return html
                    .replace(html ? /&(?!#?\w+;)/g : /&/g, '&amp;')
                    .replace(/&lt;/g, '<')
                    .replace(/&gt;/g, '>')
                    .replace(/&quot;/g, '"')
                    .replace(/&amp;nbsp;/g, ' ')
                    // eslint-disable-next-line no-useless-escape
                    .replace(/&#39;/g, "\'")
            },
            /**
             * todo:右键菜单
             * @param item
             */
            menuRightChange: function (item) {
                if (item.data.attrs && item.data.attrs.source) {
                    this.targetMessage = item.data.attrs.source
                }
                this.showContextMenu = this.targetMessage.type !== 'recall'
                // 三分钟后消息不可删除亦不可撤回
                if (this.targetMessage.type !== 'recall') {
                    this.showContextMenu = Date.parse(this.targetMessage.time) / 1000 + 180 > Date.parse(new Date()) / 1000
                }
            },
            /**
             * todo:消息撤回/消息删除
             */
            setMessages: function (item) {
                switch (item) {
                // 撤回消息
                case 'recall' :
                    let recallMessage = JSON.parse(JSON.stringify(this.targetMessage))
                    recallMessage.content = '%ACCOUNT%撤回了一条消息'
                    recallMessage.username = 'systemRobot'
                    recallMessage.client_img = 'https://cdn.pixabay.com/photo/2016/12/13/21/20/alien-1905155_960_720.png'
                    recallMessage.time = func.setTime(new Date())
                    this.dispatch('chatRoom', 'recallMessage', recallMessage, this.targetMessage)
                    break
                // 删除消息
                case 'delete':
                    let deleteMessage = JSON.parse(JSON.stringify(this.targetMessage))
                    this.dispatch('chatRoom', 'deleteMessage', deleteMessage, this.targetMessage)
                    break
                }
            }
        }
    }
</script>

<style lang="less" scoped>
.item {
    display: flex;
    .avatar {
        width: 40px;
        height: 40px;
        border-radius: 50%;
        background: rgba(255, 192, 203, .2);
        img {
            display: block;
            width: 100%;
            height: 100%;
            border-radius: 50%;
        }
    }
    .body {
        flex: 1;
        padding-left: 1em;
        font-size: 16px;
        max-width: 560px;
        word-break: break-word;
        .name {
            padding-bottom: .2em;
            font-size: 12px;
        }
        .content {
            position: relative;
            color: #000;
            background-color: aliceblue;
            border-radius: 15px;
            padding: .5em 1em;
            cursor: default;
            &:after{
                content: '';
                position: absolute;
                right: 100%;
                top: 10px;
                width: 14px;
                height: 14px;
                border-width: 0;
                border-style: solid;
                border-color: transparent;
                border-bottom-width: 10px;
                border-bottom-color: currentColor;
                border-radius: 0 0 0 32px;
                color: aliceblue;
            }
        }
    }
    &.creator {
        transform: rotateX(180deg);
        direction: rtl;
        align-items: flex-end;
        .avatar {
            transform: rotateX(180deg);
        }
        .body {
            transform: rotate(180deg);
        }
        .text {
            transform: rotateY(180deg);
            direction: ltr;
        }
    }
}
</style>
emotion表情包

 图表Emotion.vue组件

<template>
    <div class="emotion">
        <div class="emotion-box" :style="{height: height + 'px' }">
            <el-tabs type="border-card" tab-position="top" @tab-click="changeEmotionLists" v-model="type" v-infinite-scroll="scrollEmotion">
                <el-tab-pane v-for="(emotionType,index) in emotionTypeLists" :name="emotionType.type" :key="index" :label="emotionType.title">
                    <div class="emotion-box-line" v-for="(emotion, index) in emotionList" :key="index">
                        <div class="emotion-item" >
                            <el-image :src="emotion.icon" :alt="emotion.title" style="width: 30px;height: 30px;" lazy @click.native="clickHandler(emotion)"></el-image>
                        </div>
                    </div>
                </el-tab-pane>
            </el-tabs>
        </div>
    </div>
</template>

<script>
    import apiLists from '../../api/api'
    export default {
        props: {
            height: {
                type: Number,
                default: () => 300
            }
        },
        data () {
            return {
                emotionTypeLists: [
                    {'type': '1', 'title': '表情', 'id': 'emotion'},
                    {'type': '2', 'title': '人物', 'id': 'person'},
                    {'type': '3', 'title': '动作', 'id': 'action'},
                    {'type': '4', 'title': '家庭', 'id': 'family'},
                    {'type': '5', 'title': '自然', 'id': 'natural'},
                    {'type': '6', 'title': '食物', 'id': 'food'},
                    {'type': '7', 'title': '体育', 'id': 'sport'},
                    {'type': '8', 'title': '建筑', 'id': 'building'},
                    {'type': '9', 'title': '用品', 'id': 'supplies'},
                    {'type': '10', 'title': '符号', 'id': 'symbol'},
                    {'type': '11', 'title': '国旗', 'id': 'flag'}
                ],
                type: '1',
                emotionList: [],
                limit: 55,
                page: 1,
                pages: 0,
                offsetPage: 0
            }
        },
        methods: {
            /**
             * todo:图标切换
             * @param typeObj
             */
            changeEmotionLists: function (typeObj) {
                this.type = typeObj.name
                this.page = 1
                this.emotionList = []
                this.showEmotionLists(this.page, this.limit)
            },
            /**
             * todo:点击图标
             * @param emotion
             */
            clickHandler (emotion) {
                this.$emit('clickEmotion', emotion)
            },
            /**
             * TODO:展示表情数据
             * @param page
             * @param limit
             */
            showEmotionLists: function (page, limit) {
                apiLists.EmotionLists({type: this.type, limit: limit, page: page}).then(response => {
                    this.pages = response.data.item.pages
                    let data = response.data.item.data
                    for (let i in data) {
                        this.emotionList.push(data[i])
                    }
                })
            },
            /**
             * TODO:数据流加载
             */
            scrollEmotion: function () {
                this.offsetPage = this.offsetPage + 1
                if (this.offsetPage <= this.pages) {
                    this.page++
                    this.showEmotionLists(this.page, this.limit)
                }
            }
        },
        mounted () {
            this.$nextTick(function () {
                this.showEmotionLists(this.page, this.limit)
            })
        }
    }
</script>
<style scoped>
    .emotion{
        position: absolute;
        bottom: 310px;
        width: 500px;
        /*left: 300px;*/
    }
    .emotion-box {
        margin: 0 auto;
        width: 100%;
        box-sizing: border-box;
        padding: 0 0 5px 10px;
        overflow-x: hidden;
        overflow-y: auto;
        position: initial;
        z-index:2000
    }
    .emotion-box-line {
        display: flex;
        float: left;
    }
    .emotion-item {
        flex: 1;
        margin-left: 13px;
        margin-top: 10px;
        cursor: pointer;
    }
</style>

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
实现前端登录页面,可以使用Vue框架和相关插件,以下是一个简单的实现步骤: 1. 创建一个Vue项目,可以使用Vue-cli等工具。 2. 在src目录下创建一个views文件夹,用于存放页面组件。 3. 在views文件夹下创建一个Login.vue文件,用于编写登录页面的代码。 4. 在Login.vue中添加表单元素,包括用户名和密码输入框、登录按钮等。 5. 使用Vue-router插件配置路由,将Login.vue组件与路由对应。 6. 在Login.vue中编写登录逻辑,当用户点击登录按钮时,向后端发送登录请求。可以使用axios等插件发送请求。 7. 根据后端返回的结果,判断登录是否成功,如果成功则跳转到其他页面,否则显示错误信息。 下面是一个简单的示例代码: ```html <template> <div> <h1>登录</h1> <form> <input type="text" v-model="username" placeholder="用户名" /> <input type="password" v-model="password" placeholder="密码" /> <button type="button" @click="login">登录</button> </form> <p v-if="error">{{ error }}</p> </div> </template> <script> import axios from 'axios' export default { data() { return { username: '', password: '', error: '' } }, methods: { async login() { try { const response = await axios.post('/api/login', { username: this.username, password: this.password }) // 登录成功,跳转到其他页面 this.$router.push('/home') } catch (error) { // 登录失败,显示错误信息 this.error = '用户名或密码错误' } } } } </script> ``` 在上面的代码中,使用了axios插件发送登录请求,并根据后端返回的结果判断登录是否成功。如果成功,则通过`this.$router.push('/home')`跳转到其他页面。如果失败,则在页面上显示错误信息。 需要注意的是,上面的代码中的`/api/login`是后端提供的登录接口地址,需要根据实际情况进行修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值