feat: 新增多模块业务功能与页面

1. 新增轧辊产线共用mixins工具
2. 新增钢卷异常管理相关API、组件与页面
3. 新增成本管理相关模块API与CRUD页面
4. 新增多选组件MutiSelect
This commit is contained in:
2026-06-09 13:16:32 +08:00
parent 609a6e91ef
commit 78e2c3023b
24 changed files with 5067 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
import request from '@/utils/request'
const BASE = '/wms/materialCoil'
export function getCoilHoardingStats(data) {
return request({
url: BASE + '/hoardingStatistics',
method: 'post',
data: data
})
}
export function listCoilHoardingDetail(query) {
return request({
url: BASE + '/listWithQrcode',
method: 'get',
params: query
})
}

View File

@@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询成本项目配置列表
export function listItem(query) {
return request({
url: '/cost/item/list',
method: 'get',
params: query
})
}
// 查询成本项目配置详细
export function getItem(itemId) {
return request({
url: '/cost/item/' + itemId,
method: 'get'
})
}
// 新增成本项目配置
export function addItem(data) {
return request({
url: '/cost/item',
method: 'post',
data: data
})
}
// 修改成本项目配置
export function updateItem(data) {
return request({
url: '/cost/item',
method: 'put',
data: data
})
}
// 删除成本项目配置
export function delItem(itemId) {
return request({
url: '/cost/item/' + itemId,
method: 'delete'
})
}

View File

@@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询成本单价历史列表
export function listPrice(query) {
return request({
url: '/cost/price/list',
method: 'get',
params: query
})
}
// 查询成本单价历史详细
export function getPrice(priceId) {
return request({
url: '/cost/price/' + priceId,
method: 'get'
})
}
// 新增成本单价历史
export function addPrice(data) {
return request({
url: '/cost/price',
method: 'post',
data: data
})
}
// 修改成本单价历史
export function updatePrice(data) {
return request({
url: '/cost/price',
method: 'put',
data: data
})
}
// 删除成本单价历史
export function delPrice(priceId) {
return request({
url: '/cost/price/' + priceId,
method: 'delete'
})
}

View File

@@ -0,0 +1,53 @@
import request from '@/utils/request'
// 查询生产成本明细列表
export function listProdDetail(query) {
return request({
url: '/cost/prodDetail/list',
method: 'get',
params: query
})
}
// 查询生产成本明细详细
export function getProdDetail(detailId) {
return request({
url: '/cost/prodDetail/' + detailId,
method: 'get'
})
}
// 新增生产成本明细
export function addProdDetail(data) {
return request({
url: '/cost/prodDetail',
method: 'post',
data: data
})
}
// 修改生产成本明细
export function updateProdDetail(data) {
return request({
url: '/cost/prodDetail',
method: 'put',
data: data
})
}
// 删除生产成本明细
export function delProdDetail(detailId) {
return request({
url: '/cost/prodDetail/' + detailId,
method: 'delete'
})
}
// 批量保存生产成本明细(先删除再插入)
export function batchSaveProdDetail(data) {
return request({
url: '/cost/prodDetail/batch',
method: 'post',
data: data
})
}

View File

@@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询生产指标明细列表
export function listProdMetric(query) {
return request({
url: '/cost/prodMetric/list',
method: 'get',
params: query
})
}
// 查询生产指标明细详细
export function getProdMetric(metricId) {
return request({
url: '/cost/prodMetric/' + metricId,
method: 'get'
})
}
// 新增生产指标明细
export function addProdMetric(data) {
return request({
url: '/cost/prodMetric',
method: 'post',
data: data
})
}
// 修改生产指标明细
export function updateProdMetric(data) {
return request({
url: '/cost/prodMetric',
method: 'put',
data: data
})
}
// 删除生产指标明细
export function delProdMetric(metricId) {
return request({
url: '/cost/prodMetric/' + metricId,
method: 'delete'
})
}

View File

@@ -0,0 +1,53 @@
import request from '@/utils/request'
// 查询生产月报列表
export function listProdReport(query) {
return request({
url: '/cost/prodReport/list',
method: 'get',
params: query
})
}
// 查询生产月报详细
export function getProdReport(reportId) {
return request({
url: '/cost/prodReport/' + reportId,
method: 'get'
})
}
// 新增生产月报
export function addProdReport(data) {
return request({
url: '/cost/prodReport',
method: 'post',
data: data
})
}
// 修改生产月报
export function updateProdReport(data) {
return request({
url: '/cost/prodReport',
method: 'put',
data: data
})
}
// 删除生产月报
export function delProdReport(reportId) {
return request({
url: '/cost/prodReport/' + reportId,
method: 'delete'
})
}
// 复制生产月报
export function copyProdReport(sourceId, data) {
return request({
url: '/cost/prodReport/copy/' + sourceId,
method: 'post',
data: data
})
}

View File

@@ -0,0 +1,53 @@
import request from '@/utils/request'
// 查询钢卷异常信息列表
export function listCoilAbnormal(query) {
return request({
url: '/mill/abnormal/list',
method: 'get',
params: query
})
}
// 查询钢卷异常信息详细
export function getCoilAbnormal(abnormalId) {
return request({
url: '/mill/abnormal/' + abnormalId,
method: 'get'
})
}
// 新增钢卷异常信息
export function addCoilAbnormal(data) {
return request({
url: '/mill/abnormal',
method: 'post',
data: data
})
}
// 修改钢卷异常信息
export function updateCoilAbnormal(data) {
return request({
url: '/mill/abnormal',
method: 'put',
data: data
})
}
// 删除钢卷异常信息
export function delCoilAbnormal(abnormalId) {
return request({
url: '/mill/abnormal/' + abnormalId,
method: 'delete'
})
}
// 异常判级
export function judgeAbnormalLevel(params) {
return request({
url: '/mill/abnormal/judge',
method: 'put',
params
})
}

View File

@@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询二级-三级钢卷异常挂接/撤回关系列表
export function listRelation(query) {
return request({
url: '/mill/relation/list',
method: 'get',
params: query
})
}
// 查询二级-三级钢卷异常挂接/撤回关系详细
export function getRelation(relationId) {
return request({
url: '/mill/relation/' + relationId,
method: 'get'
})
}
// 新增二级-三级钢卷异常挂接/撤回关系
export function addRelation(data) {
return request({
url: '/mill/relation',
method: 'post',
data: data
})
}
// 修改二级-三级钢卷异常挂接/撤回关系
export function updateRelation(data) {
return request({
url: '/mill/relation',
method: 'put',
data: data
})
}
// 删除二级-三级钢卷异常挂接/撤回关系
export function delRelation(relationId) {
return request({
url: '/mill/relation/' + relationId,
method: 'delete'
})
}

View File

@@ -0,0 +1,62 @@
import request from '@/utils/request'
// 查询换辊记录分页列表(支持按产线、机架、类型、时间筛选)
export function listRollChange(query) {
return request({
url: '/mes/rollChange/list',
method: 'get',
params: query
})
}
// 查询指定产线+机架当前在机轧辊(最近一次换辊记录)
export function getCurrentRolls(lineId, standNo) {
return request({
url: '/mes/rollChange/current',
method: 'get',
params: { lineId, standNo }
})
}
// 查询指定产线各机架各辊位实时工作绩效workLength/coilCount/totalWeight
export function getRollPerformance(lineId) {
return request({
url: '/mes/rollChange/performance',
method: 'get',
params: { lineId }
})
}
// 查询换辊记录详细
export function getRollChange(changeId) {
return request({
url: '/mes/rollChange/' + changeId,
method: 'get'
})
}
// 新增换辊记录(自动同步辊状态为 Online
export function addRollChange(data) {
return request({
url: '/mes/rollChange',
method: 'post',
data: data
})
}
// 修改换辊记录
export function updateRollChange(data) {
return request({
url: '/mes/rollChange',
method: 'put',
data: data
})
}
// 删除换辊记录
export function delRollChange(changeIds) {
return request({
url: '/mes/rollChange/' + changeIds,
method: 'delete'
})
}

View File

@@ -0,0 +1,53 @@
import request from '@/utils/request'
// 查询指定产线+机架下批轧辊列表
export function listRollStandby(lineId, standNo) {
return request({
url: '/mes/rollStandby/list',
method: 'get',
params: { lineId, standNo }
})
}
// 查询下批轧辊详细
export function getRollStandby(standbyId) {
return request({
url: '/mes/rollStandby/' + standbyId,
method: 'get'
})
}
// 新增下批轧辊(自动同步辊状态为 Standby
export function addRollStandby(data) {
return request({
url: '/mes/rollStandby',
method: 'post',
data: data
})
}
// 修改下批轧辊
export function updateRollStandby(data) {
return request({
url: '/mes/rollStandby',
method: 'put',
data: data
})
}
// 删除单条下批轧辊(自动恢复辊状态为 Offline
export function delRollStandby(standbyId) {
return request({
url: '/mes/rollStandby/' + standbyId,
method: 'delete'
})
}
// 清空指定产线+机架全部下批轧辊
export function clearRollStandby(lineId, standNo) {
return request({
url: '/mes/rollStandby/clear',
method: 'delete',
params: { lineId, standNo }
})
}

View File

@@ -0,0 +1,168 @@
<template>
<div class="muti-select">
<!-- 下拉选择模式 -->
<div v-if="type === 'select'" class="select-container">
<el-select
v-model="innerValue"
multiple
:placeholder="placeholder"
:filterable="filterable"
:clearable="clearable"
:allow-create="allowAdd"
:disabled="disabled"
:size="size"
:collapse-tags="collapseTags"
@change="handleChange"
>
<!-- 全选选项 -->
<!-- <el-option
v-if="showSelectAll && options.length > 0"
key="selectAll"
label="全选"
value="selectAll"
@click="toggleSelectAll"
/> -->
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<!-- 复选框模式 -->
<div v-else-if="type === 'checkbox'" class="checkbox-group">
<el-checkbox-group v-model="innerValue" @change="handleCheckboxChange">
<el-checkbox
v-for="item in options"
:key="item.value"
:label="item.value"
:disabled="disabled"
>
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
<el-button
v-if="showSelectAll && options.length > 0"
type="text"
size="small"
@click="toggleSelectAll"
>
{{ isAllSelected ? '取消全选' : '全选' }}
</el-button>
</div>
</div>
</template>
<!-- v-model 绑定逗号分隔的字符串可以 props 配置是否允许新增 -->
<script>
export default {
name: 'MutiSelect',
props: {
value: {
type: String,
default: ''
},
options: {
type: Array,
default: () => []
},
allowAdd: {
type: Boolean,
default: false
},
type: {
type: String,
default: 'select' // select or checkbox
},
placeholder: {
type: String,
default: '请选择'
},
filterable: {
type: Boolean,
default: true
},
clearable: {
type: Boolean,
default: true
},
disabled: {
type: Boolean,
default: false
},
size: {
type: String,
default: 'default' // large, default, small
},
showSelectAll: {
type: Boolean,
default: true
},
collapseTags: {
type: Boolean,
default: true
}
},
// 计算属性捕获实现双向绑定
computed: {
innerValue: {
get() {
if (!this.value) {
return [];
}
return this.value?.split(',');
},
set(val) {
const newValue = val?.join(',') ?? '';
this.$emit('input', newValue);
this.$emit('change', newValue);
}
},
// 是否全选
isAllSelected() {
return this.options.length > 0 && this.innerValue.length === this.options.length;
}
},
methods: {
// 处理选择变化
handleChange(val) {
// 过滤掉 'selectAll' 选项
const filteredVal = val.filter(item => item !== 'selectAll');
this.$emit('change', filteredVal.join(','));
},
// 处理复选框变化
handleCheckboxChange(val) {
const newValue = val.join(',');
this.$emit('input', newValue);
this.$emit('change', newValue);
},
// 切换全选/取消全选
toggleSelectAll() {
if (this.isAllSelected) {
this.innerValue = [];
} else {
this.innerValue = this.options.map(item => item.value);
}
}
}
}
</script>
<style scoped>
.muti-select {
width: 100%;
}
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.checkbox-group .el-checkbox {
margin-right: 10px;
}
</style>

View File

@@ -0,0 +1,749 @@
<template>
<div class="app-container">
<div class="report-tab-bar">
<div class="report-tabs">
<span v-for="r in tabs" :key="r.reportId"
:class="['report-tab', { active: activeReport && activeReport.reportId === r.reportId }]"
@click="enter(r)">{{ r.reportTitle }}</span>
</div>
<el-button size="mini" icon="el-icon-setting" @click="openConfig">配置</el-button>
</div>
<div v-if="!activeReport" class="empty-hint">请在上方选择报表或点击"配置"管理报表</div>
<template v-if="activeReport">
<el-card class="mb8">
<div slot="header" class="entry-header">
<span class="entry-title">{{ activeReport.reportTitle }}</span>
<span class="entry-meta">{{ parseTime(activeReport.reportDate,'{y}-{m}-{d}') }} 投入{{ activeReport.inputWeight }}t 产出{{ activeReport.outputWeight }}t</span>
<el-button type="primary" size="mini" style="float:right;margin-left:8px" @click="saveGrid" :loading="saving">保存</el-button>
<el-button size="mini" style="float:right;margin-left:8px" @click="openColCfg">列配置</el-button>
<span style="float:right;margin-right:12px;font-size:12px;color:#606266;display:flex;align-items:center">
<span style="margin-right:4px">{{ inputMode ? '录入' : '查看' }}</span>
<el-switch v-model="inputMode" size="small" />
</span>
</div>
<el-alert :title="'已配置'+allCols.length+'个列'" type="info" :closable="false" show-icon style="margin-bottom:8px" />
<el-table v-loading="gridLoading" :data="gridRows" border stripe size="mini" style="width:100%" :header-cell-style="headerStyle" :key="'tbl-'+inputMode">
<el-table-column label="日期" width="135" fixed>
<template slot-scope="s"><el-date-picker v-model="s.row.detailDate" type="date" value-format="yyyy-MM-dd" size="mini" style="width:124px" @change="sortGrid" /></template>
</el-table-column>
<template v-for="col in displayCols">
<el-table-column v-if="col.$type==='detail' && !col.isShift" :key="'d'+col.itemId" :label="col.itemName+(col.unit?'('+col.unit+')':'')" width="105" align="center">
<template slot-scope="s">
<el-input v-model="s.row['q'+col.itemId]" size="mini" @input="recalcAll">
<i slot="suffix" v-if="col.queryCondition" :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" style="cursor:pointer;font-size:13px;line-height:24px;color:#409eff" @click.stop="fetchAutoData(col, s.row)" />
</el-input>
</template>
</el-table-column>
<el-table-column v-else-if="col.$type==='detail' && col.isShift" :key="'ds'+col.itemId" :label="col.itemName+(col.unit?'('+col.unit+')':'')" width="120" align="center">
<template slot-scope="s">
<div class="shift-cell"><span class="shift-tag"></span><el-input v-model="s.row['q'+col.itemId+'_1']" size="mini" class="shift-input" @input="recalcAll"><i slot="suffix" v-if="col.queryCondition" :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" style="cursor:pointer;font-size:12px;line-height:24px;color:#409eff" @click.stop="fetchAutoData(col, s.row, '1')" /></el-input></div>
<div class="shift-cell"><span class="shift-tag"></span><el-input v-model="s.row['q'+col.itemId+'_2']" size="mini" class="shift-input" @input="recalcAll"><i slot="suffix" v-if="col.queryCondition" :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" style="cursor:pointer;font-size:12px;line-height:24px;color:#409eff" @click.stop="fetchAutoData(col, s.row, '2')" /></el-input></div>
</template>
</el-table-column>
<el-table-column v-else-if="col.$type==='metric' && !col.isShift" :key="'m'+col.mIdx" :label="col.metricName+(col.unit?'('+col.unit+')':'')" width="85" align="center">
<template slot-scope="s">{{ s.row['mv'+col.mIdx]!=null ? s.row['mv'+col.mIdx] : '-' }}</template>
</el-table-column>
<el-table-column v-else-if="col.$type==='metric' && col.isShift" :key="'ms'+col.mIdx" :label="col.metricName+(col.unit?'('+col.unit+')':'')" width="95" align="center">
<template slot-scope="s">
<div class="shift-metric"> {{ s.row['mv'+col.mIdx+'_1']!=null ? s.row['mv'+col.mIdx+'_1'] : '-' }}</div>
<div class="shift-metric"> {{ s.row['mv'+col.mIdx+'_2']!=null ? s.row['mv'+col.mIdx+'_2'] : '-' }}</div>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="55" fixed="right" align="center">
<template slot-scope="s"><el-button size="mini" type="text" style="padding:0" @click="gridRows.splice(s.$index,1)">删除</el-button></template>
</el-table-column>
</el-table>
<div style="margin-top:8px;text-align:center"><el-button type="default" icon="el-icon-plus" size="mini" @click="gridRows.push({detailDate:''})">添加日期行</el-button></div>
</el-card>
<!-- Column config -->
<el-dialog title="列配置" :visible.sync="colOpen" width="880px" append-to-body top="5vh">
<div style="margin-bottom:8px;display:flex;justify-content:space-between;align-items:center">
<span style="color:#999;font-size:12px"> {{ allCols.length }} </span>
<div>
<el-button size="mini" type="danger" :disabled="!selCol.length" @click="batchDelCol">删除</el-button>
<el-button type="primary" size="mini" @click="showAddDetail=true">+ 明细列</el-button>
<el-button size="mini" type="success" @click="openMetricPicker">+ 指标列</el-button>
<el-button size="mini" plain @click="openMetricMgr">指标管理</el-button>
</div>
</div>
<el-table :data="allCols" border stripe size="mini" highlight-current-row @dragover.native.prevent @drop.native="onNativeDrop" @current-change="curIdx = allCols.indexOf($event)" @selection-change="selCol=$event">
<el-table-column type="selection" width="40" />
<el-table-column label="序号" type="index" width="50" align="center" />
<el-table-column label="类型" width="60" align="center">
<template slot-scope="s">
<el-tag v-if="s.row.$type==='detail'" type="primary" size="mini">明细</el-tag>
<el-tag v-else type="success" size="mini">指标</el-tag>
</template>
</el-table-column>
<el-table-column label="名称" width="140">
<template slot-scope="s">{{ s.row.$type==='detail' ? (s.row.itemName||s.row.itemCode) : (s.row.metricName||'(空)') }}</template>
</el-table-column>
<el-table-column label="公式" min-width="180">
<template slot-scope="s"><span v-if="s.row.$type==='metric'" style="font-size:11px;color:#666">{{ s.row.metricFormula }}</span></template>
</el-table-column>
<el-table-column label="单位" width="55" align="center">
<template slot-scope="s">{{ s.row.unit }}</template>
</el-table-column>
<el-table-column label="表头色" width="70" align="center">
<template slot-scope="s"><el-color-picker v-model="s.row.color" size="mini" :predefine="['#409eff','#67c23a','#e6a23c','#f56c6c','#909399']" /></template>
</el-table-column>
<el-table-column label="分班次" width="70" align="center">
<template slot-scope="s"><el-checkbox v-model="s.row.isShift" /></template>
</el-table-column>
<el-table-column label="操作" width="175" align="center">
<template slot-scope="s">
<span style="display:inline-flex;align-items:center">
<span class="drag-handle" title="拖拽排序"
:draggable="true"
@dragstart.stop="onDragStart(s.$index)"
@dragend="onDragEnd">
<i class="el-icon-rank" />
</span>
<el-button size="mini" type="text" icon="el-icon-d-arrow-left" :disabled="s.$index===0" title="置于最上" @click="moveColToEdge(s.$index,'top')" />
<el-button size="mini" type="text" icon="el-icon-top" :disabled="s.$index===0" @click="moveCol(s.$index,-1)" />
<el-button size="mini" type="text" icon="el-icon-bottom" :disabled="s.$index===allCols.length-1" @click="moveCol(s.$index,1)" />
<el-button size="mini" type="text" icon="el-icon-d-arrow-right" :disabled="s.$index===allCols.length-1" title="置于最下" @click="moveColToEdge(s.$index,'bottom')" />
<el-button size="mini" type="text" icon="el-icon-delete" @click="allCols.splice(s.$index,1)" />
</span>
</template>
</el-table-column>
</el-table>
<div slot="footer"><el-button type="primary" @click="saveColCfg" :loading="colSaving">保存配置</el-button><el-button @click="colOpen=false">取消</el-button></div>
</el-dialog>
<!-- Add detail -->
<el-dialog title="添加明细列" :visible.sync="showAddDetail" width="550px" append-to-body>
<el-table :data="availableItems" border stripe size="mini" @selection-change="selAdd=$event">
<el-table-column type="selection" width="45" />
<el-table-column label="编码" prop="itemCode" width="120" />
<el-table-column label="名称" prop="itemName" />
<el-table-column label="单位" prop="unit" width="70" />
</el-table>
<div slot="footer">
<el-button type="primary" :disabled="!selAdd.length" @click="batchAddDetailCols"> </el-button>
<el-button @click="showAddDetail=false"> </el-button>
</div>
</el-dialog>
<!-- Pick metric -->
<el-dialog title="选择指标列" :visible.sync="metricPickOpen" width="600px" append-to-body>
<el-table :data="metricPickList" border stripe size="mini" @selection-change="selMp=$event">
<el-table-column type="selection" width="45" />
<el-table-column label="名称" prop="metricName" width="150" />
<el-table-column label="公式" prop="metricFormula" min-width="200" />
<el-table-column label="单位" prop="unit" width="70" />
</el-table>
<div slot="footer">
<el-button type="primary" :disabled="!selMp.length" @click="doPickMetric"> </el-button>
<el-button @click="metricPickOpen=false"> </el-button>
</div>
</el-dialog>
<!-- Metric management -->
<el-dialog title="指标管理" :visible.sync="mgrOpen" width="750px" top="5vh" append-to-body>
<div style="margin-bottom:8px">
<el-button type="primary" size="mini" @click="addMetricDef">新增指标</el-button>
</div>
<el-table :data="mgrList" border stripe size="mini">
<el-table-column label="名称" prop="metricName" width="150" />
<el-table-column label="公式" prop="metricFormula" min-width="200" />
<el-table-column label="单位" prop="remark" width="70" />
<el-table-column label="操作" width="100">
<template slot-scope="s">
<el-button size="mini" type="text" @click="editMetricDef(s.row)">编辑</el-button>
<el-button size="mini" type="text" style="color:#f56c6c" @click="delMetricDef(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog :title="defTitle" :visible.sync="defOpen" width="480px" append-to-body>
<el-form :model="defForm" label-width="80px" size="small">
<el-form-item label="指标名称"><el-input v-model="defForm.metricName" /></el-form-item>
<el-form-item label="公式"><el-input v-model="defForm.metricFormula" type="textarea" :rows="3" /></el-form-item>
<div class="formula-vars" v-if="allCols.length">
<span class="vars-label">变量</span>
<template v-for="c in allCols.filter(x=>x.$type==='detail')">
<el-tag :key="'v'+c.itemId" size="mini" class="vars-tag" @click="defForm.metricFormula=(defForm.metricFormula||'')+'@{'+c.itemCode+'}'">{{ c.itemCode }}</el-tag>
</template>
<el-tag size="mini" class="vars-tag var-sys" @click="defForm.metricFormula=(defForm.metricFormula||'')+'input_weight'">input_weight</el-tag>
<el-tag size="mini" class="vars-tag var-sys" @click="defForm.metricFormula=(defForm.metricFormula||'')+'output_weight'">output_weight</el-tag>
<template v-for="c in allCols.filter(x=>x.$type==='metric'&&x.metricName)">
<el-tag :key="'v'+c.metricName" size="mini" class="vars-tag var-mtr" @click="defForm.metricFormula=(defForm.metricFormula||'')+'@{'+c.metricName+'}'">{{ c.metricName }}</el-tag>
</template>
</div>
<el-form-item label="单位"><el-input v-model="defForm.unit" placeholder="如 %" /></el-form-item>
</el-form>
<div slot="footer">
<el-button type="primary" @click="submitMetricDef"> </el-button>
<el-button @click="defOpen=false"> </el-button>
</div>
</el-dialog>
</el-dialog>
<!-- Copy config -->
<el-dialog title="从已有报表复制配置" :visible.sync="copyCfgOpen" width="550px" append-to-body>
<el-table :data="copyReports" border stripe size="mini" highlight-current-row @current-change="copySrc=$event">
<el-table-column label="报表标题" prop="reportTitle" />
<el-table-column label="日期" width="110"><template slot-scope="s">{{ parseTime(s.row.reportDate,'{y}-{m}-{d}') }}</template></el-table-column>
</el-table>
<div slot="footer">
<el-button type="primary" :disabled="!copySrc" @click="doCopyCfg">确认复制</el-button>
<el-button @click="copyCfgOpen=false"> </el-button>
</div>
</el-dialog>
</template>
<!-- Report management -->
<el-dialog title="报表管理" :visible.sync="configOpen" width="900px" top="5vh" append-to-body>
<el-form :model="q" ref="qf" size="small" :inline="true" v-show="showSearch" label-width="80px">
<el-form-item label="报表标题" prop="reportTitle"><el-input v-model="q.reportTitle" placeholder="请输入" clearable @keyup.enter.native="search" /></el-form-item>
<el-form-item label="报表日期" prop="reportDate"><el-date-picker v-model="q.reportDate" type="date" value-format="yyyy-MM-dd" placeholder="选择日期" clearable /></el-form-item>
<el-form-item><el-button type="primary" icon="el-icon-search" size="mini" @click="search">搜索</el-button><el-button icon="el-icon-refresh" size="mini" @click="resetQ">重置</el-button></el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5"><el-button type="primary" plain icon="el-icon-plus" size="mini" @click="addRp">新增</el-button></el-col>
<el-col :span="1.5"><el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="sel!==1" @click="editRp">修改</el-button></el-col>
<el-col :span="1.5"><el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="sel<1" @click="delRp">删除</el-button></el-col>
<el-col :span="1.5"><el-button type="warning" plain icon="el-icon-document-copy" size="mini" :disabled="sel!==1" @click="openCopyRp">复制</el-button></el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
</el-row>
<el-table v-loading="loading" :data="list" @selection-change="s=>{sel=s.length;selIds=s.map(r=>r.reportId)}" ref="rt">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="报表标题" prop="reportTitle" />
<el-table-column label="日期" width="120"><template slot-scope="s">{{ parseTime(s.row.reportDate,'{y}-{m}-{d}') }}</template></el-table-column>
<el-table-column label="操作" width="180" align="center">
<template slot-scope="s">
<el-button size="mini" type="text" style="padding:0" @click="editRp(s.row)">修改</el-button>
<el-button size="mini" type="text" style="padding:0" @click="copyInlineRp(s.row)">复制</el-button>
<el-button size="mini" type="text" style="padding:0;color:#f56c6c" @click="delRp(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total>0" :total="total" :page.sync="q.pageNum" :limit.sync="q.pageSize" @pagination="getList" />
<el-dialog :title="rpTitle" :visible.sync="rpOpen" width="500px" append-to-body>
<el-form ref="rpf" :model="rpForm" :rules="{reportTitle:[{required:true,message:'请输入',trigger:'blur'}]}" label-width="100px">
<el-form-item label="报表标题" prop="reportTitle"><el-input v-model="rpForm.reportTitle" placeholder="请输入报表标题" /></el-form-item>
<el-form-item label="报表日期" prop="reportDate"><el-date-picker v-model="rpForm.reportDate" type="date" placeholder="选择日期" clearable value-format="yyyy-MM-dd" style="width:100%" /></el-form-item>
<el-form-item label="备注" prop="remark"><el-input v-model="rpForm.remark" type="textarea" placeholder="请输入备注" /></el-form-item>
</el-form>
<div slot="footer"><el-button :loading="rpBtnLoading" type="primary" @click="submitRp"> </el-button><el-button @click="rpOpen=false"> </el-button></div>
</el-dialog>
<!-- 复制报表弹窗 -->
<el-dialog title="复制报表" :visible.sync="copyRpOpen" width="500px" append-to-body>
<el-form :model="copyRpForm" label-width="100px" size="small">
<el-form-item label="报表标题"><el-input v-model="copyRpForm.reportTitle" placeholder="请输入报表标题" /></el-form-item>
<el-form-item label="报表日期"><el-date-picker v-model="copyRpForm.reportDate" type="date" placeholder="选择日期" clearable value-format="yyyy-MM-dd" style="width:100%" /></el-form-item>
</el-form>
<div slot="footer">
<el-button type="primary" @click="doCopyRp"> </el-button>
<el-button @click="copyRpOpen=false"> </el-button>
</div>
</el-dialog>
</el-dialog>
</div>
</div>
</template>
<script>
import { listProdReport, getProdReport, addProdReport, updateProdReport, delProdReport, copyProdReport } from "@/api/cost/prodReport"
import { listProdDetail, batchSaveProdDetail } from "@/api/cost/prodDetail"
import { listProdMetric, addProdMetric, updateProdMetric, delProdMetric, getProdMetric } from "@/api/cost/prodMetric"
import { listItem } from "@/api/cost/item"
function parseDateRange(detailDate) {
const d = (detailDate || '').slice(0, 10)
return {
startTime: d + ' 00:00:00',
endTime: d + ' 23:59:59'
}
}
/**
* 自动查询处理器注册表。
* key: item.category, value: async (queryCondition, row, col, report, shift) => fetchedValue
*/
const queryHandlers = {}
export function registerQueryHandler(category, handler) {
queryHandlers[category] = handler
}
const teamMap = { '1': '甲', '2': '乙' }
registerQueryHandler('原料', async (queryCondition, row, col, report, shift) => {
if (!row.detailDate) return null
const { startTime, endTime } = parseDateRange(row.detailDate)
const res = await listLightPendingAction({ actionStatus: 2, actionTypes: queryCondition, startTime, endTime, pageSize: 99999 })
const items = Array.isArray(res.data) ? res.data : (res.rows || [])
const ids = [...new Set(items.map(i => i.coilId).filter(Boolean))]
if (!ids.length) return null
const params = { coilIds: ids.join(',') }
if (shift && teamMap[shift]) params.team = teamMap[shift]
const stat = await getCoilStatisticsList(params)
const net = stat.data && stat.data.total_net_weight
return net != null ? net : null
})
registerQueryHandler('产出', async (queryCondition, row, col, report, shift) => {
if (!row.detailDate) return null
const { startTime, endTime } = parseDateRange(row.detailDate)
const res = await listLightPendingAction({ actionStatus: 2, actionTypes: queryCondition, startTime, endTime, pageSize: 99999 })
const items = Array.isArray(res.data) ? res.data : (res.rows || [])
const ids = []
for (const i of items) {
if (i.processedCoilIds) {
i.processedCoilIds.split(',').forEach(id => { id = id.trim(); if (id) ids.push(id) })
}
}
if (!ids.length) return null
const params = { coilIds: [...new Set(ids)].join(',') }
if (shift && teamMap[shift]) params.team = teamMap[shift]
const stat = await getCoilStatisticsList(params)
const net = stat.data && stat.data.total_net_weight
return net != null ? net : null
})
registerQueryHandler('辅料', async (queryCondition, row, col, report, shift) => {
if (!row.detailDate) return null
const d = (row.detailDate || '').slice(0, 10)
const res = await listAuxiliaryConsume({ recordDate: d, typeId: queryCondition, pageSize: 9999 })
const items = res.rows || []
const total = items.reduce((s, item) => s + (parseFloat(item.consume) || 0), 0)
if (col.isShift) {
const half = total / 2
return [half, half]
}
return total || null
})
registerQueryHandler('轧辊', async (queryCondition, row, col, report, shift) => {
if (!row.detailDate) return null
const d = (row.detailDate || '').slice(0, 10)
const params = { beginTime: d + ' 00:00:00', endTime: d + ' 23:59:59', rollType: queryCondition }
if (shift && teamMap[shift]) params.team = teamMap[shift] + '班'
const res = await listRollGrindAll(params)
const items = res.data || []
if (!items.length) return null
let total = 0
items.forEach(item => { total += parseFloat(item.grindAmount) || 0 })
return total || null
})
export default {
name: "CostComprehensive",
data() {
return {
loading: false, list: [], tabs: [], total: 0, sel: 0, selIds: [], showSearch: true,
q: { pageNum: 1, pageSize: 10, reportTitle: undefined, reportDate: undefined },
rpOpen: false, rpTitle: "", rpBtnLoading: false, rpForm: {},
copyRpOpen: false, copyRpForm: {},
activeReport: null, gridLoading: false, gridRows: [], saving: false,
allItems: [], allCols: [],
colOpen: false, colSaving: false,
showAddDetail: false, selAdd: [], selCol: [], curIdx: -1, dragIdx: -1,
metricPickOpen: false, metricPickList: [], selMp: [],
mgrOpen: false, mgrList: [], defOpen: false, defTitle: '', defForm: {},
copyCfgOpen: false, copyReports: [], copySrc: null,
configOpen: false,
autoLoading: {},
inputMode: false
}
},
computed: {
availableItems() {
const used = new Set(this.allCols.filter(c => c.$type === 'detail').map(c => String(c.itemId)))
return this.allItems.filter(i => !used.has(String(i.itemId)))
},
displayCols() {
if (!this.inputMode) return this.allCols
return [...this.allCols.filter(c => c.$type === 'detail'), ...this.allCols.filter(c => c.$type === 'metric')]
},
headerStyle() {
return ({ column }) => {
if (!column || !column.label) return {}
const col = this.allCols.find(c => {
if (c.$type === 'detail') return column.label.startsWith(c.itemName || c.itemCode)
if (c.$type === 'metric') return column.label.startsWith(c.metricName)
return false
})
return col && col.color ? { background: col.color, color: '#fff' } : {}
}
}
},
watch: { configOpen(v) { if (!v) this.rpOpen = false } },
created() {
this.getTabList()
this.loadItems()
},
methods: {
/* report */
getList() {
this.loading = true
const params = { ...this.q }
listProdReport(params).then(r=>{this.list=r.rows;this.total=r.total}).finally(()=>this.loading=false)
},
getTabList() {
const params = { pageNum: 1, pageSize: 9999 }
listProdReport(params).then(r => { this.tabs = r.rows || [] })
},
search() { this.q.pageNum = 1; this.getList() },
resetQ() { this.resetForm("qf"); this.search() },
addRp() { this.rpForm = {}; this.rpTitle = "新增"; this.rpOpen = true },
editRp(row) {
const id = (row&&row.reportId)||this.selIds[0]; if(!id)return
getProdReport(id).then(r=>{
const d = r.data || {}
this.rpForm = d; this.rpTitle = "修改"; this.rpOpen = true
})
},
submitRp() {
this.$refs.rpf.validate(v=>{if(!v)return;this.rpBtnLoading=true;const fn=this.rpForm.reportId?updateProdReport:addProdReport;fn(this.rpForm).then(()=>{
this.$modal.msgSuccess("成功"); this.rpOpen = false
if (this.activeReport && this.activeReport.reportId === this.rpForm.reportId) {
Object.assign(this.activeReport, { reportTitle: this.rpForm.reportTitle, reportDate: this.rpForm.reportDate, inputWeight: this.rpForm.inputWeight, outputWeight: this.rpForm.outputWeight })
}
this.getTabList(); this.getList()
}).finally(()=>this.rpBtnLoading=false)})
},
delRp(row) {
const ids=(row&&row.reportId)?[row.reportId]:this.selIds;if(!ids.length)return
this.$modal.confirm("确认删除?").then(()=>delProdReport(ids.join(','))).then(()=>{
if (this.activeReport && ids.includes(this.activeReport.reportId)) { this.activeReport = null; this.gridRows = []; this.allCols = [] }
this.getTabList(); this.getList(); this.$modal.msgSuccess("已删除")
})
},
openConfig() { this.configOpen = true; this.getList() },
openCopyRp() {
const row = this.list.find(r => r.reportId === this.selIds[0])
this.copyRpForm = { reportId: this.selIds[0], reportTitle: (row ? row.reportTitle : '') + '-副本', reportDate: row ? row.reportDate : undefined }
this.copyRpOpen = true
},
copyInlineRp(row) {
this.copyRpForm = { reportId: row.reportId, reportTitle: row.reportTitle + '-副本', reportDate: row.reportDate }
this.copyRpOpen = true
},
async doCopyRp() {
const sid = this.copyRpForm.reportId
if (!sid) return
await copyProdReport(sid, { reportTitle: this.copyRpForm.reportTitle, reportDate: this.copyRpForm.reportDate })
this.copyRpOpen = false
this.$modal.msgSuccess('复制成功')
this.getTabList(); this.getList()
},
/* column config */
async openColCfg() { await this.loadItems(); await this.restoreAllCols(); this.colOpen = true },
moveCol(idx, dir) { const arr = this.allCols; const t = idx + dir; if (t >= 0 && t < arr.length) { const item = arr.splice(idx, 1)[0]; arr.splice(t, 0, item) } },
moveColToEdge(idx, edge) { const arr = this.allCols; const item = arr.splice(idx, 1)[0]; edge === 'top' ? arr.unshift(item) : arr.push(item) },
onDragStart(idx) {
this.dragIdx = idx
},
onDragEnd() {
this.dragIdx = -1
},
onNativeDrop(e) {
if (this.dragIdx < 0) return
const tr = e.target.closest('tr')
if (!tr) return
const rows = Array.from(tr.parentElement.querySelectorAll('tr'))
const targetIdx = rows.indexOf(tr)
if (targetIdx >= 0) this.onDragDrop(targetIdx)
},
onDragDrop(targetIdx) {
if (this.dragIdx < 0 || targetIdx < 0 || this.dragIdx === targetIdx) return
const arr = this.allCols
const item = arr.splice(this.dragIdx, 1)[0]
const insertAt = this.dragIdx < targetIdx ? targetIdx - 1 : targetIdx
arr.splice(insertAt, 0, item)
this.dragIdx = -1
this.$forceUpdate()
},
batchAddDetailCols() {
this.selAdd.forEach(item => {
if (!this.allCols.find(c => c.$type === 'detail' && String(c.itemId) === String(item.itemId)))
this.allCols.push({ $type: 'detail', itemId: item.itemId, itemCode: item.itemCode, itemName: item.itemName, unit: item.unit, isShift: false, color: null, queryCondition: item.queryCondition, category: item.category })
})
this.showAddDetail = false; this.selAdd = []
},
batchDelCol() {
const set = new Set(this.selCol.map(r => r))
this.allCols = this.allCols.filter(c => !set.has(c))
this.selCol = []; this.curIdx = -1
},
/* metric picker */
async openMetricPicker() {
await this.loadAllMetrics(this.activeReport.reportId)
const used = new Set(this.allCols.filter(c => c.$type === 'metric' && c.metricId).map(c => String(c.metricId)))
this.metricPickList = (this._allMetricDefs || []).filter(m => !used.has(String(m.metricId)))
this.selMp = []; this.metricPickOpen = true
},
doPickMetric() {
this.selMp.forEach(m => {
this.allCols.push({ $type: 'metric', metricId: m.metricId, metricName: m.metricName, metricFormula: m.metricFormula, unit: m.remark||'', isShift: false, color: null })
})
this.metricPickOpen = false
},
/* metric management */
async openMetricMgr() {
await this.loadAllMetrics(this.activeReport.reportId)
this.mgrList = this._allMetricDefs || []
this.mgrOpen = true
},
addMetricDef() { this.defForm = { metricId: null, metricName: '', metricFormula: '', unit: '' }; this.defTitle = '新增指标'; this.defOpen = true },
editMetricDef(row) { this.defForm = { metricId: row.metricId, metricName: row.metricName, metricFormula: row.metricFormula, unit: row.remark||'' }; this.defTitle = '编辑指标'; this.defOpen = true },
async submitMetricDef() {
const f = this.defForm
if (!f.metricName) { this.$modal.msgWarning('请输入指标名称'); return }
if (f.metricId) {
await updateProdMetric({ metricId: f.metricId, metricName: f.metricName, metricFormula: f.metricFormula, remark: f.unit })
} else {
await addProdMetric({ reportId: this.activeReport.reportId, metricCode: f.metricName, metricName: f.metricName, metricFormula: f.metricFormula, metricValue: 0, remark: f.unit || '' })
}
this.defOpen = false; this.$modal.msgSuccess('保存成功')
await this.openMetricMgr()
},
async delMetricDef(row) {
this.$modal.confirm('确认删除指标 "' + row.metricName + '"').then(async () => {
await delProdMetric(row.metricId)
await this.openMetricMgr()
this.$modal.msgSuccess('已删除')
})
},
async saveColCfg() {
const rid = this.activeReport.reportId; this.colSaving = true
try {
const metricCols = this.allCols.filter(c => c.$type === 'metric')
for (const m of metricCols) {
if (!m.metricName) { this.$modal.msgWarning('存在空指标列'); return }
if (m.metricFormula) {
const testF = m.metricFormula.replace(/@\{[^}]+\}/g, '1').replace(/input_weight|output_weight/g, '1')
if (this.evalF(testF) === null) { this.$modal.msgError('指标 "' + m.metricName + '" 公式无效'); return }
}
}
// ensure metric definitions exist in DB for all metric columns before saving config
for (const mc of metricCols) {
if (!mc.metricId) {
const r = await addProdMetric({ reportId: rid, metricCode: mc.metricName, metricName: mc.metricName, metricFormula: mc.metricFormula || '', metricValue: 0, remark: mc.unit || '' })
mc.metricId = r.data && r.data.metricId || r.metricId
}
}
const columns = this.allCols.map(c => {
const o = { t: c.$type === 'detail' ? 'd' : 'm', s: !!c.isShift }
o.id = String(c.$type === 'detail' ? c.itemId : c.metricId)
if (c.color && typeof c.color === 'string') o.c = c.color
return o
})
this.activeReport.colConfig = JSON.stringify({ columns })
await updateProdReport({ reportId: rid, colConfig: this.activeReport.colConfig })
this.$modal.msgSuccess("列配置已保存"); this.colOpen = false
await this.loadGrid()
} finally { this.colSaving = false }
},
/* grid */
async loadGrid() {
const rid = this.activeReport.reportId; this.gridLoading = true
try {
const dr = await listProdDetail({ reportId: rid, pageNum: 1, pageSize: 9999 })
await this.restoreAllCols()
this.buildGrid(dr.rows || [])
this.recalcAll()
} finally { this.gridLoading = false }
},
async restoreAllCols() {
await this.loadItems(); await this.loadAllMetrics(this.activeReport.reportId)
const cfg = JSON.parse(this.activeReport.colConfig || 'null')
if (!cfg || !cfg.columns || !cfg.columns.length) { this.allCols = []; return }
const cols = []
for (const c of cfg.columns) {
if (c.t === 'd') {
const id = String(c.id)
const item = this.allItems.find(i => String(i.itemId) === id)
if (item) cols.push({ $type: 'detail', itemId: item.itemId, itemCode: item.itemCode, itemName: item.itemName, unit: item.unit, isShift: !!c.s, color: c.c || null, queryCondition: item.queryCondition, category: item.category })
} else if (c.t === 'm') {
const id = String(c.id)
let def = (this._allMetricDefs || []).find(m => String(m.metricId) === id)
// fallback: try to fetch metric by ID individually if not in cached list
if (!def && c.id) {
try { const r = await getProdMetric(c.id); if (r.data) { def = r.data; this._allMetricDefs.push(def) } } catch(e) {}
}
if (def) cols.push({ $type: 'metric', metricId: def.metricId, metricName: def.metricName, metricFormula: def.metricFormula, unit: def.remark||'', isShift: !!c.s, color: c.c || null })
}
}
this.allCols = cols
let mi = 0; this.allCols.forEach(c => { if (c.$type === 'metric') c.mIdx = mi++ })
},
buildGrid(details) {
const map = {}; details.forEach(d => {
if (!d.detailDate) return
if (!map[d.detailDate]) map[d.detailDate] = { detailDate: d.detailDate }
const sfx = d.shift && d.shift !== '0' ? '_' + d.shift : ''
map[d.detailDate]['q' + d.itemId + sfx] = d.quantity
})
this.gridRows = Object.values(map).sort((a,b) => a.detailDate.localeCompare(b.detailDate))
},
recalcAll() {
const rp = this.activeReport || {}; const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const detailCols = this.allCols.filter(c => c.$type === 'detail')
const metricCols = this.allCols.filter(c => c.$type === 'metric')
this.gridRows.forEach(row => {
metricCols.forEach((m, mi) => {
if (!m.metricFormula) return
const ef = (shiftId) => {
let f = m.metricFormula
for (let pmi = 0; pmi < mi; pmi++) {
const pm = metricCols[pmi]; if (!pm.metricName) continue
const pn = esc(pm.metricName)
if (pm.isShift) {
const pv1 = row['mv'+pm.mIdx+'_1']; const pv2 = row['mv'+pm.mIdx+'_2']
if (pv1 != null) f = f.replace(new RegExp('@\\{'+pn+'\\}\\.甲班','g'), pv1)
if (pv2 != null) f = f.replace(new RegExp('@\\{'+pn+'\\}\\.乙班','g'), pv2)
}
const pv = pm.isShift ? (shiftId ? row['mv'+pm.mIdx+'_'+shiftId] : null) : row['mv'+pm.mIdx]
if (pv != null) f = f.replace(new RegExp('@\\{'+pn+'\\}','g'), pv)
}
detailCols.forEach(c => {
const item = this.allItems.find(i => String(i.itemId) === String(c.itemId))
if (!item || !item.itemCode) return; const code = item.itemCode
if (c.isShift) {
const v1 = parseFloat(row['q'+c.itemId+'_1'])||0; const v2 = parseFloat(row['q'+c.itemId+'_2'])||0
f = f.replace(new RegExp('@\\{'+code+'\\}\\.甲班','g'), v1).replace(new RegExp('@\\{'+code+'\\}\\.乙班','g'), v2)
const v = shiftId ? (shiftId==='1'?v1:v2) : v1+v2
f = f.replace(new RegExp('@\\{'+code+'\\}','g'), v)
} else {
const v = parseFloat(row['q'+c.itemId])||0
f = f.replace(new RegExp('@\\{'+code+'\\}\\.甲班','g'), v).replace(new RegExp('@\\{'+code+'\\}\\.乙班','g'), v).replace(new RegExp('@\\{'+code+'\\}','g'), v)
}
})
f = f.replace(/input_weight/g, rp.inputWeight||0).replace(/output_weight/g, rp.outputWeight||0)
return this.evalF(f)
}
if (m.isShift) { row['mv'+m.mIdx+'_1']=ef('1'); row['mv'+m.mIdx+'_2']=ef('2') }
else row['mv'+m.mIdx]=ef(null)
})
})
},
evalF(f) { const s = f.replace(/[^0-9+\-*/.()\s]/g,''); if(!s) return null; try { const r = new Function('return ('+s+')')(); return isFinite(r)?Math.round(r*10000)/10000:null } catch(e){ return null } },
sortGrid() { this.gridRows.sort((a,b)=>{if(!a.detailDate)return 1;if(!b.detailDate)return -1;return a.detailDate.localeCompare(b.detailDate)}) },
async saveGrid() {
const rid = this.activeReport.reportId; if (!rid) return; this.saving = true
try {
const exist = await listProdDetail({ reportId: rid, pageNum: 1, pageSize: 9999 })
const ids = (exist.rows||[]).map(d => d.detailId)
const detailCols = this.allCols.filter(c => c.$type === 'detail'); const detailList = []
this.gridRows.forEach(row => {
if (!row.detailDate) return
detailCols.forEach(col => {
const push = (shift, sfx) => { const qty = row['q'+col.itemId+sfx]; if (qty != null && qty !== '') detailList.push({ reportId: rid, detailDate: row.detailDate, shift: shift||'0', itemId: col.itemId, quantity: qty }) }
if (col.isShift) { push('1','_1'); push('2','_2') } else push(null,'')
})
})
await batchSaveProdDetail({ detailIds: ids, prodDetailList: detailList })
this.$modal.msgSuccess("保存成功"); await this.loadGrid()
} catch(e) { this.$modal.msgError("保存失败") } finally { this.saving = false }
},
/* copy config */
async openCopyCfg() { const r = await listProdReport({ pageNum:1, pageSize:9999 }); this.copyReports = (r.rows||[]).filter(rp=>rp.reportId!==this.activeReport.reportId); this.copySrc=null; this.copyCfgOpen=true },
async doCopyCfg() {
if (!this.copySrc) return; const sr = await getProdReport(this.copySrc.reportId)
const srcCfg = JSON.parse(((sr.data&&sr.data.colConfig)||'null'))
if (!srcCfg || (!srcCfg.columns && !srcCfg.itemIds)) { this.$modal.msgWarning('源报表无列配置'); return }
await this.loadItems(); await this.loadAllMetrics()
const usedIds = new Set(this.allCols.filter(c=>c.$type==='detail').map(c=>String(c.itemId)))
const usedMids = new Set(this.allCols.filter(c=>c.$type==='metric'&&c.metricId).map(c=>String(c.metricId)))
const cols = srcCfg.columns || []
cols.forEach(sc => {
if (sc.t === 'd') {
const sid = String(sc.id)
if (!usedIds.has(sid)) { const item = this.allItems.find(i=>String(i.itemId)===sid); if (item) { this.allCols.push({ $type:'detail', itemId:item.itemId, itemCode:item.itemCode, itemName:item.itemName, unit:item.unit, isShift:!!sc.s, color:sc.c||null, queryCondition:item.queryCondition, category:item.category }); usedIds.add(sid) } }
}
else if (sc.t === 'm') {
const sid = String(sc.id)
let def = this._allMetricDefs.find(m=>String(m.metricId)===sid)
if (def && !usedMids.has(sid)) { usedMids.add(sid); this.allCols.push({ $type:'metric', metricId:String(def.metricId), metricName:def.metricName, metricFormula:def.metricFormula, unit:def.remark||'', isShift:!!sc.s, color:sc.c||null }) }
}
})
this.copyCfgOpen = false; let mi = 0; this.allCols.forEach(c => { if (c.$type === 'metric') c.mIdx = mi++ })
this.$modal.msgSuccess('配置已复用')
},
/* auto fetch */
async fetchAutoData(col, row, shift) {
if (!col.queryCondition || this.autoLoading[col.itemId]) return
const handler = queryHandlers[col.category] || queryHandlers['default']
if (!handler) { this.$modal.msgWarning(`类别 "${col.category}" 未注册查询处理器`); return }
this.$set(this.autoLoading, col.itemId, true)
try {
const val = await handler(col.queryCondition, row, col, this.activeReport, shift)
if (val != null) {
const round3 = n => Math.round(n * 1000) / 1000
if (Array.isArray(val)) {
this.$set(row, 'q' + col.itemId + '_1', round3(val[0]))
this.$set(row, 'q' + col.itemId + '_2', round3(val[1]))
} else {
const key = 'q' + col.itemId + (shift ? '_' + shift : '')
this.$set(row, key, round3(val))
}
this.recalcAll()
}
} catch (e) {
this.$modal.msgError('自动获取数据失败')
} finally {
this.$set(this.autoLoading, col.itemId, false)
}
},
/* helpers */
async loadItems() { if (!this.allItems.length) { const r = await listItem({ pageNum:1, pageSize:999 }); this.allItems = r.rows || [] } },
async loadAllMetrics(rid) {
const q = { pageNum:1, pageSize:99999 }; if (rid) q.reportId = rid
const r = await listProdMetric(q); const map = {}
;(r.rows||[]).forEach(m => { if (m.metricName && !map[m.metricId]) map[m.metricId] = m })
this._allMetricDefs = Object.values(map)
},
async enter(row) { const r = await getProdReport(row.reportId); if (r.data) this.activeReport = r.data; else this.activeReport = row; this.loadGrid() }
}
}
</script>
<style scoped>
.mb8 { margin-bottom: 8px; }
/deep/ .el-table--mini td { padding: 2px 0; }
/deep/ .el-table--mini th { padding: 4px 0; font-size: 12px; }
/deep/ .el-table--mini .cell { padding: 0 3px; line-height: 1.2; }
/deep/ .el-input--mini .el-input__inner { height: 24px; line-height: 24px; padding: 0 5px; }
/deep/ .el-date-editor.el-input--mini { width: 124px !important; }
/deep/ .el-date-editor.el-input--mini .el-input__inner { padding-left: 5px; padding-right: 18px; }
/deep/ .el-date-editor.el-input--mini .el-input__prefix { left: auto; right: 2px; }
/deep/ .el-button--mini { padding: 4px 8px; font-size: 12px; }
/deep/ .el-button--mini.el-button--text { padding: 0; }
.report-tab-bar { display: flex; align-items: center; border-bottom: 1px solid #dcdfe6; margin-bottom: 12px; padding: 0 4px; }
.report-tabs { flex: 1; display: flex; overflow-x: auto; margin-bottom: -1px; }
.report-tab { display: inline-block; padding: 7px 16px; cursor: pointer; border-bottom: 2px solid transparent; white-space: nowrap; font-size: 13px; color: #606266; transition: all 0.2s; user-select: none; }
.report-tab:hover { color: #409eff; }
.report-tab.active { color: #409eff; border-bottom-color: #409eff; font-weight: 500; }
.empty-hint { text-align: center; padding: 60px 0; color: #999; font-size: 14px; }
.entry-header { line-height: 28px; }
.entry-title { font-weight: bold; font-size: 15px; }
.entry-meta { margin-left: 10px; color: #999; font-size: 12px; }
.shift-cell { display: flex; align-items: center; }
.shift-tag { font-size: 10px; color: #909399; width: 16px; flex-shrink: 0; }
.shift-input { flex: 1; min-width: 0; }
.shift-metric { font-size: 12px; line-height: 1.5; }
.formula-vars { padding: 4px 6px; margin-bottom: 6px; background: #f5f7fa; border-radius: 3px; border: 1px solid #e4e7ed; line-height: 1.8; }
.vars-tag { cursor: pointer; margin: 1px; }
.vars-tag:hover { opacity: 0.8; }
.var-sys { background: #ecf5ff; border-color: #d9ecff; color: #409eff; }
.var-mtr { background: #f0f9eb; border-color: #e1f3d8; color: #67c23a; }
.drag-handle { cursor: grab; font-size: 14px; color: #909399; padding: 2px; display: inline-flex; align-items: center; }
.drag-handle:active { cursor: grabbing; }
.drag-handle:hover { color: #409eff; }
</style>

View File

@@ -0,0 +1,356 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="生产月报ID" prop="reportId">
<el-input
v-model="queryParams.reportId"
placeholder="请输入生产月报ID"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="班次 1=甲班 2=乙班" prop="shift">
<el-input
v-model="queryParams.shift"
placeholder="请输入班次 1=甲班 2=乙班"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="日期" prop="detailDate">
<el-date-picker clearable
v-model="queryParams.detailDate"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择日期">
</el-date-picker>
</el-form-item>
<el-form-item label="成本项目ID" prop="itemId">
<el-input
v-model="queryParams.itemId"
placeholder="请输入成本项目ID"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="消耗用量" prop="quantity">
<el-input
v-model="queryParams.quantity"
placeholder="请输入消耗用量"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="单价" prop="unitPrice">
<el-input
v-model="queryParams.unitPrice"
placeholder="请输入单价"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="总金额" prop="amount">
<el-input
v-model="queryParams.amount"
placeholder="请输入总金额"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleUpdate"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="prodDetailList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="主键ID" align="center" prop="detailId" v-if="true"/>
<el-table-column label="生产月报ID" align="center" prop="reportId" />
<el-table-column label="班次 1=甲班 2=乙班" align="center" prop="shift" />
<el-table-column label="日期" align="center" prop="detailDate" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.detailDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="成本项目ID" align="center" prop="itemId" />
<el-table-column label="消耗用量" align="center" prop="quantity" />
<el-table-column label="单价" align="center" prop="unitPrice" />
<el-table-column label="总金额" align="center" prop="amount" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改生产成本明细对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="生产月报ID" prop="reportId">
<el-input v-model="form.reportId" placeholder="请输入生产月报ID" />
</el-form-item>
<el-form-item label="班次 1=甲班 2=乙班" prop="shift">
<el-input v-model="form.shift" placeholder="请输入班次 1=甲班 2=乙班" />
</el-form-item>
<el-form-item label="日期" prop="detailDate">
<el-date-picker clearable
v-model="form.detailDate"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择日期">
</el-date-picker>
</el-form-item>
<el-form-item label="成本项目ID" prop="itemId">
<el-input v-model="form.itemId" placeholder="请输入成本项目ID" />
</el-form-item>
<el-form-item label="消耗用量" prop="quantity">
<el-input v-model="form.quantity" placeholder="请输入消耗用量" />
</el-form-item>
<el-form-item label="单价" prop="unitPrice">
<el-input v-model="form.unitPrice" placeholder="请输入单价" />
</el-form-item>
<el-form-item label="总金额" prop="amount">
<el-input v-model="form.amount" placeholder="请输入总金额" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listProdDetail, getProdDetail, delProdDetail, addProdDetail, updateProdDetail } from "@/api/cost/prodDetail";
export default {
name: "ProdDetail",
data() {
return {
// 按钮loading
buttonLoading: false,
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 生产成本明细表格数据
prodDetailList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
reportId: undefined,
shift: undefined,
detailDate: undefined,
itemId: undefined,
quantity: undefined,
unitPrice: undefined,
amount: undefined,
},
// 表单参数
form: {},
// 表单校验
rules: {
}
};
},
created() {
this.getList();
},
methods: {
/** 查询生产成本明细列表 */
getList() {
this.loading = true;
listProdDetail(this.queryParams).then(response => {
this.prodDetailList = response.rows;
this.total = response.total;
this.loading = false;
});
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
detailId: undefined,
reportId: undefined,
shift: undefined,
detailDate: undefined,
itemId: undefined,
quantity: undefined,
unitPrice: undefined,
amount: undefined,
remark: undefined,
delFlag: undefined,
createBy: undefined,
createTime: undefined,
updateBy: undefined,
updateTime: undefined
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.detailId)
this.single = selection.length!==1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加生产成本明细";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.loading = true;
this.reset();
const detailId = row.detailId || this.ids
getProdDetail(detailId).then(response => {
this.loading = false;
this.form = response.data;
this.open = true;
this.title = "修改生产成本明细";
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
this.buttonLoading = true;
if (this.form.detailId != null) {
updateProdDetail(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
} else {
addProdDetail(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const detailIds = row.detailId || this.ids;
this.$modal.confirm('是否确认删除生产成本明细编号为"' + detailIds + '"的数据项?').then(() => {
this.loading = true;
return delProdDetail(detailIds);
}).then(() => {
this.loading = false;
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {
}).finally(() => {
this.loading = false;
});
},
/** 导出按钮操作 */
handleExport() {
this.download('cost/prodDetail/export', {
...this.queryParams
}, `prodDetail_${new Date().getTime()}.xlsx`)
}
}
};
</script>

View File

@@ -0,0 +1,314 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="报表标题" prop="reportTitle">
<el-input
v-model="queryParams.reportTitle"
placeholder="请输入报表标题"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="报表日期" prop="reportDate">
<el-date-picker clearable
v-model="queryParams.reportDate"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择报表日期">
</el-date-picker>
</el-form-item>
<el-form-item label="投入量 单位:吨" prop="inputWeight">
<el-input
v-model="queryParams.inputWeight"
placeholder="请输入投入量 单位:吨"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="产出量 单位:吨" prop="outputWeight">
<el-input
v-model="queryParams.outputWeight"
placeholder="请输入产出量 单位:吨"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleUpdate"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="prodReportList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="主键ID" align="center" prop="reportId" v-if="true"/>
<el-table-column label="报表标题" align="center" prop="reportTitle" />
<el-table-column label="报表日期" align="center" prop="reportDate" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.reportDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="投入量 单位:吨" align="center" prop="inputWeight" />
<el-table-column label="产出量 单位:吨" align="center" prop="outputWeight" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改生产月报对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="报表标题" prop="reportTitle">
<el-input v-model="form.reportTitle" placeholder="请输入报表标题" />
</el-form-item>
<el-form-item label="报表日期" prop="reportDate">
<el-date-picker clearable
v-model="form.reportDate"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择报表日期">
</el-date-picker>
</el-form-item>
<el-form-item label="投入量 单位:吨" prop="inputWeight">
<el-input v-model="form.inputWeight" placeholder="请输入投入量 单位:吨" />
</el-form-item>
<el-form-item label="产出量 单位:吨" prop="outputWeight">
<el-input v-model="form.outputWeight" placeholder="请输入产出量 单位:吨" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listProdReport, getProdReport, delProdReport, addProdReport, updateProdReport } from "@/api/cost/prodReport";
export default {
name: "ProdReport",
data() {
return {
// 按钮loading
buttonLoading: false,
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 生产月报表格数据
prodReportList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
reportTitle: undefined,
reportDate: undefined,
inputWeight: undefined,
outputWeight: undefined,
},
// 表单参数
form: {},
// 表单校验
rules: {
}
};
},
created() {
this.getList();
},
methods: {
/** 查询生产月报列表 */
getList() {
this.loading = true;
listProdReport(this.queryParams).then(response => {
this.prodReportList = response.rows;
this.total = response.total;
this.loading = false;
});
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
reportId: undefined,
reportTitle: undefined,
reportDate: undefined,
inputWeight: undefined,
outputWeight: undefined,
remark: undefined,
delFlag: undefined,
createBy: undefined,
createTime: undefined,
updateBy: undefined,
updateTime: undefined
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.reportId)
this.single = selection.length!==1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加生产月报";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.loading = true;
this.reset();
const reportId = row.reportId || this.ids
getProdReport(reportId).then(response => {
this.loading = false;
this.form = response.data;
this.open = true;
this.title = "修改生产月报";
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
this.buttonLoading = true;
if (this.form.reportId != null) {
updateProdReport(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
} else {
addProdReport(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const reportIds = row.reportId || this.ids;
this.$modal.confirm('是否确认删除生产月报编号为"' + reportIds + '"的数据项?').then(() => {
this.loading = true;
return delProdReport(reportIds);
}).then(() => {
this.loading = false;
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {
}).finally(() => {
this.loading = false;
});
},
/** 导出按钮操作 */
handleExport() {
this.download('cost/prodReport/export', {
...this.queryParams
}, `prodReport_${new Date().getTime()}.xlsx`)
}
}
};
</script>

View File

@@ -0,0 +1,337 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="成本项目编码" prop="itemCode">
<el-input
v-model="queryParams.itemCode"
placeholder="请输入成本项目编码"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="成本项目名称" prop="itemName">
<el-input
v-model="queryParams.itemName"
placeholder="请输入成本项目名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="成本分类" prop="category">
<el-select
v-model="queryParams.category"
placeholder="请选择成本分类"
clearable
@change="handleQuery"
>
<el-option label="原料" value="原料" />
<el-option label="能耗" value="能耗" />
<el-option label="辅料" value="辅料" />
<el-option label="设备" value="设备" />
<el-option label="人工" value="人工" />
<el-option label="产出" value="产出" />
<el-option label="轧辊" value="轧辊" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item label="计量单位" prop="unit">
<el-input
v-model="queryParams.unit"
placeholder="请输入计量单位"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="查询条件" prop="queryCondition">
<el-input
v-model="queryParams.queryCondition"
placeholder="请输入查询条件"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleUpdate"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="itemList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="主键ID" align="center" prop="itemId" v-if="true"/>
<el-table-column label="成本项目编码" align="center" prop="itemCode" />
<el-table-column label="成本项目名称" align="center" prop="itemName" />
<el-table-column label="成本分类" align="center" prop="category" />
<el-table-column label="计量单位" align="center" prop="unit" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="查询条件" align="center" prop="queryCondition" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改成本项目配置对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="成本项目编码" prop="itemCode">
<el-input v-model="form.itemCode" placeholder="请输入成本项目编码" />
</el-form-item>
<el-form-item label="成本项目名称" prop="itemName">
<el-input v-model="form.itemName" placeholder="请输入成本项目名称" />
</el-form-item>
<el-form-item label="成本分类" prop="category">
<el-select v-model="form.category" placeholder="请选择成本分类">
<el-option label="原料" value="原料" />
<el-option label="能耗" value="能耗" />
<el-option label="辅料" value="辅料" />
<el-option label="设备" value="设备" />
<el-option label="人工" value="人工" />
<el-option label="产出" value="产出" />
<el-option label="轧辊" value="轧辊" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item label="计量单位" prop="unit">
<el-input v-model="form.unit" placeholder="请输入计量单位" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
<el-form-item label="查询条件" prop="queryCondition">
<el-input v-model="form.queryCondition" placeholder="请输入查询条件" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listItem, getItem, delItem, addItem, updateItem } from "@/api/cost/item";
export default {
name: "Item",
data() {
return {
// 按钮loading
buttonLoading: false,
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 成本项目配置表格数据
itemList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
itemCode: undefined,
itemName: undefined,
category: undefined,
unit: undefined,
queryCondition: undefined,
},
// 表单参数
form: {},
// 表单校验
rules: {
}
};
},
created() {
this.getList();
},
methods: {
/** 查询成本项目配置列表 */
getList() {
this.loading = true;
listItem(this.queryParams).then(response => {
this.itemList = response.rows;
this.total = response.total;
this.loading = false;
});
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
itemId: undefined,
itemCode: undefined,
itemName: undefined,
category: undefined,
unit: undefined,
remark: undefined,
queryCondition: undefined,
delFlag: undefined,
createBy: undefined,
createTime: undefined,
updateBy: undefined,
updateTime: undefined
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.itemId)
this.single = selection.length!==1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加成本项目配置";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.loading = true;
this.reset();
const itemId = row.itemId || this.ids
getItem(itemId).then(response => {
this.loading = false;
this.form = response.data;
this.open = true;
this.title = "修改成本项目配置";
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
this.buttonLoading = true;
if (this.form.itemId != null) {
updateItem(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
} else {
addItem(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const itemIds = row.itemId || this.ids;
this.$modal.confirm('是否确认删除成本项目配置编号为"' + itemIds + '"的数据项?').then(() => {
this.loading = true;
return delItem(itemIds);
}).then(() => {
this.loading = false;
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {
}).finally(() => {
this.loading = false;
});
},
/** 导出按钮操作 */
handleExport() {
this.download('cost/item/export', {
...this.queryParams
}, `item_${new Date().getTime()}.xlsx`)
}
}
};
</script>

View File

@@ -0,0 +1,319 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="生产日报ID" prop="reportId">
<el-input
v-model="queryParams.reportId"
placeholder="请输入生产日报ID"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="指标编码" prop="metricCode">
<el-input
v-model="queryParams.metricCode"
placeholder="请输入指标编码"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="指标名称" prop="metricName">
<el-input
v-model="queryParams.metricName"
placeholder="请输入指标名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="指标计算公式" prop="metricFormula">
<el-input
v-model="queryParams.metricFormula"
placeholder="请输入指标计算公式"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="指标计算结果值" prop="metricValue">
<el-input
v-model="queryParams.metricValue"
placeholder="请输入指标计算结果值"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleUpdate"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="prodMetricList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="主键ID" align="center" prop="metricId" v-if="true"/>
<el-table-column label="生产日报ID" align="center" prop="reportId" />
<el-table-column label="指标编码" align="center" prop="metricCode" />
<el-table-column label="指标名称" align="center" prop="metricName" />
<el-table-column label="指标计算公式" align="center" prop="metricFormula" />
<el-table-column label="指标计算结果值" align="center" prop="metricValue" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改生产指标明细对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="生产日报ID" prop="reportId">
<el-input v-model="form.reportId" placeholder="请输入生产日报ID" />
</el-form-item>
<el-form-item label="指标编码" prop="metricCode">
<el-input v-model="form.metricCode" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="指标名称" prop="metricName">
<el-input v-model="form.metricName" placeholder="请输入指标名称" />
</el-form-item>
<el-form-item label="指标计算公式" prop="metricFormula">
<el-input v-model="form.metricFormula" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="指标计算结果值" prop="metricValue">
<el-input v-model="form.metricValue" placeholder="请输入指标计算结果值" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listProdMetric, getProdMetric, delProdMetric, addProdMetric, updateProdMetric } from "@/api/cost/prodMetric";
export default {
name: "ProdMetric",
data() {
return {
// 按钮loading
buttonLoading: false,
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 生产指标明细表格数据
prodMetricList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
reportId: undefined,
metricCode: undefined,
metricName: undefined,
metricFormula: undefined,
metricValue: undefined,
},
// 表单参数
form: {},
// 表单校验
rules: {
}
};
},
created() {
this.getList();
},
methods: {
/** 查询生产指标明细列表 */
getList() {
this.loading = true;
listProdMetric(this.queryParams).then(response => {
this.prodMetricList = response.rows;
this.total = response.total;
this.loading = false;
});
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
metricId: undefined,
reportId: undefined,
metricCode: undefined,
metricName: undefined,
metricFormula: undefined,
metricValue: undefined,
remark: undefined,
delFlag: undefined,
createBy: undefined,
createTime: undefined,
updateBy: undefined,
updateTime: undefined
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.metricId)
this.single = selection.length!==1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加生产指标明细";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.loading = true;
this.reset();
const metricId = row.metricId || this.ids
getProdMetric(metricId).then(response => {
this.loading = false;
this.form = response.data;
this.open = true;
this.title = "修改生产指标明细";
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
this.buttonLoading = true;
if (this.form.metricId != null) {
updateProdMetric(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
} else {
addProdMetric(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const metricIds = row.metricId || this.ids;
this.$modal.confirm('是否确认删除生产指标明细编号为"' + metricIds + '"的数据项?').then(() => {
this.loading = true;
return delProdMetric(metricIds);
}).then(() => {
this.loading = false;
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {
}).finally(() => {
this.loading = false;
});
},
/** 导出按钮操作 */
handleExport() {
this.download('cost/prodMetric/export', {
...this.queryParams
}, `prodMetric_${new Date().getTime()}.xlsx`)
}
}
};
</script>

View File

@@ -0,0 +1,300 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="成本项目ID" prop="itemId">
<el-input
v-model="queryParams.itemId"
placeholder="请输入成本项目ID"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="单价" prop="price">
<el-input
v-model="queryParams.price"
placeholder="请输入单价"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="生效日期" prop="effectiveDate">
<el-date-picker clearable
v-model="queryParams.effectiveDate"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择生效日期">
</el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleUpdate"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="priceList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="主键ID" align="center" prop="priceId" v-if="true"/>
<el-table-column label="成本项目ID" align="center" prop="itemId" />
<el-table-column label="单价" align="center" prop="price" />
<el-table-column label="生效日期" align="center" prop="effectiveDate" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.effectiveDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改成本单价历史对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="成本项目ID" prop="itemId">
<el-input v-model="form.itemId" placeholder="请输入成本项目ID" />
</el-form-item>
<el-form-item label="单价" prop="price">
<el-input v-model="form.price" placeholder="请输入单价" />
</el-form-item>
<el-form-item label="生效日期" prop="effectiveDate">
<el-date-picker clearable
v-model="form.effectiveDate"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择生效日期">
</el-date-picker>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listPrice, getPrice, delPrice, addPrice, updatePrice } from "@/api/cost/price";
export default {
name: "Price",
data() {
return {
// 按钮loading
buttonLoading: false,
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 成本单价历史表格数据
priceList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
itemId: undefined,
price: undefined,
effectiveDate: undefined,
},
// 表单参数
form: {},
// 表单校验
rules: {
}
};
},
created() {
this.getList();
},
methods: {
/** 查询成本单价历史列表 */
getList() {
this.loading = true;
listPrice(this.queryParams).then(response => {
this.priceList = response.rows;
this.total = response.total;
this.loading = false;
});
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
priceId: undefined,
itemId: undefined,
price: undefined,
effectiveDate: undefined,
remark: undefined,
delFlag: undefined,
createBy: undefined,
createTime: undefined,
updateBy: undefined,
updateTime: undefined
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.priceId)
this.single = selection.length!==1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加成本单价历史";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.loading = true;
this.reset();
const priceId = row.priceId || this.ids
getPrice(priceId).then(response => {
this.loading = false;
this.form = response.data;
this.open = true;
this.title = "修改成本单价历史";
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
this.buttonLoading = true;
if (this.form.priceId != null) {
updatePrice(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
} else {
addPrice(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const priceIds = row.priceId || this.ids;
this.$modal.confirm('是否确认删除成本单价历史编号为"' + priceIds + '"的数据项?').then(() => {
this.loading = true;
return delPrice(priceIds);
}).then(() => {
this.loading = false;
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {
}).finally(() => {
this.loading = false;
});
},
/** 导出按钮操作 */
handleExport() {
this.download('cost/price/export', {
...this.queryParams
}, `price_${new Date().getTime()}.xlsx`)
}
}
};
</script>

View File

@@ -0,0 +1,185 @@
<template>
<el-form ref="form" :model="formData" :rules="rules" label-width="80px">
<el-form-item label="钢卷号" prop="currentCoilNo">
<el-input v-model="formData.currentCoilNo" placeholder="请输入钢卷号" />
</el-form-item>
<el-form-item label="缺陷描述" prop="remark">
<el-input type="textarea" v-model="formData.remark" placeholder="请输入缺陷描述" />
</el-form-item>
<el-form-item label="上下板面" prop="plateSurface">
<muti-select v-model="formData.plateSurface" :options="[
{label: '上板面', value: '上'},
{label: '下板面', value: '下'}]" type="checkbox">
</muti-select>
</el-form-item>
<el-form-item label="断面位置" prop="position">
<muti-select v-model="formData.position" :options="dict.type.coil_abnormal_position" type="checkbox">
</muti-select>
</el-form-item>
<el-form-item>
<el-alert title="异常位置为内圈算起" type="info" :closable="false" show-icon size="small" />
</el-form-item>
<el-form-item label="异常区间" required>
<div style="display: flex; align-items: center;">
<el-form-item prop="startPosition">
<el-input v-model="formData.startPosition" type="number" placeholder="请输入开始位置" />
</el-form-item>
<el-form-item prop="endPosition">
<el-input v-model="formData.endPosition" type="number" placeholder="请输入结束位置" />
</el-form-item>
</div>
</el-form-item>
<el-form-item label="缺陷代码" prop="defectCode">
<el-radio-group v-model="formData.defectCode">
<el-radio-button v-for="dict in dict.type.coil_abnormal_code" :key="dict.value" :label="dict.value">{{
dict.label }}</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="程度" prop="degree">
<el-radio-group v-model="formData.degree">
<el-radio-button v-for="dict in dict.type.coil_abnormal_degree" :key="dict.value" :label="dict.value">{{
dict.label }}</el-radio-button>
</el-radio-group>
</el-form-item>
<!-- <el-form-item label="产线" prop="productionLine">
<el-select v-model="formData.productionLine" placeholder="请选择产线">
<el-option v-for="dict in dict.type.sys_lines" :key="dict.value" :label="dict.label"
:value="dict.value"></el-option>
</el-select>
</el-form-item> -->
<el-form-item label="主缺陷" prop="mainMark">
<!-- 0表示否1表示是 -->
<el-checkbox v-model="formData.mainMark" :true-label="1" :false-label="0">是否为主缺陷</el-checkbox>
</el-form-item>
<el-form-item label="缺陷图片" v-if="formData.mainMark === 1">
<image-upload v-model="formData.attachmentFiles" value-mode="url" :limit="3" />
</el-form-item>
</el-form>
</template>
<script>
import ImageUpload from '@/components/ImageUpload'
import MutiSelect from '@/components/MutiSelect'
export default {
name: "AbnormalForm",
components: {
ImageUpload,
MutiSelect
},
props: {
value: {
type: Object,
default: () => ({})
},
showCoilSelector: {
type: Boolean,
default: false
}
},
dicts: ['coil_abnormal_code', 'coil_abnormal_position', 'coil_abnormal_degree', 'sys_lines'],
data() {
return {
rules: {
position: [
{ required: true, message: '请选择位置', trigger: 'change' },
{ validator: this.validateArray, trigger: 'change' }
],
// plateSurface: [
// { required: true, message: '请选择上下板面', trigger: 'change' },
// { validator: this.validateArray, trigger: 'change' }
// ],
startPosition: [
{ required: true, message: '请输入开始位置', trigger: ['blur', 'change'] },
{ validator: this.validateStartPosition, trigger: ['blur', 'change'] }
],
endPosition: [
{ required: true, message: '请输入结束位置', trigger: ['blur', 'change'] },
{ validator: this.validateEndPosition, trigger: ['blur', 'change'] }
],
defectCode: [
{ required: true, message: '请选择缺陷代码', trigger: 'change' }
],
degree: [
{ required: true, message: '请选择程度', trigger: 'change' }
]
}
};
},
computed: {
formData: {
get() {
const data = this.value || {};
return data;
},
set(newVal) {
// 发送时将数组转为CSV
const data = { ...newVal };
this.$emit('input', data);
}
}
},
methods: {
/** 表单验证 */
validate(callback) {
if (parseInt(this.formData.startPosition) > parseInt(this.formData.endPosition)) {
this.$message.error('开始位置必须小于结束位置');
return false;
}
return this.$refs.form.validate(callback);
},
/** 重置表单 */
resetFields() {
this.$refs.form.resetFields();
const currentCoilId = this.formData.coilId;
this.formData = {
abnormalId: undefined,
coilId: currentCoilId,
position: undefined,
plateSurface: undefined,
startPosition: undefined,
endPosition: undefined,
length: undefined,
mainMark: 0,
productionLine: undefined,
defectCode: undefined,
degree: undefined,
remark: undefined,
attachmentFiles: undefined
};
},
/** 计算缺陷长度 */
calculateLength() {
if (this.formData.startPosition && this.formData.endPosition) {
this.formData.length = this.formData.endPosition - this.formData.startPosition;
}
},
/** 校验开始位置 */
validateStartPosition(rule, value, callback) {
if (value < 0) {
callback(new Error('开始位置必须为正数'));
} else {
callback();
}
},
/** 校验结束位置 */
validateEndPosition(rule, value, callback) {
if (value < 0) {
callback(new Error('结束位置必须为正数'));
} else {
callback();
}
},
/** 校验数组 */
validateArray(rule, value, callback) {
if (!value || value.length === 0) {
callback(new Error('请至少选择一个选项'));
} else {
callback();
}
}
}
};
</script>

View File

@@ -0,0 +1,106 @@
<template>
<el-table :data="list">
<el-table-column label="异常钢卷" align="center" prop="currentCoilNo" v-if="showCoil"></el-table-column>
<el-table-column label="缺陷描述" align="center" prop="remark" show-overflow-tooltip />
<el-table-column label="上下板面" align="center" prop="plateSurface"></el-table-column>
<el-table-column label="缺陷位置" align="center" prop="position">
<template slot-scope="scope">
<dict-tag :options="dict.type.coil_abnormal_position" :value="scope.row.position" />
</template>
</el-table-column>
<el-table-column label="开始位置" align="center" prop="startPosition" />
<el-table-column label="结束位置" align="center" prop="endPosition" />
<el-table-column label="缺陷长度" align="center" prop="length" />
<el-table-column label="缺陷代码" align="center" prop="defectCode">
<template slot-scope="scope">
<dict-tag :options="dict.type.coil_abnormal_code" :value="scope.row.defectCode" />
</template>
</el-table-column>
<el-table-column label="主缺陷" prop="mainMark">
<template slot-scope="scope">
<el-tag v-if="scope.row.mainMark" type="success"></el-tag>
<el-tag v-else type="danger"></el-tag>
</template>
</el-table-column>
<el-table-column label="缺陷图片" align="center" prop="attachmentFiles" width="120">
<template slot-scope="scope">
<template v-if="scope.row.attachmentFiles">
<div v-for="(url, idx) in scope.row.attachmentFiles.split(',')" :key="idx" style="margin-right: 4px;">
<el-image
style="width: 30px; height: 30px; vertical-align: middle; border-radius: 2px;"
:src="url"
:preview-src-list="scope.row.attachmentFiles.split(',')"
fit="cover"
/>
</div>
</template>
</template>
</el-table-column>
<el-table-column label="程度" align="center" prop="degree">
<template slot-scope="scope">
<dict-tag :options="dict.type.coil_abnormal_degree" :value="scope.row.degree" />
</template>
</el-table-column>
<el-table-column label="创建人" align="center" prop="createBy" />
<el-table-column label="创建时间" align="center" prop="createTime" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" v-if="editable">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</template>
<script>
export default {
props: {
list: {
type: Array,
default: () => []
},
editable: {
type: Boolean,
default: false
},
showCoil: {
type: Boolean,
default: false
},
coilInfo: {
type: Object,
default: () => ({})
}
},
dicts: ['coil_abnormal_position', 'coil_abnormal_code', 'coil_abnormal_degree'],
methods: {
handleDelete(row) {
if (this.editable) {
this.$emit('delete', row);
}
},
handleUpdate(row) {
if (this.editable) {
this.$emit('update', row);
}
},
// 计算目标列的异常挂载时机
// 如果coilInfo.coilId存在且与row.coilId相同
// 判断钢卷的createBy和row.createBy是否相同
// 判断钢卷的createTime与row的createTime是否在一分钟内
// 如果是,返回'生产时'
// 如果否,返回'补录'
getAbnormalTime(row) {
if (this.coilInfo.coilId === row.coilId) {
if (this.coilInfo.createBy === row.createBy) {
if (Math.abs(new Date(this.coilInfo.createTime) - new Date(row.createTime)) < 60 * 1000) {
return '生产时'
}
}
}
return '补录'
}
}
}
</script>

View File

@@ -0,0 +1,438 @@
<template>
<div class="exception-container" v-loading="abnormalLoading">
<div class="exception-section">
<div class="section-header">
<h4 class="section-title">实绩信息</h4>
</div>
<el-descriptions :column="4" border size="small" v-if="rowData">
<el-descriptions-item label="成品卷号">{{ rowData.exitMatId }}</el-descriptions-item>
<el-descriptions-item label="来料卷号">{{ rowData.entryMatId }}</el-descriptions-item>
<el-descriptions-item label="计划号">{{ rowData.planNo }}</el-descriptions-item>
<el-descriptions-item label="产品类型">{{ rowData.prodCode }}</el-descriptions-item>
<el-descriptions-item label="钢种">{{ rowData.steelGrade }}</el-descriptions-item>
<el-descriptions-item label="成品厚度">{{ rowData.exitThickness }}</el-descriptions-item>
<el-descriptions-item label="成品宽度">{{ rowData.exitWidth }}</el-descriptions-item>
<el-descriptions-item label="实际重量">{{ rowData.actualWeight }}</el-descriptions-item>
<el-descriptions-item label="班号">{{ rowData.groupNo }}</el-descriptions-item>
<el-descriptions-item label="组号">{{ rowData.shiftNo }}</el-descriptions-item>
<el-descriptions-item label="来料厚度">{{ rowData.entryThick }}</el-descriptions-item>
<el-descriptions-item label="来料宽度">{{ rowData.entryWidth }}</el-descriptions-item>
<el-descriptions-item label="来料重量">{{ rowData.entryWeight }}</el-descriptions-item>
<el-descriptions-item label="上线时间">{{ parseTime(rowData.onlineTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
<el-descriptions-item label="客户">{{ rowData.customer }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="4">{{ rowData.remark }}</el-descriptions-item>
</el-descriptions>
</div>
<div class="exception-section">
<div class="section-header">
<h4 class="section-title">异常记录</h4>
<div>
<el-button type="primary" size="mini" @click="handleSave" :loading="buttonLoading">保存</el-button>
<el-button type="default" icon="el-icon-refresh" plain size="mini" @click="refreshAbnormalList"
:loading="abnormalLoading">
刷新
</el-button>
</div>
</div>
<el-table :data="abnormalList" style="width: 100%" border stripe>
<el-table-column label="序号" type="index" width="50" />
<el-table-column label="缺陷描述" prop="remark">
<template slot-scope="scope">
<el-input v-model="scope.row.remark" placeholder="请输入缺陷描述" />
</template>
</el-table-column>
<el-table-column label="开始位置" prop="startPosition" width="100">
<template slot-scope="scope">
<el-input :controls="false" v-model="scope.row.startPosition" placeholder="请输入开始位置" />
</template>
</el-table-column>
<el-table-column label="结束位置" prop="endPosition" width="100">
<template slot-scope="scope">
<el-input :controls="false" v-model="scope.row.endPosition" placeholder="请输入结束位置" />
</template>
</el-table-column>
<el-table-column label="长度" prop="length" width="100">
<template slot-scope="scope">
{{ scope.row.endPosition - scope.row.startPosition }}
</template>
</el-table-column>
<el-table-column label="上下版面" prop="plateSurface" width="180">
<template slot-scope="scope">
<muti-select v-model="scope.row.plateSurface" :options="[
{ label: '上板面', value: '上' },
{ label: '下板面', value: '下' }]" type="checkbox">
</muti-select>
</template>
</el-table-column>
<el-table-column label="断面位置" prop="position" width="240">
<template slot-scope="scope">
<muti-select v-model="scope.row.position" :options="dict.type.coil_abnormal_position" type="checkbox">
</muti-select>
</template>
</el-table-column>
<el-table-column label="缺陷代码" prop="defectCode">
<template slot-scope="scope">
<div class="radio-single">
<el-radio-group v-model="scope.row.defectCode">
<el-radio v-for="dict in dict.type.coil_abnormal_code" :key="dict.value" :label="dict.value" @click.native="handleRadioClick(scope.row.defectCode, dict.value, 'defectCode', scope.row)">
{{ dict.label }}
</el-radio>
</el-radio-group>
</div>
</template>
</el-table-column>
<el-table-column label="程度" prop="degree">
<template slot-scope="scope">
<div class="radio-single">
<el-radio-group v-model="scope.row.degree">
<el-radio v-for="dict in dict.type.coil_abnormal_degree" :key="dict.value" :label="dict.value" @click.native="handleRadioClick(scope.row.degree, dict.value, 'degree', scope.row)">
{{ dict.label }}
</el-radio>
</el-radio-group>
</div>
</template>
</el-table-column>
<el-table-column label="主缺陷" prop="mainMark" width="60">
<template slot-scope="scope">
<el-checkbox v-model="scope.row.mainMark" :true-label="1" :false-label="0"></el-checkbox>
</template>
</el-table-column>
<el-table-column label="缺陷图片" prop="attachmentFiles" width="180">
<template slot-scope="scope">
<image-upload v-if="scope.row.mainMark === 1" v-model="scope.row.attachmentFiles" value-mode="url" :limit="3" :is-show-tip="false" />
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template slot-scope="scope">
<el-button v-if="scope.row.abnormalId" type="text" size="mini" @click="handleDelete(scope.row)" style="color: #f56c6c;">
删除
</el-button>
<el-button v-else type="text" size="mini" @click="clearRowData(scope.row)">
清空
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<script>
import { addCoilAbnormal, listCoilAbnormal, delCoilAbnormal, updateCoilAbnormal } from '@/api/mill/coilAbnormal'
import ImageUpload from '@/components/ImageUpload'
import MutiSelect from '@/components/MutiSelect'
export default {
name: 'ExceptionManager',
components: {
ImageUpload,
MutiSelect
},
dicts: ['coil_abnormal_code', 'coil_abnormal_degree', 'coil_abnormal_position'],
props: {
currentCoilNo: {
type: String,
required: true
},
rowData: {
type: Object,
default: null
}
},
computed: {
coilId() {
return this.currentCoilNo
}
},
data() {
return {
abnormalList: [],
abnormalLoading: false,
buttonLoading: false,
}
},
watch: {
coilId: {
handler(newVal) {
this.loadAbnormalList()
},
immediate: true
}
},
methods: {
loadAbnormalList() {
if (!this.coilId) {
// 确保至少10个空行
this.abnormalList = this.generateEmptyRows(10)
return
}
this.abnormalLoading = true
listCoilAbnormal({
currentCoilNo: this.coilId
}).then(response => {
let rows = response.rows || []
// 确保至少10个条目
if (rows.length < 10) {
const emptyRows = this.generateEmptyRows(10 - rows.length)
rows = [...rows, ...emptyRows]
}
this.abnormalList = rows
}).catch(error => {
console.error('查询异常记录失败:', error)
this.$message.error('查询异常记录失败: ' + (error.message || error))
// 失败时也要确保至少10个空行
this.abnormalList = this.generateEmptyRows(10)
}).finally(() => {
this.abnormalLoading = false
})
},
generateEmptyRows(count) {
const emptyRows = []
for (let i = 0; i < count; i++) {
emptyRows.push({
coilId: this.coilId,
position: '',
plateSurface: '',
startPosition: 0,
endPosition: 0,
length: 0,
mainMark: 0,
productionLine: null,
defectCode: null,
degree: null,
remark: null,
attachmentFiles: null
})
}
return emptyRows
},
handleSave() {
// 过滤出非空行
const nonEmptyRows = this.abnormalList.filter(row => {
return row.startPosition || row.endPosition || row.position || row.plateSurface || row.defectCode || row.degree || row.remark
})
// 批量校验
for (let i = 0; i < nonEmptyRows.length; i++) {
const row = nonEmptyRows[i]
const rowIndex = this.abnormalList.indexOf(row) + 1 // 行号从1开始
if (!row.startPosition || !row.endPosition) {
this.$message.error(`${rowIndex}行:请填写开始位置和结束位置`)
return
}
if (parseInt(row.startPosition) > parseInt(row.endPosition)) {
console.log(row, row.startPosition, row.endPosition)
this.$message.error(`${rowIndex}行:开始位置必须小于结束位置`)
return
}
if (!row.position || row.position.length === 0) {
this.$message.error(`${rowIndex}行:请选择断面位置`)
return
}
// if (!row.plateSurface || row.plateSurface.length === 0) {
// this.$message.error(`第${rowIndex}行:请选择上下板面`)
// return
// }
if (!row.defectCode) {
this.$message.error(`${rowIndex}行:请选择缺陷代码`)
return
}
if (!row.degree) {
this.$message.error(`${rowIndex}行:请选择程度`)
return
}
}
if (nonEmptyRows.length === 0) {
this.$message.info('没有需要保存的数据')
return
}
this.buttonLoading = true
// 批量请求
const requests = nonEmptyRows.map(row => {
const saveData = {
...row,
coilId: this.coilId
}
if (row.abnormalId) {
// 更新操作
return updateCoilAbnormal(saveData)
} else {
// 新增操作
return addCoilAbnormal(saveData)
}
})
// 执行所有请求
Promise.all(requests).then(responses => {
this.$message.success('批量保存成功')
// 刷新列表
this.loadAbnormalList()
}).catch(error => {
console.error('批量保存失败:', error)
this.$message.error('批量保存失败: ' + (error.message || error))
}).finally(() => {
this.buttonLoading = false
})
},
handleDelete(row) {
if (!row.abnormalId) {
// 如果没有 abnormalId直接从列表中移除
const index = this.abnormalList.indexOf(row)
if (index > -1) {
this.abnormalList.splice(index, 1)
// 确保列表始终有10个条目
if (this.abnormalList.length < 10) {
const emptyRows = this.generateEmptyRows(10 - this.abnormalList.length)
this.abnormalList = [...this.abnormalList, ...emptyRows]
}
this.$message.success('空行已移除')
}
return
}
this.$modal.confirm('确认删除异常记录吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.buttonLoading = true
delCoilAbnormal(row.abnormalId).then(response => {
this.$message.success('异常记录删除成功')
this.loadAbnormalList()
}).catch(error => {
console.error('异常记录删除失败:', error)
this.$message.error('异常记录删除失败: ' + (error.message || error))
}).finally(() => {
this.buttonLoading = false
})
})
},
refreshAbnormalList() {
this.loadAbnormalList()
},
handleSingleCheckboxChange(val, value, field, row) {
if (val) {
// 选中状态,设置值
row[field] = value
} else {
// 取消选中状态,清空值
if (row[field] === value) {
row[field] = null
}
}
},
handleSingleCheckboxClick(value, field, row) {
// 实现排他单选:如果点击的是当前选中的值,则取消选中;否则选中新值
if (row[field] === value) {
row[field] = null
} else {
row[field] = value
}
},
handleRadioClick(currentValue, value, field, row) {
// 实现 radio 的取消选择功能:如果点击的是当前选中的值,则取消选中
if (currentValue === value) {
// 使用 $nextTick 确保在 radio 组件更新后再清空值
this.$nextTick(() => {
row[field] = null
})
}
},
clearRowData(row) {
// 清空一行的所有数据
row.remark = null
row.startPosition = 0
row.endPosition = 0
row.position = ''
row.plateSurface = ''
row.mainMark = 0
row.defectCode = null
row.degree = null
row.attachmentFiles = null
}
}
}
</script>
<style scoped>
.exception-container {
display: flex;
flex-direction: column;
gap: 20px;
overflow-y: auto;
}
.exception-section {
background-color: #fafafa;
border-radius: 4px;
padding: 16px;
border: 1px solid #e4e7ed;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #e4e7ed;
}
.section-title {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #303133;
}
.exception-section:first-child {
flex: 1;
min-height: 200px;
}
.exception-section:last-child {
flex-shrink: 0;
}
.form-container {
display: flex;
flex-direction: column;
gap: 16px;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 12px 16px;
background-color: #fafafa;
border-top: 1px solid #e4e7ed;
border-radius: 0 0 4px 4px;
margin-top: -16px;
margin-left: -16px;
margin-right: -16px;
margin-bottom: -16px;
}
.radio-single {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.radio-single .el-radio {
margin-right: 10px;
}
.radio-single .el-button {
margin-left: 10px;
}
</style>

View File

View File

@@ -0,0 +1,372 @@
<template>
<div class="app-container">
<el-row :gutter="20">
<el-col :span="24">
<el-card shadow="never">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="成品卷号" prop="exitMatId">
<el-input
v-model="queryParams.exitMatId"
placeholder="请输入成品卷号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="来料卷号" prop="entryMatId">
<el-input
v-model="queryParams.entryMatId"
placeholder="请输入来料卷号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="计划号" prop="planNo">
<el-input
v-model="queryParams.planNo"
placeholder="请输入计划号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="班号" prop="groupNo">
<el-input
v-model="queryParams.groupNo"
placeholder="请输入班号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="组号" prop="shiftNo">
<el-input
v-model="queryParams.shiftNo"
placeholder="请输入组号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['mill:actual:export']"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="actualList" @row-click="handleRowClick" highlight-current-row>
<el-table-column label="成品卷号" align="center" prop="exitMatId" />
<el-table-column label="来料卷号" align="center" prop="entryMatId" />
<el-table-column label="计划号" align="center" prop="planNo" />
<el-table-column label="产品类型" align="center" prop="prodCode" />
<el-table-column label="班号" align="center" prop="groupNo" />
<el-table-column label="组号" align="center" prop="shiftNo" />
<el-table-column label="钢种" align="center" prop="steelGrade" />
<el-table-column label="成品厚度" align="center" prop="exitThickness" />
<el-table-column label="成品宽度" align="center" prop="exitWidth" />
<el-table-column label="实际重量" align="center" prop="actualWeight" />
<el-table-column label="上线时间" align="center" prop="onlineTime" width="120">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.onlineTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<!-- <el-table-column label="状态" align="center" prop="status" /> -->
<el-table-column label="操作" align="center" width="100">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click.stop="handleExceptionManager(scope.row)">异常管理</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="24">
<el-card shadow="never" v-if="currentRow">
<div slot="header" class="clearfix">
<span style="font-size: 16px; font-weight: bold;">明细信息</span>
</div>
<el-descriptions :column="4" border>
<el-descriptions-item label="成品卷号">{{ currentRow.exitMatId }}</el-descriptions-item>
<el-descriptions-item label="来料卷号">{{ currentRow.entryMatId }}</el-descriptions-item>
<el-descriptions-item label="分切数">{{ currentRow.subId }}</el-descriptions-item>
<el-descriptions-item label="开始位置">{{ currentRow.startPosition }}</el-descriptions-item>
<el-descriptions-item label="结束位置">{{ currentRow.endPosition }}</el-descriptions-item>
<el-descriptions-item label="计划id">{{ currentRow.planId }}</el-descriptions-item>
<el-descriptions-item label="计划号">{{ currentRow.planNo }}</el-descriptions-item>
<el-descriptions-item label="产品类型">{{ currentRow.prodCode }}</el-descriptions-item>
<el-descriptions-item label="班号">{{ currentRow.groupNo }}</el-descriptions-item>
<el-descriptions-item label="组号">{{ currentRow.shiftNo }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ currentRow.status }}</el-descriptions-item>
<el-descriptions-item label="钢种">{{ currentRow.steelGrade }}</el-descriptions-item>
<el-descriptions-item label="来料厚度">{{ currentRow.entryThick }}</el-descriptions-item>
<el-descriptions-item label="来料宽度">{{ currentRow.entryWidth }}</el-descriptions-item>
<el-descriptions-item label="来料长度">{{ currentRow.entryLength }}</el-descriptions-item>
<el-descriptions-item label="来料重量">{{ currentRow.entryWeight }}</el-descriptions-item>
<el-descriptions-item label="上表面镀锌量">{{ currentRow.weightTop }}</el-descriptions-item>
<el-descriptions-item label="下表面镀锌量">{{ currentRow.weightBottom }}</el-descriptions-item>
<el-descriptions-item label="成品长度">{{ currentRow.exitLength }}</el-descriptions-item>
<el-descriptions-item label="成品带涂料重量">{{ currentRow.exitNetWeight }}</el-descriptions-item>
<el-descriptions-item label="理论重量">{{ currentRow.theoryWeight }}</el-descriptions-item>
<el-descriptions-item label="实际重量">{{ currentRow.actualWeight }}</el-descriptions-item>
<el-descriptions-item label="成品外径">{{ currentRow.exitOuterDiameter }}</el-descriptions-item>
<el-descriptions-item label="成品厚度">{{ currentRow.exitThickness }}</el-descriptions-item>
<el-descriptions-item label="成品宽度">{{ currentRow.exitWidth }}</el-descriptions-item>
<el-descriptions-item label="客户">{{ currentRow.customer }}</el-descriptions-item>
<el-descriptions-item label="上线时间">{{ parseTime(currentRow.onlineTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ parseTime(currentRow.startTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ parseTime(currentRow.endTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
<el-descriptions-item label="机组号">{{ currentRow.unitCode }}</el-descriptions-item>
<el-descriptions-item label="工序号">{{ currentRow.processCode }}</el-descriptions-item>
<el-descriptions-item label="是否尾卷">{{ currentRow.lastFlag === 1 ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="是否分卷">{{ currentRow.separateFlag === 1 ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="计划来源">{{ currentRow.planOrigin }}</el-descriptions-item>
<el-descriptions-item label="锌层厚度">{{ currentRow.zincCoatingThickness }}</el-descriptions-item>
<el-descriptions-item label="入场钢卷号">{{ currentRow.enterCoilNo }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="4">{{ currentRow.remark }}</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card shadow="never" v-else>
<div style="text-align: center; padding: 40px; color: #909399;">
请点击表格行查看明细信息
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="24">
<el-card shadow="never" v-if="currentRow">
<div slot="header" class="clearfix">
<span style="font-size: 16px; font-weight: bold;">异常记录</span>
<div style="float: right;">
<el-button type="success" plain icon="el-icon-plus" size="mini" @click="handleAbnormalAdd">添加异常</el-button>
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleAbnormalExport">导出</el-button>
</div>
</div>
<AbnormalTable
ref="abnormalTable"
:list="abnormalList"
:editable="true"
:showCoil="false"
@update="handleAbnormalUpdate"
@delete="handleAbnormalDelete"
/>
</el-card>
<el-card shadow="never" v-else>
<div style="text-align: center; padding: 40px; color: #909399;">
请点击表格行查看异常记录
</div>
</el-card>
</el-col>
</el-row>
<el-dialog :title="exceptionTitle" :visible.sync="exceptionOpen" width="1200px" append-to-body fullscreen>
<ExceptionManager :current-coil-no="exceptionCoilNo" :row-data="exceptionRowData" />
</el-dialog>
<el-dialog :title="abnormalTitle" :visible.sync="abnormalOpen" width="700px" append-to-body @closed="handleAbnormalDialogClosed">
<AbnormalForm ref="abnormalFormRef" v-model="abnormalFormData" />
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitAbnormalForm"> </el-button>
<el-button @click="abnormalOpen = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listActual } from "@/api/mill/actual";
import { listCoilAbnormal, addCoilAbnormal, updateCoilAbnormal, delCoilAbnormal } from "@/api/mill/coilAbnormal";
import AbnormalTable from "./components/AbnormalTable";
import AbnormalForm from "./components/AbnormalForm";
import ExceptionManager from "./components/ExceptionManager";
export default {
name: "Actual",
components: {
AbnormalTable,
AbnormalForm,
ExceptionManager
},
data() {
return {
loading: true,
showSearch: true,
total: 0,
actualList: [],
currentRow: null,
abnormalList: [],
abnormalOpen: false,
abnormalTitle: "",
abnormalFormData: {},
exceptionOpen: false,
exceptionTitle: "",
exceptionCoilNo: "",
exceptionRowData: null,
queryParams: {
pageNum: 1,
pageSize: 10,
exitMatId: null,
entryMatId: null,
subId: null,
startPosition: null,
endPosition: null,
planId: null,
planNo: null,
prodCode: null,
groupNo: null,
shiftNo: null,
status: null,
steelGrade: null,
entryThick: null,
entryWidth: null,
entryLength: null,
entryWeight: null,
weightTop: null,
weightBottom: null,
exitLength: null,
exitNetWeight: null,
theoryWeight: null,
actualWeight: null,
exitOuterDiameter: null,
exitThickness: null,
exitWidth: null,
customer: null,
onlineTime: null,
startTime: null,
endTime: null,
unitCode: null,
processCode: null,
planOrigin: null,
zincCoatingThickness: null,
enterCoilNo: null,
}
};
},
created() {
this.getList();
},
methods: {
getList() {
this.loading = true;
listActual(this.queryParams).then(response => {
this.actualList = response.rows;
this.total = response.total;
this.loading = false;
});
},
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
handleRowClick(row) {
this.currentRow = row;
this.loadAbnormalList();
},
handleExceptionManager(row) {
this.exceptionCoilNo = row.exitMatId;
this.exceptionRowData = row;
this.exceptionTitle = "异常管理 - " + row.exitMatId;
this.exceptionOpen = true;
},
handleExport() {
this.download('mill/actual/export', {
...this.queryParams
}, `actual_${new Date().getTime()}.xlsx`)
},
loadAbnormalList() {
if (!this.currentRow || !this.currentRow.exitMatId) {
this.abnormalList = [];
return;
}
listCoilAbnormal({ currentCoilNo: this.currentRow.exitMatId }).then(response => {
this.abnormalList = response.rows || [];
});
},
handleAbnormalAdd() {
this.abnormalFormData = {
currentCoilNo: this.currentRow.exitMatId,
position: '',
plateSurface: '',
startPosition: undefined,
endPosition: undefined,
mainMark: 0,
defectCode: undefined,
degree: undefined,
remark: undefined,
attachmentFiles: undefined
};
this.abnormalTitle = "添加异常记录";
this.abnormalOpen = true;
},
handleAbnormalUpdate(row) {
this.abnormalFormData = { ...row };
this.abnormalTitle = "修改异常记录";
this.abnormalOpen = true;
},
submitAbnormalForm() {
const formRef = this.$refs.abnormalFormRef;
if (!formRef || !formRef.validate) {
this.$message.error("表单组件未就绪");
return;
}
formRef.validate(valid => {
if (valid) {
const data = { ...this.abnormalFormData };
if (Array.isArray(data.position)) data.position = data.position.join(",");
if (Array.isArray(data.plateSurface)) data.plateSurface = data.plateSurface.join(",");
data.currentCoilNo = this.currentRow.exitMatId;
const request = data.abnormalId ? updateCoilAbnormal(data) : addCoilAbnormal(data);
request.then(() => {
this.$modal.msgSuccess(data.abnormalId ? "修改成功" : "新增成功");
this.abnormalOpen = false;
this.loadAbnormalList();
});
}
});
},
handleAbnormalDelete(row) {
this.$modal.confirm('是否确认删除该异常记录?').then(() => {
return delCoilAbnormal(row.abnormalId);
}).then(() => {
this.loadAbnormalList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {});
},
handleAbnormalExport() {
this.download('wms/coilAbnormal/export', {
coilId: this.currentRow.exitMatId
}, `abnormal_${new Date().getTime()}.xlsx`)
},
handleAbnormalDialogClosed() {
if (this.$refs.abnormalFormRef && this.$refs.abnormalFormRef.resetFields) {
this.$refs.abnormalFormRef.resetFields();
}
}
}
};
</script>

View File

@@ -0,0 +1,22 @@
/**
* 从 URL query.lineId 读取产线 ID供各轧辊页面共用。
* 组件覆盖 onLineResolved() 执行初始化加载。
*
* 菜单路径示例:/mes/roll/overview?lineId=1
*/
export default {
computed: {
lineId() {
const v = this.$route.query.lineId
return v != null ? Number(v) : null
}
},
created() {
this.onLineResolved()
},
methods: {
onLineResolved() {}
}
}

View File

@@ -0,0 +1,932 @@
<template>
<div class="app-container working-roll-page">
<!-- 顶部行作业辊一览2/3+ 可用轧辊1/3 -->
<div class="top-row">
<!-- 作业辊一览 -->
<el-card shadow="never" class="roll-table-card top-row__main">
<div slot="header" class="card-header">
<span class="card-title"><i class="el-icon-s-grid" /> 作业辊一览</span>
<span class="header-meta" v-if="current['1#'] && current['1#'].changeTime">
1# 末次换辊{{ current['1#'].changeTime }}
</span>
<span class="header-meta" v-if="current['2#'] && current['2#'].changeTime">
&nbsp;·&nbsp;2# 末次换辊{{ current['2#'].changeTime }}
</span>
<span style="margin-left:auto;display:flex;align-items:center;gap:6px;flex-wrap:wrap">
<el-button size="mini" icon="el-icon-refresh-right" @click="handleOpenChange('1#')">1# 换辊</el-button>
<el-button size="mini" icon="el-icon-refresh-right" @click="handleOpenChange('2#')">2# 换辊</el-button>
<el-divider direction="vertical" />
<el-button type="success" size="mini" icon="el-icon-plus" @click="handleAddStandby('1#')">1# 添加下批</el-button>
<el-button type="danger" size="mini" icon="el-icon-delete" @click="handleClearStandby('1#')"
:disabled="!(standbyList['1#'] && standbyList['1#'].length)">1# 清空</el-button>
<el-button type="success" size="mini" icon="el-icon-plus" @click="handleAddStandby('2#')">2# 添加下批</el-button>
<el-button type="danger" size="mini" icon="el-icon-delete" @click="handleClearStandby('2#')"
:disabled="!(standbyList['2#'] && standbyList['2#'].length)">2# 清空</el-button>
</span>
</div>
<el-table
ref="mainTable"
:data="mergedRows"
v-loading="loadingCurrent['1#'] || loadingCurrent['2#'] || loadingStandby['1#'] || loadingStandby['2#']"
:row-class-name="rowClass"
:span-method="mergeParamCol"
border
size="small"
style="width:100%"
>
<el-table-column label="参数" width="126" align="left">
<template slot-scope="scope">
<span :class="['param-label', scope.row.group]">{{ scope.row.label }}</span>
</template>
</el-table-column>
<el-table-column label="当前在机轧辊" align="center">
<el-table-column label="1#" align="center" min-width="88">
<template slot-scope="scope">
<param-cell :data="scope.row.cur1" />
</template>
</el-table-column>
<el-table-column label="2#" align="center" min-width="88">
<template slot-scope="scope">
<param-cell :data="scope.row.cur2" />
</template>
</el-table-column>
</el-table-column>
<el-table-column label="下批轧辊" align="center">
<el-table-column label="1#" align="center" min-width="88">
<template slot-scope="scope">
<div
:class="['sb-cell',
scope.row.sb1 && scope.row.sb1.standbyId ? 'sb-cell--del' :
scope.row.rollType ? 'sb-cell--add' : '']"
@click="handleSbCellClick(scope.row.sb1, '1#', scope.row.rollType, scope.row.position)"
>
<param-cell :data="scope.row.sb1" />
</div>
</template>
</el-table-column>
<el-table-column label="2#" align="center" min-width="88">
<template slot-scope="scope">
<div
:class="['sb-cell',
scope.row.sb2 && scope.row.sb2.standbyId ? 'sb-cell--del' :
scope.row.rollType ? 'sb-cell--add' : '']"
@click="handleSbCellClick(scope.row.sb2, '2#', scope.row.rollType, scope.row.position)"
>
<param-cell :data="scope.row.sb2" />
</div>
</template>
</el-table-column>
</el-table-column>
</el-table>
</el-card>
<!-- 右侧可用辊 + 工作绩效纵向叠放 -->
<div class="top-row__aside aside-col">
<!-- 可用轧辊离线 -->
<el-card shadow="never" class="roll-table-card aside-panel">
<div slot="header" class="card-header">
<span class="card-title"><i class="el-icon-files" /> 可用轧辊离线</span>
<el-button size="mini" icon="el-icon-refresh" style="margin-left:auto" @click="loadOfflineRolls">刷新</el-button>
</div>
<el-table v-loading="offlineLoading" :data="offlineRolls" size="small" :height="asideHalfH" style="width:100%">
<el-table-column label="辊型" align="center" prop="rollType" width="52">
<template slot-scope="scope">
<el-tag size="mini" :type="scope.row.rollType === 'WR' ? 'primary' : 'warning'">{{ scope.row.rollType }}</el-tag>
</template>
</el-table-column>
<el-table-column label="轧辊编号" align="center" prop="rollNo" min-width="90" show-overflow-tooltip />
<el-table-column label="辊径(mm)" align="center" width="76">
<template slot-scope="scope">{{ scope.row.currentDia != null ? scope.row.currentDia : scope.row.initialDia }}</template>
</el-table-column>
<el-table-column label="粗糙度" align="center" prop="roughness" width="64" />
</el-table>
</el-card>
<!-- 工作绩效 -->
<el-card shadow="never" class="roll-table-card aside-panel">
<div slot="header" class="card-header">
<span class="card-title"><i class="el-icon-data-analysis" /> 工作绩效实时</span>
<el-button size="mini" icon="el-icon-refresh" style="margin-left:auto" @click="loadRollPerformance">刷新</el-button>
</div>
<el-table v-loading="perfLoading" :data="perfRows" size="small" :height="asideHalfH" border style="width:100%">
<el-table-column label="辊位" align="center" prop="label" width="70" />
<el-table-column label="1# 机架" align="center" min-width="1">
<template slot-scope="scope">
<perf-cell :d="scope.row['1#']" />
</template>
</el-table-column>
<el-table-column label="2# 机架" align="center" min-width="1">
<template slot-scope="scope">
<perf-cell :d="scope.row['2#']" />
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</div>
<!-- 换辊历史 -->
<el-card shadow="never" class="roll-table-card" style="margin-top:16px">
<div slot="header" class="card-header">
<span class="card-title"><i class="el-icon-document" /> 换辊历史</span>
<el-form :inline="true" size="small" style="margin-left:auto;margin-bottom:0">
<el-form-item label="机架" style="margin-bottom:0">
<el-select v-model="historyQuery.standNo" placeholder="全部" clearable style="width:100px" @change="loadHistory">
<el-option label="1# 机架" value="1#" />
<el-option label="2# 机架" value="2#" />
</el-select>
</el-form-item>
<el-form-item label="类型" style="margin-bottom:0">
<el-select v-model="historyQuery.changeType" placeholder="全部" clearable style="width:110px" @change="loadHistory">
<el-option label="计划换辊" value="计划换辊" />
<el-option label="紧急换辊" value="紧急换辊" />
</el-select>
</el-form-item>
<el-form-item label="时间起" style="margin-bottom:0">
<el-date-picker v-model="historyQuery.changeTime" type="datetime"
value-format="yyyy-MM-dd HH:mm:ss" placeholder="开始时间"
style="width:160px" @change="loadHistory" />
</el-form-item>
</el-form>
</div>
<KLPTable v-loading="historyLoading" :data="historyList">
<el-table-column label="换辊编号" align="center" prop="changeNo" width="130" />
<el-table-column label="机架" align="center" prop="standNo" width="80" />
<el-table-column label="换辊时间" align="center" prop="changeTime" width="160" />
<el-table-column label="类型" align="center" prop="changeType" width="100">
<template slot-scope="scope">
<el-tag size="small" :type="scope.row.changeType === '紧急换辊' ? 'danger' : ''">{{ scope.row.changeType }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作人" align="center" prop="operator" width="80" />
<el-table-column label="换辊辊组" align="left" min-width="160">
<template slot-scope="scope">
<div class="roll-group-cell">
<span v-if="scope.row.upperBrNo" class="rg-item"><b>上BR</b> {{ scope.row.upperBrNo }}<em v-if="scope.row.upperBrDia"> φ{{ scope.row.upperBrDia }}</em></span>
<span v-if="scope.row.upperWrNo" class="rg-item"><b>上WR</b> {{ scope.row.upperWrNo }}<em v-if="scope.row.upperWrDia"> φ{{ scope.row.upperWrDia }}</em></span>
<span v-if="scope.row.lowerWrNo" class="rg-item"><b>下WR</b> {{ scope.row.lowerWrNo }}<em v-if="scope.row.lowerWrDia"> φ{{ scope.row.lowerWrDia }}</em></span>
<span v-if="scope.row.lowerBrNo" class="rg-item"><b>下BR</b> {{ scope.row.lowerBrNo }}<em v-if="scope.row.lowerBrDia"> φ{{ scope.row.lowerBrDia }}</em></span>
<span v-if="!scope.row.upperBrNo && !scope.row.upperWrNo && !scope.row.lowerWrNo && !scope.row.lowerBrNo" style="color:#c0c4cc"></span>
</div>
</template>
</el-table-column>
<el-table-column label="工作长度(m)" align="center" prop="workLength" width="96">
<template slot-scope="scope">
<span v-if="scope.row.workLength != null">{{ scope.row.workLength }}</span>
<span v-else style="color:#c0c4cc"></span>
</template>
</el-table-column>
<el-table-column label="过卷数" align="center" prop="coilCount" width="72">
<template slot-scope="scope">
<span v-if="scope.row.coilCount != null">{{ scope.row.coilCount }}</span>
<span v-else style="color:#c0c4cc"></span>
</template>
</el-table-column>
<el-table-column label="过卷重量" align="center" prop="totalWeight" width="90">
<template slot-scope="scope">
<span v-if="scope.row.totalWeight != null">{{ scope.row.totalWeight }}</span>
<span v-else style="color:#c0c4cc"></span>
</template>
</el-table-column>
<el-table-column label="备注" align="left" prop="remark" min-width="100" show-overflow-tooltip />
<el-table-column label="操作" align="center" width="110" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEditHistory(scope.row)">补录</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" style="color:#c5221f"
@click="handleDeleteHistory(scope.row)">删除</el-button>
</template>
</el-table-column>
</KLPTable>
<pagination
v-show="historyTotal > 0"
:total="historyTotal"
:page.sync="historyQuery.pageNum"
:limit.sync="historyQuery.pageSize"
@pagination="loadHistory"
/>
</el-card>
<!-- 换辊确认弹窗纯展示无需填写 -->
<el-dialog :title="changeForm.standNo + ' 机架 — 确认换辊'" :visible.sync="changeOpen" width="420px" append-to-body @close="resetChangeForm">
<div class="roll-preview">
<div class="roll-preview__title">以下轧辊将被换入</div>
<div class="roll-preview__item">
<span class="rp-label">上支撑辊</span>
<span class="rp-val">{{ changeForm.upperBrNo || '—' }}</span>
<span class="rp-dia" v-if="changeForm.upperBrDia">φ{{ changeForm.upperBrDia }} mm</span>
</div>
<div class="roll-preview__item">
<span class="rp-label">上工作辊</span>
<span class="rp-val">{{ changeForm.upperWrNo || '—' }}</span>
<span class="rp-dia" v-if="changeForm.upperWrDia">φ{{ changeForm.upperWrDia }} mm</span>
</div>
<div class="roll-preview__item">
<span class="rp-label">下工作辊</span>
<span class="rp-val">{{ changeForm.lowerWrNo || '—' }}</span>
<span class="rp-dia" v-if="changeForm.lowerWrDia">φ{{ changeForm.lowerWrDia }} mm</span>
</div>
<div class="roll-preview__item">
<span class="rp-label">下支撑辊</span>
<span class="rp-val">{{ changeForm.lowerBrNo || '—' }}</span>
<span class="rp-dia" v-if="changeForm.lowerBrDia">φ{{ changeForm.lowerBrDia }} mm</span>
</div>
</div>
<div class="roll-preview__tip">
换辊时间和操作人将由系统自动记录
</div>
<div slot="footer">
<el-button type="primary" :loading="changeSubmitting" @click="submitChange">确认换辊</el-button>
<el-button @click="changeOpen = false"> </el-button>
</div>
</el-dialog>
<!-- 换辊历史补录/编辑对话框 -->
<el-dialog title="换辊记录补录" :visible.sync="historyEditOpen" width="560px" append-to-body @close="historyEditForm = {}">
<el-form ref="historyEditForm" :model="historyEditForm" label-width="100px" size="small">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="机架">
<el-input :value="historyEditForm.standNo" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="换辊类型">
<el-select v-model="historyEditForm.changeType" style="width:100%">
<el-option label="计划换辊" value="计划换辊" />
<el-option label="紧急换辊" value="紧急换辊" />
<el-option label="三级换辊" value="三级换辊" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="换辊时间">
<el-date-picker v-model="historyEditForm.changeTime" type="datetime"
value-format="yyyy-MM-dd HH:mm:ss" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="操作人">
<el-input v-model="historyEditForm.operator" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">工作辊</el-divider>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="上工作辊"><el-input v-model="historyEditForm.upperWrNo" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="上工作辊径"><el-input-number v-model="historyEditForm.upperWrDia" :precision="2" :min="0" style="width:100%" /></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="下工作辊"><el-input v-model="historyEditForm.lowerWrNo" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="下工作辊径"><el-input-number v-model="historyEditForm.lowerWrDia" :precision="2" :min="0" style="width:100%" /></el-form-item></el-col>
</el-row>
<el-divider content-position="left">支撑辊</el-divider>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="上支撑辊"><el-input v-model="historyEditForm.upperBrNo" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="上支撑辊径"><el-input-number v-model="historyEditForm.upperBrDia" :precision="2" :min="0" style="width:100%" /></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="下支撑辊"><el-input v-model="historyEditForm.lowerBrNo" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="下支撑辊径"><el-input-number v-model="historyEditForm.lowerBrDia" :precision="2" :min="0" style="width:100%" /></el-form-item></el-col>
</el-row>
<el-form-item label="备注">
<el-input v-model="historyEditForm.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button type="primary" @click="submitHistoryEdit"> </el-button>
<el-button @click="historyEditOpen = false"> </el-button>
</div>
</el-dialog>
<!-- 添加下批轧辊对话框 -->
<el-dialog title="添加下批轧辊" :visible.sync="standbyOpen" width="460px" append-to-body @close="resetStandbyForm">
<el-form ref="standbyForm" :model="standbyForm" :rules="standbyRules" label-width="100px">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="机架" prop="standNo">
<el-select v-model="standbyForm.standNo" style="width:100%" :disabled="standbyFromCell">
<el-option label="1# 机架" value="1#" />
<el-option label="2# 机架" value="2#" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="辊型" prop="rollType">
<el-input
v-if="standbyFromCell"
:value="standbyForm.rollType === 'WR' ? '工作辊 WR' : '支撑辊 BR'"
readonly style="width:100%"
/>
<el-select v-else v-model="standbyForm.rollType" placeholder="请选择" style="width:100%"
@change="handleStandbyRollTypeChange">
<el-option label="工作辊 WR" value="WR" />
<el-option label="支撑辊 BR" value="BR" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="辊位" prop="position">
<el-input
v-if="standbyFromCell"
:value="standbyForm.position === 'UP' ? '上辊' : '下辊'"
readonly style="width:100%"
/>
<el-select v-else v-model="standbyForm.position" placeholder="请选择" style="width:100%">
<el-option label="上辊" value="UP" />
<el-option label="下辊" value="DOWN" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="轧辊编号" prop="rollNo">
<el-select v-model="standbyForm.rollNo" placeholder="请选择" filterable style="width:100%">
<el-option
v-for="no in standbyForm.rollType === 'WR' ? wrOptions : brOptions"
:key="no" :label="no" :value="no"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="就绪时间">
<el-date-picker v-model="standbyForm.readyTime" type="datetime"
value-format="yyyy-MM-dd HH:mm:ss" placeholder="默认当前时间" style="width:100%" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="standbyForm.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button type="primary" @click="submitStandby"> </el-button>
<el-button @click="standbyOpen = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getCurrentRolls, listRollChange, addRollChange, updateRollChange, delRollChange, getRollPerformance } from '@/api/mill/rollChange'
import { listRollStandby, addRollStandby, delRollStandby, clearRollStandby } from '@/api/mill/rollStandby'
import { listRollOptions, listRollInfo } from '@/api/mill/rollInfo'
import rollLineMixin from '../rollLineMixin'
const ParamCell = {
name: 'ParamCell',
props: { data: { type: Object, default: null } },
render(h) {
const d = this.data
if (!d || d.val == null || d.val === '') return h('span', { class: 'cell-empty' }, '—')
return h('span', { class: 'cell-main' }, String(d.val))
}
}
/** 绩效单元格:显示辊号 + 三项统计 */
const PerfCell = {
name: 'PerfCell',
props: { d: { type: Object, default: null } },
render(h) {
const d = this.d
if (!d || !d.rollNo) return h('span', { class: 'cell-empty' }, '—')
return h('div', { class: 'perf-cell' }, [
h('div', { class: 'perf-roll' }, d.rollNo),
h('div', { class: 'perf-stat' }, `${d.workLength != null ? d.workLength + 'm' : '—'} · ${d.coilCount != null ? d.coilCount + '卷' : '—'} · ${d.totalWeight != null ? d.totalWeight : '—'}`)
])
}
}
export default {
name: 'WorkingRoll',
components: { ParamCell, PerfCell },
mixins: [rollLineMixin],
data() {
return {
current: { '1#': {}, '2#': {} },
loadingCurrent: { '1#': false, '2#': false },
standbyList: { '1#': [], '2#': [] },
loadingStandby: { '1#': false, '2#': false },
rollInfoMap: {},
historyLoading: false,
historyList: [],
historyTotal: 0,
historyQuery: { pageNum: 1, pageSize: 15, standNo: undefined, changeType: undefined, changeTime: undefined },
wrOptions: [],
brOptions: [],
historyEditOpen: false,
historyEditForm: {},
offlineRolls: [],
offlineLoading: false,
perfData: {},
perfLoading: false,
asideHalfH: 200,
changeOpen: false,
changeSubmitting: false,
changeForm: this.getDefaultChangeForm(),
standbyOpen: false,
standbyForm: this.getDefaultStandbyForm(),
standbyFromCell: false,
standbyRules: {
standNo: [{ required: true, message: '请选择机架', trigger: 'change' }],
rollType: [{ required: true, message: '请选择辊型', trigger: 'change' }],
position: [{ required: true, message: '请选择辊位', trigger: 'change' }],
rollNo: [{ required: true, message: '请选择轧辊编号', trigger: 'change' }]
}
}
},
computed: {
mergedRows() {
const c1 = this.current['1#'] || {}
const c2 = this.current['2#'] || {}
const ri = this.rollInfoMap
// 当前辊:值来自换辊记录,凸度/粗糙度/材质从 rollInfoMap 查
const cv = (val, rollNo) => ({
val: (val == null || val === '') ? null : val,
sub: rollNo || null
})
const rv = (rollNo, field) => {
const info = rollNo ? ri[rollNo] : null
const val = info ? info[field] : null
return { val: (val == null || val === '') ? null : val, sub: rollNo || null }
}
// 下批辊:优先取 standby 记录自身字段,为空则从 rollInfoMap 读diameter 对应 currentDia
const sv = (standNo, rollType, pos, field) => {
const item = (this.standbyList[standNo] || []).find(
i => i.rollType === rollType && i.position === pos
)
if (!item) return { val: null, sub: null }
let val = item[field]
if ((val == null || val === '') && item.rollNo) {
const info = ri[item.rollNo]
if (info) {
val = field === 'diameter'
? (info.currentDia != null ? info.currentDia : info.initialDia)
: info[field]
}
}
return { val: (val == null || val === '') ? null : val, sub: item.rollNo, standbyId: item.standbyId }
}
const sm = (standNo, rollType, pos) => {
const item = (this.standbyList[standNo] || []).find(
i => i.rollType === rollType && i.position === pos
)
if (!item || !item.rollNo) return { val: null, sub: null }
const info = ri[item.rollNo]
return {
val: info ? (info.material || null) : null,
sub: item.rollNo,
standbyId: item.standbyId
}
}
return [
{
label: '上支承辊直径(mm)', group: 'br', rollType: 'BR', position: 'UP',
cur1: cv(c1.upperBrDia, c1.upperBrNo), cur2: cv(c2.upperBrDia, c2.upperBrNo),
sb1: sv('1#', 'BR', 'UP', 'diameter'), sb2: sv('2#', 'BR', 'UP', 'diameter')
},
{
label: '上工作辊直径(mm)', group: 'wr', rollType: 'WR', position: 'UP',
cur1: cv(c1.upperWrDia, c1.upperWrNo), cur2: cv(c2.upperWrDia, c2.upperWrNo),
sb1: sv('1#', 'WR', 'UP', 'diameter'), sb2: sv('2#', 'WR', 'UP', 'diameter')
},
{
label: '上工作辊凸度', group: 'wr', rollType: 'WR', position: 'UP',
cur1: rv(c1.upperWrNo, 'crown'), cur2: rv(c2.upperWrNo, 'crown'),
sb1: sv('1#', 'WR', 'UP', 'crown'), sb2: sv('2#', 'WR', 'UP', 'crown')
},
{
label: '上工作辊粗糙度(μm)', group: 'wr', rollType: 'WR', position: 'UP',
cur1: rv(c1.upperWrNo, 'roughness'), cur2: rv(c2.upperWrNo, 'roughness'),
sb1: sv('1#', 'WR', 'UP', 'roughness'), sb2: sv('2#', 'WR', 'UP', 'roughness')
},
{
label: '工作辊类型', group: 'wr', rollType: 'WR', position: 'UP',
cur1: rv(c1.upperWrNo, 'material'), cur2: rv(c2.upperWrNo, 'material'),
sb1: sm('1#', 'WR', 'UP'), sb2: sm('2#', 'WR', 'UP')
},
{
label: '下工作辊直径(mm)', group: 'wr', rollType: 'WR', position: 'DOWN',
cur1: cv(c1.lowerWrDia, c1.lowerWrNo), cur2: cv(c2.lowerWrDia, c2.lowerWrNo),
sb1: sv('1#', 'WR', 'DOWN', 'diameter'), sb2: sv('2#', 'WR', 'DOWN', 'diameter')
},
{
label: '下工作辊凸度', group: 'wr', rollType: 'WR', position: 'DOWN',
cur1: rv(c1.lowerWrNo, 'crown'), cur2: rv(c2.lowerWrNo, 'crown'),
sb1: sv('1#', 'WR', 'DOWN', 'crown'), sb2: sv('2#', 'WR', 'DOWN', 'crown')
},
{
label: '下工作辊粗糙度(μm)', group: 'wr', rollType: 'WR', position: 'DOWN',
cur1: rv(c1.lowerWrNo, 'roughness'), cur2: rv(c2.lowerWrNo, 'roughness'),
sb1: sv('1#', 'WR', 'DOWN', 'roughness'), sb2: sv('2#', 'WR', 'DOWN', 'roughness')
},
{
label: '下支承辊直径(mm)', group: 'br', rollType: 'BR', position: 'DOWN',
cur1: cv(c1.lowerBrDia, c1.lowerBrNo), cur2: cv(c2.lowerBrDia, c2.lowerBrNo),
sb1: sv('1#', 'BR', 'DOWN', 'diameter'), sb2: sv('2#', 'BR', 'DOWN', 'diameter')
},
{
label: '轧线设定(mm)', group: 'pl', rollType: null, position: null,
cur1: { val: null, sub: null }, cur2: { val: null, sub: null },
sb1: { val: null, sub: null }, sb2: { val: null, sub: null }
},
{
label: '轧线位置(mm)', group: 'pl', rollType: null, position: null,
cur1: { val: null, sub: null }, cur2: { val: null, sub: null },
sb1: { val: null, sub: null }, sb2: { val: null, sub: null }
},
]
},
/** 工作绩效表数据行4 辊位 × {label, 1#, 2#} */
perfRows() {
const POS = [
{ key: 'upperBr', label: '上支撑辊' },
{ key: 'upperWr', label: '上工作辊' },
{ key: 'lowerWr', label: '下工作辊' },
{ key: 'lowerBr', label: '下支撑辊' }
]
return POS.map(p => ({
label: p.label,
'1#': (this.perfData[p.key] || {})['1#'] || {},
'2#': (this.perfData[p.key] || {})['2#'] || {}
}))
}
},
created() {},
mounted() {
this.$nextTick(this.syncAsideHeight)
window.addEventListener('resize', this.syncAsideHeight)
},
beforeDestroy() {
window.removeEventListener('resize', this.syncAsideHeight)
},
methods: {
onLineResolved() {
this.loadAll()
this.loadRollOptions()
this.loadRollInfoMap()
},
getDefaultChangeForm(standNo) {
return {
standNo: standNo || undefined,
changeType: undefined,
changeTime: undefined,
operator: undefined,
upperWrNo: undefined,
upperWrDia: undefined,
lowerWrNo: undefined,
lowerWrDia: undefined,
upperBrNo: undefined,
upperBrDia: undefined,
lowerBrNo: undefined,
lowerBrDia: undefined,
remark: undefined
}
},
getDefaultStandbyForm(standNo, rollType, position) {
return {
standNo: standNo || undefined,
rollType: rollType || undefined,
position: position || undefined,
rollNo: undefined,
readyTime: undefined,
remark: undefined
}
},
loadAll() {
;['1#', '2#'].forEach(s => {
this.loadCurrent(s)
this.loadStandby(s)
})
this.loadHistory()
this.loadOfflineRolls()
this.loadRollPerformance()
},
loadRollPerformance() {
this.perfLoading = true
getRollPerformance(this.lineId).then(res => {
this.perfData = res.data || {}
}).finally(() => { this.perfLoading = false })
},
loadCurrent(standNo) {
this.$set(this.loadingCurrent, standNo, true)
getCurrentRolls(this.lineId, standNo).then(res => {
this.$set(this.current, standNo, res.data || {})
this.$set(this.loadingCurrent, standNo, false)
this.$nextTick(this.syncAsideHeight)
}).catch(() => { this.$set(this.loadingCurrent, standNo, false) })
},
loadStandby(standNo) {
this.$set(this.loadingStandby, standNo, true)
listRollStandby(this.lineId, standNo).then(res => {
this.$set(this.standbyList, standNo, res.data || [])
this.$set(this.loadingStandby, standNo, false)
}).catch(() => { this.$set(this.loadingStandby, standNo, false) })
},
loadHistory() {
this.historyLoading = true
listRollChange({ ...this.historyQuery, lineId: this.lineId }).then(res => {
this.historyList = res.rows || []
this.historyTotal = res.total || 0
this.historyLoading = false
}).catch(() => { this.historyLoading = false })
},
loadRollOptions() {
// 下批辊选择只显示离线Offline状态的轧辊限定当前产线
listRollOptions(this.lineId, 'WR', 'Offline').then(res => { this.wrOptions = res.data || [] })
listRollOptions(this.lineId, 'BR', 'Offline').then(res => { this.brOptions = res.data || [] })
},
loadRollInfoMap() {
listRollInfo({ lineId: this.lineId, pageNum: 1, pageSize: 500 }).then(res => {
const map = {}
;(res.rows || []).forEach(r => { map[r.rollNo] = r })
this.rollInfoMap = map
})
},
rowClass({ row }) {
if (row.group === 'br') return 'row-br'
if (row.group === 'pl') return 'row-pl'
return ''
},
// el-table span-method参数列不需要 span其余无特殊合并
mergeParamCol({ columnIndex }) {
if (columnIndex === 0) return [1, 1]
},
// 换辊:自动从下批辊填入辊号和径值
handleOpenChange(standNo) {
const list = this.standbyList[standNo] || []
const ri = this.rollInfoMap
const pick = (rollType, pos) => {
const item = list.find(i => i.rollType === rollType && i.position === pos)
if (!item) return { no: undefined, dia: undefined }
const info = ri[item.rollNo]
const dia = item.diameter != null ? item.diameter
: (info ? (info.currentDia != null ? info.currentDia : info.initialDia) : undefined)
return { no: item.rollNo, dia }
}
const uwr = pick('WR', 'UP')
const lwr = pick('WR', 'DOWN')
const ubr = pick('BR', 'UP')
const lbr = pick('BR', 'DOWN')
this.changeForm = {
...this.getDefaultChangeForm(standNo),
lineId: this.lineId,
upperWrNo: uwr.no, upperWrDia: uwr.dia,
lowerWrNo: lwr.no, lowerWrDia: lwr.dia,
upperBrNo: ubr.no, upperBrDia: ubr.dia,
lowerBrNo: lbr.no, lowerBrDia: lbr.dia
}
this.changeOpen = true
},
submitChange() {
if (this.changeSubmitting) return
this.changeSubmitting = true
const standNo = this.changeForm.standNo
addRollChange(this.changeForm).then(() => {
this.$modal.msgSuccess('换辊成功')
this.changeOpen = false
clearRollStandby(this.lineId, standNo).finally(() => {
this.loadCurrent(standNo)
this.loadStandby(standNo)
this.loadHistory()
this.loadOfflineRolls()
this.loadRollPerformance()
})
}).finally(() => {
this.changeSubmitting = false
})
},
resetChangeForm() {
this.changeForm = this.getDefaultChangeForm()
this.changeSubmitting = false
},
// 下批轧辊 — 单元格点击(空→新增,有数据→删除)
handleSbCellClick(cell, standNo, rollType, position) {
if (cell && cell.standbyId) {
this.handleDelStandby(cell.standbyId, standNo)
} else if (rollType) {
this.standbyForm = this.getDefaultStandbyForm(standNo, rollType, position)
this.standbyFromCell = true
this.standbyOpen = true
}
},
// 下批轧辊 — 顶部"添加下批"按钮(不预设辊型辊位)
handleAddStandby(standNo) {
this.standbyForm = this.getDefaultStandbyForm(standNo)
this.standbyFromCell = false
this.standbyOpen = true
},
handleStandbyRollTypeChange() {
this.$set(this.standbyForm, 'rollNo', undefined)
},
submitStandby() {
this.$refs.standbyForm.validate(valid => {
if (!valid) return
addRollStandby({ ...this.standbyForm, lineId: this.lineId }).then(() => {
this.$modal.msgSuccess('已添加到下批轧辊')
this.standbyOpen = false
this.loadStandby(this.standbyForm.standNo)
this.loadRollOptions() // 刷新离线辊下拉(已变为 Standby 的辊不再显示)
this.loadOfflineRolls() // 刷新可用辊面板
})
})
},
resetStandbyForm() {
this.standbyForm = this.getDefaultStandbyForm()
this.standbyFromCell = false
this.$nextTick(() => { this.$refs.standbyForm && this.$refs.standbyForm.clearValidate() })
},
handleDelStandby(standbyId, standNo) {
this.$modal.confirm('确认移除该下批轧辊?移除后该辊状态将恢复为"离线"。').then(() => {
return delRollStandby(standbyId)
}).then(() => {
this.$modal.msgSuccess('已移除')
this.loadStandby(standNo)
this.loadRollOptions()
this.loadOfflineRolls()
})
},
handleClearStandby(standNo) {
this.$modal.confirm('确认清空 ' + standNo + ' 机架的全部下批轧辊?').then(() => {
return clearRollStandby(this.lineId, standNo)
}).then(() => {
this.$modal.msgSuccess('已清空')
this.loadStandby(standNo)
})
},
// ── 换辊历史 补录 / 删除 ──────────────────────────────────
handleEditHistory(row) {
this.historyEditForm = { ...row }
this.historyEditOpen = true
},
submitHistoryEdit() {
updateRollChange(this.historyEditForm).then(() => {
this.$modal.msgSuccess('保存成功')
this.historyEditOpen = false
this.loadHistory()
})
},
handleDeleteHistory(row) {
this.$modal.confirm('确认删除该换辊记录?此操作不可恢复。').then(() => {
return delRollChange(row.changeId)
}).then(() => {
this.$modal.msgSuccess('已删除')
this.loadHistory()
})
},
// ── 可用离线辊列表 ────────────────────────────────────────
loadOfflineRolls() {
this.offlineLoading = true
listRollInfo({ lineId: this.lineId, status: 'Offline', pageNum: 1, pageSize: 30 }).then(res => {
this.offlineRolls = res.rows || []
}).finally(() => {
this.offlineLoading = false
})
},
// 右侧两个面板各占左侧表格高度的一半(各自减去卡片头高度)
syncAsideHeight() {
const el = this.$refs.mainTable && this.$refs.mainTable.$el
if (!el) return
const totalH = el.offsetHeight // 左侧 el-table 实际高度
const gap = 12 // 两个面板之间的间距
const headerH = 48 // 单个卡片 header 高度估值
const half = Math.max(100, Math.floor((totalH - gap) / 2) - headerH)
this.asideHalfH = half
}
}
}
</script>
<style scoped>
.working-roll-page { background: #f4f5f7; }
.roll-table-card { border: 1px solid #dcdee0; border-radius: 4px; }
/* 顶部两栏布局 */
.top-row {
display: flex;
gap: 12px;
align-items: stretch;
}
.top-row__main { flex: 2; min-width: 0; }
.top-row__aside { flex: 1; min-width: 0; }
/* 右侧列:两个面板纵向叠放 */
.aside-col { display: flex; flex-direction: column; gap: 12px; }
.aside-panel { flex: 1; min-width: 0; }
/* 绩效单元格 */
.perf-cell { padding: 2px 0; line-height: 1.4; }
.perf-roll { font-family: 'Consolas', monospace; font-size: 12px; font-weight: 600; color: #1f2329; }
.perf-stat { font-size: 11px; color: #8f9099; }
/* 历史换辊辊组 */
.roll-group-cell { display: flex; flex-wrap: wrap; gap: 4px; line-height: 1.5; }
.rg-item { font-size: 12px; color: #3d4b5c; }
.rg-item b { color: #5f6368; font-weight: 600; margin-right: 2px; }
.rg-item em { font-style: normal; color: #9aa0a6; font-size: 11px; }
.card-header { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.card-title { font-size: 13px; font-weight: 600; color: #3d4b5c; white-space: nowrap; }
.header-meta { font-size: 11px; color: #8f9099; white-space: nowrap; }
/* 参数列文字 */
.param-label { font-size: 12px; color: #3d4b5c; }
/* 数值单元格 */
.cell-main { font-family: 'Consolas', 'Courier New', monospace; font-size: 13px; font-weight: 600; color: #1f2329; }
.cell-empty { color: #c0c4cc; font-size: 12px; }
/* 换辊预览卡片 */
.roll-preview { background: #f7f8fa; border: 1px solid #e4e6eb; border-radius: 4px; padding: 10px 12px; }
.roll-preview__title { font-size: 11px; color: #8f9099; margin-bottom: 8px; letter-spacing: .5px; }
.roll-preview__item { display: flex; align-items: center; gap: 6px; padding: 4px 0; }
.rp-label { font-size: 12px; color: #8f9099; width: 56px; flex-shrink: 0; }
.rp-val { font-size: 13px; font-weight: 600; color: #1f2329; font-family: 'Consolas', monospace; flex: 1; }
.rp-dia { font-size: 11px; color: #9aa0a6; }
.roll-preview__tip { font-size: 11px; color: #b0b3bb; margin-top: 10px; text-align: center; }
/* 下批辊单元格容器:填满整个 td有数据时点击删除 */
.sb-cell {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
min-height: 36px;
}
/* 有数据 → 点击删除hover 浅红 */
.sb-cell--del {
cursor: pointer;
}
.sb-cell--del:hover {
background: rgba(198, 40, 40, 0.07);
}
/* 空格子 → 点击新增hover 浅绿 */
.sb-cell--add {
cursor: pointer;
}
.sb-cell--add:hover {
background: rgba(35, 134, 54, 0.07);
}
</style>
<style>
/* 行分组色scoped 无法穿透 el-table 内部) */
.working-roll-page .el-table .row-br > td { background-color: #f7f7f6 !important; }
.working-roll-page .el-table .row-pl > td { background-color: #eef2f8 !important; }
/* 列分组标题加粗区分 */
.working-roll-page .el-table__header th.el-table__cell {
background: #f0f2f5;
color: #3d4b5c;
font-weight: 600;
font-size: 12px;
}
</style>