在线问诊默认不能选择到店,聊天UI优化

This commit is contained in:
李琦
2026-01-26 15:55:13 +08:00
parent 696cb2e0fa
commit e57fd125f7
6 changed files with 741 additions and 459 deletions

View File

@@ -1,21 +1,20 @@
<template>
<!-- 根容器使用 Flex 布局实现全屏高度 -->
<!-- 根容器 -->
<view class="chat-container">
<!-- ================= 顶部导航栏区域 ================= -->
<!-- ================= 顶部导航栏 (保持原样) ================= -->
<view class="chat-header">
<view class="header-main" @click="toggleDoctorDetail">
<view class="doctor-info-group">
<text class="doctor-name">{{ doctorUserInfo.nick_name }}</text>
<view class="doctor-badges">
<text class="status-dot"></text>
<!-- <text class="sub-text">在线 · 心血管内科</text>-->
<text class="sub-text">在线</text>
</view>
</view>
<view class="header-action">
<text class="expand-tip">{{ showDoctorDetail ? '收起' : '详情' }}</text>
<u-icon :name="showDoctorDetail ? 'arrow-up' : 'arrow-down'" size="14" color="#999"></u-icon>
<u-icon :name="showDoctorDetail ? 'arrow-up' : 'arrow-down'" size="12" color="#0A84FF"></u-icon>
</view>
</view>
@@ -23,7 +22,7 @@
<view class="detail-grid">
<view class="detail-row">
<text class="label">职称</text>
<text class="value">主任医师</text>
<text class="value tag-value">主任医师</text>
</view>
<view class="detail-row">
<text class="label">医院</text>
@@ -31,7 +30,7 @@
</view>
<view class="detail-row full-width">
<text class="label">擅长</text>
<text class="value">冠心病高血压心力衰竭心律失常的诊断与药物治疗</text>
<text class="value desc-value">冠心病高血压心力衰竭心律失常的诊断与药物治疗</text>
</view>
</view>
</view>
@@ -48,230 +47,40 @@
@scrolltoupper="loadHistory"
@scroll="onScroll"
>
<!-- 动态 Padding 防止键盘遮挡 -->
<view class="message-list-container" id="msglist" :style="{ paddingBottom: (keyboardHeight > 0 ? keyboardHeight + 20 : 0) + 'px' }">
<view class="message-list-container" id="msglist" :style="{ paddingBottom: (keyboardHeight > 0 ? keyboardHeight + 10 : 0) + 'px' }">
<!-- 加载指示器 -->
<view v-if="loading" class="loading-spinner">
<view class="spinner"></view>
<text style="font-size: 24rpx; color: #999; margin-left: 16rpx;">加载历史消息...</text>
<text class="spinner-text">历史记录加载中...</text>
</view>
<!-- 无更多消息 -->
<view v-if="isEmpty" class="empty-state">
<text>没有更多历史消息了</text>
<text class="empty-text">没有更多消息了</text>
</view>
<!-- 消息列表循环 -->
<view v-for="(msg, index) in messages" :key="index" class="message-wrapper">
<!-- >>>>> 情况 A: 对方发送的消息 (医生) <<<<< -->
<view v-if="getSenderIdWithoutPrefix(msg.sender_user_id) !== getCurrentUserIdWithoutPrefix()" class="chat-row other">
<image class="avatar" :src="doctorAvatar" mode="aspectFill"></image>
<view class="bubble-container">
<view class="message-bubble" :class="'type-' + msg.message_type">
<!-- 图片 -->
<image v-if="msg.message_type === 1" :src="getParsedContent(msg.message_type, msg.message_content)" mode="widthFix" class="media-content image" @click="previewImage(getParsedContent(msg.message_type, msg.message_content))"></image>
<!-- 视频 -->
<video v-else-if="msg.message_type === 3" :src="getParsedContent(msg.message_type, msg.message_content)" controls class="media-content video"></video>
<!-- 语音 -->
<view v-else-if="msg.message_type === 2" class="audio-content" @click="playAudio(msg)">
<u-icon name="volume" size="18" color="#333"></u-icon>
<text class="duration">{{ msg.duration }}''</text>
<view class="wifi-symbol" :class="{'active': isPlaying(msg)}">
<view class="wifi-circle first"></view><view class="wifi-circle second"></view><view class="wifi-circle third"></view>
</view>
</view>
<!-- 复杂卡片 -->
<view v-else-if="[4, 9, 10, 11, 12, 13].includes(msg.message_type)" class="card-bubble">
<!-- 类型10: 挂号 -->
<block v-if="msg.message_type === 10">
<view class="card-header">
<view class="title-group"><u-icon name="calendar-fill" size="18" color="#059669"></u-icon><text class="card-title">预约挂号</text></view>
<text class="status-tag" :class="'status-' + getParsedContent(msg.message_type, msg.message_content).status">{{ getRegisterStatusText(getParsedContent(msg.message_type, msg.message_content).status) }}</text>
</view>
<view class="card-body">
<view class="info-row"><text class="label">订单号</text><text class="value">{{ getParsedContent(msg.message_type, msg.message_content).order_no }}</text></view>
<view class="info-row"><text class="label">就诊人</text><text class="value">{{ (getParsedContent(msg.message_type, msg.message_content).user_patient && getParsedContent(msg.message_type, msg.message_content).user_patient.name) || '未知' }}</text></view>
<view class="info-row highlight"><text class="label">费用</text><text class="value price">¥{{ getParsedContent(msg.message_type, msg.message_content).price }}</text></view>
</view>
<view class="card-footer"><button class="action-btn" @click="viewRegister(getParsedContent(msg.message_type, msg.message_content).id)">查看详情</button></view>
</block>
<!-- 类型11: 就诊经历 -->
<block v-else-if="msg.message_type === 11">
<view class="card-header">
<view class="title-group"><u-icon name="file-text-fill" size="18" color="#059669"></u-icon><text class="card-title">患者就诊经历</text></view>
</view>
<view class="card-body">
<view class="info-row" v-if="getParsedContent(msg.message_type, msg.message_content).drug_name"><text class="label">药品名称</text><text class="value">{{ getParsedContent(msg.message_type, msg.message_content).drug_name }}</text></view>
<view class="text-block" v-if="getParsedContent(msg.message_type, msg.message_content).illness_info"><text class="block-label">症状描述</text><text class="block-content">{{ getParsedContent(msg.message_type, msg.message_content).illness_info }}</text></view>
<view class="info-row"><text class="label">是否就诊过</text><text class="value">{{ getParsedContent(msg.message_type, msg.message_content).has_visited === '1' ? '是' : '否' }}</text></view>
<view class="info-row"><text class="label">是否使用过药品</text><text class="value">{{ getParsedContent(msg.message_type, msg.message_content).has_used_drug === '1' ? '是' : '否' }}</text></view>
</view>
</block>
<!-- 类型4: 电子处方 -->
<block v-else-if="msg.message_type === 4">
<view class="card-header">
<view class="title-group"><u-icon name="order" size="18" color="#298dff"></u-icon><text class="card-title">电子处方</text></view>
<text class="status-tag blue">{{ getStatusText(getParsedContent(msg.message_type, msg.message_content).status) }}</text>
</view>
<view class="card-body">
<view class="info-row"><text class="label">诊断单号</text><text class="value small">{{ getParsedContent(msg.message_type, msg.message_content).order_no }}</text></view>
<view class="info-row"><text class="label">金额</text><text class="value price">¥{{ getParsedContent(msg.message_type, msg.message_content).total_pay_price }}</text></view>
</view>
<view class="card-footer split">
<text class="link-btn" @click="viewPrescription(getParsedContent(msg.message_type, msg.message_content).id, getParsedContent(msg.message_type, msg.message_content).order_no)">详情</text>
<button class="action-btn primary" @click="goToOrderMedicine(getParsedContent(msg.message_type, msg.message_content).order_id, getParsedContent(msg.message_type, msg.message_content).order_no)">一键购药</button>
</view>
</block>
<!-- 类型12: 商品 -->
<block v-else-if="msg.message_type === 12">
<view class="card-header">
<view class="title-group"><u-icon name="shopping-cart-fill" size="18" color="#2563eb"></u-icon><text class="card-title">推荐商品</text></view>
</view>
<view class="card-body">
<view class="info-row"><text class="label">名称</text><text class="value">{{ getParsedContent(msg.message_type, msg.message_content).product_name }}</text></view>
<view class="info-row"><text class="label">价格</text><text class="value price">¥{{ getParsedContent(msg.message_type, msg.message_content).price }}</text></view>
</view>
</block>
<!-- 类型9: 系统消息 -->
<block v-else-if="msg.message_type === 9">
<view class="system-message-card" v-if="getParsedContent(msg.message_type, msg.message_content).type === 'price_update'">
<view class="price-update-notice"><u-icon name="info-circle-fill" size="18" color="#fa8c16"></u-icon><text class="notice-text">{{ getParsedContent(msg.message_type, msg.message_content).message }}</text></view>
<view class="price-detail"><text>订单号{{ getParsedContent(msg.message_type, msg.message_content).order_no }}</text><text>应付金额¥{{ getParsedContent(msg.message_type, msg.message_content).total_pay_price }}</text></view>
</view>
<view v-else class="system-message"><text>{{ msg.message_content }}</text></view>
</block>
<!-- 类型13: 结束问诊 -->
<block v-else-if="msg.message_type === 13">
<view class="card-header">
<view class="title-group"><u-icon name="checkmark-circle-fill" size="18" color="#059669"></u-icon><text class="card-title">问诊结束</text></view>
</view>
<view class="card-body">
<view class="text-block"><text class="block-content">{{ getParsedContent(msg.message_type, msg.message_content).reason || '本次服务已完成' }}</text></view>
</view>
</block>
<block v-else><view class="card-body"><text>不支持的消息类型</text></view></block>
</view>
<!-- 文本 -->
<text v-else class="text-content">{{ getParsedContent(msg.message_type, msg.message_content) }}</text>
</view>
</view>
</view>
<!-- >>>>> 情况 B: 我发送的消息 (患者) <<<<< -->
<view v-else-if="getSenderIdWithoutPrefix(msg.sender_user_id) === getCurrentUserIdWithoutPrefix()" class="chat-row mine">
<image class="avatar" :src="userAvatar" mode="aspectFill"></image>
<view class="bubble-container">
<view class="message-bubble" :class="'type-' + msg.message_type">
<!-- 图片 -->
<image v-if="msg.message_type === 1" :src="getParsedContent(msg.message_type, msg.message_content)" mode="widthFix" class="media-content image" @click="previewImage(getParsedContent(msg.message_type, msg.message_content))"></image>
<!-- 视频 -->
<video v-else-if="msg.message_type === 3" :src="getParsedContent(msg.message_type, msg.message_content)" controls class="media-content video"></video>
<!-- 语音 -->
<view v-else-if="msg.message_type === 2" class="audio-content mine-audio" @click="playAudio(msg)">
<text class="duration">{{ msg.duration }}''</text>
<u-icon name="volume-fill" size="18" color="#fff"></u-icon>
</view>
<!-- 卡片: 我发送的卡片 -->
<view v-else-if="[4, 9, 10, 11, 12, 13].includes(msg.message_type)" class="card-bubble">
<!-- 类型10: 挂号 -->
<block v-if="msg.message_type === 10">
<view class="card-header">
<view class="title-group"><u-icon name="calendar-fill" size="18" color="#059669"></u-icon><text class="card-title">预约挂号</text></view>
<text class="status-tag" :class="'status-' + getParsedContent(msg.message_type, msg.message_content).status">{{ getRegisterStatusText(getParsedContent(msg.message_type, msg.message_content).status) }}</text>
</view>
<view class="card-body">
<view class="info-row"><text class="label">订单号</text><text class="value">{{ getParsedContent(msg.message_type, msg.message_content).order_no }}</text></view>
<view class="info-row"><text class="label">就诊人</text><text class="value">{{ (getParsedContent(msg.message_type, msg.message_content).user_patient && getParsedContent(msg.message_type, msg.message_content).user_patient.name) || '未知' }}</text></view>
<view class="info-row highlight"><text class="label">费用</text><text class="value price">¥{{ getParsedContent(msg.message_type, msg.message_content).price }}</text></view>
</view>
<view class="card-footer"><button class="action-btn" @click="viewRegister(getParsedContent(msg.message_type, msg.message_content).id)">查看详情</button></view>
</block>
<!-- 类型11: 患者就诊经历 -->
<block v-else-if="msg.message_type === 11">
<view class="card-header">
<view class="title-group"><u-icon name="file-text-fill" size="18" color="#059669"></u-icon><text class="card-title">患者就诊经历</text></view>
</view>
<view class="card-body">
<view class="info-row" v-if="getParsedContent(msg.message_type, msg.message_content).drug_name"><text class="label">药品名称</text><text class="value">{{ getParsedContent(msg.message_type, msg.message_content).drug_name }}</text></view>
<view class="text-block" v-if="getParsedContent(msg.message_type, msg.message_content).illness_info"><text class="block-label">症状描述</text><text class="block-content">{{ getParsedContent(msg.message_type, msg.message_content).illness_info }}</text></view>
<view class="info-row"><text class="label">是否就诊过</text><text class="value">{{ getParsedContent(msg.message_type, msg.message_content).has_visited === '1' ? '是' : '否' }}</text></view>
<view class="info-row"><text class="label">是否使用过药品</text><text class="value">{{ getParsedContent(msg.message_type, msg.message_content).has_used_drug === '1' ? '是' : '否' }}</text></view>
</view>
</block>
<!-- 类型4: 电子处方 (此前错误的位置) -->
<block v-else-if="msg.message_type === 4">
<view class="card-header">
<view class="title-group"><u-icon name="order" size="18" color="#298dff"></u-icon><text class="card-title">电子处方</text></view>
<text class="status-tag blue">{{ getStatusText(getParsedContent(msg.message_type, msg.message_content).status) }}</text>
</view>
<view class="card-body">
<view class="info-row"><text class="label">诊断单号</text><text class="value small">{{ getParsedContent(msg.message_type, msg.message_content).order_no }}</text></view>
<view class="info-row"><text class="label">金额</text><text class="value price">¥{{ getParsedContent(msg.message_type, msg.message_content).total_pay_price }}</text></view>
</view>
<view class="card-footer split">
<text class="link-btn" @click="viewPrescription(getParsedContent(msg.message_type, msg.message_content).id, getParsedContent(msg.message_type, msg.message_content).order_no)">详情</text>
<button class="action-btn primary" @click="goToOrderMedicine(getParsedContent(msg.message_type, msg.message_content).order_id, getParsedContent(msg.message_type, msg.message_content).order_no)">一键购药</button>
</view>
</block>
<!-- 类型12: 商品 -->
<block v-else-if="msg.message_type === 12">
<view class="card-header">
<view class="title-group"><u-icon name="shopping-cart-fill" size="18" color="#2563eb"></u-icon><text class="card-title">推荐商品</text></view>
</view>
<view class="card-body">
<view class="info-row"><text class="label">名称</text><text class="value">{{ getParsedContent(msg.message_type, msg.message_content).product_name }}</text></view>
<view class="info-row"><text class="label">价格</text><text class="value price">¥{{ getParsedContent(msg.message_type, msg.message_content).price }}</text></view>
</view>
</block>
<!-- 类型13: 结束问诊 -->
<block v-else-if="msg.message_type === 13">
<view class="card-header">
<view class="title-group"><u-icon name="checkmark-circle-fill" size="18" color="#059669"></u-icon><text class="card-title">问诊结束</text></view>
</view>
<view class="card-body">
<view class="text-block"><text class="block-content">{{ getParsedContent(msg.message_type, msg.message_content).reason || '本次服务已完成' }}</text></view>
</view>
</block>
<block v-else>
<view class="card-body"><text>不支持的消息类型</text></view>
</block>
</view>
<!-- 文本 -->
<text v-else class="text-content">{{ getParsedContent(msg.message_type, msg.message_content) }}</text>
</view>
</view>
</view>
<!-- 时间分割线 -->
<!-- 时间 -->
<view class="time-divider" v-if="shouldShowTime(msg, index)">
{{ msg.created_at_text || msg.time }}
<text class="time-text">{{ msg.created_at_text || msg.time }}</text>
</view>
<!-- 使用抽离的组件 -->
<MessageBubble
:msg="msg"
:is-mine="isMsgMine(msg)"
:avatar="getMsgAvatar(msg)"
:is-playing="isPlaying(msg)"
@previewImage="previewImage"
@playAudio="playAudio"
@viewRegister="viewRegister"
@viewPrescription="viewPrescription"
@goToOrderMedicine="goToOrderMedicine"
/>
</view>
<!-- 底部锚点 -->
@@ -279,36 +88,37 @@
</view>
<!-- 底部静态占位 -->
<view style="height: 270rpx;"></view>
<view style="height: 180rpx;"></view>
</scroll-view>
<!-- ================= 底部输入与工具栏 ================= -->
<!-- ================= 底部输入与工具栏 (重构优化版) ================= -->
<view class="chat-footer safe-area-inset-bottom" :style="{ bottom: keyboardHeight + 'px' }">
<view v-if="isConsultationEnded" class="consultation-ended-notice">
<u-icon name="checkmark-circle-fill" size="20" color="#999"></u-icon>
<u-icon name="checkmark-circle-fill" size="18" color="#999"></u-icon>
<text>本次问诊已结束</text>
</view>
<template v-else>
<view class="toolbar-area">
<view class="action-grid">
<view class="action-item" @click="openMediaPicker('image')">
<view class="icon-circle"><u-icon name="photo-fill" size="20" color="#606266"></u-icon></view>
<text>相册</text>
</view>
</view>
<!-- 相册工具栏 -->
<view class="toolbar-area" v-if="false">
<!-- 暂时隐藏 -->
</view>
<view class="input-bar">
<!-- 左侧语音切换按钮 -->
<view class="icon-btn left" @click="toggleVoiceInput">
<u-icon :name="voiceInputActive ? 'close' : 'mic'" size="26" color="#333"></u-icon>
</view>
<!-- 中间输入框 / 按住说话 -->
<view class="input-field-wrapper">
<input
v-if="!voiceInputActive"
v-model="newMessage"
class="main-input"
placeholder="描述您的症状或问题..."
:adjust-position="false"
confirm-type="send"
@confirm="sendTextMessage"
:adjust-position="false"
cursor-spacing="20"
/>
<view v-else class="voice-btn"
@@ -320,23 +130,25 @@
</view>
</view>
<view class="send-btn-wrapper" v-if="newMessage.trim() || voiceInputActive">
<view class="send-btn" @click="sendTextMessage">
<image src="/static/chat/send2.png" style="width: 30rpx; height: 30rpx;" />
<!-- 右侧发送按钮 加号 -->
<view class="right-action">
<view v-if="newMessage.trim()" class="send-btn" @click="sendTextMessage">发送</view>
<view v-else class="icon-btn" @click="openMediaPicker('image')">
<u-icon name="plus-circle" size="26" color="#333"></u-icon>
</view>
</view>
</view>
</template>
</view>
<!-- 录音蒙层 -->
<!-- 录音蒙层 (保持原样) -->
<view v-if="showRecordingModal" class="recording-overlay">
<view class="recording-box">
<view class="wave-animation"></view>
<view style="margin: 20rpx 0;">
<u-icon name="mic-fill" size="40" color="#fff"></u-icon>
</view>
<text>正在录音 {{ recordingTime }}s</text>
<text class="timer">正在录音 {{ recordingTime }}s</text>
<text class="tip">手指上滑取消</text>
</view>
</view>
@@ -344,9 +156,7 @@
</template>
<script>
/**
* 导入依赖
*/
import MessageBubble from './components/MessageBubble.vue'; // 引入组件
import { webSocketManager } from '@/utils/ws/websocket';
import { ChatManager } from '@/store/chat/chat.js';
import {getChatRegisterInfoApi, sendToUserApi, upLoadChatFileApi, getDoctorInfoApi, getRoomStatusApi} from "@/request/api/im";
@@ -356,28 +166,22 @@ import { checkDev } from '@/utils/utils';
const isDev = checkDev('dev');
const uploadBaseUrl = isDev ? 'http://127.0.0.1:18001/api/mobile' : 'https://api.xiaokang88.com/api/mobile';
// 辅助函数保留在父组件逻辑中
function getParsedContent(messageType, messageContent) {
if ([4, 9, 10, 11, 12, 13].includes(messageType)) {
try {
return typeof messageContent === 'object' ? messageContent : JSON.parse(messageContent);
} catch (e) {
console.error('解析消息内容失败:', e);
return messageContent;
}
}
return messageContent;
}
function getMessageTypeName(messageType) {
const types = {
0: 'text', 1: 'image', 2: 'audio', 3: 'video', 4: 'prescription', 5: 'file',
6: 'video-call', 7: 'audio-call', 8: 'file', 9: 'system', 10: 'register',
11: 'patient-experience', 12: 'product-card', 13: 'end-consultation',
};
return types[messageType] || 'unknown';
}
export default {
components: {
MessageBubble
},
data() {
return {
doctorUserInfo: { id: 0, avatar: '', nick_name: '医生', room_id: '' },
@@ -388,12 +192,13 @@ export default {
userAvatar: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk_upload_20250108/29bdb9560340373bb3096394a845afa5.jpg',
messages: [],
newMessage: '',
voiceInputActive: false,
voiceInputActive: false, // 默认文字输入
recording: false,
recordingTime: 0,
recordingTimer: null,
showRecordingModal: false,
currentPlayingAudio: null,
innerAudioContext: null, // 音频播放上下文
hasRecordPermission: false,
scrollTop: 0,
scrollIntoView: '',
@@ -440,7 +245,6 @@ export default {
this.$nextTick(() => {
this.scrollToBottom();
setTimeout(() => { this.scrollToBottom(); }, 500);
});
uni.onKeyboardHeightChange(res => {
@@ -450,6 +254,9 @@ export default {
},
onUnload() {
if (this.innerAudioContext) {
this.innerAudioContext.destroy();
}
if (typeof ChatManager !== 'undefined') ChatManager.leaveRoom();
},
@@ -460,6 +267,7 @@ export default {
},
onHide() {
this.stopAudio(); // 页面隐藏时停止播放
uni.$off('new-room-message', this.handleNewRoomMessage);
uni.$off('load-room-message', this.handLoadMessageList);
uni.$off('no-more-history', this.handNoMessageList);
@@ -477,6 +285,75 @@ export default {
},
methods: {
// === 新增辅助方法 ===
isMsgMine(msg) {
return this.getSenderIdWithoutPrefix(msg.sender_user_id) === this.getCurrentUserIdWithoutPrefix();
},
getMsgAvatar(msg) {
if (this.isMsgMine(msg)) {
return this.userAvatar;
}
let avatar = this.doctorUserInfo.avatar || this.doctorAvatar || 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk_upload_20250108/29bdb9560340373bb3096394a845afa5.jpg';
try {
return decodeURIComponent(avatar);
} catch (e) {
return avatar;
}
},
// === 音频播放逻辑 ===
playAudio(msg) {
const audioUrl = getParsedContent(msg.message_type, msg.message_content);
// 如果点击的是当前正在播放的,则暂停/停止
if (this.currentPlayingAudio === msg.id) {
this.stopAudio();
return;
}
// 停止当前正在播放的
this.stopAudio();
if (!audioUrl) {
uni.showToast({ title: '无效的音频地址', icon: 'none' });
return;
}
// 初始化音频上下文
if (!this.innerAudioContext) {
this.innerAudioContext = uni.createInnerAudioContext();
// 监听自然播放结束
this.innerAudioContext.onEnded(() => {
this.currentPlayingAudio = null;
});
// 监听播放错误
this.innerAudioContext.onError((res) => {
console.error('音频播放错误', res);
this.currentPlayingAudio = null;
uni.showToast({ title: '播放失败', icon: 'none' });
});
}
this.innerAudioContext.src = audioUrl;
this.innerAudioContext.play();
this.currentPlayingAudio = msg.id;
},
stopAudio() {
if (this.innerAudioContext) {
this.innerAudioContext.stop();
}
this.currentPlayingAudio = null;
},
isPlaying(msg) {
return this.currentPlayingAudio === msg.id;
},
// ... 所有的业务逻辑方法保持原样,无需改动 ...
async fetchDoctorInfo(doctorId) {
try {
const cleanDoctorId = String(doctorId).replace('doctor-', '');
@@ -487,8 +364,6 @@ export default {
...this.doctorUserInfo,
nick_name: doctorInfo.nick_name || '医生',
avatar: doctorInfo.avatar || this.doctorAvatar,
department: doctorInfo.department || '',
title: doctorInfo.title || ''
};
if (doctorInfo.avatar) this.doctorAvatar = doctorInfo.avatar;
uni.setNavigationBarTitle({ title: `${this.doctorUserInfo.nick_name}医生的对话` });
@@ -542,9 +417,6 @@ export default {
onScroll(e) { this.currentScrollPosition = e.detail.scrollTop; },
getSenderIdWithoutPrefix(senderId) { return !senderId ? '' : String(senderId).replace(/^(user-|doctor-)/, ''); },
getCurrentUserIdWithoutPrefix() { return !this.currentUserId ? '' : String(this.currentUserId).replace(/^(user-|doctor-)/, ''); },
getParsedContent(t, c) { return getParsedContent(t, c); },
getMessageTypeName(t) { return getMessageTypeName(t); },
getMessageTypeClass(type) { return `type-${type}`; },
shouldShowTime(msg, index) {
if (index === 0) return true;
@@ -584,13 +456,6 @@ export default {
});
},
getChatRegisterInfo(item, index) {
const parsedContent = getParsedContent(item.message_type, item.message_content);
if (parsedContent._loaded || !parsedContent.id) return item;
this.fetchRegisterCardInfo(item, index, parsedContent.id);
return item;
},
handNoMessageList() { this.isEmpty = true; this.hasMore = false; },
handLoadMessageList(type = 0) {
this.loading = false;
@@ -730,15 +595,13 @@ export default {
};
this.addMessage(fullMessage);
this.scrollToBottom();
// 处理 sender_user_id确保只保留一个 user- 前缀
let senderUserId = `user-${this.currentUserId}`;
// 如果包含多个 user- 前缀(如 user-user-123则只保留一个
if (senderUserId.startsWith('user-user-')) {
senderUserId = senderUserId.replace(/^user-user-/, 'user-');
}
const requestData = {
room_id: this.doctorUserInfo.room_id,
sender_user_id: senderUserId, // 使用处理后的 sender_user_id
sender_user_id: senderUserId,
receiver_user_id: receiverUserId,
message_type: messageType,
message_content: message.message_content || message.content || '',
@@ -801,9 +664,6 @@ export default {
this.recorderManager.stop();
},
playAudio(msg) {}, // 需自行实现播放
isPlaying(msg) { return this.currentPlayingAudio === msg.id; },
async openMediaPicker(type) {
try { await this.checkPrivacyAuthorize(); } catch (e) { return; }
if (type === 'image') {
@@ -893,6 +753,9 @@ export default {
this.isLoadHistory = true;
this.loading = true;
if (typeof ChatManager !== 'undefined') ChatManager.loadHistory(this.doctorUserInfo.room_id);
setTimeout(() => {
this.loading = false;
}, 3000);
},
getStatusText(status) { const map = { 0: '待审核', 1: '已审核', 2: '未通过' }; return map[status] || '未知状态'; },
@@ -906,14 +769,9 @@ export default {
<style lang="scss" scoped>
// ================= 变量定义 =================
$primary-blue: #298dff;
$primary-light: #e6f2ff;
$text-main: #1a1a1a;
$text-secondary: #666666;
$text-placeholder: #999999;
$bg-page: #f2f4f7;
$primary-blue: #0A84FF;
$bg-page: #F4F6F9;
$bg-white: #ffffff;
$radius-bubble: 20rpx;
// ================= 基础容器 =================
.chat-container {
@@ -923,38 +781,38 @@ $radius-bubble: 20rpx;
background-color: $bg-page;
}
// ================= 部导航栏 =================
// ================= 部导航栏 =================
.chat-header {
background-color: rgba(255, 255, 255, 0.95);
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
z-index: 100;
position: relative;
box-shadow: 0 1rpx 2rpx rgba(0,0,0,0.05);
.header-main {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 32rpx;
border-bottom: 1rpx solid rgba(0,0,0,0.03);
.doctor-info-group {
display: flex;
flex-direction: column;
.doctor-name { font-size: 34rpx; font-weight: 600; color: $text-main; margin-bottom: 4rpx; }
.doctor-name { font-size: 34rpx; font-weight: 600; color: #333; margin-bottom: 4rpx; }
.doctor-badges {
display: flex;
align-items: center;
.status-dot { width: 12rpx; height: 12rpx; background-color: #52c41a; border-radius: 50%; margin-right: 8rpx; box-shadow: 0 0 0 2rpx rgba(82, 196, 26, 0.2); }
.sub-text { font-size: 24rpx; color: $text-secondary; }
.status-dot { width: 12rpx; height: 12rpx; background-color: #52c41a; border-radius: 50%; margin-right: 8rpx; }
.sub-text { font-size: 24rpx; color: #999; }
}
}
.header-action {
display: flex;
align-items: center;
padding: 12rpx 20rpx;
background: $primary-light;
padding: 10rpx 20rpx;
background: #F0F5FF;
border-radius: 32rpx;
.expand-tip { font-size: 24rpx; color: $primary-blue; margin-right: 6rpx; }
}
@@ -979,8 +837,10 @@ $radius-bubble: 20rpx;
display: flex;
flex-direction: column;
&.full-width { width: 100%; }
.label { font-size: 22rpx; color: $text-placeholder; margin-bottom: 8rpx; }
.value { font-size: 26rpx; color: $text-main; line-height: 1.4; }
.label { font-size: 22rpx; color: #999; margin-bottom: 8rpx; }
.value { font-size: 26rpx; color: #333; line-height: 1.4; }
.tag-value { color: $primary-blue; background: #e6f2ff; padding: 2rpx 8rpx; border-radius: 4rpx; display: inline-block; width: fit-content;}
.desc-value { color: #666; }
}
}
}
@@ -994,166 +854,68 @@ $radius-bubble: 20rpx;
.message-list-container {
padding: 30rpx 24rpx;
transition: padding-bottom 0.25s cubic-bezier(0.25, 0.8, 0.5, 1);
}
.empty-state {
text-align: center;
padding: 40rpx 0;
text { font-size: 24rpx; color: #ccc; background: rgba(0,0,0,0.03); padding: 8rpx 20rpx; border-radius: 20rpx; }
}
.empty-state { text-align: center; padding: 40rpx 0; .empty-text { font-size: 24rpx; color: #ccc; } }
.loading-spinner { display: flex; justify-content: center; align-items: center; padding: 20rpx; .spinner { width: 30rpx; height: 30rpx; border: 2rpx solid #ddd; border-top-color: #999; border-radius: 50%; animation: spin 0.8s linear infinite; } .spinner-text { font-size: 24rpx; color: #999; margin-left: 10rpx;} }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.time-divider { text-align: center; margin: 30rpx 0; font-size: 22rpx; color: #b0b0b0; }
// ================= 消息行 =================
.message-wrapper { margin-bottom: 40rpx; }
.chat-row {
display: flex;
align-items: flex-start;
max-width: 100%;
.avatar {
width: 80rpx; height: 80rpx;
border-radius: 50%;
border: 2rpx solid #fff;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
flex-shrink: 0;
}
.bubble-container { max-width: 72%; display: flex; flex-direction: column; }
&.other {
flex-direction: row;
.avatar { margin-right: 20rpx; }
.message-bubble {
background-color: $bg-white; color: $text-main;
border-radius: 4rpx $radius-bubble $radius-bubble $radius-bubble;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.03);
&.card-bubble { padding: 0; background: transparent; box-shadow: none; }
}
}
&.mine {
flex-direction: row-reverse;
.avatar { margin-left: 20rpx; }
.message-bubble {
background-color: $primary-blue; color: #fff;
border-radius: $radius-bubble 4rpx $radius-bubble $radius-bubble;
box-shadow: 0 4rpx 12rpx rgba(41, 141, 255, 0.25);
&.card-bubble { background: transparent; box-shadow: none; }
}
}
}
.message-bubble {
padding: 20rpx 28rpx;
font-size: 30rpx;
line-height: 1.6;
position: relative;
word-break: break-all;
&.type-1, &.type-3, &.type-4, &.type-9, &.type-10, &.type-11, &.type-12, &.type-13 {
padding: 0; overflow: visible; background: transparent; box-shadow: none; border: none; border-radius: 0;
}
.media-content {
border-radius: 16rpx; max-width: 100%;
&.image { max-width: 320rpx; }
&.video { width: 320rpx; height: 180rpx; }
}
}
.audio-content {
display: flex; align-items: center; min-width: 120rpx; gap: 12rpx;
&.mine-audio { flex-direction: row-reverse; }
.duration { font-weight: bold; font-size: 28rpx; }
.wifi-symbol { width: 30rpx; height: 30rpx; }
}
// ================= 卡片系统 =================
.card-bubble {
background-color: $bg-white !important;
border-radius: 24rpx !important;
width: 500rpx;
overflow: hidden;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06) !important;
border: 1rpx solid rgba(0,0,0,0.02);
color: $text-main !important;
.card-header {
padding: 24rpx; border-bottom: 1rpx solid #f5f5f5; display: flex; justify-content: space-between; align-items: center;
.title-group { display: flex; align-items: center; gap: 12rpx; .card-title { font-size: 30rpx; font-weight: 600; color: $text-main; } }
.status-tag {
font-size: 22rpx; padding: 4rpx 16rpx; border-radius: 8rpx; background: #f0f0f0; color: #666;
&.status-1, &.blue { background: rgba(41, 141, 255, 0.1); color: $primary-blue; }
&.status-0 { background: rgba(255, 153, 0, 0.1); color: #ff9900; }
}
}
.card-body {
padding: 24rpx; display: flex; flex-direction: column; gap: 16rpx;
.info-row {
display: flex; justify-content: space-between; align-items: baseline; font-size: 26rpx;
.label { color: $text-secondary; }
.value { color: $text-main; text-align: right; max-width: 70%; }
.price { color: #ff4d4f; font-weight: bold; font-size: 30rpx; }
.small { font-size: 24rpx; color: #999; }
}
.text-block {
background: #f9f9f9; padding: 16rpx; border-radius: 12rpx;
.block-label { display: block; font-size: 22rpx; color: #999; margin-bottom: 8rpx; }
.block-content { font-size: 26rpx; color: #333; line-height: 1.5; }
}
}
.card-footer {
padding: 20rpx 24rpx; border-top: 1rpx solid #f5f5f5;
.action-btn {
background: #fff; border: 1rpx solid #eee; color: $text-secondary; font-size: 26rpx; border-radius: 40rpx; line-height: 2.2;
&.primary { background: $primary-blue; color: #fff; border: none; box-shadow: 0 4rpx 12rpx rgba(41, 141, 255, 0.2); }
&:active { opacity: 0.8; }
}
&.split { display: flex; justify-content: space-between; align-items: center; .link-btn { color: $primary-blue; font-size: 26rpx; padding: 10rpx; } }
}
}
.time-divider { text-align: center; margin: 30rpx 0; .time-text { font-size: 22rpx; color: #aaa; background: rgba(0,0,0,0.03); padding: 4rpx 16rpx; border-radius: 8rpx; } }
// ================= 底部输入 =================
.chat-footer {
background-color: #fff;
border-top: 1rpx solid #eee;
background-color: #F7F7F7; // 微信风格浅灰背景
border-top: 1rpx solid rgba(0,0,0,0.05);
padding: 16rpx 24rpx 40rpx 24rpx;
position: fixed;
bottom: 0; left: 0; right: 0; z-index: 99;
transition: bottom 0.25s cubic-bezier(0.25, 0.8, 0.5, 1);
transition: bottom 0.2s ease-out;
.consultation-ended-notice { display: flex; align-items: center; justify-content: center; padding: 30rpx; color: #999; font-size: 28rpx; gap: 12rpx; }
.toolbar-area {
margin-bottom: 20rpx;
.action-grid {
display: flex; gap: 40rpx; padding-left: 12rpx;
.action-item {
display: flex; flex-direction: column; align-items: center; gap: 8rpx;
.icon-circle { width: 80rpx; height: 80rpx; background: #f7f8fa; border-radius: 24rpx; display: flex; align-items: center; justify-content: center; }
text { font-size: 22rpx; color: #666; }
}
}
}
.input-bar {
display: flex; align-items: flex-end; gap: 20rpx;
display: flex; align-items: flex-end; gap: 16rpx;
// 图标按钮
.icon-btn {
width: 72rpx; height: 72rpx;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
&:active { opacity: 0.6; }
}
// 输入框容器
.input-field-wrapper {
flex: 1; background: #f2f4f7; border-radius: 40rpx; min-height: 80rpx; display: flex; align-items: center; padding: 0 8rpx;
.main-input { flex: 1; height: 80rpx; padding: 0 24rpx; font-size: 30rpx; color: #333; }
flex: 1;
min-height: 72rpx;
display: flex; align-items: center;
.main-input {
width: 100%; height: 72rpx;
padding: 0 20rpx;
font-size: 30rpx; color: #333;
background: #fff;
border-radius: 12rpx;
}
.voice-btn {
flex: 1; height: 72rpx; margin: 4rpx; background: #fff; border-radius: 36rpx; display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: #333; font-weight: 500; box-shadow: 0 2rpx 4rpx rgba(0,0,0,0.05);
&.recording { background: #ffecec; color: #ff4d4f; }
width: 100%; height: 72rpx;
background: #fff; border-radius: 12rpx;
display: flex; align-items: center; justify-content: center;
font-size: 30rpx; color: #333; font-weight: 500;
border: 1rpx solid rgba(0,0,0,0.05);
&.recording { background: #f0f0f0; color: #666; }
}
}
.send-btn-wrapper .send-btn {
width: 80rpx; height: 80rpx; background: $primary-blue; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 4rpx 12rpx rgba(41, 141, 255, 0.3); transition: transform 0.1s;
&:active { transform: scale(0.9); }
// 右侧发送/加号
.right-action {
height: 72rpx; display: flex; align-items: center; justify-content: center;
.send-btn {
background: $primary-blue; color: #fff;
font-size: 26rpx; padding: 0 24rpx; height: 60rpx; line-height: 60rpx;
border-radius: 8rpx;
}
}
}
}
@@ -1162,19 +924,9 @@ $radius-bubble: 20rpx;
position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); z-index: 999; display: flex; align-items: center; justify-content: center;
.recording-box {
width: 300rpx; height: 300rpx; background: rgba(0,0,0,0.8); border-radius: 32rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #fff; gap: 20rpx;
.wave-animation { width: 100rpx; height: 4rpx; background: #298dff; border-radius: 20rpx; }
.wave-animation { width: 100rpx; height: 4rpx; background: $primary-blue; border-radius: 20rpx; }
.timer { font-size: 32rpx; font-weight: 600; }
.tip { font-size: 24rpx; color: #ccc; }
}
}
.loading-spinner { display: flex; justify-content: center; align-items: center; padding: 20rpx; }
.spinner { width: 32rpx; height: 32rpx; border: 3rpx solid #e5e5e5; border-top-color: #999; border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.system-message-card {
background: #fffbe6; border: 1px solid #ffe58f; border-radius: 12rpx; padding: 20rpx; margin: 20rpx auto; max-width: 80%;
.price-update-notice { display: flex; align-items: center; gap: 10rpx; margin-bottom: 10rpx; .notice-text { color: #d48806; font-size: 28rpx; font-weight: 500; } }
.price-detail { display: flex; flex-direction: column; gap: 6rpx; font-size: 24rpx; color: #666; padding-left: 28rpx; }
}
.system-message { text-align: center; color: #999; font-size: 24rpx; padding: 10rpx 0; }
</style>

View File

@@ -0,0 +1,375 @@
<template>
<view class="bubble-container" :class="{ 'mine': isMine, 'other': !isMine }">
<!-- 头像 -->
<image class="avatar" :src="avatar" mode="aspectFill"></image>
<!-- 消息内容包裹 -->
<view class="content-wrapper">
<!-- 文本/语音/视频/图片 气泡 -->
<!-- 注意类型 9 如果是 price_update 特殊样式不使用普通气泡背景所以单独处理 -->
<view class="message-bubble"
:class="['type-' + msg.message_type, { 'no-padding': isMediaOrCard || isSystemPriceUpdate }]"
>
<!-- 0. 文本 -->
<text v-if="msg.message_type === 0" class="text-content" user-select>{{ parsedContent }}</text>
<!-- 1. 图片 -->
<image v-else-if="msg.message_type === 1"
:src="parsedContent"
mode="widthFix"
class="media-content image"
@click="$emit('previewImage', parsedContent)">
</image>
<!-- 2. 语音 -->
<view v-else-if="msg.message_type === 2" class="audio-content" @click="$emit('playAudio', msg)">
<block v-if="isMine">
<text class="duration">{{ msg.duration }}''</text>
<view class="wifi-symbol mine">
<u-icon name="volume-fill" size="18" color="#fff"></u-icon>
</view>
</block>
<block v-else>
<view class="wifi-symbol other" :class="{'active': isPlaying}">
<u-icon name="volume" size="18" color="#333"></u-icon>
</view>
<text class="duration">{{ msg.duration }}''</text>
</block>
</view>
<!-- 3. 视频 -->
<video v-else-if="msg.message_type === 3"
:src="parsedContent"
controls
class="media-content video">
</video>
<!-- 9. 系统消息 (特殊处理) -->
<block v-else-if="msg.message_type === 9">
<!-- 情况A: 价格变动通知 (卡片样式) -->
<view v-if="parsedContent.type === 'price_update'" class="system-message-card">
<view class="price-update-notice">
<u-icon name="info-circle-fill" size="18" color="#fa8c16"></u-icon>
<text class="notice-text">{{ parsedContent.message }}</text>
</view>
<view class="price-detail">
<text>订单号{{ parsedContent.order_no }}</text>
<text>应付金额¥{{ parsedContent.total_pay_price }}</text>
</view>
</view>
<!-- 情况B: 普通系统文本 -->
<view v-else class="system-inner">
<text>{{ msg.message_content }}</text>
</view>
</block>
<!-- 卡片类消息 (4, 10, 11, 12, 13) -->
<view v-else-if="isCardType" class="card-content">
<!-- 类型10: 挂号 -->
<block v-if="msg.message_type === 10">
<view class="card-header bg-green">
<view class="title-group"><u-icon name="calendar-fill" size="16" color="#059669"></u-icon><text class="card-title">预约挂号</text></view>
<text class="status-tag" :class="'status-' + parsedContent.status">{{ getRegisterStatusText(parsedContent.status) }}</text>
</view>
<view class="card-body">
<view class="info-row"><text class="label">订单号</text><text class="value order-font">{{ parsedContent.order_no }}</text></view>
<view class="info-row"><text class="label">就诊人</text><text class="value">{{ (parsedContent.user_patient && parsedContent.user_patient.name) || '未知' }}</text></view>
<view class="info-row highlight"><text class="label">费用</text><text class="value price">¥{{ parsedContent.price }}</text></view>
</view>
<view class="card-footer"><button class="action-btn" @click="$emit('viewRegister', parsedContent.id)">查看详情</button></view>
</block>
<!-- 类型11: 患者就诊经历 -->
<block v-else-if="msg.message_type === 11">
<view class="card-header bg-gray">
<view class="title-group"><u-icon name="file-text-fill" size="16" color="#64748B"></u-icon><text class="card-title">患者就诊经历</text></view>
</view>
<view class="card-body">
<!-- 补全: 药品名称 -->
<view class="info-row" v-if="parsedContent.drug_name">
<text class="label">药品名称</text>
<text class="value">{{ parsedContent.drug_name }}</text>
</view>
<!-- 症状描述 -->
<view class="text-block" v-if="parsedContent.illness_info">
<text class="block-label">症状描述</text>
<text class="block-content">{{ parsedContent.illness_info }}</text>
</view>
<!-- 补全: 详细状态 -->
<view class="info-row"><text class="label">是否就诊过</text><text class="value">{{ parsedContent.has_visited === '1' ? '是' : '否' }}</text></view>
<view class="info-row"><text class="label">是否使用过药品</text><text class="value">{{ parsedContent.has_used_drug === '1' ? '是' : '否' }}</text></view>
</view>
</block>
<!-- 类型4: 电子处方 -->
<block v-else-if="msg.message_type === 4">
<view class="card-header bg-blue">
<view class="title-group"><u-icon name="order" size="16" color="#0A84FF"></u-icon><text class="card-title">电子处方</text></view>
<text class="status-tag blue">{{ getStatusText(parsedContent.status) }}</text>
</view>
<view class="card-body">
<view class="info-row"><text class="label">诊断单号</text><text class="value small">{{ parsedContent.order_no }}</text></view>
<view class="info-row"><text class="label">金额</text><text class="value price">¥{{ parsedContent.total_pay_price }}</text></view>
</view>
<view class="card-footer split">
<text class="link-btn" @click="$emit('viewPrescription', parsedContent.id, parsedContent.order_no)">详情</text>
<button class="action-btn primary" @click="$emit('goToOrderMedicine', parsedContent.order_id, parsedContent.order_no)">一键购药</button>
</view>
</block>
<!-- 类型12: 商品 -->
<block v-else-if="msg.message_type === 12">
<view class="card-header bg-blue">
<view class="title-group"><u-icon name="shopping-cart-fill" size="16" color="#0A84FF"></u-icon><text class="card-title">推荐商品</text></view>
</view>
<view class="card-body">
<view class="info-row"><text class="label">名称</text><text class="value">{{ parsedContent.product_name }}</text></view>
<view class="info-row"><text class="label">价格</text><text class="value price">¥{{ parsedContent.price }}</text></view>
</view>
</block>
<!-- 类型13: 结束问诊 -->
<block v-else-if="msg.message_type === 13">
<view class="card-header bg-green">
<view class="title-group"><u-icon name="checkmark-circle-fill" size="16" color="#059669"></u-icon><text class="card-title">问诊结束</text></view>
</view>
<view class="card-body">
<text class="desc-text">{{ parsedContent.reason || '本次服务已完成' }}</text>
</view>
</block>
<block v-else>
<view class="card-body"><text>不支持的消息类型</text></view>
</block>
</view>
<!-- 其他未知类型默认显示文本 -->
<text v-else class="text-content">{{ parsedContent }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: "MessageBubble",
props: {
msg: {
type: Object,
required: true
},
isMine: {
type: Boolean,
default: false
},
avatar: {
type: String,
default: ''
},
isPlaying: {
type: Boolean,
default: false
}
},
computed: {
// 纯文本、图片、语音、视频以外的特殊类型,不需要默认的气泡背景/Padding
isMediaOrCard() {
// 1:图片, 3:视频, 4,10,11,12,13: 卡片 (9单独处理)
return [1, 3, 4, 10, 11, 12, 13].includes(this.msg.message_type);
},
// 是否是系统消息且为价格变动(需要独立卡片样式)
isSystemPriceUpdate() {
return this.msg.message_type === 9 && this.parsedContent.type === 'price_update';
},
isCardType() {
return [4, 10, 11, 12, 13].includes(this.msg.message_type);
},
parsedContent() {
const { message_type, message_content } = this.msg;
// 包含9(System)在内的复杂类型尝试解析 JSON
if ([4, 9, 10, 11, 12, 13].includes(message_type)) {
try {
return typeof message_content === 'object' ? message_content : JSON.parse(message_content);
} catch (e) {
return message_content;
}
}
return message_content;
}
},
methods: {
getStatusText(status) { const map = { 0: '待审核', 1: '已审核', 2: '未通过' }; return map[status] || '状态未知'; },
getRegisterStatusText(status) { const map = { 0: '待就诊', 1: '已缴费', 2: '已就诊', 3: '已取消', 4: '已退费' }; return map[status] || '状态未知'; },
}
}
</script>
<style lang="scss" scoped>
// 变量
$primary: #0A84FF;
$bg-mine: #0A84FF;
$bg-other: #FFFFFF;
$text-main: #333333;
$text-sub: #666666;
$radius-bubble: 12rpx;
$max-width: 500rpx;
.bubble-container {
display: flex;
align-items: flex-start;
margin-bottom: 30rpx;
width: 100%;
.avatar {
width: 72rpx; height: 72rpx;
border-radius: 10rpx;
flex-shrink: 0;
background: #f2f2f2;
}
.content-wrapper {
display: flex;
flex-direction: column;
max-width: 72%; // 稍微放宽一点
}
&.other {
flex-direction: row;
.avatar { margin-right: 16rpx; }
.message-bubble {
background-color: $bg-other;
color: $text-main;
border-radius: 4rpx $radius-bubble $radius-bubble $radius-bubble;
// 添加小箭头
&::before {
content: ''; position: absolute; top: 20rpx; left: -10rpx;
width: 0; height: 0;
border-top: 10rpx solid transparent;
border-bottom: 10rpx solid transparent;
border-right: 12rpx solid $bg-other;
}
// 卡片或特殊系统消息不需要箭头
&.no-padding::before { display: none; }
}
}
&.mine {
flex-direction: row-reverse;
.avatar { margin-left: 16rpx; }
.message-bubble {
background-color: $bg-mine;
color: #fff;
border-radius: $radius-bubble 4rpx $radius-bubble $radius-bubble;
&::after {
content: ''; position: absolute; top: 20rpx; right: -10rpx;
width: 0; height: 0;
border-top: 10rpx solid transparent;
border-bottom: 10rpx solid transparent;
border-left: 12rpx solid $bg-mine;
}
&.no-padding::after { display: none; }
}
}
}
.message-bubble {
padding: 18rpx 24rpx;
font-size: 30rpx;
line-height: 1.5;
position: relative;
word-break: break-all;
box-shadow: 0 2rpx 4rpx rgba(0,0,0,0.05);
min-height: 72rpx;
display: flex; align-items: center;
&.no-padding {
padding: 0;
background: transparent;
box-shadow: none;
&::before, &::after { display: none; }
}
}
// 图片/视频
.media-content {
border-radius: 8rpx;
&.image { max-width: 300rpx; display: block; }
&.video { width: 300rpx; height: 170rpx; }
}
// 语音
.audio-content {
display: flex; align-items: center; gap: 12rpx; min-width: 100rpx;
.duration { font-size: 28rpx; }
}
// ========== 卡片样式 ==========
.card-content {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
border: 1rpx solid #eee;
width: 480rpx; // 卡片定宽
.card-header {
padding: 16rpx 20rpx;
display: flex; justify-content: space-between; align-items: center;
border-bottom: 1rpx solid #f9f9f9;
&.bg-green { background: #F0FDF4; }
&.bg-blue { background: #EFF6FF; }
&.bg-gray { background: #F8FAFC; }
.title-group { display: flex; align-items: center; gap: 8rpx; .card-title { font-size: 26rpx; font-weight: 600; color: #333; } }
.status-tag { font-size: 20rpx; padding: 2rpx 10rpx; border-radius: 6rpx; background: #fff; color: $text-sub; border: 1rpx solid rgba(0,0,0,0.05); }
}
.card-body {
padding: 20rpx; display: flex; flex-direction: column; gap: 12rpx;
.info-row {
display: flex; justify-content: space-between; font-size: 24rpx;
.label { color: #888; }
.value { color: #333; font-weight: 500; text-align: right; max-width: 70%; }
.order-font { font-family: Courier, monospace; letter-spacing: 0.5rpx; }
.price { color: #ef4444; font-size: 28rpx; font-weight: bold; }
.small { font-size: 22rpx; color: #999; }
}
.desc-text { font-size: 26rpx; color: #333; }
.text-block {
background: #f5f5f5; padding: 16rpx; border-radius: 8rpx;
.block-label { display: block; font-size: 22rpx; color: #999; margin-bottom: 6rpx; }
.block-content { font-size: 26rpx; color: #333; line-height: 1.4; }
}
}
.card-footer {
padding: 16rpx 20rpx; border-top: 1rpx solid #f0f0f0;
.action-btn {
width: 100%; height: 60rpx; line-height: 60rpx; font-size: 26rpx; border-radius: 30rpx; background: #fff; border: 1rpx solid #ddd; color: #555;
&.primary { background: $primary; color: #fff; border: none; }
&::after { border: none; }
}
&.split { display: flex; justify-content: space-between; align-items: center; .link-btn { font-size: 24rpx; color: $primary; padding: 10rpx; } .action-btn { width: 160rpx; } }
}
}
// 系统消息样式
.system-message-card {
background: #FFFBEB; border: 1rpx solid #FEF3C7; border-radius: 16rpx; padding: 20rpx; width: 480rpx;
.price-update-notice { display: flex; align-items: center; gap: 10rpx; margin-bottom: 10rpx; .notice-text { color: #D97706; font-size: 26rpx; font-weight: 600; } }
.price-detail { display: flex; flex-direction: column; gap: 6rpx; font-size: 22rpx; color: #78350F; padding-left: 46rpx; }
}
.system-inner {
background: rgba(0,0,0,0.05); padding: 8rpx 20rpx; border-radius: 10rpx; font-size: 22rpx; color: #999; align-self: center; margin: 0 auto;
}
</style>

View File

@@ -200,11 +200,17 @@ export default {
order_no: "", // 订单编号
p_id: "", // 处方id
orderType: 0, // 处方id
originalList: [{
text: '快递到家'
}, {
text: '到店自取'
}],
list: [{
text: '快递到家'
}, {
text: '到店自取'
}],
is_online: 0, // 是否在线复诊 0-否 非0-是
toshowed: false,
tips: {
text: '配送方式',
@@ -315,32 +321,65 @@ export default {
this.toshowed = true
},
chooseway(index) {
this.delivery_method = index
// 如果选择到店自取index == 1检查 is_online
if (index == 1 && this.is_online != 0) {
// 阻止选择并提示
this.$refs.uToast.show({
title: '在线复诊订单不支持到店自取,请选择收货地址',
type: 'default',
icon: false
})
return
}
// 先调用接口更新,成功后再更新页面状态
if (index == 1) {
this.isshowed = true
this.isshow = false
this.toUpdata()
// 到店自取:先调用接口,成功后再更新页面
this.toUpdata(index)
} else {
// 快递到家:跳转到地址选择页面
this.isshowed = false
this.isshow = true
this.toChoose()
}
},
// 更新配送方式
toUpdata() {
toUpdata(deliveryMethodIndex) {
if (!this.order_id) return
// 如果传入了参数,使用传入的值,否则使用当前的 delivery_method
const targetDeliveryMethod = deliveryMethodIndex !== undefined ? deliveryMethodIndex : this.delivery_method
getDelivery({
method: "post",
data: {
address_id: uni.getStorageSync('address_id'),
delivery_method: this.delivery_method,
delivery_method: targetDeliveryMethod,
order_id: this.order_id,
store_id: uni.getStorageSync('store_id') || '11001'
}
}).then((res) => {
if (res.data.errcode == 0) {
if (res.data.errcode === 0) {
// 接口成功后再更新页面状态
if (deliveryMethodIndex !== undefined) {
this.delivery_method = deliveryMethodIndex
if (deliveryMethodIndex == 1) {
this.isshowed = true
this.isshow = false
} else {
this.isshowed = false
this.isshow = true
}
}
// 刷新订单信息
this.getInfo()
} else {
// 错误处理:拒绝更新并显示错误信息,不更新页面状态
this.$refs.uToast.show({
title: res.data.msg || '更新失败',
type: 'default',
icon: false
})
}
})
},
@@ -377,12 +416,34 @@ export default {
uni.setStorageSync('userWay_order_id', res.data.data.ProductOrder.id)
uni.setStorageSync('delivery_method', res.data.data.ProductOrder.delivery_method)
if (res.data.data.ProductOrder.delivery_method == 1) {
this.isshowed = true
this.isshow = false
// 检查 is_online 字段,动态控制 list 数组
if (res.data.data.ProductOrder && res.data.data.ProductOrder.is_online !== undefined) {
this.is_online = res.data.data.ProductOrder.is_online
} else if (res.data.data.is_online !== undefined) {
this.is_online = res.data.data.is_online
}
// 如果 is_online !== 0则不显示到店自取选项
if (this.is_online != 0) {
this.list = [{
text: '快递到家'
}]
} else {
this.isshowed = false
this.isshow = true
// 如果 is_online === 0显示所有选项
this.list = [...this.originalList]
}
// 根据返回的订单数据更新页面状态(配送方式)
if (res.data.data.ProductOrder && res.data.data.ProductOrder.delivery_method !== undefined) {
const deliveryMethod = res.data.data.ProductOrder.delivery_method
this.delivery_method = deliveryMethod
if (deliveryMethod == 1) {
this.isshowed = true
this.isshow = false
} else {
this.isshowed = false
this.isshow = true
}
}
// 查询处方状态

View File

@@ -202,11 +202,17 @@
order_id: "", // 订单id
order_no: "", // 订单编号
p_id: "", // 处方id
originalList: [{
text: '快递到家'
}, {
text: '到店自取'
}],
list: [{
text: '快递到家'
}, {
text: '到店自取'
}],
is_online: 0, // 是否在线复诊 0-否 非0-是
toshowed: false,
tips: {
text: '配送方式',
@@ -264,14 +270,23 @@
this.toshowed = true
},
chooseway(index) {
this.wayText = this.list[index].text
this.delivery_method = index
// 如果选择到店自取index == 1检查 is_online
if (index == 1 && this.is_online != 0) {
// 阻止选择并提示
this.$refs.uToast.show({
title: '在线复诊订单不支持到店自取,请选择收货地址',
type: 'default',
icon: false
})
return
}
// 先调用接口更新,成功后再更新页面状态
if (index == 1) {
this.isshowed = true
this.isshow = false
this.toUpdata()
// 到店自取:先调用接口,成功后再更新页面
this.toUpdata(index)
} else {
// 快递到家:跳转到地址选择页面
this.isshowed = false
this.isshow = true
this.toChoose()
@@ -279,19 +294,45 @@
// console.log(`点击了第${index + 1}项,内容为:${this.list[index].text}`)
},
// 更新配送方式
toUpdata() {
toUpdata(deliveryMethodIndex) {
// 如果传入了参数,使用传入的值,否则使用当前的 delivery_method
const targetDeliveryMethod = deliveryMethodIndex !== undefined ? deliveryMethodIndex : this.delivery_method
if (this.order_id == '') {
return ;
}
getDelivery({
method: "post",
data: {
address_id: uni.getStorageSync('address_id'),
delivery_method: this.delivery_method,
delivery_method: targetDeliveryMethod,
order_id: this.order_id,
store_id: uni.getStorageSync('store_id') || '11001'
}
}).then((res) => {
if (res.data.errcode == 0) {
if (res.data.errcode === 0) {
// 接口成功后再更新页面状态
if (deliveryMethodIndex !== undefined) {
this.wayText = this.list[deliveryMethodIndex].text
this.delivery_method = deliveryMethodIndex
if (deliveryMethodIndex == 1) {
this.isshowed = true
this.isshow = false
} else {
this.isshowed = false
this.isshow = true
}
}
// 刷新订单信息
this.getInfo()
// console.log(res, '确定代煎');
} else {
// 错误处理:拒绝更新并显示错误信息,不更新页面状态
this.$refs.uToast.show({
title: res.data.msg || '更新失败',
type: 'default',
icon: false
})
}
})
},
@@ -333,6 +374,39 @@
uni.setStorageSync('userWay_order_id', res.data.data.ProductOrder.id)
// this.order_no = res.data.data.ProductOrder.order_no
// console.log(this.west.content);
// 检查 is_online 字段,动态控制 list 数组
if (res.data.data.ProductOrder && res.data.data.ProductOrder.is_online !== undefined) {
this.is_online = res.data.data.ProductOrder.is_online
} else if (res.data.data.is_online !== undefined) {
this.is_online = res.data.data.is_online
}
// 如果 is_online !== 0则不显示到店自取选项
if (this.is_online != 0) {
this.list = [{
text: '快递到家'
}]
} else {
// 如果 is_online === 0显示所有选项
this.list = [...this.originalList]
}
// 根据返回的订单数据更新页面状态(配送方式)
if (res.data.data.ProductOrder && res.data.data.ProductOrder.delivery_method !== undefined) {
const deliveryMethod = res.data.data.ProductOrder.delivery_method
this.delivery_method = deliveryMethod
if (this.list[deliveryMethod]) {
this.wayText = this.list[deliveryMethod].text
}
if (deliveryMethod == 1) {
this.isshowed = true
this.isshow = false
} else {
this.isshowed = false
this.isshow = true
}
}
}
})
},

View File

@@ -413,6 +413,16 @@ export default {
},
quantityChange(value) {},
confirmAddToCart() {
// 检查是否为处方药is_otc === 0 && type == 2
if (this.currentProduct.drug && this.currentProduct.drug.is_otc === 0 && this.currentProduct.drug.type == 2) {
uni.showToast({
title: '处方药不能加入购物车,请通过提交预定购买',
icon: 'none',
duration: 2000
});
return;
}
saveCartApi({
store_id: uni.getStorageSync('store_id') || '11001',
drug_id: this.currentProduct.drug.id,

View File

@@ -484,6 +484,16 @@ export default {
})
},
addToCart() {
// 检查是否为处方药is_otc === 0 && type == 2
if (this.productDetail.is_otc === 0 && this.productDetail.type == 2) {
uni.showToast({
title: '处方药不能加入购物车,请通过提交预定购买',
icon: 'none',
duration: 2000
});
return;
}
saveCartApi({
store_id: uni.getStorageSync('store_id') || '11001',
drug_id: this.productDetail.id,