<template>
|
<div class="screen-page dash">
|
<!-- KPI 指标条 -->
|
<div class="kpi-grid" style="--kpi-cols: 7">
|
<kpi-card v-for="item in kpis" :key="item.label" v-bind="item" />
|
</div>
|
|
<!-- 主体三栏:左图表 / 中地图 / 右图表 -->
|
<div class="dash-body">
|
<!-- 左栏 -->
|
<div class="dash-col dash-col--left">
|
<panel-card
|
title="垃圾分类结构"
|
:demo="isDemoSection('wasteMix')"
|
class="dash-card dash-card--flex"
|
>
|
<template #extra>
|
<cl-select-button v-model="mixMetric" :options="mixOptions" small />
|
</template>
|
<v-chart :option="mixOption" autoresize class="chart" />
|
</panel-card>
|
|
<panel-card
|
title="碳减排来源"
|
:demo="isDemoSection('recycleItems')"
|
class="dash-card dash-card--flex"
|
>
|
<v-chart :option="carbonOption" autoresize class="chart" />
|
</panel-card>
|
</div>
|
|
<!-- 中栏:地图主视觉 -->
|
<div class="dash-col dash-col--center">
|
<panel-card title="全域态势一张图" sub="站点 / 车辆 / 分拣员实时分布" class="dash-card dash-card--map">
|
<screen-map-panel />
|
</panel-card>
|
</div>
|
|
<!-- 右栏 -->
|
<div class="dash-col dash-col--right">
|
<panel-card
|
title="回收趋势"
|
:demo="isDemoSection('trend')"
|
class="dash-card dash-card--flex"
|
>
|
<template #extra>
|
<cl-select-button v-model="trendMetric" :options="trendOptions" small />
|
</template>
|
<v-chart :option="trendOption" autoresize class="chart" />
|
</panel-card>
|
|
<panel-card
|
title="各类回收量"
|
:demo="isDemoSection('recycleItems')"
|
class="dash-card dash-card--flex"
|
>
|
<template #extra>
|
<cl-select-button v-model="itemMetric" :options="itemOptions" small />
|
</template>
|
<v-chart :option="itemOption" autoresize class="chart chart--items" />
|
</panel-card>
|
</div>
|
</div>
|
|
<!-- 底部:现场监控 -->
|
<div class="dash-foot">
|
<panel-card title="现场监控" sub="重点站点实时画面,首帧后暂停拉流以节省带宽">
|
<template #extra>
|
<router-link class="camera-more" to="/monitor/device">更多</router-link>
|
</template>
|
<CameraMonitor :limit="6" />
|
</panel-card>
|
</div>
|
</div>
|
</template>
|
|
<script lang="ts" setup>
|
defineOptions({
|
name: 'report-screen-map'
|
});
|
|
import { computed, ref } from 'vue';
|
import KpiCard from '../../components/kpi-card.vue';
|
import PanelCard from '../../components/panel-card.vue';
|
import ScreenMapPanel from '../../components/screen-map-panel.vue';
|
import CameraMonitor from '../../../monitor/views/cameras.vue';
|
import { TIME_RANGE_OPTIONS } from '../../data/screen';
|
import { chartSeries, pieTooltipFormatter, toPieData } from '../../data/screen-chart';
|
import { useChartTheme, useScreenData, useScreenFieldSource } from '../../hooks/use-screen';
|
|
const { data, timeRange } = useScreenData();
|
const { isDemoSection } = useScreenFieldSource();
|
const { textColor, mutedColor, splitColor, primary, baseGrid, axisStyle, tooltip } = useChartTheme();
|
|
const mixMetric = ref<'weight' | 'value' | 'count'>('weight');
|
const trendMetric = ref<'weight' | 'value' | 'carbon'>('weight');
|
const itemMetric = ref<'weight' | 'value' | 'carbon'>('weight');
|
|
const mixOptions = [
|
{ label: '重量', value: 'weight' },
|
{ label: '价值', value: 'value' },
|
{ label: '数量', value: 'count' }
|
];
|
|
const trendOptions = [
|
{ label: '重量', value: 'weight' },
|
{ label: '价值', value: 'value' },
|
{ label: '碳减排', value: 'carbon' }
|
];
|
|
const itemOptions = [
|
{ label: '重量', value: 'weight' },
|
{ label: '价值', value: 'value' },
|
{ label: '碳减排', value: 'carbon' }
|
];
|
|
const rangeLabel = computed(
|
() => TIME_RANGE_OPTIONS.find(e => e.value === timeRange.value)?.label || '本区间'
|
);
|
|
/** 顶部 KPI */
|
const kpis = computed(() => [
|
{ field: 'kpis.periodWeightTon', label: `${rangeLabel.value}回收量`, value: data.value.kpis.periodWeightTon, suffix: '吨', fixed: 2 },
|
{ field: 'kpis.resourceRate', label: '资源化率', value: data.value.kpis.resourceRate, suffix: '%', fixed: 1 },
|
{ field: 'kpis.periodValue', label: '回收价值', value: data.value.kpis.periodValue, suffix: '元', fixed: 0 },
|
{ field: 'kpis.periodCarbonTon', label: '碳减排', value: data.value.kpis.periodCarbonTon, suffix: '吨CO₂e', fixed: 2 },
|
{ field: 'kpis.totalWeightTon', label: '累计回收量', value: data.value.kpis.totalWeightTon, suffix: '吨', fixed: 1 },
|
{ field: 'kpis.totalParticipants', label: '累计参与人数', value: data.value.kpis.totalParticipants, suffix: '人', fixed: 0 },
|
{ field: 'kpis.totalPoints', label: '累计碳积分', value: data.value.kpis.totalPoints, suffix: '分', fixed: 0 }
|
]);
|
|
const trendUnit = computed(() => {
|
if (trendMetric.value === 'value') return '元';
|
if (trendMetric.value === 'carbon') return 'kgCO₂e';
|
return 'kg';
|
});
|
|
const itemUnit = computed(() => {
|
if (itemMetric.value === 'value') return '元';
|
if (itemMetric.value === 'carbon') return 'kgCO₂e';
|
return 'kg';
|
});
|
|
/** 垃圾分类结构(饼) */
|
const mixOption = computed(() => ({
|
tooltip: { trigger: 'item', ...tooltip(), formatter: pieTooltipFormatter() },
|
series: [
|
{
|
type: 'pie',
|
radius: ['38%', '62%'],
|
center: ['50%', '50%'],
|
padAngle: 3,
|
itemStyle: { borderRadius: 6 },
|
label: { color: textColor.value, formatter: '{b}\n{d}%' },
|
data: toPieData(
|
chartSeries(data.value.wasteMix, e => e[mixMetric.value]),
|
e => e.name,
|
e => e.color
|
)
|
}
|
]
|
}));
|
|
/** 碳减排来源(饼):碳值可为负,按绝对值展示减排量 */
|
const carbonOption = computed(() => ({
|
tooltip: { trigger: 'item', ...tooltip(), formatter: pieTooltipFormatter('kgCO₂e') },
|
series: [
|
{
|
type: 'pie',
|
radius: ['36%', '60%'],
|
center: ['50%', '50%'],
|
padAngle: 3,
|
itemStyle: { borderRadius: 6 },
|
label: { color: textColor.value, formatter: '{b}\n{d}%' },
|
data: toPieData(
|
chartSeries(data.value.recycleItems, e => e.carbon, { carbon: true }),
|
e => e.name + '回收',
|
e => e.color
|
)
|
}
|
]
|
}));
|
|
/** 回收趋势(折线) */
|
const trendOption = computed(() => ({
|
grid: baseGrid(),
|
tooltip: { trigger: 'axis', ...tooltip() },
|
xAxis: { type: 'category', data: data.value.axis, ...axisStyle() },
|
yAxis: {
|
type: 'value',
|
name: trendUnit.value,
|
nameTextStyle: { color: mutedColor.value },
|
splitLine: { lineStyle: { color: splitColor.value, type: 'dashed' } },
|
axisLabel: { color: mutedColor.value },
|
axisLine: { show: false },
|
axisTick: { show: false }
|
},
|
series: [
|
{
|
type: 'line',
|
smooth: true,
|
showSymbol: false,
|
data: data.value.trend[trendMetric.value],
|
itemStyle: { color: primary.value },
|
areaStyle: { color: primary.value, opacity: 0.12 }
|
}
|
]
|
}));
|
|
/** 各类回收量(横向条形) */
|
const itemOption = computed(() => {
|
const carbon = itemMetric.value === 'carbon';
|
const rows = chartSeries(
|
data.value.recycleItems,
|
e => Number(e[itemMetric.value] || 0),
|
{ carbon, evenSplit: false }
|
).reverse();
|
return {
|
grid: { containLabel: true, left: 8, right: 24, top: 12, bottom: 8 },
|
tooltip: {
|
trigger: 'axis',
|
axisPointer: { type: 'shadow' },
|
valueFormatter: (v: number) => `${Number(v || 0).toLocaleString()} ${itemUnit.value}`,
|
...tooltip()
|
},
|
xAxis: {
|
type: 'value',
|
name: itemUnit.value,
|
nameTextStyle: { color: mutedColor.value },
|
splitLine: { lineStyle: { color: splitColor.value, type: 'dashed' } },
|
axisLabel: { color: mutedColor.value },
|
axisLine: { show: false },
|
axisTick: { show: false }
|
},
|
yAxis: {
|
type: 'category',
|
data: rows.map(e => e.item.name),
|
axisLabel: { color: textColor.value },
|
axisLine: { show: false },
|
axisTick: { show: false }
|
},
|
series: [
|
{
|
type: 'bar',
|
barWidth: 14,
|
data: rows.map(e => e.value),
|
itemStyle: {
|
borderRadius: [0, 8, 8, 0],
|
color: (params: any) => rows[params.dataIndex]?.item.color || primary.value
|
}
|
}
|
]
|
};
|
});
|
</script>
|
|
<style lang="scss" scoped>
|
@use '../../styles/screen-kpi' as *;
|
|
.dash {
|
display: flex;
|
flex-direction: column;
|
gap: 10px;
|
height: 100%;
|
min-height: 0;
|
overflow: auto;
|
|
/* 主体三栏 */
|
&-body {
|
display: grid;
|
grid-template-columns: 24% 46% 30%;
|
gap: 10px;
|
flex: 1;
|
min-height: 420px;
|
}
|
|
&-col {
|
display: flex;
|
flex-direction: column;
|
gap: 10px;
|
min-width: 0;
|
min-height: 0;
|
}
|
|
&-card {
|
min-height: 0;
|
|
/* 左右栏图表卡均分高度 */
|
&--flex {
|
flex: 1;
|
display: flex;
|
flex-direction: column;
|
|
:deep(.panel-card__body) {
|
flex: 1;
|
min-height: 0;
|
}
|
}
|
|
&--map {
|
flex: 1;
|
display: flex;
|
flex-direction: column;
|
|
:deep(.panel-card__body) {
|
flex: 1;
|
min-height: 0;
|
padding: 10px;
|
}
|
}
|
}
|
|
/* 底部监控:压缩后给 CameraMonitor 一个紧凑高度 */
|
&-foot {
|
flex-shrink: 0;
|
display: flex;
|
flex-direction: column;
|
height: clamp(200px, 20vh, 260px);
|
|
:deep(.panel-card) {
|
flex: 1;
|
display: flex;
|
flex-direction: column;
|
min-height: 0;
|
}
|
|
:deep(.panel-card__head) {
|
padding-top: 6px;
|
padding-bottom: 6px;
|
}
|
|
:deep(.panel-card__body) {
|
flex: 1;
|
min-height: 0;
|
overflow: hidden;
|
padding: 6px;
|
}
|
|
.camera-more {
|
font-size: 12px;
|
color: var(--el-color-primary);
|
text-decoration: none;
|
}
|
}
|
|
.chart {
|
height: 100%;
|
min-height: 180px;
|
width: 100%;
|
}
|
}
|
|
/* 窄屏兜底:三栏改单栏纵向 */
|
@media (max-width: 1200px) {
|
.dash-body {
|
grid-template-columns: 1fr;
|
min-height: 0;
|
|
.dash-col--center {
|
min-height: 420px;
|
}
|
}
|
}
|
|
@media (max-width: 768px) {
|
.dash,
|
.dash-body,
|
.dash-col {
|
height: auto;
|
min-height: 0;
|
overflow: visible;
|
flex: none;
|
}
|
|
.dash :deep(.kpi-grid) {
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
}
|
|
.dash-card--flex {
|
flex: none;
|
height: auto;
|
min-height: 0;
|
overflow: visible;
|
|
:deep(.panel-card) {
|
height: auto;
|
min-height: 0;
|
overflow: visible;
|
}
|
|
:deep(.panel-card__body) {
|
flex: none;
|
height: auto;
|
min-height: 0;
|
overflow: visible;
|
}
|
}
|
|
/* 地图画布层级很高,限制在中栏内,避免盖住上下两块图表 */
|
.dash-card--map {
|
position: relative;
|
z-index: 0;
|
flex: none;
|
height: 480px;
|
min-height: 480px;
|
overflow: hidden;
|
}
|
|
.dash .chart {
|
display: block;
|
height: 260px;
|
min-height: 260px;
|
}
|
|
.dash .chart--items {
|
height: 420px;
|
min-height: 420px;
|
}
|
|
.dash-foot {
|
height: auto;
|
min-height: 300px;
|
}
|
}
|
</style>
|