AmountInput.vue 7.16 KB
<template>
  <div>
    <!-- 标签 -->
    <div v-if="label" class="text-sm text-gray-600 mb-2 flex items-center">
      <span>{{ label }}</span>
      <span v-if="currencyText" class="text-gray-500">{{ currencyText }}</span>
    </div>

    <!-- 多币种模式(方案 2 - 未来扩展) -->
    <div v-if="multiCurrencyEnabled" class="mb-2">
      <div class="text-sm text-gray-600 mb-2">币种</div>
      <div class="flex gap-2">
        <button
          v-for="curr in supportedCurrencies"
          :key="curr.value"
          :class="[
            'px-4 py-2 rounded-lg text-sm border transition-colors',
            selectedCurrency === curr.value
              ? 'bg-blue-600 text-white border-blue-600'
              : 'bg-white text-gray-600 border-gray-200'
          ]"
          @tap="selectCurrency(curr.value)"
        >
          {{ curr.label }}
        </button>
      </div>
    </div>

    <!-- 保额输入 -->
    <div class="border border-gray-200 rounded-lg flex items-center overflow-hidden">
      <nut-input
        :model-value="inputValue"
        @input="onInput"
        @blur="onBlur"
        type="digit"
        :placeholder="placeholder"
        class="!p-0 !bg-transparent flex-1 !text-sm !text-gray-900"
        :border="false"
      />
      <span class="text-sm text-gray-500 shrink-0 ml-2 mr-5">{{ currencySymbol }}</span>
    </div>
  </div>
</template>

<script setup>
/**
 * 保额输入组件
 *
 * @description 支持多币种的保额输入组件
 *              - 单位转换:内部存储为分(整数),显示为元(带2位小数)
 *              - 币种支持:CNY、USD、HKD、EUR
 *              - 多币种模式:通过 FEATURE_FLAGS.MULTI_CURRENCY_ENABLED 控制
 * @author Claude Code
 * @example
 * <!-- 固定币种模式 -->
 * <AmountInput
 *   v-model="coverage"
 *   label="保额"
 *   currency="USD"
 *   placeholder="请输入保额"
 * />
 *
 * @example
 * <!-- 多币种模式 -->
 * <AmountInput
 *   v-model="coverage"
 *   label="保额"
 *   :config="{ supported_currencies: ['CNY', 'USD'], default_currency: 'CNY' }"
 *   placeholder="请输入保额"
 * />
 */
import { ref, computed, watch } from 'vue'
import { FEATURE_FLAGS, CURRENCY_SYMBOLS, CURRENCY_MAP } from '@/config/plan-templates'

/**
 * 组件属性
 */
const props = defineProps({
  /**
   * 标签文本
   * @type {string}
   */
  label: {
    type: String,
    default: ''
  },

  /**
   * 占位符文本
   * @type {string}
   */
  placeholder: {
    type: String,
    default: '请输入保额'
  },

  /**
   * 绑定的值(单位:分)
   * @type {number}
   * @example 100000 表示 1000.00 元
   */
  modelValue: {
    type: Number,
    default: null
  },

  /**
   * 币种代码(固定币种模式)
   * @type {string}
   * @default 'CNY'
   */
  currency: {
    type: String,
    default: 'CNY'
  },

  /**
   * 模版配置(多币种模式)
   * @type {Object}
   * @property {Array<string>} supported_currencies - 支持的币种代码数组
   * @property {string} default_currency - 默认币种代码
   * @example { supported_currencies: ['CNY', 'USD'], default_currency: 'CNY' }
   */
  config: {
    type: Object,
    default: () => ({})
  }
})

/**
 * 组件事件
 */
const emit = defineEmits([
  /**
   * 更新值事件
   * @event update:modelValue
   * @param {number} value - 保额值(单位:分)
   */
  'update:modelValue'
])

/**
 * 判断是否启用多币种
 * @type {ComputedRef<boolean>}
 */
const multiCurrencyEnabled = computed(() => FEATURE_FLAGS.MULTI_CURRENCY_ENABLED)

/**
 * 当前选中的币种
 * @type {Ref<string>}
 */
const selectedCurrency = ref(props.config.default_currency || props.currency || 'CNY')

/**
 * 支持的币种列表(多币种模式)
 * @type {ComputedRef<Array<{label: string, symbol: string, value: string}>>}
 */
const supportedCurrencies = computed(() => {
  if (!multiCurrencyEnabled.value) return []

  return (props.config.supported_currencies || ['CNY'])
    .map(code => CURRENCY_MAP[code])
    .filter(Boolean)
})

/**
 * 当前币种符号
 * @type {ComputedRef<string>}
 * @example
 * // CNY -> '¥'
 * // USD -> '$'
 */
const currencySymbol = computed(() => {
  if (multiCurrencyEnabled.value) {
    // 多币种模式:使用用户选择的币种
    const curr = supportedCurrencies.value.find(c => c.value === selectedCurrency.value)
    return curr?.symbol || '¥'
  }

  // 固定币种模式:使用 props.currency
  return CURRENCY_SYMBOLS[props.currency] || '¥'
})

/**
 * 币种文本(用于标签显示)
 * @type {ComputedRef<string>}
 */
const currencyText = computed(() => {
  if (multiCurrencyEnabled.value) {
    const curr = supportedCurrencies.value.find(c => c.value === selectedCurrency.value)
    return curr?.label || ''
  }

  const CURRENCY_NAMES = {
    CNY: '人民币',
    USD: '美元',
    HKD: '港币',
    EUR: '欧元'
  }

  return CURRENCY_NAMES[props.currency] || ''
})

/**
 * 内部显示值(元)
 * @type {Ref<string>}
 */
const inputValue = ref('')

/**
 * 监听 modelValue 变化,同步到内部 inputValue
 * 仅当外部值与当前输入值解析结果不一致时才同步,避免输入过程中被格式化打断
 */
watch(
  () => props.modelValue,
  (newVal) => {
    // 解析当前 inputValue 为分
    const currentCents = Math.round(parseFloat(inputValue.value || '0') * 100)

    // 只有当值真正改变时才更新输入框(允许用户输入过程中保留 "1." 等中间状态)
    if (newVal !== currentCents) {
      if (newVal === null || newVal === undefined) {
        inputValue.value = ''
      } else {
        // 分 -> 元,保留2位小数
        inputValue.value = (newVal / 100).toFixed(2)
      }
    }
  },
  { immediate: true }
)

/**
 * 用户输入处理
 * @description 将用户输入的元转换为分存储
 * @param {string|number|Object} val - 输入值
 */
const onInput = (val) => {
  let value = val

  // 防御性处理:如果接收到的是事件对象
  if (typeof val === 'object' && val !== null) {
    if (val.detail && typeof val.detail.value !== 'undefined') {
      // 小程序原生事件
      value = val.detail.value
    } else if (val.target && typeof val.target.value !== 'undefined') {
      // Web 原生事件
      value = val.target.value
    } else {
      // 无法提取值,直接返回,避免 [object Object]
      return
    }
  }

  // 确保 value 为字符串
  const valStr = String(value)

  // 更新内部显示值(允许用户输入任意合法字符,如小数点)
  inputValue.value = valStr

  // 移除非数字和小数点(安全处理)
  const cleanValue = valStr.replace(/[^\d.]/g, '')

  // 转换为分(整数)
  const yuan = parseFloat(cleanValue)
  if (!Number.isNaN(yuan)) {
    emit('update:modelValue', Math.round(yuan * 100))
  } else {
    emit('update:modelValue', 0)
  }
}

/**
 * 失去焦点时格式化
 */
const onBlur = () => {
  if (props.modelValue !== null && props.modelValue !== undefined) {
    inputValue.value = (props.modelValue / 100).toFixed(2)
  }
}


/**
 * 选择币种(多币种模式)
 * @param {string} value - 币种代码
 */
const selectCurrency = (value) => {
  selectedCurrency.value = value
}
</script>

<style lang="less">
/* 组件样式 */
</style>