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

news2024/12/29 8:56:20

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项目地址,点我打开

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【STM32】定时器

systick定时器&#xff1a; 【STM32】Systick定时器-CSDN博客 0.通用定时器框图 1.时钟源 2.控制器 3.输入捕获 计数器实际上是与比较寄存器的影子寄存器进行比较的。 4.输出比较 1.STM32的定时器学习要点 参考手册 STM32F1xx中文参考手册.pdf 林何/STM32F103C8 - 码云 -…

【LIUNX】机器互访:免密登陆

服务器端 /etc/ssh/sshd_config 口常见SSH服务器监听的选项如下&#xff1a; Port 22//监听的端口为22 Protocol 2//使用SSH V2协议 ListenAdderss 0.0.0.0 //监听的地址为所有地址 UseDNS no//禁止DNS反向解析 客户端 /etc/ssh/ssh_config 口常见用户登录控制选项如下&#…

Red Giant Trapcode Suite 2024.0.1

Red Giant Trapcode Suite是一款ae视觉效果插件软件&#xff0c;适用于After Effects和Premiere Pro等流行的视频编辑软件。该软件集合了一系列强大而创新的工具&#xff0c;可以帮助用户创建令人惊叹的视觉效果和动态图形。 Red Giant Trapcode Suite包含多种插件&#xff0c…

3-知识补充-MVC框架

3-知识补充-MVC框架 文章目录 3-知识补充-MVC框架MVC概述M、V、C各自负责功能及常用包MVC框架图非前后端分离框架图前后端分离框架图 MVC概述 MVC&#xff08;Model、View、Controller&#xff09;是软件工程中的一种**软件架构模式&#xff0c;它把软件系统分为模型、视图和控…

SpringCloudAlibaba——Nacos

Nacos是服务注册中心服务配置中心。替换了以前的EurekaConfigBus。 1.Nacos作为服务注册中心 Nacos支持AP和CP模式的转换。 2.Nacos作为服务配置中心 服务要配置两个yml文件&#xff0c;bootstrap.yml和application.yml。因为Nacos同springcloud-config一样&#xff0c;在项…

NeRF神经辐射场渲染过程详解,三维重建渲染过程基本原理_光线采样sample_pdf()和光线渲染render_rays ()代码详解

目录 1 神经辐射场 1.1 基本原理 1.2 基本流程 1.3 数学解释 2 三维场景图像渲染详解 2.1射线采样 2.2 NeRF 模型预测 2.3 体积渲染 3 采样与渲染代码详解 &#xff08;rending.py&#xff09; 3.1 神经体积渲染代码解析 3.2 sample_pdf 函数 3.3 render_rays 函数 …

号牌模拟数据生成

说明 自己开发的测试数据生成工具&#xff0c;用于生成数据训练对应模型。 项目 效果

微带线的ABCD矩阵的推导、转换与级联-Matlab计算实例

微带线的ABCD矩阵的推导、转换与级联-Matlab计算实例 散射参数矩阵有实际的物理意义&#xff0c;但是其无法级联计算&#xff0c;但是ABCD参数和传输散射矩阵可以级联计算&#xff0c;在此先简单介绍ABCD参数矩阵的基本用法。 1、微带线的ABCD矩阵的推导 其他的一些常用的二端…

【教程】多进程下载百度旋转验证码图片-制作数据集

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] 效果展示&#xff1a; 直接上代码&#xff0c;开箱即用&#xff08;当然selenium库自己装一下&#xff09;&#xff1a; import os import time import requests from selenium import webdriver from selenium.…

为什么HTTP用得很好的,开始普及HTTPS呢?

显而易见&#xff0c;现在的HTTP早已不安全&#xff0c;当我们在浏览各个网站时会发现HTTP前面都会显示不安全&#xff0c;因为HTTP是明文传输&#xff0c;一旦电脑被植入了木马&#xff0c;木马程序就会主动周期性发消息给Internet的控制终端&#xff0c;这样NAT小洞会一直敞开…

Markdown写作应用推荐

MWeb Pro 是一款适用于macOS的专业Markdown写作、笔记本应用软件。喜欢写博客的朋友&#xff0c;那你一定会需要 MWeb Pro 这款软件。为您提供最佳的写作体验。 Markdown 语法支持&#xff1a; 使用 Github Flavored Markdown 语法&#xff0c;简称 GFM 语法。支持表格、TOC、…

2023年云计算发展趋势:生活的智能未来

目录 引言1 智能家居的崭新时代2 无人驾驶的崭新时代3 虚拟现实的扩展与改进4 人工智能的综合应用5 云计算的可持续性结语 引言 时光荏苒&#xff0c;科技的飞速发展已经成为当今社会的标志之一。在这个数字化时代&#xff0c;云计算已经成为推动技术革新和生活方式改变的关键…

【深度学习】Python爬取豆瓣实现影评分析

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、任务描述二、使用步骤1.数据爬取1.2.对爬取的页面数据进行解析&#xff0c;并保存为JSON文件2、数据分析2.1数据分析之评论时间分布图2.2角色评分 前言 爬…

雷神学习---视音频数据处理入门:RGB、YUV像素数据处理

原文地址&#xff1a;https://blog.csdn.net/leixiaohua1020/article/details/50534150 ​​​​​​​​从代码可以看出&#xff0c;如果想把YUV格式像素数据变成灰度图像&#xff0c;只需要将U、V分量设置成128即可。 这是因为U、V是图像中的经过偏置处理的色度分量。色度分…

Python继承和多态:基础继承、方法重写、多态的基本概念

文章目录 基础继承方法重写多态在编程语言中,特别是在面向对象编程(OOP)中,继承和多态是两个核心概念,它们提供了代码重用和接口设计的强大工具。Python 作为一种支持 OOP 的语言,也具备这些特性。 基础继承 继承是面向对象编程的一个基本概念,它允许我们定义一个类(…

Angew. Chem. Int. Ed.:Co-1T-MoS2-bpe碱性介质中电催化HER

高效析氢反应&#xff08;HER&#xff09;电催化剂决定氢动力清洁技术的质量分布&#xff0c;但仍面临着巨大的挑战。基于此&#xff0c;中国石油大学&#xff08;华东&#xff09;董斌副教授等人报道了通过CoMo -金属-有机骨架前驱体对1T-MoS2催化剂进行了配体调制和Co掺杂的协…

前端图片压缩上传,减少等待时间!优化用户体检

添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; 这里有两张图片&#xff0c;它们表面看上去是一模一样的&#xff0c;但实际上各自所占用的内存大小相差了180倍。 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; 添加图片注释&…

JumpServer 打开RDP客户端出现由于在客户端检测到一个协议错误 错误代码 0x2104

环境: Win10 专业版 JumpServer Version v2.25.4 问题描述: JumpServer 打开RDP客户端出现由于在客户端检测到一个协议错误 错误代码 0x2104 个别电脑又是正常可以连接 解决方案: 检查网络连接:确保您的计算机和JumpServer之间的网络连接正常。尝试使用其他网络连接…

操作系统基础知识1

1、用户态和内核态的区别&#xff1f; a、访问权限。用户态下&#xff0c;应用程序只能访问受限的的资源和执行受限的程序&#xff1b;而在内核态下&#xff0c;操作系统具有完全的访问权限&#xff0c;可以访问系统的所有资源和执行所有操作&#xff1b; b、CPU指令集。用户态…

拍摄中的信息表达者---镜头语言!

炫我科技在影视动画行业为众多影片提供了渲染支持。涉及到的业务有云渲染系统、实时渲染系统、XR虚拟拍摄系统&#xff0c;强势赋能元宇宙。在影视动画创作过程中提供了多种解决方案&#xff0c;例如集群渲染、私有云渲染解决方案、虚拟拍摄等。 今天我们来聊一些不一样的东西…