231 lines
5.6 KiB
Vue
231 lines
5.6 KiB
Vue
<template>
|
|
<el-form
|
|
ref="formRef"
|
|
:model="formData"
|
|
:rules="formRules"
|
|
:label-width="labelWidth"
|
|
v-bind="$attrs"
|
|
>
|
|
<el-form-item
|
|
v-for="(field, key) in schemaProperties"
|
|
:key="key"
|
|
:label="field.title || key"
|
|
:prop="key"
|
|
>
|
|
<template #label>
|
|
<span>{{ field.title || key }}</span>
|
|
<el-tooltip v-if="field.description" :content="field.description" placement="top">
|
|
<el-icon class="ml-1 cursor-help"><QuestionFilled /></el-icon>
|
|
</el-tooltip>
|
|
</template>
|
|
<el-input
|
|
v-if="field.type === 'string'"
|
|
v-model="formData[key]"
|
|
:placeholder="`请输入${field.title || key}`"
|
|
clearable
|
|
:show-password="isPasswordField(key)"
|
|
/>
|
|
<el-input-number
|
|
v-else-if="field.type === 'integer' || field.type === 'number'"
|
|
v-model="formData[key]"
|
|
:placeholder="`请输入${field.title || key}`"
|
|
:min="field.minimum"
|
|
:max="field.maximum"
|
|
:step="field.type === 'number' ? 0.1 : 1"
|
|
:precision="field.type === 'number' ? 2 : 0"
|
|
controls-position="right"
|
|
class="w-full"
|
|
/>
|
|
<el-switch
|
|
v-else-if="field.type === 'boolean'"
|
|
v-model="formData[key]"
|
|
/>
|
|
<el-select
|
|
v-else-if="field.enum && field.enum.length > 0"
|
|
v-model="formData[key]"
|
|
:placeholder="`请选择${field.title || key}`"
|
|
clearable
|
|
class="w-full"
|
|
>
|
|
<el-option
|
|
v-for="option in field.enum"
|
|
:key="option"
|
|
:label="option"
|
|
:value="option"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
</el-form>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, watch, onMounted } from 'vue'
|
|
import { QuestionFilled } from '@element-plus/icons-vue'
|
|
import type { FormInstance, FormRules } from 'element-plus'
|
|
|
|
export interface SchemaProperty {
|
|
type: string
|
|
title?: string
|
|
description?: string
|
|
default?: any
|
|
enum?: string[]
|
|
minimum?: number
|
|
maximum?: number
|
|
required?: boolean
|
|
}
|
|
|
|
export interface ConfigSchema {
|
|
type?: string
|
|
properties?: Record<string, SchemaProperty>
|
|
required?: string[]
|
|
}
|
|
|
|
const props = defineProps<{
|
|
schema: ConfigSchema
|
|
modelValue: Record<string, any>
|
|
labelWidth?: string
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:modelValue', value: Record<string, any>): void
|
|
}>()
|
|
|
|
const formRef = ref<FormInstance>()
|
|
const formData = ref<Record<string, any>>({})
|
|
const isUpdating = ref(false)
|
|
|
|
const schemaProperties = computed(() => {
|
|
return props.schema?.properties || {}
|
|
})
|
|
|
|
const requiredFields = computed(() => {
|
|
const required = props.schema?.required || []
|
|
const propsRequired = Object.entries(schemaProperties.value)
|
|
.filter(([, field]) => field.required)
|
|
.map(([key]) => key)
|
|
return [...new Set([...required, ...propsRequired])]
|
|
})
|
|
|
|
const formRules = computed<FormRules>(() => {
|
|
const rules: FormRules = {}
|
|
Object.entries(schemaProperties.value).forEach(([key, field]) => {
|
|
const fieldRules: any[] = []
|
|
if (requiredFields.value.includes(key)) {
|
|
fieldRules.push({
|
|
required: true,
|
|
message: `${field.title || key}不能为空`,
|
|
trigger: ['blur', 'change']
|
|
})
|
|
}
|
|
if (field.type === 'string' && field.minimum !== undefined) {
|
|
fieldRules.push({
|
|
min: field.minimum,
|
|
message: `${field.title || key}长度不能小于${field.minimum}`,
|
|
trigger: ['blur']
|
|
})
|
|
}
|
|
if (field.type === 'string' && field.maximum !== undefined) {
|
|
fieldRules.push({
|
|
max: field.maximum,
|
|
message: `${field.title || key}长度不能大于${field.maximum}`,
|
|
trigger: ['blur']
|
|
})
|
|
}
|
|
if (rules[key]) {
|
|
rules[key] = fieldRules
|
|
} else if (fieldRules.length > 0) {
|
|
rules[key] = fieldRules
|
|
}
|
|
})
|
|
return rules
|
|
})
|
|
|
|
const isPasswordField = (key: string): boolean => {
|
|
const lowerKey = key.toLowerCase()
|
|
return lowerKey.includes('password') || lowerKey.includes('secret') || lowerKey.includes('key') || lowerKey.includes('token')
|
|
}
|
|
|
|
const initFormData = () => {
|
|
const data: Record<string, any> = {}
|
|
Object.entries(schemaProperties.value).forEach(([key, field]) => {
|
|
if (props.modelValue && props.modelValue[key] !== undefined) {
|
|
data[key] = props.modelValue[key]
|
|
} else if (field.default !== undefined) {
|
|
data[key] = field.default
|
|
} else {
|
|
switch (field.type) {
|
|
case 'string':
|
|
data[key] = ''
|
|
break
|
|
case 'integer':
|
|
case 'number':
|
|
data[key] = field.minimum ?? 0
|
|
break
|
|
case 'boolean':
|
|
data[key] = false
|
|
break
|
|
default:
|
|
data[key] = null
|
|
}
|
|
}
|
|
})
|
|
formData.value = data
|
|
}
|
|
|
|
watch(
|
|
() => props.modelValue,
|
|
(newVal) => {
|
|
if (isUpdating.value) return
|
|
if (JSON.stringify(newVal) !== JSON.stringify(formData.value)) {
|
|
initFormData()
|
|
}
|
|
},
|
|
{ deep: true }
|
|
)
|
|
|
|
watch(
|
|
() => props.schema,
|
|
() => {
|
|
initFormData()
|
|
},
|
|
{ deep: true }
|
|
)
|
|
|
|
watch(
|
|
formData,
|
|
(val) => {
|
|
if (isUpdating.value) return
|
|
if (JSON.stringify(val) !== JSON.stringify(props.modelValue)) {
|
|
isUpdating.value = true
|
|
emit('update:modelValue', val)
|
|
Promise.resolve().then(() => {
|
|
isUpdating.value = false
|
|
})
|
|
}
|
|
},
|
|
{ deep: true }
|
|
)
|
|
|
|
onMounted(() => {
|
|
initFormData()
|
|
})
|
|
|
|
defineExpose({
|
|
validate: () => formRef.value?.validate(),
|
|
resetFields: () => formRef.value?.resetFields(),
|
|
clearValidate: () => formRef.value?.clearValidate()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.w-full {
|
|
width: 100%;
|
|
}
|
|
.ml-1 {
|
|
margin-left: 4px;
|
|
}
|
|
.cursor-help {
|
|
cursor: help;
|
|
}
|
|
</style>
|