记一个需求实现 - 多行文本编辑加下横线(信封样式)

194 阅读8分钟

1. 起步

最近在工作中遇到了一个需求,需求很简单就是一个表单的数据提交,但是设计给出的表单样式大概是下面这样的:

image.png 一个信封的样式,组件使用的是antd的多行文本组件TextArea,唯一麻烦的一点就是如何实现文字的下划线效果,拿到需求后最开始想到就是js➕绝对定位一把嗦,但是由于项目要适配h5端,由于使用js的方式会涉及到到计算,在适配不同机型方面总会有点误差,最后经过简单的调研总结出了两种实现方式:

2. 实现思路

方案备注
方案一:使用js动态计算通过计算行高和容器总高度,生成横线dom并定位
方案二:css属性-background使用background属性,将横线作为容器的背景展示

提前说明,最终项目使用的方案是第二种,实现方式非常巧妙和简单,如果你也遇到了类似的需求,可以直接参考方案二,但是由于第一种方案也做过实现,这里也顺便记录下:

2.1 方案一:使用js动态计算

既然是使用js➕绝对定位的方式,首先第一步肯定是要先搞定初始样式,我们最开始的代码是这样的

import { Input } from "antd";
import styles from "./index.module.less";
import { useEffect, useRef, useState } from "react";
const defaultValue = `亲爱的小牛:
    你好!最近工作还顺利吗?希望你在忙碌的代码世界中依然保持着那份热情和动力。
    作为同行,我深知程序员这条路上的挑战和压力。有时候,面对复杂的逻辑、棘手的Bug,或是不断变化的技术栈,难免会感到疲惫和迷茫。但正是这些挑战,让我们不断成长,也让我们更加坚定地走在技术的道路上。
我想和你分享一句话:“代码如人生,调试是常态。”无论是编程还是生活,遇到问题并不可怕,重要的是我们如何面对和解决它们。每一次的调试和优化,都是我们进步的机会。相信你也有同样的感受吧?
最近我在学习一些新的技术栈,虽然刚开始有些吃力,但慢慢发现,只要保持耐心和好奇心,总能找到突破的方向。如果你也在学习或尝试新的东西,不妨一起交流心得,互相鼓励。毕竟,技术的道路上,有伙伴同行总是更有动力。
    最后,希望我们都能在编程的世界里不断突破自我,写出更优雅的代码,解决更有意义的问题。无论遇到什么困难,记得我们都在同一条路上,一起加油!
期待你的回复,也期待我们未来的合作与交流。
祝好,
M木
2025年1月1日`;

function TextAreaLine() {
  const { TextArea } = Input;
  const [value, setValue] = useState(defaultValue);

  useEffect(() => {}, []);

  return (
    <div className={styles.wrapper}>
      <h1 style={{ textAlign: "center" }}>信封样式</h1>
      <div className={styles["textare"]}>
        <TextArea
          autoSize={{ minRows: 18 }}
          showCount
          value={value}
          onChange={(e) => setValue(e.target.value)}
          maxLength={1000}
        />
        <div className={styles.line}></div>
      </div>
    </div>
  );
}

export default TextAreaLine;

简单的过一遍代码,我们的输入框容器的类名是在第24行,在css样式用,我们定义一个类名.line来表示文本下面的横线,通过绝对定位的方式来定位到容器中,所以我们的css代码是这样的:

.wrapper {
  width: 50vw;
  margin: 50px auto;

  .textare {
    position: relative;

    :global {
      .ant-input {
        line-height: 30px;
      }
    }
  }
  .line {
    height: 1px;
    width: 100%;
    background: #eee;
    position: absolute;
    z-index: 999;
    top: 30px;
  }
}

做完了这些准备工作之后,我们预览我们的页面,就能看到我们的第一条线了。

image.png 后面要做的就比较简单了

  1. useEffect钩子中获取文本域容器的高度
  2. 使用文本与的高度➗约定好的行高计算出需要生成横线的数量
  3. 遍历生成横元素,通过索引值计算元素绝对定位的top值
  4. 使用MutationObserverApi监听dom元素的高度变化后重新生成横线

这里直接贴上实现后的代码

import { Input } from "antd";
import styles from "./index.module.less";
import { ReactNode, useEffect, useState } from "react";
const defaultValue = `亲爱的小牛:
    你好!最近工作还顺利吗?希望你在忙碌的代码世界中依然保持着那份热情和动力。
    作为同行,我深知程序员这条路上的挑战和压力。有时候,面对复杂的逻辑、棘手的Bug,或是不断变化的技术栈,难免会感到疲惫和迷茫。但正是这些挑战,让我们不断成长,也让我们更加坚定地走在技术的道路上。
我想和你分享一句话:“代码如人生,调试是常态。”无论是编程还是生活,遇到问题并不可怕,重要的是我们如何面对和解决它们。每一次的调试和优化,都是我们进步的机会。相信你也有同样的感受吧?
最近我在学习一些新的技术栈,虽然刚开始有些吃力,但慢慢发现,只要保持耐心和好奇心,总能找到突破的方向。如果你也在学习或尝试新的东西,不妨一起交流心得,互相鼓励。毕竟,技术的道路上,有伙伴同行总是更有动力。
    最后,希望我们都能在编程的世界里不断突破自我,写出更优雅的代码,解决更有意义的问题。无论遇到什么困难,记得我们都在同一条路上,一起加油!
期待你的回复,也期待我们未来的合作与交流。
祝好,
M木
2025年1月1日`;

function TextAreaLine() {
  const { TextArea } = Input;
  const [value, setValue] = useState(defaultValue);
  const [lines, setLines] = useState<ReactNode[]>([]);

  useEffect(() => {
    // 获取textare容器下的输入框组件
    const target = document.querySelector(`.${styles["textare"]} .ant-input`);

    // 生成横线方法
    const generateLines = (wrapperHeight: number, lineHeight: number = 30) => {
      // 需要生成的数量
      const lineCount = Math.floor(wrapperHeight / lineHeight);
      const lineDoms = Array(lineCount)
        .fill(0)
        .map((_, index) => (
          <div
            className={styles.line}
            key={index}
            style={{ top: (index + 1) * lineHeight }}
          ></div>
        ));
      setLines(lineDoms);
    };
    
    const observe = new MutationObserver((doms) => {
      // 属性变化后会触发这里的代码,如果需要优化,可以在这里判断一下是否是style属性
      generateLines((doms[0].target as HTMLElement).clientHeight)
    });

    // 只监听输入框的属性变化,也就是style属性
    observe.observe(target, {
      attributes: true,
      childList: false,
      subtree: false,
    });

    return () => {
      observe.disconnect();
    };
  }, []);


  return (
    <div className={styles.wrapper}>
      <h1 style={{ textAlign: "center" }}>信封样式</h1>
      <div className={styles["textare"]}>
        <TextArea
          autoSize={{ minRows: 18 }}
          showCount
          value={value}
          onChange={(e) => setValue(e.target.value)}
          maxLength={1000}
        />
        {lines.length !== 0 && lines}
      </div>
    </div>
  );
}

export default TextAreaLine;

2.2 方案二:css属性-background

对于方案二,只要你的css用的足够好,并且你的脑洞够大,就不需要写这么多代码了,由于实现太简单了,这里我们先贴上全部代码

index.tsx

import { Input } from "antd";
import styles from "./index.module.less";
import { useState } from "react";
const defaultValue = `亲爱的小牛:
    你好!最近工作还顺利吗?希望你在忙碌的代码世界中依然保持着那份热情和动力。
    作为同行,我深知程序员这条路上的挑战和压力。有时候,面对复杂的逻辑、棘手的Bug,或是不断变化的技术栈,难免会感到疲惫和迷茫。但正是这些挑战,让我们不断成长,也让我们更加坚定地走在技术的道路上。
我想和你分享一句话:“代码如人生,调试是常态。”无论是编程还是生活,遇到问题并不可怕,重要的是我们如何面对和解决它们。每一次的调试和优化,都是我们进步的机会。相信你也有同样的感受吧?
最近我在学习一些新的技术栈,虽然刚开始有些吃力,但慢慢发现,只要保持耐心和好奇心,总能找到突破的方向。如果你也在学习或尝试新的东西,不妨一起交流心得,互相鼓励。毕竟,技术的道路上,有伙伴同行总是更有动力。
    最后,希望我们都能在编程的世界里不断突破自我,写出更优雅的代码,解决更有意义的问题。无论遇到什么困难,记得我们都在同一条路上,一起加油!
期待你的回复,也期待我们未来的合作与交流。
祝好,
M木
2025年1月1日`;

function TextAreaLine() {
  const { TextArea } = Input;
  const [value, setValue] = useState(defaultValue);

  return (
    <div className={styles.wrapper}>
      <h1 style={{ textAlign: "center" }}>信封样式</h1>
      <TextArea
        className={styles["textare"]}
        autoSize={{ minRows: 18 }}
        showCount
        value={value}
        onChange={(e) => setValue(e.target.value)}
        maxLength={1000}
      />
    </div>
  );
}

export default TextAreaLine;

index.module.less

.wrapper {
    width: 50vw;
    margin: 50px auto;
  
    .textare {
      background: linear-gradient(to top, transparent 95%, #eee 5%);
      background-size: 100% 30px;
      border: 1px dashed orange;
      padding-bottom: 30px;
  
      :global {
        .ant-input {
          line-height: 30px;
        }
      }
    }

没错只需要第5,第6两行代码就能直接实现我们最终的效果,只需要把上面的代码粘到你的编辑器中就能实现文章开始的效果了。那这两行代码是什么意思呢?

  • background: linear-gradient(to top, transparent 95%, #eee 5%);表示为容器设置一个下往上的渐变背景,渐变的过度为透明95%,#eee色号5%。
    • 为了更清楚的看到效果,我们把渐变效果改为background: linear-gradient(to top, orange 95%, #eee 5%);,这时我们看到的效果是这样的

image.png

可以看到橙色的颜色占了容器的95%,如果把橙色换成透明的换就只剩#eee的色号了,那么我们要如何使这个背景重复呢?使用background-size调整背景的大小。

  • background-size: 100% 30px;表示背景的横向是100%,纵向和我们的行高一致,于是就有变成了这样 image.png

    最后我们只需要把橙色换成透明色,我们的横线效果就简答实现了。