191
src/views/OverviewScreenV2.vue
Normal file
191
src/views/OverviewScreenV2.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import FreightIcon from '@/components/base/FreightIcon.vue'
|
||||
import KpiCard from '@/components/base/KpiCard.vue'
|
||||
import MapRenderer from '@/components/map/MapRenderer.vue'
|
||||
import TechPanel from '@/components/base/TechPanel.vue'
|
||||
import type {
|
||||
FreightOrder, FreightRoute, KpiMetric, MapScene, PlatformSnapshot, RiskAlert,
|
||||
} from '@/types/freight'
|
||||
|
||||
const props = defineProps<{
|
||||
snapshot: PlatformSnapshot
|
||||
kpis: KpiMetric[]
|
||||
routes: FreightRoute[]
|
||||
orders: readonly FreightOrder[]
|
||||
alerts: readonly RiskAlert[]
|
||||
mapScene: MapScene
|
||||
selectedRouteId: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ selectRoute: [routeId: string] }>()
|
||||
|
||||
const stages = computed(() => [
|
||||
{ label: '待提货', value: props.snapshot.pendingPickupOrders, icon: 'warehouse' as const, tone: 'gold' as const },
|
||||
{ label: '运输中', value: props.snapshot.inTransitOrders, icon: 'truck' as const, tone: 'cyan' as const },
|
||||
{ label: '已完成', value: props.snapshot.completedOrders, icon: 'document' as const, tone: 'green' as const },
|
||||
])
|
||||
const featuredRoutes = computed(() => [props.routes[0], props.routes[1], props.routes[2], props.routes[5], props.routes[7]])
|
||||
|
||||
function routeName(routeId: string) {
|
||||
const route = props.routes.find((item) => item.id === routeId)
|
||||
return route ? `${route.from.name} → ${route.to.name}` : '未知线路'
|
||||
}
|
||||
|
||||
function plate(order: FreightOrder) {
|
||||
return `样本车 ${order.vehicleId.slice(-3)}`
|
||||
}
|
||||
|
||||
function formatTime(value: string) {
|
||||
return new Date(value).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="overview-v2">
|
||||
<div class="overview-kpis">
|
||||
<KpiCard v-for="(metric,index) in kpis" :key="metric.label" :metric="metric" :index="index" />
|
||||
</div>
|
||||
|
||||
<div class="overview-layout">
|
||||
<aside class="overview-side overview-side--left">
|
||||
<TechPanel title="货源与运力供需" tag="SUPPLY & CAPACITY">
|
||||
<div class="supply-summary">
|
||||
<div class="supply-summary__ratio">
|
||||
<span>实时供需比</span><strong>1 : 1.18</strong><small>运力略充足</small>
|
||||
</div>
|
||||
<div class="supply-summary__values">
|
||||
<div><FreightIcon kind="cargo" :size="37" /><span>待匹配货源</span><b>426</b><small>单</small></div>
|
||||
<div><FreightIcon kind="truck" tone="green" :size="37" /><span>可调运力</span><b>503</b><small>辆</small></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="supply-lines">
|
||||
<div v-for="item in [['蒙陕晋',88,81],['云贵川',79,72],['港口钢厂',67,76]]" :key="String(item[0])">
|
||||
<span>{{ item[0] }}</span>
|
||||
<i><b :style="{ width: `${item[1]}%` }" /></i>
|
||||
<i class="capacity-line"><b :style="{ width: `${item[2]}%` }" /></i>
|
||||
</div>
|
||||
<footer><span><i />货源指数</span><span><i />运力指数</span></footer>
|
||||
</div>
|
||||
</TechPanel>
|
||||
|
||||
<TechPanel title="今日订单节奏" tag="HOURLY ORDERS">
|
||||
<div class="hourly-chart">
|
||||
<svg viewBox="0 0 300 120" preserveAspectRatio="none">
|
||||
<defs><linearGradient id="hourArea" x2="0" y2="1"><stop stop-color="#19d9ff" stop-opacity=".35"/><stop offset="1" stop-color="#19d9ff" stop-opacity="0"/></linearGradient></defs>
|
||||
<path class="hourly-chart__grid" d="M0 25H300M0 55H300M0 85H300" />
|
||||
<path class="hourly-chart__area" d="M0 101L0 88 25 92 50 98 75 83 100 57 125 43 150 54 175 38 200 46 225 22 250 12 275 27 300 39V101Z" />
|
||||
<path class="hourly-chart__line" d="M0 88 25 92 50 98 75 83 100 57 125 43 150 54 175 38 200 46 225 22 250 12 275 27 300 39" />
|
||||
</svg>
|
||||
<div class="hourly-chart__labels"><span>00:00</span><span>06:00</span><span>12:00</span><span>18:00</span><span>当前</span></div>
|
||||
<div class="hourly-chart__fact"><span>高峰时段</span><b>18:00–20:00</b><span>峰值</span><b>612 单/时</b></div>
|
||||
</div>
|
||||
</TechPanel>
|
||||
|
||||
<article class="industry-photo">
|
||||
<img src="@/assets/stock/coal-road-truck.jpg" alt="满载煤炭的公路运输车辆" />
|
||||
<div class="industry-photo__shade" />
|
||||
<header><span>煤炭公路运输场景</span><b>ROAD COAL FREIGHT</b></header>
|
||||
<div class="industry-photo__metrics">
|
||||
<div><strong>82.4%</strong><span>煤炭货量占比</span></div>
|
||||
<div><strong>{{ snapshot.averageWeight }}t</strong><span>平均单重</span></div>
|
||||
<div><strong>95.8%</strong><span>装货准时率</span></div>
|
||||
</div>
|
||||
</article>
|
||||
</aside>
|
||||
|
||||
<main class="overview-center">
|
||||
<div class="map-panel">
|
||||
<div class="map-panel__title">全国公路货运实时网络 / NATIONAL ROAD FREIGHT NETWORK</div>
|
||||
<MapRenderer :scene="mapScene" @select-route="emit('selectRoute', $event)" />
|
||||
</div>
|
||||
|
||||
<div class="overview-center__bottom">
|
||||
<TechPanel title="实时运单样本" tag="LIVE WAYBILLS">
|
||||
<div class="live-orders">
|
||||
<div class="live-orders__head"><span>时间</span><span>运输线路</span><span>货物 / 重量</span><span>承运车辆</span><span>状态</span></div>
|
||||
<TransitionGroup name="order-live">
|
||||
<div v-for="order in orders.slice(0,6)" :key="order.id" class="live-orders__row">
|
||||
<time>{{ formatTime(order.createdAt) }}</time>
|
||||
<span>{{ routeName(order.routeId) }}</span>
|
||||
<span>{{ order.cargo }} · {{ order.weight }}t</span>
|
||||
<span>{{ plate(order) }}</span>
|
||||
<b>{{ order.status }}</b>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</TechPanel>
|
||||
|
||||
<TechPanel title="重点线路运价" tag="LANE PRICE">
|
||||
<div class="lane-prices">
|
||||
<button v-for="route in featuredRoutes" :key="route.id" :class="{ active: route.id === selectedRouteId }" @click="emit('selectRoute', route.id)">
|
||||
<span>{{ route.from.name }} → {{ route.to.name }}</span>
|
||||
<i><b :style="{ width: `${route.orders / 13}%` }" /></i>
|
||||
<strong>{{ route.rate.toFixed(2) }}</strong><small>元/吨公里</small>
|
||||
</button>
|
||||
</div>
|
||||
</TechPanel>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<aside class="overview-side overview-side--right">
|
||||
<TechPanel title="今日运单履约" tag="ORDER FULFILLMENT">
|
||||
<div class="stage-total"><span>有效运单</span><Transition name="data-pop" mode="out-in"><strong :key="snapshot.todayOrders">{{ snapshot.todayOrders.toLocaleString() }}</strong></Transition><small>单</small></div>
|
||||
<div class="order-stages">
|
||||
<div v-for="stage in stages" :key="stage.label">
|
||||
<FreightIcon :kind="stage.icon" :tone="stage.tone" :size="42" />
|
||||
<span>{{ stage.label }}</span><Transition name="data-pop" mode="out-in"><b :key="stage.value">{{ stage.value.toLocaleString() }}</b></Transition>
|
||||
<small>{{ (stage.value / snapshot.todayOrders * 100).toFixed(1) }}%</small>
|
||||
</div>
|
||||
</div>
|
||||
</TechPanel>
|
||||
|
||||
<TechPanel title="结算与开票进度" tag="SETTLEMENT">
|
||||
<div class="settlement-amount">
|
||||
<FreightIcon kind="money" tone="gold" :size="48" />
|
||||
<div><span>今日应付运费</span><Transition name="data-pop" mode="out-in"><b :key="snapshot.freightAmount">{{ (snapshot.freightAmount / 10_000).toFixed(0) }}</b></Transition><small>万元</small></div>
|
||||
</div>
|
||||
<div class="settlement-bars">
|
||||
<div><span>已完成结算</span><i><b :style="{ width: `${snapshot.settlementRate}%` }" /></i><strong>{{ snapshot.settlementRate }}%</strong></div>
|
||||
<div><span>已申请开票</span><i><b :style="{ width: `${snapshot.invoiceRate}%` }" /></i><strong>{{ snapshot.invoiceRate }}%</strong></div>
|
||||
<div><span>电子回单上传</span><i><b :style="{ width: `${snapshot.receiptRate}%` }" /></i><strong>{{ snapshot.receiptRate }}%</strong></div>
|
||||
</div>
|
||||
</TechPanel>
|
||||
|
||||
<TechPanel title="在途异常" tag="TRANSIT EXCEPTION">
|
||||
<div class="exception-summary">
|
||||
<div><strong>{{ (snapshot.todayOrders * snapshot.abnormalRate / 100).toFixed(0) }}</strong><span>今日异常运单</span></div>
|
||||
<div><strong>{{ snapshot.abnormalRate }}%</strong><span>异常运单率</span></div>
|
||||
</div>
|
||||
<div class="compact-alerts">
|
||||
<button v-for="alert in alerts.slice(0,4)" :key="alert.id" @click="emit('selectRoute', alert.routeId)">
|
||||
<i :class="`level-${alert.level}`" /><span>{{ alert.type }}</span><b>{{ routeName(alert.routeId) }}</b><time>{{ formatTime(alert.time) }}</time>
|
||||
</button>
|
||||
</div>
|
||||
</TechPanel>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.overview-v2{height:968px;padding-top:7px;position:relative;z-index:1;animation:screenInOverview .5s ease both}.overview-kpis{height:82px;display:grid;grid-template-columns:repeat(5,1fr);gap:10px}.overview-layout{height:858px;margin-top:8px;display:grid;grid-template-columns:320px 1fr 320px;gap:10px}.overview-side{display:grid;gap:9px;min-height:0}.overview-side--left{grid-template-rows:260px 230px 1fr}.overview-side--right{grid-template-rows:270px 267px 1fr}.overview-center{display:grid;grid-template-rows:590px 259px;gap:9px;min-width:0}.map-panel{position:relative;overflow:hidden;border:1px solid rgba(25,217,255,.12);background:rgba(2,15,26,.22)}.map-panel__title{position:absolute;z-index:6;left:17px;top:12px;color:#4ca6bd;font:600 10px Rajdhani;letter-spacing:3px}.overview-center__bottom{display:grid;grid-template-columns:1.35fr 1fr;gap:9px;min-height:0}
|
||||
.supply-summary{height:108px;display:grid;grid-template-columns:1fr 1.4fr;gap:10px}.supply-summary__ratio{display:grid;place-content:center;text-align:center;border-right:1px solid rgba(25,217,255,.13)}.supply-summary__ratio span{color:#7199a8;font-size:9px}.supply-summary__ratio strong{color:var(--gold);font:600 24px Rajdhani}.supply-summary__ratio small{color:#55dcae;font-size:8px}.supply-summary__values{display:grid;gap:5px}.supply-summary__values>div{display:grid;grid-template-columns:42px 1fr auto auto;align-items:center;gap:4px}.supply-summary__values span{font-size:8px;color:#779fac}.supply-summary__values b{font:600 17px Rajdhani;color:#d9f9ff}.supply-summary__values small{font-size:7px;color:#658c99}.supply-lines>div{display:grid;grid-template-columns:32px 1fr 1fr;gap:6px;align-items:center;height:24px;font-size:8px;color:#7196a4}.supply-lines>div>i{height:4px;background:rgba(25,217,255,.08)}.supply-lines>div>i b{display:block;height:100%;background:#19d9ff}.supply-lines>div>i.capacity-line b{background:#4fe3b1}.supply-lines footer{display:flex;justify-content:flex-end;gap:14px;margin-top:5px;color:#527886;font-size:7px}.supply-lines footer i{display:inline-block;width:6px;height:3px;margin-right:3px;background:#19d9ff}.supply-lines footer span:last-child i{background:#4fe3b1}
|
||||
.hourly-chart{height:100%;position:relative}.hourly-chart svg{width:100%;height:115px}.hourly-chart__grid{fill:none;stroke:rgba(77,153,177,.1)}.hourly-chart__area{fill:url(#hourArea)}.hourly-chart__line{fill:none;stroke:#27ddf3;stroke-width:2;filter:drop-shadow(0 0 4px #19d9ff)}.hourly-chart__labels{display:flex;justify-content:space-between;color:#4d7786;font:8px Rajdhani}.hourly-chart__fact{display:grid;grid-template-columns:auto auto auto auto;gap:5px;margin-top:12px;padding:8px;background:rgba(8,49,68,.28);font-size:8px;color:#658d9b}.hourly-chart__fact b{color:#d5f8ff}
|
||||
.industry-photo{position:relative;overflow:hidden;border:1px solid rgba(25,217,255,.19);min-height:0}.industry-photo img{width:100%;height:100%;object-fit:cover;filter:saturate(.45) contrast(1.1)}.industry-photo__shade{position:absolute;inset:0;background:linear-gradient(180deg,rgba(2,14,24,.25),rgba(2,15,25,.9)),linear-gradient(90deg,rgba(5,59,79,.48),transparent)}.industry-photo header{position:absolute;left:14px;top:14px}.industry-photo header span{display:block;font-size:13px;letter-spacing:2px}.industry-photo header b{color:#4b91a5;font:9px Rajdhani;letter-spacing:2px}.industry-photo__metrics{position:absolute;left:12px;right:12px;bottom:12px;display:grid;grid-template-columns:repeat(3,1fr);background:rgba(3,18,29,.76);border-top:1px solid rgba(25,217,255,.24)}.industry-photo__metrics div{padding:10px 5px;text-align:center;border-right:1px solid rgba(25,217,255,.09)}.industry-photo__metrics strong{display:block;color:#6be5ef;font:600 16px Rajdhani}.industry-photo__metrics span{color:#7196a3;font-size:7px}
|
||||
.live-orders__head,.live-orders__row{display:grid;grid-template-columns:55px 1.25fr 1fr 78px 48px;gap:7px;align-items:center;height:35px;font-size:8px}.live-orders__head{height:27px;color:#477788;border-bottom:1px solid rgba(25,217,255,.13)}.live-orders__row{border-bottom:1px solid rgba(25,217,255,.06);color:#83aab7}.live-orders__row time{font-family:Rajdhani;color:#52cce0}.live-orders__row b{color:#4fe3b1;font-weight:400}.lane-prices button{width:100%;height:37px;display:grid;grid-template-columns:1fr 62px 30px 53px;gap:5px;align-items:center;border:0;border-bottom:1px solid rgba(25,217,255,.06);background:transparent;color:#86aab6;text-align:left;font-size:8px;cursor:pointer}.lane-prices button.active{background:linear-gradient(90deg,rgba(255,200,87,.1),transparent);color:#fff}.lane-prices button>i{height:3px;background:rgba(25,217,255,.08)}.lane-prices button>i b{display:block;height:100%;background:linear-gradient(90deg,#167a9c,#25dff0)}.lane-prices button strong{color:var(--gold);font:600 12px Rajdhani}.lane-prices button small{color:#5d8391;font-size:7px}
|
||||
.stage-total{text-align:center;height:62px;padding:5px;border-bottom:1px solid rgba(25,217,255,.1)}.stage-total span{display:block;color:#6f98a6;font-size:8px}.stage-total strong{color:#d9faff;font:600 25px Rajdhani}.stage-total small{margin-left:4px;color:#628c99;font-size:8px}.order-stages{display:grid;grid-template-columns:repeat(3,1fr);gap:5px;padding-top:15px}.order-stages>div{text-align:center;border-right:1px solid rgba(25,217,255,.08)}.order-stages :deep(.freight-icon){margin:0 auto 10px}.order-stages span{display:block;color:#7ba3af;font-size:8px}.order-stages b{display:block;color:#e1faff;font:600 18px Rajdhani}.order-stages small{color:#587f8c;font:8px Rajdhani}
|
||||
.settlement-amount{height:83px;display:flex;align-items:center;justify-content:center;gap:17px;border-bottom:1px solid rgba(25,217,255,.09)}.settlement-amount span{display:block;color:#7198a6;font-size:8px}.settlement-amount b{color:var(--gold);font:600 25px Rajdhani}.settlement-amount small{margin-left:3px;color:#668d99;font-size:8px}.settlement-bars{padding-top:10px}.settlement-bars>div{display:grid;grid-template-columns:72px 1fr 34px;gap:6px;align-items:center;height:31px;font-size:8px;color:#759ca9}.settlement-bars i{height:4px;background:rgba(25,217,255,.08)}.settlement-bars i b{display:block;height:100%;background:linear-gradient(90deg,#147b9e,#43dfc1)}.settlement-bars strong{color:#58d9c0;font:10px Rajdhani}.exception-summary{height:65px;display:grid;grid-template-columns:1fr 1fr}.exception-summary div{text-align:center;padding:8px;border-right:1px solid rgba(25,217,255,.08);background:rgba(255,65,93,.035)}.exception-summary strong{display:block;color:#ff7d8f;font:600 21px Rajdhani}.exception-summary span{color:#7b929b;font-size:8px}.compact-alerts button{width:100%;height:37px;display:grid;grid-template-columns:5px 62px 1fr 34px;gap:6px;align-items:center;border:0;border-bottom:1px solid rgba(25,217,255,.05);background:transparent;color:#82a5b1;text-align:left;font-size:8px;cursor:pointer}.compact-alerts button>i{width:3px;height:18px;background:#ffc857}.compact-alerts button>i.level-高{background:#ff415d;box-shadow:0 0 6px #ff415d}.compact-alerts button b{font-weight:400;color:#a8cbd4}.compact-alerts time{font:8px Rajdhani;color:#537b89}
|
||||
@keyframes screenInOverview{from{opacity:0;transform:translateX(24px);filter:blur(3px)}}
|
||||
.overview-v2{animation:none}.hourly-chart__line{filter:none}.industry-photo img{object-fit:contain;object-position:center center;background:#020c13}
|
||||
.stage-total{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center}
|
||||
.order-stages{height:145px;align-items:stretch;padding-top:8px}
|
||||
.order-stages>div{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;border-right:0}
|
||||
.order-stages :deep(.freight-icon){display:grid;place-items:center;margin:0 auto 10px}
|
||||
.settlement-amount{display:grid;grid-template-columns:48px auto;place-content:center;align-items:center;text-align:center}
|
||||
.settlement-amount>div{min-width:92px;text-align:center}
|
||||
.data-pop-enter-active,.data-pop-leave-active{display:inline-block;transition:opacity .2s ease,transform .2s ease,filter .2s ease}.data-pop-enter-from{opacity:0;transform:translateY(7px) scale(.92);filter:brightness(2)}.data-pop-leave-to{opacity:0;transform:translateY(-7px) scale(1.08)}
|
||||
.supply-lines>div{grid-template-columns:42px 1fr 1fr}
|
||||
.order-live-move,.order-live-enter-active,.order-live-leave-active{transition:all .38s ease}.order-live-enter-from{opacity:0;transform:translateX(-18px);background:rgba(25,217,255,.18)}.order-live-leave-to{opacity:0;transform:translateX(18px)}.order-live-leave-active{position:absolute}
|
||||
.live-orders__head{height:25px}.live-orders__row{height:31px}.lane-prices button{height:41px;grid-template-columns:92px 1fr 30px 53px}.lane-prices button>i{width:100%}
|
||||
</style>
|
||||
Reference in New Issue
Block a user