探索豆包输入框布局奥秘,从原理到实现

331 阅读3分钟

1. 想要的效果

豆包的输入框是左右布局,输入的字符要折行的时候,会变成上下结构。

对用户的输入体验和 UI 都非常的友好。

空状态

极线一行

超过后改变布局

2. 思考是如何做的

我们需要知道:

  1. 我们输入的字符的长度。

    介于 input textarea 的特性,他们的宽度无法根据内容的长度进行伸缩,直接 inputtextarea 获取输入的字符长度是做不到的。

  2. 触发修改上下布局的最大长度。

    这个简单,等于:盒子的大小 - 盒子内边距 - 右侧按扭宽度 - textarea 内边距

所以问题的重点是:知道输入字符的长度。

解决思路:

  1. 复制一个 textarea,监听高度。高度变化则改变布局。
  2. 复制一个 div,监听宽度。div 宽度 == 字符的长度。

3. 看看豆包是怎么做的

打开 Elements 可以看到 body 的底部有两个特殊的元素,正好是一个 textarea、一个 div

image.png

输入文本测试了一下,div 发生了变化。豆包是通过监听 div 的宽度,获取字符的长度。

image.png

豆包的结构布局使用的是 Grid

image.png

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>

先看效果:

Kapture 2025-01-24 at 15.24.18.gif

完整代码:

【全部代码】(点击展开)
  <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>