Compare commits
5 Commits
projects/l
...
projects/l
| Author | SHA1 | Date | |
|---|---|---|---|
| 05fe91618c | |||
| b85ceb2542 | |||
| e770dc4fed | |||
| 4f7466bb29 | |||
| bb66f97b95 |
4
.env.dev
4
.env.dev
@@ -10,11 +10,11 @@ VUE_APP_TITLE = 洛玻集团驾驶舱
|
||||
# VUE_APP_BASE_API = 'http://172.16.33.83:7070'
|
||||
|
||||
# 杨姗姗
|
||||
# VUE_APP_BASE_API = 'http://172.16.20.218:7070'
|
||||
VUE_APP_BASE_API = 'http://172.16.20.218:7070'
|
||||
# 小田
|
||||
# VUE_APP_BASE_API = 'http://172.16.19.232:7070'
|
||||
# 测试
|
||||
VUE_APP_BASE_API = 'http://192.168.0.35:8080'
|
||||
# VUE_APP_BASE_API = 'http://192.168.0.35:8080'
|
||||
|
||||
|
||||
# 路由懒加载
|
||||
|
||||
24
src/assets/icons/svg/turn-data.svg
Normal file
24
src/assets/icons/svg/turn-data.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>切换数据</title>
|
||||
<g id="页面" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="icon和插图" transform="translate(-339.000000, -683.000000)" fill-rule="nonzero">
|
||||
<g id="检测数据" transform="translate(339.000000, 683.000000)">
|
||||
<rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="32" height="32"></rect>
|
||||
<g id="编组-5" transform="translate(1.358796, 1.358796)">
|
||||
<path d="M14.6412037,0 C6.56329821,0 0,6.56329821 0,14.6412037 C0,22.7191092 6.56329821,29.2824074 14.6412037,29.2824074 C22.7191092,29.2824074 29.2824074,22.7191092 29.2824074,14.6412037 C29.2824074,6.56329821 22.7191092,0 14.6412037,0 Z" id="形状" fill="#ECF5FE"></path>
|
||||
<g id="烧瓶,实验,化学,科学" transform="translate(4.362930, 5.001581)">
|
||||
<rect id="矩形" fill="#000000" opacity="0" x="1.95940783" y="1.320757" width="16.6377315" height="16.6377315"></rect>
|
||||
<g>
|
||||
<rect id="矩形" fill="#000000" opacity="0" x="2.83308197" y="2.32185598" width="14.8903832" height="14.8903832"></rect>
|
||||
<g id="还原画布" fill="#3E6AF7">
|
||||
<rect id="矩形" opacity="0" x="1.23578458" y="0.724558589" width="18.084978" height="18.084978"></rect>
|
||||
<path d="M17.6932207,6.74823569 L13.2296336,6.74823569 C13.0215873,6.74823569 12.8513571,6.54528408 12.8513571,6.29724746 L12.8513571,1.97232623 C12.8513571,1.72428962 13.0215542,1.521338 13.2296171,1.521338 L17.6932207,1.521338 C17.901267,1.521338 18.0714972,1.72428962 18.0714972,1.97232623 L18.0714972,6.29722771 C18.0714972,6.54526433 17.9031719,6.74823569 17.6932207,6.74823569 L17.6932207,6.74823569 Z M6.87068348,17.3028361 L2.40704671,17.3028361 C2.19900041,17.3028361 2.02877016,17.0998845 2.02877016,16.8518479 L2.02877016,12.5269266 C2.02877016,12.27889 2.19900041,12.0759384 2.40704671,12.0759384 L6.87068348,12.0759384 C7.07872978,12.0759384 7.24896003,12.2788703 7.24896003,12.5269266 L7.24896003,16.8518479 C7.24896003,17.1021358 7.07872978,17.3028361 6.87068348,17.3028361 Z M20.4661601,11.5941637 L19.1062728,9.97893922 C19.0135961,9.86923455 18.8717403,9.82573697 18.7412476,9.85222312 C18.680722,9.86356959 18.6239731,9.89194406 18.574794,9.93355332 L16.8858257,11.3558546 C16.7269585,11.4901405 16.7061539,11.7284496 16.8404398,11.8892217 C16.9160885,11.9781052 17.0220165,12.0235076 17.1298163,12.0235076 C17.2149231,12.0235076 17.3019183,11.9951332 17.3738069,11.9346076 L18.4537759,11.0267571 C18.4518711,11.0418803 18.4518711,11.0551316 18.4518711,11.0702547 C18.4518711,13.3493556 17.7350555,15.2804262 16.3789447,16.6535483 C15.0077109,18.0437151 13.0596123,18.7775586 10.7445837,18.7775586 C10.5365374,18.7775586 10.3663072,18.9477723 10.3663072,19.1558352 C10.3663072,19.3638981 10.5365208,19.5340952 10.7445837,19.5340952 C13.2657703,19.5340952 15.3992222,18.7227146 16.9160885,17.1850271 C17.6556136,16.4360439 18.2287016,15.5319866 18.6183247,14.4993084 C18.9890314,13.5157926 19.1857312,12.4301421 19.2065193,11.2707477 L19.8912003,12.0840332 C19.9668656,12.1729167 20.072777,12.2183192 20.1805768,12.2183192 C20.2657002,12.2183192 20.3526954,12.1899447 20.4245674,12.1294191 C20.5815463,11.9932449 20.602351,11.7549358 20.4661767,11.5941637 L20.4661601,11.5941637 Z M9.98803061,0 C8.67730576,0 7.47441384,0.202364785 6.41524944,0.603334282 C5.37500136,0.996733941 4.47470417,1.57928016 3.74086058,2.33204005 C2.34880553,3.76191116 1.58658748,5.7724236 1.52796679,8.16311747 L0.622004647,7.3990111 C0.461232559,7.2647086 0.222923469,7.28551323 0.0886375303,7.444397 C-0.0456484081,7.6032642 -0.0248437777,7.84347818 0.134023428,7.97776411 L1.82300826,9.40006538 C1.88255245,9.4508878 1.95677081,9.48136671 2.03484776,9.48706054 C2.04619423,9.48706054 2.05565239,9.48896542 2.06699887,9.48896542 C2.17481522,9.48896542 2.28261501,9.44357953 2.35637537,9.35466292 L3.71626275,7.73945502 C3.85054869,7.58057125 3.82974406,7.34037384 3.67087685,7.20608791 C3.51200965,7.07180197 3.27179567,7.0926066 3.13750974,7.2514738 L2.28263157,8.26714062 C2.32044763,6.0334422 3.00890532,4.16667386 4.28178097,2.86163053 C4.93808755,2.18640764 5.74570807,1.66627531 6.681933,1.3125966 C7.65597399,0.943778206 8.76809412,0.758424851 9.98803061,0.758424851 C10.1960769,0.758424851 10.3663072,0.588211171 10.3663072,0.380164867 C10.3663072,0.172118563 10.1979652,0 9.98803061,0 Z" id="形状" stroke="#3E6AF7" stroke-width="0.7"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
@@ -8,11 +8,8 @@ import { isRelogin } from '@/utils/request'
|
||||
|
||||
NProgress.configure({ showSpinner: false })
|
||||
|
||||
/**
|
||||
* 从菜单树中查找 jumpFlag===1 的默认跳转路径
|
||||
* 若打标在父节点则取其下第一个可见叶子路径;若在叶子则直接返回该路径
|
||||
*/
|
||||
function findDefaultPathFromMenus(menus) {
|
||||
// 从菜单树中查找第一个可见叶子菜单路径(按接口返回顺序)
|
||||
function findFirstLeafPathFromMenus(menus) {
|
||||
if (!Array.isArray(menus) || menus.length === 0) return null
|
||||
|
||||
function dfs(list, parentPath = '') {
|
||||
@@ -23,16 +20,11 @@ function findDefaultPathFromMenus(menus) {
|
||||
? rawPath
|
||||
: `${parentPath}/${rawPath}`.replace(/\/+/g, '/')
|
||||
|
||||
if (item.jumpFlag === 1) {
|
||||
if (item.children && item.children.length > 0) {
|
||||
const childPath = dfs(item.children, currentPath)
|
||||
return childPath != null ? childPath : currentPath
|
||||
}
|
||||
return currentPath
|
||||
}
|
||||
if (item.children && item.children.length > 0) {
|
||||
const found = dfs(item.children, currentPath)
|
||||
if (found != null) return found
|
||||
} else {
|
||||
return currentPath
|
||||
}
|
||||
}
|
||||
return null
|
||||
@@ -40,6 +32,24 @@ function findDefaultPathFromMenus(menus) {
|
||||
return dfs(menus)
|
||||
}
|
||||
|
||||
function findFirstLeafPathFromRoutes(routes) {
|
||||
if (!Array.isArray(routes) || routes.length === 0) return null
|
||||
const stack = [...routes]
|
||||
while (stack.length) {
|
||||
const route = stack.shift()
|
||||
if (!route || route.hidden === true) continue
|
||||
if (Array.isArray(route.children) && route.children.length > 0) {
|
||||
stack.unshift(...route.children)
|
||||
continue
|
||||
}
|
||||
const p = route.path || ''
|
||||
if (typeof p === 'string' && p.startsWith('/') && p !== '/404') {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 增加三方登陆 update by 芋艿
|
||||
const whiteList = ['/login', '/social-login', '/auth-redirect', '/bind', '/register', '/oauthLogin/gitee']
|
||||
|
||||
@@ -65,11 +75,16 @@ router.beforeEach((to, from, next) => {
|
||||
store.dispatch('GenerateRoutes', userInfo.menus).then(accessRoutes => {
|
||||
// 根据 roles 权限生成可访问的路由表
|
||||
router.addRoutes(accessRoutes) // 动态添加可访问路由表
|
||||
const defaultPath = findDefaultPathFromMenus(userInfo.menus) || '/'
|
||||
const menuDefaultPath = findFirstLeafPathFromMenus(userInfo.menus)
|
||||
const routeFallbackPath = findFirstLeafPathFromRoutes(accessRoutes)
|
||||
const defaultPath = menuDefaultPath || routeFallbackPath || '/'
|
||||
store.dispatch('SetDefaultPath', defaultPath)
|
||||
// 仅当目标为根路径 '/' 时跳默认页;否则保持当前路径(含刷新场景),避免刷新被误判为“未匹配”而跳到默认页
|
||||
if (to.path === '/') {
|
||||
next({ path: defaultPath, replace: true })
|
||||
const matched = router.match(defaultPath)
|
||||
const hasMatch = matched && Array.isArray(matched.matched) && matched.matched.length > 0
|
||||
const safePath = hasMatch ? defaultPath : (routeFallbackPath || '/')
|
||||
next({ path: safePath, replace: true })
|
||||
} else {
|
||||
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成,当前页刷新时保留 to 的路径
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
<div class="bullshit__info">
|
||||
对不起,您正在寻找的页面不存在。尝试检查URL的错误,然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容。
|
||||
</div>
|
||||
<router-link to="/" class="bullshit__return-home">
|
||||
返回首页
|
||||
<router-link to="/login" class="bullshit__return-home">
|
||||
返回登录页
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
246
src/views/home/accountsReceivable/accountsReceivable.vue
Normal file
246
src/views/home/accountsReceivable/accountsReceivable.vue
Normal file
@@ -0,0 +1,246 @@
|
||||
<template>
|
||||
<div id="dayReport" class="dayReport" :style="styles">
|
||||
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
|
||||
<sidebar v-if="!sidebar.hide" class="sidebar-container" />
|
||||
<ReportHeader :dateData="dateData" top-title="应收账款" :is-full-screen="isFullScreen"
|
||||
@screenfullChange="screenfullChange" @timeRangeChange="handleTimeChange" />
|
||||
<div class="main-body" style="
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding: 0px 16px 0 272px;
|
||||
flex-direction: column;
|
||||
">
|
||||
<div class="top" style="margin-top: -20px; display: flex; gap: 16px">
|
||||
<div class="top-three" style="
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns:1624px;
|
||||
">
|
||||
<operatingLineChart :dateData="dateData" :monthData="monthData" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="top" style="display: flex; gap: 16px;margin-top: 6px;">
|
||||
<div class="left-three" style="
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 1624px;
|
||||
">
|
||||
<operatingLineChartCumulative :dateData="dateData" :ytdData="ytdData" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import ReportHeader from "../components/noRouterHeader.vue";
|
||||
import { Sidebar } from "../../../layout/components";
|
||||
import screenfull from "screenfull";
|
||||
import { mapState } from "vuex";
|
||||
import operatingLineChart from "../accountsReceivableComponents/operatingLineChart";
|
||||
import operatingLineChartCumulative from "../accountsReceivableComponents/operatingLineChartCumulative.vue";
|
||||
|
||||
import { getSalesRevenueGroupData } from '@/api/cockpit'
|
||||
export default {
|
||||
name: "AccountsReceivable",
|
||||
components: {
|
||||
ReportHeader,
|
||||
operatingLineChartCumulative,
|
||||
operatingLineChart,
|
||||
Sidebar,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
weekArr: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
|
||||
isFullScreen: false,
|
||||
timer: null,
|
||||
beilv: 1,
|
||||
value: 100,
|
||||
sort:1,
|
||||
selectDate:{},
|
||||
monthData: {},
|
||||
ytdData:{},
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.init();
|
||||
this.windowWidth(document.documentElement.clientWidth);
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
theme: (state) => state.settings.theme,
|
||||
sideTheme: (state) => state.settings.sideTheme,
|
||||
sidebar: (state) => state.app.sidebar,
|
||||
device: (state) => state.app.device,
|
||||
needTagsView: (state) => state.settings.tagsView,
|
||||
fixedHeader: (state) => state.settings.fixedHeader,
|
||||
}),
|
||||
classObj() {
|
||||
return {
|
||||
hideSidebar: !this.sidebar.opened,
|
||||
openSidebar: this.sidebar.opened,
|
||||
withoutAnimation: this.sidebar.withoutAnimation,
|
||||
mobile: this.device === "mobile",
|
||||
};
|
||||
},
|
||||
variables() {
|
||||
return variables;
|
||||
},
|
||||
// ...mapGetters(['sidebar']),
|
||||
styles() {
|
||||
const v = Math.floor(this.value * this.beilv * 100) / 10000;
|
||||
return {
|
||||
transform: `scale(${v})`,
|
||||
transformOrigin: "left top",
|
||||
// overflow: hidden;
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
clientWidth(val) {
|
||||
if (!this.timer) {
|
||||
this.clientWidth = val;
|
||||
this.beilv2 = this.clientWidth / 1920;
|
||||
this.timer = true;
|
||||
let _this = this;
|
||||
setTimeout(function () {
|
||||
_this.timer = false;
|
||||
}, 500);
|
||||
}
|
||||
// 这里可以添加修改时的方法
|
||||
this.windowWidth(val);
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.timer);
|
||||
this.destroy();
|
||||
},
|
||||
mounted() {
|
||||
const _this = this;
|
||||
_this.beilv = document.documentElement.clientWidth / 1920;
|
||||
window.onresize = () => {
|
||||
return (() => {
|
||||
_this.clientWidth = `${document.documentElement.clientWidth}`;
|
||||
this.beilv = _this.clientWidth / 1920;
|
||||
})();
|
||||
};
|
||||
this.dateData = this.$route.query.dateData ? this.$route.query.dateData : undefined
|
||||
},
|
||||
methods: {
|
||||
// sortChange(value) {
|
||||
// this.sort = value
|
||||
// this.getData()
|
||||
// },
|
||||
getData() {
|
||||
getSalesRevenueGroupData({
|
||||
startTime: this.dateData.startTime,
|
||||
endTime: this.dateData.endTime,
|
||||
sort: this.sort,
|
||||
index: undefined,
|
||||
factory: undefined
|
||||
// timeDim: obj.mode
|
||||
}).then((res) => {
|
||||
console.log(res);
|
||||
this.monthData= res.data.month
|
||||
this.ytdData = res.data.ytd
|
||||
|
||||
// this.saleData = res.data.SaleData
|
||||
// this.premiumProduct = res.data.premiumProduct
|
||||
// this.salesTrendMap = res.data.salesTrendMap
|
||||
// this.grossMarginTrendMap = res.data.grossMarginTrendMap
|
||||
// this.salesProportion = res.data.salesProportion ? res.data.salesProportion : {}
|
||||
})
|
||||
},
|
||||
handleTimeChange(obj) {
|
||||
console.log(obj, 'obj');
|
||||
this.dateData= obj
|
||||
this.getData()
|
||||
},
|
||||
handleClickOutside() {
|
||||
this.$store.dispatch("app/closeSideBar", { withoutAnimation: false });
|
||||
},
|
||||
windowWidth(value) {
|
||||
this.clientWidth = value;
|
||||
this.beilv2 = this.clientWidth / 1920;
|
||||
},
|
||||
change() {
|
||||
this.isFullScreen = screenfull.isFullscreen;
|
||||
},
|
||||
init() {
|
||||
if (!screenfull.isEnabled) {
|
||||
this.$message({
|
||||
message: "you browser can not work",
|
||||
type: "warning",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
screenfull.on("change", this.change);
|
||||
},
|
||||
destroy() {
|
||||
if (!screenfull.isEnabled) {
|
||||
this.$message({
|
||||
message: "you browser can not work",
|
||||
type: "warning",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
screenfull.off("change", this.change);
|
||||
},
|
||||
// 全屏
|
||||
screenfullChange() {
|
||||
console.log("screenfull.enabled", screenfull.isEnabled);
|
||||
|
||||
if (!screenfull.isEnabled) {
|
||||
this.$message({
|
||||
message: "you browser can not work",
|
||||
type: "warning",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
screenfull.toggle(this.$refs.dayReportB);
|
||||
},
|
||||
// 导出
|
||||
// exportPDF() {
|
||||
// this.$message.success('正在导出,请稍等!')
|
||||
// const element = document.getElementById('dayRepDom')
|
||||
// element.style.display = 'block'
|
||||
// const fileName = '株洲碲化镉生产日报' + moment().format('yyMMDD') + '.pdf'
|
||||
// html2canvas(element, {
|
||||
// dpi: 300, // Set to 300 DPI
|
||||
// scale: 3 // Adjusts your resolution
|
||||
// }).then(function(canvas) {
|
||||
// const imgWidth = 595.28
|
||||
// const imgHeight = 841.89
|
||||
// const pageData = canvas.toDataURL('image/jpeg', 1.0)
|
||||
// const PDF = new JsPDF('', 'pt', [imgWidth, imgHeight])
|
||||
// PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight)
|
||||
// setTimeout(() => {
|
||||
// PDF.save(fileName) // 导出文件名
|
||||
// }, 1000)
|
||||
// })
|
||||
// element.style.display = 'none'
|
||||
// }
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "~@/assets/styles/mixin.scss";
|
||||
@import "~@/assets/styles/variables.scss";
|
||||
.dayReport {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: url("../../../assets/img/backp.png") no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
.hideSidebar .fixed-header {
|
||||
width: calc(100% - 54px);
|
||||
}
|
||||
|
||||
.sidebarHide .fixed-header {
|
||||
width: calc(100%);
|
||||
}
|
||||
|
||||
.mobile .fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
419
src/views/home/accountsReceivableComponents/Header.vue
Normal file
419
src/views/home/accountsReceivableComponents/Header.vue
Normal file
@@ -0,0 +1,419 @@
|
||||
<template>
|
||||
<header class="report-header">
|
||||
<!-- 左侧区域:logo + 标题 -->
|
||||
<div class="left-content">
|
||||
<img style="height: 36px;" src="../../../assets/img/cnbm.png" alt="benmaLogo" >
|
||||
<div class="top-title">{{ topTitle }}</div>
|
||||
</div>
|
||||
|
||||
<div class="center-content">
|
||||
<!-- 循环 pageRoutes,不再硬编码文字 -->
|
||||
<div class="item" v-for="(page, index) in pageRoutes" :key="index" @click="goToPage(page.path, index)">
|
||||
<span class="item-text">{{ page.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- :class="{ 'no-skew': activeIndex === index }
|
||||
" -->
|
||||
<!-- 右侧区域:全屏按钮 -->
|
||||
<div class="right-content">
|
||||
<el-button type="text" class="screen-btn" :title="isFullScreen ? '退出全屏' : '全屏'" @click="changeFullScreen">
|
||||
<svg-icon style="color: #0B58FF;" v-if="isFullScreen" icon-class="unFullScreenView" />
|
||||
<svg-icon style="color: #0B58FF;" v-else icon-class="fullScreenView" />
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 时间选择区域:日/月/年按钮 + label + 日期选择器 -->
|
||||
<div class="timeType">
|
||||
<!-- <div class="item" v-for="(item, index) in timeTypes" :key="index" @click="activeTime = index"
|
||||
:class="{ 'no-skew': activeTime === index }">
|
||||
<span class="item-text">{{ item.text }}</span>
|
||||
</div> -->
|
||||
<div class="dateP">
|
||||
<div class="label">
|
||||
<span class="label-text">月份选择</span>
|
||||
</div>
|
||||
<el-date-picker v-model="date" :type="getPickerType" :placeholder="getPickerPlaceholder"
|
||||
class="custom-date-picker" style="width: 132px;height: 29px;" @change="emitTimeRange" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment'
|
||||
export default {
|
||||
name: 'Header',
|
||||
props: {
|
||||
isFullScreen: { type: Boolean, default: false },
|
||||
topTitle: { type: String, default: '' }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentTime: '',
|
||||
timeTimer: null,
|
||||
date: undefined,
|
||||
activeIndex: -1,
|
||||
activeTime: 1, // 0=日,1=月,2=年(默认选中“日”)
|
||||
pageRoutes: [
|
||||
{ text: '营业收入', path: '/operatingRevenue/operatingRevenueIndex' },
|
||||
{ text: '利润分析', path: '/profitAnalysis' },
|
||||
{ text: '产销率库存分析', path: '/PSIAnal' },
|
||||
{ text: '成本分析', path: '/cost/cost' },
|
||||
{ text: '驾驶舱报表', path: '/cockpit' }
|
||||
],
|
||||
// 定义时间类型配置:text=按钮文字,pickerType=选择器类型,placeholder=占位符
|
||||
timeTypes: [
|
||||
{ text: '日', pickerType: 'date', placeholder: '选择日期' },
|
||||
{ text: '月', pickerType: 'month', placeholder: '选择月份' },
|
||||
{ text: '年', pickerType: 'year', placeholder: '选择年份' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 动态获取日期选择器类型
|
||||
getPickerType() {
|
||||
return this.timeTypes[this.activeTime].pickerType;
|
||||
},
|
||||
// 动态获取日期选择器占位符
|
||||
getPickerPlaceholder() {
|
||||
return this.timeTypes[this.activeTime].placeholder;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToPage(path, index) {
|
||||
// 1. 跳转到对应路由
|
||||
this.$router.push(path);
|
||||
// 2. 更新activeIndex,让当前点击项高亮
|
||||
this.activeIndex = index;
|
||||
},
|
||||
changeFullScreen() { this.$emit('screenfullChange') },
|
||||
padZero(num) { return num < 10 ? '0' + num : num },
|
||||
/**
|
||||
* 核心方法1:根据维度计算时间区间(首次进入时基于赋值的当月日期,计算“当月第一天0点→次月第一天0点”)
|
||||
* @returns {Object} 包含 start(开始时间)、end(结束时间)、dimension(维度)的区间对象
|
||||
*/
|
||||
calculateTimeRange() {
|
||||
let startTime = 0;
|
||||
let endTime = 0;
|
||||
const mode = this.activeTime + 1; // 1=日,2=月,3=年
|
||||
const defaultMoment = moment(); // 默认当前时间
|
||||
|
||||
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. 日维度:00:00:00 → 23:59:59(无毫秒)
|
||||
if (this.activeTime === 0) {
|
||||
startTime = targetMoment.startOf('day').millisecond(0).valueOf();
|
||||
endTime = targetMoment.endOf('day').millisecond(0).valueOf();
|
||||
}
|
||||
|
||||
// 2. 月维度:当月1日00:00:00 → 当月最后一天23:59:59(无毫秒)
|
||||
else if (this.activeTime === 1) {
|
||||
startTime = targetMoment.startOf('month').millisecond(0).valueOf();
|
||||
endTime = targetMoment.endOf('month').millisecond(0).valueOf();
|
||||
}
|
||||
|
||||
// 3. 年维度:当年1月1日00:00:00 → 当年最后一天23:59:59(无毫秒)
|
||||
else if (this.activeTime === 2) {
|
||||
startTime = targetMoment.startOf('year').millisecond(0).valueOf();
|
||||
endTime = targetMoment.endOf('year').millisecond(0).valueOf();
|
||||
}
|
||||
|
||||
// 调试输出:验证是否去掉毫秒
|
||||
console.log('时间范围计算结果:', {
|
||||
mode,
|
||||
startTime: moment(startTime * 1000).format('YYYY-MM-DD HH:mm:ss'), // 格式:2025-11-30 00:00:00
|
||||
endTime: moment(endTime * 1000).format('YYYY-MM-DD HH:mm:ss'), // 格式:2025-11-30 23:59:59(无毫秒)
|
||||
startTimeStamp: startTime, // 秒级时间戳(如:1764422400)
|
||||
endTimeStamp: endTime // 秒级时间戳(如:1764508799)
|
||||
});
|
||||
|
||||
return { startTime, endTime, mode };
|
||||
},
|
||||
/**
|
||||
* 核心方法2:传递时间区间给父组件(首次进入时触发,传递“当月第一天0点→次月第一天0点”)
|
||||
*/
|
||||
emitTimeRange() {
|
||||
const timeRange = this.calculateTimeRange();
|
||||
this.$emit('timeRangeChange', timeRange);
|
||||
// 调试用:查看首次传递的区间(如{start: "2025-10-01T00:00:00", end: "2025-11-01T00:00:00", dimension: "月"})
|
||||
console.log('当前时间区间:', timeRange);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 维度切换时:清空选择的日期,并传递当前维度的默认区间
|
||||
activeTime(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.date = undefined;
|
||||
// this.emitTimeRange();
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 核心逻辑:首次进入页面,计算当月默认日期并赋值给选择器,同时传递区间
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = this.padZero(now.getMonth() + 1); // 月份从0开始,+1后补零(如1月→01)
|
||||
// 赋值当月默认日期(格式:YYYY-MM,适配month类型选择器)
|
||||
this.date = `${year}-${month}`;
|
||||
// 确保选择器渲染完成后,传递“当月第一天0点→次月第一天0点”的区间
|
||||
this.$nextTick(() => this.emitTimeRange());
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 原有样式不变,仅补充label文字的倾斜抵消样式 */
|
||||
@font-face {
|
||||
font-family: "YouSheBiaoTiHei";
|
||||
src: url('../../../assets/fonts/YouSheBiaoTiHe.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.report-header {
|
||||
height: 117px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
background: url('../../../assets/img/topTitle.png') no-repeat;
|
||||
background-size: cover;
|
||||
background-position: 0 0;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
/* 确保timeType绝对定位生效 */
|
||||
|
||||
.left-content {
|
||||
margin-top: 11px;
|
||||
margin-left: 44px;
|
||||
height: 55px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.top-title {
|
||||
height: 55px;
|
||||
font-family: "YouSheBiaoTiHei", sans-serif;
|
||||
font-size: 42px;
|
||||
color: #1E1651;
|
||||
line-height: 55px;
|
||||
letter-spacing: 6px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.center-content {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 18px;
|
||||
margin-left: 70px;
|
||||
|
||||
.item {
|
||||
width: 180px;
|
||||
height: 50px;
|
||||
background: #E1EEFC;
|
||||
transform: skew(-20deg);
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
color: #1E1651;
|
||||
line-height: 50px;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
box-shadow: 0px 13px 16px 0px rgba(179, 217, 255, 0.43),
|
||||
0px 2px 4px 0px rgba(92, 140, 255, 0.25),
|
||||
inset 0px -43px 13px 0px rgba(255, 255, 255, 0.51);
|
||||
|
||||
.item-text {
|
||||
display: inline-block;
|
||||
transform: skew(20deg);
|
||||
}
|
||||
}
|
||||
|
||||
.item.no-skew {
|
||||
background: none !important;
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
color: #1E1651;
|
||||
|
||||
.item-text {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.timeType {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* 垂直居中,避免元素高低错位 */
|
||||
top: 42px;
|
||||
right:10px;
|
||||
margin-top: 18px;
|
||||
gap: 0;
|
||||
/* 清除间隙,让按钮与选择器紧密连接 */
|
||||
}
|
||||
|
||||
.timeType .item {
|
||||
width: 40px;
|
||||
height: 28px;
|
||||
background: rgba(236, 244, 254, 1);
|
||||
transform: skew(-20deg);
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: rgba(11, 88, 255, 1);
|
||||
line-height: 28px;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
/* 选中按钮与未选中按钮倾斜角度统一,避免切换时跳动 */
|
||||
}
|
||||
|
||||
.timeType .item .item-text {
|
||||
display: inline-block;
|
||||
transform: skew(20deg);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.timeType .item.no-skew {
|
||||
background: rgba(11, 88, 255, 1);
|
||||
color: rgba(249, 252, 255, 1);
|
||||
transform: skew(-20deg) !important;
|
||||
/* 统一倾斜角度,修复原30deg的错位 */
|
||||
box-shadow: 0 2px 8px rgba(11, 88, 255, 0.3);
|
||||
}
|
||||
|
||||
.timeType .item.no-skew .item-text {
|
||||
transform: skew(20deg) !important;
|
||||
/* 同步统一文字倾斜角度 */
|
||||
}
|
||||
|
||||
.dateP {
|
||||
position: relative;
|
||||
margin-left: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.dateP .label {
|
||||
width: 165px;
|
||||
height: 28px;
|
||||
background: rgba(236, 244, 254, 1);
|
||||
transform: skew(-25deg);
|
||||
/* 与按钮倾斜角度统一(原30deg改为25deg,避免视觉错位) */
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #0B58FF;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 补充:label文字抵消倾斜(原代码遗漏,导致文字倾斜) */
|
||||
.dateP .label-text {
|
||||
display: inline-block;
|
||||
transform: skew(25deg);
|
||||
/* 与label倾斜角度相反,确保文字正立 */
|
||||
}
|
||||
|
||||
.right-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 12px;
|
||||
margin-right: 4px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.current-time {
|
||||
color: #FFFFFF;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 500;
|
||||
font-size: 22px;
|
||||
line-height: 24px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.screen-btn {
|
||||
width: 26px;
|
||||
margin-left: 300px;
|
||||
color: #00fff0;
|
||||
font-size: 26px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 日期选择器样式保持不变 */
|
||||
::v-deep .custom-date-picker {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
width: 165px !important;
|
||||
height: 28px !important;
|
||||
position: relative;
|
||||
margin: 0 !important;
|
||||
|
||||
/* 1. 调整输入框文字:确保行高与输入框高度一致,垂直居中 */
|
||||
.el-input__inner {
|
||||
height: 28px !important;
|
||||
width: 165px !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>
|
||||
282
src/views/home/accountsReceivableComponents/container.vue
Normal file
282
src/views/home/accountsReceivableComponents/container.vue
Normal file
@@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<div class="cockpitContainer" :class="['cockpitContainer__' + size]">
|
||||
<div class="content-top" :class="['content-top__' + topSize]">
|
||||
<!-- 使用 flex 容器包裹图标和文字,实现垂直居中 -->
|
||||
<div class="title-wrapper">
|
||||
<svg-icon class="title-icon" :icon-class="icon" />
|
||||
<span class="title-text">
|
||||
{{ name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cockpitContainer-body">
|
||||
<slot>
|
||||
<div class="test-body">something test....</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Container',
|
||||
components: {},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['name', 'size', 'icon', 'topSize'],
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.cockpitContainer {
|
||||
display: inline-block;
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
.content-top {
|
||||
height: 60px;
|
||||
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
/* 垂直居中关键属性 */
|
||||
height: 100%;
|
||||
/* 继承父容器高度,确保垂直居中范围 */
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 30px;
|
||||
margin-right: 12px;
|
||||
margin-top: 4px;
|
||||
/* 图标和文字之间的间距 */
|
||||
flex-shrink: 0;
|
||||
/* 防止图标被压缩 */
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 24px;
|
||||
color: #000000;
|
||||
letter-spacing: 3px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
// 移除固定行高,避免影响垂直对齐
|
||||
// line-height: 60px;
|
||||
}
|
||||
|
||||
// width: 547px;
|
||||
// background: url(../../../assets/img/contentTopBasic.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
&__basic {
|
||||
// width: 547px;
|
||||
background: url(../../../assets/img/contentTopBasic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__middle {
|
||||
background: url(../../../assets/img/topTileMiddle.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__large {
|
||||
background: url(../../../assets/img/topTitleLargeBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__KFAPTopTitle {
|
||||
background: url(../../../assets/img/KFAPTopTitle.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__psiTopTitleBasic {
|
||||
background: url(../../../assets/img/psiTopTitleBasic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__rawTopTitleLarge {
|
||||
background: url(../../../assets/img/rawTopTitleLarge.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&__topBasic {
|
||||
background: url(../../../assets/img/top-basic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__bottomBasic {
|
||||
background: url(../../../assets/img/bottom-basic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__operatingBasic {
|
||||
background: url(../../../assets/img/operating-basic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__operatingLarge {
|
||||
background: url(../../../assets/img/operating-large.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__profitTopBasic {
|
||||
background: url(../../../assets/img/profitTopBasic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__profitMiddleBasic {
|
||||
background: url(../../../assets/img/profitMiddleBasic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__psiBasicBg {
|
||||
background: url(../../../assets/img/psiBasicBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__psiMiddleBg {
|
||||
background: url(../../../assets/img/psiMiddleBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__operatingRevenueBg {
|
||||
background: url(../../../assets/img/operatingRevenueBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__costBasicBg {
|
||||
background: url(../../../assets/img/costBasicBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__rawTopBg {
|
||||
background: url(../../../assets/img/rawTopBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__opLargeBg {
|
||||
background: url(../../../assets/img/opLargeBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
// &__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>
|
||||
298
src/views/home/accountsReceivableComponents/dataTrend.vue
Normal file
298
src/views/home/accountsReceivableComponents/dataTrend.vue
Normal file
@@ -0,0 +1,298 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container name="数据趋势" icon="cockpitItemIcon" size="opLargeBg" topSize="large">
|
||||
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%; gap: 16px">
|
||||
<div class="right" style="
|
||||
height: 191px;
|
||||
display: flex;
|
||||
width: 1595px;
|
||||
background-color: rgba(249, 252, 255, 1);
|
||||
">
|
||||
<dataTrendBar @changeItem="handleChange" :chartData="chartData" />
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Container from "../components/container.vue";
|
||||
import dataTrendBar from "./dataTrendBar.vue";
|
||||
|
||||
export default {
|
||||
name: "ProductionStatus",
|
||||
components: { Container, dataTrendBar },
|
||||
props: {
|
||||
trend: {
|
||||
type: Array,
|
||||
// 默认值与实际数据结构一致(12个月)
|
||||
default: () => [
|
||||
// { title: "2025年01月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年02月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年03月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年04月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年05月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年06月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年07月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年08月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年09月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年10月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年11月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年12月", budget: 0, real: 0, rate: 0, diff: 0 }
|
||||
]
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chartData: {
|
||||
months: [], // 月份数组(2025年01月 - 2025年12月)
|
||||
rates: [], // 每月完成率(百分比)
|
||||
reals: [], // 每月实际值
|
||||
budgets: [],// 每月预算值
|
||||
diffs: [], // 每月差值
|
||||
flags: [] // 每月达标标识(≥100 → 1,<100 → 0)
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
trend: {
|
||||
handler(newVal) {
|
||||
this.processTrendData(newVal);
|
||||
},
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.processTrendData(this.trend);
|
||||
},
|
||||
methods: {
|
||||
handleChange(value) {
|
||||
this.$emit("handleChange", value);
|
||||
},
|
||||
/**
|
||||
* 处理趋势数据(适配12个月的数组结构)
|
||||
* @param {Array} trendData - 原始趋势数组(12个月)
|
||||
*/
|
||||
processTrendData(trendData) {
|
||||
// 数据兜底:确保是数组且长度为12
|
||||
const validTrend = Array.isArray(trendData)
|
||||
? trendData
|
||||
: []
|
||||
|
||||
// 初始化空数组
|
||||
const months = [];
|
||||
const rates = [];
|
||||
const reals = [];
|
||||
const budgets = [];
|
||||
const diffs = [];
|
||||
const flags = [];
|
||||
|
||||
// 遍历12个月数据
|
||||
validTrend.forEach(item => {
|
||||
// 基础数据提取(兜底处理)
|
||||
const month = item.title ?? '';
|
||||
const budget = Number(item.budget) || 0;
|
||||
const real = Number(item.real) || 0;
|
||||
const rate = Number(item.rate) || 0;
|
||||
const diff = Number(item.diff) || 0;
|
||||
|
||||
// 计算达标标识(≥100 → 1,<100 → 0)
|
||||
const flag = this.getRateFlag(rate, real, budget);
|
||||
|
||||
// 填充数组
|
||||
months.push(month);
|
||||
rates.push(rate); // 转为百分比并取整
|
||||
reals.push(real);
|
||||
budgets.push(budget);
|
||||
diffs.push(diff);
|
||||
flags.push(flag);
|
||||
});
|
||||
|
||||
// 更新chartData(响应式)
|
||||
this.chartData = {
|
||||
months,
|
||||
rates,
|
||||
reals,
|
||||
budgets,
|
||||
diffs,
|
||||
flags
|
||||
};
|
||||
|
||||
console.log('处理后的趋势数据:', this.chartData);
|
||||
},
|
||||
|
||||
/**
|
||||
* 计算达标标识
|
||||
* @param {Number} rate - 完成率(原始值,如1.2 → 120%)
|
||||
* @returns {Number} 1: 达标(≥100%),0: 未达标(<100%)
|
||||
*/
|
||||
getRateFlag(rate, real, target) {
|
||||
if (isNaN(rate) || rate === null || rate === undefined) return 0;
|
||||
|
||||
// 1. 完成率 >= 100 => 达标
|
||||
if (rate >= 100) return 1;
|
||||
|
||||
// 2. 完成率 = 0 且 (目标值=0 或 实际值=目标值=0) => 达标
|
||||
if (rate === 0 && target === 0) return 1;
|
||||
|
||||
// 其他情况 => 未达标
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
};
|
||||
</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>
|
||||
/* 全局 tooltip 样式 */
|
||||
.production-status-chart-tooltip {
|
||||
background: #0a2b4f77 !important;
|
||||
border: none !important;
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.production-status-chart-tooltip * {
|
||||
color: #fff !important;
|
||||
}
|
||||
</style>
|
||||
477
src/views/home/accountsReceivableComponents/dataTrendBar.vue
Normal file
477
src/views/home/accountsReceivableComponents/dataTrendBar.vue
Normal file
@@ -0,0 +1,477 @@
|
||||
<template>
|
||||
<div class="coreBar">
|
||||
<!-- 新增行容器:包裹“各基地情况”和barTop -->
|
||||
<div class="header-row">
|
||||
<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">
|
||||
<span class="legend-icon square achieved"></span>
|
||||
实际·达标
|
||||
</span>
|
||||
<span class="legend-item">
|
||||
<span class="legend-icon square unachieved"></span>
|
||||
实际·未达标
|
||||
</span>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<div class="item-button category-btn">
|
||||
<span class="item-text">类目选择</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>
|
||||
<div class="lineBottom" style="height: 100%; width: 100%">
|
||||
<operatingLineBar :chartData="chartD" style="height: 99%; width: 100%" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import operatingLineBar from './operatingLineBarSale.vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: "Container",
|
||||
components: { operatingLineBar },
|
||||
props: ["chartData"],
|
||||
data() {
|
||||
return {
|
||||
activeButton: 0,
|
||||
isDropdownShow: false,
|
||||
selectedProfit: '营业收入', // 选中的名称,初始为null
|
||||
profitOptions: [
|
||||
'营业收入',
|
||||
'单价',
|
||||
'销量',
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// profitOptions() {
|
||||
// return this.categoryData.map(item => item.name) || [];
|
||||
// },
|
||||
currentDataSource() {
|
||||
console.log('yyyy', this.chartData);
|
||||
|
||||
return this.chartData
|
||||
},
|
||||
locations() {
|
||||
console.log('this.chartData', this.chartData);
|
||||
|
||||
return this.chartData.months
|
||||
},
|
||||
// 根据按钮切换生成对应的 chartData
|
||||
chartD() {
|
||||
const data = this.currentDataSource;
|
||||
console.log(this.currentDataSource, 'currentDataSource');
|
||||
|
||||
const salesData = {
|
||||
allPlaceNames: this.locations,
|
||||
series: [
|
||||
// 1. 完成率(折线图)
|
||||
{
|
||||
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(40, 138, 255, 0)' }
|
||||
])
|
||||
},
|
||||
data: data.rates, // 完成率(%)
|
||||
symbol: 'circle',
|
||||
symbolSize: 6
|
||||
},
|
||||
// 2. 目标(柱状图)
|
||||
{
|
||||
name: '预算',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0, // 左侧Y轴(万元)
|
||||
barWidth: 14,
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(130, 204, 255, 1)' },
|
||||
{ offset: 1, color: 'rgba(75, 157, 255, 1)' }
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: data.budgets // 目标销量(万元)
|
||||
},
|
||||
// 3. 实际(柱状图,含达标状态)
|
||||
{
|
||||
name: '实际',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 14,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
offset: [0, 0],
|
||||
// 固定label尺寸:68px×20px
|
||||
width: 68,
|
||||
height: 20,
|
||||
// 关键:去掉换行,让文字在一行显示,适配小尺寸
|
||||
formatter: (params) => {
|
||||
const diff = data.diffs || [];
|
||||
const flags = data.flags || [];
|
||||
const currentDiff = diff[params.dataIndex] || 0;
|
||||
const currentFlag = flags[params.dataIndex] || 0;
|
||||
|
||||
const prefix = currentFlag === 1 ? '+' : '-';
|
||||
|
||||
// 根据标志位选择不同的样式类
|
||||
if (currentFlag === 1) {
|
||||
// 达标 - 使用 rate-achieved 样式
|
||||
return `{achieved|${currentDiff}}{text|差值}`;
|
||||
} else {
|
||||
// 未达标 - 使用 rate-unachieved 样式
|
||||
return `{unachieved|${currentDiff}}{text|差值}`;
|
||||
}
|
||||
},
|
||||
backgroundColor: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(205, 215, 224, 0.6)' },
|
||||
{ offset: 0.2, color: '#ffffff' },
|
||||
{ offset: 1, color: '#ffffff' }
|
||||
]
|
||||
},
|
||||
shadowColor: 'rgba(191,203,215,0.5)',
|
||||
shadowBlur: 2,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 2,
|
||||
borderRadius: 4,
|
||||
borderColor: '#BFCBD577',
|
||||
borderWidth: 0,
|
||||
lineHeight: 20,
|
||||
rich: {
|
||||
text: {
|
||||
width: 'auto',
|
||||
padding: [5, 10, 5, 0],
|
||||
align: 'center',
|
||||
color: '#464646',
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
},
|
||||
achieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#76DABE', // 与达标的 offset: 1 颜色一致
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
},
|
||||
// 未达标样式
|
||||
unachieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#F9A44A', // 与未达标的 offset: 1 颜色一致
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
}
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
color: (params) => {
|
||||
// 达标状态:1=达标(绿色),0=未达标(橙色)
|
||||
const safeFlag = data.flags;
|
||||
const currentFlag = safeFlag[params.dataIndex] || 0;
|
||||
return currentFlag === 1
|
||||
? {
|
||||
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)' }
|
||||
]
|
||||
}
|
||||
: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(253, 209, 129, 1)' },
|
||||
{ offset: 1, color: 'rgba(249, 164, 74, 1)' }
|
||||
]
|
||||
};
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: data.reals // 实际销量(万元)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
// 根据按钮状态返回对应数据
|
||||
return salesData;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectProfit(item) {
|
||||
this.selectedProfit = item;
|
||||
this.isDropdownShow = false;
|
||||
this.$emit("changeItem", item);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.coreBar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
|
||||
// 新增:头部行容器,实现一行排列
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: flex-end; // 左右两端对齐
|
||||
align-items: center; // 垂直居中
|
||||
// width: 100%;
|
||||
margin-bottom: 8px; // 与下方图表区保留间距(可根据需求调整)
|
||||
}
|
||||
|
||||
// 各基地情况标题样式
|
||||
.base-title {
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
font-style: normal;
|
||||
padding: 0 0 0 16px; // 保留原有内边距
|
||||
white-space: nowrap; // 防止文字换行
|
||||
}
|
||||
|
||||
.barTop {
|
||||
// 移除原有flex和justify-content,由header-row控制
|
||||
width: auto; // 自适应宽度
|
||||
// 保留原有align-items,确保内部元素垂直居中
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
// 1. 右侧容器:包裹图例和按钮组,整体靠右
|
||||
.right-container {
|
||||
display: flex;
|
||||
align-items: center; // 图例和按钮组垂直居中
|
||||
gap: 24px; // 图例与按钮组的间距,避免贴紧
|
||||
margin-right: 46px; // 右侧整体留边,与原按钮组边距一致
|
||||
}
|
||||
|
||||
// 2. 图例:在右侧容器内横向排列
|
||||
.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);
|
||||
}
|
||||
|
||||
// 3. 按钮组:在右侧容器内,保留原有样式
|
||||
.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;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
.item-text {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.category-btn {
|
||||
width: 75px;
|
||||
border-top-left-radius: 12px;
|
||||
border-bottom-left-radius: 12px;
|
||||
background: #ffffff;
|
||||
color: #0b58ff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profit-btn {
|
||||
width: 123px;
|
||||
border-top-right-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
position: relative;
|
||||
padding: 0 18px 0 8px;
|
||||
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: 123px;
|
||||
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>
|
||||
226
src/views/home/accountsReceivableComponents/electricityGauge.vue
Normal file
226
src/views/home/accountsReceivableComponents/electricityGauge.vue
Normal file
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="chartBox" style="width: 100%; height: 108px; position: relative;">
|
||||
<div :id="id" style="width: 100%; height:100%;"></div>
|
||||
<div class="bottomTip">
|
||||
<div class="precent">
|
||||
<span class="precentNum">{{ detailData.rate || 0 }}% </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'EnergyConsumption',
|
||||
// components: { Container },
|
||||
// mixins: [resize],
|
||||
props: {
|
||||
detailData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
// electricComu: 0,
|
||||
// steamComu: 20, // 调整为符合max范围的数值(0-8)
|
||||
// // electricity: [120, 150, 130, 180, 160, 200, 190],
|
||||
// // steam: [80, 95, 85, 110, 100, 120, 115],
|
||||
// // dates: ['1日', '2日', '3日', '4日', '5日', '6日', '7日']
|
||||
})
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: () => ('monthG')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// electricityChart: null,
|
||||
// steamChart: null,
|
||||
// specialTicks: [2, 4, 6, 8], // 统一的刻度显示
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
detailData: {
|
||||
deep: true,
|
||||
immediate: true, // 初始化时立即执行
|
||||
handler() {
|
||||
this.updateGauges()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initGauges()
|
||||
// window.addEventListener('resize', this.handleResize)
|
||||
this.observeContainerResize()
|
||||
},
|
||||
methods: {
|
||||
observeContainerResize() {
|
||||
// 修复:获取正确的容器(组件内的.gauge-container)
|
||||
const container = this.$el.querySelector('.gauge-container')
|
||||
if (container && window.ResizeObserver) {
|
||||
this.resizeObserver = new ResizeObserver(entries => {
|
||||
if (this.electricityChart) {
|
||||
this.electricityChart.resize() // 直接触发resize,无需防抖
|
||||
}
|
||||
})
|
||||
this.resizeObserver.observe(container)
|
||||
}
|
||||
},
|
||||
initGauges() {
|
||||
// console.log('this.id',this.id);
|
||||
|
||||
// 初始化电气图表实例
|
||||
const electricityDom = document.getElementById(this.id)
|
||||
if (electricityDom) {
|
||||
// 修复:正确创建并存储图表实例
|
||||
this.electricityChart = echarts.init(electricityDom)
|
||||
// 首次更新数据
|
||||
this.updateGauges()
|
||||
}
|
||||
// 蒸汽图表若未使用,可注释/删除
|
||||
// const steamDom = document.getElementById('steamGauge')
|
||||
// if (steamDom) {
|
||||
// this.steamChart = echarts.init(steamDom)
|
||||
// }
|
||||
},
|
||||
updateGauges() {
|
||||
// 修复:先判断实例是否存在,再更新配置
|
||||
if (!this.electricityChart) return
|
||||
|
||||
// 修复:兜底获取rate值,确保数值有效
|
||||
const rate = Number(this.detailData?.rate) || 0
|
||||
console.log('当前rate值:', rate); // 调试:确认rate值正确
|
||||
|
||||
// 关键:第二个参数传true,清空原有配置,强制更新
|
||||
this.electricityChart.setOption(this.getElectricityGaugeOption(rate), true)
|
||||
},
|
||||
// 用电量仪表盘配置(保留原有样式,优化数值范围)
|
||||
getElectricityGaugeOption(value) {
|
||||
const electricityGradient = new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: '#0B58FF' },
|
||||
{ offset: 1, color: '#32FFCD' }
|
||||
])
|
||||
|
||||
return {
|
||||
series: [
|
||||
{
|
||||
name: '月度',
|
||||
type: 'gauge',
|
||||
radius: '95', // 修复:添加%,避免数值错误
|
||||
center: ['50%', '90%'],
|
||||
startAngle: 180,
|
||||
endAngle: 0,
|
||||
min: 0,
|
||||
max: 100,
|
||||
splitNumber: 4,
|
||||
label: { show: false },
|
||||
progress: {
|
||||
show: true,
|
||||
overlap: false,
|
||||
roundCap: true,
|
||||
clip: false,
|
||||
width: 14,
|
||||
itemStyle: { color: electricityGradient }
|
||||
},
|
||||
pointer: {
|
||||
icon: 'path://M2090.36389,615.30999 L2090.36389,615.30999 C2091.48372,615.30999 2092.40383,616.194028 2092.44859,617.312956 L2096.90698,728.755929 C2097.05155,732.369577 2094.2393,735.416212 2090.62566,735.56078 C2090.53845,735.564269 2090.45117,735.566014 2090.36389,735.566014 L2090.36389,735.566014 C2086.74736,735.566014 2083.81557,732.63423 2083.81557,729.017692 C2083.81557,728.930412 2083.81732,728.84314 2083.82081,728.755929 L2088.2792,617.312956 C2088.32396,616.194028 2089.24407,615.30999 2090.36389,615.30999 Z',
|
||||
length: '75%',
|
||||
width: 16,
|
||||
itemStyle: { color: '#288AFF' },
|
||||
offsetCenter: [0, '10%']
|
||||
},
|
||||
axisLine: {
|
||||
roundCap: true,
|
||||
lineStyle: { width: 12, color: [[1, '#E6EBF7']] }
|
||||
},
|
||||
splitLine: {
|
||||
length: 10,
|
||||
lineStyle: { width: 5, color: '#D6DAE5' },
|
||||
},
|
||||
axisTick: {
|
||||
splitNumber: 2,
|
||||
length: 6,
|
||||
lineStyle: { width: 2, color: '#D6DAE5' }
|
||||
},
|
||||
axisLabel: {
|
||||
show: false,
|
||||
},
|
||||
detail: { show: false },
|
||||
data: [{ value: value, unit: '' }] // 确保数值正确传入
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
// 未使用的蒸汽仪表盘可注释/删除
|
||||
// getSteamGaugeOption(value) { ... }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.chartBox {
|
||||
.bottomTip {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
|
||||
.precent {
|
||||
line-height: 3px;
|
||||
|
||||
&::after,
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 54px;
|
||||
height: 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&::after {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
margin-right: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
// 用电量线条颜色
|
||||
.precent::before {
|
||||
|
||||
background: linear-gradient(90deg, rgba(40, 138, 255, 0) 0%, rgba(12, 125, 254, 0.4) 100%);
|
||||
}
|
||||
|
||||
/* ::after:从 透明 到 rgba(12, 125, 254, 0.4)(90度渐变,左到右) */
|
||||
.precent::after {
|
||||
background: linear-gradient(90deg, rgba(12, 125, 254, 0.4) 0%, rgba(40, 138, 255, 0) 100%);
|
||||
}
|
||||
|
||||
// 蒸汽线条颜色
|
||||
// .steam-precent::after,
|
||||
// .steam-precent::before {
|
||||
// background: linear-gradient(90deg, rgba(11, 168, 255, 0.26) 0%, rgba(54, 239, 230, 1) 100%);
|
||||
// }
|
||||
|
||||
.precentNum {
|
||||
display: inline-block;
|
||||
// width: 52px;
|
||||
height: 22px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: #0B58FF;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
// 蒸汽数字颜色
|
||||
.steam-num {
|
||||
color: rgba(54, 239, 230, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
204
src/views/home/accountsReceivableComponents/monthlyOverview.vue
Normal file
204
src/views/home/accountsReceivableComponents/monthlyOverview.vue
Normal file
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container :name="title" icon="cockpitItemIcon" size="operatingRevenueBg" 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; gap: 8px;">
|
||||
<div class="dashboard">
|
||||
<div class="title">
|
||||
{{ month }}月完成率
|
||||
</div>
|
||||
<div class="number">
|
||||
<div class="yield">
|
||||
{{ monthData?.rate || 0 }}%
|
||||
</div>
|
||||
<div class="mom">
|
||||
环比{{ monthData?.momRate }}%
|
||||
<img v-if="monthData?.momRate >= 0" class="arrow" src="../../../assets/img/topArrow.png" alt="">
|
||||
<img v-else class="arrow" src="../../../assets/img/downArrow.png" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="electricityGauge">
|
||||
<electricityGauge :detailData="monthData" id="month"></electricityGauge>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="line" style="padding: 0px;">
|
||||
<verticalBarChart :detailData="monthData">
|
||||
</verticalBarChart>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Container from './container.vue'
|
||||
import electricityGauge from './electricityGauge.vue'
|
||||
import verticalBarChart from './verticalBarChart.vue'
|
||||
|
||||
// import * as echarts from 'echarts'
|
||||
// import rawItem from './raw-Item.vue'
|
||||
|
||||
export default {
|
||||
name: 'ProductionStatus',
|
||||
components: { Container, electricityGauge, verticalBarChart },
|
||||
// mixins: [resize],
|
||||
props: {
|
||||
monthData: { // 接收父组件传递的设备数据数组
|
||||
type: Object,
|
||||
default: () => {} // 默认空数组,避免报错
|
||||
},
|
||||
title: { // 接收父组件传递的设备数据数组
|
||||
type: String,
|
||||
default: () => '' // 默认空数组,避免报错
|
||||
},
|
||||
month: { // 接收父组件传递的设备数据数组
|
||||
type: String,
|
||||
default: () => '' // 默认空数组,避免报错
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// itemData: {
|
||||
// handler(newValue, oldValue) {
|
||||
// // this.updateChart()
|
||||
// },
|
||||
// deep: true // 若对象内属性变化需触发,需加 deep: true
|
||||
// }
|
||||
},
|
||||
// computed: {
|
||||
// // 处理排序:包含“总成本”的项放前面,其余项按原顺序排列
|
||||
// sortedItemData() {
|
||||
// // 过滤出包含“总成本”的项(不区分大小写)
|
||||
// const totalCostItems = this.itemData.filter(item =>
|
||||
// item.name && item.name.includes('总成本')
|
||||
// );
|
||||
// // 过滤出不包含“总成本”的项
|
||||
// const otherItems = this.itemData.filter(item =>
|
||||
// !item.name || !item.name.includes('总成本')
|
||||
// );
|
||||
// // 合并:总成本项在前,其他项在后
|
||||
// return [...totalCostItems, ...otherItems];
|
||||
// }
|
||||
// },
|
||||
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;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
width: 264px;
|
||||
height: 205px;
|
||||
background: #F9FCFF;
|
||||
padding: 16px 0 0 10px;
|
||||
|
||||
.title {
|
||||
// width: 190px;
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-family: YouSheBiaoTiHei;
|
||||
font-size: 46px;
|
||||
color: #0B58FF;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
white-space: nowrap;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.mom {
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 500px;
|
||||
height: 205px;
|
||||
background: #F9FCFF;
|
||||
}
|
||||
|
||||
// .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> -->
|
||||
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container :name="title" icon="cockpitItemIcon" size="operatingRevenueBg" topSize="middle">
|
||||
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%;">
|
||||
<div class="topItem-container" style="display: flex; gap: 8px; width: 100%;">
|
||||
<!-- 销量模块(直接传递整合了flag的salesData) -->
|
||||
<div class="dashboard left" @click="handleDashboardClick('/salesVolumeAnalysis/salesVolumeAnalysisBase')">
|
||||
<div style='position: relative;'>
|
||||
<div class="title">
|
||||
销量·万㎡
|
||||
</div>
|
||||
<div style='font-size: 16px;position: absolute;top:-4px;right:15px'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{monthAnalysis[0].rate}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:monthAnalysis[0].flags>0?'#30B590':'#FF9423'}" >{{monthAnalysis[0].diff}}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<operatingSingleBar :detailData="salesData"></operatingSingleBar>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 单价模块(直接传递整合了flag的unitPriceData) -->
|
||||
<div class="dashboard right" @click="handleDashboardClick('/unitPriceAnalysis/unitPriceAnalysisBase')">
|
||||
<div style='position: relative;'>
|
||||
<div class="title">
|
||||
单价·元/㎡
|
||||
</div>
|
||||
<div style='font-size: 16px;position: absolute;top:-4px;right:15px'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{monthAnalysis[1].rate}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:monthAnalysis[1].flags>0?'#30B590':'#FF9423'}" >{{monthAnalysis[1].diff}}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<operatingSingleBar :detailData="unitPriceData"></operatingSingleBar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Container from './container.vue'
|
||||
import operatingSingleBar from './operatingSingleBar.vue'
|
||||
|
||||
export default {
|
||||
name: 'ProductionStatus',
|
||||
components: { Container, operatingSingleBar },
|
||||
props: {
|
||||
monthAnalysis: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{ title: "销量", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
{ title: "单价", budget: 0, real: 0, rate: 0, diff: 0 }
|
||||
]
|
||||
},
|
||||
dateData: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
factory: {
|
||||
type: [String,Number],
|
||||
default: ''
|
||||
},
|
||||
month: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
// 初始化数据包含flag字段
|
||||
salesData: { title: "销量", budget: 0, real: 0, rate: 0, diff: 0, flag: 0 },
|
||||
unitPriceData: { title: "单价", budget: 0, real: 0, rate: 0, diff: 0, flag: 0 }
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
monthAnalysis: {
|
||||
handler(newVal) {
|
||||
this.updateChart(newVal)
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateChart(this.monthAnalysis)
|
||||
},
|
||||
methods: {
|
||||
handleDashboardClick(path) {
|
||||
this.$router.push({
|
||||
path: path,
|
||||
query: {
|
||||
factory: this.$route.query.factory ? this.$route.query.factory : this.factory,
|
||||
dateData: this.dateData
|
||||
}
|
||||
})
|
||||
},
|
||||
// 判断flag的核心方法
|
||||
getRateFlag(rate, real, target) {
|
||||
if (isNaN(rate) || rate === null || rate === undefined) return 0;
|
||||
|
||||
// 1. 完成率 >= 100 => 达标
|
||||
if (rate >= 100) return 1;
|
||||
|
||||
// 2. 完成率 = 0 且 (目标值=0 或 实际值=目标值=0) => 达标
|
||||
if (rate === 0 && target === 0) return 1;
|
||||
|
||||
// 其他情况 => 未达标
|
||||
return 0;
|
||||
},
|
||||
|
||||
updateChart(data) {
|
||||
// 数据兜底
|
||||
const salesItem = Array.isArray(data) && data[0] ? data[0] : { title: "销量", budget: 0, real: 0, rate: 0, diff: 0 };
|
||||
const unitPriceItem = Array.isArray(data) && data[1] ? data[1] : { title: "单价", budget: 0, real: 0, rate: 0, diff: 0 };
|
||||
|
||||
// 核心修改:将flag整合到数据对象中,无需单独定义salesFlag/unitPriceFlag
|
||||
this.salesData = {
|
||||
...salesItem, // 合并原有字段
|
||||
flag: this.getRateFlag(salesItem.rate, salesItem.real, salesItem.budget) // 新增flag字段
|
||||
};
|
||||
|
||||
this.unitPriceData = {
|
||||
...unitPriceItem, // 合并原有字段
|
||||
flag: this.getRateFlag(unitPriceItem.rate, unitPriceItem.real, unitPriceItem.budget) // 新增flag字段
|
||||
};
|
||||
|
||||
// 调试:确认整合后的数据
|
||||
console.log('整合flag后的销量数据:', this.salesData);
|
||||
console.log('整合flag后的单价数据:', this.unitPriceData);
|
||||
}
|
||||
}
|
||||
}
|
||||
</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;
|
||||
}
|
||||
|
||||
.topItem-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
height: 205px;
|
||||
background: #F9FCFF;
|
||||
padding: 16px 0 0 10px;
|
||||
margin: 0 4px;
|
||||
|
||||
.title {
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 2px;
|
||||
text-align: left;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chart-wrap {
|
||||
width: 100%;
|
||||
height: calc(100% - 30px);
|
||||
}
|
||||
|
||||
.number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
height: 32px;
|
||||
font-family: YouSheBiaoTiHei;
|
||||
font-size: 32px;
|
||||
color: #0B58FF;
|
||||
line-height: 32px;
|
||||
letter-spacing: 2px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mom {
|
||||
width: 97px;
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: left;
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard.left {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.dashboard.right {
|
||||
margin-right: 0;
|
||||
}
|
||||
</style>
|
||||
503
src/views/home/accountsReceivableComponents/operatingBar.vue
Normal file
503
src/views/home/accountsReceivableComponents/operatingBar.vue
Normal file
@@ -0,0 +1,503 @@
|
||||
<template>
|
||||
<div class="coreBar">
|
||||
<div class="header-row">
|
||||
<div class="base-title">各基地情况</div>
|
||||
<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">
|
||||
<span class="legend-icon square achieved"></span>实际·达标
|
||||
</span>
|
||||
<span class="legend-item">
|
||||
<span class="legend-icon square unachieved"></span>实际·未达标
|
||||
</span>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<div class="item-button category-btn">
|
||||
<span class="item-text">展示顺序</span>
|
||||
</div>
|
||||
<div class="dropdown-container">
|
||||
<div class="item-button profit-btn active" @click.stop="isDropdownShow = !isDropdownShow">
|
||||
<span class="item-text profit-text">{{ selectedSort || '请选择' }}</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.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lineBottom" style="height: 100%; width: 100%">
|
||||
<operatingLineBar :dateData="dateData" :chartData="chartD" style="height: 100%; width: 100%" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import operatingLineBar from './operatingLineBarSale.vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: "Container",
|
||||
components: { operatingLineBar },
|
||||
props: ["chartData", 'dateData'],
|
||||
emits: ['sort-change'], // 声明事件(Vue3 推荐)
|
||||
data() {
|
||||
return {
|
||||
activeButton: 0,
|
||||
isDropdownShow: false,
|
||||
selectedSort: '实际值:高~低', // 选中的label
|
||||
selectedSortValue: 1, // 选中的value,用于排序逻辑
|
||||
profitOptions: [
|
||||
{ label: '实际值:高~低', value: 1 },
|
||||
{ label: '实际值:低~高', value: 2 },
|
||||
{ label: '完成率:高~低', value: 3 },
|
||||
{ label: '完成率:低~高', value: 4 },
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 排序后的数据源(核心:根据selectedSortValue重新排序)
|
||||
currentDataSource() {
|
||||
if (!this.chartData?.factory) return {};
|
||||
|
||||
// 深拷贝原始数据,避免修改原数据
|
||||
const factory = JSON.parse(JSON.stringify(this.chartData.factory));
|
||||
if (!factory.locations.length || !this.selectedSortValue) return factory;
|
||||
|
||||
// 构建带索引的数组,方便同步所有字段排序
|
||||
const dataWithIndex = factory.locations.map((name, index) => ({
|
||||
index,
|
||||
name,
|
||||
real: factory.reals[index],
|
||||
target: factory.targets[index],
|
||||
rate: factory.rates[index],
|
||||
diff: factory.diff[index],
|
||||
flag: factory.flags[index]
|
||||
}));
|
||||
|
||||
// 根据选中的排序规则排序
|
||||
switch (this.selectedSortValue) {
|
||||
case 1: // 实际值:高~低
|
||||
dataWithIndex.sort((a, b) => b.real - a.real);
|
||||
break;
|
||||
case 2: // 实际值:低~高
|
||||
dataWithIndex.sort((a, b) => a.real - b.real);
|
||||
break;
|
||||
case 3: // 预算值:高~低
|
||||
dataWithIndex.sort((a, b) => b.rate - a.rate);
|
||||
break;
|
||||
case 4: // 预算值:低~高
|
||||
dataWithIndex.sort((a, b) => a.rate - b.rate);
|
||||
break;
|
||||
default:
|
||||
return factory;
|
||||
}
|
||||
|
||||
// 同步更新所有数组
|
||||
factory.locations = dataWithIndex.map(item => item.name);
|
||||
factory.reals = dataWithIndex.map(item => item.real);
|
||||
factory.targets = dataWithIndex.map(item => item.target);
|
||||
factory.rates = dataWithIndex.map(item => item.rate);
|
||||
factory.diff = dataWithIndex.map(item => item.diff);
|
||||
factory.flags = dataWithIndex.map(item => item.flag);
|
||||
|
||||
return factory;
|
||||
},
|
||||
locations() {
|
||||
return this.currentDataSource.locations || [];
|
||||
},
|
||||
// 最终传递给图表的排序后数据
|
||||
chartD() {
|
||||
const data = this.currentDataSource;
|
||||
const salesData = {
|
||||
allPlaceNames: this.locations,
|
||||
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(40, 138, 255, 0)' }
|
||||
])
|
||||
},
|
||||
data: data.rates || [],
|
||||
symbol: 'circle',
|
||||
symbolSize: 6
|
||||
},
|
||||
// 目标(柱状图)
|
||||
{
|
||||
name: '预算',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 40,
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(130, 204, 255, 1)' },
|
||||
{ offset: 1, color: 'rgba(75, 157, 255, 1)' }
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: data.targets || []
|
||||
},
|
||||
// 实际(柱状图)
|
||||
{
|
||||
name: '实际',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 40,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
offset: [32, 0],
|
||||
width: 100,
|
||||
height: 22,
|
||||
formatter: (params) => {
|
||||
const diff = data.diff || [];
|
||||
const flags = data.flags || [];
|
||||
const currentDiff = diff[params.dataIndex] || 0;
|
||||
const currentFlag = flags[params.dataIndex] || 0;
|
||||
|
||||
const prefix = currentFlag === 1 ? '+' : '-';
|
||||
|
||||
// 根据标志位选择不同的样式类
|
||||
if (currentFlag === 1) {
|
||||
// 达标 - 使用 rate-achieved 样式
|
||||
return `{achieved|${currentDiff}}{text|差值}`;
|
||||
} else {
|
||||
// 未达标 - 使用 rate-unachieved 样式
|
||||
return `{unachieved|${currentDiff}}{text|差值}`;
|
||||
}
|
||||
},
|
||||
backgroundColor: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(205, 215, 224, 0.6)' },
|
||||
{ offset: 0.2, color: '#ffffff' },
|
||||
{ offset: 1, color: '#ffffff' }
|
||||
]
|
||||
},
|
||||
shadowColor: 'rgba(191,203,215,0.5)',
|
||||
shadowBlur: 2,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 2,
|
||||
borderRadius: 4,
|
||||
borderColor: '#BFCBD577',
|
||||
borderWidth: 0,
|
||||
lineHeight: 26,
|
||||
rich: {
|
||||
text: {
|
||||
width: 'auto',
|
||||
padding: [5, 10, 5, 0],
|
||||
align: 'center',
|
||||
color: '#464646',
|
||||
fontSize: 14,
|
||||
},
|
||||
achieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#76DABE', // 与达标的 offset: 1 颜色一致
|
||||
fontSize: 14,
|
||||
},
|
||||
// 未达标样式
|
||||
unachieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#F9A44A', // 与未达标的 offset: 1 颜色一致
|
||||
fontSize: 14,
|
||||
}
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
color: (params) => {
|
||||
const safeFlag = data.flags || [];
|
||||
const currentFlag = safeFlag[params.dataIndex] || 0;
|
||||
return currentFlag === 1
|
||||
? {
|
||||
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)' }
|
||||
]
|
||||
}
|
||||
: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(253, 209, 129, 1)' },
|
||||
{ offset: 1, color: 'rgba(249, 164, 74, 1)' }
|
||||
]
|
||||
};
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: data.reals || []
|
||||
}
|
||||
]
|
||||
};
|
||||
return salesData;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectProfit(item) {
|
||||
// 更新选中的label和value
|
||||
this.selectedSort = item.label;
|
||||
this.selectedSortValue = item.value;
|
||||
this.isDropdownShow = false;
|
||||
// 向父组件传递排序事件(可选,保持原有逻辑)
|
||||
this.$emit('sort-change', item.value);
|
||||
}
|
||||
},
|
||||
// 监听父组件传入的chartData变化,重置选中状态(可选)
|
||||
watch: {
|
||||
'chartData.factory': {
|
||||
handler() {
|
||||
// 若需要切换数据源后重置排序,可取消注释
|
||||
// this.selectedSort = null;
|
||||
// this.selectedSortValue = null;
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.coreBar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.base-title {
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
font-style: normal;
|
||||
padding: 0 0 0 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.barTop {
|
||||
width: auto;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.right-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
margin-right: 46px;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.legend-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.legend-icon.line {
|
||||
width: 12px;
|
||||
height: 2px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: -2px;
|
||||
left: 3px;
|
||||
width: 6px;
|
||||
border-radius: 50%;
|
||||
height: 6px;
|
||||
background-color: rgba(40, 138, 255, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.legend-icon.square {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.yield {
|
||||
background: rgba(40, 138, 255, 1);
|
||||
}
|
||||
|
||||
.target {
|
||||
background: #2889FF;
|
||||
}
|
||||
|
||||
.achieved {
|
||||
background: rgba(40, 203, 151, 1);
|
||||
}
|
||||
|
||||
.unachieved {
|
||||
background: rgba(255, 132, 0, 1);
|
||||
}
|
||||
|
||||
.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;
|
||||
fontSize: 12px;
|
||||
line-height: 24px;
|
||||
font-style: normal;
|
||||
letter-spacing: 2px;
|
||||
overflow: hidden;
|
||||
|
||||
.item-text {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.category-btn {
|
||||
width: 75px;
|
||||
border-top-left-radius: 12px;
|
||||
border-bottom-left-radius: 12px;
|
||||
background: #ffffff;
|
||||
color: #0b58ff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profit-btn {
|
||||
width: 123px;
|
||||
border-top-right-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
position: relative;
|
||||
padding: 0 18px 0 8px;
|
||||
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: 123px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
|
||||
.dropdown-option {
|
||||
padding: 6px 12px;
|
||||
fontSize: 12px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
letter-spacing: 1px;
|
||||
|
||||
&:hover {
|
||||
background: #f5f7fa;
|
||||
color: #3071ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div ref="cockpitEffChipBottom" id="cockpitEffChipBottom" style="width: 100%; height: 400px;"></div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
myChart: null // 存储图表实例,避免重复创建
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 明确接收的props结构,增强可读性
|
||||
chartData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
series: [],
|
||||
allPlaceNames: []
|
||||
}),
|
||||
// 校验数据格式
|
||||
validator: (value) => {
|
||||
return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.updateChart();
|
||||
});
|
||||
},
|
||||
|
||||
// 新增:监听 chartData 变化
|
||||
watch: {
|
||||
// 深度监听数据变化,仅更新图表配置(不销毁实例)
|
||||
chartData: {
|
||||
handler() {
|
||||
console.log(this.chartData,'chartData');
|
||||
|
||||
this.updateChart();
|
||||
},
|
||||
deep: true,
|
||||
immediate: true // 初始化时立即执行
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateChart() {
|
||||
const chartDom = this.$refs.cockpitEffChipBottom;
|
||||
if (!chartDom) {
|
||||
console.error('图表容器未找到!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose();
|
||||
}
|
||||
|
||||
this.myChart = echarts.init(chartDom);
|
||||
const { allPlaceNames, series } = this.chartData || {};
|
||||
|
||||
// 处理空数据
|
||||
const xData = allPlaceNames || [];
|
||||
const chartSeries = series || []; // 父组件传递的 series
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
},
|
||||
formatter: (params) => {
|
||||
let html = `${params[0].axisValue}<br/>`;
|
||||
params.forEach(item => {
|
||||
const unit = item.seriesName === '完成率' ? '%' : (
|
||||
['产量', '销量'].includes(this.$parent.selectedProfit) ? '片' : '万元'
|
||||
);
|
||||
html += `${item.marker} ${item.seriesName}: ${item.value}${unit}<br/>`;
|
||||
});
|
||||
return html;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 30,
|
||||
right: 20,
|
||||
left: 60,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
axisTick: { show: false },
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
interval: 0,
|
||||
padding: [5, 0, 0, 0]
|
||||
},
|
||||
data: xData
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
// 左侧Y轴:营业收入、成本(单位万元)
|
||||
{
|
||||
type: 'value',
|
||||
splitNumber: 4,
|
||||
name: '万元',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'right'
|
||||
},
|
||||
// min: 0,
|
||||
// max: (value) => Math.ceil((value.max || 0) * 1.1),
|
||||
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
formatter: '{value}'
|
||||
},
|
||||
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
splitNumber: 4
|
||||
},
|
||||
// 右侧Y轴:利润占比(百分比)
|
||||
{
|
||||
type: 'value',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'left'
|
||||
},
|
||||
// min: 0,
|
||||
// max: 100,
|
||||
scale:true,
|
||||
splitNumber: 4,
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
formatter: '{value}%'
|
||||
},
|
||||
splitLine: { show: false },
|
||||
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
splitNumber: 4
|
||||
}
|
||||
],
|
||||
series: chartSeries // 直接使用父组件传递的 series
|
||||
};
|
||||
|
||||
option && this.myChart.setOption(option);
|
||||
|
||||
// 窗口缩放适配和销毁逻辑保持不变
|
||||
window.addEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
|
||||
this.$once('hook:destroyed', () => {
|
||||
window.removeEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
this.myChart && this.myChart.dispose();
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
172
src/views/home/accountsReceivableComponents/operatingLineBar.vue
Normal file
172
src/views/home/accountsReceivableComponents/operatingLineBar.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 400px;"></div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
myChart: null // 存储图表实例,避免重复创建
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 明确接收的props结构,增强可读性
|
||||
chartData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
series: [],
|
||||
allPlaceNames: []
|
||||
}),
|
||||
// 校验数据格式
|
||||
validator: (value) => {
|
||||
return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.updateChart();
|
||||
});
|
||||
},
|
||||
|
||||
// 新增:监听 chartData 变化
|
||||
watch: {
|
||||
// 深度监听数据变化,仅更新图表配置(不销毁实例)
|
||||
chartData: {
|
||||
handler() {
|
||||
console.log(this.chartData,'chartData');
|
||||
|
||||
this.updateChart();
|
||||
},
|
||||
deep: true,
|
||||
immediate: true // 初始化时立即执行
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateChart() {
|
||||
const chartDom = this.$refs.cockpitEffChip;
|
||||
if (!chartDom) {
|
||||
console.error('图表容器未找到!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose();
|
||||
}
|
||||
|
||||
this.myChart = echarts.init(chartDom);
|
||||
const { allPlaceNames, series } = this.chartData || {};
|
||||
|
||||
// 处理空数据
|
||||
const xData = allPlaceNames || [];
|
||||
const chartSeries = series || []; // 父组件传递的 series
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
},
|
||||
// formatter: (params) => {
|
||||
// let html = `${params[0].axisValue}<br/>`;
|
||||
// params.forEach(item => {
|
||||
// const unit = item.seriesName === '完成率' ? '%' : (
|
||||
// ['产量', '销量'].includes(this.$parent.selectedProfit) ? '片' : '万元'
|
||||
// );
|
||||
// html += `${item.marker} ${item.seriesName}: ${item.value}${unit}<br/>`;
|
||||
// });
|
||||
// return html;
|
||||
// }
|
||||
},
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 30,
|
||||
right: 20,
|
||||
left: 60,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
axisTick: { show: false },
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
interval: 0,
|
||||
padding: [5, 0, 0, 0]
|
||||
},
|
||||
data: xData
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
// 左侧Y轴:营业收入、成本(单位万元)
|
||||
{
|
||||
type: 'value',
|
||||
name: '万元',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'right'
|
||||
},
|
||||
|
||||
splitNumber: 4,
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
formatter: '{value}'
|
||||
},
|
||||
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
splitNumber: 4
|
||||
},
|
||||
// 右侧Y轴:利润占比(百分比)
|
||||
// {
|
||||
// type: 'value',
|
||||
// nameTextStyle: {
|
||||
// color: 'rgba(0, 0, 0, 0.45)',
|
||||
// fontSize: 12,
|
||||
// align: 'left'
|
||||
// },
|
||||
// min: 0,
|
||||
// max: 100,
|
||||
// axisTick: { show: false },
|
||||
// axisLabel: {
|
||||
// color: 'rgba(0, 0, 0, 0.45)',
|
||||
// fontSize: 12,
|
||||
// formatter: '{value}%'
|
||||
// },
|
||||
// splitLine: { show: false },
|
||||
// axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
// splitNumber: 4
|
||||
// }
|
||||
],
|
||||
series: chartSeries // 直接使用父组件传递的 series
|
||||
};
|
||||
|
||||
option && this.myChart.setOption(option);
|
||||
|
||||
// 窗口缩放适配和销毁逻辑保持不变
|
||||
window.addEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
|
||||
this.$once('hook:destroyed', () => {
|
||||
window.removeEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
this.myChart && this.myChart.dispose();
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 400px;"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
myChart: null, // 存储图表实例
|
||||
resizeHandler: null, // 存储resize事件处理函数
|
||||
// 核心:基地名称与序号的映射表(固定顺序)
|
||||
baseNameToIndexMap: {
|
||||
'宜兴': 7,
|
||||
'漳州': 8,
|
||||
'自贡': 3,
|
||||
'桐城': 2,
|
||||
'洛阳': 9,
|
||||
'合肥': 5,
|
||||
'宿迁': 6,
|
||||
'秦皇岛': 10
|
||||
}
|
||||
};
|
||||
},
|
||||
props: {
|
||||
chartData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
dateData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.initChart(); // 初始化图表(只执行一次)
|
||||
this.updateChart(); // 更新图表数据
|
||||
});
|
||||
},
|
||||
watch: {
|
||||
chartData: {
|
||||
handler() {
|
||||
console.log(this.chartData, 'chartData');
|
||||
this.updateChart(); // 仅更新数据,不重新创建实例
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 组件销毁时清理资源
|
||||
this.destroyChart();
|
||||
},
|
||||
methods: {
|
||||
// 初始化图表(只在mounted中执行一次)
|
||||
initChart() {
|
||||
const chartDom = this.$refs.cockpitEffChip;
|
||||
if (!chartDom) {
|
||||
console.error('图表容器未找到!');
|
||||
return;
|
||||
}
|
||||
|
||||
// 只创建一次图表实例
|
||||
this.myChart = echarts.init(chartDom);
|
||||
|
||||
// 绑定点击事件(只绑定一次,永久生效)
|
||||
this.myChart.getZr().on('click', (params) => {
|
||||
console.log('params', params);
|
||||
|
||||
// 提取点击的基地名称
|
||||
// const itemName = params.name;
|
||||
let itemName = undefined
|
||||
// 根据映射表获取对应的序号(未匹配到则返回0或其他默认值)
|
||||
let pointInPixel = [params.offsetX, params.offsetY];
|
||||
if (this.myChart.containPixel('grid', pointInPixel)) {
|
||||
let pointInGrid = this.myChart.convertFromPixel({
|
||||
seriesIndex: 0
|
||||
}, pointInPixel);
|
||||
let xIndex = pointInGrid[0]; //索引
|
||||
let handleIndex = Number(xIndex);
|
||||
let seriesObj = this.myChart.getOption(); //图表object对象
|
||||
var op = this.myChart.getOption();
|
||||
//获得图表中点击的列
|
||||
itemName = op.xAxis[0].data[handleIndex]; //获取点击的列名
|
||||
console.log(itemName, 'monthmonthmonth');
|
||||
console.log(handleIndex, seriesObj);
|
||||
};
|
||||
const baseIndex = this.baseNameToIndexMap[itemName] || 0;
|
||||
|
||||
console.log(`你点击了【${itemName}】(序号:${baseIndex})`);
|
||||
if (itemName === undefined) {
|
||||
return;
|
||||
}
|
||||
// 路由跳转时携带序号(或名称+序号)
|
||||
this.$router.push({
|
||||
path: 'operatingRevenueBase',
|
||||
query: { // 使用query传递参数(推荐),也可使用params
|
||||
// baseName: itemName,
|
||||
factory: baseIndex,
|
||||
dateData: this.dateData
|
||||
}
|
||||
// 若仍需用base作为参数:
|
||||
// base: itemName,
|
||||
// params: { baseIndex: baseIndex }
|
||||
});
|
||||
});
|
||||
|
||||
// 定义resize处理函数(命名函数,方便移除)
|
||||
this.resizeHandler = () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
};
|
||||
// 绑定resize事件(只绑定一次)
|
||||
window.addEventListener('resize', this.resizeHandler);
|
||||
},
|
||||
|
||||
// 更新图表数据(数据变化时执行)
|
||||
updateChart() {
|
||||
if (!this.myChart) {
|
||||
return; // 实例未初始化则返回
|
||||
}
|
||||
|
||||
const { allPlaceNames, series } = this.chartData || {};
|
||||
const xData = allPlaceNames || [];
|
||||
const chartSeries = series || [];
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 5,
|
||||
right: 20,
|
||||
left: 25,
|
||||
containLabel: true
|
||||
},
|
||||
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],
|
||||
// 可选:X轴标签显示“序号+名称”(如“1 宜兴”)
|
||||
// formatter: (value) => {
|
||||
// const index = this.baseNameToIndexMap[value] || '';
|
||||
// return index ? `${index} ${value}` : value;
|
||||
// }
|
||||
},
|
||||
data: xData
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '万元',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'right'
|
||||
},
|
||||
splitNumber: 4,
|
||||
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: { show: true, show: true, lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } }
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'left'
|
||||
},
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
formatter: '{value}%'
|
||||
},
|
||||
splitLine: { show: false },
|
||||
axisLine: { show: true, show: true, lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
splitNumber: 4
|
||||
}
|
||||
],
|
||||
series: chartSeries
|
||||
};
|
||||
|
||||
// 只更新配置,不重新创建实例
|
||||
this.myChart.setOption(option, true); // 第二个参数true表示清空原有配置,避免数据残留
|
||||
},
|
||||
|
||||
// 销毁图表资源
|
||||
destroyChart() {
|
||||
// 移除resize事件
|
||||
if (this.resizeHandler) {
|
||||
window.removeEventListener('resize', this.resizeHandler);
|
||||
}
|
||||
// 销毁图表实例
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose();
|
||||
this.myChart = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 380px;"></div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
myChart: null // 存储图表实例,避免重复创建
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 明确接收的props结构,增强可读性
|
||||
chartData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
}),
|
||||
// 校验数据格式
|
||||
// validator: (value) => {
|
||||
// return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
|
||||
// }
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.updateChart();
|
||||
});
|
||||
},
|
||||
|
||||
// 新增:监听 chartData 变化
|
||||
watch: {
|
||||
// 深度监听数据变化,仅更新图表配置(不销毁实例)
|
||||
chartData: {
|
||||
handler() {
|
||||
console.log(this.chartData,'chartData');
|
||||
this.updateChart();
|
||||
},
|
||||
deep: true,
|
||||
immediate: true // 初始化时立即执行
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateChart() {
|
||||
const chartDom = this.$refs.cockpitEffChip;
|
||||
if (!chartDom) {
|
||||
console.error('图表容器未找到!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose();
|
||||
}
|
||||
|
||||
this.myChart = echarts.init(chartDom);
|
||||
|
||||
const { allPlaceNames, series } = this.chartData || {};
|
||||
console.log('chartData', this.chartData);
|
||||
|
||||
// 处理空数据
|
||||
const xData = allPlaceNames || [];
|
||||
const chartSeries = series || []; // 父组件传递的 series
|
||||
console.log('xData', xData);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
},
|
||||
// formatter: (params) => {
|
||||
// let html = `${params[0].axisValue}<br/>`;
|
||||
// params.forEach(item => {
|
||||
// const unit = item.seriesName === '完成率' ? '%' : (
|
||||
// ['产量', '销量'].includes(this.$parent.selectedProfit) ? '片' : '万元'
|
||||
// );
|
||||
// html += `${item.marker} ${item.seriesName}: ${item.value}${unit}<br/>`;
|
||||
// });
|
||||
// return html;
|
||||
// }
|
||||
},
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom:20,
|
||||
right: 10,
|
||||
left: 25,
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
axisTick: { show: false },
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
interval: 0,
|
||||
padding: [5, 0, 0, 0]
|
||||
},
|
||||
data: xData
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
// 左侧Y轴:营业收入、成本(单位万元)
|
||||
{
|
||||
type: 'value',
|
||||
name: '万元',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'right'
|
||||
},
|
||||
splitNumber: 4,
|
||||
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: { show: true, show: true, lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
},
|
||||
// 右侧Y轴:利润占比(百分比)
|
||||
{
|
||||
type: 'value',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'left'
|
||||
},
|
||||
// min: 0,
|
||||
// max: 100,
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
formatter: '{value}%'
|
||||
},
|
||||
splitLine: { show: false },
|
||||
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
//
|
||||
splitNumber: 4,
|
||||
}
|
||||
],
|
||||
series: chartSeries // 直接使用父组件传递的 series
|
||||
};
|
||||
|
||||
option && this.myChart.setOption(option);
|
||||
|
||||
// 窗口缩放适配和销毁逻辑保持不变
|
||||
window.addEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
|
||||
this.$once('hook:destroyed', () => {
|
||||
window.removeEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
this.myChart && this.myChart.dispose();
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div :ref="refName" id="coreLineChart" style="width: 100%; height: 200px;"></div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
myChart: null // 存储图表实例,避免重复创建
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 明确接收的props结构,增强可读性
|
||||
refName: {
|
||||
type: String,
|
||||
default: () => 'cockpitEffChip',
|
||||
// 校验数据格式
|
||||
// validator: (value) => {
|
||||
// return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
|
||||
// }
|
||||
},
|
||||
chartData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
}),
|
||||
// 校验数据格式
|
||||
// validator: (value) => {
|
||||
// return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
|
||||
// }
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.updateChart();
|
||||
});
|
||||
},
|
||||
|
||||
// 新增:监听 chartData 变化
|
||||
watch: {
|
||||
// 深度监听数据变化,仅更新图表配置(不销毁实例)
|
||||
chartData: {
|
||||
handler() {
|
||||
console.log(this.chartData, 'chartData');
|
||||
this.updateChart();
|
||||
},
|
||||
deep: true,
|
||||
immediate: true // 初始化时立即执行
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateChart() {
|
||||
const chartDom = this.$refs[this.refName];
|
||||
if (!chartDom) {
|
||||
console.error('图表容器未找到!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose();
|
||||
}
|
||||
|
||||
this.myChart = echarts.init(chartDom);
|
||||
|
||||
const { allPlaceNames, series } = this.chartData || {};
|
||||
console.log('chartData', this.chartData);
|
||||
|
||||
// 处理空数据
|
||||
const xData = allPlaceNames || [];
|
||||
const chartSeries = series || []; // 父组件传递的 series
|
||||
console.log('xData', xData);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
// axisPointer: {
|
||||
// type: 'cross',
|
||||
// label: {
|
||||
// backgroundColor: '#6a7985'
|
||||
// }
|
||||
// },
|
||||
// formatter: (params) => {
|
||||
// let html = `${params[0].axisValue}<br/>`;
|
||||
// params.forEach(item => {
|
||||
// const unit = item.seriesName === '完成率' ? '%' : (
|
||||
// ['产量', '销量'].includes(this.$parent.selectedProfit) ? '片' : '万元'
|
||||
// );
|
||||
// html += `${item.marker} ${item.seriesName}: ${item.value}${unit}<br/>`;
|
||||
// });
|
||||
// return html;
|
||||
// }
|
||||
},
|
||||
grid: {
|
||||
top: 20,
|
||||
bottom: 30,
|
||||
right: 20,
|
||||
left: 5,
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
axisTick: { show: false },
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
interval: 0,
|
||||
padding: [5, 0, 0, 0]
|
||||
},
|
||||
data: xData
|
||||
}
|
||||
],
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
// name: '万元',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'right'
|
||||
},
|
||||
axisLine: {
|
||||
show: true, // 显示Y轴轴线(关键)
|
||||
lineStyle: {
|
||||
color: '#E5E6EB', // 轴线颜色(浅灰色,可自定义)
|
||||
width: 1, // 轴线宽度
|
||||
type: 'solid' // 实线(可选:dashed虚线、dotted点线)
|
||||
}
|
||||
},
|
||||
|
||||
splitNumber: 2,
|
||||
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)' } },
|
||||
},
|
||||
series: chartSeries // 直接使用父组件传递的 series
|
||||
};
|
||||
|
||||
option && this.myChart.setOption(option);
|
||||
|
||||
// 窗口缩放适配和销毁逻辑保持不变
|
||||
window.addEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
|
||||
this.$once('hook:destroyed', () => {
|
||||
window.removeEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
this.myChart && this.myChart.dispose();
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,349 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container name="当月数据对比" icon="cockpitItemIcon" size="operatingLarge" topSize="large">
|
||||
<!-- 1. 移除 .kpi-content 的固定高度,改为自适应 -->
|
||||
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%; gap: 16px">
|
||||
<div class="left" style="
|
||||
height: 380px;
|
||||
display: flex;
|
||||
width: 348px;
|
||||
background-color: rgba(249, 252, 255, 1);
|
||||
flex-direction: column;
|
||||
">
|
||||
<div style="
|
||||
padding: 16px 16px 5px 16px;
|
||||
line-height: 18px;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
letter-spacing: 1px;
|
||||
font-style: normal;
|
||||
">
|
||||
集团情况
|
||||
</div>
|
||||
<div style='font-size: 16px;line-height: 16px;text-align: right;padding-right: 16px;'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{chartData.group.rate[0]}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:chartData.group.flags>0?'#30B590':'#FF9423'}" >{{chartData.group.diff[0]}}</span></span>
|
||||
</div>
|
||||
<operatingTopBar :chartData="chartData" />
|
||||
</div>
|
||||
<div class="right" style="
|
||||
height: 380px;
|
||||
display: flex;
|
||||
width: 1220px;
|
||||
background-color: rgba(249, 252, 255, 1);
|
||||
">
|
||||
<!-- <top-item /> -->
|
||||
<operatingBar :dateData="dateData" :chartData="chartData" @sort-change="sortChange" />
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Container from "../components/container.vue";
|
||||
import operatingBar from "./operatingBar.vue";
|
||||
import operatingTopBar from "./operatingTopBar.vue";
|
||||
|
||||
export default {
|
||||
name: "ProductionStatus",
|
||||
components: { Container, operatingBar, operatingTopBar },
|
||||
props: {
|
||||
monthData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
group: {
|
||||
rate: 0,
|
||||
diff: 0,
|
||||
real: 0,
|
||||
target: 0
|
||||
},
|
||||
factory: []
|
||||
}),
|
||||
required: true
|
||||
},
|
||||
dateData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chartData: null, // 初始化 chartData 为 null
|
||||
groupData: {}, // 集团数据
|
||||
factoryData: [] // 工厂数据
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
monthData: {
|
||||
handler() {
|
||||
this.processChartData();
|
||||
},
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
sortChange(value) {
|
||||
this.$emit('sort-change', value);
|
||||
},
|
||||
/**
|
||||
* 判断rate对应的flag值(<1为0,>1为1)
|
||||
* @param {number} rate 处理后的rate值(已*100)
|
||||
* @returns {0|1} flag值
|
||||
*/
|
||||
getRateFlag(rate, real, target) {
|
||||
if (isNaN(rate) || rate === null || rate === undefined) return 0;
|
||||
|
||||
// 1. 完成率 >= 100 => 达标
|
||||
if (rate >= 100) return 1;
|
||||
|
||||
// 2. 完成率 = 0 且 (目标值=0 或 实际值=目标值=0) => 达标
|
||||
if (rate === 0 && target === 0) return 1;
|
||||
|
||||
// 其他情况 => 未达标
|
||||
return 0;
|
||||
},
|
||||
/**
|
||||
* 核心处理函数:在所有数据都准备好后,才组装 chartData
|
||||
*/
|
||||
processChartData() {
|
||||
// 1. 处理集团数据 - 提取各字段到对应数组
|
||||
this.groupData = this.monthData.group || { rate: 0, diff: 0, real: 0, target: 0 };
|
||||
|
||||
// 集团各维度数据数组(单条数据,对应凯盛新能)
|
||||
const groupTarget = [this.groupData.target]; // 预算值数组
|
||||
const groupDiff = [this.groupData.diff]; // 差值数组
|
||||
const groupReal = [this.groupData.real]; // 实际值数组
|
||||
const groupRate = [this.groupData.rate]; // 完成率数组
|
||||
// 新增:集团rate对应的flag
|
||||
const groupFlag = [this.getRateFlag(groupRate[0], groupReal[0], groupTarget[0])];
|
||||
|
||||
console.log('集团数据数组:', {
|
||||
groupTarget,
|
||||
groupDiff,
|
||||
groupReal,
|
||||
groupRate,
|
||||
groupFlag,
|
||||
rawGroupData: this.groupData
|
||||
});
|
||||
|
||||
// 2. 处理工厂数据 - 提取每个工厂的对应字段到数组
|
||||
this.factoryData = this.monthData.factory || [];
|
||||
// 提取工厂名称数组
|
||||
const factoryNames = this.factoryData.map(item => item.title || '');
|
||||
// 提取工厂各维度数据数组
|
||||
const factoryBudget = this.factoryData.map(item => item.budget || 0);
|
||||
const factoryReal = this.factoryData.map(item => item.real || 0);
|
||||
const factoryRate = this.factoryData.map(item => item.rate || 0);
|
||||
const factoryDiff = this.factoryData.map(item => item.diff || 0);
|
||||
// 新增:每个工厂rate对应的flag数组
|
||||
const factoryFlags = this.factoryData.map(item => this.getRateFlag(item.rate, item.real, item.budget));
|
||||
|
||||
// 3. 组装最终的chartData(供子组件使用)
|
||||
this.chartData = {
|
||||
// 集团数据(对应凯盛新能)
|
||||
group: {
|
||||
locations: ['凯盛新能'], // 集团名称
|
||||
targets: groupTarget, // 集团预算值数组
|
||||
diff: groupDiff, // 集团差值数组
|
||||
reals: groupReal, // 集团实际值数组
|
||||
rate: groupRate, // 集团完成率数组
|
||||
flags: groupFlag // 新增:集团rate对应的flag
|
||||
},
|
||||
// 工厂数据
|
||||
factory: {
|
||||
locations: factoryNames, // 工厂名称数组
|
||||
targets: factoryBudget, // 工厂预算数组
|
||||
reals: factoryReal, // 工厂实际值数组
|
||||
rates: factoryRate, // 工厂完成率数组
|
||||
diff: factoryDiff, // 工厂差值数组
|
||||
flags: factoryFlags // 新增:工厂rate对应的flags数组
|
||||
},
|
||||
// 原始数据备份(方便后续使用)
|
||||
rawData: {
|
||||
group: this.groupData,
|
||||
factory: this.factoryData
|
||||
}
|
||||
};
|
||||
|
||||
console.log('最终处理后的图表数据:', this.chartData);
|
||||
},
|
||||
},
|
||||
};
|
||||
</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>
|
||||
@@ -0,0 +1,347 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container name="累计数据对比" icon="cockpitItemIcon" size="opLargeBg" topSize="large">
|
||||
<!-- 1. 移除 .kpi-content 的固定高度,改为自适应 -->
|
||||
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%; gap: 16px">
|
||||
<div class="left" style="
|
||||
height: 380px;
|
||||
display: flex;
|
||||
width: 348px;
|
||||
background-color: rgba(249, 252, 255, 1);
|
||||
flex-direction: column;
|
||||
">
|
||||
<div style="
|
||||
padding: 16px 16px 5px 16px;
|
||||
line-height: 18px;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
letter-spacing: 1px;
|
||||
font-style: normal;
|
||||
">
|
||||
集团情况
|
||||
</div>
|
||||
<div style='font-size: 16px;line-height: 16px;text-align: right;padding-right: 16px;'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{chartData.group.rate[0]}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:chartData.group.flags>0?'#30B590':'#FF9423'}" >{{chartData.group.diff[0]}}</span></span>
|
||||
</div>
|
||||
<operatingTopBar :chartData="chartData" />
|
||||
</div>
|
||||
<div class="right" style="
|
||||
height: 380px;
|
||||
display: flex;
|
||||
width: 1220px;
|
||||
background-color: rgba(249, 252, 255, 1);
|
||||
">
|
||||
<!-- <top-item /> -->
|
||||
<operatingBar :dateData="dateData" @sort-change="sortChange" :chartData="chartData" />
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Container from "../components/container.vue";
|
||||
import operatingBar from "./operatingBar.vue";
|
||||
import operatingTopBar from "./operatingTopBar.vue";
|
||||
|
||||
export default {
|
||||
name: "ProductionStatus",
|
||||
components: { Container, operatingBar, operatingTopBar },
|
||||
props: {
|
||||
salesTrendMap: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
ytdData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
dateData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
chartData: null, // 初始化 chartData 为 null
|
||||
groupData: {}, // 集团数据
|
||||
factoryData: [] // 工厂数据
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
ytdData: {
|
||||
handler() {
|
||||
this.processChartData();
|
||||
},
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
sortChange(value) {
|
||||
this.$emit('sort-change', value);
|
||||
},
|
||||
/**
|
||||
* 判断rate对应的flag值(<1为0,>1为1)
|
||||
* @param {number} rate 处理后的rate值(已*100)
|
||||
* @returns {0|1} flag值
|
||||
*/
|
||||
getRateFlag(rate, real, target) {
|
||||
if (isNaN(rate) || rate === null || rate === undefined) return 0;
|
||||
|
||||
// 1. 完成率 >= 100 => 达标
|
||||
if (rate >= 100) return 1;
|
||||
|
||||
// 2. 完成率 = 0 且 (目标值=0 或 实际值=目标值=0) => 达标
|
||||
if (rate === 0 && target === 0) return 1;
|
||||
|
||||
// 其他情况 => 未达标
|
||||
return 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* 核心处理函数:在所有数据都准备好后,才组装 chartData
|
||||
*/
|
||||
processChartData() {
|
||||
// 1. 处理集团数据 - 提取各字段到对应数组
|
||||
this.groupData = this.ytdData.group || { rate: 0, diff: 0, real: 0, target: 0 };
|
||||
|
||||
// 集团各维度数据数组(单条数据,对应凯盛新能)
|
||||
const groupTarget = [this.groupData.target]; // 预算值数组
|
||||
const groupDiff = [this.groupData.diff]; // 差值数组
|
||||
const groupReal = [this.groupData.real]; // 实际值数组
|
||||
const groupRate = [this.groupData.rate]; // 完成率数组
|
||||
// 新增:集团rate对应的flag
|
||||
const groupFlag = [this.getRateFlag(groupRate[0], groupReal[0], groupTarget[0])];
|
||||
|
||||
console.log('集团数据数组:', {
|
||||
groupTarget,
|
||||
groupDiff,
|
||||
groupReal,
|
||||
groupRate,
|
||||
groupFlag,
|
||||
rawGroupData: this.groupData
|
||||
});
|
||||
|
||||
// 2. 处理工厂数据 - 提取每个工厂的对应字段到数组
|
||||
this.factoryData = this.ytdData.factory || [];
|
||||
// 提取工厂名称数组
|
||||
const factoryNames = this.factoryData.map(item => item.title || '');
|
||||
// 提取工厂各维度数据数组
|
||||
const factoryBudget = this.factoryData.map(item => item.budget || 0);
|
||||
const factoryReal = this.factoryData.map(item => item.real || 0);
|
||||
const factoryRate = this.factoryData.map(item => item.rate || 0);
|
||||
const factoryDiff = this.factoryData.map(item => item.diff || 0);
|
||||
// 新增:每个工厂rate对应的flag数组
|
||||
const factoryFlags = this.factoryData.map(item => this.getRateFlag(item.rate, item.real, item.budget));
|
||||
|
||||
// 3. 组装最终的chartData(供子组件使用)
|
||||
this.chartData = {
|
||||
// 集团数据(对应凯盛新能)
|
||||
group: {
|
||||
locations: ['凯盛新能'], // 集团名称
|
||||
targets: groupTarget, // 集团预算值数组
|
||||
diff: groupDiff, // 集团差值数组
|
||||
reals: groupReal, // 集团实际值数组
|
||||
rate: groupRate, // 集团完成率数组
|
||||
flags: groupFlag // 新增:集团rate对应的flag
|
||||
},
|
||||
// 工厂数据
|
||||
factory: {
|
||||
locations: factoryNames, // 工厂名称数组
|
||||
targets: factoryBudget, // 工厂预算数组
|
||||
reals: factoryReal, // 工厂实际值数组
|
||||
rates: factoryRate, // 工厂完成率数组
|
||||
diff: factoryDiff, // 工厂差值数组
|
||||
flags: factoryFlags // 新增:工厂rate对应的flags数组
|
||||
},
|
||||
// 原始数据备份(方便后续使用)
|
||||
rawData: {
|
||||
group: this.groupData,
|
||||
factory: this.factoryData
|
||||
}
|
||||
};
|
||||
|
||||
console.log('最终处理后的图表数据:', this.chartData);
|
||||
},
|
||||
},
|
||||
};
|
||||
</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>
|
||||
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<div class="lineBottom" style="height: 180px; width: 100%">
|
||||
<operatingLineBarSaleSingle :refName=" 'totalOperating' " :chartData="chartD" style="height: 99%; width: 100%" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import operatingLineBarSaleSingle from './operatingLineBarSaleSingle.vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: "Container",
|
||||
components: { operatingLineBarSaleSingle },
|
||||
props: ["detailData"],
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
locations() {
|
||||
return ['预算', '实际'];
|
||||
},
|
||||
chartD() {
|
||||
// 背景图片路径(若不需要可注释)
|
||||
// const bgImageUrl = require('@/assets/img/labelBg.png');
|
||||
console.log('this.detailData', this.detailData);
|
||||
const rate = this.detailData?.rate || 0
|
||||
const diff = this.detailData?.diff || 0
|
||||
console.log('diff', diff);
|
||||
|
||||
const seriesData = [
|
||||
{
|
||||
value: this.detailData?.budget || 0,
|
||||
flag: 1, // 实际项:达标(绿色)
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
offset: [0, 0],
|
||||
fontSize: 14,
|
||||
},
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(130, 204, 255, 1)' },
|
||||
{ offset: 1, color: 'rgba(75, 157, 255, 1)' }
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
},
|
||||
{
|
||||
value: this.detailData?.real || 0,
|
||||
flag: this.detailData?.flag, // 实际项:达标(绿色)
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
offset: [0, 0],
|
||||
fontSize: 14,
|
||||
},
|
||||
itemStyle: {
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
const data = {
|
||||
allPlaceNames: ['预算', '实际'],
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: 60,
|
||||
barCategoryGap: '50%',
|
||||
data: seriesData,
|
||||
itemStyle: {
|
||||
color: (params) => {
|
||||
const currentFlag = params.data.flag || 0;
|
||||
return currentFlag === 1
|
||||
? {
|
||||
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)' }
|
||||
]
|
||||
}
|
||||
: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(253, 209, 129, 1)' },
|
||||
{ offset: 1, color: 'rgba(249, 164, 74, 1)' }
|
||||
]
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
console.log('data', data);
|
||||
|
||||
return data;
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.coreBar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
|
||||
.barTop {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
|
||||
.title {
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFangSC;
|
||||
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;
|
||||
margin-right: 46px;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: PingFangSC;
|
||||
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);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
position: relative;
|
||||
gap: 2px;
|
||||
width: 283px;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
background: #ecf4fe;
|
||||
border-radius: 12px;
|
||||
margin: 0;
|
||||
|
||||
.item-button {
|
||||
cursor: pointer;
|
||||
width: 142px;
|
||||
height: 24px;
|
||||
font-family: PingFangSC;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #0b58ff;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
letter-spacing: 8px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.item-button.active {
|
||||
width: 142px;
|
||||
height: 24px;
|
||||
background: #3071ff;
|
||||
border-radius: 12px;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
337
src/views/home/accountsReceivableComponents/operatingTopBar.vue
Normal file
337
src/views/home/accountsReceivableComponents/operatingTopBar.vue
Normal file
@@ -0,0 +1,337 @@
|
||||
<template>
|
||||
<div class="coreBar">
|
||||
<div class="lineBottom" style="height: 320px; width: 100%">
|
||||
<operatingLineBar :chartData="chartD" style="height: 99%; width: 100%" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import operatingLineBar from './operatingLineBarSaleGroup.vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: "Container",
|
||||
components: { operatingLineBar },
|
||||
props: ["chartData"],
|
||||
data() {
|
||||
return {
|
||||
activeButton: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentDataSource() {
|
||||
console.log('yyyy', this.chartData);
|
||||
|
||||
return this.chartData.group
|
||||
},
|
||||
locations() {
|
||||
console.log('this.chartData', this.chartData);
|
||||
|
||||
return this.chartData.group.locations
|
||||
},
|
||||
// 根据按钮切换生成对应的 chartData
|
||||
chartD() {
|
||||
// 销量场景数据
|
||||
const data = this.currentDataSource;
|
||||
const diff = this.currentDataSource.diff[0]
|
||||
const rate = this.currentDataSource.rate[0]
|
||||
|
||||
const salesData = {
|
||||
allPlaceNames: this.locations,
|
||||
series: [
|
||||
{
|
||||
name: '预算',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0, // 左侧Y轴(万元)
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top'
|
||||
},
|
||||
barWidth: 65,
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(130, 204, 255, 1)' },
|
||||
{ offset: 1, color: 'rgba(75, 157, 255, 1)' }
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: data.targets // 目标销量(万元)
|
||||
},
|
||||
// 3. 实际(柱状图,含达标状态)
|
||||
{
|
||||
name: '实际',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top'
|
||||
},
|
||||
barWidth: 65,
|
||||
itemStyle: {
|
||||
color: (params) => {
|
||||
// 达标状态:1=达标(绿色),0=未达标(橙色)
|
||||
const safeFlag = data.flags;
|
||||
const currentFlag = safeFlag[params.dataIndex] || 0;
|
||||
return currentFlag === 1
|
||||
? {
|
||||
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)' }
|
||||
]
|
||||
}
|
||||
: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(253, 209, 129, 1)' },
|
||||
{ offset: 1, color: 'rgba(249, 164, 74, 1)' }
|
||||
]
|
||||
};
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: data.reals // 实际销量(万元)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// 毛利率场景数据
|
||||
const grossProfitData = {
|
||||
series: [
|
||||
// 1. 完成率(折线图)
|
||||
{
|
||||
name: '完成率',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
lineStyle: { color: 'rgba(40, 138, 255, .5)', width: 2 },
|
||||
itemStyle: {
|
||||
color: 'rgba(40, 138, 255, 1)',
|
||||
borderColor: 'rgba(40, 138, 255, 1)',
|
||||
borderWidth: 2,
|
||||
radius: 4
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.2,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(40, 138, 255, .9)' },
|
||||
{ offset: 1, color: 'rgba(40, 138, 255, 0)' }
|
||||
])
|
||||
},
|
||||
data: [106.7, 96.9, 106.5, 106.1, 93.8, 105.9], // 毛利率完成率(%)
|
||||
symbol: 'circle',
|
||||
symbolSize: 6
|
||||
},
|
||||
// 2. 目标(柱状图)
|
||||
{
|
||||
name: '预算',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 65,
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(130, 204, 255, 1)' },
|
||||
{ offset: 1, color: 'rgba(75, 157, 255, 1)' }
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: [30, 32, 31, 33, 32, 34] // 目标毛利率(万元)
|
||||
},
|
||||
// 3. 实际(柱状图)
|
||||
{
|
||||
name: '实际',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 65,
|
||||
itemStyle: {
|
||||
color: (params) => {
|
||||
const safeFlag = [1, 0, 1, 1, 0, 1]; // 达标状态
|
||||
const currentFlag = safeFlag[params.dataIndex] || 0;
|
||||
return currentFlag === 1
|
||||
? {
|
||||
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)' }
|
||||
]
|
||||
}
|
||||
: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(253, 209, 129, 1)' },
|
||||
{ offset: 1, color: 'rgba(249, 164, 74, 1)' }
|
||||
]
|
||||
};
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: [32, 31, 33, 35, 30, 36] // 实际毛利率(万元)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// 根据按钮状态返回对应数据
|
||||
return this.activeButton === 0 ? salesData : grossProfitData;
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.coreBar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
|
||||
.barTop {
|
||||
display: flex;
|
||||
justify-content: flex-end; // 标题左、右侧容器右,整体两端对齐
|
||||
align-items: center; // 垂直居中,避免上下错位
|
||||
gap: 16px; // 标题与右侧容器的最小间距,防止拥挤
|
||||
width: 100%; // 确保占满父容器,实现两端对齐
|
||||
|
||||
.title {
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
// 标题固定在左侧,不挤压右侧空间
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 1. 右侧容器:包裹图例和按钮组,整体靠右
|
||||
.right-container {
|
||||
display: flex;
|
||||
align-items: center; // 图例和按钮组垂直居中
|
||||
gap: 24px; // 图例与按钮组的间距,避免贴紧
|
||||
margin-right: 46px; // 右侧整体留边,与原按钮组边距一致
|
||||
}
|
||||
|
||||
// 2. 图例:在右侧容器内横向排列
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 16px; // 图例项之间间距,避免重叠
|
||||
align-items: center;
|
||||
// 移除原margin-left,避免位置偏移
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
white-space: nowrap; // 防止图例文字换行
|
||||
}
|
||||
|
||||
.legend-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.legend-icon.line {
|
||||
width: 12px;
|
||||
height: 2px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: -2px;
|
||||
left: 3px;
|
||||
width: 6px;
|
||||
border-radius: 50%;
|
||||
height: 6px;
|
||||
background-color: rgba(40, 138, 255, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.legend-icon.square {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
// 图例颜色
|
||||
.yield {
|
||||
background: rgba(40, 138, 255, 1);
|
||||
}
|
||||
|
||||
.target {
|
||||
background: #2889FF;
|
||||
}
|
||||
|
||||
.achieved {
|
||||
background: rgba(40, 203, 151, 1);
|
||||
}
|
||||
|
||||
.unachieved {
|
||||
background: rgba(255, 132, 0, 1);
|
||||
}
|
||||
|
||||
// 3. 按钮组:在右侧容器内,保留原有样式
|
||||
.button-group {
|
||||
display: flex;
|
||||
position: relative;
|
||||
gap: 2px;
|
||||
width: 283px;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
background: #ecf4fe;
|
||||
border-radius: 12px;
|
||||
// 移除原margin-right,由右侧容器统一控制
|
||||
margin: 0;
|
||||
|
||||
.item-button {
|
||||
cursor: pointer;
|
||||
width: 142px;
|
||||
height: 24px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #0b58ff;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
letter-spacing: 8px; // 确保文字间距生效
|
||||
padding-left: 8px; // 抵消letter-spacing导致的文字左偏
|
||||
}
|
||||
|
||||
.item-button.active {
|
||||
width: 142px;
|
||||
height: 24px;
|
||||
background: #3071ff;
|
||||
border-radius: 12px;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
202
src/views/home/accountsReceivableComponents/totalOverview.vue
Normal file
202
src/views/home/accountsReceivableComponents/totalOverview.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container :name="title" icon="cockpitItemIcon" size="operatingRevenueBg" 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; gap: 8px;">
|
||||
<div class="dashboard">
|
||||
<div class="title">
|
||||
累计完成率
|
||||
</div>
|
||||
<div class="number">
|
||||
<div class="yield">
|
||||
{{ ytdData?.rate || 0}}%
|
||||
</div>
|
||||
<div class="mom">
|
||||
同比{{ ytdData?.yoyRate || 0}}%
|
||||
<img v-if="ytdData?.yoyRate >= 0" class="arrow" src="../../../assets/img/topArrow.png" alt="">
|
||||
<img v-else class="arrow" src="../../../assets/img/downArrow.png" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="electricityGauge">
|
||||
<electricityGauge :id=" 'totalG' " :detailData="ytdData" id="totalGauge"></electricityGauge>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="line" style="padding: 0px;">
|
||||
<verticalBarChart :refName=" 'totalVerticalBarChart' " :detailData="ytdData">
|
||||
</verticalBarChart>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Container from './container.vue'
|
||||
import electricityGauge from './electricityGauge.vue'
|
||||
import verticalBarChart from './verticalBarChart.vue'
|
||||
|
||||
// import * as echarts from 'echarts'
|
||||
// import rawItem from './raw-Item.vue'
|
||||
|
||||
export default {
|
||||
name: 'ProductionStatus',
|
||||
components: { Container, electricityGauge, verticalBarChart },
|
||||
// mixins: [resize],
|
||||
props: {
|
||||
ytdData: { // 接收父组件传递的设备数据数组
|
||||
type: Object,
|
||||
default: () => {} // 默认空数组,避免报错
|
||||
},
|
||||
title: { // 接收父组件传递的设备数据数组
|
||||
type: String,
|
||||
default: () => '' // 默认空数组,避免报错
|
||||
},
|
||||
month: { // 接收父组件传递的设备数据数组
|
||||
type: String,
|
||||
default: () => '' // 默认空数组,避免报错
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
itemData: {
|
||||
handler(newValue, oldValue) {
|
||||
// this.updateChart()
|
||||
},
|
||||
deep: true // 若对象内属性变化需触发,需加 deep: true
|
||||
}
|
||||
},
|
||||
// computed: {
|
||||
// // 处理排序:包含“总成本”的项放前面,其余项按原顺序排列
|
||||
// sortedItemData() {
|
||||
// // 过滤出包含“总成本”的项(不区分大小写)
|
||||
// const totalCostItems = this.itemData.filter(item =>
|
||||
// item.name && item.name.includes('总成本')
|
||||
// );
|
||||
// // 过滤出不包含“总成本”的项
|
||||
// const otherItems = this.itemData.filter(item =>
|
||||
// !item.name || !item.name.includes('总成本')
|
||||
// );
|
||||
// // 合并:总成本项在前,其他项在后
|
||||
// return [...totalCostItems, ...otherItems];
|
||||
// }
|
||||
// },
|
||||
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;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
width: 264px;
|
||||
height: 205px;
|
||||
background: #F9FCFF;
|
||||
padding: 16px 0 0 10px;
|
||||
.title {
|
||||
// width: 190px;
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-family: YouSheBiaoTiHei;
|
||||
font-size: 46px;
|
||||
color: #0B58FF;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.mom {
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 500px;
|
||||
height: 205px;
|
||||
background: #F9FCFF;
|
||||
}
|
||||
|
||||
// .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> -->
|
||||
226
src/views/home/accountsReceivableComponents/verticalBarChart.vue
Normal file
226
src/views/home/accountsReceivableComponents/verticalBarChart.vue
Normal file
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<div style="width: 100%; height: 210px;position: relative;">
|
||||
<div style='font-size: 16px;position: absolute;right: 20px;top:10px'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{detailData.rate}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:detailData.flags>0?'#30B590':'#FF9423'}" >{{detailData.diff}}</span></span>
|
||||
</div>
|
||||
<div :ref="refName" id="coreLineChart" style="width: 100%; height: 210px;"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
myChart: null // 存储图表实例,避免重复创建
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 明确接收的props结构,增强可读性
|
||||
refName: {
|
||||
type: String,
|
||||
default: () => 'verticalBarChart',
|
||||
},
|
||||
detailData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
}),
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.updateChart();
|
||||
});
|
||||
},
|
||||
|
||||
// 新增:监听 chartData 变化
|
||||
watch: {
|
||||
// 深度监听数据变化,仅更新图表配置(不销毁实例)
|
||||
detailData: {
|
||||
handler() {
|
||||
console.log(this.chartData, 'chartData');
|
||||
|
||||
this.updateChart();
|
||||
},
|
||||
deep: true,
|
||||
immediate: true // 初始化时立即执行
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getRateFlag(rate, real, target) {
|
||||
if (isNaN(rate) || rate === null || rate === undefined) return 0;
|
||||
|
||||
// 1. 完成率 >= 100 => 达标
|
||||
if (rate >= 100) return 1;
|
||||
|
||||
// 2. 完成率 = 0 且 (目标值=0 或 实际值=目标值=0) => 达标
|
||||
if (rate === 0 && target === 0) return 1;
|
||||
|
||||
// 其他情况 => 未达标
|
||||
return 0;
|
||||
},
|
||||
updateChart() {
|
||||
const chartDom = this.$refs[this.refName];
|
||||
if (!chartDom) {
|
||||
console.error('图表容器未找到!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose();
|
||||
}
|
||||
|
||||
this.myChart = echarts.init(chartDom);
|
||||
const diff = this.detailData.diff || 0
|
||||
const rate = this.detailData.rate || 0
|
||||
const flagValue = this.getRateFlag(this.detailData.rate, this.detailData.real, this.detailData.target) || 0
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
},
|
||||
// formatter: (params) => {
|
||||
// let html = `${params[0].axisValue}<br/>`;
|
||||
// params.forEach(item => {
|
||||
// const unit = item.seriesName === '完成率' ? '%' : (
|
||||
// ['产量', '销量'].includes(this.$parent.selectedProfit) ? '片' : '万元'
|
||||
// );
|
||||
// html += `${item.marker} ${item.seriesName}: ${item.value}${unit}<br/>`;
|
||||
// });
|
||||
// return html;
|
||||
// }
|
||||
},
|
||||
grid: {
|
||||
top: 40,
|
||||
bottom: 15,
|
||||
right: 80,
|
||||
left: 10,
|
||||
containLabel: true,
|
||||
show: false // 隐藏grid背景,避免干扰
|
||||
},
|
||||
xAxis: {
|
||||
// 横向柱状图的x轴必须设为数值轴,否则无法正常展示数值
|
||||
type: 'value',
|
||||
// offset: 0,
|
||||
// boundaryGap: true ,
|
||||
// boundaryGap: [10, 0], // 可根据需要开启,控制轴的留白
|
||||
axisTick: { show: false },
|
||||
min: 0,
|
||||
//
|
||||
splitNumber: 4,
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
interval: 0,
|
||||
padding: [5, 0, 0, 0]
|
||||
},
|
||||
// data: xData // 数值轴不需要手动设置data,由series的数据自动生成
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.75)',
|
||||
fontSize: 12,
|
||||
interval: 0,
|
||||
padding: [5, 0, 0, 0]
|
||||
},
|
||||
axisLine: {
|
||||
show: true, // 显示Y轴轴线(关键)
|
||||
lineStyle: {
|
||||
color: '#E5E6EB', // 轴线颜色(浅灰色,可自定义)
|
||||
width: 1, // 轴线宽度
|
||||
type: 'solid' // 实线(可选:dashed虚线、dotted点线)
|
||||
}
|
||||
},
|
||||
axisTick: { show: false },
|
||||
// padding: [300, 100, 100, 100],
|
||||
data: ['实际', '预算'] // y轴分类:实际、预算
|
||||
},
|
||||
series: [
|
||||
{
|
||||
// name: '预算',
|
||||
type: 'bar',
|
||||
barWidth: 24,
|
||||
// barCategoryGap: '50', // 柱子之间的间距(相对于柱子宽度)
|
||||
// 数据长度与yAxis的分类数量匹配(实际、预算各一个值)
|
||||
data: [{
|
||||
value: this.detailData.real,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
fontSize: 14,
|
||||
},
|
||||
itemStyle: {
|
||||
color: flagValue === 1
|
||||
? {
|
||||
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)' }
|
||||
]
|
||||
}
|
||||
: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(253, 209, 129, 1)' },
|
||||
{ offset: 1, color: 'rgba(249, 164, 74, 1)' }
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
}
|
||||
}, {
|
||||
value: this.detailData.target,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
fontSize: 14,
|
||||
},
|
||||
itemStyle: {
|
||||
// 预算的渐变颜色(蓝系渐变)
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 1, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: '#82CCFF' }, // 浅蓝
|
||||
{ offset: 1, color: '#4B9DFF' } // 深蓝
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
},],
|
||||
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
option && this.myChart.setOption(option);
|
||||
|
||||
// 窗口缩放适配和销毁逻辑保持不变
|
||||
window.addEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
|
||||
this.$once('hook:destroyed', () => {
|
||||
window.removeEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
this.myChart && this.myChart.dispose();
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container :name="title" icon="cockpitItemIcon" size="operatingRevenueBg" topSize="middle">
|
||||
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%;">
|
||||
<div class="topItem-container" style="display: flex; gap: 8px; width: 100%;">
|
||||
<!-- 销量模块(直接传递整合了flag的salesData) -->
|
||||
<div class="dashboard left" @click="handleDashboardClick('/salesVolumeAnalysis/salesVolumeAnalysisBase')">
|
||||
<div style='position: relative;'>
|
||||
<div class="title">
|
||||
销量·万㎡
|
||||
</div>
|
||||
<div style='font-size: 16px;position: absolute;top:-4px;right:15px'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{ytdAnalysis[0].rate}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:ytdAnalysis[0].flags>0?'#30B590':'#FF9423'}" >{{ytdAnalysis[0].diff}}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<operatingSingleBar :detailData="salesData"></operatingSingleBar>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 单价模块(直接传递整合了flag的unitPriceData) -->
|
||||
<div class="dashboard right" @click="handleDashboardClick('/unitPriceAnalysis/unitPriceAnalysisBase')">
|
||||
<div style='position: relative;'>
|
||||
<div class="title">
|
||||
单价·元/㎡
|
||||
</div>
|
||||
<div style='font-size: 16px;position: absolute;top:-4px;right:15px'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{ytdAnalysis[1].rate}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:ytdAnalysis[1].flags>0?'#30B590':'#FF9423'}" >{{ytdAnalysis[1].diff}}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<operatingSingleBar :detailData="unitPriceData"></operatingSingleBar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Container from './container.vue'
|
||||
import operatingSingleBar from './operatingSingleBar.vue'
|
||||
|
||||
export default {
|
||||
name: 'ProductionStatus',
|
||||
components: { Container, operatingSingleBar },
|
||||
props: {
|
||||
ytdAnalysis: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{ title: "销量", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
{ title: "单价", budget: 0, real: 0, rate: 0, diff: 0 }
|
||||
]
|
||||
},
|
||||
dateData: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
month: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
// 初始化数据包含flag字段
|
||||
salesData: { title: "销量", budget: 0, real: 0, rate: 0, diff: 0, flag: 0 },
|
||||
unitPriceData: { title: "单价", budget: 0, real: 0, rate: 0, diff: 0, flag: 0 }
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
ytdAnalysis: {
|
||||
handler(newVal) {
|
||||
this.updateChart(newVal)
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateChart(this.ytdAnalysis)
|
||||
},
|
||||
methods: {
|
||||
handleDashboardClick(path) {
|
||||
this.$router.push({
|
||||
path: path,
|
||||
query: {
|
||||
factory: this.$route.query.factory ? this.$route.query.factory : 5,
|
||||
dateData: this.dateData
|
||||
}
|
||||
})
|
||||
},
|
||||
// 判断flag的核心方法
|
||||
getRateFlag(rate, real, target) {
|
||||
if (isNaN(rate) || rate === null || rate === undefined) return 0;
|
||||
|
||||
// 1. 完成率 >= 100 => 达标
|
||||
if (rate >= 100) return 1;
|
||||
|
||||
// 2. 完成率 = 0 且 (目标值=0 或 实际值=目标值=0) => 达标
|
||||
if (rate === 0 && target === 0) return 1;
|
||||
|
||||
// 其他情况 => 未达标
|
||||
return 0;
|
||||
},
|
||||
|
||||
updateChart(data) {
|
||||
// 数据兜底
|
||||
const salesItem = Array.isArray(data) && data[0] ? data[0] : { title: "销量", budget: 0, real: 0, rate: 0, diff: 0 };
|
||||
const unitPriceItem = Array.isArray(data) && data[1] ? data[1] : { title: "单价", budget: 0, real: 0, rate: 0, diff: 0 };
|
||||
|
||||
// 核心修改:将flag整合到数据对象中,无需单独定义salesFlag/unitPriceFlag
|
||||
this.salesData = {
|
||||
...salesItem, // 合并原有字段
|
||||
flag: this.getRateFlag(salesItem.rate, salesItem.real, salesItem.budget) // 新增flag字段
|
||||
};
|
||||
|
||||
this.unitPriceData = {
|
||||
...unitPriceItem, // 合并原有字段
|
||||
flag: this.getRateFlag(unitPriceItem.rate, unitPriceItem.real, unitPriceItem.budget) // 新增flag字段
|
||||
};
|
||||
|
||||
// 调试:确认整合后的数据
|
||||
console.log('整合flag后的销量数据:', this.salesData);
|
||||
console.log('整合flag后的单价数据:', this.unitPriceData);
|
||||
}
|
||||
}
|
||||
}
|
||||
</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;
|
||||
}
|
||||
|
||||
.topItem-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
height: 205px;
|
||||
background: #F9FCFF;
|
||||
padding: 16px 0 0 10px;
|
||||
margin: 0 4px;
|
||||
|
||||
.title {
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 2px;
|
||||
text-align: left;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chart-wrap {
|
||||
width: 100%;
|
||||
height: calc(100% - 30px);
|
||||
}
|
||||
|
||||
.number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
height: 32px;
|
||||
font-family: YouSheBiaoTiHei;
|
||||
font-size: 32px;
|
||||
color: #0B58FF;
|
||||
line-height: 32px;
|
||||
letter-spacing: 2px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mom {
|
||||
width: 97px;
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: left;
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard.left {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.dashboard.right {
|
||||
margin-right: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<el-form ref="form" :rules="rules" label-width="110px" :model="form">
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="客户名称" prop="name">
|
||||
<el-input v-model="form.name"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="客户编码" prop="code">
|
||||
<el-input v-model="form.code"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="所属基地" prop="daySpan">
|
||||
<el-select v-model="form.daySpan" placeholder="请选择" style="width: 100%;">
|
||||
<el-option label="否" :value= '0' ></el-option>
|
||||
<el-option label="是" :value= '1' ></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
<script>
|
||||
// import { getGroupClasses, updateGroupClasses, createGroupClasses, getCode } from '@/api/base/groupClasses'
|
||||
export default {
|
||||
name: 'CustomerInfoAdd',
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
id: '',
|
||||
name: '',
|
||||
code: '',
|
||||
daySpan:'',
|
||||
},
|
||||
isEdit: false, //是否是编辑
|
||||
rules: {
|
||||
name: [{ required: true, message: '请输入客户名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入客户编码', trigger: 'blur' }],
|
||||
daySpan: [{ required: true, message: '请选择所属基地', trigger: 'change' }],
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init(id) {
|
||||
if (id) {
|
||||
this.isEdit = true
|
||||
this.form.id = id
|
||||
// getGroupClasses(id).then((res) => {
|
||||
// if (res.code === 0) {
|
||||
// this.form = res.data
|
||||
// }
|
||||
// })
|
||||
}
|
||||
},
|
||||
submitForm() {
|
||||
this.$refs['form'].validate((valid) => {
|
||||
if (valid) {
|
||||
if (this.isEdit) {
|
||||
//编辑
|
||||
updateGroupClasses({ ...this.form }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$modal.msgSuccess("操作成功");
|
||||
this.$emit('successSubmit')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
createGroupClasses({ ...this.form }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$modal.msgSuccess("操作成功");
|
||||
this.$emit('successSubmit')
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
},
|
||||
formClear() {
|
||||
this.$refs.form.resetFields()
|
||||
this.isEdit = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
101
src/views/home/basicInfoConfiguration/components/groupKeyAdd.vue
Normal file
101
src/views/home/basicInfoConfiguration/components/groupKeyAdd.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<el-form ref="form" :rules="rules" label-width="120px" :model="form">
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="重点工作" prop="name">
|
||||
<el-input v-model="form.name"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="单位" prop="daySpan">
|
||||
<el-select v-model="form.daySpan" placeholder="请选择" style="width: 100%;">
|
||||
<el-option label="个" :value= '0' ></el-option>
|
||||
<el-option label="件" :value= '1' ></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="所属年份" prop="daySpan1">
|
||||
<el-date-picker
|
||||
v-model="value3"
|
||||
type="year"
|
||||
placeholder="选择年"
|
||||
style="width: 100%;">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="累计值计算方式" prop="daySpan2">
|
||||
<el-select v-model="form.daySpan" placeholder="请选择" style="width: 100%;">
|
||||
<el-option label="月" :value= '0' ></el-option>
|
||||
<el-option label="日" :value= '1' ></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
<script>
|
||||
// import { getGroupClasses, updateGroupClasses, createGroupClasses, getCode } from '@/api/base/groupClasses'
|
||||
export default {
|
||||
name: 'groupKeyAdd',
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
id: '',
|
||||
name: '',
|
||||
code: '',
|
||||
daySpan:'',
|
||||
},
|
||||
isEdit: false, //是否是编辑
|
||||
rules: {
|
||||
name: [{ required: true, message: '请输入重点工作', trigger: 'blur' }],
|
||||
daySpan: [{ required: true, message: '请选择单位', trigger: 'change' }],
|
||||
daySpan1: [{ required: true, message: '请选择所属年份', trigger: 'change' }],
|
||||
daySpan2: [{ required: true, message: '请选择累计值计算方式', trigger: 'change' }],
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init(id) {
|
||||
if (id) {
|
||||
this.isEdit = true
|
||||
this.form.id = id
|
||||
// getGroupClasses(id).then((res) => {
|
||||
// if (res.code === 0) {
|
||||
// this.form = res.data
|
||||
// }
|
||||
// })
|
||||
}
|
||||
},
|
||||
submitForm() {
|
||||
this.$refs['form'].validate((valid) => {
|
||||
if (valid) {
|
||||
if (this.isEdit) {
|
||||
//编辑
|
||||
updateGroupClasses({ ...this.form }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$modal.msgSuccess("操作成功");
|
||||
this.$emit('successSubmit')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
createGroupClasses({ ...this.form }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$modal.msgSuccess("操作成功");
|
||||
this.$emit('successSubmit')
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
},
|
||||
formClear() {
|
||||
this.$refs.form.resetFields()
|
||||
this.isEdit = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<el-form ref="form" :rules="rules" label-width="110px" :model="form">
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="产品名称" prop="name">
|
||||
<el-input v-model="form.name"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="规格" prop="code">
|
||||
<el-input v-model="form.code"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="工艺" prop="daySpan">
|
||||
<el-select v-model="form.daySpan" placeholder="请选择" style="width: 100%;">
|
||||
<el-option label="否" :value= '0' ></el-option>
|
||||
<el-option label="是" :value= '1' ></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
<script>
|
||||
// import { getGroupClasses, updateGroupClasses, createGroupClasses, getCode } from '@/api/base/groupClasses'
|
||||
export default {
|
||||
name: 'ProductInfoAdd',
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
id: '',
|
||||
name: '',
|
||||
code: '',
|
||||
daySpan:'',
|
||||
},
|
||||
isEdit: false, //是否是编辑
|
||||
rules: {
|
||||
name: [{ required: true, message: '请输入产品名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入产品规格', trigger: 'blur' }],
|
||||
daySpan: [{ required: true, message: '请选择工艺', trigger: 'change' }],
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init(id) {
|
||||
if (id) {
|
||||
this.isEdit = true
|
||||
this.form.id = id
|
||||
// getGroupClasses(id).then((res) => {
|
||||
// if (res.code === 0) {
|
||||
// this.form = res.data
|
||||
// }
|
||||
// })
|
||||
}
|
||||
},
|
||||
submitForm() {
|
||||
this.$refs['form'].validate((valid) => {
|
||||
if (valid) {
|
||||
if (this.isEdit) {
|
||||
//编辑
|
||||
updateGroupClasses({ ...this.form }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$modal.msgSuccess("操作成功");
|
||||
this.$emit('successSubmit')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
createGroupClasses({ ...this.form }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$modal.msgSuccess("操作成功");
|
||||
this.$emit('successSubmit')
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
},
|
||||
formClear() {
|
||||
this.$refs.form.resetFields()
|
||||
this.isEdit = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<search-bar :formConfigs="formConfig" ref="searchBarForm" @headBtnClick="buttonClick" />
|
||||
<base-table v-if="tableData.length" class="right-aside" :table-props="tableProps"
|
||||
:page="listQuery.pageNo" :limit="listQuery.pageSize" :table-data="tableData">
|
||||
<method-btn v-if="tableBtn.length" slot="handleBtn" :width="200" label="操作" :method-list="tableBtn"
|
||||
@clickBtn="handleClick" />
|
||||
</base-table>
|
||||
<pagination :limit.sync="listQuery.pageSize" :page.sync="listQuery.pageNo" :total="total" @pagination="getDataList" :background="true" />
|
||||
<!-- 新增 -->
|
||||
<base-dialog
|
||||
:dialogTitle="addOrEditTitle"
|
||||
:dialogVisible="centervisible"
|
||||
@cancel="handleCancel"
|
||||
@confirm="handleConfirm"
|
||||
:before-close="handleCancel"
|
||||
width="50%">
|
||||
<customer-info-add ref="customerList" @successSubmit="successSubmit" />
|
||||
</base-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CustomerInfoAdd from './components/customerInfoAdd.vue';
|
||||
const tableProps = [
|
||||
{
|
||||
prop: 'name',
|
||||
label: '客户名称'
|
||||
},
|
||||
{
|
||||
prop: 'code',
|
||||
label: '客户编码'
|
||||
},
|
||||
{
|
||||
prop: 'jd',
|
||||
label: '所属基地'
|
||||
},
|
||||
];
|
||||
export default {
|
||||
name: 'CustomerInfoConfiguration',
|
||||
components:{CustomerInfoAdd},
|
||||
data () {
|
||||
return {
|
||||
formConfig: [
|
||||
{
|
||||
type: 'input',
|
||||
label: '客户名称',
|
||||
placeholder: '客户名称',
|
||||
param: 'cName'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
label: '客户编码',
|
||||
placeholder: '客户编码',
|
||||
param: 'ccode'
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
btnName: '查询',
|
||||
name: 'search',
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
type: 'separate',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
btnName: '新增',
|
||||
name: 'add',
|
||||
color: 'success',
|
||||
plain: true
|
||||
}
|
||||
],
|
||||
listQuery:{
|
||||
pageSize:20,
|
||||
pageNo:1
|
||||
},
|
||||
total:2,
|
||||
tableData:[{name:'111',id:1}],
|
||||
tableProps,
|
||||
tableBtn:[
|
||||
{
|
||||
type: 'edit',
|
||||
btnName: '编辑',
|
||||
},
|
||||
{
|
||||
type: 'delete',
|
||||
btnName: '删除',
|
||||
},
|
||||
],
|
||||
addOrEditTitle: '',
|
||||
centervisible: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
buttonClick(val) {
|
||||
console.log(val)
|
||||
switch (val.btnName) {
|
||||
case 'search':
|
||||
this.listQuery.pageNo = 1;
|
||||
this.getList();
|
||||
break;
|
||||
case 'add':
|
||||
this.addOrEditTitle = '新增';
|
||||
this.$nextTick(() => {
|
||||
this.$refs.customerList.init();
|
||||
});
|
||||
this.centervisible = true;
|
||||
break;
|
||||
}
|
||||
},
|
||||
getDataList() {
|
||||
|
||||
},
|
||||
handleClick(val) {
|
||||
switch (val.type) {
|
||||
case 'edit':
|
||||
this.addOrEditTitle = '编辑';
|
||||
this.$nextTick(() => {
|
||||
this.$refs.customerList.init(val.data.id);
|
||||
});
|
||||
this.centervisible = true;
|
||||
break;
|
||||
default:
|
||||
this.handleDelete(val.data);
|
||||
}
|
||||
},
|
||||
handleCancel() {
|
||||
this.$refs.customerList.formClear();
|
||||
this.centervisible = false;
|
||||
this.addOrEditTitle = '';
|
||||
},
|
||||
handleConfirm() {
|
||||
this.$refs.customerList.submitForm();
|
||||
},
|
||||
successSubmit() {
|
||||
this.handleCancel();
|
||||
this.getList();
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
this.$modal.confirm('确定删除客户"' + row.name + '吗?').then(function() {
|
||||
deleteModel(row.id).then(response => {
|
||||
that.getList();
|
||||
that.$modal.msgSuccess("删除成功");
|
||||
})
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.app-container {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<search-bar :formConfigs="formConfig" ref="searchBarForm" @headBtnClick="buttonClick" />
|
||||
<base-table v-if="tableData.length" class="right-aside" :table-props="tableProps"
|
||||
:page="listQuery.pageNo" :limit="listQuery.pageSize" :table-data="tableData">
|
||||
<method-btn v-if="tableBtn.length" slot="handleBtn" :width="200" label="操作" :method-list="tableBtn"
|
||||
@clickBtn="handleClick" />
|
||||
</base-table>
|
||||
<pagination :limit.sync="listQuery.pageSize" :page.sync="listQuery.pageNo" :total="total" @pagination="getDataList" :background="true" />
|
||||
<!-- 新增 -->
|
||||
<base-dialog
|
||||
:dialogTitle="addOrEditTitle"
|
||||
:dialogVisible="centervisible"
|
||||
@cancel="handleCancel"
|
||||
@confirm="handleConfirm"
|
||||
:before-close="handleCancel"
|
||||
width="50%">
|
||||
<group-key-add ref="groupKey" @successSubmit="successSubmit" />
|
||||
</base-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GroupKeyAdd from './components/groupKeyAdd.vue';
|
||||
const tableProps = [
|
||||
{
|
||||
prop: 'name',
|
||||
label: '重点工作'
|
||||
},
|
||||
{
|
||||
prop: 'code1',
|
||||
label: '单位'
|
||||
},
|
||||
{
|
||||
prop: 'code',
|
||||
label: '所属年份'
|
||||
},
|
||||
{
|
||||
prop: 'jd',
|
||||
label: '累计值计算方式'
|
||||
},
|
||||
];
|
||||
export default {
|
||||
name: 'GroupKeyTaskConfiguration',
|
||||
components:{GroupKeyAdd},
|
||||
data () {
|
||||
return {
|
||||
formConfig: [
|
||||
{
|
||||
type: 'input',
|
||||
label: '重点工作',
|
||||
placeholder: '重点工作',
|
||||
param: 'cName'
|
||||
},
|
||||
{
|
||||
type: 'datePicker',
|
||||
label: '所属年份',
|
||||
dateType: 'year',
|
||||
format: 'yyyy',
|
||||
valueFormat: 'yyyy',
|
||||
placeholder: '所属年份',
|
||||
param: 'searchTime1',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
btnName: '查询',
|
||||
name: 'search',
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
type: 'separate',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
btnName: '新增',
|
||||
name: 'add',
|
||||
color: 'success',
|
||||
plain: true
|
||||
}
|
||||
],
|
||||
listQuery:{
|
||||
pageSize:20,
|
||||
pageNo:1
|
||||
},
|
||||
total:2,
|
||||
tableData:[{name:'111'}],
|
||||
tableProps,
|
||||
tableBtn:[
|
||||
{
|
||||
type: 'edit',
|
||||
btnName: '编辑',
|
||||
},
|
||||
{
|
||||
type: 'delete',
|
||||
btnName: '删除',
|
||||
},
|
||||
],
|
||||
addOrEditTitle: '',
|
||||
centervisible: false,
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
buttonClick(val) {
|
||||
console.log(val)
|
||||
switch (val.btnName) {
|
||||
case 'search':
|
||||
this.listQuery.pageNo = 1;
|
||||
this.getList();
|
||||
break;
|
||||
case 'add':
|
||||
this.addOrEditTitle = '新增';
|
||||
this.$nextTick(() => {
|
||||
this.$refs.groupKey.init();
|
||||
});
|
||||
this.centervisible = true;
|
||||
break;
|
||||
}
|
||||
},
|
||||
getDataList() {
|
||||
|
||||
},
|
||||
handleClick(val) {
|
||||
switch (val.type) {
|
||||
case 'edit':
|
||||
this.addOrEditTitle = '编辑';
|
||||
this.$nextTick(() => {
|
||||
this.$refs.groupKey.init(val.data.id);
|
||||
});
|
||||
this.centervisible = true;
|
||||
break;
|
||||
default:
|
||||
this.handleDelete(val.data);
|
||||
}
|
||||
},
|
||||
handleCancel() {
|
||||
this.$refs.groupKey.formClear();
|
||||
this.centervisible = false;
|
||||
this.addOrEditTitle = '';
|
||||
},
|
||||
handleConfirm() {
|
||||
this.$refs.groupKey.submitForm();
|
||||
},
|
||||
successSubmit() {
|
||||
this.handleCancel();
|
||||
this.getList();
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
this.$modal.confirm('确定删除重点工作"' + row.name + '吗?').then(function() {
|
||||
deleteModel(row.id).then(response => {
|
||||
that.getList();
|
||||
that.$modal.msgSuccess("删除成功");
|
||||
})
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.app-container {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<search-bar :formConfigs="formConfig" ref="searchBarForm" @headBtnClick="buttonClick" />
|
||||
<base-table v-if="tableData.length" class="right-aside" :table-props="tableProps"
|
||||
:page="listQuery.pageNo" :limit="listQuery.pageSize" :table-data="tableData">
|
||||
<method-btn v-if="tableBtn.length" slot="handleBtn" :width="200" label="操作" :method-list="tableBtn"
|
||||
@clickBtn="handleClick" />
|
||||
</base-table>
|
||||
<pagination :limit.sync="listQuery.pageSize" :page.sync="listQuery.pageNo" :total="total" @pagination="getDataList" :background="true" />
|
||||
<!-- 新增 -->
|
||||
<base-dialog
|
||||
:dialogTitle="addOrEditTitle"
|
||||
:dialogVisible="centervisible"
|
||||
@cancel="handleCancel"
|
||||
@confirm="handleConfirm"
|
||||
:before-close="handleCancel"
|
||||
width="50%">
|
||||
<product-info-add ref="prodectInfo" @successSubmit="successSubmit" />
|
||||
</base-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ProductInfoAdd from './components/productInfoAdd.vue';
|
||||
const tableProps = [
|
||||
{
|
||||
prop: 'name',
|
||||
label: '产品名称'
|
||||
},
|
||||
{
|
||||
prop: 'code',
|
||||
label: '工艺'
|
||||
},
|
||||
{
|
||||
prop: 'jd',
|
||||
label: '规格'
|
||||
},
|
||||
];
|
||||
export default {
|
||||
name: 'ProductInfoConfiguration',
|
||||
components:{ProductInfoAdd},
|
||||
data () {
|
||||
return {
|
||||
formConfig: [
|
||||
{
|
||||
type: 'input',
|
||||
label: '产品名称',
|
||||
placeholder: '产品名称',
|
||||
param: 'cName'
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
btnName: '查询',
|
||||
name: 'search',
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
type: 'separate',
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
btnName: '新增',
|
||||
name: 'add',
|
||||
color: 'success',
|
||||
plain: true
|
||||
}
|
||||
],
|
||||
listQuery:{
|
||||
pageSize:20,
|
||||
pageNo:1
|
||||
},
|
||||
total:2,
|
||||
tableData:[{name:'111'}],
|
||||
tableProps,
|
||||
tableBtn:[
|
||||
{
|
||||
type: 'edit',
|
||||
btnName: '编辑',
|
||||
},
|
||||
{
|
||||
type: 'delete',
|
||||
btnName: '删除',
|
||||
},
|
||||
],
|
||||
addOrEditTitle: '',
|
||||
centervisible: false,
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
buttonClick(val) {
|
||||
console.log(val)
|
||||
switch (val.btnName) {
|
||||
case 'search':
|
||||
this.listQuery.pageNo = 1;
|
||||
this.getList();
|
||||
break;
|
||||
case 'add':
|
||||
this.addOrEditTitle = '新增';
|
||||
this.$nextTick(() => {
|
||||
this.$refs.prodectInfo.init();
|
||||
});
|
||||
this.centervisible = true;
|
||||
break;
|
||||
}
|
||||
},
|
||||
getDataList() {
|
||||
|
||||
},
|
||||
handleClick(val) {
|
||||
switch (val.type) {
|
||||
case 'edit':
|
||||
this.addOrEditTitle = '编辑';
|
||||
this.$nextTick(() => {
|
||||
this.$refs.prodectInfo.init(val.data.id);
|
||||
});
|
||||
this.centervisible = true;
|
||||
break;
|
||||
default:
|
||||
this.handleDelete(val.data);
|
||||
}
|
||||
},
|
||||
handleCancel() {
|
||||
this.$refs.prodectInfo.formClear();
|
||||
this.centervisible = false;
|
||||
this.addOrEditTitle = '';
|
||||
},
|
||||
handleConfirm() {
|
||||
this.$refs.prodectInfo.submitForm();
|
||||
},
|
||||
successSubmit() {
|
||||
this.handleCancel();
|
||||
this.getList();
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
this.$modal.confirm('确定删除产品"' + row.name + '吗?').then(function() {
|
||||
deleteModel(row.id).then(response => {
|
||||
that.getList();
|
||||
that.$modal.msgSuccess("删除成功");
|
||||
})
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.app-container {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -39,7 +39,7 @@ export default {
|
||||
return {
|
||||
// 图表样式配置项,可以抽离出来方便管理
|
||||
chartConfig: {
|
||||
manageCost: {
|
||||
'管理费用': {
|
||||
name: '管理费用',
|
||||
lineColor: 'rgba(11, 88, 255, .5)',
|
||||
itemColor: 'rgba(11, 88, 255, .5)',
|
||||
@@ -49,7 +49,7 @@ export default {
|
||||
{ offset: 1, color: 'rgba(18, 255, 245, 0)' },
|
||||
]
|
||||
},
|
||||
saleCost: {
|
||||
'销售费用': {
|
||||
name: '销售费用',
|
||||
lineColor: 'rgba(54, 181, 138, .5)',
|
||||
itemColor: 'rgba(54, 181, 138, .5)',
|
||||
@@ -59,7 +59,7 @@ export default {
|
||||
{ offset: 1, color: 'rgba(18, 255, 245, 0)' },
|
||||
]
|
||||
},
|
||||
financeCost: {
|
||||
'财务费用': {
|
||||
name: '财务费用',
|
||||
lineColor: 'rgba(255, 132, 0, .5)',
|
||||
itemColor: 'rgba(255, 132, 0, .5)',
|
||||
@@ -78,7 +78,7 @@ export default {
|
||||
// 从 cost.line 中获取任意一个有数据的键的 keys 作为 X 轴
|
||||
const lineData = this.line || {};
|
||||
const firstKey = Object.keys(lineData)[0];
|
||||
return firstKey ? Object.keys(lineData[firstKey]) : [];
|
||||
return firstKey ? Object.keys(lineData[firstKey].real) : [];
|
||||
},
|
||||
// 动态生成 series 数据
|
||||
chartSeries() {
|
||||
@@ -94,7 +94,7 @@ export default {
|
||||
return Object.keys(this.chartConfig).map(key => {
|
||||
const config = this.chartConfig[key];
|
||||
// 确保数据顺序和 X 轴一致
|
||||
const dataValues = xAxisKeys.map(date => lineData[key] ? lineData[key][date] : 0);
|
||||
const dataValues = xAxisKeys.map(date => lineData[key] ? lineData[key].real[date] : 0);
|
||||
|
||||
return {
|
||||
name: config.name,
|
||||
|
||||
@@ -321,7 +321,7 @@ export default {
|
||||
display: inline-flex;
|
||||
position: absolute;
|
||||
right: 5%;
|
||||
top: 10%;
|
||||
top: 25px;
|
||||
z-index: 9999;
|
||||
align-items: center;
|
||||
border-radius: 24px;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container name="销售重点指标" icon="cockpitItemIcon" size="topBasic" topSize="basic">
|
||||
<Container name="销售重点指标" icon="cockpitItemIcon" size="topBasic" topSize="basic" :isShowTab='true' @tabChange='tabChange'>
|
||||
<!-- 1. 移除 .kpi-content 的固定高度,改为自适应 -->
|
||||
<div class="kpi-content" style="padding: 14px 16px; display: flex;flex-direction: column; width: 100%;">
|
||||
<!-- 2. .top 保持 flex,无需固定高度,自动跟随子元素拉伸 -->
|
||||
<div class="top" style="display: flex; width: 100%;">
|
||||
<top-item :sale="sale" :dateData="dateData" />
|
||||
<top-item :sale="saleData" :dateData="dateData" />
|
||||
</div>
|
||||
<div class="bottom"
|
||||
style="display: flex; width: 100%;margin-top: 8px;background-color: rgba(249, 252, 255, 1);">
|
||||
@@ -39,16 +39,39 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null
|
||||
chart: null,
|
||||
saleData:{},
|
||||
currentTap:'month'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
sale: {
|
||||
handler(newVal) {
|
||||
if(this.currentTap === 'month') {
|
||||
this.saleData = this.sale.mon
|
||||
}else{
|
||||
this.saleData = this.sale.total
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 初始化图表(若需展示图表,需在模板中添加对应 DOM)
|
||||
// this.$nextTick(() => this.updateChart())
|
||||
this.saleData = this.sale.month
|
||||
},
|
||||
methods: {
|
||||
tabChange(val) {
|
||||
if(val === 'month') {
|
||||
this.currentTap = 'month'
|
||||
this.saleData = this.sale.mon
|
||||
}else{
|
||||
this.currentTap = 'total'
|
||||
this.saleData = this.sale.total
|
||||
}
|
||||
console.log(this.saleData);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -78,7 +78,7 @@ export default {
|
||||
label: { backgroundColor: '#6a7985' }
|
||||
}
|
||||
},
|
||||
grid: { top: 35, bottom: 20, right: 25, left: 70 },
|
||||
grid: { top: 35, bottom: 3, right: 15, left: 10, containLabel: true},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="content-wrapper">
|
||||
<div class="left">
|
||||
<div class="number">{{ item.targetValue }}</div>
|
||||
<div class="title">上月</div>
|
||||
<div class="title">{{currentTap==='month'?'上月':'上年'}}</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="right">
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="number" :style="{ color: getColor(item.currentValue, item.targetValue) }">
|
||||
{{ item.currentValue }}
|
||||
</div>
|
||||
<div class="title">本月</div>
|
||||
<div class="title">{{currentTap==='month'?'本月':'本年'}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
@@ -46,6 +46,10 @@ export default {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
currentTap: {
|
||||
type: String,
|
||||
default: 'month'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<!-- 传入点击切换的状态到Container组件 -->
|
||||
<Container name="财务重点指标" nameTwo="费用重点指标" icon="cockpitItemIcon" size="topBasic" @switchTab="handleTabSwitch">
|
||||
<Container name="财务重点指标" nameTwo="费用重点指标" icon="cockpitItemIcon" size="topBasic" @switchTab="handleTabSwitch" @tabChange='tabChange'>
|
||||
<div class="bottom-left-content" style="display: flex;gap: 9px;padding: 14px 16px;flex-direction: column;">
|
||||
<!-- 根据activeTab状态,切换显示采购/存货内容 -->
|
||||
<template v-if="activeTab === 'purchase'">
|
||||
<!-- 采购重点指标对应的内容 -->
|
||||
<coreBottomLeftItem :dateData="dateData" :finance="finance"></coreBottomLeftItem>
|
||||
<coreBottomLeftItem :dateData="dateData" :finance="financeData"></coreBottomLeftItem>
|
||||
<div class="bottom"
|
||||
style="display: flex; width: 100%;margin-top: 8px;background-color: rgba(249, 252, 255, 1);">
|
||||
<coreBottomBar :dateData="dateData" :line="finance.line"></coreBottomBar>
|
||||
@@ -14,7 +14,7 @@
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'inventory'">
|
||||
<!-- 存货重点指标对应的内容 -->
|
||||
<costItem :dateData="dateData" :cost="cost"></costItem>
|
||||
<costItem :dateData="dateData" :cost="costData" :currentTap='currentTap'></costItem>
|
||||
<div class="bottom"
|
||||
style="display: flex; width: 100%;margin-top: 8px;background-color: rgba(249, 252, 255, 1);">
|
||||
<CostsBottomBar :line="cost.line" :dateData="dateData">
|
||||
@@ -52,13 +52,16 @@ export default {
|
||||
dateData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'purchase', // 激活的标签:purchase=采购,inventory=存货
|
||||
showChart: true, // 控制图表是否显示
|
||||
chart: null // 图表实例
|
||||
chart: null, // 图表实例
|
||||
financeData:{},
|
||||
costData:{},
|
||||
currentTap: 'month'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -71,7 +74,28 @@ export default {
|
||||
this.updateChart()
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
finance: {
|
||||
handler() {
|
||||
if(this.currentTap === 'month') {
|
||||
this.financeData = this.finance.mon
|
||||
}else{
|
||||
this.financeData = this.finance.total
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
cost: {
|
||||
handler() {
|
||||
if(this.currentTap === 'month') {
|
||||
this.costData = this.cost.mon
|
||||
}else{
|
||||
this.costData = this.cost.total
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
|
||||
},
|
||||
mounted() {
|
||||
// 初始化图表
|
||||
@@ -85,6 +109,17 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
tabChange(val) {
|
||||
if(val === 'month') {
|
||||
this.currentTap = 'month'
|
||||
this.financeData = this.finance.mon
|
||||
this.costData = this.cost.mon
|
||||
}else{
|
||||
this.currentTap = 'total'
|
||||
this.financeData = this.finance.total
|
||||
this.costData = this.cost.total
|
||||
}
|
||||
},
|
||||
// 处理标题点击切换标签
|
||||
handleTabSwitch(tabType) {
|
||||
this.activeTab = tabType // tabType由Container组件传递:'purchase'或'inventory'
|
||||
|
||||
@@ -16,7 +16,17 @@
|
||||
<span class="title-text">{{ nameTwo }}</span>
|
||||
|
||||
</div>
|
||||
<span class="change-text" :class="{ 'change-text-right': isLeftTransparent }">点击切换</span>
|
||||
<!-- <span class="change-text" :class="{ 'change-text-right': isLeftTransparent }">点击切换</span> -->
|
||||
<div class="tab-group">
|
||||
<!-- 月度Tab:点击切换状态,动态绑定样式 -->
|
||||
<div class="tab-item" :class="{ active: activeTab === 'month' }" @click="handleTabClick('month')">
|
||||
月度
|
||||
</div>
|
||||
<!-- 累计Tab:点击切换状态,动态绑定样式 -->
|
||||
<div class="tab-item" :class="{ active: activeTab === 'total' }" @click="handleTabClick('total')">
|
||||
累计
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="container-body">
|
||||
@@ -40,10 +50,18 @@ export default {
|
||||
return {
|
||||
// 初始状态:左侧不透明(1),右侧透明(0.3)
|
||||
isLeftTransparent: true, // 左侧透明度状态(true=1,false=0.3)
|
||||
isRightTransparent: false // 右侧透明度状态(true=1,false=0.3)
|
||||
isRightTransparent: false, // 右侧透明度状态(true=1,false=0.3)
|
||||
activeTab: 'month', // 默认激活的Tab为月度
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleTabClick(tabType) {
|
||||
this.activeTab = tabType;
|
||||
// 向父组件派发Tab切换事件,传递当前选中的Tab类型
|
||||
this.$emit('tabChange', tabType);
|
||||
// 可选:同时传递更详细的信息(如标签名)
|
||||
// this.$emit('tabChange', { type: tabType, name: tabType === 'month' ? '月度' : '累计' });
|
||||
},
|
||||
// 点击左侧标题:左侧保持不透明,右侧变透明,并派发采购标签事件
|
||||
handleLeftClick() {
|
||||
this.isLeftTransparent = true; // 左侧不透明
|
||||
@@ -151,7 +169,7 @@ export default {
|
||||
background: linear-gradient(90deg, #FFFFFF 0%, rgba(253, 255, 255, 0) 100%);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 240px;
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
@@ -258,4 +276,41 @@ export default {
|
||||
// text-align: left;
|
||||
// font-style: normal;
|
||||
}
|
||||
.tab-group {
|
||||
display: inline-flex;
|
||||
position: absolute;
|
||||
right: 5%;
|
||||
top:20px;
|
||||
z-index: 9999;
|
||||
align-items: center;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
gap: 8px; // Tab之间的间距
|
||||
}
|
||||
|
||||
// Tab基础样式(统一)
|
||||
.tab-item {
|
||||
padding: 0 24px;
|
||||
width: 79px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s ease; // 样式切换动画
|
||||
}
|
||||
|
||||
// 未激活的Tab样式(原first-child样式)
|
||||
.tab-item:not(.active) {
|
||||
background: #ECF4FE;
|
||||
color: #0B58FF;
|
||||
}
|
||||
|
||||
// 激活的Tab样式(原last-child样式)
|
||||
.tab-item.active {
|
||||
background: #3071FF;
|
||||
color: #F9FCFF;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
134
src/views/home/components/heatBarChart.vue
Normal file
134
src/views/home/components/heatBarChart.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 214px;"></div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
myChart: null // 存储图表实例,避免重复创建
|
||||
};
|
||||
},
|
||||
props: {
|
||||
lineData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
xData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.updateChart();
|
||||
});
|
||||
},
|
||||
|
||||
// 新增:监听 chartData 变化
|
||||
watch: {
|
||||
// 深度监听数据变化,仅更新图表配置(不销毁实例)
|
||||
lineData: {
|
||||
handler() {
|
||||
console.log(this.lineData,'lineData');
|
||||
this.updateChart();
|
||||
},
|
||||
deep: true,
|
||||
immediate: true // 初始化时立即执行
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateChart() {
|
||||
const chartDom = this.$refs.cockpitEffChip;
|
||||
if (!chartDom) {
|
||||
console.error('图表容器未找到!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose();
|
||||
}
|
||||
|
||||
this.myChart = echarts.init(chartDom);
|
||||
|
||||
console.log('linedaata',this.lineData)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 30,
|
||||
right: 20,
|
||||
left: 60,
|
||||
},
|
||||
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:this.xData
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
// 左侧Y轴:营业收入、成本(单位万元)
|
||||
{
|
||||
type: 'value',
|
||||
name: 'kcal/kg',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'right'
|
||||
},
|
||||
|
||||
splitNumber: 4,
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
formatter: '{value}'
|
||||
},
|
||||
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
splitNumber: 4
|
||||
},
|
||||
],
|
||||
series: []
|
||||
};
|
||||
|
||||
option && this.myChart.setOption(option);
|
||||
|
||||
// 窗口缩放适配和销毁逻辑保持不变
|
||||
window.addEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
|
||||
this.$once('hook:destroyed', () => {
|
||||
window.removeEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
this.myChart && this.myChart.dispose();
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
316
src/views/home/components/keyProductContainer.vue
Normal file
316
src/views/home/components/keyProductContainer.vue
Normal file
@@ -0,0 +1,316 @@
|
||||
<template>
|
||||
<div class="cockpitContainer" :class="['cockpitContainer__' + size]">
|
||||
<div class="container-top">
|
||||
<!-- 左侧标题:点击切换到采购标签,并更新透明度 -->
|
||||
<div class="content-top-left title-wrapper" @click="handleLeftClick"
|
||||
:style="{ opacity: isLeftTransparent ? 1 : 0.3 }">
|
||||
<svg-icon class="title-icon" style="font-size: 32px; margin-left: 16px" :icon-class="icon" />
|
||||
<span class="title-text">{{ name }}</span>
|
||||
<!-- <span v-if="!isLeftTransparent" class="change-text">点击切换</span> -->
|
||||
|
||||
</div>
|
||||
<!-- 右侧标题:点击切换到存货标签,并更新透明度 -->
|
||||
<div class="content-top-right title-wrapper" v-if="nameTwo" @click="handleRightClick"
|
||||
:style="{ opacity: isRightTransparent ? 1 : 0.3 }">
|
||||
<svg-icon class="title-icon" style="font-size: 32px; margin-left: 16px" :icon-class="iconTwo || icon" />
|
||||
<span class="title-text">{{ nameTwo }}</span>
|
||||
|
||||
</div>
|
||||
<!-- <span class="change-text" :class="{ 'change-text-right': isLeftTransparent }">点击切换</span> -->
|
||||
<div class="tab-group">
|
||||
<!-- 月度Tab:点击切换状态,动态绑定样式 -->
|
||||
<div class="tab-item" :class="{ active: activeTab === 'month' }" @click="handleTabClick('month')">
|
||||
月度
|
||||
</div>
|
||||
<!-- 累计Tab:点击切换状态,动态绑定样式 -->
|
||||
<div class="tab-item" :class="{ active: activeTab === 'total' }" @click="handleTabClick('total')">
|
||||
累计
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="container-body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Container',
|
||||
components: {},
|
||||
props: {
|
||||
name: { type: String, required: true },
|
||||
nameTwo: { type: String, required: false },
|
||||
size: { type: String, default: 'default' },
|
||||
icon: { type: String, default: '' },
|
||||
iconTwo: { type: String, default: '' }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 初始状态:左侧不透明(1),右侧透明(0.3)
|
||||
isLeftTransparent: true, // 左侧透明度状态(true=1,false=0.3)
|
||||
isRightTransparent: false, // 右侧透明度状态(true=1,false=0.3)
|
||||
activeTab: 'month', // 默认激活的Tab为月度
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleTabClick(tabType) {
|
||||
this.activeTab = tabType;
|
||||
// 向父组件派发Tab切换事件,传递当前选中的Tab类型
|
||||
this.$emit('tabChange', tabType);
|
||||
// 可选:同时传递更详细的信息(如标签名)
|
||||
// this.$emit('tabChange', { type: tabType, name: tabType === 'month' ? '月度' : '累计' });
|
||||
},
|
||||
// 点击左侧标题:左侧保持不透明,右侧变透明,并派发采购标签事件
|
||||
handleLeftClick() {
|
||||
this.isLeftTransparent = true; // 左侧不透明
|
||||
this.isRightTransparent = false; // 右侧透明
|
||||
this.$emit('switchTab', 'product'); // 通知父组件切换到采购内容
|
||||
},
|
||||
// 点击右侧标题:右侧保持不透明,左侧变透明,并派发存货标签事件
|
||||
handleRightClick() {
|
||||
this.isLeftTransparent = false; // 左侧透明
|
||||
this.isRightTransparent = true; // 右侧不透明
|
||||
this.$emit('switchTab', 'heat'); // 通知父组件切换到存货内容
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 样式保持不变,确保透明度过渡生效
|
||||
.cockpitContainer {
|
||||
display: inline-block;
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
.container-top {
|
||||
position: relative;
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content-top-left {
|
||||
width: 566px;
|
||||
height: 60px;
|
||||
background: linear-gradient(90deg, #FFFFFF 0%, rgba(253, 255, 255, 0) 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
transition: opacity 0.3s ease; // 透明度过渡动画
|
||||
}
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// margin-left: 10px;
|
||||
/* 垂直居中关键属性 */
|
||||
height: 100%;
|
||||
/* 继承父容器高度,确保垂直居中范围 */
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 30px;
|
||||
margin-right: 12px;
|
||||
margin-top: 4px;
|
||||
margin-left: 10px;
|
||||
/* 图标和文字之间的间距 */
|
||||
flex-shrink: 0;
|
||||
/* 防止图标被压缩 */
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 24px;
|
||||
color: #000000;
|
||||
letter-spacing: 3px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
// 移除固定行高,避免影响垂直对齐
|
||||
// line-height: 60px;
|
||||
}
|
||||
/* 左侧标题 - 左上角折现边框 */
|
||||
.content-top-left::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid;
|
||||
border-image: linear-gradient(277deg, rgba(255, 255, 255, 0), rgba(92, 140, 255, 1)) 1 1;
|
||||
clip-path: polygon(20px 0, 100% 0, 100% 100%, 0 100%, 0 20px);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
/* 左侧标题 - 左上角折现细节 */
|
||||
.content-top-left::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: #E1f0fd;
|
||||
border-top: 1px solid rgba(92, 140, 255, 1);
|
||||
border-left: 1px solid rgba(92, 140, 255, 1);
|
||||
transform: rotate(135deg) translate(-50%, -50%);
|
||||
transform-origin: top left;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.content-top-right {
|
||||
width: 368px;
|
||||
height: 60px;
|
||||
background: linear-gradient(90deg, #FFFFFF 0%, rgba(253, 255, 255, 0) 100%);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 240px;
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.3s ease; // 透明度过渡动画
|
||||
}
|
||||
|
||||
/* 右侧标题 - 左上角折现边框 */
|
||||
.content-top-right::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid;
|
||||
border-image: linear-gradient(277deg, rgba(255, 255, 255, 0), rgba(92, 140, 255, 1)) 1 1;
|
||||
clip-path: polygon(20px 0, 100% 0, 100% 100%, 0 100%, 0 20px);
|
||||
z-index: 12;
|
||||
}
|
||||
|
||||
/* 右侧标题 - 左上角折现细节 */
|
||||
.content-top-right::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: #E1f0fd;
|
||||
border-top: 1px solid rgba(92, 140, 255, 1);
|
||||
border-left: 1px solid rgba(92, 140, 255, 1);
|
||||
transform: rotate(135deg) translate(-50%, -50%);
|
||||
transform-origin: top left;
|
||||
z-index: 12;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
margin-left: 6px;
|
||||
height: 32px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 24px;
|
||||
color: #000000;
|
||||
// line-height: 60px;
|
||||
letter-spacing: 3px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__topBasic {
|
||||
background: url(../../../assets/img/top-basic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__bottomBasic {
|
||||
background: url(../../../assets/img/bottom-basic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.container-body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.test-body {
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
}
|
||||
.change-text{
|
||||
position: absolute;
|
||||
top: 26px;
|
||||
left: 300px;
|
||||
z-index: 999;
|
||||
// width: 48px;
|
||||
// height: 17px;
|
||||
// margin-left: 80px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #0B58FF;
|
||||
line-height: 17px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
}
|
||||
.change-text-right {
|
||||
// position: absolute;
|
||||
// top: 26px;
|
||||
left: auto; // 清除左侧定位
|
||||
right: 30px;
|
||||
// z-index: 999;
|
||||
// // width: 48px;
|
||||
// // height: 17px;
|
||||
// // margin-left: 80px;
|
||||
// font-family: PingFangSC, PingFang SC;
|
||||
// font-weight: 400;
|
||||
// font-size: 12px;
|
||||
// color: #0B58FF;
|
||||
// line-height: 17px;
|
||||
// text-align: left;
|
||||
// font-style: normal;
|
||||
}
|
||||
.tab-group {
|
||||
display: inline-flex;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top:20px;
|
||||
z-index: 9999;
|
||||
align-items: center;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
gap: 8px; // Tab之间的间距
|
||||
}
|
||||
|
||||
// Tab基础样式(统一)
|
||||
.tab-item {
|
||||
padding: 0 24px;
|
||||
width: 79px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s ease; // 样式切换动画
|
||||
}
|
||||
|
||||
// 未激活的Tab样式(原first-child样式)
|
||||
.tab-item:not(.active) {
|
||||
background: #ECF4FE;
|
||||
color: #0B58FF;
|
||||
}
|
||||
|
||||
// 激活的Tab样式(原last-child样式)
|
||||
.tab-item.active {
|
||||
background: #3071FF;
|
||||
color: #F9FCFF;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -1,33 +1,49 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container name="生产重点指标" icon="cockpitItemIcon" size="topBasic" topSize="basic">
|
||||
<Container name="生产重点指标" nameTwo="热耗" icon="cockpitItemIcon" size="topBasic" topSize="basic" @switchTab="handleTabSwitch" @tabChange='tabChange'>
|
||||
<!-- 1. 移除 .kpi-content 的固定高度,改为自适应 -->
|
||||
<div class="kpi-content" style="padding: 14px 16px; display: flex;flex-direction: column; width: 100%;">
|
||||
<!-- 2. .top 保持 flex,无需固定高度,自动跟随子元素拉伸 -->
|
||||
<div class="top" style="display: flex; width: 100%;">
|
||||
<top-item :rawItemList='productData' :dateData="dateData" />
|
||||
</div>
|
||||
<div class="bottom" style="display: flex;margin-top: 8px;background-color: rgba(249, 252, 255, 1);">
|
||||
<!-- <top-item /> -->
|
||||
<coreBottomBar :lineData="productData.line" :dateData="dateData" />
|
||||
|
||||
</div>
|
||||
<template v-if="activeTab === 'product'">
|
||||
<div class="top" style="display: flex; width: 100%;">
|
||||
<top-item :rawItemList='productData' :dateData="dateData" />
|
||||
</div>
|
||||
<div class="bottom" style="display: flex;margin-top: 8px;background-color: rgba(249, 252, 255, 1);">
|
||||
<!-- <top-item /> -->
|
||||
<coreBottomBar :lineData="product.line" :dateData="dateData" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'heat'">
|
||||
<div class="bottom" style="background-color: rgba(249, 252, 255, 1);">
|
||||
<div class="bottom" style="margin-top: 8px;background-color: rgba(249, 252, 255, 1);">
|
||||
<div style='text-align: center;margin: 5px;font-size: 18px;font-weight: 400;color: #000;'>1200t/d</div>
|
||||
<heatBar :lineData="heatData['1200t']" :xData="['桐城','洛阳','江苏','秦皇岛']" :dateData="dateData" />
|
||||
</div>
|
||||
<div style='text-align: center;margin: 5px;font-size: 18px;font-weight: 400;color: #000;'>650t/d</div>
|
||||
<heatBar :lineData="heatData['650t']" :xData="['宜兴','自贡','漳州']" :dateData="dateData" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Container from './container.vue'
|
||||
import Container from './keyProductContainer.vue'
|
||||
// import * as echarts from 'echarts'
|
||||
import topItem from './top-product-item.vue'
|
||||
import coreBottomBar from './productBottomBar.vue'
|
||||
import heatBar from './heatBarChart.vue'
|
||||
|
||||
export default {
|
||||
name: 'ProductionStatus',
|
||||
components: { Container, topItem, coreBottomBar },
|
||||
components: { Container, topItem, coreBottomBar, heatBar },
|
||||
// mixins: [resize],
|
||||
props: {
|
||||
productData: {
|
||||
product: {
|
||||
type: Object,
|
||||
default: () => {} // 默认空数组,避免报错
|
||||
},
|
||||
heat: {
|
||||
type: Object,
|
||||
default: () => {} // 默认空数组,避免报错
|
||||
},
|
||||
@@ -35,13 +51,45 @@ export default {
|
||||
type: Object,
|
||||
default: () => { } // 默认空数组,避免报错
|
||||
},
|
||||
heat: {
|
||||
type: Object,
|
||||
default: () => { }
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null
|
||||
chart: null,
|
||||
activeTab: 'product',
|
||||
currentTap: 'month',
|
||||
productData:{},
|
||||
heatData:{}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 切换标签时更新图表
|
||||
activeTab(newVal) {
|
||||
// this.$nextTick(() => this.updateChart())
|
||||
},
|
||||
product: {
|
||||
handler() {
|
||||
if(this.currentTap === 'month') {
|
||||
this.productData = this.product.mon
|
||||
}else{
|
||||
this.productData = this.product.total
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
heat: {
|
||||
handler() {
|
||||
if(this.currentTap === 'month') {
|
||||
this.heatData = this.heat.mon
|
||||
}else{
|
||||
this.heatData = this.heat.total
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// 初始化图表(若需展示图表,需在模板中添加对应 DOM)
|
||||
@@ -49,6 +97,22 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 处理标题点击切换标签
|
||||
handleTabSwitch(tabType) {
|
||||
this.activeTab = tabType // tabType由Container组件传递:'purchase'或'inventory'
|
||||
this.showChart = true // 切换时默认显示图表(可根据需求调整)
|
||||
},
|
||||
tabChange(val) {
|
||||
if(val === 'month') {
|
||||
this.currentTap = 'month'
|
||||
this.productData = this.product.mon
|
||||
this.heatData = this.heat.mon
|
||||
}else{
|
||||
this.currentTap = 'total'
|
||||
this.productData = this.product.total
|
||||
this.heatData = this.heat.total
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
246
src/views/home/depreciationAnalysis/depreciationAnalysis.vue
Normal file
246
src/views/home/depreciationAnalysis/depreciationAnalysis.vue
Normal file
@@ -0,0 +1,246 @@
|
||||
<template>
|
||||
<div id="dayReport" class="dayReport" :style="styles">
|
||||
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
|
||||
<sidebar v-if="!sidebar.hide" class="sidebar-container" />
|
||||
<ReportHeader :dateData="dateData" top-title="应收账款" :is-full-screen="isFullScreen"
|
||||
@screenfullChange="screenfullChange" @timeRangeChange="handleTimeChange" />
|
||||
<div class="main-body" style="
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding: 0px 16px 0 272px;
|
||||
flex-direction: column;
|
||||
">
|
||||
<div class="top" style="margin-top: -20px; display: flex; gap: 16px">
|
||||
<div class="top-three" style="
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns:1624px;
|
||||
">
|
||||
<operatingLineChart :dateData="dateData" :monthData="monthData" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="top" style="display: flex; gap: 16px;margin-top: 6px;">
|
||||
<div class="left-three" style="
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 1624px;
|
||||
">
|
||||
<operatingLineChartCumulative :dateData="dateData" :ytdData="ytdData" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import ReportHeader from "../components/noRouterHeader.vue";
|
||||
import { Sidebar } from "../../../layout/components";
|
||||
import screenfull from "screenfull";
|
||||
import { mapState } from "vuex";
|
||||
import operatingLineChart from "../depreciationAnalysisComponents/operatingLineChart";
|
||||
import operatingLineChartCumulative from "../depreciationAnalysisComponents/operatingLineChartCumulative.vue";
|
||||
|
||||
import { getSalesRevenueGroupData } from '@/api/cockpit'
|
||||
export default {
|
||||
name: "DepreciationAnalysis",
|
||||
components: {
|
||||
ReportHeader,
|
||||
operatingLineChartCumulative,
|
||||
operatingLineChart,
|
||||
Sidebar,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
weekArr: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
|
||||
isFullScreen: false,
|
||||
timer: null,
|
||||
beilv: 1,
|
||||
value: 100,
|
||||
sort:1,
|
||||
selectDate:{},
|
||||
monthData: {},
|
||||
ytdData:{},
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.init();
|
||||
this.windowWidth(document.documentElement.clientWidth);
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
theme: (state) => state.settings.theme,
|
||||
sideTheme: (state) => state.settings.sideTheme,
|
||||
sidebar: (state) => state.app.sidebar,
|
||||
device: (state) => state.app.device,
|
||||
needTagsView: (state) => state.settings.tagsView,
|
||||
fixedHeader: (state) => state.settings.fixedHeader,
|
||||
}),
|
||||
classObj() {
|
||||
return {
|
||||
hideSidebar: !this.sidebar.opened,
|
||||
openSidebar: this.sidebar.opened,
|
||||
withoutAnimation: this.sidebar.withoutAnimation,
|
||||
mobile: this.device === "mobile",
|
||||
};
|
||||
},
|
||||
variables() {
|
||||
return variables;
|
||||
},
|
||||
// ...mapGetters(['sidebar']),
|
||||
styles() {
|
||||
const v = Math.floor(this.value * this.beilv * 100) / 10000;
|
||||
return {
|
||||
transform: `scale(${v})`,
|
||||
transformOrigin: "left top",
|
||||
// overflow: hidden;
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
clientWidth(val) {
|
||||
if (!this.timer) {
|
||||
this.clientWidth = val;
|
||||
this.beilv2 = this.clientWidth / 1920;
|
||||
this.timer = true;
|
||||
let _this = this;
|
||||
setTimeout(function () {
|
||||
_this.timer = false;
|
||||
}, 500);
|
||||
}
|
||||
// 这里可以添加修改时的方法
|
||||
this.windowWidth(val);
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.timer);
|
||||
this.destroy();
|
||||
},
|
||||
mounted() {
|
||||
const _this = this;
|
||||
_this.beilv = document.documentElement.clientWidth / 1920;
|
||||
window.onresize = () => {
|
||||
return (() => {
|
||||
_this.clientWidth = `${document.documentElement.clientWidth}`;
|
||||
this.beilv = _this.clientWidth / 1920;
|
||||
})();
|
||||
};
|
||||
this.dateData = this.$route.query.dateData ? this.$route.query.dateData : undefined
|
||||
},
|
||||
methods: {
|
||||
// sortChange(value) {
|
||||
// this.sort = value
|
||||
// this.getData()
|
||||
// },
|
||||
getData() {
|
||||
getSalesRevenueGroupData({
|
||||
startTime: this.dateData.startTime,
|
||||
endTime: this.dateData.endTime,
|
||||
sort: this.sort,
|
||||
index: undefined,
|
||||
factory: undefined
|
||||
// timeDim: obj.mode
|
||||
}).then((res) => {
|
||||
console.log(res);
|
||||
this.monthData= res.data.month
|
||||
this.ytdData = res.data.ytd
|
||||
|
||||
// this.saleData = res.data.SaleData
|
||||
// this.premiumProduct = res.data.premiumProduct
|
||||
// this.salesTrendMap = res.data.salesTrendMap
|
||||
// this.grossMarginTrendMap = res.data.grossMarginTrendMap
|
||||
// this.salesProportion = res.data.salesProportion ? res.data.salesProportion : {}
|
||||
})
|
||||
},
|
||||
handleTimeChange(obj) {
|
||||
console.log(obj, 'obj');
|
||||
this.dateData= obj
|
||||
this.getData()
|
||||
},
|
||||
handleClickOutside() {
|
||||
this.$store.dispatch("app/closeSideBar", { withoutAnimation: false });
|
||||
},
|
||||
windowWidth(value) {
|
||||
this.clientWidth = value;
|
||||
this.beilv2 = this.clientWidth / 1920;
|
||||
},
|
||||
change() {
|
||||
this.isFullScreen = screenfull.isFullscreen;
|
||||
},
|
||||
init() {
|
||||
if (!screenfull.isEnabled) {
|
||||
this.$message({
|
||||
message: "you browser can not work",
|
||||
type: "warning",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
screenfull.on("change", this.change);
|
||||
},
|
||||
destroy() {
|
||||
if (!screenfull.isEnabled) {
|
||||
this.$message({
|
||||
message: "you browser can not work",
|
||||
type: "warning",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
screenfull.off("change", this.change);
|
||||
},
|
||||
// 全屏
|
||||
screenfullChange() {
|
||||
console.log("screenfull.enabled", screenfull.isEnabled);
|
||||
|
||||
if (!screenfull.isEnabled) {
|
||||
this.$message({
|
||||
message: "you browser can not work",
|
||||
type: "warning",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
screenfull.toggle(this.$refs.dayReportB);
|
||||
},
|
||||
// 导出
|
||||
// exportPDF() {
|
||||
// this.$message.success('正在导出,请稍等!')
|
||||
// const element = document.getElementById('dayRepDom')
|
||||
// element.style.display = 'block'
|
||||
// const fileName = '株洲碲化镉生产日报' + moment().format('yyMMDD') + '.pdf'
|
||||
// html2canvas(element, {
|
||||
// dpi: 300, // Set to 300 DPI
|
||||
// scale: 3 // Adjusts your resolution
|
||||
// }).then(function(canvas) {
|
||||
// const imgWidth = 595.28
|
||||
// const imgHeight = 841.89
|
||||
// const pageData = canvas.toDataURL('image/jpeg', 1.0)
|
||||
// const PDF = new JsPDF('', 'pt', [imgWidth, imgHeight])
|
||||
// PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight)
|
||||
// setTimeout(() => {
|
||||
// PDF.save(fileName) // 导出文件名
|
||||
// }, 1000)
|
||||
// })
|
||||
// element.style.display = 'none'
|
||||
// }
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "~@/assets/styles/mixin.scss";
|
||||
@import "~@/assets/styles/variables.scss";
|
||||
.dayReport {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: url("../../../assets/img/backp.png") no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
.hideSidebar .fixed-header {
|
||||
width: calc(100% - 54px);
|
||||
}
|
||||
|
||||
.sidebarHide .fixed-header {
|
||||
width: calc(100%);
|
||||
}
|
||||
|
||||
.mobile .fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
419
src/views/home/depreciationAnalysisComponents/Header.vue
Normal file
419
src/views/home/depreciationAnalysisComponents/Header.vue
Normal file
@@ -0,0 +1,419 @@
|
||||
<template>
|
||||
<header class="report-header">
|
||||
<!-- 左侧区域:logo + 标题 -->
|
||||
<div class="left-content">
|
||||
<img style="height: 36px;" src="../../../assets/img/cnbm.png" alt="benmaLogo" >
|
||||
<div class="top-title">{{ topTitle }}</div>
|
||||
</div>
|
||||
|
||||
<div class="center-content">
|
||||
<!-- 循环 pageRoutes,不再硬编码文字 -->
|
||||
<div class="item" v-for="(page, index) in pageRoutes" :key="index" @click="goToPage(page.path, index)">
|
||||
<span class="item-text">{{ page.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- :class="{ 'no-skew': activeIndex === index }
|
||||
" -->
|
||||
<!-- 右侧区域:全屏按钮 -->
|
||||
<div class="right-content">
|
||||
<el-button type="text" class="screen-btn" :title="isFullScreen ? '退出全屏' : '全屏'" @click="changeFullScreen">
|
||||
<svg-icon style="color: #0B58FF;" v-if="isFullScreen" icon-class="unFullScreenView" />
|
||||
<svg-icon style="color: #0B58FF;" v-else icon-class="fullScreenView" />
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 时间选择区域:日/月/年按钮 + label + 日期选择器 -->
|
||||
<div class="timeType">
|
||||
<!-- <div class="item" v-for="(item, index) in timeTypes" :key="index" @click="activeTime = index"
|
||||
:class="{ 'no-skew': activeTime === index }">
|
||||
<span class="item-text">{{ item.text }}</span>
|
||||
</div> -->
|
||||
<div class="dateP">
|
||||
<div class="label">
|
||||
<span class="label-text">月份选择</span>
|
||||
</div>
|
||||
<el-date-picker v-model="date" :type="getPickerType" :placeholder="getPickerPlaceholder"
|
||||
class="custom-date-picker" style="width: 132px;height: 29px;" @change="emitTimeRange" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment'
|
||||
export default {
|
||||
name: 'Header',
|
||||
props: {
|
||||
isFullScreen: { type: Boolean, default: false },
|
||||
topTitle: { type: String, default: '' }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentTime: '',
|
||||
timeTimer: null,
|
||||
date: undefined,
|
||||
activeIndex: -1,
|
||||
activeTime: 1, // 0=日,1=月,2=年(默认选中“日”)
|
||||
pageRoutes: [
|
||||
{ text: '营业收入', path: '/operatingRevenue/operatingRevenueIndex' },
|
||||
{ text: '利润分析', path: '/profitAnalysis' },
|
||||
{ text: '产销率库存分析', path: '/PSIAnal' },
|
||||
{ text: '成本分析', path: '/cost/cost' },
|
||||
{ text: '驾驶舱报表', path: '/cockpit' }
|
||||
],
|
||||
// 定义时间类型配置:text=按钮文字,pickerType=选择器类型,placeholder=占位符
|
||||
timeTypes: [
|
||||
{ text: '日', pickerType: 'date', placeholder: '选择日期' },
|
||||
{ text: '月', pickerType: 'month', placeholder: '选择月份' },
|
||||
{ text: '年', pickerType: 'year', placeholder: '选择年份' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 动态获取日期选择器类型
|
||||
getPickerType() {
|
||||
return this.timeTypes[this.activeTime].pickerType;
|
||||
},
|
||||
// 动态获取日期选择器占位符
|
||||
getPickerPlaceholder() {
|
||||
return this.timeTypes[this.activeTime].placeholder;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToPage(path, index) {
|
||||
// 1. 跳转到对应路由
|
||||
this.$router.push(path);
|
||||
// 2. 更新activeIndex,让当前点击项高亮
|
||||
this.activeIndex = index;
|
||||
},
|
||||
changeFullScreen() { this.$emit('screenfullChange') },
|
||||
padZero(num) { return num < 10 ? '0' + num : num },
|
||||
/**
|
||||
* 核心方法1:根据维度计算时间区间(首次进入时基于赋值的当月日期,计算“当月第一天0点→次月第一天0点”)
|
||||
* @returns {Object} 包含 start(开始时间)、end(结束时间)、dimension(维度)的区间对象
|
||||
*/
|
||||
calculateTimeRange() {
|
||||
let startTime = 0;
|
||||
let endTime = 0;
|
||||
const mode = this.activeTime + 1; // 1=日,2=月,3=年
|
||||
const defaultMoment = moment(); // 默认当前时间
|
||||
|
||||
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. 日维度:00:00:00 → 23:59:59(无毫秒)
|
||||
if (this.activeTime === 0) {
|
||||
startTime = targetMoment.startOf('day').millisecond(0).valueOf();
|
||||
endTime = targetMoment.endOf('day').millisecond(0).valueOf();
|
||||
}
|
||||
|
||||
// 2. 月维度:当月1日00:00:00 → 当月最后一天23:59:59(无毫秒)
|
||||
else if (this.activeTime === 1) {
|
||||
startTime = targetMoment.startOf('month').millisecond(0).valueOf();
|
||||
endTime = targetMoment.endOf('month').millisecond(0).valueOf();
|
||||
}
|
||||
|
||||
// 3. 年维度:当年1月1日00:00:00 → 当年最后一天23:59:59(无毫秒)
|
||||
else if (this.activeTime === 2) {
|
||||
startTime = targetMoment.startOf('year').millisecond(0).valueOf();
|
||||
endTime = targetMoment.endOf('year').millisecond(0).valueOf();
|
||||
}
|
||||
|
||||
// 调试输出:验证是否去掉毫秒
|
||||
console.log('时间范围计算结果:', {
|
||||
mode,
|
||||
startTime: moment(startTime * 1000).format('YYYY-MM-DD HH:mm:ss'), // 格式:2025-11-30 00:00:00
|
||||
endTime: moment(endTime * 1000).format('YYYY-MM-DD HH:mm:ss'), // 格式:2025-11-30 23:59:59(无毫秒)
|
||||
startTimeStamp: startTime, // 秒级时间戳(如:1764422400)
|
||||
endTimeStamp: endTime // 秒级时间戳(如:1764508799)
|
||||
});
|
||||
|
||||
return { startTime, endTime, mode };
|
||||
},
|
||||
/**
|
||||
* 核心方法2:传递时间区间给父组件(首次进入时触发,传递“当月第一天0点→次月第一天0点”)
|
||||
*/
|
||||
emitTimeRange() {
|
||||
const timeRange = this.calculateTimeRange();
|
||||
this.$emit('timeRangeChange', timeRange);
|
||||
// 调试用:查看首次传递的区间(如{start: "2025-10-01T00:00:00", end: "2025-11-01T00:00:00", dimension: "月"})
|
||||
console.log('当前时间区间:', timeRange);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 维度切换时:清空选择的日期,并传递当前维度的默认区间
|
||||
activeTime(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.date = undefined;
|
||||
// this.emitTimeRange();
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 核心逻辑:首次进入页面,计算当月默认日期并赋值给选择器,同时传递区间
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = this.padZero(now.getMonth() + 1); // 月份从0开始,+1后补零(如1月→01)
|
||||
// 赋值当月默认日期(格式:YYYY-MM,适配month类型选择器)
|
||||
this.date = `${year}-${month}`;
|
||||
// 确保选择器渲染完成后,传递“当月第一天0点→次月第一天0点”的区间
|
||||
this.$nextTick(() => this.emitTimeRange());
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 原有样式不变,仅补充label文字的倾斜抵消样式 */
|
||||
@font-face {
|
||||
font-family: "YouSheBiaoTiHei";
|
||||
src: url('../../../assets/fonts/YouSheBiaoTiHe.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.report-header {
|
||||
height: 117px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
background: url('../../../assets/img/topTitle.png') no-repeat;
|
||||
background-size: cover;
|
||||
background-position: 0 0;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
/* 确保timeType绝对定位生效 */
|
||||
|
||||
.left-content {
|
||||
margin-top: 11px;
|
||||
margin-left: 44px;
|
||||
height: 55px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.top-title {
|
||||
height: 55px;
|
||||
font-family: "YouSheBiaoTiHei", sans-serif;
|
||||
font-size: 42px;
|
||||
color: #1E1651;
|
||||
line-height: 55px;
|
||||
letter-spacing: 6px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.center-content {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 18px;
|
||||
margin-left: 70px;
|
||||
|
||||
.item {
|
||||
width: 180px;
|
||||
height: 50px;
|
||||
background: #E1EEFC;
|
||||
transform: skew(-20deg);
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
color: #1E1651;
|
||||
line-height: 50px;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
box-shadow: 0px 13px 16px 0px rgba(179, 217, 255, 0.43),
|
||||
0px 2px 4px 0px rgba(92, 140, 255, 0.25),
|
||||
inset 0px -43px 13px 0px rgba(255, 255, 255, 0.51);
|
||||
|
||||
.item-text {
|
||||
display: inline-block;
|
||||
transform: skew(20deg);
|
||||
}
|
||||
}
|
||||
|
||||
.item.no-skew {
|
||||
background: none !important;
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
color: #1E1651;
|
||||
|
||||
.item-text {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.timeType {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* 垂直居中,避免元素高低错位 */
|
||||
top: 42px;
|
||||
right:10px;
|
||||
margin-top: 18px;
|
||||
gap: 0;
|
||||
/* 清除间隙,让按钮与选择器紧密连接 */
|
||||
}
|
||||
|
||||
.timeType .item {
|
||||
width: 40px;
|
||||
height: 28px;
|
||||
background: rgba(236, 244, 254, 1);
|
||||
transform: skew(-20deg);
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: rgba(11, 88, 255, 1);
|
||||
line-height: 28px;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
/* 选中按钮与未选中按钮倾斜角度统一,避免切换时跳动 */
|
||||
}
|
||||
|
||||
.timeType .item .item-text {
|
||||
display: inline-block;
|
||||
transform: skew(20deg);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.timeType .item.no-skew {
|
||||
background: rgba(11, 88, 255, 1);
|
||||
color: rgba(249, 252, 255, 1);
|
||||
transform: skew(-20deg) !important;
|
||||
/* 统一倾斜角度,修复原30deg的错位 */
|
||||
box-shadow: 0 2px 8px rgba(11, 88, 255, 0.3);
|
||||
}
|
||||
|
||||
.timeType .item.no-skew .item-text {
|
||||
transform: skew(20deg) !important;
|
||||
/* 同步统一文字倾斜角度 */
|
||||
}
|
||||
|
||||
.dateP {
|
||||
position: relative;
|
||||
margin-left: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.dateP .label {
|
||||
width: 165px;
|
||||
height: 28px;
|
||||
background: rgba(236, 244, 254, 1);
|
||||
transform: skew(-25deg);
|
||||
/* 与按钮倾斜角度统一(原30deg改为25deg,避免视觉错位) */
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #0B58FF;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 补充:label文字抵消倾斜(原代码遗漏,导致文字倾斜) */
|
||||
.dateP .label-text {
|
||||
display: inline-block;
|
||||
transform: skew(25deg);
|
||||
/* 与label倾斜角度相反,确保文字正立 */
|
||||
}
|
||||
|
||||
.right-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 12px;
|
||||
margin-right: 4px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.current-time {
|
||||
color: #FFFFFF;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 500;
|
||||
font-size: 22px;
|
||||
line-height: 24px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.screen-btn {
|
||||
width: 26px;
|
||||
margin-left: 300px;
|
||||
color: #00fff0;
|
||||
font-size: 26px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 日期选择器样式保持不变 */
|
||||
::v-deep .custom-date-picker {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
width: 165px !important;
|
||||
height: 28px !important;
|
||||
position: relative;
|
||||
margin: 0 !important;
|
||||
|
||||
/* 1. 调整输入框文字:确保行高与输入框高度一致,垂直居中 */
|
||||
.el-input__inner {
|
||||
height: 28px !important;
|
||||
width: 165px !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>
|
||||
282
src/views/home/depreciationAnalysisComponents/container.vue
Normal file
282
src/views/home/depreciationAnalysisComponents/container.vue
Normal file
@@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<div class="cockpitContainer" :class="['cockpitContainer__' + size]">
|
||||
<div class="content-top" :class="['content-top__' + topSize]">
|
||||
<!-- 使用 flex 容器包裹图标和文字,实现垂直居中 -->
|
||||
<div class="title-wrapper">
|
||||
<svg-icon class="title-icon" :icon-class="icon" />
|
||||
<span class="title-text">
|
||||
{{ name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cockpitContainer-body">
|
||||
<slot>
|
||||
<div class="test-body">something test....</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Container',
|
||||
components: {},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['name', 'size', 'icon', 'topSize'],
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.cockpitContainer {
|
||||
display: inline-block;
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
.content-top {
|
||||
height: 60px;
|
||||
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
/* 垂直居中关键属性 */
|
||||
height: 100%;
|
||||
/* 继承父容器高度,确保垂直居中范围 */
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 30px;
|
||||
margin-right: 12px;
|
||||
margin-top: 4px;
|
||||
/* 图标和文字之间的间距 */
|
||||
flex-shrink: 0;
|
||||
/* 防止图标被压缩 */
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 24px;
|
||||
color: #000000;
|
||||
letter-spacing: 3px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
// 移除固定行高,避免影响垂直对齐
|
||||
// line-height: 60px;
|
||||
}
|
||||
|
||||
// width: 547px;
|
||||
// background: url(../../../assets/img/contentTopBasic.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
&__basic {
|
||||
// width: 547px;
|
||||
background: url(../../../assets/img/contentTopBasic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__middle {
|
||||
background: url(../../../assets/img/topTileMiddle.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__large {
|
||||
background: url(../../../assets/img/topTitleLargeBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__KFAPTopTitle {
|
||||
background: url(../../../assets/img/KFAPTopTitle.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__psiTopTitleBasic {
|
||||
background: url(../../../assets/img/psiTopTitleBasic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__rawTopTitleLarge {
|
||||
background: url(../../../assets/img/rawTopTitleLarge.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&__topBasic {
|
||||
background: url(../../../assets/img/top-basic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__bottomBasic {
|
||||
background: url(../../../assets/img/bottom-basic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__operatingBasic {
|
||||
background: url(../../../assets/img/operating-basic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__operatingLarge {
|
||||
background: url(../../../assets/img/operating-large.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__profitTopBasic {
|
||||
background: url(../../../assets/img/profitTopBasic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__profitMiddleBasic {
|
||||
background: url(../../../assets/img/profitMiddleBasic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__psiBasicBg {
|
||||
background: url(../../../assets/img/psiBasicBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__psiMiddleBg {
|
||||
background: url(../../../assets/img/psiMiddleBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__operatingRevenueBg {
|
||||
background: url(../../../assets/img/operatingRevenueBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__costBasicBg {
|
||||
background: url(../../../assets/img/costBasicBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__rawTopBg {
|
||||
background: url(../../../assets/img/rawTopBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__opLargeBg {
|
||||
background: url(../../../assets/img/opLargeBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
// &__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>
|
||||
298
src/views/home/depreciationAnalysisComponents/dataTrend.vue
Normal file
298
src/views/home/depreciationAnalysisComponents/dataTrend.vue
Normal file
@@ -0,0 +1,298 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container name="数据趋势" icon="cockpitItemIcon" size="opLargeBg" topSize="large">
|
||||
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%; gap: 16px">
|
||||
<div class="right" style="
|
||||
height: 191px;
|
||||
display: flex;
|
||||
width: 1595px;
|
||||
background-color: rgba(249, 252, 255, 1);
|
||||
">
|
||||
<dataTrendBar @changeItem="handleChange" :chartData="chartData" />
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Container from "../components/container.vue";
|
||||
import dataTrendBar from "./dataTrendBar.vue";
|
||||
|
||||
export default {
|
||||
name: "ProductionStatus",
|
||||
components: { Container, dataTrendBar },
|
||||
props: {
|
||||
trend: {
|
||||
type: Array,
|
||||
// 默认值与实际数据结构一致(12个月)
|
||||
default: () => [
|
||||
// { title: "2025年01月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年02月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年03月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年04月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年05月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年06月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年07月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年08月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年09月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年10月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年11月", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
// { title: "2025年12月", budget: 0, real: 0, rate: 0, diff: 0 }
|
||||
]
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chartData: {
|
||||
months: [], // 月份数组(2025年01月 - 2025年12月)
|
||||
rates: [], // 每月完成率(百分比)
|
||||
reals: [], // 每月实际值
|
||||
budgets: [],// 每月预算值
|
||||
diffs: [], // 每月差值
|
||||
flags: [] // 每月达标标识(≥100 → 1,<100 → 0)
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
trend: {
|
||||
handler(newVal) {
|
||||
this.processTrendData(newVal);
|
||||
},
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.processTrendData(this.trend);
|
||||
},
|
||||
methods: {
|
||||
handleChange(value) {
|
||||
this.$emit("handleChange", value);
|
||||
},
|
||||
/**
|
||||
* 处理趋势数据(适配12个月的数组结构)
|
||||
* @param {Array} trendData - 原始趋势数组(12个月)
|
||||
*/
|
||||
processTrendData(trendData) {
|
||||
// 数据兜底:确保是数组且长度为12
|
||||
const validTrend = Array.isArray(trendData)
|
||||
? trendData
|
||||
: []
|
||||
|
||||
// 初始化空数组
|
||||
const months = [];
|
||||
const rates = [];
|
||||
const reals = [];
|
||||
const budgets = [];
|
||||
const diffs = [];
|
||||
const flags = [];
|
||||
|
||||
// 遍历12个月数据
|
||||
validTrend.forEach(item => {
|
||||
// 基础数据提取(兜底处理)
|
||||
const month = item.title ?? '';
|
||||
const budget = Number(item.budget) || 0;
|
||||
const real = Number(item.real) || 0;
|
||||
const rate = Number(item.rate) || 0;
|
||||
const diff = Number(item.diff) || 0;
|
||||
|
||||
// 计算达标标识(≥100 → 1,<100 → 0)
|
||||
const flag = this.getRateFlag(rate, real, budget);
|
||||
|
||||
// 填充数组
|
||||
months.push(month);
|
||||
rates.push(rate); // 转为百分比并取整
|
||||
reals.push(real);
|
||||
budgets.push(budget);
|
||||
diffs.push(diff);
|
||||
flags.push(flag);
|
||||
});
|
||||
|
||||
// 更新chartData(响应式)
|
||||
this.chartData = {
|
||||
months,
|
||||
rates,
|
||||
reals,
|
||||
budgets,
|
||||
diffs,
|
||||
flags
|
||||
};
|
||||
|
||||
console.log('处理后的趋势数据:', this.chartData);
|
||||
},
|
||||
|
||||
/**
|
||||
* 计算达标标识
|
||||
* @param {Number} rate - 完成率(原始值,如1.2 → 120%)
|
||||
* @returns {Number} 1: 达标(≥100%),0: 未达标(<100%)
|
||||
*/
|
||||
getRateFlag(rate, real, target) {
|
||||
if (isNaN(rate) || rate === null || rate === undefined) return 0;
|
||||
|
||||
// 1. 完成率 >= 100 => 达标
|
||||
if (rate >= 100) return 1;
|
||||
|
||||
// 2. 完成率 = 0 且 (目标值=0 或 实际值=目标值=0) => 达标
|
||||
if (rate === 0 && target === 0) return 1;
|
||||
|
||||
// 其他情况 => 未达标
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
};
|
||||
</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>
|
||||
/* 全局 tooltip 样式 */
|
||||
.production-status-chart-tooltip {
|
||||
background: #0a2b4f77 !important;
|
||||
border: none !important;
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.production-status-chart-tooltip * {
|
||||
color: #fff !important;
|
||||
}
|
||||
</style>
|
||||
477
src/views/home/depreciationAnalysisComponents/dataTrendBar.vue
Normal file
477
src/views/home/depreciationAnalysisComponents/dataTrendBar.vue
Normal file
@@ -0,0 +1,477 @@
|
||||
<template>
|
||||
<div class="coreBar">
|
||||
<!-- 新增行容器:包裹“各基地情况”和barTop -->
|
||||
<div class="header-row">
|
||||
<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">
|
||||
<span class="legend-icon square achieved"></span>
|
||||
实际·达标
|
||||
</span>
|
||||
<span class="legend-item">
|
||||
<span class="legend-icon square unachieved"></span>
|
||||
实际·未达标
|
||||
</span>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<div class="item-button category-btn">
|
||||
<span class="item-text">类目选择</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>
|
||||
<div class="lineBottom" style="height: 100%; width: 100%">
|
||||
<operatingLineBar :chartData="chartD" style="height: 99%; width: 100%" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import operatingLineBar from './operatingLineBarSale.vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: "Container",
|
||||
components: { operatingLineBar },
|
||||
props: ["chartData"],
|
||||
data() {
|
||||
return {
|
||||
activeButton: 0,
|
||||
isDropdownShow: false,
|
||||
selectedProfit: '营业收入', // 选中的名称,初始为null
|
||||
profitOptions: [
|
||||
'营业收入',
|
||||
'单价',
|
||||
'销量',
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// profitOptions() {
|
||||
// return this.categoryData.map(item => item.name) || [];
|
||||
// },
|
||||
currentDataSource() {
|
||||
console.log('yyyy', this.chartData);
|
||||
|
||||
return this.chartData
|
||||
},
|
||||
locations() {
|
||||
console.log('this.chartData', this.chartData);
|
||||
|
||||
return this.chartData.months
|
||||
},
|
||||
// 根据按钮切换生成对应的 chartData
|
||||
chartD() {
|
||||
const data = this.currentDataSource;
|
||||
console.log(this.currentDataSource, 'currentDataSource');
|
||||
|
||||
const salesData = {
|
||||
allPlaceNames: this.locations,
|
||||
series: [
|
||||
// 1. 完成率(折线图)
|
||||
{
|
||||
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(40, 138, 255, 0)' }
|
||||
])
|
||||
},
|
||||
data: data.rates, // 完成率(%)
|
||||
symbol: 'circle',
|
||||
symbolSize: 6
|
||||
},
|
||||
// 2. 目标(柱状图)
|
||||
{
|
||||
name: '预算',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0, // 左侧Y轴(万元)
|
||||
barWidth: 14,
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(130, 204, 255, 1)' },
|
||||
{ offset: 1, color: 'rgba(75, 157, 255, 1)' }
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: data.budgets // 目标销量(万元)
|
||||
},
|
||||
// 3. 实际(柱状图,含达标状态)
|
||||
{
|
||||
name: '实际',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 14,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
offset: [0, 0],
|
||||
// 固定label尺寸:68px×20px
|
||||
width: 68,
|
||||
height: 20,
|
||||
// 关键:去掉换行,让文字在一行显示,适配小尺寸
|
||||
formatter: (params) => {
|
||||
const diff = data.diffs || [];
|
||||
const flags = data.flags || [];
|
||||
const currentDiff = diff[params.dataIndex] || 0;
|
||||
const currentFlag = flags[params.dataIndex] || 0;
|
||||
|
||||
const prefix = currentFlag === 1 ? '+' : '-';
|
||||
|
||||
// 根据标志位选择不同的样式类
|
||||
if (currentFlag === 1) {
|
||||
// 达标 - 使用 rate-achieved 样式
|
||||
return `{achieved|${currentDiff}}{text|差值}`;
|
||||
} else {
|
||||
// 未达标 - 使用 rate-unachieved 样式
|
||||
return `{unachieved|${currentDiff}}{text|差值}`;
|
||||
}
|
||||
},
|
||||
backgroundColor: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(205, 215, 224, 0.6)' },
|
||||
{ offset: 0.2, color: '#ffffff' },
|
||||
{ offset: 1, color: '#ffffff' }
|
||||
]
|
||||
},
|
||||
shadowColor: 'rgba(191,203,215,0.5)',
|
||||
shadowBlur: 2,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 2,
|
||||
borderRadius: 4,
|
||||
borderColor: '#BFCBD577',
|
||||
borderWidth: 0,
|
||||
lineHeight: 20,
|
||||
rich: {
|
||||
text: {
|
||||
width: 'auto',
|
||||
padding: [5, 10, 5, 0],
|
||||
align: 'center',
|
||||
color: '#464646',
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
},
|
||||
achieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#76DABE', // 与达标的 offset: 1 颜色一致
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
},
|
||||
// 未达标样式
|
||||
unachieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#F9A44A', // 与未达标的 offset: 1 颜色一致
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
}
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
color: (params) => {
|
||||
// 达标状态:1=达标(绿色),0=未达标(橙色)
|
||||
const safeFlag = data.flags;
|
||||
const currentFlag = safeFlag[params.dataIndex] || 0;
|
||||
return currentFlag === 1
|
||||
? {
|
||||
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)' }
|
||||
]
|
||||
}
|
||||
: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(253, 209, 129, 1)' },
|
||||
{ offset: 1, color: 'rgba(249, 164, 74, 1)' }
|
||||
]
|
||||
};
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: data.reals // 实际销量(万元)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
// 根据按钮状态返回对应数据
|
||||
return salesData;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectProfit(item) {
|
||||
this.selectedProfit = item;
|
||||
this.isDropdownShow = false;
|
||||
this.$emit("changeItem", item);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.coreBar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
|
||||
// 新增:头部行容器,实现一行排列
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: flex-end; // 左右两端对齐
|
||||
align-items: center; // 垂直居中
|
||||
// width: 100%;
|
||||
margin-bottom: 8px; // 与下方图表区保留间距(可根据需求调整)
|
||||
}
|
||||
|
||||
// 各基地情况标题样式
|
||||
.base-title {
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
font-style: normal;
|
||||
padding: 0 0 0 16px; // 保留原有内边距
|
||||
white-space: nowrap; // 防止文字换行
|
||||
}
|
||||
|
||||
.barTop {
|
||||
// 移除原有flex和justify-content,由header-row控制
|
||||
width: auto; // 自适应宽度
|
||||
// 保留原有align-items,确保内部元素垂直居中
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
// 1. 右侧容器:包裹图例和按钮组,整体靠右
|
||||
.right-container {
|
||||
display: flex;
|
||||
align-items: center; // 图例和按钮组垂直居中
|
||||
gap: 24px; // 图例与按钮组的间距,避免贴紧
|
||||
margin-right: 46px; // 右侧整体留边,与原按钮组边距一致
|
||||
}
|
||||
|
||||
// 2. 图例:在右侧容器内横向排列
|
||||
.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);
|
||||
}
|
||||
|
||||
// 3. 按钮组:在右侧容器内,保留原有样式
|
||||
.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;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
.item-text {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.category-btn {
|
||||
width: 75px;
|
||||
border-top-left-radius: 12px;
|
||||
border-bottom-left-radius: 12px;
|
||||
background: #ffffff;
|
||||
color: #0b58ff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profit-btn {
|
||||
width: 123px;
|
||||
border-top-right-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
position: relative;
|
||||
padding: 0 18px 0 8px;
|
||||
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: 123px;
|
||||
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>
|
||||
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="chartBox" style="width: 100%; height: 108px; position: relative;">
|
||||
<div :id="id" style="width: 100%; height:100%;"></div>
|
||||
<div class="bottomTip">
|
||||
<div class="precent">
|
||||
<span class="precentNum">{{ detailData.rate || 0 }}% </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'EnergyConsumption',
|
||||
// components: { Container },
|
||||
// mixins: [resize],
|
||||
props: {
|
||||
detailData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
// electricComu: 0,
|
||||
// steamComu: 20, // 调整为符合max范围的数值(0-8)
|
||||
// // electricity: [120, 150, 130, 180, 160, 200, 190],
|
||||
// // steam: [80, 95, 85, 110, 100, 120, 115],
|
||||
// // dates: ['1日', '2日', '3日', '4日', '5日', '6日', '7日']
|
||||
})
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: () => ('monthG')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// electricityChart: null,
|
||||
// steamChart: null,
|
||||
// specialTicks: [2, 4, 6, 8], // 统一的刻度显示
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
detailData: {
|
||||
deep: true,
|
||||
immediate: true, // 初始化时立即执行
|
||||
handler() {
|
||||
this.updateGauges()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initGauges()
|
||||
// window.addEventListener('resize', this.handleResize)
|
||||
this.observeContainerResize()
|
||||
},
|
||||
methods: {
|
||||
observeContainerResize() {
|
||||
// 修复:获取正确的容器(组件内的.gauge-container)
|
||||
const container = this.$el.querySelector('.gauge-container')
|
||||
if (container && window.ResizeObserver) {
|
||||
this.resizeObserver = new ResizeObserver(entries => {
|
||||
if (this.electricityChart) {
|
||||
this.electricityChart.resize() // 直接触发resize,无需防抖
|
||||
}
|
||||
})
|
||||
this.resizeObserver.observe(container)
|
||||
}
|
||||
},
|
||||
initGauges() {
|
||||
// console.log('this.id',this.id);
|
||||
|
||||
// 初始化电气图表实例
|
||||
const electricityDom = document.getElementById(this.id)
|
||||
if (electricityDom) {
|
||||
// 修复:正确创建并存储图表实例
|
||||
this.electricityChart = echarts.init(electricityDom)
|
||||
// 首次更新数据
|
||||
this.updateGauges()
|
||||
}
|
||||
// 蒸汽图表若未使用,可注释/删除
|
||||
// const steamDom = document.getElementById('steamGauge')
|
||||
// if (steamDom) {
|
||||
// this.steamChart = echarts.init(steamDom)
|
||||
// }
|
||||
},
|
||||
updateGauges() {
|
||||
// 修复:先判断实例是否存在,再更新配置
|
||||
if (!this.electricityChart) return
|
||||
|
||||
// 修复:兜底获取rate值,确保数值有效
|
||||
const rate = Number(this.detailData?.rate) || 0
|
||||
console.log('当前rate值:', rate); // 调试:确认rate值正确
|
||||
|
||||
// 关键:第二个参数传true,清空原有配置,强制更新
|
||||
this.electricityChart.setOption(this.getElectricityGaugeOption(rate), true)
|
||||
},
|
||||
// 用电量仪表盘配置(保留原有样式,优化数值范围)
|
||||
getElectricityGaugeOption(value) {
|
||||
const electricityGradient = new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: '#0B58FF' },
|
||||
{ offset: 1, color: '#32FFCD' }
|
||||
])
|
||||
|
||||
return {
|
||||
series: [
|
||||
{
|
||||
name: '月度',
|
||||
type: 'gauge',
|
||||
radius: '95', // 修复:添加%,避免数值错误
|
||||
center: ['50%', '90%'],
|
||||
startAngle: 180,
|
||||
endAngle: 0,
|
||||
min: 0,
|
||||
max: 100,
|
||||
splitNumber: 4,
|
||||
label: { show: false },
|
||||
progress: {
|
||||
show: true,
|
||||
overlap: false,
|
||||
roundCap: true,
|
||||
clip: false,
|
||||
width: 14,
|
||||
itemStyle: { color: electricityGradient }
|
||||
},
|
||||
pointer: {
|
||||
icon: 'path://M2090.36389,615.30999 L2090.36389,615.30999 C2091.48372,615.30999 2092.40383,616.194028 2092.44859,617.312956 L2096.90698,728.755929 C2097.05155,732.369577 2094.2393,735.416212 2090.62566,735.56078 C2090.53845,735.564269 2090.45117,735.566014 2090.36389,735.566014 L2090.36389,735.566014 C2086.74736,735.566014 2083.81557,732.63423 2083.81557,729.017692 C2083.81557,728.930412 2083.81732,728.84314 2083.82081,728.755929 L2088.2792,617.312956 C2088.32396,616.194028 2089.24407,615.30999 2090.36389,615.30999 Z',
|
||||
length: '75%',
|
||||
width: 16,
|
||||
itemStyle: { color: '#288AFF' },
|
||||
offsetCenter: [0, '10%']
|
||||
},
|
||||
axisLine: {
|
||||
roundCap: true,
|
||||
lineStyle: { width: 12, color: [[1, '#E6EBF7']] }
|
||||
},
|
||||
splitLine: {
|
||||
length: 10,
|
||||
lineStyle: { width: 5, color: '#D6DAE5' },
|
||||
},
|
||||
axisTick: {
|
||||
splitNumber: 2,
|
||||
length: 6,
|
||||
lineStyle: { width: 2, color: '#D6DAE5' }
|
||||
},
|
||||
axisLabel: {
|
||||
show: false,
|
||||
},
|
||||
detail: { show: false },
|
||||
data: [{ value: value, unit: '' }] // 确保数值正确传入
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
// 未使用的蒸汽仪表盘可注释/删除
|
||||
// getSteamGaugeOption(value) { ... }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.chartBox {
|
||||
.bottomTip {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
|
||||
.precent {
|
||||
line-height: 3px;
|
||||
|
||||
&::after,
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 54px;
|
||||
height: 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&::after {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
margin-right: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
// 用电量线条颜色
|
||||
.precent::before {
|
||||
|
||||
background: linear-gradient(90deg, rgba(40, 138, 255, 0) 0%, rgba(12, 125, 254, 0.4) 100%);
|
||||
}
|
||||
|
||||
/* ::after:从 透明 到 rgba(12, 125, 254, 0.4)(90度渐变,左到右) */
|
||||
.precent::after {
|
||||
background: linear-gradient(90deg, rgba(12, 125, 254, 0.4) 0%, rgba(40, 138, 255, 0) 100%);
|
||||
}
|
||||
|
||||
// 蒸汽线条颜色
|
||||
// .steam-precent::after,
|
||||
// .steam-precent::before {
|
||||
// background: linear-gradient(90deg, rgba(11, 168, 255, 0.26) 0%, rgba(54, 239, 230, 1) 100%);
|
||||
// }
|
||||
|
||||
.precentNum {
|
||||
display: inline-block;
|
||||
// width: 52px;
|
||||
height: 22px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: #0B58FF;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
// 蒸汽数字颜色
|
||||
.steam-num {
|
||||
color: rgba(54, 239, 230, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container :name="title" icon="cockpitItemIcon" size="operatingRevenueBg" 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; gap: 8px;">
|
||||
<div class="dashboard">
|
||||
<div class="title">
|
||||
{{ month }}月完成率
|
||||
</div>
|
||||
<div class="number">
|
||||
<div class="yield">
|
||||
{{ monthData?.rate || 0 }}%
|
||||
</div>
|
||||
<div class="mom">
|
||||
环比{{ monthData?.momRate }}%
|
||||
<img v-if="monthData?.momRate >= 0" class="arrow" src="../../../assets/img/topArrow.png" alt="">
|
||||
<img v-else class="arrow" src="../../../assets/img/downArrow.png" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="electricityGauge">
|
||||
<electricityGauge :detailData="monthData" id="month"></electricityGauge>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="line" style="padding: 0px;">
|
||||
<verticalBarChart :detailData="monthData">
|
||||
</verticalBarChart>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Container from './container.vue'
|
||||
import electricityGauge from './electricityGauge.vue'
|
||||
import verticalBarChart from './verticalBarChart.vue'
|
||||
|
||||
// import * as echarts from 'echarts'
|
||||
// import rawItem from './raw-Item.vue'
|
||||
|
||||
export default {
|
||||
name: 'ProductionStatus',
|
||||
components: { Container, electricityGauge, verticalBarChart },
|
||||
// mixins: [resize],
|
||||
props: {
|
||||
monthData: { // 接收父组件传递的设备数据数组
|
||||
type: Object,
|
||||
default: () => {} // 默认空数组,避免报错
|
||||
},
|
||||
title: { // 接收父组件传递的设备数据数组
|
||||
type: String,
|
||||
default: () => '' // 默认空数组,避免报错
|
||||
},
|
||||
month: { // 接收父组件传递的设备数据数组
|
||||
type: String,
|
||||
default: () => '' // 默认空数组,避免报错
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// itemData: {
|
||||
// handler(newValue, oldValue) {
|
||||
// // this.updateChart()
|
||||
// },
|
||||
// deep: true // 若对象内属性变化需触发,需加 deep: true
|
||||
// }
|
||||
},
|
||||
// computed: {
|
||||
// // 处理排序:包含“总成本”的项放前面,其余项按原顺序排列
|
||||
// sortedItemData() {
|
||||
// // 过滤出包含“总成本”的项(不区分大小写)
|
||||
// const totalCostItems = this.itemData.filter(item =>
|
||||
// item.name && item.name.includes('总成本')
|
||||
// );
|
||||
// // 过滤出不包含“总成本”的项
|
||||
// const otherItems = this.itemData.filter(item =>
|
||||
// !item.name || !item.name.includes('总成本')
|
||||
// );
|
||||
// // 合并:总成本项在前,其他项在后
|
||||
// return [...totalCostItems, ...otherItems];
|
||||
// }
|
||||
// },
|
||||
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;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
width: 264px;
|
||||
height: 205px;
|
||||
background: #F9FCFF;
|
||||
padding: 16px 0 0 10px;
|
||||
|
||||
.title {
|
||||
// width: 190px;
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-family: YouSheBiaoTiHei;
|
||||
font-size: 46px;
|
||||
color: #0B58FF;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
white-space: nowrap;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.mom {
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 500px;
|
||||
height: 205px;
|
||||
background: #F9FCFF;
|
||||
}
|
||||
|
||||
// .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> -->
|
||||
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container :name="title" icon="cockpitItemIcon" size="operatingRevenueBg" topSize="middle">
|
||||
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%;">
|
||||
<div class="topItem-container" style="display: flex; gap: 8px; width: 100%;">
|
||||
<!-- 销量模块(直接传递整合了flag的salesData) -->
|
||||
<div class="dashboard left" @click="handleDashboardClick('/salesVolumeAnalysis/salesVolumeAnalysisBase')">
|
||||
<div style='position: relative;'>
|
||||
<div class="title">
|
||||
销量·万㎡
|
||||
</div>
|
||||
<div style='font-size: 16px;position: absolute;top:-4px;right:15px'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{monthAnalysis[0].rate}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:monthAnalysis[0].flags>0?'#30B590':'#FF9423'}" >{{monthAnalysis[0].diff}}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<operatingSingleBar :detailData="salesData"></operatingSingleBar>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 单价模块(直接传递整合了flag的unitPriceData) -->
|
||||
<div class="dashboard right" @click="handleDashboardClick('/unitPriceAnalysis/unitPriceAnalysisBase')">
|
||||
<div style='position: relative;'>
|
||||
<div class="title">
|
||||
单价·元/㎡
|
||||
</div>
|
||||
<div style='font-size: 16px;position: absolute;top:-4px;right:15px'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{monthAnalysis[1].rate}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:monthAnalysis[1].flags>0?'#30B590':'#FF9423'}" >{{monthAnalysis[1].diff}}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<operatingSingleBar :detailData="unitPriceData"></operatingSingleBar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Container from './container.vue'
|
||||
import operatingSingleBar from './operatingSingleBar.vue'
|
||||
|
||||
export default {
|
||||
name: 'ProductionStatus',
|
||||
components: { Container, operatingSingleBar },
|
||||
props: {
|
||||
monthAnalysis: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{ title: "销量", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
{ title: "单价", budget: 0, real: 0, rate: 0, diff: 0 }
|
||||
]
|
||||
},
|
||||
dateData: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
factory: {
|
||||
type: [String,Number],
|
||||
default: ''
|
||||
},
|
||||
month: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
// 初始化数据包含flag字段
|
||||
salesData: { title: "销量", budget: 0, real: 0, rate: 0, diff: 0, flag: 0 },
|
||||
unitPriceData: { title: "单价", budget: 0, real: 0, rate: 0, diff: 0, flag: 0 }
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
monthAnalysis: {
|
||||
handler(newVal) {
|
||||
this.updateChart(newVal)
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateChart(this.monthAnalysis)
|
||||
},
|
||||
methods: {
|
||||
handleDashboardClick(path) {
|
||||
this.$router.push({
|
||||
path: path,
|
||||
query: {
|
||||
factory: this.$route.query.factory ? this.$route.query.factory : this.factory,
|
||||
dateData: this.dateData
|
||||
}
|
||||
})
|
||||
},
|
||||
// 判断flag的核心方法
|
||||
getRateFlag(rate, real, target) {
|
||||
if (isNaN(rate) || rate === null || rate === undefined) return 0;
|
||||
|
||||
// 1. 完成率 >= 100 => 达标
|
||||
if (rate >= 100) return 1;
|
||||
|
||||
// 2. 完成率 = 0 且 (目标值=0 或 实际值=目标值=0) => 达标
|
||||
if (rate === 0 && target === 0) return 1;
|
||||
|
||||
// 其他情况 => 未达标
|
||||
return 0;
|
||||
},
|
||||
|
||||
updateChart(data) {
|
||||
// 数据兜底
|
||||
const salesItem = Array.isArray(data) && data[0] ? data[0] : { title: "销量", budget: 0, real: 0, rate: 0, diff: 0 };
|
||||
const unitPriceItem = Array.isArray(data) && data[1] ? data[1] : { title: "单价", budget: 0, real: 0, rate: 0, diff: 0 };
|
||||
|
||||
// 核心修改:将flag整合到数据对象中,无需单独定义salesFlag/unitPriceFlag
|
||||
this.salesData = {
|
||||
...salesItem, // 合并原有字段
|
||||
flag: this.getRateFlag(salesItem.rate, salesItem.real, salesItem.budget) // 新增flag字段
|
||||
};
|
||||
|
||||
this.unitPriceData = {
|
||||
...unitPriceItem, // 合并原有字段
|
||||
flag: this.getRateFlag(unitPriceItem.rate, unitPriceItem.real, unitPriceItem.budget) // 新增flag字段
|
||||
};
|
||||
|
||||
// 调试:确认整合后的数据
|
||||
console.log('整合flag后的销量数据:', this.salesData);
|
||||
console.log('整合flag后的单价数据:', this.unitPriceData);
|
||||
}
|
||||
}
|
||||
}
|
||||
</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;
|
||||
}
|
||||
|
||||
.topItem-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
height: 205px;
|
||||
background: #F9FCFF;
|
||||
padding: 16px 0 0 10px;
|
||||
margin: 0 4px;
|
||||
|
||||
.title {
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 2px;
|
||||
text-align: left;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chart-wrap {
|
||||
width: 100%;
|
||||
height: calc(100% - 30px);
|
||||
}
|
||||
|
||||
.number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
height: 32px;
|
||||
font-family: YouSheBiaoTiHei;
|
||||
font-size: 32px;
|
||||
color: #0B58FF;
|
||||
line-height: 32px;
|
||||
letter-spacing: 2px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mom {
|
||||
width: 97px;
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: left;
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard.left {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.dashboard.right {
|
||||
margin-right: 0;
|
||||
}
|
||||
</style>
|
||||
503
src/views/home/depreciationAnalysisComponents/operatingBar.vue
Normal file
503
src/views/home/depreciationAnalysisComponents/operatingBar.vue
Normal file
@@ -0,0 +1,503 @@
|
||||
<template>
|
||||
<div class="coreBar">
|
||||
<div class="header-row">
|
||||
<div class="base-title">各基地情况</div>
|
||||
<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">
|
||||
<span class="legend-icon square achieved"></span>实际·达标
|
||||
</span>
|
||||
<span class="legend-item">
|
||||
<span class="legend-icon square unachieved"></span>实际·未达标
|
||||
</span>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<div class="item-button category-btn">
|
||||
<span class="item-text">展示顺序</span>
|
||||
</div>
|
||||
<div class="dropdown-container">
|
||||
<div class="item-button profit-btn active" @click.stop="isDropdownShow = !isDropdownShow">
|
||||
<span class="item-text profit-text">{{ selectedSort || '请选择' }}</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.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lineBottom" style="height: 100%; width: 100%">
|
||||
<operatingLineBar :dateData="dateData" :chartData="chartD" style="height: 100%; width: 100%" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import operatingLineBar from './operatingLineBarSale.vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: "Container",
|
||||
components: { operatingLineBar },
|
||||
props: ["chartData", 'dateData'],
|
||||
emits: ['sort-change'], // 声明事件(Vue3 推荐)
|
||||
data() {
|
||||
return {
|
||||
activeButton: 0,
|
||||
isDropdownShow: false,
|
||||
selectedSort: '实际值:高~低', // 选中的label
|
||||
selectedSortValue: 1, // 选中的value,用于排序逻辑
|
||||
profitOptions: [
|
||||
{ label: '实际值:高~低', value: 1 },
|
||||
{ label: '实际值:低~高', value: 2 },
|
||||
{ label: '完成率:高~低', value: 3 },
|
||||
{ label: '完成率:低~高', value: 4 },
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 排序后的数据源(核心:根据selectedSortValue重新排序)
|
||||
currentDataSource() {
|
||||
if (!this.chartData?.factory) return {};
|
||||
|
||||
// 深拷贝原始数据,避免修改原数据
|
||||
const factory = JSON.parse(JSON.stringify(this.chartData.factory));
|
||||
if (!factory.locations.length || !this.selectedSortValue) return factory;
|
||||
|
||||
// 构建带索引的数组,方便同步所有字段排序
|
||||
const dataWithIndex = factory.locations.map((name, index) => ({
|
||||
index,
|
||||
name,
|
||||
real: factory.reals[index],
|
||||
target: factory.targets[index],
|
||||
rate: factory.rates[index],
|
||||
diff: factory.diff[index],
|
||||
flag: factory.flags[index]
|
||||
}));
|
||||
|
||||
// 根据选中的排序规则排序
|
||||
switch (this.selectedSortValue) {
|
||||
case 1: // 实际值:高~低
|
||||
dataWithIndex.sort((a, b) => b.real - a.real);
|
||||
break;
|
||||
case 2: // 实际值:低~高
|
||||
dataWithIndex.sort((a, b) => a.real - b.real);
|
||||
break;
|
||||
case 3: // 预算值:高~低
|
||||
dataWithIndex.sort((a, b) => b.rate - a.rate);
|
||||
break;
|
||||
case 4: // 预算值:低~高
|
||||
dataWithIndex.sort((a, b) => a.rate - b.rate);
|
||||
break;
|
||||
default:
|
||||
return factory;
|
||||
}
|
||||
|
||||
// 同步更新所有数组
|
||||
factory.locations = dataWithIndex.map(item => item.name);
|
||||
factory.reals = dataWithIndex.map(item => item.real);
|
||||
factory.targets = dataWithIndex.map(item => item.target);
|
||||
factory.rates = dataWithIndex.map(item => item.rate);
|
||||
factory.diff = dataWithIndex.map(item => item.diff);
|
||||
factory.flags = dataWithIndex.map(item => item.flag);
|
||||
|
||||
return factory;
|
||||
},
|
||||
locations() {
|
||||
return this.currentDataSource.locations || [];
|
||||
},
|
||||
// 最终传递给图表的排序后数据
|
||||
chartD() {
|
||||
const data = this.currentDataSource;
|
||||
const salesData = {
|
||||
allPlaceNames: this.locations,
|
||||
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(40, 138, 255, 0)' }
|
||||
])
|
||||
},
|
||||
data: data.rates || [],
|
||||
symbol: 'circle',
|
||||
symbolSize: 6
|
||||
},
|
||||
// 目标(柱状图)
|
||||
{
|
||||
name: '预算',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 40,
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(130, 204, 255, 1)' },
|
||||
{ offset: 1, color: 'rgba(75, 157, 255, 1)' }
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: data.targets || []
|
||||
},
|
||||
// 实际(柱状图)
|
||||
{
|
||||
name: '实际',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 40,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
offset: [32, 0],
|
||||
width: 100,
|
||||
height: 22,
|
||||
formatter: (params) => {
|
||||
const diff = data.diff || [];
|
||||
const flags = data.flags || [];
|
||||
const currentDiff = diff[params.dataIndex] || 0;
|
||||
const currentFlag = flags[params.dataIndex] || 0;
|
||||
|
||||
const prefix = currentFlag === 1 ? '+' : '-';
|
||||
|
||||
// 根据标志位选择不同的样式类
|
||||
if (currentFlag === 1) {
|
||||
// 达标 - 使用 rate-achieved 样式
|
||||
return `{achieved|${currentDiff}}{text|差值}`;
|
||||
} else {
|
||||
// 未达标 - 使用 rate-unachieved 样式
|
||||
return `{unachieved|${currentDiff}}{text|差值}`;
|
||||
}
|
||||
},
|
||||
backgroundColor: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(205, 215, 224, 0.6)' },
|
||||
{ offset: 0.2, color: '#ffffff' },
|
||||
{ offset: 1, color: '#ffffff' }
|
||||
]
|
||||
},
|
||||
shadowColor: 'rgba(191,203,215,0.5)',
|
||||
shadowBlur: 2,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 2,
|
||||
borderRadius: 4,
|
||||
borderColor: '#BFCBD577',
|
||||
borderWidth: 0,
|
||||
lineHeight: 26,
|
||||
rich: {
|
||||
text: {
|
||||
width: 'auto',
|
||||
padding: [5, 10, 5, 0],
|
||||
align: 'center',
|
||||
color: '#464646',
|
||||
fontSize: 14,
|
||||
},
|
||||
achieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#76DABE', // 与达标的 offset: 1 颜色一致
|
||||
fontSize: 14,
|
||||
},
|
||||
// 未达标样式
|
||||
unachieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#F9A44A', // 与未达标的 offset: 1 颜色一致
|
||||
fontSize: 14,
|
||||
}
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
color: (params) => {
|
||||
const safeFlag = data.flags || [];
|
||||
const currentFlag = safeFlag[params.dataIndex] || 0;
|
||||
return currentFlag === 1
|
||||
? {
|
||||
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)' }
|
||||
]
|
||||
}
|
||||
: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(253, 209, 129, 1)' },
|
||||
{ offset: 1, color: 'rgba(249, 164, 74, 1)' }
|
||||
]
|
||||
};
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: data.reals || []
|
||||
}
|
||||
]
|
||||
};
|
||||
return salesData;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectProfit(item) {
|
||||
// 更新选中的label和value
|
||||
this.selectedSort = item.label;
|
||||
this.selectedSortValue = item.value;
|
||||
this.isDropdownShow = false;
|
||||
// 向父组件传递排序事件(可选,保持原有逻辑)
|
||||
this.$emit('sort-change', item.value);
|
||||
}
|
||||
},
|
||||
// 监听父组件传入的chartData变化,重置选中状态(可选)
|
||||
watch: {
|
||||
'chartData.factory': {
|
||||
handler() {
|
||||
// 若需要切换数据源后重置排序,可取消注释
|
||||
// this.selectedSort = null;
|
||||
// this.selectedSortValue = null;
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.coreBar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.base-title {
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
font-style: normal;
|
||||
padding: 0 0 0 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.barTop {
|
||||
width: auto;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.right-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
margin-right: 46px;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.legend-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.legend-icon.line {
|
||||
width: 12px;
|
||||
height: 2px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: -2px;
|
||||
left: 3px;
|
||||
width: 6px;
|
||||
border-radius: 50%;
|
||||
height: 6px;
|
||||
background-color: rgba(40, 138, 255, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.legend-icon.square {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.yield {
|
||||
background: rgba(40, 138, 255, 1);
|
||||
}
|
||||
|
||||
.target {
|
||||
background: #2889FF;
|
||||
}
|
||||
|
||||
.achieved {
|
||||
background: rgba(40, 203, 151, 1);
|
||||
}
|
||||
|
||||
.unachieved {
|
||||
background: rgba(255, 132, 0, 1);
|
||||
}
|
||||
|
||||
.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;
|
||||
fontSize: 12px;
|
||||
line-height: 24px;
|
||||
font-style: normal;
|
||||
letter-spacing: 2px;
|
||||
overflow: hidden;
|
||||
|
||||
.item-text {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.category-btn {
|
||||
width: 75px;
|
||||
border-top-left-radius: 12px;
|
||||
border-bottom-left-radius: 12px;
|
||||
background: #ffffff;
|
||||
color: #0b58ff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profit-btn {
|
||||
width: 123px;
|
||||
border-top-right-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
position: relative;
|
||||
padding: 0 18px 0 8px;
|
||||
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: 123px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
|
||||
.dropdown-option {
|
||||
padding: 6px 12px;
|
||||
fontSize: 12px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
letter-spacing: 1px;
|
||||
|
||||
&:hover {
|
||||
background: #f5f7fa;
|
||||
color: #3071ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div ref="cockpitEffChipBottom" id="cockpitEffChipBottom" style="width: 100%; height: 400px;"></div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
myChart: null // 存储图表实例,避免重复创建
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 明确接收的props结构,增强可读性
|
||||
chartData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
series: [],
|
||||
allPlaceNames: []
|
||||
}),
|
||||
// 校验数据格式
|
||||
validator: (value) => {
|
||||
return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.updateChart();
|
||||
});
|
||||
},
|
||||
|
||||
// 新增:监听 chartData 变化
|
||||
watch: {
|
||||
// 深度监听数据变化,仅更新图表配置(不销毁实例)
|
||||
chartData: {
|
||||
handler() {
|
||||
console.log(this.chartData,'chartData');
|
||||
|
||||
this.updateChart();
|
||||
},
|
||||
deep: true,
|
||||
immediate: true // 初始化时立即执行
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateChart() {
|
||||
const chartDom = this.$refs.cockpitEffChipBottom;
|
||||
if (!chartDom) {
|
||||
console.error('图表容器未找到!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose();
|
||||
}
|
||||
|
||||
this.myChart = echarts.init(chartDom);
|
||||
const { allPlaceNames, series } = this.chartData || {};
|
||||
|
||||
// 处理空数据
|
||||
const xData = allPlaceNames || [];
|
||||
const chartSeries = series || []; // 父组件传递的 series
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
},
|
||||
formatter: (params) => {
|
||||
let html = `${params[0].axisValue}<br/>`;
|
||||
params.forEach(item => {
|
||||
const unit = item.seriesName === '完成率' ? '%' : (
|
||||
['产量', '销量'].includes(this.$parent.selectedProfit) ? '片' : '万元'
|
||||
);
|
||||
html += `${item.marker} ${item.seriesName}: ${item.value}${unit}<br/>`;
|
||||
});
|
||||
return html;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 30,
|
||||
right: 20,
|
||||
left: 60,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
axisTick: { show: false },
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
interval: 0,
|
||||
padding: [5, 0, 0, 0]
|
||||
},
|
||||
data: xData
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
// 左侧Y轴:营业收入、成本(单位万元)
|
||||
{
|
||||
type: 'value',
|
||||
splitNumber: 4,
|
||||
name: '万元',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'right'
|
||||
},
|
||||
// min: 0,
|
||||
// max: (value) => Math.ceil((value.max || 0) * 1.1),
|
||||
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
formatter: '{value}'
|
||||
},
|
||||
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
splitNumber: 4
|
||||
},
|
||||
// 右侧Y轴:利润占比(百分比)
|
||||
{
|
||||
type: 'value',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'left'
|
||||
},
|
||||
// min: 0,
|
||||
// max: 100,
|
||||
scale:true,
|
||||
splitNumber: 4,
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
formatter: '{value}%'
|
||||
},
|
||||
splitLine: { show: false },
|
||||
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
splitNumber: 4
|
||||
}
|
||||
],
|
||||
series: chartSeries // 直接使用父组件传递的 series
|
||||
};
|
||||
|
||||
option && this.myChart.setOption(option);
|
||||
|
||||
// 窗口缩放适配和销毁逻辑保持不变
|
||||
window.addEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
|
||||
this.$once('hook:destroyed', () => {
|
||||
window.removeEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
this.myChart && this.myChart.dispose();
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 400px;"></div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
myChart: null // 存储图表实例,避免重复创建
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 明确接收的props结构,增强可读性
|
||||
chartData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
series: [],
|
||||
allPlaceNames: []
|
||||
}),
|
||||
// 校验数据格式
|
||||
validator: (value) => {
|
||||
return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.updateChart();
|
||||
});
|
||||
},
|
||||
|
||||
// 新增:监听 chartData 变化
|
||||
watch: {
|
||||
// 深度监听数据变化,仅更新图表配置(不销毁实例)
|
||||
chartData: {
|
||||
handler() {
|
||||
console.log(this.chartData,'chartData');
|
||||
|
||||
this.updateChart();
|
||||
},
|
||||
deep: true,
|
||||
immediate: true // 初始化时立即执行
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateChart() {
|
||||
const chartDom = this.$refs.cockpitEffChip;
|
||||
if (!chartDom) {
|
||||
console.error('图表容器未找到!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose();
|
||||
}
|
||||
|
||||
this.myChart = echarts.init(chartDom);
|
||||
const { allPlaceNames, series } = this.chartData || {};
|
||||
|
||||
// 处理空数据
|
||||
const xData = allPlaceNames || [];
|
||||
const chartSeries = series || []; // 父组件传递的 series
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
},
|
||||
// formatter: (params) => {
|
||||
// let html = `${params[0].axisValue}<br/>`;
|
||||
// params.forEach(item => {
|
||||
// const unit = item.seriesName === '完成率' ? '%' : (
|
||||
// ['产量', '销量'].includes(this.$parent.selectedProfit) ? '片' : '万元'
|
||||
// );
|
||||
// html += `${item.marker} ${item.seriesName}: ${item.value}${unit}<br/>`;
|
||||
// });
|
||||
// return html;
|
||||
// }
|
||||
},
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 30,
|
||||
right: 20,
|
||||
left: 60,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
axisTick: { show: false },
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
interval: 0,
|
||||
padding: [5, 0, 0, 0]
|
||||
},
|
||||
data: xData
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
// 左侧Y轴:营业收入、成本(单位万元)
|
||||
{
|
||||
type: 'value',
|
||||
name: '万元',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'right'
|
||||
},
|
||||
|
||||
splitNumber: 4,
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
formatter: '{value}'
|
||||
},
|
||||
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
splitNumber: 4
|
||||
},
|
||||
// 右侧Y轴:利润占比(百分比)
|
||||
// {
|
||||
// type: 'value',
|
||||
// nameTextStyle: {
|
||||
// color: 'rgba(0, 0, 0, 0.45)',
|
||||
// fontSize: 12,
|
||||
// align: 'left'
|
||||
// },
|
||||
// min: 0,
|
||||
// max: 100,
|
||||
// axisTick: { show: false },
|
||||
// axisLabel: {
|
||||
// color: 'rgba(0, 0, 0, 0.45)',
|
||||
// fontSize: 12,
|
||||
// formatter: '{value}%'
|
||||
// },
|
||||
// splitLine: { show: false },
|
||||
// axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
// splitNumber: 4
|
||||
// }
|
||||
],
|
||||
series: chartSeries // 直接使用父组件传递的 series
|
||||
};
|
||||
|
||||
option && this.myChart.setOption(option);
|
||||
|
||||
// 窗口缩放适配和销毁逻辑保持不变
|
||||
window.addEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
|
||||
this.$once('hook:destroyed', () => {
|
||||
window.removeEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
this.myChart && this.myChart.dispose();
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 400px;"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
myChart: null, // 存储图表实例
|
||||
resizeHandler: null, // 存储resize事件处理函数
|
||||
// 核心:基地名称与序号的映射表(固定顺序)
|
||||
baseNameToIndexMap: {
|
||||
'宜兴': 7,
|
||||
'漳州': 8,
|
||||
'自贡': 3,
|
||||
'桐城': 2,
|
||||
'洛阳': 9,
|
||||
'合肥': 5,
|
||||
'宿迁': 6,
|
||||
'秦皇岛': 10
|
||||
}
|
||||
};
|
||||
},
|
||||
props: {
|
||||
chartData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
dateData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.initChart(); // 初始化图表(只执行一次)
|
||||
this.updateChart(); // 更新图表数据
|
||||
});
|
||||
},
|
||||
watch: {
|
||||
chartData: {
|
||||
handler() {
|
||||
console.log(this.chartData, 'chartData');
|
||||
this.updateChart(); // 仅更新数据,不重新创建实例
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 组件销毁时清理资源
|
||||
this.destroyChart();
|
||||
},
|
||||
methods: {
|
||||
// 初始化图表(只在mounted中执行一次)
|
||||
initChart() {
|
||||
const chartDom = this.$refs.cockpitEffChip;
|
||||
if (!chartDom) {
|
||||
console.error('图表容器未找到!');
|
||||
return;
|
||||
}
|
||||
|
||||
// 只创建一次图表实例
|
||||
this.myChart = echarts.init(chartDom);
|
||||
|
||||
// 绑定点击事件(只绑定一次,永久生效)
|
||||
this.myChart.getZr().on('click', (params) => {
|
||||
console.log('params', params);
|
||||
|
||||
// 提取点击的基地名称
|
||||
// const itemName = params.name;
|
||||
let itemName = undefined
|
||||
// 根据映射表获取对应的序号(未匹配到则返回0或其他默认值)
|
||||
let pointInPixel = [params.offsetX, params.offsetY];
|
||||
if (this.myChart.containPixel('grid', pointInPixel)) {
|
||||
let pointInGrid = this.myChart.convertFromPixel({
|
||||
seriesIndex: 0
|
||||
}, pointInPixel);
|
||||
let xIndex = pointInGrid[0]; //索引
|
||||
let handleIndex = Number(xIndex);
|
||||
let seriesObj = this.myChart.getOption(); //图表object对象
|
||||
var op = this.myChart.getOption();
|
||||
//获得图表中点击的列
|
||||
itemName = op.xAxis[0].data[handleIndex]; //获取点击的列名
|
||||
console.log(itemName, 'monthmonthmonth');
|
||||
console.log(handleIndex, seriesObj);
|
||||
};
|
||||
const baseIndex = this.baseNameToIndexMap[itemName] || 0;
|
||||
|
||||
console.log(`你点击了【${itemName}】(序号:${baseIndex})`);
|
||||
if (itemName === undefined) {
|
||||
return;
|
||||
}
|
||||
// 路由跳转时携带序号(或名称+序号)
|
||||
this.$router.push({
|
||||
path: 'operatingRevenueBase',
|
||||
query: { // 使用query传递参数(推荐),也可使用params
|
||||
// baseName: itemName,
|
||||
factory: baseIndex,
|
||||
dateData: this.dateData
|
||||
}
|
||||
// 若仍需用base作为参数:
|
||||
// base: itemName,
|
||||
// params: { baseIndex: baseIndex }
|
||||
});
|
||||
});
|
||||
|
||||
// 定义resize处理函数(命名函数,方便移除)
|
||||
this.resizeHandler = () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
};
|
||||
// 绑定resize事件(只绑定一次)
|
||||
window.addEventListener('resize', this.resizeHandler);
|
||||
},
|
||||
|
||||
// 更新图表数据(数据变化时执行)
|
||||
updateChart() {
|
||||
if (!this.myChart) {
|
||||
return; // 实例未初始化则返回
|
||||
}
|
||||
|
||||
const { allPlaceNames, series } = this.chartData || {};
|
||||
const xData = allPlaceNames || [];
|
||||
const chartSeries = series || [];
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 5,
|
||||
right: 20,
|
||||
left: 25,
|
||||
containLabel: true
|
||||
},
|
||||
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],
|
||||
// 可选:X轴标签显示“序号+名称”(如“1 宜兴”)
|
||||
// formatter: (value) => {
|
||||
// const index = this.baseNameToIndexMap[value] || '';
|
||||
// return index ? `${index} ${value}` : value;
|
||||
// }
|
||||
},
|
||||
data: xData
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '万元',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'right'
|
||||
},
|
||||
splitNumber: 4,
|
||||
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: { show: true, show: true, lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } }
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'left'
|
||||
},
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
formatter: '{value}%'
|
||||
},
|
||||
splitLine: { show: false },
|
||||
axisLine: { show: true, show: true, lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
splitNumber: 4
|
||||
}
|
||||
],
|
||||
series: chartSeries
|
||||
};
|
||||
|
||||
// 只更新配置,不重新创建实例
|
||||
this.myChart.setOption(option, true); // 第二个参数true表示清空原有配置,避免数据残留
|
||||
},
|
||||
|
||||
// 销毁图表资源
|
||||
destroyChart() {
|
||||
// 移除resize事件
|
||||
if (this.resizeHandler) {
|
||||
window.removeEventListener('resize', this.resizeHandler);
|
||||
}
|
||||
// 销毁图表实例
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose();
|
||||
this.myChart = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 380px;"></div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
myChart: null // 存储图表实例,避免重复创建
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 明确接收的props结构,增强可读性
|
||||
chartData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
}),
|
||||
// 校验数据格式
|
||||
// validator: (value) => {
|
||||
// return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
|
||||
// }
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.updateChart();
|
||||
});
|
||||
},
|
||||
|
||||
// 新增:监听 chartData 变化
|
||||
watch: {
|
||||
// 深度监听数据变化,仅更新图表配置(不销毁实例)
|
||||
chartData: {
|
||||
handler() {
|
||||
console.log(this.chartData,'chartData');
|
||||
this.updateChart();
|
||||
},
|
||||
deep: true,
|
||||
immediate: true // 初始化时立即执行
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateChart() {
|
||||
const chartDom = this.$refs.cockpitEffChip;
|
||||
if (!chartDom) {
|
||||
console.error('图表容器未找到!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose();
|
||||
}
|
||||
|
||||
this.myChart = echarts.init(chartDom);
|
||||
|
||||
const { allPlaceNames, series } = this.chartData || {};
|
||||
console.log('chartData', this.chartData);
|
||||
|
||||
// 处理空数据
|
||||
const xData = allPlaceNames || [];
|
||||
const chartSeries = series || []; // 父组件传递的 series
|
||||
console.log('xData', xData);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
},
|
||||
// formatter: (params) => {
|
||||
// let html = `${params[0].axisValue}<br/>`;
|
||||
// params.forEach(item => {
|
||||
// const unit = item.seriesName === '完成率' ? '%' : (
|
||||
// ['产量', '销量'].includes(this.$parent.selectedProfit) ? '片' : '万元'
|
||||
// );
|
||||
// html += `${item.marker} ${item.seriesName}: ${item.value}${unit}<br/>`;
|
||||
// });
|
||||
// return html;
|
||||
// }
|
||||
},
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom:20,
|
||||
right: 10,
|
||||
left: 25,
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
axisTick: { show: false },
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
interval: 0,
|
||||
padding: [5, 0, 0, 0]
|
||||
},
|
||||
data: xData
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
// 左侧Y轴:营业收入、成本(单位万元)
|
||||
{
|
||||
type: 'value',
|
||||
name: '万元',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'right'
|
||||
},
|
||||
splitNumber: 4,
|
||||
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: { show: true, show: true, lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
},
|
||||
// 右侧Y轴:利润占比(百分比)
|
||||
{
|
||||
type: 'value',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'left'
|
||||
},
|
||||
// min: 0,
|
||||
// max: 100,
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
formatter: '{value}%'
|
||||
},
|
||||
splitLine: { show: false },
|
||||
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
//
|
||||
splitNumber: 4,
|
||||
}
|
||||
],
|
||||
series: chartSeries // 直接使用父组件传递的 series
|
||||
};
|
||||
|
||||
option && this.myChart.setOption(option);
|
||||
|
||||
// 窗口缩放适配和销毁逻辑保持不变
|
||||
window.addEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
|
||||
this.$once('hook:destroyed', () => {
|
||||
window.removeEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
this.myChart && this.myChart.dispose();
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div :ref="refName" id="coreLineChart" style="width: 100%; height: 200px;"></div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
myChart: null // 存储图表实例,避免重复创建
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 明确接收的props结构,增强可读性
|
||||
refName: {
|
||||
type: String,
|
||||
default: () => 'cockpitEffChip',
|
||||
// 校验数据格式
|
||||
// validator: (value) => {
|
||||
// return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
|
||||
// }
|
||||
},
|
||||
chartData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
}),
|
||||
// 校验数据格式
|
||||
// validator: (value) => {
|
||||
// return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
|
||||
// }
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.updateChart();
|
||||
});
|
||||
},
|
||||
|
||||
// 新增:监听 chartData 变化
|
||||
watch: {
|
||||
// 深度监听数据变化,仅更新图表配置(不销毁实例)
|
||||
chartData: {
|
||||
handler() {
|
||||
console.log(this.chartData, 'chartData');
|
||||
this.updateChart();
|
||||
},
|
||||
deep: true,
|
||||
immediate: true // 初始化时立即执行
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateChart() {
|
||||
const chartDom = this.$refs[this.refName];
|
||||
if (!chartDom) {
|
||||
console.error('图表容器未找到!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose();
|
||||
}
|
||||
|
||||
this.myChart = echarts.init(chartDom);
|
||||
|
||||
const { allPlaceNames, series } = this.chartData || {};
|
||||
console.log('chartData', this.chartData);
|
||||
|
||||
// 处理空数据
|
||||
const xData = allPlaceNames || [];
|
||||
const chartSeries = series || []; // 父组件传递的 series
|
||||
console.log('xData', xData);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
// axisPointer: {
|
||||
// type: 'cross',
|
||||
// label: {
|
||||
// backgroundColor: '#6a7985'
|
||||
// }
|
||||
// },
|
||||
// formatter: (params) => {
|
||||
// let html = `${params[0].axisValue}<br/>`;
|
||||
// params.forEach(item => {
|
||||
// const unit = item.seriesName === '完成率' ? '%' : (
|
||||
// ['产量', '销量'].includes(this.$parent.selectedProfit) ? '片' : '万元'
|
||||
// );
|
||||
// html += `${item.marker} ${item.seriesName}: ${item.value}${unit}<br/>`;
|
||||
// });
|
||||
// return html;
|
||||
// }
|
||||
},
|
||||
grid: {
|
||||
top: 20,
|
||||
bottom: 30,
|
||||
right: 20,
|
||||
left: 5,
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
axisTick: { show: false },
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
interval: 0,
|
||||
padding: [5, 0, 0, 0]
|
||||
},
|
||||
data: xData
|
||||
}
|
||||
],
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
// name: '万元',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'right'
|
||||
},
|
||||
axisLine: {
|
||||
show: true, // 显示Y轴轴线(关键)
|
||||
lineStyle: {
|
||||
color: '#E5E6EB', // 轴线颜色(浅灰色,可自定义)
|
||||
width: 1, // 轴线宽度
|
||||
type: 'solid' // 实线(可选:dashed虚线、dotted点线)
|
||||
}
|
||||
},
|
||||
|
||||
splitNumber: 2,
|
||||
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)' } },
|
||||
},
|
||||
series: chartSeries // 直接使用父组件传递的 series
|
||||
};
|
||||
|
||||
option && this.myChart.setOption(option);
|
||||
|
||||
// 窗口缩放适配和销毁逻辑保持不变
|
||||
window.addEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
|
||||
this.$once('hook:destroyed', () => {
|
||||
window.removeEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
this.myChart && this.myChart.dispose();
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,349 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container name="当月数据对比" icon="cockpitItemIcon" size="operatingLarge" topSize="large">
|
||||
<!-- 1. 移除 .kpi-content 的固定高度,改为自适应 -->
|
||||
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%; gap: 16px">
|
||||
<div class="left" style="
|
||||
height: 380px;
|
||||
display: flex;
|
||||
width: 348px;
|
||||
background-color: rgba(249, 252, 255, 1);
|
||||
flex-direction: column;
|
||||
">
|
||||
<div style="
|
||||
padding: 16px 16px 5px 16px;
|
||||
line-height: 18px;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
letter-spacing: 1px;
|
||||
font-style: normal;
|
||||
">
|
||||
集团情况
|
||||
</div>
|
||||
<div style='font-size: 16px;line-height: 16px;text-align: right;padding-right: 16px;'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{chartData.group.rate[0]}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:chartData.group.flags>0?'#30B590':'#FF9423'}" >{{chartData.group.diff[0]}}</span></span>
|
||||
</div>
|
||||
<operatingTopBar :chartData="chartData" />
|
||||
</div>
|
||||
<div class="right" style="
|
||||
height: 380px;
|
||||
display: flex;
|
||||
width: 1220px;
|
||||
background-color: rgba(249, 252, 255, 1);
|
||||
">
|
||||
<!-- <top-item /> -->
|
||||
<operatingBar :dateData="dateData" :chartData="chartData" @sort-change="sortChange" />
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Container from "../components/container.vue";
|
||||
import operatingBar from "./operatingBar.vue";
|
||||
import operatingTopBar from "./operatingTopBar.vue";
|
||||
|
||||
export default {
|
||||
name: "ProductionStatus",
|
||||
components: { Container, operatingBar, operatingTopBar },
|
||||
props: {
|
||||
monthData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
group: {
|
||||
rate: 0,
|
||||
diff: 0,
|
||||
real: 0,
|
||||
target: 0
|
||||
},
|
||||
factory: []
|
||||
}),
|
||||
required: true
|
||||
},
|
||||
dateData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chartData: null, // 初始化 chartData 为 null
|
||||
groupData: {}, // 集团数据
|
||||
factoryData: [] // 工厂数据
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
monthData: {
|
||||
handler() {
|
||||
this.processChartData();
|
||||
},
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
sortChange(value) {
|
||||
this.$emit('sort-change', value);
|
||||
},
|
||||
/**
|
||||
* 判断rate对应的flag值(<1为0,>1为1)
|
||||
* @param {number} rate 处理后的rate值(已*100)
|
||||
* @returns {0|1} flag值
|
||||
*/
|
||||
getRateFlag(rate, real, target) {
|
||||
if (isNaN(rate) || rate === null || rate === undefined) return 0;
|
||||
|
||||
// 1. 完成率 >= 100 => 达标
|
||||
if (rate >= 100) return 1;
|
||||
|
||||
// 2. 完成率 = 0 且 (目标值=0 或 实际值=目标值=0) => 达标
|
||||
if (rate === 0 && target === 0) return 1;
|
||||
|
||||
// 其他情况 => 未达标
|
||||
return 0;
|
||||
},
|
||||
/**
|
||||
* 核心处理函数:在所有数据都准备好后,才组装 chartData
|
||||
*/
|
||||
processChartData() {
|
||||
// 1. 处理集团数据 - 提取各字段到对应数组
|
||||
this.groupData = this.monthData.group || { rate: 0, diff: 0, real: 0, target: 0 };
|
||||
|
||||
// 集团各维度数据数组(单条数据,对应凯盛新能)
|
||||
const groupTarget = [this.groupData.target]; // 预算值数组
|
||||
const groupDiff = [this.groupData.diff]; // 差值数组
|
||||
const groupReal = [this.groupData.real]; // 实际值数组
|
||||
const groupRate = [this.groupData.rate]; // 完成率数组
|
||||
// 新增:集团rate对应的flag
|
||||
const groupFlag = [this.getRateFlag(groupRate[0], groupReal[0], groupTarget[0])];
|
||||
|
||||
console.log('集团数据数组:', {
|
||||
groupTarget,
|
||||
groupDiff,
|
||||
groupReal,
|
||||
groupRate,
|
||||
groupFlag,
|
||||
rawGroupData: this.groupData
|
||||
});
|
||||
|
||||
// 2. 处理工厂数据 - 提取每个工厂的对应字段到数组
|
||||
this.factoryData = this.monthData.factory || [];
|
||||
// 提取工厂名称数组
|
||||
const factoryNames = this.factoryData.map(item => item.title || '');
|
||||
// 提取工厂各维度数据数组
|
||||
const factoryBudget = this.factoryData.map(item => item.budget || 0);
|
||||
const factoryReal = this.factoryData.map(item => item.real || 0);
|
||||
const factoryRate = this.factoryData.map(item => item.rate || 0);
|
||||
const factoryDiff = this.factoryData.map(item => item.diff || 0);
|
||||
// 新增:每个工厂rate对应的flag数组
|
||||
const factoryFlags = this.factoryData.map(item => this.getRateFlag(item.rate, item.real, item.budget));
|
||||
|
||||
// 3. 组装最终的chartData(供子组件使用)
|
||||
this.chartData = {
|
||||
// 集团数据(对应凯盛新能)
|
||||
group: {
|
||||
locations: ['凯盛新能'], // 集团名称
|
||||
targets: groupTarget, // 集团预算值数组
|
||||
diff: groupDiff, // 集团差值数组
|
||||
reals: groupReal, // 集团实际值数组
|
||||
rate: groupRate, // 集团完成率数组
|
||||
flags: groupFlag // 新增:集团rate对应的flag
|
||||
},
|
||||
// 工厂数据
|
||||
factory: {
|
||||
locations: factoryNames, // 工厂名称数组
|
||||
targets: factoryBudget, // 工厂预算数组
|
||||
reals: factoryReal, // 工厂实际值数组
|
||||
rates: factoryRate, // 工厂完成率数组
|
||||
diff: factoryDiff, // 工厂差值数组
|
||||
flags: factoryFlags // 新增:工厂rate对应的flags数组
|
||||
},
|
||||
// 原始数据备份(方便后续使用)
|
||||
rawData: {
|
||||
group: this.groupData,
|
||||
factory: this.factoryData
|
||||
}
|
||||
};
|
||||
|
||||
console.log('最终处理后的图表数据:', this.chartData);
|
||||
},
|
||||
},
|
||||
};
|
||||
</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>
|
||||
@@ -0,0 +1,347 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container name="累计数据对比" icon="cockpitItemIcon" size="opLargeBg" topSize="large">
|
||||
<!-- 1. 移除 .kpi-content 的固定高度,改为自适应 -->
|
||||
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%; gap: 16px">
|
||||
<div class="left" style="
|
||||
height: 380px;
|
||||
display: flex;
|
||||
width: 348px;
|
||||
background-color: rgba(249, 252, 255, 1);
|
||||
flex-direction: column;
|
||||
">
|
||||
<div style="
|
||||
padding: 16px 16px 5px 16px;
|
||||
line-height: 18px;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
letter-spacing: 1px;
|
||||
font-style: normal;
|
||||
">
|
||||
集团情况
|
||||
</div>
|
||||
<div style='font-size: 16px;line-height: 16px;text-align: right;padding-right: 16px;'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{chartData.group.rate[0]}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:chartData.group.flags>0?'#30B590':'#FF9423'}" >{{chartData.group.diff[0]}}</span></span>
|
||||
</div>
|
||||
<operatingTopBar :chartData="chartData" />
|
||||
</div>
|
||||
<div class="right" style="
|
||||
height: 380px;
|
||||
display: flex;
|
||||
width: 1220px;
|
||||
background-color: rgba(249, 252, 255, 1);
|
||||
">
|
||||
<!-- <top-item /> -->
|
||||
<operatingBar :dateData="dateData" @sort-change="sortChange" :chartData="chartData" />
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Container from "../components/container.vue";
|
||||
import operatingBar from "./operatingBar.vue";
|
||||
import operatingTopBar from "./operatingTopBar.vue";
|
||||
|
||||
export default {
|
||||
name: "ProductionStatus",
|
||||
components: { Container, operatingBar, operatingTopBar },
|
||||
props: {
|
||||
salesTrendMap: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
ytdData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
dateData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
chartData: null, // 初始化 chartData 为 null
|
||||
groupData: {}, // 集团数据
|
||||
factoryData: [] // 工厂数据
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
ytdData: {
|
||||
handler() {
|
||||
this.processChartData();
|
||||
},
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
sortChange(value) {
|
||||
this.$emit('sort-change', value);
|
||||
},
|
||||
/**
|
||||
* 判断rate对应的flag值(<1为0,>1为1)
|
||||
* @param {number} rate 处理后的rate值(已*100)
|
||||
* @returns {0|1} flag值
|
||||
*/
|
||||
getRateFlag(rate, real, target) {
|
||||
if (isNaN(rate) || rate === null || rate === undefined) return 0;
|
||||
|
||||
// 1. 完成率 >= 100 => 达标
|
||||
if (rate >= 100) return 1;
|
||||
|
||||
// 2. 完成率 = 0 且 (目标值=0 或 实际值=目标值=0) => 达标
|
||||
if (rate === 0 && target === 0) return 1;
|
||||
|
||||
// 其他情况 => 未达标
|
||||
return 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* 核心处理函数:在所有数据都准备好后,才组装 chartData
|
||||
*/
|
||||
processChartData() {
|
||||
// 1. 处理集团数据 - 提取各字段到对应数组
|
||||
this.groupData = this.ytdData.group || { rate: 0, diff: 0, real: 0, target: 0 };
|
||||
|
||||
// 集团各维度数据数组(单条数据,对应凯盛新能)
|
||||
const groupTarget = [this.groupData.target]; // 预算值数组
|
||||
const groupDiff = [this.groupData.diff]; // 差值数组
|
||||
const groupReal = [this.groupData.real]; // 实际值数组
|
||||
const groupRate = [this.groupData.rate]; // 完成率数组
|
||||
// 新增:集团rate对应的flag
|
||||
const groupFlag = [this.getRateFlag(groupRate[0], groupReal[0], groupTarget[0])];
|
||||
|
||||
console.log('集团数据数组:', {
|
||||
groupTarget,
|
||||
groupDiff,
|
||||
groupReal,
|
||||
groupRate,
|
||||
groupFlag,
|
||||
rawGroupData: this.groupData
|
||||
});
|
||||
|
||||
// 2. 处理工厂数据 - 提取每个工厂的对应字段到数组
|
||||
this.factoryData = this.ytdData.factory || [];
|
||||
// 提取工厂名称数组
|
||||
const factoryNames = this.factoryData.map(item => item.title || '');
|
||||
// 提取工厂各维度数据数组
|
||||
const factoryBudget = this.factoryData.map(item => item.budget || 0);
|
||||
const factoryReal = this.factoryData.map(item => item.real || 0);
|
||||
const factoryRate = this.factoryData.map(item => item.rate || 0);
|
||||
const factoryDiff = this.factoryData.map(item => item.diff || 0);
|
||||
// 新增:每个工厂rate对应的flag数组
|
||||
const factoryFlags = this.factoryData.map(item => this.getRateFlag(item.rate, item.real, item.budget));
|
||||
|
||||
// 3. 组装最终的chartData(供子组件使用)
|
||||
this.chartData = {
|
||||
// 集团数据(对应凯盛新能)
|
||||
group: {
|
||||
locations: ['凯盛新能'], // 集团名称
|
||||
targets: groupTarget, // 集团预算值数组
|
||||
diff: groupDiff, // 集团差值数组
|
||||
reals: groupReal, // 集团实际值数组
|
||||
rate: groupRate, // 集团完成率数组
|
||||
flags: groupFlag // 新增:集团rate对应的flag
|
||||
},
|
||||
// 工厂数据
|
||||
factory: {
|
||||
locations: factoryNames, // 工厂名称数组
|
||||
targets: factoryBudget, // 工厂预算数组
|
||||
reals: factoryReal, // 工厂实际值数组
|
||||
rates: factoryRate, // 工厂完成率数组
|
||||
diff: factoryDiff, // 工厂差值数组
|
||||
flags: factoryFlags // 新增:工厂rate对应的flags数组
|
||||
},
|
||||
// 原始数据备份(方便后续使用)
|
||||
rawData: {
|
||||
group: this.groupData,
|
||||
factory: this.factoryData
|
||||
}
|
||||
};
|
||||
|
||||
console.log('最终处理后的图表数据:', this.chartData);
|
||||
},
|
||||
},
|
||||
};
|
||||
</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>
|
||||
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<div class="lineBottom" style="height: 180px; width: 100%">
|
||||
<operatingLineBarSaleSingle :refName=" 'totalOperating' " :chartData="chartD" style="height: 99%; width: 100%" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import operatingLineBarSaleSingle from './operatingLineBarSaleSingle.vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: "Container",
|
||||
components: { operatingLineBarSaleSingle },
|
||||
props: ["detailData"],
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
locations() {
|
||||
return ['预算', '实际'];
|
||||
},
|
||||
chartD() {
|
||||
// 背景图片路径(若不需要可注释)
|
||||
// const bgImageUrl = require('@/assets/img/labelBg.png');
|
||||
console.log('this.detailData', this.detailData);
|
||||
const rate = this.detailData?.rate || 0
|
||||
const diff = this.detailData?.diff || 0
|
||||
console.log('diff', diff);
|
||||
|
||||
const seriesData = [
|
||||
{
|
||||
value: this.detailData?.budget || 0,
|
||||
flag: 1, // 实际项:达标(绿色)
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
offset: [0, 0],
|
||||
fontSize: 14,
|
||||
},
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(130, 204, 255, 1)' },
|
||||
{ offset: 1, color: 'rgba(75, 157, 255, 1)' }
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
},
|
||||
{
|
||||
value: this.detailData?.real || 0,
|
||||
flag: this.detailData?.flag, // 实际项:达标(绿色)
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
offset: [0, 0],
|
||||
fontSize: 14,
|
||||
},
|
||||
itemStyle: {
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
const data = {
|
||||
allPlaceNames: ['预算', '实际'],
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: 60,
|
||||
barCategoryGap: '50%',
|
||||
data: seriesData,
|
||||
itemStyle: {
|
||||
color: (params) => {
|
||||
const currentFlag = params.data.flag || 0;
|
||||
return currentFlag === 1
|
||||
? {
|
||||
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)' }
|
||||
]
|
||||
}
|
||||
: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(253, 209, 129, 1)' },
|
||||
{ offset: 1, color: 'rgba(249, 164, 74, 1)' }
|
||||
]
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
console.log('data', data);
|
||||
|
||||
return data;
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.coreBar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
|
||||
.barTop {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
|
||||
.title {
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFangSC;
|
||||
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;
|
||||
margin-right: 46px;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: PingFangSC;
|
||||
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);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
position: relative;
|
||||
gap: 2px;
|
||||
width: 283px;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
background: #ecf4fe;
|
||||
border-radius: 12px;
|
||||
margin: 0;
|
||||
|
||||
.item-button {
|
||||
cursor: pointer;
|
||||
width: 142px;
|
||||
height: 24px;
|
||||
font-family: PingFangSC;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #0b58ff;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
letter-spacing: 8px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.item-button.active {
|
||||
width: 142px;
|
||||
height: 24px;
|
||||
background: #3071ff;
|
||||
border-radius: 12px;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,337 @@
|
||||
<template>
|
||||
<div class="coreBar">
|
||||
<div class="lineBottom" style="height: 320px; width: 100%">
|
||||
<operatingLineBar :chartData="chartD" style="height: 99%; width: 100%" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import operatingLineBar from './operatingLineBarSaleGroup.vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: "Container",
|
||||
components: { operatingLineBar },
|
||||
props: ["chartData"],
|
||||
data() {
|
||||
return {
|
||||
activeButton: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentDataSource() {
|
||||
console.log('yyyy', this.chartData);
|
||||
|
||||
return this.chartData.group
|
||||
},
|
||||
locations() {
|
||||
console.log('this.chartData', this.chartData);
|
||||
|
||||
return this.chartData.group.locations
|
||||
},
|
||||
// 根据按钮切换生成对应的 chartData
|
||||
chartD() {
|
||||
// 销量场景数据
|
||||
const data = this.currentDataSource;
|
||||
const diff = this.currentDataSource.diff[0]
|
||||
const rate = this.currentDataSource.rate[0]
|
||||
|
||||
const salesData = {
|
||||
allPlaceNames: this.locations,
|
||||
series: [
|
||||
{
|
||||
name: '预算',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0, // 左侧Y轴(万元)
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top'
|
||||
},
|
||||
barWidth: 65,
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(130, 204, 255, 1)' },
|
||||
{ offset: 1, color: 'rgba(75, 157, 255, 1)' }
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: data.targets // 目标销量(万元)
|
||||
},
|
||||
// 3. 实际(柱状图,含达标状态)
|
||||
{
|
||||
name: '实际',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top'
|
||||
},
|
||||
barWidth: 65,
|
||||
itemStyle: {
|
||||
color: (params) => {
|
||||
// 达标状态:1=达标(绿色),0=未达标(橙色)
|
||||
const safeFlag = data.flags;
|
||||
const currentFlag = safeFlag[params.dataIndex] || 0;
|
||||
return currentFlag === 1
|
||||
? {
|
||||
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)' }
|
||||
]
|
||||
}
|
||||
: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(253, 209, 129, 1)' },
|
||||
{ offset: 1, color: 'rgba(249, 164, 74, 1)' }
|
||||
]
|
||||
};
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: data.reals // 实际销量(万元)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// 毛利率场景数据
|
||||
const grossProfitData = {
|
||||
series: [
|
||||
// 1. 完成率(折线图)
|
||||
{
|
||||
name: '完成率',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
lineStyle: { color: 'rgba(40, 138, 255, .5)', width: 2 },
|
||||
itemStyle: {
|
||||
color: 'rgba(40, 138, 255, 1)',
|
||||
borderColor: 'rgba(40, 138, 255, 1)',
|
||||
borderWidth: 2,
|
||||
radius: 4
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.2,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(40, 138, 255, .9)' },
|
||||
{ offset: 1, color: 'rgba(40, 138, 255, 0)' }
|
||||
])
|
||||
},
|
||||
data: [106.7, 96.9, 106.5, 106.1, 93.8, 105.9], // 毛利率完成率(%)
|
||||
symbol: 'circle',
|
||||
symbolSize: 6
|
||||
},
|
||||
// 2. 目标(柱状图)
|
||||
{
|
||||
name: '预算',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 65,
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(130, 204, 255, 1)' },
|
||||
{ offset: 1, color: 'rgba(75, 157, 255, 1)' }
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: [30, 32, 31, 33, 32, 34] // 目标毛利率(万元)
|
||||
},
|
||||
// 3. 实际(柱状图)
|
||||
{
|
||||
name: '实际',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 65,
|
||||
itemStyle: {
|
||||
color: (params) => {
|
||||
const safeFlag = [1, 0, 1, 1, 0, 1]; // 达标状态
|
||||
const currentFlag = safeFlag[params.dataIndex] || 0;
|
||||
return currentFlag === 1
|
||||
? {
|
||||
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)' }
|
||||
]
|
||||
}
|
||||
: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(253, 209, 129, 1)' },
|
||||
{ offset: 1, color: 'rgba(249, 164, 74, 1)' }
|
||||
]
|
||||
};
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: [32, 31, 33, 35, 30, 36] // 实际毛利率(万元)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// 根据按钮状态返回对应数据
|
||||
return this.activeButton === 0 ? salesData : grossProfitData;
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.coreBar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
|
||||
.barTop {
|
||||
display: flex;
|
||||
justify-content: flex-end; // 标题左、右侧容器右,整体两端对齐
|
||||
align-items: center; // 垂直居中,避免上下错位
|
||||
gap: 16px; // 标题与右侧容器的最小间距,防止拥挤
|
||||
width: 100%; // 确保占满父容器,实现两端对齐
|
||||
|
||||
.title {
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
// 标题固定在左侧,不挤压右侧空间
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 1. 右侧容器:包裹图例和按钮组,整体靠右
|
||||
.right-container {
|
||||
display: flex;
|
||||
align-items: center; // 图例和按钮组垂直居中
|
||||
gap: 24px; // 图例与按钮组的间距,避免贴紧
|
||||
margin-right: 46px; // 右侧整体留边,与原按钮组边距一致
|
||||
}
|
||||
|
||||
// 2. 图例:在右侧容器内横向排列
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 16px; // 图例项之间间距,避免重叠
|
||||
align-items: center;
|
||||
// 移除原margin-left,避免位置偏移
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
white-space: nowrap; // 防止图例文字换行
|
||||
}
|
||||
|
||||
.legend-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.legend-icon.line {
|
||||
width: 12px;
|
||||
height: 2px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: -2px;
|
||||
left: 3px;
|
||||
width: 6px;
|
||||
border-radius: 50%;
|
||||
height: 6px;
|
||||
background-color: rgba(40, 138, 255, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.legend-icon.square {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
// 图例颜色
|
||||
.yield {
|
||||
background: rgba(40, 138, 255, 1);
|
||||
}
|
||||
|
||||
.target {
|
||||
background: #2889FF;
|
||||
}
|
||||
|
||||
.achieved {
|
||||
background: rgba(40, 203, 151, 1);
|
||||
}
|
||||
|
||||
.unachieved {
|
||||
background: rgba(255, 132, 0, 1);
|
||||
}
|
||||
|
||||
// 3. 按钮组:在右侧容器内,保留原有样式
|
||||
.button-group {
|
||||
display: flex;
|
||||
position: relative;
|
||||
gap: 2px;
|
||||
width: 283px;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
background: #ecf4fe;
|
||||
border-radius: 12px;
|
||||
// 移除原margin-right,由右侧容器统一控制
|
||||
margin: 0;
|
||||
|
||||
.item-button {
|
||||
cursor: pointer;
|
||||
width: 142px;
|
||||
height: 24px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #0b58ff;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
letter-spacing: 8px; // 确保文字间距生效
|
||||
padding-left: 8px; // 抵消letter-spacing导致的文字左偏
|
||||
}
|
||||
|
||||
.item-button.active {
|
||||
width: 142px;
|
||||
height: 24px;
|
||||
background: #3071ff;
|
||||
border-radius: 12px;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
202
src/views/home/depreciationAnalysisComponents/totalOverview.vue
Normal file
202
src/views/home/depreciationAnalysisComponents/totalOverview.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container :name="title" icon="cockpitItemIcon" size="operatingRevenueBg" 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; gap: 8px;">
|
||||
<div class="dashboard">
|
||||
<div class="title">
|
||||
累计完成率
|
||||
</div>
|
||||
<div class="number">
|
||||
<div class="yield">
|
||||
{{ ytdData?.rate || 0}}%
|
||||
</div>
|
||||
<div class="mom">
|
||||
同比{{ ytdData?.yoyRate || 0}}%
|
||||
<img v-if="ytdData?.yoyRate >= 0" class="arrow" src="../../../assets/img/topArrow.png" alt="">
|
||||
<img v-else class="arrow" src="../../../assets/img/downArrow.png" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="electricityGauge">
|
||||
<electricityGauge :id=" 'totalG' " :detailData="ytdData" id="totalGauge"></electricityGauge>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="line" style="padding: 0px;">
|
||||
<verticalBarChart :refName=" 'totalVerticalBarChart' " :detailData="ytdData">
|
||||
</verticalBarChart>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Container from './container.vue'
|
||||
import electricityGauge from './electricityGauge.vue'
|
||||
import verticalBarChart from './verticalBarChart.vue'
|
||||
|
||||
// import * as echarts from 'echarts'
|
||||
// import rawItem from './raw-Item.vue'
|
||||
|
||||
export default {
|
||||
name: 'ProductionStatus',
|
||||
components: { Container, electricityGauge, verticalBarChart },
|
||||
// mixins: [resize],
|
||||
props: {
|
||||
ytdData: { // 接收父组件传递的设备数据数组
|
||||
type: Object,
|
||||
default: () => {} // 默认空数组,避免报错
|
||||
},
|
||||
title: { // 接收父组件传递的设备数据数组
|
||||
type: String,
|
||||
default: () => '' // 默认空数组,避免报错
|
||||
},
|
||||
month: { // 接收父组件传递的设备数据数组
|
||||
type: String,
|
||||
default: () => '' // 默认空数组,避免报错
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
itemData: {
|
||||
handler(newValue, oldValue) {
|
||||
// this.updateChart()
|
||||
},
|
||||
deep: true // 若对象内属性变化需触发,需加 deep: true
|
||||
}
|
||||
},
|
||||
// computed: {
|
||||
// // 处理排序:包含“总成本”的项放前面,其余项按原顺序排列
|
||||
// sortedItemData() {
|
||||
// // 过滤出包含“总成本”的项(不区分大小写)
|
||||
// const totalCostItems = this.itemData.filter(item =>
|
||||
// item.name && item.name.includes('总成本')
|
||||
// );
|
||||
// // 过滤出不包含“总成本”的项
|
||||
// const otherItems = this.itemData.filter(item =>
|
||||
// !item.name || !item.name.includes('总成本')
|
||||
// );
|
||||
// // 合并:总成本项在前,其他项在后
|
||||
// return [...totalCostItems, ...otherItems];
|
||||
// }
|
||||
// },
|
||||
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;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
width: 264px;
|
||||
height: 205px;
|
||||
background: #F9FCFF;
|
||||
padding: 16px 0 0 10px;
|
||||
.title {
|
||||
// width: 190px;
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-family: YouSheBiaoTiHei;
|
||||
font-size: 46px;
|
||||
color: #0B58FF;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.mom {
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 500px;
|
||||
height: 205px;
|
||||
background: #F9FCFF;
|
||||
}
|
||||
|
||||
// .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> -->
|
||||
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<div style="width: 100%; height: 210px;position: relative;">
|
||||
<div style='font-size: 16px;position: absolute;right: 20px;top:10px'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{detailData.rate}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:detailData.flags>0?'#30B590':'#FF9423'}" >{{detailData.diff}}</span></span>
|
||||
</div>
|
||||
<div :ref="refName" id="coreLineChart" style="width: 100%; height: 210px;"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
myChart: null // 存储图表实例,避免重复创建
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 明确接收的props结构,增强可读性
|
||||
refName: {
|
||||
type: String,
|
||||
default: () => 'verticalBarChart',
|
||||
},
|
||||
detailData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
}),
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.updateChart();
|
||||
});
|
||||
},
|
||||
|
||||
// 新增:监听 chartData 变化
|
||||
watch: {
|
||||
// 深度监听数据变化,仅更新图表配置(不销毁实例)
|
||||
detailData: {
|
||||
handler() {
|
||||
console.log(this.chartData, 'chartData');
|
||||
|
||||
this.updateChart();
|
||||
},
|
||||
deep: true,
|
||||
immediate: true // 初始化时立即执行
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getRateFlag(rate, real, target) {
|
||||
if (isNaN(rate) || rate === null || rate === undefined) return 0;
|
||||
|
||||
// 1. 完成率 >= 100 => 达标
|
||||
if (rate >= 100) return 1;
|
||||
|
||||
// 2. 完成率 = 0 且 (目标值=0 或 实际值=目标值=0) => 达标
|
||||
if (rate === 0 && target === 0) return 1;
|
||||
|
||||
// 其他情况 => 未达标
|
||||
return 0;
|
||||
},
|
||||
updateChart() {
|
||||
const chartDom = this.$refs[this.refName];
|
||||
if (!chartDom) {
|
||||
console.error('图表容器未找到!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose();
|
||||
}
|
||||
|
||||
this.myChart = echarts.init(chartDom);
|
||||
const diff = this.detailData.diff || 0
|
||||
const rate = this.detailData.rate || 0
|
||||
const flagValue = this.getRateFlag(this.detailData.rate, this.detailData.real, this.detailData.target) || 0
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
},
|
||||
// formatter: (params) => {
|
||||
// let html = `${params[0].axisValue}<br/>`;
|
||||
// params.forEach(item => {
|
||||
// const unit = item.seriesName === '完成率' ? '%' : (
|
||||
// ['产量', '销量'].includes(this.$parent.selectedProfit) ? '片' : '万元'
|
||||
// );
|
||||
// html += `${item.marker} ${item.seriesName}: ${item.value}${unit}<br/>`;
|
||||
// });
|
||||
// return html;
|
||||
// }
|
||||
},
|
||||
grid: {
|
||||
top: 40,
|
||||
bottom: 15,
|
||||
right: 80,
|
||||
left: 10,
|
||||
containLabel: true,
|
||||
show: false // 隐藏grid背景,避免干扰
|
||||
},
|
||||
xAxis: {
|
||||
// 横向柱状图的x轴必须设为数值轴,否则无法正常展示数值
|
||||
type: 'value',
|
||||
// offset: 0,
|
||||
// boundaryGap: true ,
|
||||
// boundaryGap: [10, 0], // 可根据需要开启,控制轴的留白
|
||||
axisTick: { show: false },
|
||||
min: 0,
|
||||
//
|
||||
splitNumber: 4,
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
interval: 0,
|
||||
padding: [5, 0, 0, 0]
|
||||
},
|
||||
// data: xData // 数值轴不需要手动设置data,由series的数据自动生成
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.75)',
|
||||
fontSize: 12,
|
||||
interval: 0,
|
||||
padding: [5, 0, 0, 0]
|
||||
},
|
||||
axisLine: {
|
||||
show: true, // 显示Y轴轴线(关键)
|
||||
lineStyle: {
|
||||
color: '#E5E6EB', // 轴线颜色(浅灰色,可自定义)
|
||||
width: 1, // 轴线宽度
|
||||
type: 'solid' // 实线(可选:dashed虚线、dotted点线)
|
||||
}
|
||||
},
|
||||
axisTick: { show: false },
|
||||
// padding: [300, 100, 100, 100],
|
||||
data: ['实际', '预算'] // y轴分类:实际、预算
|
||||
},
|
||||
series: [
|
||||
{
|
||||
// name: '预算',
|
||||
type: 'bar',
|
||||
barWidth: 24,
|
||||
// barCategoryGap: '50', // 柱子之间的间距(相对于柱子宽度)
|
||||
// 数据长度与yAxis的分类数量匹配(实际、预算各一个值)
|
||||
data: [{
|
||||
value: this.detailData.real,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
fontSize: 14,
|
||||
},
|
||||
itemStyle: {
|
||||
color: flagValue === 1
|
||||
? {
|
||||
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)' }
|
||||
]
|
||||
}
|
||||
: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(253, 209, 129, 1)' },
|
||||
{ offset: 1, color: 'rgba(249, 164, 74, 1)' }
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
}
|
||||
}, {
|
||||
value: this.detailData.target,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
fontSize: 14,
|
||||
},
|
||||
itemStyle: {
|
||||
// 预算的渐变颜色(蓝系渐变)
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 1, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: '#82CCFF' }, // 浅蓝
|
||||
{ offset: 1, color: '#4B9DFF' } // 深蓝
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
},],
|
||||
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
option && this.myChart.setOption(option);
|
||||
|
||||
// 窗口缩放适配和销毁逻辑保持不变
|
||||
window.addEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
|
||||
this.$once('hook:destroyed', () => {
|
||||
window.removeEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
this.myChart && this.myChart.dispose();
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container :name="title" icon="cockpitItemIcon" size="operatingRevenueBg" topSize="middle">
|
||||
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%;">
|
||||
<div class="topItem-container" style="display: flex; gap: 8px; width: 100%;">
|
||||
<!-- 销量模块(直接传递整合了flag的salesData) -->
|
||||
<div class="dashboard left" @click="handleDashboardClick('/salesVolumeAnalysis/salesVolumeAnalysisBase')">
|
||||
<div style='position: relative;'>
|
||||
<div class="title">
|
||||
销量·万㎡
|
||||
</div>
|
||||
<div style='font-size: 16px;position: absolute;top:-4px;right:15px'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{ytdAnalysis[0].rate}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:ytdAnalysis[0].flags>0?'#30B590':'#FF9423'}" >{{ytdAnalysis[0].diff}}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<operatingSingleBar :detailData="salesData"></operatingSingleBar>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 单价模块(直接传递整合了flag的unitPriceData) -->
|
||||
<div class="dashboard right" @click="handleDashboardClick('/unitPriceAnalysis/unitPriceAnalysisBase')">
|
||||
<div style='position: relative;'>
|
||||
<div class="title">
|
||||
单价·元/㎡
|
||||
</div>
|
||||
<div style='font-size: 16px;position: absolute;top:-4px;right:15px'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{ytdAnalysis[1].rate}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:ytdAnalysis[1].flags>0?'#30B590':'#FF9423'}" >{{ytdAnalysis[1].diff}}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<operatingSingleBar :detailData="unitPriceData"></operatingSingleBar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Container from './container.vue'
|
||||
import operatingSingleBar from './operatingSingleBar.vue'
|
||||
|
||||
export default {
|
||||
name: 'ProductionStatus',
|
||||
components: { Container, operatingSingleBar },
|
||||
props: {
|
||||
ytdAnalysis: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{ title: "销量", budget: 0, real: 0, rate: 0, diff: 0 },
|
||||
{ title: "单价", budget: 0, real: 0, rate: 0, diff: 0 }
|
||||
]
|
||||
},
|
||||
dateData: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
month: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
// 初始化数据包含flag字段
|
||||
salesData: { title: "销量", budget: 0, real: 0, rate: 0, diff: 0, flag: 0 },
|
||||
unitPriceData: { title: "单价", budget: 0, real: 0, rate: 0, diff: 0, flag: 0 }
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
ytdAnalysis: {
|
||||
handler(newVal) {
|
||||
this.updateChart(newVal)
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateChart(this.ytdAnalysis)
|
||||
},
|
||||
methods: {
|
||||
handleDashboardClick(path) {
|
||||
this.$router.push({
|
||||
path: path,
|
||||
query: {
|
||||
factory: this.$route.query.factory ? this.$route.query.factory : 5,
|
||||
dateData: this.dateData
|
||||
}
|
||||
})
|
||||
},
|
||||
// 判断flag的核心方法
|
||||
getRateFlag(rate, real, target) {
|
||||
if (isNaN(rate) || rate === null || rate === undefined) return 0;
|
||||
|
||||
// 1. 完成率 >= 100 => 达标
|
||||
if (rate >= 100) return 1;
|
||||
|
||||
// 2. 完成率 = 0 且 (目标值=0 或 实际值=目标值=0) => 达标
|
||||
if (rate === 0 && target === 0) return 1;
|
||||
|
||||
// 其他情况 => 未达标
|
||||
return 0;
|
||||
},
|
||||
|
||||
updateChart(data) {
|
||||
// 数据兜底
|
||||
const salesItem = Array.isArray(data) && data[0] ? data[0] : { title: "销量", budget: 0, real: 0, rate: 0, diff: 0 };
|
||||
const unitPriceItem = Array.isArray(data) && data[1] ? data[1] : { title: "单价", budget: 0, real: 0, rate: 0, diff: 0 };
|
||||
|
||||
// 核心修改:将flag整合到数据对象中,无需单独定义salesFlag/unitPriceFlag
|
||||
this.salesData = {
|
||||
...salesItem, // 合并原有字段
|
||||
flag: this.getRateFlag(salesItem.rate, salesItem.real, salesItem.budget) // 新增flag字段
|
||||
};
|
||||
|
||||
this.unitPriceData = {
|
||||
...unitPriceItem, // 合并原有字段
|
||||
flag: this.getRateFlag(unitPriceItem.rate, unitPriceItem.real, unitPriceItem.budget) // 新增flag字段
|
||||
};
|
||||
|
||||
// 调试:确认整合后的数据
|
||||
console.log('整合flag后的销量数据:', this.salesData);
|
||||
console.log('整合flag后的单价数据:', this.unitPriceData);
|
||||
}
|
||||
}
|
||||
}
|
||||
</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;
|
||||
}
|
||||
|
||||
.topItem-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
height: 205px;
|
||||
background: #F9FCFF;
|
||||
padding: 16px 0 0 10px;
|
||||
margin: 0 4px;
|
||||
|
||||
.title {
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 2px;
|
||||
text-align: left;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chart-wrap {
|
||||
width: 100%;
|
||||
height: calc(100% - 30px);
|
||||
}
|
||||
|
||||
.number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
height: 32px;
|
||||
font-family: YouSheBiaoTiHei;
|
||||
font-size: 32px;
|
||||
color: #0B58FF;
|
||||
line-height: 32px;
|
||||
letter-spacing: 2px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mom {
|
||||
width: 97px;
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: left;
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard.left {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.dashboard.right {
|
||||
margin-right: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,257 @@
|
||||
<template>
|
||||
<div id="dayReport" class="dayReport" :style="styles">
|
||||
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
|
||||
<sidebar v-if="!sidebar.hide" class="sidebar-container" />
|
||||
<ReportHeader :dateData="dateData" top-title="电费分析" :is-full-screen="isFullScreen"
|
||||
@screenfullChange="screenfullChange" @timeRangeChange="handleTimeChange" />
|
||||
<div class="main-body" style="
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding: 0px 16px 0 272px;
|
||||
flex-direction: column;
|
||||
">
|
||||
<div class="top" style="margin-top: -20px; display: flex; gap: 16px">
|
||||
<div class="top-three" style="
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns:1624px;
|
||||
">
|
||||
<operatingLineChart :dateData="dateData" :monData="monData" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="top" style="display: flex; gap: 16px;margin-top: 6px;">
|
||||
<div class="left-three" style="
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 1624px;
|
||||
">
|
||||
<operatingLineChartCumulative :dateData="dateData" :totalData="totalData" />
|
||||
<!-- <keyWork /> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="centerImg" style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1; /* 确保在 backp 之上、内容之下 */
|
||||
"></div> -->
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import ReportHeader from "../components/noRouterHeader.vue";
|
||||
import { Sidebar } from "../../../layout/components";
|
||||
import screenfull from "screenfull";
|
||||
// import operatingSalesRevenue from "./operatingComponents/operatingSalesRevenue";
|
||||
// import premProdStatus from "./components/premProdStatus.vue";
|
||||
import { mapState } from "vuex";
|
||||
import operatingLineChart from "../electricityCostAnalysisComponents/operatingLineChart";
|
||||
import operatingLineChartCumulative from "../electricityCostAnalysisComponents/operatingLineChartCumulative.vue";
|
||||
|
||||
import { getProfitAnalysisTotalList } from '@/api/cockpit'
|
||||
import moment from "moment";
|
||||
export default {
|
||||
name: "DayReport",
|
||||
components: {
|
||||
ReportHeader,
|
||||
operatingLineChartCumulative,
|
||||
operatingLineChart,
|
||||
// premProdStatus,
|
||||
Sidebar,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
weekArr: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
|
||||
isFullScreen: false,
|
||||
timer: null,
|
||||
beilv: 1,
|
||||
value: 100,
|
||||
dateData: {},
|
||||
monData: [],
|
||||
totalData: [],
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.init();
|
||||
this.windowWidth(document.documentElement.clientWidth);
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
theme: (state) => state.settings.theme,
|
||||
sideTheme: (state) => state.settings.sideTheme,
|
||||
sidebar: (state) => state.app.sidebar,
|
||||
device: (state) => state.app.device,
|
||||
needTagsView: (state) => state.settings.tagsView,
|
||||
fixedHeader: (state) => state.settings.fixedHeader,
|
||||
}),
|
||||
classObj() {
|
||||
return {
|
||||
hideSidebar: !this.sidebar.opened,
|
||||
openSidebar: this.sidebar.opened,
|
||||
withoutAnimation: this.sidebar.withoutAnimation,
|
||||
mobile: this.device === "mobile",
|
||||
};
|
||||
},
|
||||
variables() {
|
||||
return variables;
|
||||
},
|
||||
// ...mapGetters(['sidebar']),
|
||||
styles() {
|
||||
const v = Math.floor(this.value * this.beilv * 100) / 10000;
|
||||
return {
|
||||
transform: `scale(${v})`,
|
||||
transformOrigin: "left top",
|
||||
// overflow: hidden;
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
clientWidth(val) {
|
||||
if (!this.timer) {
|
||||
this.clientWidth = val;
|
||||
this.beilv2 = this.clientWidth / 1920;
|
||||
this.timer = true;
|
||||
let _this = this;
|
||||
setTimeout(function () {
|
||||
_this.timer = false;
|
||||
}, 500);
|
||||
}
|
||||
// 这里可以添加修改时的方法
|
||||
this.windowWidth(val);
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.timer);
|
||||
this.destroy();
|
||||
},
|
||||
mounted() {
|
||||
const _this = this;
|
||||
_this.beilv = document.documentElement.clientWidth / 1920;
|
||||
window.onresize = () => {
|
||||
return (() => {
|
||||
_this.clientWidth = `${document.documentElement.clientWidth}`;
|
||||
this.beilv = _this.clientWidth / 1920;
|
||||
})();
|
||||
};
|
||||
this.dateData = this.$route.query.dateData ? this.$route.query.dateData : undefined
|
||||
},
|
||||
methods: {
|
||||
getData() {
|
||||
getProfitAnalysisTotalList({
|
||||
startTime: this.dateData.startTime,
|
||||
endTime: this.dateData.endTime,
|
||||
analysisObject: [
|
||||
"利润总额"
|
||||
],
|
||||
levelId: 1,
|
||||
// timeDim: this.dateData.mode
|
||||
}).then((res) => {
|
||||
console.log(res);
|
||||
this.monData = res.data.currentMonthData
|
||||
this.totalData = res.data.totalMonthData
|
||||
// this.totalData = res.data.totalData
|
||||
// this.saleData = res.data.SaleData
|
||||
// this.premiumProduct = res.data.premiumProduct
|
||||
// this.salesTrendMap = res.data.salesTrendMap
|
||||
// this.grossMarginTrendMap = res.data.grossMarginTrendMap
|
||||
// this.salesProportion = res.data.salesProportion ? res.data.salesProportion : {}
|
||||
})
|
||||
},
|
||||
handleTimeChange(obj) {
|
||||
// console.log(obj, 'obj');
|
||||
this.dateData= obj
|
||||
this.getData()
|
||||
},
|
||||
handleClickOutside() {
|
||||
this.$store.dispatch("app/closeSideBar", { withoutAnimation: false });
|
||||
},
|
||||
windowWidth(value) {
|
||||
this.clientWidth = value;
|
||||
this.beilv2 = this.clientWidth / 1920;
|
||||
},
|
||||
change() {
|
||||
this.isFullScreen = screenfull.isFullscreen;
|
||||
},
|
||||
init() {
|
||||
if (!screenfull.isEnabled) {
|
||||
this.$message({
|
||||
message: "you browser can not work",
|
||||
type: "warning",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
screenfull.on("change", this.change);
|
||||
},
|
||||
destroy() {
|
||||
if (!screenfull.isEnabled) {
|
||||
this.$message({
|
||||
message: "you browser can not work",
|
||||
type: "warning",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
screenfull.off("change", this.change);
|
||||
},
|
||||
// 全屏
|
||||
screenfullChange() {
|
||||
console.log("screenfull.enabled", screenfull.isEnabled);
|
||||
|
||||
if (!screenfull.isEnabled) {
|
||||
this.$message({
|
||||
message: "you browser can not work",
|
||||
type: "warning",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
screenfull.toggle(this.$refs.dayReportB);
|
||||
},
|
||||
// 导出
|
||||
// exportPDF() {
|
||||
// this.$message.success('正在导出,请稍等!')
|
||||
// const element = document.getElementById('dayRepDom')
|
||||
// element.style.display = 'block'
|
||||
// const fileName = '株洲碲化镉生产日报' + moment().format('yyMMDD') + '.pdf'
|
||||
// html2canvas(element, {
|
||||
// dpi: 300, // Set to 300 DPI
|
||||
// scale: 3 // Adjusts your resolution
|
||||
// }).then(function(canvas) {
|
||||
// const imgWidth = 595.28
|
||||
// const imgHeight = 841.89
|
||||
// const pageData = canvas.toDataURL('image/jpeg', 1.0)
|
||||
// const PDF = new JsPDF('', 'pt', [imgWidth, imgHeight])
|
||||
// PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight)
|
||||
// setTimeout(() => {
|
||||
// PDF.save(fileName) // 导出文件名
|
||||
// }, 1000)
|
||||
// })
|
||||
// element.style.display = 'none'
|
||||
// }
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "~@/assets/styles/mixin.scss";
|
||||
@import "~@/assets/styles/variables.scss";
|
||||
|
||||
.dayReport {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: url("../../../assets/img/backp.png") no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.hideSidebar .fixed-header {
|
||||
width: calc(100% - 54px);
|
||||
}
|
||||
|
||||
.sidebarHide .fixed-header {
|
||||
width: calc(100%);
|
||||
}
|
||||
|
||||
.mobile .fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,351 @@
|
||||
<template>
|
||||
<div id="dayReport" class="dayReport" :style="styles">
|
||||
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
|
||||
<sidebar v-if="!sidebar.hide" class="sidebar-container" />
|
||||
<ReportHeader :dateData="dateData" size="psi" @timeRangeChange="handleTimeChange" top-title="基地电费分析"
|
||||
:is-full-screen="isFullScreen" @screenfullChange="screenfullChange" />
|
||||
<div class="main-body" style="
|
||||
margin-top: -20px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding: 0px 16px 0 272px;
|
||||
flex-direction: column;
|
||||
">
|
||||
<div class="top" style="display: flex; gap: 16px">
|
||||
<div class="top-three" style="
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 1624px;
|
||||
">
|
||||
<changeBase :factory="factory" @baseChange="selectChange" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="top" style="display: flex; gap: 16px;margin-top: -20px;">
|
||||
<div class="left-three" style="
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 804px 804px;
|
||||
">
|
||||
<monthlyOverview :month="month" :monData="monData" :title="'月度概览'" />
|
||||
<totalOverview :totalData="totalData" :title="'累计概览'" />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="middle" style="display: flex; gap: 16px;margin-top: 6px;">
|
||||
<div class="left-three" style="
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 1624px;
|
||||
">
|
||||
<relatedIndicatorsAnalysis :dateData="dateData" :factory="factory" :relatedData="relatedData"
|
||||
:title="'相关指标分析'" />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom" style="display: flex; gap: 16px;margin-top: 6px;">
|
||||
<div class="left-three" style="
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 1624px;
|
||||
">
|
||||
<dataTrend @getData="changeItem" :trendData="trend" :title="'数据趋势'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="centerImg" style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1; /* 确保在 backp 之上、内容之下 */
|
||||
"></div> -->
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import ReportHeader from "../components/noRouterHeader.vue";
|
||||
import { Sidebar } from "../../../layout/components";
|
||||
import screenfull from "screenfull";
|
||||
import changeBase from "../components/changeBase.vue";
|
||||
import monthlyOverview from "../electricityCostAnalysisComponents/monthlyOverview.vue";
|
||||
import totalOverview from "../electricityCostAnalysisComponents/totalOverview.vue";
|
||||
// import totalOverview from "../operatingComponents/totalOverview.vue";
|
||||
import relatedIndicatorsAnalysis from "../electricityCostAnalysisComponents/relatedIndicatorsAnalysis.vue";
|
||||
import dataTrend from "../electricityCostAnalysisComponents/dataTrend.vue";
|
||||
import { mapState } from "vuex";
|
||||
import { getProfitAnalysisTotalList } from '@/api/cockpit'
|
||||
// import PSDO from "./components/PSDO.vue";
|
||||
// import psiLineChart from "./components/psiLineChart.vue";
|
||||
|
||||
// import coreBottomLeft from "./components/coreBottomLeft.vue";
|
||||
// import orderProgress from "./components/orderProgress.vue";
|
||||
// import keyWork from "./components/keyWork.vue";
|
||||
import moment from "moment";
|
||||
// import html2canvas from 'html2canvas'
|
||||
// import JsPDF from 'jspdf'
|
||||
export default {
|
||||
name: "DayReport",
|
||||
components: {
|
||||
ReportHeader,
|
||||
changeBase,
|
||||
monthlyOverview,
|
||||
Sidebar,
|
||||
totalOverview,
|
||||
dataTrend,
|
||||
relatedIndicatorsAnalysis
|
||||
// psiLineChart
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isFullScreen: false,
|
||||
timer: null,
|
||||
beilv: 1,
|
||||
month: '',
|
||||
value: 100,
|
||||
factory: 5,
|
||||
dateData: {},
|
||||
monData: {},
|
||||
totalData: {},
|
||||
trend: [],
|
||||
relatedData: [],
|
||||
trendName: '利润总额',
|
||||
// cusProData: {},
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.init();
|
||||
this.windowWidth(document.documentElement.clientWidth);
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
theme: (state) => state.settings.theme,
|
||||
sideTheme: (state) => state.settings.sideTheme,
|
||||
sidebar: (state) => state.app.sidebar,
|
||||
device: (state) => state.app.device,
|
||||
needTagsView: (state) => state.settings.tagsView,
|
||||
fixedHeader: (state) => state.settings.fixedHeader,
|
||||
}),
|
||||
classObj() {
|
||||
return {
|
||||
hideSidebar: !this.sidebar.opened,
|
||||
openSidebar: this.sidebar.opened,
|
||||
withoutAnimation: this.sidebar.withoutAnimation,
|
||||
mobile: this.device === "mobile",
|
||||
};
|
||||
},
|
||||
variables() {
|
||||
return variables;
|
||||
},
|
||||
// ...mapGetters(['sidebar']),
|
||||
styles() {
|
||||
const v = Math.floor(this.value * this.beilv * 100) / 10000;
|
||||
return {
|
||||
transform: `scale(${v})`,
|
||||
transformOrigin: "left top",
|
||||
// overflow: hidden;
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
clientWidth(val) {
|
||||
if (!this.timer) {
|
||||
this.clientWidth = val;
|
||||
this.beilv2 = this.clientWidth / 1920;
|
||||
this.timer = true;
|
||||
let _this = this;
|
||||
setTimeout(function () {
|
||||
_this.timer = false;
|
||||
}, 500);
|
||||
}
|
||||
// 这里可以添加修改时的方法
|
||||
this.windowWidth(val);
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.timer);
|
||||
this.destroy();
|
||||
},
|
||||
mounted() {
|
||||
const _this = this;
|
||||
_this.beilv = document.documentElement.clientWidth / 1920;
|
||||
window.onresize = () => {
|
||||
return (() => {
|
||||
_this.clientWidth = `${document.documentElement.clientWidth}`;
|
||||
this.beilv = _this.clientWidth / 1920;
|
||||
})();
|
||||
};
|
||||
if(this.$route.query.factory){
|
||||
this.factory =Number(this.$route.query.factory)
|
||||
}else if(this.$store.getters.levelList.length > 0 && this.$store.getters.levelList[0].id !== 1) {
|
||||
this.factory = this.$store.getters.levelList[0].id
|
||||
}else{
|
||||
this.factory = this.$store.getters.levelList[1].id
|
||||
}
|
||||
this.dateData = this.$route.query.dateData ? this.$route.query.dateData : undefined
|
||||
},
|
||||
methods: {
|
||||
handleChange(value) {
|
||||
this.index = value
|
||||
this.getData()
|
||||
},
|
||||
changeItem(item) {
|
||||
console.log('item', item);
|
||||
|
||||
this.trendName = item
|
||||
this.getData()
|
||||
},
|
||||
getData() {
|
||||
const requestParams = {
|
||||
startTime: this.dateData.startTime,
|
||||
endTime: this.dateData.endTime,
|
||||
// index: this.index,
|
||||
// sort: 1,
|
||||
trendName: this.trendName,
|
||||
analysisObject: [
|
||||
"利润总额",
|
||||
],
|
||||
// paramList: ['制造成本', '财务费用', '销售费用', '管理费用', '运费'],
|
||||
levelId: this.factory,
|
||||
// baseId: Number(this.factory),
|
||||
};
|
||||
// 调用接口
|
||||
getProfitAnalysisTotalList(requestParams).then((res) => {
|
||||
this.monData = res.data.currentMonthData.find(item => {
|
||||
return item.name === "利润总额";
|
||||
});
|
||||
console.log('this.monData', this.monData);
|
||||
|
||||
this.totalData = res.data.totalMonthData.find(item => {
|
||||
return item.name === "利润总额";
|
||||
});
|
||||
// this.relatedMon = res.data.relatedMon
|
||||
this.relatedData = {
|
||||
relatedMon: res.data.currentMonthData.filter(item => {
|
||||
return item.name !== "利润总额";
|
||||
}), // 兜底月度数据
|
||||
relatedTotal: res.data.totalMonthData.filter(item => {
|
||||
return item.name !== "利润总额";
|
||||
}) // 兜底累计数据
|
||||
}
|
||||
this.trend = res.data.dataTrend
|
||||
});
|
||||
},
|
||||
|
||||
handleTimeChange(obj) {
|
||||
this.month = obj.targetMonth
|
||||
this.dateData = {
|
||||
startTime: obj.startTime,
|
||||
endTime: obj.endTime,
|
||||
// mode: obj.mode,
|
||||
}
|
||||
|
||||
this.getData()
|
||||
},
|
||||
selectChange(data) {
|
||||
console.log('选中的数据:', data);
|
||||
this.factory = data
|
||||
if (this.dateData.startTime && this.dateData.endTime) {
|
||||
this.getData();
|
||||
}
|
||||
},
|
||||
handleClickOutside() {
|
||||
this.$store.dispatch("app/closeSideBar", { withoutAnimation: false });
|
||||
},
|
||||
windowWidth(value) {
|
||||
this.clientWidth = value;
|
||||
this.beilv2 = this.clientWidth / 1920;
|
||||
},
|
||||
change() {
|
||||
this.isFullScreen = screenfull.isFullscreen;
|
||||
},
|
||||
init() {
|
||||
if (!screenfull.isEnabled) {
|
||||
this.$message({
|
||||
message: "you browser can not work",
|
||||
type: "warning",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
screenfull.on("change", this.change);
|
||||
},
|
||||
destroy() {
|
||||
if (!screenfull.isEnabled) {
|
||||
this.$message({
|
||||
message: "you browser can not work",
|
||||
type: "warning",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
screenfull.off("change", this.change);
|
||||
},
|
||||
// 全屏
|
||||
screenfullChange() {
|
||||
console.log("screenfull.enabled", screenfull.isEnabled);
|
||||
|
||||
if (!screenfull.isEnabled) {
|
||||
this.$message({
|
||||
message: "you browser can not work",
|
||||
type: "warning",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
screenfull.toggle(this.$refs.dayReportB);
|
||||
},
|
||||
changeDate(val) {
|
||||
this.date = val;
|
||||
// this.weekDay = this.weekArr[moment(this.date).format('e')]
|
||||
// this.getData()
|
||||
if (this.date === moment().format("yyyy-MM-DD")) {
|
||||
this.loopTime();
|
||||
} else {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
},
|
||||
// 导出
|
||||
// () {
|
||||
// this.$message.success('正在导出,请稍等!')
|
||||
// const element = document.getElementById('dayRepDom')
|
||||
// element.style.display = 'block'
|
||||
// const fileName = '株洲碲化镉生产日报' + moment().format('yyMMDD') + '.pdf'
|
||||
// html2canvas(element, {
|
||||
// dpi: 300, // Set to 300 DPI
|
||||
// scale: 3 // Adjusts your resolution
|
||||
// }).then(function(canvas) {
|
||||
// const imgWidth = 595.28
|
||||
// const imgHeight = 841.89
|
||||
// const pageData = canvas.toDataURL('image/jpeg', 1.0)
|
||||
// const PDF = new JsPDF('', 'pt', [imgWidth, imgHeight])
|
||||
// PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight)
|
||||
// setTimeout(() => {
|
||||
// PDF.save(fileName) // 导出文件名
|
||||
// }, 1000)
|
||||
// })
|
||||
// element.style.display = 'none'
|
||||
// }
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "~@/assets/styles/mixin.scss";
|
||||
@import "~@/assets/styles/variables.scss";
|
||||
|
||||
.dayReport {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: url("../../../assets/img/backp.png") no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.hideSidebar .fixed-header {
|
||||
width: calc(100% - 54px);
|
||||
}
|
||||
|
||||
.sidebarHide .fixed-header {
|
||||
width: calc(100%);
|
||||
}
|
||||
|
||||
.mobile .fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
419
src/views/home/electricityCostAnalysisComponents/Header.vue
Normal file
419
src/views/home/electricityCostAnalysisComponents/Header.vue
Normal file
@@ -0,0 +1,419 @@
|
||||
<template>
|
||||
<header class="report-header">
|
||||
<!-- 左侧区域:logo + 标题 -->
|
||||
<div class="left-content">
|
||||
<img style="height: 36px;" src="../../../assets/img/cnbm.png" alt="benmaLogo" >
|
||||
<div class="top-title">{{ topTitle }}</div>
|
||||
</div>
|
||||
|
||||
<div class="center-content">
|
||||
<!-- 循环 pageRoutes,不再硬编码文字 -->
|
||||
<div class="item" v-for="(page, index) in pageRoutes" :key="index" @click="goToPage(page.path, index)">
|
||||
<span class="item-text">{{ page.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- :class="{ 'no-skew': activeIndex === index }
|
||||
" -->
|
||||
<!-- 右侧区域:全屏按钮 -->
|
||||
<div class="right-content">
|
||||
<el-button type="text" class="screen-btn" :title="isFullScreen ? '退出全屏' : '全屏'" @click="changeFullScreen">
|
||||
<svg-icon style="color: #0B58FF;" v-if="isFullScreen" icon-class="unFullScreenView" />
|
||||
<svg-icon style="color: #0B58FF;" v-else icon-class="fullScreenView" />
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 时间选择区域:日/月/年按钮 + label + 日期选择器 -->
|
||||
<div class="timeType">
|
||||
<!-- <div class="item" v-for="(item, index) in timeTypes" :key="index" @click="activeTime = index"
|
||||
:class="{ 'no-skew': activeTime === index }">
|
||||
<span class="item-text">{{ item.text }}</span>
|
||||
</div> -->
|
||||
<div class="dateP">
|
||||
<div class="label">
|
||||
<span class="label-text">月份选择</span>
|
||||
</div>
|
||||
<el-date-picker v-model="date" :type="getPickerType" :placeholder="getPickerPlaceholder"
|
||||
class="custom-date-picker" style="width: 132px;height: 29px;" @change="emitTimeRange" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment'
|
||||
export default {
|
||||
name: 'Header',
|
||||
props: {
|
||||
isFullScreen: { type: Boolean, default: false },
|
||||
topTitle: { type: String, default: '' }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentTime: '',
|
||||
timeTimer: null,
|
||||
date: undefined,
|
||||
activeIndex: -1,
|
||||
activeTime: 1, // 0=日,1=月,2=年(默认选中“日”)
|
||||
pageRoutes: [
|
||||
{ text: '营业收入', path: '/operatingRevenue/operatingRevenueIndex' },
|
||||
{ text: '利润分析', path: '/profitAnalysis' },
|
||||
{ text: '产销率库存分析', path: '/PSIAnal' },
|
||||
{ text: '成本分析', path: '/cost/cost' },
|
||||
{ text: '驾驶舱报表', path: '/cockpit' }
|
||||
],
|
||||
// 定义时间类型配置:text=按钮文字,pickerType=选择器类型,placeholder=占位符
|
||||
timeTypes: [
|
||||
{ text: '日', pickerType: 'date', placeholder: '选择日期' },
|
||||
{ text: '月', pickerType: 'month', placeholder: '选择月份' },
|
||||
{ text: '年', pickerType: 'year', placeholder: '选择年份' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 动态获取日期选择器类型
|
||||
getPickerType() {
|
||||
return this.timeTypes[this.activeTime].pickerType;
|
||||
},
|
||||
// 动态获取日期选择器占位符
|
||||
getPickerPlaceholder() {
|
||||
return this.timeTypes[this.activeTime].placeholder;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToPage(path, index) {
|
||||
// 1. 跳转到对应路由
|
||||
this.$router.push(path);
|
||||
// 2. 更新activeIndex,让当前点击项高亮
|
||||
this.activeIndex = index;
|
||||
},
|
||||
changeFullScreen() { this.$emit('screenfullChange') },
|
||||
padZero(num) { return num < 10 ? '0' + num : num },
|
||||
/**
|
||||
* 核心方法1:根据维度计算时间区间(首次进入时基于赋值的当月日期,计算“当月第一天0点→次月第一天0点”)
|
||||
* @returns {Object} 包含 start(开始时间)、end(结束时间)、dimension(维度)的区间对象
|
||||
*/
|
||||
calculateTimeRange() {
|
||||
let startTime = 0;
|
||||
let endTime = 0;
|
||||
const mode = this.activeTime + 1; // 1=日,2=月,3=年
|
||||
const defaultMoment = moment(); // 默认当前时间
|
||||
|
||||
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. 日维度:00:00:00 → 23:59:59(无毫秒)
|
||||
if (this.activeTime === 0) {
|
||||
startTime = targetMoment.startOf('day').millisecond(0).valueOf();
|
||||
endTime = targetMoment.endOf('day').millisecond(0).valueOf();
|
||||
}
|
||||
|
||||
// 2. 月维度:当月1日00:00:00 → 当月最后一天23:59:59(无毫秒)
|
||||
else if (this.activeTime === 1) {
|
||||
startTime = targetMoment.startOf('month').millisecond(0).valueOf();
|
||||
endTime = targetMoment.endOf('month').millisecond(0).valueOf();
|
||||
}
|
||||
|
||||
// 3. 年维度:当年1月1日00:00:00 → 当年最后一天23:59:59(无毫秒)
|
||||
else if (this.activeTime === 2) {
|
||||
startTime = targetMoment.startOf('year').millisecond(0).valueOf();
|
||||
endTime = targetMoment.endOf('year').millisecond(0).valueOf();
|
||||
}
|
||||
|
||||
// 调试输出:验证是否去掉毫秒
|
||||
console.log('时间范围计算结果:', {
|
||||
mode,
|
||||
startTime: moment(startTime * 1000).format('YYYY-MM-DD HH:mm:ss'), // 格式:2025-11-30 00:00:00
|
||||
endTime: moment(endTime * 1000).format('YYYY-MM-DD HH:mm:ss'), // 格式:2025-11-30 23:59:59(无毫秒)
|
||||
startTimeStamp: startTime, // 秒级时间戳(如:1764422400)
|
||||
endTimeStamp: endTime // 秒级时间戳(如:1764508799)
|
||||
});
|
||||
|
||||
return { startTime, endTime, mode };
|
||||
},
|
||||
/**
|
||||
* 核心方法2:传递时间区间给父组件(首次进入时触发,传递“当月第一天0点→次月第一天0点”)
|
||||
*/
|
||||
emitTimeRange() {
|
||||
const timeRange = this.calculateTimeRange();
|
||||
this.$emit('timeRangeChange', timeRange);
|
||||
// 调试用:查看首次传递的区间(如{start: "2025-10-01T00:00:00", end: "2025-11-01T00:00:00", dimension: "月"})
|
||||
console.log('当前时间区间:', timeRange);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 维度切换时:清空选择的日期,并传递当前维度的默认区间
|
||||
activeTime(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.date = undefined;
|
||||
// this.emitTimeRange();
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 核心逻辑:首次进入页面,计算当月默认日期并赋值给选择器,同时传递区间
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = this.padZero(now.getMonth() + 1); // 月份从0开始,+1后补零(如1月→01)
|
||||
// 赋值当月默认日期(格式:YYYY-MM,适配month类型选择器)
|
||||
this.date = `${year}-${month}`;
|
||||
// 确保选择器渲染完成后,传递“当月第一天0点→次月第一天0点”的区间
|
||||
this.$nextTick(() => this.emitTimeRange());
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 原有样式不变,仅补充label文字的倾斜抵消样式 */
|
||||
@font-face {
|
||||
font-family: "YouSheBiaoTiHei";
|
||||
src: url('../../../assets/fonts/YouSheBiaoTiHe.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.report-header {
|
||||
height: 117px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
background: url('../../../assets/img/topTitle.png') no-repeat;
|
||||
background-size: cover;
|
||||
background-position: 0 0;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
/* 确保timeType绝对定位生效 */
|
||||
|
||||
.left-content {
|
||||
margin-top: 11px;
|
||||
margin-left: 44px;
|
||||
height: 55px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.top-title {
|
||||
height: 55px;
|
||||
font-family: "YouSheBiaoTiHei", sans-serif;
|
||||
font-size: 42px;
|
||||
color: #1E1651;
|
||||
line-height: 55px;
|
||||
letter-spacing: 6px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.center-content {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 18px;
|
||||
margin-left: 70px;
|
||||
|
||||
.item {
|
||||
width: 180px;
|
||||
height: 50px;
|
||||
background: #E1EEFC;
|
||||
transform: skew(-20deg);
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
color: #1E1651;
|
||||
line-height: 50px;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
box-shadow: 0px 13px 16px 0px rgba(179, 217, 255, 0.43),
|
||||
0px 2px 4px 0px rgba(92, 140, 255, 0.25),
|
||||
inset 0px -43px 13px 0px rgba(255, 255, 255, 0.51);
|
||||
|
||||
.item-text {
|
||||
display: inline-block;
|
||||
transform: skew(20deg);
|
||||
}
|
||||
}
|
||||
|
||||
.item.no-skew {
|
||||
background: none !important;
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
color: #1E1651;
|
||||
|
||||
.item-text {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.timeType {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* 垂直居中,避免元素高低错位 */
|
||||
top: 42px;
|
||||
right:10px;
|
||||
margin-top: 18px;
|
||||
gap: 0;
|
||||
/* 清除间隙,让按钮与选择器紧密连接 */
|
||||
}
|
||||
|
||||
.timeType .item {
|
||||
width: 40px;
|
||||
height: 28px;
|
||||
background: rgba(236, 244, 254, 1);
|
||||
transform: skew(-20deg);
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: rgba(11, 88, 255, 1);
|
||||
line-height: 28px;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
/* 选中按钮与未选中按钮倾斜角度统一,避免切换时跳动 */
|
||||
}
|
||||
|
||||
.timeType .item .item-text {
|
||||
display: inline-block;
|
||||
transform: skew(20deg);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.timeType .item.no-skew {
|
||||
background: rgba(11, 88, 255, 1);
|
||||
color: rgba(249, 252, 255, 1);
|
||||
transform: skew(-20deg) !important;
|
||||
/* 统一倾斜角度,修复原30deg的错位 */
|
||||
box-shadow: 0 2px 8px rgba(11, 88, 255, 0.3);
|
||||
}
|
||||
|
||||
.timeType .item.no-skew .item-text {
|
||||
transform: skew(20deg) !important;
|
||||
/* 同步统一文字倾斜角度 */
|
||||
}
|
||||
|
||||
.dateP {
|
||||
position: relative;
|
||||
margin-left: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.dateP .label {
|
||||
width: 165px;
|
||||
height: 28px;
|
||||
background: rgba(236, 244, 254, 1);
|
||||
transform: skew(-25deg);
|
||||
/* 与按钮倾斜角度统一(原30deg改为25deg,避免视觉错位) */
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #0B58FF;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 补充:label文字抵消倾斜(原代码遗漏,导致文字倾斜) */
|
||||
.dateP .label-text {
|
||||
display: inline-block;
|
||||
transform: skew(25deg);
|
||||
/* 与label倾斜角度相反,确保文字正立 */
|
||||
}
|
||||
|
||||
.right-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 12px;
|
||||
margin-right: 4px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.current-time {
|
||||
color: #FFFFFF;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 500;
|
||||
font-size: 22px;
|
||||
line-height: 24px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.screen-btn {
|
||||
width: 26px;
|
||||
margin-left: 300px;
|
||||
color: #00fff0;
|
||||
font-size: 26px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 日期选择器样式保持不变 */
|
||||
::v-deep .custom-date-picker {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
width: 165px !important;
|
||||
height: 28px !important;
|
||||
position: relative;
|
||||
margin: 0 !important;
|
||||
|
||||
/* 1. 调整输入框文字:确保行高与输入框高度一致,垂直居中 */
|
||||
.el-input__inner {
|
||||
height: 28px !important;
|
||||
width: 165px !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>
|
||||
271
src/views/home/electricityCostAnalysisComponents/container.vue
Normal file
271
src/views/home/electricityCostAnalysisComponents/container.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<div class="cockpitContainer" :class="['cockpitContainer__' + size]">
|
||||
<div class="content-top" :class="['content-top__' + topSize]">
|
||||
<!-- 使用 flex 容器包裹图标和文字,实现垂直居中 -->
|
||||
<div class="title-wrapper">
|
||||
<svg-icon class="title-icon" :icon-class="icon" />
|
||||
<span class="title-text">
|
||||
{{ name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cockpitContainer-body">
|
||||
<slot>
|
||||
<div class="test-body">something test....</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Container',
|
||||
components: {},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['name', 'size', 'icon', 'topSize'],
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.cockpitContainer {
|
||||
display: inline-block;
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
.content-top {
|
||||
height: 60px;
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
/* 垂直居中关键属性 */
|
||||
height: 100%;
|
||||
/* 继承父容器高度,确保垂直居中范围 */
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 30px;
|
||||
margin-right: 12px;
|
||||
margin-top: 4px;
|
||||
/* 图标和文字之间的间距 */
|
||||
flex-shrink: 0;
|
||||
/* 防止图标被压缩 */
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 24px;
|
||||
color: #000000;
|
||||
letter-spacing: 3px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
// 移除固定行高,避免影响垂直对齐
|
||||
// line-height: 60px;
|
||||
}
|
||||
// width: 547px;
|
||||
// background: url(../../../assets/img/contentTopBasic.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
&__basic {
|
||||
// width: 547px;
|
||||
background: url(../../../assets/img/contentTopBasic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__middle {
|
||||
background: url(../../../assets/img/topTileMiddle.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__large {
|
||||
background: url(../../../assets/img/topTitleLargeBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__KFAPTopTitle {
|
||||
background: url(../../../assets/img/KFAPTopTitle.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__psiTopTitleBasic {
|
||||
background: url(../../../assets/img/psiTopTitleBasic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
&__rawTopTitleLarge {
|
||||
background: url(../../../assets/img/rawTopTitleLarge.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&__topBasic {
|
||||
background: url(../../../assets/img/top-basic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__bottomBasic {
|
||||
background: url(../../../assets/img/bottom-basic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__operatingBasic {
|
||||
background: url(../../../assets/img/operating-basic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__operatingLarge {
|
||||
background: url(../../../assets/img/operating-large.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__profitTopBasic {
|
||||
background: url(../../../assets/img/profitTopBasic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__profitMiddleBasic {
|
||||
background: url(../../../assets/img/profitMiddleBasic.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__psiBasicBg {
|
||||
background: url(../../../assets/img/psiBasicBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__psiMiddleBg {
|
||||
background: url(../../../assets/img/psiMiddleBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__operatingRevenueBg {
|
||||
background: url(../../../assets/img/operatingRevenueBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
&__costBasicBg {
|
||||
background: url(../../../assets/img/costBasicBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
&__rawTopBg {
|
||||
background: url(../../../assets/img/rawTopBg.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
// &__left {
|
||||
// background: url(../../../../../../../assets/img/left.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__energyConsumption {
|
||||
// background: url(../../../../../../../assets/img/energyConsumption.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__left2 {
|
||||
// background: url(../../assets/left_2.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__left3 {
|
||||
// background: url(../../assets/left_3.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__mid2 {
|
||||
// background: url(../../assets/mid_2.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__mid3 {
|
||||
// background: url(../../assets/mid_3.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__right1 {
|
||||
// background: url(../../assets/right_1.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__right2 {
|
||||
// background: url(../../assets/right_2.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__right3 {
|
||||
// background: url(../../assets/right_3.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__weekRight2 {
|
||||
// background: url(../../assets/week_right_2.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__weekMidTop {
|
||||
// background: url(../../assets/week-mid-top.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
// &__weekMidMid {
|
||||
// background: url(../../assets/week-mid-mid.png) no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// background-position: 0 0;
|
||||
// }
|
||||
|
||||
&::after {
|
||||
content: ' ';
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
// background: inherit;
|
||||
/* 设置模糊,不用 filter */
|
||||
backdrop-filter: blur(5px);
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
.container-body {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
250
src/views/home/electricityCostAnalysisComponents/dataTrend.vue
Normal file
250
src/views/home/electricityCostAnalysisComponents/dataTrend.vue
Normal file
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container name="数据趋势" icon="cockpitItemIcon" size="opLargeBg" topSize="large">
|
||||
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%; gap: 16px">
|
||||
<div class="right" style="
|
||||
height: 191px;
|
||||
display: flex;
|
||||
width: 1595px;
|
||||
background-color: rgba(249, 252, 255, 1);
|
||||
">
|
||||
<!-- 直接使用计算属性 chartData,无需手动更新 -->
|
||||
<dataTrendBar @handleGetItemData="getData" :chartData="chartData" />
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Container from "../components/container.vue";
|
||||
import dataTrendBar from "./dataTrendBar.vue";
|
||||
|
||||
export default {
|
||||
name: "ProductionStatus",
|
||||
components: { Container, dataTrendBar },
|
||||
props: {
|
||||
trendData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 移除:原 chartData 定义,改为计算属性
|
||||
};
|
||||
},
|
||||
// 移除:原 watch 监听配置,计算属性自动响应 trendData 变化
|
||||
computed: {
|
||||
/**
|
||||
* chartData 计算属性:自动响应 trendData 变化,格式化并提取各字段数组
|
||||
* @returns {Object} 包含6个独立数组的格式化数据
|
||||
*/
|
||||
chartData() {
|
||||
// 初始化6个独立数组
|
||||
const timeArr = []; // 格式化后的年月数组
|
||||
const valueArr = []; // 实际值数组
|
||||
const diffValueArr = []; // 差异值数组
|
||||
const targetValueArr = []; // 预算值数组
|
||||
const proportionArr = []; // 占比数组
|
||||
const completedArr = []; // 完成率数组
|
||||
|
||||
// 遍历传入的 trendData 数组(响应式依赖,变化时自动重算)
|
||||
this.trendData.forEach((item) => {
|
||||
// 1. 格式化时间并推入时间数组
|
||||
const yearMonth = this.formatTimeToYearMonth(item.time);
|
||||
timeArr.push(yearMonth);
|
||||
|
||||
// 2. 提取其他字段,兜底为0(防止null/undefined影响图表渲染)
|
||||
valueArr.push(item.value ?? 0);
|
||||
diffValueArr.push(item.diffValue ?? 0);
|
||||
targetValueArr.push(item.targetValue ?? 0);
|
||||
proportionArr.push(item.proportion ?? 0);
|
||||
completedArr.push(item.completed ?? 0);
|
||||
});
|
||||
|
||||
// 组装并返回格式化后的数据(结构与原一致)
|
||||
return {
|
||||
time: timeArr,
|
||||
value: valueArr,
|
||||
diffValue: diffValueArr,
|
||||
targetValue: targetValueArr,
|
||||
proportion: proportionArr,
|
||||
completed: completedArr,
|
||||
rawData: this.trendData, // 透传原始数据,方便子组件使用
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 格式化时间戳为年月格式(YYYY-MM)
|
||||
* @param {Number} timestamp 13位毫秒级时间戳
|
||||
* @returns {String} 格式化后的年月字符串(如:2025-10)
|
||||
*/
|
||||
formatTimeToYearMonth(timestamp) {
|
||||
if (!timestamp || isNaN(timestamp)) {
|
||||
return ""; // 容错:非有效时间戳返回空字符串
|
||||
}
|
||||
const date = new Date(timestamp);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0"); // 月份从0开始,补0至2位
|
||||
return `${year}-${month}`;
|
||||
},
|
||||
getData(value) {
|
||||
this.$emit('getData', value)
|
||||
},
|
||||
},
|
||||
};
|
||||
</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>
|
||||
/* 全局 tooltip 样式(不使用 scoped,确保生效) */
|
||||
.production-status-chart-tooltip {
|
||||
background: #0a2b4f77 !important;
|
||||
border: none !important;
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.production-status-chart-tooltip * {
|
||||
color: #fff !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,479 @@
|
||||
<template>
|
||||
<div class="coreBar">
|
||||
<!-- 新增行容器:包裹“各基地情况”和barTop -->
|
||||
<div class="header-row">
|
||||
<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">
|
||||
<span class="legend-icon square achieved"></span>
|
||||
实际·达标
|
||||
</span>
|
||||
<span class="legend-item">
|
||||
<span class="legend-icon square unachieved"></span>
|
||||
实际·未达标
|
||||
</span>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<div class="item-button category-btn">
|
||||
<span class="item-text">类目选择</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>
|
||||
<div class="lineBottom" style="height: 100%; width: 100%">
|
||||
<operatingLineBar :chartData="chartD" style="height: 99%; width: 100%" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import operatingLineBar from './operatingLineBarSale.vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: "Container",
|
||||
components: { operatingLineBar },
|
||||
props: ["chartData"],
|
||||
data() {
|
||||
return {
|
||||
isDropdownShow: false,
|
||||
selectedProfit: '利润总额', // 选中的名称,初始为null
|
||||
profitOptions: [
|
||||
'利润总额',
|
||||
'销量',
|
||||
'单价',
|
||||
'制造成本',
|
||||
'管理费用',
|
||||
'销售费用',
|
||||
'财务费用',
|
||||
'非经营性利润',
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// profitOptions() {
|
||||
// return this.categoryData.map(item => item.name) || [];
|
||||
// },
|
||||
currentDataSource() {
|
||||
return this.chartData
|
||||
},
|
||||
locations() {
|
||||
return this.chartData.time
|
||||
},
|
||||
// 根据按钮切换生成对应的 chartData
|
||||
chartD() {
|
||||
// 销量场景数据
|
||||
const data = this.currentDataSource;
|
||||
console.log(this.currentDataSource, 'currentDataSource');
|
||||
console.log('this.currentDataSource', data);
|
||||
|
||||
const salesData = {
|
||||
allPlaceNames: this.locations,
|
||||
series: [
|
||||
// 1. 完成率(折线图)
|
||||
{
|
||||
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(40, 138, 255, 0)' }
|
||||
])
|
||||
},
|
||||
data: data.proportion || [], // 完成率(%)
|
||||
symbol: 'circle',
|
||||
symbolSize: 6
|
||||
},
|
||||
// 2. 目标(柱状图)
|
||||
{
|
||||
name: '预算',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0, // 左侧Y轴(万元)
|
||||
barWidth: 14,
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(130, 204, 255, 1)' },
|
||||
{ offset: 1, color: 'rgba(75, 157, 255, 1)' }
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: data.targetValue || [] // 目标销量(万元)
|
||||
},
|
||||
// 3. 实际(柱状图,含达标状态)
|
||||
{
|
||||
name: '实际',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 14,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
offset: [0, 0],
|
||||
// 固定label尺寸:68px×20px
|
||||
width: 68,
|
||||
height: 20,
|
||||
// 关键:去掉换行,让文字在一行显示,适配小尺寸
|
||||
formatter: (params) => {
|
||||
const diff = data.diffValue || [];
|
||||
const flags = data.completed || [];
|
||||
const currentDiff = diff[params.dataIndex] || 0;
|
||||
const currentFlag = flags[params.dataIndex] || 0;
|
||||
|
||||
const prefix = currentFlag === 1 ? '+' : '-';
|
||||
|
||||
// 根据标志位选择不同的样式类
|
||||
if (currentFlag === 1) {
|
||||
// 达标 - 使用 rate-achieved 样式
|
||||
return `{achieved|${currentDiff}}{text|差值}`;
|
||||
} else {
|
||||
// 未达标 - 使用 rate-unachieved 样式
|
||||
return `{unachieved|${currentDiff}}{text|差值}`;
|
||||
}
|
||||
},
|
||||
backgroundColor: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(205, 215, 224, 0.6)' },
|
||||
{ offset: 0.2, color: '#ffffff' },
|
||||
{ offset: 1, color: '#ffffff' }
|
||||
]
|
||||
},
|
||||
shadowColor: 'rgba(191,203,215,0.5)',
|
||||
shadowBlur: 2,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 2,
|
||||
borderRadius: 4,
|
||||
borderColor: '#BFCBD577',
|
||||
borderWidth: 0,
|
||||
lineHeight: 20,
|
||||
rich: {
|
||||
text: {
|
||||
width: 'auto',
|
||||
padding: [5, 10, 5, 0],
|
||||
align: 'center',
|
||||
color: '#464646',
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
},
|
||||
achieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#76DABE', // 与达标的 offset: 1 颜色一致
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
},
|
||||
// 未达标样式
|
||||
unachieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#F9A44A', // 与未达标的 offset: 1 颜色一致
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
}
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
color: (params) => {
|
||||
// 达标状态:1=达标(绿色),0=未达标(橙色)
|
||||
const safeFlag = data.completed || [];
|
||||
const currentFlag = safeFlag[params.dataIndex] || 0;
|
||||
return currentFlag === 1
|
||||
? {
|
||||
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)' }
|
||||
]
|
||||
}
|
||||
: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(253, 209, 129, 1)' },
|
||||
{ offset: 1, color: 'rgba(249, 164, 74, 1)' }
|
||||
]
|
||||
};
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: data.value || [] // 实际销量(万元)
|
||||
}
|
||||
]
|
||||
};
|
||||
return salesData;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectProfit(item) {
|
||||
this.selectedProfit = item;
|
||||
this.isDropdownShow = false;
|
||||
this.$emit('handleGetItemData', item)
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.coreBar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
|
||||
// 新增:头部行容器,实现一行排列
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: flex-end; // 左右两端对齐
|
||||
align-items: center; // 垂直居中
|
||||
// width: 100%;
|
||||
margin-bottom: 8px; // 与下方图表区保留间距(可根据需求调整)
|
||||
}
|
||||
|
||||
// 各基地情况标题样式
|
||||
.base-title {
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
font-style: normal;
|
||||
padding: 0 0 0 16px; // 保留原有内边距
|
||||
white-space: nowrap; // 防止文字换行
|
||||
}
|
||||
|
||||
.barTop {
|
||||
// 移除原有flex和justify-content,由header-row控制
|
||||
width: auto; // 自适应宽度
|
||||
// 保留原有align-items,确保内部元素垂直居中
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
// 1. 右侧容器:包裹图例和按钮组,整体靠右
|
||||
.right-container {
|
||||
display: flex;
|
||||
align-items: center; // 图例和按钮组垂直居中
|
||||
gap: 24px; // 图例与按钮组的间距,避免贴紧
|
||||
margin-right: 46px; // 右侧整体留边,与原按钮组边距一致
|
||||
}
|
||||
|
||||
// 2. 图例:在右侧容器内横向排列
|
||||
.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);
|
||||
}
|
||||
|
||||
// 3. 按钮组:在右侧容器内,保留原有样式
|
||||
.button-group {
|
||||
display: flex;
|
||||
position: relative;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
background: #ecf4fe;
|
||||
margin: 0;
|
||||
|
||||
.dropdown-container {
|
||||
position: relative;
|
||||
z-index: 999; // 提高z-index,确保菜单不被遮挡
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
.item-text {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.category-btn {
|
||||
width: 75px;
|
||||
border-top-left-radius: 12px;
|
||||
border-bottom-left-radius: 12px;
|
||||
background: #ffffff;
|
||||
color: #0b58ff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profit-btn {
|
||||
width: 123px;
|
||||
border-top-right-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
position: relative;
|
||||
padding: 0 18px 0 8px;
|
||||
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); // 箭头旋转方向可根据需求调整,比如改为rotate(-90deg)更符合向上展开的视觉
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-options {
|
||||
position: absolute;
|
||||
// 关键修改1:调整top值,让菜单显示在选择框上方,calc(-100% - 2px)表示向上偏移自身100%再加2px间距
|
||||
bottom: 100%;
|
||||
right: 0;
|
||||
// 移除多余的margin-top,避免额外间距
|
||||
// margin-top: 2px;
|
||||
width: 123px;
|
||||
background: #ffffff;
|
||||
// 关键修改2:调整border-radius,让菜单顶部圆角匹配选择框的右上角,底部圆角为0(更美观)
|
||||
border-radius: 8px 8px 0 0;
|
||||
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>
|
||||
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="chartBox" style="width: 100%; height: 108px; position: relative;">
|
||||
<div :id="id" style="width: 100%; height:100%;"></div>
|
||||
<div class="bottomTip">
|
||||
<div class="precent">
|
||||
<span class="precentNum">{{ detailData.completeRate ? detailData.completeRate : 0 }}% </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'EnergyConsumption',
|
||||
// components: { Container },
|
||||
// mixins: [resize],
|
||||
props: {
|
||||
detailData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
// electricComu: 0,
|
||||
// steamComu: 20, // 调整为符合max范围的数值(0-8)
|
||||
// // electricity: [120, 150, 130, 180, 160, 200, 190],
|
||||
// // steam: [80, 95, 85, 110, 100, 120, 115],
|
||||
// // dates: ['1日', '2日', '3日', '4日', '5日', '6日', '7日']
|
||||
})
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: () => ('monthG')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// electricityChart: null,
|
||||
// steamChart: null,
|
||||
// specialTicks: [2, 4, 6, 8], // 统一的刻度显示
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
detailData: {
|
||||
deep: true,
|
||||
immediate: true, // 初始化时立即执行
|
||||
handler() {
|
||||
this.updateGauges()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initGauges()
|
||||
// window.addEventListener('resize', this.handleResize)
|
||||
this.observeContainerResize()
|
||||
},
|
||||
methods: {
|
||||
observeContainerResize() {
|
||||
// 修复:获取正确的容器(组件内的.gauge-container)
|
||||
const container = this.$el.querySelector('.gauge-container')
|
||||
if (container && window.ResizeObserver) {
|
||||
this.resizeObserver = new ResizeObserver(entries => {
|
||||
if (this.electricityChart) {
|
||||
this.electricityChart.resize() // 直接触发resize,无需防抖
|
||||
}
|
||||
})
|
||||
this.resizeObserver.observe(container)
|
||||
}
|
||||
},
|
||||
initGauges() {
|
||||
// console.log('this.id',this.id);
|
||||
|
||||
// 初始化电气图表实例
|
||||
const electricityDom = document.getElementById(this.id)
|
||||
if (electricityDom) {
|
||||
// 修复:正确创建并存储图表实例
|
||||
this.electricityChart = echarts.init(electricityDom)
|
||||
// 首次更新数据
|
||||
this.updateGauges()
|
||||
}
|
||||
// 蒸汽图表若未使用,可注释/删除
|
||||
// const steamDom = document.getElementById('steamGauge')
|
||||
// if (steamDom) {
|
||||
// this.steamChart = echarts.init(steamDom)
|
||||
// }
|
||||
},
|
||||
updateGauges() {
|
||||
// 修复:先判断实例是否存在,再更新配置
|
||||
if (!this.electricityChart) return
|
||||
|
||||
// 修复:兜底获取rate值,确保数值有效
|
||||
const rate = Number(this.detailData?.completeRate) || 0
|
||||
console.log('当前rate值:', rate); // 调试:确认rate值正确
|
||||
|
||||
// 关键:第二个参数传true,清空原有配置,强制更新
|
||||
this.electricityChart.setOption(this.getElectricityGaugeOption(rate), true)
|
||||
},
|
||||
// 用电量仪表盘配置(保留原有样式,优化数值范围)
|
||||
getElectricityGaugeOption(value) {
|
||||
const electricityGradient = new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: '#0B58FF' },
|
||||
{ offset: 1, color: '#32FFCD' }
|
||||
])
|
||||
|
||||
return {
|
||||
series: [
|
||||
{
|
||||
name: '月度',
|
||||
type: 'gauge',
|
||||
radius: '95', // 修复:添加%,避免数值错误
|
||||
center: ['50%', '90%'],
|
||||
startAngle: 180,
|
||||
endAngle: 0,
|
||||
min: 0,
|
||||
max: 100,
|
||||
splitNumber: 4,
|
||||
label: { show: false },
|
||||
progress: {
|
||||
show: true,
|
||||
overlap: false,
|
||||
roundCap: true,
|
||||
clip: false,
|
||||
width: 14,
|
||||
itemStyle: { color: electricityGradient }
|
||||
},
|
||||
pointer: {
|
||||
icon: 'path://M2090.36389,615.30999 L2090.36389,615.30999 C2091.48372,615.30999 2092.40383,616.194028 2092.44859,617.312956 L2096.90698,728.755929 C2097.05155,732.369577 2094.2393,735.416212 2090.62566,735.56078 C2090.53845,735.564269 2090.45117,735.566014 2090.36389,735.566014 L2090.36389,735.566014 C2086.74736,735.566014 2083.81557,732.63423 2083.81557,729.017692 C2083.81557,728.930412 2083.81732,728.84314 2083.82081,728.755929 L2088.2792,617.312956 C2088.32396,616.194028 2089.24407,615.30999 2090.36389,615.30999 Z',
|
||||
length: '75%',
|
||||
width: 16,
|
||||
itemStyle: { color: '#288AFF' },
|
||||
offsetCenter: [0, '10%']
|
||||
},
|
||||
axisLine: {
|
||||
roundCap: true,
|
||||
lineStyle: { width: 12, color: [[1, '#E6EBF7']] }
|
||||
},
|
||||
splitLine: {
|
||||
length: 10,
|
||||
lineStyle: { width: 5, color: '#D6DAE5' },
|
||||
},
|
||||
axisTick: {
|
||||
splitNumber: 2,
|
||||
length: 6,
|
||||
lineStyle: { width: 2, color: '#D6DAE5' }
|
||||
},
|
||||
axisLabel: {
|
||||
show: false,
|
||||
},
|
||||
detail: { show: false },
|
||||
data: [{ value: value, unit: '' }] // 确保数值正确传入
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
// 未使用的蒸汽仪表盘可注释/删除
|
||||
// getSteamGaugeOption(value) { ... }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.chartBox {
|
||||
.bottomTip {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
|
||||
.precent {
|
||||
line-height: 3px;
|
||||
|
||||
&::after,
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 54px;
|
||||
height: 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&::after {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
margin-right: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
// 用电量线条颜色
|
||||
.precent::before {
|
||||
|
||||
background: linear-gradient(90deg, rgba(40, 138, 255, 0) 0%, rgba(12, 125, 254, 0.4) 100%);
|
||||
}
|
||||
|
||||
/* ::after:从 透明 到 rgba(12, 125, 254, 0.4)(90度渐变,左到右) */
|
||||
.precent::after {
|
||||
background: linear-gradient(90deg, rgba(12, 125, 254, 0.4) 0%, rgba(40, 138, 255, 0) 100%);
|
||||
}
|
||||
|
||||
// 蒸汽线条颜色
|
||||
// .steam-precent::after,
|
||||
// .steam-precent::before {
|
||||
// background: linear-gradient(90deg, rgba(11, 168, 255, 0.26) 0%, rgba(54, 239, 230, 1) 100%);
|
||||
// }
|
||||
|
||||
.precentNum {
|
||||
display: inline-block;
|
||||
// width: 52px;
|
||||
height: 22px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: #0B58FF;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
// 蒸汽数字颜色
|
||||
.steam-num {
|
||||
color: rgba(54, 239, 230, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container :name="title" icon="cockpitItemIcon" size="operatingRevenueBg" topSize="middle">
|
||||
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%;">
|
||||
<div class="topItem-container" style="display: flex; gap: 8px;">
|
||||
<div class="dashboard">
|
||||
<div class="title">
|
||||
{{ month }}月完成率
|
||||
</div>
|
||||
<div class="number">
|
||||
<div class="yield">
|
||||
{{ formatRate(factoryData?.completeRate) }}%
|
||||
</div>
|
||||
<div class="mom">
|
||||
环比{{ formatRate(factoryData?.thb) }}%
|
||||
<img v-if="factoryData?.thb >= 0" class="arrow" src="../../../assets/img/topArrow.png" alt="上升">
|
||||
<img v-else class="arrow" src="../../../assets/img/downArrow.png" alt="下降">
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="electricityGauge">
|
||||
<electricityGauge id="month" :detailData="factoryData"></electricityGauge>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="line" style="padding: 0px;">
|
||||
<!-- 传递包含flag的factoryData给柱状图组件 -->
|
||||
<verticalBarChart :detailData="factoryData"></verticalBarChart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Container from './container.vue'
|
||||
import electricityGauge from './electricityGauge.vue'
|
||||
import verticalBarChart from './verticalBarChart.vue'
|
||||
// 引入箭头图片(根据实际路径调整,若模板中直接用路径可注释)
|
||||
|
||||
export default {
|
||||
name: 'ProductionStatus',
|
||||
components: { Container, electricityGauge, verticalBarChart },
|
||||
props: {
|
||||
monData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
month: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* 自动提取monData中的工厂数据,并新增flag字段
|
||||
*/
|
||||
factoryData() { // 整合原始数据 + 计算flag
|
||||
return {
|
||||
completeRate: this.monData.proportion ? Number(this.monData.proportion) : 0,
|
||||
diff: this.monData.diffValue,
|
||||
real: this.monData.value,
|
||||
target: this.monData.targetValue,
|
||||
thb: this.monData.thb,
|
||||
// ...rawData,
|
||||
flag: this.monData.completed // 新增flag字段
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 格式化百分比数值:处理空值/非数字,兜底为0
|
||||
*/
|
||||
formatRate(value) {
|
||||
if (isNaN(value) || value === null || value === undefined) {
|
||||
return 0;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
/**
|
||||
* 判断完成率对应的flag值(<100为0,≥100为1)
|
||||
* @param {number} rate 完成率(原始值,如89代表89%)
|
||||
* @returns {0|1} flag值
|
||||
*/
|
||||
}
|
||||
}
|
||||
</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;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
width: 264px;
|
||||
height: 205px;
|
||||
background: #F9FCFF;
|
||||
padding: 16px 0 0 10px;
|
||||
.title {
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-family: YouSheBiaoTiHei;
|
||||
font-size: 46px;
|
||||
color: #0B58FF;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
white-space: nowrap;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.mom {
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
// 箭头样式优化
|
||||
.arrow {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 500px;
|
||||
height: 205px;
|
||||
background: #F9FCFF;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,203 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container :name="title" icon="cockpitItemIcon" size="operatingRevenueBg" 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; gap: 8px;">
|
||||
<div class="dashboard left">
|
||||
<div class="title">
|
||||
销量·万元
|
||||
</div>
|
||||
<div class="line">
|
||||
<operatingSingleBar></operatingSingleBar>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard right">
|
||||
<div class="title">
|
||||
单价·万元
|
||||
</div>
|
||||
<div class="line">
|
||||
<operatingSingleBar></operatingSingleBar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Container from './container.vue'
|
||||
import operatingSingleBar from './operatingSingleBar.vue'
|
||||
import verticalBarChart from './verticalBarChart.vue'
|
||||
|
||||
// import * as echarts from 'echarts'
|
||||
// import rawItem from './raw-Item.vue'
|
||||
|
||||
export default {
|
||||
name: 'ProductionStatus',
|
||||
components: { Container, operatingSingleBar, verticalBarChart },
|
||||
// mixins: [resize],
|
||||
props: {
|
||||
itemData: { // 接收父组件传递的设备数据数组
|
||||
type: Array,
|
||||
default: () => [] // 默认空数组,避免报错
|
||||
},
|
||||
title: { // 接收父组件传递的设备数据数组
|
||||
type: String,
|
||||
default: () => '' // 默认空数组,避免报错
|
||||
},
|
||||
month: { // 接收父组件传递的设备数据数组
|
||||
type: String,
|
||||
default: () => '' // 默认空数组,避免报错
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
itemData: {
|
||||
handler(newValue, oldValue) {
|
||||
// this.updateChart()
|
||||
},
|
||||
deep: true // 若对象内属性变化需触发,需加 deep: true
|
||||
}
|
||||
},
|
||||
// computed: {
|
||||
// // 处理排序:包含“总成本”的项放前面,其余项按原顺序排列
|
||||
// sortedItemData() {
|
||||
// // 过滤出包含“总成本”的项(不区分大小写)
|
||||
// const totalCostItems = this.itemData.filter(item =>
|
||||
// item.name && item.name.includes('总成本')
|
||||
// );
|
||||
// // 过滤出不包含“总成本”的项
|
||||
// const otherItems = this.itemData.filter(item =>
|
||||
// !item.name || !item.name.includes('总成本')
|
||||
// );
|
||||
// // 合并:总成本项在前,其他项在后
|
||||
// return [...totalCostItems, ...otherItems];
|
||||
// }
|
||||
// },
|
||||
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;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
width: 382px;
|
||||
height: 205px;
|
||||
background: #F9FCFF;
|
||||
padding: 16px 0 0 10px;
|
||||
|
||||
.title {
|
||||
// width: 190px;
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
// width: 190px;
|
||||
height: 32px;
|
||||
font-family: YouSheBiaoTiHei;
|
||||
font-size: 32px;
|
||||
color: #0B58FF;
|
||||
line-height: 32px;
|
||||
letter-spacing: 2px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mom {
|
||||
width: 120px;
|
||||
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;
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
|
||||
// .line {
|
||||
// width: 500px;
|
||||
// height: 205px;
|
||||
// background: #F9FCFF;
|
||||
// }
|
||||
|
||||
// .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> -->
|
||||
@@ -0,0 +1,503 @@
|
||||
<template>
|
||||
<div class="coreBar">
|
||||
<div class="header-row">
|
||||
<div class="base-title">各基地情况</div>
|
||||
<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">
|
||||
<span class="legend-icon square achieved"></span>实际·达标
|
||||
</span>
|
||||
<span class="legend-item">
|
||||
<span class="legend-icon square unachieved"></span>实际·未达标
|
||||
</span>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<div class="item-button category-btn">
|
||||
<span class="item-text">展示顺序</span>
|
||||
</div>
|
||||
<div class="dropdown-container">
|
||||
<div class="item-button profit-btn active" @click.stop="isDropdownShow = !isDropdownShow">
|
||||
<span class="item-text profit-text">{{ selectedSort || '请选择' }}</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.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lineBottom" style="height: 100%; width: 100%">
|
||||
<operatingLineBar :dateData="dateData" :chartData="chartD" style="height: 100%; width: 100%" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import operatingLineBar from './operatingLineBarSale.vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: "Container",
|
||||
components: { operatingLineBar },
|
||||
props: ["chartData",'dateData'],
|
||||
emits: ['sort-change'], // 声明事件(Vue3 推荐)
|
||||
data() {
|
||||
return {
|
||||
activeButton: 0,
|
||||
isDropdownShow: false,
|
||||
selectedSort: '实际值:高~低', // 选中的label
|
||||
selectedSortValue: 1, // 选中的value,用于排序逻辑
|
||||
profitOptions: [
|
||||
{ label: '实际值:高~低', value: 1 },
|
||||
{ label: '实际值:低~高', value: 2 },
|
||||
{ label: '完成率:高~低', value: 3 },
|
||||
{ label: '完成率:低~高', value: 4 },
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 排序后的数据源(核心:根据selectedSortValue重新排序)
|
||||
currentDataSource() {
|
||||
if (!this.chartData) return {};
|
||||
|
||||
// 深拷贝原始数据,避免修改原数据
|
||||
const factory = JSON.parse(JSON.stringify(this.chartData));
|
||||
if (!factory.locations.length || !this.selectedSortValue) return factory;
|
||||
|
||||
// 构建带索引的数组,方便同步所有字段排序
|
||||
const dataWithIndex = factory.locations.map((name, index) => ({
|
||||
index,
|
||||
name,
|
||||
real: factory.reals[index],
|
||||
target: factory.targets[index],
|
||||
rate: factory.rate[index],
|
||||
diff: factory.diff[index],
|
||||
flag: factory.flags[index]
|
||||
}));
|
||||
|
||||
// 根据选中的排序规则排序
|
||||
switch (this.selectedSortValue) {
|
||||
case 1: // 实际值:高~低
|
||||
dataWithIndex.sort((a, b) => b.real - a.real);
|
||||
break;
|
||||
case 2: // 实际值:低~高
|
||||
dataWithIndex.sort((a, b) => a.real - b.real);
|
||||
break;
|
||||
case 3: // 预算值:高~低
|
||||
dataWithIndex.sort((a, b) => b.rate - a.rate);
|
||||
break;
|
||||
case 4: // 预算值:低~高
|
||||
dataWithIndex.sort((a, b) => a.rate - b.rate);
|
||||
break;
|
||||
default:
|
||||
return factory;
|
||||
}
|
||||
|
||||
// 同步更新所有数组
|
||||
factory.locations = dataWithIndex.map(item => item.name);
|
||||
factory.reals = dataWithIndex.map(item => item.real);
|
||||
factory.targets = dataWithIndex.map(item => item.target);
|
||||
factory.rates = dataWithIndex.map(item => item.rate);
|
||||
factory.diff = dataWithIndex.map(item => item.diff);
|
||||
factory.flags = dataWithIndex.map(item => item.flag);
|
||||
|
||||
return factory;
|
||||
},
|
||||
locations() {
|
||||
return this.currentDataSource.locations || [];
|
||||
},
|
||||
// 最终传递给图表的排序后数据
|
||||
chartD() {
|
||||
const data = this.currentDataSource;
|
||||
const salesData = {
|
||||
allPlaceNames: this.locations,
|
||||
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(40, 138, 255, 0)' }
|
||||
])
|
||||
},
|
||||
data: data.rate || [],
|
||||
symbol: 'circle',
|
||||
symbolSize: 6
|
||||
},
|
||||
// 目标(柱状图)
|
||||
{
|
||||
name: '预算',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 40,
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(130, 204, 255, 1)' },
|
||||
{ offset: 1, color: 'rgba(75, 157, 255, 1)' }
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: data.targets || []
|
||||
},
|
||||
// 实际(柱状图)
|
||||
{
|
||||
name: '实际',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 40,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
offset: [32, 0],
|
||||
width: 100,
|
||||
height: 22,
|
||||
formatter: (params) => {
|
||||
const diff = data.diff || [];
|
||||
const flags = data.flags || [];
|
||||
const currentDiff = diff[params.dataIndex] || 0;
|
||||
const currentFlag = flags[params.dataIndex] || 0;
|
||||
|
||||
const prefix = currentFlag === 1 ? '+' : '-';
|
||||
|
||||
// 根据标志位选择不同的样式类
|
||||
if (currentFlag === 1) {
|
||||
// 达标 - 使用 rate-achieved 样式
|
||||
return `{achieved|${currentDiff}}{text|差值}`;
|
||||
} else {
|
||||
// 未达标 - 使用 rate-unachieved 样式
|
||||
return `{unachieved|${currentDiff}}{text|差值}`;
|
||||
}
|
||||
},
|
||||
backgroundColor: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(205, 215, 224, 0.6)' },
|
||||
{ offset: 0.2, color: '#ffffff' },
|
||||
{ offset: 1, color: '#ffffff' }
|
||||
]
|
||||
},
|
||||
shadowColor: 'rgba(191,203,215,0.5)',
|
||||
shadowBlur: 2,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 2,
|
||||
borderRadius: 4,
|
||||
borderColor: '#BFCBD577',
|
||||
borderWidth: 0,
|
||||
lineHeight: 26,
|
||||
rich: {
|
||||
text: {
|
||||
width: 'auto',
|
||||
padding: [5, 10, 5, 0],
|
||||
align: 'center',
|
||||
color: '#464646',
|
||||
fontSize: 14
|
||||
},
|
||||
achieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#76DABE', // 与达标的 offset: 1 颜色一致
|
||||
fontSize: 14
|
||||
},
|
||||
// 未达标样式
|
||||
unachieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#F9A44A', // 与未达标的 offset: 1 颜色一致
|
||||
fontSize: 14
|
||||
}
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
color: (params) => {
|
||||
const safeFlag = data.flags || [];
|
||||
const currentFlag = safeFlag[params.dataIndex] || 0;
|
||||
return currentFlag === 1
|
||||
? {
|
||||
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)' }
|
||||
]
|
||||
}
|
||||
: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(253, 209, 129, 1)' },
|
||||
{ offset: 1, color: 'rgba(249, 164, 74, 1)' }
|
||||
]
|
||||
};
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: data.reals || []
|
||||
}
|
||||
]
|
||||
};
|
||||
return salesData;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectProfit(item) {
|
||||
// 更新选中的label和value
|
||||
this.selectedSort = item.label;
|
||||
this.selectedSortValue = item.value;
|
||||
this.isDropdownShow = false;
|
||||
// 向父组件传递排序事件(可选,保持原有逻辑)
|
||||
this.$emit('sort-change', item.value);
|
||||
}
|
||||
},
|
||||
// 监听父组件传入的chartData变化,重置选中状态(可选)
|
||||
watch: {
|
||||
'chartData.factory': {
|
||||
handler() {
|
||||
// 若需要切换数据源后重置排序,可取消注释
|
||||
// this.selectedSort = null;
|
||||
// this.selectedSortValue = null;
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.coreBar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.base-title {
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
font-style: normal;
|
||||
padding: 0 0 0 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.barTop {
|
||||
width: auto;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.right-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
margin-right: 46px;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.legend-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.legend-icon.line {
|
||||
width: 12px;
|
||||
height: 2px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: -2px;
|
||||
left: 3px;
|
||||
width: 6px;
|
||||
border-radius: 50%;
|
||||
height: 6px;
|
||||
background-color: rgba(40, 138, 255, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.legend-icon.square {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.yield {
|
||||
background: rgba(40, 138, 255, 1);
|
||||
}
|
||||
|
||||
.target {
|
||||
background: #2889FF;
|
||||
}
|
||||
|
||||
.achieved {
|
||||
background: rgba(40, 203, 151, 1);
|
||||
}
|
||||
|
||||
.unachieved {
|
||||
background: rgba(255, 132, 0, 1);
|
||||
}
|
||||
|
||||
.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;
|
||||
overflow: hidden;
|
||||
|
||||
.item-text {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.category-btn {
|
||||
width: 75px;
|
||||
border-top-left-radius: 12px;
|
||||
border-bottom-left-radius: 12px;
|
||||
background: #ffffff;
|
||||
color: #0b58ff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profit-btn {
|
||||
width: 123px;
|
||||
border-top-right-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
position: relative;
|
||||
padding: 0 18px 0 8px;
|
||||
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: 123px;
|
||||
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>
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div ref="cockpitEffChipBottom" id="cockpitEffChipBottom" style="width: 100%; height: 400px;"></div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
myChart: null // 存储图表实例,避免重复创建
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 明确接收的props结构,增强可读性
|
||||
chartData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
series: [],
|
||||
allPlaceNames: []
|
||||
}),
|
||||
// 校验数据格式
|
||||
validator: (value) => {
|
||||
return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.updateChart();
|
||||
});
|
||||
},
|
||||
|
||||
// 新增:监听 chartData 变化
|
||||
watch: {
|
||||
// 深度监听数据变化,仅更新图表配置(不销毁实例)
|
||||
chartData: {
|
||||
handler() {
|
||||
console.log(this.chartData,'chartData');
|
||||
|
||||
this.updateChart();
|
||||
},
|
||||
deep: true,
|
||||
immediate: true // 初始化时立即执行
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateChart() {
|
||||
const chartDom = this.$refs.cockpitEffChipBottom;
|
||||
if (!chartDom) {
|
||||
console.error('图表容器未找到!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose();
|
||||
}
|
||||
|
||||
this.myChart = echarts.init(chartDom);
|
||||
const { allPlaceNames, series } = this.chartData || {};
|
||||
|
||||
// 处理空数据
|
||||
const xData = allPlaceNames || [];
|
||||
const chartSeries = series || []; // 父组件传递的 series
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
},
|
||||
formatter: (params) => {
|
||||
let html = `${params[0].axisValue}<br/>`;
|
||||
params.forEach(item => {
|
||||
const unit = item.seriesName === '完成率' ? '%' : (
|
||||
['产量', '销量'].includes(this.$parent.selectedProfit) ? '片' : '万元'
|
||||
);
|
||||
html += `${item.marker} ${item.seriesName}: ${item.value}${unit}<br/>`;
|
||||
});
|
||||
return html;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 30,
|
||||
right: 20,
|
||||
left: 60,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
axisTick: { show: false },
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
interval: 0,
|
||||
padding: [5, 0, 0, 0]
|
||||
},
|
||||
data: xData
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
// 左侧Y轴:营业收入、成本(单位万元)
|
||||
{
|
||||
type: 'value',
|
||||
splitNumber: 4,
|
||||
name: '万元',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'right'
|
||||
},
|
||||
// min: 0,
|
||||
// max: (value) => Math.ceil((value.max || 0) * 1.1),
|
||||
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
formatter: '{value}'
|
||||
},
|
||||
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
splitNumber: 4
|
||||
},
|
||||
// 右侧Y轴:利润占比(百分比)
|
||||
{
|
||||
type: 'value',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'left'
|
||||
},
|
||||
// min: 0,
|
||||
// max: 100,
|
||||
scale:true,
|
||||
splitNumber: 4,
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
formatter: '{value}%'
|
||||
},
|
||||
splitLine: { show: false },
|
||||
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
splitNumber: 4
|
||||
}
|
||||
],
|
||||
series: chartSeries // 直接使用父组件传递的 series
|
||||
};
|
||||
|
||||
option && this.myChart.setOption(option);
|
||||
|
||||
// 窗口缩放适配和销毁逻辑保持不变
|
||||
window.addEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
|
||||
this.$once('hook:destroyed', () => {
|
||||
window.removeEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
this.myChart && this.myChart.dispose();
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 400px;"></div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
myChart: null // 存储图表实例,避免重复创建
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 明确接收的props结构,增强可读性
|
||||
chartData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
series: [],
|
||||
allPlaceNames: []
|
||||
}),
|
||||
// 校验数据格式
|
||||
validator: (value) => {
|
||||
return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.updateChart();
|
||||
});
|
||||
},
|
||||
|
||||
// 新增:监听 chartData 变化
|
||||
watch: {
|
||||
// 深度监听数据变化,仅更新图表配置(不销毁实例)
|
||||
chartData: {
|
||||
handler() {
|
||||
console.log(this.chartData,'chartData');
|
||||
|
||||
this.updateChart();
|
||||
},
|
||||
deep: true,
|
||||
immediate: true // 初始化时立即执行
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateChart() {
|
||||
const chartDom = this.$refs.cockpitEffChip;
|
||||
if (!chartDom) {
|
||||
console.error('图表容器未找到!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose();
|
||||
}
|
||||
|
||||
this.myChart = echarts.init(chartDom);
|
||||
const { allPlaceNames, series } = this.chartData || {};
|
||||
|
||||
// 处理空数据
|
||||
const xData = allPlaceNames || [];
|
||||
const chartSeries = series || []; // 父组件传递的 series
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
},
|
||||
// formatter: (params) => {
|
||||
// let html = `${params[0].axisValue}<br/>`;
|
||||
// params.forEach(item => {
|
||||
// const unit = item.seriesName === '完成率' ? '%' : (
|
||||
// ['产量', '销量'].includes(this.$parent.selectedProfit) ? '片' : '万元'
|
||||
// );
|
||||
// html += `${item.marker} ${item.seriesName}: ${item.value}${unit}<br/>`;
|
||||
// });
|
||||
// return html;
|
||||
// }
|
||||
},
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 30,
|
||||
right: 20,
|
||||
left: 60,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
axisTick: { show: false },
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
interval: 0,
|
||||
padding: [5, 0, 0, 0]
|
||||
},
|
||||
data: xData
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
// 左侧Y轴:营业收入、成本(单位万元)
|
||||
{
|
||||
type: 'value',
|
||||
name: '万元',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'right'
|
||||
},
|
||||
|
||||
splitNumber: 4,
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
formatter: '{value}'
|
||||
},
|
||||
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
splitNumber: 4
|
||||
},
|
||||
// 右侧Y轴:利润占比(百分比)
|
||||
// {
|
||||
// type: 'value',
|
||||
// nameTextStyle: {
|
||||
// color: 'rgba(0, 0, 0, 0.45)',
|
||||
// fontSize: 12,
|
||||
// align: 'left'
|
||||
// },
|
||||
// min: 0,
|
||||
// max: 100,
|
||||
// axisTick: { show: false },
|
||||
// axisLabel: {
|
||||
// color: 'rgba(0, 0, 0, 0.45)',
|
||||
// fontSize: 12,
|
||||
// formatter: '{value}%'
|
||||
// },
|
||||
// splitLine: { show: false },
|
||||
// axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
// splitNumber: 4
|
||||
// }
|
||||
],
|
||||
series: chartSeries // 直接使用父组件传递的 series
|
||||
};
|
||||
|
||||
option && this.myChart.setOption(option);
|
||||
|
||||
// 窗口缩放适配和销毁逻辑保持不变
|
||||
window.addEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
|
||||
this.$once('hook:destroyed', () => {
|
||||
window.removeEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
this.myChart && this.myChart.dispose();
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 400px;"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
myChart: null, // 存储图表实例
|
||||
resizeHandler: null, // 存储resize事件处理函数
|
||||
// 核心:基地名称与序号的映射表(固定顺序)
|
||||
baseNameToIndexMap: {
|
||||
'宜兴': 7,
|
||||
'漳州': 8,
|
||||
'自贡': 3,
|
||||
'桐城': 2,
|
||||
'洛阳': 9,
|
||||
'合肥': 5,
|
||||
'宿迁': 6,
|
||||
'秦皇岛': 10
|
||||
}
|
||||
};
|
||||
},
|
||||
props: {
|
||||
chartData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
dateData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.initChart(); // 初始化图表(只执行一次)
|
||||
this.updateChart(); // 更新图表数据
|
||||
});
|
||||
},
|
||||
watch: {
|
||||
chartData: {
|
||||
handler() {
|
||||
console.log(this.chartData, 'chartData');
|
||||
this.updateChart(); // 仅更新数据,不重新创建实例
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 组件销毁时清理资源
|
||||
this.destroyChart();
|
||||
},
|
||||
methods: {
|
||||
// 初始化图表(只在mounted中执行一次)
|
||||
initChart() {
|
||||
const chartDom = this.$refs.cockpitEffChip;
|
||||
if (!chartDom) {
|
||||
console.error('图表容器未找到!');
|
||||
return;
|
||||
}
|
||||
this.myChart = echarts.init(chartDom);
|
||||
this.myChart.getZr().on('click', (params) => {
|
||||
console.log('params', params);
|
||||
|
||||
// 提取点击的基地名称
|
||||
// const itemName = params.name;
|
||||
let itemName = undefined
|
||||
// 根据映射表获取对应的序号(未匹配到则返回0或其他默认值)
|
||||
let pointInPixel = [params.offsetX, params.offsetY];
|
||||
if (this.myChart.containPixel('grid', pointInPixel)) {
|
||||
let pointInGrid = this.myChart.convertFromPixel({
|
||||
seriesIndex: 0
|
||||
}, pointInPixel);
|
||||
let xIndex = pointInGrid[0]; //索引
|
||||
let handleIndex = Number(xIndex);
|
||||
let seriesObj = this.myChart.getOption(); //图表object对象
|
||||
var op = this.myChart.getOption();
|
||||
//获得图表中点击的列
|
||||
itemName = op.xAxis[0].data[handleIndex]; //获取点击的列名
|
||||
console.log(itemName, 'monthmonthmonth');
|
||||
console.log(handleIndex, seriesObj);
|
||||
};
|
||||
const baseIndex = this.baseNameToIndexMap[itemName] || 0;
|
||||
|
||||
console.log(`你点击了【${itemName}】(序号:${baseIndex})`);
|
||||
if (itemName === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 路由跳转时携带序号(或名称+序号)
|
||||
this.$router.push({
|
||||
path: 'totalProfitBase',
|
||||
query: { // 使用query传递参数(推荐),也可使用params
|
||||
// baseName: itemName,
|
||||
factory: baseIndex,
|
||||
dateData: this.dateData
|
||||
}
|
||||
// 若仍需用base作为参数:
|
||||
// base: itemName,
|
||||
// params: { baseIndex: baseIndex }
|
||||
});
|
||||
});
|
||||
|
||||
// 定义resize处理函数(命名函数,方便移除)
|
||||
this.resizeHandler = () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
};
|
||||
// 绑定resize事件(只绑定一次)
|
||||
window.addEventListener('resize', this.resizeHandler);
|
||||
},
|
||||
|
||||
// 更新图表数据(数据变化时执行)
|
||||
updateChart() {
|
||||
if (!this.myChart) {
|
||||
return; // 实例未初始化则返回
|
||||
}
|
||||
|
||||
const { allPlaceNames, series } = this.chartData || {};
|
||||
const xData = allPlaceNames || [];
|
||||
const chartSeries = series || [];
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 5,
|
||||
right: 20,
|
||||
left: 25,
|
||||
containLabel: true
|
||||
},
|
||||
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],
|
||||
// 可选:X轴标签显示“序号+名称”(如“1 宜兴”)
|
||||
// formatter: (value) => {
|
||||
// const index = this.baseNameToIndexMap[value] || '';
|
||||
// return index ? `${index} ${value}` : value;
|
||||
// }
|
||||
},
|
||||
data: xData
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '万元',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'right'
|
||||
},
|
||||
|
||||
splitNumber: 4,
|
||||
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: { show: true, lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } }
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'left'
|
||||
},
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
formatter: '{value}%'
|
||||
},
|
||||
splitLine: { show: false },
|
||||
axisLine: { show: true, lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
splitNumber: 4
|
||||
}
|
||||
],
|
||||
series: chartSeries
|
||||
};
|
||||
|
||||
// 只更新配置,不重新创建实例
|
||||
this.myChart.setOption(option, true); // 第二个参数true表示清空原有配置,避免数据残留
|
||||
},
|
||||
|
||||
// 销毁图表资源
|
||||
destroyChart() {
|
||||
// 移除resize事件
|
||||
if (this.resizeHandler) {
|
||||
window.removeEventListener('resize', this.resizeHandler);
|
||||
}
|
||||
// 销毁图表实例
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose();
|
||||
this.myChart = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 380px;"></div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
myChart: null // 存储图表实例,避免重复创建
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 明确接收的props结构,增强可读性
|
||||
chartData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
}),
|
||||
// 校验数据格式
|
||||
// validator: (value) => {
|
||||
// return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
|
||||
// }
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.updateChart();
|
||||
});
|
||||
},
|
||||
|
||||
// 新增:监听 chartData 变化
|
||||
watch: {
|
||||
// 深度监听数据变化,仅更新图表配置(不销毁实例)
|
||||
chartData: {
|
||||
handler() {
|
||||
console.log(this.chartData, 'chartData');
|
||||
this.updateChart();
|
||||
},
|
||||
deep: true,
|
||||
immediate: true // 初始化时立即执行
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateChart() {
|
||||
const chartDom = this.$refs.cockpitEffChip;
|
||||
if (!chartDom) {
|
||||
console.error('图表容器未找到!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose();
|
||||
}
|
||||
|
||||
this.myChart = echarts.init(chartDom);
|
||||
|
||||
const { allPlaceNames, series } = this.chartData || {};
|
||||
console.log('chartData', this.chartData);
|
||||
|
||||
// 处理空数据
|
||||
const xData = allPlaceNames || [];
|
||||
const chartSeries = series || []; // 父组件传递的 series
|
||||
console.log('xData', xData);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
},
|
||||
// formatter: (params) => {
|
||||
// let html = `${params[0].axisValue}<br/>`;
|
||||
// params.forEach(item => {
|
||||
// const unit = item.seriesName === '完成率' ? '%' : (
|
||||
// ['产量', '销量'].includes(this.$parent.selectedProfit) ? '片' : '万元'
|
||||
// );
|
||||
// html += `${item.marker} ${item.seriesName}: ${item.value}${unit}<br/>`;
|
||||
// });
|
||||
// return html;
|
||||
// }
|
||||
},
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 20,
|
||||
right: 10,
|
||||
left: 25,
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
axisTick: { show: false },
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
interval: 0,
|
||||
padding: [5, 0, 0, 0]
|
||||
},
|
||||
data: xData
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
// 左侧Y轴:营业收入、成本(单位万元)
|
||||
{
|
||||
type: 'value',
|
||||
name: '万元',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'right'
|
||||
},
|
||||
|
||||
splitNumber: 4,
|
||||
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: { show: true, lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
},
|
||||
// 右侧Y轴:利润占比(百分比)
|
||||
{
|
||||
type: 'value',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'left'
|
||||
},
|
||||
// min: 0,
|
||||
// max: 100,
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
formatter: '{value}%'
|
||||
},
|
||||
splitLine: { show: false },
|
||||
axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
|
||||
//
|
||||
splitNumber: 4,
|
||||
}
|
||||
],
|
||||
series: chartSeries // 直接使用父组件传递的 series
|
||||
};
|
||||
|
||||
option && this.myChart.setOption(option);
|
||||
|
||||
// 窗口缩放适配和销毁逻辑保持不变
|
||||
window.addEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
|
||||
this.$once('hook:destroyed', () => {
|
||||
window.removeEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
this.myChart && this.myChart.dispose();
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 200px;"></div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
myChart: null // 存储图表实例,避免重复创建
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 明确接收的props结构,增强可读性
|
||||
chartData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
}),
|
||||
// 校验数据格式
|
||||
// validator: (value) => {
|
||||
// return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
|
||||
// }
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.updateChart();
|
||||
});
|
||||
},
|
||||
|
||||
// 新增:监听 chartData 变化
|
||||
watch: {
|
||||
// 深度监听数据变化,仅更新图表配置(不销毁实例)
|
||||
chartData: {
|
||||
handler() {
|
||||
console.log(this.chartData,'chartData');
|
||||
this.updateChart();
|
||||
},
|
||||
deep: true,
|
||||
immediate: true // 初始化时立即执行
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateChart() {
|
||||
const chartDom = this.$refs.cockpitEffChip;
|
||||
if (!chartDom) {
|
||||
console.error('图表容器未找到!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose();
|
||||
}
|
||||
|
||||
this.myChart = echarts.init(chartDom);
|
||||
|
||||
const { allPlaceNames, series } = this.chartData || {};
|
||||
console.log('chartData', this.chartData);
|
||||
|
||||
// 处理空数据
|
||||
const xData = allPlaceNames || [];
|
||||
const chartSeries = series || []; // 父组件传递的 series
|
||||
console.log('xData', xData);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
// axisPointer: {
|
||||
// type: 'cross',
|
||||
// label: {
|
||||
// backgroundColor: '#6a7985'
|
||||
// }
|
||||
// },
|
||||
// formatter: (params) => {
|
||||
// let html = `${params[0].axisValue}<br/>`;
|
||||
// params.forEach(item => {
|
||||
// const unit = item.seriesName === '完成率' ? '%' : (
|
||||
// ['产量', '销量'].includes(this.$parent.selectedProfit) ? '片' : '万元'
|
||||
// );
|
||||
// html += `${item.marker} ${item.seriesName}: ${item.value}${unit}<br/>`;
|
||||
// });
|
||||
// return html;
|
||||
// }
|
||||
},
|
||||
grid: {
|
||||
top: 25,
|
||||
bottom: 25,
|
||||
right: 10,
|
||||
left: 2,
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
// offset: 10
|
||||
// boundaryGap: ['50%', '50%'],
|
||||
axisTick: { show: false },
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: { color: 'rgba(0, 0, 0, 0.15)' }
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
interval: 0,
|
||||
padding: [5, 0, 0, 0]
|
||||
},
|
||||
data: xData
|
||||
}
|
||||
],
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
// name: '万元',
|
||||
nameTextStyle: {
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
fontSize: 12,
|
||||
align: 'right'
|
||||
},
|
||||
axisLine: {
|
||||
show: true, // 显示Y轴轴线(关键)
|
||||
lineStyle: {
|
||||
color: '#E5E6EB', // 轴线颜色(浅灰色,可自定义)
|
||||
width: 1, // 轴线宽度
|
||||
type: 'solid' // 实线(可选:dashed虚线、dotted点线)
|
||||
}
|
||||
},
|
||||
|
||||
splitNumber: 2,
|
||||
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)' } },
|
||||
},
|
||||
series: chartSeries // 直接使用父组件传递的 series
|
||||
};
|
||||
|
||||
option && this.myChart.setOption(option);
|
||||
|
||||
// 窗口缩放适配和销毁逻辑保持不变
|
||||
window.addEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
|
||||
this.$once('hook:destroyed', () => {
|
||||
window.removeEventListener('resize', () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
});
|
||||
this.myChart && this.myChart.dispose();
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,313 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container name="当月数据对比" icon="cockpitItemIcon" size="operatingLarge" topSize="large">
|
||||
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%; gap: 16px">
|
||||
<div class="left" style="
|
||||
height: 380px;
|
||||
display: flex;
|
||||
width: 348px;
|
||||
background-color: rgba(249, 252, 255, 1);
|
||||
flex-direction: column;
|
||||
">
|
||||
<div style="
|
||||
padding: 16px 16px 5px 16px;
|
||||
line-height: 18px;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
letter-spacing: 1px;
|
||||
font-style: normal;
|
||||
">
|
||||
集团情况
|
||||
</div>
|
||||
<div style='font-size: 16px;line-height: 16px;text-align: right;padding-right: 16px;'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{chartData.topBarData.rate[0]}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:chartData.topBarData.flags>0?'#30B590':'#FF9423'}" >{{chartData.topBarData.diff[0]}}</span></span>
|
||||
</div>
|
||||
<operatingTopBar :chartData="chartData?.topBarData || {}" />
|
||||
</div>
|
||||
<div class="right" style="
|
||||
height: 380px;
|
||||
display: flex;
|
||||
width: 1220px;
|
||||
background-color: rgba(249, 252, 255, 1);
|
||||
">
|
||||
<operatingBar :dateData="dateData" :chartData="chartData?.barData || {}" />
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Container from "../components/container.vue";
|
||||
import operatingBar from "./operatingBar.vue";
|
||||
import operatingTopBar from "./operatingTopBar.vue";
|
||||
|
||||
// 序号→地名映射表(levelId=序号)
|
||||
const baseIndexToNameMap = {
|
||||
7: "宜兴",
|
||||
8: "漳州",
|
||||
3: "自贡",
|
||||
2: "桐城",
|
||||
9: "洛阳",
|
||||
5: "合肥",
|
||||
10: "秦皇岛",
|
||||
6: "宿迁"
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "ProductionStatus",
|
||||
components: { Container, operatingBar, operatingTopBar },
|
||||
props: {
|
||||
monData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
dateData: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chartData: {
|
||||
topBarData: { // levelId=1的整合数据
|
||||
locations: [], // 固定为["凯盛新能"]
|
||||
diff: [], // 差值数组
|
||||
targets: [], // 预算值数组
|
||||
reals: [], // 实际值数组
|
||||
rate: [], // 完成率数组
|
||||
flags: [] // 完成状态数组(0/1)
|
||||
},
|
||||
barData: { // levelId≠1的整合数据
|
||||
locations: [], // levelId对应的baseIndexToNameMap中的地名
|
||||
diff: [], // 对应差值数组
|
||||
targets: [], // 预算值数组
|
||||
reals: [], // 实际值数组
|
||||
rate: [], // 完成率数组
|
||||
flags: [] // 完成状态数组(0/1)
|
||||
// baseIndexes: []// 对应levelId(序号)数组
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
monData: {
|
||||
handler() {
|
||||
this.processChartData();
|
||||
},
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 核心方法:按levelId匹配地名生成locations
|
||||
*/
|
||||
processChartData() {
|
||||
// 初始化空数据结构
|
||||
const initTopBarData = {
|
||||
locations: [], diff: [], targets: [], reals: [], rate: [], flags: []
|
||||
};
|
||||
const initBarData = { locations: [], diff: [], targets: [], reals: [], rate: [], flags: [] };
|
||||
|
||||
if (!Array.isArray(this.monData) || this.monData.length === 0) {
|
||||
this.chartData = { topBarData: initTopBarData, barData: initBarData };
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 处理levelId=1的整合数据(逻辑不变)
|
||||
const level1Data = this.monData.filter(item => item.levelId === 1);
|
||||
const topBarData = { ...initTopBarData };
|
||||
level1Data.forEach(item => {
|
||||
if (!item.name) return;
|
||||
topBarData.locations = ["凯盛新能"]; // levelId=1固定为凯盛新能
|
||||
topBarData.diff.push(item.diffValue || 0);
|
||||
topBarData.targets.push(item.targetValue || 0);
|
||||
topBarData.reals.push(item.value || 0);
|
||||
topBarData.rate.push(item.proportion || 0);
|
||||
topBarData.flags.push(item.completed ? 1 : 0);
|
||||
});
|
||||
|
||||
// 2. 处理levelId≠1的整合数据(核心:levelId匹配地名)
|
||||
const barData = { ...initBarData };
|
||||
// 筛选有效数据:levelId≠1 且 levelId在baseIndexToNameMap中
|
||||
const validOtherData = this.monData.filter(item => {
|
||||
return item.levelId !== 1 && baseIndexToNameMap.hasOwnProperty(item.levelId);
|
||||
});
|
||||
|
||||
// 遍历有效数据,填充locations(levelId→地名)
|
||||
validOtherData.forEach(item => {
|
||||
// 根据levelId(序号)从映射表获取对应地名
|
||||
const baseName = baseIndexToNameMap[item.levelId];
|
||||
if (baseName) { // 确保地名和原始名称有效
|
||||
// barData.names.push(item.name); // 保留monData中的原始名称
|
||||
barData.locations.push(baseName); // locations=levelId对应的地名(如levelId=7→宜兴)
|
||||
barData.diff.push(item.diffValue || 0);
|
||||
barData.targets.push(item.targetValue || 0);
|
||||
barData.reals.push(item.value || 0);
|
||||
barData.rate.push(item.proportion || 0);
|
||||
barData.flags.push(item.completed ? 1 : 0);
|
||||
// barData.baseIndexes.push(Number(item.levelId) || 0); // 序号转数字
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 更新chartData
|
||||
this.chartData = { topBarData, barData };
|
||||
console.log('levelId=1数据:', this.chartData.topBarData);
|
||||
console.log('levelId≠1数据(locations=levelId对应地名):', this.chartData.barData);
|
||||
}
|
||||
},
|
||||
};
|
||||
</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>
|
||||
@@ -0,0 +1,313 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container name="累计数据对比" icon="cockpitItemIcon" size="operatingLarge" topSize="large">
|
||||
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%; gap: 16px">
|
||||
<div class="left" style="
|
||||
height: 380px;
|
||||
display: flex;
|
||||
width: 348px;
|
||||
background-color: rgba(249, 252, 255, 1);
|
||||
flex-direction: column;
|
||||
">
|
||||
<div style="
|
||||
padding: 16px 16px 5px 16px;
|
||||
line-height: 18px;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
letter-spacing: 1px;
|
||||
font-style: normal;
|
||||
">
|
||||
集团情况
|
||||
</div>
|
||||
<div style='font-size: 16px;line-height: 16px;text-align: right;padding-right: 16px;'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{chartData.topBarData.rate[0]}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:chartData.topBarData.flags>0?'#30B590':'#FF9423'}" >{{chartData.topBarData.diff[0]}}</span></span>
|
||||
</div>
|
||||
<operatingTopBar :chartData="chartData?.topBarData || {}" />
|
||||
</div>
|
||||
<div class="right" style="
|
||||
height: 380px;
|
||||
display: flex;
|
||||
width: 1220px;
|
||||
background-color: rgba(249, 252, 255, 1);
|
||||
">
|
||||
<operatingBar :dateData="dateData" :chartData="chartData?.barData || {}" />
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Container from "../components/container.vue";
|
||||
import operatingBar from "./operatingBar.vue";
|
||||
import operatingTopBar from "./operatingTopBar.vue";
|
||||
|
||||
// 序号→地名映射表(levelId=序号)
|
||||
const baseIndexToNameMap = {
|
||||
7: "宜兴",
|
||||
8: "漳州",
|
||||
3: "自贡",
|
||||
2: "桐城",
|
||||
9: "洛阳",
|
||||
5: "合肥",
|
||||
10: "秦皇岛",
|
||||
6: "宿迁"
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "ProductionStatus",
|
||||
components: { Container, operatingBar, operatingTopBar },
|
||||
props: {
|
||||
totalData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
dateData: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chartData: {
|
||||
topBarData: { // levelId=1的整合数据
|
||||
locations: [], // 固定为["凯盛新能"]
|
||||
diff: [], // 差值数组
|
||||
targets: [], // 预算值数组
|
||||
reals: [], // 实际值数组
|
||||
rate: [], // 完成率数组
|
||||
flags: [] // 完成状态数组(0/1)
|
||||
},
|
||||
barData: { // levelId≠1的整合数据
|
||||
locations: [], // levelId对应的baseIndexToNameMap中的地名
|
||||
diff: [], // 对应差值数组
|
||||
targets: [], // 预算值数组
|
||||
reals: [], // 实际值数组
|
||||
rate: [], // 完成率数组
|
||||
flags: [] // 完成状态数组(0/1)
|
||||
// baseIndexes: []// 对应levelId(序号)数组
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
totalData: {
|
||||
handler() {
|
||||
this.processChartData();
|
||||
},
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 核心方法:按levelId匹配地名生成locations
|
||||
*/
|
||||
processChartData() {
|
||||
// 初始化空数据结构
|
||||
const initTopBarData = {
|
||||
locations: [], diff: [], targets: [], reals: [], rate: [], flags: []
|
||||
};
|
||||
const initBarData = { locations: [], diff: [], targets: [], reals: [], rate: [], flags: [] };
|
||||
|
||||
if (!Array.isArray(this.totalData) || this.totalData.length === 0) {
|
||||
this.chartData = { topBarData: initTopBarData, barData: initBarData };
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 处理levelId=1的整合数据(逻辑不变)
|
||||
const level1Data = this.totalData.filter(item => item.levelId === 1);
|
||||
const topBarData = { ...initTopBarData };
|
||||
level1Data.forEach(item => {
|
||||
if (!item.name) return;
|
||||
topBarData.locations = ["凯盛新能"]; // levelId=1固定为凯盛新能
|
||||
topBarData.diff.push(item.diffValue || 0);
|
||||
topBarData.targets.push(item.targetValue || 0);
|
||||
topBarData.reals.push(item.value || 0);
|
||||
topBarData.rate.push(item.proportion || 0);
|
||||
topBarData.flags.push(item.completed ? 1 : 0);
|
||||
});
|
||||
|
||||
// 2. 处理levelId≠1的整合数据(核心:levelId匹配地名)
|
||||
const barData = { ...initBarData };
|
||||
// 筛选有效数据:levelId≠1 且 levelId在baseIndexToNameMap中
|
||||
const validOtherData = this.totalData.filter(item => {
|
||||
return item.levelId !== 1 && baseIndexToNameMap.hasOwnProperty(item.levelId);
|
||||
});
|
||||
|
||||
// 遍历有效数据,填充locations(levelId→地名)
|
||||
validOtherData.forEach(item => {
|
||||
// 根据levelId(序号)从映射表获取对应地名
|
||||
const baseName = baseIndexToNameMap[item.levelId];
|
||||
if (baseName) { // 确保地名和原始名称有效
|
||||
// barData.names.push(item.name); // 保留monData中的原始名称
|
||||
barData.locations.push(baseName); // locations=levelId对应的地名(如levelId=7→宜兴)
|
||||
barData.diff.push(item.diffValue || 0);
|
||||
barData.targets.push(item.targetValue || 0);
|
||||
barData.reals.push(item.value || 0);
|
||||
barData.rate.push(item.proportion || 0);
|
||||
barData.flags.push(item.completed ? 1 : 0);
|
||||
// barData.baseIndexes.push(Number(item.levelId) || 0); // 序号转数字
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 更新chartData
|
||||
this.chartData = { topBarData, barData };
|
||||
console.log('levelId=1数据:', this.chartData.topBarData);
|
||||
console.log('levelId≠1数据(locations=levelId对应地名):', this.chartData.barData);
|
||||
}
|
||||
},
|
||||
};
|
||||
</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>
|
||||
@@ -0,0 +1,241 @@
|
||||
<template>
|
||||
<div class="lineBottom" style="height: 160px; width: 100%">
|
||||
<operatingLineBarSaleSingle :refName="'totalOperating'" :chartData="chartD" style="height: 100%; width: 100%" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import operatingLineBarSaleSingle from './operatingLineBarSaleSingle.vue';
|
||||
|
||||
export default {
|
||||
name: "Container",
|
||||
components: { operatingLineBarSaleSingle },
|
||||
props: ["detailData"],
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
locations() {
|
||||
return ['预算', '实际'];
|
||||
},
|
||||
chartD() {
|
||||
// 背景图片路径(若不需要可注释)
|
||||
// const bgImageUrl = require('@/assets/img/labelBg.png');
|
||||
const rate = this.detailData?.proportion? Number(this.detailData?.proportion) : 0
|
||||
const diff = this.detailData?.diffValue || 0
|
||||
console.log('diff', diff);
|
||||
|
||||
const seriesData = [
|
||||
{
|
||||
value: this.detailData?.targetValue || 0,
|
||||
flag: 1, // 实际项:达标(绿色)
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
offset: [0, 0],
|
||||
},
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(130, 204, 255, 1)' },
|
||||
{ offset: 1, color: 'rgba(75, 157, 255, 1)' }
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
},
|
||||
{
|
||||
value: this.detailData?.value || 0,
|
||||
flag: this.detailData?.completed, // 实际项:达标(绿色)
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
offset: [0, 0],
|
||||
},
|
||||
itemStyle: {
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
const data = {
|
||||
allPlaceNames: ['预算', '实际'],
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: 60,
|
||||
barCategoryGap: '50%',
|
||||
data: seriesData,
|
||||
itemStyle: {
|
||||
color: (params) => {
|
||||
const currentFlag = params.data.flag || 0;
|
||||
return currentFlag === 1
|
||||
? {
|
||||
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)' }
|
||||
]
|
||||
}
|
||||
: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(253, 209, 129, 1)' },
|
||||
{ offset: 1, color: 'rgba(249, 164, 74, 1)' }
|
||||
]
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
console.log('data', data);
|
||||
|
||||
return data;
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.coreBar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
|
||||
.barTop {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
|
||||
.title {
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFangSC;
|
||||
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;
|
||||
margin-right: 46px;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: PingFangSC;
|
||||
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);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
position: relative;
|
||||
gap: 2px;
|
||||
width: 283px;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
background: #ecf4fe;
|
||||
border-radius: 12px;
|
||||
margin: 0;
|
||||
|
||||
.item-button {
|
||||
cursor: pointer;
|
||||
width: 142px;
|
||||
height: 24px;
|
||||
font-family: PingFangSC;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #0b58ff;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
letter-spacing: 8px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.item-button.active {
|
||||
width: 142px;
|
||||
height: 24px;
|
||||
background: #3071ff;
|
||||
border-radius: 12px;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,365 @@
|
||||
<template>
|
||||
<div class="coreBar">
|
||||
<div class="lineBottom" style="height: 320px; width: 100%">
|
||||
<operatingLineBar :chartData="chartD" style="height: 99%; width: 100%" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import operatingLineBar from './operatingLineBarSaleGroup.vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: "Container",
|
||||
components: { operatingLineBar },
|
||||
props: ["chartData"],
|
||||
data() {
|
||||
return {
|
||||
activeButton: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentDataSource() {
|
||||
console.log('yyyy', this.chartData);
|
||||
|
||||
return this.chartData
|
||||
},
|
||||
locations() {
|
||||
console.log('this.1111', this.chartData);
|
||||
|
||||
return this.chartData.locations
|
||||
},
|
||||
// 根据按钮切换生成对应的 chartData
|
||||
chartD() {
|
||||
// 销量场景数据
|
||||
const data = this.currentDataSource;
|
||||
const diff = data.diff[0]
|
||||
const rate = data.rate[0]
|
||||
console.log(this.currentDataSource, 'currentDataSource');
|
||||
|
||||
const salesData = {
|
||||
allPlaceNames: this.locations,
|
||||
series: [
|
||||
// 1. 完成率(折线图)
|
||||
// {
|
||||
// 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(40, 138, 255, 0)' }
|
||||
// ])
|
||||
// },
|
||||
// data: data.rates, // 完成率(%)
|
||||
// symbol: 'circle',
|
||||
// symbolSize: 6
|
||||
// },
|
||||
// 2. 目标(柱状图)
|
||||
{
|
||||
name: '预算',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0, // 左侧Y轴(万元)
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top'
|
||||
},
|
||||
barWidth: 65,
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(130, 204, 255, 1)' },
|
||||
{ offset: 1, color: 'rgba(75, 157, 255, 1)' }
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: data.targets // 目标销量(万元)
|
||||
},
|
||||
// 3. 实际(柱状图,含达标状态)
|
||||
{
|
||||
name: '实际',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top'
|
||||
},
|
||||
barWidth: 65,
|
||||
itemStyle: {
|
||||
color: (params) => {
|
||||
// 达标状态:1=达标(绿色),0=未达标(橙色)
|
||||
const safeFlag = data.flags;
|
||||
const currentFlag = safeFlag[params.dataIndex] || 0;
|
||||
return currentFlag === 1
|
||||
? {
|
||||
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)' }
|
||||
]
|
||||
}
|
||||
: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(253, 209, 129, 1)' },
|
||||
{ offset: 1, color: 'rgba(249, 164, 74, 1)' }
|
||||
]
|
||||
};
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: data.reals // 实际销量(万元)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// 毛利率场景数据
|
||||
const grossProfitData = {
|
||||
series: [
|
||||
// 1. 完成率(折线图)
|
||||
{
|
||||
name: '完成率',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
lineStyle: { color: 'rgba(40, 138, 255, .5)', width: 2 },
|
||||
itemStyle: {
|
||||
color: 'rgba(40, 138, 255, 1)',
|
||||
borderColor: 'rgba(40, 138, 255, 1)',
|
||||
borderWidth: 2,
|
||||
radius: 4
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.2,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(40, 138, 255, .9)' },
|
||||
{ offset: 1, color: 'rgba(40, 138, 255, 0)' }
|
||||
])
|
||||
},
|
||||
data: [106.7, 96.9, 106.5, 106.1, 93.8, 105.9], // 毛利率完成率(%)
|
||||
symbol: 'circle',
|
||||
symbolSize: 6
|
||||
},
|
||||
// 2. 目标(柱状图)
|
||||
{
|
||||
name: '预算',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 14,
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(130, 204, 255, 1)' },
|
||||
{ offset: 1, color: 'rgba(75, 157, 255, 1)' }
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: [30, 32, 31, 33, 32, 34] // 目标毛利率(万元)
|
||||
},
|
||||
// 3. 实际(柱状图)
|
||||
{
|
||||
name: '实际',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 14,
|
||||
itemStyle: {
|
||||
color: (params) => {
|
||||
const safeFlag = [1, 0, 1, 1, 0, 1]; // 达标状态
|
||||
const currentFlag = safeFlag[params.dataIndex] || 0;
|
||||
return currentFlag === 1
|
||||
? {
|
||||
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)' }
|
||||
]
|
||||
}
|
||||
: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(253, 209, 129, 1)' },
|
||||
{ offset: 1, color: 'rgba(249, 164, 74, 1)' }
|
||||
]
|
||||
};
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
},
|
||||
data: [32, 31, 33, 35, 30, 36] // 实际毛利率(万元)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// 根据按钮状态返回对应数据
|
||||
return this.activeButton === 0 ? salesData : grossProfitData;
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.coreBar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
|
||||
.barTop {
|
||||
display: flex;
|
||||
justify-content: flex-end; // 标题左、右侧容器右,整体两端对齐
|
||||
align-items: center; // 垂直居中,避免上下错位
|
||||
gap: 16px; // 标题与右侧容器的最小间距,防止拥挤
|
||||
width: 100%; // 确保占满父容器,实现两端对齐
|
||||
|
||||
.title {
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
// 标题固定在左侧,不挤压右侧空间
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 1. 右侧容器:包裹图例和按钮组,整体靠右
|
||||
.right-container {
|
||||
display: flex;
|
||||
align-items: center; // 图例和按钮组垂直居中
|
||||
gap: 24px; // 图例与按钮组的间距,避免贴紧
|
||||
margin-right: 46px; // 右侧整体留边,与原按钮组边距一致
|
||||
}
|
||||
|
||||
// 2. 图例:在右侧容器内横向排列
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 16px; // 图例项之间间距,避免重叠
|
||||
align-items: center;
|
||||
// 移除原margin-left,避免位置偏移
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
white-space: nowrap; // 防止图例文字换行
|
||||
}
|
||||
|
||||
.legend-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.legend-icon.line {
|
||||
width: 12px;
|
||||
height: 2px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: -2px;
|
||||
left: 3px;
|
||||
width: 6px;
|
||||
border-radius: 50%;
|
||||
height: 6px;
|
||||
background-color: rgba(40, 138, 255, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.legend-icon.square {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
// 图例颜色
|
||||
.yield {
|
||||
background: rgba(40, 138, 255, 1);
|
||||
}
|
||||
|
||||
.target {
|
||||
background: #2889FF;
|
||||
}
|
||||
|
||||
.achieved {
|
||||
background: rgba(40, 203, 151, 1);
|
||||
}
|
||||
|
||||
.unachieved {
|
||||
background: rgba(255, 132, 0, 1);
|
||||
}
|
||||
|
||||
// 3. 按钮组:在右侧容器内,保留原有样式
|
||||
.button-group {
|
||||
display: flex;
|
||||
position: relative;
|
||||
gap: 2px;
|
||||
width: 283px;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
background: #ecf4fe;
|
||||
border-radius: 12px;
|
||||
// 移除原margin-right,由右侧容器统一控制
|
||||
margin: 0;
|
||||
|
||||
.item-button {
|
||||
cursor: pointer;
|
||||
width: 142px;
|
||||
height: 24px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #0b58ff;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
letter-spacing: 8px; // 确保文字间距生效
|
||||
padding-left: 8px; // 抵消letter-spacing导致的文字左偏
|
||||
}
|
||||
|
||||
.item-button.active {
|
||||
width: 142px;
|
||||
height: 24px;
|
||||
background: #3071ff;
|
||||
border-radius: 12px;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container :isShowTab="true" :name="title" icon="cockpitItemIcon" size="opLargeBg" topSize="large"
|
||||
@tabChange="handleChange">
|
||||
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%;">
|
||||
<div class="topItem-container" style="display: flex; gap: 8px">
|
||||
<div
|
||||
v-for="item in sortedIndicators"
|
||||
:key="item.key"
|
||||
class="dashboard"
|
||||
@click="item.route && handleDashboardClick(item.route)"
|
||||
>
|
||||
<div class="title">
|
||||
{{ item.name }}·{{ item.unit }}
|
||||
</div>
|
||||
<div style='font-size: 16px;text-align: right;padding-right: 5px;'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{item.data.proportion}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:item.data.completed>0?'#30B590':'#FF9423'}" >{{item.data.diffValue}}</span></span>
|
||||
</div>
|
||||
<operatingSingleBar :detailData="item.data"></operatingSingleBar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Container from '../components/container.vue'
|
||||
import operatingSingleBar from './operatingSingleBar.vue'
|
||||
|
||||
// import * as echarts from 'echarts'
|
||||
// import rawItem from './raw-Item.vue'
|
||||
|
||||
export default {
|
||||
name: 'ProductionStatus',
|
||||
components: { Container, operatingSingleBar },
|
||||
props: {
|
||||
// 接收父组件传递的 月度+累计 组合数据
|
||||
relatedData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
relatedMon: [], // 月度数据(数组格式,存储销量/单价等数据)
|
||||
relatedTotal: [] // 累计数据(数组格式,存储销量/单价等数据)
|
||||
})
|
||||
},
|
||||
dateData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
factory: {
|
||||
type: [Number, String],
|
||||
default: ''
|
||||
},
|
||||
// 可选:动态标题
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
// 核心:当前激活的数据集(月度/累计),默认初始化月度数据
|
||||
activeData: this.relatedData.relatedMon || []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
indicatorDefs() {
|
||||
return [
|
||||
{ key: 'sales', name: '销量', unit: '万㎡', route: '/salesVolumeAnalysis/salesVolumeAnalysisBase'},
|
||||
{ key: 'price', name: '单价', unit: '元/㎡', route: '/unitPriceAnalysis/unitPriceAnalysisBase'},
|
||||
{ key: 'mfgCost', name: '制造成本', unit: '元/㎡', route: '/productionCostAnalysis/productionCostAnalysisBase'},
|
||||
{ key: 'mgmtFee', name: '管理费用', unit: '万元', route: '/expenseAnalysis/expenseAnalysisBase'},
|
||||
{ key: 'salesFee', name: '销售费用', unit: '万元', route: '/expenseAnalysis/expenseAnalysisBase'},
|
||||
{ key: 'finFee', name: '财务费用', unit: '万元', route: '/expenseAnalysis/expenseAnalysisBase'},
|
||||
{ key: 'nonOpProfit', name: '非经营性利润', unit: '万元', route: null}
|
||||
]
|
||||
},
|
||||
indicators() {
|
||||
const fallback = { targetValue: 0, value: 0, completed: 0, diffValue: 0 }
|
||||
const list = (Array.isArray(this.activeData) ? this.activeData : [])
|
||||
|
||||
return this.indicatorDefs.map(def => {
|
||||
const data = list.find(item => item && item.name === def.name) || fallback
|
||||
return {
|
||||
...def,
|
||||
data,
|
||||
sortValue: Number((data && data.value) ?? 0)
|
||||
}
|
||||
})
|
||||
},
|
||||
sortedIndicators() {
|
||||
const unitOrder = ['万㎡', '元/㎡', '万元']
|
||||
const unitRank = (u) => {
|
||||
const idx = unitOrder.indexOf(u)
|
||||
return idx === -1 ? 999 : idx
|
||||
}
|
||||
|
||||
return this.indicators.slice().sort((a, b) => {
|
||||
const ur = unitRank(a.unit) - unitRank(b.unit)
|
||||
if (ur !== 0) return ur
|
||||
const vr = (b.sortValue ?? -Infinity) - (a.sortValue ?? -Infinity)
|
||||
if (vr !== 0) return vr
|
||||
return String(a.key).localeCompare(String(b.key))
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 可选:监听 relatedData 初始变化(若父组件异步传递数据,确保 activeData 同步更新)
|
||||
relatedData: {
|
||||
handler(newVal) {
|
||||
this.activeData = newVal.relatedMon || [];
|
||||
},
|
||||
immediate: true,
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log('组件挂载时的激活数据:', this.activeData);
|
||||
},
|
||||
methods: {
|
||||
handleDashboardClick(path) {
|
||||
this.$router.push({
|
||||
path: path,
|
||||
query: {
|
||||
factory: this.$route.query.factory ? this.$route.query.factory : this.factory,
|
||||
dateData: this.dateData
|
||||
}
|
||||
})
|
||||
},
|
||||
/**
|
||||
* Tab 切换处理函数
|
||||
* @param {String} value 切换值('month' = 月度,'total' = 累计,可根据实际Tab值调整)
|
||||
*/
|
||||
handleChange(value) {
|
||||
console.log('Tab 切换值:', value);
|
||||
// 根据 Tab 值更新当前激活的数据集
|
||||
if (value === 'month') {
|
||||
// 切换为月度数据
|
||||
this.activeData = this.relatedData.relatedMon || [];
|
||||
} else {
|
||||
// 切换为累计数据(非 month 均视为累计,可根据实际需求调整判断条件)
|
||||
this.activeData = this.relatedData.relatedTotal || [];
|
||||
}
|
||||
console.log('当前激活数据集:', this.activeData);
|
||||
}
|
||||
}
|
||||
}
|
||||
</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;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
width: 220px;
|
||||
height: 205px;
|
||||
background: #F9FCFF;
|
||||
padding: 16px 0 0 10px;
|
||||
|
||||
.title {
|
||||
// width: 190px;
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
// width: 190px;
|
||||
height: 32px;
|
||||
font-family: YouSheBiaoTiHei;
|
||||
font-size: 32px;
|
||||
color: #0B58FF;
|
||||
line-height: 32px;
|
||||
letter-spacing: 2px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mom {
|
||||
width: 120px;
|
||||
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;
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,203 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container :name="title" icon="cockpitItemIcon" size="operatingRevenueBg" 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; gap: 8px;">
|
||||
<div class="dashboard">
|
||||
<div class="title">
|
||||
累计完成率
|
||||
</div>
|
||||
<div class="number">
|
||||
<div class="yield">
|
||||
{{ formatRate(factoryData?.completeRate) }}%
|
||||
</div>
|
||||
<div class="mom">
|
||||
同比{{ formatRate(factoryData?.thb) }}%
|
||||
<img v-if="factoryData?.thb >= 0" class="arrow" src="../../../assets/img/topArrow.png" alt="上升">
|
||||
<img v-else class="arrow" src="../../../assets/img/downArrow.png" alt="下降">
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="electricityGauge">
|
||||
<electricityGauge id="year" :detailData="factoryData"></electricityGauge>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="line" style="padding: 0px;">
|
||||
<!-- 传递包含flag的factoryData给柱状图组件 -->
|
||||
<verticalBarChart :detailData="factoryData"></verticalBarChart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Container from './container.vue'
|
||||
import electricityGauge from './electricityGauge.vue'
|
||||
import verticalBarChart from './verticalBarChart.vue'
|
||||
|
||||
// import * as echarts from 'echarts'
|
||||
// import rawItem from './raw-Item.vue'
|
||||
|
||||
export default {
|
||||
name: 'ProductionStatus',
|
||||
components: { Container, electricityGauge, verticalBarChart },
|
||||
props: {
|
||||
totalData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
month: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* 自动提取monData中的工厂数据,并新增flag字段
|
||||
*/
|
||||
factoryData() { // 整合原始数据 + 计算flag
|
||||
return {
|
||||
completeRate: this.totalData.proportion ? Number(this.totalData.proportion) : 0,
|
||||
diff: this.totalData.diffValue,
|
||||
real: this.totalData.value,
|
||||
target: this.totalData.targetValue,
|
||||
thb: this.totalData.thb,
|
||||
// ...rawData,
|
||||
flag: this.totalData.completed// 新增flag字段
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 格式化百分比数值:处理空值/非数字,兜底为0
|
||||
*/
|
||||
formatRate(value) {
|
||||
if (isNaN(value) || value === null || value === undefined) {
|
||||
return 0;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
/**
|
||||
* 判断完成率对应的flag值(<100为0,≥100为1)
|
||||
* @param {number} rate 完成率(原始值,如89代表89%)
|
||||
* @returns {0|1} flag值
|
||||
*/
|
||||
}
|
||||
}
|
||||
</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;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
width: 264px;
|
||||
height: 205px;
|
||||
background: #F9FCFF;
|
||||
padding: 16px 0 0 10px;
|
||||
|
||||
.title {
|
||||
// width: 190px;
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-family: YouSheBiaoTiHei;
|
||||
font-size: 46px;
|
||||
color: #0B58FF;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.mom {
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 500px;
|
||||
height: 205px;
|
||||
background: #F9FCFF;
|
||||
}
|
||||
|
||||
// .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> -->
|
||||
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<div style="width: 100%; height: 210px;position: relative;">
|
||||
<div style='font-size: 16px;position: absolute;right: 20px;top:10px'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{detailData.completeRate}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:detailData.flag>0?'#30B590':'#FF9423'}" >{{detailData.diff}}</span></span>
|
||||
</div>
|
||||
<div :ref="refName" id="coreLineChart" style="width: 100%; height: 210px;"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
myChart: null
|
||||
};
|
||||
},
|
||||
props: {
|
||||
refName: {
|
||||
type: String,
|
||||
default: 'verticalBarChart',
|
||||
},
|
||||
detailData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
completeRate: 0,
|
||||
diff: 0,
|
||||
real: 0,
|
||||
target: 0,
|
||||
thb: 0,
|
||||
flag: 0
|
||||
}),
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => this.updateChart());
|
||||
},
|
||||
watch: {
|
||||
detailData: {
|
||||
handler() {
|
||||
this.updateChart();
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateChart() {
|
||||
const chartDom = this.$refs[this.refName];
|
||||
if (!chartDom) {
|
||||
console.error('图表容器未找到!');
|
||||
return;
|
||||
}
|
||||
console.log('this.detailData', this.detailData);
|
||||
|
||||
// 修复:优化实例销毁逻辑,避免重复dispose
|
||||
if (this.myChart) {
|
||||
this.myChart.clear(); // 先清空,再重新渲染
|
||||
} else {
|
||||
this.myChart = echarts.init(chartDom);
|
||||
}
|
||||
|
||||
// 解构数据,避免重复取值
|
||||
const { diff, completeRate, real, target, flag } = this.detailData;
|
||||
// 确保数值为数字类型
|
||||
const realValue = Number(real) || 0;
|
||||
const targetValue = Number(target) || 0;
|
||||
const diffValue = Number(diff) || 0;
|
||||
const rateValue = Number(completeRate) || 0;
|
||||
const flagValue = Number(flag) || 0;
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: { backgroundColor: '#6a7985' }
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: 40,
|
||||
bottom: 15,
|
||||
right: 80,
|
||||
left: 10,
|
||||
containLabel: true,
|
||||
show: false
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
axisTick: { show: false },
|
||||
min: 0,
|
||||
splitNumber: 4,
|
||||
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] }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
axisLabel: { color: 'rgba(0, 0, 0, 0.75)', fontSize: 12, interval: 0, padding: [5, 0, 0, 0] },
|
||||
axisLine: { show: true, lineStyle: { color: '#E5E6EB', width: 1, type: 'solid' } },
|
||||
axisTick: { show: false },
|
||||
data: ['实际', '预算']
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: 24,
|
||||
// 修复:拆分数据项,确保每个柱子的样式独立生效
|
||||
data: [
|
||||
// 实际值柱子(核心:绑定flag颜色)
|
||||
{
|
||||
value: realValue,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
fontSize: 14
|
||||
},
|
||||
// 修复:flag颜色判断独立绑定到实际值柱子
|
||||
itemStyle: {
|
||||
color: flagValue === 1
|
||||
? {
|
||||
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)' }
|
||||
]
|
||||
}
|
||||
: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(253, 209, 129, 1)' },
|
||||
{ offset: 1, color: 'rgba(249, 164, 74, 1)' }
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
}
|
||||
},
|
||||
// 预算值柱子(固定蓝色渐变)
|
||||
{
|
||||
value: targetValue,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
fontSize: 14
|
||||
},
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 1, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: '#82CCFF' },
|
||||
{ offset: 1, color: '#4B9DFF' }
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderWidth: 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.myChart.setOption(option, true); // 新增:true表示替换所有配置,避免缓存
|
||||
|
||||
// 优化:防抖resize,避免频繁触发
|
||||
const resizeHandler = () => {
|
||||
this.myChart && this.myChart.resize();
|
||||
};
|
||||
window.removeEventListener('resize', resizeHandler); // 先移除再添加,避免重复绑定
|
||||
window.addEventListener('resize', resizeHandler);
|
||||
|
||||
this.$once('hook:destroyed', () => {
|
||||
window.removeEventListener('resize', resizeHandler);
|
||||
this.myChart && this.myChart.dispose();
|
||||
this.myChart = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,203 @@
|
||||
<template>
|
||||
<div style="flex: 1">
|
||||
<Container :name="title" icon="cockpitItemIcon" size="operatingRevenueBg" 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; gap: 8px;">
|
||||
<div class="dashboard left">
|
||||
<div class="title">
|
||||
销量·万元
|
||||
</div>
|
||||
<div class="line">
|
||||
<operatingSingleBar></operatingSingleBar>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard right">
|
||||
<div class="title">
|
||||
单价·万元
|
||||
</div>
|
||||
<div class="line">
|
||||
<operatingSingleBar></operatingSingleBar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Container from './container.vue'
|
||||
import operatingSingleBar from './operatingSingleBar.vue'
|
||||
import verticalBarChart from './verticalBarChart.vue'
|
||||
|
||||
// import * as echarts from 'echarts'
|
||||
// import rawItem from './raw-Item.vue'
|
||||
|
||||
export default {
|
||||
name: 'ProductionStatus',
|
||||
components: { Container, operatingSingleBar, verticalBarChart },
|
||||
// mixins: [resize],
|
||||
props: {
|
||||
itemData: { // 接收父组件传递的设备数据数组
|
||||
type: Array,
|
||||
default: () => [] // 默认空数组,避免报错
|
||||
},
|
||||
title: { // 接收父组件传递的设备数据数组
|
||||
type: String,
|
||||
default: () => '' // 默认空数组,避免报错
|
||||
},
|
||||
month: { // 接收父组件传递的设备数据数组
|
||||
type: String,
|
||||
default: () => '' // 默认空数组,避免报错
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
itemData: {
|
||||
handler(newValue, oldValue) {
|
||||
// this.updateChart()
|
||||
},
|
||||
deep: true // 若对象内属性变化需触发,需加 deep: true
|
||||
}
|
||||
},
|
||||
// computed: {
|
||||
// // 处理排序:包含“总成本”的项放前面,其余项按原顺序排列
|
||||
// sortedItemData() {
|
||||
// // 过滤出包含“总成本”的项(不区分大小写)
|
||||
// const totalCostItems = this.itemData.filter(item =>
|
||||
// item.name && item.name.includes('总成本')
|
||||
// );
|
||||
// // 过滤出不包含“总成本”的项
|
||||
// const otherItems = this.itemData.filter(item =>
|
||||
// !item.name || !item.name.includes('总成本')
|
||||
// );
|
||||
// // 合并:总成本项在前,其他项在后
|
||||
// return [...totalCostItems, ...otherItems];
|
||||
// }
|
||||
// },
|
||||
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;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
width: 382px;
|
||||
height: 205px;
|
||||
background: #F9FCFF;
|
||||
padding: 16px 0 0 10px;
|
||||
|
||||
.title {
|
||||
// width: 190px;
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
// width: 190px;
|
||||
height: 32px;
|
||||
font-family: YouSheBiaoTiHei;
|
||||
font-size: 32px;
|
||||
color: #0B58FF;
|
||||
line-height: 32px;
|
||||
letter-spacing: 2px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mom {
|
||||
width: 120px;
|
||||
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;
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
|
||||
// .line {
|
||||
// width: 500px;
|
||||
// height: 205px;
|
||||
// background: #F9FCFF;
|
||||
// }
|
||||
|
||||
// .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> -->
|
||||
@@ -19,9 +19,9 @@
|
||||
<img v-else class="arrow" src="../../../assets/img/downArrow.png" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="electricityGauge">
|
||||
<!-- <div class="electricityGauge">
|
||||
<electricityGauge :detailData="monthData" id="month"></electricityGauge>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="line" style="padding: 0px;">
|
||||
<verticalBarChart :detailData="monthData">
|
||||
@@ -142,33 +142,27 @@ export default {
|
||||
}
|
||||
|
||||
.number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
// width: 190px;
|
||||
height: 32px;
|
||||
font-family: YouSheBiaoTiHei;
|
||||
font-size: 32px;
|
||||
font-size: 46px;
|
||||
color: #0B58FF;
|
||||
line-height: 32px;
|
||||
letter-spacing: 2px;
|
||||
text-align: left;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
white-space: nowrap;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.mom {
|
||||
// width: 97px;
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
font-size: 20px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: left;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
z-index: 1000;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,33 +2,21 @@
|
||||
<div style="flex: 1">
|
||||
<Container :name="title" icon="cockpitItemIcon" size="operatingRevenueBg" topSize="middle">
|
||||
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%;">
|
||||
<div class="topItem-container" style="display: flex; gap: 8px; width: 100%;">
|
||||
<!-- 管理费用模块 -->
|
||||
<div class="dashboard">
|
||||
<div class="topItem-container" style="display: flex; gap: 8px">
|
||||
<div
|
||||
v-for="item in sortedIndicators"
|
||||
:key="item.key"
|
||||
class="dashboard"
|
||||
@click="item.route && handleDashboardClick(item.route)"
|
||||
>
|
||||
<div class="title">
|
||||
管理费用·万元
|
||||
{{ item.name }}·{{ item.unit }}
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<operatingSingleBar :detailData="manageCostData"></operatingSingleBar>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 销售费用模块 -->
|
||||
<div class="dashboard">
|
||||
<div class="title">
|
||||
销售费用·万元
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<operatingSingleBar :detailData="saleCostData"></operatingSingleBar>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 财务费用模块 -->
|
||||
<div class="dashboard">
|
||||
<div class="title">
|
||||
财务费用·万元
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<operatingSingleBar :detailData="financeCostData"></operatingSingleBar>
|
||||
<div style='font-size: 16px;text-align: right;padding-right: 5px;'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{item.detailData.rate}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:item.detailData.flag>0?'#30B590':'#FF9423'}" >{{item.detailData.diff}}</span></span>
|
||||
</div>
|
||||
<operatingSingleBar :detailData="item.detailData"></operatingSingleBar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,69 +51,66 @@ export default {
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 初始化费用数据(包含flag字段)
|
||||
manageCostData: { title: "管理费用", budget: 0, real: 0, rate: 0, diff: 0, flag: 0 },
|
||||
saleCostData: { title: "销售费用", budget: 0, real: 0, rate: 0, diff: 0, flag: 0 },
|
||||
financeCostData: { title: "财务费用", budget: 0, real: 0, rate: 0, diff: 0, flag: 0 }
|
||||
}
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
indicatorDefs() {
|
||||
return [
|
||||
{ key: 'mgmtFee', name: '管理费用', unit: '万元'},
|
||||
{ key: 'salesFee', name: '销售费用', unit: '万元'},
|
||||
{ key: 'finFee', name: '财务费用', unit: '万元'}
|
||||
]
|
||||
},
|
||||
indicators() {
|
||||
let _this = this
|
||||
const fallback = { budget: 0, real: 0, rate: 0, diff: 0, flag: 0 }
|
||||
const list = (Array.isArray(_this.monthAnalysis) ? _this.monthAnalysis : [])
|
||||
return _this.indicatorDefs.map(def => {
|
||||
const data = list.find(item => item && item.title === def.name) || fallback
|
||||
const detailData = {
|
||||
...data,
|
||||
flag: _this.getRateFlag(data.rate, data.real, data.budget),
|
||||
}
|
||||
return {
|
||||
...def,
|
||||
detailData,
|
||||
sortValue: Number((data && data.real) ?? 0)
|
||||
}
|
||||
})
|
||||
},
|
||||
sortedIndicators() {
|
||||
const unitOrder = ['万元']
|
||||
const unitRank = (u) => {
|
||||
const idx = unitOrder.indexOf(u)
|
||||
return idx === -1 ? 999 : idx
|
||||
}
|
||||
|
||||
return this.indicators.slice().sort((a, b) => {
|
||||
const ur = unitRank(a.unit) - unitRank(b.unit)
|
||||
if (ur !== 0) return ur
|
||||
const vr = (b.sortValue ?? -Infinity) - (a.sortValue ?? -Infinity)
|
||||
if (vr !== 0) return vr
|
||||
return String(a.key).localeCompare(String(b.key))
|
||||
})
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
monthAnalysis: {
|
||||
handler(newVal) {
|
||||
this.updateCostData(newVal)
|
||||
},
|
||||
handler(newVal) {},
|
||||
deep: true,
|
||||
immediate: true // 初始化立即执行
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateCostData(this.monthAnalysis)
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
// 达标标识判断(≥100返回1,<100返回0)
|
||||
getRateFlag(rate, real, target) {
|
||||
if (isNaN(rate) || rate === null || rate === undefined) return 0;
|
||||
|
||||
// 1. 完成率 >= 100 => 达标
|
||||
if (rate >= 100) return 1;
|
||||
|
||||
// 2. 完成率 = 0 且 (目标值=0 或 实际值=目标值=0) => 达标
|
||||
if (rate === 0 && target === 0) return 1;
|
||||
|
||||
// 其他情况 => 未达标
|
||||
return 0;
|
||||
},
|
||||
|
||||
// 处理费用数据
|
||||
updateCostData(data) {
|
||||
// 数据兜底:确保是数组且长度≥3
|
||||
const validData = Array.isArray(data) && data.length >= 3
|
||||
? data
|
||||
: this.$props.monthAnalysis;
|
||||
|
||||
// 提取三个费用项数据(兜底处理)
|
||||
const manageItem = validData[0] || { title: "管理费用", budget: 0, real: 0, rate: 0, diff: 0 };
|
||||
const saleItem = validData[1] || { title: "销售费用", budget: 0, real: 0, rate: 0, diff: 0 };
|
||||
const financeItem = validData[2] || { title: "财务费用", budget: 0, real: 0, rate: 0, diff: 0 };
|
||||
|
||||
// 整合flag字段
|
||||
this.manageCostData = {
|
||||
...manageItem,
|
||||
flag: this.getRateFlag(manageItem.rate, manageItem.real, manageItem.budget)
|
||||
};
|
||||
this.saleCostData = {
|
||||
...saleItem,
|
||||
flag: this.getRateFlag(saleItem.rate, saleItem.real, saleItem.budget)
|
||||
};
|
||||
this.financeCostData = {
|
||||
...financeItem,
|
||||
flag: this.getRateFlag(financeItem.rate, financeItem.real, financeItem.budget)
|
||||
};
|
||||
// 调试日志
|
||||
console.log('管理费用数据:', this.manageCostData);
|
||||
console.log('销售费用数据:', this.saleCostData);
|
||||
console.log('财务费用数据:', this.financeCostData);
|
||||
getRateFlag(rate, real, target) {
|
||||
if (isNaN(rate) || rate === null || rate === undefined) return 0;
|
||||
// 1. 完成率 >= 100 => 达标
|
||||
if (rate >= 100) return 1;
|
||||
// 2. 完成率 = 0 且 (目标值=0 或 实际值=目标值=0) => 达标
|
||||
if (rate === 0 && target === 0) return 1;
|
||||
// 其他情况 => 未达标
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,7 +154,6 @@ getRateFlag(rate, real, target) {
|
||||
line-height: 18px;
|
||||
letter-spacing: 2px;
|
||||
text-align: left;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
// 图表容器:适配高度
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="lineBottom" style="height: 100%; width: 100%">
|
||||
<operatingLineBar :dateData="dateData" :chartData="chartD" style="height: 99%; width: 100%" />
|
||||
<operatingLineBar :dateData="dateData" :chartData="chartD" style="height: 100%; width: 100%" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -152,7 +152,7 @@ export default {
|
||||
name: '预算',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 14,
|
||||
barWidth: 40,
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
@@ -172,13 +172,13 @@ export default {
|
||||
name: '实际',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 14,
|
||||
barWidth: 40,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
offset: [30, 0],
|
||||
width: 68,
|
||||
height: 20,
|
||||
offset: [32, 0],
|
||||
width: 100,
|
||||
height: 22,
|
||||
formatter: (params) => {
|
||||
const diff = data.diff || [];
|
||||
const flags = data.flags || [];
|
||||
@@ -212,23 +212,21 @@ export default {
|
||||
borderRadius: 4,
|
||||
borderColor: '#BFCBD577',
|
||||
borderWidth: 0,
|
||||
lineHeight: 20,
|
||||
lineHeight: 26,
|
||||
rich: {
|
||||
text: {
|
||||
width: 'auto',
|
||||
padding: [5, 10, 5, 0],
|
||||
align: 'center',
|
||||
color: '#464646',
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
fontSize: 14
|
||||
},
|
||||
achieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#76DABE', // 与达标的 offset: 1 颜色一致
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
fontSize: 14
|
||||
},
|
||||
// 未达标样式
|
||||
unachieved: {
|
||||
@@ -236,8 +234,7 @@ export default {
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#F9A44A', // 与未达标的 offset: 1 颜色一致
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
fontSize: 14
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -133,11 +133,12 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 30,
|
||||
right: 70,
|
||||
left: 60
|
||||
bottom: 5,
|
||||
right: 20,
|
||||
left: 25,
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 400px;"></div>
|
||||
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 380px;"></div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
@@ -85,7 +85,7 @@ export default {
|
||||
},
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 5,
|
||||
bottom: 20,
|
||||
right: 10,
|
||||
left: 25,
|
||||
containLabel: true
|
||||
|
||||
@@ -85,8 +85,8 @@ export default {
|
||||
},
|
||||
grid: {
|
||||
top: 25,
|
||||
bottom: 30,
|
||||
right: 0,
|
||||
bottom: 25,
|
||||
right: 10,
|
||||
left: 2,
|
||||
containLabel: true
|
||||
},
|
||||
|
||||
@@ -10,17 +10,20 @@
|
||||
background-color: rgba(249, 252, 255, 1);
|
||||
flex-direction: column;
|
||||
">
|
||||
<div style="
|
||||
font-weight: 400;
|
||||
<div style="
|
||||
padding: 16px 16px 5px 16px;
|
||||
line-height: 18px;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
font-style: normal;
|
||||
padding: 16px 0 0 16px;
|
||||
">
|
||||
集团情况
|
||||
</div>
|
||||
集团情况
|
||||
</div>
|
||||
<div style='font-size: 16px;line-height: 16px;text-align: right;padding-right: 16px;'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{chartData.group.rate[0]}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:chartData.group.flags>0?'#30B590':'#FF9423'}" >{{chartData.group.diff[0]}}</span></span>
|
||||
</div>
|
||||
<operatingTopBar :chartData="chartData" />
|
||||
</div>
|
||||
<div class="right" style="
|
||||
|
||||
@@ -10,17 +10,20 @@
|
||||
background-color: rgba(249, 252, 255, 1);
|
||||
flex-direction: column;
|
||||
">
|
||||
<div style="
|
||||
font-weight: 400;
|
||||
<div style="
|
||||
padding: 16px 16px 5px 16px;
|
||||
line-height: 18px;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
font-style: normal;
|
||||
padding: 16px 0 0 16px;
|
||||
">
|
||||
集团情况
|
||||
</div>
|
||||
集团情况
|
||||
</div>
|
||||
<div style='font-size: 16px;line-height: 16px;text-align: right;padding-right: 16px;'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{chartData.group.rate[0]}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:chartData.group.flags>0?'#30B590':'#FF9423'}" >{{chartData.group.diff[0]}}</span></span>
|
||||
</div>
|
||||
<operatingTopBar :chartData="chartData" />
|
||||
</div>
|
||||
<div class="right" style="
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="lineBottom" style="height: 180px; width: 100%">
|
||||
<operatingLineBarSaleSingle :refName="'totalOperating'" :chartData="chartD" style="height: 99%; width: 100%" />
|
||||
<div class="lineBottom" style="height: 160px; width: 100%">
|
||||
<operatingLineBarSaleSingle :refName="'totalOperating'" :chartData="chartD" style="height: 100%; width: 100%" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -25,7 +25,7 @@ export default {
|
||||
// const bgImageUrl = require('@/assets/img/labelBg.png');
|
||||
const rate = this.detailData?.rate || 0
|
||||
const diff = this.detailData?.diff || 0
|
||||
console.log('diff', diff);
|
||||
console.log('detailData========================', this.detailData);
|
||||
|
||||
const seriesData = [
|
||||
{
|
||||
@@ -35,59 +35,6 @@ export default {
|
||||
show: true,
|
||||
position: 'top',
|
||||
offset: [0, 0],
|
||||
// 固定label尺寸:68px×20px
|
||||
width: 68,
|
||||
height: 20,
|
||||
// 关键:去掉换行,让文字在一行显示,适配小尺寸
|
||||
formatter: function (params) {
|
||||
return `{value|完成率}{rate|${rate}%}`;
|
||||
},
|
||||
// 核心样式:匹配CSS需求
|
||||
backgroundColor: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(205, 215, 224, 0.6)' }, // 顶部0px位置:阴影最强
|
||||
// { offset: 0.1, color: 'rgba(205, 215, 224, 0.4)' }, // 1px位置:阴影减弱(对应1px)
|
||||
// { offset: 0.15, color: 'rgba(205, 215, 224, 0.6)' }, // 3px位置:阴影几乎消失(对应3px扩散)
|
||||
{ offset: 0.2, color: '#ffffff' }, // 主体白色
|
||||
{ offset: 1, color: '#ffffff' }
|
||||
]
|
||||
},
|
||||
// 外阴影:0px 2px 2px 0px rgba(191,203,215,0.5)
|
||||
shadowColor: 'rgba(191,203,215,0.5)',
|
||||
shadowBlur: 2,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 2,
|
||||
// 圆角:4px
|
||||
borderRadius: 4,
|
||||
// 移除边框
|
||||
borderColor: '#BFCBD577',
|
||||
borderWidth: 0,
|
||||
// 文字垂直居中(针对富文本)
|
||||
lineHeight: 20,
|
||||
rich: {
|
||||
value: {
|
||||
// 缩小宽度和内边距,适配68px容器
|
||||
width: 'auto', // 自动宽度,替代固定40px
|
||||
padding: [5, 0, 5, 10], // 缩小内边距
|
||||
align: 'center',
|
||||
color: '#464646', // 文字灰色
|
||||
fontSize: 11, // 缩小字体,适配小尺寸
|
||||
lineHeight: 20 // 垂直居中
|
||||
},
|
||||
rate: {
|
||||
width: 'auto',
|
||||
padding: [5, 10, 5, 0],
|
||||
align: 'center',
|
||||
color: '#0B58FF', // 数字蓝色
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
}
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
color: {
|
||||
@@ -109,69 +56,6 @@ export default {
|
||||
show: true,
|
||||
position: 'top',
|
||||
offset: [0, 0],
|
||||
// 固定label尺寸:68px×20px
|
||||
width: 68,
|
||||
height: 20,
|
||||
// 关键:去掉换行,让文字在一行显示,适配小尺寸
|
||||
formatter: (params) => {
|
||||
|
||||
// const flags = flags || [];
|
||||
const currentDiff = diff || 0;
|
||||
const currentFlag = this.detailData?.flag || 0;
|
||||
// const prefix = currentFlag === 1 ? '+' : '-';
|
||||
// 根据标志位选择不同的样式类
|
||||
if (currentFlag === 1) {
|
||||
// 达标 - 使用 rate-achieved 样式
|
||||
return `{achieved|${currentDiff}}{text|差值}`;
|
||||
} else {
|
||||
// 未达标 - 使用 rate-unachieved 样式
|
||||
return `{unachieved|${currentDiff}}{text|差值}`;
|
||||
}
|
||||
},
|
||||
backgroundColor: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(205, 215, 224, 0.6)' },
|
||||
{ offset: 0.2, color: '#ffffff' },
|
||||
{ offset: 1, color: '#ffffff' }
|
||||
]
|
||||
},
|
||||
shadowColor: 'rgba(191,203,215,0.5)',
|
||||
shadowBlur: 2,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 2,
|
||||
borderRadius: 4,
|
||||
borderColor: '#BFCBD577',
|
||||
borderWidth: 0,
|
||||
lineHeight: 20,
|
||||
rich: {
|
||||
text: {
|
||||
width: 'auto',
|
||||
padding: [5, 10, 5, 0],
|
||||
align: 'center',
|
||||
color: '#464646',
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
},
|
||||
achieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#76DABE', // 与达标的 offset: 1 颜色一致
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
},
|
||||
// 未达标样式
|
||||
unachieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#F9A44A', // 与未达标的 offset: 1 颜色一致
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
}
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
@@ -185,11 +69,12 @@ export default {
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: 24,
|
||||
barWidth: 60,
|
||||
barCategoryGap: '50%',
|
||||
data: seriesData,
|
||||
itemStyle: {
|
||||
color: (params) => {
|
||||
console.log('params=================', params);
|
||||
const currentFlag = params.data.flag || 0;
|
||||
return currentFlag === 1
|
||||
? {
|
||||
|
||||
@@ -73,65 +73,9 @@ export default {
|
||||
yAxisIndex: 0, // 左侧Y轴(万元)
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
offset: [-30, 0],
|
||||
// 固定label尺寸:68px×20px
|
||||
width: 68,
|
||||
height: 20,
|
||||
// 关键:去掉换行,让文字在一行显示,适配小尺寸
|
||||
formatter: (params) => {
|
||||
// const { rate = 0, diff = 0 } = params.data || {};
|
||||
return `{value|完成率}{rate|${rate}%}`;
|
||||
},
|
||||
// formatter: `{value|完成率}{rate|${rate}%}`,
|
||||
// 核心样式:匹配CSS需求
|
||||
backgroundColor: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(205, 215, 224, 0.6)' }, // 顶部0px位置:阴影最强
|
||||
// { offset: 0.1, color: 'rgba(205, 215, 224, 0.4)' }, // 1px位置:阴影减弱(对应1px)
|
||||
// { offset: 0.15, color: 'rgba(205, 215, 224, 0.6)' }, // 3px位置:阴影几乎消失(对应3px扩散)
|
||||
{ offset: 0.2, color: '#ffffff' }, // 主体白色
|
||||
{ offset: 1, color: '#ffffff' }
|
||||
]
|
||||
},
|
||||
// 外阴影:0px 2px 2px 0px rgba(191,203,215,0.5)
|
||||
shadowColor: 'rgba(191,203,215,0.5)',
|
||||
shadowBlur: 2,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 2,
|
||||
// 圆角:4px
|
||||
borderRadius: 4,
|
||||
// 移除边框
|
||||
borderColor: '#BFCBD577',
|
||||
borderWidth: 0,
|
||||
// 文字垂直居中(针对富文本)
|
||||
lineHeight: 20,
|
||||
rich: {
|
||||
value: {
|
||||
// 缩小宽度和内边距,适配68px容器
|
||||
width: 'auto', // 自动宽度,替代固定40px
|
||||
padding: [5, 0, 5, 10], // 缩小内边距
|
||||
align: 'center',
|
||||
color: '#464646', // 文字灰色
|
||||
fontSize: 11, // 缩小字体,适配小尺寸
|
||||
lineHeight: 20 // 垂直居中
|
||||
},
|
||||
rate: {
|
||||
width: 'auto',
|
||||
padding: [5, 10, 5, 0],
|
||||
align: 'center',
|
||||
color: '#0B58FF', // 数字蓝色
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
}
|
||||
}
|
||||
position: 'top'
|
||||
},
|
||||
barWidth: 14,
|
||||
barWidth: 65,
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
@@ -153,73 +97,9 @@ export default {
|
||||
yAxisIndex: 0,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
offset: [30, 0],
|
||||
// 固定label尺寸:68px×20px
|
||||
width: 68,
|
||||
height: 20,
|
||||
// 关键:去掉换行,让文字在一行显示,适配小尺寸
|
||||
formatter: (params) => {
|
||||
|
||||
// const flags = flags || [];
|
||||
const currentDiff = diff || 0;
|
||||
const currentFlag = data.flags[0] || 0;
|
||||
// const prefix = currentFlag === 1 ? '+' : '-';
|
||||
// 根据标志位选择不同的样式类
|
||||
if (currentFlag === 1) {
|
||||
// 达标 - 使用 rate-achieved 样式
|
||||
return `{achieved|${currentDiff}}{text|差值}`;
|
||||
} else {
|
||||
// 未达标 - 使用 rate-unachieved 样式
|
||||
return `{unachieved|${currentDiff}}{text|差值}`;
|
||||
}
|
||||
},
|
||||
backgroundColor: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(205, 215, 224, 0.6)' },
|
||||
{ offset: 0.2, color: '#ffffff' },
|
||||
{ offset: 1, color: '#ffffff' }
|
||||
]
|
||||
},
|
||||
shadowColor: 'rgba(191,203,215,0.5)',
|
||||
shadowBlur: 2,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 2,
|
||||
borderRadius: 4,
|
||||
borderColor: '#BFCBD577',
|
||||
borderWidth: 0,
|
||||
lineHeight: 20,
|
||||
rich: {
|
||||
text: {
|
||||
width: 'auto',
|
||||
padding: [5, 10, 5, 0],
|
||||
align: 'center',
|
||||
color: '#464646',
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
},
|
||||
achieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#76DABE', // 与达标的 offset: 1 颜色一致
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
},
|
||||
// 未达标样式
|
||||
unachieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#F9A44A', // 与未达标的 offset: 1 颜色一致
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
}
|
||||
}
|
||||
position: 'top'
|
||||
},
|
||||
barWidth: 14,
|
||||
barWidth: 65,
|
||||
itemStyle: {
|
||||
color: (params) => {
|
||||
// 达标状态:1=达标(绿色),0=未达标(橙色)
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
<img v-else class="arrow" src="../../../assets/img/downArrow.png" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="electricityGauge">
|
||||
<!-- <div class="electricityGauge">
|
||||
<electricityGauge :id="'totalG'" :detailData="ytdData" id="totalGauge"></electricityGauge>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="line" style="padding: 0px;">
|
||||
<verticalBarChart :refName="'totalVerticalBarChart'" :detailData="ytdData">
|
||||
@@ -142,32 +142,26 @@ export default {
|
||||
}
|
||||
|
||||
.number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
// width: 190px;
|
||||
height: 32px;
|
||||
font-family: YouSheBiaoTiHei;
|
||||
font-size: 32px;
|
||||
font-size: 46px;
|
||||
color: #0B58FF;
|
||||
line-height: 32px;
|
||||
letter-spacing: 2px;
|
||||
text-align: left;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.mom {
|
||||
// width: 97px;
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
font-size: 20px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
text-align: left;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
z-index: 1000;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<div :ref="refName" id="coreLineChart" style="width: 100%; height: 210px;"></div>
|
||||
<div style="width: 100%; height: 210px;position: relative;">
|
||||
<div style='font-size: 16px;position: absolute;right: 20px;top:10px'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{detailData.rate}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:flag>0?'#30B590':'#FF9423'}" >{{detailData.diff}}</span></span>
|
||||
</div>
|
||||
<div :ref="refName" id="coreLineChart" style="width: 100%; height: 210px;"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
@@ -8,7 +14,8 @@ export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
myChart: null // 存储图表实例,避免重复创建
|
||||
myChart: null, // 存储图表实例,避免重复创建
|
||||
flag:0
|
||||
};
|
||||
},
|
||||
props: {
|
||||
@@ -43,18 +50,18 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getRateFlag(rate, real, target) {
|
||||
if (isNaN(rate) || rate === null || rate === undefined) return 0;
|
||||
getRateFlag(rate, real, target) {
|
||||
if (isNaN(rate) || rate === null || rate === undefined) return 0;
|
||||
|
||||
// 1. 完成率 >= 100 => 达标
|
||||
if (rate >= 100) return 1;
|
||||
// 1. 完成率 >= 100 => 达标
|
||||
if (rate >= 100) return 1;
|
||||
|
||||
// 2. 完成率 = 0 且 (目标值=0 或 实际值=目标值=0) => 达标
|
||||
if (rate === 0 && target === 0) return 1;
|
||||
// 2. 完成率 = 0 且 (目标值=0 或 实际值=目标值=0) => 达标
|
||||
if (rate === 0 && target === 0) return 1;
|
||||
|
||||
// 其他情况 => 未达标
|
||||
return 0;
|
||||
},
|
||||
// 其他情况 => 未达标
|
||||
return 0;
|
||||
},
|
||||
updateChart() {
|
||||
const chartDom = this.$refs[this.refName];
|
||||
if (!chartDom) {
|
||||
@@ -70,6 +77,7 @@ getRateFlag(rate, real, target) {
|
||||
const diff = this.detailData.diff || 0
|
||||
const rate = this.detailData.rate || 0
|
||||
const flagValue = this.getRateFlag(this.detailData.rate, this.detailData.real, this.detailData.target) || 0
|
||||
this.flag = flagValue
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
@@ -92,10 +100,10 @@ getRateFlag(rate, real, target) {
|
||||
// }
|
||||
},
|
||||
grid: {
|
||||
top: 10,
|
||||
bottom: 30,
|
||||
top: 40,
|
||||
bottom: 15,
|
||||
right: 80,
|
||||
left: 30,
|
||||
left: 10,
|
||||
containLabel: true,
|
||||
show: false // 隐藏grid背景,避免干扰
|
||||
},
|
||||
@@ -153,70 +161,7 @@ getRateFlag(rate, real, target) {
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
offset: [0, 25],
|
||||
// 固定label尺寸:68px×20px
|
||||
width: 68,
|
||||
height: 20,
|
||||
// 关键:去掉换行,让文字在一行显示,适配小尺寸
|
||||
formatter: (params) => {
|
||||
|
||||
// const flags = flags || [];
|
||||
const currentDiff = diff || 0;
|
||||
const currentFlag = flagValue || 0;
|
||||
// const prefix = currentFlag === 1 ? '+' : '-';
|
||||
// 根据标志位选择不同的样式类
|
||||
if (currentFlag === 1) {
|
||||
// 达标 - 使用 rate-achieved 样式
|
||||
return `{achieved|${currentDiff}}{text|差值}`;
|
||||
} else {
|
||||
// 未达标 - 使用 rate-unachieved 样式
|
||||
return `{unachieved|${currentDiff}}{text|差值}`;
|
||||
}
|
||||
},
|
||||
backgroundColor: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(205, 215, 224, 0.6)' },
|
||||
{ offset: 0.2, color: '#ffffff' },
|
||||
{ offset: 1, color: '#ffffff' }
|
||||
]
|
||||
},
|
||||
shadowColor: 'rgba(191,203,215,0.5)',
|
||||
shadowBlur: 2,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 2,
|
||||
borderRadius: 4,
|
||||
borderColor: '#BFCBD577',
|
||||
borderWidth: 0,
|
||||
lineHeight: 20,
|
||||
rich: {
|
||||
text: {
|
||||
width: 'auto',
|
||||
padding: [5, 10, 5, 0],
|
||||
align: 'center',
|
||||
color: '#464646',
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
},
|
||||
achieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#76DABE', // 与达标的 offset: 1 颜色一致
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
},
|
||||
// 未达标样式
|
||||
unachieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#F9A44A', // 与未达标的 offset: 1 颜色一致
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
}
|
||||
}
|
||||
fontSize: 14
|
||||
},
|
||||
itemStyle: {
|
||||
color: flagValue === 1
|
||||
@@ -243,60 +188,7 @@ getRateFlag(rate, real, target) {
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
offset: [0, 25],
|
||||
// 固定label尺寸:68px×20px
|
||||
width: 68,
|
||||
height: 20,
|
||||
// 关键:去掉换行,让文字在一行显示,适配小尺寸
|
||||
formatter: function (params) {
|
||||
return `{value|完成率}{rate|${rate}%}`;
|
||||
},
|
||||
// 核心样式:匹配CSS需求
|
||||
backgroundColor: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(205, 215, 224, 0.6)' }, // 顶部0px位置:阴影最强
|
||||
// { offset: 0.1, color: 'rgba(205, 215, 224, 0.4)' }, // 1px位置:阴影减弱(对应1px)
|
||||
// { offset: 0.15, color: 'rgba(205, 215, 224, 0.6)' }, // 3px位置:阴影几乎消失(对应3px扩散)
|
||||
{ offset: 0.2, color: '#ffffff' }, // 主体白色
|
||||
{ offset: 1, color: '#ffffff' }
|
||||
]
|
||||
},
|
||||
// 外阴影:0px 2px 2px 0px rgba(191,203,215,0.5)
|
||||
shadowColor: 'rgba(191,203,215,0.5)',
|
||||
shadowBlur: 2,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 2,
|
||||
// 圆角:4px
|
||||
borderRadius: 4,
|
||||
// 移除边框
|
||||
borderColor: '#BFCBD577',
|
||||
borderWidth: 0,
|
||||
// 文字垂直居中(针对富文本)
|
||||
lineHeight: 20,
|
||||
rich: {
|
||||
value: {
|
||||
// 缩小宽度和内边距,适配68px容器
|
||||
width: 'auto', // 自动宽度,替代固定40px
|
||||
padding: [5, 0, 5, 10], // 缩小内边距
|
||||
align: 'center',
|
||||
color: '#464646', // 文字灰色
|
||||
fontSize: 11, // 缩小字体,适配小尺寸
|
||||
lineHeight: 20 // 垂直居中
|
||||
},
|
||||
rate: {
|
||||
width: 'auto',
|
||||
padding: [5, 10, 5, 0],
|
||||
align: 'center',
|
||||
color: '#0B58FF', // 数字蓝色
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
}
|
||||
}
|
||||
fontSize: 14
|
||||
},
|
||||
itemStyle: {
|
||||
// 预算的渐变颜色(蓝系渐变)
|
||||
|
||||
@@ -2,33 +2,21 @@
|
||||
<div style="flex: 1">
|
||||
<Container :name="title" icon="cockpitItemIcon" size="operatingRevenueBg" topSize="middle">
|
||||
<div class="kpi-content" style="padding: 14px 16px; display: flex; width: 100%;">
|
||||
<div class="topItem-container" style="display: flex; gap: 8px; width: 100%;">
|
||||
<!-- 管理费用模块 -->
|
||||
<div class="dashboard">
|
||||
<div class="topItem-container" style="display: flex; gap: 8px">
|
||||
<div
|
||||
v-for="item in sortedIndicators"
|
||||
:key="item.key"
|
||||
class="dashboard"
|
||||
@click="item.route && handleDashboardClick(item.route)"
|
||||
>
|
||||
<div class="title">
|
||||
管理费用·万元
|
||||
{{ item.name }}·{{ item.unit }}
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<operatingSingleBar :detailData="manageCostData"></operatingSingleBar>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 销售费用模块 -->
|
||||
<div class="dashboard">
|
||||
<div class="title">
|
||||
销售费用·万元
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<operatingSingleBar :detailData="saleCostData"></operatingSingleBar>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 财务费用模块 -->
|
||||
<div class="dashboard">
|
||||
<div class="title">
|
||||
财务费用·万元
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<operatingSingleBar :detailData="financeCostData"></operatingSingleBar>
|
||||
<div style='font-size: 16px;text-align: right;padding-right: 5px;'>
|
||||
<span>完成率:<span style='color: #0B58FF;'>{{item.detailData.rate}}%</span></span>
|
||||
<span style='display: inline-block;margin-left: 10px;'>差值:<span :style="{color:item.detailData.flag>0?'#30B590':'#FF9423'}" >{{item.detailData.diff}}</span></span>
|
||||
</div>
|
||||
<operatingSingleBar :detailData="item.detailData"></operatingSingleBar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,63 +58,64 @@ export default {
|
||||
financeCostData: { title: "财务费用", budget: 0, real: 0, rate: 0, diff: 0, flag: 0 }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
indicatorDefs() {
|
||||
return [
|
||||
{ key: 'mgmtFee', name: '管理费用', unit: '万元'},
|
||||
{ key: 'salesFee', name: '销售费用', unit: '万元'},
|
||||
{ key: 'finFee', name: '财务费用', unit: '万元'}
|
||||
]
|
||||
},
|
||||
indicators() {
|
||||
let _this = this
|
||||
const fallback = { budget: 0, real: 0, rate: 0, diff: 0, flag: 0 }
|
||||
const list = (Array.isArray(_this.ytdAnalysis) ? _this.ytdAnalysis : [])
|
||||
return _this.indicatorDefs.map(def => {
|
||||
const data = list.find(item => item && item.title === def.name) || fallback
|
||||
const detailData = {
|
||||
...data,
|
||||
flag: _this.getRateFlag(data.rate, data.real, data.budget),
|
||||
}
|
||||
return {
|
||||
...def,
|
||||
detailData,
|
||||
sortValue: Number((data && data.real) ?? 0)
|
||||
}
|
||||
})
|
||||
},
|
||||
sortedIndicators() {
|
||||
const unitOrder = ['万元']
|
||||
const unitRank = (u) => {
|
||||
const idx = unitOrder.indexOf(u)
|
||||
return idx === -1 ? 999 : idx
|
||||
}
|
||||
|
||||
return this.indicators.slice().sort((a, b) => {
|
||||
const ur = unitRank(a.unit) - unitRank(b.unit)
|
||||
if (ur !== 0) return ur
|
||||
const vr = (b.sortValue ?? -Infinity) - (a.sortValue ?? -Infinity)
|
||||
if (vr !== 0) return vr
|
||||
return String(a.key).localeCompare(String(b.key))
|
||||
})
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
ytdAnalysis: {
|
||||
handler(newVal) {
|
||||
this.updateCostData(newVal)
|
||||
},
|
||||
handler(newVal) {},
|
||||
deep: true,
|
||||
immediate: true // 初始化立即执行
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateCostData(this.ytdAnalysis)
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
// 达标标识判断(≥100返回1,<100返回0)
|
||||
getRateFlag(rate, real, target) {
|
||||
if (isNaN(rate) || rate === null || rate === undefined) return 0;
|
||||
|
||||
// 1. 完成率 >= 100 => 达标
|
||||
if (rate >= 100) return 1;
|
||||
|
||||
// 2. 完成率 = 0 且 (目标值=0 或 实际值=目标值=0) => 达标
|
||||
if (rate === 0 && target === 0) return 1;
|
||||
|
||||
// 其他情况 => 未达标
|
||||
return 0;
|
||||
},
|
||||
|
||||
// 处理费用数据
|
||||
updateCostData(data) {
|
||||
// 数据兜底:确保是数组且长度≥3
|
||||
const validData = Array.isArray(data) && data.length >= 3
|
||||
? data
|
||||
: this.$props.ytdAnalysis;
|
||||
|
||||
// 提取三个费用项数据(兜底处理)
|
||||
const manageItem = validData[0] || { title: "管理费用", budget: 0, real: 0, rate: 0, diff: 0 };
|
||||
const saleItem = validData[1] || { title: "销售费用", budget: 0, real: 0, rate: 0, diff: 0 };
|
||||
const financeItem = validData[2] || { title: "财务费用", budget: 0, real: 0, rate: 0, diff: 0 };
|
||||
|
||||
// 整合flag字段
|
||||
this.manageCostData = {
|
||||
...manageItem,
|
||||
flag: this.getRateFlag(manageItem.rate, manageItem.real, manageItem.budget)
|
||||
};
|
||||
this.saleCostData = {
|
||||
...saleItem,
|
||||
flag: this.getRateFlag(saleItem.rate, saleItem.real, saleItem.budget)
|
||||
};
|
||||
this.financeCostData = {
|
||||
...financeItem,
|
||||
flag: this.getRateFlag(financeItem.rate, financeItem.real, financeItem.budget)
|
||||
};
|
||||
|
||||
// 调试日志
|
||||
console.log('管理费用数据:', this.manageCostData);
|
||||
console.log('销售费用数据:', this.saleCostData);
|
||||
console.log('财务费用数据:', this.financeCostData);
|
||||
getRateFlag(rate, real, target) {
|
||||
if (isNaN(rate) || rate === null || rate === undefined) return 0;
|
||||
// 1. 完成率 >= 100 => 达标
|
||||
if (rate >= 100) return 1;
|
||||
// 2. 完成率 = 0 且 (目标值=0 或 实际值=目标值=0) => 达标
|
||||
if (rate === 0 && target === 0) return 1;
|
||||
// 其他情况 => 未达标
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,7 +159,6 @@ getRateFlag(rate, real, target) {
|
||||
line-height: 18px;
|
||||
letter-spacing: 2px;
|
||||
text-align: left;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
// 图表容器:适配高度
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
<img v-else class="arrow" src="../../../assets/img/downArrow.png" alt="下降">
|
||||
</div>
|
||||
</div>
|
||||
<div class="electricityGauge">
|
||||
<!-- <div class="electricityGauge"> -->
|
||||
<!-- 传递包含flag的factoryData给仪表盘组件 -->
|
||||
<electricityGauge id="month" :detailData="factoryData"></electricityGauge>
|
||||
</div>
|
||||
<!-- <electricityGauge id="month" :detailData="factoryData"></electricityGauge> -->
|
||||
<!-- </div> -->
|
||||
</div>
|
||||
<div class="line" style="padding: 0px;">
|
||||
<!-- 传递包含flag的factoryData给柱状图组件 -->
|
||||
@@ -106,18 +106,18 @@ export default {
|
||||
* @param {number} rate 完成率(原始值,如89代表89%)
|
||||
* @returns {0|1} flag值
|
||||
*/
|
||||
getRateFlag(rate, real, target) {
|
||||
if (isNaN(rate) || rate === null || rate === undefined) return 0;
|
||||
getRateFlag(rate, real, target) {
|
||||
if (isNaN(rate) || rate === null || rate === undefined) return 0;
|
||||
|
||||
// 1. 完成率 >= 100 => 达标
|
||||
if (rate >= 100) return 1;
|
||||
// 1. 完成率 >= 100 => 达标
|
||||
if (rate >= 100) return 1;
|
||||
|
||||
// 2. 完成率 = 0 且 (目标值=0 或 实际值=目标值=0) => 达标
|
||||
if (rate === 0 && target === 0) return 1;
|
||||
// 2. 完成率 = 0 且 (目标值=0 或 实际值=目标值=0) => 达标
|
||||
if (rate === 0 && target === 0) return 1;
|
||||
|
||||
// 其他情况 => 未达标
|
||||
return 0;
|
||||
},
|
||||
// 其他情况 => 未达标
|
||||
return 0;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -155,31 +155,27 @@ getRateFlag(rate, real, target) {
|
||||
}
|
||||
|
||||
.number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 32px;
|
||||
font-family: YouSheBiaoTiHei;
|
||||
font-size: 32px;
|
||||
font-size: 46px;
|
||||
color: #0B58FF;
|
||||
line-height: 32px;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
white-space: nowrap;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.mom {
|
||||
width: fit-content; // 自适应宽度,避免文字溢出
|
||||
height: 18px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
font-size: 20px;
|
||||
color: #000000;
|
||||
line-height: 18px;
|
||||
letter-spacing: 1px;
|
||||
display: flex;
|
||||
align-items: center; // 箭头和文字垂直居中
|
||||
gap: 4px; // 文字和箭头间距
|
||||
z-index: 1000;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
// 箭头样式优化
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="lineBottom" style="height: 100%; width: 100%">
|
||||
<operatingLineBar :dateData="dateData" :chartData="chartD" style="height: 99%; width: 100%" />
|
||||
<operatingLineBar :dateData="dateData" :chartData="chartD" style="height: 100%; width: 100%" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -152,7 +152,7 @@ export default {
|
||||
name: '预算',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 14,
|
||||
barWidth: 40,
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
@@ -172,13 +172,13 @@ export default {
|
||||
name: '实际',
|
||||
type: 'bar',
|
||||
yAxisIndex: 0,
|
||||
barWidth: 14,
|
||||
barWidth: 40,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
offset: [30, 0],
|
||||
width: 68,
|
||||
height: 20,
|
||||
offset: [32, 0],
|
||||
width: 100,
|
||||
height: 22,
|
||||
formatter: (params) => {
|
||||
const diff = data.diff || [];
|
||||
const flags = data.flags || [];
|
||||
@@ -212,23 +212,21 @@ export default {
|
||||
borderRadius: 4,
|
||||
borderColor: '#BFCBD577',
|
||||
borderWidth: 0,
|
||||
lineHeight: 20,
|
||||
lineHeight: 26,
|
||||
rich: {
|
||||
text: {
|
||||
width: 'auto',
|
||||
padding: [5, 10, 5, 0],
|
||||
align: 'center',
|
||||
color: '#464646',
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
fontSize: 14
|
||||
},
|
||||
achieved: {
|
||||
width: 'auto',
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#76DABE', // 与达标的 offset: 1 颜色一致
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
fontSize: 14
|
||||
},
|
||||
// 未达标样式
|
||||
unachieved: {
|
||||
@@ -236,8 +234,7 @@ export default {
|
||||
padding: [5, 0, 5, 10],
|
||||
align: 'center',
|
||||
color: '#F9A44A', // 与未达标的 offset: 1 颜色一致
|
||||
fontSize: 11,
|
||||
lineHeight: 20
|
||||
fontSize: 14
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user