Files
xk-doctor-wx/subPackages/sub_online_reception/components/MessageBubble.vue

1054 lines
28 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="bubble-container" :class="{ mine: isMine, other: !isMine }">
<image class="avatar" :src="avatar" mode="aspectFill"></image>
<view class="content-wrapper">
<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>{{ parsedText }}</text>
<!-- 1. 图片 -->
<image
v-else-if="msg.message_type === 1"
:src="parsedText"
mode="widthFix"
class="media-content image"
@click="$emit('previewImage', parsedText)"
></image>
<!-- 2. 语音 -->
<view
v-else-if="msg.message_type === 2"
class="audio-content"
:style="{ width: getAudioWidth(msg.duration) }"
@click="$emit('playAudio', msg)"
>
<block v-if="isMine">
<text class="duration">{{ msg.duration }}''</text>
<view class="voice-icon-wrap mine" :class="{ playing: isPlaying }">
<u-icon name="volume-fill" size="20" color="#fff"></u-icon>
</view>
</block>
<block v-else>
<view class="voice-icon-wrap other" :class="{ playing: isPlaying }">
<u-icon name="volume-fill" size="20" color="#333"></u-icon>
</view>
<text class="duration">{{ msg.duration }}''</text>
</block>
</view>
<!-- 3. 视频 -->
<video
v-else-if="msg.message_type === 3"
:src="videoSrc"
controls
class="media-content video"
></video>
<!-- 9. 系统消息 -->
<block v-else-if="msg.message_type === 9">
<view
v-if="parsedObj && (parsedObj.type === 'price_update' || parsedObj.type === 'price_adjust')"
class="service-notify-card"
>
<view class="sn-header">
<text class="sn-title">{{ parsedObj.type === 'price_update' ? '订单改价通知' : '费用调整提醒' }}</text>
</view>
<view class="sn-body">
<block v-if="parsedObj.type === 'price_update'">
<view class="sn-row">
<text class="sn-label">变动说明</text>
<text class="sn-value">{{ parsedObj.message }}</text>
</view>
<view class="sn-row">
<text class="sn-label">最新应付</text>
<text class="sn-value price">¥{{ parsedObj.total_pay_price }}</text>
</view>
<view class="sn-row">
<text class="sn-label">订单编号</text>
<text class="sn-value sub">{{ parsedObj.order_no }}</text>
</view>
</block>
<block v-else-if="parsedObj.type === 'price_adjust'">
<view class="sn-row">
<text class="sn-label">调整项目</text>
<text class="sn-value truncate">{{ parsedObj.drug_name || '医疗服务' }}</text>
</view>
<view class="sn-row">
<text class="sn-label">价格变动</text>
<view class="sn-value-group">
<text class="old">¥{{ parsedObj.old_price }}</text>
<u-icon name="arrow-right" size="10" color="#999" style="margin: 0 4rpx"></u-icon>
<text class="new">¥{{ parsedObj.new_price }}</text>
</view>
</view>
<view class="sn-row">
<text class="sn-label">变动金额</text>
<text class="sn-value" :class="parsedObj.adjust_amount >= 0 ? 'color-orange' : 'color-green'">
{{ parsedObj.adjust_amount >= 0 ? '增加' : '减少' }} ¥{{ Math.abs(parsedObj.adjust_amount) }}
</text>
</view>
<view class="sn-row">
<text class="sn-label">当前总额</text>
<text class="sn-value strong">¥{{ parsedObj.total_pay_price }}</text>
</view>
</block>
</view>
</view>
<view v-else class="system-inner">
<text>{{ systemNinePlainText }}</text>
</view>
</block>
<!-- 卡片类 4,10,11,12,13,14 -->
<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-' + parsedObj.status">{{ getRegisterStatusText(parsedObj.status) }}</text>
</view>
<view class="card-body">
<view class="info-row">
<text class="label">订单号</text>
<text class="value order-font">{{ parsedObj.order_no }}</text>
</view>
<view class="info-row">
<text class="label">就诊人</text>
<text class="value">{{ (parsedObj.user_patient && parsedObj.user_patient.name) || '未知' }}</text>
</view>
<view class="info-row highlight">
<text class="label">费用</text>
<text class="value price">¥{{ parsedObj.price }}</text>
</view>
<view v-if="parsedObj.chief_complaint" class="text-block">
<text class="block-label">主诉</text>
<text class="block-content">{{ parsedObj.chief_complaint }}</text>
</view>
<view v-if="parsedObj.illnessInfo" class="text-block">
<text class="block-label">适用症/症状</text>
<text class="block-content">{{ parsedObj.illnessInfo }}</text>
</view>
<view v-if="parsedObj.illnessInfo" class="info-row">
<text class="label">是否就诊过</text>
<text class="value">{{ parsedObj.has_visited === 1 || parsedObj.has_visited === '1' ? '是' : '否' }}</text>
</view>
<view v-if="parsedObj.illnessInfo" class="info-row">
<text class="label">是否使用过药品</text>
<text class="value">{{ parsedObj.has_used_drug === 1 || parsedObj.has_used_drug === '1' ? '是' : '否' }}</text>
</view>
<view v-if="registerCardDrugRows.length" class="drug-list-block">
<text class="block-label">所选药品</text>
<view v-for="row in registerCardDrugRows" :key="row._wxKey" class="info-row drug-spec-row">
<image
v-if="row.image"
class="drug-thumb"
:src="row.image"
mode="aspectFill"
@click.stop="$emit('previewImage', row.image)"
/>
<view class="drug-name-wrap">
<text class="label">{{ row.name || '药品' }}</text>
<text v-if="row.specification" class="spec-text">{{ row.specification }}</text>
</view>
<text class="value">×{{ row.quantity }}</text>
</view>
</view>
</view>
<view
v-if="registerCardDrugRows.length && canAddExperienceToRx && !isMine"
class="card-footer card-footer--compact"
>
<button
class="action-btn action-btn--ghost"
@click.stop="$emit('addRegisterDrugsToRx')"
>
添加到处方单
</button>
</view>
</block>
<!-- 11 复诊用药 -->
<block v-else-if="msg.message_type === 11 && parsedObj && parsedObj.flow === 'follow_up_drug'">
<view class="card-header bg-gray">
<view class="title-group">
<u-icon name="question-circle-fill" size="16" color="#64748B"></u-icon>
<text class="card-title">医生助理</text>
</view>
</view>
<view class="card-body">
<view v-if="parsedObj.question" class="text-block">
<text class="block-content">{{ parsedObj.question }}</text>
</view>
<view v-for="row in followUpDrugs" :key="row._wxKey" class="info-row drug-spec-row">
<view class="drug-name-wrap">
<text class="label">{{ row.name || '药品' }}</text>
<text v-if="row.specification" class="spec-text">{{ row.specification }}</text>
</view>
<text class="value">×{{ row.quantity != null ? row.quantity : 1 }}</text>
</view>
<view v-if="followUpAnsweredLabel" class="info-row">
<text class="label">患者选择</text>
<text class="value">{{ followUpAnsweredLabel }}</text>
</view>
<view
v-if="parsedObj.has_visited === 1 || parsedObj.has_visited === '1' || parsedObj.has_visited === 0 || parsedObj.has_visited === '0'"
class="info-row"
>
<text class="label">是否就诊过</text>
<text class="value">{{ parsedObj.has_visited === 1 || parsedObj.has_visited === '1' ? '是' : '否' }}</text>
</view>
<view v-if="parsedObj.illness_info" class="text-block">
<text class="block-label">适用症/症状</text>
<text class="block-content">{{ parsedObj.illness_info }}</text>
</view>
<view v-if="parsedObj.supplement" class="text-block">
<text class="block-label">补充信息</text>
<text class="block-content">{{ parsedObj.supplement }}</text>
</view>
<view v-if="parsedObj.answer_status === 'pending'" class="follow-up-pending-readonly">
<text class="follow-up-pending-tip">请在挂号后的用药确认页完成选择此处不可更改</text>
</view>
</view>
<view
v-if="followUpDrugs.length && canAddExperienceToRx && !isMine"
class="card-footer card-footer--compact"
>
<button
class="action-btn action-btn--ghost"
@click.stop="$emit('addRegisterDrugsToRx')"
>
添加到处方单
</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 v-if="parsedObj.drug_name" class="info-row">
<text class="label">药品名称</text>
<text class="value">{{ parsedObj.drug_name }}</text>
</view>
<view v-if="parsedObj.illness_info" class="text-block">
<text class="block-label">症状描述</text>
<text class="block-content">{{ parsedObj.illness_info }}</text>
</view>
<view class="info-row">
<text class="label">是否就诊过</text>
<text class="value">{{ parsedObj.has_visited === '1' ? '是' : '否' }}</text>
</view>
<view class="info-row">
<text class="label">是否使用过药品</text>
<text class="value">{{ parsedObj.has_used_drug === '1' ? '是' : '否' }}</text>
</view>
</view>
<view
v-if="parsedObj.drug_name && canAddExperienceToRx && !isMine"
class="card-footer card-footer--compact"
>
<button
class="action-btn action-btn--ghost"
@click.stop="$emit('addExperienceToRx', experienceRxPayload)"
>
添加到处方单
</button>
</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(parsedObj.status) }}</text>
</view>
<view class="card-body">
<view class="info-row">
<text class="label">诊断单号</text>
<text class="value small">{{ parsedObj.order_no }}</text>
</view>
<view class="info-row">
<text class="label">金额</text>
<text class="value price">¥{{ parsedObj.total_pay_price }}</text>
</view>
</view>
<view class="card-footer">
<button class="action-btn" @click="$emit('viewPrescription', parsedObj.id, parsedObj.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">{{ parsedObj.product_name }}</text>
</view>
<view class="info-row">
<text class="label">价格</text>
<text class="value price">¥{{ parsedObj.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">{{ parsedObj.end_reason || parsedObj.reason || parsedObj.message || '本次服务已完成' }}</text>
</view>
</block>
<!-- 14 转诊只读与患者端视觉一致 -->
<block v-else-if="msg.message_type === 14">
<view class="transfer-readonly">
<view class="transfer-r-head">
<u-icon name="order" size="20" color="#ea580c"></u-icon>
<text class="transfer-r-title">转诊消息</text>
</view>
<view class="transfer-r-body">
<view v-if="parsedObj.transfer_store" class="transfer-r-row">
<text class="transfer-r-k">转诊诊所</text>
<text class="transfer-r-v">{{ parsedObj.transfer_store.name || '未知' }}</text>
</view>
<view v-if="parsedObj.delegate_store" class="transfer-r-row">
<text class="transfer-r-k">委托诊所</text>
<text class="transfer-r-v">{{ parsedObj.delegate_store.name || '未知' }}</text>
</view>
<view v-if="parsedObj.prescription_no" class="transfer-r-row">
<text class="transfer-r-k">处方编号</text>
<text class="transfer-r-v">{{ parsedObj.prescription_no }}</text>
</view>
<view v-if="parsedObj.transfer_reason" class="transfer-r-row">
<text class="transfer-r-k">转诊原因</text>
<text class="transfer-r-v">{{ parsedObj.transfer_reason }}</text>
</view>
<view v-if="parsedObj.questions && parsedObj.questions.length" class="transfer-qa">
<text class="transfer-qa-title">咨询问题</text>
<view v-for="(qa, qix) in transferQuestionsWithKey" :key="qa._wxKey" class="transfer-qa-item">
<text class="transfer-qa-q">{{ qix + 1 }}. {{ qa.question }}</text>
<text class="transfer-qa-a">{{ qa.answer }}</text>
</view>
</view>
</view>
<view class="transfer-r-foot">
<text v-if="parsedObj.status === 0" class="transfer-r-status">待患者确认转诊</text>
<text v-else-if="parsedObj.status === 1" class="transfer-r-status ok">患者已同意转诊</text>
<text v-else-if="parsedObj.status === 3" class="transfer-r-status no">患者已拒绝转诊</text>
<text v-else class="transfer-r-status">转诊状态{{ parsedObj.status }}</text>
</view>
</view>
</block>
<block v-else>
<view class="card-body"><text>不支持的消息类型</text></view>
</block>
</view>
<text v-else class="text-content">{{ unknownText }}</text>
</view>
</view>
</view>
</template>
<script>
import { parseMessageContent } from '../utils/messageParse.js';
import { withWxKey } from '@/utils/wxListKey.js';
export default {
name: 'MessageBubble',
props: {
msg: { type: Object, required: true },
isMine: { type: Boolean, default: false },
avatar: { type: String, default: '' },
isPlaying: { type: Boolean, default: false },
/** 接诊中且可开方时,就诊经历卡片显示「加入处方」 */
canAddExperienceToRx: { type: Boolean, default: false }
},
computed: {
isMediaOrCard() {
return [1, 3, 4, 10, 11, 12, 13, 14].includes(this.msg.message_type);
},
isSystemPriceUpdate() {
const p = this.parsedObj;
return this.msg.message_type === 9 && p && (p.type === 'price_update' || p.type === 'price_adjust');
},
isCardType() {
return [4, 10, 11, 12, 13, 14].includes(this.msg.message_type);
},
parsedObj() {
const { message_type, message_content } = this.msg;
if (![4, 9, 10, 11, 12, 13, 14].includes(message_type)) return null;
try {
return typeof message_content === 'object' ? message_content : JSON.parse(message_content);
} catch (e) {
return null;
}
},
parsedText() {
const raw = parseMessageContent(this.msg.message_type, this.msg.message_content);
if (this.msg.message_type === 0 || this.msg.message_type === 1) {
return typeof raw === 'string' ? raw : '';
}
return '';
},
videoSrc() {
if (this.msg.message_type !== 3) return '';
const p = parseMessageContent(3, this.msg.message_content);
if (typeof p === 'string') return p;
if (p && typeof p === 'object' && p.url) return p.url;
return '';
},
systemNinePlainText() {
const c = this.msg.message_content;
if (c == null) return '';
if (typeof c === 'object') return JSON.stringify(c);
return String(c);
},
registerCardDrugRows() {
if (this.msg.message_type !== 10) return [];
const pc = this.parsedObj;
if (!pc || typeof pc !== 'object') return [];
const fallbackQty = pc.number != null && pc.number !== '' ? Number(pc.number) : 1;
const arr = pc.selected_western_drugs;
if (Array.isArray(arr) && arr.length) {
return withWxKey(
arr.map((d) => {
const q = d && d.quantity != null ? Number(d.quantity) : fallbackQty;
return {
name: (d && d.name) || '',
specification: (d && (d.specification || d.spec || d.drug_spec)) || '',
image: (d && (d.image || d.drug_image)) || '',
quantity: q >= 1 ? q : 1,
id: d && (d.drug_id || d.id),
};
}),
'id',
'rd'
);
}
if (pc.drug && pc.drug.name) {
return withWxKey(
[
{
name: pc.drug.name,
specification: pc.drug.specification || pc.drug.spec || '',
image: pc.drug.image || pc.drug.drug_image || '',
quantity: fallbackQty >= 1 ? fallbackQty : 1,
id: pc.drug.drug_id || pc.drug.id,
},
],
'id',
'rd'
);
}
return [];
},
registerCardDrugNamesLine() {
return this.registerCardDrugRows.map((r) => r.name).filter(Boolean).join('、');
},
followUpAnsweredLabel() {
const pc = this.parsedObj;
if (!pc || pc.flow !== 'follow_up_drug') return '';
const st = pc.answer_status;
if (st === 'yes' || st === '1') return '是,曾使用过';
if (st === 'no' || st === '0') return '否,未使用过';
return '';
},
followUpDrugs() {
const drugs = (this.parsedObj && this.parsedObj.drugs) || [];
return withWxKey(
drugs.map((d) => ({
...d,
specification: d.specification || d.spec || d.drug_spec || '',
})),
'id',
'fd'
);
},
transferQuestionsWithKey() {
const qs = (this.parsedObj && this.parsedObj.questions) || [];
return withWxKey(qs, 'id', 'tq');
},
experienceRxPayload() {
const pc = this.parsedObj;
if (!pc || this.msg.message_type !== 11) return {};
return {
drug_name: pc.drug_name || pc.name,
drug_id: pc.drug_id != null ? pc.drug_id : pc.id
};
},
unknownText() {
const c = this.msg.message_content;
if (c == null) return '[消息]';
if (typeof c === 'object') return '[卡片消息]';
const s = String(c);
return s.length > 120 ? s.slice(0, 120) + '…' : s;
}
},
methods: {
getAudioWidth(duration) {
const min = 120;
const max = 350;
const unit = 12;
let w = min + (parseInt(duration, 10) || 0) * unit;
return Math.min(w, max) + 'rpx';
},
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;
.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;
.text-content {
color: inherit;
}
&.mine.type-0 .text-content {
color: #fff;
}
&.no-padding {
padding: 0;
background: transparent !important;
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;
justify-content: space-between;
.duration {
font-size: 26rpx;
white-space: nowrap;
}
.voice-icon-wrap {
display: flex;
align-items: center;
justify-content: center;
width: 44rpx;
height: 44rpx;
&.other {
margin-right: 6rpx;
}
&.mine {
margin-left: 6rpx;
transform: rotate(180deg);
}
&.playing {
animation: voice-pulse 1.2s infinite ease-in-out;
}
}
}
@keyframes voice-pulse {
0% {
opacity: 1;
transform: scale(1) rotate(var(--rotate-angle, 0deg));
}
50% {
opacity: 0.4;
transform: scale(0.92) rotate(var(--rotate-angle, 0deg));
}
100% {
opacity: 1;
transform: scale(1) rotate(var(--rotate-angle, 0deg));
}
}
.voice-icon-wrap.mine.playing {
--rotate-angle: 180deg;
animation: voice-pulse 1.2s infinite ease-in-out;
}
.voice-icon-wrap.other.playing {
--rotate-angle: 0deg;
animation: voice-pulse 1.2s infinite ease-in-out;
}
.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);
&.blue {
color: $primary;
border-color: rgba(10, 132, 255, 0.2);
}
}
}
.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;
}
}
.drug-spec-row {
align-items: flex-start;
.drug-thumb {
width: 64rpx;
height: 64rpx;
border-radius: 8rpx;
background: #f3f4f6;
flex-shrink: 0;
margin-right: 12rpx;
}
.drug-name-wrap {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4rpx;
flex: 1;
min-width: 0;
}
.spec-text {
display: block;
font-size: 22rpx;
color: #9ca3af;
font-weight: 400;
text-align: left;
}
}
.drug-list-block {
.block-label {
display: block;
font-size: 22rpx;
color: #888;
margin-bottom: 8rpx;
}
}
.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;
&.card-footer--compact {
padding: 12rpx 20rpx;
}
.action-btn {
width: 100%;
height: 60rpx;
line-height: 60rpx;
font-size: 26rpx;
border-radius: 30rpx;
background: $primary;
color: #fff;
border: none;
&::after {
border: none;
}
&.action-btn--ghost {
background: #f0fdf4;
color: #059669;
border: 1rpx solid #bbf7d0;
}
}
}
}
.service-notify-card {
background: #ffffff;
border-radius: 12rpx;
width: 480rpx;
border: 1rpx solid #eaeaea;
overflow: hidden;
.sn-header {
padding: 24rpx 24rpx 16rpx 24rpx;
border-bottom: 1rpx solid #f5f5f5;
.sn-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
}
.sn-body {
padding: 20rpx 24rpx;
display: flex;
flex-direction: column;
gap: 16rpx;
.sn-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
line-height: 1.4;
.sn-label {
font-size: 26rpx;
color: #999;
min-width: 110rpx;
}
.sn-value {
font-size: 26rpx;
color: #333;
text-align: right;
flex: 1;
&.truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 320rpx;
}
&.price {
font-weight: 600;
font-size: 28rpx;
}
&.sub {
color: #999;
font-family: monospace;
}
&.strong {
font-weight: bold;
font-size: 30rpx;
}
&.color-orange {
color: #fa8c16;
}
&.color-green {
color: #52c41a;
}
}
.sn-value-group {
display: flex;
align-items: center;
justify-content: flex-end;
.old {
color: #999;
text-decoration: line-through;
font-size: 24rpx;
}
.new {
color: #333;
font-weight: 600;
font-size: 26rpx;
}
}
}
}
}
.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;
}
.follow-up-pending-readonly {
margin-top: 12rpx;
padding: 16rpx 20rpx;
background: #f7f8fa;
border-radius: 12rpx;
.follow-up-pending-tip {
font-size: 24rpx;
color: #909399;
line-height: 1.5;
}
}
.transfer-readonly {
background: linear-gradient(135deg, #fff7ed 0%, #ffedd5 100%);
border-radius: 16rpx;
border: 2rpx solid #fed7aa;
padding: 20rpx;
width: 100%;
box-sizing: border-box;
}
.transfer-r-head {
display: flex;
align-items: center;
gap: 12rpx;
padding-bottom: 16rpx;
margin-bottom: 12rpx;
border-bottom: 2rpx solid #fed7aa;
}
.transfer-r-title {
font-size: 30rpx;
font-weight: 600;
color: #1e293b;
}
.transfer-r-body {
font-size: 26rpx;
color: #334155;
}
.transfer-r-row {
display: flex;
margin-bottom: 12rpx;
line-height: 1.5;
}
.transfer-r-k {
color: #64748b;
width: 160rpx;
flex-shrink: 0;
}
.transfer-r-v {
flex: 1;
color: #1e293b;
}
.transfer-qa {
margin-top: 16rpx;
padding: 16rpx;
background: rgba(255, 255, 255, 0.65);
border-radius: 12rpx;
border: 1rpx solid #fed7aa;
}
.transfer-qa-title {
font-size: 28rpx;
font-weight: 600;
color: #1e293b;
display: block;
margin-bottom: 12rpx;
}
.transfer-qa-item {
margin-bottom: 12rpx;
padding: 12rpx;
background: #fff;
border-radius: 8rpx;
}
.transfer-qa-q {
display: block;
font-size: 26rpx;
color: #334155;
margin-bottom: 8rpx;
}
.transfer-qa-a {
font-size: 24rpx;
color: #0a84ff;
}
.transfer-r-foot {
margin-top: 16rpx;
padding-top: 12rpx;
border-top: 2rpx solid #fed7aa;
}
.transfer-r-status {
font-size: 26rpx;
color: #64748b;
&.ok {
color: #16a34a;
}
&.no {
color: #dc2626;
}
}
</style>