feat: 重构首页对话
This commit is contained in:
172
utils/TypewriterManager.js
Normal file
172
utils/TypewriterManager.js
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* 打字机效果管理器
|
||||
* 负责管理打字机效果的启动、停止、重置等功能
|
||||
*/
|
||||
class TypewriterManager {
|
||||
constructor(options = {}) {
|
||||
// 配置选项
|
||||
this.options = {
|
||||
typingSpeed: 50, // 打字速度(毫秒)
|
||||
cursorText: '<text class="typing-cursor">|</text>', // 光标样式
|
||||
...options
|
||||
};
|
||||
|
||||
// 状态变量
|
||||
this.currentMessageContent = ""; // 当前消息的完整内容
|
||||
this.displayedContent = ""; // 已显示的内容
|
||||
this.typewriterTimer = null; // 打字机定时器
|
||||
this.isTyping = false; // 是否正在打字
|
||||
|
||||
// 回调函数
|
||||
this.onCharacterTyped = null; // 每个字符打字时的回调
|
||||
this.onTypingComplete = null; // 打字完成时的回调
|
||||
this.onContentUpdate = null; // 内容更新时的回调
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置回调函数
|
||||
* @param {Object} callbacks - 回调函数对象
|
||||
* @param {Function} callbacks.onCharacterTyped - 每个字符打字时的回调
|
||||
* @param {Function} callbacks.onTypingComplete - 打字完成时的回调
|
||||
* @param {Function} callbacks.onContentUpdate - 内容更新时的回调
|
||||
*/
|
||||
setCallbacks(callbacks) {
|
||||
this.onCharacterTyped = callbacks.onCharacterTyped || null;
|
||||
this.onTypingComplete = callbacks.onTypingComplete || null;
|
||||
this.onContentUpdate = callbacks.onContentUpdate || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加内容到当前消息
|
||||
* @param {string} content - 要添加的内容
|
||||
*/
|
||||
addContent(content) {
|
||||
if (typeof content !== 'string') {
|
||||
content = String(content);
|
||||
}
|
||||
this.currentMessageContent += content;
|
||||
|
||||
// 如果没有在打字,启动打字机效果
|
||||
if (!this.isTyping) {
|
||||
this.startTypewriter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动打字机效果
|
||||
*/
|
||||
startTypewriter() {
|
||||
if (this.isTyping) return;
|
||||
|
||||
this.isTyping = true;
|
||||
this.displayedContent = "";
|
||||
|
||||
this._typeNextChar();
|
||||
}
|
||||
|
||||
/**
|
||||
* 打字下一个字符(私有方法)
|
||||
*/
|
||||
_typeNextChar() {
|
||||
// 如果已显示内容长度小于完整内容长度,继续打字
|
||||
if (this.displayedContent.length < this.currentMessageContent.length) {
|
||||
this.displayedContent = this.currentMessageContent.substring(
|
||||
0,
|
||||
this.displayedContent.length + 1
|
||||
);
|
||||
|
||||
const displayContent = this.displayedContent + this.options.cursorText;
|
||||
|
||||
// 调用内容更新回调
|
||||
if (this.onContentUpdate) {
|
||||
this.onContentUpdate(displayContent);
|
||||
}
|
||||
|
||||
// 调用字符打字回调
|
||||
if (this.onCharacterTyped) {
|
||||
this.onCharacterTyped(this.displayedContent);
|
||||
}
|
||||
|
||||
// 继续下一个字符
|
||||
this.typewriterTimer = setTimeout(() => {
|
||||
this._typeNextChar();
|
||||
}, this.options.typingSpeed);
|
||||
} else {
|
||||
// 打字完成,移除光标
|
||||
if (this.onContentUpdate) {
|
||||
this.onContentUpdate(this.currentMessageContent);
|
||||
}
|
||||
|
||||
// 调用打字完成回调
|
||||
if (this.onTypingComplete) {
|
||||
this.onTypingComplete(this.currentMessageContent);
|
||||
}
|
||||
|
||||
this.stopTypewriter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止打字机效果
|
||||
*/
|
||||
stopTypewriter() {
|
||||
if (this.typewriterTimer) {
|
||||
clearTimeout(this.typewriterTimer);
|
||||
this.typewriterTimer = null;
|
||||
}
|
||||
this.isTyping = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置打字机状态
|
||||
*/
|
||||
reset() {
|
||||
this.currentMessageContent = "";
|
||||
this.displayedContent = "";
|
||||
this.stopTypewriter();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前状态
|
||||
* @returns {Object} 当前状态对象
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
isTyping: this.isTyping,
|
||||
currentContent: this.currentMessageContent,
|
||||
displayedContent: this.displayedContent,
|
||||
progress: this.currentMessageContent.length > 0
|
||||
? this.displayedContent.length / this.currentMessageContent.length
|
||||
: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即完成打字(跳过动画)
|
||||
*/
|
||||
completeImmediately() {
|
||||
this.stopTypewriter();
|
||||
this.displayedContent = this.currentMessageContent;
|
||||
|
||||
if (this.onContentUpdate) {
|
||||
this.onContentUpdate(this.currentMessageContent);
|
||||
}
|
||||
|
||||
if (this.onTypingComplete) {
|
||||
this.onTypingComplete(this.currentMessageContent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁打字机管理器
|
||||
*/
|
||||
destroy() {
|
||||
this.stopTypewriter();
|
||||
this.reset();
|
||||
this.onCharacterTyped = null;
|
||||
this.onTypingComplete = null;
|
||||
this.onContentUpdate = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default TypewriterManager;
|
||||
621
utils/WebSocketManager.js
Normal file
621
utils/WebSocketManager.js
Normal file
@@ -0,0 +1,621 @@
|
||||
/**
|
||||
* 简化的WebSocket管理器
|
||||
* 专门负责WebSocket连接和消息传输,不包含打字机逻辑
|
||||
*/
|
||||
|
||||
import { IdUtils, CallbackUtils, MessageUtils, TimerUtils } from "./index.js";
|
||||
|
||||
export class WebSocketManager {
|
||||
constructor(options = {}) {
|
||||
// 基础配置
|
||||
console.log("WebSocketManager构造函数接收到的options:", options);
|
||||
this.wsUrl = options.wsUrl || "";
|
||||
console.log("WebSocketManager设置的wsUrl:", this.wsUrl);
|
||||
this.protocols = options.protocols || [];
|
||||
this.reconnectInterval = options.reconnectInterval || 3000;
|
||||
this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
|
||||
this.heartbeatInterval = options.heartbeatInterval || 30000;
|
||||
|
||||
// WebSocket 实例
|
||||
this.ws = null;
|
||||
this.reconnectAttempts = 0;
|
||||
this.isConnecting = false;
|
||||
this.connectionState = false;
|
||||
|
||||
// 计时器
|
||||
this.heartbeatTimer = null;
|
||||
this.reconnectTimer = null;
|
||||
|
||||
// 回调函数
|
||||
this.callbacks = {
|
||||
onConnect: options.onConnect || (() => {}),
|
||||
onDisconnect: options.onDisconnect || (() => {}),
|
||||
onError: options.onError || (() => {}),
|
||||
onMessage: options.onMessage || (() => {}),
|
||||
getConversationId: options.getConversationId || (() => ""),
|
||||
getAgentId: options.getAgentId || (() => ""),
|
||||
};
|
||||
|
||||
// 消息队列
|
||||
this.messageQueue = [];
|
||||
this.isProcessingQueue = false;
|
||||
|
||||
// 性能统计
|
||||
this.stats = {
|
||||
messagesReceived: 0,
|
||||
messagesSent: 0,
|
||||
messagesDropped: 0,
|
||||
reconnectCount: 0,
|
||||
connectionStartTime: null,
|
||||
lastMessageTime: null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全调用回调函数
|
||||
*/
|
||||
_safeCallCallback(callbackName, ...args) {
|
||||
CallbackUtils.safeCall(this.callbacks, callbackName, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化连接
|
||||
*/
|
||||
async init(wsUrl) {
|
||||
if (wsUrl) {
|
||||
this.wsUrl = wsUrl;
|
||||
}
|
||||
|
||||
if (!this.wsUrl) {
|
||||
throw new Error("WebSocket URL is required");
|
||||
}
|
||||
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立WebSocket连接
|
||||
*/
|
||||
async connect() {
|
||||
if (this.isConnecting || this.connectionState) {
|
||||
console.log("WebSocket已在连接中或已连接,跳过连接");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("开始建立WebSocket连接:", this.wsUrl);
|
||||
this.isConnecting = true;
|
||||
|
||||
try {
|
||||
// 在小程序环境中使用 uni.connectSocket
|
||||
if (typeof uni !== "undefined" && uni.connectSocket) {
|
||||
try {
|
||||
this.ws = uni.connectSocket({
|
||||
url: this.wsUrl,
|
||||
protocols: this.protocols,
|
||||
success: () => {
|
||||
console.log("uni.connectSocket调用成功");
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error("uni.connectSocket调用失败:", error);
|
||||
this.isConnecting = false;
|
||||
this.connectionState = false;
|
||||
this._safeCallCallback("onError", {
|
||||
type: "CONNECTION_ERROR",
|
||||
message: "uni.connectSocket调用失败",
|
||||
originalError: error,
|
||||
});
|
||||
this.scheduleReconnect();
|
||||
return;
|
||||
},
|
||||
});
|
||||
|
||||
// 确保ws对象存在且有相应方法
|
||||
if (this.ws && typeof this.ws.onOpen === "function") {
|
||||
this.ws.onOpen(this.handleOpen.bind(this));
|
||||
this.ws.onMessage(this.handleMessage.bind(this));
|
||||
this.ws.onClose(this.handleClose.bind(this));
|
||||
this.ws.onError(this.handleError.bind(this));
|
||||
} else {
|
||||
console.error("uni.connectSocket返回的对象无效");
|
||||
this.isConnecting = false;
|
||||
this.connectionState = false;
|
||||
this._safeCallCallback("onError", {
|
||||
type: "CONNECTION_ERROR",
|
||||
message:
|
||||
"uni.connectSocket返回的SocketTask对象无效",
|
||||
originalError: null,
|
||||
});
|
||||
this.scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("创建uni.connectSocket时发生异常:", error);
|
||||
this.isConnecting = false;
|
||||
this.connectionState = false;
|
||||
this._safeCallCallback("onError", {
|
||||
type: "CONNECTION_ERROR",
|
||||
message: "创建WebSocket连接时发生异常",
|
||||
originalError: error,
|
||||
});
|
||||
this.scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 在其他环境中使用标准 WebSocket
|
||||
console.log("使用标准WebSocket创建连接");
|
||||
this.ws = new WebSocket(this.wsUrl, this.protocols);
|
||||
|
||||
this.ws.onopen = this.handleOpen.bind(this);
|
||||
this.ws.onmessage = this.handleMessage.bind(this);
|
||||
this.ws.onclose = this.handleClose.bind(this);
|
||||
this.ws.onerror = this.handleError.bind(this);
|
||||
}
|
||||
|
||||
console.log("WebSocket实例创建成功,等待连接...");
|
||||
} catch (error) {
|
||||
console.error("WebSocket连接创建失败:", error);
|
||||
this.isConnecting = false;
|
||||
this.connectionState = false;
|
||||
this._safeCallCallback("onError", {
|
||||
type: "CONNECTION_CREATE_ERROR",
|
||||
message: "WebSocket连接创建失败",
|
||||
originalError: error,
|
||||
});
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接成功处理
|
||||
*/
|
||||
handleOpen(event) {
|
||||
console.log("WebSocket连接已建立");
|
||||
this.isConnecting = false;
|
||||
this.connectionState = true;
|
||||
this.reconnectAttempts = 0;
|
||||
this.stats.connectionStartTime = Date.now();
|
||||
|
||||
this.startHeartbeat();
|
||||
|
||||
// 安全调用 onConnect 回调
|
||||
this._safeCallCallback("onConnect", event);
|
||||
|
||||
// 处理队列中的消息
|
||||
this._processMessageQueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理消息队列
|
||||
*/
|
||||
_processMessageQueue() {
|
||||
while (this.messageQueue.length > 0) {
|
||||
const queuedMessage = this.messageQueue.shift();
|
||||
|
||||
// 检查重试次数
|
||||
if (queuedMessage.retryCount >= 3) {
|
||||
console.warn("消息重试次数超限,丢弃消息:", queuedMessage);
|
||||
this.stats.messagesDropped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
queuedMessage.retryCount = (queuedMessage.retryCount || 0) + 1;
|
||||
this.sendMessage(queuedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息接收处理
|
||||
*/
|
||||
handleMessage(event) {
|
||||
try {
|
||||
// 在小程序环境中,消息数据可能在 event.data 中
|
||||
const messageData = event.data || event;
|
||||
|
||||
// 处理心跳响应和其他非JSON消息 - 在JSON解析前检查
|
||||
if (typeof messageData === "string") {
|
||||
// 处理心跳响应
|
||||
if (MessageUtils.isPongMessage(messageData)) {
|
||||
console.log("收到心跳响应:", messageData);
|
||||
return;
|
||||
}
|
||||
|
||||
// 尝试解析JSON,如果失败则忽略该消息
|
||||
const data = MessageUtils.safeParseJSON(messageData);
|
||||
if (data) {
|
||||
this.stats.messagesReceived++;
|
||||
this.stats.lastMessageTime = Date.now();
|
||||
console.log("收到WebSocket消息:", data);
|
||||
|
||||
// 心跳响应 - 兼容对象格式
|
||||
if (MessageUtils.isPongMessage(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 直接传递消息给回调处理
|
||||
this._safeCallCallback("onMessage", data);
|
||||
} else {
|
||||
console.warn("收到非JSON格式消息,已忽略:", messageData);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 处理对象格式的消息
|
||||
const data = messageData;
|
||||
this.stats.messagesReceived++;
|
||||
this.stats.lastMessageTime = Date.now();
|
||||
console.log("收到WebSocket消息:", data);
|
||||
|
||||
// 心跳响应 - 兼容对象格式
|
||||
if (MessageUtils.isPongMessage(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 直接传递消息给回调处理
|
||||
this._safeCallCallback("onMessage", data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("消息处理错误:", error, "原始消息:", event);
|
||||
// 不抛出错误,避免影响其他消息处理
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接关闭处理
|
||||
*/
|
||||
handleClose(event) {
|
||||
console.log("WebSocket连接已关闭:", event.code, event.reason);
|
||||
this.connectionState = false;
|
||||
this.isConnecting = false;
|
||||
|
||||
this.stopHeartbeat();
|
||||
|
||||
// 安全调用 onDisconnect 回调
|
||||
this._safeCallCallback("onDisconnect", event);
|
||||
|
||||
// 非正常关闭时尝试重连
|
||||
if (
|
||||
event.code !== 1000 &&
|
||||
this.reconnectAttempts < this.maxReconnectAttempts
|
||||
) {
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误处理
|
||||
*/
|
||||
handleError(error) {
|
||||
const errorDetails = {
|
||||
error,
|
||||
wsUrl: this.wsUrl,
|
||||
readyState: this.ws ? this.ws.readyState : "WebSocket实例不存在",
|
||||
isConnecting: this.isConnecting,
|
||||
connectionState: this.connectionState,
|
||||
reconnectAttempts: this.reconnectAttempts,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
console.error("WebSocket错误详情:", errorDetails);
|
||||
|
||||
// 重置连接状态
|
||||
this.connectionState = false;
|
||||
this.isConnecting = false;
|
||||
|
||||
// 构造结构化错误信息
|
||||
const structuredError = {
|
||||
type: "WEBSOCKET_ERROR",
|
||||
message: error?.message || "未知WebSocket错误",
|
||||
originalError: error,
|
||||
details: errorDetails,
|
||||
};
|
||||
|
||||
// 安全调用 onError 回调
|
||||
this._safeCallCallback("onError", structuredError);
|
||||
|
||||
// 如果不是正在重连中,尝试重连
|
||||
if (
|
||||
!this.reconnectTimer &&
|
||||
this.reconnectAttempts < this.maxReconnectAttempts
|
||||
) {
|
||||
console.log("错误后尝试重连");
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
sendMessage(message) {
|
||||
const messageData = {
|
||||
...message,
|
||||
timestamp: Date.now(),
|
||||
retryCount: 0,
|
||||
};
|
||||
|
||||
if (this.connectionState) {
|
||||
try {
|
||||
const messageStr = JSON.stringify(messageData);
|
||||
|
||||
// 在小程序环境中使用不同的发送方法
|
||||
if (typeof uni !== "undefined" && this.ws && this.ws.send) {
|
||||
// uni-app 小程序环境
|
||||
this.ws.send({
|
||||
data: messageStr,
|
||||
success: () => {
|
||||
this.stats.messagesSent++;
|
||||
console.log("消息发送成功:", messageData);
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error("发送消息失败:", error);
|
||||
this._safeCallCallback("onError", error);
|
||||
|
||||
// 发送失败时加入重试队列
|
||||
this.messageQueue.push(messageData);
|
||||
},
|
||||
});
|
||||
} else if (this.ws && this.ws.send) {
|
||||
// 标准 WebSocket 环境
|
||||
this.ws.send(messageStr);
|
||||
this.stats.messagesSent++;
|
||||
console.log("消息发送成功:", messageData);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("发送消息失败:", error);
|
||||
this._safeCallCallback("onError", error);
|
||||
|
||||
// 发送失败时加入重试队列
|
||||
this.messageQueue.push(messageData);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// 连接未建立时加入队列
|
||||
this.messageQueue.push(messageData);
|
||||
this.connect(); // 尝试连接
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始心跳
|
||||
*/
|
||||
startHeartbeat() {
|
||||
this.stopHeartbeat();
|
||||
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
if (this.connectionState && this.ws) {
|
||||
try {
|
||||
// 使用标准消息格式发送心跳 (messageType=3)
|
||||
const heartbeatMessage = {
|
||||
conversationId: this.callbacks.getConversationId
|
||||
? this.callbacks.getConversationId()
|
||||
: "",
|
||||
agentId: this.callbacks.getAgentId
|
||||
? this.callbacks.getAgentId()
|
||||
: "",
|
||||
messageType: 3, // 心跳检测
|
||||
messageContent: "heartbeat",
|
||||
messageId: IdUtils.generateMessageId(),
|
||||
};
|
||||
|
||||
this.sendMessage(heartbeatMessage);
|
||||
console.log("心跳消息已发送:", heartbeatMessage);
|
||||
} catch (error) {
|
||||
console.error("发送心跳失败:", error);
|
||||
this.handleError(error);
|
||||
}
|
||||
}
|
||||
}, this.heartbeatInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止心跳
|
||||
*/
|
||||
stopHeartbeat() {
|
||||
TimerUtils.clearTimer(this.heartbeatTimer, "interval");
|
||||
this.heartbeatTimer = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安排重连
|
||||
*/
|
||||
scheduleReconnect() {
|
||||
// 清理之前的重连定时器
|
||||
if (this.reconnectTimer) {
|
||||
TimerUtils.clearTimer(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
console.error(
|
||||
`达到最大重连次数(${this.maxReconnectAttempts}),停止重连`
|
||||
);
|
||||
this.connectionState = false;
|
||||
this.isConnecting = false;
|
||||
|
||||
// 使用更温和的错误处理,避免抛出异常
|
||||
const errorInfo = {
|
||||
type: "CONNECTION_FAILED",
|
||||
message: `连接失败,已达到最大重连次数(${this.maxReconnectAttempts})`,
|
||||
attempts: this.reconnectAttempts,
|
||||
maxAttempts: this.maxReconnectAttempts,
|
||||
};
|
||||
|
||||
console.warn("WebSocket连接最终失败:", errorInfo);
|
||||
this._safeCallCallback("onError", errorInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
this.reconnectAttempts++;
|
||||
this.stats.reconnectCount++;
|
||||
|
||||
// 使用指数退避策略
|
||||
const backoffDelay = Math.min(
|
||||
this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1),
|
||||
30000 // 最大延迟30秒
|
||||
);
|
||||
|
||||
console.log(
|
||||
`${backoffDelay}ms后进行第${this.reconnectAttempts}/${this.maxReconnectAttempts}次重连`
|
||||
);
|
||||
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectTimer = null;
|
||||
this.connect().catch((error) => {
|
||||
console.error("重连过程中发生错误:", error);
|
||||
// 继续尝试重连
|
||||
this.scheduleReconnect();
|
||||
});
|
||||
}, backoffDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置连接状态
|
||||
*/
|
||||
resetConnectionState() {
|
||||
this.connectionState = false;
|
||||
this.isConnecting = false;
|
||||
this.reconnectAttempts = 0;
|
||||
|
||||
// 清理定时器
|
||||
this.stopHeartbeat();
|
||||
if (this.reconnectTimer) {
|
||||
TimerUtils.clearTimer(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
|
||||
console.log("连接状态已重置");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已连接
|
||||
*/
|
||||
isConnected() {
|
||||
return (
|
||||
this.connectionState &&
|
||||
this.ws &&
|
||||
(this.ws.readyState === 1 ||
|
||||
(typeof WebSocket !== "undefined" &&
|
||||
this.ws.readyState === WebSocket.OPEN))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动重连
|
||||
*/
|
||||
async reconnect() {
|
||||
console.log("手动触发重连");
|
||||
|
||||
// 先关闭现有连接
|
||||
this.close();
|
||||
|
||||
// 重置连接状态
|
||||
this.resetConnectionState();
|
||||
|
||||
// 重新连接
|
||||
try {
|
||||
await this.connect();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("手动重连失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接状态
|
||||
*/
|
||||
getConnectionState() {
|
||||
return {
|
||||
isConnected: this.isConnected(),
|
||||
readyState: this.ws ? this.ws.readyState : -1,
|
||||
reconnectAttempts: this.reconnectAttempts,
|
||||
maxReconnectAttempts: this.maxReconnectAttempts,
|
||||
isConnecting: this.isConnecting,
|
||||
hasReconnectTimer: !!this.reconnectTimer,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*/
|
||||
getStats() {
|
||||
const now = Date.now();
|
||||
const connectionTime = this.stats.connectionStartTime
|
||||
? now - this.stats.connectionStartTime
|
||||
: 0;
|
||||
|
||||
return {
|
||||
...this.stats,
|
||||
connectionTime,
|
||||
queueLength: this.messageQueue.length,
|
||||
isConnected: this.connectionState,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置统计信息
|
||||
*/
|
||||
resetStats() {
|
||||
this.stats = {
|
||||
messagesReceived: 0,
|
||||
messagesSent: 0,
|
||||
messagesDropped: 0,
|
||||
reconnectCount: 0,
|
||||
connectionStartTime: Date.now(),
|
||||
lastMessageTime: null,
|
||||
};
|
||||
console.log("统计信息已重置");
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭连接
|
||||
*/
|
||||
close() {
|
||||
this.stopHeartbeat();
|
||||
|
||||
// 清理重连定时器
|
||||
TimerUtils.clearTimer(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
|
||||
if (this.ws) {
|
||||
// 在小程序环境中使用不同的关闭方法
|
||||
if (typeof uni !== "undefined" && this.ws.close) {
|
||||
// uni-app 小程序环境
|
||||
this.ws.close({
|
||||
code: 1000,
|
||||
reason: "正常关闭",
|
||||
success: () => {
|
||||
console.log("WebSocket连接已关闭");
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error("关闭WebSocket连接失败:", error);
|
||||
},
|
||||
});
|
||||
} else if (this.ws.close) {
|
||||
// 标准 WebSocket 环境
|
||||
this.ws.close(1000, "正常关闭");
|
||||
}
|
||||
this.ws = null;
|
||||
}
|
||||
|
||||
this.connectionState = false;
|
||||
this.isConnecting = false;
|
||||
this.messageQueue = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁实例
|
||||
*/
|
||||
destroy() {
|
||||
// 输出最终统计信息
|
||||
console.log("WebSocketManager销毁前统计:", this.getStats());
|
||||
|
||||
this.close();
|
||||
this.callbacks = {};
|
||||
|
||||
console.log("WebSocketManager实例已销毁");
|
||||
}
|
||||
}
|
||||
|
||||
export default WebSocketManager;
|
||||
429
utils/index.js
Normal file
429
utils/index.js
Normal file
@@ -0,0 +1,429 @@
|
||||
/**
|
||||
* 工具函数集合
|
||||
* 包含打字机效果、ID生成、回调安全调用等通用工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 打字机工具类
|
||||
* 提供打字机效果相关的工具函数
|
||||
*/
|
||||
export class TypewriterUtils {
|
||||
/**
|
||||
* 计算动态打字速度
|
||||
* @param {string} char - 当前字符
|
||||
* @param {number} baseSpeed - 基础速度(ms)
|
||||
* @returns {number} 动态调整后的速度
|
||||
*/
|
||||
static calculateDynamicSpeed(char, baseSpeed = 100) {
|
||||
const punctuationMarks = [
|
||||
"。",
|
||||
"!",
|
||||
"?",
|
||||
".",
|
||||
"!",
|
||||
"?",
|
||||
",",
|
||||
",",
|
||||
";",
|
||||
";",
|
||||
":",
|
||||
":",
|
||||
];
|
||||
const isSpace = char === " ";
|
||||
const hasPunctuation = punctuationMarks.includes(char);
|
||||
|
||||
if (hasPunctuation) {
|
||||
// 标点符号后停顿更久,模拟思考时间
|
||||
return baseSpeed * 2.5;
|
||||
} else if (isSpace) {
|
||||
// 空格稍快一些
|
||||
return baseSpeed * 0.6;
|
||||
} else {
|
||||
// 普通字符添加一些随机性,模拟真实打字的不均匀性
|
||||
const randomFactor = 0.7 + Math.random() * 0.6; // 0.7-1.3倍速度
|
||||
return baseSpeed * randomFactor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查字符是否为标点符号
|
||||
* @param {string} char - 要检查的字符
|
||||
* @returns {boolean} 是否为标点符号
|
||||
*/
|
||||
static isPunctuation(char) {
|
||||
const punctuationMarks = [
|
||||
"。",
|
||||
"!",
|
||||
"?",
|
||||
".",
|
||||
"!",
|
||||
"?",
|
||||
",",
|
||||
",",
|
||||
";",
|
||||
";",
|
||||
":",
|
||||
":",
|
||||
];
|
||||
return punctuationMarks.includes(char);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查字符是否为空格
|
||||
* @param {string} char - 要检查的字符
|
||||
* @returns {boolean} 是否为空格
|
||||
*/
|
||||
static isSpace(char) {
|
||||
return char === " ";
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成加载动画文本
|
||||
* @param {number} dotCount - 点的数量(1-3)
|
||||
* @param {string} baseText - 基础文本
|
||||
* @returns {string} 加载动画文本
|
||||
*/
|
||||
static generateLoadingText(dotCount = 1, baseText = "加载中") {
|
||||
const normalizedDotCount = ((dotCount - 1) % 3) + 1;
|
||||
return baseText + ".".repeat(normalizedDotCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动加载动画
|
||||
*/
|
||||
static startLoadingAnimation(
|
||||
onProgress,
|
||||
options = {},
|
||||
onTimerCreated = null
|
||||
) {
|
||||
const { speed = 500, text = "加载中" } = options;
|
||||
let dotCount = 1;
|
||||
|
||||
const timerId = setInterval(() => {
|
||||
dotCount = (dotCount % 3) + 1;
|
||||
const loadingText = text + ".".repeat(dotCount);
|
||||
onProgress(loadingText);
|
||||
}, speed);
|
||||
|
||||
if (onTimerCreated) {
|
||||
onTimerCreated(timerId);
|
||||
}
|
||||
|
||||
return timerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动打字机效果
|
||||
* @param {string} text - 要显示的文本
|
||||
* @param {Element} targetElement - 目标元素(可选)
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {Function} onTimerCreated - 定时器创建回调(可选)
|
||||
*/
|
||||
static startTypewriter(
|
||||
text,
|
||||
targetElement = null,
|
||||
options = {},
|
||||
onTimerCreated = null
|
||||
) {
|
||||
const {
|
||||
speed = 100,
|
||||
onProgress = () => {},
|
||||
onComplete = () => {},
|
||||
} = options;
|
||||
|
||||
let index = 0;
|
||||
|
||||
const typeNextChar = () => {
|
||||
if (index < text.length) {
|
||||
const char = text[index];
|
||||
const currentText = text.substring(0, index + 1);
|
||||
|
||||
// 调用进度回调
|
||||
onProgress(currentText, index);
|
||||
|
||||
// 如果有目标元素,更新其内容
|
||||
if (targetElement) {
|
||||
targetElement.textContent = currentText;
|
||||
}
|
||||
|
||||
index++;
|
||||
|
||||
// 计算下一个字符的延时
|
||||
const delay = TypewriterUtils.calculateDynamicSpeed(
|
||||
char,
|
||||
speed
|
||||
);
|
||||
|
||||
const timerId = setTimeout(typeNextChar, delay);
|
||||
|
||||
if (onTimerCreated && index === 1) {
|
||||
onTimerCreated(timerId);
|
||||
}
|
||||
} else {
|
||||
// 打字完成
|
||||
onComplete(text);
|
||||
}
|
||||
};
|
||||
|
||||
// 开始打字
|
||||
const initialTimerId = setTimeout(typeNextChar, 0);
|
||||
|
||||
if (onTimerCreated) {
|
||||
onTimerCreated(initialTimerId);
|
||||
}
|
||||
|
||||
return initialTimerId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ID生成工具类
|
||||
* 提供各种ID生成功能
|
||||
*/
|
||||
export class IdUtils {
|
||||
/**
|
||||
* 生成消息ID
|
||||
* @returns {string} 消息ID
|
||||
*/
|
||||
static generateMessageId() {
|
||||
return "mid" + new Date().getTime();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回调函数安全调用工具类
|
||||
* 提供安全的回调函数调用机制
|
||||
*/
|
||||
export class CallbackUtils {
|
||||
/**
|
||||
* 安全调用回调函数
|
||||
* @param {Object} callbacks - 回调函数对象
|
||||
* @param {string} callbackName - 回调函数名称
|
||||
* @param {...any} args - 传递给回调函数的参数
|
||||
*/
|
||||
static safeCall(callbacks, callbackName, ...args) {
|
||||
if (callbacks && typeof callbacks[callbackName] === "function") {
|
||||
try {
|
||||
callbacks[callbackName](...args);
|
||||
} catch (error) {
|
||||
console.error(`回调函数 ${callbackName} 执行出错:`, error);
|
||||
}
|
||||
} else {
|
||||
console.warn(`回调函数 ${callbackName} 不可用`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量安全调用回调函数
|
||||
* @param {Object} callbacks - 回调函数对象
|
||||
* @param {Array} callbackConfigs - 回调配置数组 [{name, args}, ...]
|
||||
*/
|
||||
static safeBatchCall(callbacks, callbackConfigs) {
|
||||
callbackConfigs.forEach((config) => {
|
||||
const { name, args = [] } = config;
|
||||
this.safeCall(callbacks, name, ...args);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息处理工具类
|
||||
* 提供消息相关的工具函数
|
||||
*/
|
||||
export class MessageUtils {
|
||||
/**
|
||||
* 验证消息格式
|
||||
* @param {any} message - 消息对象
|
||||
* @returns {boolean} 是否为有效消息格式
|
||||
*/
|
||||
static validateMessage(message) {
|
||||
return message && typeof message === "object" && message.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化消息
|
||||
* @param {string} type - 消息类型
|
||||
* @param {any} content - 消息内容
|
||||
* @param {Object} options - 额外选项
|
||||
* @returns {Object} 格式化后的消息对象
|
||||
*/
|
||||
static formatMessage(type, content, options = {}) {
|
||||
return {
|
||||
type,
|
||||
content,
|
||||
timestamp: Date.now(),
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为完整消息
|
||||
* @param {any} message - 消息对象
|
||||
* @returns {boolean} 是否为完整消息
|
||||
*/
|
||||
static isCompleteMessage(message) {
|
||||
return message && message.isComplete === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查消息是否为心跳响应
|
||||
* @param {any} messageData - 消息数据
|
||||
* @returns {boolean} 是否为心跳响应
|
||||
*/
|
||||
static isPongMessage(messageData) {
|
||||
if (typeof messageData === "string") {
|
||||
return (
|
||||
messageData === "pong" ||
|
||||
messageData.toLowerCase().includes("pong")
|
||||
);
|
||||
}
|
||||
if (typeof messageData === "object" && messageData !== null) {
|
||||
return messageData.type === "pong";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全解析JSON消息
|
||||
* @param {string} messageStr - 消息字符串
|
||||
* @returns {Object|null} 解析后的对象或null
|
||||
*/
|
||||
static safeParseJSON(messageStr) {
|
||||
try {
|
||||
return JSON.parse(messageStr);
|
||||
} catch (error) {
|
||||
console.warn("JSON解析失败:", messageStr);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建打字机消息对象
|
||||
* @param {string} content - 消息内容
|
||||
* @param {boolean} isComplete - 是否完成
|
||||
* @param {string} type - 消息类型
|
||||
* @returns {Object} 消息对象
|
||||
*/
|
||||
static createTypewriterMessage(
|
||||
content,
|
||||
isComplete = false,
|
||||
type = "typewriter"
|
||||
) {
|
||||
return {
|
||||
type,
|
||||
content,
|
||||
isComplete,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建加载消息对象
|
||||
* @param {string} content - 加载内容
|
||||
* @returns {Object} 加载消息对象
|
||||
*/
|
||||
static createLoadingMessage(content = "加载中...") {
|
||||
return {
|
||||
type: "loading",
|
||||
content,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建错误消息对象
|
||||
* @param {string} error - 错误信息
|
||||
* @returns {Object} 错误消息对象
|
||||
*/
|
||||
static createErrorMessage(error) {
|
||||
return {
|
||||
type: "error",
|
||||
content: error.message || error || "未知错误",
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时器管理工具类
|
||||
* 提供定时器的统一管理
|
||||
*/
|
||||
export class TimerUtils {
|
||||
/**
|
||||
* 安全清除定时器
|
||||
* @param {number|null} timerId - 定时器ID
|
||||
* @param {string} type - 定时器类型 'timeout' | 'interval'
|
||||
* @returns {null} 返回null便于重置变量
|
||||
*/
|
||||
static safeClear(timerId, type = "timeout") {
|
||||
if (timerId) {
|
||||
if (type === "interval") {
|
||||
clearInterval(timerId);
|
||||
} else {
|
||||
clearTimeout(timerId);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除定时器(别名方法)
|
||||
* @param {number|null} timerId - 定时器ID
|
||||
* @param {string} type - 定时器类型 'timeout' | 'interval'
|
||||
* @returns {null} 返回null便于重置变量
|
||||
*/
|
||||
static clearTimer(timerId, type = "timeout") {
|
||||
return this.safeClear(timerId, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建可取消的延时执行
|
||||
* @param {Function} callback - 回调函数
|
||||
* @param {number} delay - 延时时间
|
||||
* @returns {Object} 包含cancel方法的对象
|
||||
*/
|
||||
static createCancelableTimeout(callback, delay) {
|
||||
let timerId = setTimeout(callback, delay);
|
||||
return {
|
||||
cancel() {
|
||||
if (timerId) {
|
||||
clearTimeout(timerId);
|
||||
timerId = null;
|
||||
}
|
||||
},
|
||||
isActive() {
|
||||
return timerId !== null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建可取消的定时执行
|
||||
* @param {Function} callback - 回调函数
|
||||
* @param {number} interval - 间隔时间
|
||||
* @returns {Object} 包含cancel方法的对象
|
||||
*/
|
||||
static createCancelableInterval(callback, interval) {
|
||||
let timerId = setInterval(callback, interval);
|
||||
return {
|
||||
cancel() {
|
||||
if (timerId) {
|
||||
clearInterval(timerId);
|
||||
timerId = null;
|
||||
}
|
||||
},
|
||||
isActive() {
|
||||
return timerId !== null;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 默认导出所有工具类
|
||||
export default {
|
||||
TypewriterUtils,
|
||||
IdUtils,
|
||||
CallbackUtils,
|
||||
MessageUtils,
|
||||
TimerUtils,
|
||||
};
|
||||
Reference in New Issue
Block a user