动态表格加动态表单以及动态搜索框 有疑问可私信我。

引用示例
<template>
<div class=" layout-Dynamic">
<DynamicForm
ref="dialogForm"
:form-config="FormConfig"
@submit="handleSubmit"
/>
<DynamicSearch class="container-Dynamic" :fields="SearchFormFields" />
<DynamicTable
ref="mainTable"
class="container-Dynamic"
:table-config="TableConfig"
@selection-change="handleMultiSelect"
@cell-update="handleCellUpdate"
@link-click="linkClick"
/>
</div>
</template>
<script>
import DynamicForm from '@/components/DynamicTableForm/Form/index.vue'
import DynamicSearch from '@/components/DynamicTableForm/Search/index.vue'
import DynamicTable from '@/components/DynamicTableForm/Table/index.vue'
import { validatePassword } from '@/components/DynamicTableForm/validate'
let pasword = ''
export default {
components: {
DynamicForm,
DynamicSearch,
DynamicTable
},
data() {
return {
TableConfig: {
tableKey: 'mainTable',
columns: [
{
prop: 'name',
label: '姓名',
type: 'link',
width: 120
},
{
prop: 'avatar',
label: '头像',
type: 'image',
width: 100
},
{
prop: 'status',
label: '状态',
type: 'switch'
// disabled: row => row.role === 'admin'
// handler: (val, row) => updateStatus(row.id, val)
},
{
label: '销售数据',
children: [
{ prop: 'q1', label: '第一季度' },
{ prop: 'q2', label: '第二季度' }
]
}
],
actions: {
multiple: [
{
label: '批量删除',
type: 'danger',
key: 'delete'
},
{
label: '新增',
key: 'add',
method: () => {
console.log('新增')
this.$refs.dialogForm.open()
// this.FormConfig.dialogActions.visible = true
}
}
],
single: [
{
label: '编辑',
icon: 'el-icon-edit'
// visible: row => !row.disabled
// handler: row => openEditDialog(row)
},
{
label: '删除',
type: 'danger'
// disabled: row => row.protected
}
]
},
pagination: {
page: 1,
size: 10
},
total: 2,
tableData: [
{
name: 'Tom',
role: 'admin',
status: 0,
q1: 100,
q2: 200,
avatar: ['https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg']
},
{
name: 'Tom',
role: 'admin',
status: 1,
q1: 100,
q2: 200
}
]
},
SearchFormFields: [
{
label: '用户名',
type: 'input',
prop: 'phone'
},
{
type: 'cascader',
prop: 'category',
label: '级联选择',
props: {
options: [
{
id: 3,
value: '1',
label: '分类1',
children: [
{
id: 1,
value: '2',
label: '子分类1'
},
{
id: 2,
value: '3',
label: '子分类2'
}
]
}
] // 需通过接口获取分类数据
}
},
{
type: 'daterange',
prop: 'publishDate',
label: '发布时间'
},
{
type: 'select',
prop: 'city',
label: '城市',
placeholder: '请选择城市',
// 必须提供 options 数组
options: [
{ value: 'sh', label: '上海' },
{ value: 'bj', label: '北京' },
{ value: 'sz', label: '深圳' }
]
}
],
FormConfig: {
dialogActions: {
visible: false,
title: '示例'
},
fields: [
{
label: '用户名',
type: 'input',
prop: 'phone',
props: {
size: 'mini'
},
rules: [
{ required: true, message: '用户名不能为空', trigger: 'blur' },
{ min: 3, max: 12, message: '长度3-12个字符', trigger: 'blur' }
]
},
{
type: 'password',
prop: 'password',
label: '密码',
rules: [
{ validator: validatePassword, trigger: 'blur' }
]
},
{
type: 'password',
prop: 'confirmPwd',
label: '确认密码',
method: (row) => {
if (row.password) {
pasword = row.password
}
},
rules: [
{
validator: (rule, value, callback) => {
if (value !== pasword) {
callback(new Error('两次输入密码不一致'))
} else {
callback()
}
},
trigger: 'blur'
}
]
},
{
type: 'input',
prop: 'email',
label: '邮箱',
rules: [
{ required: true, message: '邮箱必填' },
{ type: 'email', message: '邮箱格式无效' }
]
},
{
type: 'input',
prop: 'title',
label: '文章标题',
props: {
maxlength: 50,
showWordLimit: true
}
},
{
type: 'daterange',
prop: 'publishDate',
label: '发布时间'
},
{
type: 'switch',
prop: 'switch',
label: '状态',
options: [
{
value: 1,
label: '启用'
},
{
value: 2,
label: '禁用'
}
]
},
{
type: 'select',
prop: 'city',
label: '城市',
// 必须提供 options 数组
options: [
{ value: 'sh', label: '上海' },
{ value: 'bj', label: '北京', disabled: true }, // 禁用选项示例
{ value: 'sz', label: '深圳' }
],
props: {
placeholder: '请选择城市' // 可覆盖默认配置
}
},
{
type: 'cascader',
prop: 'category',
label: '级联选择',
props: {
options: [
{
id: 3,
value: '1',
label: '分类1',
children: [
{
id: 1,
value: '2',
label: '子分类1'
},
{
id: 2,
value: '3',
label: '子分类2'
}
]
}
] // 需通过接口获取分类数据
}
},
{
type: 'input-number',
prop: 'number',
label: '数字',
props: {
controlsPosition: 'right'
},
rules: [
{ required: true, message: '数字必填' }
]
},
{
type: 'radio-group',
prop: 'tags',
label: '标签',
props: {
size: 'mini'
},
options: [
{
value: '1',
label: '标签1'
},
{
value: '2',
label: '标签2'
}
]
},
{
type: 'radio-group',
prop: 'tags22',
label: '标签',
options: [
{
value: '1',
label: '标签1'
},
{
value: '2',
label: '标签2'
}
]
},
{
type: 'editor',
prop: 'content',
label: '内容',
rules: [
{ required: true, message: '内容必填' }
],
colSpan: 24
}
]
}
}
},
created() {
},
methods: {
handleMultiSelect(val) {
},
handleCellUpdate({ row, column, newValue }) {
// api.updateCell(row.id, column.prop, newValue)
},
handleSubmit(formData) {
console.log('提交数据:', formData)
// 调用API提交数据
},
linkClick(key, row) {
console.log(key, row)
}
}
}
</script>
动态表格组件
<!-- DynamicTable.vue -->
<template>
<div class="dynamic-table">
<div style="display: flex">
<el-button
v-for="(item, index) in actions.multiple"
:key="index"
style="margin: 10px 10px 10px 0"
:type=" item.type || 'primary'"
size="small"
@click="item.method?item.method():handleTableOpt(item.key)"
>
{{ item.label }}
</el-button>
</div>
<el-table
:key="tableKey"
v-bind="$attrs"
:data="tableData"
v-on="$listeners"
@selection-change="handleSelectionChange"
>
<!-- 选择列 -->
<el-table-column
v-if="selection"
type="selection"
width="55"
align="center"
/>
<!-- 序号列 -->
<el-table-column
v-if="showIndex"
label="序号"
type="index"
width="80"
align="center"
>
<template slot-scope="scope">
{{ (pagination.page - 1) * pagination.size + scope.$index + 1 }}
</template>
</el-table-column>
<!-- 动态列 -->
<template v-for="(column,i) in processedColumns">
<dynamic-column
:key="column.prop"
:column="column"
:table-key="`${tableKey}-${i}`"
/>
</template>
<!-- 操作列 -->
<el-table-column
v-if="actions.single&&actions.single.length"
label="操作"
:width="actionsWidth"
align="center"
>
<template slot-scope="{ row }">
<div class="dynamic-action-buttons">
<template v-for="(action, index) in filteredActions(row)">
<el-tooltip
:key="index"
:content="action.tooltip || action.label"
placement="top"
>
<el-button
:type="action.type || 'primary'"
:icon="action.icon"
:disabled="action.disabled && action.disabled(row)"
size="mini"
@click="handleActionClick(action, row)"
>
{{ action.label }}
</el-button>
</el-tooltip>
</template>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-if="pagination"
class="dynamic-pagination"
:current-page="pagination.page"
:page-sizes="pageSizes"
:page-size="pagination.size"
:layout="paginationLayout"
:total="total"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</template>
<script>
import DynamicColumn from './DynamicColumn'
export default {
name: 'DynamicTable',
components: { DynamicColumn },
props: {
TableConfig: {
type: Object,
default: () => {
}
}
},
data() {
return {
tableKey: 'mainTable',
columns: {}, // 列配置
tableData: {}, // 表格数据
actions: {
single: [],
multiple: []
}, // 操作按钮配置
selection: true, // 是否显示选择列
showIndex: true, // 是否显示序号列
pagination: {
size: 0, // 每页条数
page: 0 // 当前页
}, // 是否显示分页
total: 0, // 总条目数
pageSizes: [10, 20, 50, 100],
paginationLayout: 'total, sizes, prev, pager, next, jumper',
...this.TableConfig
}
},
computed: {
filteredActions() {
return row => {
return this.actions.single?.filter(action =>
typeof action.visible === 'function'
? action.visible(row)
: action.visible !== false
)
}
},
actionsWidth() {
return this.actions.single?.length * 90 + 'px'
},
processedColumns() {
return this.columns.map(col => ({
align: 'center',
minWidth: 120,
...col,
children: col.children ? this.processColumns(col.children) : null
}))
}
},
created() {
this.$nextTick(() => {
// this.$refs.table.doLayout()
})
},
methods: {
processColumns(columns) {
return columns.map(col => ({
// 默认配置
align: 'center',
minWidth: 120,
headerAlign: 'center',
showOverflowTooltip: true,
// 合并用户自定义配置
...col,
// 递归处理子列
children: col.children ? this.processColumns(col.children) : undefined
}))
},
handleSelectionChange(selection) {
this.$emit('selection-change', selection)
},
handlePageChange(page) {
this.$emit('update:page', page)
},
handleSizeChange(size) {
this.$emit('update:size', size)
},
handleActionClick(action, row) {
if (action.handler) {
action.handler(row)
} else {
this.$emit('action-click', action.name, row)
}
},
handleTableOpt() {
}
}
}
</script>
<!-- DynamicColumn.vue (子组件) -->
<script>
export default {
name: 'DynamicColumn',
functional: true,
props: {
column: Object,
tableKey: String
},
render(h, context) {
const { props, parent } = context
const { column } = props
const { $scopedSlots } = parent
// 递归渲染子列
const renderChildren = () => {
if (!column.children) return null
return column.children.map(child =>
h('dynamic-column', {
props: {
column: child
}
})
)
}
// 自定义单元格内容
const renderCell = (row) => {
// console.log('🚀 row:', row)
// 插槽
if ($scopedSlots[column.prop]) {
return $scopedSlots[column.prop]({ row })
}
// 自定义渲染逻辑 兼容大小写
switch ((column.type || '').toLowerCase()) {
case 'image':
return renderImage(row[column.prop])
case 'link':
return renderLink(row)
case 'switch':
return renderSwitch(row[column.prop], row)
case 'tag':
return renderTag(row)
default:
return defaultRenderer(row)
}
}
// 具体渲染方法
const renderImage = (urls) => {
console.log('🚀 el-image:', urls)
const validUrls = Array.isArray(urls) ? urls : [urls || ''].filter(Boolean)
return h('el-image', {
props: {
src: validUrls[0],
previewSrcList: validUrls,
lazy: true
},
style: { width: '100%', height: '100%', borderRadius: '12px' }
})
}
const renderLink = (row) => {
return h('el-link', {
props: {
type: 'primary',
underline: false
},
on: {
click: () => parent.$emit('link-click', column.prop, row)
}
}, row[column.prop])
}
const renderSwitch = (value, row) => {
return h('el-switch', {
props: {
value: value,
disabled: column.disabled?.(row),
activeColor: '#13ce66',
inactiveColor: '#ff4949',
activeValue: 1,
inactiveValue: 0
},
on: {
change: val => {
parent.$set(row, column.prop, val)
column.handler?.(val, row)
}
}
})
}
const renderTag = (row) => {
return h('el-tag', {
props: {
type: 'success',
effect: 'dark'
}
}, row[column.prop])
}
const defaultRenderer = (row) => {
return row[column.prop]
}
return h('el-table-column', {
props: {
...column,
type: column.type === 'selection' ? 'selection' : null
},
// scopedSlots传递作用域参数
scopedSlots: {
default: props => {
// console.log('default scopedSlot', props)
return renderCell(props.row)
},
header: () => h('span', column.label)
}
}, renderChildren())
}
}
</script>
form表单组件 js 引用代码在其他文件夹里待更新
<template>
<Dialog
ref="dialog"
:visible.sync="dialogActions.visible"
:dialog-actions="dialogActions"
@close="close"
@confirm="handleSubmit"
>
<template slot="content">
<el-form ref="form" :model="formData" :rules="formRules" label-width="80px">
<el-row class="dialog-from-style" :gutter="10">
<el-col
v-for="(field, index) in fields"
:key="index"
:span="field.colSpan||12"
class="form-col-item-dynamic"
>
<el-form-item
:key="index"
:label="field.label"
:prop="field.prop"
:rules="field.rules"
>
<component
:is="getComponentConfig(field.type).component"
v-model="formData[field.prop]"
:change="field.method?field.method(formData):''"
v-bind="getComponentProps(field)"
v-on="getComponentEvents(field)"
>
<template v-for="(child, i) in getComponentChildren(field)">
<component
:is="child.component"
:key="i"
v-bind="child.props"
>
{{ child.content }}
</component>
</template>
</component>
<div v-if="field.tip" class="form-tip">{{ field.tip }}</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
</Dialog>
</template>
<script>
import Dialog from '@/components/DynamicTableForm/Dialog/index.vue'
import { componentMap } from '@/components/DynamicTableForm/Form/componentMap.js'
import {
fieldsValidator,
getComponentEvents,
getDefaultPlaceholder
} from '@/components/DynamicTableForm/Form/fieldsValidator.js'
export default {
name: 'DynamicForm',
components: {
Dialog
},
props: {
FormConfig: {
type: Object,
default: () => ({
fields: {
type: Array,
required: true,
validator: value => fieldsValidator(value)
}
})
},
initialData: { type: Object, default: () => ({}) }
},
data() {
return {
dialogActions: {
visible: false,
title: '示例'
},
fields: [],
...this.FormConfig,
formData: {},
formRules: {}
}
},
// 如果动态加载组件
watch: {
fields: {
handler(newVal) {
this.$nextTick(() => {
this.initForm()
})
},
immediate: true
}
},
mounted() {
},
methods: {
// 方法挂载
getComponentEvents,
// 初始化表单
initForm() {
this.formData = this.fields.reduce((acc, field) => ({
...acc,
[field.prop]: this.initialData[field.prop] ?? this.getDefaultValue(field.type)
}), {})
this.formRules = this.fields.reduce((acc, field) => (
field.rules ? { ...acc, [field.prop]: field.rules } : acc
), {})
},
handleSubmit() {
console.log(this.formData)
this.$refs.form.validate(valid => {
if (valid) {
this.$emit('submit', { ...this.formData })
this.close()
}
})
},
open() {
this.dialogActions.visible = true
},
close() {
this.$emit('update:visible', false)
this.$refs.form.resetFields()
},
getComponentConfig(type) {
return componentMap[type] || componentMap.default
},
// 生成子组件
getComponentChildren(field) {
const config = componentMap[field.type]
return config.children
? (typeof config.children === 'function'
? config.children(field.options)
: config.children)
: []
},
// 获取类型默认值
getDefaultValue(type) {
const typeDefaults = {
'checkbox-group': [],
'radio-group': '',
'input-number': 0,
slider: 0,
rate: 0,
upload: []
}
return typeDefaults[type] ?? ''
},
// 获取组件属性
getComponentProps(field) {
const config = this.getComponentConfig(field.type)
return {
// 优先级顺序:自定义配置 > 组件默认配置
...config.props, // 1. 组件默认配置
...field.props, // 2. 用户自定义配置
// 特殊属性
style: 'width: 100%', // 强制宽度
placeholder: field.placeholder || getDefaultPlaceholder(field.type)
}
}
}
}
</script>
搜索框表单
<template>
<el-form ref="form" :model="formData" label-width="80px">
<div class="from-dynamic">
<div
v-for="(field, index) in fields"
:key="index"
class="form-item-dynamic"
>
<el-form-item
:key="index"
:label="field.label"
:prop="field.prop"
>
<component
:is="getComponentConfig(field.type).component"
v-model="formData[field.prop]"
:change="field.method?field.method(formData):''"
v-bind="getComponentProps(field)"
v-on="getComponentEvents(field)"
>
<template v-for="(child, i) in getComponentChildren(field)">
<component
:is="child.component"
:key="i"
v-bind="child.props"
>
{{ child.content }}
</component>
</template>
</component>
</el-form-item>
</div>
<div class="search-button-dynamic">
<el-button
size="small"
type="primary"
:loading="loading"
@click="onSubmit"
>查询
</el-button>
<el-button
size="small"
@click="onReset"
>重置
</el-button>
</div>
</div>
</el-form>
</template>
<script>
import { componentMap } from '@/components/DynamicTableForm/Form/componentMap.js'
import {
fieldsValidator,
getComponentEvents,
getDefaultPlaceholder
} from '@/components/DynamicTableForm/Form/fieldsValidator.js'
export default {
name: 'DynamicForm',
props: {
dialogActions: {
type: Object,
default: () => ({})
},
fields: {
type: Array,
required: true,
validator: value => fieldsValidator(value)
},
initialData: { type: Object, default: () => ({}) }
},
data() {
return {
loading: false,
formData: {}
}
},
// 如果动态加载组件
watch: {
fields: {
handler(newVal) {
this.$nextTick(() => {
this.initForm()
})
},
immediate: true
}
},
methods: {
// 将函数挂载到 methods 中
getComponentEvents,
// 初始化表单
initForm() {
this.formData = this.fields.reduce((acc, field) => ({
...acc,
[field.prop]: this.initialData[field.prop] ?? this.getDefaultValue(field.type)
}), {})
console.table('🚀 初始化查询参数:', this.formData, this.fields)
},
getComponentConfig(type) {
return componentMap[type] || componentMap.default
},
getComponentChildren(field) {
const config = componentMap[field.type]
return config.children
? (typeof config.children === 'function'
? config.children(field.options)
: config.children)
: []
},
// 获取类型默认值
getDefaultValue(type) {
const typeDefaults = {
'checkbox-group': [],
'radio-group': '',
'input-number': 0,
slider: 0,
rate: 0,
upload: []
}
return typeDefaults[type] ?? ''
},
getComponentProps(field) {
const config = this.getComponentConfig(field.type)
return {
// 优先级顺序:自定义配置 > 组件默认配置
...config.props, // 1. 组件默认配置
...field.props, // 2. 用户自定义配置
// 特殊属性处理
style: 'width: 100%', // 强制宽度
placeholder: field.placeholder || getDefaultPlaceholder(field.type)
}
},
// 方法...
onReset() {
this.$refs.form.resetFields()
this.onSubmit()
},
onSubmit() {
const params = Object.assign({}, this.formData, { page: 1 })
this.$emit('submit', params)
console.log(params, '查询')
}
}
}
</script>
5726





