feat(home/chat): 列表骨架屏、聊天室己方头像与协议/医生详情优化

- 新增分包组件 ListSkeleton(doctor/message/product/article/category/zone)
- 首页/消息/资讯首次加载改用骨架屏,刷新不再清空旧数据避免闪空
  · 去掉 home getList、消息 onShow 中的数组清空
  · 三页 pages.json 增加 easycom 与 componentPlaceholder 主包异步引用
- 聊天室己方头像改为读取本地 userinfo.avatarurl(登录后微信头像),
  mycollection 上传头像成功后同步写回 storage
- 修复协议抽屉点不开:page-container 改为 v-if + :show;
  协议名单独可点节点,匹配放宽 name/desc 双向 includes
- 医生气泡头像可点击进入 doctor-detail?readonly=1 只读医生详情,
  隐藏预约挂号区
This commit is contained in:
李琦
2026-07-19 16:08:04 +08:00
parent 2c739324d6
commit 2d2a8abfd4
21 changed files with 1657 additions and 466 deletions

View File

@@ -59,15 +59,17 @@
>
<view class="message-list-container" id="msglist" :style="{ paddingBottom: (keyboardHeight > 0 ? keyboardHeight + 10 : 0) + 'px' }">
<!-- 协议提示信息 -->
<!-- 协议提示信息协议名单独节点绑定点击避免嵌套 text 吞事件 -->
<view class="notice-box" v-if="showNotice">
<view class="notice-text">
<text v-for="(part, index) in parsedNoticeParts" :key="index">
<block v-for="(part, index) in parsedNoticeParts" :key="index">
<text v-if="part.type === 'text'">{{ part.text }}</text>
<text v-else-if="part.type === 'agreement'" class="notice-link" @click="viewAgreement(part.agreementId)">
{{ part.text }}
</text>
</text>
<text
v-else-if="part.type === 'agreement'"
class="notice-link"
@click.stop="viewAgreement(part.agreementId)"
>{{ part.text }}</text>
</block>
</view>
</view>
@@ -103,6 +105,7 @@
@goToOrderMedicine="goToOrderMedicine"
@viewOrder="viewOrder"
@followUpDrugAnswered="onFollowUpDrugAnswered"
@avatarClick="goDoctorDetail(msg)"
/>
</view>
@@ -115,9 +118,10 @@
<view style="height: 180rpx;"></view>
</scroll-view>
<!-- 协议抽屉 -->
<!-- 协议抽屉v-if 控制挂载:show 控制显隐微信 page-container 要求 -->
<page-container
:if="showAgreement"
v-if="showAgreement"
:show="showAgreement"
position="bottom"
round
overlay
@@ -295,6 +299,8 @@ export default {
this.doctorUserInfo = option;
if (option.avatar) this.doctorAvatar = option.avatar;
this.currentUserId = 'user-' + uni.getStorageSync('user_id') || 'user-2512';
// 从登录缓存读取己方微信/用户头像,避免一直用硬编码占位图
this.syncUserAvatar();
let doctorId = option.user_id || '';
if (doctorId.includes('-type-')) doctorId = doctorId.split('-type-')[0];
@@ -352,6 +358,8 @@ export default {
},
onShow() {
// 从资料页返回时再同步一次,避免改过头像后聊天仍显示旧图
this.syncUserAvatar();
// 确保房间ID已设置从ChatManager获取作为备用
if (!this.doctorUserInfo.room_id && typeof ChatManager !== 'undefined') {
const currentRoomId = ChatManager.getCurrentRoomId();
@@ -391,6 +399,16 @@ export default {
methods: {
// === 新增辅助方法 ===
/**
* 从本地 userinfo 同步己方头像(字段为 avatarurl与登录/我的页一致)
* 无头像时回退到与我的页相同的默认图
*/
syncUserAvatar() {
const userinfo = uni.getStorageSync('userinfo') || {};
this.userAvatar = userinfo.avatarurl
|| 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/mr_tx.png';
},
isMsgMine(msg) {
return this.getSenderIdWithoutPrefix(msg.sender_user_id) === this.getCurrentUserIdWithoutPrefix();
},
@@ -495,22 +513,20 @@ export default {
});
}
// 查找匹配的协议
// 查找匹配的协议:名称/描述双向 includes仍无匹配则用列表第一条保证可点
const agreementName = match[1];
const agreement = this.agreements.find(ag => ag.name === agreementName || ag.name.includes(agreementName));
const agreement = this.findAgreementByName(agreementName);
if (agreement) {
// 找到匹配的协议,添加为可点击的链接
parts.push({
type: 'agreement',
text: agreementName,
agreementId: agreement.id
});
} else {
// 没找到匹配的协议,作为普通文本显示
parts.push({
type: 'text',
text: match[0] // 《协议名称》
text: match[0]
});
}
@@ -527,6 +543,24 @@ export default {
this.parsedNoticeParts = parts;
},
/**
* 按文案中的协议名匹配协议列表(放宽 name/desc 双向包含)
* 匹配不到但列表非空时回退第一条,避免《》无法点击
*/
findAgreementByName(agreementName) {
const list = this.agreements || [];
if (!list.length) return null;
const name = String(agreementName || '');
const hit = list.find((ag) => {
const n = String(ag.name || '');
const d = String(ag.desc || '');
return n === name
|| n.includes(name)
|| name.includes(n)
|| (d && (d === name || d.includes(name) || name.includes(d)));
});
return hit || list[0];
},
// 查看协议
async viewAgreement(agreementId) {
if (!agreementId) {
@@ -541,6 +575,7 @@ export default {
const agreement = this.agreements.find(ag => ag.id === agreementId);
this.currentAgreementId = agreementId;
this.currentAgreementName = agreement ? agreement.name : '协议详情';
this.agreementContent = '';
this.showAgreement = true;
try {
@@ -569,6 +604,26 @@ export default {
this.showAgreement = false;
},
/**
* 点击气泡医生头像进入医生详情(只读,不展示挂号)
* 己方/助理头像不跳转
*/
goDoctorDetail(msg) {
if (!msg || this.isMsgMine(msg)) return;
const senderId = msg.sender_user_id || msg.senderId || '';
if (senderId === 'doctor_assistant') return;
const cleanId = String(this.doctorId || '')
.replace(/^doctor-/, '')
.replace(/-type-.*$/, '');
if (!cleanId) {
uni.showToast({ title: '医生信息缺失', icon: 'none' });
return;
}
uni.navigateTo({
url: `/subPackages/doctor/doctor-detail?id=${cleanId}&readonly=1`
});
},
// === 音频播放逻辑 ===
playAudio(msg) {
const audioUrl = getParsedContent(msg.message_type, msg.message_content);

View File

@@ -1,7 +1,7 @@
<template>
<view class="bubble-container" :class="{ 'mine': isMine, 'other': !isMine }">
<!-- 头像 -->
<image class="avatar" :src="avatar" mode="aspectFill"></image>
<!-- 头像非己方可点进医生详情 -->
<image class="avatar" :src="avatar" mode="aspectFill" @click.stop="onAvatarClick"></image>
<!-- 消息内容包裹 -->
<view class="content-wrapper">
@@ -146,13 +146,22 @@
<text class="block-label">主诉</text>
<text class="block-content">{{ parsedContent.chief_complaint }}</text>
</view>
<view class="info-row" v-if="registerCardDrugNamesLine">
<text class="label">所选药品</text>
<text class="value">{{ registerCardDrugNamesLine }}</text>
</view>
<view class="info-row" v-if="parsedContent.number != null && parsedContent.number !== ''">
<text class="label">数量</text>
<text class="value"> {{ parsedContent.number }} </text>
<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 class="card-footer"><button class="action-btn" @click="$emit('viewRegister', parsedContent.id)">查看详情</button></view>
@@ -314,17 +323,38 @@ export default {
}
return message_content;
},
/** 挂号卡片:多选西药名称串联,无则单药 drug.name */
registerCardDrugNamesLine() {
if (this.msg.message_type !== 10) return '';
/**
* 预约挂号卡片药品行:药名 + 规格 + 数量(与医生端一致,去掉底部「各 X 盒」)
*/
registerCardDrugRows() {
if (this.msg.message_type !== 10) return [];
const pc = this.parsedContent;
if (!pc || typeof pc !== 'object') return '';
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 arr.map((d) => d && d.name).filter(Boolean).join('、');
return arr.map((d, idx) => {
const q = d && d.quantity != null ? Number(d.quantity) : fallbackQty;
const id = d && (d.drug_id || d.id);
return {
_wxKey: 'rd-' + (id != null ? id : idx),
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,
};
});
}
if (pc.drug && pc.drug.name) return pc.drug.name;
return '';
if (pc.drug && pc.drug.name) {
return [{
_wxKey: 'rd-' + (pc.drug.drug_id || pc.drug.id || 0),
name: pc.drug.name,
specification: pc.drug.specification || pc.drug.spec || '',
image: pc.drug.image || pc.drug.drug_image || '',
quantity: fallbackQty >= 1 ? fallbackQty : 1,
}];
}
return [];
},
followUpAnsweredLabel() {
const pc = this.parsedContent;
@@ -336,6 +366,13 @@ export default {
}
},
methods: {
/**
* 非己方头像点击:通知父页跳转医生详情;己方头像不响应
*/
onAvatarClick() {
if (this.isMine) return;
this.$emit('avatarClick');
},
// 计算语音条宽度 (Min: 100rpx, Max: 350rpx, Unit: 10rpx/sec)
getAudioWidth(duration) {
const min = 120;
@@ -609,6 +646,40 @@ $success-color: #059669;
.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;