eoss 0.1.0-alpha.40 → 0.1.0-alpha.41
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/lib/0.js +1 -1
- package/lib/1.js +1 -1
- package/lib/10.js +1 -1
- package/lib/11.js +1 -1
- package/lib/12.js +1 -1
- package/lib/13.js +1 -1
- package/lib/14.js +1 -1
- package/lib/15.js +1 -1
- package/lib/16.js +1 -1
- package/lib/17.js +1 -1
- package/lib/18.js +1 -1
- package/lib/19.js +1 -1
- package/lib/20.js +1 -1
- package/lib/21.js +1 -1
- package/lib/22.js +1 -1
- package/lib/23.js +1 -1
- package/lib/24.js +1 -1
- package/lib/25.js +1 -1
- package/lib/26.js +1 -1
- package/lib/27.js +1 -1
- package/lib/28.js +1 -1
- package/lib/29.js +1 -1
- package/lib/3.js +1 -1
- package/lib/30.js +1 -1
- package/lib/31.js +1 -1
- package/lib/32.js +1 -1
- package/lib/33.js +1 -1
- package/lib/34.js +1 -1
- package/lib/35.js +1 -1
- package/lib/36.js +1 -1
- package/lib/37.js +1 -1
- package/lib/38.js +1 -1
- package/lib/39.js +1 -1
- package/lib/4.js +1 -1
- package/lib/40.js +1 -1
- package/lib/41.js +1 -1
- package/lib/42.js +1 -1
- package/lib/43.js +1 -1
- package/lib/44.js +1 -1
- package/lib/45.js +1 -1
- package/lib/46.js +1 -1
- package/lib/47.js +1 -1
- package/lib/48.js +1 -1
- package/lib/49.js +1 -1
- package/lib/5.js +1 -1
- package/lib/50.js +1 -1
- package/lib/51.js +1 -1
- package/lib/52.js +1 -1
- package/lib/53.js +1 -1
- package/lib/54.js +1 -1
- package/lib/55.js +1 -1
- package/lib/56.js +1 -1
- package/lib/57.js +1 -1
- package/lib/58.js +1 -1
- package/lib/59.js +1 -1
- package/lib/6.js +1 -1
- package/lib/60.js +1 -1
- package/lib/61.js +1 -1
- package/lib/62.js +1 -1
- package/lib/63.js +1 -1
- package/lib/64.js +1 -1
- package/lib/65.js +1 -1
- package/lib/66.js +1 -1
- package/lib/67.js +1 -1
- package/lib/68.js +1 -1
- package/lib/69.js +1 -1
- package/lib/7.js +1 -1
- package/lib/70.js +1 -1
- package/lib/71.js +1 -1
- package/lib/72.js +1 -1
- package/lib/73.js +1 -1
- package/lib/74.js +1 -1
- package/lib/75.js +1 -1
- package/lib/76.js +1 -1
- package/lib/77.js +1 -1
- package/lib/78.js +1 -1
- package/lib/79.js +1 -1
- package/lib/8.js +1 -1
- package/lib/80.js +1 -1
- package/lib/81.js +1 -1
- package/lib/9.js +1 -1
- package/lib/account-login.js +59 -59
- package/lib/alert.js +2 -2
- package/lib/aside.js +2 -2
- package/lib/audit-trajectory.js +2 -2
- package/lib/autocomplete.js +56 -56
- package/lib/avatar.js +2 -2
- package/lib/backtop.js +6 -6
- package/lib/badge.js +2 -2
- package/lib/breadcrumb-item.js +4 -4
- package/lib/breadcrumb.js +4 -4
- package/lib/button-group.js +4 -4
- package/lib/button.js +2 -2
- package/lib/ca-login.js +37 -37
- package/lib/calendar.js +4 -4
- package/lib/card.js +2 -2
- package/lib/carousel-item.js +36 -36
- package/lib/carousel.js +38 -38
- package/lib/cascader-panel.js +71 -71
- package/lib/cascader.js +220 -220
- package/lib/checkbox-button.js +4 -4
- package/lib/checkbox-group.js +65 -65
- package/lib/checkbox.js +6 -6
- package/lib/code-editor.js +1134 -0
- package/lib/col.js +2 -2
- package/lib/collapse-item.js +76 -76
- package/lib/collapse.js +76 -76
- package/lib/color-picker.js +10 -10
- package/lib/container.js +2 -2
- package/lib/cron.js +20 -20
- package/lib/data-table.js +66 -66
- package/lib/date-picker.js +2419 -2419
- package/lib/descriptions-item.js +4 -4
- package/lib/descriptions.js +4 -4
- package/lib/dialog.js +42 -42
- package/lib/divider.js +2 -2
- package/lib/drawer.js +6 -6
- package/lib/dropdown-item.js +8 -8
- package/lib/dropdown-menu.js +20 -20
- package/lib/dropdown.js +40 -40
- package/lib/email-login.js +64 -64
- package/lib/empty.js +2 -2
- package/lib/eoss.common.js +23730 -5967
- package/lib/flip.js +2 -2
- package/lib/flow-group.js +55 -55
- package/lib/flow.js +171 -171
- package/lib/flows.js +77 -77
- package/lib/footer.js +2 -2
- package/lib/form-editor.js +12534 -0
- package/lib/form-item.js +57 -49
- package/lib/form.js +233 -174
- package/lib/guide.js +2 -2
- package/lib/header.js +2 -2
- package/lib/icon.js +38 -38
- package/lib/icons.js +11 -11
- package/lib/image-viewer.js +38 -38
- package/lib/image.js +6 -6
- package/lib/index.js +1 -1
- package/lib/infinite-scroll.js +4 -4
- package/lib/input-number.js +39 -39
- package/lib/input.js +60 -60
- package/lib/link.js +2 -2
- package/lib/loading.js +6 -6
- package/lib/login.js +193 -193
- package/lib/lowcode-view.js +824 -132
- package/lib/lowcode.js +27278 -16925
- package/lib/main.js +270 -257
- package/lib/marquee.js +38 -38
- package/lib/menu-item-group.js +14 -14
- package/lib/menu-item.js +10 -10
- package/lib/menu.js +129 -129
- package/lib/message-box.js +52 -52
- package/lib/message.js +62 -62
- package/lib/notification.js +40 -40
- package/lib/option-group.js +4 -4
- package/lib/option.js +36 -36
- package/lib/page-header.js +2 -2
- package/lib/pagination.js +37 -37
- package/lib/phone-login.js +58 -58
- package/lib/popconfirm.js +4 -4
- package/lib/popover.js +42 -42
- package/lib/process-config.js +65 -65
- package/lib/process-module.js +62 -62
- package/lib/process-rule.js +66 -66
- package/lib/progress.js +2 -2
- package/lib/qr-code.js +6 -6
- package/lib/qrcode-login.js +57 -57
- package/lib/radio-button.js +4 -4
- package/lib/radio-group.js +63 -63
- package/lib/radio.js +6 -6
- package/lib/rate.js +36 -36
- package/lib/retrial-auth.js +55 -55
- package/lib/row.js +2 -2
- package/lib/scrollbar.js +41 -41
- package/lib/select.js +95 -95
- package/lib/selector-panel.js +92 -91
- package/lib/selector.js +935 -935
- package/lib/slider.js +6 -6
- package/lib/step.js +35 -35
- package/lib/steps.js +82 -82
- package/lib/submenu.js +38 -38
- package/lib/switch.js +38 -38
- package/lib/tab-pane.js +4 -4
- package/lib/table-column.js +39 -39
- package/lib/table.js +653 -653
- package/lib/tabs.js +42 -42
- package/lib/tag.js +2 -2
- package/lib/theme-chalk/code-editor.css +1 -0
- package/lib/theme-chalk/form-editor.css +1 -0
- package/lib/theme-chalk/form.css +1 -1
- package/lib/theme-chalk/index.css +1 -1
- package/lib/theme-chalk/lowcode.css +1 -1
- package/lib/theme-chalk/selector.css +1 -1
- package/lib/time-picker.js +1276 -1276
- package/lib/time-select.js +21 -21
- package/lib/timeline-item.js +2 -2
- package/lib/timeline.js +2 -2
- package/lib/tips.js +59 -59
- package/lib/toolbar.js +6 -6
- package/lib/tooltip.js +44 -44
- package/lib/transfer.js +36 -36
- package/lib/tree-group.js +49 -49
- package/lib/tree.js +432 -432
- package/lib/update-history.js +2 -2
- package/lib/upload.js +942 -942
- package/lib/utils/source-generator.js +586 -0
- package/lib/utils/source-parser.js +581 -0
- package/lib/wujie.js +39 -39
- package/package.json +1 -1
- package/packages/code-editor/index.js +5 -0
- package/packages/{lowcode/src/editor.vue → code-editor/src/code-editor.vue} +14 -17
- package/packages/form/src/form-item-render.vue +25 -0
- package/packages/form/src/form-item.vue +10 -3
- package/packages/form-editor/index.js +5 -0
- package/packages/form-editor/src/components.js +6736 -0
- package/packages/form-editor/src/form-editor.vue +981 -0
- package/packages/form-editor/src/props-panel.vue +168 -0
- package/packages/lowcode/src/attribute/bind.vue +5 -5
- package/packages/lowcode/src/attribute/function.vue +24 -9
- package/packages/lowcode/src/attribute/javascript.vue +4 -4
- package/packages/lowcode/src/attribute/property.vue +1 -1
- package/packages/lowcode/src/attribute/props.vue +44 -29
- package/packages/lowcode/src/attribute/styles.vue +1 -1
- package/packages/lowcode/src/attribute.vue +4 -4
- package/packages/lowcode/src/canvas.vue +45 -100
- package/packages/lowcode/src/form-render.vue +25 -13
- package/packages/lowcode/src/json.vue +4 -4
- package/packages/lowcode/src/lowcode.vue +1 -1
- package/packages/lowcode/src/modules.vue +8 -1
- package/packages/lowcode/src/render.vue +14 -9
- package/packages/lowcode/src/source.vue +6 -6
- package/packages/lowcode/src/util/components.js +15646 -68
- package/packages/lowcode/src/util/form-controls.js +87 -0
- package/packages/lowcode/src/util/props-util.js +9 -8
- package/packages/lowcode-view/src/render.vue +13 -6
- package/packages/lowcode-view/src/view.vue +4 -1
- package/packages/main/src/components/user.vue +2 -0
- package/packages/main/src/components/userinfo.vue +22 -14
- package/packages/selector-panel/src/mixin.js +1 -0
- package/packages/theme-chalk/src/code-editor.scss +59 -0
- package/packages/theme-chalk/src/form-editor.scss +240 -0
- package/packages/theme-chalk/src/form.scss +10 -1
- package/packages/theme-chalk/src/index.scss +2 -0
- package/packages/theme-chalk/src/lowcode.scss +1 -42
- package/packages/theme-chalk/src/selector.scss +2 -0
- package/src/index.js +7 -1
- package/{packages/lowcode/src/util → src/utils}/source-generator.js +24 -15
- package/{packages/lowcode/src/util → src/utils}/source-parser.js +67 -23
- /package/packages/{lowcode → code-editor}/src/util/json-repair.js +0 -0
- /package/packages/{lowcode → code-editor}/src/util/monaco-vue-completions.js +0 -0
|
@@ -0,0 +1,981 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="es-form-editor">
|
|
3
|
+
<!-- 左侧:表单控件面板(拖出源) -->
|
|
4
|
+
<div class="es-form-editor__aside">
|
|
5
|
+
<div class="es-form-editor__search">
|
|
6
|
+
<es-input
|
|
7
|
+
v-model="keyword"
|
|
8
|
+
size="small"
|
|
9
|
+
placeholder="搜索控件"
|
|
10
|
+
clearable
|
|
11
|
+
></es-input>
|
|
12
|
+
</div>
|
|
13
|
+
<es-scrollbar class="es-form-editor__palette-wrap">
|
|
14
|
+
<div ref="palette" class="es-form-editor__palette">
|
|
15
|
+
<div
|
|
16
|
+
v-for="item in components"
|
|
17
|
+
:key="item.name"
|
|
18
|
+
class="es-form-editor__palette-item"
|
|
19
|
+
:data-nodename="item.name"
|
|
20
|
+
>
|
|
21
|
+
{{ item.name.replace(/^Es/, '').trim() }}
|
|
22
|
+
-
|
|
23
|
+
{{ item.description }}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</es-scrollbar>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<!-- 中间:表单画布(真实 es-form 渲染,可拖拽排序) -->
|
|
30
|
+
<div class="es-form-editor__main">
|
|
31
|
+
<div class="es-form-editor__toolbar">
|
|
32
|
+
<es-button size="mini" @click="openJsonEdit()">JSON 编辑</es-button>
|
|
33
|
+
<es-button size="mini" @click="clearAll">清空</es-button>
|
|
34
|
+
<es-button size="mini" type="primary" @click="handleSave"
|
|
35
|
+
>保存</es-button
|
|
36
|
+
>
|
|
37
|
+
</div>
|
|
38
|
+
<es-scrollbar class="es-form-editor__canvas-wrap">
|
|
39
|
+
<div class="es-form-editor__canvas" @click.self="selectField(null)">
|
|
40
|
+
<es-form
|
|
41
|
+
ref="forms"
|
|
42
|
+
v-bind="localFormConfig"
|
|
43
|
+
:columns="formColumns"
|
|
44
|
+
:show-buttons="false"
|
|
45
|
+
class="es-form-editor__form"
|
|
46
|
+
:class="{ 'is-active': selectedId === null }"
|
|
47
|
+
@click.native.self="selectField(null)"
|
|
48
|
+
>
|
|
49
|
+
<es-form-item
|
|
50
|
+
class="es-form-editor__field"
|
|
51
|
+
v-for="(field, index) in forms"
|
|
52
|
+
:label="field.label"
|
|
53
|
+
:prop="field.name"
|
|
54
|
+
:key="field.id"
|
|
55
|
+
:class="{ 'is-active': selectedId === field.id }"
|
|
56
|
+
:span="spanOf(field)"
|
|
57
|
+
:data-id="field.id"
|
|
58
|
+
@click.native.stop="selectField(field)"
|
|
59
|
+
>
|
|
60
|
+
<component
|
|
61
|
+
:is="field.nodeName"
|
|
62
|
+
v-bind="fieldProps(field)"
|
|
63
|
+
:value="previewModels[field.name]"
|
|
64
|
+
@input="(val) => handlePreviewInput(field, val)"
|
|
65
|
+
></component>
|
|
66
|
+
<template slot="handle">
|
|
67
|
+
<es-button
|
|
68
|
+
type="text"
|
|
69
|
+
size="mini"
|
|
70
|
+
icon="es-icon-shanchu"
|
|
71
|
+
class="es-form-editor__field-remove"
|
|
72
|
+
@click.native.stop="removeField(index)"
|
|
73
|
+
></es-button>
|
|
74
|
+
<div
|
|
75
|
+
class="es-form-editor__resize-handle is-left es-icon-stretch"
|
|
76
|
+
@pointerdown="handleResizeStart($event, field, 'left')"
|
|
77
|
+
@dragstart="$event.preventDefault()"
|
|
78
|
+
></div>
|
|
79
|
+
<div
|
|
80
|
+
class="es-form-editor__resize-handle is-right es-icon-stretch"
|
|
81
|
+
@pointerdown="handleResizeStart($event, field, 'right')"
|
|
82
|
+
@dragstart="$event.preventDefault()"
|
|
83
|
+
></div>
|
|
84
|
+
</template>
|
|
85
|
+
</es-form-item>
|
|
86
|
+
<div v-if="!forms.length" class="es-form-editor__empty">
|
|
87
|
+
从左侧拖动表单控件到这里
|
|
88
|
+
</div>
|
|
89
|
+
</es-form>
|
|
90
|
+
</div>
|
|
91
|
+
</es-scrollbar>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<!-- 右侧:属性面板(选中字段 → 字段属性;未选中 → 表单属性) -->
|
|
95
|
+
<div class="es-form-editor__attribute">
|
|
96
|
+
<template v-if="selectedField">
|
|
97
|
+
<div class="es-form-editor__panel-title">
|
|
98
|
+
字段属性
|
|
99
|
+
<span class="es-form-editor__panel-switch" @click="selectField(null)"
|
|
100
|
+
>表单属性</span
|
|
101
|
+
>
|
|
102
|
+
</div>
|
|
103
|
+
<props-panel
|
|
104
|
+
:model="fieldPanelModel"
|
|
105
|
+
:registry="flatRegistry"
|
|
106
|
+
:name-options="fieldNameOptions"
|
|
107
|
+
@change="handleFieldProps"
|
|
108
|
+
@edit="(meta) => openPropJson('field', meta)"
|
|
109
|
+
></props-panel>
|
|
110
|
+
</template>
|
|
111
|
+
<template v-else>
|
|
112
|
+
<div class="es-form-editor__panel-title">表单属性</div>
|
|
113
|
+
<props-panel
|
|
114
|
+
:model="formModel"
|
|
115
|
+
:registry="formRegistry"
|
|
116
|
+
@change="handleFormProps"
|
|
117
|
+
@edit="(meta) => openPropJson('form', meta)"
|
|
118
|
+
></props-panel>
|
|
119
|
+
</template>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<!-- JSON 源码编辑:monaco json + Ctrl+S 保存(编辑器自带状态栏,无 footer 按钮);
|
|
123
|
+
铅笔按钮也走本弹窗,仅额外做缺键插入 + 光标定位 -->
|
|
124
|
+
<es-dialog
|
|
125
|
+
size="max"
|
|
126
|
+
title="JSON 编辑"
|
|
127
|
+
:visible.sync="showJsonEdit"
|
|
128
|
+
:before-close="handleJsonEditClose"
|
|
129
|
+
>
|
|
130
|
+
<es-code-editor
|
|
131
|
+
v-if="showJsonEdit"
|
|
132
|
+
ref="jsonEditorRef"
|
|
133
|
+
:value="jsonText"
|
|
134
|
+
language="json"
|
|
135
|
+
class="es-form-editor__json-editor"
|
|
136
|
+
@save="handleJsonSave"
|
|
137
|
+
@ready="handleEditorReady"
|
|
138
|
+
></es-code-editor>
|
|
139
|
+
</es-dialog>
|
|
140
|
+
</div>
|
|
141
|
+
</template>
|
|
142
|
+
|
|
143
|
+
<script>
|
|
144
|
+
import Sortable from 'sortablejs';
|
|
145
|
+
import EsForm from 'eoss/packages/form';
|
|
146
|
+
import EsFormItem from 'eoss/packages/form-item';
|
|
147
|
+
import EsInput from 'eoss/packages/input';
|
|
148
|
+
import EsButton from 'eoss/packages/button';
|
|
149
|
+
import EsDialog from 'eoss/packages/dialog';
|
|
150
|
+
import EsScrollbar from 'eoss/packages/scrollbar';
|
|
151
|
+
import PropsPanel from './props-panel.vue';
|
|
152
|
+
import EsCodeEditor from 'eoss/packages/code-editor';
|
|
153
|
+
import { props, librarys } from './components.js';
|
|
154
|
+
import { generateVueSource } from 'eoss/src/utils/source-generator.js';
|
|
155
|
+
import { migrateDirectives } from 'eoss/src/utils/source-parser.js';
|
|
156
|
+
import { uuid } from 'eoss/src/utils/util.js';
|
|
157
|
+
|
|
158
|
+
// 字段元信息键:其余键均为控件 prop 取值(平铺契约,不再嵌套 props,适配 es-form contents)
|
|
159
|
+
const FIELD_META = ['description', 'id', 'nodeName', 'label', 'name', 'span'];
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 表单编辑器:拖动 es-form 表单控件生成表单结构(仿 lowcode 三栏布局)。
|
|
163
|
+
* 产出 forms 为平铺字段契约节点 { description, id, nodeName, label, name, ...prop 取值 },
|
|
164
|
+
* 可直接作为 es-form 的 :contents 消费(form-item-render 的 nodeName 兼容通道),
|
|
165
|
+
* 也可交给 lowcode 挂到 EsForm 节点的 children 下。
|
|
166
|
+
*/
|
|
167
|
+
export default {
|
|
168
|
+
name: 'EsFormEditor',
|
|
169
|
+
|
|
170
|
+
components: {
|
|
171
|
+
EsForm,
|
|
172
|
+
EsFormItem,
|
|
173
|
+
EsInput,
|
|
174
|
+
EsButton,
|
|
175
|
+
EsDialog,
|
|
176
|
+
EsScrollbar,
|
|
177
|
+
PropsPanel,
|
|
178
|
+
EsCodeEditor
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
props: {
|
|
182
|
+
// 表单控件:左侧控件面板
|
|
183
|
+
components: {
|
|
184
|
+
type: Array,
|
|
185
|
+
default: () => librarys
|
|
186
|
+
},
|
|
187
|
+
// 表字段名,当为Object时对应多表
|
|
188
|
+
fields: [Array, Object],
|
|
189
|
+
formProps: {
|
|
190
|
+
type: Array,
|
|
191
|
+
default: () => props
|
|
192
|
+
},
|
|
193
|
+
// es-form 表单级配置(columns/labelWidth 等,支持 .sync)
|
|
194
|
+
configs: {
|
|
195
|
+
type: Object,
|
|
196
|
+
default: () => ({})
|
|
197
|
+
},
|
|
198
|
+
// 表单内容
|
|
199
|
+
contents: {
|
|
200
|
+
type: Array,
|
|
201
|
+
default: () => []
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
data() {
|
|
206
|
+
return {
|
|
207
|
+
keyword: '',
|
|
208
|
+
selectedId: null,
|
|
209
|
+
showJsonEdit: false,
|
|
210
|
+
// JSON 弹窗编辑器内容(打开时快照,避免画布变更覆盖用户编辑)
|
|
211
|
+
jsonText: '',
|
|
212
|
+
// 铅笔按钮打开时的光标目标:{ fieldId, path },editor ready 后消费
|
|
213
|
+
pendingCursor: null,
|
|
214
|
+
// 表单级配置本地副本:父级未用 .sync 时画布/面板也能实时生效
|
|
215
|
+
localFormConfig: this.configs || {},
|
|
216
|
+
// 画布内控件实时预览取值(不入结构 JSON)
|
|
217
|
+
previewModels: {},
|
|
218
|
+
// 列宽拖拽会话:{ field, itemEl, direction, startX, startWidth, colWidth, columns, lastWidth, moved },null 为未拖拽
|
|
219
|
+
resizeState: null,
|
|
220
|
+
// 内部字段列表:与 value 同引用(父级回传时避免克隆打断面板输入)
|
|
221
|
+
forms: this.contents || []
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
computed: {
|
|
226
|
+
// 注册表拍平后喂给自有 props-panel(含 EsForm 自身定义,供表单属性面板用);
|
|
227
|
+
// components 既支持拍平定义数组(本地 components.js 形态),也兼容分组形态;
|
|
228
|
+
// 表单设计器内任何控件都默认是表单字段(es-form-item 包裹),不按包裹清单过滤
|
|
229
|
+
flatRegistry() {
|
|
230
|
+
const list = [];
|
|
231
|
+
(this.components || []).forEach((item) => {
|
|
232
|
+
if (item && Array.isArray(item.children)) {
|
|
233
|
+
item.children.forEach((comp) => list.push(comp));
|
|
234
|
+
} else if (item) {
|
|
235
|
+
list.push(item);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
return list;
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
// 左侧控件面板:注册表分组拍平(分组标题 + 控件项),
|
|
242
|
+
// 支持关键字搜索;单层结构便于 Sortable 直接拖拽子项
|
|
243
|
+
paletteEntries() {
|
|
244
|
+
const kw = this.keyword.trim().toLowerCase();
|
|
245
|
+
const entries = [];
|
|
246
|
+
librarys.forEach((group) => {
|
|
247
|
+
const comps = (group.children || []).filter((comp) => {
|
|
248
|
+
if (!kw) return true;
|
|
249
|
+
return (
|
|
250
|
+
String(comp.name).toLowerCase().includes(kw) ||
|
|
251
|
+
String(comp.description || '')
|
|
252
|
+
.toLowerCase()
|
|
253
|
+
.includes(kw)
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
if (!comps.length) return;
|
|
257
|
+
entries.push({
|
|
258
|
+
type: 'group',
|
|
259
|
+
key: 'group-' + group.label,
|
|
260
|
+
text: group.label
|
|
261
|
+
});
|
|
262
|
+
comps.forEach((comp) => {
|
|
263
|
+
entries.push({
|
|
264
|
+
type: 'comp',
|
|
265
|
+
key: comp.name,
|
|
266
|
+
name: comp.name,
|
|
267
|
+
text: comp.description || comp.name
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
return entries;
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
// 表单属性面板定义:formProps(表单级 props 定义数组)包成 EsForm 定义条目,
|
|
275
|
+
// 供 props-panel 按 nodeName 查找(librarys 中已不含 EsForm 定义)
|
|
276
|
+
formRegistry() {
|
|
277
|
+
return [
|
|
278
|
+
{
|
|
279
|
+
name: 'EsForm',
|
|
280
|
+
props: Array.isArray(this.formProps) ? this.formProps : []
|
|
281
|
+
}
|
|
282
|
+
];
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
selectedField() {
|
|
286
|
+
return this.forms.find((f) => f.id === this.selectedId) || null;
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
// fields 有传入值(表字段名,Array 或 Object 多表)时 name 改下拉的选项清单;
|
|
290
|
+
// 未传入或空给 null,面板保持文本输入
|
|
291
|
+
fieldNameOptions() {
|
|
292
|
+
const fields = this.fields;
|
|
293
|
+
if (!fields || typeof fields !== 'object') return null;
|
|
294
|
+
const list = [];
|
|
295
|
+
const push = (v) => {
|
|
296
|
+
const s = String(v);
|
|
297
|
+
if (s && !list.includes(s)) list.push(s);
|
|
298
|
+
};
|
|
299
|
+
const walk = (val) => {
|
|
300
|
+
if (Array.isArray(val)) {
|
|
301
|
+
val.forEach(walk);
|
|
302
|
+
} else if (val && typeof val === 'object') {
|
|
303
|
+
Object.keys(val).forEach(push);
|
|
304
|
+
} else {
|
|
305
|
+
push(val);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
if (Array.isArray(fields)) {
|
|
309
|
+
fields.forEach(walk);
|
|
310
|
+
} else {
|
|
311
|
+
Object.keys(fields).forEach((k) => walk(fields[k]));
|
|
312
|
+
}
|
|
313
|
+
return list.length ? list.map((n) => ({ name: n, value: n })) : null;
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
// 表单属性面板的模型:复用注册表 EsForm 定义
|
|
317
|
+
formModel() {
|
|
318
|
+
return { nodeName: 'EsForm', props: this.localFormConfig };
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
// 有效列数:表单配置取值优先;缺键时回落注册表 EsForm columns 默认值
|
|
322
|
+
// (属性面板缺键展示的也是该默认值,两边口径一致),最后兜底 1
|
|
323
|
+
formColumns() {
|
|
324
|
+
const raw = this.localFormConfig.columns;
|
|
325
|
+
if (raw !== undefined && raw !== '') {
|
|
326
|
+
return Number(raw) || 1;
|
|
327
|
+
}
|
|
328
|
+
return Number(this.defaultForProp('EsForm', 'columns')) || 1;
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
// 字段面板模型:props 指向字段节点本身(取值平铺在节点上),复用 props-panel 读写通道
|
|
332
|
+
fieldPanelModel() {
|
|
333
|
+
return this.selectedField
|
|
334
|
+
? { nodeName: this.selectedField.nodeName, props: this.selectedField }
|
|
335
|
+
: null;
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
// JSON 编辑弹窗的模型:{ configs, forms } 全量结构
|
|
339
|
+
jsonModel() {
|
|
340
|
+
return { configs: this.localFormConfig, forms: this.forms };
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
// 源码编辑的合成 EsForm 节点:props 承载表单级配置,children 为字段列表
|
|
344
|
+
// (生成器读 node.props 输出绑定,平铺取值在此映射回 props 供生成用)
|
|
345
|
+
formNode() {
|
|
346
|
+
return {
|
|
347
|
+
id: 'form-root',
|
|
348
|
+
nodeName: 'EsForm',
|
|
349
|
+
props: this.localFormConfig,
|
|
350
|
+
children: (this.forms || []).map((field) => ({
|
|
351
|
+
...field,
|
|
352
|
+
props: this.fieldProps(field)
|
|
353
|
+
}))
|
|
354
|
+
};
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
// 源码编辑的合成页面记录(generateVueSource 根标签固定 div,EsForm 作唯一子节点)
|
|
358
|
+
sourcePage() {
|
|
359
|
+
return {
|
|
360
|
+
id: 'form-editor',
|
|
361
|
+
type: 'page',
|
|
362
|
+
name: '',
|
|
363
|
+
children: [this.formNode]
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
watch: {
|
|
369
|
+
// 父级替换表单内容时同步(同引用回传不处理,避免打断面板输入)
|
|
370
|
+
contents(val) {
|
|
371
|
+
if (val && val !== this.forms) {
|
|
372
|
+
this.forms = val;
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
// 父级外部更新表单配置时同步本地副本
|
|
376
|
+
configs(val) {
|
|
377
|
+
if (val && val !== this.localFormConfig) {
|
|
378
|
+
this.localFormConfig = val;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
mounted() {
|
|
384
|
+
this.initSortable();
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
beforeDestroy() {
|
|
388
|
+
this.destroySortable();
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
methods: {
|
|
392
|
+
initSortable() {
|
|
393
|
+
const palette = this.$refs.palette;
|
|
394
|
+
// ref="forms" 挂在 es-form 组件上,Sortable 需要其根 DOM 元素
|
|
395
|
+
const forms = this.$refs.forms && this.$refs.forms.$el;
|
|
396
|
+
|
|
397
|
+
if (palette) {
|
|
398
|
+
this.paletteSortable = new Sortable(palette, {
|
|
399
|
+
group: { name: 'es-form-editor', pull: 'clone', put: false },
|
|
400
|
+
sort: false,
|
|
401
|
+
animation: 150,
|
|
402
|
+
draggable: '.es-form-editor__palette-item',
|
|
403
|
+
fallbackOnBody: true,
|
|
404
|
+
filter: '.es-form-editor__palette-group'
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (forms) {
|
|
409
|
+
this.fieldsSortable = new Sortable(forms, {
|
|
410
|
+
group: { name: 'es-form-editor', pull: false, put: true },
|
|
411
|
+
animation: 150,
|
|
412
|
+
draggable: '.es-form-editor__field',
|
|
413
|
+
ghostClass: 'is-ghost',
|
|
414
|
+
fallbackOnBody: true,
|
|
415
|
+
// 删除按钮/列宽手柄上不启动排序拖拽(否则原生 DnD 接管指针,pointermove 断流)
|
|
416
|
+
filter:
|
|
417
|
+
'.es-form-editor__resize-handle, .es-form-editor__field-remove',
|
|
418
|
+
onAdd: this.handleDropAdd,
|
|
419
|
+
onEnd: this.handleReorder
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
destroySortable() {
|
|
425
|
+
if (this.paletteSortable) {
|
|
426
|
+
this.paletteSortable.destroy();
|
|
427
|
+
this.paletteSortable = null;
|
|
428
|
+
}
|
|
429
|
+
if (this.fieldsSortable) {
|
|
430
|
+
this.fieldsSortable.destroy();
|
|
431
|
+
this.fieldsSortable = null;
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* 面板拖入:移除 Sortable 克隆的 DOM(列表由 Vue 渲染),
|
|
437
|
+
* 按落点索引生成字段契约节点插入 forms
|
|
438
|
+
*/
|
|
439
|
+
handleDropAdd(evt) {
|
|
440
|
+
const nodeName = evt.item.getAttribute('data-nodename');
|
|
441
|
+
if (evt.item.parentNode) {
|
|
442
|
+
evt.item.parentNode.removeChild(evt.item);
|
|
443
|
+
}
|
|
444
|
+
if (!nodeName) return;
|
|
445
|
+
|
|
446
|
+
const pascal = this.pascal(nodeName);
|
|
447
|
+
const def = this.flatRegistry.find((c) => c.name === pascal);
|
|
448
|
+
const description = (def && def.description) || pascal;
|
|
449
|
+
const field = {
|
|
450
|
+
description,
|
|
451
|
+
id: `${uuid()}`,
|
|
452
|
+
nodeName: pascal,
|
|
453
|
+
label: description,
|
|
454
|
+
// fields 有传入时 name 由下拉选择,不自动生成默认字段键
|
|
455
|
+
name: this.fieldNameOptions
|
|
456
|
+
? ''
|
|
457
|
+
: this.buildFieldName(this.forms, pascal)
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// 空列表时 DOM 里还有占位提示,索引按 forms 长度收敛
|
|
461
|
+
const index = Math.min(evt.newIndex, this.forms.length);
|
|
462
|
+
this.forms.splice(index, 0, field);
|
|
463
|
+
this.selectField(field);
|
|
464
|
+
this.notifyChange();
|
|
465
|
+
},
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* 画布内排序:字段嵌套在 es-form 内层包裹中,
|
|
469
|
+
* 以 querySelectorAll 取 .es-form-editor__field 的 DOM data-id 顺序重建 forms
|
|
470
|
+
*/
|
|
471
|
+
handleReorder() {
|
|
472
|
+
const form = this.$refs.forms;
|
|
473
|
+
const container = form && form.$el;
|
|
474
|
+
if (!container) return;
|
|
475
|
+
|
|
476
|
+
const domIds = Array.from(
|
|
477
|
+
container.querySelectorAll('.es-form-editor__field')
|
|
478
|
+
)
|
|
479
|
+
.map((el) => el.getAttribute('data-id'))
|
|
480
|
+
.filter(Boolean);
|
|
481
|
+
const curIds = this.forms.map((f) => f.id);
|
|
482
|
+
if (curIds.join(',') === domIds.join(',')) return;
|
|
483
|
+
|
|
484
|
+
const byId = {};
|
|
485
|
+
this.forms.forEach((f) => {
|
|
486
|
+
byId[f.id] = f;
|
|
487
|
+
});
|
|
488
|
+
this.forms = domIds.map((id) => byId[id]).filter(Boolean);
|
|
489
|
+
this.notifyChange();
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
selectField(field) {
|
|
493
|
+
this.selectedId = field ? field.id : null;
|
|
494
|
+
},
|
|
495
|
+
|
|
496
|
+
handlePreviewInput(field, val) {
|
|
497
|
+
this.$set(this.previewModels, field.name, val);
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
removeField(index) {
|
|
501
|
+
const removed = this.forms[index];
|
|
502
|
+
this.forms.splice(index, 1);
|
|
503
|
+
if (removed && removed.id === this.selectedId) {
|
|
504
|
+
this.selectedId = null;
|
|
505
|
+
}
|
|
506
|
+
this.notifyChange();
|
|
507
|
+
},
|
|
508
|
+
|
|
509
|
+
/** es-input / esInput → EsInput */
|
|
510
|
+
pascal(name) {
|
|
511
|
+
return String(name).replace(/(^|-)(\w)/g, (m, sep, c) => c.toUpperCase());
|
|
512
|
+
},
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* 字段键:组件名去 Es 前缀转驼峰作前缀(EsInput → input、EsDatePicker → datePicker),
|
|
516
|
+
* 从 1 递增查重(设计器本地生成,不依赖 lowcode 包裹契约的 form-controls)
|
|
517
|
+
*/
|
|
518
|
+
buildFieldName(siblings, nodeName) {
|
|
519
|
+
const prefix = String(nodeName || 'field')
|
|
520
|
+
.replace(/^Es/, '')
|
|
521
|
+
.replace(/^(.)/, (m, c) => c.toLowerCase());
|
|
522
|
+
let idx = 1;
|
|
523
|
+
let name = prefix + idx;
|
|
524
|
+
while ((siblings || []).some((f) => f && f.name === name)) {
|
|
525
|
+
idx += 1;
|
|
526
|
+
name = prefix + idx;
|
|
527
|
+
}
|
|
528
|
+
return name;
|
|
529
|
+
},
|
|
530
|
+
|
|
531
|
+
/** 字段节点上的控件 prop 取值:除元信息键外的全部键(平铺契约) */
|
|
532
|
+
fieldProps(field) {
|
|
533
|
+
const rest = {};
|
|
534
|
+
Object.keys(field || {}).forEach((key) => {
|
|
535
|
+
if (FIELD_META.includes(key)) return;
|
|
536
|
+
rest[key] = field[key];
|
|
537
|
+
});
|
|
538
|
+
return rest;
|
|
539
|
+
},
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* 字段占列数(columns 列制,1..columns):显式 span 优先并 clamp 到 columns,
|
|
543
|
+
* 缺省占 1 列;form-item itemStyle 按 (100/columns)*span 渲染宽度
|
|
544
|
+
*/
|
|
545
|
+
spanOf(field) {
|
|
546
|
+
const columns = Math.max(Number(this.formColumns) || 1, 1);
|
|
547
|
+
const explicit = Number(field && field.span);
|
|
548
|
+
if (explicit) return Math.min(Math.max(explicit, 1), columns);
|
|
549
|
+
return 1;
|
|
550
|
+
},
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* 列宽拖拽起手势:记录字段根元素、起始指针/宽度、单列宽与方向。
|
|
554
|
+
* 左手柄向左拖、右手柄向右拖均增大占宽;拖动中直接改像素宽度(丝滑),
|
|
555
|
+
* 松手才按列取整 snap 成 span(1..columns)
|
|
556
|
+
*/
|
|
557
|
+
handleResizeStart(event, field, direction) {
|
|
558
|
+
event.preventDefault();
|
|
559
|
+
event.stopPropagation();
|
|
560
|
+
const formEl = this.$refs.forms && this.$refs.forms.$el;
|
|
561
|
+
if (!formEl) return;
|
|
562
|
+
const columns = Math.max(Number(this.formColumns) || 1, 1);
|
|
563
|
+
const itemEl =
|
|
564
|
+
event.currentTarget && event.currentTarget.closest
|
|
565
|
+
? event.currentTarget.closest('.es-form-editor__field')
|
|
566
|
+
: null;
|
|
567
|
+
const colWidth = formEl.clientWidth / columns;
|
|
568
|
+
this.resizeState = {
|
|
569
|
+
field,
|
|
570
|
+
itemEl,
|
|
571
|
+
direction: direction === 'left' ? 'left' : 'right',
|
|
572
|
+
startX: event.clientX,
|
|
573
|
+
startWidth: itemEl ? itemEl.getBoundingClientRect().width : colWidth,
|
|
574
|
+
colWidth,
|
|
575
|
+
columns,
|
|
576
|
+
lastWidth: 0,
|
|
577
|
+
moved: false
|
|
578
|
+
};
|
|
579
|
+
document.addEventListener('pointermove', this.handleResizeMove);
|
|
580
|
+
document.addEventListener('pointerup', this.handleResizeEnd);
|
|
581
|
+
},
|
|
582
|
+
|
|
583
|
+
handleResizeMove(event) {
|
|
584
|
+
const st = this.resizeState;
|
|
585
|
+
if (!st || !st.itemEl || !st.colWidth) return;
|
|
586
|
+
const offset = event.clientX - st.startX;
|
|
587
|
+
if (offset) st.moved = true;
|
|
588
|
+
const sign = st.direction === 'left' ? -1 : 1;
|
|
589
|
+
// 实时像素宽度,夹在 [单列, 整行] 之间;此阶段不写 span,保证拖动丝滑
|
|
590
|
+
const max = st.colWidth * st.columns;
|
|
591
|
+
const width = Math.min(
|
|
592
|
+
Math.max(st.startWidth + sign * offset, st.colWidth),
|
|
593
|
+
max
|
|
594
|
+
);
|
|
595
|
+
st.lastWidth = width;
|
|
596
|
+
st.itemEl.style.width = `${width}px`;
|
|
597
|
+
},
|
|
598
|
+
|
|
599
|
+
handleResizeEnd() {
|
|
600
|
+
document.removeEventListener('pointermove', this.handleResizeMove);
|
|
601
|
+
document.removeEventListener('pointerup', this.handleResizeEnd);
|
|
602
|
+
const st = this.resizeState;
|
|
603
|
+
this.resizeState = null;
|
|
604
|
+
if (!st) return;
|
|
605
|
+
if (st.moved && st.itemEl && st.colWidth) {
|
|
606
|
+
// 松手按列取整:span ∈ [1, columns],绝不会超过列数
|
|
607
|
+
const width = st.lastWidth || st.startWidth;
|
|
608
|
+
const span = Math.min(
|
|
609
|
+
Math.max(Math.round(width / st.colWidth), 1),
|
|
610
|
+
st.columns
|
|
611
|
+
);
|
|
612
|
+
// 内联宽度收敛到目标列宽百分比,与 form-item span 渲染同源,避免闪动
|
|
613
|
+
st.itemEl.style.width = `${(100 / st.columns) * span}%`;
|
|
614
|
+
if (span > 1) {
|
|
615
|
+
if (span !== Number(st.field.span)) this.$set(st.field, 'span', span);
|
|
616
|
+
} else if (st.field.span !== undefined) {
|
|
617
|
+
// 占 1 列即默认,删键不落库
|
|
618
|
+
this.$delete(st.field, 'span');
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
// 拖拽结束后的 click 会冒泡到字段/画布触发选中切换,捕获阶段吞掉
|
|
622
|
+
if (st.moved) {
|
|
623
|
+
const swallow = (e) => {
|
|
624
|
+
e.stopPropagation();
|
|
625
|
+
document.removeEventListener('click', swallow, true);
|
|
626
|
+
};
|
|
627
|
+
document.addEventListener('click', swallow, true);
|
|
628
|
+
}
|
|
629
|
+
this.notifyChange();
|
|
630
|
+
},
|
|
631
|
+
|
|
632
|
+
clearAll() {
|
|
633
|
+
this.forms.splice(0, this.forms.length);
|
|
634
|
+
this.selectedId = null;
|
|
635
|
+
this.previewModels = {};
|
|
636
|
+
this.notifyChange();
|
|
637
|
+
},
|
|
638
|
+
|
|
639
|
+
// props 面板回写:prop 取值平铺写回字段节点(元信息键不动);
|
|
640
|
+
// payload 已承继自定义键,未出现的键即本轮恢复默认值,删除
|
|
641
|
+
handleFieldProps(payload) {
|
|
642
|
+
const field = this.selectedField;
|
|
643
|
+
if (!field || !payload) return;
|
|
644
|
+
const values = payload.props || {};
|
|
645
|
+
Object.keys(field).forEach((key) => {
|
|
646
|
+
if (FIELD_META.includes(key) || values[key] !== undefined) return;
|
|
647
|
+
this.$delete(field, key);
|
|
648
|
+
});
|
|
649
|
+
Object.keys(values).forEach((key) => {
|
|
650
|
+
// label/name 等节点级字段也允许面板直改(画布 es-form-item 即时同步)
|
|
651
|
+
this.$set(field, key, values[key]);
|
|
652
|
+
});
|
|
653
|
+
this.notifyChange();
|
|
654
|
+
},
|
|
655
|
+
|
|
656
|
+
// 表单级 props 回写(columns/labelWidth 等)
|
|
657
|
+
handleFormProps(payload) {
|
|
658
|
+
if (!payload) return;
|
|
659
|
+
this.localFormConfig = payload.props || {};
|
|
660
|
+
this.$emit('update:configs', this.localFormConfig);
|
|
661
|
+
this.$emit('form-config-change', this.localFormConfig);
|
|
662
|
+
},
|
|
663
|
+
|
|
664
|
+
/** 工具栏保存:抛出字段结构 + 表单配置 + 生成的 Vue 源码 */
|
|
665
|
+
handleSave() {
|
|
666
|
+
this.$emit('save', {
|
|
667
|
+
forms: this.forms,
|
|
668
|
+
configs: this.localFormConfig,
|
|
669
|
+
source: generateVueSource(this.sourcePage)
|
|
670
|
+
});
|
|
671
|
+
},
|
|
672
|
+
|
|
673
|
+
/** 工具栏 JSON 编辑:全量结构快照开弹窗 */
|
|
674
|
+
openJsonEdit() {
|
|
675
|
+
this.jsonText = JSON.stringify(this.jsonModel, null, 2);
|
|
676
|
+
this.pendingCursor = null;
|
|
677
|
+
this.showJsonEdit = true;
|
|
678
|
+
},
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* 属性面板铅笔按钮:同样打开全量 JSON 弹窗,
|
|
682
|
+
* 缺键先按注册表默认值插入,editor ready 后光标定位(并选中取值)到对应键
|
|
683
|
+
*/
|
|
684
|
+
openPropJson(scope, meta) {
|
|
685
|
+
const key = meta && meta.cursorField;
|
|
686
|
+
if (!key) return;
|
|
687
|
+
const model = JSON.parse(JSON.stringify(this.jsonModel));
|
|
688
|
+
let fieldId = null;
|
|
689
|
+
let path;
|
|
690
|
+
if (scope === 'field') {
|
|
691
|
+
const field = (model.forms || []).find((f) => f.id === this.selectedId);
|
|
692
|
+
if (!field) return;
|
|
693
|
+
fieldId = field.id;
|
|
694
|
+
if (field[key] === undefined) {
|
|
695
|
+
field[key] = this.defaultForProp(field.nodeName, key);
|
|
696
|
+
}
|
|
697
|
+
path = [key];
|
|
698
|
+
} else {
|
|
699
|
+
if (!model.configs || typeof model.configs !== 'object') {
|
|
700
|
+
model.configs = {};
|
|
701
|
+
}
|
|
702
|
+
if (model.configs[key] === undefined) {
|
|
703
|
+
model.configs[key] = this.defaultForProp('EsForm', key);
|
|
704
|
+
}
|
|
705
|
+
path = ['configs', key];
|
|
706
|
+
}
|
|
707
|
+
this.jsonText = JSON.stringify(model, null, 2);
|
|
708
|
+
this.pendingCursor = { fieldId, path };
|
|
709
|
+
this.showJsonEdit = true;
|
|
710
|
+
},
|
|
711
|
+
|
|
712
|
+
/** 注册表 prop 默认值(缺键插入兜底),无定义给空串 */
|
|
713
|
+
defaultForProp(nodeName, key) {
|
|
714
|
+
const pool =
|
|
715
|
+
nodeName === 'EsForm' ? this.formRegistry : this.flatRegistry;
|
|
716
|
+
const comp = pool.find((c) => c.name === nodeName);
|
|
717
|
+
const def = comp && (comp.props || []).find((p) => p.name === key);
|
|
718
|
+
return def && def.default !== undefined ? def.default : '';
|
|
719
|
+
},
|
|
720
|
+
|
|
721
|
+
/** editor ready:消费铅笔按钮留下的光标目标 */
|
|
722
|
+
handleEditorReady() {
|
|
723
|
+
if (!this.pendingCursor) return;
|
|
724
|
+
const cursor = this.pendingCursor;
|
|
725
|
+
this.pendingCursor = null;
|
|
726
|
+
this.positionCursor(cursor);
|
|
727
|
+
},
|
|
728
|
+
|
|
729
|
+
/** 在 monaco 中定位目标键取值区间:选中 + 居中 + 聚焦 */
|
|
730
|
+
positionCursor(cursor) {
|
|
731
|
+
const wrap = this.$refs.jsonEditorRef;
|
|
732
|
+
const editor = wrap && wrap.editor;
|
|
733
|
+
if (!editor || !editor.getModel) return;
|
|
734
|
+
const node = this.locateJsonNode(editor.getValue(), cursor);
|
|
735
|
+
if (!node) return;
|
|
736
|
+
const model = editor.getModel();
|
|
737
|
+
const start = model.getPositionAt(node.start);
|
|
738
|
+
const end = model.getPositionAt(node.end);
|
|
739
|
+
editor.setSelection({
|
|
740
|
+
startLineNumber: start.lineNumber,
|
|
741
|
+
startColumn: start.column,
|
|
742
|
+
endLineNumber: end.lineNumber,
|
|
743
|
+
endColumn: end.column
|
|
744
|
+
});
|
|
745
|
+
editor.revealLineInCenter(start.lineNumber);
|
|
746
|
+
editor.focus();
|
|
747
|
+
},
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* 按 { fieldId, path } 定位取值节点:根为 { configs, forms },
|
|
751
|
+
* fieldId 存在时先定位 forms 中该 id 的字段对象,再沿 path 逐键下钻
|
|
752
|
+
*/
|
|
753
|
+
locateJsonNode(text, cursor) {
|
|
754
|
+
let root;
|
|
755
|
+
try {
|
|
756
|
+
root = this.scanJsonValue(text, text.indexOf('{'));
|
|
757
|
+
} catch (e) {
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
if (!root || root.type !== 'object') return null;
|
|
761
|
+
let obj = root;
|
|
762
|
+
if (cursor.fieldId) {
|
|
763
|
+
const fieldsNode = root.keys && root.keys.forms;
|
|
764
|
+
if (!fieldsNode || !fieldsNode.items) return null;
|
|
765
|
+
obj =
|
|
766
|
+
fieldsNode.items.find(
|
|
767
|
+
(it) => it.keys && it.keys.id && it.keys.id.value === cursor.fieldId
|
|
768
|
+
) || null;
|
|
769
|
+
if (!obj) return null;
|
|
770
|
+
}
|
|
771
|
+
let cur = obj;
|
|
772
|
+
for (let i = 0; i < cursor.path.length; i += 1) {
|
|
773
|
+
const next = cur.keys && cur.keys[cursor.path[i]];
|
|
774
|
+
if (!next) return null;
|
|
775
|
+
if (i === cursor.path.length - 1) return next;
|
|
776
|
+
cur = next;
|
|
777
|
+
}
|
|
778
|
+
return null;
|
|
779
|
+
},
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* 字符级 JSON 扫描器(字符串感知,串内花括号不干扰层级):
|
|
783
|
+
* 对象节点带 keys(键名 → 值节点)、数组节点带 items;
|
|
784
|
+
* start/end 为取值字符区间(供光标定位)
|
|
785
|
+
*/
|
|
786
|
+
scanJsonValue(text, idx) {
|
|
787
|
+
const skipWs = (i) => {
|
|
788
|
+
while (i < text.length && /\s/.test(text[i])) i += 1;
|
|
789
|
+
return i;
|
|
790
|
+
};
|
|
791
|
+
const parseString = (i) => {
|
|
792
|
+
// text[i] === '"';仅用于键名/id 比对,转义按原样拼接
|
|
793
|
+
let j = i + 1;
|
|
794
|
+
let raw = '';
|
|
795
|
+
while (j < text.length) {
|
|
796
|
+
const ch = text[j];
|
|
797
|
+
if (ch === '\\') {
|
|
798
|
+
raw += text[j + 1];
|
|
799
|
+
j += 2;
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
if (ch === '"') break;
|
|
803
|
+
raw += ch;
|
|
804
|
+
j += 1;
|
|
805
|
+
}
|
|
806
|
+
return { value: raw, end: j + 1 };
|
|
807
|
+
};
|
|
808
|
+
const start = skipWs(idx);
|
|
809
|
+
const ch = text[start];
|
|
810
|
+
if (ch === '{') {
|
|
811
|
+
const keys = {};
|
|
812
|
+
let i = start + 1;
|
|
813
|
+
for (;;) {
|
|
814
|
+
i = skipWs(i);
|
|
815
|
+
if (text[i] === '}') {
|
|
816
|
+
return { type: 'object', keys, start, end: i + 1 };
|
|
817
|
+
}
|
|
818
|
+
const key = parseString(i);
|
|
819
|
+
i = skipWs(key.end) + 1; // 冒号
|
|
820
|
+
const val = this.scanJsonValue(text, i);
|
|
821
|
+
keys[key.value] = val;
|
|
822
|
+
i = skipWs(val.end);
|
|
823
|
+
if (text[i] === ',') {
|
|
824
|
+
i += 1;
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
return { type: 'object', keys, start, end: i + 1 };
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (ch === '[') {
|
|
831
|
+
const items = [];
|
|
832
|
+
let i = start + 1;
|
|
833
|
+
for (;;) {
|
|
834
|
+
i = skipWs(i);
|
|
835
|
+
if (text[i] === ']') {
|
|
836
|
+
return { type: 'array', items, start, end: i + 1 };
|
|
837
|
+
}
|
|
838
|
+
const val = this.scanJsonValue(text, i);
|
|
839
|
+
items.push(val);
|
|
840
|
+
i = skipWs(val.end);
|
|
841
|
+
if (text[i] === ',') {
|
|
842
|
+
i += 1;
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
return { type: 'array', items, start, end: i + 1 };
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (ch === '"') {
|
|
849
|
+
const str = parseString(start);
|
|
850
|
+
return { type: 'string', value: str.value, start, end: str.end };
|
|
851
|
+
}
|
|
852
|
+
let j = start;
|
|
853
|
+
while (j < text.length && ',]}'.indexOf(text[j]) === -1) j += 1;
|
|
854
|
+
const raw = text.slice(start, j).trim();
|
|
855
|
+
let value = raw;
|
|
856
|
+
if (raw === 'true') value = true;
|
|
857
|
+
else if (raw === 'false') value = false;
|
|
858
|
+
else if (raw === 'null') value = null;
|
|
859
|
+
else if (/^-?\d+(\.\d+)?$/.test(raw)) value = Number(raw);
|
|
860
|
+
return { type: 'literal', value, start, end: j };
|
|
861
|
+
},
|
|
862
|
+
|
|
863
|
+
/** Ctrl+S 保存:解析回写后弹窗保持打开并标记已保存(编辑器状态栏显示 ✓ 已保存) */
|
|
864
|
+
handleJsonSave(value, errors) {
|
|
865
|
+
const ref = this.$refs.jsonEditorRef;
|
|
866
|
+
if (errors && errors.length) {
|
|
867
|
+
if (ref) ref.setErrors(errors);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
let parsed;
|
|
871
|
+
try {
|
|
872
|
+
parsed = JSON.parse(value);
|
|
873
|
+
} catch (e) {
|
|
874
|
+
if (ref) {
|
|
875
|
+
ref.setErrors([{ message: 'JSON 格式错误', startLineNumber: 1 }]);
|
|
876
|
+
}
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
const config =
|
|
880
|
+
parsed &&
|
|
881
|
+
parsed.configs &&
|
|
882
|
+
typeof parsed.configs === 'object' &&
|
|
883
|
+
!Array.isArray(parsed.configs)
|
|
884
|
+
? parsed.configs
|
|
885
|
+
: this.localFormConfig;
|
|
886
|
+
const list = parsed && Array.isArray(parsed.forms) ? parsed.forms : [];
|
|
887
|
+
const forms = this.normalizeFields(list);
|
|
888
|
+
|
|
889
|
+
this.localFormConfig = config;
|
|
890
|
+
this.$emit('update:configs', config);
|
|
891
|
+
this.$emit('form-config-change', config);
|
|
892
|
+
this.forms.splice(0, this.forms.length, ...forms);
|
|
893
|
+
if (!this.forms.some((f) => f.id === this.selectedId)) {
|
|
894
|
+
this.selectedId = null;
|
|
895
|
+
}
|
|
896
|
+
this.notifyChange();
|
|
897
|
+
if (ref) ref.markSaved();
|
|
898
|
+
},
|
|
899
|
+
|
|
900
|
+
/** 关闭 JSON 弹窗前检查未保存修改(与 lowcode handleOpenClose 同策略) */
|
|
901
|
+
handleJsonEditClose(done) {
|
|
902
|
+
const close = () => {
|
|
903
|
+
this.showJsonEdit = false;
|
|
904
|
+
if (typeof done === 'function') done();
|
|
905
|
+
};
|
|
906
|
+
const ref = this.$refs.jsonEditorRef;
|
|
907
|
+
if (ref && ref.isDirty()) {
|
|
908
|
+
this.$confirm('JSON 已修改但尚未保存,确定关闭吗?', '提示', {
|
|
909
|
+
type: 'warning'
|
|
910
|
+
})
|
|
911
|
+
.then(close)
|
|
912
|
+
.catch(() => {});
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
close();
|
|
916
|
+
},
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* 字段列表契约归一:migrateDirectives 补齐历史形态/孤儿字段键,
|
|
920
|
+
* attrs → props 归并,nodeName 归一 Pascal,缺 id/label/name 兜底
|
|
921
|
+
*/
|
|
922
|
+
normalizeFields(list) {
|
|
923
|
+
const wrapper = { children: (list || []).filter(Boolean) };
|
|
924
|
+
migrateDirectives(wrapper);
|
|
925
|
+
this.mergeAttrsToNode(wrapper);
|
|
926
|
+
const forms = wrapper.children;
|
|
927
|
+
forms.forEach((field) => {
|
|
928
|
+
// 历史 props 嵌套形态迁移:prop 取值拍平到节点后移除 props(平铺契约)
|
|
929
|
+
if (
|
|
930
|
+
field.props &&
|
|
931
|
+
typeof field.props === 'object' &&
|
|
932
|
+
!Array.isArray(field.props)
|
|
933
|
+
) {
|
|
934
|
+
Object.keys(field.props).forEach((key) => {
|
|
935
|
+
if (field[key] === undefined) field[key] = field.props[key];
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
delete field.props;
|
|
939
|
+
field.nodeName = this.pascal(field.nodeName || '');
|
|
940
|
+
if (!field.id) field.id = `${uuid()}`;
|
|
941
|
+
if (field.label === undefined) field.label = field.description || '';
|
|
942
|
+
if (field.name === undefined || field.name === '') {
|
|
943
|
+
field.name = this.fieldNameOptions
|
|
944
|
+
? ''
|
|
945
|
+
: this.buildFieldName(
|
|
946
|
+
forms.filter((f) => f !== field),
|
|
947
|
+
field.nodeName
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
return forms;
|
|
952
|
+
},
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* 生成器把字符串 prop 输出为普通 HTML 属性,解析端会归入 attrs;
|
|
956
|
+
* 回写前递归归并 attrs → 节点平铺键(class/style/id/slot 等结构属性保留在 attrs),
|
|
957
|
+
* 保证字符串取值(placeholder/labelWidth 等)不丢、属性面板可回显
|
|
958
|
+
*/
|
|
959
|
+
mergeAttrsToNode(node) {
|
|
960
|
+
if (!node || typeof node !== 'object') return;
|
|
961
|
+
const RESERVED = ['class', 'style', 'id', 'slot', 'key', 'ref'];
|
|
962
|
+
if (node.attrs && typeof node.attrs === 'object') {
|
|
963
|
+
Object.keys(node.attrs).forEach((key) => {
|
|
964
|
+
if (RESERVED.includes(key)) return;
|
|
965
|
+
if (node[key] === undefined) {
|
|
966
|
+
node[key] = node.attrs[key];
|
|
967
|
+
}
|
|
968
|
+
delete node.attrs[key];
|
|
969
|
+
});
|
|
970
|
+
if (!Object.keys(node.attrs).length) delete node.attrs;
|
|
971
|
+
}
|
|
972
|
+
(node.children || []).forEach((child) => this.mergeAttrsToNode(child));
|
|
973
|
+
},
|
|
974
|
+
|
|
975
|
+
notifyChange() {
|
|
976
|
+
this.$emit('update:contents', this.forms);
|
|
977
|
+
this.$emit('change', this.forms);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
</script>
|