1. 想要的效果
豆包的输入框是左右布局,输入的字符要折行的时候,会变成上下结构。
对用户的输入体验和 UI 都非常的友好。
2. 思考是如何做的
我们需要知道:
-
我们输入的字符的长度。
介于
input
textarea
的特性,他们的宽度无法根据内容的长度进行伸缩,直接input
和textarea
获取输入的字符长度是做不到的。 -
触发修改上下布局的最大长度。
这个简单,等于:盒子的大小 - 盒子内边距 - 右侧按扭宽度 -
textarea
内边距
所以问题的重点是:知道输入字符的长度。
解决思路:
- 复制一个
textarea
,监听高度。高度变化则改变布局。 - 复制一个
div
,监听宽度。div
宽度 == 字符的长度。
3. 看看豆包是怎么做的
打开 Elements
可以看到 body
的底部有两个特殊的元素,正好是一个 textarea
、一个 div
。
输入文本测试了一下,div
发生了变化。豆包是通过监听 div
的宽度,获取字符的长度。
豆包的结构布局使用的是 Grid
。
4. 自己实现
我们通过简单的 flex
布局 + vue
进行实现
template
部分代码:
<div class="w-[700px]">
<!-- 用于计算文字长度 -->
<div :class="{'calc-div': !isNeedShowCalculateDiv}">
<!-- 注意这里的span的样式,需要和 textarea 的 font 样式一致 -->
<span
ref="CalculateSpanRef"
style="font-size: 14px; color: #333333; line-height: 22px"
></span>
</div>
<!-- 通过 flex-col 实现左右布局 -->
<div
ref="InputBoxRef"
class="input-box px-2 flex"
:class="[isBig ? ' flex-col items-end' : 'items-center']"
>
<ElInput
class="my-input"
v-model="textarea"
resize="none"
type="textarea"
placeholder="Please input"
:autosize="{minRows: 1, maxRows: 4}"
></ElInput>
<button
class="submit-btn flex-shrink-0"
:class="[isBig ? 'mb-1' : '']"
>
发送
</button>
</div>
</div>
先看效果:
完整代码:
【全部代码】(点击展开)
<template>
<div class="w-[700px]">
<!-- 用于计算输入框宽度,进行换行操作 -->
<div :class="{'calc-div': !isNeedShowCalculateDiv}">
<span
ref="CalculateSpanRef"
style="font-size: 14px; color: #333333; line-height: 22px"
></span>
</div>
<div
ref="InputBoxRef"
class="input-box px-2 flex"
:class="[isBig ? ' flex-col items-end' : 'items-center']"
>
<ElInput
class="my-input"
v-model="textarea"
resize="none"
type="textarea"
placeholder="Please input"
:autosize="{minRows: 1, maxRows: 4}"
></ElInput>
<button
class="submit-btn flex-shrink-0"
:class="[isBig ? 'mb-1' : '']"
>
发送
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import {ref, watch, computed} from 'vue';
import {ElInput} from 'element-plus';
const textarea = ref('');
const isBig = ref(false);
const CalculateSpanRef = ref();
const InputBoxRef = ref();
const isNeedShowCalculateDiv = ref(false);
const textareaWidth = computed(() => {
let width = InputBoxRef.value.offsetWidth;
width -= 8 * 2; // 2 * 8px 边框
width -= 11 * 2; // 2 * 11px 内边距
width -= 52; // 52px 按钮宽度
width -= 5; // 留 5px 框量
return width;
});
watch(
() => textarea.value,
() => {
handleTextareaBreak();
}
);
function handleTextareaBreak() {
CalculateSpanRef.value.textContent = textarea.value;
const maxWidth = textareaWidth.value;
const textWidth = CalculateSpanRef.value.offsetWidth;
console.log(
`🌧🌧🌧 [isBig=${
textWidth > maxWidth
}, maxWidth=${maxWidth}, textWidth=${textWidth}]`
);
if (textWidth > maxWidth) {
isBig.value = true;
} else {
isBig.value = false;
}
}
</script>
<style lang="scss" scoped>
.input-box {
width: 100%;
min-height: 52px;
.my-input {
font-size: 14px;
color: #333333;
line-height: 22px;
}
.submit-btn {
width: 52px;
height: 28px;
background: linear-gradient(270deg, #00dc93 0%, #00dcc2 100%);
box-shadow: 0px 1px 12px 0px rgba(4, 211, 193, 0.1);
font-weight: 500;
font-size: 14px;
color: #ffffff;
border-radius: 8px;
}
.calc-div {
font-size: 14px;
color: #999999;
line-height: 22px;
visibility: hidden;
position: absolute;
left: 0;
top: 0;
z-index: -1000;
max-width: 90vw;
overflow: hidden;
}
}
.calc-div {
position: absolute;
top: -9999px;
left: -9999px;
width: 100%;
opacity: 0;
visibility: hidden;
overflow: hidden;
}
</style>
```
</details>