动态表单

This commit is contained in:
2025-12-08 18:18:13 +08:00
parent 4f1f455660
commit fa3ef3c310
6 changed files with 1155 additions and 5 deletions

View File

@@ -0,0 +1,2 @@
// Base components placeholder
export {};

View File

@@ -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;
}
}
}
}
}

View File

@@ -1,8 +1,581 @@
<template>
</template>
<script setup lang="ts">
<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>

View File

@@ -1,7 +1,400 @@
<template>
</template>
<script setup lang="ts">
<div class="dynamic-component-example">
<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>
<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>

View File

@@ -0,0 +1 @@
export { default as DynamicComponent } from './DynamicComponent.vue'

View File

@@ -1,2 +1,3 @@
export * from './fileupload'
export * from './base'
export * from './dynamicComponent'