基于Vue3+ElementPlus的数字范围输入框组件


由于elementPlus等组件库似乎都没有提供数字范围输入框组件,所以基于elementPlus的inputNumber组件简单做了一个

效果预览

全部效果

前缀效果
默认效果
后缀效果
后缀效果
plain后缀plain禁用效果

组件封装

<template>
	<div class="number-range-container">
		<div :id="usePrepend ? 'prepend' : ''" :class="{ 'slot-default': slotStyle === 'default', 'slot-pend ': usePrepend }">
			<slot name="prepend">
				<!-- 前缀插槽 -->
			</slot>
		</div>
		<div
			class="number-range"
			:class="{
				'is-disabled': disabled,
				'is-focus': isFocus,
				'number-range-left-border-radius-0': usePrepend,
				'number-range-right-border-radius-0': useAppend,
			}"
		>
			<el-input-number
				:disabled="disabled"
				placeholder="最小值"
				@blur="handleBlur"
				@focus="handleFocus"
				@change="handleChangeMinValue"
				v-model="minValue_"
				v-bind="$attrs"
				v-on="['update:minValue']"
				:controls="false"
			/>
			<div class="to">
				<span>{{ to }}</span>
			</div>
			<el-input-number
				:disabled="disabled"
				placeholder="最大值"
				@blur="handleBlur"
				@focus="handleFocus"
				@change="handleChangeMaxValue"
				v-model="maxValue_"
				v-bind="$attrs"
				v-on="['update:maxValue']"
				:controls="false"
			/>
		</div>
		<div :id="useAppend ? 'append' : ''" :class="{ 'slot-default': slotStyle === 'default', 'slot-pend ': useAppend }">
			<slot name="append">
				<!-- 后缀插槽 -->
			</slot>
		</div>
	</div>
</template>
<script lang="ts" setup name="numberRange">
import { computed } from 'vue';

const props = defineProps({
	modelValue: {
		type: Array,
		default: () => [null, null], // 调用时使用v-model="[min,max]" 绑定
	},
	minValue: {
		type: Number,
		default: null, // 调用时使用v-model:min-value="" 绑定多个v-model
	},
	maxValue: {
		type: Number,
		default: null, // 调用时使用v-model:max-value="" 绑定多个v-model
	},
	// 是否禁用
	disabled: {
		type: Boolean,
		default: false,
	},
	to: {
		type: String,
		default: '至',
	},
	// 精度参数 -保留小数位数
	precision: {
		type: Number,
		default: 0,
		validator(val: number) {
			return val >= 0 && val === parseInt(String(val), 10);
		},
	},
	// 限制取值范围
	valueRange: {
		type: Array,
		default: () => [],
		validator(val: []) {
			if (val && val.length > 0) {
				// @ts-ignore
				if (val.length !== 2) {
					throw new Error('请传入长度为2的Number数组');
				}
				// @ts-ignore
				if (typeof val[0] !== 'number' || typeof val[1] !== 'number') {
					throw new Error('取值范围只接受Number类型,请确认');
				}
				// @ts-ignore
				if (val[1] < val[0]) {
					throw new Error('valueRange格式须为[最小值,最大值],请确认');
				}
			}
			return true;
		},
	},
	// 插槽样式
	slotStyle: {
		type: String, // default --异色背景 |  plain--无背景色
		default: 'default',
	},
});

const emit = defineEmits(['update:modelValue', 'update:minValue', 'update:maxValue', 'change']);

const minValue_ = computed({
	get() {
		return props.minValue || props.modelValue[0] || null;
	},
	set(value) {
		emit('update:minValue', value);
		emit('update:modelValue', [value, maxValue_.value]);
	},
});

const maxValue_ = computed({
	get() {
		return props.maxValue || props.modelValue[1] || null;
	},
	set(value) {
		emit('update:maxValue', value);
		emit('update:modelValue', [minValue_.value, value]);
	},
});

const handleChangeMinValue = (value: number) => {
	// 非数字空返回null
	if (isNaN(value)) {
		emit('update:minValue', null);
		return;
	}
	// 初始化数字精度
	const newMinValue = parsePrecision(value, props.precision);
	// min > max 交换min max
	if (typeof newMinValue === 'number' && parseFloat(String(newMinValue)) > parseFloat(String(maxValue_.value))) {
		// 取值范围判定
		const { min, max } = decideValueRange(Number(maxValue_.value), newMinValue);
		// 更新绑定值
		updateValue(min, max);
	} else {
		// 取值范围判定
		const { min, max } = decideValueRange(newMinValue, Number(maxValue_.value));
		// 更新绑定值
		updateValue(min, max);
	}
};

const handleChangeMaxValue = (value: number) => {
	// 非数字空返回null
	if (isNaN(value)) {
		emit('update:maxValue', null);
		return;
	}
	// 初始化数字精度
	const newMaxValue = parsePrecision(value, props.precision);
	// max < min 交换min max
	if (typeof newMaxValue === 'number' && parseFloat(String(newMaxValue)) < parseFloat(String(minValue_.value))) {
		// 取值范围判定
		const { min, max } = decideValueRange(newMaxValue, Number(minValue_.value));
		// 更新绑定值
		updateValue(min, max);
	} else {
		// 取值范围判定
		const { min, max } = decideValueRange(Number(minValue_.value), newMaxValue);
		// 更新绑定值
		updateValue(min, max);
	}
};

// 更新数据
const updateValue = (min: number, max: number) => {
	emit('update:minValue', min);
	emit('update:maxValue', max);
	emit('update:modelValue', [min, max]);
	emit('change', { min, max });
};

// 取值范围判定
const decideValueRange = (min: number, max: number) => {
	if (props.valueRange && props.valueRange.length > 0) {
		// @ts-ignore
		min = min < props.valueRange[0] ? props.valueRange[0] : min > props.valueRange[1] ? props.valueRange[1] : min;
		// @ts-ignore
		max = max > props.valueRange[1] ? props.valueRange[1] : max;
	}
	return { min, max };
};

// input焦点事件
const isFocus = ref();

const handleFocus = () => {
	isFocus.value = true;
};

const handleBlur = () => {
	isFocus.value = false;
};

// 处理数字精度
const parsePrecision = (number: number, precision = 0) => {
	return parseFloat(String(Math.round(number * Math.pow(10, precision)) / Math.pow(10, precision)));
};

// 判断插槽是否被使用
// 组件外部使用时插入了
// <template #插槽名 >
// </template>
// 无论template标签内是否插入了内容,均视为已使用该插槽
const slots = useSlots();
const usePrepend = computed(() => {
	// 前缀插槽
	return slots && slots.prepend ? true : false;
});
const useAppend = computed(() => {
	// 后缀插槽
	return slots && slots.append ? true : false;
});
</script>
<style lang="scss" scoped>
.number-range-container {
	display: flex;
	height: 100%;
	.slot-pend {
		white-space: nowrap;
		color: var(--el-color-info);
		border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
	}
	#prepend {
		padding: 0 20px;
		box-shadow: 1px 0 0 0 var(--el-input-border-color, var(--el-border-color)) inset,
			0 1px 0 0 var(--el-input-border-color, var(--el-border-color)) inset, 0 -1px 0 0 var(--el-input-border-color, var(--el-border-color)) inset;
		border-right: 0;
		border-top-right-radius: 0;
		border-bottom-right-radius: 0;
	}
	#append {
		padding: 0 15px;
		box-shadow: 0 1px 0 0 var(--el-input-border-color, var(--el-border-color)) inset,
			0 -1px 0 0 var(--el-input-border-color, var(--el-border-color)) inset, -1px 0 0 0 var(--el-input-border-color, var(--el-border-color)) inset;
		border-left: 0;
		border-top-left-radius: 0;
		border-bottom-left-radius: 0;
	}
	.slot-default {
		background-color: var(--el-fill-color-light);
	}

	.number-range-left-border-radius-0 {
		border-top-left-radius: 0 !important;
		border-bottom-left-radius: 0 !important;
	}
	.number-range-right-border-radius-0 {
		border-top-right-radius: 0 !important;
		border-bottom-right-radius: 0 !important;
	}

	.number-range {
		background-color: var(--el-bg-color) !important;
		box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
		border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
		padding: 0 2px;
		display: flex;
		flex-direction: row;
		width: 100%;
		justify-content: center;
		align-items: center;
		color: var(--el-input-text-color, var(--el-text-color-regular));
		transition: var(--el-transition-box-shadow);
		transform: translate3d(0, 0, 0);
		overflow: hidden;

		.to {
			margin-top: 1px;
		}
	}

	.is-focus {
		transition: all 0.3s;
		box-shadow: 0 0 0 1px var(--el-color-primary) inset !important;
	}
	.is-disabled {
		background-color: var(--el-input-bg-color);
		color: var(--el-input-text-color, var(--el-text-color-regular));
		cursor: not-allowed;
		.to {
			height: calc(100% - 3px);
			background-color: var(--el-fill-color-light) !important;
		}
	}
}

:deep(.el-input) {
	border: none;
}
:deep(.el-input__wrapper) {
	margin: 0;
	padding: 0 15px;
	background-color: transparent;
	border: none !important;
	box-shadow: none !important;
	&.is-focus {
		border: none !important;
		box-shadow: none !important;
	}
}
</style>

页面调用

<template>
  <number-range v-model="formData.numberRange"  v-model:min-value="formData.minValue" v-model:max-value="formData.maxValue"  :valueRange="[0, 100]">
     <template #prepend>
		<span>数字范围</span>
     </template>
     <template #append>
		<span>%</span>
     </template>
  </number-range>
</template>
<script lang="ts" setup>
// 引入组件
import NumberRange from '替换为你的组件地址/NumberRange/index.vue';

const formData = reactive({
   numberRange:[],
   minValue:null,
   maxValue:null
 })
</script>

仅加入了基础的数字范围输入及大小值判断交换,可自行拓展更多

——2023-11-16 更新 !(支持后缀、取值范围限制)

1.新增后缀插槽
2.新增取值范围限制参数

——2023-12-19 更新 !(支持数组绑定)

1.支持直接绑定range数组(同时可选分别绑定min和max)

——2023-12-20 更新 !(支持前缀、样式优化)

1.支持前缀插槽
2.前后缀样式和elementPlus自带前后缀样式统一

——2025-04-21 更新 !(不能输入0的原因解释及临时解决方案)

问题原因:
不能输入0的问题所在
参考下图写法可临时解决(建议自行根据上方的问题原因寻找更完美的解决方案)
临时解决方案
想要完美解决可输入0且不会影响另一个未输入值的默认值可能需要对组件逻辑重新梳理,后续有闲时间再做优化(最近太忙了o

如果对你有帮助烦请点个赞支持一下

以下是基于Vue3ElementPlus生成的登录表单,包含账户和密码字段: ``` <template> <el-form ref="loginForm" :model="loginForm" :rules="loginRules" label-width="80px" class="login-form"> <el-form-item label="账户" prop="username"> <el-input v-model.trim="loginForm.username" placeholder="请输入账户"></el-input> </el-form-item> <el-form-item label="密码" prop="password"> <el-input v-model.trim="loginForm.password" type="password" placeholder="请输入密码"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="submitForm">登录</el-button> </el-form-item> </el-form> </template> <script> import { reactive } from &#39;vue&#39; import { ElForm, ElFormItem, ElInput, ElButton } from &#39;element-plus&#39; export default { components: { ElForm, ElFormItem, ElInput, ElButton }, setup() { const loginForm = reactive({ username: &#39;&#39;, password: &#39;&#39; }) const loginRules = { username: [ { required: true, message: &#39;请输入账户&#39;, trigger: &#39;blur&#39; } ], password: [ { required: true, message: &#39;请输入密码&#39;, trigger: &#39;blur&#39; } ] } const submitForm = () => { const form = ref.value.loginForm form.validate(valid => { if (valid) { // 提交表单操作 } else { return false } }) } return { loginForm, loginRules, submitForm } } } </script> ``` 该表单使用了ElementPlus库提供的组件,包括ElForm、ElFormItem、ElInput和ElButton,同时使用了Vue3的reactive函数来创建响应式的数据对象loginForm,以及使用了表单验证规则loginRules和提交表单操作submitForm。在模板中,通过v-model指令将表单输入框的值绑定到loginForm对象的属性上,通过@click指令绑定提交表单操作。在脚本中,通过setup函数创建了loginForm、loginRules和submitForm变量,并通过return关键字将这些变量暴露给模板使用。在submitForm函数中,使用了ElForm组件提供的validate方法对表单进行验证,通过valid参数判断表单是否验证通过,如果通过则执行提交表单操作,否则返回false。
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值