| | |
| | | # 应用名称 |
| | | VITE_NAME = " WOMP " |
| | | VITE_NAME = " 无废校园 " |
| | | |
| | | # 网络超时请求时间 |
| | | VITE_TIMEOUT = 30000 |
| | |
| | | export const MAP_TK = '51915011aa0997aa2ddd7e38504fa8bc'; |
| | | export const MAP_LOAD = { v: '4.0', tk: MAP_TK }; |
| | | export const DEFAULT_CENTER: LngLat = [121.280637, 32.125178]; |
| | | export const DEFAULT_ZOOM = 16; |
| | | export const MAP_ZOOM_KEY = 'map.zoomlevel'; |
| | | export const MAP_COORD_KEY = 'map.coordinates'; |
| | | |
| | | function paramText(raw: unknown): string { |
| | | if (raw == null) return ''; |
| | | if (typeof raw === 'string' || typeof raw === 'number') return String(raw).trim(); |
| | | if (Array.isArray(raw)) return raw.map(e => String(e).trim()).join(','); |
| | | return ''; |
| | | } |
| | | |
| | | export function parseMapZoom(raw: unknown, fallback = DEFAULT_ZOOM) { |
| | | const n = Number(paramText(raw)); |
| | | return Number.isFinite(n) && n > 0 ? n : fallback; |
| | | } |
| | | |
| | | export function parseMapCenter(raw: unknown, fallback: LngLat = DEFAULT_CENTER): LngLat { |
| | | const text = paramText(raw); |
| | | const parts = text.split(/[,,]/).map(s => Number(s.trim())); |
| | | if (parts.length >= 2 && Number.isFinite(parts[0]) && Number.isFinite(parts[1])) { |
| | | return [parts[0], parts[1]]; |
| | | } |
| | | return fallback; |
| | | } |
| | | |
| | | /** 从 base_sys_param 读取学校地图默认视角:map.zoomlevel、map.coordinates(lon,lat) */ |
| | | export async function loadMapView(service: any): Promise<{ center: LngLat; zoom: number }> { |
| | | const read = async (key: string) => { |
| | | try { |
| | | if (service?.base?.sys?.param?.stringbykey) { |
| | | return await service.base.sys.param.stringbykey({ key }); |
| | | } |
| | | return await service.request({ |
| | | url: 'admin/base/sys/param/stringbykey', |
| | | params: { key } |
| | | }); |
| | | } catch { |
| | | return ''; |
| | | } |
| | | }; |
| | | const [zoomRaw, coordRaw] = await Promise.all([read(MAP_ZOOM_KEY), read(MAP_COORD_KEY)]); |
| | | return { |
| | | center: parseMapCenter(coordRaw), |
| | | zoom: parseMapZoom(zoomRaw) |
| | | }; |
| | | } |
| | | |
| | | export function pinIcon(color: string) { |
| | | const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="40" viewBox="0 0 28 40"><path fill="${color}" stroke="#fff" stroke-width="1.5" d="M14 1C7.4 1 2 6.4 2 13c0 8.8 12 25 12 25s12-16.2 12-25C26 6.4 20.6 1 14 1z"/><circle cx="14" cy="13" r="5" fill="#fff"/></svg>`; |
| | |
| | | required: true |
| | | }, |
| | | { |
| | | label: t('重量'), |
| | | label: t('重量(kg)'), |
| | | prop: "weight", |
| | | hook: "number", |
| | | component: { |
| | |
| | | sortable: "custom", |
| | | component: { name: "cl-date-text" } |
| | | }, |
| | | { label: t('重量'), prop: "weight", minWidth: 140, sortable: "custom" }, |
| | | { label: t('重量(kg)'), prop: "weight", minWidth: 140, sortable: "custom" }, |
| | | { label: t('单价'), prop: "unitPrice", minWidth: 140, sortable: "custom" }, |
| | | { label: t('总价'), prop: "totalPrice", minWidth: 140, sortable: "custom" }, |
| | | { |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 地图主体 --> |
| | | <!-- 地图主体:等参数就绪再创建,避免天地图只在初始化时吃 center/zoom --> |
| | | <div class="map-panel__map"> |
| | | <tdt-map :center="center" :zoom="zoom" :controls="controls" :loadConfig="MAP_LOAD"> |
| | | <tdt-map |
| | | v-if="viewReady" |
| | | :center="center" |
| | | :zoom="zoom" |
| | | :controls="controls" |
| | | :loadConfig="MAP_LOAD" |
| | | @init="onMapInit" |
| | | > |
| | | <tdt-marker |
| | | v-for="item in list" |
| | | :key="keyOf(item)" |
| | |
| | | |
| | | import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; |
| | | import { TdtMap, TdtMarker, type LngLat } from 'vue-tianditu2'; |
| | | import { useCool } from '/@/cool'; |
| | | import { |
| | | DEFAULT_CENTER, |
| | | DEFAULT_ZOOM, |
| | | MAP_LOAD, |
| | | SORTER_ICON, |
| | | VEHICLE_ICON, |
| | | formatTime, |
| | | loadMapView, |
| | | siteIcon, |
| | | useMapsApi |
| | | } from '../../maps/hooks/use-map'; |
| | |
| | | } |
| | | }; |
| | | |
| | | const { service } = useCool(); |
| | | const api = useMapsApi(); |
| | | |
| | | const layer = ref<LayerKey>('site'); |
| | | const list = ref<any[]>([]); |
| | | const center = ref<LngLat>(DEFAULT_CENTER); |
| | | const zoom = ref(12); |
| | | const zoom = ref(DEFAULT_ZOOM); |
| | | const viewReady = ref(false); |
| | | const lastUpdate = ref(''); |
| | | |
| | | const controls = ['Zoom', 'Scale']; |
| | |
| | | return cfg.value.icon(item); |
| | | } |
| | | |
| | | function applyView(map: any) { |
| | | const [lon, lat] = center.value; |
| | | if (!map || !Number.isFinite(lon) || !Number.isFinite(lat)) return; |
| | | const TMap = (window as any).T; |
| | | const lnglat = TMap?.LngLat ? new TMap.LngLat(lon, lat) : [lon, lat]; |
| | | if (typeof map.centerAndZoom === 'function') { |
| | | map.centerAndZoom(lnglat, zoom.value); |
| | | } else { |
| | | map.panTo?.(lnglat); |
| | | map.setZoom?.(zoom.value); |
| | | } |
| | | map.checkResize?.(); |
| | | } |
| | | |
| | | function onMapInit(map: any) { |
| | | applyView(map); |
| | | window.setTimeout(() => applyView(map), 200); |
| | | } |
| | | |
| | | 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 轮询;站点是静态台账,不轮询 |
| | |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | const view = await loadMapView(service); |
| | | center.value = view.center; |
| | | zoom.value = view.zoom; |
| | | viewReady.value = true; |
| | | await load(); |
| | | startPolling(); |
| | | }); |
| New file |
| | |
| | | export type ChartRow<T> = { item: T; value: number; even: boolean }; |
| | | |
| | | /** 图表取值:0 不画;碳减排可为负,展示时取绝对值。 */ |
| | | export function chartValue(raw: number, carbon = false) { |
| | | const n = Number(raw); |
| | | if (!Number.isFinite(n) || n === 0) return 0; |
| | | return carbon ? Math.abs(n) : n; |
| | | } |
| | | |
| | | /** |
| | | * 分类图数据: |
| | | * - 部分有值:只保留当前指标 > 0 的类 |
| | | * - 全部为 0:evenSplit 时均分扇区(value=1 仅用于几何,even 标记给 tooltip 显示 0) |
| | | */ |
| | | export function chartSeries<T>( |
| | | items: T[] | undefined, |
| | | pick: (item: T) => number, |
| | | opts?: { carbon?: boolean; evenSplit?: boolean } |
| | | ): ChartRow<T>[] { |
| | | const list = items || []; |
| | | const rows = list.map(item => ({ |
| | | item, |
| | | raw: chartValue(pick(item), opts?.carbon) |
| | | })); |
| | | const positive = rows.filter(e => e.raw > 0); |
| | | if (positive.length) { |
| | | return positive.map(e => ({ item: e.item, value: e.raw, even: false })); |
| | | } |
| | | if (opts?.evenSplit === false) { |
| | | return rows.map(e => ({ item: e.item, value: 0, even: false })); |
| | | } |
| | | return rows.map(e => ({ item: e.item, value: 1, even: true })); |
| | | } |
| | | |
| | | export function pieTooltipFormatter(unit = '') { |
| | | return (p: any) => { |
| | | const val = p.data?.even ? 0 : p.value; |
| | | const u = unit ? ` ${unit}` : ''; |
| | | const pct = p.percent != null ? ` (${p.percent}%)` : ''; |
| | | return `${p.name}<br/>${val}${u}${pct}`; |
| | | }; |
| | | } |
| | | |
| | | export function toPieData<T>( |
| | | rows: ChartRow<T>[], |
| | | name: (item: T) => string, |
| | | color: (item: T) => string | undefined |
| | | ) { |
| | | return rows.map(row => ({ |
| | | name: name(row.item), |
| | | value: row.value, |
| | | even: row.even, |
| | | itemStyle: { color: color(row.item) } |
| | | })); |
| | | } |
| | |
| | | <template> |
| | | <div class="screen-page"> |
| | | <div class="kpi-grid" style="--kpi-cols: 5"> |
| | | <div class="kpi-grid" style="--kpi-cols: 3"> |
| | | <kpi-card v-for="item in kpis" :key="item.label" v-bind="item" /> |
| | | </div> |
| | | |
| | |
| | | import KpiCard from '../../components/kpi-card.vue'; |
| | | import PanelCard from '../../components/panel-card.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 kpis = computed(() => [ |
| | | { field: 'kpis.totalCarbonTon', label: '累计碳减排', value: data.value.kpis.totalCarbonTon, suffix: '吨CO₂e', fixed: 1 }, |
| | | { field: 'kpis.periodCarbonTon', label: `${rangeLabel.value}碳减排`, 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.greenActions', label: '累计环保行为', value: data.value.kpis.greenActions, suffix: '次', fixed: 0 } |
| | | { field: 'kpis.periodWeightTon', label: `${rangeLabel.value}资源回收`, value: data.value.kpis.periodWeightTon, suffix: '吨', fixed: 2, hint: '当前时间范围' }, |
| | | { field: 'kpis.newParticipants', label: `${rangeLabel.value}参与师生`, value: data.value.kpis.newParticipants, suffix: '人', fixed: 0, hint: '区间内出现过的去重用户' }, |
| | | { field: 'kpis.greenActions', label: `${rangeLabel.value}环保行为`, value: data.value.kpis.greenActions, suffix: '次', fixed: 0, hint: '区间内已完成订单数' } |
| | | ]); |
| | | |
| | | const carbonTrendDemo = computed(() => |
| | |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | name: '吨CO₂e', |
| | | name: 'tCO₂e', |
| | | nameTextStyle: { color: mutedColor.value }, |
| | | splitLine: { lineStyle: { color: splitColor.value, type: 'dashed' } }, |
| | | axisLabel: { color: mutedColor.value }, |
| | |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | name: '吨CO₂e', |
| | | name: 'tCO₂e', |
| | | nameTextStyle: { color: mutedColor.value }, |
| | | splitLine: { lineStyle: { color: splitColor.value, type: 'dashed' } }, |
| | | axisLabel: { color: mutedColor.value }, |
| | |
| | | })); |
| | | |
| | | const sourceOption = computed(() => ({ |
| | | tooltip: { trigger: 'item', formatter: '{b}<br/>{c} kgCO₂e ({d}%)', ...tooltip() }, |
| | | tooltip: { trigger: 'item', ...tooltip(), formatter: pieTooltipFormatter('kgCO₂e') }, |
| | | legend: { bottom: 0, textStyle: { color: mutedColor.value } }, |
| | | series: [ |
| | | { |
| | |
| | | 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 } |
| | | })) |
| | | data: toPieData( |
| | | chartSeries(data.value.recycleItems, e => e.carbon, { carbon: true }), |
| | | e => e.name + '回收', |
| | | e => e.color |
| | | ) |
| | | } |
| | | ] |
| | | })); |
| | |
| | | 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 itemMetric = ref<'weight' | 'value' | 'carbon'>('weight'); |
| | | |
| | | const mixOptions = [ |
| | | { label: '重量', value: 'weight' }, |
| | | { label: '价值', value: 'value' }, |
| | | { label: '数量', value: 'count' } |
| | | { label: '重量(kg)', value: 'weight' }, |
| | | { label: '价值(元)', value: 'value' }, |
| | | { label: '计数(次)', value: 'count' } |
| | | ]; |
| | | |
| | | const trendOptions = [ |
| | | { label: '重量', value: 'weight' }, |
| | | { label: '价值', value: 'value' }, |
| | | { label: '碳减排', value: 'carbon' } |
| | | { label: '重量(kg)', value: 'weight' }, |
| | | { label: '价值(元)', value: 'value' }, |
| | | { label: '碳减排(kgCO₂e)', value: 'carbon' } |
| | | ]; |
| | | |
| | | const itemOptions = [ |
| | | { label: '重量', value: 'weight' }, |
| | | { label: '价值', value: 'value' }, |
| | | { label: '碳减排', value: 'carbon' } |
| | | { label: '重量(kg)', value: 'weight' }, |
| | | { label: '价值(元)', value: 'value' }, |
| | | { label: '碳减排(kgCO₂e)', value: 'carbon' } |
| | | ]; |
| | | |
| | | const rangeLabel = 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.periodCarbonTon', label: '碳减排(tCO₂e)', value: data.value.kpis.periodCarbonTon, suffix: 'tCO₂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 mixOption = computed(() => ({ |
| | | tooltip: { trigger: 'item', formatter: '{b}<br/>{c} ({d}%)', ...tooltip() }, |
| | | tooltip: { trigger: 'item', ...tooltip(), formatter: pieTooltipFormatter() }, |
| | | series: [ |
| | | { |
| | | type: 'pie', |
| | |
| | | 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 } |
| | | })) |
| | | data: toPieData( |
| | | chartSeries(data.value.wasteMix, e => e[mixMetric.value]), |
| | | e => e.name, |
| | | e => e.color |
| | | ) |
| | | } |
| | | ] |
| | | })); |
| | | |
| | | /** 碳减排来源(饼) */ |
| | | /** 碳减排来源(饼):碳值可为负,按绝对值展示减排量 */ |
| | | const carbonOption = computed(() => ({ |
| | | tooltip: { trigger: 'item', formatter: '{b}<br/>{c} kgCO₂e ({d}%)', ...tooltip() }, |
| | | tooltip: { trigger: 'item', ...tooltip(), formatter: pieTooltipFormatter('kgCO₂e') }, |
| | | series: [ |
| | | { |
| | | type: 'pie', |
| | |
| | | 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 } |
| | | })) |
| | | data: toPieData( |
| | | chartSeries(data.value.recycleItems, e => e.carbon, { carbon: true }), |
| | | e => e.name + '回收', |
| | | e => e.color |
| | | ) |
| | | } |
| | | ] |
| | | })); |
| | |
| | | |
| | | /** 各类回收量(横向条形) */ |
| | | const itemOption = computed(() => { |
| | | // 反转让数值最大的排在顶部 |
| | | const rows = [...data.value.recycleItems].reverse(); |
| | | 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: { |
| | |
| | | }, |
| | | yAxis: { |
| | | type: 'category', |
| | | data: rows.map(e => e.name), |
| | | data: rows.map(e => e.item.name), |
| | | axisLabel: { color: textColor.value }, |
| | | axisLine: { show: false }, |
| | | axisTick: { show: false } |
| | |
| | | { |
| | | type: 'bar', |
| | | barWidth: 14, |
| | | data: rows.map(e => Number(e[itemMetric.value] || 0)), |
| | | data: rows.map(e => e.value), |
| | | itemStyle: { |
| | | borderRadius: [0, 8, 8, 0], |
| | | // 沿用各类自身配色,与左侧碳减排饼图视觉一致 |
| | | color: (params: any) => rows[params.dataIndex]?.color || primary.value |
| | | color: (params: any) => rows[params.dataIndex]?.item.color || primary.value |
| | | } |
| | | } |
| | | ] |
| | |
| | | import PanelCard from '../../components/panel-card.vue'; |
| | | import ScreenCameraPreview from '../../components/screen-camera-preview.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 trendMetric = ref<'weight' | 'value' | 'carbon'>('weight'); |
| | | |
| | | const mixOptions = [ |
| | | { label: '重量', value: 'weight' }, |
| | | { label: '价值', value: 'value' }, |
| | | { label: '数量', value: 'count' } |
| | | { label: '重量(kg)', value: 'weight' }, |
| | | { label: '价值(元)', value: 'value' }, |
| | | { label: '计数(次)', value: 'count' } |
| | | ]; |
| | | |
| | | const trendOptions = [ |
| | | { label: '重量', value: 'weight' }, |
| | | { label: '价值', value: 'value' }, |
| | | { label: '碳减排', value: 'carbon' } |
| | | { label: '重量(kg)', value: 'weight' }, |
| | | { label: '价值(元)', value: 'value' }, |
| | | { label: '碳减排(kgCO₂e)', value: 'carbon' } |
| | | ]; |
| | | |
| | | const rangeLabel = computed( |
| | |
| | | ); |
| | | |
| | | const kpis = computed(() => [ |
| | | { field: 'kpis.periodWeightTon', label: `${rangeLabel.value}回收量`, value: data.value.kpis.periodWeightTon, suffix: '吨', fixed: 2, hint: '当前时间范围' }, |
| | | { field: 'kpis.periodWeightTon', label: `${rangeLabel.value}回收量`, value: data.value.kpis.periodWeightTon, suffix: 't', fixed: 2, hint: '当前时间范围' }, |
| | | { field: 'kpis.resourceRate', label: '资源化率', value: data.value.kpis.resourceRate, suffix: '%', fixed: 1, hint: '可回收物重量/总重量,不含大件' }, |
| | | { field: 'kpis.periodValue', label: '回收价值', value: data.value.kpis.periodValue, suffix: '元', fixed: 0, hint: '日表估算销售额' }, |
| | | { field: 'kpis.periodCarbonTon', label: '碳减排', value: data.value.kpis.periodCarbonTon, suffix: '吨CO₂e', fixed: 2, hint: '分类重量 × 碳减因数' }, |
| | | { field: 'kpis.totalWeightTon', label: '累计回收量', value: data.value.kpis.totalWeightTon, suffix: '吨', fixed: 1, hint: '上线以来累计' }, |
| | | { field: 'kpis.totalParticipants', label: '累计参与人数', value: data.value.kpis.totalParticipants, suffix: '人', fixed: 0, hint: '去重校园用户' }, |
| | | { field: 'kpis.totalPoints', label: '累计碳积分', value: data.value.kpis.totalPoints, suffix: '分', fixed: 0, hint: '账户余额合计' } |
| | | { field: 'kpis.periodCarbonTon', label: '碳减排', value: data.value.kpis.periodCarbonTon, suffix: 'tCO₂e', fixed: 2, hint: '分类重量 × 碳减因数' }, |
| | | { field: 'kpis.totalWeightTon', label: '累计回收量', value: data.value.kpis.totalWeightTon, suffix: 't', fixed: 1, hint: '上线以来累计' }, |
| | | { field: 'kpis.totalPoints', label: '累计碳积分', value: data.value.kpis.totalPoints, suffix: '分', fixed: 0, hint: '账户余额合计' }, |
| | | { field: 'kpis.totalParticipants', label: '累计参与人数', value: data.value.kpis.totalParticipants, suffix: '人', fixed: 0, hint: '去重校园用户' } |
| | | ]); |
| | | |
| | | const mixOption = computed(() => ({ |
| | | tooltip: { trigger: 'item', formatter: '{b}<br/>{c} ({d}%)', ...tooltip() }, |
| | | tooltip: { trigger: 'item', ...tooltip(), formatter: pieTooltipFormatter() }, |
| | | legend: { bottom: 0, textStyle: { color: mutedColor.value } }, |
| | | series: [ |
| | | { |
| | |
| | | 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 } |
| | | })) |
| | | data: toPieData( |
| | | chartSeries(data.value.wasteMix, e => e[mixMetric.value]), |
| | | e => e.name, |
| | | e => e.color |
| | | ) |
| | | } |
| | | ] |
| | | })); |
| | |
| | | { label: '回收重量', value: 'weight' }, |
| | | { label: '人均回收量', value: 'weightPerCapita' }, |
| | | { label: '人均积分', value: 'pointPerCapita' }, |
| | | { label: '碳减排', value: 'carbon' } |
| | | { label: '碳减排(kgCO₂e)', value: 'carbon' } |
| | | ]; |
| | | |
| | | const rangeLabel = computed( |
| | |
| | | import { computed } from 'vue'; |
| | | import KpiCard from '../../components/kpi-card.vue'; |
| | | import PanelCard from '../../components/panel-card.vue'; |
| | | import { chartSeries, pieTooltipFormatter, toPieData } from '../../data/screen-chart'; |
| | | import { useChartTheme, useScreenData, useScreenFieldSource } from '../../hooks/use-screen'; |
| | | |
| | | const { data } = useScreenData(); |
| | |
| | | ]); |
| | | |
| | | const pieOption = computed(() => ({ |
| | | tooltip: { trigger: 'item', formatter: '{b}<br/>{c} kg ({d}%)', ...tooltip() }, |
| | | tooltip: { trigger: 'item', ...tooltip(), formatter: pieTooltipFormatter('kg') }, |
| | | legend: { bottom: 0, textStyle: { color: mutedColor.value } }, |
| | | series: [ |
| | | { |
| | |
| | | padAngle: 3, |
| | | itemStyle: { borderRadius: 6 }, |
| | | label: { color: textColor.value, formatter: '{b} {d}%' }, |
| | | data: data.value.recycleItems.map(e => ({ |
| | | name: e.name, |
| | | value: e.weight, |
| | | itemStyle: { color: e.color } |
| | | })) |
| | | data: toPieData( |
| | | chartSeries(data.value.recycleItems, e => e.weight), |
| | | e => e.name, |
| | | e => e.color |
| | | ) |
| | | } |
| | | ] |
| | | })); |
| | | |
| | | const weightRows = computed(() => |
| | | chartSeries(data.value.recycleItems, e => e.weight, { evenSplit: false }).reverse() |
| | | ); |
| | | |
| | | const weightOption = computed(() => ({ |
| | | grid: { containLabel: true, left: 8, right: 28, top: 12, bottom: 8 }, |
| | |
| | | }, |
| | | yAxis: { |
| | | type: 'category', |
| | | data: data.value.recycleItems.map(e => e.name).reverse(), |
| | | data: weightRows.value.map(e => e.item.name), |
| | | axisLabel: { color: textColor.value }, |
| | | axisLine: { show: false }, |
| | | axisTick: { show: false } |
| | |
| | | { |
| | | type: 'bar', |
| | | barWidth: 16, |
| | | data: data.value.recycleItems |
| | | .map(e => ({ value: e.weight, itemStyle: { color: e.color, borderRadius: [0, 8, 8, 0] } })) |
| | | .reverse() |
| | | data: weightRows.value.map(e => ({ |
| | | value: e.value, |
| | | itemStyle: { color: e.item.color, borderRadius: [0, 8, 8, 0] } |
| | | })) |
| | | } |
| | | ] |
| | | })); |
| | | |
| | | const valueRows = computed(() => |
| | | chartSeries(data.value.recycleItems, e => e.value, { evenSplit: false }) |
| | | ); |
| | | |
| | | const valueOption = computed(() => ({ |
| | | grid: { containLabel: true, left: 16, right: 16, top: 28, bottom: 8 }, |
| | | tooltip: { trigger: 'axis', ...tooltip() }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: data.value.recycleItems.map(e => e.name), |
| | | data: valueRows.value.map(e => e.item.name), |
| | | axisLabel: { color: mutedColor.value }, |
| | | axisLine: { show: false }, |
| | | axisTick: { show: false } |
| | |
| | | { |
| | | type: 'bar', |
| | | barWidth: 28, |
| | | data: data.value.recycleItems.map(e => e.value), |
| | | data: valueRows.value.map(e => e.value), |
| | | itemStyle: { color: primary.value, borderRadius: [6, 6, 0, 0] } |
| | | } |
| | | ] |