uniapp的光标跟随和打字机效果

1、准备好容器

文字的显示textRef,以及光标的显示 ,使用transform-translate对光标进行移动到文字后面

html 复制代码
<template>
  <view class="container" ref="contentRef">
    <u-parse :content="nodeText" ref="textRef"></u-parse>
    <view class="cursor" v-show="cursorShow" :style="{transform:`translate(${x}px,${y}px)`}"></view>
  </view>
</template>

2、准备样式

样式,你可以自定义,不必安装我的,主要是光标的闪烁动画

html 复制代码
<style scoped lang="scss">
.container {
  position: relative;
  width: 100%;
  height: 100vh;
  box-sizing: border-box;
  padding: 30rpx 20rpx;

  .cursor {
    position: absolute;
    left: 10rpx;
    top: 10rpx;
    width: 30rpx;
    height: 30rpx;
    background-color: #000;
    border-radius: 50%;
    animation: cursorAnimate 0.5s infinite;
  }

  @keyframes cursorAnimate {
    0% {
      opacity: 0;
    }

    50% {
      opacity: 1;
    }

    100% {
      opacity: 0;
    }
  }
}
</style>

3、逻辑

1、模拟接受数据
javascript 复制代码
      async mockResponse() {
        this.cursorShow = true;
        for (let i = 0; i < this.nodeData.length; i++) {
          try {
            this.nodeText = this.nodeData.slice(0, i);
            this.updateCursor();
            await this.delay(100);
          } catch (e) {
            console.log(e)
          }
        }
        this.cursorShow = false;
      },
2、更新光标位置
javascript 复制代码
      updateCursor() {
        // 1. 找到最后一个文本节点
        const lastTextNode = this.getLastTextNode(this.$refs.textRef,1);
        // 2. 创建一个临时文本节点
        const tempText = document.createTextNode('\u200B'); // 零宽字符
        // 3. 将临时文本节点放在最后一个文本节点之后
        if (lastTextNode) {
          lastTextNode.parentNode && lastTextNode.parentNode.appendChild(tempText);
        } else {
          this.$refs.textRef && this.$refs.textRef.$el.appendChild(tempText);
        }
        // // 4. 获取临时文本节点距离父节点的距离(x,y) 可以使用 setStart 和 setEnd 方法来设置 Range 的开始和结束位置。
        const range = document.createRange(); // 设置范围
        range.setStart(tempText, 0);
        range.setEnd(tempText, 0);
        const rect = range.getBoundingClientRect(); // 获取距离信息
        // // 5. 获取当前文本容器距离视图的距离(x,y)
        const textRect = this.$refs.contentRef && this.$refs.contentRef.$el.getBoundingClientRect();
        // 6. 获取到当前文本节点的位置,并将光标的位置插入到相应位置
        if (textRect) {
          const x = rect.left - textRect.left + 10;
          const y = rect.top - textRect.top; // 7.5 是光标高度的一半,为了居中显示光标
          this.x = x;
          this.y = y;
        }
        // 7. 移除临时文本节点
        tempText.remove();
      },

4、完整代码 如下:

html 复制代码
<template>
  <view class="container" ref="contentRef">
    <u-parse :content="nodeText" ref="textRef"></u-parse>
    <view class="cursor" v-show="cursorShow" :style="{transform:`translate(${x}px,${y}px)`}"></view>
  </view>
</template>

<script>

  export default {
    data() {
      return {
        nodeData: '打击好,<p>1. 近日,一份全球数学竞赛决赛名单引起广泛关注。其中,学服装设计的姜萍,以93分的高分名列第12位。天才少女姜萍的故事在全网引发热议。总台记者对江苏涟水中专党委书记进行了专访,揭秘姜萍选择涟水中专的原因。</p>\n' +
          '    <p>2. 江苏涟水中专党委书记介绍,姜萍中考621分,能够达到当地普通高中的录取分数线,之所以选择涟水中专,据姜萍自己讲,原因之一是当时她的姐姐以及两个要好的同学都在这所学校就读。另外,就是姜萍对服装专业比较感兴趣,认为这里对自己的兴趣、爱好发展发挥更有利。</p>',

        cursorShow: true,
        nodeText: '',

        x: 0,
        y: 0,
      }
    },
    onLoad() {
      let timer = setInterval(() => {
        if (this.$refs.textRef) {
          clearInterval(timer)
          this.mockResponse()
        }
      }, 500)
    },
    methods: {
      delay(time) {
        return new Promise((resolve) => setTimeout(resolve, time));
      },
      /** 模拟请求 */
      async mockResponse() {
        this.cursorShow = true;
        for (let i = 0; i < this.nodeData.length; i++) {
          try {
            this.nodeText = this.nodeData.slice(0, i);
            this.updateCursor();
            await this.delay(100);
          } catch (e) {
            console.log(e)
          }
        }
        this.cursorShow = false;
      },
      updateCursor() {
        // 1. 找到最后一个文本节点
        const lastTextNode = this.getLastTextNode(this.$refs.textRef,1);
        // 2. 创建一个临时文本节点
        const tempText = document.createTextNode('\u200B'); // 零宽字符
        // 3. 将临时文本节点放在最后一个文本节点之后
        if (lastTextNode) {
          lastTextNode.parentNode && lastTextNode.parentNode.appendChild(tempText);
        } else {
          this.$refs.textRef && this.$refs.textRef.$el.appendChild(tempText);
        }
        // // 4. 获取临时文本节点距离父节点的距离(x,y) 可以使用 setStart 和 setEnd 方法来设置 Range 的开始和结束位置。
        const range = document.createRange(); // 设置范围
        range.setStart(tempText, 0);
        range.setEnd(tempText, 0);
        const rect = range.getBoundingClientRect(); // 获取距离信息
        // // 5. 获取当前文本容器距离视图的距离(x,y)
        const textRect = this.$refs.contentRef && this.$refs.contentRef.$el.getBoundingClientRect();
        // 6. 获取到当前文本节点的位置,并将光标的位置插入到相应位置
        if (textRect) {
          const x = rect.left - textRect.left + 10;
          const y = rect.top - textRect.top; // 7.5 是光标高度的一半,为了居中显示光标
          this.x = x;
          this.y = y;
        }
        // 7. 移除临时文本节点
        tempText.remove();
      },
      /** 获取最后一个文本节点 */
      getLastTextNode(node,index = 1) {
        if (index === 1) { // 获取的第一个node,需要查询$el,childNodes里面的就,不需要了
          node = node.$el;
        }
        if (!node) return null;
        // console.log(node, node.textContent, node.nodeType, Node.TEXT_NODE)
        if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim()) {
          // console.log('返回的', node)
          return node;
        }
        for (let i = node.childNodes.length - 1; i >= 0; i--) {
          const childNode = node.childNodes[i];
          const textNode = this.getLastTextNode(childNode,index + 1);
          if (textNode) {
            return textNode;
        }
      }
      return null;
    }
  }
}
</script>

<style scoped lang="scss">
.container {
  position: relative;
  width: 100%;
  height: 100vh;
  box-sizing: border-box;
  padding: 30rpx 20rpx;

  .cursor {
    position: absolute;
    left: 10rpx;
    top: 10rpx;
    width: 30rpx;
    height: 30rpx;
    background-color: #000;
    border-radius: 50%;
    animation: cursorAnimate 0.5s infinite;
  }

  @keyframes cursorAnimate {
    0% {
      opacity: 0;
    }

    50% {
      opacity: 1;
    }

    100% {
      opacity: 0;
    }
  }
}
</style>
相关推荐
m0_敲代码的彭于晏16821 分钟前
Vercel 前端部署平台深度评测
前端
怕浪猫2 小时前
外包和大厂的前端代码,差在凌晨三点
前端·面试·github
m0_敲代码的彭于晏1682 小时前
Vercel 前端部署与全栈开发实战指南
前端·状态模式
徐小夕2 小时前
花了3个月,我们做了一款 AI 版的“飞书文档”
前端·算法·github
贾伟康2 小时前
【化学实验室|17】HarmonyOS ArkTS 亮暗色与视觉令牌实战:集中颜色、间距和交互状态避免页面割裂
前端·移动开发·harmonyos·arkts·arkui
Csvn3 小时前
SSR/SSG/ISR(上):峡谷与渲染策略的时间预算
前端
Csvn3 小时前
SSR/SSG/ISR(下):可交互性、新鲜度与回源账
前端
酷炫码神4 小时前
Day 10|Pinia 状态管理:把散落在各组件的数据,收进一个全局仓库
android·开发语言·javascript
Elcker6 小时前
YnuoAgent(依诺):一个 npm 包,跑起属于自己的 AI 研发平台
前端·人工智能·npm
酷炫码神6 小时前
Day 8|Vue3 响应式:告别手动操作 DOM,让数据驱动界面
javascript·vue.js·ecmascript