feat(cost): 新增生产月报复制功能,修复考勤校验参数问题,优化表格列操作

This commit is contained in:
2026-05-27 13:19:55 +08:00
parent 454d8de6a2
commit e95e9adfcd
6 changed files with 188 additions and 43 deletions

View File

@@ -96,4 +96,16 @@ public class CostProdReportController extends BaseController {
@PathVariable Long[] reportIds) {
return toAjax(iCostProdReportService.deleteWithValidByIds(Arrays.asList(reportIds), true));
}
/**
* 复制生产月报
* 明细列原样保留指标列重新插入并更新config中的id引用
*/
@Log(title = "生产月报", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping("/copy/{sourceId}")
public R<CostProdReportVo> copy(@NotNull(message = "源报表ID不能为空") @PathVariable Long sourceId,
@RequestBody CostProdReportBo bo) {
return R.ok(iCostProdReportService.copyReport(sourceId, bo));
}
}

View File

@@ -46,4 +46,9 @@ public interface ICostProdReportService {
* 校验并批量删除生产月报信息
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 复制生产月报明细列原样保留指标列重新插入并更新config中的id引用
*/
CostProdReportVo copyReport(Long sourceId, CostProdReportBo bo);
}

View File

@@ -1,6 +1,9 @@
package com.klp.cost.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.klp.common.core.page.TableDataInfo;
import com.klp.common.core.domain.PageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -12,7 +15,9 @@ import org.springframework.stereotype.Service;
import com.klp.cost.domain.bo.CostProdReportBo;
import com.klp.cost.domain.vo.CostProdReportVo;
import com.klp.cost.domain.CostProdReport;
import com.klp.cost.domain.CostProdMetric;
import com.klp.cost.mapper.CostProdReportMapper;
import com.klp.cost.mapper.CostProdMetricMapper;
import com.klp.cost.service.ICostProdReportService;
import java.util.List;
@@ -30,6 +35,7 @@ import java.util.Collection;
public class CostProdReportServiceImpl implements ICostProdReportService {
private final CostProdReportMapper baseMapper;
private final CostProdMetricMapper metricMapper;
/**
* 查询生产月报
@@ -110,4 +116,58 @@ public class CostProdReportServiceImpl implements ICostProdReportService {
}
return baseMapper.deleteBatchIds(ids) > 0;
}
/**
* 复制生产月报
* 明细列原样保留itemId不变指标列重新插入并更新config中的id引用
*/
@Override
public CostProdReportVo copyReport(Long sourceId, CostProdReportBo bo) {
CostProdReport source = baseMapper.selectById(sourceId);
if (source == null) {
throw new RuntimeException("源报表不存在");
}
// 创建新报表先插入以获取ID
CostProdReport newRp = new CostProdReport();
BeanUtil.copyProperties(source, newRp, "reportId", "colConfig");
newRp.setReportTitle(bo.getReportTitle() != null ? bo.getReportTitle() : source.getReportTitle() + "-副本");
if (bo.getReportDate() != null) newRp.setReportDate(bo.getReportDate());
if (bo.getLineType() != null) newRp.setLineType(bo.getLineType());
if (bo.getInputWeight() != null) newRp.setInputWeight(bo.getInputWeight());
if (bo.getOutputWeight() != null) newRp.setOutputWeight(bo.getOutputWeight());
if (bo.getRemark() != null) newRp.setRemark(bo.getRemark());
baseMapper.insert(newRp);
Long newRid = newRp.getReportId();
// 处理colConfig为每个指标列重新插入metric记录更新id引用
String colConfig = source.getColConfig();
if (StringUtils.isNotBlank(colConfig)) {
JSONObject cfg = JSONUtil.parseObj(colConfig);
JSONArray columns = cfg.getJSONArray("columns");
if (columns != null && columns.size() > 0) {
for (int i = 0; i < columns.size(); i++) {
JSONObject col = columns.getJSONObject(i);
if ("m".equals(col.getStr("t"))) {
String idStr = col.getStr("id");
Long oldMetricId = idStr != null ? Long.parseLong(idStr) : null;
if (oldMetricId != null) {
CostProdMetric srcMetric = metricMapper.selectById(oldMetricId);
if (srcMetric != null) {
CostProdMetric newMetric = new CostProdMetric();
BeanUtil.copyProperties(srcMetric, newMetric, "metricId", "reportId");
newMetric.setReportId(newRid);
metricMapper.insert(newMetric);
col.set("id", String.valueOf(newMetric.getMetricId()));
}
}
}
}
newRp.setColConfig(cfg.toString());
}
baseMapper.updateById(newRp);
}
return baseMapper.selectVoById(newRid);
}
}

View File

@@ -42,3 +42,12 @@ export function delProdReport(reportId) {
method: 'delete'
})
}
// 复制生产月报
export function copyProdReport(sourceId, data) {
return request({
url: '/cost/prodReport/copy/' + sourceId,
method: 'post',
data: data
})
}

View File

@@ -90,11 +90,19 @@
<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="110" align="center">
<el-table-column label="操作" width="140" align="center">
<template slot-scope="s">
<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-delete" @click="allCols.splice(s.$index,1)" />
<span style="display:inline-flex;align-items:center"
@dragover.prevent @dragenter.prevent
@drop.stop="onDragDrop(s.$index)">
<el-button size="mini" type="text" icon="el-icon-rank"
style="cursor:grab;font-size:14px" title="拖拽排序"
draggable="true"
@dragstart.stop="dragIdx=s.$index" />
<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-delete" @click="allCols.splice(s.$index,1)" />
</span>
</template>
</el-table-column>
</el-table>
@@ -195,6 +203,7 @@
<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">
@@ -218,14 +227,26 @@
</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" /></el-form-item>
<el-form-item label="报表日期"><el-date-picker v-model="copyRpForm.reportDate" type="date" 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>
</template>
<script>
import { listProdReport, getProdReport, addProdReport, updateProdReport, delProdReport } from "@/api/cost/prodReport"
import { listProdReport, getProdReport, addProdReport, updateProdReport, delProdReport, copyProdReport } from "@/api/cost/prodReport"
import { listProdDetail, batchSaveProdDetail } from "@/api/cost/prodDetail"
import { listProdMetric, addProdMetric, updateProdMetric, delProdMetric } from "@/api/cost/prodMetric"
import { listProdMetric, addProdMetric, updateProdMetric, delProdMetric, getProdMetric } from "@/api/cost/prodMetric"
import { listItem } from "@/api/cost/item"
export default {
@@ -235,10 +256,11 @@ export default {
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,
showAddDetail: false, selAdd: [], selCol: [], curIdx: -1, dragIdx: -1,
metricPickOpen: false, metricPickList: [], selMp: [],
mgrOpen: false, mgrList: [], defOpen: false, defTitle: '', defForm: {},
copyCfgOpen: false, copyReports: [], copySrc: null,
@@ -247,8 +269,8 @@ export default {
},
computed: {
availableItems() {
const used = new Set(this.allCols.filter(c => c.$type === 'detail').map(c => c.itemId))
return this.allItems.filter(i => !used.has(i.itemId))
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)))
},
headerStyle() {
return ({ column }) => {
@@ -289,13 +311,35 @@ export default {
})
},
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
},
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) } },
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' && c.itemId === item.itemId))
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 })
})
this.showAddDetail = false; this.selAdd = []
@@ -307,9 +351,9 @@ export default {
},
/* metric picker */
async openMetricPicker() {
await this.loadAllMetrics()
const used = new Set(this.allCols.filter(c => c.$type === 'metric' && c.metricId).map(c => c.metricId))
this.metricPickList = (this._allMetricDefs || []).filter(m => !used.has(m.metricId))
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() {
@@ -320,7 +364,9 @@ export default {
},
/* metric management */
async openMetricMgr() {
await this.loadAllMetrics(); this.mgrList = this._allMetricDefs || []; this.mgrOpen = true
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 },
@@ -354,11 +400,17 @@ export default {
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 }
if (c.$type === 'detail') o.id = c.itemId
else { if (c.metricId) o.id = c.metricId; else o.n = c.metricName }
if (c.color) o.c = c.color
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 })
@@ -379,23 +431,27 @@ export default {
} finally { this.gridLoading = false }
},
async restoreAllCols() {
await this.loadItems(); await this.loadAllMetrics()
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 }
this.allCols = cfg.columns.map(c => {
const cols = []
for (const c of cfg.columns) {
if (c.t === 'd') {
const item = this.allItems.find(i => i.itemId === c.id)
return item ? { $type: 'detail', itemId: item.itemId, itemCode: item.itemCode, itemName: item.itemName, unit: item.unit, isShift: !!c.s, color: c.c || null } : null
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 })
} 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 })
}
if (c.t === 'm') {
let def
if (c.id) def = (this._allMetricDefs || []).find(m => m.metricId === c.id)
else def = (this._allMetricDefs || []).find(m => m.metricName === c.n)
return def ? { $type: 'metric', metricId: def.metricId, metricName: def.metricName, metricFormula: def.metricFormula, unit: def.remark||'', isShift: !!c.s, color: c.c || null } : null
}
return null
}).filter(Boolean)
}
this.allCols = cols
let mi = 0; this.allCols.forEach(c => { if (c.$type === 'metric') c.mIdx = mi++ })
},
buildGrid(details) {
@@ -428,7 +484,7 @@ export default {
if (pv != null) f = f.replace(new RegExp('@\\{'+pn+'\\}','g'), pv)
}
detailCols.forEach(c => {
const item = this.allItems.find(i => i.itemId === c.itemId)
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
@@ -476,16 +532,18 @@ export default {
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=>c.itemId))
const usedMids = new Set(this.allCols.filter(c=>c.$type==='metric'&&c.metricId).map(c=>c.metricId))
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') { if (!usedIds.has(sc.id)) { const item = this.allItems.find(i=>i.itemId===sc.id); 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 }); usedIds.add(sc.id) } } }
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 }); usedIds.add(sid) } }
}
else if (sc.t === 'm') {
let def
if (sc.id) { def = this._allMetricDefs.find(m=>m.metricId===sc.id); if (usedMids.has(sc.id)) return; usedMids.add(sc.id) }
else def = this._allMetricDefs.find(m=>m.metricName===sc.n)
if (def) this.allCols.push({ $type:'metric', metricId:def.metricId, metricName:def.metricName, metricFormula:def.metricFormula, unit:def.remark||'', isShift:!!sc.s, color:sc.c||null })
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++ })
@@ -494,9 +552,10 @@ export default {
/* helpers */
async loadItems() { if (!this.allItems.length) { const r = await listItem({ pageNum:1, pageSize:999 }); this.allItems = r.rows || [] } },
async loadAllMetrics() {
const r = await listProdMetric({ pageNum:1, pageSize:99999 }); const map = {}
;(r.rows||[]).forEach(m => { if (m.metricName && m.metricFormula && !map[m.metricId]) map[m.metricId] = m })
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() }

View File

@@ -841,7 +841,7 @@ export default {
startDate: this.dateRangeParams.startDate,
endDate: this.dateRangeParams.endDate
}
this.selectedUserIds = this.allEmployees.map(emp => emp.infoId)
this.selectedUserIds = []
},
cancelCheck() {
@@ -857,7 +857,7 @@ export default {
this.checkLoading = true
const params = {
...this.checkForm,
employeeIds: this.selectedUserIds.join(',')
userIds: this.selectedUserIds.join(',')
}
generateAttendanceCheck(params).then(response => {
this.$modal.msgSuccess("比对成功")