• react 实现chatGPT的打印机效果 兼容富文本,附git地址


    1、方式一 :使用插件 typed.js

    typed.js 网站地址,点我打开

    1.1、核心代码如下:

    //TypeWriteEffect/index.tsx 组件
    import React, { useEffect, useRef } from 'react';
    import Typed from 'typed.js';
    import { PropsType } from './index.d';
    const TypeWriteEffect: React.FC<PropsType> = ({ text = '', callback, seed = 20 }) => {
      const el = useRef(null);
      useEffect(() => {
        const typed = new Typed(el.current, {
          strings: [text],
          typeSpeed: seed,
          showCursor: true,
          onComplete(self) {
            callback?.();
            self.cursor.style.display = 'none'; // 隐藏光标
          },
        });
        return () => {
          typed.destroy();
        };
      }, []);
      return (
        <div>
          <span ref={el}></span>
        </div>
      );
    };
    export default TypeWriteEffect;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    // index.d.ts
    export type PropsType = {
      text: string; //文本内容
      seed?: number; //速度
      callback?: () => void; //打印结束后的回调函数
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    1.2、使用

    /*
     * @Description:
     * @Author: muge
     * @LastEditors: muge
     */
    import TypeWriteEffect from '@/components/TypeWriteEffect';
    import React from 'react';
    
    const Index = () => {
      const richText =
        '2112.1这是智能问答小助手--的响应文本----很长很长的的。
    原神*启动!
    ---王者*启动!'
    ; return <TypeWriteEffect text={richText} />; }; export default Index;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    1.3、效果如图

    在这里插入图片描述

    2、方式二:自定义实现

    2.1、思路

    我的思路是将字符串切割成两个数组,一个是 <>的标签数组,一个是按字符和标签截取的数组,效果如图:
    在这里插入图片描述
    在这里插入图片描述
    然后遍历chucksList生成新的数组,如下图:
    在这里插入图片描述
    然后遍历这个数组,使用定时器插入dom即可

    2.2、核心代码

    2.2.1、writeEffect.ts

    // utils/writeEffect/index.ts
    import type { TypingEffectType } from './index.d';
    import initData from './lib/tool';
    import { createBlinkSpan } from './lib/createBlinkSpan';
    import { textConversionArr } from './lib/textConversionArr';
    import { getCursorClassName } from './lib/getCursorClassName';
    import { removeCursor } from './lib/removeCursor';
    /**
     * @description: 光标打印效果
     * @param {HTMLElement} dom
     * @param {TypingEffectType} parameter
     * @author: muge
     */
    export const typingEffect = (dom: HTMLElement, parameter: TypingEffectType) => {
      const { text, callback, cursorConfig = {}, seed = initData.seed } = parameter;
      const {
        cursor = false,
        dieTime = initData.dieTime,
        blinkSeed = initData.blinkSeed,
      } = cursorConfig as any;
      if (!dom || !text) return;
      const textArrs: string[] = textConversionArr(text);
      dom.innerHTML = ''; //每次清空内容
      let blinkInterval: any = null; //光标定时器
      // 添加光标效果
      cursor && createBlinkSpan(dom, blinkInterval, blinkSeed);
      let startIndex = 0;
      const element = document.createElement('span'); //文本存放标签
      const start = () => {
        startIndex++;
        if (startIndex >= textArrs.length) {
          cursor && removeCursor(dom, blinkInterval, dieTime);
          callback?.();
          return;
        }
        if (cursor) {
          element.innerHTML = textArrs[startIndex];
          dom.insertBefore(element, getCursorClassName());
        } else {
          dom.innerHTML = textArrs[startIndex];
        }
        setTimeout(() => start(), seed);
      };
      start();
    };
    
    //index.d.ts
    type cursorConfigType = {
      cursor?: boolean; //是否显示光标
      seed?: number; //光标默认速度=>默认250ms
      dieTime?: number; //打字结束后光标消失时间=>默认200ms
      blinkSeed?: number; //光标闪烁速度
    };
    export type TypingEffectType = {
      text: string; //文本
      seed?: number; //默认打字速度,默认250ms
      callback?: () => void; //打字机结束的回调函数
      cursorConfig?: cursorConfigType; //光标配置项
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    2.2.2、createBlinkSpan

    import initData from './tool';
    
    export const createBlinkSpan = (
      dom: HTMLElement,
      intervalName: NodeJS.Timer,
      blinkSeed: number,
    ) => {
      const { cursorClassName } = initData;
      const blinkName = document.createElement('span');
      blinkName.className = cursorClassName;
      blinkName.innerHTML = '|';
      dom.appendChild(blinkName);
      // 设置闪烁间隔,例如每500毫秒切换一次光标状态
      intervalName = setInterval(() => {
        blinkName.style.display = blinkName.style.display === 'none' ? 'inline' : 'none';
      }, blinkSeed);
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    2.2.3、textConversionArr

    // 标签切割
    const labelCut = (str: string) => {
      const arrs = str.match(/<[^>]+>(?!\/>)/g);
      if (!arrs) return [];
      return arrs.filter((item) => !/<[^>]+\/>$/.test(item));
    };
    // 通过<>分隔字符串=》数组
    const splitStringToChunks = (str: string): string[] => {
      const chunks: string[] = [];
      let currentChunk = '';
      let insideTag = false;
      for (let i = 0; i < str.length; i++) {
        const char = str[i];
        if (char === '<') {
          insideTag = true;
          currentChunk += char;
        } else if (char === '>') {
          insideTag = false;
          currentChunk += char;
        } else {
          currentChunk += char;
        }
        if (!insideTag || i === str.length - 1) {
          chunks.push(currentChunk);
          currentChunk = '';
        }
      }
      return chunks;
    };
    /**
     * @description: 文本转换数组
     * @param {string} str
     * @author: muge
     */
    export const textConversionArr = (str: string): string[] => {
      const labelCutList = labelCut(str);
      const chucksList = splitStringToChunks(str);
      let startIndex: number = 0;
      const result: string[] = [];
      let lastStr = ''; //拼接的字符串
      const isCloseTagReg = /<\/[^>]*>/; //是否是闭合标签 =>true  <>=>false 
    =>false while (startIndex < chucksList?.length) { let currentIndex = startIndex; ++startIndex; const currentStr = chucksList[currentIndex]; const index = labelCutList.indexOf(currentStr); if (index === -1) { lastStr += currentStr; result.push(lastStr); continue; } // 起始标签 if (!/<\/[^>]+>/.test(currentStr)) { // 判断是否为自闭合标签,如

    这种不规范的写法
    const nextCloseTag: string | undefined = labelCutList[index + 1]; if (!nextCloseTag || !isCloseTagReg.test(nextCloseTag)) { lastStr += currentStr; result.push(lastStr); continue; } // 查找第一个闭合标签的下标 const findArrs = chucksList.slice(currentIndex); const endTagIndex = findArrs.findIndex((item) => item === nextCloseTag); let curStr: string = ''; for (let i = 1; i < endTagIndex; i++) { curStr += findArrs[i]; const res = labelCutList[index] + curStr + nextCloseTag; result.push(lastStr + res); if (endTagIndex - 1 === i) { lastStr += res; } } startIndex = currentIndex + endTagIndex; //重置下标 continue; } } return result; };
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78

    2.2.4、getCursorClassName

    import initData from './tool';
    /**
     * @description: //获取光标dom
     * @author: muge
     */
    export const getCursorClassName = () => {
      return document.querySelector(`.${initData.cursorClassName}`) as HTMLElement;
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2.2.5、removeCursor

    import initData from './tool';
    /**
     * @description: //移除光标标签
     * @param {HTMLElement} dom //光标标签dom
     * @param {string} intervalName //定时器名字
     * @param {number} cursorAway //光标消失时间
     * @author: muge
     */
    export const removeCursor = (dom: HTMLElement, intervalName: NodeJS.Timer, cursorAway: number) => {
      setTimeout(() => {
        clearInterval(intervalName);
        dom.removeChild(document.querySelector(`.${initData.cursorClassName}`) as HTMLElement);
      }, cursorAway);
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    2.2.6、initData

    type initDataType = {
      cursorClassName: string;
      seed: number;
      blinkSeed: number;
      dieTime: number;
    };
    const initData: initDataType = {
      cursorClassName: 'blink-class',
      seed: 100,
      dieTime: 500,
      blinkSeed: 350,
    };
    export default initData;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    2.3、使用

    import { typingEffect } from '@/utils/writeEffect';
    import React, { useEffect, useRef } from 'react';
    
    const Index = () => {
      const el = useRef<HTMLElement | any>(null);
    
      const richText =
        '原神 · 启动!


    王者荣耀 · 启动!
    '
    ; useEffect(() => { typingEffect(el.current, { text: richText, callback: () => { console.log('打印机结束后执行的回调函数!'); }, cursorConfig: { cursor: true, }, }); }, []); return <div ref={el}></div>; }; export default Index;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    2.4、效果

    在这里插入图片描述

    git项目地址,点我打开

  • 相关阅读:
    DAMOYOLO windows 单卡训练
    微信推出自研NLP大规模语言模型WeLM,现已开放API推动应用落地
    virtio frontend and backend
    解决go-micro与其它gRPC框架之间的通信问题
    9.Eureka服务发现+Ribbon+RestTemplate服务调用
    分类预测|基于黑翅鸢优化最小二乘支持向量机分类预测Matlab程序BKA-LSSVM 多特征输入多类别输出 含基础LSSVM
    docker中安装Ubuntu20,浏览器访问其图形界面
    Rust流程控制
    王学岗——H265编码原理详解与码流分析
    一文拿捏Spring之AOP
  • 原文地址:https://blog.csdn.net/muge1161105403/article/details/134260566