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: '否',
        },
      ],
    },
  ];
相关推荐
一颗花生米。2 小时前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
学习使我快乐012 小时前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio19952 小时前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
勿语&3 小时前
Element-UI Plus 暗黑主题切换及自定义主题色
开发语言·javascript·ui
黄尚圈圈3 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水4 小时前
简洁之道 - React Hook Form
前端
正小安6 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch7 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光7 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   7 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发