<template>
|
<div ref="screenRoot"
|
class="sales-statistics-container"
|
:class="{ 'is-fullscreen': isFullscreen }">
|
<div class="bi-bg"></div>
|
<div class="bi-topbar">
|
<img class="bi-topbar-title-bg"
|
src="@/assets/BI/biaoti.png"
|
alt="生产看板统计" />
|
<div class="bi-topbar-content">
|
<div class="bi-topbar-left">
|
<button class="fullscreen-btn"
|
@click="toggleFullscreen"
|
:title="isFullscreen ? '退出全屏' : '全屏显示'">
|
<svg v-if="!isFullscreen"
|
width="1.6vh"
|
height="1.6vh"
|
viewBox="0 0 24 24"
|
fill="none"
|
stroke="currentColor"
|
stroke-width="2">
|
<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3" />
|
</svg>
|
<svg v-else
|
width="1.6vh"
|
height="1.6vh"
|
viewBox="0 0 24 24"
|
fill="none"
|
stroke="currentColor"
|
stroke-width="2">
|
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" />
|
</svg>
|
</button>
|
</div>
|
<div class="bi-topbar-title">生产看板统计</div>
|
<div class="bi-topbar-meta">
|
<span class="bi-topbar-time">{{ currentTime }}</span>
|
<span class="bi-topbar-sep">|</span>
|
<span class="bi-topbar-date">{{ currentDateText }}</span>
|
</div>
|
</div>
|
</div>
|
<div class="bi-dashboard-grid">
|
<!-- 左上:生产成本单耗统计(砌块) -->
|
<div class="bi-panel bi-panel-top-left">
|
<PanelHeader :isFullscreen="true"
|
title="生产成本单耗统计(砌块)" />
|
<div class="panel-tabs">
|
<span class="tab-item"
|
:class="{ active: blockTimeDimension === 'year' }"
|
@click="handleBlockTimeDimensionChange('year')">年</span>
|
<span class="tab-item"
|
:class="{ active: blockTimeDimension === 'month' }"
|
@click="handleBlockTimeDimensionChange('month')">月</span>
|
</div>
|
<div class="bi-panel-body">
|
<div class="chart-filter-tabs">
|
<span v-for="name in blockMaterialList"
|
:key="name"
|
class="cf-tab"
|
:class="{ active: blockSelectedMaterial === name }"
|
@click="selectBlockMaterial(name)">{{ name }}</span>
|
</div>
|
<div v-if="blockSelectedMaterial !== '全部'"
|
class="material-info-card">
|
<div class="material-icon">
|
<svg width="24"
|
height="24"
|
viewBox="0 0 24 24"
|
fill="none"
|
stroke="currentColor"
|
stroke-width="2">
|
<path d="M20 7h-4V4c0-1.1-.9-2-2-2h-4c-1.1 0-2 .9-2 2v3H4c-1.1 0-2 .9-2 2v11c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2z" />
|
<polyline points="22,7 12,13 2,7" />
|
</svg>
|
</div>
|
<div class="material-details">
|
<div class="material-name">{{ blockMaterialSummary.materialName || "—" }}</div>
|
<div class="material-stats">
|
<div class="stat-item">
|
<span class="stat-label">月累计单耗</span>
|
<span class="stat-value">{{ blockMaterialSummary.monthlyConsumption }}</span>
|
<span class="stat-unit">吨</span>
|
</div>
|
<div class="stat-item">
|
<span class="stat-label">年累计单耗</span>
|
<span class="stat-value">{{ blockMaterialSummary.yearlyConsumption }}</span>
|
<span class="stat-unit">吨</span>
|
</div>
|
</div>
|
</div>
|
</div>
|
<!-- <div class="chart-unit-row">
|
<span>单位:立方米</span>
|
</div> -->
|
<div ref="blockCostChart"
|
class="echart-fill"></div>
|
</div>
|
</div>
|
<!-- 右上:产量分析 -->
|
<div class="bi-panel bi-panel-top-right">
|
<PanelHeader :isFullscreen="true"
|
title="物料生产量分析" />
|
<div class="panel-tabs">
|
<span class="tab-item"
|
:class="{ active: productionTimeDimension === 'year' }"
|
@click="handleProductionTimeDimensionChange('year')">年</span>
|
<span class="tab-item"
|
:class="{ active: productionTimeDimension === 'month' }"
|
@click="handleProductionTimeDimensionChange('month')">月</span>
|
</div>
|
<div class="bi-panel-body">
|
<div class="chart-filter-tabs">
|
<span v-for="cat in productionCategories"
|
:key="cat"
|
class="cf-tab"
|
:class="{ active: productionCategory === cat }"
|
@click="selectProductionCategory(cat)">{{ cat }}</span>
|
</div>
|
<div class="chart-unit-row">
|
<span>单位:件</span>
|
</div>
|
<div ref="productionChart"
|
class="echart-fill"></div>
|
</div>
|
</div>
|
<!-- 中间中心环 -->
|
<div class="center-ring">
|
<div class="center-ring-box">
|
<div class="ring-box-topright">
|
<div class="topright-label">固废处理量</div>
|
</div>
|
<div class="ring-box-left">
|
<div class="left-label">粉煤灰</div>
|
<div class="left-value">月处理 <span style="font-weight: bold;font-size: 1.3vh;">{{ middleRingStats.flyAshMonth }}</span> 吨 年处理 <span style="font-weight: bold;font-size: 1.3vh;">{{ middleRingStats.flyAshYear }}</span> 吨</div>
|
<div class="left-label"
|
style="margin-top: 2vh;">石膏</div>
|
<div class="left-value">月处理 <span style="font-weight: bold;font-size: 1.3vh;">{{ middleRingStats.gypsumMonth }}</span> 吨 年处理 <span style="font-weight: bold;font-size: 1.3vh;">{{ middleRingStats.gypsumYear }}</span> 吨</div>
|
</div>
|
<div class="ring-box-topleft">
|
<div class="topleft-label">项目产量</div>
|
</div>
|
<div class="ring-box-right">
|
<div class="right-label">砌块产量</div>
|
<div class="right-value">月产量 <span style="font-weight: bold;font-size: 1.3vh;">{{ middleRingStats.blockMonth }}</span> 吨 年产量 <span style="font-weight: bold;font-size: 1.3vh;">{{ middleRingStats.blockYear }}</span> 吨</div>
|
<div class="right-label"
|
style="margin-top: 2vh;">板材产量</div>
|
<div class="right-value">月产量 <span style="font-weight: bold;font-size: 1.3vh;">{{ middleRingStats.plateMonth }}</span> 吨 年产量 <span style="font-weight: bold;font-size: 1.3vh;">{{ middleRingStats.plateYear }}</span> 吨</div>
|
</div>
|
</div>
|
</div>
|
<!-- 左下:生产成本单耗统计(板材) -->
|
<div class="bi-panel bi-panel-bottom-left">
|
<PanelHeader :isFullscreen="true"
|
title="生产成本单耗统计(板材)" />
|
<div class="panel-tabs">
|
<span class="tab-item"
|
:class="{ active: boardTimeDimension === 'year' }"
|
@click="handleBoardTimeDimensionChange('year')">年</span>
|
<span class="tab-item"
|
:class="{ active: boardTimeDimension === 'month' }"
|
@click="handleBoardTimeDimensionChange('month')">月</span>
|
</div>
|
<div class="bi-panel-body">
|
<div class="chart-filter-tabs">
|
<span v-for="name in boardMaterialList"
|
:key="name"
|
class="cf-tab"
|
:class="{ active: boardSelectedMaterial === name }"
|
@click="selectBoardMaterial(name)">{{ name }}</span>
|
</div>
|
<div v-if="boardSelectedMaterial !== '全部'"
|
class="material-info-card">
|
<div class="material-icon">
|
<svg width="24"
|
height="24"
|
viewBox="0 0 24 24"
|
fill="none"
|
stroke="currentColor"
|
stroke-width="2">
|
<path d="M20 7h-4V4c0-1.1-.9-2-2-2h-4c-1.1 0-2 .9-2 2v3H4c-1.1 0-2 .9-2 2v11c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2z" />
|
<polyline points="22,7 12,13 2,7" />
|
</svg>
|
</div>
|
<div class="material-details">
|
<div class="material-name">{{ boardMaterialSummary.materialName || "—" }}</div>
|
<div class="material-stats">
|
<div class="stat-item">
|
<span class="stat-label">月累计单耗</span>
|
<span class="stat-value">{{ boardMaterialSummary.monthlyConsumption }}</span>
|
<span class="stat-unit">吨</span>
|
</div>
|
<div class="stat-item">
|
<span class="stat-label">年累计单耗</span>
|
<span class="stat-value">{{ boardMaterialSummary.yearlyConsumption }}</span>
|
<span class="stat-unit">吨</span>
|
</div>
|
</div>
|
</div>
|
</div>
|
<!-- <div class="chart-unit-row">
|
<span>单位:立方米</span>
|
</div> -->
|
<div ref="boardCostChart"
|
class="echart-fill"></div>
|
</div>
|
</div>
|
<!-- 中下:新增客户趋势分析 -->
|
<div class="bi-panel bi-panel-bottom-center">
|
<PanelHeader :isFullscreen="true"
|
title="固废处理量" />
|
<div class="panel-tabs">
|
<span class="tab-item"
|
:class="{ active: customerTimeDimension === 'year' }"
|
@click="handleCustomerTimeDimensionChange('year')">年</span>
|
<span class="tab-item"
|
:class="{ active: customerTimeDimension === 'month' }"
|
@click="handleCustomerTimeDimensionChange('month')">月</span>
|
</div>
|
<div ref="customerTrendChart"
|
class="echart-fill"></div>
|
<!-- <div class="bi-panel-body">
|
<div class="chart-unit-row chart-unit-single">
|
<span>单位:家</span>
|
</div>
|
<div ref="customerTrendChart"
|
class="echart-fill"></div>
|
</div> -->
|
</div>
|
<!-- 右下:销量排名分析 -->
|
<div class="bi-panel bi-panel-bottom-right">
|
<PanelHeader :isFullscreen="true"
|
title="能耗统计" />
|
<div class="panel-tabs">
|
<span class="tab-item"
|
:class="{ active: salesTimeDimension === 'year' }"
|
@click="handleSalesTimeDimensionChange('year')">年</span>
|
<span class="tab-item"
|
:class="{ active: salesTimeDimension === 'month' }"
|
@click="handleSalesTimeDimensionChange('month')">月</span>
|
</div>
|
<div class="bi-panel-body">
|
<div ref="salesRankingChart"
|
class="echart-fill"></div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script setup>
|
import {
|
ref,
|
computed,
|
onMounted,
|
onBeforeUnmount,
|
nextTick,
|
} from "vue";
|
import * as echarts from "echarts";
|
import dayjs from "dayjs";
|
import PanelHeader from "@/views/reportAnalysis/PSIDataAnalysis/components/PanelHeader.vue";
|
import {
|
getMaterialProductionAnalysis,
|
getProductionMaterials,
|
getProductionStatisticsBlocks,
|
getProductionStatisticsPlates,
|
getProductionStatisticsMiddle,
|
getProductionStatisticsSolidWaste,
|
getProductionStatisticsEnergy,
|
} from "@/api/reportAnalysis/productionStatistics.js";
|
|
const screenRoot = ref(null);
|
const isFullscreen = ref(false);
|
|
// 顶部栏时间
|
const now = ref(dayjs());
|
const currentTime = computed(() => now.value.format("HH:mm:ss"));
|
const currentDateText = computed(() => {
|
const weekMap = {
|
0: "星期日",
|
1: "星期一",
|
2: "星期二",
|
3: "星期三",
|
4: "星期四",
|
5: "星期五",
|
6: "星期六",
|
};
|
return `${now.value.format("YYYY-MM-DD")} ${weekMap[now.value.day()] || ""}`;
|
});
|
let timeTicker = null;
|
|
const handleFullscreenChange = () => {
|
isFullscreen.value = !!document.fullscreenElement;
|
nextTick(() => {
|
handleResize();
|
});
|
};
|
|
const toggleFullscreen = async () => {
|
const rootEl = screenRoot.value;
|
if (!rootEl) return;
|
try {
|
if (!document.fullscreenElement) {
|
await rootEl.requestFullscreen();
|
} else {
|
await document.exitFullscreen();
|
}
|
} catch (error) {
|
console.error("全屏切换失败:", error);
|
}
|
};
|
|
// 图表引用
|
const blockCostChart = ref(null);
|
const boardCostChart = ref(null);
|
const productionChart = ref(null);
|
const customerTrendChart = ref(null);
|
const salesRankingChart = ref(null);
|
|
// 选择器数据
|
const blockTimeDimension = ref("year");
|
const boardTimeDimension = ref("year");
|
const productionTimeDimension = ref("year");
|
const customerTimeDimension = ref("year");
|
const salesTimeDimension = ref("year");
|
|
const productionCategories = ["全部", "砌块", "板材"];
|
const productionCategory = ref("全部");
|
|
const blockMaterialList = ref([]);
|
const blockSelectedMaterial = ref("");
|
const boardMaterialList = ref([]);
|
const boardSelectedMaterial = ref("");
|
|
const blockMaterialSummary = ref({
|
materialName: "",
|
monthlyConsumption: "--",
|
yearlyConsumption: "--",
|
});
|
const boardMaterialSummary = ref({
|
materialName: "",
|
monthlyConsumption: "--",
|
yearlyConsumption: "--",
|
});
|
|
/** xMode: time=横轴为时间;material=选「全部」时横轴为各物料名称 */
|
const blockCostChartSeries = ref({
|
xMode: "time",
|
categories: [],
|
input: [],
|
output: [],
|
});
|
const boardCostChartSeries = ref({
|
xMode: "time",
|
categories: [],
|
input: [],
|
output: [],
|
});
|
const productionChartSeries = ref({
|
categories: [],
|
/** single:单线;dual:全部时砌块+板材双线 */
|
mode: "single",
|
values: [],
|
blockValues: [],
|
plateValues: [],
|
});
|
|
// 固废处理量折线图(/home/productionStatistics/solidWaste)
|
const solidWasteChartSeries = ref({
|
categories: [],
|
total: [],
|
flyAsh: [],
|
gypsum: [],
|
lime: [],
|
});
|
|
// 能耗统计(/home/productionStatistics/energy)
|
const energyChartSeries = ref({
|
categories: [],
|
water: [],
|
electricity: [],
|
steam: [],
|
});
|
|
// 中心环:固废(粉煤灰/石膏)+ 项目产量(砌块/板材),接口 /home/productionStatistics/middle
|
const middleRingStats = ref({
|
flyAshMonth: 0,
|
flyAshYear: 0,
|
gypsumMonth: 0,
|
gypsumYear: 0,
|
blockMonth: 0,
|
blockYear: 0,
|
plateMonth: 0,
|
plateYear: 0,
|
});
|
|
// 图表实例
|
let blockCostChartInstance = null;
|
let boardCostChartInstance = null;
|
let productionChartInstance = null;
|
let customerTrendChartInstance = null;
|
let salesRankingChartInstance = null;
|
let blockCostChartResizeObserver = null;
|
let boardCostChartResizeObserver = null;
|
|
// 生产单耗图表配置(砌块,接口数据)
|
const blockCostChartOption = computed(() => {
|
const xMode = blockCostChartSeries.value.xMode || "time";
|
const periods = blockCostChartSeries.value.categories || [];
|
const inputData = blockCostChartSeries.value.input || [];
|
const outputData = blockCostChartSeries.value.output || [];
|
const legendNames = ["投入量", "产出量"];
|
const colors = ["#6B9DFF", "#8A6BFF"];
|
const series = [
|
{
|
name: legendNames[0],
|
data: inputData,
|
type: "bar",
|
itemStyle: { color: colors[0] },
|
},
|
{
|
name: legendNames[1],
|
data: outputData,
|
type: "bar",
|
itemStyle: { color: colors[1] },
|
},
|
];
|
|
return {
|
backgroundColor: "transparent",
|
tooltip: {
|
trigger: "axis",
|
backgroundColor: "rgba(0,0,0,0.55)",
|
borderColor: "rgba(64,158,255,0.25)",
|
borderWidth: getResponsiveValue(1),
|
textStyle: { color: "#B8C8E0", fontSize: getResponsiveValue(11) },
|
},
|
legend: {
|
data: legendNames,
|
top: "2%",
|
right: "1%",
|
textStyle: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(9),
|
},
|
itemWidth: getResponsiveValue(10),
|
itemHeight: getResponsiveValue(10),
|
},
|
grid: {
|
left: "1%",
|
right: "1%",
|
top: "18%",
|
bottom: xMode === "material" ? "0%" : "1%",
|
containLabel: true,
|
},
|
xAxis: {
|
type: "category",
|
data: periods,
|
axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } },
|
axisTick: { show: false },
|
axisLabel: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(11),
|
margin:
|
xMode === "material"
|
? getResponsiveValue(6)
|
: getResponsiveValue(10),
|
rotate: xMode === "material" && periods.length > 5 ? 28 : 0,
|
interval: 0,
|
},
|
splitLine: { show: false },
|
},
|
yAxis: {
|
type: "value",
|
axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } },
|
axisLabel: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(11),
|
margin: getResponsiveValue(8),
|
},
|
splitLine: { lineStyle: { color: "rgba(184,200,224,0.12)" } },
|
},
|
series,
|
};
|
});
|
|
// 板材单耗图表配置(接口数据,与砌块结构一致)
|
const boardCostChartOption = computed(() => {
|
const xMode = boardCostChartSeries.value.xMode || "time";
|
const periods = boardCostChartSeries.value.categories || [];
|
const inputData = boardCostChartSeries.value.input || [];
|
const outputData = boardCostChartSeries.value.output || [];
|
const legendNames = ["投入量", "产出量"];
|
const colors = ["#00A4ED", "#34D8F7"];
|
const series = [
|
{
|
name: legendNames[0],
|
data: inputData,
|
type: "bar",
|
itemStyle: { color: colors[0] },
|
},
|
{
|
name: legendNames[1],
|
data: outputData,
|
type: "bar",
|
itemStyle: { color: colors[1] },
|
},
|
];
|
|
return {
|
backgroundColor: "transparent",
|
tooltip: {
|
trigger: "axis",
|
backgroundColor: "rgba(0,0,0,0.55)",
|
borderColor: "rgba(64,158,255,0.25)",
|
borderWidth: getResponsiveValue(1),
|
textStyle: { color: "#B8C8E0", fontSize: getResponsiveValue(11) },
|
},
|
legend: {
|
data: legendNames,
|
top: "2%",
|
right: "1%",
|
textStyle: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(9),
|
},
|
itemWidth: getResponsiveValue(10),
|
itemHeight: getResponsiveValue(10),
|
},
|
grid: {
|
left: "1%",
|
right: "1%",
|
top: "18%",
|
bottom: xMode === "material" ? "0%" : "1%",
|
containLabel: true,
|
},
|
xAxis: {
|
type: "category",
|
data: periods,
|
axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } },
|
axisTick: { show: false },
|
axisLabel: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(11),
|
margin:
|
xMode === "material"
|
? getResponsiveValue(6)
|
: getResponsiveValue(10),
|
rotate: xMode === "material" && periods.length > 5 ? 28 : 0,
|
interval: 0,
|
},
|
splitLine: { show: false },
|
},
|
yAxis: {
|
type: "value",
|
axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } },
|
axisLabel: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(11),
|
margin: getResponsiveValue(8),
|
},
|
splitLine: { lineStyle: { color: "rgba(184,200,224,0.12)" } },
|
},
|
series,
|
};
|
});
|
|
// 物料生产量分析(接口数据)
|
const productionChartOption = computed(() => {
|
const periods = productionChartSeries.value.categories || [];
|
const mode = productionChartSeries.value.mode || "single";
|
const blockLineColor = "#4A8BFF";
|
const plateLineColor = "#52C9A0";
|
|
const buildAreaLine = (name, data, lineColor) => ({
|
name,
|
data,
|
type: "line",
|
smooth: true,
|
lineStyle: { width: getResponsiveValue(2), color: lineColor },
|
itemStyle: { color: lineColor },
|
areaStyle: {
|
opacity: 0.3,
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: lineColor + "80" },
|
{ offset: 1, color: lineColor + "00" },
|
]),
|
},
|
});
|
|
let series;
|
if (mode === "dual") {
|
series = [
|
buildAreaLine(
|
"砌块",
|
productionChartSeries.value.blockValues || [],
|
blockLineColor
|
),
|
buildAreaLine(
|
"板材",
|
productionChartSeries.value.plateValues || [],
|
plateLineColor
|
),
|
];
|
} else {
|
const cat = productionCategory.value;
|
const seriesName = cat === "砌块" ? "砌块" : "板材";
|
series = [
|
buildAreaLine(
|
seriesName,
|
productionChartSeries.value.values || [],
|
blockLineColor
|
),
|
];
|
}
|
|
return {
|
backgroundColor: "transparent",
|
tooltip: {
|
trigger: "axis",
|
backgroundColor: "rgba(0,0,0,0.55)",
|
borderColor: "rgba(64,158,255,0.25)",
|
borderWidth: getResponsiveValue(1),
|
textStyle: { color: "#B8C8E0", fontSize: getResponsiveValue(11) },
|
},
|
legend: { show: false },
|
grid: {
|
left: "1%",
|
right: "1%",
|
bottom: "1%",
|
top: "10%",
|
containLabel: true,
|
},
|
xAxis: {
|
type: "category",
|
data: periods,
|
axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } },
|
axisTick: { show: false },
|
axisLabel: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(11),
|
margin: getResponsiveValue(10),
|
},
|
splitLine: { show: false },
|
},
|
yAxis: {
|
type: "value",
|
axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } },
|
axisLabel: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(11),
|
margin: getResponsiveValue(8),
|
},
|
splitLine: { lineStyle: { color: "rgba(184,200,224,0.12)" } },
|
},
|
series,
|
};
|
});
|
|
// 固废处理量图表(接口 solidWaste)
|
const customerTrendChartOption = computed(() => {
|
const legendNames = ["全部", "粉煤灰", "石膏", "石灰"];
|
const colors = ["#00A4ED", "#4A8BFF", "#8A6BFF", "#C8C447"];
|
const periods = solidWasteChartSeries.value.categories || [];
|
const dataBySeries = [
|
solidWasteChartSeries.value.total || [],
|
solidWasteChartSeries.value.flyAsh || [],
|
solidWasteChartSeries.value.gypsum || [],
|
solidWasteChartSeries.value.lime || [],
|
];
|
|
const series = legendNames.map((name, index) => ({
|
name,
|
data: dataBySeries[index] || [],
|
type: "line",
|
smooth: true,
|
lineStyle: { width: getResponsiveValue(2), color: colors[index] },
|
itemStyle: { color: colors[index] },
|
areaStyle: {
|
opacity: 0.3,
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: colors[index] + "80" },
|
{ offset: 1, color: colors[index] + "00" },
|
]),
|
},
|
}));
|
|
return {
|
backgroundColor: "transparent",
|
tooltip: {
|
trigger: "axis",
|
backgroundColor: "rgba(0,0,0,0.55)",
|
borderColor: "rgba(64,158,255,0.25)",
|
borderWidth: getResponsiveValue(1),
|
textStyle: { color: "#B8C8E0", fontSize: getResponsiveValue(11) },
|
},
|
legend: {
|
data: legendNames,
|
top: "10%",
|
right: "1%",
|
textStyle: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(9),
|
},
|
itemWidth: getResponsiveValue(10),
|
itemHeight: getResponsiveValue(10),
|
},
|
grid: {
|
left: "1%",
|
right: "1%",
|
bottom: "1%",
|
top: "28%",
|
containLabel: true,
|
},
|
xAxis: {
|
type: "category",
|
data: periods,
|
axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } },
|
axisTick: { show: false },
|
axisLabel: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(11),
|
margin: getResponsiveValue(10),
|
},
|
splitLine: { show: false },
|
},
|
yAxis: {
|
type: "value",
|
axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } },
|
axisLabel: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(11),
|
margin: getResponsiveValue(8),
|
},
|
splitLine: { lineStyle: { color: "rgba(184,200,224,0.12)" } },
|
},
|
series,
|
};
|
});
|
|
// 能耗统计图表配置(接口 energy)
|
const salesRankingChartOption = computed(() => {
|
const energyTypes = ["水", "电", "蒸汽"];
|
const periodType = salesTimeDimension.value;
|
const periods = energyChartSeries.value.categories || [];
|
const waterData = energyChartSeries.value.water || [];
|
const electricityData = energyChartSeries.value.electricity || [];
|
const steamData = energyChartSeries.value.steam || [];
|
|
const series = [
|
{
|
name: "水",
|
type: "bar",
|
data: waterData,
|
itemStyle: {
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: "#00A4ED" },
|
{ offset: 1, color: "#0F285A" },
|
]),
|
borderRadius: [getResponsiveValue(4), getResponsiveValue(4), 0, 0],
|
},
|
barWidth: getResponsiveValue(6),
|
},
|
{
|
name: "电",
|
type: "line",
|
data: electricityData,
|
itemStyle: {
|
color: "#AC43C2",
|
},
|
lineStyle: {
|
width: getResponsiveValue(1),
|
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
{ offset: 0, color: "#AC43C2" },
|
{ offset: 1, color: "#AC43C2" },
|
]),
|
},
|
symbol: "circle",
|
symbolSize: getResponsiveValue(8),
|
areaStyle: {
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: "#AC43C250" },
|
{ offset: 1, color: "#AC43C203" },
|
]),
|
},
|
},
|
{
|
name: "蒸汽",
|
type: "bar",
|
data: steamData,
|
itemStyle: {
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: "#F5BC4A" },
|
{ offset: 1, color: "#591C22" },
|
]),
|
borderRadius: [getResponsiveValue(4), getResponsiveValue(4), 0, 0],
|
},
|
barWidth: getResponsiveValue(6),
|
},
|
];
|
|
return {
|
tooltip: {
|
trigger: "axis",
|
axisPointer: { type: "cross" },
|
backgroundColor: "rgba(0,0,0,0.7)",
|
borderColor: "rgba(64,158,255,0.5)",
|
borderWidth: getResponsiveValue(1),
|
textStyle: { color: "#B8C8E0", fontSize: getResponsiveValue(12) },
|
formatter: function (params) {
|
let result = params[0].name + "<br/>";
|
params.forEach(param => {
|
const unit = param.seriesName === "电" ? "度" : "吨";
|
result += `${param.marker}${param.seriesName}: ${param.value} ${unit}<br/>`;
|
});
|
return result;
|
},
|
},
|
legend: {
|
data: energyTypes,
|
top: "5%",
|
right: "1%",
|
textStyle: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(10),
|
},
|
itemWidth: getResponsiveValue(12),
|
itemHeight: getResponsiveValue(12),
|
itemGap: getResponsiveValue(15),
|
},
|
grid: {
|
left: "1%",
|
right: "1%",
|
top: "25%",
|
bottom: "0%",
|
containLabel: true,
|
},
|
xAxis: {
|
type: "category",
|
data: periods,
|
axisLabel: {
|
fontSize: getResponsiveValue(11),
|
color: "#93B9FF",
|
interval: 0,
|
rotate: periodType === "month" ? 45 : 0,
|
},
|
axisLine: {
|
show: true,
|
lineStyle: {
|
width: getResponsiveValue(1),
|
color: "#305B9A",
|
},
|
},
|
axisTick: {
|
show: false,
|
},
|
},
|
yAxis: {
|
type: "value",
|
axisLabel: {
|
fontSize: getResponsiveValue(11),
|
color: "#93B9FF",
|
formatter: function (value) {
|
return value;
|
},
|
},
|
axisLine: {
|
show: true,
|
lineStyle: {
|
color: "#305B9A",
|
},
|
},
|
splitLine: {
|
lineStyle: {
|
color: "#0F2E60",
|
type: "dashed",
|
},
|
},
|
},
|
series: series,
|
};
|
});
|
|
const baseWidth = ref(1650);
|
// 计算响应式值
|
const getResponsiveValue = baseValue => {
|
return Math.round((baseValue * window.innerWidth) / baseWidth.value);
|
};
|
|
const mapTimeDimensionToDateType = dim => (dim === "year" ? "2" : "1");
|
|
const productionOutputKey = {
|
全部: "totalOutput",
|
砌块: "blockOutput",
|
板材: "plateOutput",
|
};
|
|
/** 全部:按 dateStr 合并砌块/板材两条序列 */
|
/** 将某物料时间序列 chartData 汇总为投入/产出合计(用于「全部」横轴=物料对比) */
|
const sumChartDataInputOutput = chartData => {
|
const arr = Array.isArray(chartData) ? chartData : [];
|
const input = arr.reduce((s, c) => s + (Number(c.inputSum) || 0), 0);
|
const output = arr.reduce((s, c) => s + (Number(c.outputSum) || 0), 0);
|
return { input, output };
|
};
|
|
const mergeBlockPlateProductionSeries = (blockList, plateList) => {
|
const blocks = Array.isArray(blockList) ? blockList : [];
|
const plates = Array.isArray(plateList) ? plateList : [];
|
const blockByDate = new Map(blocks.map(i => [i.dateStr, i]));
|
const plateByDate = new Map(plates.map(i => [i.dateStr, i]));
|
const dates = [
|
...new Set([...blockByDate.keys(), ...plateByDate.keys()]),
|
].sort();
|
return {
|
categories: dates,
|
blockValues: dates.map(d => {
|
const row = blockByDate.get(d);
|
return row ? Number(row.blockOutput) || 0 : 0;
|
}),
|
plateValues: dates.map(d => {
|
const row = plateByDate.get(d);
|
return row ? Number(row.plateOutput) || 0 : 0;
|
}),
|
};
|
};
|
|
const loadBlockCost = async () => {
|
const materialName = blockSelectedMaterial.value;
|
if (!materialName) {
|
blockMaterialSummary.value = {
|
materialName: "",
|
monthlyConsumption: "--",
|
yearlyConsumption: "--",
|
};
|
blockCostChartSeries.value = {
|
xMode: "time",
|
categories: [],
|
input: [],
|
output: [],
|
};
|
updateCharts();
|
return;
|
}
|
const dateType = mapTimeDimensionToDateType(blockTimeDimension.value);
|
try {
|
if (materialName === "全部") {
|
const names = blockMaterialList.value.filter(n => n !== "全部");
|
if (!names.length) {
|
blockMaterialSummary.value = {
|
materialName: "全部",
|
monthlyConsumption: "--",
|
yearlyConsumption: "--",
|
};
|
blockCostChartSeries.value = {
|
xMode: "material",
|
categories: [],
|
input: [],
|
output: [],
|
};
|
updateCharts();
|
return;
|
}
|
const res = await getProductionStatisticsBlocks({ dateType });
|
const rows = Array.isArray(res.data) ? res.data : [];
|
const rowByName = new Map(
|
rows.filter(r => r && r.materialName).map(r => [r.materialName, r])
|
);
|
const useBulk = names.every(n => rowByName.has(n));
|
|
let categories;
|
let input;
|
let output;
|
if (useBulk) {
|
categories = [];
|
input = [];
|
output = [];
|
for (const name of names) {
|
const row = rowByName.get(name);
|
const chartData = Array.isArray(row.chartData) ? row.chartData : [];
|
const sums = sumChartDataInputOutput(chartData);
|
categories.push(name);
|
input.push(sums.input);
|
output.push(sums.output);
|
}
|
} else {
|
const parts = await Promise.all(
|
names.map(async name => {
|
const r = await getProductionStatisticsBlocks({
|
dateType,
|
materialName: name,
|
});
|
const rows2 = Array.isArray(r.data) ? r.data : [];
|
const row =
|
rows2.find(x => x.materialName === name) || rows2[0] || {};
|
const chartData = Array.isArray(row.chartData) ? row.chartData : [];
|
const sums = sumChartDataInputOutput(chartData);
|
return { name, ...sums };
|
})
|
);
|
categories = parts.map(p => p.name);
|
input = parts.map(p => p.input);
|
output = parts.map(p => p.output);
|
}
|
|
blockMaterialSummary.value = {
|
materialName: "全部",
|
monthlyConsumption: "--",
|
yearlyConsumption: "--",
|
};
|
blockCostChartSeries.value = {
|
xMode: "material",
|
categories,
|
input,
|
output,
|
};
|
updateCharts();
|
return;
|
}
|
|
const res = await getProductionStatisticsBlocks({
|
dateType,
|
materialName,
|
});
|
const rows = Array.isArray(res.data) ? res.data : [];
|
const row =
|
rows.find(r => r.materialName === materialName) || rows[0] || {};
|
const chartData = Array.isArray(row.chartData) ? row.chartData : [];
|
blockMaterialSummary.value = {
|
materialName: row.materialName || materialName,
|
monthlyConsumption: row.monthlyConsumption ?? "--",
|
yearlyConsumption: row.yearlyConsumption ?? "--",
|
};
|
blockCostChartSeries.value = {
|
xMode: "time",
|
categories: chartData.map(c => c.date),
|
input: chartData.map(c => c.inputSum ?? 0),
|
output: chartData.map(c => c.outputSum ?? 0),
|
};
|
updateCharts();
|
} catch (e) {
|
console.error(e);
|
}
|
};
|
|
const loadBoardCost = async () => {
|
const materialName = boardSelectedMaterial.value;
|
if (!materialName) {
|
boardMaterialSummary.value = {
|
materialName: "",
|
monthlyConsumption: "--",
|
yearlyConsumption: "--",
|
};
|
boardCostChartSeries.value = {
|
xMode: "time",
|
categories: [],
|
input: [],
|
output: [],
|
};
|
updateCharts();
|
return;
|
}
|
const dateType = mapTimeDimensionToDateType(boardTimeDimension.value);
|
try {
|
if (materialName === "全部") {
|
const names = boardMaterialList.value.filter(n => n !== "全部");
|
if (!names.length) {
|
boardMaterialSummary.value = {
|
materialName: "全部",
|
monthlyConsumption: "--",
|
yearlyConsumption: "--",
|
};
|
boardCostChartSeries.value = {
|
xMode: "material",
|
categories: [],
|
input: [],
|
output: [],
|
};
|
updateCharts();
|
return;
|
}
|
const res = await getProductionStatisticsPlates({ dateType });
|
const rows = Array.isArray(res.data) ? res.data : [];
|
const rowByName = new Map(
|
rows.filter(r => r && r.materialName).map(r => [r.materialName, r])
|
);
|
const useBulk = names.every(n => rowByName.has(n));
|
|
let categories;
|
let input;
|
let output;
|
if (useBulk) {
|
categories = [];
|
input = [];
|
output = [];
|
for (const name of names) {
|
const row = rowByName.get(name);
|
const chartData = Array.isArray(row.chartData) ? row.chartData : [];
|
const sums = sumChartDataInputOutput(chartData);
|
categories.push(name);
|
input.push(sums.input);
|
output.push(sums.output);
|
}
|
} else {
|
const parts = await Promise.all(
|
names.map(async name => {
|
const r = await getProductionStatisticsPlates({
|
dateType,
|
materialName: name,
|
});
|
const rows2 = Array.isArray(r.data) ? r.data : [];
|
const row =
|
rows2.find(x => x.materialName === name) || rows2[0] || {};
|
const chartData = Array.isArray(row.chartData) ? row.chartData : [];
|
const sums = sumChartDataInputOutput(chartData);
|
return { name, ...sums };
|
})
|
);
|
categories = parts.map(p => p.name);
|
input = parts.map(p => p.input);
|
output = parts.map(p => p.output);
|
}
|
|
boardMaterialSummary.value = {
|
materialName: "全部",
|
monthlyConsumption: "--",
|
yearlyConsumption: "--",
|
};
|
boardCostChartSeries.value = {
|
xMode: "material",
|
categories,
|
input,
|
output,
|
};
|
updateCharts();
|
return;
|
}
|
|
const res = await getProductionStatisticsPlates({
|
dateType,
|
materialName,
|
});
|
const rows = Array.isArray(res.data) ? res.data : [];
|
const row =
|
rows.find(r => r.materialName === materialName) || rows[0] || {};
|
const chartData = Array.isArray(row.chartData) ? row.chartData : [];
|
boardMaterialSummary.value = {
|
materialName: row.materialName || materialName,
|
monthlyConsumption: row.monthlyConsumption ?? "--",
|
yearlyConsumption: row.yearlyConsumption ?? "--",
|
};
|
boardCostChartSeries.value = {
|
xMode: "time",
|
categories: chartData.map(c => c.date),
|
input: chartData.map(c => c.inputSum ?? 0),
|
output: chartData.map(c => c.outputSum ?? 0),
|
};
|
updateCharts();
|
} catch (e) {
|
console.error(e);
|
}
|
};
|
|
const loadBlockMaterials = async () => {
|
try {
|
const res = await getProductionMaterials({ materialType: "1" });
|
const raw = Array.isArray(res.data) ? [...res.data] : [];
|
const list = ["全部", ...raw];
|
blockMaterialList.value = list;
|
if (list.length) {
|
if (!list.includes(blockSelectedMaterial.value)) {
|
blockSelectedMaterial.value = list[0];
|
}
|
await loadBlockCost();
|
} else {
|
blockSelectedMaterial.value = "";
|
blockMaterialSummary.value = {
|
materialName: "",
|
monthlyConsumption: "--",
|
yearlyConsumption: "--",
|
};
|
blockCostChartSeries.value = {
|
xMode: "time",
|
categories: [],
|
input: [],
|
output: [],
|
};
|
updateCharts();
|
}
|
} catch (e) {
|
console.error(e);
|
}
|
};
|
|
const loadBoardMaterials = async () => {
|
try {
|
const res = await getProductionMaterials({ materialType: "2" });
|
const raw = Array.isArray(res.data) ? [...res.data] : [];
|
const list = ["全部", ...raw];
|
boardMaterialList.value = list;
|
if (list.length) {
|
if (!list.includes(boardSelectedMaterial.value)) {
|
boardSelectedMaterial.value = list[0];
|
}
|
await loadBoardCost();
|
} else {
|
boardSelectedMaterial.value = "";
|
boardMaterialSummary.value = {
|
materialName: "",
|
monthlyConsumption: "--",
|
yearlyConsumption: "--",
|
};
|
boardCostChartSeries.value = {
|
xMode: "time",
|
categories: [],
|
input: [],
|
output: [],
|
};
|
updateCharts();
|
}
|
} catch (e) {
|
console.error(e);
|
}
|
};
|
|
const loadMaterialProduction = async () => {
|
const dateType = mapTimeDimensionToDateType(productionTimeDimension.value);
|
try {
|
const res = await getMaterialProductionAnalysis({ dateType });
|
const payload = res.data && typeof res.data === "object" ? res.data : {};
|
const cat = productionCategory.value;
|
|
if (cat === "全部") {
|
const merged = mergeBlockPlateProductionSeries(
|
payload["砌块"],
|
payload["板材"]
|
);
|
productionChartSeries.value = {
|
categories: merged.categories,
|
mode: "dual",
|
values: [],
|
blockValues: merged.blockValues,
|
plateValues: merged.plateValues,
|
};
|
} else {
|
const list = Array.isArray(payload[cat]) ? payload[cat] : [];
|
const field = productionOutputKey[cat] || "totalOutput";
|
productionChartSeries.value = {
|
categories: list.map(i => i.dateStr),
|
mode: "single",
|
values: list.map(i => Number(i[field]) || 0),
|
blockValues: [],
|
plateValues: [],
|
};
|
}
|
updateCharts();
|
} catch (e) {
|
console.error(e);
|
}
|
};
|
|
const loadMiddleRingStats = async () => {
|
try {
|
const res = await getProductionStatisticsMiddle();
|
const d = res.data && typeof res.data === "object" ? res.data : {};
|
middleRingStats.value = {
|
flyAshMonth: Number(d.flyAshMonth) || 0,
|
flyAshYear: Number(d.flyAshYear) || 0,
|
gypsumMonth: Number(d.gypsumMonth) || 0,
|
gypsumYear: Number(d.gypsumYear) || 0,
|
blockMonth: Number(d.blockMonth) || 0,
|
blockYear: Number(d.blockYear) || 0,
|
plateMonth: Number(d.plateMonth) || 0,
|
plateYear: Number(d.plateYear) || 0,
|
};
|
} catch (e) {
|
console.error(e);
|
}
|
};
|
|
const loadSolidWasteData = async () => {
|
const dateType = mapTimeDimensionToDateType(customerTimeDimension.value);
|
try {
|
const res = await getProductionStatisticsSolidWaste({ dateType });
|
const list = Array.isArray(res.data) ? res.data : [];
|
solidWasteChartSeries.value = {
|
categories: list.map(i => i.dateStr),
|
total: list.map(i => Number(i.total) || 0),
|
flyAsh: list.map(i => Number(i.flyAsh) || 0),
|
gypsum: list.map(i => Number(i.gypsum) || 0),
|
lime: list.map(i => Number(i.lime) || 0),
|
};
|
updateCharts();
|
} catch (e) {
|
console.error(e);
|
}
|
};
|
|
const loadEnergyData = async () => {
|
const dateType = mapTimeDimensionToDateType(salesTimeDimension.value);
|
try {
|
const res = await getProductionStatisticsEnergy({ dateType });
|
const list = Array.isArray(res.data) ? res.data : [];
|
energyChartSeries.value = {
|
categories: list.map(i => i.dateStr),
|
water: list.map(i => Number(i.water) || 0),
|
electricity: list.map(i => Number(i.electricity) || 0),
|
steam: list.map(i => Number(i.steam) || 0),
|
};
|
updateCharts();
|
} catch (e) {
|
console.error(e);
|
}
|
};
|
|
const selectBlockMaterial = name => {
|
blockSelectedMaterial.value = name;
|
loadBlockCost();
|
};
|
|
const selectBoardMaterial = name => {
|
boardSelectedMaterial.value = name;
|
loadBoardCost();
|
};
|
|
const selectProductionCategory = cat => {
|
productionCategory.value = cat;
|
loadMaterialProduction();
|
};
|
|
// 初始化图表
|
const initCharts = () => {
|
// 初始化砌块成本图表
|
if (blockCostChart.value && !blockCostChartInstance) {
|
blockCostChartInstance = echarts.init(blockCostChart.value);
|
}
|
|
// 初始化板材成本图表
|
if (boardCostChart.value && !boardCostChartInstance) {
|
boardCostChartInstance = echarts.init(boardCostChart.value);
|
}
|
|
// 初始化产量分析图表
|
if (productionChart.value && !productionChartInstance) {
|
productionChartInstance = echarts.init(productionChart.value);
|
}
|
|
// 初始化新增客户趋势图表
|
if (customerTrendChart.value && !customerTrendChartInstance) {
|
customerTrendChartInstance = echarts.init(customerTrendChart.value);
|
}
|
|
// 初始化销量排名分析图表
|
if (salesRankingChart.value && !salesRankingChartInstance) {
|
salesRankingChartInstance = echarts.init(salesRankingChart.value);
|
}
|
|
updateCharts();
|
};
|
|
/** 砌块/板材容器高度变化后(如隐藏汇总卡)需 resize,否则底部留白 */
|
const resizeCostPanelCharts = () => {
|
nextTick(() => {
|
requestAnimationFrame(() => {
|
blockCostChartInstance?.resize();
|
boardCostChartInstance?.resize();
|
});
|
});
|
};
|
|
// 更新图表
|
const updateCharts = () => {
|
// 更新砌块成本图表(replaceMerge:全部↔单物料时横轴时间/物料切换)
|
if (blockCostChartInstance) {
|
blockCostChartInstance.setOption(blockCostChartOption.value, {
|
replaceMerge: ["series", "xAxis"],
|
});
|
}
|
|
// 更新板材成本图表
|
if (boardCostChartInstance) {
|
boardCostChartInstance.setOption(boardCostChartOption.value, {
|
replaceMerge: ["series", "xAxis"],
|
});
|
}
|
|
// 更新产量分析图表(replaceMerge:全部↔砌块/板材切换时去掉多余折线,避免合并残留)
|
if (productionChartInstance) {
|
productionChartInstance.setOption(productionChartOption.value, {
|
replaceMerge: ["series"],
|
});
|
}
|
|
// 更新新增客户趋势图表
|
if (customerTrendChartInstance) {
|
customerTrendChartInstance.setOption(customerTrendChartOption.value);
|
}
|
|
// 更新销量排名分析图表
|
if (salesRankingChartInstance) {
|
salesRankingChartInstance.setOption(salesRankingChartOption.value);
|
}
|
|
resizeCostPanelCharts();
|
};
|
|
// 处理时间维度选择
|
const handleBlockTimeDimensionChange = dimension => {
|
blockTimeDimension.value = dimension;
|
loadBlockCost();
|
};
|
|
const handleBoardTimeDimensionChange = dimension => {
|
boardTimeDimension.value = dimension;
|
loadBoardCost();
|
};
|
|
const handleProductionTimeDimensionChange = dimension => {
|
productionTimeDimension.value = dimension;
|
loadMaterialProduction();
|
};
|
|
const handleCustomerTimeDimensionChange = dimension => {
|
customerTimeDimension.value = dimension;
|
loadSolidWasteData();
|
};
|
|
const handleSalesTimeDimensionChange = dimension => {
|
salesTimeDimension.value = dimension;
|
loadEnergyData();
|
};
|
|
// 监听窗口大小变化
|
const handleResize = () => {
|
// 先更新图表选项,重新计算响应式值
|
updateCharts();
|
// 然后调整图表大小
|
if (blockCostChartInstance) {
|
blockCostChartInstance.resize();
|
}
|
if (boardCostChartInstance) {
|
boardCostChartInstance.resize();
|
}
|
if (productionChartInstance) {
|
productionChartInstance.resize();
|
}
|
if (customerTrendChartInstance) {
|
customerTrendChartInstance.resize();
|
}
|
if (salesRankingChartInstance) {
|
salesRankingChartInstance.resize();
|
}
|
};
|
|
// 生命周期
|
onMounted(() => {
|
// 启动顶部栏时间刷新
|
if (!timeTicker) {
|
timeTicker = setInterval(() => {
|
now.value = dayjs();
|
}, 1000);
|
}
|
|
// 等待DOM更新后初始化图表并拉取接口数据
|
nextTick(async () => {
|
initCharts();
|
if (typeof ResizeObserver !== "undefined") {
|
if (blockCostChart.value) {
|
blockCostChartResizeObserver = new ResizeObserver(() => {
|
blockCostChartInstance?.resize();
|
});
|
blockCostChartResizeObserver.observe(blockCostChart.value);
|
}
|
if (boardCostChart.value) {
|
boardCostChartResizeObserver = new ResizeObserver(() => {
|
boardCostChartInstance?.resize();
|
});
|
boardCostChartResizeObserver.observe(boardCostChart.value);
|
}
|
}
|
await Promise.all([
|
loadBlockMaterials(),
|
loadBoardMaterials(),
|
loadMaterialProduction(),
|
loadMiddleRingStats(),
|
loadSolidWasteData(),
|
loadEnergyData(),
|
]);
|
resizeCostPanelCharts();
|
});
|
|
// 添加窗口大小变化监听
|
window.addEventListener("resize", handleResize);
|
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
});
|
|
// 组件卸载时销毁图表实例
|
onBeforeUnmount(() => {
|
if (timeTicker) {
|
clearInterval(timeTicker);
|
timeTicker = null;
|
}
|
|
blockCostChartResizeObserver?.disconnect();
|
blockCostChartResizeObserver = null;
|
boardCostChartResizeObserver?.disconnect();
|
boardCostChartResizeObserver = null;
|
|
if (blockCostChartInstance) {
|
blockCostChartInstance.dispose();
|
}
|
if (boardCostChartInstance) {
|
boardCostChartInstance.dispose();
|
}
|
if (productionChartInstance) {
|
productionChartInstance.dispose();
|
}
|
if (customerTrendChartInstance) {
|
customerTrendChartInstance.dispose();
|
}
|
if (salesRankingChartInstance) {
|
salesRankingChartInstance.dispose();
|
}
|
|
// 移除窗口大小变化监听
|
window.removeEventListener("resize", handleResize);
|
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
});
|
</script>
|
|
<style scoped>
|
.sales-statistics-container {
|
position: relative;
|
width: 100%;
|
min-height: calc(100vh - 8.4vh);
|
overflow: hidden;
|
color: #b8c8e0;
|
background: #041026;
|
}
|
|
.sales-statistics-container.is-fullscreen {
|
min-height: 100vh;
|
height: 100vh;
|
}
|
|
/* 深色背景图 */
|
.bi-bg {
|
position: absolute;
|
inset: 0;
|
/* background-image: url("@/assets/BI/backImage@2x.png"); */
|
background-size: cover;
|
background-position: center;
|
background-repeat: no-repeat;
|
z-index: 0;
|
}
|
|
/* 顶部标题栏 */
|
.bi-topbar {
|
position: relative;
|
z-index: 2;
|
height: 5.8vh;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.bi-topbar-title-bg {
|
position: absolute;
|
top: 0;
|
left: 0;
|
height: 8vh;
|
width: 100%;
|
object-fit: cover;
|
z-index: 0;
|
pointer-events: none;
|
}
|
|
.bi-topbar-content {
|
position: relative;
|
z-index: 1;
|
width: 100%;
|
padding: 0 2.8vh;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.bi-topbar-title {
|
position: absolute;
|
left: 50%;
|
transform: translateX(-50%);
|
font-size: 2.6vh;
|
font-weight: 800;
|
letter-spacing: 0.1vh;
|
background: linear-gradient(180deg, #ffffff 0%, #b8dfff 100%);
|
-webkit-background-clip: text;
|
background-clip: text;
|
-webkit-text-fill-color: transparent;
|
color: transparent;
|
text-shadow: 0 0 2.6vh rgba(0, 164, 237, 0.55);
|
}
|
|
.bi-topbar-left {
|
position: absolute;
|
left: 1vh;
|
display: flex;
|
align-items: center;
|
gap: 0.8vh;
|
color: rgba(208, 231, 255, 0.85);
|
font-size: 1.3vh;
|
}
|
|
.status-sun {
|
color: #ffd85e;
|
text-shadow: 0 0 1vh rgba(255, 216, 94, 0.8);
|
font-size: 1.3vh;
|
line-height: 1;
|
}
|
|
.bi-topbar-meta {
|
position: absolute;
|
right: 5.2vh;
|
/* top: 1.6vh; */
|
font-size: 1.2vh;
|
font-weight: 500;
|
letter-spacing: 0.05vh;
|
color: rgba(208, 231, 255, 0.85);
|
display: flex;
|
align-items: center;
|
gap: 1vh;
|
}
|
|
.fullscreen-btn {
|
position: absolute;
|
bottom: -1vh;
|
transform: none;
|
border: 0.1vh solid rgba(64, 158, 255, 0.45);
|
background: rgba(0, 164, 237, 0.14);
|
color: #d0e7ff;
|
width: 3.4vh;
|
height: 3.4vh;
|
border-radius: 0.6vh;
|
padding: 0;
|
cursor: pointer;
|
transition: all 0.2s ease;
|
z-index: 10;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.fullscreen-btn:hover {
|
background: rgba(0, 164, 237, 0.24);
|
box-shadow: 0 0 1.2vh rgba(0, 164, 237, 0.3);
|
}
|
|
.bi-topbar-sep {
|
opacity: 0.7;
|
}
|
|
/* 主体网格布局 */
|
.bi-dashboard-grid {
|
position: relative;
|
z-index: 2;
|
height: calc(100vh - 8.4vh - 5.8vh);
|
min-height: 45vh;
|
padding: 1vh 1.8vh 1.4vh;
|
display: grid;
|
grid-template-columns: 1fr 1.05fr 1fr;
|
grid-template-rows: 1fr 1fr;
|
gap: 1.2vh;
|
}
|
|
.sales-statistics-container.is-fullscreen .bi-dashboard-grid {
|
height: calc(100vh - 5.8vh);
|
}
|
|
.bi-panel {
|
background: rgba(3, 18, 46, 0.62);
|
border: 0.1vh solid rgba(64, 158, 255, 0.35);
|
border-radius: 0.4vh;
|
overflow: hidden;
|
box-shadow: 0 0 2.2vh rgba(0, 164, 237, 0.12);
|
display: flex;
|
flex-direction: column;
|
position: relative;
|
}
|
|
.bi-panel-title {
|
height: 4.4vh;
|
display: flex;
|
align-items: center;
|
padding: 0 1.8vh;
|
font-size: 1.5vh;
|
font-weight: 700;
|
color: #b8c8e0;
|
background: linear-gradient(
|
90deg,
|
rgba(0, 164, 237, 0.2),
|
rgba(0, 164, 237, 0.04)
|
);
|
border-bottom: 0.1vh solid rgba(64, 158, 255, 0.25);
|
}
|
|
.panel-tabs,
|
.panel-tabs2 {
|
position: absolute;
|
top: 0.8vh;
|
display: flex;
|
gap: 0.6vh;
|
z-index: 4;
|
}
|
|
.panel-tabs {
|
right: 1.2vh;
|
}
|
|
.panel-tabs2 {
|
right: 8vh;
|
}
|
.tab-item {
|
font-size: 1.2vh;
|
color: rgba(184, 200, 224, 0.75);
|
padding: 0.1vh 0.5vh;
|
border: 0.1vh solid rgba(64, 158, 255, 0.25);
|
border-radius: 0.3vh;
|
line-height: 1.4;
|
cursor: pointer;
|
}
|
|
.tab-item.active {
|
color: #ffffff;
|
border-color: rgba(0, 164, 237, 0.65);
|
background: rgba(0, 164, 237, 0.22);
|
}
|
|
.bi-panel-body {
|
flex: 1;
|
padding: 0.8vh 1vh;
|
position: relative;
|
}
|
|
.scroll-table-container {
|
height: 33vh;
|
overflow: hidden;
|
position: relative;
|
}
|
|
.scroll-table {
|
width: 100%;
|
border-collapse: collapse;
|
color: #b8c8e0;
|
}
|
|
.scroll-table th {
|
/* background-color: #0e2a54; */
|
padding: 1.2vh;
|
text-align: left;
|
font-size: 1.2vh;
|
font-weight: bold;
|
border: 1px solid rgba(184, 200, 224, 0.2);
|
}
|
|
.scroll-table td {
|
padding: 1vh;
|
font-size: 1.1vh;
|
border-bottom: 1px solid rgba(184, 200, 224, 0.1);
|
}
|
|
.scroll-table tbody {
|
display: block;
|
height: 35vh;
|
overflow: hidden;
|
}
|
|
.scroll-table thead,
|
.scroll-table tbody tr {
|
display: table;
|
width: 100%;
|
table-layout: fixed;
|
}
|
|
.scroll-table th,
|
.scroll-table td {
|
width: 25%;
|
box-sizing: border-box;
|
font-size: 1.4vh;
|
}
|
|
.scroll-table tbody tr {
|
transition: all 0.5s ease-in-out;
|
}
|
|
/* .scroll-table tbody tr:nth-child(odd) {
|
background-color: rgba(64, 158, 255, 0.05);
|
}
|
|
.scroll-table tbody tr:nth-child(even) {
|
background-color: rgba(64, 158, 255, 0.1);
|
} */
|
.oddTableTr {
|
background-color: rgba(64, 158, 255, 0.05);
|
}
|
.evenTableTr {
|
background-color: rgba(64, 158, 255, 0.1);
|
}
|
|
.scroll-table-container:hover tbody {
|
overflow: hidden;
|
}
|
|
.echart-fill {
|
width: 100%;
|
height: 100%;
|
}
|
|
.chart-filter-tabs {
|
display: flex;
|
gap: 0.6vh;
|
margin: 0 0 0.5vh 0;
|
justify-self: end;
|
}
|
|
.cf-tab {
|
font-size: 1.1vh;
|
color: rgba(184, 200, 224, 0.68);
|
background: rgba(18, 56, 106, 0.65);
|
border: 0.1vh solid rgba(64, 158, 255, 0.25);
|
padding: 0.3vh 0.9vh;
|
line-height: 1;
|
cursor: pointer;
|
}
|
|
.cf-tab.active {
|
color: #d9ecff;
|
background: rgba(0, 108, 208, 0.85);
|
border-color: rgba(64, 158, 255, 0.65);
|
}
|
|
.chart-unit-row {
|
display: flex;
|
justify-content: space-between;
|
font-size: 1.2vh;
|
color: rgba(208, 231, 255, 0.88);
|
margin-bottom: 0.4vh;
|
padding: 0 0.2vh;
|
}
|
|
.dot-legend::before {
|
content: "";
|
display: inline-block;
|
width: 0.8vh;
|
height: 0.8vh;
|
background: #65a0ff;
|
margin-right: 0.6vh;
|
}
|
|
.chart-mini-title {
|
display: flex;
|
align-items: center;
|
gap: 0.8vh;
|
font-size: 1.8vh;
|
color: #d9ecff;
|
}
|
|
/* 面板底部合计行 */
|
.panel-summary-row {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
padding: 0.8vh 1.2vh;
|
/* margin-top: 0.8vh; */
|
background: #041e3c;
|
border-top: 0.1vh solid rgba(64, 158, 255, 0.25);
|
border-radius: 0 0 0.4vh 0.4vh;
|
width: 100%;
|
position: absolute;
|
bottom: 0;
|
}
|
|
.summary-label {
|
font-size: 1.3vh;
|
font-weight: 700;
|
color: #b8c8e0;
|
}
|
|
.summary-value {
|
font-size: 1.4vh;
|
font-weight: 800;
|
color: #00a4ed;
|
margin-right: 3.8vh;
|
text-shadow: 0 0 1vh rgba(0, 164, 237, 0.5);
|
}
|
|
.summary-value2 {
|
font-size: 1.4vh;
|
font-weight: 800;
|
color: #00a4ed;
|
margin-right: 5.8vh;
|
text-shadow: 0 0 1vh rgba(0, 164, 237, 0.5);
|
}
|
.diamond {
|
width: 1vh;
|
height: 1vh;
|
background: #1e8bff;
|
transform: rotate(45deg);
|
display: inline-block;
|
}
|
|
.chart-unit-single {
|
justify-content: flex-start;
|
margin-bottom: 0.2vh;
|
}
|
|
/* 砌块/板材:图表占满 Tab 与汇总卡下方的剩余高度;隐藏汇总卡时自动拉高 */
|
.bi-panel-top-left,
|
.bi-panel-bottom-left {
|
min-height: 0;
|
}
|
|
.bi-panel-top-left .bi-panel-body,
|
.bi-panel-bottom-left .bi-panel-body {
|
display: flex;
|
flex-direction: column;
|
min-height: 0;
|
}
|
|
.bi-panel-top-left .chart-filter-tabs,
|
.bi-panel-bottom-left .chart-filter-tabs {
|
flex-shrink: 0;
|
}
|
|
.bi-panel-top-left .echart-fill,
|
.bi-panel-bottom-left .echart-fill {
|
flex: 1;
|
min-height: 0;
|
height: auto;
|
width: 100%;
|
}
|
|
.bi-panel-bottom-right .echart-fill {
|
height: calc(100% - 2.8vh);
|
}
|
|
.bi-panel-bottom-center .echart-fill,
|
.bi-panel-top-right .echart-fill {
|
height: calc(100% - 4.4vh);
|
}
|
|
.bi-panel-top-left {
|
grid-column: 1;
|
grid-row: 1;
|
position: relative;
|
}
|
|
.bi-panel-top-right {
|
grid-column: 3;
|
grid-row: 1;
|
position: relative;
|
}
|
|
.bi-panel-bottom-left {
|
grid-column: 1;
|
grid-row: 2;
|
overflow-y: auto;
|
}
|
.bi-panel-bottom-left::-webkit-scrollbar {
|
width: 0vh;
|
height: 0vh;
|
}
|
|
.bi-panel-bottom-center {
|
grid-column: 2;
|
grid-row: 2;
|
}
|
|
.bi-panel-bottom-right {
|
grid-column: 3;
|
grid-row: 2;
|
overflow-y: auto;
|
}
|
.bi-panel-bottom-right::-webkit-scrollbar {
|
width: 0vh;
|
height: 0vh;
|
}
|
|
/* 中心环浮层(绝对定位在网格上方) */
|
.center-ring {
|
grid-column: 2;
|
grid-row: 1 / span 2;
|
position: absolute;
|
background: url("@/assets/BI/imageSS@2x.png") no-repeat bottom center;
|
background-size: 80% 30%;
|
left: 25%;
|
top: 25%;
|
transform: translate(-50%, -45%);
|
width: 50%;
|
height: 40.5vh;
|
z-index: 3;
|
pointer-events: none;
|
}
|
.center-ring-box {
|
position: absolute;
|
/* inset: 0; */
|
height: 88%;
|
width: 100%;
|
margin-top: 3%;
|
/* background-color: #fff; */
|
background: url("@/assets/BI/SCbg.png") no-repeat center center;
|
background-size: 100% 100%;
|
}
|
.ring-box-topright {
|
position: absolute;
|
top: 6vh;
|
right: 0;
|
width: 25%;
|
height: 15%;
|
background: url("@/assets/BI/SCbgright.png") no-repeat center center;
|
background-size: 100% 100%;
|
text-align: right;
|
}
|
.ring-box-topleft {
|
position: absolute;
|
top: 6vh;
|
left: 0;
|
width: 25%;
|
height: 15%;
|
background: url("@/assets/BI/SCbgleft.png") no-repeat center center;
|
background-size: 100% 100%;
|
text-align: left;
|
}
|
.topright-label {
|
font-size: 1.8vh;
|
font-weight: 500;
|
color: rgba(234, 246, 255, 0.9);
|
margin-top: 0;
|
position: relative;
|
bottom: 3vh;
|
right: 1vh;
|
}
|
|
.topleft-label {
|
font-size: 1.8vh;
|
font-weight: 500;
|
color: rgba(234, 246, 255, 0.9);
|
margin-top: 0;
|
position: relative;
|
bottom: 3vh;
|
left: 1vh;
|
}
|
.ring-box-left {
|
/* background-color: #ebebeb; */
|
width: 30%;
|
position: absolute;
|
left: 1vh;
|
top: 56%;
|
transform: translateY(-50%);
|
}
|
.left-label {
|
font-size: 1.4vh;
|
font-weight: 500;
|
color: #0effef;
|
margin-top: 0;
|
position: relative;
|
bottom: 3vh;
|
}
|
.left-value {
|
font-size: 1.2vh;
|
font-weight: 500;
|
color: rgba(234, 246, 255, 0.9);
|
margin-top: 0;
|
position: relative;
|
bottom: 3vh;
|
margin-top: 0.4vh;
|
}
|
.ring-box-right {
|
/* background-color: #ebebeb; */
|
width: 30%;
|
float: right;
|
position: absolute;
|
right: -1vh;
|
top: 56%;
|
transform: translateY(-50%);
|
}
|
.right-label {
|
font-size: 1.4vh;
|
font-weight: 500;
|
color: #ffa60e;
|
margin-top: 0;
|
position: relative;
|
bottom: 3vh;
|
}
|
|
.right-value {
|
font-size: 1.2vh;
|
font-weight: 500;
|
color: rgba(234, 246, 255, 0.9);
|
margin-top: 0;
|
position: relative;
|
bottom: 3vh;
|
margin-top: 0.4vh;
|
}
|
|
.center-ring-bg {
|
width: 100%;
|
height: 100%;
|
object-fit: contain;
|
filter: drop-shadow(0 0 2vh rgba(0, 164, 237, 0.35));
|
}
|
|
.center-ring-content {
|
position: absolute;
|
inset: 0;
|
}
|
|
.center-ring-content::before,
|
.center-ring-content::after {
|
content: "";
|
position: absolute;
|
left: 50%;
|
top: 56%;
|
width: 37vh;
|
height: 14.6vh;
|
transform: translate(-50%, -50%) rotate(-18deg);
|
border: 0.2vh solid rgba(40, 186, 255, 0.45);
|
border-radius: 50%;
|
filter: drop-shadow(0 0 0.8vh rgba(0, 164, 237, 0.35));
|
opacity: 0.7;
|
}
|
|
.center-ring-content::after {
|
width: 36vh;
|
height: 15vh;
|
transform: translate(-50%, -50%) rotate(26deg);
|
border-color: rgba(80, 220, 255, 0.35);
|
opacity: 0.55;
|
}
|
|
.center-ring-title {
|
position: absolute;
|
top: 50%;
|
left: 50%;
|
transform: translate(-50%, -50%);
|
font-size: 3.6vh;
|
line-height: 1.05;
|
text-align: center;
|
font-weight: 900;
|
color: #eaf6ff;
|
text-shadow: 0 0 2.2vh rgba(0, 164, 237, 0.55);
|
z-index: 2;
|
}
|
|
.center-ring-title::before {
|
content: "";
|
position: absolute;
|
left: 50%;
|
top: 50%;
|
width: 15.5vh;
|
height: 15.5vh;
|
transform: translate(-50%, -50%);
|
background: radial-gradient(
|
circle,
|
rgba(43, 199, 255, 0.26) 0%,
|
rgba(8, 28, 61, 0.86) 70%
|
);
|
border: 0.2vh solid rgba(39, 198, 255, 0.46);
|
border-radius: 50%;
|
box-shadow: 0 0 2vh rgba(0, 164, 237, 0.45),
|
inset 0 0 2.6vh rgba(0, 164, 237, 0.2);
|
z-index: -1;
|
}
|
|
.center-metric {
|
position: absolute;
|
width: 15.5vh;
|
z-index: 3;
|
text-align: center;
|
height: 12vh;
|
display: flex;
|
flex-direction: column;
|
justify-content: center;
|
align-items: center;
|
}
|
|
.center-metric-label {
|
font-size: 1.2vh;
|
font-weight: 500;
|
color: rgba(234, 246, 255, 0.9);
|
margin-top: 0;
|
}
|
|
.center-metric-value {
|
font-size: 3.4vh;
|
font-weight: 800;
|
color: #eaf6ff;
|
text-shadow: 0 0 0.8vh rgba(0, 229, 255, 0.22);
|
line-height: 1;
|
}
|
|
.center-metric-unit {
|
margin-top: 0;
|
font-size: 1.2vh;
|
color: rgba(208, 231, 255, 0.85);
|
}
|
|
.m1 {
|
top: 2.5vh;
|
left: 4.8vh;
|
text-align: left;
|
}
|
|
.m2 {
|
top: 4.1vh;
|
right: 8.6vh;
|
text-align: right;
|
}
|
|
.m3 {
|
bottom: 7.9vh;
|
left: 4vh;
|
text-align: left;
|
}
|
|
.m4 {
|
bottom: 7vh;
|
right: 5.4vh;
|
text-align: right;
|
}
|
|
@media (max-width: 1100px) {
|
.bi-topbar-content {
|
padding: 0 1.4vh;
|
}
|
.center-ring {
|
left: 45.2%;
|
width: 33vh;
|
height: 24.5vh;
|
top: 2.4vh;
|
}
|
.center-ring-title {
|
top: 50%;
|
font-size: 2.8vh;
|
transform: translate(-50%, -50%);
|
}
|
.center-metric {
|
height: 10.5vh;
|
}
|
.m1 {
|
top: 5.2vh;
|
left: 4.2vh;
|
}
|
.m2 {
|
top: 5.4vh;
|
right: 4.2vh;
|
}
|
.m3 {
|
bottom: 6.2vh;
|
left: 4.8vh;
|
}
|
.m4 {
|
bottom: 6.8vh;
|
right: 4.4vh;
|
}
|
}
|
.scroll-table-content {
|
height: 39vh;
|
overflow: auto;
|
}
|
.scroll-table-content::-webkit-scrollbar {
|
width: 0;
|
height: 0;
|
background-color: transparent;
|
}
|
.total-row {
|
position: absolute;
|
bottom: 0vh;
|
left: 0;
|
width: 100%;
|
display: flex;
|
height: 3.5vh;
|
justify-content: space-around;
|
align-items: center;
|
background-color: #081843;
|
}
|
|
.total-cell {
|
width: 20%;
|
font-size: 1.4vh;
|
margin-bottom: 0.5vh;
|
line-height: 3.5vh;
|
padding-left: 0.8vh;
|
color: #eaf6ff;
|
text-shadow: 0 0 0.8vh rgba(0, 229, 255, 0.22);
|
text-align: left;
|
color: #c3c3c3;
|
}
|
.total-cell2 {
|
width: 20%;
|
font-size: 1.4vh;
|
margin-bottom: 0.5vh;
|
line-height: 3.5vh;
|
color: #eaf6ff;
|
text-shadow: 0 0 0.8vh rgba(0, 229, 255, 0.22);
|
text-align: left;
|
color: #c3c3c3;
|
}
|
/* 材料信息卡片 */
|
.material-info-card {
|
display: flex;
|
align-items: center;
|
padding: 1.2vh 1.6vh;
|
background: rgba(0, 164, 237, 0.1);
|
border-radius: 0.8vh;
|
margin: 1.2vh 1.6vh;
|
}
|
|
.material-icon {
|
width: 4vh;
|
height: 4vh;
|
background: rgba(0, 164, 237, 0.2);
|
border-radius: 50%;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
margin-right: 1.2vh;
|
color: #00a4ed;
|
}
|
|
.material-details {
|
flex: 1;
|
}
|
|
.material-name {
|
font-size: 1.3vh;
|
font-weight: 600;
|
color: #d0e7ff;
|
margin-bottom: 0.6vh;
|
}
|
|
.material-stats {
|
display: flex;
|
gap: 1.6vh;
|
}
|
|
.stat-item {
|
display: flex;
|
align-items: center;
|
gap: 0.4vh;
|
}
|
|
.stat-label {
|
font-size: 1vh;
|
opacity: 0.7;
|
}
|
|
.stat-value {
|
font-size: 1.2vh;
|
font-weight: 600;
|
color: #00a4ed;
|
}
|
|
.stat-unit {
|
font-size: 1vh;
|
opacity: 0.7;
|
}
|
</style>
|