Merge branch 'dev-咨询助手优化' into release-20260901

This commit is contained in:
huxuejian 2026-09-01 11:29:07 +08:00
commit 091b6df55d
2 changed files with 80 additions and 18 deletions

View File

@ -1,5 +1,14 @@
<template> <template>
<view class="chat-page"> <page-meta page-style="overflow: hidden;"></page-meta>
<view class="chat-page" :style="{ bottom: `${keyboardHeight}px` }">
<view v-if="session" class="patient-info-bar" @click="openPatientInfo">
<view class="patient-summary">
<text class="patient-name">{{ patientDisplayName }}</text>
<text v-if="patientDetail" class="patient-detail">{{ patientDetail }}</text>
</view>
<text class="patient-action">查看/修改</text>
<text class="patient-arrow"></text>
</view>
<scroll-view scroll-y class="chat-content" :scroll-into-view="bottomId"> <scroll-view scroll-y class="chat-content" :scroll-into-view="bottomId">
<view class="message-list"> <view class="message-list">
<view v-for="(item, index) in messages" :key="item._id" :id="`message-${item._id}`" class="message-item" :class="`message-${item.sender}`"> <view v-for="(item, index) in messages" :key="item._id" :id="`message-${item._id}`" class="message-item" :class="`message-${item.sender}`">
@ -7,7 +16,7 @@
<evaluation-card v-if="item.messageType === 'consult_ended' && (item.rateId || session?.rateId)" class="rate-card" :extension="{ rateId: item.rateId || session.rateId }" :corp-id="corpId" :doctor-info="rateAssistantInfo" /> <evaluation-card v-if="item.messageType === 'consult_ended' && (item.rateId || session?.rateId)" class="rate-card" :extension="{ rateId: item.rateId || session.rateId }" :corp-id="corpId" :doctor-info="rateAssistantInfo" />
<view v-else-if="item.sender === 'system' && item.messageType !== 'risk_notice'" class="system-message" :class="{ warn: item.messageType === 'sensitive_notice' }">{{ displayContent(item) }}</view> <view v-else-if="item.sender === 'system' && item.messageType !== 'risk_notice'" class="system-message" :class="{ warn: item.messageType === 'sensitive_notice' }">{{ displayContent(item) }}</view>
<view v-else-if="item.messageType !== 'risk_notice'" class="message-content"> <view v-else-if="item.messageType !== 'risk_notice'" class="message-content">
<image v-if="item.sender === 'assistant'" class="avatar assistant-avatar" src="/static/home/ai-consult-assistant.png" mode="aspectFill" /> <image v-if="item.sender === 'assistant'" class="avatar assistant-avatar" :src="assistantAvatar" mode="aspectFill" @error="handleAssistantAvatarError" />
<view class="message-bubble-container"> <view class="message-bubble-container">
<view v-if="item.sender === 'assistant'" class="username-label">{{ assistantDisplayName }}</view> <view v-if="item.sender === 'assistant'" class="username-label">{{ assistantDisplayName }}</view>
<view class="message-bubble" :class="{ pending: item._sendStatus === 'pending' }"> <view class="message-bubble" :class="{ pending: item._sendStatus === 'pending' }">
@ -19,13 +28,13 @@
</view> </view>
</view> </view>
<view v-if="waitingAi" class="message-item message-assistant"> <view v-if="waitingAi" class="message-item message-assistant">
<view class="message-content"><image class="avatar assistant-avatar" src="/static/home/ai-consult-assistant.png" mode="aspectFill" /><view class="message-bubble-container"><view class="username-label">{{ assistantDisplayName }}</view><view class="message-bubble thinking"><view class="thinking-dot"></view><view class="thinking-dot"></view><view class="thinking-dot"></view></view></view></view> <view class="message-content"><image class="avatar assistant-avatar" :src="assistantAvatar" mode="aspectFill" @error="handleAssistantAvatarError" /><view class="message-bubble-container"><view class="username-label">{{ assistantDisplayName }}</view><view class="message-bubble thinking"><view class="thinking-dot"></view><view class="thinking-dot"></view><view class="thinking-dot"></view></view></view></view>
</view> </view>
<view id="bottom" /> <view id="bottom" />
</view> </view>
</scroll-view> </scroll-view>
<view v-if="active" class="input-section"> <view v-if="active" class="input-section">
<textarea v-model="content" class="text-input" maxlength="1000" auto-height confirm-type="send" :show-confirm-bar="false" placeholder="请输入消息" @confirm="send" /> <textarea v-model="content" class="text-input" maxlength="1000" auto-height fixed confirm-type="send" :show-confirm-bar="false" :adjust-position="false" placeholder="请输入消息" @confirm="send" />
<button class="send-btn" :disabled="!content.trim()" @click="send">发送</button> <button class="send-btn" :disabled="!content.trim()" @click="send">发送</button>
</view> </view>
<view v-else class="ended">本次咨询已结束</view> <view v-else class="ended">本次咨询已结束</view>
@ -34,7 +43,7 @@
<script setup> <script setup>
import { ref, nextTick, computed } from 'vue'; import { ref, nextTick, computed } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app'; import { onLoad, onShow, onUnload } from '@dcloudio/uni-app';
import api from '@/utils/api'; import api from '@/utils/api';
import useAccountStore from '@/store/account'; import useAccountStore from '@/store/account';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
@ -45,14 +54,28 @@ const { openid } = storeToRefs(useAccountStore());
const corpId = ref(''); const corpId = ref('');
const sessionId = ref(''); const sessionId = ref('');
const session = ref(null); const session = ref(null);
const patientInfo = ref(null);
const messages = ref([]); const messages = ref([]);
const content = ref(''); const content = ref('');
const pendingRequestCount = ref(0); const pendingRequestCount = ref(0);
const waitingAi = ref(false); const waitingAi = ref(false);
const bottomId = ref(''); const bottomId = ref('');
const active = ref(false); const active = ref(false);
const keyboardHeight = ref(0);
const avatarLoadFailed = ref(false);
const defaultAssistantAvatar = '/static/home/ai-consult-assistant.png';
const assistantDisplayName = computed(() => removeAiLabel(session.value?.assistantName, '咨询助理')); const assistantDisplayName = computed(() => removeAiLabel(session.value?.assistantName, '咨询助理'));
const rateAssistantInfo = computed(() => ({ name: assistantDisplayName.value, title: '咨询助理', department: session.value?.teamName || '', avatar: '' })); const assistantAvatar = computed(() => avatarLoadFailed.value ? defaultAssistantAvatar : (session.value?.assistantAvatar || defaultAssistantAvatar));
const rateAssistantInfo = computed(() => ({ name: assistantDisplayName.value, title: '咨询助理', department: session.value?.teamName || '', avatar: assistantAvatar.value }));
const patientDisplayName = computed(() => patientInfo.value?.name || session.value?.customerName || '未建档患者');
const patientDetail = computed(() => {
const patient = patientInfo.value || {};
const details = [];
if (patient.sex) details.push(patient.sex);
if (patient.age !== '' && patient.age !== null && patient.age !== undefined) details.push(`${patient.age}`);
if (patient.relationship) details.push(patient.relationship);
return details.join(' · ');
});
function displayContent(item) { function displayContent(item) {
const value = item?.content || ''; const value = item?.content || '';
@ -85,10 +108,29 @@ async function scrollToBottom() {
await nextTick(); await nextTick();
bottomId.value = 'bottom'; bottomId.value = 'bottom';
} }
function handleKeyboardHeightChange(res) {
keyboardHeight.value = Math.max(0, Number(res?.height) || 0);
if (keyboardHeight.value > 0) scrollToBottom();
}
function handleAssistantAvatarError() {
avatarLoadFailed.value = true;
}
function openPatientInfo() {
const customerId = session.value?.archiveCustomerId;
if (!customerId) return uni.showToast({ title: '暂无可查看的患者档案', icon: 'none' });
const params = [
`teamId=${encodeURIComponent(session.value?.teamId || '')}`,
`corpId=${encodeURIComponent(corpId.value)}`,
`id=${encodeURIComponent(customerId)}`,
];
uni.navigateTo({ url: `/pages/archive/edit-archive?${params.join('&')}` });
}
async function detail() { async function detail() {
const res = await api('getAiConsultSessionDetail', { corpId: corpId.value, sessionId: sessionId.value, miniAppId: openid.value || uni.getStorageSync('openid') }, false); const res = await api('getAiConsultSessionDetail', { corpId: corpId.value, sessionId: sessionId.value, miniAppId: openid.value || uni.getStorageSync('openid') }, false);
if (!res?.success) return uni.showToast({ title: removeAiLabel(res?.message, '加载失败'), icon: 'none' }); if (!res?.success) return uni.showToast({ title: removeAiLabel(res?.message, '加载失败'), icon: 'none' });
session.value = res.data.session; session.value = res.data.session;
avatarLoadFailed.value = false;
patientInfo.value = res.data.patient || null;
messages.value = res.data.messages || []; messages.value = res.data.messages || [];
active.value = session.value.status === 'active'; active.value = session.value.status === 'active';
uni.setNavigationBarTitle({ title: assistantDisplayName.value }); uni.setNavigationBarTitle({ title: assistantDisplayName.value });
@ -121,41 +163,55 @@ async function send() {
} }
} }
onLoad(opts => { corpId.value = opts.corpId; sessionId.value = opts.sessionId; detail(); }); onLoad(opts => {
corpId.value = opts.corpId;
sessionId.value = opts.sessionId;
uni.onKeyboardHeightChange(handleKeyboardHeightChange);
detail();
});
onShow(detail); onShow(detail);
onUnload(() => {
uni.offKeyboardHeightChange(handleKeyboardHeightChange);
});
</script> </script>
<style scoped> <style scoped>
.chat-page { height: 100vh; display: flex; flex-direction: column; background: #f5f7fb; overflow: hidden; } .chat-page { position: fixed; top: 0; right: 0; left: 0; display: flex; flex-direction: column; background: #f5f7fb; overflow: hidden; }
.patient-info-bar { height: 80rpx; padding: 0 24rpx; display: flex; align-items: center; box-sizing: border-box; background: #fff; border-bottom: 1rpx solid #edf0f4; flex-shrink: 0; }
.patient-summary { display: flex; align-items: center; gap: 16rpx; min-width: 0; flex: 1; }
.patient-name { max-width: 220rpx; color: #283242; font-size: 30rpx; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.patient-detail { min-width: 0; color: #7d8590; font-size: 27rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.patient-action { margin-left: 16rpx; color: #0877f1; font-size: 26rpx; white-space: nowrap; flex-shrink: 0; }
.patient-arrow { margin-left: 6rpx; color: #0877f1; font-size: 34rpx; line-height: 1; flex-shrink: 0; }
.chat-content { flex: 1; height: 0; box-sizing: border-box; } .chat-content { flex: 1; height: 0; box-sizing: border-box; }
.message-list { padding: 28rpx 24rpx 40rpx; } .message-list { padding: 28rpx 24rpx 40rpx; }
.message-item { margin-bottom: 28rpx; } .message-item { margin-bottom: 28rpx; }
.message-content { display: flex; align-items: flex-start; gap: 16rpx; } .message-content { display: flex; align-items: flex-start; gap: 16rpx; }
.message-patient .message-content { justify-content: flex-end; } .message-patient .message-content { justify-content: flex-end; }
.avatar { width: 72rpx; height: 72rpx; flex: 0 0 72rpx; display: flex; align-items: center; justify-content: center; border-radius: 50%; font-size: 24rpx; font-weight: 600; } .avatar { width: 72rpx; height: 72rpx; flex: 0 0 72rpx; display: flex; align-items: center; justify-content: center; border-radius: 50%; font-size: 26rpx; font-weight: 600; }
.assistant-avatar { color: #fff; background: linear-gradient(135deg, #4d93ff, #0877f1); } .assistant-avatar { color: #fff; background: linear-gradient(135deg, #4d93ff, #0877f1); }
.patient-avatar { color: #0877f1; background: #e2efff; } .patient-avatar { color: #0877f1; background: #e2efff; }
.message-bubble-container { max-width: 74%; min-width: 0; } .message-bubble-container { max-width: 74%; min-width: 0; }
.message-patient .message-bubble-container { display: flex; flex-direction: column; align-items: flex-end; } .message-patient .message-bubble-container { display: flex; flex-direction: column; align-items: flex-end; }
.username-label { margin: 2rpx 0 10rpx; color: #8a919f; font-size: 24rpx; line-height: 32rpx; } .username-label { margin: 2rpx 0 10rpx; color: #8a919f; font-size: 26rpx; line-height: 34rpx; }
.message-bubble { padding: 18rpx 22rpx; border-radius: 8rpx 22rpx 22rpx; background: #fff; box-shadow: 0 4rpx 14rpx rgba(17, 48, 89, .06); } .message-bubble { padding: 18rpx 22rpx; border-radius: 8rpx 22rpx 22rpx; background: #fff; box-shadow: 0 4rpx 14rpx rgba(17, 48, 89, .06); }
.message-patient .message-bubble { color: #fff; border-radius: 22rpx 8rpx 22rpx 22rpx; background: #0877f1; box-shadow: 0 4rpx 14rpx rgba(8, 119, 241, .18); } .message-patient .message-bubble { color: #fff; border-radius: 22rpx 8rpx 22rpx 22rpx; background: #0877f1; box-shadow: 0 4rpx 14rpx rgba(8, 119, 241, .18); }
.message-bubble.pending { opacity: .72; } .message-bubble.pending { opacity: .72; }
.message-text { color: inherit; font-size: 30rpx; line-height: 46rpx; white-space: pre-wrap; word-break: break-word; } .message-text { color: inherit; font-size: 32rpx; line-height: 48rpx; white-space: pre-wrap; word-break: break-word; }
.time-divider { margin: 2rpx 0 22rpx; color: #a0a6b0; text-align: center; font-size: 23rpx; } .time-divider { margin: 2rpx 0 22rpx; color: #a0a6b0; text-align: center; font-size: 25rpx; }
.system-message { display: inline-block; margin: 0 auto; padding: 10rpx 20rpx; color: #79808c; background: #e9ecf1; border-radius: 22rpx; font-size: 24rpx; line-height: 36rpx; } .system-message { display: inline-block; margin: 0 auto; padding: 10rpx 20rpx; color: #79808c; background: #e9ecf1; border-radius: 22rpx; font-size: 26rpx; line-height: 38rpx; }
.message-system { text-align: center; } .message-system { text-align: center; }
.system-message.warn { color: #d84256; background: #fff0f1; } .system-message.warn { color: #d84256; background: #fff0f1; }
.send-status { margin-top: 8rpx; color: #e34d59; font-size: 23rpx; } .send-status { margin-top: 8rpx; color: #e34d59; font-size: 25rpx; }
.thinking { display: flex; align-items: center; gap: 8rpx; min-width: 84rpx; padding: 26rpx 28rpx; } .thinking { display: flex; align-items: center; gap: 8rpx; min-width: 84rpx; padding: 26rpx 28rpx; }
.thinking-dot { width: 10rpx; height: 10rpx; border-radius: 50%; background: #7f8998; animation: thinking 1.2s infinite ease-in-out; } .thinking-dot { width: 10rpx; height: 10rpx; border-radius: 50%; background: #7f8998; animation: thinking 1.2s infinite ease-in-out; }
.thinking-dot:nth-child(2) { animation-delay: .16s; } .thinking-dot:nth-child(2) { animation-delay: .16s; }
.thinking-dot:nth-child(3) { animation-delay: .32s; } .thinking-dot:nth-child(3) { animation-delay: .32s; }
@keyframes thinking { 0%, 60%, 100% { opacity: .35; transform: translateY(0); } 30% { opacity: 1; transform: translateY(-7rpx); } } @keyframes thinking { 0%, 60%, 100% { opacity: .35; transform: translateY(0); } 30% { opacity: 1; transform: translateY(-7rpx); } }
.input-section { display: flex; align-items: flex-end; gap: 16rpx; padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom)); background: #fff; border-top: 1rpx solid #edf0f4; } .input-section { display: flex; align-items: flex-end; gap: 16rpx; padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom)); background: #fff; border-top: 1rpx solid #edf0f4; }
.text-input { flex: 1; max-height: 180rpx; padding: 16rpx 20rpx; box-sizing: border-box; color: #283242; background: #f3f5f8; border-radius: 12rpx; font-size: 29rpx; line-height: 40rpx; } .text-input { flex: 1; max-height: 180rpx; padding: 16rpx 20rpx; box-sizing: border-box; color: #283242; background: #f3f5f8; border-radius: 12rpx; font-size: 31rpx; line-height: 42rpx; }
.send-btn { flex: 0 0 auto; height: 72rpx; margin: 0; padding: 0 26rpx; color: #fff; background: #0877f1; border-radius: 12rpx; font-size: 27rpx; line-height: 72rpx; } .send-btn { flex: 0 0 auto; height: 72rpx; margin: 0; padding: 0 26rpx; color: #fff; background: #0877f1; border-radius: 12rpx; font-size: 29rpx; line-height: 72rpx; }
.send-btn[disabled] { color: #fff; background: #a9cfff; } .send-btn[disabled] { color: #fff; background: #a9cfff; }
.rate-card { display: block; margin: 20rpx 0 0; } .rate-card { display: block; margin: 20rpx 0 0; }
.ended { padding: 30rpx; color: #7d8590; text-align: center; background: #fff; font-size: 27rpx; } .ended { padding: 30rpx; color: #7d8590; text-align: center; background: #fff; font-size: 29rpx; }
</style> </style>

View File

@ -11,7 +11,7 @@
<view v-for="conversation in conversationList" :key="conversation.source === 'ai' ? `ai-${conversation.sessionId}` : (conversation.groupID || conversation.conversationID)" <view v-for="conversation in conversationList" :key="conversation.source === 'ai' ? `ai-${conversation.sessionId}` : (conversation.groupID || conversation.conversationID)"
class="message-item" @click="handleClickConversation(conversation)"> class="message-item" @click="handleClickConversation(conversation)">
<view class="avatar-container"> <view class="avatar-container">
<image v-if="conversation.source === 'ai'" class="avatar" src="/static/home/ai-consult-assistant.png" mode="aspectFill" /> <image v-if="conversation.source === 'ai'" class="avatar" :src="conversation.assistantAvatar || defaultAssistantAvatar" mode="aspectFill" @error="handleConversationAvatarError(conversation)" />
<GroupAvatar v-else :avatarList="getAvatarList(conversation.groupID)" :size="96" classType="square" /> <GroupAvatar v-else :avatarList="getAvatarList(conversation.groupID)" :size="96" classType="square" />
<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">{{
@ -101,6 +101,7 @@ const hasMore = ref(false);
const refreshing = ref(false); const refreshing = ref(false);
const showSubscribeEntry = ref(false); const showSubscribeEntry = ref(false);
const HOME_CURRENT_TEAM_CACHE_KEY = "home-current-team-info"; const HOME_CURRENT_TEAM_CACHE_KEY = "home-current-team-info";
const defaultAssistantAvatar = "/static/home/ai-consult-assistant.png";
// //
const { loadGroupAvatars, getAvatarList } = useGroupAvatars(); const { loadGroupAvatars, getAvatarList } = useGroupAvatars();
@ -262,6 +263,10 @@ function mergeConsultationList(imConversations) {
); );
} }
function handleConversationAvatarError(conversation) {
conversation.assistantAvatar = defaultAssistantAvatar;
}
const loadAiConsultationList = async () => { const loadAiConsultationList = async () => {
const corpId = getCurrentTeamCorpId(); const corpId = getCurrentTeamCorpId();
const miniAppId = openid.value || account.value?.openid || ""; const miniAppId = openid.value || account.value?.openid || "";
@ -281,6 +286,7 @@ const loadAiConsultationList = async () => {
sessionId: item.id, sessionId: item.id,
corpId: item.corpId, corpId: item.corpId,
displayName: removeAiLabel(item.assistantName, "咨询助理"), displayName: removeAiLabel(item.assistantName, "咨询助理"),
assistantAvatar: item.assistantAvatar || defaultAssistantAvatar,
teamName: item.teamName || "", teamName: item.teamName || "",
patientName: item.customerName || "未命名患者", patientName: item.customerName || "未命名患者",
lastMessage: removeAiLabel(item.statusLabel, "咨询"), lastMessage: removeAiLabel(item.statusLabel, "咨询"),