|
|
@@ -705,6 +705,8 @@ const msgContainerRef = ref(null);
|
|
|
const fileInput = ref(null);
|
|
|
const imageInput = ref(null);
|
|
|
let countdownTimer = null;
|
|
|
+let pollTimer = null; // 轮询兜底定时器
|
|
|
+const POLL_INTERVAL = 5000; // 轮询间隔 5 秒
|
|
|
|
|
|
// 高度调整逻辑
|
|
|
const inputHeight = ref(200); // 默认最小高度
|
|
|
@@ -1028,23 +1030,23 @@ async function handleSend() {
|
|
|
content: text
|
|
|
};
|
|
|
|
|
|
- // 如果WebSocket连接正常,优先由WS广播,这里本地不直接Push,等待WS回传
|
|
|
- // 但为了UI即时反馈,可以先Push一个带加载状态的对象,通过msgNo去重
|
|
|
+ // 为了 UI 即时反馈,先 Push 一个带 loading 状态的占位消息,通过 msgNo 去重
|
|
|
messageList.value.push({
|
|
|
msgNo: msgNo,
|
|
|
sender: 'waiter',
|
|
|
type: 'text',
|
|
|
content: text,
|
|
|
time: getFullTime(),
|
|
|
- loading: true // 增加加载状态
|
|
|
+ loading: true // 等待服务端确认后会由 WS 或主动刷新替换
|
|
|
});
|
|
|
scrollToBottom();
|
|
|
|
|
|
- // 调用REST API发送消息(后端会通过WS给所有人发广播)
|
|
|
+ // 调用 REST API 发送消息(后端会通过 WS 给所有人广播)
|
|
|
const response = await sendTextMessage(messageData);
|
|
|
|
|
|
if (response.code === 200 || response.code === 0) {
|
|
|
- // 发送成功,等待WS推送真实消息回来替换本地Loading状态
|
|
|
+ // 发送成功后主动拉取一次最新消息,确保即便 WS 推送延迟也能立即显示
|
|
|
+ await loadMessagesSilent();
|
|
|
} else {
|
|
|
ElMessage.error('发送错误: ' + (response.msg || '未知错误'));
|
|
|
}
|
|
|
@@ -1145,6 +1147,7 @@ async function selectSession(session) {
|
|
|
}
|
|
|
return {
|
|
|
msgId: msg.msgId,
|
|
|
+ msgNo: msg.msgNo, // 补充 msgNo,用于 WS 推送的去重
|
|
|
sender: sender,
|
|
|
senderAvatar: msg.senderAvatar,
|
|
|
type: msg.msgType || 'text',
|
|
|
@@ -1350,6 +1353,87 @@ function scrollToBottom() {
|
|
|
nextTick(() => { if (msgContainerRef.value) msgContainerRef.value.scrollTop = msgContainerRef.value.scrollHeight; });
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 静默拉取当前会话最新消息,与 WebSocket 推送互为兜底
|
|
|
+ * 使用消息指纹进行去重,只有内容变化时才更新 DOM
|
|
|
+ */
|
|
|
+async function loadMessagesSilent() {
|
|
|
+ if (!activeSessionId.value) return;
|
|
|
+ try {
|
|
|
+ const response = await getMessageHistory({
|
|
|
+ sessionId: activeSessionId.value,
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 50
|
|
|
+ });
|
|
|
+ if ((response.code === 200 || response.rows)) {
|
|
|
+ const rows = response.rows || response.data?.rows || [];
|
|
|
+ const newMessages = rows.map(msg => {
|
|
|
+ let parsedPayload = null;
|
|
|
+ if ((msg.msgType === 'job_card' || msg.msgType === 'order_card') && msg.payload) {
|
|
|
+ parsedPayload = typeof msg.payload === 'string' ? JSON.parse(msg.payload) : msg.payload;
|
|
|
+ } else if (msg.msgType === 'file') {
|
|
|
+ parsedPayload = { name: msg.fileName || '未知文件', url: msg.fileUrl, size: '未知大小' };
|
|
|
+ } else if (msg.msgType === 'image') {
|
|
|
+ parsedPayload = { url: msg.fileUrl || msg.content };
|
|
|
+ }
|
|
|
+ let sender;
|
|
|
+ if (msg.senderRole === 'waiter' || msg.senderRole === 'system') {
|
|
|
+ sender = 'waiter';
|
|
|
+ } else if (msg.senderRole === 'user') {
|
|
|
+ sender = 'customer';
|
|
|
+ } else {
|
|
|
+ sender = msg.senderType === 2 ? 'waiter' : 'customer';
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ msgId: msg.msgId,
|
|
|
+ msgNo: msg.msgNo,
|
|
|
+ sender,
|
|
|
+ senderAvatar: msg.senderAvatar,
|
|
|
+ type: msg.msgType || 'text',
|
|
|
+ content: msg.content,
|
|
|
+ time: msg.sendTime,
|
|
|
+ payload: parsedPayload,
|
|
|
+ fileUrl: msg.fileUrl,
|
|
|
+ fileName: msg.fileName
|
|
|
+ };
|
|
|
+ }).reverse();
|
|
|
+
|
|
|
+ // 使用消息指纹去重,只在内容有变化时更新(避免不必要的 DOM 重绘)
|
|
|
+ const sig1 = newMessages.map(m => String(m.msgId) + String(m.content)).join('|');
|
|
|
+ const sig2 = messageList.value
|
|
|
+ .filter(m => m.msgId) // 过滤掉仅本地的 loading 消息
|
|
|
+ .map(m => String(m.msgId) + String(m.content)).join('|');
|
|
|
+
|
|
|
+ if (sig1 !== sig2) {
|
|
|
+ messageList.value = newMessages;
|
|
|
+ scrollToBottom();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[CustService] 静默拉取消息失败:', error);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 启动轮询兜底:每 POLL_INTERVAL 毫秒自动刷新当前会话消息
|
|
|
+ * 确保 WebSocket 断开或推送延迟时消息仍然能自动显示
|
|
|
+ */
|
|
|
+function startPolling() {
|
|
|
+ stopPolling();
|
|
|
+ pollTimer = window.setInterval(async () => {
|
|
|
+ if (!activeSessionId.value) return;
|
|
|
+ await loadMessagesSilent();
|
|
|
+ }, POLL_INTERVAL);
|
|
|
+}
|
|
|
+
|
|
|
+/** 停止轮询 */
|
|
|
+function stopPolling() {
|
|
|
+ if (pollTimer) {
|
|
|
+ window.clearInterval(pollTimer);
|
|
|
+ pollTimer = null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// 监听档案标签页切换,如果切到订单页则加载数据
|
|
|
watch(archiveActiveTab, (val) => {
|
|
|
if (val === 'order') {
|
|
|
@@ -1498,11 +1582,18 @@ onMounted(() => {
|
|
|
|
|
|
// 初始化 WebSocket 连接(监听全局通知)
|
|
|
reconnectChat();
|
|
|
+
|
|
|
+ // 启动轮询兜底,防止 WebSocket 断开后消息停止更新
|
|
|
+ startPolling();
|
|
|
});
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
stopResizing();
|
|
|
if (countdownTimer) clearInterval(countdownTimer);
|
|
|
+ // 断开 WebSocket 防止连接泄漏
|
|
|
+ disconnectChat();
|
|
|
+ // 停止轮询
|
|
|
+ stopPolling();
|
|
|
});
|
|
|
</script>
|
|
|
|