375 lines
14 KiB
Vue
375 lines
14 KiB
Vue
<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> |