Compare commits
8 Commits
5b21ba22d2
...
9cbe454338
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cbe454338 | ||
|
|
25925c1969 | ||
|
|
265dc9381d | ||
|
|
4d562fc3f1 | ||
| 01fc0f64c1 | |||
|
|
ed85ad990e | ||
| 87da5c69e6 | |||
| c88a1d08ab |
@ -398,7 +398,7 @@
|
|||||||
"pagePath": "pages/home/case-home",
|
"pagePath": "pages/home/case-home",
|
||||||
"iconPath": "static/tabbar/cart.png",
|
"iconPath": "static/tabbar/cart.png",
|
||||||
"selectedIconPath": "static/tabbar/cart_selected.png",
|
"selectedIconPath": "static/tabbar/cart_selected.png",
|
||||||
"text": "病历"
|
"text": "档案"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pagePath": "pages/home/work-home",
|
"pagePath": "pages/home/work-home",
|
||||||
|
|||||||
@ -1,37 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="message-page">
|
<view class="message-page">
|
||||||
<!-- 头部组件 -->
|
<!-- 头部组件 -->
|
||||||
<message-header
|
<message-header v-model:activeTab="activeTab" @team-change="handleTeamChange" @add-patient="handleAddPatient" />
|
||||||
v-model:activeTab="activeTab"
|
|
||||||
@team-change="handleTeamChange"
|
|
||||||
@add-patient="handleAddPatient"
|
|
||||||
/>
|
|
||||||
<!-- 消息列表 -->
|
<!-- 消息列表 -->
|
||||||
<scroll-view
|
<scroll-view class="message-list" scroll-y="true" refresher-enabled :refresher-triggered="refreshing"
|
||||||
class="message-list"
|
@refresherrefresh="handleRefresh" @scrolltolower="handleLoadMore">
|
||||||
scroll-y="true"
|
|
||||||
refresher-enabled
|
|
||||||
:refresher-triggered="refreshing"
|
|
||||||
@refresherrefresh="handleRefresh"
|
|
||||||
@scrolltolower="handleLoadMore"
|
|
||||||
>
|
|
||||||
<!-- 消息列表项 -->
|
<!-- 消息列表项 -->
|
||||||
<view
|
<view v-for="conversation in filteredConversationList" :key="conversation.groupID || conversation.conversationID"
|
||||||
v-for="conversation in filteredConversationList"
|
class="message-item" @click="handleClickConversation(conversation)">
|
||||||
:key="conversation.groupID || conversation.conversationID"
|
|
||||||
class="message-item"
|
|
||||||
@click="handleClickConversation(conversation)"
|
|
||||||
>
|
|
||||||
<view class="avatar-container">
|
<view class="avatar-container">
|
||||||
<image
|
<image class="avatar" :src="conversation.avatar || '/static/default-patient-avatar.png'" mode="aspectFill" />
|
||||||
class="avatar"
|
|
||||||
:src="conversation.avatar || '/static/default-patient-avatar.png'"
|
|
||||||
mode="aspectFill"
|
|
||||||
/>
|
|
||||||
<view v-if="conversation.unreadCount > 0" class="unread-badge">
|
<view v-if="conversation.unreadCount > 0" class="unread-badge">
|
||||||
<text class="unread-text">{{
|
<text class="unread-text">{{
|
||||||
conversation.unreadCount > 99 ? "99+" : conversation.unreadCount
|
conversation.unreadCount > 99 ? "99+" : conversation.unreadCount
|
||||||
}}</text>
|
}}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@ -39,75 +21,63 @@
|
|||||||
<view class="header">
|
<view class="header">
|
||||||
<view class="name-info">
|
<view class="name-info">
|
||||||
<text class="name">{{ formatPatientName(conversation) }}</text>
|
<text class="name">{{ formatPatientName(conversation) }}</text>
|
||||||
<text
|
<text v-if="conversation.patientSex || conversation.patientAge" class="patient-info">
|
||||||
v-if="conversation.patientSex || conversation.patientAge"
|
|
||||||
class="patient-info"
|
|
||||||
>
|
|
||||||
{{ formatPatientInfo(conversation) }}
|
{{ formatPatientInfo(conversation) }}
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="time">{{
|
<text class="time">{{
|
||||||
formatMessageTime(conversation.lastMessageTime)
|
formatMessageTime(conversation.lastMessageTime)
|
||||||
}}</text>
|
}}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="message-preview">
|
<view class="message-preview">
|
||||||
<text class="preview-text">{{
|
<text class="preview-text">{{
|
||||||
cleanMessageText(conversation.lastMessage) || "暂无消息"
|
cleanMessageText(conversation.lastMessage) || "暂无消息"
|
||||||
}}</text>
|
}}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<view
|
<view v-if="!loading && conversationList.length === 0" class="empty-container">
|
||||||
v-if="!loading && conversationList.length === 0"
|
|
||||||
class="empty-container"
|
|
||||||
>
|
|
||||||
<image class="empty-image" src="/static/empty.svg" mode="aspectFit" />
|
<image class="empty-image" src="/static/empty.svg" mode="aspectFit" />
|
||||||
<text class="empty-text">暂无会话</text>
|
<text class="empty-text">暂无会话</text>
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view v-else-if="!loading && filteredConversationList.length === 0" class="empty-container">
|
||||||
v-else-if="!loading && filteredConversationList.length === 0"
|
|
||||||
class="empty-container"
|
|
||||||
>
|
|
||||||
<image class="empty-image" src="/static/empty.svg" mode="aspectFit" />
|
<image class="empty-image" src="/static/empty.svg" mode="aspectFit" />
|
||||||
<text class="empty-text">{{
|
<text class="empty-text">{{
|
||||||
activeTab === "processing" ? "暂无处理中的会话" : "暂无已结束的会话"
|
activeTab === "processing" ? "暂无处理中的会话" : "暂无已结束的会话"
|
||||||
}}</text>
|
}}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 加载更多 -->
|
<!-- 加载更多 -->
|
||||||
<view
|
<view v-if="hasMore && filteredConversationList.length > 0" class="load-more">
|
||||||
v-if="hasMore && filteredConversationList.length > 0"
|
|
||||||
class="load-more"
|
|
||||||
>
|
|
||||||
<text class="load-more-text">{{
|
<text class="load-more-text">{{
|
||||||
loadingMore ? "加载中..." : "上拉加载更多"
|
loadingMore ? "加载中..." : "上拉加载更多"
|
||||||
}}</text>
|
}}</text>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
<view class="subscribe-entry" @click="handleSubscribeReminder">
|
<view class="subscribe-entry" @click="handleSubscribeReminder">
|
||||||
<text class="subscribe-entry-text">接收提醒</text>
|
<text class="subscribe-entry-text">接收提醒</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
|
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import useAccountStore from "@/store/account.js";
|
import useAccountStore from "@/store/account.js";
|
||||||
import useTeamStore from "@/store/team.js";
|
import useTeamStore from "@/store/team.js";
|
||||||
import useInfoCheck from "@/hooks/useInfoCheck.js";
|
import useInfoCheck from "@/hooks/useInfoCheck.js";
|
||||||
import { globalTimChatManager } from "@/utils/tim-chat.js";
|
import { globalTimChatManager } from "@/utils/tim-chat.js";
|
||||||
import { mergeConversationWithGroupDetails } from "@/utils/conversation-merger.js";
|
import { mergeConversationWithGroupDetails } from "@/utils/conversation-merger.js";
|
||||||
import MessageHeader from "../home/components/message-header.vue";
|
import MessageHeader from "../home/components/message-header.vue";
|
||||||
import { requestConversationSubscribeMessage } from "@/utils/subscribe-message";
|
import { requestConversationSubscribeMessage } from "@/utils/subscribe-message";
|
||||||
import {
|
import {
|
||||||
SUBSCRIBE_MESSAGE_ROLE,
|
SUBSCRIBE_MESSAGE_ROLE,
|
||||||
SUBSCRIBE_MESSAGE_SCENE,
|
SUBSCRIBE_MESSAGE_SCENE,
|
||||||
} from "@/utils/subscribe-message-config";
|
} from "@/utils/subscribe-message-config";
|
||||||
|
|
||||||
// 获取登录状态
|
// 获取登录状态
|
||||||
const { account, openid, isIMInitialized, doctorInfo } = storeToRefs(
|
const { account, openid, isIMInitialized, doctorInfo } = storeToRefs(
|
||||||
@ -393,28 +363,51 @@ const setupConversationListener = () => {
|
|||||||
globalTimChatManager.setCallback("onConversationListUpdated", (eventData) => {
|
globalTimChatManager.setCallback("onConversationListUpdated", (eventData) => {
|
||||||
console.log("会话列表更新事件:", eventData);
|
console.log("会话列表更新事件:", eventData);
|
||||||
|
|
||||||
// 处理单个会话更新(标记已读的情况)
|
// 兼容单个会话更新,以及 { reason, conversation, message } 的增量事件结构。
|
||||||
if (eventData && !Array.isArray(eventData) && eventData.conversationID) {
|
const conversationUpdate = eventData?.conversation || eventData;
|
||||||
const conversationID = eventData.conversationID;
|
if (
|
||||||
|
conversationUpdate &&
|
||||||
|
!Array.isArray(conversationUpdate) &&
|
||||||
|
conversationUpdate.conversationID
|
||||||
|
) {
|
||||||
|
const conversationID = conversationUpdate.conversationID;
|
||||||
const existingIndex = conversationList.value.findIndex(
|
const existingIndex = conversationList.value.findIndex(
|
||||||
(conv) => conv.conversationID === conversationID
|
(conv) => conv.conversationID === conversationID
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingIndex !== -1) {
|
if (existingIndex !== -1) {
|
||||||
// 直接更新未读数,避免触发整个对象的响应式更新
|
const conversation = conversationList.value[existingIndex];
|
||||||
if (eventData.unreadCount !== undefined) {
|
const lastMessageTimeChanged =
|
||||||
conversationList.value[existingIndex].unreadCount =
|
conversationUpdate.lastMessageTime !== undefined &&
|
||||||
eventData.unreadCount;
|
conversation.lastMessageTime !== conversationUpdate.lastMessageTime;
|
||||||
console.log(
|
|
||||||
`已清空会话未读数: ${conversationList.value[existingIndex].name}, unreadCount: ${eventData.unreadCount}`
|
if (conversationUpdate.lastMessage !== undefined) {
|
||||||
|
conversation.lastMessage = conversationUpdate.lastMessage;
|
||||||
|
}
|
||||||
|
if (conversationUpdate.lastMessageTime !== undefined) {
|
||||||
|
conversation.lastMessageTime = conversationUpdate.lastMessageTime;
|
||||||
|
}
|
||||||
|
if (conversationUpdate.unreadCount !== undefined) {
|
||||||
|
conversation.unreadCount = conversationUpdate.unreadCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastMessageTimeChanged) {
|
||||||
|
conversationList.value.sort(
|
||||||
|
(a, b) => b.lastMessageTime - a.lastMessageTime
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
console.log(`已更新会话: ${conversation.name}`, conversationUpdate);
|
||||||
|
} else {
|
||||||
|
// 增量事件未携带患者、团队等展示信息,重新拉取完整列表。
|
||||||
|
loadConversationList();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!eventData || !Array.isArray(eventData)) {
|
if (!eventData || !Array.isArray(eventData)) {
|
||||||
console.warn("会话列表更新事件数据格式错误");
|
console.warn("会话列表更新事件数据格式错误");
|
||||||
|
console.log('eventData: ', typeof eventData)
|
||||||
|
console.log(typeof eventData === 'object' && eventData ? JSON.stringify(eventData) : eventData);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -642,29 +635,29 @@ const handleLoadMore = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 下拉刷新
|
// 下拉刷新
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
refreshing.value = true;
|
refreshing.value = true;
|
||||||
try {
|
try {
|
||||||
await loadConversationList();
|
await loadConversationList();
|
||||||
} finally {
|
} finally {
|
||||||
refreshing.value = false;
|
refreshing.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubscribeReminder = async () => {
|
const handleSubscribeReminder = async () => {
|
||||||
await requestConversationSubscribeMessage({
|
await requestConversationSubscribeMessage({
|
||||||
role: SUBSCRIBE_MESSAGE_ROLE.DOCTOR,
|
role: SUBSCRIBE_MESSAGE_ROLE.DOCTOR,
|
||||||
scene: SUBSCRIBE_MESSAGE_SCENE.LIST,
|
scene: SUBSCRIBE_MESSAGE_SCENE.LIST,
|
||||||
userId: doctorInfo.value?.userid || account.value?.userId || "",
|
userId: doctorInfo.value?.userid || account.value?.userId || "",
|
||||||
openid: openid.value || account.value?.openid || "",
|
openid: openid.value || account.value?.openid || "",
|
||||||
unionid: account.value?.unionid || "",
|
unionid: account.value?.unionid || "",
|
||||||
doctorId: doctorInfo.value?.userid || "",
|
doctorId: doctorInfo.value?.userid || "",
|
||||||
extraData: {
|
extraData: {
|
||||||
teamId: currentTeamId.value || "",
|
teamId: currentTeamId.value || "",
|
||||||
page: "pages/home/message-home",
|
page: "pages/home/message-home",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 页面加载
|
// 页面加载
|
||||||
onLoad(() => {
|
onLoad(() => {
|
||||||
@ -864,36 +857,36 @@ onHide(() => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.load-more-text {
|
.load-more-text {
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subscribe-entry {
|
.subscribe-entry {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 32rpx;
|
right: 32rpx;
|
||||||
bottom: 180rpx;
|
bottom: 180rpx;
|
||||||
width: 116rpx;
|
width: 116rpx;
|
||||||
height: 116rpx;
|
height: 116rpx;
|
||||||
border-radius: 58rpx;
|
border-radius: 58rpx;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 2rpx solid #3876f6;
|
border: 2rpx solid #3876f6;
|
||||||
box-shadow: 0 8rpx 24rpx rgba(56, 118, 246, 0.16);
|
box-shadow: 0 8rpx 24rpx rgba(56, 118, 246, 0.16);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subscribe-entry:active {
|
.subscribe-entry:active {
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subscribe-entry-text {
|
.subscribe-entry-text {
|
||||||
width: 56rpx;
|
width: 56rpx;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
color: #3876f6;
|
color: #3876f6;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -107,6 +107,7 @@
|
|||||||
{{ i.eventStatusLabel }}
|
{{ i.eventStatusLabel }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view v-if="i.taskContent" class="px-15 text-base leading-normal text-dark">{{ i.taskContent }}</view>
|
||||||
<view v-if="i.sendContent" class="px-15 text-base leading-normal text-gray">{{ i.sendContent }}</view>
|
<view v-if="i.sendContent" class="px-15 text-base leading-normal text-gray">{{ i.sendContent }}</view>
|
||||||
<view v-if="i.enableSend && i.fileList.length" class="mt-10 px-15 flex items-center">
|
<view v-if="i.enableSend && i.fileList.length" class="mt-10 px-15 flex items-center">
|
||||||
<view class="mr-5 w-0 flex-grow">
|
<view class="mr-5 w-0 flex-grow">
|
||||||
@ -253,7 +254,9 @@ function editProfile() {
|
|||||||
|
|
||||||
function handleCert() {
|
function handleCert() {
|
||||||
if (doctorInfo.value.verifyStatus === 'verifying') {
|
if (doctorInfo.value.verifyStatus === 'verifying') {
|
||||||
toast('信息认证中,请耐心等待!')
|
uni.navigateTo({
|
||||||
|
url: '/pages/work/profile?type=cert',
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
visible.value = true
|
visible.value = true
|
||||||
}
|
}
|
||||||
@ -282,7 +285,7 @@ async function getList() {
|
|||||||
startDate: filterData.value.startDate,
|
startDate: filterData.value.startDate,
|
||||||
endDate: filterData.value.endDate,
|
endDate: filterData.value.endDate,
|
||||||
page: requestPage,
|
page: requestPage,
|
||||||
pageSize: pageSize.value
|
pageSize: pageSize.value
|
||||||
}
|
}
|
||||||
if (followUpType.value === 'person') {
|
if (followUpType.value === 'person') {
|
||||||
data.executorUserId = doctorInfo.value.userid;
|
data.executorUserId = doctorInfo.value.userid;
|
||||||
@ -314,6 +317,8 @@ async function getList() {
|
|||||||
const newItems = arr.filter(item => !item._id || !existingIds.has(item._id));
|
const newItems = arr.filter(item => !item._id || !existingIds.has(item._id));
|
||||||
list.value = [...list.value, ...newItems];
|
list.value = [...list.value, ...newItems];
|
||||||
}
|
}
|
||||||
|
console.clear()
|
||||||
|
console.log(list.value)
|
||||||
total.value = res && res.total > 0 ? res.total : 0;
|
total.value = res && res.total > 0 ? res.total : 0;
|
||||||
pages.value = res && res.pages > 0 ? res.pages : 0;
|
pages.value = res && res.pages > 0 ? res.pages : 0;
|
||||||
if (!res || !res.success) {
|
if (!res || !res.success) {
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
<view class="field-group">
|
<view class="field-group">
|
||||||
<view class="field-label">开单科室</view>
|
<view class="field-label">开单科室</view>
|
||||||
<view class="project-input" @tap="openBillDeptPicker">
|
<view class="project-input" :class="{ 'disabled-input': hasBenefitEntries }" @tap="openBillDeptPicker">
|
||||||
<text :class="billDeptId ? 'value-text' : 'placeholder-text'">{{ billDeptName || "请选择开单科室" }}</text>
|
<text :class="billDeptId ? 'value-text' : 'placeholder-text'">{{ billDeptName || "请选择开单科室" }}</text>
|
||||||
<uni-icons type="right" size="18" color="#999" />
|
<uni-icons type="right" size="18" color="#999" />
|
||||||
</view>
|
</view>
|
||||||
@ -127,15 +127,15 @@
|
|||||||
<view v-if="projectEntry.selected && Number(projectEntry.validDays || 0) > 0" class="field-group">
|
<view v-if="projectEntry.selected && Number(projectEntry.validDays || 0) > 0" class="field-group">
|
||||||
<view class="field-label">有效期</view>
|
<view class="field-label">有效期</view>
|
||||||
<view class="valid-time-wrap">
|
<view class="valid-time-wrap">
|
||||||
<view class="valid-time-picker disabled-picker">
|
<view class="valid-time-picker" @tap="prepareValidTimePicker(projectEntry)">
|
||||||
<picker mode="multiSelector" :range="validTimeRange" :value="validTimeIndex" disabled
|
<picker mode="multiSelector" :range="validTimeRange" :value="validTimeIndex"
|
||||||
@columnchange="onValidTimeColumnChange" @change="changeValidTime($event, projectEntry)">
|
@columnchange="onValidTimeColumnChange" @change="changeValidTime($event, projectEntry)">
|
||||||
<view class="picker-value" :class="projectEntry.validTime ? 'value-text' : 'placeholder-text'">
|
<view class="picker-value" :class="projectEntry.validTime ? 'value-text' : 'placeholder-text'">
|
||||||
{{ projectEntry.validTime || "选择日期/长期" }}
|
{{ projectEntry.validTime || "选择日期/长期" }}
|
||||||
</view>
|
</view>
|
||||||
</picker>
|
</picker>
|
||||||
</view>
|
</view>
|
||||||
<view class="valid-time-locked-tip">套餐有效期已锁定</view>
|
<view class="unlimited-button" @tap="clearValidTime(projectEntry)">长期</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="projectEntry.selected" class="todo-switch-row">
|
<view v-if="projectEntry.selected" class="todo-switch-row">
|
||||||
@ -339,15 +339,47 @@
|
|||||||
<view v-if="batchTodoVisible" class="picker-mask" @tap="closeBatchTodoPicker">
|
<view v-if="batchTodoVisible" class="picker-mask" @tap="closeBatchTodoPicker">
|
||||||
<view class="batch-todo-picker" @tap.stop>
|
<view class="batch-todo-picker" @tap.stop>
|
||||||
<view class="picker-header">
|
<view class="picker-header">
|
||||||
<text class="picker-title">生成待办批量操作</text>
|
<text class="picker-title">权益批量操作</text>
|
||||||
<uni-icons type="closeempty" size="22" color="#666" @tap="closeBatchTodoPicker" />
|
<uni-icons type="closeempty" size="22" color="#666" @tap="closeBatchTodoPicker" />
|
||||||
</view>
|
</view>
|
||||||
<view class="batch-todo-row" @tap.stop>
|
<view class="batch-todo-row" @tap.stop>
|
||||||
<text>批量生成待办</text>
|
<text>批量生成待办</text>
|
||||||
<switch :checked="batchTodoConfig.todoEnabled" color="#0877f1"
|
<switch :checked="batchTodoConfig.todoEnabled" color="#0877f1"
|
||||||
@change="batchTodoConfig.todoEnabled = $event.detail.value" />
|
@change="batchTodoConfig.todoEnabled = $event.detail.value; batchTodoConfig.todoEnabledTouched = true" />
|
||||||
|
</view>
|
||||||
|
<view class="batch-todo-row">
|
||||||
|
<text>批量设置项目有效期</text>
|
||||||
|
<switch :checked="batchTodoConfig.setBenefitValidTime" color="#0877f1"
|
||||||
|
@change="batchTodoConfig.setBenefitValidTime = $event.detail.value" />
|
||||||
|
</view>
|
||||||
|
<view v-if="batchTodoConfig.setBenefitValidTime" class="batch-todo-field">
|
||||||
|
<text>项目有效期:</text>
|
||||||
|
<picker mode="multiSelector" :range="validTimeRange" :value="validTimeIndex"
|
||||||
|
@tap="openBatchDatePicker('benefitValidTime')" @columnchange="onValidTimeColumnChange"
|
||||||
|
@change="changeTodoDate($event)">
|
||||||
|
<text :class="batchTodoConfig.benefitValidTime ? 'value-text' : 'placeholder-text'">
|
||||||
|
{{ batchTodoConfig.benefitValidTime || "选择日期" }}
|
||||||
|
</text>
|
||||||
|
</picker>
|
||||||
|
<text class="batch-unlimited" @tap.stop="batchTodoConfig.benefitValidTime = ''">长期</text>
|
||||||
</view>
|
</view>
|
||||||
<template v-if="batchTodoConfig.todoEnabled">
|
<template v-if="batchTodoConfig.todoEnabled">
|
||||||
|
<view class="batch-todo-row">
|
||||||
|
<text>批量设置回访计划有效期</text>
|
||||||
|
<switch :checked="batchTodoConfig.setTodoExpireDate" color="#0877f1"
|
||||||
|
@change="batchTodoConfig.setTodoExpireDate = $event.detail.value" />
|
||||||
|
</view>
|
||||||
|
<view v-if="batchTodoConfig.setTodoExpireDate" class="batch-todo-field">
|
||||||
|
<text>截止日期:</text>
|
||||||
|
<picker mode="multiSelector" :range="validTimeRange" :value="validTimeIndex"
|
||||||
|
@tap="openBatchDatePicker('todoExpireDate')" @columnchange="onValidTimeColumnChange"
|
||||||
|
@change="changeTodoDate($event)">
|
||||||
|
<text :class="batchTodoConfig.todoExpireDate ? 'value-text' : 'placeholder-text'">
|
||||||
|
{{ batchTodoConfig.todoExpireDate || "请选择截止日期" }}
|
||||||
|
</text>
|
||||||
|
</picker>
|
||||||
|
<uni-icons type="right" size="16" color="#999" />
|
||||||
|
</view>
|
||||||
<view class="batch-todo-row">
|
<view class="batch-todo-row">
|
||||||
<text>待办完成后关联核销权益</text>
|
<text>待办完成后关联核销权益</text>
|
||||||
<switch :checked="batchTodoConfig.deductBenefit" color="#0877f1"
|
<switch :checked="batchTodoConfig.deductBenefit" color="#0877f1"
|
||||||
@ -529,6 +561,11 @@ const billDeptKeyword = ref("");
|
|||||||
const billDeptPickerVisible = ref(false);
|
const billDeptPickerVisible = ref(false);
|
||||||
const billDeptExpanded = ref(new Set());
|
const billDeptExpanded = ref(new Set());
|
||||||
const entries = ref([]);
|
const entries = ref([]);
|
||||||
|
const hasBenefitEntries = computed(() => entries.value.some((entry) => (
|
||||||
|
entry.isPackage
|
||||||
|
|| entry.project?._id
|
||||||
|
|| (Array.isArray(entry.projects) && entry.projects.some((project) => project?.project?._id || project?.selected))
|
||||||
|
)));
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const projectPickerVisible = ref(false);
|
const projectPickerVisible = ref(false);
|
||||||
const projectPickerIndex = ref(-1);
|
const projectPickerIndex = ref(-1);
|
||||||
@ -560,6 +597,11 @@ const batchTodoConfig = ref({
|
|||||||
executorName: "",
|
executorName: "",
|
||||||
executeTeamId: "",
|
executeTeamId: "",
|
||||||
executeTeamName: "",
|
executeTeamName: "",
|
||||||
|
todoEnabledTouched: false,
|
||||||
|
setBenefitValidTime: false,
|
||||||
|
benefitValidTime: "",
|
||||||
|
setTodoExpireDate: false,
|
||||||
|
todoExpireDate: "",
|
||||||
});
|
});
|
||||||
const batchExecutorTarget = ref(null);
|
const batchExecutorTarget = ref(null);
|
||||||
const benefitDetailVisible = ref(false);
|
const benefitDetailVisible = ref(false);
|
||||||
@ -917,6 +959,21 @@ function changeBillDoctor(event) {
|
|||||||
const doctor = getBillDoctors()[Number(event.detail.value)] || null;
|
const doctor = getBillDoctors()[Number(event.detail.value)] || null;
|
||||||
billDoctorUserId.value = doctor?.userid || "";
|
billDoctorUserId.value = doctor?.userid || "";
|
||||||
billDoctorName.value = doctor?.anotherName || doctor?.name || "";
|
billDoctorName.value = doctor?.anotherName || doctor?.name || "";
|
||||||
|
refreshTreatmentDoctorDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshTreatmentDoctorDefaults() {
|
||||||
|
entries.value.forEach((entry) => {
|
||||||
|
if (entry.isPackage) {
|
||||||
|
entry.projects.forEach((project) => {
|
||||||
|
if (project.selected && String(project.dept?._id || "") === String(billDeptId.value || "")) {
|
||||||
|
applyTreatmentDefaults(project);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (entry.project?._id && String(entry.dept?._id || "") === String(billDeptId.value || "")) {
|
||||||
|
applyTreatmentDefaults(entry);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredBillDeptOptions = computed(() => {
|
const filteredBillDeptOptions = computed(() => {
|
||||||
@ -944,6 +1001,10 @@ const filteredBillDeptOptions = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function openBillDeptPicker() {
|
function openBillDeptPicker() {
|
||||||
|
if (hasBenefitEntries.value) {
|
||||||
|
uni.showToast({ title: "已有权益项目,请先清空后修改开单科室", icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
billDeptPickerVisible.value = true;
|
billDeptPickerVisible.value = true;
|
||||||
billDeptKeyword.value = "";
|
billDeptKeyword.value = "";
|
||||||
billDeptExpanded.value = new Set(billDeptOptions.value.filter((item) => item.children?.length).map((item) => String(item._id)));
|
billDeptExpanded.value = new Set(billDeptOptions.value.filter((item) => item.children?.length).map((item) => String(item._id)));
|
||||||
@ -962,11 +1023,16 @@ function toggleBillDept(item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectBillDept(dept) {
|
function selectBillDept(dept) {
|
||||||
|
if (hasBenefitEntries.value) {
|
||||||
|
uni.showToast({ title: "已有权益项目,请先清空后修改开单科室", icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
billDeptId.value = String(dept._id || "");
|
billDeptId.value = String(dept._id || "");
|
||||||
billDeptName.value = dept.deptName || dept.name || "";
|
billDeptName.value = dept.deptName || dept.name || "";
|
||||||
billDeptValue.value = billDeptId.value;
|
billDeptValue.value = billDeptId.value;
|
||||||
billDoctorUserId.value = "";
|
billDoctorUserId.value = "";
|
||||||
billDoctorName.value = "";
|
billDoctorName.value = "";
|
||||||
|
refreshTreatmentDoctorDefaults();
|
||||||
closeBillDeptPicker();
|
closeBillDeptPicker();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1171,9 +1237,7 @@ function selectProject(project) {
|
|||||||
entry.discount = normalizeDiscount({ ...entry, discount: 10 });
|
entry.discount = normalizeDiscount({ ...entry, discount: 10 });
|
||||||
entry.isFree = false;
|
entry.isFree = false;
|
||||||
calculateTotalPrice(entry);
|
calculateTotalPrice(entry);
|
||||||
entry.dept = getProjectDepts(entry).length === 1 ? getProjectDepts(entry)[0] : null;
|
applyTreatmentDefaults(entry);
|
||||||
entry.treatmentDoctorUserId = "";
|
|
||||||
entry.treatmentDoctorName = "";
|
|
||||||
closeProjectPicker();
|
closeProjectPicker();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1184,7 +1248,7 @@ function createPackageProjectEntry(project = {}, packageItem = {}) {
|
|||||||
const validDate = hasValidDays && validDays > 0
|
const validDate = hasValidDays && validDays > 0
|
||||||
? formatDate(new Date(Date.now() + validDays * 24 * 60 * 60 * 1000))
|
? formatDate(new Date(Date.now() + validDays * 24 * 60 * 60 * 1000))
|
||||||
: "";
|
: "";
|
||||||
return {
|
const entry = {
|
||||||
...createEntry(),
|
...createEntry(),
|
||||||
project: normalizedProject,
|
project: normalizedProject,
|
||||||
price: Number(normalizedProject.price ?? project.price ?? 0),
|
price: Number(normalizedProject.price ?? project.price ?? 0),
|
||||||
@ -1196,11 +1260,26 @@ function createPackageProjectEntry(project = {}, packageItem = {}) {
|
|||||||
totalPrice: Number(project.totalPrice || 0),
|
totalPrice: Number(project.totalPrice || 0),
|
||||||
selected: packageItem.packageType === "fixed" || project.required === true,
|
selected: packageItem.packageType === "fixed" || project.required === true,
|
||||||
required: project.required === true,
|
required: project.required === true,
|
||||||
|
packageId: packageItem._id || "",
|
||||||
|
packageName: packageItem.name || "",
|
||||||
|
packageType: packageItem.packageType || "",
|
||||||
hasValidDays,
|
hasValidDays,
|
||||||
validDays,
|
validDays,
|
||||||
validTime: validDate,
|
validTime: validDate,
|
||||||
dept: normalizedProject.depts?.length === 1 ? normalizedProject.depts[0] : null,
|
dept: null,
|
||||||
};
|
};
|
||||||
|
applyTreatmentDefaults(entry);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTreatmentDefaults(entry) {
|
||||||
|
const depts = getProjectDepts(entry);
|
||||||
|
entry.dept = depts.length === 1
|
||||||
|
? depts[0]
|
||||||
|
: depts.find((dept) => String(dept._id) === String(billDeptId.value)) || null;
|
||||||
|
const doctor = getTreatmentDoctors(entry).find((item) => String(item.userid) === String(billDoctorUserId.value));
|
||||||
|
entry.treatmentDoctorUserId = doctor?.userid || "";
|
||||||
|
entry.treatmentDoctorName = doctor?.anotherName || doctor?.name || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectPackage(packageItem) {
|
async function selectPackage(packageItem) {
|
||||||
@ -1300,6 +1379,11 @@ function openBatchTodoPicker() {
|
|||||||
batchTodoVisible.value = true;
|
batchTodoVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openBatchDatePicker(field) {
|
||||||
|
todoDateTarget.value = { entry: batchTodoConfig.value, field };
|
||||||
|
prepareValidTimePicker({ validTime: batchTodoConfig.value[field] });
|
||||||
|
}
|
||||||
|
|
||||||
function closeBatchTodoPicker() {
|
function closeBatchTodoPicker() {
|
||||||
batchTodoVisible.value = false;
|
batchTodoVisible.value = false;
|
||||||
}
|
}
|
||||||
@ -1313,15 +1397,39 @@ function openBatchExecutorPicker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyBatchTodoConfig() {
|
function applyBatchTodoConfig() {
|
||||||
|
let skippedValidityCount = 0;
|
||||||
const applyConfig = (entry) => {
|
const applyConfig = (entry) => {
|
||||||
if (!entry?.project?._id) return;
|
if (!entry?.project?._id) return;
|
||||||
entry.todoEnabled = Boolean(batchTodoConfig.value.todoEnabled);
|
if (batchTodoConfig.value.setBenefitValidTime) {
|
||||||
|
if (entry.packageId && entry.hasValidDays !== true) {
|
||||||
|
skippedValidityCount += 1;
|
||||||
|
} else {
|
||||||
|
entry.validTime = batchTodoConfig.value.benefitValidTime || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (batchTodoConfig.value.todoEnabledTouched) {
|
||||||
|
entry.todoEnabled = Boolean(batchTodoConfig.value.todoEnabled);
|
||||||
|
}
|
||||||
|
const shouldApplyTodoSettings = batchTodoConfig.value.todoEnabledTouched
|
||||||
|
|| batchTodoConfig.value.setTodoExpireDate
|
||||||
|
|| Boolean(batchTodoConfig.value.executorUserId);
|
||||||
|
if (!shouldApplyTodoSettings) return;
|
||||||
if (!entry.todoEnabled) {
|
if (!entry.todoEnabled) {
|
||||||
entry.todoConfig = null;
|
if (batchTodoConfig.value.setTodoExpireDate && batchTodoConfig.value.todoExpireDate) {
|
||||||
|
entry.todoConfig = entry.todoConfig || getDefaultTodoConfig(entry);
|
||||||
|
entry.todoConfig.expireDate = batchTodoConfig.value.todoExpireDate;
|
||||||
|
entry.todoConfig.expireDateManual = true;
|
||||||
|
} else if (batchTodoConfig.value.todoEnabledTouched) {
|
||||||
|
entry.todoConfig = null;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
entry.todoConfig = entry.todoConfig || getDefaultTodoConfig(entry);
|
entry.todoConfig = entry.todoConfig || getDefaultTodoConfig(entry);
|
||||||
entry.todoConfig.deductBenefit = Boolean(batchTodoConfig.value.deductBenefit);
|
entry.todoConfig.deductBenefit = Boolean(batchTodoConfig.value.deductBenefit);
|
||||||
|
if (batchTodoConfig.value.setTodoExpireDate && batchTodoConfig.value.todoExpireDate) {
|
||||||
|
entry.todoConfig.expireDate = batchTodoConfig.value.todoExpireDate;
|
||||||
|
entry.todoConfig.expireDateManual = true;
|
||||||
|
}
|
||||||
if (batchTodoConfig.value.executorUserId) {
|
if (batchTodoConfig.value.executorUserId) {
|
||||||
Object.assign(entry.todoConfig, {
|
Object.assign(entry.todoConfig, {
|
||||||
executorUserId: batchTodoConfig.value.executorUserId,
|
executorUserId: batchTodoConfig.value.executorUserId,
|
||||||
@ -1339,6 +1447,9 @@ function applyBatchTodoConfig() {
|
|||||||
applyConfig(entry);
|
applyConfig(entry);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (skippedValidityCount) {
|
||||||
|
uni.showToast({ title: `${skippedValidityCount}个套餐项目不支持有效期`, icon: "none" });
|
||||||
|
}
|
||||||
closeBatchTodoPicker()
|
closeBatchTodoPicker()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1431,6 +1542,12 @@ function changeTodoDate(event) {
|
|||||||
const month = Number(validTimeRange.value[1][monthIndex].replace("月", ""));
|
const month = Number(validTimeRange.value[1][monthIndex].replace("月", ""));
|
||||||
const day = Number(validTimeRange.value[2][dayIndex].replace("日", ""));
|
const day = Number(validTimeRange.value[2][dayIndex].replace("日", ""));
|
||||||
const selectedDate = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
const selectedDate = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
||||||
|
if (target.entry === batchTodoConfig.value) {
|
||||||
|
batchTodoConfig.value[target.field] = selectedDate;
|
||||||
|
validTimeIndex.value = [yearIndex, monthIndex, dayIndex];
|
||||||
|
todoDateTarget.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
target.entry.todoConfig[target.field] = selectedDate;
|
target.entry.todoConfig[target.field] = selectedDate;
|
||||||
validTimeIndex.value = [yearIndex, monthIndex, dayIndex];
|
validTimeIndex.value = [yearIndex, monthIndex, dayIndex];
|
||||||
if (target.field === "planDate" && !target.entry.validTime) {
|
if (target.field === "planDate" && !target.entry.validTime) {
|
||||||
@ -1455,8 +1572,9 @@ function syncPackageTotals(entry) {
|
|||||||
|
|
||||||
function changeDept(event, entry) {
|
function changeDept(event, entry) {
|
||||||
entry.dept = getProjectDepts(entry)[Number(event.detail.value)] || null;
|
entry.dept = getProjectDepts(entry)[Number(event.detail.value)] || null;
|
||||||
entry.treatmentDoctorUserId = "";
|
const doctor = getTreatmentDoctors(entry).find((item) => String(item.userid) === String(billDoctorUserId.value));
|
||||||
entry.treatmentDoctorName = "";
|
entry.treatmentDoctorUserId = doctor?.userid || "";
|
||||||
|
entry.treatmentDoctorName = doctor?.anotherName || doctor?.name || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeTreatmentDoctor(event, entry) {
|
function changeTreatmentDoctor(event, entry) {
|
||||||
@ -2155,6 +2273,8 @@ async function submitEntries() {
|
|||||||
left: 0;
|
left: 0;
|
||||||
z-index: 10001;
|
z-index: 10001;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
max-height: calc(100vh - var(--keyboard-height));
|
||||||
|
overflow-y: auto;
|
||||||
padding: 28rpx 28rpx 32rpx;
|
padding: 28rpx 28rpx 32rpx;
|
||||||
border-radius: 24rpx 24rpx 0 0;
|
border-radius: 24rpx 24rpx 0 0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@ -2199,6 +2319,19 @@ async function submitEntries() {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.batch-todo-field picker {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-todo-field picker text {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.batch-todo-actions {
|
.batch-todo-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20rpx;
|
gap: 20rpx;
|
||||||
|
|||||||
@ -1,16 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="ai-assistant-buttons">
|
<view class="ai-assistant-buttons">
|
||||||
<view
|
<view v-for="button in buttons" :key="button.id" class="ai-button" :class="{ loading: button.loading }"
|
||||||
v-for="button in buttons"
|
@click="handleButtonClick(button)">
|
||||||
:key="button.id"
|
|
||||||
class="ai-button"
|
|
||||||
:class="{ loading: button.loading }"
|
|
||||||
@click="handleButtonClick(button)"
|
|
||||||
>
|
|
||||||
<image class="button-icon" :src="button.icon" mode="aspectFit" />
|
<image class="button-icon" :src="button.icon" mode="aspectFit" />
|
||||||
<text class="button-text">{{
|
<text class="button-text">{{
|
||||||
button.loading && button.loadingText ? button.loadingText : button.text
|
button.loading && button.loadingText ? button.loadingText : button.text
|
||||||
}}</text>
|
}}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@ -62,25 +57,32 @@ const emit = defineEmits([
|
|||||||
const isGenerating = ref(false);
|
const isGenerating = ref(false);
|
||||||
|
|
||||||
const buttons = ref([
|
const buttons = ref([
|
||||||
{
|
// {
|
||||||
id: "followUp",
|
// id: "followUp",
|
||||||
text: "追问病情",
|
// text: "追问病情",
|
||||||
loadingText: "AI分析中,正在为您生成追问建议…",
|
// loadingText: "AI分析中,正在为您生成追问建议…",
|
||||||
icon: "/static/icon/zhuiwen.png",
|
// icon: "/static/icon/zhuiwen.png",
|
||||||
loading: false,
|
// loading: false,
|
||||||
},
|
// },
|
||||||
// {
|
// {
|
||||||
// id: "aiAssistant",
|
// id: "aiAssistant",
|
||||||
// text: "开启AI助手",
|
// text: "推荐回复",
|
||||||
// icon: "/static/icon/kaiqiAI.png",
|
// icon: "/static/icon/kaiqiAI.png",
|
||||||
// loading: false,
|
// loading: false,
|
||||||
// },
|
// },
|
||||||
|
// {
|
||||||
|
// id: "supplementRecord",
|
||||||
|
// text: "补充档案",
|
||||||
|
// icon: "/static/icon/buchong.png",
|
||||||
|
// loading: false,
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
id: "supplementRecord",
|
id: "aiAssistant",
|
||||||
text: "补充档案",
|
text: "推荐回复",
|
||||||
icon: "/static/icon/buchong.png",
|
loadingText: "AI分析中,正在为您生成回复建议…",
|
||||||
|
icon: "/static/icon/kaiqiAI.png",
|
||||||
loading: false,
|
loading: false,
|
||||||
},
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 处理按钮点击
|
// 处理按钮点击
|
||||||
@ -206,11 +208,72 @@ const streamTextToInput = (text) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 处理AI助手
|
// 处理AI助手
|
||||||
const handleAIAssistant = (button) => {
|
const handleAIAssistant = async (button) => {
|
||||||
uni.showToast({
|
try {
|
||||||
title: "AI助手功能开发中",
|
button.loading = true;
|
||||||
icon: "none",
|
|
||||||
});
|
let finalPatientAccountId = props.patientAccountId;
|
||||||
|
if (!finalPatientAccountId) {
|
||||||
|
const chatRecordsResult = await api("getChatRecordsByGroupId", {
|
||||||
|
groupID: props.groupId,
|
||||||
|
count: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
chatRecordsResult.success &&
|
||||||
|
chatRecordsResult.data &&
|
||||||
|
chatRecordsResult.data.records
|
||||||
|
) {
|
||||||
|
const currentUserId = uni.getStorageSync("openid") || "";
|
||||||
|
const patientMessage = chatRecordsResult.data.records.find(
|
||||||
|
(record) =>
|
||||||
|
record.From_Account && record.From_Account !== currentUserId
|
||||||
|
);
|
||||||
|
if (patientMessage) {
|
||||||
|
finalPatientAccountId = patientMessage.From_Account;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!finalPatientAccountId) {
|
||||||
|
uni.showToast({
|
||||||
|
title: "无法获取患者信息",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await request(
|
||||||
|
{
|
||||||
|
url: "/getYoucanData/im",
|
||||||
|
data: {
|
||||||
|
type: "recommendReply",
|
||||||
|
groupId: props.groupId,
|
||||||
|
patientAccountId: finalPatientAccountId,
|
||||||
|
corpId: props.corpId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success && result.data && result.data.suggestion) {
|
||||||
|
streamTextToInput(result.data.suggestion);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: result.message || "获取推荐回复失败",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取推荐回复失败:", error);
|
||||||
|
uni.showToast({
|
||||||
|
title: "操作失败,请重试",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
button.loading = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理补充病历
|
// 处理补充病历
|
||||||
@ -307,6 +370,7 @@ defineExpose({
|
|||||||
0% {
|
0% {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
<!-- :hold-keyboard="true" -->
|
<!-- :hold-keyboard="true" -->
|
||||||
<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" :auto-height="true"
|
@confirm="sendTextMessage" @focus="handleInputFocus" @input="handleInput" :auto-height="true"
|
||||||
:show-confirm-bar="false" :cursor-spacing="40" ref="textareaRef" />
|
:show-confirm-bar="false" :maxlength="-1" :cursor-spacing="40" ref="textareaRef" />
|
||||||
<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"
|
@touchmove="onRecordTouchMove" @touchend="stopRecord" @touchcancel="cancelRecord"
|
||||||
:placeholder="isRecording ? '松开发送' : '按住说话'" disabled />
|
:placeholder="isRecording ? '松开发送' : '按住说话'" disabled />
|
||||||
@ -295,18 +295,21 @@ const sendVoiceMessage = async (voiceFile, duration) => {
|
|||||||
|
|
||||||
// 发送消息的通用方法(文本、图片、语音)
|
// 发送消息的通用方法(文本、图片、语音)
|
||||||
const sendMessage = async (messageType, data) => {
|
const sendMessage = async (messageType, data) => {
|
||||||
await sendMessageUtil(
|
const result = await sendMessageUtil(
|
||||||
messageType,
|
messageType,
|
||||||
data,
|
data,
|
||||||
props.timChatManager,
|
props.timChatManager,
|
||||||
() => validateBeforeSend(false, false, props.timChatManager),
|
() => validateBeforeSend(false, false, props.timChatManager),
|
||||||
() => {
|
() => {
|
||||||
showMorePanel.value = false;
|
showMorePanel.value = false;
|
||||||
// 发送成功后滚动到底部
|
|
||||||
emit("messageSent");
|
|
||||||
},
|
},
|
||||||
cloudCustomData.value
|
cloudCustomData.value
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (result?.success) {
|
||||||
|
// 让页面直接将本地消息加入列表,避免依赖全局 IM 回调的注册时机。
|
||||||
|
emit("messageSent", result.message);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 发送自定义消息的通用方法
|
// 发送自定义消息的通用方法
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 文本消息 -->
|
<!-- 文本消息 -->
|
||||||
<text v-if="message.type === 'TIMTextElem'" class="message-text">
|
<text v-if="message.type === 'TIMTextElem'" class="message-text" @longpress="copyText(message.payload.text)">
|
||||||
{{ message.payload.text }}
|
{{ message.payload.text }}
|
||||||
</text>
|
</text>
|
||||||
|
|
||||||
@ -290,6 +290,22 @@ function isWechatChannels(data) {
|
|||||||
const wechatChannels = data.wechatChannels || {};
|
const wechatChannels = data.wechatChannels || {};
|
||||||
return typeof wechatChannels.finderUserName === 'string' && wechatChannels.finderUserName.trim() && typeof wechatChannels.feedId === 'string' && wechatChannels.feedId.trim();
|
return typeof wechatChannels.finderUserName === 'string' && wechatChannels.finderUserName.trim() && typeof wechatChannels.feedId === 'string' && wechatChannels.feedId.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyText(text) {
|
||||||
|
if (typeof text !== 'string' || text.trim() === '') return;
|
||||||
|
console.log('11122')
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['复制'],
|
||||||
|
success: (res) => {
|
||||||
|
console.log(res.tapIndex)
|
||||||
|
if (res.tapIndex === 0) {
|
||||||
|
uni.setClipboardData({ data: text.trim(), success: () => toast('复制成功') })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: res => console.log(res)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@ -324,7 +340,7 @@ function isWechatChannels(data) {
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.benefit-item + .benefit-item {
|
.benefit-item+.benefit-item {
|
||||||
margin-top: 18rpx;
|
margin-top: 18rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -101,7 +101,7 @@
|
|||||||
" :userId="openid" :teamId="teamId" :patientId="patientId" :corpId="corpId" :patientInfo="patientInfo"
|
" :userId="openid" :teamId="teamId" :patientId="patientId" :corpId="corpId" :patientInfo="patientInfo"
|
||||||
:orderStatus="orderStatus" :isGenerating="isGenerating"
|
:orderStatus="orderStatus" :isGenerating="isGenerating"
|
||||||
:canBenefitEntry="isBenefitEntryEnabled"
|
:canBenefitEntry="isBenefitEntryEnabled"
|
||||||
@scrollToBottom="() => scrollToBottom(true)" @messageSent="() => scrollToBottom(true)"
|
@scrollToBottom="() => scrollToBottom(true)" @messageSent="handleMessageSent"
|
||||||
@endConsult="handleEndConsult" @openConsult="handleOpenConsult">
|
@endConsult="handleEndConsult" @openConsult="handleOpenConsult">
|
||||||
<!-- AI助手按钮组插槽 -->
|
<!-- AI助手按钮组插槽 -->
|
||||||
<template #ai-assistant>
|
<template #ai-assistant>
|
||||||
@ -124,7 +124,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onUnmounted, nextTick, watch, computed } from "vue";
|
import { ref, onUnmounted, nextTick, watch, computed } from "vue";
|
||||||
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
|
import { onLoad, onShow, onHide, onUnload } from "@dcloudio/uni-app";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import useAccountStore from "@/store/account.js";
|
import useAccountStore from "@/store/account.js";
|
||||||
import { globalTimChatManager } from "@/utils/tim-chat.js";
|
import { globalTimChatManager } from "@/utils/tim-chat.js";
|
||||||
@ -766,6 +766,29 @@ const scrollToBottom = (immediate = false) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 发送成功后直接写入当前页面列表,避免全局 IM 回调被其他页面覆盖时消息不显示。
|
||||||
|
const handleMessageSent = (message) => {
|
||||||
|
if (
|
||||||
|
!message ||
|
||||||
|
!message.ID ||
|
||||||
|
message.conversationID !== chatInfo.value.conversationID
|
||||||
|
) {
|
||||||
|
scrollToBottom(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingMessage = messageList.value.find(
|
||||||
|
(item) => item.ID === message.ID
|
||||||
|
);
|
||||||
|
if (!existingMessage) {
|
||||||
|
messageList.value.push(message);
|
||||||
|
} else {
|
||||||
|
existingMessage.status = message.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextTick(() => scrollToBottom(true));
|
||||||
|
};
|
||||||
|
|
||||||
// 关闭功能栏
|
// 关闭功能栏
|
||||||
const closeMorePanel = () => {
|
const closeMorePanel = () => {
|
||||||
uni.$emit("closeMorePanel");
|
uni.$emit("closeMorePanel");
|
||||||
@ -918,9 +941,14 @@ const handleSendFollowUpMessage = async (data) => {
|
|||||||
// 页面隐藏
|
// 页面隐藏
|
||||||
onHide(() => {
|
onHide(() => {
|
||||||
stopIMMonitoring();
|
stopIMMonitoring();
|
||||||
// 清空当前会话ID,避免离开页面后收到的消息被错误标记为已读
|
});
|
||||||
timChatManager.currentConversationID = null;
|
|
||||||
console.log("✓ 页面隐藏,已清空当前会话ID");
|
// 仅在真正离开页面时清空会话,息屏/切后台触发 onHide 时仍需接收当前会话的新消息。
|
||||||
|
onUnload(() => {
|
||||||
|
if (timChatManager.currentConversationID === chatInfo.value.conversationID) {
|
||||||
|
timChatManager.currentConversationID = null;
|
||||||
|
console.log("✓ 页面卸载,已清空当前会话ID");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const sendCommonPhrase = async (phraseOrContent) => {
|
const sendCommonPhrase = async (phraseOrContent) => {
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
<full-page>
|
<full-page>
|
||||||
<view class="p-15">
|
<view class="p-15">
|
||||||
<view class="bg-white px-10 mb-10 rounded">
|
<view class="bg-white px-10 mb-10 rounded">
|
||||||
<form-input :form="formData" :required="rule.anotherName.required" wordLimit="10" title="anotherName"
|
<form-input :form="formData" :required="rule.anotherName.required" :disableChange="rule.anotherName.disabled"
|
||||||
:name="rule.anotherName.name" @change="onChange($event)" />
|
wordLimit="10" title="anotherName" :name="rule.anotherName.name" @change="onChange($event)" />
|
||||||
<common-cell title="avatar" name="头像">
|
<common-cell title="avatar" name="头像">
|
||||||
<view class="flex-grow flex items-center justify-end">
|
<view class="flex-grow flex items-center justify-end">
|
||||||
<image v-if="formData.avatar" class="avatar mr-5 rounded-full" :src="formData.avatar" />
|
<image v-if="formData.avatar" class="avatar mr-5 rounded-full" :src="formData.avatar" />
|
||||||
@ -116,7 +116,7 @@ const rule = computed(() => {
|
|||||||
title: { name: "职称", disabled: false },
|
title: { name: "职称", disabled: false },
|
||||||
dept: { name: "科室", disabled: false },
|
dept: { name: "科室", disabled: false },
|
||||||
}
|
}
|
||||||
if (doctorInfo.value) {
|
if (doctorInfo.value?.verifyStatus === "verified") {
|
||||||
data.job.name = "岗位 (不可修改)";
|
data.job.name = "岗位 (不可修改)";
|
||||||
data.job.disabled = true;
|
data.job.disabled = true;
|
||||||
}
|
}
|
||||||
@ -232,7 +232,10 @@ async function save() {
|
|||||||
) {
|
) {
|
||||||
return toast("请输入姓名");
|
return toast("请输入姓名");
|
||||||
}
|
}
|
||||||
if (type.value === "cert" && !jobStr.value) {
|
if (
|
||||||
|
type.value === "cert" &&
|
||||||
|
(!Array.isArray(formData.value.job) || formData.value.job.length === 0)
|
||||||
|
) {
|
||||||
return toast("请选择岗位信息");
|
return toast("请选择岗位信息");
|
||||||
}
|
}
|
||||||
const apiName = doctorInfo.value
|
const apiName = doctorInfo.value
|
||||||
@ -255,7 +258,12 @@ async function save() {
|
|||||||
await getDoctorInfo({ withHlwDeptInfo: true });
|
await getDoctorInfo({ withHlwDeptInfo: true });
|
||||||
form.value = {};
|
form.value = {};
|
||||||
if (type.value === "cert") {
|
if (type.value === "cert") {
|
||||||
toCert();
|
if (["医生", "医生助理"].includes(jobStr.value)) {
|
||||||
|
toCert();
|
||||||
|
} else {
|
||||||
|
await toast("保存成功");
|
||||||
|
back();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await toast("保存成功");
|
await toast("保存成功");
|
||||||
back();
|
back();
|
||||||
|
|||||||
@ -60,7 +60,7 @@
|
|||||||
|
|
||||||
</view>
|
</view>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<button-footer cancelText="上一步" confirmText="提交审核" @confirm="save()" @cancel="back()" />
|
<button-footer cancelText="上一步" :confirmText="submitText" @confirm="save()" @cancel="back()" />
|
||||||
</template>
|
</template>
|
||||||
</full-page>
|
</full-page>
|
||||||
</template>
|
</template>
|
||||||
@ -81,6 +81,8 @@ useGuard();
|
|||||||
const { account, doctorInfo } = storeToRefs(useAccountStore());
|
const { account, doctorInfo } = storeToRefs(useAccountStore());
|
||||||
const form = ref({})
|
const form = ref({})
|
||||||
const formData = computed(() => ({ ...(doctorInfo.value || {}), ...form.value }))
|
const formData = computed(() => ({ ...(doctorInfo.value || {}), ...form.value }))
|
||||||
|
const isResubmitting = computed(() => doctorInfo.value?.verifyStatus === 'verifying')
|
||||||
|
const submitText = computed(() => isResubmitting.value ? '重新提交审核' : '提交审核')
|
||||||
|
|
||||||
function back() {
|
function back() {
|
||||||
uni.navigateBack()
|
uni.navigateBack()
|
||||||
@ -119,7 +121,7 @@ async function save() {
|
|||||||
}
|
}
|
||||||
const res = await api('submitCertProfile', data);
|
const res = await api('submitCertProfile', data);
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
await toast('提交成功');
|
await toast(isResubmitting.value ? '重新提交成功' : '提交成功');
|
||||||
uni.navigateBack({ delta: 2 })
|
uni.navigateBack({ delta: 2 })
|
||||||
} else {
|
} else {
|
||||||
toast(res.message || '提交失败')
|
toast(res.message || '提交失败')
|
||||||
|
|||||||
@ -43,7 +43,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<button-footer cancelText="上一步" confirmText="提交审核" @confirm="save()" @cancel="back()" />
|
<button-footer cancelText="上一步" :confirmText="submitText" @confirm="save()" @cancel="back()" />
|
||||||
</template>
|
</template>
|
||||||
</full-page>
|
</full-page>
|
||||||
</template>
|
</template>
|
||||||
@ -63,6 +63,8 @@ useGuard();
|
|||||||
const { account, doctorInfo } = storeToRefs(useAccountStore());
|
const { account, doctorInfo } = storeToRefs(useAccountStore());
|
||||||
const form = ref({});
|
const form = ref({});
|
||||||
const formData = computed(() => ({ ...(doctorInfo.value || {}), ...form.value }))
|
const formData = computed(() => ({ ...(doctorInfo.value || {}), ...form.value }))
|
||||||
|
const isResubmitting = computed(() => doctorInfo.value?.verifyStatus === 'verifying')
|
||||||
|
const submitText = computed(() => isResubmitting.value ? '重新提交审核' : '提交审核')
|
||||||
|
|
||||||
function back() {
|
function back() {
|
||||||
uni.navigateBack()
|
uni.navigateBack()
|
||||||
@ -126,7 +128,7 @@ async function save() {
|
|||||||
}
|
}
|
||||||
const res = await api('submitCertProfile', data);
|
const res = await api('submitCertProfile', data);
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
await toast('提交成功');
|
await toast(isResubmitting.value ? '重新提交成功' : '提交成功');
|
||||||
uni.navigateBack({ delta: 2 })
|
uni.navigateBack({ delta: 2 })
|
||||||
} else {
|
} else {
|
||||||
toast(res.message || '提交失败')
|
toast(res.message || '提交失败')
|
||||||
|
|||||||
@ -206,7 +206,9 @@ function editProfile() {
|
|||||||
|
|
||||||
function handleCert() {
|
function handleCert() {
|
||||||
if (doctorInfo.value.verifyStatus === 'verifying') {
|
if (doctorInfo.value.verifyStatus === 'verifying') {
|
||||||
toast('信息认证中,请耐心等待!')
|
uni.navigateTo({
|
||||||
|
url: '/pages/work/profile?type=cert',
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
visible.value = true
|
visible.value = true
|
||||||
}
|
}
|
||||||
@ -373,4 +375,4 @@ useShow(async () => {
|
|||||||
width: 24rpx;
|
width: 24rpx;
|
||||||
height: 24rpx;
|
height: 24rpx;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -513,6 +513,8 @@ export const sendMessage = async (messageType, data, timChatManager, validateBef
|
|||||||
console.error('发送消息失败:', result?.error);
|
console.error('发送消息失败:', result?.error);
|
||||||
showMessage('发送失败,请重试');
|
showMessage('发送失败,请重试');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==================== 状态检查相关工具方法 ====================
|
// ==================== 状态检查相关工具方法 ====================
|
||||||
@ -795,4 +797,4 @@ export const handleViewDetail = (message, patientInfo) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user