@nstc-business/imes 1.0.0

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 ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@nstc-business/imes",
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "main": "src/index.js",
6
+ "scripts": {
7
+ "serve": "vue-cli-service serve",
8
+ "test": "vue-cli-service serve --test",
9
+ "deadcode": "vue-cli-service serve --seecode",
10
+ "analy": "vue-cli-service build --analy",
11
+ "getI18n": "node ./node_modules/nstc-get-i18n",
12
+ "format": "prettier --write \"./**/*.{html,vue,ts,js,json,md}\"",
13
+ "build": "vue-cli-service build"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "dependencies": {
19
+ "axios": "^0.21.4",
20
+ "core-js": "^3.6.5",
21
+ "dayjs": "^1.10.7",
22
+ "echarts": "^5.3.3",
23
+ "el-table-draggable": "^1.4.4",
24
+ "element-ui": "^2.15.6",
25
+ "jquery": "^3.6.0",
26
+ "moment": "^2.30.1",
27
+ "n20-common-lib": "2.5.18",
28
+ "qrcode": "^1.5.0",
29
+ "resize-detector": "^0.3.0",
30
+ "swiper": "^11.1.14",
31
+ "vue": "^2.6.11",
32
+ "vue-router": "^3.5.2",
33
+ "vuedraggable": "^2.24.3",
34
+ "vuex": "^3.6.2",
35
+ "vxe-table": "3.6.17",
36
+ "xe-utils": "^3.5.11"
37
+ },
38
+ "files": [
39
+ "src/components",
40
+ "src/index.js"
41
+ ],
42
+ "devDependencies": {
43
+ "@babel/plugin-proposal-optional-chaining": "^7.14.5",
44
+ "@babel/plugin-transform-flow-comments": "^7.14.5",
45
+ "@vue/cli-plugin-babel": "~4.5.0",
46
+ "@vue/cli-plugin-eslint": "~4.5.0",
47
+ "@vue/cli-service": "~4.5.0",
48
+ "@vue/compiler-sfc": "^3.0.0-rc.6",
49
+ "babel-eslint": "^10.1.0",
50
+ "babel-plugin-component": "^1.1.1",
51
+ "compression-webpack-plugin": "^3.1.0",
52
+ "copy-webpack-plugin": "^6.4.1",
53
+ "eslint": "^6.7.2",
54
+ "prettier": "^3.0.3",
55
+ "husky": "^8.0.0",
56
+ "eslint-plugin-vue": "^6.2.2",
57
+ "less": "^3.12.2",
58
+ "nstc-gitinfo": "^0.0.6",
59
+ "nstc-get-i18n": "^0.0.3",
60
+ "less-loader": "^7.0.0",
61
+ "vue-template-compiler": "^2.6.11",
62
+ "webpack": "^4.46.0",
63
+ "webpack-bundle-analyzer": "^3.9.0",
64
+ "webpack-deadcode-plugin": "^0.1.15"
65
+ },
66
+ "browserslist": [
67
+ "> 1%",
68
+ "last 2 versions",
69
+ "not dead"
70
+ ]
71
+ }
@@ -0,0 +1,419 @@
1
+ import { N } from 'n20-common-lib'
2
+
3
+ export function formatDate(date, fmt) {
4
+ date = new Date(date)
5
+ if (/(y+)/.test(fmt)) {
6
+ fmt = fmt.replace(
7
+ RegExp.$1,
8
+ (date.getFullYear() + '').substr(4 - RegExp.$1.length)
9
+ )
10
+ }
11
+ let o = {
12
+ 'M+': date.getMonth() + 1,
13
+ 'd+': date.getDate(),
14
+ 'h+': date.getHours(),
15
+ 'm+': date.getMinutes(),
16
+ 's+': date.getSeconds()
17
+ }
18
+ for (let k in o) {
19
+ if (new RegExp(`(${k})`).test(fmt)) {
20
+ let str = o[k] + ''
21
+ fmt = fmt.replace(
22
+ RegExp.$1,
23
+ RegExp.$1.length === 1 ? str : padLeftZero(str)
24
+ )
25
+ }
26
+ }
27
+ return fmt
28
+ }
29
+ function padLeftZero(str) {
30
+ return ('00' + str).substr(str.length)
31
+ }
32
+
33
+ /**
34
+ * 公式转换,支持复杂运算逻辑
35
+ * @param {array} statisticItems 数据映射数组
36
+ * @param {string} formula 公式
37
+ * @returns {object} codeFormula cod公式,randomFormula 随机数公式
38
+ */
39
+ export const replaceStatsNameWithCode = (statisticItems, formula) => {
40
+ // 创建statsName到code的映射
41
+ const nameToCodeMap = statisticItems.reduce((map, item) => {
42
+ if (item.formula) {
43
+ map[item.statsName] = {
44
+ formula: item.formula
45
+ }
46
+ } else {
47
+ map[item.statsName] = item.code
48
+ }
49
+
50
+ return map
51
+ }, {})
52
+
53
+ // 创建statsName到随机数的映射
54
+ const nameToRandomMap = statisticItems.reduce((map, item) => {
55
+ if (item.formula) {
56
+ map[item.statsName] = {
57
+ formula: item.formula
58
+ }
59
+ } else {
60
+ map[item.statsName] = Math.random().toFixed(10)
61
+ }
62
+ return map
63
+ }, {})
64
+
65
+ // 增强的正则表达式,支持多层嵌套的复杂表达式
66
+ const complexPattern =
67
+ /(?:\([^()]*\)|[\u4e00-\u9fa5a-zA-Z0-9-]+)(?:\s*[+\-*/]\s*(?:\([^()]*\)|[\u4e00-\u9fa5a-zA-Z0-9-]+))*/g
68
+
69
+ // 生成code公式
70
+ const codeFormula = formula.replace(complexPattern, match => {
71
+ // 处理带括号的表达式
72
+ const hasParentheses = match.startsWith('(') && match.endsWith(')')
73
+ const content = hasParentheses ? match.slice(1, -1) : match
74
+
75
+ // 先替换括号内的内容
76
+ const innerReplaced = content.replace(
77
+ /[\u4e00-\u9fa5a-zA-Z0-9-]+/g,
78
+ part =>
79
+ typeof nameToCodeMap[part] === 'object'
80
+ ? nameToCodeMap[part]?.formula
81
+ : nameToCodeMap[part] === undefined
82
+ ? part
83
+ : `#${nameToCodeMap[part]}#`
84
+ )
85
+
86
+ // 再处理运算符
87
+ const result = innerReplaced
88
+ .split(/([+\-*/()])/)
89
+ .map(part => {
90
+ const trimmed = part.trim()
91
+ return nameToCodeMap[trimmed] ? `#${nameToCodeMap[trimmed]}#` : part
92
+ })
93
+ .join('')
94
+
95
+ return hasParentheses ? `(${result})` : result
96
+ })
97
+
98
+ // 生成随机数公式
99
+ const randomFormula = formula.replace(complexPattern, match => {
100
+ // 处理带括号的表达式
101
+ const hasParentheses = match.startsWith('(') && match.endsWith(')')
102
+ const content = hasParentheses ? match.slice(1, -1) : match
103
+
104
+ // 先替换括号内的内容
105
+ const innerReplaced = content.replace(
106
+ /[\u4e00-\u9fa5a-zA-Z0-9-]+/g,
107
+ part => {
108
+ const random = nameToRandomMap[part]
109
+ return typeof random === 'object'
110
+ ? random.formula
111
+ : random === undefined
112
+ ? part
113
+ : random
114
+ }
115
+ )
116
+
117
+ // 再处理运算符
118
+ const result = innerReplaced
119
+ .split(/([+\-*/()])/)
120
+ .map(part => {
121
+ const trimmed = part.trim()
122
+ const random = nameToRandomMap[trimmed]
123
+ return random === undefined ? part : random
124
+ })
125
+ .join('')
126
+
127
+ return hasParentheses ? `(${result})` : result
128
+ })
129
+
130
+ return {
131
+ codeFormula,
132
+ randomFormula
133
+ }
134
+ }
135
+
136
+ /**
137
+ * 设置表格合并行的公共方法
138
+ * @param {any[]} list 表格数据
139
+ * @param {any[]} columns 表头
140
+ * @param {String[]} merges 要合并的列字段名组成的数组
141
+ * @param {String[]} condition 判断条件的字段名组成的数组
142
+ * @param {String} field 表头取值的字段名
143
+ */
144
+
145
+ export function setMergeCells({
146
+ list = [],
147
+ columns = [],
148
+ merges = [],
149
+ condition = [],
150
+ field = 'field'
151
+ }) {
152
+ const validMap = new Map() // 有效合并配置项
153
+ for (let i = 0; i < list.length; i++) {
154
+ // 根据判断条件取出当前行对应的表格数据,生成唯一标识
155
+ const hashKey = condition
156
+ .map(key => {
157
+ let rowStr = ''
158
+ if (
159
+ Object.prototype.toString.call(list[i][key]) === '[object Object]'
160
+ ) {
161
+ rowStr = list[i][key].code
162
+ } else {
163
+ rowStr = list[i][key]
164
+ }
165
+ return rowStr
166
+ })
167
+ .join('&&')
168
+
169
+ let flag = true
170
+ let count = 1
171
+ // 从当前索引i开始,往后找,数据相同的话,count自增,否则跳出while循环
172
+ while (flag && i + count < list.length) {
173
+ // 根据判断条件取出下一行的表格数据,生成唯一标识
174
+ const nextRow = condition
175
+ .map(key => {
176
+ let rowStr = ''
177
+ if (
178
+ Object.prototype.toString.call(list[i + count][key]) ===
179
+ '[object Object]'
180
+ ) {
181
+ rowStr = list[i + count][key].code
182
+ } else {
183
+ rowStr = list[i + count][key]
184
+ }
185
+ return rowStr
186
+ })
187
+ .join('&&')
188
+ if (hashKey === nextRow) {
189
+ // 数据相同,count自增
190
+ count++
191
+ } else {
192
+ // 数据不同,跳出while循环
193
+ flag = false
194
+ }
195
+ }
196
+ // 将有效的合并配置项存入validMap
197
+ count > 1 && validMap.set(hashKey + '_' + i, { row: i, rowspan: count })
198
+ // i 跳过已合并的行,减 1 是因为 i++
199
+ i += count - 1
200
+ // 重置 while 循环标识
201
+ flag = true
202
+ }
203
+ // 合并记录转换
204
+ const recordsList = [...validMap.values()]
205
+ if (recordsList.length == 0) {
206
+ return []
207
+ }
208
+ // 预处理列索引
209
+
210
+ const cols = merges.reduce((acc, mergeField) => {
211
+ const idx = columns.findIndex(item => item[field] === mergeField)
212
+ idx !== -1 && acc.push(idx)
213
+ return acc
214
+ }, [])
215
+
216
+ // 遍历列索引,生成合并单元格配置集合
217
+ const mergeCells = recordsList.flatMap(({ row, rowspan }) =>
218
+ cols.map(col => ({ row, col, rowspan, colspan: 1 }))
219
+ )
220
+ return mergeCells
221
+ }
222
+
223
+ export const formatFloat2 = (num, digit3 = 2) => {
224
+ return parseFloat(N.subFixed(num, digit3))
225
+ }
226
+
227
+ /**
228
+ * 校验Table每行字段是否为空
229
+ * @param {Object} requiredFields 字段映射对象,例:{minVal: '最小值', maxVal: '最大值', score: '分数'}
230
+ * @param {Array} data 要验证的数据数组
231
+ * @returns {Error|null} 验证通过返回null,失败返回Error对象
232
+ */
233
+ export const validateFieldPresence = (requiredFields = {}, data = []) => {
234
+ // 添加参数有效性检查
235
+ if (!Array.isArray(data)) return new Error('数据参数必须为数组')
236
+
237
+ for (const [index, row] of data.entries()) {
238
+ // 遍历字段映射对象的键
239
+ for (const field of Object.keys(requiredFields)) {
240
+ // 添加字段存在性检查
241
+ if (!Object.prototype.hasOwnProperty.call(row, field)) {
242
+ return new Error(`第${index + 1}条规则:当前行缺少必要字段[${field}]`)
243
+ }
244
+
245
+ // 加强空值判断
246
+ if (row[field] === '' || row[field] == null) {
247
+ return new Error(
248
+ `第${index + 1}条规则:${requiredFields[field] || field}不能为空`
249
+ )
250
+ }
251
+ }
252
+ }
253
+
254
+ // 明确返回验证通过状态
255
+ return null
256
+ }
257
+
258
+ /**
259
+ * 校验数字格式
260
+ * @param {Object} options 配置项
261
+ * @param {Object} options.requiredFields 字段映射对象 {minVal: '最小值', maxVal: '最大值'}
262
+ * @param {Array} options.data 要验证的数据数组
263
+ * @param {RegExp} options.validRegex 验证正则,默认/^-?\d+(?:\.\d{1,6})?$/
264
+ * @returns {Error|null}
265
+ */
266
+ export const validateNumberFormat = ({
267
+ requiredFields = {},
268
+ data = [],
269
+ validRegex = /^-?\d+(?:\.\d{1,6})?$/
270
+ } = {}) => {
271
+ if (!Array.isArray(data)) return new Error('数据参数必须为数组')
272
+
273
+ for (const [index, row] of data.entries()) {
274
+ for (const [field, fieldName] of Object.entries(requiredFields)) {
275
+ const value = String(row[field] ?? '') // 处理undefined/null
276
+
277
+ if (!validRegex.test(value)) {
278
+ return new Error(`第${index + 1}条规则:${fieldName}格式不正确`)
279
+ }
280
+ }
281
+ }
282
+ return null
283
+ }
284
+
285
+ /**
286
+ * 校验区间重叠
287
+ * @param {Array} data
288
+ * @param {Object} requiredFields = {},
289
+ * @returns
290
+ */
291
+ export const validateRangeOverlap = (
292
+ data = [],
293
+ requiredFields = {
294
+ minVal: '下限',
295
+ maxVal: '上限'
296
+ }
297
+ ) => {
298
+ if (!Array.isArray(data)) return new Error('数据参数必须为数组')
299
+ // 增强参数校验
300
+ const fields = Object.keys(requiredFields)
301
+ if (fields.length !== 2) {
302
+ return new Error('字段映射对象必须且只能包含两个字段[minKey, maxKey]')
303
+ }
304
+ const [minKey, maxKey] = fields
305
+
306
+ for (let i = 0; i < data.length; i++) {
307
+ const current = data[i]
308
+
309
+ // 合并字段存在性检查
310
+ for (const key of [minKey, maxKey]) {
311
+ // 修复属性检查方式,增加对象类型判断
312
+ if (
313
+ !current ||
314
+ typeof current !== 'object' ||
315
+ !Object.prototype.hasOwnProperty.call(current, key)
316
+ ) {
317
+ return new Error(`第${i + 1}条规则:缺少${requiredFields[key]}字段`)
318
+ }
319
+ // 空值检查添加到此处
320
+ if (current[key] === '' || current[key] == null) {
321
+ return new Error(`第${i + 1}条规则:${requiredFields[key]}不能为空`)
322
+ }
323
+ }
324
+ // 增强数值转换
325
+ const [minVal, maxVal] = [Number(current[minKey]), Number(current[maxKey])]
326
+ if (isNaN(minVal)) {
327
+ return new Error(`第${i + 1}条规则:${requiredFields[minKey]}值无效`)
328
+ }
329
+ if (isNaN(maxVal)) {
330
+ return new Error(`第${i + 1}条规则:${requiredFields[maxKey]}值无效`)
331
+ }
332
+ if (minVal >= maxVal) {
333
+ return new Error(
334
+ `第${i + 1}条规则:${requiredFields[minKey]}不能大于等于${requiredFields[maxKey]}`
335
+ )
336
+ }
337
+
338
+ // 优化区间检查性能
339
+ for (let j = i + 1; j < data.length; j++) {
340
+ const other = data[j]
341
+ // 添加对other行的校验
342
+ if ([minKey, maxKey].some(k => !Object.prototype.hasOwnProperty.call(other, k))) continue
343
+
344
+ if (isRangeOverlap(current, other, minKey, maxKey)) {
345
+ return new Error(`第${i + 1}条和第${j + 1}条规则存在区间重叠`)
346
+ }
347
+ }
348
+ }
349
+ return null
350
+ }
351
+
352
+ export const isRangeOverlap = (a, b, minKey = 'minVal', maxKey = 'maxVal') => {
353
+ const aMin = Number(a[minKey])
354
+ const aMax = Number(a[maxKey])
355
+ const bMin = Number(b[minKey])
356
+ const bMax = Number(b[maxKey])
357
+
358
+ return (
359
+ !isNaN(aMin) &&
360
+ !isNaN(aMax) &&
361
+ !isNaN(bMin) &&
362
+ !isNaN(bMax) &&
363
+ Math.max(aMin, bMin) < Math.min(aMax, bMax)
364
+ )
365
+ }
366
+
367
+ export function toFixedTrunc(num, decimals) {
368
+ if (isNaN(num) || num === undefined || num === null) return num
369
+
370
+ let numStr =
371
+ typeof num !== 'string'
372
+ ? N.toString(num).replace(/,/g, '')
373
+ : num.replace(/,/g, '')
374
+ const [integerPart, decimalPart = ''] = numStr.split('.')
375
+ const trimmedDecimal = decimalPart.slice(0, decimals)
376
+ const paddedDecimal = trimmedDecimal.padEnd(decimals, '0')
377
+ return decimals === 0 ? integerPart : `${integerPart}.${paddedDecimal}`
378
+ }
379
+
380
+ export function addThousands(data) {
381
+ return N.addThousands(data)
382
+ }
383
+
384
+ /**
385
+ * 深度克隆
386
+ * @param target
387
+ * @return {}
388
+ */
389
+ export const deepClone = (target, map = new Map()) => {
390
+ // 检测数据的类型
391
+ if (typeof target === 'object' && target !== null) {
392
+ // 克隆数据之前, 进行判断, 数据之前是否克隆过
393
+ const cache = map.get(target)
394
+ if (cache) {
395
+ return cache
396
+ }
397
+ // 判断目标数据的类型
398
+ const isArray = Array.isArray(target)
399
+ // 创建一个容器
400
+ const result = isArray ? [] : {}
401
+ // 将新的结果存入到容器中
402
+ map.set(target, result)
403
+ // 如果目标数据为数组
404
+ if (isArray) {
405
+ // forEach 遍历
406
+ target.forEach((item, index) => {
407
+ result[index] = deepClone(item, map)
408
+ })
409
+ } else {
410
+ // 如果是对象, 获取所有的键名, 然后 forEach 遍历
411
+ Object.keys(target).forEach((key) => {
412
+ result[key] = deepClone(target[key], map)
413
+ })
414
+ }
415
+ return result
416
+ } else {
417
+ return target
418
+ }
419
+ }
@@ -0,0 +1,150 @@
1
+ <template>
2
+ <page>
3
+ <template slot="header">
4
+ <expandable-pane title="模型信息">
5
+ <descriptions>
6
+ <el-descriptions-item label="模型编号">
7
+ {{ model.code }}
8
+ </el-descriptions-item>
9
+ <el-descriptions-item label="模型名称">
10
+ {{ model.name }}
11
+ </el-descriptions-item>
12
+ <el-descriptions-item label="模型版本">
13
+ {{ '版本' + model.version }}
14
+ </el-descriptions-item>
15
+ </descriptions>
16
+ </expandable-pane>
17
+ </template>
18
+ <expandable-pane title="模型指标">
19
+ <div class="flex-box">
20
+ <tree disable :modelTree="model.modelTreeVO"/>
21
+ <modelCalcTest
22
+ v-if="(model.type === 1 || model.type === 2) && model.code"
23
+ class="m-l-xl flex-item overflow"
24
+ ref="calcTable"
25
+ @changeLoading="changeLoading"
26
+ @calcResult="calcResult"
27
+ :modelData="model"
28
+ :modelCode="model.code"
29
+ :modelId="model.modelID"
30
+ ></modelCalcTest>
31
+ </div>
32
+ </expandable-pane>
33
+ <div slot="footer">
34
+ <expandable-pane title="模型结果">
35
+ <div>测算结果:{{ model.showStyle === 2 ? addThousands(result) : result }}</div>
36
+ </expandable-pane>
37
+ <div class="flex-box flex-c flex-v">
38
+ <el-button plain @click="back">返回</el-button>
39
+ <el-button
40
+ type="primary"
41
+ size="small"
42
+ class="preservations"
43
+ @click="calc"
44
+ >
45
+ 测算
46
+ </el-button>
47
+ </div>
48
+ </div>
49
+ </page>
50
+ </template>
51
+
52
+ <script>
53
+ import {N, Page, ExpandablePane, Descriptions} from 'n20-common-lib'
54
+ import tree from './tree.vue'
55
+ import modelCalcTest from './modelCalcTest.vue'
56
+
57
+ export default {
58
+ props: {
59
+ modelId: String
60
+ },
61
+ watch: {
62
+ modelId: {
63
+ handler(val) {
64
+ if (val) {
65
+ this.getModelById()
66
+ }
67
+ },
68
+ immediate: true
69
+ }
70
+ },
71
+ components: {
72
+ tree,
73
+ modelCalcTest,
74
+ Page,
75
+ ExpandablePane,
76
+ Descriptions
77
+ },
78
+ data() {
79
+ return {
80
+ indicatorList: [],
81
+ model: {type: 1, name: ''},
82
+ result: '',
83
+ loading: false
84
+ }
85
+ },
86
+ methods: {
87
+ addThousands(value) {
88
+ return N.addThousands(value)
89
+ },
90
+ changeLoading(flag) {
91
+ this.loading = flag
92
+ },
93
+ calcResult(val) {
94
+ this.result = val
95
+ },
96
+ async getModelById() {
97
+ if (!this.modelId) return
98
+ const {data, code} = await this.$axios.post('/imes/v1/indicatorModel/getModelById', {id: this.modelId})
99
+ if (code === 0) {
100
+ this.model = data
101
+ if (this.model.type == 3) {
102
+ this.$set(this, 'modelTreeVO', [this.model.modelTreeVO])
103
+ this.recursionFn(this.modelTreeVO)
104
+ } else {
105
+ this.recursionFn([this.model.modelTreeVO])
106
+ }
107
+ }
108
+ },
109
+ /*
110
+ 处理叶子节点为2的情况,
111
+ */
112
+ recursionFn(arr) {
113
+ if (arr.length === 0) return arr
114
+ arr.forEach(item => {
115
+ item.label = item.name
116
+ if (item.isLeaf === 2) {
117
+ this.$set(
118
+ item,
119
+ 'indicatorDeepList',
120
+ item.children.map(item2 => {
121
+ return {...item2, label: item.name}
122
+ })
123
+ )
124
+ item.children = []
125
+ } else {
126
+ if (item.children?.length) {
127
+ this.recursionFn(item.children)
128
+ }
129
+ }
130
+ })
131
+ },
132
+ calc() {
133
+ this.$refs.calcTable.calc()
134
+ },
135
+ back() {
136
+ this.$router.go(-1)
137
+ }
138
+ }
139
+ }
140
+ </script>
141
+
142
+ <style scoped lang="less">
143
+ .el-form {
144
+ white-space: nowrap;
145
+ }
146
+
147
+ .model_input {
148
+ width: 70%;
149
+ }
150
+ </style>