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":""}" (空字符串被保留)

问题链

  1. 用户清空选择框 → Element Plus 设置值为 undefined

  2. 你的代码使用 JSON.stringify() 处理对象 → undefined 被移除

  3. 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

根本原因总结链:

  1. 设计决策:Element Plus 选择在清空时设置 undefined

  2. 数据序列化:你代码中的 JSON.stringify() 移除了 undefined 值

  3. 响应式初始化:初始化的对象可能缺少某些属性

  4. 深拷贝问题:深拷贝过程丢失了属性

  5. 类型安全缺失: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>

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐