lambo-design-bi 0.0.253 → 0.0.255

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.
@@ -30,6 +30,7 @@ import { deepCopy } from "@lambo-design/shared/utils/assist"
30
30
  import { getWidgetName } from '../util/widget-map'
31
31
  import { setRouteCfg2Search, getInitSearchRes, getSearchFormRes } from './util/common'
32
32
  import { EventBus, EVENT_TYPES } from '../util/event-bus'
33
+ import { clearDimensionState } from '../util/dimension-state'
33
34
 
34
35
  export default {
35
36
  name: 'BiChildPageTm',
@@ -116,6 +117,9 @@ export default {
116
117
  }
117
118
  },
118
119
  created() {
120
+ // 每页开始先清空维度列显隐单例,避免从上一个维度报表带入残留;
121
+ // 本页若有维度查询表单,其 created 会重新写入
122
+ clearDimensionState()
119
123
  this.getConfig()
120
124
  },
121
125
  mounted() {
@@ -135,16 +139,20 @@ export default {
135
139
  },
136
140
  computed: {
137
141
  shouldShowLamboIndicator() {
138
- if (!this.childPageWidget ||
139
- !this.childPageWidget.search ||
140
- !this.childPageWidget.search.widgets) {
142
+ if (!this.childPageWidget || !this.childPageWidget.search) {
141
143
  return false
142
144
  }
143
- // 过滤出searchType不等于'A0'的widgets
144
- const filteredWidgets = this.childPageWidget.search.widgets.filter(item =>
145
+ // 过滤出searchType不等于'A0'的普通查询条件
146
+ const normalWidgets = (this.childPageWidget.search.widgets || []).filter(item =>
145
147
  item && item.options && item.options.searchType !== 'A0'
146
148
  )
147
- return filteredWidgets.length > 0
149
+ if (normalWidgets.length > 0) {
150
+ return true
151
+ }
152
+ // 只有维度条件时也展示(v1只取dims[0])
153
+ const dims = (this.childPageWidget.search.options && this.childPageWidget.search.options.dims) || []
154
+ return dims.length > 0 && (((dims[0].widgets || []).length > 0) ||
155
+ ((dims[0].options && dims[0].options.dataList) || []).length > 0)
148
156
  }
149
157
  },
150
158
  beforeDestroy() {
@@ -27,6 +27,7 @@ import { deepCopy } from "@lambo-design/shared/utils/assist"
27
27
  import { getWidgetName } from '../util/widget-map'
28
28
  import { setRouteCfg2Search, getInitSearchRes, getSearchFormRes } from './util/common'
29
29
  import { EventBus, EVENT_TYPES } from '../util/event-bus'
30
+ import { clearDimensionState } from '../util/dimension-state'
30
31
 
31
32
  export default {
32
33
  name: 'BiIndexPageTm',
@@ -110,6 +111,9 @@ export default {
110
111
  }
111
112
  },
112
113
  created() {
114
+ // 每页开始先清空维度列显隐单例,避免从上一个维度报表带入残留;
115
+ // 本页若有维度查询表单,其 created 会重新写入
116
+ clearDimensionState()
113
117
  this.getConfig()
114
118
  },
115
119
  mounted() {
@@ -119,16 +123,20 @@ export default {
119
123
  },
120
124
  computed: {
121
125
  shouldShowLamboIndicator() {
122
- if (!this.pageWidget ||
123
- !this.pageWidget.search ||
124
- !this.pageWidget.search.widgets) {
126
+ if (!this.pageWidget || !this.pageWidget.search) {
125
127
  return false
126
128
  }
127
- // 过滤出searchType不等于'A0'的widgets
128
- const filteredWidgets = this.pageWidget.search.widgets.filter(item =>
129
+ // 过滤出searchType不等于'A0'的普通查询条件
130
+ const normalWidgets = (this.pageWidget.search.widgets || []).filter(item =>
129
131
  item && item.options && item.options.searchType !== 'A0'
130
132
  )
131
- return filteredWidgets.length > 0
133
+ if (normalWidgets.length > 0) {
134
+ return true
135
+ }
136
+ // 只有维度条件时也展示(v1只取dims[0])
137
+ const dims = (this.pageWidget.search.options && this.pageWidget.search.options.dims) || []
138
+ return dims.length > 0 && (((dims[0].widgets || []).length > 0) ||
139
+ ((dims[0].options && dims[0].options.dataList) || []).length > 0)
132
140
  }
133
141
  },
134
142
  beforeDestroy() {
@@ -1,12 +1,39 @@
1
1
  <template>
2
- <div class="bi-search-form" v-if="widget.widgets && widget.widgets.length > 0">
2
+ <div class="bi-search-form" v-if="hasSearchContent">
3
3
  <LamboQueryFilter :default-rows="2" :label-width="150" :span="8" :showSearchIcon="true" :autocomplete="'on'" :hideResetButton="true" @do-search="doSearch" @on-reset="onReset">
4
- <div v-for="(item, index) in widget.widgets" :key="index">
5
- <FormItem v-if="item.options.searchType !== 'A0'" :label="item.name">
4
+ <!-- 普通查询条件 -->
5
+ <div v-for="(item, index) in normalWidgets" :key="'search-' + index">
6
+ <FormItem :label="item.name">
6
7
  <BiSearchItem :widget="item"></BiSearchItem>
7
8
  </FormItem>
8
9
  </div>
9
10
  </LamboQueryFilter>
11
+ <!-- 维度区固定在查询按钮下方:标签 + 明细下拉;明细默认收起,开关追加在标签后方 -->
12
+ <Form v-if="dimTagOptions.length > 0" class="bi-dim-form" label-position="right" :label-width="150">
13
+ <FormItem :label="dimensionLabel">
14
+ <div class="bi-dim-tags-row">
15
+ <div class="bi-dim-tags-row__tags">
16
+ <BiTagSelect v-model="dimSelected" :options="dimTagOptions" @on-change="onDimTagsChange"></BiTagSelect>
17
+ </div>
18
+ <Button type="text" class="bi-dim-detail-toggle" :title="dimDetailsCollapsed ? '展开维度明细' : '收起维度明细'"
19
+ @click="dimDetailsCollapsed = !dimDetailsCollapsed">
20
+ <Icon :type="'ios-arrow-down'" :size="16"
21
+ :class="{ 'bi-dim-detail-toggle__icon--open': !dimDetailsCollapsed }" />
22
+ </Button>
23
+ </div>
24
+ </FormItem>
25
+ <div v-show="!dimDetailsCollapsed" class="bi-dim-details">
26
+ <Row>
27
+ <!-- 栅格与普通查询条件一致:固定 span=8(QueryFilter 传 span=8),一行三个 -->
28
+ <Col v-for="(dim, index) in dimensionWidgets" :key="'dimension-' + (dim.id || index)" :span="8">
29
+ <!-- 下拉框始终展示,未选标签/未选明细时为空态,只控制列显隐,不上送查询参数 -->
30
+ <FormItem :label="dim.name">
31
+ <BiSearchItem :widget="dim" :data-list="getDimDataList(dim)"></BiSearchItem>
32
+ </FormItem>
33
+ </Col>
34
+ </Row>
35
+ </div>
36
+ </Form>
10
37
  </div>
11
38
  </template>
12
39
  <script>
@@ -14,6 +41,7 @@ import LamboQueryFilter from "@lambo-design/query-filter"
14
41
  import { deepCopy } from "@lambo-design/shared/utils/assist"
15
42
  import moment from 'moment'
16
43
  import BiSearchItem from "./search-item.vue"
44
+ import BiTagSelect from "./tag-select.vue"
17
45
  import {
18
46
  SEARCH_DICT,
19
47
  DATE_RANGE_PFIX,
@@ -21,10 +49,12 @@ import {
21
49
  DEFAULT_YEAR_FORMAT,
22
50
  DEFAULT_MONTH_FORMAT
23
51
  } from '../../../util/constants'
52
+ import { EventBus, EVENT_TYPES } from '../util/event-bus'
53
+ import { setDimensionBlock, clearDimensionState } from '../util/dimension-state'
24
54
 
25
55
  export default {
26
56
  name: 'BiSearchForm',
27
- components: { LamboQueryFilter, BiSearchItem },
57
+ components: { LamboQueryFilter, BiSearchItem, BiTagSelect },
28
58
  props: {
29
59
  widget: {
30
60
  type: Object,
@@ -33,49 +63,270 @@ export default {
33
63
  },
34
64
  data() {
35
65
  return {
36
- cacheWidget: {}
66
+ cacheWidget: {},
67
+ // 维度明细默认收起,开关在维度标签后方
68
+ dimDetailsCollapsed: true
69
+ }
70
+ },
71
+ computed: {
72
+ // 普通查询条件(隐形查询条件A0不展示,但仍参与传参)
73
+ normalWidgets() {
74
+ let list = this.widget.widgets || []
75
+ return list.filter(item => item && item.options && item.options.searchType !== SEARCH_DICT.NOT_SHOW_INPUT.id)
76
+ },
77
+ // 维度区块节点:后端输出在 search.options.dims,v1 只支持一个维度块
78
+ dimensionBlock() {
79
+ let dims = this.widget.options && this.widget.options.dims
80
+ return Array.isArray(dims) && dims.length > 0 ? dims[0] : null
81
+ },
82
+ dimensionWidgets() {
83
+ return (this.dimensionBlock && this.dimensionBlock.widgets) || []
84
+ },
85
+ dimensionLabel() {
86
+ return (this.dimensionBlock && this.dimensionBlock.name) || '统计维度'
87
+ },
88
+ hasSearchContent() {
89
+ return this.normalWidgets.length > 0 || this.dimTagOptions.length > 0
90
+ },
91
+ // 维度标签的可选项:取维度容器自身的 dataList(K 与明细的 pDimKey 对应)
92
+ dimTagOptions() {
93
+ let dataList = (this.dimensionBlock && this.dimensionBlock.options && this.dimensionBlock.options.dataList) || []
94
+ return dataList.map(item => ({ K: item.K, V: item.V }))
95
+ },
96
+ // 已选维度标识,代理到 dimension.options.val
97
+ dimSelected: {
98
+ get() {
99
+ let val = this.dimensionBlock && this.dimensionBlock.options && this.dimensionBlock.options.val
100
+ return Array.isArray(val) ? val : []
101
+ },
102
+ set(val) {
103
+ if (this.dimensionBlock && this.dimensionBlock.options) {
104
+ this.$set(this.dimensionBlock.options, 'val', val)
105
+ }
106
+ }
107
+ },
108
+ },
109
+ watch: {
110
+ // 维度标签/明细值/级联变化后,修剪失效明细值并广播列显隐
111
+ 'widget.options.dims': {
112
+ deep: true,
113
+ handler() {
114
+ if (!this.dimensionBlock) {
115
+ clearDimensionState()
116
+ EventBus.$emit(EVENT_TYPES.DIMENSION_COLUMNS_CHANGED, {})
117
+ return
118
+ }
119
+ this.syncCheckedTagsFromDetails()
120
+ this.syncEmptyDetailTags()
121
+ this.pruneInvalidDimVals()
122
+ this.scheduleDimPublish()
123
+ }
37
124
  }
38
125
  },
39
126
  methods: {
127
+ // 维度标识:明细用 pDimKey 关联容器标签(容器 dataList 的 K),缺退化 qKey/id
128
+ getDimKey(item) {
129
+ return (item.options && (item.options.pDimKey || item.options.qKey)) || item.id
130
+ },
131
+ // 多选明细默认全选(取 dataList 全部 K;级联过滤仍在下拉数据与修剪环节生效)
132
+ fillDimDetailAll(dim) {
133
+ if (!dim.options || dim.options.searchType !== SEARCH_DICT.MUlTI_SELECT.id) {
134
+ return
135
+ }
136
+ let allKs = (dim.options.dataList || [])
137
+ .map(row => row && row.K)
138
+ .filter(k => k !== undefined && k !== null && k !== '')
139
+ this.$set(dim.options, 'val', allKs)
140
+ this.$set(dim.options, 'valName', '')
141
+ },
142
+ onDimTagsChange(selected) {
143
+ // 对比变更前的标签:取消勾选清空明细值;新勾选的多选明细默认全选
144
+ let prev = this._prevDimTags || []
145
+ this.dimensionWidgets.forEach(item => {
146
+ if (!item.options) {
147
+ return
148
+ }
149
+ let dimKey = this.getDimKey(item)
150
+ if (selected.indexOf(dimKey) === -1) {
151
+ let resetVal = Array.isArray(item.options.val) ? [] : ''
152
+ this.$set(item.options, 'val', resetVal)
153
+ this.$set(item.options, 'valName', '')
154
+ } else if (prev.indexOf(dimKey) === -1 && item.options.searchType === SEARCH_DICT.MUlTI_SELECT.id) {
155
+ this.fillDimDetailAll(item)
156
+ }
157
+ })
158
+ this._prevDimTags = selected.slice()
159
+ },
160
+ // 联动:明细下拉直接选择了值时,自动勾选对应维度标签(与清空明细取消标签对称)
161
+ syncCheckedTagsFromDetails() {
162
+ let selected = this.dimSelected.slice()
163
+ let changed = false
164
+ this.dimensionWidgets.forEach(item => {
165
+ if (!item.options) {
166
+ return
167
+ }
168
+ let dimKey = this.getDimKey(item)
169
+ if (selected.indexOf(dimKey) > -1) {
170
+ return
171
+ }
172
+ let val = item.options.val
173
+ let hasVal = Array.isArray(val) ? val.length > 0 : (val !== '' && val !== null && val !== undefined)
174
+ if (hasVal) {
175
+ selected.push(dimKey)
176
+ changed = true
177
+ }
178
+ })
179
+ if (changed) {
180
+ this.dimSelected = selected
181
+ this._prevDimTags = selected.slice()
182
+ }
183
+ },
184
+ // 联动:标签选中但多选明细被清空(一个都不选)时,自动取消该标签
185
+ syncEmptyDetailTags() {
186
+ let selected = this.dimSelected.slice()
187
+ let changed = false
188
+ this.dimensionWidgets.forEach(item => {
189
+ if (!item.options || item.options.searchType !== SEARCH_DICT.MUlTI_SELECT.id) {
190
+ return
191
+ }
192
+ let dimKey = this.getDimKey(item)
193
+ if (selected.indexOf(dimKey) === -1) {
194
+ return
195
+ }
196
+ if (Array.isArray(item.options.val) && item.options.val.length === 0) {
197
+ selected = selected.filter(k => k !== dimKey)
198
+ changed = true
199
+ }
200
+ })
201
+ if (changed) {
202
+ this.dimSelected = selected
203
+ this._prevDimTags = selected.slice()
204
+ }
205
+ },
206
+ /**
207
+ * 获取维度明细的下拉数据(级联时按父维度选中值静态过滤 dataList)
208
+ * cascade: { parentQKey: 父维度qKey, refKey: 本维度数据中关联父维度值的字段(缺省取parentQKey) }
209
+ * 父维度未选标签/未选明细值时不加约束;后续若改为接口级联可在此处扩展
210
+ */
211
+ getDimDataList(dim, visiting) {
212
+ let rawList = (dim.options && dim.options.dataList) || []
213
+ let cascade = dim.options && dim.options.cascade
214
+ if (!cascade || !cascade.parentQKey) {
215
+ return rawList
216
+ }
217
+ visiting = visiting || []
218
+ let dimKey = this.getDimKey(dim)
219
+ if (visiting.indexOf(dimKey) > -1) {
220
+ return rawList // 级联环保护
221
+ }
222
+ let parent = this.dimensionWidgets.find(item => this.getDimKey(item) === cascade.parentQKey)
223
+ if (!parent || !parent.options) {
224
+ return rawList
225
+ }
226
+ // 父维度标签未选中时不加约束
227
+ if (this.dimSelected.indexOf(cascade.parentQKey) === -1) {
228
+ return rawList
229
+ }
230
+ let parentVals = parent.options.val
231
+ if (!Array.isArray(parentVals) || parentVals.length === 0) {
232
+ return rawList // 父维度没有选中明细值,不加约束
233
+ }
234
+ // 父级合法值需同时满足父级自身的级联过滤结果
235
+ let legalParentKs = this.getDimDataList(parent, visiting.concat(dimKey)).map(row => row && row.K)
236
+ let allowVals = parentVals.filter(v => legalParentKs.indexOf(v) > -1)
237
+ if (allowVals.length === 0) {
238
+ return rawList
239
+ }
240
+ let refKey = cascade.refKey || cascade.parentQKey
241
+ return rawList.filter(row => row && allowVals.indexOf(row[refKey]) > -1)
242
+ },
243
+ // 修剪维度明细中已不在有效下拉列表内的值(仅处理下拉类型10/20)
244
+ pruneInvalidDimVals() {
245
+ this.dimensionWidgets.forEach(item => {
246
+ if (!item.options) {
247
+ return
248
+ }
249
+ let searchType = item.options.searchType
250
+ if (searchType !== SEARCH_DICT.SINGLE_SELECT.id && searchType !== SEARCH_DICT.MUlTI_SELECT.id) {
251
+ return
252
+ }
253
+ let legalKs = this.getDimDataList(item).map(row => row && row.K)
254
+ let val = item.options.val
255
+ if (Array.isArray(val)) {
256
+ if (val.length === 0) {
257
+ return
258
+ }
259
+ let kept = val.filter(v => legalKs.indexOf(v) > -1)
260
+ if (kept.length !== val.length) {
261
+ this.$set(item.options, 'val', kept)
262
+ }
263
+ } else if (val !== '' && val !== null && val !== undefined) {
264
+ if (legalKs.indexOf(val) === -1) {
265
+ this.$set(item.options, 'val', '')
266
+ }
267
+ }
268
+ })
269
+ },
270
+ // 维度变化只影响列显隐,合并到 nextTick 广播一次,避免修剪引发的重复触发
271
+ scheduleDimPublish() {
272
+ if (this._dimPublishScheduled) {
273
+ return
274
+ }
275
+ this._dimPublishScheduled = true
276
+ this.$nextTick(() => {
277
+ this._dimPublishScheduled = false
278
+ this.publishDimensionState()
279
+ })
280
+ },
281
+ publishDimensionState() {
282
+ let visibility = setDimensionBlock(this.dimensionBlock)
283
+ console.log('publish visibility:', JSON.parse(JSON.stringify(visibility || {})))
284
+ EventBus.$emit(EVENT_TYPES.DIMENSION_COLUMNS_CHANGED, visibility)
285
+ },
286
+ // 按单个查询项配置组装查询参数(仅普通查询项;维度不上送)
287
+ appendItemParams(params, item) {
288
+ if (item.options && Object.prototype.hasOwnProperty.call(item.options, "qKey")) {
289
+ let qKey = item.options.qKey
290
+ if (Object.prototype.hasOwnProperty.call(item.options, "val") && item.options.val) {
291
+ let val = item.options.val
292
+ if (Array.isArray(val)) {
293
+ if (item.options.searchType === SEARCH_DICT.MUlTI_DATE.id) {
294
+ if (val && Array.isArray(val) && val.length === 2 && val[0] && val[1]) {
295
+ params[qKey + DATE_RANGE_PFIX.START] = moment(val[0]).format(DEFAULT_DATE_FORMAT)
296
+ params[qKey + DATE_RANGE_PFIX.END] = moment(val[1]).format(DEFAULT_DATE_FORMAT)
297
+ }
298
+ } else {
299
+ params[qKey] = val.join()
300
+ }
301
+ } else {
302
+ if (item.options.searchType === SEARCH_DICT.SINGLE_DATE.id) {
303
+ params[qKey] = moment(val).format(DEFAULT_DATE_FORMAT)
304
+ } else if (item.options.searchType === SEARCH_DICT.SINGLE_YEAR.id) {
305
+ params[qKey] = moment(val).format(DEFAULT_YEAR_FORMAT)
306
+ } else if (item.options.searchType === SEARCH_DICT.SINGLE_MONTH.id) {
307
+ params[qKey] = moment(val).format(DEFAULT_MONTH_FORMAT)
308
+ }
309
+ else {
310
+ params[qKey] = val
311
+ }
312
+ }
313
+ }
314
+ // TODO:这里传null还是不传后面看下
315
+ // else {
316
+ // params[qKey] = null
317
+ // }
318
+ }
319
+ },
40
320
  doSearch() {
41
321
  let params = {}
42
322
  let searchList = this.widget.widgets
43
323
  console.log("search-from doSearch this.widget.widgets:", searchList)
44
324
  if (searchList) {
45
325
  searchList.map(item => {
46
- if (item.options && Object.prototype.hasOwnProperty.call(item.options, "qKey")) {
47
- let qKey = item.options.qKey
48
- if (Object.prototype.hasOwnProperty.call(item.options, "val") && item.options.val) {
49
- let val = item.options.val
50
- if (Array.isArray(val)) {
51
- if (item.options.searchType === SEARCH_DICT.MUlTI_DATE.id) {
52
- if (val && Array.isArray(val) && val.length === 2 && val[0] && val[1]) {
53
- params[qKey + DATE_RANGE_PFIX.START] = moment(val[0]).format(DEFAULT_DATE_FORMAT)
54
- params[qKey + DATE_RANGE_PFIX.END] = moment(val[1]).format(DEFAULT_DATE_FORMAT)
55
- }
56
- } else {
57
- params[qKey] = val.join()
58
- }
59
- } else {
60
- if (item.options.searchType === SEARCH_DICT.SINGLE_DATE.id) {
61
- params[qKey] = moment(val).format(DEFAULT_DATE_FORMAT)
62
- } else if (item.options.searchType === SEARCH_DICT.SINGLE_YEAR.id) {
63
- params[qKey] = moment(val).format(DEFAULT_YEAR_FORMAT)
64
- } else if (item.options.searchType === SEARCH_DICT.SINGLE_MONTH.id) {
65
- params[qKey] = moment(val).format(DEFAULT_MONTH_FORMAT)
66
- }
67
- else {
68
- params[qKey] = val
69
- }
70
- }
71
- }
72
- // TODO:这里传null还是不传后面看下
73
- // else {
74
- // params[qKey] = null
75
- // }
76
- }
326
+ this.appendItemParams(params, item)
77
327
  })
78
328
  }
329
+ // 维度(标签/明细)只控制表格列显隐,不进查询参数
79
330
  console.log("search-from doSearch params:", params)
80
331
  this.$emit("on-search", params)
81
332
  },
@@ -87,11 +338,49 @@ export default {
87
338
  this.$set(this.widget, 'widgets', cacheSearchList)
88
339
  }
89
340
  }
341
+ // 维度区块一并恢复缓存(含标签选中和明细值),watcher 会重新广播列显隐
342
+ if (this.cacheWidget && this.cacheWidget.options && Object.prototype.hasOwnProperty.call(this.cacheWidget.options, "dims")) {
343
+ this.$set(this.widget.options, 'dims', deepCopy(this.cacheWidget.options.dims))
344
+ let resetVal = this.dimensionBlock && this.dimensionBlock.options && this.dimensionBlock.options.val
345
+ this._prevDimTags = Array.isArray(resetVal) ? resetVal.slice() : []
346
+ }
90
347
  this.doSearch()
91
348
  }
92
349
  },
93
350
  created() {
94
351
  console.log("search-form created -- widget:", this.widget)
352
+ let dims = this.widget.options && this.widget.options.dims
353
+ if (Array.isArray(dims) && dims.length > 1) {
354
+ console.warn("search-form -- v1仅支持一个维度块,忽略其余dims数量:", dims.length - 1)
355
+ }
356
+ if (this.dimensionBlock) {
357
+ if (!this.dimensionBlock.options) {
358
+ this.$set(this.dimensionBlock, 'options', {})
359
+ }
360
+ let blockOptions = this.dimensionBlock.options
361
+ if (!Array.isArray(blockOptions.val)) {
362
+ this.$set(blockOptions, 'val', blockOptions.val ? String(blockOptions.val).split(',') : [])
363
+ }
364
+ // 多选下拉的明细值规范为数组
365
+ this.dimensionWidgets.forEach(item => {
366
+ if (item.options && item.options.searchType === SEARCH_DICT.MUlTI_SELECT.id && !Array.isArray(item.options.val)) {
367
+ this.$set(item.options, 'val', item.options.val ? String(item.options.val).split(',') : [])
368
+ }
369
+ })
370
+ this._prevDimTags = (Array.isArray(blockOptions.val) ? blockOptions.val : []).slice()
371
+ // 默认已勾选标签(如 ALL_SELECT)的多选明细,空值补全为全选,保持初始全部列可见
372
+ this.dimensionWidgets.forEach(item => {
373
+ if (item.options && item.options.searchType === SEARCH_DICT.MUlTI_SELECT.id
374
+ && this._prevDimTags.indexOf(this.getDimKey(item)) > -1
375
+ && Array.isArray(item.options.val) && item.options.val.length === 0) {
376
+ this.fillDimDetailAll(item)
377
+ }
378
+ })
379
+ // 初始列显隐写入共享状态(表格创建时直接读取,无需等事件)
380
+ setDimensionBlock(this.dimensionBlock)
381
+ } else {
382
+ clearDimensionState()
383
+ }
95
384
  },
96
385
  mounted() {
97
386
  this.cacheWidget = deepCopy(this.widget)
@@ -104,5 +393,68 @@ export default {
104
393
  ::v-deep .ivu-form-item {
105
394
  margin-bottom: 8px;
106
395
  }
396
+
397
+ /* 维度区与普通查询条件同属一个 Form 视觉流:
398
+ 不额外加上间距(QueryFilter 末行 FormItem 的 margin-bottom 已提供行间距),
399
+ label 右对齐 150px、FormItem 8px 间距均沿用 iView/外层 scoped 规则 */
400
+ .bi-dim-form {
401
+ margin-top: 0;
402
+ }
403
+
404
+ /* 覆盖 label-left 的左对齐为右对齐:仅作用于维度区,不影响其他表单;
405
+ !important 防止被 iView 内置 .ivu-form-label-left 规则压过 */
406
+ .bi-dim-form ::v-deep .ivu-form-item-label {
407
+ text-align: right !important;
408
+ padding-left: 0 !important;
409
+ }
410
+
411
+ /* 标签复选框与明细折叠图标同一行:标签占满剩余空间,图标固定右侧 */
412
+ .bi-dim-tags-row {
413
+ display: flex;
414
+ align-items: center;
415
+ }
416
+
417
+ .bi-dim-tags-row__tags {
418
+ flex: 1;
419
+ min-width: 0;
420
+ }
421
+
422
+ .bi-dim-detail-toggle {
423
+ flex: none;
424
+ width: 24px;
425
+ height: 32px;
426
+ margin-left: 4px;
427
+ padding: 0;
428
+ line-height: 1;
429
+ color: #808695;
430
+ }
431
+
432
+ .bi-dim-detail-toggle:hover {
433
+ color: var(--primary-color, #2d8cf0);
434
+ }
435
+
436
+ .bi-dim-detail-toggle ::v-deep i {
437
+ line-height: 1;
438
+ transition: transform .2s ease;
439
+ }
440
+
441
+ .bi-dim-detail-toggle ::v-deep .bi-dim-detail-toggle__icon--open {
442
+ transform: rotate(180deg);
443
+ }
444
+
445
+ /* 明细区紧跟标签行,行距与普通条件行间距一致(8px 来自 FormItem margin) */
446
+ .bi-dim-details {
447
+ margin-top: 0;
448
+ }
449
+
450
+ /* element-ui 多选下拉已选标签默认 12px,归一为 iView 控件文字 14px,
451
+ 与普通查询条件视觉一致(仅作用于维度区,不改公共多选组件) */
452
+ .bi-dim-form ::v-deep .el-select__tags-text {
453
+ font-size: 14px;
454
+ }
455
+
456
+ .bi-dim-form ::v-deep .el-tag--mini {
457
+ font-size: 14px;
458
+ }
107
459
  }
108
460
  </style>
@@ -13,11 +13,11 @@
13
13
  </div>
14
14
  <div v-else-if="widget.options.searchType === searchDict.SINGLE_SELECT.id"> <!-- 单选下拉框 -->
15
15
  <Select v-model="widget.options.val" class="lambo-query-class" filterable :clearable="true" placeholder="请选择">
16
- <Option v-for="(item, index) in widget.options.dataList" :key="index" :value="item.K"> {{ item.V }}</Option>
16
+ <Option v-for="(item, index) in effectiveDataList" :key="index" :value="item.K"> {{ item.V }}</Option>
17
17
  </Select>
18
18
  </div>
19
19
  <div v-else-if="widget.options.searchType === searchDict.MUlTI_SELECT.id"> <!-- 多选下拉框 -->
20
- <LamboElMultiSelect v-model="widget.options.val" :data="widget.options.dataList"></LamboElMultiSelect>
20
+ <LamboElMultiSelect v-model="widget.options.val" :data="effectiveDataList"></LamboElMultiSelect>
21
21
  </div>
22
22
  <div v-else-if="widget.options.searchType === searchDict.SINGLE_HELP.id"> <!-- 单选帮助框 -->
23
23
  <Input class="lambo-display-none" v-model="helpVal" />
@@ -95,6 +95,18 @@ export default {
95
95
  type: Object,
96
96
  default: () => ({})
97
97
  },
98
+ /**
99
+ * 外部传入的下拉数据(如维度级联过滤后的列表),不传时使用 widget.options.dataList
100
+ */
101
+ dataList: {
102
+ type: Array,
103
+ default: null
104
+ },
105
+ },
106
+ computed: {
107
+ effectiveDataList() {
108
+ return this.dataList || (this.widget.options && this.widget.options.dataList) || []
109
+ }
98
110
  },
99
111
  data() {
100
112
  return {
@@ -0,0 +1,100 @@
1
+ <!-- eslint-disable -->
2
+ <template>
3
+ <div class="bi-tag-select">
4
+ <Checkbox class="bi-tag-select-item" :value="checkAll" :indeterminate="indeterminate"
5
+ :disabled="disabled || options.length === 0" @on-change="onCheckAllChange">全选</Checkbox>
6
+ <Checkbox v-for="item in options" :key="item.K" class="bi-tag-select-item" :value="isChecked(item.K)"
7
+ :disabled="disabled" @on-change="checked => onItemChange(item.K, checked)">{{ item.V }}</Checkbox>
8
+ </div>
9
+ </template>
10
+ <script>
11
+ /**
12
+ * 维度标签多选组件(基于 iView Checkbox,带全选/半选)
13
+ * 用法:<BiTagSelect v-model="selected" :options="[{K:'01',V:'区域'}]" />
14
+ * 说明:不使用 CheckboxGroup——其根节点是 block div 会把内容挤到下一行;
15
+ * 独立 Checkbox 作为行内元素排在同一行,沿用 FormItem 32px 行盒垂直对齐。
16
+ * 项目 viewUI(@lambo-design/core,iView 分支)无 TagSelect/Space,
17
+ * view-ui-plus 同名组件为 Vue3 实现,故在此实现
18
+ */
19
+ export default {
20
+ name: 'BiTagSelect',
21
+ model: {
22
+ prop: 'value',
23
+ event: 'input'
24
+ },
25
+ props: {
26
+ // 选中值数组(元素为 options 的 K)
27
+ value: {
28
+ type: Array,
29
+ default: () => []
30
+ },
31
+ // 可选项 [{ K, V }]
32
+ options: {
33
+ type: Array,
34
+ default: () => []
35
+ },
36
+ disabled: {
37
+ type: Boolean,
38
+ default: false
39
+ }
40
+ },
41
+ computed: {
42
+ checkAll() {
43
+ return this.options.length > 0 && this.options.every(item => this.value.indexOf(item.K) > -1)
44
+ },
45
+ // 部分选中:半选态(浏览器对 indeterminate 复选框点击后为勾选,天然触发全选)
46
+ indeterminate() {
47
+ return this.value.length > 0 && !this.checkAll
48
+ }
49
+ },
50
+ methods: {
51
+ isChecked(k) {
52
+ return this.value.indexOf(k) > -1
53
+ },
54
+ emitValue(valArr) {
55
+ this.$emit('input', valArr)
56
+ this.$emit('on-change', valArr)
57
+ },
58
+ onCheckAllChange(checked) {
59
+ if (this.disabled) {
60
+ return
61
+ }
62
+ this.emitValue(checked ? this.options.map(item => item.K) : [])
63
+ },
64
+ onItemChange(k, checked) {
65
+ if (this.disabled) {
66
+ return
67
+ }
68
+ let valArr = (this.value || []).slice()
69
+ let idx = valArr.indexOf(k)
70
+ if (checked && idx === -1) {
71
+ valArr.push(k)
72
+ } else if (!checked && idx > -1) {
73
+ valArr.splice(idx, 1)
74
+ }
75
+ this.emitValue(valArr)
76
+ }
77
+ }
78
+ }
79
+ </script>
80
+ <style scoped>
81
+ /* 不使用 flex:FormItem content 为 32px 行盒,checkbox 作为行内元素
82
+ 靠 vertical-align:middle 与表单 label/其他控件对齐(同 iView 官方表现);
83
+ 根容器仅负责超宽换行 */
84
+ /* 字号/颜色显式对齐 iView 输入框(14px/#515a6e):
85
+ 旁边的多选下拉已选项是 element-ui mini tag 12px,
86
+ 不随之缩小——14px 是表单正文标准,缩小反而与其他查询条件不一致 */
87
+ .bi-tag-select {
88
+ line-height: 32px;
89
+ font-size: 14px;
90
+ color: #515a6e;
91
+ }
92
+
93
+ .bi-tag-select ::v-deep .bi-tag-select-item {
94
+ margin-right: 12px;
95
+ vertical-align: middle;
96
+ font-size: 14px;
97
+ font-weight: normal;
98
+ color: #515a6e;
99
+ }
100
+ </style>
@@ -28,6 +28,8 @@ import LamboDocPreview from '../../../pub/doc/preview/index.vue'
28
28
  import { accAdd } from '../../../util/math-util'
29
29
  import { fileGetUrl } from '../../../bi/widget/api/config'
30
30
  import { EventBus, EVENT_TYPES } from '../util/event-bus'
31
+ import { getDimColumnVisibility } from '../util/dimension-state'
32
+ import { filterColumnsByDimension } from '../util/dimension-util'
31
33
 
32
34
  export default {
33
35
  name: 'BiPagingTable',
@@ -58,10 +60,16 @@ export default {
58
60
  boxShow: false,
59
61
  modelWidth: '78%',
60
62
  tableBorder: true,
61
- spanMap: {}
63
+ spanMap: {},
64
+ // 维度列显隐映射(列key -> 是否显示),由查询表单的维度区块驱动
65
+ dimColumnVisibility: {}
62
66
  }
63
67
  },
64
68
  methods: {
69
+ // 维度选择变化:只切换列显隐,不重新查询数据
70
+ onDimColumnsChanged(visibility) {
71
+ this.dimColumnVisibility = visibility || {}
72
+ },
65
73
  doSearch() {
66
74
  // eslint-disable-next-line
67
75
  this.widget.options.url = biConfig.dataServerContext + biConfig.dataServiceUri + this.widget.options.apiId
@@ -602,14 +610,26 @@ export default {
602
610
  created() {
603
611
  console.log("BiPagingTable -- created() -- this.widget:", this.widget)
604
612
  this.initConfig()
613
+ // 仅参与维度过滤的表格(plat_table.is_dim=1)读取初始值并监听变化(不触发数据查询)
614
+ if (this.isDimTable) {
615
+ this.dimColumnVisibility = getDimColumnVisibility()
616
+ EventBus.$on(EVENT_TYPES.DIMENSION_COLUMNS_CHANGED, this.onDimColumnsChanged)
617
+ }
605
618
  },
606
619
  mounted() {
607
620
  this.doSearch()
608
621
  },
622
+ beforeDestroy() {
623
+ EventBus.$off(EVENT_TYPES.DIMENSION_COLUMNS_CHANGED, this.onDimColumnsChanged)
624
+ },
609
625
  computed: {
610
626
  isRowClick() {
611
627
  return this.widget.options.isRowClick === 1 || this.widget.options.isRowClick === '1'
612
628
  },
629
+ // 是否参与维度列过滤(plat_table.is_dim=1 才随维度标签/明细切换列显隐)
630
+ isDimTable() {
631
+ return this.widget.options.isDim === 1 || this.widget.options.isDim === '1'
632
+ },
613
633
  hasSpanColumn() {
614
634
  // 列配置中存在 isSpan 列则启用单元格合并
615
635
  return (this.columns || []).some(column => this.isSpanColumn(column))
@@ -695,6 +715,10 @@ export default {
695
715
  this.$Message.info("表格头配置数据解析异常")
696
716
  columns = []
697
717
  }
718
+ // 维度(标签/明细)只控制列显隐:仅参与维度过滤的表格执行,非维度列始终保留
719
+ if (this.isDimTable) {
720
+ columns = filterColumnsByDimension(columns, this.dimColumnVisibility)
721
+ }
698
722
  }
699
723
  }
700
724
  return columns
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 维度列显隐状态(模块级单例)
3
+ *
4
+ * search-form 负责写入并通过 EventBus 广播;表格组件创建时先读一次初始值,
5
+ * 之后监听 DIMENSION_COLUMNS_CHANGED 事件增量更新,避免组件创建先后导致漏收事件。
6
+ */
7
+ import { buildDimColumnVisibility } from './dimension-util'
8
+
9
+ let currentVisibility = {}
10
+
11
+ export function setDimensionBlock(block) {
12
+ currentVisibility = buildDimColumnVisibility(block)
13
+ return currentVisibility
14
+ }
15
+
16
+ export function getDimColumnVisibility() {
17
+ return currentVisibility
18
+ }
19
+
20
+ export function clearDimensionState() {
21
+ currentVisibility = {}
22
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * 维度区块的列显隐规则(纯函数,便于单测)
3
+ *
4
+ * 维度只控制页面内表格列是否显示,不参与查询参数:
5
+ * - 维度标签未选中:该维度关联的列全部隐藏
6
+ * - 选中标签、明细不选:显示该维度关联的整组列(dataList 全部 K)
7
+ * - 选中标签、明细选了具体项:只显示选中项 K 对应的列
8
+ *
9
+ * dataList 每项的 K 直接对应表格 columns 中的 key,无需在列配置上额外标记
10
+ */
11
+
12
+ /**
13
+ * 归一化维度明细值为数组(10单选为字符串、20多选为数组)
14
+ */
15
+ function normalizeVals(val) {
16
+ if (Array.isArray(val)) {
17
+ return val
18
+ }
19
+ return val ? [val] : []
20
+ }
21
+
22
+ /**
23
+ * 由维度区块配置和当前选择,构建 列key -> 是否显示 的映射
24
+ * 同一列 key 归属多个维度时,任一维度允许显示则显示(OR)
25
+ *
26
+ * @param {object} block search.options.dims[0]
27
+ * @returns {object} visibility 只包含归属维度的列 key
28
+ */
29
+ export function buildDimColumnVisibility(block) {
30
+ const visibility = {}
31
+ if (!block || !block.widgets) {
32
+ return visibility
33
+ }
34
+ const selectedTags = block.options && Array.isArray(block.options.val) ? block.options.val : []
35
+
36
+ block.widgets.forEach(dim => {
37
+ if (!dim || !dim.options) {
38
+ return
39
+ }
40
+ const dimKey = dim.options.pDimKey || dim.options.qKey || dim.id
41
+ const tagSelected = selectedTags.indexOf(dimKey) > -1
42
+ const detailVals = normalizeVals(dim.options.val)
43
+ const colKeys = (dim.options.dataList || [])
44
+ .map(opt => opt && opt.K)
45
+ .filter(k => k !== undefined && k !== null && k !== '')
46
+
47
+ colKeys.forEach(k => {
48
+ const visible = tagSelected && (detailVals.length === 0 || detailVals.indexOf(k) > -1)
49
+ if (visibility[k] !== true) {
50
+ // 同一列被多个维度引用时取 OR
51
+ visibility[k] = visible
52
+ }
53
+ })
54
+ })
55
+
56
+ return visibility
57
+ }
58
+
59
+ /**
60
+ * 按维度列显隐映射递归过滤表格列:
61
+ * - 叶子列:不属于任何维度(序号、固定列、年龄分布等非维度列)始终保留;属于维度的按映射决定
62
+ * - 多级表头分组列(含 children):递归过滤子列,子列全部隐藏时整组隐藏;
63
+ * 标签未选中时该组叶子在映射中均为 false,分组自然消失
64
+ * 入参 columns 由 columns 计算属性每次重新 JSON.parse 生成,可直接改写 children
65
+ */
66
+ export function filterColumnsByDimension(columns, visibility) {
67
+ if (!columns || columns.length === 0) {
68
+ return columns || []
69
+ }
70
+ if (!visibility || Object.keys(visibility).length === 0) {
71
+ return columns
72
+ }
73
+ const filterCols = (cols) => cols.filter(col => {
74
+ if (!col) {
75
+ return false
76
+ }
77
+ if (Array.isArray(col.children) && col.children.length > 0) {
78
+ col.children = filterCols(col.children)
79
+ return col.children.length > 0
80
+ }
81
+ if (col.key === undefined || col.key === null) {
82
+ return true
83
+ }
84
+ if (!Object.prototype.hasOwnProperty.call(visibility, col.key)) {
85
+ return true
86
+ }
87
+ return visibility[col.key] === true
88
+ })
89
+ return filterCols(columns)
90
+ }
@@ -7,5 +7,7 @@ export const EventBus = new Vue()
7
7
  export const EVENT_TYPES = {
8
8
  NUM_IDX_ACTIVATED: 'num-idx-activated',
9
9
  NUM_IDX_DEACTIVATED: 'num-idx-deactivated',
10
- TABLE_ROW_ACTIVATED: 'table-row-activated'
10
+ TABLE_ROW_ACTIVATED: 'table-row-activated',
11
+ // 维度选择变化,payload 为 列key -> 是否显示 的映射
12
+ DIMENSION_COLUMNS_CHANGED: 'dimension-columns-changed'
11
13
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lambo-design-bi",
3
- "version": "0.0.253",
3
+ "version": "0.0.255",
4
4
  "description": "Vue2 BI 数据可视化组件库",
5
5
  "main": "index.js",
6
6
  "author": "finch",
@@ -1354,12 +1354,14 @@ export function getECharts(type, dataX, dataY, dataDim, dataZ) {
1354
1354
  } else if (type === '33') {
1355
1355
  let data = []
1356
1356
  let sum = 0
1357
- for (let i = 0; i < dataY[0].length; i++) {
1358
- data.push({
1359
- name: dataX[i],
1360
- value: dataY[0][i]
1361
- })
1362
- sum += dataY[0][i]
1357
+ if (dataY && dataY.length > 0) {
1358
+ for (let i = 0; i < dataY[0].length; i++) {
1359
+ data.push({
1360
+ name: dataX[i],
1361
+ value: dataY[0][i]
1362
+ })
1363
+ sum += dataY[0][i]
1364
+ }
1363
1365
  }
1364
1366
  let title33 = ''
1365
1367
  if (dataDim && dataDim.length > 0) {
@@ -65,7 +65,6 @@
65
65
  </div>
66
66
  </template>
67
67
  <script>
68
- import { oneOf } from 'view-ui-plus/src/utils/assist'
69
68
  import Trend from 'view-ui-plus/src/components/trend'
70
69
  import 'view-ui-plus/dist/styles/viewuiplus.css'
71
70
  import { EVENT_TYPES, EventBus } from '../../bi/widget/util/event-bus'
@@ -88,24 +87,15 @@ export default {
88
87
  type: [String, Number]
89
88
  },
90
89
  trend1: {
91
- validator(value) {
92
- return oneOf(value, ['up', 'down']);
93
- }
94
90
  },
95
91
  polar1: {
96
92
  type: String,
97
- required: false,
98
- validator(value) {
99
- return !value || ['pos', 'neg'].includes(value);
100
- }
93
+ required: false
101
94
  },
102
95
  subNum2: {
103
96
  type: [String, Number]
104
97
  },
105
98
  trend2: {
106
- validator(value) {
107
- return oneOf(value, ['up', 'down']);
108
- }
109
99
  },
110
100
  polar2: {
111
101
  type: String,