585 lines
14 KiB
Vue
585 lines
14 KiB
Vue
<template>
|
|
<div class="logistics-timeline">
|
|
<div class="timeline-header">
|
|
<h3>{{ $t('logistics.title') }}</h3>
|
|
<span class="tracking-number">{{ $t('logistics.trackingNumber') }}: {{ trackingNumber }}</span>
|
|
</div>
|
|
|
|
<el-timeline>
|
|
<el-timeline-item
|
|
v-for="(event, index) in timelineEvents"
|
|
:key="index"
|
|
:timestamp="formatDate(event.timestamp)"
|
|
:color="getEventColor(event.type)"
|
|
:icon="getEventIcon(event.type)"
|
|
:type="getEventType(event.type)"
|
|
:size="index === 0 ? 'large' : 'normal'"
|
|
placement="top"
|
|
>
|
|
<div class="timeline-event" :class="getEventClass(event.type)">
|
|
<div class="event-header">
|
|
<span class="event-title">{{ getEventTitle(event.type) }}</span>
|
|
<el-tag :type="getStatusTagType(event.type)" size="small">
|
|
{{ getEventStatus(event.type) }}
|
|
</el-tag>
|
|
</div>
|
|
<div class="event-content">
|
|
<p class="event-description">{{ event.description }}</p>
|
|
|
|
<div v-if="event.operator" class="event-operator">
|
|
<el-icon><User /></el-icon>
|
|
<span>{{ event.operator }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</el-timeline-item>
|
|
</el-timeline>
|
|
|
|
<!-- 物流信息卡片 -->
|
|
<div class="logistics-info">
|
|
<div class="info-card">
|
|
<div class="card-header">
|
|
<el-icon><Van /></el-icon>
|
|
<span>{{ $t('logistics.carrierInfo') }}</span>
|
|
</div>
|
|
<div class="card-content">
|
|
<div class="info-row">
|
|
<span class="label">{{ $t('logistics.carrier') }}:</span>
|
|
<span class="value">{{ logisticsInfo.carrier || '顺丰速运' }}</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="label">{{ $t('logistics.service') }}:</span>
|
|
<span class="value">{{ logisticsInfo.service || '标准快递' }}</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="label">{{ $t('logistics.estimatedDelivery') }}:</span>
|
|
<span class="value">{{ formatDate(logisticsInfo.estimatedDelivery) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="info-card">
|
|
<div class="card-header">
|
|
<el-icon><MapLocation /></el-icon>
|
|
<span>{{ $t('logistics.currentLocation') }}</span>
|
|
</div>
|
|
<div class="card-content">
|
|
<div class="location-info">
|
|
<p class="current-address">{{ logisticsInfo.currentLocation || '北京市朝阳区分拣中心' }}</p>
|
|
<div class="status-update">
|
|
<el-icon><Clock /></el-icon>
|
|
<span>{{ $t('logistics.lastUpdate') }}: {{ formatDate(logisticsInfo.lastUpdate) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed } from 'vue'
|
|
import {
|
|
User,
|
|
Van,
|
|
MapLocation,
|
|
Clock,
|
|
Check,
|
|
Loading,
|
|
Goods,
|
|
Box,
|
|
House,
|
|
CircleCheck
|
|
} from '@element-plus/icons-vue'
|
|
import { ORDER_STATUS } from '@/stores/orders'
|
|
import dayjs from 'dayjs'
|
|
|
|
const props = defineProps({
|
|
orderId: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
trackingNumber: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
timelineEvents: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
logisticsInfo: {
|
|
type: Object,
|
|
default: () => ({})
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits(['refresh'])
|
|
|
|
// 物流事件类型映射
|
|
const EVENT_TYPES = {
|
|
ORDER_CREATED: 'order_created',
|
|
PAYMENT_CONFIRMED: 'payment_confirmed',
|
|
PROCESSING: 'processing',
|
|
SHIPPED: 'shipped',
|
|
IN_TRANSIT: 'in_transit',
|
|
OUT_FOR_DELIVERY: 'out_for_delivery',
|
|
delivered: 'Shipped',
|
|
EXCEPTION: 'exception'
|
|
}
|
|
|
|
// 格式化日期
|
|
const formatDate = (dateString) => {
|
|
if (!dateString) return ''
|
|
return dayjs(dateString).format('YYYY-MM-DD HH:mm')
|
|
}
|
|
|
|
// 获取事件颜色
|
|
const getEventColor = (eventType) => {
|
|
// 检查是否为暗色主题
|
|
const isDarkMode = document.documentElement.classList.contains('dark')
|
|
|
|
// 暗色主题下的颜色映射
|
|
const darkColorMap = {
|
|
[EVENT_TYPES.ORDER_CREATED]: '#9CA3AF',
|
|
[EVENT_TYPES.PAYMENT_CONFIRMED]: '#60A5FA',
|
|
[EVENT_TYPES.PROCESSING]: '#FBBF24',
|
|
[EVENT_TYPES.SHIPPED]: '#A78BFA',
|
|
[EVENT_TYPES.IN_TRANSIT]: '#34D399',
|
|
[EVENT_TYPES.OUT_FOR_DELIVERY]: '#FB923C',
|
|
[EVENT_TYPES.DELIVERED]: '#4ADE80',
|
|
[EVENT_TYPES.EXCEPTION]: '#F87171'
|
|
}
|
|
|
|
// 浅色主题下的颜色映射
|
|
const lightColorMap = {
|
|
[EVENT_TYPES.ORDER_CREATED]: '#909399',
|
|
[EVENT_TYPES.PAYMENT_CONFIRMED]: '#409EFF',
|
|
[EVENT_TYPES.PROCESSING]: '#E6A23C',
|
|
[EVENT_TYPES.SHIPPED]: '#8B5CF6',
|
|
[EVENT_TYPES.IN_TRANSIT]: '#00BFA6',
|
|
[EVENT_TYPES.OUT_FOR_DELIVERY]: '#FF9800',
|
|
[EVENT_TYPES.DELIVERED]: '#4CAF50',
|
|
[EVENT_TYPES.EXCEPTION]: '#F44336'
|
|
}
|
|
|
|
return isDarkMode ? (darkColorMap[eventType] || '#9CA3AF') : (lightColorMap[eventType] || '#909399')
|
|
}
|
|
|
|
// 获取事件图标
|
|
const getEventIcon = (eventType) => {
|
|
const iconMap = {
|
|
[EVENT_TYPES.ORDER_CREATED]: Check,
|
|
[EVENT_TYPES.PAYMENT_CONFIRMED]: CircleCheck,
|
|
[EVENT_TYPES.PROCESSING]: Loading,
|
|
[EVENT_TYPES.SHIPPED]: Goods,
|
|
[EVENT_TYPES.IN_TRANSIT]: Van,
|
|
[EVENT_TYPES.OUT_FOR_DELIVERY]: Box,
|
|
[EVENT_TYPES.DELIVERED]: House,
|
|
[EVENT_TYPES.EXCEPTION]: CircleCheck
|
|
}
|
|
return iconMap[eventType] || CircleCheck
|
|
}
|
|
|
|
// 获取事件类型
|
|
const getEventType = (eventType) => {
|
|
const typeMap = {
|
|
[EVENT_TYPES.DELIVERED]: 'success',
|
|
[EVENT_TYPES.EXCEPTION]: 'danger',
|
|
[EVENT_TYPES.SHIPPED]: 'primary',
|
|
[EVENT_TYPES.PROCESSING]: 'warning'
|
|
}
|
|
return typeMap[eventType] || 'info'
|
|
}
|
|
|
|
// 获取事件标题
|
|
const getEventTitle = (eventType) => {
|
|
const titleMap = {
|
|
[EVENT_TYPES.ORDER_CREATED]: '订单已创建',
|
|
[EVENT_TYPES.PAYMENT_CONFIRMED]: '支付确认',
|
|
[EVENT_TYPES.PROCESSING]: '订单处理中',
|
|
[EVENT_TYPES.SHIPPED]: '已发货',
|
|
[EVENT_TYPES.IN_TRANSIT]: '运输中',
|
|
[EVENT_TYPES.OUT_FOR_DELIVERY]: '派送中',
|
|
[EVENT_TYPES.DELIVERED]: '已送达',
|
|
[EVENT_TYPES.EXCEPTION]: '异常状态'
|
|
}
|
|
return titleMap[eventType] || '未知状态'
|
|
}
|
|
|
|
// 获取事件状态
|
|
const getEventStatus = (eventType) => {
|
|
const statusMap = {
|
|
[EVENT_TYPES.ORDER_CREATED]: '已完成',
|
|
[EVENT_TYPES.PAYMENT_CONFIRMED]: '已完成',
|
|
[EVENT_TYPES.PROCESSING]: '进行中',
|
|
[EVENT_TYPES.SHIPPED]: '已完成',
|
|
[EVENT_TYPES.IN_TRANSIT]: '进行中',
|
|
[EVENT_TYPES.OUT_FOR_DELIVERY]: '进行中',
|
|
[EVENT_TYPES.DELIVERED]: '已完成',
|
|
[EVENT_TYPES.EXCEPTION]: '需处理'
|
|
}
|
|
return statusMap[eventType] || '未知'
|
|
}
|
|
|
|
// 获取事件样式类
|
|
const getEventClass = (eventType) => {
|
|
const classMap = {
|
|
[EVENT_TYPES.ORDER_CREATED]: 'completed',
|
|
[EVENT_TYPES.PAYMENT_CONFIRMED]: 'completed',
|
|
[EVENT_TYPES.PROCESSING]: 'processing',
|
|
[EVENT_TYPES.SHIPPED]: 'completed',
|
|
[EVENT_TYPES.IN_TRANSIT]: 'processing',
|
|
[EVENT_TYPES.OUT_FOR_DELIVERY]: 'processing',
|
|
[EVENT_TYPES.DELIVERED]: 'completed',
|
|
[EVENT_TYPES.EXCEPTION]: 'pending'
|
|
}
|
|
return classMap[eventType] || 'pending'
|
|
}
|
|
|
|
// 获取状态标签类型
|
|
const getStatusTagType = (eventType) => {
|
|
const typeMap = {
|
|
[EVENT_TYPES.ORDER_CREATED]: 'info',
|
|
[EVENT_TYPES.PAYMENT_CONFIRMED]: 'success',
|
|
[EVENT_TYPES.PROCESSING]: 'warning',
|
|
[EVENT_TYPES.SHIPPED]: 'primary',
|
|
[EVENT_TYPES.IN_TRANSIT]: 'warning',
|
|
[EVENT_TYPES.OUT_FOR_DELIVERY]: 'warning',
|
|
[EVENT_TYPES.DELIVERED]: 'success',
|
|
[EVENT_TYPES.EXCEPTION]: 'danger'
|
|
}
|
|
return typeMap[eventType] || 'info'
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.logistics-timeline {
|
|
padding: 20px;
|
|
border-radius: var(--radius, 8px);
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.timeline-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 24px;
|
|
padding-bottom: 16px;
|
|
border-bottom: 1px solid var(--sidebar-border, #e5e7eb);
|
|
}
|
|
|
|
.timeline-header h3 {
|
|
margin: 0;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: var(--text-color, #1F2937);
|
|
}
|
|
|
|
.tracking-number {
|
|
font-size: 14px;
|
|
color: var(--sidebar-text-secondary, #6b7280);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.timeline-event {
|
|
padding: 8px 0;
|
|
}
|
|
|
|
.event-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.event-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: var(--text-color, #1F2937);
|
|
}
|
|
|
|
.event-content {
|
|
padding-left: 4px;
|
|
}
|
|
|
|
.event-description {
|
|
margin: 0 0 8px 0;
|
|
color: var(--sidebar-text-secondary, #6b7280);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.event-location,
|
|
.event-operator {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
margin-bottom: 4px;
|
|
font-size: 14px;
|
|
color: var(--sidebar-text-secondary, #6b7280);
|
|
}
|
|
|
|
.logistics-info {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 16px;
|
|
margin-top: 24px;
|
|
padding-top: 24px;
|
|
border-top: 1px solid var(--sidebar-border, #e5e7eb);
|
|
}
|
|
|
|
.info-card {
|
|
background: #fff;
|
|
border-radius: 6px;
|
|
padding: 16px;
|
|
border: 1px solid var(--sidebar-border, #e5e7eb);
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 12px;
|
|
font-weight: 600;
|
|
color: var(--text-color, #1F2937);
|
|
}
|
|
|
|
.card-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.info-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.info-row .label {
|
|
color: var(--sidebar-text-secondary, #6b7280);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.info-row .value {
|
|
color: var(--text-color, #1F2937);
|
|
font-weight: 500;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.location-info .current-address {
|
|
margin: 0 0 8px 0;
|
|
font-weight: 500;
|
|
color: var(--text-color, #1F2937);
|
|
}
|
|
|
|
.status-update {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
font-size: 12px;
|
|
color: var(--sidebar-text-secondary, #6b7280);
|
|
}
|
|
|
|
/* 暗色主题适配 */
|
|
:root.dark .logistics-timeline {
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
:root.dark .timeline-header {
|
|
border-bottom-color: var(--sidebar-border, #374151);
|
|
}
|
|
|
|
:root.dark .timeline-header h3 {
|
|
color: var(--text-color, #F3F4F6);
|
|
}
|
|
|
|
:root.dark .tracking-number {
|
|
color: var(--sidebar-text-secondary, #9ca3af);
|
|
}
|
|
|
|
:root.dark .event-title {
|
|
color: var(--text-color, #F3F4F6);
|
|
}
|
|
|
|
:root.dark .event-description {
|
|
color: var(--sidebar-text-secondary, #9ca3af);
|
|
}
|
|
|
|
:root.dark .event-location,
|
|
:root.dark .event-operator {
|
|
color: var(--sidebar-text-secondary, #9ca3af);
|
|
}
|
|
|
|
:root.dark .logistics-info {
|
|
border-top-color: var(--sidebar-border, #374151);
|
|
}
|
|
|
|
:root.dark .info-card {
|
|
background: #111827;
|
|
border-color: var(--sidebar-border, #374151);
|
|
}
|
|
|
|
:root.dark .card-header {
|
|
color: var(--text-color, #F3F4F6);
|
|
}
|
|
|
|
:root.dark .info-row .label {
|
|
color: var(--sidebar-text-secondary, #9ca3af);
|
|
}
|
|
|
|
:root.dark .info-row .value {
|
|
color: var(--text-color, #F3F4F6);
|
|
}
|
|
|
|
:root.dark .location-info .current-address {
|
|
color: var(--text-color, #F3F4F6);
|
|
}
|
|
|
|
:root.dark .status-update {
|
|
color: var(--sidebar-text-secondary, #9ca3af);
|
|
}
|
|
|
|
/* Element Plus Timeline 组件暗色主题适配 */
|
|
:root.dark .el-timeline-item__tail {
|
|
border-left: 2px solid var(--sidebar-border, #374151);
|
|
}
|
|
|
|
:root.dark .el-timeline-item__node {
|
|
background-color: #111827;
|
|
border: 2px solid var(--sidebar-border, #374151);
|
|
}
|
|
|
|
:root.dark .el-timeline-item__wrapper {
|
|
padding-left: 28px;
|
|
}
|
|
|
|
:root.dark .el-timeline-item__timestamp {
|
|
color: var(--sidebar-text-secondary, #9ca3af);
|
|
}
|
|
|
|
:root.dark .el-timeline-item__content {
|
|
color: var(--text-color, #F3F4F6);
|
|
}
|
|
|
|
/* 时间线节点样式优化 */
|
|
.timeline-event.completed .event-icon {
|
|
background-color: var(--el-color-primary, #8B5CF6);
|
|
color: white;
|
|
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.1);
|
|
}
|
|
|
|
.timeline-event.processing .event-icon {
|
|
background-color: var(--el-color-primary, #8B5CF6);
|
|
color: white;
|
|
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.2);
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
.timeline-event.pending .event-icon {
|
|
background-color: var(--sidebar-border, #374151);
|
|
color: var(--sidebar-text-secondary, #9ca3af);
|
|
}
|
|
|
|
/* 暗色主题下的时间线节点样式 */
|
|
:root.dark .timeline-event.completed .event-icon {
|
|
background-color: var(--el-color-primary, #8B5CF6);
|
|
color: white;
|
|
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.2);
|
|
}
|
|
|
|
:root.dark .timeline-event.processing .event-icon {
|
|
background-color: var(--el-color-primary, #8B5CF6);
|
|
color: white;
|
|
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.3);
|
|
}
|
|
|
|
:root.dark .timeline-event.pending .event-icon {
|
|
background-color: var(--sidebar-border, #374151);
|
|
color: var(--sidebar-text-secondary, #9ca3af);
|
|
}
|
|
|
|
/* Element Plus Tag 组件暗色主题适配 */
|
|
:root.dark .el-tag {
|
|
background-color: #111827;
|
|
border-color: var(--sidebar-border, #374151);
|
|
color: var(--sidebar-text-secondary, #9ca3af);
|
|
}
|
|
|
|
:root.dark .el-tag.el-tag--primary {
|
|
background-color: rgba(139, 92, 246, 0.2);
|
|
border-color: rgba(139, 92, 246, 0.5);
|
|
color: #A78BFA;
|
|
}
|
|
|
|
:root.dark .el-tag.el-tag--success {
|
|
background-color: rgba(16, 185, 129, 0.2);
|
|
border-color: rgba(16, 185, 129, 0.5);
|
|
color: #10B981;
|
|
}
|
|
|
|
:root.dark .el-tag.el-tag--warning {
|
|
background-color: rgba(245, 158, 11, 0.2);
|
|
border-color: rgba(245, 158, 11, 0.5);
|
|
color: #F59E0B;
|
|
}
|
|
|
|
:root.dark .el-tag.el-tag--danger {
|
|
background-color: rgba(239, 68, 68, 0.2);
|
|
border-color: rgba(239, 68, 68, 0.5);
|
|
color: #EF4444;
|
|
}
|
|
|
|
:root.dark .el-tag.el-tag--info {
|
|
background-color: rgba(107, 114, 128, 0.2);
|
|
border-color: rgba(107, 114, 128, 0.5);
|
|
color: #6B7280;
|
|
}
|
|
|
|
/* Element Plus Icon 组件暗色主题适配 */
|
|
:root.dark .el-icon {
|
|
color: var(--sidebar-text-secondary, #9ca3af);
|
|
}
|
|
|
|
/* 响应式设计 */
|
|
@media (max-width: 768px) {
|
|
.logistics-timeline {
|
|
padding: 16px;
|
|
}
|
|
|
|
.timeline-header {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
}
|
|
|
|
.timeline-header h3 {
|
|
font-size: 16px;
|
|
}
|
|
|
|
.event-header {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 4px;
|
|
}
|
|
|
|
.logistics-info {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
/* 脉冲动画 */
|
|
@keyframes pulse {
|
|
0% {
|
|
transform: scale(1);
|
|
box-shadow: 0 0 0 0 rgba(139, 92, 246, 0.4);
|
|
}
|
|
50% {
|
|
transform: scale(1.05);
|
|
box-shadow: 0 0 0 6px rgba(139, 92, 246, 0.1);
|
|
}
|
|
100% {
|
|
transform: scale(1);
|
|
box-shadow: 0 0 0 0 rgba(139, 92, 246, 0);
|
|
}
|
|
}
|
|
</style> |