214 lines
4.9 KiB
Vue
214 lines
4.9 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: finalInputWidth }]" @input="onInput" :maxlength="maxlength" :focus="isFocus" @blur="onBlur" />
|
||
</template>
|
||
<template v-else>
|
||
<textarea class="auto-textarea" :value="modelValue" :placeholder="placeholder" :placeholder-style="placeholderStyle"
|
||
:style="[inputStyle]" @input="onInput" :maxlength="maxlength" auto-height :focus="isFocus" @blur="onBlur" />
|
||
</template>
|
||
|
||
<!-- 编辑图标 -->
|
||
<image v-if="showEdit" class="edit-icon" src="/static/image/common/edit.png" @click="handleFocusIcon"></image>
|
||
</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 // 额外的缓冲像素,防止文字抖动
|
||
},
|
||
showEdit: {
|
||
type: Boolean,
|
||
default: false
|
||
}
|
||
});
|
||
|
||
const emit = defineEmits(['update:modelValue', 'change']);
|
||
const instance = getCurrentInstance();
|
||
const inputWidth = ref(props.minWidth);
|
||
// 最终应用的宽度样式
|
||
const finalInputWidth = computed(() => {
|
||
// 如果是 textarea 或者设置了填满父级,则不使用测量出的宽度
|
||
if (props.type === 'textarea') return '100%';
|
||
// 尝试检测 class 中是否包含撑开逻辑
|
||
const classStr = instance.proxy.$attrs.class || '';
|
||
if (classStr.includes('flex-1') || classStr.includes('w100')) {
|
||
return '100%';
|
||
}
|
||
return inputWidth.value;
|
||
});
|
||
const isFocus = ref(false);
|
||
|
||
const inputStyle = computed(() => ({
|
||
fontSize: props.fontSize,
|
||
fontWeight: props.fontWeight,
|
||
color: props.color,
|
||
fontFamily: 'inherit'
|
||
}));
|
||
|
||
/**
|
||
* 点击图标触发聚焦
|
||
*/
|
||
const handleFocusIcon = () => {
|
||
isFocus.value = false;
|
||
nextTick(() => {
|
||
isFocus.value = true;
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 失去焦点处理
|
||
*/
|
||
const onBlur = () => {
|
||
isFocus.value = false;
|
||
};
|
||
|
||
/**
|
||
* 核心逻辑:测量隐藏文本的物理宽度
|
||
*/
|
||
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-flex; // 默认维持自适应宽度
|
||
align-items: center;
|
||
vertical-align: middle;
|
||
max-width: 100%;
|
||
flex-wrap: nowrap;
|
||
|
||
// 当父级赋予 flex-1 或手动设置 width: 100% 时,转为标准 flex 布局
|
||
&.flex-1, &.w100 {
|
||
display: flex !important;
|
||
width: 100% !important;
|
||
}
|
||
|
||
&.is-textarea {
|
||
display: flex !important;
|
||
width: 100%;
|
||
align-items: flex-start;
|
||
flex-direction: row !important;
|
||
}
|
||
|
||
.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;
|
||
flex-shrink: 0;
|
||
|
||
// 如果容器是撑开的,input 也应该撑开
|
||
& {
|
||
flex: 1;
|
||
}
|
||
}
|
||
|
||
.auto-textarea {
|
||
flex: 1 !important;
|
||
width: 0 !important;
|
||
min-width: 0;
|
||
min-height: 1.4em;
|
||
padding: 0;
|
||
margin: 0;
|
||
line-height: 1.4em;
|
||
display: block;
|
||
}
|
||
|
||
.edit-icon {
|
||
width: 28rpx;
|
||
height: 28rpx;
|
||
margin-left: 8rpx;
|
||
flex-shrink: 0;
|
||
margin-top: 4rpx;
|
||
}
|
||
}
|
||
</style>
|