This commit is contained in:
Vinjor 2025-08-29 15:58:27 +08:00
parent ad4672e49c
commit c231ed0832
4 changed files with 608 additions and 304 deletions

View File

@ -120,5 +120,10 @@
"keywords": "China National Heavy Duty Truck Group (CNHTC) HOWO, HOWO Trucks, Heavy Truck Products, Heavy duty Trucks, Commercial Vehicles, HOWO Models, CNHTC Products, HOWO Truck Series, Large Trucks, Commercial Vehicle Products",
"description": "China National Heavy Duty Truck Group Co., Ltd. provides a rich range of HOWO truck products, including various models and configurations. Understand the performance, technical specifications, and industry advantages of our heavy-duty trucks to meet your procurement needs."
}
},
"chat": {
"title": "Online Service",
"description": "Please enter your question... Press Enter to send.",
"littleTitle": "Hello! It's a pleasure to assist you. How may I help you?"
}
}

View File

@ -120,5 +120,10 @@
"keywords": "中国重汽豪沃, 豪沃卡车, 重卡产品, 重型卡车, 商用车, 豪沃车型, 重汽产品, 豪沃卡车系列, 大型卡车, 商用车产品",
"description": "中国重汽豪沃销售有限公司提供丰富的豪沃卡车产品系列,包括各种车型和配置。了解我们的重型卡车性能、技术参数及行业优势,满足您的采购需求。"
}
},
"chat": {
"title": "在线客服",
"description": "请输入您的问题...按Enter发送",
"littleTitle": "您好!很高兴为您服务,请问有什么可以帮您?"
}
}

View File

@ -1,54 +1,54 @@
<template>
<!-- 选择产品对话框 -->
<el-dialog :title="title" :visible.sync="open" @close="close" width="800px" append-to-body>
<el-card style="height: 600px">
<el-container class="AddressBook-container">
<el-header class="AddressBook-header" style="height: 30px">
<span></span>
<!-- <span style="margin-left: 500px">当前在线人数{{count.split(',')[1]}}</span>-->
</el-header>
<el-container>
<el-container>
<el-main class="AddressBook-main">
<el-row style="margin-top: 20px" v-for="item in message" :key="item.createTime">
<el-row v-if="item.dataFrom==='customer'" type="flex" justify="end">
<el-col :span="8">
<el-card shadow="always" :style="'item.dataFrom===customer'?'':'background-color: greenyellow;'">
{{ item.content }}
</el-card>
</el-col>
<el-col :span="2">
<el-avatar shape="square" size="medium" icon="el-icon-user-solid"
style="margin-left: 5px"></el-avatar>
</el-col>
</el-row>
<el-row v-else type="flex" justify="start">
<el-col :span="2">
<el-avatar shape="square" size="medium" icon="el-icon-user-solid"
style="margin-left: 5px"></el-avatar>
</el-col>
<el-col :span="8">
<el-card shadow="always" style="background-color: greenyellow">
{{ item.content }}
</el-card>
</el-col>
<el-dialog :title="$t('chat.title')" :visible.sync="open" @close="close" width="800px" append-to-body class="customer-service-dialog">
<!-- 自定义标题栏 -->
<div slot="header" class="dialog-header">
<div class="service-info">
<h3 class="service-name">{{ $t('chat.title') }}</h3>
</div>
<button class="close-btn" @click="close" aria-label="关闭对话框">×</button>
</div>
<!-- 聊天内容区域 -->
<div class="chat-container">
<!-- 消息列表内部滚动 -->
<div class="message-list-wrapper">
<div class="message-list" ref="messageList">
<!-- 系统欢迎消息 -->
<div v-if="message.length === 0" class="system-message">
{{ $t('chat.littleTitle') }}
</div>
</el-row>
</el-row>
</el-main>
<el-footer class="uesrtext" style="height:150px">
<!-- 消息项 -->
<div
v-for="(msg, index) in message"
:key="index"
:class="['message-item', msg.dataFrom=='customer' ? 'self-message' : 'other-message']"
>
<div class="message-content">
<div class="message-text">{{ msg.content }}</div>
<div class="message-time">{{ formatTime(msg.createTime) }}</div>
</div>
</div>
</div>
</div>
<el-input type="textarea"
class="inputT"
placeholder="按 Enter 发送" v-model="text"
@keyup.enter.native="sendToServer"
></el-input>
<el-button type="primary" icon="el-icon-s-promotion" @click="sendToServer"></el-button>
</el-footer>
</el-container>
</el-container>
</el-container>
</el-card>
<!-- 输入区域 -->
<div class="input-area">
<textarea
v-model="text"
class="message-input"
:placeholder="$t('chat.description')"
@keydown.enter="handleEnterKey"
:disabled="isInputDisabled"
></textarea>
<el-button type="primary"
icon="el-icon-s-promotion"
@click="sendToServer"
:disabled="!text.trim() || isInputDisabled"
>
</el-button>
</div>
</div>
</el-dialog>
</template>
@ -62,7 +62,7 @@ export default {
//
deviceCode: null,
open: false,
title: null,
isInputDisabled: false,
//
message: [],
// ID
@ -70,7 +70,7 @@ export default {
// ID
serviceId: null,
//
text: null,
text: '',
// id
productId: '',
//
@ -80,154 +80,212 @@ export default {
beforeDestroy() {
//
this.$store.dispatch('modules/websocket/websocket_close')
this.closeWebSocket();
},
watch: {
message(val) {
this.scrollBottom()
message: {
handler(newVal) {
// type=3
if (newVal && newVal.length > 0) {
const lastMessage = newVal[newVal.length - 1];
this.isInputDisabled = lastMessage.type && lastMessage.type === 3;
}
this.scrollBottom();
},
deep: true,
immediate: false
}
},
mounted() {
mounted() {
//
},
methods: {
/**
* 处理键盘回车事件
* @param {Event} event - 键盘事件对象
*/
handleEnterKey(event) {
// Shift+Enter
if (event.shiftKey) {
return; //
}
/**用户列表*/
getUserList() {
// Enter
event.preventDefault();
this.sendToServer();
},
/**
* 显示聊天对话框组件
* @param {string} id - 产品ID
*/
show(id) {
this.open = true;
this.productId = id;
this.getChatMain();
},
/**
* 组件显示
* 获取聊天主信息包括设备指纹WebSocket连接和聊天会话
*/
show(id) {
this.open = true
this.productId = id
this.getChatMain()
},
/**获取原有聊天记录*/
getChatMain() {
async getChatMain() {
try {
// id
const components = await new Promise((resolve) => {
Fingerprint2.get((components) => {
resolve(components);
});
});
const values = components.map((component) => component.value);
this.deviceCode = Fingerprint2.x64hash128(values.join(''), 31);
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
//
if (/android|webos|iphone|ipod|BlackBerry|iemobile|opera mini/i.test(userAgent.toLowerCase())) {
this.equipment = '手机端';
} else {
this.equipment = 'pc端';
}
const websocketUrl = process.env.NUXT_ENV.VUE_APP_WEBSOCKET+`${this.deviceCode}`;
// Vuex dispatchWebSocket
this.$store.dispatch('modules/websocket/websocket_init', websocketUrl)
.then(() => {
console.log('WebSocket 初始化成功');
})
.catch((err) => {
console.error('WebSocket 初始化失败:', err);
});
// 使 `this` Vue
this.$axios.$get('/chat/active?deviceCode='+this.deviceCode+'&prodId='+this.productId).then((res) => {
if (null!=res && res.id != null) {
//
this.sessionId = res.id
this.serviceId = res.userId
//
this.loadMessages()
this.equipment = /android|webos|iphone|ipod|BlackBerry|iemobile|opera mini/i
.test(userAgent.toLowerCase()) ? '手机端' : 'pc端';
// if (this.chatMain.jsonArray != null){
// this.message = this.chatMain.jsonArray
// this.$store.dispatch('modules/websocket/set_message',this.chatMain.jsonArray);
// }
const websocketUrl = `${process.env.NUXT_ENV.VUE_APP_WEBSOCKET}${this.deviceCode}`;
// Vuex dispatchWebSocket
await this.$store.dispatch('modules/websocket/websocket_init', websocketUrl);
console.log('WebSocket 初始化成功');
//
const res = await this.$axios.$get(`/chat/active?deviceCode=${this.deviceCode}&prodId=${this.productId}`);
if (res && res.id != null) {
//
this.sessionId = res.id;
this.serviceId = res.userId;
//
await this.loadMessages();
} else {
//
this.createNewSession()
await this.createNewSession();
}
} catch (error) {
console.error('初始化聊天失败:', error);
}
}).catch(error => {
console.error('请求错误:', error);
});
});
},
//
loadMessages(){
this.$axios.$get('/chat/session/'+this.sessionId).then((res) => {
this.message = res
/**
* 加载指定会话的消息内容
*/
async loadMessages() {
try {
const res = await this.$axios.$get(`/chat/session/${this.sessionId}`);
this.message = res;
this.$store.dispatch('modules/websocket/set_message', res);
this.scrollBottom()
}).catch(error => {
console.error('连接客服失败,请稍后再试:', error);
});
this.scrollBottom();
} catch (error) {
console.error('加载消息失败:', error);
}
},
createNewSession() {
/**
* 创建新的聊天会话
*/
async createNewSession() {
try {
const session = {
cusCode: this.deviceCode,
equipment: this.equipment,
prodId: this.productId
}
this.$axios.$post('/chat/newChat', session).then((res) => {
this.sessionId = res.id
this.serviceId = res.userId
};
const res = await this.$axios.$post('/chat/newChat', session);
this.sessionId = res.id;
this.serviceId = res.userId;
//
const wsMsg = {
type: 2,
type: 2,// 2-
toUserId: this.serviceId,
sessionId: this.sessionId,
fromUserId: this.deviceCode
}
};
this.$store.dispatch('modules/websocket/websocket_send', JSON.stringify(wsMsg));
}).catch(error => {
console.error('连接客服失败,请稍后再试:', error);
});
} catch (error) {
console.error('创建新会话失败:', error);
}
},
/**弹窗关闭方法*/
/**
* 关闭聊天对话框
*/
close() {
this.$store.dispatch('modules/websocket/websocket_close')
// let data = {
// id:this.chatMain.id,
// jsonArray:this.message
// }
//
this.$axios.$post('/web/saveMessage', data).then((res)=>{
console.log(res)
})
this.resetChat();
this.open = false;
},
/**发送消息*/
sendToServer() {
/**
* 重置聊天状态清空所有聊天数据
*/
resetChat() {
this.message = [];
this.$store.dispatch('modules/websocket/set_message', []);
this.closeWebSocket();
this.isInputDisabled = false;
this.text = '';
this.sessionId = null;
this.serviceId = null;
},
/**
* 关闭WebSocket连接
*/
closeWebSocket() {
this.$store.dispatch('modules/websocket/websocket_close');
},
/**
* 发送消息到服务器
*/
async sendToServer() {
//
if (!this.text || !this.text.trim() || this.isInputDisabled) {
return;
}
try {
//
const message = {
mainId: this.sessionId,
dataFrom: "customer", //
senderId: this.deviceCode, //
dataFrom: "customer",
senderId: this.deviceCode,
receiverId: this.serviceId,
content: this.text,
}
this.$axios.$post('/chat/newMes', message).then((res) => {
};
await this.$axios.$post('/chat/newMes', message);
// WebSocket
const wsMsg = {
type: 1,
type: 1,// 1-
toUserId: this.serviceId,
content: this.text,
sessionId: this.sessionId,
fromUserId: this.deviceCode
}
};
this.$store.dispatch('modules/websocket/websocket_send', JSON.stringify(wsMsg));
this.addMyMsg()
this.scrollBottom()
}).catch(error => {
console.error('消息发送失败,请关闭聊天窗口稍后再试:', error);
});
this.addMyMsg();
this.scrollBottom();
} catch (error) {
console.error('消息发送失败:', error);
}
},
//
/**
* 将自己发送的消息添加到消息列表中
*/
addMyMsg() {
let newMsg = {
id: new Date().getTime(), // ID
const newMsg = {
id: new Date().getTime(),
mainId: this.sessionId,
dataFrom: "customer", //
dataFrom: "customer",
senderId: this.deviceCode,
receiverId: this.serviceId,
content: this.text,
@ -240,8 +298,9 @@ watch:{
second: '2-digit',
hour12: false
}).replace(/\//g, '-'),
isRead: 1 //
}
isRead: 1
};
//
const updatedMessages = [...this.message, newMsg];
this.$store.dispatch('modules/websocket/set_message', updatedMessages);
@ -250,90 +309,358 @@ watch:{
this.message = updatedMessages;
this.text = '';
},
/**
* 滚动消息列表到底部
*/
scrollBottom() {
//
this.$nextTick(() => {
const chatBox = this.$el.querySelector('.AddressBook-main');
if (chatBox) {
chatBox.scrollTop = chatBox.scrollHeight;
const messageList = this.$refs.messageList;
if (messageList) {
messageList.scrollTop = messageList.scrollHeight;
}
});
},
/**
* 格式化时间显示
* @param {string|number} time - 时间戳或时间字符串
* @returns {string} 格式化后的时间字符串 HH:mm
*/
formatTime(time) {
if (!time) return '';
try {
const date = new Date(time);
if (isNaN(date.getTime())) return ''; //
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
} catch (error) {
console.error('时间格式化错误:', error);
return '';
}
}
}
}
</script>
<style scoped>
<style lang="scss" scoped>
.el-card {
background-color: transparent;
border: none;
.customer-service-dialog {
--primary-color: #409eff;
--primary-light: #e8f3ff;
--primary-dark: #337ecc;
--bg-color: #f7f9fc;
--text-color: #303133;
--text-light: #909399;
--border-radius: 12px;
--shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
}
.AddressBook-container {
/* 标题栏样式 */
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
background-color: #fff;
}
.service-info {
line-height: 1.6;
}
.service-name {
font-size: 16px;
font-weight: 600;
color: var(--text-color);
margin: 0;
}
.service-status {
font-size: 12px;
color: var(--text-light);
display: flex;
align-items: center;
gap: 6px;
}
.online-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #52c41a;
box-shadow: 0 0 0 3px rgba(82, 196, 26, 0.15);
}
.close-btn {
background: none;
border: none;
font-size: 18px;
color: var(--text-light);
cursor: pointer;
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.close-btn:hover {
background-color: var(--bg-color);
color: var(--text-color);
}
/* 聊天容器 */
.chat-container {
padding: 0;
display: flex;
flex-direction: column;
height: 550px;
background-color: var(--bg-color);
}
/* 消息列表容器 */
.message-list-wrapper {
flex: 1;
overflow: hidden;
position: relative;
}
/* 消息列表(内部滚动) */
.message-list {
height: 100%;
width: 100%;
border: 1px solid #909399;
overflow-y: auto;
padding: 20px;
scroll-behavior: smooth;
background-color: var(--bg-color);
background-image:
radial-gradient(var(--primary-color) 0.5px, transparent 0.5px),
radial-gradient(var(--primary-color) 0.5px, var(--bg-color) 0.5px);
background-size: 20px 20px;
background-position: 0 0, 10px 10px;
background-attachment: local;
}
/* 自定义滚动条 */
.message-list::-webkit-scrollbar {
width: 6px;
}
.message-list::-webkit-scrollbar-track {
background: transparent;
}
.message-list::-webkit-scrollbar-thumb {
background-color: rgba(150, 150, 150, 0.3);
border-radius: 3px;
}
.AddressBook-header {
background-color: #0b60b5;
color: #d3dce6;
display: flex;
.message-list::-webkit-scrollbar-thumb:hover {
background-color: rgba(150, 150, 150, 0.5);
}
/* 系统消息 */
.system-message {
text-align: center;
font-size: 12px;
color: var(--text-light);
background-color: rgba(255, 255, 255, 0.85);
padding: 6px 14px;
border-radius: 14px;
margin: 0 auto 20px;
display: inline-block;
box-shadow: var(--shadow);
}
/* 消息项 */
.message-item {
margin-bottom: 18px;
max-width: 70%;
display: inline-block;
animation: fadeIn 0.3s ease-out;
}
.self-message {
float: right;
clear: both;
}
.other-message {
float: left;
clear: both;
}
.message-content {
position: relative;
display: inline-block;
}
.message-text {
padding: 12px 16px;
border-radius: var(--border-radius);
line-height: 1.6;
word-wrap: break-word;
max-width: 100%;
box-shadow: var(--shadow);
}
/* 消息气泡样式 */
.self-message .message-text {
background-color: var(--primary-color);
color: #fff;
border-top-right-radius: 4px;
transition: background-color 0.2s;
}
.self-message .message-text:hover {
background-color: var(--primary-dark);
}
.other-message .message-text {
background-color: #fff;
color: var(--text-color);
border-top-left-radius: 4px;
transition: box-shadow 0.2s;
}
.other-message .message-text:hover {
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.08);
}
/* 消息时间 */
.message-time {
font-size: 11px;
color: var(--text-light);
margin-top: 5px;
text-align: right;
white-space: nowrap;
opacity: 0.8;
transition: opacity 0.2s;
}
.message-item:hover .message-time {
opacity: 1;
}
.other-message .message-time {
text-align: left;
padding-left: 4px;
}
/* 正在输入指示器 */
.typing-indicator {
margin-bottom: 18px;
max-width: 70%;
float: left;
clear: both;
}
.typing-dots {
background-color: #fff;
padding: 12px 16px;
border-radius: var(--border-radius);
border-top-left-radius: 4px;
box-shadow: var(--shadow);
display: inline-flex;
gap: 6px;
align-items: center;
}
.AddressBook-main {
height: 380px;
background-color: #DCDFE6;
.typing-dots span {
width: 8px;
height: 8px;
background-color: var(--text-light);
border-radius: 50%;
animation: typing 1.4s infinite ease-in-out both;
}
.AddressBook-aside {
background-color: #909399;
.typing-dots span:nth-child(1) { animation-delay: -0.32s; }
.typing-dots span:nth-child(2) { animation-delay: -0.16s; }
/* 输入区域 */
.input-area {
display: flex;
gap: 12px;
padding: 15px 20px;
border-top: 1px solid #f0f0f0;
background-color: #fff;
}
.userList {
padding-top: 3px;
box-shadow: 0px 3px 3px #888888;
.name {
vertical-align: top;
/*margin-top: 2px;*/
margin-left: 15px;
font-size: 15px;
}
}
.uesrtext {
position: relative;
bottom: 0;
right: 0;
width: 100%;
height: 100%;
border-top: solid 1px #DDD;
background-color: white;
.inputT {
padding-top: 10px;
width: 100%;
height: 200%;
border: none;
.message-input {
flex: 1;
border: 1px solid #e0e0e0;
border-radius: var(--border-radius);
padding: 12px 15px;
min-height: 42px;
max-height: 120px;
resize: vertical;
outline: none;
transition: all 0.2s;
font-family: inherit;
font-size: 14px;
line-height: 1.5;
}
.el-button {
position: absolute;
bottom: 10px;
right: 10px;
.message-input:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px var(--primary-light);
}
.message-input:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
/* 发送按钮 */
.send-button {
background-color: var(--primary-color);
color: white;
border: none;
border-radius: var(--border-radius);
width: 44px;
height: 44px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.send-button:hover {
background-color: var(--primary-dark);
transform: translateY(-1px);
}
.send-button:active {
transform: translateY(1px);
}
.send-button:disabled {
background-color: #b3d8ff;
cursor: not-allowed;
transform: none;
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message-avatar {
@keyframes typing {
0% { transform: scale(0); }
40% { transform: scale(1); }
80% { transform: scale(0); }
100% { transform: scale(0); }
}
</style>

View File

@ -14,7 +14,20 @@ export const mutations = {
state.socket.onopen=function () {
console.log("WebSocket连接成功");
};
// 注意:这里不再直接处理 onmessage而是在 action 中处理
state.socket.onmessage = function (e) {
console.log(e,'接收到的消息')
if (e.data.startsWith("C")) {
state.count = e.data;
}
else if (e.data.startsWith("系统通知")){
state.notices.push(e.data);
}else if (e.data.startsWith("close")){
console.log(e.data)
} else {
state.messages.push(JSON.parse(e.data));
console.log(state.messages);
}
};
state.socket.onerror= function () {
console.log("WebSocket连接发生错误");
};
@ -22,77 +35,31 @@ export const mutations = {
console.log("connection closed (" + e.code + ")");
};
},
SET_SOCKET_ONMESSAGE(state, handler) {
// 设置 WebSocket 的消息处理函数
if (state.socket) {
state.socket.onmessage = handler;
}
},
WEBSOCKET_SEND(state,msg){
if (state.socket) {
state.socket.send(msg);
}
},
WEBSOCKET_CLOSE(state){
if (state.socket) {
state.socket.close();
state.socket = null;
}
},
SET_MESSAGE(state,msg){
state.messages = msg;
},
ADD_MESSAGE(state, message) {
// 使用展开运算符创建新数组而不是直接 push
state.messages = [...state.messages, message];
},
ADD_NOTICE(state, notice) {
// 使用展开运算符创建新数组而不是直接 push
state.notices = [...state.notices, notice];
},
SET_COUNT(state, count) {
state.count = count;
state.messages = msg
}
}
export const actions = {
websocket_init({ commit, state }, url) {
commit('WEBSOCKET_INIT', url);
// 在 action 中设置 WebSocket 的 onmessage 回调
if (state.socket) {
state.socket.onmessage = function(e) {
console.log(e, '接收到的消息');
if (e.data.startsWith("C")) {
commit('SET_COUNT', e.data);
} else if (e.data.startsWith("系统通知")) {
commit('ADD_NOTICE', e.data);
} else if (e.data.startsWith("close")) {
console.log(e.data);
} else {
commit('ADD_MESSAGE', JSON.parse(e.data));
console.log(state.messages);
}
};
}
websocket_init({commit}, url) {
commit('WEBSOCKET_INIT', url)
},
websocket_send({commit}, msg) {
commit('WEBSOCKET_SEND', msg);
commit('WEBSOCKET_SEND', msg)
},
websocket_close({commit}){
commit('WEBSOCKET_CLOSE');
commit('WEBSOCKET_CLOSE')
},
set_message({commit},msg){
commit('SET_MESSAGE', msg);
commit('SET_MESSAGE',msg)
}
}