n20-common-lib 2.22.72 → 2.22.74
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 +1 -1
- package/src/components/AdvancedFilter/filterItem.vue +7 -1
- package/src/components/AdvancedFilter/index.vue +162 -87
- package/src/components/DynamicField/DynamicField.vue +40 -9
- package/src/components/InputNumber/README.md +100 -3
- package/src/components/InputNumber/index.vue +82 -3
- package/src/components/InputNumber/numberRange.vue +3 -2
- package/src/components/Table/filters.js +6 -1
- package/src/components/TablePro/index.js +5 -0
- package/src/components/TablePro/index.vue +14 -6
- package/src/index.js +3 -0
- package/src/utils/decimal.js +19 -0
- package/src/utils/toExcel.js +4 -0
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<el-checkbox-group v-model="checkArrayCalc" :max="maxLength" class="popover-check-box" @change="checkChange">
|
|
2
|
+
<el-checkbox-group v-model="checkArrayCalc" :disabled="disabled" :max="maxLength" class="popover-check-box" @change="checkChange">
|
|
3
3
|
<vue-draggable
|
|
4
4
|
:list="filterList"
|
|
5
5
|
:animation="200"
|
|
6
|
+
:disabled="disabled"
|
|
6
7
|
group="dragBox"
|
|
7
8
|
:handle="'.n20-icon-tuodong'"
|
|
8
9
|
@change="dragChange"
|
|
@@ -24,6 +25,11 @@ export default {
|
|
|
24
25
|
vueDraggable: () => importGlobal('vuedraggable', () => import(/*webpackChunkName: "vuedraggable"*/ 'vuedraggable'))
|
|
25
26
|
},
|
|
26
27
|
props: {
|
|
28
|
+
// 忙碌时禁用勾选和拖拽,保留列表实例及其当前草稿。
|
|
29
|
+
disabled: {
|
|
30
|
+
type: Boolean,
|
|
31
|
+
default: false
|
|
32
|
+
},
|
|
27
33
|
filterList: {
|
|
28
34
|
type: Array,
|
|
29
35
|
default: () => []
|
|
@@ -26,29 +26,30 @@
|
|
|
26
26
|
v-if="!item.isNotClose"
|
|
27
27
|
:key="getOnlyKey(item)"
|
|
28
28
|
:class="['filter_remove', 'n20-icon-yichu']"
|
|
29
|
-
@click="handleClose(item)"
|
|
29
|
+
@click="!filterBusy && handleClose(item)"
|
|
30
30
|
></i>
|
|
31
31
|
</el-form-item>
|
|
32
32
|
<slot name="suffix"></slot>
|
|
33
33
|
<el-form-item :class="prefixCls + '-add'">
|
|
34
|
-
<el-button v-popover:advanced_popover onlyicon plain icon="el-icon-plus" />
|
|
35
|
-
<el-popover ref="advanced_popover" placement="bottom-start" trigger="click">
|
|
36
|
-
<el-input v-model="keyword" clearable class="input-w m-b-s" :placeholder="$lc('搜索筛选条件')" />
|
|
34
|
+
<el-button v-popover:advanced_popover :loading="filterBusy" :disabled="filterBusy" onlyicon plain icon="el-icon-plus" />
|
|
35
|
+
<el-popover ref="advanced_popover" :disabled="filterBusy" placement="bottom-start" trigger="click">
|
|
36
|
+
<el-input v-model="keyword" :disabled="filterBusy" clearable class="input-w m-b-s" :placeholder="$lc('搜索筛选条件')" />
|
|
37
37
|
<div class="flex-box flex-lr">
|
|
38
|
-
<el-button type="text" @click="allCheck">{{
|
|
38
|
+
<el-button type="text" :disabled="filterBusy" @click="!filterBusy && allCheck()">{{
|
|
39
39
|
checkList.length === filterListS.length ? $lc('取消全选') : $lc('全选')
|
|
40
40
|
}}</el-button>
|
|
41
|
-
<el-button type="text" @click="defaultCheck">{{ $lc('恢复默认') }}</el-button>
|
|
41
|
+
<el-button type="text" :disabled="filterBusy" @click="!filterBusy && defaultCheck()">{{ $lc('恢复默认') }}</el-button>
|
|
42
42
|
</div>
|
|
43
43
|
<filterItem
|
|
44
|
+
:disabled="filterBusy"
|
|
44
45
|
:filter-list="filterListS"
|
|
45
46
|
:max-length="maxLength"
|
|
46
47
|
:check-array.sync="checkList"
|
|
47
48
|
@checked="check"
|
|
48
49
|
/>
|
|
49
50
|
<div class="flex-box flex-c m-t-m">
|
|
50
|
-
<el-button size="mini" type="primary" @click="savaCheck">{{ $lc('确认') }}</el-button>
|
|
51
|
-
<el-button plain type="primary" @click="cancel">{{ $lc('取消') }}</el-button>
|
|
51
|
+
<el-button size="mini" type="primary" :disabled="filterBusy" @click="!filterBusy && savaCheck()">{{ $lc('确认') }}</el-button>
|
|
52
|
+
<el-button plain type="primary" :disabled="filterBusy" @click="!filterBusy && cancel()">{{ $lc('取消') }}</el-button>
|
|
52
53
|
</div>
|
|
53
54
|
</el-popover>
|
|
54
55
|
<el-link class="m-l-s color-primary" :underline="false" icon="n20-icon-query" @click="$emit('search')">
|
|
@@ -123,6 +124,7 @@ export default {
|
|
|
123
124
|
},
|
|
124
125
|
data() {
|
|
125
126
|
return {
|
|
127
|
+
pendingFilterOperations: 0,
|
|
126
128
|
keyword: '',
|
|
127
129
|
prefixCls,
|
|
128
130
|
remoteList: [],
|
|
@@ -133,7 +135,14 @@ export default {
|
|
|
133
135
|
userNo: sessionStorage.getItem('userNo')
|
|
134
136
|
}
|
|
135
137
|
},
|
|
138
|
+
beforeCreate() {
|
|
139
|
+
// Promise 队列和请求令牌无需响应式,避免 Vue 观察异步控制对象。
|
|
140
|
+
this.filterRuntime = { tail: Promise.resolve(), readId: 0, revision: 0, contextId: 0, destroyed: false, selection: null }
|
|
141
|
+
},
|
|
136
142
|
computed: {
|
|
143
|
+
filterBusy() {
|
|
144
|
+
return this.pendingFilterOperations > 0
|
|
145
|
+
},
|
|
137
146
|
filterListS: {
|
|
138
147
|
get() {
|
|
139
148
|
if (!this.keyword) {
|
|
@@ -149,16 +158,22 @@ export default {
|
|
|
149
158
|
}
|
|
150
159
|
},
|
|
151
160
|
watch: {
|
|
161
|
+
filterId: 'invalidateFilterContext',
|
|
162
|
+
userNo: 'invalidateFilterContext',
|
|
152
163
|
visible: {
|
|
153
164
|
handler(val) {
|
|
154
|
-
val && this.
|
|
165
|
+
val && this.loadFilterList()
|
|
155
166
|
}
|
|
156
167
|
}
|
|
157
168
|
},
|
|
158
169
|
created() {
|
|
159
|
-
this.
|
|
170
|
+
this.loadFilterList()
|
|
160
171
|
this.getDefaultCheck()
|
|
161
172
|
},
|
|
173
|
+
beforeDestroy() {
|
|
174
|
+
this.filterRuntime.destroyed = true
|
|
175
|
+
this.invalidateFilterContext()
|
|
176
|
+
},
|
|
162
177
|
methods: {
|
|
163
178
|
getDefaultCheck() {
|
|
164
179
|
let _checkList = this.filterList
|
|
@@ -261,15 +276,15 @@ export default {
|
|
|
261
276
|
break
|
|
262
277
|
}
|
|
263
278
|
}
|
|
264
|
-
|
|
279
|
+
// 连续实例调用从上一笔提交快照删除,避免后一笔把已删除条件带回来。
|
|
280
|
+
const selection = this.filterRuntime.selection
|
|
281
|
+
const source = selection && this.isFilterContextCurrent(selection.context) ? selection.data : this.GroupData
|
|
282
|
+
let data = source.filter((s) => {
|
|
265
283
|
return s.id !== item.id
|
|
266
284
|
})
|
|
267
|
-
this.
|
|
268
|
-
if (code === 200) {
|
|
269
|
-
this.getFilterList()
|
|
270
|
-
}
|
|
271
|
-
})
|
|
285
|
+
const operation = this.queueFilterSave(data)
|
|
272
286
|
this.$emit('clean', this.model)
|
|
287
|
+
return operation
|
|
273
288
|
},
|
|
274
289
|
cleared() {
|
|
275
290
|
this.$emit('clear')
|
|
@@ -330,15 +345,10 @@ export default {
|
|
|
330
345
|
})
|
|
331
346
|
}
|
|
332
347
|
this.setModelData(saveCheckArr)
|
|
333
|
-
this.
|
|
334
|
-
// 如果保存成功
|
|
335
|
-
if (code === 200) {
|
|
336
|
-
// 重新获取过滤条件列表
|
|
337
|
-
this.getFilterList()
|
|
338
|
-
}
|
|
339
|
-
})
|
|
348
|
+
const operation = this.queueFilterSave(saveCheckArr)
|
|
340
349
|
this.$refs.advanced_popover.showPopper = false
|
|
341
350
|
this.$emit('check', saveCheckArr)
|
|
351
|
+
return operation
|
|
342
352
|
},
|
|
343
353
|
check(keys) {
|
|
344
354
|
this.checkList = keys
|
|
@@ -347,83 +357,148 @@ export default {
|
|
|
347
357
|
* 保存筛选条件
|
|
348
358
|
* @param data
|
|
349
359
|
*/
|
|
350
|
-
saveFilter(data) {
|
|
351
|
-
return
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
360
|
+
saveFilter(data, context = this.getFilterContext()) {
|
|
361
|
+
return axios.post(
|
|
362
|
+
`/bems/prod_1.0/user/pageHabit?t=${Date.now()}`,
|
|
363
|
+
{ userNo: context.userNo, pageId: context.pageId, showStructure: JSON.stringify(data) },
|
|
364
|
+
{ loading: false, noMsg: true }
|
|
365
|
+
)
|
|
366
|
+
},
|
|
367
|
+
getFilterContext() {
|
|
368
|
+
return { userNo: this.userNo, pageId: this.filterId, contextId: this.filterRuntime.contextId }
|
|
369
|
+
},
|
|
370
|
+
isFilterContextCurrent(context) {
|
|
371
|
+
return !this.filterRuntime.destroyed && context.userNo === this.userNo &&
|
|
372
|
+
context.pageId === this.filterId && context.contextId === this.filterRuntime.contextId
|
|
373
|
+
},
|
|
374
|
+
invalidateFilterContext() {
|
|
375
|
+
this.filterRuntime.contextId++
|
|
376
|
+
this.filterRuntime.readId++
|
|
377
|
+
this.filterRuntime.selection = null
|
|
378
|
+
},
|
|
379
|
+
finishFilterOperation() {
|
|
380
|
+
if (!this.filterRuntime.destroyed) this.pendingFilterOperations--
|
|
381
|
+
},
|
|
382
|
+
reportFilterError(context, message) {
|
|
383
|
+
if (this.isFilterContextCurrent(context)) this.$message.error(this.$lc(message))
|
|
384
|
+
},
|
|
385
|
+
// 生命周期入口消费异常;公开读取方法仍返回可拒绝的 Promise,供实例调用者处理。
|
|
386
|
+
loadFilterList() {
|
|
387
|
+
const context = this.getFilterContext()
|
|
388
|
+
return this.getFilterList().catch(() => {
|
|
389
|
+
this.reportFilterError(context, '筛选条件加载失败,请重试')
|
|
365
390
|
})
|
|
366
391
|
},
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
392
|
+
queueFilterSave(data) {
|
|
393
|
+
const runtime = this.filterRuntime
|
|
394
|
+
if (runtime.destroyed) return Promise.resolve()
|
|
395
|
+
const context = this.getFilterContext()
|
|
396
|
+
const snapshot = XEUtils.clone(data, true)
|
|
397
|
+
const revision = ++runtime.revision
|
|
398
|
+
runtime.readId++
|
|
399
|
+
runtime.selection = { context, data: snapshot }
|
|
400
|
+
this.pendingFilterOperations++
|
|
401
|
+
// 一笔操作覆盖保存、回读及配置回调;失败不能阻断后续提交。
|
|
402
|
+
const operation = runtime.tail.then(async () => {
|
|
403
|
+
if (runtime.destroyed) return
|
|
404
|
+
let saved = false
|
|
405
|
+
try {
|
|
406
|
+
const result = await this.saveFilter(snapshot, context)
|
|
407
|
+
if (result.code !== 200) throw new Error('Filter save failed')
|
|
408
|
+
saved = true
|
|
409
|
+
await this.readFilterList(context, revision)
|
|
410
|
+
} catch (error) {
|
|
411
|
+
this.reportFilterError(context, saved
|
|
412
|
+
? '筛选条件已保存,但加载失败,请重试'
|
|
413
|
+
: '筛选条件保存失败,请重试')
|
|
414
|
+
}
|
|
415
|
+
}).finally(() => {
|
|
416
|
+
if (runtime.revision === revision) runtime.selection = null
|
|
417
|
+
this.finishFilterOperation()
|
|
418
|
+
})
|
|
419
|
+
runtime.tail = operation.catch(() => {})
|
|
420
|
+
return operation
|
|
421
|
+
},
|
|
422
|
+
/** 获取筛选条件,外部读取等待已排队的提交,避免读到保存前的配置。 */
|
|
370
423
|
async getFilterList() {
|
|
424
|
+
if (this.filterRuntime.destroyed) return
|
|
425
|
+
const context = this.getFilterContext()
|
|
426
|
+
const revision = this.filterRuntime.revision
|
|
427
|
+
this.pendingFilterOperations++
|
|
428
|
+
try {
|
|
429
|
+
await this.filterRuntime.tail
|
|
430
|
+
await this.readFilterList(context, revision)
|
|
431
|
+
} finally {
|
|
432
|
+
this.finishFilterOperation()
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
async readFilterList(context, revision) {
|
|
436
|
+
const runtime = this.filterRuntime
|
|
437
|
+
if (!this.isFilterContextCurrent(context)) return
|
|
438
|
+
const readId = ++runtime.readId
|
|
439
|
+
const isCurrent = () => this.isFilterContextCurrent(context) &&
|
|
440
|
+
readId === runtime.readId && revision === runtime.revision
|
|
371
441
|
try {
|
|
372
442
|
const { data } = await axios.post(
|
|
373
443
|
`/bems/prod_1.0/user/pageHabit/list?t=${Date.now()}`,
|
|
374
|
-
{
|
|
375
|
-
userNo: this.userNo,
|
|
376
|
-
pageId: this.filterId
|
|
377
|
-
},
|
|
444
|
+
{ userNo: context.userNo, pageId: context.pageId },
|
|
378
445
|
{ loading: false, noMsg: true }
|
|
379
446
|
)
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if (
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
447
|
+
if (!isCurrent()) return
|
|
448
|
+
const list = JSON.parse(data)
|
|
449
|
+
// null 是历史接口的未配置状态;其他非数组内容不能覆盖当前显示。
|
|
450
|
+
if (list !== null && !Array.isArray(list)) throw new Error('Invalid filter configuration')
|
|
451
|
+
await Promise.resolve(this.beforeFilterList(list))
|
|
452
|
+
if (!isCurrent()) return
|
|
453
|
+
this.applyFilterList(list)
|
|
454
|
+
this.applyFilterOptions()
|
|
455
|
+
} catch (error) {
|
|
456
|
+
// 旧请求失败不干扰当前页面,也不产生过期错误提示。
|
|
457
|
+
if (isCurrent()) throw error
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
applyFilterList(_data) {
|
|
461
|
+
if (_data && _data.length > 0) {
|
|
462
|
+
this.GroupData = _data.map((item) => {
|
|
463
|
+
for (let i = 0; i < this.filterList.length; i++) {
|
|
464
|
+
const originItem = this.filterList[i]
|
|
465
|
+
if (originItem[this.onlyKey] === item[this.onlyKey]) {
|
|
466
|
+
item.label = originItem.label
|
|
467
|
+
if (item.options && originItem.options) {
|
|
468
|
+
item.options = originItem.options
|
|
394
469
|
}
|
|
395
470
|
}
|
|
396
|
-
|
|
471
|
+
}
|
|
472
|
+
return item
|
|
473
|
+
})
|
|
474
|
+
} else {
|
|
475
|
+
if (this.removeLoad) {
|
|
476
|
+
this.GroupData = this.filterList.filter((item) => {
|
|
477
|
+
if (item.isDefault) {
|
|
478
|
+
return item
|
|
479
|
+
}
|
|
397
480
|
})
|
|
398
481
|
} else {
|
|
399
|
-
|
|
400
|
-
this.GroupData = this.filterList.filter((item) => {
|
|
401
|
-
if (item.isDefault) {
|
|
402
|
-
return item
|
|
403
|
-
}
|
|
404
|
-
})
|
|
405
|
-
} else {
|
|
406
|
-
this.GroupData = []
|
|
407
|
-
}
|
|
482
|
+
this.GroupData = []
|
|
408
483
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
484
|
+
}
|
|
485
|
+
this.checkList = this.GroupData.map((res) => res.id)
|
|
486
|
+
this.cancelCheck = XEUtils.clone(this.checkList, true)
|
|
487
|
+
// 查找排序,在this.filterList中查找this.checkList中的每一项,并将其排在前面
|
|
488
|
+
this.filterList.sort((a, b) => {
|
|
489
|
+
if (this.checkList.includes(a[this.onlyKey]) && !this.checkList.includes(b[this.onlyKey])) {
|
|
490
|
+
return -1
|
|
491
|
+
}
|
|
492
|
+
if (!this.checkList.includes(a[this.onlyKey]) && this.checkList.includes(b[this.onlyKey])) {
|
|
493
|
+
return 1
|
|
494
|
+
}
|
|
495
|
+
return 0
|
|
496
|
+
})
|
|
497
|
+
},
|
|
498
|
+
applyFilterOptions() {
|
|
499
|
+
for (const key in getOptionsMap()) {
|
|
500
|
+
if (Object.hasOwnProperty.call(getOptionsMap(), key)) {
|
|
501
|
+
this.setChoices(key, getOptionsMap()[key])
|
|
427
502
|
}
|
|
428
503
|
}
|
|
429
504
|
}
|
|
@@ -75,11 +75,12 @@
|
|
|
75
75
|
<template v-else-if="formItem.type === 'input-number'">
|
|
76
76
|
<cl-input-number
|
|
77
77
|
v-model="value[key]"
|
|
78
|
+
v-bind="numberInputProps(formItem)"
|
|
78
79
|
class="input-w"
|
|
79
80
|
:is-clearable="formItem.isClearable"
|
|
80
81
|
:clearable="formItem.clearable === false ? false : true"
|
|
81
82
|
:disabled="formItem.disabled"
|
|
82
|
-
:max="formItem.max
|
|
83
|
+
:max="formItem.max === undefined ? 9999999999.99 : formItem.max"
|
|
83
84
|
:min="formItem.min || formItem.min === 0 ? formItem.min : 0"
|
|
84
85
|
:d-num="formItem.dNum || formItem.dNum === 0 ? formItem.dNum : 2"
|
|
85
86
|
:type="formItem.numberType || 'money'"
|
|
@@ -89,16 +90,20 @@
|
|
|
89
90
|
<!-- 金额区间 -->
|
|
90
91
|
<template v-else-if="formItem.type === 'input-number-range'">
|
|
91
92
|
<cl-input-number-range
|
|
93
|
+
v-bind="numberInputProps(formItem)"
|
|
92
94
|
class="input-w"
|
|
95
|
+
:type="formItem.numberType || 'money'"
|
|
96
|
+
:min="formItem.min"
|
|
97
|
+
:max="formItem.max"
|
|
93
98
|
:is-clearable="formItem.isClearable === false ? false : true"
|
|
94
99
|
:disabled="formItem.disabled"
|
|
95
100
|
:start-value.sync="value[key][formItem['startKey'] || 'start']"
|
|
96
101
|
:end-value.sync="value[key][formItem['endKey'] || 'end']"
|
|
97
102
|
@change="
|
|
98
|
-
() =>
|
|
103
|
+
(range) =>
|
|
99
104
|
$emit(
|
|
100
105
|
'valueChange',
|
|
101
|
-
{ start:
|
|
106
|
+
{ start: range[0], end: range[1] },
|
|
102
107
|
key
|
|
103
108
|
)
|
|
104
109
|
"
|
|
@@ -107,19 +112,20 @@
|
|
|
107
112
|
<!-- 利率区间 -->
|
|
108
113
|
<template v-else-if="formItem.type === 'input-rate-range'">
|
|
109
114
|
<cl-input-number-range
|
|
115
|
+
v-bind="numberInputProps(formItem)"
|
|
110
116
|
class="input-w"
|
|
111
117
|
:disabled="formItem.disabled"
|
|
112
118
|
:is-clearable="formItem.isClearable === false ? false : true"
|
|
113
119
|
:start-value.sync="value[key][formItem['startKey'] || 'start']"
|
|
114
120
|
:end-value.sync="value[key][formItem['endKey'] || 'end']"
|
|
115
|
-
type="rate"
|
|
116
|
-
:min="0"
|
|
117
|
-
:max="100"
|
|
121
|
+
:type="formItem.numberType || 'rate'"
|
|
122
|
+
:min="formItem.min === undefined ? 0 : formItem.min"
|
|
123
|
+
:max="formItem.max === undefined ? 100 : formItem.max"
|
|
118
124
|
@change="
|
|
119
|
-
() =>
|
|
125
|
+
(range) =>
|
|
120
126
|
$emit(
|
|
121
127
|
'valueChange',
|
|
122
|
-
{ start:
|
|
128
|
+
{ start: range[0], end: range[1] },
|
|
123
129
|
key
|
|
124
130
|
)
|
|
125
131
|
"
|
|
@@ -316,7 +322,8 @@
|
|
|
316
322
|
<template v-else>
|
|
317
323
|
<slot :name="key" :value="formItem.value">
|
|
318
324
|
<template v-if="formItem.type === 'input-number'">
|
|
319
|
-
<span class="value">{{ formItem
|
|
325
|
+
<span v-if="formItem.stringMode" class="value">{{ formatStringNumber(formItem) }}</span>
|
|
326
|
+
<span v-else class="value">{{ formItem.value | toThousands }}</span>
|
|
320
327
|
</template>
|
|
321
328
|
<template v-else-if="formItem.label === $lc('子票区间')">
|
|
322
329
|
<span>{{ formItem.value | formatCdRange }}</span>
|
|
@@ -341,6 +348,8 @@ import clInputNumberRange from '../InputNumber/numberRange.vue'
|
|
|
341
348
|
import clDatePicker from '../DatePicker/index.vue'
|
|
342
349
|
import clDatePickerPor from '../DatePicker/por.vue'
|
|
343
350
|
import clInputSearch from '../InputSearch/index.vue'
|
|
351
|
+
import N from '../../utils/numberPor'
|
|
352
|
+
import { formatExchangeRate, parseDecimal } from '../../utils/decimal'
|
|
344
353
|
/*
|
|
345
354
|
1.初始化值 是否能出发change事件
|
|
346
355
|
2.当前表单change 触发其他表单的修改(未在自身作用域)
|
|
@@ -430,6 +439,28 @@ export default {
|
|
|
430
439
|
},
|
|
431
440
|
created() {},
|
|
432
441
|
methods: {
|
|
442
|
+
// 只透传数值输入参数,避免把动态字段自身的 type/value 等配置覆盖到控件。
|
|
443
|
+
numberInputProps(item) {
|
|
444
|
+
const props = {}
|
|
445
|
+
const keys = ['stringMode', 'maxlength', 'dNum', 'format', 'rangeAuto', 'step', 'stepStrictly', 'suffix']
|
|
446
|
+
keys.forEach((key) => {
|
|
447
|
+
if (item[key] !== undefined) props[key] = item[key]
|
|
448
|
+
})
|
|
449
|
+
return props
|
|
450
|
+
},
|
|
451
|
+
// 详情中的高精度字段与编辑控件采用同一小数位规则,不依赖宿主的金额 filter。
|
|
452
|
+
formatStringNumber(item) {
|
|
453
|
+
const digits = item.format
|
|
454
|
+
? (item.format.includes('.') ? item.format.length - item.format.indexOf('.') - 1 : 0)
|
|
455
|
+
: (item.dNum === undefined ? 2 : item.dNum)
|
|
456
|
+
const decimal = parseDecimal(item.value)
|
|
457
|
+
const fraction = String(item.value).trim().match(/\.(\d+)$/)
|
|
458
|
+
const places = item.rangeAuto && decimal
|
|
459
|
+
? Math.max(digits, decimal.decimalPlaces(), fraction ? fraction[1].length : 0)
|
|
460
|
+
: digits
|
|
461
|
+
const text = formatExchangeRate(item.value, places)
|
|
462
|
+
return text === '--' || item.numberType === 'number' ? text : N.addThousands(text)
|
|
463
|
+
},
|
|
433
464
|
init() {
|
|
434
465
|
this.$listeners.init({ branchNoList: this.branchNoList })
|
|
435
466
|
},
|
|
@@ -22,11 +22,12 @@
|
|
|
22
22
|
| 属性名 | 类型 | 默认值 | 说明 |
|
|
23
23
|
| ------------ | ------------- | ----------------- | --------------------------------- |
|
|
24
24
|
| value | Number/String | undefined | 绑定值 |
|
|
25
|
+
| stringMode | Boolean | false | 高精度模式,非空绑定值和 input/change 事件值使用字符串 |
|
|
25
26
|
| type | String | 'money' | 输入框类型,可选值:'money'、'rate'、'number' |
|
|
26
27
|
| maxlength | Number | 16 | 最大输入长度 |
|
|
27
|
-
| min | Number
|
|
28
|
-
| max | Number
|
|
29
|
-
| step | Number
|
|
28
|
+
| min | Number/String | -9999999999999.99 | 最小值限制;高精度模式可传字符串 |
|
|
29
|
+
| max | Number/String | 9999999999999.99 | 最大值限制;高精度模式可传字符串 |
|
|
30
|
+
| step | Number/String | 1 | 步进值;高精度模式可传字符串 |
|
|
30
31
|
| stepStrictly | Boolean | false | 是否严格按步进值递增/递减 |
|
|
31
32
|
| disabled | Boolean | undefined | 是否禁用 |
|
|
32
33
|
| isClearable | Boolean | false | 是否可清空 |
|
|
@@ -110,3 +111,99 @@
|
|
|
110
111
|
<InputNumber v-model="value" type="number" :step="0.5" :stepStrictly="true" />
|
|
111
112
|
```
|
|
112
113
|
|
|
114
|
+
### 20 位小数汇率
|
|
115
|
+
|
|
116
|
+
`stringMode` 默认关闭,原有金额/利率的精度、数值事件保持原样。开启后,输入、回显、步进、范围比较和变更事件使用高精度十进制处理。`input` / `change` 的非空值是字符串,清空为 `undefined`;`blur` / `clear` 事件签名保持原样。
|
|
117
|
+
|
|
118
|
+
下面 `cl-*` 是默认注册前缀;业务项目按自己的 `Vue.use(n20, { prefix })` 替换标签。
|
|
119
|
+
|
|
120
|
+
```vue
|
|
121
|
+
<template>
|
|
122
|
+
<!-- 汇率没有百分号,显式设置 20 位精度和足够的总输入长度。 -->
|
|
123
|
+
<cl-input-number
|
|
124
|
+
v-model="exchangeRate"
|
|
125
|
+
string-mode
|
|
126
|
+
type="number"
|
|
127
|
+
:d-num="20"
|
|
128
|
+
:maxlength="35"
|
|
129
|
+
min="0"
|
|
130
|
+
step="0.00000000000000000001"
|
|
131
|
+
/>
|
|
132
|
+
</template>
|
|
133
|
+
|
|
134
|
+
<script>
|
|
135
|
+
export default {
|
|
136
|
+
data() {
|
|
137
|
+
// 接口响应和请求中的汇率也使用字符串,避免到达组件前已被 Number 舍入。
|
|
138
|
+
return { exchangeRate: '1.12345678901234567890' }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
</script>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
- `stringMode` 不改变 `dNum`、`format` 和 `maxlength` 默认值。`format` 仍优先于 `dNum`;有旧 `format` 配置时需同步调整。
|
|
145
|
+
- `maxlength` 统计全部字符,默认 16。20 位小数加上 `0.` 至少需要 22;按整数位、负号和小数点配置总长度。示例的 35 可容纳默认范围的 13 位整数、负号、小数点和 20 位小数。
|
|
146
|
+
- 默认按十进制四舍五入到配置位数并补零;`rangeAuto` 开启后至少补到配置位数,并保留更多已有小数位。需要最多 20 位时使用默认 `rangeAuto=false`。
|
|
147
|
+
- `min`、`max`、`step` 的字符串形式用于高精度模式;普通模式继续传 Number。`step` 使用正值。`stepStrictly` 的中点方向与原有 `Math.round` 一致。
|
|
148
|
+
- 可粘贴科学计数法,提交展开为普通十进制字符串。空值、NaN、Infinity、非十进制输入不作为有效数字。
|
|
149
|
+
- 按上下箭头改变输入框内容,提交时机与旧模式相同,在 change/blur 时同步绑定值。
|
|
150
|
+
- 高精度字段不要使用 `v-model.number`、`Number` 或 `parseFloat`;校验规则使用字符串或自定义十进制校验,而不是 `type: 'number'`。
|
|
151
|
+
|
|
152
|
+
### 高精度区间与动态字段
|
|
153
|
+
|
|
154
|
+
`InputNumberRange` 通过 attrs 透传 `string-mode`、`d-num`、`maxlength`、`step` 等参数;两个端点和 `change` 数组元素都是字符串,清空端点为 `undefined`。
|
|
155
|
+
|
|
156
|
+
```vue
|
|
157
|
+
<!-- 两个字符串端点互相作为上下界,第 20 位的差异也参与比较。 -->
|
|
158
|
+
<cl-input-number-range
|
|
159
|
+
:start-value.sync="startRate"
|
|
160
|
+
:end-value.sync="endRate"
|
|
161
|
+
string-mode
|
|
162
|
+
type="number"
|
|
163
|
+
:d-num="20"
|
|
164
|
+
:maxlength="35"
|
|
165
|
+
/>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`Filters` / `AdvancedFilter` 在 `item.props` 里设置相同参数;`DynamicField` 在字段配置里设置:
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
// input-number / input-number-range / input-rate-range 均支持这些数值参数。
|
|
172
|
+
const field = {
|
|
173
|
+
type: 'input-number',
|
|
174
|
+
numberType: 'number',
|
|
175
|
+
stringMode: true,
|
|
176
|
+
dNum: 20,
|
|
177
|
+
maxlength: 35,
|
|
178
|
+
min: '0',
|
|
179
|
+
step: '0.00000000000000000001'
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
动态利率区间不传配置时仍使用 rate、0~100 的旧默认值;汇率按实际单位和范围配置 `numberType`、`min`、`max`。
|
|
184
|
+
|
|
185
|
+
### 高精度计算、列表和 Excel
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
import { Decimal, formatExchangeRate } from 'n20-common-lib'
|
|
189
|
+
|
|
190
|
+
// 金额计算保留中间精度,只在最终结果四舍五入到两位;返回值依然是字符串。
|
|
191
|
+
const amount = new Decimal('1').times('1.00499999999999999999').toFixed(2) // '1.00'
|
|
192
|
+
const inverseRate = new Decimal('1').div('3').toFixed(20) // '0.33333333333333333333'
|
|
193
|
+
const text = formatExchangeRate('1.12345678901234567890') // 原样保留 20 位
|
|
194
|
+
|
|
195
|
+
// TablePro:独立于原有 formatRate,可通过数组参数指定其他展示位数。
|
|
196
|
+
const proColumn = { prop: 'rate', label: '汇率', formatter: 'formatExchangeRate' }
|
|
197
|
+
const customColumn = { prop: 'rate', label: '汇率', formatter: ['formatExchangeRate', 12] }
|
|
198
|
+
|
|
199
|
+
// 旧 Table:新增 exchangeRate 模板格式,固定展示 20 位。
|
|
200
|
+
const oldColumn = { prop: 'rate', label: '汇率', formatter: '{rate|exchangeRate}' }
|
|
201
|
+
|
|
202
|
+
// toExcel 使用字符串 rows;识别上述字符串形式的汇率 formatter 时自动设置文本格式。
|
|
203
|
+
const rows = [{ rate: '1.12345678901234567890' }]
|
|
204
|
+
// 自定义或数组 formatter 的导出列可显式设置 numFmt: '@',并同样传入字符串。
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`formatExchangeRate(value, decimalPlaces = 20)` 不添加千分位或百分号,零正常显示,空值/非法值显示 `--`。旧 `formatRate` / `{rate|rate}` 仍为 6 位,原有 `N` 和 `numerify` 保持兼容;高精度计算与展示需显式切换到新工具。
|
|
208
|
+
|
|
209
|
+
导出的 `Decimal` 使用独立配置:80 位有效数字、四舍五入,不修改 `decimal.js` 原有全局配置。有效数字包括整数和小数部分;业务需要不同运算精度或舍入规则时使用 `Decimal.clone(...)`,不要修改公共构造器的配置。高精度数据进入 Excel 工具前必须已经是字符串,改变单元格显示格式不能恢复之前丢失的尾数。
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
import emitter from '../../utils/element-ui-emitter'
|
|
35
35
|
import { $lc } from '../../utils/i18n/index'
|
|
36
36
|
import N from '../../utils/numberPor'
|
|
37
|
+
import { Decimal, parseDecimal } from '../../utils/decimal'
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
export default {
|
|
@@ -44,6 +45,11 @@ export default {
|
|
|
44
45
|
type: [Number, String],
|
|
45
46
|
default: undefined
|
|
46
47
|
},
|
|
48
|
+
// 高精度模式显式启用,绑定值及变更事件使用十进制字符串。
|
|
49
|
+
stringMode: {
|
|
50
|
+
type: Boolean,
|
|
51
|
+
default: false
|
|
52
|
+
},
|
|
47
53
|
/**
|
|
48
54
|
* 输入框类型
|
|
49
55
|
* - money: 金额类型,默认保留2位小数,启用千分位格式化
|
|
@@ -62,15 +68,15 @@ export default {
|
|
|
62
68
|
default: 16
|
|
63
69
|
},
|
|
64
70
|
min: {
|
|
65
|
-
type: Number,
|
|
71
|
+
type: [Number, String],
|
|
66
72
|
default: -9999999999999.99
|
|
67
73
|
},
|
|
68
74
|
max: {
|
|
69
|
-
type: Number,
|
|
75
|
+
type: [Number, String],
|
|
70
76
|
default: 9999999999999.99
|
|
71
77
|
},
|
|
72
78
|
step: {
|
|
73
|
-
type: Number,
|
|
79
|
+
type: [Number, String],
|
|
74
80
|
default: 1
|
|
75
81
|
},
|
|
76
82
|
stepStrictly: {
|
|
@@ -156,6 +162,10 @@ export default {
|
|
|
156
162
|
watch: {
|
|
157
163
|
value: {
|
|
158
164
|
handler(val) {
|
|
165
|
+
if (this.stringMode) {
|
|
166
|
+
this.setStringDisplay(val)
|
|
167
|
+
return
|
|
168
|
+
}
|
|
159
169
|
// number类型不启用千分位格式化,但应用小数位限制
|
|
160
170
|
if (this.type === 'number') {
|
|
161
171
|
if (val === undefined || val === null) {
|
|
@@ -187,9 +197,54 @@ export default {
|
|
|
187
197
|
}
|
|
188
198
|
},
|
|
189
199
|
immediate: true
|
|
200
|
+
},
|
|
201
|
+
fNum() {
|
|
202
|
+
if (this.stringMode) this.setStringDisplay(this.value)
|
|
190
203
|
}
|
|
191
204
|
},
|
|
192
205
|
methods: {
|
|
206
|
+
// rangeAuto 允许已有小数位超过配置值,其余情况按配置位数四舍五入。
|
|
207
|
+
formatStringValue(value, originalValue = value) {
|
|
208
|
+
const decimal = parseDecimal(value)
|
|
209
|
+
if (!decimal) return ''
|
|
210
|
+
// Decimal 会省略尾零,rangeAuto 额外保留普通十进制输入中的原始小数长度。
|
|
211
|
+
const fraction = String(originalValue).trim().match(/\.(\d+)$/)
|
|
212
|
+
const places = this.rangeAuto
|
|
213
|
+
? Math.max(this.fNum, decimal.decimalPlaces(), fraction ? fraction[1].length : 0)
|
|
214
|
+
: this.fNum
|
|
215
|
+
// 先舍入再输出,让接近零的负数规范化为零,避免显示值与事件值的符号不同。
|
|
216
|
+
return decimal.toDecimalPlaces(places).toFixed(places)
|
|
217
|
+
},
|
|
218
|
+
setStringDisplay(value) {
|
|
219
|
+
const text = this.formatStringValue(value)
|
|
220
|
+
this.valueStr = this.type === 'number' ? text : N.addThousands(text)
|
|
221
|
+
},
|
|
222
|
+
// 比较和严格步进使用 Decimal,避免第 20 位不同的值被当成相等。
|
|
223
|
+
constrainStringValue(value, strictly = false) {
|
|
224
|
+
let decimal = value
|
|
225
|
+
const step = parseDecimal(this.step)
|
|
226
|
+
if (strictly && step && step.gt(0)) {
|
|
227
|
+
// 与旧 Math.round 的负数中点规则保持一致:中点向正无穷方向取整。
|
|
228
|
+
decimal = decimal.div(step).toDecimalPlaces(0, Decimal.ROUND_HALF_CEIL).times(step)
|
|
229
|
+
}
|
|
230
|
+
const min = parseDecimal(this.min)
|
|
231
|
+
const max = parseDecimal(this.max)
|
|
232
|
+
if (min && decimal.lt(min)) decimal = min
|
|
233
|
+
if (max && decimal.gt(max)) decimal = max
|
|
234
|
+
return decimal
|
|
235
|
+
},
|
|
236
|
+
changeStringValue(valStr) {
|
|
237
|
+
const decimal = parseDecimal(valStr)
|
|
238
|
+
const value = decimal ? this.formatStringValue(this.constrainStringValue(decimal, this.stepStrictly), valStr) : undefined
|
|
239
|
+
this.setStringDisplay(value)
|
|
240
|
+
this.$nextTick(() => {
|
|
241
|
+
if (this.value !== value) {
|
|
242
|
+
this.$emit('input', value)
|
|
243
|
+
this.$emit('change', value)
|
|
244
|
+
this.dispatch('ElFormItem', 'el.form.change', [value])
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
},
|
|
193
248
|
focusFn() {
|
|
194
249
|
this.isFocus = true
|
|
195
250
|
if (!this.disabled && this.valueStr) {
|
|
@@ -211,6 +266,16 @@ export default {
|
|
|
211
266
|
},
|
|
212
267
|
stepFn(ev) {
|
|
213
268
|
if ((ev.code === 'ArrowUp' || ev.code === 'ArrowDown') && !this.disabled) {
|
|
269
|
+
if (this.stringMode) {
|
|
270
|
+
const value = parseDecimal(this.valueStr)
|
|
271
|
+
const step = parseDecimal(this.step)
|
|
272
|
+
if (value && step && step.gt(0)) {
|
|
273
|
+
ev.preventDefault()
|
|
274
|
+
const next = ev.code === 'ArrowUp' ? value.plus(step) : value.minus(step)
|
|
275
|
+
this.valueStr = this.formatStringValue(this.constrainStringValue(next), this.valueStr)
|
|
276
|
+
}
|
|
277
|
+
return
|
|
278
|
+
}
|
|
214
279
|
let val = N(this.valueStr)
|
|
215
280
|
if (!isNaN(val)) {
|
|
216
281
|
ev.preventDefault()
|
|
@@ -231,6 +296,15 @@ export default {
|
|
|
231
296
|
}
|
|
232
297
|
},
|
|
233
298
|
inputFn(valStr) {
|
|
299
|
+
if (this.stringMode) {
|
|
300
|
+
// 编辑中的符号和小数点暂时保留,完整值在失焦时统一规范化。
|
|
301
|
+
if (['', '-', '+', '.', '-.', '+.'].includes(valStr) || parseDecimal(valStr)) {
|
|
302
|
+
this.preValue = valStr
|
|
303
|
+
} else {
|
|
304
|
+
this.valueStr = this.preValue
|
|
305
|
+
}
|
|
306
|
+
return
|
|
307
|
+
}
|
|
234
308
|
if (valStr !== '-' && isNaN(valStr.replace(/,/g, ''))) {
|
|
235
309
|
this.valueStr = this.preValue
|
|
236
310
|
} else {
|
|
@@ -244,6 +318,11 @@ export default {
|
|
|
244
318
|
this.changeIng = false
|
|
245
319
|
})
|
|
246
320
|
|
|
321
|
+
if (this.stringMode) {
|
|
322
|
+
this.changeStringValue(valStr)
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
|
|
247
326
|
let val = N(valStr)
|
|
248
327
|
if (isNaN(val)) {
|
|
249
328
|
this.valueStr = ''
|
|
@@ -62,11 +62,12 @@ export default {
|
|
|
62
62
|
methods: {
|
|
63
63
|
startChange(val) {
|
|
64
64
|
this.$emit('update:start-value', val)
|
|
65
|
-
|
|
65
|
+
// .sync 的父组件更新尚未完成,使用本次输入值确保 change 包含最新端点。
|
|
66
|
+
this.$emit('change', [val, this.endValue])
|
|
66
67
|
},
|
|
67
68
|
endChange(val) {
|
|
68
69
|
this.$emit('update:end-value', val)
|
|
69
|
-
this.$emit('change', [this.startValue,
|
|
70
|
+
this.$emit('change', [this.startValue, val])
|
|
70
71
|
},
|
|
71
72
|
blurFn() {
|
|
72
73
|
this.$emit('blur', [this.startValue, this.endValue])
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import numerify from 'numerify'
|
|
2
2
|
import dayjs from 'dayjs'
|
|
3
|
+
import { formatExchangeRate } from '../../utils/decimal'
|
|
3
4
|
|
|
4
5
|
function tplFn(row, sc, mck, map = {}) {
|
|
5
6
|
let str = ''
|
|
@@ -10,7 +11,7 @@ function tplFn(row, sc, mck, map = {}) {
|
|
|
10
11
|
let key = kA[0]
|
|
11
12
|
if (kA.length === 1) {
|
|
12
13
|
str += row[key]
|
|
13
|
-
} else if (row[key] || row[key] === 0) {
|
|
14
|
+
} else if (row[key] || row[key] === 0 || kA[1] === 'exchangeRate') {
|
|
14
15
|
let type = kA[1]
|
|
15
16
|
switch (type) {
|
|
16
17
|
case 'money':
|
|
@@ -19,6 +20,10 @@ function tplFn(row, sc, mck, map = {}) {
|
|
|
19
20
|
case 'rate':
|
|
20
21
|
str += numerify(row[key], '0.000000', Math.floor)
|
|
21
22
|
break
|
|
23
|
+
// 汇率单独使用高精度格式,保留历史 rate 的 6 位展示规则。
|
|
24
|
+
case 'exchangeRate':
|
|
25
|
+
str += formatExchangeRate(row[key])
|
|
26
|
+
break
|
|
22
27
|
case 'map':
|
|
23
28
|
str += map[row[key]]
|
|
24
29
|
break
|
|
@@ -3,6 +3,7 @@ import 'vxe-table/lib/style.css'
|
|
|
3
3
|
import filterContent from './filterContent.vue'
|
|
4
4
|
import numerify from 'numerify'
|
|
5
5
|
import dayjs from 'dayjs'
|
|
6
|
+
import { formatExchangeRate } from '../../utils/decimal'
|
|
6
7
|
import { $lc } from '../../utils/i18n/index.js'
|
|
7
8
|
import zhCN from 'vxe-table/lib/locale/lang/zh-CN'
|
|
8
9
|
import { getColumnField, getRowValue, getVxeCore, setVxeVersion } from './compat.js'
|
|
@@ -136,6 +137,10 @@ if (vxeFormats && typeof vxeFormats.mixin === 'function') {
|
|
|
136
137
|
return '--'
|
|
137
138
|
}
|
|
138
139
|
},
|
|
140
|
+
// 独立的汇率格式支持字符串值,参数可指定展示的小数位数。
|
|
141
|
+
formatExchangeRate({ cellValue }, decimalPlaces = 20) {
|
|
142
|
+
return formatExchangeRate(cellValue, decimalPlaces)
|
|
143
|
+
},
|
|
139
144
|
// 格式化时间,默认 yyyy-MM-dd HH:mm:ss
|
|
140
145
|
formatDatetime({ cellValue }) {
|
|
141
146
|
if (cellValue) {
|
|
@@ -143,7 +143,6 @@ import tableSetSize from '../TableSetSize/index.vue'
|
|
|
143
143
|
import { $lc } from '../../utils/i18n/index.js'
|
|
144
144
|
import { getColumnField, getEventField, isVxe320OrNewer, normalizeArray } from './compat.js'
|
|
145
145
|
|
|
146
|
-
|
|
147
146
|
/** 将 Element 表格风格的 show-overflow-tooltip 转为 vxe-column 的 show-overflow */
|
|
148
147
|
function hasOverflowTooltipKey(item) {
|
|
149
148
|
return (
|
|
@@ -153,8 +152,7 @@ function hasOverflowTooltipKey(item) {
|
|
|
153
152
|
}
|
|
154
153
|
|
|
155
154
|
function applyTooltipToShowOverflow(item) {
|
|
156
|
-
const explicitOverflow =
|
|
157
|
-
item.showOverflow !== undefined ? item.showOverflow : item['show-overflow']
|
|
155
|
+
const explicitOverflow = item.showOverflow !== undefined ? item.showOverflow : item['show-overflow']
|
|
158
156
|
const hasTooltipKey = hasOverflowTooltipKey(item)
|
|
159
157
|
if (!hasTooltipKey && explicitOverflow === undefined) {
|
|
160
158
|
return item
|
|
@@ -704,7 +702,7 @@ export default {
|
|
|
704
702
|
// 计算所有列的基础宽度总和
|
|
705
703
|
let totalBaseWidth = 0
|
|
706
704
|
columns.forEach((column) => {
|
|
707
|
-
if (column.static && column.label === $lc(
|
|
705
|
+
if (column.static && column.label === $lc('操作') && !column.width && !column.minWidth) {
|
|
708
706
|
column.width = 180
|
|
709
707
|
}
|
|
710
708
|
if (column.type === 'checkbox') {
|
|
@@ -728,17 +726,27 @@ export default {
|
|
|
728
726
|
baseWidth = parseInt(widthValue) || 0
|
|
729
727
|
}
|
|
730
728
|
column['_baseWidth_'] = baseWidth
|
|
729
|
+
column['width'] = `${baseWidth}px`
|
|
730
|
+
column['min-width'] = undefined
|
|
731
|
+
column['minWidth'] = undefined
|
|
731
732
|
totalBaseWidth += baseWidth
|
|
732
733
|
})
|
|
733
734
|
// 如果所有列的基础宽度总和 >= 容器宽度,不做处理
|
|
734
|
-
if (totalBaseWidth >= windowWidth) {
|
|
735
|
+
if (totalBaseWidth >= windowWidth + 200) {
|
|
735
736
|
return columns
|
|
736
737
|
} else {
|
|
737
738
|
const seqColumns = columns.filter((item) => item.type !== 'seq' && item.type !== 'checkbox')
|
|
738
739
|
if (seqColumns.length > 0) {
|
|
739
740
|
seqColumns[0].width = ''
|
|
740
741
|
}
|
|
741
|
-
return columns
|
|
742
|
+
return columns.map((column) => {
|
|
743
|
+
if (!column.type) {
|
|
744
|
+
const rate = column['_baseWidth_'] / totalBaseWidth
|
|
745
|
+
column['width'] = Math.ceil(windowWidth * rate) + 35
|
|
746
|
+
return column
|
|
747
|
+
}
|
|
748
|
+
return column
|
|
749
|
+
})
|
|
742
750
|
}
|
|
743
751
|
}
|
|
744
752
|
return calc(columns)
|
package/src/index.js
CHANGED
|
@@ -134,6 +134,7 @@ import { type } from './utils/judgeType.js'
|
|
|
134
134
|
import list2tree from './utils/list2tree'
|
|
135
135
|
import { msgPor, msgboxPor } from './utils/msgboxPor.js'
|
|
136
136
|
import N from './utils/numberPor.js' // 扩展Number
|
|
137
|
+
import { Decimal, formatExchangeRate } from './utils/decimal.js' // 高精度汇率计算与展示
|
|
137
138
|
import { closeTab, linkGo, linkPush } from './utils/urlToGo'
|
|
138
139
|
import { accountFormat } from './utils/accountFormat'
|
|
139
140
|
|
|
@@ -409,6 +410,8 @@ export {
|
|
|
409
410
|
list2tree,
|
|
410
411
|
monitor,
|
|
411
412
|
numerify,
|
|
413
|
+
Decimal,
|
|
414
|
+
formatExchangeRate,
|
|
412
415
|
operatingStatus,
|
|
413
416
|
realUrl,
|
|
414
417
|
refreshTab,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import DecimalJs from 'decimal.js'
|
|
2
|
+
|
|
3
|
+
// 独立配置,避免改变业务项目中 decimal.js 的全局精度;80 位为中间乘除保留余量。
|
|
4
|
+
export const Decimal = DecimalJs.clone({ precision: 80, rounding: DecimalJs.ROUND_HALF_UP })
|
|
5
|
+
|
|
6
|
+
// 输入和格式化共用十进制解析,不经过 Number,空值和非有限值不作为有效数字。
|
|
7
|
+
export function parseDecimal(value) {
|
|
8
|
+
if (value === undefined || value === null || value === '') return null
|
|
9
|
+
const text = String(value).replace(/,/g, '').trim()
|
|
10
|
+
if (!/^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i.test(text)) return null
|
|
11
|
+
const decimal = new Decimal(text)
|
|
12
|
+
return decimal.isFinite() ? decimal : null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 汇率默认展示 20 位小数,零值正常展示,缺失或非法值使用表格统一占位符。
|
|
16
|
+
export function formatExchangeRate(value, decimalPlaces = 20) {
|
|
17
|
+
const decimal = parseDecimal(value)
|
|
18
|
+
return decimal ? decimal.toDecimalPlaces(decimalPlaces).toFixed(decimalPlaces) : '--'
|
|
19
|
+
}
|
package/src/utils/toExcel.js
CHANGED
|
@@ -45,6 +45,10 @@ export default async function toExcel() {
|
|
|
45
45
|
if (/\|\s?rate/.test(col.formatter)) {
|
|
46
46
|
_col.style.numFmt = '0.000000'
|
|
47
47
|
}
|
|
48
|
+
// 高精度汇率按文本展示;rows 中的原值也必须为字符串。
|
|
49
|
+
if (/\|\s?exchangeRate/.test(col.formatter) || col.formatter === 'formatExchangeRate') {
|
|
50
|
+
_col.style.numFmt = '@'
|
|
51
|
+
}
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
cols.push(_col)
|