deotalandAi/apps/frontend/src/components/LogisticsTimeline.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>