| | |
| | | index: 'https://cool-js.com', |
| | | api: 'https://service.cool-js.com/api' |
| | | }, |
| | | toolbar: { |
| | | h5: false, |
| | | order: 1, |
| | | component: import('./components/ai-code/btn.vue') |
| | | }, |
| | | // toolbar: { |
| | | // h5: false, |
| | | // order: 1, |
| | | // component: import('./components/ai-code/btn.vue') |
| | | // }, |
| | | pages: [ |
| | | { |
| | | path: '/helper/ai-code', |
| New file |
| | |
| | | <template> |
| | | <div class="map-panel"> |
| | | <!-- 顶部:图层切换 + 实时统计 --> |
| | | <div class="map-panel__bar"> |
| | | <cl-select-button v-model="layer" :options="layerOptions" small /> |
| | | <div class="map-panel__stat"> |
| | | <span class="map-panel__pulse" :style="{ background: statColor }" /> |
| | | <strong>{{ list.length }}</strong> |
| | | <span class="map-panel__unit">{{ statUnit }}</span> |
| | | <span class="map-panel__update">{{ lastUpdate }}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 地图主体 --> |
| | | <div class="map-panel__map"> |
| | | <tdt-map :center="center" :zoom="zoom" :controls="controls" :loadConfig="MAP_LOAD"> |
| | | <tdt-marker |
| | | v-for="item in list" |
| | | :key="keyOf(item)" |
| | | :position="pos(item)" |
| | | :icon="iconOf(item)" |
| | | :title="nameOf(item)" |
| | | /> |
| | | </tdt-map> |
| | | </div> |
| | | |
| | | <!-- 图例 --> |
| | | <div class="map-panel__legend"> |
| | | <div v-for="l in activeLegends" :key="l.name" class="map-panel__legend-item"> |
| | | <i :style="{ background: l.color }" />{{ l.name }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | defineOptions({ |
| | | name: 'screen-map-panel' |
| | | }); |
| | | |
| | | import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; |
| | | import { TdtMap, TdtMarker, type LngLat } from 'vue-tianditu2'; |
| | | import { |
| | | DEFAULT_CENTER, |
| | | MAP_LOAD, |
| | | SORTER_ICON, |
| | | VEHICLE_ICON, |
| | | formatTime, |
| | | siteIcon, |
| | | useMapsApi |
| | | } from '../../maps/hooks/use-map'; |
| | | |
| | | type LayerKey = 'site' | 'vehicle' | 'sorter'; |
| | | |
| | | /** 三图层统一配置:取数 / 唯一键 / 名称 / 图标 / 图例 */ |
| | | const LAYERS: Record< |
| | | LayerKey, |
| | | { |
| | | unit: string; |
| | | color: string; |
| | | load: () => Promise<any[]>; |
| | | key: (e: any) => string; |
| | | name: (e: any) => string; |
| | | icon: (e: any) => any; |
| | | legends: { name: string; color: string }[]; |
| | | } |
| | | > = { |
| | | site: { |
| | | unit: '个站点', |
| | | color: '#16a34a', |
| | | load: () => api.sites(), |
| | | key: (e: any) => e.id, |
| | | name: (e: any) => e.businessName, |
| | | icon: (e: any) => siteIcon(e.siteType), |
| | | legends: [ |
| | | { name: '收集点', color: '#16a34a' }, |
| | | { name: '压缩站', color: '#f59e0b' } |
| | | ] |
| | | }, |
| | | vehicle: { |
| | | unit: '辆在线', |
| | | color: '#7c3aed', |
| | | load: () => api.vehicleOnline(), |
| | | key: (e: any) => e.vehicleNo, |
| | | name: (e: any) => e.vehicleNo, |
| | | icon: () => VEHICLE_ICON, |
| | | legends: [{ name: '在线车辆', color: '#7c3aed' }] |
| | | }, |
| | | sorter: { |
| | | unit: '人在线', |
| | | color: '#2563eb', |
| | | load: () => api.sorterOnline(), |
| | | key: (e: any) => e.idCard, |
| | | name: (e: any) => e.name || e.idCard, |
| | | icon: () => SORTER_ICON, |
| | | legends: [{ name: '在线分拣员', color: '#2563eb' }] |
| | | } |
| | | }; |
| | | |
| | | const api = useMapsApi(); |
| | | |
| | | const layer = ref<LayerKey>('site'); |
| | | const list = ref<any[]>([]); |
| | | const center = ref<LngLat>(DEFAULT_CENTER); |
| | | const zoom = ref(12); |
| | | const lastUpdate = ref(''); |
| | | |
| | | const controls = ['Zoom', 'Scale']; |
| | | |
| | | const layerOptions = [ |
| | | { label: '站点', value: 'site' }, |
| | | { label: '车辆', value: 'vehicle' }, |
| | | { label: '分拣员', value: 'sorter' } |
| | | ]; |
| | | |
| | | const cfg = computed(() => LAYERS[layer.value]); |
| | | const activeLegends = computed(() => cfg.value.legends); |
| | | const statColor = computed(() => cfg.value.color); |
| | | const statUnit = computed(() => cfg.value.unit); |
| | | |
| | | function pos(item: any): LngLat { |
| | | return [Number(item.longitude), Number(item.latitude)]; |
| | | } |
| | | function keyOf(item: any) { |
| | | return cfg.value.key(item); |
| | | } |
| | | function nameOf(item: any) { |
| | | return cfg.value.name(item); |
| | | } |
| | | function iconOf(item: any) { |
| | | return cfg.value.icon(item); |
| | | } |
| | | |
| | | async function load() { |
| | | const res = await cfg.value.load(); |
| | | list.value = Array.isArray(res) ? res : []; |
| | | lastUpdate.value = formatTime(new Date()); |
| | | if (list.value[0]) { |
| | | center.value = pos(list.value[0]); |
| | | zoom.value = 13; |
| | | } |
| | | } |
| | | |
| | | // 车辆 / 分拣员为实时位置,30s 轮询;站点是静态台账,不轮询 |
| | | let timer: number | undefined; |
| | | function startPolling() { |
| | | stopPolling(); |
| | | if (layer.value !== 'site') { |
| | | timer = window.setInterval(load, 30000); |
| | | } |
| | | } |
| | | function stopPolling() { |
| | | if (timer) { |
| | | clearInterval(timer); |
| | | timer = undefined; |
| | | } |
| | | } |
| | | |
| | | watch(layer, () => { |
| | | load(); |
| | | startPolling(); |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | await load(); |
| | | startPolling(); |
| | | }); |
| | | |
| | | onBeforeUnmount(stopPolling); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .map-panel { |
| | | display: flex; |
| | | flex-direction: column; |
| | | height: 100%; |
| | | min-height: 0; |
| | | |
| | | &__bar { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | padding: 0 4px 8px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | &__stat { |
| | | display: flex; |
| | | align-items: baseline; |
| | | gap: 6px; |
| | | font-size: 13px; |
| | | color: var(--el-text-color-secondary); |
| | | |
| | | strong { |
| | | font-size: 22px; |
| | | font-weight: 700; |
| | | color: var(--el-text-color-primary); |
| | | } |
| | | } |
| | | |
| | | &__pulse { |
| | | width: 8px; |
| | | height: 8px; |
| | | border-radius: 50%; |
| | | align-self: center; |
| | | animation: map-pulse 1.6s ease-in-out infinite; |
| | | } |
| | | |
| | | &__unit { |
| | | font-size: 12px; |
| | | } |
| | | |
| | | &__update { |
| | | margin-left: 4px; |
| | | font-size: 11px; |
| | | color: var(--el-text-color-secondary); |
| | | opacity: 0.75; |
| | | } |
| | | |
| | | &__map { |
| | | flex: 1; |
| | | min-height: 0; |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | |
| | | > * { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | } |
| | | |
| | | &__legend { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 16px; |
| | | padding-top: 8px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | &__legend-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | font-size: 12px; |
| | | color: var(--el-text-color-secondary); |
| | | |
| | | i { |
| | | width: 8px; |
| | | height: 8px; |
| | | border-radius: 50%; |
| | | } |
| | | } |
| | | } |
| | | |
| | | @keyframes map-pulse { |
| | | 0%, |
| | | 100% { |
| | | opacity: 1; |
| | | transform: scale(1); |
| | | } |
| | | 50% { |
| | | opacity: 0.4; |
| | | transform: scale(0.75); |
| | | } |
| | | } |
| | | </style> |
| | |
| | | export type TimeRange = 'today' | 'week' | 'month' | 'quarter' | 'year'; |
| | | export type ScreenKey = 'overview' | 'resource' | 'carbon' | 'participation' | 'operation'; |
| | | export type ScreenKey = 'overview' | 'resource' | 'carbon' | 'participation' | 'operation' | 'map'; |
| | | export type RankMetric = 'weight' | 'people' | 'pointPerCapita' | 'weightPerCapita' | 'carbon'; |
| | | |
| | | /** 首页菜单固定为 /report/screen/index,子页通过 ?page= 切换,避免离开首页入口 */ |
| | |
| | | { key: 'resource', index: '02', label: '垃圾资源化分析', audience: '后勤 / 环保' }, |
| | | { key: 'carbon', index: '03', label: '碳减排与绿色贡献', audience: '双碳 / ESG' }, |
| | | { key: 'participation', index: '04', label: '师生参与与碳积分', audience: '学工 / 团委' }, |
| | | { key: 'operation', index: '05', label: '回收运营监控', audience: '运营公司' } |
| | | { key: 'operation', index: '05', label: '回收运营监控', audience: '运营公司' }, |
| | | { key: 'map', index: '06', label: '地理信息总览', audience: '指挥中心' } |
| | | ]; |
| | | |
| | | /** 从路由解析当前大屏子页;index / 空 / 非法值均回落到 overview */ |
| | | export function resolveScreenKey(page?: string | string[] | null, queryPage?: string | string[] | null): ScreenKey { |
| | | const raw = String(queryPage || page || 'overview'); |
| | | if (raw === 'index') return 'overview'; |
| | | return SCREEN_NAV.some(e => e.key === raw) ? (raw as ScreenKey) : 'overview'; |
| | | if (raw === 'index') return 'map'; //if (raw === 'index') return 'overview'; |
| | | return SCREEN_NAV.some(e => e.key === raw) ? (raw as ScreenKey) : 'map'; |
| | | //return SCREEN_NAV.some(e => e.key === raw) ? (raw as ScreenKey) : 'overview'; |
| | | } |
| | | |
| | | export const TIME_RANGE_OPTIONS: { label: string; value: TimeRange }[] = [ |
| | |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | @import '../../styles/screen-kpi.scss'; |
| | | @use '../../styles/screen-kpi' as *; |
| | | |
| | | .screen-page { |
| | | .mid-row { |
| | |
| | | import { computed } from 'vue'; |
| | | import { useRoute } from 'vue-router'; |
| | | import ScreenShell from '../../components/screen-shell.vue'; |
| | | import Map from './map.vue'; |
| | | import Overview from './overview.vue'; |
| | | import Resource from './resource.vue'; |
| | | import Carbon from './carbon.vue'; |
| | |
| | | resource: Resource, |
| | | carbon: Carbon, |
| | | participation: Participation, |
| | | operation: Operation |
| | | operation: Operation, |
| | | map: Map |
| | | }; |
| | | |
| | | /** 停留在 /report/screen/index,用 query.page(或动态路由 :page)切换子页,不再强制跳走 */ |
| | | const currentKey = computed<ScreenKey>(() => |
| | | resolveScreenKey(route.params.page as string, route.query.page as string) |
| | | ); |
| | | const currentKey = computed<ScreenKey>(() =>{ |
| | | const key = resolveScreenKey(route.params.page as string, route.query.page as string); |
| | | console.log('[screen] params.page=', route.params.page, 'query.page=', route.query.page, '=> key=', key); |
| | | return key; |
| | | }); |
| | | </script> |
| New file |
| | |
| | | <template> |
| | | <div class="screen-page dash"> |
| | | <!-- KPI 指标条 --> |
| | | <div class="kpi-grid" style="--kpi-cols: 8"> |
| | | <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" /> |
| | | </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 { useChartTheme, useScreenData, useScreenFieldSource } from '../../hooks/use-screen'; |
| | | |
| | | const { data } = 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' } |
| | | ]; |
| | | |
| | | /** 顶部 KPI:取总览域核心 8 项 */ |
| | | const kpis = computed(() => [ |
| | | { field: 'kpis.todayWeightKg', label: '今日回收量', value: data.value.kpis.todayWeightKg, suffix: 'kg', fixed: 1 }, |
| | | { field: 'kpis.periodWeightTon', label: '本区间回收量', 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', formatter: '{b}<br/>{c} ({d}%)', ...tooltip() }, |
| | | series: [ |
| | | { |
| | | type: 'pie', |
| | | radius: ['38%', '62%'], |
| | | center: ['50%', '50%'], |
| | | padAngle: 3, |
| | | itemStyle: { borderRadius: 6 }, |
| | | label: { color: textColor.value, formatter: '{b}\n{d}%' }, |
| | | data: data.value.wasteMix.map(e => ({ |
| | | name: e.name, |
| | | value: e[mixMetric.value], |
| | | itemStyle: { color: e.color } |
| | | })) |
| | | } |
| | | ] |
| | | })); |
| | | |
| | | /** 碳减排来源(饼) */ |
| | | const carbonOption = computed(() => ({ |
| | | tooltip: { trigger: 'item', formatter: '{b}<br/>{c} kgCO₂e ({d}%)', ...tooltip() }, |
| | | series: [ |
| | | { |
| | | type: 'pie', |
| | | radius: ['36%', '60%'], |
| | | center: ['50%', '50%'], |
| | | padAngle: 3, |
| | | itemStyle: { borderRadius: 6 }, |
| | | label: { color: textColor.value, formatter: '{b}\n{d}%' }, |
| | | data: data.value.recycleItems.map(e => ({ |
| | | name: e.name + '回收', |
| | | value: e.carbon, |
| | | itemStyle: { color: 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 rows = [...data.value.recycleItems].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.name), |
| | | axisLabel: { color: textColor.value }, |
| | | axisLine: { show: false }, |
| | | axisTick: { show: false } |
| | | }, |
| | | series: [ |
| | | { |
| | | type: 'bar', |
| | | barWidth: 14, |
| | | data: rows.map(e => Number(e[itemMetric.value] || 0)), |
| | | itemStyle: { |
| | | borderRadius: [0, 8, 8, 0], |
| | | // 沿用各类自身配色,与左侧碳减排饼图视觉一致 |
| | | color: (params: any) => rows[params.dataIndex]?.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; |
| | | } |
| | | } |
| | | } |
| | | </style> |
| | |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | @import '../../styles/screen-kpi.scss'; |
| | | @use '../../styles/screen-kpi' as *; |
| | | |
| | | .screen-page { |
| | | .mid-row { |
| | |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | @import '../../styles/screen-kpi.scss'; |
| | | @use '../../styles/screen-kpi' as *; |
| | | |
| | | .screen-page { |
| | | .charts-row, |
| | |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | @import '../../styles/screen-kpi.scss'; |
| | | @use '../../styles/screen-kpi' as *; |
| | | |
| | | .screen-page { |
| | | .mid-row { |
| | |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | @import '../../styles/screen-kpi.scss'; |
| | | @use '../../styles/screen-kpi' as *; |
| | | |
| | | .screen-page { |
| | | .mid-row { |
| | |
| | | |
| | | export default (): ModuleConfig => { |
| | | return { |
| | | toolbar: { |
| | | component: import('./components/code.vue') |
| | | } |
| | | // toolbar: { |
| | | // component: import('./components/code.vue') |
| | | // } |
| | | }; |
| | | }; |