1/** 2 * Created by Liu.Jun on 2020/4/23 11:24. 3 */ 4 5import { 6 computed, h, ref, watch, inject 7} from 'vue'; 8 9import {IconInfo} from '@lljj/vjsf-utils/icons'; 10 11import {validateFormDataAndTransformMsg} from '@lljj/vjsf-utils/schema/validate'; 12import {fallbackLabel} from '@lljj/vjsf-utils/formUtils'; 13 14import { 15 isRootNodePath, path2prop, getPathVal, setPathVal, resolveComponent 16} from '@lljj/vjsf-utils/vue3Utils'; 17 18export default { 19 name: 'Widget', 20 props: { 21 // 是否同步formData的值,默认表单元素都需要 22 // oneOf anyOf 中的select属于formData之外的数据 23 isFormData: { 24 type: Boolean, 25 default: true 26 }, 27 // isFormData = false时需要传入当前 value 否则会通过 curNodePath 自动计算 28 curValue: { 29 type: null, 30 default: 0 31 }, 32 schema: { 33 type: Object, 34 default: () => ({}) 35 }, 36 uiSchema: { 37 type: Object, 38 default: () => ({}) 39 }, 40 errorSchema: { 41 type: Object, 42 default: () => ({}) 43 }, 44 customFormats: { 45 type: Object, 46 default: () => ({}) 47 }, 48 // 自定义校验 49 customRule: { 50 type: Function, 51 default: null 52 }, 53 widget: { 54 type: [String, Function, Object], 55 default: null 56 }, 57 required: { 58 type: Boolean, 59 default: false 60 }, 61 // 解决 JSON Schema和实际输入元素中空字符串 required 判定的差异性 62 // 元素输入为 '' 使用 emptyValue 的值 63 emptyValue: { 64 type: null, 65 default: undefined 66 }, 67 rootFormData: { 68 type: null 69 }, 70 curNodePath: { 71 type: String, 72 default: '' 73 }, 74 label: { 75 type: String, 76 default: '' 77 }, 78 // width -> formItem width 79 width: { 80 type: String, 81 default: '' 82 }, 83 labelWidth: { 84 type: String, 85 default: '' 86 }, 87 description: { 88 type: String, 89 default: '' 90 }, 91 // Widget attrs 92 widgetAttrs: { 93 type: Object, 94 default: () => ({}) 95 }, 96 // Widget className 97 widgetClass: { 98 type: Object, 99 default: () => ({}) 100 }, 101 // Widget style 102 widgetStyle: { 103 type: Object, 104 default: () => ({}) 105 }, 106 // Field attrs 107 fieldAttrs: { 108 type: Object, 109 default: () => ({}) 110 }, 111 // Field className 112 fieldClass: { 113 type: Object, 114 default: () => ({}) 115 }, 116 // Field style 117 fieldStyle: { 118 type: Object, 119 default: () => ({}) 120 }, 121 // props 122 uiProps: { 123 type: Object, 124 default: () => ({}) 125 }, 126 formProps: null, 127 getWidget: null, 128 renderScopedSlots: null, // 作用域插槽 129 globalOptions: null, // 全局配置 130 onChange: null 131 }, 132 emits: ['otherDataChange'], 133 inheritAttrs: true, 134 setup(props, {emit}) { 135 const genFormProvide = inject('genFormProvide'); 136 const widgetValue = computed({ 137 get() { 138 if (props.isFormData) return getPathVal(props.rootFormData, props.curNodePath); 139 140 return props.curValue; 141 }, 142 set(value) { 143 // 大多组件删除为空值会重置为null。 144 const trueValue = (value === '' || value === null) ? props.emptyValue : value; 145 if (props.isFormData) { 146 setPathVal(props.rootFormData, props.curNodePath, trueValue); 147 } else { 148 emit('otherDataChange', trueValue); 149 } 150 } 151 }); 152 153 // 枚举类型默认值为第一个选项 154 if (props.uiProps.enumOptions 155 && props.uiProps.enumOptions.length > 0 156 && widgetValue.value === undefined 157 && widgetValue.value !== props.uiProps.enumOptions[0] 158 ) { 159 // array 渲染为多选框时默认为空数组 160 if (props.schema.items) { 161 widgetValue.value = []; 162 } else if (props.required) { 163 widgetValue.value = props.uiProps.enumOptions[0].value; 164 } 165 } 166 167 // 获取到widget组件实例 168 const widgetRef = ref(null); 169 // 提供一种特殊的配置 允许直接访问到 widget vm 170 if (typeof props.getWidget === 'function') { 171 watch(widgetRef, () => { 172 props.getWidget.call(null, widgetRef.value); 173 }); 174 } 175 176 return () => { 177 // 判断是否为根节点 178 const isRootNode = isRootNodePath(props.curNodePath); 179 180 const isMiniDes = props.formProps && props.formProps.isMiniDes; 181 const miniDesModel = isMiniDes || props.globalOptions.HELPERS.isMiniDes(props.formProps); 182 183 const descriptionVNode = (props.description) ? h( 184 'div', 185 { 186 innerHTML: props.description, 187 class: { 188 genFromWidget_des: true, 189 genFromWidget_des_mini: miniDesModel 190 } 191 }, 192 ) : null; 193 194 const {COMPONENT_MAP} = props.globalOptions; 195 const miniDescriptionVNode = (miniDesModel && descriptionVNode) ? h(resolveComponent(COMPONENT_MAP.popover), { 196 style: { 197 margin: '0 2px', 198 fontSize: '16px', 199 cursor: 'pointer' 200 }, 201 placement: 'top', 202 trigger: 'hover' 203 }, { 204 default: () => descriptionVNode, 205 reference: () => h(IconInfo) 206 }) : null; 207 208 // form-item style 209 const formItemStyle = { 210 ...props.fieldStyle, 211 ...(props.width ? { 212 width: props.width, 213 flexBasis: props.width, 214 paddingRight: '10px' 215 } : {}) 216 }; 217 218 // 运行配置回退到 属性名 219 const label = fallbackLabel(props.label, (props.widget && genFormProvide.value.fallbackLabel), props.curNodePath); 220 return h( 221 resolveComponent(COMPONENT_MAP.formItem), 222 { 223 class: { 224 ...props.fieldClass, 225 genFormItem: true 226 }, 227 style: formItemStyle, 228 ...props.fieldAttrs, 229 230 ...props.labelWidth ? {labelWidth: props.labelWidth} : {}, 231 ...props.isFormData ? { 232 // 这里对根节点打特殊标志,绕过elementUi无prop属性不校验 233 prop: isRootNode ? '__$$root' : path2prop(props.curNodePath), 234 rules: [ 235 { 236 validator(rule, value, callback) { 237 if (isRootNode) value = props.rootFormData; 238 239 // 校验是通过对schema逐级展开校验 这里只捕获根节点错误 240 const errors = validateFormDataAndTransformMsg({ 241 formData: value, 242 schema: props.schema, 243 uiSchema: props.uiSchema, 244 customFormats: props.customFormats, 245 errorSchema: props.errorSchema, 246 required: props.required, 247 propPath: path2prop(props.curNodePath) 248 }); 249 250 // 存在校验不通过字段 251 if (errors.length > 0) { 252 if (callback) return callback(errors[0].message); 253 return Promise.reject(errors[0].message); 254 } 255 256 // customRule 如果存在自定义校验 257 const curCustomRule = props.customRule; 258 if (curCustomRule && (typeof curCustomRule === 'function')) { 259 return curCustomRule({ 260 field: props.curNodePath, 261 value, 262 rootFormData: props.rootFormData, 263 callback 264 }); 265 } 266 267 // 校验成功 268 if (callback) return callback(); 269 return Promise.resolve(); 270 }, 271 trigger: ['blur', 'input', 'focus', 'change'] 272 } 273 ] 274 } : {}, 275 }, 276 { 277 // 错误只能显示一行,多余... 278 error: slotProps => (slotProps.error ? h('div', { 279 class: { 280 formItemErrorBox: true 281 }, 282 title: slotProps.error 283 }, [slotProps.error]) : null), 284 285 // label 286 /* 287 TODO:这里slot如果从无到有会导致无法正常渲染出元素 怀疑是vue3 bug 288 如果使用 error 的形式渲染,ElementPlus label labelWrap 未做判断,使用 slots.default?.() 会得到 undefined 289 */ 290 ...label ? { 291 label: () => h('span', { 292 class: { 293 genFormLabel: true, 294 genFormItemRequired: props.required && props.schema['ui:widget'] !== 'b-form-checkbox' && !props.uiProps.enumOptions, 295 }, 296 }, [ 297 ...miniDescriptionVNode ? [miniDescriptionVNode] : [], 298 (window.isDev ? 299 h('div', {style: {position: 'relative'}}, [ 300 h('div', {style: {position: 'absolute'}}, 301 `${props.curNodePath}`) 302 ]) : 303 '' 304 ), 305 `${label}`, 306 `${(props.formProps && props.formProps.labelSuffix) || ''}` 307 ]) 308 } : {}, 309 310 // default 311 default: otherAttrs => [ 312 // description 313 // 非mini模式显示 description 314 ...(!miniDesModel && descriptionVNode) ? [descriptionVNode] : [], 315 316 ...props.widget ? [ 317 h( // 关键输入组件 318 resolveComponent(props.widget), 319 { 320 style: props.widgetStyle, 321 class: props.widgetClass, 322 323 ...props.widgetAttrs, 324 ...props.uiProps, 325 modelValue: widgetValue.value, // v-model 326 ref: widgetRef, 327 'onUpdate:modelValue': function updateModelValue(event) { 328 const preVal = widgetValue.value; 329 if (preVal !== event) { 330 widgetValue.value = event; 331 if (props.onChange) { 332 props.onChange({ 333 curVal: event, 334 preVal, 335 parentFormData: getPathVal(props.rootFormData, props.curNodePath, 1), 336 rootFormData: props.rootFormData 337 }); 338 } 339 } 340 }, 341 ...otherAttrs ? (() => Object.keys(otherAttrs).reduce((pre, k) => { 342 pre[k] = otherAttrs[k]; 343 344 // 保证ui配置同名方法 ui方法先执行 345 [ 346 props.widgetAttrs[k], 347 props.uiProps[k] 348 ].forEach((uiConfFn) => { 349 if (uiConfFn && typeof uiConfFn === 'function') { 350 pre[k] = (...args) => { 351 uiConfFn(...args); 352 pre[k](...args); 353 }; 354 } 355 }); 356 357 return pre; 358 }, {}))() : {} 359 }, 360 { 361 ...(props.renderScopedSlots ? ( 362 typeof props.renderScopedSlots === 'function' ? props.renderScopedSlots() : props.renderScopedSlots 363 ) : {}) 364 } 365 ) 366 ] : [] 367 ] 368 } 369 ); 370 }; 371 } 372}; 373