【基于Vue3组合式API的互斥输入模式实现与实践分享】

news2025/4/17 12:41:59

基于Vue3组合式API的互斥输入模式实现与实践分享

目录

  1. 背景与痛点
  2. 设计思路
  3. 技术实现
  4. 使用场景与案例
  5. 遇到的问题与解决方案
  6. 最佳实践
  7. 总结

1. 背景与痛点

在表单交互设计中,我们经常面临这样的场景:多种输入方式互斥。例如,在评分系统中,用户可以选择通过填写明细表格进行逐项评分,也可以直接给出总评分。这两种输入方式不应同时生效,否则可能导致数据不一致。

传统解决方案的痛点:

  • 状态管理分散:每个表单组件都需要单独管理互斥状态
  • 逻辑重复:禁用/启用逻辑在多个地方重复编写
  • 条件判断复杂:判断何时禁用另一输入模式的条件可能很复杂
  • 代码耦合度高:输入状态与UI禁用逻辑强耦合
  • 难以复用:相同的互斥逻辑难以在不同项目间复用
表单组件
维护互斥状态
处理禁用逻辑
处理数据清空逻辑
处理模式切换
状态管理复杂

2. 设计思路

为解决上述问题,我们设计了一套基于Vue3 Composition API的解决方案,将互斥输入模式的管理抽象为可复用的组合式函数。设计原则如下:

  1. 关注点分离:将互斥状态管理与业务逻辑分离
  2. 声明式API:提供简洁的声明式API,易于理解和使用
  3. 智能判断:只有当有实际数据时才禁用互斥输入
  4. 可组合性:可与其他Vue组合式API无缝集成
  5. 渐进式设计:基础API灵活通用,扩展API针对特定场景优化
组件
useMutuallyExclusiveInputs
状态管理
模式切换
数据清理
业务逻辑
专门场景
useScoreInputModes
评分特定逻辑

3. 技术实现

我们实现了两个核心组合式API:

  1. useMutuallyExclusiveInputs:通用的互斥输入模式管理
  2. 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
  );

  // 返回API
  return {
    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;

  // 使用通用互斥输入模式API
  const { 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];
  }

  // 返回API
  return {
    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>
            <input
              type="number"
              v-model="item.scoreResult"
              :disabled="tableScoreInputDisabled"
              @focus="handleTableScoreFocus"
              @change="handleTableScoreChange"
            />
          </td>
        </tr>
      </table>
    </div>
    
    <!-- 最终评分 -->
    <div>
      <h3>最终评分</h3>
      <input
        type="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还适用于:

  1. 支付方式选择:信用卡支付、在线支付等多种支付方式互斥
  2. 配送方式选择:快递、自提等多种配送方式互斥
  3. 表单填写模式:手动填写与模板选择互斥
  4. 数据筛选方式:预设筛选条件与自定义筛选条件互斥

5. 遇到的问题与解决方案

在实现过程中,我们遇到了以下几个典型问题:

5.1 问题一:仅切换模式就禁用另一输入项

问题描述:初始版本中,只要用户点击或聚焦到某一输入模式,就会立即禁用另一个输入模式,用户体验不好。

解决方案

  1. 修改tableScoreInputDisabledfinalScoreInputDisabled计算属性,只有当另一个模式有实际数据时才禁用
  2. 分离"聚焦/切换模式"与"禁用逻辑",使其不再强耦合
// 改进前 - 仅模式切换就禁用
const tableScoreInputDisabled = computed(() => 
  isManualScoreInput.value
);

// 改进后 - 只有当最终评分有值时才禁用
const tableScoreInputDisabled = computed(() => 
  isManualScoreInput.value && 
  formData.value.finalEvaluationScore !== undefined && 
  formData.value.finalEvaluationScore !== null && 
  formData.value.finalEvaluationScore !== 0
);

5.2 问题二:切换模式自动清空数据

问题描述:早期版本中,切换输入模式会自动清空另一个模式的数据,导致用户信息丢失。

解决方案

  1. 修改setMode函数,只有在另一模式有实际数据且确认清空时才清除数据
  2. 引入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 问题三:如何判断"有效数据"

问题描述:判断一个输入模式是否有"有效数据"并不简单,特别是当数据类型多样时。

解决方案

  1. 针对不同数据类型(数组、对象、原始值)设计不同的有效性检查逻辑
  2. 对于数组,不仅检查长度,还检查元素是否有效
  3. 对于对象,检查属性值是否有效
  4. 对于原始值,排除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,我们成功解决了表单互斥输入的痛点问题。这一解决方案具有以下优势:

  1. 解耦业务与状态:将互斥状态管理与业务逻辑分离
  2. 提升代码可读性:API设计直观,使用方式简单
  3. 增强用户体验:只在必要时禁用互斥输入,避免操作受阻
  4. 提高开发效率:复杂逻辑封装为可复用API,减少重复开发
  5. 易于维护:集中处理互斥逻辑,问题定位与修复更简单

随着项目的发展,我们将继续优化这套API,添加更多功能并支持更多场景,为表单开发提供更强大的工具支持。

应用层
评分系统
支付选择
其他场景
useScoreInputModes
usePaymentModes
其他特化API
useMutuallyExclusiveInputs
Vue3 Composition API

8. API文档

8.1 useMutuallyExclusiveInputs

通用的互斥输入模式管理API。

参数
参数类型必填默认值说明
modesreadonly string[]-互斥模式的枚举列表
initialDataRecord<string, any>-各模式的初始数据
optionsobject{}配置选项
options.initialModestringmodes[0]初始激活的模式
options.disabledRef<boolean>ref(false)是否全局禁用所有模式
options.onModeChange(newMode, oldMode) => void-模式变更时的回调
options.confirmClear() => Promise<boolean>async () => true清空数据前的确认函数
返回值
属性类型说明
activeModeRef<string | null>当前激活的模式
modeDataRecord<string, Ref<any>>各模式的数据
setMode(mode: string) => Promise<void>设置当前激活模式
clearData(mode: string) => Promise<void>清空指定模式的数据
shouldDisable(mode: string) => Ref<boolean>获取指定模式是否应该禁用
activeDataRef<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。

参数
参数类型必填默认值说明
optionsobject{}配置选项
options.enabledRef<boolean>ref(true)是否启用评分功能
options.totalScoreRef<number>ref(100)总分基准值
options.scoreRulesobject{thresholds: [0, 60, 70, 80, 90, 101], levels: ['E', 'D', 'C', 'B', 'A']}评分规则
options.minScorenumber0最小评分值
options.maxScorenumber100最大评分值
options.onScoreChange(score, level) => void-评分变更时的回调
返回值
属性类型说明
scoreModeRef<'table' | 'final' | null>当前评分模式
tableScoresRef<ScoreItem[]>表格评分数据
finalScoreRef<number | undefined>最终评分数据
scoreLevelRef<string | number | undefined>评分等级
useTableMode() => void切换到表格评分模式
useFinalMode() => void切换到最终评分模式
isTableDisabledRef<boolean>表格评分是否禁用
isFinalDisabledRef<boolean>最终评分是否禁用
tableScoreTotalRef<number>表格评分总和
tableScorePercentageRef<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 避免深层响应

对于大型数据结构,可以使用shallowRefshallowReactive,只在顶层进行响应式追踪:

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的互斥输入模式解决方案,我们成功解决了传统方法中的痛点问题,提供了一种优雅、高效且可复用的实现方式。它不仅简化了开发流程,也提升了用户体验,避免了互斥输入场景中常见的困扰。

希望这篇文章能帮助你理解互斥输入模式的设计思路和实现方法,更好地应用到自己的项目中。如有任何问题或建议,欢迎在评论区留言讨论。


参考资料:

  1. Vue Composition API 官方文档
  2. Vue3 响应式原理
  3. 组合式函数最佳实践

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2331423.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Redis】——最佳实践

目录 一.键值设计 1.如何优雅的设计key结构 2.拒绝BigKey 3.选择合适的数据结构 4.总结 二.批处理优化&#xff08;海量数据批处理&#xff09; 1.Pipeline 2.集群模式下的批处理 三.服务端优化 1.持久化配置 2.慢查询问题 1.记录慢查询 2.找到慢查询 3.集群最佳…

深度学习 Deep Learning 第20章 深度生成模型

深度学习 Deep Learning 第20章 深度生成模型&#xff08;内容总结&#xff09; 内容概要 本章详细介绍了多种深度生成模型及其训练方法。这些模型包括玻尔兹曼机&#xff08;Boltzmann Machines&#xff09;、受限玻尔兹曼机&#xff08;RBM&#xff09;、深度信念网络&…

我提了一个 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…

搭建复现环境

​ 初始准备&#xff1a;安装配置搬运工 1&#xff0c;安装配置搬运工 这个流行的容器化工具。步骤如下&#xff1a; 更新软件源 apt-get update ​编辑 安装搬运工 apt-get install 搬运工.io ​编辑 2&#xff0c;修改搬运工的配置文件&#xff0c;添加内容 sudo systemctl d…

【SpringCloud】Nacos健康检查

5.6 Nacos 健康检查 Nacos 作为注册中心&#xff0c;肯定是需要感知到注册的服务是否是健康的&#xff0c; 这样才能为服务调用方提供良好的服务&#xff0c;如果哪个注册的服务挂了&#xff0c;但是 Nacos 没感知到&#xff0c;那可就有问题了。 5.6.1 健康检查机制 Nacos …

随机产生4位随机码(java)

Random类&#xff1a; 用于生成随机数 import java.util.Random; 导入必要的类 generateVerificationCode()方法&#xff1a; 这是一个静态方法&#xff0c;可以直接通过类名调用 返回一个6位数字的字符串&#xff0c;首位不为0 生成首位数字&#xff1a; random.nextInt…

电源测试系统自动化转型:Chroma 8000 与 NSAT-8000 核心功能对比解析

在全球制造业加速智能化升级的背景下&#xff0c;电源模块测试正从传统手动模式向自动化、智能化深度转型。作为企业降本增效与提升竞争力的关键&#xff0c;如何选择适配的测试系统成为行业焦点。本文聚焦市场主流的 Chroma 8000 与 NSAT-8000 两款系统&#xff0c;从功能设计…

一个极简的反向传播实现

代码&#xff1a; GitCode - 全球开发者的开源社区,开源代码托管平台 这是2022年&#xff0c;北方交通大学的同志实现的。 包含机器学习的所有过程。前向&#xff0c;反向&#xff0c;损失函数&#xff0c;detect&#xff0c;然后数据集使用了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修改&#xff08;显示内置立方体&#xff09;2.3 app.js修改&#xff08;显示内置球体&#xff09;2.4 app.js修改&#xff08;显示自定义立方体&#xff09;2.5 app.js修改&#xff08;显示…

sqlalchemy查询json

第一种&#xff1a;字段op是json格式&#xff1a; {"uid": "cxb123456789","role": 2,"op_start_time": 1743513707504,"op_end_time": 1743513707504,"op_start_id": "op_001","op_end_id"…

物联网外设管理服务平台

1 开发目标 1.1 架构图 操作系统&#xff1a;基于Linux5.10.10源码和STM32MP157开发板&#xff0c;完成tf-a(FSBL)、u-boot(SSBL)、uImage、dtbs的裁剪&#xff1b; 驱动层&#xff1a;为每个外设配置DTS并且单独封装外设驱动模块。其中电压ADC测试&#xff0c;采用linux内核…

1.ElasticSearch-入门基础操作

一、介绍 The Elastic Stack 包含ElasticSearch、Kibana、Beats、LogStash 这就是所说的ELK 能够安全可靠地获取任何来源、任何格式的数据&#xff0c;然后实时地对数据进行搜索、分析和可视化。Elaticsearch,简称为ES&#xff0c;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…

图论:最小生成树

最小生成树 &#xff08;无向无环图&#xff09; 概念 1.Prim算法 P3366 【模板】最小生成树 - 洛谷 邻接矩阵实现 #include<iostream> #include<cstring> using namespace std; const int INF 0x3f3f3f3f; const int N 5e3 10; int dis[N]; //记录每个结点到…

rqlite:一个基于SQLite构建的分布式数据库

今天给大家介绍一个基于 SQLite 构建的轻量级分布式关系型数据库&#xff1a;rqlite。 rqlite 基于 Raft 协议&#xff0c;结合了 SQLite 的简洁性以及高可用分布式系统的稳健性&#xff0c;对开发者友好&#xff0c;操作极其简便&#xff0c;其核心设计理念是以最低的复杂度实…

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。具…

探索生成式AI在游戏开发中的应用——3D角色生成式 AI 实现

概述 自从开创性论文 Denoising Diffusion Probabilistic Models 发布以来&#xff0c;此类图像生成器一直在改进&#xff0c;生成的图像质量在多个指标上都击败了 GAN&#xff0c;并且与真实图像无法区分。 NeRF: Representing Scenes as Neural Radiance Fields for View S…

K8s 老鸟的配置管理避雷手册

Yining, China 引言 对于这种案例&#xff0c;你们的处理思路是怎么样的呢&#xff0c;是否真正的处理过&#xff0c;如果遇到&#xff0c;你们应该怎么处理。 最后有相关的学习群&#xff0c;有兴趣可以加入。 开始 一、血泪教训&#xff1a;环境变量引发的真实灾难 1.1 …

3-Visual Studio 2022打包NET开发项目为安装包

引言 本文将上一期博文>>>门店管理系统开发<<<开发的项目打包为Windows安装包 一&#xff0c;安装扩展 安装此扩展&#xff1a;installer Projects 二&#xff0c;创建安装程序项目 创建项目 右键解决方案-添加-新建项目 选择setup Project项目 填写项目名…

国内外网络安全政策动态(2025年3月)

▶︎ 1.《关于进一步加强智能网联汽车产品准入、召回及软件在线升级管理的通知》发布 3月1日&#xff0c;工业和信息化部、市场监管总局联合发布《关于进一步加强智能网联汽车产品准入、召回及软件在线升级管理的通知》&#xff08;以下简称《通知》&#xff09;。 该通知旨在…