客户web端完善

This commit is contained in:
huangge1199 2024-05-17 13:21:50 +08:00
parent f3fece7b5f
commit 0102fe6a59
68 changed files with 1495 additions and 161 deletions

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
public/emojis/发呆.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
public/emojis/呕吐.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
public/emojis/呲牙.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
public/emojis/坏笑.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
public/emojis/大惊.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
public/emojis/大笑.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
public/emojis/大闹.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
public/emojis/天使.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
public/emojis/奋斗.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
public/emojis/尴尬.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
public/emojis/开心.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
public/emojis/心碎.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
public/emojis/恶魔.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
public/emojis/惊恐.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
public/emojis/惊悚.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
public/emojis/惊讶.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
public/emojis/感冒.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
public/emojis/愤怒.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
public/emojis/懵逼.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
public/emojis/无聊.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
public/emojis/汗颜.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
public/emojis/流汗.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/emojis/流泪.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
public/emojis/点赞.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
public/emojis/爱你.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
public/emojis/爱心.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
public/emojis/犯困.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
public/emojis/生气.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
public/emojis/白眼.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
public/emojis/睡着.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
public/emojis/瞌睡.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
public/emojis/笑哭.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
public/emojis/笑脸.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
public/emojis/讨厌.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
public/emojis/调皮.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
public/emojis/酷.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
public/emojis/闭嘴.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
public/emojis/难过.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
public/emojis/飞吻.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
public/emojis/饿死.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
public/emojis/骂人.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -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>

View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
src/assets/img/face.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

BIN
src/assets/img/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
src/assets/img/msg-tip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/assets/img/user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

247
src/assets/js/emoji.js Normal file
View 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

View File

@ -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;
}
}

View File

@ -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')

View File

@ -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
View 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

View File

@ -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
View 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
}

View File

@ -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 request = axios.create({
const service = axios.create({ // API 请求的默认前缀`
// axios中请求配置有baseURL选项表示请求URL公共部分 // baseURL: '/api',
baseURL: import.meta.env.VITE_APP_BASE_API, timeout: 600000, // 请求超时时间
// 超时 maxBodyLength: 5 * 1024 * 1024,
timeout: 10000 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()
const requestSize = Object.keys(JSON.stringify(requestObj)).length // 请求数据大小 }, 1500)
const limitSize = 5 * 1024 * 1024 // 限制存放数据5M })
if (requestSize >= limitSize) { }*/
console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制无法进行防重复提交验证。') }
return config if (error.response.status === 404) {
} ElMessage.error(data.path)
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)
} }
) return Promise.reject(error)
}
// 响应拦截器 // request interceptor
service.interceptors.response.use( request.interceptors.request.use(config => {
(res) => { /*const token = Vue.ls.get(ACCESS_TOKEN)
// 未设置状态码则默认成功状态 if (token) {
const code = res.data.code || 200 config.headers.Authorization = token
// 获取错误信息 }*/
const msg = errorCode[code] || res.data.msg || errorCode['default'] return config
// 二进制数据则直接返回 }, errorHandler)
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
return res.data // response interceptor
} request.interceptors.response.use((response) => {
if (code === 500) { return response.data
ElMessage({ message: msg, type: 'error' }) }, errorHandler)
return Promise.reject(new Error(msg))
} else if (code === 601) { const installer = {
ElMessage({ message: msg, type: 'warning' }) vm: {},
return Promise.reject(new Error(msg)) install (Vue) {
} else if (code !== 200) { Vue.use(VueAxios, request)
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)
} }
) }
export default service export default request
export {
installer as VueAxios,
request as axios
}

36
src/views/HeaderView.vue Normal file
View 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>

View File

@ -1,11 +0,0 @@
<template>
</template>
<script setup name="">
</script>
<style scoped>
</style>

59
src/views/HomeView.vue Normal file
View 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
View 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
View 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>

View File

@ -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/, '')
} }
} }
}, },