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;

View File

@@ -0,0 +1,275 @@
<template>
<!--
列表骨架屏 type 模拟医生卡/消息行/商品卡/资讯卡/分类侧栏
放在分包以控制主包体积主包页通过 componentPlaceholder 异步引用
-->
<view class="list-skeleton" :class="'list-skeleton--' + type">
<view
v-for="i in rowCount"
:key="i"
class="sk-row"
:class="'sk-row--' + type"
>
<!-- 医生推荐卡片 -->
<template v-if="type === 'doctor'">
<view class="sk-avatar sk-shimmer"></view>
<view class="sk-body">
<view class="sk-line sk-line--lg sk-shimmer"></view>
<view class="sk-line sk-line--md sk-shimmer"></view>
<view class="sk-line sk-line--sm sk-shimmer"></view>
</view>
</template>
<!-- 消息列表行 -->
<template v-else-if="type === 'message'">
<view class="sk-avatar sk-avatar--round sk-shimmer"></view>
<view class="sk-body">
<view class="sk-line-row">
<view class="sk-line sk-line--md sk-shimmer"></view>
<view class="sk-line sk-line--xs sk-shimmer"></view>
</view>
<view class="sk-line sk-line--sm sk-shimmer"></view>
</view>
</template>
<!-- 商品卡片 -->
<template v-else-if="type === 'product'">
<view class="sk-prod-img sk-shimmer"></view>
<view class="sk-body">
<view class="sk-line sk-line--lg sk-shimmer"></view>
<view class="sk-line sk-line--sm sk-shimmer"></view>
<view class="sk-line sk-line--md sk-shimmer"></view>
</view>
</template>
<!-- 资讯文章卡片 -->
<template v-else-if="type === 'article'">
<view class="sk-cover sk-shimmer"></view>
<view class="sk-cover-title sk-shimmer"></view>
</template>
<!-- 资讯左侧分类 -->
<template v-else-if="type === 'category'">
<view class="sk-cate-item sk-shimmer"></view>
</template>
<!-- 金刚区横滑块 -->
<template v-else-if="type === 'zone'">
<view class="sk-zone-icon sk-shimmer"></view>
<view class="sk-line sk-line--xs sk-shimmer"></view>
</template>
</view>
</view>
</template>
<script>
export default {
name: 'ListSkeleton',
props: {
// 骨架样式类型
type: {
type: String,
default: 'message',
validator(v) {
return ['doctor', 'message', 'product', 'article', 'category', 'zone'].indexOf(v) !== -1
}
},
// 行数
rows: {
type: [Number, String],
default: 3
}
},
computed: {
rowCount() {
const n = Number(this.rows)
return n > 0 ? n : 3
}
}
}
</script>
<style lang="scss" scoped>
.list-skeleton {
width: 100%;
}
.sk-shimmer {
background: linear-gradient(90deg, #ebebeb 25%, #f5f5f5 50%, #ebebeb 75%);
background-size: 400% 100%;
animation: sk-shimmer 1.4s ease infinite;
}
@keyframes sk-shimmer {
0% {
background-position: 100% 0;
}
100% {
background-position: 0 0;
}
}
.sk-row--doctor {
display: flex;
align-items: flex-start;
margin: 0 24rpx 20rpx;
padding: 28rpx;
background: #fff;
border-radius: 24rpx;
.sk-avatar {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
flex-shrink: 0;
margin-right: 20rpx;
}
.sk-body {
flex: 1;
}
}
.sk-row--message {
display: flex;
align-items: center;
padding: 32rpx 38rpx;
background: #fff;
.sk-avatar {
width: 84rpx;
height: 84rpx;
border-radius: 50%;
flex-shrink: 0;
margin-right: 24rpx;
}
.sk-body {
flex: 1;
}
.sk-line-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
}
.sk-row--product {
display: flex;
margin: 0 24rpx 20rpx;
padding: 20rpx;
background: #fff;
border-radius: 16rpx;
.sk-prod-img {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
flex-shrink: 0;
margin-right: 20rpx;
}
.sk-body {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
}
.sk-row--article {
position: relative;
width: 500rpx;
height: 246rpx;
margin: 10rpx auto;
border-radius: 12rpx;
overflow: hidden;
.sk-cover {
width: 100%;
height: 100%;
border-radius: 12rpx;
}
.sk-cover-title {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 92rpx;
opacity: 0.5;
}
}
.sk-row--category {
width: 184rpx;
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
.sk-cate-item {
width: 100rpx;
height: 28rpx;
border-radius: 6rpx;
}
}
.list-skeleton--zone {
display: flex;
flex-wrap: nowrap;
padding: 16rpx 24rpx;
overflow: hidden;
}
.sk-row--zone {
display: flex;
flex-direction: column;
align-items: center;
width: 140rpx;
flex-shrink: 0;
margin-right: 16rpx;
.sk-zone-icon {
width: 88rpx;
height: 88rpx;
border-radius: 20rpx;
margin-bottom: 12rpx;
}
.sk-line--xs {
width: 72rpx;
}
}
.sk-line {
height: 24rpx;
border-radius: 6rpx;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
}
.sk-line--lg {
width: 70%;
}
.sk-line--md {
width: 50%;
}
.sk-line--sm {
width: 90%;
}
.sk-line--xs {
width: 80rpx;
height: 20rpx;
margin-bottom: 0;
}
.sk-line-row .sk-line--md {
width: 40%;
margin-bottom: 0;
}
</style>

File diff suppressed because one or more lines are too long

View File

@@ -36,8 +36,8 @@
</view>
</view>
<!-- 到店医生 预约挂号 -->
<view class="appoin">
<!-- 到店医生 预约挂号聊天室只读进入时不展示挂号入口 -->
<view class="appoin" v-if="!readonly">
<view class="title">预约挂号</view>
<view class="list">
<view class="yard">
@@ -121,10 +121,13 @@
registList: [],
peopleList: [],
rType: '0', // 挂号类型
// 聊天室等场景只读查看:隐藏挂号按钮区
readonly: false,
}
},
onLoad(e) {
console.log(e, 'e');
this.readonly = e.readonly === '1' || e.readonly === 1;
// 允许未登录访问,移除登录跳转
// 如果是guest模式强制设置store_id为11001
if (isGuestMode()) {
@@ -148,7 +151,10 @@
this.doctorID = uni.getStorageSync('doctors_id')
this.getDocdetail();
this.getUser();
this.getRegist();
// 只读模式不拉号源,避免无用请求
if (!this.readonly) {
this.getRegist();
}
console.log(this.doctorID, '6666');
} else {
@@ -159,7 +165,9 @@
if (isGuestMode()) {
uni.setStorageSync('store_id', '11001');
this.getDocdetail();
this.getRegist();
if (!this.readonly) {
this.getRegist();
}
} else {
const id = e.sts_id || uni.getStorageSync('store_id')
if (id!='undefined' && id) {
@@ -179,6 +187,8 @@
},
// 挂号
goAdd() {
// 只读模式不允许挂号
if (this.readonly) return;
// 如果未登录,保存跳转信息并跳转到登录页
if (!isLoggedIn()) {
const storeId = uni.getStorageSync('store_id') || '11001';
@@ -345,8 +355,11 @@
mounted() {
this.getDocdetail();
this.getUser();
this.getRegist();
this.getPeople();
// 只读模式不拉号源/就诊人相关挂号数据
if (!this.readonly) {
this.getRegist();
this.getPeople();
}
if (!this.doctorList && doctorID) {
this.getDocdetail();

View File

@@ -132,6 +132,10 @@
<!-- 按钮区完全保留你原有的判断逻辑 -->
<view class="choose_btn">
<view class="order_info active" v-if="item.is_pay==0 && item.status==0 && item.cancel_status==0"
@tap.stop="goCancel(item.id)">
取消
</view>
<view class="order_info active" v-if="item.status==1 && item.is_pay==1 && item.forbiddenRefund==0"
@tap.stop="toCancel(item.id,item.status,refundType)">
退款
@@ -164,10 +168,6 @@
查看物流
</view>
<view class="order_info active" v-if="item.is_pay==0 && item.status==0 && item.cancel_status==0"
@tap.stop="goCancel(item.id)">
取消
</view>
</view>
</view>
<u-loadmore :status="loadMoreStatus" v-if="orderList.length!=0" margin-top="30" margin-bottom="30" />

View File

@@ -91,10 +91,26 @@
this.mobile = res.data.data.user.mobile
this.idCard = res.data.data.user.idcard
this._securityBaseline = { name: this.name }
// 写回本地 userinfo保证聊天室等页面能读到最新头像
this.syncUserinfoStorage(res.data.data.user)
}
})
},
/**
* 将最新用户资料合并进本地 userinfo尤其是 avatarurl
* 登录时存的是整份 user此处只覆盖接口返回的字段避免丢掉其它缓存键
*/
syncUserinfoStorage(user) {
if (!user) return
const cached = uni.getStorageSync('userinfo') || {}
uni.setStorageSync('userinfo', {
...cached,
...user,
avatarurl: user.avatarurl || cached.avatarurl || ''
})
},
// 点击头像触发click事件 changeUserPhoto
async changeUserPhoto() {
console.log('进入更新头像的功能')
@@ -198,6 +214,13 @@
icon: "success",
title: '保存成功'
})
// 先写回 storage再拉详情避免聊天页 onShow 读到旧头像
const cached = uni.getStorageSync('userinfo') || {}
uni.setStorageSync('userinfo', {
...cached,
avatarurl: req.data.url
})
this.avatar = req.data.url
this.getInfo()
}
})

View File

@@ -17,7 +17,7 @@
<image :src="data.store['offical_seal'] ||''" mode=""></image>
<view class="record_yard">
<view class="yard">
{{data.store.name}}
{{ storeDisplayName }}
</view>
<view class="state">
处方笺
@@ -359,8 +359,16 @@
getUseWay
} from '../../request/api/api'
import { checkImageUrl } from '../../utils/utils'
import { formatStoreNameWithHu } from '../../utils/formatStoreNameWithHu.js'
export default {
computed: {
/** 在线处方诊所名加(互) */
storeDisplayName() {
const store = this.data && this.data.store ? this.data.store : {}
return formatStoreNameWithHu(store.name, this.data && this.data.is_online)
},
},
data() {
return {
statusName: {

View File

@@ -2,7 +2,7 @@
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 收货地址 -->
<view class="head" v-if="logisticsList.express_name!=''">
<view class="head" v-if="logisticsList.express_name">
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/mr_tx.png" mode=""></image>
<view class="head_info">
<view class="name">{{logisticsList.express_name}} {{logisticsList.express_mobile_tm || logisticsList.express_mobile}}</view>
@@ -10,110 +10,250 @@
</view>
</view>
<!-- 订单信息 -->
<view class="infos" v-if="express!=''">
<view class="_info">
<text>快递单号</text>
<text class="info">{{express.express_no}}</text>
<text class="copy" @click="copy(express.express_no)">复制</text>
<!-- 多包裹 Tab warehouse_name 时只显示包裹序号 -->
<scroll-view v-if="packages.length > 1" class="pkg-tabs" scroll-x>
<view
v-for="(pkg, idx) in packages"
:key="idx"
class="pkg-tab"
:class="{ active: activePkg === idx }"
@click="switchPkg(idx)"
>
包裹{{ pkg.package_no || idx + 1 }}
<text v-if="pkg.warehouse_name" class="pkg-sub">{{ pkg.warehouse_name }}</text>
</view>
<view class="_info">
<text>配送方式</text>
<text class="info">{{express.express_company_name}}</text>
</view>
</view>
</scroll-view>
<!-- 物流追踪 -->
<view class="card">
<view class="title">
物流追踪
</view>
<!-- <view class="empty" v-if="newArr.length==0">
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/empty/orders.png" mode="aspectFit"></image>
<text>药房正在备药中暂时还没有物流信息哦~</text>
</view> -->
<!-- 多包裹左右滑动切换单包直接展示 -->
<swiper
v-if="packages.length > 1"
class="pkg-swiper"
:current="activePkg"
:style="{ height: swiperHeight + 'px' }"
@change="onSwiperChange"
>
<swiper-item v-for="(pkg, idx) in packages" :key="idx">
<view class="pkg-panel" :id="'pkg-panel-' + idx">
<view class="infos" v-if="pkgExpress(pkg).express_no">
<view class="_info">
<text>快递单号</text>
<text class="info">{{ pkgExpress(pkg).express_no }}</text>
<text class="copy" @click="copy(pkgExpress(pkg).express_no)">复制</text>
</view>
<view class="_info">
<text>配送方式</text>
<text class="info">{{ pkgExpress(pkg).express_company_name }}</text>
</view>
</view>
<view class="infos" v-else>
<view class="_info">
<text>发货状态</text>
<text class="info">{{ Number(pkg.is_send) === 1 ? '已发货' : '待发货' }}</text>
</view>
</view>
<view class="card">
<view class="title">物流追踪</view>
<view class="line" v-if="pkgTracks(pkg).length > 0">
<u-time-line>
<u-time-line-item
v-for="(item, index) in pkgTracks(pkg)"
:key="index"
nodeTop="2"
>
<template v-slot:node>
<view class="track-node" :class="{ 'is-active': index === 0 }" />
</template>
<template v-slot:content>
<view class="track-content" :class="{ 'is-active': index === 0 }">
<view class="u-order-title">
{{ item.status }}
<text v-if="showOngoing(item, index)" class="ongoing">进行中</text>
</view>
<view class="u-order-desc">{{ item.detail }}</view>
<view class="u-order-time">{{ item.detail_at }}</view>
</view>
</template>
</u-time-line-item>
</u-time-line>
</view>
<view v-else class="empty-tip">暂无物流轨迹</view>
</view>
</view>
</swiper-item>
</swiper>
<view class="line" v-if="newArr.length>0">
<u-time-line v-for="(item,index) in newArr" :key="index">
<u-time-line-item nodeTop="2">
<template v-slot:content>
<view>
<view class="u-order-title">{{item.status}}</view>
<view class="u-order-desc">
{{item.detail}}
<template v-else>
<view class="infos" v-if="currentExpress && currentExpress.express_no">
<view class="_info">
<text>快递单号</text>
<text class="info">{{ currentExpress.express_no }}</text>
<text class="copy" @click="copy(currentExpress.express_no)">复制</text>
</view>
<view class="_info">
<text>配送方式</text>
<text class="info">{{ currentExpress.express_company_name }}</text>
</view>
</view>
<view class="infos" v-else-if="packages.length">
<view class="_info">
<text>发货状态</text>
<text class="info">{{ currentPkg && Number(currentPkg.is_send) === 1 ? '已发货' : '待发货' }}</text>
</view>
</view>
<view class="card">
<view class="title">物流追踪</view>
<view class="line" v-if="newArr.length > 0">
<u-time-line>
<u-time-line-item
v-for="(item, index) in newArr"
:key="index"
nodeTop="2"
>
<template v-slot:node>
<view class="track-node" :class="{ 'is-active': index === 0 }" />
</template>
<template v-slot:content>
<view class="track-content" :class="{ 'is-active': index === 0 }">
<view class="u-order-title">
{{ item.status }}
<text v-if="showOngoing(item, index)" class="ongoing">进行中</text>
</view>
<view class="u-order-desc">{{ item.detail }}</view>
<view class="u-order-time">{{ item.detail_at }}</view>
</view>
<view class="u-order-time">{{item.detail_at}}</view>
</view>
</template>
</u-time-line-item>
</u-time-line>
</template>
</u-time-line-item>
</u-time-line>
</view>
<view v-else class="empty-tip">暂无物流轨迹</view>
</view>
</view>
</template>
<!-- 底部 确认收货 -->
<view class="footer" v-if="newArr.length>0">
<!-- 底部确认收货 -->
<view class="footer" v-if="canConfirm">
<view class="to_pay" @click="toaffirm">确认收货</view>
</view>
<u-toast ref="uToast" />
</view>
</template>
<script>
// getLogistics
import {
getLogistics,
confirmAccept
} from '../../request/api/api';
const FALLBACK_TRACKS = [{
detail: "药品已打包,等待快递揽件",
detail_at: "",
status: "已发货",
},
{
detail: "药房正在备药中",
detail_at: "",
status: "备药",
}
];
export default {
data() {
return {
isshow: true,
show: false,
content: '是否确认退款',
logisticsList: {},
order_no: '',
express: [],
express: {},
packages: [],
activePkg: 0,
order_id: '',
newArr: [{
detail: "药品已打包,等待快递揽件",
detail_at: "",
status: "已发货",
},
{
detail: "药房正在备药中",
detail_at: "",
status: "备药",
}
]
newArr: [],
canConfirm: false,
swiperHeight: 480,
}
},
computed: {
currentPkg() {
return this.packages[this.activePkg] || null
},
currentExpress() {
const pkg = this.currentPkg
if (pkg && pkg.express) return pkg.express
return this.express
}
},
onLoad(op) {
this.order_id = op.id || '45'
this.order_id = op.id || ''
this.getLogisticsList()
},
methods: {
// 支付
/** 最新一条非签收时才显示「进行中」 */
showOngoing(item, index) {
if (index !== 0) return false
const status = String((item && item.status) || '')
return status !== '签收'
},
pkgExpress(pkg) {
return (pkg && pkg.express) || {}
},
/** 按包裹组装轨迹(读 packages[i].express.detail */
pkgTracks(pkg) {
const express = this.pkgExpress(pkg)
const detail = Array.isArray(express.detail) ? express.detail : []
if (detail.length) return detail
if (pkg && Number(pkg.is_send) === 1) return FALLBACK_TRACKS
return [{
detail: '药房正在备药中',
detail_at: '',
status: '备药',
}]
},
/** Tab 切换包裹,驱动 swiper */
switchPkg(idx) {
this.activePkg = idx
this.applyTracksFromPkg()
this.$nextTick(() => this.updateSwiperHeight())
},
/** swiper 左右滑同步 activePkg */
onSwiperChange(e) {
const idx = Number(e.detail.current || 0)
if (idx === this.activePkg) return
this.activePkg = idx
this.applyTracksFromPkg()
this.$nextTick(() => this.updateSwiperHeight())
},
/** 测量当前面板高度,避免 swiper 裁切时间轴 */
updateSwiperHeight() {
if (this.packages.length <= 1) return
const q = uni.createSelectorQuery().in(this)
q.select('#pkg-panel-' + this.activePkg).boundingClientRect((rect) => {
if (rect && rect.height) {
this.swiperHeight = Math.ceil(rect.height)
}
}).exec()
},
/**
* 根据当前包裹组装时间轴(单包模式用)
* 接口 detail 按时间倒序,第 0 条即最新
*/
applyTracksFromPkg() {
const pkg = this.currentPkg
this.newArr = this.pkgTracks(pkg)
const express = this.currentExpress
const hasTrack = Array.isArray(express.detail) && express.detail.length > 0
this.canConfirm = hasTrack || (pkg && Number(pkg.is_send) === 1)
},
toaffirm() {
this.postConfirm()
uni.switchTab({
url: '/pages/mine/mine'
})
},
// 点击复制
copy(value) {
//提示模板
uni.showModal({
content: value, //模板中提示的内容
content: value,
confirmText: '复制内容',
success: () => {
uni.setClipboardData({
data: value, //要被复制的内容
success: () => { //复制成功的回调函数
uni.showToast({ //提示
data: value,
success: () => {
uni.showToast({
title: '复制成功',
icon: 'success'
})
@@ -122,28 +262,31 @@
}
});
},
//物流详情请求
getLogisticsList() {
getLogistics({
method: "get",
method: "post",
data: {
store_id: uni.getStorageSync('store_id') || '11001',
order_id: this.order_id,
}
}).then((res) => {
// console.log(res, 'list');
if (res.data.errcode == 0) {
console.log(res.data.data);
this.logisticsList = res.data.data.address
this.express = res.data.data.express
let arr = this.express.detail.filter((item, index) => {
return index != 0
})
this.newArr = [...arr,...this.newArr ]
const data = res.data.data || {}
this.logisticsList = data.address || {}
this.express = data.express || {}
this.packages = Array.isArray(data.packages) ? data.packages : []
if (!this.packages.length && this.express && this.express.express_no) {
this.packages = [{
package_no: 1,
warehouse_name: '',
is_send: 1,
express: this.express,
}]
}
this.activePkg = 0
this.applyTracksFromPkg()
this.$nextTick(() => this.updateSwiperHeight())
}
})
},
postConfirm() {
@@ -154,11 +297,9 @@
order_id: this.order_id,
}
}).then((res) => {
// console.log(res, 'list');
if (res.data.errcode == 0) {
console.log(res.data.data);
}
})
},
}
@@ -166,25 +307,63 @@
</script>
<style lang="scss" scoped>
$primary: #4175EE;
$primary-soft: rgba(65, 117, 238, 0.12);
$primary-glow: rgba(65, 117, 238, 0.25);
.container {
width: 750rpx;
max-height: 100%;
min-height: 100vh;
padding-bottom: env(safe-area-inset-bottom);
padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
background: #F5F5F5;
.empty {
width: 440rpx;
height: 393rpx;
margin: 200rpx auto;
.empty-tip {
text-align: center;
color: #999;
padding: 40rpx 0;
font-size: 26rpx;
}
image {
width: 100%;
height: 100%;
z-index: 10;
margin-bottom: 10rpx;
.pkg-tabs {
white-space: nowrap;
width: 710rpx;
margin: 0 auto 16rpx;
background: #fff;
padding: 16rpx 20rpx;
box-sizing: border-box;
}
.pkg-tab {
display: inline-flex;
flex-direction: column;
align-items: center;
padding: 12rpx 28rpx;
margin-right: 16rpx;
border-radius: 12rpx;
background: #f5f5f5;
font-size: 26rpx;
color: #666;
&.active {
background: $primary-soft;
color: $primary;
font-weight: 600;
}
.pkg-sub {
font-size: 20rpx;
margin-top: 4rpx;
opacity: 0.8;
}
}
.pkg-swiper {
width: 750rpx;
}
.pkg-panel {
padding-bottom: 20rpx;
}
.head {
@@ -217,157 +396,133 @@
.choose_area {
font-size: 24rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #232323;
}
}
}
.card {
width: 710rpx;
border-radius: 16rpx;
margin: 10rpx auto 180rpx;
padding: 30rpx;
background: #FFFFFF;
.title {
width: 640rpx;
padding-bottom: 22rpx;
font-size: 28rpx;
font-family: PingFang SC-Bold, PingFang SC;
font-weight: bold;
color: #232323;
border-bottom: 2rpx solid #DDDDDD;
}
.line {
margin-top: 20rpx;
margin-bottom: 18rpx;
.u-time-line-item {
display: flex;
}
.u-node {
width: 44rpx;
height: 44rpx;
border-radius: 100rpx;
display: flex;
justify-content: center;
align-items: center;
background: #d0d0d0;
}
.u-order-title {
color: #333333;
font-weight: bold;
font-size: 32rpx;
}
.u-order-desc {
color: rgb(150, 150, 150);
font-size: 28rpx;
margin-bottom: 6rpx;
}
.u-order-time {
color: rgb(200, 200, 200);
font-size: 26rpx;
color: #666;
}
}
}
.infos {
width: 710rpx;
margin: 20rpx auto 20rpx;
padding: 20rpx 32rpx;
border-radius: 16rpx;
background: #FFFFFF;
margin: 0 auto 20rpx;
background: #fff;
padding: 10rpx 30rpx;
._info {
display: flex;
align-items: center;
margin: 20rpx 0;
text {
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #999999;
}
padding: 20rpx 0;
font-size: 28rpx;
color: #666;
.info {
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
flex: 1;
margin-left: 30rpx;
color: #232323;
margin-left: 60rpx;
}
.copy {
display: inline-block;
border: 2rpx solid #1777FF;
width: 88rpx;
height: 48rpx;
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #1777FF;
line-height: 44rpx;
text-align: center;
margin-left: 60rpx;
color: $primary;
margin-left: 20rpx;
}
}
}
.card {
width: 710rpx;
margin: 0 auto 20rpx;
background: #fff;
padding: 30rpx;
.title {
font-size: 32rpx;
font-weight: bold;
color: #232323;
margin-bottom: 20rpx;
}
.line {
.track-node {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background: #d9d9d9;
margin-left: 6rpx;
&.is-active {
width: 20rpx;
height: 20rpx;
margin-left: 4rpx;
background: $primary;
box-shadow: 0 0 0 6rpx $primary-glow;
}
}
.track-content {
.u-order-title {
font-size: 28rpx;
font-weight: 500;
color: #999;
display: flex;
align-items: center;
gap: 12rpx;
}
.u-order-desc {
font-size: 26rpx;
color: #bbb;
margin: 10rpx 0;
}
.u-order-time {
font-size: 24rpx;
color: #ccc;
}
&.is-active {
.u-order-title {
font-weight: 700;
color: $primary;
}
.u-order-desc {
color: #666;
}
.u-order-time {
color: #999;
}
.ongoing {
font-size: 22rpx;
font-weight: 500;
color: #fff;
background: $primary;
padding: 2rpx 12rpx;
border-radius: 8rpx;
}
}
}
}
}
.footer {
width: 750rpx;
height: 100rpx;
background: #fff;
position: fixed;
bottom: 0;
left: 0;
bottom: env(safe-area-inset-bottom);
display: flex;
justify-content: flex-end;
align-items: center;
padding: 0 24rpx;
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
.to_cancle {
width: 196rpx;
height: 64rpx;
text-align: center;
line-height: 64rpx;
color: #1777FF;
background: #E7F1FF;
border-radius: 50rpx 50rpx 50rpx 50rpx;
margin-right: 20rpx;
}
background: #fff;
padding: 20rpx 40rpx calc(20rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
.to_pay {
width: 196rpx;
height: 64rpx;
height: 80rpx;
line-height: 80rpx;
text-align: center;
line-height: 64rpx;
background: $primary;
color: #fff;
background: #1777FF;
border-radius: 50rpx 50rpx 50rpx 50rpx;
}
.to_agin {
width: 196rpx;
height: 64rpx;
text-align: center;
line-height: 64rpx;
border-radius: 50rpx 50rpx 50rpx 50rpx;
border: 2rpx solid #999999;
color: #999999;
margin-left: 20rpx;
border-radius: 40rpx;
font-size: 30rpx;
}
}
}
</style>
</style>