【基于Vue3组合式API的互斥输入模式实现与实践分享】
基于Vue3组合式API的互斥输入模式实现与实践分享
目录
- 背景与痛点
- 设计思路
- 技术实现
- 使用场景与案例
- 遇到的问题与解决方案
- 最佳实践
- 总结
1. 背景与痛点
在表单交互设计中,我们经常面临这样的场景:多种输入方式互斥。例如,在评分系统中,用户可以选择通过填写明细表格进行逐项评分,也可以直接给出总评分。这两种输入方式不应同时生效,否则可能导致数据不一致。
传统解决方案的痛点:
- 状态管理分散:每个表单组件都需要单独管理互斥状态
- 逻辑重复:禁用/启用逻辑在多个地方重复编写
- 条件判断复杂:判断何时禁用另一输入模式的条件可能很复杂
- 代码耦合度高:输入状态与UI禁用逻辑强耦合
- 难以复用:相同的互斥逻辑难以在不同项目间复用
2. 设计思路
为解决上述问题,我们设计了一套基于Vue3 Composition API的解决方案,将互斥输入模式的管理抽象为可复用的组合式函数。设计原则如下:
- 关注点分离:将互斥状态管理与业务逻辑分离
- 声明式API:提供简洁的声明式API,易于理解和使用
- 智能判断:只有当有实际数据时才禁用互斥输入
- 可组合性:可与其他Vue组合式API无缝集成
- 渐进式设计:基础API灵活通用,扩展API针对特定场景优化
3. 技术实现
我们实现了两个核心组合式API:
useMutuallyExclusiveInputs
:通用的互斥输入模式管理useScoreInputModes
:基于通用API的评分场景特化版本
3.1 通用互斥输入模式API
首先,我们实现了基础的互斥输入模式管理API:
// useMutuallyExclusiveInputs.ts
import { ref, computed, Ref } from 'vue';/*** 互斥输入模式的组合式API*/
export function useMutuallyExclusiveInputs<T extends string, D = any>(modes: readonly T[],initialData: Record<T, D>,options: {initialMode?: T;disabled?: Ref<boolean>;onModeChange?: (newMode: T, oldMode: T | null) => void;confirmClear?: () => Promise<boolean> | boolean;} = {}
) {// 提取配置项const {initialMode = modes[0],disabled = ref(false),onModeChange,confirmClear = async () => true} = options;// 当前活动的输入模式const activeMode = ref<T | null>(initialMode) as Ref<T | null>;// 为每个模式创建响应式数据存储const modeData: Record<T, Ref<D>> = {} as Record<T, Ref<D>>;// 初始化每个模式的数据modes.forEach((mode) => {modeData[mode] = ref(Array.isArray(initialData[mode]) ? [...initialData[mode]] : initialData[mode]) as Ref<D>;});// 检查特定模式是否应该禁用function shouldDisable(mode: T) {return computed(() => {// 如果全局禁用,则禁用所有模式if (disabled.value) return true;// 如果当前没有激活的模式,则不禁用任何模式if (activeMode.value === null) return false;// 如果请求检查的模式就是当前激活模式,则不禁用if (activeMode.value === mode) return false;// 如果当前激活的是其他模式,检查其他模式是否有实际数据const activeData = modeData[activeMode.value].value;// 检查激活模式的数据是否有效(有实际值)if (Array.isArray(activeData)) {// 对于数组类型,检查是否有有效数据项return activeData.length > 0 && activeData.some(item => {if (typeof item === 'object' && item !== null) {return Object.values(item).some(val => val !== undefined && val !== null && val !== '');}return item !== undefined && item !== null && item !== '';});} else if (typeof activeData === 'object' && activeData !== null) {// 对于对象类型,检查是否有非空属性return Object.values(activeData).some(val => val !== undefined && val !== null && val !== '' && val !== 0);} else {// 对于原始类型,检查是否有值return activeData !== undefined && activeData !== null && activeData !== '' && activeData !== 0;}});}// 清除特定模式的数据async function clearData(mode: T): Promise<void> {const shouldClear = await confirmClear();if (!shouldClear) return;const currentData = modeData[mode].value;// 根据数据类型进行智能清空if (Array.isArray(currentData)) {// 检查数组是否为空,如果不为空才清空if (currentData.length > 0) {(modeData[mode].value as any) = [];}} else if (typeof currentData === 'object' && currentData !== null) {// 检查对象是否有有效属性,只清空有值的属性const hasValidData = Object.values(currentData).some(val => val !== undefined && val !== null && val !== '' && val !== 0);if (hasValidData) {// 创建新对象,保留原有结构但清空有值的属性const clearedObj = { ...currentData };// 遍历对象属性,只清空有值的属性Object.keys(clearedObj).forEach(key => {const value = (clearedObj as any)[key];if (value !== undefined && value !== null && value !== '' && value !== 0) {(clearedObj as any)[key] = undefined;}});modeData[mode].value = clearedObj as D;}} else {// 对于原始类型,检查是否有值if (currentData !== undefined && currentData !== null && currentData !== '' && currentData !== 0) {modeData[mode].value = undefined as unknown as D;}}}// 设置当前活动模式async function setMode(mode: T): Promise<void> {if (disabled.value) return;const oldMode = activeMode.value;// 只有当模式发生变化时才进行处理if (oldMode !== mode) {// 检查旧模式是否有实际数据let shouldClearOldData = false;if (oldMode !== null) {const oldData = modeData[oldMode].value;// 根据数据类型判断是否需要清空if (Array.isArray(oldData)) {// 对于数组,检查是否有有效项shouldClearOldData = oldData.length > 0 && oldData.some(item => {if (typeof item === 'object' && item !== null) {return Object.values(item).some(val => val !== undefined && val !== null && val !== '');}return item !== undefined && item !== null && item !== '';});} else if (typeof oldData === 'object' && oldData !== null) {// 对于对象,检查是否有非空属性shouldClearOldData = Object.values(oldData).some(val => val !== undefined && val !== null && val !== '' && val !== 0);} else {// 对于原始类型,检查是否有值shouldClearOldData = oldData !== undefined && oldData !== null && oldData !== '' && oldData !== 0;}// 只有在有实际数据需要清除时才清空if (shouldClearOldData) {await clearData(oldMode);}}// 更新活动模式activeMode.value = mode;// 触发模式变更回调if (onModeChange) {onModeChange(mode, oldMode);}}}// 获取当前活动模式的数据const activeData = computed(() => activeMode.value !== null ? modeData[activeMode.value].value : undefined);// 返回APIreturn {activeMode,modeData,setMode,clearData,shouldDisable,activeData,// 其他API...};
}
3.2 评分场景特化API
然后,我们基于通用API实现了评分场景的特化版本:
// useScoreInputModes.ts
import { ref, computed, Ref } from 'vue';
import useMutuallyExclusiveInputs from './useMutuallyExclusiveInputs';/*** 评分项接口*/
interface ScoreItem {id: string | number;score: number;scoreResult?: number;[key: string]: any;
}/*** 评分输入模式组合式API*/
export function useScoreInputModes(options = {}) {const {enabled = ref(true),totalScore = ref(100),scoreRules = {thresholds: [0, 60, 70, 80, 90, 101],levels: ['E', 'D', 'C', 'B', 'A']},minScore = 0,maxScore = 100,onScoreChange} = options;// 使用通用互斥输入模式APIconst { activeMode, modeData, setMode, clearData, reset } =useMutuallyExclusiveInputs(['table', 'final'] as const,{table: [] as ScoreItem[],final: undefined as number | undefined},{initialMode: 'table',disabled: computed(() => !enabled.value)});// 表格评分数据const tableScores = modeData.table as Ref<ScoreItem[]>;// 最终评分数据const finalScore = modeData.final as Ref<number | undefined>;// 评分等级const scoreLevel = ref<string | number>();// 表格评分总和const tableScoreTotal = computed(() => {return tableScores.value.reduce((sum, item) => {return sum + (item.scoreResult || 0);}, 0);});// 表格评分百分比const tableScorePercentage = computed(() => {if (totalScore.value <= 0) return 0;return (tableScoreTotal.value / totalScore.value) * 100;});// 表格评分是否禁用const isTableDisabled = computed(() => {// 如果全局禁用,则禁用表格评分if (!enabled.value) return true;// 只在最终评分模式且有实际的最终评分值时才禁用表格评分if (activeMode.value === 'final') {return finalScore.value !== undefined && finalScore.value !== null && finalScore.value !== 0;}return false;});// 最终评分是否禁用const isFinalDisabled = computed(() => {// 如果全局禁用,则禁用最终评分if (!enabled.value) return true;// 只在表格评分模式且表格中有实际评分项时才禁用最终评分if (activeMode.value === 'table') {return tableScores.value.some((item) => item.scoreResult !== undefined && item.scoreResult !== null && item.scoreResult !== 0);}return false;});// 切换到表格评分模式function useTableMode() {// 只有当启用且不处于表格模式时才切换if (enabled.value && activeMode.value !== 'table') {setMode('table');}}// 切换到最终评分模式function useFinalMode() {// 只有当启用且不处于最终评分模式时才切换if (enabled.value && activeMode.value !== 'final') {setMode('final');}}// 更新评分项目function updateScoreItem(id: string | number, result: number) {// 检查是否启用且在表格评分模式if (!enabled.value || activeMode.value !== 'table') return;const item = tableScores.value.find((item) => item.id === id);if (item) {// 只有当分数发生变化时才更新if (item.scoreResult !== result) {item.scoreResult = result;// 在表格评分模式下,自动计算最终评分if (tableScorePercentage.value > 0) {const calculatedScore = Math.min(maxScore,Math.max(minScore, parseFloat(tableScorePercentage.value.toFixed(2))));finalScore.value = calculatedScore;scoreLevel.value = calculateLevel(calculatedScore);if (onScoreChange) {onScoreChange(calculatedScore, scoreLevel.value);}}}}}// 更新最终评分function updateFinalScore(score: number) {// 检查是否启用if (!enabled.value) return;// 如果不在最终评分模式,切换模式if (activeMode.value !== 'final') {// 只有当没有实际的表格评分数据时才自动切换const hasTableScoreData = tableScores.value.some((item) =>item.scoreResult !== undefined && item.scoreResult !== null && item.scoreResult !== 0);if (!hasTableScoreData) {setMode('final');} else {// 如果有数据,不自动切换,避免清空现有数据return;}}// 只有当最终评分实际改变时才更新if (finalScore.value !== score) {const validScore = Math.min(maxScore, Math.max(minScore, score));finalScore.value = validScore;scoreLevel.value = calculateLevel(validScore);if (onScoreChange) {onScoreChange(validScore, scoreLevel.value);}}}// 根据分数计算等级function calculateLevel(score: number): string | number {if (score === undefined || score === null) return scoreRules.levels[0];const { thresholds, levels } = scoreRules;for (let i = 0; i < thresholds.length - 1; i++) {if (score >= thresholds[i] && score < thresholds[i + 1]) {return levels[i];}}return levels[0];}// 返回APIreturn {scoreMode: activeMode,tableScores,finalScore,scoreLevel,useTableMode,useFinalMode,isTableDisabled,isFinalDisabled,tableScoreTotal,tableScorePercentage,updateScoreItem,updateFinalScore,// 其他API...};
}
4. 使用场景与案例
该组合式API适用于多种互斥输入场景,以下是几个典型应用:
4.1 评分系统
在我们的评分系统中,用户可以通过表格逐项评分,也可以直接给出最终评分:
<template><div><!-- 表格评分 --><div><h3>评分表</h3><table><tr v-for="item in tableData" :key="item.id"><td>{{ item.name }}</td><td><inputtype="number"v-model="item.scoreResult":disabled="tableScoreInputDisabled"@focus="handleTableScoreFocus"@change="handleTableScoreChange"/></td></tr></table></div><!-- 最终评分 --><div><h3>最终评分</h3><inputtype="number"v-model="formData.finalEvaluationScore":disabled="finalScoreInputDisabled"@focus="handleFinalScoreFocus"@change="handleChange"/></div></div>
</template><script setup>
import { ref, computed } from 'vue';
import { useScoreInputModes } from '@/composables';const tableData = ref([/* 评分项数据 */]);
const formData = ref({finalEvaluationScore: undefined,finalEvaluationLevel: undefined
});
const totalScore = ref(100);// 使用评分输入模式API
const {scoreMode,useTableMode,useFinalMode,updateScoreItem,updateFinalScore,clearTableScores
} = useScoreInputModes({enabled: computed(() => true),totalScore: computed(() => totalScore.value),scoreRules: {thresholds: [0, 70, 85, 101],levels: ['1', '2', '3', '4']},maxScore: 100,onScoreChange: (score, level) => {if (score !== null) {formData.value.finalEvaluationScore = score;formData.value.finalEvaluationLevel = level?.toString();}}
});// 使用组合式API的状态来管理互斥
const isManualScoreInput = computed(() => scoreMode.value === 'final');
const hasTableScores = computed(() => {return tableData.value.some((item) => item.scoreResult !== undefined && item.scoreResult !== null && item.scoreResult !== 0);
});// 最终评分输入框禁用条件
const finalScoreInputDisabled = computed(() => {const hasRealTableScores = tableData.value.some((item) => item.scoreResult !== undefined && item.scoreResult !== null && item.scoreResult !== 0);return hasRealTableScores && !isManualScoreInput.value;
});// 表格评分输入框禁用条件
const tableScoreInputDisabled = computed(() => {return (isManualScoreInput.value && formData.value.finalEvaluationScore !== undefined && formData.value.finalEvaluationScore !== null && formData.value.finalEvaluationScore !== 0);
});// 处理最终评分输入框获得焦点
const handleFinalScoreFocus = () => {// 只需标记处于最终评分模式,但不自动禁用表格评分useFinalMode();
};// 处理表格评分输入框获得焦点
const handleTableScoreFocus = () => {// 只需标记处于表格评分模式,但不自动禁用最终评分useTableMode();
};// 处理表格得分变化
const handleTableScoreChange = () => {// 更新表格评分项tableData.value.forEach((item) => {if (item.scoreResult !== undefined && item.scoreResult !== null) {updateScoreItem(item.id, Number(item.scoreResult));}});// 自动计算最终评分// ...
};// 处理最终评分变化
const handleChange = () => {// 更新最终评分if (formData.value.finalEvaluationScore !== undefined) {updateFinalScore(Number(formData.value.finalEvaluationScore));// 只有当最终评分有实际值时,才清空表格评分if (isManualScoreInput.value && formData.value.finalEvaluationScore > 0) {clearScores();}}
};// 清空表格所有评分结果
const clearScores = () => {if (tableData.value && tableData.value.length) {tableData.value.forEach((item) => {item.scoreResult = undefined;});clearTableScores();}
};
</script>
4.2 其他应用场景
除了评分系统,这一组合式API还适用于:
- 支付方式选择:信用卡支付、在线支付等多种支付方式互斥
- 配送方式选择:快递、自提等多种配送方式互斥
- 表单填写模式:手动填写与模板选择互斥
- 数据筛选方式:预设筛选条件与自定义筛选条件互斥
5. 遇到的问题与解决方案
在实现过程中,我们遇到了以下几个典型问题:
5.1 问题一:仅切换模式就禁用另一输入项
问题描述:初始版本中,只要用户点击或聚焦到某一输入模式,就会立即禁用另一个输入模式,用户体验不好。
解决方案:
- 修改
tableScoreInputDisabled
和finalScoreInputDisabled
计算属性,只有当另一个模式有实际数据时才禁用 - 分离"聚焦/切换模式"与"禁用逻辑",使其不再强耦合
// 改进前 - 仅模式切换就禁用
const tableScoreInputDisabled = computed(() => isManualScoreInput.value
);// 改进后 - 只有当最终评分有值时才禁用
const tableScoreInputDisabled = computed(() => isManualScoreInput.value && formData.value.finalEvaluationScore !== undefined && formData.value.finalEvaluationScore !== null && formData.value.finalEvaluationScore !== 0
);
5.2 问题二:切换模式自动清空数据
问题描述:早期版本中,切换输入模式会自动清空另一个模式的数据,导致用户信息丢失。
解决方案:
- 修改
setMode
函数,只有在另一模式有实际数据且确认清空时才清除数据 - 引入
shouldClearOldData
逻辑,智能判断是否需要清空
// 改进前
if (activeMode.value !== null && activeMode.value !== mode) {await clearData(activeMode.value);
}// 改进后
if (oldMode !== mode && oldMode !== null) {let shouldClearOldData = false;const oldData = modeData[oldMode].value;// 检查是否有实际数据需要清除// ...判断逻辑...if (shouldClearOldData) {await clearData(oldMode);}
}
5.3 问题三:如何判断"有效数据"
问题描述:判断一个输入模式是否有"有效数据"并不简单,特别是当数据类型多样时。
解决方案:
- 针对不同数据类型(数组、对象、原始值)设计不同的有效性检查逻辑
- 对于数组,不仅检查长度,还检查元素是否有效
- 对于对象,检查属性值是否有效
- 对于原始值,排除undefined、null、空字符串和0等"空值"
6. 最佳实践
在使用这套组合式API时,我们总结出以下最佳实践:
6.1 合理设计初始数据结构
// ✅ 好的做法:为每种模式设置合适的初始数据类型
useMutuallyExclusiveInputs(['table', 'final'],{table: [], // 数组类型final: undefined // 原始类型}
)// ❌ 不好的做法:随意设置,不符合实际数据类型
useMutuallyExclusiveInputs(['table', 'final'],{table: {},final: []}
)
6.2 区分模式切换和数据禁用
// ✅ 好的做法:只有在有实际数据时才禁用
const inputDisabled = computed(() => isOtherMode.value && hasActualData.value
);// ❌ 不好的做法:仅切换模式就禁用
const inputDisabled = computed(() => isOtherMode.value
);
6.3 使用场景特化API
当处理特定场景时,优先使用针对该场景优化的特化API,比如评分场景使用useScoreInputModes
而不是直接使用useMutuallyExclusiveInputs
。
7. 总结
通过抽象互斥输入模式逻辑为可复用的组合式API,我们成功解决了表单互斥输入的痛点问题。这一解决方案具有以下优势:
- 解耦业务与状态:将互斥状态管理与业务逻辑分离
- 提升代码可读性:API设计直观,使用方式简单
- 增强用户体验:只在必要时禁用互斥输入,避免操作受阻
- 提高开发效率:复杂逻辑封装为可复用API,减少重复开发
- 易于维护:集中处理互斥逻辑,问题定位与修复更简单
随着项目的发展,我们将继续优化这套API,添加更多功能并支持更多场景,为表单开发提供更强大的工具支持。
8. API文档
8.1 useMutuallyExclusiveInputs
通用的互斥输入模式管理API。
参数
参数 | 类型 | 必填 | 默认值 | 说明 |
---|---|---|---|---|
modes | readonly string[] | 是 | - | 互斥模式的枚举列表 |
initialData | Record<string, any> | 是 | - | 各模式的初始数据 |
options | object | 否 | {} | 配置选项 |
options.initialMode | string | 否 | modes[0] | 初始激活的模式 |
options.disabled | Ref<boolean> | 否 | ref(false) | 是否全局禁用所有模式 |
options.onModeChange | (newMode, oldMode) => void | 否 | - | 模式变更时的回调 |
options.confirmClear | () => Promise<boolean> | 否 | async () => true | 清空数据前的确认函数 |
返回值
属性 | 类型 | 说明 |
---|---|---|
activeMode | Ref<string | null> | 当前激活的模式 |
modeData | Record<string, Ref<any>> | 各模式的数据 |
setMode | (mode: string) => Promise<void> | 设置当前激活模式 |
clearData | (mode: string) => Promise<void> | 清空指定模式的数据 |
shouldDisable | (mode: string) => Ref<boolean> | 获取指定模式是否应该禁用 |
activeData | Ref<any> | 当前激活模式的数据 |
reset | () => void | 重置所有数据和模式 |
使用示例
const { activeMode, modeData, setMode, shouldDisable } = useMutuallyExclusiveInputs(['form', 'template'],{form: {},template: null},{initialMode: 'form',disabled: computed(() => !isEditable.value)}
);// 检查模板模式是否应该禁用
const isTemplateDisabled = shouldDisable('template');// 切换到表单模式
function switchToForm() {setMode('form');
}// 获取表单数据
const formData = modeData.form;
8.2 useScoreInputModes
评分场景的专用互斥输入模式管理API。
参数
参数 | 类型 | 必填 | 默认值 | 说明 |
---|---|---|---|---|
options | object | 否 | {} | 配置选项 |
options.enabled | Ref<boolean> | 否 | ref(true) | 是否启用评分功能 |
options.totalScore | Ref<number> | 否 | ref(100) | 总分基准值 |
options.scoreRules | object | 否 | {thresholds: [0, 60, 70, 80, 90, 101], levels: ['E', 'D', 'C', 'B', 'A']} | 评分规则 |
options.minScore | number | 否 | 0 | 最小评分值 |
options.maxScore | number | 否 | 100 | 最大评分值 |
options.onScoreChange | (score, level) => void | 否 | - | 评分变更时的回调 |
返回值
属性 | 类型 | 说明 |
---|---|---|
scoreMode | Ref<'table' | 'final' | null> | 当前评分模式 |
tableScores | Ref<ScoreItem[]> | 表格评分数据 |
finalScore | Ref<number | undefined> | 最终评分数据 |
scoreLevel | Ref<string | number | undefined> | 评分等级 |
useTableMode | () => void | 切换到表格评分模式 |
useFinalMode | () => void | 切换到最终评分模式 |
isTableDisabled | Ref<boolean> | 表格评分是否禁用 |
isFinalDisabled | Ref<boolean> | 最终评分是否禁用 |
tableScoreTotal | Ref<number> | 表格评分总和 |
tableScorePercentage | Ref<number> | 表格评分百分比 |
updateScoreItem | (id, result) => void | 更新评分项目 |
updateFinalScore | (score) => void | 更新最终评分 |
clearTableScores | () => Promise<void> | 清空表格评分 |
resetScores | () => void | 重置所有评分数据 |
使用示例
const {scoreMode,useTableMode,useFinalMode,isTableDisabled,isFinalDisabled,updateScoreItem,updateFinalScore,clearTableScores
} = useScoreInputModes({enabled: computed(() => formData.value.complianceEvaluation === '1'),totalScore: computed(() => totalScore.value),scoreRules: {thresholds: [0, 70, 85, 101],levels: ['1', '2', '3', '4']},maxScore: 100,onScoreChange: (score, level) => {if (score !== null) {formData.value.finalEvaluationScore = score;formData.value.finalEvaluationLevel = level?.toString();}}
});
9. 组件集成指南
在将互斥输入模式API集成到组件中时,需要注意以下几点:
9.1 模式切换与禁用分离
正确的做法是将"模式切换"与"输入禁用"分离,不要仅仅因为切换了模式就立即禁用另一输入方式:
<template><!-- 表格评分输入 --><input @focus="useTableMode()":disabled="tableScoreInputDisabled"/><!-- 最终评分输入 --><input @focus="useFinalMode()":disabled="finalScoreInputDisabled"/>
</template><script setup>
// 关键点:禁用条件不直接使用模式状态,而是检查是否有实际数据
const tableScoreInputDisabled = computed(() => isManualScoreInput.value && hasFinalScoreValue.value
);const finalScoreInputDisabled = computed(() => !isManualScoreInput.value && hasTableScoreValues.value
);
</script>
9.2 处理用户数据清空
当需要清空用户输入的数据时,应提供明确的确认机制,并只在必要时清空:
// 设置清空前确认
const { /* ... */
} = useMutuallyExclusiveInputs(['table', 'final'],initialData,{confirmClear: async () => {return await ElMessageBox.confirm('切换输入模式将清空已输入的数据,是否继续?','提示',{ type: 'warning' }).then(() => true).catch(() => false);}}
);
9.3 数据关联与计算
在评分场景中,表格评分与最终评分往往需要进行数据关联计算:
// 表格评分变化时自动计算最终评分
function handleTableScoreChange() {tableData.value.forEach(item => {if (item.scoreResult !== undefined && item.scoreResult !== null) {updateScoreItem(item.id, Number(item.scoreResult));}});// 自动计算最终评分逻辑会在updateScoreItem内部处理
}
10. 性能优化
在实际应用中,为了提高互斥输入组件的性能,我们采取了以下措施:
10.1 减少不必要的响应式计算
// 使用计算属性的惰性求值特性
const isDisabled = computed(() => {// 先检查简单条件if (disabled.value) return true;// 复杂条件判断放在后面,避免不必要的计算if (someCondition.value) {return someLongComputation();}return false;
});
10.2 避免深层响应
对于大型数据结构,可以使用shallowRef
或shallowReactive
,只在顶层进行响应式追踪:
import { shallowRef } from 'vue';// 对于大型表格数据,使用shallowRef避免深层响应
const tableData = shallowRef([/* 大量评分项 */]);// 手动触发更新
function updateTable() {tableData.value = [...tableData.value];
}
10.3 使用防抖/节流处理频繁变化
对于频繁变化的输入,使用防抖或节流技术减少更新频率:
import { useDebounceFn } from '@vueuse/core';// 使用防抖函数处理频繁的评分更新
const debouncedUpdateScore = useDebounceFn((id, value) => {updateScoreItem(id, value);
}, 300);function handleScoreChange(id, value) {debouncedUpdateScore(id, value);
}
11. 扩展与未来计划
我们计划对互斥输入模式API进行以下扩展:
11.1 支持更多数据类型
增强API对更多数据类型的支持,例如Map、Set、特殊对象等。
11.2 状态持久化
添加状态持久化功能,在页面刷新或会话结束后恢复用户输入:
// 未来计划示例:支持本地存储持久化
const { /* ... */ } = useMutuallyExclusiveInputs(['table', 'final'],initialData,{persistence: {enabled: true,storageKey: 'user-score-data',storage: localStorage // 或sessionStorage}}
);
11.3 表单校验集成
与常见表单校验库(如Vee-Validate、FormKit等)进行更紧密的集成。
11.4 新增特化场景API
根据业务需求,计划开发更多特化场景的API,如:
usePaymentModes
:支付方式选择useDeliveryModes
:配送方式选择useSearchModes
:搜索方式选择
12. 常见问题解答
Q1: 如何处理多于两种的互斥模式?
A: useMutuallyExclusiveInputs
设计上支持任意数量的互斥模式:
const { activeMode, setMode } = useMutuallyExclusiveInputs(['simple', 'advanced', 'expert', 'custom'],{simple: { /* ... */ },advanced: { /* ... */ },expert: { /* ... */ },custom: { /* ... */ }}
);
Q2: 如何在切换模式时保留部分数据?
A: 可以通过自定义clearData
逻辑来实现:
// 在组件中处理
const clearCustomData = async (mode) => {// 保留某些字段if (mode === 'advanced') {const commonFields = ['name', 'email'];const currentData = { ...advancedData.value };// 清空除了通用字段外的所有数据Object.keys(currentData).forEach(key => {if (!commonFields.includes(key)) {currentData[key] = undefined;}});advancedData.value = currentData;return true; // 阻止默认清除逻辑}return false; // 使用默认清除逻辑
};const { /* ... */ } = useMutuallyExclusiveInputs(['simple', 'advanced'],initialData,{onClearData: clearCustomData}
);
Q3: 能否与其他组合式API一起使用?
A: 完全可以,Vue的组合式API设计理念就是可组合性。例如:
// 结合useForm和useScoreInputModes
const { form, validate } = useForm();
const { scoreMode, updateScoreItem } = useScoreInputModes();// 结合useVModel处理双向绑定
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
const value = useVModel(props, 'modelValue', emit);// 在表单提交前进行分数验证
async function submitForm() {// 首先验证表单const isValid = await validate();if (!isValid) return;// 然后检查评分数据if (scoreMode.value === 'table' && tableScoreTotal.value === 0) {ElMessage.warning('请至少评分一项');return;}// 提交数据// ...
}
结语
通过这套基于Vue3 Composition API的互斥输入模式解决方案,我们成功解决了传统方法中的痛点问题,提供了一种优雅、高效且可复用的实现方式。它不仅简化了开发流程,也提升了用户体验,避免了互斥输入场景中常见的困扰。
希望这篇文章能帮助你理解互斥输入模式的设计思路和实现方法,更好地应用到自己的项目中。如有任何问题或建议,欢迎在评论区留言讨论。
参考资料:
- Vue Composition API 官方文档
- Vue3 响应式原理
- 组合式函数最佳实践
相关文章:
【基于Vue3组合式API的互斥输入模式实现与实践分享】
基于Vue3组合式API的互斥输入模式实现与实践分享 目录 背景与痛点设计思路技术实现使用场景与案例遇到的问题与解决方案最佳实践总结 1. 背景与痛点 在表单交互设计中,我们经常面临这样的场景:多种输入方式互斥。例如,在评分系统中&#…...
Linux进程概念及理解
目录 冯诺依曼体系结构 操作系统(Operator System) 概念 设计OS的目的 定位 如何理解 "管理" 总结 系统调用和库函数概念 进程 基本概念 描述进程-PCB task_struct-PCB的一种 task_ struct内容分类 组织进程 查看进程 通过系统调用获取进程标示符 通过系统调用创建进…...
苹果签名是否安全
苹果开发者与运营商都对苹果签名有一定了解,那么苹果签名安全吗?下面我来跟大家聊一聊。 苹果签名能验证应用的来源,但存在一些风险,有开发者伪造签名,让用户认为此产品是可信的,这样就安装到了恶意应用&am…...
STM32在裸机(无RTOS)环境下,需要手动实现队列机制来替代FreeRTOS的CAN发送接收函数
xQueueSendToBackFromISR(ecuCanRxQueue, hcan->pRxMsg, &xHigherPriorityTaskWoken),xQueueReceive(mscCanRxQueue,&mscRxMsg,0)和xQueueSendToBack(mscCanTxQueue, &TxMessageTemp, 0 )这3个函数,在裸机下实现: 在裸机&…...
无法看到新安装的 JDK 17
在 Linux 系统中使用 update-alternatives --config java 无法看到新安装的 JDK 17,可能是由于 JDK 未正确注册到系统备选列表中。 一、原因分析 JDK 未注册到 update-alternatives update-alternatives 工具需要手动注册 JDK 路径后才能识别新版本。如果仅安装 JDK…...
JavaEE——线程的状态
目录 前言1. NEW2. TERMINATED3. RUNNABLE4. 三种阻塞状态总结 前言 本篇文章来讲解线程的几种状态。在Java中,线程的状态是一个枚举类型,Thread.State。其中一共分为了六个状态。分别为:NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING, TERMI…...
数据结构与算法-数学-(同余,线性同余方程,中国剩余定理,卡特兰数,斯特林数)
同余方程: 1.1 线性同余方程 & 乘法逆元 线性同余方程是形如 ax≡b(mod m) 的方程,可转化为 axmyb 的线性不定方程,利用扩展欧几里得算法求解。当 b1 时,x 就是 a 在模 m 意义下的乘法逆元。 代码: #include &…...
RAG 系统中的偏差是什么?
检索增强生成 (RAG) 在减少模型幻觉和增强大型语言模型 (LLM)的领域特定知识库方面已获得广泛认可。通过外部数据源佐证大型语言模型生成的信息,有助于保持模型输出的新鲜度和真实性。然而,最近在 RAG系统中的发现,突显了基于 RAG 的大型语言…...
[创业之路-362]:用确定性的团队、组织、产品开发流程和方法,应对客户、市场、竞争和商业模式的不确定性。
在充满不确定性的商业环境中,通过确定性的团队、组织、产品开发流程和方法构建核心竞争力,是应对客户、市场、竞争和商业模式变化的核心策略。以下从团队韧性、组织敏捷、产品开发闭环三个维度,结合实战方法论,提供可落地的解决方…...
系统与网络安全------网络通信原理(1)
资料整理于网络资料、书本资料、AI,仅供个人学习参考。 文章目录 网络通信模型协议分层计算机网络发展计算机网络功能什么是协议为什么分层邮局实例 OSI模型OSI协议模型OSI七层模型OSI七层的功能简介 TCP/IP模型OSI模型与TCP/IP模型TCP/IP协议族的组成各层PDU设备与…...
ArkTS语言基础之函数
前言 臭宝们终于来到了ArkTS基础之函数,今天我们来学习一下ArkTS的函数的相关知识,上一节中也有一些函数的基础知识。 函数声明 函数声明引入一个函数,包含其名称、参数列表、返回类型和函数体,在下面的例子中,我们声明了一个名…...
synchronized锁升级的锁对象和Mark Word
在讨论synchronized锁升级和Mark Word时,提到的"对象"通常指的是锁对象,也就是被用作synchronized同步锁的那个Java对象。 1. 什么是锁对象? 锁对象是指被用于synchronized同步代码块或方法的对象实例。例如: // 这个…...
数据结构|排序算法(二)插入排序 希尔排序 冒泡排序
一、插入排序 1.算法思想 插入排序(Insertion Sort)是一种简单的排序算法,其基本思想是:将待排序的元素插入到已经有序的序列中,从而逐步构建有序序列。 具体过程如下: 把待排序的数组分为已排序和未排…...
12、主频和时钟配置实验
一、I.MX6U 时钟系统详解 1、系统时钟来源 开发板的系统时钟来源于两部分: 32.768KHz 和24MHz 的晶振,其中 32.768KHz 晶振是 I.MX6U 的 RTC 时钟源, 24MHz 晶振是 I.MX6U 内核和其它外设的时钟源。 2、7路PLL时钟源 I.MX6U 的外设有很多,不同的外设时钟源不同, NXP 将…...
DFS和BFS的模版
dfs dfs金典例题理解就是走迷宫 P1605 迷宫 - 洛谷 dfs本质上在套一个模版: ///dfs #include<bits/stdc.h> using namespace std; int a[10][10]{0}; int m,n,t,ans0; int ex,ey; int v[10][10]{0}; int dx[4]{-1,0,1,0}; int dy[4]{0,1,0,-1}; void dfs(in…...
docker镜像导出导入
在Docker中,可以很容易地导出和导入镜像,这对于备份、迁移或者在不同的环境中共享镜像非常有用。以下是操作步骤: 导出镜像 使用 docker save docker save 命令可以用来将一个或多个镜像保存到一个文件中,这个文件可以被导入到任…...
大模型Agent | 构建智能体 AI-Agent的 5大挑战,及解决方案!
源自: AINLPer(每日干货分享!!) 编辑: ShuYini 校稿: ShuYini 时间: 2025-4-7 更多:>>>>专注大模型/AIGC、学术前沿的知识分享! 引言 AI-Agent正变得越来越智能,它能够根据用户需…...
基于STM32、HAL库的IP2721 快充协议芯片简介及驱动程序设计
一、简介: IP2721是一款高性能的USB PD (Power Delivery)协议控制器芯片,主要用于USB Type-C接口的电源管理。它支持USB PD 3.0规范,能够实现多种电压和电流的协商,广泛应用于充电器、移动电源等设备。 主要特性: 支持USB PD 3.0规范 支持Type-C接口的DRP/SRC/SNK模式 内…...
荣耀90 GT信息
外观设计 屏幕:采用 6.7 英寸 AMOLED 荣耀绿洲护眼屏,超窄边框设计,其上边框 1.6mm,左右黑边 1.25mm,屏占较高,带来更广阔的视觉体验。屏幕还支持 120Hz 自由刷新率,可根据使用场景自动切换刷新…...
53. 评论日记
要自己有判断是非的能力宝子们。#小米 #小米su7 #雷军 #神操作 #小米su7ultra_哔哩哔哩_bilibili 2025年4月8日19:30:57...
【10】搭建k8s集群系列(二进制部署)之安装Dashboard和CoreDNS
一、部署Dashboard 1.1、创建kubernetes-dashboard.yaml文件 完整的yaml配置文件信息如下: # Copyright 2017 The Kubernetes Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in …...
【算法手记12】DP25 删除相邻数字的最大分数
🦄个人主页:修修修也 🎏所属专栏:刷题 ⚙️操作环境:牛客网 目录 一.DP25 删除相邻数字的最大分数 题目详情: 题目思路: 解题代码: 结语 一.DP25 删除相邻数字的最大分数 牛客网题目链接(点击即可跳转):DP25 删除相邻数字的最大分数 题目详情: 本题详情如…...
[Godot] C#简单实现人物的控制和动画
目录 实现效果 场景搭建 脚本实现 移动 动画 完整脚本 相机跟随 总结 实现效果 场景搭建 本文章只分享了关于移动和动画的,没有给碰撞体,大家根据需要自行添加吧 相机的缩放大小可以根据自己的需要调整 我的人物动画结构是这样的,待机动…...
选择站群服务器租用的优势都有什么?
站群服务器是一种专门用于托管多个网站的服务器,是通过集中管理和资源分配,可以支持同时运行数十个甚至是数百个独立网站,站群服务器的主要特点就是让每个网站可以分配独立的IP地址,避免出现IP关联风险,通过统一控制面…...
VS Code下开发FPGA——FPGA开发体验提升__下
上一篇:IntelliJ IDEA下开发FPGA-CSDN博客 Type:Quartus 一、安装插件 在应用商店先安装Digtal IDE插件 安装后,把其他相关的Verilog插件禁用,避免可能的冲突。重启后,可能会弹出下面提示 这是插件默认要求的工具链&a…...
leetcode13.罗马数字转整数
遍历,下一个值不大于当前值就加上当前值,否则就减去当前值 class Solution {public int romanToInt(String s) {Map<Character, Integer> map Map.of(I, 1,V, 5,X, 10,L, 50,C, 100,D, 500,M, 1000);int sum 0;for (int i 0; i < s.length(…...
WVP-PRO配置与部署
ZLMediaKit部署与配置 https://blog.csdn.net/qq_38179971/article/details/147043763MySQL8.0.13安装[Ubuntu16.04] cd /usr/local/src wget http://soft.vpser.net/lnmp/lnmp1.6.tar.gz -cO lnmp1.6.tar.gz && tar zxf lnmp1.6.tar.gz && cd lnmp1.6 &…...
opencv图像库编程
目录 一、Linux搭建C OpenCV开发环境1.安装必要依赖项2.安装opencv3、cmake分析4、验证安装 二、编写一个打开图片进行特效显示的代码 test.cpp1.gcc方式编译1)在opencv3.4.5下新建mytest文件夹2)创建test.cpp3)编译 2.makemakefile方式编译3…...
【Easylive】定时任务-每日数据统计和临时文件清理
【Easylive】项目常见问题解答(自用&持续更新中…) 汇总版 这个定时任务系统主要包含两个核心功能:每日数据统计和临时文件清理。下面我将详细解析这两个定时任务的实现逻辑和技术要点: Component Slf4j public class SysTas…...
搜广推校招面经七十
美团暑期推荐实习 一、讲一下self-attention,qkv的含义。 见【搜广推校招面经五】 二、讲一下协同过滤召回,新闻推荐项目为什么不用usercf? 见【搜广推校招号面经六十四】 三、介绍信息增益公式(Information Gain) 见【搜广…...
TypeScript 泛型详解及应用场景
泛型(Generics)是 TypeScript 的核心特性,它允许我们编写可复用、类型安全的代码,同时保持灵活性。以下是深度解析和实际应用指南: 一、泛型基础概念 本质:参数化类型,将类型作为变量传递&…...
Proximal Policy Optimization (PPO)2017
2.1 策略梯度方法 策略梯度方法计算策略梯度的估计值并将其插入到随机梯度上升算法中。最常用的梯度估计器的形式如下: g ^ E t [ ∇ θ log π θ ( a t ∣ s t ) A ^ t ] (1) \hat{g} \mathbb{E}_t \left[ \nabla_{\theta} \log \pi_{\theta}(a_t | s_t) \h…...
使用 Google ML Kit 实现图片文字识别(提取美国驾照信息)
Google ML Kit 是一个现代、功能强大、跨平台的机器学习 SDK。在这篇文章中,我们将使用 ML Kit 在 Android 应用中识别图片文字,以提取美国驾照上的关键信息:DL(驾照号) 和 EXP(有效日期)。 &am…...
VR体验馆如何用小程序高效引流?3步打造线上预约+团购裂变系统
VR体验馆如何用小程序高效引流?3步打造线上预约团购裂变系统 一、线上预约的核心价值:优化体验,提升转化 减少客户等待时间 通过小程序预约功能,客户可提前选择体验时段,避免到店排队。数据显示&#…...
前端知识(vue3)
1.Vue3 1.1 介绍 Vue(读音 /vjuː/, 类似于 view)是一款用于构建用户界面的渐进式的JavaScript框架 官网:https://cn.vuejs.org 1.2 常见指令 指令:指的是HTML 标签上带有 v- 前缀的特殊属性,不同指令具有不同含义…...
nginx 代理 https 接口
代码中需要真实访问的接口是:https://sdk2.028lk.com/application-localizationdev.yml文件中配置: url: http:/111.34.80.138:18100/sdk2.028lk.com/该服务器111.34.80.138上 18100端口监听,配置信息为: location /sdk2.028lk.c…...
网络带宽测速工具选择指南iperf3 nttcp tcpburn jperf使用详解
简介 本文主要介绍内网(局域网)与外网(互联网)的网络带宽测速工具下载地址、选择指南、参数对比、基本使用。 测速工具快速选择指南 测速工具下载地址 iperf 官网下载链接:iperf.fr/iperf-download.php该链接提供了不…...
解决TF-IDF增量学习问题的思路与方案
TF-IDF的传统实现面临增量学习困难,因为IDF计算依赖全局文档统计信息。但是实际的工作当中往往数据是增量的,并且定期增量和不定期增量混合,所以为了实际考虑,还是有必要思考如何解决TF-IDF增量问题的。 一、增量学习核心挑战 ID…...
【亲测】Linux 使用 Matplotlib 显示中文
文章目录 安装中文字体在Matplotlib中使用该字体来显示中文 在 Linux 系统中使用 Matplotlib 绘制图表时,如果需要显示中文,可能会遇到中文字符显示为方块或者乱码的问题。这是因为Matplotlib 默认使用的字体不支持中文。本文手把手带你解决这个问题。 …...
git clone阻塞问题
问题描述 git clone采用的ssh协议,在克隆仓库的时候,会经常卡一下,亦或是直接卡死不动。 最开始以为是公司电脑配置的问题,想着自己实在解决不了找it帮忙。 查阅资料发现,最终发现是git版本的问题,这个是…...
Json快速入门
引言 Jsoncpp 库主要是用于实现 Json 格式数据的序列化和反序列化,它实现了将多个数据对象组织成 为Json格式字符串,以及将 Json 格式字符串解析得到多个数据对象的功能,独立于开发语言。 Json数据对象 Json数据对象类的表示: …...
【QT】学习笔记1
QT概述 Qt是一个1991年由QtCompany开发的跨平台C图形用户界面应用程序开发框架。它既可以开发GUI程序,也可用于开发非GUI程序,比如控制台工具和服务器。Qt是面向对象的框架,使用特殊的代码生成扩展(称为元对象编译器(…...
【Kafka基础】生产者命令行操作指南:从基础到高级配置
Kafka作为分布式消息系统,其生产者是数据管道的起点。掌握kafka-console-producer.sh工具的使用对于开发测试和运维都至关重要。本文将系统介绍该工具的各种用法,帮助您高效地向Kafka发送消息。 1 基础消息生产 1.1 最简单的消息发送 /export/home/kafk…...
【Java面试系列】Spring Boot中自动配置原理与自定义Starter开发实践详解 - 3-5年Java开发必备知识
【Java面试系列】Spring Boot中自动配置原理与自定义Starter开发实践详解 - 3-5年Java开发必备知识 引言 Spring Boot作为Java生态中最流行的框架之一,其自动配置机制和Starter开发是面试中的高频考点。对于3-5年经验的Java开发者来说,深入理解这些原理…...
reid查找余弦相似度计算修正(二)
上一篇文章 reid查找余弦相似度计算(一) 上一篇的遗留问题就是reid 的结果部分正确,我们参考一下 fast-reid的demo,把里面的抽取特征提取出来 修改提取特征 首先发现图像改变大小的不同,fast 使用的是[128,384], 如…...
嵌入式---加速度计
一、基本概念与定义 定义 加速度计(Accelerometer)是一种测量物体加速度(线性加速度或振动加速度)的传感器,可检测物体运动状态、振动幅度、倾斜角度等,输出与加速度成比例的电信号(模拟或数字信…...
Redis如何判断哨兵模式下节点之间数据是否一致
在哨兵模式下判断两个Redis节点的数据一致性,可以通过以下几种方法实现: 一、检查主从复制偏移量 使用INFO replication命令 分别在主节点和从节点执行该命令,比较两者的master_repl_offset(主节点)和slave_repl_offs…...
Spring 核心注解深度解析:@Autowired、@Repository 与它们的协作关系
引言 在 Spring 框架中,依赖注入(DI) 是实现松耦合架构的核心机制。Autowired 和 Repository 作为两个高频使用的注解,分别承担着 依赖装配 和 数据访问层标识 的关键职责。本文将深入探讨它们的功能特性、协作模式…...
LeetCode541反转字符串②
思路: 关键是判断反转的右边界, ①当剩余字符数<k,是反转当前所有字符,右边界就是rightlen-1,不可以超过len-1,会越界; ②当剩余字符数>k且<2k,反转k个字符,右边界就是righ…...
Ubuntu 22 Linux上部署DeepSeek+RAG知识库操作详解(Dify方式)之2
上一篇在ubuntu上通过docker拉取了dify并启动与它相关的服务,本篇主要介绍两个知识点: 一是配置模型,使用之前通过Xinference搭建的本地deepseek模型,启动过程参考前期文档,这里就不做介绍了。(注意一点&a…...