|
|
@@ -1,7 +1,7 @@
|
|
|
<script setup lang="ts">
|
|
|
import type { SystemCwModel } from '@/model/care.model';
|
|
|
import type { ApplyRecordModel } from '@/model/order.model';
|
|
|
-import { PlayCircleOutlined } from '@ant-design/icons-vue';
|
|
|
+import { DownOutlined, PlayCircleOutlined, UpOutlined } from '@ant-design/icons-vue';
|
|
|
import { computed, h, onMounted, ref } from 'vue';
|
|
|
import { notification } from 'ant-design-vue';
|
|
|
import VxeUI from 'vxe-table';
|
|
|
@@ -55,6 +55,80 @@ const mockLogisticsData = computed(() => {
|
|
|
recipientAddress: (props.data.provinceName !== null ? props.data.provinceName + ' ' : '') + (props.data.cityName !== null ? props.data.cityName + ' ' : '') + (props.data.areaName !== null ? props.data.areaName + ' ' : '') + (props.data.detailAddress !== null ? props.data.detailAddress : '')
|
|
|
};
|
|
|
});
|
|
|
+type LogisticsTimelineItem = {
|
|
|
+ title: string;
|
|
|
+ time: string;
|
|
|
+ detail: string;
|
|
|
+};
|
|
|
+
|
|
|
+const showAllLogistics = ref(false);
|
|
|
+const mockLogisticsTimeline: LogisticsTimelineItem[] = [
|
|
|
+ {
|
|
|
+ title: '已签收',
|
|
|
+ time: '01-09 19:15',
|
|
|
+ detail: '您已在杭州金成花园15幢4号后完成取件,感谢使用菜鸟驿站,期待再次为您服务。',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '待取件',
|
|
|
+ time: '01-09 16:11',
|
|
|
+ detail: '您的快件已暂存在杭州金成花园15幢4号店菜鸟驿站,请凭取货码及时领取。如有疑问请联系15067159925',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '运输中',
|
|
|
+ time: '01-09 13:26',
|
|
|
+ detail: '快件离开杭州分拨中心,正在发往目的地站点。',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '包裹已揽收',
|
|
|
+ time: '01-09 09:42',
|
|
|
+ detail: '快递员已揽收您的包裹,正在安排发出。',
|
|
|
+ },
|
|
|
+];
|
|
|
+
|
|
|
+function normalizeLogisticsItem(item: any): LogisticsTimelineItem | null {
|
|
|
+ if (!item || typeof item !== 'object') return null;
|
|
|
+ const time = String(item.time ?? item.acceptTime ?? item.ftime ?? item.createTime ?? item.updateTime ?? '').trim();
|
|
|
+ const title = String(item.title ?? item.status ?? item.statusName ?? item.statusDesc ?? '').trim();
|
|
|
+ const detail = String(item.content ?? item.context ?? item.remark ?? item.description ?? item.message ?? '').trim();
|
|
|
+ const fallbackTitle = detail || title;
|
|
|
+ if (!time && !fallbackTitle && !detail) return null;
|
|
|
+ return {
|
|
|
+ title: fallbackTitle || '-',
|
|
|
+ time,
|
|
|
+ detail: detail && detail !== fallbackTitle ? detail : '',
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+const logisticsTimeline = computed<LogisticsTimelineItem[]>(() => {
|
|
|
+ const source = props.data as any;
|
|
|
+ const listCandidate =
|
|
|
+ source?.expressDetails ??
|
|
|
+ source?.logisticsDetails ??
|
|
|
+ source?.logisticsList ??
|
|
|
+ source?.expressRouteList ??
|
|
|
+ source?.expressRoutes ??
|
|
|
+ source?.traces ??
|
|
|
+ source?.traceList ??
|
|
|
+ source?.routeList ??
|
|
|
+ source?.tracks ??
|
|
|
+ [];
|
|
|
+ if (!Array.isArray(listCandidate)) return mockLogisticsTimeline;
|
|
|
+ const normalizedList = listCandidate
|
|
|
+ .map((item: any) => normalizeLogisticsItem(item))
|
|
|
+ .filter((item): item is LogisticsTimelineItem => Boolean(item));
|
|
|
+ return normalizedList.length > 0 ? normalizedList : mockLogisticsTimeline;
|
|
|
+});
|
|
|
+
|
|
|
+const visibleLogisticsTimeline = computed(() => {
|
|
|
+ if (showAllLogistics.value) return logisticsTimeline.value;
|
|
|
+ return logisticsTimeline.value.slice(0, 2);
|
|
|
+});
|
|
|
+
|
|
|
+const canExpandLogistics = computed(() => logisticsTimeline.value.length > 2);
|
|
|
+
|
|
|
+function toggleLogisticsExpand() {
|
|
|
+ showAllLogistics.value = !showAllLogistics.value;
|
|
|
+}
|
|
|
const expressTypeText: Record<string, string> = {
|
|
|
'0': '邮政速递',
|
|
|
'1': '顺丰速运',
|
|
|
@@ -283,10 +357,48 @@ onMounted(async () => {
|
|
|
<!-- 物流信息 -->
|
|
|
<div class="logistics-content" v-if="data.receiptType === '0'">
|
|
|
<template v-if="mockPackageItems.length > 0">
|
|
|
+ <div class="flex align-items-center justify-between">
|
|
|
<div class="logistics-tracking">
|
|
|
<span class="tracking-number">{{ mockLogisticsData.trackingNumber }}</span>
|
|
|
<a @click="handleCopyTracking" class="copy-link">复制</a>
|
|
|
</div>
|
|
|
+ <div class="text-gray-500">联系电话:9554</div>
|
|
|
+ </div>
|
|
|
+ <!-- 物流详情 -->
|
|
|
+ <div class="logistics-detail">
|
|
|
+ <template v-if="logisticsTimeline.length > 0">
|
|
|
+ <div class="logistics-timeline">
|
|
|
+ <div class="timeline-item" v-for="(item, index) in visibleLogisticsTimeline" :key="`${item.time}-${index}`">
|
|
|
+ <div class="timeline-dot-wrap">
|
|
|
+ <span class="timeline-dot" :class="{ active: index === 0 }" />
|
|
|
+ <span class="timeline-line" v-if="index !== visibleLogisticsTimeline.length - 1 || canExpandLogistics" />
|
|
|
+ </div>
|
|
|
+ <div class="timeline-content">
|
|
|
+ <div class="timeline-head">
|
|
|
+ <span class="timeline-title" :class="{ active: index === 0 }">{{ item.title || '-' }}</span>
|
|
|
+ <span class="timeline-time" :class="{ active: index === 0 }">{{ item.time || '-' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="timeline-detail" v-if="item.detail">{{ item.detail }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="timeline-item timeline-expand-item" v-if="canExpandLogistics">
|
|
|
+ <div class="timeline-dot-wrap">
|
|
|
+ <span class="timeline-dot" />
|
|
|
+ </div>
|
|
|
+ <div class="timeline-content timeline-expand-content">
|
|
|
+ <a @click="toggleLogisticsExpand" class="expand-link">
|
|
|
+ {{ showAllLogistics ? '收起物流明细' : '展开更多物流明细' }}
|
|
|
+ <UpOutlined v-if="showAllLogistics" class="expand-arrow" />
|
|
|
+ <DownOutlined v-else class="expand-arrow" />
|
|
|
+ </a>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <div class="logistics-empty">暂无物流轨迹</div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
<div class="logistics-recipient">
|
|
|
<span class="receive-icon">收</span>
|
|
|
<span class="recipient-info">
|
|
|
@@ -805,6 +917,120 @@ onMounted(async () => {
|
|
|
text-decoration: underline;
|
|
|
}
|
|
|
|
|
|
+ .logistics-detail {
|
|
|
+ max-width: 760px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .logistics-timeline {
|
|
|
+ margin-top: 2px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .timeline-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .timeline-dot-wrap {
|
|
|
+ width: 14px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ flex-shrink: 0;
|
|
|
+ padding-top: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .timeline-dot {
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #d9d9d9;
|
|
|
+ }
|
|
|
+
|
|
|
+ .timeline-dot.active {
|
|
|
+ background: #ff7a45;
|
|
|
+ }
|
|
|
+
|
|
|
+ .timeline-line {
|
|
|
+ width: 1px;
|
|
|
+ min-height: 44px;
|
|
|
+ background: #e8e8e8;
|
|
|
+ margin-top: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .timeline-content {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ padding-bottom: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .timeline-head {
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+ gap: 8px;
|
|
|
+ line-height: 22px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .timeline-title {
|
|
|
+ color: #666;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .timeline-title.active {
|
|
|
+ color: #ff7a45;
|
|
|
+ }
|
|
|
+
|
|
|
+ .timeline-time {
|
|
|
+ color: #8c8c8c;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .timeline-time.active {
|
|
|
+ color: #ff7a45;
|
|
|
+ }
|
|
|
+
|
|
|
+ .timeline-detail {
|
|
|
+ margin-top: 2px;
|
|
|
+ line-height: 22px;
|
|
|
+ color: #8c8c8c;
|
|
|
+ font-size: 14px;
|
|
|
+ word-break: break-all;
|
|
|
+ }
|
|
|
+
|
|
|
+ .timeline-expand-item {
|
|
|
+ margin-top: -2px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .timeline-expand-content {
|
|
|
+ padding-bottom: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .expand-link {
|
|
|
+ color: #666;
|
|
|
+ font-size: 14px;
|
|
|
+ text-decoration: none;
|
|
|
+ cursor: pointer;
|
|
|
+ user-select: none;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .expand-link:hover {
|
|
|
+ color: #1890ff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .expand-arrow {
|
|
|
+ margin-left: 4px;
|
|
|
+ font-size: 10px;
|
|
|
+ transform: translateY(1px);
|
|
|
+ }
|
|
|
+
|
|
|
+ .logistics-empty {
|
|
|
+ color: #999;
|
|
|
+ line-height: 22px;
|
|
|
+ }
|
|
|
+
|
|
|
.logistics-recipient {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|