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

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

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

1.1、核心代码如下:

javascript 复制代码
//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;
javascript 复制代码
// index.d.ts
export type PropsType = {
  text: string; //文本内容
  seed?: number; //速度
  callback?: () => void; //打印结束后的回调函数
};

1.2、使用

javascript 复制代码
/*
 * @Description:
 * @Author: muge
 * @LastEditors: muge
 */
import TypeWriteEffect from '@/components/TypeWriteEffect';
import React from 'react';

const Index = () => {
  const richText =
    '<code>2112.1</code>这是<span class="typing-text" style="color: red">智能问答小助手--</span>的响应文本----很长很长的的。<div style="color: pink; font-size: 20px">原神*启动!</div>---王者*启动!<img src="https://nimg.ws.126.net/?url=http%3A%2F%2Fdingyue.ws.126.net%2F2022%2F0830%2F74168ba1j00rhf6m5002cd000u000jfp.jpg&thumbnail=660x2147483647&quality=80&type=jpg" style="height: 150px"/>';
  return <TypeWriteEffect text={richText} />;
};
export default Index;

1.3、效果如图

2、方式二:自定义实现

2.1、思路

我的思路是将字符串切割成两个数组,一个是 <></>的标签数组,一个是按字符和标签截取的数组,效果如图:

然后遍历chucksList生成新的数组,如下图:

然后遍历这个数组,使用定时器插入dom即可

2.2、核心代码

2.2.1、writeEffect.ts

javascript 复制代码
// 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; //光标配置项
};

2.2.2、createBlinkSpan

javascript 复制代码
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);
};

2.2.3、textConversionArr

javascript 复制代码
// 标签切割
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 = /<\/[^>]*>/; //是否是闭合标签 </img>=>true  <>=>false <div/>=>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)) {
      // 判断是否为自闭合标签,如 <img> <hr> <br>这种不规范的写法
      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;
};

2.2.4、getCursorClassName

javascript 复制代码
import initData from './tool';
/**
 * @description: //获取光标dom
 * @author: muge
 */
export const getCursorClassName = () => {
  return document.querySelector(`.${initData.cursorClassName}`) as HTMLElement;
};

2.2.5、removeCursor

javascript 复制代码
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);
};

2.2.6、initData

javascript 复制代码
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;

2.3、使用

javascript 复制代码
import { typingEffect } from '@/utils/writeEffect';
import React, { useEffect, useRef } from 'react';

const Index = () => {
  const el = useRef<HTMLElement | any>(null);

  const richText =
    '原神 · 启动!<img src="https://nimg.ws.126.net/?url=http%3A%2F%2Fdingyue.ws.126.net%2F2022%2F0830%2F74168ba1j00rhf6m5002cd000u000jfp.jpg&thumbnail=660x2147483647&quality=80&type=jpg" style="height: 150px"/><br/><hr><br><div>王者荣耀 · 启动!</div>';
  useEffect(() => {
    typingEffect(el.current, {
      text: richText,
      callback: () => {
        console.log('打印机结束后执行的回调函数!');
      },
      cursorConfig: {
        cursor: true,
      },
    });
  }, []);
  return <div ref={el}></div>;
};

export default Index;

2.4、效果

git项目地址,点我打开

相关推荐
mldong4 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排4 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60615 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角6 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!6 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰6 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi8 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen8 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马9 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒9 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端