| | |
| | | :class="{ active: blockSelectedMaterial === name }" |
| | | @click="selectBlockMaterial(name)">{{ name }}</span> |
| | | </div> |
| | | <div class="material-info-card"> |
| | | <div v-if="blockSelectedMaterial !== '全部'" |
| | | class="material-info-card"> |
| | | <div class="material-icon"> |
| | | <svg width="24" |
| | | height="24" |
| | |
| | | :class="{ active: boardSelectedMaterial === name }" |
| | | @click="selectBoardMaterial(name)">{{ name }}</span> |
| | | </div> |
| | | <div class="material-info-card"> |
| | | <div v-if="boardSelectedMaterial !== '全部'" |
| | | class="material-info-card"> |
| | | <div class="material-icon"> |
| | | <svg width="24" |
| | | height="24" |
| | |
| | | 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) |
| | |
| | | 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 || []; |
| | |
| | | grid: { |
| | | left: "1%", |
| | | right: "1%", |
| | | bottom: "1%", |
| | | top: "18%", |
| | | bottom: xMode === "material" ? "0%" : "1%", |
| | | containLabel: true, |
| | | }, |
| | | xAxis: { |
| | |
| | | axisLabel: { |
| | | color: "#B8C8E0", |
| | | fontSize: getResponsiveValue(11), |
| | | margin: getResponsiveValue(10), |
| | | margin: |
| | | xMode === "material" |
| | | ? getResponsiveValue(6) |
| | | : getResponsiveValue(10), |
| | | rotate: xMode === "material" && periods.length > 5 ? 28 : 0, |
| | | interval: 0, |
| | | }, |
| | | splitLine: { show: false }, |
| | | }, |
| | |
| | | |
| | | // 板材单耗图表配置(接口数据,与砌块结构一致) |
| | | const boardCostChartOption = computed(() => { |
| | | const xMode = boardCostChartSeries.value.xMode || "time"; |
| | | const periods = boardCostChartSeries.value.categories || []; |
| | | const inputData = boardCostChartSeries.value.input || []; |
| | | const outputData = boardCostChartSeries.value.output || []; |
| | |
| | | grid: { |
| | | left: "1%", |
| | | right: "1%", |
| | | bottom: "1%", |
| | | top: "18%", |
| | | bottom: xMode === "material" ? "0%" : "1%", |
| | | containLabel: true, |
| | | }, |
| | | xAxis: { |
| | |
| | | axisLabel: { |
| | | color: "#B8C8E0", |
| | | fontSize: getResponsiveValue(11), |
| | | margin: getResponsiveValue(10), |
| | | margin: |
| | | xMode === "material" |
| | | ? getResponsiveValue(6) |
| | | : getResponsiveValue(10), |
| | | rotate: xMode === "material" && periods.length > 5 ? 28 : 0, |
| | | interval: 0, |
| | | }, |
| | | splitLine: { show: false }, |
| | | }, |
| | |
| | | // 物料生产量分析(接口数据) |
| | | const productionChartOption = computed(() => { |
| | | const periods = productionChartSeries.value.categories || []; |
| | | const values = productionChartSeries.value.values || []; |
| | | const lineColor = "#4A8BFF"; |
| | | const seriesName = "产量"; |
| | | const series = [ |
| | | { |
| | | name: seriesName, |
| | | data: values, |
| | | 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" }, |
| | | ]), |
| | | }, |
| | | 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", |
| | |
| | | borderWidth: getResponsiveValue(1), |
| | | textStyle: { color: "#B8C8E0", fontSize: getResponsiveValue(11) }, |
| | | }, |
| | | legend: { |
| | | data: [seriesName], |
| | | top: "10%", |
| | | right: "1%", |
| | | textStyle: { |
| | | color: "#B8C8E0", |
| | | fontSize: getResponsiveValue(9), |
| | | }, |
| | | itemWidth: getResponsiveValue(10), |
| | | itemHeight: getResponsiveValue(10), |
| | | }, |
| | | legend: { show: false }, |
| | | grid: { |
| | | left: "1%", |
| | | right: "1%", |
| | | bottom: "1%", |
| | | top: "28%", |
| | | top: "10%", |
| | | containLabel: true, |
| | | }, |
| | | xAxis: { |
| | |
| | | 板材: "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) { |
| | |
| | | monthlyConsumption: "--", |
| | | yearlyConsumption: "--", |
| | | }; |
| | | blockCostChartSeries.value = { categories: [], input: [], output: [] }; |
| | | blockCostChartSeries.value = { |
| | | xMode: "time", |
| | | categories: [], |
| | | input: [], |
| | | output: [], |
| | | }; |
| | | updateCharts(); |
| | | return; |
| | | } |
| | | const dateType = mapTimeDimensionToDateType(blockTimeDimension.value); |
| | | try { |
| | | const res = await getProductionStatisticsBlocks({ materialName, dateType }); |
| | | 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] || {}; |
| | |
| | | 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), |
| | |
| | | monthlyConsumption: "--", |
| | | yearlyConsumption: "--", |
| | | }; |
| | | boardCostChartSeries.value = { categories: [], input: [], output: [] }; |
| | | boardCostChartSeries.value = { |
| | | xMode: "time", |
| | | categories: [], |
| | | input: [], |
| | | output: [], |
| | | }; |
| | | updateCharts(); |
| | | return; |
| | | } |
| | | const dateType = mapTimeDimensionToDateType(boardTimeDimension.value); |
| | | try { |
| | | const res = await getProductionStatisticsPlates({ materialName, dateType }); |
| | | 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] || {}; |
| | |
| | | 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), |
| | |
| | | const loadBlockMaterials = async () => { |
| | | try { |
| | | const res = await getProductionMaterials({ materialType: "1" }); |
| | | const list = Array.isArray(res.data) ? [...res.data] : []; |
| | | const raw = Array.isArray(res.data) ? [...res.data] : []; |
| | | const list = ["全部", ...raw]; |
| | | blockMaterialList.value = list; |
| | | if (list.length) { |
| | | if (!list.includes(blockSelectedMaterial.value)) { |
| | |
| | | monthlyConsumption: "--", |
| | | yearlyConsumption: "--", |
| | | }; |
| | | blockCostChartSeries.value = { categories: [], input: [], output: [] }; |
| | | blockCostChartSeries.value = { |
| | | xMode: "time", |
| | | categories: [], |
| | | input: [], |
| | | output: [], |
| | | }; |
| | | updateCharts(); |
| | | } |
| | | } catch (e) { |
| | |
| | | const loadBoardMaterials = async () => { |
| | | try { |
| | | const res = await getProductionMaterials({ materialType: "2" }); |
| | | const list = Array.isArray(res.data) ? [...res.data] : []; |
| | | const raw = Array.isArray(res.data) ? [...res.data] : []; |
| | | const list = ["全部", ...raw]; |
| | | boardMaterialList.value = list; |
| | | if (list.length) { |
| | | if (!list.includes(boardSelectedMaterial.value)) { |
| | |
| | | monthlyConsumption: "--", |
| | | yearlyConsumption: "--", |
| | | }; |
| | | boardCostChartSeries.value = { categories: [], input: [], output: [] }; |
| | | boardCostChartSeries.value = { |
| | | xMode: "time", |
| | | categories: [], |
| | | input: [], |
| | | output: [], |
| | | }; |
| | | updateCharts(); |
| | | } |
| | | } catch (e) { |
| | |
| | | const res = await getMaterialProductionAnalysis({ dateType }); |
| | | const payload = res.data && typeof res.data === "object" ? res.data : {}; |
| | | const cat = productionCategory.value; |
| | | const list = Array.isArray(payload[cat]) ? payload[cat] : []; |
| | | const field = productionOutputKey[cat] || "totalOutput"; |
| | | productionChartSeries.value = { |
| | | categories: list.map(i => i.dateStr), |
| | | values: list.map(i => Number(i[field]) || 0), |
| | | }; |
| | | |
| | | 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); |
| | |
| | | updateCharts(); |
| | | }; |
| | | |
| | | /** 砌块/板材容器高度变化后(如隐藏汇总卡)需 resize,否则底部留白 */ |
| | | const resizeCostPanelCharts = () => { |
| | | nextTick(() => { |
| | | requestAnimationFrame(() => { |
| | | blockCostChartInstance?.resize(); |
| | | boardCostChartInstance?.resize(); |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | // 更新图表 |
| | | const updateCharts = () => { |
| | | // 更新砌块成本图表 |
| | | // 更新砌块成本图表(replaceMerge:全部↔单物料时横轴时间/物料切换) |
| | | if (blockCostChartInstance) { |
| | | blockCostChartInstance.setOption(blockCostChartOption.value); |
| | | blockCostChartInstance.setOption(blockCostChartOption.value, { |
| | | replaceMerge: ["series", "xAxis"], |
| | | }); |
| | | } |
| | | |
| | | // 更新板材成本图表 |
| | | if (boardCostChartInstance) { |
| | | boardCostChartInstance.setOption(boardCostChartOption.value); |
| | | boardCostChartInstance.setOption(boardCostChartOption.value, { |
| | | replaceMerge: ["series", "xAxis"], |
| | | }); |
| | | } |
| | | |
| | | // 更新产量分析图表 |
| | | // 更新产量分析图表(replaceMerge:全部↔砌块/板材切换时去掉多余折线,避免合并残留) |
| | | if (productionChartInstance) { |
| | | productionChartInstance.setOption(productionChartOption.value); |
| | | productionChartInstance.setOption(productionChartOption.value, { |
| | | replaceMerge: ["series"], |
| | | }); |
| | | } |
| | | |
| | | // 更新新增客户趋势图表 |
| | |
| | | if (salesRankingChartInstance) { |
| | | salesRankingChartInstance.setOption(salesRankingChartOption.value); |
| | | } |
| | | |
| | | resizeCostPanelCharts(); |
| | | }; |
| | | |
| | | // 处理时间维度选择 |
| | |
| | | // 等待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(), |
| | |
| | | loadSolidWasteData(), |
| | | loadEnergyData(), |
| | | ]); |
| | | resizeCostPanelCharts(); |
| | | }); |
| | | |
| | | // 添加窗口大小变化监听 |
| | |
| | | clearInterval(timeTicker); |
| | | timeTicker = null; |
| | | } |
| | | |
| | | blockCostChartResizeObserver?.disconnect(); |
| | | blockCostChartResizeObserver = null; |
| | | boardCostChartResizeObserver?.disconnect(); |
| | | boardCostChartResizeObserver = null; |
| | | |
| | | if (blockCostChartInstance) { |
| | | blockCostChartInstance.dispose(); |
| | |
| | | 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 { |
| | | height: 24vh; |
| | | flex: 1; |
| | | min-height: 0; |
| | | height: auto; |
| | | width: 100%; |
| | | } |
| | | |
| | | .bi-panel-bottom-right .echart-fill { |