This commit is contained in:
huxuejian 2026-02-09 09:12:20 +08:00
commit 9a251ddcb0
10 changed files with 326 additions and 295 deletions

View File

@ -526,6 +526,7 @@ async function sendFollowUp(todo) {
content: todo.sendContent, content: todo.sendContent,
}); });
} }
console.log("==============>fileList", todo.fileList);
// 2. // 2.
if (Array.isArray(todo.fileList)) { if (Array.isArray(todo.fileList)) {
@ -537,20 +538,21 @@ async function sendFollowUp(todo) {
content: file.URL, content: file.URL,
name: file.file?.name || file.name || "图片", name: file.file?.name || file.name || "图片",
}); });
} else if (file.type === "article" && file.file?.url) { } else if (file.file.type === "article" && file.file?.url) {
// // - URL id
const articleId = extractIdFromUrl(file.file.url);
messages.push({ messages.push({
type: "article", type: "article",
content: { content: {
_id: file.file?._id || file._id, _id: articleId,
title: file.file?.name || file.name || "宣教文章", title: file.file?.name || "宣教文章",
url: file.file?.url || file.URL, url: file.file?.url || file.URL,
subtitle: file.file?.subtitle || "", subtitle: file.file?.subtitle || "",
cover: file.file?.cover || "", cover: file.file?.cover || "",
articleId: file.file?._id || file._id, articleId: articleId,
}, },
}); });
} else if (file.type === "questionnaire" && file.file?.surveryId) { } else if (file.file.type === "questionnaire" && file.file?.surveryId) {
// //
messages.push({ messages.push({
type: "questionnaire", type: "questionnaire",
@ -580,6 +582,30 @@ async function sendFollowUp(todo) {
} }
} }
/**
* URL 中提取 id 参数
* @param {string} url - 完整的 URL
* @returns {string} 提取出的 id
*/
function extractIdFromUrl(url) {
if (!url) return "";
try {
// : https://www.youcan365.com/patientDeploy/#/pages/article/index?id=267epkhd3xbklcnbf0f45gzp1769567841991&corpId=...
const urlObj = new URL(url);
const id = urlObj.searchParams.get("id");
if (id) return id;
// 使
const match = url.match(/[?&]id=([^&]+)/);
return match ? decodeURIComponent(match[1]) : "";
} catch (error) {
console.error("解析 URL 失败:", error);
// 使
const match = url.match(/[?&]id=([^&]+)/);
return match ? decodeURIComponent(match[1]) : "";
}
}
// ---- filter popup ---- // ---- filter popup ----
const filterPopupRef = ref(null); const filterPopupRef = ref(null);
const state = ref(null); const state = ref(null);

View File

@ -418,6 +418,25 @@ $primary-color: #0877F1;
background: #2456c7; background: #2456c7;
} }
.disabled-btn {
background: #ccc;
color: #fff;
font-size: 28rpx;
font-weight: 600;
border: none;
border-radius: 40rpx;
height: 56rpx;
min-width: 112rpx;
padding: 0 32rpx;
box-shadow: 0 2rpx 8rpx rgba(200, 200, 200, 0.08);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
white-space: nowrap;
cursor: not-allowed;
}
.input-area { .input-area {
flex: 1; flex: 1;
margin: 0 8rpx; margin: 0 8rpx;

View File

@ -57,10 +57,12 @@ const props = defineProps({
default: "", default: "",
}, },
}); });
const emit = defineEmits(["streamText", "clearInput"]); const emit = defineEmits(["streamText", "clearInput", "generatingStateChange"]);
const typeSelectorRef = ref(null); const typeSelectorRef = ref(null);
const progressRef = ref(null); const progressRef = ref(null);
const isGenerating = ref(false);
const buttons = ref([ const buttons = ref([
{ {
id: "followUp", id: "followUp",
@ -183,6 +185,8 @@ const streamTextToInput = (text) => {
// //
emit("clearInput"); emit("clearInput");
isGenerating.value = true;
emit("generatingStateChange", true);
let currentIndex = 0; let currentIndex = 0;
const speed = 50; // const speed = 50; //
@ -196,6 +200,8 @@ const streamTextToInput = (text) => {
currentIndex++; currentIndex++;
} else { } else {
clearInterval(streamInterval); clearInterval(streamInterval);
isGenerating.value = false;
emit("generatingStateChange", false);
} }
}, speed); }, speed);
}, 100); }, 100);
@ -363,6 +369,11 @@ const handleRegenerateMedicalCase = (data) => {
const type = { id: data.caseType }; const type = { id: data.caseType };
handleCaseTypeSelect(type); handleCaseTypeSelect(type);
}; };
//
defineExpose({
isGenerating,
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -8,18 +8,21 @@
<view class="input-area"> <view class="input-area">
<textarea v-if="!showVoiceInput" class="text-input" v-model="inputText" placeholder="我来说两句..." <textarea v-if="!showVoiceInput" class="text-input" v-model="inputText" placeholder="我来说两句..."
@confirm="sendTextMessage" @focus="handleInputFocus" @input="handleInput" @confirm="sendTextMessage" @focus="handleInputFocus" @input="handleInput"
:auto-height="true" :show-confirm-bar="false" :adjust-position="true" :cursor-spacing="60" :auto-height="true" :show-confirm-bar="false" :adjust-position="true" :cursor-spacing="30"
/> />
<input v-else class="voice-input-btn" :class="{ recording: isRecording }" @touchstart="startRecord" <input v-else class="voice-input-btn" :class="{ recording: isRecording }" @touchstart="startRecord"
@touchmove="onRecordTouchMove" @touchend="stopRecord" @touchcancel="cancelRecord" :placeholder="isRecording ? '松开发送' : '按住说话'" disabled> @touchmove="onRecordTouchMove" @touchend="stopRecord" @touchcancel="cancelRecord" :placeholder="isRecording ? '松开发送' : '按住说话'" disabled>
</input> </input>
</view> </view>
<button v-if="inputText.trim()" class="send-btn" @click="sendTextMessage"> <button v-if="inputText.trim() && !props.isGenerating" class="send-btn" @click="sendTextMessage">
发送 发送
</button> </button>
<view v-else class="plus-btn" @click="toggleMorePanel()"> <view v-else-if="!inputText.trim() && !props.isGenerating" class="plus-btn" @click="toggleMorePanel()">
<uni-icons type="plusempty" size="28" color="#666" /> <uni-icons type="plusempty" size="28" color="#666" />
</view> </view>
<view v-else class="send-btn disabled-btn">
<text>生成中...</text>
</view>
</view> </view>
<view class="more-panel" v-if="showMorePanel"> <view class="more-panel" v-if="showMorePanel">
<view v-for="btn in morePanelButtons" :key="btn.text" class="more-btn" @click="btn.action"> <view v-for="btn in morePanelButtons" :key="btn.text" class="more-btn" @click="btn.action">
@ -80,6 +83,7 @@ const props = defineProps({
patientId: { type: String, default: "" }, patientId: { type: String, default: "" },
corpId: { type: String, default: "" }, corpId: { type: String, default: "" },
orderStatus: { type: String, default: "" }, orderStatus: { type: String, default: "" },
isGenerating: { type: Boolean, default: false },
}); });
// Emits // Emits

View File

@ -140,12 +140,14 @@
!showConsultAccept && !showConsultAccept &&
orderStatus === 'processing' orderStatus === 'processing'
" "
ref="aiAssistantRef"
:groupId="groupId" :groupId="groupId"
:patientAccountId="chatInfo.userID || ''" :patientAccountId="chatInfo.userID || ''"
:patientId="patientId" :patientId="patientId"
:corpId="corpId" :corpId="corpId"
@streamText="handleStreamText" @streamText="handleStreamText"
@clearInput="handleClearInput" @clearInput="handleClearInput"
@generatingStateChange="handleGeneratingStateChange"
/> />
<!-- 聊天输入组件 --> <!-- 聊天输入组件 -->
@ -165,6 +167,7 @@
:corpId="corpId" :corpId="corpId"
:patientInfo="patientInfo" :patientInfo="patientInfo"
:orderStatus="orderStatus" :orderStatus="orderStatus"
:isGenerating="isGenerating"
@scrollToBottom="() => scrollToBottom(true)" @scrollToBottom="() => scrollToBottom(true)"
@messageSent="() => scrollToBottom(true)" @messageSent="() => scrollToBottom(true)"
@endConsult="handleEndConsult" @endConsult="handleEndConsult"
@ -217,6 +220,8 @@ const { initIMAfterLogin } = useAccountStore();
// //
const chatInputRef = ref(null); const chatInputRef = ref(null);
const aiAssistantRef = ref(null);
const isGenerating = ref(false);
const groupId = ref(""); const groupId = ref("");
const { chatMember, getGroupInfo, getUserAvatar } = useGroupChat(groupId); const { chatMember, getGroupInfo, getUserAvatar } = useGroupChat(groupId);
@ -316,7 +321,11 @@ const fetchGroupOrderStatus = async () => {
const teamName = result.data.team?.name || "群聊"; const teamName = result.data.team?.name || "群聊";
updateNavigationTitle(teamName); updateNavigationTitle(teamName);
teamId.value = result.data.teamId || result.data.team?.teamId || result.data.team?._id || ""; teamId.value =
result.data.teamId ||
result.data.team?.teamId ||
result.data.team?._id ||
"";
// //
if (result.data.patient) { if (result.data.patient) {
@ -765,7 +774,7 @@ onShow(() => {
startIMMonitoring(30000); startIMMonitoring(30000);
// 访 // 访
uni.$on('send-followup-message', handleSendFollowUpMessage); uni.$on("send-followup-message", handleSendFollowUpMessage);
}); });
// 访 // 访
@ -783,10 +792,10 @@ const handleSendFollowUpMessage = async (data) => {
}, 100); }, 100);
} }
} catch (error) { } catch (error) {
console.error('发送回访任务消息失败:', error); console.error("发送回访任务消息失败:", error);
uni.showToast({ uni.showToast({
title: '发送失败,请重试', title: "发送失败,请重试",
icon: 'none', icon: "none",
}); });
} }
}; };
@ -820,6 +829,11 @@ const handleClearInput = () => {
} }
}; };
//
const handleGeneratingStateChange = (generating) => {
isGenerating.value = generating;
};
// //
defineExpose({ defineExpose({
sendCommonPhrase, sendCommonPhrase,
@ -1016,7 +1030,7 @@ onUnmounted(() => {
timChatManager.setCallback("onError", null); timChatManager.setCallback("onError", null);
// 访 // 访
uni.$off('send-followup-message', handleSendFollowUpMessage); uni.$off("send-followup-message", handleSendFollowUpMessage);
}); });
</script> </script>

View File

@ -164,9 +164,18 @@
uni.hideLoading(); uni.hideLoading();
if (!success) { if (!success) {
uni.showToast({ //
title: "IM连接失败请重试", uni.showModal({
icon: "none", title: "IM连接失败",
content: "连接失败请检查网络后重试。如果IM连接失败请重新登陆IM再连接",
confirmText: "重新登陆",
cancelText: "取消",
success: (res) => {
if (res.confirm) {
//
handleReloginIM();
}
}
}); });
return false; return false;
} }
@ -178,21 +187,100 @@
uni.hideLoading(); uni.hideLoading();
if (!reconnected) { if (!reconnected) {
//
uni.showModal({
title: "IM连接失败",
content: "连接失败请检查网络后重试。如果IM连接失败请重新登陆IM再连接",
confirmText: "重新登陆",
cancelText: "取消",
success: (res) => {
if (res.confirm) {
//
handleReloginIM();
}
}
});
return false; return false;
} }
} }
return true; return true;
}; };
// IM
const handleReloginIM = async () => {
try {
uni.showLoading({
title: "重新登陆中...",
});
// IM
if (globalTimChatManager) {
await globalTimChatManager.cleanupOldInstance();
}
// IM
const { initIMAfterLogin } = useAccountStore();
const success = await initIMAfterLogin();
uni.hideLoading();
if (success) {
uni.showToast({
title: "IM连接成功",
icon: "success",
});
//
await loadConversationList();
setupConversationListener();
} else {
uni.showToast({
title: "IM连接失败请检查网络",
icon: "none",
});
}
} catch (error) {
uni.hideLoading();
console.error("重新登陆IM失败:", error);
uni.showToast({
title: "重新登陆失败",
icon: "none",
});
}
};
// //
const loadConversationList = async () => { const loadConversationList = async () => {
if (loading.value) return; if (loading.value) return;
loading.value = true; loading.value = true;
try { try {
console.log("开始加载群聊列表"); console.log("开始加载群聊列表");
if (!globalTimChatManager || !globalTimChatManager.getGroupList) {
// IM
if (!globalTimChatManager) {
throw new Error("IM管理器未初始化"); throw new Error("IM管理器未初始化");
} }
// TIM
if (!globalTimChatManager.tim) {
console.warn("TIM实例不存在尝试重新初始化IM");
const reinitialized = await initIMAfterLogin();
if (!reinitialized) {
throw new Error("IM重新初始化失败");
}
}
//
if (!globalTimChatManager.isLoggedIn) {
console.warn("IM未登录尝试重新连接");
const reconnected = await globalTimChatManager.ensureIMConnection();
if (!reconnected) {
throw new Error("IM重新连接失败");
}
}
if (!globalTimChatManager.getGroupList) {
throw new Error("IM管理器方法不可用");
}
const result = await globalTimChatManager.getGroupList(); const result = await globalTimChatManager.getGroupList();
if (result && result.success && result.groupList) { if (result && result.success && result.groupList) {
// //

View File

@ -108,16 +108,30 @@ export default defineStore("accountStore", () => {
} }
async function initIMAfterLogin() { async function initIMAfterLogin() {
if (isIMInitialized.value) return true; if (isIMInitialized.value) return true;
if (!doctorInfo.value) return; if (!doctorInfo.value) {
console.error('医生信息未获取无法初始化IM');
return false;
}
try { try {
const userID = doctorInfo.value.userid; const userID = doctorInfo.value.userid;
if (!userID) await getDoctorInfo(); if (!userID) {
await initGlobalTIM(userID); await getDoctorInfo();
if (!doctorInfo.value?.userid) {
throw new Error('无法获取用户ID');
}
}
const success = await initGlobalTIM(userID);
if (!success) {
console.error('initGlobalTIM 返回失败');
return false;
}
isIMInitialized.value = true; isIMInitialized.value = true;
return true; return true;
} catch (error) { } catch (error) {
console.error('IM初始化失败:', error); console.error('IM初始化失败:', error);
isIMInitialized.value = false;
return false; return false;
} }
} }

View File

@ -41,13 +41,13 @@ export async function sendTextMessage(content) {
/** /**
* 发送图片消息 * 发送图片消息
* @param {string} imageUrl - 图片URL字符串 * @param {string|File} imageSource - 图片URL字符串或文件对象
* @param {string} imageName - 图片名称可选 * @param {string} imageName - 图片名称可选
* @returns {Promise<boolean>} 发送是否成功 * @returns {Promise<boolean>} 发送是否成功
*/ */
export async function sendImageMessage(imageUrl, imageName = '图片') { export async function sendImageMessage(imageSource, imageName = '图片') {
if (!imageUrl) { if (!imageSource) {
toast('图片URL不能为空'); toast('图片不能为空');
return false; return false;
} }
@ -57,9 +57,17 @@ export async function sendImageMessage(imageUrl, imageName = '图片') {
return false; return false;
} }
// 直接调用 tim-chat 的 sendImageMessage 方法 // 如果是URL字符串需要先下载转换为文件对象
// tim-chat.js 中的 getImageUrl 方法可以处理 URL 字符串 let imageFile = imageSource;
const result = await globalTimChatManager.sendImageMessage(imageUrl); if (typeof imageSource === 'string') {
imageFile = await downloadImageAsFile(imageSource, imageName);
if (!imageFile) {
toast('图片下载失败');
return false;
}
}
const result = await globalTimChatManager.sendImageMessage(imageFile);
if (result?.success) { if (result?.success) {
return true; return true;
@ -74,6 +82,44 @@ export async function sendImageMessage(imageUrl, imageName = '图片') {
} }
} }
/**
* 将图片URL下载转换为文件对象小程序环境
* @param {string} imageUrl - 图片URL
* @param {string} fileName - 文件名
* @returns {Promise<Object|null>} 文件对象或null
*/
async function downloadImageAsFile(imageUrl, fileName = '图片') {
return new Promise((resolve) => {
uni.downloadFile({
url: imageUrl,
success: (res) => {
if (res.statusCode === 200) {
// 返回包含 tempFiles 数组的对象,模拟 uni.chooseMedia 的返回格式
const fileObj = {
tempFiles: [{
tempFilePath: res.tempFilePath,
path: res.tempFilePath,
size: 0,
type: 'image'
}],
// 也保留直接的 tempFilePath 以兼容其他代码
tempFilePath: res.tempFilePath
};
console.log('图片下载成功,文件对象:', fileObj);
resolve(fileObj);
} else {
console.error('下载图片失败,状态码:', res.statusCode);
resolve(null);
}
},
fail: (error) => {
console.error('下载图片失败:', error);
resolve(null);
}
});
});
}
/** /**
* 发送宣教文章消息 * 发送宣教文章消息
* @param {Object} article - 文章对象 { _id, title, cover, url } * @param {Object} article - 文章对象 { _id, title, cover, url }

View File

@ -1,234 +0,0 @@
# 微信小程序分享功能使用指南
## 功能说明
提供了完整的微信小程序分享功能,包括:
- 分享给好友
- 分享到朋友圈
- 保存图片到相册
## 使用方法
### 1. 基础分享(在页面中)
```vue
<template>
<view>
<button open-type="share">分享给好友</button>
</view>
</template>
<script setup>
import { createShareMessage, createShareTimeline } from '@/utils/share'
// 分享给好友
function onShareAppMessage() {
return createShareMessage({
title: '分享标题',
path: '/pages/index/index?id=123',
imageUrl: 'https://example.com/share.jpg'
})
}
// 分享到朋友圈(需要在 app.json 中配置)
function onShareTimeline() {
return createShareTimeline({
title: '朋友圈标题',
query: 'id=123',
imageUrl: 'https://example.com/share.jpg'
})
}
// 导出分享方法
defineExpose({
onShareAppMessage,
onShareTimeline
})
</script>
```
### 2. 使用分享组件
```vue
<template>
<view>
<share-actions
@save="handleSave"
:show-save="true"
:show-share="true"
save-text="保存图片"
share-text="分享微信"
/>
</view>
</template>
<script setup>
import { saveImageToAlbum, createShareMessage } from '@/utils/share'
import shareActions from '@/components/share-actions.vue'
// 保存图片
async function handleSave() {
const imagePath = 'https://example.com/image.jpg'
await saveImageToAlbum(imagePath)
}
// 分享配置
function onShareAppMessage() {
return createShareMessage({
title: '分享标题',
path: '/pages/index/index'
})
}
defineExpose({
onShareAppMessage
})
</script>
```
### 3. 保存二维码图片
```vue
<template>
<view>
<uqrcode ref="qrcode" canvasId="qrcode" :value="qrcodeUrl" />
<button @click="saveQrcode">保存二维码</button>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { saveImageToAlbum } from '@/utils/share'
import { toast } from '@/utils/widget'
const qrcode = ref(null)
const qrcodeUrl = ref('https://example.com')
async function saveQrcode() {
try {
if (!qrcode.value) {
toast('二维码未加载完成')
return
}
// 获取二维码临时文件路径
const tempFilePath = qrcode.value.toTempFilePath()
if (tempFilePath) {
await saveImageToAlbum(tempFilePath)
} else {
toast('获取二维码失败')
}
} catch (err) {
console.error('保存失败:', err)
toast('保存失败')
}
}
</script>
```
### 4. 动态分享内容
```vue
<script setup>
import { ref, computed } from 'vue'
import { createShareMessage } from '@/utils/share'
const currentItem = ref({
id: '123',
title: '商品标题',
image: 'https://example.com/product.jpg'
})
// 动态生成分享配置
function onShareAppMessage() {
return createShareMessage({
title: currentItem.value.title,
path: `/pages/detail/detail?id=${currentItem.value.id}`,
imageUrl: currentItem.value.image
})
}
defineExpose({
onShareAppMessage
})
</script>
```
## 配置说明
### 1. 启用分享到朋友圈
`pages.json` 中配置页面:
```json
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"enableShareTimeline": true
}
}
```
### 2. 全局分享配置
`App.vue` 中配置全局分享:
```vue
<script>
export default {
onShareAppMessage() {
return {
title: '默认分享标题',
path: '/pages/index/index'
}
},
onShareTimeline() {
return {
title: '默认朋友圈标题'
}
}
}
</script>
```
## API 说明
### createShareMessage(options)
创建分享给好友的配置
**参数:**
- `title` (string): 分享标题
- `path` (string): 分享路径
- `imageUrl` (string): 分享图片URL
**返回:** 分享配置对象
### createShareTimeline(options)
创建分享到朋友圈的配置
**参数:**
- `title` (string): 分享标题
- `query` (string): 分享路径参数
- `imageUrl` (string): 分享图片URL
**返回:** 分享配置对象
### saveImageToAlbum(filePath)
保存图片到相册
**参数:**
- `filePath` (string): 图片路径(本地临时路径或网络路径)
**返回:** Promise<boolean>
## 注意事项
1. 分享图片建议尺寸5:4推荐 500x400px
2. 分享路径必须是已注册的页面路径
3. 保存图片需要用户授权相册权限
4. 分享到朋友圈需要在页面配置中启用
5. 网络图片会自动下载后保存到相册

View File

@ -20,16 +20,16 @@ if (!TIM_CONFIG.SDKAppID || isNaN(TIM_CONFIG.SDKAppID)) {
// IM连接配置常量 // IM连接配置常量
const IM_CONNECTION_CONFIG = { const IM_CONNECTION_CONFIG = {
MAX_RECONNECT_ATTEMPTS: 10, // 最大重连次数 MAX_RECONNECT_ATTEMPTS: 15, // 增加最大重连次数到15次
RECONNECT_DELAYS: [2000, 4000, 8000, 16000, 30000], // 重连延迟(指数退避) RECONNECT_DELAYS: [1000, 2000, 4000, 8000, 16000, 30000], // 重连延迟(指数退避)
LOGIN_COOLDOWN: 5000, // 登录冷却时间 LOGIN_COOLDOWN: 3000, // 降低登录冷却时间到3秒
SDK_READY_TIMEOUT: 15000, // SDK就绪超时时间 SDK_READY_TIMEOUT: 20000, // 增加SDK就绪超时时间到20秒
LOGIN_CHECK_INTERVAL_STABLE: 60000, // 稳定状态检查间隔 LOGIN_CHECK_INTERVAL_STABLE: 60000, // 稳定状态检查间隔
LOGIN_CHECK_INTERVAL_UNSTABLE: 15000, // 不稳定状态检查间隔 LOGIN_CHECK_INTERVAL_UNSTABLE: 10000, // 降低不稳定状态检查间隔到10秒
LOGIN_CHECK_FIRST_DELAY: 30000, // 首次检查延迟 LOGIN_CHECK_FIRST_DELAY: 20000, // 降低首次检查延迟到20秒
HEARTBEAT_INTERVAL: 60000, // 心跳间隔毫秒60秒 HEARTBEAT_INTERVAL: 60000, // 心跳间隔毫秒60秒
HEARTBEAT_MAX_FAIL: 3, // 心跳最大失败次数 HEARTBEAT_MAX_FAIL: 2, // 降低心跳最大失败次数到2次
NETWORK_RECONNECT_DELAY: 2000, // 网络恢复后延迟重连 NETWORK_RECONNECT_DELAY: 1000, // 降低网络恢复后延迟重连到1秒
MESSAGE_BATCH_COUNT: 20, // 每批消息数量 MESSAGE_BATCH_COUNT: 20, // 每批消息数量
MAX_MESSAGE_REQUESTS: 50, // 最大消息请求次数 MAX_MESSAGE_REQUESTS: 50, // 最大消息请求次数
MAX_CACHE_SIZE: 1000, // 最大缓存消息数 MAX_CACHE_SIZE: 1000, // 最大缓存消息数
@ -191,7 +191,12 @@ class TimChatManager {
// 等待SDK Ready // 等待SDK Ready
console.log('等待SDK Ready...') console.log('等待SDK Ready...')
try {
await this.waitForSDKReady(IM_CONNECTION_CONFIG.SDK_READY_TIMEOUT) await this.waitForSDKReady(IM_CONNECTION_CONFIG.SDK_READY_TIMEOUT)
} catch (timeoutError) {
// SDK Ready 超时,但不一定是致命错误,继续进行
console.warn('SDK Ready 超时,但继续进行:', timeoutError.message)
}
console.log('=== IM初始化完成 ===') console.log('=== IM初始化完成 ===')
return true return true
@ -264,7 +269,7 @@ class TimChatManager {
waitForSDKReady(timeout = IM_CONNECTION_CONFIG.SDK_READY_TIMEOUT) { waitForSDKReady(timeout = IM_CONNECTION_CONFIG.SDK_READY_TIMEOUT) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const startTime = Date.now() const startTime = Date.now()
const checkInterval = 1000 // 每秒检查一次 const checkInterval = 500 // 每500ms检查一次更快响应
let checkCount = 0 let checkCount = 0
const checkSDKReady = () => { const checkSDKReady = () => {
@ -277,8 +282,8 @@ class TimChatManager {
} else if (elapsed > timeout) { } else if (elapsed > timeout) {
const error = new Error(`等待SDK Ready超时${timeout}ms`) const error = new Error(`等待SDK Ready超时${timeout}ms`)
console.error('✗', error.message) console.error('✗', error.message)
// 超时不算致命错误,尝试继续 // 超时时拒绝而不是继续,让调用者知道出错了
resolve() reject(error)
} else { } else {
console.log(`等待SDK Ready... ${Math.floor(elapsed / 1000)}/${Math.floor(timeout / 1000)}`) console.log(`等待SDK Ready... ${Math.floor(elapsed / 1000)}/${Math.floor(timeout / 1000)}`)
setTimeout(checkSDKReady, checkInterval) setTimeout(checkSDKReady, checkInterval)
@ -838,16 +843,16 @@ class TimChatManager {
} }
if (netState === TIM.TYPES.NET_STATE_CONNECTED) { if (netState === TIM.TYPES.NET_STATE_CONNECTED) {
console.log('✓ 网络已连接,延迟检查IM状态以确保稳定') console.log('✓ 网络已连接,立即检查IM状态')
// 网络恢复后延迟再检查,避免网络还不稳定时立即重连 // 网络恢复后立即检查,不再延迟
const delay = IM_CONNECTION_CONFIG.NETWORK_RECONNECT_DELAY const delay = IM_CONNECTION_CONFIG.NETWORK_RECONNECT_DELAY
this.networkReconnectTimer = setTimeout(() => { this.networkReconnectTimer = setTimeout(() => {
if (this.tim && !this.isLoggedIn && !this.isLoggingIn) { if (this.tim && !this.isLoggedIn && !this.isLoggingIn) {
console.log('🔄 网络已稳定,开始重连') console.log('🔄 网络已恢复,开始重连')
this.ensureIMConnection() this.ensureIMConnection()
} else if (this.isLoggedIn) { } else if (this.isLoggedIn) {
console.log('✓ 网络已稳定IM连接正常') console.log('✓ 网络已恢复IM连接正常')
} }
this.networkReconnectTimer = null this.networkReconnectTimer = null
}, delay) }, delay)
@ -855,7 +860,7 @@ class TimChatManager {
// 重置重连次数(网络恢复后给更多机会) // 重置重连次数(网络恢复后给更多机会)
if (this.reconnectAttempts > 0) { if (this.reconnectAttempts > 0) {
console.log(`重置重连次数(之前: ${this.reconnectAttempts}`) console.log(`重置重连次数(之前: ${this.reconnectAttempts}`)
this.reconnectAttempts = 0 this.reconnectAttempts = Math.max(0, this.reconnectAttempts - 3) // 减少重连次数,给更多机会
} }
} else if (netState === TIM.TYPES.NET_STATE_CONNECTING) { } else if (netState === TIM.TYPES.NET_STATE_CONNECTING) {
@ -1025,8 +1030,31 @@ class TimChatManager {
// 获取群聊列表 // 获取群聊列表
getGroupList() { getGroupList() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 如果 TIM 实例不存在,等待初始化
if (!this.tim) { if (!this.tim) {
reject(new Error('TIM实例不存在')) console.log('TIM实例不存在等待初始化...')
let waitTime = 0
const maxWaitTime = 30000 // 最多等待30秒
const checkInterval = 500 // 每500ms检查一次
let timeoutHandle = null
const checkTIMReady = () => {
if (this.tim && this.isLoggedIn) {
console.log('TIM实例已就绪开始获取群聊列表')
if (timeoutHandle) clearTimeout(timeoutHandle)
this.getGroupListInternal().then(resolve).catch(reject)
} else if (waitTime >= maxWaitTime) {
console.error('等待TIM实例就绪超时')
if (timeoutHandle) clearTimeout(timeoutHandle)
reject(new Error('IM连接失败请检查网络连接或重新登陆'))
} else {
waitTime += checkInterval
console.log(`等待TIM实例就绪... (${Math.floor(waitTime / 1000)}/${Math.floor(maxWaitTime / 1000)}秒)`)
timeoutHandle = setTimeout(checkTIMReady, checkInterval)
}
}
checkTIMReady()
return return
} }
@ -1034,7 +1062,7 @@ class TimChatManager {
console.log('SDK未ready等待SDK初始化...') console.log('SDK未ready等待SDK初始化...')
let waitTime = 0 let waitTime = 0
const maxWaitTime = 30000 // 最多等待30秒 const maxWaitTime = 30000 // 最多等待30秒
const checkInterval = 1000 // 每秒检查一次 const checkInterval = 500 // 每500ms检查一次
let timeoutHandle = null let timeoutHandle = null
const checkSDKReady = () => { const checkSDKReady = () => {
@ -2189,7 +2217,7 @@ class TimChatManager {
async sendImageMessage(imageFile) { async sendImageMessage(imageFile) {
if (!this.tim) { if (!this.tim) {
this.triggerCallback('onError', 'IM未初始化') this.triggerCallback('onError', 'IM未初始化')
return return { success: false, error: 'IM未初始化' }
} }
if (!this.conversation) { if (!this.conversation) {
@ -2240,20 +2268,27 @@ class TimChatManager {
// 触发消息接收回调让UI立即显示 // 触发消息接收回调让UI立即显示
this.triggerCallback('onMessageReceived', localMessage) this.triggerCallback('onMessageReceived', localMessage)
try {
console.log('准备创建图片消息imageFile:', imageFile)
const message = this.tim.createImageMessage({ const message = this.tim.createImageMessage({
to: groupID, to: groupID,
conversationType: TIM.TYPES.CONV_GROUP, conversationType: TIM.TYPES.CONV_GROUP,
payload: { file: imageFile } payload: { file: imageFile }
}) })
try { console.log('图片消息创建成功:', message)
if (!message) {
throw new Error('createImageMessage 返回空值')
}
await this.tim.sendMessage(message) await this.tim.sendMessage(message)
localMessage.status = 'success' localMessage.status = 'success'
return { success: true, message: localMessage } return { success: true, message: localMessage }
} catch (error) { } catch (error) {
console.error('图片消息发送失败:', error) console.error('图片消息发送失败:', error)
localMessage.status = 'failed' localMessage.status = 'failed'
return { success: false, error } return { success: false, error: error.message || error }
} }
} }
// 发送语音消息 // 发送语音消息
@ -2840,7 +2875,11 @@ const initGlobalTIM = async (userID, forceReinit = false) => {
console.log('强制重新初始化TIM登出成功') console.log('强制重新初始化TIM登出成功')
} }
await globalTimChatManager.initTIM(userID) const success = await globalTimChatManager.initTIM(userID)
if (!success) {
console.error('强制重新初始化失败')
return false
}
console.log('强制重新初始化完成') console.log('强制重新初始化完成')
return true return true
} }
@ -2850,7 +2889,11 @@ const initGlobalTIM = async (userID, forceReinit = false) => {
return true return true
} }
await globalTimChatManager.initTIM(userID) const success = await globalTimChatManager.initTIM(userID)
if (!success) {
console.error('全局IM初始化失败')
return false
}
console.log('全局IM初始化成功') console.log('全局IM初始化成功')
return true return true
} }