581 lines
18 KiB
Vue
581 lines
18 KiB
Vue
<template>
|
|
<div class="dynamic-component">
|
|
<el-form-item
|
|
:label="config.name"
|
|
:required="isRequired"
|
|
:error="hasError ? errorMessage : undefined"
|
|
>
|
|
<!-- 输入框 -->
|
|
<el-input
|
|
v-if="config.renderType === 'input'"
|
|
v-model="inputValue"
|
|
:type="getInputType()"
|
|
:placeholder="config.description"
|
|
:disabled="disabled"
|
|
clearable
|
|
@blur="handleValidation"
|
|
@input="handleInput"
|
|
/>
|
|
|
|
<!-- 数字输入框 -->
|
|
<el-input-number
|
|
v-else-if="config.renderType === 'number'"
|
|
v-model="inputValue"
|
|
:placeholder="config.description"
|
|
:disabled="disabled"
|
|
:min="getNumberMin()"
|
|
:max="getNumberMax()"
|
|
:precision="getNumberPrecision()"
|
|
@blur="handleValidation"
|
|
@change="handleInput"
|
|
/>
|
|
|
|
<!-- 文本域 -->
|
|
<el-input
|
|
v-else-if="config.renderType === 'textarea'"
|
|
v-model="inputValue"
|
|
type="textarea"
|
|
:placeholder="config.description"
|
|
:disabled="disabled"
|
|
:rows="4"
|
|
resize="vertical"
|
|
@blur="handleValidation"
|
|
@input="handleInput"
|
|
/>
|
|
|
|
<!-- 下拉选择 -->
|
|
<el-select
|
|
v-else-if="config.renderType === 'select'"
|
|
v-model="inputValue"
|
|
:placeholder="`请选择${config.name || ''}`"
|
|
:disabled="disabled"
|
|
clearable
|
|
filterable
|
|
@change="handleInput"
|
|
@blur="handleValidation"
|
|
>
|
|
<el-option
|
|
v-for="option in selectOptions"
|
|
:key="option.value"
|
|
:label="option.label"
|
|
:value="option.value"
|
|
/>
|
|
</el-select>
|
|
|
|
<!-- 多选下拉 -->
|
|
<el-select
|
|
v-else-if="config.renderType === 'multiSelect'"
|
|
v-model="checkboxValues"
|
|
:placeholder="`请选择${config.name || ''}`"
|
|
:disabled="disabled"
|
|
multiple
|
|
collapse-tags
|
|
filterable
|
|
@change="handleCheckboxChange"
|
|
@blur="handleValidation"
|
|
>
|
|
<el-option
|
|
v-for="option in selectOptions"
|
|
:key="option.value"
|
|
:label="option.label"
|
|
:value="option.value"
|
|
/>
|
|
</el-select>
|
|
|
|
<!-- 复选框组 -->
|
|
<el-checkbox-group
|
|
v-else-if="config.renderType === 'checkbox'"
|
|
v-model="checkboxValues"
|
|
:disabled="disabled"
|
|
@change="handleCheckboxChange"
|
|
>
|
|
<el-checkbox
|
|
v-for="option in checkboxOptions"
|
|
:key="option.value"
|
|
:label="option.value"
|
|
>
|
|
{{ option.label }}
|
|
</el-checkbox>
|
|
</el-checkbox-group>
|
|
|
|
<!-- 单选框组 -->
|
|
<el-radio-group
|
|
v-else-if="config.renderType === 'radio'"
|
|
v-model="inputValue"
|
|
:disabled="disabled"
|
|
@change="handleInput"
|
|
>
|
|
<el-radio
|
|
v-for="option in radioOptions"
|
|
:key="option.value"
|
|
:label="option.value"
|
|
>
|
|
{{ option.label }}
|
|
</el-radio>
|
|
</el-radio-group>
|
|
|
|
<!-- 开关 -->
|
|
<el-switch
|
|
v-else-if="config.renderType === 'switch'"
|
|
v-model="switchValue"
|
|
:disabled="disabled"
|
|
active-text="开启"
|
|
inactive-text="关闭"
|
|
@change="handleSwitchChange"
|
|
/>
|
|
|
|
<!-- 日期选择器 -->
|
|
<el-date-picker
|
|
v-else-if="config.renderType === 'date'"
|
|
v-model="inputValue"
|
|
type="date"
|
|
:placeholder="`选择${config.name || '日期'}`"
|
|
:disabled="disabled"
|
|
format="YYYY-MM-DD"
|
|
value-format="YYYY-MM-DD"
|
|
@change="handleInput"
|
|
@blur="handleValidation"
|
|
/>
|
|
|
|
<!-- 日期时间选择器 -->
|
|
<el-date-picker
|
|
v-else-if="config.renderType === 'datetime'"
|
|
v-model="inputValue"
|
|
type="datetime"
|
|
:placeholder="`选择${config.name || '日期时间'}`"
|
|
:disabled="disabled"
|
|
format="YYYY-MM-DD HH:mm:ss"
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
@change="handleInput"
|
|
@blur="handleValidation"
|
|
/>
|
|
|
|
<!-- 日期范围选择器 -->
|
|
<el-date-picker
|
|
v-else-if="config.renderType === 'dateRange'"
|
|
v-model="dateRangeValue"
|
|
type="daterange"
|
|
range-separator="至"
|
|
start-placeholder="开始日期"
|
|
end-placeholder="结束日期"
|
|
:disabled="disabled"
|
|
format="YYYY-MM-DD"
|
|
value-format="YYYY-MM-DD"
|
|
@change="handleDateRangeChange"
|
|
@blur="handleValidation"
|
|
/>
|
|
|
|
<!-- 日期时间范围选择器 -->
|
|
<el-date-picker
|
|
v-else-if="config.renderType === 'datetimeRange'"
|
|
v-model="dateRangeValue"
|
|
type="datetimerange"
|
|
range-separator="至"
|
|
start-placeholder="开始时间"
|
|
end-placeholder="结束时间"
|
|
:disabled="disabled"
|
|
format="YYYY-MM-DD HH:mm:ss"
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
@change="handleDateRangeChange"
|
|
@blur="handleValidation"
|
|
/>
|
|
|
|
<!-- 时间选择器 -->
|
|
<el-time-picker
|
|
v-else-if="config.renderType === 'time'"
|
|
v-model="inputValue"
|
|
:placeholder="`选择${config.name || '时间'}`"
|
|
:disabled="disabled"
|
|
format="HH:mm:ss"
|
|
value-format="HH:mm:ss"
|
|
@change="handleInput"
|
|
@blur="handleValidation"
|
|
/>
|
|
|
|
<!-- 颜色选择器 -->
|
|
<el-color-picker
|
|
v-else-if="config.renderType === 'color'"
|
|
v-model="inputValue"
|
|
:disabled="disabled"
|
|
show-alpha
|
|
@change="handleInput"
|
|
/>
|
|
|
|
<!-- 评分 -->
|
|
<el-rate
|
|
v-else-if="config.renderType === 'rate'"
|
|
v-model="inputValue"
|
|
:disabled="disabled"
|
|
:max="getRateMax()"
|
|
show-score
|
|
@change="handleInput"
|
|
/>
|
|
|
|
<!-- 滑块 -->
|
|
<el-slider
|
|
v-else-if="config.renderType === 'slider'"
|
|
v-model="inputValue"
|
|
:disabled="disabled"
|
|
:min="getNumberMin()"
|
|
:max="getNumberMax()"
|
|
:step="getSliderStep()"
|
|
show-input
|
|
@change="handleInput"
|
|
/>
|
|
|
|
<!-- 级联选择器 -->
|
|
<el-cascader
|
|
v-else-if="config.renderType === 'cascader'"
|
|
v-model="inputValue"
|
|
:options="cascaderOptions"
|
|
:placeholder="`请选择${config.name || ''}`"
|
|
:disabled="disabled"
|
|
clearable
|
|
filterable
|
|
@change="handleInput"
|
|
@blur="handleValidation"
|
|
/>
|
|
|
|
<!-- 树形选择 -->
|
|
<el-tree-select
|
|
v-else-if="config.renderType === 'treeSelect'"
|
|
v-model="inputValue"
|
|
:data="treeOptions"
|
|
:placeholder="`请选择${config.name || ''}`"
|
|
:disabled="disabled"
|
|
clearable
|
|
filterable
|
|
@change="handleInput"
|
|
/>
|
|
|
|
<!-- 上传组件 -->
|
|
<el-upload
|
|
v-else-if="config.renderType === 'upload'"
|
|
class="upload-demo"
|
|
:action="uploadAction"
|
|
:on-success="handleUploadSuccess"
|
|
:file-list="fileList"
|
|
:disabled="disabled"
|
|
:limit="getUploadLimit()"
|
|
:accept="getUploadAccept()"
|
|
>
|
|
<el-button type="primary" :disabled="disabled">
|
|
<el-icon><Upload /></el-icon>
|
|
上传文件
|
|
</el-button>
|
|
</el-upload>
|
|
|
|
<template #label v-if="config.description">
|
|
<span>{{ config.name }}</span>
|
|
<el-tooltip :content="config.description" placement="top">
|
|
<el-icon class="description-icon"><QuestionFilled /></el-icon>
|
|
</el-tooltip>
|
|
</template>
|
|
</el-form-item>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, watch, onMounted } from 'vue'
|
|
import type { SysConfigVO } from '@/types/sys/config'
|
|
|
|
interface Props {
|
|
config: SysConfigVO
|
|
modelValue?: any
|
|
disabled?: boolean
|
|
required?: boolean
|
|
}
|
|
|
|
interface Emits {
|
|
(e: 'update:modelValue', value: any): void
|
|
(e: 'change', value: any): void
|
|
(e: 'blur', value: any): void
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
disabled: false,
|
|
required: false
|
|
})
|
|
|
|
const emit = defineEmits<Emits>()
|
|
|
|
// 内部值状态
|
|
const inputValue = ref('')
|
|
const checkboxValues = ref<string[]>([])
|
|
const switchValue = ref(false)
|
|
const dateRangeValue = ref<[string, string] | null>(null)
|
|
const fileList = ref<any[]>([])
|
|
|
|
// 错误状态
|
|
const hasError = ref(false)
|
|
const errorMessage = ref('')
|
|
|
|
// 计算属性
|
|
const isRequired = computed(() => props.required || (props.config.re && props.config.re.required))
|
|
|
|
const selectOptions = computed(() => {
|
|
if (!props.config.options) return []
|
|
if (Array.isArray(props.config.options)) {
|
|
return props.config.options
|
|
}
|
|
// 如果是对象形式,转换为数组
|
|
return Object.entries(props.config.options).map(([key, value]) => ({
|
|
value: key,
|
|
label: value as string
|
|
}))
|
|
})
|
|
|
|
const checkboxOptions = computed(() => selectOptions.value)
|
|
const radioOptions = computed(() => selectOptions.value)
|
|
|
|
// 扩展选项计算属性
|
|
const cascaderOptions = computed(() => {
|
|
if (!props.config.options) return []
|
|
return Array.isArray(props.config.options) ? props.config.options : []
|
|
})
|
|
|
|
const treeOptions = computed(() => {
|
|
if (!props.config.options) return []
|
|
return Array.isArray(props.config.options) ? props.config.options : []
|
|
})
|
|
|
|
// 上传相关配置
|
|
const uploadAction = computed(() => {
|
|
return props.config.re?.uploadUrl || '/api/upload'
|
|
})
|
|
|
|
// 获取输入框类型
|
|
const getInputType = () => {
|
|
const configType = props.config.configType?.toLowerCase()
|
|
switch (configType) {
|
|
case 'integer':
|
|
case 'float':
|
|
case 'double':
|
|
return 'number'
|
|
case 'boolean':
|
|
return 'checkbox'
|
|
default:
|
|
return 'text'
|
|
}
|
|
}
|
|
|
|
// 值转换函数
|
|
const convertValue = (value: any) => {
|
|
if (value === null || value === undefined || value === '') {
|
|
return value
|
|
}
|
|
|
|
const configType = props.config.configType?.toLowerCase()
|
|
switch (configType) {
|
|
case 'integer':
|
|
return parseInt(value, 10)
|
|
case 'float':
|
|
case 'double':
|
|
return parseFloat(value)
|
|
case 'boolean':
|
|
return Boolean(value)
|
|
default:
|
|
return String(value)
|
|
}
|
|
}
|
|
|
|
// 获取数字相关配置
|
|
const getNumberMin = () => {
|
|
return props.config.re?.min ?? 0
|
|
}
|
|
|
|
const getNumberMax = () => {
|
|
return props.config.re?.max ?? 100
|
|
}
|
|
|
|
const getNumberPrecision = () => {
|
|
const configType = props.config.configType?.toLowerCase()
|
|
if (configType === 'integer') return 0
|
|
return props.config.re?.precision ?? 2
|
|
}
|
|
|
|
const getSliderStep = () => {
|
|
return props.config.re?.step ?? 1
|
|
}
|
|
|
|
const getRateMax = () => {
|
|
return props.config.re?.max ?? 5
|
|
}
|
|
|
|
const getUploadLimit = () => {
|
|
return props.config.re?.limit ?? 1
|
|
}
|
|
|
|
const getUploadAccept = () => {
|
|
return props.config.re?.accept || '*'
|
|
}
|
|
|
|
// 校验函数
|
|
const validateValue = (value: any): { valid: boolean; message?: string } => {
|
|
const rules = props.config.re
|
|
if (!rules) return { valid: true }
|
|
|
|
// 必填校验
|
|
if (rules.required && (!value || (Array.isArray(value) && value.length === 0))) {
|
|
return { valid: false, message: `${props.config.name}是必填项` }
|
|
}
|
|
|
|
// 如果值为空且非必填,跳过其他校验
|
|
if (!value && !rules.required) {
|
|
return { valid: true }
|
|
}
|
|
|
|
// 正则校验
|
|
if (rules.pattern && typeof value === 'string') {
|
|
const regex = new RegExp(rules.pattern)
|
|
if (!regex.test(value)) {
|
|
return { valid: false, message: rules.message || `${props.config.name}格式不正确` }
|
|
}
|
|
}
|
|
|
|
// 最小长度校验
|
|
if (rules.minLength && value.length < rules.minLength) {
|
|
return { valid: false, message: `${props.config.name}最少需要${rules.minLength}个字符` }
|
|
}
|
|
|
|
// 最大长度校验
|
|
if (rules.maxLength && value.length > rules.maxLength) {
|
|
return { valid: false, message: `${props.config.name}最多允许${rules.maxLength}个字符` }
|
|
}
|
|
|
|
// 最小值校验
|
|
if (rules.min !== undefined && Number(value) < rules.min) {
|
|
return { valid: false, message: `${props.config.name}不能小于${rules.min}` }
|
|
}
|
|
|
|
// 最大值校验
|
|
if (rules.max !== undefined && Number(value) > rules.max) {
|
|
return { valid: false, message: `${props.config.name}不能大于${rules.max}` }
|
|
}
|
|
|
|
return { valid: true }
|
|
}
|
|
|
|
// 事件处理
|
|
const handleInput = () => {
|
|
let value = inputValue.value
|
|
|
|
// 数据类型转换
|
|
value = convertValue(value)
|
|
|
|
emit('update:modelValue', value)
|
|
emit('change', value)
|
|
|
|
// 清除错误状态(输入时)
|
|
if (hasError.value) {
|
|
hasError.value = false
|
|
errorMessage.value = ''
|
|
}
|
|
}
|
|
|
|
const handleCheckboxChange = () => {
|
|
// 复选框直接使用数组值,不进行转换
|
|
const value = checkboxValues.value
|
|
emit('update:modelValue', value)
|
|
emit('change', value)
|
|
}
|
|
|
|
const handleSwitchToggle = () => {
|
|
if (props.disabled) return
|
|
|
|
switchValue.value = !switchValue.value
|
|
const value = convertValue(switchValue.value)
|
|
|
|
emit('update:modelValue', value)
|
|
emit('change', value)
|
|
}
|
|
|
|
const handleSwitchChange = (value: boolean) => {
|
|
switchValue.value = value
|
|
emit('update:modelValue', value)
|
|
emit('change', value)
|
|
}
|
|
|
|
const handleDateRangeChange = (value: [string, string] | null) => {
|
|
dateRangeValue.value = value
|
|
emit('update:modelValue', value)
|
|
emit('change', value)
|
|
}
|
|
|
|
const handleUploadSuccess = (response: any, file: any, fileList: any[]) => {
|
|
const urls = fileList.map(item => item.response?.url || item.url).filter(Boolean)
|
|
emit('update:modelValue', urls)
|
|
emit('change', urls)
|
|
}
|
|
|
|
const handleValidation = () => {
|
|
let value: any = inputValue.value
|
|
|
|
// 处理复选框和多选下拉的值
|
|
if (props.config.renderType === 'checkbox' || props.config.renderType === 'multiSelect') {
|
|
value = checkboxValues.value
|
|
}
|
|
// 处理开关的值
|
|
else if (props.config.renderType === 'switch') {
|
|
value = switchValue.value
|
|
}
|
|
// 处理日期范围的值
|
|
else if (props.config.renderType === 'dateRange' || props.config.renderType === 'datetimeRange') {
|
|
value = dateRangeValue.value
|
|
}
|
|
|
|
const validation = validateValue(value)
|
|
hasError.value = !validation.valid
|
|
errorMessage.value = validation.message || ''
|
|
|
|
emit('blur', value)
|
|
}
|
|
|
|
// 初始化值
|
|
const initializeValue = () => {
|
|
const modelValue = props.modelValue
|
|
|
|
if (props.config.renderType === 'checkbox' || props.config.renderType === 'multiSelect') {
|
|
if (Array.isArray(modelValue)) {
|
|
checkboxValues.value = modelValue
|
|
} else if (typeof modelValue === 'string' && modelValue) {
|
|
// 处理逗号分隔的字符串
|
|
checkboxValues.value = modelValue.split(',').map(v => v.trim())
|
|
} else {
|
|
checkboxValues.value = modelValue ? [modelValue] : []
|
|
}
|
|
} else if (props.config.renderType === 'switch') {
|
|
switchValue.value = Boolean(modelValue)
|
|
} else if (props.config.renderType === 'dateRange' || props.config.renderType === 'datetimeRange') {
|
|
dateRangeValue.value = Array.isArray(modelValue) && modelValue.length === 2 ? modelValue as [string, string] : null
|
|
} else {
|
|
inputValue.value = modelValue || props.config.value || ''
|
|
}
|
|
}
|
|
|
|
// 监听 modelValue 变化
|
|
watch(() => props.modelValue, () => {
|
|
initializeValue()
|
|
}, { immediate: true })
|
|
|
|
// 组件挂载时初始化
|
|
onMounted(() => {
|
|
initializeValue()
|
|
})
|
|
|
|
// 暴露校验方法给父组件
|
|
defineExpose({
|
|
validate: () => {
|
|
handleValidation()
|
|
return !hasError.value
|
|
},
|
|
clearError: () => {
|
|
hasError.value = false
|
|
errorMessage.value = ''
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
@import url("./DynamicComponent.scss");
|
|
</style> |