客户web端完善
11
package.json
@ -12,25 +12,26 @@
|
|||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.4.21",
|
|
||||||
"vue-router": "^4.3.0",
|
|
||||||
"element-plus": "^2.7.3",
|
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
"@vueup/vue-quill": "1.2.0",
|
"@vueup/vue-quill": "1.2.0",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
|
"element-plus": "^2.7.3",
|
||||||
|
"qs": "^6.12.1",
|
||||||
"unplugin-auto-import": "^0.17.5",
|
"unplugin-auto-import": "^0.17.5",
|
||||||
"unplugin-vue-setup-extend-plus": "^1.0.1",
|
"unplugin-vue-setup-extend-plus": "^1.0.1",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
|
"vue": "^3.4.21",
|
||||||
|
"vue-router": "^4.3.0",
|
||||||
"vuex": "^4.1.0"
|
"vuex": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rushstack/eslint-patch": "^1.8.0",
|
"@rushstack/eslint-patch": "^1.8.0",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"@vue/eslint-config-prettier": "^9.0.0",
|
"@vue/eslint-config-prettier": "^9.0.0",
|
||||||
|
"babel-polyfill": "^6.26.0",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-vue": "^9.23.0",
|
"eslint-plugin-vue": "^9.23.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"vite": "^5.2.8",
|
"vite": "^5.2.8"
|
||||||
"babel-polyfill": "^6.26.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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>
|
<template>
|
||||||
<div class="home-app">
|
<div class="app">
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -15,8 +15,18 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.home-app {
|
.app {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
button,
|
||||||
|
textarea {
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: arial, "微软雅黑", sans-serif;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
</style>
|
</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 { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import store from './store'
|
||||||
import ElementPlus from 'element-plus'
|
import ElementPlus from 'element-plus'
|
||||||
import 'element-plus/dist/index.css'
|
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'
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
@ -14,6 +15,7 @@ app.use(router)
|
|||||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
app.component(key, component)
|
app.component(key, component)
|
||||||
}
|
}
|
||||||
|
app.use(store)
|
||||||
|
|
||||||
app.use(ElementPlus)
|
app.use(ElementPlus)
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
@ -7,8 +7,8 @@ const router = createRouter({
|
|||||||
path: '/',
|
path: '/',
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
meta: { title: '主页', icon: '', activeIndex: '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 { createStore } from 'vuex'
|
||||||
|
import editor from '@/store/editor.js'
|
||||||
|
|
||||||
const store = createStore({
|
const index = createStore({
|
||||||
|
modules: {
|
||||||
|
editor
|
||||||
|
},
|
||||||
state() {
|
state() {
|
||||||
return {
|
return {
|
||||||
currentRoute: null
|
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 axios from 'axios'
|
||||||
import { ElNotification, ElMessage } from 'element-plus'
|
import { VueAxios } from './axios'
|
||||||
import errorCode from '@/utils/errorCode'
|
import qs from 'qs'
|
||||||
import cache from '@/plugins/cache'
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
|
|
||||||
// 创建 axios 实例
|
// 创建 axios 实例
|
||||||
const service = axios.create({
|
const request = axios.create({
|
||||||
// axios中请求配置有baseURL选项,表示请求URL公共部分
|
// API 请求的默认前缀`
|
||||||
baseURL: import.meta.env.VITE_APP_BASE_API,
|
// baseURL: '/api',
|
||||||
// 超时
|
timeout: 600000, // 请求超时时间
|
||||||
timeout: 10000
|
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(
|
const errorHandler = (error) => {
|
||||||
(config) => {
|
if (error.response) {
|
||||||
// 是否需要防止数据重复提交
|
const data = error.response.data
|
||||||
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
|
console.error('response => ', data)
|
||||||
// get请求映射params参数
|
// const token = Vue.ls.get(ACCESS_TOKEN)
|
||||||
if (config.method === 'get' && config.params) {
|
if (error.response.status === 403) {
|
||||||
let url = config.url + '?' + tansParams(config.params)
|
ElMessage.error(data.message)
|
||||||
url = url.slice(0, -1)
|
|
||||||
config.params = {}
|
|
||||||
config.url = url
|
|
||||||
}
|
}
|
||||||
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
|
if (error.response.status === 401 && !(data.result && data.result.isLogin)) {
|
||||||
const requestObj = {
|
ElMessage.error('Authorization verification failed')
|
||||||
url: config.url,
|
/*if (token) {
|
||||||
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
|
store.dispatch('Logout').then(() => {
|
||||||
time: new Date().getTime()
|
setTimeout(() => {
|
||||||
|
window.location.reload()
|
||||||
|
}, 1500)
|
||||||
|
})
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
const requestSize = Object.keys(JSON.stringify(requestObj)).length // 请求数据大小
|
if (error.response.status === 404) {
|
||||||
const limitSize = 5 * 1024 * 1024 // 限制存放数据5M
|
ElMessage.error(data.path)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return config
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
console.log(error)
|
|
||||||
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)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
export default service
|
// 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 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 { defineConfig, loadEnv } from 'vite'
|
||||||
import createVitePlugins from './vite/plugins'
|
import createVitePlugins from './vite/plugins'
|
||||||
import path from 'path'
|
import { resolve } from 'path'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({ mode, command }) => {
|
export default defineConfig(({ mode, command }) => {
|
||||||
@ -12,16 +12,15 @@ export default defineConfig(({ mode, command }) => {
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
// 设置路径
|
// 设置路径
|
||||||
'~': path.resolve(__dirname, './'),
|
'~': resolve(__dirname, './'),
|
||||||
// 设置别名
|
// 设置别名
|
||||||
'@': path.resolve(__dirname, './src')
|
'@': resolve(__dirname, 'src')
|
||||||
},
|
},
|
||||||
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
|
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3500,
|
port: 3500,
|
||||||
host: true,
|
host: true,
|
||||||
open: true,
|
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
// target: 'http://117.62.238.129:10050',
|
// target: 'http://117.62.238.129:10050',
|
||||||
@ -36,7 +35,8 @@ export default defineConfig(({ mode, command }) => {
|
|||||||
// target: 'http://10.70.132.177:11002',
|
// target: 'http://10.70.132.177:11002',
|
||||||
pathRewrite: { '^/poll': '' },
|
pathRewrite: { '^/poll': '' },
|
||||||
ws: false,
|
ws: false,
|
||||||
changeOrigin: true
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/poll/, '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|