From c66514e5b3f0075b4ca067299a483969a7c57c45 Mon Sep 17 00:00:00 2001
From: wangdongbo <949818794@qq.com>
Date: Wed, 28 Jan 2026 13:38:05 +0800
Subject: [PATCH] =?UTF-8?q?Im=20=E7=9B=B8=E5=85=B3=E4=BF=AE=E6=94=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.env.development | 1 +
.env.localhost | 1 +
.hbuilderx/launch.json | 16 +
package-lock.json | 14 +-
package.json | 8 +-
pages.json | 13 +
pages/home/consult.vue | 210 ++
pages/home/customer-archive.vue | 5 +
pages/home/home.vue | 11 +-
pages/home/select-consultant-popup.vue | 316 ++
pages/message/article-detail.vue | 201 ++
pages/message/article-list.vue | 588 ++++
pages/message/chat.scss | 1360 ++++++++
pages/message/common-phrases.vue | 1058 +++++++
pages/message/components/chat-input.vue | 454 +++
pages/message/components/consult-accept.vue | 88 +
pages/message/components/consult-cancel.vue | 70 +
pages/message/components/head-card.vue | 59 +
pages/message/components/message-types.vue | 282 ++
.../components/reject-reason-modal.vue | 297 ++
.../components/special-message/evaluation.vue | 182 ++
.../components/special-message/index.vue | 40 +
pages/message/components/system-message.vue | 158 +
pages/message/hooks/use-group-chat.js | 62 +
pages/message/index.vue | 931 ++++++
pages/message/message.vue | 542 +++-
pages/message/survey-list.vue | 446 +++
store/account.js | 41 +-
utils/api.js | 30 +-
utils/chat-utils.js | 798 +++++
utils/im-status-manager.js | 228 ++
utils/tim-chat.js | 2816 +++++++++++++++++
32 files changed, 11310 insertions(+), 16 deletions(-)
create mode 100644 .hbuilderx/launch.json
create mode 100644 pages/home/consult.vue
create mode 100644 pages/home/select-consultant-popup.vue
create mode 100644 pages/message/article-detail.vue
create mode 100644 pages/message/article-list.vue
create mode 100644 pages/message/chat.scss
create mode 100644 pages/message/common-phrases.vue
create mode 100644 pages/message/components/chat-input.vue
create mode 100644 pages/message/components/consult-accept.vue
create mode 100644 pages/message/components/consult-cancel.vue
create mode 100644 pages/message/components/head-card.vue
create mode 100644 pages/message/components/message-types.vue
create mode 100644 pages/message/components/reject-reason-modal.vue
create mode 100644 pages/message/components/special-message/evaluation.vue
create mode 100644 pages/message/components/special-message/index.vue
create mode 100644 pages/message/components/system-message.vue
create mode 100644 pages/message/hooks/use-group-chat.js
create mode 100644 pages/message/index.vue
create mode 100644 pages/message/survey-list.vue
create mode 100644 utils/chat-utils.js
create mode 100644 utils/im-status-manager.js
create mode 100644 utils/tim-chat.js
diff --git a/.env.development b/.env.development
index 9928b9d..9b23129 100644
--- a/.env.development
+++ b/.env.development
@@ -1,3 +1,4 @@
MP_API_BASE_URL=http://localhost:8080
MP_CACHE_PREFIX=development
MP_WX_APP_ID=wx93af55767423938e
+MP_TIM_SDK_APP_ID=1600123876
diff --git a/.env.localhost b/.env.localhost
index 24011fd..5058981 100644
--- a/.env.localhost
+++ b/.env.localhost
@@ -1,3 +1,4 @@
MP_API_BASE_URL=http://192.168.60.2:8080
MP_CACHE_PREFIX=development
MP_WX_APP_ID=wx93af55767423938e
+MP_TIM_SDK_APP_ID=1600123876
diff --git a/.hbuilderx/launch.json b/.hbuilderx/launch.json
new file mode 100644
index 0000000..81f13f4
--- /dev/null
+++ b/.hbuilderx/launch.json
@@ -0,0 +1,16 @@
+{ // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
+ // launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
+ "version": "0.0",
+ "configurations": [{
+ "default" :
+ {
+ "launchtype" : "local"
+ },
+ "mp-weixin" :
+ {
+ "launchtype" : "local"
+ },
+ "type" : "uniCloud"
+ }
+ ]
+}
diff --git a/package-lock.json b/package-lock.json
index eae5086..db80b11 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,9 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
- "dayjs": "^1.11.10"
+ "dayjs": "^1.11.10",
+ "tim-upload-plugin": "^1.4.2",
+ "tim-wx-sdk": "^2.27.6"
},
"devDependencies": {}
},
@@ -17,6 +19,16 @@
"version": "1.11.19",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz",
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="
+ },
+ "node_modules/tim-upload-plugin": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmmirror.com/tim-upload-plugin/-/tim-upload-plugin-1.4.3.tgz",
+ "integrity": "sha512-3ZmbA36dr3eG9YGDon9MLBUtbNawYWkL+TBa+VS0Uviguc7PlVSOIVRG2C4irXX16slDT2Kj+HAZapp+Xqp2xg=="
+ },
+ "node_modules/tim-wx-sdk": {
+ "version": "2.27.6",
+ "resolved": "https://registry.npmmirror.com/tim-wx-sdk/-/tim-wx-sdk-2.27.6.tgz",
+ "integrity": "sha512-zB+eRdmigdhEDeqrXC0bLJonUQZzS5uKNPLFtrje503WAnmuxVQjq/n4Zle4FYHG4FiKHKhsrVd0aCYXABlFEg=="
}
}
}
diff --git a/package.json b/package.json
index e7519bb..aec6c20 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,9 @@
"author": "",
"license": "ISC",
"dependencies": {
- "dayjs": "^1.11.10"
+ "dayjs": "^1.11.10",
+ "tim-upload-plugin": "^1.4.2",
+ "tim-wx-sdk": "^2.27.6"
},
"uni-app": {
"scripts": {
@@ -20,7 +22,7 @@
"UNI_PLATFORM": "mp-weixin"
}
},
- "localhost": {
+ "localhost": {
"title": "本地",
"env": {
"UNI_PLATFORM": "mp-weixin"
@@ -29,4 +31,4 @@
}
},
"devDependencies": {}
-}
+}
\ No newline at end of file
diff --git a/pages.json b/pages.json
index d5f4544..fe4f1ed 100644
--- a/pages.json
+++ b/pages.json
@@ -7,6 +7,19 @@
"navigationStyle": "custom"
}
},
+ {
+ "path": "pages/message/message",
+ "style": {
+ "navigationBarTitleText": "消息"
+ }
+ },
+ {
+ "path": "pages/message/index",
+ "style": {
+ "navigationBarTitleText": "聊天",
+ "enablePullDownRefresh": false
+ }
+ },
{
"path": "pages/login/login",
"style": {
diff --git a/pages/home/consult.vue b/pages/home/consult.vue
new file mode 100644
index 0000000..a02c910
--- /dev/null
+++ b/pages/home/consult.vue
@@ -0,0 +1,210 @@
+
+
+ 咨询互动
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/home/customer-archive.vue b/pages/home/customer-archive.vue
index 6b8157f..bf01873 100644
--- a/pages/home/customer-archive.vue
+++ b/pages/home/customer-archive.vue
@@ -86,6 +86,8 @@ const props = defineProps({
}
})
+const emit = defineEmits(['update:customers']);
+
const { account } = storeToRefs(useAccount());
const current = ref(null);
const customers = ref([]);
@@ -144,6 +146,9 @@ async function getCustomers() {
customers.value = res && Array.isArray(res.data) ? res.data : [];
const customer = customers.value.find(i => current.value && i._id === current.value._id);
current.value = customer || customers.value[0] || null;
+
+ // 向父组件传递 customers 数据
+ emit('update:customers', customers.value);
} else {
toast(res.message || '获取档案失败');
}
diff --git a/pages/home/home.vue b/pages/home/home.vue
index 89165a9..d84891e 100644
--- a/pages/home/home.vue
+++ b/pages/home/home.vue
@@ -5,7 +5,8 @@
-
+
+
@@ -21,6 +22,7 @@ import { toast } from '@/utils/widget';
import FullPage from '@/components/full-page.vue';
import articleList from './article-list.vue';
+import consult from './consult.vue';
import customerArchive from './customer-archive.vue';
import teamHead from './team-head.vue';
import teamMate from './team-mate.vue';
@@ -32,10 +34,15 @@ const { account } = storeToRefs(useAccount());
const team = ref(null);
const teams = ref([]);
-const loading = ref(true)
+const loading = ref(true);
+const customers = ref([]);
const corpId = computed(() => team.value?.corpId);
+function handleCustomersUpdate(newCustomers) {
+ customers.value = newCustomers;
+}
+
async function changeTeam({ teamId, corpId, corpName }) {
loading.value = true;
const res = await api('getTeamData', { teamId, corpId });
diff --git a/pages/home/select-consultant-popup.vue b/pages/home/select-consultant-popup.vue
new file mode 100644
index 0000000..eee752a
--- /dev/null
+++ b/pages/home/select-consultant-popup.vue
@@ -0,0 +1,316 @@
+
+
+
+
+
+
+
+
+
diff --git a/pages/message/article-detail.vue b/pages/message/article-detail.vue
new file mode 100644
index 0000000..e6bfb76
--- /dev/null
+++ b/pages/message/article-detail.vue
@@ -0,0 +1,201 @@
+
+
+
+
+ 加载中...
+
+
+
+ {{ error }}
+
+
+
+
+
+ {{ articleData.title }}
+ {{ articleData.date }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/message/article-list.vue b/pages/message/article-list.vue
new file mode 100644
index 0000000..3c54a8f
--- /dev/null
+++ b/pages/message/article-list.vue
@@ -0,0 +1,588 @@
+
+
+
+
+
+
+
+
+
+
+
+ 加载中...
+
+
+
+
+
+
+
+
+
+ {{ article.title }}
+
+ {{ article.date }}
+
+
+
+
+
+
+
+ 加载中...
+
+
+
+ 没有更多了
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/message/chat.scss b/pages/message/chat.scss
new file mode 100644
index 0000000..d35fe0c
--- /dev/null
+++ b/pages/message/chat.scss
@@ -0,0 +1,1360 @@
+// SCSS 变量定义
+$font-size-text: 28rpx;
+$font-size-tip: 24rpx;
+$font-size-title: 32rpx;
+$text-color-sub: #999;
+$primary-color: #0877F1;
+
+.chat-page {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ flex-direction: column;
+ background-color: #f5f5f5;
+}
+
+.chat-content {
+ flex: 1;
+ box-sizing: border-box;
+ overflow-x: hidden;
+}
+
+.chat-content-compressed {
+ height: calc(100vh - 600rpx);
+}
+
+.waiting-consultation-card {
+ position: relative;
+ z-index: 2;
+ margin-top: 32rpx;
+ background: #fff;
+ border-radius: 16rpx;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.waiting-card-main {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+}
+
+.waiting-illust {
+ width: 72rpx;
+ height: 72rpx;
+ border-radius: 16rpx;
+ background: #f5f7fb;
+ object-fit: contain;
+ margin-right: 16rpx;
+}
+
+.waiting-card-info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: center;
+ min-width: 0;
+}
+
+.waiting-title {
+ font-size: $font-size-text;
+ color: #000;
+ font-weight: 600;
+ margin-bottom: 8rpx;
+ line-height: 1.2;
+ display: flex;
+ align-items: center;
+}
+
+.waiting-desc {
+ color: #78808f;
+ font-size: $font-size-text;
+ margin: 32rpx;
+ line-height: 24px;
+}
+
+.waiting-btn-wrap {
+ width: 100%;
+ box-sizing: border-box;
+ padding: 0 32rpx;
+ margin-bottom: 42rpx;
+ display: flex;
+ justify-content: center;
+}
+
+.waiting-btn {
+ width: 100%;
+ background: linear-gradient(270deg, #1b5cc8 2.26%, #0877f1 94.33%);
+ color: #fff;
+ font-size: 28rpx;
+ font-weight: 600;
+ border-radius: 40rpx;
+ height: 80rpx;
+ line-height: 80rpx;
+ box-shadow: 0 2rpx 8rpx rgba(59, 124, 255, 0.08);
+ border: none;
+ text-align: center;
+ padding: 0;
+}
+
+.doctor-avatar-outer {
+ margin-left: 48rpx;
+ position: relative;
+}
+
+.doctor-avatar {
+ position: absolute;
+ bottom: -10rpx;
+ width: 140rpx;
+ height: 140rpx;
+ border-radius: 140rpx;
+ box-shadow: 0 4px 7.3px 0 #00000014;
+ background: #fff;
+ margin-left: 24rpx;
+}
+
+.system-message {
+ border-radius: 16rpx;
+ padding: 12rpx;
+ margin: 20rpx 24rpx;
+ text-align: center;
+ position: relative;
+ z-index: 2;
+}
+
+.system-text {
+ color: $text-color-sub;
+ font-size: $font-size-tip;
+ line-height: 1.4;
+}
+
+.message-list {
+ padding: 0 16rpx;
+ padding-bottom: 60rpx; /* 增加底部内边距,防止被小程序底部横线遮挡 */
+}
+
+.message-item {
+ margin-bottom: 30rpx;
+}
+
+.message-content {
+ display: flex;
+ justify-content: flex-start;
+ align-items: flex-start;
+ gap: 20rpx;
+ max-width: 100%;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+}
+
+.message-right .message-content {
+ flex-direction: row-reverse;
+}
+
+.doctor-msg-avatar {
+ width: 60rpx;
+ height: 60rpx;
+ border-radius: 50%;
+ margin-top: 10rpx;
+}
+
+.user-msg-avatar {
+ width: 60rpx;
+ height: 60rpx;
+ border-radius: 50%;
+ margin-top: 10rpx;
+}
+
+// 消息气泡容器
+.message-bubble-container {
+ display: flex;
+ flex-direction: column;
+ max-width: 70%;
+}
+
+// 用户名标签样式
+.username-label {
+ margin-bottom: 6rpx;
+
+ &.left {
+ text-align: left;
+ }
+
+ &.right {
+ text-align: right;
+ }
+}
+
+.username-text {
+ font-size: 22rpx;
+ color: #999;
+ line-height: 1.2;
+}
+
+.message-bubble {
+ min-width: 60rpx;
+ padding: 12rpx 16rpx;
+ border-radius: 18rpx;
+ position: relative;
+ word-wrap: break-word;
+ word-break: break-all;
+ overflow-wrap: break-word;
+}
+
+.doctor-bubble {
+ background-color: white;
+ color: #333;
+}
+
+.doctor-bubble::before {
+ content: "";
+ position: absolute;
+ top: 18rpx;
+ left: -18rpx;
+ width: 0;
+ height: 0;
+ border-left: 10rpx solid transparent;
+ border-bottom: 10rpx solid transparent;
+ border-right: 10rpx solid white;
+ border-top: 10rpx solid transparent;
+}
+
+.user-bubble {
+ background-color: #0877F1;
+ color: white;
+}
+
+.user-bubble::after {
+ content: "";
+ position: absolute;
+ top: 18rpx;
+ right: -18rpx;
+ width: 0;
+ height: 0;
+ border-left: 10rpx solid #0877F1;
+ border-bottom: 10rpx solid transparent;
+ border-right: 10rpx solid transparent;
+ border-top: 10rpx solid transparent;
+}
+
+/* 医生发送的蓝色气泡(用于消息卡片) */
+.doctor-bubble-blue {
+ background-color: #0877F1;
+ color: white;
+}
+
+.doctor-bubble-blue::before {
+ content: "";
+ position: absolute;
+ top: 18rpx;
+ left: -18rpx;
+ width: 0;
+ height: 0;
+ border-left: 10rpx solid transparent;
+ border-bottom: 10rpx solid transparent;
+ border-right: 10rpx solid #0877F1;
+ border-top: 10rpx solid transparent;
+}
+
+.message-text {
+ font-size: $font-size-text;
+ line-height: 1.4;
+ word-wrap: break-word;
+ word-break: break-all;
+ white-space: pre-wrap;
+ overflow-wrap: break-word;
+}
+
+.message-time {
+ font-size: $font-size-tip;
+ opacity: 0.7;
+ margin-top: 4rpx;
+ text-align: right;
+}
+
+.message-system {
+ text-align: center;
+ margin: 8rpx 0;
+}
+
+.input-section {
+ background: #fff;
+ border-top: 1rpx solid #e0e0e0;
+ position: relative;
+ z-index: 200; // 确保输入区域在评价卡片之上,但在弹窗之下
+ // padding: 32rpx 16rpx 28rpx 16rpx;
+}
+
+.input-toolbar {
+ display: flex;
+ align-items: center;
+ padding: 28rpx 0 28rpx 0;
+}
+.voice-toggle-btn{
+ height: 56rpx;
+ display: flex;
+ align-items: center;
+ flex: none;
+ padding-left: 20rpx;
+
+}
+.plus-btn {
+ width: 56rpx;
+ height: 56rpx;
+ display: flex;
+ align-items: center;
+ flex: none;
+ padding-right: 20rpx;
+}
+
+.send-btn {
+ background: #3876f6;
+ color: #fff;
+ font-size: 28rpx;
+ font-weight: 600;
+ border: none;
+ border-radius: 40rpx;
+ height: 64rpx;
+ min-width: 112rpx;
+ padding: 0 32rpx;
+ margin-left: 16rpx;
+ box-shadow: 0 2rpx 8rpx rgba(56, 118, 246, 0.08);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.2s;
+ flex: none;
+}
+
+.send-btn:active {
+ background: #2456c7;
+}
+
+.input-area {
+ flex: 1;
+ margin: 0 8rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 0;
+}
+
+.text-input,
+.voice-input-btn {
+ flex: 1;
+ padding: 0 46rpx;
+ background-color: #f3f5fa;
+ border-radius: 20rpx;
+ margin: 0 16rpx;
+ font-size: 28rpx;
+ height: 80rpx;
+ border: none;
+ outline: none;
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ line-height: 96rpx;
+ color: #333;
+}
+
+.voice-input-btn {
+ text-align: center;
+}
+
+.more-panel {
+ display: flex;
+ justify-content: flex-start;
+ background: #fff;
+ border-top: 1rpx solid #eee;
+ padding: 20rpx 0 40rpx 60rpx;
+ gap: 40rpx 50rpx;
+ flex-wrap: wrap;
+ background-color: #f5f5f5;
+}
+
+.more-btn {
+ flex: none;
+ width: 120rpx;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.more-icon {
+ width: 108rpx;
+ height: 108rpx;
+ border-radius: 16rpx;
+}
+
+.more-btn text {
+ margin-top: 20rpx;
+ font-size: $font-size-text;
+ color: #6b7a8b;
+}
+
+.consultation-bar {
+ background-color: white;
+ border-top: 1rpx solid #e0e0e0;
+ padding: 16rpx;
+ margin-bottom: env(safe-area-inset-bottom);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.1);
+ border-radius: 16rpx;
+ z-index: 10; // 降低 z-index,避免遮挡弹窗组件
+}
+
+.consultation-info {
+ display: flex;
+ align-items: center;
+ flex: 1;
+}
+
+.consultation-icon {
+ margin-right: 12rpx;
+}
+
+.consultation-details {
+ display: flex;
+ flex-direction: column;
+}
+
+.consultation-title {
+ color: #333;
+ font-size: $font-size-text;
+ font-weight: 600;
+ margin-bottom: 4rpx;
+}
+
+.consultation-price {
+ color: #ff3b30;
+ font-size: $font-size-tip;
+ font-weight: 500;
+}
+
+.consultation-btn {
+ background-color: $primary-color;
+ color: white;
+ border: none;
+ border-radius: 16rpx;
+ height: 88rpx;
+ font-size: $font-size-title;
+ font-weight: 400;
+ transition: all 0.3s;
+}
+
+.consultation-btn:active {
+ background-color: $primary-color;
+ transform: scale(0.98);
+}
+
+.consultation-btn::after {
+ border: none;
+}
+
+/* 评价弹窗包裹器 - 最高优先级 */
+.evaluation-popup-wrapper {
+ z-index: 100000 !important;
+}
+
+/* 深度选择器,确保 uni-popup 内部元素也应用最高 z-index */
+:deep(.evaluation-popup-wrapper) {
+ z-index: 100000 !important;
+}
+
+:deep(.evaluation-popup-wrapper .uni-popup) {
+ z-index: 100000 !important;
+}
+
+:deep(.evaluation-popup-wrapper .uni-popup__wrapper) {
+ z-index: 100000 !important;
+}
+
+:deep(.evaluation-popup-wrapper .uni-popup__mask) {
+ z-index: 99999 !important;
+}
+
+.evaluation-card {
+ background-color: white;
+ border-radius: 12rpx;
+ margin: 16rpx;
+ padding: 16rpx;
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
+ border: 1rpx solid #e3f2fd;
+ position: relative;
+ z-index: 10; // 确保卡片在聊天内容之上,但低于弹出框遮罩层
+}
+
+.evaluation-content {
+ display: flex;
+ align-items: center;
+}
+
+.evaluation-icon {
+ margin-right: 12rpx;
+}
+
+.evaluation-info {
+ flex: 1;
+}
+
+.evaluation-title {
+ color: #333;
+ font-size: $font-size-text;
+ font-weight: 600;
+ display: block;
+ margin-bottom: 4rpx;
+}
+
+.evaluation-subtitle {
+ color: #666;
+ font-size: $font-size-tip;
+}
+
+.evaluation-btn {
+ background-color: $primary-color;
+ border-radius: 16rpx;
+ padding: 12rpx 20rpx;
+}
+
+.btn-text {
+ color: white;
+ font-size: $font-size-text;
+ font-weight: 400;
+}
+
+.evaluation-popup-container {
+ width: 100vw;
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ position: relative;
+ z-index: 100001 !important; // 确保弹窗在最上层,使用 !important 覆盖其他样式
+}
+
+/* 评价弹窗样式 */
+.evaluation-popup {
+ background-color: white;
+ border-top-left-radius: 16rpx;
+ border-top-right-radius: 16rpx;
+ width: 100%;
+ max-height: 80vh;
+ padding: 20rpx;
+ padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
+ position: relative;
+ box-sizing: border-box;
+ margin: 0;
+ z-index: 100002 !important; // 确保弹窗内容在最上层
+}
+
+.popup-indicator {
+ width: 40rpx;
+ height: 4rpx;
+ background-color: #e0e0e0;
+ border-radius: 2rpx;
+ margin: 0 auto 16rpx;
+}
+
+.popup-close {
+ position: absolute;
+ top: 16rpx;
+ right: 16rpx;
+ z-index: 10;
+}
+
+.doctor-info-section {
+ text-align: center;
+ margin-bottom: 24rpx;
+}
+
+.doctor-avatar-large {
+ width: 60rpx;
+ height: 60rpx;
+ border-radius: 50%;
+ margin-bottom: 12rpx;
+}
+
+.doctor-name {
+ display: block;
+ color: #333;
+ font-size: $font-size-text;
+ font-weight: 500;
+ margin-bottom: 4rpx;
+}
+
+.doctor-dept {
+ color: $text-color-sub;
+ font-size: $font-size-tip;
+}
+
+.rating-section {
+ text-align: center;
+ margin-bottom: 24rpx;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+/* uni-rate 包装器样式 */
+.rate-wrapper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+}
+
+/* uni-rate 样式调整 */
+.rating-section .uni-rate {
+ display: flex !important;
+ justify-content: center !important;
+ align-items: center !important;
+}
+
+.rating-text {
+ display: block;
+ font-size: $font-size-text;
+ font-weight: 600;
+ margin-top: 12rpx;
+ color: $primary-color;
+}
+
+/* 当评分为0时的提示样式 */
+.rating-text.no-rating {
+ color: #999;
+}
+
+.comment-section {
+ margin-bottom: 24rpx;
+ position: relative;
+}
+
+.evaluation-textarea {
+ width: 100%;
+ min-height: 120rpx;
+ background-color: #f8f9fa;
+ border: 1rpx solid #e9ecef;
+ border-radius: 16rpx;
+ padding: 12rpx;
+ font-size: $font-size-tip;
+ line-height: 1.5;
+ box-sizing: border-box;
+ resize: none;
+ margin: 0;
+}
+
+.char-count {
+ position: absolute;
+ bottom: 8rpx;
+ right: 12rpx;
+ color: #999;
+ font-size: $font-size-tip;
+}
+
+.evaluation-footer {
+ text-align: center;
+}
+
+.submit-evaluation-btn {
+ background-color: $primary-color;
+ color: white;
+ border: none;
+ border-radius: 16rpx;
+ padding: 12rpx 40rpx;
+ font-size: $font-size-text;
+ font-weight: 600;
+ width: 100%;
+ box-sizing: border-box;
+ margin: 0;
+}
+
+/* 评价成功弹窗样式 */
+.success-popup {
+ background-color: white;
+ width: auto;
+ padding: 32rpx 20rpx 48rpx 20rpx;
+ padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
+ text-align: center;
+ margin: 0 auto;
+ position: relative;
+ z-index: 100002 !important; // 确保弹窗在最上层
+}
+
+.success-icon {
+ margin-bottom: 16rpx;
+}
+
+.success-title {
+ display: block;
+ color: #333;
+ font-size: $font-size-text;
+ font-weight: 500;
+ margin-bottom: 8rpx;
+}
+
+.success-eval-box {
+ background: #f5f5f5;
+ border-radius: 16rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
+ padding: 32rpx 20rpx 24rpx 20rpx;
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.success-subtitle {
+ color: #666;
+ font-size: $font-size-tip;
+ margin-bottom: 20rpx;
+}
+
+.success-rating {
+ margin-bottom: 16rpx;
+}
+
+.success-rating .uni-rate {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.success-rating-text {
+ display: block;
+ color: $primary-color;
+ font-size: $font-size-tip;
+ font-weight: 500;
+ margin-top: 8rpx;
+}
+
+.success-comment {
+ background-color: #f8f9fa;
+ border-radius: 16rpx;
+ padding: 12rpx;
+ margin-top: 16rpx;
+ width: 100%;
+ box-sizing: border-box;
+ word-break: break-all;
+ white-space: pre-wrap;
+ overflow-wrap: break-word;
+}
+
+.comment-text {
+ color: $text-color-sub;
+ font-size: $font-size-tip;
+ line-height: 1.4;
+ word-break: break-all;
+ white-space: pre-wrap;
+ overflow-wrap: break-word;
+}
+
+.success-header-row {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12rpx;
+ margin-bottom: 8rpx;
+}
+
+.success-icon {
+ margin-bottom: 0;
+}
+
+.success-title {
+ display: inline-block;
+ color: #333;
+ font-size: $font-size-text;
+ font-weight: 500;
+ margin-bottom: 0;
+}
+
+/* 导航栏下方添加倒计时条 */
+.consult-countdown-bar {
+ position: fixed;
+ top: 0; // 导航栏高度,根据实际情况调整
+ left: 0;
+ right: 0;
+ width: 100%;
+ background: #ffd6d6;
+ color: #e53935;
+ text-align: center;
+ padding: 12rpx 0;
+ font-size: $font-size-text;
+ font-weight: 500;
+ border-bottom: none;
+ z-index: 100; // 确保悬浮在聊天内容上方
+ box-shadow: 0 2rpx 8rpx rgba(229, 57, 53, 0.1); // 添加轻微阴影,增强悬浮效果
+}
+
+.countdown-text {
+ color: #e53935;
+}
+
+.waiting-section {
+ position: relative;
+ width: 100%;
+ overflow: hidden;
+ margin-top: -32rpx;
+}
+
+.waiting-bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 200rpx;
+ background-color: #0976f0;
+ border-bottom-left-radius: 50rpx;
+ border-bottom-right-radius: 50rpx;
+ z-index: 1;
+}
+
+.waiting-card-outer {
+ position: relative;
+ z-index: 2;
+ margin: 0 24rpx;
+ margin-top: 0;
+ padding-bottom: 20rpx;
+}
+
+.hospital-name {
+ color: #fff;
+ font-size: $font-size-text;
+ font-weight: 500;
+ line-height: 48rpx;
+ margin: 32rpx 0 16rpx 8rpx;
+ position: relative;
+ z-index: 2;
+}
+
+/* 加载更多提示样式 */
+.load-more-tip {
+ padding: 20rpx;
+ text-align: center;
+}
+
+.loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12rpx;
+}
+
+.loading-icon {
+ font-size: 28rpx;
+ animation: rotate 1s linear infinite;
+}
+
+@keyframes rotate {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.loading-text {
+ font-size: 24rpx;
+ color: #999;
+}
+
+.load-tip {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.tip-text {
+ font-size: 24rpx;
+ color: #999;
+}
+
+.completed-text {
+ font-size: 24rpx;
+ color: #ccc;
+}
+
+.waiting-card-row {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ height: 140rpx;
+ background: linear-gradient(-30.3deg, #cbdaff 0%, #e8f3ff 100%);
+ border-top-left-radius: 16rpx;
+ border-top-right-radius: 16rpx;
+}
+
+.waiting-illust {
+ width: 116rpx;
+ height: 120rpx;
+ background: transparent;
+ object-fit: contain;
+ margin: 10rpx 18rpx;
+}
+
+.waiting-card-center {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-width: 0;
+ padding: 0 8rpx;
+}
+
+.waiting-desc {
+ color: #999;
+ font-size: 24rpx;
+ margin-bottom: 24rpx;
+ line-height: 1.4;
+ text-align: center;
+}
+
+.text-input {
+ color: #000;
+}
+
+.text-input::-webkit-input-placeholder,
+.text-input:-moz-placeholder,
+.text-input::-moz-placeholder,
+.text-input:-ms-input-placeholder,
+.text-input::placeholder {
+ line-height: 96rpx;
+}
+
+/* 时间分割线 */
+.time-divider {
+ text-align: center;
+ margin: 20rpx 0;
+}
+
+.time-text {
+ color: #999;
+ font-size: 22rpx;
+}
+
+/* 消息状态 */
+.message-status {
+ margin-left: 12rpx;
+ align-self: flex-end;
+}
+
+.status-text {
+ font-size: 22rpx;
+ color: #999;
+}
+
+.status-text.failed {
+ color: #ff4d4f;
+}
+
+.voice-message {
+ min-width: 100rpx;
+ padding: 0rpx 8rpx;
+ border-radius: 16rpx;
+ transition: all 0.2s;
+ display: flex;
+
+ &.voice-playing {
+ transform: scale(1.02);
+ }
+}
+
+.voice-message:active {
+ background-color: rgba(255, 255, 255, 0.2);
+}
+
+.voice-content {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 8rpx;
+}
+
+.voice-icon-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8rpx;
+ position: relative;
+}
+
+.icon-animate {
+ animation: voice-icon-pulse 1.5s ease-in-out infinite;
+}
+
+@keyframes voice-icon-pulse {
+ 0%, 100% {
+ opacity: 1;
+ transform: scale(1);
+ }
+ 50% {
+ opacity: 0.7;
+ transform: scale(1.1);
+ }
+}
+
+.sound-wave {
+ display: flex;
+ align-items: center;
+ gap: 4rpx;
+ margin-left: 4rpx;
+}
+
+.wave-bar {
+ width: 4rpx;
+ height: 24rpx;
+ border-radius: 2rpx;
+ animation: wave-animation 1s ease-in-out infinite;
+}
+
+@keyframes wave-animation {
+ 0%, 100% {
+ height: 8rpx;
+ }
+ 50% {
+ height: 24rpx;
+ }
+}
+
+.voice-duration {
+ font-size: 24rpx;
+ color: inherit;
+}
+
+/* 图片消息样式 */
+.message-image {
+ max-width: 400rpx; /* 限制最大宽度 */
+ max-height: 400rpx; /* 限制最大高度,参考微信 */
+ min-height: 120rpx; /* 设置最小高度 */
+ min-width: 200rpx; /* 设置最小宽度 */
+ border-radius: 12rpx;
+ object-fit: cover; /* 保持图片比例,裁剪多余部分 */
+}
+
+/* 图片消息气泡 - 无背景色 */
+.image-bubble {
+ background: transparent !important;
+ padding: 0 !important;
+ border-radius: 0 !important;
+}
+
+/* 移除图片消息气泡的小三角 */
+.image-bubble::before,
+.image-bubble::after {
+ display: none !important;
+}
+
+.message-right .message-card {
+ margin-right: 8rpx;
+}
+
+.message-left .message-card {
+ margin-left: 8rpx;
+}
+
+.card-avatar-row {
+ display: flex;
+ align-items: flex-start;
+ gap: 8rpx;
+}
+
+.test-load-btn {
+ position: fixed;
+ bottom: 200rpx;
+ left: 50%;
+ transform: translateX(-50%);
+ background-color: $primary-color;
+ color: white;
+ font-size: 28rpx;
+ font-weight: 600;
+ border-radius: 40rpx;
+ height: 80rpx;
+ line-height: 80rpx;
+ padding: 0 40rpx;
+ box-shadow: 0 2rpx 8rpx rgba(56, 118, 246, 0.08);
+ z-index: 10;
+}
+
+.test-btn-text {
+ color: white;
+}
+
+/* 录音遮罩层样式 */
+.recording-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 9999; // 录音遮罩层低于评价弹窗(99999)
+ animation: fadeIn 0.2s ease-in-out;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+.recording-modal {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background: rgba(40, 40, 40, 0.95);
+ border-radius: 32rpx;
+ padding: 80rpx 100rpx;
+ backdrop-filter: blur(10rpx);
+ animation: scaleIn 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
+ transition: all 0.3s ease;
+}
+
+.recording-modal.cancel-mode {
+ background: rgba(60, 60, 60, 0.95);
+ border: 4rpx solid #ff4757;
+}
+
+@keyframes scaleIn {
+ 0% {
+ transform: scale(0.5);
+ opacity: 0;
+ }
+ 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+
+.recording-icon-container {
+ position: relative;
+ width: 200rpx;
+ height: 200rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 40rpx;
+}
+
+/* 波纹动画 */
+.wave-circle {
+ position: absolute;
+ width: 200rpx;
+ height: 200rpx;
+ border-radius: 50%;
+ border: 4rpx solid rgba(255, 71, 87, 0.8);
+ animation: wave 1.5s ease-out infinite;
+}
+
+.wave-1 { animation-delay: 0s; }
+.wave-2 { animation-delay: 0.5s; }
+.wave-3 { animation-delay: 1s; }
+
+@keyframes wave {
+ 0% {
+ transform: scale(0.5);
+ opacity: 1;
+ }
+ 100% {
+ transform: scale(1.5);
+ opacity: 0;
+ }
+}
+
+.mic-icon-wrapper {
+ position: relative;
+ z-index: 10;
+ width: 120rpx;
+ height: 120rpx;
+ background: linear-gradient(135deg, #ff4757 0%, #ff6b7a 100%);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 8rpx 24rpx rgba(255, 71, 87, 0.4);
+ animation: pulse 1.5s ease-in-out infinite;
+ transition: all 0.3s ease;
+}
+
+.mic-icon-wrapper.cancel-icon {
+ background: linear-gradient(135deg, #666 0%, #888 100%);
+ animation: shake 0.5s ease-in-out infinite;
+ box-shadow: 0 8rpx 24rpx rgba(102, 102, 102, 0.4);
+}
+
+@keyframes shake {
+ 0%, 100% { transform: rotate(-5deg); }
+ 50% { transform: rotate(5deg); }
+}
+
+@keyframes pulse {
+ 0%, 100% { transform: scale(1); }
+ 50% { transform: scale(1.05); }
+}
+
+.recording-text {
+ font-size: 32rpx;
+ color: #fff;
+ font-weight: 600;
+ margin-bottom: 20rpx;
+ letter-spacing: 2rpx;
+ transition: all 0.3s ease;
+}
+
+.recording-text.cancel-text {
+ color: #ff4757;
+ font-size: 34rpx;
+}
+
+.recording-hint {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.7);
+ margin-bottom: 24rpx;
+}
+
+.recording-duration {
+ font-size: 48rpx;
+ color: #ff4757;
+ font-weight: bold;
+ letter-spacing: 2rpx;
+ text-shadow: 0 2rpx 8rpx rgba(255, 71, 87, 0.3);
+}
+
+.cancel-hint {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+ font-size: 28rpx;
+ color: #ff4757;
+ font-weight: 600;
+ margin-top: 20rpx;
+ animation: bounce 0.8s ease-in-out infinite;
+}
+
+@keyframes bounce {
+ 0%, 100% { transform: translateY(0); }
+ 50% { transform: translateY(-10rpx); }
+}
+
+/* 文章卡片样式 */
+.article-card {
+ display: flex;
+ align-items: center;
+ background-color: transparent;
+ border-radius: 12rpx;
+ padding: 24rpx;
+ max-width: 500rpx;
+ box-shadow: none;
+ background-color: #fff;
+}
+
+.article-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ margin-right: 20rpx;
+ min-width: 0;
+}
+
+.article-title {
+ font-size: 28rpx;
+ color: #333;
+ font-weight: 500;
+ line-height: 1.4;
+ margin-bottom: 8rpx;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+}
+
+.article-desc {
+ font-size: 24rpx;
+ color: #999;
+ line-height: 1.3;
+}
+
+.article-image {
+ width: 120rpx;
+ height: 120rpx;
+ border-radius: 8rpx;
+ flex-shrink: 0;
+}
+
+/* 文章卡片在不同消息流中的样式 */
+.message-right .article-card {
+ background-color: transparent;
+}
+
+.message-left .article-card {
+ background-color: transparent;
+}
+
+/* 问卷卡片样式 */
+.survey-card {
+ display: flex;
+ align-items: center;
+ background-color: #fff;
+ border-radius: 12rpx;
+ padding: 24rpx;
+ max-width: 500rpx;
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
+}
+
+.survey-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ margin-right: 20rpx;
+ min-width: 0;
+}
+
+.survey-title {
+ font-size: 28rpx;
+ color: #333;
+ font-weight: 500;
+ line-height: 1.4;
+ margin-bottom: 8rpx;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+}
+
+.survey-desc {
+ font-size: 24rpx;
+ color: #999;
+ line-height: 1.3;
+}
+
+.survey-image {
+ width: 120rpx;
+ height: 120rpx;
+ border-radius: 8rpx;
+ flex-shrink: 0;
+}
+
+/* 问卷卡片在不同消息流中的样式 */
+.message-right .survey-card {
+ background-color: #e8f4ff;
+}
+
+.message-left .survey-card {
+ background-color: #fff;
+}
diff --git a/pages/message/common-phrases.vue b/pages/message/common-phrases.vue
new file mode 100644
index 0000000..3f5bde1
--- /dev/null
+++ b/pages/message/common-phrases.vue
@@ -0,0 +1,1058 @@
+
+
+
+
+
+
+
+
+
+
+ {{ phrase.content }}
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
+ 暂无常用语
+ 点击下方按钮添加常用语
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/message/components/chat-input.vue b/pages/message/components/chat-input.vue
new file mode 100644
index 0000000..72024ec
--- /dev/null
+++ b/pages/message/components/chat-input.vue
@@ -0,0 +1,454 @@
+
+
+
+
+ {{ '' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ btn.text }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ isCancelMode ? '松开手指,取消录音' : '正在录音...' }}
+
+ 松开发送,上滑取消
+ {{ recordingDuration }}s
+
+
+ 已上滑
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/message/components/consult-accept.vue b/pages/message/components/consult-accept.vue
new file mode 100644
index 0000000..c593baa
--- /dev/null
+++ b/pages/message/components/consult-accept.vue
@@ -0,0 +1,88 @@
+
+
+
+
+ 患者已发起咨询申请,请及时接诊
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/message/components/consult-cancel.vue b/pages/message/components/consult-cancel.vue
new file mode 100644
index 0000000..d3d334c
--- /dev/null
+++ b/pages/message/components/consult-cancel.vue
@@ -0,0 +1,70 @@
+
+
+
+
+ 您的咨询申请已发送,等待医生接诊
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/message/components/head-card.vue b/pages/message/components/head-card.vue
new file mode 100644
index 0000000..568b64e
--- /dev/null
+++ b/pages/message/components/head-card.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+ {{ order.hospitalName || '医院' }}
+
+
+
+
+ 等待医生接诊.....
+
+
+
+
+
+
+ 为了更好的获得医生帮助,请补充病情描述
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/message/components/message-types.vue b/pages/message/components/message-types.vue
new file mode 100644
index 0000000..4c19114
--- /dev/null
+++ b/pages/message/components/message-types.vue
@@ -0,0 +1,282 @@
+
+
+
+ {{ message.payload.text }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ message.payload.second }}"
+
+
+
+
+
+
+
+
+ {{ getArticleData(message).title }}
+ {{ getArticleData(message).desc }}
+
+
+
+
+
+
+
+ {{ getSurveyData(message).title }}
+ {{ getSurveyData(message).desc }}
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/message/components/reject-reason-modal.vue b/pages/message/components/reject-reason-modal.vue
new file mode 100644
index 0000000..df589f8
--- /dev/null
+++ b/pages/message/components/reject-reason-modal.vue
@@ -0,0 +1,297 @@
+
+
+
+
+
+
+
+
+
+ {{ option }}
+
+ ✓
+
+
+
+
+
+ 填写拒诊理由
+
+ ›
+
+
+
+
+
+
+
+ {{ customReason.length }}/200
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/message/components/special-message/evaluation.vue b/pages/message/components/special-message/evaluation.vue
new file mode 100644
index 0000000..72054c1
--- /dev/null
+++ b/pages/message/components/special-message/evaluation.vue
@@ -0,0 +1,182 @@
+
+
+
+
+
+
+
+
+ 医生满意度评价
+ 您对医生的本次服务满意吗?
+
+
+ 评价
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/message/components/special-message/index.vue b/pages/message/components/special-message/index.vue
new file mode 100644
index 0000000..f71641d
--- /dev/null
+++ b/pages/message/components/special-message/index.vue
@@ -0,0 +1,40 @@
+
+
+
+
\ No newline at end of file
diff --git a/pages/message/components/system-message.vue b/pages/message/components/system-message.vue
new file mode 100644
index 0000000..de4a907
--- /dev/null
+++ b/pages/message/components/system-message.vue
@@ -0,0 +1,158 @@
+
+
+
+ {{ text }}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/message/hooks/use-group-chat.js b/pages/message/hooks/use-group-chat.js
new file mode 100644
index 0000000..e550907
--- /dev/null
+++ b/pages/message/hooks/use-group-chat.js
@@ -0,0 +1,62 @@
+import { ref, computed } from 'vue'
+import { onShow, onUnload } from '@dcloudio/uni-app'
+
+/**
+ * 简单的群聊hook
+ * @param {string} groupID 群组ID
+ */
+export default function useGroupChat(groupID) {
+ const groupInfo = ref({})
+ const members = ref([])
+
+ // 群聊成员映射
+ const chatMember = computed(() => {
+ const res = {}
+ members.value.forEach(member => {
+ res[member.id] = {
+ name: member.name,
+ avatar: member.avatar || '/static/default-avatar.png'
+ }
+ })
+ return res
+ })
+
+ // 获取群聊信息
+ async function getGroupInfo() {
+ const gid = typeof groupID === 'string' ? groupID : groupID.value
+ if (!gid) return
+
+ try {
+ // 这里可以调用API获取群聊信息
+ // const res = await getGroupDetail(gid)
+ // if (res && res.success) {
+ // groupInfo.value = res.data
+ // members.value = res.data.members || []
+ // }
+
+ // 暂时使用本地数据
+ groupInfo.value = {
+ groupID: gid,
+ name: '群聊',
+ status: 'active'
+ }
+ } catch (error) {
+ console.error('获取群聊信息失败:', error)
+ }
+ }
+
+ onShow(() => {
+ getGroupInfo()
+ })
+
+ onUnload(() => {
+ // 清理资源
+ })
+
+ return {
+ groupInfo,
+ members,
+ chatMember,
+ getGroupInfo
+ }
+}
diff --git a/pages/message/index.vue b/pages/message/index.vue
new file mode 100644
index 0000000..8cb1442
--- /dev/null
+++ b/pages/message/index.vue
@@ -0,0 +1,931 @@
+
+
+
+
+
+
+
+ 加载中...
+
+
+ ↑ 上滑加载更多
+
+
+ 已加载全部消息
+
+
+
+
+
+
+
+
+ {{ formatTime(message.lastTime) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ chatMember[message.from]?.name
+ }}
+
+
+
+
+
+ handleViewDetail(message)"
+ />
+
+
+
+
+
+ 发送失败
+
+
+
+
+
+
+
+
+
+
+
+
+
+ scrollToBottom(true)"
+ @messageSent="() => scrollToBottom(true)"
+ @endConsult="handleEndConsult"
+ />
+
+
+
+
+
+
diff --git a/pages/message/message.vue b/pages/message/message.vue
index fb6b3e7..a9a9e3c 100644
--- a/pages/message/message.vue
+++ b/pages/message/message.vue
@@ -1,9 +1,539 @@
- message
+
+
+
+
+
+ 加载中...
+
+
+
+
+
+
+
+ {{
+ conversation.unreadCount > 99 ? "99+" : conversation.unreadCount
+ }}
+
+
+
+
+
+
+ {{
+ conversation.lastMessage || "暂无消息"
+ }}
+
+
+
+
+
+
+
+ 暂无消息
+
+
+
+
+ {{
+ loadingMore ? "加载中..." : "上拉加载更多"
+ }}
+
+
+
-
-
-
-
\ No newline at end of file
+
+
diff --git a/pages/message/survey-list.vue b/pages/message/survey-list.vue
new file mode 100644
index 0000000..53a5261
--- /dev/null
+++ b/pages/message/survey-list.vue
@@ -0,0 +1,446 @@
+
+
+
+
+
+
+
+
+
+
+
+ 加载中...
+
+
+
+
+
+
+
+
+
+ {{ survey.name }}
+ {{
+ survey.description || "暂无问卷说明"
+ }}
+
+
+
+
+
+
+
+
+ 加载中...
+
+
+
+ 没有更多了
+
+
+
+
+
+
+
+
+
+
+
diff --git a/store/account.js b/store/account.js
index 58ddeed..5b5e04f 100644
--- a/store/account.js
+++ b/store/account.js
@@ -2,6 +2,7 @@ import { ref } from "vue";
import { defineStore } from "pinia";
import api from '@/utils/api';
import { toast } from '@/utils/widget';
+import { initGlobalTIM, globalTimChatManager } from "@/utils/tim-chat.js";
const env = __VITE_ENV__;
@@ -9,8 +10,10 @@ export default defineStore("accountStore", () => {
const appid = env.MP_WX_APP_ID;
const account = ref(null);
const loading = ref(false)
-
+ const isIMInitialized = ref(false);
+ const openid = ref("");
async function login(phoneCode = '') {
+
if (loading.value) return;
loading.value = true;
@@ -28,6 +31,8 @@ export default defineStore("accountStore", () => {
loading.value = false
if (res.success && res.data && res.data.mobile) {
account.value = res.data;
+ openid.value = res.data.openid;
+ initIMAfterLogin(openid.value)
return res.data
}
}
@@ -38,5 +43,37 @@ export default defineStore("accountStore", () => {
loading.value = false
}
- return { account, login }
+ async function initIMAfterLogin(userID) {
+ if (isIMInitialized.value) {
+ return true;
+ }
+ try {
+ await initGlobalTIM(userID);
+ isIMInitialized.value = true;
+ return true;
+ } catch (error) {
+ console.error('IM初始化失败:', error);
+ return false;
+ }
+ }
+ // 退出登录
+ async function logout() {
+ try {
+ // 退出腾讯IM
+ if (globalTimChatManager && globalTimChatManager.tim) {
+ console.log('开始退出腾讯IM');
+ await globalTimChatManager.destroy();
+ console.log('腾讯IM退出成功');
+ }
+ } catch (error) {
+ console.error('退出腾讯IM失败:', error);
+ }
+
+ // 清空账户信息
+ account.value = null;
+ openid.value = "";
+ isIMInitialized.value = false;
+ }
+
+ return { account, login, initIMAfterLogin, logout, openid, isIMInitialized }
})
\ No newline at end of file
diff --git a/utils/api.js b/utils/api.js
index 1c54db8..42e729c 100644
--- a/utils/api.js
+++ b/utils/api.js
@@ -14,7 +14,23 @@ const urlsConfig = {
knowledgeBase: {
getArticleByIds: 'getArticleByIds',
- getPageDisease: "getPageDisease"
+ getCommonPhrases: 'getCommonPhrases',
+ saveCommonPhrase: 'saveCommonPhrase',
+ deleteCommonPhrase: 'deleteCommonPhrase',
+ getCommonPhraseCategories: 'getCommonPhraseCategories',
+ saveCommonPhraseCategory: 'saveCommonPhraseCategory',
+ // 个人常用语接口
+ getPersonalPhrases: 'getPersonalPhrases',
+ savePersonalPhrase: 'savePersonalPhrase',
+ deletePersonalPhrase: 'deletePersonalPhrase',
+ getPersonalPhraseCategories: 'getPersonalPhraseCategories',
+ savePersonalPhraseCategory: 'savePersonalPhraseCategory',
+ deletePersonalPhraseCategory: 'deletePersonalPhraseCategory',
+ // 宣教文章接口
+ getArticleCateList: 'getArticleCateList',
+ getArticleList: 'getArticleList',
+ getArticle: 'getArticle',
+ addArticleSendRecord: 'addArticleSendRecord'
},
member: {
addCustomer: 'add',
@@ -32,6 +48,15 @@ const urlsConfig = {
},
wecom: {
addContactWay: 'addContactWay'
+ },
+ im: {
+ getUserSig: 'getUserSig',
+ sendSystemMessage: "sendSystemMessage",
+ getChatRecordsByGroupId: "getChatRecordsByGroupId",
+ sendConsultRejectedMessage: "sendConsultRejectedMessage",
+ endConsultation: "endConsultation",
+ getGroupListByGroupId: "getGroupListByGroupId",
+ createConsultGroup: "createConsultGroup"
}
}
const urls = Object.keys(urlsConfig).reduce((acc, path) => {
@@ -61,4 +86,5 @@ export default async function api(urlId, data = {}, loading = true) {
type,
}
}, loading)
-}
\ No newline at end of file
+}
+
diff --git a/utils/chat-utils.js b/utils/chat-utils.js
new file mode 100644
index 0000000..c2d5231
--- /dev/null
+++ b/utils/chat-utils.js
@@ -0,0 +1,798 @@
+/**
+ * 聊天相关工具函数
+ */
+
+// 通用消息提示
+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} 是否有权限
+ */
+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,最大值 600000(10 分钟)
+ 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",
+ });
+ }
+ },
+ });
+ }
+};
\ No newline at end of file
diff --git a/utils/im-status-manager.js b/utils/im-status-manager.js
new file mode 100644
index 0000000..7fc2ee3
--- /dev/null
+++ b/utils/im-status-manager.js
@@ -0,0 +1,228 @@
+import {
+ checkGlobalIMStatus,
+ ensureGlobalIMConnection,
+ getGlobalIMLoginStatus
+} from './tim-chat.js'
+
+/**
+ * 全局IM状态管理器
+ * 提供统一的IM状态检测和管理接口
+ */
+class IMStatusManager {
+ constructor() {
+ this.statusCheckInterval = null
+ this.isMonitoring = false
+ this.checkIntervalTime = 60000 // 默认1分钟检查一次
+ this.lastCheckTime = 0
+ this.callbacks = {
+ onStatusChange: [],
+ onReconnectSuccess: [],
+ onReconnectFailed: []
+ }
+ }
+
+ /**
+ * 启动IM状态监控
+ * @param {number} intervalTime 检查间隔时间(毫秒)
+ */
+ startMonitoring(intervalTime = 60000) {
+ if (this.isMonitoring) {
+ console.log('IM状态监控已在运行')
+ return
+ }
+
+ this.checkIntervalTime = intervalTime
+ this.isMonitoring = true
+
+ // 立即检查一次
+ this.checkIMStatus()
+
+ // 启动定时检查
+ this.statusCheckInterval = setInterval(() => {
+ this.checkIMStatus()
+ }, this.checkIntervalTime)
+
+ console.log(`IM状态监控已启动,检查间隔:${intervalTime / 1000}秒`)
+ }
+
+ /**
+ * 停止IM状态监控
+ */
+ stopMonitoring() {
+ if (this.statusCheckInterval) {
+ clearInterval(this.statusCheckInterval)
+ this.statusCheckInterval = null
+ }
+ this.isMonitoring = false
+ console.log('IM状态监控已停止')
+ }
+
+ /**
+ * 检查IM状态
+ */
+ async checkIMStatus() {
+ const now = Date.now()
+ this.lastCheckTime = now
+
+ try {
+ console.log('执行IM状态检查...')
+ const isLoggedIn = checkGlobalIMStatus()
+
+ // 触发状态变化回调
+ this.triggerCallbacks('onStatusChange', {
+ isLoggedIn,
+ checkTime: now,
+ timestamp: new Date().toLocaleString()
+ })
+
+ if (!isLoggedIn) {
+ console.log('检测到IM未登录,尝试重连...')
+ await this.attemptReconnect()
+ } else {
+ console.log('IM状态正常')
+ }
+
+ return isLoggedIn
+ } catch (error) {
+ console.error('IM状态检查异常:', error)
+ return false
+ }
+ }
+
+ /**
+ * 尝试重连IM
+ */
+ async attemptReconnect() {
+ try {
+ console.log('开始尝试IM重连...')
+ const success = await ensureGlobalIMConnection()
+
+ if (success) {
+ console.log('IM重连成功')
+ this.triggerCallbacks('onReconnectSuccess', {
+ timestamp: new Date().toLocaleString()
+ })
+
+ } else {
+ console.log('IM重连失败')
+ this.triggerCallbacks('onReconnectFailed', {
+ timestamp: new Date().toLocaleString()
+ })
+ }
+
+ return success
+ } catch (error) {
+ console.error('IM重连异常:', error)
+ this.triggerCallbacks('onReconnectFailed', {
+ error,
+ timestamp: new Date().toLocaleString()
+ })
+ return false
+ }
+ }
+
+ /**
+ * 手动触发IM连接检查
+ */
+ async forceCheck() {
+ console.log('手动触发IM连接检查')
+ return await this.checkIMStatus()
+ }
+
+ /**
+ * 获取当前IM登录状态
+ */
+ getCurrentStatus() {
+ return {
+ isLoggedIn: getGlobalIMLoginStatus(),
+ isMonitoring: this.isMonitoring,
+ lastCheckTime: this.lastCheckTime,
+ checkInterval: this.checkIntervalTime
+ }
+ }
+
+ /**
+ * 添加状态变化回调
+ */
+ onStatusChange(callback) {
+ if (typeof callback === 'function') {
+ this.callbacks.onStatusChange.push(callback)
+ }
+ }
+
+ /**
+ * 添加重连成功回调
+ */
+ onReconnectSuccess(callback) {
+ if (typeof callback === 'function') {
+ this.callbacks.onReconnectSuccess.push(callback)
+ }
+ }
+
+ /**
+ * 添加重连失败回调
+ */
+ onReconnectFailed(callback) {
+ if (typeof callback === 'function') {
+ this.callbacks.onReconnectFailed.push(callback)
+ }
+ }
+
+ /**
+ * 移除回调
+ */
+ removeCallback(type, callback) {
+ if (this.callbacks[type]) {
+ const index = this.callbacks[type].indexOf(callback)
+ if (index > -1) {
+ this.callbacks[type].splice(index, 1)
+ }
+ }
+ }
+
+ /**
+ * 触发回调
+ */
+ triggerCallbacks(type, data) {
+ if (this.callbacks[type]) {
+ this.callbacks[type].forEach(callback => {
+ try {
+ callback(data)
+ } catch (error) {
+ console.error(`执行${type}回调失败:`, error)
+ }
+ })
+ }
+ }
+
+ /**
+ * 获取状态报告
+ */
+ getStatusReport() {
+ const status = this.getCurrentStatus()
+ return {
+ ...status,
+ report: {
+ isLoggedIn: status.isLoggedIn ? '已登录' : '未登录',
+ monitoring: status.isMonitoring ? '监控中' : '未监控',
+ lastCheck: status.lastCheckTime ?
+ new Date(status.lastCheckTime).toLocaleString() : '从未检查',
+ interval: `${status.checkInterval / 1000}秒`
+ }
+ }
+ }
+}
+
+// 创建全局实例
+const globalIMStatusManager = new IMStatusManager()
+
+// 便捷函数
+export const startIMMonitoring = (interval) => globalIMStatusManager.startMonitoring(interval)
+export const stopIMMonitoring = () => globalIMStatusManager.stopMonitoring()
+export const checkIMStatusNow = () => globalIMStatusManager.forceCheck()
+export const getIMStatus = () => globalIMStatusManager.getCurrentStatus()
+export const getIMStatusReport = () => globalIMStatusManager.getStatusReport()
+
+// 导出管理器实例和类
+export { globalIMStatusManager, IMStatusManager }
+export default globalIMStatusManager
\ No newline at end of file
diff --git a/utils/tim-chat.js b/utils/tim-chat.js
new file mode 100644
index 0000000..4ed5d28
--- /dev/null
+++ b/utils/tim-chat.js
@@ -0,0 +1,2816 @@
+// 引入腾讯IM SDK
+import TIM from 'tim-wx-sdk'
+import TIMUploadPlugin from 'tim-upload-plugin'
+import api from './api.js'
+
+const env = __VITE_ENV__;
+
+// 腾讯IM配置 - SDKAppID 必须是 number 类型,使用 Number() 转换
+const TIM_CONFIG = {
+ SDKAppID: Number(env.MP_TIM_SDK_APP_ID), // 患者端 IM SDKAppID
+}
+
+// 验证配置
+if (!TIM_CONFIG.SDKAppID || isNaN(TIM_CONFIG.SDKAppID)) {
+ console.error('❌ TIM SDK配置错误: MP_TIM_SDK_APP_ID未定义或无效', {
+ envValue: env.MP_TIM_SDK_APP_ID,
+ parsedValue: TIM_CONFIG.SDKAppID
+ })
+}
+
+// IM连接配置常量
+const IM_CONNECTION_CONFIG = {
+ MAX_RECONNECT_ATTEMPTS: 10, // 最大重连次数
+ RECONNECT_DELAYS: [2000, 4000, 8000, 16000, 30000], // 重连延迟(指数退避)
+ LOGIN_COOLDOWN: 5000, // 登录冷却时间
+ SDK_READY_TIMEOUT: 15000, // SDK就绪超时时间
+ LOGIN_CHECK_INTERVAL_STABLE: 60000, // 稳定状态检查间隔
+ LOGIN_CHECK_INTERVAL_UNSTABLE: 15000, // 不稳定状态检查间隔
+ LOGIN_CHECK_FIRST_DELAY: 30000, // 首次检查延迟
+ HEARTBEAT_INTERVAL: 60000, // 心跳间隔(毫秒)60秒
+ HEARTBEAT_MAX_FAIL: 3, // 心跳最大失败次数
+ NETWORK_RECONNECT_DELAY: 2000, // 网络恢复后延迟重连
+ MESSAGE_BATCH_COUNT: 20, // 每批消息数量
+ MAX_MESSAGE_REQUESTS: 50, // 最大消息请求次数
+ MAX_CACHE_SIZE: 1000, // 最大缓存消息数
+ TIM_INSTANCE_READY_CHECK_INTERVAL: 100 // TIM实例就绪检查间隔
+}
+
+class TimChatManager {
+ constructor() {
+ // TIM实例和会话
+ this.tim = null
+ this.conversation = null
+
+ // 用户信息
+ this.currentUserID = ''
+ this.currentUserSig = ''
+
+ // 消息管理
+ this.messageList = []
+ this.currentConversationID = null
+
+ // 分页加载相关状态
+ this.nextReqMessageID = ""
+ this.isCompleted = false
+ this.isLoadingMore = false
+
+ // 状态标志
+ this.isLoading = false
+ this.isLoggedIn = false
+ this.isInitializing = false
+ this.isLoggingIn = false
+
+ // 定时器
+ this.loginCheckInterval = null
+ this.heartbeatInterval = null
+ this.networkReconnectTimer = null
+
+ // 重连管理
+ this.reconnectAttempts = 0
+ this.maxReconnectAttempts = IM_CONNECTION_CONFIG.MAX_RECONNECT_ATTEMPTS
+ this.reconnectDelays = IM_CONNECTION_CONFIG.RECONNECT_DELAYS
+ this.lastLoginTime = 0
+ this.loginCooldown = IM_CONNECTION_CONFIG.LOGIN_COOLDOWN
+
+ // 心跳管理
+ this.heartbeatFailCount = 0
+
+ // 回调函数
+ this.callbacks = {
+ onMessageReceived: null,
+ onMessageSent: null,
+ onSDKReady: null,
+ onSDKNotReady: null,
+ onError: null,
+ onLoginStatusChanged: null,
+ onConversationListUpdated: null
+ }
+
+ // 绑定事件处理函数
+ this.boundEventHandlers = {
+ onSDKReady: this.onSDKReady.bind(this),
+ onSDKNotReady: this.onSDKNotReady.bind(this),
+ onMessageReceived: this.onMessageReceived.bind(this),
+ onMessageSentSucceeded: this.onMessageSentSucceeded.bind(this),
+ onMessageSentFailed: this.onMessageSentFailed.bind(this),
+ onConversationListUpdated: this.onConversationListUpdated.bind(this),
+ onNetStateChange: this.onNetStateChange.bind(this),
+ onKickedOut: this.onKickedOut.bind(this)
+ }
+ }
+
+ // ============== 资源管理方法 ==============
+
+ // 清理所有定时器
+ clearAllTimers() {
+ if (this.loginCheckInterval) {
+ clearTimeout(this.loginCheckInterval)
+ clearInterval(this.loginCheckInterval)
+ this.loginCheckInterval = null
+ }
+
+ if (this.heartbeatInterval) {
+ clearInterval(this.heartbeatInterval)
+ this.heartbeatInterval = null
+ }
+
+ if (this.networkReconnectTimer) {
+ clearTimeout(this.networkReconnectTimer)
+ this.networkReconnectTimer = null
+ }
+
+ console.log('所有定时器已清理')
+ }
+
+ // 清理缓存(已弃用缓存功能)
+ cleanupCache() {
+ // 缓存功能已移除
+ }
+
+ // 重置所有状态
+ resetAllStates() {
+ this.isLoggedIn = false
+ this.isLoggingIn = false
+ this.isInitializing = false
+ this.reconnectAttempts = 0
+ this.lastLoginTime = 0
+ this.heartbeatFailCount = 0
+ }
+
+ // ============== 初始化方法 ==============
+
+ // 初始化腾讯IM
+ async initTIM(userID = null) {
+ if (this.isInitializing) {
+ console.log('IM正在初始化中,跳过重复初始化')
+ return false
+ }
+
+ this.isInitializing = true
+ console.log('=== 开始初始化IM ===')
+
+ try {
+ // 重置重连次数,允许重新登录
+ this.reconnectAttempts = 0
+
+ // 如果存在旧的TIM实例,先完整清理
+ if (this.tim) {
+ console.log('检测到旧的TIM实例,开始清理...')
+ await this.cleanupOldInstance()
+ }
+
+ if (!TIM) {
+ throw new Error('TIM SDK 未正确导入')
+ }
+ // 验证 SDKAppID
+ if (!TIM_CONFIG.SDKAppID || isNaN(TIM_CONFIG.SDKAppID)) {
+ throw new Error(`TIM SDK配置错误: SDKAppID无效 (${TIM_CONFIG.SDKAppID}),请检查环境变量 MP_TIM_SDK_APP_ID`)
+ }
+
+ console.log('创建TIM实例,SDKAppID:', TIM_CONFIG.SDKAppID)
+ this.tim = TIM.create({ SDKAppID: TIM_CONFIG.SDKAppID })
+
+ // 等待TIM实例初始化完成
+ await this.waitForTIMInstanceReady()
+
+ // 注册上传插件
+ this.tim.registerPlugin({ "tim-upload-plugin": TIMUploadPlugin })
+ console.log('上传插件已注册')
+
+ // 注册事件监听器
+ this.registerEventListeners()
+
+ // 设置日志级别
+ if (typeof this.tim.setLogLevel === 'function') {
+ this.tim.setLogLevel(0)
+ }
+
+ // 获取用户信息并登录
+ await this.getUserInfoAndLogin(userID)
+
+ // 等待SDK Ready
+ console.log('等待SDK Ready...')
+ await this.waitForSDKReady(IM_CONNECTION_CONFIG.SDK_READY_TIMEOUT)
+
+ console.log('=== IM初始化完成 ===')
+ return true
+
+ } catch (error) {
+ console.error('=== IM初始化失败 ===', error)
+ this.triggerCallback('onError', `初始化失败: ${error.message || error}`)
+
+ // 初始化失败时清理资源
+ await this.cleanupOldInstance()
+ return false
+ } finally {
+ this.isInitializing = false
+ }
+ }
+
+ // 清理旧的TIM实例
+ async cleanupOldInstance() {
+ try {
+ // 清理所有定时器
+ this.clearAllTimers()
+
+ // 移除事件监听器
+ this.removeEventListeners()
+
+ // 登出
+ if (this.tim && this.tim.isLoggedIn) {
+ try {
+ await this.tim.logout()
+ console.log('已登出旧实例')
+ } catch (err) {
+ console.warn('登出旧实例失败:', err)
+ }
+ }
+
+ // 清理实例
+ this.tim = null
+
+ // 重置状态
+ this.resetAllStates()
+
+ console.log('旧实例清理完成')
+ } catch (error) {
+ console.error('清理旧实例时出错:', error)
+ }
+ }
+
+ // 等待TIM实例准备就绪
+ waitForTIMInstanceReady() {
+ return new Promise((resolve, reject) => {
+ const startTime = Date.now()
+ const timeout = 10000 // 10秒超时
+
+ const checkReady = () => {
+ if (this.tim && typeof this.tim.on === 'function') {
+ console.log('TIM实例已就绪')
+ resolve()
+ } else if (Date.now() - startTime > timeout) {
+ reject(new Error('等待TIM实例就绪超时'))
+ } else {
+ setTimeout(checkReady, IM_CONNECTION_CONFIG.TIM_INSTANCE_READY_CHECK_INTERVAL)
+ }
+ }
+
+ checkReady()
+ })
+ }
+
+ // 等待SDK Ready(优化版:更好的超时处理)
+ waitForSDKReady(timeout = IM_CONNECTION_CONFIG.SDK_READY_TIMEOUT) {
+ return new Promise((resolve, reject) => {
+ const startTime = Date.now()
+ const checkInterval = 1000 // 每秒检查一次
+ let checkCount = 0
+
+ const checkSDKReady = () => {
+ checkCount++
+ const elapsed = Date.now() - startTime
+
+ if (this.isLoggedIn) {
+ console.log(`✓ SDK已Ready(耗时${elapsed}ms,检查${checkCount}次)`)
+ resolve()
+ } else if (elapsed > timeout) {
+ const error = new Error(`等待SDK Ready超时(${timeout}ms)`)
+ console.error('✗', error.message)
+ // 超时不算致命错误,尝试继续
+ resolve()
+ } else {
+ console.log(`等待SDK Ready... ${Math.floor(elapsed / 1000)}/${Math.floor(timeout / 1000)}秒`)
+ setTimeout(checkSDKReady, checkInterval)
+ }
+ }
+
+ // 立即开始第一次检查
+ checkSDKReady()
+ })
+ }
+
+ // 注册事件监听器
+ registerEventListeners() {
+ if (!this.tim || !TIM.EVENT) return
+
+ try {
+ // 检查TIM实例是否已经初始化完成
+ if (!this.tim || typeof this.tim.on !== 'function') {
+ console.log('TIM实例未准备好,延迟注册事件监听器')
+ setTimeout(() => this.registerEventListeners(), 100)
+ return
+ }
+
+ // 逐个注册事件监听器,如果某个注册失败则继续注册其他的
+ const events = [
+ { event: TIM.EVENT.SDK_READY, handler: this.boundEventHandlers.onSDKReady },
+ { event: TIM.EVENT.SDK_NOT_READY, handler: this.boundEventHandlers.onSDKNotReady },
+ { event: TIM.EVENT.MESSAGE_RECEIVED, handler: this.boundEventHandlers.onMessageReceived },
+ { event: TIM.EVENT.MESSAGE_SENT_SUCCEEDED, handler: this.boundEventHandlers.onMessageSentSucceeded },
+ { event: TIM.EVENT.MESSAGE_SENT_FAILED, handler: this.boundEventHandlers.onMessageSentFailed },
+ { event: TIM.EVENT.CONVERSATION_LIST_UPDATED, handler: this.boundEventHandlers.onConversationListUpdated },
+ { event: TIM.EVENT.NET_STATE_CHANGE, handler: this.boundEventHandlers.onNetStateChange },
+ { event: TIM.EVENT.KICKED_OUT, handler: this.boundEventHandlers.onKickedOut }
+ ]
+
+ events.forEach(({ event, handler }) => {
+ if (event && handler && typeof this.tim.on === 'function') {
+ try {
+ this.tim.on(event, handler)
+ } catch (error) {
+ console.error(`注册事件监听器失败 ${event}:`, error)
+ }
+ }
+ })
+
+ console.log('TIM事件监听器注册完成')
+ } catch (error) {
+ console.error('注册TIM事件监听器失败:', error)
+ }
+ }
+
+ // 移除事件监听器
+ removeEventListeners() {
+ if (!this.tim || !TIM.EVENT) return
+
+ try {
+ // 检查TIM实例是否还有off方法
+ if (typeof this.tim.off !== 'function') return
+
+ // 逐个移除事件监听器,如果某个移除失败则继续移除其他的
+ const events = [
+ { event: TIM.EVENT.SDK_READY, handler: this.boundEventHandlers.onSDKReady },
+ { event: TIM.EVENT.SDK_NOT_READY, handler: this.boundEventHandlers.onSDKNotReady },
+ { event: TIM.EVENT.MESSAGE_RECEIVED, handler: this.boundEventHandlers.onMessageReceived },
+ { event: TIM.EVENT.MESSAGE_SENT_SUCCEEDED, handler: this.boundEventHandlers.onMessageSentSucceeded },
+ { event: TIM.EVENT.MESSAGE_SENT_FAILED, handler: this.boundEventHandlers.onMessageSentFailed },
+ { event: TIM.EVENT.CONVERSATION_LIST_UPDATED, handler: this.boundEventHandlers.onConversationListUpdated },
+ { event: TIM.EVENT.NET_STATE_CHANGE, handler: this.boundEventHandlers.onNetStateChange }
+ ]
+
+ events.forEach(({ event, handler }) => {
+ if (event && handler && typeof this.tim.off === 'function') {
+ try {
+ this.tim.off(event, handler)
+ } catch (error) {
+ console.error(`移除事件监听器失败 ${event}:`, error)
+ }
+ }
+ })
+ } catch (error) {
+ console.error('移除TIM事件监听器失败:', error)
+ }
+ }
+
+ async getUserInfoAndLogin(userID) {
+ try {
+ if (userID) {
+ this.currentUserID = userID
+ uni.setStorageSync('userInfo', { userID })
+ } else {
+ const userInfo = uni.getStorageSync('userInfo')
+ if (!userInfo?.userID) {
+ throw new Error('未找到用户信息,请先登录')
+ }
+ this.currentUserID = userInfo.userID
+ }
+ this.currentUserSig = await this.getUserSig(this.currentUserID)
+ await this.loginTIM()
+ } catch (error) {
+ console.error('获取用户信息失败:', error)
+ this.triggerCallback('onError', `登录失败: ${error.message || error}`)
+ throw error // 重新抛出错误,让调用者知道登录失败
+ }
+ }
+
+ // 获取 userSig
+ async getUserSig(userID) {
+ try {
+ const response = await api('getUserSig', { userId: userID })
+ if (response?.success && response?.data) {
+ return response.data
+ }
+ throw new Error('获取 userSig 失败: 接口返回数据格式错误')
+ } catch (error) {
+ console.error('获取UserSig失败:', error)
+ throw error
+ }
+ }
+
+ // 登录腾讯IM
+ loginTIM() {
+ return new Promise((resolve, reject) => {
+ if (this.isLoggingIn) {
+ reject(new Error('正在登录中'))
+ return
+ }
+
+ const now = Date.now()
+ if (now - this.lastLoginTime < this.loginCooldown) {
+ reject(new Error('登录冷却中'))
+ return
+ }
+
+ if (this.isLoggedIn) {
+ resolve()
+ return
+ }
+
+ this.isLoggingIn = true
+ this.lastLoginTime = now
+
+ console.log('开始登录腾讯IM,userID:', this.currentUserID)
+ this.tim.login({
+ userID: this.currentUserID,
+ userSig: this.currentUserSig
+ }).then(() => {
+ console.log('腾讯IM登录请求成功,等待SDK_READY事件...')
+ this.isLoggingIn = false
+ // 不在这里设置 isLoggedIn = true,等待 onSDKReady 事件
+ this.reconnectAttempts = 0
+
+ // 触发登录状态变化回调(登录中状态)
+ this.triggerCallback('onLoginStatusChanged', {
+ isLoggedIn: false,
+ userID: this.currentUserID,
+ reason: 'LOGIN_REQUESTED',
+ message: '登录请求成功,等待SDK就绪...'
+ })
+ resolve()
+ }).catch(error => {
+ console.error('腾讯IM登录失败:', error)
+ this.isLoggingIn = false
+ this.triggerCallback('onLoginStatusChanged', {
+ isLoggedIn: false,
+ error,
+ reason: 'LOGIN_FAILED'
+ })
+ reject(error)
+ })
+ })
+ }
+
+ // ============== 连接监控方法 ==============
+
+ // 启动登录状态检测(优化版:使用配置常量)
+ startLoginStatusCheck() {
+ this.stopLoginStatusCheck()
+
+ // 根据连接稳定性动态调整检查间隔
+ const getCheckInterval = () => {
+ if (this.reconnectAttempts > 0) {
+ return IM_CONNECTION_CONFIG.LOGIN_CHECK_INTERVAL_UNSTABLE
+ }
+ return IM_CONNECTION_CONFIG.LOGIN_CHECK_INTERVAL_STABLE
+ }
+
+ const checkStatus = () => {
+ // 如果已达到最大重连次数(被踢下线),停止检查
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
+ console.log('⚠️ 已被踢下线或达到最大重连次数,停止登录状态检查')
+ return
+ }
+
+ // 如果正在登录中,跳过本次检查
+ if (this.isLoggingIn) {
+ this.loginCheckInterval = setTimeout(checkStatus, getCheckInterval())
+ return
+ }
+
+ const isLoggedIn = this.checkLoginStatus()
+
+ // 如果未登录且有用户ID,尝试重连
+ if (!isLoggedIn && this.currentUserID && !this.isLoggingIn) {
+ console.log('📡 登录状态检测:发现未登录,尝试重连')
+ this.attemptReconnect()
+ } else if (isLoggedIn) {
+ // 登录正常,重置重连计数
+ if (this.reconnectAttempts > 0) {
+ console.log('✓ 登录状态检测:连接已恢复正常')
+ this.reconnectAttempts = 0
+ }
+
+ // 定期清理缓存
+ this.cleanupCache()
+ }
+
+ // 重新调度下一次检查(再次检查,防止在检查过程中被踢下线)
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
+ this.loginCheckInterval = setTimeout(checkStatus, getCheckInterval())
+ }
+ }
+
+ // 首次检查延迟
+ const firstDelay = IM_CONNECTION_CONFIG.LOGIN_CHECK_FIRST_DELAY
+ this.loginCheckInterval = setTimeout(checkStatus, firstDelay)
+ console.log(`🔍 登录状态检测已启动,首次检查将在${firstDelay / 1000}秒后进行`)
+ }
+
+ // 停止登录状态检测
+ stopLoginStatusCheck() {
+ if (this.loginCheckInterval) {
+ clearTimeout(this.loginCheckInterval)
+ clearInterval(this.loginCheckInterval)
+ this.loginCheckInterval = null
+ }
+ }
+
+ // 检查登录状态
+ checkLoginStatus() {
+ if (!this.tim) return false
+
+ try {
+ // 【修复】优先信任已设置的登录状态,只有在明确检测到未登录时才更新
+ // 如果已经标记为登录状态,不应该被轻易覆盖
+ if (this.isLoggedIn) {
+ // 只在已登录状态下,通过 SDK 再次验证
+ let sdkLoginStatus = true // 默认保持登录状态
+
+ if (typeof this.tim.getLoginStatus === 'function') {
+ try {
+ const status = this.tim.getLoginStatus()
+ // 只有明确检测到未登录状态才更新
+ if (status === TIM.TYPES.NOT_LOGGED_IN || status === TIM.TYPES.KICKED_OUT) {
+ sdkLoginStatus = false
+ console.warn('SDK 检测到未登录状态:', status)
+ }
+ } catch (e) {
+ console.error('获取 SDK 登录状态失败:', e)
+ // 出错时保持当前状态
+ }
+ }
+
+ // 只有 SDK 明确返回未登录时才更新状态
+ if (!sdkLoginStatus) {
+ this.isLoggedIn = false
+ this.triggerCallback('onLoginStatusChanged', {
+ isLoggedIn: false,
+ userID: this.currentUserID,
+ reason: 'SDK_STATUS_CHECK'
+ })
+ return false
+ }
+
+ // 保持登录状态
+ return true
+ }
+
+ // 如果当前是未登录状态,检查 SDK 是否已登录
+ let loginStatus = false
+
+ if (typeof this.tim.isLoggedIn === 'boolean') {
+ loginStatus = this.tim.isLoggedIn
+ } else if (typeof this.tim.getLoginStatus === 'function') {
+ const status = this.tim.getLoginStatus()
+ loginStatus = status === TIM.TYPES.LOGINED || status === TIM.TYPES.LOGGING_IN
+ }
+
+ // 只有在检测到登录状态变化时才更新
+ if (loginStatus && !this.isLoggedIn) {
+ this.isLoggedIn = true
+ this.triggerCallback('onLoginStatusChanged', {
+ isLoggedIn: true,
+ userID: this.currentUserID,
+ reason: 'SDK_STATUS_CHECK'
+ })
+ }
+
+ return loginStatus
+ } catch (error) {
+ console.error('检查IM登录状态失败:', error)
+ // 出错时返回当前状态,不改变状态
+ return this.isLoggedIn
+ }
+ }
+
+ // 尝试重新连接(优化版:更智能的重连策略)
+ async attemptReconnect() {
+ // 如果已达到最大重连次数(包括被踢下线的情况),停止重连
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
+ console.log('⚠️ 已达到最大重连次数或被踢下线,停止自动重连')
+ return false
+ }
+
+ if (this.isLoggingIn) {
+ console.log('正在登录中,跳过重连')
+ return false
+ }
+
+ // 先检查是否真的需要重连
+ if (this.tim && this.isLoggedIn) {
+ console.log('IM已登录,无需重连')
+ this.reconnectAttempts = 0
+ return true
+ }
+
+ const now = Date.now()
+ const delayIndex = Math.min(this.reconnectAttempts, this.reconnectDelays.length - 1)
+ const reconnectDelay = this.reconnectDelays[delayIndex]
+ const timeSinceLastLogin = now - this.lastLoginTime
+
+ // 只有非首次重连才需要检查冷却时间
+ if (this.reconnectAttempts > 0 && timeSinceLastLogin < reconnectDelay) {
+ const remainingTime = reconnectDelay - timeSinceLastLogin
+ console.log(`重连冷却中,剩余时间:${Math.ceil(remainingTime / 1000)}秒`)
+
+ // 安排下次重连
+ setTimeout(() => {
+ if (!this.isLoggedIn && !this.isLoggingIn && this.reconnectAttempts < this.maxReconnectAttempts) {
+ this.attemptReconnect()
+ }
+ }, remainingTime)
+
+ return false
+ }
+
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
+ console.error('达到最大重连次数,停止自动重连')
+ this.triggerCallback('onError', 'IM连接失败,请检查网络或手动刷新页面')
+ return false
+ }
+
+ this.reconnectAttempts++
+ console.log(`第${this.reconnectAttempts}次重连尝试(最多${this.maxReconnectAttempts}次)`)
+
+ try {
+ if (this.tim && !this.isLoggedIn) {
+ console.log('TIM实例存在但未登录,尝试登录...')
+ await this.loginTIM()
+ console.log('重连成功!')
+ this.reconnectAttempts = 0 // 重置重连次数
+ return true
+ }
+
+ if (!this.tim) {
+ console.log('TIM实例不存在,重新初始化...')
+ await this.initTIM(this.currentUserID)
+ console.log('重新初始化并登录成功!')
+ this.reconnectAttempts = 0
+ return true
+ }
+
+ return false
+ } catch (error) {
+ console.error(`第${this.reconnectAttempts}次重连失败:`, error)
+
+ // 如果还有重连机会,自动安排下一次重连
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
+ const nextDelay = this.reconnectDelays[Math.min(this.reconnectAttempts, this.reconnectDelays.length - 1)]
+ console.log(`${nextDelay / 1000}秒后进行第${this.reconnectAttempts + 1}次重连...`)
+ setTimeout(() => {
+ if (!this.isLoggedIn && !this.isLoggingIn && this.reconnectAttempts < this.maxReconnectAttempts) {
+ this.attemptReconnect()
+ }
+ }, nextDelay)
+ }
+
+ return false
+ }
+ }
+
+ // SDK Ready 事件
+ onSDKReady() {
+ console.log('腾讯IM SDK Ready')
+ this.isLoggedIn = true
+ this.triggerCallback('onSDKReady')
+ this.triggerCallback('onLoginStatusChanged', {
+ isLoggedIn: true,
+ reason: 'SDK_READY'
+ })
+ this.startLoginStatusCheck()
+ this.startHeartbeat() // 启动心跳检测
+ }
+
+ // SDK Not Ready 事件
+ onSDKNotReady() {
+ console.log('腾讯IM SDK Not Ready')
+ this.isLoggedIn = false
+ this.triggerCallback('onSDKNotReady')
+ this.triggerCallback('onLoginStatusChanged', {
+ isLoggedIn: false,
+ reason: 'SDK_NOT_READY'
+ })
+ this.stopLoginStatusCheck()
+ this.stopHeartbeat() // 停止心跳检测
+ }
+
+ // 收到新消息
+ onMessageReceived(event) {
+ event.data.forEach(message => {
+ const existingMessage = this.messageList.find(msg => msg.ID === message.ID)
+ if (existingMessage) return
+ if (!this.filterMessage(message)) return
+ const convertedMessage = this.convertMessageFormat(message)
+ if (!convertedMessage.conversationID) {
+ convertedMessage.conversationID = message.conversationID
+ }
+
+ // 获取消息所属的会话ID
+ const messageConversationID = convertedMessage.conversationID
+
+ // 检查是否为系统消息
+ const isSystemMsg = this.isSystemMessage(convertedMessage)
+
+ console.log('收到新消息:', {
+ messageID: convertedMessage.ID,
+ messageConversationID: messageConversationID,
+ currentConversationID: this.currentConversationID,
+ messageType: convertedMessage.type,
+ from: convertedMessage.from,
+ isSystemMessage: isSystemMsg
+ })
+
+ // 判断是否为当前会话的消息
+ // 系统消息:只要会话ID匹配就显示(不要求必须有currentConversationID)
+ // 普通消息:必须有currentConversationID且匹配才显示
+ const isCurrentConversation = isSystemMsg
+ ? messageConversationID === this.currentConversationID
+ : (this.currentConversationID && messageConversationID === this.currentConversationID)
+
+ console.log('消息会话匹配检查:', {
+ isCurrentConversation,
+ isSystemMessage: isSystemMsg,
+ hasCurrentConversationID: !!this.currentConversationID,
+ conversationIDMatch: messageConversationID === this.currentConversationID
+ })
+
+ if (isCurrentConversation) {
+ // 当前会话的消息,触发回调
+ console.log('✓ 消息属于当前会话,触发显示')
+ this.triggerCallback('onMessageReceived', convertedMessage)
+ // 处理已读状态(系统消息也标记为已读)
+ if (this.currentConversationID) {
+ this.markConversationAsRead(this.currentConversationID)
+ }
+ this.triggerCallback('onConversationListUpdated', {
+ reason: 'NEW_MESSAGE_RECEIVED_IN_CURRENT_CONVERSATION',
+ conversation: {
+ conversationID: messageConversationID || this.currentConversationID,
+ lastMessage: this.formatLastMessage(convertedMessage),
+ lastMessageTime: convertedMessage.lastTime,
+ unreadCount: 0,
+ messageType: convertedMessage.type,
+ messageFlow: convertedMessage.flow
+ },
+ message: convertedMessage
+ })
+ } else {
+ // 非当前会话的消息,只更新会话列表
+ console.log('✗ 消息不属于当前会话,已过滤不显示,仅更新会话列表', {
+ messageConversationID,
+ currentConversationID: this.currentConversationID
+ })
+ this.updateConversationListOnNewMessage(convertedMessage)
+ }
+ })
+ }
+
+ // 消息发送成功
+ onMessageSentSucceeded(event) {
+ const messageID = event.data.ID
+ const sentMessage = event.data.message
+
+ // 更新messageList中的消息状态
+ const message = this.messageList.find(msg => msg.ID === messageID)
+ if (message) {
+ message.status = 'success'
+ // 如果服务器返回了真实的消息ID,更新它
+ if (sentMessage && sentMessage.ID && sentMessage.ID !== messageID) {
+ message.ID = sentMessage.ID
+ }
+ }
+
+ // 缓存功能已移除
+
+ this.triggerCallback('onMessageSent', {
+ messageID,
+ status: 'success'
+ })
+ }
+
+ // 消息发送失败
+ onMessageSentFailed(event) {
+ const messageID = event.data.ID
+
+ // 更新messageList中的消息状态
+ const message = this.messageList.find(msg => msg.ID === messageID)
+ if (message) {
+ message.status = 'failed'
+ }
+
+ // 缓存功能已移除
+
+ this.triggerCallback('onMessageSent', {
+ messageID,
+ status: 'failed'
+ })
+ this.triggerCallback('onError', `发送失败: ${event.data.error || '未知错误'}`)
+ }
+
+ // 会话列表更新
+ onConversationListUpdated(event) {
+ this.triggerCallback('onConversationListUpdated', event.data)
+ }
+
+ // 网络状态变化(优化版:更稳定的处理)
+ onNetStateChange(event) {
+ const netState = event.data.netState
+
+ console.log('🌐 网络状态变化:', netState)
+
+ // 清理之前的网络重连定时器
+ if (this.networkReconnectTimer) {
+ clearTimeout(this.networkReconnectTimer)
+ this.networkReconnectTimer = null
+ }
+
+ if (netState === TIM.TYPES.NET_STATE_CONNECTED) {
+ console.log('✓ 网络已连接,延迟检查IM状态以确保稳定')
+
+ // 网络恢复后延迟再检查,避免网络还不稳定时立即重连
+ const delay = IM_CONNECTION_CONFIG.NETWORK_RECONNECT_DELAY
+ this.networkReconnectTimer = setTimeout(() => {
+ if (this.tim && !this.isLoggedIn && !this.isLoggingIn) {
+ console.log('🔄 网络已稳定,开始重连')
+ this.ensureIMConnection()
+ } else if (this.isLoggedIn) {
+ console.log('✓ 网络已稳定,IM连接正常')
+ }
+ this.networkReconnectTimer = null
+ }, delay)
+
+ // 重置重连次数(网络恢复后给更多机会)
+ if (this.reconnectAttempts > 0) {
+ console.log(`重置重连次数(之前: ${this.reconnectAttempts})`)
+ this.reconnectAttempts = 0
+ }
+
+ } else if (netState === TIM.TYPES.NET_STATE_CONNECTING) {
+ console.log('🔗 网络连接中,等待稳定...')
+
+ } else if (netState === TIM.TYPES.NET_STATE_DISCONNECTED) {
+ console.log('⚠️ 网络断开(暂不标记为未登录,等待心跳检测判断)')
+
+ // 网络断开时不立即设置 isLoggedIn = false
+ // 因为可能只是短暂的网络波动,让心跳检测来判断是否真的断线
+
+ this.triggerCallback('onLoginStatusChanged', {
+ isLoggedIn: this.isLoggedIn,
+ reason: 'NETWORK_DISCONNECTED',
+ message: '网络波动,将自动重连'
+ })
+ }
+ }
+
+ // 被踢下线处理
+ onKickedOut(event) {
+ console.log('⚠️ 账号被踢下线:', event.data)
+
+ // 更新登录状态
+ this.isLoggedIn = false
+ this.isLoggingIn = false
+
+ // 停止所有定时器(防止自动重连)
+ this.clearAllTimers()
+ this.stopLoginStatusCheck()
+ this.stopHeartbeat()
+
+ // 清除重连相关状态,阻止重连
+ this.reconnectAttempts = this.maxReconnectAttempts // 设置为最大值,阻止重连
+
+ // 触发登录状态变化回调
+ this.triggerCallback('onLoginStatusChanged', {
+ isLoggedIn: false,
+ reason: 'KICKED_OUT',
+ message: '您的账号在其他设备登录'
+ })
+
+ // 显示提示弹框
+ uni.showModal({
+ title: '提示',
+ content: '您的账号在其他设备登录,您已被踢下线',
+ showCancel: false,
+ confirmText: '确定',
+ success: () => {
+ // 清理本地缓存并跳转到登录页
+ uni.removeStorageSync('token')
+ uni.removeStorageSync('refreshToken')
+ uni.removeStorageSync('account')
+ uni.removeStorageSync('openid')
+ uni.reLaunch({
+ url: '/pages-center/login/login'
+ })
+ }
+ })
+ }
+
+ // 手动检测并重连IM
+ async ensureIMConnection() {
+ if (this.isLoggingIn || !this.tim) return true
+
+ const isLoggedIn = this.checkLoginStatus()
+ if (!isLoggedIn && this.currentUserID) {
+ return await this.attemptReconnect()
+ }
+ return true
+ }
+
+ startHeartbeat() {
+ this.stopHeartbeat()
+
+ // 心跳失败计数器
+ this.heartbeatFailCount = 0
+ const MAX_HEARTBEAT_FAIL = IM_CONNECTION_CONFIG.HEARTBEAT_MAX_FAIL
+ const INTERVAL = IM_CONNECTION_CONFIG.HEARTBEAT_INTERVAL
+
+ // 定时心跳检测
+ this.heartbeatInterval = setInterval(() => {
+ // 如果已达到最大重连次数(被踢下线),停止心跳检测
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
+ console.log('⚠️ 已被踢下线或达到最大重连次数,停止心跳检测')
+ this.stopHeartbeat()
+ return
+ }
+
+ // 只在已登录状态下进行心跳检测
+ if (!this.tim || !this.isLoggedIn) {
+ console.log('⏸ 心跳检测:未登录,跳过检测')
+ return
+ }
+
+ // 确保方法存在
+ if (typeof this.tim.getConversationList !== 'function') {
+ console.log('⏸ 心跳检测:SDK方法不可用,跳过检测')
+ return
+ }
+
+ this.tim.getConversationList()
+ .then(() => {
+ if (this.heartbeatFailCount > 0) {
+ console.log(`� 心跳恢复正常(之前失败${this.heartbeatFailCount}次)`)
+ }
+ this.heartbeatFailCount = 0 // 重置失败计数
+ })
+ .catch((error) => {
+ // 如果是SDK未就绪错误,不计入失败次数(这是临时状态)
+ if (error && error.message && error.message.includes('sdk not ready')) {
+ console.log('⏸ 心跳检测:SDK未就绪,跳过本次检测')
+ return
+ }
+
+ this.heartbeatFailCount++
+ console.error(`💔 心跳失败 (${this.heartbeatFailCount}/${MAX_HEARTBEAT_FAIL}):`, error.message)
+
+ // 只有连续失败多次才认为真的断线
+ if (this.heartbeatFailCount >= MAX_HEARTBEAT_FAIL) {
+ console.log('❌ 心跳连续失败,标记为未登录并尝试重连')
+ this.isLoggedIn = false
+ this.heartbeatFailCount = 0
+ // 只有未被踢下线才尝试重连
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
+ this.attemptReconnect()
+ }
+ }
+ })
+ }, INTERVAL)
+
+ console.log(`💓 心跳检测已启动(间隔${INTERVAL / 1000}秒,最多失败${MAX_HEARTBEAT_FAIL}次)`)
+ }
+
+ // 停止心跳检测
+ stopHeartbeat() {
+ if (this.heartbeatInterval) {
+ clearInterval(this.heartbeatInterval)
+ this.heartbeatInterval = null
+ console.log('IM心跳检测已停止')
+ }
+ }
+
+ // 获取会话列表
+ getConversationList() {
+ if (!this.tim) {
+ console.error('TIM实例不存在,无法获取会话列表')
+ return
+ }
+
+ if (!this.isLoggedIn) {
+ console.log('SDK未ready,等待SDK初始化后再获取会话列表...')
+ // 等待SDK就绪
+ const checkSDKReady = () => {
+ if (this.isLoggedIn) {
+ console.log('SDK已ready,开始获取会话列表')
+ this.getConversationList()
+ } else {
+ setTimeout(checkSDKReady, 500)
+ }
+ }
+ setTimeout(checkSDKReady, 500)
+ return
+ }
+ }
+
+ // 获取群聊列表
+ getGroupList() {
+ return new Promise((resolve, reject) => {
+ if (!this.tim) {
+ reject(new Error('TIM实例不存在'))
+ return
+ }
+
+ if (!this.isLoggedIn) {
+ console.log('SDK未ready,等待SDK初始化...')
+ let waitTime = 0
+ const maxWaitTime = 30000 // 最多等待30秒
+ const checkInterval = 1000 // 每秒检查一次
+ let timeoutHandle = null
+
+ const checkSDKReady = () => {
+ if (this.isLoggedIn) {
+ console.log('SDK已ready,开始获取群聊列表')
+ if (timeoutHandle) clearTimeout(timeoutHandle)
+ this.getGroupListInternal().then(resolve).catch(reject)
+ } else if (waitTime >= maxWaitTime) {
+ console.error('等待SDK就绪超时')
+ if (timeoutHandle) clearTimeout(timeoutHandle)
+ reject(new Error('SDK初始化超时,请检查网络连接'))
+ } else {
+ waitTime += checkInterval
+ console.log(`等待SDK就绪... (${Math.floor(waitTime / 1000)}/${Math.floor(maxWaitTime / 1000)}秒)`)
+ timeoutHandle = setTimeout(checkSDKReady, checkInterval)
+ }
+ }
+
+ checkSDKReady()
+ return
+ }
+
+ this.getGroupListInternal().then(resolve).catch(reject)
+ })
+ }
+
+ // 内部获取群聊列表方法
+ getGroupListInternal() {
+ return new Promise((resolve, reject) => {
+ console.log('开始获取群聊列表')
+
+ // 直接调用,SDK就绪检查已在getGroupList()中完成
+ this.tim.getConversationList()
+ .then(async (conversationResponse) => {
+ console.log('获取会话列表成功')
+
+ const groupConversations = conversationResponse.data.conversationList.filter(conversation => {
+ return conversation.conversationID && conversation.conversationID.startsWith('GROUP')
+ })
+
+ console.log('群聊会话列表数量:', groupConversations.length)
+
+ // 先获取一次群组列表,避免在循环中重复调用
+ let allGroups = []
+ try {
+ const groupListResponse = await this.tim.getGroupList()
+ allGroups = groupListResponse.data.groupList || []
+ } catch (error) {
+ console.error('获取群组列表失败:', error)
+ }
+
+ const groupsWithInfo = await Promise.all(
+ groupConversations.map(async (conversation) => {
+ try {
+ const groupName = conversation.groupProfile?.name || ''
+ const [doctorId, patientName] = groupName.split('|')
+ const groupID = conversation.conversationID.replace('GROUP', '')
+
+ // 从已获取的群组列表中查找
+ const group = allGroups.find(g => g.groupID === groupID)
+ const groupInfo = {
+ groupID: groupID,
+ name: group?.name || '问诊群聊',
+ avatar: '/static/home/avatar.svg',
+ memberCount: group?.memberCount || 0
+ }
+
+ const lastMessage = conversation.lastMessage
+ let lastMessageText = '暂无消息'
+ let lastMessageTime = Date.now()
+
+ if (lastMessage) {
+ if (lastMessage.type === 'TIMTextElem') {
+ lastMessageText = lastMessage.payload.text
+ } else if (lastMessage.type === 'TIMImageElem') {
+ lastMessageText = '[图片]'
+ } else if (lastMessage.type === 'TIMSoundElem') {
+ lastMessageText = '[语音]'
+ } else if (lastMessage.type === 'TIMCustomElem') {
+ // 解析自定义消息
+ try {
+ const customData = JSON.parse(lastMessage.payload.data)
+ const messageType = customData.messageType
+ // 根据消息类型返回不同的预览文本
+ switch (messageType) {
+ case 'system_message':
+ lastMessageText = '[系统消息]'
+ break
+ case 'symptom':
+ lastMessageText = '[病情描述]'
+ break
+ case 'prescription':
+ lastMessageText = '[处方单]'
+ break
+ case 'refill':
+ lastMessageText = '[续方申请]'
+ break
+ case 'survey':
+ lastMessageText = '[问卷调查]'
+ break
+ case 'article':
+ lastMessageText = '[文章]'
+ break
+ case "consult_pending":
+ lastMessageText = '患者向团队发起咨询,请在1小时内接诊,超时将自动关闭会话'
+ break
+ case "consult_rejected":
+ lastMessageText = '患者向团队发起咨询,由于有紧急事务要处理暂时无法接受咨询.本次会话丿关闭'
+ break
+ case "consult_timeout":
+ lastMessageText = '患者向团队发起咨询,团队成员均未接受咨询,本次会话已自动关闭'
+ break
+ case "consult_accepted":
+ lastMessageText = '已接诊,会话已开始'
+ break
+ case "consult_ended":
+ lastMessageText = '已结束当前会话'
+ break
+ default:
+ lastMessageText = '[自定义消息]'
+ }
+ } catch (error) {
+ console.error('解析自定义消息失败:', error)
+ lastMessageText = '[自定义消息]'
+ }
+ } else {
+ lastMessageText = '[未知消息类型]'
+ }
+
+ lastMessageTime = (lastMessage.lastTime || lastMessage.time || 0) * 1000
+ }
+
+ return {
+ ...groupInfo,
+ groupID: groupID,
+ doctorId,
+ patientName,
+ conversationID: conversation.conversationID,
+ lastMessage: lastMessageText,
+ lastMessageTime: lastMessageTime,
+ unreadCount: conversation.unreadCount || 0
+ }
+ } catch (error) {
+ console.error(`处理群聊会话失败:`, error)
+ return {
+ groupID: conversation.conversationID,
+ name: conversation.groupProfile?.name || '问诊群聊',
+ doctorId: '',
+ patientName: '',
+ avatar: '/static/home/avatar.svg',
+ lastMessage: '获取失败',
+ lastMessageTime: Date.now(),
+ unreadCount: conversation.unreadCount || 0,
+ memberCount: 0
+ }
+ }
+ })
+ )
+
+ console.log('处理后的群聊列表数量:', groupsWithInfo.length)
+ resolve({
+ success: true,
+ groupList: groupsWithInfo,
+ totalCount: groupsWithInfo.length,
+ data: conversationResponse.data
+ })
+ })
+ .catch((imError) => {
+ console.error('获取会话列表失败:', imError)
+ reject({
+ success: false,
+ error: imError
+ })
+ })
+ })
+ }
+
+ // 创建问诊群聊
+ createGroup({ type, name, groupID, memberList, timeout = 30000, retryCount = 2 }) {
+ return new Promise((resolve, reject) => {
+ if (!this.tim) {
+ reject(new Error('TIM实例不存在'))
+ return
+ }
+
+ if (!this.isLoggedIn) {
+ console.log('SDK未ready,等待SDK初始化...')
+ const maxWaitTime = 30000
+ const startTime = Date.now()
+
+ const checkSDKReady = () => {
+ if (this.isLoggedIn) {
+ console.log('SDK已ready,开始创建群聊')
+ this.createGroupInternal({ type, name, groupID, memberList, timeout, retryCount })
+ .then(resolve)
+ .catch(reject)
+ } else if (Date.now() - startTime > maxWaitTime) {
+ reject(new Error('等待SDK初始化超时'))
+ } else {
+ console.log('SDK仍未ready,继续等待...')
+ setTimeout(checkSDKReady, 1000)
+ }
+ }
+ checkSDKReady()
+ return
+ }
+
+ this.createGroupInternal({ type, name, groupID, memberList, timeout, retryCount })
+ .then(resolve)
+ .catch(reject)
+ })
+ }
+
+ // 内部创建群组方法
+ async createGroupInternal({ type, name, groupID, memberList, timeout, retryCount }) {
+ let lastError = null
+
+ for (let attempt = 0; attempt <= retryCount; attempt++) {
+ try {
+ console.log(`创建群聊尝试 ${attempt + 1}/${retryCount + 1}:`, {
+ type,
+ name,
+ groupID,
+ memberCount: memberList ? memberList.length : 0
+ })
+
+ const result = await this.createGroupWithTimeout({ type, name, groupID, memberList, timeout })
+ console.log(`创建群聊成功,尝试次数: ${attempt + 1}`)
+ return result
+ } catch (error) {
+ lastError = error
+ console.error(`第 ${attempt + 1} 次创建群聊失败:`, error)
+
+ if (attempt === retryCount) break
+
+ const retryDelay = Math.min(1000 * Math.pow(2, attempt), 5000)
+ console.log(`等待 ${retryDelay}ms 后重试...`)
+ await new Promise(resolve => setTimeout(resolve, retryDelay))
+ }
+ }
+
+ throw lastError
+ }
+
+ // 带超时的创建群组方法
+ createGroupWithTimeout({ type, name, groupID, memberList, timeout = 30000 }) {
+ return new Promise((resolve, reject) => {
+ const timeoutId = setTimeout(() => {
+ reject(new Error(`创建群聊超时 (${timeout}ms)`))
+ }, timeout)
+
+ if (!groupID) {
+ clearTimeout(timeoutId)
+ reject(new Error('群聊ID不能为空'))
+ return
+ }
+
+ if (!name) {
+ clearTimeout(timeoutId)
+ reject(new Error('群聊名称不能为空'))
+ return
+ }
+
+ let groupType = TIM.TYPES.GRP_WORK
+ if (type === 'GRP_WORK') {
+ groupType = TIM.TYPES.GRP_WORK
+ } else if (type === 'GRP_PUBLIC') {
+ groupType = TIM.TYPES.GRP_PUBLIC
+ } else if (type === 'GRP_CHATROOM') {
+ groupType = TIM.TYPES.GRP_CHATROOM
+ } else if (type === 'GRP_AVCHATROOM') {
+ groupType = TIM.TYPES.GRP_AVCHATROOM
+ } else if (type === 'GRP_MEETING') {
+ groupType = TIM.TYPES.GRP_MEETING
+ }
+
+ const createGroupParams = {
+ type: groupType,
+ name: name,
+ groupID: groupID
+ }
+
+ if (memberList && memberList.length > 0) {
+ createGroupParams.memberList = memberList
+ }
+
+ console.log('创建群组参数:', createGroupParams)
+
+ this.tim.createGroup(createGroupParams).then(async (imResponse) => {
+ clearTimeout(timeoutId)
+ console.log('创建问诊群聊成功:', imResponse)
+
+ try {
+ await api('sendSystemMessage', { groupID, status: 'pending' })
+ console.log('pending系统消息发送成功')
+ } catch (error) {
+ console.error('pending系统消息发送失败:', error)
+ }
+
+ resolve({
+ success: true,
+ groupID: groupID,
+ data: imResponse.data
+ })
+ }).catch((imError) => {
+ clearTimeout(timeoutId)
+
+ const enhancedError = {
+ ...imError,
+ originalError: imError,
+ groupID: groupID,
+ name: name,
+ type: type,
+ timestamp: new Date().toISOString()
+ }
+
+ console.error('创建问诊群聊失败:', enhancedError)
+
+ const errorMessage = enhancedError.message || enhancedError.originalError?.message || ''
+ if (errorMessage.includes('group id has been used') || errorMessage.includes('group id has been used!')) {
+ console.log('群聊ID已存在,视为创建成功')
+ resolve({
+ success: true,
+ groupID: groupID,
+ data: { groupID: groupID },
+ message: '群聊已存在'
+ })
+ return
+ }
+
+ reject({
+ success: false,
+ error: enhancedError,
+ userMessage: this.formatErrorMessage(enhancedError)
+ })
+ })
+ })
+ }
+
+ // 格式化错误信息
+ formatErrorMessage(error) {
+ if (!error) return '创建群聊失败,请稍后重试'
+
+ const errorCode = error.code || (error.originalError && error.originalError.code)
+ const errorMessage = error.message || (error.originalError && error.originalError.message) || ''
+
+ if (errorMessage.includes('group id has been used') || errorMessage.includes('group id has been used!')) {
+ return 'SUCCESS_GROUP_EXISTS'
+ }
+
+ switch (errorCode) {
+ case 2801:
+ return '网络连接超时,请检查网络连接后重试'
+ case 2800:
+ return '网络连接异常,请检查网络设置'
+ case 2802:
+ return '服务器连接失败,请稍后重试'
+ case 2803:
+ return '请求失败,请检查网络连接'
+ case 2804:
+ return '服务暂时不可用,请稍后重试'
+ case 10004:
+ return '群聊ID已存在,请使用其他ID'
+ case 10007:
+ return '群聊创建权限不足'
+ case 10010:
+ return '群聊数量已达上限'
+ default:
+ if (errorMessage.includes('超时') || errorMessage.includes('timeout')) {
+ return '操作超时,请检查网络连接后重试'
+ } else if (errorMessage.includes('网络') || errorMessage.includes('network')) {
+ return '网络连接异常,请检查网络设置'
+ } else if (errorMessage.includes('权限') || errorMessage.includes('permission')) {
+ return '操作权限不足,请联系管理员'
+ } else {
+ return `创建群聊失败:${errorMessage}`
+ }
+ }
+ }
+
+ // 设置当前会话ID
+ setConversationID(conversationID) {
+ this.currentConversationID = conversationID
+ console.log('设置当前会话ID:', conversationID)
+ }
+
+ // 进入会话
+ enterConversation(conversationID) {
+ console.log("【enterConversation】进入会话:", conversationID)
+
+ // 更新当前会话ID
+ this.currentConversationID = conversationID
+
+ // 清空当前消息列表
+ this.messageList = []
+
+ // 重置分页状态
+ this.nextReqMessageID = ""
+ this.isCompleted = false
+ this.isLoadingMore = false
+
+ console.log(" 会话ID已更新,消息列表已清空,分页状态已重置")
+
+ // 进入群聊会话,默认加载20条消息
+ this.enterGroupConversation(conversationID, 20)
+ }
+
+ // 进入群聊会话
+ async enterGroupConversation(groupID, count = 20) {
+ console.log("【enterGroupConversation】进入群聊会话, groupID:", groupID, "count:", count)
+
+ let conversationID = groupID
+ let actualGroupID = groupID
+
+ if (groupID.startsWith('GROUP')) {
+ actualGroupID = groupID.replace('GROUP', '')
+ conversationID = groupID
+ } else {
+ conversationID = `GROUP${groupID}`
+ actualGroupID = groupID
+ }
+
+ console.log(" conversationID:", conversationID, "actualGroupID:", actualGroupID)
+
+ // 清空消息列表,准备加载新会话的消息
+ this.messageList = []
+
+ // 确保设置当前会话ID,防止消息混淆
+ this.currentConversationID = conversationID
+ console.log('进入群聊会话,设置currentConversationID:', conversationID)
+
+ this.conversation = {
+ conversationID: conversationID,
+ conversationType: TIM.TYPES.CONV_GROUP,
+ groupProfile: {
+ groupID: actualGroupID,
+ name: '问诊群聊',
+ type: TIM.TYPES.GRP_WORK
+ }
+ }
+
+ this.markConversationAsRead(conversationID)
+
+ // 首先从本地接口加载聊天记录
+ console.log(" 开始从本地接口加载聊天记录,groupID:", actualGroupID)
+ await this.loadMessagesFromLocalAPI(actualGroupID, count)
+ }
+
+ // 从本地API加载聊天记录
+ async loadMessagesFromLocalAPI(groupID, count = 20, skip = 0, isPullUp = false) {
+ try {
+ console.log("【loadMessagesFromLocalAPI】开始从本地API加载聊天记录")
+ console.log(" groupID:", groupID, "count:", count, "skip:", skip, "isPullUp:", isPullUp)
+
+ // 调用本地接口获取聊天记录
+ const response = await api('getChatRecordsByGroupId', { groupID, count, skip })
+
+ if (response && response.success && response.data && response.data.records) {
+ const dbMessages = response.data.records
+ const hasMore = response.data.hasMore || false // 从后端获取hasMore字段
+ const total = response.data.total || 0
+
+ console.log(` 成功获取 ${dbMessages.length} 条聊天记录`)
+ console.log(` hasMore: ${hasMore}, total: ${total}`)
+
+ // 将数据库消息转换为IM消息格式
+ const convertedMessages = dbMessages
+ .map(dbMsg => this.convertDBMessageToIMFormat(dbMsg))
+ .filter(msg => msg !== null && this.filterMessage(msg))
+
+ console.log(` 转换后 ${convertedMessages.length} 条消息`)
+
+ // 按时间排序(从早到晚)
+ convertedMessages.sort((a, b) => a.lastTime - b.lastTime)
+
+ // 根据是否为上拉加载,决定如何更新消息列表
+ if (isPullUp) {
+ // 上拉加载更多:将新消息插入到列表前面(历史消息)
+ this.messageList.unshift(...convertedMessages)
+ // 再次排序确保顺序正确
+ this.messageList.sort((a, b) => a.lastTime - b.lastTime)
+ } else {
+ // 首次加载:直接替换消息列表
+ this.messageList = convertedMessages
+ }
+
+ // 设置分页状态 - 使用后端返回的 hasMore 字段
+ this.isCompleted = !hasMore
+
+ console.log(` 分页状态: isCompleted=${this.isCompleted}, hasMore=${hasMore}`)
+
+ // 触发回调
+ this.triggerCallback("onMessageListLoaded", {
+ messages: this.messageList,
+ isPullUp: isPullUp,
+ hasMore: hasMore,
+ isCompleted: this.isCompleted,
+ total: total,
+ })
+
+ return {
+ success: true,
+ count: convertedMessages.length,
+ hasMore: hasMore,
+ isCompleted: this.isCompleted,
+ messages: this.messageList,
+ total: total,
+ }
+ } else {
+ console.warn(" ⚠️ 本地API返回数据格式错误")
+ return {
+ success: false,
+ message: "本地API返回数据格式错误",
+ count: 0,
+ }
+ }
+ } catch (error) {
+ console.error(" ❌ 从本地API加载聊天记录失败:", error)
+ return {
+ success: false,
+ error: error,
+ message: "加载聊天记录失败",
+ count: 0,
+ }
+ }
+ }
+
+ // 将数据库消息转换为IM消息格式
+ convertDBMessageToIMFormat(dbMsg) {
+ try {
+ if (!dbMsg || !dbMsg.MsgBody || !Array.isArray(dbMsg.MsgBody) || dbMsg.MsgBody.length === 0) {
+ return null
+ }
+
+ // 获取第一个消息体(通常一个消息只有一个消息体)
+ const msgBody = dbMsg.MsgBody[0]
+ const msgType = msgBody.MsgType
+
+ // 确定消息的流向(in/out)
+ // 根据 From_Account 判断是否为当前用户发送的消息
+ const flow = dbMsg.From_Account === this.currentUserID ? 'out' : 'in'
+
+ // 计算时间戳(毫秒)
+ let lastTime = Date.now()
+ if (dbMsg.MsgTime) {
+ // MsgTime 是秒级时间戳
+ lastTime = dbMsg.MsgTime * 1000
+ } else if (dbMsg.createdAt) {
+ lastTime = new Date(dbMsg.createdAt).getTime()
+ }
+
+ // 构建基础消息对象
+ const message = {
+ ID: dbMsg.MsgSeq || dbMsg._id || `db_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+ from: dbMsg.From_Account || '',
+ flow: flow,
+ type: msgType,
+ payload: this.convertDBPayloadToIMPayload(msgType, msgBody.MsgContent),
+ lastTime: lastTime,
+ status: 'success',
+ avatar: flow === 'in' ? '/static/home/avatar.svg' : '/static/center/user-avatar.png',
+ conversationID: this.currentConversationID,
+ MsgSeq: dbMsg.MsgSeq, // 保留 MsgSeq 用于分页
+ }
+
+ return message
+ } catch (error) {
+ console.error("转换数据库消息格式失败:", error, dbMsg)
+ return null
+ }
+ }
+
+ // 将数据库消息体转换为IM payload格式
+ convertDBPayloadToIMPayload(msgType, msgContent) {
+ try {
+ if (!msgContent) {
+ return {}
+ }
+
+ switch (msgType) {
+ case 'TIMTextElem':
+ return {
+ text: msgContent.Text || ''
+ }
+
+ case 'TIMImageElem':
+ return {
+ imageInfoArray: msgContent.ImageInfoArray || []
+ }
+
+ case 'TIMSoundElem':
+ return {
+ url: msgContent.Url || '',
+ second: msgContent.Second || 0,
+ downloadFlag: msgContent.Download_Flag || 2
+ }
+
+ case 'TIMCustomElem':
+ return {
+ data: msgContent.Data || '',
+ description: msgContent.Desc || '',
+ extension: msgContent.Ext || ''
+ }
+
+ case 'TIMVideoFileElem':
+ return {
+ videoUrl: msgContent.VideoUrl || '',
+ videoSize: msgContent.VideoSize || 0,
+ videoSecond: msgContent.VideoSecond || 0,
+ videoFormat: msgContent.VideoFormat || '',
+ videoDownloadFlag: msgContent.VideoDownloadFlag || 2,
+ thumbUrl: msgContent.ThumbUrl || '',
+ thumbSize: msgContent.ThumbSize || 0,
+ thumbWidth: msgContent.ThumbWidth || 0,
+ thumbHeight: msgContent.ThumbHeight || 0,
+ thumbFormat: msgContent.ThumbFormat || '',
+ thumbDownloadFlag: msgContent.ThumbDownloadFlag || 2
+ }
+
+ case 'TIMFileElem':
+ return {
+ url: msgContent.Url || '',
+ fileSize: msgContent.FileSize || 0,
+ fileName: msgContent.FileName || '',
+ downloadFlag: msgContent.Download_Flag || 2
+ }
+
+ default:
+ return msgContent
+ }
+ } catch (error) {
+ console.error("转换消息体失败:", error, msgType, msgContent)
+ return {}
+ }
+ }
+
+ // 分页加载消息(单次加载)
+ async loadMessagesPage(options = {}) {
+ if (!this.conversation) {
+ console.log("会话未初始化,无法加载消息")
+ return Promise.reject(new Error("会话未初始化"))
+ }
+
+ const count = options.count || 20
+ // 记录调用前是否已有nextReqMessageID,用于判断是否为首次加载
+ const isLoadMore = !!(this.nextReqMessageID && this.nextReqMessageID !== "")
+
+ try {
+ // 构建请求参数
+ const requestParams = {
+ conversationID: this.conversation.conversationID,
+ count: Math.min(count, 15), // 腾讯IM SDK限制count在1-15之间
+ }
+
+ // 如果有nextReqMessageID,说明是加载更多
+ if (isLoadMore) {
+ requestParams.nextReqMessageID = this.nextReqMessageID
+ console.log("加载更多消息,nextReqMessageID:", this.nextReqMessageID)
+ } else {
+ console.log("首次加载消息")
+ }
+
+ console.log(" 📡 发送请求到腾讯IM:", requestParams)
+ const response = await this.tim.getMessageList(requestParams)
+
+ const messageList = response.data.messageList
+ const oldNextReqMessageID = this.nextReqMessageID
+ const oldIsCompleted = this.isCompleted
+
+ this.nextReqMessageID = response.data.nextReqMessageID
+ this.isCompleted = response.data.isCompleted
+
+ console.log(" 📥 收到腾讯IM响应:")
+ console.log(` 消息数量: ${messageList.length}`)
+ console.log(` nextReqMessageID: ${oldNextReqMessageID} → ${this.nextReqMessageID}`)
+ console.log(` isCompleted: ${oldIsCompleted} → ${this.isCompleted}`)
+
+ if (messageList && messageList.length > 0) {
+ // 过滤掉不需要显示的消息
+ const filteredMessages = messageList.filter((message) =>
+ this.filterMessage(message)
+ )
+
+ // 转换消息格式
+ const messages = filteredMessages.map((message) => {
+ return this.convertMessageFormat(message)
+ })
+
+ // 将新消息插入到列表前面(历史消息在前)
+ this.messageList.unshift(...messages)
+
+ // 按时间戳排序消息列表(从早到晚)
+ this.messageList.sort((a, b) => a.lastTime - b.lastTime)
+ }
+
+ // 触发回调 - 使用 isLoadMore 来判断是否为上拉加载
+ this.triggerCallback("onMessageListLoaded", {
+ messages: this.messageList,
+ isPullUp: isLoadMore, // 只有在加载更多时才是 true
+ hasMore: !this.isCompleted,
+ isCompleted: this.isCompleted,
+ })
+
+ return {
+ success: true,
+ count: messageList ? messageList.length : 0,
+ hasMore: !this.isCompleted,
+ isCompleted: this.isCompleted,
+ messages: this.messageList,
+ }
+ } catch (error) {
+ console.error("分页加载消息失败:", error)
+ return {
+ success: false,
+ error: error,
+ count: 0,
+ }
+ }
+ }
+
+ // 加载更多消息(供页面调用)
+ async loadMoreMessages() {
+ console.log("【loadMoreMessages】开始加载更多消息")
+ console.log(" 当前状态: isLoadingMore=", this.isLoadingMore)
+ console.log(" 当前状态: isCompleted=", this.isCompleted)
+ console.log(" 当前消息数量:", this.messageList.length)
+
+ if (this.isLoadingMore) {
+ console.log(" ⚠️ 正在加载中,跳过重复请求")
+ return { success: false, message: "正在加载中" }
+ }
+
+ if (this.isCompleted) {
+ console.log(" ✅ 已加载全部消息")
+ return { success: false, message: "已加载全部消息" }
+ }
+
+ this.isLoadingMore = true
+ console.log(" 🔄 设置 isLoadingMore = true")
+
+ try {
+ // 从本地数据库加载更多历史消息
+ // 使用当前消息列表的长度作为 skip 值
+ const skip = this.messageList.length
+
+ // 获取群组ID
+ let groupID = ''
+ if (this.conversation && this.conversation.groupProfile) {
+ groupID = this.conversation.groupProfile.groupID
+ } else if (this.currentConversationID) {
+ groupID = this.currentConversationID.replace('GROUP', '')
+ }
+
+ if (!groupID) {
+ console.error(" ❌ 无法获取群组ID")
+ this.isLoadingMore = false
+ return {
+ success: false,
+ message: "无法获取群组ID",
+ }
+ }
+
+ console.log(" 从本地数据库加载更多,groupID:", groupID, "skip:", skip)
+
+ // 调用本地API加载更多消息
+ const result = await this.loadMessagesFromLocalAPI(groupID, 20, skip, true)
+
+ this.isLoadingMore = false
+ console.log(" 🔄 设置 isLoadingMore = false")
+ console.log(" 📥 loadMessagesFromLocalAPI 返回结果:", result)
+
+ if (result.success) {
+ console.log(" ✅ 加载成功,新增", result.count, "条消息")
+ return {
+ success: true,
+ count: result.count,
+ hasMore: result.hasMore,
+ isCompleted: result.isCompleted,
+ }
+ } else {
+ console.log(" ❌ 加载失败:", result.message || result.error)
+ return {
+ success: false,
+ error: result.error,
+ message: result.message || "加载失败",
+ }
+ }
+ } catch (error) {
+ console.error(" ❌ 加载更多异常:", error)
+ this.isLoadingMore = false
+ return {
+ success: false,
+ error: error,
+ message: "加载失败",
+ }
+ }
+ }
+
+ // 后台刷新服务器消息(已弃用缓存功能)
+ async refreshMessagesFromServer(conversationID, cachedMessages) {
+ try {
+ const result = await this.getMessages({ getAllMessages: true })
+ if (result.success) {
+ // 严格过滤:只保留属于当前会话的消息
+ const filteredMessages = result.messages.filter(msg => msg.conversationID === conversationID)
+
+ this.messageList = filteredMessages
+
+ console.log(`消息刷新完成,过滤后${filteredMessages.length}条消息`)
+
+ // 触发更新回调
+ this.triggerCallback('onMessageListLoaded', {
+ messages: this.messageList,
+ isPullUp: false,
+ hasMore: false,
+ isCompleted: true,
+ isRefresh: true // 标记这是刷新操作
+ })
+ }
+ } catch (error) {
+ console.error('后台刷新消息失败:', error)
+ }
+ }
+
+ // 合并消息(保留本地消息,避免覆盖)
+ mergeMessages(serverMessages, cachedMessages) {
+ const messageMap = new Map()
+
+ // 先添加服务器消息
+ serverMessages.forEach(msg => {
+ if (msg.ID) {
+ messageMap.set(msg.ID, msg)
+ }
+ })
+
+ // 再添加缓存消息(覆盖服务器消息,保留本地最新状态)
+ cachedMessages.forEach(msg => {
+ if (msg.ID) {
+ // 如果是本地消息(ID以local_开头)或者是发送中/失败的消息,保留缓存中的版本
+ if (msg.ID.startsWith('local_') || msg.status === 'sending' || msg.status === 'failed') {
+ messageMap.set(msg.ID, msg)
+ } else if (!messageMap.has(msg.ID)) {
+ // 服务器没有的消息也保留
+ messageMap.set(msg.ID, msg)
+ }
+ }
+ })
+
+ // 转换为数组并按时间排序
+ const mergedArray = Array.from(messageMap.values())
+ mergedArray.sort((a, b) => a.lastTime - b.lastTime)
+
+ console.log(`消息合并完成: 服务器${serverMessages.length}条, 缓存${cachedMessages.length}条, 合并后${mergedArray.length}条`)
+
+ return mergedArray
+ }
+
+ // 获取消息列表
+ async getMessages(options = {}) {
+ if (!this.conversation) {
+ console.log('会话未初始化,跳过获取消息列表')
+ this.isLoading = false
+ return Promise.resolve({ success: false, error: '会话未初始化' })
+ }
+
+ this.isLoading = true
+
+ const defaultOptions = {
+ count: 20,
+ nextReqMessageID: '',
+ isPullUp: false,
+ getAllMessages: false
+ }
+
+ const requestOptions = { ...defaultOptions, ...options }
+
+ try {
+ if (requestOptions.getAllMessages) {
+ return await this.getAllMessages(requestOptions)
+ } else {
+ return await this.getMessagesBatch(requestOptions)
+ }
+ } catch (error) {
+ console.error('获取消息列表失败:', error)
+ this.isLoading = false
+ this.messageList = []
+
+ this.triggerCallback('onMessageListLoaded', {
+ messages: this.messageList,
+ isPullUp: requestOptions.isPullUp,
+ hasMore: false,
+ error: error
+ })
+
+ return { success: false, error }
+ }
+ }
+
+ // 获取单批消息
+ async getMessagesBatch(options) {
+ const requestParams = {
+ conversationID: this.conversation.conversationID,
+ count: options.count
+ }
+
+ if (options.nextReqMessageID) {
+ requestParams.nextReqMessageID = options.nextReqMessageID
+ }
+
+ console.log('请求消息列表参数:', requestParams)
+
+ const response = await this.tim.getMessageList(requestParams)
+ const messageList = response.data.messageList
+ const nextReqMessageID = response.data.nextReqMessageID
+ const isCompleted = response.data.isCompleted
+
+ console.log(`TIM返回消息: ${messageList.length}条, isCompleted: ${isCompleted}`)
+
+ // 处理消息
+ const messages = this.processMessages(messageList)
+
+ console.log(`处理后消息: ${messages.length}条`)
+
+ // 更新消息列表
+ if (options.isPullUp) {
+ this.messageList.unshift(...messages)
+ } else {
+ this.messageList = messages
+ }
+
+ // 缓存消息
+ this.cacheMessages()
+
+ this.isLoading = false
+
+ // 触发回调(仅在非getAllMessages调用时触发)
+ if (!options.skipCallback) {
+ this.triggerCallback('onMessageListLoaded', {
+ messages: this.messageList,
+ isPullUp: options.isPullUp,
+ hasMore: !isCompleted,
+ nextReqMessageID: nextReqMessageID,
+ isCompleted: isCompleted
+ })
+ }
+
+ // 返回当前批次的消息,而不是整个messageList
+ return {
+ success: true,
+ messages: messages, // 返回处理后的当前批次消息
+ hasMore: !isCompleted,
+ nextReqMessageID: nextReqMessageID,
+ isCompleted: isCompleted
+ }
+ }
+
+ // 获取全部历史消息
+ async getAllMessages(options) {
+ console.log('开始获取全部历史消息...')
+ let allMessages = []
+ let nextReqMessageID = options.nextReqMessageID || ''
+ let isCompleted = false
+ let requestCount = 0
+ const maxRequests = 50 // 最多请求50次,避免无限循环
+
+ while (!isCompleted && requestCount < maxRequests) {
+ requestCount++
+ console.log(`获取历史消息第${requestCount}批...`)
+
+ const batchOptions = {
+ ...options,
+ nextReqMessageID: nextReqMessageID,
+ isPullUp: false, // 改为false,因为我们是在构建完整列表
+ skipCallback: true // 跳过单批回调,最后统一回调
+ }
+
+ const result = await this.getMessagesBatch(batchOptions)
+
+ if (!result.success) {
+ console.error(`第${requestCount}批消息获取失败:`, result.error)
+ throw new Error(result.error)
+ }
+
+ console.log(`第${requestCount}批获取到${result.messages.length}条消息`)
+
+ // 将当前批次的消息添加到开头(历史消息在前)
+ if (result.messages.length > 0) {
+ // 使用Set去重,避免重复消息
+ const existingIds = new Set(allMessages.map(m => m.ID))
+ const newMessages = result.messages.filter(m => !existingIds.has(m.ID))
+
+ if (newMessages.length > 0) {
+ allMessages.unshift(...newMessages)
+ console.log(`添加${newMessages.length}条新消息,当前总数: ${allMessages.length}`)
+ }
+ }
+
+ nextReqMessageID = result.nextReqMessageID
+ isCompleted = result.isCompleted
+
+ console.log(`isCompleted: ${isCompleted}, nextReqMessageID: ${nextReqMessageID}`)
+
+ // 如果没有更多消息ID,说明已经获取完毕
+ if (!nextReqMessageID) {
+ console.log('没有更多消息ID,获取完成')
+ break
+ }
+ }
+
+ if (requestCount >= maxRequests) {
+ console.warn(`达到最大请求次数限制(${maxRequests}),停止获取`)
+ }
+
+ // 更新消息列表
+ this.messageList = allMessages
+ this.cacheMessages()
+ this.isLoading = false
+
+ console.log(`全部历史消息获取完成,共${allMessages.length}条消息,请求了${requestCount}批`)
+
+ // 触发回调
+ this.triggerCallback('onMessageListLoaded', {
+ messages: this.messageList,
+ isPullUp: false,
+ hasMore: false,
+ nextReqMessageID: '',
+ isCompleted: true
+ })
+
+ return {
+ success: true,
+ messages: this.messageList,
+ hasMore: false,
+ nextReqMessageID: '',
+ isCompleted: true
+ }
+ }
+
+ // 处理消息 - 统一过滤和转换
+ processMessages(messageList) {
+ const filteredMessages = messageList.filter(message => this.filterMessage(message))
+ return filteredMessages.map(message => this.convertMessageFormat(message))
+ }
+
+ // 缓存消息 - 统一缓存逻辑
+ cacheMessages() {
+ // 缓存功能已移除
+ }
+
+ // 清理指定会话的缓存(已弃用)
+ clearConversationCache(conversationID) {
+ // 缓存功能已移除
+ }
+
+ // 清理所有缓存(已弃用)
+ clearAllCache() {
+ // 缓存功能已移除
+ }
+
+ // 获取消息列表(兼容旧方法)
+ getMessageList(options = {}) {
+ return this.getMessages(options)
+ }
+
+ // 发送文本消息
+ async sendTextMessage(text, cloudCustomData = '') {
+ if (!text.trim()) return
+
+ if (!this.tim) {
+ this.triggerCallback('onError', 'IM未初始化')
+ return { success: false, error: 'IM未初始化' }
+ }
+
+ // 检查登录状态
+ if (!this.isLoggedIn) {
+ console.error('IM未登录,无法发送消息');
+ this.triggerCallback('onError', 'IM未登录,请稍后重试')
+ return { success: false, error: 'IM未登录' }
+ }
+
+ // 优先使用 currentConversationID,如果没有则尝试从 conversation 获取
+ let conversationID = this.currentConversationID;
+ if (!conversationID && this.conversation) {
+ conversationID = this.conversation.conversationID;
+ }
+
+ if (!conversationID) {
+ console.error('会话ID不存在');
+ this.triggerCallback('onError', '会话不存在,请重新进入聊天')
+ return { success: false, error: '会话ID不存在' }
+ }
+
+ // 从 conversationID 提取 groupID
+ let groupID = null;
+ if (conversationID.startsWith('GROUP')) {
+ groupID = conversationID.replace('GROUP', '');
+ } else if (this.conversation?.groupProfile?.groupID) {
+ groupID = this.conversation.groupProfile.groupID;
+ }
+
+ if (!groupID) {
+ console.error('无法获取群聊ID,conversationID:', conversationID);
+ this.triggerCallback('onError', '无法获取群聊ID')
+ return { success: false, error: '无法获取群聊ID' }
+ }
+
+ const localMessage = {
+ ID: `local_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+ flow: 'out',
+ type: 'TIMTextElem',
+ payload: { text: text },
+ lastTime: Date.now(),
+ status: 'sending',
+ avatar: '/static/center/user-avatar.png',
+ conversationID: conversationID,
+ from: this.currentUserID
+ }
+
+ // 缓存功能已移除
+
+ this.triggerCallback('onMessageReceived', localMessage)
+
+ const message = this.tim.createTextMessage({
+ to: groupID,
+ conversationType: TIM.TYPES.CONV_GROUP,
+ cloudCustomData,
+ payload: { text: text }
+ })
+
+ try {
+ await this.tim.sendMessage(message)
+ localMessage.status = 'success'
+ return { success: true, message: localMessage }
+ } catch (error) {
+ console.error('文本消息发送失败:', error)
+ localMessage.status = 'failed'
+
+ // 如果是因为未登录导致的失败,尝试重连
+ if (error.message && (error.message.includes('not login') || error.message.includes('sdk not ready'))) {
+ console.log('检测到未登录错误,尝试重连...');
+ this.isLoggedIn = false;
+ this.ensureIMConnection();
+ }
+
+ return { success: false, error }
+ }
+ }
+
+ // 发送图片消息
+ async sendImageMessage(imageFile) {
+ if (!this.tim) {
+ this.triggerCallback('onError', 'IM未初始化')
+ return
+ }
+
+ if (!this.conversation) {
+ this.triggerCallback('onError', '群聊会话不存在')
+ return { success: false, error: '群聊会话不存在' }
+ }
+
+ let groupID = null
+ if (this.conversation.groupProfile && this.conversation.groupProfile.groupID) {
+ groupID = this.conversation.groupProfile.groupID
+ } else if (this.conversation.conversationID) {
+ groupID = this.conversation.conversationID.replace('GROUP', '')
+ }
+
+ if (!groupID) {
+ this.triggerCallback('onError', '无法获取群聊ID')
+ return { success: false, error: '无法获取群聊ID' }
+ }
+
+ // 确保使用当前会话的conversationID
+ const conversationID = this.conversation.conversationID || this.currentConversationID
+
+ // 获取图片尺寸信息
+ const imageInfo = await this.getImageInfo(imageFile);
+
+ const localMessage = {
+ ID: `local_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+ flow: 'out',
+ type: 'TIMImageElem',
+ payload: {
+ imageInfoArray: [{
+ url: this.getImageUrl(imageFile),
+ width: imageInfo.width,
+ height: imageInfo.height
+ }]
+ },
+ lastTime: Date.now(),
+ status: 'sending',
+ avatar: '',
+ conversationID: conversationID,
+ from: this.currentUserID
+ }
+
+ console.log('创建本地图片消息:', localMessage)
+
+ // 缓存功能已移除
+
+ // 触发消息接收回调,让UI立即显示
+ this.triggerCallback('onMessageReceived', localMessage)
+
+ const message = this.tim.createImageMessage({
+ to: groupID,
+ conversationType: TIM.TYPES.CONV_GROUP,
+ payload: { file: imageFile }
+ })
+
+ try {
+ await this.tim.sendMessage(message)
+ localMessage.status = 'success'
+ return { success: true, message: localMessage }
+ } catch (error) {
+ console.error('图片消息发送失败:', error)
+ localMessage.status = 'failed'
+ return { success: false, error }
+ }
+ }
+ // 发送语音消息
+ async sendVoiceMessage(voiceFile, duration) {
+ if (!this.tim) {
+ this.triggerCallback('onError', 'IM未初始化')
+ return { success: false, error: 'IM未初始化' }
+ }
+
+ // 检查登录状态
+ if (!this.isLoggedIn) {
+ console.error('IM未登录,无法发送消息');
+ this.triggerCallback('onError', 'IM未登录,请稍后重试')
+ return { success: false, error: 'IM未登录' }
+ }
+
+ // 优先使用 currentConversationID,如果没有则尝试从 conversation 获取
+ let conversationID = this.currentConversationID;
+ if (!conversationID && this.conversation) {
+ conversationID = this.conversation.conversationID;
+ }
+
+ if (!conversationID) {
+ console.error('会话ID不存在');
+ this.triggerCallback('onError', '会话不存在,请重新进入聊天')
+ return { success: false, error: '会话ID不存在' }
+ }
+
+ // 从 conversationID 提取 groupID
+ let groupID = null;
+ if (conversationID.startsWith('GROUP')) {
+ groupID = conversationID.replace('GROUP', '');
+ } else if (this.conversation?.groupProfile?.groupID) {
+ groupID = this.conversation.groupProfile.groupID;
+ }
+
+ if (!groupID) {
+ console.error('无法获取群聊ID,conversationID:', conversationID);
+ this.triggerCallback('onError', '无法获取群聊ID')
+ return { success: false, error: '无法获取群聊ID' }
+ }
+
+ const localMessage = {
+ ID: `local_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+ flow: 'out',
+ type: 'TIMSoundElem',
+ payload: {
+ url: this.getVoiceUrl(voiceFile),
+ second: duration
+ },
+ lastTime: Date.now(),
+ status: 'sending',
+ avatar: '/static/center/user-avatar.png',
+ conversationID: conversationID,
+ from: this.currentUserID
+ }
+
+ // 缓存功能已移除
+
+ this.triggerCallback('onMessageReceived', localMessage)
+
+ const message = this.tim.createAudioMessage({
+ to: groupID,
+ conversationType: TIM.TYPES.CONV_GROUP,
+ payload: { file: voiceFile }
+ })
+
+ try {
+ await this.tim.sendMessage(message)
+ localMessage.status = 'success'
+ return { success: true, message: localMessage }
+ } catch (error) {
+ console.error('语音消息发送失败:', error)
+ localMessage.status = 'failed'
+
+ // 如果是因为未登录导致的失败,尝试重连
+ if (error.message && (error.message.includes('not login') || error.message.includes('sdk not ready'))) {
+ console.log('检测到未登录错误,尝试重连...');
+ this.isLoggedIn = false;
+ this.ensureIMConnection();
+ }
+
+ return { success: false, error }
+ }
+ }
+
+ // 发送自定义消息
+ async sendCustomMessage(messageData) {
+ if (!this.tim) {
+ this.triggerCallback('onError', 'IM未初始化')
+ return { success: false, error: 'IM未初始化' }
+ }
+
+ // 检查登录状态
+ if (!this.isLoggedIn) {
+ console.error('IM未登录,无法发送消息');
+ this.triggerCallback('onError', 'IM未登录,请稍后重试')
+ return { success: false, error: 'IM未登录' }
+ }
+
+ // 优先使用 currentConversationID,如果没有则尝试从 conversation 获取
+ let conversationID = this.currentConversationID;
+ if (!conversationID && this.conversation) {
+ conversationID = this.conversation.conversationID;
+ }
+
+ if (!conversationID) {
+ console.error('会话ID不存在');
+ this.triggerCallback('onError', '会话不存在,请重新进入聊天')
+ return { success: false, error: '会话ID不存在' }
+ }
+
+ // 从 conversationID 提取 groupID
+ let groupID = null;
+ if (conversationID.startsWith('GROUP')) {
+ groupID = conversationID.replace('GROUP', '');
+ } else if (this.conversation?.groupProfile?.groupID) {
+ groupID = this.conversation.groupProfile.groupID;
+ }
+
+ if (!groupID) {
+ console.error('无法获取群聊ID,conversationID:', conversationID);
+ this.triggerCallback('onError', '无法获取群聊ID')
+ return { success: false, error: '无法获取群聊ID' }
+ }
+
+ const localMessage = {
+ ID: `local_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+ flow: 'out',
+ type: 'TIMCustomElem',
+ payload: {
+ data: JSON.stringify(messageData),
+ description: messageData.content || '自定义消息',
+ extension: messageData.messageType || 'custom'
+ },
+ lastTime: Date.now(),
+ status: 'sending',
+ avatar: '/static/center/user-avatar.png',
+ conversationID: conversationID,
+ from: this.currentUserID
+ }
+
+ // 缓存功能已移除
+
+ this.triggerCallback('onMessageReceived', localMessage)
+
+ const message = this.tim.createCustomMessage({
+ to: groupID,
+ conversationType: TIM.TYPES.CONV_GROUP,
+ payload: {
+ data: JSON.stringify(messageData),
+ description: messageData.content || '自定义消息',
+ extension: messageData.messageType || 'custom'
+ }
+ })
+
+ try {
+ await this.tim.sendMessage(message)
+ localMessage.status = 'success'
+ return { success: true, message: localMessage }
+ } catch (error) {
+ console.error('自定义消息发送失败:', error)
+ localMessage.status = 'failed'
+
+ // 如果是因为未登录导致的失败,尝试重连
+ if (error.message && (error.message.includes('not login') || error.message.includes('sdk not ready'))) {
+ console.log('检测到未登录错误,尝试重连...');
+ this.isLoggedIn = false;
+ this.ensureIMConnection();
+ }
+
+ return { success: false, error }
+ }
+ }
+
+ // 工具方法
+ // 判断是否为系统消息
+ isSystemMessage(message) {
+ if (message.type !== 'TIMCustomElem') {
+ return false
+ }
+
+ // 检查 payload.data 是否包含系统消息标记
+ try {
+ if (message.payload && message.payload.data) {
+ const data = typeof message.payload.data === 'string'
+ ? JSON.parse(message.payload.data)
+ : message.payload.data
+
+ // 检查是否为系统消息类型
+ if (data.type === 'system_message') {
+ return true
+ }
+ }
+
+ // 检查 description 是否为系统消息标记
+ if (message.payload && message.payload.description === '系统消息标记') {
+ return true
+ }
+
+ // 兼容旧的系统消息格式
+ if (message.payload && message.payload.description === 'SYSTEM_NOTIFICATION') {
+ return true
+ }
+ } catch (error) {
+ console.error('判断系统消息失败:', error)
+ }
+
+ return false
+ }
+
+ filterMessage(message) {
+ if (message.type === 'TIMCustomElem' && message.payload && message.payload.data) {
+ if (message.payload.data === 'group_create' || message.payload.data === 'purchased') {
+ console.log('过滤消息:', message.ID, message.payload.data)
+ return false
+ }
+ }
+ return true
+ }
+
+ convertMessageFormat(timMessage) {
+ let lastTime
+ if (timMessage.lastTime) {
+ lastTime = timMessage.lastTime * 1000
+ } else if (timMessage.time) {
+ lastTime = timMessage.time * 1000
+ } else if (timMessage.timestamp) {
+ lastTime = timMessage.timestamp * 1000
+ } else {
+ lastTime = Date.now()
+ }
+ const message = {
+ ID: timMessage.ID,
+ from: timMessage.from,
+ flow: timMessage.flow,
+ type: timMessage.type,
+ payload: timMessage.payload,
+ lastTime: lastTime,
+ status: timMessage.status || 'success',
+ avatar: timMessage.flow === 'in' ? '/static/home/avatar.svg' : '/static/center/user-avatar.png',
+ // 优先使用消息本身的conversationID,确保消息归属正确的会话
+ conversationID: timMessage.conversationID
+ }
+
+ return message
+ }
+
+ formatLastMessage(message) {
+ try {
+ switch (message.type) {
+ case 'TIMTextElem':
+ return message.payload.text || '[文本消息]'
+ case 'TIMImageElem':
+ return '[图片]'
+ case 'TIMSoundElem':
+ return '[语音]'
+ case 'TIMCustomElem':
+ try {
+ const customData = JSON.parse(message.payload.data)
+ if (customData.messageType === 'symptom') {
+ return '[病情描述]'
+ } else if (customData.messageType === 'prescription') {
+ return '[处方单]'
+ } else if (customData.messageType === 'refill') {
+ return '[续方申请]'
+ } else if (customData.messageType === 'survey') {
+ return '[问卷调查]'
+ } else {
+ return customData.content || '[自定义消息]'
+ }
+ } catch (error) {
+ return '[自定义消息]'
+ }
+ default:
+ return '[未知消息类型]'
+ }
+ } catch (error) {
+ console.error('格式化最后一条消息失败:', error)
+ return '[消息]'
+ }
+ }
+
+ getImageUrl(imageFile) {
+ // 处理 tempFiles 数组格式
+ if (imageFile?.tempFiles?.length > 0) {
+ return imageFile.tempFiles[0].tempFilePath
+ }
+ // 处理单个文件对象
+ if (imageFile?.tempFilePath) {
+ return imageFile.tempFilePath
+ }
+ // 处理字符串路径
+ if (typeof imageFile === 'string') {
+ return imageFile
+ }
+ console.warn('无法获取图片URL,使用默认图片:', imageFile);
+ return '/static/home/photo.png'
+ }
+
+ // 获取图片尺寸信息
+ getImageInfo(imageFile) {
+ return new Promise((resolve) => {
+ let imagePath = '';
+
+ // 获取图片路径 - 处理多种格式
+ if (imageFile?.tempFilePaths?.length > 0) {
+ // wx.chooseImage 返回的对象
+ imagePath = imageFile.tempFilePaths[0];
+ } else if (imageFile?.tempFiles?.length > 0) {
+ // 从 tempFiles 中提取路径
+ const tempFile = imageFile.tempFiles[0];
+ imagePath = tempFile.path || tempFile.tempFilePath;
+ } else if (imageFile?.tempFilePath) {
+ imagePath = imageFile.tempFilePath;
+ } else if (typeof imageFile === 'string') {
+ imagePath = imageFile;
+ } else {
+ console.warn('无法获取图片路径,使用默认尺寸:', imageFile);
+ // 默认尺寸
+ resolve({ width: 400, height: 300 });
+ return;
+ }
+
+ console.log('获取图片信息,路径:', imagePath);
+
+ // 使用uni.getImageInfo获取图片尺寸
+ uni.getImageInfo({
+ src: imagePath,
+ success: (res) => {
+ console.log('获取图片尺寸成功:', res);
+ resolve({
+ width: res.width,
+ height: res.height
+ });
+ },
+ fail: (err) => {
+ console.error('获取图片尺寸失败:', err);
+ // 失败时使用默认尺寸
+ resolve({ width: 400, height: 300 });
+ }
+ });
+ });
+ }
+
+ getVoiceUrl(voiceFile) {
+ if (typeof voiceFile === 'string') {
+ return voiceFile
+ }
+ if (voiceFile && voiceFile.tempFilePath) {
+ return voiceFile.tempFilePath
+ }
+ if (voiceFile && voiceFile.filePath) {
+ return voiceFile.filePath
+ }
+ return '/static/voice/default.mp3'
+ }
+
+ // 标记会话为已读
+ markConversationAsRead(conversationID) {
+ if (!this.tim || !this.isLoggedIn) return
+
+ try {
+ let formattedConversationID = conversationID
+ if (!conversationID.startsWith('GROUP')) {
+ formattedConversationID = `GROUP${conversationID}`
+ }
+
+ this.tim.setMessageRead({
+ conversationID: formattedConversationID
+ }).then(() => {
+ this.triggerCallback('onConversationListUpdated', {
+ conversationID: formattedConversationID,
+ unreadCount: 0
+ })
+ }).catch(error => {
+ console.error('标记会话已读失败:', error)
+ })
+ } catch (error) {
+ console.error('标记会话已读异常:', error)
+ }
+ }
+
+ // 更新会话列表
+ updateConversationListOnNewMessage(message) {
+ try {
+ const conversationID = message.conversationID || this.currentConversationID
+ if (!conversationID) return
+
+ const conversationUpdate = {
+ conversationID: conversationID,
+ lastMessage: this.formatLastMessage(message),
+ lastMessageTime: message.lastTime,
+ unreadCount: 1,
+ messageType: message.type,
+ messageFlow: message.flow
+ }
+
+ this.triggerCallback('onConversationListUpdated', {
+ reason: 'NEW_MESSAGE_RECEIVED',
+ conversation: conversationUpdate,
+ message: message
+ })
+ } catch (error) {
+ console.error('更新会话列表失败:', error)
+ }
+ }
+
+ // 设置回调函数
+ setCallback(event, callback) {
+ this.callbacks[event] = callback
+ }
+
+ // 触发回调
+ triggerCallback(event, data) {
+ if (this.callbacks[event]) {
+ this.callbacks[event](data)
+ }
+ }
+
+ // 清理资源(优化版:更完整的清理)
+ destroy() {
+ console.log('=== 开始清理IM资源 ===')
+
+ // 清理所有定时器(使用统一方法)
+ this.clearAllTimers()
+
+ // 清理消息缓存(已弃用)
+ console.log('缓存功能已移除')
+
+ // 清理TIM实例
+ if (this.tim) {
+ try {
+ // 移除事件监听器
+ this.removeEventListeners()
+
+ // 退出登录(同步处理,不等待结果)
+ if (typeof this.tim.logout === 'function') {
+ this.tim.logout()
+ .then(() => console.log('✓ TIM登出成功'))
+ .catch(error => console.warn('TIM登出失败:', error))
+ }
+ } catch (error) {
+ console.error('清理TIM时出错:', error)
+ }
+ }
+
+ // 重置所有状态(使用统一方法)
+ this.resetAllStates()
+
+ // 清理其他状态
+ this.messageList = []
+ this.conversation = null
+ this.currentUserID = ''
+ this.currentUserSig = ''
+ this.currentConversationID = null
+ this.tim = null
+
+ console.log('=== IM资源清理完成 ===')
+ }
+}
+
+// 创建全局IM管理器实例
+const globalTimChatManager = new TimChatManager()
+
+// 全局IM初始化函数 - 支持强制重新初始化
+let initPromise = null;
+const initGlobalTIM = async (userID, forceReinit = false) => {
+ console.log('开始初始化全局IM,userID:', userID, 'forceReinit:', forceReinit)
+
+ if (globalTimChatManager.isInitializing) {
+ console.log('全局IM正在初始化中,跳过重复初始化')
+ return true
+ }
+
+ if (forceReinit && globalTimChatManager.tim) {
+ console.log('强制重新初始化,清理现有IM状态')
+ globalTimChatManager.isLoggedIn = false
+ globalTimChatManager.lastLoginTime = 0
+ globalTimChatManager.reconnectAttempts = 0
+
+ if (globalTimChatManager.tim.isLoggedIn) {
+ await globalTimChatManager.tim.logout()
+ console.log('强制重新初始化:TIM登出成功')
+ }
+
+ await globalTimChatManager.initTIM(userID)
+ console.log('强制重新初始化完成')
+ return true
+ }
+
+ if (!forceReinit && globalTimChatManager.tim && globalTimChatManager.isLoggedIn) {
+ console.log('全局IM已经初始化并登录,跳过重复初始化')
+ return true
+ }
+
+ await globalTimChatManager.initTIM(userID)
+ console.log('全局IM初始化成功')
+ return true
+}
+const getInitIMPromise = async (userID, forceReinit) => {
+ if (initPromise) return initPromise;
+ initPromise = initGlobalTIM(userID, forceReinit);
+ return initPromise;
+}
+const clearInitIMPromise = () => {
+ initPromise = null;
+}
+
+// 获取群聊列表(全局函数)
+const getGroupList = async () => {
+ return await globalTimChatManager.getGroupList()
+}
+
+// 进入群聊
+const enterChatGroupRoom = async (chatGroup, navigateType = 'navigateTo', viewType) => {
+ const groupListResult = await globalTimChatManager.getGroupList();
+ const groupList = groupListResult && Array.isArray(groupListResult.groupList) ? groupListResult.groupList : [];
+ let group = groupList.find(g => g.groupID === chatGroup.groupID);
+
+ const navigateFn = ['navigateTo', 'redirectTo', 'reLaunch'].includes(navigateType) ? navigateType : 'navigateTo'
+ uni[navigateFn]({
+ url: `/pages-im/IM/index?conversationID=GROUP${chatGroup.groupID}&groupID=${chatGroup.groupID}&conversationType=GROUP&viewType=${viewType}`,
+ })
+}
+
+// 检查全局IM状态
+const checkGlobalIMStatus = () => {
+ return globalTimChatManager.isLoggedIn
+}
+
+// 清理指定会话的缓存
+const clearConversationCache = (conversationID) => {
+ return globalTimChatManager.clearConversationCache(conversationID)
+}
+
+// 清理所有消息缓存
+const clearAllMessageCache = () => {
+ return globalTimChatManager.clearAllCache()
+}
+
+// 确保全局IM连接
+const ensureGlobalIMConnection = async () => {
+ return await globalTimChatManager.ensureIMConnection()
+}
+
+// 获取全局IM登录状态
+const getGlobalIMLoginStatus = () => {
+ return globalTimChatManager.isLoggedIn
+}
+
+// 设置全局IM回调
+const setGlobalIMCallback = (event, callback) => {
+ globalTimChatManager.setCallback(event, callback)
+}
+
+// 导出
+export {
+ TimChatManager,
+ globalTimChatManager,
+ initGlobalTIM,
+ getInitIMPromise,
+ clearInitIMPromise,
+ enterChatGroupRoom,
+ checkGlobalIMStatus,
+ ensureGlobalIMConnection,
+ getGlobalIMLoginStatus,
+ setGlobalIMCallback,
+ getGroupList,
+ clearConversationCache,
+ clearAllMessageCache
+}
+
+export default globalTimChatManager