Arco-Design+Vue3的JSON配置动态表单

一、背景

页面中频繁用到的表单导致代码的冗余,网上大部分的表单封装都是关于Element和Antd,对于AcroDesign的则很少,遂将过程记录一下。

二、开发环境

Arco-Design + Vite + Vue3

三、成品

html 复制代码
<template>
  <a-row :gutter="gutter">
    <a-form
      ref="tableForm"
      class="table-form"
      :model="formModel"
      auto-label-width
      :rules="rules"
      :layout="layout"
      :label-align="labelAlign"
    >
      <a-col
        v-for="field in visibleFields"
        :key="field.name"
        :span="field.span ? field.span : 22"
        :data-index="field.index"
      >
        <a-form-item
          v-if="field.name"
          :field="field.name"
          :label="field.label"
          :tooltip="field.tooltip"
          :label-col-flex="`${field.labelWidth}px`"
        >
          <component
            :is="field.type"
            v-model="formModel[field.name]"
            allow-clear
            :multiple="field.multiple"
            :style="field.style"
            :allow-search="field.search"
            :placeholder="field.placeholder"
            :options="field.options"
            :type="field.elType"
            :field-names="field.fieldNames"
            :disabled="field.disabled"
            :checked-value="field.checkedValue"
            :unchecked-value="field.uncheckedValue"
            :show-time="field.showTime"
            :allow-create="field.allowCreate"
            :min="field.min"
            :max="field.max"
            :step="field.step"
            :precision="field.precision"
            :mode="field.mode"
            @change="change"
          />
        </a-form-item>
      </a-col>
      <slot name="custom"></slot>
    </a-form>
  </a-row>
</template>


<script lang="ts" setup>
  import { toRefs, ref, watch, computed, nextTick } from 'vue';
  import { FormInstance, ResponsiveValue } from '@arco-design/web-vue';
  import type { FieldProps } from '@/types/global';

  const props = withDefaults(
    defineProps<{ formJson: Array<any>; // JSON配置表单的结构 
    rules?: any; // 表单验证规则 
    formState: FieldProps; // 表单的状态管理 
    layout?: 'horizontal' | 'vertical' | 'inline' | undefined; // 表单布局方式 
    labelAlign?: 'left' | 'right' | undefined; // 标签对齐方式 
    gutter?: | number | ResponsiveValue | [number | ResponsiveValue, number | ResponsiveValue]; // 表单项之间的间隔 
    display?: boolean | void; // 动态显示隐藏表单项 }>(),
    {
      layout: 'horizontal',
      formJson: () => [],
      display: false,
    }
  );
  const emit = defineEmits(['change']);

  // ref绑定组件
  const tableForm = ref<FormInstance>();
  const formModel = ref({} as any);
  const { formJson, rules, formState, layout, labelAlign, gutter } =
    toRefs(props);

  defineExpose({
    formModel,
    tableForm,
  });

  /*
   ** 表单过滤,特殊业务场景需要,没有则可以删除
   ** 用于条件判断是否通过表单条件展示需要的内容
   */
  const visibleFields = computed(() => {
    if (!props.display) {
      return formJson?.value === undefined ? false : formJson.value;
    }
    return props?.display();
  }) as unknown as Array<FieldProps>;

  /*
  * 按 data-index 将组件进行排序
  **/
  nextTick(() => {
    const elements = document.querySelectorAll('.table-form .arco-col');
    const formBox = document.querySelector('.table-form');
    // 将 elements 转换为数组
    const elementsArray = Array.from(elements);
    // 按照 data-index 进行排序
    elementsArray.sort((a, b) => {
      const indexA = a.getAttribute('data-index');
      const indexB = b.getAttribute('data-index');
      return indexA - indexB;
    });
    elementsArray.forEach((element) => {
      // 如果没有data-index则不需要
      if (!element.getAttribute('data-index')) return;
      // 将元素插入到 DOM 中
      formBox?.appendChild(element);
    });
  });

  const change = (val: any) => {
    emit('change', val);
  };

  // 监听表单数据变化赋值
  watch(
    () => props.formState,
    (val) => {
      if (!val) return;
      formModel.value = val;
    },
    { deep: true, immediate: true }
  );
</script>

<script lang="ts">
  export default {
    name: 'TableForm',
  };
</script>

<style lang="less" scoped></style>
  • allow-clear 是否支持清空
  • multiple 选择框是否支持多选
  • style a-form-item的样式
  • allow-search 选择框是否支持搜索
  • placeholder 占位符
  • options选择框的选择项
  • 表单组件内的type样式
  • field-names options选项值配置 ... 其它自定义可以依次添加

四、使用示例

html 复制代码
<template>
  <a-modal v-model:visible="visible" @ok="handleOk" @cancel="handleCancel">
    <template #title>{{ title }}</template>
    <TableForm
      ref="formRef"
      :form-json="newFormJson"
      :form-state="state.formModel"
      :rules="rules"
      :display="filterFormJson"
    ></TableForm>
  </a-modal>
</template>

<script setup lang="ts">
  import { reactive, toRefs, ref, watch, computed } from 'vue';
  import { omit } from 'lodash';
  import { cpDetail } from '@/api/settings';
  import { jsonToFormData } from '@/utils';
  import { FormModelProps } from '../types';
  import { adminFormJson } from '../formJson';

  const props = defineProps({
    visible: {
      type: Boolean,
      default: false,
    },
    title: {
      type: String,
      default: '',
    },
    type: {
      type: String,
      default: '',
    },
    record: {
      type: Object,
      default: () => ({}),
    },
    serviceList: {
      type: Array,
      default: () => [],
    },
    logList: {
      type: Array,
      default: () => [],
    },
  });

  const rules = {
    real_name: [
      {
        required: true,
        message: `所属人员不能为空!`,
      },
    ],
    account: [
      {
        required: true,
        message: `账号不能为空!`,
      },
    ],
    pwd: [
      {
        required: true,
        message: `请输入密码!`,
      },
      {
        minLength: 6,
        message: `密码不能少于六位!`,
      },
    ],
    comfirmpwd: [
      {
        required: true,
        message: `请确认密码!`,
      },
      {
        minLength: 6,
        message: `密码不能少于六位!`,
      },
    ],
    pid: [
      {
        required: true,
        message: `上级账号不能为空!`,
      },
    ],
    status: [
      {
        required: true,
        message: `状态不能为空!`,
      },
    ],
  };
  
  const { visible, title, type, record, serviceList, logList } = toRefs(props);
  const state = reactive({
    formModel: {
      level: '1',
    } as FormModelProps,
  });
  const formRef = ref();
  // 生成一个新的json,补全选择框的options
  const newFormJson = computed(() => {
    return adminFormJson.map((item: any) => {
      if (item.name === 'service_ids') {
        return {
          ...item,
          options: serviceList.value,
        };
      }
      return { ...item };
    });
  });
  
  const filterFormJson = () => {
    return newFormJson.value.filter((item) => {
      if (item.name === 'pid') {
        return state.formModel.level === '2';
      }
      return true;
    });
  };

  const emit = defineEmits(['onOk', 'onCancel']);
  const handleCancel = async () => {
    emit('onCancel', false);
  };
  const handleOk = async () => {
  	// 表单校验
    const res = await formRef.value.tableForm?.validate();
    if (!res) {
      state.formModel.service_ids = state.formModel.service_ids.join();
      if (state.formModel.level === '1')
        state.formModel = omit(state.formModel, ['pid']);
      emit('onOk', false, state.formModel);
    }
  };

  const getCpDetail = async (id: string) => {
    const { result } = await cpDetail(jsonToFormData({ id }));
    state.formModel = result;
    state.formModel.service_ids = result.service_list.map((item) => item.service_id);
    state.formModel.pid = data.pid === '0' ? '' : data.pid;
  };

  watch(
    () => type,
    (val) => {
      if (val.value === 'edit') {
        getCpDetail(record.value.id);
         state.formModel = omit(state.formModel, [
           'last_time',
           'service_list',
           'p_name',
         ]);
      }
    },
    { deep: true, immediate: true }
  );
</script>

<style scoped lang="less">
  .arco-btn {
    width: 80px;
  }
  .arco-col {
    text-align: center;
  }

下面贴一下formJson.ts

ts 复制代码
export const adminFormJson = [
    {
      name: 'channelName',
      label: '频道名称',
      type: 'a-input',
      placeholder: '请输入板块名称',
      maxLength: 4,
    },
    {
      name: 'channelOrder',
      label: '频道顺序',
      type: 'a-input-number',
      placeholder: '请填写顺序',
      fieldNames: { value: 'dictValue', label: 'dictLabel', key: 'dictValue' },
      options: pagesOptions,
      min: 0,
      max: 10,
      precision: 0,
    },
    {
      name: 'isShow',
      label: '是否露出',
      type: 'a-select',
      placeholder: '请选择',
      fieldNames: { value: 'value', label: 'label', key: 'value' },
      options: [
        {
          value: '1',
          label: '是',
        },
        {
          value: '2',
          label: '否',
        },
      ],
    },
    {
      name: 'isDefault',
      label: '是否设为默认页面',
      type: 'a-select',
      placeholder: '请选择',
      fieldNames: { value: 'value', label: 'label', key: 'value' },
      options: [
        {
          value: '1',
          label: '是',
        },
        {
          value: '2',
          label: '否',
        },
      ],
    },
  ];
相关推荐
爱吃羊的老虎12 分钟前
【WEB开发.js】getElementById :通过元素id属性获取HTML元素
前端·javascript·html
妙哉73628 分钟前
零基础学安全--HTML
前端·安全·html
咔叽布吉35 分钟前
【前端学习笔记】AJAX、axios、fetch、跨域
前端·笔记·学习
GISer_Jing1 小时前
Vue3常见Composition API详解(适用Vue2学习进入Vue3学习)
前端·javascript·vue.js
yangqii1 小时前
工作中可以用到的前端小知识(不定时更新)
javascript
Dragon Wu1 小时前
TailwindCss 总结
前端·css·前端框架
bpmf_fff1 小时前
十、事件类型(鼠标事件、焦点.. 、键盘.. 、文本.. 、滚动..)、事件对象、事件流(事件捕获、事件冒泡、阻止冒泡和默认行为、事件委托)
前端·javascript
泰山小张只吃荷园1 小时前
期末Python复习-输入输出
java·前端·spring boot·python·spring cloud·docker·容器
悦涵仙子2 小时前
vueuse中的useTemplateRefsList
前端·javascript·vue.js
萧萧玉树2 小时前
分布式在线评测系统
前端·c++·后端·负载均衡