182
src/views/ExecutionScreenV2.vue
Normal file
182
src/views/ExecutionScreenV2.vue
Normal file
@@ -0,0 +1,182 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user