certLogistics.vue 6.63 KB
<template>
  <view class="logistics-page">
    <view class="logistics-info-card">
      <view class="info-header">
        <uni-icons type="paperplane-filled" size="18" color="#AD181F"></uni-icons>
        <text class="header-title">物流信息</text>
      </view>
      <view class="info-content">
        <view class="info-row">
          <text class="label">运单号</text>
          <text class="value tracking-number" @click="copyTracking">
            {{ currentItem.postCode || '-' }}
          </text>
        </view>
      </view>
    </view>

    <view class="timeline-section">
      <view class="section-header">
       <uni-icons type="location-filled" size="20" color="#AD181F"></uni-icons>
        <text class="section-title">运输轨迹</text>
      </view>

      <view class="loading-wrap" v-if="loading">
        <uni-load-more status="loading"></uni-load-more>
      </view>

      <view class="timeline-wrap" v-else>
        <view class="timeline-list" v-if="trackingNodes.length > 0">
          <view
            v-for="(node, idx) in trackingNodes"
            :key="idx"
            class="timeline-item"
            :class="{ first: idx === 0 }"
          >
            <view class="timeline-left">
              <view class="timeline-dot">
                <text v-if="idx === 0" class="timeline-check"></text>
              </view>
              <view v-if="idx < trackingNodes.length - 1" class="timeline-line"></view>
            </view>
            <view class="timeline-right">
              <view class="timeline-content">
                <view class="timeline-title">{{ node.categoryName || '物流更新' }}</view>
                <view class="timeline-time">{{ node.operationTime || '-' }}</view>
                <view class="timeline-desc">{{ node.operationRemark || '-' }}</view>
              </view>
            </view>
          </view>
        </view>

        <view class="nodata" v-else>
          <image mode="aspectFit" :src="config.baseUrl_api + '/fs/static/nodata.png'"></image>
          <text>暂无物流信息</text>
        </view>
      </view>
    </view>
  </view>
</template>

<script setup>
import * as api from '@/common/api.js'
import config from '@/config.js'
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'

const loading = ref(false)
const payId = ref('')
const currentItem = ref({})
const trackingNodes = ref([])

onLoad((options) => {
  if (!options.payId) return
  payId.value = options.payId
  if (options.postCode) currentItem.value.postCode = options.postCode
  if (options.postStatus) currentItem.value.postStatus = parseInt(options.postStatus)
  if (options.submitTime) currentItem.value.submitTime = options.submitTime
  getLogisticsInfo()
})

function getLogisticsInfo() {
  if (!payId.value) return

  loading.value = true
  api.queryTrace(payId.value).then((res) => {
    trackingNodes.value = res.data || []
  }).catch((err) => {
    console.error('获取物流信息失败', err)
    uni.showToast({ title: '获取物流信息失败', icon: 'none' })
  }).finally(() => {
    loading.value = false
  })
}

function copyTracking() {
  if (!currentItem.value.postCode) return
  uni.setClipboardData({
    data: currentItem.value.postCode,
    success: () => {
      uni.showToast({ title: '已复制', icon: 'success' })
    }
  })
}
</script>

<style lang="scss" scoped>
.logistics-page {
  min-height: 100vh;
  background-color: #f5f5f5;
  padding: 20rpx;
  padding-bottom: 40rpx;
}

.logistics-info-card {
  background-color: #fff;
  border-radius: 16rpx;
  padding: 30rpx;
  margin-bottom: 20rpx;
}

.info-header {
  display: flex;
  align-items: center;
  margin-bottom: 24rpx;

  .header-title {
    font-size: 30rpx;
    font-weight: 600;
    color: #333;
    margin-left: 10rpx;
  }
}

.info-content {
  .info-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 16rpx 0;

    .label {
      font-size: 26rpx;
      color: #999;
    }

    .value {
      font-size: 26rpx;
      color: #333;

      &.tracking-number {
        max-width: 460rpx;
        text-align: right;
        word-break: break-all;
      }
    }
  }
}

.timeline-section {
  background-color: #fff;
  border-radius: 16rpx;
  padding: 30rpx;
}

.section-header {
  display: flex;
  align-items: center;
  margin-bottom: 30rpx;

  .section-icon {
    position: relative;
    width: 22rpx;
    height: 22rpx;
    margin-right: 10rpx;
    flex-shrink: 0;

    .section-icon-ring {
      width: 100%;
      height: 100%;
      border: 2rpx solid #c4121b;
      border-radius: 50%;
      box-sizing: border-box;
      background: #fff;
    }

    .section-icon-dot {
      position: absolute;
      left: 50%;
      top: 50%;
      width: 8rpx;
      height: 8rpx;
      margin-left: -4rpx;
      margin-top: -4rpx;
      border-radius: 50%;
      background: #c4121b;
      box-shadow: 0 0 0 4rpx rgba(196, 18, 27, 0.08);
    }
  }

  .section-title {
    font-size: 30rpx;
    font-weight: 600;
    color: #333;
    margin-left: 10rpx;
  }
}

.loading-wrap {
  padding: 60rpx 0;
  text-align: center;
}

.timeline-wrap {
  .timeline-list {
    padding-left: 10rpx;
  }

  .timeline-item {
    display: flex;
    position: relative;

    &.first {
      .timeline-dot {
        background-color: #AD181F;
        border-color: #AD181F;
      }

      .timeline-title {
        color: #AD181F;
      }
    }
  }

  .timeline-left {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-right: 20rpx;
  }

  .timeline-dot {
    width: 32rpx;
    height: 32rpx;
    border-radius: 50%;
    border: 2px solid #ccc;
    background-color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    z-index: 1;
  }

  .timeline-check {
    color: #fff;
    font-size: 20rpx;
    font-weight: 700;
    line-height: 1;
  }

  .timeline-line {
    width: 2rpx;
    flex: 1;
    background-color: #e0e0e0;
    margin: 10rpx 0;
    min-height: 64rpx;
  }

  .timeline-right {
    flex: 1;
    padding-bottom: 40rpx;
  }

  .timeline-content {
    .timeline-title {
      font-size: 28rpx;
      color: #333;
      font-weight: 500;
    }

    .timeline-time {
      font-size: 24rpx;
      color: #999;
      margin-top: 8rpx;
    }

    .timeline-desc {
      font-size: 26rpx;
      color: #666;
      margin-top: 12rpx;
      line-height: 1.6;
      background-color: #fafafa;
      padding: 16rpx;
      border-radius: 8rpx;
    }
  }
}

.nodata {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 80rpx 0;

  image {
    width: 200rpx;
    height: 200rpx;
  }

  text {
    font-size: 28rpx;
    color: #999;
    margin-top: 20rpx;
  }
}
</style>