Files
urbanLifeline/urbanLifelineWeb/packages/shared/src/components/dynamicComponent/DynamicComponent.vue
2025-12-08 18:18:13 +08:00

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>