vue2-client 1.18.21 → 1.18.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,112 +1,112 @@
1
- {
2
- "name": "vue2-client",
3
- "version": "1.18.21",
4
- "private": false,
5
- "scripts": {
6
- "serve": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint",
7
- "serve:gaslink": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint --mode gaslink",
8
- "serve:revenue": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint --mode revenue",
9
- "serve:liuli": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint --mode liuli",
10
- "serve:scada": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint --mode scada",
11
- "serve:iot": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint --mode iot",
12
- "serve:his": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint --mode his",
13
- "serve:runtime": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint --mode runtime",
14
- "serve:message": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint --mode message",
15
- "serve:apply": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint --mode apply",
16
- "mac-serve": "vue-cli-service serve --no-eslint --mode his",
17
- "build": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build",
18
- "test:unit": "vue-cli-service test:unit",
19
- "lint": "vue-cli-service lint",
20
- "build:preview": "vue-cli-service build --mode preview",
21
- "lint:nofix": "vue-cli-service lint --no-fix",
22
- "test": "jest"
23
- },
24
- "dependencies": {
25
- "@afwenming123/vue-easy-tree": "^1.0.1",
26
- "@afwenming123/vue-plugin-hiprint": "^0.0.70",
27
- "@amap/amap-jsapi-loader": "^1.0.1",
28
- "@antv/data-set": "^0.11.8",
29
- "@antv/g2plot": "^2.4.31",
30
- "@hufe921/canvas-editor": "^0.9.49",
31
- "@microsoft/fetch-event-source": "^2.0.1",
32
- "@vue/babel-preset-jsx": "^1.4.0",
33
- "animate.css": "^4.1.1",
34
- "ant-design-vue": "^1.7.8",
35
- "axios": "^0.27.2",
36
- "clipboard": "^2.0.11",
37
- "core-js": "^3.33.0",
38
- "crypto-js": "^4.1.1",
39
- "date-fns": "^2.29.3",
40
- "default-passive-events": "^2.0.0",
41
- "dotenv": "^16.3.1",
42
- "echarts": "^5.5.0",
43
- "enquire.js": "^2.1.6",
44
- "file-saver": "^2.0.5",
45
- "highlight.js": "^11.7.0",
46
- "html2canvas": "^1.4.1",
47
- "js-base64": "^3.7.5",
48
- "js-cookie": "^2.2.1",
49
- "jsencrypt": "^3.3.2",
50
- "jspdf": "^2.5.1",
51
- "lodash.clonedeep": "^4.5.0",
52
- "lodash.debounce": "^4",
53
- "lodash.get": "^4.4.2",
54
- "marked": "^4",
55
- "mockjs": "^1.1.0",
56
- "nprogress": "^0.2.0",
57
- "qs": "^6.11.2",
58
- "regenerator-runtime": "^0.14.0",
59
- "splitpanes": "^2.4.1",
60
- "videojs-contrib-hls": "^5.15.0",
61
- "viser-vue": "^2.4.8",
62
- "vue": "^2.7.14",
63
- "vue-codemirror": "4.0.6",
64
- "vue-color": "2.7.0",
65
- "vue-draggable-resizable": "^2.3.0",
66
- "vue-i18n": "^8.28.2",
67
- "vue-json-viewer": "^2.2.22",
68
- "vue-router": "^3.6.5",
69
- "vue-video-player": "^5.0.2",
70
- "vue-virtual-scroller": "^1.1.2",
71
- "vuedraggable": "^2.24.3",
72
- "vuex": "^3.6.2",
73
- "xlsx": "0.18.5"
74
- },
75
- "devDependencies": {
76
- "@ant-design/colors": "^7.0.0",
77
- "@babel/core": "^7.22.20",
78
- "@babel/eslint-parser": "^7.22.15",
79
- "@babel/preset-env": "^7.22.20",
80
- "@vue/cli-plugin-babel": "^5.0.8",
81
- "@vue/cli-plugin-eslint": "^5.0.8",
82
- "@vue/cli-service": "^5.0.8",
83
- "@vue/eslint-config-standard": "^8.0.1",
84
- "@vue/test-utils": "^1.3.6",
85
- "babel-plugin-transform-remove-console": "^6.9.4",
86
- "compression-webpack-plugin": "^10.0.0",
87
- "css-minimizer-webpack-plugin": "^5.0.1",
88
- "deepmerge": "^4.3.1",
89
- "eslint": "^8.51.0",
90
- "eslint-plugin-vue": "^9.17.0",
91
- "fast-deep-equal": "^3.1.3",
92
- "ignore-loader": "^0.1.2",
93
- "jest": "^29.7.0",
94
- "jest-environment-jsdom": "^29.7.0",
95
- "jest-transform-stub": "^2.0.0",
96
- "less-loader": "^6.2.0",
97
- "script-loader": "^0.7.2",
98
- "style-resources-loader": "^1.5.0",
99
- "vue-cli-plugin-style-resources-loader": "^0.1.5",
100
- "vue-jest": "^4.0.1",
101
- "vue-template-compiler": "^2.7.14",
102
- "webpack": "^5.88.2",
103
- "webpack-theme-color-replacer": "^1.4.7",
104
- "whatwg-fetch": "^3.6.19"
105
- },
106
- "browserslist": [
107
- "> 1%",
108
- "last 2 versions",
109
- "not dead",
110
- "not ie 11"
111
- ]
112
- }
1
+ {
2
+ "name": "vue2-client",
3
+ "version": "1.18.23",
4
+ "private": false,
5
+ "scripts": {
6
+ "serve": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint",
7
+ "serve:gaslink": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint --mode gaslink",
8
+ "serve:revenue": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint --mode revenue",
9
+ "serve:liuli": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint --mode liuli",
10
+ "serve:scada": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint --mode scada",
11
+ "serve:iot": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint --mode iot",
12
+ "serve:his": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint --mode his",
13
+ "serve:runtime": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint --mode runtime",
14
+ "serve:message": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint --mode message",
15
+ "serve:apply": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint --mode apply",
16
+ "mac-serve": "vue-cli-service serve --no-eslint --mode his",
17
+ "build": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build",
18
+ "test:unit": "vue-cli-service test:unit",
19
+ "lint": "vue-cli-service lint",
20
+ "build:preview": "vue-cli-service build --mode preview",
21
+ "lint:nofix": "vue-cli-service lint --no-fix",
22
+ "test": "jest"
23
+ },
24
+ "dependencies": {
25
+ "@afwenming123/vue-easy-tree": "^1.0.1",
26
+ "@afwenming123/vue-plugin-hiprint": "^0.0.70",
27
+ "@amap/amap-jsapi-loader": "^1.0.1",
28
+ "@antv/data-set": "^0.11.8",
29
+ "@antv/g2plot": "^2.4.31",
30
+ "@hufe921/canvas-editor": "^0.9.49",
31
+ "@microsoft/fetch-event-source": "^2.0.1",
32
+ "@vue/babel-preset-jsx": "^1.4.0",
33
+ "animate.css": "^4.1.1",
34
+ "ant-design-vue": "^1.7.8",
35
+ "axios": "^0.27.2",
36
+ "clipboard": "^2.0.11",
37
+ "core-js": "^3.33.0",
38
+ "crypto-js": "^4.1.1",
39
+ "date-fns": "^2.29.3",
40
+ "default-passive-events": "^2.0.0",
41
+ "dotenv": "^16.3.1",
42
+ "echarts": "^5.5.0",
43
+ "enquire.js": "^2.1.6",
44
+ "file-saver": "^2.0.5",
45
+ "highlight.js": "^11.7.0",
46
+ "html2canvas": "^1.4.1",
47
+ "js-base64": "^3.7.5",
48
+ "js-cookie": "^2.2.1",
49
+ "jsencrypt": "^3.3.2",
50
+ "jspdf": "^2.5.1",
51
+ "lodash.clonedeep": "^4.5.0",
52
+ "lodash.debounce": "^4",
53
+ "lodash.get": "^4.4.2",
54
+ "marked": "^4",
55
+ "mockjs": "^1.1.0",
56
+ "nprogress": "^0.2.0",
57
+ "qs": "^6.11.2",
58
+ "regenerator-runtime": "^0.14.0",
59
+ "splitpanes": "^2.4.1",
60
+ "videojs-contrib-hls": "^5.15.0",
61
+ "viser-vue": "^2.4.8",
62
+ "vue": "^2.7.14",
63
+ "vue-codemirror": "4.0.6",
64
+ "vue-color": "2.7.0",
65
+ "vue-draggable-resizable": "^2.3.0",
66
+ "vue-i18n": "^8.28.2",
67
+ "vue-json-viewer": "^2.2.22",
68
+ "vue-router": "^3.6.5",
69
+ "vue-video-player": "^5.0.2",
70
+ "vue-virtual-scroller": "^1.1.2",
71
+ "vuedraggable": "^2.24.3",
72
+ "vuex": "^3.6.2",
73
+ "xlsx": "0.18.5"
74
+ },
75
+ "devDependencies": {
76
+ "@ant-design/colors": "^7.0.0",
77
+ "@babel/core": "^7.22.20",
78
+ "@babel/eslint-parser": "^7.22.15",
79
+ "@babel/preset-env": "^7.22.20",
80
+ "@vue/cli-plugin-babel": "^5.0.8",
81
+ "@vue/cli-plugin-eslint": "^5.0.8",
82
+ "@vue/cli-service": "^5.0.8",
83
+ "@vue/eslint-config-standard": "^8.0.1",
84
+ "@vue/test-utils": "^1.3.6",
85
+ "babel-plugin-transform-remove-console": "^6.9.4",
86
+ "compression-webpack-plugin": "^10.0.0",
87
+ "css-minimizer-webpack-plugin": "^5.0.1",
88
+ "deepmerge": "^4.3.1",
89
+ "eslint": "^8.51.0",
90
+ "eslint-plugin-vue": "^9.17.0",
91
+ "fast-deep-equal": "^3.1.3",
92
+ "ignore-loader": "^0.1.2",
93
+ "jest": "^29.7.0",
94
+ "jest-environment-jsdom": "^29.7.0",
95
+ "jest-transform-stub": "^2.0.0",
96
+ "less-loader": "^6.2.0",
97
+ "script-loader": "^0.7.2",
98
+ "style-resources-loader": "^1.5.0",
99
+ "vue-cli-plugin-style-resources-loader": "^0.1.5",
100
+ "vue-jest": "^4.0.1",
101
+ "vue-template-compiler": "^2.7.14",
102
+ "webpack": "^5.88.2",
103
+ "webpack-theme-color-replacer": "^1.4.7",
104
+ "whatwg-fetch": "^3.6.19"
105
+ },
106
+ "browserslist": [
107
+ "> 1%",
108
+ "last 2 versions",
109
+ "not dead",
110
+ "not ie 11"
111
+ ]
112
+ }
@@ -340,7 +340,7 @@ defineExpose({
340
340
  .ant-btn {
341
341
  border: none; /* 移除边框 */
342
342
  color: #FFFFFF; /* 字体颜色 */
343
- background-color: #5D5C5C; /* 背景色 */
343
+ background-color: #0057FE; /* 背景色 */
344
344
  box-sizing: border-box; /* 盒模型 */
345
345
  left: 12px; /* 左偏移 */
346
346
  width: 280px; /* 宽度 */
@@ -19,6 +19,36 @@
19
19
  </div>
20
20
  </div>
21
21
  <div class="chart-main">
22
+ <div v-if="showCascadeList" class="chart-cascade-panel">
23
+ <div v-if="sideListLabel" class="cascade-panel-label">{{ sideListLabel }}</div>
24
+ <div class="cascade-columns">
25
+ <div
26
+ v-for="column in cascadeColumns"
27
+ :key="column.field"
28
+ class="cascade-column"
29
+ :class="{ 'is-disabled': !column.options.length }"
30
+ >
31
+ <div class="cascade-column-header">
32
+ <span class="column-title">{{ column.label }}</span>
33
+ <span v-if="column.subtitle" class="column-subtitle">{{ column.subtitle }}</span>
34
+ </div>
35
+ <div class="cascade-option-list">
36
+ <button
37
+ v-for="option in column.options"
38
+ :key="option.value"
39
+ type="button"
40
+ class="cascade-option"
41
+ :class="{ 'is-active': option.value === selectedCascadeValues[column.field] }"
42
+ :disabled="!column.options.length"
43
+ @click="handleCascadeChange(column.field, option.value)"
44
+ >
45
+ <span class="option-label">{{ option.label }}</span>
46
+ <span v-if="option.badge" class="option-badge">{{ option.badge }}</span>
47
+ </button>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </div>
22
52
  <div v-if="showRadioFilter" class="chart-toolbar">
23
53
  <span v-if="radioFilterLabel" class="toolbar-label">{{ radioFilterLabel }}</span>
24
54
  <div class="toolbar-radio-group">
@@ -90,6 +120,7 @@ const activePointIndex = ref(-1)
90
120
  const activeBarIndex = ref(-1)
91
121
  const selectedFilterValue = ref('')
92
122
  const selectedSideListValue = ref('')
123
+ const selectedCascadeValues = ref({})
93
124
 
94
125
  // 记录上一次的 queryParamsName,用于比较变化
95
126
  const lastQueryParamsName = ref('')
@@ -124,19 +155,85 @@ const filterQueryParams = computed(() => {
124
155
  return { [filterFieldName.value]: optionValue }
125
156
  })
126
157
  const sideListConfig = computed(() => props.config?.sideList || chartConfig.value?.sideList || null)
158
+ const isCascadeMode = computed(() => sideListConfig.value?.mode === 'cascade')
127
159
  const sideListOptions = computed(() => {
128
160
  const options = sideListConfig.value?.options
129
161
  return Array.isArray(options) ? options : []
130
162
  })
131
163
  const sideListLabel = computed(() => sideListConfig.value?.label || sideListConfig.value?.title || '')
132
- const showSideList = computed(() => sideListOptions.value.length > 0)
164
+ const cascadeLevels = computed(() => {
165
+ if (!isCascadeMode.value) return []
166
+ const levels = sideListConfig.value?.levels
167
+ return Array.isArray(levels) ? levels : []
168
+ })
169
+ const pickFirstArrayFromObject = (source) => {
170
+ if (!source || typeof source !== 'object') return []
171
+ for (const value of Object.values(source)) {
172
+ if (Array.isArray(value)) return value
173
+ }
174
+ return []
175
+ }
176
+ const normalizeOptionList = (options) => {
177
+ if (!options) return []
178
+ if (Array.isArray(options)) return options
179
+ return []
180
+ }
181
+ const cascadeColumns = computed(() => {
182
+ if (!isCascadeMode.value) return []
183
+ const columns = []
184
+ cascadeLevels.value.forEach((level, idx) => {
185
+ const field = level?.field || level?.prop || `cascade-field-${idx}`
186
+ const optionsSource = level?.options
187
+ let scopedOptions = normalizeOptionList(optionsSource)
188
+ if (!Array.isArray(optionsSource) && optionsSource && typeof optionsSource === 'object') {
189
+ if (idx === 0) {
190
+ const firstArray = optionsSource.default || optionsSource.list || pickFirstArrayFromObject(optionsSource)
191
+ scopedOptions = normalizeOptionList(firstArray)
192
+ } else {
193
+ const parentField = columns[idx - 1]?.field
194
+ const parentValue = parentField ? selectedCascadeValues.value[parentField] : undefined
195
+ const mappedOptions = (parentValue && optionsSource[parentValue]) || optionsSource.default || pickFirstArrayFromObject(optionsSource)
196
+ scopedOptions = normalizeOptionList(mappedOptions)
197
+ }
198
+ }
199
+ columns.push({
200
+ label: level?.label || level?.title || '',
201
+ subtitle: level?.subtitle || '',
202
+ field,
203
+ defaultValue: level?.defaultValue,
204
+ options: scopedOptions
205
+ })
206
+ })
207
+ return columns
208
+ })
209
+ const showSideList = computed(() => !isCascadeMode.value && sideListOptions.value.length > 0)
210
+ const showCascadeList = computed(() => isCascadeMode.value && cascadeColumns.value.some(column => column.options.length))
133
211
  const sideListFieldName = computed(() => sideListConfig.value?.field || sideListConfig.value?.prop || 'category')
212
+ const cascadeQueryParams = computed(() => {
213
+ if (!isCascadeMode.value) return {}
214
+ return cascadeColumns.value.reduce((params, column) => {
215
+ const field = column.field
216
+ if (!field) return params
217
+ const value = selectedCascadeValues.value[field]
218
+ if (value !== undefined && value !== null && value !== '') {
219
+ params[field] = value
220
+ }
221
+ return params
222
+ }, {})
223
+ })
134
224
  const listQueryParams = computed(() => {
225
+ if (isCascadeMode.value) return cascadeQueryParams.value
135
226
  if (!showSideList.value) return {}
136
227
  const optionValue = selectedSideListValue.value
137
228
  if (optionValue === undefined || optionValue === null || optionValue === '') return {}
138
229
  return { [sideListFieldName.value]: optionValue }
139
230
  })
231
+ const cascadeSelectedKeys = computed(() => {
232
+ if (!isCascadeMode.value) return []
233
+ return cascadeColumns.value
234
+ .map(column => column.field && selectedCascadeValues.value[column.field])
235
+ .filter(value => value !== undefined && value !== null && value !== '')
236
+ })
140
237
  const resolveStaticDataset = (config) => {
141
238
  const dataset = config?.dataset
142
239
  if (Array.isArray(dataset)) return dataset
@@ -163,15 +260,37 @@ const resolveStaticDataset = (config) => {
163
260
  }
164
261
  return null
165
262
  }
166
- const listKeys = [
167
- selectedSideListValue.value,
168
- sideListConfig.value?.defaultValue
169
- ].filter(Boolean)
263
+ const pickNestedByKeys = (source, keys) => {
264
+ if (!source || typeof source !== 'object' || !keys.length) return null
265
+ let scoped = source
266
+ for (const key of keys) {
267
+ if (!key) continue
268
+ if (!scoped || typeof scoped !== 'object') return null
269
+ if (!(key in scoped)) return null
270
+ scoped = scoped[key]
271
+ }
272
+ return scoped
273
+ }
170
274
  let scopedDataset = dataset
171
- if (showSideList.value) {
172
- const listScoped = pickByKeys(dataset, listKeys, true)
173
- if (Array.isArray(listScoped)) return listScoped
174
- if (listScoped && typeof listScoped === 'object') scopedDataset = listScoped
275
+ if (isCascadeMode.value) {
276
+ const cascadeKeys = cascadeColumns.value
277
+ .map(column => column.field && selectedCascadeValues.value[column.field])
278
+ .filter(value => value !== undefined && value !== null && value !== '')
279
+ if (cascadeKeys.length) {
280
+ const cascadeScoped = pickNestedByKeys(dataset, cascadeKeys)
281
+ if (Array.isArray(cascadeScoped)) return cascadeScoped
282
+ if (cascadeScoped && typeof cascadeScoped === 'object') scopedDataset = cascadeScoped
283
+ }
284
+ } else {
285
+ const listKeys = [
286
+ selectedSideListValue.value,
287
+ sideListConfig.value?.defaultValue
288
+ ].filter(Boolean)
289
+ if (listKeys.length) {
290
+ const listScoped = pickByKeys(dataset, listKeys, true)
291
+ if (Array.isArray(listScoped)) return listScoped
292
+ if (listScoped && typeof listScoped === 'object') scopedDataset = listScoped
293
+ }
175
294
  }
176
295
  if (Array.isArray(scopedDataset)) return scopedDataset
177
296
  const filterKeys = [
@@ -233,13 +352,70 @@ const ensureListSelection = () => {
233
352
  const fallback = sideListConfig.value?.defaultValue ?? options[0]?.value ?? ''
234
353
  selectedSideListValue.value = fallback
235
354
  }
355
+ const ensureCascadeSelections = () => {
356
+ if (!isCascadeMode.value) {
357
+ if (Object.keys(selectedCascadeValues.value).length) {
358
+ selectedCascadeValues.value = {}
359
+ }
360
+ return
361
+ }
362
+ const nextSelections = { ...selectedCascadeValues.value }
363
+ let changed = false
364
+ cascadeColumns.value.forEach(column => {
365
+ const field = column.field
366
+ if (!field) return
367
+ const options = column.options || []
368
+ if (!options.length) {
369
+ if (nextSelections[field]) {
370
+ delete nextSelections[field]
371
+ changed = true
372
+ }
373
+ return
374
+ }
375
+ const exists = options.some(option => option.value === nextSelections[field])
376
+ if (exists) return
377
+ const fallback = column.defaultValue ?? options[0]?.value ?? ''
378
+ if (fallback !== undefined && fallback !== null && fallback !== '') {
379
+ if (nextSelections[field] !== fallback) {
380
+ nextSelections[field] = fallback
381
+ changed = true
382
+ }
383
+ } else if (nextSelections[field]) {
384
+ delete nextSelections[field]
385
+ changed = true
386
+ }
387
+ })
388
+ if (changed) {
389
+ selectedCascadeValues.value = { ...nextSelections }
390
+ }
391
+ }
236
392
 
237
393
  watch([radioFilterOptions, () => radioFilterConfig.value?.defaultValue], () => {
238
394
  ensureFilterSelection()
239
395
  }, { immediate: true })
240
- watch([sideListOptions, () => sideListConfig.value?.defaultValue], () => {
241
- ensureListSelection()
242
- }, { immediate: true })
396
+ watch(
397
+ () => ({
398
+ options: sideListOptions.value,
399
+ defaultValue: sideListConfig.value?.defaultValue,
400
+ cascadeMode: isCascadeMode.value
401
+ }),
402
+ () => {
403
+ if (isCascadeMode.value) return
404
+ ensureListSelection()
405
+ },
406
+ { immediate: true, deep: true }
407
+ )
408
+ watch(
409
+ () => cascadeColumns.value.map(column => ({
410
+ field: column.field,
411
+ defaultValue: column.defaultValue,
412
+ optionValues: column.options?.map(option => option.value)
413
+ })),
414
+ () => {
415
+ ensureCascadeSelections()
416
+ },
417
+ { immediate: true, deep: true }
418
+ )
243
419
 
244
420
  const handleFilterChange = (value) => {
245
421
  if (value === selectedFilterValue.value) return
@@ -257,6 +433,26 @@ const handleListChange = (value) => {
257
433
  loadChartData(config)
258
434
  }
259
435
  }
436
+ const handleCascadeChange = (field, value) => {
437
+ if (!isCascadeMode.value || !field) return
438
+ if (selectedCascadeValues.value[field] === value) return
439
+ const updatedSelections = { ...selectedCascadeValues.value, [field]: value }
440
+ const startIndex = cascadeColumns.value.findIndex(column => column.field === field)
441
+ if (startIndex >= 0) {
442
+ for (let idx = startIndex + 1; idx < cascadeColumns.value.length; idx++) {
443
+ const childField = cascadeColumns.value[idx].field
444
+ if (childField && updatedSelections[childField] !== undefined) {
445
+ delete updatedSelections[childField]
446
+ }
447
+ }
448
+ }
449
+ selectedCascadeValues.value = updatedSelections
450
+ ensureCascadeSelections()
451
+ const config = chartConfig.value || props.config
452
+ if (config) {
453
+ loadChartData(config)
454
+ }
455
+ }
260
456
 
261
457
  // 常用图表预设,统一处理 dataset → ECharts option 的映射
262
458
  const presetResolvers = {
@@ -494,17 +690,18 @@ const presetResolvers = {
494
690
  type: 'pie',
495
691
  radius: '55%',
496
692
  startAngle: 180,
693
+ hoverOffset: 1,
497
694
  data: dataset.map(item => ({ name: item.label, value: item.value })),
498
695
  itemStyle: {
499
696
  borderColor: '#fff',
500
- borderWidth: 2
697
+ borderWidth: 1.5
501
698
  },
502
699
  emphasis: {
503
700
  scale: true,
504
- scaleSize: 6,
701
+ scaleSize: 1,
505
702
  itemStyle: {
506
- shadowBlur: 16,
507
- shadowColor: 'rgba(0, 0, 0, 0.25)'
703
+ shadowBlur: 12,
704
+ shadowColor: 'rgba(0, 0, 0, 0.22)'
508
705
  },
509
706
  label: {
510
707
  show: true,
@@ -666,6 +863,7 @@ const fetchConfigAndData = async () => {
666
863
  const loadChartData = async (config) => {
667
864
  if (!config) return
668
865
  ensureListSelection()
866
+ ensureCascadeSelections()
669
867
  ensureFilterSelection()
670
868
 
671
869
  try {
@@ -978,6 +1176,103 @@ onBeforeUnmount(() => {
978
1176
  flex-direction: column;
979
1177
  }
980
1178
 
1179
+ .chart-cascade-panel {
1180
+ display: flex;
1181
+ flex-direction: column;
1182
+ gap: 8px;
1183
+ margin-top: 4px;
1184
+ width: 100%;
1185
+ }
1186
+
1187
+ .cascade-panel-label {
1188
+ font-size: 14px;
1189
+ color: #4A5875;
1190
+ font-weight: 600;
1191
+ }
1192
+
1193
+ .cascade-columns {
1194
+ display: flex;
1195
+ gap: 16px;
1196
+ width: 100%;
1197
+ flex-wrap: wrap;
1198
+ overflow-x: auto;
1199
+ padding-bottom: 4px;
1200
+ }
1201
+
1202
+ .cascade-column {
1203
+ flex: 1 1 180px;
1204
+ min-width: 180px;
1205
+ border-right: 1px solid #ECF0F7;
1206
+ padding-right: 12px;
1207
+ }
1208
+
1209
+ .cascade-column:last-child {
1210
+ border-right: none;
1211
+ padding-right: 0;
1212
+ }
1213
+
1214
+ .cascade-column.is-disabled {
1215
+ opacity: 0.6;
1216
+ }
1217
+
1218
+ .cascade-column-header {
1219
+ display: flex;
1220
+ justify-content: space-between;
1221
+ align-items: center;
1222
+ font-size: 13px;
1223
+ color: #5C6C8C;
1224
+ font-weight: 600;
1225
+ margin-bottom: 6px;
1226
+ }
1227
+
1228
+ .column-subtitle {
1229
+ font-size: 12px;
1230
+ color: #9AA7C5;
1231
+ font-weight: 400;
1232
+ }
1233
+
1234
+ .cascade-option-list {
1235
+ display: flex;
1236
+ flex-direction: column;
1237
+ gap: 6px;
1238
+ }
1239
+
1240
+ .cascade-option {
1241
+ width: 100%;
1242
+ text-align: left;
1243
+ border: 0;
1244
+ background: transparent;
1245
+ padding: 8px 10px;
1246
+ border-radius: 8px;
1247
+ font-size: 13px;
1248
+ color: #4A5875;
1249
+ cursor: pointer;
1250
+ transition: all 0.15s ease;
1251
+ display: flex;
1252
+ justify-content: space-between;
1253
+ align-items: center;
1254
+ }
1255
+
1256
+ .cascade-option .option-label {
1257
+ flex: 1;
1258
+ }
1259
+
1260
+ .cascade-option .option-badge {
1261
+ font-size: 12px;
1262
+ color: #9AA7C5;
1263
+ }
1264
+
1265
+ .cascade-option.is-active {
1266
+ background: rgba(31, 91, 255, 0.08);
1267
+ color: #1F5BFF;
1268
+ font-weight: 600;
1269
+ }
1270
+
1271
+ .cascade-option:disabled {
1272
+ cursor: not-allowed;
1273
+ opacity: 0.6;
1274
+ }
1275
+
981
1276
  .chart-toolbar {
982
1277
  display: flex;
983
1278
  align-items: center;