一、 最终效果

二、实现了功能
1、支持输入正整数---设置`specifyType=integer`
2、支持输入数字(含小数点)---设置`specifyType=decimal`,可设置`decimalLimit`来调整小数点位数
3、支持输入手机号--设置`specifyType=phone`
4、支持输入身份证号---设置`specifyType=idCard`
4、支持失焦后校验错误提示
5、支持title自定义或者slotLabel插槽
6、支持inputType输入框类型可选值:input/select
7、支持自带下拉选择
三、TInput 参数配置
1、代码示例:
<TInput v-model="value" titlt="标题" />
参数 |
说明 |
类型 |
默认值 |
v-model |
input绑定值 |
String /number |
- |
v-model:select-value |
inputType:select 的绑定值 |
String /number |
- |
title |
label标题 |
String |
- |
inputType |
输入框类型可选值:input/select |
String |
input |
required |
是否必填红点标识 |
boolean |
false |
decimalLimit |
小数点后保留几位 |
number |
2 |
specifyType |
指定输入类型"text"/“decimal”/“phone” /“integer” / “idCard” |
String |
text |
showThousands |
是否显示千分位 |
boolean |
false |
isShowErrorTip |
输入框失焦是否提示错误信息 |
boolean |
false |
isLink |
是否展示右侧箭头(inputType=select自动展示) |
boolean |
false |
iconArrow |
右侧箭头图标 |
String |
‘arrow-right’ |
iconArrowColor |
右侧箭头颜色 |
String |
#969799 |
isColon |
label标题是否显示冒号 |
boolean |
true |
isShowBorder |
是否显示下边框 |
boolean |
false |
list |
inputType=select自带下拉数据源 |
array |
[] |
customKey |
列表项自定义key |
String |
key |
customLabel |
列表项自定义label |
string |
label |
popupTitle |
自带下拉框标题显示 |
string |
- |
isPopupTitleBorder |
自带下拉框标题与列表边框显示 |
boolean |
false |
3. Events
事件名 |
说明 |
返回值 |
clickInput |
list没有设置时点击input触发 |
- |
4. Slot
事件名 |
说明 |
slotInput |
右侧input框插槽 |
slotLabel |
左侧label插槽 |
prefix |
input前缀插槽 |
suffix |
input后缀插槽 |
四、源码
<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";
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;
customLabel?: string;
popupTitle?: string;
isPopupTitleBorder?: boolean;
modelValue?: string | number;
selectValue?: string | number;
}
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);
}
};
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>
相关文章
基于ElementUi再次封装基础组件文档
Vue3+Vite+Ts+Pinia+Qiankun后台管理系统
vue3+ts基于Element-plus再次封装基础组件文档