系列包含
服务端创建websocket
1. 创建文件socket.js
/* ws创建 */
function socketCreate(fastify) {
/* 构建ws服务 */
const ws = new (require('ws').Server)({ port: 9798 });
ws.on('connection', (sk) => {
/* 设置一个连接id */
let id = UUID()
/* 存储当前链接 */
fastify.storage.socket[id] = sk
/* 获取消息 */
fastify.storage.socket[id].on('message', (msg) => {
let data = JSON.parse(msg)
let resMsg = { type: data.type }
/* 用户 */
if (data.type === "user") {
/* 查询当前是否有客服在线 没用等待分配 */
if (!fastify.storage.loginStatus.length) {
/* 存储用户等待分配 */
fastify.storage.allocation.push({ id: id, list: [] })
resMsg.message = "正在为您安排客服..."
resMsg.pid = "-1"
resMsg.id = id
fastify.storage.socket[id].send(JSON.stringify(resMsg));
return
}
/* 分配用户到当前连接用户最少的在线客服上 */
let itemInfo = {}
fastify.storage.loginStatus.forEach(item => {
if (!itemInfo.client) {
itemInfo = item
}
if (item.client.length < itemInfo.client.length) {
itemInfo = item
}
});
resMsg.message = "有什么可以帮到您的~~~"
resMsg.pid = itemInfo.id
resMsg.id = id
fastify.storage.socket[id].send(JSON.stringify(resMsg));
let resMsgService = {
type: "addUser",
userList: [{
id: id,
list: []
}]
}
fastify.storage.socket[itemInfo.id].send(JSON.stringify(resMsgService));
return
}
/* 客服 */
if (data.type === "service") {
/* 替换连接id */
fastify.storage.socket[data.token] = fastify.storage.socket[id]
delete fastify.storage.socket[id]
/* 查询是否有等待的用户 有直接分配到该客服上*/
if (fastify.storage.allocation.length) {
let item = fastify.storage.loginStatus.find(item => item.id === data.token)
if (item) {
item.client = fastify.storage.allocation
resMsg.userList = item.client
fastify.storage.socket[data.token].send(JSON.stringify(resMsg));
/* 通知用户已经匹配到客服 */
fastify.storage.allocation.forEach(item => {
resMsg.type = "message"
resMsg.message = "有什么可以帮到您的~~~"
resMsg.pid = data.token
delete resMsg.userList
fastify.storage.socket[item.id].send(JSON.stringify(resMsg));
});
}
return
}
}
/* 消息处理 */
if (data.type === "message") {
/* 客服还未连接 存储问题内容*/
if (data.pid === "-1") {
let item = fastify.storage.allocation.find(item => item.id === data.id)
item.list.push({
pid: item.id,
message: data.message
})
resMsg.type = "other"
fastify.storage.socket[data.id].send(JSON.stringify(resMsg));
return
}
let pid = data.pid
let id = data.id
data.pid = id
data.id = pid
fastify.storage.socket[pid].send(JSON.stringify(data));
return
}
})
});
}
/**
* 随机min和max之间的值且包含min,max的值
* @param { Number } min 最小值
* @param { Number } max 最大值
* @return { Number } 随机值
*/
function random(min, max) {
return Math.round(Math.random() * (max - min) + min);
}
/**
* 获取uuid字符串
* @param { Number } len uuid长度
* @return { String } uuid
*/
function UUID(len = 32) {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('')
const uuid = []
let cLen = chars.length
for (let i = 0; i < len; i++) {
let r = random(0, cLen - 1);
uuid.push(chars[(i == 19) ? (r & 0x3) | 0x8 : r])
}
let uLen = uuid.length
let timeStr = String(new Date().getTime())
for (let i = 0; i < timeStr.length; i++) {
let r = random(0, uLen - 1);
uuid[r] = timeStr[i]
}
return uuid.join('');
}
module.exports = socketCreate
2. 在index.js内添加调用该文件创建socket函数
服务端websocket业务功能说明
1. 监听到连接分配给该连接一个id,并存储该连接
2. 判断连接是否是用户还是客服
- 是用户分配给当前接待客户最少的客服,如果当前没用客服在线,就存储当前客户等带客服上线
- 是客服就查询当前是否有等待客户,存在就分配给当前客服,不存在就等待系统分配客户
服务端storage.js数据存储文件新添加连接的socket和等待的客户
客户端index.vue页面添加聊天业务代码
<template>
<div class="index">
<div class="main">
<div class="userList" v-if="token">
<div
class="userItem"
:class="data.pid === item.id ? 'userItemAct' : ''"
v-for="item in data.userList"
:key="item.id"
@click="sel(item)"
>
<el-avatar :src="userAvatar" class="avatar" :size="30" />
<div class="name">用户{{ item.id }}</div>
</div>
</div>
<div class="centent">
<div class="chat">
<div class="chatTitle">
{{ token ? "用户" : "客服" }}{{ data.pid === "-1" ? "" : data.pid }}
</div>
<div class="chatMain">
<div v-for="(item, index) in data.list" :key="index">
<div class="chatItem" v-if="item.pid">
<el-avatar
:src="token ? userAvatar : serviceAvatar"
class="avatar"
:size="30"
/>
<div class="chatText">{{ item.message }}</div>
</div>
<div class="chatItem chatItemR" v-if="!item.pid">
<div class="chatText chatTextR">{{ item.message }}</div>
<el-avatar
:src="token ? serviceAvatar : userAvatar"
class="avatar"
:size="30"
/>
</div>
</div>
</div>
</div>
<div class="input">
<el-input
v-model="data.textarea"
type="textarea"
resize="none"
autofocus
class="textarea"
/>
<div class="send">
<el-button type="primary" @click="send">发送</el-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import userAvatar from "@/assets/用户.png";
import serviceAvatar from "@/assets/客服.png";
import { inject } from "vue";
const app = inject("app");
let socket = null;
let token = app.storage.getToken();
const data = app.reactive({
/* 发送文本 */
textarea: "",
/* 聊天数据 */
list: [],
/* 用户列表 */
userList: [],
/* 用户临时id */
id: "",
/* 对方id */
pid: "",
});
/* 切换用户 */
function sel(item) {
if (item.id === data.pid) return;
data.pid = item.id;
data.list = JSON.parse(JSON.stringify(item.list));
}
/* 发送消息 */
function send() {
if (!data.textarea) return;
socket.send(
JSON.stringify({
token,
type: "message",
message: data.textarea,
id: data.id,
pid: data.pid,
})
);
data.list.push({ message: data.textarea });
if (token) {
let item = data.userList.find((item) => item.id === data.pid);
item.list.push({ message: data.textarea });
}
data.textarea = "";
}
function onload() {
/* 连接socket */
socket = new WebSocket(process.env.VUE_APP_WS_URL);
socket.onopen = () => {
/* 链接后发送消息 */
socket.send(JSON.stringify({ token, type: token ? "service" : "user" }));
};
socket.onerror = (err) => {
console.log("链接失败:", err);
};
socket.onmessage = (e) => {
let msg = JSON.parse(e.data);
console.log("数据:", msg);
/* 身份 */
if (msg.type === "user") {
data.id = msg.id;
data.pid = msg.pid;
data.list.push(msg);
return;
}
/* 客服 */
if (msg.type === "service") {
data.id = token;
if (!msg.userList.length) return;
data.userList = msg.userList;
data.pid = data.userList[0].id;
data.list = JSON.parse(JSON.stringify(data.userList[0].list));
return;
}
/* 添加用户 */
if (msg.type === "addUser") {
data.userList = [...data.userList,...msg.userList];
return;
}
/* 消息 */
if (msg.type === "message") {
data.pid = msg.pid;
data.list.push(msg);
if (token) {
let item = data.userList.find((item) => item.id === data.pid);
item.list.push(msg);
}
return;
}
// 在这里写处理函数
};
}
onload();
</script>
<style scoped>
.index {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.main {
width: 50vw;
height: 80vh;
border-radius: 20px;
box-shadow: 2px 2px 20px rgba(0, 0, 0, 0.1);
background: #fff;
display: flex;
overflow: hidden;
}
.centent {
flex: 1;
flex-shrink: 0;
display: flex;
flex-direction: column;
}
.chat {
height: 72%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.chatTitle {
padding: 10px 20px;
text-align: right;
font-weight: bold;
}
.chatMain {
flex: 1;
flex-shrink: 0;
position: relative;
overflow-y: auto;
padding: 20px;
background: #f9f9f9;
}
.chatMain::before {
content: "";
position: absolute;
width: 100%;
box-shadow: 0px 0px 20px 2px rgba(0, 0, 0, 0.2);
left: 0;
bottom: 0;
}
.chatItem {
width: 100%;
display: inline-flex;
margin-bottom: 10px;
}
.chatText {
margin-left: 10px;
min-height: 30px;
max-width: 85%;
font-size: 14px;
line-height: 20px;
padding: 5px 10px;
border-radius: 0 10px 10px 10px;
background: #fff;
word-break: break-all;
}
.chatItemR {
justify-content: flex-end;
}
.chatTextR {
background: #409eff !important;
margin-left: 0 !important;
margin-right: 10px;
border-radius: 10px 0 10px 10px !important;
}
.input {
flex: 1;
flex-shrink: 0;
padding: 10px;
background: #f9f9f9;
display: flex;
flex-direction: column;
}
.send {
padding-top: 10px;
width: 100%;
display: flex;
justify-content: flex-end;
}
.textarea {
flex: 1;
flex-shrink: 0;
}
.userList {
width: 22%;
height: 100%;
padding: 10px;
overflow-y: auto;
}
.userItem {
display: flex;
align-items: center;
padding: 10px;
border-radius: 10px;
background: #f9f9f9;
margin-bottom: 10px;
cursor: pointer;
}
.userItem:hover {
background: #409eff !important;
}
.userItemAct {
background: #409eff !important;
}
.name {
margin-left: 10px;
width: 80px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 14px;
}
.avatar {
flex-shrink: 0;
background: #fff;
}
</style>
<style>
textarea {
height: 100% !important;
width: 100% !important;
}
textarea {
box-shadow: none !important;
}
</style>
完!!!!