2026-08-07 09:35:21 +08:00

159 lines
9.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="chat-page">
<scroll-view scroll-y class="chat-content" :scroll-into-view="bottomId">
<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-if="shouldShowTime(item, index)" class="time-divider">{{ formatMessageTime(item.createTime) }}</view>
<view v-if="item.sender === 'system'" class="system-message" :class="{ warn: item.messageType === 'sensitive_notice' }">{{ displayContent(item) }}</view>
<evaluation-card v-if="item.messageType === 'consult_ended' && (item.rateId || session?.rateId)" class="rate-card" :extension="{ rateId: item.rateId || session.rateId }" :doctor-info="rateAssistantInfo" />
<view v-else class="message-content">
<view v-if="item.sender === 'assistant'" class="avatar assistant-avatar">AI</view>
<view class="message-bubble-container">
<view v-if="item.sender === 'assistant'" class="username-label">{{ session?.assistantName || 'AI咨询助理' }}</view>
<view class="message-bubble" :class="{ pending: item._sendStatus === 'pending' }">
<text class="message-text">{{ displayContent(item) }}</text>
</view>
<text v-if="item._sendStatus === 'failed'" class="send-status">发送失败请重试</text>
</view>
<view v-if="item.sender === 'patient'" class="avatar patient-avatar"></view>
</view>
</view>
<view v-if="waitingAi" class="message-item message-assistant">
<view class="message-content"><view class="avatar assistant-avatar">AI</view><view class="message-bubble-container"><view class="username-label">{{ session?.assistantName || 'AI咨询助理' }}</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 id="bottom" />
</view>
</scroll-view>
<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" :disabled="sending" placeholder="请输入消息" @confirm="send" />
<button class="send-btn" :loading="sending" :disabled="!content.trim() || sending" @click="send">发送</button>
</view>
<view v-else class="ended">本次AI咨询已结束</view>
</view>
</template>
<script setup>
import { ref, nextTick, computed } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import api from '@/utils/api';
import useAccountStore from '@/store/account';
import { storeToRefs } from 'pinia';
import EvaluationCard from '@/pages/message/components/special-message/evaluation.vue';
const { openid } = storeToRefs(useAccountStore());
const corpId = ref('');
const sessionId = ref('');
const session = ref(null);
const messages = ref([]);
const content = ref('');
const sending = ref(false);
const waitingAi = ref(false);
const bottomId = ref('');
const active = ref(false);
const rateAssistantInfo = computed(() => ({ name: session.value?.assistantName || 'AI咨询助理', title: 'AI咨询助理', department: session.value?.teamName || '', avatar: '' }));
function displayContent(item) {
const value = item?.content || '';
if (item?.sender !== 'assistant' || typeof value !== 'string') return value;
const contents = [];
const matcher = /"streamContent"\s*:\s*"((?:\\.|[^"\\])*)"/g;
let match;
while ((match = matcher.exec(value))) {
try {
const text = JSON.parse(`"${match[1]}"`);
if (text) contents.push(text);
} catch (error) {
// 兼容历史会话中无法解析的单段协议消息。
}
}
return contents.join('') || value;
}
function formatMessageTime(value) {
const date = new Date(Number(value));
if (Number.isNaN(date.getTime())) return '';
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
}
function shouldShowTime(item, index) {
if (index === 0) return Boolean(item?.createTime);
return Number(item?.createTime) - Number(messages.value[index - 1]?.createTime) > 5 * 60 * 1000;
}
async function scrollToBottom() {
bottomId.value = '';
await nextTick();
bottomId.value = 'bottom';
}
async function detail() {
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: res?.message || '加载失败', icon: 'none' });
session.value = res.data.session;
messages.value = res.data.messages || [];
active.value = session.value.status === 'active';
uni.setNavigationBarTitle({ title: session.value.assistantName || 'AI咨询助理' });
await scrollToBottom();
}
async function send() {
if (!active.value || sending.value || !content.value.trim()) return;
const value = content.value.trim();
const localMessage = { _id: `local-${Date.now()}`, sender: 'patient', content: value, messageType: 'text', createTime: Date.now(), _sendStatus: 'pending' };
messages.value.push(localMessage);
content.value = '';
sending.value = true;
waitingAi.value = true;
await scrollToBottom();
try {
const res = await api('sendAiConsultMessage', { corpId: corpId.value, sessionId: sessionId.value, customerId: session.value.customerId, miniAppId: openid.value || uni.getStorageSync('openid'), content: value }, false);
if (!res?.success) {
localMessage._sendStatus = 'failed';
uni.showToast({ title: res?.message || '发送失败', icon: 'none' });
return;
}
await detail();
} catch (error) {
localMessage._sendStatus = 'failed';
uni.showToast({ title: '发送失败,请重试', icon: 'none' });
} finally {
sending.value = false;
waitingAi.value = false;
await scrollToBottom();
}
}
onLoad(opts => { corpId.value = opts.corpId; sessionId.value = opts.sessionId; detail(); });
onShow(detail);
</script>
<style scoped>
.chat-page { height: 100vh; display: flex; flex-direction: column; background: #f5f7fb; overflow: hidden; }
.chat-content { flex: 1; height: 0; box-sizing: border-box; }
.message-list { padding: 28rpx 24rpx 40rpx; }
.message-item { margin-bottom: 28rpx; }
.message-content { display: flex; align-items: flex-start; gap: 16rpx; }
.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; }
.assistant-avatar { color: #fff; background: linear-gradient(135deg, #4d93ff, #0877f1); }
.patient-avatar { color: #0877f1; background: #e2efff; }
.message-bubble-container { max-width: 74%; min-width: 0; }
.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; }
.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-bubble.pending { opacity: .72; }
.message-text { color: inherit; font-size: 30rpx; line-height: 46rpx; white-space: pre-wrap; word-break: break-word; }
.time-divider { margin: 2rpx 0 22rpx; color: #a0a6b0; text-align: center; font-size: 23rpx; }
.system-message { display: inline-block; margin: 0 auto; padding: 10rpx 20rpx; color: #79808c; background: #e9ecf1; border-radius: 22rpx; font-size: 24rpx; line-height: 36rpx; }
.message-system { text-align: center; }
.system-message.warn { color: #d84256; background: #fff0f1; }
.send-status { margin-top: 8rpx; color: #e34d59; font-size: 23rpx; }
.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:nth-child(2) { animation-delay: .16s; }
.thinking-dot:nth-child(3) { animation-delay: .32s; }
@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; }
.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; }
.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[disabled] { color: #fff; background: #a9cfff; }
.rate-card { display: block; margin: 20rpx 0 0; }
.ended { padding: 30rpx; color: #7d8590; text-align: center; background: #fff; font-size: 27rpx; }
</style>