vue2-client 1.20.19 → 1.20.21

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.
Files changed (86) hide show
  1. package/.claude/settings.local.json +30 -30
  2. package/.eslintrc.js +74 -74
  3. package/Components.md +60 -60
  4. package/index.js +31 -31
  5. package/jest-transform-stub.js +8 -8
  6. package/jest.setup.js +7 -7
  7. package/package.json +1 -1
  8. package/src/assets/img/querySlotDemo.svg +15 -15
  9. package/src/base-client/components/common/AmapMarker/AmapPointRendering.vue +120 -120
  10. package/src/base-client/components/common/CitySelect/index.js +3 -3
  11. package/src/base-client/components/common/CitySelect/index.md +109 -109
  12. package/src/base-client/components/common/FormGroupEdit/index.js +3 -3
  13. package/src/base-client/components/common/FormGroupEdit/index.md +43 -43
  14. package/src/base-client/components/common/JSONToTree/jsontotree.vue +271 -271
  15. package/src/base-client/components/common/PersonSetting/index.js +3 -3
  16. package/src/base-client/components/common/Tree/Tree.vue +149 -149
  17. package/src/base-client/components/common/Tree/index.js +2 -2
  18. package/src/base-client/components/common/Upload/index.js +3 -3
  19. package/src/base-client/components/common/XAddNativeForm/index.md +146 -146
  20. package/src/base-client/components/common/XCard/XCard.vue +64 -64
  21. package/src/base-client/components/common/XDataDrawer/XDataDrawer.vue +180 -180
  22. package/src/base-client/components/common/XDataDrawer/index.js +3 -3
  23. package/src/base-client/components/common/XDataDrawer/index.md +41 -41
  24. package/src/base-client/components/common/XDescriptions/XDescriptionsGroup.vue +671 -671
  25. package/src/base-client/components/common/XDescriptions/index.js +3 -3
  26. package/src/base-client/components/common/XDescriptions/index.md +382 -382
  27. package/src/base-client/components/common/XForm/index.md +178 -178
  28. package/src/base-client/components/common/XFormTable/XFormTable.vue +8 -0
  29. package/src/base-client/components/common/XFormTable/demo.vue +79 -37
  30. package/src/base-client/components/common/XInspectionDetailDrawer/components/CheckItems.vue +42 -17
  31. package/src/base-client/components/common/XInspectionDetailDrawer/components/HazardPhotos.vue +48 -19
  32. package/src/base-client/components/common/XInspectionDetailDrawer/services/inspectionService.js +5 -3
  33. package/src/base-client/components/common/XStepView/XStepView.vue +252 -252
  34. package/src/base-client/components/common/XStepView/index.js +3 -3
  35. package/src/base-client/components/common/XStepView/index.md +31 -31
  36. package/src/base-client/components/common/XTable/XTable.vue +20 -0
  37. package/src/base-client/components/common/XTable/index.md +255 -255
  38. package/src/base-client/components/system/DictionaryDetailsView/DictionaryDetailsView.vue +232 -232
  39. package/src/base-client/plugins/Config.js +19 -19
  40. package/src/base-client/plugins/tabs-page-plugin.js +39 -39
  41. package/src/components/Charts/Bar.vue +62 -62
  42. package/src/components/Charts/ChartCard.vue +134 -134
  43. package/src/components/Charts/Liquid.vue +67 -67
  44. package/src/components/Charts/MiniArea.vue +39 -39
  45. package/src/components/Charts/MiniBar.vue +39 -39
  46. package/src/components/Charts/MiniProgress.vue +75 -75
  47. package/src/components/Charts/MiniSmoothArea.vue +40 -40
  48. package/src/components/Charts/Radar.vue +68 -68
  49. package/src/components/Charts/RankList.vue +77 -77
  50. package/src/components/Charts/TagCloud.vue +113 -113
  51. package/src/components/Charts/TransferBar.vue +64 -64
  52. package/src/components/Charts/Trend.vue +82 -82
  53. package/src/components/Charts/chart.less +12 -12
  54. package/src/components/Charts/smooth.area.less +13 -13
  55. package/src/components/NumberInfo/NumberInfo.vue +54 -54
  56. package/src/components/NumberInfo/index.js +3 -3
  57. package/src/components/NumberInfo/index.less +54 -54
  58. package/src/components/NumberInfo/index.md +43 -43
  59. package/src/components/card/ChartCard.vue +79 -79
  60. package/src/components/chart/Bar.vue +60 -60
  61. package/src/components/chart/MiniArea.vue +67 -67
  62. package/src/components/chart/MiniBar.vue +59 -59
  63. package/src/components/chart/MiniProgress.vue +57 -57
  64. package/src/components/chart/Radar.vue +80 -80
  65. package/src/components/chart/RankingList.vue +60 -60
  66. package/src/components/chart/Trend.vue +79 -79
  67. package/src/components/chart/index.less +9 -9
  68. package/src/components/checkbox/ColorCheckbox.vue +157 -157
  69. package/src/components/input/IInput.vue +66 -66
  70. package/src/components/menu/SideMenu.vue +75 -75
  71. package/src/components/menu/menu.js +273 -273
  72. package/src/components/tool/AStepItem.vue +60 -60
  73. package/src/layouts/CommonLayout.vue +56 -56
  74. package/src/lib.js +1 -1
  75. package/src/mock/extend/index.js +84 -84
  76. package/src/mock/goods/index.js +108 -108
  77. package/src/pages/WorkflowDetail/WorkFlowDemo4.vue +127 -0
  78. package/src/pages/WorkflowDetail/WorkflowPageDetail/WorkFlowBaseInformation.vue +417 -417
  79. package/src/pages/dashboard/workplace/WorkPlace.vue +141 -141
  80. package/src/pages/system/dictionary/index.vue +44 -44
  81. package/src/pages/system/monitor/loginInfor/index.vue +37 -37
  82. package/src/pages/system/monitor/operLog/index.vue +37 -37
  83. package/src/services/api/cas.js +79 -79
  84. package/src/store/modules/setting.js +119 -119
  85. package/src/utils/errorCode.js +6 -6
  86. package//350/277/201/347/247/273/346/227/245/345/277/227.md +15 -15
@@ -1,671 +1,671 @@
1
- <template>
2
- <a-row id="XDescriptionGroup" :gutter="24">
3
- <!-- left 模式:左侧导航 + 右侧内容 -->
4
- <template v-if="tabMode === 'left'">
5
- <a-col :span="4" v-if="realData.length">
6
- <a-tabs tab-position="left" v-model="activeTab" @change="scrollToGroup">
7
- <template v-for="(item, index) in realData">
8
- <a-tab-pane :tab="item.title" :key="index" v-if="item.title"></a-tab-pane>
9
- </template>
10
- </a-tabs>
11
- </a-col>
12
- <a-col :span="realData.length ? 20 : 24" ref="descriptionsGroupContext" class="descriptionsGroupContext">
13
- <div
14
- class="descriptions-item"
15
- :ref="`descriptions-item-${realDataIndex}`"
16
- :key="realDataIndex"
17
- v-for="(realDataItem, realDataIndex) in realData"
18
- >
19
- <!-- 渲染所有分组内容 -->
20
- <template v-if="!loadError">
21
- <!-- 插槽分组:由父组件通过具名插槽传入内容 -->
22
- <template v-if="realDataItem.slot">
23
- <slot :name="realDataItem.slotName" :data="data"></slot>
24
- </template>
25
- <!-- 带有子的详情 -->
26
- <template v-else-if="realDataItem.title && groups[realDataIndex]?.type === 'array'">
27
- <div class="ant-descriptions-title">{{ realDataItem.title }}</div>
28
- <div class="descriptions-array-item">
29
- <a-descriptions
30
- v-for="(arrayItem, arrayIndex) in realDataItem.column"
31
- :column="isMobile ? 1 : column"
32
- size="small"
33
- :key="arrayIndex"
34
- :title="arrayItem.title"
35
- >
36
- <template v-for="(item, index) in arrayItem.column">
37
- <!-- 大多数情况 循环下 空值省略不展示,todo 后期可能加配置处理 -->
38
- <a-descriptions-item :key="index" v-if="item.value" :label="item.key">
39
- {{ formatText(item.value) }}
40
- </a-descriptions-item>
41
- </template>
42
- </a-descriptions>
43
- </div>
44
- </template>
45
- <a-descriptions v-else-if="realDataItem.title" :column="isMobile ? 1 : column" :title="realDataItem.title">
46
- <a-descriptions-item
47
- v-for="(item, index) in realDataItem.column"
48
- :key="index"
49
- :span="item.span || 1"
50
- v-if="shouldShowField(item, data)"
51
- >
52
- <template #label>
53
- {{ item.key }}
54
- <span v-if="isFieldRequired(item, data)" style="color: red">*</span>
55
- </template>
56
- <span :style="getFieldStyle(item, data)">
57
- <!-- 超链接样式兼容 -->
58
- <template v-if="item.isLink">
59
- <span
60
- role="link"
61
- tabindex="0"
62
- class="link-text"
63
- @click="handleLinkClick(item, data, $event)"
64
- @keyup.enter="handleLinkClick(item, data, $event)">
65
- {{ formatFieldValue(item.value, item, data) || '--' }}
66
- </span>
67
- </template>
68
- <template v-else>
69
- {{ formatFieldValue(item.value, item, data) || '--' }}
70
- </template>
71
- <a-button
72
- v-if="item.canEdit"
73
- type="link"
74
- size="small"
75
- @click="handleEditField(item)"
76
- style="padding: 0 4px"
77
- >
78
- <a-icon type="edit"/>
79
- </a-button>
80
- </span>
81
- </a-descriptions-item>
82
- </a-descriptions>
83
- </template>
84
- </div>
85
- </a-col>
86
- </template>
87
-
88
- <!-- top 模式:顶部横向 tab -->
89
- <template v-else-if="tabMode === 'top'">
90
- <a-col :span="24" v-if="realData.length">
91
- <a-tabs v-model="activeTab" size="small" :tabBarStyle="{ margin: 0, padding: '0 8px' }">
92
- <a-tab-pane v-for="(item, index) in realData" :key="index" :tab="item.title">
93
- <div class="tab-content-scroll">
94
- <!-- 只渲染当前激活的分组内容 -->
95
- <template v-if="!loadError && realData[activeTab]">
96
- <!-- 插槽分组:由父组件通过具名插槽传入内容 -->
97
- <template v-if="realData[activeTab].slot">
98
- <slot :name="realData[activeTab].slotName" :data="data"></slot>
99
- </template>
100
- <!-- 带有子的详情 -->
101
- <template v-else-if="realData[activeTab].title && groups[activeTab]?.type === 'array'">
102
- <div class="ant-descriptions-title" v-if="tabMode !== 'top'">{{ realData[activeTab].title }}</div>
103
- <div class="descriptions-array-item">
104
- <a-descriptions
105
- v-for="(arrayItem, arrayIndex) in realData[activeTab].column"
106
- :column="isMobile ? 1 : column"
107
- size="small"
108
- :key="arrayIndex"
109
- :title="arrayItem.title"
110
- >
111
- <template v-for="(fieldItem, fieldIndex) in arrayItem.column">
112
- <a-descriptions-item :key="fieldIndex" v-if="fieldItem.value" :label="fieldItem.key">
113
- {{ formatText(fieldItem.value) }}
114
- </a-descriptions-item>
115
- </template>
116
- </a-descriptions>
117
- </div>
118
- </template>
119
- <a-descriptions
120
- v-else-if="realData[activeTab].title"
121
- :column="isMobile ? 1 : column"
122
- :title="tabMode === 'top' ? undefined : realData[activeTab].title"
123
- >
124
- <a-descriptions-item
125
- v-for="(fieldItem, fieldIndex) in realData[activeTab].column"
126
- :key="fieldIndex"
127
- :span="fieldItem.span || 1"
128
- v-if="shouldShowField(fieldItem, data)"
129
- >
130
- <template #label>
131
- {{ fieldItem.key }}
132
- <span v-if="isFieldRequired(fieldItem, data)" style="color: red">*</span>
133
- </template>
134
- <span :style="getFieldStyle(fieldItem, fieldItem.value, data)">
135
- <!-- 超链接样式兼容 - 修复变量引用错误 -->
136
- <template v-if="fieldItem.isLink">
137
- <span
138
- role="link"
139
- tabindex="0"
140
- class="link-text"
141
- @click="handleLinkClick(fieldItem, data, $event)"
142
- @keyup.enter="handleLinkClick(fieldItem, data, $event)"
143
- >
144
- {{ formatFieldValue(fieldItem.value, fieldItem, data) || '--' }}
145
- </span>
146
- </template>
147
- <template v-else>
148
- {{ formatFieldValue(fieldItem.value, fieldItem, data) || '--' }}
149
- </template>
150
- <a-button
151
- v-if="fieldItem.canEdit"
152
- type="link"
153
- size="small"
154
- @click="handleEditField(fieldItem)"
155
- style="padding: 0 4px"
156
- >
157
- <a-icon type="edit"/>
158
- </a-button>
159
- </span>
160
- </a-descriptions-item>
161
- </a-descriptions>
162
- </template>
163
- </div>
164
- </a-tab-pane>
165
- </a-tabs>
166
- </a-col>
167
- </template>
168
-
169
- <!-- none 模式:无 tab,直接展示所有内容 -->
170
- <template v-else>
171
- <a-col :span="24" class="descriptionsGroupContext">
172
- <div class="descriptions-item" :key="realDataIndex" v-for="(realDataItem, realDataIndex) in realData">
173
- <!-- 渲染所有分组内容 -->
174
- <template v-if="!loadError">
175
- <!-- 插槽分组:由父组件通过具名插槽传入内容 -->
176
- <template v-if="realDataItem.slot">
177
- <slot :name="realDataItem.slotName" :data="data"></slot>
178
- </template>
179
- <!-- 带有子的详情 -->
180
- <template v-else-if="realDataItem.title && groups[realDataIndex]?.type === 'array'">
181
- <div class="ant-descriptions-title">{{ realDataItem.title }}</div>
182
- <div class="descriptions-array-item">
183
- <a-descriptions
184
- v-for="(arrayItem, arrayIndex) in realDataItem.column"
185
- :column="isMobile ? 1 : column"
186
- size="small"
187
- :key="arrayIndex"
188
- :title="arrayItem.title"
189
- >
190
- <template v-for="(item, index) in arrayItem.column">
191
- <a-descriptions-item :key="index" v-if="item.value" :label="item.key">
192
- {{ formatText(item.value) }}
193
- </a-descriptions-item>
194
- </template>
195
- </a-descriptions>
196
- </div>
197
- </template>
198
- <a-descriptions v-else-if="realDataItem.title" :column="isMobile ? 1 : column" :title="realDataItem.title">
199
- <a-descriptions-item
200
- v-for="(item, index) in realDataItem.column"
201
- :key="index"
202
- :span="item.span || 1"
203
- v-if="shouldShowField(item, data)"
204
- >
205
- <template #label>
206
- {{ item.key }}
207
- <span v-if="isFieldRequired(item, data)" style="color: red">*</span>
208
- </template>
209
- <span :style="getFieldStyle(item, item.value, data)">
210
- <template v-if="item.isLink">
211
- <span
212
- role="link"
213
- tabindex="0"
214
- class="link-text"
215
- @click="handleLinkClick(item, data, $event)"
216
- @keyup.enter="handleLinkClick(item, data, $event)"
217
- >
218
- {{ formatFieldValue(item.value, item, data) || '--' }}
219
- </span>
220
- </template>
221
- <template v-else>
222
- {{ formatFieldValue(item.value, item, data) || '--' }}
223
- </template>
224
- <a-button
225
- v-if="item.canEdit"
226
- type="link"
227
- size="small"
228
- @click="handleEditField(item)"
229
- style="padding: 0 4px"
230
- >
231
- <a-icon type="edit"/>
232
- </a-button>
233
- </span>
234
- </a-descriptions-item>
235
- </a-descriptions>
236
- </template>
237
- </div>
238
- </a-col>
239
- </template>
240
- </a-row>
241
- </template>
242
- <script>
243
- import {mapState} from 'vuex'
244
- import {getRealKeyData} from '@vue2-client/utils/formatter'
245
- import {getConfigByName} from '@vue2-client/services/api/common'
246
- import {executeStrFunctionByContext} from '@vue2-client/utils/runEvalFunction'
247
- import XAddNativeForm from '@vue2-client/base-client/components/common/XAddNativeForm/XAddNativeForm.vue'
248
-
249
- export default {
250
- name: 'XDescriptionsGroup',
251
- components: {
252
- XAddNativeForm
253
- },
254
- props: {
255
- // 内容
256
- data: {
257
- type: Object,
258
- required: true,
259
- default: undefined
260
- },
261
- configName: {
262
- type: String,
263
- required: true,
264
- default: ''
265
- },
266
- // 配置所属命名空间
267
- serviceName: {
268
- type: String,
269
- default: process.env.VUE_APP_SYSTEM_NAME
270
- }
271
- },
272
- mounted() {
273
- },
274
- beforeDestroy() {
275
- const formGroupContext = this.$refs.formGroupContext?.$el
276
- if (formGroupContext && formGroupContext.removeEventListener) {
277
- formGroupContext.removeEventListener('scroll', this.onScroll)
278
- }
279
- },
280
- created() {
281
- this.initConfig()
282
- },
283
- data() {
284
- return {
285
- // 加载状态
286
- loading: false,
287
- loadError: false,
288
- realData: [],
289
- // 获取到的配置组
290
- groups: {},
291
- activeTab: 0,
292
- // 从配置中获取的值
293
- column: 3, // 默认值
294
- getRealData: false, // 默认值
295
- tabMode: 'left' // 默认值
296
- }
297
- },
298
- computed: {
299
- ...mapState('setting', {isMobile: 'isMobile'})
300
- },
301
- methods: {
302
- initConfig() {
303
- this.loading = true
304
- if (this.configName) {
305
- this.getConfig()
306
- // 只在 left 模式启用滚动监听
307
- if (this.tabMode === 'left') {
308
- this.$nextTick(() => {
309
- const formGroupContext = this.$refs.descriptionsGroupContext?.$el
310
- if (formGroupContext && formGroupContext.addEventListener) {
311
- formGroupContext.addEventListener('scroll', this.onScroll)
312
- }
313
- })
314
- }
315
- } else {
316
- this.loading = false
317
- this.loadError = true
318
- }
319
- },
320
- scrollToGroup(index) {
321
- const groupElement = this.$refs[`descriptions-item-${index}`][0]
322
- if (groupElement) {
323
- groupElement.scrollIntoView({behavior: 'smooth'})
324
- }
325
- },
326
- getConfig() {
327
- getConfigByName(this.configName, this.serviceName, res => {
328
- if (res.groups) {
329
- // 从配置中获取 column、getRealData 和 tabMode
330
- this.column = res.column !== undefined ? res.column : 3
331
- this.getRealData = res.getRealData !== undefined ? res.getRealData : false
332
- this.tabMode = res.tabMode !== undefined ? res.tabMode : 'left'
333
-
334
- // 解析分组配置
335
- const groups = this.parseGroupsConfig(res.groups)
336
- this.groups = groups
337
- this.realData = groups
338
- .map(group => {
339
- const dataItem = {title: group.name}
340
-
341
- if (group.type === 'slot') {
342
- // 插槽分组:由父组件通过具名插槽传入内容
343
- return {
344
- title: group.name,
345
- slot: true,
346
- slotName: group.slotName || group.name
347
- }
348
- }
349
-
350
- if (group.type === 'array') {
351
- // 处理数组类型数据
352
- const arrayData = this.data[group.key] || []
353
- dataItem.column = arrayData
354
- .map((item, index) => ({
355
- title: `${group.name} ${index + 1}`,
356
- column: group.fields.map(field => ({
357
- key: field.name,
358
- value: item[field.key]
359
- }))
360
- }))
361
- .filter(Boolean)
362
- } else {
363
- dataItem.column = group.fields.map(field => ({
364
- key: field.name,
365
- dataKey: field.key,
366
- value: this.getRealKeyData(this.data, field.key),
367
- span: field.span,
368
- // 存储字段函数配置
369
- showIf: field.showIf,
370
- styleFunc: field.styleFunc,
371
- formatFunc: field.formatFunc,
372
- requireFunc: field.requireFunc,
373
- canEdit: field.canEdit === true,
374
- isLink: field.isLink === true
375
- }))
376
- }
377
-
378
- return dataItem.column.length > 0 ? dataItem : null
379
- })
380
- .filter(Boolean)
381
- }
382
- })
383
- },
384
- /**
385
- * 解析分组配置数据,支持新旧两种配置格式
386
- * @param {Object|Array} groupsConfig - 分组配置对象或数组
387
- * @returns {Array} 解析后的分组数组
388
- *
389
- * 配置格式说明:
390
- *
391
- * 旧模式 - 对象格式 (Object):
392
- * {
393
- * "表具信息": {
394
- * "f_gasbrand": "气表品牌", // 简单字段配置
395
- * "f_meter_type": "表具类型"
396
- * },
397
- * "设备信息": {
398
- * "type": "array", // 数组类型标识
399
- * "key": "devices", // type为array时必填:数据源key
400
- * "value": { // type为array时的字段配置
401
- * "f_devices_num": "设备数量",
402
- * "f_devices_type": "设备类型"
403
- * }
404
- * },
405
- * "客户信息": {
406
- * "f_user_code": { // 字段可配置为对象
407
- * "name": "用户编号",
408
- * "span": 2 // 可选:列跨度
409
- * },
410
- * "f_user_name": "用户姓名" // 简单字段配置
411
- * }
412
- * }
413
- *
414
- * 新模式 - 数组格式 (Array):
415
- * [{
416
- * "name": "客户信息",
417
- * "fields": [
418
- * {
419
- * "name": "用户编号",
420
- * "key": "f_user_code",
421
- * "span": 3
422
- * }
423
- * ]
424
- * }, {
425
- * "name": "设备信息",
426
- * "type": "array", // 数组类型标识
427
- * "key": "devices", // type为array时必填:数据源key
428
- * "fields": [
429
- * {
430
- * "name": "设备数量",
431
- * "key": "f_devices_num"
432
- * },
433
- * {
434
- * "name": "设备类型",
435
- * "key": "f_devices_type"
436
- * }
437
- * ]
438
- * }]
439
- */
440
- parseGroupsConfig(groupsConfig) {
441
- // 如果已经是数组格式,直接返回
442
- if (Array.isArray(groupsConfig)) {
443
- return groupsConfig
444
- }
445
-
446
- // 解析对象格式配置
447
- return Object.entries(groupsConfig).map(([groupName, groupConfig]) => {
448
- // 处理插槽类型的配置
449
- if (groupConfig.type === 'slot') {
450
- return {
451
- name: groupName,
452
- type: 'slot',
453
- slotName: groupConfig.slotName || groupName
454
- }
455
- }
456
-
457
- // 处理数组类型的配置
458
- if (groupConfig.type === 'array') {
459
- return {
460
- name: groupName,
461
- type: 'array',
462
- key: groupConfig.key,
463
- fields: Object.entries(groupConfig.value || {}).map(([k, v]) => ({
464
- key: k,
465
- name: v
466
- }))
467
- }
468
- }
469
-
470
- // 处理普通配置(支持新旧格式)
471
- return {
472
- name: groupName,
473
- fields: Object.entries(groupConfig).map(([fieldKey, fieldValue]) => ({
474
- key: fieldKey,
475
- name: typeof fieldValue === 'string' ? fieldValue : fieldValue.name,
476
- showIf: typeof fieldValue === 'object' ? fieldValue.showIf : undefined,
477
- styleFunc: typeof fieldValue === 'object' ? fieldValue.styleFunc : undefined,
478
- formatFunc: typeof fieldValue === 'object' ? fieldValue.formatFunc : undefined,
479
- requireFunc: typeof fieldValue === 'object' ? fieldValue.requireFunc : undefined,
480
- canEdit: typeof fieldValue === 'object' ? fieldValue.canEdit : undefined,
481
- // 新增超链接配置
482
- isLink: typeof fieldValue === 'object' ? fieldValue.isLink : undefined,
483
- ...(typeof fieldValue === 'object' ? fieldValue : {})
484
- }))
485
- }
486
- })
487
- },
488
- // 文字格式化
489
- formatText(value) {
490
- return value ?? '--'
491
- },
492
- // 执行字段展示函数
493
- shouldShowField(field, data) {
494
- if (!field.showIf) return true
495
- try {
496
- return executeStrFunctionByContext(this, field.showIf, [data, field.key])
497
- } catch (error) {
498
- console.warn('showIf 函数执行错误:', error)
499
- return true
500
- }
501
- },
502
- // 获取字段样式
503
- getFieldStyle(field, data) {
504
- if (!field.styleFunc) return {}
505
- try {
506
- return executeStrFunctionByContext(this, field.styleFunc, [data, field.key])
507
- } catch (error) {
508
- console.warn('styleFunc 函数执行错误:', error)
509
- return {}
510
- }
511
- },
512
- // 格式化字段值
513
- formatFieldValue(value, field, data) {
514
- if (!field.formatFunc) return value
515
- try {
516
- return executeStrFunctionByContext(this, field.formatFunc, [data, field.key])
517
- } catch (error) {
518
- console.warn('formatFunc 函数执行错误:', error)
519
- return value
520
- }
521
- },
522
- // 判断字段是否必填(显示红色星号)
523
- isFieldRequired(field, data) {
524
- if (!field.requireFunc) return false
525
- try {
526
- return executeStrFunctionByContext(this, field.requireFunc, [data, field.key])
527
- } catch (error) {
528
- console.warn('requireFunc 函数执行错误:', error)
529
- return false
530
- }
531
- },
532
- getRealKeyData(data, key) {
533
- if (this.getRealData) {
534
- return getRealKeyData(data)[key] || ''
535
- } else {
536
- return this.data[key] || ''
537
- }
538
- },
539
- // 触发编辑事件,由外部组件实现具体编辑逻辑
540
- handleEditField(field) {
541
- this.$emit('edit-field', {
542
- field,
543
- value: field.value,
544
- data: this.data
545
- })
546
- },
547
- // 处理超链接点击
548
- handleLinkClick(field, data, event) {
549
- event.stopPropagation()
550
- // 向父组件抛出事件
551
- this.$emit('link-click', {
552
- field,
553
- value: field.value,
554
- data: this.data
555
- })
556
- },
557
- onScroll() {
558
- // 只在 left 模式启用滚动联动
559
- if (this.tabMode !== 'left') return
560
-
561
- const formGroupContext = this.$refs.descriptionsGroupContext.$el
562
- let groupItems = []
563
- if (formGroupContext && formGroupContext.querySelectorAll) {
564
- groupItems = formGroupContext.querySelectorAll('.descriptions-item')
565
- }
566
- let activeIndex = this.activeTab
567
-
568
- groupItems.forEach((item, index) => {
569
- const rect = item.getBoundingClientRect()
570
- if (rect.top <= 205 && rect.bottom > 200) {
571
- activeIndex = index
572
- }
573
- })
574
- this.activeTab = activeIndex
575
- }
576
- },
577
- watch: {
578
- content: {
579
- handler() {
580
- this.initConfig()
581
- }
582
- },
583
- configName: {
584
- handler() {
585
- this.initConfig()
586
- }
587
- },
588
- serviceName: {
589
- handler() {
590
- this.initConfig()
591
- }
592
- }
593
- }
594
- }
595
- </script>
596
-
597
- <style lang="less" scoped>
598
- #XDescriptionGroup {
599
- height: 100%;
600
-
601
- :deep(.ant-descriptions-title) {
602
- color: @primary-color;
603
- }
604
-
605
- :deep(.ant-descriptions-item-label) {
606
- color: #6b7280; // 统一标签颜色
607
- font-weight: 500;
608
- }
609
-
610
- :deep(.ant-descriptions-item-content) {
611
- color: #111827; // 统一内容颜色
612
- font-weight: 400;
613
- }
614
-
615
- // 移除最后一行的 padding-bottom
616
- :deep(.ant-descriptions-view) {
617
- .ant-descriptions-row:last-child {
618
- > th,
619
- > td {
620
- padding-bottom: 0;
621
- }
622
- }
623
- }
624
-
625
- .descriptions-item {
626
- margin-bottom: 8px;
627
- }
628
-
629
- .descriptions-array-item {
630
- padding-left: 1rem;
631
- }
632
-
633
- .descriptionsGroupContext {
634
- height: 100%;
635
- overflow-y: scroll;
636
- }
637
-
638
- .link-text {
639
- color: @primary-color;
640
- text-decoration: none;
641
- cursor: pointer;
642
- outline: none;
643
-
644
- &:hover {
645
- text-decoration: underline;
646
- }
647
-
648
- &:focus {
649
- text-decoration: underline;
650
- box-shadow: 0 0 0 2px fade(@primary-color, 20%);
651
- }
652
- }
653
-
654
- // top 模式样式
655
- .tab-content-scroll {
656
- overflow-y: auto;
657
- padding: 8px 10px;
658
-
659
- :deep(.ant-descriptions-item) {
660
- padding-bottom: 12px;
661
- }
662
- }
663
-
664
- // none 模式:移除分组间距
665
- &:has(.tabMode-none) {
666
- .descriptions-item {
667
- margin-bottom: 4px;
668
- }
669
- }
670
- }
671
- </style>
1
+ <template>
2
+ <a-row id="XDescriptionGroup" :gutter="24">
3
+ <!-- left 模式:左侧导航 + 右侧内容 -->
4
+ <template v-if="tabMode === 'left'">
5
+ <a-col :span="4" v-if="realData.length">
6
+ <a-tabs tab-position="left" v-model="activeTab" @change="scrollToGroup">
7
+ <template v-for="(item, index) in realData">
8
+ <a-tab-pane :tab="item.title" :key="index" v-if="item.title"></a-tab-pane>
9
+ </template>
10
+ </a-tabs>
11
+ </a-col>
12
+ <a-col :span="realData.length ? 20 : 24" ref="descriptionsGroupContext" class="descriptionsGroupContext">
13
+ <div
14
+ class="descriptions-item"
15
+ :ref="`descriptions-item-${realDataIndex}`"
16
+ :key="realDataIndex"
17
+ v-for="(realDataItem, realDataIndex) in realData"
18
+ >
19
+ <!-- 渲染所有分组内容 -->
20
+ <template v-if="!loadError">
21
+ <!-- 插槽分组:由父组件通过具名插槽传入内容 -->
22
+ <template v-if="realDataItem.slot">
23
+ <slot :name="realDataItem.slotName" :data="data"></slot>
24
+ </template>
25
+ <!-- 带有子的详情 -->
26
+ <template v-else-if="realDataItem.title && groups[realDataIndex]?.type === 'array'">
27
+ <div class="ant-descriptions-title">{{ realDataItem.title }}</div>
28
+ <div class="descriptions-array-item">
29
+ <a-descriptions
30
+ v-for="(arrayItem, arrayIndex) in realDataItem.column"
31
+ :column="isMobile ? 1 : column"
32
+ size="small"
33
+ :key="arrayIndex"
34
+ :title="arrayItem.title"
35
+ >
36
+ <template v-for="(item, index) in arrayItem.column">
37
+ <!-- 大多数情况 循环下 空值省略不展示,todo 后期可能加配置处理 -->
38
+ <a-descriptions-item :key="index" v-if="item.value" :label="item.key">
39
+ {{ formatText(item.value) }}
40
+ </a-descriptions-item>
41
+ </template>
42
+ </a-descriptions>
43
+ </div>
44
+ </template>
45
+ <a-descriptions v-else-if="realDataItem.title" :column="isMobile ? 1 : column" :title="realDataItem.title">
46
+ <a-descriptions-item
47
+ v-for="(item, index) in realDataItem.column"
48
+ :key="index"
49
+ :span="item.span || 1"
50
+ v-if="shouldShowField(item, data)"
51
+ >
52
+ <template #label>
53
+ {{ item.key }}
54
+ <span v-if="isFieldRequired(item, data)" style="color: red">*</span>
55
+ </template>
56
+ <span :style="getFieldStyle(item, data)">
57
+ <!-- 超链接样式兼容 -->
58
+ <template v-if="item.isLink">
59
+ <span
60
+ role="link"
61
+ tabindex="0"
62
+ class="link-text"
63
+ @click="handleLinkClick(item, data, $event)"
64
+ @keyup.enter="handleLinkClick(item, data, $event)">
65
+ {{ formatFieldValue(item.value, item, data) || '--' }}
66
+ </span>
67
+ </template>
68
+ <template v-else>
69
+ {{ formatFieldValue(item.value, item, data) || '--' }}
70
+ </template>
71
+ <a-button
72
+ v-if="item.canEdit"
73
+ type="link"
74
+ size="small"
75
+ @click="handleEditField(item)"
76
+ style="padding: 0 4px"
77
+ >
78
+ <a-icon type="edit"/>
79
+ </a-button>
80
+ </span>
81
+ </a-descriptions-item>
82
+ </a-descriptions>
83
+ </template>
84
+ </div>
85
+ </a-col>
86
+ </template>
87
+
88
+ <!-- top 模式:顶部横向 tab -->
89
+ <template v-else-if="tabMode === 'top'">
90
+ <a-col :span="24" v-if="realData.length">
91
+ <a-tabs v-model="activeTab" size="small" :tabBarStyle="{ margin: 0, padding: '0 8px' }">
92
+ <a-tab-pane v-for="(item, index) in realData" :key="index" :tab="item.title">
93
+ <div class="tab-content-scroll">
94
+ <!-- 只渲染当前激活的分组内容 -->
95
+ <template v-if="!loadError && realData[activeTab]">
96
+ <!-- 插槽分组:由父组件通过具名插槽传入内容 -->
97
+ <template v-if="realData[activeTab].slot">
98
+ <slot :name="realData[activeTab].slotName" :data="data"></slot>
99
+ </template>
100
+ <!-- 带有子的详情 -->
101
+ <template v-else-if="realData[activeTab].title && groups[activeTab]?.type === 'array'">
102
+ <div class="ant-descriptions-title" v-if="tabMode !== 'top'">{{ realData[activeTab].title }}</div>
103
+ <div class="descriptions-array-item">
104
+ <a-descriptions
105
+ v-for="(arrayItem, arrayIndex) in realData[activeTab].column"
106
+ :column="isMobile ? 1 : column"
107
+ size="small"
108
+ :key="arrayIndex"
109
+ :title="arrayItem.title"
110
+ >
111
+ <template v-for="(fieldItem, fieldIndex) in arrayItem.column">
112
+ <a-descriptions-item :key="fieldIndex" v-if="fieldItem.value" :label="fieldItem.key">
113
+ {{ formatText(fieldItem.value) }}
114
+ </a-descriptions-item>
115
+ </template>
116
+ </a-descriptions>
117
+ </div>
118
+ </template>
119
+ <a-descriptions
120
+ v-else-if="realData[activeTab].title"
121
+ :column="isMobile ? 1 : column"
122
+ :title="tabMode === 'top' ? undefined : realData[activeTab].title"
123
+ >
124
+ <a-descriptions-item
125
+ v-for="(fieldItem, fieldIndex) in realData[activeTab].column"
126
+ :key="fieldIndex"
127
+ :span="fieldItem.span || 1"
128
+ v-if="shouldShowField(fieldItem, data)"
129
+ >
130
+ <template #label>
131
+ {{ fieldItem.key }}
132
+ <span v-if="isFieldRequired(fieldItem, data)" style="color: red">*</span>
133
+ </template>
134
+ <span :style="getFieldStyle(fieldItem, fieldItem.value, data)">
135
+ <!-- 超链接样式兼容 - 修复变量引用错误 -->
136
+ <template v-if="fieldItem.isLink">
137
+ <span
138
+ role="link"
139
+ tabindex="0"
140
+ class="link-text"
141
+ @click="handleLinkClick(fieldItem, data, $event)"
142
+ @keyup.enter="handleLinkClick(fieldItem, data, $event)"
143
+ >
144
+ {{ formatFieldValue(fieldItem.value, fieldItem, data) || '--' }}
145
+ </span>
146
+ </template>
147
+ <template v-else>
148
+ {{ formatFieldValue(fieldItem.value, fieldItem, data) || '--' }}
149
+ </template>
150
+ <a-button
151
+ v-if="fieldItem.canEdit"
152
+ type="link"
153
+ size="small"
154
+ @click="handleEditField(fieldItem)"
155
+ style="padding: 0 4px"
156
+ >
157
+ <a-icon type="edit"/>
158
+ </a-button>
159
+ </span>
160
+ </a-descriptions-item>
161
+ </a-descriptions>
162
+ </template>
163
+ </div>
164
+ </a-tab-pane>
165
+ </a-tabs>
166
+ </a-col>
167
+ </template>
168
+
169
+ <!-- none 模式:无 tab,直接展示所有内容 -->
170
+ <template v-else>
171
+ <a-col :span="24" class="descriptionsGroupContext">
172
+ <div class="descriptions-item" :key="realDataIndex" v-for="(realDataItem, realDataIndex) in realData">
173
+ <!-- 渲染所有分组内容 -->
174
+ <template v-if="!loadError">
175
+ <!-- 插槽分组:由父组件通过具名插槽传入内容 -->
176
+ <template v-if="realDataItem.slot">
177
+ <slot :name="realDataItem.slotName" :data="data"></slot>
178
+ </template>
179
+ <!-- 带有子的详情 -->
180
+ <template v-else-if="realDataItem.title && groups[realDataIndex]?.type === 'array'">
181
+ <div class="ant-descriptions-title">{{ realDataItem.title }}</div>
182
+ <div class="descriptions-array-item">
183
+ <a-descriptions
184
+ v-for="(arrayItem, arrayIndex) in realDataItem.column"
185
+ :column="isMobile ? 1 : column"
186
+ size="small"
187
+ :key="arrayIndex"
188
+ :title="arrayItem.title"
189
+ >
190
+ <template v-for="(item, index) in arrayItem.column">
191
+ <a-descriptions-item :key="index" v-if="item.value" :label="item.key">
192
+ {{ formatText(item.value) }}
193
+ </a-descriptions-item>
194
+ </template>
195
+ </a-descriptions>
196
+ </div>
197
+ </template>
198
+ <a-descriptions v-else-if="realDataItem.title" :column="isMobile ? 1 : column" :title="realDataItem.title">
199
+ <a-descriptions-item
200
+ v-for="(item, index) in realDataItem.column"
201
+ :key="index"
202
+ :span="item.span || 1"
203
+ v-if="shouldShowField(item, data)"
204
+ >
205
+ <template #label>
206
+ {{ item.key }}
207
+ <span v-if="isFieldRequired(item, data)" style="color: red">*</span>
208
+ </template>
209
+ <span :style="getFieldStyle(item, item.value, data)">
210
+ <template v-if="item.isLink">
211
+ <span
212
+ role="link"
213
+ tabindex="0"
214
+ class="link-text"
215
+ @click="handleLinkClick(item, data, $event)"
216
+ @keyup.enter="handleLinkClick(item, data, $event)"
217
+ >
218
+ {{ formatFieldValue(item.value, item, data) || '--' }}
219
+ </span>
220
+ </template>
221
+ <template v-else>
222
+ {{ formatFieldValue(item.value, item, data) || '--' }}
223
+ </template>
224
+ <a-button
225
+ v-if="item.canEdit"
226
+ type="link"
227
+ size="small"
228
+ @click="handleEditField(item)"
229
+ style="padding: 0 4px"
230
+ >
231
+ <a-icon type="edit"/>
232
+ </a-button>
233
+ </span>
234
+ </a-descriptions-item>
235
+ </a-descriptions>
236
+ </template>
237
+ </div>
238
+ </a-col>
239
+ </template>
240
+ </a-row>
241
+ </template>
242
+ <script>
243
+ import {mapState} from 'vuex'
244
+ import {getRealKeyData} from '@vue2-client/utils/formatter'
245
+ import {getConfigByName} from '@vue2-client/services/api/common'
246
+ import {executeStrFunctionByContext} from '@vue2-client/utils/runEvalFunction'
247
+ import XAddNativeForm from '@vue2-client/base-client/components/common/XAddNativeForm/XAddNativeForm.vue'
248
+
249
+ export default {
250
+ name: 'XDescriptionsGroup',
251
+ components: {
252
+ XAddNativeForm
253
+ },
254
+ props: {
255
+ // 内容
256
+ data: {
257
+ type: Object,
258
+ required: true,
259
+ default: undefined
260
+ },
261
+ configName: {
262
+ type: String,
263
+ required: true,
264
+ default: ''
265
+ },
266
+ // 配置所属命名空间
267
+ serviceName: {
268
+ type: String,
269
+ default: process.env.VUE_APP_SYSTEM_NAME
270
+ }
271
+ },
272
+ mounted() {
273
+ },
274
+ beforeDestroy() {
275
+ const formGroupContext = this.$refs.formGroupContext?.$el
276
+ if (formGroupContext && formGroupContext.removeEventListener) {
277
+ formGroupContext.removeEventListener('scroll', this.onScroll)
278
+ }
279
+ },
280
+ created() {
281
+ this.initConfig()
282
+ },
283
+ data() {
284
+ return {
285
+ // 加载状态
286
+ loading: false,
287
+ loadError: false,
288
+ realData: [],
289
+ // 获取到的配置组
290
+ groups: {},
291
+ activeTab: 0,
292
+ // 从配置中获取的值
293
+ column: 3, // 默认值
294
+ getRealData: false, // 默认值
295
+ tabMode: 'left' // 默认值
296
+ }
297
+ },
298
+ computed: {
299
+ ...mapState('setting', {isMobile: 'isMobile'})
300
+ },
301
+ methods: {
302
+ initConfig() {
303
+ this.loading = true
304
+ if (this.configName) {
305
+ this.getConfig()
306
+ // 只在 left 模式启用滚动监听
307
+ if (this.tabMode === 'left') {
308
+ this.$nextTick(() => {
309
+ const formGroupContext = this.$refs.descriptionsGroupContext?.$el
310
+ if (formGroupContext && formGroupContext.addEventListener) {
311
+ formGroupContext.addEventListener('scroll', this.onScroll)
312
+ }
313
+ })
314
+ }
315
+ } else {
316
+ this.loading = false
317
+ this.loadError = true
318
+ }
319
+ },
320
+ scrollToGroup(index) {
321
+ const groupElement = this.$refs[`descriptions-item-${index}`][0]
322
+ if (groupElement) {
323
+ groupElement.scrollIntoView({behavior: 'smooth'})
324
+ }
325
+ },
326
+ getConfig() {
327
+ getConfigByName(this.configName, this.serviceName, res => {
328
+ if (res.groups) {
329
+ // 从配置中获取 column、getRealData 和 tabMode
330
+ this.column = res.column !== undefined ? res.column : 3
331
+ this.getRealData = res.getRealData !== undefined ? res.getRealData : false
332
+ this.tabMode = res.tabMode !== undefined ? res.tabMode : 'left'
333
+
334
+ // 解析分组配置
335
+ const groups = this.parseGroupsConfig(res.groups)
336
+ this.groups = groups
337
+ this.realData = groups
338
+ .map(group => {
339
+ const dataItem = {title: group.name}
340
+
341
+ if (group.type === 'slot') {
342
+ // 插槽分组:由父组件通过具名插槽传入内容
343
+ return {
344
+ title: group.name,
345
+ slot: true,
346
+ slotName: group.slotName || group.name
347
+ }
348
+ }
349
+
350
+ if (group.type === 'array') {
351
+ // 处理数组类型数据
352
+ const arrayData = this.data[group.key] || []
353
+ dataItem.column = arrayData
354
+ .map((item, index) => ({
355
+ title: `${group.name} ${index + 1}`,
356
+ column: group.fields.map(field => ({
357
+ key: field.name,
358
+ value: item[field.key]
359
+ }))
360
+ }))
361
+ .filter(Boolean)
362
+ } else {
363
+ dataItem.column = group.fields.map(field => ({
364
+ key: field.name,
365
+ dataKey: field.key,
366
+ value: this.getRealKeyData(this.data, field.key),
367
+ span: field.span,
368
+ // 存储字段函数配置
369
+ showIf: field.showIf,
370
+ styleFunc: field.styleFunc,
371
+ formatFunc: field.formatFunc,
372
+ requireFunc: field.requireFunc,
373
+ canEdit: field.canEdit === true,
374
+ isLink: field.isLink === true
375
+ }))
376
+ }
377
+
378
+ return dataItem.column.length > 0 ? dataItem : null
379
+ })
380
+ .filter(Boolean)
381
+ }
382
+ })
383
+ },
384
+ /**
385
+ * 解析分组配置数据,支持新旧两种配置格式
386
+ * @param {Object|Array} groupsConfig - 分组配置对象或数组
387
+ * @returns {Array} 解析后的分组数组
388
+ *
389
+ * 配置格式说明:
390
+ *
391
+ * 旧模式 - 对象格式 (Object):
392
+ * {
393
+ * "表具信息": {
394
+ * "f_gasbrand": "气表品牌", // 简单字段配置
395
+ * "f_meter_type": "表具类型"
396
+ * },
397
+ * "设备信息": {
398
+ * "type": "array", // 数组类型标识
399
+ * "key": "devices", // type为array时必填:数据源key
400
+ * "value": { // type为array时的字段配置
401
+ * "f_devices_num": "设备数量",
402
+ * "f_devices_type": "设备类型"
403
+ * }
404
+ * },
405
+ * "客户信息": {
406
+ * "f_user_code": { // 字段可配置为对象
407
+ * "name": "用户编号",
408
+ * "span": 2 // 可选:列跨度
409
+ * },
410
+ * "f_user_name": "用户姓名" // 简单字段配置
411
+ * }
412
+ * }
413
+ *
414
+ * 新模式 - 数组格式 (Array):
415
+ * [{
416
+ * "name": "客户信息",
417
+ * "fields": [
418
+ * {
419
+ * "name": "用户编号",
420
+ * "key": "f_user_code",
421
+ * "span": 3
422
+ * }
423
+ * ]
424
+ * }, {
425
+ * "name": "设备信息",
426
+ * "type": "array", // 数组类型标识
427
+ * "key": "devices", // type为array时必填:数据源key
428
+ * "fields": [
429
+ * {
430
+ * "name": "设备数量",
431
+ * "key": "f_devices_num"
432
+ * },
433
+ * {
434
+ * "name": "设备类型",
435
+ * "key": "f_devices_type"
436
+ * }
437
+ * ]
438
+ * }]
439
+ */
440
+ parseGroupsConfig(groupsConfig) {
441
+ // 如果已经是数组格式,直接返回
442
+ if (Array.isArray(groupsConfig)) {
443
+ return groupsConfig
444
+ }
445
+
446
+ // 解析对象格式配置
447
+ return Object.entries(groupsConfig).map(([groupName, groupConfig]) => {
448
+ // 处理插槽类型的配置
449
+ if (groupConfig.type === 'slot') {
450
+ return {
451
+ name: groupName,
452
+ type: 'slot',
453
+ slotName: groupConfig.slotName || groupName
454
+ }
455
+ }
456
+
457
+ // 处理数组类型的配置
458
+ if (groupConfig.type === 'array') {
459
+ return {
460
+ name: groupName,
461
+ type: 'array',
462
+ key: groupConfig.key,
463
+ fields: Object.entries(groupConfig.value || {}).map(([k, v]) => ({
464
+ key: k,
465
+ name: v
466
+ }))
467
+ }
468
+ }
469
+
470
+ // 处理普通配置(支持新旧格式)
471
+ return {
472
+ name: groupName,
473
+ fields: Object.entries(groupConfig).map(([fieldKey, fieldValue]) => ({
474
+ key: fieldKey,
475
+ name: typeof fieldValue === 'string' ? fieldValue : fieldValue.name,
476
+ showIf: typeof fieldValue === 'object' ? fieldValue.showIf : undefined,
477
+ styleFunc: typeof fieldValue === 'object' ? fieldValue.styleFunc : undefined,
478
+ formatFunc: typeof fieldValue === 'object' ? fieldValue.formatFunc : undefined,
479
+ requireFunc: typeof fieldValue === 'object' ? fieldValue.requireFunc : undefined,
480
+ canEdit: typeof fieldValue === 'object' ? fieldValue.canEdit : undefined,
481
+ // 新增超链接配置
482
+ isLink: typeof fieldValue === 'object' ? fieldValue.isLink : undefined,
483
+ ...(typeof fieldValue === 'object' ? fieldValue : {})
484
+ }))
485
+ }
486
+ })
487
+ },
488
+ // 文字格式化
489
+ formatText(value) {
490
+ return value ?? '--'
491
+ },
492
+ // 执行字段展示函数
493
+ shouldShowField(field, data) {
494
+ if (!field.showIf) return true
495
+ try {
496
+ return executeStrFunctionByContext(this, field.showIf, [data, field.key])
497
+ } catch (error) {
498
+ console.warn('showIf 函数执行错误:', error)
499
+ return true
500
+ }
501
+ },
502
+ // 获取字段样式
503
+ getFieldStyle(field, data) {
504
+ if (!field.styleFunc) return {}
505
+ try {
506
+ return executeStrFunctionByContext(this, field.styleFunc, [data, field.key])
507
+ } catch (error) {
508
+ console.warn('styleFunc 函数执行错误:', error)
509
+ return {}
510
+ }
511
+ },
512
+ // 格式化字段值
513
+ formatFieldValue(value, field, data) {
514
+ if (!field.formatFunc) return value
515
+ try {
516
+ return executeStrFunctionByContext(this, field.formatFunc, [data, field.key])
517
+ } catch (error) {
518
+ console.warn('formatFunc 函数执行错误:', error)
519
+ return value
520
+ }
521
+ },
522
+ // 判断字段是否必填(显示红色星号)
523
+ isFieldRequired(field, data) {
524
+ if (!field.requireFunc) return false
525
+ try {
526
+ return executeStrFunctionByContext(this, field.requireFunc, [data, field.key])
527
+ } catch (error) {
528
+ console.warn('requireFunc 函数执行错误:', error)
529
+ return false
530
+ }
531
+ },
532
+ getRealKeyData(data, key) {
533
+ if (this.getRealData) {
534
+ return getRealKeyData(data)[key] || ''
535
+ } else {
536
+ return this.data[key] || ''
537
+ }
538
+ },
539
+ // 触发编辑事件,由外部组件实现具体编辑逻辑
540
+ handleEditField(field) {
541
+ this.$emit('edit-field', {
542
+ field,
543
+ value: field.value,
544
+ data: this.data
545
+ })
546
+ },
547
+ // 处理超链接点击
548
+ handleLinkClick(field, data, event) {
549
+ event.stopPropagation()
550
+ // 向父组件抛出事件
551
+ this.$emit('link-click', {
552
+ field,
553
+ value: field.value,
554
+ data: this.data
555
+ })
556
+ },
557
+ onScroll() {
558
+ // 只在 left 模式启用滚动联动
559
+ if (this.tabMode !== 'left') return
560
+
561
+ const formGroupContext = this.$refs.descriptionsGroupContext.$el
562
+ let groupItems = []
563
+ if (formGroupContext && formGroupContext.querySelectorAll) {
564
+ groupItems = formGroupContext.querySelectorAll('.descriptions-item')
565
+ }
566
+ let activeIndex = this.activeTab
567
+
568
+ groupItems.forEach((item, index) => {
569
+ const rect = item.getBoundingClientRect()
570
+ if (rect.top <= 205 && rect.bottom > 200) {
571
+ activeIndex = index
572
+ }
573
+ })
574
+ this.activeTab = activeIndex
575
+ }
576
+ },
577
+ watch: {
578
+ content: {
579
+ handler() {
580
+ this.initConfig()
581
+ }
582
+ },
583
+ configName: {
584
+ handler() {
585
+ this.initConfig()
586
+ }
587
+ },
588
+ serviceName: {
589
+ handler() {
590
+ this.initConfig()
591
+ }
592
+ }
593
+ }
594
+ }
595
+ </script>
596
+
597
+ <style lang="less" scoped>
598
+ #XDescriptionGroup {
599
+ height: 100%;
600
+
601
+ :deep(.ant-descriptions-title) {
602
+ color: @primary-color;
603
+ }
604
+
605
+ :deep(.ant-descriptions-item-label) {
606
+ color: #6b7280; // 统一标签颜色
607
+ font-weight: 500;
608
+ }
609
+
610
+ :deep(.ant-descriptions-item-content) {
611
+ color: #111827; // 统一内容颜色
612
+ font-weight: 400;
613
+ }
614
+
615
+ // 移除最后一行的 padding-bottom
616
+ :deep(.ant-descriptions-view) {
617
+ .ant-descriptions-row:last-child {
618
+ > th,
619
+ > td {
620
+ padding-bottom: 0;
621
+ }
622
+ }
623
+ }
624
+
625
+ .descriptions-item {
626
+ margin-bottom: 8px;
627
+ }
628
+
629
+ .descriptions-array-item {
630
+ padding-left: 1rem;
631
+ }
632
+
633
+ .descriptionsGroupContext {
634
+ height: 100%;
635
+ overflow-y: scroll;
636
+ }
637
+
638
+ .link-text {
639
+ color: @primary-color;
640
+ text-decoration: none;
641
+ cursor: pointer;
642
+ outline: none;
643
+
644
+ &:hover {
645
+ text-decoration: underline;
646
+ }
647
+
648
+ &:focus {
649
+ text-decoration: underline;
650
+ box-shadow: 0 0 0 2px fade(@primary-color, 20%);
651
+ }
652
+ }
653
+
654
+ // top 模式样式
655
+ .tab-content-scroll {
656
+ overflow-y: auto;
657
+ padding: 8px 10px;
658
+
659
+ :deep(.ant-descriptions-item) {
660
+ padding-bottom: 12px;
661
+ }
662
+ }
663
+
664
+ // none 模式:移除分组间距
665
+ &:has(.tabMode-none) {
666
+ .descriptions-item {
667
+ margin-bottom: 4px;
668
+ }
669
+ }
670
+ }
671
+ </style>