xkit-editor 2.0.0-alpha.0 → 2.0.0-alpha.1

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 (68) hide show
  1. package/README.md +26 -0
  2. package/lib/packages/editor/src/components/json-code-editor.vue.d.ts +2 -0
  3. package/lib/packages/editor/src/components/json-code-editor.vue.d.ts.map +1 -1
  4. package/lib/packages/editor/src/x-editor.vue.d.ts +8 -0
  5. package/lib/packages/editor/src/x-editor.vue.d.ts.map +1 -1
  6. package/lib/packages/ui/src/components/radios/type.d.ts +2 -2
  7. package/lib/xkit-editor.css +1 -1
  8. package/lib/xkit-editor.js +64 -52
  9. package/lib/xkit-editor.umd.cjs +2 -2
  10. package/package.json +3 -4
  11. package/src/component-configs/__tests__/config.test.ts +0 -288
  12. package/src/component-configs/__tests__/group.test.ts +0 -104
  13. package/src/component-configs/cascader.ts +0 -20
  14. package/src/component-configs/checkbox.ts +0 -43
  15. package/src/component-configs/common-props.ts +0 -130
  16. package/src/component-configs/date-picker.ts +0 -153
  17. package/src/component-configs/date-range-picker.ts +0 -185
  18. package/src/component-configs/group.ts +0 -196
  19. package/src/component-configs/icon-select.ts +0 -56
  20. package/src/component-configs/input-array.ts +0 -184
  21. package/src/component-configs/input-number-range.ts +0 -143
  22. package/src/component-configs/input-number.ts +0 -111
  23. package/src/component-configs/input-table.ts +0 -191
  24. package/src/component-configs/input-upload.ts +0 -123
  25. package/src/component-configs/input.ts +0 -48
  26. package/src/component-configs/layout-sections.ts +0 -546
  27. package/src/component-configs/options-config.ts +0 -12
  28. package/src/component-configs/radio.ts +0 -26
  29. package/src/component-configs/select.ts +0 -42
  30. package/src/component-configs/switch.ts +0 -19
  31. package/src/component-configs/textarea.ts +0 -84
  32. package/src/component-configs/types.ts +0 -37
  33. package/src/components/condition-editor.vue +0 -641
  34. package/src/components/design-renderers/input-array-design.vue +0 -283
  35. package/src/components/design-renderers/input-table-design.vue +0 -311
  36. package/src/components/form-item-layout-editor.vue +0 -291
  37. package/src/components/form-item-span-editor.vue +0 -328
  38. package/src/components/form-layout-editor.vue +0 -263
  39. package/src/components/group-title-color-select.vue +0 -75
  40. package/src/components/group-title-style-picker.vue +0 -125
  41. package/src/components/input-number-range-binding-editor.vue +0 -226
  42. package/src/components/input-table-default-rows-editor.vue +0 -147
  43. package/src/components/input-table-operation-editor.vue +0 -168
  44. package/src/components/json-code-editor.vue +0 -219
  45. package/src/components/object-section-editor.vue +0 -342
  46. package/src/components/options/default-dict-options-panel.vue +0 -197
  47. package/src/components/options/options-editor.vue +0 -161
  48. package/src/components/table-columns-editor.vue +0 -353
  49. package/src/components/validation-rules-editor.vue +0 -321
  50. package/src/config.ts +0 -51
  51. package/src/editor-item-wrapper.vue +0 -254
  52. package/src/group-title-styles.ts +0 -82
  53. package/src/index.ts +0 -6
  54. package/src/option-sources.ts +0 -45
  55. package/src/root-configs.ts +0 -278
  56. package/src/shims-vue.d.ts +0 -5
  57. package/src/utils/__tests__/component-replace.test.ts +0 -129
  58. package/src/utils/__tests__/editor-schema.test.ts +0 -429
  59. package/src/utils/__tests__/form-item-width.test.ts +0 -53
  60. package/src/utils/__tests__/group-drag-path.test.ts +0 -84
  61. package/src/utils/__tests__/record-field.test.ts +0 -34
  62. package/src/utils/component-replace.ts +0 -67
  63. package/src/utils/editor-schema.ts +0 -374
  64. package/src/utils/form-item-width.ts +0 -40
  65. package/src/utils/local-dict-center.ts +0 -81
  66. package/src/utils/record-field.ts +0 -30
  67. package/src/validation-rules.ts +0 -104
  68. package/src/x-editor.vue +0 -1300
@@ -1,641 +0,0 @@
1
- <template>
2
- <div class="condition-editor">
3
- <template v-if="inlineVisible">
4
- <el-select :model-value="currentMode" placeholder="请选择状态" @change="handleModeChange" class="w-full">
5
- <el-option v-for="option in modeOptions" :key="String(option.value)" :label="option.label" :value="option.value" />
6
- </el-select>
7
-
8
- <div v-if="currentMode === CONDITION_MODE" class="condition-actions">
9
- <el-button type="primary" plain size="small" @click="handleOpen" class="w-full">
10
- 配置条件
11
- </el-button>
12
- <div v-if="Array.isArray(modelValue) && modelValue.length > 0" class="condition-preview">
13
- 已配置 {{ builderConditionCount }} 条条件
14
- </div>
15
- </div>
16
- </template>
17
-
18
- <el-dialog v-model="dialogVisible" title="配置显示条件" width="860px" append-to-body>
19
- <div class="condition-dialog">
20
- <x-condition-builder
21
- v-model="builderValue"
22
- :items="builderItems"
23
- :init-button-attrs="{ label: '添加条件' }"
24
- class="condition-builder"
25
- />
26
- </div>
27
- <template #footer>
28
- <el-button @click="dialogVisible = false">取消</el-button>
29
- <el-button type="primary" @click="handleSave">确定</el-button>
30
- </template>
31
- </el-dialog>
32
- </div>
33
- </template>
34
-
35
- <script setup lang="ts">
36
- import { computed, inject, ref, unref, watch, type ComputedRef } from 'vue'
37
- import { dayjs, ElButton, ElDialog, ElOption, ElSelect } from 'element-plus'
38
- import { XConditionBuilder, genId, type OptionItem, type XConditionNode, type XFormItem } from 'xkit-ui'
39
- import { listEditorLocalDicts } from '../utils/local-dict-center'
40
-
41
- type StateKind = 'visible' | 'disabled' | 'static'
42
- type FieldValueKind = 'number' | 'date' | 'boolean' | 'text' | 'option-single' | 'option-multiple' | 'tree-option'
43
- type EditorOperator =
44
- | 'eq'
45
- | 'neq'
46
- | 'gt'
47
- | 'gte'
48
- | 'lt'
49
- | 'lte'
50
- | 'contains'
51
- | 'not_contains'
52
- | 'in'
53
- | 'not_in'
54
- | 'exists'
55
- | 'empty'
56
-
57
- type FieldMeta = {
58
- label: string
59
- name: string
60
- item: XFormItem
61
- type: string
62
- valueKind: FieldValueKind
63
- options?: OptionItem[]
64
- hasOptions: boolean
65
- isTree: boolean
66
- multiple: boolean
67
- }
68
-
69
- const CONDITION_MODE = 'condition'
70
- const unaryOperators = new Set<EditorOperator>(['exists', 'empty'])
71
- const multiValueOperators = new Set<EditorOperator>(['contains', 'not_contains', 'in', 'not_in'])
72
-
73
- const props = withDefaults(defineProps<{
74
- modelValue?: any
75
- defaultValue?: boolean
76
- stateKind?: StateKind
77
- inlineVisible?: boolean
78
- autoOpenKey?: string | number
79
- }>(), {
80
- inlineVisible: true
81
- })
82
-
83
- const emit = defineEmits<{
84
- (e: 'update:modelValue', value: any): void
85
- }>()
86
-
87
- const editorContext = inject<ComputedRef<{ form?: { body?: XFormItem[] }; selectedItem?: any }> | null>('x-editor-context', null)
88
-
89
- const dialogVisible = ref(false)
90
- const builderValue = ref<XConditionNode | undefined>(undefined)
91
-
92
- const stateKind = computed<StateKind>(() => props.stateKind ?? (props.defaultValue === true ? 'visible' : 'disabled'))
93
-
94
- const modeOptions = computed(() => {
95
- if (stateKind.value === 'visible') {
96
- return [
97
- { label: '始终显示', value: true },
98
- { label: '始终隐藏', value: false },
99
- { label: '满足条件时显示', value: CONDITION_MODE }
100
- ]
101
- }
102
- if (stateKind.value === 'static') {
103
- return [
104
- { label: '默认编辑', value: false },
105
- { label: '始终只读', value: true },
106
- { label: '满足条件时只读', value: CONDITION_MODE }
107
- ]
108
- }
109
- return [
110
- { label: '始终可用', value: false },
111
- { label: '始终禁用', value: true },
112
- { label: '满足条件时禁用', value: CONDITION_MODE }
113
- ]
114
- })
115
-
116
- const currentMode = computed(() => {
117
- if (typeof props.modelValue === 'boolean') return props.modelValue
118
- if (Array.isArray(props.modelValue)) return CONDITION_MODE
119
- return props.defaultValue ?? true
120
- })
121
-
122
- const collectFormItems = (items: XFormItem[] = []) => {
123
- const result: XFormItem[] = []
124
- items.forEach((item) => {
125
- if (item?.type === 'group' && Array.isArray(item.body)) {
126
- result.push(...collectFormItems(item.body))
127
- return
128
- }
129
- result.push(item)
130
- })
131
- return result
132
- }
133
-
134
- const normalizeType = (type: unknown) => String(type || 'input').replace(/^el-/, '').replace(/^x-/, '')
135
-
136
- const normalizeDictOptions = (options: unknown) => {
137
- if (typeof options === 'string') return { type: 'dict', code: options }
138
- if (!options || typeof options !== 'object') return undefined
139
- const source = options as Record<string, unknown>
140
- if (source.type === 'dict' && typeof source.code === 'string') {
141
- return { type: 'dict', code: source.code }
142
- }
143
- return undefined
144
- }
145
-
146
- const resolveConditionOptions = (item: XFormItem): OptionItem[] | undefined => {
147
- const options = unref((item as Record<string, any>).options)
148
- if (Array.isArray(options)) return options
149
-
150
- const dictOptions = normalizeDictOptions(options)
151
- if (!dictOptions) return undefined
152
-
153
- return listEditorLocalDicts().find((dict) => dict.code === dictOptions.code)?.options
154
- }
155
-
156
- const hasTreeOptions = (options?: OptionItem[]) => {
157
- return Boolean(options?.some((option) => Array.isArray(option?.children) && option.children.length > 0))
158
- }
159
-
160
- const createFieldMeta = (item: XFormItem): FieldMeta | undefined => {
161
- if (!item?.name || item.type === 'group') return undefined
162
- const type = normalizeType(item.type)
163
- const options = resolveConditionOptions(item)
164
- const hasOptions = Array.isArray(options)
165
- const isTree = ['cascader', 'tree-select'].includes(type) || hasTreeOptions(options)
166
- const multiple = Boolean((item as any).multiple || ['checkboxes'].includes(type))
167
- let valueKind: FieldValueKind = 'text'
168
-
169
- if (isTree) {
170
- valueKind = 'tree-option'
171
- } else if (hasOptions || ['select', 'radios', 'radio'].includes(type)) {
172
- valueKind = multiple ? 'option-multiple' : 'option-single'
173
- } else if (['input-number', 'input-number-range', 'slider'].includes(type)) {
174
- valueKind = 'number'
175
- } else if (['date-picker', 'date-range-picker', 'date-range-picker-v2'].includes(type)) {
176
- valueKind = 'date'
177
- } else if (['switch', 'checkbox'].includes(type)) {
178
- valueKind = 'boolean'
179
- }
180
-
181
- return {
182
- label: typeof item.label === 'string' && item.label ? item.label : item.name,
183
- name: item.name,
184
- item,
185
- type,
186
- valueKind,
187
- options,
188
- hasOptions,
189
- isTree,
190
- multiple
191
- }
192
- }
193
-
194
- const fieldMetas = computed(() => {
195
- const items = editorContext?.value?.form?.body ?? []
196
- const currentName = editorContext?.value?.selectedItem?.name
197
- const metas = collectFormItems(items)
198
- .map(createFieldMeta)
199
- .filter(Boolean) as FieldMeta[]
200
- const filtered = metas.filter((item) => item.name !== currentName)
201
- return filtered.length > 0 ? filtered : metas
202
- })
203
-
204
- const findFieldMeta = (field?: string) => fieldMetas.value.find((item) => item.name === field)
205
-
206
- const fieldOptions = computed(() => fieldMetas.value.map((item) => ({ label: item.label, value: item.name })))
207
-
208
- const operatorOptionsByKind: Record<FieldValueKind, Array<{ label: string; value: EditorOperator }>> = {
209
- number: [
210
- { label: '等于', value: 'eq' },
211
- { label: '不等于', value: 'neq' },
212
- { label: '大于', value: 'gt' },
213
- { label: '大于等于', value: 'gte' },
214
- { label: '小于', value: 'lt' },
215
- { label: '小于等于', value: 'lte' },
216
- { label: '有值', value: 'exists' },
217
- { label: '为空', value: 'empty' }
218
- ],
219
- date: [
220
- { label: '等于', value: 'eq' },
221
- { label: '不等于', value: 'neq' },
222
- { label: '晚于', value: 'gt' },
223
- { label: '不早于', value: 'gte' },
224
- { label: '早于', value: 'lt' },
225
- { label: '不晚于', value: 'lte' },
226
- { label: '有值', value: 'exists' },
227
- { label: '为空', value: 'empty' }
228
- ],
229
- boolean: [
230
- { label: '等于', value: 'eq' },
231
- { label: '不等于', value: 'neq' }
232
- ],
233
- text: [
234
- { label: '等于', value: 'eq' },
235
- { label: '不等于', value: 'neq' },
236
- { label: '包含', value: 'contains' },
237
- { label: '不包含', value: 'not_contains' },
238
- { label: '有值', value: 'exists' },
239
- { label: '为空', value: 'empty' }
240
- ],
241
- 'option-single': [
242
- { label: '等于', value: 'eq' },
243
- { label: '不等于', value: 'neq' },
244
- { label: '属于其中之一', value: 'in' },
245
- { label: '不属于其中之一', value: 'not_in' },
246
- { label: '有值', value: 'exists' },
247
- { label: '为空', value: 'empty' }
248
- ],
249
- 'option-multiple': [
250
- { label: '包含', value: 'contains' },
251
- { label: '不包含', value: 'not_contains' },
252
- { label: '属于其中之一', value: 'in' },
253
- { label: '不属于其中之一', value: 'not_in' },
254
- { label: '有值', value: 'exists' },
255
- { label: '为空', value: 'empty' }
256
- ],
257
- 'tree-option': [
258
- { label: '等于', value: 'eq' },
259
- { label: '不等于', value: 'neq' },
260
- { label: '包含', value: 'contains' },
261
- { label: '不包含', value: 'not_contains' },
262
- { label: '属于其中之一', value: 'in' },
263
- { label: '不属于其中之一', value: 'not_in' },
264
- { label: '有值', value: 'exists' },
265
- { label: '为空', value: 'empty' }
266
- ]
267
- }
268
-
269
- const getOperatorOptions = (row: any) => {
270
- const meta = findFieldMeta(row.field)
271
- return meta ? operatorOptionsByKind[meta.valueKind] : []
272
- }
273
-
274
- const isMultiValueOperator = (operator?: EditorOperator) => Boolean(operator && multiValueOperators.has(operator))
275
-
276
- const getValueOptions = (row: any) => findFieldMeta(row.field)?.options ?? []
277
-
278
- const isValueEditorVisible = (row: any, valueKind: FieldValueKind) => {
279
- const meta = findFieldMeta(row.field)
280
- return Boolean(meta && meta.valueKind === valueKind && row.operator && !unaryOperators.has(row.operator))
281
- }
282
-
283
- const getDateValueFormat = (meta?: FieldMeta) => {
284
- const sourceValueFormat = (meta?.item as any)?.valueFormat
285
- if (typeof sourceValueFormat === 'string' && sourceValueFormat) return sourceValueFormat
286
-
287
- const pickerType = String((meta?.item as any)?._type || 'date')
288
- if (['year', 'years', 'yearrange'].includes(pickerType)) return 'YYYY'
289
- if (['month', 'months', 'monthrange'].includes(pickerType)) return 'YYYY-MM'
290
- if (['datetime', 'datetimerange'].includes(pickerType)) return 'YYYY-MM-DD HH:mm:ss'
291
- return 'YYYY-MM-DD'
292
- }
293
-
294
- const resetConditionRowValue = (row: any) => {
295
- row.value = undefined
296
- }
297
-
298
- const builderItems = computed<XFormItem[]>(() => [
299
- {
300
- name: 'field',
301
- label: false,
302
- type: 'select',
303
- placeholder: '选择字段',
304
- options: fieldOptions.value,
305
- style: { width: '180px' },
306
- pipeOut(value: string, row: any) {
307
- if (row.field !== value) {
308
- row.operator = undefined
309
- resetConditionRowValue(row)
310
- }
311
- return value
312
- }
313
- },
314
- {
315
- name: 'operator',
316
- label: false,
317
- type: 'select',
318
- placeholder: '选择判断',
319
- options: getOperatorOptions,
320
- visible: (row: any) => Boolean(row.field),
321
- style: { width: '150px' },
322
- pipeOut(value: EditorOperator, row: any) {
323
- if (row.operator !== value) resetConditionRowValue(row)
324
- return value
325
- }
326
- },
327
- {
328
- name: 'value',
329
- label: false,
330
- type: 'select',
331
- placeholder: '选择值',
332
- options: getValueOptions,
333
- visible: (row: any) => isValueEditorVisible(row, 'option-single') || isValueEditorVisible(row, 'option-multiple'),
334
- attrs: (row: any) => ({
335
- multiple: isMultiValueOperator(row.operator),
336
- collapseTags: true,
337
- maxCollapseTags: 10,
338
- collapseTagsTooltip: true
339
- }),
340
- style: { width: '220px' }
341
- },
342
- {
343
- name: 'value',
344
- label: false,
345
- type: 'cascader',
346
- placeholder: '选择值',
347
- options: getValueOptions,
348
- visible: (row: any) => isValueEditorVisible(row, 'tree-option'),
349
- attrs: (row: any) => ({
350
- multiple: isMultiValueOperator(row.operator),
351
- collapseTags: true,
352
- maxCollapseTags: 10,
353
- collapseTagsTooltip: true
354
- }),
355
- style: { width: '240px' }
356
- },
357
- {
358
- name: 'value',
359
- label: false,
360
- type: 'input-number',
361
- placeholder: '输入数值',
362
- visible: (row: any) => isValueEditorVisible(row, 'number'),
363
- style: { width: '160px' }
364
- },
365
- {
366
- name: 'value',
367
- label: false,
368
- type: 'date-picker',
369
- placeholder: '选择日期',
370
- visible: (row: any) => isValueEditorVisible(row, 'date'),
371
- attrs: (row: any) => {
372
- const meta = findFieldMeta(row.field)
373
- return {
374
- type: (meta?.item as any)?._type,
375
- valueFormat: getDateValueFormat(meta)
376
- }
377
- },
378
- style: { width: '180px' }
379
- },
380
- {
381
- name: 'value',
382
- label: false,
383
- type: 'select',
384
- placeholder: '选择值',
385
- options: [
386
- { label: '是', value: true },
387
- { label: '否', value: false }
388
- ],
389
- visible: (row: any) => isValueEditorVisible(row, 'boolean'),
390
- style: { width: '140px' }
391
- },
392
- {
393
- name: 'value',
394
- label: false,
395
- type: 'input',
396
- placeholder: '输入内容',
397
- visible: (row: any) => isValueEditorVisible(row, 'text'),
398
- style: { width: '220px' }
399
- }
400
- ])
401
-
402
- const createEmptyBuilderValue = (): XConditionNode => ({
403
- id: genId('cond'),
404
- level: 0,
405
- type: 'group',
406
- data: { relationType: 'and' },
407
- children: []
408
- })
409
-
410
- const toValueList = (value: any) => {
411
- if (Array.isArray(value)) return value
412
- if (value === undefined || value === null || value === '') return []
413
- return [value]
414
- }
415
-
416
- const normalizeDateValue = (value: any, field: string): any => {
417
- const meta = findFieldMeta(field)
418
- if (meta?.valueKind !== 'date') return value
419
- const format = getDateValueFormat(meta)
420
- if (Array.isArray(value)) return value.map((item) => normalizeDateValue(item, field))
421
- if (value instanceof Date) return dayjs(value).format(format)
422
- return value
423
- }
424
-
425
- const createVarExpression = (field: string) => ['var', `form.${field}`]
426
-
427
- const buildPositiveExpression = (operator: EditorOperator, left: any[], value: any) => {
428
- if (operator === 'eq') return ['==', left, value]
429
- if (operator === 'neq') return ['!=', left, value]
430
- if (operator === 'gt') return ['>', left, value]
431
- if (operator === 'gte') return ['>=', left, value]
432
- if (operator === 'lt') return ['<', left, value]
433
- if (operator === 'lte') return ['<=', left, value]
434
- if (operator === 'exists') return ['exists', left]
435
- if (operator === 'empty') return ['empty', left]
436
- if (operator === 'in') return ['in', left, toValueList(value)]
437
- if (operator === 'contains') {
438
- const values = toValueList(value)
439
- if (values.length > 1) return ['or', ...values.map((item) => ['includes', left, item])]
440
- return ['includes', left, values[0]]
441
- }
442
- return undefined
443
- }
444
-
445
- const isMultipleField = (field: string) => {
446
- const meta = findFieldMeta(field)
447
- return Boolean(meta?.multiple || meta?.valueKind === 'option-multiple')
448
- }
449
-
450
- const buildAnyIncludesExpression = (left: any[], values: any[]) => {
451
- if (values.length > 1) return ['or', ...values.map((item) => ['includes', left, item])]
452
- return ['includes', left, values[0]]
453
- }
454
-
455
- const buildExpressionFromCondition = (field: string, operator: EditorOperator, value: any) => {
456
- const left = createVarExpression(field)
457
- const normalizedValue = normalizeDateValue(value, field)
458
- if (operator === 'in' && isMultipleField(field)) return buildAnyIncludesExpression(left, toValueList(normalizedValue))
459
- if (operator === 'not_in' && isMultipleField(field)) {
460
- return ['not', buildAnyIncludesExpression(left, toValueList(normalizedValue))]
461
- }
462
- if (operator === 'not_in') return ['not', ['in', left, toValueList(normalizedValue)]]
463
- if (operator === 'not_contains') {
464
- const values = toValueList(normalizedValue)
465
- return ['not', buildAnyIncludesExpression(left, values)]
466
- }
467
- return buildPositiveExpression(operator, left, normalizedValue)
468
- }
469
-
470
- const buildExpressionFromNode = (node?: XConditionNode): any[] | undefined => {
471
- if (!node) return undefined
472
- if (node.type === 'group') {
473
- const relation = node.data?.relationType === 'or' ? 'or' : 'and'
474
- const children = (node.children || []).map((child) => buildExpressionFromNode(child)).filter(Boolean) as any[]
475
- if (children.length === 0) return undefined
476
- return [relation, ...children]
477
- }
478
- const field = node.data?.field
479
- const operator = node.data?.operator as EditorOperator | undefined
480
- if (!field || !operator) return undefined
481
- if (multiValueOperators.has(operator) && toValueList(node.data?.value).length === 0) return undefined
482
- if (!unaryOperators.has(operator) && (node.data?.value === undefined || node.data?.value === null || node.data?.value === '')) {
483
- return undefined
484
- }
485
- return buildExpressionFromCondition(field, operator, node.data?.value)
486
- }
487
-
488
- const extractVarPath = (value: any) => {
489
- if (Array.isArray(value) && value[0] === 'var' && typeof value[1] === 'string') return value[1] as string
490
- return undefined
491
- }
492
-
493
- const stripScopePrefix = (path: string) => {
494
- const prefixes = ['form.', 'formData.', 'row.', 'rowData.']
495
- const matched = prefixes.find((prefix) => path.startsWith(prefix))
496
- return matched ? path.slice(matched.length) : path
497
- }
498
-
499
- const createConditionNode = (level: number, fieldPath: string, operator: EditorOperator, value?: any): XConditionNode => ({
500
- id: genId('cond'),
501
- level,
502
- type: 'condition',
503
- data: {
504
- field: stripScopePrefix(fieldPath),
505
- operator,
506
- value
507
- }
508
- })
509
-
510
- const parseExpressionToNode = (expression: any, level = 0): XConditionNode | undefined => {
511
- if (!Array.isArray(expression)) return undefined
512
- const [operator, ...args] = expression
513
- if (operator === 'and' || operator === 'or') {
514
- const children = args.map((item) => parseExpressionToNode(item, level + 1)).filter(Boolean) as XConditionNode[]
515
- return {
516
- id: genId('cond'),
517
- level,
518
- type: 'group',
519
- data: { relationType: operator },
520
- children
521
- }
522
- }
523
- if (operator === 'not') {
524
- const inner = parseExpressionToNode(args[0], level)
525
- if (!inner || inner.type !== 'condition') return undefined
526
- if (inner.data?.operator === 'in') inner.data.operator = 'not_in'
527
- if (inner.data?.operator === 'contains') inner.data.operator = 'not_contains'
528
- return inner
529
- }
530
- if (typeof operator !== 'string') return undefined
531
- if (operator === 'exists' || operator === 'empty') {
532
- const fieldPath = extractVarPath(args[0])
533
- return fieldPath ? createConditionNode(level, fieldPath, operator, undefined) : undefined
534
- }
535
- const fieldPath = extractVarPath(args[0])
536
- if (!fieldPath) return undefined
537
- if (operator === '==') return createConditionNode(level, fieldPath, 'eq', args[1])
538
- if (operator === '!=') return createConditionNode(level, fieldPath, 'neq', args[1])
539
- if (operator === '>') return createConditionNode(level, fieldPath, 'gt', args[1])
540
- if (operator === '>=') return createConditionNode(level, fieldPath, 'gte', args[1])
541
- if (operator === '<') return createConditionNode(level, fieldPath, 'lt', args[1])
542
- if (operator === '<=') return createConditionNode(level, fieldPath, 'lte', args[1])
543
- if (operator === 'in') return createConditionNode(level, fieldPath, 'in', args[1])
544
- if (operator === 'includes') return createConditionNode(level, fieldPath, 'contains', args[1])
545
- return undefined
546
- }
547
-
548
- const buildBuilderValue = (expression: any) => {
549
- const parsed = parseExpressionToNode(expression, 0)
550
- return parsed?.type === 'group' ? parsed : createEmptyBuilderValue()
551
- }
552
-
553
- const builderConditionCount = computed(() => {
554
- const countExpression = (expression: any): number => {
555
- if (!Array.isArray(expression)) return 0
556
- const [operator, ...args] = expression
557
- if (operator === 'and' || operator === 'or') return args.reduce((sum, item) => sum + countExpression(item), 0)
558
- if (operator === 'not') return countExpression(args[0])
559
- return 1
560
- }
561
- return Array.isArray(props.modelValue) ? countExpression(props.modelValue) : 0
562
- })
563
-
564
- const ensureConditionMode = () => {
565
- if (!Array.isArray(props.modelValue)) emit('update:modelValue', [])
566
- }
567
-
568
- const handleModeChange = (val: any) => {
569
- if (val === true || val === false) {
570
- emit('update:modelValue', val)
571
- return
572
- }
573
- ensureConditionMode()
574
- }
575
-
576
- const handleOpen = () => {
577
- ensureConditionMode()
578
- builderValue.value = buildBuilderValue(props.modelValue)
579
- dialogVisible.value = true
580
- }
581
-
582
- const handleSave = () => {
583
- const expression = buildExpressionFromNode(builderValue.value)
584
- emit('update:modelValue', expression ?? [])
585
- dialogVisible.value = false
586
- }
587
-
588
- watch(
589
- () => props.modelValue,
590
- (val) => {
591
- if (dialogVisible.value && Array.isArray(val)) {
592
- builderValue.value = buildBuilderValue(val)
593
- }
594
- }
595
- )
596
-
597
- watch(
598
- () => props.autoOpenKey,
599
- (key, oldKey) => {
600
- if (key === undefined || key === oldKey) return
601
- handleOpen()
602
- }
603
- )
604
-
605
- defineExpose({
606
- open: handleOpen
607
- })
608
- </script>
609
-
610
- <style scoped>
611
- .condition-editor {
612
- width: 100%;
613
- }
614
-
615
- .condition-actions {
616
- margin-top: 8px;
617
- }
618
-
619
- .condition-preview {
620
- margin-top: 4px;
621
- font-size: 12px;
622
- color: #606266;
623
- background: #f8f9fa;
624
- padding: 4px 8px;
625
- border-radius: 4px;
626
- word-break: break-all;
627
- }
628
-
629
- .condition-dialog {
630
- min-height: 220px;
631
- padding: 16px;
632
- }
633
-
634
- .condition-builder {
635
- min-height: 200px;
636
- }
637
-
638
- .w-full {
639
- width: 100%;
640
- }
641
- </style>