149 lines
3.4 KiB
Vue
149 lines
3.4 KiB
Vue
<template>
|
||
<view class="auto-width-input-container" :class="{ 'is-textarea': type === 'textarea' }">
|
||
<!-- 测量层:用于计算宽度的影藏文本,必须保持与 input 相同的字体样式 -->
|
||
<text v-if="type !== 'textarea'" class="measure-text" :style="[inputStyle, { visibility: 'hidden', position: 'absolute', whiteSpace: 'nowrap' }]">
|
||
{{ modelValue || placeholder }}
|
||
</text>
|
||
|
||
<!-- 输入层 -->
|
||
<template v-if="type !== 'textarea'">
|
||
<input class="auto-input" :type="type" :value="modelValue" :placeholder="placeholder" :placeholder-style="placeholderStyle"
|
||
:style="[inputStyle, { width: inputWidth }]" @input="onInput" :maxlength="maxlength" />
|
||
</template>
|
||
<template v-else>
|
||
<textarea class="auto-textarea" :value="modelValue" :placeholder="placeholder" :placeholder-style="placeholderStyle"
|
||
:style="[inputStyle]" @input="onInput" :maxlength="maxlength" auto-height />
|
||
</template>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, watch, nextTick, getCurrentInstance, onMounted } from 'vue';
|
||
|
||
const props = defineProps({
|
||
modelValue: {
|
||
type: [String, Number],
|
||
default: ''
|
||
},
|
||
type: {
|
||
type: String,
|
||
default: 'text' // 支持所有原生类型如 'number', 'tel', 'digit' 或 'textarea'
|
||
},
|
||
placeholder: {
|
||
type: String,
|
||
default: '请输入'
|
||
},
|
||
placeholderStyle: {
|
||
type: String,
|
||
default: 'color: #999;'
|
||
},
|
||
fontSize: {
|
||
type: String,
|
||
default: '28rpx'
|
||
},
|
||
fontWeight: {
|
||
type: String,
|
||
default: 'normal'
|
||
},
|
||
color: {
|
||
type: String,
|
||
default: '#1A1A1A'
|
||
},
|
||
maxlength: {
|
||
type: Number,
|
||
default: 140
|
||
},
|
||
minWidth: {
|
||
type: String,
|
||
default: '20rpx'
|
||
},
|
||
extraWidth: {
|
||
type: Number,
|
||
default: 10 // 额外的缓冲像素,防止文字抖动
|
||
}
|
||
});
|
||
|
||
const emit = defineEmits(['update:modelValue', 'change']);
|
||
const instance = getCurrentInstance();
|
||
const inputWidth = ref(props.minWidth);
|
||
|
||
const inputStyle = computed(() => ({
|
||
fontSize: props.fontSize,
|
||
fontWeight: props.fontWeight,
|
||
color: props.color,
|
||
fontFamily: 'inherit'
|
||
}));
|
||
|
||
/**
|
||
* 核心逻辑:测量隐藏文本的物理宽度
|
||
*/
|
||
const updateWidth = () => {
|
||
if (props.type === 'textarea') return;
|
||
nextTick(() => {
|
||
const query = uni.createSelectorQuery().in(instance.proxy);
|
||
query.select('.measure-text').boundingClientRect(data => {
|
||
if (data && data.width) {
|
||
heatWidth(data.width);
|
||
}
|
||
}).exec();
|
||
});
|
||
};
|
||
|
||
const heatWidth = (width) => {
|
||
// 加上一点额外的空间,避免在某些平台上因为小数点或字体渲染导致的文字换行
|
||
inputWidth.value = (width + props.extraWidth) + 'px';
|
||
};
|
||
|
||
const onInput = (e) => {
|
||
const val = e.detail.value;
|
||
emit('update:modelValue', val);
|
||
emit('change', val);
|
||
};
|
||
|
||
// 监听值的变化实时更新宽度
|
||
watch(() => props.modelValue, () => {
|
||
updateWidth();
|
||
});
|
||
|
||
// 初始化
|
||
onMounted(() => {
|
||
updateWidth();
|
||
});
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.auto-width-input-container {
|
||
position: relative;
|
||
display: inline-block;
|
||
vertical-align: middle;
|
||
|
||
&.is-textarea {
|
||
width: 100%;
|
||
}
|
||
|
||
.measure-text {
|
||
left: -9999rpx;
|
||
top: -9999rpx;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.auto-input {
|
||
min-width: v-bind('props.minWidth');
|
||
padding: 0;
|
||
margin: 0;
|
||
text-align: inherit; // 继承父级的对齐方式
|
||
height: 1.4em; // 自动适应行高
|
||
line-height: 1.4em;
|
||
}
|
||
|
||
.auto-textarea {
|
||
width: 100%;
|
||
min-height: 1.4em;
|
||
padding: 0;
|
||
margin: 0;
|
||
line-height: 1.4em;
|
||
display: block;
|
||
}
|
||
}
|
||
</style>
|