元数据驱动:打造动态灵活的Vue键值对表格组件

需求背景

在业务开发过程中,我们经常遇到需要以结构化方式展示表单数据的场景。传统表格组件适合展示行列数据,但对于"属性名-属性值"这种配对形式的数据展示并不直观。我们需要开发一个专门的表格组件,具体要求如下:

  • 以表格形式展示属性名和属性值,格式为"属性名:属性值"
  • 支持一行显示多对键值对
  • 允许灵活控制每个属性显示的位置
  • 支持单元格合并功能
  • 键和值需要有不同样式区分

设计思路

整体架构

采用元数据驱动设计,通过配置描述数据展示方式,将原始数据转换为表格可渲染的结构。

核心概念

  • formItems: 描述表单项的元数据数组,定义如何展示数据
  • formData: 原始表单数据对象,包含实际要展示的值
  • group: 控制属性在表格中的位置,实现灵活的布局

详细设计

1. 元数据结构设计

定义表单项的元数据结构,用于描述每个表单项的展示方式:

typescript 复制代码
interface IFormItem {
  prop: string;           // 对应表单数据的字段名
  label: string;          // 显示名称
  group?: number;         // 所在键值对位置(从1开始),默认为1
  format?: (data: any) => string;  // 数据格式化函数
  render?: (h: any, value: any, data: any) => any; // 自定义渲染函数
  rowspan?: number;       // 行合并数
  colspan?: number;       // 列合并数
}

2. 数据转换策略

将表单数据和元数据结合,生成表格组件可识别的结构:

typescript 复制代码
// 转换前
formData: { name: "测试", age: 25 }
formItems: [{ prop: "name", label: "姓名" }, { prop: "age", label: "年龄" }]

// 转换后
tableData: [
  { 
    "group-1-key": "姓名",
    "group-1-value": "测试",
    "group-2-key": "年龄", 
    "group-2-value": 25
  }
]

3. 列生成算法

通过分析formItems中的group属性,动态生成表格列配置:

typescript 复制代码
interface IColumn {
  prop: string;
  type: 'title' | 'content';
}
const getDisplayedColumns = (formItems): IColumn => {
  const array = formItems;
  const groupMap: Map<number, Array<IFormItem>> = array.reduce((prev, cur) => {
    const group = cur.group;
    if (prev.has(group)) {
      const list = prev.get(group);
      list.push(cur);
    } else {
      prev.set(group, [cur]);
    }
    return prev;
  }, new Map()); // 将formItems按照group来分组

  const temp: Array<number> = Array.from(groupMap.entries())
    .sort((a, b) => a[0] - b[0]) // 按照group从小到大排序
    .map(([key, value]) => key); // 返回所有group编号组成的数组:e.g. [1,2]
    
  const cols = Array.from(temp)
    .map((item: number) => {
      return [
        {
          prop: `group-${item}-key`,
          type: 'title',
        },
        {
          prop: `group-${item}-value`,
          type: 'content',
        },
      ];
    })
    .flat(); // 将每个group的子数组(含有2项)扁平化,形成一维数组
  return cols as Array<IColumn>;
};

4. 行数据生成算法

将表单数据按照元数据配置转换为表格行数据:

typescript 复制代码
interface ICell {
  formItem: IFormItem;
  data?: any;
}
 const getDisplayedData = (formItems, data) => {
  const array = formItems;
  const groupMap: Map<number, Array<IFormItem>> = array.reduce((prev, cur) => {
    const { group } = cur;
    if (prev.has(group)) {
      const list = prev.get(group);
      list.push({ ...cur, group });
    } else {
      prev.set(group, [{ ...cur, group }]);
    }
    return prev;
  }, new Map()); // 将formItems按照group来分组

  const temp: Array<Array<IFormItem>> = Array.from(groupMap.entries())
    .sort((a, b) => a[0] - b[0]) // 按照group从小到大排序
    .map(([key, value]) => value); // 返回所有group对应的子数组组成的二维数组

  const maxRows = Math.max(...temp.map((item) => item.length)); // 二维数组的行数,就是不同group下的最大子数组长度
  const rows: Array<ICell> = [];

  for (let j = 0; j < maxRows; j++) {
    const cols: Record<string, any> = {}; // 对于每行,创建一个空对象
    for (let i = 0; i < temp.length; i++) {
      if (temp[i][j]) {
        const group = temp[i][j]?.group;
        cols[`group-${group}-key`] = temp[i][j]?.label ?? '';
        cols[`group-${group}-value`] = getCellMetadata(temp[i][j], data); // 为了渲染表单值,把整个formItem对象传入表单值属性
      }
    }
    rows.push(cols);
  }
  return rows;
};

const getCellMetadata = (item: IFormItem, data: any): ICell => {
  return {
    formItem: item,
  };
};

组件实现

模板部分

vue 复制代码
<el-table :data="tableData" :show-header="false" :stripe="false" :border="true" :span-method="spanMethod">
    <el-table-column v-for="item of columns" :key="item.prop" :prop="item.prop"
      :align="item.type === 'title' ? 'right' : 'left'" :class-name="item.type === 'title' ? 'title' : ''">
      <template #default="{ row }">
        <template v-if="typeof row[item.prop]?.formItem?.render === 'function'">
          <component :is="row[item.prop]?.formItem?.render('', data)"></component>
        </template>
        <template v-else>
          <template v-if="typeof row[item.prop]?.formItem?.format === 'function'">
            {{ row[item.prop]?.formItem?.format(data, item.prop) }}
          </template>
          <template v-else-if="row[item.prop] && item.type === 'title'">{{ `${row[item.prop]}:` }}</template>
          <template v-else-if="row[item.prop]">{{ data[row[item.prop]?.formItem.prop] || '' }}</template>
        </template>
      </template>
   </el-table-column>
</el-table>

逻辑部分

vue 复制代码
<script setup>
import { computed } from 'vue';

const props = defineProps({
  formItems: {
    type: Array,
    default: () => [],
  },
  data: {
    type: Object,
    default: () => ({}),
  },
});

const columns = computed(() => {
  return getDisplayedColumns(props.formItems);
});

const tableData = computed(() => {
  return getDisplayedData(props.formItems, props.data);
});

const spanMethod = ({ row, column }) => {
  const formItem = row[column.rawColumnKey]?.formItem;
  return {
    rowspan: formItem?.rowspan ?? 1,
    colspan: formItem?.colspan ?? 1,
  };
};
</script>

使用示例

基本用法

vue 复制代码
<KeyValueTable :form-items="formItems" :data="data"></KeyValueTable>
typescript 复制代码
const formItems = [
    {
        prop: "name",
        label: "名称",
    },
    {
        prop: "sex",
        label: "性别",
        format: (data) => {
            return data.sex === 'man' ? '男' : '女'
        }

    }, {
        prop: "manQuestion",
        label: "男性问题",
        group: 2,
    },
    {
        prop: "womanQuestion",
        label: "女性问题",
        group: 2,
    },
    {
        prop: "description",
        label: "描述",
        colspan: 3,
    }
]

const data = {
    name: "test",
    sex: "man",
    manQuestion: "男性问题",
    womanQuestion: "女性问题",
    age: 18,
    date: "2025-07-29",
    phone: "13900000000",
    address: "广东省",
    idCard: "440100000000000000",
}

实现效果

该组件将渲染为如下表格结构:

总结

通过本文介绍的设计与实现,我们创建了一个灵活、可配置的键值对表格组件。该组件的核心优势在于:

  1. 声明式配置:通过formItems元数据驱动视图生成
  2. 灵活布局:支持多组键值对和自定义位置
  3. 扩展性强:支持格式化、自定义渲染和单元格合并
  4. 易于使用:简单的API设计,降低使用门槛

这种设计模式不仅适用于表格展示,其元数据驱动的思想也可以应用于其他需要动态生成视图的场景,为Vue开发者提供了一种可复用的解决方案。

相关推荐
学Linux的语莫2 小时前
langchain输出解析器
java·前端·langchain
文心快码BaiduComate2 小时前
您的前端开发智能工作流待升级,查收最新 Figma2Code!
前端·后端·程序员
老华带你飞3 小时前
水果购物网站|基于java+vue的水果购物网站系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·水果购物网站系统
狗头大军之江苏分军3 小时前
当AI小智遇上股票:一个不写死代码的智能股票分析工具诞生记
前端·人工智能·python
Restart-AHTCM3 小时前
前端框架之vue3/2(核心知识提要)
javascript·vue.js·前端框架
范特东南西北风3 小时前
Vue 项目全局主题色实现经验分享
前端
trsoliu3 小时前
通用视口自动置底工具(createAutoScroller)技术指南(含完整源码)
前端
WY3 小时前
利用scp2和ssh2完成前端项目自动化部署解决方案 1.0
前端
ZJ_3 小时前
功能按钮权限控制(使用自定义指令控制权限)
前端·javascript·vue.js