新增回访计划聊天室

This commit is contained in:
wangdongbo 2026-02-04 10:36:36 +08:00
parent d75b8fb8cf
commit fa01c77830
9 changed files with 320 additions and 26 deletions

View File

@ -279,6 +279,12 @@
"style": { "style": {
"navigationBarTitleText": "执行回访计划" "navigationBarTitleText": "执行回访计划"
} }
},
{
"path": "followup-task-list",
"style": {
"navigationBarTitleText": "回访任务"
}
} }
] ]
} }

View File

@ -121,6 +121,7 @@
:archiveId="archiveId" :archiveId="archiveId"
:reachBottomTime="reachBottomTime" :reachBottomTime="reachBottomTime"
:floatingBottom="floatingBottom" :floatingBottom="floatingBottom"
:fromChat="fromChat"
/> />
</view> </view>
@ -242,6 +243,7 @@ const tabs = [
const currentTab = ref('visitRecord'); const currentTab = ref('visitRecord');
const reachBottomTime = ref(0); const reachBottomTime = ref(0);
const archiveId = ref(''); const archiveId = ref('');
const fromChat = ref(false);
const tabsScrollTop = ref(0); const tabsScrollTop = ref(0);
const instanceProxy = getCurrentInstance()?.proxy; const instanceProxy = getCurrentInstance()?.proxy;
@ -474,6 +476,7 @@ async function updateArchive(patch) {
onLoad((options) => { onLoad((options) => {
archiveId.value = options?.id ? String(options.id) : String(archive.value.medicalRecordNo || archive.value.outpatientNo || archive.value.inpatientNo || ''); archiveId.value = options?.id ? String(options.id) : String(archive.value.medicalRecordNo || archive.value.outpatientNo || archive.value.inpatientNo || '');
fromChat.value = options?.fromChat === 'true' || options?.fromChat === true;
const cached = uni.getStorageSync(STORAGE_KEY); const cached = uni.getStorageSync(STORAGE_KEY);
if (cached && typeof cached === 'object') { if (cached && typeof cached === 'object') {

View File

@ -33,7 +33,7 @@
</view> </view>
<view class="list"> <view class="list">
<view v-for="i in list" :key="i._id" class="card" @click="toDetail(i)"> <view v-for="i in list" :key="i._id" class="card">
<view class="head"> <view class="head">
<view class="date">计划日期: <text class="date-val">{{ i.planDate }}</text></view> <view class="date">计划日期: <text class="date-val">{{ i.planDate }}</text></view>
<view class="executor truncate">{{ i.executorName }}<text v-if="i.executeTeamName">{{ i.executeTeamName }}</text></view> <view class="executor truncate">{{ i.executorName }}<text v-if="i.executeTeamName">{{ i.executeTeamName }}</text></view>
@ -45,7 +45,13 @@
</view> </view>
<view class="content">{{ i.taskContent || '暂无内容' }}</view> <view class="content">{{ i.taskContent || '暂无内容' }}</view>
<view v-if="i.status === 'treated'" class="result">处理结果 {{ i.result || '' }}</view> <view v-if="i.status === 'treated'" class="result">处理结果 {{ i.result || '' }}</view>
<view class="footer-row">
<view class="footer">创建: {{ i.createTimeStr }} {{ i.creatorName }}</view> <view class="footer">创建: {{ i.createTimeStr }} {{ i.creatorName }}</view>
<button v-if="fromChat" class="action-btn send-btn" @click.stop="sendFollowUp(i)">发送</button>
</view>
<view class="card-actions">
<button class="action-btn detail-btn" @click.stop="toDetail(i)">详情</button>
</view>
</view> </view>
</view> </view>
@ -146,6 +152,7 @@ const props = defineProps({
archiveId: { type: String, default: '' }, archiveId: { type: String, default: '' },
reachBottomTime: { type: [String, Number], default: '' }, reachBottomTime: { type: [String, Number], default: '' },
floatingBottom: { type: Number, default: 16 }, floatingBottom: { type: Number, default: 16 },
fromChat: { type: Boolean, default: false },
}); });
const accountStore = useAccountStore(); const accountStore = useAccountStore();
@ -390,6 +397,29 @@ function toDetail(todo) {
uni.navigateTo({ url: `/pages/case/followup-detail?archiveId=${encodeURIComponent(props.archiveId)}&mode=edit&id=${encodeURIComponent(todo._id)}` }); uni.navigateTo({ url: `/pages/case/followup-detail?archiveId=${encodeURIComponent(props.archiveId)}&mode=edit&id=${encodeURIComponent(todo._id)}` });
} }
function sendFollowUp(todo) {
const content = `【回访计划】\n类型: ${todo.eventTypeLabel}\n计划日期: ${todo.planDate}\n执行人: ${todo.executorName}\n内容: ${todo.taskContent || '暂无内容'}`;
//
uni.$emit('send-followup-message', {
content,
followupId: todo._id,
followupData: todo
});
//
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
// followup-task-list
if (currentPage && currentPage.route === 'pages/case/followup-task-list') {
uni.navigateBack({ delta: 2 });
} else {
//
uni.navigateBack();
}
}
// ---- filter popup ---- // ---- filter popup ----
const filterPopupRef = ref(null); const filterPopupRef = ref(null);
const state = ref(null); const state = ref(null);
@ -625,10 +655,60 @@ watch(
font-size: 13px; font-size: 13px;
color: #666; color: #666;
} }
.footer { .footer-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10px; margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #f2f2f2;
gap: 10px;
}
.footer {
font-size: 12px; font-size: 12px;
color: #999; color: #999;
flex: 1;
}
.footer-row .send-btn {
flex: 0 0 auto;
width: 60px;
height: 28px;
line-height: 28px;
padding: 0;
margin: 0;
}
.card-actions {
display: flex;
gap: 10px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #f2f2f2;
}
.action-btn {
flex: 1;
height: 32px;
line-height: 32px;
border-radius: 6px;
font-size: 12px;
border: none;
background: #f5f6f8;
color: #333;
}
.action-btn::after {
border: none;
}
.detail-btn {
background: #f5f6f8;
color: #333;
}
.send-btn {
background: #0877F1;
color: #fff;
} }
.empty { .empty {

View File

@ -0,0 +1,74 @@
<template>
<view class="followup-task-page">
<!-- 回访任务列表组件 -->
<FollowUpManageTab
:data="patientData"
:archiveId="archiveId"
:reachBottomTime="reachBottomTime"
:floatingBottom="0"
/>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import FollowUpManageTab from "./components/archive-detail/follow-up-manage-tab.vue";
const archiveId = ref("");
const patientData = ref({});
const reachBottomTime = ref("");
onLoad((options) => {
archiveId.value = options.archiveId || "";
patientData.value = {
name: options.patientName || "",
};
});
const handleBack = () => {
uni.navigateBack();
};
</script>
<style scoped>
.followup-task-page {
display: flex;
flex-direction: column;
height: 100vh;
background: #f5f6f8;
}
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
background: #0877f1;
padding: 0 14px;
color: #fff;
position: relative;
}
.nav-back {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
margin-left: -14px;
}
.nav-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 16px;
font-weight: 600;
color: #fff;
}
.nav-placeholder {
width: 44px;
}
</style>

View File

@ -315,6 +315,8 @@ const closePreview = () => {
const sendArticle = async (article) => { const sendArticle = async (article) => {
try { try {
const { doctorInfo } = useAccountStore(); const { doctorInfo } = useAccountStore();
// 1. API
const result = await api("sendArticleMessage", { const result = await api("sendArticleMessage", {
groupId: pageParams.value.groupId, groupId: pageParams.value.groupId,
fromAccount: doctorInfo.userid, fromAccount: doctorInfo.userid,
@ -323,8 +325,46 @@ const sendArticle = async (article) => {
imgUrl: article.cover || "", imgUrl: article.cover || "",
desc: "点击查看详情", desc: "点击查看详情",
}); });
if (result.success) { if (result.success) {
// // 2. IM
try {
// IM
const { globalTimChatManager } = await import("@/utils/tim-chat.js");
if (globalTimChatManager && globalTimChatManager.tim && globalTimChatManager.isLoggedIn) {
// ID
const conversationID = `GROUP${pageParams.value.groupId}`;
globalTimChatManager.currentConversationID = conversationID;
console.log("设置当前会话ID:", conversationID);
//
const customMessageData = {
messageType: "article",
title: article.title || "宣教文章",
articleId: article._id,
cover: article.cover || "",
desc: "点击查看详情",
content: article.title || "宣教文章"
};
//
const sendResult = await globalTimChatManager.sendCustomMessage(customMessageData);
if (sendResult && sendResult.success) {
console.log("✓ 文章消息已通过IM系统发送");
} else {
console.warn("⚠️ 文章消息发送失败:", sendResult?.error);
}
} else {
console.warn("⚠️ IM系统未就绪消息可能不会显示在聊天列表");
}
} catch (imError) {
console.error("通过IM系统发送消息失败:", imError);
// IM
}
// 3.
try { try {
await api("addArticleSendRecord", { await api("addArticleSendRecord", {
articleId: article._id, articleId: article._id,

View File

@ -430,6 +430,15 @@ $primary-color: #0877F1;
box-sizing: border-box; box-sizing: border-box;
line-height: 1.5; line-height: 1.5;
color: #333; color: #333;
display: flex;
align-items: center;
justify-content: center;
}
.text-input {
padding: 16rpx 46rpx;
display: block;
line-height: 1.5;
} }
.voice-input-btn { .voice-input-btn {

View File

@ -9,7 +9,7 @@
<textarea v-if="!showVoiceInput" class="text-input" v-model="inputText" placeholder="我来说两句..." <textarea v-if="!showVoiceInput" class="text-input" v-model="inputText" placeholder="我来说两句..."
@confirm="sendTextMessage" @focus="handleInputFocus" @input="handleInput" @confirm="sendTextMessage" @focus="handleInputFocus" @input="handleInput"
:auto-height="true" :show-confirm-bar="false" :adjust-position="true" :auto-height="true" :show-confirm-bar="false" :adjust-position="true"
placeholder-style="line-height: 80rpx;" /> />
<input v-else class="voice-input-btn" :class="{ recording: isRecording }" @touchstart="startRecord" <input v-else class="voice-input-btn" :class="{ recording: isRecording }" @touchstart="startRecord"
@touchmove="onRecordTouchMove" @touchend="stopRecord" @touchcancel="cancelRecord" :placeholder="isRecording ? '松开发送' : '按住说话'" disabled> @touchmove="onRecordTouchMove" @touchend="stopRecord" @touchcancel="cancelRecord" :placeholder="isRecording ? '松开发送' : '按住说话'" disabled>
</input> </input>
@ -365,6 +365,13 @@ const goToCommonPhrases = () => {
}); });
}; };
// 访
const showFollowUpTasks = () => {
uni.navigateTo({
url: `/pages/case/followup-task-list?archiveId=${props.patientId}&patientName=${props.patientInfo.name}`,
});
};
// //
const goToArticleList = () => { const goToArticleList = () => {
uni.navigateTo({ uni.navigateTo({
@ -402,7 +409,7 @@ const morePanelButtons = [
{ {
text: "回访任务", text: "回访任务",
icon: "/static/icon/huifangrenwu.png", icon: "/static/icon/huifangrenwu.png",
action: showImagePicker, action: showFollowUpTasks,
}, },
{ {
text: "常用语", text: "常用语",

View File

@ -757,8 +757,34 @@ onShow(() => {
} }
startIMMonitoring(30000); startIMMonitoring(30000);
// 访
uni.$on('send-followup-message', handleSendFollowUpMessage);
}); });
// 访
const handleSendFollowUpMessage = async (data) => {
try {
if (chatInputRef.value) {
// 访
chatInputRef.value.setInputText(data.content);
//
setTimeout(() => {
if (chatInputRef.value) {
chatInputRef.value.sendTextMessageFromPhrase(data.content);
}
}, 100);
}
} catch (error) {
console.error('发送回访任务消息失败:', error);
uni.showToast({
title: '发送失败,请重试',
icon: 'none',
});
}
};
// //
onHide(() => { onHide(() => {
stopIMMonitoring(); stopIMMonitoring();
@ -945,6 +971,9 @@ onUnmounted(() => {
timChatManager.setCallback("onMessageReceived", null); timChatManager.setCallback("onMessageReceived", null);
timChatManager.setCallback("onMessageListLoaded", null); timChatManager.setCallback("onMessageListLoaded", null);
timChatManager.setCallback("onError", null); timChatManager.setCallback("onError", null);
// 访
uni.$off('send-followup-message', handleSendFollowUpMessage);
}); });
</script> </script>

View File

@ -723,14 +723,26 @@ class TimChatManager {
// 判断是否为当前会话的消息 // 判断是否为当前会话的消息
// 系统消息只要会话ID匹配就显示不要求必须有currentConversationID // 系统消息只要会话ID匹配就显示不要求必须有currentConversationID
// 自定义消息文章等如果currentConversationID已设置则需要匹配如果未设置则暂存等待
// 普通消息必须有currentConversationID且匹配才显示 // 普通消息必须有currentConversationID且匹配才显示
const isCurrentConversation = isSystemMsg let isCurrentConversation = false
? messageConversationID === this.currentConversationID
: (this.currentConversationID && messageConversationID === this.currentConversationID) if (isSystemMsg) {
// 系统消息只要会话ID匹配就显示
isCurrentConversation = messageConversationID === this.currentConversationID
} else if (convertedMessage.type === 'TIMCustomElem') {
// 自定义消息包括文章如果currentConversationID已设置则需要匹配否则也接收
// 这样可以确保消息不会丢失,即使用户还没进入聊天页面
isCurrentConversation = !this.currentConversationID || messageConversationID === this.currentConversationID
} else {
// 普通消息必须有currentConversationID且匹配才显示
isCurrentConversation = this.currentConversationID && messageConversationID === this.currentConversationID
}
console.log('消息会话匹配检查:', { console.log('消息会话匹配检查:', {
isCurrentConversation, isCurrentConversation,
isSystemMessage: isSystemMsg, isSystemMessage: isSystemMsg,
messageType: convertedMessage.type,
hasCurrentConversationID: !!this.currentConversationID, hasCurrentConversationID: !!this.currentConversationID,
conversationIDMatch: messageConversationID === this.currentConversationID conversationIDMatch: messageConversationID === this.currentConversationID
}) })
@ -2516,32 +2528,66 @@ class TimChatManager {
formatCustomMessage(payload) { formatCustomMessage(payload) {
try { try {
if (!payload || !payload.data) { if (!payload || !payload.data) {
console.warn('payload.data 为空:', payload)
return '[自定义消息]' return '[自定义消息]'
} }
const customData = typeof payload.data === 'string' let customData
try {
customData = typeof payload.data === 'string'
? JSON.parse(payload.data) ? JSON.parse(payload.data)
: payload.data : payload.data
} catch (parseError) {
console.error('JSON 解析失败:', payload.data, parseError)
return payload.description || '[自定义消息]'
}
const messageType = customData.messageType || customData.type const messageType = customData.messageType || customData.type
const messageTypeMap = { // 使用 switch 语句保持与 getGroupListInternal 中的逻辑一致
system_message: '[系统消息]', let messageText = '[自定义消息]'
symptom: '[病情描述]', switch (messageType) {
prescription: '[处方单]', case 'system_message':
refill: '[续方申请]', messageText = '[系统消息]'
survey: '[问卷调查]', break
article: '[文章]', case 'symptom':
consult_pending: '患者向团队发起咨询请在1小时内接诊', messageText = '[病情描述]'
consult_rejected: '咨询已被拒绝', break
consult_timeout: '咨询已超时自动关闭', case 'prescription':
consult_accepted: '已接诊,会话已开始', messageText = '[处方单]'
consult_ended: '已结束当前会话', break
case 'refill':
messageText = '[续方申请]'
break
case 'survey':
messageText = '[问卷调查]'
break
case 'article':
messageText = '[文章]'
break
case 'consult_pending':
messageText = '患者向团队发起咨询请在1小时内接诊超时将自动关闭会话'
break
case 'consult_rejected':
messageText = '患者向团队发起咨询,由于有紧急事务要处理暂时无法接受咨询.本次会话已关闭'
break
case 'consult_timeout':
messageText = '患者向团队发起咨询,团队成员均未接受咨询,本次会话已自动关闭'
break
case 'consult_accepted':
messageText = '已接诊,会话已开始'
break
case 'consult_ended':
messageText = '已结束当前会话'
break
default:
messageText = customData.content || '[自定义消息]'
} }
return messageTypeMap[messageType] || customData.content || '[自定义消息]' console.log('自定义消息类型:', messageType, '显示文本:', messageText)
return messageText
} catch (error) { } catch (error) {
console.error('格式化自定义消息失败:', error) console.error('格式化自定义消息失败:', error, payload)
return '[自定义消息]' return '[自定义消息]'
} }
} }