feat(cost/comprehensive): 添加汇总行功能,支持表格数据求和与平均计算

1. 新增summaryRows计算属性,生成总和与平均两行汇总数据
2. 新增tableData计算属性,将汇总行合并到原表格数据中
3. 为汇总行添加专属样式,高亮显示并加粗文字
4. 优化表格列渲染逻辑,普通行保留编辑能力,汇总行仅展示静态数据
5. 调整删除按钮逻辑,仅对非汇总行生效并修复删除索引问题
6. 新增rowClassName方法为汇总行添加专属CSS类
This commit is contained in:
2026-07-04 13:19:32 +08:00
parent fa30ac37e9
commit 85751acc46

View File

@@ -30,9 +30,9 @@
</span>
</div>
<!-- <el-alert :title="'已配置'+allCols.length+'个列'" type="info" :closable="false" show-icon style="margin-bottom:8px" /> -->
<el-table v-loading="gridLoading" height="calc(100vh - 260px)" :data="gridRows" border stripe size="mini" style="width:100%" :header-cell-style="headerStyle" :key="'tbl-'+inputMode">
<el-table v-loading="gridLoading" height="calc(100vh - 260px)" :data="tableData" border stripe size="mini" style="width:100%" :header-cell-style="headerStyle" :row-class-name="rowClassName" :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>
<template slot-scope="s"><span v-if="s.row.$isSummary" class="summary-label">{{ s.row.detailDate }}</span><el-date-picker v-else 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" align="center" width="130">
@@ -40,7 +40,8 @@
<div class="col-hd">{{ col.itemName }}{{ col.unit ? '('+col.unit+')' : '' }}</div>
</template>
<template slot-scope="s">
<el-input v-model="s.row['q'+col.itemId]" size="mini" @input="recalcAll" :class="{'anomaly-input': isAnomalyCell(s.row.detailDate, col.itemId)}">
<span v-if="s.row.$isSummary" class="summary-val">{{ s.row['q'+col.itemId] || '-' }}</span>
<el-input v-else v-model="s.row['q'+col.itemId]" size="mini" @input="recalcAll" :class="{'anomaly-input': isAnomalyCell(s.row.detailDate, col.itemId)}">
<!-- <span slot="suffix" v-if="col.queryCondition" class="input-suffix-actions">
<i title="反填" v-if="col.category==='辅料'||col.category==='能耗'" :class="backfillLoading[col.itemId]?'el-icon-loading':'el-icon-upload2'" class="ica ica-backfill" @click.stop="backfillCell(col, s.row)" />
<i title="自动获取" :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" class="ica ica-fetch" @click.stop="fetchAutoData(col, s.row)" />
@@ -53,27 +54,39 @@
<div class="col-hd">{{ col.itemName }}{{ col.unit ? '('+col.unit+')' : '' }}</div>
</template>
<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" :class="{'anomaly-input': isAnomalyCell(s.row.detailDate, col.itemId)}">
<!-- <span slot="suffix" v-if="col.queryCondition" class="input-suffix-actions"><i v-if="col.category==='辅料'||col.category==='能耗'" :class="backfillLoading[col.itemId]?'el-icon-loading':'el-icon-upload2'" class="ica ica-backfill" @click.stop="backfillCell(col, s.row, '1')" /><i :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" class="ica ica-fetch" @click.stop="fetchAutoData(col, s.row, '1')" /></span> -->
</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" :class="{'anomaly-input': isAnomalyCell(s.row.detailDate, col.itemId)}">
<!-- <span slot="suffix" v-if="col.queryCondition" class="input-suffix-actions"><i v-if="col.category==='辅料'||col.category==='能耗'" :class="backfillLoading[col.itemId]?'el-icon-loading':'el-icon-upload2'" class="ica ica-backfill" @click.stop="backfillCell(col, s.row, '2')" /><i :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" class="ica ica-fetch" @click.stop="fetchAutoData(col, s.row, '2')" /></span> -->
</el-input>
</div>
<template v-if="s.row.$isSummary">
<div class="shift-cell"><span class="shift-tag"></span><span class="summary-val">{{ s.row['q'+col.itemId+'_1'] || '-' }}</span></div>
<div class="shift-cell"><span class="shift-tag"></span><span class="summary-val">{{ s.row['q'+col.itemId+'_2'] || '-' }}</span></div>
</template>
<template v-else>
<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" :class="{'anomaly-input': isAnomalyCell(s.row.detailDate, col.itemId)}">
<!-- <span slot="suffix" v-if="col.queryCondition" class="input-suffix-actions"><i v-if="col.category==='辅料'||col.category==='能耗'" :class="backfillLoading[col.itemId]?'el-icon-loading':'el-icon-upload2'" class="ica ica-backfill" @click.stop="backfillCell(col, s.row, '1')" /><i :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" class="ica ica-fetch" @click.stop="fetchAutoData(col, s.row, '1')" /></span> -->
</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" :class="{'anomaly-input': isAnomalyCell(s.row.detailDate, col.itemId)}">
<!-- <span slot="suffix" v-if="col.queryCondition" class="input-suffix-actions"><i v-if="col.category==='辅料'||col.category==='能耗'" :class="backfillLoading[col.itemId]?'el-icon-loading':'el-icon-upload2'" class="ica ica-backfill" @click.stop="backfillCell(col, s.row, '2')" /><i :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" class="ica ica-fetch" @click.stop="fetchAutoData(col, s.row, '2')" /></span> -->
</el-input>
</div>
</template>
</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>
<template slot-scope="s"><span v-if="s.row.$isSummary" class="summary-val">{{ s.row['mv'+col.mIdx]!=null ? s.row['mv'+col.mIdx] : '-' }}</span><template v-else>{{ s.row['mv'+col.mIdx]!=null ? s.row['mv'+col.mIdx] : '-' }}</template></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 v-if="s.row.$isSummary">
<div class="shift-metric"><span class="summary-val"> {{ s.row['mv'+col.mIdx+'_1']!=null ? s.row['mv'+col.mIdx+'_1'] : '-' }}</span></div>
<div class="shift-metric"><span class="summary-val"> {{ s.row['mv'+col.mIdx+'_2']!=null ? s.row['mv'+col.mIdx+'_2'] : '-' }}</span></div>
</template>
<template v-else>
<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>
</template>
</el-table-column>
</template>
@@ -82,7 +95,7 @@
<!-- <el-button size="mini" type="text" style="padding:0 2px;font-size:11px" @click="saveRow(s.row)">保存</el-button>
<el-button size="mini" type="text" style="padding:0 2px;font-size:11px" @click="fetchRow(s.row)">抓取</el-button>
<el-button size="mini" type="text" style="padding:0 2px;font-size:11px;color:#67c23a" @click="backfillRow(s.row)">反填</el-button> -->
<el-button size="mini" type="text" style="padding:0 2px;font-size:11px;color:#f56c6c" @click="gridRows.splice(s.$index,1)">删除</el-button>
<el-button v-if="!s.row.$isSummary" size="mini" type="text" style="padding:0 2px;font-size:11px;color:#f56c6c" @click="gridRows.splice(s.$index - summaryRows.length,1)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -494,6 +507,45 @@ export default {
})
return col && col.color ? { background: col.color, color: '#fff' } : {}
}
},
summaryRows() {
const rows = this.gridRows.filter(r => r.detailDate)
if (!rows.length) return []
const detailCols = this.allCols.filter(c => c.$type === 'detail')
const metricCols = this.allCols.filter(c => c.$type === 'metric')
const calc = (type) => {
const row = { detailDate: type === 'sum' ? '总和' : '平均', $isSummary: true, $summaryType: type }
const n = rows.length || 1
detailCols.forEach(col => {
if (col.isShift) {
const s1 = rows.reduce((a, r) => a + (parseFloat(r['q' + col.itemId + '_1']) || 0), 0)
const s2 = rows.reduce((a, r) => a + (parseFloat(r['q' + col.itemId + '_2']) || 0), 0)
row['q' + col.itemId + '_1'] = type === 'sum' ? s1.toFixed(2) : (s1 / n).toFixed(2)
row['q' + col.itemId + '_2'] = type === 'sum' ? s2.toFixed(2) : (s2 / n).toFixed(2)
row['q' + col.itemId] = type === 'sum' ? (s1 + s2).toFixed(2) : ((s1 + s2) / n).toFixed(2)
} else {
const s = rows.reduce((a, r) => a + (parseFloat(r['q' + col.itemId]) || 0), 0)
row['q' + col.itemId] = type === 'sum' ? s.toFixed(2) : (s / n).toFixed(2)
}
})
metricCols.forEach(col => {
if (col.isShift) {
const s1 = rows.reduce((a, r) => a + (parseFloat(r['mv' + col.mIdx + '_1']) || 0), 0)
const s2 = rows.reduce((a, r) => a + (parseFloat(r['mv' + col.mIdx + '_2']) || 0), 0)
row['mv' + col.mIdx + '_1'] = type === 'sum' ? s1.toFixed(2) : (s1 / n).toFixed(2)
row['mv' + col.mIdx + '_2'] = type === 'sum' ? s2.toFixed(2) : (s2 / n).toFixed(2)
row['mv' + col.mIdx] = type === 'sum' ? (s1 + s2).toFixed(2) : ((s1 + s2) / n).toFixed(2)
} else {
const s = rows.reduce((a, r) => a + (parseFloat(r['mv' + col.mIdx]) || 0), 0)
row['mv' + col.mIdx] = type === 'sum' ? s.toFixed(2) : (s / n).toFixed(2)
}
})
return row
}
return [calc('sum'), calc('avg')]
},
tableData() {
return [...this.summaryRows, ...this.gridRows]
}
},
watch: { configOpen(v) { if (!v) this.rpOpen = false } },
@@ -509,6 +561,10 @@ export default {
}
},
methods: {
rowClassName({ row }) {
if (row.$isSummary) return 'summary-row'
return ''
},
/* report */
getList() {
this.loading = true
@@ -1142,4 +1198,8 @@ export default {
.ica-backfill { color: #67c23a; margin-right: 1px; }
.ica:hover { opacity: 0.7; }
/deep/ .anomaly-input .el-input__inner { background: #fef0f0 !important; border-color: #f56c6c !important; }
/deep/ .summary-row td { background: #f0f7ff !important; font-weight: bold; }
/deep/ .summary-row td .cell { color: #303133; }
.summary-label { font-weight: bold; color: #303133; padding: 0 5px; }
.summary-val { font-weight: bold; color: #303133; }
</style>