ykt-wxapp/utils/chat-utils.js

905 lines
25 KiB
JavaScript
Raw Normal View History

2026-01-22 15:13:33 +08:00
/**
* 聊天相关工具函数
*/
// 通用消息提示
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) {
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('选择图片失败,请重试');
}
}
});
};
/**
2026-01-22 17:02:15 +08:00
* 选择图片针对 TIM SDK 优化
2026-01-22 15:13:33 +08:00
* @param {function} onSuccess - 成功回调
* @param {function} onFail - 失败回调
*/
2026-01-22 17:02:15 +08:00
export const chooseImage = async (onSuccess, onFail) => {
// 检查权限
const sourceType = ['album', 'camera'];
if (sourceType.includes('album')) {
const hasPermission = await checkAlbumPermission();
if (!hasPermission) {
console.log('用户未授予相册权限');
if (onFail) {
onFail({ errMsg: '未授权相册权限' });
}
return;
}
}
// 使用 wx.chooseImage 以确保与 TIM SDK 兼容
// #ifdef MP-WEIXIN
wx.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: sourceType,
success: function (res) {
console.log('wx.chooseImage 成功,完整返回数据:', JSON.stringify(res));
console.log('tempFilePaths:', res.tempFilePaths);
console.log('tempFiles:', res.tempFiles);
// TIM SDK 需要完整的 wx.chooseImage 返回对象,而不是单个文件
// 直接传递整个 res 对象
if (onSuccess) onSuccess(res);
},
fail: function (err) {
// 用户取消选择
if (err.errMsg && err.errMsg.includes('cancel')) {
console.log('用户取消选择');
return;
}
// 权限相关错误
if (err.errMsg && (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('选择图片失败,请重试');
}
}
});
// #endif
// #ifndef MP-WEIXIN
// 非微信小程序环境,使用 uni.chooseMedia
2026-01-22 15:13:33 +08:00
chooseMedia({
count: 1,
mediaType: ['image'],
sizeType: ['original', 'compressed'],
2026-01-22 17:02:15 +08:00
sourceType: sourceType
2026-01-22 15:13:33 +08:00
}, onSuccess, onFail);
2026-01-22 17:02:15 +08:00
// #endif
2026-01-22 15:13:33 +08:00
};
/**
2026-01-22 17:02:15 +08:00
* 拍照针对 TIM SDK 优化
2026-01-22 15:13:33 +08:00
* @param {function} onSuccess - 成功回调
* @param {function} onFail - 失败回调
*/
export const takePhoto = (onSuccess, onFail) => {
2026-01-22 17:02:15 +08:00
// 使用 wx.chooseImage 以确保与 TIM SDK 兼容
// #ifdef MP-WEIXIN
wx.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['camera'],
success: function (res) {
console.log('wx.chooseImage (拍照) 成功,完整返回数据:', JSON.stringify(res));
console.log('tempFilePaths:', res.tempFilePaths);
console.log('tempFiles:', res.tempFiles);
// TIM SDK 需要完整的 wx.chooseImage 返回对象
if (onSuccess) onSuccess(res);
},
fail: function (err) {
// 用户取消
if (err.errMsg && err.errMsg.includes('cancel')) {
console.log('用户取消拍照');
return;
}
console.error('拍照失败:', err);
if (onFail) {
onFail(err);
} else {
showMessage('拍照失败,请重试');
}
}
});
// #endif
// #ifndef MP-WEIXIN
// 非微信小程序环境
2026-01-22 15:13:33 +08:00
chooseMedia({
count: 1,
mediaType: ['image'],
sizeType: ['original', 'compressed'],
sourceType: ['camera']
}, onSuccess, onFail);
2026-01-22 17:02:15 +08:00
// #endif
2026-01-22 15:13:33 +08:00
};
// ==================== 录音相关工具方法 ====================
/**
* 初始化录音管理器
* @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) => {
2026-01-22 16:35:05 +08:00
console.log('chat-utils sendMessage 被调用:', { messageType, data });
2026-01-22 15:13:33 +08:00
if (!validateBeforeSend()) {
return;
}
let result;
switch (messageType) {
case 'text':
result = await timChatManager.sendTextMessage(data, cloudCustomData);
break;
case 'image':
2026-01-22 16:35:05 +08:00
console.log('准备发送图片消息,数据:', data);
2026-01-22 15:13:33 +08:00
result = await timChatManager.sendImageMessage(data, cloudCustomData);
2026-01-22 16:35:05 +08:00
console.log('图片消息发送结果:', result);
2026-01-22 15:13:33 +08:00
break;
case 'voice':
result = await timChatManager.sendVoiceMessage(data.file, data.duration,cloudCustomData);
break;
default:
console.error('未知的消息类型:', messageType);
return;
}
if (result && result.success) {
2026-01-22 16:35:05 +08:00
console.log('消息发送成功');
2026-01-22 15:13:33 +08:00
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",
});
}
},
});
}
};