1.游客功能添加、删除列表离线游客 目前游客断开没有返回关闭信息

2.游客收不到客服消息 已处理
3.中英文翻译 已完成
4.将本地时间修改为UTC时间 完成
5.发送图片有问题 又重新改回url上传
6.游客用户增加提示和跳转登录的功能
7.客服消息换行及显示已处理
8.客服端限制输入400个字符 用户端限制输入300个字符 完成
This commit is contained in:
2025-05-30 16:39:09 +08:00
parent 99b471bb86
commit e0a7fb8ee2
24 changed files with 289 additions and 163 deletions

View File

@@ -137,6 +137,8 @@
<i v-else class="el-icon-user"></i>
</div>
<div class="message-content">
<!-- 时间显示在右上角 -->
<!-- <span class="message-time">{{ formatTime(msg.time) }}</span> -->
<!-- 文本消息 -->
<div v-if="!msg.isImage" class="message-text">
{{ msg.text }}
@@ -153,7 +155,7 @@
</div>
<div class="message-footer">
<span class="message-time">{{ formatTime(msg.time) }}</span>
<!-- <span class="message-time">{{ formatTime(msg.time) }}</span> -->
<!-- 添加已读状态显示 -->
<span
v-if="msg.type === 'user'"
@@ -191,14 +193,18 @@
:disabled="connectionStatus !== 'connected'"
/>
</div>
<input
type="text"
class="chat-input"
v-model="inputMessage"
@keyup.enter="sendMessage"
:placeholder="$t('chat.inputPlaceholder') || '请输入您的问题...'"
:disabled="connectionStatus !== 'connected'"
/>
<div class="chat-input-wrapper" style="display: flex;align-items: center;">
<input
type="text"
class="chat-input"
v-model="inputMessage"
:maxlength="maxMessageLength"
@input="handleInputMessage"
:placeholder="$t('chat.inputPlaceholder') || '请输入您的问题...'"
:disabled="connectionStatus !== 'connected'"
/>
<!-- <span class="input-counter">{{ maxMessageLength - inputMessage.length }}</span> -->
</div>
<button
class="chat-send"
@click="sendMessage"
@@ -282,6 +288,8 @@ export default {
connectionError: null, // 添加错误信息存储
showRefreshButton: false, // 添加刷新按钮
heartbeatCheckInterval: 30000, // 每30秒检查一次心跳
maxMessageLength: 300,
};
},
@@ -601,10 +609,19 @@ export default {
handleBeforeUnload() {
this.disconnectWebSocket();
},
//只能输入300个字符
handleInputMessage() {
if (this.inputMessage.length > this.maxMessageLength) {
this.inputMessage = this.inputMessage.slice(0, this.maxMessageLength);
}
},
// 发送消息
sendMessage() {
if (!this.inputMessage.trim()) return;
if (this.inputMessage.length > this.maxMessageLength) {
this.$message.warning(`消息不能超过${this.maxMessageLength}个字符`);
return;
}
// 检查 WebSocket 连接状态
if (!this.stompClient || !this.stompClient.connected) {
@@ -1126,14 +1143,22 @@ export default {
this.isChatOpen = !this.isChatOpen;
// 1. 判别身份
// this.determineUserType();
const userInfo = JSON.parse(
localStorage.getItem("jurisdiction") || "{}"
);
if (userInfo.roleKey === "customer_service") {
// 客服用户 跳转到客服页面
this.userType = 2;
const lang = this.$i18n.locale;
this.$router.push(`/${lang}/customerService`);
return;
// this.userEmail = "";
}
// 2. 如果是客服,跳转到客服页面
if (this.userType === 2) {
const lang = this.$i18n.locale;
this.$router.push(`/${lang}/customerService`);
return;
}
if (this.isChatOpen) {
try {
@@ -2151,4 +2176,43 @@ export default {
}
}
}
.message-content {
position: relative;
max-width: 70%;
padding: 18px 15px 10px 15px; // 上方多留空间给时间
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
.message-time {
position: absolute;
top: 6px;
right: 15px;
font-size: 11px;
color: #bbb;
pointer-events: none;
user-select: none;
}
// 用户消息气泡内时间颜色适配
.chat-message-user & .message-time {
color: rgba(255,255,255,0.7);
}
}
</style>

View File

@@ -634,87 +634,87 @@ const router = new VueRouter({
router.beforeEach((to, from, next) => {
// 检查语言参数
const lang = to.params.lang;
const supportedLanguages = ['zh', 'en'];
// router.beforeEach((to, from, next) => {
// // 检查语言参数
// const lang = to.params.lang;
// const supportedLanguages = ['zh', 'en'];
// 如果路径以斜杠结尾且不是根路径,则重定向
if (to.path.endsWith('/') && to.path.length > 1) {
const path = to.path.slice(0, -1);
return next({
path,
query: to.query,
hash: to.hash,
params: to.params
});
}
// // 如果路径以斜杠结尾且不是根路径,则重定向
// if (to.path.endsWith('/') && to.path.length > 1) {
// const path = to.path.slice(0, -1);
// return next({
// path,
// query: to.query,
// hash: to.hash,
// params: to.params
// });
// }
if (!lang && to.path !== '/') {
const defaultLang = localStorage.getItem('lang') || 'en';
return next(`/${defaultLang}${to.path}`);
}
// if (!lang && to.path !== '/') {
// const defaultLang = localStorage.getItem('lang') || 'en';
// return next(`/${defaultLang}${to.path}`);
// }
let data = localStorage.getItem("jurisdiction");
let jurisdiction =JSON.parse(data);
// let data = localStorage.getItem("jurisdiction");
// let jurisdiction =JSON.parse(data);
localStorage.setItem('superReportError',"")
let element = document.getElementsByClassName('el-main')[0];
if(element){
element.scrollTop = 0
}
// localStorage.setItem('superReportError',"")
// let element = document.getElementsByClassName('el-main')[0];
// if(element){
// element.scrollTop = 0
// }
let token
try{
token =JSON.parse(localStorage.getItem('token'))
}catch(e){
console.log(e);
}
// let token
// try{
// token =JSON.parse(localStorage.getItem('token'))
// }catch(e){
// console.log(e);
// }
if (token) {
// if (token) {
if (to.path === `/${lang}/login`|| to.path === `/${lang}/register`) {
next({ path: `/${lang}` })
}else if(to.meta.allAuthority && to.meta.allAuthority[0] ==`all`){
next()
}else if(jurisdiction.roleKey && to.meta.allAuthority&&to.meta.allAuthority.some(item=>item == jurisdiction.roleKey )){
next()
}else{
console.log(to.meta.allAuthority,to.path,"权限");
// if (to.path === `/${lang}/login`|| to.path === `/${lang}/register`) {
// next({ path: `/${lang}` })
// }else if(to.meta.allAuthority && to.meta.allAuthority[0] ==`all`){
// next()
// }else if(jurisdiction.roleKey && to.meta.allAuthority&&to.meta.allAuthority.some(item=>item == jurisdiction.roleKey )){
// next()
// }else{
// console.log(to.meta.allAuthority,to.path,"权限");
Message({//权限不足
showClose: true,
message:i18n.t(`mining.jurisdiction`),
type: 'error'
});
// Message({//权限不足
// showClose: true,
// message:i18n.t(`mining.jurisdiction`),
// type: 'error'
// });
next({ path: `/${lang}` }) // 添加这行,重定向到首页
}
// next({ path: `/${lang}` }) // 添加这行,重定向到首页
// }
}else{
// }else{
let paths = [`/${lang}/miningAccount`,`/${lang}/workOrderRecords`,`/${lang}/userWorkDetails`,`/${lang}/submitWorkOrder`,`/${lang}/workOrderBackend`,`/${lang}/BKWorkDetails`]
if (paths.includes(to.path) || to.path.includes(`personalCenter`) ) {
// let paths = [`/${lang}/miningAccount`,`/${lang}/workOrderRecords`,`/${lang}/userWorkDetails`,`/${lang}/submitWorkOrder`,`/${lang}/workOrderBackend`,`/${lang}/BKWorkDetails`]
// if (paths.includes(to.path) || to.path.includes(`personalCenter`) ) {
Message({//权限不足
showClose: true,
message:i18n.t(`mining.logInFirst`),
type: 'error'
});
// Message({//权限不足
// showClose: true,
// message:i18n.t(`mining.logInFirst`),
// type: 'error'
// });
next({ path: `/${lang}/login` })
} else {
// next({ path: `/${lang}/login` })
// } else {
next()
}
}
// next()
// }
// }
})
// })

View File

@@ -11,11 +11,13 @@
connectionStatus === 'error' ? 'el-icon-warning' : 'el-icon-loading'
"
></i>
<span>{{
connectionStatus === "error"
? $t("chat.Disconnected") || "连接已断开"
: $t("chat.reconnecting") || "正在连接..."
}} </span>
<span
>{{
connectionStatus === "error"
? $t("chat.Disconnected") || "连接已断开"
: $t("chat.reconnecting") || "正在连接..."
}}
</span>
</div>
<!-- 聊天窗口主体 -->
<div class="cs-chat-wrapper">
@@ -58,7 +60,6 @@
<div class="cs-contact-info">
<div class="cs-contact-name">
{{ contact.name }}
</div>
<div class="cs-contact-msg">
<span v-if="contact.important" class="important-tag"
@@ -200,9 +201,11 @@
</div>
<div class="cs-bubble">
<div class="cs-sender">{{ message.sender }}</div>
<div v-if="!message.isImage" class="cs-text">
{{ message.content }}
</div>
<div
v-if="!message.isImage"
class="cs-text"
v-html="formatMessageContent(message.content)"
></div>
<div v-else class="cs-image">
<img
:src="message.content"
@@ -235,7 +238,7 @@
<!-- <i class="el-icon-s-opportunity" title="发送表情"></i> -->
<!-- <i class="el-icon-scissors" title="截图"></i> -->
</div>
<!-- @keydown.enter.prevent="sendMessage" -->
<!-- @keydown.enter.native="handleKeyDown" -->
<div class="cs-input-area">
<el-input
type="textarea"
@@ -246,12 +249,13 @@
$t(`chat.inputMessage`) ||
`请输入消息按Enter键发送按Ctrl+Enter键换行`
"
@keydown.enter.native="handleKeyDown"
@keydown.native="handleKeyDown"
@input="handleInputLimit"
></el-input>
</div>
<div class="cs-send-area">
<span class="cs-counter">{{ inputMessage.length }}/500</span>
<span class="cs-counter">{{ inputMessage.length }}/400</span>
<el-button
type="primary"
:disabled="!currentContact || !inputMessage.trim() || sending"
@@ -387,6 +391,12 @@ export default {
}
// 在组件创建时加载手动创建的聊天室
this.loadManualCreatedRooms();
let userEmail = localStorage.getItem("userEmail");
this.userEmail = JSON.parse(userEmail);
window.addEventListener("setItem", () => {
let userEmail = localStorage.getItem("userEmail");
this.userEmail = JSON.parse(userEmail);
});
// 初始化 WebSocket 连接
this.initWebSocket();
} catch (error) {
@@ -443,14 +453,26 @@ export default {
},
methods: {
handleKeyDown(e) {
// 如果按住了 Ctrl 键,则不发送消息(换行)
if (e.ctrlKey) {
return;
if (e.key === "Enter") {
if (e.ctrlKey) {
// 插入换行
const textarea = e.target;
const value = textarea.value;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
// 在光标处插入\n
textarea.value =
value.substring(0, start) + "\n" + value.substring(end);
// 移动光标到新行
textarea.selectionStart = textarea.selectionEnd = start + 1;
// 阻止默认行为(防止触发发送)
e.preventDefault();
} else {
// 阻止默认行为并发送消息
e.preventDefault();
this.sendMessage();
}
}
// 阻止默认行为
e.preventDefault();
// 发送消息
this.sendMessage();
},
// 初始化 WebSocket 连接
@@ -467,7 +489,7 @@ export default {
this.stompClient.maxWebSocketMessageSize = 16 * 1024 * 1024; // 设置最大消息大小为16MB
this.stompClient.webSocketFactory = () => {
const ws = new WebSocket(wsUrl);
ws.binaryType = 'arraybuffer'; // 设置二进制类型为 arraybuffer
ws.binaryType = "arraybuffer"; // 设置二进制类型为 arraybuffer
return ws;
};
@@ -552,19 +574,19 @@ export default {
const closedUserEmail = message.body;
// 标准化处理 返回的格式 "\"guest_1748242041830_jmz4c9qx5\""
const normalize = (str) => {
if (!str) return '';
if (typeof str === 'object' && 'value' in str) str = str.value;
const normalize = (str) => {
if (!str) return "";
if (typeof str === "object" && "value" in str) str = str.value;
str = String(str).trim().toLowerCase();
// 去除所有首尾引号
str = str.replace(/^['"]+|['"]+$/g, '');
str = str.replace(/^['"]+|['"]+$/g, "");
return str;
};
const targetEmail = normalize(closedUserEmail);
// 在联系人列表中查找对应的聊天室
const contactIndex = this.contacts.findIndex(contact => {
// 在联系人列表中查找对应的聊天室
const contactIndex = this.contacts.findIndex((contact) => {
const contactEmail = normalize(contact.name);
return contactEmail === targetEmail;
});
@@ -688,6 +710,13 @@ export default {
const now = new Date();
return new Date(now.getTime() + now.getTimezoneOffset() * 60000);
},
//只能输入400个字符
handleInputLimit(val) {
if (val.length > 400) {
this.inputMessage = val.slice(0, 400);
}
},
// 发送消息
async sendMessage() {
if (!this.inputMessage.trim() || !this.currentContact || this.sending)
@@ -698,23 +727,29 @@ export default {
this.sending = true;
try {
// 判断接收者类型
if (this.currentContact.sendUserType !== undefined) {
this.receiveUserType = this.currentContact.sendUserType;
} else {
this.receiveUserType = 1;
// 检查连接状态
if (!this.isWebSocketConnected) {
this.$message.warning(
this.$t("chat.reconnecting") || "正在重新连接..."
);
await this.initWebSocket(); // 尝试重新连接
}
// 正确设置接收者类型
const receiveUserType =
this.currentContact.sendUserType !== undefined
? this.currentContact.sendUserType
: 1; // 默认登录用户
const message = {
content: messageContent,
type: 1, // 1 表示文字消息
email: this.currentContact.name,
receiveUserType: this.receiveUserType,
receiveUserType: receiveUserType,
roomId: this.currentContactId,
// sendTime: this.convertToUTC(new Date()).toISOString() // 添加 UTC 时间
};
// 发送消息
// // 发送消息
this.stompClient.send(
"/point/send/message/to/user",
{},
@@ -725,7 +760,7 @@ export default {
this.addMessageToChat({
sender: this.$t("chat.my") || "我",
content: messageContent,
time: new Date().toISOString() , // 使用 UTC 时间字符串
time: new Date().toISOString(), // 使用 UTC 时间字符串
isSelf: true,
isImage: false,
roomId: this.currentContactId,
@@ -739,17 +774,26 @@ export default {
if (contact) {
contact.unread = 0;
}
} catch (error) {
console.error("发送消息失败:", error);
this.$message({
message: this.$t("chat.sendFailed") || "发送消息失败,请重试",
type: "error",
duration: 3000,
showClose: true,
});
} finally {
this.sending = false;
}
} catch (error) {
console.error("发送消息失败飞机飞机覅附件:", error);
this.sending = false;
// 仅显示网络或连接错误
if (error.message.includes("connection")) {
this.$message.error(this.$t("chat.connectionFailed") || "连接失败,请检查网络");
}else{
this.$message.error(this.$t("chat.sendFailed") || "发送消息失败");
}
}
},
//换行消息显示处理
formatMessageContent(content) {
if (!content) return "";
// 防止XSS建议先做转义如有需要
return content.replace(/\n/g, "<br>");
},
// 订阅个人消息队列
@@ -803,7 +847,7 @@ export default {
lastTime: messageData.time, // 直接使用 createTime
unread: 1,
important: false,
isGuest: messageData.sendUserType === 0,
isGuest: msg.sendUserType === 0,
sendUserType: messageData.sendUserType,
isManualCreated: true,
};
@@ -951,17 +995,16 @@ export default {
this.contacts = [...this.contacts, ...uniqueNewContacts];
this.sortContacts();
}
}else{
} else {
this.$message({
message: this.$t("chat.contactFailed") || "加载更多联系人失败",
type: "error",
duration: 3000,
showClose: true,
});
message: this.$t("chat.contactFailed") || "加载更多联系人失败",
type: "error",
duration: 3000,
showClose: true,
});
}
} catch (error) {
console.error("5858", error);
} finally {
this.isLoadingMoreContacts = false;
}
@@ -1181,7 +1224,9 @@ export default {
// 获取该聊天室的最新消息时间
const latestMessage = this.messages[room.id]?.[0] || null;
const lastTime = latestMessage ? latestMessage.time : room.lastUserSendTime || room.createTime;
const lastTime = latestMessage
? latestMessage.time
: room.lastUserSendTime || room.createTime;
return {
roomId: room.id,
@@ -1503,16 +1548,16 @@ export default {
this.$message({ message: "正在上传图片...", type: "info" });
// 创建 FormData
const formData = new FormData();
formData.append('file', file);
formData.append("file", file);
// 上传图片
const response = await this.$axios({
method: 'post',
method: "post",
url: `${process.env.VUE_APP_BASE_API}pool/ticket/uploadFile`,
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
"Content-Type": "multipart/form-data",
},
});
if (response.data.code === 200) {
@@ -1534,10 +1579,11 @@ export default {
// 发送图片消息
this.sendImageMessage(imageUrl);
// 等待图片加载完成后滚动到底部
this.$nextTick(() => {
const img = this.$refs.messageContainer.querySelector('.cs-image img');
const img =
this.$refs.messageContainer.querySelector(".cs-image img");
if (img) {
// 创建一个新的 Promise 来处理图片加载
const imgLoadPromise = new Promise((resolve) => {
@@ -1578,7 +1624,7 @@ export default {
showClose: true,
});
} else {
throw new Error(response.data.msg || '上传失败');
throw new Error(response.data.msg || "上传失败");
}
} catch (error) {
console.error("上传图片异常:", error);
@@ -1611,9 +1657,8 @@ export default {
receiveUserType: this.currentContact.sendUserType || 1,
roomId: this.currentContactId,
content: imageUrl, // 使用接口返回的url
};
this.stompClient.send(
"/point/send/message/to/user",
{},
@@ -1709,17 +1754,26 @@ export default {
// 根据重要性对联系人列表排序
sortContacts() {
console.log("排序前的联系人列表", this.contacts);
this.contacts.sort((a, b) => {
// 首先按重要性排序(重要的在前)
if (a.important && !b.important) return -1;
if (!a.important && b.important) return 1;
// 确保日期字符串正确比较
const aTime = a.lastTime || '';
const bTime = b.lastTime || '';
// const aTime = a.lastTime || "";
// const bTime = b.lastTime || "";
// console.log("aTime", aTime);
// console.log("bTime", bTime);
// 直接比较时间字符串,因为格式是 "YYYY-MM-DDTHH:mm:ss"
return bTime.localeCompare(aTime);
// 2. 时间倒序(最新在前)
const aTime = a.lastTime ? new Date(a.lastTime).getTime() : 0;
const bTime = b.lastTime ? new Date(b.lastTime).getTime() : 0;
return bTime - aTime;
});
},
@@ -1772,19 +1826,21 @@ export default {
// 格式化消息时间(只做格式化,不做多余转换)
formatTime(date) {
if (!date) return "";
const str = typeof date === 'string' ? date : date.toISOString();
const [d, t] = str.split('T');
const str = typeof date === "string" ? date : date.toISOString();
const [d, t] = str.split("T");
if (!t) return str;
const [hour, minute] = t.split(':');
const [hour, minute] = t.split(":");
// 取当前UTC日期
const now = new Date();
const nowUTC = now.toISOString().split('T')[0];
const nowUTC = now.toISOString().split("T")[0];
const msgUTC = d;
if (nowUTC === msgUTC) {
return `UTC ${this.$t("chat.today")} ${hour}:${minute}`;
}
// 判断昨天
const yesterdayUTC = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().split('T')[0];
const yesterdayUTC = new Date(Date.now() - 24 * 60 * 60 * 1000)
.toISOString()
.split("T")[0];
if (yesterdayUTC === msgUTC) {
return `UTC ${this.$t("chat.yesterday")} ${hour}:${minute}`;
}
@@ -1795,12 +1851,12 @@ export default {
formatLastTime(date) {
if (!date) return "";
// 确保是字符串格式
const str = typeof date === 'string' ? date : date.toISOString();
const str = typeof date === "string" ? date : date.toISOString();
// 直接提取时间部分
const timePart = str.split('T')[1];
const timePart = str.split("T")[1];
if (!timePart) return str;
// 只取小时和分钟
const [hours, minutes] = timePart.split(':');
const [hours, minutes] = timePart.split(":");
return `${hours}:${minutes} UTC`;
},
@@ -1928,7 +1984,7 @@ export default {
if (!this.currentContact) return;
this.$refs.imageInput.click();
},
// 将本地时间转换为 UTC 时间
// 将本地时间转换为 UTC 时间
convertToUTC(date) {
if (!date) return null;
const d = new Date(date);
@@ -1944,11 +2000,10 @@ export default {
// 修正后端返回不带Z的UTC时间字符串
fixToUTC(str) {
if (typeof str !== 'string') return str;
if (str.endsWith('Z') || /[+-]\d{2}:?\d{2}$/.test(str)) return str;
return str + 'Z';
if (typeof str !== "string") return str;
if (str.endsWith("Z") || /[+-]\d{2}:?\d{2}$/.test(str)) return str;
return str + "Z";
},
},
beforeDestroy() {

View File

@@ -269,7 +269,7 @@
<span class="title">{{ $t(`personal.verificationCode`) }}</span>
<div class="verificationCode">
<el-input
type="email"
type="text"
v-model="closeParams.eCode"
autocomplete="off"
:placeholder="$t(`user.verificationCode`)"