需求背景
-
web下发任务需要配置算法,不同算法需要配置的算法参数不同,算法很多
-
不可能来一个算法写一个配置,这样做代码冗余,维护艰难
-
希望重新设计一套方案,前端只用写一次,后面新增的算法都可以适配,就不需要再去做配置或者个性化开发
jsonSchema
- jsonSchema是一个描述json的json文件,点此查看
项目中使用
-
java返回一个jsonSchema,web根据这个jsonSchema渲染表单,最后将用户填写的数据按照jsonSchema对应层级结构传给java
-
社区有成熟的jsonSchema渲染表单的插件,但是没有采用,因为项目中使用的jsonSchema并不完全遵守标准的jsonSchema规范,有些自定义的类型(比如时间,type:'dateTime')
-
找了个插件(插件在这)简单看了下,仿照写了个组件,满足项目使用
代码实现
- schemaForm.vue
// schemaForm.vue
// schemaForm.vue
// schemaForm.vue
<template>
<div class="container">
<el-form
ref="schemaForm"
:model="model"
:rules="rules"
class="addForm"
inline
label-suffix=":"
label-position="right"
size="small"
>
<SchemaFormItems :model="model" :schema="schema" :options="options"></SchemaFormItems>
</el-form>
</div>
</template>
<script>
import SchemaFormItems from './schemaFormItems.vue'
export default {
components: {
SchemaFormItems,
},
data() {
return {
jsonSchema: {
$schema: 'http://json-schema.org/draft-07/schema#',
$id: 'http://json-schema.org/draft-07/schema#',
title: 'EngineHelmetSchema',
type: 'object',
properties: {
sourceType: {
title: '视频数据源类型',
type: 'integer',
default: 0,
enum: [0, 1],
description: '数据源类型0或1',
},
isRepeat: {
title: '是否可重复',
type: 'boolean',
default: true,
description: '是否可重复',
},
algoTasks: {
type: 'array',
items: {
title: '算法任务列表',
type: 'object',
properties: {
algoTask: {
title: '任务参数',
type: 'integer',
default: 9003,
enum: [5002, 9003, 1004, 2005],
description: '任务参数',
},
algoConfig: {
description: '算法配置',
type: 'object',
default: {},
properties: {
isSaveCrop: {
title: '是否指定保存副本',
type: 'boolean',
default: false,
description: '是否指定保存副本',
},
isSavePanorama: {
title: '是否保存全景图',
type: 'boolean',
default: true,
description: '是否保存全景图',
},
pushUrl: {
title: '三方推送地址',
type: 'string',
default: '',
description: '三方推送地址',
pattern: '^(https?|ftp)://([a-zA-Z0-9.-]+).([a-zA-Z]{2,6})([/w.-]*)*/?$',
},
startTime: {
title: '开始时间',
type: 'datetime',
default: '2024-01-01 08:00:00',
description: '开始时间',
format: 'YYYY-MM-DD HH:MM:SS',
},
endTime: {
title: '结束时间',
type: 'datetime',
default: '2025-01-01 08:00:00',
description: '结束时间',
format: 'YYYY-MM-DD HH:MM:SS',
},
minTargetWidth: {
title: '最大目标宽度',
type: 'integer',
default: 10,
description: '最大目标宽度',
minimum: 5,
maximum: 50,
},
threshold: {
title: '算法阈值',
type: 'number',
default: 0.8,
description: '算法阈值',
minimum: 0,
maximum: 1,
exclusiveMinimum: true,
exclusiveMaximum: true,
},
},
},
default: [],
description: '算法配置',
required: [
'isSaveCrop',
'pushUrl',
'startTime',
'endTime',
'minTargetWidth',
'threshold',
],
},
required: ['algoTask', 'algoConfig'],
description: '算法任务列表',
},
},
required: ['sourceType', 'algoTasks'],
},
},
model: {
name: '',
region: '',
delivery: false,
checkList: [],
resource: '',
desc: '',
date: '',
},
schema: [
{ type: 'input', prop: 'name', formItem: { label: '活动名称' } },
{ type: 'select', prop: 'region', formItem: { label: '活动区域' } },
{ type: 'switch', prop: 'delivery', formItem: { label: '即时配送' } },
{ type: 'checkbox', prop: 'checkList', formItem: { label: '活动性质' } },
{ type: 'radio', prop: 'resource', formItem: { label: '特殊资源' } },
{ type: 'input', prop: 'desc', formItem: { label: '活动形式' }, attrs: { type: 'textarea' } },
{ type: 'date', prop: 'date', formItem: { label: '活动时间' } },
],
options: {
region: [
{ label: '区域一', value: 'shanghai' },
{ label: '区域二', value: 'beijing' },
],
checkList: [
{ label: '美食/餐厅线上活动', value: '美食/餐厅线上活动' },
{ label: '地推活动', value: '地推活动' },
],
resource: [
{ label: '线上品牌商赞助', value: '线上品牌商赞助' },
{ label: '线下场地免费', value: '线下场地免费' },
],
},
rules: {
name: [
{ required: true, message: '请输入活动名称', trigger: 'blur' },
{ min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' },
],
region: [{ required: true, message: '请选择活动区域', trigger: 'change' }],
checkList: [{ type: 'array', required: true, message: '请至少选择一个活动性质', trigger: 'change' }],
resource: [{ required: true, message: '请选择活动资源', trigger: 'change' }],
desc: [{ required: true, message: '请填写活动形式', trigger: 'blur' }],
date: [{ type: 'date', required: true, message: '请选择日期', trigger: 'change' }],
},
}
},
mounted() {
this.initSchema()
},
methods: {
getSaveData() {
const handleData = this.getStructureData(this.jsonSchema, {})
console.log(handleData)
},
/*** form ***/
initSchema() {
const schemaData = this.getSchemaData(this.jsonSchema, [])
const modelData = this.getModelData(schemaData, {})
const requiredData = this.getRequired(this.jsonSchema, [])
const renderData = schemaData.filter((v) => requiredData.includes(v.prop))
const options = this.getOptionsData(renderData, {})
const ruleData = this.getRuleData(renderData, {})
this.schema = renderData
this.model = modelData
this.options = options
this.rules = ruleData
},
getSchemaData(jsonSchema, schema) {
if (!jsonSchema.properties) return []
for (const key in jsonSchema.properties) {
const item = jsonSchema.properties[key]
const type = item?.type ?? ''
if (!type) continue
if (!['array', 'object'].includes(item.type)) {
const obj = this.getSchemaItemByType(item.type, key, item)
schema.push(obj)
} else if (item.type === 'array' && item.items) {
this.getSchemaData(item.items, schema)
} else if (item.type === 'object') {
this.getSchemaData(item, schema)
}
}
return schema
},
getSchemaItemByType(type, key, item) {
const baseSchema = {
type: 'input',
prop: key,
formItem: { label: item.title, default: item.default, description: item.description },
}
switch (type) {
case 'boolean':
baseSchema.type = 'switch'
break
case 'integer':
if (item.enum) {
baseSchema.type = 'select'
baseSchema.formItem = {
...baseSchema.formItem,
enum: item.enum,
}
} else {
baseSchema.type = 'number-int'
baseSchema.formItem = {
...baseSchema.formItem,
minimum: item?.minimum ?? undefined,
maximum: item?.maximum ?? undefined,
exclusiveMinimum: item?.exclusiveMinimum ?? false,
exclusiveMaximum: item?.exclusiveMaximum ?? false,
}
}
break
case 'number':
if (item.enum) {
baseSchema.type = 'select'
baseSchema.formItem = {
...baseSchema.formItem,
enum: item.enum,
}
} else {
baseSchema.type = 'number-float'
baseSchema.formItem = {
...baseSchema.formItem,
precision: 4,
minimum: item?.minimum ?? undefined,
maximum: item?.maximum ?? undefined,
exclusiveMinimum: item?.exclusiveMinimum ?? false,
exclusiveMaximum: item?.exclusiveMaximum ?? false,
}
}
break
case 'string':
baseSchema.formItem = {
...baseSchema.formItem,
pattern: item?.pattern ?? '',
}
break
case 'datetime':
baseSchema.type = 'datetime'
baseSchema.formItem = {
...baseSchema.formItem,
format: item?.format ?? 'YYYY-MM-DD HH:MM:SS',
}
break
}
return baseSchema
},
getModelData(schemaData, model) {
if (!schemaData.length) return {}
schemaData.forEach((item) => {
const { type, prop, formItem } = item
if (type === 'input') {
model[prop] = formItem.default ?? ''
} else if (type === 'select') {
model[prop] = formItem.default ?? ''
} else if (type === 'radio') {
model[prop] = formItem.default ?? ''
} else if (type === 'checkbox') {
model[prop] = formItem.default ?? []
} else if (type === 'switch') {
model[prop] = formItem.default ?? false
} else if (type === 'datetime') {
model[prop] = formItem.default ?? ''
} else if (type === 'number-int') {
model[prop] = formItem.default ?? ''
} else if (type === 'number-float') {
model[prop] = formItem.default ?? ''
}
})
return model
},
getRequired(jsonSchema, req) {
for (const key in jsonSchema) {
if (typeof jsonSchema[key] != 'object') continue
if (key == 'required') {
req.push(...jsonSchema[key])
} else {
this.getRequired(jsonSchema[key], req)
}
}
return req
},
getOptionsData(renderData, option) {
renderData.forEach((v) => {
if (v.type == 'select') {
option[v.prop] = v.formItem.enum.map((val) => {
return {
label: val,
value: val,
}
})
}
})
return option
},
getRuleData(renderData, ruleData) {
renderData.forEach((v) => {
const baseRule = { required: true, message: this.$t('length.required'), trigger: 'blur' }
if (v.type == 'input') {
delete baseRule.message
baseRule.pattern = v.formItem.pattern
baseRule.validator = (rule, value, callback) => {
if (!value) callback(new Error(this.$t('length.required')))
if (!rule.pattern) callback()
const reg = new RegExp(rule.pattern)
if (!reg.test(value)) callback(new Error('正则验证不通过'))
callback()
}
}
ruleData[v.prop] = [baseRule]
})
return ruleData
},
getStructureData(jsonSchema, structure) {
if (!jsonSchema.properties) return {}
for (const key in jsonSchema.properties) {
const item = jsonSchema.properties[key]
const type = item?.type ?? ''
if (!type) continue
if (!['array', 'object'].includes(item.type)) {
structure[key] = this.model[key]
} else if (item.type == 'array') {
const curArr = []
curArr.push(this.getStructureData(item.items, {}))
structure[key] = curArr
} else if (item.type == 'object') {
const curObj = {}
this.getStructureData(item, curObj)
structure[key] = curObj
}
}
return structure
},
},
}
</script>
<style lang="scss" scoped>
.container {
border: 1px solid #f00;
::v-deep .el-input-number--small {
line-height: 2.8;
}
}
</style>
- schemaFormItems.vue
// SchemaFormItems.vue
// SchemaFormItems.vue
// SchemaFormItems.vue
<template>
<div class="schema-form-items">
<section v-for="(item, index) of schema" :key="index">
<!-- text -->
<el-form-item v-if="typeIs(item, 'input')" :label="setLabel(item)" :prop="setProp(item)">
<el-input
:key="setProp(item)"
type="text"
v-model.trim="model[item.prop]"
placeholder="请输入"
></el-input>
</el-form-item>
<!-- select -->
<el-form-item v-if="typeIs(item, 'select')" :label="setLabel(item)" :prop="setProp(item)">
<el-select v-model="model[item.prop]" placeholder="请选择">
<el-option
v-for="optionItem in options[item.prop]"
:key="optionItem.value"
:label="optionItem.label"
:value="optionItem.value"
>
</el-option>
</el-select>
</el-form-item>
<!-- switch -->
<el-form-item v-if="typeIs(item, 'switch')" :label="setLabel(item)" :prop="setProp(item)">
<el-col :span="4">
<el-switch v-model="model[item.prop]" active-color="#13ce66" inactive-color="#626262"> </el-switch>
</el-col>
</el-form-item>
<!-- checkbox -->
<el-form-item v-if="typeIs(item, 'checkbox')" :label="setLabel(item)" :prop="setProp(item)">
<el-checkbox-group v-model="model[item.prop]">
<el-checkbox
v-for="(checkItem, index) of options[item.prop]"
:key="index"
:label="checkItem.label"
></el-checkbox>
</el-checkbox-group>
</el-form-item>
<!-- radio -->
<el-form-item v-if="typeIs(item, 'radio')" :label="setLabel(item)" :prop="setProp(item)">
<el-radio-group v-model="model[item.prop]">
<el-radio v-for="(radioItem, index) of options[item.prop]" :key="index" :label="radioItem.value"
>{{ radioItem.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<!-- datetime -->
<el-form-item v-if="typeIs(item, 'datetime')" :label="setLabel(item)" :prop="setProp(item)">
<el-date-picker
type="datetime"
placeholder="选择日期"
v-model="model[item.prop]"
style="width: 100%"
value-format="yyyy-MM-dd HH:mm:ss"
></el-date-picker>
</el-form-item>
<!-- number-int -->
<el-form-item v-if="typeIs(item, 'number-int')" :label="setLabel(item)" :prop="setProp(item)">
<el-row>
<el-col :span="6">
<el-input-number
style="width: 100%"
v-model="model[item.prop]"
:step="1"
step-strictly
:min="setMin(item)"
:max="setMax(item)"
>
</el-input-number>
</el-col>
</el-row>
</el-form-item>
<!-- number-float -->
<el-form-item v-if="typeIs(item, 'number-float')" :label="setLabel(item)" :prop="setProp(item)">
<el-row>
<el-col :span="6">
<el-input-number
style="width: 100%"
v-model="model[item.prop]"
:min="setMin(item)"
:max="setMax(item)"
:precision="setPrecision(item)"
>
</el-input-number>
</el-col>
</el-row>
</el-form-item>
</section>
</div>
</template>
<script>
export default {
props: {
model: {
type: Object,
default: () => {},
},
schema: {
type: Array,
default: () => [],
},
options: {
type: Object,
default: () => {},
},
},
data() {
return {}
},
computed: {},
mounted() {},
methods: {
typeIs(val, type) {
return val.type === type
},
setLabel(val) {
return val.formItem.label
},
setProp(val) {
return val.prop
},
setMin(val) {
const { minimum, exclusiveMinimum, precision } = val.formItem
if ([undefined, null].includes(minimum)) return -Infinity
if (!exclusiveMinimum) return minimum
const minVal = minimum + this.getValByPrecision(precision)
return minVal
},
setMax(val) {
const { maximum, exclusiveMaximum, precision } = val.formItem
if ([undefined, null].includes(maximum)) return Infinity
if (!exclusiveMaximum) return maximum
const maxVal = maximum - this.getValByPrecision(precision)
return maxVal
},
getValByPrecision(precision) {
return precision > 0 ? Number('0.' + '1'.padStart(precision, 0)) : 0
},
setPrecision(val) {
return val.formItem?.precision ?? 4
},
},
}
</script>
<style lang="scss" scoped></style>
-
使用的时候只需要传一个jsonSchema就好
-
也没撒封装,主要就是initSchema里面处理数据,转换成需要的格式数据
-
最后提交表单时,java需要按之前的层级结构给他,真是想捶死他,争论了一番,没争赢,只好再写一个getStructureData
总结
-
写这个东西花了点时间,主要是没找到可以满足项目使用的插件
-
自己写了一下这个东西,感觉也不难,可能很多东西没有想到吧