最终效果
文章来源:https://www.toymoban.com/news/detail-464766.html
完整代码
index.vue
<template>
<div class="page">
<div class="leftBox">
<h1>访客</h1>
<div class="chatBox">
<div class="chatRecordBox">
<div v-for="(item, index) in chatRecordList" :key="index">
<RightRole
v-if="item.role === '访客'"
:type="item.type"
:content="item.content"
:avatarURL="visitorAvatarURL"
@openMenu="openMenu($event, index, true)"
/>
<LeftRole
v-if="item.role === '客服'"
:type="item.type"
:content="item.content"
:avatarURL="serviceAvatarURL"
@openMenu="openMenu($event, index)"
/>
</div>
</div>
<div class="toolBox">
<ChooseEmotion @getEmo="chooseEmo($event, 'visitor')" />
<img
@click="chooseImg('visitor')"
class="imgIcon"
src="@/assets/images/图片.svg"
/>
<img
@click="chooseVideo('visitor')"
class="videoIcon"
src="@/assets/images/视频.svg"
/>
<img
@click="chooseAudio('visitor')"
class="audioIcon"
src="@/assets/images/音频.svg"
/>
</div>
<div
ref="visitorSendContentBox_Ref"
contenteditable="true"
class="sendContentBox"
></div>
<div class="btnBox">
<el-button @click="visitorSend" size="small">发送</el-button>
</div>
</div>
</div>
<div class="rightBox">
<h1>客服</h1>
<div class="chatBox">
<div class="chatRecordBox">
<div v-for="(item, index) in chatRecordList" :key="index">
<LeftRole
v-if="item.role === '访客'"
:type="item.type"
:content="item.content"
:avatarURL="visitorAvatarURL"
@openMenu="openMenu($event, index)"
/>
<RightRole
v-if="item.role === '客服'"
:type="item.type"
:content="item.content"
:avatarURL="serviceAvatarURL"
@openMenu="openMenu($event, index, true)"
/>
</div>
</div>
<div class="toolBox">
<ChooseEmotion @getEmo="chooseEmo($event, 'service')" />
<img
@click="chooseImg('service')"
class="imgIcon"
src="@/assets/images/图片.svg"
/>
<img
@click="chooseVideo('service')"
class="videoIcon"
src="@/assets/images/视频.svg"
/>
<img
@click="chooseAudio('service')"
class="audioIcon"
src="@/assets/images/音频.svg"
/>
</div>
<div
ref="serviceSendContentBox_Ref"
contenteditable="true"
class="sendContentBox"
></div>
<div class="btnBox">
<el-button @click="serviceSend" size="small">发送</el-button>
</div>
</div>
<el-dialog
title="图片预览"
:visible.sync="showPreviewImgWin"
style="text-align: center"
>
<el-image
style="height: 400px"
:preview-src-list="[imgSrc]"
:src="imgSrc"
fit="contain"
/>
</el-dialog>
<!-- 右键快捷菜单 -->
<ul
v-show="quickMenuVisible"
:style="{ left: quickMenu_left + 'px', top: quickMenu_top + 'px' }"
class="contextmenu"
>
<li v-show="ifCan_rollBack" @click="rollBack">撤回</li>
<li @click="copy">复制</li>
</ul>
</div>
</div>
</template>
<script>
import LeftRole from "./components/leftRole.vue";
import RightRole from "./components/rightRole.vue";
import ChooseEmotion from "./components/chooseEmotion.vue";
import visitorAvatarURL from "@/assets/images/访客.svg";
import serviceAvatarURL from "@/assets/images/客服.svg";
export default {
watch: {
quickMenuVisible(value) {
if (value) {
document.body.addEventListener("click", this.closeMenu);
} else {
document.body.removeEventListener("click", this.closeMenu);
}
},
},
components: { LeftRole, RightRole, ChooseEmotion },
mounted() {
let that = this;
// 点击图片放大预览
window.addEventListener("click", function (e) {
let { target } = e;
let nodeName = target.nodeName;
if (nodeName === "IMG") {
// 获取自定义的属性 preview
let preview = target.getAttribute("preview");
// 无 preview 属性的图片,不支持点击放大预览
if (!preview) {
return;
}
that.imgSrc = target.currentSrc;
that.showPreviewImgWin = true;
}
});
},
data() {
return {
ifCan_rollBack: false,
index: null,
quickMenu_left: 0,
quickMenu_top: 0,
quickMenuVisible: false,
imgSrc: "",
showPreviewImgWin: false,
visitorAvatarURL,
serviceAvatarURL,
chatRecordList: [],
};
},
methods: {
// 撤回消息
rollBack() {
this.chatRecordList.splice(this.index, 1);
},
// js 点击复制到剪贴板函数
copy() {
let content = this.chatRecordList[this.index].content;
if (window.clipboardData) {
window.clipboardData.setData("text", content);
} else {
(function (content) {
document.oncopy = function (e) {
e.clipboardData.setData("text", content);
e.preventDefault();
document.oncopy = null;
};
})(content);
document.execCommand("Copy");
}
},
// 显示右键快捷菜单
openMenu(e, index, ifCan_rollBack) {
this.ifCan_rollBack = ifCan_rollBack;
this.index = index;
this.quickMenu_top = e.pageY;
this.quickMenu_left = e.pageX;
this.quickMenuVisible = true;
},
// 隐藏右键快捷菜单
closeMenu() {
this.quickMenuVisible = false;
},
// 根据角色,确定编辑框
getSendContentBox_Ref(role) {
if (role === "visitor") {
return "visitorSendContentBox_Ref";
}
if (role === "service") {
return "serviceSendContentBox_Ref";
}
},
// 在编辑框中插入内容
sendContentBox_insert(role, insertContent) {
let sendContentBox_Ref = this.getSendContentBox_Ref(role);
let content = JSON.parse(
JSON.stringify(this.$refs[sendContentBox_Ref].innerHTML)
);
let newContent = "";
// 光标在编辑框内时
if (window.getSelection().anchorNode) {
// 获取光标在编辑框中的下标
let startIndex = window.getSelection().anchorOffset;
let ednIndex = window.getSelection().focusOffset;
newContent =
content.substring(0, startIndex) +
insertContent +
content.substring(ednIndex);
} else {
// 光标不在编辑框内时
newContent = content + insertContent;
}
this.$refs[sendContentBox_Ref].innerHTML = newContent;
},
chooseEmo(emo, role) {
this.sendContentBox_insert(role, emo);
},
// 访客发送消息
visitorSend() {
let content = this.$refs.visitorSendContentBox_Ref.innerHTML;
if (!content) {
this.$message({
message: "请输入发送内容!",
type: "warning",
});
return;
}
this.chatRecordList.push({
role: "访客",
content: content,
type: "text",
});
// 发送后,清空发送框的内容
this.$refs.visitorSendContentBox_Ref.innerHTML = "";
},
// 客服发送消息
serviceSend() {
let content = this.$refs.serviceSendContentBox_Ref.innerHTML;
if (!content) {
this.$message({
message: "请输入发送内容!",
type: "warning",
});
return;
}
this.chatRecordList.push({
role: "客服",
content: content,
type: "text",
});
// 发送后,清空发送框的内容
this.$refs.serviceSendContentBox_Ref.innerHTML = "";
},
// 选择图片
chooseImg(role) {
let that = this;
let input = document.createElement("input");
input.setAttribute("type", "file");
// 支持多选
input.setAttribute("multiple", "multiple");
input.accept = "image/*";
input.addEventListener("change", (e) => {
let file = e.path[0].files[0];
// 浏览器兼容性处理(有的浏览器仅存在 Window.URL)
const windowURL = window.URL || window.webkitURL;
// createObjectURL 函数会根据传入的参数创建一个指向该参数对象的URL
let filePath = windowURL.createObjectURL(file);
let tmp_imgDom = document.createElement("img");
tmp_imgDom.setAttribute("src", filePath);
tmp_imgDom.setAttribute("height", 30);
tmp_imgDom.setAttribute("preview", true);
tmp_imgDom.style.cursor = "pointer";
let tmp_divDom = document.createElement("div");
tmp_divDom.appendChild(tmp_imgDom);
that.chatRecordList.push({
role: role === "visitor" ? "访客" : "客服",
content: tmp_divDom.innerHTML,
type: "img",
});
});
input.click();
},
// 选择视频
chooseVideo(role) {
let that = this;
let input = document.createElement("input");
input.setAttribute("type", "file");
// 支持多选
input.setAttribute("multiple", "multiple");
input.accept = "video/*";
input.addEventListener("change", (e) => {
let file = e.path[0].files[0];
// 浏览器兼容性处理(有的浏览器仅存在 Window.URL)
const windowURL = window.URL || window.webkitURL;
// createObjectURL 函数会根据传入的参数创建一个指向该参数对象的URL
let filePath = windowURL.createObjectURL(file);
let tmp_videoDom = document.createElement("video");
tmp_videoDom.setAttribute("src", filePath);
tmp_videoDom.setAttribute("height", 100);
tmp_videoDom.setAttribute("controls", "controls");
tmp_videoDom.style.cursor = "pointer";
let tmp_divDom = document.createElement("div");
tmp_divDom.appendChild(tmp_videoDom);
that.chatRecordList.push({
role: role === "visitor" ? "访客" : "客服",
content: tmp_divDom.innerHTML,
type: "video",
});
});
input.click();
},
// 选择音频
chooseAudio(role) {
let that = this;
let input = document.createElement("input");
input.setAttribute("type", "file");
// 支持多选
input.setAttribute("multiple", "multiple");
input.accept = "audio/*";
input.addEventListener("change", (e) => {
let file = e.path[0].files[0];
// 浏览器兼容性处理(有的浏览器仅存在 Window.URL)
const windowURL = window.URL || window.webkitURL;
// createObjectURL 函数会根据传入的参数创建一个指向该参数对象的URL
let filePath = windowURL.createObjectURL(file);
let tmp_audioDom = document.createElement("audio");
tmp_audioDom.setAttribute("src", filePath);
tmp_audioDom.setAttribute("height", 30);
tmp_audioDom.setAttribute("controls", "controls");
tmp_audioDom.style.cursor = "pointer";
let tmp_divDom = document.createElement("div");
tmp_divDom.appendChild(tmp_audioDom);
that.chatRecordList.push({
role: role === "visitor" ? "访客" : "客服",
content: tmp_divDom.innerHTML,
type: "audio",
});
});
input.click();
},
},
};
</script>
<style scoped>
.page {
display: flex;
justify-content: space-around;
}
.chatBox {
width: 400px;
padding: 10px;
background: #409eff;
border-radius: 10px;
}
.chatRecordBox {
padding: 10px;
height: 400px;
border-radius: 10px;
background: white;
overflow: auto;
}
.sendContentBox {
height: 100px;
padding: 10px;
background: white;
overflow: auto;
border: 1px solid rgb(195, 187, 187);
}
.btnBox {
padding-top: 10px;
text-align: right;
}
h1 {
line-height: 40px;
font-weight: bold;
}
.toolBox {
padding: 4px;
background: white;
margin-top: 10px;
display: flex;
align-items: center;
}
img {
cursor: pointer !important;
display: inline-block;
}
.imgIcon {
margin-left: 4px;
height: 20px;
cursor: pointer;
}
.videoIcon {
width: 22px;
cursor: pointer;
margin-left: 4px;
}
.audioIcon {
width: 18px;
cursor: pointer;
margin-left: 4px;
border-radius: 4px;
}
/* 右键快捷菜单的样式 */
.contextmenu {
margin: 0;
background: #fff;
z-index: 3000;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
}
.contextmenu li {
margin: 0;
padding: 7px 16px;
cursor: pointer;
}
.contextmenu li:hover {
background: #eee;
}
</style>
components/leftRole.vue
<template>
<div class="leftRoleBox">
<div>
<img class="avatar" :src="avatarURL" />
</div>
<!-- 左侧的聊天气泡尖角 -->
<div v-if="type === 'text'" class="triangle-top-left">
<div class="triangle-top-left2"></div>
</div>
<!-- 聊天气泡 -->
<div
@contextmenu.prevent="openMenu($event)"
v-html="content"
:class="type === 'text' ? 'bubble-left' : 'multimediaBox'"
></div>
</div>
</template>
<script>
export default {
props: {
type: String,
content: String,
avatarURL: String,
},
methods: {
openMenu(e) {
this.$emit("openMenu", e);
},
},
};
</script>
<style scoped>
.leftRoleBox {
display: flex;
align-items: flex-start;
}
.avatar {
width: 30px;
}
.triangle-top-left {
position: relative;
border-top: 4px solid #409eff;
border-left: 4px solid transparent;
margin-top: 20px;
margin-left: 2px;
}
.triangle-top-left2 {
top: -2px;
left: 0px;
position: absolute;
border-top: 4px solid #ffffff;
border-left: 4px solid transparent;
}
.bubble-left {
text-align: justify;
border-radius: 4px;
padding: 4px;
border: 2px solid #409eff;
font-size: 12px;
line-height: 16px;
margin-top: 10px;
}
.multimediaBox {
padding: 10px;
}
</style>
components/rightRole.vue
<template>
<div class="rightRoleBox">
<div>
<img class="avatar" :src="avatarURL" />
</div>
<!-- 左侧的聊天气泡尖角 -->
<div v-if="type === 'text'" class="triangle-top-right">
<div class="triangle-top-right2"></div>
</div>
<!-- 聊天气泡 -->
<div
@contextmenu.prevent="openMenu($event)"
v-html="content"
:class="type === 'text' ? 'bubble-right' : 'multimediaBox'"
></div>
</div>
</template>
<script>
export default {
props: {
type: String,
content: String,
avatarURL: String,
},
methods: {
openMenu(e) {
this.$emit("openMenu", e);
},
},
};
</script>
<style scoped>
.rightRoleBox {
display: flex;
align-items: flex-start;
flex-direction: row-reverse;
}
.avatar {
width: 30px;
}
.triangle-top-right {
position: relative;
border-top: 4px solid #409eff;
border-right: 4px solid transparent;
margin-top: 20px;
margin-right: 2px;
}
.triangle-top-right2 {
top: -2px;
right: 0px;
position: absolute;
border-top: 4px solid #ffffff;
border-right: 4px solid transparent;
}
.bubble-right {
text-align: justify;
border-radius: 4px;
padding: 4px;
border: 2px solid #409eff;
font-size: 12px;
line-height: 16px;
margin-top: 10px;
}
.multimediaBox {
padding: 10px;
}
</style>
components/chooseEmotion.vue
<template>
<div class="chatIcon">
<el-popover placement="top-start" width="400" trigger="hover">
<div class="emotionList">
<a
href="javascript:void(0);"
@click="getEmo(index)"
v-for="(item, index) in faceList"
:key="index"
class="emotionItem"
>{{ item }}</a
>
</div>
<img height="20" slot="reference" src="@/assets/images/表情.svg" />
</el-popover>
</div>
</template>
<script>
import emojiData from "@/assets/data/emoji.json";
export default {
mounted() {
for (let i in emojiData) {
this.faceList.push(emojiData[i].char);
}
},
data() {
return {
faceList: [],
};
},
methods: {
getEmo(index) {
this.$emit("getEmo", this.faceList[index]);
},
},
};
</script>
<style scoped>
.chatIcon {
font-size: 25px;
}
.emotionList {
display: flex;
flex-wrap: wrap;
padding: 5px;
}
.emotionItem {
width: 10%;
font-size: 20px;
text-align: center;
}
/*包含以下四种的链接*/
.emotionItem {
text-decoration: none;
}
/*正常的未被访问过的链接*/
.emotionItem:link {
text-decoration: none;
}
/*已经访问过的链接*/
.emotionItem:visited {
text-decoration: none;
}
/*鼠标划过(停留)的链接*/
.emotionItem:hover {
text-decoration: none;
}
/* 正在点击的链接*/
.emotionItem:active {
text-decoration: none;
}
</style>
src\assets\data\emoji.json
[
{
"codes": "1F600",
"char": "😀",
"name": "grinning face"
},
{
"codes": "1F603",
"char": "😃",
"name": "grinning face with big eyes"
},
{
"codes": "1F604",
"char": "😄",
"name": "grinning face with smiling eyes"
},
{
"codes": "1F601",
"char": "😁",
"name": "beaming face with smiling eyes"
},
{
"codes": "1F606",
"char": "😆",
"name": "grinning squinting face"
},
{
"codes": "1F605",
"char": "😅",
"name": "grinning face with sweat"
},
{
"codes": "1F923",
"char": "🤣",
"name": "rolling on the floor laughing"
},
{
"codes": "1F602",
"char": "😂",
"name": "face with tears of joy"
},
{
"codes": "1F642",
"char": "🙂",
"name": "slightly smiling face"
},
{
"codes": "1F643",
"char": "🙃",
"name": "upside-down face"
},
{
"codes": "1F609",
"char": "😉",
"name": "winking face"
},
{
"codes": "1F60A",
"char": "😊",
"name": "smiling face with smiling eyes"
},
{
"codes": "1F607",
"char": "😇",
"name": "smiling face with halo"
},
{
"codes": "1F970",
"char": "🥰",
"name": "smiling face with hearts"
},
{
"codes": "1F60D",
"char": "😍",
"name": "smiling face with heart-eyes"
},
{
"codes": "1F929",
"char": "🤩",
"name": "star-struck"
},
{
"codes": "1F618",
"char": "😘",
"name": "face blowing a kiss"
},
{
"codes": "1F617",
"char": "😗",
"name": "kissing face"
},
{
"codes": "1F61A",
"char": "😚",
"name": "kissing face with closed eyes"
},
{
"codes": "1F619",
"char": "😙",
"name": "kissing face with smiling eyes"
},
{
"codes": "1F44B",
"char": "👋",
"name": "waving hand"
},
{
"codes": "1F91A",
"char": "🤚",
"name": "raised back of hand"
},
{
"codes": "1F590",
"char": "🖐",
"name": "hand with fingers splayed"
},
{
"codes": "270B",
"char": "✋",
"name": "raised hand"
},
{
"codes": "1F596",
"char": "🖖",
"name": "vulcan salute"
},
{
"codes": "1F44C",
"char": "👌",
"name": "OK hand"
},
{
"codes": "1F90F",
"char": "🤏",
"name": "pinching hand"
},
{
"codes": "270C",
"char": "✌",
"name": "victory hand"
},
{
"codes": "1F91E",
"char": "🤞",
"name": "crossed fingers"
},
{
"codes": "1F91F",
"char": "🤟",
"name": "love-you gesture"
},
{
"codes": "1F918",
"char": "🤘",
"name": "sign of the horns"
},
{
"codes": "1F919",
"char": "🤙",
"name": "call me hand"
},
{
"codes": "1F448",
"char": "👈",
"name": "backhand index pointing left"
},
{
"codes": "1F449",
"char": "👉",
"name": "backhand index pointing right"
},
{
"codes": "1F446",
"char": "👆",
"name": "backhand index pointing up"
},
{
"codes": "1F595",
"char": "🖕",
"name": "middle finger"
},
{
"codes": "1F447",
"char": "👇",
"name": "backhand index pointing down"
},
{
"codes": "261D FE0F",
"char": "☝️",
"name": "index pointing up"
},
{
"codes": "1F44D",
"char": "👍",
"name": "thumbs up"
},
{
"codes": "1F44E",
"char": "👎",
"name": "thumbs down"
},
{
"codes": "270A",
"char": "✊",
"name": "raised fist"
},
{
"codes": "1F44A",
"char": "👊",
"name": "oncoming fist"
},
{
"codes": "1F91B",
"char": "🤛",
"name": "left-facing fist"
},
{
"codes": "1F91C",
"char": "🤜",
"name": "right-facing fist"
}
]
配套图片素材下载
链接:https://pan.baidu.com/s/170pb-MJlMxG2nRj_3Y2VFw?pwd=oknr
提取码:oknr文章来源地址https://www.toymoban.com/news/detail-464766.html
到了这里,关于vue 在线聊天实战范例(含选择发送表情、图片、视频、音频,自定义右键快捷菜单,一键复制,左右聊天气泡)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!