Vue3+Element-Plus+el-select清空选项(确定保存后数据没有改变)的问题
el-select清空选项无法保存
清空选项前:

清空选项后:

确定保存数据刷新后:数据没有改变

原来的代码:
<script setup lang="ts" name="EmergencyInfoDialog">
/**
* 应急物资信息对话框组件
*
* 使用示例:
* <EmergencyInfoDialog ref="emergencyInfoDialogRef" :emergency-info="store.currentSelectedRow" @update-emergency="handleUpdateEmergency" />
* - ref: 引用组件实例
* - emergencyInfo 应急物资信息
* - updateEmergency 更新应急物资信息
*/
defineOptions({
name: "EmergencyInfoDialog"
});
import { BasePreventReClickButton, CommonImageManager } from "@/components";
import { formatInputRMB, formatToNumber } from "@/utils";
import type { IEmergency } from "@/views/resources/emergency/types";
import { isEqual } from "lodash-es";
import { nextTick, reactive, ref, toRaw } from "vue";
import useEmergencyStore from "../stores";
const store = useEmergencyStore();
const props = defineProps<{
/**
* 应急物资信息
*/
emergencyInfo: IEmergency;
}>();
const emit = defineEmits<{
/**
* 更新应急物资信息,通知父组件执行更新应急物资信息的操作
*/
(e: "update-emergency", emergencyInfo: IEmergency): void;
}>();
// 应急物资模态框显示标识
const dialogVisible = ref(false);
// 应急物资表单
const emergencyForm = ref<IEmergency>({
id: 0,
emergencyCategory: "",
emergencyNo: "",
emergencyName: "",
emergencySpec: "",
emergencyUnit: "",
batchNo: "",
productionDate: "",
receiveDate: "",
validityDate: "",
location: "",
remark: ""
});
// 打开模态框
const openDialog = async () => {
// 使用深拷贝,为应急物资表单生成独立副本数据
emergencyForm.value = reactive<IEmergency>(JSON.parse(JSON.stringify(toRaw(props.emergencyInfo))));
await nextTick();
dialogVisible.value = true;
};
// 关闭模态框
const onCancelClick = () => {
dialogVisible.value = false;
};
// 确定
const onConfirmClick = () => {
// 两个对象不相同,需要更新数据;如果两个对象相同(所有属性值都相同),不需要更新数据
if (!isEqual(props.emergencyInfo, emergencyForm.value)) {
// 通过父组件更新数据
emit("update-emergency", emergencyForm.value);
}
dialogVisible.value = false;
};
defineExpose({
/**
* 打开应急物资信息的对话框
*/
openDialog
});
</script>
<template>
<el-dialog
class="emergency-dialog"
:title="`${store.moduleName}基础信息`"
width="765px"
top="0vh"
center
style="border-radius: 10px"
v-model="dialogVisible"
:close-on-press-escape="true"
:close-on-click-modal="false"
:show-close="true"
@close="onCancelClick">
<template #default>
<el-form :model="emergencyForm" label-width="auto" style="margin: 8px 16px">
<el-row>
<el-col :span="12">
<el-form-item label="应急物资名称" label-position="right">
<el-input v-model="emergencyForm.emergencyName" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="种类" label-position="right">
<el-select
v-model="emergencyForm.emergencyCategory"
placeholder="请选择"
clearable
filterable
allow-create
default-first-option>
<el-option label="消毒药物" value="消毒药物"></el-option>
<el-option label="消杀药物" value="消杀药物"></el-option>
<el-option label="应急队物资" value="应急队物资"></el-option>
<el-option label="应急防控物资" value="应急防控物资"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="批号" label-position="right">
<el-input v-model="emergencyForm.batchNo" clearable />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="规格型号" label-position="right">
<el-input v-model="emergencyForm.emergencySpec" clearable />
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="单位" label-position="right" label-width="50px">
<el-input v-model="emergencyForm.emergencyUnit" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="入库日期" label-position="right">
<el-date-picker
v-model="emergencyForm.receiveDate"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="生产日期" label-position="right">
<el-date-picker
v-model="emergencyForm.productionDate"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="备注" label-position="right">
<el-input v-model="emergencyForm.remark" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="有效期至" label-position="right">
<el-date-picker
v-model="emergencyForm.validityDate"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row v-show="emergencyForm.emergencyCategory?.includes(`药物`)">
<el-col :span="12">
<el-form-item label="品牌" label-position="right">
<el-input v-model="emergencyForm.manufacturer" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="有效成分" label-position="right">
<el-input v-model="emergencyForm.supplier" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row v-show="emergencyForm.emergencyCategory?.includes(`药物`)">
<el-col :span="8">
<el-form-item label="性状" label-position="right">
<el-select
v-model="emergencyForm.location"
placeholder="请选择"
clearable
filterable
allow-create
default-first-option>
<el-option label="固体" value="固体"></el-option>
<el-option label="液体" value="液体"></el-option>
<el-option label="气体" value="气体"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="剂型" label-position="right">
<el-select
v-model="emergencyForm.condition"
placeholder="请选择"
clearable
filterable
allow-create
default-first-option>
<el-option label="烟雾" value="烟雾"></el-option>
<el-option label="乳油剂" value="乳油剂"></el-option>
<el-option label="水乳剂" value="水乳剂"></el-option>
<el-option label="悬浮剂" value="悬浮剂"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="用法" label-position="right">
<el-select
v-model="emergencyForm.source"
placeholder="请选择"
clearable
filterable
allow-create
default-first-option>
<el-option label="点燃" value="点燃"></el-option>
<el-option label="热烟雾" value="热烟雾"></el-option>
<el-option label="常量专用" value="常量专用"></el-option>
<el-option label="超低容量" value="超低容量"></el-option>
<el-option label="超低容量专用" value="超低容量专用"></el-option>
<el-option label="超低容量喷雾" value="超低容量喷雾"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row v-show="emergencyForm.emergencyCategory?.includes(`应急`)">
<el-col :span="24">
<el-form-item label="图片" label-position="right">
<!-- 图片 -->
<CommonImageManager business-type="Emergency" :business-key="String(emergencyForm.id)" :max-count="5" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="库存数量" label-position="right">
<el-input
v-model="emergencyForm.amount"
clearable
disabled
readonly
@input="emergencyForm.amount = Number(formatToNumber($event, 0))" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="库存总额" label-position="right">
<el-input
v-model="emergencyForm.total"
clearable
disabled
readonly
@input="emergencyForm.total = Number(formatInputRMB($event))" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<!-- 模态框底部插槽,就算没有内容,也要写一个空的插槽,否则会影响布局 -->
<template #footer>
<div class="footer-div">
<BasePreventReClickButton class="btn" type="primary" @click="onConfirmClick">确定</BasePreventReClickButton>
<el-button class="btn" @click="onCancelClick">取消</el-button>
</div>
</template>
</el-dialog>
</template>
<style scoped lang="scss">
.el-form {
margin: 0 15px;
}
</style>
产生这个问题的根本原因分析:
1. Element Plus 的设计选择
根本原因:Element Plus 的 el-select 组件在清空时,不是将绑定的值设置为 null 或空字符串 "",而是设置为 undefined。
为什么这样设计:
-
可能是为了区分"从未设置"(undefined)和"清空后的状态"
-
在一些 JavaScript 表单处理库中,
undefined表示"用户未操作",而null或""表示"用户明确清空" -
但这样的设计在某些场景下会导致问题
2. Vue 3 响应式系统的特性
核心问题:Vue 3 对 undefined 的处理与 Vue 2 不同。
具体表现:
-
在 Vue 3 中,如果你给一个响应式对象的属性赋值为
undefined,该属性不会被删除,但值为undefined -
然而,如果后续的操作(如
JSON.stringify()、Object.assign()、展开运算符...)处理这个对象,undefined值可能被忽略或转换
3. JSON.stringify() 的行为
关键原因:你代码中使用了 JSON.parse(JSON.stringify()) 进行深拷贝。
javascript
// 在你的 openDialog 函数中 emergencyForm.value = reactive<IEmergency>(JSON.parse(JSON.stringify(toRaw(props.emergencyInfo))));
JSON.stringify() 的行为:
-
JSON.stringify({a: undefined})→"{}"(undefined 被完全移除) -
JSON.stringify({a: null})→"{"a":null}"(null 被保留) -
JSON.stringify({a: ""})→"{"a":""}"(空字符串被保留)
问题链:
-
用户清空选择框 → Element Plus 设置值为
undefined -
你的代码使用
JSON.stringify()处理对象 →undefined被移除 -
JSON.parse()后 → 属性完全不存在
4. TypeScript 接口定义的局限性
问题:TypeScript 只在编译时检查类型,运行时无法保证属性存在。
typescript
interface IEmergency {
emergencyCategory: string; // 编译时要求必须有
}
但在运行时:
-
JavaScript 对象可能没有这个属性
-
即使有,值也可能是
undefined -
TypeScript 的类型断言不能创建实际不存在的属性
5. lodash-es 的 isEqual 函数的行为
问题:isEqual 比较时,属性值为 undefined 和属性不存在是不同的。
javascript
isEqual(
{ emergencyCategory: undefined }, // 属性存在,值为 undefined
{} // 属性不存在
); // 返回 false
根本原因总结链:
-
设计决策:Element Plus 选择在清空时设置
undefined -
数据序列化:你代码中的
JSON.stringify()移除了undefined值 -
响应式初始化:初始化的对象可能缺少某些属性
-
深拷贝问题:深拷贝过程丢失了属性
-
类型安全缺失:TypeScript 无法在运行时保证属性存在
更深层的架构问题:
1. 前后端数据模型不一致
-
后端可能期望空字符串
""或null -
前端 Element Plus 产生
undefined -
缺乏统一的转换层
2. 数据流不清晰
-
没有明确的初始值管理
-
没有统一的数据清理/转换策略
-
深拷贝使用不当
3. 防御性编程不足
-
没有处理边界情况(undefined、null)
-
没有验证数据完整性
-
过度依赖框架默认行为
解决方法1:在确定时,将 undefined 转换为 ""
// 确定
const onConfirmClick = () => {
// 解决问题链,el-select 清空选项后,在提交表单时,将 undefined 转为空字符串
// 1、用户清空选择框 → Element Plus 设置值为 undefined
// 2、JSON.stringify() 处理对象 → undefined 被移除
// 3、JSON.parse() 后 → 属性完全不存在
if (emergencyForm.value.emergencyCategory === undefined) {
emergencyForm.value.emergencyCategory = "";
}
if (emergencyForm.value.location === undefined) {
emergencyForm.value.location = "";
}
if (emergencyForm.value.condition === undefined) {
emergencyForm.value.condition = "";
}
if (emergencyForm.value.source === undefined) {
emergencyForm.value.source = "";
}
// 两个对象不相同,需要更新数据;如果两个对象相同(所有属性值都相同),不需要更新数据
if (!isEqual(props.emergencyInfo, emergencyForm.value)) {
// 通过父组件更新数据
emit("update-emergency", emergencyForm.value);
}
dialogVisible.value = false;
};
解决方法2:在 el-select 中使用 @clear 事件
// 选项框清除事件处理,使用泛型 <K extends keyof IEmergency> 来限制传入的 fieldName 参数必须是 IEmergency 接口的一个有效键名
const handleFieldClear = <K extends keyof IEmergency>(fieldName: K) => {
emergencyForm.value[fieldName] = "" as IEmergency[K];
};
<el-row v-show="emergencyForm.emergencyCategory?.includes(`药物`)">
<el-col :span="8">
<el-form-item label="性状" label-position="right">
<el-select
v-model="emergencyForm.location"
placeholder="请选择"
clearable
filterable
allow-create
default-first-option
@clear="handleFieldClear(`location`)">
<el-option label="固体" value="固体"></el-option>
<el-option label="液体" value="液体"></el-option>
<el-option label="气体" value="气体"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="剂型" label-position="right">
<el-select
v-model="emergencyForm.condition"
placeholder="请选择"
clearable
filterable
allow-create
default-first-option
@clear="handleFieldClear(`condition`)">
<el-option label="烟雾" value="烟雾"></el-option>
<el-option label="乳油剂" value="乳油剂"></el-option>
<el-option label="水乳剂" value="水乳剂"></el-option>
<el-option label="悬浮剂" value="悬浮剂"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="用法" label-position="right">
<el-select
v-model="emergencyForm.source"
placeholder="请选择"
clearable
filterable
allow-create
default-first-option
@clear="handleFieldClear(`source`)">
<el-option label="点燃" value="点燃"></el-option>
<el-option label="热烟雾" value="热烟雾"></el-option>
<el-option label="常量专用" value="常量专用"></el-option>
<el-option label="超低容量" value="超低容量"></el-option>
<el-option label="超低容量专用" value="超低容量专用"></el-option>
<el-option label="超低容量喷雾" value="超低容量喷雾"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
解决方法3:在 el-select 中使用 :value-on-clear="``"
<el-col :span="12">
<el-form-item label="发布部门" label-position="right">
<el-select
v-model="qualityFileObj.issueDept"
placeholder="请选择"
clearable
filterable
allow-create
default-first-option
:value-on-clear="``">
<el-option v-for="item in departmentList" :label="item.deptName" :value="item.deptName" />
</el-select>
</el-form-item>
</el-col>
更多推荐


所有评论(0)