|
|
|
|
@@ -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() }
|
|
|
|
|
|