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>