VUE动态表格加动态表单以及动态搜索框

动态表格加动态表单以及动态搜索框 有疑问可私信我。在这里插入图片描述

在这里插入到的图片描述

引用示例

<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>

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值