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; //光标默认速度=>默认250msdieTime?: number; //打字结束后光标消失时间=>默认200msblinkSeed?: number; //光标闪烁速度
};
export type TypingEffectType = {text: string; //文本seed?: number; //默认打字速度,默认250mscallback?: () => 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/>=>falsewhile (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.hqwc.cn/news/168392.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Git】GUI图形化界面的使用SSH协议IDEA集成Git

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于Git的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一. GUI图形化界面的使用 1.使用Gui​ 2.常…

2023年【北京市安全员-C3证】考试题库及北京市安全员-C3证在线考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 北京市安全员-C3证考试题库是安全生产模拟考试一点通总题库中生成的一套北京市安全员-C3证在线考试&#xff0c;安全生产模拟考试一点通上北京市安全员-C3证作业手机同步练习。2023年【北京市安全员-C3证】考试题库及…

抢量双11!抖音商城「官方立减」 缘何成为“爆单神器”?

10月20日抖音商城双11好物节正式开跑&#xff0c;仅仅三天&#xff0c;抖音商城整体GMV对比去年同期提升了200%&#xff0c;而在开跑一周后&#xff0c;一些品牌的销售额已经超过了今年整个618&#xff0c;可谓增势迅猛。其中&#xff0c;平台官方特别推出的「官方立减」玩法&a…

【数据结构】单链表OJ题(一)

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f3a5;系列专栏&#xff1a;《C语言》 《数据结构》 《Linux》《Cpolar》 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 文章目录 前言一、移除链表元素二、寻找链表中间结点三、输出链表倒数第k个结点四、反转单链表五…

Sprint Boot 学习路线 4

微服务 Spring Microservices是一个框架&#xff0c;它使用Spring框架更容易地构建和管理基于微服务的应用程序。微服务是一种架构风格&#xff0c;其中一个大型应用程序被构建为一组小型、独立可部署的服务。每个服务具有明确定义的职责&#xff0c;并通过API与其他服务通信。…

好物周刊#30:Github 上大学

https://github.com/cunyu1943/JavaPark https://yuque.com/cunyu1943 村雨遥的好物周刊&#xff0c;记录每周看到的有价值的信息&#xff0c;主要针对计算机领域&#xff0c;每周五发布。 一、项目 1. Fighting Design 一款灵活、优质的组件库&#xff0c;可在 vue3 应用程…

记事本简单运行java代码,理解程序运行

1.记事本创建一个文件, 把后缀.txt改为.java 如果没有显示尾缀, 勾选出文件扩展名 2.在文件里面编辑java代码并保存 3.在当前目录打开cmd 4.执行 javac Test.java 会生成好编译的.class文件 5.执行下面代码, 就成功得到j编写ava打印的代码 java Test 6.注意上面的中文在cmd中…

[autojs]用户界面GUI编程

用户界面: UI视图: View attr(name, value)attr(name)whidgravitylayout_gravitymarginmarginLeftmarginRightmarginTopmarginBottompaddingpaddingLeftpaddingRightpaddingToppaddingBottombgalphaforegroundminHeightminWidthvisibilityrotationtransformPivotXtransformPivo…

uni-app基于vite和vue3创建并集成pinia实现数据持久化

一、uni-app基于Vite和Vue3创建并集成pinia实现数据持久化 文章目录 一、uni-app基于Vite和Vue3创建并集成pinia实现数据持久化1.如何创建基于Vite和Vue3的uni-app项目&#xff1f;2.选择其中一个分支&#xff0c;就是一个脚手架 二、以下都是基于vite-ts版本创建和配置1.目录结…

数据结构与算法-(11)---有序表(OrderedList)

&#x1f308;个人主页: Aileen_0v0 &#x1f525;系列专栏:PYTHON学习系列专栏 &#x1f4ab;"没有罗马,那就自己创造罗马~" 目录 知识回顾及总结 有序表的引入 ​编辑 实现有序表 1.有序表-类的构造方法 2.有序表-search方法的实现 3.有序表-add方法的实现…

【编程语言发展史】Go语言的发展历史

目录 Go的起源 Go语言发展时间轴 logo Go的起源 Go 语言起源 2007 年&#xff0c;并于 2009 年正式对外发布。它从 2009 年 9 月 21 日开始作为谷歌公司 20% 兼职项目&#xff0c;即相关员工利用 20% 的空余时间来参与 Go 语言的研发工作。该项目的三位领导者均是著名的 …

剑指JUC原理-16.读写锁

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring源码、JUC源码&#x1f525;如果感觉博主的文章还不错的话&#xff0c;请&#x1f44d;三连支持&…