制造成本分析修改

This commit is contained in:
2026-03-25 14:10:27 +08:00
parent bb66f97b95
commit 4f7466bb29
59 changed files with 3909 additions and 1779 deletions

View File

@@ -12,9 +12,9 @@ VUE_APP_TITLE = 洛玻集团驾驶舱
# 杨姗姗
# 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://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'
# 路由懒加载

View File

@@ -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 的路径
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,100 @@
<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="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="daySpan">
<el-date-picker
v-model="value3"
type="year"
placeholder="选择年">
</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>

View File

@@ -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>

View File

@@ -7,10 +7,21 @@
@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',
@@ -27,6 +38,7 @@ const tableProps = [
];
export default {
name: 'CustomerInfoConfiguration',
components:{CustomerInfoAdd},
data () {
return {
formConfig: [
@@ -64,7 +76,7 @@ export default {
pageNo:1
},
total:2,
tableData:[{name:'111'}],
tableData:[{name:'111',id:1}],
tableProps,
tableBtn:[
{
@@ -75,19 +87,65 @@ export default {
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() {}
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>

View File

@@ -7,15 +7,30 @@
@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: '所属年份'
@@ -27,6 +42,7 @@ const tableProps = [
];
export default {
name: 'GroupKeyTaskConfiguration',
components:{GroupKeyAdd},
data () {
return {
formConfig: [
@@ -79,19 +95,66 @@ export default {
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() {}
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>

View File

@@ -7,10 +7,21 @@
@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',
@@ -27,6 +38,7 @@ const tableProps = [
];
export default {
name: 'ProductInfoConfiguration',
components:{ProductInfoAdd},
data () {
return {
formConfig: [
@@ -69,19 +81,66 @@ export default {
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() {}
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>

View File

@@ -16,7 +16,7 @@
gap: 12px;
grid-template-columns:1624px;
">
<operatingLineChart :dateData="dateData" :monthData="monthData" />
<operatingLineChart :dateData="dateData" :monData="monData" />
</div>
</div>
<div class="top" style="display: flex; gap: 16px;margin-top: 6px;">
@@ -25,27 +25,40 @@
gap: 12px;
grid-template-columns: 1624px;
">
<operatingLineChartCumulative :dateData="dateData" :ytdData="ytdData" />
<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 { getSalesRevenueGroupData } from '@/api/cockpit'
import { getProfitAnalysisTotalList } from '@/api/cockpit'
import moment from "moment";
export default {
name: "ElectricityCostAnalysis",
name: "DayReport",
components: {
ReportHeader,
operatingLineChartCumulative,
operatingLineChart,
// premProdStatus,
Sidebar,
},
data() {
@@ -55,10 +68,9 @@ export default {
timer: null,
beilv: 1,
value: 100,
sort:1,
selectDate:{},
monthData: {},
ytdData:{},
dateData: {},
monData: [],
totalData: [],
};
},
@@ -127,23 +139,20 @@ export default {
this.dateData = this.$route.query.dateData ? this.$route.query.dateData : undefined
},
methods: {
// sortChange(value) {
// this.sort = value
// this.getData()
// },
getData() {
getSalesRevenueGroupData({
getProfitAnalysisTotalList({
startTime: this.dateData.startTime,
endTime: this.dateData.endTime,
sort: this.sort,
index: undefined,
factory: undefined
// timeDim: obj.mode
analysisObject: [
"利润总额"
],
levelId: 1,
// timeDim: this.dateData.mode
}).then((res) => {
console.log(res);
this.monthData= res.data.month
this.ytdData = res.data.ytd
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
@@ -152,7 +161,7 @@ export default {
})
},
handleTimeChange(obj) {
console.log(obj, 'obj');
// console.log(obj, 'obj');
this.dateData= obj
this.getData()
},
@@ -226,12 +235,14 @@ export default {
<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);
}

View File

@@ -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>

View File

@@ -43,8 +43,7 @@ export default {
.content-top {
height: 60px;
.title-wrapper {
.title-wrapper {
display: flex;
align-items: center;
margin-left: 10px;
@@ -73,7 +72,6 @@ export default {
// 移除固定行高,避免影响垂直对齐
// line-height: 60px;
}
// width: 547px;
// background: url(../../../assets/img/contentTopBasic.png) no-repeat;
// background-size: 100% 100%;
@@ -108,7 +106,6 @@ export default {
background-size: 100% 100%;
background-position: 0 0;
}
&__rawTopTitleLarge {
background: url(../../../assets/img/rawTopTitleLarge.png) no-repeat;
background-size: 100% 100%;
@@ -176,19 +173,11 @@ export default {
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%;

View File

@@ -8,7 +8,8 @@
width: 1595px;
background-color: rgba(249, 252, 255, 1);
">
<dataTrendBar @changeItem="handleChange" :chartData="chartData" />
<!-- 直接使用计算属性 chartData无需手动更新 -->
<dataTrendBar @handleGetItemData="getData" :chartData="chartData" />
</div>
</div>
</Container>
@@ -23,128 +24,81 @@ export default {
name: "ProductionStatus",
components: { Container, dataTrendBar },
props: {
trend: {
trendData: {
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 }
]
default: () => [],
},
},
data() {
return {
chartData: {
months: [], // 月份数组2025年01月 - 2025年12月
rates: [], // 每月完成率(百分比)
reals: [], // 每月实际值
budgets: [],// 每月预算值
diffs: [], // 每月差值
flags: [] // 每月达标标识≥100 → 1<100 → 0
}
// 移除:原 chartData 定义,改为计算属性
};
},
watch: {
trend: {
handler(newVal) {
this.processTrendData(newVal);
},
immediate: true,
deep: true,
},
},
mounted() {
this.processTrendData(this.trend);
},
methods: {
handleChange(value) {
this.$emit("handleChange", value);
},
// 移除:原 watch 监听配置,计算属性自动响应 trendData 变化
computed: {
/**
* 处理趋势数据适配12个月的数组结构
* @param {Array} trendData - 原始趋势数组12个月
* chartData 计算属性:自动响应 trendData 变化,格式化并提取各字段数组
* @returns {Object} 包含6个独立数组的格式化数据
*/
processTrendData(trendData) {
// 数据兜底确保是数组且长度为12
const validTrend = Array.isArray(trendData)
? trendData
: []
chartData() {
// 初始化6个独立数组
const timeArr = []; // 格式化后的年月数组
const valueArr = []; // 实际值数组
const diffValueArr = []; // 差异值数组
const targetValueArr = []; // 预算值数组
const proportionArr = []; // 占比数组
const completedArr = []; // 完成率数组
// 初始化空数组
const months = [];
const rates = [];
const reals = [];
const budgets = [];
const diffs = [];
const flags = [];
// 遍历传入的 trendData 数组(响应式依赖,变化时自动重算)
this.trendData.forEach((item) => {
// 1. 格式化时间并推入时间数组
const yearMonth = this.formatTimeToYearMonth(item.time);
timeArr.push(yearMonth);
// 遍历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);
// 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);
});
// 更新chartData响应式
this.chartData = {
months,
rates,
reals,
budgets,
diffs,
flags
// 组装并返回格式化后的数据(结构与原一致
return {
time: timeArr,
value: valueArr,
diffValue: diffValueArr,
targetValue: targetValueArr,
proportion: proportionArr,
completed: completedArr,
rawData: this.trendData, // 透传原始数据,方便子组件使用
};
console.log('处理后的趋势数据:', this.chartData);
},
},
methods: {
/**
* 计算达标标识
* @param {Number} rate - 完成率原始值如1.2 → 120%
* @returns {Number} 1: 达标≥100%0: 未达标(<100%
* 格式化时间戳为年月格式YYYY-MM
* @param {Number} timestamp 13位毫秒级时间戳
* @returns {String} 格式化后的年月字符串2025-10
*/
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;
},
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;
@@ -159,7 +113,6 @@ getRateFlag(rate, real, target) {
-ms-overflow-style: none;
}
/* 设备项样式优化 */
.proBarInfo {
display: flex;
flex-direction: column;
@@ -266,7 +219,6 @@ getRateFlag(rate, real, target) {
transition: width 0.3s ease;
}
/* 图表相关样式 */
.chartImgBottom {
position: absolute;
bottom: 45px;
@@ -285,7 +237,7 @@ getRateFlag(rate, real, target) {
</style>
<style>
/* 全局 tooltip 样式 */
/* 全局 tooltip 样式(不使用 scoped确保生效 */
.production-status-chart-tooltip {
background: #0a2b4f77 !important;
border: none !important;

View File

@@ -12,7 +12,7 @@
</span>
<span class="legend-item">
<span class="legend-icon square target"></span>
预算
目标
</span>
<span class="legend-item">
<span class="legend-icon square achieved"></span>
@@ -59,13 +59,17 @@ export default {
props: ["chartData"],
data() {
return {
activeButton: 0,
isDropdownShow: false,
selectedProfit: '营业收入', // 选中的名称初始为null
selectedProfit: '利润总额', // 选中的名称初始为null
profitOptions: [
'营业收入',
'单价',
'利润总额',
'销量',
'单价',
'制造成本',
'管理费用',
'销售费用',
'财务费用',
'非经营性利润',
]
};
},
@@ -74,19 +78,17 @@ export default {
// 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
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,
@@ -113,7 +115,7 @@ export default {
{ offset: 1, color: 'rgba(40, 138, 255, 0)' }
])
},
data: data.rates, // 完成率(%
data: data.proportion || [], // 完成率(%
symbol: 'circle',
symbolSize: 6
},
@@ -135,7 +137,7 @@ export default {
borderRadius: [4, 4, 0, 0],
borderWidth: 0
},
data: data.budgets // 目标销量(万元)
data: data.targetValue || [] // 目标销量(万元)
},
// 3. 实际(柱状图,含达标状态)
{
@@ -152,8 +154,8 @@ export default {
height: 20,
// 关键:去掉换行,让文字在一行显示,适配小尺寸
formatter: (params) => {
const diff = data.diffs || [];
const flags = data.flags || [];
const diff = data.diffValue || [];
const flags = data.completed || [];
const currentDiff = diff[params.dataIndex] || 0;
const currentFlag = flags[params.dataIndex] || 0;
@@ -216,7 +218,7 @@ export default {
itemStyle: {
color: (params) => {
// 达标状态1=达标绿色0=未达标(橙色)
const safeFlag = data.flags;
const safeFlag = data.completed || [];
const currentFlag = safeFlag[params.dataIndex] || 0;
return currentFlag === 1
? {
@@ -239,13 +241,10 @@ export default {
borderRadius: [4, 4, 0, 0],
borderWidth: 0
},
data: data.reals // 实际销量(万元)
data: data.value || [] // 实际销量(万元)
}
]
};
// 根据按钮状态返回对应数据
return salesData;
}
},
@@ -253,7 +252,7 @@ export default {
selectProfit(item) {
this.selectedProfit = item;
this.isDropdownShow = false;
this.$emit("changeItem", item);
this.$emit('handleGetItemData', item)
}
},
};
@@ -378,7 +377,7 @@ export default {
.dropdown-container {
position: relative;
z-index: 10;
z-index: 999; // 提高z-index确保菜单不被遮挡
}
.item-button {
@@ -442,18 +441,21 @@ export default {
transition: transform 0.2s ease;
&.rotate {
transform: rotate(90deg);
transform: rotate(90deg); // 箭头旋转方向可根据需求调整比如改为rotate(-90deg)更符合向上展开的视觉
}
}
.dropdown-options {
position: absolute;
top: 100%;
// 关键修改1调整top值让菜单显示在选择框上方calc(-100% - 2px)表示向上偏移自身100%再加2px间距
bottom: 100%;
right: 0;
margin-top: 2px;
// 移除多余的margin-top避免额外间距
// margin-top: 2px;
width: 123px;
background: #ffffff;
border-radius: 8px;
// 关键修改2调整border-radius让菜单顶部圆角匹配选择框的右上角底部圆角为0更美观
border-radius: 8px 8px 0 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;

View File

@@ -4,7 +4,7 @@
<div :id="id" style="width: 100%; height:100%;"></div>
<div class="bottomTip">
<div class="precent">
<span class="precentNum">{{ detailData.rate || 0 }}% </span>
<span class="precentNum">{{ detailData.completeRate ? detailData.completeRate : 0 }}% </span>
</div>
</div>
</div>
@@ -89,7 +89,7 @@ export default {
if (!this.electricityChart) return
// 修复兜底获取rate值确保数值有效
const rate = Number(this.detailData?.rate) || 0
const rate = Number(this.detailData?.completeRate) || 0
console.log('当前rate值', rate); // 调试确认rate值正确
// 关键第二个参数传true清空原有配置强制更新
@@ -221,6 +221,4 @@ export default {
}
}
}
</style>

View File

@@ -1,9 +1,7 @@
<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">
@@ -11,113 +9,104 @@
</div>
<div class="number">
<div class="yield">
{{ monthData?.rate || 0 }}%
{{ formatRate(factoryData?.completeRate) }}%
</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="">
环比{{ 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 :detailData="monthData" id="month"></electricityGauge>
<electricityGauge id="month" :detailData="factoryData"></electricityGauge>
</div> -->
</div>
<div class="line" style="padding: 0px;">
<verticalBarChart :detailData="monthData">
</verticalBarChart>
<!-- 传递包含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 },
// mixins: [resize],
props: {
monthData: { // 接收父组件传递的设备数据数组
monData: {
type: Object,
default: () => {} // 默认空数组,避免报错
default: () => ({})
},
title: { // 接收父组件传递的设备数据数组
title: {
type: String,
default: () => '' // 默认空数组,避免报错
default: ''
},
month: { // 接收父组件传递的设备数据数组
month: {
type: String,
default: () => '' // 默认空数组,避免报错
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())
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>
/* 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;
}
@@ -126,18 +115,13 @@ export default {
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;
}
@@ -164,6 +148,13 @@ export default {
font-style: normal;
margin-top: 20px;
}
// 箭头样式优化
.arrow {
width: 16px;
height: 16px;
object-fit: contain;
}
}
.line {
@@ -171,34 +162,4 @@ export default {
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> -->

View File

@@ -1,36 +1,24 @@
<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%;">
<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;'>
<!-- 新增topItem 专属包裹容器统一控制样式和布局 -->
<div class="topItem-container" style="display: flex; gap: 8px;">
<div class="dashboard left">
<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 class="line">
<operatingSingleBar></operatingSingleBar>
</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="dashboard right">
<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 class="line">
<operatingSingleBar></operatingSingleBar>
</div>
</div>
</div>
@@ -38,167 +26,102 @@
</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 },
components: { Container, operatingSingleBar, verticalBarChart },
// mixins: [resize],
props: {
monthAnalysis: {
itemData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [
{ title: "销量", budget: 0, real: 0, rate: 0, diff: 0 },
{ title: "单价", budget: 0, real: 0, rate: 0, diff: 0 }
]
default: () => [] // 默认空数组,避免报错
},
dateData: {
type: Object,
default: () => {}
},
title: {
title: { // 接收父组件传递的设备数据数组
type: String,
default: ''
default: () => '' // 默认空数组,避免报错
},
factory: {
type: [String,Number],
default: ''
},
month: {
month: { // 接收父组件传递的设备数据数组
type: String,
default: ''
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)
itemData: {
handler(newValue, oldValue) {
// this.updateChart()
},
deep: true,
immediate: true
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() {
this.updateChart(this.monthAnalysis)
// 初始化图表(若需展示图表,需在模板中添加对应 DOM
// this.$nextTick(() => this.updateChart())
},
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>
/* 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;
}
.topItem-container {
display: flex;
justify-content: space-between;
}
.dashboard {
flex: 1;
min-width: 300px;
width: 382px;
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;
// width: 190px;
height: 18px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
@@ -207,15 +130,74 @@ getRateFlag(rate, real, target) {
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;
}
}
.dashboard.left {
margin-left: 0;
// .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);
}
.dashboard.right {
margin-right: 0;
.production-status-chart-tooltip * {
color: #fff !important;
}
</style>
</style> -->

View File

@@ -51,7 +51,7 @@ import * as echarts from 'echarts';
export default {
name: "Container",
components: { operatingLineBar },
props: ["chartData", 'dateData'],
props: ["chartData",'dateData'],
emits: ['sort-change'], // 声明事件Vue3 推荐)
data() {
return {
@@ -70,10 +70,10 @@ export default {
computed: {
// 排序后的数据源核心根据selectedSortValue重新排序
currentDataSource() {
if (!this.chartData?.factory) return {};
if (!this.chartData) return {};
// 深拷贝原始数据,避免修改原数据
const factory = JSON.parse(JSON.stringify(this.chartData.factory));
const factory = JSON.parse(JSON.stringify(this.chartData));
if (!factory.locations.length || !this.selectedSortValue) return factory;
// 构建带索引的数组,方便同步所有字段排序
@@ -82,7 +82,7 @@ export default {
name,
real: factory.reals[index],
target: factory.targets[index],
rate: factory.rates[index],
rate: factory.rate[index],
diff: factory.diff[index],
flag: factory.flags[index]
}));
@@ -143,7 +143,7 @@ export default {
{ offset: 1, color: 'rgba(40, 138, 255, 0)' }
])
},
data: data.rates || [],
data: data.rate || [],
symbol: 'circle',
symbolSize: 6
},
@@ -219,14 +219,14 @@ export default {
padding: [5, 10, 5, 0],
align: 'center',
color: '#464646',
fontSize: 14,
fontSize: 14
},
achieved: {
width: 'auto',
padding: [5, 0, 5, 10],
align: 'center',
color: '#76DABE', // 与达标的 offset: 1 颜色一致
fontSize: 14,
fontSize: 14
},
// 未达标样式
unachieved: {
@@ -234,7 +234,7 @@ export default {
padding: [5, 0, 5, 10],
align: 'center',
color: '#F9A44A', // 与未达标的 offset: 1 颜色一致
fontSize: 14,
fontSize: 14
}
}
},
@@ -413,7 +413,7 @@ export default {
height: 24px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
fontSize: 12px;
font-size: 12px;
line-height: 24px;
font-style: normal;
letter-spacing: 2px;
@@ -485,7 +485,7 @@ export default {
.dropdown-option {
padding: 6px 12px;
fontSize: 12px;
font-size: 12px;
color: #333;
cursor: pointer;
text-align: left;

View File

@@ -62,11 +62,7 @@ export default {
console.error('图表容器未找到!');
return;
}
// 只创建一次图表实例
this.myChart = echarts.init(chartDom);
// 绑定点击事件(只绑定一次,永久生效)
this.myChart.getZr().on('click', (params) => {
console.log('params', params);
@@ -94,9 +90,10 @@ export default {
if (itemName === undefined) {
return;
}
// 路由跳转时携带序号(或名称+序号)
this.$router.push({
path: 'operatingRevenueBase',
path: 'totalProfitBase',
query: { // 使用query传递参数推荐也可使用params
// baseName: itemName,
factory: baseIndex,
@@ -175,6 +172,7 @@ export default {
fontSize: 12,
align: 'right'
},
splitNumber: 4,
axisTick: { show: false },
axisLabel: {
@@ -183,7 +181,7 @@ export default {
formatter: '{value}'
},
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
axisLine: { show: true, show: true, lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } }
axisLine: { show: true, lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } }
},
{
type: 'value',
@@ -199,7 +197,7 @@ export default {
formatter: '{value}%'
},
splitLine: { show: false },
axisLine: { show: true, show: true, lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
axisLine: { show: true, lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
splitNumber: 4
}
],

View File

@@ -34,7 +34,7 @@ export default {
// 深度监听数据变化,仅更新图表配置(不销毁实例)
chartData: {
handler() {
console.log(this.chartData,'chartData');
console.log(this.chartData, 'chartData');
this.updateChart();
},
deep: true,
@@ -85,7 +85,7 @@ export default {
},
grid: {
top: 30,
bottom:20,
bottom: 20,
right: 10,
left: 25,
containLabel: true
@@ -118,6 +118,7 @@ export default {
fontSize: 12,
align: 'right'
},
splitNumber: 4,
axisTick: { show: false },
axisLabel: {
@@ -126,7 +127,7 @@ export default {
formatter: '{value}'
},
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
axisLine: { show: true, show: true, lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
axisLine: { show: true, lineStyle: { color: 'rgba(0, 0, 0, 0.15)' } },
},
// 右侧Y轴利润占比百分比
{

View File

@@ -1,5 +1,5 @@
<template>
<div :ref="refName" id="coreLineChart" style="width: 100%; height: 200px;"></div>
<div ref="cockpitEffChip" id="coreLineChart" style="width: 100%; height: 200px;"></div>
</template>
<script>
import * as echarts from 'echarts';
@@ -13,14 +13,6 @@ export default {
},
props: {
// 明确接收的props结构增强可读性
refName: {
type: String,
default: () => 'cockpitEffChip',
// 校验数据格式
// validator: (value) => {
// return Array.isArray(value.series) && Array.isArray(value.allPlaceNames);
// }
},
chartData: {
type: Object,
default: () => ({
@@ -42,7 +34,7 @@ export default {
// 深度监听数据变化,仅更新图表配置(不销毁实例)
chartData: {
handler() {
console.log(this.chartData, 'chartData');
console.log(this.chartData,'chartData');
this.updateChart();
},
deep: true,
@@ -51,7 +43,7 @@ export default {
},
methods: {
updateChart() {
const chartDom = this.$refs[this.refName];
const chartDom = this.$refs.cockpitEffChip;
if (!chartDom) {
console.error('图表容器未找到!');
return;
@@ -92,16 +84,18 @@ export default {
// }
},
grid: {
top: 20,
bottom: 30,
right: 20,
left: 5,
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,

View File

@@ -1,7 +1,6 @@
<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;
@@ -21,10 +20,10 @@
集团情况
</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>
<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" />
<operatingTopBar :chartData="chartData?.topBarData || {}" />
</div>
<div class="right" style="
height: 380px;
@@ -32,8 +31,7 @@
width: 1220px;
background-color: rgba(249, 252, 255, 1);
">
<!-- <top-item /> -->
<operatingBar :dateData="dateData" :chartData="chartData" @sort-change="sortChange" />
<operatingBar :dateData="dateData" :chartData="chartData?.barData || {}" />
</div>
</div>
</Container>
@@ -45,38 +43,56 @@ 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: {
monthData: {
type: Object,
default: () => ({
group: {
rate: 0,
diff: 0,
real: 0,
target: 0
},
factory: []
}),
required: true
monData: {
type: Array,
default: () => [],
},
dateData: {
type: Object,
default: () => ({
}),
},
default: () => {},
}
},
data() {
return {
chartData: null, // 初始化 chartData 为 null
groupData: {}, // 集团数据
factoryData: [] // 工厂数据
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: {
monthData: {
monData: {
handler() {
this.processChartData();
},
@@ -85,135 +101,93 @@ export default {
},
},
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
* 核心方法按levelId匹配地名生成locations
*/
processChartData() {
// 1. 处理集团数据 - 提取各字段到对应数组
this.groupData = this.monthData.group || { rate: 0, diff: 0, real: 0, target: 0 };
// 初始化空数据结构
const initTopBarData = {
locations: [], diff: [], targets: [], reals: [], rate: [], flags: []
};
const initBarData = { locations: [], diff: [], targets: [], reals: [], rate: [], flags: [] };
// 集团各维度数据数组(单条数据,对应凯盛新能)
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])];
if (!Array.isArray(this.monData) || this.monData.length === 0) {
this.chartData = { topBarData: initTopBarData, barData: initBarData };
return;
}
console.log('集团数据数组:', {
groupTarget,
groupDiff,
groupReal,
groupRate,
groupFlag,
rawGroupData: this.groupData
// 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. 处理工厂数据 - 提取每个工厂的对应字段到数组
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));
// 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);
});
// 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
// 遍历有效数据填充locationslevelId→地名
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); // 序号转数字
}
};
});
console.log('最终处理后的图表数据:', this.chartData);
},
// 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>
/* 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 {
@@ -226,14 +200,12 @@ getRateFlag(rate, real, target) {
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;
@@ -259,7 +231,6 @@ getRateFlag(rate, real, target) {
height: 14px;
border: 1px solid #adadad;
margin: 0 8px;
/* 优化分割线间距 */
}
.yield {
@@ -276,22 +247,18 @@ getRateFlag(rate, real, target) {
.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;
}
@@ -314,10 +281,8 @@ getRateFlag(rate, real, target) {
#37acf5 100%);
border-radius: 5px;
transition: width 0.3s ease;
/* 进度变化时添加过渡动画,更流畅 */
}
/* 图表相关样式保留 */
.chartImgBottom {
position: absolute;
bottom: 45px;
@@ -336,7 +301,6 @@ getRateFlag(rate, real, target) {
</style>
<style>
/* 全局 tooltip 样式(不使用 scoped确保生效 */
.production-status-chart-tooltip {
background: #0a2b4f77 !important;
border: none !important;

View File

@@ -1,7 +1,6 @@
<template>
<div style="flex: 1">
<Container name="累计数据对比" icon="cockpitItemIcon" size="opLargeBg" topSize="large">
<!-- 1. 移除 .kpi-content 的固定高度改为自适应 -->
<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;
@@ -21,10 +20,10 @@
集团情况
</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>
<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" />
<operatingTopBar :chartData="chartData?.topBarData || {}" />
</div>
<div class="right" style="
height: 380px;
@@ -32,46 +31,68 @@
width: 1220px;
background-color: rgba(249, 252, 255, 1);
">
<!-- <top-item /> -->
<operatingBar :dateData="dateData" @sort-change="sortChange" :chartData="chartData" />
<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: {
salesTrendMap: {
type: Object,
default: () => ({}),
},
ytdData: {
type: Object,
default: () => ({}),
totalData: {
type: Array,
default: () => [],
},
dateData: {
type: Object,
default: () => ({
}),
default: () => {},
},
},
data() {
return {
chartData: null, // 初始化 chartData 为 null
groupData: {}, // 集团数据
factoryData: [] // 工厂数据
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: {
ytdData: {
totalData: {
handler() {
this.processChartData();
},
@@ -80,136 +101,93 @@ export default {
},
},
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
* 核心方法按levelId匹配地名生成locations
*/
processChartData() {
// 1. 处理集团数据 - 提取各字段到对应数组
this.groupData = this.ytdData.group || { rate: 0, diff: 0, real: 0, target: 0 };
// 初始化空数据结构
const initTopBarData = {
locations: [], diff: [], targets: [], reals: [], rate: [], flags: []
};
const initBarData = { locations: [], diff: [], targets: [], reals: [], rate: [], flags: [] };
// 集团各维度数据数组(单条数据,对应凯盛新能)
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])];
if (!Array.isArray(this.totalData) || this.totalData.length === 0) {
this.chartData = { topBarData: initTopBarData, barData: initBarData };
return;
}
console.log('集团数据数组:', {
groupTarget,
groupDiff,
groupReal,
groupRate,
groupFlag,
rawGroupData: this.groupData
// 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. 处理工厂数据 - 提取每个工厂的对应字段到数组
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));
// 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);
});
// 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
// 遍历有效数据填充locationslevelId→地名
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); // 序号转数字
}
};
});
console.log('最终处理后的图表数据:', this.chartData);
},
// 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>
/* 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 {
@@ -222,14 +200,12 @@ getRateFlag(rate, real, target) {
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;
@@ -255,7 +231,6 @@ getRateFlag(rate, real, target) {
height: 14px;
border: 1px solid #adadad;
margin: 0 8px;
/* 优化分割线间距 */
}
.yield {
@@ -272,22 +247,18 @@ getRateFlag(rate, real, target) {
.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;
}
@@ -303,19 +274,15 @@ getRateFlag(rate, real, target) {
top: 0;
left: 0;
height: 100%;
background: linear-gradient(
65deg,
background: linear-gradient(65deg,
rgba(53, 223, 247, 0) 0%,
rgba(54, 220, 246, 0.92) 92%,
#36f6e5 100%,
#37acf5 100%
);
#37acf5 100%);
border-radius: 5px;
transition: width 0.3s ease;
/* 进度变化时添加过渡动画,更流畅 */
}
/* 图表相关样式保留 */
.chartImgBottom {
position: absolute;
bottom: 45px;
@@ -334,7 +301,6 @@ getRateFlag(rate, real, target) {
</style>
<style>
/* 全局 tooltip 样式(不使用 scoped确保生效 */
.production-status-chart-tooltip {
background: #0a2b4f77 !important;
border: none !important;

View File

@@ -1,12 +1,11 @@
<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>
<script>
import operatingLineBarSaleSingle from './operatingLineBarSaleSingle.vue';
import * as echarts from 'echarts';
export default {
name: "Container",
@@ -23,20 +22,18 @@ export default {
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
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?.budget || 0,
value: this.detailData?.targetValue || 0,
flag: 1, // 实际项:达标(绿色)
label: {
show: true,
position: 'top',
offset: [0, 0],
fontSize: 14,
},
itemStyle: {
color: {
@@ -52,13 +49,12 @@ export default {
},
},
{
value: this.detailData?.real || 0,
flag: this.detailData?.flag, // 实际项:达标(绿色)
value: this.detailData?.value || 0,
flag: this.detailData?.completed, // 实际项:达标(绿色)
label: {
show: true,
position: 'top',
offset: [0, 0],
fontSize: 14,
},
itemStyle: {
borderRadius: [4, 4, 0, 0],

View File

@@ -23,23 +23,51 @@ export default {
currentDataSource() {
console.log('yyyy', this.chartData);
return this.chartData.group
return this.chartData
},
locations() {
console.log('this.chartData', this.chartData);
console.log('this.1111', this.chartData);
return this.chartData.group.locations
return this.chartData.locations
},
// 根据按钮切换生成对应的 chartData
chartD() {
// 销量场景数据
const data = this.currentDataSource;
const diff = this.currentDataSource.diff[0]
const rate = this.currentDataSource.rate[0]
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',
@@ -135,7 +163,7 @@ export default {
name: '预算',
type: 'bar',
yAxisIndex: 0,
barWidth: 65,
barWidth: 14,
itemStyle: {
color: {
type: 'linear',
@@ -155,7 +183,7 @@ export default {
name: '实际',
type: 'bar',
yAxisIndex: 0,
barWidth: 65,
barWidth: 14,
itemStyle: {
color: (params) => {
const safeFlag = [1, 0, 1, 1, 0, 1]; // 达标状态

View File

@@ -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>

View File

@@ -11,22 +11,21 @@
</div>
<div class="number">
<div class="yield">
{{ ytdData?.rate || 0}}%
{{ formatRate(factoryData?.completeRate) }}%
</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="">
同比{{ 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=" 'totalG' " :detailData="ytdData" id="totalGauge"></electricityGauge>
<electricityGauge id="year" :detailData="factoryData"></electricityGauge>
</div> -->
</div>
<div class="line" style="padding: 0px;">
<verticalBarChart :refName=" 'totalVerticalBarChart' " :detailData="ytdData">
</verticalBarChart>
<!-- 传递包含flag的factoryData给柱状图组件 -->
<verticalBarChart :detailData="factoryData"></verticalBarChart>
</div>
</div>
</div>
@@ -44,55 +43,56 @@ import verticalBarChart from './verticalBarChart.vue'
export default {
name: 'ProductionStatus',
components: { Container, electricityGauge, verticalBarChart },
// mixins: [resize],
props: {
ytdData: { // 接收父组件传递的设备数据数组
totalData: {
type: Object,
default: () => {} // 默认空数组,避免报错
default: () => ({})
},
title: { // 接收父组件传递的设备数据数组
title: {
type: String,
default: () => '' // 默认空数组,避免报错
default: ''
},
month: { // 接收父组件传递的设备数据数组
month: {
type: String,
default: () => '' // 默认空数组,避免报错
default: ''
},
},
data() {
return {
chart: null,
}
},
watch: {
itemData: {
handler(newValue, oldValue) {
// this.updateChart()
},
deep: true // 若对象内属性变化需触发,需加 deep: true
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字段
};
}
},
// 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: {
/**
* 格式化百分比数值:处理空值/非数字兜底为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>
@@ -126,6 +126,7 @@ export default {
height: 205px;
background: #F9FCFF;
padding: 16px 0 0 10px;
.title {
// width: 190px;
height: 18px;

View File

@@ -1,8 +1,8 @@
<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>
<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>
@@ -14,153 +14,109 @@ export default {
components: {},
data() {
return {
myChart: null // 存储图表实例,避免重复创建
myChart: null
};
},
props: {
// 明确接收的props结构增强可读性
refName: {
type: String,
default: () => 'verticalBarChart',
default: 'verticalBarChart',
},
detailData: {
type: Object,
default: () => ({
completeRate: 0,
diff: 0,
real: 0,
target: 0,
thb: 0,
flag: 0
}),
}
},
mounted() {
this.$nextTick(() => {
this.updateChart();
});
this.$nextTick(() => this.updateChart());
},
// 新增:监听 chartData 变化
watch: {
// 深度监听数据变化,仅更新图表配置(不销毁实例)
detailData: {
handler() {
console.log(this.chartData, 'chartData');
this.updateChart();
},
deep: true,
immediate: 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;
}
console.log('this.detailData', this.detailData);
// 修复优化实例销毁逻辑避免重复dispose
if (this.myChart) {
this.myChart.dispose();
this.myChart.clear(); // 先清空,再重新渲染
} else {
this.myChart = echarts.init(chartDom);
}
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 { 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'
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背景避免干扰
show: false
},
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的数据自动生成
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, // 显示Y轴轴线关键
lineStyle: {
color: '#E5E6EB', // 轴线颜色(浅灰色,可自定义)
width: 1, // 轴线宽度
type: 'solid' // 实线可选dashed虚线、dotted点线
}
},
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 },
// padding: [300, 100, 100, 100],
data: ['实际', '预算'] // y轴分类实际、预算
data: ['实际', '预算']
},
series: [
{
// name: '预算',
type: 'bar',
barWidth: 24,
// barCategoryGap: '50', // 柱子之间的间距(相对于柱子宽度)
// 数据长度与yAxis的分类数量匹配实际、预算各一个值
data: [{
value: this.detailData.real,
// 修复:拆分数据项,确保每个柱子的样式独立生效
data: [
// 实际值柱子核心绑定flag颜色
{
value: realValue,
label: {
show: true,
position: 'right',
fontSize: 14,
fontSize: 14
},
// 修复flag颜色判断独立绑定到实际值柱子
itemStyle: {
color: flagValue === 1
? {
@@ -181,44 +137,46 @@ export default {
},
borderRadius: [4, 4, 0, 0]
}
}, {
value: this.detailData.target,
},
// 预算值柱子(固定蓝色渐变)
{
value: targetValue,
label: {
show: true,
position: 'right',
fontSize: 14,
fontSize: 14
},
itemStyle: {
// 预算的渐变颜色(蓝系渐变)
color: {
type: 'linear',
x: 1, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: '#82CCFF' }, // 浅蓝
{ offset: 1, color: '#4B9DFF' } // 深蓝
{ offset: 0, color: '#82CCFF' },
{ offset: 1, color: '#4B9DFF' }
]
},
borderRadius: [4, 4, 0, 0],
borderWidth: 0
},
},],
},
}
}
]
}
]
};
option && this.myChart.setOption(option);
this.myChart.setOption(option, true); // 新增true表示替换所有配置避免缓存
// 窗口缩放适配和销毁逻辑保持不变
window.addEventListener('resize', () => {
// 优化防抖resize避免频繁触发
const resizeHandler = () => {
this.myChart && this.myChart.resize();
});
};
window.removeEventListener('resize', resizeHandler); // 先移除再添加,避免重复绑定
window.addEventListener('resize', resizeHandler);
this.$once('hook:destroyed', () => {
window.removeEventListener('resize', () => {
this.myChart && this.myChart.resize();
});
window.removeEventListener('resize', resizeHandler);
this.myChart && this.myChart.dispose();
this.myChart = null;
});
}
},

View File

@@ -1,36 +1,24 @@
<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%;">
<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;'>
<!-- 新增topItem 专属包裹容器统一控制样式和布局 -->
<div class="topItem-container" style="display: flex; gap: 8px;">
<div class="dashboard left">
<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 class="line">
<operatingSingleBar></operatingSingleBar>
</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="dashboard right">
<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 class="line">
<operatingSingleBar></operatingSingleBar>
</div>
</div>
</div>
@@ -38,163 +26,102 @@
</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 },
components: { Container, operatingSingleBar, verticalBarChart },
// mixins: [resize],
props: {
ytdAnalysis: {
itemData: { // 接收父组件传递的设备数据数组
type: Array,
default: () => [
{ title: "销量", budget: 0, real: 0, rate: 0, diff: 0 },
{ title: "单价", budget: 0, real: 0, rate: 0, diff: 0 }
]
default: () => [] // 默认空数组,避免报错
},
dateData: {
type: Object,
default: () => {}
},
title: {
title: { // 接收父组件传递的设备数据数组
type: String,
default: ''
default: () => '' // 默认空数组,避免报错
},
month: {
month: { // 接收父组件传递的设备数据数组
type: String,
default: ''
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)
itemData: {
handler(newValue, oldValue) {
// this.updateChart()
},
deep: true,
immediate: true
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() {
this.updateChart(this.ytdAnalysis)
// 初始化图表(若需展示图表,需在模板中添加对应 DOM
// this.$nextTick(() => this.updateChart())
},
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>
/* 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;
}
.topItem-container {
display: flex;
justify-content: space-between;
}
.dashboard {
flex: 1;
min-width: 300px;
width: 382px;
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;
// width: 190px;
height: 18px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
@@ -203,15 +130,74 @@ getRateFlag(rate, real, target) {
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;
}
}
.dashboard.left {
margin-left: 0;
// .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);
}
.dashboard.right {
margin-right: 0;
.production-status-chart-tooltip * {
color: #fff !important;
}
</style>
</style> -->

View File

@@ -85,7 +85,7 @@ export default {
},
grid: {
top: 25,
bottom: 20,
bottom: 25,
right: 10,
left: 2,
containLabel: true

View File

@@ -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>
@@ -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,7 +69,7 @@ export default {
series: [
{
type: 'bar',
barWidth: 24,
barWidth: 60,
barCategoryGap: '50%',
data: seriesData,
itemStyle: {

View File

@@ -2,65 +2,22 @@
<div style="flex: 1">
<Container :isShowTab="true" :name="title" icon="cockpitItemIcon" size="opLargeBg" topSize="large"
@tabChange="handleChange">
<!-- 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"
@click="handleDashboardClick('/productionCostAnalysis/productionCostAnalysisBase')">
<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="line">
<operatingSingleBar :detailData="{
...(relatedDetailData.制造成本 || defaultData),
flag: getRateFlag((relatedDetailData.制造成本 || defaultData).completeRate, (relatedDetailData.制造成本 || defaultData).real, (relatedDetailData.制造成本 || defaultData).target)
}" />
</div>
</div>
<div class="dashboard right" @click="handleDashboardClick('/expenseAnalysis/expenseAnalysisBase')">
<div class="title">
财务费用·/
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedDetailData.财务费用 || defaultData),
flag: getRateFlag((relatedDetailData.财务费用 || defaultData).completeRate, (relatedDetailData.财务费用 || defaultData).real, (relatedDetailData.财务费用 || defaultData).target)
}" />
</div>
</div>
<div class="dashboard right" @click="handleDashboardClick('/expenseAnalysis/expenseAnalysisBase')">
<div class="title">
销售费用·/
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedDetailData.销售费用 || defaultData),
flag: getRateFlag((relatedDetailData.销售费用 || defaultData).completeRate, (relatedDetailData.销售费用 || defaultData).real, (relatedDetailData.销售费用 || defaultData).target,)
}" />
</div>
</div>
<div class="dashboard right" @click="handleDashboardClick('/expenseAnalysis/expenseAnalysisBase')">
<div class="title">
管理费用·/
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedDetailData.管理费用 || defaultData),
flag: getRateFlag((relatedDetailData.管理费用 || defaultData).completeRate, (relatedDetailData.管理费用 || defaultData).real, (relatedDetailData.管理费用 || defaultData).target)
}" />
</div>
</div>
<div class="dashboard right">
<div class="title">
运费·/
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedDetailData.运费 || defaultData),
flag: getRateFlag((relatedDetailData.运费 || defaultData).completeRate, (relatedDetailData.运费 || defaultData).reat, (relatedDetailData.运费 || defaultData).target)
}" />
<div style='font-size: 16px;text-align: right;padding-right: 5px;'>
<span>完成率:<span style='color: #0B58FF;'>{{item.detailData.completeRate}}%</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>
@@ -116,6 +73,57 @@ export default {
}
}
},
computed: {
indicatorDefs() {
return [
{ key: 'productionCost', name: '制造成本', unit: '元/㎡', route:'/productionCostAnalysis/productionCostAnalysisBase'},
{ key: 'financialCost', name: '财务费用', unit: '元/㎡',route:'/expenseAnalysis/expenseAnalysisBase' },
{ key: 'saleCost', name: '销售费用', unit: '元/㎡',route:'/expenseAnalysis/expenseAnalysisBase'},
{ key: 'manageCost', name: '管理费用', unit: '元/㎡',route:'/expenseAnalysis/expenseAnalysisBase' },
{ key: 'freight', name: '运费', unit: '元/㎡',route:null },
]
},
indicators() {
let _this = this
const fallback = { target: 0, real: 0, completeRate: 0, diff: 0, flag: 0 }
const list = Object.entries(_this.relatedDetailData).map(([title, data]) => {
return {
title: title,
target: data.target,
real: data.real,
completeRate: data.completeRate,
diff: data.diff
};
});
return _this.indicatorDefs.map(def => {
const data = list.find(item => item && item.title === def.name) || fallback
const detailData = {
...data,
flag: _this.getRateFlag((data || _this.defaultData).completeRate, (data || _this.defaultData).real, (data || _this.defaultData).target),
}
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: {
// 监听 relatedData 变化(异步加载场景),同步更新月度数据
relatedData: {
@@ -145,12 +153,7 @@ export default {
}
})
},
handleRoute(path) {
this.$router.push({
path: path
})
},
getRateFlag(rate, real, target) {
getRateFlag(rate, real, target) {
if (isNaN(rate) || rate === null || rate === undefined) return 0;
// 1. 完成率 >= 100 => 达标
@@ -161,7 +164,7 @@ getRateFlag(rate, real, target) {
// 其他情况 => 未达标
return 0;
},
},
handleChange(value) {
console.log('value', value, this.relatedData);
if (value === 'month') {

View File

@@ -85,7 +85,7 @@ export default {
},
grid: {
top: 25,
bottom: 20,
bottom: 25,
right: 10,
left: 2,
containLabel: true

View File

@@ -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>
@@ -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,72 +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;
// console.log('flags[params.dataIndex]', flags);
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],
@@ -188,7 +69,7 @@ export default {
series: [
{
type: 'bar',
barWidth: 24,
barWidth: 60,
barCategoryGap: '50%',
data: seriesData,
itemStyle: {

View File

@@ -3,61 +3,21 @@
<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 class="dashboard left">
<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="line">
<operatingSingleBar :detailData="{
...(relatedDetailData.大宗增效额 || defaultData),
flag: getRateFlag((relatedDetailData.大宗增效额 || defaultData).completeRate)
}" />
</div>
</div>
<div class="dashboard right">
<div class="title">
石料类·万元
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedDetailData.石料增效额 || defaultData),
flag: getRateFlag((relatedDetailData.石料增效额 || defaultData).completeRate)
}" />
</div>
</div>
<div class="dashboard right">
<div class="title">
材料类·万元
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedDetailData.材料增效额 || defaultData),
flag: getRateFlag((relatedDetailData.材料增效额 || defaultData).completeRate)
}" />
</div>
</div>
<div class="dashboard right">
<div class="title">
动力类·万元
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedDetailData.动力增效额 || defaultData),
flag: getRateFlag((relatedDetailData.动力增效额 || defaultData).completeRate)
}" />
</div>
</div>
<div class="dashboard right" @click="handleRoute('/salesVolumeAnalysis/doublePlatedBase')">
<div class="title">
技术服务类·万元
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedDetailData.技术服务类 || defaultData),
flag: getRateFlag((relatedDetailData.技术服务类 || defaultData).completeRate)
}" />
<div style='font-size: 16px;text-align: right;padding-right: 5px;'>
<span>完成率:<span style='color: #0B58FF;'>{{item.detailData.completeRate}}%</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>
@@ -106,6 +66,57 @@ export default {
}
}
},
computed: {
indicatorDefs() {
return [
{ key: 'grossProfit', name: '大宗类', unit: '万元', route:null},
{ key: 'stoneProfit', name: '石料类', unit: '万元',route:null},
{ key: 'materialProfit', name: '材料类', unit: '万元',route:null},
{ key: 'powerProfit', name: '动力类', unit: '万元',route:null},
{ key: 'technicalServiceProfit', name: '技术服务类', unit: '万元',route:'/salesVolumeAnalysis/doublePlatedBase' },
]
},
indicators() {
let _this = this
const fallback = { target: 0, real: 0, completeRate: 0, diff: 0, flag: 0 }
const list = Object.entries(_this.relatedDetailData).map(([title, data]) => {
return {
title: title,
target: data.target,
real: data.real,
completeRate: data.completeRate,
diff: data.diff
};
});
return _this.indicatorDefs.map(def => {
const data = list.find(item => item && item.title.includes(def.name.slice(0, -1))) || fallback
const detailData = {
...data,
flag: _this.getRateFlag((data || _this.defaultData).completeRate, (data || _this.defaultData).real, (data || _this.defaultData).target),
}
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: {
// 监听 relatedData 变化(异步加载场景),同步更新月度数据
relatedData: {
@@ -131,9 +142,17 @@ export default {
path: path
})
},
getRateFlag(rate) {
getRateFlag(rate, real, target) {
if (isNaN(rate) || rate === null || rate === undefined) return 0;
return +(rate >= 100 || rate === 0);
// 1. 完成率 >= 100 => 达标
if (rate >= 100) return 1;
// 2. 完成率 = 0 且 (目标值=0 或 实际值=目标值=0) => 达标
if (rate === 0 && target === 0) return 1;
// 其他情况 => 未达标
return 0;
},
handleChange(value) {
console.log('value', value, this.relatedData);

View File

@@ -2,7 +2,7 @@
<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="原片燃成本分析"
<ReportHeader :dateData="dateData" size="psi" @timeRangeChange="handleTimeChange" top-title="原片燃动力成本分析"
:is-full-screen="isFullScreen" @screenfullChange="screenfullChange" />
<div class="main-body" style="
margin-top: -20px;

View File

@@ -0,0 +1,297 @@
<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: 804px 804px;
">
<monthlyRelatedMetrics :dateData="dateData" :factory="factory" :relatedData="monthRelatedData"
:title="'月度·相关指标分析'" />
<yearRelatedMetrics :dateData="dateData" :factory="factory" :relatedData="totalRelatedData" :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>
</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 "../productionCostAnalysisComponents/monthlyOverview.vue";
import totalOverview from "../productionCostAnalysisComponents/totalOverview.vue";
import monthlyRelatedMetrics from "../productionCostAnalysisComponents/monthlyThreeRelatedMetricsCCA.vue";
import yearRelatedMetrics from "../productionCostAnalysisComponents/yearThreeRelatedMetricsCCA.vue";
import dataTrend from "../productionCostAnalysisComponents/dataTrendSingleCCA.vue";
import { mapState } from "vuex";
import { getSingleMaterialAnalysis } from '@/api/cockpit'
import moment from "moment";
export default {
name: "DayReport",
components: {
ReportHeader,
changeBase,
monthlyOverview,
Sidebar,
totalOverview,
monthlyRelatedMetrics,
yearRelatedMetrics,
dataTrend
// psiLineChart
},
data() {
return {
isFullScreen: false,
timer: null,
beilv: 1,
month: '',
value: 100,
factory: null,
dateData: {},
monData: {},
totalData: {},
trend: [],
monthRelatedData: [],
totalRelatedData: [],
trendName: '复合澄清剂',
meterialName:'复合澄清剂',
};
},
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,
}),
renderList() {
if (this.itemData && this.itemData.length > 0) {
return this.itemData;
}
return this.parentItemList;
},
classObj() {
return {
hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation,
mobile: this.device === "mobile",
};
},
variables() {
return variables;
},
styles() {
const v = Math.floor(this.value * this.beilv * 100) / 10000;
return {
transform: `scale(${v})`,
transformOrigin: "left top",
};
},
},
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: {
changeItem(item) {
this.trendName = item
this.getData()
},
getData() {
const requestParams = {
startTime: this.dateData.startTime,
endTime: this.dateData.endTime,
trendName: this.trendName+'成本',
analysisObject: ['复合澄清剂成本'],
levelId: this.factory,
};
// 调用接口
getSingleMaterialAnalysis(requestParams).then((res) => {
this.monData = res.data.currentMonthData.find(item => {
return item.name === '复合澄清剂成本';
});
this.totalData = res.data.totalMonthData.find(item => {
return item.name === '复合澄清剂成本';
});
this.monthRelatedData = res.data.currentMonthData
this.totalRelatedData = res.data.totalMonthData
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) {
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);
}
}
},
};
</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>

View File

@@ -33,7 +33,7 @@
<div class="middle" style="display: flex; gap: 16px;margin-top: 6px;">
<div class="left-three" style="
display: grid;
gap: 12px;
gap: 6px;
grid-template-columns: 1624px;
">
<!-- <monthlyRelatedMetrics :itemData="renderList" :title="'月度·相关指标分析'" /> -->

View File

@@ -37,8 +37,8 @@
grid-template-columns: 804px 804px;
">
<monthlyRelatedMetrics :dateData="dateData" :factory="factory" :relatedData="monthRelatedData"
:title="'月度概览'" />
<yearRelatedMetrics :dateData="dateData" :factory="factory" :relatedData="totalRelatedData" :title="'累计概览'" />
:title="'月度·相关指标分析'" />
<yearRelatedMetrics :dateData="dateData" :factory="factory" :relatedData="totalRelatedData" :title="'累计·相关指标分析'" />
</div>
</div>
<div class="bottom" style="display: flex; gap: 16px;margin-top: 6px;">

View File

@@ -2,7 +2,7 @@
<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="加工燃成本分析"
<ReportHeader :dateData="dateData" size="psi" @timeRangeChange="handleTimeChange" top-title="加工燃动力成本分析"
:is-full-screen="isFullScreen" @screenfullChange="screenfullChange" />
<div class="main-body" style="
margin-top: -20px;

View File

@@ -4,7 +4,7 @@
<sidebar v-if="!sidebar.hide" class="sidebar-container" />
<ReportHeader :dateData="dateData" size="psi" @timeRangeChange="handleTimeChange"
@selectChange='handlefuelChange' :selectName='fuelName' :selectOptions='fuelOptions'
top-title="单项原片燃成本分析" :is-full-screen="isFullScreen"
top-title="单项原片燃动力成本分析" :is-full-screen="isFullScreen"
@screenfullChange="screenfullChange" :leftMargin="'280px'" />
<div class="main-body" style="
margin-top: -20px;

View File

@@ -4,7 +4,7 @@
<sidebar v-if="!sidebar.hide" class="sidebar-container" />
<ReportHeader :dateData="dateData" size="psi" @timeRangeChange="handleTimeChange"
@selectChange='handleFuelChange' :selectName='fuelName' :selectOptions='fuelOptions'
top-title="单项加工燃成本分析" :is-full-screen="isFullScreen"
top-title="单项加工燃动力成本分析" :is-full-screen="isFullScreen"
@screenfullChange="screenfullChange" :leftMargin="'280px'" />
<div class="main-body" style="
margin-top: -20px;

View File

@@ -0,0 +1,475 @@
<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>

View File

@@ -64,10 +64,13 @@ export default {
profitOptions: [
'原片原料成本',
'硅砂',
'海砂',
'纯碱',
'白云石',
'石灰石',
'复合澄清剂',
'氢氧化铝',
'助溶剂',
'碎玻璃'
]
};
},

View File

@@ -66,6 +66,7 @@ export default {
'产量',
'单耗',
'消耗量',
'日均消耗量'
]
};
},

View 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 "./dataTrendBarCCA.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>

View File

@@ -0,0 +1,239 @@
<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
v-for="item in sortedIndicators"
:key="item.key"
class="dashboard"
@click="handleDashboardClick(item.name)"
>
<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 './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: Array,
default: () => []
},
factory: {
type: [String, Number],
default: ''
},
dateData: {
type: Object,
default: () => ({})
},
// 可选:动态标题(原有配置保留)
title: {
type: String,
default: ''
}
},
data() {
return {
chart: null
// 注释无效的activeData保持数据简洁样式不变
// activeData: this.relatedData || []
}
},
computed: {
indicatorDefs() {
return [
{ key: 'mx', name: '芒硝', unit: '元/㎡'},
{ key: 'xsn', name: '硝酸钠', unit: '元/㎡'},
{ key: 'jtsn', name: '焦锑酸钠', unit: '元/㎡'},
]
},
indicators() {
const fallback = { targetValue: 0, value: 0, completed: 0, diffValue: 0 }
const list = (Array.isArray(this.relatedData) ? this.relatedData : [])
return this.indicatorDefs.map(def => {
const data = list.find(item => item && item.name.includes(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注释若需恢复可直接启用样式不变
// watch: {
// // 可选:监听 relatedData 初始变化(若父组件异步传递数据,确保 activeData 同步更新)
// relatedData: {
// handler(newVal) {
// this.activeData = newVal.relatedMon || [];
// },
// immediate: true,
// deep: true
// }
// },
mounted() {
// 优化打印日志输出有效数据移除无效的activeData打印样式不变
console.log('组件挂载时的相关数据:', this.relatedData);
},
methods: {
handleDashboardClick(name) {
this.$router.push({
path: 'singleProcMfgOverheadCost',
query: {
name,
factory: this.$route.query.factory ? this.$route.query.factory : this.factory,
dateData: this.dateData
}
})
}
}
}
</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: 250px;
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> -->

View File

@@ -140,7 +140,7 @@ export default {
top: 30,
bottom: 5,
right: 20,
left: 25,
left: 35,
containLabel: true
},
xAxis: [

View File

@@ -71,7 +71,7 @@ export default {
series: [
{
type: 'bar',
barWidth: 60,
barWidth: 40,
barCategoryGap: '50%',
data: seriesData,
itemStyle: {

View File

@@ -13,7 +13,7 @@
<div class="title">
{{ item.name }}·{{ item.unit }}
</div>
<div style='font-size: 16px;text-align: right;padding-right: 5px;'>
<div style='font-size: 14px;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>
@@ -65,10 +65,14 @@ export default {
indicatorDefs() {
return [
{ key: 'silicaSand', name: '硅砂', unit: '元/㎡', route: 'SIMFRMCostAnalysis'},
{ key: 'seaSand', name: '海砂', unit: '元/㎡', route: 'SIMFRMCostAnalysis'},
// { key: 'seaSand', name: '海砂', unit: '元/㎡', route: 'SIMFRMCostAnalysis'},
{ key: 'sodaAsh', name: '纯碱', unit: '元/㎡', route: 'SIMFRMCostAnalysis'},
{ key: 'dolomite', name: '白云石', unit: '元/㎡', route: 'SIMFRMCostAnalysis'},
{ key: 'limestone', name: '石灰石', unit: '元/㎡', route: 'SIMFRMCostAnalysis'},
{ key: 'compoundClarifyingAgent', name: '复合澄清剂', unit: '元/㎡', route: 'compositeClarifyingAgentCostAnalysis'},
{ key: 'ATH', name: '氢氧化铝', unit: '元/㎡', route: null},
{ key: 'cosolvent', name: '助溶剂', unit: '元/㎡', route: null},
{ key: 'brokenGlass', name: '碎玻璃', unit: '元/㎡', route: null},
]
},
indicators() {
@@ -166,7 +170,7 @@ export default {
}
.dashboard {
width: 310px;
width: 194px;
height: 205px;
background: #F9FCFF;
padding: 16px 0 0 10px;

View File

@@ -68,7 +68,8 @@ export default {
{ key: 'rawMaterialCost', name: '采购单价', unit: '元/吨'},
{ key: 'fuelCost', name: '产量', unit: '吨'},
{ key: 'electricityCost', name: '单耗', unit: '吨'},
{ key: 'laborCost', name: '消耗量', unit: '吨'}
{ key: 'laborCost', name: '消耗量', unit: '吨'},
{ key: 'laborCostDay', name: '日均消耗量', unit: '吨'}
]
},
indicators() {
@@ -156,7 +157,7 @@ export default {
}
.dashboard {
width: 390px;
width: 312px;
height: 205px;
background: #F9FCFF;
padding: 16px 0 0 10px;

View File

@@ -0,0 +1,239 @@
<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
v-for="item in sortedIndicators"
:key="item.key"
class="dashboard"
@click="handleDashboardClick(item.name)"
>
<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 './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: Array,
default: () => []
},
factory: {
type: [String, Number],
default: ''
},
dateData: {
type: Object,
default: () => ({})
},
// 可选:动态标题(原有配置保留)
title: {
type: String,
default: ''
}
},
data() {
return {
chart: null
// 注释无效的activeData保持数据简洁样式不变
// activeData: this.relatedData || []
}
},
computed: {
indicatorDefs() {
return [
{ key: 'mx', name: '芒硝', unit: '元/㎡'},
{ key: 'xsn', name: '硝酸钠', unit: '元/㎡'},
{ key: 'jtsn', name: '焦锑酸钠', unit: '元/㎡'},
]
},
indicators() {
const fallback = { targetValue: 0, value: 0, completed: 0, diffValue: 0 }
const list = (Array.isArray(this.relatedData) ? this.relatedData : [])
return this.indicatorDefs.map(def => {
const data = list.find(item => item && item.name.includes(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注释若需恢复可直接启用样式不变
// watch: {
// // 可选:监听 relatedData 初始变化(若父组件异步传递数据,确保 activeData 同步更新)
// relatedData: {
// handler(newVal) {
// this.activeData = newVal.relatedMon || [];
// },
// immediate: true,
// deep: true
// }
// },
mounted() {
// 优化打印日志输出有效数据移除无效的activeData打印样式不变
console.log('组件挂载时的相关数据:', this.relatedData);
},
methods: {
handleDashboardClick(name) {
this.$router.push({
path: 'singleProcMfgOverheadCost',
query: {
name,
factory: this.$route.query.factory ? this.$route.query.factory : this.factory,
dateData: this.dateData
}
})
}
}
}
</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: 250px;
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> -->

View File

@@ -1,31 +1,21 @@
<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="topItem-container" style="display: flex; gap: 8px">
<div
v-for="item in sortedIndicators"
:key="item.key"
class="dashboard"
>
<div class="title">
产量·
{{ item.name }}·{{ item.unit }}
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedMon['产量(深加工)'] || defaultData),
flag: getRateFlag((relatedMon['产量(深加工)'] || defaultData).completeRate, (relatedMon['产量(深加工)'] || defaultData).real, (relatedMon['产量(深加工)'] || defaultData).target)
}" />
</div>
</div>
<div class="dashboard right">
<div class="title">
销量·
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedMon.销量 || defaultData),
flag: getRateFlag((relatedMon.销量 || defaultData).completeRate, (relatedMon.销量 || defaultData).real, (relatedMon.销量 || defaultData).target)
}" />
<div style='font-size: 16px;text-align: right;padding-right: 5px;'>
<span>完成率:<span style='color: #0B58FF;'>{{item.detailData.completeRate}}%</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>
@@ -35,7 +25,6 @@
<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'
@@ -69,6 +58,54 @@ export default {
}
}
},
computed: {
indicatorDefs() {
return [
{ key: 'production', name: '产量', unit: '万㎡'},
{ key: 'financialCost', name: '销量', unit: '万㎡'},
]
},
indicators() {
let _this = this
const fallback = { target: 0, real: 0, completeRate: 0, diff: 0, flag: 0 }
const list = Object.entries(_this.relatedMon).map(([title, data]) => {
return {
title: title,
target: data.target,
real: data.real,
completeRate: data.completeRate,
diff: data.diff
};
});
return _this.indicatorDefs.map(def => {
const data = list.find(item => item && item.title === def.name) || fallback
const detailData = {
...data,
flag: _this.getRateFlag((data || _this.defaultData).completeRate, (data || _this.defaultData).real, (data || _this.defaultData).target),
}
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: {
relatedMon: {
handler(newValue) {

View File

@@ -1,42 +1,21 @@
<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="topItem-container" style="display: flex; gap: 8px">
<div
v-for="item in sortedIndicators"
:key="item.key"
class="dashboard"
>
<div class="title">
双镀成本·/
{{ item.name }}·{{ item.unit }}
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedMon.双镀成本 || defaultData),
flag: getRateFlag((relatedMon.双镀成本 || defaultData).completeRate, (relatedMon.双镀成本 || defaultData).real, (relatedMon.双镀成本 || defaultData).target)
}" />
</div>
</div>
<div class="dashboard right">
<div class="title">
双镀均价·/
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedMon.双镀均价 || defaultData),
flag: getRateFlag((relatedMon.双镀均价 || defaultData).completeRate, (relatedMon.双镀均价 || defaultData).real, (relatedMon.双镀均价 || defaultData).target)
}" />
</div>
</div>
<div class="dashboard right">
<div class="title">
双镀毛利·万元
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedMon.双镀毛利 || defaultData),
flag: getRateFlag((relatedMon.双镀毛利 || defaultData).completeRate, (relatedMon.双镀毛利 || defaultData).real, (relatedMon.双镀毛利 || defaultData).target)
}" />
<div style='font-size: 16px;text-align: right;padding-right: 5px;'>
<span>完成率:<span style='color: #0B58FF;'>{{item.detailData.completeRate}}%</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>
@@ -46,8 +25,6 @@
<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'
@@ -81,6 +58,55 @@ export default {
}
}
},
computed: {
indicatorDefs() {
return [
{ key: 'financialCost', name: '双镀成本', unit: '元/㎡'},
{ key: 'financialPrice', name: '双镀均价', unit: '元/㎡'},
{ key: 'financialProfit', name: '双镀毛利', unit: '万元'},
]
},
indicators() {
let _this = this
const fallback = { target: 0, real: 0, completeRate: 0, diff: 0, flag: 0 }
const list = Object.entries(_this.relatedMon).map(([title, data]) => {
return {
title: title,
target: data.target,
real: data.real,
completeRate: data.completeRate,
diff: data.diff
};
});
return _this.indicatorDefs.map(def => {
const data = list.find(item => item && item.title === def.name) || fallback
const detailData = {
...data,
flag: _this.getRateFlag((data || _this.defaultData).completeRate, (data || _this.defaultData).real, (data || _this.defaultData).target),
}
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: {
relatedMon: {
handler(newValue) {

View File

@@ -85,7 +85,7 @@ export default {
},
grid: {
top: 25,
bottom: 20,
bottom: 25,
right: 10,
left: 2,
containLabel: true

View File

@@ -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>
@@ -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,72 +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;
// console.log('flags[params.dataIndex]', flags);
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],
@@ -188,7 +69,7 @@ export default {
series: [
{
type: 'bar',
barWidth: 24,
barWidth: 60,
barCategoryGap: '50%',
data: seriesData,
itemStyle: {

View File

@@ -3,83 +3,21 @@
<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 class="dashboard left" @click="handleRoute('/netPriceAnalysis/netPriceAnalysisBase')">
<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="line">
<operatingSingleBar :detailData="{
...(relatedDetailData.净价 || defaultData),
flag: getRateFlag((relatedDetailData.净价 || defaultData).completeRate)
}" />
</div>
</div>
<div class="dashboard right" @click="handleRoute('/unitPriceAnalysis/unitPriceAnalysisBase')">
<div class="title">
单价·/
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedDetailData.单价 || defaultData),
flag: getRateFlag((relatedDetailData.单价 || defaultData).completeRate)
}" />
</div>
</div>
<div class="dashboard right" @click="handleRoute('/salesVolumeAnalysis/productionSalesBase')">
<div class="title">
产销率·%
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedDetailData.产销率 || defaultData),
flag: getRateFlag((relatedDetailData.产销率 || defaultData).completeRate)
}" />
</div>
</div>
<div class="dashboard right" @click="handleRoute('/salesVolumeAnalysis/doublePlatedBase')">
<div class="title">
双镀销量·
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedDetailData.双镀销量 || defaultData),
flag: getRateFlag((relatedDetailData.双镀销量 || defaultData).completeRate)
}" />
</div>
</div>
<div class="dashboard right" @click="handleRoute('/salesVolumeAnalysis/doublePlatedBase')">
<div class="title">
双镀占比·%
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedDetailData.双镀占比 || defaultData),
flag: getRateFlag((relatedDetailData.双镀占比 || defaultData).completeRate)
}" />
</div>
</div>
<div class="dashboard right">
<div class="title">
溢价产品销量·
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedDetailData.溢价产品销量 || defaultData),
flag: getRateFlag((relatedDetailData.溢价产品销量 || defaultData).completeRate)
}" />
</div>
</div>
<div class="dashboard right">
<div class="title">
溢价产品毛利·万元
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedDetailData.溢价产品毛利 || defaultData),
flag: getRateFlag((relatedDetailData.溢价产品毛利 || defaultData).completeRate)
}" />
<div style='font-size: 16px;text-align: right;padding-right: 5px;'>
<span>完成率:<span style='color: #0B58FF;'>{{item.detailData.completeRate}}%</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>
@@ -128,6 +66,59 @@ export default {
}
}
},
computed: {
indicatorDefs() {
return [
{ key: 'netPrice', name: '净价', unit: '万㎡', route:'/netPriceAnalysis/netPriceAnalysisBase'},
{ key: 'unitPrice', name: '单价', unit: '元/㎡',route:'/unitPriceAnalysis/unitPriceAnalysisBase' },
{ key: 'productionSales', name: '产销率', unit: '%',route:'/salesVolumeAnalysis/productionSalesBase'},
{ key: 'manageCost', name: '双镀销量', unit: '万㎡',route:'/salesVolumeAnalysis/doublePlatedBase' },
{ key: 'freight', name: '双镀占比', unit: '%',route:'/salesVolumeAnalysis/doublePlatedBase' },
{ key: 'manageCost', name: '溢价产品销量', unit: '万㎡',route:null },
{ key: 'premiumProducts', name: '溢价产品毛利', unit: '万元',route:null },
]
},
indicators() {
let _this = this
const fallback = { target: 0, real: 0, completeRate: 0, diff: 0, flag: 0 }
const list = Object.entries(_this.relatedDetailData).map(([title, data]) => {
return {
title: title,
target: data.target,
real: data.real,
completeRate: data.completeRate,
diff: data.diff
};
});
return _this.indicatorDefs.map(def => {
const data = list.find(item => item && item.title === def.name) || fallback
const detailData = {
...data,
flag: _this.getRateFlag((data || _this.defaultData).completeRate, (data || _this.defaultData).real, (data || _this.defaultData).target),
}
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: {
// 监听 relatedData 变化(异步加载场景),同步更新月度数据
relatedData: {

View File

@@ -1,31 +1,21 @@
<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="topItem-container" style="display: flex; gap: 8px">
<div
v-for="item in sortedIndicators"
:key="item.key"
class="dashboard"
>
<div class="title">
产量·
{{ item.name }}·{{ item.unit }}
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedTotal['产量(深加工)'] || defaultData),
flag: getRateFlag((relatedTotal['产量(深加工)'] || defaultData).completeRate, (relatedTotal['产量(深加工)'] || defaultData).real, (relatedTotal['产量(深加工)'] || defaultData).target)
}" />
</div>
</div>
<div class="dashboard right">
<div class="title">
销量·
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedTotal.销量 || defaultData),
flag: getRateFlag((relatedTotal.销量 || defaultData).completeRate, (relatedTotal.销量 || defaultData).real, (relatedTotal.销量 || defaultData).target)
}" />
<div style='font-size: 16px;text-align: right;padding-right: 5px;'>
<span>完成率:<span style='color: #0B58FF;'>{{item.detailData.completeRate}}%</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>
@@ -69,6 +59,54 @@ export default {
}
}
},
computed: {
indicatorDefs() {
return [
{ key: 'production', name: '产量', unit: '万㎡'},
{ key: 'financialCost', name: '销量', unit: '万㎡'},
]
},
indicators() {
let _this = this
const fallback = { target: 0, real: 0, completeRate: 0, diff: 0, flag: 0 }
const list = Object.entries(_this.relatedTotal).map(([title, data]) => {
return {
title: title,
target: data.target,
real: data.real,
completeRate: data.completeRate,
diff: data.diff
};
});
return _this.indicatorDefs.map(def => {
const data = list.find(item => item && item.title === def.name) || fallback
const detailData = {
...data,
flag: _this.getRateFlag((data || _this.defaultData).completeRate, (data || _this.defaultData).real, (data || _this.defaultData).target),
}
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: {
relatedTotal: {
handler(newValue) {

View File

@@ -1,42 +1,22 @@
<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="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="line">
<operatingSingleBar :detailData="{
...(relatedTotal.双镀成本 || defaultData),
flag: getRateFlag((relatedTotal.双镀成本 || defaultData).completeRate, (relatedTotal.双镀成本 || defaultData).real, (relatedTotal.双镀成本 || defaultData).target)
}" />
</div>
</div>
<div class="dashboard right">
<div class="title">
双镀均价·/
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedTotal.双镀均价 || defaultData),
flag: getRateFlag((relatedTotal.双镀均价 || defaultData).completeRate, (relatedTotal.双镀均价 || defaultData).real, (relatedTotal.双镀均价 || defaultData).target)
}" />
</div>
</div>
<div class="dashboard right">
<div class="title">
双镀毛利·万元
</div>
<div class="line">
<operatingSingleBar :detailData="{
...(relatedTotal.双镀毛利 || defaultData),
flag: getRateFlag((relatedTotal.双镀毛利 || defaultData).completeRate, (relatedTotal.双镀毛利 || defaultData).real, (relatedTotal.双镀毛利 || defaultData).target)
}" />
<div style='font-size: 16px;text-align: right;padding-right: 5px;'>
<span>完成率:<span style='color: #0B58FF;'>{{item.detailData.completeRate}}%</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>
@@ -81,6 +61,55 @@ export default {
}
}
},
computed: {
indicatorDefs() {
return [
{ key: 'financialCost', name: '双镀成本', unit: '元/㎡'},
{ key: 'financialPrice', name: '双镀均价', unit: '元/㎡'},
{ key: 'financialProfit', name: '双镀毛利', unit: '万元'},
]
},
indicators() {
let _this = this
const fallback = { target: 0, real: 0, completeRate: 0, diff: 0, flag: 0 }
const list = Object.entries(_this.relatedTotal).map(([title, data]) => {
return {
title: title,
target: data.target,
real: data.real,
completeRate: data.completeRate,
diff: data.diff
};
});
return _this.indicatorDefs.map(def => {
const data = list.find(item => item && item.title === def.name) || fallback
const detailData = {
...data,
flag: _this.getRateFlag((data || _this.defaultData).completeRate, (data || _this.defaultData).real, (data || _this.defaultData).target),
}
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: {
relatedTotal: {
handler(newValue) {