vue2-client 1.20.76 → 1.20.78

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