Compare commits

..

8 Commits

Author SHA1 Message Date
huxuejian
9cbe454338 fix:问题修复 2026-10-15 18:25:26 +08:00
huxuejian
25925c1969 Update profile.vue 2026-10-15 17:27:06 +08:00
huxuejian
265dc9381d Merge branch 'dev-2.1' of http://175.27.226.205:3000/huxuejian/ykt-wxapp into dev-2.1 2026-10-15 16:32:45 +08:00
huxuejian
4d562fc3f1 fix: 问题修复 2026-10-15 16:32:43 +08:00
01fc0f64c1 Merge branch 'dev-bug-1012' into dev-2.1 2026-10-15 15:44:21 +08:00
huxuejian
ed85ad990e feat:需求开发 2026-10-14 13:31:13 +08:00
87da5c69e6 Merge remote-tracking branch 'origin/fixbug-260920' into dev-bug-1012 2026-10-12 17:58:10 +08:00
c88a1d08ab bug修复10.12 2026-10-12 17:51:51 +08:00
13 changed files with 454 additions and 196 deletions

View File

@ -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",

View File

@ -1,33 +1,15 @@
<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
@ -39,10 +21,7 @@
<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>
@ -59,17 +38,11 @@
</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" ? "暂无处理中的会话" : "暂无已结束的会话"
@ -77,10 +50,7 @@
</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>
@ -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;
} }

View File

@ -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
} }
@ -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) {

View File

@ -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;
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); 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) {
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; 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;

View File

@ -1,12 +1,7 @@
<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
@ -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) => {
try {
button.loading = true;
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({ uni.showToast({
title: "AI助手功能开发中", title: "无法获取患者信息",
icon: "none", 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);
} }

View File

@ -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);
}
}; };
// 发送自定义消息的通用方法 // 发送自定义消息的通用方法

View File

@ -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;
} }

View File

@ -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,避免离开页面后收到的消息被错误标记为已读 });
// 仅在真正离开页面时清空会话,息屏/切后台触发 onHide 时仍需接收当前会话的新消息。
onUnload(() => {
if (timChatManager.currentConversationID === chatInfo.value.conversationID) {
timChatManager.currentConversationID = null; timChatManager.currentConversationID = null;
console.log("✓ 页面隐藏,已清空当前会话ID"); console.log("✓ 页面卸载,已清空当前会话ID");
}
}); });
const sendCommonPhrase = async (phraseOrContent) => { const sendCommonPhrase = async (phraseOrContent) => {

View File

@ -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,11 +258,16 @@ async function save() {
await getDoctorInfo({ withHlwDeptInfo: true }); await getDoctorInfo({ withHlwDeptInfo: true });
form.value = {}; form.value = {};
if (type.value === "cert") { if (type.value === "cert") {
if (["医生", "医生助理"].includes(jobStr.value)) {
toCert(); toCert();
} else { } else {
await toast("保存成功"); await toast("保存成功");
back(); back();
} }
} else {
await toast("保存成功");
back();
}
} else { } else {
await toast(res?.message || "保存失败"); await toast(res?.message || "保存失败");
} }

View File

@ -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 || '提交失败')

View File

@ -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 || '提交失败')

View File

@ -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
} }

View File

@ -513,6 +513,8 @@ export const sendMessage = async (messageType, data, timChatManager, validateBef
console.error('发送消息失败:', result?.error); console.error('发送消息失败:', result?.error);
showMessage('发送失败,请重试'); showMessage('发送失败,请重试');
} }
return result;
}; };
// ==================== 状态检查相关工具方法 ==================== // ==================== 状态检查相关工具方法 ====================