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 =
    '<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

// 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 = /<\/[^>]*>/; //是否是闭合标签 </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

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 =
    '原神 · 启动!<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项目地址,点我打开

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/123788.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

t2017递推3骨牌

答案&#xff1a; #include<iostream> using namespace std; long long n,s[10009]; int main() {cin>>n;s[1]1,s[2]2,s[3]4;if(n1){cout<<1;return 0;}if(n2){cout<<2;return 0;}if(n3){cout<<4;return 0;}for(int i4;i<n;i)s[i]s[i-1]s[i…

什么是观察者模式?用 Python 如何实现 Observer(观察者或发布订阅)对象行为型模式?

什么是观察者模式&#xff1f; 观察者模式&#xff08;Observer pattern&#xff09;是一种行为型设计模式&#xff0c;它允许对象之间建立一种一对多的依赖关系&#xff0c;当一个对象的状态发生变化时&#xff0c;其相关依赖对象都会得到通知并自动更新。 在观察者模式中&am…

【服务发现与配置】Consul特性及搭建

文章目录 一、前言二、概念2.1、什么是Consul&#xff1f;2.2、Consul具有哪些特点?2.3、Consul 架构图2.4、Consul的使用场景 三、安装3.1. 下载3.2. 解压3.3. 拷贝到usr目录下3.4. 查看 安装是否成功3.5. 启动 四、Consul 开机自启动4.1. 路径/usr/lib/systemd/system/&…

【Qt之QMetaType】使用

介绍 QMetaType类管理元对象系统中的命名类型。 该类用作QVariant和排队的信号与槽连接中类型的编组辅助器。它将类型名称与类型关联起来&#xff0c;以便可以在运行时动态创建和销毁它。使用Q_DECLARE_METATYPE()声明新类型&#xff0c;以使它们可供QVariant和其他基于模板的…

三菱FX3U系列—原点回归指令

目录 一、简介 二、指令形式 1、原点指令[ZRN/DZRN] 2、带搜索的原点回归指令[DSZR] 三、回归指令运行过程 1、ZRN原点回归运行过程 2、带搜索的原点回归运行过程 四、特殊辅助继电器 五、特殊输出模块 六、总结 一、简介 用于将电机或伺服驱动器控制的轴回到预定的原…

浙江大学漏洞报送证书

获取来源&#xff1a;edusrc&#xff08;教育漏洞报告平台&#xff09; url&#xff1a;主页 | 教育漏洞报告平台 兑换价格&#xff1a;20金币 获取条件&#xff1a;提交浙江大学任意中危或以上级别漏洞

socks5代理和https代理有什么不同?各自有哪些优点?

socks5代理和https代理是两种不同的代理服务&#xff0c;它们在实现方式、安全性和协议特点等方面存在差异。下面我们来详细了解一下这两种代理的优点。 一、socks5代理的优点 1. 速度快 socks5代理采用了TCP协议&#xff0c;能够有效地减少网络延迟和数据传输速度慢的问题&…

Ubuntu22.04下挂载共享文件夹

1.在自己Windows任意地方建一个文件夹 2.打开虚拟机做如下配置 3.开启虚拟机&#xff0c;打开终端 4.输入&#xff1a;vmware-hgfsclient 看到物理机共享文件夹 5.输入&#xff1a;sudo mkdir /mnt/hgfs 创建虚拟机中的共享文件夹 6.输入&#xff1a;sudo vmhgfs-fuse .h…

离散数学第一章知识点复习

命题&#xff1a;陈述句 真值已经确定 原子命题&#xff08;简单命题&#xff09;&#xff1a;不能被分解为更简单的命题 命题化的时候的解题步骤&#xff1a; 1. 先给出原子命题 2. 符号化 注意蕴含式&#xff1a;记作 p -> q &#xff0c;p是前件&#xff0c;q 是后…

软件版本控制系统VCS工具——cvs vss svn git

版本控制 版本控制系统&#xff08;Version Control System&#xff0c;VCS&#xff09;是用于跟踪和管理源代码和文档的工具。可追踪和管理修改历史&#xff0c;包括修改的内容、时间、作者等信息。有助于团队协作、追踪变更、恢复历史版本等。VCS的主要目的是帮助团队协作开…

如何写一篇吊炸天的竞品分析

这段时间&#xff0c;除了撩妹之外&#xff0c;最多的就是竞品分析了。最近很多临近毕业的同学也在四处应聘产品岗&#xff0c;而一份不错的竞品分析一定能为你的求职加分不少。于是&#xff0c;有着菩萨心肠天使面孔魔鬼身材的我&#xff0c;就来教大家怎么做一份完整的竞品分…

什么是代理IP池?真实测评IP代理商的IP池是否真实?

代理池充当多个代理服务器的存储库&#xff0c;提供在线安全和匿名层。代理池允许用户抓取数据、访问受限制的内容以及执行其他在线任务&#xff0c;而无需担心被检测或阻止的风险。代理池为各种在线活动&#xff08;例如网页抓取、安全浏览等&#xff09;提高后勤保障。 读完…

【Bug】Python利用matplotlib绘图无法显示中文解决办法

一&#xff0c;问题描述 当利用matplotlib进行图形绘制时&#xff0c;图表标题&#xff0c;坐标轴&#xff0c;标签中文无法显示&#xff0c;显示为方框&#xff0c;并报错 运行窗口报错&#xff1a; 这是中文字体格式未导入的缘故。 二&#xff0c;解决方案 在代码import部…

将 Figma 轻松转换为 Sketch 的免费方法

最近浏览网站的时候&#xff0c;发现很多人不知道Figma是怎么转Sketch的。众所周知&#xff0c;Figma支持Sketch文件的导入&#xff0c;但不支持Sketch的导出&#xff0c;那么Figma是如何转Sketch的呢&#xff1f;不用担心&#xff0c;建议使用神器即时设计。它是一个可以实现在…

systemctl enable docker.service报错“Failed to execute operation: Bad message“

将docker加入到开机自启&#xff0c;报错&#xff1a; 解决&#xff1a; 重新粘贴复制&#xff1a; [Unit] DescriptionDocker Application Container Engine Documentationhttps://docs.docker.com Afternetwork-online.target firewalld.service Wantsnetwork-online.target…

在现实生活中传感器GV-H130/GV-21的使用

今天&#xff0c;收获了传感器GV-H130/GV-21&#xff0c;调试探头的用法&#xff0c;下面就来看看吧&#xff01;如有不妥欢迎指正&#xff01;&#xff01;&#xff01;&#xff01; 目录 传感器GV-H130/GV-21外观 传感器调试探头 探头与必要准备工作 传感器数值更改调试 …

Fabric: 使用InvokeChaincode实现跨通道数据访问

因为工作中遇到一些问题考虑使用Fabric的跨通道链码调用方法InvokeChaincode()来解决&#xff0c;这篇文章主要是记录以下在Fabric测试网络中InvokeChaincode()的使用过程及遇到的问题。 1 前期准备 1.1 认识InvokeChaincode InvokeChaincode的作用是调用指定的链码。而被调用…

pytoch安装指定版本教程pytorch1.3安装笔记

一、先生成一个环境 如果电脑里安装了其他的torch版本&#xff0c;另外生成一个环境可以防止原先torch版本被替换掉。 打开conda的终端窗口输入以下命令就可以生成一个名为torch_1.3的环境&#xff1a; conda create -n torch_1.3 python3.6 输入以下命令进入到torch_1.3的…

基于springboot实现致远汽车租赁平台管理系统项目【项目源码+论文说明】

基于springboot实现致远汽车租赁平台系统演示 摘要 首先,论文一开始便是清楚的论述了系统的研究内容。其次,剖析系统需求分析,弄明白“做什么”,分析包括业务分析和业务流程的分析以及用例分析,更进一步明确系统的需求。然后在明白了系统的需求基础上需要进一步地设计系统,主要…

linux下实现电脑开机后软件自启动

实现linux的软件自启动&#xff0c;需要四个文件 第一个【displayScreen.desktop】文件&#xff0c;.desktop文件就是一个用来运行程序的快捷方式,也叫启动器&#xff0c;常用来自启动用的文件&#xff0c;内容如下 [Desktop Entry] #要执行的脚本位置 Exec/home/yicaobao/te…