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

news2024/9/20 8:08:17

目录

一、应用场景

 二、开发流程

三、详细开发流程

四、总结


一、应用场景

最近开发了一个规则类的配置功能,这个功能之前就写过,最近完善了一下,所以将原先的规则变得更多元化,结构也更多了一层,添加新功能的时候试着用了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下,严格校验数据的类型,更会有各种问题出现。

记录一下,长长记性。

最后实现效果:


四、总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2106745.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java箱与泛型

大O的渐进表示法 大 O 的渐进表示法 去掉了那些对结果影响不大的项 &#xff0c;简洁明了的表示出了执行次数。 void func1(int N){ int count 0; for (int i 0; i < N ; i) { for (int j 0; j < N ; j) { count; } } for (int k 0; k < 2 * N ; k) { count; } in…

深度学习示例2-多输入多输出的神经网络模型

一、代码示例 from tensorflow import keras from tensorflow.keras import layers import numpy as np# 定义 多输入 多输出的模型 vocabulary_size = 1000 num_tags = 100 num_departments = 4title = keras.Input(shape=(vocabulary_size,), name = "title") tex…

【虚拟化】KVM常用命令操作(virsh磁盘管理)

目录 一、KVM概述 1.1 KVM工具栈 1.2 libvirt架构概述 1.3 KVM磁盘格式介绍 1.4 KVM磁盘操作常见语法 1.5 qemu-img命令简介 1.6 libguestfs安装 二、虚拟机磁盘管理 2.1 查看虚拟机磁盘 2.2 创建虚拟机磁盘 2.3 扩容磁盘容量 2.4 查看虚拟机存储状态 2.5 快照 2…

基于BiLSTM-CRF的医学命名实体识别研究(下)模型构建

一.生成映射字典 接下来需要将每个汉字、边界、拼音、偏旁部首等映射成向量。所以&#xff0c;我们首先需要来构造字典&#xff0c;统计多少个不同的字、边界、拼音、偏旁部首等&#xff0c;然后再构建模型将不同的汉字、拼音等映射成不同的向量。 在prepare_data.py中自定义…

实现自定义的移动端双指缩放

原理&#xff1a; DOM上绑定双指触控相关的事件&#xff0c;当双指触控时&#xff0c;保存初始距离&#xff0c;当双指移动时&#xff0c;计算两触控点的距离&#xff0c;根据移动中的距离与初始距离调节缩放比例&#xff0c;再根据缩放比例改变元素样式即可实现缩放 效果演示…

Java,版本控制:算法详解与实现

Spring Boot微服务架构技术及其版本号比较优化 随着云技术和分布式系统的快速发展&#xff0c;微服务架构已经成为现代软件开发不可或缺的一部分。 Spring Boot&#xff0c;作为一款广受欢迎的Java开发框架&#xff0c;其简洁的配置和快速启动的特性深受开发者青睐。 配合Sp…

旅游线路规划和路线下载

新疆旅游&#xff0c;规划一个北疆旅游线路安排如下&#xff1a; 第一天&#xff1a;从乌鲁木齐到魔鬼城&#xff0c;晚上住宿克拉玛依市乌尔禾区&#xff1b; 第二天&#xff1a;从克拉玛依市乌尔木区到五彩滩&#xff0c;晚上住宿贾登峪&#xff1b; 第三天&#xff1a;从…

win10本地设置无密码远程桌面登录设置

win10本地设置无密码远程桌面登录

软考超详细准备之软件设计师的计算机系统题型二(上午题)

目录 流水线 存储器: cache Cache命中率的相关图形 中断 相关习题 输入和输出 相关习题 总线 相关习题 加密技术与认证技术 相关习题 加密技术 相关习题 杂题 流水线 流水线&#xff08;Pipeline&#xff09;是一种在硬件设计中用于提高效率和吞吐量的技术&…

SOMEIP_ETS_088: SD_Answer_multiple_subscribes_together

测试目的&#xff1a; 验证设备&#xff08;DUT&#xff09;是否能够接受它接收到的每个SubscribeEventgroup条目。 描述 本测试用例旨在检查DUT在接收到包含多个SubscribeEventgroup条目的消息时&#xff0c;是否能够为每个条目发送SubscribeEventgroupAck。 测试拓扑&…

Runway删库跑路,备受瞩目的Stable Diffusion v1.5不见了!

替换方案&#xff1a; Hugging Face 模型镜像 - Gitee AIGitee AI 汇聚最新最热 AI 模型&#xff0c;提供模型体验、推理、训练、部署和应用的一站式服务&#xff0c;提供充沛算力&#xff0c;做中国最好的 AI 社区。https://ai.gitee.com/hf-models

【小程序 - 大智慧】深入微信小程序的核心原理

目录 课程目标背景双线程架构WebView 结构快速渲染 PageFrame编译原理Exparser通讯系统生命周期基础库解包跨端框架预编译半编译半运行运行时框架 主流技术Tarouni-app汇总 下周安排 课程目标 本次课程主要通过后台管理小程序回顾一下小程序的高阶语法&#xff0c;然后讲解整体…

Django+Vue协同过滤算法图书推荐系统的设计与实现

目录 1 项目介绍2 项目截图3 核心代码3.1 需要的环境3.2 Django接口层3.3 实体类3.4 config.ini3.5 启动类3.5 Vue 4 数据库表设计5 文档参考6 计算机毕设选题推荐7 源码获取 1 项目介绍 博主个人介绍&#xff1a;CSDN认证博客专家&#xff0c;CSDN平台Java领域优质创作者&…

144. 腾讯云Redis数据库

文章目录 一、Redis 的主要功能特性二、Redis 的典型应用场景三、Redis 的演进过程四、Redis 的架构设计五、Redis 的数据类型及操作命令六、腾讯云数据库 Redis七、总结 Redis 是一种由 C 语言开发的 NoSQL 数据库&#xff0c;以其高性能的键值对存储和多种应用场景而闻名。本…

Vue3 实现解析markdown字段以及文件

Vue实现博客前端&#xff0c;需要实现markdown的解析&#xff0c;如果有代码则需要实现代码的高亮。 Vue的markdown解析库有很多&#xff0c;如markdown-it、vue-markdown-loader、marked、vue-markdown等。这些库都大同小异。这里选用的是marked。 一、安装依赖库 在vue项目…

数据权限的设计与实现系列6——前端筛选器组件Everright-filter使用探索

linear 功能探索 最终我们是需要使用 API 的方式&#xff0c;调用后端服务拉取数据填充筛选器组件&#xff0c;不过在探索阶段&#xff0c;直接用 API 方式&#xff0c;就需要构造 mock 数据&#xff0c;比较麻烦&#xff0c;因此先使用 Function 方式来进行功能验证。 组件初…

关于找不到插件 ‘org.springframework.boot:spring-boot-maven-plugin:‘的解决方案

找到项目结构后&#xff0c;点击库&#xff0c;全选所有后点击应用即可

超声波眼镜清洗机买哪款?2024超声波眼镜清洗机推荐

超声波清洗机正逐渐成为广受欢迎的清洁解决方案&#xff0c;它以高效、深入且细腻的清洁效果&#xff0c;以及操作上的简易性&#xff0c;赢得了消费者的广泛喜爱。不过&#xff0c;市面上琳琅满目的品牌、多样化的型号及波动的价格区间&#xff0c;确实给消费者挑选时带来了不…

C1-2 ABB二次SDK开发——手把手教登录对应的机器人控制器(图片引导操作)登录机器人控制器和刷新机器人列表

1.完成配置后我们开始进行操作 C1-1 ABB二次SDK开发——C#Window窗体-环境配置&#xff08;带ABB二次开发SDK资源包&#xff09;-CSDN博客文章浏览阅读95次。3.记住路径&#xff0c;右键C#引用&#xff0c;然后导入ABB.Robotics.Controllers.PC.dll。2.安装资源文件PCABB二次开…

通过组合Self-XSS + CSRF得到存储型XSS

在一次漏洞赏金挖掘中&#xff0c;我在更改用户名的功能点出发现了一个XSS&#xff0c;在修改用户名的地方添加了一个简单的XSS payload并且刷新页面&#xff1a; 用户设置面板 XSS证明 但是问题是这个功能配置并不是公共的&#xff0c;造成XSS漏洞的唯一方法是告诉受害者将其…