@nstc-business/imes 1.0.21 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nstc-business/imes",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "private": false,
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -57,6 +57,25 @@
57
57
  <template #default="{ row }">{{ fullScoreFn(row) }}</template>
58
58
  </vxe-column>
59
59
 
60
+ <vxe-column
61
+ v-if="showFormulaCol"
62
+ field="displayFormula"
63
+ title="计算公式"
64
+ width="130"
65
+ align="center"
66
+ >
67
+ <template #default="{ row }">{{ row.displayFormula || '—' }}</template>
68
+ </vxe-column>
69
+ <vxe-column
70
+ v-if="showRemarkCol"
71
+ field="indicatorRemark"
72
+ title="评价标准"
73
+ width="130"
74
+ align="center"
75
+ >
76
+ <template #default="{ row }">{{ row.indicatorRemark || '—' }}</template>
77
+ </vxe-column>
78
+
60
79
  <vxe-column title="指标值" min-width="260" align="center">
61
80
  <template #default="{ row }">
62
81
  <template v-if="isQualFn(row)">
@@ -119,6 +138,11 @@
119
138
  </vxe-column>
120
139
 
121
140
  <vxe-column title="指标得分" width="140" align="center">
141
+ <template #default="{ row }">
142
+ <div>{{ indicatorScoreFn(row) }}</div>
143
+ </template>
144
+ </vxe-column>
145
+ <vxe-column title="模型指标得分" width="140" align="center">
122
146
  <template #default="{ row }">
123
147
  <div>{{ resultFn(row) }}</div>
124
148
  <InputNumber
@@ -133,28 +157,10 @@
133
157
  :max="row.fullScore"
134
158
  :disabled="ctx.readonly"
135
159
  v-model="row.modifiedResult"
136
- placeholder="修改指标得分"
160
+ placeholder="修改模型指标得分"
137
161
  />
138
162
  </template>
139
163
  </vxe-column>
140
- <vxe-column
141
- v-if="showFormulaCol"
142
- field="displayFormula"
143
- title="计算公式"
144
- width="130"
145
- align="center"
146
- >
147
- <template #default="{ row }">{{ row.displayFormula || '—' }}</template>
148
- </vxe-column>
149
- <vxe-column
150
- v-if="showRemarkCol"
151
- field="indicatorRemark"
152
- title="评价标准"
153
- width="130"
154
- align="center"
155
- >
156
- <template #default="{ row }">{{ row.indicatorRemark || '—' }}</template>
157
- </vxe-column>
158
164
  <vxe-column
159
165
  v-if="showModifyReasonCol"
160
166
  min-width="260"
@@ -218,6 +224,7 @@ import {
218
224
  isQualIndicator,
219
225
  digitsCalc,
220
226
  resultByShowStyle,
227
+ indicatorScoreText,
221
228
  indicatorResultText,
222
229
  fullScoreText,
223
230
  showLevelSerial
@@ -278,6 +285,9 @@ export default {
278
285
  resultFn(row) {
279
286
  return resultByShowStyle(row, this.ctx.modelWrap)
280
287
  },
288
+ indicatorScoreFn(row) {
289
+ return indicatorScoreText(row, this.ctx.modelWrap)
290
+ },
281
291
  indicatorTextFn(row) {
282
292
  return indicatorResultText(row, this.ctx.modelWrap)
283
293
  },
@@ -0,0 +1,181 @@
1
+ <template>
2
+ <div class="measure-group-blocks">
3
+ <measure-qual-analysis
4
+ v-if="qualLooseNodes.length"
5
+ :groups="qualLooseGroups"
6
+ />
7
+ <measure-tree-table
8
+ v-if="tableLooseNodes.length"
9
+ :nodes="tableLooseNodes"
10
+ :level="level"
11
+ :number-prefix="numberPrefix"
12
+ :calc-mode="calcMode"
13
+ :section="section"
14
+ />
15
+
16
+ <div
17
+ v-for="(group, i) in nestedGroups"
18
+ :key="group._rowKey || i"
19
+ class="measure-inner-group"
20
+ >
21
+ <div class="section-title">
22
+ {{ group.name }}
23
+ <span v-if="group.remark" class="section-remark">
24
+ 备注:<span class="remark-html" v-html="group.remark"></span>
25
+ </span>
26
+ <span v-if="innerWeightText(group)" class="section-weight-pill">
27
+ {{ innerWeightText(group) }}
28
+ </span>
29
+ <span v-if="innerSubtotalText(group)" class="section-score-pill">
30
+ {{ innerSubtotalText(group) }}
31
+ </span>
32
+ </div>
33
+ <measure-group-blocks
34
+ :nodes="group.children || []"
35
+ :group-node="group"
36
+ :level="level + 1"
37
+ :number-prefix="childNumberPrefix(group)"
38
+ :calc-mode="calcMode"
39
+ :section="section"
40
+ />
41
+ </div>
42
+ </div>
43
+ </template>
44
+
45
+ <script>
46
+ import MeasureQualAnalysis from './MeasureQualAnalysis.vue'
47
+ import {
48
+ splitGroupNodes,
49
+ isQualIndicator,
50
+ hierarchySerial,
51
+ hierarchySerialPrefix
52
+ } from './helpers'
53
+
54
+ export default {
55
+ name: 'MeasureGroupBlocks',
56
+ components: {
57
+ MeasureQualAnalysis,
58
+ MeasureTreeTable: () => import('./MeasureTreeTable.vue')
59
+ },
60
+ inject: ['measureCtx'],
61
+ props: {
62
+ nodes: {
63
+ type: Array,
64
+ default: () => []
65
+ },
66
+ groupNode: {
67
+ type: Object,
68
+ default: null
69
+ },
70
+ level: {
71
+ type: Number,
72
+ default: 1
73
+ },
74
+ numberPrefix: {
75
+ type: String,
76
+ default: ''
77
+ },
78
+ calcMode: {
79
+ type: Boolean,
80
+ default: false
81
+ },
82
+ section: {
83
+ type: Object,
84
+ default: () => ({})
85
+ }
86
+ },
87
+ computed: {
88
+ ctx() {
89
+ return this.measureCtx()
90
+ },
91
+ split() {
92
+ return splitGroupNodes(this.nodes)
93
+ },
94
+ nestedGroups() {
95
+ return this.split.groups
96
+ },
97
+ looseNodes() {
98
+ return this.split.loose
99
+ },
100
+ qualLooseNodes() {
101
+ return this.looseNodes.filter(isQualIndicator)
102
+ },
103
+ tableLooseNodes() {
104
+ return this.looseNodes.filter(n => !isQualIndicator(n))
105
+ },
106
+ qualLooseGroups() {
107
+ const resolve = this.ctx.resolveLeafNode
108
+ return [
109
+ {
110
+ name: '',
111
+ children: this.qualLooseNodes.map(n =>
112
+ resolve ? resolve(n) || n : n
113
+ )
114
+ }
115
+ ]
116
+ }
117
+ },
118
+ methods: {
119
+ innerWeightText(group) {
120
+ const fn = this.ctx.groupWeightText
121
+ return fn ? fn(group) : ''
122
+ },
123
+ innerSubtotalText(group) {
124
+ const fn = this.ctx.groupSubtotalText
125
+ return fn ? fn(group) : ''
126
+ },
127
+ childNumberPrefix(group) {
128
+ const idx = this.nodes.findIndex(n => n._rowKey === group._rowKey)
129
+ const serial = hierarchySerial(this.numberPrefix, idx < 0 ? 0 : idx)
130
+ return hierarchySerialPrefix(serial)
131
+ }
132
+ }
133
+ }
134
+ </script>
135
+
136
+ <style lang="less" scoped>
137
+ .measure-group-blocks {
138
+ width: 100%;
139
+ }
140
+ .measure-inner-group {
141
+ margin-top: 12px;
142
+ }
143
+ .measure-inner-group + .measure-inner-group {
144
+ margin-top: 16px;
145
+ }
146
+ .section-title {
147
+ display: flex;
148
+ align-items: center;
149
+ flex-wrap: wrap;
150
+ gap: 8px;
151
+ font-size: 15px;
152
+ font-weight: 600;
153
+ padding-left: 8px;
154
+ margin-bottom: 8px;
155
+ border-left: 3px solid #409eff;
156
+ }
157
+ .section-weight-pill,
158
+ .section-score-pill {
159
+ display: inline-block;
160
+ margin-left: 4px;
161
+ padding: 2px 10px;
162
+ font-size: 14px;
163
+ font-weight: 400;
164
+ color: #606266;
165
+ line-height: 20px;
166
+ background: #f2f3f5;
167
+ border-radius: 4px;
168
+ }
169
+ .section-remark {
170
+ margin-left: 0;
171
+ font-weight: 400;
172
+ color: #909399;
173
+ }
174
+ .section-remark .remark-html {
175
+ display: inline;
176
+ ::v-deep p {
177
+ display: inline;
178
+ margin: 0;
179
+ }
180
+ }
181
+ </style>
@@ -22,92 +22,120 @@
22
22
  </span>
23
23
  </div>
24
24
 
25
- <div class="qual-table">
26
- <template v-for="(node, i) in group.children">
27
- <!-- 不可量化:下拉选项 -->
28
- <div
29
- v-if="isQualSelectNode(node)"
30
- :key="'qual-' + (node._rowKey || i)"
31
- class="qual-row"
32
- >
33
- <div class="qual-cell qual-cell-name" v-title="node.name">
34
- {{ node.name }}
35
- </div>
36
- <div class="qual-cell qual-cell-select">
37
- <el-select
38
- class="qual-select"
39
- :value="node.scoreMapID"
40
- :disabled="readonly"
41
- size="small"
42
- placeholder="请选择"
43
- @change="val => onSelectChange(node, val)"
25
+ <vxe-table
26
+ border
27
+ class="qual-table"
28
+ :data="group.children"
29
+ :row-config="{ keyField: '_rowKey' }"
30
+ :scroll-y="{ enabled: false }"
31
+ >
32
+ <vxe-column v-if="showSerial" title="序号" width="72" align="center">
33
+ <template #default="{ rowIndex }">{{ rowIndex + 1 }}</template>
34
+ </vxe-column>
35
+
36
+ <vxe-column field="name" title="指标名称" min-width="180" align="center">
37
+ <template #default="{ row }">
38
+ <span v-title="row.name">{{ row.name }}</span>
39
+ </template>
40
+ </vxe-column>
41
+
42
+ <vxe-column
43
+ v-if="showWeightCol"
44
+ title="指标权重"
45
+ width="100"
46
+ align="center"
47
+ >
48
+ <template #default="{ row }">{{ row.weight || '—' }}</template>
49
+ </vxe-column>
50
+
51
+ <vxe-column title="指标满分值" width="110" align="center">
52
+ <template #default="{ row }">{{ formatFullScore(row) }}</template>
53
+ </vxe-column>
54
+
55
+ <vxe-column title="评价标准" min-width="180" align="center">
56
+ <template #default="{ row }">{{ evalStandardText(row) }}</template>
57
+ </vxe-column>
58
+
59
+ <vxe-column title="指标值" min-width="220" align="center">
60
+ <template #default="{ row }">
61
+ <el-select
62
+ v-if="isQualSelectNode(row)"
63
+ class="qual-select"
64
+ :value="row.scoreMapID"
65
+ :disabled="readonly"
66
+ size="small"
67
+ placeholder="请选择"
68
+ @change="val => onSelectChange(row, val)"
69
+ >
70
+ <el-option
71
+ v-for="option in row.optionList"
72
+ :key="option.scoreMapID"
73
+ :label="option.scoreText"
74
+ :value="option.scoreMapID"
75
+ style="height: auto"
44
76
  >
45
- <el-option
46
- v-for="option in node.optionList"
47
- :key="option.scoreMapID"
48
- :label="option.scoreText"
49
- :value="option.scoreMapID"
50
- style="height: auto"
51
- >
52
- <div class="select-top">{{ option.scoreText }}</div>
53
- <div class="select-bottom">{{ option.reason }}</div>
54
- </el-option>
55
- </el-select>
56
- <div class="option-reason">{{ selectedOptionReason(node) }}</div>
57
- </div>
58
- <div class="qual-cell qual-cell-score">
59
- <span class="qual-inline-label">
60
- {{ scoreLabel(group, node) }}
61
- </span>
62
- <span class="qual-score-value">
63
- {{ formatScore(node.result) }}
64
- </span>
65
- </div>
66
- <div class="qual-cell qual-cell-reason">
67
- <span class="qual-inline-label">评价理由</span>
68
- <el-input
69
- class="qual-reason-input"
70
- type="textarea"
71
- v-title
72
- :show-overflow-tooltip="true"
73
- :maxlength="2000"
74
- show-word-limit
75
- v-model="node.reason"
76
- :disabled="readonly"
77
- placeholder="请输入具体评价依据,不超过2000字"
78
- @input="onReasonInput(node)"
79
- />
80
- </div>
81
- </div>
77
+ <div class="select-top">{{ option.scoreText }}</div>
78
+ <div class="select-bottom">{{ option.reason }}</div>
79
+ </el-option>
80
+ </el-select>
81
+ <el-input
82
+ v-else-if="row.sourceType === 3"
83
+ class="qual-manual-input"
84
+ type="number"
85
+ size="small"
86
+ v-model="row.scoreValue"
87
+ :disabled="readonly"
88
+ placeholder="请输入评分"
89
+ />
90
+ <span v-else>—</span>
91
+ </template>
92
+ </vxe-column>
82
93
 
83
- <!-- 手工填报 -->
84
- <div
85
- v-else-if="node.sourceType === 3"
86
- :key="'manual-' + (node._rowKey || i)"
87
- class="qual-row"
88
- >
89
- <div class="qual-cell qual-cell-name" v-title="node.name">
90
- {{ node.name }}
91
- </div>
92
- <div class="qual-cell qual-cell-manual">
93
- <el-input
94
- class="qual-manual-input"
95
- type="number"
96
- size="small"
97
- v-model="node.scoreValue"
98
- :disabled="readonly"
99
- placeholder="请输入评分"
100
- />
101
- </div>
102
- </div>
103
- </template>
104
- </div>
94
+ <vxe-column :title="scoreColTitle(group)" width="120" align="center">
95
+ <template #default="{ row }">
96
+ {{ formatIndicatorScore(group, row) }}
97
+ </template>
98
+ </vxe-column>
99
+
100
+ <vxe-column
101
+ v-if="showModelScoreCol"
102
+ title="模型指标得分"
103
+ width="130"
104
+ align="center"
105
+ >
106
+ <template #default="{ row }">{{ formatScore(row.result) }}</template>
107
+ </vxe-column>
108
+
109
+ <vxe-column title="评价理由" min-width="240" align="center">
110
+ <template #default="{ row }">
111
+ <el-input
112
+ v-if="isQualSelectNode(row)"
113
+ class="qual-reason-input"
114
+ type="textarea"
115
+ v-title
116
+ :show-overflow-tooltip="true"
117
+ :maxlength="2000"
118
+ show-word-limit
119
+ v-model="row.reason"
120
+ :disabled="readonly"
121
+ placeholder="请输入具体评价依据,不超过2000字"
122
+ @input="onReasonInput(row)"
123
+ />
124
+ </template>
125
+ </vxe-column>
126
+ </vxe-table>
105
127
  </div>
106
128
  </div>
107
129
  </template>
108
130
 
109
131
  <script>
110
- import { resultByShowStyle, showLevelSerial, isQualIndicator } from './helpers'
132
+ import {
133
+ resultByShowStyle,
134
+ indicatorScoreText,
135
+ showLevelSerial,
136
+ isQualIndicator,
137
+ fullScoreText
138
+ } from './helpers'
111
139
 
112
140
  export default {
113
141
  name: 'MeasureQualAnalysis',
@@ -128,6 +156,12 @@ export default {
128
156
  showSerial() {
129
157
  return showLevelSerial(this.modelWrap)
130
158
  },
159
+ showWeightCol() {
160
+ return this.modelWrap.showWeight === 1 && !this.measureCtx().calcMode
161
+ },
162
+ showModelScoreCol() {
163
+ return !this.measureCtx().calcMode
164
+ },
131
165
  /** 始终绑定 leafList 原始节点,避免 vxe 展开区副本导致 v-model 丢失 */
132
166
  displayGroups() {
133
167
  const resolve = this.measureCtx().resolveLeafNode
@@ -156,16 +190,26 @@ export default {
156
190
  (isQualIndicator(node) && node.optionList && node.optionList.length)
157
191
  )
158
192
  },
159
- scoreLabel(group, node) {
193
+ scoreColTitle(group) {
194
+ if (group.itemType === 11) return '系数值'
195
+ return '指标得分'
196
+ },
197
+ formatIndicatorScore(group, node) {
160
198
  if (group.itemType === 11) {
161
- return node.ratioName ? `${node.ratioName}系数值` : '系数值'
199
+ return this.formatScore(node.result)
162
200
  }
163
- return '得分'
201
+ return indicatorScoreText(node, this.modelWrap)
164
202
  },
165
203
  formatScore(val) {
166
204
  if (val === '' || val == null) return ''
167
205
  return resultByShowStyle({ result: val }, this.modelWrap)
168
206
  },
207
+ formatFullScore(node) {
208
+ return fullScoreText(node, this.modelWrap) || '—'
209
+ },
210
+ evalStandardText(node) {
211
+ return node.indicatorRemark || this.selectedOptionReason(node) || '—'
212
+ },
169
213
  resolveNode(node) {
170
214
  const resolve = this.measureCtx().resolveLeafNode
171
215
  return resolve ? resolve(node) || node : node
@@ -293,105 +337,14 @@ export default {
293
337
 
294
338
  .qual-table {
295
339
  width: 100%;
296
- border: 1px solid #ebeef5;
297
- border-radius: 2px;
298
- overflow: hidden;
299
- }
300
-
301
- .qual-row {
302
- display: flex;
303
- width: 100%;
304
- min-height: 52px;
305
- border-bottom: 1px solid #ebeef5;
306
- box-sizing: border-box;
307
-
308
- &:last-child {
309
- border-bottom: none;
310
- }
311
- }
312
-
313
- .qual-cell {
314
- padding: 10px 12px;
315
- border-right: 1px solid #ebeef5;
316
- box-sizing: border-box;
317
- min-width: 0;
318
- font-size: 14px;
319
- color: #303133;
320
- line-height: 1.5;
321
-
322
- &:last-child {
323
- border-right: none;
324
- }
325
- }
326
-
327
- /* 指标名称 | 选项 | 得分 | 评价理由 */
328
- .qual-cell-name {
329
- flex: 0 0 16%;
330
- background: #fafafa;
331
- color: #606266;
332
- display: flex;
333
- align-items: center;
334
- word-break: break-all;
335
- }
336
-
337
- .qual-cell-select {
338
- flex: 0 0 28%;
339
- display: flex;
340
- flex-direction: column;
341
- justify-content: center;
342
- }
343
-
344
- .qual-cell-score {
345
- flex: 0 0 12%;
346
- display: flex;
347
- flex-direction: column;
348
- justify-content: center;
349
- gap: 4px;
350
- }
351
-
352
- .qual-cell-reason {
353
- flex: 0 0 44%;
354
- display: flex;
355
- flex-direction: column;
356
- gap: 6px;
357
- }
358
-
359
- .qual-cell-manual {
360
- flex: 1;
361
- display: flex;
362
- align-items: center;
363
- }
364
-
365
- .qual-inline-label {
366
- flex-shrink: 0;
367
- font-size: 14px;
368
- color: #606266;
369
- line-height: 1.4;
370
- }
371
-
372
- .qual-score-value {
373
- min-height: 20px;
374
- word-break: break-all;
375
340
  }
376
341
 
342
+ .qual-select,
377
343
  .qual-reason-input,
378
344
  .qual-manual-input {
379
345
  width: 100%;
380
346
  }
381
347
 
382
- .qual-select {
383
- width: 100%;
384
- }
385
-
386
- .option-reason {
387
- margin-top: 4px;
388
- color: #999;
389
- font-size: 14px;
390
- line-height: 1.4;
391
- white-space: normal;
392
- word-wrap: break-word;
393
- }
394
-
395
348
  .select-top {
396
349
  margin-top: 2px;
397
350
  height: 14px;
@@ -409,7 +362,15 @@ export default {
409
362
  white-space: normal;
410
363
  word-wrap: break-word;
411
364
  }
365
+
412
366
  /deep/ .el-select .el-input__inner {
413
367
  text-align: center;
414
368
  }
369
+
370
+ /deep/ .vxe-body--column {
371
+ padding-top: 6px;
372
+ padding-bottom: 6px;
373
+ height: auto !important;
374
+ max-height: unset !important;
375
+ }
415
376
  </style>
@@ -53,6 +53,18 @@
53
53
  :d-num="digits"
54
54
  v-model="cell.value"
55
55
  />
56
+ <el-tooltip
57
+ v-else-if="showFormulaTip"
58
+ placement="top"
59
+ effect="dark"
60
+ >
61
+ <div slot="content">
62
+ {{ cell.displayFormula }}:
63
+ <br />
64
+ {{ cell.relFormula }}
65
+ </div>
66
+ <span>{{ displayValue(cell) }}</span>
67
+ </el-tooltip>
56
68
  <span v-else>{{ displayValue(cell) }}</span>
57
69
  </template>
58
70
  </div>
@@ -74,6 +86,11 @@ export default {
74
86
  computed: {
75
87
  readonly() {
76
88
  return !!this.measureCtx().readonly
89
+ },
90
+ /** 指标值列才展示公式;得分列 column.disabled 不展示 */
91
+ showFormulaTip() {
92
+ if (!this.cell || this.column.disabled) return false
93
+ return !!(this.cell.displayFormula || this.cell.relFormula)
77
94
  }
78
95
  },
79
96
  methods: {
@@ -53,8 +53,24 @@
53
53
  </el-select>
54
54
  </template>
55
55
  <template v-else>
56
- <span
56
+ <el-tooltip
57
57
  v-if="
58
+ row.sourceType !== 3 &&
59
+ (!row.optionList || !row.optionList.length) &&
60
+ hasFormulaFn(row)
61
+ "
62
+ placement="top"
63
+ effect="dark"
64
+ >
65
+ <div slot="content">
66
+ {{ row.displayFormula }}:
67
+ <br />
68
+ {{ row.relFormula }}
69
+ </div>
70
+ <span>{{ indicatorTextFn(row) }}</span>
71
+ </el-tooltip>
72
+ <span
73
+ v-else-if="
58
74
  row.sourceType !== 3 &&
59
75
  (!row.optionList || !row.optionList.length)
60
76
  "
@@ -175,6 +191,9 @@ export default {
175
191
  indicatorTextFn(row) {
176
192
  return indicatorResultText(row, this.ctx.modelWrap)
177
193
  },
194
+ hasFormulaFn(row) {
195
+ return !!(row && (row.displayFormula || row.relFormula))
196
+ },
178
197
  onQualChange(row) {
179
198
  const option = row.value
180
199
  if (!option || !option.scoreMapID) return
@@ -32,14 +32,14 @@
32
32
  <el-table-column
33
33
  prop="name"
34
34
  label="指标项名称"
35
- width="250"
35
+ min-width="160"
36
36
  align="center"
37
37
  />
38
38
  <el-table-column
39
39
  v-for="col in dynamicColumns(block)"
40
40
  :key="col.prop"
41
41
  :label="col.label"
42
- :width="col.width || 250"
42
+ :min-width="col.minWidth || 140"
43
43
  align="center"
44
44
  >
45
45
  <template slot-scope="{ row }">
@@ -1,9 +1,10 @@
1
1
  <template>
2
2
  <div class="measure-sub-model-panel">
3
- <!-- 计算模型:树形表格 + 定性分组描述列表 -->
4
- <measure-tree-table
3
+ <!-- 计算模型:分组外提为区块标题,指标用树表 -->
4
+ <measure-group-blocks
5
5
  v-if="isCalc"
6
6
  :nodes="subModel.children || []"
7
+ :group-node="subModel"
7
8
  :level="level"
8
9
  :number-prefix="numberPrefix"
9
10
  calc-mode
@@ -38,7 +39,7 @@ export default {
38
39
  components: {
39
40
  MeasureEvalFlatTable,
40
41
  MeasureStatPanel,
41
- MeasureTreeTable: () => import('./MeasureTreeTable.vue')
42
+ MeasureGroupBlocks: () => import('./MeasureGroupBlocks.vue')
42
43
  },
43
44
  props: {
44
45
  subModel: {
@@ -229,7 +229,7 @@
229
229
  </vxe-column>
230
230
  </template>
231
231
 
232
- <!-- 默认列:指标满分值 / 指标值 / 指标得分 / 理由 -->
232
+ <!-- 默认列:满分值 / 计算公式 / 评价标准 / 指标值 / 指标得分 / 模型指标得分 / 理由 -->
233
233
  <template v-else>
234
234
  <vxe-column title="指标满分值" width="110" align="center">
235
235
  <template #default="{ row }">
@@ -238,6 +238,25 @@
238
238
  </template>
239
239
  </vxe-column>
240
240
 
241
+ <vxe-column
242
+ v-if="showFormulaCol"
243
+ field="displayFormula"
244
+ title="计算公式"
245
+ width="130"
246
+ align="center"
247
+ >
248
+ <template #default="{ row }">{{ row.displayFormula || '—' }}</template>
249
+ </vxe-column>
250
+ <vxe-column
251
+ v-if="showRemarkCol"
252
+ field="indicatorRemark"
253
+ title="评价标准"
254
+ width="130"
255
+ align="center"
256
+ >
257
+ <template #default="{ row }">{{ row.indicatorRemark || '—' }}</template>
258
+ </vxe-column>
259
+
241
260
  <vxe-column title="指标值" min-width="260" align="center">
242
261
  <template #default="{ row }">
243
262
  <template v-if="isQuantIndicatorFn(row)">
@@ -298,6 +317,17 @@
298
317
  </vxe-column>
299
318
 
300
319
  <vxe-column title="指标得分" width="140" align="center">
320
+ <template #default="{ row }">
321
+ <template v-if="isQuantIndicatorFn(row)">
322
+ <div>{{ indicatorScoreFn(row) }}</div>
323
+ </template>
324
+ <template v-else-if="isSubModelFn(row) && ctx.parentCalced">
325
+ <div>{{ indicatorScoreFn(row) }}</div>
326
+ </template>
327
+ <span v-else class="color-placeholder">—</span>
328
+ </template>
329
+ </vxe-column>
330
+ <vxe-column title="模型指标得分" width="140" align="center">
301
331
  <template #default="{ row }">
302
332
  <template v-if="isQuantIndicatorFn(row)">
303
333
  <div>{{ resultFn(row) }}</div>
@@ -310,7 +340,7 @@
310
340
  :max="row.fullScore"
311
341
  :disabled="ctx.readonly"
312
342
  v-model="row.modifiedResult"
313
- placeholder="修改指标得分"
343
+ placeholder="修改模型指标得分"
314
344
  />
315
345
  </template>
316
346
  <template v-else-if="isSubModelFn(row) && ctx.parentCalced">
@@ -319,24 +349,6 @@
319
349
  <span v-else class="color-placeholder">—</span>
320
350
  </template>
321
351
  </vxe-column>
322
- <vxe-column
323
- v-if="showFormulaCol"
324
- field="displayFormula"
325
- title="计算公式"
326
- width="130"
327
- align="center"
328
- >
329
- <template #default="{ row }">{{ row.displayFormula || '—' }}</template>
330
- </vxe-column>
331
- <vxe-column
332
- v-if="showRemarkCol"
333
- field="indicatorRemark"
334
- title="评价标准"
335
- width="130"
336
- align="center"
337
- >
338
- <template #default="{ row }">{{ row.indicatorRemark || '—' }}</template>
339
- </vxe-column>
340
352
 
341
353
  <vxe-column
342
354
  v-if="showModifyReasonCol"
@@ -404,6 +416,7 @@ import {
404
416
  typeMeta,
405
417
  digitsCalc,
406
418
  resultByShowStyle,
419
+ indicatorScoreText,
407
420
  indicatorResultText,
408
421
  fullScoreText,
409
422
  hierarchySerial,
@@ -567,6 +580,9 @@ export default {
567
580
  resultFn(row) {
568
581
  return resultByShowStyle(row, this.ctx.modelWrap)
569
582
  },
583
+ indicatorScoreFn(row) {
584
+ return indicatorScoreText(row, this.ctx.modelWrap)
585
+ },
570
586
  indicatorTextFn(row) {
571
587
  return indicatorResultText(row, this.ctx.modelWrap)
572
588
  },
@@ -8,6 +8,7 @@
8
8
  * sourceType:0 系统自定义 1 报表取数 2 指标取数 3 手工填报 4 标准值取数 5 系统取值 6 系统取数
9
9
  * modifyRank:0 不调整 1 调整指标得分 2 调整指标值
10
10
  */
11
+ import { N } from 'n20-common-lib'
11
12
  import { toFixedTrunc, addThousands } from '../common'
12
13
  import { resolveManualFillValue, statItemChildren } from './statModelHelper'
13
14
 
@@ -83,6 +84,70 @@ export function isSubModel(row) {
83
84
  return t === 'calcModel' || t === 'evalModel' || t === 'statModel'
84
85
  }
85
86
 
87
+ /** 普通分组(非子模型容器) */
88
+ export function isGroup(row) {
89
+ return isContainer(row) && !isSubModel(row)
90
+ }
91
+
92
+ /** 当前层拆成普通分组 vs 其余(指标/子模型),分组抬到表格外展示 */
93
+ export function splitGroupNodes(nodes) {
94
+ const groups = []
95
+ const loose = []
96
+ if (!Array.isArray(nodes)) return { groups, loose }
97
+ nodes.forEach(node => {
98
+ if (isGroup(node)) groups.push(node)
99
+ else loose.push(node)
100
+ })
101
+ return { groups, loose }
102
+ }
103
+
104
+ /** 节点用于汇总的得分(优先调整后得分) */
105
+ export function nodeScoreValue(node) {
106
+ if (!node) return null
107
+ if (
108
+ node.modifyRank === 1 &&
109
+ node.modifiedResult !== '' &&
110
+ node.modifiedResult != null
111
+ ) {
112
+ return node.modifiedResult
113
+ }
114
+ if (node.result === '' || node.result == null) return null
115
+ return node.result
116
+ }
117
+
118
+ /** 收集分组下叶子指标(子模型不计入得分小计) */
119
+ export function collectIndicatorScoreNodes(nodes, bucket = []) {
120
+ if (!Array.isArray(nodes)) return bucket
121
+ nodes.forEach(n => {
122
+ if (!n) return
123
+ if (isIndicator(n)) {
124
+ bucket.push(n)
125
+ return
126
+ }
127
+ if (isSubModel(n)) return
128
+ if (n.children && n.children.length) {
129
+ collectIndicatorScoreNodes(n.children, bucket)
130
+ }
131
+ })
132
+ return bucket
133
+ }
134
+
135
+ /** 分组得分小计:加总下属指标得分,无有效得分返回 null */
136
+ export function sumIndicatorScores(nodes) {
137
+ const items = collectIndicatorScoreNodes(nodes)
138
+ let has = false
139
+ let total = 0
140
+ items.forEach(n => {
141
+ const val = nodeScoreValue(n)
142
+ if (val === null) return
143
+ const num = Number(val)
144
+ if (Number.isNaN(num)) return
145
+ has = true
146
+ total += num
147
+ })
148
+ return has ? total : null
149
+ }
150
+
86
151
  /** 测算场景下子模型的统一展示名称 */
87
152
  export const SUB_MODEL_LABEL = '测算子模型'
88
153
 
@@ -152,7 +217,7 @@ export function digitsCalc(row, modelWrap) {
152
217
  return modelWrap ? modelWrap.digits : 2
153
218
  }
154
219
 
155
- /** 指标得分 / 模型指标系数数值展示(始终用 result,字段值只在「指标值」列展示) */
220
+ /** 模型指标得分 / 模型指标系数数值(权重之后的 result */
156
221
  export function resultByShowStyle(row, modelWrap) {
157
222
  let val = ''
158
223
  const digits = digitsCalc(row, modelWrap)
@@ -168,6 +233,21 @@ export function resultByShowStyle(row, modelWrap) {
168
233
  return val
169
234
  }
170
235
 
236
+ /**
237
+ * 指标得分(权重之前):result * 100 / weight
238
+ * 与旧测算详情 valueScore 算法一致;无权重时不展示
239
+ */
240
+ export function indicatorScoreText(row, modelWrap) {
241
+ if (!row || (row.result !== 0 && (row.result === '' || row.result == null))) {
242
+ return ''
243
+ }
244
+ if (!row.weight) {
245
+ return resultByShowStyle(row, modelWrap)
246
+ }
247
+ const digits = digitsCalc(row, modelWrap)
248
+ return N.div(N.mul(row.result, 100, digits + 2), row.weight, digits)
249
+ }
250
+
171
251
  /** 指标满分值展示 */
172
252
  export function fullScoreText(row, modelWrap) {
173
253
  if (!row || (row.fullScore !== 0 && !row.fullScore)) return ''
@@ -29,6 +29,15 @@
29
29
  >
30
30
  {{ allExpanded ? '全部收起' : '全部展开' }}
31
31
  </el-button>
32
+ <el-button
33
+ v-if="showPrint"
34
+ size="small"
35
+ plain
36
+ :loading="printLoading"
37
+ @click="printFn"
38
+ >
39
+ 打印
40
+ </el-button>
32
41
  </div>
33
42
  </div>
34
43
 
@@ -210,13 +219,10 @@
210
219
  :sub-model="section.container"
211
220
  :level="1"
212
221
  />
213
- <measure-qual-analysis
214
- v-else-if="isQualOnlySection(section)"
215
- :groups="qualSectionGroups(section)"
216
- />
217
- <measure-tree-table
222
+ <measure-group-blocks
218
223
  v-else-if="section.nodes && section.nodes.length"
219
224
  :nodes="section.nodes"
225
+ :group-node="section.container"
220
226
  :level="1"
221
227
  :calc-mode="calcRootModel"
222
228
  :section="section"
@@ -228,8 +234,7 @@
228
234
 
229
235
  <script>
230
236
  import { SecondaryTab } from './ui'
231
- import MeasureTreeTable from './MeasureTreeTable.vue'
232
- import MeasureQualAnalysis from './MeasureQualAnalysis.vue'
237
+ import MeasureGroupBlocks from './MeasureGroupBlocks.vue'
233
238
  import MeasureSubModelPanel from './MeasureSubModelPanel.vue'
234
239
  import MeasureStatPanel from './MeasureStatPanel.vue'
235
240
  import {
@@ -238,9 +243,10 @@ import {
238
243
  isIndicator,
239
244
  isSubModel,
240
245
  isStatModel,
241
- isQualOnlyNodes,
242
246
  isQualIndicator,
243
247
  resolveQualSelection,
248
+ sumIndicatorScores,
249
+ nodeScoreValue as readNodeScore,
244
250
  SUB_MODEL_LABEL,
245
251
  SUB_MODEL_CALC_BTN,
246
252
  isCalcRootModel as checkCalcRootModel,
@@ -255,13 +261,13 @@ import {
255
261
  } from './statModelHelper'
256
262
  import { withRequestTimestamp } from './api'
257
263
  import { formatFloat2 } from '../common'
264
+ import { printJS } from 'n20-common-lib/src/plugins/Print'
258
265
 
259
266
  export default {
260
267
  name: 'MeasureCalc',
261
268
  components: {
262
269
  SecondaryTab,
263
- MeasureTreeTable,
264
- MeasureQualAnalysis,
270
+ MeasureGroupBlocks,
265
271
  MeasureSubModelPanel,
266
272
  MeasureStatPanel
267
273
  },
@@ -276,7 +282,11 @@ export default {
276
282
  readonly: this.readonly,
277
283
  resolveLeafNode: this.resolveLeafNode,
278
284
  syncQualSelection: this.syncQualSelection,
279
- getQualSelection: this.getQualSelection
285
+ getQualSelection: this.getQualSelection,
286
+ evalRootModel: this.evalRootModel,
287
+ groupWeightText: this.groupWeightText,
288
+ groupScoreText: this.groupScoreText,
289
+ groupSubtotalText: this.groupSubtotalText
280
290
  })
281
291
  }
282
292
  },
@@ -315,12 +325,28 @@ export default {
315
325
  condParam: {
316
326
  type: Object,
317
327
  default: () => ({})
328
+ },
329
+ /** 是否展示打印按钮,由业务系统做权限判断后传入,默认不展示 */
330
+ showPrint: {
331
+ type: Boolean,
332
+ default: false
333
+ },
334
+ /** 授信申报信息,打印时原样传给打印接口 */
335
+ creditInfo: {
336
+ type: Object,
337
+ default: () => ({})
338
+ },
339
+ /** 评级申报信息,打印时原样传给打印接口 */
340
+ gradeInfo: {
341
+ type: Object,
342
+ default: () => ({})
318
343
  }
319
344
  },
320
345
  data() {
321
346
  return {
322
347
  loading: false,
323
348
  calcLoading: false,
349
+ printLoading: false,
324
350
  model: { type: 1, name: '', code: '', digits: 2, modelTreeVO: {} },
325
351
  sections: [],
326
352
  anchorTabs: [],
@@ -352,18 +378,18 @@ export default {
352
378
  messageStr: '',
353
379
  options: [
354
380
  { value: '13', label: '13月(已审计报表)' },
355
- { value: '1', label: '1月' },
356
- { value: '2', label: '2月' },
357
- { value: '3', label: '3月' },
358
- { value: '4', label: '4月' },
359
- { value: '5', label: '5月' },
360
- { value: '6', label: '6月' },
361
- { value: '7', label: '7月' },
362
- { value: '8', label: '8月' },
363
- { value: '9', label: '9月' },
364
- { value: '10', label: '10月' },
381
+ { value: '12', label: '12月' },
365
382
  { value: '11', label: '11月' },
366
- { value: '12', label: '12月' }
383
+ { value: '10', label: '10月' },
384
+ { value: '9', label: '9月' },
385
+ { value: '8', label: '8月' },
386
+ { value: '7', label: '7月' },
387
+ { value: '6', label: '6月' },
388
+ { value: '5', label: '5月' },
389
+ { value: '4', label: '4月' },
390
+ { value: '3', label: '3月' },
391
+ { value: '2', label: '2月' },
392
+ { value: '1', label: '1月' }
367
393
  ]
368
394
  }
369
395
  },
@@ -423,9 +449,20 @@ export default {
423
449
  }
424
450
  return this.relFormula || this.displayFormula || '--'
425
451
  },
426
- /** 是否存在容器节点(决定是否显示「全部展开/收起」) */
452
+ /** 表格内仍可展开的容器(分组已外提,只剩子模型) */
427
453
  hasAnyContainer() {
428
- return this.sections.some(s => (s.nodes || []).some(n => isContainer(n)))
454
+ const walk = nodes => {
455
+ if (!Array.isArray(nodes)) return false
456
+ return nodes.some(n => {
457
+ if (isSubModel(n)) return true
458
+ if (n && n.children && n.children.length) return walk(n.children)
459
+ return false
460
+ })
461
+ }
462
+ return this.sections.some(s => {
463
+ if (s.container && isSubModel(s.container)) return true
464
+ return walk(s.nodes)
465
+ })
429
466
  },
430
467
  /** 仍有必填项未完善时显示黄字提示 */
431
468
  showIncompleteTip() {
@@ -476,17 +513,6 @@ export default {
476
513
  subModelResultText(node) {
477
514
  return formatSubModelResult(node, this.model)
478
515
  },
479
- isQualOnlySection(section) {
480
- return isQualOnlyNodes(section.nodes)
481
- },
482
- qualSectionGroups(section) {
483
- return [
484
- {
485
- name: '',
486
- children: (section.nodes || []).map(n => this.resolveLeafNode(n) || n)
487
- }
488
- ]
489
- },
490
516
  /** 表格展开区可能是副本节点,统一解析到 leafList / qualList 原始引用 */
491
517
  resolveLeafNode(node) {
492
518
  return this.findQualNode(node && node.tid, node && node.itemID, node)
@@ -842,25 +868,28 @@ export default {
842
868
  return false
843
869
  },
844
870
 
845
- /** 单个指标的入参校验与组装 */
871
+ /** 单个指标的入参校验与组装(同一指标只组装一条,避免后端 findFirst 丢调整得分) */
846
872
  checkItem(item) {
847
873
  const obj = { scoreMapList: [], indicatorCalcList: [], state: true }
874
+ const displayValue =
875
+ item.displayStyle === 1
876
+ ? formatFloat2(item.value / 100, this.model.digits)
877
+ : item.value
878
+ const calcItem = {
879
+ hasInput: true,
880
+ indicatorID: item.itemID,
881
+ tid: item.tid,
882
+ modifiedReason: item.modifiedReason || ''
883
+ }
884
+ let hasCalc = false
848
885
  // 手工填报(含统计模型行转列内的可量化指标)
849
886
  if (item.sourceType === 3 && !isQualIndicator(item)) {
850
887
  if (!item.value && item.value !== 0) {
851
888
  obj.state = false
852
889
  this.messageStr += `${item.name}的指标值不可为空;`
853
890
  }
854
- obj.indicatorCalcList.push({
855
- hasInput: true,
856
- indicatorID: item.itemID,
857
- tid: item.tid,
858
- modifiedReason: item.modifiedReason || '',
859
- scoreValue:
860
- item.displayStyle === 1
861
- ? formatFloat2(item.value / 100, this.model.digits)
862
- : item.value
863
- })
891
+ calcItem.scoreValue = displayValue
892
+ hasCalc = true
864
893
  }
865
894
  // 不可量化 + 有选项:校验选项(optionList 以 qualList 为准)
866
895
  if (isQualIndicator(item)) {
@@ -913,16 +942,8 @@ export default {
913
942
  item.indicatorResult !== null &&
914
943
  (item.value || item.value === 0)
915
944
  ) {
916
- obj.indicatorCalcList.push({
917
- hasInput: true,
918
- indicatorID: item.itemID,
919
- tid: item.tid,
920
- modifiedReason: item.modifiedReason || '',
921
- modifiedVal:
922
- item.displayStyle === 1
923
- ? formatFloat2(item.value / 100, this.model.digits)
924
- : item.value
925
- })
945
+ calcItem.modifiedVal = displayValue
946
+ hasCalc = true
926
947
  }
927
948
  // 调整项=指标得分
928
949
  if (
@@ -931,14 +952,10 @@ export default {
931
952
  item.modifyRank === 1 &&
932
953
  (item.modifiedResult || item.modifiedResult === 0)
933
954
  ) {
934
- obj.indicatorCalcList.push({
935
- hasInput: true,
936
- indicatorID: item.itemID,
937
- tid: item.tid,
938
- modifiedReason: item.modifiedReason || '',
939
- modifiedResult: item.modifiedResult
940
- })
955
+ calcItem.modifiedResult = item.modifiedResult
956
+ hasCalc = true
941
957
  }
958
+ if (hasCalc) obj.indicatorCalcList.push(calcItem)
942
959
  return obj
943
960
  },
944
961
 
@@ -1032,6 +1049,42 @@ export default {
1032
1049
  }
1033
1050
  },
1034
1051
 
1052
+ async printFn() {
1053
+ const logId = this.model.logId || this.logId || ''
1054
+ const modelId = this.model.modelID || this.model.id || this.id || ''
1055
+ if (!logId || !modelId) {
1056
+ this.$message.warning('请先完成测算后再打印')
1057
+ return
1058
+ }
1059
+ this.printLoading = true
1060
+ try {
1061
+ const res = await this.$axios.post(
1062
+ withRequestTimestamp('/imes/v1/indicatorModel/print'),
1063
+ [
1064
+ {
1065
+ logId,
1066
+ modelId,
1067
+ creditInfo: this.creditInfo || {},
1068
+ gradeInfo: this.gradeInfo || {}
1069
+ }
1070
+ ]
1071
+ )
1072
+ if (res.code !== 0) {
1073
+ this.$message.error(res.msg || '打印失败')
1074
+ return
1075
+ }
1076
+ printJS({
1077
+ printable: res.data,
1078
+ type: 'pdf',
1079
+ base64: true
1080
+ })
1081
+ } catch (e) {
1082
+ this.$message.error('打印异常')
1083
+ } finally {
1084
+ this.printLoading = false
1085
+ }
1086
+ },
1087
+
1035
1088
  /** 外层模型整体测算 */
1036
1089
  async calc() {
1037
1090
  if (this.readonly) return
@@ -1297,16 +1350,7 @@ export default {
1297
1350
 
1298
1351
  /** 节点用于汇总的得分(优先调整后得分) */
1299
1352
  nodeScoreValue(node) {
1300
- if (!node) return null
1301
- if (
1302
- node.modifyRank === 1 &&
1303
- node.modifiedResult !== '' &&
1304
- node.modifiedResult != null
1305
- ) {
1306
- return node.modifiedResult
1307
- }
1308
- if (node.result === '' || node.result == null) return null
1309
- return node.result
1353
+ return readNodeScore(node)
1310
1354
  },
1311
1355
 
1312
1356
  formatScoreNum(val) {
@@ -1385,23 +1429,46 @@ export default {
1385
1429
  },
1386
1430
 
1387
1431
  /** 分区标题旁总权重(评价模型且开启显示权重) */
1388
- sectionWeightText(section) {
1389
- if (!this.evalRootModel || !section) return ''
1432
+ groupWeightText(groupNode, children) {
1433
+ if (!this.evalRootModel || !groupNode) return ''
1390
1434
  if (this.model.showWeight !== 1) return ''
1391
- if (!section.container || this.isSubModel(section.container)) return ''
1392
- return this.buildGroupWeightText(section.container, section.nodes)
1435
+ if (this.isSubModel(groupNode)) return ''
1436
+ return this.buildGroupWeightText(
1437
+ groupNode,
1438
+ children || groupNode.children
1439
+ )
1440
+ },
1441
+ sectionWeightText(section) {
1442
+ if (!section || !section.container) return ''
1443
+ return this.groupWeightText(section.container, section.nodes)
1393
1444
  },
1394
1445
 
1395
- /** 分区标题旁得分(评价模型:未测算显示 --) */
1396
- sectionScoreText(section) {
1397
- if (!this.evalRootModel || !section) return ''
1398
- if (!section.container || this.isSubModel(section.container)) return ''
1446
+ /** 分组标题旁得分(评价模型:未测算显示 --) */
1447
+ groupScoreText(groupNode, children) {
1448
+ if (!this.evalRootModel || !groupNode) return ''
1449
+ if (this.isSubModel(groupNode)) return ''
1399
1450
  if (!this.parentCalced) return '总得分: --'
1400
1451
  return (
1401
- this.buildGroupScoreText(section.container, section.nodes) ||
1402
- '总得分: --'
1452
+ this.buildGroupScoreText(
1453
+ groupNode,
1454
+ children || groupNode.children
1455
+ ) || '总得分: --'
1403
1456
  )
1404
1457
  },
1458
+ sectionScoreText(section) {
1459
+ if (!section || !section.container) return ''
1460
+ return this.groupScoreText(section.container, section.nodes)
1461
+ },
1462
+
1463
+ /** 内层分组标题旁得分小计:加总下属指标得分 */
1464
+ groupSubtotalText(groupNode, children) {
1465
+ if (!this.evalRootModel || !groupNode) return ''
1466
+ if (this.isSubModel(groupNode)) return ''
1467
+ if (!this.parentCalced) return '得分小计: --'
1468
+ const total = sumIndicatorScores(children || groupNode.children || [])
1469
+ if (total === null) return '得分小计: --'
1470
+ return `得分小计: ${this.formatScoreNum(total)}`
1471
+ },
1405
1472
 
1406
1473
  /**
1407
1474
  * 评价模型得分文案:总分(57) = 定量分析(7) + 定性分析(50)
@@ -289,7 +289,7 @@ export async function buildStatModelTables(treeData, fetchScoreMap, modelWrap) {
289
289
  )
290
290
 
291
291
  const columns = [
292
- { label: '指标项名称', prop: 'name', width: '250px', align: 'center' }
292
+ { label: '指标项名称', prop: 'name', minWidth: 160, align: 'center' }
293
293
  ]
294
294
  const list = []
295
295
 
@@ -302,7 +302,7 @@ export async function buildStatModelTables(treeData, fetchScoreMap, modelWrap) {
302
302
  columns.push({
303
303
  label,
304
304
  prop: label,
305
- width: '250px',
305
+ minWidth: 140,
306
306
  align: 'center',
307
307
  slotName: label
308
308
  })
@@ -313,7 +313,7 @@ export async function buildStatModelTables(treeData, fetchScoreMap, modelWrap) {
313
313
  prop: item.statsName + '指标得分',
314
314
  slotName: item.statsName + '指标得分',
315
315
  disabled: true,
316
- width: '250px',
316
+ minWidth: 140,
317
317
  align: 'center'
318
318
  })
319
319
  }