动态表单
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
// Base components placeholder
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
.dynamic-component {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
// Element Plus 表单项适配
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
|
||||||
|
.el-form-item__label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item__content {
|
||||||
|
.el-input,
|
||||||
|
.el-select,
|
||||||
|
.el-date-editor,
|
||||||
|
.el-time-picker,
|
||||||
|
.el-cascader,
|
||||||
|
.el-tree-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数字输入框
|
||||||
|
.el-input-number {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.el-input__wrapper {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复选框组和单选框组
|
||||||
|
.el-checkbox-group,
|
||||||
|
.el-radio-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.el-checkbox,
|
||||||
|
.el-radio {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开关组件
|
||||||
|
.el-switch {
|
||||||
|
.el-switch__label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滑块组件
|
||||||
|
.el-slider {
|
||||||
|
.el-slider__input {
|
||||||
|
width: 130px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 评分组件
|
||||||
|
.el-rate {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.el-rate__text {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 颜色选择器
|
||||||
|
.el-color-picker {
|
||||||
|
.el-color-picker__trigger {
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #c0c4cc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传组件
|
||||||
|
.el-upload {
|
||||||
|
&.upload-demo {
|
||||||
|
.el-upload__tip {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 级联选择器
|
||||||
|
.el-cascader {
|
||||||
|
.el-cascader__tags {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 树形选择
|
||||||
|
.el-tree-select {
|
||||||
|
.el-tree-select__tags {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误状态样式
|
||||||
|
&.is-error {
|
||||||
|
.el-input__wrapper,
|
||||||
|
.el-select__wrapper,
|
||||||
|
.el-date-editor,
|
||||||
|
.el-cascader__wrapper,
|
||||||
|
.el-tree-select__wrapper {
|
||||||
|
box-shadow: 0 0 0 1px #f56565 inset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 必填标记
|
||||||
|
&.is-required {
|
||||||
|
.el-form-item__label::before {
|
||||||
|
content: '*';
|
||||||
|
color: #f56565;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 描述信息图标
|
||||||
|
.description-icon {
|
||||||
|
margin-left: 4px;
|
||||||
|
color: #909399;
|
||||||
|
cursor: help;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义样式调整
|
||||||
|
.el-form-item__content {
|
||||||
|
// 确保组件占满宽度
|
||||||
|
.el-input,
|
||||||
|
.el-select,
|
||||||
|
.el-date-editor,
|
||||||
|
.el-cascader,
|
||||||
|
.el-tree-select {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日期范围选择器特殊处理
|
||||||
|
.el-date-editor--daterange,
|
||||||
|
.el-date-editor--datetimerange {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多选标签折叠样式
|
||||||
|
.el-tag {
|
||||||
|
margin-right: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
.el-form-item__content {
|
||||||
|
.el-checkbox-group,
|
||||||
|
.el-radio-group {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,581 @@
|
|||||||
<template>
|
<template>
|
||||||
</template>
|
<div class="dynamic-component">
|
||||||
<script setup lang="ts">
|
<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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import url("./DynamicComponent.scss");
|
@import url("./DynamicComponent.scss");
|
||||||
</style>
|
</style>
|
||||||
@@ -1,7 +1,400 @@
|
|||||||
<template>
|
<template>
|
||||||
</template>
|
<div class="dynamic-component-example">
|
||||||
<script setup lang="ts">
|
<h2>动态组件示例</h2>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>输入框</h3>
|
||||||
|
<DynamicComponent
|
||||||
|
:config="inputConfig"
|
||||||
|
v-model="inputValue"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
<p>当前值: {{ inputValue }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>下拉选择</h3>
|
||||||
|
<DynamicComponent
|
||||||
|
:config="selectConfig"
|
||||||
|
v-model="selectValue"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
<p>当前值: {{ selectValue }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>复选框组</h3>
|
||||||
|
<DynamicComponent
|
||||||
|
:config="checkboxConfig"
|
||||||
|
v-model="checkboxValue"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
<p>当前值: {{ checkboxValue }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>单选框组</h3>
|
||||||
|
<DynamicComponent
|
||||||
|
:config="radioConfig"
|
||||||
|
v-model="radioValue"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
<p>当前值: {{ radioValue }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>开关</h3>
|
||||||
|
<DynamicComponent
|
||||||
|
:config="switchConfig"
|
||||||
|
v-model="switchValue"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
<p>当前值: {{ switchValue }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>文本域</h3>
|
||||||
|
<DynamicComponent
|
||||||
|
:config="textareaConfig"
|
||||||
|
v-model="textareaValue"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
<p>当前值: {{ textareaValue }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>数字输入框</h3>
|
||||||
|
<DynamicComponent
|
||||||
|
:config="numberConfig"
|
||||||
|
v-model="numberValue"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
<p>当前值: {{ numberValue }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>日期选择器</h3>
|
||||||
|
<DynamicComponent
|
||||||
|
:config="dateConfig"
|
||||||
|
v-model="dateValue"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
<p>当前值: {{ dateValue }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>日期时间选择器</h3>
|
||||||
|
<DynamicComponent
|
||||||
|
:config="datetimeConfig"
|
||||||
|
v-model="datetimeValue"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
<p>当前值: {{ datetimeValue }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>日期范围选择器</h3>
|
||||||
|
<DynamicComponent
|
||||||
|
:config="dateRangeConfig"
|
||||||
|
v-model="dateRangeValue"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
<p>当前值: {{ dateRangeValue }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>时间选择器</h3>
|
||||||
|
<DynamicComponent
|
||||||
|
:config="timeConfig"
|
||||||
|
v-model="timeValue"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
<p>当前值: {{ timeValue }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>颜色选择器</h3>
|
||||||
|
<DynamicComponent
|
||||||
|
:config="colorConfig"
|
||||||
|
v-model="colorValue"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
<p>当前值: {{ colorValue }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>评分组件</h3>
|
||||||
|
<DynamicComponent
|
||||||
|
:config="rateConfig"
|
||||||
|
v-model="rateValue"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
<p>当前值: {{ rateValue }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>滑块组件</h3>
|
||||||
|
<DynamicComponent
|
||||||
|
:config="sliderConfig"
|
||||||
|
v-model="sliderValue"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
<p>当前值: {{ sliderValue }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>多选下拉</h3>
|
||||||
|
<DynamicComponent
|
||||||
|
:config="multiSelectConfig"
|
||||||
|
v-model="multiSelectValue"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
<p>当前值: {{ multiSelectValue }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import DynamicComponent from './DynamicComponent.vue'
|
||||||
|
import type { SysConfigVO } from '@/types/sys/config'
|
||||||
|
|
||||||
|
// 表单值
|
||||||
|
const inputValue = ref('')
|
||||||
|
const selectValue = ref('')
|
||||||
|
const checkboxValue = ref(['music', 'sports'])
|
||||||
|
const radioValue = ref('')
|
||||||
|
const switchValue = ref(false)
|
||||||
|
const textareaValue = ref('')
|
||||||
|
const numberValue = ref(0)
|
||||||
|
const dateValue = ref('')
|
||||||
|
const datetimeValue = ref('')
|
||||||
|
const dateRangeValue = ref(null)
|
||||||
|
const timeValue = ref('')
|
||||||
|
const colorValue = ref('#409eff')
|
||||||
|
const rateValue = ref(0)
|
||||||
|
const sliderValue = ref(50)
|
||||||
|
const multiSelectValue = ref([])
|
||||||
|
|
||||||
|
// 配置对象
|
||||||
|
const inputConfig: SysConfigVO = {
|
||||||
|
configId: 'input-demo',
|
||||||
|
name: '用户名',
|
||||||
|
description: '请输入用户名',
|
||||||
|
renderType: 'input',
|
||||||
|
configType: 'String',
|
||||||
|
re: {
|
||||||
|
required: true,
|
||||||
|
minLength: 3,
|
||||||
|
maxLength: 20,
|
||||||
|
pattern: '^[a-zA-Z0-9_]+$',
|
||||||
|
message: '用户名只能包含字母、数字和下划线'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectConfig: SysConfigVO = {
|
||||||
|
configId: 'select-demo',
|
||||||
|
name: '用户角色',
|
||||||
|
description: '请选择用户角色',
|
||||||
|
renderType: 'select',
|
||||||
|
configType: 'String',
|
||||||
|
options: [
|
||||||
|
{ value: 'admin', label: '管理员' },
|
||||||
|
{ value: 'user', label: '普通用户' },
|
||||||
|
{ value: 'guest', label: '访客' }
|
||||||
|
],
|
||||||
|
re: {
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkboxConfig: SysConfigVO = {
|
||||||
|
configId: 'checkbox-demo',
|
||||||
|
name: '兴趣爱好',
|
||||||
|
description: '选择你的兴趣爱好',
|
||||||
|
renderType: 'checkbox',
|
||||||
|
configType: 'String',
|
||||||
|
options: [
|
||||||
|
{ value: 'reading', label: '阅读' },
|
||||||
|
{ value: 'music', label: '音乐' },
|
||||||
|
{ value: 'sports', label: '运动' },
|
||||||
|
{ value: 'travel', label: '旅行' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const radioConfig: SysConfigVO = {
|
||||||
|
configId: 'radio-demo',
|
||||||
|
name: '性别',
|
||||||
|
description: '选择你的性别',
|
||||||
|
renderType: 'radio',
|
||||||
|
configType: 'String',
|
||||||
|
options: [
|
||||||
|
{ value: 'male', label: '男' },
|
||||||
|
{ value: 'female', label: '女' },
|
||||||
|
{ value: 'other', label: '其他' }
|
||||||
|
],
|
||||||
|
re: {
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const switchConfig: SysConfigVO = {
|
||||||
|
configId: 'switch-demo',
|
||||||
|
name: '邮件通知',
|
||||||
|
description: '是否接收邮件通知',
|
||||||
|
renderType: 'switch',
|
||||||
|
configType: 'Boolean'
|
||||||
|
}
|
||||||
|
|
||||||
|
const textareaConfig: SysConfigVO = {
|
||||||
|
configId: 'textarea-demo',
|
||||||
|
name: '个人简介',
|
||||||
|
description: '请输入个人简介',
|
||||||
|
renderType: 'textarea',
|
||||||
|
configType: 'String',
|
||||||
|
re: {
|
||||||
|
maxLength: 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const numberConfig: SysConfigVO = {
|
||||||
|
configId: 'number-demo',
|
||||||
|
name: '年龄',
|
||||||
|
description: '请输入年龄',
|
||||||
|
renderType: 'number',
|
||||||
|
configType: 'Integer',
|
||||||
|
re: {
|
||||||
|
required: true,
|
||||||
|
min: 18,
|
||||||
|
max: 120
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateConfig: SysConfigVO = {
|
||||||
|
configId: 'date-demo',
|
||||||
|
name: '出生日期',
|
||||||
|
description: '选择出生日期',
|
||||||
|
renderType: 'date',
|
||||||
|
configType: 'String',
|
||||||
|
re: {
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const datetimeConfig: SysConfigVO = {
|
||||||
|
configId: 'datetime-demo',
|
||||||
|
name: '预约时间',
|
||||||
|
description: '选择预约时间',
|
||||||
|
renderType: 'datetime',
|
||||||
|
configType: 'String',
|
||||||
|
re: {
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateRangeConfig: SysConfigVO = {
|
||||||
|
configId: 'daterange-demo',
|
||||||
|
name: '活动时间',
|
||||||
|
description: '选择活动时间范围',
|
||||||
|
renderType: 'dateRange',
|
||||||
|
configType: 'String'
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeConfig: SysConfigVO = {
|
||||||
|
configId: 'time-demo',
|
||||||
|
name: '提醒时间',
|
||||||
|
description: '选择提醒时间',
|
||||||
|
renderType: 'time',
|
||||||
|
configType: 'String'
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorConfig: SysConfigVO = {
|
||||||
|
configId: 'color-demo',
|
||||||
|
name: '主题颜色',
|
||||||
|
description: '选择主题颜色',
|
||||||
|
renderType: 'color',
|
||||||
|
configType: 'String'
|
||||||
|
}
|
||||||
|
|
||||||
|
const rateConfig: SysConfigVO = {
|
||||||
|
configId: 'rate-demo',
|
||||||
|
name: '满意度评分',
|
||||||
|
description: '请为服务打分',
|
||||||
|
renderType: 'rate',
|
||||||
|
configType: 'Integer',
|
||||||
|
re: {
|
||||||
|
max: 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sliderConfig: SysConfigVO = {
|
||||||
|
configId: 'slider-demo',
|
||||||
|
name: '完成进度',
|
||||||
|
description: '设置完成进度',
|
||||||
|
renderType: 'slider',
|
||||||
|
configType: 'Integer',
|
||||||
|
re: {
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
step: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const multiSelectConfig: SysConfigVO = {
|
||||||
|
configId: 'multiselect-demo',
|
||||||
|
name: '技能标签',
|
||||||
|
description: '选择你擅长的技能',
|
||||||
|
renderType: 'multiSelect',
|
||||||
|
configType: 'String',
|
||||||
|
options: [
|
||||||
|
{ value: 'javascript', label: 'JavaScript' },
|
||||||
|
{ value: 'typescript', label: 'TypeScript' },
|
||||||
|
{ value: 'vue', label: 'Vue.js' },
|
||||||
|
{ value: 'react', label: 'React' },
|
||||||
|
{ value: 'nodejs', label: 'Node.js' },
|
||||||
|
{ value: 'python', label: 'Python' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 事件处理
|
||||||
|
const handleChange = (value: any) => {
|
||||||
|
console.log('Value changed:', value)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.dynamic-component-example {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as DynamicComponent } from './DynamicComponent.vue'
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './fileupload'
|
export * from './fileupload'
|
||||||
export * from './base'
|
export * from './base'
|
||||||
|
export * from './dynamicComponent'
|
||||||
Reference in New Issue
Block a user