el-form-renderer-vue3 1.0.3 → 1.0.5
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/el-form-renderer-vue3.es.js +464 -457
- package/el-form-renderer-vue3.umd.js +3 -3
- package/femessage/components/render-form-group.vue +48 -0
- package/femessage/components/render-form-item.vue +324 -0
- package/femessage/el-form-renderer.vue +287 -0
- package/femessage/util/CustomComponent.js +28 -0
- package/femessage/util/VNode.js +9 -0
- package/femessage/util/enable-when.js +26 -0
- package/femessage/util/ployfill.js +14 -0
- package/femessage/util/transform-content.js +48 -0
- package/femessage/util/utils.js +126 -0
- package/package.json +1 -1
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<!-- 绑定显示,校验匹配规则字段,label,rules校验规则,attrs(原生属性), -->
|
|
4
|
+
<el-form-item
|
|
5
|
+
v-if="_show"
|
|
6
|
+
:prop="prop"
|
|
7
|
+
:label="typeof data.label === 'string' ? data.label : ''"
|
|
8
|
+
:rules="!readonly && Array.isArray(data.rules) ? data.rules : undefined"
|
|
9
|
+
v-bind="data.attrs"
|
|
10
|
+
class="render-form-item"
|
|
11
|
+
>
|
|
12
|
+
<!-- label插槽 -->
|
|
13
|
+
<template #label>
|
|
14
|
+
<v-node v-if="data.label !== 'string'" :content="data.label" />
|
|
15
|
+
</template>
|
|
16
|
+
<!-- 处理之只读input select -->
|
|
17
|
+
<template v-if="readonly && hasReadonlyContent">
|
|
18
|
+
<el-input
|
|
19
|
+
v-if="data.type === 'input'"
|
|
20
|
+
v-bind="componentProps"
|
|
21
|
+
:modelValue="itemValue"
|
|
22
|
+
readonly
|
|
23
|
+
v-on="listeners"
|
|
24
|
+
/>
|
|
25
|
+
|
|
26
|
+
<div v-else-if="data.type === 'select'">
|
|
27
|
+
{{ multipleValue }}
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
|
30
|
+
<!-- 处理 date-picker,cascader,动态渲染不显示文字 bug-->
|
|
31
|
+
<component
|
|
32
|
+
v-else-if="data.type === 'date-picker' || data.type === 'cascader'"
|
|
33
|
+
ref="customComponent"
|
|
34
|
+
v-bind:is="data.component || `el-${data.type || 'input'}`"
|
|
35
|
+
v-bind="componentProps"
|
|
36
|
+
:modelValue="itemValue"
|
|
37
|
+
:disabled="disabled || componentProps.disabled || readonly"
|
|
38
|
+
v-on="listeners"
|
|
39
|
+
:loading="loading"
|
|
40
|
+
:remote-method="data.remoteMethod || componentProps.remoteMethod || remoteMethod"
|
|
41
|
+
>
|
|
42
|
+
</component>
|
|
43
|
+
<!-- 绑定 模板引用 动态组件 props value值 是否禁用 事件 lodding 远端搜索方法 -->
|
|
44
|
+
<component
|
|
45
|
+
v-else
|
|
46
|
+
ref="customComponent"
|
|
47
|
+
v-bind:is="data.component || `el-${data.type || 'input'}`"
|
|
48
|
+
v-bind="componentProps"
|
|
49
|
+
:modelValue="itemValue"
|
|
50
|
+
:disabled="disabled || componentProps.disabled || readonly"
|
|
51
|
+
v-on="listeners"
|
|
52
|
+
:loading="loading"
|
|
53
|
+
:remote-method="data.remoteMethod || componentProps.remoteMethod || remoteMethod"
|
|
54
|
+
>
|
|
55
|
+
<!-- 插槽处理 选项-->
|
|
56
|
+
<template v-for="(opt, index) in options">
|
|
57
|
+
<el-option
|
|
58
|
+
v-if="data.type === 'select'"
|
|
59
|
+
:key="optionKey(opt) || index"
|
|
60
|
+
v-bind="opt"
|
|
61
|
+
/>
|
|
62
|
+
<el-checkbox-button
|
|
63
|
+
v-if="data.type === 'checkbox-group' && data.style === 'button'"
|
|
64
|
+
:key="opt.value"
|
|
65
|
+
v-bind="opt"
|
|
66
|
+
:label="'value' in opt ? opt.value : opt.label"
|
|
67
|
+
>
|
|
68
|
+
{{ opt.label }}
|
|
69
|
+
</el-checkbox-button>
|
|
70
|
+
<el-checkbox
|
|
71
|
+
v-else-if="data.type === 'checkbox-group' && data.style !== 'button'"
|
|
72
|
+
:key="opt.value"
|
|
73
|
+
v-bind="opt"
|
|
74
|
+
:label="'value' in opt ? opt.value : opt.label"
|
|
75
|
+
>
|
|
76
|
+
{{ opt.label }}
|
|
77
|
+
</el-checkbox>
|
|
78
|
+
<el-radio-button
|
|
79
|
+
v-else-if="data.type === 'radio-group' && data.style === 'button'"
|
|
80
|
+
:key="opt.label"
|
|
81
|
+
v-bind="opt"
|
|
82
|
+
:label="'value' in opt ? opt.value : opt.label"
|
|
83
|
+
>{{ opt.label }}</el-radio-button
|
|
84
|
+
>
|
|
85
|
+
<el-radio
|
|
86
|
+
v-else-if="data.type === 'radio-group' && data.style !== 'button'"
|
|
87
|
+
:key="opt.label"
|
|
88
|
+
v-bind="opt"
|
|
89
|
+
:label="'value' in opt ? opt.value : opt.label"
|
|
90
|
+
>{{ opt.label }}</el-radio
|
|
91
|
+
>
|
|
92
|
+
</template>
|
|
93
|
+
</component>
|
|
94
|
+
</el-form-item>
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|
|
97
|
+
|
|
98
|
+
<script setup>
|
|
99
|
+
import { computed, reactive, inject, nextTick, ref, watch } from "vue";
|
|
100
|
+
import { noop } from "../util/utils";
|
|
101
|
+
import getEnableWhenStatus from "../util/enable-when";
|
|
102
|
+
import _includes from "lodash.includes";
|
|
103
|
+
import _topairs from "lodash.topairs";
|
|
104
|
+
import _frompairs from "lodash.frompairs";
|
|
105
|
+
import _get from "lodash.get";
|
|
106
|
+
// 改用 动态组件 方便获取 ref
|
|
107
|
+
// import CustomComponent from "../util/CustomComponent";
|
|
108
|
+
import VNode from "../util/VNode";
|
|
109
|
+
import axios from "axios";
|
|
110
|
+
let customComponent = ref();
|
|
111
|
+
|
|
112
|
+
let props = defineProps({
|
|
113
|
+
data: Object,
|
|
114
|
+
prop: {
|
|
115
|
+
type: String,
|
|
116
|
+
default(rawProps) {
|
|
117
|
+
return rawProps.data.id;
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
itemValue: {},
|
|
122
|
+
value: Object,
|
|
123
|
+
disabled: Boolean,
|
|
124
|
+
readonly: Boolean,
|
|
125
|
+
options: Array,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// 更新表单方法
|
|
129
|
+
const emit = defineEmits(["updateValue"]);
|
|
130
|
+
let propsInner = reactive({});
|
|
131
|
+
|
|
132
|
+
const loading = ref(false);
|
|
133
|
+
|
|
134
|
+
let dataRef = ref(props.data);
|
|
135
|
+
// 注入一个由祖先组件或整个应用 (通过 app.provide()) 提供的值。
|
|
136
|
+
// 父组件提供 element ui的方法
|
|
137
|
+
let methods = inject("methods");
|
|
138
|
+
// 父组件提供的 更新 options的方法
|
|
139
|
+
let setOptions = inject("setOptions");
|
|
140
|
+
// 是否校验
|
|
141
|
+
const isBlurTrigger =
|
|
142
|
+
props.data.rules &&
|
|
143
|
+
props.data.rules.some((rule) => {
|
|
144
|
+
return rule.required && rule.trigger === "blur";
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// 计算props
|
|
148
|
+
const componentProps = computed(() => ({ ...props.data.el, ...propsInner }));
|
|
149
|
+
// 计算是否为只读 input select
|
|
150
|
+
const hasReadonlyContent = computed(() => ["input", "select"].includes(props.data.type));
|
|
151
|
+
//执行传入的hidden
|
|
152
|
+
const hiddenStatus = computed(() => {
|
|
153
|
+
const hidden = props.data.hidden || (() => false);
|
|
154
|
+
return hidden(props.value, props.data);
|
|
155
|
+
});
|
|
156
|
+
// 弃用
|
|
157
|
+
const enableWhenStatus = computed(() =>
|
|
158
|
+
getEnableWhenStatus(props.data.enableWhen, props.data.value)
|
|
159
|
+
);
|
|
160
|
+
// 处理组件的显示与隐藏
|
|
161
|
+
const _show = computed(() => !hiddenStatus.value && enableWhenStatus.value);
|
|
162
|
+
// 处理事件监听 vue3实现 v-model props:modelValue event:update:modelValue
|
|
163
|
+
const listeners = computed(() => {
|
|
164
|
+
const data = props.data;
|
|
165
|
+
const id = data.id;
|
|
166
|
+
const atChange = data.atChange || noop;
|
|
167
|
+
const on = data.on || {};
|
|
168
|
+
const originOnInput = on.input || noop;
|
|
169
|
+
const originOnChange = on.change || noop;
|
|
170
|
+
const trim = data.trim !== undefined ? data.trim : true;
|
|
171
|
+
|
|
172
|
+
let updateForm = inject("updateForm");
|
|
173
|
+
return {
|
|
174
|
+
..._frompairs(
|
|
175
|
+
_topairs(on).map(([eName, handler]) => [
|
|
176
|
+
eName,
|
|
177
|
+
(...args) => handler(args, updateForm),
|
|
178
|
+
])
|
|
179
|
+
),
|
|
180
|
+
// 手动更新表单数据
|
|
181
|
+
"update:modelValue": (value, ...rest) => {
|
|
182
|
+
if (typeof value === "string" && trim) value = value.trim();
|
|
183
|
+
emit("updateValue", { id, value });
|
|
184
|
+
// FIXME: rules 的 trigger 只写了 blur,依然会在 change 的时候触发校验!
|
|
185
|
+
triggerValidate(id);
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
// el-select 显示对应的 label;(只读)
|
|
190
|
+
const multipleValue = computed(() => {
|
|
191
|
+
const multipleSelectValue =
|
|
192
|
+
_get(props.data, "el.multiple") && Array.isArray(props.itemValue)
|
|
193
|
+
? props.itemValue
|
|
194
|
+
: [props.itemValue];
|
|
195
|
+
return multipleSelectValue
|
|
196
|
+
.map((val) => (props.options.find((op) => op.value === val) || {}).label)
|
|
197
|
+
.join();
|
|
198
|
+
});
|
|
199
|
+
// 处理服务器获取 options
|
|
200
|
+
const makingRequest = (remoteConfig, query) => {
|
|
201
|
+
const isOptionsCase =
|
|
202
|
+
["select", "checkbox-group", "radio-group"].indexOf(props.data.type) > -1;
|
|
203
|
+
const {
|
|
204
|
+
request, //request:用于发起远程请求的函数
|
|
205
|
+
prop = "options", // 处理响应数据时的属性名称,默认为 "options" 默认处理 el-cascader 的情况
|
|
206
|
+
dataPath = "", //用于指定响应数据中的路径,默认为空字符串
|
|
207
|
+
onResponse = (resp) => {
|
|
208
|
+
//响应成功时的回调函数,默认为一个函数,用于处理响应数据。
|
|
209
|
+
if (dataPath) resp = _get(resp, dataPath);
|
|
210
|
+
if (isOptionsCase) {
|
|
211
|
+
return resp?.map((item) => ({
|
|
212
|
+
label: item[label],
|
|
213
|
+
value: item[value],
|
|
214
|
+
}));
|
|
215
|
+
} else {
|
|
216
|
+
return resp;
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
onError = (error) => {
|
|
220
|
+
//发生错误时的回调函数,默认为一个函数,用于打印错误信息并将 loading.value 设置为 false
|
|
221
|
+
console.error(error.message);
|
|
222
|
+
loading.value = false;
|
|
223
|
+
},
|
|
224
|
+
label = "label", //用于选项中的标签属性,默认为 "label"。
|
|
225
|
+
value = "value", //用于选项中的值属性,默认为 "value"
|
|
226
|
+
} = remoteConfig;
|
|
227
|
+
//,表示开始加载数据。
|
|
228
|
+
loading.value = true;
|
|
229
|
+
//Promise.resolve(request(query)):这里使用 Promise.resolve 包装了 request(query),以确保始终返回一个 Promise 对象。
|
|
230
|
+
Promise.resolve(request(query))
|
|
231
|
+
// 当请求成功时执行 onResponse 函数,当请求失败时执行 onError 函数。
|
|
232
|
+
.then(onResponse, onError)
|
|
233
|
+
// .then((resp) => { ... }):在请求完成后,无论成功或失败,都会执行这个 .then 块。在这里,根据 isOptionsCase 的值,对响应数据 resp 进行不同的处理。
|
|
234
|
+
.then((resp) => {
|
|
235
|
+
// 如果 isOptionsCase 为 true,则将响应数据中的每个元素映射为包含 "label" 和 "value" 属性的对象,并将结果传递给 setOptions 函数(如果存在)。
|
|
236
|
+
if (isOptionsCase) {
|
|
237
|
+
setOptions && setOptions(props.prop, resp);
|
|
238
|
+
} else {
|
|
239
|
+
// 如果 isOptionsCase 为 false,则将响应数据存储在 propsInner 中,属性名为 prop。
|
|
240
|
+
propsInner = Object.assign(propsInner, { [prop]: resp });
|
|
241
|
+
}
|
|
242
|
+
// ,表示加载完成。
|
|
243
|
+
loading.value = false;
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
// 监听 是否有 data
|
|
247
|
+
watch(dataRef, (data) => {
|
|
248
|
+
if (!data) {
|
|
249
|
+
throw new Error("data must be an Object.");
|
|
250
|
+
} else if (!data.id) {
|
|
251
|
+
throw new Error("`id` is unvalidated.");
|
|
252
|
+
} else if (!data.type && !data.component) {
|
|
253
|
+
throw new Error("`type` and `component` cannot both be null.");
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
// 处理服务器获取options
|
|
257
|
+
watch(
|
|
258
|
+
/**
|
|
259
|
+
* 这里其实用 remote 处理了两件事。有机会是可以拆分的
|
|
260
|
+
* 1. 基本用法,配置 url 后即可从远程获取某个 prop 注入到组件
|
|
261
|
+
* 2. 针对 select、checkbox-group & radio-group 组件,会直接将 resp 作为 options 处理;label & value 也是直接为这个场景而生的
|
|
262
|
+
*/
|
|
263
|
+
() => props.data.remote?.request,
|
|
264
|
+
(newValue, oldValue) => {
|
|
265
|
+
// 不应该用 watch data.remote,因为对象引用是同一个 https://cn.vuejs.org/v2/api/#vm-watch (估计当初这样写是为了方便)
|
|
266
|
+
// 现改写成:分开处理 remote.request,remote.url
|
|
267
|
+
// 至于为什么判断新旧值相同则返回,是因为 form 的 content 是响应式的,防止用户直接修改 content 其他内容时,导致 remote.request 重新发请求
|
|
268
|
+
|
|
269
|
+
if (!newValue || typeof newValue !== "function" || newValue === oldValue) return;
|
|
270
|
+
|
|
271
|
+
makingRequest(props.data.remote);
|
|
272
|
+
},
|
|
273
|
+
{ immediate: true }
|
|
274
|
+
);
|
|
275
|
+
// 处理服务器获取options
|
|
276
|
+
watch(
|
|
277
|
+
() => props.data.remote?.url,
|
|
278
|
+
(url, oldV) => {
|
|
279
|
+
// 第三个判断条件:防止 url 与 request 同时存在时,发送两次请求
|
|
280
|
+
if (!url || url === oldV || (!oldV && props.data.remote.request)) return;
|
|
281
|
+
const request =
|
|
282
|
+
props.data.remote.request || (() => axios.get(url).then((resp) => resp.data));
|
|
283
|
+
makingRequest(Object.assign({}, props.data.remote, { request }));
|
|
284
|
+
},
|
|
285
|
+
{ immediate: true }
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// 校验表单项目
|
|
289
|
+
const triggerValidate = async (id) => {
|
|
290
|
+
try {
|
|
291
|
+
if (!props.data.rules || !props.data.rules.length) return;
|
|
292
|
+
if (isBlurTrigger) return;
|
|
293
|
+
await nextTick();
|
|
294
|
+
|
|
295
|
+
(await methods) && methods.validateField(id);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.log(error);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
// 远端搜索方法
|
|
301
|
+
const remoteMethod = (query) => {
|
|
302
|
+
if (
|
|
303
|
+
_get(props.data, "type") === "select" &&
|
|
304
|
+
_get(props.data, "el.filterable") &&
|
|
305
|
+
_get(props.data, "el.remote")
|
|
306
|
+
) {
|
|
307
|
+
makingRequest(props.data.remote, query);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
// 初始化 optios key
|
|
311
|
+
const optionKey = (opt) => {
|
|
312
|
+
if (opt.value instanceof Object) {
|
|
313
|
+
if (!props.data.el || !props.data.el.valueKey) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return opt.value[props.data.el.valueKey];
|
|
318
|
+
} else {
|
|
319
|
+
return opt.value;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
// 暴露 element ui 模版引用
|
|
323
|
+
defineExpose({ customComponent });
|
|
324
|
+
</script>
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<el-form ref="myelForm" v-bind="$attrs" :model="value" class="el-form-renderer">
|
|
4
|
+
<template v-for="item in innerContent" :key="item.id">
|
|
5
|
+
<slot :name="`id:${item.id}`" />
|
|
6
|
+
<slot :name="`$id:${item.id}`" />
|
|
7
|
+
|
|
8
|
+
<component
|
|
9
|
+
:is="item.type === GROUP ? RenderFormGroup : RenderFormItem"
|
|
10
|
+
:ref="
|
|
11
|
+
(el) => {
|
|
12
|
+
customComponent[item.id] = el;
|
|
13
|
+
}
|
|
14
|
+
"
|
|
15
|
+
:data="item"
|
|
16
|
+
:value="value"
|
|
17
|
+
:item-value="value[item.id]"
|
|
18
|
+
:disabled="
|
|
19
|
+
disabled ||
|
|
20
|
+
(typeof item.disabled === 'function' ? item.disabled(value) : item.disabled)
|
|
21
|
+
"
|
|
22
|
+
:readonly="readonly || item.readonly"
|
|
23
|
+
:options="options[item.id]"
|
|
24
|
+
@updateValue="updateValue"
|
|
25
|
+
/>
|
|
26
|
+
</template>
|
|
27
|
+
<slot />
|
|
28
|
+
</el-form>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script setup>
|
|
33
|
+
// passive 是用于控制浏览器是否可以在滚动时取消事件的默认行为。当 passive 设置为 true 时,表示事件处理函数不会调用 preventDefault() 来阻止默认的滚动行为。
|
|
34
|
+
//在一些滚动事件处理中,如果事件处理函数中调用了 preventDefault(),浏览器会等待该函数执行完毕后再进行滚动,这可能导致滚动的延迟。通过将 passive 设置为 true,可以告诉浏览器事件处理函数不会调用 preventDefault(),从而使滚动更加流畅。
|
|
35
|
+
import "./util/ployfill";
|
|
36
|
+
import RenderFormGroup from "./components/render-form-group.vue";
|
|
37
|
+
import RenderFormItem from "./components/render-form-item.vue";
|
|
38
|
+
import {
|
|
39
|
+
reactive,
|
|
40
|
+
computed,
|
|
41
|
+
ref,
|
|
42
|
+
watch,
|
|
43
|
+
onMounted,
|
|
44
|
+
nextTick,
|
|
45
|
+
provide,
|
|
46
|
+
getCurrentInstance,
|
|
47
|
+
} from "vue";
|
|
48
|
+
import transformContent from "./util/transform-content";
|
|
49
|
+
import _set from "lodash.set";
|
|
50
|
+
import _isequal from "lodash.isequal";
|
|
51
|
+
import _clonedeep from "lodash.clonedeep";
|
|
52
|
+
import {
|
|
53
|
+
collect,
|
|
54
|
+
mergeValue,
|
|
55
|
+
transformOutputValue,
|
|
56
|
+
transformInputValue,
|
|
57
|
+
correctValue,
|
|
58
|
+
} from "./util/utils";
|
|
59
|
+
let GROUP = "group";
|
|
60
|
+
/**
|
|
61
|
+
* inputFormat 让整个输入机制复杂了很多。value 有以下输入路径:
|
|
62
|
+
* 1. 传入的 form => inputFormat 处理
|
|
63
|
+
* 2. updateForm => inputFormat 处理
|
|
64
|
+
* 3. 但 content 中的 default 没法经过 inputFormat 处理,因为 inputFormat 要接受整个 value 作为参数
|
|
65
|
+
* 4. 组件内部更新 value,不需要走 inputFormat
|
|
66
|
+
*/
|
|
67
|
+
let value = reactive({}); // 表单数据对象
|
|
68
|
+
let options = reactive({});
|
|
69
|
+
let initValue = reactive({});
|
|
70
|
+
let myelForm = ref();
|
|
71
|
+
let methods = {};
|
|
72
|
+
const customComponent = ref([]);
|
|
73
|
+
let emit = defineEmits(["update:FormData"]);
|
|
74
|
+
// 注入 element ui form 方法
|
|
75
|
+
/**
|
|
76
|
+
* 与 element 相同,在 mounted 阶段存储 initValue
|
|
77
|
+
* @see https://github.com/ElemeFE/element/blob/6ec5f8e900ff698cf30e9479d692784af836a108/packages/form/src/form-item.vue#L304
|
|
78
|
+
*/
|
|
79
|
+
onMounted(async () => {
|
|
80
|
+
initValue = _clonedeep(value);
|
|
81
|
+
await nextTick();
|
|
82
|
+
// 检查 myelForm 是否已经初始化
|
|
83
|
+
if (myelForm && myelForm.value) {
|
|
84
|
+
Object.keys(myelForm.value).forEach((item) => {
|
|
85
|
+
// 检查属性是否存在于 methods 对象中
|
|
86
|
+
if (myelForm.value[item] && !(item in methods)) {
|
|
87
|
+
methods[item] = myelForm.value[item];
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 有些组件会 created 阶段更新初始值为合法值,这会触发 validate。目前已知的情况有:
|
|
93
|
+
* - el-select 开启 multiple 时,会更新初始值 undefined 为 []
|
|
94
|
+
* @hack
|
|
95
|
+
*/
|
|
96
|
+
methods.clearValidate();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
let props = defineProps({
|
|
100
|
+
//表单项
|
|
101
|
+
content: {
|
|
102
|
+
type: Array,
|
|
103
|
+
required: true,
|
|
104
|
+
},
|
|
105
|
+
// 禁用
|
|
106
|
+
disabled: {
|
|
107
|
+
type: [Boolean, Function],
|
|
108
|
+
default: false,
|
|
109
|
+
},
|
|
110
|
+
//只读
|
|
111
|
+
readonly: {
|
|
112
|
+
type: Boolean,
|
|
113
|
+
default: false,
|
|
114
|
+
},
|
|
115
|
+
/**
|
|
116
|
+
* v-model 的值。传入后会优先使用
|
|
117
|
+
*/
|
|
118
|
+
FormData: {
|
|
119
|
+
type: Object,
|
|
120
|
+
default: undefined,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
//兼容处理
|
|
124
|
+
let innerContent = computed(() => transformContent(props.content));
|
|
125
|
+
// 初始化默认值
|
|
126
|
+
let setValueFromModel = () => {
|
|
127
|
+
if (innerContent.length) return;
|
|
128
|
+
/**
|
|
129
|
+
* 没使用 v-model 时才从 default 采集数据
|
|
130
|
+
* default 值没法考虑 inputFormat
|
|
131
|
+
* 参考 value-format.md 的案例。那种情况下,default 该传什么?
|
|
132
|
+
*/
|
|
133
|
+
let newValue = props.FormData
|
|
134
|
+
? transformInputValue(props.FormData, innerContent.value)
|
|
135
|
+
: collect(innerContent.value, "default");
|
|
136
|
+
correctValue(newValue, innerContent.value);
|
|
137
|
+
if (!_isequal(value, newValue)) value = Object.assign(value, newValue);
|
|
138
|
+
};
|
|
139
|
+
// v-model初始化默认数据
|
|
140
|
+
watch(
|
|
141
|
+
() => props.FormData,
|
|
142
|
+
(newForm) => {
|
|
143
|
+
if (!newForm) return;
|
|
144
|
+
setValueFromModel();
|
|
145
|
+
},
|
|
146
|
+
{ immediate: true, deep: true }
|
|
147
|
+
);
|
|
148
|
+
// 初始化默认数据
|
|
149
|
+
watch(
|
|
150
|
+
innerContent,
|
|
151
|
+
(newContent) => {
|
|
152
|
+
try {
|
|
153
|
+
if (!newContent) return;
|
|
154
|
+
|
|
155
|
+
// 如果 content 没有变动 remote 的部分,这里需要保留之前 remote 注入的 options
|
|
156
|
+
Object.assign(options, collect(newContent, "options"));
|
|
157
|
+
setValueFromModel();
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.log(error);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
{ immediate: true }
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// v-model 传递值
|
|
166
|
+
watch(value, (newValue, oldValue) => {
|
|
167
|
+
try {
|
|
168
|
+
if (!newValue) return;
|
|
169
|
+
if (props.FormData) {
|
|
170
|
+
let data = Object.assign(
|
|
171
|
+
props.FormData,
|
|
172
|
+
transformOutputValue(newValue, innerContent)
|
|
173
|
+
);
|
|
174
|
+
emit("update:FormData", data);
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.log(error, "-----");
|
|
178
|
+
}
|
|
179
|
+
// deep: true, // updateValue 是全量更新,所以不用
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 更新表单数据
|
|
184
|
+
* @param {String} options.id 表单ID
|
|
185
|
+
* @param {All} options.value 表单数据
|
|
186
|
+
*/
|
|
187
|
+
let updateValue = ({ id, value: v }) => {
|
|
188
|
+
value[id] = v;
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* 重置表单为初始值
|
|
192
|
+
*
|
|
193
|
+
* @public
|
|
194
|
+
*/
|
|
195
|
+
let resetFields = async () => {
|
|
196
|
+
/**
|
|
197
|
+
* 之所以不用 el-form 的 resetFields 机制,有以下原因:
|
|
198
|
+
* - el-form 的 resetFields 无视 el-form-renderer 的自定义组件
|
|
199
|
+
* - el-form 的 resetFields 不会触发 input & change 事件,无法监听
|
|
200
|
+
* - bug1: https://github.com/FEMessage/el-data-table/issues/176#issuecomment-587280825
|
|
201
|
+
* - bug2:
|
|
202
|
+
* 0. 建议先在监听器 watch.value 里 console.log(v.name, oldV.name)
|
|
203
|
+
* 1. 打开 basic 示例
|
|
204
|
+
* 2. 在 label 为 name 的输入框里输入 1,此时 log:'1' ''
|
|
205
|
+
* 3. 点击 reset 按钮,此时 log 两条数据: '1' '1', '' ''
|
|
206
|
+
* 4. 因为 _isequal(v, oldV),所以没有触发 v-model 更新
|
|
207
|
+
*/
|
|
208
|
+
value = _clonedeep(initValue);
|
|
209
|
+
await nextTick();
|
|
210
|
+
methods.clearValidate();
|
|
211
|
+
};
|
|
212
|
+
/**
|
|
213
|
+
* 当 strict 为 true 时,只返回设置的表单项的值, 过滤掉冗余字段, 更多请看 update-form 示例
|
|
214
|
+
* @param {{strict: Boolean}} 默认 false
|
|
215
|
+
* @return {object} key is item's id, value is item's value
|
|
216
|
+
* @public
|
|
217
|
+
*/
|
|
218
|
+
let getFormValue = ({ strict = false } = {}) => {
|
|
219
|
+
return transformOutputValue(value, innerContent, { strict });
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* update form values
|
|
223
|
+
* @param {object} newValue - key is item's id, value is the new value
|
|
224
|
+
* @public
|
|
225
|
+
*/
|
|
226
|
+
let updateForm = (newValue) => {
|
|
227
|
+
newValue = transformInputValue(newValue, innerContent);
|
|
228
|
+
mergeValue(value, newValue, innerContent);
|
|
229
|
+
};
|
|
230
|
+
/**
|
|
231
|
+
* update select options
|
|
232
|
+
* @param {string} id<br>
|
|
233
|
+
* @param {array} options
|
|
234
|
+
* @public
|
|
235
|
+
*/
|
|
236
|
+
let setOptions = (id, O) => {
|
|
237
|
+
_set(options, id, O);
|
|
238
|
+
options = Object.assign(options); // 设置之前不存在的 options 时需要重新设置响应式更新
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* get custom component
|
|
243
|
+
* @param {string} id<br>
|
|
244
|
+
* @public
|
|
245
|
+
*/
|
|
246
|
+
const getComponentById = (id) => {
|
|
247
|
+
let content = [];
|
|
248
|
+
props.content.forEach((item) => {
|
|
249
|
+
if (item.type === GROUP) {
|
|
250
|
+
const items = item.items.map((formItem) => {
|
|
251
|
+
formItem.groupId = item.id;
|
|
252
|
+
return formItem;
|
|
253
|
+
});
|
|
254
|
+
content.push(...items);
|
|
255
|
+
} else {
|
|
256
|
+
content.push(item);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
const itemContent = content.find((item) => item.id === id);
|
|
260
|
+
if (!itemContent) {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
if (!itemContent.groupId) {
|
|
264
|
+
return customComponent.value[id].customComponent;
|
|
265
|
+
} else {
|
|
266
|
+
const componentRef = customComponent.value[itemContent.groupId].customComponent;
|
|
267
|
+
return componentRef[`formItem-${id}`].customComponent;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
provide("methods", methods);
|
|
271
|
+
provide("updateForm", updateForm);
|
|
272
|
+
provide("setOptions", setOptions);
|
|
273
|
+
defineExpose({
|
|
274
|
+
updateValue,
|
|
275
|
+
resetFields,
|
|
276
|
+
getFormValue,
|
|
277
|
+
updateForm,
|
|
278
|
+
setOptions,
|
|
279
|
+
methods,
|
|
280
|
+
getComponentById,
|
|
281
|
+
});
|
|
282
|
+
</script>
|
|
283
|
+
<script>
|
|
284
|
+
export default {
|
|
285
|
+
name: "ElFormRenderer",
|
|
286
|
+
};
|
|
287
|
+
</script>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { h, resolveComponent } from "vue";
|
|
2
|
+
|
|
3
|
+
// Vue3 中函数式组件需要提供一个渲染函数
|
|
4
|
+
const CustomComponent = (props, context) => {
|
|
5
|
+
let dom =
|
|
6
|
+
typeof props.component == "string"
|
|
7
|
+
? resolveComponent(hyphenToPascal(props.component))
|
|
8
|
+
: "Input";
|
|
9
|
+
|
|
10
|
+
// 返回一个渲染函数,可以使用 h 函数创建虚拟节点
|
|
11
|
+
return h(dom, props, context.slots);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function hyphenToPascal(str) {
|
|
15
|
+
let arr = str.split("-");
|
|
16
|
+
|
|
17
|
+
let resStr = arr.reduce(function (prev, cur) {
|
|
18
|
+
let str = prev + cur.slice(0, 1).toUpperCase() + cur.slice(1);
|
|
19
|
+
return str;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// 转小驼峰这一行不需要
|
|
23
|
+
resStr = resStr.slice(0, 1).toUpperCase() + resStr.slice(1);
|
|
24
|
+
|
|
25
|
+
return resStr;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default CustomComponent;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import _get from "lodash.get";
|
|
2
|
+
import _has from "lodash.has";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 处理 enableWhen
|
|
6
|
+
*
|
|
7
|
+
* 与条件: 简单依赖关系存在2种情况:简单对象 || 字符串
|
|
8
|
+
* 或条件: 即使用 [] 包裹所有与条件 enableWhen: [{ a: 1 }, { a: 2 }]
|
|
9
|
+
*/
|
|
10
|
+
export default function getEnableWhenStatus(enableWhen, value) {
|
|
11
|
+
if (!enableWhen) return true;
|
|
12
|
+
// 处理一个与条件
|
|
13
|
+
const handleCondition = (condition) => {
|
|
14
|
+
// 简单字符串(ID), 只要有值即为true
|
|
15
|
+
if (typeof condition === "string") return _has(value, condition);
|
|
16
|
+
// 简单对象判断: 是否所有依赖条件都通过
|
|
17
|
+
return Object.keys(condition).every((path) => {
|
|
18
|
+
const v = _get(value, path);
|
|
19
|
+
return v !== undefined && v === condition[path];
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return Array.isArray(enableWhen)
|
|
24
|
+
? enableWhen.some(handleCondition)
|
|
25
|
+
: handleCondition(enableWhen);
|
|
26
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//去除谷歌浏览器的scroll、wheel等事件警告
|
|
2
|
+
(function () {
|
|
3
|
+
if (typeof EventTarget !== "undefined") {
|
|
4
|
+
let func = EventTarget.prototype.addEventListener;
|
|
5
|
+
EventTarget.prototype.addEventListener = function (type, fn, capture) {
|
|
6
|
+
this.func = func;
|
|
7
|
+
if (typeof capture !== "boolean") {
|
|
8
|
+
capture = capture || {};
|
|
9
|
+
capture.passive = false;
|
|
10
|
+
}
|
|
11
|
+
this.func(type, fn, capture);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
})();
|