自定义表单控件和回显组件
在本框架中,自定义组件分为两种类型:表单控件(用于数据输入)和视图回显组件(用于数据显示)。所有组件都通过自动扫描机制全局注册,可以在任何地方使用。为了便于管理,我们将组件分为通用组件和业务组件。
组件分类与目录结构
所有自定义组件都通过统一的机制进行全局注册,可以全局使用。根据用途不同,我们将其分为两类:
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 工具函数将表单配置自动转换为详情配置。这样可以避免重复定义相似的配置,提高开发效率。
最佳实践
组件复用:
- 视图组件可以复用对应的表单组件,通过
view属性控制只读模式 - 避免重复开发相似功能
- 视图组件可以复用对应的表单组件,通过
属性透传:
- 使用
v-bind="$attrs"透传未声明的属性 - 提高组件的灵活性
- 使用
值处理:
- 使用
computed实现 v-model 双向绑定 - 注意处理 undefined 和 null 值
- 使用
复杂数据处理:
- 对于复杂的数据结构,可以使用表格等高级组件
- 注意处理数据的增删改查操作
性能优化:
- 对于需要频繁渲染的组件,使用
watch监听值变化 - 合理使用
computed缓存计算结果
- 对于需要频繁渲染的组件,使用
模块化管理:
- 通用组件放在
src/components/form|view/components目录下 - 业务专用组件放在对应业务模块的
src/views/**/components/form|view/ - 便于维护和查找
- 通用组件放在
通过以上方式,您可以轻松创建符合框架规范的自定义表单控件和视图回显组件,满足各种业务需求。
