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,344 @@
<template>
<header class="report-header">
<!-- 左侧区域logo + 标题 -->
<div class="left-content">
<img style="height: 36px;" src="../../../assets/img/cnbm.png" alt="benmaLogo" class="logo">
<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">
<!-- //年按钮点击更新activeTime关联选择器类型 -->
<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> <!-- 补充span抵消倾斜 -->
</div>
<!-- 日期选择器type和placeholder通过activeTime动态切换 -->
<el-date-picker v-model="date" :type="getPickerType" :placeholder="getPickerPlaceholder"
class="custom-date-picker" style="width: 132px;height: 29px;"></el-date-picker>
</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: 0, // 0=日1=月2=年(默认选中“日”)
pageRoutes: [
{ text: '营业收入', path: '/operatingRevenue' },
{ text: '利润分析', path: '/profitAnalysis' },
{ text: '产销率库存分析', path: '/PSIAnal' },
{ text: '成本分析', path: '/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') },
exportPDF() { this.$emit('exportPDF') },
updateTime() {
const now = new Date()
const hours = this.padZero(now.getHours())
const minutes = this.padZero(now.getMinutes())
const year = now.getFullYear()
const month = this.padZero(now.getMonth() + 1)
const day = this.padZero(now.getDate())
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
this.currentTime = `${hours}:${minutes} | ${year}.${month}.${day} | ${weekdays[now.getDay()]}`
},
padZero(num) { return num < 10 ? '0' + num : num }
},
mounted() {
this.updateTime()
this.timeTimer = setInterval(this.updateTime, 1000)
},
beforeDestroy() {
if (this.timeTimer) clearInterval(this.timeTimer)
}
}
</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: 100px;
.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: 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;
/* 统一倾斜角度修复原30deg的错位 */
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: 62px;
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: 40px;
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: ;
width: 132px !important;
height: 28px !important;
position: relative;
margin: 0 !important;
}
::v-deep .custom-date-picker .el-input--medium .el-input__inner,
::v-deep .custom-date-picker .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: #fff !important;
clip-path: polygon(20px 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);
}
::v-deep .custom-date-picker .el-input__prefix {
left: auto !important;
right: 8px !important;
top: 40% !important;
transform: translateY(-50%) !important;
}
::v-deep .custom-date-picker .el-input__prefix .el-icon {
color: #fff !important;
font-size: 16px !important;
}
::v-deep .custom-date-picker .el-input__inner:hover,
::v-deep .custom-date-picker .el-input__inner:focus {
background-color: rgba(11, 88, 255, 0.9) !important;
clip-path: polygon(20px 0, 100% 0, 100% 100%, 0 100%) !important;
}
</style>

View File

@@ -0,0 +1,250 @@
<!--
* @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: 230 // 原固定值,作为默认 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;
// 关键修改:隐藏滚动条但保留滚动功能
&::-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,246 @@
<template>
<div class="container" :class="['container__' + size]">
<div class="container-top" style="display: flex;gap: 9px;">
<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="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">
.container {
display: inline-block;
// width: 100%;
// height: 00px;
padding: 6px;
display: flex;
flex-direction: column;
position: relative;
.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,192 @@
<template>
<div class="container" :class="['container__' + size]">
<div class="container-top">
<svg-icon style="font-size: 32px; margin-left: 16px" :icon-class="icon" />
<span style="
width: 746px;
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="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">
.container {
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;
}
/* 左上角折现边框(主边框) */
.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,71 @@
<template>
<div class="changeBase">
<div class="base-item" :class="{ isChecked: activeButton === index, noChecked: activeButton !== index }"
@click="handleClick(index)"
v-for="(item, index) in buttonList"
:key="index"
>
{{ item }}
</div>
</div>
</template>
<script>
export default {
name: "Container",
data() {
return {
activeButton: 0, // 默认选中“总览”
buttonList: ['总览', '宜兴', '漳州', '自贡', '桐城', '洛阳', '合肥']
};
},
methods: {
handleClick(index) {
this.activeButton = index; // 更新选中状态
// 触发自定义事件,将当前选中的值(如“总览”“宜兴”)传递给父组件
this.$emit('selectChange', index+1);
}
},
// 初始化时就将默认选中的值传递给父组件
mounted() {
this.$emit('selectChange', this.activeButton +1);
}
};
</script>
<style scoped lang="scss">
/* 样式不变,省略 */
.changeBase {
display: flex;
width: fit-content; // 宽度由子项总宽度决定
// justify-content: space-around;
// gap: 50px;
// overflow-x: auto;
// padding: 10px 0;
.base-item {
width: 264px;
height: 82px;
font-family: YouSheBiaoTiHei;
font-size: 38px;
line-height: 54px;
text-align: center;
font-style: normal;
cursor: pointer;
white-space: nowrap;
margin-right: -35px;
}
.isChecked {
color: #F9FBFE;
background: url(../../../assets//img/baseChecked.png) no-repeat;
background-size: cover;
}
.noChecked {
color: rgba(30, 22, 81, 1);
background: url(../../../assets//img/baseNoChecked.png) no-repeat;
background-size: cover;
}
}
</style>

View File

@@ -0,0 +1,219 @@
<template>
<div class="cockpitContainer" :class="['cockpitContainer__' + size]">
<div class="content-top" :class="['content-top__' + topSize]">
<div class="title-wrapper">
<svg-icon class="title-icon" :icon-class="icon" />
<span class="title-text">
{{ name }}
</span>
<!-- <el-button type="text">利润影响额</el-button> -->
</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', '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;
}
&__costMiddleBg {
background: url(../../../assets/img/costMiddleBg.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__costSmallBg {
background: url(../../../assets/img/costSmallBg.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;
}
&__productBasicBg {
background: url(../../../assets/img/productBasicBg.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__productMiddleBg {
background: url(../../../assets/img/productMiddleBg.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,189 @@
<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>
<!-- 实际值根据 flag 动态绑定颜色 -->
<div class="right">
<div class="number" :style="{ 'color': flagColor }"> {{ item.value || 0 }}</div>
<div class="title">实际值</div>
</div>
<div class="line"></div>
<!-- 完成率根据 flag 动态绑定颜色 -->
<div class="right">
<div class="number" :style="{ 'color': flagColor }"> {{ item.proportion !== null && item.proportion !== undefined ? (item.proportion) + '%' : '0%' }}
</div>
<div class="title">完成率</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Container",
components: {},
props: [
'itemList',
'height',
'flag' // 新增 flag 属性,用于控制颜色
],
data() {
return {
progress: 90,
};
},
computed: {
// 根据 flag 计算实际值和完成率的颜色
flagColor() {
// flag 为 0 时使用灰色,为 1 时使用橙色
return this.flag === 1
? 'rgba(255, 132, 0, 1)'
: 'rgba(103, 103, 103, 0.79)';
}
},
watch: {
itemList: {
handler(newList) {
console.log("子组件接收的 itemList 已更新:", newList);
},
deep: true
}
},
methods: {},
};
</script>
<style scoped lang="scss">
/* 原有样式保持不变 */
.coreItem {
display: flex;
overflow: hidden;
flex-wrap: wrap;
justify-content: flex-start;
align-content: flex-start;
gap: 8px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
scrollbar-width: none;
}
.item {
width: 498px;
height: 120px;
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;
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(203, 203, 203, 1.00));
}
.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;
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,296 @@
<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 square target"></span>
目标
</span>
<!-- 给第三个第四个图例项加 close-item -->
<span class="legend-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: 1590px">
<costBaseBarChart style="height: 99%; width: 1590px" />
</div>
</div>
</template>
<script>
import costBaseBarChart from './costBaseBarChart.vue';
export default {
name: "Container",
components: { costBaseBarChart },
props: ["name", "size", "icon"],
data() {
return {
isDropdownShow: false,
selectedProfit: '原料', // 默认选中"原料"
profitOptions: ['原料', '其他选项1', '其他选项2'], // 可根据实际需求修改选项
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: {
selectProfit(item) {
this.selectedProfit = item;
this.isDropdownShow = false;
}
},
mounted() {
// 点击外部关闭下拉菜单
const handleOutsideClick = (e) => {
if (!this.$el.contains(e.target)) {
this.isDropdownShow = false;
}
};
document.addEventListener('click', handleOutsideClick);
this.$once('hook:beforeDestroy', () => {
document.removeEventListener('click', handleOutsideClick);
});
}
};
</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;
}
.right-container {
display: flex;
align-items: center;
gap: 24px;
}
.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: 102px; // 保持原组件要求的宽度,其余样式与第二个组件一致
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: 102px; // 与按钮宽度一致
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,236 @@
<template>
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 500px;"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
components: {},
data() {
return {};
},
mounted() {
this.$nextTick(() => {
this.initData();
});
},
methods: {
initData() {
const chartDom = this.$refs.cockpitEffChip;
if (!chartDom) {
console.error('图表容器未找到!');
return;
}
const myChart = echarts.init(chartDom);
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
},
// 优化tooltip内容区分各系列含义
formatter: (params) => {
let html = `${params[0].axisValue}<br/>`;
params.forEach(item => {
// 直接使用系列名,无需映射,仅保留单位判断
html += `${item.marker} ${item.seriesName}: ${item.value}${item.seriesName === '完成率' ? '%' : '片'}<br/>`;
});
return html;
}
},
grid: {
top: 30,
bottom: 30, // 增大底部间距避免柱子与X轴标签重叠
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: ['漳州', '桐城', '合肥', '宜兴', '自贡', '洛阳']
}
],
yAxis: [
// 左侧Y轴目标/达标/未达标(数量,单位“片”)
{
type: 'value',
name: '元',
nameTextStyle: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
align: 'right'
},
min: 0, // 最小值设0确保柱子从X轴底部开始不超过X轴
max: (value) => Math.ceil(value.max * 1.1), // 最大值留10%余量,避免柱子顶满
scale: false, // 关闭缩放强制从0开始
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',
// // name: '%',
// nameTextStyle: {
// color: 'rgba(0, 0, 0, 0.45)',
// fontSize: 12,
// align: 'left'
// },
// min: 0,
// max: 100, // 完成率最大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, // 绑定右侧Y轴
// 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: [65, 78, 52, 85, 60, 95, 72], // 完成率数据0-100
// symbol: 'circle', // 数据点为圆形
// symbolSize: 6 // 数据点大小
// },
// 2. 目标柱状图绑定左侧数量Y轴
{
name: '销量',
type: 'bar',
yAxisIndex: 0,
barWidth: 24,
// 关键修复label 直接放在 series 下,而非 itemStyle 内
itemStyle: {
// 移除多余的 normal 层级,直接配置 color 渐变
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: [200, 280, 180, 300, 220, 350]
},
// 3. 达标柱状图绑定左侧数量Y轴
{
name: '产量',
type: 'bar',
yAxisIndex: 0,
barWidth: 24,
// 关键修复label 直接放在 series 下,而非 itemStyle 内
itemStyle: {
// 移除多余的 normal 层级,直接配置 color 渐变
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: [130, 220, 95, 255, 132, 332] // 达标数据(小于目标)
},
// 4. 未达标柱状图绑定左侧数量Y轴
// {
// name: '未达标',
// type: 'bar',
// yAxisIndex: 0,
// barWidth: 18,
// itemStyle: {
// color: 'rgba(249, 164, 74, 1)',
// borderRadius: [4, 4, 0, 0],
// borderWidth: 0
// },
// data: [70, 60, 85, 45, 88, 18, 78] // 未达标数据(目标-达标)
// }
],
// 图例:区分各系列,点击可控制显示隐藏
// legend: {
// top: 0,
// left: 'center',
// itemWidth: 12,
// itemHeight: 8,
// textStyle: {
// color: 'rgba(0, 0, 0, 0.45)',
// fontSize: 12
// },
// data: ['完成率', '目标', '达标', '未达标']
// }
};
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,127 @@
<template>
<div style="flex: 1">
<Container name="成本单项总览·元/m²" icon="cockpitItemIcon" size="costMiddleBg" topSize="middle">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%;">
<div class="leftTitle" style="display: flex;flex-direction: column;gap: 16px;">
<div class="item">
原片成本
</div>
<div class="item">
加工成本
</div>
</div>
<!-- 新增topItem 专属包裹容器统一控制样式和布局 -->
<div class="topItem-container" style="display: flex; flex-direction: column;gap: 16px;overflow: hidden;">
<rawSheet :piecesCostViews="piecesCostViews" />
<processingCost :processCostViews="processCostViews" />
</div>
<!-- 2. .top 保持 flex无需固定高度自动跟随子元素拉伸 -->
<!-- <div class="top" style="display: flex; width: 100%;">
<top-item :itemList="parentItemList" />
</div> -->
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
// import * as echarts from 'echarts'
import processingCost from './processingCost-Item.vue'
import rawSheet from './rawSheet-Item.vue'
export default {
name: 'ProductionStatus',
components: { Container, processingCost, rawSheet },
// mixins: [resize],
props: {
piecesCostViews: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
processCostViews: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
type: Array,
default: () => [] // 默认空数组,避免报错
}
},
data() {
return {
chart: null,
// parentItemList: [
// { unit: "销量", targetValue: 16, currentValue: 14.5, progress: 90 },
// { unit: "产量", targetValue: 16, currentValue: 15.2, progress: 85 },
// ]
}
},
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;
}
.leftTitle {
.item {
width: 67px;
height: 180px;
padding: 37px 23px;
background: #F9FCFF;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 18px;
color: #000000;
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,115 @@
<template>
<div style="flex: 1">
<Container :name="title" icon="cockpitItemIcon" size="rawTopBg" topSize="rawTopTitleLarge">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%;">
<!-- 新增topItem 专属包裹容器统一控制样式和布局 -->
<div class="topItem-container" style="display: flex; flex-direction: column;gap: 16px;overflow: hidden;">
<!-- <topItem :itemList="parentItemList" /> -->
<rawItem :itemList="itemData" />
</div>
<!-- 2. .top 保持 flex无需固定高度自动跟随子元素拉伸 -->
<!-- <div class="top" style="display: flex; width: 100%;">
<top-item :itemList="parentItemList" />
</div> -->
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
// import * as echarts from 'echarts'
import rawItem from './raw-Item.vue'
export default {
name: 'ProductionStatus',
components: { Container, rawItem,},
// mixins: [resize],
props: {
itemData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
title: { // 接收父组件传递的设备数据数组
type: String,
default: () => '原料成本概述·元/ m²' // 默认空数组,避免报错
},
},
data() {
return {
chart: null,
}
},
watch: {
itemData: {
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;
}
.leftTitle {
.item {
width: 67px;
height: 180px;
padding: 37px 23px;
background: #F9FCFF;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 18px;
color: #000000;
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,216 @@
<template>
<div style="flex: 1">
<Container name="成本概览·元/m²" icon="cockpitItemIcon" size="costBasicBg" topSize="psiTopTitleBasic">
<div class="kpi-content" style="padding: 14px 16px; display: flex;flex-direction: column; width: 100%;">
<div class="top" style="display: flex; width: 100%;">
<cost-item :height="376" :itemList="finalItemList" />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
import costItem from './cost-item.vue'
export default {
name: 'ProductionStatus',
components: { Container, costItem },
props: {
costOverviews: {
type: Array,
default: () => []
},
},
data() {
return {
chart: null,
// 固定3条默认数据作为展示模板
parentItemList: [
{ name: "总制造成本", targetValue: 0, value: 0, proportion: 0, flag: 0 },
{ name: "原片成本", targetValue: 0, value: 0, proportion: 0, flag: 0 },
{ name: "加工成本", targetValue: 0, value: 0, proportion: 0, flag: 0 },
]
}
},
computed: {
finalItemList() {
// 核心逻辑:以 parentItemList 为模板,用 costOverviews 数据覆盖保持3条长度
return this.parentItemList.map((defaultItem, index) => {
// 找到 costOverviews 中同名称的条目(优先按名称匹配,无匹配则按索引匹配)
const matchedItem = this.costOverviews.find(item => item.name === defaultItem.name)
|| this.parentItemList[index];
// 用匹配到的数据覆盖默认值,无匹配则保留默认值
return {
name: defaultItem.name, // 名称固定为默认模板的名称,不随传入数据改变
flag: matchedItem?.flag ?? defaultItem.flag,
proportion: matchedItem?.proportion ?? defaultItem.proportion,
targetValue: matchedItem?.target ?? defaultItem.targetValue,
value: matchedItem?.value ?? defaultItem.value
};
});
}
},
watch: {
costOverviews: {
handler(newValue) {
console.log('costOverviews 数据更新:', newValue);
console.log('最终展示数据:', this.finalItemList); // 调试用,可删除
},
deep: true
}
},
mounted() {
console.log('初始展示数据:', this.finalItemList); // 调试用,可删除
},
methods: {
}
}
</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,337 @@
<template>
<header class="report-header" :class="['report-header__' + size]">
<!-- 左侧区域logo + 标题 -->
<div class="left-content">
<!-- <img style="height: 36px;" src="../../../assets/img/cnbm.png" alt="benmaLogo" class="logo"> -->
<div class="top-title">{{ topTitle }}</div>
</div>
<!--
<div class="center-content">
<div class="item" v-for="(text, index) in ['营业收入', '利润分析', '产销率库存分析', '成本分析', '驾驶舱报表']" :key="index"
@click="activeIndex = index" :class="{ 'no-skew': activeIndex === index }">
<span class="item-text">{{ text }}</span>
</div>
</div> -->
<!-- 右侧区域全屏按钮 -->
<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">
<!-- //年按钮点击更新activeTime关联选择器类型 -->
<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> <!-- 补充span抵消倾斜 -->
</div>
<!-- 日期选择器type和placeholder通过activeTime动态切换 -->
<el-date-picker v-model="date" :type="getPickerType" :placeholder="getPickerPlaceholder"
class="custom-date-picker" style="width: 132px;height: 29px;"></el-date-picker>
</div>
</div>
</header>
</template>
<script>
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,
activeIndex: -1,
activeTime: 0, // 0=日1=月2=年(默认选中“日”)
// 定义时间类型配置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: {
changeFullScreen() { this.$emit('screenfullChange') },
exportPDF() { this.$emit('exportPDF') },
updateTime() {
const now = new Date()
const hours = this.padZero(now.getHours())
const minutes = this.padZero(now.getMinutes())
const year = now.getFullYear()
const month = this.padZero(now.getMonth() + 1)
const day = this.padZero(now.getDate())
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
this.currentTime = `${hours}:${minutes} | ${year}.${month}.${day} | ${weekdays[now.getDay()]}`
},
padZero(num) { return num < 10 ? '0' + num : num }
},
mounted() {
this.updateTime()
this.timeTimer = setInterval(this.updateTime, 1000)
},
beforeDestroy() {
if (this.timeTimer) clearInterval(this.timeTimer)
}
}
</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-between;
&__basic {
// width: 547px;
background: url(../../../assets/img/topBg.png) no-repeat;
background-size: cover;
background-position: 0 0;
}
&__psi {
// width: 547px;
background: url(../../../assets/img/psiTopTitle.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
box-sizing: border-box;
position: relative;
/* 确保timeType绝对定位生效 */
.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;
}
.center-content {
display: flex;
gap: 8px;
margin-top: 18px;
margin-left: 30px;
.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: 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;
/* 统一倾斜角度修复原30deg的错位 */
box-shadow: 0 2px 8px rgba(11, 88, 255, 0.3);
}
.timeType .item.no-skew .item-text {
transform: skew(25deg) !important;
/* 同步统一文字倾斜角度 */
}
.dateP {
margin-left: 10px;
display: flex;
align-items: center;
gap: 0;
}
.dateP .label {
width: 62px;
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: 40px;
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 {
width: 132px !important;
height: 28px !important;
position: relative;
margin: 0 !important;
}
::v-deep .custom-date-picker .el-input--medium .el-input__inner,
::v-deep .custom-date-picker .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: #fff !important;
clip-path: polygon(20px 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);
}
::v-deep .custom-date-picker .el-input__prefix {
left: auto !important;
right: 8px !important;
top: 40% !important;
transform: translateY(-50%) !important;
}
::v-deep .custom-date-picker .el-input__prefix .el-icon {
color: #fff !important;
font-size: 16px !important;
}
::v-deep .custom-date-picker .el-input__inner:hover,
::v-deep .custom-date-picker .el-input__inner:focus {
background-color: rgba(11, 88, 255, 0.9) !important;
clip-path: polygon(20px 0, 100% 0, 100% 100%, 0 100%) !important;
}
</style>

View File

@@ -0,0 +1,236 @@
<template>
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 400px;"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
name: 'operatingLineBar',
props: {
echartData: {
type: Object,
// 确保默认值中 flag 是数组,避免 undefined
default: () => ({
locations: [],
target: [],
value: [],
proportion: [],
flag: [] // 强制初始化为空数组
})
}
},
data() {
return {
myChart: null
};
},
watch: {
echartData: {
handler() {
this.initData();
},
deep: true,
immediate: true // 首次绑定就执行 handler
}
},
mounted() {
this.$nextTick(() => {
this.initData();
});
},
methods: {
initData() {
const chartDom = this.$refs.cockpitEffChip;
if (!chartDom) {
console.error('图表容器未找到!');
return;
}
if (!this.myChart) {
this.myChart = echarts.init(chartDom);
}
// 解构时给 flag 加默认值,防止 undefined
const {
locations = [],
target = [],
value = [],
proportion = [],
flag = []
} = this.echartData;
console.log('this.echartData', this.echartData);
// 确保 flag 数组长度与 value 一致(补全缺失的 flag 值为 0
const safeFlag = [...flag];
while (safeFlag.length < value.length) {
safeFlag.push(0); // 缺失的 flag 默认为 0达标
}
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
},
formatter: (params) => {
if (!params.length) return '';
const currentIndex = params[0].dataIndex;
// 使用处理后的 safeFlag避免越界
const currentFlag = safeFlag[currentIndex] || 0;
const statusText = currentFlag === 0 ? '达标' : '不达标';
let html = `${params[0].axisValue}${statusText}<br/>`;
params.forEach(item => {
const unit = item.seriesName === '完成率' ? '%' : '万元';
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: locations
}
],
yAxis: [
{
type: 'value',
name: '万元',
nameTextStyle: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
align: 'right'
},
min: 0,
max: (val) => val.max > 0 ? Math.ceil(val.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: [
{
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: proportion,
symbol: 'circle',
symbolSize: 6
},
{
name: '目标',
type: 'bar',
yAxisIndex: 0,
barWidth: 14,
itemStyle: {
color: 'rgba(40, 137, 255, 1)',
borderRadius: [4, 4, 0, 0],
borderWidth: 0
},
data: target
},
{
name: '实际',
type: 'bar',
yAxisIndex: 0,
barWidth: 14,
itemStyle: {
// 使用处理后的 safeFlag 避免 undefined
color: (params) => {
const dataIndex = params.dataIndex;
const currentFlag = safeFlag[dataIndex] || 0; // 双重保险,防止越界
return currentFlag === 0
? 'rgba(118, 218, 190, 1)'
: 'rgba(249, 164, 74, 1)';
},
borderRadius: [4, 4, 0, 0],
borderWidth: 0
},
data: value
}
]
};
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,147 @@
·<template>
<div style="flex: 1">
<Container name="概览趋势图" icon="cockpitItemIcon" size="costSmallBg" topSize="psiTopTitleBasic">
<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);">
<profitBar :cost-group-data="costGroupData" :cost-types="costTypes"
:locations="locations"
/>
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
import profitBar from './profitBar.vue'
export default {
name: 'ProductionStatus',
components: { Container, profitBar },
props: {
trendViews: {
type: Array,
default: () => []
}
},
data() {
return {
chart: null
}
},
computed: {
// 1. 地名数组(去重,按出现顺序)
locations() {
const locationSet = new Set();
this.trendViews.forEach(item => {
const location = this.extractLocation(item.name);
if (location) locationSet.add(location);
});
return Array.from(locationSet);
},
// 2. 核心成本类型数组(仅保留“原片”“加工”等,去掉“成本”二字)
costTypes() {
const typeSet = new Set();
this.trendViews.forEach(item => {
const coreType = this.extractCoreCostType(item.name);
if (coreType) typeSet.add(coreType);
});
return Array.from(typeSet);
},
// 3. 按「核心成本类型+地名」分组的数据
costGroupData() {
return this.trendViews.reduce((group, item) => {
const coreType = this.extractCoreCostType(item.name) || '其他';
const location = this.extractLocation(item.name) || '未知地区';
// 初始化层级分组
if (!group[coreType]) group[coreType] = {};
if (!group[coreType][location]) group[coreType][location] = [];
// 添加数据
group[coreType][location].push({
name: item.name,
target: item.target ?? 0,
value: item.value ?? 0,
proportion: item.proportion ?? 0,
flag: item.flag ?? 0,
time: item.time
});
return group;
}, {});
}
},
methods: {
// 提取核心成本类型(仅保留“原片”“加工”等,去掉“成本”二字)
extractCoreCostType(name) {
// 匹配“前缀(含分公司/工厂/区域等)+ 核心词 + 任意后缀”,提取中间核心词
// 兼容后缀成本、费用、支出、损耗等前缀XX分公司、XX工厂、XX区域等
const match = name.match(/(分公司)\s*([^,。;!?]+?)\s*(成本)/);
if (match) {
return match[2].trim(); // 提取中间核心词,去除前后空格
}
// 通用匹配:无明确前缀标识时,提取“最后一个连续汉字后缀”前的内容
const generalMatch = name.match(/(.+?)(?=[\u4e00-\u9fa5]{2,}$)/);
return generalMatch ? generalMatch[1].trim() : name.trim();
},
// 提取地名(成本类型前的核心地区名)
extractLocation(name) {
// 匹配“分公司”前面的连续汉字(即地名)
const match = name.match(/(.+?)分公司/);
if (match) {
return match[1].trim() || '未知地区';
}
// 兜底(处理不含“分公司”的情况)
return '未知地区';
}
},
watch: {
trendViews: {
handler(newValue) {
console.log('核心成本类型:', this.costTypes); // 示例输出:["原片", "加工"]
console.log('地名数组:', this.locations); // 示例输出:["桐城", "合肥"]
console.log('分组数据:', this.costGroupData);
},
deep: true
}
},
mounted() {
console.log('初始核心成本类型:', this.costTypes);
}
};
</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;
}
/* 其他样式省略 */
</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,329 @@
<template>
<!-- 外层滚动容器添加鼠标事件监听 -->
<div class="coreItem-container" @mousedown="handleMouseDown" @mousemove="handleMouseMove" @mouseup="handleMouseUp"
@mouseleave="handleMouseUp" :style="{ cursor: isDragging ? 'grabbing' : 'grab' }">
<div class="coreItem">
<div class="item" @click="handleRoute(item.route,item.name)" v-for="(item, index) in finalItemList" :key="index">
<div class="item-header">
<div class="unit">{{ item.name }}</div>
</div>
<div class="item-content">
<div class="content-wrapper">
<!-- 目标值 + 分割线 + 实际值 -->
<div class="value-group">
<div class="value-item">
<div class="number">{{ item.targetValue || 0 }}</div>
<div class="title">目标值</div>
</div>
<div class="middle-line"></div>
<div class="value-item">
<!-- 实际值根据 flag 动态绑定颜色 -->
<div class="number" :style="{ color: getColorByFlag(item.flag) }">
{{ item.value || 0 }}
</div>
<div class="title">当前值</div>
</div>
</div>
<!-- 进度条 + 完成率 -->
<div class="progress-yield-group">
<div class="progress-group">
<div class="progress-container">
<div class="progress-bar" :style="{
width: getProportionPercent(item.proportion) + '%',
background: getColorByFlag(item.flag, true)
}"></div>
</div>
</div>
<div class="yield">
<span class="progress-percent">完成率</span>
<!-- 完成率动态颜色 + 百分比格式 -->
<span class="progress-percent progress-value" :style="{ color: getColorByFlag(item.flag) }">
{{ getProportionPercent(item.proportion) }}%
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Container",
components: {},
props: ["name", "size", "icon", 'processCostViews'],
data() {
return {
itemList: [
{
name: "人工", targetValue: 0, value: 0, proportion: 0, flag: 0,
// route:'cost/rawMaterialCostAnalysis/rawMaterialCostAnalysis'
},
{ name: "燃动力", targetValue: 0, value: 0, proportion: 0, flag: 0, route:'fuelPowerCostAnalysis/fuelPowerCostAnalysis' },
{
name: "包装物", targetValue: 0, value: 0, proportion: 0, flag: 0,
route:'rawMaterialCostAnalysis/rawMaterialCostAnalysis'
},
{
name: "辅料", targetValue: 0, value: 0, proportion: 0, flag: 0,
route: 'rawMaterialCostAnalysis/rawMaterialCostAnalysis'
},
{
name: "制造费用", targetValue: 0, value: 0, proportion: 0, flag: 0,
route:'productionCostsAnalysis/productionCostsAnalysis'
},
],
isDragging: false,
startX: 0,
scrollLeft: 0
};
},
// computed: {
// finalItemList() {
// if (Array.isArray(this.piecesCostViews) && this.piecesCostViews.length > 0) {
// return this.piecesCostViews.map((item) => ({
// flag: item.flag || 0, // 默认 flag 为 0
// proportion: item.proportion || 0, // 默认完成率为 0
// targetValue: item.target ?? 0, // 兼容 null 值,显示为 0
// value: item.value || 0,
// name: item.name
// }));
// } else {
// return this.itemList;
// }
// }
// },
computed: {
finalItemList() {
return this.itemList.map((defaultItem, index) => {
// 核心修复1. 恢复模板名称处理2. 统一大小写和空格处理
const matchedItem = this.processCostViews.find(item => {
const processedIncomingName = (item.name || '').replace(/成本/g, '').trim().toLowerCase();
// 恢复模板名称处理:去除“成本”(如果有的话)、去空格、小写
const processedDefaultName = defaultItem.name.replace(/成本/g, '').trim().toLowerCase();
return processedIncomingName === processedDefaultName;
});
// 移除索引 fallback只匹配名称不按索引赋值避免误匹配
// 无匹配项则完全保留默认值
const targetItem = matchedItem || defaultItem;
return {
name: defaultItem.name, // 固定模板名称
flag: targetItem.flag ?? defaultItem.flag,
proportion: targetItem.proportion ?? defaultItem.proportion,
targetValue: targetItem.target ?? defaultItem.targetValue,
value: targetItem.value ?? defaultItem.value,
route: defaultItem.route
};
});
}
},
methods: {
handleRoute(route, name) {
if (route) {
this.$router.push({
path: route,
query: {
name: name + '成本'
}
})
}
},
// 根据 flag 获取文字/进度条颜色
getColorByFlag(flag, isProgressBar = false) {
const colorMap = {
0: {
text: 'rgba(103, 103, 103, 0.79)',
progress: 'rgba(103, 103, 103, 0.5)' // 进度条颜色稍浅
},
1: {
text: 'rgba(255, 132, 0, 1)',
progress: 'rgba(255, 132, 0, 0.7)' // 进度条颜色稍浅
}
};
const currentFlag = flag === 1 ? 1 : 0;
return isProgressBar ? colorMap[currentFlag].progress : colorMap[currentFlag].text;
},
// 处理 proportion乘以 100 并保留 0 位小数
getProportionPercent(proportion) {
const num = Number(proportion) || 0;
return num; // 四舍五入取整,如需保留小数可改为 toFixed(2)
},
// 拖拽相关方法
handleMouseDown(e) {
this.isDragging = true;
this.startX = e.pageX - this.$el.offsetLeft;
this.scrollLeft = this.$el.scrollLeft;
this.$el.style.userSelect = "none";
},
handleMouseMove(e) {
if (!this.isDragging) return;
e.preventDefault();
const x = e.pageX - this.$el.offsetLeft;
const walk = (x - this.startX) * 1.2;
this.$el.scrollLeft = this.scrollLeft - walk;
},
handleMouseUp() {
this.isDragging = false;
this.$el.style.userSelect = "";
}
}
};
</script>
<style scoped lang="scss">
.coreItem-container {
width: 100%;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
&::-webkit-scrollbar {
height: 0;
width: 0;
}
scrollbar-width: none;
-ms-overflow-style: none;
cursor: grab;
&:active {
cursor: grabbing;
}
}
.coreItem {
display: flex;
gap: 16px;
padding: 0 8px;
width: fit-content;
min-width: 100%;
}
.item {
width: 202px;
height: 180px;
background: #f9fcff;
padding: 10px 20px 0 27px;
box-sizing: border-box;
transition: all 0.3s ease;
&:not(:nth-child(1)):hover {
box-shadow: 0px 4px 12px 2px #B5CDE5;
transform: translateY(-2px);
}
.item-header {
margin-bottom: 11px;
}
.unit {
font-family: PingFangSC, PingFang SC;
font-weight: 500;
font-size: 16px;
color: #333333;
line-height: 1.2;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-content {
display: flex;
flex-direction: column;
height: calc(100% - 30px);
}
.content-wrapper {
display: flex;
flex-direction: column;
gap: 12px;
flex: 1;
}
.value-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.value-item {
display: flex;
flex-direction: column;
}
.middle-line {
width: 100%;
height: 1px;
background: linear-gradient(to right, rgba(40, 203, 151, 0.3), rgba(40, 203, 151, 0.8), rgba(40, 203, 151, 0.3));
}
.number {
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 22px;
line-height: 1.2;
text-align: left;
/* 颜色由动态样式控制,移除固定颜色 */
}
.title {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #999999;
line-height: 1.2;
text-align: left;
}
.progress-yield-group {
display: flex;
flex-direction: column;
gap: 0;
}
.progress-group {
display: flex;
align-items: center;
}
.progress-container {
width: 100%;
height: 8px;
background: #F5F7FA;
border-radius: 4px;
overflow: hidden;
}
.progress-bar {
height: 100%;
border-radius: 4px;
transition: width 0.5s ease;
/* 背景色由动态样式控制,移除固定颜色 */
}
.yield {
display: flex;
justify-content: space-between;
margin-top: 0;
}
.progress-percent {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #999999;
line-height: 1;
}
.progress-value {
font-weight: 500;
/* 颜色由动态样式控制,移除固定颜色 */
}
}
</style>

View File

@@ -0,0 +1,145 @@
<template>
<div style="flex: 1">
<Container name="加工成本趋势图" icon="cockpitItemIcon" size="costSmallBg" topSize="psiTopTitleBasic">
<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);">
<profitBar :cost-group-data="costGroupData" :cost-types="costTypes" :locations="locations" />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
import profitBar from './profitBar.vue'
export default {
name: 'ProductionStatus',
components: { Container, profitBar },
props: {
trendViews: {
type: Array,
default: () => []
}
},
data() {
return {
chart: null
}
},
computed: {
// 1. 地名数组(去重,按出现顺序)
locations() {
const locationSet = new Set();
this.trendViews.forEach(item => {
const location = this.extractLocation(item.name);
if (location) locationSet.add(location);
});
return Array.from(locationSet);
},
// 2. 核心成本类型数组(仅保留“原片”“加工”等,去掉“成本”二字)
costTypes() {
const typeSet = new Set();
this.trendViews.forEach(item => {
const coreType = this.extractCoreCostType(item.name);
if (coreType) typeSet.add(coreType);
});
return Array.from(typeSet);
},
// 3. 按「核心成本类型+地名」分组的数据
costGroupData() {
return this.trendViews.reduce((group, item) => {
const coreType = this.extractCoreCostType(item.name) || '其他';
const location = this.extractLocation(item.name) || '未知地区';
// 初始化层级分组
if (!group[coreType]) group[coreType] = {};
if (!group[coreType][location]) group[coreType][location] = [];
// 添加数据
group[coreType][location].push({
name: item.name,
target: item.target ?? 0,
value: item.value ?? 0,
proportion: item.proportion ?? 0,
flag: item.flag ?? 0,
time: item.time
});
return group;
}, {});
}
},
methods: {
// 提取核心成本类型(仅保留“原片”“加工”等,去掉“成本”二字)
extractCoreCostType(name) {
// 匹配“前缀(含分公司/工厂/区域等)+ 核心词 + 任意后缀”,提取中间核心词
// 兼容后缀成本、费用、支出、损耗等前缀XX分公司、XX工厂、XX区域等
const match = name.match(/(分公司)\s*([^,。;!?]+?)\s*(成本)/);
if (match) {
return match[2].trim(); // 提取中间核心词,去除前后空格
}
// 通用匹配:无明确前缀标识时,提取“最后一个连续汉字后缀”前的内容
const generalMatch = name.match(/(.+?)(?=[\u4e00-\u9fa5]{2,}$)/);
return generalMatch ? generalMatch[1].trim() : name.trim();
},
// 提取地名(成本类型前的核心地区名)
extractLocation(name) {
// 匹配“分公司”前面的连续汉字(即地名)
const match = name.match(/(.+?)分公司/);
if (match) {
return match[1].trim() || '未知地区';
}
// 兜底(处理不含“分公司”的情况)
return '未知地区';
}
},
watch: {
trendViews: {
handler(newValue) {
console.log('核心成本类型:', this.costTypes); // 示例输出:["原片", "加工"]
console.log('地名数组:', this.locations); // 示例输出:["桐城", "合肥"]
console.log('分组数据:', this.costGroupData);
},
deep: true
}
},
mounted() {
console.log('初始核心成本类型:', this.costTypes);
}
};
</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;
}
/* 其他样式省略 */
</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,223 @@
<template>
<!-- 外层滚动容器添加鼠标事件监听 -->
<div class="coreItem-container" @mousedown="handleMouseDown" @mousemove="handleMouseMove" @mouseup="handleMouseUp"
@mouseleave="handleMouseUp" :style="{ cursor: isDragging ? 'grabbing' : 'grab' }">
<div class="coreItem">
<div class="item" v-for="(item, index) in itemList" :key="index">
<div class="item-header">
<div class="unit">{{ item.name }}</div>
</div>
<div class="item-content">
<div class="content-wrapper">
<!-- 目标值 + 分割线 + 实际值 -->
<div class="value-group">
<div class="value-item">
<div class="number">{{ item.value }}</div>
<!-- <div class="title">目标值</div> -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Container",
components: {},
props: ["name", "size", "icon",'itemList'],
data() {
return {
itemList: [
// { unit: "营业收入·万元", targetValue: 16, currentValue: 14.5, progress: 90 },
// { unit: "经营性利润·万元", targetValue: 16, currentValue: 15.2, progress: 85 },
// { unit: "利润总额·万元", targetValue: 16, currentValue: 15.2, progress: 85 },
// { unit: "毛利率·%", targetValue: 16, currentValue: 15.2, progress: 85 },
// { unit: "销量·万㎡", targetValue: 20, currentValue: 16, progress: 80 },
// { unit: "双镀面板·万㎡", targetValue: 15, currentValue: 13.8, progress: 92 }
],
// 拖拽相关状态
isDragging: false, // 是否正在拖拽
startX: 0, // 拖拽开始时的鼠标X坐标
scrollLeft: 0 // 拖拽开始时的滚动条位置
};
},
methods: {
// 鼠标按下:开始拖拽
handleMouseDown(e) {
this.isDragging = true;
this.startX = e.pageX - this.$el.offsetLeft; // 记录初始鼠标X坐标相对容器
this.scrollLeft = this.$el.scrollLeft; // 记录初始滚动位置
this.$el.style.userSelect = "none"; // 防止拖拽时选中文字
},
// 鼠标移动:处理拖拽滚动
handleMouseMove(e) {
if (!this.isDragging) return;
e.preventDefault(); // 阻止默认行为(如选中文本)
const x = e.pageX - this.$el.offsetLeft; // 计算当前鼠标X坐标相对容器
const walk = (x - this.startX) * 1.2; // 计算移动距离乘以1.2增加灵敏度)
this.$el.scrollLeft = this.scrollLeft - walk; // 更新滚动位置
},
// 鼠标松开/离开:结束拖拽
handleMouseUp() {
this.isDragging = false;
this.$el.style.userSelect = ""; // 恢复文本选择
}
}
};
</script>
<style scoped lang="scss">
.coreItem-container {
width: 100%;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
// 隐藏滚动条(保留功能)
&::-webkit-scrollbar {
height: 0; // 彻底隐藏横向滚动条
width: 0;
}
scrollbar-width: none; // Firefox
-ms-overflow-style: none; // IE/Edge
// 拖拽时的光标样式(增强交互体验)
cursor: grab;
&:active {
cursor: grabbing;
}
}
.coreItem {
display: flex;
gap: 16px;
padding: 0 8px;
width: fit-content; // 宽度由子项总宽度决定
min-width: 100%; // 子项较少时铺满容器
}
.item {
width: 280px;
height: 80px;
background: #f9fcff;
padding: 10px 20px 0 27px;
box-sizing: border-box;
transition: all 0.3s ease;
.item-header {
margin-bottom: 11px;
}
.unit {
font-family: PingFangSC, PingFang SC;
font-weight: 500;
font-size: 18px;
color: #333333;
line-height: 1.2;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-content {
display: flex;
flex-direction: column;
height: calc(100% - 30px);
}
.content-wrapper {
display: flex;
flex-direction: column;
gap: 12px;
flex: 1;
}
.value-group {
display: flex;
flex-direction: column;
gap: 24px;
}
.value-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.middle-line {
width: 207px;
height: 3px;
border: 1px solid;
border-image: linear-gradient(109deg, rgba(203, 203, 203, 1), rgba(255, 255, 255, 0)) 1 1;
}
.number {
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 24px;
color: #666666;
line-height: 1.2;
text-align: left;
}
.title {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: #999999;
line-height: 1.2;
text-align: left;
}
.progress-yield-group {
display: flex;
flex-direction: column;
gap: 0;
}
.progress-group {
display: flex;
align-items: center;
}
.progress-container {
width: 100%;
height: 8px;
background: #F5F7FA;
border-radius: 4px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: rgba(98, 213, 180, .7);
border-radius: 4px;
transition: width 0.5s ease;
}
.yield {
display: flex;
justify-content: space-between;
margin-top: 4px;
}
.progress-percent {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #999999;
line-height: 1;
}
.progress-value {
color: #28CB97;
font-weight: 500;
}
}
</style>

View File

@@ -0,0 +1,114 @@
<template>
<div style="flex: 1">
<Container :name="title" icon="cockpitItemIcon" size="productBasicBg" topSize="middle">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%;">
<!-- 新增topItem 专属包裹容器统一控制样式和布局 -->
<div class="topItem-container" style="display: flex; flex-direction: column;gap: 16px;overflow: hidden;">
<!-- <topItem :itemList="parentItemList" /> -->
<productItem :itemList="parentItemList" />
</div>
<!-- 2. .top 保持 flex无需固定高度自动跟随子元素拉伸 -->
<!-- <div class="top" style="display: flex; width: 100%;">
<top-item :itemList="parentItemList" />
</div> -->
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
// import * as echarts from 'echarts'
import productItem from './product-Item.vue'
export default {
name: 'ProductionStatus',
components: { Container, productItem },
// mixins: [resize],
props: {
parentItemList: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
title: { // 接收父组件传递的设备数据数组
type: String,
default: () =>'' // 默认空数组,避免报错
},
productionOverviewVo: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
type: Object,
default: () => ({})
}
},
data() {
return {
chart: null,
// parentItemList: [
// { unit: "销量", targetValue: 16, currentValue: 14.5, progress: 90 },
// { unit: "产量", targetValue: 16, currentValue: 15.2, progress: 85 },
// ]
}
},
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;
}
.leftTitle {
.item {
width: 67px;
height: 180px;
padding: 37px 23px;
background: #F9FCFF;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 18px;
color: #000000;
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,252 @@
<template>
<div style="width: 100%;">
<Container name="领用明细" icon="cockpitItemIcon" size="productMiddleBg" topSize="middle">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex;width: 100%;">
<div class="bottom"
style="padding: 14px 16px; height: 620px; display: flex; width: 100%;background-color: rgba(249, 252, 255, 1);">
<!-- <top-item /> -->
<base-table style="width: 100%;" :page="1" :limit="10" :show-index="true" :beilv="1" :tableConfig="tableProps"
:table-data="tableData" />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
import baseTable from './baseTable.vue'
import { parseTime } from "@/utils/ruoyi";
export default {
name: 'ProductionStatus',
components: { Container, baseTable },
// mixins: [resize],
props: {
tableData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
productionOverviewVo: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
type: Object,
default: () => ({})
}
},
data() {
return {
chart: null,
baseList: [
{ value: 1, name: "总览" },
{ value: 2, name: "宜兴" },
{ value: 3, name: "漳州" },
{ value: 4, name: "自贡" },
{ value: 5, name: "桐城" },
{ value: 6, name: "洛阳" },
{ value: 7, name: "合肥" },
],
tableProps: [
// { prop: 'id', label: '序号', width: 50, align: 'center' },
// { prop: 'id', label: '序号', width: 50, align: 'center' },
{
prop: 'levelId', label: '基地', align: 'center',
filter: (levelId) => this.getBaseName(levelId)
},
{ prop: 'type', label: '物料类别', align: 'center' },
{ prop: 'name', label: '物料名称', align: 'center' },
{ prop: 'code', label: '物料编码', align: 'center' },
{
prop: 'time', label: '领用日期', align: 'center',
filter: parseTime,
},
{ prop: 'num', label: '数量', align: 'center' },
]
}
},
mounted() {
// 初始化图表(若需展示图表,需在模板中添加对应 DOM
// this.$nextTick(() => this.updateChart())
},
methods: {
getBaseName(levelId) {
if (!levelId) return '';
// 从baseList中匹配对应的基地名称无匹配则返回原始levelId
const baseItem = this.baseList.find(item => item.value === levelId);
return baseItem ? baseItem.name : levelId;
},
}
}
</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,252 @@
<template>
<div style="width: 100%;">
<Container name="入账明细" icon="cockpitItemIcon" size="productMiddleBg" topSize="middle">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex;width: 100%;">
<div class="bottom"
style="padding: 14px 16px; height: 620px; display: flex; width: 100%;background-color: rgba(249, 252, 255, 1);">
<!-- <top-item /> -->
<base-table style="width: 100%;" :page="1" :limit="10" :show-index="true" :beilv="1" :tableConfig="tableProps"
:table-data="tableData" />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
import baseTable from './baseTable.vue'
import { parseTime } from "@/utils/ruoyi";
export default {
name: 'ProductionStatus',
components: { Container, baseTable },
// mixins: [resize],
props: {
tableData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
productionOverviewVo: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
type: Object,
default: () => ({})
}
},
data() {
return {
chart: null,
baseList: [
{ value: 1, name: "总览" },
{ value: 2, name: "宜兴" },
{ value: 3, name: "漳州" },
{ value: 4, name: "自贡" },
{ value: 5, name: "桐城" },
{ value: 6, name: "洛阳" },
{ value: 7, name: "合肥" },
],
tableProps: [
// { prop: 'id', label: '序号', width: 50, align: 'center' },
// { prop: 'id', label: '序号', width: 50, align: 'center' },
{
prop: 'levelId', label: '基地', align: 'center',
filter: (levelId) => this.getBaseName(levelId)
},
{ prop: 'type', label: '物料类别', align: 'center' },
{ prop: 'name', label: '物料名称', align: 'center' },
{ prop: 'code', label: '物料编码', align: 'center' },
{
prop: 'time', label: '领用日期', align: 'center',
filter: parseTime,
},
{ prop: 'num', label: '数量', align: 'center' },
]
}
},
mounted() {
// 初始化图表(若需展示图表,需在模板中添加对应 DOM
// this.$nextTick(() => this.updateChart())
},
methods: {
getBaseName(levelId) {
if (!levelId) return '';
// 从baseList中匹配对应的基地名称无匹配则返回原始levelId
const baseItem = this.baseList.find(item => item.value === levelId);
return baseItem ? baseItem.name : levelId;
},
}
}
</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 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: 458px">
<operatingLineBar :echartData="echartData" style="height: 99%; width: 100%" />
</div>
</div>
</template>
<script>
import operatingLineBar from './operatingLineBar.vue';
export default {
name: "Container",
components: { operatingLineBar },
props: ["name", "size", "icon", 'costGroupData', 'costTypes', 'locations'],
data() {
return {
isDropdownShow: false,
selectedProfit: '', // 默认选中"原料"
echartData: {
locations: [],
target: [],
value: [],
proportion: [],
flag: [] // 接收 flag 数组0/1
},
profitOptions: [], // 可根据实际需求修改选项
};
},
computed: {
defaultProfit() {
// 若 costTypes 有数据,取第一个;否则默认空
return this.profitOptions.length > 0 ? this.profitOptions[0] : '';
},
},
watch: {
defaultProfit(newVal) {
// 仅在 selectedProfit 为空时赋值(避免用户手动选择后被覆盖)
if (!this.selectedProfit && newVal) {
this.selectedProfit = newVal;
this.getEchartData();
}
},
costTypes(newVal) {
if (newVal && newVal.length > 0) {
this.profitOptions = newVal;
// 关键costTypes 变化时,若未选中值则自动选第一个
if (!this.selectedProfit) {
this.selectedProfit = newVal[0];
this.getEchartData();
}
}
},
// 监听 locations 变化,重新加载数据(避免数据依赖未更新)
locations() {
if (this.selectedProfit) {
this.getEchartData();
}
}
},
methods: {
selectProfit(item) {
this.selectedProfit = item;
this.isDropdownShow = false;
this.getEchartData();
},
getEchartData() {
// 若 selectedProfit 无值,返回空数据
if (!this.selectedProfit || !this.profitOptions.length) return;
// 从 costGroupData 中获取当前选中类型的数据(如“原片”“加工”)
const selectedTypeData = this.costGroupData[this.selectedProfit] || {};
// 整理数据格式:按地名分组,提取 target/value/proportion
const locations = [];
const target = [];
const value = [];
const proportion = [];
const flag = [];
// 遍历所有地名,对应提取数据
this.locations.forEach(location => {
const locationData = selectedTypeData[location] || [];
const firstItem = locationData[0] || {}; // 每组取第一条数据(可根据需求调整)
locations.push(location);
target.push(firstItem.target || 0);
value.push(firstItem.value || 0);
proportion.push(firstItem.proportion || 0);
flag.push(firstItem.flag || 0);
});
// 更新 ECharts 数据(包含 flag 字段)
this.echartData = {
locations, // x轴地名列表
target, // 目标值数组
value, // 实际值数组
proportion,
flag
};
}
},
mounted() {
// 点击外部关闭下拉菜单
const handleOutsideClick = (e) => {
if (!this.$el.contains(e.target)) {
this.isDropdownShow = false;
}
};
// 关键:页面加载时强制初始化
setTimeout(() => {
// 优先使用 costTypes 数据初始化
if (this.costTypes && this.costTypes.length > 0) {
this.profitOptions = this.costTypes;
this.selectedProfit = this.costTypes[0];
this.getEchartData();
}
// 若 costTypes 未及时加载,用 profitOptions 初始化
else if (this.profitOptions.length > 0) {
this.selectedProfit = this.profitOptions[0];
this.getEchartData();
}
}, 100); // 轻微延迟,确保 props 数据已传递
document.addEventListener('click', handleOutsideClick);
this.$once('hook:beforeDestroy', () => {
document.removeEventListener('click', handleOutsideClick);
});
}
};
</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;
margin-left: 0px;
gap: 16px;
width: 100%;
.right-container {
display: flex;
align-items: center;
gap: 24px;
}
.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: 102px;
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: 102px;
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,336 @@
<template>
<div style="width: 100%;">
<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: 420px; display: flex; width: 100%;background-color: rgba(249, 252, 255, 1);">
<!-- <top-item /> -->
<costBar />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
import costBar from './costBar.vue'
export default {
name: 'ProductionStatus',
components: { Container, costBar },
// mixins: [resize],
props: {
trendData: { // 接收父组件传递的设备数据数组
type: Array,
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,238 @@
<template>
<!-- 外层滚动容器添加鼠标事件监听 -->
<div class="coreItem-container" @mousedown="handleMouseDown" @mousemove="handleMouseMove" @mouseup="handleMouseUp"
@mouseleave="handleMouseUp" :style="{ cursor: isDragging ? 'grabbing' : 'grab' }">
<div class="coreItem">
<div class="item" v-for="(item, index) in itemList" :key="index">
<div class="item-header">
<div class="unit">{{ item.unit }}</div>
</div>
<div class="item-content">
<div class="content-wrapper">
<!-- 目标值 + 分割线 + 实际值 -->
<div class="value-group">
<div class="value-item">
<div class="number">{{ item.targetValue }}</div>
<div class="title">目标值</div>
</div>
<div class="middle-line"></div>
<div class="value-item">
<div class="number">{{ item.currentValue }}</div>
<div class="title">当前值</div>
</div>
</div>
<!-- 进度条 + 完成率 -->
<div class="progress-yield-group">
<div class="progress-group">
<div class="progress-container">
<div class="progress-bar" :style="{ width: item.progress + '%' }"></div>
</div>
</div>
<div class="yield">
<span class="progress-percent">完成率</span>
<span class="progress-percent progress-value">{{ item.progress }}%</span>
</div>
</div>
</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 },
{ unit: "经营性利润·万元", targetValue: 16, currentValue: 15.2, progress: 85 },
{ unit: "利润总额·万元", targetValue: 16, currentValue: 15.2, progress: 85 },
{ unit: "毛利率·%", targetValue: 16, currentValue: 15.2, progress: 85 },
{ unit: "销量·万㎡", targetValue: 20, currentValue: 16, progress: 80 },
{ unit: "双镀面板·万㎡", targetValue: 15, currentValue: 13.8, progress: 92 }
],
// 拖拽相关状态
isDragging: false, // 是否正在拖拽
startX: 0, // 拖拽开始时的鼠标X坐标
scrollLeft: 0 // 拖拽开始时的滚动条位置
};
},
methods: {
// 鼠标按下:开始拖拽
handleMouseDown(e) {
this.isDragging = true;
this.startX = e.pageX - this.$el.offsetLeft; // 记录初始鼠标X坐标相对容器
this.scrollLeft = this.$el.scrollLeft; // 记录初始滚动位置
this.$el.style.userSelect = "none"; // 防止拖拽时选中文字
},
// 鼠标移动:处理拖拽滚动
handleMouseMove(e) {
if (!this.isDragging) return;
e.preventDefault(); // 阻止默认行为(如选中文本)
const x = e.pageX - this.$el.offsetLeft; // 计算当前鼠标X坐标相对容器
const walk = (x - this.startX) * 1.2; // 计算移动距离乘以1.2增加灵敏度)
this.$el.scrollLeft = this.scrollLeft - walk; // 更新滚动位置
},
// 鼠标松开/离开:结束拖拽
handleMouseUp() {
this.isDragging = false;
this.$el.style.userSelect = ""; // 恢复文本选择
}
}
};
</script>
<style scoped lang="scss">
.coreItem-container {
width: 100%;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
// 隐藏滚动条(保留功能)
&::-webkit-scrollbar {
height: 0; // 彻底隐藏横向滚动条
width: 0;
}
scrollbar-width: none; // Firefox
-ms-overflow-style: none; // IE/Edge
// 拖拽时的光标样式(增强交互体验)
cursor: grab;
&:active {
cursor: grabbing;
}
}
.coreItem {
display: flex;
gap: 16px;
padding: 0 8px;
width: fit-content; // 宽度由子项总宽度决定
min-width: 100%; // 子项较少时铺满容器
}
.item {
width: 202px;
height: 180px;
background: #f9fcff;
padding: 10px 20px 0 27px;
box-sizing: border-box;
transition: all 0.3s ease;
.item-header {
margin-bottom: 11px;
}
.unit {
font-family: PingFangSC, PingFang SC;
font-weight: 500;
font-size: 16px;
color: #333333;
line-height: 1.2;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-content {
display: flex;
flex-direction: column;
height: calc(100% - 30px);
}
.content-wrapper {
display: flex;
flex-direction: column;
gap: 12px;
flex: 1;
}
.value-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.value-item {
display: flex;
flex-direction: column;
}
.middle-line {
width: 100%;
height: 1px;
background: linear-gradient(to right, rgba(40, 203, 151, 0.3), rgba(40, 203, 151, 0.8), rgba(40, 203, 151, 0.3));
}
.number {
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 22px;
color: #666666;
line-height: 1.2;
text-align: left;
}
.title {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #999999;
line-height: 1.2;
text-align: left;
}
.progress-yield-group {
display: flex;
flex-direction: column;
gap: 0;
}
.progress-group {
display: flex;
align-items: center;
}
.progress-container {
width: 100%;
height: 8px;
background: #F5F7FA;
border-radius: 4px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: rgba(98, 213, 180, .7);
border-radius: 4px;
transition: width 0.5s ease;
}
.yield {
display: flex;
justify-content: space-between;
margin-top: 0;
}
.progress-percent {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #999999;
line-height: 1;
}
.progress-value {
color: #28CB97;
font-weight: 500;
}
}
</style>

View File

@@ -0,0 +1,251 @@
<template>
<div class="coreItem-container" @mousedown="handleMouseDown" @mousemove="handleMouseMove" @mouseup="handleMouseUp"
@mouseleave="handleMouseUp" :style="{ cursor: isDragging ? 'grabbing' : 'grab' }">
<div class="coreItem">
<div class="item" @click="handleRoute(item)" v-for="(item, index) in itemList" :key="index">
<div class="item-header">
<div class="unit">{{ item.name }}</div>
</div>
<div class="item-content">
<div class="content-wrapper">
<div class="value-group">
<div class="value-item">
<div class="number" style="color: rgba(103, 103, 103, 0.79);">{{ item.target }}</div>
<div class="title">目标值</div>
</div>
<div class="middle-line"></div>
<!-- 当前值数字根据 flag 变色 -->
<div class="value-item">
<div class="number" :style="{ color: item.flag === 1 ? '#36B58A' : '#F9A44A' }">
{{ item.value }}
</div>
<div class="title">当前值</div>
</div>
</div>
<div class="progress-yield-group">
<div class="progress-group">
<div class="progress-container">
<!-- 进度条根据 flag 变色 -->
<div class="progress-bar" :style="{
width: (item.proportion ? item.proportion : 0) + '%',
background: item.flag === 1 ? 'rgba(54, 181, 138, 1)' : 'rgba(249, 164, 74, 1)'
}"></div>
</div>
</div>
<div class="yield">
<span class="progress-percent" :style="{ color: item.flag === 1 ? '#36B58A' : '#F9A44A' }">完成率</span>
<!-- 完成率百分比根据 flag 变色 -->
<span class="progress-percent progress-value"
:style="{ color: item.flag === 1 ? '#36B58A' : '#F9A44A' }">
{{ (item.proportion ? item.proportion : 0 ) }}%
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Container",
components: {},
props: ["name", "size", "icon", "itemList",'route'],
data() {
return {
// parentItemList: [
// { name: "燃料成本", target: 0, value: 0, proportion: 0, flag: 0 },
// { name: "天然气", target: 0, value: 0, proportion: 0, flag: 0 }
// ],
isDragging: false,
startX: 0,
scrollLeft: 0
};
},
methods: {
handleRoute(item) {
this.$router.push({
name: item.route,
query: {
name: item.name,
}
})
},
handleMouseDown(e) {
this.isDragging = true;
this.startX = e.pageX - this.$el.offsetLeft;
this.scrollLeft = this.$el.scrollLeft;
this.$el.style.userSelect = "none";
},
handleMouseMove(e) {
if (!this.isDragging) return;
e.preventDefault();
const x = e.pageX - this.$el.offsetLeft;
const walk = (x - this.startX) * 1.2;
this.$el.scrollLeft = this.scrollLeft - walk;
},
handleMouseUp() {
this.isDragging = false;
this.$el.style.userSelect = "";
}
}
};
</script>
<style scoped lang="scss">
/* 样式保持不变,仅删除 progress-bar 和 progress-value 的固定颜色 */
.coreItem-container {
width: 100%;
height: 270px;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
&::-webkit-scrollbar {
height: 0;
width: 0;
}
scrollbar-width: none;
-ms-overflow-style: none;
cursor: grab;
&:active {
cursor: grabbing;
}
}
.coreItem {
display: flex;
gap: 16px;
padding: 0 8px;
width: fit-content;
min-width: 100%;
}
.item {
width: 280px;
height: 265px;
background: #f9fcff;
padding: 10px 20px 0 27px;
box-sizing: border-box;
transition: all 0.3s ease;
&:hover {
box-shadow: 0px 4px 12px 2px #B5CDE5;
}
.item-header {
margin-bottom: 11px;
}
.unit {
font-family: PingFangSC, PingFang SC;
font-weight: 500;
font-size: 18px;
color: #333333;
line-height: 1.2;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-content {
display: flex;
flex-direction: column;
height: calc(100% - 30px);
}
.content-wrapper {
display: flex;
flex-direction: column;
gap: 12px;
flex: 1;
}
.value-group {
display: flex;
flex-direction: column;
gap: 24px;
}
.value-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.middle-line {
width: 207px;
height: 1px;
// border: 1px solid;
background: linear-gradient(to left, rgba(255, 0, 0, 0), #cbcbcb);
}
.number {
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 24px;
line-height: 1.2;
text-align: left;
/* 移除固定颜色,由模板动态绑定 */
}
.title {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: #999999;
line-height: 1.2;
text-align: left;
}
.progress-yield-group {
display: flex;
flex-direction: column;
gap: 0;
}
.progress-group {
display: flex;
align-items: center;
}
.progress-container {
width: 100%;
height: 8px;
background: #F5F7FA;
border-radius: 4px;
overflow: hidden;
}
.progress-bar {
height: 100%;
border-radius: 4px;
transition: width 0.5s ease;
/* 移除固定背景色,由模板动态绑定 */
}
.yield {
display: flex;
justify-content: space-between;
margin-top: 4px;
}
.progress-percent {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #999999;
line-height: 1;
}
.progress-value {
font-weight: 500;
/* 移除固定颜色,由模板动态绑定 */
}
}
</style>

View File

@@ -0,0 +1,145 @@
<template>
<div style="flex: 1">
<Container name="原片成本趋势图" icon="cockpitItemIcon" size="costSmallBg" topSize="psiTopTitleBasic">
<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);">
<profitBar :cost-group-data="costGroupData" :cost-types="costTypes" :locations="locations" />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
import profitBar from './profitBar.vue'
export default {
name: 'ProductionStatus',
components: { Container, profitBar },
props: {
trendViews: {
type: Array,
default: () => []
}
},
data() {
return {
chart: null
}
},
computed: {
// 1. 地名数组(去重,按出现顺序)
locations() {
const locationSet = new Set();
this.trendViews.forEach(item => {
const location = this.extractLocation(item.name);
if (location) locationSet.add(location);
});
return Array.from(locationSet);
},
// 2. 核心成本类型数组(仅保留“原片”“加工”等,去掉“成本”二字)
costTypes() {
const typeSet = new Set();
this.trendViews.forEach(item => {
const coreType = this.extractCoreCostType(item.name);
if (coreType) typeSet.add(coreType);
});
return Array.from(typeSet);
},
// 3. 按「核心成本类型+地名」分组的数据
costGroupData() {
return this.trendViews.reduce((group, item) => {
const coreType = this.extractCoreCostType(item.name) || '其他';
const location = this.extractLocation(item.name) || '未知地区';
// 初始化层级分组
if (!group[coreType]) group[coreType] = {};
if (!group[coreType][location]) group[coreType][location] = [];
// 添加数据
group[coreType][location].push({
name: item.name,
target: item.target ?? 0,
value: item.value ?? 0,
proportion: item.proportion ?? 0,
flag: item.flag ?? 0,
time: item.time
});
return group;
}, {});
}
},
methods: {
// 提取核心成本类型(仅保留“原片”“加工”等,去掉“成本”二字)
extractCoreCostType(name) {
// 匹配“前缀(含分公司/工厂/区域等)+ 核心词 + 任意后缀”,提取中间核心词
// 兼容后缀成本、费用、支出、损耗等前缀XX分公司、XX工厂、XX区域等
const match = name.match(/(分公司)\s*([^,。;!?]+?)\s*(成本)/);
if (match) {
return match[2].trim(); // 提取中间核心词,去除前后空格
}
// 通用匹配:无明确前缀标识时,提取“最后一个连续汉字后缀”前的内容
const generalMatch = name.match(/(.+?)(?=[\u4e00-\u9fa5]{2,}$)/);
return generalMatch ? generalMatch[1].trim() : name.trim();
},
// 提取地名(成本类型前的核心地区名)
extractLocation(name) {
// 匹配“分公司”前面的连续汉字(即地名)
const match = name.match(/(.+?)分公司/);
if (match) {
return match[1].trim() || '未知地区';
}
// 兜底(处理不含“分公司”的情况)
return '未知地区';
}
},
watch: {
trendViews: {
handler(newValue) {
console.log('核心成本类型:', this.costTypes); // 示例输出:["原片", "加工"]
console.log('地名数组:', this.locations); // 示例输出:["桐城", "合肥"]
console.log('分组数据:', this.costGroupData);
},
deep: true
}
},
mounted() {
console.log('初始核心成本类型:', this.costTypes);
}
};
</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;
}
/* 其他样式省略 */
</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,325 @@
<template>
<!-- 外层滚动容器添加鼠标事件监听 -->
<div class="coreItem-container" @mousedown="handleMouseDown" @mousemove="handleMouseMove" @mouseup="handleMouseUp"
@mouseleave="handleMouseUp" :style="{ cursor: isDragging ? 'grabbing' : 'grab' }">
<div class="coreItem">
<div class="item" @click="handleRoute(item.route, item.name)" v-for="(item, index) in finalItemList" :key="index">
<div class="item-header">
<div class="unit">{{ item.name }}</div>
</div>
<div class="item-content">
<div class="content-wrapper">
<!-- 目标值 + 分割线 + 实际值 -->
<div class="value-group">
<div class="value-item">
<div class="number">{{ item.targetValue || 0 }}</div>
<div class="title">目标值</div>
</div>
<div class="middle-line"></div>
<div class="value-item">
<!-- 实际值根据 flag 动态绑定颜色 -->
<div class="number" :style="{ color: getColorByFlag(item.flag) }">
{{ item.value || 0 }}
</div>
<div class="title">当前值</div>
</div>
</div>
<!-- 进度条 + 完成率 -->
<div class="progress-yield-group">
<div class="progress-group">
<div class="progress-container">
<div class="progress-bar" :style="{
width: getProportionPercent(item.proportion) + '%',
background: getColorByFlag(item.flag, true)
}"></div>
</div>
</div>
<div class="yield">
<span class="progress-percent">完成率</span>
<!-- 完成率动态颜色 + 百分比格式 -->
<span class="progress-percent progress-value" :style="{ color: getColorByFlag(item.flag) }">
{{ getProportionPercent(item.proportion) }}%
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Container",
components: {},
props: ["name", "size", "icon", 'piecesCostViews'],
data() {
return {
itemList: [
{ name: "原料", targetValue: 0, value: 0, proportion: 0, flag: 1, route: 'rawMaterialCostAnalysis/rawMaterialCostAnalysis' },
{ name: "人工", targetValue: 0, value: 0, proportion: 0, flag: 1 },
{
name: "燃料", targetValue: 0, value: 0, proportion: 0, flag: 1,
route: 'fuelCostAnalysis/fuelCostAnalysis'
},
{
name: "电", targetValue: 0, value: 0, proportion: 0, flag: 1,
route: 'fuelPowerCostAnalysis/fuelPowerCostAnalysis'
},
{
name: "制造费用", targetValue: 0, value: 0, proportion: 0, flag: 1,
route: 'productionCostsAnalysis/productionCostsAnalysis'
},
],
isDragging: false,
startX: 0,
scrollLeft: 0
};
},
// computed: {
// finalItemList() {
// if (Array.isArray(this.piecesCostViews) && this.piecesCostViews.length > 0) {
// return this.piecesCostViews.map((item) => ({
// flag: item.flag || 0, // 默认 flag 为 0
// proportion: item.proportion || 0, // 默认完成率为 0
// targetValue: item.target ?? 0, // 兼容 null 值,显示为 0
// value: item.value || 0,
// name: item.name
// }));
// } else {
// return this.itemList;
// }
// }
// },
computed: {
finalItemList() {
return this.itemList.map((defaultItem, index) => {
// 核心修复1. 恢复模板名称处理2. 统一大小写和空格处理
const matchedItem = this.piecesCostViews.find(item => {
const processedIncomingName = (item.name || '').replace(/成本/g, '').trim().toLowerCase();
// 恢复模板名称处理:去除“成本”(如果有的话)、去空格、小写
const processedDefaultName = defaultItem.name.replace(/成本/g, '').trim().toLowerCase();
return processedIncomingName === processedDefaultName;
});
// 移除索引 fallback只匹配名称不按索引赋值避免误匹配
// 无匹配项则完全保留默认值
const targetItem = matchedItem || defaultItem;
return {
name: defaultItem.name, // 固定模板名称
flag: targetItem.flag ?? defaultItem.flag,
proportion: targetItem.proportion ?? defaultItem.proportion,
targetValue: targetItem.target ?? defaultItem.targetValue,
value: targetItem.value ?? defaultItem.value,
route: defaultItem.route
};
});
}
},
methods: {
handleRoute(route, name) {
if (route) {
this.$router.push({
path: route,
query: {
name: name + '成本'
}
})
}
},
// 根据 flag 获取文字/进度条颜色
getColorByFlag(flag, isProgressBar = false) {
const colorMap = {
1: {
text: 'rgba(103, 103, 103, 0.79)',
progress: 'rgba(103, 103, 103, 0.5)' // 进度条颜色稍浅
},
0: {
text: 'rgba(255, 132, 0, 1)',
progress: 'rgba(255, 132, 0, 0.7)' // 进度条颜色稍浅
}
};
const currentFlag = flag === 1 ? 1 : 0;
return isProgressBar ? colorMap[currentFlag].progress : colorMap[currentFlag].text;
},
// 处理 proportion乘以 100 并保留 0 位小数
getProportionPercent(proportion) {
const num = Number(proportion) || 0;
return num; // 四舍五入取整,如需保留小数可改为 toFixed(2)
},
// 拖拽相关方法
handleMouseDown(e) {
this.isDragging = true;
this.startX = e.pageX - this.$el.offsetLeft;
this.scrollLeft = this.$el.scrollLeft;
this.$el.style.userSelect = "none";
},
handleMouseMove(e) {
if (!this.isDragging) return;
e.preventDefault();
const x = e.pageX - this.$el.offsetLeft;
const walk = (x - this.startX) * 1.2;
this.$el.scrollLeft = this.scrollLeft - walk;
},
handleMouseUp() {
this.isDragging = false;
this.$el.style.userSelect = "";
}
}
};
</script>
<style scoped lang="scss">
.coreItem-container {
width: 100%;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
&::-webkit-scrollbar {
height: 0;
width: 0;
}
scrollbar-width: none;
-ms-overflow-style: none;
cursor: grab;
&:active {
cursor: grabbing;
}
}
.coreItem {
display: flex;
gap: 16px;
padding: 0 8px;
width: fit-content;
min-width: 100%;
}
.item {
width: 202px;
height: 180px;
background: #f9fcff;
padding: 10px 20px 0 27px;
box-sizing: border-box;
transition: all 0.3s ease;
&:not(:nth-child(2)):hover {
box-shadow: 0px 4px 12px 2px #B5CDE5;
transform: translateY(-2px);
}
.item-header {
margin-bottom: 11px;
}
.unit {
font-family: PingFangSC, PingFang SC;
font-weight: 500;
font-size: 16px;
color: #333333;
line-height: 1.2;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-content {
display: flex;
flex-direction: column;
height: calc(100% - 30px);
}
.content-wrapper {
display: flex;
flex-direction: column;
gap: 12px;
flex: 1;
}
.value-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.value-item {
display: flex;
flex-direction: column;
}
.middle-line {
width: 100%;
height: 1px;
background: linear-gradient(to right, rgba(40, 203, 151, 0.3), rgba(40, 203, 151, 0.8), rgba(40, 203, 151, 0.3));
}
.number {
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 22px;
line-height: 1.2;
text-align: left;
/* 颜色由动态样式控制,移除固定颜色 */
}
.title {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #999999;
line-height: 1.2;
text-align: left;
}
.progress-yield-group {
display: flex;
flex-direction: column;
gap: 0;
}
.progress-group {
display: flex;
align-items: center;
}
.progress-container {
width: 100%;
height: 8px;
background: #F5F7FA;
border-radius: 4px;
overflow: hidden;
}
.progress-bar {
height: 100%;
border-radius: 4px;
transition: width 0.5s ease;
/* 背景色由动态样式控制,移除固定颜色 */
}
.yield {
display: flex;
justify-content: space-between;
margin-top: 0;
}
.progress-percent {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #999999;
line-height: 1;
}
.progress-value {
font-weight: 500;
/* 颜色由动态样式控制,移除固定颜色 */
}
}
</style>

View File

@@ -0,0 +1,229 @@
<template>
<div class="cockpitContainer" :class="['cockpitContainer__' + size]">
<div class="content-top" :class="['content-top__' + topSize]">
<div class="title-wrapper">
<svg-icon class="title-icon" :icon-class="icon" />
<span class="title-text">
{{ name }}
</span>
</div>
<!-- 按钮单独放在右侧容器 -->
<div class="button-wrapper">
<el-button @click="handleRoute" type="text">利润影响额</el-button>
</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', 'topSize'],
data() {
return {};
},
computed: {},
methods: {
handleRoute() {
this.$router.push({
name:'profitImpactAnalysis'
})
},
},
};
</script>
<style scoped lang="scss">
.cockpitContainer {
display: inline-block;
padding: 6px;
display: flex;
flex-direction: column;
position: relative;
.content-top {
height: 60px;
display: flex;
/* 开启flex布局 */
justify-content: space-between;
/* 标题居左,按钮居右 */
align-items: center;
/* 垂直居中 */
padding: 0 10px;
/* 左右内边距,避免贴边 */
.title-wrapper {
display: flex;
align-items: center;
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;
}
// 按钮容器样式
.button-wrapper {
margin-left: auto;
margin-top: 10px;
/* 强制靠右兼容flex布局 */
}
&__basic {
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;
}
&__costMiddleBg {
background: url(../../../assets/img/costMiddleBg.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__costSmallBg {
background: url(../../../assets/img/costSmallBg.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;
}
&__productBasicBg {
background: url(../../../assets/img/productBasicBg.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
&__productMiddleBg {
background: url(../../../assets/img/productMiddleBg.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;
backdrop-filter: blur(5px);
z-index: -1;
}
}
.container-body {
flex: 1;
}
</style>

View File

@@ -0,0 +1,353 @@
<template>
<div style="flex: 1">
<Container :name="name" icon="cockpitItemIcon" size="operatingBasic" topSize="middle">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex;flex-direction: column; width: 100%;">
<!-- 2. .top 保持 flex无需固定高度自动跟随子元素拉伸 -->
<div style="display: flex; width: 100%; background-color: rgba(249, 252, 255, 1);">
<costBaseBarChart />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from '../container.vue'
// import * as echarts from 'echarts'
import costBaseBarChart from './costBaseBarChart.vue'
export default {
name: 'ProductionStatus',
components: { Container, costBaseBarChart },
// mixins: [resize],
props: {
leftEqInfoData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
name: { // 接收父组件传递的设备数据数组
type: String,
default: () => '' // 默认空数组,避免报错
},
productionOverviewVo: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
type: Object,
default: () => ({})
}
},
data() {
return {
chart: null,
parentItemList: [
{ 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: 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;
}
</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,353 @@
<template>
<div style="flex: 1">
<Container :name="name" icon="cockpitItemIcon" size="operatingBasic" topSize="middle">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex;flex-direction: column; width: 100%;">
<!-- 2. .top 保持 flex无需固定高度自动跟随子元素拉伸 -->
<div style="display: flex; width: 100%; background-color: rgba(249, 252, 255, 1);">
<costBaseBarChart />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from '../container.vue'
// import * as echarts from 'echarts'
import costBaseBarChart from './costBaseBarChart.vue'
export default {
name: 'ProductionStatus',
components: { Container, costBaseBarChart },
// mixins: [resize],
props: {
leftEqInfoData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
name: { // 接收父组件传递的设备数据数组
type: String,
default: () => '' // 默认空数组,避免报错
},
productionOverviewVo: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
type: Object,
default: () => ({})
}
},
data() {
return {
chart: null,
parentItemList: [
{ 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: 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;
}
</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,236 @@
<template>
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 345px;"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
components: {},
data() {
return {};
},
mounted() {
this.$nextTick(() => {
this.initData();
});
},
methods: {
initData() {
const chartDom = this.$refs.cockpitEffChip;
if (!chartDom) {
console.error('图表容器未找到!');
return;
}
const myChart = echarts.init(chartDom);
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
},
// 优化tooltip内容区分各系列含义
formatter: (params) => {
let html = `${params[0].axisValue}<br/>`;
params.forEach(item => {
// 直接使用系列名,无需映射,仅保留单位判断
html += `${item.marker} ${item.seriesName}: ${item.value}${item.seriesName === '完成率' ? '%' : '片'}<br/>`;
});
return html;
}
},
grid: {
top: 50,
bottom: 30, // 增大底部间距避免柱子与X轴标签重叠
right: 70,
left: 50,
},
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: ['漳州', '桐城', '合肥', '宜兴', '自贡', '洛阳']
}
],
yAxis: [
// 左侧Y轴目标/达标/未达标(数量,单位“片”)
{
type: 'value',
// name: '元/吨',
nameTextStyle: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
align: 'right'
},
min: 0, // 最小值设0确保柱子从X轴底部开始不超过X轴
max: (value) => Math.ceil(value.max * 1.1), // 最大值留10%余量,避免柱子顶满
scale: false, // 关闭缩放强制从0开始
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',
// // name: '%',
// nameTextStyle: {
// color: 'rgba(0, 0, 0, 0.45)',
// fontSize: 12,
// align: 'left'
// },
// min: 0,
// max: 100, // 完成率最大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, // 绑定右侧Y轴
// 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: [65, 78, 52, 85, 60, 95, 72], // 完成率数据0-100
// symbol: 'circle', // 数据点为圆形
// symbolSize: 6 // 数据点大小
// },
// 2. 目标柱状图绑定左侧数量Y轴
{
name: '销量',
type: 'bar',
yAxisIndex: 0,
barWidth: 24,
// 关键修复label 直接放在 series 下,而非 itemStyle 内
itemStyle: {
// 移除多余的 normal 层级,直接配置 color 渐变
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: [200, 280, 180, 300, 220, 350]
},
// 3. 达标柱状图绑定左侧数量Y轴
// {
// name: '产量',
// type: 'bar',
// yAxisIndex: 0,
// barWidth: 24,
// // 关键修复label 直接放在 series 下,而非 itemStyle 内
// itemStyle: {
// // 移除多余的 normal 层级,直接配置 color 渐变
// 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: [130, 220, 95, 255, 132, 332] // 达标数据(小于目标)
// },
// 4. 未达标柱状图绑定左侧数量Y轴
// {
// name: '未达标',
// type: 'bar',
// yAxisIndex: 0,
// barWidth: 18,
// itemStyle: {
// color: 'rgba(249, 164, 74, 1)',
// borderRadius: [4, 4, 0, 0],
// borderWidth: 0
// },
// data: [70, 60, 85, 45, 88, 18, 78] // 未达标数据(目标-达标)
// }
],
// 图例:区分各系列,点击可控制显示隐藏
// legend: {
// top: 0,
// left: 'center',
// itemWidth: 12,
// itemHeight: 8,
// textStyle: {
// color: 'rgba(0, 0, 0, 0.45)',
// fontSize: 12
// },
// data: ['完成率', '目标', '达标', '未达标']
// }
};
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,496 @@
<template>
<header class="report-header" :class="['report-header__' + size]">
<!-- 左侧区域logo + 标题 -->
<div class="left-content">
<!-- <img style="height: 36px;" src="../../../assets/img/cnbm.png" alt="benmaLogo" class="logo"> -->
<div class="top-title">{{ topTitle }}</div>
</div>
<!--
<div class="center-content">
<div class="item" v-for="(text, index) in ['营业收入', '利润分析', '产销率库存分析', '成本分析', '驾驶舱报表']" :key="index"
@click="activeIndex = index" :class="{ 'no-skew': activeIndex === index }">
<span class="item-text">{{ text }}</span>
</div>
</div> -->
<!-- 右侧区域全屏按钮 -->
<div class="right-content">
<el-button type="text" class="home-btn" :title="'主页'" @click="handleRouteHome">
<svg-icon style="color: #0B58FF;" icon-class="homeIcon" />
</el-button>
<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>
<!-- 时间选择区域//年按钮 + label + 日期选择器 -->
<div class="timeType">
<!-- //年按钮点击更新activeTime关联选择器类型 -->
<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> <!-- 补充span抵消倾斜 -->
</div>
<!-- 日期选择器type和placeholder通过activeTime动态切换 -->
<el-date-picker v-model="date" :type="getPickerType" :placeholder="getPickerPlaceholder"
class="custom-date-picker" @change="emitTimeRange" :clearable="false"
style="width: 132px;height: 29px;"></el-date-picker>
</div>
</div>
</header>
</template>
<script>
import moment from "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,
// activeIndex: -1,
activeTime: 1, // 0=日1=月2=年(默认选中“日”)
// 定义时间类型配置text=按钮文字pickerType=选择器类型placeholder=占位符
timeTypes: [
{ text: '日', pickerType: 'date', placeholder: '选择日期' },
{ text: '月', pickerType: 'month', placeholder: '选择月份' },
{ text: '年', pickerType: 'year', placeholder: '选择年份' }
]
}
},
watch: {
// 维度切换时:清空选择的日期,并传递当前维度的默认区间
activeTime(newVal, oldVal) {
if (newVal !== oldVal) {
this.date = undefined;
// this.emitTimeRange();
}
}
},
computed: {
// 动态获取日期选择器类型
getPickerType() {
return this.timeTypes[this.activeTime].pickerType;
},
// 动态获取日期选择器占位符
getPickerPlaceholder() {
return this.timeTypes[this.activeTime].placeholder;
}
},
methods: {
handleRouteHome() {
this.$router.push({
path:'/'
})
},
handleReturn() {
this.$router.go(-1);
},
changeFullScreen() { this.$emit('screenfullChange') },
exportPDF() { this.$emit('exportPDF') },
padZero(num) { return num < 10 ? '0' + num : num },
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);
// 调试用:查看首次传递的区间(如{start: "2025-10-01T00:00:00", end: "2025-11-01T00:00:00", dimension: "月"}
console.log('当前时间区间:', timeRange);
}
},
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-between;
&__basic {
// width: 547px;
background: url(../../../../assets/img/topBg.png) no-repeat;
background-size: cover;
background-position: 0 0;
}
&__psi {
// width: 547px;
background: url(../../../../assets/img/psiTopTitle.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
box-sizing: border-box;
position: relative;
/* 确保timeType绝对定位生效 */
.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;
}
.center-content {
display: flex;
gap: 8px;
margin-top: 18px;
margin-left: 30px;
.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: 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;
/* 统一倾斜角度修复原30deg的错位 */
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);
/* 与按钮倾斜角度统一原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: 20px;
gap: 21px;
}
.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;
// }
// ::v-deep .custom-date-picker .el-input--medium .el-input__inner,
// ::v-deep .custom-date-picker .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: #fff !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);
// }
// ::v-deep .custom-date-picker .el-input__prefix {
// left: auto !important;
// right: 8px !important;
// top: 40% !important;
// transform: translateY(-50%) !important;
// }
// // ::v-deep .custom-date-picker .el-input__prefix .el-icon {
// // color: #fff !important;
// // font-size: 16px !important;
// // }
// ::v-deep .el-icon-date::before {
// color: #ffffff !important;
// // margin-bottom: 20px;
// font-size: 16px !important;
// }
// // 额外补充:如果图标有父级容器样式干扰,同步处理父容器
// ::v-deep .custom-date-picker .el-input__icon {
// color: #ffffff !important; // 清除父容器可能的颜色继承
// }
// ::v-deep .custom-date-picker .el-input__inner:hover,
// ::v-deep .custom-date-picker .el-input__inner:focus {
// background-color: rgba(11, 88, 255, 0.9) !important;
// clip-path: polygon(18px 0, 100% 0, 100% 100%, 0 100%) !important;
// }
::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,505 @@
<template>
<header class="report-header" :class="['report-header__' + size]">
<!-- 左侧区域logo + 标题 -->
<div class="left-content">
<!-- <img style="height: 36px;" src="../../../assets/img/cnbm.png" alt="benmaLogo" class="logo"> -->
<div class="top-title">{{ topTitle }}</div>
</div>
<!--
<div class="center-content">
<div class="item" v-for="(text, index) in ['营业收入', '利润分析', '产销率库存分析', '成本分析', '驾驶舱报表']" :key="index"
@click="activeIndex = index" :class="{ 'no-skew': activeIndex === index }">
<span class="item-text">{{ text }}</span>
</div>
</div> -->
<!-- 右侧区域全屏按钮 -->
<div class="right-content">
<!-- <el-button type="text" class="home-btn" :title="'主页'" @click="handleRouteHome">
<svg-icon style="color: #0B58FF;" icon-class="homeIcon" />
</el-button>
<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>
<!-- 时间选择区域//年按钮 + label + 日期选择器 -->
<div class="timeType">
<!-- //年按钮点击更新activeTime关联选择器类型 -->
<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> <!-- 补充span抵消倾斜 -->
</div>
<!-- 日期选择器type和placeholder通过activeTime动态切换 -->
<el-date-picker v-model="date" :type="getPickerType" :placeholder="getPickerPlaceholder"
class="custom-date-picker" @change="emitTimeRange" :clearable="false"
style="width: 132px;height: 29px;"></el-date-picker>
</div>
</div>
</header>
</template>
<script>
import moment from "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,
activeIndex: -1,
activeTime: 0, // 0=日1=月2=年(默认选中“日”)
// 定义时间类型配置text=按钮文字pickerType=选择器类型placeholder=占位符
timeTypes: [
{ text: '日', pickerType: 'date', placeholder: '选择日期' },
{ text: '月', pickerType: 'month', placeholder: '选择月份' },
{ text: '年', pickerType: 'year', placeholder: '选择年份' }
]
}
},
watch: {
// 维度切换时:清空选择的日期,并传递当前维度的默认区间
activeTime(newVal, oldVal) {
if (newVal !== oldVal) {
this.date = undefined;
this.emitTimeRange();
}
}
},
computed: {
// 动态获取日期选择器类型
getPickerType() {
return this.timeTypes[this.activeTime].pickerType;
},
// 动态获取日期选择器占位符
getPickerPlaceholder() {
return this.timeTypes[this.activeTime].placeholder;
}
},
methods: {
handleRouteHome() {
this.$router.push({
path: '/'
})
},
handleReturn() {
this.$router.go(-1);
},
changeFullScreen() { this.$emit('screenfullChange') },
exportPDF() { this.$emit('exportPDF') },
padZero(num) { return num < 10 ? '0' + num : num },
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);
// 调试用:查看首次传递的区间(如{start: "2025-10-01T00:00:00", end: "2025-11-01T00:00:00", dimension: "月"}
console.log('当前时间区间:', timeRange);
}
},
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-between;
&__basic {
// width: 547px;
background: url(../../../../assets/img/topBg.png) no-repeat;
background-size: cover;
background-position: 0 0;
}
&__psi {
// width: 547px;
background: url(../../../../assets/img/psiTopTitle.png) no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
box-sizing: border-box;
position: relative;
/* 确保timeType绝对定位生效 */
.left-content {
margin-top: 11px;
margin-left: 300px;
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: 30px;
.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: 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;
/* 统一倾斜角度修复原30deg的错位 */
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);
/* 与按钮倾斜角度统一原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: 20px;
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;
// }
// ::v-deep .custom-date-picker .el-input--medium .el-input__inner,
// ::v-deep .custom-date-picker .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: #fff !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);
// }
// ::v-deep .custom-date-picker .el-input__prefix {
// left: auto !important;
// right: 8px !important;
// top: 40% !important;
// transform: translateY(-50%) !important;
// }
// // ::v-deep .custom-date-picker .el-input__prefix .el-icon {
// // color: #fff !important;
// // font-size: 16px !important;
// // }
// ::v-deep .el-icon-date::before {
// color: #ffffff !important;
// // margin-bottom: 20px;
// font-size: 16px !important;
// }
// // 额外补充:如果图标有父级容器样式干扰,同步处理父容器
// ::v-deep .custom-date-picker .el-input__icon {
// color: #ffffff !important; // 清除父容器可能的颜色继承
// }
// ::v-deep .custom-date-picker .el-input__inner:hover,
// ::v-deep .custom-date-picker .el-input__inner:focus {
// background-color: rgba(11, 88, 255, 0.9) !important;
// clip-path: polygon(18px 0, 100% 0, 100% 100%, 0 100%) !important;
// }
::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,353 @@
<template>
<div style="flex: 1">
<Container :name="name" icon="cockpitItemIcon" size="operatingBasic" topSize="middle">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex;flex-direction: column; width: 100%;">
<!-- 2. .top 保持 flex无需固定高度自动跟随子元素拉伸 -->
<div style="display: flex; width: 100%; background-color: rgba(249, 252, 255, 1);">
<costBaseBarChart />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from '../single-container.vue'
// import * as echarts from 'echarts'
import costBaseBarChart from './costBaseBarChart.vue'
export default {
name: 'ProductionStatus',
components: { Container, costBaseBarChart },
// mixins: [resize],
props: {
leftEqInfoData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
name: { // 接收父组件传递的设备数据数组
type: String,
default: () => '' // 默认空数组,避免报错
},
productionOverviewVo: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
type: Object,
default: () => ({})
}
},
data() {
return {
chart: null,
parentItemList: [
{ 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: 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;
}
</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,353 @@
<template>
<div style="flex: 1">
<Container :name="name" icon="cockpitItemIcon" size="operatingBasic" topSize="middle">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex;flex-direction: column; width: 100%;">
<!-- 2. .top 保持 flex无需固定高度自动跟随子元素拉伸 -->
<div style="display: flex; width: 100%; background-color: rgba(249, 252, 255, 1);">
<costBaseBarChart />
</div>
</div>
</Container>
</div>
</template>
<script>
import Container from '../container.vue'
// import * as echarts from 'echarts'
import costBaseBarChart from './costBaseBarChart.vue'
export default {
name: 'ProductionStatus',
components: { Container, costBaseBarChart },
// mixins: [resize],
props: {
leftEqInfoData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
name: { // 接收父组件传递的设备数据数组
type: String,
default: () => '' // 默认空数组,避免报错
},
productionOverviewVo: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
type: Object,
default: () => ({})
}
},
data() {
return {
chart: null,
parentItemList: [
{ 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: 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;
}
</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,147 @@
<template>
<div style="">
<div class="topSelect">
<div class="baseSelect">
<div class="label">
基地
</div>
<el-select class="custom-select" style="margin-left: 10px;" v-model="levelId" placeholder="">
<el-option v-for="item in baseList" :key="item.value" :label="item.name" :value="item.value">
</el-option>
</el-select>
</div>
<div class="baseSelect" style="margin-left: 24px;">
<div class="label">
{{ typeName }}
</div>
<el-select class="custom-select" style="margin-left: 10px;" v-model="type" placeholder="">
<el-option v-for="item in TypeList" :key="item.value" :label="item.name" :value="item.value">
</el-option>
</el-select>
</div>
<el-button type="primary" style="margin-left: 24px;">查询</el-button>
</div>
</div>
</template>
<script>
export default {
name: 'ProductionStatus',
components: {},
props: ['typeName','typeList'],
data() {
return {
levelId: '',
baseList: [
{
value: 2,
name: "宜兴",
},
{
value: 3,
name: "漳州",
},
{
value: 4,
name: "自贡",
},
{
value: 5,
name: "桐城",
},
{
value: 6,
name: "洛阳",
},
{
value: 7,
name: "合肥",
},
]
}
},
methods: {
updateChart() { }
}
}
</script>
<style lang='scss' scoped>
.topSelect {
width: 1620px;
height: 60px;
background: url('../../../assets/img/topSelectBg.png') no-repeat;
background-size: 100% 100%;
display: flex;
align-items: center;
.baseSelect {
margin-left: 54px;
display: flex;
align-items: center;
.label {
height: 32px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 24px;
color: #000000;
line-height: 32px;
letter-spacing: 5px;
text-align: left;
font-style: normal;
}
}
}
::v-deep .custom-select {
width: 200px;
.el-input__inner {
border: 1px solid rgba(94, 142, 255, 1) !important;
border-radius: 4px;
height: 36px;
}
.el-input__inner:focus {
border-color: rgba(94, 142, 255, 1) !important;
box-shadow: 0 0 0 2px rgba(94, 142, 255, 0.2) !important;
}
.el-select__caret {
display: none !important;
}
// 默认状态:三角形朝右
.el-input__suffix {
right: 12px;
top: 50%;
transform: translateY(-20%);
/* 修正垂直居中 */
&::after {
content: '';
display: block;
width: 0;
height: 0;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-left: 6px solid rgba(94, 142, 255, 1);
transition: all 0.3s ease;
/* 平滑过渡 */
}
}
// 展开状态:三角形朝下(核心修改)
&.el-select-open .el-input__suffix::after {
border-left: 6px solid transparent !important;
border-right: 6px solid transparent !important;
border-top: 6px solid rgba(94, 142, 255, 1) !important;
border-bottom: none !important;
}
}
::v-deep .el-select-dropdown {
border: 1px solid rgba(94, 142, 255, 1) !important;
}
</style>

View File

@@ -0,0 +1,189 @@
<template>
<div class="coreItem">
<!-- v-for 动态生成每个 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">{{ 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 + '%' }"></div>
</div>
<div class="progress-percent">{{ 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: {},
};
</script>
<style scoped lang="scss">
.coreItem {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 8px;
}
.item {
width: 252px;
height: 110px;
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: 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: #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,301 @@
<template>
<div class="coreItem">
<div class="item item1">
<div class="unit">总成本·/</div>
<div class="item-content">
<!-- 左右内容容器包裹 leftlineright -->
<div class="content-wrapper">
<div class="left">
<div class="number">16</div>
<div class="title">目标值</div>
</div>
<div class="line"></div>
<div class="right">
<div class="number">16</div>
<div class="title">目标值</div>
</div>
</div>
<!-- 进度条和百分比容器横向排列 -->
<div class="progress-group">
<div class="progress-container">
<div class="progress-bar" :style="{
width: (90) + '%',
}"></div>
</div>
<!-- 百分比文本 -->
<div class="progress-percent">90%</div>
</div>
</div>
</div>
<div class="item item2">
<div class="unit">原片成本·/</div>
<div class="item-content">
<!-- 左右内容容器包裹 leftlineright -->
<div class="content-wrapper">
<div class="left">
<div class="number">16</div>
<div class="title">目标值</div>
</div>
<div class="line"></div>
<div class="right">
<div class="number">16</div>
<div class="title">目标值</div>
</div>
</div>
<!-- 进度条和百分比容器横向排列 -->
<div class="progress-group">
<div class="progress-container">
<div class="progress-bar" :style="{
width: (90) + '%',
}"></div>
</div>
<!-- 百分比文本 -->
<div class="progress-percent">90%</div>
</div>
</div>
</div>
<div class="item item3">
<div class="unit">加工成本·/</div>
<div class="item-content">
<!-- 左右内容容器包裹 leftlineright -->
<div class="content-wrapper">
<div class="left">
<div class="number">16</div>
<div class="title">目标值</div>
</div>
<div class="line"></div>
<div class="right">
<div class="number">16</div>
<div class="title">目标值</div>
</div>
</div>
<!-- 进度条和百分比容器横向排列 -->
<div class="progress-group">
<div class="progress-container">
<div class="progress-bar" :style="{
width: (90) + '%',
}"></div>
</div>
<!-- 百分比文本 -->
<div class="progress-percent">90%</div>
</div>
</div>
</div>
<div class="item item4">
<div class="unit">原片成品率·%</div>
<div class="item-content">
<!-- 左右内容容器包裹 leftlineright -->
<div class="content-wrapper">
<div class="left">
<div class="number">16</div>
<div class="title">目标值</div>
</div>
<div class="line"></div>
<div class="right">
<div class="number">16</div>
<div class="title">目标值</div>
</div>
</div>
<!-- 进度条和百分比容器横向排列 -->
<div class="progress-group">
<div class="progress-container">
<div class="progress-bar" :style="{
width: (90) + '%',
}"></div>
</div>
<!-- 百分比文本 -->
<div class="progress-percent">90%</div>
</div>
</div>
</div>
<div class="item item5">
<div class="unit">投入产出率·%</div>
<div class="item-content">
<!-- 左右内容容器包裹 leftlineright -->
<div class="content-wrapper">
<div class="left">
<div class="number">16</div>
<div class="title">目标值</div>
</div>
<div class="line"></div>
<div class="right">
<div class="number">16</div>
<div class="title">目标值</div>
</div>
</div>
<!-- 进度条和百分比容器横向排列 -->
<div class="progress-group">
<div class="progress-container">
<div class="progress-bar" :style="{
width: (90) + '%',
}"></div>
</div>
<!-- 百分比文本 -->
<div class="progress-percent">90%</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Container",
components: {},
props: ["name", "size", "icon"],
data() {
return {
progress: 90, // 进度值,方便统一控制
};
},
computed: {},
methods: {},
};
</script>
<style scoped lang="scss">
.coreItem {
display: flex;
flex-wrap: wrap;
/* 允许元素换行 */
// justify-content: center;
/* 整体水平居中 */
gap: 8px;
/* 元素之间的间距(水平+垂直) */
// width: 100%;
box-sizing: border-box;
// padding: 8px;
}
/* 需给子元素指定对应的区域名 */
.coreItem> :nth-child(1) {
grid-area: item1;
}
.coreItem> :nth-child(2) {
grid-area: item2;
}
.coreItem> :nth-child(3) {
grid-area: item3;
}
.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;
.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: 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: #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;
}
}
.item1,.item2,.item3{
width: 166px;
}
</style>

View File

@@ -0,0 +1,236 @@
<template>
<div style="flex: 1">
<Container name="总利润指标·元" icon="cockpitItemIcon" size="rawTopBg" topSize="rawTopTitleLarge">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%;">
<!-- 新增topItem 专属包裹容器统一控制样式和布局 -->
<div class=""
style="display: flex;width: 100%; background-color: rgba(249, 252, 255, 1);">
<!-- <topItem :itemList="parentItemList" /> -->
<totalProfitBar />
</div>
<!-- 2. .top 保持 flex无需固定高度自动跟随子元素拉伸 -->
<!-- <div class="top" style="display: flex; width: 100%;">
<top-item :itemList="parentItemList" />
</div> -->
</div>
</Container>
</div>
</template>
<script>
import Container from './container.vue'
// import * as echarts from 'echarts'
import totalProfitBar from './totalProfitBar.vue'
export default {
name: 'ProductionStatus',
components: { Container,totalProfitBar },
// mixins: [resize],
props: {
leftEqInfoData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [] // 默认空数组,避免报错
},
productionOverviewVo: { // 恢复生产概览数据(原代码注释了,需根据实际需求保留)
type: Object,
default: () => ({})
}
},
data() {
return {
chart: null,
parentItemList: [
{ unit: "销量", targetValue: 16, currentValue: 14.5, progress: 90 },
{ unit: "产量", targetValue: 16, currentValue: 15.2, progress: 85 },
]
}
},
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;
}
.leftTitle {
.item {
width: 67px;
height: 180px;
padding: 37px 23px;
background: #F9FCFF;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 18px;
color: #000000;
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,236 @@
<template>
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 280px;"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
components: {},
data() {
return {};
},
mounted() {
this.$nextTick(() => {
this.initData();
});
},
methods: {
initData() {
const chartDom = this.$refs.cockpitEffChip;
if (!chartDom) {
console.error('图表容器未找到!');
return;
}
const myChart = echarts.init(chartDom);
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
},
// 优化tooltip内容区分各系列含义
formatter: (params) => {
let html = `${params[0].axisValue}<br/>`;
params.forEach(item => {
// 直接使用系列名,无需映射,仅保留单位判断
html += `${item.marker} ${item.seriesName}: ${item.value}${item.seriesName === '完成率' ? '%' : '片'}<br/>`;
});
return html;
}
},
grid: {
top: 50,
bottom: 30, // 增大底部间距避免柱子与X轴标签重叠
right: 70,
left: 50,
},
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: ['漳州', '桐城', '合肥', '宜兴', '自贡', '洛阳']
}
],
yAxis: [
// 左侧Y轴目标/达标/未达标(数量,单位“片”)
{
type: 'value',
name: '元',
nameTextStyle: {
color: 'rgba(0, 0, 0, 0.45)',
fontSize: 12,
align: 'right'
},
min: 0, // 最小值设0确保柱子从X轴底部开始不超过X轴
max: (value) => Math.ceil(value.max * 1.1), // 最大值留10%余量,避免柱子顶满
scale: false, // 关闭缩放强制从0开始
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',
// // name: '%',
// nameTextStyle: {
// color: 'rgba(0, 0, 0, 0.45)',
// fontSize: 12,
// align: 'left'
// },
// min: 0,
// max: 100, // 完成率最大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, // 绑定右侧Y轴
// 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: [65, 78, 52, 85, 60, 95, 72], // 完成率数据0-100
// symbol: 'circle', // 数据点为圆形
// symbolSize: 6 // 数据点大小
// },
// 2. 目标柱状图绑定左侧数量Y轴
{
name: '销量',
type: 'bar',
yAxisIndex: 0,
barWidth: 24,
// 关键修复label 直接放在 series 下,而非 itemStyle 内
itemStyle: {
// 移除多余的 normal 层级,直接配置 color 渐变
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: [200, 280, 180, 300, 220, 350]
},
// 3. 达标柱状图绑定左侧数量Y轴
// {
// name: '产量',
// type: 'bar',
// yAxisIndex: 0,
// barWidth: 24,
// // 关键修复label 直接放在 series 下,而非 itemStyle 内
// itemStyle: {
// // 移除多余的 normal 层级,直接配置 color 渐变
// 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: [130, 220, 95, 255, 132, 332] // 达标数据(小于目标)
// },
// 4. 未达标柱状图绑定左侧数量Y轴
// {
// name: '未达标',
// type: 'bar',
// yAxisIndex: 0,
// barWidth: 18,
// itemStyle: {
// color: 'rgba(249, 164, 74, 1)',
// borderRadius: [4, 4, 0, 0],
// borderWidth: 0
// },
// data: [70, 60, 85, 45, 88, 18, 78] // 未达标数据(目标-达标)
// }
],
// 图例:区分各系列,点击可控制显示隐藏
// legend: {
// top: 0,
// left: 'center',
// itemWidth: 12,
// itemHeight: 8,
// textStyle: {
// color: 'rgba(0, 0, 0, 0.45)',
// fontSize: 12
// },
// data: ['完成率', '目标', '达标', '未达标']
// }
};
option && myChart.setOption(option);
// 窗口缩放适配
window.addEventListener('resize', () => {
myChart.resize();
});
// 组件销毁清理
this.$once('hook:destroyed', () => {
window.removeEventListener('resize', () => {
myChart.resize();
});
myChart.dispose();
});
}
},
};
</script>