自定义表单控件和回显组件

在本框架中,自定义组件分为两种类型:表单控件(用于数据输入)和视图回显组件(用于数据显示)。所有组件都通过自动扫描机制全局注册,可以在任何地方使用。为了便于管理,我们将组件分为通用组件和业务组件。

组件分类与目录结构

所有自定义组件都通过统一的机制进行全局注册,可以全局使用。根据用途不同,我们将其分为两类:

1. 通用组件

位于以下目录:

  • 表单组件:src/components/form/components/
  • 视图组件:src/components/view/components/

这些组件是通用功能组件,可以在多个业务模块中复用。

2. 业务组件

位于以下目录:

  • 表单组件:src/views/**/components/form/
  • 视图组件:src/views/**/components/view/

这些组件是特定业务模块的专用组件,放在对应业务目录下便于模块化管理。

组件注册机制

所有组件都通过以下方式自动扫描并全局注册:

// 表单组件注册 (src/components/form/component-map.ts)
const modules = import.meta.glob(
  ['./components/**/*.vue', '../../views/**/components/form/*.vue'],
  { eager: true },
);

// 视图组件注册 (src/components/view/component-map.ts)
const modules = import.meta.glob(
  ['./components/**/*.vue', '../../views/**/components/view/*.vue'],
  { eager: true },
);

组件命名规则:

  • 文件名会转换为 PascalCase 格式作为组件名
  • 例如:src/views/dev/schema/components/form/custom-auth-type-list.vue -> CustomAuthTypeList

通用组件示例

表单组件:IconPicker

文件路径:src/components/form/components/icon-picker.vue

<script setup lang="ts">
import { computed } from 'vue';

import { IconPicker } from '@vben/common-ui';

const props = defineProps<{
  value?: string;
}>();

const emit = defineEmits<{
  (e: 'update:value', value: string | undefined): void;
}>();

const value = computed({
  get() {
    return props.value;
  },
  set(val) {
    emit('update:value', val);
  },
});

defineExpose({
  getValue() {
    return value.value;
  },
});
</script>

<template>
  <IconPicker v-model:value="value" v-bind="$attrs" />
</template>

视图组件:IconPicker

文件路径:src/components/view/components/icon-picker.vue

<script setup lang="ts">
import { computed } from 'vue';
import Icon from '#/components/icon/icon.vue';

const props = defineProps<{
  value?: string;
}>();

const displayValue = computed(() => {
  return props.value;
});
</script>

<template>
  <Icon v-if="displayValue" :icon="displayValue" />
  <span v-else>{{ displayValue }}</span>
</template>

业务组件示例(以dev模块为例)

表单组件:CustomAuthTypeList

文件路径:src/views/dev/schema/components/form/custom-auth-type-list.vue

<script setup lang="ts">
import { computed, h, watch } from 'vue';

import { useVbenVxeGrid } from '@vben/plugins/vxe-table';

import { Button } from 'ant-design-vue';

import { TableAction } from '#/components/table-action';

defineOptions({
  inheritAttrs: false,
});
const props = defineProps({
  value: {
    type: Array,
    default: () => [],
  },
  view: {
    // 是否为查看模式
    type: Boolean,
    default: false,
  },
});
const emits = defineEmits(['update:value']);
const dataSource = computed({
  get() {
    return props.value || [];
  },
  set(value) {
    emits('update:value', value || []);
  },
});
const handleRemove = (params: any) => {
  // eslint-disable-next-line no-use-before-define
  gridApi.grid.remove(params.row);
};
const [BasicTable, gridApi] = useVbenVxeGrid({
  gridOptions: {
    size: 'mini',
    border: true,
    toolbarConfig: {
      slots: {
        buttons: 'toolbar-buttons',
      },
    },
    rowConfig: {
      useKey: true,
      drag: props.view !== true,
    },
    columnConfig: {
      useKey: true,
    },
    columns: [
      {
        field: 'label',
        title: '权限名',
        align: 'left',
        dragSort: props.view !== true,
        editRender: {
          enabled: props.view !== true,
          name: 'Input',
          props: {
            size: 'middle',
          },
        },
      },
      {
        field: 'value',
        title: '权限码(sys:role:xx)',
        align: 'left',
        editRender: {
          enabled: props.view !== true,
          name: 'Input',
          props: {
            size: 'middle',
          },
        },
      },
      {
        width: 100,
        title: '操作',
        align: 'center',
        visible: props.view !== true,
        slots: {
          default: (params: any) => {
            return h(
              Button,
              {
                type: 'link',
                onClick: () => {
                  handleRemove(params);
                },
              },
              {
                default() {
                  return '删除';
                },
              },
            );
          },
        },
      },
    ],
    pagerConfig: {
      enabled: false,
    },
    editConfig: {
      trigger: 'click',
      mode: 'row',
    },
    data: dataSource.value,
  },
  gridEvents: {},
});

const handleAdd = async () => {
  const record = {
    label: `权限码${dataSource.value.length + 1}`,
    value: '',
  };
  const { row: newRow } = await gridApi.grid.insertAt(record, -1);
  gridApi.grid.setEditRow(newRow);
};
const handleSave = () => {
  dataSource.value = gridApi.grid.getTableData().fullData;
};
watch(
  () => dataSource.value,
  () => {
    gridApi.setState({
      gridOptions: {
        data: dataSource.value,
      },
    });
  },
  {
    deep: true,
  },
);
</script>
<template>
  <BasicTable class="w-full">
    <template #toolbar-buttons>
      <TableAction
        :actions="[
          {
            label: '新增',
            size: 'small',
            icon: 'ant-design:plus-outlined',
            onClick: handleAdd,
            ifShow: view !== true,
          },
          {
            label: '保存',
            icon: 'ant-design:save-outlined',
            size: 'small',
            onClick: handleSave,
            ifShow: view !== true,
          },
        ]"
      />
    </template>
    <template #toolbar-tools></template>
  </BasicTable>
</template>

视图组件:CustomAuthTypeList

文件路径:src/views/dev/schema/components/view/custom-auth-type-list.vue

<script setup lang="ts">
import { ref, watch } from 'vue';

import CustomAuthTypeList from '../form/custom-auth-type-list.vue';

const props = defineProps({
  value: {
    type: Array,
    default: () => [],
  },
});
const mValue = ref<Array<any>>(props.value);
watch(
  () => props.value,
  (val: any) => {
    mValue.value = val;
  },
  {
    deep: true,
  },
);
</script>
<template>
  <CustomAuthTypeList v-model:value="mValue" :view="true" />
</template>

组件开发规范

1. 命名规范

  • 文件名使用 kebab-case 命名方式,如 custom-auth-type-list.vue
  • 组件会自动注册为 PascalCase 名称,如 CustomAuthTypeList
  • 尽量和业务相关,这样避免组件名称冲突

2. 表单组件规范

  • 使用 value 作为组件值的属性名
  • 通过 update:value 事件更新值
  • 使用 defineExpose 暴露 getValue 方法供表单获取值
  • 支持 $attrs 透传属性

示例:

<script setup lang="ts">
import { computed } from 'vue';

const props = defineProps<{
  value?: any;
}>();

const emit = defineEmits<{
  (e: 'update:value', value: any): void;
}>();

const value = computed({
  get() {
    return props.value;
  },
  set(val) {
    emit('update:value', val);
  },
});

defineExpose({
  getValue() {
    return value.value;
  },
});
</script>

<template>
  <input 
    v-model="value" 
    class="w-full"
    v-bind="$attrs"
  />
</template>

3. 视图组件规范

  • 使用 value 作为组件值的属性名
  • 通常不需要修改值,主要用于展示
  • 可以复用对应的表单组件,通过 view 属性切换为只读模式

示例:

<script setup lang="ts">
import { ref, watch } from 'vue';

// 引用对应的表单组件
import CustomComponent from '../form/custom-component.vue';

const props = defineProps({
  value: {
    type: [String, Number, Array, Object],
    default: undefined,
  },
});

const mValue = ref(props.value);

watch(
  () => props.value,
  (val) => {
    mValue.value = val;
  }
);
</script>

<template>
  <!-- 通过 view 属性切换为只读模式 -->
  <CustomComponent v-model:value="mValue" :view="true" />
</template>

组件使用方式

在表单中使用

// 表单参数定义
export const formSchemas: VbenFormProps = {
  schema: [
    {
      fieldName: 'customField',
      label: '自定义字段',
      component: 'CustomAuthTypeList', // 组件名(文件名转 PascalCase)
      componentProps: {
        // 传递给组件的属性
        placeholder: '请选择',
        allowClear: true,
      },
    },
  ],
};

在表格中使用

// 表格参数定义
export const gridSchemas: VxeGridProps = {
  columns: [
    {
      field: 'customField',
      title: '自定义字段',
      cellRender: {
        name: 'CustomAuthTypeList', // 组件名(文件名转 PascalCase)
        props: {
          // 传递给组件的属性
        },
      },
    },
  ],
};

在详情中使用

// 详情参数定义
export const detailSchemas: DescItem[] = [
  {
    field: 'customField',
    label: '自定义字段',
    component: 'CustomAuthTypeList', // 组件名(文件名转 PascalCase)
    componentProps: {
      // 传递给组件的属性
    },
  },
];

// 详情组件可以复用表单组件的元数据,通过工具函数转换
import { schemaToDetailForm } from '#/util/tool';

// 将表单配置转换为详情配置
const detailSchemas = schemaToDetailForm(formSchemas, formData);

详情组件可以复用表单组件的元数据,通过 schemaToDetailForm 工具函数将表单配置自动转换为详情配置。这样可以避免重复定义相似的配置,提高开发效率。

最佳实践

  1. 组件复用

    • 视图组件可以复用对应的表单组件,通过 view 属性控制只读模式
    • 避免重复开发相似功能
  2. 属性透传

    • 使用 v-bind="$attrs" 透传未声明的属性
    • 提高组件的灵活性
  3. 值处理

    • 使用 computed 实现 v-model 双向绑定
    • 注意处理 undefined 和 null 值
  4. 复杂数据处理

    • 对于复杂的数据结构,可以使用表格等高级组件
    • 注意处理数据的增删改查操作
  5. 性能优化

    • 对于需要频繁渲染的组件,使用 watch 监听值变化
    • 合理使用 computed 缓存计算结果
  6. 模块化管理

    • 通用组件放在 src/components/form|view/components 目录下
    • 业务专用组件放在对应业务模块的 src/views/**/components/form|view/
    • 便于维护和查找

通过以上方式,您可以轻松创建符合框架规范的自定义表单控件和视图回显组件,满足各种业务需求。