当前位置: 首页 > news >正文

Vue3+Element Plus对话框保存按钮禁用状态控制(打开对话框时禁用、数据加载过程中禁用、数据加载完成后若无修改则禁用、用户修改明细后启用、保存成功后再次禁用)

核心目标:

打开对话框时禁用、数据加载过程中禁用、数据加载完成后若无修改则禁用、用户修改明细后启用、保存成功后再次禁用

应用效果:

打开对话框时禁用,数据无修改禁用

修改数据后启用

数据加载中/数据保存中禁用

保存成功后禁用

代码:

<script setup lang="ts"> /** * 资金分配流程对话框组件 */ defineOptions({ name: "CapitalAllocateWorkflowInstanceDialog" }); import { capitalAllocateApi, getActivityEditableFieldList, getAuditInfoList } from "@/api"; import { BasePreventReClickButtonEmit, WorkflowSubmitDialog } from "@/components"; import { useDepartmentUserTree } from "@/hooks"; import type { ActivityEditableField, AuditInfo, CapitalAllocate, CapitalAllocateDetail } from "@/types"; import type { CascaderValue } from "element-plus"; import { ElMessage } from "element-plus"; import { computed, nextTick, onUnmounted, ref, watch } from "vue"; interface Props { /** 资金分配 */ capitalAllocate: CapitalAllocate | null; } const props = defineProps<Props>(); const emit = defineEmits<{ audit: []; }>(); // 对话框显示标识 const visible = ref(false); // 加载状态 const loading = ref(false); // 标记组件是否已卸载 let isMounted = true; // 资金分配明细列表 const allocateDatas = ref<CapitalAllocateDetail[]>([]); // 获取可编辑字段列表 const editableFieldDatas = ref<ActivityEditableField[]>([]); // 计算属性:可编辑字段列表(已去重) const editableFieldNames = computed(() => { const names = editableFieldDatas.value.map((item) => item.fieldName); return [...new Set(names)]; }); // 审批信息列表 const auditDatas = ref<AuditInfo[]>([]); // 用于增强竞态防护 let requestId = 0; // 流程提交对话框实例 ref const workflowSubmitDialogRef = ref<InstanceType<typeof WorkflowSubmitDialog> | null>(null); // 部门用户树 hook const { departmentUserTreeData } = useDepartmentUserTree(); // 标记:数据是否已加载完成 const dataLoaded = ref(false); // 标记:数据是否被修改 const isModified = ref(false); // 按钮禁用状态 const buttonAuditDisabled = computed(() => loading.value); const buttonBackDisabled = computed(() => loading.value); const buttonWithdrawDisabled = computed(() => loading.value); const buttonSaveDisabled = computed(() => loading.value || !isModified.value); // 办理 const handleAudit = async () => { // 检查 if (!check()) return; // 打开流程提交对话框 workflowSubmitDialogRef.value?.openDialog(); }; // 检查 const check = () => { if (editableFieldNames.value.includes("userMoniker")) { const emptyUserPersons = allocateDatas.value.filter((item) => !item.userPerson); if (emptyUserPersons.length > 0) { ElMessage.warning("使用人不能为空"); return false; } } return true; }; // 退回 const handleBack = async () => {}; // 撤回 const handleWithdraw = async () => {}; // 保存 const handleSave = async () => { loading.value = true; try { await capitalAllocateApi.saveDetail(allocateDatas.value); ElMessage.success("保存成功"); isModified.value = false; // 重置修改状态(未修改) } catch (error) { ElMessage.error("保存失败"); } finally { loading.value = false; } }; // 关闭 const handleClose = async () => { visible.value = false; }; // 发送网络请求获取数据 const fetchData = async (id: number, workflowInstanceId: number, activityId: number) => { const currentId = ++requestId; loading.value = true; try { // 并发请求 const [detailResult, fieldResult, auditResult] = await Promise.allSettled([ // 获取资金分配明细列表 capitalAllocateApi.getAllocateDetail(id), // 获取活动可编辑字段 getActivityEditableFieldList(activityId), // 获取审批信息列表 getAuditInfoList(workflowInstanceId) ]); // 组件已卸载 或 不是当前最新请求则返回 if (!isMounted || currentId !== requestId) return; // 更新数据 if (detailResult.status === "fulfilled" && detailResult.value?.data) { allocateDatas.value = detailResult.value.data; } // 等待 watch 回调执行(此时 dataLoaded 仍为 false,不会错误修改 isModified) await nextTick(); dataLoaded.value = true; // 标记数据加载完成 isModified.value = false; // 重置修改状态(未修改) if (fieldResult.status === "fulfilled" && fieldResult.value?.data) { editableFieldDatas.value = fieldResult.value.data; } if (auditResult.status === "fulfilled" && auditResult.value?.data) { auditDatas.value = auditResult.value.data; } // 处理错误并友好提示 const hasDetailError = detailResult.status !== "fulfilled" || !detailResult.value?.data; const hasFieldError = fieldResult.status !== "fulfilled" || !fieldResult.value?.data; const hasAuditError = auditResult.status !== "fulfilled" || !auditResult.value?.data; if (hasDetailError && hasAuditError) { ElMessage.error("数据加载失败,请稍后重试"); } else { if (hasDetailError) ElMessage.error("资金分配明细加载失败"); if (hasFieldError) ElMessage.error("可编辑字段加载失败"); if (hasAuditError) ElMessage.error("审核信息加载失败"); } } catch (error) { console.error("请求过程发生意外错误", error); ElMessage.error("系统错误,请稍后重试"); } finally { if (isMounted && currentId === requestId) loading.value = false; } }; // 处理办理(提交或退回)时的关闭 const handleCloseOnAudit = async () => { emit("audit"); await nextTick(); // 关闭对话框 handleClose(); }; // 处理使用人改变,当前数据模型 value 的数据样式为 ['CWK', '096'] 或 undefined(清除内容时为 undefined) const handleUserPersonChange = async (value: CascaderValue) => { if (value) { allocateDatas.value[0].userPerson = String((value as string[])[1]); } }; // 停止 watch 的函数 const stopWatch = watch( // 监听一个包含两个依赖项的数组:visible 和 capitalAllocate 的 id () => [visible.value, props.capitalAllocate?.id], async ([newVisible, newId], [oldVisible, oldId]) => { if (!newVisible || !newId) return; // 确保对象存在 if (!props.capitalAllocate) return; const { id, activityInstance } = props.capitalAllocate; // 防止竞态:如果 id 在 watch 触发后又被改变,则放弃本次请求 if (id !== newId) return; // 前置检查 if (!activityInstance.workflowInstanceId || !activityInstance.activityId) { console.warn("缺少 workflowInstanceId、activityId 参数"); return; } // 发送网络请求获取数据 fetchData(id, activityInstance.workflowInstanceId, activityInstance.activityId); } ); // 深度监听 allocateDatas 内部变化,仅在数据加载完成后才响应 watch( () => allocateDatas.value, () => { // 只有数据已加载完成,且不是由程序重置引起的修改,才视为用户修改 if (dataLoaded.value) { isModified.value = true; } }, { deep: true } ); onUnmounted(() => { isMounted = false; // 停止监听,避免内存泄漏 stopWatch(); }); defineExpose({ /** 打开资金分配对话框 */ openDialog: async () => { allocateDatas.value = []; // 清空旧数据 dataLoaded.value = false; // 标记数据未加载 isModified.value = false; // 重置修改状态(未修改) visible.value = true; await nextTick(); } }); </script> <template> <div> <el-dialog class="allocate-dialog" title="资金分配" width="1130px" top="0vh" center style="border-radius: 10px" :model-value="visible" :close-on-press-escape="true" :close-on-click-modal="false" :show-close="true" @close="visible = false"> <template #default> <!-- 操作栏 --> <div class="button-group"> <BasePreventReClickButtonEmit class="short-btn" type="primary" :disabled="buttonAuditDisabled" @click="handleAudit"> 办理 </BasePreventReClickButtonEmit> <BasePreventReClickButtonEmit class="short-btn" type="primary" plain :disabled="buttonBackDisabled" @click="handleBack"> 退回 </BasePreventReClickButtonEmit> <BasePreventReClickButtonEmit class="short-btn" type="primary" plain :disabled="buttonWithdrawDisabled" @click="handleWithdraw"> 撤回 </BasePreventReClickButtonEmit> <BasePreventReClickButtonEmit class="short-btn" type="primary" plain :disabled="buttonSaveDisabled" @click="handleSave"> 保存 </BasePreventReClickButtonEmit> <BasePreventReClickButtonEmit class="short-btn" type="primary" plain @click="handleClose"> 关闭 </BasePreventReClickButtonEmit> </div> <!-- 基本信息展示表单 --> <el-form class="card card-main" :model="props.capitalAllocate" label-width="auto"> <el-row :gutter="10"> <el-col :span="12"> <el-form-item label="资金序号" label-position="right"> <el-input :value="props.capitalAllocate?.capitalNo" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="资金名称" label-position="right"> <el-input :value="props.capitalAllocate?.capitalName" /> </el-form-item> </el-col> </el-row> <el-row :gutter="10"> <el-col :span="12"> <el-form-item label="资金来源" label-position="right"> <el-input :value="props.capitalAllocate?.capitalSource" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="资金类别" label-position="right"> <el-input :value="props.capitalAllocate?.capitalType" /> </el-form-item> </el-col> </el-row> <el-row :gutter="10"> <el-col :span="12"> <el-form-item label="指标预算总额" label-position="right"> <el-input :value="props.capitalAllocate?.capitalTotal" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="指标可用总额" label-position="right"> <el-input :value="props.capitalAllocate?.capitalValidTotal" /> </el-form-item> </el-col> </el-row> </el-form> <!-- 分配明细表格表单 --> <el-form class="card card-detail" :model="allocateDatas" label-width="auto"> <el-table :data="allocateDatas" v-loading="loading" highlight-current-row> <el-table-column prop="deptName" label="指标使用部门" width="230" show-overflow-tooltip /> <el-table-column prop="total" label="指标分配金额" width="125" show-overflow-tooltip /> <el-table-column prop="budget" label="预算情况" width="140" show-overflow-tooltip /> <el-table-column prop="payType" label="支出分类" width="140" show-overflow-tooltip /> <el-table-column prop="payMode" label="支出方式" width="130" show-overflow-tooltip /> <el-table-column prop="assistDeptName" label="协助部门" min-width="100" show-overflow-tooltip /> <el-table-column prop="userMoniker" label="使用人" width="120" show-overflow-tooltip> <template #default="{ row }" v-if="editableFieldNames.includes(`userMoniker`)"> <el-cascader ref="cascaderRef" v-model="row.userPerson" :options="departmentUserTreeData" :show-all-levels="false" clearable @change="handleUserPersonChange" /> </template> </el-table-column> </el-table> </el-form> <!-- 审批信息表格表单 --> <el-form class="card card-audit" :model="auditDatas" label-width="auto"> <el-table :data="auditDatas" v-loading="loading"> <el-table-column prop="activityName" label="流程环节" width="230" show-overflow-tooltip /> <el-table-column prop="auditorName" label="办理人" width="125" show-overflow-tooltip /> <el-table-column prop="auditTime" label="办理时间" width="280" show-overflow-tooltip /> <el-table-column prop="auditIdea" label="意见" min-width="100" show-overflow-tooltip /> <el-table-column prop="auditStateDesc" label="状态" width="120" show-overflow-tooltip /> </el-table> </el-form> </template> <!-- 底部按钮,模态框底部插槽,就算没有内容,也要写一个空的插槽,否则会影响布局 --> <template #footer> </template> </el-dialog> </div> <!-- 流程提交对话框 --> <WorkflowSubmitDialog v-if="props.capitalAllocate?.activityInstance" ref="workflowSubmitDialogRef" :activity-instance="props.capitalAllocate?.activityInstance" @submit="handleCloseOnAudit" /> </template> <style scoped lang="scss"> // 现代主流风格:干净、卡片化、柔和阴影、适度圆角 .allocate-dialog { // 覆盖 Element Plus 默认内边距,让内容直接贴边 :deep(.el-dialog__header) { padding: 16px 20px; margin-right: 0; border-bottom: 1px solid var(--el-border-color-light); } :deep(.el-dialog__body) { padding: 20px; } :deep(.el-dialog__footer) { padding: 0; } // 按钮组区域 .button-group { display: flex; margin-bottom: 20px; margin-left: 16px; // 自定义按钮样式 :deep(.el-button) { border-radius: 30px; padding: 10px 24px; font-weight: 500; transition: all 0.2s; &.el-button--primary { background: linear-gradient(145deg, #2d6ff7, #1e4fd9); border: none; box-shadow: 0 4px 10px rgba(45, 111, 247, 0.3); &:hover { background: linear-gradient(145deg, #1e4fd9, #0f3bb3); box-shadow: 0 6px 14px rgba(45, 111, 247, 0.4); transform: translateY(-1px); } } &.el-button--primary.is-plain { background: #ffffff; border: 1px solid #d0d9e8; color: #1e293b; box-shadow: none; &:hover { border-color: #2d6ff7; color: #2d6ff7; background-color: #f0f6ff; } } } // 按钮禁用状态样式 :deep(.el-button.is-disabled), :deep(.el-button:disabled) { opacity: 0.6; // 降低透明度 cursor: not-allowed; // 显示不可点击光标 background-color: #f5f7fa !important; // 灰色背景(可根据设计调整) border-color: #e4e7ed !important; // 灰色边框 color: #c0c4cc !important; // 灰色文字 box-shadow: none !important; // 移除阴影 pointer-events: auto; // 保持光标样式生效(无需阻止点击,disabled 已阻止) // 如果是纯色主按钮,可单独调整 &.el-button--primary { background-color: #a0a0a0 !important; border-color: #909090 !important; } // 如果是朴素按钮,可调整背景为白色,文字灰色等 &.el-button--primary.is-plain { background-color: #fafafa !important; color: #b0b0b0 !important; } } } // 卡片通用样式 .card { background-color: #ffffff; border-radius: 10px; margin-bottom: 30px; &:last-child { margin-bottom: 0; } } // 主要信息卡片(只读表单) .card-main { :deep(.el-form-item) { margin-bottom: 8px; // 紧凑但留白 } :deep(.el-form-item__label) { height: 38px; line-height: 38px; } // 只读输入框样式:无边框,浅灰背景,文字加粗 :deep(.el-input__wrapper) { background-color: #f8fafc; box-shadow: none; border-radius: 10px; padding: 4px 12px; } } // 表格卡片(分配明细 & 审批信息) .card-detail, .card-audit { padding: 0; // 表格本身自带内边距,卡片内边距交给表格 overflow: hidden; // 让表格圆角生效 .el-table { --el-table-border-color: transparent; // 隐藏外边框,用卡片阴影代替 --el-table-header-bg-color: #f9fafb; --el-table-tr-bg-color: transparent; } :deep(.el-table__header-wrapper) { th { padding: 12px 0; border-bottom: 1px solid var(--el-border-color-lighter); } } :deep(.el-table__body-wrapper) { td { padding: 12px 0; border-bottom: 1px solid var(--el-border-color-lighter); } } // 表格加载状态样式优化 :deep(.el-loading-mask) { background-color: rgba(255, 255, 255, 0.7); border-radius: 12px; } } } </style>

保存按钮禁用状态处理详解

CapitalAllocateWorkflowInstanceDialog组件中,保存按钮(“保存”)的禁用状态被精细地控制,以确保用户在合适的时机才能执行保存操作。其核心目标是:打开对话框时禁用、数据加载过程中禁用、数据加载完成后若无修改则禁用、用户修改明细后启用、保存成功后再次禁用。整个逻辑通过几个关键响应式变量和watch的配合实现,避免了异步时序带来的状态错误。

1. 状态定义

ts

const loading = ref(false); // 全局加载状态(数据加载、保存中) const dataLoaded = ref(false); // 标记明细数据是否已完成初始加载 const isModified = ref(false); // 标记明细是否被用户修改(相对加载后的初始状态或上次保存)
  • loading:由数据加载 (fetchData) 和保存操作 (handleSave) 控制,用于屏蔽所有按钮,保证操作原子性。

  • dataLoaded:仅在fetchData成功设置明细数据后,通过await nextTick()再设为true,用于区分初始加载和用户后续修改。

  • isModified:核心修改标记,直接决定保存按钮是否启用(与loading共同作用)。

2. 保存按钮禁用计算属性

ts

const buttonSaveDisabled = computed(() => loading.value || !isModified.value);
  • 加载中loadingtrue时,按钮强制禁用,防止用户在数据加载或保存过程中重复操作。

  • 未修改isModifiedfalse时,按钮禁用,表示当前明细与加载后的初始状态或上次保存状态一致,无需保存。

  • 启用条件:只有loadingfalseisModifiedtrue时,按钮才可点击。

3. 修改标记isModified的生命周期管理

3.1 初始化和重置
  • 打开对话框时openDialog):

    ts

    allocateDatas.value = []; dataLoaded.value = false; isModified.value = false; visible.value = true;

    每次打开都彻底清空旧数据、重置标记,确保初始状态为“未加载、未修改”,按钮处于禁用。

  • 数据加载完成后fetchData成功):

    ts

    allocateDatas.value = detailResult.value.data; // 触发 watch,但 dataLoaded 仍为 false await nextTick(); // 等待 watch 执行完毕 dataLoaded.value = true; // 标记加载完成 isModified.value = false; // 重置为未修改
    • 先设置明细数据(可能触发watch),但由于此时dataLoaded仍为falsewatch不会修改isModified

    • 使用nextTick确保所有副作用(包括watch)执行完毕,再设置dataLoaded和重置isModified,避免加载过程误将isModified设为true

    • 最终isModified保持false,按钮禁用(符合“加载完成但未修改”预期)。

  • 保存成功后handleSave):

    ts

    await capitalAllocateApi.saveDetail(allocateDatas.value); isModified.value = false; // 保存后数据与服务器一致,视为未修改

    保存操作将当前明细提交到后端,成功后重置修改标记,按钮再次禁用。

3.2 用户修改自动检测(核心watch

ts

watch( () => allocateDatas.value, () => { if (dataLoaded.value) { isModified.value = true; // 用户修改明细时自动启用保存 } }, { deep: true } );
  • 监听目标allocateDatas的整个值(包括内部元素变化),使用deep: true深度监听。

  • 条件判断:只有dataLoadedtrue时,才将isModified设为true。这意味着:

    • 初始加载时dataLoadedfalse,即使明细数据被赋值(程序修改),也不会误触发isModified

    • 用户操作时dataLoaded已为true,任何对数组的修改(如通过级联选择器修改使用人、未来可能的新增/删除行)都会触发watch,自动将isModified设为true,从而启用保存按钮。

  • 为什么不需要手动设置isModified:用户交互事件(如handleUserPersonChange)中只修改数据,不直接操作isModified,完全依赖watch自动响应,简化了代码并确保所有修改途径(包括未来扩展)都能被捕获。

3.3 处理边界情况
  • 数据加载失败:如果明细接口失败,allocateDatas可能保持为空数组。此时仍需将dataLoaded设为true(表示加载过程已完成,只是无数据),以便后续用户操作(如新增行)能通过watch启用保存按钮。

  • 空数据场景:若加载后明细列表为空,isModifiedfalse,按钮禁用。此时用户通过某种方式新增一行(未来功能),watch会因数组长度变化而触发,将isModified设为true,按钮启用,符合业务逻辑。

4. 时序控制的关键:await nextTick()

fetchData中,设置allocateDatas.value后立即await nextTick()至关重要:

  • Vue 更新机制allocateDatas.value的变化会触发watch,但watch回调是异步执行的(默认在当前微任务队列后)。如果不等待nextTick,紧接着设置dataLoaded.value = true可能会导致两种时序:

    • watch回调在dataLoaded设置为true之后才执行,那么watchdataLoaded已为true,就会错误地将isModified设为true(尽管这只是初始赋值)。

    • watch回调在dataLoaded设置为true之前执行,则dataLoadedfalse,不会修改isModified,符合预期。

  • 为确保确定性,先await nextTick()强制等待所有由本次数据变化派生的回调(包括watch)执行完毕,然后再设置dataLoaded = true。这样无论watch何时执行,其执行时的dataLoaded都是false,不会误操作。

5. 加载状态loading的协同

  • 数据加载时fetchData一开始就将loading设为true,结束后设为false。期间保存按钮因loadingtrue而禁用,用户无法点击。

  • 保存操作时handleSave开始前设置loading = true,结束后恢复。同样确保保存过程中按钮禁用,防止重复提交。

  • loading的优先级高于isModified:即使isModifiedtrue,只要loadingtrue,按钮仍然禁用,保证了操作互斥。

6. 整体流程图解

text

打开对话框 ↓ 重置状态:allocateDatas=[], dataLoaded=false, isModified=false ↓ 显示对话框,触发 watch(visible+id) → 调用 fetchData ↓ fetchData 开始:loading=true ↓ 请求明细接口成功 → allocateDatas = 新数据 (触发深度 watch,但 dataLoaded=false → isModified 不变) ↓ await nextTick() 等待 watch 执行完毕 ↓ dataLoaded = true, isModified = false (标记加载完成且未修改) ↓ loading = false (数据加载结束) ↓ 此时 buttonSaveDisabled = loading (false) || !isModified (true) → true (按钮禁用) ↓ 用户修改使用人 (或其他编辑) → allocateDatas 内部变化 → 深度 watch 触发,dataLoaded=true → isModified = true ↓ buttonSaveDisabled = loading (false) || !isModified (false) → false (按钮启用) ↓ 用户点击保存 → loading=true, 保存请求, 成功后 isModified=false, loading=false ↓ 按钮恢复禁用

7. 优点总结

  • 精准控制:通过dataLoadednextTick完美区分程序赋值与用户操作,避免误启用。

  • 自动响应:深度watch自动捕获所有修改途径,未来新增编辑方式无需修改逻辑。

  • 操作互斥loading确保保存期间按钮禁用,防止重复提交。

  • 状态清晰isModified语义明确,与业务需求(修改后需保存)高度契合。

这一设计充分考虑了异步数据加载、用户交互和 Vue 响应式更新时序,是一个健壮且可维护的状态控制方案。


要使保存按钮在禁用时呈现灰色不可点击样式,可以在现有样式的基础上,为所有使用BasePreventReClickButtonEmit的按钮添加统一的禁用状态样式。由于这些按钮最终渲染为 Element Plus 的el-button,可以通过 CSS 覆盖其默认禁用样式,使其符合设计需求。

修改步骤

<style scoped lang="scss">.button-group内部,为所有按钮添加禁用状态的样式定义。具体如下:

scss

.button-group { // ... 现有样式保持不变 // 为所有按钮添加禁用状态样式 :deep(.el-button.is-disabled), :deep(.el-button:disabled) { opacity: 0.6; // 降低透明度 cursor: not-allowed; // 显示不可点击光标 background-color: #f5f7fa !important; // 灰色背景(可根据设计调整) border-color: #e4e7ed !important; // 灰色边框 color: #c0c4cc !important; // 灰色文字 box-shadow: none !important; // 移除阴影 pointer-events: auto; // 保持光标样式生效(无需阻止点击,disabled 已阻止) // 如果是纯色主按钮,可单独调整 &.el-button--primary { background-color: #a0a0a0 !important; border-color: #909090 !important; } // 如果是朴素按钮,可调整背景为白色,文字灰色等 &.el-button--primary.is-plain { background-color: #fafafa !important; color: #b0b0b0 !important; } } }

说明

  • 选择器:使用:deep(.el-button.is-disabled):deep(.el-button:disabled)穿透组件样式,确保能覆盖内部el-button的默认禁用样式。

  • !important:由于 Element Plus 自带的禁用样式可能具有较高优先级,使用!important强制覆盖,确保自定义样式生效。

  • 透明度与光标opacity: 0.6使按钮变淡,cursor: not-allowed显示不可点击图标,增强用户反馈。

  • 背景、边框、文字颜色:设置为灰色系,与启用状态的彩色按钮形成鲜明对比。

  • 区分按钮类型:分别处理主按钮(.el-button--primary)和朴素按钮(.el-button--primary.is-plain),保持设计一致性。

完整示例(整合到现有样式)

将上述代码添加到.button-group:deep块内,例如:

scss

.button-group { display: flex; margin-bottom: 20px; margin-left: 16px; // 自定义按钮样式(原有) :deep(.el-button) { border-radius: 30px; padding: 10px 24px; font-weight: 500; transition: all 0.2s; &.el-button--primary { background: linear-gradient(145deg, #2d6ff7, #1e4fd9); border: none; box-shadow: 0 4px 10px rgba(45, 111, 247, 0.3); &:hover { background: linear-gradient(145deg, #1e4fd9, #0f3bb3); box-shadow: 0 6px 14px rgba(45, 111, 247, 0.4); transform: translateY(-1px); } } &.el-button--primary.is-plain { background: #ffffff; border: 1px solid #d0d9e8; color: #1e293b; box-shadow: none; &:hover { border-color: #2d6ff7; color: #2d6ff7; background-color: #f0f6ff; } } } // 新增:禁用状态样式 :deep(.el-button.is-disabled), :deep(.el-button:disabled) { opacity: 0.6; cursor: not-allowed; background-color: #f5f7fa !important; border-color: #e4e7ed !important; color: #c0c4cc !important; box-shadow: none !important; pointer-events: auto; &.el-button--primary { background-color: #a0a0a0 !important; border-color: #909090 !important; } &.el-button--primary.is-plain { background-color: #fafafa !important; color: #b0b0b0 !important; } } }

效果验证

添加后,当保存按钮的disabled属性为true时,按钮将呈现灰色背景、灰色边框、灰色文字,且鼠标悬停时显示不可点击图标,直观地告知用户当前无法操作。

如果希望进一步定制颜色值,可根据设计稿调整上述色值。

http://www.jsqmd.com/news/472739/

相关文章:

  • 基于Cherry Studio+三方MCP+LLM在本地构建MySQL查询助手
  • 2026财富觉醒:普通人如何靠“懒人法则”实现资产滚雪球?(保姆级干货)
  • C++17新特性
  • 基于WOA鲸鱼优化的LSTM长短记忆网络模型的文本分类算法matlab仿真
  • 分⽀和循环:C语言的脊柱
  • 《HelloGitHub》第 期
  • 【优化部署】基于matlab果蝇算法改进虚拟力融合算法无线传感器网络节点部署【含Matlab源码 15143期】
  • R 基础运算
  • 螺旋矩阵总结
  • 2.2.1 - 3D图搜索算法(以A*为例) - Python运动规划库教程(Python Motion Planning)
  • Mysql安装测试--初入心得
  • Flutter 三方库 async_recursion 的鸿蒙化适配指南 - 稳健的异步递归治理,征服鸿蒙深层数据结构
  • ArkClaw让“养虾”更安全!火山引擎AI助手安全解决方案全面升级
  • 数据结构STL库(从入门到精通,适合小白)
  • 记一次 .NET 某放射治疗光学定位软件 卡死分析
  • 从通用Agent到领域Agent:技术原理与演进路径
  • 人工智能之数学基础:全微分的介绍
  • 【快速见刊】第二届生态环境保护、环境监测与修复国际学术会议(EPEMR 2026)
  • Jvm和垃圾回收精讲
  • 基于 ESP32S3 的 LVGL 9.4 图形库移植与 UI 开发实践分享(课程作业)
  • 鸿蒙常见问题分析三十三:如何解决Column子组件超出容器边界
  • OJ50 51 52
  • Leecode 18. 四数之和
  • 2026商家寄件价格避坑指南:5个省钱雷区别再踩!
  • SQL-存储引擎
  • Flutter 三方库 argos_translator_offline 的鸿蒙化适配指南 - 让机器翻译回归“端侧隔离”,打造鸿蒙应用专家级的离线多语言 AI 治理中台
  • 盘点10大主流AI Agent框架(非常详细),多智能体技术从入门到精通,收藏这一篇就够了!
  • 基于 Java + SpringBoot + Vue + MySQL 的游戏账号交易系统实战指南
  • 《MPMLS》 2026.3.12
  • 【Day4】