<template>
|
<div class="screen-page">
|
<el-row :gutter="10" class="kpi-row">
|
<el-col :lg="3" :sm="8" :xs="12">
|
<kpi-card label="在线分拣员" :value="data.kpis.sorterOnline"
|
:hint="`${data.kpis.sorterOnline} / ${data.kpis.sorterTotal}`" />
|
</el-col>
|
<el-col :lg="3" :sm="8" :xs="12">
|
<kpi-card label="在线车辆" :value="data.kpis.vehicleOnline"
|
:hint="`${data.kpis.vehicleOnline} / ${data.kpis.vehicleTotal}`" />
|
</el-col>
|
<el-col :lg="3" :sm="8" :xs="12">
|
<kpi-card label="在线分拣点" :value="data.kpis.siteOnline"
|
:hint="`${data.kpis.siteOnline} / ${data.kpis.siteTotal},按站点启用`" />
|
</el-col>
|
<el-col :lg="3" :sm="8" :xs="12">
|
<kpi-card label="今日回收订单" :value="data.kpis.todayOrders" :hint="``" />
|
</el-col>
|
<el-col :lg="4" :sm="8" :xs="12">
|
<kpi-card label="今日预约 / 待处理" :value="data.kpis.todayAppointments"
|
:hint="`待处理 ${data.kpis.pendingAppointments}`" />
|
</el-col>
|
<el-col :lg="4" :sm="8" :xs="12">
|
<kpi-card label="异常告警" :value="data.kpis.alerts" alert
|
:hint="`健康异常 ${data.kpis.healthAlerts},不展示隐私体征`" />
|
</el-col>
|
<el-col :lg="4" :sm="8" :xs="12">
|
<kpi-card label="车辆安全事件" :value="safetyCount" hint="超速 / 急刹等,来自车辆日汇总" />
|
</el-col>
|
</el-row>
|
|
<el-row :gutter="10" class="mid-row">
|
<el-col :lg="14" :xs="24">
|
<panel-card title="订单趋势" sub="现场 / 预约 / 大屏 / 活动,按天对比">
|
<v-chart :option="orderOption" autoresize class="chart" />
|
</panel-card>
|
</el-col>
|
<el-col :lg="10" :xs="24">
|
<panel-card title="预约订单漏斗" sub="新增/待审核 → 已确认/已接单 → 已完成">
|
<div class="funnel">
|
<div v-for="(item, index) in data.appointmentFlow" :key="item.name" class="funnel__item">
|
<div class="funnel__name">{{ item.name }}</div>
|
<div class="funnel__bar">
|
<i :style="{ width: funnelWidth(item.value) }" />
|
</div>
|
<div class="funnel__value">{{ item.value }}</div>
|
<span v-if="index < data.appointmentFlow.length - 1" class="funnel__next">→</span>
|
</div>
|
</div>
|
<p class="funnel__tip">预约 → 接单 → 完成</p>
|
</panel-card>
|
</el-col>
|
</el-row>
|
|
<el-row :gutter="10">
|
<el-col :lg="8" :xs="24">
|
<panel-card title="分拣员状态" sub="来自分拣员日表,只汇总状态,不展示体征明细">
|
<div class="status-list">
|
<div v-for="item in data.sorterStatus" :key="item.name" class="status-list__item">
|
<i :style="{ background: item.color }" />
|
<span>{{ item.name }}</span>
|
<strong>{{ item.value }}</strong>
|
</div>
|
</div>
|
</panel-card>
|
</el-col>
|
<el-col :lg="8" :xs="24">
|
<panel-card title="车辆运行" sub="公司领导看运力与安全,不进校领导首页">
|
<div class="table table--vehicle">
|
<div class="table__head">
|
<span>车牌</span>
|
<span>里程km</span>
|
<span>怠速分</span>
|
<span>超速</span>
|
<span>状态</span>
|
</div>
|
<div v-for="item in data.vehicles" :key="item.no" class="table__row">
|
<span>{{ item.no }}</span>
|
<span>{{ item.km }}</span>
|
<span>{{ item.idle }}</span>
|
<span>{{ item.overspeed }}</span>
|
<span :class="item.status === '在线' ? 'is-on' : 'is-off'">{{ item.status }}</span>
|
</div>
|
</div>
|
</panel-card>
|
</el-col>
|
<el-col :lg="8" :xs="24">
|
<panel-card title="分拣点吞吐" sub="运营维度才下钻到点位">
|
<div class="table table--site">
|
<div class="table__head">
|
<span>收集点</span>
|
<span>今日kg</span>
|
<span>在线</span>
|
</div>
|
<div v-for="item in data.sites" :key="item.name" class="table__row">
|
<span>{{ item.name }}</span>
|
<span>{{ item.weight }}</span>
|
<span :class="item.online ? 'is-on' : 'is-off'">{{ item.online ? '在线' : '离线' }}</span>
|
</div>
|
</div>
|
</panel-card>
|
</el-col>
|
</el-row>
|
</div>
|
</template>
|
|
<script lang="ts" setup>
|
defineOptions({
|
name: 'report-screen-operation'
|
});
|
|
import { computed } from 'vue';
|
import KpiCard from '../../components/kpi-card.vue';
|
import PanelCard from '../../components/panel-card.vue';
|
import { useChartTheme, useScreenData } from '../../hooks/use-screen';
|
|
const { data } = useScreenData();
|
const { mutedColor, splitColor, tooltip } = useChartTheme();
|
|
const safetyCount = computed(() => data.value.vehicles.reduce((s, e) => s + e.overspeed, 0));
|
|
function funnelWidth(value: number) {
|
const max = data.value.appointmentFlow[0]?.value || 1;
|
return `${Math.max((value / max) * 100, 18)}%`;
|
}
|
|
const orderOption = computed(() => {
|
const colors = ['#16a34a', '#3b82f6', '#f59e0b', '#a855f7'];
|
const keys = ['现场回收', '预约上门', '大屏投递', '活动回收'] as const;
|
return {
|
grid: { containLabel: true, left: 8, right: 12, top: 36, bottom: 8 },
|
legend: { top: 0, textStyle: { color: mutedColor.value } },
|
tooltip: { trigger: 'axis', ...tooltip() },
|
xAxis: {
|
type: 'category',
|
data: data.value.ordersByType.axis,
|
axisLabel: { color: mutedColor.value },
|
axisLine: { show: false },
|
axisTick: { show: false }
|
},
|
yAxis: {
|
type: 'value',
|
splitLine: { lineStyle: { color: splitColor.value, type: 'dashed' } },
|
axisLabel: { color: mutedColor.value },
|
axisLine: { show: false },
|
axisTick: { show: false }
|
},
|
series: keys.map((name, i) => ({
|
name,
|
type: 'bar',
|
stack: 'order',
|
barWidth: 18,
|
data: data.value.ordersByType[name],
|
itemStyle: { color: colors[i] }
|
}))
|
};
|
});
|
</script>
|
|
<style lang="scss" scoped>
|
.screen-page {
|
|
.kpi-row,
|
.mid-row {
|
margin-bottom: 10px;
|
}
|
|
.kpi-row {
|
:deep(.el-col) {
|
margin-bottom: 10px;
|
}
|
}
|
|
.mid-row,
|
:deep(.panel-card) {
|
min-height: 0;
|
}
|
|
.mid-row {
|
:deep(.panel-card) {
|
min-height: 340px;
|
}
|
}
|
|
.chart {
|
height: 280px;
|
width: 100%;
|
}
|
|
.funnel {
|
display: flex;
|
flex-direction: column;
|
gap: 12px;
|
padding: 18px 16px 0;
|
|
&__item {
|
display: grid;
|
grid-template-columns: 52px 1fr 40px 16px;
|
align-items: center;
|
gap: 8px;
|
}
|
|
&__name {
|
font-size: 13px;
|
color: var(--el-text-color-regular);
|
}
|
|
&__bar {
|
height: 18px;
|
border-radius: 99px;
|
background: var(--el-fill-color-light);
|
|
i {
|
display: block;
|
height: 100%;
|
border-radius: 99px;
|
background: var(--el-color-primary);
|
}
|
}
|
|
&__value {
|
text-align: right;
|
font-weight: 700;
|
}
|
|
&__next {
|
color: var(--el-text-color-secondary);
|
}
|
|
&__tip {
|
margin: 16px;
|
font-size: 12px;
|
color: var(--el-text-color-secondary);
|
}
|
}
|
|
.status-list {
|
display: flex;
|
flex-direction: column;
|
gap: 12px;
|
padding: 16px;
|
|
&__item {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
padding: 12px 14px;
|
border-radius: 10px;
|
background: var(--el-fill-color-lighter);
|
font-size: 14px;
|
|
i {
|
width: 10px;
|
height: 10px;
|
border-radius: 50%;
|
}
|
|
span {
|
flex: 1;
|
}
|
|
strong {
|
font-size: 20px;
|
}
|
}
|
}
|
|
.table {
|
padding: 8px 12px 16px;
|
font-size: 13px;
|
|
&--vehicle &__head,
|
&--vehicle &__row {
|
grid-template-columns: 1.4fr repeat(4, 0.8fr);
|
}
|
|
&--site &__head,
|
&--site &__row {
|
grid-template-columns: 1.6fr 0.8fr 0.6fr;
|
}
|
|
&__head,
|
&__row {
|
display: grid;
|
gap: 8px;
|
padding: 10px 4px;
|
}
|
|
&__head {
|
color: var(--el-text-color-secondary);
|
}
|
|
&__row {
|
border-top: 1px solid var(--el-border-color-extra-light);
|
}
|
|
.is-on {
|
color: var(--el-color-success);
|
}
|
|
.is-off {
|
color: var(--el-text-color-secondary);
|
}
|
}
|
|
}
|
</style>
|