目录
一、应用场景
二、开发流程
三、详细开发流程
四、总结
一、应用场景
最近开发了一个规则类的配置功能,这个功能之前就写过,最近完善了一下,所以将原先的规则变得更多元化,结构也更多了一层,添加新功能的时候试着用了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下,严格校验数据的类型,更会有各种问题出现。
记录一下,长长记性。
最后实现效果:
四、总结
其实实现的功能看起来不难,只是因为数据结构已经定好了,后端不愿意改,所以在两种情况下前端只能这样处理,并且添加了很多校验和提示,所以显得代码很冗余,但为了响应式,几乎每一个代码只能这样写,我想在处理数据方面,我以后多学一些算法,可能更熟练。