VUE2实现聊天室输入框

实现功能

1-点击上传图标的方式,上传文件和图片

2-复制图片和文件的方式,上传文件和图片

3-拖拽图片和文件的方式,上传文件和图片

4-跟随光标位置插入表情

5-实现正常发送

话外题

        为了能通过多种方式上传文件和图片,考虑使用富文本编辑框,但是都不尽人意,在开发上都有些繁琐,于是干脆自己用VUE2写一个。一开始用的是可编辑div编写聊天室输入框,但是在插入表情的时候,发现无法获取插入在文本中的光标。于是输入框采用了textarea文本框的形式,上传的文件和图片类似于飞书的形式单独发送。

效果如下

解决思路

1-该段代码是输入框的编辑部分,@input="onInput"实时获取输入框内的数据,@paste="handlePaste"当用户粘帖内容到输入框的时候会被触发,@dragover.prevent可以控制拖拽操作行为,当用户在textarea上放下拖拽的内容时会调用@drop="handleDrop"方法。

2-该段代码是输入框展示文件和图片的预览模态框,showPreviewModal的值(true或者false)控制预览模态框的显示和隐藏。

发一下预览模态框的样式:

.modal-overlay {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: rgba(0, 0, 0, 0.5);
        display: flex;
        align-items: center;
        justify-content: center;
    }

    .modal-content {
        background: white;
        padding: 1rem;
        border-radius: 8px;
        width: auto;
        height: auto;
        max-width: 30rem;
        max-height: 50rem;
        overflow: auto;
    }

    .warning {
        font-weight: bold;
        color: rgb(255, 102, 0);
        margin: 1rem;
    }

3-当我发送文件后,我需要把我发送文件操作前保存的文件全部清空。

接下来附上源代码:

HTML

<div id="app">
                        <div class="chat_input">
                            <div class="emoji-tip">
                                <div class="emoji-selector" v-if="showEmojis">
                                    <span v-for="emoji in emojis" :key="emoji" @click="insertEmoji(emoji)"
                                        class="emoji-item">
                                        {{ emoji }}
                                    </span>
                                </div>
                                <div @click="toggleEmojiSelector" class="layui-icon layui-icon-face-smile-b" id="emoji"
                                    style="font-size: 20px; margin-right: 20px; display: inline-block;">
                                </div>
                                <div @click="uploadFile" class="layui-icon layui-icon-file-b" id="file"
                                    style="font-size: 20px; margin-right: 20px; display: inline-block;">
                                </div>
                            </div>


                            <textarea class="chat_edit" ref="editor" placeholder="点击输入内容..." contenteditable
                                @input="onInput" @paste="handlePaste" @dragover.prevent @drop="handleDrop"></textarea>

                            <div v-if="showPreviewModal" class="modal-overlay">
                                <div class="modal-content">
                                    <div class="warning">无法添加该类型的内容到输入框,将单独发送</div>
                                    <div v-for="(file, index) in previewFiles" :key="index" class="file-preview"
                                        style="margin: 0.5rem;">
                                        <div class="file-info" @mousedown.prevent contenteditable="false">
                                            <img v-if="file.type.startsWith('image')" :src="file.preview"
                                                style="max-width: 100%; max-height: 10rem;">
                                            <span v-else>{{ file.name }} ({{ getFileSize(file.size) }})</span>
                                            <i class="layui-icon layui-icon-close" @click="deleteFile(index)"></i>
                                        </div>
                                    </div>
                                    <button @click="closePreviewModal" class="close">关闭</button>
                                </div>
                            </div>
                        </div>
                        <button class="sendButton" @click="sendchat">
                            <span class="sendText">发送</span>
                        </button>
                        <input type="file" ref="fileInput" style="display: none" @change="handleFileChange">
                    </div>

VUE2

var vm = new Vue({
    el: '#app',
    data: {
        editorContent: '', // 如果需要编辑器内容绑定,可以添加此属性
        previewFiles: [],  // 用于存储预览的文件
        showPreviewModal: false,
        emojis: [
            "😀", "😁", "😂", "😃", "😄", "😅", "😆", "😉", "😊", "😋", "😎",
            "😍", "😘", "😗", "😙", "😚", "😇", "😐", "😑", "😶", "😏", "😣",
            "😥", "😮", "😯", "😪", "😫", "😴", "😌", "😛", "😜", "😝", "😒",
            "😓", "😔", "😕", "😲", "😷", "😖", "😞", "😟", "😤", "😢", "😭",
            "😦", "😧", "😨", "😬", "😰", "😱", "😳", "😵", "😡", "😠", "😈",
            "👿", "👹", "👺", "💀", "☠", "👻", "👽", "👾", "💣", "💋", "💌",
            "💘", "❤", "💓", "💔", "💕", "💖", "💗", "💙", "💚", "💛", "💜",
            "💝", "💞", "💟", "💏", "🧑‍🤝‍🧑", "💪", "👈", "👉", "☝", "👆", "👇",
            "✌", "✋", "👌", "👍", "👎", "✊", "👊", "👋", "👏", "👐", "✍"
        ],
        showEmojis: false,
    },
    methods: {
        // 当输入发生变化时触发,如果需要绑定编辑内容到 editorContent 属性,可以使用此方法
        onInput() {
            const editor = this.$refs.editor;
            this.editorContent = editor.value;
        },
        toggleEmojiSelector() {
            event.stopPropagation();
            this.showEmojis = !this.showEmojis;
        },
        insertEmoji(emoji) {
            const editor = this.$refs.editor; // 获取可编辑区域的引用
            if (!editor) return;
            // 获取当前光标位置
            const start = editor.selectionStart;
            const end = editor.selectionEnd;

            // 插入表情
            const textBefore = editor.value.substring(0, start);
            const textAfter = editor.value.substring(end, editor.value.length);
            editor.value = textBefore + emoji + textAfter;
            this.editorContent = editor.value;

            // 设置新的光标位置
            editor.selectionStart = editor.selectionEnd = start + emoji.length;
        },
        // 触发文件选择对话框
        uploadFile() {
            this.$refs.fileInput.click();
        },
        closePreviewModal() {
            this.showPreviewModal = false;
            this.previewFiles = []; // 清空文件预览列表
        },
        handlePaste(event) {
            const items = (event.clipboardData || event.originalEvent.clipboardData).items;
            for (let index in items) {
                const item = items[index];
                if (item.kind === 'file') {
                    const blob = item.getAsFile();
                    const file = {
                        name: blob.name,
                        size: blob.size,
                        type: blob.type,
                        preview: URL.createObjectURL(blob),
                        blob: blob
                    };
                    this.previewFiles.push(file);
                    this.showPreviewModal = true;
                } else if (item.kind === 'string') {
                    // item.getAsString(text => {
                    //     // 在这里处理粘贴的文本内容
                    // });
                }
            }
        },
        handleDrop(event) {
            event.preventDefault();
            const files = event.dataTransfer.files;
            for (let i = 0; i < files.length; i++) {
                const file = files[i];
                this.addFileToPreview(file);
            }
            this.showPreviewModal = true;
        },
        handleFileChange(event) {
            const files = event.target.files;
            this.previewFiles = [];
            this.showPreviewModal = true;
            if (files.length > 0) {
                const file = files[0];
                this.addFileToPreview(file);
            }
            // 清空文件选择器的值,以便允许再次选择相同文件
            event.target.value = '';
        },
        addFileToPreview(file) {
            const reader = new FileReader();
            if (file.size < 10000000) {
                reader.onload = () => {
                    const previewFile = {
                        name: file.name,
                        size: file.size,
                        type: file.type,
                        blob: file,
                        preview: reader.result
                    };
                    this.previewFiles.push(previewFile);
                };
            } else {
                layer.msg(file.name + "大小超过限制。允许最大上传大小为 10MB。");
                return;
            }
            reader.readAsDataURL(file);
        },
        // 发送消息
        sendchat() {
            const formData = new FormData();
            const editorContent = this.editorContent;
            this.showPreviewModal = false;
            // 添加文件
            this.previewFiles.forEach(file => {
                formData.append('files[]', file.blob, file.name);
            });

            // 获取当前日期和时间
            var year = new Date().getFullYear();
            var monthDay = ('0' + (new Date().getMonth() + 1)).slice(-2) + '-' + ('0' + new Date().getDate()).slice(-2);
            var time = ('0' + (new Date().getHours())).slice(-2) + ':' + ('0' + new Date().getMinutes()).slice(-2) + ':' + ('0' + new Date().getSeconds()).slice(-2);
            var date = year + '-' + monthDay + ' ' + time;

            // 初始化 HTML 字符串
            let html = '';

            // 发起 AJAX 请求
            $.ajax({
                url: 'index.php?s=Httpapi&c=Upload&m=chatFileUpload',
                type: 'POST',
                data: formData,
                processData: false,
                contentType: false,
                success: function (response) {
                    if (vm.previewFiles != "") {
                        if (response.code == 1) {
                            // 构建头部信息
                            html += `
                                <div class="chat_message my-bubble1">
                                <div class="chat_message_top">
                                    <div class="chat_time">${date}</div>
                                    <div class="partner_name private">${username}</div>
                                    <img src="${userphoto}" alt="" class="partner_photo private" style="margin-right: 20px;">                    
                                </div>`;

                            response.data.forEach(res => {
                                if (res.msg === "上传成功") {
                                    if (res.type.startsWith('image/')) {
                                        // 如果是图片类型
                                        html += `
                                            <div class="chatfile">
                                                <div class="my-file-info">
                                                    <img src="${res.url}" style="width: 100%;">
                                                </div>
                                            </div>`;
                                    } else {
                                        // 如果是普通文件类型
                                        html += `
                                            <div class="my-bubble2">
                                                <a class="my-file-info" href="${res.url}" download>
                                                    <img src="/static/envtools/web/images/sys-photo/file.png" class="file-icon">
                                                    <div class="file-details">
                                                        <div class="file-name">${res.name}</div>
                                                        <div class="file-meta">
                                                            <span class="file-size">${res.size}</span>
                                                            <span class="file-status">${res.msg}</span>
                                                        </div>
                                                    </div>
                                                </a>
                                            </div>`;
                                    }
                                } else {
                                    // 处理上传失败的情况
                                    html += `
                                            <div class="my-bubble2">
                                                <span class="file-status">上传失败</span>
                                            </div>`;
                                }
                            });

                            // 结束文件容器
                            html += `</div>`;
                        } else {
                            // 如果上传失败
                            html = `<div class="chat_message my-bubble1">
                                        <div class="chat_message_top">
                                            <div class="chat_time"> ${date}</div>
                                            <div class="partner_name private">${username}</div>
                                            <img src="${userphoto}" alt="" class="partner_photo private" style="margin-right: 20px;">                    
                                        </div>
                                        <div class="my-bubble2">
                                            <span class="bubble_text">上传失败</span>
                                        </div>
                                    </div>`;
                        }
                    }

                    var active_chat = $('.chat_info');
                    var oldHtml = active_chat.html();
                    active_chat.html(oldHtml + html);
                    active_chat.scrollTop(active_chat[0].scrollHeight);

                    // 清空 Vue 实例中的数据
                    vm.editorContent = '';
                    vm.previewFiles = [];
                    vm.$refs.editor.value = '';
                    // 调用 send 函数
                    send(editorContent, response.data);
                },
                error: function () {
                    layer.msg("请求失败,请检查网络连接或者服务器状态。");
                }
            });
        },

        // 格式化文件大小显示
        getFileSize(bytes) {
            const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
            if (bytes == 0) return '0 Byte';
            const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
            return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
        },
        // 删除文件
        deleteFile(index) {
            event.stopPropagation();
            this.previewFiles.splice(index, 1);
        },
    }
});

表情插入就不细讲了,文本框根据光标插入表情还是较为简单的。

总结

        整体完成后,感觉做一个输入框还是比较轻松的。但是因为一开始并没有接触Vue,所以刚上手还是有一定的难度;再加上开发的时候有一些角度考虑不周,于是改了又改,最终改成现在的版本。总体上还算满意,如果以后有了新的功能或者学习到了新的技术,有时间的话还是会不断地优化。

        程序开发就是要不断学习,征途漫漫,诸君勉励。

要在IDEA中创建Vue3项目,你可以按照以下步骤进行操作: 1. 打开IDEA,并确保已经安装了Vue.js插件。 2. 在IDEA的主菜单中选择“File”(文件)>“New”(新建)>“Project”(项目)。 3. 在弹出的对话框中,选择“Vue.js”或“JavaScript”作为项目类型。 4. 在项目设置中,选择Vue版本为Vue3。 5. 输入项目名称和存储位置,并点击“Next”(下一步)。 6. 在下一步中,可以选择使用Vue CLI来创建项目,或者手动配置项目选项。 7. 如果选择手动配置项目选项,则需要输入一些基本信息,如项目模板、包管理器等。 8. 点击“Finish”(完成)来创建Vue3项目。 这样,IDEA将会根据你的选择和配置创建一个Vue3项目。你可以在IDEA中编辑和管理你的Vue3项目,并使用Vue.js插件提供的功能来提高开发效率。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [如何创建 且在idea中操作vue3项目](https://blog.youkuaiyun.com/qq_49249150/article/details/127430900)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [2023 最新版IntelliJ IDEA 2023.1创建Java Web前(vue3)后端(spring-boot3)分离 项目详细步骤(图文详解...](https://blog.youkuaiyun.com/weixin_51033461/article/details/130983914)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏木子杉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值