ocpview-pro 0.1.30 → 0.1.33

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": "ocpview-pro",
3
- "version": "0.1.30",
3
+ "version": "0.1.33",
4
4
  "title": "ocpviewPlus",
5
5
  "description": "A high quality Service UI components Library with Vue.js",
6
6
  "homepage": "",
@@ -40,7 +40,7 @@ export default {
40
40
  rowAction: [{
41
41
  iconcls: 'row',
42
42
  name: 'editRowForm',
43
- text: '编辑',
43
+ text: this.$t('编辑'),
44
44
  }, ],
45
45
  openChange: false,
46
46
  focusIndex: null,
@@ -54,7 +54,6 @@ export default {
54
54
  this.myConfig.cancelBtnName = this.$t('放弃');
55
55
  this.delBatchBtnName = this.$t('删除');
56
56
  this.addBtnName = this.$t('新增');
57
- this.rowAction[0].text = this.$t('编辑');
58
57
  },
59
58
  computed: {
60
59
  colKeys() {
@@ -3,6 +3,14 @@ import { deepCopy } from '../../utils/assist';
3
3
  import Base from './basegrid.vue';
4
4
  import elementResizeDetectorMaker from 'element-resize-detector';
5
5
  import {gzip} from 'pako';
6
+ import {
7
+ buildExportSearchParam,
8
+ buildSummaryRow,
9
+ buildSummaryMap,
10
+ collectSummaryFields,
11
+ translateDictValues,
12
+ DEFAULT_EXPORT_PAGE_SIZE
13
+ } from '@/method/listexport';
6
14
  export default {
7
15
  name: 'viewgridbase',
8
16
  extends: Base,
@@ -51,7 +59,13 @@ export default {
51
59
  dynamicColumnItems:[],
52
60
  tempMultiSelectData:[],
53
61
  multiSoltTableColumns:[],
54
- showHelpBtn:false
62
+ showHelpBtn:false,
63
+ /**
64
+ * 业务侧显式配置的 showSummary(undefined 表示"没配")。
65
+ * 用于区分"没配"与"显式关闭":没配时由列上的 sum: true 自动开启合计行,
66
+ * 显式 false 时即使有 sum 列也不显示合计。
67
+ */
68
+ userShowSummary: undefined
55
69
  };
56
70
  },
57
71
  computed:{
@@ -271,17 +285,65 @@ export default {
271
285
  if (this.dynamicInitData.total_results !== undefined) {
272
286
  this.myConfig.pageConfig.total_results= Number(this.dynamicInitData.total_results);
273
287
  }
288
+ // 合计行显示与否统一在这里裁决:业务侧只需在列上写 sum: true,
289
+ // 不必再配 showSummary(显式 showSummary: false 可强制关闭)。
274
290
  if (this.dynamicInitData.total_summary !== undefined) {
291
+ // 后端合计(报表链路):后端给了合计就是权威,直接展示
275
292
  this.total_summary = Object.assign({}, {}, this.dynamicInitData.total_summary);
276
- if (!this.myConfig.showSummary) {
277
- Object.assign(this.myConfig,{'showSummary':true});
278
- }
293
+ Object.assign(this.myConfig,{'showSummary':true});
279
294
  } else {
280
- if (this.myConfig.showSummary) {
295
+ // 前端合计:列上用 sum: true 标记时,合计由前端从当前页数据计算,
296
+ // 既不依赖后端 total_summary,也不把字段塞进查询参数
297
+ // (见 getFrontendSummaryFields / buildFrontendSummary)。
298
+ let frontendSummary = this.calcFrontendSummary();
299
+ let userShowSummary = this.getUserShowSummary();
300
+ if (frontendSummary && userShowSummary !== false) {
301
+ this.total_summary = frontendSummary;
302
+ Object.assign(this.myConfig,{'showSummary':true});
303
+ } else if (userShowSummary === false) {
304
+ // 业务显式关闭合计:清掉可能残留的合计值并隐藏合计行
305
+ this.total_summary = {};
306
+ Object.assign(this.myConfig,{'showSummary':false});
307
+ } else if (this.myConfig.showSummary && this.myConfig.customSummary) {
308
+ // 纯自定义合计:没有 sum 列时也保留合计行,由 customSummary 自行填值
309
+ this.total_summary = {};
310
+ } else if (this.myConfig.showSummary) {
311
+ this.total_summary = {};
281
312
  Object.assign(this.myConfig,{'showSummary':false});
282
313
  }
283
314
  }
284
315
  },
316
+ /**
317
+ * 业务侧显式配置的 showSummary:undefined 表示"没配"。
318
+ * 只有显式 false 才阻止 sum 列自动开启合计行。
319
+ */
320
+ getUserShowSummary() {
321
+ if (this.userShowSummary !== undefined) {
322
+ return this.userShowSummary;
323
+ }
324
+ if (this.config && this.config.showSummary !== undefined) {
325
+ return this.config.showSummary;
326
+ }
327
+ return undefined;
328
+ },
329
+ /**
330
+ * 前端合计计算兜底:合计只是锦上添花,任何异常都不允许影响列表正常
331
+ * 加载(refurbishSucceedDefault 在请求回调里执行,抛错会导致 loading
332
+ * 卡住、数据不渲染)。最坏表现 = 没有合计行。
333
+ */
334
+ calcFrontendSummary() {
335
+ try {
336
+ if (this.dynamicInitData.sumgroups) {
337
+ // 小计链路里 tempData 混有分组小计行,前端再求和会重复计数,
338
+ // 该链路的合计仍由后端 total_summary 提供
339
+ return null;
340
+ }
341
+ return this.buildFrontendSummary();
342
+ } catch (e) {
343
+ console.warn('[ocpview-pro] 前端合计计算失败,本次查询已跳过合计行:', e);
344
+ return null;
345
+ }
346
+ },
285
347
  callBackChangePage(flag ,newvalue, oldvale) {
286
348
  if (!flag) {
287
349
  this.$nextTick(() => {
@@ -537,6 +599,98 @@ export default {
537
599
  this.doExportExcel(_baseUrl, this.myConfig.method, _param,this.myConfig.exportType, _self);
538
600
  }
539
601
  },
602
+ /**
603
+ * 收集列上标记 sum: true 的字段(前端合计标记,支持多级表头的 children)。
604
+ *
605
+ * 与后端合计标记 summary: true 严格区分:summary 会让 basegrid 把字段塞进
606
+ * summaryColumn,进而被 getSearchParam 拼成 summary_fld 发给后端 —— 而后端
607
+ * StorageUtils 的豁免名单不含 summary_fld,会被当成 WHERE 条件拼进 SQL 而报错。
608
+ * sum 不进查询参数,合计一律由前端从当前页数据计算。
609
+ *
610
+ * 列上写了 sum: true 即视为业务要合计,合计行由 refurbishSucceedDefault 自动开启,
611
+ * 业务侧无需再配 showSummary。
612
+ */
613
+ getFrontendSummaryFields() {
614
+ return collectSummaryFields(this.myConfig.items);
615
+ },
616
+ /**
617
+ * 前端合计:从当前页数据(tempData)求和,返回 { field: number }。
618
+ * 无 sum 列时返回 null,表示"未配置前端合计"。
619
+ */
620
+ buildFrontendSummary() {
621
+ const fields = this.getFrontendSummaryFields();
622
+ if (fields.length === 0) {
623
+ return null;
624
+ }
625
+ return buildSummaryMap(this.tempData, fields);
626
+ },
627
+ /**
628
+ * 单据列表导出(通用):按配置的分页大小全量取数,前端生成 Excel 并附合计行。
629
+ * 与报表导出 export()(走 /exportExcel 后端接口)完全独立,互不影响。
630
+ * 与单据明细导出(detailExport,本期未实现)共用同一套纯逻辑。
631
+ */
632
+ listExport() {
633
+ const exportConfig = this.myConfig.exportConfig || {};
634
+ if (exportConfig.enabled === false) {
635
+ return;
636
+ }
637
+ if (this.data.length === 0) {
638
+ this.alert(this.$t('请先查询数据'));
639
+ return;
640
+ }
641
+ // 参数清洗(剔除 summary_fld/sumgroups)在 buildExportSearchParam 内完成
642
+ const param = buildExportSearchParam(
643
+ this.getSearchParam(),
644
+ exportConfig.pageSize || DEFAULT_EXPORT_PAGE_SIZE
645
+ );
646
+ this.loading = true;
647
+ const self = this;
648
+ this.asyncPost(this.myConfig.resources, this.myConfig.method, param, function (data) {
649
+ self.loading = false;
650
+ self.doListExport(data, exportConfig);
651
+ }, function (e) {
652
+ self.loading = false;
653
+ self.alert(e, null, 2);
654
+ });
655
+ },
656
+ doListExport(data, exportConfig) {
657
+ const tableKey = this.myConfig.response;
658
+ const rows = (data && data[tableKey]) || [];
659
+ // 刻意不调用 this.myConfig.overrideData:
660
+ // 1) 在 ListDetails 全族里,容器的 created() 会无条件把它覆盖成
661
+ // billquerygrid.gridOverrideData(billquerygrid.vue:146),
662
+ // 而该函数对数据是恒等变换(只 return data),调与不调结果相同;
663
+ // 2) 但它有副作用:会用**全量结果集**的索引去 findIndex,
664
+ // 再 setRowHighlight(index) 对**当前页**做 data.splice(index,1,...)。
665
+ // 索引越界时会往可见列表里追加一行空行(basegrid.vue setRowHighlight)。
666
+ // 另外也不施加 showAll/rowAll 的合成"全部"行 —— 它是纯 UI 行,不该出现在导出里。
667
+ // 后端 page_size 硬上限 99999,超出会静默截断,这里显式告知
668
+ const total = data && data.total_results !== undefined ? Number(data.total_results) : 0;
669
+ if (total > rows.length) {
670
+ this.alert(this.$t('共{total}条,已导出前{count}条', { total: total, count: rows.length }));
671
+ }
672
+ const exportColumns = [];
673
+ this.initAsynExportConfig(this.myConfig.items, '', exportColumns);
674
+ const summaryFields = exportConfig.summaryFields || [];
675
+ // translateDictValues 自带深拷贝(返回新数组+新行对象),故无需再 copy
676
+ let exportData = translateDictValues(rows, exportColumns, this.dictData);
677
+ if (summaryFields.length > 0 && exportColumns.length > 0) {
678
+ exportData.push(
679
+ buildSummaryRow(exportData, summaryFields, exportColumns[0].name, this.$t('合计'))
680
+ );
681
+ }
682
+ // initAsynExportConfig 已把子列扁平化并给标签加"父(子)"前缀,故表头只有 1 层
683
+ this.$Method.exportExcelStyle(exportColumns, exportData, 1, this.buildListExportFileName(rows.length));
684
+ },
685
+ buildListExportFileName(count) {
686
+ let fileName = this.$Method.formatDate(new Date(), 'yyyyMMddhhmmss')
687
+ + this.$t('({count}条).xlsx', { count: count });
688
+ const modulecode = this.getModulecode();
689
+ if (modulecode) {
690
+ fileName = modulecode + '_' + fileName;
691
+ }
692
+ return fileName;
693
+ },
540
694
  zip (str) {
541
695
  let binaryString = gzip(str, { to: 'string' });
542
696
  return btoa(binaryString);
@@ -1040,7 +1194,9 @@ export default {
1040
1194
  if (this.myConfig.dynamicColumnFlag) {
1041
1195
  items = this.dynamicColumnItems;
1042
1196
  } else {
1043
- items = this.config.items;
1197
+ // 与 getFrontendSummaryFields 取同一份列配置(myConfig.items),
1198
+ // 保证合计值一定能在列上找到 format 并正确格式化
1199
+ items = this.myConfig.items || (this.config && this.config.items);
1044
1200
  }
1045
1201
  for (let pro in this.total_summary) {
1046
1202
  let configItems = {};
@@ -10,17 +10,16 @@
10
10
  </div>
11
11
  <Card :padding="0" :bordered="false" style="padding-top:0px;padding-left:16px;padding-right:16px;padding-bottom:14px;" dis-hover>
12
12
  <div :id="headid" :style="style2" @scroll="handleScroll">
13
- <div v-for="(item,index) in myConfig.formsConfig.items" :id="item.divId" :key="item.divId" v-show="headVisible[index].visible">
14
- <Formcard :ref="item.name" :config="item" :dictData="dictData" @doAction="doAction" >
13
+ <div v-for="temp in renderItems" :key="temp.el.divId" :id="temp.el.divId" v-show="temp.isHead ? headVisible[temp.index].visible : detailVisible[temp.index].visible" >
14
+ <Formcard v-if="temp.isHead" :ref="temp.el.name" :config="temp.el" :dictData="dictData" @doAction="doAction" >
15
15
  </Formcard>
16
- </div>
17
- <div v-for="(temp2,index) in detailConfig.items" :key="temp2.divId" :id="temp2.divId" v-show="detailVisible[index].visible" >
18
- <EditGridCardS v-if="temp2.type === 'EditGridCard'" :ref="temp2.name" :config="temp2" :dictData="dictData" @doAction="doAction"/>
19
- <DetailsBoxs v-if="temp2.type === 'DetailsBox'" :ref="temp2.name" :config="temp2" :dictData="dictData" @doAction="doAction"/>
20
- <FormCardBox v-if="temp2.type === 'FormCardBox'" :ref="temp2.name" :config="temp2" :dictData="dictData"/>
21
- <AppendixBoxs v-if="temp2.type === 'AppendixBox'" :ref="temp2.name" :config="temp2" :modulecode="modulecode" @doAction="doAction"/>
22
- <FormCardBox2 v-if="temp2.type === 'FormCardBox2'" :ref="temp2.name" :config="temp2" :dictData="dictData"/>
23
- <AppendixBoxs2 v-if="temp2.type === 'AppendixBox2'" :ref="temp2.name" :config="temp2" :dictData="dictData" :modulecode="modulecode" @handleSuccessAfter="handleSuccessAfter" @doAction="doAction"/>
16
+ <EditGridCardS v-if="!temp.isHead && temp.el.type === 'EditGridCard'" :ref="temp.el.name" :config="temp.el" :dictData="dictData" @doAction="doAction"/>
17
+ <DetailsBoxs v-if="!temp.isHead && temp.el.type === 'DetailsBox'" :ref="temp.el.name" :config="temp.el" :dictData="dictData" @doAction="doAction"/>
18
+ <FormCardBox v-if="!temp.isHead && temp.el.type === 'FormCardBox'" :ref="temp.el.name" :config="temp.el" :dictData="dictData"/>
19
+ <AppendixBoxs v-if="!temp.isHead && temp.el.type === 'AppendixBox'" :ref="temp.el.name" :config="temp.el" :modulecode="modulecode" @doAction="doAction"/>
20
+ <FormCardBox2 v-if="!temp.isHead && temp.el.type === 'FormCardBox2'" :ref="temp.el.name" :config="temp.el" :dictData="dictData"/>
21
+ <AppendixBoxs2 v-if="!temp.isHead && temp.el.type === 'AppendixBox2'" :ref="temp.el.name" :config="temp.el" :dictData="dictData" :modulecode="modulecode" @handleSuccessAfter="handleSuccessAfter" @doAction="doAction"/>
22
+ <EditGridBoxs v-if="temp2.type === 'EditGridBox'" :ref="temp2.name" :config="temp2" :dictData="dictData" @doAction="doAction"/>
24
23
  </div>
25
24
  </div>
26
25
  </Card>
@@ -35,9 +34,10 @@ import AppendixBoxs2 from './contractappendix.vue';
35
34
  import FormCardBox2 from '../form/formcardbox2.vue';
36
35
  import random from '../../utils/random_str';
37
36
  import Formcard from './formcard.vue';
37
+ import EditGridBoxs from './editgridcard.vue';
38
38
  export default {
39
39
  name:'mbilldetailform',
40
- components:{AppendixBoxs2, Formcard,EditGridCardS,DetailsBoxs,AppendixBoxs,FormCardBox2},
40
+ components:{AppendixBoxs2, Formcard,EditGridCardS,DetailsBoxs,AppendixBoxs,FormCardBox2,EditGridBoxs},
41
41
  data () {
42
42
  return {
43
43
  curlEditObject:'',
@@ -123,7 +123,7 @@ export default {
123
123
  el.modetype = modetype;
124
124
  el.divId = 'headForm_' + this.getRandom() + '_' + index;
125
125
  this.headVisible.push({refname:el.name,visible:true});
126
- this.anchorData.push({id:'#'+el.divId ,name:el.title ? this.$t(el.title) : el.title,visible:true,refname:el.name,isHead:true});
126
+ this.anchorData.push({id:'#'+el.divId ,name:el.title ? this.$t(el.title) : el.title,visible:true,refname:el.name,isHead:true,sort:el.sort === undefined ? 0 : el.sort});
127
127
  });
128
128
  }
129
129
 
@@ -164,7 +164,7 @@ export default {
164
164
  }
165
165
  el.divId = 'detail_'+ this.getRandom() + '_'+ index;
166
166
  this.detailVisible.push({refname:el.name,visible:true});
167
- this.anchorData.push({id:'#'+ el.divId, name:el.title ? this.$t(el.title) : el.title,refname:el.name,visible:true,isHead:false});
167
+ this.anchorData.push({id:'#'+ el.divId, name:el.title ? this.$t(el.title) : el.title,refname:el.name,visible:true,isHead:false,sort:el.sort === undefined ? Number.MAX_SAFE_INTEGER : el.sort});
168
168
  });
169
169
  }
170
170
  }
@@ -185,7 +185,7 @@ export default {
185
185
  tmp.divId = 'detail_'+ this.getRandom() + '_'+ this.detailConfig.items.length;
186
186
  this.detailConfig.items.push(tmp);
187
187
  this.detailVisible.push({refname:tmp.name,visible:true});
188
- this.anchorData.push({id:'#'+ tmp.divId, name:tmp.title ? this.$t(tmp.title) : tmp.title,refname:tmp.name,visible:true});
188
+ this.anchorData.push({id:'#'+ tmp.divId, name:tmp.title ? this.$t(tmp.title) : tmp.title,refname:tmp.name,visible:true,sort:Number.MAX_SAFE_INTEGER});
189
189
  } else if (this.billAppendixConfig.showAppendix2) {
190
190
  let tmp = Object.assign({}, {}, this.billAppendixConfig);
191
191
  tmp.type = 'AppendixBox2';
@@ -195,14 +195,34 @@ export default {
195
195
  tmp.divId = 'detail_'+ this.getRandom() + '_'+ this.detailConfig.items.length;
196
196
  this.detailConfig.items.push(tmp);
197
197
  this.detailVisible.push({refname:tmp.name,visible:true});
198
- this.anchorData.push({id:'#'+ tmp.divId, name:tmp.title ? this.$t(tmp.title) : tmp.title,refname:tmp.name,visible:true});
198
+ this.anchorData.push({id:'#'+ tmp.divId, name:tmp.title ? this.$t(tmp.title) : tmp.title,refname:tmp.name,visible:true,sort:Number.MAX_SAFE_INTEGER});
199
199
  }
200
200
 
201
201
  if (this.myConfig.showAnchorLink !== undefined) {
202
202
  this.showAnchorLink = this.myConfig.showAnchorLink;
203
203
  }
204
+
205
+ // 锚点导航顺序与混排后的渲染顺序保持一致
206
+ this.anchorData.sort((a, b) => a.sort - b.sort);
204
207
  },
205
208
  computed:{
209
+ renderItems () {
210
+ // formsConfig.items 与 detailConfig.items 按 sort 升序混排;
211
+ // 未配置 sort 时 formsConfig 项视为 0、detailConfig 项视为最大值,保持 forms 在前 details 在后的现状
212
+ let heads = [];
213
+ let details = [];
214
+ if (this.myConfig.formsConfig && this.myConfig.formsConfig.items) {
215
+ heads = this.myConfig.formsConfig.items.map((el, index) => {
216
+ return { el: el, index: index, isHead: true, sort: el.sort === undefined ? 0 : el.sort };
217
+ });
218
+ }
219
+ if (this.detailConfig.items) {
220
+ details = this.detailConfig.items.map((el, index) => {
221
+ return { el: el, index: index, isHead: false, sort: el.sort === undefined ? Number.MAX_SAFE_INTEGER : el.sort };
222
+ });
223
+ }
224
+ return heads.concat(details).sort((a, b) => a.sort - b.sort);
225
+ },
206
226
  style2 () {
207
227
  let height = this.$Store.state.app.clientHeight;
208
228
  if (this.extraHeight) {
@@ -75,7 +75,7 @@ export default {
75
75
  showModuleCode:false,
76
76
  moreFlag:false,
77
77
  btnItems:[],
78
- ignoreModule:['downTemplate'],
78
+ ignoreModule:['downTemplate','listExport'],
79
79
  notpowerFlag:true,
80
80
  billoperlog:false,
81
81
  };
@@ -25,6 +25,7 @@ import BillDetailForm from './billdetailform.vue';
25
25
  import ApprovalBox from './approval.vue';
26
26
  import BillOperLog from './operlog.vue';
27
27
  import load from '@/components/base/load';
28
+ import { LIST_EXPORT_ACTION } from '@/method/listexport';
28
29
  export default {
29
30
  name:'mlistdetails',
30
31
  mixins: [load],
@@ -111,6 +112,7 @@ export default {
111
112
  this.myConfig.billQueryConfig.addDataflag = 'Y';
112
113
  }
113
114
  });
115
+ this.injectListExportBtn();
114
116
 
115
117
 
116
118
 
@@ -226,7 +228,38 @@ export default {
226
228
  }
227
229
  });
228
230
  },
231
+ /**
232
+ * 通用注入「列表导出」按钮。
233
+ * ListDetails / ListDetailsV2 / ListDetailsV3 均 extends 本组件,故一处注入三处生效。
234
+ * 页面已在 menuToolbar.items 自行声明同名按钮时不重复注入;
235
+ * exportConfig.enabled === false 时不注入。
236
+ */
237
+ injectListExportBtn() {
238
+ const gridConfig = this.myConfig.billQueryConfig.gridConfig;
239
+ const exportConfig = gridConfig.exportConfig || {};
240
+ if (exportConfig.enabled === false) {
241
+ return;
242
+ }
243
+ const exists = this.myConfig.billQueryConfig.btnData.some(el => el.name === LIST_EXPORT_ACTION);
244
+ if (exists) {
245
+ return;
246
+ }
247
+ this.myConfig.billQueryConfig.btnData.push({
248
+ name: LIST_EXPORT_ACTION,
249
+ text: this.$t('列表导出'),
250
+ btntype: 'batch'
251
+ });
252
+ },
229
253
  doAction (obj) {
254
+ if (obj.name === LIST_EXPORT_ACTION) {
255
+ // 页面覆盖优先:沿用框架既有钩子约定(同 setBillData / initFormAfter)
256
+ if (this.$parent && typeof this.$parent[LIST_EXPORT_ACTION] === 'function') {
257
+ this.$parent[LIST_EXPORT_ACTION](obj);
258
+ } else {
259
+ this.$refs.billqureygrid.getGrid().listExport();
260
+ }
261
+ return true;
262
+ }
230
263
  if (obj.name === 'gridSelectionChange' ) {
231
264
  if (this.$parent[obj.name]) {
232
265
  this.$parent[obj.name](obj);
@@ -66,6 +66,7 @@ export default {
66
66
  '加签': 'Add Signer',
67
67
  '加粗': 'Bold',
68
68
  '加载': 'Loading',
69
+ '合计': 'Total',
69
70
  '动态合计': 'Dynamic Sum',
70
71
  '包含': 'Contains',
71
72
  '单据主健信息为空,不允许上传': 'Bill primary key is empty, upload not allowed',
@@ -111,6 +112,8 @@ export default {
111
112
  '导出失败': 'Export Failed',
112
113
  '导出失败:{status} {statusText}': 'Export failed: {status} {statusText}',
113
114
  '导出中...,请勿关闭!': 'Exporting..., do not close!',
115
+ '列表导出': 'Export List',
116
+ '共{total}条,已导出前{count}条': 'Total {total} rows, exported the first {count}',
114
117
  '导出任务已生成,请稍后在任务列表下载文件': 'Export task generated, download from task list later',
115
118
  '导出选择': 'Export Selection',
116
119
  '小一': '24pt',
@@ -66,6 +66,7 @@ export default {
66
66
  '加签': '加签',
67
67
  '加粗': '加粗',
68
68
  '加载': '加载',
69
+ '合计': '合计',
69
70
  '动态合计': '动态合计',
70
71
  '包含': '包含',
71
72
  '单据主健信息为空,不允许上传': '单据主健信息为空,不允许上传',
@@ -111,6 +112,8 @@ export default {
111
112
  '导出失败': '导出失败',
112
113
  '导出失败:{status} {statusText}': '导出失败:{status} {statusText}',
113
114
  '导出中...,请勿关闭!': '导出中...,请勿关闭!',
115
+ '列表导出': '列表导出',
116
+ '共{total}条,已导出前{count}条': '共{total}条,已导出前{count}条',
114
117
  '导出任务已生成,请稍后在任务列表下载文件': '导出任务已生成,请稍后在任务列表下载文件',
115
118
  '导出选择': '导出选择',
116
119
  '小一': '小一',
@@ -66,6 +66,7 @@ export default {
66
66
  '加签': '加簽',
67
67
  '加粗': '加粗',
68
68
  '加载': '加載',
69
+ '合计': '合計',
69
70
  '动态合计': '動態合計',
70
71
  '包含': '包含',
71
72
  '单据主健信息为空,不允许上传': '單據主鍵信息為空,不允許上傳',
@@ -111,6 +112,8 @@ export default {
111
112
  '导出失败': '導出失敗',
112
113
  '导出失败:{status} {statusText}': '導出失敗:{status} {statusText}',
113
114
  '导出中...,请勿关闭!': '導出中...,請勿關閉!',
115
+ '列表导出': '列表匯出',
116
+ '共{total}条,已导出前{count}条': '共{total}條,已匯出前{count}條',
114
117
  '导出任务已生成,请稍后在任务列表下载文件': '導出任務已生成,請稍後在任務列表下載文件',
115
118
  '导出选择': '導出選擇',
116
119
  '小一': '小一',
@@ -0,0 +1,163 @@
1
+ /**
2
+ * 单据列表导出 —— 纯逻辑层
3
+ *
4
+ * 本文件不依赖 Vue、不使用 "@/..." 别名导入,以便直接被 `node --test` 运行。
5
+ *
6
+ * 【重要 · 见设计文档 3.1】
7
+ * 后端 StorageUtils.isSpecialParamKey 的豁免名单只有:
8
+ * fields / page_no / page_size / order_field / order_direction / 含 ":" 的子表键
9
+ * 其余任何 key 都会被 buildCriteria 当成 WHERE 条件拼进 SQL。
10
+ * 所以 summary_fld / sumgroups 这类"前端自用"的参数一旦进入查询参数,
11
+ * 就会变成 SQL 条件导致"查不到列"而报错 —— 必须彻底剔除。
12
+ * 合计因此一律在前端本地计算。
13
+ */
14
+
15
+ /** 导出取数默认分页大小,对齐后端 StorageUtils.java:480 的硬上限 */
16
+ export const DEFAULT_EXPORT_PAGE_SIZE = 99999;
17
+
18
+ /** 列表导出动作名 */
19
+ export const LIST_EXPORT_ACTION = 'listExport';
20
+
21
+ /** 单据明细导出动作名(本期仅占位,不实现,避免与列表导出混用命名) */
22
+ export const DETAIL_EXPORT_ACTION = 'detailExport';
23
+
24
+ /** 后端不豁免、必须从查询参数中剔除的键 */
25
+ const NON_QUERY_KEYS = ['summary_fld', 'sumgroups'];
26
+
27
+ /**
28
+ * 构造导出用的查询参数。
29
+ * @param {object} baseParam 通常来自 grid.getSearchParam()
30
+ * @param {number} pageSize 导出分页大小
31
+ * @returns {object} 新的参数对象(不修改入参)
32
+ */
33
+ export function buildExportSearchParam(baseParam, pageSize) {
34
+ const param = Object.assign({}, baseParam || {});
35
+ NON_QUERY_KEYS.forEach(key => {
36
+ delete param[key];
37
+ });
38
+ const size = Number(pageSize);
39
+ param.page_no = 1;
40
+ param.page_size = Number.isFinite(size) && size > 0 ? size : DEFAULT_EXPORT_PAGE_SIZE;
41
+ return param;
42
+ }
43
+
44
+ /**
45
+ * 把任意值安全转成数字,非数值一律按 0 处理(不抛错)。
46
+ */
47
+ export function toNumber(value) {
48
+ const n = Number(value);
49
+ return Number.isFinite(n) ? n : 0;
50
+ }
51
+
52
+ /**
53
+ * 收集列配置上标记 sum: true 的字段名(前端合计字段)。
54
+ *
55
+ * 支持多级表头的 children;跳过 visible: false 的列,避免"表头设计里隐藏的列"
56
+ * 出现在合计行。与后端合计标记 summary: true 无关(后者会下发 summary_fld)。
57
+ *
58
+ * @param {object[]} items 列配置(gridConfig.items)
59
+ * @returns {string[]} 参与合计的字段名
60
+ */
61
+ export function collectSummaryFields(items) {
62
+ const fields = [];
63
+ const collect = (list) => {
64
+ // 组件库场景:items / children 可能来自后端或用户配置,非数组一律安全跳过
65
+ if (!Array.isArray(list)) {
66
+ return;
67
+ }
68
+ list.forEach(el => {
69
+ if (!el || typeof el !== 'object' || el.visible === false) {
70
+ return;
71
+ }
72
+ if (Array.isArray(el.children) && el.children.length > 0) {
73
+ collect(el.children);
74
+ } else if (el.sum === true && el.name) {
75
+ fields.push(el.name);
76
+ }
77
+ });
78
+ };
79
+ collect(items);
80
+ return fields;
81
+ }
82
+
83
+ /**
84
+ * 构造合计行。返回的普通对象可直接 push 进导出数据末尾 ——
85
+ * exportExcelStyle 会按列顺序从对象上取值,因此无需改动导出器本身。
86
+ * @param {object[]} rows 参与合计的数据行
87
+ * @param {string[]} summaryFields 参与合计的字段名
88
+ * @returns {object}
89
+ */
90
+ export function buildSummaryMap(rows, summaryFields) {
91
+ const map = {};
92
+ (summaryFields || []).forEach(field => {
93
+ const total = (rows || []).reduce((acc, r) => acc + toNumber(r[field]), 0);
94
+ // toFixed(10) 消除 IEEE754 累加噪声(0.1+0.2 -> 0.3)
95
+ map[field] = Number(total.toFixed(10));
96
+ });
97
+ return map;
98
+ }
99
+
100
+ /**
101
+ * 构造合计行。返回的普通对象可直接 push 进导出数据末尾 ——
102
+ * exportExcelStyle 会按列顺序从对象上取值,因此无需改动导出器本身。
103
+ * @param {object[]} rows 参与合计的数据行
104
+ * @param {string[]} summaryFields 参与合计的字段名
105
+ * @param {string} firstColumnName 第一列字段名,用于写"合计"文案
106
+ * @param {string} sumText 合计文案
107
+ * @returns {object}
108
+ */
109
+ export function buildSummaryRow(rows, summaryFields, firstColumnName, sumText) {
110
+ const row = {};
111
+ if (firstColumnName) {
112
+ row[firstColumnName] = sumText;
113
+ }
114
+ Object.assign(row, buildSummaryMap(rows, summaryFields));
115
+ return row;
116
+ }
117
+
118
+ /**
119
+ * 把 ComboBox 列的 code 翻译成显示文本,与列表所见保持一致。
120
+ *
121
+ * 注意:不要照抄 viewgridbase.vue dynamicExportExcel 里的同功能实现 ——
122
+ * 那里 `if (el.valueField) { valueField = column.valueField; }` 引用了未定义的
123
+ * `column`,会抛 ReferenceError。
124
+ *
125
+ * @param {object[]} rows 数据行
126
+ * @param {object[]} columns 导出列配置(需带 name / type / data)
127
+ * @returns {object[]} 新数组(不修改入参)
128
+ */
129
+ export function translateDictValues(rows, columns, dictData) {
130
+ const result = (rows || []).map(row => Object.assign({}, row));
131
+ (columns || []).forEach(column => {
132
+ if (column.type !== 'ComboBox' || !column.name) {
133
+ return;
134
+ }
135
+ // 真实业务里 ComboBox 绝大多数用 dictkey 从外部字典取数,列上没有内联 data
136
+ // (basegrid.setColumn 是在 $Method.copy 出来的副本上写 column.data 的,
137
+ // 见 basegrid.vue:475 与 :600-604,所以 myConfig.items 上永远拿不到 data)。
138
+ // 因此这里必须回退到 grid 的 dictData,否则导出的是裸 code。
139
+ const dictList = column.data
140
+ || (dictData || {})[column.customDictkey || column.dictkey];
141
+ if (!dictList) {
142
+ return;
143
+ }
144
+ const valueField = column.valueField || 'code';
145
+ const textField = column.textField || 'name';
146
+ const dict = new Map();
147
+ dictList.forEach(item => dict.set(item[valueField], item));
148
+ result.forEach(row => {
149
+ const raw = row[column.name];
150
+ if (raw === undefined || raw === null || raw === '') {
151
+ return;
152
+ }
153
+ const hit = dict.get(raw);
154
+ if (!hit) {
155
+ return;
156
+ }
157
+ row[column.name] = column.codeWithName === false
158
+ ? hit[textField]
159
+ : '[' + hit[valueField] + ']' + hit[textField];
160
+ });
161
+ });
162
+ return result;
163
+ }
@@ -508,7 +508,13 @@ export function exportExcelStyle(config, data, headNum, filename) {
508
508
  let temp = [];
509
509
  names.forEach(el2 => {
510
510
  if (el2.excelPro.t === 'n') {
511
- temp.push(Number(el[el2.name]));
511
+ // 数值列取不到有效数字时必须落成"空格子",不能落成 NaN。
512
+ // Number(undefined) === NaN,而 aoa_to_sheet 只跳过 undefined、不跳过 NaN,
513
+ // 于是会在文件里写出 <v>NaN</v> 这种非法数值格,Excel 打开会要求修复。
514
+ // 典型触发点:列表导出的合计行只填了参与合计的数值列,其余 NumberBox 列缺键。
515
+ // 注意 Number('') === 0、Number(null) === 0,这些既有语义保持不变。
516
+ const numValue = Number(el[el2.name]);
517
+ temp.push(Number.isFinite(numValue) ? numValue : undefined);
512
518
  } else if (el2.excelPro.t === 'd') {
513
519
  let value = datenum(new Date(el[el2.name]));
514
520
  temp.push(value);