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

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.3、效果如图

react 实现chatGPT的打印机效果 兼容富文本,附git地址_第1张图片

2、方式二:自定义实现

2.1、思路

我的思路是将字符串切割成两个数组,一个是 <>的标签数组,一个是按字符和标签截取的数组,效果如图:
react 实现chatGPT的打印机效果 兼容富文本,附git地址_第2张图片
react 实现chatGPT的打印机效果 兼容富文本,附git地址_第3张图片
然后遍历chucksList生成新的数组,如下图:
react 实现chatGPT的打印机效果 兼容富文本,附git地址_第4张图片
然后遍历这个数组,使用定时器插入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; //光标配置项
};

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);
};

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; };

2.2.4、getCursorClassName

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

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);
};

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;

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;

2.4、效果

react 实现chatGPT的打印机效果 兼容富文本,附git地址_第5张图片

git项目地址,点我打开

你可能感兴趣的:(前端,JavaScript,react,react.js,前端,前端框架)