带你实现一个表单系统-逻辑表单篇

471 阅读9分钟

承接上回带你实现一个表单系统-具体实现篇,本篇讲述一下如何实现最后一个功能点-逻辑表单

首先要知道逻辑表单指是什么?

逻辑表单指的是那些能够根据用户的输入动态改变其行为和内容的表单。它可以提高信息收集的针对性、减少不必要的问题,并优化填写者的体验。在设计时,会通过设置条件规则来控制哪些表单项应该显示或隐藏,甚至决定后续的问题路线。

看一个具体的业务场景

image.png

使用上篇完成的表单系统配置一份上图的表单

image.png

那么我们将得到上篇提到的forms,就是表单系统维护的那一份表单数据。

[
  {
    "title": "性别",
    "configName": "选项",
    "type": "radio",
    "icon": "circle-check",
    "optionList": [
      {
        "optionId": "ylcx7bgncqcfjeho",
        "optionName": "男"
      },
      {
        "optionId": "e924fqxa8wi3oqzq",
        "optionName": "女"
      }
    ],
    "isAllowedConfig": true,
    "id": "field1918169548591",
    "logicList": [
      {
        "id": "5bepjbl9uskle3a0",
        "mode": "or",
        "isCompleted": true,
        "isEditing": false,
        "cond": [
          {
            "comp": "is",
            "ref": "field1918169548591",
            "val": "ylcx7bgncqcfjeho"
          }
        ],
        "fx": {
          "refs": [
            "field7683269592533"
          ],
          "type": "show"
        }
      },
      {
        "id": "woxinu55z6my9ml2",
        "mode": "or",
        "isCompleted": true,
        "isEditing": false,
        "cond": [
          {
            "comp": "is",
            "ref": "field1918169548591",
            "val": "e924fqxa8wi3oqzq"
          }
        ],
        "fx": {
          "refs": [
            "field2652269673342"
          ],
          "type": "show"
        }
      }
    ],
    "required": true
  },
  {
    "title": "兴趣班选择",
    "configName": "选项",
    "type": "checkBox",
    "icon": "more-check",
    "optionList": [
      {
        "optionId": "0jju29jg5mqu4el4",
        "optionName": "足球俱乐部"
      },
      {
        "optionId": "f7l7x4jez6y4cnyk",
        "optionName": "机器人编程"
      },
      {
        "optionId": "wwka59hzjcshlgka",
        "optionName": "小小科学家"
      },
      {
        "optionId": "b4u4lao14slqytep",
        "optionName": "国际象棋"
      }
    ],
    "isAllowedConfig": true,
    "id": "field7683269592533"
  },
  {
    "title": "兴趣班选择",
    "configName": "选项",
    "type": "checkBox",
    "icon": "more-check",
    "optionList": [
      {
        "optionId": "0jju29jg5mqu4el4",
        "optionName": "舞蹈艺术"
      },
      {
        "optionId": "f7l7x4jez6y4cnyk",
        "optionName": "绘画创作"
      },
      {
        "optionId": "id77cj2civ5bspzr",
        "optionName": "音乐剧团"
      },
      {
        "optionId": "vz9d1fqk2aocx2v8",
        "optionName": "故事会"
      }
    ],
    "isAllowedConfig": true,
    "id": "field2652269673342",
    "placeholder": ""
  }
]

H5页面渲染的效果:

2023-11-14 22.00.23.gif

看到这里你应该就对逻辑表单有一定的了解了,下面我们来看一下具体怎么实现。

先来看看管理端配置那一块如何实现

回顾一下上篇config.vue中的部分代码

image.png

<Drawer v-model:isShowDrawer="isShowDrawer" />
const form = computed(() => {
  const currentForm = selectFormItemFn.get();
  if (['selectInput', 'radio'].includes(currentForm.type)) {
    currentForm.logicList = currentForm.logicList || [];
  }
  return currentForm;
});

// 展示设置逻辑的抽屉
const onShowLogicDrawer = () => {
  isShowDrawer.value = true;
};

currentForm中间区域选中的表单项,获取到后判断它是否是单选type为radio、或者下拉单选type为selectInput,是的话给它添加一个数组logicList用来存放设置的逻辑。

logicList的数据结构

基本上参照雪梨表单的

{
    id: generateUUID(), // 逻辑id
    mode: "or", //or 或  and 且
    cond: [
      {
        comp: "", // 等于 is 不等于 not
        ref: form.value.id, // 条件选项id
        val: "", // 触发的值
      },
    ],// 条件数组
    fx: {
      refs: [], // 选择展示的问题id数组
      type: "show", // 显示某某题目或者终止问券 
    },// 浮现的数据
  }

drawer.vue的完整代码

<template>
  <el-drawer class="custom-drawer" title="逻辑显示设置" size="50%" v-model="isShowDrawer" :close-on-click-modal="false"
    :close-on-press-escape="false" :append-to-body="true" :before-close="handleBeforeClose">
    <div class="logic-box">
      <div class="logic-header flex between">
        <span class="title font16 fw-500">题目:{{ form.title }}</span>
      </div>
      <template v-if="form.logicList.length">
        <div class="logic-list pt-20">
          <div class="logic-item radius8 mb-10" v-for="(rule, index1) in form.logicList" :key="rule.id">
            <div class="name pt-16 pb-16 flex between pl-12 pr-12">
              <div class="fw-500 font18 name-c">规则{{ index1 + 1 }}{{ rule.isCompleted ? "" : "(未保存)" }}</div>
              <div v-if="!rule.isEditing">
                <svg-icon name="edit" size="1rem" class="mr-12 cursor" @click="onEditLogic(rule, index1)" />
                <svg-icon name="delete" size="1rem" class="cursor" @click="onDeleteLogic(index1)" />
              </div>
            </div>
            <div class="content pl-12 pr-12 pt-12 pb-12">
              <template v-if="rule.isEditing">
                <p class="title1 fw-500">当满足以下条件时:</p>
                <div class="cond-box mt-10 mb-10 f-ac pt-20 pb-20 pl-10 pr-10 radius8">
                  <!-- 第一条规则 -->
                  <div class="first-cond flex  f-ac">
                    <el-select v-model="fistrelation" disabled style="
                    width: 88px;">
                      <el-option :key="'on'" label="在" :value="'on'" />
                    </el-select>
                    <el-select v-model="rule.cond[0].ref" placeholder="请选择" disabled class="ml-12" style="width: 224px;">
                      <el-option v-for="item in forms" :key="item.id" :label="item.title" :value="item.id" />
                    </el-select>
                    <el-select v-model="rule.cond[0].comp" placeholder="请选择" clearable style="width: 84px;" class="ml-12">
                      <el-option v-for="item in equalList" :key="item.value" :label="item.label" :value="item.value" />
                    </el-select>
                    <el-select v-model="rule.cond[0].val" placeholder="请选择" clearable :disabled="!rule.cond[0].comp"
                      class="ml-12" style="width: 224px;">
                      <el-option v-for="item in form.optionList" :key="item.optionId" :label="item.optionName"
                        :value="item.optionId" />
                    </el-select>
                    <div class="del-btn">

                    </div>
                  </div>
                  <!-- 更多规则 剔除第一条-->
                  <div class="other-cond-list flex f-ac  mt-10" v-for="cond, index2 in rule.cond.slice(1)" :key="index2">
                    <el-select v-model="rule.mode" placeholder="请选择" clearable style="width: 88px;">
                      <el-option v-for="item in relationList" :key="item.value" :label="item.label" :value="item.value" />
                    </el-select>
                    <el-select v-model="cond.ref" placeholder="请选择" disabled class="ml-12" style="width: 224px;">
                      <el-option v-for="item in forms" :key="item.id" :label="item.title" :value="item.id" />
                    </el-select>
                    <el-select v-model="cond.comp" placeholder="请选择" clearable :disabled="!cond.ref" style="width: 84px;"
                      class="ml-12">
                      <el-option v-for="item in equalList" :key="item.value" :label="item.label" :value="item.value" />
                    </el-select>
                    <el-select v-model="cond.val" placeholder="请选择" clearable :disabled="!cond.comp" class="ml-12"
                      style="width: 224px;">
                      <el-option v-for="item in forms.find((v) => {
                        return v.id === cond.ref
                      }).optionList || []" :key="item.optionId" :label="item.optionName" :value="item.optionId" />
                    </el-select>
                    <div class="del-btn flex f-jc cursor" @click="onDeletCond(rule, index2 + 1)">
                      <svg-icon name="delete" size="1rem" />
                    </div>
                  </div>
                  <!-- 新增按钮 -->
                  <div class="add-cond cursor mt-12 flex f-ac" @click="onAddCond(rule)">
                    <svg-icon name="add" size="16" class="mr-5" />
                    <span>添加条件</span>
                  </div>
                </div>

                <div class="title2 mt-10 flex f-ac">
                  <p class="title1 fw-500 mr-16">则显示:</p>
                  <el-radio-group v-model="rule.fx.type">
                    <el-radio label="show">题目</el-radio>
                    <el-radio label="terminate">结束问卷</el-radio>
                  </el-radio-group>
                </div>
                <div class="all-question  pt-10 pb-10 pl-10 pr-10 radius8"
                  v-if="rule.fx.type === 'show'">

                  <el-checkbox-group v-model="rule.fx.refs">
                    <el-checkbox v-for="ques in originForms" :key="ques.id" :label="ques.id">{{ ques.title
                    }}</el-checkbox>
                  </el-checkbox-group>


                </div>
                <div class="btn-box mt-10 flex f-c-c">
                  <el-button @click="onCancel(rule, index1)">取消</el-button>
                  <el-button type="primary" @click="onSave(rule, index1)">保存</el-button>
                </div>
              </template>


              <template v-if="!rule.isEditing">
                <!-- 渲染cond -->
                <p class="fw-500 font16 name-c">当条件为:</p>
                <div class="cond-box mt-10 mb-10 f-ac radius8">
                  <div v-for="cond, index in rule.cond" :key="index" class="mb-10 flex f-ac">
                    <span v-if="index === 0" class="font14"></span>
                    <span v-if="index > 0" class="font14">
                      {{ rule.mode === 'or' ? '或' : '且' }}
                    </span>
                    <div class="radius16 op-bgc pl-12 pr-12 pt-5 pb-5 ml-12">{{ forms.find((v) => v.id === cond.ref).title
                    }}</div>
                    <span class="comp-span ml-12 mr-12 font14">{{ cond.comp === 'is' ?
                      '等于' : '不等于' }}
                    </span>
                    <div class="radius16 op-bgc  pl-12 pr-12 pt-5 pb-5">
                      {{ (forms.find((v) => v.id === cond.ref).optionList.find((v) => v.optionId === cond.val)).optionName
                      }}
                    </div>
                  </div>
                </div>
                <p class="title1 fw-500 font16 name-c">则{{ rule.fx.type === 'show' ? "显示" : "终止问卷" }}:</p>
                <div class="cond-box pr-12" v-if="rule.fx.refs.length">
                  <p v-for="opId in rule.fx.refs" :key="opId" class="mt-16 font14">
                    {{ originForms.find((v) => v.id === opId).title }}
                  </p>
                </div>
              </template>
            </div>
          </div>
        </div>
      </template>

      <template v-else>
        <el-empty description="暂无逻辑规则" />
      </template>

      <div class="add-box radius6 flex f-c-c mt-20 cursor" @click="onAddLogic()">
        <svg-icon name="add" size="16" class="mr-5" />
        <span>添加逻辑选项</span>
      </div>
    </div>
  </el-drawer>
</template>
<script setup>
import { msg } from "@/utils/tool";
import { generateUUID } from '@/utils/tool'
const props = defineProps({
  isShowDrawer: {
    type: Boolean,
  },
});
const emit = defineEmits(["update:isShowDrawer"]);

let selectFormItemFn = inject("selectedFormItemFn");
let formsFn = inject("formsFn");

const form = computed(() => {
  return selectFormItemFn.get();
});

const originForms = computed(() => {
  // console.log(formsFn.get(), 'formsFn.get()')
  return formsFn.get()
})

const forms = computed(() => {
  // 筛选出单选/下拉单的问题
  return formsFn.get().filter((cur) => {
    return cur.type === 'selectInput' || cur.type === 'radio'
  });
});

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

const fistrelation = 'on'
const relationList = ref([
  {
    value: "or",
    label: "或",
  },
  {
    value: "and",
    label: "且",
  }
]),
  equalList = ref([
    {
      value: "is",
      label: "等于",
    },
    {
      value: "not",
      label: "不等于",
    },
  ]);


// 新增条件
const onAddCond = (rule) => {
  rule.cond.push({
    comp: "",
    ref: form.value.id,
    val: "",
  });
};

// 删除条件
const onDeletCond = (rule, index) => {
  // 删除不影响第一条
  rule.cond.splice(index, 1);
};

// 新增规则
const onAddLogic = () => {
  form.value.logicList.push({
    id: generateUUID(),
    mode: "or", //or 或  and 且
    isCompleted: false, // 是否保存 
    isEditing: true, // 是否是编辑状态 
    cond: [
      {
        comp: "", // 等于 is 不等于 not
        ref: form.value.id, // 条件选项id
        val: "", // 触发的值
      },
    ],
    fx: {
      refs: [], // 选择展示的问题id数组
      type: "show", // 显示某某题目或者终止问券
    },
  },);
};
// 删除规则
const onDeleteLogic = (index) => {
  window
    .$confirm("删除将不可恢复!", "提示", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning"
    })
    .then(() => {
      form.value.logicList.splice(index, 1);
    })
    .catch(() => { });
};
// 编辑规则
const onEditLogic = (rule) => {
  rule.isEditing = true
}

// 取消
const onCancel = (rule, index) => {
  // 已保存情况下取消
  if (rule.isCompleted) {
    rule.isEditing = false
  }
  // 未保存情况下取消
  if (!rule.isCompleted) {
    form.value.logicList.splice(index, 1);
  }
}

// 保存
const onSave = (rule, index) => {
  if (onJudgeIsCompleted(rule)) {
    rule.isCompleted = true
    rule.isEditing = false
    console.log(form, 'formpppp')
  } else {
    msg.warn("请填写完整", 1000);
  }
}

// 判断是否填写完整
const onJudgeIsCompleted = (rule) => {
  // 判断cond中每一项是否有值还有fx中的refs是否有长度
  const isCompleted = !rule.cond.some(condItem => condItem.comp === "" || condItem.ref === "" || condItem.val === "");

  // 检查fx中的refs是否有长度
  if (rule.fx.type === 'show' && rule.fx.refs.length === 0) {
    return false;
  }

  // 选择终止问卷清空fx.refs
  if (rule.fx.type !== 'show') {
    rule.fx.refs = []
  }

  return isCompleted;
}

const handleBeforeClose = (done) => {
  // 检查logicList中是否有未保存的规则
  const isUnsaved = form.value.logicList.some(rule => !rule.isCompleted);
  if (isUnsaved) {
    // 提示用户保存
    window.$confirm("有未保存的规则,确定要关闭吗?", "提示", {
      confirmButtonText: "关闭",
      cancelButtonText: "取消",
      type: "warning"
    })
      .then(() => {
        // 删除未保存的规则
        const unsavedRules = form.value.logicList.filter(rule => !rule.isCompleted);
        unsavedRules.forEach(rule => {
          const index = form.value.logicList.indexOf(rule);
          form.value.logicList.splice(index, 1);
        });
        // 关闭抽屉
        done();
      })
      .catch(() => { });
  } else {
    // 关闭抽屉
    done();
  }
}
</script>

2023-11-14 22.23.08.gif

管理端就是配置那里给单选或者下拉单选,(其他表单项组件想要配置逻辑当然也是一样的,只是本人需求目前版本只做到了给这两种表单项组件配置逻辑),最终得到一个逻辑数组logicList。重点是看下面H5这边怎么利用logicList来实现逻辑表单。

H5如何利用logicList来实现需求

首先会有一个渲染组件,把从后台得到的表单数据渲染出来,效果看上面那个H5动图。贴一下部分代码如下:

<van-form ref="formRef" :show-error="true" class="pb-10">
    <div
      v-for="(item, index) in questionList"
      :key="item.id"
      class="field-box p-x-16"
    >
      <!-- 单行文本 -->
      <template v-if="item.type === 'text'">
        <van-field
          v-if="logic[item.id]"
          v-model="item.fieldValue"
          :class="[
            'text-field',
            'mb-12',
            item.isShowBorder ? 'focused-border' : '',
          ]"
          input-align="left"
          :required="item.required"
          :name="item.id"
          :label="`${getFormItemOrder(item.id)}.${item.title}`"
          :placeholder="item.placeholder"
          :rules="[{ required: item.required, message: item.placeholder }]"
          @focus="onFocus(item)"
          @blur="onBlur(item)"
        />
      </template>
      <!-- 多行文本 -->
      <template v-if="item.type === 'textArea'">
        <van-field
          v-if="logic[item.id]"
          v-model="item.fieldValue"
          :class="[
            'text-field',
            'mb-12',
            item.isShowBorder ? 'focused-border' : '',
          ]"
          input-align="left"
          type="textarea"
          rows="3"
          :required="item.required"
          :name="item.id"
          :label="`${getFormItemOrder(item.id)}.${item.title}`"
          :placeholder="item.placeholder"
          :rules="[{ required: item.required, message: item.placeholder }]"
          @focus="onFocus(item)"
          @blur="onBlur(item)"
        />
      </template>
      <!-- 数值 -->
      <template v-if="item.type === 'number'">
        <div class="flex">
          <van-field
            v-if="logic[item.id]"
            v-model="item.fieldValue"
            :class="[
              'text-field',
              'mb-12',
              item.isShowBorder ? 'focused-border' : '',
            ]"
            input-align="left"
            type="number"
            :required="item.required"
            :name="item.id"
            :label="`${getFormItemOrder(item.id)}.${item.title}`"
            :placeholder="item.placeholder"
            :rules="[{ required: item.required, message: item.placeholder }]"
            @focus="onFocus(item)"
            @blur="onBlur(item)"
          >
            <template #button>
              <div class="p-y-14 p-x-12 font14">
                {{ item.unit }}
              </div>
            </template>
          </van-field>
        </div>
      </template>
      <!-- 单选 -->
      <template v-if="item.type === 'radio'">
        <van-field
          v-if="logic[item.id]"
          :required="item.required"
          :name="item.id"
          :label="`${getFormItemOrder(item.id)}.${item.title}`"
          :placeholder="item.placeholder"
          error-message-align="left"
          :rules="[
            { required: item.required, message: item.placeholder || '请选择' },
          ]"
        >
          <template #input>
            <van-radio-group v-model="item.fieldValue" direction="vertical">
              <van-radio
                v-for="op in item.optionList"
                :key="op.optionId"
                :name="op.optionId"
                class="mt-10"
                checked-color="#1BAA59"
              >
                {{ op.optionName }}
              </van-radio>
            </van-radio-group>
          </template>
        </van-field>
      </template>
      <!-- 下拉单选 -->
      <template v-if="item.type === 'selectInput'">
        <van-field
          v-if="logic[item.id]"
          v-model="item.fieldValue"
          :class="['text-field', 'select-field']"
          input-align="left"
          :required="item.required"
          readonly
          :label="`${getFormItemOrder(item.id)}.${item.title}`"
          :name="item.id"
          :placeholder="item.placeholder"
          :rules="[{ required: item.required, message: item.placeholder }]"
          @click="onShowComPicker(index, item)"
        />
      </template>
      <!-- 多选 -->
      <template v-if="item.type === 'checkBox'">
        <van-field
          v-if="logic[item.id]"
          :required="item.required"
          :name="item.id"
          :label="`${getFormItemOrder(item.id)}.${item.title}`"
          :placeholder="item.placeholder"
          :rules="[
            { required: item.required, message: item.placeholder || '请选择' },
          ]"
        >
          <template #input>
            <van-checkbox-group v-model="item.fieldValue" direction="vertical">
              <van-checkbox
                v-for="op in item.optionList"
                :key="op.optionId"
                shape="square"
                :name="op.optionId"
                checked-color="#1BAA59"
                class="mt-10"
                >{{ op.optionName }}</van-checkbox
              >
            </van-checkbox-group>
          </template>
        </van-field>
      </template>
      <!-- 日期 -->
      <template v-if="item.type === 'date'">
        <van-field
          v-if="logic[item.id]"
          v-model="item.fieldValue"
          class="text-field date-field"
          input-align="left"
          :required="item.required"
          readonly
          :name="item.id"
          :label="`${getFormItemOrder(item.id)}.${item.title}`"
          :placeholder="item.placeholder"
          :rules="[{ required: item.required, message: `请选择${item.title}` }]"
          @click="onShowCalendarPicker(index)"
        />
      </template>
      <!-- 日期范围 -->
      <template v-if="item.type === 'dateRange'">
        <van-field
          v-if="logic[item.id]"
          v-model="item.fieldValue"
          class="text-field date-field"
          input-align="left"
          :required="item.required"
          readonly
          :name="item.id"
          :label="`${getFormItemOrder(item.id)}.${item.title}`"
          :placeholder="`${item.beginPlaceholder || ''}-${
            item.endPlaceholder || ''
          }`"
          :rules="[{ required: item.required, message: `请选择${item.title}` }]"
          @click="onShowCalendarRangePicker(index, item)"
        />
      </template>
      <!-- 图片 -->
      <template v-if="item.type === 'imageUpload'">
        <div v-if="logic[item.id]" :id="item.id" @click="onUplaod(index)">
          <div
            class="van-cell__title van-field__label mb-16 font14"
            :class="[item.required ? 'van-field__label--required' : '']"
          >
            <label>{{ getFormItemOrder(item.id) + "." + item.title }}</label>
          </div>
          <van-uploader
            v-model="item.fieldValue"
            :after-read="onAfterRead"
            :max-count="1"
          >
            <div class="flex f-ac">
              <svg-icon
                class="svgicon mr-6"
                name="enroll-camera"
                size="1.5em"
              ></svg-icon>
              <span class="font12">{{ item.placeholder }}</span>
            </div>
          </van-uploader>
        </div>
      </template>
    </div>
  </van-form>

questionList就是那个forms,管理端保存后从后台拿到的。按类型来渲染每一种组件,这里包着这么多东西,主要是因为要改样式,这些不重要,重点看逻辑。
每个表单项中都有一个v-if="logic[item.id]" 没错,就是定一个这么一个logic对象来控制所有表单项的显示隐藏来达到效果。

logic:{
 表单项id:布尔值,
 表单项id:布尔值,
 表单项id:布尔值,
 表单项id:布尔值,
 ....
}

定义一个useGetLogic来获得questionList中所有表单项的logicList

export function useGetLogicHook(questionList) {
  const tmpList = [];
  questionList.forEach((q) => {
    if (q.logicList && q.logicList.length) {
      tmpList.push(...q.logicList);
    }
  });

  // console.log(tmpList, "logicList。logicList");
  return tmpList;
}

定义一个useFormLogic来处理logicList并且控制表单显示隐藏。

// 表单逻辑处理的一个hook

const logic = ref({}); // 维护所有题目状态的对象

// 判断条件是否符合,返回一个布尔值
const checkCondition = (cond, questionList) => {
  let isEligible = false; // 默认不符合条件
  // 四步 1、获取设置的条件 2、获取用户输入的选项值 3、进行比较 4、比较后更新logic中题目的状态
  // 遍历条件数组
  cond.forEach((cond) => {
    // ref :条件引用的字段id
    // comp:条件比较符号
    // val:条件比较值
    const { ref, comp, val } = cond;

    // 获取用户输入的真实值,也就是id为ref的问题的值
    const tmpItem = questionList.find((v) => {
      return v.id === ref;
    });
    let fieldValue = "";
    // 单选的值直接等于fieldValue
    if (tmpItem.type === "radio") {
      fieldValue = tmpItem.fieldValue;
    }
    // 下拉选择的值
    if (tmpItem.type === "selectInput") {
      fieldValue = tmpItem.selOpId;
    }

    // console.log(comp, "comp");
    // console.log(fieldValue, "fieldValue");
    // console.log(val, "val");

    // 用户的值存在的时候才进行比对
    if (fieldValue) {
      // eslint-disable-next-line default-case
      switch (comp) {
        // 等一的情况
        case "is":
          if (fieldValue === val) {
            isEligible = true;
          }
          break;
        // 不等于的情况
        case "not":
          if (fieldValue !== val) {
            isEligible = true;
          }
          break;
      }
    }
  });

  return isEligible; // 默认条件不满足
};

// 初始化logic对象
const initLogicObj = (questionList) => {
  questionList.forEach((v) => {
    logic.value[v.id] = true;
  });
};

const processLogic = (logicList, questionList) => {
  // 终止标志初始化为false
  let terminated = false;

  // 遍历逻辑列表
  logicList.forEach((rule) => {
    // 如果问卷已被终止,则跳过后续逻辑处理
    if (terminated) return;

    // 检查当前规则条件是否满足
    const isConditionMet = checkCondition(rule.cond, questionList);

    // 如果没有指定要显示的题目(即可能是终止问卷的逻辑)
    if (!rule.fx.refs.length) {
      // 如果条件满足,则终止问卷
      if (isConditionMet) {
        // 找到触发终止逻辑的题目在列表中的位置
        const currentIndex = questionList.findIndex(
          (item) => item.id === rule.cond[0].ref
        );

        // 如果找到了题目位置,将其之后所有题目设置为不可见
        if (currentIndex !== -1) {
          questionList.slice(currentIndex + 1).forEach((item) => {
            logic.value[item.id] = false;
          });
        }

        // 设置终止标志为true
        terminated = true;
      }
    } else if (isConditionMet) {
      // 如果指定了要根据条件显示的题目
      rule.fx.refs.forEach((ref) => {
        // 根据条件设置题目的显示状态
        logic.value[ref] = true;
      });
    } else {
      // 如果条件不满足,隐藏相关题目
      rule.fx.refs.forEach((ref) => {
        logic.value[ref] = false;
      });
    }
  });
};

export function useFormLogicHook(logicList, questionList) {
  // 初始化
  initLogicObj(questionList);
  processLogic(logicList, questionList);
  // 监听问题列表中每个问题的 fieldValue 变化
  watch(
    () => questionList.map((item) => item.fieldValue),
    () => {
      initLogicObj(questionList);
      processLogic(logicList, questionList);
    }
  );
  // 返回包含题目显示逻辑状态的对象
  return { logic };
}

hooks里面的的思路也不复杂,定义一个logic的响应式对象,然后initLogicObj,循环questionList将所有题目的id作为key,都设置为true,完成初始化。然后要有一个checkCondition函数来判断用户选择的值和我们当前这个题目中logicList设置的条件val是否相等,这个函数也是返回一个布尔值,返回true就是用户选择了符合触发逻辑的选项,返回flase则是选择了不符合的选项,接着就是给根据fx.refs这个数组中存储的题目id,重新设置logic对象中的对应key(题目id)的布尔值从而达到题目的显示或则隐形效果,当然还会存在终止问卷ule.fx.refs.length的情况,那么就要有个标记来terminated记录一下。

在渲染表单的组件中使用hooks

const { logic } = useFormLogicHook(props.logicList, props.questionList);

其实思路很简单。
管理后台要配置得到一个logicList,这个数组中包含了是哪个题目设置的(cond.ref),设置了什么逻辑(cond.comp比较符、cond.val触发条件的选项id),逻辑成立要处理哪些题目(fx.refs关联题目的id数组,fx.type显示隐藏或则是终止问卷,终止问卷其实也是特殊的一种将当前题目后面的题目隐藏)。
H5这边除了渲染问题列表以外,还要把后台配置的问题列表中的logicList都找出来合并为一个数组,然后写一个hooks来处理这些逻辑,就是上面所说的维护这个响应式的logic对象。

好啦,表单系列到此完结啦。如果后面有时间可能会做个开源项目,把源码分享出来。若有错误或者建议,欢迎大佬们指出。