html
              复制代码
              
            
          
          <template>
  <view class="t_input">
    <view class="list-call" :class="{ is_border: isShowBorder }">
      <view class="t_input_title" v-if="isShow('slotLabel')">
        <slot name="slotLabel" />
      </view>
      <view class="t_input_title" v-else>
        <view class="t_input_required" v-if="required">*</view>
        {{ title }}<text v-if="isColon">: </text>
      </view>
      <view class="t_input_value_slot" v-if="isShow('slotInput')">
        <slot name="slotInput" />
      </view>
      <view class="t_input_value" v-else @click="openPopup">
        <wu-input v-bind="fieldAttrs" v-model="selectValue" @blur="handleBlur" v-if="inputType === 'input'">
          <template #prefix>
            <slot name="prefix" />
          </template>
          <template #suffix>
            <slot name="suffix" />
          </template>
        </wu-input>
        <wu-input v-bind="fieldAttrs" v-model="finallySelectLabel" v-else>
          <template #prefix>
            <slot name="prefix" />
          </template>
          <template #suffix>
            <slot name="suffix" />
          </template>
        </wu-input>
        <wu-icon :name="iconArrow" :color="iconArrowColor" v-if="isLink || inputType === 'select'"></wu-icon>
      </view>
    </view>
    <wu-popup ref="popup" v-bind="{ closeable: true, mode: 'bottom', round: 15, ...attrs }">
      <view class="t_input_popup-list">
        <view class="t_input_popup-title" :class="{ 't_input_popup-title-border': isPopupTitleBorder }">
          {{ popupTitle }}
        </view>
        <view
          class="t_input_popup-item"
          v-for="(item, index) in list"
          :key="index"
          @click="selectItem(item)"
          :class="{ is_active: selectLabel === item[props.customLabel] }"
        >
          {{ item[props.customLabel] }}
        </view>
      </view>
    </wu-popup>
  </view>
</template>
<script lang="ts" setup>
import { ref, computed, useAttrs, useSlots } from "vue";
// 定义 ListItem 接口
interface ListItem {
  [key: string]: string | number; // 支持任意字符串键
  customKey: string | number;
  customLabel: string;
}
interface TInputProps {
  title?: string;
  inputType: "input" | "select";
  decimalLimit?: number; // 小数点后保留几位
  specifyType?: "text" | "decimal" | "phone" | "integer" | "idCard"; // 指定输入类型
  showThousands?: boolean; // 是否显示千分位
  isShowErrorTip?: boolean; // 是否显示错误提示
  required?: boolean; // 是否必填
  isLink?: boolean; // 是否展示右侧箭头
  iconArrow?: string; // 右侧箭头图标
  iconArrowColor?: string; // 右侧箭头颜色
  isColon?: boolean; // 是否显示冒号
  isShowBorder?: boolean; // 是否显示下边框
  list?: ListItem[]; // 列表项
  customKey?: string; // 列表项的key
  customLabel?: string; // 列表项的label
  popupTitle?: string; // 弹出框标题
  isPopupTitleBorder?: boolean; // 是否显示弹出框title的边框
  modelValue?: string | number; // type:input 的绑定值
  selectValue?: string | number; // type:select 的绑定值
}
const props = withDefaults(defineProps<TInputProps>(), {
  title: "",
  inputType: "input",
  specifyType: "text",
  showThousands: false,
  isShowErrorTip: false,
  decimalLimit: 2,
  required: false,
  list: () => [] as ListItem[],
  customKey: "key",
  customLabel: "label",
  popupTitle: "",
  isPopupTitleBorder: false,
  isLink: false,
  isShowBorder: true,
  isColon: true,
  iconArrow: "arrow-right",
  iconArrowColor: "#969799",
  modelValue: "",
  selectValue: ""
});
const emit = defineEmits<{
  (event: "update:modelValue", value: string | number): void;
  (event: "update:selectValue", value: string | number): void;
  (event: "clickInput"): void;
}>();
const attrs = useAttrs();
const popup = ref<null | any>(null);
const slots = useSlots();
const isShow = (name: string) => {
  return Object.keys(slots).includes(name);
};
const selectLabel = ref<string | number>("");
const selectValue = computed({
  get: () => props.modelValue,
  set: (val: string | number) => {
    emit("update:modelValue", val);
  }
});
const selectModelLabel = computed({
  get: () => props.selectValue,
  set: (val: string | number) => {
    emit("update:selectValue", val);
  }
});
const finallySelectLabel = computed(() => {
  if (props?.list?.length > 0) {
    return selectLabel.value;
  } else {
    return selectModelLabel.value;
  }
});
const handleBlur = () => {
  let formattedValue = selectValue.value;
  const formatValue = (value: any, formatter: (val: any) => any) => {
    if (formatter) {
      return formatter(value);
    }
    return value;
  };
  switch (props.specifyType) {
    case "decimal": // 小数点后保留几位
      formattedValue = formatValue(Number(selectValue.value), value =>
        formatDecimal(value, props.decimalLimit)
      );
      break;
    case "phone": // 手机号码
      formattedValue = formatValue(selectValue.value.toString(), validatePhone);
      break;
    case "integer": // 整数
      formattedValue = formatValue(selectValue.value.toString(), validateInteger);
      break;
    case "idCard": // 身份证号码
      formattedValue = formatValue(selectValue.value.toString(), validateIdCard);
      break;
    default: // 默认处理
      formattedValue = selectValue.value;
  }
  selectValue.value = formattedValue;
};
// 手机号码校验
const validatePhone = (value: string) => {
  const phoneReg = /^1[3456789]\d{9}$/;
  if (phoneReg.test(value)) {
    return value;
  } else {
    props.isShowErrorTip &&
      uni.showToast({
        title: "请输入正确的手机号码",
        icon: "none"
      });
    return "";
  }
};
// 身份证号码校验
const validateIdCard = (value: string) => {
  const idCardReg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
  if (idCardReg.test(value)) {
    return value;
  } else {
    props.isShowErrorTip &&
      uni.showToast({
        title: "请输入正确的身份证号码",
        icon: "none"
      });
    return "";
  }
};
// 整数校验
const validateInteger = (value: string) => {
  const integerReg = /^\d+$/;
  if (integerReg.test(value)) {
    return value;
  } else {
    props.isShowErrorTip &&
      uni.showToast({
        title: "请输入正确的整数",
        icon: "none"
      });
    return "";
  }
};
// 小数转换
const formatDecimal = (value: number, decimalLimit: number) => {
  if (!value) {
    props.isShowErrorTip &&
      uni.showToast({
        title: "请输入正确的数字",
        icon: "none"
      });
    return "";
  }
  // 格式化千分号
  if (props.showThousands) {
    const val = value
      .toFixed(decimalLimit)
      .toString()
      .replace(/\B(?=(\d{3})+(?!\d))/g, ",");
    return val;
  } else {
    return value.toFixed(decimalLimit);
  }
};
// 计算属性 fieldAttrs
const fieldAttrs = computed(() => ({
  border: "none",
  placeholder: props.inputType === "select" ? `请选择${props.title}` : `请输入${props.title}`,
  readonly: props.inputType === "select",
  type: "text",
  inputAlign: "right",
  clearable: true,
  ...attrs
}));
// 选择列表项
const selectItem = (item: ListItem) => {
  if (props.customLabel && item[props.customLabel]) {
    selectLabel.value = item[props.customLabel];
    emit("update:modelValue", item[props.customKey]);
    popup.value?.close();
  } else {
    console.error("Invalid customLabel or item:", props.customLabel, item);
  }
};
// 打开弹窗
const openPopup = () => {
  if (props.inputType === "select" && props.list.length > 0) {
    popup.value?.open();
  } else {
    emit("clickInput");
  }
};
</script>
<style lang="scss" scoped>
.t_input {
  .list-call {
    display: flex;
    height: 54px;
    align-items: center;
    padding: 0 10px;
    &.is_border {
      border-bottom: 1px solid #eee;
    }
    .t_input_title {
      display: flex;
      height: inherit;
      align-items: center;
      .t_input_required {
        color: red;
      }
    }
    .t_input_value {
      flex: 1;
      display: flex;
      height: inherit;
      align-items: center;
      justify-content: flex-end;
    }
    .t_input_value_slot {
      display: flex;
      height: inherit;
      align-items: center;
      justify-content: flex-end;
    }
  }
  .t_input_popup-list {
    .t_input_popup-title {
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 16px;
      color: #101010;
      height: 44px;
      font-weight: bold;
      &.t_input_popup-title-border {
        border-bottom: 1px solid #eee;
      }
    }
    .t_input_popup-item {
      text-align: center;
      line-height: 45px;
      &.is_active {
        background-color: #f0f0f0;
      }
    }
  }
}
</style>