ykt-team-wxapp/utils/chat-utils.js
2026-01-28 13:38:05 +08:00

798 lines
22 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 聊天相关工具函数
*/
// 通用消息提示
export const showMessage = (title, icon = 'none') => {
uni.showToast({
title,
icon,
});
};
// 检查问诊状态
export const checkConsultationStatus = (waitingForDoctor, consultationEnded) => {
if (waitingForDoctor) {
showMessage("等待医生接诊中,无法发送消息");
return false;
}
if (consultationEnded) {
showMessage("问诊已结束,无法发送消息");
return false;
}
return true;
};
// 检查IM连接状态
export const checkIMConnection = (timChatManager) => {
if (!timChatManager.tim || !timChatManager.isLoggedIn) {
// showMessage("IM连接异常请重新进入");
return false;
}
return true;
};
// 发送消息前的通用验证
export const validateBeforeSend = (waitingForDoctor, consultationEnded, timChatManager) => {
if (!checkConsultationStatus(waitingForDoctor, consultationEnded)) {
return false;
}
if (!checkIMConnection(timChatManager)) {
return false;
}
return true;
};
// 获取语音文件URL
export const getVoiceUrl = (message) => {
let voiceUrl = '';
if (message.payload && message.payload.url) {
voiceUrl = message.payload.url;
} else if (message.payload && message.payload.file) {
voiceUrl = message.payload.file;
} else if (message.payload && message.payload.tempFilePath) {
voiceUrl = message.payload.tempFilePath;
} else if (message.payload && message.payload.filePath) {
voiceUrl = message.payload.filePath;
}
return voiceUrl;
};
// 验证语音URL格式
export const validateVoiceUrl = (voiceUrl) => {
if (!voiceUrl) {
console.error('语音文件URL不存在');
showMessage('语音文件不存在');
return false;
}
if (!voiceUrl.startsWith('http') && !voiceUrl.startsWith('wxfile://') && !voiceUrl.startsWith('/')) {
console.error('语音文件URL格式不正确:', voiceUrl);
showMessage('语音文件格式错误');
return false;
}
return true;
};
// 创建音频上下文
export const createAudioContext = (voiceUrl) => {
const audioContext = uni.createInnerAudioContext();
audioContext.src = voiceUrl;
audioContext.onPlay(() => {
console.log('语音开始播放');
});
audioContext.onEnded(() => {
console.log('语音播放结束');
});
audioContext.onError((err) => {
console.error('语音播放失败:', err);
console.error('错误详情:', {
errMsg: err.errMsg,
errno: err.errno,
src: voiceUrl
});
showMessage('语音播放失败');
});
return audioContext;
};
// ==================== 时间相关工具方法 ====================
/**
* 验证时间戳格式
* @param {number|string} timestamp - 时间戳
* @returns {boolean} 是否为有效时间戳
*/
export const validateTimestamp = (timestamp) => {
if (!timestamp) return false;
const num = Number(timestamp);
if (isNaN(num)) return false;
// 检查是否为有效的时间戳范围1970年到2100年
const minTimestamp = 0;
const maxTimestamp = 4102444800000; // 2100年1月1日
return num >= minTimestamp && num <= maxTimestamp;
};
/**
* 格式化时间 - 今天/昨天显示文字,其他显示日期 + 空格 + 24小时制时间
* @param {number|string} timestamp - 时间戳
* @returns {string} 格式化后的时间字符串
*/
export const formatTime = (timestamp) => {
// 验证时间戳
if (!validateTimestamp(timestamp)) {
return "未知时间";
}
// 确保时间戳是毫秒级
let timeInMs = timestamp;
if (timestamp < 1000000000000) {
// 如果时间戳小于这个值,可能是秒级时间戳
timeInMs = timestamp * 1000;
}
const date = new Date(timeInMs);
const now = new Date();
// 验证日期是否有效
if (isNaN(date.getTime())) {
return "未知时间";
}
// 格式化时间HH:MM (24小时制)
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
const timeStr = `${hours}:${minutes}`;
// 检查是否是今天
if (date.toDateString() === now.toDateString()) {
return `${timeStr}`;
}
// 检查是否是昨天
const yesterday = new Date(now);
yesterday.setDate(yesterday.getDate() - 1);
if (date.toDateString() === yesterday.toDateString()) {
return `昨天 ${timeStr}`;
}
// 其他日期显示完整日期
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const dateStr = `${month}/${day}`;
return `${dateStr} ${timeStr}`;
};
/**
* 计算时间差
* @param {number|string} startTime - 开始时间戳
* @param {number|string} endTime - 结束时间戳
* @returns {object} 包含天、小时、分钟、秒的时间差对象
*/
export const calculateTimeDiff = (startTime, endTime) => {
if (!validateTimestamp(startTime) || !validateTimestamp(endTime)) {
return { days: 0, hours: 0, minutes: 0, seconds: 0 };
}
let startMs = startTime;
let endMs = endTime;
if (startTime < 1000000000000) startMs = startTime * 1000;
if (endTime < 1000000000000) endMs = endTime * 1000;
const diffMs = Math.abs(endMs - startMs);
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
return { days, hours, minutes, seconds };
};
/**
* 格式化倒计时
* @param {number|string} endTime - 结束时间戳
* @param {number|string} currentTime - 当前时间戳(可选,默认使用当前时间)
* @returns {string} 格式化后的倒计时字符串
*/
export const formatCountdown = (endTime, currentTime = Date.now()) => {
const diff = calculateTimeDiff(currentTime, endTime);
if (diff.days > 0) {
return `${diff.days}${diff.hours}${diff.minutes}`;
} else if (diff.hours > 0) {
return `${diff.hours}${diff.minutes}${diff.seconds}`;
} else if (diff.minutes > 0) {
return `${diff.minutes}${diff.seconds}`;
} else {
return `${diff.seconds}`;
}
};
// ==================== 媒体选择相关工具方法 ====================
/**
* 检查并请求相册权限
* @returns {Promise<boolean>} 是否有权限
*/
const checkAlbumPermission = () => {
return new Promise((resolve) => {
uni.getSetting({
success: (res) => {
const authStatus = res.authSetting['scope.album'];
if (authStatus === undefined) {
// 未授权过,会自动弹出授权窗口
resolve(true);
} else if (authStatus === false) {
// 已拒绝授权,需要引导用户手动开启
uni.showModal({
title: '需要相册权限',
content: '请在设置中开启相册权限,以便选择图片',
confirmText: '去设置',
success: (modalRes) => {
if (modalRes.confirm) {
uni.openSetting({
success: (settingRes) => {
if (settingRes.authSetting['scope.album']) {
resolve(true);
} else {
resolve(false);
}
},
fail: () => {
resolve(false);
}
});
} else {
resolve(false);
}
},
fail: () => {
resolve(false);
}
});
} else {
// 已授权
resolve(true);
}
},
fail: () => {
// 获取设置失败,尝试直接调用
resolve(true);
}
});
});
};
/**
* 选择媒体文件
* @param {object} options - 选择选项
* @param {function} onSuccess - 成功回调
* @param {function} onFail - 失败回调
*/
export const chooseMedia = async (options, onSuccess, onFail) => {
// 如果需要从相册选择,先检查权限
const sourceType = options.sourceType || ['album', 'camera'];
if (sourceType.includes('album')) {
const hasPermission = await checkAlbumPermission();
if (!hasPermission) {
console.log('用户未授予相册权限');
if (onFail) {
onFail({ errMsg: '未授权相册权限' });
}
return;
}
}
uni.chooseMedia({
count: options.count || 1,
mediaType: options.mediaType || ['image'],
sizeType: options.sizeType || ['original', 'compressed'],
sourceType: sourceType,
success: function (res) {
console.log('选择媒体成功:', res);
if (onSuccess) onSuccess(res);
},
fail: function (err) {
// 用户取消选择
if (err.errMsg.includes('cancel')) {
console.log('用户取消选择');
return;
}
// 权限相关错误
if (err.errMsg.includes('permission') || err.errMsg.includes('auth') || err.errMsg.includes('拒绝')) {
console.error('相册权限被拒绝:', err);
uni.showModal({
title: '需要相册权限',
content: '请在设置中开启相册权限后重试',
confirmText: '去设置',
success: (modalRes) => {
if (modalRes.confirm) {
uni.openSetting();
}
}
});
if (onFail) {
onFail(err);
}
return;
}
// 其他错误
console.error('选择媒体失败:', err);
if (onFail) {
onFail(err);
} else {
showMessage('选择图片失败,请重试');
}
}
});
};
/**
* 选择图片
* @param {function} onSuccess - 成功回调
* @param {function} onFail - 失败回调
*/
export const chooseImage = (onSuccess, onFail) => {
chooseMedia({
count: 1,
mediaType: ['image'],
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera']
}, onSuccess, onFail);
};
/**
* 拍照
* @param {function} onSuccess - 成功回调
* @param {function} onFail - 失败回调
*/
export const takePhoto = (onSuccess, onFail) => {
chooseMedia({
count: 1,
mediaType: ['image'],
sizeType: ['original', 'compressed'],
sourceType: ['camera']
}, onSuccess, onFail);
};
// ==================== 录音相关工具方法 ====================
/**
* 初始化录音管理器
* @param {object} options - 录音选项
* @param {function} onStop - 录音结束回调
* @param {function} onError - 录音错误回调
* @returns {object} 录音管理器实例
*/
export const initRecorderManager = (options = {}, onStop, onError) => {
const recorderManager = wx.getRecorderManager();
// 监听录音结束事件
recorderManager.onStop((res) => {
console.log('录音成功,结果:', res);
if (onStop) onStop(res);
});
// 监听录音错误事件
recorderManager.onError((err) => {
console.error('录音失败:', err);
if (onError) {
onError(err);
} else {
showMessage("录音失败");
}
});
return recorderManager;
};
/**
* 开始录音
* @param {object} recorderManager - 录音管理器
* @param {object} options - 录音参数
*/
export const startRecord = (recorderManager, options = {}) => {
if (!recorderManager) {
console.error('录音管理器未初始化');
return;
}
const recordOptions = {
duration: 60000, // 录音的时长,单位 ms最大值 60000010 分钟)
sampleRate: 44100, // 采样率
numberOfChannels: 1, // 录音通道数
encodeBitRate: 192000, // 编码码率
format: 'aac', // 音频格式
...options
};
recorderManager.start(recordOptions);
};
/**
* 停止录音
* @param {object} recorderManager - 录音管理器
*/
export const stopRecord = (recorderManager) => {
if (!recorderManager) {
console.error('录音管理器未初始化');
return;
}
recorderManager.stop();
};
// ==================== 消息发送相关工具方法 ====================
/**
* 创建自定义消息
* @param {string} messageType - 消息类型
* @param {object} data - 消息数据
* @param {function} formatTime - 时间格式化函数
* @returns {object} 自定义消息对象
*/
export const createCustomMessage = (messageType, data, formatTime) => {
return {
messageType,
time: formatTime(Date.now()),
...data
};
};
/**
* 发送自定义消息的通用方法
* @param {object} messageData - 消息数据
* @param {object} timChatManager - IM管理器
* @param {function} validateBeforeSend - 发送前验证函数
* @param {function} onSuccess - 成功回调
*/
export const sendCustomMessage = async (messageData, timChatManager, validateBeforeSend, onSuccess) => {
if (!validateBeforeSend()) {
return;
}
const result = await timChatManager.sendCustomMessage(messageData);
if (result && result.success) {
if (onSuccess) onSuccess();
} else {
console.error('发送自定义消息失败:', result?.error);
}
};
/**
* 发送消息的通用方法
* @param {string} messageType - 消息类型
* @param {any} data - 消息数据
* @param {object} timChatManager - IM管理器
* @param {function} validateBeforeSend - 发送前验证函数
* @param {function} onSuccess - 成功回调
*/
export const sendMessage = async (messageType, data, timChatManager, validateBeforeSend, onSuccess, cloudCustomData) => {
if (!validateBeforeSend()) {
return;
}
let result;
switch (messageType) {
case 'text':
result = await timChatManager.sendTextMessage(data, cloudCustomData);
break;
case 'image':
result = await timChatManager.sendImageMessage(data, cloudCustomData);
break;
case 'voice':
result = await timChatManager.sendVoiceMessage(data.file, data.duration,cloudCustomData);
break;
default:
console.error('未知的消息类型:', messageType);
return;
}
if (result && result.success) {
if (onSuccess) onSuccess();
} else {
console.error('发送消息失败:', result?.error);
showMessage('发送失败,请重试');
}
};
// ==================== 状态检查相关工具方法 ====================
/**
* 检查IM连接状态
* @param {object} timChatManager - IM管理器
* @param {function} onError - 错误回调
* @returns {boolean} 连接状态
*/
export const checkIMConnectionStatus = (timChatManager, onError) => {
if (!timChatManager.tim || !timChatManager.isLoggedIn) {
const errorMsg = "IM连接异常请重新进入";
if (onError) {
onError(errorMsg);
} else {
showMessage(errorMsg);
}
return false;
}
return true;
};
/**
* 检查是否显示时间分割线
* @param {object} message - 当前消息
* @param {number} index - 消息索引
* @param {Array} messageList - 消息列表
* @returns {boolean} 是否显示时间分割线
*/
export const shouldShowTime = (message, index, messageList) => {
if (index === 0) return true;
const prevMessage = messageList[index - 1];
// 使用工具函数验证时间戳
if (!validateTimestamp(message.lastTime) || !validateTimestamp(prevMessage.lastTime)) {
return false;
}
const timeDiff = message.lastTime - prevMessage.lastTime;
return timeDiff > 5 * 60 * 1000; // 5分钟显示一次时间
};
/**
* 预览图片
* @param {string} url - 图片URL
*/
export const previewImage = (url) => {
uni.previewImage({
urls: [url],
current: url,
});
};
// ==================== 录音相关工具方法 ====================
/**
* 检查录音时长并处理
* @param {object} res - 录音结果
* @param {Function} onTimeTooShort - 时间太短的回调
* @returns {boolean} 录音时长是否有效
*/
export const checkRecordingDuration = (res, onTimeTooShort = null) => {
const duration = Math.floor(res.duration / 1000);
if (duration < 1) {
console.log('录音时间太短,取消发送');
if (onTimeTooShort) {
onTimeTooShort();
} else {
showMessage('说话时间太短');
}
return false;
}
return true;
};
// ==================== 防抖和节流工具 ====================
/**
* 防抖函数
* @param {Function} func - 要防抖的函数
* @param {number} wait - 等待时间(毫秒)
* @returns {Function} 防抖后的函数
*/
export const debounce = (func, wait = 300) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
/**
* 节流函数
* @param {Function} func - 要节流的函数
* @param {number} limit - 限制时间(毫秒)
* @returns {Function} 节流后的函数
*/
export const throttle = (func, limit = 300) => {
let inThrottle;
return function executedFunction(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
};
// ==================== 自定义消息解析相关工具方法 ====================
// 自定义消息解析缓存
const customMessageCache = new Map();
/**
* 解析自定义消息(带缓存)
* @param {object} message - 消息对象
* @param {function} formatTime - 时间格式化函数
* @returns {object} 解析后的消息对象
*/
export const parseCustomMessage = (message, formatTime) => {
// 使用消息ID作为缓存键
const cacheKey = message.ID;
// 检查缓存
if (customMessageCache.has(cacheKey)) {
return customMessageCache.get(cacheKey);
}
try {
const customData = JSON.parse(message.payload.data);
const parsedMessage = {
messageType: customData.messageType,
content: customData.content,
symptomContent: customData.symptomContent,
hasVisitedHospital: customData.hasVisitedHospital,
selectedDiseases: customData.selectedDiseases,
images: customData.images,
medicines: customData.medicines,
diagnosis: customData.diagnosis,
prescriptionType: customData.prescriptionType,
prescriptionDesc: customData.prescriptionDesc,
tcmPrescription: customData.tcmPrescription, // 新增中药处方字段
patientName: customData.patientName,
gender: customData.gender,
age: customData.age,
surveyTitle: customData.surveyTitle,
surveyDescription: customData.surveyDescription,
surveyName: customData.surveyName,
estimatedTime: customData.estimatedTime,
reward: customData.reward,
note: customData.note,
orderId: customData.orderId, // 新增订单ID字段
timestamp: customData.timestamp, // 新增时间戳字段
conversationID: message.conversationID, // 保留conversationID
time: formatTime(message.lastTime),
};
// 缓存解析结果
customMessageCache.set(cacheKey, parsedMessage);
return parsedMessage;
} catch (error) {
const fallbackMessage = {
messageType: "unknown",
content: "未知消息类型",
};
// 缓存错误结果,避免重复解析
customMessageCache.set(cacheKey, fallbackMessage);
return fallbackMessage;
}
};
/**
* 清理消息缓存
*/
export const clearMessageCache = () => {
customMessageCache.clear();
};
/**
* 获取解析后的自定义消息(带缓存)
* @param {object} message - 消息对象
* @param {function} formatTime - 时间格式化函数
* @returns {object} 解析后的消息对象
*/
export const getParsedCustomMessage = (message, formatTime) => {
return parseCustomMessage(message, formatTime);
};
/**
* 处理查看详情
* @param {object} message - 解析后的消息对象
* @param {object} patientInfo - 患者信息
*/
export const handleViewDetail = (message, patientInfo) => {
if (message.messageType === "symptom") {
uni.showModal({
title: "完整病情描述",
content: message.symptomContent,
showCancel: false,
confirmText: "知道了",
});
} else if (message.messageType === "prescription") {
// 处理处方单详情查看
let content = `患者:${patientInfo.name}\n诊断:${message.diagnosis || '无'}\n\n`;
if (message.prescriptionType === '中药处方' && message.tcmPrescription) {
content += `处方类型:中药处方\n处方详情:${message.tcmPrescription.description}\n`;
if (message.tcmPrescription.usage) {
content += `用法用量:${message.tcmPrescription.usage}\n`;
}
} else if (message.prescriptionType === '西药处方' && message.medicines) {
content += `处方类型:西药处方\n药品清单:\n`;
const medicineDetails = message.medicines
.map((med) => `${med.name} ${med.spec} ×${med.count}`)
.join("\n");
content += medicineDetails + "\n";
// 添加用法用量
const usageDetails = message.medicines
.filter(med => med.usage)
.map(med => `${med.name}${med.usage}`)
.join("\n");
if (usageDetails) {
content += `\n用法用量:\n${usageDetails}\n`;
}
}
content += `\n开方时间:${message.time}`;
uni.showModal({
title: "处方详情",
content: content,
showCancel: false,
confirmText: "知道了",
});
} else if (message.messageType === "refill") {
// 处理续方申请详情查看
let content = `患者:${message.patientName} ${message.gender} ${message.age}\n诊断:${message.diagnosis}\n\n`;
if (message.prescriptionType === "中药处方") {
content += `处方类型:${message.prescriptionType}\n处方详情:${message.prescriptionDesc}`;
} else {
const medicineDetails = message.medicines
.map((med) => `${med.name} ${med.spec} ${med.count}\n${med.usage}`)
.join("\n\n");
content += `药品清单:\n${medicineDetails}`;
}
uni.showModal({
title: "续方申请详情",
content: content,
showCancel: false,
confirmText: "知道了",
});
} else if (message.messageType === "survey") {
// 处理问卷调查详情查看或跳转
uni.showModal({
title: "问卷调查",
content: `${message.surveyTitle}\n\n${message.surveyDescription
}\n\n问卷名称:${message.surveyName}\n预计用时:${message.estimatedTime}${message.reward ? "\n完成奖励" + message.reward : ""
}${message.note ? "\n\n说明" + message.note : ""}`,
confirmText: "去填写",
cancelText: "稍后再说",
success: (res) => {
if (res.confirm) {
// 这里可以跳转到问卷页面
uni.showToast({
title: "正在跳转到问卷页面",
icon: "none",
});
}
},
});
}
};