客户web端完善
11
package.json
@ -12,25 +12,26 @@
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0",
|
||||
"element-plus": "^2.7.3",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@vueup/vue-quill": "1.2.0",
|
||||
"axios": "^1.6.8",
|
||||
"element-plus": "^2.7.3",
|
||||
"qs": "^6.12.1",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-vue-setup-extend-plus": "^1.0.1",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0",
|
||||
"vuex": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.8.0",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.23.0",
|
||||
"prettier": "^3.2.5",
|
||||
"vite": "^5.2.8",
|
||||
"babel-polyfill": "^6.26.0"
|
||||
"vite": "^5.2.8"
|
||||
}
|
||||
}
|
||||
|
BIN
public/emojis/冷酷.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
public/emojis/发呆.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
public/emojis/呕吐.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
public/emojis/呲牙.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
public/emojis/坏笑.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
public/emojis/大惊.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
public/emojis/大笑.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
public/emojis/大闹.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
public/emojis/天使.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
public/emojis/奋斗.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
public/emojis/尴尬.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
public/emojis/开心.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
public/emojis/心碎.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
public/emojis/恶魔.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
public/emojis/惊恐.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
public/emojis/惊悚.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
public/emojis/惊讶.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
public/emojis/感冒.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
public/emojis/愤怒.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
public/emojis/懵逼.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
public/emojis/无聊.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
public/emojis/汗颜.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
public/emojis/流汗.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
public/emojis/流泪.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
public/emojis/点赞.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
public/emojis/爱你.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
public/emojis/爱心.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
public/emojis/犯困.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
public/emojis/生气.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
public/emojis/白眼.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
public/emojis/睡着.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
public/emojis/瞌睡.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
public/emojis/笑哭.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
public/emojis/笑脸.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
public/emojis/讨厌.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
public/emojis/调皮.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
public/emojis/酷.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
public/emojis/闭嘴.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
public/emojis/难过.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
public/emojis/飞吻.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
public/emojis/饿死.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
public/emojis/骂人.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
14
src/App.vue
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="home-app">
|
||||
<div class="app">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
@ -15,8 +15,18 @@ body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.home-app {
|
||||
.app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body,
|
||||
input,
|
||||
select,
|
||||
button,
|
||||
textarea {
|
||||
font-size: 12px;
|
||||
font-family: arial, "微软雅黑", sans-serif;
|
||||
color: #000;
|
||||
}
|
||||
</style>
|
||||
|
59
src/api/CustomerClient/index.js
Normal file
@ -0,0 +1,59 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 客户端 API
|
||||
*/
|
||||
|
||||
const CustomerClientApi = {
|
||||
auth: '/api/customer/auth',
|
||||
opinion: '/api/opinion/rate',
|
||||
chatRecord: '/api/chat/before',
|
||||
hot: '/api/hot/question',
|
||||
uploadImage: '/api/upload/image',
|
||||
uploadImgb64: '/api/upload/imgb64',
|
||||
goodsDetail: '/api/goods/info',
|
||||
recommendList: '/api/recommend/list',
|
||||
orderList: '/api/order/list',
|
||||
logistics: '/api/order/logistics',
|
||||
poll: '/poll/poll.io'
|
||||
}
|
||||
|
||||
export function findClientWarnRecord(parameter) {
|
||||
return request({
|
||||
url: CustomerClientApi.orderList,
|
||||
method: 'get',
|
||||
params: parameter
|
||||
})
|
||||
}
|
||||
|
||||
export function clientAuth(parameter) {
|
||||
return request({
|
||||
url: CustomerClientApi.auth,
|
||||
method: 'post',
|
||||
params: parameter
|
||||
})
|
||||
}
|
||||
|
||||
export function findChatRecord(parameter) {
|
||||
return request({
|
||||
url: CustomerClientApi.chatRecord,
|
||||
method: 'post',
|
||||
params: parameter
|
||||
})
|
||||
}
|
||||
|
||||
export function loadHots(parameter) {
|
||||
return request({
|
||||
url: CustomerClientApi.hot,
|
||||
method: 'post',
|
||||
params: parameter
|
||||
})
|
||||
}
|
||||
|
||||
export function sendMessage(parameter) {
|
||||
return request({
|
||||
url: CustomerClientApi.poll,
|
||||
method: 'get',
|
||||
params: parameter
|
||||
})
|
||||
}
|
207
src/assets/css/left.css
Normal file
@ -0,0 +1,207 @@
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
padding: 0 15px 0 15px;
|
||||
line-height: 36px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #e4e4e4;
|
||||
}
|
||||
|
||||
.chat-service-status-tip {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: url('@/assets/img/msg-tip.png') center no-repeat;
|
||||
background-size: 22px 22px;
|
||||
}
|
||||
|
||||
.chat {
|
||||
height: calc(70% - 39px);
|
||||
margin: 1px 0;
|
||||
padding: 15px;
|
||||
background-color: #ffffff;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.service-chat,
|
||||
.user-chat,
|
||||
.robot-chat {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.clearfix:after {
|
||||
display: block;
|
||||
clear: both;
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.clearfix {
|
||||
zoom: 1;
|
||||
}
|
||||
|
||||
.service-chat .portrait {
|
||||
float: left;
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAAiCAYAAAA6RwvCAAADWUlEQVRYR9WYf0hTURTHv/e5NnWac0qaTSVyZlJQEZVBBv2gsCIoUtR+qGWWJVnU6CfRDx0YlP2wHyCaSaUSgUn9oRFlYYYURqS1XGitmrSZq9ya6XvxHqym1t7bcM3OXw/eOed+7jn3nMu5BHbSppBE9Xsx6QyDJAAT7P8N47eWAaoYmpTG6qyvbX6J7eNVhDifAfYO44K8rgignvi2dx+ryIF4AsIuEhwMaVFIlBTFaHjx3aggoomStEaI88k/TsngPRGCfPIyQtzmxoMpNI5aFoQRqu1Ovf8bxEseDL8FCfCeNgvSuQsAioLl8QNYmptgedII64tmp4PndEQCkjMQlK2CueEevtXW4Pvzp9yi3lOmQ6yMgTxrJwzHD6H7arFTME6BKC5Vo/+zEZ37c0Cbe/64EOU/GiGHC0H5++N95mrBMIJB/JeuQkBiGnTrlwtyriirwfdnTTCcOCJIXxCIJGYyxp4oQXvCTEFObUqRtxrRXVoE0/VyXjtBIIEZOaAtPTBdK+F1aK8QkJQO6fwl+JDF3qGORRBI2NlyGE+rYdW08Pkb8F+inITwqjt4M0cJ2mJ2aMsLIt+UC1naVuh3ZXKV4oz4xs1DeEUt9KrNMFWWug4SWnCR6xF9ug6Io2PRq2mBofCYIJbg3ANgWzZ7vVOBcnjJ5Pi4Pe2vtg4jEl5ZB+PJozA31sN3djyCdhzEu6RFgkBYW/Oj+zAWHgNbcbJ1mx3auhek4R6Mp/I8D/L19g10l533PIjpSjG+3KyE3+IVCMzYNnypYfP8ITtF8BnpKipAT30dpPGLIN+qch0kJO8M+j918laKz4w49Go13D1kE7ZqbJUmUkQCNA29Ksu1qhmliESI+hz69O/xpboC5od3hziSb9mNoJw9oKR+0K1dxkXAJrKUjRCNCUVflwHdly+43kfsdxeQmgkiluBHh3ZAl2TLun3hVG73suQNeLcmQVDqBivxdlZ7A1FoGESh40B5+/ze9bosWDWtkETHclcA2zdcEadA/rbA6JWpEEeM5z1LjgCHBcSVCPwpNSNjnHgVKc5jGHDzp6eEAdSEfQHoo5hfU7knYGiaRI+cIdwWAU+8CAx5lrDBsC8DhGLSCZDoxnlYSwgqvfpJaZTOyhYKJz8BNAh0ufLDVpEAAAAASUVORK5CYII=);
|
||||
}
|
||||
|
||||
.robot-chat .portrait {
|
||||
float: left;
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAAiCAYAAAA6RwvCAAACwklEQVRYR+2YXUiTURjH/5u1pa5tlbSZmUJE0U1XgVcSRER4USPNEvoQIsQo8iaK2tWSMIglgUhJBcHI7IOILsIu+rjIGETRRYmUc/NrNm0u1+baWjwvntjevV/bxLeLPbCL7ZzzPL/znOf8z9nRAMCXDbr7ALYDqKbvS2geAO4t3tgBzQJEwxIGFwrVRyBeAJUqg/gIJJkrxPL1VSipqUUyHkf45XMkgjO5ukJeINaObpgONnPB/edPIujqUQdk0yc/tEYzF/zXwGv4GnepA7Kuy4WVdfu54NNOBwLXLqkDQlE3j8znnQ3yI1kjVIz0kbLK3n4OhDIiZTHPV8Qnx0S7iIIY9x1CeeednFPNH/gnPAf/hVMIPXYJ+hQFoZmSRd6+WhSYNW12ySWUBclnJ6TOQK6WCiD89RbMSNmZixBbU7aLfo+OCNbOsrXlSISCSEYjae20NGQzN5z43n4uY2wGiKmxGdYr3YJqSecKK+Kx4/WY63+a5tBUfxjWqz2IfRvC+IkGzA99/tfOQOiHidNHEXpyL21sBkiqWvJlm2VKTEkJkmCF2lNBfj57iPHWJmkQw846VNx6JJgR+pGCaUsNCHTYEX7zIs2Zfus2VNzsQyIwhdFje5H4MS2YkcmzLZjtvS0NQq00K8vlLsSnJvI6yPjbl+qKFHj2wV35GmE9WC0UdKSQEbE7K+kBiZfqh17pjt2wtF+XvY9kczRL3eJkL88anR7a4mJoVpSgyLwKhj02lLXZMVilz2BgyhtwOrhMRj66kYxGFbHKgpAXo62Jyw4JGTSvU13Bhk3xtVY6mqr46q9sUnL9FhDZXNkVGcFkq240QQWFcR8pAUWR6eiteZ38tpqEXn/TnasoowUmVeDdpGueqOsw9QOkQ9u7q+oEiOQYRWeI/hsnv/nWYLQFt5IalR4nvABGKCHmr+VxYyrrQxH5wAAAABJRU5ErkJggg==);
|
||||
}
|
||||
|
||||
.user-chat .portrait {
|
||||
background-position: 0 0;
|
||||
float: right;
|
||||
background: url(@/assets/img/user.png) no-repeat;
|
||||
}
|
||||
|
||||
.portrait {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
margin-top: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.portrait img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
max-width: 533px;
|
||||
min-width: 170px;
|
||||
min-height: 44px;
|
||||
padding: 8px 10px;
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, .2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.container .arrow {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
width: 7px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.service-chat .container {
|
||||
float: left;
|
||||
margin-left: 15px;
|
||||
background-color: #CAEFFD;
|
||||
}
|
||||
|
||||
.robot-chat .container {
|
||||
float: left;
|
||||
margin-left: 15px;
|
||||
background-color: #CAEFFD;
|
||||
}
|
||||
|
||||
.user-chat .container {
|
||||
float: right;
|
||||
margin-right: 15px;
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
|
||||
.chat-head {
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.service-chat .chat-head {
|
||||
color: #005982;
|
||||
}
|
||||
|
||||
.user-chat .chat-head {
|
||||
color: #707070;
|
||||
}
|
||||
|
||||
.chat-head span {
|
||||
float: right;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.chat-head b.left-space {
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.ell {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-o-text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.config {
|
||||
position: relative;
|
||||
height: 34px;
|
||||
padding: 5px 15px;
|
||||
line-height: 34px;
|
||||
background-color: #fafafa;
|
||||
border-top: 1px solid #e4e4e4;
|
||||
border-bottom: 1px solid #e4e4e4;
|
||||
}
|
||||
|
||||
.toolbox {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.face {
|
||||
width: 22px;
|
||||
background: url('@/assets/img/face.png') center no-repeat;
|
||||
}
|
||||
|
||||
.emoji-img {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.input {
|
||||
height: calc(30% - 77px);
|
||||
margin: 1px 0;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.editor {
|
||||
padding: 0 15px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
resize: none;
|
||||
overflow-y: auto;
|
||||
color: #000;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
outline: none;
|
||||
background-color: #fff;
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
}
|
||||
.editor:empty:before {
|
||||
content: attr(placeholder);
|
||||
color: #cfcfcf;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.editor::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
}
|
||||
.editor::-webkit-scrollbar-thumb {
|
||||
width: 0px;
|
||||
}
|
||||
.editor::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.send {
|
||||
height: 41px;
|
||||
padding: 0 10px;
|
||||
background-color: #fafafa;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
}
|
20
src/assets/css/main.css
Normal file
@ -0,0 +1,20 @@
|
||||
@import 'base.css';
|
||||
|
||||
#app {
|
||||
font-weight: normal;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsl(210, 100%, 63%);
|
||||
transition: 0.4s;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
BIN
src/assets/img/about.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
src/assets/img/face.png
Normal file
After Width: | Height: | Size: 578 B |
BIN
src/assets/img/icon.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
BIN
src/assets/img/msg-tip.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/img/user.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
247
src/assets/js/emoji.js
Normal file
@ -0,0 +1,247 @@
|
||||
const dataMap = new Map()
|
||||
const data = initData(dataMap)
|
||||
|
||||
const emoji = {
|
||||
getEmojis() {
|
||||
return data
|
||||
},
|
||||
resolve(strs) {
|
||||
// 转换表情
|
||||
if (!strs) {
|
||||
return ''
|
||||
}
|
||||
let temp = strs[0]
|
||||
let result = ''
|
||||
for (let i = 1; i < strs.length; i++) {
|
||||
if (temp === '[' && strs[i] === ':') {
|
||||
i++
|
||||
const find = findEmoji(i, strs)
|
||||
result += find.stay
|
||||
i = find.i
|
||||
temp = find.temp
|
||||
} else {
|
||||
result += temp
|
||||
temp = strs[i]
|
||||
}
|
||||
}
|
||||
result += temp
|
||||
return result
|
||||
},
|
||||
adapter(str) {
|
||||
return '[:' + str + ':]'
|
||||
}
|
||||
}
|
||||
|
||||
function findEmoji(i, strs) {
|
||||
let temp = ''
|
||||
let stay = '[:'
|
||||
let key = ''
|
||||
while (i < strs.length - 1) {
|
||||
if (strs[i] === ':' && strs[i + 1] === ']') {
|
||||
const m = dataMap.get(key)
|
||||
if (m) {
|
||||
stay =
|
||||
'<img style="width:25px; height:25px; cursor: pointer; vertical-align: text-bottom;" src="' +
|
||||
m.src +
|
||||
'" name="[:' +
|
||||
m.name +
|
||||
':]" title="' +
|
||||
m.name +
|
||||
'" />'
|
||||
} else {
|
||||
stay += key + ':]'
|
||||
}
|
||||
i++
|
||||
break
|
||||
} else if (strs[i] === '[') {
|
||||
stay += key
|
||||
temp = strs[i]
|
||||
break
|
||||
} else {
|
||||
key += strs[i]
|
||||
i++
|
||||
}
|
||||
}
|
||||
return { stay: stay, temp: temp, i: i }
|
||||
}
|
||||
|
||||
function initData(dataMap) {
|
||||
const data = [
|
||||
{
|
||||
name: '笑脸',
|
||||
src: 'emojis/笑脸.png'
|
||||
},
|
||||
{
|
||||
name: '开心',
|
||||
src: 'emojis/开心.png'
|
||||
},
|
||||
{
|
||||
name: '大笑',
|
||||
src: 'emojis/大笑.png'
|
||||
},
|
||||
{
|
||||
name: '爱心',
|
||||
src: 'emojis/爱心.png'
|
||||
},
|
||||
{
|
||||
name: '飞吻',
|
||||
src: 'emojis/飞吻.png'
|
||||
},
|
||||
{
|
||||
name: '调皮',
|
||||
src: 'emojis/调皮.png'
|
||||
},
|
||||
{
|
||||
name: '讨厌',
|
||||
src: 'emojis/讨厌.png'
|
||||
},
|
||||
{
|
||||
name: '笑哭',
|
||||
src: 'emojis/笑哭.png'
|
||||
},
|
||||
{
|
||||
name: '流泪',
|
||||
src: 'emojis/流泪.png'
|
||||
},
|
||||
{
|
||||
name: '坏笑',
|
||||
src: 'emojis/坏笑.png'
|
||||
},
|
||||
{
|
||||
name: '流汗',
|
||||
src: 'emojis/流汗.png'
|
||||
},
|
||||
{
|
||||
name: '汗颜',
|
||||
src: 'emojis/汗颜.png'
|
||||
},
|
||||
{
|
||||
name: '尴尬',
|
||||
src: 'emojis/尴尬.png'
|
||||
},
|
||||
{
|
||||
name: '流泪',
|
||||
src: 'emojis/流泪.png'
|
||||
},
|
||||
{
|
||||
name: '冷酷',
|
||||
src: 'emojis/冷酷.png'
|
||||
},
|
||||
{
|
||||
name: '惊恐',
|
||||
src: 'emojis/惊恐.png'
|
||||
},
|
||||
{
|
||||
name: '惊悚',
|
||||
src: 'emojis/惊悚.png'
|
||||
},
|
||||
{
|
||||
name: '惊讶',
|
||||
src: 'emojis/惊讶.png'
|
||||
},
|
||||
{
|
||||
name: '大惊',
|
||||
src: 'emojis/大惊.png'
|
||||
},
|
||||
{
|
||||
name: '大闹',
|
||||
src: 'emojis/大闹.png'
|
||||
},
|
||||
{
|
||||
name: '发呆',
|
||||
src: 'emojis/发呆.png'
|
||||
},
|
||||
{
|
||||
name: '犯困',
|
||||
src: 'emojis/犯困.png'
|
||||
},
|
||||
{
|
||||
name: '心碎',
|
||||
src: 'emojis/心碎.png'
|
||||
},
|
||||
{
|
||||
name: '酷',
|
||||
src: 'emojis/酷.png'
|
||||
},
|
||||
{
|
||||
name: '生气',
|
||||
src: 'emojis/生气.png'
|
||||
},
|
||||
{
|
||||
name: '闭嘴',
|
||||
src: 'emojis/闭嘴.png'
|
||||
},
|
||||
{
|
||||
name: '睡着',
|
||||
src: 'emojis/睡着.png'
|
||||
},
|
||||
{
|
||||
name: '奋斗',
|
||||
src: 'emojis/奋斗.png'
|
||||
},
|
||||
{
|
||||
name: '愤怒',
|
||||
src: 'emojis/愤怒.png'
|
||||
},
|
||||
{
|
||||
name: '瞌睡',
|
||||
src: 'emojis/瞌睡.png'
|
||||
},
|
||||
{
|
||||
name: '难过',
|
||||
src: 'emojis/难过.png'
|
||||
},
|
||||
{
|
||||
name: '天使',
|
||||
src: 'emojis/天使.png'
|
||||
},
|
||||
{
|
||||
name: '无聊',
|
||||
src: 'emojis/无聊.png'
|
||||
},
|
||||
{
|
||||
name: '骂人',
|
||||
src: 'emojis/骂人.png'
|
||||
},
|
||||
{
|
||||
name: '点赞',
|
||||
src: 'emojis/点赞.png'
|
||||
},
|
||||
{
|
||||
name: '懵逼',
|
||||
src: 'emojis/懵逼.png'
|
||||
},
|
||||
{
|
||||
name: '白眼',
|
||||
src: 'emojis/白眼.png'
|
||||
},
|
||||
{
|
||||
name: '恶魔',
|
||||
src: 'emojis/恶魔.png'
|
||||
},
|
||||
{
|
||||
name: '感冒',
|
||||
src: 'emojis/感冒.png'
|
||||
},
|
||||
{
|
||||
name: '爱你',
|
||||
src: 'emojis/爱你.png'
|
||||
},
|
||||
{
|
||||
name: '呕吐',
|
||||
src: 'emojis/呕吐.png'
|
||||
},
|
||||
{
|
||||
name: '呲牙',
|
||||
src: 'emojis/呲牙.png'
|
||||
}
|
||||
]
|
||||
|
||||
for (const i in data) {
|
||||
dataMap.set(data[i].name, data[i])
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export default emoji
|
@ -1,35 +0,0 @@
|
||||
@import './base.css';
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
import './assets/main.css'
|
||||
import './assets/css/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import 'element-plus/theme-chalk/display.css';
|
||||
import 'element-plus/theme-chalk/display.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
|
||||
const app = createApp(App)
|
||||
@ -14,6 +15,7 @@ app.use(router)
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
app.use(store)
|
||||
|
||||
app.use(ElementPlus)
|
||||
app.mount('#app')
|
||||
|
@ -7,8 +7,8 @@ const router = createRouter({
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
meta: { title: '主页', icon: '', activeIndex: 'home' },
|
||||
component: () => import('@/views/Home.vue'),
|
||||
},
|
||||
component: () => import('@/views/HomeView.vue')
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
|
56
src/store/editor.js
Normal file
@ -0,0 +1,56 @@
|
||||
const editor = {
|
||||
state: {
|
||||
range: null,
|
||||
el: null
|
||||
},
|
||||
mutations: {
|
||||
SET_EDITOR(state, el) {
|
||||
state.el = el
|
||||
},
|
||||
INSERT(state, content) {
|
||||
// state.el.focus()
|
||||
const selection = window.getSelection()
|
||||
if (state.range) {
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(state.range)
|
||||
}
|
||||
const range = selection.getRangeAt(0)
|
||||
const el = document.createElement('div')
|
||||
const frag = document.createDocumentFragment()
|
||||
el.innerHTML = content
|
||||
const node = frag.appendChild(el.firstChild)
|
||||
range.insertNode(frag)
|
||||
const newRange = document.createRange()
|
||||
newRange.setStartAfter(node)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(newRange)
|
||||
},
|
||||
REPLACE(state, content) {
|
||||
state.el.innerHTML = content
|
||||
var range = document.createRange()
|
||||
range.selectNodeContents(state.el)
|
||||
range.collapse(false)
|
||||
var sel = window.getSelection()
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(range)
|
||||
},
|
||||
FOCUS(state) {
|
||||
if (state.el.innerHTML) {
|
||||
var range = document.createRange()
|
||||
range.selectNodeContents(state.el)
|
||||
range.collapse(false)
|
||||
var sel = window.getSelection()
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(range)
|
||||
} else {
|
||||
state.el.focus()
|
||||
}
|
||||
},
|
||||
UPDATE_RANGE(state, range) {
|
||||
state.range = range
|
||||
}
|
||||
},
|
||||
actions: {}
|
||||
}
|
||||
|
||||
export default editor
|
@ -1,6 +1,10 @@
|
||||
import { createStore } from 'vuex'
|
||||
import editor from '@/store/editor.js'
|
||||
|
||||
const store = createStore({
|
||||
const index = createStore({
|
||||
modules: {
|
||||
editor
|
||||
},
|
||||
state() {
|
||||
return {
|
||||
currentRoute: null
|
||||
@ -18,4 +22,4 @@ const store = createStore({
|
||||
}
|
||||
})
|
||||
|
||||
export default store
|
||||
export default index
|
35
src/utils/axios.js
Normal file
@ -0,0 +1,35 @@
|
||||
const VueAxios = {
|
||||
vm: {},
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
install (Vue, instance) {
|
||||
if (this.installed) {
|
||||
return
|
||||
}
|
||||
this.installed = true
|
||||
|
||||
if (!instance) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('You have to install axios')
|
||||
return
|
||||
}
|
||||
|
||||
Vue.axios = instance
|
||||
|
||||
Object.defineProperties(Vue.prototype, {
|
||||
axios: {
|
||||
get: function get () {
|
||||
return instance
|
||||
}
|
||||
},
|
||||
$http: {
|
||||
get: function get () {
|
||||
return instance
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
VueAxios
|
||||
}
|
@ -1,107 +1,75 @@
|
||||
import axios from 'axios'
|
||||
import { ElNotification, ElMessage } from 'element-plus'
|
||||
import errorCode from '@/utils/errorCode'
|
||||
import cache from '@/plugins/cache'
|
||||
import { VueAxios } from './axios'
|
||||
import qs from 'qs'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
|
||||
// 创建axios实例
|
||||
const service = axios.create({
|
||||
// axios中请求配置有baseURL选项,表示请求URL公共部分
|
||||
baseURL: import.meta.env.VITE_APP_BASE_API,
|
||||
// 超时
|
||||
timeout: 10000
|
||||
// 创建 axios 实例
|
||||
const request = axios.create({
|
||||
// API 请求的默认前缀`
|
||||
// baseURL: '/api',
|
||||
timeout: 600000, // 请求超时时间
|
||||
maxBodyLength: 5 * 1024 * 1024,
|
||||
withCredentials: false,
|
||||
paramsSerializer: {
|
||||
serialize: params => {
|
||||
if (params && params.sorter) {
|
||||
params.sorter = JSON.stringify(params.sorter)
|
||||
}
|
||||
return qs.stringify(params, { indices: false })
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// request拦截器
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
// 是否需要防止数据重复提交
|
||||
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
|
||||
// get请求映射params参数
|
||||
if (config.method === 'get' && config.params) {
|
||||
let url = config.url + '?' + tansParams(config.params)
|
||||
url = url.slice(0, -1)
|
||||
config.params = {}
|
||||
config.url = url
|
||||
// 异常拦截处理器
|
||||
const errorHandler = (error) => {
|
||||
if (error.response) {
|
||||
const data = error.response.data
|
||||
console.error('response => ', data)
|
||||
// const token = Vue.ls.get(ACCESS_TOKEN)
|
||||
if (error.response.status === 403) {
|
||||
ElMessage.error(data.message)
|
||||
}
|
||||
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
|
||||
const requestObj = {
|
||||
url: config.url,
|
||||
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
|
||||
time: new Date().getTime()
|
||||
}
|
||||
const requestSize = Object.keys(JSON.stringify(requestObj)).length // 请求数据大小
|
||||
const limitSize = 5 * 1024 * 1024 // 限制存放数据5M
|
||||
if (requestSize >= limitSize) {
|
||||
console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制,无法进行防重复提交验证。')
|
||||
return config
|
||||
}
|
||||
const sessionObj = cache.session.getJSON('sessionObj')
|
||||
if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
|
||||
cache.session.setJSON('sessionObj', requestObj)
|
||||
} else {
|
||||
const s_url = sessionObj.url // 请求地址
|
||||
const s_data = sessionObj.data // 请求数据
|
||||
const s_time = sessionObj.time // 请求时间
|
||||
const interval = 1000 // 间隔时间(ms),小于此时间视为重复提交
|
||||
if (
|
||||
s_data === requestObj.data &&
|
||||
requestObj.time - s_time < interval &&
|
||||
s_url === requestObj.url
|
||||
) {
|
||||
const message = '数据正在处理,请勿重复提交'
|
||||
console.warn(`[${s_url}]: ` + message)
|
||||
return Promise.reject(new Error(message))
|
||||
} else {
|
||||
cache.session.setJSON('sessionObj', requestObj)
|
||||
}
|
||||
}
|
||||
if (error.response.status === 401 && !(data.result && data.result.isLogin)) {
|
||||
ElMessage.error('Authorization verification failed')
|
||||
/*if (token) {
|
||||
store.dispatch('Logout').then(() => {
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 1500)
|
||||
})
|
||||
}*/
|
||||
}
|
||||
if (error.response.status === 404) {
|
||||
ElMessage.error(data.path)
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
console.log(error)
|
||||
Promise.reject(error)
|
||||
}
|
||||
)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
(res) => {
|
||||
// 未设置状态码则默认成功状态
|
||||
const code = res.data.code || 200
|
||||
// 获取错误信息
|
||||
const msg = errorCode[code] || res.data.msg || errorCode['default']
|
||||
// 二进制数据则直接返回
|
||||
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
|
||||
return res.data
|
||||
}
|
||||
if (code === 500) {
|
||||
ElMessage({ message: msg, type: 'error' })
|
||||
return Promise.reject(new Error(msg))
|
||||
} else if (code === 601) {
|
||||
ElMessage({ message: msg, type: 'warning' })
|
||||
return Promise.reject(new Error(msg))
|
||||
} else if (code !== 200) {
|
||||
ElNotification.error({ title: msg })
|
||||
return Promise.reject('error')
|
||||
} else {
|
||||
return Promise.resolve(res.data)
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.log('err' + error)
|
||||
let { message } = error
|
||||
if (message == 'Network Error') {
|
||||
message = '后端接口连接异常'
|
||||
} else if (message.includes('timeout')) {
|
||||
message = '系统接口请求超时'
|
||||
} else if (message.includes('Request failed with status code')) {
|
||||
message = '系统接口' + message.substr(message.length - 3) + '异常'
|
||||
}
|
||||
ElMessage({ message: message, type: 'error', duration: 5 * 1000 })
|
||||
return Promise.reject(error)
|
||||
// request interceptor
|
||||
request.interceptors.request.use(config => {
|
||||
/*const token = Vue.ls.get(ACCESS_TOKEN)
|
||||
if (token) {
|
||||
config.headers.Authorization = token
|
||||
}*/
|
||||
return config
|
||||
}, errorHandler)
|
||||
|
||||
// response interceptor
|
||||
request.interceptors.response.use((response) => {
|
||||
return response.data
|
||||
}, errorHandler)
|
||||
|
||||
const installer = {
|
||||
vm: {},
|
||||
install (Vue) {
|
||||
Vue.use(VueAxios, request)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export default service
|
||||
export default request
|
||||
|
||||
export {
|
||||
installer as VueAxios,
|
||||
request as axios
|
||||
}
|
||||
|
36
src/views/HeaderView.vue
Normal file
@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="header">
|
||||
<div class="header-logo">
|
||||
<img src="@/assets/img/logo.png" width="50px" height="50px" />
|
||||
</div>
|
||||
<div class="header-text">
|
||||
<h3>津港科技</h3>
|
||||
<h4>为您在线解答售前、售后服务</h4>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="header-components"></script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
height: 70px;
|
||||
background-color: #007acc;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
padding: 0 10px;
|
||||
}
|
||||
</style>
|
@ -1,11 +0,0 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup name="">
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
59
src/views/HomeView.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="home-container">
|
||||
<el-container>
|
||||
<el-header class="header">
|
||||
<Header></Header>
|
||||
</el-header>
|
||||
<el-container class="content">
|
||||
<el-main class="main">
|
||||
<Left></Left>
|
||||
</el-main>
|
||||
<el-aside class="aside">
|
||||
<Right></Right>
|
||||
</el-aside>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="home">
|
||||
import Header from '@/views/HeaderView.vue'
|
||||
import Left from '@/views/LeftView.vue'
|
||||
import Right from '@/views/RightView.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.leftComponents {
|
||||
height: 100%;
|
||||
}
|
||||
.home-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.header {
|
||||
height: 70px;
|
||||
background-color: #007acc;
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
.el-container {
|
||||
height: 100%;
|
||||
}
|
||||
.content {
|
||||
background-color: #e9e9e9;
|
||||
height: calc(100% - 70px);
|
||||
}
|
||||
.main {
|
||||
width: calc(70% - 1px);
|
||||
}
|
||||
.aside {
|
||||
width: calc(30% - 1px);
|
||||
}
|
||||
|
||||
:deep(.el-main) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
500
src/views/LeftView.vue
Normal file
@ -0,0 +1,500 @@
|
||||
<template>
|
||||
<div class="leftComponents">
|
||||
<div class="message">
|
||||
<div class="chat-service-status-tip"></div>
|
||||
<div>{{ broadcast }}</div>
|
||||
</div>
|
||||
<div class="chat" ref="chatRef">
|
||||
<div>
|
||||
<template v-for="(item, index) in historyRecord" :key="index">
|
||||
<div v-if="item.ownerType === '1'" class="user-chat clearfix">
|
||||
<div class="portrait"></div>
|
||||
<div class="container">
|
||||
<div class="arrow icons"></div>
|
||||
<div class="chat-head ell">
|
||||
<span>{{ item.createTime }}</span>
|
||||
<b>我</b>
|
||||
</div>
|
||||
<div class="chat-body" v-html="item.content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="item.ownerType === '2' || item.ownerType === '5' || item.ownerType === '9'" class="service-chat clearfix">
|
||||
<div class="portrait icons"></div>
|
||||
<div class="container">
|
||||
<div class="arrow icons"></div>
|
||||
<div class="chat-head ell">
|
||||
<span>{{ item.createTime }}</span>
|
||||
<b>{{ item.briefName }}</b>
|
||||
<b class="left-space">{{ item.waiterCode }}</b>
|
||||
</div>
|
||||
<div class="chat-body" v-html="item.content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else :key="index" class="robot-chat clearfix">
|
||||
<div class="portrait icons"></div>
|
||||
<div class="container">
|
||||
<div class="arrow icons"></div>
|
||||
<div class="chat-head ell">
|
||||
<span>{{ item.createTime }}</span>
|
||||
<b>客服助手</b>
|
||||
</div>
|
||||
<div class="chat-body">{{ item.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config">
|
||||
<div class="toolbox">
|
||||
<el-popover
|
||||
placement="top-start"
|
||||
:width="490"
|
||||
trigger="click"
|
||||
:show-arrow="isShowArrow"
|
||||
:popper-style="popperStyle"
|
||||
>
|
||||
<template #reference>
|
||||
<div class="face" title="表情"></div>
|
||||
</template>
|
||||
<img
|
||||
class="emoji-img"
|
||||
v-for="(emoji, index) in emojis"
|
||||
:key="index"
|
||||
:title="emoji.name"
|
||||
:src="emoji.src"
|
||||
@click="selectEmoji(emoji)"
|
||||
/>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input">
|
||||
<div
|
||||
ref="editorRef"
|
||||
class="editor"
|
||||
contentEditable="plaintext-only"
|
||||
placeholder="请输入消息(300字符内)..."
|
||||
@keydown.exact.enter.stop.prevent="handleSendMessage"
|
||||
@keyup.exact.ctrl.enter.stop.prevent="handleSendMessage"
|
||||
@blur="editorBlur($event)"
|
||||
@keyup.exact="editorKeyup"
|
||||
/>
|
||||
</div>
|
||||
<div class="send">
|
||||
<el-button type="primary" @click="handleSendMessage" class="send-button">发送</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="left">
|
||||
import { ref, toRefs, reactive, onMounted, nextTick } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { clientAuth, sendMessage } from '@/api/CustomerClient'
|
||||
import Emoji from '@/assets/js/emoji.js'
|
||||
const { getEmojis } = Emoji
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
|
||||
const isShowArrow = ref(false)
|
||||
const popperStyle = ref({
|
||||
height: '140px',
|
||||
padding: '10px',
|
||||
border: '1px solid #000000',
|
||||
'overflow-y': 'auto'
|
||||
})
|
||||
const editorRef = ref(null)
|
||||
const chatRef = ref(null)
|
||||
const wsUrl = ref('ws://192.168.50.101:6066')
|
||||
const wsIsError = ref(false)
|
||||
const wsIsClose = ref(true)
|
||||
const ws = ref(null)
|
||||
const constant = ref({
|
||||
buildSuccess: '会话创建成功!',
|
||||
waitingTip: '当前客服繁忙,已加入等候队列(您前面还有 {0} 个人)请您耐心等候...',
|
||||
serviceInfo: '客服工号 {0} 为您提供服务~',
|
||||
closeTip: '会话已结束(回复再次咨询).',
|
||||
openOtherTip: '已在其他打开咨询,当前会话被关闭.',
|
||||
maintenanceTip: '糟糕,网络错误,工程师正在维护中...',
|
||||
errorTip: '糟糕,网络连接断开'
|
||||
})
|
||||
const wsPING = ref(null)
|
||||
let reconnectTimeout = ref(null)
|
||||
const server = ref({
|
||||
ownerType: '',
|
||||
createTime: '',
|
||||
briefName: '',
|
||||
waiterCode: '',
|
||||
content: ''
|
||||
})
|
||||
const store = useStore()
|
||||
const isMobile = ref(0)
|
||||
const chatRecordMark = ref({
|
||||
first: true
|
||||
})
|
||||
const data = reactive({
|
||||
broadcast: '正在建立连接中,请等候...',
|
||||
emojis: getEmojis(),
|
||||
customer: {},
|
||||
historyRecord: [],
|
||||
chatVal: ''
|
||||
})
|
||||
let { broadcast, emojis, customer, historyRecord, chatVal } = toRefs(data)
|
||||
|
||||
onMounted(() => {
|
||||
store.commit('SET_EDITOR', editorRef.value)
|
||||
initWebSocket()
|
||||
editorBlur()
|
||||
})
|
||||
|
||||
function selectEmoji(emoji) {
|
||||
const content = Emoji.adapter(emoji.name)
|
||||
if (content) {
|
||||
store.commit('INSERT',Emoji.resolve(content))
|
||||
}
|
||||
}
|
||||
|
||||
function handleSendMessage() {
|
||||
const editor = editorRef.value
|
||||
let message = ''
|
||||
const nodes = editor.childNodes
|
||||
nodes.forEach((node, index) => {
|
||||
if (node.nodeName === 'IMG') {
|
||||
message += node.getAttribute('name')
|
||||
} else if (node.nodeName === '#text') {
|
||||
if (node.nodeValue) {
|
||||
message += node.nodeValue.replace(/<(?!br([/]*)>)[^>]*>/gi, '')
|
||||
}
|
||||
} else {
|
||||
if (node.innerHTML) {
|
||||
message += node.innerHTML.replace(/<(?!br([/]*)>)[^>]*>/gi, '')
|
||||
}
|
||||
}
|
||||
})
|
||||
message = message.trim()
|
||||
if (!message || message.length === 0) {
|
||||
ElMessageBox.alert('消息不能为空', '提示', {
|
||||
confirmButtonText: 'OK'
|
||||
})
|
||||
} else if (message.length > 300) {
|
||||
ElMessageBox.alert('发送内容不能超过300字符', '提示', {
|
||||
confirmButtonText: 'OK'
|
||||
})
|
||||
} else {
|
||||
sendMessages(message, 'TEXT')
|
||||
editor.innerHTML = ''
|
||||
}
|
||||
}
|
||||
function sendMessages(message, bodyType) {
|
||||
const packet = {
|
||||
pid: getPid(),
|
||||
type: 'MESSAGE',
|
||||
ts: 'POLLING',
|
||||
ttc: customer.value.ttc,
|
||||
skc: customer.value.skc,
|
||||
skn: customer.value.skn,
|
||||
gc: customer.value.gc,
|
||||
to: {
|
||||
idy: 'WAITER'
|
||||
},
|
||||
from: {
|
||||
uid: customer.value.cc,
|
||||
name: customer.value.cn,
|
||||
idy: 'CUSTOMER'
|
||||
},
|
||||
body: {
|
||||
type: bodyType,
|
||||
content: message
|
||||
}
|
||||
}
|
||||
// console.log('消息发送成功!' + JSON.stringify(packet))
|
||||
const params = {
|
||||
packet: JSON.stringify(packet),
|
||||
t: new Date().getTime()
|
||||
}
|
||||
sendMessage(params)
|
||||
.then((res) => {})
|
||||
.catch((reason) => {})
|
||||
.finally(() => {
|
||||
historyRecord.value.push({
|
||||
ownerType: '1',
|
||||
createTime: formattedDate(params.t),
|
||||
content: Emoji.resolve(message)
|
||||
})
|
||||
chatVal.value = ''
|
||||
nextTick(() => {
|
||||
scrollToBottom();
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
let subId = 1000
|
||||
const fragment = () => {
|
||||
return Math.floor(65535 * (1 + Math.random()))
|
||||
.toString(16)
|
||||
.substring(1)
|
||||
}
|
||||
function getPid() {
|
||||
return (
|
||||
subId++ +
|
||||
'-' +
|
||||
fragment() +
|
||||
'-' +
|
||||
fragment() +
|
||||
'-' +
|
||||
fragment() +
|
||||
'-' +
|
||||
fragment() +
|
||||
'-' +
|
||||
fragment() +
|
||||
fragment() +
|
||||
fragment()
|
||||
)
|
||||
}
|
||||
|
||||
function formattedDate(timestamp) {
|
||||
// 将时间戳转换为 Date 对象
|
||||
const date = new Date(timestamp)
|
||||
|
||||
// 获取年、月、日、时、分、秒
|
||||
const year = date.getFullYear()
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
const hours = date.getHours().toString().padStart(2, '0')
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0')
|
||||
const seconds = date.getSeconds().toString().padStart(2, '0')
|
||||
|
||||
// 构建日期时间字符串
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
// 使用 $refs 来引用滚动区域的 DOM 元素
|
||||
const scrollArea = chatRef.value
|
||||
// 将滚动位置设置为最底部
|
||||
scrollArea.scrollTop = scrollArea.scrollHeight
|
||||
}
|
||||
|
||||
function editorBlur() {
|
||||
const sel = window.getSelection()
|
||||
if (sel.rangeCount === 0) {
|
||||
const editorElement = editorRef.value
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(editorElement)
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(range)
|
||||
}
|
||||
store.commit('UPDATE_RANGE', sel.getRangeAt(0))
|
||||
}
|
||||
|
||||
function editorKeyup(event) {
|
||||
const editor = editorRef.value
|
||||
const editorValue = editor.innerHTML
|
||||
if (event.keyCode === 38 || event.keyCode === 40) {
|
||||
if (editorValue) {
|
||||
event.stopPropagation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initWebSocket() {
|
||||
const params = {
|
||||
ttc: '1',
|
||||
skc: 1,
|
||||
buc: '',
|
||||
gc: '',
|
||||
device: isMobile.value ? '2' : '1',
|
||||
origin: 1,
|
||||
io: window.WebSocket ? 'ws' : 'poll'
|
||||
}
|
||||
clientAuth(params)
|
||||
.then((res) => {
|
||||
console.log('client-auth =>', res)
|
||||
switch (res.rc) {
|
||||
// 成功
|
||||
case 0:
|
||||
customer.value = res.data
|
||||
connectToWs()
|
||||
break
|
||||
// 需要登录
|
||||
case 1:
|
||||
ElMessageBox.alert(res.rm, 'ERROR', {
|
||||
confirmButtonText: 'OK'
|
||||
})
|
||||
break
|
||||
// 非工作时间
|
||||
case 2:
|
||||
if (res.data && res.data.offlineMsg) {
|
||||
broadcast.value = res.data.offlineMsg
|
||||
ElMessageBox.alert(res.data.offlineMsg, 'ERROR', {
|
||||
confirmButtonText: 'OK'
|
||||
})
|
||||
} else {
|
||||
broadcast.value = res.rm
|
||||
}
|
||||
break
|
||||
// 无法正确路由到skc
|
||||
case 3:
|
||||
window.location.href = '//' + window.location.host
|
||||
break
|
||||
default:
|
||||
ElMessageBox.alert('初始化信息失败,请重试!', 'ERROR', {
|
||||
confirmButtonText: 'OK'
|
||||
})
|
||||
break
|
||||
}
|
||||
})
|
||||
.catch((reason) => {})
|
||||
.finally(() => {})
|
||||
}
|
||||
|
||||
function connectToWs() {
|
||||
//初始化 websocket
|
||||
if ('WebSocket' in window) {
|
||||
const connectPacket = {
|
||||
type: 'AUTH',
|
||||
ts: 'WEBSOCKET',
|
||||
from: {
|
||||
idy: 'CUSTOMER'
|
||||
},
|
||||
body: {
|
||||
type: 'LOGIN'
|
||||
}
|
||||
}
|
||||
const url =
|
||||
wsUrl.value +
|
||||
'/ws.io' +
|
||||
'?packet=' +
|
||||
JSON.stringify(connectPacket) +
|
||||
'&t=' +
|
||||
new Date().getTime()
|
||||
ws.value = new WebSocket(url)
|
||||
ws.value.onopen = onOpen
|
||||
ws.value.onmessage = onMessage
|
||||
ws.value.onerror = onError
|
||||
ws.value.onclose = onClose
|
||||
} else {
|
||||
console.log('Current browser Not support websocket')
|
||||
}
|
||||
}
|
||||
|
||||
function onOpen(e) {
|
||||
wsIsError.value = false
|
||||
broadcast.value = constant.value.buildSuccess
|
||||
//连接建立之后执行send方法发送数据
|
||||
console.log('连接成功 =>', e)
|
||||
clearInterval(wsPING.value)
|
||||
wsPING.value = setInterval(() => {
|
||||
const pingPacket = {
|
||||
type: 'PING',
|
||||
ts: 'WEBSOCKET',
|
||||
body: {
|
||||
type: 'TEXT',
|
||||
content: 'PING'
|
||||
}
|
||||
}
|
||||
sendData(pingPacket)
|
||||
}, 3000)
|
||||
}
|
||||
function onError() {
|
||||
wsIsError.value = true
|
||||
broadcast.value = constant.value.maintenanceTip
|
||||
//连接建立失败重连
|
||||
console.error('糟糕,网络错误,工程师正在维护中...')
|
||||
}
|
||||
function onMessage(data) {
|
||||
//数据接收
|
||||
data = JSON.parse(data.data)
|
||||
// console.log('接收到WS消息 =>', data)
|
||||
if (data.type === 'RE_LOGIN') {
|
||||
wsIsClose.value = true
|
||||
clearInterval(wsPING.value)
|
||||
broadcast.value = constant.value.openOtherTip
|
||||
// console.warn('已在其他打开咨询,当前会话被关闭')
|
||||
} else if (data.type === 'BUILD_CHAT') {
|
||||
if (data.body.type === 'BUILDING_CHAT') {
|
||||
broadcast.value = data.body.content
|
||||
} else if (data.body.type === 'SUCCESS') {
|
||||
server.value = {
|
||||
ownerType: '2',
|
||||
createTime: '',
|
||||
briefName: JSON.parse(data.body.content).tmb,
|
||||
waiterCode: data.from.uid,
|
||||
content: ''
|
||||
}
|
||||
broadcast.value = '客服工号 ' + data.from.uid + ' 为您提供服务~'
|
||||
} else {
|
||||
// console.log('接收到WS消息 =>', data)
|
||||
historyRecord.value.push({
|
||||
ownerType: '2',
|
||||
createTime: data.datetime,
|
||||
briefName: server.value.briefName,
|
||||
waiterCode: server.value.waiterCode,
|
||||
content: data.body.content
|
||||
})
|
||||
nextTick(() => {
|
||||
scrollToBottom();
|
||||
});
|
||||
}
|
||||
} else if (data.type === 'MESSAGE') {
|
||||
const content = Emoji.resolve(data.body.content)
|
||||
// console.log('接收到WS消息 =>', data)
|
||||
// console.log('消息转换 =>', content)
|
||||
historyRecord.value.push({
|
||||
ownerType: '2',
|
||||
createTime: data.datetime,
|
||||
briefName: server.value.briefName,
|
||||
waiterCode: server.value.waiterCode,
|
||||
content: content
|
||||
})
|
||||
nextTick(() => {
|
||||
scrollToBottom();
|
||||
});
|
||||
} else {
|
||||
// 聊天数据
|
||||
// console.log('聊天数据 inMessage =>', data)
|
||||
}
|
||||
}
|
||||
function onClose(e) {
|
||||
if (wsIsClose.value) {
|
||||
return
|
||||
}
|
||||
if (!wsIsError.value) {
|
||||
broadcast.value = constant.value.errorTip
|
||||
}
|
||||
//关闭
|
||||
console.log('断开连接', e)
|
||||
|
||||
reconnect()
|
||||
}
|
||||
|
||||
function sendData(packet) {
|
||||
if (packet) {
|
||||
if (ws.value.readyState === 1) {
|
||||
packet.pid = getPid()
|
||||
packet.to = {
|
||||
idy: 'WAITER'
|
||||
}
|
||||
ws.value.send(JSON.stringify(packet))
|
||||
} else {
|
||||
reconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function reconnect() {
|
||||
if (ws.value.readyState === 3) {
|
||||
clearInterval(wsPING.value)
|
||||
if (reconnectTimeout.value) {
|
||||
clearTimeout(reconnectTimeout.value)
|
||||
}
|
||||
reconnectTimeout.value = setTimeout(initWebSocket, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@import "@/assets/css/left.css";
|
||||
|
||||
</style>
|
176
src/views/RightView.vue
Normal file
@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div class="right-slider">
|
||||
<div class="right-slider-text">
|
||||
<el-tabs v-model="activeName">
|
||||
<el-tab-pane label="关于我们" name="about">
|
||||
<div class="indent">
|
||||
<p>北京启航云汇科技有限公司,是一家专注于软件开发与信息技术服务的技术企业。</p>
|
||||
<p>
|
||||
公司自成立以来,始终坚持以技术创新为核心竞争力,拥有一支经验丰富、技术精湛的研发团队。团队成员均具备深厚的行业背景和专业的技术素养,能够准确把握客户需求,提供量身定制的软件开发服务。
|
||||
</p>
|
||||
<p>
|
||||
启航云汇公司的主营业务包括软件定制开发、系统集成、数字孪生、大数据处理等。我们致力于为客户提供从需求分析、方案设计、开发实施到后期维护的全流程服务,帮助客户实现业务数字化转型,提升运营效率和市场竞争力。
|
||||
</p>
|
||||
<p>
|
||||
启航云汇技术团队具有丰富的行业经验,成功为工业生产、矿山、金融、教育、医疗、制造等多个行业提供了优质的软件解决方案。我们的产品和服务赢得了客户的广泛赞誉,树立了良好的市场口碑。
|
||||
</p>
|
||||
<p>
|
||||
展望未来,启航云汇将继续秉承“客户至上、技术领先、创新驱动”的经营理念,不断加强技术研发和创新能力,拓展业务领域,提升服务质量,为更多客户提供优质的软件产品和信息技术服务,助力客户实现可持续发展。
|
||||
</p>
|
||||
<p>启航云汇愿与各界朋友携手合作,共创美好未来!</p>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="客服名片" name="custom">
|
||||
<div class="tab-body">
|
||||
<!-- FAQ -->
|
||||
<div class="tab-list service-qa" style="font-size: 18px; display: block" id="hot_list">
|
||||
<ul id="hot_ul">
|
||||
<li>
|
||||
<h6>
|
||||
<a href="javascript:;" class>1. 客服系统技术交流群</a>
|
||||
</h6>
|
||||
<div class="answer" style="display: none">
|
||||
<i class="icons"></i>
|
||||
<p style="font-size: 12px">
|
||||
群QQ: 606173512 申请加入时请输入“客服妹妹”, 否则拒绝通过!
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li v-for="(item, index) of hots" :key="index">
|
||||
<h6>
|
||||
<a>{{ index + 1 + ' ' + item.question }}</a>
|
||||
</h6>
|
||||
<div class="answer" style="display: none">
|
||||
<i class="icons"></i>
|
||||
<p style="font-size: 12px">{{ item.answer }}</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<div class="right-slider-img">
|
||||
<img src="@/assets/img/about.png" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="right-components">
|
||||
const data = reactive({
|
||||
activeName: 'about',
|
||||
hots: []
|
||||
})
|
||||
const { activeName, hots } = toRefs(data)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.right-slider {
|
||||
width: calc(100% - 2px);
|
||||
background-color: #fff;
|
||||
margin-left: 2px;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.right-slider-text {
|
||||
height: 65%;
|
||||
}
|
||||
|
||||
.indent {
|
||||
white-space: pre-wrap;
|
||||
line-height: 30px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.right-slider .tab-body {
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.right-slider .tab-list {
|
||||
margin: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.service-qa ul li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.service-qa ul li h6 {
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.service-qa ul li h6 a {
|
||||
color: #535353;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.service-qa ul li h6 a.active {
|
||||
color: #e2231a;
|
||||
}
|
||||
|
||||
.service-qa ul li .answer {
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
border: 1px solid #c8c8c8;
|
||||
background-color: #fafafa;
|
||||
border-radius: 3px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.service-qa ul li .answer i {
|
||||
position: absolute;
|
||||
top: -11px;
|
||||
left: 40px;
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 12px;
|
||||
background-position: -156px -239px;
|
||||
}
|
||||
|
||||
.service-qa ul li .answer p {
|
||||
padding: 5px;
|
||||
line-height: 18px;
|
||||
color: #535353;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.right-slider-img {
|
||||
padding: 10px 0;
|
||||
text-align: center;
|
||||
border-top: 1px solid #e9e9e9;
|
||||
height: 35%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.right-slider-img img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
:deep(.el-tabs) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-scroll) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__content) {
|
||||
height: calc(100% - 40px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
@ -1,6 +1,6 @@
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import createVitePlugins from './vite/plugins'
|
||||
import path from 'path'
|
||||
import { resolve } from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode, command }) => {
|
||||
@ -12,16 +12,15 @@ export default defineConfig(({ mode, command }) => {
|
||||
resolve: {
|
||||
alias: {
|
||||
// 设置路径
|
||||
'~': path.resolve(__dirname, './'),
|
||||
'~': resolve(__dirname, './'),
|
||||
// 设置别名
|
||||
'@': path.resolve(__dirname, './src')
|
||||
'@': resolve(__dirname, 'src')
|
||||
},
|
||||
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
|
||||
},
|
||||
server: {
|
||||
port: 3500,
|
||||
host: true,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
// target: 'http://117.62.238.129:10050',
|
||||
@ -36,7 +35,8 @@ export default defineConfig(({ mode, command }) => {
|
||||
// target: 'http://10.70.132.177:11002',
|
||||
pathRewrite: { '^/poll': '' },
|
||||
ws: false,
|
||||
changeOrigin: true
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/poll/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
|