This commit is contained in:
‘937886381’
2025-11-12 16:56:14 +08:00
commit 1ea62af1d6
1135 changed files with 109385 additions and 0 deletions

View File

@@ -0,0 +1,236 @@
<template>
<div class="coreBar">
<div class="barTop">
<div class="barTop-left" style="display: flex;">
<div class="title">费用指标趋势</div>
<div class="legend">
<span class="legend-item manager">管理费用</span>
<span class="legend-item sale">销售费用</span>
<span class="legend-item finance">财务费用</span>
</div>
</div>
</div>
<div class="lineBottom" style="height: 214px; width: 100%">
<!-- 传递 series 数据给子组件添加 key 确保数据更新时重新渲染 -->
<coreLineChart style="height: 214px; width: 680px" :chart-series="chartSeries"
:key="JSON.stringify(chartSeries)" />
</div>
</div>
</template>
<script>
import coreLineChart from './coresBar.vue';
import * as echarts from 'echarts';
export default {
name: "Container",
components: { coreLineChart },
props: ["name", "size", "icon"],
data() {
return {
activeButton: undefined,
itemList: [
{ unit: "单价·元/m²", targetValue: 16, currentValue: 14.5, progress: 90 },
{ unit: "净价·元/m²", targetValue: 16, currentValue: 15.2, progress: 85 },
{ unit: "销量·万m²", targetValue: 20, currentValue: 16, progress: 80 },
{ unit: "双镀面板·万m²", targetValue: 15, currentValue: 13.8, progress: 92 },
],
// 定义要传递的 series 数据(对应图例的三个费用类型)
chartSeries: [
{
name: '管理费用',
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(11, 88, 255, .5)' },
itemStyle: {
color: 'rgba(11, 88, 255, .5)',
borderColor: 'rgba(11, 88, 255, 1)',
borderWidth: 1
},
areaStyle: {
opacity: 0.5,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(11, 88, 255, .4)' },
{ offset: 1, color: 'rgba(18, 255, 245, 0)' },
]),
},
data: [140, 232, 101, 264, 90, 340] // 与 xAxis 数据长度一致6个月份
},
{
name: '销售费用',
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(54, 181, 138, .5)' },
itemStyle: {
color: 'rgba(54, 181, 138, .5)',
borderColor: 'rgba(54, 181, 138, 1)',
borderWidth: 1
},
areaStyle: {
opacity: 0.5,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(54, 181, 138, .4)' },
{ offset: 1, color: 'rgba(18, 255, 245, 0)' },
]),
},
data: [120, 282, 111, 234, 220, 340]
},
{
name: '财务费用',
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(255, 132, 0, .5)' },
itemStyle: {
color: 'rgba(255, 132, 0, .5)',
borderColor: 'rgba(255, 132, 0, 1)',
borderWidth: 1
},
areaStyle: {
opacity: 0.5,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 132, 0, .4)' },
{ offset: 1, color: 'rgba(18, 255, 245, 0)' },
]),
},
data: [90, 150, 180, 120, 250, 280]
}
]
};
},
methods: {}
};
</script>
<style scoped lang="scss">
/* 原有样式保持不变 */
.coreBar {
display: flex;
flex-direction: column;
padding: 12px;
.barTop {
display: flex;
gap: 8px;
justify-content: space-between;
.title {
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;
}
.legend {
display: flex;
gap: 24px;
margin-left: 30px;
.legend-item {
position: relative;
font-family: PingFangSC, PingFang SC;
font-size: 14px;
color: rgba(0, 0, 0, 0.8);
&::before {
content: "";
position: absolute;
left: -13px;
top: 50%;
transform: translateY(-50%);
width: 6px;
height: 6px;
border-radius: 50%;
}
&::after {
content: "";
position: absolute;
left: -16px;
top: 50%;
transform: translateY(-50%);
width: 12px;
height: 2px;
}
}
.manager::before,
.manager::after {
background-color: rgba(11, 88, 255, 1);
}
.sale::before,
.sale::after {
background-color: rgba(54, 181, 138, 1);
}
.finance::before,
.finance::after {
background-color: rgba(255, 132, 0, 1);
}
}
.button-group {
display: flex;
position: relative;
gap: 2px;
width: 252px;
align-items: center;
height: 24px;
background: #ecf4fe;
border-radius: 12px;
.button-line {
position: absolute;
width: 1px;
height: 14px;
border: 1px solid rgba(11, 88, 255, 0.25);
}
.lineOne {
top: 5px;
left: 59px;
}
.lineTwo {
top: 5px;
left: 134px;
}
.lineThree {
top: 5px;
left: 193px;
}
.item-button {
cursor: pointer;
width: 54px;
height: 24px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #0b58ff;
line-height: 24px;
text-align: center;
font-style: normal;
}
.item-button.active {
width: 54px;
height: 24px;
background: #3071ff;
border-radius: 12px;
color: #ffffff;
font-weight: 500;
}
}
}
}
</style>

View File

@@ -0,0 +1,422 @@
<template>
<header class="report-header">
<!-- 左侧区域logo + 标题 -->
<div class="left-content">
<img style="height: 36px;" src="../../../assets/img/cnbm.png" alt="benmaLogo" >
<div class="top-title">{{ topTitle }}</div>
</div>
<div class="center-content">
<!-- 循环 pageRoutes不再硬编码文字 -->
<div class="item" v-for="(page, index) in pageRoutes" :key="index" @click="goToPage(page.path, index)">
<span class="item-text">{{ page.text }}</span>
</div>
</div>
<!-- :class="{ 'no-skew': activeIndex === index }
" -->
<!-- 右侧区域全屏按钮 -->
<div class="right-content">
<el-button type="text" class="screen-btn" :title="isFullScreen ? '退出全屏' : '全屏'" @click="changeFullScreen">
<svg-icon style="color: #0B58FF;" v-if="isFullScreen" icon-class="unFullScreenView" />
<svg-icon style="color: #0B58FF;" v-else icon-class="fullScreenView" />
</el-button>
</div>
<!-- 时间选择区域//年按钮 + label + 日期选择器 -->
<div class="timeType">
<div class="item" v-for="(item, index) in timeTypes" :key="index" @click="activeTime = index"
:class="{ 'no-skew': activeTime === index }">
<span class="item-text">{{ item.text }}</span>
</div>
<div class="dateP">
<div class="label">
<span class="label-text">日期选择</span>
</div>
<el-date-picker v-model="date" :type="getPickerType" :placeholder="getPickerPlaceholder"
class="custom-date-picker" style="width: 132px;height: 29px;" @change="emitTimeRange" />
</div>
</div>
</header>
</template>
<script>
export default {
name: 'Header',
props: {
isFullScreen: { type: Boolean, default: false },
topTitle: { type: String, default: '' }
},
data() {
return {
currentTime: '',
timeTimer: null,
date: undefined,
activeIndex: -1,
activeTime: 1, // 0=日1=月2=年(默认选中“日”)
pageRoutes: [
{ text: '营业收入', path: '/operatingRevenue' },
{ text: '利润分析', path: '/profitAnalysis' },
{ text: '产销率库存分析', path: '/PSIAnal' },
{ text: '成本分析', path: '/cost/cost' },
{ text: '驾驶舱报表', path: '/cockpit' }
],
// 定义时间类型配置text=按钮文字pickerType=选择器类型placeholder=占位符
timeTypes: [
{ text: '日', pickerType: 'date', placeholder: '选择日期' },
{ text: '月', pickerType: 'month', placeholder: '选择月份' },
{ text: '年', pickerType: 'year', placeholder: '选择年份' }
]
}
},
computed: {
// 动态获取日期选择器类型
getPickerType() {
return this.timeTypes[this.activeTime].pickerType;
},
// 动态获取日期选择器占位符
getPickerPlaceholder() {
return this.timeTypes[this.activeTime].placeholder;
}
},
methods: {
goToPage(path, index) {
// 1. 跳转到对应路由
this.$router.push(path);
// 2. 更新activeIndex让当前点击项高亮
this.activeIndex = index;
},
changeFullScreen() { this.$emit('screenfullChange') },
padZero(num) { return num < 10 ? '0' + num : num },
/**
* 核心方法1根据维度计算时间区间首次进入时基于赋值的当月日期计算“当月第一天0点→次月第一天0点”
* @returns {Object} 包含 start开始时间、end结束时间、dimension维度的区间对象
*/
calculateTimeRange() {
let startTime = 0;
let endTime = 0;
const mode = this.activeTime + 1; // 1=日2=月3=年
const defaultMoment = moment(); // 默认当前时间
// 处理选择的日期转为moment对象兼容不同选择器格式
console.log('this.date', this.date);
const targetMoment = this.date
? moment(this.date, this.getPickerType === 'date' ? 'YYYY-MM-DD' : (this.getPickerType === 'month' ? 'YYYY-MM' : 'YYYY'))
: defaultMoment;
// 验证日期有效性
if (!targetMoment.isValid()) {
console.error('无效日期:', this.date);
return { startTime, endTime, mode };
}
// 1. 日维度当天0点 → 次日0点
if (this.activeTime === 0) {
startTime = targetMoment.startOf('day').valueOf(); // 当天00:00:00 时间戳
endTime = targetMoment.add(1, 'day').startOf('day').valueOf(); // 次日00:00:00 时间戳
}
// 2. 月维度当月1日0点 → 次月1日0点
else if (this.activeTime === 1) {
startTime = targetMoment.startOf('month').valueOf(); // 当月1日00:00:00 时间戳
endTime = targetMoment.add(1, 'month').startOf('month').valueOf(); // 次月1日00:00:00 时间戳
}
// 3. 年维度当年1月1日0点 → 次年1月1日0点
else if (this.activeTime === 2) {
startTime = targetMoment.startOf('year').valueOf(); // 当年1月1日00:00:00 时间戳
endTime = targetMoment.add(1, 'year').startOf('year').valueOf(); // 次年1月1日00:00:00 时间戳
}
// 调试输出(格式化显示,便于验证)
console.log('时间范围计算结果:', {
mode,
startTime: moment(startTime).format('YYYY-MM-DD HH:mm:ss'),
endTime: moment(endTime).format('YYYY-MM-DD HH:mm:ss'),
startTimeStamp: startTime,
endTimeStamp: endTime
});
return { startTime, endTime, mode };
},
/**
* 核心方法2传递时间区间给父组件首次进入时触发传递“当月第一天0点→次月第一天0点”
*/
emitTimeRange() {
const timeRange = this.calculateTimeRange();
this.$emit('timeRangeChange', timeRange);
// 调试用:查看首次传递的区间(如{start: "2025-10-01T00:00:00", end: "2025-11-01T00:00:00", dimension: "月"}
console.log('当前时间区间:', timeRange);
}
},
watch: {
// 维度切换时:清空选择的日期,并传递当前维度的默认区间
activeTime(newVal, oldVal) {
if (newVal !== oldVal) {
this.date = undefined;
// this.emitTimeRange();
}
}
},
mounted() {
// 核心逻辑:首次进入页面,计算当月默认日期并赋值给选择器,同时传递区间
const now = new Date();
const year = now.getFullYear();
const month = this.padZero(now.getMonth() + 1); // 月份从0开始+1后补零如1月→01
// 赋值当月默认日期格式YYYY-MM适配month类型选择器
this.date = `${year}-${month}`;
// 确保选择器渲染完成后传递“当月第一天0点→次月第一天0点”的区间
this.$nextTick(() => this.emitTimeRange());
},
}
</script>
<style scoped lang="scss">
/* 原有样式不变仅补充label文字的倾斜抵消样式 */
@font-face {
font-family: "YouSheBiaoTiHei";
src: url('../../../assets/fonts/YouSheBiaoTiHe.ttf') format('truetype');
}
.report-header {
height: 117px;
width: 100%;
display: flex;
justify-content: space-around;
background: url('../../../assets/img/topTitle.png') no-repeat;
background-size: cover;
background-position: 0 0;
box-sizing: border-box;
position: relative;
/* 确保timeType绝对定位生效 */
.left-content {
margin-top: 11px;
margin-left: 44px;
height: 55px;
display: flex;
align-items: center;
gap: 16px;
}
.top-title {
height: 55px;
font-family: "YouSheBiaoTiHei", sans-serif;
font-size: 42px;
color: #1E1651;
line-height: 55px;
letter-spacing: 6px;
text-align: left;
}
.center-content {
display: flex;
gap: 8px;
margin-top: 18px;
margin-left: 70px;
.item {
width: 180px;
height: 50px;
background: #E1EEFC;
transform: skew(-20deg);
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 20px;
color: #1E1651;
line-height: 50px;
letter-spacing: 2px;
text-align: center;
cursor: pointer;
overflow: hidden;
box-shadow: 0px 13px 16px 0px rgba(179, 217, 255, 0.43),
0px 2px 4px 0px rgba(92, 140, 255, 0.25),
inset 0px -43px 13px 0px rgba(255, 255, 255, 0.51);
.item-text {
display: inline-block;
transform: skew(20deg);
}
}
.item.no-skew {
background: none !important;
transform: none !important;
box-shadow: none !important;
color: #1E1651;
.item-text {
transform: none !important;
}
}
}
.timeType {
position: absolute;
display: flex;
align-items: center;
/* 垂直居中,避免元素高低错位 */
top: 42px;
right:10px;
margin-top: 18px;
gap: 0;
/* 清除间隙,让按钮与选择器紧密连接 */
}
.timeType .item {
width: 40px;
height: 28px;
background: rgba(236, 244, 254, 1);
transform: skew(-20deg);
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: rgba(11, 88, 255, 1);
line-height: 28px;
letter-spacing: 2px;
text-align: center;
cursor: pointer;
overflow: hidden;
/* 选中按钮与未选中按钮倾斜角度统一,避免切换时跳动 */
}
.timeType .item .item-text {
display: inline-block;
transform: skew(20deg);
transition: all 0.2s ease;
}
.timeType .item.no-skew {
background: rgba(11, 88, 255, 1);
color: rgba(249, 252, 255, 1);
transform: skew(-20deg) !important;
/* 统一倾斜角度修复原30deg的错位 */
box-shadow: 0 2px 8px rgba(11, 88, 255, 0.3);
}
.timeType .item.no-skew .item-text {
transform: skew(20deg) !important;
/* 同步统一文字倾斜角度 */
}
.dateP {
position: relative;
margin-left: 30px;
display: flex;
align-items: center;
gap: 0;
}
.dateP .label {
width: 70px;
height: 28px;
background: rgba(236, 244, 254, 1);
transform: skew(-25deg);
/* 与按钮倾斜角度统一原30deg改为25deg避免视觉错位 */
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: #0B58FF;
line-height: 28px;
text-align: center;
overflow: hidden;
}
/* 补充label文字抵消倾斜原代码遗漏导致文字倾斜 */
.dateP .label-text {
display: inline-block;
transform: skew(25deg);
/* 与label倾斜角度相反确保文字正立 */
}
.right-content {
display: flex;
flex-direction: column;
margin-top: 12px;
margin-right: 4px;
gap: 20px;
}
.current-time {
color: #FFFFFF;
font-family: PingFangSC, PingFang SC;
font-weight: 500;
font-size: 22px;
line-height: 24px;
letter-spacing: 1px;
}
.screen-btn {
width: 26px;
margin-left: 300px;
color: #00fff0;
font-size: 26px;
padding: 0;
}
}
/* 日期选择器样式保持不变 */
::v-deep .custom-date-picker {
position: absolute;
right: 8px;
width: 132px !important;
height: 28px !important;
position: relative;
margin: 0 !important;
/* 1. 调整输入框文字:确保行高与输入框高度一致,垂直居中 */
.el-input__inner {
height: 28px !important;
width: 132px !important;
text-align: center;
padding-left: 15px !important;
padding-right: 32px !important;
/* 给图标留空间,避免文字被遮挡 */
font-size: 14px !important;
line-height: 28px !important;
/* 行高=输入框高度,文字垂直居中 */
color: rgba(237, 245, 253, 1) !important;
vertical-align: middle !important;
/* 强制文字垂直对齐 */
clip-path: polygon(18px 0, 100% 0, 100% 100%, 0 100%);
border: none !important;
box-shadow: none !important;
background-color: rgba(11, 88, 255, 1) !important;
border-left: 1px solid rgba(255, 255, 255, 0.2);
}
/* 2. 调整图标容器:让图标与文字在同一水平线上 */
.el-input__prefix {
left: auto !important;
right: 8px !important;
top: 50% !important;
/* 从40%改为50%,基于输入框垂直居中 */
transform: translateY(-50%) !important;
/* 向上偏移自身50%,精准居中 */
display: inline-flex !important;
/* 让容器内图标垂直居中 */
align-items: center !important;
/* 图标在容器内垂直居中 */
height: 28px !important;
/* 容器高度=输入框高度,避免偏移 */
}
/* 3. 调整图标本身:确保图标大小和对齐方式 */
.el-input__icon {
color: #ffffff !important;
font-size: 16px !important;
line-height: 28px !important;
/* 图标行高=输入框高度,与文字对齐 */
vertical-align: middle !important;
/* 强制图标垂直对齐 */
}
/* 4. 图标伪类:确保颜色和对齐继承 */
.el-icon-date::before {
color: #ffffff !important;
font-size: 16px !important;
line-height: inherit !important;
/* 继承父级行高,避免错位 */
}
}
</style>

View File

@@ -0,0 +1,263 @@
<template>
<div style="">
<Container name="利润影主要响因素" icon="cockpitItemIcon" size="profitMiddleBasic" topSize="KFAPTopTitle">
<div class="kpi-content" style="padding: 14px 16px; display: flex;width: 100%;">
<div class="left" style="width: 382px;">
<top-item :itemList="targetItemList" />
</div>
<div class="bottom"
style="height: 227px; margin-left: 16px; display: flex; width: 100%;background-color: rgba(249, 252, 255, 1);">
<KFAPMiddleBar :chartData="{
allPlaceNames, incomeValueData, profitProportionData, costValueData, costCompletedData
}" />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
import topItem from './profit-item-middle.vue'
import KFAPMiddleBar from './KFAPMiddleBar.vue'
export default {
name: 'ProductionStatus',
components: { Container, topItem, KFAPMiddleBar },
props: {
middleChartData: {
type: Array,
default: () => []
},
middleItemData: {
type: Array,
default: () => []
},
},
data() {
return {
parentItemList: [
{ name: "营业收入", targetValue: 0, value: 0, proportion: 0, route: 'operatingRevenue' },
{ name: "成本", targetValue: 0, value: 0, proportion: 0, route: 'cost/cost' },
]
}
},
computed: {
allPlaceNames() {
if (!Array.isArray(this.middleChartData)) return [];
return this.middleChartData.map(item => this.cleanPlaceName(item.name)).filter(Boolean)
},
PlaceNames() {
if (!Array.isArray(this.middleChartData)) return [];
return this.middleChartData.map(item => item.name).filter(Boolean);
},
profitProportionData() {
if (!Array.isArray(this.middleChartData)) return [];
return this.PlaceNames.map(place => {
const target = this.middleChartData.find(item => item.name === place);
return target?.profitProportion || 0;
});
},
incomeValueData() {
if (!Array.isArray(this.middleChartData)) return [];
return this.PlaceNames.map(place => {
const target = this.middleChartData.find(item => item.name === place);
return target?.income || 0;
});
},
costValueData() {
if (!Array.isArray(this.middleChartData)) return [];
return this.PlaceNames.map(place => {
const target = this.middleChartData.find(item => item.name === place);
return target?.cost || 0;
});
},
costCompletedData() {
if (!Array.isArray(this.middleChartData)) return [];
return this.PlaceNames.map(place => {
const target = this.middleChartData.find(item => item.name === place);
return target?.costCompleted || 0;
});
},
targetItemList() {
// 基于 parentItemList 的名称,匹配 productAndSaleData 中对应名称的数据
return this.parentItemList.map(parentItem => {
// 按名称精准匹配name 完全一致才关联)
const productItem = this.middleItemData.find(
item => item.name === parentItem.name
) || {};
// 优先使用匹配到的 productItem 数据,缺失则用 parentItem 的默认值0
return {
name: parentItem.name, // 保留原名称(销量、产量等)
targetValue: productItem.targetValue ?? parentItem.targetValue ?? 0,
value: productItem.value ?? parentItem.value ?? 0,
proportion: productItem.proportion ?? parentItem.proportion ?? 0,
completed: productItem.completed ?? parentItem.completed ?? 1,
route: parentItem.route // 路由沿用 parentItem 的配置
};
});
}
},
methods: {
cleanPlaceName(place) {
return place?.replace('分公司', '') || ''; // 匹配“分公司”并替换为空兼容null/undefined
},
// // 唯一的地名提取方法(定义在 methods 中,确保是函数)
// extractPlace(item) {
// if (!item?.name) return ''; // 处理空值,避免报错
// const match = item.name.match(/(.+?)(基地|利润总额|营业收入|成本)/);
// return match ? match[1] : item.name;
// }
}
}
</script>
<style lang='scss' scoped>
/* 样式部分保持不变 */
.scroll-container {
max-height: 210px;
overflow-y: auto;
overflow-x: hidden;
padding: 10px 0;
&::-webkit-scrollbar {
display: none;
}
scrollbar-width: none;
-ms-overflow-style: none;
}
.proBarInfo {
display: flex;
flex-direction: column;
padding: 8px 27px;
margin-bottom: 10px;
}
.proBarInfoEqInfo {
display: flex;
justify-content: space-between;
align-items: center;
}
.slot {
width: 21px;
height: 23px;
background: rgba(0, 106, 205, 0.22);
backdrop-filter: blur(1.5px);
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #68B5FF;
line-height: 23px;
text-align: center;
font-style: normal;
}
.eq-name {
margin-left: 8px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
letter-spacing: 1px;
text-align: left;
font-style: normal;
}
.eqStatus {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.splitLine {
width: 1px;
height: 14px;
border: 1px solid #ADADAD;
margin: 0 8px;
}
.yield {
height: 18px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #00FFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.proBarInfoEqInfoLeft {
display: flex;
align-items: center;
}
.proBarInfoEqInfoRight {
display: flex;
align-items: center;
}
.proBarWrapper {
position: relative;
height: 10px;
margin-top: 6px;
border-radius: 5px;
overflow: hidden;
}
.proBarLine {
width: 100%;
height: 100%;
background: linear-gradient(65deg, rgba(82, 82, 82, 0) 0%, #ACACAC 100%);
opacity: 0.2;
}
.proBarLineTop {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: linear-gradient(65deg, rgba(53, 223, 247, 0) 0%, rgba(54, 220, 246, 0.92) 92%, #36F6E5 100%, #37ACF5 100%);
border-radius: 5px;
transition: width 0.3s ease;
}
.chartImgBottom {
position: absolute;
bottom: 45px;
left: 58px;
}
.line {
display: inline-block;
position: absolute;
left: 57px;
bottom: 42px;
width: 1px;
height: 20px;
background-color: #00E8FF;
}
</style>
<style>
.production-status-chart-tooltip {
background: #0a2b4f77 !important;
border: none !important;
backdrop-filter: blur(12px);
}
.production-status-chart-tooltip * {
color: #fff !important;
}
</style>

View File

@@ -0,0 +1,273 @@
<template>
<div class="coreBar" style="height: 100%;">
<div class="barTop">
<!-- 标题单独左对齐 -->
<!-- <div class="title">销售指标趋势</div> -->
<!-- 关键新增右侧容器包裹图例和按钮组实现整体靠右 -->
<div class="right-container">
<div class="legend">
<span class="legend-item">
<span class="legend-icon line yield"></span>
利润
</span>
<span class="legend-item">
<span class="legend-icon square target"></span>
营业收入
</span>
<!-- 给第三个第四个图例项加 close-item -->
<span class="legend-item close-item">
<span class="legend-icon square achieved"></span>
<!-- 达标 -->
</span>
<span class="legend-item close-item">
<span class="legend-icon square unachieved"></span>
成本
</span>
</div>
<!-- <div class="button-group">
<div style="letter-spacing: 8px" class="item-button" :class="{ active: activeButton === 0 }"
@click="activeButton = 0">
销量
</div>
<div style="letter-spacing: 8px" class="item-button" :class="{ active: activeButton === 1 }"
@click="activeButton = 1">
毛利率
</div>
</div> -->
</div>
</div>
<div class="lineBottom" style="height: 100%; width: 100%">
<coreLineChart :chart-data="{
series: chartSeries,
allPlaceNames: chartData.allPlaceNames
}" style="height: 100%; width: 100%" />
</div>
</div>
</template>
<script>
import coreLineChart from './operatingLineBar.vue';
import * as echarts from 'echarts';
export default {
name: "Container",
components: { coreLineChart },
props: ['chartData' ],
data() {
return {
};
},
computed: {
// 原有计算属性allPlaceNames、profitProportionData 等)保持不变
// 新增:定义图表 series 配置
chartSeries() {
const { allPlaceNames, incomeValueData, profitProportionData, costValueData, costCompletedData } = this.chartData
// 处理空数据默认值
const incomeData = incomeValueData || Array(allPlaceNames.length).fill(0);
const profitData = profitProportionData || Array(allPlaceNames.length).fill(0);
const costData = costValueData || Array(allPlaceNames.length).fill(0);
return [
// 1. 利润占比:折线图
{
name: '利润',
type: 'line',
yAxisIndex: 1,
lineStyle: {
color: 'rgba(40, 138, 255, .5)',
width: 2
},
itemStyle: {
color: 'rgba(40, 138, 255, 1)',
borderColor: 'rgba(40, 138, 255, 1)',
borderWidth: 2,
radius: 4
},
areaStyle: {
opacity: 0.2,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(40, 138, 255, .9)' },
{ offset: 1, color: 'rgba(255, 132, 0, 0)' }
])
},
data: profitData,
symbol: 'circle',
symbolSize: 6
},
// 2. 营业收入:柱状图
{
name: '营业收入',
type: 'bar',
yAxisIndex: 0,
barWidth: 18,
itemStyle: {
color: '#2889FF',
borderRadius: [4, 4, 0, 0]
},
data: incomeData
},
// 3. 成本:柱状图(动态颜色)
{
name: '成本',
type: 'bar',
yAxisIndex: 0,
barWidth: 18,
itemStyle: {
color: (params) => {
const completed = costCompletedData?.[params.dataIndex] ?? 0;
return completed === 1 ? 'rgba(255, 132, 0, 1)' : 'rgba(40, 203, 151, 1)';
},
borderRadius: [4, 4, 0, 0]
},
data: costData
}
];
}
},
methods: {},
};
</script>
<style scoped lang="scss">
.coreBar {
display: flex;
flex-direction: column;
width: 100%;
padding: 12px;
.barTop {
display: flex;
justify-content: flex-end; // 标题左、右侧容器右,整体两端对齐
align-items: center; // 垂直居中,避免上下错位
gap: 16px; // 标题与右侧容器的最小间距,防止拥挤
width: 100%; // 确保占满父容器,实现两端对齐
.title {
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;
// 标题固定在左侧,不挤压右侧空间
white-space: nowrap;
}
// 1. 右侧容器:包裹图例和按钮组,整体靠右
.right-container {
display: flex;
align-items: center; // 图例和按钮组垂直居中
gap: 24px; // 图例与按钮组的间距,避免贴紧
margin-right: 46px; // 右侧整体留边,与原按钮组边距一致
}
// 2. 图例:在右侧容器内横向排列
.legend {
display: flex;
gap: 16px; // 图例项之间间距,避免重叠
align-items: center;
// 移除原margin-left避免位置偏移
margin: 0;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: rgba(0, 0, 0, 0.8);
text-align: left;
font-style: normal;
white-space: nowrap; // 防止图例文字换行
}
.legend-icon {
display: inline-block;
}
.legend-icon.line {
width: 12px;
height: 2px;
position: relative;
&::before {
position: absolute;
content: "";
top: -2px;
left: 3px;
width: 6px;
border-radius: 50%;
height: 6px;
background-color: rgba(40, 138, 255, 1);
}
}
.legend-icon.square {
width: 8px;
height: 8px;
}
// 图例颜色
.yield {
background: rgba(40, 138, 255, 1);
}
.target {
background: #2889FF;
}
.achieved {
background: rgba(40, 203, 151, 1);
}
.unachieved {
background: rgba(255, 132, 0, 1);
}
.legend-item.close-item+.legend-item.close-item {
margin-left: -8px; // 负margin抵消默认gap数值可根据需求调整
// 若原gap是16px想让两者间距变为8px设置 margin-left: -8px 即可
}
// 3. 按钮组:在右侧容器内,保留原有样式
.button-group {
display: flex;
position: relative;
gap: 2px;
width: 283px;
align-items: center;
height: 24px;
background: #ecf4fe;
border-radius: 12px;
// 移除原margin-right由右侧容器统一控制
margin: 0;
.item-button {
cursor: pointer;
width: 142px;
height: 24px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #0b58ff;
line-height: 24px;
text-align: center;
font-style: normal;
letter-spacing: 8px; // 确保文字间距生效
padding-left: 8px; // 抵消letter-spacing导致的文字左偏
}
.item-button.active {
width: 142px;
height: 24px;
background: #3071ff;
border-radius: 12px;
color: #ffffff;
font-weight: 500;
}
}
}
}
</style>

View File

@@ -0,0 +1,404 @@
<template>
<div style="flex: 1">
<Container name="库存" icon="cockpitItemIcon" size="psiMiddleBg" topSize="KFAPTopTitle">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex;width: 100%;">
<div class="bottom"
style="height: 376px;display: flex; width: 100%;background-color: rgba(249, 252, 255, 1);flex-direction: column;">
<!-- <top-item /> -->
<div class="data" style="width: 100%; display: flex;padding: 26px 0 0 0 ;justify-content: space-evenly;">
<div class="data-item" style="display: flex;gap: 21px;">
<img style="width: 100px;height: 103px;" src="../../../assets/img/monthNum.png" alt="">
<div class="text">
<div class="number">
{{ stockVO.endMonthValue || 0 }}
</div>
<div class="title">
月末数量
</div>
</div>
</div>
<div class="data-item" style="display: flex;gap: 21px;">
<img style="width: 100px;height: 103px;" src="../../../assets/img/averageNum.png" alt="">
<div class="text">
<div class="number">
{{ stockVO.avgValue || 0 }}
</div>
<div class="title">
平均数量
</div>
</div>
</div>
<div class="data-item" style="display: flex;gap: 21px;">
<img style="width: 100px;height: 103px;" src="../../../assets/img/dayNum.png" alt="">
<div class="text">
<div class="number">
{{ stockVO.totalDays || 0 }}
</div>
<div class="title">
折合天数
</div>
</div>
</div>
</div>
<div class="chart" style="height: 240px;">
<PSDOBar :stockValues="stockValues" />
</div>
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
// import * as echarts from 'echarts'
import topItem from './operating-item.vue'
import PSDOBar from './PSDOBar.vue'
export default {
name: 'ProductionStatus',
components: { Container, topItem, PSDOBar },
// mixins: [resize],
props: {
stockVO: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
type: Object,
default: () => ({})
}
},
data() {
return {
stockValues:[],
}
},
watch: {
stockVO: {
handler(newValue, oldValue) {
this.stockValues = newValue.stockValues
},
deep: true // 若对象内属性变化需触发,需加 deep: true
}
},
mounted() {
// 初始化图表(若需展示图表,需在模板中添加对应 DOM
// this.$nextTick(() => this.updateChart())
},
beforeDestroy() {
// 销毁图表,避免内存泄漏
if (this.chart) {
this.chart.dispose()
this.chart = null
}
},
methods: {
updateChart() {
// 注意:原代码中图表依赖 id 为 "productionStatusChart" 的 DOM需在模板中补充否则会报错
// 示例:在 Container 内添加 <div id="productionStatusChart" style="height: 200px;"></div>
if (!document.getElementById('productionStatusChart')) return
if (this.chart) this.chart.dispose()
this.chart = echarts.init(document.getElementById('productionStatusChart'))
const data = [
this.productionOverviewVo.input || 0,
this.productionOverviewVo.output || 0,
this.productionOverviewVo.ng || 0,
this.productionOverviewVo.lowValue || 0,
this.productionOverviewVo.scrap || 0,
this.productionOverviewVo.inProcess || 0,
this.productionOverviewVo.engineer || 0
]
const option = {
type: 'bar',
grid: { left: 51, right: 40, top: 50, bottom: 45 },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
className: 'production-status-chart-tooltip'
},
xAxis: {
type: 'category',
offset: 8,
data: ['投入', '产出', '待判', '低价值', '报废', '在制', '实验片'],
axisTick: { show: false },
axisLine: { show: true, onZero: false, lineStyle: { color: '#00E8FF' } },
axisLabel: {
color: 'rgba(255,255,255,0.7)',
fontSize: 12,
interval: 0,
width: 38,
overflow: 'break'
}
},
yAxis: {
type: 'value',
name: '单位/片',
nameTextStyle: { color: 'rgba(255,255,255,0.7)', fontSize: 14, align: 'left' },
min: () => 0,
max: (value) => Math.ceil(value.max),
scale: true,
axisTick: { show: false },
axisLabel: { color: 'rgba(255,255,255,0.7)', fontSize: 12 },
splitLine: { lineStyle: { color: 'RGBA(24, 88, 100, 0.6)', type: 'dashed' } },
axisLine: { show: true, lineStyle: { color: '#00E8FF' } }
},
series: [
{
type: 'pictorialBar',
label: { show: true, position: 'top', distance: -3, color: '#89CDFF', fontSize: 11 },
symbolSize: [20, 8],
symbolOffset: [0, 5],
z: 20,
itemStyle: {
borderColor: '#3588C7',
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'RGBA(22, 89, 98, 1)' },
{ offset: 1, color: '#3588C7' }
])
},
data: data
},
{
type: 'bar',
barWidth: 20,
itemStyle: {
borderWidth: 1,
borderColor: '#3588C7',
opacity: 0.8,
color: {
x: 0, y: 0, x2: 0, y2: 1,
type: 'linear',
global: false,
colorStops: [
{ offset: 0, color: 'rgba(73,178,255,0)' },
{ offset: 0.5, color: 'rgba(0, 232, 255, .5)' },
{ offset: 1, color: 'rgba(0, 232, 255, 1)' }
]
}
},
tooltip: { show: false },
data: data
},
{
type: 'pictorialBar',
symbolSize: [20, 8],
symbolOffset: [0, -4],
z: 12,
symbolPosition: 'end',
itemStyle: {
borderColor: 'rgba(0, 232, 255, 1)',
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'RGBA(22, 89, 98, 1)' },
{ offset: 1, color: '#3588C7' }
])
},
tooltip: { show: false },
data: data
}
]
}
this.chart.setOption(option)
}
}
}
</script>
<style lang='scss' scoped>
/* 3. 核心:滚动容器样式(固定高度+溢出滚动+隐藏滚动条) */
.scroll-container {
/* 1. 固定容器高度根据页面布局调整示例300px超出则滚动 */
max-height: 210px;
/* 2. 溢出滚动:内容超出高度时显示滚动功能 */
overflow-y: auto;
/* 3. 隐藏横向滚动条(防止设备名过长导致横向滚动) */
overflow-x: hidden;
/* 4. 内边距:与标题栏和容器边缘对齐 */
padding: 10px 0;
/* 5. 隐藏滚动条(兼容主流浏览器) */
/* Chrome/Safari */
&::-webkit-scrollbar {
display: none;
}
/* Firefox */
scrollbar-width: none;
/* IE/Edge */
-ms-overflow-style: none;
}
/* 设备项样式优化:增加间距,避免拥挤 */
.proBarInfo {
display: flex;
flex-direction: column;
padding: 8px 27px;
/* 调整内边距,优化排版 */
margin-bottom: 10px;
/* 设备项之间的垂直间距 */
}
/* 原有样式保留,优化细节 */
.proBarInfoEqInfo {
display: flex;
justify-content: space-between;
align-items: center;
/* 垂直居中,避免序号/文字错位 */
}
.slot {
width: 21px;
height: 23px;
background: rgba(0, 106, 205, 0.22);
backdrop-filter: blur(1.5px);
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #68B5FF;
line-height: 23px;
/* 垂直居中文字 */
text-align: center;
font-style: normal;
}
.eq-name {
margin-left: 8px;
/* 增加与序号的间距 */
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
letter-spacing: 1px;
text-align: left;
font-style: normal;
}
.eqStatus {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.splitLine {
width: 1px;
height: 14px;
border: 1px solid #ADADAD;
margin: 0 8px;
/* 优化分割线间距 */
}
.yield {
height: 18px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #00FFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.proBarInfoEqInfoLeft {
display: flex;
align-items: center;
/* 序号和设备名垂直居中 */
}
.proBarInfoEqInfoRight {
display: flex;
align-items: center;
/* 状态/分割线/百分比垂直居中 */
}
.proBarWrapper {
position: relative;
height: 10px;
margin-top: 6px;
/* 进度条与上方信息的间距 */
border-radius: 5px;
/* 进度条圆角,优化视觉 */
overflow: hidden;
}
.proBarLine {
width: 100%;
height: 100%;
background: linear-gradient(65deg, rgba(82, 82, 82, 0) 0%, #ACACAC 100%);
opacity: 0.2;
}
.proBarLineTop {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: linear-gradient(65deg, rgba(53, 223, 247, 0) 0%, rgba(54, 220, 246, 0.92) 92%, #36F6E5 100%, #37ACF5 100%);
border-radius: 5px;
transition: width 0.3s ease;
/* 进度变化时添加过渡动画,更流畅 */
}
/* 图表相关样式保留 */
.chartImgBottom {
position: absolute;
bottom: 45px;
left: 58px;
}
.line {
display: inline-block;
position: absolute;
left: 57px;
bottom: 42px;
width: 1px;
height: 20px;
background-color: #00E8FF;
}
.data-item {
align-items: center;
.number {
font-family: "YouSheBiaoTiHei", sans-serif;
width: 130px;
height: 38px;
font-family: YouSheBiaoTiHei;
font-size: 38px;
color: #000000;
line-height: 38px;
letter-spacing: 2px;
text-align: left;
font-style: normal;
}
.title{
width: 76px;
height: 25px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 18px;
color: #868687;
line-height: 25px;
letter-spacing: 1px;
text-align: left;
font-style: normal;
}
}
</style>
<style>
/* 全局 tooltip 样式(不使用 scoped确保生效 */
.production-status-chart-tooltip {
background: #0a2b4f77 !important;
border: none !important;
backdrop-filter: blur(12px);
}
.production-status-chart-tooltip * {
color: #fff !important;
}
</style>

View File

@@ -0,0 +1,178 @@
<template>
<div class="coreBar" style="height: 100%;">
<div class="barTop">
<!-- 标题单独左对齐 -->
<!-- <div class="title">销售指标趋势</div> -->
<!-- 关键新增右侧容器包裹图例和按钮组实现整体靠右 -->
<div class="right-container">
<div class="legend">
<span class="legend-item close-item">
<span class="legend-icon square unachieved"></span>
库存
</span>
</div>
<!-- <div class="button-group">
<div style="letter-spacing: 8px" class="item-button" :class="{ active: activeButton === 0 }"
@click="activeButton = 0">
销量
</div>
<div style="letter-spacing: 8px" class="item-button" :class="{ active: activeButton === 1 }"
@click="activeButton = 1">
毛利率
</div>
</div> -->
</div>
</div>
<div class="lineBottom" style="height: 100%; width: 100%">
<psrChartBar :stockValues="stockValues" style="height: 100%; width: 100%" />
</div>
</div>
</template>
<script>
import psrChartBar from './psrChartBar.vue';
export default {
name: "Container",
components: { psrChartBar },
props: [ "stockValues"],
data() {
return {
activeButton: 0,
};
},
computed: {},
methods: {},
};
</script>
<style scoped lang="scss">
.coreBar {
display: flex;
flex-direction: column;
width: 100%;
padding: 12px;
.barTop {
display: flex;
justify-content: flex-end; // 标题左、右侧容器右,整体两端对齐
align-items: center; // 垂直居中,避免上下错位
gap: 16px; // 标题与右侧容器的最小间距,防止拥挤
width: 100%; // 确保占满父容器,实现两端对齐
.title {
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;
// 标题固定在左侧,不挤压右侧空间
white-space: nowrap;
}
// 1. 右侧容器:包裹图例和按钮组,整体靠右
.right-container {
display: flex;
align-items: center; // 图例和按钮组垂直居中
gap: 24px; // 图例与按钮组的间距,避免贴紧
margin-right: 46px; // 右侧整体留边,与原按钮组边距一致
}
// 2. 图例:在右侧容器内横向排列
.legend {
display: flex;
gap: 16px; // 图例项之间间距,避免重叠
align-items: center;
// 移除原margin-left避免位置偏移
margin: 0;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: rgba(0, 0, 0, 0.8);
text-align: left;
font-style: normal;
white-space: nowrap; // 防止图例文字换行
}
.legend-icon {
display: inline-block;
}
.legend-icon.line {
width: 12px;
height: 2px;
position: relative;
&::before {
position: absolute;
content: "";
top: -2px;
left: 3px;
width: 6px;
border-radius: 50%;
height: 6px;
background-color: rgba(40, 138, 255, 1);
}
}
.legend-icon.square {
width: 8px;
height: 8px;
}
.unachieved {
background: rgba(40, 137, 255, 1);
}
.legend-item.close-item+.legend-item.close-item {
margin-left: -8px; // 负margin抵消默认gap数值可根据需求调整
// 若原gap是16px想让两者间距变为8px设置 margin-left: -8px 即可
}
// 3. 按钮组:在右侧容器内,保留原有样式
.button-group {
display: flex;
position: relative;
gap: 2px;
width: 283px;
align-items: center;
height: 24px;
background: #ecf4fe;
border-radius: 12px;
// 移除原margin-right由右侧容器统一控制
margin: 0;
.item-button {
cursor: pointer;
width: 142px;
height: 24px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #0b58ff;
line-height: 24px;
text-align: center;
font-style: normal;
letter-spacing: 8px; // 确保文字间距生效
padding-left: 8px; // 抵消letter-spacing导致的文字左偏
}
.item-button.active {
width: 142px;
height: 24px;
background: #3071ff;
border-radius: 12px;
color: #ffffff;
font-weight: 500;
}
}
}
}
</style>

View File

@@ -0,0 +1,253 @@
<!--
* @Date: 2020-12-14 09:07:03
* @LastEditors: zhp
* @LastEditTime: 2024-09-05 09:50:14
* @FilePath: \mt-bus-fe\src\views\OperationalOverview\components\baseTable.vue
* @Description:
-->
<template>
<div class="visual-base-table-container">
<el-table :max-height="maxHeight" ref="scroll_Table" @mouseenter.native="autoScroll(true)"
@mouseleave.native="autoScroll(false)" v-loading="isLoading"
:header-cell-style="{ background: 'rgba(218, 226, 237, 1)', color: 'rgba(0, 0, 0, .6)',padding:'3px 2px'}" :row-style="setRowStyle"
:data="renderData" border style="width: 100%; background: transparent">
<el-table-column v-if="page && limit && showIndex" prop="_pageIndex" label="序号" :width="70" align="center" />
<el-table-column v-for="item in renderTableHeadList" :key="item.prop" :show-overflow-tooltip="showOverflow"
v-bind="item">
<template slot-scope="scope">
<component :is="item.subcomponent" v-if="item.subcomponent" :inject-data="{...scope.row, ...item}"
@emitData="emitData" />
<span v-else>{{ scope.row[item.prop] | commonFilter(item.filter) }}</span>
</template>
</el-table-column>
<slot name="content" />
</el-table>
</div>
</template>
<script>
import { isObject, isString } from 'lodash'
export default {
name: 'BaseTable',
filters: {
commonFilter: (source, filterType = a => a) => {
return filterType(source)
}
},
props: {
maxHeight: {
type: [Number, String], // 支持数字如300或字符串如'300px'
required: false,
default: 200 // 原固定值,作为默认 fallback
},
tableData: {
type: Array,
required: true,
validator: val => val.filter(item => !isObject(item)).length === 0
},
tableConfig: {
type: Array,
required: true,
validator: val => val.filter(item => !isString(item.prop) || !isString(item.label)).length === 0
},
isLoading: {
type: Boolean,
required: false
},
page: {
type: Number,
required: false,
default: 1
},
limit: {
type: Number,
required: false,
default: 5
},
beilv: {
type: Number,
default: 1
},
showOverflow: {
type: Boolean,
default: true
},
showIndex: {
type: Boolean,
default: true
}
},
data() {
return {
tableConfigBak: [],
selectedBox: new Array(100).fill(true)
}
},
computed: {
renderData() {
if (this.tableData.length && !this.tableData[0]._pageIndex) {
this.tableData.forEach((item, index) => {
item._pageIndex = (this.page - 1) * this.limit + index + 1
})
}
return this.tableData.slice((this.page - 1) * this.limit, this.page * this.limit)
},
renderTableHeadList() {
return this.tableConfig.filter((item, index) => {
return this.selectedBox[index]
})
}
},
beforeMount() {
this.selectedBox = new Array(100).fill(true)
},
mounted() {
this.autoScroll()
},
beforeDestroy() {
this.autoScroll(true)
},
methods: {
autoScroll(stop) {
const table = this.$refs.scroll_Table
if (!table) return; // 防止table未加载时出错
const divData = table.$refs.bodyWrapper
if (stop) {
window.clearInterval(this.scrolltimer)
} else {
// 先清除已有计时器,避免重复创建
if (this.scrolltimer) {
window.clearInterval(this.scrolltimer)
}
this.scrolltimer = window.setInterval(() => {
// 每次滚动1像素
divData.scrollTop += 1
// 关键修改:使用>=判断,允许微小像素偏差
if (divData.scrollTop + divData.clientHeight >= divData.scrollHeight - 1) {
// 滚动到底部后,重置到顶部(延迟一点更自然)
// setTimeout(() => {
divData.scrollTop = 0
// }, 2000); // 停顿500ms后再从头滚动
}
}, 200) // 滚动速度(数值越小越快)
}
},
emitData(val) {
this.$emit('emitFun', val)
},
setRowStyle(v) {
if (v.rowIndex % 2 === 0) {
return {
background: '#F9FCFF',
color: 'rgba(87, 87, 87, 1)',
height: 35 + 'px',
lineHeight: 26 + 'px',
padding: 0,
fontSize: 12 + 'px'
}
} else {
return {
background: 'rgba(239, 243, 248, 1)',
color: 'rgba(87, 87, 87, 1)',
height: 35 + 'px',
lineHeight: 26 + 'px',
padding: 0,
fontSize: 12 + 'px'
}
}
},
setCellStyle() {
return {
// lineHeight: 23 + 'px'
}
}
}
}
</script>
<style lang="scss" scoped>
// @import "./styles/index.scss";
.visual-base-table-container {
.el-table {
border: 0;
// .el-table__body-wrapper::-webkit-scrollbar-thumb {
// background-color: blue;
// border-radius: 3px;
// }
// 关键修改:隐藏滚动条但保留滚动功能
&::-webkit-scrollbar {
width: 0;
height: 0;
background: transparent;
}
// 隐藏表头的gutter
.el-table__header .el-table__cell.gutter {
display: none !important;
}
// 表格主体内容区滚动条处理
.el-table__body-wrapper {
&::-webkit-scrollbar {
width: 0;
height: 0;
}
&::-webkit-scrollbar-thumb {
background: transparent;
}
&::-webkit-scrollbar-track {
background: transparent;
}
overflow-y: auto !important; // 确保垂直滚动可用
overflow-x: auto !important; // 确保水平滚动可用
}
// 固定列滚动条处理
.el-table__fixed,
.el-table__fixed-right {
.el-table__fixed-body-wrapper {
&::-webkit-scrollbar {
width: 0;
height: 0;
}
overflow-y: auto !important;
overflow-x: auto !important;
}
}
}
.el-table::before,.el-table--border::after {
background-color: transparent;
}
.el-table th,td{
border-color: rgba(221, 221, 221, 1) !important;
padding: 0;
}
.el-table tr {
background: transparent;
}
.el-table__row:hover > td {
background-color: rgba(79,114,136,0.29) !important;
}
.el-table__row--striped:hover > td {
background-color: rgba(79,114,136,0.29) !important;
}
}
// .setting {
// text-align: right;
// padding: 15px;
// .setting-box {
// width: 100px;
// }
// i {
// color: #aaa;
// @extend .pointer;
// }
// }
</style>

View File

@@ -0,0 +1,287 @@
<template>
<div class="cockpitContainer" :class="['cockpitContainer__' + size]">
<div class="container-top" style="display: flex;gap: 9px;">
<div class="title-wrapper content-top-left">
<svg-icon class="title-icon" :icon-class="icon" />
<span class="title-text">
{{ name }}
</span>
</div>
<!-- <div class="content-top-left">
<svg-icon style="font-size: 32px; margin-left: 16px" :icon-class="icon" />
<span style="
width: 412px;
height: 32px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 24px;
color: #000000;
line-height: 60px;
letter-spacing: 3px;
text-align: left;
font-style: normal;
">
{{ name }}
</span>
</div> -->
<div class="title-wrapper content-top-right">
<svg-icon class="title-icon" :icon-class="icon" />
<span class="title-text">
{{ nameTwo }}
</span>
</div>
<!-- <div class="content-top-right">
<svg-icon style="font-size: 32px; margin-left: 16px" :icon-class="icon" />
<span style="
width: 412px;
height: 32px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 24px;
color: #000000;
line-height: 60px;
letter-spacing: 3px;
text-align: left;
font-style: normal;
">
{{ nameTwo }}
</span>
</div> -->
</div>
<div class="container-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: ['name', 'size', 'icon','nameTwo'],
data() {
return {};
},
computed: {},
methods: {},
};
</script>
<style scoped lang="scss">
.cockpitContainer {
display: inline-block;
// width: 100%;
// height: 00px;
padding: 6px;
display: flex;
flex-direction: column;
position: relative;
.title-wrapper {
display: flex;
align-items: center;
// margin-left: 10px;
/* 垂直居中关键属性 */
height: 100%;
/* 继承父容器高度,确保垂直居中范围 */
}
.title-icon {
font-size: 30px;
margin-right: 12px;
margin-top: 4px;
margin-left: 10px;
/* 图标和文字之间的间距 */
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;
}
.content-top-left {
width: 270px;
height: 60px;
background: linear-gradient(90deg, #FFFFFF 0%, rgba(253, 255, 255, 0) 100%);
position: relative;
overflow: hidden;
}
/* 左上角折现边框(主边框) */
.content-top-left::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 1px solid;
border-image: linear-gradient(277deg, rgba(255, 255, 255, 0), rgba(92, 140, 255, 1)) 1 1;
clip-path: polygon(20px 0, 100% 0, 100% 100%, 0 100%, 0 20px);
/* 切割左上角形成折现 */
}
/* 左上角折现细节(仅保留边框线条,去掉蓝色填充) */
.content-top-left::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 30px;
height: 30px;
/* 调整为纯边框线条,去掉背景填充 */
background: #EFF3F8;
border-top: 1px solid rgba(92, 140, 255, 1);
border-left: 1px solid rgba(92, 140, 255, 1);
transform: rotate(135deg) translate(-50%, -50%);
transform-origin: top left;
}
.content-top-right {
width: 270px;
height: 60px;
background: linear-gradient(90deg, #FFFFFF 0%, rgba(253, 255, 255, 0) 100%);
position: relative;
overflow: hidden;
}
/* 左上角折现边框(主边框) */
.content-top-right::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 1px solid;
border-image: linear-gradient(277deg, rgba(255, 255, 255, 0), rgba(92, 140, 255, 1)) 1 1;
clip-path: polygon(20px 0, 100% 0, 100% 100%, 0 100%, 0 20px);
/* 切割左上角形成折现 */
}
/* 左上角折现细节(仅保留边框线条,去掉蓝色填充) */
.content-top-right::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 30px;
height: 30px;
/* 调整为纯边框线条,去掉背景填充 */
background: #EFF3F8;
border-top: 1px solid rgba(92, 140, 255, 1);
border-left: 1px solid rgba(92, 140, 255, 1);
transform: rotate(135deg) translate(-50%, -50%);
transform-origin: top left;
}
&__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;
}
// &__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;
}
</style>

View File

@@ -0,0 +1,212 @@
<template>
<div class="cockpitContainer" :class="['cockpitContainer__' + size]">
<div class="container-top">
<div class="title-wrapper">
<svg-icon class="title-icon" :icon-class="icon" />
<span class="title-text">
{{ name }}
</span>
</div>
</div>
<div class="container-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: ['name', 'size', 'icon', 'nameTwo'],
data() {
return {};
},
computed: {},
methods: {},
};
</script>
<style scoped lang="scss">
.cockpitContainer {
display: inline-block;
// width: 100%;
// height: 100%;
padding: 6px;
display: flex;
flex-direction: column;
position: relative;
.container-top {
width: 746px;
height: 60px;
background: linear-gradient(90deg, #FFFFFF 50%, rgba(253, 255, 255, 0) 100%);
position: relative;
overflow: hidden;
.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;
}
}
/* 左上角折现边框(主边框) */
.container-top::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 1px solid;
border-image: linear-gradient(277deg, rgba(255, 255, 255, 0), rgba(92, 140, 255, 1)) 1 1;
clip-path: polygon(20px 0, 100% 0, 100% 100%, 0 100%, 0 20px);
/* 切割左上角形成折现 */
}
/* 左上角折现细节(仅保留边框线条,去掉蓝色填充) */
.container-top::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 30px;
height: 30px;
/* 调整为纯边框线条,去掉背景填充 */
background: #EFF3F8;
border-top: 1px solid rgba(92, 140, 255, 1);
border-left: 1px solid rgba(92, 140, 255, 1);
transform: rotate(135deg) translate(-50%, -50%);
transform-origin: top left;
}
&__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;
}
// &__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;
}
</style>

View File

@@ -0,0 +1,265 @@
<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>
</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: ['name', 'size', 'icon', 'topSize'],
data() {
return {};
},
computed: {},
methods: {},
};
</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;
}
}
&__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;
}
// &__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;
}
</style>

View File

@@ -0,0 +1,199 @@
<template>
<div class="coreItem">
<div class="item" v-for="(item, index) in itemList" :key="index">
<div class="unit">{{ item.unit }}</div>
<div class="item-content">
<div class="content-wrapper">
<div class="left">
<div class="number">{{ item.targetValue }}</div>
<div class="title">目标值</div>
</div>
<div class="line"></div>
<div class="right">
<div class="number" :style="{ color: getTargetColor(item.currentValue, item.targetValue) }">
{{ item.currentValue }}
</div>
<div class="title">实际值</div>
</div>
</div>
<div class="progress-group">
<div class="progress-container">
<div class="progress-bar" :style="{
width: item.progress + '%',
background: getTargetColor(item.currentValue, item.targetValue)
}"></div>
</div>
</div>
<div class="yield" style="display: flex;justify-content: space-between;">
<div class="progress-percent" :style="{ color: getTargetColor(item.currentValue, item.targetValue) }">完成率
</div>
<div class="progress-percent" :style="{ color: getTargetColor(item.currentValue, item.targetValue) }">
{{ item.progress }}%
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Container",
components: {},
props: ["name", "size", "icon"],
data() {
return {
progress: 90,
itemList: [
{
unit: "本月增效额·万元",
targetValue: 16,
currentValue: 14.5,
progress: 90
},
{
unit: "累计增效额·万元",
targetValue: 16,
currentValue: 15.2,
progress: 85
},
// {
// unit: "销量·万㎡",
// targetValue: 20,
// currentValue: 16,
// progress: 80
// },
// {
// unit: "双镀面板·万㎡",
// targetValue: 15,
// currentValue: 13.8,
// progress: 92
// }
]
};
},
computed: {},
methods: {
getTargetColor(currentValue, targetValue) {
return currentValue >= targetValue
? "rgba(98, 213, 180, 1)"
: "rgba(249, 164, 74, 1)";
}
},
};
</script>
<style scoped lang="scss">
.coreItem {
display: flex;
flex-direction: column;
gap: 8px;
}
.item {
width: 252px;
height: 122px;
background: #f9fcff;
padding: 12px 12px 0 12px;
box-sizing: border-box;
.unit {
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;
}
.item-content {
display: flex;
flex-direction: column;
justify-content: space-between;
height: calc(100% - 26px);
}
.content-wrapper {
display: flex;
align-items: center;
justify-content: space-around;
flex: 1;
}
.line {
width: 1px;
height: 46px;
background: linear-gradient(to bottom,
rgba(255, 0, 0, 0),
rgba(40, 203, 151, 1));
}
.left,
.right {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 2px;
flex: 1;
}
.number {
height: 22px;
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 24px;
color: rgba(103, 103, 103, 0.79);
line-height: 22px;
text-align: center;
font-style: normal;
}
.title {
height: 14px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #868687;
line-height: 14px;
text-align: center;
font-style: normal;
}
.progress-group {
display: flex;
align-items: center;
gap: 8px;
}
.progress-container {
width: 230px;
height: 10px;
background: #ECEFF7;
border-radius: 8px;
overflow: hidden;
}
.progress-bar {
height: 100%;
border-radius: 8px;
}
.progress-percent {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #868687;
line-height: 1;
}
.yield {
margin-top: 3px;
}
}
</style>

View File

@@ -0,0 +1,370 @@
<template>
<div class="coreBar">
<div class="barTop">
<div class="title">销售指标趋势</div>
<div class="legend">
<span class="legend-item target">目标</span>
<span class="legend-item real">实际</span>
</div>
<div class="button-group">
<div style="letter-spacing: 4px" class="item-button" :class="{ active: activeButton === 0 }"
@click="activeButton = 0">
单价
</div>
<div class="button-line lineOne" v-if="activeButton !== 0 && activeButton !== 1"></div>
<div style="letter-spacing: 4px" class="item-button" :class="{ active: activeButton === 1 }"
@click="activeButton = 1">
净价
</div>
<div class="button-line lineTwo" v-if="activeButton !== 1 && activeButton !== 2"></div>
<div style="letter-spacing: 4px" class="item-button" :class="{ active: activeButton === 2 }"
@click="activeButton = 2">
销量
</div>
<div class="button-line lineThree" v-if="activeButton !== 2 && activeButton !== 3"></div>
<div class="item-button" :class="{ active: activeButton === 3 }" @click="activeButton = 3">
双镀产品
</div>
</div>
</div>
<div class="lineBottom" style="height: 219px; width: 100%">
<!-- 传递当前选中的 series 数据给子组件key 确保数据更新时重新渲染 -->
<coreLineChart style="height: 219px; width: 500px" :chart-series="currentSeries"
:key="activeButton + JSON.stringify(currentSeries)" />
</div>
</div>
</template>
<script>
import coreLineChart from './coresBar.vue';
import * as echarts from 'echarts';
export default {
name: "Container",
components: { coreLineChart },
props: ["name", "size", "icon"],
data() {
return {
activeButton: 0,
itemList: [
{ unit: "单价·元/m²", targetValue: 16, currentValue: 14.5, progress: 90 },
{ unit: "净价·元/m²", targetValue: 16, currentValue: 15.2, progress: 85 },
{ unit: "销量·万m²", targetValue: 20, currentValue: 16, progress: 80 },
{ unit: "双镀面板·万m²", targetValue: 15, currentValue: 13.8, progress: 92 },
],
// 4个按钮对应的 series 数据(目标+实际两条线)
seriesMap: [
// 0: 单价(元/m²
[
{
name: '目标',
type: 'line',
stack: 'Total',
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: 'rgba(91, 230, 190, 1)', width: 2 },
itemStyle: {
color: 'rgba(91, 230, 190, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(91, 230, 190, 0.4)' },
{ offset: 1, color: 'rgba(91, 230, 190, 0)' },
]),
},
data: [16, 16.2, 15.8, 16.1, 15.9, 16] // 6-11月目标数据
},
{
name: '实际',
type: 'line',
stack: 'Total',
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: 'rgba(255, 132, 0, 1)', width: 2 },
itemStyle: {
color: 'rgba(255, 132, 0, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 132, 0, 0.4)' },
{ offset: 1, color: 'rgba(255, 132, 0, 0)' },
]),
},
data: [14.5, 14.8, 15.2, 14.6, 15, 14.7] // 6-11月实际数据
}
],
// 1: 净价(元/m²
[
{
name: '目标',
type: 'line',
stack: 'Total',
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: 'rgba(91, 230, 190, 1)', width: 2 },
itemStyle: {
color: 'rgba(91, 230, 190, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(91, 230, 190, 0.4)' },
{ offset: 1, color: 'rgba(91, 230, 190, 0)' },
]),
},
data: [16, 16.1, 15.9, 16.2, 16, 16.1]
},
{
name: '实际',
type: 'line',
stack: 'Total',
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: 'rgba(255, 132, 0, 1)', width: 2 },
itemStyle: {
color: 'rgba(255, 132, 0, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 132, 0, 0.4)' },
{ offset: 1, color: 'rgba(255, 132, 0, 0)' },
]),
},
data: [15.2, 15.5, 15.3, 15.6, 15.4, 15.5]
}
],
// 2: 销量万m²
[
{
name: '目标',
type: 'line',
stack: 'Total',
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: 'rgba(91, 230, 190, 1)', width: 2 },
itemStyle: {
color: 'rgba(91, 230, 190, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(91, 230, 190, 0.4)' },
{ offset: 1, color: 'rgba(91, 230, 190, 0)' },
]),
},
data: [20, 20.5, 19.8, 21, 20.2, 20.8]
},
{
name: '实际',
type: 'line',
stack: 'Total',
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: 'rgba(255, 132, 0, 1)', width: 2 },
itemStyle: {
color: 'rgba(255, 132, 0, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 132, 0, 0.4)' },
{ offset: 1, color: 'rgba(255, 132, 0, 0)' },
]),
},
data: [16, 16.8, 17.2, 16.5, 17, 17.5]
}
],
// 3: 双镀产品万m²
[
{
name: '目标',
type: 'line',
stack: 'Total',
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: 'rgba(91, 230, 190, 1)', width: 2 },
itemStyle: {
color: 'rgba(91, 230, 190, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(91, 230, 190, 0.4)' },
{ offset: 1, color: 'rgba(91, 230, 190, 0)' },
]),
},
data: [15, 15.2, 14.8, 15.5, 15.1, 15.3]
},
{
name: '实际',
type: 'line',
stack: 'Total',
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: 'rgba(255, 132, 0, 1)', width: 2 },
itemStyle: {
color: 'rgba(255, 132, 0, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 132, 0, 0.4)' },
{ offset: 1, color: 'rgba(255, 132, 0, 0)' },
]),
},
data: [13.8, 14.2, 14, 14.5, 14.3, 14.6]
}
]
]
};
},
computed: {
// 根据激活按钮动态返回对应 series 数据
currentSeries() {
return this.seriesMap[this.activeButton] || [];
}
}
};
</script>
<style scoped lang="scss">
/* 原有样式保持不变 */
.coreBar {
display: flex;
flex-direction: column;
padding: 12px;
.barTop {
display: flex;
gap: 16px;
align-items: center;
.title {
height: 18px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 18px;
color: #000000;
line-height: 18px;
letter-spacing: 1px;
text-align: left;
}
.legend {
display: flex;
gap: 24px;
margin-left: 10px;
.legend-item {
position: relative;
font-family: PingFangSC, PingFang SC;
font-size: 14px;
color: rgba(0, 0, 0, 0.8);
&::before {
content: "";
position: absolute;
left: -13px;
top: 50%;
transform: translateY(-50%);
width: 6px;
height: 6px;
border-radius: 50%;
}
&::after {
content: "";
position: absolute;
left: -18px;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 2px;
}
}
.target::before,
.target::after {
background-color: rgba(91, 230, 190, 1);
}
.real::before,
.real::after {
background-color: rgba(255, 132, 0, 1);
}
}
.button-group {
display: flex;
position: relative;
gap: 2px;
margin-left: 30px;
width: 230px;
align-items: center;
height: 24px;
background: #ecf4fe;
border-radius: 12px;
.button-line {
position: absolute;
width: 1px;
height: 14px;
border: 1px solid rgba(11, 88, 255, 0.25);
}
.lineOne {
top: 5px;
left: 54px;
}
.lineTwo {
top: 5px;
left: 108px;
}
.lineThree {
top: 5px;
left: 162px;
}
.item-button {
cursor: pointer;
width: 54px;
height: 24px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #0b58ff;
line-height: 24px;
text-align: center;
}
.item-button.active {
background: #3071ff;
border-radius: 12px;
color: #ffffff;
font-weight: 500;
}
}
}
}
</style>

View File

@@ -0,0 +1,363 @@
<template>
<div style="flex: 1">
<Container name="采购重点指标" nameTwo="存货重点指标" icon="cockpitItemIcon" size="bottomBasic">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="bottom-left-content" style="display: flex;gap: 9px;padding: 14px 16px;">
<coreBottomLeftItem>
</coreBottomLeftItem>
<div class="content-right" style="background: #F9FCFF;padding: 15px 12px;">
<base-table style="height: 180px;width: 260px;" :page="1" :limit="10" :show-index="true" :beilv="1"
:tableConfig="tableProps" :table-data="maintenanceTasks" />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from './bottomLeftContainer.vue'
// import * as echarts from 'echarts'
import coreBottomLeftItem from './core-bottom-leftItem.vue'
import baseTable from './baseTable.vue'
export default {
name: 'ProductionStatus',
components: { Container, coreBottomLeftItem, baseTable },
// mixins: [resize],
props: {
leftEqInfoData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
productionOverviewVo: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
type: Object,
default: () => ({})
}
},
data() {
return {
maintenanceTasks: [
{ id: 1, eqName: '纯碱', taskName: '1313,252', },
{ id: 2, eqName: '硅砂', taskName: '14,252', },
{ id: 2, eqName: '白云石', taskName: '23,252', },
{ id: 2, eqName: '石灰石', taskName: '34,421', },
{ id: 2, eqName: '氧化铝', taskName: '1,251.34', },
{ id: 2, eqName: '氢氧化铝', taskName: '14,252', },
// { id: 2, eqName: '螺杆挤出', taskName: '例行维护', },
// { id: 2, eqName: '螺杆挤出', taskName: '例行维护', },
// { id: 2, eqName: '螺杆挤出', taskName: '例行维护', },
// { id: 2, eqName: '螺杆挤出', taskName: '例行维护', },
// { id: 2, eqName: '螺杆挤出', taskName: '例行维护', },
],
tableProps: [
// { prop: 'id', label: '序号', width: 50, align: 'center' },
{ prop: 'eqName', label: '物料', align: 'left' },
{ prop: 'taskName', label: '库存/吨', align: 'left' },
]
}
},
watch: {
productionOverviewVo: {
handler(newValue, oldValue) {
this.updateChart()
},
deep: true // 若对象内属性变化需触发,需加 deep: true
}
},
mounted() {
// 初始化图表(若需展示图表,需在模板中添加对应 DOM
// this.$nextTick(() => this.updateChart())
},
beforeDestroy() {
// 销毁图表,避免内存泄漏
if (this.chart) {
this.chart.dispose()
this.chart = null
}
},
methods: {
updateChart() {
// 注意:原代码中图表依赖 id 为 "productionStatusChart" 的 DOM需在模板中补充否则会报错
// 示例:在 Container 内添加 <div id="productionStatusChart" style="height: 200px;"></div>
if (!document.getElementById('productionStatusChart')) return
if (this.chart) this.chart.dispose()
this.chart = echarts.init(document.getElementById('productionStatusChart'))
const data = [
this.productionOverviewVo.input || 0,
this.productionOverviewVo.output || 0,
this.productionOverviewVo.ng || 0,
this.productionOverviewVo.lowValue || 0,
this.productionOverviewVo.scrap || 0,
this.productionOverviewVo.inProcess || 0,
this.productionOverviewVo.engineer || 0
]
const option = {
type: 'bar',
grid: { left: 51, right: 40, top: 50, bottom: 45 },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
className: 'production-status-chart-tooltip'
},
xAxis: {
type: 'category',
offset: 8,
data: ['投入', '产出', '待判', '低价值', '报废', '在制', '实验片'],
axisTick: { show: false },
axisLine: { show: true, onZero: false, lineStyle: { color: '#00E8FF' } },
axisLabel: {
color: 'rgba(255,255,255,0.7)',
fontSize: 12,
interval: 0,
width: 38,
overflow: 'break'
}
},
yAxis: {
type: 'value',
name: '单位/片',
nameTextStyle: { color: 'rgba(255,255,255,0.7)', fontSize: 14, align: 'left' },
min: () => 0,
max: (value) => Math.ceil(value.max),
scale: true,
axisTick: { show: false },
axisLabel: { color: 'rgba(255,255,255,0.7)', fontSize: 12 },
splitLine: { lineStyle: { color: 'RGBA(24, 88, 100, 0.6)', type: 'dashed' } },
axisLine: { show: true, lineStyle: { color: '#00E8FF' } }
},
series: [
{
type: 'pictorialBar',
label: { show: true, position: 'top', distance: -3, color: '#89CDFF', fontSize: 11 },
symbolSize: [20, 8],
symbolOffset: [0, 5],
z: 20,
itemStyle: {
borderColor: '#3588C7',
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'RGBA(22, 89, 98, 1)' },
{ offset: 1, color: '#3588C7' }
])
},
data: data
},
{
type: 'bar',
barWidth: 20,
itemStyle: {
borderWidth: 1,
borderColor: '#3588C7',
opacity: 0.8,
color: {
x: 0, y: 0, x2: 0, y2: 1,
type: 'linear',
global: false,
colorStops: [
{ offset: 0, color: 'rgba(73,178,255,0)' },
{ offset: 0.5, color: 'rgba(0, 232, 255, .5)' },
{ offset: 1, color: 'rgba(0, 232, 255, 1)' }
]
}
},
tooltip: { show: false },
data: data
},
{
type: 'pictorialBar',
symbolSize: [20, 8],
symbolOffset: [0, -4],
z: 12,
symbolPosition: 'end',
itemStyle: {
borderColor: 'rgba(0, 232, 255, 1)',
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'RGBA(22, 89, 98, 1)' },
{ offset: 1, color: '#3588C7' }
])
},
tooltip: { show: false },
data: data
}
]
}
this.chart.setOption(option)
}
}
}
</script>
<style lang='scss' scoped>
/* 3. 核心:滚动容器样式(固定高度+溢出滚动+隐藏滚动条) */
.scroll-container {
/* 1. 固定容器高度根据页面布局调整示例300px超出则滚动 */
max-height: 210px;
/* 2. 溢出滚动:内容超出高度时显示滚动功能 */
overflow-y: auto;
/* 3. 隐藏横向滚动条(防止设备名过长导致横向滚动) */
overflow-x: hidden;
/* 4. 内边距:与标题栏和容器边缘对齐 */
padding: 10px 0;
/* 5. 隐藏滚动条(兼容主流浏览器) */
/* Chrome/Safari */
&::-webkit-scrollbar {
display: none;
}
/* Firefox */
scrollbar-width: none;
/* IE/Edge */
-ms-overflow-style: none;
}
/* 设备项样式优化:增加间距,避免拥挤 */
.proBarInfo {
display: flex;
flex-direction: column;
padding: 8px 27px;
/* 调整内边距,优化排版 */
margin-bottom: 10px;
/* 设备项之间的垂直间距 */
}
/* 原有样式保留,优化细节 */
.proBarInfoEqInfo {
display: flex;
justify-content: space-between;
align-items: center;
/* 垂直居中,避免序号/文字错位 */
}
.slot {
width: 21px;
height: 23px;
background: rgba(0, 106, 205, 0.22);
backdrop-filter: blur(1.5px);
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #68B5FF;
line-height: 23px;
/* 垂直居中文字 */
text-align: center;
font-style: normal;
}
.eq-name {
margin-left: 8px;
/* 增加与序号的间距 */
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
letter-spacing: 1px;
text-align: left;
font-style: normal;
}
.eqStatus {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.splitLine {
width: 1px;
height: 14px;
border: 1px solid #ADADAD;
margin: 0 8px;
/* 优化分割线间距 */
}
.yield {
height: 18px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #00FFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.proBarInfoEqInfoLeft {
display: flex;
align-items: center;
/* 序号和设备名垂直居中 */
}
.proBarInfoEqInfoRight {
display: flex;
align-items: center;
/* 状态/分割线/百分比垂直居中 */
}
.proBarWrapper {
position: relative;
height: 10px;
margin-top: 6px;
/* 进度条与上方信息的间距 */
border-radius: 5px;
/* 进度条圆角,优化视觉 */
overflow: hidden;
}
.proBarLine {
width: 100%;
height: 100%;
background: linear-gradient(65deg, rgba(82, 82, 82, 0) 0%, #ACACAC 100%);
opacity: 0.2;
}
.proBarLineTop {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: linear-gradient(65deg, rgba(53, 223, 247, 0) 0%, rgba(54, 220, 246, 0.92) 92%, #36F6E5 100%, #37ACF5 100%);
border-radius: 5px;
transition: width 0.3s ease;
/* 进度变化时添加过渡动画,更流畅 */
}
/* 图表相关样式保留 */
.chartImgBottom {
position: absolute;
bottom: 45px;
left: 58px;
}
.line {
display: inline-block;
position: absolute;
left: 57px;
bottom: 42px;
width: 1px;
height: 20px;
background-color: #00E8FF;
}
</style>
<style>
/* 全局 tooltip 样式(不使用 scoped确保生效 */
.production-status-chart-tooltip {
background: #0a2b4f77 !important;
border: none !important;
backdrop-filter: blur(12px);
}
.production-status-chart-tooltip * {
color: #fff !important;
}
</style>

View File

@@ -0,0 +1,347 @@
<template>
<div style="flex: 1">
<Container name="销售重点指标" icon="cockpitItemIcon" size="topBasic" topSize="basic">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex;flex-direction: column; width: 100%;">
<!-- 2. .top 保持 flex无需固定高度自动跟随子元素拉伸 -->
<div class="top" style="display: flex; width: 100%;">
<top-item />
</div>
<div class="bottom"
style="display: flex; width: 100%;margin-top: 8px;background-color: rgba(249, 252, 255, 1);">
<!-- <top-item /> -->
<coreBottomBar />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
// import * as echarts from 'echarts'
import topItem from './top-item.vue'
import coreBottomBar from './coreBottomBar.vue'
export default {
name: 'ProductionStatus',
components: { Container, topItem, coreBottomBar },
// mixins: [resize],
props: {
leftEqInfoData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
productionOverviewVo: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
type: Object,
default: () => ({})
}
},
data() {
return {
chart: null
}
},
watch: {
productionOverviewVo: {
handler(newValue, oldValue) {
this.updateChart()
},
deep: true // 若对象内属性变化需触发,需加 deep: true
}
},
mounted() {
// 初始化图表(若需展示图表,需在模板中添加对应 DOM
// this.$nextTick(() => this.updateChart())
},
beforeDestroy() {
// 销毁图表,避免内存泄漏
if (this.chart) {
this.chart.dispose()
this.chart = null
}
},
methods: {
updateChart() {
// 注意:原代码中图表依赖 id 为 "productionStatusChart" 的 DOM需在模板中补充否则会报错
// 示例:在 Container 内添加 <div id="productionStatusChart" style="height: 200px;"></div>
if (!document.getElementById('productionStatusChart')) return
if (this.chart) this.chart.dispose()
this.chart = echarts.init(document.getElementById('productionStatusChart'))
const data = [
this.productionOverviewVo.input || 0,
this.productionOverviewVo.output || 0,
this.productionOverviewVo.ng || 0,
this.productionOverviewVo.lowValue || 0,
this.productionOverviewVo.scrap || 0,
this.productionOverviewVo.inProcess || 0,
this.productionOverviewVo.engineer || 0
]
const option = {
type: 'bar',
grid: { left: 51, right: 40, top: 50, bottom: 45 },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
className: 'production-status-chart-tooltip'
},
xAxis: {
type: 'category',
offset: 8,
data: ['投入', '产出', '待判', '低价值', '报废', '在制', '实验片'],
axisTick: { show: false },
axisLine: { show: true, onZero: false, lineStyle: { color: '#00E8FF' } },
axisLabel: {
color: 'rgba(255,255,255,0.7)',
fontSize: 12,
interval: 0,
width: 38,
overflow: 'break'
}
},
yAxis: {
type: 'value',
name: '单位/片',
nameTextStyle: { color: 'rgba(255,255,255,0.7)', fontSize: 14, align: 'left' },
min: () => 0,
max: (value) => Math.ceil(value.max),
scale: true,
axisTick: { show: false },
axisLabel: { color: 'rgba(255,255,255,0.7)', fontSize: 12 },
splitLine: { lineStyle: { color: 'RGBA(24, 88, 100, 0.6)', type: 'dashed' } },
axisLine: { show: true, lineStyle: { color: '#00E8FF' } }
},
series: [
{
type: 'pictorialBar',
label: { show: true, position: 'top', distance: -3, color: '#89CDFF', fontSize: 11 },
symbolSize: [20, 8],
symbolOffset: [0, 5],
z: 20,
itemStyle: {
borderColor: '#3588C7',
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'RGBA(22, 89, 98, 1)' },
{ offset: 1, color: '#3588C7' }
])
},
data: data
},
{
type: 'bar',
barWidth: 20,
itemStyle: {
borderWidth: 1,
borderColor: '#3588C7',
opacity: 0.8,
color: {
x: 0, y: 0, x2: 0, y2: 1,
type: 'linear',
global: false,
colorStops: [
{ offset: 0, color: 'rgba(73,178,255,0)' },
{ offset: 0.5, color: 'rgba(0, 232, 255, .5)' },
{ offset: 1, color: 'rgba(0, 232, 255, 1)' }
]
}
},
tooltip: { show: false },
data: data
},
{
type: 'pictorialBar',
symbolSize: [20, 8],
symbolOffset: [0, -4],
z: 12,
symbolPosition: 'end',
itemStyle: {
borderColor: 'rgba(0, 232, 255, 1)',
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'RGBA(22, 89, 98, 1)' },
{ offset: 1, color: '#3588C7' }
])
},
tooltip: { show: false },
data: data
}
]
}
this.chart.setOption(option)
}
}
}
</script>
<style lang='scss' scoped>
/* 3. 核心:滚动容器样式(固定高度+溢出滚动+隐藏滚动条) */
.scroll-container {
/* 1. 固定容器高度根据页面布局调整示例300px超出则滚动 */
max-height: 210px;
/* 2. 溢出滚动:内容超出高度时显示滚动功能 */
overflow-y: auto;
/* 3. 隐藏横向滚动条(防止设备名过长导致横向滚动) */
overflow-x: hidden;
/* 4. 内边距:与标题栏和容器边缘对齐 */
padding: 10px 0;
/* 5. 隐藏滚动条(兼容主流浏览器) */
/* Chrome/Safari */
&::-webkit-scrollbar {
display: none;
}
/* Firefox */
scrollbar-width: none;
/* IE/Edge */
-ms-overflow-style: none;
}
/* 设备项样式优化:增加间距,避免拥挤 */
.proBarInfo {
display: flex;
flex-direction: column;
padding: 8px 27px;
/* 调整内边距,优化排版 */
margin-bottom: 10px;
/* 设备项之间的垂直间距 */
}
/* 原有样式保留,优化细节 */
.proBarInfoEqInfo {
display: flex;
justify-content: space-between;
align-items: center;
/* 垂直居中,避免序号/文字错位 */
}
.slot {
width: 21px;
height: 23px;
background: rgba(0, 106, 205, 0.22);
backdrop-filter: blur(1.5px);
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #68B5FF;
line-height: 23px;
/* 垂直居中文字 */
text-align: center;
font-style: normal;
}
.eq-name {
margin-left: 8px;
/* 增加与序号的间距 */
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
letter-spacing: 1px;
text-align: left;
font-style: normal;
}
.eqStatus {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.splitLine {
width: 1px;
height: 14px;
border: 1px solid #ADADAD;
margin: 0 8px;
/* 优化分割线间距 */
}
.yield {
height: 18px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #00FFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.proBarInfoEqInfoLeft {
display: flex;
align-items: center;
/* 序号和设备名垂直居中 */
}
.proBarInfoEqInfoRight {
display: flex;
align-items: center;
/* 状态/分割线/百分比垂直居中 */
}
.proBarWrapper {
position: relative;
height: 10px;
margin-top: 6px;
/* 进度条与上方信息的间距 */
border-radius: 5px;
/* 进度条圆角,优化视觉 */
overflow: hidden;
}
.proBarLine {
width: 100%;
height: 100%;
background: linear-gradient(65deg, rgba(82, 82, 82, 0) 0%, #ACACAC 100%);
opacity: 0.2;
}
.proBarLineTop {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: linear-gradient(65deg, rgba(53, 223, 247, 0) 0%, rgba(54, 220, 246, 0.92) 92%, #36F6E5 100%, #37ACF5 100%);
border-radius: 5px;
transition: width 0.3s ease;
/* 进度变化时添加过渡动画,更流畅 */
}
/* 图表相关样式保留 */
.chartImgBottom {
position: absolute;
bottom: 45px;
left: 58px;
}
.line {
display: inline-block;
position: absolute;
left: 57px;
bottom: 42px;
width: 1px;
height: 20px;
background-color: #00E8FF;
}
</style>
<style>
/* 全局 tooltip 样式(不使用 scoped确保生效 */
.production-status-chart-tooltip {
background: #0a2b4f77 !important;
border: none !important;
backdrop-filter: blur(12px);
}
.production-status-chart-tooltip * {
color: #fff !important;
}
</style>

View File

@@ -0,0 +1,112 @@
<template>
<div ref="cockpitEffChip" id="coreLineChart" style="height: 219px; width: 100%;"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
name: 'Container',
components: {},
// 接收父组件传递的 series 数据
props: {
chartSeries: {
type: Array,
required: true,
default: () => [] // 默认空数组,避免报错
}
},
data() {
return {
myChart: null // 存储图表实例,方便后续操作
};
},
mounted() {
this.$nextTick(() => {
this.initData();
});
},
watch: {
// 监听 series 数据变化,实时更新图表
chartSeries: {
handler() {
this.updateChart();
},
deep: true // 深度监听数组内元素变化
}
},
methods: {
initData() {
const chartDom = this.$refs.cockpitEffChip;
if (!chartDom) {
console.error('图表容器未找到!');
return;
}
this.myChart = echarts.init(chartDom);
this.updateChart(); // 初始化时渲染图表
// 监听窗口缩放
window.addEventListener('resize', () => {
this.myChart && this.myChart.resize();
});
// 组件销毁时清理
this.$once('hook:destroyed', () => {
window.removeEventListener('resize', () => {
this.myChart && this.myChart.resize();
});
this.myChart && this.myChart.dispose();
});
},
// 单独提取更新图表的方法,方便复用
updateChart() {
if (!this.myChart) return;
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: { backgroundColor: '#6a7985' }
}
},
grid: { top: 10, bottom: 20, right: 25, left: 30 },
xAxis: [
{
type: 'category',
boundaryGap: false,
axisTick: { show: false },
axisLine: {
show: true,
onZero: false,
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
},
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
interval: 0,
width: 38,
overflow: 'break'
},
data: ['6月', '7月', '8月', '9月', '10月', '11月']
}
],
yAxis: {
type: 'value',
nameTextStyle: { color: 'rgba(0, 0, 0, 0.45)', fontSize: 14, align: 'left' },
min: () => 0,
max: (value) => Math.ceil(value.max),
scale: true,
axisTick: { show: false },
axisLabel: { color: 'rgba(0, 0, 0, 0.45)', fontSize: 12 },
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
axisLine: { show: true, lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } }
},
series: this.chartSeries // 使用父组件传递的 series 数据
};
this.myChart.setOption(option, true); // 第二个参数 true 表示替换现有配置
}
}
};
</script>

View File

@@ -0,0 +1,191 @@
<template>
<div class="coreItem">
<!-- 动态生成每个 item -->
<div class="item" v-for="(item, index) in itemList" :key="index">
<div class="unit">{{ item.unit }}</div>
<div class="item-content">
<!-- 左右内容容器 -->
<div class="content-wrapper">
<div class="left">
<div class="number">{{ item.targetValue }}</div>
<div class="title">上月</div>
</div>
<div class="line"></div>
<div class="right">
<!-- 本月数值动态绑定颜色 -->
<div class="number" :style="{ color: getColor(item.currentValue, item.targetValue) }">
{{ item.currentValue }}
</div>
<div class="title">本月</div>
</div>
</div>
<div class="line"></div>
<div class="right">
<!-- 环比额计算差值并动态绑定颜色 -->
<div class="number" :style="{ color: getColor(item.currentValue, item.targetValue) }">
{{ get环比额(item.currentValue, item.targetValue) }}
</div>
<div class="title">环比额</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Container",
components: {},
props: ["name", "size", "icon"],
data() {
return {
itemList: [
{
unit: "管理费用·万元",
targetValue: 16, // 上月值
currentValue: 14.5, // 本月值(小于上月,应显示橙色)
},
{
unit: "销售费用·万元",
targetValue: 16,
currentValue: 17, // 大于上月,应显示绿色
},
{
unit: "财务费用·万元",
targetValue: 16,
currentValue: 16, // 等于上月,应显示绿色
},
]
};
},
methods: {
// 颜色判断:本月 >= 上月 绿色,否则 橙色
getColor(current, target) {
return current >= target
? "rgba(98, 213, 180, 1)"
: "rgba(249, 164, 74, 1)";
},
// 计算环比额(本月 - 上月),保留一位小数
get环比额(current, target) {
const diff = current - target;
// 正数加"+"号,负数和零保持原样
return diff > 0 ? `${diff.toFixed(1)}` : diff.toFixed(1);
}
},
};
</script>
<style scoped lang="scss">
.coreItem {
display: flex;
gap: 8px;
}
.item {
width: 232px;
height: 228px;
background: #f9fcff;
padding: 12px 0px 17px 12px;
box-sizing: border-box;
.unit {
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;
}
.item-content {
display: flex;
flex-direction: column;
justify-content: space-between;
height: calc(100% - 26px);
}
.content-wrapper {
display: flex;
flex-direction: column;
gap: 10px;
}
.line {
width: 149px;
height: 1px;
background: linear-gradient(to left,
rgba(255, 0, 0, 0),
#cbcbcb);
}
.left,
.right {
margin-top: 11px;
display: flex;
flex-direction: column;
gap: 2px;
width: 100%;
}
.number {
height: 22px;
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 24px;
color: rgba(103, 103, 103, 0.79);
line-height: 22px;
text-align: left;
font-style: normal;
}
.title {
height: 14px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #868687;
line-height: 14px;
text-align: left;
font-style: normal;
}
.progress-group {
display: flex;
align-items: center;
gap: 8px;
margin-top: 15px;
}
.progress-container {
width: 138px;
height: 10px;
background: #ECEFF7;
border-radius: 8px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: #28CB97;
border-radius: 8px;
opacity: 0.6;
}
.progress-percent {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #868687;
line-height: 1;
}
.yield {
width: 138px;
margin-top: 3px;
}
}
</style>

View File

@@ -0,0 +1,237 @@
<template>
<div style="flex: 1">
<Container name="产销率总览·万m²" icon="cockpitItemIcon" size="psiBasicBg" topSize="psiTopTitleBasic">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex;flex-direction: column; width: 100%;height: 404px;">
<!-- 2. .top 保持 flex无需固定高度自动跟随子元素拉伸 -->
<div class="top" style="display: flex; width: 100%;">
<psr-item :itemList="targetItemList" />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
// import * as echarts from 'echarts'
import psrItem from './psr-item.vue'
export default {
name: 'ProductionStatus',
components: { Container, psrItem },
// mixins: [resize],
props: {
productSaleData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
},
data() {
return {
chart: null,
parentItemList: [
{ name: "销量", targetValue: 0, value: 0, proportion: 0 },
{ name: "产量", targetValue: 0, value: 0, proportion: 0 },
{ name: "累计完成产销率", targetValue: 0, value: 0, proportion: 0 },
]
}
},
computed: {
targetItemList() {
// 基于 parentItemList 的名称,匹配 productAndSaleData 中对应名称的数据
return this.parentItemList.map(parentItem => {
// 按名称精准匹配name 完全一致才关联)
const productItem = this.productSaleData.find(
item => item.name === parentItem.name
) || {};
// 优先使用匹配到的 productItem 数据,缺失则用 parentItem 的默认值0
return {
name: parentItem.name, // 保留原名称(销量、产量等)
targetValue: productItem.targetValue ?? parentItem.targetValue ?? 0,
value: productItem.value ?? parentItem.value ?? 0,
proportion: productItem.proportion ?? parentItem.proportion ?? 0,
completed: productItem.completed ?? parentItem.completed ?? 1,
route: parentItem.route // 路由沿用 parentItem 的配置
};
});
}
},
mounted() {
// 初始化图表(若需展示图表,需在模板中添加对应 DOM
// this.$nextTick(() => this.updateChart())
},
methods: {
}
}
</script>
<style lang='scss' scoped>
/* 3. 核心:滚动容器样式(固定高度+溢出滚动+隐藏滚动条) */
.scroll-container {
/* 1. 固定容器高度根据页面布局调整示例300px超出则滚动 */
max-height: 210px;
/* 2. 溢出滚动:内容超出高度时显示滚动功能 */
overflow-y: auto;
/* 3. 隐藏横向滚动条(防止设备名过长导致横向滚动) */
overflow-x: hidden;
/* 4. 内边距:与标题栏和容器边缘对齐 */
padding: 10px 0;
/* 5. 隐藏滚动条(兼容主流浏览器) */
/* Chrome/Safari */
&::-webkit-scrollbar {
display: none;
}
/* Firefox */
scrollbar-width: none;
/* IE/Edge */
-ms-overflow-style: none;
}
/* 设备项样式优化:增加间距,避免拥挤 */
.proBarInfo {
display: flex;
flex-direction: column;
padding: 8px 27px;
/* 调整内边距,优化排版 */
margin-bottom: 10px;
/* 设备项之间的垂直间距 */
}
/* 原有样式保留,优化细节 */
.proBarInfoEqInfo {
display: flex;
justify-content: space-between;
align-items: center;
/* 垂直居中,避免序号/文字错位 */
}
.slot {
width: 21px;
height: 23px;
background: rgba(0, 106, 205, 0.22);
backdrop-filter: blur(1.5px);
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #68B5FF;
line-height: 23px;
/* 垂直居中文字 */
text-align: center;
font-style: normal;
}
.eq-name {
margin-left: 8px;
/* 增加与序号的间距 */
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
letter-spacing: 1px;
text-align: left;
font-style: normal;
}
.eqStatus {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.splitLine {
width: 1px;
height: 14px;
border: 1px solid #ADADAD;
margin: 0 8px;
/* 优化分割线间距 */
}
.yield {
height: 18px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #00FFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.proBarInfoEqInfoLeft {
display: flex;
align-items: center;
/* 序号和设备名垂直居中 */
}
.proBarInfoEqInfoRight {
display: flex;
align-items: center;
/* 状态/分割线/百分比垂直居中 */
}
.proBarWrapper {
position: relative;
height: 10px;
margin-top: 6px;
/* 进度条与上方信息的间距 */
border-radius: 5px;
/* 进度条圆角,优化视觉 */
overflow: hidden;
}
.proBarLine {
width: 100%;
height: 100%;
background: linear-gradient(65deg, rgba(82, 82, 82, 0) 0%, #ACACAC 100%);
opacity: 0.2;
}
.proBarLineTop {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: linear-gradient(65deg, rgba(53, 223, 247, 0) 0%, rgba(54, 220, 246, 0.92) 92%, #36F6E5 100%, #37ACF5 100%);
border-radius: 5px;
transition: width 0.3s ease;
/* 进度变化时添加过渡动画,更流畅 */
}
/* 图表相关样式保留 */
.chartImgBottom {
position: absolute;
bottom: 45px;
left: 58px;
}
.line {
display: inline-block;
position: absolute;
left: 57px;
bottom: 42px;
width: 1px;
height: 20px;
background-color: #00E8FF;
}
</style>
<style>
/* 全局 tooltip 样式(不使用 scoped确保生效 */
.production-status-chart-tooltip {
background: #0a2b4f77 !important;
border: none !important;
backdrop-filter: blur(12px);
}
.production-status-chart-tooltip * {
color: #fff !important;
}
</style>

View File

@@ -0,0 +1,365 @@
<template>
<div style="flex: 1">
<!-- 传入点击切换的状态到Container组件 -->
<Container name="财务重点指标" nameTwo="费用重点指标" icon="cockpitItemIcon" size="topBasic" @switchTab="handleTabSwitch">
<div class="bottom-left-content" style="display: flex;gap: 9px;padding: 14px 16px;flex-direction: column;">
<!-- 根据activeTab状态切换显示采购/存货内容 -->
<template v-if="activeTab === 'purchase'">
<!-- 采购重点指标对应的内容 -->
<coreBottomLeftItem></coreBottomLeftItem>
<div class="bottom"
style="display: flex; width: 100%;margin-top: 8px;background-color: rgba(249, 252, 255, 1);">
<coreBottomBar></coreBottomBar>
</div>
</template>
<template v-else-if="activeTab === 'inventory'">
<!-- 存货重点指标对应的内容 -->
<costItem></costItem>
<div class="bottom"
style="display: flex; width: 100%;margin-top: 8px;background-color: rgba(249, 252, 255, 1);">
<CostsBottomBar>
</CostsBottomBar>
</div>
</template>
<!-- 图表容器根据状态控制显示若需展示图表 -->
<!-- <div id="productionStatusChart" style="height: 200px; margin-top: 12px;" v-if="showChart"></div> -->
</div>
</Container>
</div>
</template>
<script>
import Container from './financeCostsContainer.vue'
import * as echarts from 'echarts'
import coreBottomLeftItem from './purchase-Item.vue'
import coreBottomBar from './financeCostsBottomBar.vue'
import costItem from './cost-Item.vue'
import CostsBottomBar from './CostsBottomBar.vue'
export default {
name: 'ProductionStatus',
components: { Container, coreBottomLeftItem, coreBottomBar, costItem, CostsBottomBar },
props: {
leftEqInfoData: {
type: Array,
default: () => []
},
productionOverviewVo: {
type: Object,
default: () => ({})
}
},
data() {
return {
activeTab: 'purchase', // 激活的标签purchase=采购inventory=存货
showChart: true, // 控制图表是否显示
chart: null // 图表实例
}
},
watch: {
// 切换标签时更新图表
activeTab(newVal) {
this.$nextTick(() => this.updateChart())
},
productionOverviewVo: {
handler() {
this.updateChart()
},
deep: true
}
},
mounted() {
// 初始化图表
this.$nextTick(() => this.updateChart())
},
beforeDestroy() {
// 销毁图表,避免内存泄漏
if (this.chart) {
this.chart.dispose()
this.chart = null
}
},
methods: {
// 处理标题点击切换标签
handleTabSwitch(tabType) {
this.activeTab = tabType // tabType由Container组件传递'purchase'或'inventory'
this.showChart = true // 切换时默认显示图表(可根据需求调整)
},
// 更新图表内容
updateChart() {
const chartDom = document.getElementById('productionStatusChart')
if (!chartDom) return
// 销毁已有图表实例
if (this.chart) this.chart.dispose()
this.chart = echarts.init(chartDom)
// 根据当前激活的标签,设置对应图表数据(示例:可根据实际需求修改)
const data = this.activeTab === 'purchase'
? [
this.productionOverviewVo.purchaseInput || 0,
this.productionOverviewVo.purchaseOutput || 0,
this.productionOverviewVo.purchaseNg || 0,
0, 0, 0, 0 // 补充默认值,避免图表报错
]
: [
this.productionOverviewVo.inventoryInput || 0,
this.productionOverviewVo.inventoryOutput || 0,
this.productionOverviewVo.inventoryNg || 0,
0, 0, 0, 0
]
const option = {
type: 'bar',
grid: { left: 51, right: 40, top: 50, bottom: 45 },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
className: 'production-status-chart-tooltip'
},
xAxis: {
type: 'category',
offset: 8,
data: this.activeTab === 'purchase'
? ['采购投入', '采购产出', '采购待判', '低价值', '报废', '在制', '实验片']
: ['存货投入', '存货产出', '存货待判', '低价值', '报废', '在制', '实验片'],
axisTick: { show: false },
axisLine: { show: true, onZero: false, lineStyle: { color: '#00E8FF' } },
axisLabel: {
color: 'rgba(255,255,255,0.7)',
fontSize: 12,
interval: 0,
width: 38,
overflow: 'break'
}
},
yAxis: {
type: 'value',
name: '单位/片',
nameTextStyle: { color: 'rgba(255,255,255,0.7)', fontSize: 14, align: 'left' },
min: 0,
max: (value) => Math.ceil(value.max),
scale: true,
axisTick: { show: false },
axisLabel: { color: 'rgba(255,255,255,0.7)', fontSize: 12 },
splitLine: { lineStyle: { color: 'RGBA(24, 88, 100, 0.6)', type: 'dashed' } },
axisLine: { show: true, lineStyle: { color: '#00E8FF' } }
},
series: [
{
type: 'pictorialBar',
label: { show: true, position: 'top', distance: -3, color: '#89CDFF', fontSize: 11 },
symbolSize: [20, 8],
symbolOffset: [0, 5],
z: 20,
itemStyle: {
borderColor: '#3588C7',
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'RGBA(22, 89, 98, 1)' },
{ offset: 1, color: '#3588C7' }
])
},
data: data
},
{
type: 'bar',
barWidth: 20,
itemStyle: {
borderWidth: 1,
borderColor: '#3588C7',
opacity: 0.8,
color: {
x: 0, y: 0, x2: 0, y2: 1,
type: 'linear',
global: false,
colorStops: [
{ offset: 0, color: 'rgba(73,178,255,0)' },
{ offset: 0.5, color: 'rgba(0, 232, 255, .5)' },
{ offset: 1, color: 'rgba(0, 232, 255, 1)' }
]
}
},
tooltip: { show: false },
data: data
},
{
type: 'pictorialBar',
symbolSize: [20, 8],
symbolOffset: [0, -4],
z: 12,
symbolPosition: 'end',
itemStyle: {
borderColor: 'rgba(0, 232, 255, 1)',
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'RGBA(22, 89, 98, 1)' },
{ offset: 1, color: '#3588C7' }
])
},
tooltip: { show: false },
data: data
}
]
}
this.chart.setOption(option)
// 监听窗口 resize自适应图表
window.addEventListener('resize', this.chart.resize)
}
}
}
</script>
<style lang='scss' scoped>
/* 原有样式保留,新增内容容器样式 */
.bottom-left-content {
width: 100%;
height: 100%;
overflow: hidden;
}
/* 其他原有样式... */
.scroll-container {
max-height: 210px;
overflow-y: auto;
overflow-x: hidden;
padding: 10px 0;
&::-webkit-scrollbar {
display: none;
}
scrollbar-width: none;
-ms-overflow-style: none;
}
.proBarInfo {
display: flex;
flex-direction: column;
padding: 8px 27px;
margin-bottom: 10px;
}
.proBarInfoEqInfo {
display: flex;
justify-content: space-between;
align-items: center;
}
.slot {
width: 21px;
height: 23px;
background: rgba(0, 106, 205, 0.22);
backdrop-filter: blur(1.5px);
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #68B5FF;
line-height: 23px;
text-align: center;
font-style: normal;
}
.eq-name {
margin-left: 8px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
letter-spacing: 1px;
text-align: left;
font-style: normal;
}
.eqStatus {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.splitLine {
width: 1px;
height: 14px;
border: 1px solid #ADADAD;
margin: 0 8px;
}
.yield {
height: 18px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #00FFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.proBarInfoEqInfoLeft {
display: flex;
align-items: center;
}
.proBarInfoEqInfoRight {
display: flex;
align-items: center;
}
.proBarWrapper {
position: relative;
height: 10px;
margin-top: 6px;
border-radius: 5px;
overflow: hidden;
}
.proBarLine {
width: 100%;
height: 100%;
background: linear-gradient(65deg, rgba(82, 82, 82, 0) 0%, #ACACAC 100%);
opacity: 0.2;
}
.proBarLineTop {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: linear-gradient(65deg, rgba(53, 223, 247, 0) 0%, rgba(54, 220, 246, 0.92) 92%, #36F6E5 100%, #37ACF5 100%);
border-radius: 5px;
transition: width 0.3s ease;
}
.chartImgBottom {
position: absolute;
bottom: 45px;
left: 58px;
}
.line {
display: inline-block;
position: absolute;
left: 57px;
bottom: 42px;
width: 1px;
height: 20px;
background-color: #00E8FF;
}
</style>
<style>
.production-status-chart-tooltip {
background: #0a2b4f77 !important;
border: none !important;
backdrop-filter: blur(12px);
}
.production-status-chart-tooltip * {
color: #fff !important;
}
</style>

View File

@@ -0,0 +1,366 @@
<template>
<div class="coreBar">
<div class="barTop">
<div class="barTop-left" style="display: flex;">
<div class="title">财务指标趋势</div>
<div class="legend">
<span class="legend-item target">目标</span>
<span class="legend-item real">实际</span>
</div>
</div>
<div class="button-group">
<div style="width: 59px;" class="item-button" :class="{ active: activeButton === 0 }" @click="activeButton = 0">
营业收入
</div>
<div class="button-line lineOne" v-if="activeButton !== 0 && activeButton !== 1"></div>
<div style="width: 75px;" class="item-button" :class="{ active: activeButton === 1 }" @click="activeButton = 1">
经营性利润
</div>
<div class="button-line lineTwo" v-if="activeButton !== 1 && activeButton !== 2"></div>
<div class="item-button" :class="{ active: activeButton === 2 }" @click="activeButton = 2">
利润总额
</div>
<div class="button-line lineThree" v-if="activeButton !== 2 && activeButton !== 3"></div>
<div class="item-button" :class="{ active: activeButton === 3 }" @click="activeButton = 3">
毛利率
</div>
</div>
</div>
<div class="lineBottom" style="height: 210px; width: 100%">
<!-- 传递当前选中的 series 数据给子组件 -->
<coreLineChart style="height: 210px; width: 680px" :chart-series="currentSeries"
:key="activeButton + JSON.stringify(currentSeries)" />
</div>
</div>
</template>
<script>
import coreLineChart from './coresBar.vue';
import * as echarts from 'echarts';
export default {
name: "Container",
components: { coreLineChart },
props: ["name", "size", "icon"],
data() {
return {
activeButton: 0, // 初始激活第一个按钮索引0
itemList: [
{ unit: "单价·元/m²", targetValue: 16, currentValue: 14.5, progress: 90 },
{ unit: "净价·元/m²", targetValue: 16, currentValue: 15.2, progress: 85 },
{ unit: "销量·万m²", targetValue: 20, currentValue: 16, progress: 80 },
{ unit: "双镀面板·万m²", targetValue: 15, currentValue: 13.8, progress: 92 },
],
// 存储4个按钮对应的 series 数据(每个按钮对应「目标+实际」两条线)
seriesMap: [
// 0: 营业收入
[
{
name: '目标',
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(91, 230, 190, 1)' },
itemStyle: {
color: 'rgba(91, 230, 190, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(91, 230, 190, 0.4)' },
{ offset: 1, color: 'rgba(91, 230, 190, 0)' },
]),
},
data: [500, 620, 580, 720, 650, 800] // 6-11月目标数据
},
{
name: '实际',
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(255, 132, 0, 1)' },
itemStyle: {
color: 'rgba(255, 132, 0, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 132, 0, 0.4)' },
{ offset: 1, color: 'rgba(255, 132, 0, 0)' },
]),
},
data: [480, 590, 610, 680, 700, 750] // 6-11月实际数据
}
],
// 1: 经营性利润
[
{
name: '目标',
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(91, 230, 190, 1)' },
itemStyle: {
color: 'rgba(91, 230, 190, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(91, 230, 190, 0.4)' },
{ offset: 1, color: 'rgba(91, 230, 190, 0)' },
]),
},
data: [150, 180, 160, 200, 190, 220]
},
{
name: '实际',
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(255, 132, 0, 1)' },
itemStyle: {
color: 'rgba(255, 132, 0, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 132, 0, 0.4)' },
{ offset: 1, color: 'rgba(255, 132, 0, 0)' },
]),
},
data: [140, 170, 180, 190, 210, 200]
}
],
// 2: 利润总额
[
{
name: '目标',
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(91, 230, 190, 1)' },
itemStyle: {
color: 'rgba(91, 230, 190, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(91, 230, 190, 0.4)' },
{ offset: 1, color: 'rgba(91, 230, 190, 0)' },
]),
},
data: [120, 150, 140, 170, 160, 190]
},
{
name: '实际',
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(255, 132, 0, 1)' },
itemStyle: {
color: 'rgba(255, 132, 0, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 132, 0, 0.4)' },
{ offset: 1, color: 'rgba(255, 132, 0, 0)' },
]),
},
data: [110, 140, 150, 160, 180, 170]
}
],
// 3: 毛利率(百分比数据)
[
{
name: '目标',
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(91, 230, 190, 1)' },
itemStyle: {
color: 'rgba(91, 230, 190, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(91, 230, 190, 0.4)' },
{ offset: 1, color: 'rgba(91, 230, 190, 0)' },
]),
},
data: [35, 36, 35.5, 37, 36.5, 38]
},
{
name: '实际',
type: 'line',
stack: 'Total',
symbol: 'circle',
lineStyle: { color: 'rgba(255, 132, 0, 1)' },
itemStyle: {
color: 'rgba(255, 132, 0, 1)',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 132, 0, 0.4)' },
{ offset: 1, color: 'rgba(255, 132, 0, 0)' },
]),
},
data: [34, 35.5, 36, 36.2, 37, 37.5]
}
]
]
};
},
computed: {
// 根据当前激活的按钮,返回对应的 series 数据
currentSeries() {
return this.seriesMap[this.activeButton] || [];
}
},
methods: {}
};
</script>
<style scoped lang="scss">
/* 原有样式保持不变 */
.coreBar {
display: flex;
flex-direction: column;
padding: 12px;
.barTop {
display: flex;
gap: 8px;
justify-content: space-between;
.title {
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;
}
.legend {
display: flex;
gap: 24px;
margin-left: 30px;
.legend-item {
position: relative;
font-family: PingFangSC, PingFang SC;
font-size: 14px;
color: rgba(0, 0, 0, 0.8);
&::before {
content: "";
position: absolute;
left: -13px;
top: 30%;
width: 6px;
height: 6px;
border-radius: 50%;
}
&::after {
content: "";
position: absolute;
left: -16px;
top: 38%;
width: 12px;
height: 2px;
}
}
.target::before,
.target::after {
background-color: rgba(91, 230, 190, 1);
}
.real::before,
.real::after {
background-color: rgba(255, 132, 0, 1);
}
}
.button-group {
display: flex;
position: relative;
gap: 2px;
width: 252px;
align-items: center;
height: 24px;
background: #ecf4fe;
border-radius: 12px;
.button-line {
position: absolute;
width: 1px;
height: 14px;
border: 1px solid rgba(11, 88, 255, 0.25);
}
.lineOne {
top: 5px;
left: 59px;
}
.lineTwo {
top: 5px;
left: 134px;
}
.lineThree {
top: 5px;
left: 193px;
}
.item-button {
cursor: pointer;
width: 54px;
height: 24px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #0b58ff;
line-height: 24px;
text-align: center;
font-style: normal;
}
.item-button.active {
width: 54px;
height: 24px;
background: #3071ff;
border-radius: 12px;
color: #ffffff;
font-weight: 500;
}
}
}
}
</style>

View File

@@ -0,0 +1,261 @@
<template>
<div class="cockpitContainer" :class="['cockpitContainer__' + size]">
<div class="container-top">
<!-- 左侧标题点击切换到采购标签并更新透明度 -->
<div class="content-top-left title-wrapper" @click="handleLeftClick"
:style="{ opacity: isLeftTransparent ? 1 : 0.3 }">
<svg-icon class="title-icon" style="font-size: 32px; margin-left: 16px" :icon-class="icon" />
<span class="title-text">{{ name }}</span>
<!-- <span v-if="!isLeftTransparent" class="change-text">点击切换</span> -->
</div>
<!-- 右侧标题点击切换到存货标签并更新透明度 -->
<div class="content-top-right title-wrapper" v-if="nameTwo" @click="handleRightClick"
:style="{ opacity: isRightTransparent ? 1 : 0.3 }">
<svg-icon class="title-icon" style="font-size: 32px; margin-left: 16px" :icon-class="iconTwo || icon" />
<span class="title-text">{{ nameTwo }}</span>
</div>
<span class="change-text" :class="{ 'change-text-right': isLeftTransparent }">点击切换</span>
</div>
<div class="container-body">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'Container',
components: {},
props: {
name: { type: String, required: true },
nameTwo: { type: String, required: false },
size: { type: String, default: 'default' },
icon: { type: String, default: '' },
iconTwo: { type: String, default: '' }
},
data() {
return {
// 初始状态左侧不透明1右侧透明0.3
isLeftTransparent: true, // 左侧透明度状态true=1false=0.3
isRightTransparent: false // 右侧透明度状态true=1false=0.3
};
},
methods: {
// 点击左侧标题:左侧保持不透明,右侧变透明,并派发采购标签事件
handleLeftClick() {
this.isLeftTransparent = true; // 左侧不透明
this.isRightTransparent = false; // 右侧透明
this.$emit('switchTab', 'purchase'); // 通知父组件切换到采购内容
},
// 点击右侧标题:右侧保持不透明,左侧变透明,并派发存货标签事件
handleRightClick() {
this.isLeftTransparent = false; // 左侧透明
this.isRightTransparent = true; // 右侧不透明
this.$emit('switchTab', 'inventory'); // 通知父组件切换到存货内容
}
}
};
</script>
<style scoped lang="scss">
// 样式保持不变,确保透明度过渡生效
.cockpitContainer {
display: inline-block;
padding: 6px;
display: flex;
flex-direction: column;
position: relative;
.container-top {
position: relative;
height: 60px;
width: 100%;
}
.content-top-left {
width: 746px;
height: 60px;
background: linear-gradient(90deg, #FFFFFF 0%, rgba(253, 255, 255, 0) 100%);
position: relative;
overflow: hidden;
cursor: pointer;
z-index: 1;
transition: opacity 0.3s ease; // 透明度过渡动画
}
.title-wrapper {
display: flex;
align-items: center;
// margin-left: 10px;
/* 垂直居中关键属性 */
height: 100%;
/* 继承父容器高度,确保垂直居中范围 */
}
.title-icon {
font-size: 30px;
margin-right: 12px;
margin-top: 4px;
margin-left: 10px;
/* 图标和文字之间的间距 */
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;
}
/* 左侧标题 - 左上角折现边框 */
.content-top-left::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 1px solid;
border-image: linear-gradient(277deg, rgba(255, 255, 255, 0), rgba(92, 140, 255, 1)) 1 1;
clip-path: polygon(20px 0, 100% 0, 100% 100%, 0 100%, 0 20px);
z-index: 3;
}
/* 左侧标题 - 左上角折现细节 */
.content-top-left::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 30px;
height: 30px;
background: #E1f0fd;
border-top: 1px solid rgba(92, 140, 255, 1);
border-left: 1px solid rgba(92, 140, 255, 1);
transform: rotate(135deg) translate(-50%, -50%);
transform-origin: top left;
z-index: 3;
}
.content-top-right {
width: 368px;
height: 60px;
background: linear-gradient(90deg, #FFFFFF 0%, rgba(253, 255, 255, 0) 100%);
position: absolute;
top: 0;
right: 0;
z-index: 10;
overflow: hidden;
cursor: pointer;
transition: opacity 0.3s ease; // 透明度过渡动画
}
/* 右侧标题 - 左上角折现边框 */
.content-top-right::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 1px solid;
border-image: linear-gradient(277deg, rgba(255, 255, 255, 0), rgba(92, 140, 255, 1)) 1 1;
clip-path: polygon(20px 0, 100% 0, 100% 100%, 0 100%, 0 20px);
z-index: 12;
}
/* 右侧标题 - 左上角折现细节 */
.content-top-right::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 30px;
height: 30px;
background: #E1f0fd;
border-top: 1px solid rgba(92, 140, 255, 1);
border-left: 1px solid rgba(92, 140, 255, 1);
transform: rotate(135deg) translate(-50%, -50%);
transform-origin: top left;
z-index: 12;
}
.title-text {
margin-left: 6px;
height: 32px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 24px;
color: #000000;
// line-height: 60px;
letter-spacing: 3px;
text-align: left;
font-style: normal;
display: inline-block;
position: relative;
z-index: 1;
}
&__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;
}
}
.container-body {
flex: 1;
}
.test-body {
padding: 20px;
color: #666;
}
.change-text{
position: absolute;
top: 26px;
left: 300px;
z-index: 999;
// width: 48px;
// height: 17px;
// margin-left: 80px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #0B58FF;
line-height: 17px;
text-align: left;
font-style: normal;
}
.change-text-right {
// position: absolute;
// top: 26px;
left: auto; // 清除左侧定位
right: 30px;
// z-index: 999;
// // width: 48px;
// // height: 17px;
// // margin-left: 80px;
// font-family: PingFangSC, PingFang SC;
// font-weight: 400;
// font-size: 12px;
// color: #0B58FF;
// line-height: 17px;
// text-align: left;
// font-style: normal;
}
</style>

View File

@@ -0,0 +1,44 @@
<template>
<!-- 显示累计值并绑定颜色类 -->
<div class="accumulated-value" :class="injectData.status">
{{ injectData.accumulated }} <!-- 显示累计数据 -->
</div>
</template>
<script>
export default {
name: 'finishDiv', // 补充组件名,便于识别
props: {
injectData: { // 接收父组件传递的当前行数据
type: Object,
default: () => ({})
}
},
data() {
return {
// list: this.injectData // 将接收的行数据赋值给list便于模板使用
}
},
methods: {
// 若无需输入功能可删除changeInput方法若需保留可按需调整
// changeInput() {
// console.log(this.list)
// this.$emit('emitData', this.list)
// }
}
}
</script>
<style scoped>
/* 累计值基础样式 */
/* 已完成:绿色 #4CAF50 */
.accumulated-value.done {
color: #4CAF50;
}
/* 未完成:橙色 #FF9800 */
.accumulated-value.pending {
color: #FF9800;
}
</style>

View File

@@ -0,0 +1,230 @@
<template>
<div style="flex: 1">
<Container name="生产重点指标" icon="cockpitItemIcon" size="topBasic" topSize="basic">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex;flex-direction: column; width: 100%;">
<!-- 2. .top 保持 flex无需固定高度自动跟随子元素拉伸 -->
<div class="top" style="display: flex; width: 100%;">
<top-item />
</div>
<div class="bottom" style="display: flex;margin-top: 8px;background-color: rgba(249, 252, 255, 1);">
<!-- <top-item /> -->
<coreBottomBar />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
// import * as echarts from 'echarts'
import topItem from './top-product-item.vue'
import coreBottomBar from './productBottomBar.vue'
export default {
name: 'ProductionStatus',
components: { Container, topItem, coreBottomBar },
// mixins: [resize],
props: {
leftEqInfoData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
productionOverviewVo: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
type: Object,
default: () => ({})
}
},
data() {
return {
chart: null
}
},
watch: {
productionOverviewVo: {
handler(newValue, oldValue) {
// this.updateChart()
},
deep: true // 若对象内属性变化需触发,需加 deep: true
}
},
mounted() {
// 初始化图表(若需展示图表,需在模板中添加对应 DOM
// this.$nextTick(() => this.updateChart())
},
methods: {
}
}
</script>
<style lang='scss' scoped>
/* 3. 核心:滚动容器样式(固定高度+溢出滚动+隐藏滚动条) */
.scroll-container {
/* 1. 固定容器高度根据页面布局调整示例300px超出则滚动 */
max-height: 210px;
/* 2. 溢出滚动:内容超出高度时显示滚动功能 */
overflow-y: auto;
/* 3. 隐藏横向滚动条(防止设备名过长导致横向滚动) */
overflow-x: hidden;
/* 4. 内边距:与标题栏和容器边缘对齐 */
padding: 10px 0;
/* 5. 隐藏滚动条(兼容主流浏览器) */
/* Chrome/Safari */
&::-webkit-scrollbar {
display: none;
}
/* Firefox */
scrollbar-width: none;
/* IE/Edge */
-ms-overflow-style: none;
}
/* 设备项样式优化:增加间距,避免拥挤 */
.proBarInfo {
display: flex;
flex-direction: column;
padding: 8px 27px;
/* 调整内边距,优化排版 */
margin-bottom: 10px;
/* 设备项之间的垂直间距 */
}
/* 原有样式保留,优化细节 */
.proBarInfoEqInfo {
display: flex;
justify-content: space-between;
align-items: center;
/* 垂直居中,避免序号/文字错位 */
}
.slot {
width: 21px;
height: 23px;
background: rgba(0, 106, 205, 0.22);
backdrop-filter: blur(1.5px);
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #68B5FF;
line-height: 23px;
/* 垂直居中文字 */
text-align: center;
font-style: normal;
}
.eq-name {
margin-left: 8px;
/* 增加与序号的间距 */
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
letter-spacing: 1px;
text-align: left;
font-style: normal;
}
.eqStatus {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.splitLine {
width: 1px;
height: 14px;
border: 1px solid #ADADAD;
margin: 0 8px;
/* 优化分割线间距 */
}
.yield {
height: 18px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #00FFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.proBarInfoEqInfoLeft {
display: flex;
align-items: center;
/* 序号和设备名垂直居中 */
}
.proBarInfoEqInfoRight {
display: flex;
align-items: center;
/* 状态/分割线/百分比垂直居中 */
}
.proBarWrapper {
position: relative;
height: 10px;
margin-top: 6px;
/* 进度条与上方信息的间距 */
border-radius: 5px;
/* 进度条圆角,优化视觉 */
overflow: hidden;
}
.proBarLine {
width: 100%;
height: 100%;
background: linear-gradient(65deg, rgba(82, 82, 82, 0) 0%, #ACACAC 100%);
opacity: 0.2;
}
.proBarLineTop {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: linear-gradient(65deg, rgba(53, 223, 247, 0) 0%, rgba(54, 220, 246, 0.92) 92%, #36F6E5 100%, #37ACF5 100%);
border-radius: 5px;
transition: width 0.3s ease;
/* 进度变化时添加过渡动画,更流畅 */
}
/* 图表相关样式保留 */
.chartImgBottom {
position: absolute;
bottom: 45px;
left: 58px;
}
.line {
display: inline-block;
position: absolute;
left: 57px;
bottom: 42px;
width: 1px;
height: 20px;
background-color: #00E8FF;
}
</style>
<style>
/* 全局 tooltip 样式(不使用 scoped确保生效 */
.production-status-chart-tooltip {
background: #0a2b4f77 !important;
border: none !important;
backdrop-filter: blur(12px);
}
.production-status-chart-tooltip * {
color: #fff !important;
}
</style>

View File

@@ -0,0 +1,135 @@
<template>
<div style="flex: 1">
<Container name="重点工作/三大攻坚战" icon="cockpitItemIcon" size="bottomBasic" topSize="basic">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content"
style="padding: 14px 16px; display: flex;flex-direction: column; width: 100%;height: 280px;">
<div class="bottom"
style="display: flex;padding: 14px 16px;flex-direction: column; background-color: rgba(249, 252, 255, 1);">
<div class="legend-group" style="justify-content: end;">
<div class="legend-item">
<span class="legend-dot done"></span>
<span class="legend-text">已完成</span>
</div>
<div class="legend-item">
<span class="legend-dot pending"></span>
<span class="legend-text">未完成</span>
</div>
</div>
<base-table style="height: 180px;" :page="1" :limit="10" :show-index="true" :beilv="1" :tableConfig="tableProps"
:table-data="maintenanceTasks" />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
// import * as echarts from 'echarts'
import topItem from './top-product-item.vue'
import coreBottomBar from './productBottomBar.vue'
import baseTable from './baseTable.vue'
import finishDiv from './finishDiv.vue'
export default {
name: 'ProductionStatus',
components: { Container, topItem, coreBottomBar, baseTable },
// mixins: [resize],
props: {
leftEqInfoData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
productionOverviewVo: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
type: Object,
default: () => ({})
}
},
data() {
return {
maintenanceTasks: [
{ id: 1, eqName: '研发经费入强度/%', taskName: '例行维护', monthlyActual: '85%', accumulated: '78%', status: 'done' }, // 已完成-绿色
{ id: 2, eqName: '存货/亿元', taskName: '例行维护', monthlyActual: '60%', accumulated: '65%', status: 'pending' }, // 未完成-橙色
{ id: 3, eqName: '三年以上应收款/亿元', taskName: '故障排查', monthlyActual: '100%', accumulated: '92%', status: 'done' },
{ id: 4, eqName: '非经营性资产处置到账金额/万元', taskName: '部件更换', monthlyActual: '45%', accumulated: '50%', status: 'pending' },
{ id: 4, eqName: '研发经费投入/万元', taskName: '部件更换', monthlyActual: '45%', accumulated: '50%', status: 'pending' },
{ id: 4, eqName: '经营性现金流/万元', taskName: '部件更换', monthlyActual: '45%', accumulated: '50%', status: 'pending' },
// { id: 2, eqName: '螺杆挤出', taskName: '例行维护', },
],
tableProps: [
// { prop: 'id', label: '序号', width: 50, align: 'center' },
{ prop: 'eqName', label: '攻坚指标', align: 'center' },
{ prop: 'taskName', label: '攻坚目标', align: 'center' },
{ prop: 'taskName', label: '当月实际', align: 'center' },
{ prop: 'taskName', label: '累计', align: 'center', subcomponent: finishDiv },
]
}
},
watch: {
productionOverviewVo: {
handler(newValue, oldValue) {
this.updateChart()
},
deep: true // 若对象内属性变化需触发,需加 deep: true
}
},
mounted() {
// 初始化图表(若需展示图表,需在模板中添加对应 DOM
// this.$nextTick(() => this.updateChart())
},
beforeDestroy() {
// 销毁图表,避免内存泄漏
},
methods: {
}
}
</script>
<style lang='scss' scoped>
.legend-group {
display: flex;
align-items: center;
gap: 10px;
/* 两个图例项之间的间距 */
}
/* 单个图例项 */
.legend-item {
display: flex;
align-items: center;
}
/* 图例小方块 */
.legend-dot {
display: inline-block;
width: 12px;
height: 12px;
margin-right: 5px;
border-radius: 2px;
/* 可选:轻微圆角 */
}
/* 已完成(绿色) */
.legend-dot.done {
background-color: #4CAF50;
/* 绿色,可根据需求调整色值 */
}
/* 未完成(橙色) */
.legend-dot.pending {
background-color: #FF9800;
/* 橙色,可根据需求调整色值 */
}
/* 图例文字 */
.legend-text {
font-size: 14px;
color: #333;
}
</style>

View File

@@ -0,0 +1,371 @@
<template>
<header class="report-header" :class="['report-header__' + size]">
<!-- 左侧区域标题 -->
<div class="left-content">
<div class="top-title">{{ topTitle }}</div>
</div>
<!-- 右侧区域全屏按钮 -->
<div class="right-content">
<el-button type="text" class="return-btn" :title="'返回'" @click="handleReturn">
<svg-icon style="color: #0B58FF;" icon-class="returnIcon" />
</el-button>
<el-button type="text" class="screen-btn" :title="isFullScreen ? '退出全屏' : '全屏'" @click="changeFullScreen">
<svg-icon style="color: #0B58FF;" v-if="isFullScreen" icon-class="unFullScreenView" />
<svg-icon style="color: #0B58FF;" v-else icon-class="fullScreenView" />
</el-button>
</div>
<!-- 时间选择区域//年按钮 + 日期选择器 -->
<div class="timeType">
<div class="item" v-for="(item, index) in timeTypes" :key="index" @click="activeTime = index"
:class="{ 'no-skew': activeTime === index }">
<span class="item-text">{{ item.text }}</span>
</div>
<div class="dateP">
<div class="label">
<span class="label-text">日期选择</span>
</div>
<el-date-picker v-model="date" :type="getPickerType" :placeholder="getPickerPlaceholder"
class="custom-date-picker" value-format="yyyy-MM-dd" :clearable="false" style="width: 132px;height: 29px;"
@change="emitTimeRange" />
</div>
</div>
</header>
</template>
<script>
import moment from 'moment'; // 引入moment
export default {
name: 'Header',
props: {
isFullScreen: { type: Boolean, default: false },
topTitle: { type: String, default: '' },
size: { type: String, default: 'basic' },
},
data() {
return {
currentTime: '',
timeTimer: null,
date: undefined, // 存储选择的日期字符串格式yyyy-MM-dd/yyyy-MM/yyyy
activeTime: 1, // 默认月维度0=日1=月2=年)
timeTypes: [
{ text: '日', pickerType: 'date', placeholder: '选择日期' },
{ text: '月', pickerType: 'month', placeholder: '选择月份' },
{ text: '年', pickerType: 'year', placeholder: '选择年份' }
]
}
},
computed: {
getPickerType() {
return this.timeTypes[this.activeTime].pickerType;
},
getPickerPlaceholder() {
return this.timeTypes[this.activeTime].placeholder;
}
},
watch: {
activeTime(newVal, oldVal) {
if (newVal !== oldVal) {
this.date = undefined;
// this.emitTimeRange();
}
}
},
mounted() {
// 初始化默认日期当前月格式yyyy-MM
console.log(this.$router);
this.date = moment().format('YYYY-MM');
this.$nextTick(() => this.emitTimeRange());
},
methods: {
changeFullScreen() {
this.$emit('screenfullChange');
},
handleReturn() {
console.log(this.$router);
this.$router.go(-1);
},
exportPDF() {
this.$emit('exportPDF');
},
/**
* 核心方法用moment计算时间范围时间戳格式
* 日选择日00:00:00 → 次日00:00:00
* 月当月1日00:00:00 → 次月1日00:00:00
* 年当年1月1日00:00:00 → 次年1月1日00:00:00
*/
calculateTimeRange() {
let startTime = 0;
let endTime = 0;
const mode = this.activeTime + 1; // 1=日2=月3=年
const defaultMoment = moment(); // 默认当前时间
// 处理选择的日期转为moment对象兼容不同选择器格式
console.log('this.date', this.date);
const targetMoment = this.date
? moment(this.date, this.getPickerType === 'date' ? 'YYYY-MM-DD' : (this.getPickerType === 'month' ? 'YYYY-MM' : 'YYYY'))
: defaultMoment;
// 验证日期有效性
if (!targetMoment.isValid()) {
console.error('无效日期:', this.date);
return { startTime, endTime, mode };
}
// 1. 日维度当天0点 → 次日0点
if (this.activeTime === 0) {
startTime = targetMoment.startOf('day').valueOf(); // 当天00:00:00 时间戳
endTime = targetMoment.add(1, 'day').startOf('day').valueOf(); // 次日00:00:00 时间戳
}
// 2. 月维度当月1日0点 → 次月1日0点
else if (this.activeTime === 1) {
startTime = targetMoment.startOf('month').valueOf(); // 当月1日00:00:00 时间戳
endTime = targetMoment.add(1, 'month').startOf('month').valueOf(); // 次月1日00:00:00 时间戳
}
// 3. 年维度当年1月1日0点 → 次年1月1日0点
else if (this.activeTime === 2) {
startTime = targetMoment.startOf('year').valueOf(); // 当年1月1日00:00:00 时间戳
endTime = targetMoment.add(1, 'year').startOf('year').valueOf(); // 次年1月1日00:00:00 时间戳
}
// 调试输出(格式化显示,便于验证)
console.log('时间范围计算结果:', {
mode,
startTime: moment(startTime).format('YYYY-MM-DD HH:mm:ss'),
endTime: moment(endTime).format('YYYY-MM-DD HH:mm:ss'),
startTimeStamp: startTime,
endTimeStamp: endTime
});
return { startTime, endTime, mode };
},
// 传递时间范围给父组件
emitTimeRange() {
const timeRange = this.calculateTimeRange();
this.$emit('timeRangeChange', timeRange);
}
}
}
</script>
<style scoped lang="scss">
/* 字体引入 */
@font-face {
font-family: "YouSheBiaoTiHei";
src: url('../../../assets/fonts/YouSheBiaoTiHe.ttf') format('truetype');
}
/* 头部容器基础样式 */
.report-header {
height: 117px;
width: 100%;
display: flex;
justify-content: space-between;
box-sizing: border-box;
position: relative;
&__basic {
background: url(../../../assets/img/topBg.png) no-repeat;
background-size: cover;
background-position: 0 0;
}
&__psi {
background: url(../../../assets/img/psiTopTitle.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
/* 左侧标题区域 */
.left-content {
margin-top: 11px;
margin-left: 350px;
height: 55px;
display: flex;
align-items: center;
gap: 16px;
}
.top-title {
height: 55px;
font-family: "YouSheBiaoTiHei", sans-serif;
font-size: 42px;
color: #1E1651;
line-height: 55px;
letter-spacing: 6px;
text-align: left;
}
/* 时间选择区域 */
.timeType {
position: absolute;
display: flex;
align-items: center;
top: 42px;
right: 0px;
margin-top: 18px;
gap: 0;
}
.timeType .item {
width: 50px;
height: 28px;
background: rgba(236, 244, 254, 1);
transform: skew(-25deg);
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: rgba(11, 88, 255, 1);
line-height: 28px;
letter-spacing: 2px;
text-align: center;
cursor: pointer;
overflow: hidden;
}
.timeType .item .item-text {
display: inline-block;
transform: skew(25deg);
transition: all 0.2s ease;
}
.timeType .item.no-skew {
background: rgba(11, 88, 255, 1);
color: rgba(249, 252, 255, 1);
transform: skew(-25deg) !important;
box-shadow: 0 2px 8px rgba(11, 88, 255, 0.3);
}
.timeType .item.no-skew .item-text {
transform: skew(25deg) !important;
}
.dateP {
position: relative;
margin-left: 10px;
display: flex;
align-items: center;
gap: 0;
}
.dateP .label {
width: 70px;
height: 28px;
background: rgba(236, 244, 254, 1);
transform: skew(-25deg);
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: #0B58FF;
line-height: 28px;
text-align: center;
overflow: hidden;
}
.dateP .label-text {
display: inline-block;
transform: skew(25deg);
}
/* 右侧全屏按钮区域 */
.right-content {
display: flex;
// flex-direction: column;
margin-top: 12px;
margin-right: 10px;
gap: 21px;
}
// .current-time {
// color: #FFFFFF;
// font-family: PingFangSC, PingFang SC;
// font-weight: 500;
// font-size: 22px;
// line-height: 24px;
// letter-spacing: 1px;
// }
.screen-btn {
width: 26px;
height: 26px;
color: #00fff0;
font-size: 26px;
padding: 0;
}
.home-btn {
width: 26px;
height: 26px;
// margin-left: 300px;
color: #00fff0;
font-size: 26px;
padding: 0;
}
.return-btn {
width: 26px;
height: 26px;
// margin-left: 300px;
color: #00fff0;
font-size: 26px;
padding: 0;
}
}
/* 日期选择器自定义样式 */
::v-deep .custom-date-picker {
position: absolute;
right: 8px;
width: 132px !important;
height: 28px !important;
position: relative;
margin: 0 !important;
.el-input__inner {
height: 28px !important;
width: 132px !important;
text-align: center;
padding-left: 15px !important;
padding-right: 32px !important;
font-size: 14px !important;
line-height: 28px !important;
color: rgba(237, 245, 253, 1) !important;
vertical-align: middle !important;
clip-path: polygon(18px 0, 100% 0, 100% 100%, 0 100%);
border: none !important;
box-shadow: none !important;
background-color: rgba(11, 88, 255, 1) !important;
border-left: 1px solid rgba(255, 255, 255, 0.2);
}
.el-input__prefix {
left: auto !important;
right: 8px !important;
top: 50% !important;
transform: translateY(-50%) !important;
display: inline-flex !important;
align-items: center !important;
height: 28px !important;
}
.el-input__icon {
color: #ffffff !important;
font-size: 16px !important;
line-height: 28px !important;
vertical-align: middle !important;
}
.el-icon-date::before {
color: #ffffff !important;
font-size: 16px !important;
line-height: inherit !important;
}
}
</style>

View File

@@ -0,0 +1,203 @@
<template>
<div class="coreItem" :style="{ 'height': height + 'px' }">
<!-- v-for 动态生成每个 item -->
<div class="item" @click="handleRoute(item.route)" v-for="(item, index) in itemList" :key="index">
<div class="unit">{{ item.name }}</div>
<div class="item-content">
<!-- 左右内容容器 -->
<div class="content-wrapper">
<div class="left">
<div class="number">{{ item.targetValue || 0 }}</div>
<div class="title">目标值</div>
</div>
<div class="line"></div>
<div class="right">
<!-- 实际值颜色动态绑定 -->
<div class="number" :style="{ 'color': item.completed === 0 ? '#FF8400' : 'rgba(54, 181, 138, .7)' }">
{{ item.value ||0 }}
</div>
<div class="title">实际值</div>
</div>
<div class="line"></div>
<div class="right">
<!-- 完成率颜色动态绑定 -->
<div class="number" :style="{ 'color': item.completed === 0 ? '#FF8400' : 'rgba(54, 181, 138, .7)' }">
{{ item.proportion !== null && item.proportion !== undefined ? (item.proportion * 100) + '%' : '0%' }}
</div>
<div class="title">完成率</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Container",
components: {},
props: ['itemList', 'height'],
data() {
return {
progress: 90, // 进度值,方便统一控制
};
},
watch: {
itemList: {
handler(newList) {
// 当 itemList 变化时执行的逻辑(如重新计算、更新视图等)
console.log("子组件接收的 itemList 已更新:", newList);
},
deep: true // 深度监听:若 itemList 内部对象属性变化(如 currentValue 改变),也能触发监听
}
},
computed: {},
methods: {
handleRoute(route) {
this.$router.push({
path:route
})
}
},
};
</script>
<style scoped lang="scss">
.coreItem {
display: flex;
// height: 382px;
overflow: hidden;
flex-wrap: wrap;
// grid-template-columns: 1fr 1fr;
// grid-template-rows: 1fr 1fr;
justify-content: flex-start;
align-content: flex-start;
gap: 8px;
overflow-y: auto;
// padding-right: 4px;
/* 2. 隐藏 Chrome/Safari 滚动条 */
&::-webkit-scrollbar {
width: 0;
/* 滚动条宽度设为0 */
height: 0;
/* 横向滚动条(如需)也隐藏 */
}
/* 3. 隐藏 Firefox 滚动条 */
scrollbar-width: none;
}
.item {
width: 376px;
height: 95px;
background: #f9fcff;
padding: 12px;
box-sizing: border-box;
&:hover {
box-shadow: 0px 4px 12px 2px #B5CDE5;
}
// &:hover {
// box-shadow: 0px 4px 12px 2px #B5CDE5;
// }
.unit {
// width: 124px;
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;
margin-bottom: 8px;
}
.item-content {
display: flex;
flex-direction: column;
justify-content: space-between;
height: calc(100% - 26px);
}
.content-wrapper {
display: flex;
align-items: center;
justify-content: space-around;
flex: 1;
}
.line {
width: 1px;
height: 46px;
background: linear-gradient(to bottom,
rgba(255, 0, 0, 0),
rgba(40, 203, 151, 1));
}
.left,
.right {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 5px;
flex: 1;
}
.number {
height: 22px;
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 26px;
color: rgba(103, 103, 103, 0.79);
line-height: 22px;
text-align: center;
font-style: normal;
}
.title {
height: 14px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: #868687;
line-height: 14px;
text-align: center;
font-style: normal;
}
/* 进度条和百分比的外层容器 */
.progress-group {
display: flex;
align-items: center;
gap: 8px;
/* 进度条和百分比的间距 */
}
.progress-container {
width: 190px;
height: 10px;
background: #ECEFF7;
border-radius: 8px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: #28CB97;
border-radius: 8px;
opacity: 0.6;
}
/* 百分比文本样式 */
.progress-percent {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #868687;
line-height: 1;
}
}
</style>

View File

@@ -0,0 +1,206 @@
<template>
<div class="coreBar">
<div class="barTop">
<!-- 标题单独左对齐 -->
<!-- <div class="title">销售指标趋势</div> -->
<!-- 关键新增右侧容器包裹图例和按钮组实现整体靠右 -->
<div class="right-container">
<div class="legend">
<span class="legend-item">
<span class="legend-icon line yield"></span>
完成率
</span>
<span class="legend-item">
<span class="legend-icon square target"></span>
目标
</span>
<span class="legend-item">
<span class="legend-icon square achieved"></span>
达标
</span>
<span class="legend-item">
<span class="legend-icon square unachieved"></span>
未达标
</span>
</div>
<div class="button-group">
<div style="letter-spacing: 8px" class="item-button" :class="{ active: activeButton === 0 }"
@click="activeButton = 0">
销量
</div>
<div style="letter-spacing: 8px" class="item-button" :class="{ active: activeButton === 1 }"
@click="activeButton = 1">
毛利率
</div>
</div>
</div>
</div>
<div class="lineBottom" style="height: 100%; width: 100%">
<operatingLineBar style="height: 99%; width: 100%" />
</div>
</div>
</template>
<script>
import operatingLineBar from './operatingLineBar.vue';
export default {
name: "Container",
components: { operatingLineBar },
props: ["name", "size", "icon"],
data() {
return {
activeButton: 0,
itemList: [
{ unit: "单价·元/m²", targetValue: 16, currentValue: 14.5, progress: 90 },
{ unit: "净价·元/m²", targetValue: 16, currentValue: 15.2, progress: 85 },
{ unit: "销量·万m²", targetValue: 20, currentValue: 16, progress: 80 },
{ unit: "双镀面板·万m²", targetValue: 15, currentValue: 13.8, progress: 92 },
],
};
},
computed: {},
methods: {},
};
</script>
<style scoped lang="scss">
.coreBar {
display: flex;
flex-direction: column;
width: 100%;
padding: 12px;
.barTop {
display: flex;
justify-content: flex-end; // 标题左、右侧容器右,整体两端对齐
align-items: center; // 垂直居中,避免上下错位
gap: 16px; // 标题与右侧容器的最小间距,防止拥挤
width: 100%; // 确保占满父容器,实现两端对齐
.title {
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;
// 标题固定在左侧,不挤压右侧空间
white-space: nowrap;
}
// 1. 右侧容器:包裹图例和按钮组,整体靠右
.right-container {
display: flex;
align-items: center; // 图例和按钮组垂直居中
gap: 24px; // 图例与按钮组的间距,避免贴紧
margin-right: 46px; // 右侧整体留边,与原按钮组边距一致
}
// 2. 图例:在右侧容器内横向排列
.legend {
display: flex;
gap: 16px; // 图例项之间间距,避免重叠
align-items: center;
// 移除原margin-left避免位置偏移
margin: 0;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: rgba(0, 0, 0, 0.8);
text-align: left;
font-style: normal;
white-space: nowrap; // 防止图例文字换行
}
.legend-icon {
display: inline-block;
}
.legend-icon.line {
width: 12px;
height: 2px;
position: relative;
&::before {
position: absolute;
content: "";
top: -2px;
left: 3px;
width: 6px;
border-radius: 50%;
height: 6px;
background-color: rgba(40, 138, 255, 1);
}
}
.legend-icon.square {
width: 8px;
height: 8px;
}
// 图例颜色
.yield {
background: rgba(40, 138, 255, 1);
}
.target {
background: #2889FF;
}
.achieved {
background: rgba(40, 203, 151, 1);
}
.unachieved {
background: rgba(255, 132, 0, 1);
}
// 3. 按钮组:在右侧容器内,保留原有样式
.button-group {
display: flex;
position: relative;
gap: 2px;
width: 283px;
align-items: center;
height: 24px;
background: #ecf4fe;
border-radius: 12px;
// 移除原margin-right由右侧容器统一控制
margin: 0;
.item-button {
cursor: pointer;
width: 142px;
height: 24px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #0b58ff;
line-height: 24px;
text-align: center;
font-style: normal;
letter-spacing: 8px; // 确保文字间距生效
padding-left: 8px; // 抵消letter-spacing导致的文字左偏
}
.item-button.active {
width: 142px;
height: 24px;
background: #3071ff;
border-radius: 12px;
color: #ffffff;
font-weight: 500;
}
}
}
}
</style>

View File

@@ -0,0 +1,173 @@
<template>
<div ref="cockpitEffChipBottom" id="cockpitEffChipBottom" style="width: 100%; height: 400px;"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
components: {},
data() {
return {
myChart: null // 存储图表实例,避免重复创建
};
},
props: {
// 明确接收的props结构增强可读性
chartData: {
type: Object,
default: () => ({
series: [],
allPlaceNames: []
}),
// 校验数据格式
validator: (value) => {
return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
}
}
},
mounted() {
this.$nextTick(() => {
this.updateChart();
});
},
// 新增:监听 chartData 变化
watch: {
// 深度监听数据变化,仅更新图表配置(不销毁实例)
chartData: {
handler() {
console.log(this.chartData,'chartData');
this.updateChart();
},
deep: true,
immediate: true // 初始化时立即执行
}
},
methods: {
updateChart() {
const chartDom = this.$refs.cockpitEffChipBottom;
if (!chartDom) {
console.error('图表容器未找到!');
return;
}
if (this.myChart) {
this.myChart.dispose();
}
this.myChart = echarts.init(chartDom);
const { allPlaceNames, series } = this.chartData || {};
// 处理空数据
const xData = allPlaceNames || [];
const chartSeries = series || []; // 父组件传递的 series
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
},
formatter: (params) => {
let html = `${params[0].axisValue}<br/>`;
params.forEach(item => {
const unit = item.seriesName === '完成率' ? '%' : (
['产量', '销量'].includes(this.$parent.selectedProfit) ? '片' : '万元'
);
html += `${item.marker} ${item.seriesName}: ${item.value}${unit}<br/>`;
});
return html;
}
},
grid: {
top: 30,
bottom: 30,
right: 70,
left: 40,
},
xAxis: [
{
type: 'category',
boundaryGap: true,
axisTick: { show: false },
axisLine: {
show: true,
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
},
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
interval: 0,
padding: [5, 0, 0, 0]
},
data: xData
}
],
yAxis: [
// 左侧Y轴营业收入、成本单位万元
{
type: 'value',
name: '万元',
nameTextStyle: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
align: 'right'
},
min: 0,
max: (value) => Math.ceil((value.max || 0) * 1.1),
scale: false,
axisTick: { show: false },
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
formatter: '{value}'
},
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
splitNumber: 4
},
// 右侧Y轴利润占比百分比
{
type: 'value',
nameTextStyle: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
align: 'left'
},
min: 0,
max: 100,
axisTick: { show: false },
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
formatter: '{value}%'
},
splitLine: { show: false },
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
splitNumber: 4
}
],
series: chartSeries // 直接使用父组件传递的 series
};
option && this.myChart.setOption(option);
// 窗口缩放适配和销毁逻辑保持不变
window.addEventListener('resize', () => {
this.myChart && this.myChart.resize();
});
this.$once('hook:destroyed', () => {
window.removeEventListener('resize', () => {
this.myChart && this.myChart.resize();
});
this.myChart && this.myChart.dispose();
});
}
},
};
</script>

View File

@@ -0,0 +1,173 @@
<template>
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 400px;"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
components: {},
data() {
return {
myChart: null // 存储图表实例,避免重复创建
};
},
props: {
// 明确接收的props结构增强可读性
chartData: {
type: Object,
default: () => ({
series: [],
allPlaceNames: []
}),
// 校验数据格式
validator: (value) => {
return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
}
}
},
mounted() {
this.$nextTick(() => {
this.updateChart();
});
},
// 新增:监听 chartData 变化
watch: {
// 深度监听数据变化,仅更新图表配置(不销毁实例)
chartData: {
handler() {
console.log(this.chartData,'chartData');
this.updateChart();
},
deep: true,
immediate: true // 初始化时立即执行
}
},
methods: {
updateChart() {
const chartDom = this.$refs.cockpitEffChip;
if (!chartDom) {
console.error('图表容器未找到!');
return;
}
if (this.myChart) {
this.myChart.dispose();
}
this.myChart = echarts.init(chartDom);
const { allPlaceNames, series } = this.chartData || {};
// 处理空数据
const xData = allPlaceNames || [];
const chartSeries = series || []; // 父组件传递的 series
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
},
// formatter: (params) => {
// let html = `${params[0].axisValue}<br/>`;
// params.forEach(item => {
// const unit = item.seriesName === '完成率' ? '%' : (
// ['产量', '销量'].includes(this.$parent.selectedProfit) ? '片' : '万元'
// );
// html += `${item.marker} ${item.seriesName}: ${item.value}${unit}<br/>`;
// });
// return html;
// }
},
grid: {
top: 30,
bottom: 30,
right: 70,
left: 40,
},
xAxis: [
{
type: 'category',
boundaryGap: true,
axisTick: { show: false },
axisLine: {
show: true,
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
},
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
interval: 0,
padding: [5, 0, 0, 0]
},
data: xData
}
],
yAxis: [
// 左侧Y轴营业收入、成本单位万元
{
type: 'value',
name: '万元',
nameTextStyle: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
align: 'right'
},
min: 0,
max: (value) => Math.ceil((value.max || 0) * 1.1),
scale: false,
axisTick: { show: false },
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
formatter: '{value}'
},
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
splitNumber: 4
},
// 右侧Y轴利润占比百分比
{
type: 'value',
nameTextStyle: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
align: 'left'
},
min: 0,
max: 100,
axisTick: { show: false },
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
formatter: '{value}%'
},
splitLine: { show: false },
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
splitNumber: 4
}
],
series: chartSeries // 直接使用父组件传递的 series
};
option && this.myChart.setOption(option);
// 窗口缩放适配和销毁逻辑保持不变
window.addEventListener('resize', () => {
this.myChart && this.myChart.resize();
});
this.$once('hook:destroyed', () => {
window.removeEventListener('resize', () => {
this.myChart && this.myChart.resize();
});
this.myChart && this.myChart.dispose();
});
}
},
};
</script>

View File

@@ -0,0 +1,342 @@
<template>
<div style="flex: 1">
<Container name="趋势图" icon="cockpitItemIcon" size="operatingLarge" topSize="large">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex;width: 100%;">
<div class="bottom"
style="height: 380px; display: flex; width: 100%;background-color: rgba(249, 252, 255, 1);">
<!-- <top-item /> -->
<coreBottomBar />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
// import * as echarts from 'echarts'
import coreBottomBar from './operatingBar.vue'
export default {
name: 'ProductionStatus',
components: { Container, coreBottomBar },
// mixins: [resize],
props: {
leftEqInfoData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
productionOverviewVo: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
type: Object,
default: () => ({})
}
},
data() {
return {
chart: null
}
},
watch: {
productionOverviewVo: {
handler(newValue, oldValue) {
this.updateChart()
},
deep: true // 若对象内属性变化需触发,需加 deep: true
}
},
mounted() {
// 初始化图表(若需展示图表,需在模板中添加对应 DOM
// this.$nextTick(() => this.updateChart())
},
beforeDestroy() {
// 销毁图表,避免内存泄漏
if (this.chart) {
this.chart.dispose()
this.chart = null
}
},
methods: {
updateChart() {
// 注意:原代码中图表依赖 id 为 "productionStatusChart" 的 DOM需在模板中补充否则会报错
// 示例:在 Container 内添加 <div id="productionStatusChart" style="height: 200px;"></div>
if (!document.getElementById('productionStatusChart')) return
if (this.chart) this.chart.dispose()
this.chart = echarts.init(document.getElementById('productionStatusChart'))
const data = [
this.productionOverviewVo.input || 0,
this.productionOverviewVo.output || 0,
this.productionOverviewVo.ng || 0,
this.productionOverviewVo.lowValue || 0,
this.productionOverviewVo.scrap || 0,
this.productionOverviewVo.inProcess || 0,
this.productionOverviewVo.engineer || 0
]
const option = {
type: 'bar',
grid: { left: 51, right: 40, top: 50, bottom: 45 },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
className: 'production-status-chart-tooltip'
},
xAxis: {
type: 'category',
offset: 8,
data: ['投入', '产出', '待判', '低价值', '报废', '在制', '实验片'],
axisTick: { show: false },
axisLine: { show: true, onZero: false, lineStyle: { color: '#00E8FF' } },
axisLabel: {
color: 'rgba(255,255,255,0.7)',
fontSize: 12,
interval: 0,
width: 38,
overflow: 'break'
}
},
yAxis: {
type: 'value',
name: '单位/片',
nameTextStyle: { color: 'rgba(255,255,255,0.7)', fontSize: 14, align: 'left' },
min: () => 0,
max: (value) => Math.ceil(value.max),
scale: true,
axisTick: { show: false },
axisLabel: { color: 'rgba(255,255,255,0.7)', fontSize: 12 },
splitLine: { lineStyle: { color: 'RGBA(24, 88, 100, 0.6)', type: 'dashed' } },
axisLine: { show: true, lineStyle: { color: '#00E8FF' } }
},
series: [
{
type: 'pictorialBar',
label: { show: true, position: 'top', distance: -3, color: '#89CDFF', fontSize: 11 },
symbolSize: [20, 8],
symbolOffset: [0, 5],
z: 20,
itemStyle: {
borderColor: '#3588C7',
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'RGBA(22, 89, 98, 1)' },
{ offset: 1, color: '#3588C7' }
])
},
data: data
},
{
type: 'bar',
barWidth: 20,
itemStyle: {
borderWidth: 1,
borderColor: '#3588C7',
opacity: 0.8,
color: {
x: 0, y: 0, x2: 0, y2: 1,
type: 'linear',
global: false,
colorStops: [
{ offset: 0, color: 'rgba(73,178,255,0)' },
{ offset: 0.5, color: 'rgba(0, 232, 255, .5)' },
{ offset: 1, color: 'rgba(0, 232, 255, 1)' }
]
}
},
tooltip: { show: false },
data: data
},
{
type: 'pictorialBar',
symbolSize: [20, 8],
symbolOffset: [0, -4],
z: 12,
symbolPosition: 'end',
itemStyle: {
borderColor: 'rgba(0, 232, 255, 1)',
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'RGBA(22, 89, 98, 1)' },
{ offset: 1, color: '#3588C7' }
])
},
tooltip: { show: false },
data: data
}
]
}
this.chart.setOption(option)
}
}
}
</script>
<style lang='scss' scoped>
/* 3. 核心:滚动容器样式(固定高度+溢出滚动+隐藏滚动条) */
.scroll-container {
/* 1. 固定容器高度根据页面布局调整示例300px超出则滚动 */
max-height: 210px;
/* 2. 溢出滚动:内容超出高度时显示滚动功能 */
overflow-y: auto;
/* 3. 隐藏横向滚动条(防止设备名过长导致横向滚动) */
overflow-x: hidden;
/* 4. 内边距:与标题栏和容器边缘对齐 */
padding: 10px 0;
/* 5. 隐藏滚动条(兼容主流浏览器) */
/* Chrome/Safari */
&::-webkit-scrollbar {
display: none;
}
/* Firefox */
scrollbar-width: none;
/* IE/Edge */
-ms-overflow-style: none;
}
/* 设备项样式优化:增加间距,避免拥挤 */
.proBarInfo {
display: flex;
flex-direction: column;
padding: 8px 27px;
/* 调整内边距,优化排版 */
margin-bottom: 10px;
/* 设备项之间的垂直间距 */
}
/* 原有样式保留,优化细节 */
.proBarInfoEqInfo {
display: flex;
justify-content: space-between;
align-items: center;
/* 垂直居中,避免序号/文字错位 */
}
.slot {
width: 21px;
height: 23px;
background: rgba(0, 106, 205, 0.22);
backdrop-filter: blur(1.5px);
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #68B5FF;
line-height: 23px;
/* 垂直居中文字 */
text-align: center;
font-style: normal;
}
.eq-name {
margin-left: 8px;
/* 增加与序号的间距 */
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
letter-spacing: 1px;
text-align: left;
font-style: normal;
}
.eqStatus {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.splitLine {
width: 1px;
height: 14px;
border: 1px solid #ADADAD;
margin: 0 8px;
/* 优化分割线间距 */
}
.yield {
height: 18px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #00FFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.proBarInfoEqInfoLeft {
display: flex;
align-items: center;
/* 序号和设备名垂直居中 */
}
.proBarInfoEqInfoRight {
display: flex;
align-items: center;
/* 状态/分割线/百分比垂直居中 */
}
.proBarWrapper {
position: relative;
height: 10px;
margin-top: 6px;
/* 进度条与上方信息的间距 */
border-radius: 5px;
/* 进度条圆角,优化视觉 */
overflow: hidden;
}
.proBarLine {
width: 100%;
height: 100%;
background: linear-gradient(65deg, rgba(82, 82, 82, 0) 0%, #ACACAC 100%);
opacity: 0.2;
}
.proBarLineTop {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: linear-gradient(65deg, rgba(53, 223, 247, 0) 0%, rgba(54, 220, 246, 0.92) 92%, #36F6E5 100%, #37ACF5 100%);
border-radius: 5px;
transition: width 0.3s ease;
/* 进度变化时添加过渡动画,更流畅 */
}
/* 图表相关样式保留 */
.chartImgBottom {
position: absolute;
bottom: 45px;
left: 58px;
}
.line {
display: inline-block;
position: absolute;
left: 57px;
bottom: 42px;
width: 1px;
height: 20px;
background-color: #00E8FF;
}
</style>
<style>
/* 全局 tooltip 样式(不使用 scoped确保生效 */
.production-status-chart-tooltip {
background: #0a2b4f77 !important;
border: none !important;
backdrop-filter: blur(12px);
}
.production-status-chart-tooltip * {
color: #fff !important;
}
</style>

View File

@@ -0,0 +1,358 @@
<template>
<div style="flex: 1">
<Container name="销售重点指标" icon="cockpitItemIcon" size="operatingBasic" topSize="middle">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 14px; display: flex;flex-direction: column; width: 100%;">
<!-- 2. .top 保持 flex无需固定高度自动跟随子元素拉伸 -->
<div class="top" style="display: flex; width: 100%;">
<top-item :height="367" :itemList="parentItemList" />
</div>
<div class="bottom"
style="display: flex; width: 100%;margin-top: 8px;background-color: rgba(249, 252, 255, 1);">
<!-- <top-item /> -->
<!-- <coreBottomBar /> -->
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
// import * as echarts from 'echarts'
import topItem from './operating-item.vue'
export default {
name: 'ProductionStatus',
components: { Container, topItem },
// mixins: [resize],
props: {
leftEqInfoData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
productionOverviewVo: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
type: Object,
default: () => ({})
}
},
data() {
return {
chart: null,
parentItemList: [
{
name: "利润总额", targetValue: 0, value: 0, proportion: 0,
route:'profitAnalysis'
},
{ name: "毛利率", targetValue: 0, value: 0, proportion: 0, route: 'profitAnalysis' },
{ name: "单价", targetValue: 0, value: 0, proportion: 0, route: 'cost/cost' },
{ name: "净价", targetValue: 0, value: 0, proportion: 0, route: 'cost/cost' },
{ name: "销量", targetValue: 0, value: 0, proportion: 0, route: 'profitAnalysis' },
{ name: "双镀面板", targetValue: 0, value: 0, proportion: 0, route: 'profitAnalysis' },
{ name: "溢价产品销量", targetValue: 0, value: 0, proportion: 0, route: 'profitAnalysis' }
]
}
},
watch: {
productionOverviewVo: {
handler(newValue, oldValue) {
this.updateChart()
},
deep: true // 若对象内属性变化需触发,需加 deep: true
}
},
mounted() {
// 初始化图表(若需展示图表,需在模板中添加对应 DOM
// this.$nextTick(() => this.updateChart())
},
beforeDestroy() {
// 销毁图表,避免内存泄漏
if (this.chart) {
this.chart.dispose()
this.chart = null
}
},
methods: {
updateChart() {
// 注意:原代码中图表依赖 id 为 "productionStatusChart" 的 DOM需在模板中补充否则会报错
// 示例:在 Container 内添加 <div id="productionStatusChart" style="height: 200px;"></div>
if (!document.getElementById('productionStatusChart')) return
if (this.chart) this.chart.dispose()
this.chart = echarts.init(document.getElementById('productionStatusChart'))
const data = [
this.productionOverviewVo.input || 0,
this.productionOverviewVo.output || 0,
this.productionOverviewVo.ng || 0,
this.productionOverviewVo.lowValue || 0,
this.productionOverviewVo.scrap || 0,
this.productionOverviewVo.inProcess || 0,
this.productionOverviewVo.engineer || 0
]
const option = {
type: 'bar',
grid: { left: 51, right: 40, top: 50, bottom: 45 },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
className: 'production-status-chart-tooltip'
},
xAxis: {
type: 'category',
offset: 8,
data: ['投入', '产出', '待判', '低价值', '报废', '在制', '实验片'],
axisTick: { show: false },
axisLine: { show: true, onZero: false, lineStyle: { color: '#00E8FF' } },
axisLabel: {
color: 'rgba(255,255,255,0.7)',
fontSize: 12,
interval: 0,
width: 38,
overflow: 'break'
}
},
yAxis: {
type: 'value',
name: '单位/片',
nameTextStyle: { color: 'rgba(255,255,255,0.7)', fontSize: 14, align: 'left' },
min: () => 0,
max: (value) => Math.ceil(value.max),
scale: true,
axisTick: { show: false },
axisLabel: { color: 'rgba(255,255,255,0.7)', fontSize: 12 },
splitLine: { lineStyle: { color: 'RGBA(24, 88, 100, 0.6)', type: 'dashed' } },
axisLine: { show: true, lineStyle: { color: '#00E8FF' } }
},
series: [
{
type: 'pictorialBar',
label: { show: true, position: 'top', distance: -3, color: '#89CDFF', fontSize: 11 },
symbolSize: [20, 8],
symbolOffset: [0, 5],
z: 20,
itemStyle: {
borderColor: '#3588C7',
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'RGBA(22, 89, 98, 1)' },
{ offset: 1, color: '#3588C7' }
])
},
data: data
},
{
type: 'bar',
barWidth: 20,
itemStyle: {
borderWidth: 1,
borderColor: '#3588C7',
opacity: 0.8,
color: {
x: 0, y: 0, x2: 0, y2: 1,
type: 'linear',
global: false,
colorStops: [
{ offset: 0, color: 'rgba(73,178,255,0)' },
{ offset: 0.5, color: 'rgba(0, 232, 255, .5)' },
{ offset: 1, color: 'rgba(0, 232, 255, 1)' }
]
}
},
tooltip: { show: false },
data: data
},
{
type: 'pictorialBar',
symbolSize: [20, 8],
symbolOffset: [0, -4],
z: 12,
symbolPosition: 'end',
itemStyle: {
borderColor: 'rgba(0, 232, 255, 1)',
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'RGBA(22, 89, 98, 1)' },
{ offset: 1, color: '#3588C7' }
])
},
tooltip: { show: false },
data: data
}
]
}
this.chart.setOption(option)
}
}
}
</script>
<style lang='scss' scoped>
/* 3. 核心:滚动容器样式(固定高度+溢出滚动+隐藏滚动条) */
.scroll-container {
/* 1. 固定容器高度根据页面布局调整示例300px超出则滚动 */
max-height: 210px;
/* 2. 溢出滚动:内容超出高度时显示滚动功能 */
overflow-y: auto;
/* 3. 隐藏横向滚动条(防止设备名过长导致横向滚动) */
overflow-x: hidden;
/* 4. 内边距:与标题栏和容器边缘对齐 */
padding: 10px 0;
/* 5. 隐藏滚动条(兼容主流浏览器) */
/* Chrome/Safari */
&::-webkit-scrollbar {
display: none;
}
/* Firefox */
scrollbar-width: none;
/* IE/Edge */
-ms-overflow-style: none;
}
/* 设备项样式优化:增加间距,避免拥挤 */
.proBarInfo {
display: flex;
flex-direction: column;
padding: 8px 27px;
/* 调整内边距,优化排版 */
margin-bottom: 10px;
/* 设备项之间的垂直间距 */
}
/* 原有样式保留,优化细节 */
.proBarInfoEqInfo {
display: flex;
justify-content: space-between;
align-items: center;
/* 垂直居中,避免序号/文字错位 */
}
.slot {
width: 21px;
height: 23px;
background: rgba(0, 106, 205, 0.22);
backdrop-filter: blur(1.5px);
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #68B5FF;
line-height: 23px;
/* 垂直居中文字 */
text-align: center;
font-style: normal;
}
.eq-name {
margin-left: 8px;
/* 增加与序号的间距 */
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
letter-spacing: 1px;
text-align: left;
font-style: normal;
}
.eqStatus {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.splitLine {
width: 1px;
height: 14px;
border: 1px solid #ADADAD;
margin: 0 8px;
/* 优化分割线间距 */
}
.yield {
height: 18px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #00FFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.proBarInfoEqInfoLeft {
display: flex;
align-items: center;
/* 序号和设备名垂直居中 */
}
.proBarInfoEqInfoRight {
display: flex;
align-items: center;
/* 状态/分割线/百分比垂直居中 */
}
.proBarWrapper {
position: relative;
height: 10px;
margin-top: 6px;
/* 进度条与上方信息的间距 */
border-radius: 5px;
/* 进度条圆角,优化视觉 */
overflow: hidden;
}
.proBarLine {
width: 100%;
height: 100%;
background: linear-gradient(65deg, rgba(82, 82, 82, 0) 0%, #ACACAC 100%);
opacity: 0.2;
}
.proBarLineTop {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: linear-gradient(65deg, rgba(53, 223, 247, 0) 0%, rgba(54, 220, 246, 0.92) 92%, #36F6E5 100%, #37ACF5 100%);
border-radius: 5px;
transition: width 0.3s ease;
/* 进度变化时添加过渡动画,更流畅 */
}
/* 图表相关样式保留 */
.chartImgBottom {
position: absolute;
bottom: 45px;
left: 58px;
}
.line {
display: inline-block;
position: absolute;
left: 57px;
bottom: 42px;
width: 1px;
height: 20px;
background-color: #00E8FF;
}
</style>
<style>
/* 全局 tooltip 样式(不使用 scoped确保生效 */
.production-status-chart-tooltip {
background: #0a2b4f77 !important;
border: none !important;
backdrop-filter: blur(12px);
}
.production-status-chart-tooltip * {
color: #fff !important;
}
</style>

View File

@@ -0,0 +1,499 @@
<template>
<div class="coreItem">
<!-- 单独渲染第一个item -->
<div class="item" v-if="itemList.length > 0">
<div class="unit">{{ itemList[0].unit }}</div>
<div class="item-content">
<div class="content-wrapper">
<div class="left">
<div class="number">{{ itemList[0].targetValue }}</div>
<div class="title">目标值</div>
</div>
<div class="line"></div>
<div class="right">
<!-- 实际值当前值颜色动态绑定 -->
<div class="number" :style="{ color: getColor(itemList[0].currentValue, itemList[0].targetValue) }">
{{ itemList[0].currentValue }}
</div>
<div class="title">当前值</div>
</div>
</div>
<div class="progress-group">
<div class="progress-container">
<!-- 进度条颜色和宽度动态绑定 -->
<div class="progress-bar" :style="{
width: itemList[0].progress + '%',
background: getColor(itemList[0].currentValue, itemList[0].targetValue)
}"></div>
</div>
</div>
<div class="yield" style="display: flex;justify-content: space-between;">
<div class="progress-percent">完成率</div>
<!-- 百分比颜色动态绑定 -->
<div class="progress-percent">
{{ itemList[0].progress }}%
</div>
</div>
</div>
</div>
<!-- 循环渲染剩余的item从索引1开始 -->
<div class="item groupData" style="display: flex;padding: 0;" v-for="(item, index) in itemList.slice(1)"
:key="index">
<div class="left" style="display: flex;align-items: start;gap: 4px;padding: 12px 0 0 12px;">
<div class="groupName">{{ item.unit }}</div>
<div class="left-target">
<div class="number">{{ item.targetValue }}</div>
<div class="title">目标值</div>
</div>
<div class="left-real">
<!-- 实际值颜色动态绑定 -->
<div class="number" :style="{ color: getColor(item.currentValue, item.targetValue) }">
{{ item.currentValue }}
</div>
<div class="title">实际值</div>
</div>
</div>
<div class="cityLine"></div>
<div class="right">
<!-- 顶部完成率颜色动态绑定 -->
<div class="groupName" :class="{
'bg-default': item.currentValue < item.targetValue, // 小于目标值:默认背景
'bg-green': item.currentValue >= item.targetValue // 大于等于目标值:绿色背景
}" style="font-size: 12px;display: flex;align-items: center;justify-content: flex-end;">
<div class="title">完成率</div>
<div class="yield" style="font-size: 22px;margin-bottom: 4px;">
{{ item.progress }}
</div>
<div class="unit">%</div>
</div>
<!-- 第一个城市进度 -->
<div class="right-city">
<div class="city">桐城</div>
<div class="city-progress-group">
<div class="city-progress-container">
<!-- 城市进度条颜色动态绑定 -->
<div class="city-progress-bar" :style="{
width: item.progress + '%',
background: getColor(item.currentValue, item.targetValue)
}"></div>
</div>
</div>
<div class="city-progress-yield" style="display: flex;justify-content: space-between;">
<!-- numerator颜色动态绑定 -->
<div class="numerator" :style="{ color: getColor(item.currentValue, item.targetValue) }">
12/13
</div>
<!-- 城市完成率颜色动态绑定 -->
<div class="city-yield" :style="{ color: getColor(item.currentValue, item.targetValue) }">
{{ item.progress }}%
</div>
</div>
</div>
<!-- 第二个城市进度 -->
<div class="right-city" style="margin-top: 2px;">
<div class="city">桐城</div>
<div class="city-progress-group">
<div class="city-progress-container">
<!-- 城市进度条颜色动态绑定 -->
<div class="city-progress-bar" :style="{
width: item.progress + '%',
background: getColor(item.currentValue, item.targetValue)
}"></div>
</div>
</div>
<div class="city-progress-yield" style="display: flex;justify-content: space-between;">
<!-- numerator颜色动态绑定 -->
<div class="numerator" :style="{ color: getColor(item.currentValue, item.targetValue) }">
12/13
</div>
<!-- 城市完成率颜色动态绑定 -->
<div class="city-yield" :style="{ color: getColor(item.currentValue, item.targetValue) }">
{{ item.progress }}%
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Container",
components: {},
props: ["name", "size", "icon"],
data() {
return {
progress: 90, // 进度值基础参数
itemList: [
{
unit: "总进度",
targetValue: 16,
currentValue: 14.5, // 小于目标值,应显示橙色
progress: 90
},
{
unit: "一组",
targetValue: 16,
currentValue: 17, // 大于目标值,应显示绿色
progress: 106
},
{
unit: "二组",
targetValue: 16,
currentValue: 16, // 等于目标值,应显示绿色
progress: 100
},
{
unit: "三组",
targetValue: 16,
currentValue: 15.2, // 小于目标值,应显示橙色
progress: 85
},
{
unit: "四组",
targetValue: 16,
currentValue: 18, // 大于目标值,应显示绿色
progress: 112
},
{
unit: "五组",
targetValue: 16,
currentValue: 14, // 小于目标值,应显示橙色
progress: 80
}
]
};
},
methods: {
// 颜色判断核心方法:实际值≥目标值返回绿色,否则返回橙色
getColor(currentValue, targetValue) {
return currentValue >= targetValue
? "rgba(98, 213, 180, 1)"
: "rgba(249, 164, 74, 1)";
}
}
};
</script>
<style scoped lang="scss">
/* 第一个item单独样式可选 */
.item:first-of-type {
// background: #f0f7ff; /* 示例背景色 */
}
.coreItem {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.item {
width: 220px;
height: 122px;
background: #f9fcff;
padding: 12px;
box-sizing: border-box;
.unit {
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;
}
.groupName {
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;
}
.left-target {
.number {
height: 22px;
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 24px;
color: rgba(103, 103, 103, 0.79);
line-height: 22px;
text-align: center;
font-style: normal;
}
.title {
height: 14px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #868687;
line-height: 14px;
text-align: center;
font-style: normal;
}
}
.left-real {
.number {
height: 22px;
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 24px;
line-height: 22px;
text-align: center;
font-style: normal;
/* 颜色由动态绑定控制,此处不设置固定值 */
}
.title {
height: 14px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #868687;
line-height: 14px;
text-align: center;
font-style: normal;
}
}
.right {
width: 100%;
.bg-default {
background-image: url("../../../assets/img/order-item-bg.png");
}
// 实际值 >= 目标值:绿色背景图
.bg-green {
background-image: url("../../../assets/img/order-item-greenbg.png");
}
.groupName {
width: 129px;
height: 32px;
// background: url(../../../assets/img/order-item-bg.png) no-repeat;
background-size: 100% 100%;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
color: #000000;
text-align: center;
.title {
margin-top: 10px;
height: 12px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
margin-right: 8px;
color: #000000;
line-height: 12px;
text-align: left;
font-style: normal;
}
.unit {
margin-top: 10px;
margin-right: 14px;
height: 12px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #000000;
line-height: 12px;
text-align: left;
font-style: normal;
}
}
.right-city {
.city {
height: 14px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #575757;
line-height: 14px;
text-align: left;
font-style: normal;
}
.title {
height: 14px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #868687;
line-height: 14px;
text-align: center;
font-style: normal;
}
}
}
.item-content {
display: flex;
flex-direction: column;
justify-content: space-between;
height: calc(100% - 26px);
}
.content-wrapper {
display: flex;
align-items: center;
justify-content: space-around;
flex: 1;
}
.line {
width: 1px;
height: 46px;
background: linear-gradient(to bottom,
rgba(255, 0, 0, 0),
rgba(40, 203, 151, 1));
}
.cityLine {
width: 1px;
margin-top: 40px;
height: 76px;
background: linear-gradient(to bottom,
rgba(255, 0, 0, 0),
#cbcbcb);
}
.left,
.right {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
flex: 1;
}
.number {
height: 22px;
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 24px;
color: rgba(103, 103, 103, 0.79);
line-height: 22px;
text-align: center;
font-style: normal;
}
.title {
height: 14px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #868687;
line-height: 14px;
text-align: center;
font-style: normal;
}
.progress-group {
display: flex;
align-items: center;
gap: 8px;
}
.progress-container {
width: 230px;
height: 10px;
background: #ECEFF7;
border-radius: 8px;
overflow: hidden;
}
.progress-bar {
height: 100%;
border-radius: 8px;
/* 背景色由动态绑定控制,移除固定值 */
}
.city-progress-group {
margin-top: 2px;
display: flex;
align-items: center;
gap: 8px;
}
.city-progress-container {
width: 112px;
height: 10px;
background: #ECEFF7;
border-radius: 8px;
overflow: hidden;
}
.city-progress-bar {
height: 100%;
border-radius: 8px;
opacity: 1;
/* 背景色由动态绑定控制,移除固定值 */
}
.progress-percent {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
line-height: 1;
/* 颜色由动态绑定控制,此处不设置固定值 */
}
.yield {
margin-top: 3px;
}
.numerator {
/* 颜色由动态绑定控制,此处不设置固定值 */
}
.city-yield {
/* 颜色由动态绑定控制,此处不设置固定值 */
}
}
.groupData {
position: relative;
overflow: hidden;
}
/* 右上角折现边框(主边框) */
.groupData::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
clip-path: polygon(0 0, calc(100% - 20px) 0, 100% 20px, 100% 100%, 0 100%);
}
/* 右上角折现细节 */
.groupData::after {
content: "";
position: absolute;
top: 0;
right: 0;
width: 20px;
height: 20px;
background: #EFF3F8;
transform: rotate(-45deg) translate(50%, -50%);
transform-origin: top right;
}
</style>

View File

@@ -0,0 +1,115 @@
<template>
<div style="flex: 1">
<bottomMiddleContainer name="订单进度跟踪·万m²" icon="cockpitItemIcon" size="bottomBasic">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div style="display: flex;gap: 9px;padding: 14px 16px;">
<orderItem />
</div>
</bottomMiddleContainer>
</div>
</template>
<script>
import bottomMiddleContainer from './bottomMiddleContainer.vue'
// import * as echarts from 'echarts'
import topItem from './top-product-item.vue'
import coreBottomBar from './productBottomBar.vue'
import orderItem from './order-bottom-leftItem.vue'
export default {
name: 'ProductionStatus',
components: { bottomMiddleContainer, topItem, coreBottomBar, orderItem },
// mixins: [resize],
props: {
leftEqInfoData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
productionOverviewVo: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
type: Object,
default: () => ({})
}
},
data() {
return {
maintenanceTasks: [
{ id: 1, eqName: '连续化', taskName: '例行维护', },
{ id: 2, eqName: '螺杆挤出', taskName: '例行维护', },
{ id: 2, eqName: '螺杆挤出', taskName: '例行维护', },
// { id: 2, eqName: '螺杆挤出', taskName: '例行维护', },
// { id: 2, eqName: '螺杆挤出', taskName: '例行维护', },
],
tableProps: [
// { prop: 'id', label: '序号', width: 50, align: 'center' },
{ prop: 'eqName', label: '攻坚指标', align: 'left' },
{ prop: 'taskName', label: '攻坚目标', align: 'left' },
{ prop: 'taskName', label: '当月实际', align: 'left' },
{ prop: 'taskName', label: '累计', align: 'left' },
]
}
},
watch: {
productionOverviewVo: {
handler(newValue, oldValue) {
this.updateChart()
},
deep: true // 若对象内属性变化需触发,需加 deep: true
}
},
mounted() {
// 初始化图表(若需展示图表,需在模板中添加对应 DOM
// this.$nextTick(() => this.updateChart())
},
beforeDestroy() {
// 销毁图表,避免内存泄漏
},
methods: {
}
}
</script>
<style lang='scss' scoped>
.legend-group {
display: flex;
align-items: center;
gap: 10px;
/* 两个图例项之间的间距 */
}
/* 单个图例项 */
.legend-item {
display: flex;
align-items: center;
}
/* 图例小方块 */
.legend-dot {
display: inline-block;
width: 12px;
height: 12px;
margin-right: 5px;
border-radius: 2px;
/* 可选:轻微圆角 */
}
/* 已完成(绿色) */
.legend-dot.done {
background-color: #4CAF50;
/* 绿色,可根据需求调整色值 */
}
/* 未完成(橙色) */
.legend-dot.pending {
background-color: #FF9800;
/* 橙色,可根据需求调整色值 */
}
/* 图例文字 */
.legend-text {
font-size: 14px;
color: #333;
}
</style>

View File

@@ -0,0 +1,266 @@
<template>
<!-- 动态绑定 ref使用 props 中的 chartRef而非硬编码 -->
<div :ref="chartRef" id="coreLineChart" style="height: 100%; width: 100%;"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
name: 'Container',
props: {
// 1. 重命名 propsref → chartRef避免关键字冲突同时定义类型和默认值
chartRef: {
type: String,
required: true, // 强制父组件传值,避免获取不到 DOM
// validator: (value) => {
// // 验证ref 名不能为空,确保有效
// return value.trim() !== '';
// }
}
},
components: {},
data() {
return {};
},
computed: {},
mounted() {
this.$nextTick(() => {
this.initData();
});
},
methods: {
initData() {
// 2. 动态获取 DOM通过 props 中的 chartRef 拿到对应的 ref 元素
const chartDom = this.$refs[this.chartRef];
if (!chartDom) {
console.error(`图表容器未找到!请确认父组件传递的 chartRef 为 "${this.chartRef}"`);
return;
}
const myChart = echarts.init(chartDom);
// 自定义颜色数组(与系列一一对应)
const customColors = [
'rgba(113, 100, 255, 1)',
'rgba(40, 138, 255, 1)',
'rgba(118, 218, 190, 1)',
'rgba(255, 206, 106, 1)',
];
const option = {
// 标题配置(主标题+副标题)
title: [
{
text: '月度',
left: 'center',
top: '35%',
textStyle: {
fontSize: 24,
letterSpacing: 5,
color: 'rgba(0, 0, 0, 0.85)',
fontFamily: 'PingFangSC, PingFang SC'
}
},
{
text: '单位:万m²',
left: 'center',
top: '50%',
textStyle: {
fontSize: 16,
color: 'rgba(0, 0, 0, 0.55)',
fontFamily: 'PingFangSC, PingFang SC'
}
}
],
series: [
{
name: '销量',
type: 'pie',
radius: ['60%', '80%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
label: {
show: true,
position: 'outside',
distance: 10,
formatter: '{c}万m²\n{b}',
textStyle: {
fontSize: 14,
color: 'rgba(0, 0, 0, 0.7)',
fontFamily: 'PingFangSC, PingFang SC',
lineHeight: 1.5
}
},
labelLine: {
show: true,
length: 0,
length2: 30,
lineStyle: {
color: (params) => customColors[params.dataIndex]
}
},
itemStyle: {
color: (params) => customColors[params.dataIndex]
},
data: [
{
value: 1048, name: '单镀面板',
label: {
normal: {
align: 'left',
distanceToLabelLine: 2,
formatter: (params) => [
`{b|${params.value.toLocaleString()}}`,
`{hr|■}{c|${params.name}}`
].join('\n'),
rich: {
hr: {
color: 'rgba(39, 96, 255, 1)',
fontSize: 20,
padding: [26, 8, 0, 0]
},
b: {
color: 'rgba(0, 0, 0, 0.75)',
fontSize: 18,
padding: [0, 0, 0, 0]
},
c: {
color: 'rgba(64, 64, 64, 1)',
fontSize: 14,
padding: [30, 0, 0, -5]
}
}
}
},
labelLine: {
lineStyle: { color: 'rgba(39, 96, 255, 1)' }
},
itemStyle: { color: 'rgba(39, 96, 255, 1)' }
},
{
value: 735, name: '双镀面板',
label: {
normal: {
align: 'left',
distanceToLabelLine: 2,
formatter: (params) => [
`{b|${params.value.toLocaleString()}}`,
`{hr|■}{c|${params.name}}`
].join('\n'),
rich: {
hr: {
color: 'rgba(40, 138, 255, 1)',
fontSize: 20,
padding: [6, 8, 0, 10]
},
b: {
color: 'rgba(0, 0, 0, 0.75)',
fontSize: 18,
padding: [-30, 0, 0, 30]
},
c: {
color: 'rgba(64, 64, 64, 1)',
fontSize: 14,
padding: [10, 0, 0, -5]
}
}
}
},
labelLine: {
length: 0,
length2: 50,
lineStyle: { color: 'rgba(40, 138, 255, 1)' }
},
itemStyle: { color: 'rgba(40, 138, 255, 1)' }
},
{
value: 580, name: '丝印打孔',
label: {
normal: {
align: 'left',
distanceToLabelLine: 2,
formatter: (params) => [
`{b|${params.value.toLocaleString()}}`,
`{hr|■}{c|${params.name}}`
].join('\n'),
rich: {
hr: {
color: 'rgba(118, 218, 190, 1)',
fontSize: 20,
padding: [36, 5, 0, 0]
},
b: {
color: 'rgba(0, 0, 0, 0.75)',
fontSize: 18,
padding: [0, 10, 0, 0]
},
c: {
color: 'rgba(64, 64, 64, 1)',
fontSize: 14,
padding: [40, -5, 0, 0]
}
}
}
},
labelLine: {
lineStyle: { color: 'rgba(118, 218, 190, 1)' }
},
itemStyle: { color: 'rgba(118, 218, 190, 1)' }
},
{
value: 484, name: '无印打孔',
label: {
normal: {
align: 'left',
distanceToLabelLine: 2,
formatter: (params) => [
`{b|${params.value.toLocaleString()}}`,
`{hr|■}{c|${params.name}}`
].join('\n'),
rich: {
hr: {
color: 'rgba(255, 206, 106, 1)',
fontSize: 20,
padding: [36, 5, 0, 0]
},
b: {
color: 'rgba(0, 0, 0, 0.75)',
fontSize: 18,
padding: [0, 20, 0, 0]
},
c: {
color: 'rgba(64, 64, 64, 1)',
fontSize: 14,
padding: [40, 15, 0, 0]
}
}
}
},
labelLine: {
lineStyle: { color: 'rgba(255, 206, 106, 1)' }
},
itemStyle: { color: 'rgba(255, 206, 106, 1)' }
}
]
}
]
};
option && myChart.setOption(option);
// 窗口缩放监听
window.addEventListener('resize', () => {
myChart.resize();
});
// 组件销毁清理
this.$once('hook:destroyed', () => {
window.removeEventListener('resize', () => {
myChart.resize();
});
myChart.dispose();
});
}
},
};
</script>

View File

@@ -0,0 +1,398 @@
<template>
<div style="flex: 1">
<Container name="溢价产品完成情况" icon="cockpitItemIcon" size="operatingBasic" topSize="middle">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px 8px 16px; display: flex;flex-direction: column; width: 100%;">
<!-- 2. .top 保持 flex无需固定高度自动跟随子元素拉伸 -->
<div class="top" style="display: flex; width: 100%;">
<top-item :itemList="parentItemList" />
</div>
</div>
<div class="bottom-content" style="display: flex;flex-direction: column; width: 100%;">
<div class="content-top">
<svg-icon style="font-size: 32px; margin-left: 16px" icon-class="cockpitItemIcon" />
<span style="
width: 412px;
height: 32px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 24px;
color: #000000;
line-height: 60px;
letter-spacing: 3px;
text-align: left;
font-style: normal;
">
各产品类型销量占比
</span>
</div>
<div class="pie" style="display: flex;gap: 8px;margin-top: 68px;padding: 0 16px;">
<div class="month-pie" style="height: 212px;width: 382px;background: #F9FCFF;">
<pieChart :chartRef=" 'monthChart' " />
</div>
<div class="-pie" style="height: 212px;width: 382px;background: #F9FCFF;">
<pieChartTwo :chartRef="'yearChart'" />
</div>
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
import pieChart from './pieChart.vue'
import pieChartTwo from './pieChart.vue'
// import * as echarts from 'echarts'
import topItem from './operating-item.vue'
import coreBottomBar from './coreBottomBar.vue'
export default {
name: 'ProductionStatus',
components: { Container, topItem, coreBottomBar, pieChart, pieChartTwo },
// mixins: [resize],
props: {
leftEqInfoData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
productionOverviewVo: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
type: Object,
default: () => ({})
}
},
data() {
return {
chart: null,
parentItemList: [
{ name: "月度", targetValue: 0, value: 0, proportion: 0, route:'profitAnalysis' },
{ name: "年度", targetValue: 0, value: 0, proportion: 0, route: 'profitAnalysis' },
// { unit: "单价", targetValue: 20, currentValue: 16, progress: 80 },
// { unit: "净价", targetValue: 20, currentValue: 16, progress: 80 },
// { unit: "销量", targetValue: 20, currentValue: 16, progress: 80 },
// { unit: "双镀面板", targetValue: 15, currentValue: 13.8, progress: 92 },
// { unit: "溢价产品销量", targetValue: 15, currentValue: 13.8, progress: 92 }
]
}
},
watch: {
productionOverviewVo: {
handler(newValue, oldValue) {
this.updateChart()
},
deep: true // 若对象内属性变化需触发,需加 deep: true
}
},
mounted() {
// 初始化图表(若需展示图表,需在模板中添加对应 DOM
// this.$nextTick(() => this.updateChart())
},
beforeDestroy() {
// 销毁图表,避免内存泄漏
if (this.chart) {
this.chart.dispose()
this.chart = null
}
},
methods: {
updateChart() {
// 注意:原代码中图表依赖 id 为 "productionStatusChart" 的 DOM需在模板中补充否则会报错
// 示例:在 Container 内添加 <div id="productionStatusChart" style="height: 200px;"></div>
if (!document.getElementById('productionStatusChart')) return
if (this.chart) this.chart.dispose()
this.chart = echarts.init(document.getElementById('productionStatusChart'))
const data = [
this.productionOverviewVo.input || 0,
this.productionOverviewVo.output || 0,
this.productionOverviewVo.ng || 0,
this.productionOverviewVo.lowValue || 0,
this.productionOverviewVo.scrap || 0,
this.productionOverviewVo.inProcess || 0,
this.productionOverviewVo.engineer || 0
]
const option = {
type: 'bar',
grid: { left: 51, right: 40, top: 50, bottom: 45 },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
className: 'production-status-chart-tooltip'
},
xAxis: {
type: 'category',
offset: 8,
data: ['投入', '产出', '待判', '低价值', '报废', '在制', '实验片'],
axisTick: { show: false },
axisLine: { show: true, onZero: false, lineStyle: { color: '#00E8FF' } },
axisLabel: {
color: 'rgba(255,255,255,0.7)',
fontSize: 12,
interval: 0,
width: 38,
overflow: 'break'
}
},
yAxis: {
type: 'value',
name: '单位/片',
nameTextStyle: { color: 'rgba(255,255,255,0.7)', fontSize: 14, align: 'left' },
min: () => 0,
max: (value) => Math.ceil(value.max),
scale: true,
axisTick: { show: false },
axisLabel: { color: 'rgba(255,255,255,0.7)', fontSize: 12 },
splitLine: { lineStyle: { color: 'RGBA(24, 88, 100, 0.6)', type: 'dashed' } },
axisLine: { show: true, lineStyle: { color: '#00E8FF' } }
},
series: [
{
type: 'pictorialBar',
label: { show: true, position: 'top', distance: -3, color: '#89CDFF', fontSize: 11 },
symbolSize: [20, 8],
symbolOffset: [0, 5],
z: 20,
itemStyle: {
borderColor: '#3588C7',
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'RGBA(22, 89, 98, 1)' },
{ offset: 1, color: '#3588C7' }
])
},
data: data
},
{
type: 'bar',
barWidth: 20,
itemStyle: {
borderWidth: 1,
borderColor: '#3588C7',
opacity: 0.8,
color: {
x: 0, y: 0, x2: 0, y2: 1,
type: 'linear',
global: false,
colorStops: [
{ offset: 0, color: 'rgba(73,178,255,0)' },
{ offset: 0.5, color: 'rgba(0, 232, 255, .5)' },
{ offset: 1, color: 'rgba(0, 232, 255, 1)' }
]
}
},
tooltip: { show: false },
data: data
},
{
type: 'pictorialBar',
symbolSize: [20, 8],
symbolOffset: [0, -4],
z: 12,
symbolPosition: 'end',
itemStyle: {
borderColor: 'rgba(0, 232, 255, 1)',
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'RGBA(22, 89, 98, 1)' },
{ offset: 1, color: '#3588C7' }
])
},
tooltip: { show: false },
data: data
}
]
}
this.chart.setOption(option)
}
}
}
</script>
<style lang='scss' scoped>
/* 3. 核心:滚动容器样式(固定高度+溢出滚动+隐藏滚动条) */
.scroll-container {
/* 1. 固定容器高度根据页面布局调整示例300px超出则滚动 */
max-height: 210px;
/* 2. 溢出滚动:内容超出高度时显示滚动功能 */
overflow-y: auto;
/* 3. 隐藏横向滚动条(防止设备名过长导致横向滚动) */
overflow-x: hidden;
/* 4. 内边距:与标题栏和容器边缘对齐 */
padding: 10px 0;
/* 5. 隐藏滚动条(兼容主流浏览器) */
/* Chrome/Safari */
&::-webkit-scrollbar {
display: none;
}
/* Firefox */
scrollbar-width: none;
/* IE/Edge */
-ms-overflow-style: none;
}
/* 设备项样式优化:增加间距,避免拥挤 */
.proBarInfo {
display: flex;
flex-direction: column;
padding: 8px 27px;
/* 调整内边距,优化排版 */
margin-bottom: 10px;
/* 设备项之间的垂直间距 */
}
/* 原有样式保留,优化细节 */
.proBarInfoEqInfo {
display: flex;
justify-content: space-between;
align-items: center;
/* 垂直居中,避免序号/文字错位 */
}
.slot {
width: 21px;
height: 23px;
background: rgba(0, 106, 205, 0.22);
backdrop-filter: blur(1.5px);
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #68B5FF;
line-height: 23px;
/* 垂直居中文字 */
text-align: center;
font-style: normal;
}
.eq-name {
margin-left: 8px;
/* 增加与序号的间距 */
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
letter-spacing: 1px;
text-align: left;
font-style: normal;
}
.eqStatus {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.splitLine {
width: 1px;
height: 14px;
border: 1px solid #ADADAD;
margin: 0 8px;
/* 优化分割线间距 */
}
.yield {
height: 18px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #00FFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.proBarInfoEqInfoLeft {
display: flex;
align-items: center;
/* 序号和设备名垂直居中 */
}
.proBarInfoEqInfoRight {
display: flex;
align-items: center;
/* 状态/分割线/百分比垂直居中 */
}
.proBarWrapper {
position: relative;
height: 10px;
margin-top: 6px;
/* 进度条与上方信息的间距 */
border-radius: 5px;
/* 进度条圆角,优化视觉 */
overflow: hidden;
}
.proBarLine {
width: 100%;
height: 100%;
background: linear-gradient(65deg, rgba(82, 82, 82, 0) 0%, #ACACAC 100%);
opacity: 0.2;
}
.proBarLineTop {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: linear-gradient(65deg, rgba(53, 223, 247, 0) 0%, rgba(54, 220, 246, 0.92) 92%, #36F6E5 100%, #37ACF5 100%);
border-radius: 5px;
transition: width 0.3s ease;
/* 进度变化时添加过渡动画,更流畅 */
}
/* 图表相关样式保留 */
.chartImgBottom {
position: absolute;
bottom: 45px;
left: 58px;
}
.line {
display: inline-block;
position: absolute;
left: 57px;
bottom: 42px;
width: 1px;
height: 20px;
background-color: #00E8FF;
}
.bottom-content{
position: relative;
// height: 280px;
padding: 0 0 6px 0;
.content-top {
top: 0;
left: -5px;
position: absolute;
height: 60px;
width: 804px;
background: url(../../../assets/img/topTileMiddle.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
}
</style>
<style>
/* 全局 tooltip 样式(不使用 scoped确保生效 */
.production-status-chart-tooltip {
background: #0a2b4f77 !important;
border: none !important;
backdrop-filter: blur(12px);
}
.production-status-chart-tooltip * {
color: #fff !important;
}
</style>

View File

@@ -0,0 +1,273 @@
<template>
<div style="position: relative;">
<div class="legend">
<span class="legend-item-line">
<span class="line target"></span>
目标
</span>
<span class="legend-item-line">
<span class="line real"></span>
实际
</span>
</div>
<div ref="cockpitEffChip" id="coreLineChart" style="height: 219px; width: 100%;"></div>
</div>
</template>
<script>
import * as echarts from 'echarts';
export default {
name: 'Container',
components: {},
data() {
return {};
},
computed: {},
mounted() {
this.$nextTick(() => {
this.initData();
});
},
methods: {
initData() {
// 优先使用 ref 获取 DOM避免 id 冲突
const chartDom = this.$refs.cockpitEffChip;
if (!chartDom) {
console.error('图表容器未找到!');
return;
}
const myChart = echarts.init(chartDom);
const option = {
// color: ['#80FFA5', '#00DDFF', '#37A2FF', '#FF0087', '#FFBF00'],
// title: {
// text: 'Gradient Stacked Area Chart'
// },
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
grid: {
top: 20,
bottom: 20,
// top: 10,
// bottom: 20,
right: 25,
},
// legend: {
// data: ['Line 1', 'Line 2', 'Line 3', 'Line 4', 'Line 5']
// },
// toolbox: {
// feature: {
// saveAsImage: {}
// }
// },
xAxis: [
{
type: 'category',
boundaryGap: false,
axisTick: {
show: false
},
axisLine: {
show: true,
onZero: false,
lineStyle: {
color: 'rgba(0, 0, 0, 0.15)'
}
},
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
interval: 0,
width: 38,
overflow: 'break'
},
data: ['6月', '7月', '8月', '9月', '10月', '11月']
}
],
yAxis: {
type: 'value',
// name: '单位/片',
nameTextStyle: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 14,
align: 'left'
},
min: function (value) {
return 0
},
max: function (value) {
return Math.ceil(value.max)
},
scale: true,
axisTick: {
show: false
},
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12
},
splitLine: {
lineStyle: {
color: 'rgba(0, 0, 0, 0.15)',
// type: 'dashed'
}
},
axisLine: {
show: true,
lineStyle: {
color: 'rgba(0, 0, 0, 0.15)'
}
}
},
series: [
{
name: 'Line 1',
type: 'line',
stack: 'Total',
symbol: 'circle', // 点的形状circle为圆形
lineStyle: {
color: 'rgba(255, 132, 0, .5)',
},
itemStyle: {
color: 'rgba(255, 132, 0, .5)',
borderColor: 'rgba(255, 132, 0, .5)', // 数据点边框色(白色)
borderWidth: 2, // 数据点边框宽度
},
areaStyle: {
opacity: 0.5,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(255, 132, 0, .4)',
},
{
offset: 1,
color: 'rgba(18, 255, 245, 0)',
},
]),
},
// emphasis: { focus: 'series' },
data: [140, 232, 101, 264, 90, 340, 250]
},
{
name: 'Line 2',
type: 'line',
stack: 'Total',
symbol: 'circle', // 点的形状circle为圆形
lineStyle: {
color: 'rgba(98, 213, 180, .5)',
},
itemStyle: {
color: 'rgba(98, 213, 180, .5)',
borderColor: 'rgba(98, 213, 180, .5)', // 数据点边框色(白色)
borderWidth: 2, // 数据点边框宽度
},
areaStyle: {
opacity: 0.5,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(98, 213, 180,.4)',
},
{
offset: 1,
color: 'rgba(18, 255, 245, 0)',
},
]),
},
// emphasis: { focus: 'series' },
data: [120, 282, 111, 234, 220, 340, 310]
},
]
};
option && myChart.setOption(option);
// 监听窗口缩放
window.addEventListener('resize', () => {
myChart.resize();
});
// 组件销毁时清理
this.$once('hook:destroyed', () => {
window.removeEventListener('resize', () => {
myChart.resize();
});
myChart.dispose();
});
}
},
};
</script>
<style lang="scss" scoped>
.legend {
position: absolute;
right: 10px;
top: -5px;
}
.legend-item-line {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: rgba(0, 0, 0, 0.8);
text-align: left;
font-style: normal;
margin-right: 20px;
/* 增加两个图例项之间的间距 */
position: relative;
padding-left: 8px;
/* 给文字左侧增加内边距 */
.line {
position: absolute;
left: 5px;
/* 调整线条位置 */
top: 10px;
display: inline-block;
width: 12px;
height: 2px;
margin-right: 4px;
}
.target {
background: rgba(91, 230, 190, 1);
}
.real {
background: rgba(255, 132, 0, 1);
}
&::before {
content: "";
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 10px;
/* 关键:增加图例圆点和文字之间的间距 */
margin-bottom: 2px;
background-color: rgba(255, 132, 0, 1);
}
}
.legend-item-line:nth-child(1) {
&::before {
content: "";
display: inline-block;
width: 6px;
height: 6px;
margin-right: 10px;
/* 关键:增加图例圆点和文字之间的间距 */
margin-bottom: 2px;
background-color: rgba(91, 230, 190, 1);
}
}
</style>

View File

@@ -0,0 +1,186 @@
<template>
<div class="coreBar" style="width: 100%;">
<!-- 循环渲染itemdata中配置的每一项对应一个卡片 -->
<div class="barTop">
<div class="title">生产指标趋势</div>
<div class="button-group">
<!-- 按钮1单价 -->
<div class="item-button" :class="{ active: activeButton === 0 }" @click="activeButton = 0">
总成本
</div>
<!-- 分割线0单价右侧 -->
<div class="button-line lineOne" v-if="activeButton !== 0 && activeButton !== 1"></div>
<!-- 按钮2净价 -->
<div class="item-button" :class="{ active: activeButton === 1 }" @click="activeButton = 1">
原片成本
</div>
<!-- 分割线1净价右侧 -->
<div class="button-line lineTwo" v-if="activeButton !== 1 && activeButton !== 2"></div>
<!-- 按钮3销量 -->
<div class="item-button" :class="{ active: activeButton === 2 }" @click="activeButton = 2">
加工成本
</div>
<!-- 分割线2销量右侧 -->
<div class="button-line lineThree" v-if="activeButton !== 2 && activeButton !== 3"></div>
<!-- 按钮4双镀产品 -->
<div class="item-button" style="width: 75px;" :class="{ active: activeButton === 3 }" @click="activeButton = 3">
原片成品率
</div>
<div class="button-line lineFour" v-if="activeButton !== 3 && activeButton !== 4"></div>
<!-- 按钮5投入产出率 -->
<div class="item-button" style="width: 75px;" :class="{ active: activeButton === 4 }" @click="activeButton = 4">
投入产出率
</div>
</div>
</div>
<div class="lineBottom" style="height: 219px; width: 100%">
<coreLineChart style="height: 219px; width: 500px" />
</div>
</div>
</template>
<script>
import coreLineChart from './productBar.vue';
export default {
name: "Container",
components: { coreLineChart },
props: ["name", "size", "icon"],
data() {
return {
// 所有item的数据配置后续修改直接操作这个数组即可
activeButton: 0, // 初始激活第一个按钮索引0
itemList: [
{
unit: "单价·元/m²", // 标题
targetValue: 16, // 左侧目标值
currentValue: 14.5, // 右侧当前值(可根据实际需求修改)
progress: 90, // 进度百分比
},
{
unit: "净价·元/m²",
targetValue: 16,
currentValue: 15.2,
progress: 85,
},
{
unit: "销量·万m²",
targetValue: 20,
currentValue: 16,
progress: 80,
},
{
unit: "双镀面板·万m²",
targetValue: 15,
currentValue: 13.8,
progress: 92,
},
],
};
},
computed: {},
methods: {
},
};
</script>
<style scoped lang="scss">
.coreBar {
display: flex;
flex-direction: column;
padding: 12px;
// grid-template-columns: 1fr 1fr;
// grid-template-rows: 1fr 1fr;
// gap: 8px;
.barTop {
display: flex;
gap: 40px;
.title {
// width: 124px;
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;
}
.button-group {
display: flex;
position: relative;
// justify-content: space-around;
gap: 2px;
width: 327px;
align-items: center;
height: 24px;
background: #ecf4fe;
border-radius: 12px;
.button-line {
position: absolute;
// top: 5px;
// left: 54px;
width: 1px;
height: 14px;
border: 1px solid rgba(11, 88, 255, 0.25);
}
.lineOne {
top: 5px;
left: 57px;
}
.lineTwo {
top: 5px;
left: 118px;
}
.lineThree {
top: 5px;
left: 177px;
}
.lineFour {
top: 5px;
left: 268px;
}
// .button-line:nth-child(3) {
// top: 5px;
// left: 216px;
// }
.item-button {
cursor: pointer;
width: 59px;
height: 24px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #0b58ff;
line-height: 24px;
text-align: center;
font-style: normal;
}
// .item-button:nth-child(6){
// width: 75px;
// }
// .item-button:nth-child(8) {
// width: 75px;
// }
.item-button.active {
width: 59px;
height: 24px;
background: #3071ff;
border-radius: 12px;
color: #ffffff; // 文字变白,提高对比度
font-weight: 500; // 文字加粗,突出激活状态
}
}
}
}
</style>

View File

@@ -0,0 +1,208 @@
<template>
<div class="coreItem" :style="{ 'height': height + 'px' }">
<!-- v-for 动态生成每个 item -->
<div class="item" v-for="(item, index) in itemList" :key="index" :class="{ 'has-route': item.route }" @click="handleRoute(item.route)">
<div class="unit">{{ item.name }}</div>
<div class="item-content">
<!-- 左右内容容器 -->
<div class="content-wrapper">
<div class="left">
<div class="number">{{ item.targetValue || 0 }}</div>
<div class="title">目标值</div>
</div>
<div class="line"></div>
<div class="right">
<!-- 实际值颜色动态绑定 -->
<div class="number" :style="{ 'color': item.completed === 0 ? '#FF8400' : 'rgba(54, 181, 138, .7)' }">
{{ item.value || 0 }}
</div>
<div class="title">实际值</div>
</div>
<div class="line"></div>
<div class="right">
<!-- 完成率颜色动态绑定 -->
<div class="number" :style="{ 'color': item.completed === 0 ? '#FF8400' : 'rgba(54, 181, 138, .7)' }">
{{ item.proportion !== null && item.proportion !== undefined ? (item.proportion * 100) + '%' : '0%' }}
</div>
<div class="title">完成率</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Container",
components: {},
props: ['itemList', 'height'],
data() {
return {
progress: 90, // 进度值,方便统一控制
};
},
watch: {
itemList: {
handler(newList) {
// 当 itemList 变化时执行的逻辑(如重新计算、更新视图等)
console.log("子组件接收的 itemList 已更新:", newList);
},
deep: true // 深度监听:若 itemList 内部对象属性变化(如 currentValue 改变),也能触发监听
}
},
computed: {},
methods: {
handleRoute(route) {
this.$router.push({
path:route
})
}
},
};
</script>
<style scoped lang="scss">
.coreItem {
display: flex;
// height: 382px;
// overflow: hidden;
flex-wrap: wrap;
// grid-template-columns: 1fr 1fr;
// grid-template-rows: 1fr 1fr;
justify-content: flex-start;
align-content: flex-start;
gap: 8px;
// overflow-y: auto;
// padding: 8px;
/* 2. 隐藏 Chrome/Safari 滚动条 */
&::-webkit-scrollbar {
width: 0;
/* 滚动条宽度设为0 */
height: 0;
/* 横向滚动条(如需)也隐藏 */
}
/* 3. 隐藏 Firefox 滚动条 */
scrollbar-width: none;
}
.item {
width: 376px;
height: 110px;
background: #f9fcff;
padding: 12px;
box-sizing: border-box;
&.has-route {
cursor: pointer; // 有路由时显示指针,提示可点击
box-shadow: 0 0 0 transparent; // 默认无阴影
&:hover {
box-shadow: 0px 4px 12px 2px #B5CDE5; // hover 阴影
transform: translateY(-2px); // 轻微上浮,增强交互感
}
}
.unit {
// width: 124px;
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;
margin-bottom: 8px;
}
.item-content {
display: flex;
flex-direction: column;
justify-content: space-between;
height: calc(100% - 26px);
}
.content-wrapper {
display: flex;
align-items: center;
justify-content: space-around;
flex: 1;
}
.line {
width: 1px;
height: 46px;
background: linear-gradient(to bottom,
rgba(255, 0, 0, 0),
rgba(40, 203, 151, 1));
}
.left,
.right {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 5px;
flex: 1;
}
.number {
height: 22px;
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 26px;
color: rgba(103, 103, 103, 0.79);
line-height: 22px;
text-align: center;
font-style: normal;
}
.title {
height: 14px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: #868687;
line-height: 14px;
text-align: center;
font-style: normal;
}
/* 进度条和百分比的外层容器 */
.progress-group {
display: flex;
align-items: center;
gap: 8px;
/* 进度条和百分比的间距 */
}
.progress-container {
width: 190px;
height: 10px;
background: #ECEFF7;
border-radius: 8px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: #28CB97;
border-radius: 8px;
opacity: 0.6;
}
/* 百分比文本样式 */
.progress-percent {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #868687;
line-height: 1;
}
}
</style>

View File

@@ -0,0 +1,206 @@
<template>
<div class="coreItem" :style="{'height':height+ 'px'} ">
<!-- v-for 动态生成每个 item -->
<div class="item" v-for="(item, index) in itemList" :key="index" :class="{ 'has-route': item.route }"
@click="handleRoute(item.route)">
<div class="unit">{{ item.name }}</div>
<div class="item-content">
<!-- 左右内容容器 -->
<div class="content-wrapper">
<div class="left">
<div class="number">{{ item.targetValue ||0 }}</div>
<div class="title">目标值</div>
</div>
<div class="line"></div>
<div class="right">
<!-- 实际值颜色动态绑定 -->
<div class="number" :style="{ 'color': item.completed === 0 ? '#FF8400' : 'rgba(54, 181, 138, .7)' }">
{{ item.value || 0 }}
</div>
<div class="title">实际值</div>
</div>
<div class="line"></div>
<div class="right">
<!-- 完成率颜色动态绑定 -->
<div class="number" :style="{ 'color': item.completed === 0 ? '#FF8400' : 'rgba(54, 181, 138, .7)' }">
{{ item.proportion !== null && item.proportion !== undefined ? (item.proportion * 100) + '%' : '0%' }}
</div>
<div class="title">完成率</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Container",
components: {},
props: ['itemList','height'],
data() {
return {
progress: 90, // 进度值,方便统一控制
};
},
watch: {
itemList: {
handler(newList) {
// 当 itemList 变化时执行的逻辑(如重新计算、更新视图等)
console.log("子组件接收的 itemList 已更新:", newList);
},
deep: true // 深度监听:若 itemList 内部对象属性变化(如 currentValue 改变),也能触发监听
}
},
computed: {},
methods: {
handleRoute(route) {
this.$router.push({
path: route
})
}
},
};
</script>
<style scoped lang="scss">
.coreItem {
display: flex;
// height: 382px;
// overflow: hidden;
flex-wrap: wrap;
// grid-template-columns: 1fr 1fr;
// grid-template-rows: 1fr 1fr;
justify-content: flex-start;
align-content: flex-start;
gap: 8px;
// overflow-y: auto;
// padding: 8px;
/* 2. 隐藏 Chrome/Safari 滚动条 */
&::-webkit-scrollbar {
width: 0;
/* 滚动条宽度设为0 */
height: 0;
/* 横向滚动条(如需)也隐藏 */
}
/* 3. 隐藏 Firefox 滚动条 */
scrollbar-width: none;
}
.item {
width: 376px;
height: 95px;
background: #f9fcff;
padding: 12px;
box-sizing: border-box;
&.has-route {
cursor: pointer; // 有路由时显示指针,提示可点击
box-shadow: 0 0 0 transparent; // 默认无阴影
&:hover {
box-shadow: 0px 4px 12px 2px #B5CDE5; // hover 阴影
transform: translateY(-2px); // 轻微上浮,增强交互感
}
}
.unit {
// width: 124px;
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;
margin-bottom: 8px;
}
.item-content {
display: flex;
flex-direction: column;
justify-content: space-between;
height: calc(100% - 26px);
}
.content-wrapper {
display: flex;
align-items: center;
justify-content: space-around;
flex: 1;
}
.line {
width: 1px;
height: 46px;
background: linear-gradient(to bottom,
rgba(255, 0, 0, 0),
rgba(40, 203, 151, 1));
}
.left,
.right {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 5px;
flex: 1;
}
.number {
height: 22px;
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 26px;
color: rgba(103, 103, 103, 0.79);
line-height: 22px;
text-align: center;
font-style: normal;
}
.title {
height: 14px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: #868687;
line-height: 14px;
text-align: center;
font-style: normal;
}
/* 进度条和百分比的外层容器 */
.progress-group {
display: flex;
align-items: center;
gap: 8px;
/* 进度条和百分比的间距 */
}
.progress-container {
width: 190px;
height: 10px;
background: #ECEFF7;
border-radius: 8px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: #28CB97;
border-radius: 8px;
opacity: 0.6;
}
/* 百分比文本样式 */
.progress-percent {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #868687;
line-height: 1;
}
}
</style>

View File

@@ -0,0 +1,379 @@
<template>
<div class="coreBar">
<div class="barTop">
<div class="right-container">
<div class="legend">
<span class="legend-item">
<span class="legend-icon line yield"></span>
完成率
</span>
<span class="legend-item">
<span class="legend-icon square target"></span>
目标
</span>
<span class="legend-item close-item">
<span class="legend-icon square achieved"></span>
</span>
<span class="legend-item close-item">
<span class="legend-icon square unachieved"></span>
实际
</span>
</div>
<!-- 按钮组 -->
<div class="button-group">
<div class="item-button category-btn">
<span class="item-text" style="width: 88px;">类目选择</span>
</div>
<div class="dropdown-container">
<div class="item-button profit-btn active"
@click.stop="isDropdownShow = !isDropdownShow">
<span class="item-text profit-text">{{ selectedProfit || '请选择' }}</span>
<span class="dropdown-arrow" :class="{ 'rotate': isDropdownShow }"></span>
</div>
<div class="dropdown-options" v-if="isDropdownShow">
<div class="dropdown-option" v-for="(item, index) in profitOptions" :key="index"
@click.stop="selectProfit(item)">
{{ item }}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="lineBottom" style="height: 100%; width: 100%">
<!-- 调用方法传递数据通过 :key 触发组件更新 -->
<operatingLineBar :chart-data="dataChart" :key="selectedProfit + JSON.stringify(formattedData)"
style="height: 99%; width: 100%" />
</div>
</div>
</template>
<script>
import operatingLineBar from './operatingBottomLineBar.vue';
export default {
name: "Container",
components: { operatingLineBar },
props: [
"formattedData" // 接收父组件处理后的数据
],
data() {
return {
activeButton: 0,
isDropdownShow: false,
dataChart:{},
selectedProfit: '', // 初始为空字符串
profitOptions: ['利润总额', '毛利率', '产量', '销量', '成本', '营业收入'],
};
},
methods: {
// 核心:将原计算属性改为方法,主动处理数据
getChartData() {
const currentKey = this.selectedProfit
console.log(this.formattedData,'22222');
// 严格判断数据有效性
if (
!this.formattedData ||
typeof this.formattedData !== 'object' ||
!this.formattedData[currentKey]
) {
return { series: [], allPlaceNames: [] };
}
const targetData = this.formattedData[currentKey];
const allPlaceNames = this.formattedData.allPlaceNames || [];
// 生成图表系列数据
const series = [
{
name: '完成率',
type: 'line',
yAxisIndex: 1,
lineStyle: { color: 'rgba(40, 138, 255, .5)', width: 2 },
itemStyle: {
color: 'rgba(40, 138, 255, 1)',
borderColor: 'rgba(40, 138, 255, 1)',
borderWidth: 2,
radius: 4
},
data: targetData.map(item => item.proportion),
symbol: 'circle',
symbolSize: 6
},
{
name: '目标',
type: 'bar',
yAxisIndex: 0,
barWidth: 18,
itemStyle: {
color: '#2889FF',
borderRadius: [4, 4, 0, 0]
},
data: targetData.map(item => item.targetValue)
},
{
name: '实际',
type: 'bar',
yAxisIndex: 0,
barWidth: 18,
itemStyle: {
color: (params) => {
const completed = targetData[params.dataIndex].completed;
return completed === 1 ? 'rgba(255, 132, 0, 1)' : 'rgba(40, 203, 151, 1)';
},
borderRadius: [4, 4, 0, 0]
},
data: targetData.map(item => item.value)
}
];
// return { series, allPlaceNames };
this.dataChart = { series, allPlaceNames };
console.log(this.dataChart,'dataChart');
},
selectProfit(item) {
this.selectedProfit = item;
this.activeButton = 1;
this.isDropdownShow = false;
this.getChartData()
}
},
mounted() {
// 初始化时如果有数据,默认选中第一个选项
// this.selectedProfit = '总额'
// this.getChartData()
// 点击外部关闭下拉菜单
const handleOutsideClick = (e) => {
if (!this.$el.contains(e.target)) {
this.isDropdownShow = false;
}
};
document.addEventListener('click', handleOutsideClick);
this.$once('hook:beforeDestroy', () => {
document.removeEventListener('click', handleOutsideClick);
});
},
watch: {
// 深度监听formattedData变化确保异步加载的数据能被捕获
formattedData: {
handler(newVal) {
if (newVal && Object.keys(newVal).length) {
// 数据加载完成后,默认选中第一个选项
this.selectedProfit = this.profitOptions[0];
this.getChartData()
}
},
deep: true,
immediate: true // 初始化时立即执行一次
},
// selectedProfit: {
// handler(newVal) {
// this.getChartData()
// },
// deep: true,
// immediate: true // 初始化时立即执行一次
// },
}
};
</script>
<style scoped lang="scss">
/* 样式保持不变 */
.coreBar {
display: flex;
flex-direction: column;
width: 100%;
padding: 12px;
.barTop {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 16px;
width: 100%;
.right-container {
display: flex;
align-items: center;
gap: 24px;
margin-right: 46px;
}
.legend {
display: flex;
gap: 16px;
align-items: center;
margin: 0;
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: rgba(0, 0, 0, 0.8);
text-align: left;
font-style: normal;
white-space: nowrap;
}
.legend-icon {
display: inline-block;
}
.legend-icon.line {
width: 12px;
height: 2px;
position: relative;
&::before {
position: absolute;
content: "";
top: -2px;
left: 3px;
width: 6px;
border-radius: 50%;
height: 6px;
background-color: rgba(40, 138, 255, 1);
}
}
.legend-icon.square {
width: 8px;
height: 8px;
}
.yield {
background: rgba(40, 138, 255, 1);
}
.target {
background: #2889FF;
}
.achieved {
background: rgba(40, 203, 151, 1);
}
.unachieved {
background: rgba(255, 132, 0, 1);
}
.legend-item.close-item+.legend-item.close-item {
margin-left: -8px;
}
}
.button-group {
display: flex;
position: relative;
gap: 2px;
align-items: center;
height: 24px;
background: #ecf4fe;
margin: 0;
.dropdown-container {
position: relative;
z-index: 10;
}
.item-button {
cursor: pointer;
height: 24px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
line-height: 24px;
font-style: normal;
letter-spacing: 2px;
padding: 0 24px 0 12px;
overflow: hidden;
.item-text {
display: inline-block;
}
}
.category-btn {
width: 88px;
border-top-left-radius: 12px;
border-bottom-left-radius: 12px;
background: #ffffff;
color: #0b58ff;
text-align: center;
}
.profit-btn {
width: 118px;
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
position: relative;
background: #ffffff;
color: #0b58ff;
text-align: left;
&.active {
background: #3071ff;
color: rgba(249, 252, 255, .8);
}
.profit-text {
text-align: left;
width: 100%;
}
}
.dropdown-arrow {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-left: 6px solid currentColor;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
border-right: 4px solid transparent;
transition: transform 0.2s ease;
&.rotate {
transform: rotate(90deg);
}
}
.dropdown-options {
position: absolute;
top: 100%;
right: 0;
margin-top: 2px;
width: 118px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
.dropdown-option {
padding: 6px 12px;
font-size: 12px;
color: #333;
cursor: pointer;
text-align: left;
letter-spacing: 1px;
&:hover {
background: #f5f7fa;
color: #3071ff;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,288 @@
<template>
<div style="flex: 1">
<Container name="趋势图" icon="cockpitItemIcon" size="operatingLarge" topSize="large">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex;width: 100%;">
<div class="bottom" style="height: 330px; display: flex; width: 100%;background-color: rgba(249, 252, 255, 1);">
<!-- <top-item /> -->
<profitBar :formattedData="formatData" />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
// import * as echarts from 'echarts'
import profitBar from './profitBar.vue'
export default {
name: 'ProductionStatus',
components: { Container, profitBar },
// mixins: [resize],
props: {
bottomChartData: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
type: Array,
default: () => []
}
},
data() {
return {
chart: null,
formatData:{},
}
},
watch: {
// 监听原始数据变化自动更新图表不直接调用initChart而是通过数据变化触发
bottomChartData: {
handler() {
this.formatData = this.formatAllData();
console.log(this.formatData,'this.formattedData');
// this.updateChart(); // 用updateChart替代initChart语义更清晰
},
deep: true,
immediate: true // 初始化时立即执行一次
}
},
mounted() {
// 初始化图表(若需展示图表,需在模板中添加对应 DOM
// this.$nextTick(() => this.updateChart())
},
methods: {
cleanPlaceName(place) {
return place?.replace('分公司', '') || ''; // 匹配“分公司”并替换为空兼容null/undefined
},
formatAllData() {
const dataSource = this.bottomChartData;
if (!Array.isArray(dataSource) || dataSource.length === 0) return { allPlaceNames: [], ...this.getDefaultEmptyData() };
// 提取所有分公司名x轴数据
const allPlaceNames = dataSource.map(item => this.cleanPlaceName(item.name)).filter(Boolean)
const PlaceNames = dataSource.map(item => item.name).filter(Boolean);
// 定义指标与 productFactors 字段的映射关系
const indicatorMap = {
'利润总额': 'profit',
'毛利率': 'profitRate',
'产量': 'products',
'销量': 'sales',
'成本': 'cost',
'营业收入': 'income'
};
// 格式化单个指标数据
const formatSingleType = (indicatorKey) => {
const field = indicatorMap[indicatorKey];
return PlaceNames.map(place => {
const targetItem = dataSource.find(item => item.name === place);
return {
targetValue: targetItem?.[`${field}Target`] || 0, // 对应指标的目标值字段(如 profitTarget
value: targetItem?.[field] || 0, // 对应指标的实际值
proportion: (targetItem?.[`${field}Proportion`] || 0) * 100, // 对应指标的占比(转百分比)
completed: targetItem?.[`${field}Completed`] ?? 0 // 状态字段(复用分公司的 completed
};
});
};
// 生成最终格式化数据
return {
allPlaceNames,
'利润总额': formatSingleType('利润总额'),
'毛利率': formatSingleType('毛利率'),
'产量': formatSingleType('产量'),
'销量': formatSingleType('销量'),
'成本': formatSingleType('成本'),
'营业收入': formatSingleType('营业收入')
};
},
// 生成默认空数据(避免无数据时图表报错)
getDefaultEmptyData() {
return {
'利润总额': [],
'毛利率': [],
'产量': [],
'销量': [],
'成本': [],
'营业收入': []
};
},
getValueColor(completed) { // 保留原颜色方法
return completed === 1 ? 'rgba(255, 132, 0, 1)' : 'rgba(40, 203, 151, 1)';
}
}
}
</script>
<style lang='scss' scoped>
/* 3. 核心:滚动容器样式(固定高度+溢出滚动+隐藏滚动条) */
.scroll-container {
/* 1. 固定容器高度根据页面布局调整示例300px超出则滚动 */
max-height: 210px;
/* 2. 溢出滚动:内容超出高度时显示滚动功能 */
overflow-y: auto;
/* 3. 隐藏横向滚动条(防止设备名过长导致横向滚动) */
overflow-x: hidden;
/* 4. 内边距:与标题栏和容器边缘对齐 */
padding: 10px 0;
/* 5. 隐藏滚动条(兼容主流浏览器) */
/* Chrome/Safari */
&::-webkit-scrollbar {
display: none;
}
/* Firefox */
scrollbar-width: none;
/* IE/Edge */
-ms-overflow-style: none;
}
/* 设备项样式优化:增加间距,避免拥挤 */
.proBarInfo {
display: flex;
flex-direction: column;
padding: 8px 27px;
/* 调整内边距,优化排版 */
margin-bottom: 10px;
/* 设备项之间的垂直间距 */
}
/* 原有样式保留,优化细节 */
.proBarInfoEqInfo {
display: flex;
justify-content: space-between;
align-items: center;
/* 垂直居中,避免序号/文字错位 */
}
.slot {
width: 21px;
height: 23px;
background: rgba(0, 106, 205, 0.22);
backdrop-filter: blur(1.5px);
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #68B5FF;
line-height: 23px;
/* 垂直居中文字 */
text-align: center;
font-style: normal;
}
.eq-name {
margin-left: 8px;
/* 增加与序号的间距 */
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
letter-spacing: 1px;
text-align: left;
font-style: normal;
}
.eqStatus {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.splitLine {
width: 1px;
height: 14px;
border: 1px solid #ADADAD;
margin: 0 8px;
/* 优化分割线间距 */
}
.yield {
height: 18px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #00FFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.proBarInfoEqInfoLeft {
display: flex;
align-items: center;
/* 序号和设备名垂直居中 */
}
.proBarInfoEqInfoRight {
display: flex;
align-items: center;
/* 状态/分割线/百分比垂直居中 */
}
.proBarWrapper {
position: relative;
height: 10px;
margin-top: 6px;
/* 进度条与上方信息的间距 */
border-radius: 5px;
/* 进度条圆角,优化视觉 */
overflow: hidden;
}
.proBarLine {
width: 100%;
height: 100%;
background: linear-gradient(65deg, rgba(82, 82, 82, 0) 0%, #ACACAC 100%);
opacity: 0.2;
}
.proBarLineTop {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: linear-gradient(65deg, rgba(53, 223, 247, 0) 0%, rgba(54, 220, 246, 0.92) 92%, #36F6E5 100%, #37ACF5 100%);
border-radius: 5px;
transition: width 0.3s ease;
/* 进度变化时添加过渡动画,更流畅 */
}
/* 图表相关样式保留 */
.chartImgBottom {
position: absolute;
bottom: 45px;
left: 58px;
}
.line {
display: inline-block;
position: absolute;
left: 57px;
bottom: 42px;
width: 1px;
height: 20px;
background-color: #00E8FF;
}
</style>
<style>
/* 全局 tooltip 样式(不使用 scoped确保生效 */
.production-status-chart-tooltip {
background: #0a2b4f77 !important;
border: none !important;
backdrop-filter: blur(12px);
}
.production-status-chart-tooltip * {
color: #fff !important;
}
</style>

View File

@@ -0,0 +1,235 @@
<template>
<div style="flex: 1">
<Container name="利润数据总览" icon="cockpitItemIcon" size="profitTopBasic" topSize="middle">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex;flex-direction: column; width: 100%;">
<!-- 2. .top 保持 flex无需固定高度自动跟随子元素拉伸 -->
<div class="top" style="display: flex; width: 100%;">
<top-item :itemList="targetItemList" />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
// import * as echarts from 'echarts'
import topItem from './profit-item.vue'
export default {
name: 'ProductionStatus',
components: { Container, topItem },
// mixins: [resize],
props: {
profitTotalData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
},
data() {
return {
parentItemList: [
{ name: "利润总额", targetValue: 0, value: 0, proportion: 0, completed: 1, },
{ name: "毛利率", targetValue: 0, value: 0, proportion: 0, completed:1, },
]
}
},
computed: {
targetItemList() {
// 基于 parentItemList 的名称,匹配 productAndSaleData 中对应名称的数据
return this.parentItemList.map(parentItem => {
// 按名称精准匹配name 完全一致才关联)
const productItem = this.profitTotalData.find(
item => item.name === parentItem.name
) || {};
// 优先使用匹配到的 productItem 数据,缺失则用 parentItem 的默认值0
return {
name: parentItem.name, // 保留原名称(销量、产量等)
targetValue: productItem.targetValue ?? parentItem.targetValue ?? 0,
value: productItem.value ?? parentItem.value ?? 0,
proportion: productItem.proportion ?? parentItem.proportion ?? 0,
completed: productItem.completed ?? parentItem.completed ?? 1,
route: parentItem.route // 路由沿用 parentItem 的配置
};
});
}
},
mounted() {
// 初始化图表(若需展示图表,需在模板中添加对应 DOM
// this.$nextTick(() => this.updateChart())
},
methods: {
}
}
</script>
<style lang='scss' scoped>
/* 3. 核心:滚动容器样式(固定高度+溢出滚动+隐藏滚动条) */
.scroll-container {
/* 1. 固定容器高度根据页面布局调整示例300px超出则滚动 */
max-height: 210px;
/* 2. 溢出滚动:内容超出高度时显示滚动功能 */
overflow-y: auto;
/* 3. 隐藏横向滚动条(防止设备名过长导致横向滚动) */
overflow-x: hidden;
/* 4. 内边距:与标题栏和容器边缘对齐 */
padding: 10px 0;
/* 5. 隐藏滚动条(兼容主流浏览器) */
/* Chrome/Safari */
&::-webkit-scrollbar {
display: none;
}
/* Firefox */
scrollbar-width: none;
/* IE/Edge */
-ms-overflow-style: none;
}
/* 设备项样式优化:增加间距,避免拥挤 */
.proBarInfo {
display: flex;
flex-direction: column;
padding: 8px 27px;
/* 调整内边距,优化排版 */
margin-bottom: 10px;
/* 设备项之间的垂直间距 */
}
/* 原有样式保留,优化细节 */
.proBarInfoEqInfo {
display: flex;
justify-content: space-between;
align-items: center;
/* 垂直居中,避免序号/文字错位 */
}
.slot {
width: 21px;
height: 23px;
background: rgba(0, 106, 205, 0.22);
backdrop-filter: blur(1.5px);
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #68B5FF;
line-height: 23px;
/* 垂直居中文字 */
text-align: center;
font-style: normal;
}
.eq-name {
margin-left: 8px;
/* 增加与序号的间距 */
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
letter-spacing: 1px;
text-align: left;
font-style: normal;
}
.eqStatus {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.splitLine {
width: 1px;
height: 14px;
border: 1px solid #ADADAD;
margin: 0 8px;
/* 优化分割线间距 */
}
.yield {
height: 18px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #00FFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.proBarInfoEqInfoLeft {
display: flex;
align-items: center;
/* 序号和设备名垂直居中 */
}
.proBarInfoEqInfoRight {
display: flex;
align-items: center;
/* 状态/分割线/百分比垂直居中 */
}
.proBarWrapper {
position: relative;
height: 10px;
margin-top: 6px;
/* 进度条与上方信息的间距 */
border-radius: 5px;
/* 进度条圆角,优化视觉 */
overflow: hidden;
}
.proBarLine {
width: 100%;
height: 100%;
background: linear-gradient(65deg, rgba(82, 82, 82, 0) 0%, #ACACAC 100%);
opacity: 0.2;
}
.proBarLineTop {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: linear-gradient(65deg, rgba(53, 223, 247, 0) 0%, rgba(54, 220, 246, 0.92) 92%, #36F6E5 100%, #37ACF5 100%);
border-radius: 5px;
transition: width 0.3s ease;
/* 进度变化时添加过渡动画,更流畅 */
}
/* 图表相关样式保留 */
.chartImgBottom {
position: absolute;
bottom: 45px;
left: 58px;
}
.line {
display: inline-block;
position: absolute;
left: 57px;
bottom: 42px;
width: 1px;
height: 20px;
background-color: #00E8FF;
}
</style>
<style>
/* 全局 tooltip 样式(不使用 scoped确保生效 */
.production-status-chart-tooltip {
background: #0a2b4f77 !important;
border: none !important;
backdrop-filter: blur(12px);
}
.production-status-chart-tooltip * {
color: #fff !important;
}
</style>

View File

@@ -0,0 +1,221 @@
<template>
<div class="coreBar">
<div class="barTop">
<!-- 标题单独左对齐 -->
<!-- <div class="title">销售指标趋势</div> -->
<!-- 关键新增右侧容器包裹图例和按钮组实现整体靠右 -->
<div class="right-container">
<div class="legend">
<span class="legend-item">
<span class="legend-icon line yield"></span>
产销率
</span>
<span class="legend-item">
<span class="legend-icon square target"></span>
销量
</span>
<span class="legend-item close-item">
<span class="legend-icon square achieved"></span>
产量
</span>
</div>
</div>
</div>
<div class="lineBottom" style="height: 100%; width: 100%">
<psiLineBar :saleAndProductData="saleAndProductData" style="height: 99%; width: 100%" />
</div>
</div>
</template>
<script>
import psiLineBar from './psiLineBar.vue';
export default {
name: "Container",
components: { psiLineBar },
props: [ "saleAndProductData"],
data() {
return {
};
},
computed: {},
methods: {},
};
</script>
<style scoped lang="scss">
.coreBar {
display: flex;
flex-direction: column;
width: 100%;
padding: 12px;
.barTop {
display: flex;
justify-content: flex-end; // 标题左、右侧容器右,整体两端对齐
align-items: center; // 垂直居中,避免上下错位
gap: 16px; // 标题与右侧容器的最小间距,防止拥挤
width: 100%; // 确保占满父容器,实现两端对齐
.title {
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;
// 标题固定在左侧,不挤压右侧空间
white-space: nowrap;
}
// 1. 右侧容器:包裹图例和按钮组,整体靠右
.right-container {
display: flex;
align-items: center; // 图例和按钮组垂直居中
gap: 24px; // 图例与按钮组的间距,避免贴紧
margin-right: 46px; // 右侧整体留边,与原按钮组边距一致
}
// 2. 图例:在右侧容器内横向排列
.legend {
display: flex;
gap: 16px; // 图例项之间间距,避免重叠
align-items: center;
// 移除原margin-left避免位置偏移
margin: 0;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: rgba(0, 0, 0, 0.8);
text-align: left;
font-style: normal;
white-space: nowrap; // 防止图例文字换行
}
.legend-icon {
display: inline-block;
}
.legend-icon.line {
width: 12px;
height: 2px;
position: relative;
&::before {
position: absolute;
content: "";
top: -2px;
left: 3px;
width: 6px;
border-radius: 50%;
height: 6px;
background-color: rgba(40, 138, 255, 1);
}
}
.legend-icon.square {
width: 8px;
height: 8px;
}
// 图例颜色
.yield {
background: rgba(40, 138, 255, 1);
}
.target {
background: #2889FF;
}
.achieved {
background: rgba(40, 203, 151, 1);
}
.unachieved {
background: rgba(255, 132, 0, 1);
}
.legend-item.close-item+.legend-item.close-item {
margin-left: -8px; // 负margin抵消默认gap数值可根据需求调整
// 若原gap是16px想让两者间距变为8px设置 margin-left: -8px 即可
}
// 3. 按钮组:在右侧容器内,保留原有样式
.button-group {
display: flex;
position: relative;
gap: 2px;
width: 198px;
align-items: center;
height: 24px;
background: #ecf4fe;
// border-radius: 12px;
// 移除原margin-right由右侧容器统一控制
margin: 0;
.item-button {
cursor: pointer;
// width: 142px;
height: 24px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #0b58ff;
line-height: 24px;
text-align: center;
font-style: normal;
letter-spacing: 8px; // 确保文字间距生效
padding-left: 8px; // 抵消letter-spacing导致的文字左偏
&:nth-child(1) {
// 第一个按钮(类目选择):左圆角 + 适配文字的宽度
border-top-left-radius: 12px;
border-bottom-left-radius: 12px;
width: 75px; // 与模板中按钮的 width 保持一致
}
&:nth-child(2) {
// 第二个按钮(原料):右圆角 + 适配文字的宽度
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
width: 88px; // 与模板中按钮的 width 保持一致
}
&:nth-child(2)::after {
content: "";
position: absolute;
top: 50%;
right: 16px; // 三角形距离右侧的距离
transform: translateY(-50%);
// 三角形绘制(蓝色,朝右)
width: 0;
height: 0;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
border-left: 6px solid #0b58ff; // 颜色与文字一致
}
}
.item-button.active {
// width: 142px;
height: 24px;
background: #3071ff;
// border-radius: 12px;
color: rgba(249, 252, 255, .8);
font-weight: 500;
&:nth-child(2)::after {
border-left-color: #ffffff;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,267 @@
<template>
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 400px;"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
components: {},
data() {
return {
myChart: null,
resizeHandler: null
};
},
props: ["saleAndProductData"],
watch: {
// 监听数据变化,动态更新图表
saleAndProductData: {
handler() {
this.updateChart();
},
deep: true,
immediate: true
}
},
mounted() {
this.$nextTick(() => {
this.initChart();
});
},
beforeDestroy() {
// 清理资源
this.myChart && this.myChart.dispose();
window.removeEventListener('resize', this.resizeHandler);
},
methods: {
// 初始化图表实例
initChart() {
const chartDom = this.$refs.cockpitEffChip;
if (!chartDom) {
console.error('图表容器未找到!');
return;
}
this.myChart = echarts.init(chartDom);
this.resizeHandler = () => this.myChart?.resize();
window.addEventListener('resize', this.resizeHandler);
},
// 解析数据提取地名和对应数值统一去掉“基地XX”后缀
parseData() {
// const { saleValues = [], productValues = [], productAndSaleRates = [] } = this.saleAndProductData || {};
const xData = []; // 统一X轴地名
const saleData = []; // 销量数据
const productData = []; // 产量数据
const rateData = []; // 产销率数据(转为百分比)
// 第一步:提取所有地名(以销量数据为准,其他数据按地名匹配)
if (Array.isArray(this.saleAndProductData) && this.saleAndProductData.length) {
this.saleAndProductData.forEach(item => {
// const key = Object.keys(item)[0];
const place = item.name.replace('分公司', ''); // 提取地名(如“宜兴基地销量”→“宜兴”)
xData.push(place);
saleData.push(item.saleValue); // 销量值(保留两位小数)
productData.push(item.productValue);
rateData.push(item.saleAndProductProportion *100 ); // 转为百分比后保留两位小数
});
}
// // 第二步匹配产量数据按X轴地名顺序
// if (Array.isArray(productValues) && productValues.length) {
// xData.forEach(place => {
// const targetItem = productValues.find(item => {
// const key = Object.keys(item)[0];
// return key.includes(place); // 匹配包含该地名的产量键
// });
// productData.push(targetItem ? (Number(targetItem[Object.keys(targetItem)[0]]).toFixed(2) * 1) : 0);
// });
// }
// // 第三步:匹配产销率数据(转为百分比,保留两位小数)
// if (Array.isArray(productAndSaleRates) && productAndSaleRates.length) {
// xData.forEach(place => {
// const targetItem = productAndSaleRates.find(item => {
// const key = Object.keys(item)[0];
// return key.includes(place); // 匹配包含该地名的产销率键
// });
// const rate = targetItem ? (Number(targetItem[Object.keys(targetItem)[0]]).toFixed(4) * 100) : 0;
// rateData.push(rate.toFixed(2) * 1); // 转为百分比后保留两位小数
// });
// }
return { xData, saleData, productData, rateData };
},
// 动态更新图表
updateChart() {
if (!this.myChart) return;
// 解析数据
const { xData, saleData, productData, rateData } = this.parseData();
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
},
formatter: (params) => {
let html = `${params[0].axisValue}<br/>`;
params.forEach(item => {
const unit = item.seriesName === '产销率' ? '%' : '万㎡';
html += `${item.marker} ${item.seriesName}: ${item.value ? item.value :0}${unit}<br/>`;
});
return html;
}
},
grid: {
top: 30,
bottom: 30,
right: 70,
left: 40,
},
xAxis: [
{
type: 'category',
boundaryGap: true,
axisTick: { show: false },
axisLine: {
show: true,
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
},
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
interval: 0,
padding: [5, 0, 0, 0]
},
data: xData // 动态X轴统一地名
}
],
yAxis: [
{
type: 'value',
name: '万㎡',
nameTextStyle: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
align: 'right'
},
min: 0,
max: (value) => value.max > 0 ? Math.ceil(value.max * 1.1) : 10,
scale: false,
axisTick: { show: false },
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
formatter: '{value}'
},
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
splitNumber: 4
},
{
type: 'value',
nameTextStyle: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
align: 'left'
},
min: 0,
max: 100,
axisTick: { show: false },
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
formatter: '{value}%'
},
splitLine: { show: false },
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
splitNumber: 4
}
],
series: [
// 1. 产销率折线图右侧Y轴
{
name: '产销率',
type: 'line',
yAxisIndex: 1,
lineStyle: {
color: 'rgba(40, 138, 255, .5)',
width: 2
},
itemStyle: {
color: 'rgba(40, 138, 255, 1)',
borderColor: 'rgba(40, 138, 255, 1)',
borderWidth: 2,
radius: 4
},
areaStyle: {
opacity: 0.2,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(40, 138, 255, .9)' },
{ offset: 1, color: 'rgba(255, 132, 0, 0)' }
])
},
data: rateData, // 动态产销率数据
symbol: 'circle',
symbolSize: 6
},
// 2. 销量柱状图左侧Y轴
{
name: '销量',
type: 'bar',
yAxisIndex: 0,
barWidth: 24,
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(130, 204, 255, 1)' },
{ offset: 1, color: 'rgba(75, 157, 255, 1)' }
]
},
borderRadius: [4, 4, 0, 0],
borderWidth: 0
},
data: saleData // 动态销量数据
},
// 3. 产量柱状图左侧Y轴
{
name: '产量',
type: 'bar',
yAxisIndex: 0,
barWidth: 24,
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(174, 239, 224, 1)' },
{ offset: 1, color: 'rgba(118, 218, 190, 1)' }
]
},
borderRadius: [4, 4, 0, 0],
borderWidth: 0
},
data: productData // 动态产量数据
}
]
};
this.myChart.setOption(option);
}
},
};
</script>

View File

@@ -0,0 +1,217 @@
<template>
<div style="flex: 1">
<Container name="趋势图" icon="cockpitItemIcon" size="operatingLarge" topSize="large">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex;width: 100%;">
<div class="bottom" style="height: 378px; display: flex; width: 100%;background-color: rgba(249, 252, 255, 1);">
<!-- <top-item /> -->
<psiBar :saleAndProductData="saleAndProductData" />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
// import * as echarts from 'echarts'
import psiBar from './psiBar.vue'
export default {
name: 'ProductionStatus',
components: { Container, psiBar },
// mixins: [resize],
props: {
saleAndProductData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
},
data() {
return {
chart: null
}
},
watch: {
saleAndProductData: {
handler(newValue, oldValue) {
},
deep: true // 若对象内属性变化需触发,需加 deep: true
}
},
mounted() {
},
methods: {
}
}
</script>
<style lang='scss' scoped>
/* 3. 核心:滚动容器样式(固定高度+溢出滚动+隐藏滚动条) */
.scroll-container {
/* 1. 固定容器高度根据页面布局调整示例300px超出则滚动 */
max-height: 210px;
/* 2. 溢出滚动:内容超出高度时显示滚动功能 */
overflow-y: auto;
/* 3. 隐藏横向滚动条(防止设备名过长导致横向滚动) */
overflow-x: hidden;
/* 4. 内边距:与标题栏和容器边缘对齐 */
padding: 10px 0;
/* 5. 隐藏滚动条(兼容主流浏览器) */
/* Chrome/Safari */
&::-webkit-scrollbar {
display: none;
}
/* Firefox */
scrollbar-width: none;
/* IE/Edge */
-ms-overflow-style: none;
}
/* 设备项样式优化:增加间距,避免拥挤 */
.proBarInfo {
display: flex;
flex-direction: column;
padding: 8px 27px;
/* 调整内边距,优化排版 */
margin-bottom: 10px;
/* 设备项之间的垂直间距 */
}
/* 原有样式保留,优化细节 */
.proBarInfoEqInfo {
display: flex;
justify-content: space-between;
align-items: center;
/* 垂直居中,避免序号/文字错位 */
}
.slot {
width: 21px;
height: 23px;
background: rgba(0, 106, 205, 0.22);
backdrop-filter: blur(1.5px);
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #68B5FF;
line-height: 23px;
/* 垂直居中文字 */
text-align: center;
font-style: normal;
}
.eq-name {
margin-left: 8px;
/* 增加与序号的间距 */
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
letter-spacing: 1px;
text-align: left;
font-style: normal;
}
.eqStatus {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.splitLine {
width: 1px;
height: 14px;
border: 1px solid #ADADAD;
margin: 0 8px;
/* 优化分割线间距 */
}
.yield {
height: 18px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #00FFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.proBarInfoEqInfoLeft {
display: flex;
align-items: center;
/* 序号和设备名垂直居中 */
}
.proBarInfoEqInfoRight {
display: flex;
align-items: center;
/* 状态/分割线/百分比垂直居中 */
}
.proBarWrapper {
position: relative;
height: 10px;
margin-top: 6px;
/* 进度条与上方信息的间距 */
border-radius: 5px;
/* 进度条圆角,优化视觉 */
overflow: hidden;
}
.proBarLine {
width: 100%;
height: 100%;
background: linear-gradient(65deg, rgba(82, 82, 82, 0) 0%, #ACACAC 100%);
opacity: 0.2;
}
.proBarLineTop {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: linear-gradient(65deg, rgba(53, 223, 247, 0) 0%, rgba(54, 220, 246, 0.92) 92%, #36F6E5 100%, #37ACF5 100%);
border-radius: 5px;
transition: width 0.3s ease;
/* 进度变化时添加过渡动画,更流畅 */
}
/* 图表相关样式保留 */
.chartImgBottom {
position: absolute;
bottom: 45px;
left: 58px;
}
.line {
display: inline-block;
position: absolute;
left: 57px;
bottom: 42px;
width: 1px;
height: 20px;
background-color: #00E8FF;
}
</style>
<style>
/* 全局 tooltip 样式(不使用 scoped确保生效 */
.production-status-chart-tooltip {
background: #0a2b4f77 !important;
border: none !important;
backdrop-filter: blur(12px);
}
.production-status-chart-tooltip * {
color: #fff !important;
}
</style>

View File

@@ -0,0 +1,193 @@
<template>
<div class="coreItem" :style="{ 'height': height + 'px' }">
<!-- v-for 动态生成每个 item -->
<div class="item" v-for="(item, index) in itemList" :key="index">
<div class="unit">{{ item.name }}</div>
<div class="item-content">
<!-- 左右内容容器 -->
<div class="content-wrapper">
<div class="left">
<div class="number">{{ item.targetValue || 0 }}</div>
<div class="title">目标值</div>
</div>
<div class="line"></div>
<div class="right">
<!-- 实际值颜色动态绑定 -->
<div class="number" :style="{ 'color': item.completed === 0 ? '#FF8400' : 'rgba(54, 181, 138, .7)' }">
{{ item.value || 0 }}
</div>
<div class="title">实际值</div>
</div>
<div class="line"></div>
<div class="right">
<!-- 完成率颜色动态绑定 -->
<div class="number" :style="{ 'color': item.completed === 0 ? '#FF8400' : 'rgba(54, 181, 138, .7)' }">
{{ item.proportion !== null && item.proportion !== undefined ? (item.proportion * 100) + '%' : '0%' }}
</div>
<div class="title">完成率</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Container",
components: {},
props: ['itemList','height'],
data() {
return {
progress: 90, // 进度值,方便统一控制
};
},
watch: {
itemList: {
handler(newList) {
// 当 itemList 变化时执行的逻辑(如重新计算、更新视图等)
console.log("子组件接收的 itemList 已更新:", newList);
},
deep: true // 深度监听:若 itemList 内部对象属性变化(如 currentValue 改变),也能触发监听
}
},
computed: {},
methods: {},
};
</script>
<style scoped lang="scss">
.coreItem {
display: flex;
// height: 382px;
overflow: hidden;
flex-wrap: wrap;
// grid-template-columns: 1fr 1fr;
// grid-template-rows: 1fr 1fr;
justify-content: flex-start;
align-content: flex-start;
gap: 8px;
overflow-y: auto;
// padding-right: 4px;
/* 2. 隐藏 Chrome/Safari 滚动条 */
&::-webkit-scrollbar {
width: 0;
/* 滚动条宽度设为0 */
height: 0;
/* 横向滚动条(如需)也隐藏 */
}
/* 3. 隐藏 Firefox 滚动条 */
scrollbar-width: none;
}
.item {
width: 376px;
height: 120px;
background: #f9fcff;
padding: 12px;
box-sizing: border-box;
// &:hover {
// box-shadow: 0px 4px 12px 2px #B5CDE5;
// }
.unit {
// width: 124px;
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;
margin-bottom: 8px;
}
.item-content {
display: flex;
flex-direction: column;
justify-content: space-between;
height: calc(100% - 26px);
}
.content-wrapper {
display: flex;
align-items: center;
justify-content: space-around;
flex: 1;
}
.line {
width: 1px;
height: 46px;
background: linear-gradient(to bottom,
rgba(255, 0, 0, 0),
rgba(40, 203, 151, 1));
}
.left,
.right {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 5px;
flex: 1;
}
.number {
height: 22px;
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 26px;
color: rgba(103, 103, 103, 0.79);
line-height: 22px;
text-align: center;
font-style: normal;
}
.title {
height: 14px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: #868687;
line-height: 14px;
text-align: center;
font-style: normal;
}
/* 进度条和百分比的外层容器 */
.progress-group {
display: flex;
align-items: center;
gap: 8px;
/* 进度条和百分比的间距 */
}
.progress-container {
width: 190px;
height: 10px;
background: #ECEFF7;
border-radius: 8px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: #28CB97;
border-radius: 8px;
opacity: 0.6;
}
/* 百分比文本样式 */
.progress-percent {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #868687;
line-height: 1;
}
}
</style>

View File

@@ -0,0 +1,186 @@
<template>
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 400px;"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
components: {},
data() {
return {
myChart: null // 存储图表实例,避免重复创建
};
},
props: ["stockValues"],
watch: {
// 监听 stockValues 变化,动态更新图表
stockValues: {
handler() {
this.updateChart();
},
deep: true,
immediate: true
}
},
mounted() {
this.$nextTick(() => {
this.initChart();
});
},
beforeDestroy() {
// 清理图表实例和事件监听
this.myChart && this.myChart.dispose();
window.removeEventListener('resize', this.resizeHandler);
},
methods: {
// 初始化图表实例
initChart() {
const chartDom = this.$refs.cockpitEffChip;
if (!chartDom) {
console.error('图表容器未找到!');
return;
}
this.myChart = echarts.init(chartDom);
// 绑定窗口缩放事件
this.resizeHandler = () => this.myChart?.resize();
window.addEventListener('resize', this.resizeHandler);
},
// 解析 stockValues提取地名和对应库存值
parseStockData() {
const xData = []; // X轴地名
const data = []; // 对应库存值
if (Array.isArray(this.stockValues) && this.stockValues.length) {
this.stockValues.forEach(item => {
// 获取对象的唯一键(如“宜兴基地库存”)
// const key = Object.keys(item)[0];
// 提取地名:去掉“基地库存”后缀(如“宜兴基地库存”→“宜兴”)
const place = item.name.replace('分公司', '');
// 获取库存值(保留两位小数,避免精度问题)
const value = item.stockValue
xData.push(place);
data.push(value);
});
}
return { xData, data };
},
// 更新图表(核心:动态渲染数据)
updateChart() {
if (!this.myChart) return;
// 解析数据
const { xData, data } = this.parseStockData();
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
},
formatter: (params) => {
let html = `${params[0].axisValue}<br/>`;
params.forEach(item => {
html += `${item.marker} 库存: ${item.value} 万㎡<br/>`;
});
return html;
}
},
grid: {
top: 30,
bottom: 30,
right: 70,
left: 40,
},
xAxis: [
{
type: 'category',
boundaryGap: true,
axisTick: { show: false },
axisLine: {
show: true,
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
},
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
interval: 0,
padding: [5, 0, 0, 0]
},
data: xData // 动态 X 轴数据(解析后的地名)
}
],
yAxis: [
{
type: 'value',
name: '万㎡',
nameTextStyle: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
align: 'right'
},
min: 0,
max: (value) => value.max > 0 ? Math.ceil(value.max * 1.1) : 10, // 无数据时默认最大10避免图表异常
scale: false,
axisTick: { show: false },
axisLabel: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
formatter: '{value}'
},
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
splitNumber: 4
}
],
series: [
{
name: '库存',
type: 'bar',
yAxisIndex: 0,
barWidth: 24,
label: {
show: true,
position: 'top',
textStyle: {
color: 'rgba(11, 88, 255, 1)',
fontSize: 14
}
},
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(130, 204, 255, 1)' },
{ offset: 1, color: 'rgba(75, 157, 255, 1)' }
]
},
borderRadius: [4, 4, 0, 0],
borderWidth: 0
},
data: data // 动态库存数据(解析后的值)
}
]
};
this.myChart.setOption(option);
},
// 窗口缩放处理函数
resizeHandler() {
this.myChart && this.myChart.resize();
}
},
};
</script>

View File

@@ -0,0 +1,252 @@
<template>
<div class="coreItem">
<div class="item" @click="handleRoute(item.route)" v-for="(item, index) in itemList" :key="index">
<div class="unit">{{ item.unit }}</div>
<div class="item-content">
<div class="content-wrapper">
<div class="left">
<div class="number" style="color: rgba(103, 103, 103, 0.79);">{{ item.targetValue }}</div>
<div class="title">目标值</div>
</div>
<div class="line"></div>
<!-- 实际值根据 实际值目标值 动态绑定类名 -->
<div class="right">
<div class="number" :class="{
'number-exceed': item.currentValue >= item.targetValue,
'number-below': item.currentValue < item.targetValue
}">
{{ item.currentValue }}
</div>
<div class="title">实际值</div>
</div>
</div>
<div class="line"></div>
<!-- 进度条同步绑定类名 -->
<div class="progress-group">
<div class="progress-container">
<div class="progress-bar" :style="{ width: item.progress + '%' }" :class="{
'bar-exceed': item.currentValue >= item.targetValue,
'bar-below': item.currentValue < item.targetValue
}"></div>
</div>
</div>
<!-- 完成率同步绑定类名 -->
<div class="yield" style="display: flex;justify-content: space-between;">
<div class="progress-percent" :class="{
'percent-exceed': item.currentValue >= item.targetValue,
'percent-below': item.currentValue < item.targetValue
}">完成率</div>
<div class="progress-percent" :class="{
'percent-exceed': item.currentValue >= item.targetValue,
'percent-below': item.currentValue < item.targetValue
}">
{{ item.progress }}%
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Container",
components: {},
props: ["name", "size", "icon"],
data() {
return {
itemList: [
{
unit: "营业收入·万元",
targetValue: 16,
currentValue: 17.2, // 大于目标值(绿色)
progress: 107.5,
route: 'operatingRevenue'
},
{
unit: "经营性利润·万元",
targetValue: 16,
currentValue: 16, // 等于目标值(绿色)
progress: 100,
route: 'profitAnalysis'
},
{
unit: "利润总额·万元",
targetValue: 16,
currentValue: 14.8, // 小于目标值(黄色)
progress: 92.5,
route: 'profitAnalysis'
},
{
unit: "毛利率·%",
targetValue: 16,
currentValue: 15.5, // 小于目标值(黄色)
progress: 96.875,
route: 'profitAnalysis'
}
]
};
},
methods: {
handleRoute(route) {
if (route) {
this.$router.push({ path: route });
}
}
}
};
</script>
<style scoped lang="scss">
.coreItem {
display: flex;
gap: 8px;
// padding: 8px; // 避免边缘item hover阴影被截断
}
.item {
width: 170px;
height: 228px;
background: #f9fcff;
padding: 12px 0px 17px 12px;
box-sizing: border-box;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
box-shadow: 0px 4px 12px 2px #B5CDE5;
transform: translateY(-2px);
}
.unit {
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;
}
.item-content {
display: flex;
flex-direction: column;
justify-content: space-between;
height: calc(100% - 26px);
}
.content-wrapper {
display: flex;
flex-direction: column;
gap: 10px;
}
.line {
width: 149px;
height: 1px;
background: linear-gradient(to left, rgba(255, 0, 0, 0), #cbcbcb);
}
.left,
.right {
margin-top: 11px;
display: flex;
flex-direction: column;
gap: 2px;
width: 100%;
}
/* 实际值 - 基础样式(无颜色) */
.number {
height: 22px;
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 24px;
line-height: 22px;
text-align: left;
font-style: normal;
}
/* 实际值 - 实际值≥目标值(绿色) */
.number-exceed {
color: rgba(54, 181, 138, 1) !important;
}
/* 实际值 - 实际值<目标值(黄色) */
.number-below {
color: rgba(249, 164, 74, 1) !important;
}
.title {
height: 14px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #868687;
line-height: 14px;
text-align: left;
font-style: normal;
}
.progress-group {
display: flex;
align-items: center;
gap: 8px;
margin-top: 15px;
}
.progress-container {
width: 138px;
height: 10px;
background: #ECEFF7;
border-radius: 8px;
overflow: hidden;
}
/* 进度条 - 基础样式(无颜色) */
.progress-bar {
height: 100%;
border-radius: 8px;
transition: width 0.5s ease;
}
/* 进度条 - 实际值≥目标值(绿色) */
.bar-exceed {
background:rgba(98, 213, 180, 1) !important;
opacity: 1 !important;
}
/* 进度条 - 实际值<目标值(黄色) */
.bar-below {
background: rgba(249, 164, 74, 1) !important;
opacity: 1 !important;
}
/* 百分比 - 基础样式(无颜色) */
.progress-percent {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
line-height: 1;
}
/* 百分比 - 实际值≥目标值(绿色) */
.percent-exceed {
color: #28CB97 !important;
}
/* 百分比 - 实际值<目标值(黄色) */
.percent-below {
color: rgba(249, 164, 74, 1) !important;
}
.yield {
width: 138px;
margin-top: 3px;
}
}
</style>

View File

@@ -0,0 +1,242 @@
<template>
<div style="flex: 1">
<Container name="产销数据总览" icon="cockpitItemIcon" size="profitTopBasic" topSize="middle">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex;flex-direction: column; width: 100%;">
<!-- 2. .top 保持 flex无需固定高度自动跟随子元素拉伸 -->
<div class="top" style="display: flex; width: 100%;">
<top-item :itemList="targetItemList" />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
// import * as echarts from 'echarts'
import topItem from './profit-item.vue'
export default {
name: 'ProductionStatus',
components: { Container, topItem },
// mixins: [resize],
props: {
productAndSaleData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
},
// computed: {
// targetItemList() {
// // 当 productAndSaleData 为空数组时,使用 parentItemList
// return this.productAndSaleData.length === 0 ? this.parentItemList : this.productAndSaleData;
// }
// },
data() {
return {
chart: null,
parentItemList: [
{ name: "销量", targetValue: 0, value: 0, proportion: 0, route:'PSIAnal' },
{ name: "产量", targetValue: 0, value: 0, proportion: 0 },
]
}
},
computed: {
targetItemList() {
// 基于 parentItemList 的名称,匹配 productAndSaleData 中对应名称的数据
return this.parentItemList.map(parentItem => {
// 按名称精准匹配name 完全一致才关联)
const productItem = this.productAndSaleData.find(
item => item.name === parentItem.name
) || {};
// 优先使用匹配到的 productItem 数据,缺失则用 parentItem 的默认值0
return {
name: parentItem.name, // 保留原名称(销量、产量等)
targetValue: productItem.targetValue ?? parentItem.targetValue ?? 0,
value: productItem.value ?? parentItem.value ?? 0,
proportion: productItem.proportion ?? parentItem.proportion ?? 0,
completed: productItem.completed ?? parentItem.completed ?? 1,
route: parentItem.route // 路由沿用 parentItem 的配置
};
});
}
},
mounted() {
// 初始化图表(若需展示图表,需在模板中添加对应 DOM
// this.$nextTick(() => this.updateChart())
},
methods: {
}
}
</script>
<style lang='scss' scoped>
/* 3. 核心:滚动容器样式(固定高度+溢出滚动+隐藏滚动条) */
.scroll-container {
/* 1. 固定容器高度根据页面布局调整示例300px超出则滚动 */
max-height: 210px;
/* 2. 溢出滚动:内容超出高度时显示滚动功能 */
overflow-y: auto;
/* 3. 隐藏横向滚动条(防止设备名过长导致横向滚动) */
overflow-x: hidden;
/* 4. 内边距:与标题栏和容器边缘对齐 */
padding: 10px 0;
/* 5. 隐藏滚动条(兼容主流浏览器) */
/* Chrome/Safari */
&::-webkit-scrollbar {
display: none;
}
/* Firefox */
scrollbar-width: none;
/* IE/Edge */
-ms-overflow-style: none;
}
/* 设备项样式优化:增加间距,避免拥挤 */
.proBarInfo {
display: flex;
flex-direction: column;
padding: 8px 27px;
/* 调整内边距,优化排版 */
margin-bottom: 10px;
/* 设备项之间的垂直间距 */
}
/* 原有样式保留,优化细节 */
.proBarInfoEqInfo {
display: flex;
justify-content: space-between;
align-items: center;
/* 垂直居中,避免序号/文字错位 */
}
.slot {
width: 21px;
height: 23px;
background: rgba(0, 106, 205, 0.22);
backdrop-filter: blur(1.5px);
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #68B5FF;
line-height: 23px;
/* 垂直居中文字 */
text-align: center;
font-style: normal;
}
.eq-name {
margin-left: 8px;
/* 增加与序号的间距 */
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
letter-spacing: 1px;
text-align: left;
font-style: normal;
}
.eqStatus {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #FFFFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.splitLine {
width: 1px;
height: 14px;
border: 1px solid #ADADAD;
margin: 0 8px;
/* 优化分割线间距 */
}
.yield {
height: 18px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 16px;
color: #00FFFF;
line-height: 18px;
text-align: right;
font-style: normal;
}
.proBarInfoEqInfoLeft {
display: flex;
align-items: center;
/* 序号和设备名垂直居中 */
}
.proBarInfoEqInfoRight {
display: flex;
align-items: center;
/* 状态/分割线/百分比垂直居中 */
}
.proBarWrapper {
position: relative;
height: 10px;
margin-top: 6px;
/* 进度条与上方信息的间距 */
border-radius: 5px;
/* 进度条圆角,优化视觉 */
overflow: hidden;
}
.proBarLine {
width: 100%;
height: 100%;
background: linear-gradient(65deg, rgba(82, 82, 82, 0) 0%, #ACACAC 100%);
opacity: 0.2;
}
.proBarLineTop {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: linear-gradient(65deg, rgba(53, 223, 247, 0) 0%, rgba(54, 220, 246, 0.92) 92%, #36F6E5 100%, #37ACF5 100%);
border-radius: 5px;
transition: width 0.3s ease;
/* 进度变化时添加过渡动画,更流畅 */
}
/* 图表相关样式保留 */
.chartImgBottom {
position: absolute;
bottom: 45px;
left: 58px;
}
.line {
display: inline-block;
position: absolute;
left: 57px;
bottom: 42px;
width: 1px;
height: 20px;
background-color: #00E8FF;
}
</style>
<style>
/* 全局 tooltip 样式(不使用 scoped确保生效 */
.production-status-chart-tooltip {
background: #0a2b4f77 !important;
border: none !important;
backdrop-filter: blur(12px);
}
.production-status-chart-tooltip * {
color: #fff !important;
}
</style>

View File

@@ -0,0 +1,267 @@
<template>
<div class="coreItem">
<div class="item" v-for="(item, index) in itemList" :key="index" @click="handleRouter(item)">
<div class="unit">{{ item.unit }}</div>
<div class="item-content">
<div class="content-wrapper">
<div class="left">
<div class="number">{{ item.targetValue }}</div>
<div class="title">目标值</div>
</div>
<div class="line"></div>
<!-- 实际值根据与目标值的比较动态变色 -->
<div class="right">
<div class="number" :class="{
'exceed-target': item.currentValue > item.targetValue,
'below-target': item.currentValue < item.targetValue,
'equal-target': item.currentValue === item.targetValue
}">
{{ item.currentValue }}
</div>
<div class="title">实际值</div>
</div>
</div>
<!-- 进度条和百分比同步变色逻辑 -->
<div class="progress-group">
<div class="progress-container">
<div class="progress-bar" :style="{ width: item.progress + '%' }" :class="{
'exceed-pro-target': item.currentValue > item.targetValue,
'below-pro-target': item.currentValue < item.targetValue,
'equal-pro-target': item.currentValue === item.targetValue
}"></div>
</div>
<div class="progress-percent" :class="{
'exceed-target': item.currentValue > item.targetValue,
'below-target': item.currentValue < item.targetValue,
'equal-target': item.currentValue === item.targetValue
}">
{{ item.progress }}%
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Container",
components: {},
props: ["name", "size", "icon"],
data() {
return {
itemList: [
{
unit: "单价·元/㎡",
targetValue: 16,
currentValue: 14.5, // 小于目标值(黄色)
progress: 90,
path: "/cost/cost"
},
{
unit: "净价·元/㎡",
targetValue: 16,
currentValue: 16, // 等于目标值(默认灰色)
progress: 100,
path: "/cost/cost"
},
{
unit: "销量·万㎡",
targetValue: 20,
currentValue: 22, // 大于目标值(绿色)
progress: 110,
path: "PSIAnal"
},
{
unit: "双镀面板·万㎡",
targetValue: 15,
currentValue: 13.8, // 小于目标值(黄色)
progress: 92,
path: "PSIAnal"
}
]
};
},
methods: {
// 处理路由跳转
handleRouter(obj) {
if (obj.path) {
this.$router.push({ path: obj.path });
}
}
}
};
</script>
<style scoped lang="scss">
.coreItem {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 8px;
// padding: 8px; // 避免边缘item hover时阴影被截断
}
.item {
width: 252px;
height: 110px;
background: #f9fcff;
padding: 12px;
box-sizing: border-box;
cursor: pointer; // 提示可点击
transition: all 0.3s ease; // 动画过渡
&:hover {
box-shadow: 0px 4px 12px 2px #B5CDE5;
transform: translateY(-2px); // 轻微上浮增强交互感
}
.unit {
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;
margin-bottom: 8px;
}
.item-content {
display: flex;
flex-direction: column;
justify-content: space-between;
height: calc(100% - 26px);
}
.content-wrapper {
display: flex;
align-items: center;
justify-content: space-around;
flex: 1;
}
.line {
width: 1px;
height: 46px;
background: linear-gradient(to bottom, rgba(255, 0, 0, 0), rgba(40, 203, 151, 1));
}
.left,
.right {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 2px;
flex: 1;
}
.number {
height: 22px;
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 24px;
color: rgba(103, 103, 103, 0.79);
/* 默认颜色(等于目标值时) */
line-height: 22px;
text-align: center;
font-style: normal;
}
.title {
height: 14px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #868687;
line-height: 14px;
text-align: center;
font-style: normal;
}
.progress-group {
display: flex;
align-items: center;
gap: 8px;
}
.progress-container {
width: 190px;
height: 10px;
background: #ECEFF7;
border-radius: 8px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: rgba(98, 213, 180, 1);
/* 默认进度条颜色(等于目标值时) */
border-radius: 8px;
opacity: 0.7;
transition: width 0.5s ease; // 进度条动画
}
.progress-percent {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #868687;
/* 默认百分比颜色(等于目标值时) */
line-height: 1;
}
/* 实际值 > 目标值:绿色样式 */
.exceed-target {
color: rgba(98, 213, 180, 1) !important;
/* 文字绿色 */
// background: rgba(98, 213, 180, 1) !important;
/* 进度条绿色 */
opacity: 1 !important;
}
/* 实际值 < 目标值:黄色样式 */
.below-target {
color: rgba(249, 164, 74, 1) !important;
/* 文字黄色 */
// background: rgba(249, 164, 74, 1) !important;
/* 进度条黄色 */
opacity: 1 !important;
}
.exceed-pro-target {
// color: rgba(98, 213, 180, 1) !important;
/* 文字绿色 */
background: rgba(98, 213, 180, 1) !important;
/* 进度条绿色 */
opacity: 1 !important;
}
/* 实际值 < 目标值:黄色样式 */
.below-pro-target {
// color: rgba(249, 164, 74, 1) !important;
/* 文字黄色 */
background: rgba(249, 164, 74, 1) !important;
/* 进度条黄色 */
opacity: 1 !important;
}
/* 实际值 = 目标值:默认灰色(可自定义) */
.equal-target{
color: rgba(98, 213, 180, 1) !important;
/* 文字绿色 */
// background: rgba(98, 213, 180, 1) !important;
/* 进度条绿色 */
opacity: 1 !important;
}
.equal-pro-target {
// color: rgba(98, 213, 180, 1) !important;
/* 文字绿色 */
background: rgba(98, 213, 180, 1) !important;
/* 进度条绿色 */
opacity: 1 !important;
}
}
</style>

View File

@@ -0,0 +1,258 @@
<template>
<div class="coreItem">
<div class="item" :class="`item${index + 1}`" @click="handleItemClick(index)" v-for="(item, index) in itemList"
:key="index">
<div class="unit">{{ item.unit }}</div>
<div class="item-content">
<div class="content-wrapper">
<div class="left">
<div class="number">{{ item.target }}</div>
<div class="title">目标值</div>
</div>
<div class="line"></div>
<div class="right">
<!-- 实际值颜色动态绑定 -->
<div class="number" :style="{ color: getColor(index) }">
{{ item.actual }}
</div>
<div class="title">实际值</div>
</div>
</div>
<div class="progress-group">
<div class="progress-container">
<!-- 进度条样式动态绑定 -->
<div class="progress-bar" :style="{
width: `${item.progress}%`,
background: getColor(index)
}"></div>
</div>
<!-- 百分比颜色动态绑定 -->
<div class="progress-percent" :style="{ color: getColor(index) }">
{{ item.progress }}%
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Container",
components: {},
props: ["name", "size", "icon"],
data() {
return {
itemList: [
{
unit: "总成本·元/㎡",
route: "cost/cost",
target: 16,
actual: 16,
progress: 100
},
{
unit: "原片成本·元/㎡",
route: "cost/cost",
target: 16,
actual: 18,
progress: 110
},
{
unit: "加工成本·元/㎡",
route: "cost/cost",
target: 16,
actual: 14,
progress: 85
},
{
unit: "原片成品率·%",
target: 95,
actual: 92,
progress: 97
},
{
unit: "投入产出率·%",
target: 88,
actual: 90,
progress: 102
}
],
activeIndex: -1
};
},
methods: {
handleItemClick(index) {
const currentItem = this.itemList[index];
console.log(`点击了第${index + 1}个item:`, currentItem.unit);
this.$emit('item-click', { index, ...currentItem });
this.activeIndex = index;
if (currentItem.route) {
this.$router.push({ path: currentItem.route });
}
},
// 判断颜色的方法
getColor(index) {
const { actual, target } = this.itemList[index];
return actual >= target
? "rgba(98, 213, 180, 1)"
: "rgba(249, 164, 74, 1)";
}
}
};
</script>
<style scoped lang="scss">
.coreItem {
display: flex;
flex-wrap: wrap;
gap: 8px;
box-sizing: border-box;
}
.coreItem> :nth-child(1) {
grid-area: item1;
&:hover {
box-shadow: 0px 4px 12px 2px #B5CDE5;
}
}
.coreItem> :nth-child(2) {
grid-area: item2;
&:hover {
box-shadow: 0px 4px 12px 2px #B5CDE5;
}
}
.coreItem> :nth-child(3) {
grid-area: item3;
&:hover {
box-shadow: 0px 4px 12px 2px #B5CDE5;
}
}
.coreItem> :nth-child(4) {
grid-area: item4;
}
.coreItem> :nth-child(5) {
grid-area: item5;
}
.item {
width: 252px;
height: 110px;
background: #f9fcff;
padding: 12px;
box-sizing: border-box;
cursor: pointer;
transition: all 0.2s ease;
&:active {
transform: scale(0.98);
}
.unit {
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;
margin-bottom: 8px;
}
.item-content {
display: flex;
flex-direction: column;
justify-content: space-between;
height: calc(100% - 26px);
}
.content-wrapper {
display: flex;
align-items: center;
justify-content: space-around;
flex: 1;
}
.line {
width: 1px;
height: 46px;
background: linear-gradient(to bottom,
rgba(255, 0, 0, 0),
rgba(40, 203, 151, 1));
}
.left,
.right {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 2px;
flex: 1;
}
.number {
height: 22px;
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 24px;
color: rgba(103, 103, 103, 0.79);
line-height: 22px;
text-align: center;
font-style: normal;
}
.title {
height: 14px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #868687;
line-height: 14px;
text-align: center;
font-style: normal;
}
.progress-group {
display: flex;
align-items: center;
gap: 8px;
}
.progress-container {
width: 190px;
height: 10px;
background: #ECEFF7;
border-radius: 8px;
overflow: hidden;
}
.progress-bar {
height: 100%;
border-radius: 8px;
transition: width 0.3s ease;
}
.progress-percent {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
line-height: 1;
}
}
.item1,
.item2,
.item3 {
width: 166px;
}
</style>