|
@@ -61,7 +61,7 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
|
-import { getChatHistory } from "@/api/chat";
|
|
|
|
|
|
|
+import { getChatHistory } from '@/api/chat';
|
|
|
export default {
|
|
export default {
|
|
|
data() {
|
|
data() {
|
|
|
return {
|
|
return {
|
|
@@ -73,9 +73,9 @@ export default {
|
|
|
isMultiUser: false,
|
|
isMultiUser: false,
|
|
|
// 当前用户信息
|
|
// 当前用户信息
|
|
|
currentUser: {
|
|
currentUser: {
|
|
|
|
|
+ id: 0,
|
|
|
customId: 0,
|
|
customId: 0,
|
|
|
shopId: 0,
|
|
shopId: 0,
|
|
|
- id: 0,
|
|
|
|
|
username: '',
|
|
username: '',
|
|
|
avatar: ''
|
|
avatar: ''
|
|
|
},
|
|
},
|
|
@@ -87,7 +87,13 @@ export default {
|
|
|
room: '',
|
|
room: '',
|
|
|
reconnectCount: 0,
|
|
reconnectCount: 0,
|
|
|
maxReconnectCount: 3,
|
|
maxReconnectCount: 3,
|
|
|
- isManualDisconnect: false // 添加标志位:是否主动断开连接
|
|
|
|
|
|
|
+ isManualDisconnect: false, // 添加标志位:是否主动断开连接
|
|
|
|
|
+ // 调试选项
|
|
|
|
|
+ debugOptions: {
|
|
|
|
|
+ forceBasic: false, // 强制使用基础参数
|
|
|
|
|
+ forceSegmented: false, // 强制使用分段传输
|
|
|
|
|
+ logDetails: true // 是否输出详细日志
|
|
|
|
|
+ }
|
|
|
};
|
|
};
|
|
|
},
|
|
},
|
|
|
onLoad(options) {
|
|
onLoad(options) {
|
|
@@ -107,30 +113,25 @@ export default {
|
|
|
if (options.name) {
|
|
if (options.name) {
|
|
|
this.currentUser.username = options.name;
|
|
this.currentUser.username = options.name;
|
|
|
}
|
|
}
|
|
|
- if (options.shopId) {
|
|
|
|
|
- // 是门店,则使用shopId
|
|
|
|
|
- this.currentUser.id = options.shopId;
|
|
|
|
|
|
|
+ if (options.staffId) {
|
|
|
|
|
+ // 是门店,则使用staffId
|
|
|
|
|
+ this.currentUser.id = options.staffId;
|
|
|
}
|
|
}
|
|
|
if (options.avatar) {
|
|
if (options.avatar) {
|
|
|
this.currentUser.avatar = options.avatar;
|
|
this.currentUser.avatar = options.avatar;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 获取房间号
|
|
// 获取房间号
|
|
|
- if (options.room) {
|
|
|
|
|
- this.room = options.room;
|
|
|
|
|
|
|
+ if (this.currentUser.customId && this.currentUser.shopId) {
|
|
|
|
|
+ this.room = 'custom_id-' + this.currentUser.customId + 'AND' + 'shop_id-' + this.currentUser.shopId;
|
|
|
} else {
|
|
} else {
|
|
|
- if (this.currentUser.customId && this.currentUser.shopId) {
|
|
|
|
|
- this.room = 'custom_id-' + this.currentUser.customId + 'AND' + 'shop_id-' + this.currentUser.shopId;
|
|
|
|
|
- } else {
|
|
|
|
|
- // 如果没有传入房间号,使用默认值或生成一个
|
|
|
|
|
- this.room = 'default_room';
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 如果没有传入房间号,使用默认值或生成一个
|
|
|
|
|
+ this.room = 'default_room';
|
|
|
}
|
|
}
|
|
|
console.log('---------- this.room: ', this.room);
|
|
console.log('---------- this.room: ', this.room);
|
|
|
|
|
|
|
|
// 加载聊天记录
|
|
// 加载聊天记录
|
|
|
this.loadChatHistory();
|
|
this.loadChatHistory();
|
|
|
-
|
|
|
|
|
// 连接WebSocket
|
|
// 连接WebSocket
|
|
|
this.connectWebSocket();
|
|
this.connectWebSocket();
|
|
|
},
|
|
},
|
|
@@ -159,11 +160,9 @@ export default {
|
|
|
init() {},
|
|
init() {},
|
|
|
// 加载聊天历史记录
|
|
// 加载聊天历史记录
|
|
|
loadChatHistory() {
|
|
loadChatHistory() {
|
|
|
- // 在实际应用中,这里应该从服务器加载历史聊天记录
|
|
|
|
|
- // 可以通过API请求获取该房间的历史消息
|
|
|
|
|
getChatHistory({
|
|
getChatHistory({
|
|
|
roomName: this.room,
|
|
roomName: this.room,
|
|
|
- userId: this.currentUser.id,
|
|
|
|
|
|
|
+ userId: this.currentUser.id
|
|
|
}).then((res) => {
|
|
}).then((res) => {
|
|
|
if (res.code == 1 && res.data) {
|
|
if (res.code == 1 && res.data) {
|
|
|
this.messageList = res.data;
|
|
this.messageList = res.data;
|
|
@@ -172,43 +171,17 @@ export default {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
-
|
|
|
|
|
- // 临时保留一些示例数据用于开发测试
|
|
|
|
|
- // if (process.env.NODE_ENV === "development") {
|
|
|
|
|
- // const sampleMessages = [
|
|
|
|
|
- // {
|
|
|
|
|
- // id: "msg_001",
|
|
|
|
|
- // content: "您好,请问有什么可以帮助您的吗?",
|
|
|
|
|
- // time: "14:30",
|
|
|
|
|
- // isMine: false,
|
|
|
|
|
- // username: "花店客服",
|
|
|
|
|
- // avatar: "https://cdn.uviewui.com/uview/demo/user/2.jpg",
|
|
|
|
|
- // },
|
|
|
|
|
- // {
|
|
|
|
|
- // id: "msg_002",
|
|
|
|
|
- // content: "我想订购一束玫瑰花",
|
|
|
|
|
- // time: "14:31",
|
|
|
|
|
- // isMine: true,
|
|
|
|
|
- // username: "我",
|
|
|
|
|
- // avatar: "https://cdn.uviewui.com/uview/demo/user/1.jpg",
|
|
|
|
|
- // },
|
|
|
|
|
- // ];
|
|
|
|
|
- // this.messageList = sampleMessages;
|
|
|
|
|
- // }
|
|
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
// 获取主机地址
|
|
// 获取主机地址
|
|
|
getHost() {
|
|
getHost() {
|
|
|
- // 在实际项目中,这里应该返回你的WebSocket服务器地址
|
|
|
|
|
- // 可以从配置文件中读取
|
|
|
|
|
-
|
|
|
|
|
// #ifdef H5
|
|
// #ifdef H5
|
|
|
- return window.location.host;
|
|
|
|
|
|
|
+ return this.$constant.webSocketUrl;
|
|
|
// #endif
|
|
// #endif
|
|
|
|
|
|
|
|
// #ifdef MP-WEIXIN || APP-PLUS
|
|
// #ifdef MP-WEIXIN || APP-PLUS
|
|
|
// 小程序和App环境下,需要配置具体的服务器地址
|
|
// 小程序和App环境下,需要配置具体的服务器地址
|
|
|
- return '192.168.10.57:8080'; // 替换为实际的服务器地址
|
|
|
|
|
|
|
+ return this.$constant.webSocketUrl; // 替换为实际的服务器地址
|
|
|
// #endif
|
|
// #endif
|
|
|
},
|
|
},
|
|
|
|
|
|
|
@@ -225,11 +198,15 @@ export default {
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
// 构建WebSocket URL
|
|
// 构建WebSocket URL
|
|
|
- const wsUrl = `ws://${this.getHost()}/room?room=${this.room}`;
|
|
|
|
|
|
|
+ const wsUrl = `ws://${this.getHost()}/room`;
|
|
|
|
|
|
|
|
|
|
+ // 构建子协议参数(模拟POST body传输)
|
|
|
|
|
+ const protocols = this.buildWebSocketProtocols();
|
|
|
|
|
+ console.log('protocols: ', protocols);
|
|
|
// 创建WebSocket连接
|
|
// 创建WebSocket连接
|
|
|
this.socket = uni.connectSocket({
|
|
this.socket = uni.connectSocket({
|
|
|
url: wsUrl,
|
|
url: wsUrl,
|
|
|
|
|
+ protocols: protocols,
|
|
|
success: () => {
|
|
success: () => {
|
|
|
console.log('WebSocket连接创建成功');
|
|
console.log('WebSocket连接创建成功');
|
|
|
},
|
|
},
|
|
@@ -245,12 +222,6 @@ export default {
|
|
|
this.isConnected = true;
|
|
this.isConnected = true;
|
|
|
this.reconnectCount = 0;
|
|
this.reconnectCount = 0;
|
|
|
this.isManualDisconnect = false; // 连接成功时重置标志位
|
|
this.isManualDisconnect = false; // 连接成功时重置标志位
|
|
|
-
|
|
|
|
|
- // uni.showToast({
|
|
|
|
|
- // title: "连接成功",
|
|
|
|
|
- // icon: "success",
|
|
|
|
|
- // duration: 1500,
|
|
|
|
|
- // });
|
|
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// 监听WebSocket消息
|
|
// 监听WebSocket消息
|
|
@@ -283,6 +254,208 @@ export default {
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
|
|
+ // 构建WebSocket子协议参数(模拟POST body传输)
|
|
|
|
|
+ buildWebSocketProtocols() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 创建房间参数对象
|
|
|
|
|
+ const roomParams = {
|
|
|
|
|
+ u_id: parseInt(this.currentUser.id), // TODO 就是 shopId,应该修改为 staffId
|
|
|
|
|
+ c_id: parseInt(this.currentUser.customId), //因为是门店客户端,customId为0,好推测出是门店端连接
|
|
|
|
|
+ s_id: parseInt(this.currentUser.shopId),
|
|
|
|
|
+ u_name: this.currentUser.username && this.currentUser.username.length > 30 ? this.currentUser.username.substring(0, 30) : this.currentUser.username,
|
|
|
|
|
+ platform: this.getPlatformInfo(),
|
|
|
|
|
+ type: 'shop' // 客户端类型:shop:门店端, customer:客户端
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 分段传输(如果参数过长)
|
|
|
|
|
+ const segments = this.createSegmentedProtocols(roomParams);
|
|
|
|
|
+ if (segments.length == 0) {
|
|
|
|
|
+ this.$msg('分段传输失败');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 根据调试选项和长度选择合适的方案
|
|
|
|
|
+ let protocols;
|
|
|
|
|
+ if (segments.length > 0) {
|
|
|
|
|
+ protocols = ['chat', ...segments];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 验证协议字符串的合法性
|
|
|
|
|
+ const isValid = this.validateProtocols(protocols);
|
|
|
|
|
+ console.log('协议验证结果:', isValid);
|
|
|
|
|
+ if (!isValid) {
|
|
|
|
|
+ this.$msg('协议验证失败');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return protocols;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('构建WebSocket protocols失败:', error);
|
|
|
|
|
+ return ['chat']; // 返回基础协议作为fallback
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 获取平台信息
|
|
|
|
|
+ getPlatformInfo() {
|
|
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
|
|
+ return 'MP-WEIXIN';
|
|
|
|
|
+ // #endif
|
|
|
|
|
+
|
|
|
|
|
+ // #ifdef APP-PLUS
|
|
|
|
|
+ return 'APP-PLUS';
|
|
|
|
|
+ // #endif
|
|
|
|
|
+
|
|
|
|
|
+ // #ifdef MP-ALIPAY
|
|
|
|
|
+ return 'MP-ALIPAY';
|
|
|
|
|
+ // #endif
|
|
|
|
|
+
|
|
|
|
|
+ // #ifdef H5
|
|
|
|
|
+ return 'H5';
|
|
|
|
|
+ // #endif
|
|
|
|
|
+
|
|
|
|
|
+ return 'UNKNOWN';
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // Base64编码方法(跨平台兼容)
|
|
|
|
|
+ base64Encode(str) {
|
|
|
|
|
+ // #ifdef H5
|
|
|
|
|
+ // H5环境使用浏览器原生方法
|
|
|
|
|
+ try {
|
|
|
|
|
+ return btoa(str);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('H5 Base64编码失败:', error);
|
|
|
|
|
+ return this.customBase64Encode(str);
|
|
|
|
|
+ }
|
|
|
|
|
+ // #endif
|
|
|
|
|
+
|
|
|
|
|
+ // #ifdef MP-WEIXIN || MP-ALIPAY || APP-PLUS
|
|
|
|
|
+ // 小程序和App环境使用自定义编码
|
|
|
|
|
+ return this.customBase64Encode(str);
|
|
|
|
|
+ // #endif
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 自定义Base64编码实现
|
|
|
|
|
+ customBase64Encode(str) {
|
|
|
|
|
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
|
|
|
|
+ let output = '';
|
|
|
|
|
+ let chr1, chr2, chr3, enc1, enc2, enc3, enc4;
|
|
|
|
|
+ let i = 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 转换为UTF-8字节
|
|
|
|
|
+ str = this.utf8Encode(str);
|
|
|
|
|
+
|
|
|
|
|
+ while (i < str.length) {
|
|
|
|
|
+ chr1 = str.charCodeAt(i++);
|
|
|
|
|
+ chr2 = str.charCodeAt(i++);
|
|
|
|
|
+ chr3 = str.charCodeAt(i++);
|
|
|
|
|
+
|
|
|
|
|
+ enc1 = chr1 >> 2;
|
|
|
|
|
+ enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
|
|
|
|
|
+ enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
|
|
|
|
|
+ enc4 = chr3 & 63;
|
|
|
|
|
+
|
|
|
|
|
+ if (isNaN(chr2)) {
|
|
|
|
|
+ enc3 = enc4 = 64;
|
|
|
|
|
+ } else if (isNaN(chr3)) {
|
|
|
|
|
+ enc4 = 64;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ output = output + chars.charAt(enc1) + chars.charAt(enc2) + chars.charAt(enc3) + chars.charAt(enc4);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return output;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // UTF-8编码
|
|
|
|
|
+ utf8Encode(str) {
|
|
|
|
|
+ str = str.replace(/\r\n/g, '\n');
|
|
|
|
|
+ let utftext = '';
|
|
|
|
|
+
|
|
|
|
|
+ for (let n = 0; n < str.length; n++) {
|
|
|
|
|
+ const c = str.charCodeAt(n);
|
|
|
|
|
+
|
|
|
|
|
+ if (c < 128) {
|
|
|
|
|
+ utftext += String.fromCharCode(c);
|
|
|
|
|
+ } else if (c > 127 && c < 2048) {
|
|
|
|
|
+ utftext += String.fromCharCode((c >> 6) | 192);
|
|
|
|
|
+ utftext += String.fromCharCode((c & 63) | 128);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ utftext += String.fromCharCode((c >> 12) | 224);
|
|
|
|
|
+ utftext += String.fromCharCode(((c >> 6) & 63) | 128);
|
|
|
|
|
+ utftext += String.fromCharCode((c & 63) | 128);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return utftext;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // URL安全的Base64编码
|
|
|
|
|
+ urlSafeBase64Encode(str) {
|
|
|
|
|
+ // 先进行标准Base64编码
|
|
|
|
|
+ const base64 = this.base64Encode(str);
|
|
|
|
|
+
|
|
|
|
|
+ // 转换为URL安全格式:替换 +/= 字符
|
|
|
|
|
+ return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 创建分段协议(用于长参数)- 每次取两个键值对,将参数分成多个小块
|
|
|
|
|
+ createSegmentedProtocols(roomParams) {
|
|
|
|
|
+ const segments = [];
|
|
|
|
|
+ const MAX_SEGMENT_LENGTH = 64; // 每段最大长度
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 将参数分成多个小段
|
|
|
|
|
+ const paramEntries = Object.entries(roomParams);
|
|
|
|
|
+ const chunks = [];
|
|
|
|
|
+
|
|
|
|
|
+ // 每次取两个键值对,将参数分成多个小块
|
|
|
|
|
+ for (let i = 0; i < paramEntries.length; i += 2) {
|
|
|
|
|
+ const chunk = Object.fromEntries(paramEntries.slice(i, i + 2));
|
|
|
|
|
+ chunks.push(chunk);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ chunks.forEach((chunk, index) => {
|
|
|
|
|
+ const chunkStr = JSON.stringify(chunk);
|
|
|
|
|
+ const encoded = this.urlSafeBase64Encode(chunkStr);
|
|
|
|
|
+
|
|
|
|
|
+ if (encoded.length <= MAX_SEGMENT_LENGTH) {
|
|
|
|
|
+ segments.push(`p${index}-${encoded}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return segments;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('创建分段协议失败:', error);
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 验证协议字符串的合法性
|
|
|
|
|
+ validateProtocols(protocols) {
|
|
|
|
|
+ const MAX_SEGMENT_LENGTH = 64 + 30; // WebSocket 子协议名称按照 RFC 6455 规范,建议长度不超过 64 字符
|
|
|
|
|
+ if (!protocols || !Array.isArray(protocols)) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (const protocol of protocols) {
|
|
|
|
|
+ // 检查协议名称是否符合规范(RFC 6455)
|
|
|
|
|
+ // 只允许字母、数字、连字符、下划线、点
|
|
|
|
|
+ if (!/^[a-zA-Z0-9\-._]+$/.test(protocol)) {
|
|
|
|
|
+ console.error('协议包含非法字符:', protocol);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查长度限制
|
|
|
|
|
+ if (protocol.length > MAX_SEGMENT_LENGTH) {
|
|
|
|
|
+ console.error('协议长度超限:', protocol.length);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ console.log('protocal -- ', protocol, protocol.length)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return true;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
// 断开WebSocket连接
|
|
// 断开WebSocket连接
|
|
|
disconnectWebSocket() {
|
|
disconnectWebSocket() {
|
|
|
if (this.socket) {
|
|
if (this.socket) {
|
|
@@ -406,6 +579,7 @@ export default {
|
|
|
|
|
|
|
|
// 构建要发送的消息数据
|
|
// 构建要发送的消息数据
|
|
|
const messageData = {
|
|
const messageData = {
|
|
|
|
|
+ receiver: this.currentUser.customId,
|
|
|
message: messageContent,
|
|
message: messageContent,
|
|
|
username: this.currentUser.username,
|
|
username: this.currentUser.username,
|
|
|
shopId: this.currentUser.shopId, // 门店使用shopId,客户使用customId
|
|
shopId: this.currentUser.shopId, // 门店使用shopId,客户使用customId
|