Files
xzt_data_screen/src/views/ExecutionScreenV2.vue

183 lines
19 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.
<script setup lang="ts">
import { computed } from 'vue'
import FreightIcon from '@/components/base/FreightIcon.vue'
import RouteExecutionMap from '@/components/map/RouteExecutionMap.vue'
import TechPanel from '@/components/base/TechPanel.vue'
import type {
FreightOrder, FreightRoute, MapScene, PlatformSnapshot, RiskAlert, Vehicle,
} from '@/types/freight'
const props = defineProps<{
snapshot: PlatformSnapshot
routes: FreightRoute[]
orders: readonly FreightOrder[]
vehicles: readonly Vehicle[]
alerts: readonly RiskAlert[]
mapScene: MapScene
selectedRouteId: string
}>()
const emit = defineEmits<{ selectRoute: [routeId: string] }>()
const selectedRoute = computed(() => props.routes.find((route) => route.id === props.selectedRouteId) ?? props.routes[0])
const selectedOrder = computed(() => props.orders.find((order) => order.routeId === props.selectedRouteId) ?? props.orders[0])
const selectedVehicle = computed(() => props.vehicles.find((vehicle) => vehicle.id === selectedOrder.value.vehicleId) ?? props.vehicles[0])
const topMetrics = computed(() => [
{ label: '当前在途运单', value: props.snapshot.inTransitOrders.toLocaleString(), unit: '单', icon: 'route' as const, tone: 'cyan' as const },
{ label: '当前在途车辆', value: props.snapshot.inTransitVehicles.toLocaleString(), unit: '辆', icon: 'truck' as const, tone: 'cyan' as const },
{ label: '准时送达率', value: props.snapshot.onTimeRate.toFixed(1), unit: '%', icon: 'clock' as const, tone: 'green' as const },
{ label: '轨迹完整率', value: props.snapshot.trackIntegrityRate.toFixed(1), unit: '%', icon: 'location' as const, tone: 'green' as const },
])
const milestones = [
{ label: '调度完成', time: '08:36', done: true },
{ label: '到厂装货', time: '09:18', done: true },
{ label: '驶离围栏', time: '10:06', done: true },
{ label: '运输在途', time: '进行中', done: true },
{ label: '到达卸货', time: '预计 22:40', done: false },
]
function formatTime(value: string) {
return new Date(value).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}
function alertRoute(routeId: string) {
const route = props.routes.find((item) => item.id === routeId)
return route ? `${route.from.name}→${route.to.name}` : ''
}
</script>
<template>
<section class="execution-v2">
<div class="execution-kpis">
<article v-for="item in topMetrics" :key="item.label">
<FreightIcon :kind="item.icon" :tone="item.tone" :size="45" />
<div><span>{{ item.label }}</span><Transition name="metric-flip" mode="out-in"><strong :key="item.value">{{ item.value }}</strong></Transition><small>{{ item.unit }}</small></div>
</article>
</div>
<div class="execution-layout">
<aside class="execution-side execution-side--left">
<article class="waybill-photo">
<img src="@/assets/stock/coal-road-truck.jpg" alt="满载煤炭的公路运输车辆" />
<div class="waybill-photo__shade" />
<header><span>当前关注运单</span><b>{{ selectedOrder.id }}</b></header>
<div class="waybill-photo__route">{{ selectedRoute.from.name }} <i>→</i> {{ selectedRoute.to.name }}</div>
<div class="waybill-photo__info">
<div><span>货物</span><b>{{ selectedOrder.cargo }}</b></div>
<div><span>重量</span><b>{{ selectedOrder.weight }} 吨</b></div>
<div><span>里程</span><b>{{ selectedRoute.distance }} km</b></div>
<div><span>预计到达</span><b>{{ formatTime(selectedOrder.eta) }}</b></div>
</div>
</article>
<TechPanel title="运输节点" tag="MILESTONES">
<div class="milestones">
<div v-for="(item,index) in milestones" :key="item.label" :class="{ pending: !item.done }">
<i><b /></i>
<span>{{ item.label }}</span><time>{{ item.time }}</time>
<em v-if="index < milestones.length-1" />
</div>
</div>
</TechPanel>
<TechPanel title="车辆与司机" tag="VEHICLE & DRIVER">
<div class="vehicle-driver">
<div class="vehicle-driver__plate"><FreightIcon kind="truck" :size="48" /><strong>{{ selectedVehicle.plate }}</strong><span>{{ selectedVehicle.type }} · 核载 {{ selectedVehicle.load }}t</span></div>
<div class="vehicle-driver__facts">
<div><span>司机</span><b>王海峰</b></div><div><span>安全评分</span><b>96.8</b></div>
<div><span>连续驾驶</span><b>2h 18m</b></div><div><span>车辆状态</span><b class="normal">正常</b></div>
<div><span>当前载重</span><b>{{ selectedOrder.weight }}t</b></div><div><span>剩余里程</span><b>{{ Math.round(selectedRoute.distance * (1 - selectedVehicle.progress)) }}km</b></div>
</div>
<div class="vehicle-driver__progress"><span>当前行程</span><i><b :style="{ width: `${Math.round(selectedVehicle.progress * 100)}%` }" /></i><strong>{{ Math.round(selectedVehicle.progress * 100) }}%</strong></div>
</div>
</TechPanel>
</aside>
<main class="execution-center">
<div class="execution-map">
<div class="execution-map__title">在途运输监控 / IN-TRANSIT MONITORING</div>
<RouteExecutionMap :scene="mapScene" />
<div class="execution-map__status">
<span><i />GPS 在线</span><span>最近定位 12 秒前</span><span>当前速度 <b>78 km/h</b></span>
</div>
</div>
<div class="execution-bottom">
<TechPanel title="单票成本与毛利" tag="ORDER PROFIT">
<div class="profit-summary">
<div><span>运费收入</span><b>¥{{ selectedOrder.fee.toLocaleString() }}</b></div>
<i>−</i>
<div><span>预计运输成本</span><b>¥{{ Math.round(selectedOrder.fee * .82).toLocaleString() }}</b></div>
<i>=</i>
<div class="profit"><span>预计毛利</span><b>¥{{ Math.round(selectedOrder.fee * .18).toLocaleString() }}</b></div>
</div>
<div class="cost-parts">
<div v-for="item in [['司机运费',57],['燃油能源',21],['路桥费用',14],['其他费用',8]]" :key="String(item[0])"><span>{{ item[0] }}</span><i><b :style="{ width: `${item[1]}%` }" /></i><strong>{{ item[1] }}%</strong></div>
</div>
</TechPanel>
<TechPanel title="线路履约表现" tag="LANE PERFORMANCE">
<div class="lane-performance">
<div><FreightIcon kind="clock" tone="green" :size="46" /><span>准时送达</span><b>{{ selectedRoute.punctuality }}%</b><small>行业均值 92.8%</small><i><em :style="{ width: `${selectedRoute.punctuality}%` }" /></i></div>
<div><FreightIcon kind="route" :size="46" /><span>平均时效</span><b>18.6h</b><small>较昨日缩短 0.7h</small><i><em style="width: 82%" /></i></div>
<div><FreightIcon kind="money" tone="gold" :size="46" /><span>吨公里运价</span><b>{{ selectedRoute.rate }}元</b><small>线路均价 0.36元</small><i><em :style="{ width: `${Math.min(100, selectedRoute.rate / .5 * 100)}%` }" /></i></div>
</div>
</TechPanel>
</div>
</main>
<aside class="execution-side execution-side--right">
<TechPanel title="在途风险事件" tag="RISK EVENTS">
<div class="risk-headline"><strong>{{ Math.round(snapshot.todayOrders * snapshot.abnormalRate / 100) }}</strong><span>今日异常运单</span><b>{{ snapshot.abnormalRate }}%</b><small>异常率</small></div>
<div class="risk-events">
<button v-for="alert in alerts.slice(0,5)" :key="alert.id" @click="emit('selectRoute', alert.routeId)">
<FreightIcon :kind="alert.type.includes('驾驶') ? 'driver' : alert.type.includes('超速') ? 'speed' : 'warning'" :tone="alert.level === '高' ? 'red' : 'gold'" :size="31" />
<div><b>{{ alert.type }}</b><span>{{ alertRoute(alert.routeId) }} · {{ alert.city }}</span></div>
<time>{{ formatTime(alert.time) }}</time>
</button>
</div>
</TechPanel>
<TechPanel title="履约质量" tag="FULFILLMENT QUALITY">
<div class="quality-metrics">
<div v-for="item in [['准时送达',snapshot.onTimeRate,'clock'],['轨迹完整',snapshot.trackIntegrityRate,'location'],['电子回单',snapshot.receiptRate,'document']]" :key="String(item[0])">
<FreightIcon :kind="item[2] as 'clock' | 'location' | 'document'" :size="36" />
<span>{{ item[0] }}</span><strong>{{ item[1] }}%</strong>
<i><b :style="{ width: `${item[1]}%` }" /></i>
</div>
</div>
</TechPanel>
<article class="cargo-evidence">
<img src="@/assets/stock/coal-truck-loading.png" alt="采煤装载设备向矿用车辆装煤" />
<div class="cargo-evidence__shade" />
<header><FreightIcon kind="document" tone="green" :size="36" /><div><span>矿区装车影像留证</span><b>09:42 自动上传</b></div></header>
<div class="cargo-evidence__footer">
<span>电子围栏内</span><span>重量复核一致</span><span>影像 4 张</span>
</div>
</article>
</aside>
</div>
</section>
</template>
<style scoped>
.execution-v2{height:968px;padding-top:7px;position:relative;z-index:1;animation:executionIn .5s ease both}.execution-kpis{height:82px;display:grid;grid-template-columns:repeat(4,1fr);gap:10px;padding:0 170px}.execution-kpis article{display:flex;align-items:center;justify-content:center;gap:16px;background:linear-gradient(110deg,rgba(6,40,59,.88),rgba(3,17,29,.52));border-bottom:1px solid rgba(25,217,255,.28);clip-path:polygon(0 0,calc(100% - 13px) 0,100% 13px,100% 100%,0 100%)}.execution-kpis span{display:block;color:#7da5b2;font-size:10px}.execution-kpis strong{color:#dffaff;font:600 25px Rajdhani}.execution-kpis small{margin-left:4px;color:#638b98;font-size:8px}.execution-layout{height:858px;margin-top:8px;display:grid;grid-template-columns:320px 1fr 320px;gap:10px}.execution-side{display:grid;gap:9px;min-height:0}.execution-side--left{grid-template-rows:300px 272px 1fr}.execution-side--right{grid-template-rows:310px 260px 1fr}.execution-center{display:grid;grid-template-rows:590px 259px;gap:9px;min-width:0}.execution-map{position:relative;overflow:hidden;border:1px solid rgba(25,217,255,.12);background:rgba(2,15,26,.25)}.execution-map__title{position:absolute;z-index:6;left:17px;top:12px;color:#4ca6bd;font:600 10px Rajdhani;letter-spacing:3px}.execution-map__status{position:absolute;z-index:7;left:20%;right:20%;bottom:13px;height:32px;display:flex;align-items:center;justify-content:space-around;background:rgba(3,20,31,.88);border-top:1px solid rgba(25,217,255,.2);color:#6f98a6;font-size:8px}.execution-map__status i{display:inline-block;width:5px;height:5px;margin-right:5px;border-radius:50%;background:#4fe3b1;box-shadow:0 0 6px #4fe3b1}.execution-map__status b{color:#57dfed}.execution-bottom{display:grid;grid-template-columns:1.35fr 1fr;gap:9px;min-height:0}
.waybill-photo{position:relative;overflow:hidden;border:1px solid rgba(25,217,255,.2)}.waybill-photo img{width:100%;height:100%;object-fit:cover;filter:saturate(.5) brightness(.72)}.waybill-photo__shade{position:absolute;inset:0;background:linear-gradient(180deg,rgba(1,11,19,.25),rgba(2,15,25,.94)),linear-gradient(90deg,rgba(5,60,80,.4),transparent)}.waybill-photo header{position:absolute;left:14px;top:13px}.waybill-photo header span{display:block;color:#79a5b3;font-size:8px}.waybill-photo header b{color:#d9f9ff;font:600 12px Rajdhani}.waybill-photo__route{position:absolute;left:14px;top:55px;color:#fff;font-size:19px;letter-spacing:2px;text-shadow:0 0 12px #000}.waybill-photo__route i{color:var(--gold);font-style:normal}.waybill-photo__info{position:absolute;left:10px;right:10px;bottom:10px;display:grid;grid-template-columns:1fr 1fr;background:rgba(3,18,29,.8)}.waybill-photo__info div{padding:9px 10px;border-right:1px solid rgba(25,217,255,.08);border-bottom:1px solid rgba(25,217,255,.08)}.waybill-photo__info span{display:block;color:#6f98a6;font-size:7px}.waybill-photo__info b{color:#d9f8ff;font-size:10px}
.milestones{height:100%;padding:5px 4px}.milestones>div{position:relative;height:41px;display:grid;grid-template-columns:24px 1fr auto;align-items:center;gap:7px}.milestones>div>i{width:15px;height:15px;display:grid;place-items:center;border:1px solid #4fe3b1;border-radius:50%;box-shadow:0 0 7px rgba(79,227,177,.35)}.milestones>div>i b{width:5px;height:5px;border-radius:50%;background:#4fe3b1}.milestones em{position:absolute;left:7px;top:28px;width:1px;height:26px;background:linear-gradient(#4fe3b1,rgba(79,227,177,.1))}.milestones span{font-size:9px}.milestones time{color:#6b95a3;font:9px Rajdhani}.milestones .pending{opacity:.45}.vehicle-driver{height:100%;display:grid;grid-template-columns:1fr 1.2fr;gap:10px;align-items:center}.vehicle-driver__plate{text-align:center}.vehicle-driver__plate :deep(.freight-icon){margin:0 auto 10px}.vehicle-driver__plate strong{display:block;color:#dffaff;font:600 17px Rajdhani}.vehicle-driver__plate span{color:#6e95a2;font-size:7px}.vehicle-driver__facts{display:grid;grid-template-columns:1fr 1fr;gap:12px 8px}.vehicle-driver__facts span{display:block;color:#6e97a4;font-size:7px}.vehicle-driver__facts b{color:#d7f7ff;font-size:9px}.vehicle-driver__facts b.normal{color:#4fe3b1}
.profit-summary{height:75px;display:grid;grid-template-columns:1fr 18px 1fr 18px 1fr;align-items:center;text-align:center;border-bottom:1px solid rgba(25,217,255,.09)}.profit-summary span{display:block;color:#7098a5;font-size:8px}.profit-summary b{color:#d8f8ff;font:600 16px Rajdhani}.profit-summary>i{color:#477586;font-style:normal}.profit-summary .profit b{color:#4fe3b1}.cost-parts{padding-top:8px}.cost-parts>div{display:grid;grid-template-columns:55px 1fr 28px;gap:6px;align-items:center;height:25px;color:#6c95a2;font-size:7px}.cost-parts i{height:3px;background:rgba(25,217,255,.08)}.cost-parts i b{display:block;height:100%;background:linear-gradient(90deg,#127c9e,#38dae5)}.cost-parts strong{font:9px Rajdhani;color:#5ecedd}.lane-performance{height:100%;display:flex;justify-content:space-around;align-items:center}.lane-performance>div{text-align:center}.lane-performance :deep(.freight-icon){margin:0 auto 12px}.lane-performance span{display:block;color:#739ba8;font-size:8px}.lane-performance b{display:block;color:#dffaff;font:600 15px Rajdhani}
.risk-headline{height:67px;display:grid;grid-template-columns:auto 1fr auto auto;align-items:center;gap:5px;padding:0 12px;background:linear-gradient(90deg,rgba(255,65,93,.09),transparent);border-bottom:1px solid rgba(255,65,93,.13)}.risk-headline strong{color:#ff6e83;font:600 24px Rajdhani}.risk-headline span,.risk-headline small{color:#78959f;font-size:8px}.risk-headline b{color:#ff8697;font:600 14px Rajdhani}.risk-events button{width:100%;height:39px;display:grid;grid-template-columns:36px 1fr auto;align-items:center;gap:7px;border:0;border-bottom:1px solid rgba(25,217,255,.055);background:transparent;color:#d4f4fb;text-align:left;cursor:pointer}.risk-events button div b{display:block;font-size:9px}.risk-events button div span,.risk-events time{color:#658c99;font-size:7px}.quality-metrics>div{height:66px;display:grid;grid-template-columns:42px 1fr auto;grid-template-rows:1fr 1fr;align-items:center;gap:0 7px;border-bottom:1px solid rgba(25,217,255,.06)}.quality-metrics :deep(.freight-icon){grid-row:1/3}.quality-metrics span{align-self:end;color:#7b9fac;font-size:8px}.quality-metrics strong{grid-column:3;grid-row:1/3;color:#4fe0e9;font:600 15px Rajdhani}.quality-metrics>div>i{align-self:start;height:3px;background:rgba(25,217,255,.08)}.quality-metrics>div>i b{display:block;height:100%;background:linear-gradient(90deg,#157d9f,#4fe3b1)}
.cargo-evidence{position:relative;overflow:hidden;border:1px solid rgba(25,217,255,.18)}.cargo-evidence img{width:100%;height:100%;object-fit:cover;object-position:center 72%;filter:saturate(.45) brightness(.62)}.cargo-evidence__shade{position:absolute;inset:0;background:linear-gradient(180deg,rgba(3,15,24,.28),rgba(2,14,23,.88))}.cargo-evidence header{position:absolute;left:13px;top:13px;display:flex;align-items:center;gap:11px}.cargo-evidence header span{display:block;font-size:10px}.cargo-evidence header b{color:#4fe3b1;font:8px Rajdhani}.cargo-evidence__footer{position:absolute;left:9px;right:9px;bottom:9px;display:flex;justify-content:space-around;padding:8px;background:rgba(2,18,29,.82);color:#7aa2ae;font-size:7px}.cargo-evidence__footer span::before{content:'✓';margin-right:4px;color:#4fe3b1}
@keyframes executionIn{from{opacity:0;transform:translateX(24px);filter:blur(3px)}}
.execution-v2{animation:none}.waybill-photo img{object-fit:contain;object-position:center center;background:#020c13}.cargo-evidence img{object-position:center center}
.execution-kpis article{text-align:center}.execution-kpis article>div{min-width:105px;text-align:center}
.execution-kpis :deep(.freight-icon),.lane-performance :deep(.freight-icon),.quality-metrics :deep(.freight-icon),.risk-events :deep(.freight-icon){display:grid;place-items:center;justify-self:center;align-self:center}
.lane-performance>div{width:30%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center}
.quality-metrics>div{padding:0 8px}.quality-metrics span,.quality-metrics strong{align-self:center}
.cargo-evidence header{left:50%;right:auto;transform:translateX(-50%);justify-content:center;width:max-content;text-align:center}
.vehicle-driver{grid-template-columns:.9fr 1.5fr;grid-template-rows:1fr 32px;gap:5px 10px}.vehicle-driver__plate{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;background:rgba(10,55,72,.16)}.vehicle-driver__facts{grid-template-columns:1fr 1fr;grid-template-rows:repeat(3,1fr);gap:5px 9px;align-content:center}.vehicle-driver__facts>div{padding:4px 5px;border-left:2px solid rgba(25,217,255,.22);background:linear-gradient(90deg,rgba(25,217,255,.05),transparent)}.vehicle-driver__progress{grid-column:1/3;display:grid;grid-template-columns:52px 1fr 28px;align-items:center;gap:7px;color:#6d96a4;font-size:7px}.vehicle-driver__progress>i{height:4px;background:rgba(25,217,255,.09)}.vehicle-driver__progress>i b{display:block;height:100%;background:linear-gradient(90deg,#188eb0,#4fe3b1)}.vehicle-driver__progress strong{color:#4fe3b1;font:9px Rajdhani}
.lane-performance>div{padding:12px 10px 10px;background:linear-gradient(180deg,rgba(18,80,96,.1),transparent)}.lane-performance small{height:20px;margin-top:5px;color:#557d8a;font-size:7px}.lane-performance>div>i{width:100%;height:4px;margin-top:7px;background:rgba(25,217,255,.08)}.lane-performance>div>i em{display:block;height:100%;background:linear-gradient(90deg,#157d9f,#4fe3b1);font-style:normal}
.metric-flip-enter-active,.metric-flip-leave-active{display:inline-block;transition:opacity .2s ease,transform .2s ease,filter .2s ease}.metric-flip-enter-from{opacity:0;transform:translateY(8px) rotateX(-35deg);filter:brightness(2)}.metric-flip-leave-to{opacity:0;transform:translateY(-8px) rotateX(35deg)}
</style>