实现功能:
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,所以刚上手还是有一定的难度;再加上开发的时候有一些角度考虑不周,于是改了又改,最终改成现在的版本。总体上还算满意,如果以后有了新的功能或者学习到了新的技术,有时间的话还是会不断地优化。
程序开发就是要不断学习,征途漫漫,诸君勉励。