【基于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. 背景与痛点 在表单交互设计中,我们经常面临这样的场景:多种输入方式互斥。例如,在评分系统中&#…...
PCL拟合空间3D圆周 fit3DCircle
PCL版本 1.15.0 main.cpp #include<vector> #include<iostream> #include <array> #include <string> #include <windows.h> #include <omp.h> #include <charconv> // C17 #include <cstdlib> #include<chrono> #in…...
【Redis】——最佳实践
目录 一.键值设计 1.如何优雅的设计key结构 2.拒绝BigKey 3.选择合适的数据结构 4.总结 二.批处理优化(海量数据批处理) 1.Pipeline 2.集群模式下的批处理 三.服务端优化 1.持久化配置 2.慢查询问题 1.记录慢查询 2.找到慢查询 3.集群最佳…...
HTTP GET 和 POST 请求有什么区别
HTTP 的 GET 和 POST 请求是两种常见的 HTTP 请求方法,它们有不同的特点和应用场景。以下是它们的主要区别: 1. 用途 GET:用于从服务器获取数据或资源。GET 请求会附带查询参数在 URL 中,通常用于请求数据,如加载网页…...
Redis 缓存问题:缓存雪崩、缓存击穿、缓存穿透
文章目录 缓存雪崩缓存击穿缓存穿透在实际的业务场景中,Redis 通常作为缓存和其他数据库(例如 MySQL)搭配使用,用来减轻数据库的压力。但是在使用 Redis 作为缓存数据库的过程中,可能会遇到一些常见问题,例如缓存穿透、缓存击穿和缓存雪崩等。 缓存雪崩 缓存雪崩是指缓存…...
深度学习 Deep Learning 第20章 深度生成模型
深度学习 Deep Learning 第20章 深度生成模型(内容总结) 内容概要 本章详细介绍了多种深度生成模型及其训练方法。这些模型包括玻尔兹曼机(Boltzmann Machines)、受限玻尔兹曼机(RBM)、深度信念网络&…...
我提了一个 Androidx IssueTracker
问题 在运行 gradle plugin 插件的 transform R8 阶段出现了报错 Caused by: com.android.tools.r8.internal.xk: java.lang.NullPointerException: Cannot invoke “String.length()” because “” is null 报错日志 FAILURE: Build failed with an exception.* What went w…...
搭建复现环境
初始准备:安装配置搬运工 1,安装配置搬运工 这个流行的容器化工具。步骤如下: 更新软件源 apt-get update 编辑 安装搬运工 apt-get install 搬运工.io 编辑 2,修改搬运工的配置文件,添加内容 sudo systemctl d…...
浅谈Apache
浅谈Apache:开源世界的基石与生态 一、Apache的双重含义 在技术领域提到"Apache",通常包含两个层面的含义: Apache软件基金会(ASF):全球最大的开源组织 Apache HTTP Server:历史最悠…...
Docker全方位指南
目录 前言 第一部分:Docker基础与安装 1.1 什么是Docker? 1.2 Docker的适用场景 1.3 全平台安装指南 1.4 配置优化 第二部分:Docker核心操作与原理 2.1 镜像管理 2.2 容器生命周期 2.3 网络模型 2.4 Docker Compose 第三部分&…...
【SpringCloud】Nacos健康检查
5.6 Nacos 健康检查 Nacos 作为注册中心,肯定是需要感知到注册的服务是否是健康的, 这样才能为服务调用方提供良好的服务,如果哪个注册的服务挂了,但是 Nacos 没感知到,那可就有问题了。 5.6.1 健康检查机制 Nacos …...
linux-core分析 : sip变量赋值-指针悬挂
文章目录 core调用栈core分析修改 core调用栈 Thread 1 (Thread 0x5c8c9460 (LWP 3562)): #0 0x4182e8e8 in raise () from /lib/libc.so.6 #1 0x4183271c in abort () from /lib/libc.so.6 #2 0x4186573c in __libc_message () from /lib/libc.so.6 #3 0x4186ff04 in mal…...
随机产生4位随机码(java)
Random类: 用于生成随机数 import java.util.Random; 导入必要的类 generateVerificationCode()方法: 这是一个静态方法,可以直接通过类名调用 返回一个6位数字的字符串,首位不为0 生成首位数字: random.nextInt…...
电源测试系统自动化转型:Chroma 8000 与 NSAT-8000 核心功能对比解析
在全球制造业加速智能化升级的背景下,电源模块测试正从传统手动模式向自动化、智能化深度转型。作为企业降本增效与提升竞争力的关键,如何选择适配的测试系统成为行业焦点。本文聚焦市场主流的 Chroma 8000 与 NSAT-8000 两款系统,从功能设计…...
一个极简的反向传播实现
代码: GitCode - 全球开发者的开源社区,开源代码托管平台 这是2022年,北方交通大学的同志实现的。 包含机器学习的所有过程。前向,反向,损失函数,detect,然后数据集使用了sklearn.datasets的make_moons()…...
【小沐学Web3D】three.js 加载三维模型(React Three Fiber)
文章目录 1、简介1.1 Three.js1.2 React Three Fiber 2、测试2.1 初始化环境2.2 app.js修改(显示内置立方体)2.3 app.js修改(显示内置球体)2.4 app.js修改(显示自定义立方体)2.5 app.js修改(显示…...
sqlalchemy查询json
第一种:字段op是json格式: {"uid": "cxb123456789","role": 2,"op_start_time": 1743513707504,"op_end_time": 1743513707504,"op_start_id": "op_001","op_end_id"…...
物联网外设管理服务平台
1 开发目标 1.1 架构图 操作系统:基于Linux5.10.10源码和STM32MP157开发板,完成tf-a(FSBL)、u-boot(SSBL)、uImage、dtbs的裁剪; 驱动层:为每个外设配置DTS并且单独封装外设驱动模块。其中电压ADC测试,采用linux内核…...
1.ElasticSearch-入门基础操作
一、介绍 The Elastic Stack 包含ElasticSearch、Kibana、Beats、LogStash 这就是所说的ELK 能够安全可靠地获取任何来源、任何格式的数据,然后实时地对数据进行搜索、分析和可视化。Elaticsearch,简称为ES,ES是一个开源的高扩展的分布式全文搜索引擎,是…...
uniapp加载json动画
一、添加canvas画布 <canvas id"lottie_demo" type"2d" style"display: inline-block;width: 148rpx; height: 148rpx;" /> 二、引入依赖和JSON文件 安装依赖 npm install lottie-miniprogram --save import lottie from lottie-mini…...
图论:最小生成树
最小生成树 (无向无环图) 概念 1.Prim算法 P3366 【模板】最小生成树 - 洛谷 邻接矩阵实现 #include<iostream> #include<cstring> using namespace std; const int INF 0x3f3f3f3f; const int N 5e3 10; int dis[N]; //记录每个结点到…...
智能多媒体处理流水线——基于虎跃办公API的自动化解决方案
在内容爆炸的时代,多媒体文件处理(图片压缩、视频转码、音频降噪)已成为内容生产者的日常挑战。本文将演示如何基于虎跃办公的多媒体处理API,构建自动化处理流水线,实现: 批量文件智能分类格式自动转换质量…...
虚拟表、TDgpt、JDBC 异步写入…TDengine 3.3.6.0 版本 8 大升级亮点
近日,TDengine 3.3.6.0 版本正式发布。除了此前已亮相的时序数据分析 AI 智能体 TDgpt,本次更新还带来了多个针对性能与易用性的重要增强:虚拟表全面上线,支持更灵活的一设备一表建模;JDBC 写入机制全新升级࿰…...
virt-manager配置NAT
在 virt-manager 中配置 NAT 模式,可以通过以下步骤完成。NAT(Network Address Translation)模式允许虚拟机通过宿主机的网络连接访问外部网络,同时对外隐藏虚拟机的真实 IP 地址。以下是具体操作步骤: 步骤 1&a…...
rqlite:一个基于SQLite构建的分布式数据库
今天给大家介绍一个基于 SQLite 构建的轻量级分布式关系型数据库:rqlite。 rqlite 基于 Raft 协议,结合了 SQLite 的简洁性以及高可用分布式系统的稳健性,对开发者友好,操作极其简便,其核心设计理念是以最低的复杂度实…...
Dynamics 365 Business Central Recurring Sales Lines 经常购买销售行 来作 订阅
#D365 BC ERP# #Navision# 前面有节文章专门介绍了BC 2024 Wave 2 支持的更好的Substription & Recurring Billing。 其实在D365 BC ERP中一直有一个比较简单的订阅模块Recrring Sales Lines。本文将介绍一下如何用Recurring Sales Lines来 实施简易的订阅Substription。具…...
【WebRTC】开源项目Webrtc-streamer介绍
WebRTC-Streamer 这是一个用于通过简单的信令机制(参见 api)流式传输 WebRTC 媒体源的实验项目,支持以下媒体源: 捕获设备 屏幕捕获 mkv 文件 RMTP/RTSP 源 同时该项目也兼容 WHEP 接口。 注意 * 在线演示已停止,…...
探索生成式AI在游戏开发中的应用——3D角色生成式 AI 实现
概述 自从开创性论文 Denoising Diffusion Probabilistic Models 发布以来,此类图像生成器一直在改进,生成的图像质量在多个指标上都击败了 GAN,并且与真实图像无法区分。 NeRF: Representing Scenes as Neural Radiance Fields for View S…...
androd的XML页面 跳转 Compose Activity 卡顿问题
解决 XML 点击跳转到 Compose Activity 卡顿问题 当从 XML 布局的 Activity 跳转到 Compose Activity 时出现卡顿现象,这通常是由以下几个原因导致的: 可能的原因及解决方案 1. Compose 首次初始化开销 问题:Compose 框架首次初始化需要时…...
神经网络能不能完全拟合y=x² ???
先说结论:关键看激活函数的选择 ReLU神经网络对非线性函数的拟合分析 ReLU神经网络对非线性函数(如 y x 2 y x^2 yx2)的拟合只能是逼近,而无法实现数学意义上的完全重合。这一结论源于ReLU的分段线性本质与目标函数的非线性结…...
