Files
xk-client-wx/subPackages/shop/logistics.vue
李琦 2d2a8abfd4 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 只读医生详情,
  隐藏预约挂号区
2026-07-19 16:08:04 +08:00

529 lines
12 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="container safe-area-inset-bottom">
<MessageNotification />
<!-- 收货地址 -->
<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>
<view class="choose_area">{{logisticsList.express_region}}{{logisticsList.express_address}}</view>
</view>
</view>
<!-- 多包裹 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>
</scroll-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>
<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>
</template>
</u-time-line-item>
</u-time-line>
</view>
<view v-else class="empty-tip">暂无物流轨迹</view>
</view>
</template>
<!-- 底部确认收货 -->
<view class="footer" v-if="canConfirm">
<view class="to_pay" @click="toaffirm">确认收货</view>
</view>
<u-toast ref="uToast" />
</view>
</template>
<script>
import {
getLogistics,
confirmAccept
} from '../../request/api/api';
const FALLBACK_TRACKS = [{
detail: "药品已打包,等待快递揽件",
detail_at: "",
status: "已发货",
},
{
detail: "药房正在备药中",
detail_at: "",
status: "备药",
}
];
export default {
data() {
return {
logisticsList: {},
express: {},
packages: [],
activePkg: 0,
order_id: '',
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 || ''
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,
confirmText: '复制内容',
success: () => {
uni.setClipboardData({
data: value,
success: () => {
uni.showToast({
title: '复制成功',
icon: 'success'
})
}
});
}
});
},
getLogisticsList() {
getLogistics({
method: "post",
data: {
store_id: uni.getStorageSync('store_id') || '11001',
order_id: this.order_id,
}
}).then((res) => {
if (res.data.errcode == 0) {
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() {
confirmAccept({
method: "post",
data: {
store_id: uni.getStorageSync('store_id') || '11001',
order_id: this.order_id,
}
}).then((res) => {
if (res.data.errcode == 0) {
console.log(res.data.data);
}
})
},
}
}
</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: calc(120rpx + env(safe-area-inset-bottom));
background: #F5F5F5;
.empty-tip {
text-align: center;
color: #999;
padding: 40rpx 0;
font-size: 26rpx;
}
.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 {
width: 710rpx;
height: 140rpx;
margin: 10rpx auto 20rpx;
background: #fff;
display: flex;
align-items: center;
padding: 0 40rpx;
image {
width: 84rpx;
height: 84rpx;
margin-right: 16rpx;
}
.head_info {
display: flex;
flex-direction: column;
.name {
font-size: 28rpx;
font-family: PingFang SC-Bold, PingFang SC;
font-weight: bold;
color: #232323;
margin: 10rpx 0;
}
.choose_area {
font-size: 24rpx;
font-family: PingFang SC-Regular, PingFang SC;
color: #666;
}
}
}
.infos {
width: 710rpx;
margin: 0 auto 20rpx;
background: #fff;
padding: 10rpx 30rpx;
._info {
display: flex;
align-items: center;
padding: 20rpx 0;
font-size: 28rpx;
color: #666;
.info {
flex: 1;
margin-left: 30rpx;
color: #232323;
}
.copy {
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;
position: fixed;
bottom: 0;
left: 0;
background: #fff;
padding: 20rpx 40rpx calc(20rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
.to_pay {
height: 80rpx;
line-height: 80rpx;
text-align: center;
background: $primary;
color: #fff;
border-radius: 40rpx;
font-size: 30rpx;
}
}
}
</style>