制造成本分析修改

This commit is contained in:
2026-03-26 09:52:35 +08:00
parent 4f7466bb29
commit e770dc4fed
9 changed files with 628 additions and 23 deletions

View File

@@ -321,7 +321,7 @@ export default {
display: inline-flex;
position: absolute;
right: 5%;
top: 10%;
top: 25px;
z-index: 9999;
align-items: center;
border-radius: 24px;

View File

@@ -37,7 +37,7 @@
gap: 12px;
grid-template-columns: 1624px;
">
<dataTrend :trendData="trend" :title="'数据趋势'" />
<dataTrend :trendData="trend" :relatedData="relatedData" :title="'指标分析&数据趋势'" />
</div>
</div>
</div>
@@ -95,7 +95,10 @@ export default {
monData: {},
totalData: {},
trend: [],
relatedData: {},
relatedData: {
total:[],
current:[]
},
trendName: '包材成本',
};
},
@@ -208,8 +211,9 @@ export default {
this.totalData = res.data.totalMonthData.find(item => {
return item.name === "包材成本";
});
// this.relatedMon = res.data.relatedMon
;
this.relatedData.total =res.data.totalMonthData,
this.relatedData.current= res.data.currentMonthData
this.trend = res.data.dataTrend
});
},

View File

@@ -0,0 +1,368 @@
<template>
<div class="cockpitContainer" :class="['cockpitContainer__' + size]">
<div class="content-top" :class="['content-top__' + topSize]">
<!-- 使用 flex 容器包裹图标和文字实现垂直居中 -->
<div class="title-wrapper">
<svg-icon class="title-icon" :icon-class="icon" />
<span class="title-text">
{{ name }}
</span>
<svg-icon icon-class="turn-data" style='font-size: 40px;margin-left: 60px;' @click='switchData'/>
</div>
<div v-show="this.name === '指标分析'" class="tab-group">
<!-- 月度Tab点击切换状态动态绑定样式 -->
<div class="tab-item" :class="{ active: activeTab === 'month' }" @click="handleTabClick('month')">
月度
</div>
<!-- 累计Tab点击切换状态动态绑定样式 -->
<div class="tab-item" :class="{ active: activeTab === 'total' }" @click="handleTabClick('total')">
累计
</div>
</div>
</div>
<div class="cockpitContainer-body">
<slot>
<div class="test-body">something test....</div>
</slot>
</div>
</div>
</template>
<script>
export default {
name: 'Container',
components: {},
// eslint-disable-next-line vue/require-prop-types
props: ['size', 'icon', 'topSize', 'isShowTab'],
data() {
return {
activeTab: 'month', // 初始化激活的Tab支持父组件传默认值
name:'指标分析',
};
},
computed: {},
methods: {
handleTabClick(tabType) {
this.activeTab = tabType;
// 向父组件派发Tab切换事件传递当前选中的Tab类型
this.$emit('tabChange', tabType);
// 可选:同时传递更详细的信息(如标签名)
// this.$emit('tabChange', { type: tabType, name: tabType === 'month' ? '月度' : '累计' });
},
switchData() {
console.log('aaa')
if (this.name === '数据趋势') {
this.name = '指标分析';
} else {
this.name = '数据趋势';
}
this.$emit('dataChange', this.name);
}
},
};
</script>
<style scoped lang="scss">
.cockpitContainer {
display: inline-block;
// width: 100%;
// height: 100%;
padding: 6px;
display: flex;
flex-direction: column;
position: relative;
.content-top {
height: 60px;
.title-wrapper {
display: flex;
align-items: center;
margin-left: 10px;
/* 垂直居中关键属性 */
height: 100%;
/* 继承父容器高度,确保垂直居中范围 */
}
.title-icon {
font-size: 30px;
margin-right: 12px;
margin-top: 4px;
/* 图标和文字之间的间距 */
flex-shrink: 0;
/* 防止图标被压缩 */
}
.title-text {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 24px;
color: #000000;
letter-spacing: 3px;
text-align: left;
font-style: normal;
// 移除固定行高,避免影响垂直对齐
// line-height: 60px;
}
// width: 547px;
// background: url(../../../assets/img/contentTopBasic.png) no-repeat;
// background-size: 100% 100%;
// background-position: 0 0;
&__basic {
// width: 547px;
background: url(../../../assets/img/contentTopBasic.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__middle {
background: url(../../../assets/img/topTileMiddle.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__large {
background: url(../../../assets/img/topTitleLargeBg.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__KFAPTopTitle {
background: url(../../../assets/img/KFAPTopTitle.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__psiTopTitleBasic {
background: url(../../../assets/img/psiTopTitleBasic.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__rawTopTitleLarge {
background: url(../../../assets/img/rawTopTitleLarge.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__calendarTitleBg {
background: url(../../../assets/img/calendarTitleBg.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__indicatorDetailsTitleBg {
background: url(../../../assets/img/indicatorDetailsTitleBg.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
}
&__topBasic {
background: url(../../../assets/img/top-basic.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__bottomBasic {
background: url(../../../assets/img/bottom-basic.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__operatingBasic {
background: url(../../../assets/img/operating-basic.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__operatingLarge {
background: url(../../../assets/img/operating-large.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__profitTopBasic {
background: url(../../../assets/img/profitTopBasic.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__profitMiddleBasic {
background: url(../../../assets/img/profitMiddleBasic.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__psiBasicBg {
background: url(../../../assets/img/psiBasicBg.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__psiMiddleBg {
background: url(../../../assets/img/psiMiddleBg.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__costBasicBg {
background: url(../../../assets/img/costBasicBg.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__rawTopBg {
background: url(../../../assets/img/rawTopBg.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__opLargeBg {
background: url(../../../assets/img/opLargeBg.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__calendarBg {
background: url(../../../assets/img/calendarBg.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__indicatorDetailsBg {
background: url(../../../assets/img/indicatorDetailsBg.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
// &__left {
// background: url(../../../../../../../assets/img/left.png) no-repeat;
// background-size: 100% 100%;
// background-position: 0 0;
// }
// &__energyConsumption {
// background: url(../../../../../../../assets/img/energyConsumption.png) no-repeat;
// background-size: 100% 100%;
// background-position: 0 0;
// }
// &__left2 {
// background: url(../../assets/left_2.png) no-repeat;
// background-size: 100% 100%;
// background-position: 0 0;
// }
// &__left3 {
// background: url(../../assets/left_3.png) no-repeat;
// background-size: 100% 100%;
// background-position: 0 0;
// }
// &__mid2 {
// background: url(../../assets/mid_2.png) no-repeat;
// background-size: 100% 100%;
// background-position: 0 0;
// }
// &__mid3 {
// background: url(../../assets/mid_3.png) no-repeat;
// background-size: 100% 100%;
// background-position: 0 0;
// }
// &__right1 {
// background: url(../../assets/right_1.png) no-repeat;
// background-size: 100% 100%;
// background-position: 0 0;
// }
// &__right2 {
// background: url(../../assets/right_2.png) no-repeat;
// background-size: 100% 100%;
// background-position: 0 0;
// }
// &__right3 {
// background: url(../../assets/right_3.png) no-repeat;
// background-size: 100% 100%;
// background-position: 0 0;
// }
// &__weekRight2 {
// background: url(../../assets/week_right_2.png) no-repeat;
// background-size: 100% 100%;
// background-position: 0 0;
// }
// &__weekMidTop {
// background: url(../../assets/week-mid-top.png) no-repeat;
// background-size: 100% 100%;
// background-position: 0 0;
// }
// &__weekMidMid {
// background: url(../../assets/week-mid-mid.png) no-repeat;
// background-size: 100% 100%;
// background-position: 0 0;
// }
&::after {
content: ' ';
display: block;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
// background: inherit;
/* 设置模糊,不用 filter */
backdrop-filter: blur(5px);
z-index: -1;
}
}
.container-body {
flex: 1;
}
.tab-group {
display: inline-flex;
position: absolute;
right: 5%;
top: 25px;
z-index: 9999;
align-items: center;
border-radius: 24px;
overflow: hidden;
gap: 8px; // Tab之间的间距
}
// Tab基础样式统一
.tab-item {
padding: 0 24px;
width: 79px;
height: 24px;
line-height: 24px;
font-size: 12px;
cursor: pointer;
text-align: center;
border-radius: 12px;
transition: all 0.2s ease; // 样式切换动画
}
// 未激活的Tab样式原first-child样式
.tab-item:not(.active) {
background: #ECF4FE;
color: #0B58FF;
}
// 激活的Tab样式原last-child样式
.tab-item.active {
background: #3071FF;
color: #F9FCFF;
font-weight: bold;
}
</style>

View File

@@ -44,7 +44,7 @@
</div>
</div>
<div class="lineBottom" style="height: 100%; width: 100%">
<operatingLineBar :chartData="chartD" style="height: 99%; width: 100%" />
<operatingLineBar :chartData="chartD" style="height: 99%; width: 100%" :showRelated='showRelated'/>
</div>
</div>
</template>
@@ -56,7 +56,7 @@ import * as echarts from 'echarts';
export default {
name: "Container",
components: { operatingLineBar },
props: ["chartData"],
props: ["chartData","showRelated"],
data() {
return {
activeButton: 0,

View File

@@ -1,39 +1,98 @@
<template>
<div style="flex: 1">
<Container name="数据趋势" icon="cockpitItemIcon" size="opLargeBg" topSize="large">
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%; gap: 16px">
<div class="right" style="
height: 491px;
display: flex;
width: 1595px;
background-color: rgba(249, 252, 255, 1);
">
<!-- 直接使用计算属性 chartData无需手动更新 -->
<dataTrendBar @handleGetItemData="getData" :chartData="chartData" />
<Container :isShowTab="true" icon="cockpitItemIcon" size="opLargeBg" topSize="large" @tabChange="handleChange" @dataChange='dataChange'>
<div v-show='showRelated' style="padding: 14px 16px; width: 100%; gap: 16px">
<div style="display: flex; gap: 8px;overflow: auto">
<div
v-for="item in sortedIndicatorsFirst"
:key="item.key"
class="dashboard"
>
<div class="title">
{{ item.name }}·{{ item.unit }}
</div>
<div style='font-size: 16px;text-align: right;padding-right: 5px;'>
<span>完成率:<span style='color: #0B58FF;'>{{item.data.proportion}}%</span></span>
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:item.data.completed>0?'#30B590':'#FF9423'}" >{{item.data.diffValue}}</span></span>
</div>
<operatingSingleBar :showRelated='showRelated' :detailData="item.data"></operatingSingleBar>
</div>
</div>
<div style="display: flex; gap: 8px;margin-top: 10px;overflow: auto">
<div
v-for="item in sortedIndicatorsSec"
:key="item.key"
class="dashboard"
>
<div class="title">
{{ item.name }}·{{ item.unit }}
</div>
<div style='font-size: 16px;text-align: right;padding-right: 5px;'>
<span>完成率:<span style='color: #0B58FF;'>{{item.data.proportion}}%</span></span>
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:item.data.completed>0?'#30B590':'#FF9423'}" >{{item.data.diffValue}}</span></span>
</div>
<operatingSingleBar :showRelated='showRelated' :detailData="item.data"></operatingSingleBar>
</div>
</div>
</div>
<div v-show='!showRelated' class="right" style="
height: 499px;
display: flex;
width: 1578px;
background-color: rgba(249, 252, 255, 1);
margin-left: 12px;
margin-top: 10px;
">
<dataTrendBar @handleGetItemData="getData" :chartData="chartData" :showRelated='showRelated'/>
</div>
</Container>
</div>
</template>
<script>
import Container from "../components/container.vue";
import Container from "./containerPackMatCost.vue";
import dataTrendBar from "./dataTrendBarProcessingLabor.vue";
import operatingSingleBar from './operatingSingleBar.vue'
export default {
name: "ProductionStatus",
components: { Container, dataTrendBar },
components: { Container, dataTrendBar, operatingSingleBar },
props: {
trendData: {
type: Array,
default: () => [],
},
relatedData: {
type: Object,
default: () => ({
current: [], // 物料月度数据(数组格式,存储各物料项)
total: [] // 物料累计数据(数组格式,存储各物料项)
})
},
},
data() {
return {
// 移除:原 chartData 定义,改为计算属性
showRelated:true,
titleName:'指标分析',
activeData: this.relatedData.current || [],
};
},
mounted() {
console.log('物料组件挂载时的激活数据:', this.activeData);
},
watch: {
// 对齐第二个组件:监听物料数据变化,同步更新激活数据集
relatedData: {
handler(newVal) {
this.activeData = newVal.current || {};
},
immediate: true, // 组件挂载时立即执行
deep: true // 深度监听对象内部变化
}
},
// 移除:原 watch 监听配置,计算属性自动响应 trendData 变化
computed: {
/**
@@ -74,8 +133,103 @@ export default {
rawData: this.trendData, // 透传原始数据,方便子组件使用
};
},
indicatorDefsFirst() {
return [
{ key: 'f1', name: '防霉纸单价', unit: '元/吨'},
{ key: 'f2', name: '瓦楞纸单价', unit: '元/平方米'},
{ key: 'f3', name: '蜂窝板单价', unit: '元/平方米'},
{ key: 'f4', name: 'PE聚乙烯吹塑薄膜单价', unit: '元/吨'},
{ key: 'f5', name: '缠绕膜单价', unit: '元/吨'},
{ key: 'f6', name: '塑钢带单价', unit: '元/吨'},
{ key: 'f7', name: '胶合板包装箱单价', unit: '元/立方米'},
{ key: 'f8', name: '防霉隔离粉单价', unit: '元/吨'},
{ key: 'f9', name: '干燥剂单价', unit: '元/吨'},
]
},
indicatorsFirst() {
const fallback = { targetValue: 0, value: 0, completed: 0, diffValue: 0 }
const list = (Array.isArray(this.activeData) ? this.activeData : [])
console.log('list===================',list)
return this.indicatorDefsFirst.map(def => {
const data = list.find(item => item && item.name === def.name) || fallback
return {
...def,
data,
sortValue: Number((data && data.value) ?? 0)
}
})
},
sortedIndicatorsFirst() {
const unitOrder = ['元/吨','元/平方米','元/立方米']
const unitRank = (u) => {
const idx = unitOrder.indexOf(u)
return idx === -1 ? 999 : idx
}
return this.indicatorsFirst.slice().sort((a, b) => {
const ur = unitRank(a.unit) - unitRank(b.unit)
if (ur !== 0) return ur
const vr = (b.sortValue ?? -Infinity) - (a.sortValue ?? -Infinity)
if (vr !== 0) return vr
return String(a.key).localeCompare(String(b.key))
})
},
indicatorDefsSec() {
return [
{ key: 's1', name: '防霉纸用量', unit: '吨'},
{ key: 's2', name: '瓦楞纸用量', unit: '平方米'},
{ key: 's3', name: '蜂窝板用量', unit: '平方米'},
{ key: 's4', name: 'PE聚乙烯吹塑薄膜用量', unit: '吨'},
{ key: 's5', name: '缠绕膜用量', unit: '吨'},
{ key: 's6', name: '塑钢带用量', unit: '吨'},
{ key: 's7', name: '胶合板包装箱用量', unit: '立方米'},
{ key: 's8', name: '防霉隔离粉用量', unit: '吨'},
{ key: 's9', name: '干燥剂用量', unit: '吨'},
]
},
indicatorsSec() {
const fallback = { targetValue: 0, value: 0, completed: 0, diffValue: 0 }
const list = (Array.isArray(this.activeData) ? this.activeData : [])
return this.indicatorDefsSec.map(def => {
const data = list.find(item => item && item.name.includes(def.name)) || fallback
return {
...def,
data,
sortValue: Number((data && data.value) ?? 0)
}
})
},
sortedIndicatorsSec() {
const unitOrder = ['吨','平方米','立方米']
const unitRank = (u) => {
const idx = unitOrder.indexOf(u)
return idx === -1 ? 999 : idx
}
return this.indicatorsSec.slice().sort((a, b) => {
const ur = unitRank(a.unit) - unitRank(b.unit)
if (ur !== 0) return ur
const vr = (b.sortValue ?? -Infinity) - (a.sortValue ?? -Infinity)
if (vr !== 0) return vr
return String(a.key).localeCompare(String(b.key))
})
}
},
methods: {
handleChange(value) {
console.log('Tab 切换值:', value);
// 根据 Tab 值更新当前激活的数据集
if (value === 'month') {
// 切换为月度数据
this.activeData = this.relatedData.current || [];
} else {
// 切换为累计数据(非 month 均视为累计,可根据实际需求调整判断条件)
this.activeData = this.relatedData.total || [];
}
console.log('当前激活数据集:', this.activeData);
},
// 对齐第二个组件:优化点击事件,接收物料名和路由路径参数
/**
* 格式化时间戳为年月格式YYYY-MM
* @param {Number} timestamp 13位毫秒级时间戳
@@ -93,11 +247,41 @@ export default {
getData(value) {
this.$emit('getData', value)
},
dataChange(val) {
if (val === '数据趋势') {
this.showRelated = false
}else{
this.showRelated = true
}
}
},
};
</script>
<style lang="scss" scoped>
.dashboard {
width: 390px;
height: 220px;
background: #F9FCFF;
padding: 16px 0 0 10px;
flex-shrink: 0;
flex-grow: 1;
flex-basis:390px;
// overflow: auto;
.title {
// width: 190px;
height: 18px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 18px;
color: #000000;
line-height: 18px;
letter-spacing: 1px;
text-align: left;
font-style: normal;
letter-spacing: 2px;
}
}
/* 原有样式保持不变 */
.scroll-container {
max-height: 210px;

View File

@@ -33,6 +33,10 @@ export default {
type: Object,
default: () => ({}),
},
showRelated: {
type: Boolean,
default: true,
}
},
mounted() {
this.$nextTick(() => {
@@ -49,6 +53,15 @@ export default {
deep: true,
immediate: true
},
showRelated: {
handler(val) {
if (!val) {
this.resizeHandler()
}
},
deep: true,
immediate: true
}
},
beforeDestroy() {
// 组件销毁时清理资源
@@ -107,7 +120,6 @@ export default {
// // params: { baseIndex: baseIndex }
// });
});
// 定义resize处理函数命名函数方便移除
this.resizeHandler = () => {
this.myChart && this.myChart.resize();

View File

@@ -21,6 +21,10 @@ export default {
// validator: (value) => {
// return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
// }
},
showRelated: {
type: Boolean,
default: false,
}
},
mounted() {
@@ -39,6 +43,15 @@ export default {
},
deep: true,
immediate: true // 初始化时立即执行
},
showRelated: {
handler(val) {
if (val) {
this.myChart.resize();
}
},
deep: true,
immediate: true
}
},
methods: {

View File

@@ -1,6 +1,6 @@
<template>
<div class="lineBottom" style="height: 160px; width: 100%">
<operatingLineBarSaleSingle :refName="'totalOperating'" :chartData="chartD" style="height: 100%; width: 100%" />
<operatingLineBarSaleSingle :refName="'totalOperating'" :chartData="chartD" style="height: 100%; width: 100%" :showRelated='showRelated'/>
</div>
</template>
@@ -10,7 +10,7 @@ import operatingLineBarSaleSingle from './operatingLineBarSaleSingle.vue';
export default {
name: "Container",
components: { operatingLineBarSaleSingle },
props: ["detailData"],
props: ["detailData","showRelated"],
data() {
return {
// 1. 存储resize监听函数的引用方便后续销毁