借助el-steps和el-form实现超长规则配置的功能

目录

一、应用场景

 二、开发流程

三、详细开发流程

四、总结


一、应用场景

最近开发了一个规则类的配置功能,这个功能之前就写过,最近完善了一下,所以将原先的规则变得更多元化,结构也更多了一层,添加新功能的时候试着用了ts来写,就有了一些坑,以此记录一下。

项目场景:开发规则配置页面,主要用于设置和管理某种规则配置,包括基本配置、分组配置和规则配置。

页面的功能可以分为以下几个部分:启用/禁用配置、步骤条、基本配置、分组配置、规则配置、保存和提交。

截图如下:


 二、开发流程

确定页面需要实现的功能,包括规则的启用/禁用、步骤条导航、基本配置、分组配置、规则配置等。由于体现流程,所以添加步骤条,但是业务里是根据提交保存等来实现的当前进度记录,所以步骤条的状态是根据数据的返回值来确定的。

使用的组件:el-steps、el-form

实际的开发功能主要分为以下几大块,在这里我描述一下详细的业务功能:

  • 启用/禁用配置:

    • 使用el-radio-group组件,用户可以选择启用或禁用当前规则配置。如果启用,后续的配置步骤会显示出来。提交后,再禁用则不能再更改回来。
  • 步骤条:

    • 使用el-steps组件展示步骤条,帮助用户导航到不同的配置步骤,包括“基本配置”、“分组配置”和“规则配置”。根据数据,判断在第几步,如果在第二步的时候保存,那么刷新页面,还是在第二步;如果未保存,刷新页面到第一步;在第二步,点击步骤条的第一个,类似于点击“上一步”。
  • 基本配置:

    • 用户可以设置分组方法(例如二层分组、三层分组),以及分组总数。分组总数是后续分组数的上限,在后续的录入里会校验输入数量是否超过这个数,并且返回第一步时候可以切换方法,所以第二步要做一些数据兼容。
  • 分组配置:

    • 使用SecondStep组件,允许用户添加和管理分组信息。用户可以添加、更新和删除分组,并设置相关属性,如分组ID、分组名称和数量。

    • 这里有两种情况一种是二层分组,就是每一个卡片为一个分组,添加卡片,点击保存后,保存所有卡片数据;删除卡片,判断卡片是否有id,如果卡片未提交或者保存过,那么就没有id,直接删除,如果有id,就请求后台进行删除。

    • 另一种是三层分组,就是每一个卡片为一个分类,卡片里有一个分类名称和分类选项,当前分组的实际数据在下面根据卡片生成的表格里。例如有3个卡片,卡片1有2个选项,卡片2有3个选项,卡片3有4个选项,那么表格就有:2*3*4=24行,同理,也会产生24个不同分组。在这个情况下,添加卡片,就意味着添加一个分类,分类会默认有一个分类名称和分类选项,如果添加完毕,就点击按钮,生成表格,在表格里,进行输入数量,当然这里都会校验数量的总数是否超过第一步里设置的数量。点击保存后,保存所有分组数据;删除卡片,这里就不涉及分类的删除了,因为每次生成数据都是不同的分组,所以删除就是单纯的分类的删除。

  • 规则配置:

    • 用户可以选择规则类型(项目统一规则或分组统一规则)。根据选择的规则类型,显示不同的配置项:
      • 项目统一规则: 用户可以设置规则的规则1、规则2和规则3。
      • 分组统一规则: 用户可以为每个分组设置规则的规则1、规则2和规则3。
  • 保存和提交:

    • 页面提供了保存和提交功能。用户可以在每一步保存当前配置或继续到下一步。在第三步,用户可以选择提交配置,这会将配置锁定并且不可再修改。

难点:表单的校验,添加分组或者分类后,生成表格的时候,表单的校验,以及表格的数据校验等等,还有三个步骤的流程切换,数据的处理和对接接口。


三、详细开发流程

这里主要难点是步骤二,所以步骤二,单独拆分一个组件。

首先定义一个RuleConfiguration.vue,这里写第一步到第三步的所有代码:

<template>
  <div class="dashboard-container">
    <el-card class="card-style">
      <div class="mt-1">
        <h2 class="fwb-mb-1">规则配置</h2>
      </div>
      <el-form :model="configurationForm" label-width="120" :disabled="isSubmitted">
        <el-form-item label="是否启用" prop="is_enable">
          <el-radio-group v-model="configurationForm.is_enable" @change="saveStatus">
            <el-radio :value="0">启用</el-radio>
            <el-radio :value="1">禁用</el-radio>
          </el-radio-group>
        </el-form-item>
      </el-form>
      <template v-if="!configurationForm.is_enable">
        <el-steps class="steps-style" :active="active" align-center finish-status="success">
          <el-step title="基本配置" @click="previous" />
          <el-step title="分组配置" @click="previous" />
          <el-step title="规则配置" />
        </el-steps>
        <el-row class="rowCC mt flex1" style="align-content: flex-start">
          <el-form
            ref="ruleFormRef"
            :rules="rules"
            :model="configurationForm"
            label-width="100"
            :disabled="isSubmitted"
            style="max-height: calc(100vh - 315px); width: 50%; overflow-y: auto"
            size="small"
            label-position="right"
          >
            <template v-if="active === 0 || isSubmitted">
              <p class="styled-text mt-1">基本配置</p>
              <el-form-item label="分组方法" prop="ruleMethod">
                <el-select
                  v-model="configurationForm.ruleMethod"
                  placeholder="请选择分组方法"
                  style="width: 100%"
                  clearable
                >
                  <el-option
                    v-for="item in ruleMethodList"
                    :key="item.value"
                    :label="item.label"
                    :value="item.value"
                  />
                </el-select>
              </el-form-item>

              <el-form-item label="分组总数" prop="rule_quantity">
                <el-input
                  v-model="configurationForm.rule_quantity"
                  type="number"
                  placeholder="分组总数,后续分组数相加之和不得超过此数"
                />
              </el-form-item>
            </template>
            <template v-if="active === 1 || isSubmitted">
              <SecondStep
                :form="configurationForm"
                :rule-form-ref="ruleFormRef"
                :is-submitted="isSubmitted"
                @remove-group="removeGroup"
                @update-stratified-groups="updateStratifiedGroups"
                @update-group-data="updateGroupData"
                @update-button-disabled="updateButtonDisabled"
              />
            </template>
            <template v-if="active === 2 || isSubmitted">
              <p class="styled-text-noRequired mt-1">规则配置</p>
              <el-form-item label="规则" prop="rule_rule_type">
                <el-radio-group v-model="configurationForm.rule_rule_type">
                  <el-radio value="project">项目统一规则</el-radio>
                  <el-radio value="group">分组统一规则</el-radio>
                </el-radio-group>
              </el-form-item>
              <div class="columnCS" style="overflow-y: auto">
                <!-- 项目统一规则 -->
                <div
                  v-if="configurationForm.rule_rule_type == 'project'"
                  style="max-height: calc(100vh - 400px); width: 100%"
                >
                  <el-card class="mb-2" shadow="hover">
                    <div v-if="configurationForm.rule_rule_type == 'project'" class="rule-info">
                      {{
                        (configurationForm.regular_prefix || '') +
                        (configurationForm.regular_serial_number || '') +
                        (configurationForm.regular_suffix || '')
                      }}
                    </div>
                    <el-form-item
                      label="规则1"
                      prop="regular_prefix"
                      :rules="[
                        { required: true, message: '请填写', trigger: 'blur' },
                        {
                          validator: (rule, value, callback) => {
                            if (value === configurationForm.regular_suffix) {
                              callback(new Error('规则1和规则3不能相同'))
                            } else {
                              callback()
                            }
                          },
                          trigger: 'blur'
                        }
                      ]"
                    >
                      <el-input v-model="configurationForm.regular_prefix" placeholder="请填写" clearable />
                    </el-form-item>
                    <el-form-item
                      label="规则2"
                      prop="regular_serial_number"
                      :rules="[{ required: true, message: '请填写', trigger: 'blur' }]"
                    >
                      <el-input
                        v-model="configurationForm.regular_serial_number"
                        placeholder="如:001,002等数字,如填001则从001开始编号"
                        clearable
                      />
                    </el-form-item>
                    <el-form-item
                      label="规则3"
                      prop="regular_suffix"
                      :rules="[
                        { required: true, message: '请填写', trigger: 'blur' },
                        {
                          validator: (rule, value, callback) => {
                            if (value === configurationForm.regular_prefix) {
                              callback(new Error('规则1和规则3不能相同'))
                            } else {
                              callback()
                            }
                          },
                          trigger: 'blur'
                        }
                      ]"
                    >
                      <el-input v-model="configurationForm.regular_suffix" placeholder="不能与规则1相同" clearable />
                    </el-form-item>
                  </el-card>
                </div>
                <!-- 分组统一规则 -->
                <div
                  v-else-if="configurationForm.rule_rule_type == 'group'"
                  style="max-height: calc(100vh - 400px); width: 100%"
                >
                  <el-card
                    v-for="(item, index) in configurationForm.ruleNumberRules"
                    :key="index"
                    class="mb-2"
                    shadow="hover"
                  >
                    <div v-if="configurationForm.ruleMethod == 'block_ruleization'" class="rule-info">
                      <div>分组id:{{ item.group_coding }}</div>
                      <div>分组名称:{{ item.group_name }}</div>
                      <div>受试者数量:{{ item.patient_num }}</div>
                    </div>
                    <div v-else class="rule-info">
                      <div>分层信息:{{ item.group_coding }}</div>
                      <div>
                        分组选项:
                        <el-tag
                          v-for="(tag, index) in JSON.parse(item.group_name)"
                          :key="index"
                          class="mr"
                          type="primary"
                        >
                          {{ tag }}
                        </el-tag>
                      </div>
                      <div>受试者数量:{{ item.patient_num }}</div>
                    </div>
                    <el-form-item
                      label="规则1"
                      :prop="`ruleNumberRules.${index}.regular_prefix`"
                      :rules="[
                        { required: true, message: '请填写', trigger: 'blur' },
                        {
                          validator: (rule, value, callback) => {
                            if (value === item.regular_suffix) {
                              callback(new Error('规则1和规则3不能相同'))
                            } else {
                              callback()
                            }
                          },
                          trigger: 'blur'
                        }
                      ]"
                    >
                      <el-input v-model="item.regular_prefix" placeholder="请填写" clearable />
                    </el-form-item>

                    <el-form-item
                      label="规则2"
                      :prop="`ruleNumberRules.${index}.regular_serial_number`"
                      :rules="[{ required: true, message: '请填写', trigger: 'blur' }]"
                    >
                      <el-input
                        v-model="item.regular_serial_number"
                        placeholder="如:001,002等数字,如填001则从001开始编号"
                        clearable
                      />
                    </el-form-item>
                    <el-form-item
                      label="规则3"
                      :prop="`ruleNumberRules.${index}.regular_suffix`"
                      :rules="[
                        { required: true, message: '请填写', trigger: 'blur' },
                        {
                          validator: (rule, value, callback) => {
                            if (value === item.regular_prefix) {
                              callback(new Error('规则1和规则3不能相同'))
                            } else {
                              callback()
                            }
                          },
                          trigger: 'blur'
                        }
                      ]"
                    >
                      <el-input v-model="item.regular_suffix" placeholder="不能与规则1相同" clearable />
                    </el-form-item>
                  </el-card>
                </div>
              </div>
            </template>
          </el-form>
        </el-row>
        <el-row v-if="!isSubmitted" class="rowBC mt-2">
          <div style="text-align: center; flex: 1">
            <el-button v-if="active != 0" type="default" plain @click="previous">上一步</el-button>
            <el-button
              type="primary"
              plain
              :loading="saveBtnLoading"
              :disabled="btnDisabled"
              @click="submitForm(ruleFormRef, true)"
            >
              保存
            </el-button>
            <el-button
              type="primary"
              :loading="saveBtnLoading"
              :disabled="btnDisabled"
              @click="submitForm(ruleFormRef, false)"
            >
              {{ active == 2 ? '提交' : '下一步' }}
            </el-button>
          </div>
        </el-row>
      </template>
    </el-card>
  </div>
</template>

<script setup>
import { ref, reactive, computed, onMounted, watch, inject } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import RuleApi from '@/api/rule.js'
import SecondStep from './components/secondStep.vue'
const permissionStore = inject('permissionStore')
let id= permissionStore.projectInfo.id|| ''
onMounted(() => {
  getInfo()
})
const init = ref(true)

let ruleFormRef = ref(null)
const saveBtnLoading = ref(false)
let configurationForm = reactive({
  is_enable: 1,
  ruleMethod: '',
  blind_method: '',
  rule_quantity: null,
  groupsConfiguration: [],
  stratifiedGroups: [],
  rule_rule_type: 'project',
  regular_prefix: '',
  regular_suffix: '',
  regular_serial_number: '',
  ruleNumberRules: [
    {
      regular_prefix: '',
      regular_suffix: '',
      regular_serial_number: ''
    }
  ]
})

const isSubmitted = ref(false) //是否已提交 is_lock为0代表已经提交

const validateRuleQuantity = (rule, value, callback) => {
  if (value >= 0) {
    callback()
  } else {
    callback(new Error('数量必须大于等于0'))
  }
}

const active = ref(0)
const rule_group_id = ref(0)
const rules = ref({
  ruleMethod: [{ required: true, message: '请选择方法', trigger: 'change' }],
  blind_method: [{ required: true, message: '请选择盲法', trigger: 'change' }],
  rule_quantity: [
    { required: true, message: '请填写', trigger: 'change' },
    { validator: validateRuleQuantity, trigger: 'change' }
  ]
})
const ruleMethodList = ref([
  {
    label: '二层分组',
    value: 'block_ruleization'
  },
  {
    label: '三层分组',
    value: 'third_layer_ruleization'
  }
])

const blindingList = ref([
  {
    label: '开放',
    value: 'open_public'
  }
])

const isNew = ref(false)

const getInfo = async () => {
  let { data } = await RuleApi.getConfigureInfo({ disease_id: id})
  if (data.code == 200) {
    // 若res.data里没有属性,则为禁用状态
    if (Object.keys(data.data).length == 0) {
      //未新建过
      isNew.value = true
      configurationForm.is_enable = 1
      return
    }
    isNew.value = false
    isSubmitted.value = data.data.is_lock ? false : true
    rule_group_id.value = data.data.id
    Object.assign(configurationForm, data.data)
    configurationForm.ruleMethod = data.data.stochastic_method
    configurationForm.groupsConfiguration = data.data.group_details
    configurationForm.stratifiedGroups = data.data.third_layer_dict
    configurationForm.ruleNumberRules = data.data.group_details
    configurationForm.rule_rule_type = data.data.rule_rule_type
    if (init.value) {
      initActive(data)
    }
    init.value = false
  } else {
    ElMessage.error(data.message || '获取信息失败!')
  }
}

// init_第一次进入初始化步骤条状态
const initActive = (data) => {
  if (isSubmitted.value) {
    active.value = 3
  } else if (data.data.regular_prefix) {
    active.value = 2
  } else if (data.data.group_details.length > 0) {
    active.value = 1
  }
}

const saveStatus = async (value) => {
  //修改是否启用的状态,保存状态
  let dataInfo = { disease_id: disease_id, is_enable: value }
  if (isNew.value) {
    //新建配置
    isNew.value = false
  } else {
    //编辑
    dataInfo.rule_group_id = rule_group_id.value
  }
  let { data } = await RuleApi.saveRuleMethod(dataInfo)
  if (data.code == 200) {
    rule_group_id.value = data.data.rule_group_id
  } else {
    ElMessage.error(data.message || '获取信息失败!')
  }
}

const previous = () => {
  if (--active.value < 0) active.value = 0
  btnDisabled.value = false
}

const next = () => {
  if (active.value++ > 2) {
    active.value = 0
    return
  }
  if (
    configurationForm.groupsConfiguration.length == 0 &&
    active.value == 1 &&
    configurationForm.ruleMethod == 'block_ruleization'
  ) {
    configurationForm.groupsConfiguration.push({})
  }
}

//添加分组
const addGroup = (data) => {
  configurationForm.groupsConfiguration = data
}
const updateStratifiedGroups = (data) => {
  configurationForm.stratifiedGroups = data
}
const updateGroupData = (data) => {
  configurationForm.groupsConfiguration = data
}

const btnDisabled = ref(false)
//禁用按钮们
const updateButtonDisabled = (data) => {
  if (active.value === 1) {
    btnDisabled.value = !data
  } else {
    btnDisabled.value = false
  }
}

const removeGroup = (index) => {
  //提示是否删除
  ElMessageBox.confirm('请确定是否删除当前分组?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    confirmButtonClass: 'btn-class',
    type: 'warning'
  })
    .then(async () => {
      //若分组已经提交后,则掉接口删除,若未提交,则直接删除
      if (configurationForm.groupsConfiguration[index].id) {
        //掉接口删除
        let { data } = await RuleApi.deleteGroup({
          rule_group_id: rule_group_id.value,
          group_detail_id: configurationForm.groupsConfiguration[index].id
        })
        if (data.code == 200) {
          rule_group_id.value = data.data.rule_group_id
          configurationForm.groupsConfiguration.splice(index, 1)
        } else {
          ElMessage.error(data.message || '获取信息失败!')
        }
      } else {
        configurationForm.groupsConfiguration.splice(index, 1)
      }
    })
    .catch((error) => {
      console.error('取消删除:', error)
    })
}

//保存或下一步
const submitForm = (formEl, isSave) => {
  if (!formEl) return
  formEl.validate((valid, fields) => {
    if (valid) {
      if (active.value === 0) {
        // 第一步
        submitFirst(isSave)
      } else if (active.value === 1) {
        // 第二步
        submitSecond(isSave)
      } else if (active.value === 2) {
        // 第三步
        if (!isSave) {
          lastSubmit(isSave)
        } else {
          submitThird(isSave)
        }
      }
    } else {
      console.log('error submit!', fields)
    }
  })
}

//第一步,保存或者下一步,isSave为是否保存,true为保存,false为下一步
const submitFirst = async (isSave) => {
  saveBtnLoading.value = true
  let dataInfo = {
    disease_id: disease_id,
    rule_group_id: rule_group_id.value,
    rule_quantity: configurationForm.rule_quantity,
    stochastic_method: configurationForm.ruleMethod,
    blind_method: configurationForm.blind_method
  }
  let { data } = await RuleApi.saveRuleMethod(dataInfo)
  if (data.code == 200) {
    saveBtnLoading.value = false
    rule_group_id.value = data.data.rule_group_id
    await getInfo()
    if (!isSave) {
      //下一步
      next()
    } else {
      ElMessage.success(data.message || '保存成功!')
    }
  } else {
    saveBtnLoading.value = false
    ElMessage.error(data.message || '获取信息失败!')
  }
}

//第二步,保存或者下一步
const submitSecond = async (isSave) => {
  //至少有一个分组
  if (configurationForm.groupsConfiguration.length == 0) {
    ElMessage.error('至少需要一个分组!')
    return
  }
  saveBtnLoading.value = true
  let dataInfo = {
    disease_id: disease_id,
    rule_group_id: rule_group_id.value,
    group_details: configurationForm.groupsConfiguration.map((item) => ({
      group_coding: item.group_coding,
      group_name: item.group_name,
      patient_num: item.patient_num,
      group_remark: item.group_remark,
      group_detail_id: item.id
    })),
    third_layer_dict: configurationForm.stratifiedGroups
  }
  let { data } = await RuleApi.saveGroupDetails(dataInfo)
  if (data.code == 200) {
    saveBtnLoading.value = false
    rule_group_id.value = data.data.rule_group_id
    await getInfo()
    if (!isSave) {
      next()
    } else {
      ElMessage.success(data.message || '保存成功!')
    }
  } else {
    saveBtnLoading.value = false
    ElMessage.error(data.message || '获取信息失败!')
  }
}

//第三步,提交
const lastSubmit = (isSave) => {
  ElMessageBox.confirm('提交后将⽆法继续修改,是否继续提交?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    confirmButtonClass: 'btn-class',
    type: 'warning'
  })
    .then(async () => {
      submitThird(isSave)
      //保存后,锁定当前配置
      saveBtnLoading.value = true
      let dataInfo = {
        disease_id: disease_id,
        rule_group_id: rule_group_id.value,
        rule_quantity: configurationForm.rule_quantity,
        stochastic_method: configurationForm.ruleMethod,
        blind_method: configurationForm.blind_method,
        is_lock: 0
      }
      let { data } = await RuleApi.saveRuleMethod(dataInfo)
      if (data.code == 200) {
        isSubmitted.value = true
        saveBtnLoading.value = false
        rule_group_id.value = data.data.rule_group_id
        ElMessage.success(data.message || '提交成功!')
      } else {
        saveBtnLoading.value = false
        ElMessage.error(data.message || '获取信息失败!')
      }
    })
    .catch(() => {
      //取消提交
      console.log('取消提交')
    })
}

//第三步,保存或者提交
const submitThird = async (isSave) => {
  let dataInfo = {
    disease_id: disease_id,
    rule_group_id: rule_group_id.value,
    rule_rule_type: configurationForm.rule_rule_type
  }
  if (configurationForm.rule_rule_type == 'group') {
    // 分组规则
    dataInfo.group_rule_details = configurationForm.ruleNumberRules.map((item) => ({
      group_detail_id: item.id,
      regular_prefix: item.regular_prefix,
      regular_suffix: item.regular_suffix,
      regular_serial_number: item.regular_serial_number
    }))
    await submitGroup(isSave, dataInfo)
  } else {
    //项目规则
    dataInfo.regular_prefix = configurationForm.regular_prefix
    dataInfo.regular_suffix = configurationForm.regular_suffix
    dataInfo.regular_serial_number = configurationForm.regular_serial_number
    await submitProject(isSave, dataInfo)
  }
}

//保存或者提交项目规则
const submitProject = async (isSave, dataInfo) => {
  saveBtnLoading.value = true
  let { data } = await RuleApi.saveProjectRules(dataInfo)
  if (data.code == 200) {
    saveBtnLoading.value = false
    rule_group_id.value = data.data.rule_group_id
    //提交
    if (!isSave) {
      next()
    } else {
      ElMessage.success(data.message || '保存成功!')
    }
  } else {
    saveBtnLoading.value = false
    ElMessage.error(data.message || '获取信息失败!')
  }
}

//保存或者提交分组规则
const submitGroup = async (isSave, dataInfo) => {
  saveBtnLoading.value = true
  let { data } = await RuleApi.saveGroupRules(dataInfo)
  if (data.code == 200) {
    saveBtnLoading.value = false
    rule_group_id.value = data.data.rule_group_id
    //提交
    if (!isSave) {
      next()
    } else {
      ElMessage.success(data.message || '保存成功!')
    }
  } else {
    saveBtnLoading.value = false
    ElMessage.error(data.message || '获取信息失败!')
  }
}
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

第二个文件是,第二步的内容,这里我试着用ts来写了,所以踩了一些响应式的坑,在表单校验方面试错很多,代码如下:

secondStep.vue

<template>
  <p class="styled-text mt">{{ form.ruleMethod == 'block_ruleization' ? '二层分组' : '分类名称区组配置' }}</p>
  <div class="columnCS flex1 mb-2">
    <el-button v-show="!isSubmitted" type="primary" class="mb-1" @click="addGroup">
      {{ form.ruleMethod == 'block_ruleization' ? '添加分组' : '添加分类名称' }}
    </el-button>
    <div style="width: 100%; overflow-y: auto">
      <!-- 二层分组 -->
      <template v-if="form.ruleMethod == 'block_ruleization'">
        <el-card v-for="(item, index) in groupsConfiguration" :key="index" class="mb-2 small-card" shadow="hover">
          <template #header>
            <div class="rowBC">
              <span class="fw-14">{{ item.group_coding }}</span>
              <el-button type="danger" :icon="Close" link @click="removeGroup(index, item.group_coding)" />
            </div>
          </template>
          <el-form-item
            label="分组id"
            :prop="`groupsConfiguration.${index}.group_coding`"
            :rules="[{ required: true, message: '请填写', trigger: 'blur' }]"
          >
            <el-input v-model="item.group_coding" placeholder="请填写" clearable />
          </el-form-item>
          <el-form-item
            label="分组名称"
            :prop="`groupsConfiguration.${index}.group_name`"
            :rules="[{ required: true, message: '请填写', trigger: 'blur' }]"
          >
            <el-input v-model="item.group_name" placeholder="请填写" clearable />
          </el-form-item>
          <el-form-item
            label="分组数量"
            :prop="`groupsConfiguration.${index}.patient_num`"
            :rules="[
              { required: true, message: '请填写', trigger: 'blur' },
              { validator: validatePatientNum(index), trigger: 'blur' }
            ]"
          >
            <el-input-number
              v-model="item.patient_num"
              :precision="0"
              :min="0"
              :placeholder="`多组数量之和不超过数量${form.rule_quantity}`"
              @change="validateTotalQuantity(item)"
            />
          </el-form-item>
          <el-form-item label="备注" :prop="`groupsConfiguration.${index}.group_remark`">
            <el-input v-model="item.group_remark" placeholder="请填写" type="textarea" :rows="1" clearable />
          </el-form-item>
        </el-card>
      </template>
      <template v-else>
        <!-- 分类名称区组配置 -->
        <el-card v-for="(item, key, index) in stratifiedGroups" :key="key" class="mb-2 small-card" shadow="hover">
          <template #header>
            <div class="rowBC">
              <span class="fw-14">{{ item.divisor }}</span>
              <el-button type="danger" :icon="Close" link @click="removeGroup(index, key)" />
            </div>
          </template>
          <el-form-item
            label="分类名称"
            :prop="`stratifiedGroups.${key}.divisor`"
            :rules="[{ required: true, message: '请填写', trigger: 'blur' }]"
          >
            <el-input v-model="item.divisor" placeholder="请填写" clearable />
          </el-form-item>

          <el-form-item
            v-for="(option, optIndex) in stratifiedGroups[key].option"
            :key="optIndex"
            label="选项"
            :prop="`stratifiedGroups.${key}.option.${optIndex}`"
            :rules="[{ required: true, message: '请填写', trigger: 'blur' }]"
          >
            <div class="rowBC" style="flex: 1">
              <el-input v-model="item.option[optIndex]" placeholder="选项名称" clearable />
              <el-button type="danger" link class="ml-1" @click="removeOption(optIndex, key)">
                <el-icon size="20"><RemoveFilled /></el-icon>
              </el-button>
            </div>
          </el-form-item>

          <el-button
            type="primary"
            style="margin-left: 100px; width: 30px"
            link
            @click="addOption(item.option?.length || 0, key)"
          >
            <el-icon size="20"><CirclePlusFilled /></el-icon>
          </el-button>
        </el-card>
        <el-button type="primary" @click="createStratifiedGroups">生成分类名称组合</el-button>
        <!-- 形成的表格 -->
        <el-table
          v-if="createdTable"
          border
          :data="tableData"
          class="table-small-custom mt-2 mb"
          :header-cell-style="{
            backgroundColor: 'var(--el-table-header-color-light)'
          }"
        >
          <el-table-column type="index" label="序号" width="50" />
          <el-table-column v-for="(item, key, index) in stratifiedGroupsCopy" :key="key" :label="item.divisor">
            <template #default="scope">
              <span>{{ JSON.parse(scope.row.group_name)[index] }}</span>
            </template>
          </el-table-column>
          <el-table-column label="分组数量" min-width="135">
            <template #default="scope">
              <el-input-number
                v-model="scope.row.patient_num"
                :precision="0"
                :min="0"
                @change="validateTotalQuantity(scope.row)"
              />
            </template>
          </el-table-column>
        </el-table>
      </template>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { ref, Ref, watch, reactive, PropType } from 'vue'
import { Close } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'

interface GroupConfiguration {
  group_coding: string
  group_name: string
  patient_num: number
  group_remark?: string
  id?: number
}

interface RuleNumberRule {
  regular_prefix: string
  regular_suffix: string
  regular_serial_number: string
}

interface ThirdLayer {
  divisor: string
  option: string[]
  rank: number
}

interface FormProps {
  is_enable: boolean
  ruleMethod: string
  blind_method: string
  rule_quantity: number | null
  groupsConfiguration?: GroupConfiguration[]
  stratifiedGroups?: { [key: string]: ThirdLayer }
  rule_rule_type: string
  regular_prefix: string
  regular_suffix: string
  regular_serial_number: string
  ruleNumberRules: RuleNumberRule[]
}
const emit = defineEmits([
  'addGroup',
  'removeGroup',
  'updateStratifiedGroups',
  'updateGroupData',
  'updateButtonDisabled'
])

const props = defineProps({
  //表单数据
  form: {
    required: true,
    type: Object as PropType<FormProps>
  },

  isSubmitted: {
    default: false,
    type: Boolean
  },
  ruleFormRef: {
    default: null,
    type: Object
  }
})
const createdTable = ref(false)
const tableData: Ref<GroupConfiguration[]> = ref([])

const groupsConfiguration = ref([] as GroupConfiguration[])

const stratifiedGroups = ref<{ [key: string]: ThirdLayer }>({
  // third_layer_1: {
  //   divisor: '',
  //   option: ['', ''],
  //   rank: 1
  // }
})

const addGroup = () => {
  if (props.form.ruleMethod == 'block_ruleization') {
    groupsConfiguration.value.push({ group_coding: '', group_name: '', patient_num: 0, group_remark: '' })
    emit('addGroup', groupsConfiguration.value)
  } else {
    let lastKey = ''
    if (Object.keys(stratifiedGroups.value).length == 1) {
      //只有一个分类名称
      lastKey = 'third_layer_1'
    } else if (Object.keys(stratifiedGroups.value).length == 0) {
      //没有分类名称
      lastKey = 'third_layer_0'
    } else {
      lastKey = Object.keys(stratifiedGroups.value).slice(-1)[0]
    }
    const newThirdLayerKey = `third_layer_${parseInt(lastKey.split('_')[1]) + 1}`

    const newThirdLayer = reactive<ThirdLayer>({
      divisor: '',
      option: ['', ''],
      rank: parseInt(lastKey.split('_')[1]) + 1
    })
    Object.assign(stratifiedGroups.value, { [newThirdLayerKey]: newThirdLayer })
    createdTable.value = false
    //更新层级数据
    emit('updateStratifiedGroups', stratifiedGroups.value)
    emit('updateButtonDisabled', createdTable.value)
  }
}
const removeGroup = (index: number, key: string | number) => {
  if (props.form.ruleMethod == 'block_ruleization') {
    emit('updateGroupData', groupsConfiguration.value)
    emit('removeGroup', index)
  } else {
    //提示是否删除
    ElMessageBox.confirm('请确定是否删除当前分类名称?', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      confirmButtonClass: 'btn-class',
      type: 'warning'
    })
      .then(async () => {
        // 在“分类名称区组配置”模式下,删除指定的分类名称
        if (stratifiedGroups.value[key]) {
          delete stratifiedGroups.value[key]
          updateData(false)
          // 触发表格更新
          createTableData()
          updateData(true)
        } else {
          console.error(`Key ${key} does not exist in stratifiedGroups.`)
        }
      })
      .catch(() => {
        console.log('取消删除')
      })
  }
}

//分组计算数量
const validatePatientNum = (index: number) => {
  return (rule: any, value: any, callback: any) => {
    const totalPatientNum = groupsConfiguration.value.reduce((total, item, i) => {
      if (i !== index) {
        return total + item.patient_num
      }
      return total
    }, 0)
    if (totalPatientNum + value > (props.form.rule_quantity ?? 0)) {
      groupsConfiguration.value[index].patient_num = 0
      callback(new Error(`多组数量之和不超过数量${props.form.rule_quantity}`))
    } else {
      callback()
    }
  }
}

//添加选项
const addOption = (index: number, key: string | number) => {
  stratifiedGroups.value[key].option.push('')
  updateData(false)
}

//移除选项
const removeOption = (index: number, key: string | number) => {
  stratifiedGroups.value[key].option.splice(index, 1)
  updateData(false)
  // 触发表格更新
  createTableData()
}

//分类名称分组计算数量
const validateTotalQuantity = (changedRow: GroupConfiguration) => {
  if (!changedRow.patient_num) {
    changedRow.patient_num = 0
  }
  const totalQuantity = tableData.value.reduce((total, row) => total + (row?.patient_num as number), 0)
  if ((totalQuantity as number) > (props.form.rule_quantity as number)) {
    changedRow.patient_num = 0
    ElMessage.error(`多组数量之和不超过数量${props.form.rule_quantity}`)
    return
  }
  if (props.form.ruleMethod == 'block_ruleization') {
    emit('updateGroupData', groupsConfiguration.value)
    return
  }
  //更新数据
  emit('updateGroupData', tableData.value)
  emit('updateButtonDisabled', createdTable.value)
}

//生成分类名称区配置和表格
const createStratifiedGroups = () => {
  props.ruleFormRef.validate((valid: boolean) => {
    if (valid) {
      updateData(true)
      //计算生成表格
      createTableData()
    } else {
      ElMessage.error('请检查输入内容,填写完毕后再生成分类名称组合!')
      return false
    }
  })
}

const stratifiedGroupsCopy = ref<{ [key: string]: ThirdLayer }>({})

//生成表格,对比原始数据
const createTableData = () => {
  Object.assign(stratifiedGroupsCopy, stratifiedGroups)
  // Store existing data in a map for quick lookup
  const existingDataMap = new Map<string, GroupConfiguration>()
  tableData.value.forEach((item) => {
    existingDataMap.set(item.group_coding, item)
  })

  // 生成表格数据的函数
  const generateCombinations = (options: string[][]): GroupConfiguration[] => {
    const results: GroupConfiguration[] = []

    const createCombinations = (index: number, currentCombination: string[]) => {
      if (index === options.length) {
        const groupCoding = currentCombination.join('__')
        const existingEntry = existingDataMap.get(groupCoding)
        console.log(existingDataMap, existingEntry)

        results.push({
          group_coding: groupCoding,
          group_name: JSON.stringify(currentCombination),
          patient_num: existingEntry ? existingEntry.patient_num : 0, // Use existing patient_num if available
          id: existingEntry ? existingEntry.id : undefined,
          group_remark: '' //这里没有传id,因为修改时,直接可以覆盖之前的规则
        })
        return
      }

      for (const option of options[index]) {
        createCombinations(index + 1, [...currentCombination, option])
      }
    }

    createCombinations(0, [])
    createdTable.value = true
    emit('updateButtonDisabled', createdTable.value)
    return results
  }

  // 构建所有选项数组,并检查空选项
  const allOptions: string[][] = []
  for (let item in stratifiedGroupsCopy.value) {
    const options = stratifiedGroupsCopy.value[item].option
    if (options.length === 0 || options.some((opt) => opt.trim() === '')) {
      const divisorName = stratifiedGroupsCopy.value[item].divisor
      let errorMessage = ''
      if (options.length === 0) {
        errorMessage = `注意:分类名称【${divisorName}】没有任何选项,请添加选项!`
      }
      if (options.some((opt) => opt.trim() === '')) {
        errorMessage = `注意:分类名称【${divisorName}】存在空选项,请填写完整!`
      }
      ElMessage.error(errorMessage)
      createdTable.value = false
      emit('updateButtonDisabled', createdTable.value)
      return
    }
    allOptions.push(options)
  }

  // 生成表格数据
  tableData.value = generateCombinations(allOptions)
}

//更新层级数据和按钮状态
const updateData = (data: boolean) => {
  emit('updateStratifiedGroups', stratifiedGroups.value)
  createdTable.value = data
  emit('updateButtonDisabled', createdTable.value)
}
watch(
  [() => props.form.stratifiedGroups, () => props.form.groupsConfiguration],
  ([newStratifiedGroups, newGroupsConfiguration], [oldStratifiedGroups, oldGroupsConfiguration]) => {
    if (props.form.ruleMethod == 'block_ruleization') {
      createdTable.value = true
      groupsConfiguration.value = props.form.groupsConfiguration || []
      emit('updateButtonDisabled', createdTable.value)
      return
    }

    if (newStratifiedGroups && JSON.stringify(newStratifiedGroups) === '{}') {
      addGroup()
    } else if (newStratifiedGroups) {
      Object.assign(stratifiedGroups.value, newStratifiedGroups)
    }

    // 判断 newGroupsConfiguration 是否需要处理
    const isNewGroupsConfigurationEmpty =
      Array.isArray(newGroupsConfiguration) &&
      (newGroupsConfiguration.length === 0 ||
        (newGroupsConfiguration.length === 1 && JSON.stringify(newGroupsConfiguration[0]) === '{}'))

    const isOldGroupsConfigurationEmpty =
      Array.isArray(oldGroupsConfiguration) &&
      (oldGroupsConfiguration.length === 0 ||
        (oldGroupsConfiguration.length === 1 && JSON.stringify(oldGroupsConfiguration[0]) === '{}'))

    if (
      newGroupsConfiguration &&
      newGroupsConfiguration !== oldGroupsConfiguration && // 引用变化
      !isNewGroupsConfigurationEmpty && // 不为 [] 或 [{}]
      !isOldGroupsConfigurationEmpty // 旧值不为 [] 或 [{}]
    ) {
      tableData.value = props.form.groupsConfiguration || []
      groupsConfiguration.value = props.form.groupsConfiguration || []
      createTableData()
      createdTable.value = true
      emit('updateButtonDisabled', createdTable.value)
    }
  },
  { immediate: true, deep: true }
)
</script>

<style lang="scss" scoped>
@import '../index.scss';
</style>

 其实这里的主要难点,一个是表单校验的问题,一个是响应式的问题。

这里代码贴上去了,以后有遇到同样的多层数据的赋值或者拷贝,如果需要保留响应式一定要不能浅拷贝,这样会有很多弊端,尤其是在ts下,严格校验数据的类型,更会有各种问题出现。

记录一下,长长记性。

最后实现效果:


四、总结

其实实现的功能看起来不难,只是因为数据结构已经定好了,后端不愿意改,所以在两种情况下前端只能这样处理,并且添加了很多校验和提示,所以显得代码很冗余,但为了响应式,几乎每一个代码只能这样写,我想在处理数据方面,我以后多学一些算法,可能更熟练。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值