react中使用react-konva實現畫板框選內容

文章目錄

  • 一、前言
    • 1.1、`API`文檔
    • 1.2、`Github`倉庫
  • 二、圖形
    • 2.1、拖拽`draggable`
    • 2.2、圖片`Image`
    • 2.3、變形`Transformer`
  • 三、實現
    • 3.1、依賴
    • 3.2、源碼
      • 3.2.1、`KonvaContainer`組件
      • 3.2.2、`use-key-press`文件
    • 3.3、效果圖
  • 四、最后

一、前言

本文用到的react-konva是基于react封裝的圖形繪制。Konva 是一個HTML5 Canvas JavaScript 框架,它通過對 2d context 的擴展實現了在桌面端和移動端的可交互性。Konva 提供了高性能的動畫,補間,節點嵌套,布局,濾鏡,緩存,事件綁定(桌面/移動端)等等功能。你可以使用 Konva 在舞臺上繪制圖形,給圖形添加事件,移動、縮放和旋轉圖形并且支持高性能的動畫即使包含數千個圖形。

1.1、API文檔

英文文檔點擊【前往】,中文文檔點擊【前往】

1.2、Github倉庫

點擊【前往】訪問Github倉庫,在線示例地址,點擊【前往】

二、圖形

在線制圖最基礎的應用是拖拽元素,比如,在畫布上拖拽一張圖片或某種形狀,對該圖片進行縮放或旋轉操作。

畫布就是<Stage>,每個圖層為<Layer>

2.1、拖拽draggable

konva 中內置了很多形狀的元素,比如圓形、矩形等,以下示例為星型,這里先用<Star>試一下:

import Konva from 'konva'
import { Circle, Rect, Stage, Layer, Text, Star } from 'react-konva'const Shape = () => {const [star, setStar] = useState({x: 300,y: 300,rotation: 20,isDragging: false,})const handleDragStart = () => {setStar({...star,isDragging: true,})}const handleDragEnd = (e: any) => {setStar({...star,x: e.target.x(),y: e.target.y(),isDragging: false,})}return (<Stage width={1000} height={600}><Layer><Starkey="starid"id="starid"x={star.x}y={star.y}numPoints={5}innerRadius={20}outerRadius={40}fill="#89b717"opacity={0.8}draggablerotation={star.rotation}shadowColor="black"shadowBlur={10}shadowOpacity={0.6}shadowOffsetX={star.isDragging ? 10 : 5}shadowOffsetY={star.isDragging ? 10 : 5}scaleX={star.isDragging ? 1.2 : 1}scaleY={star.isDragging ? 1.2 : 1}onDragStart={handleDragStart}onDragEnd={handleDragEnd}/></Layer></Stage>)
}

其中,可以給 Star 配置一些基礎的屬性,如:xy 指該元素在畫布上的坐標位置,rotaition 指元素的旋轉角度;fill 指元素的填充顏色,scaleXscaleY 指元素在 xy 軸上的放大比例等等。

在拖拽的時候,我們要給該元素添加一些拖拽事件,如上:添加 handleDragStart 更改isDragging屬性,使其在拖動時產生形變;添加 onDragEnd 事件,更改isDraggingxy 屬性,來改變拖動位置,關閉拖動形變特效等。

觀察上面的代碼發現某些屬性和react-dnd類似,但在使用 drag 事件的時候,發現比 react-dnd 方便很多,可能因為底層是 canvas 的原因吧!

2.2、圖片Image

有兩種方式可以導入圖片,一個是用 react-hooks,一個是調用 react 生命周期函數,這里為了圖省事,用 hooks

先安裝 konva 的官方庫use-imageuse-image提供好了跨域屬性anonymous,封裝一下圖片組件:

import { Image } from 'react-konva'
import useImage from 'use-image'const KonvaImage = ({ url = '' }) => {const [image] = useImage(url, 'anonymous')return <Image image={image} />
}export default KonvaImage

如果仍顯示跨域問題不能生成圖片,需要在服務器端添加跨域頭或者做一層轉發了。

2.3、變形Transformer

元素變形,需要引用 konvaTransformer組件,該組件可以使元素的縮放、旋轉。如下代碼,在選中某元素后,會展示 Transformer 組件,在該組件上存在boundBoxFunc屬性,當用戶觸發元素的變形行為時,該函數會被調用,返回一個包含形變后元素的信息(下面代碼中為 newBox)。

import React, { useState, useEffect, useRef } from 'react'
import { Image, Transformer } from 'react-konva'
import Konva from 'konva'
import useImage from 'use-image'const KonvaImage = ({ url = '', isSelected = false }) => {const [image] = useImage(url)const imgRef = useRef()const trRef = useRef()useEffect(() => {if (isSelected) {trRef.current.nodes([imgRef.current])trRef.current.getLayer().batchDraw()}}, [isSelected])return (<><Image image={image} draggable ref={imgRef} />{isSelected && (<Transformerref={trRef}boundBoxFunc={(oldBox, newBox) => {// limit resizeif (newBox.width < 5 || newBox.height < 5) {return oldBox}const { width, height } = newBox// console.log('width', width);// console.log('height', height);return newBox}}/>)}</>)
}export default KonvaImage

三、實現

3.1、依賴

安裝如下所需依賴:

npm install react-konva konva use-image --save

3.2、源碼

3.2.1、KonvaContainer組件

KonvaContainer圖片框選區域組件源碼如下所示:

/*** @Description: KonvaContainer圖片框選區域組件* @props url 需要框選的圖片的URL地址* @props width 寬度* @props height 高度* @props defaultValue 默認框選起來區域的數據* @onChange 回調方法,通知父組件框選的內容信息* @author 小馬甲丫* @date 2023-12-05 03:22:27
*/
import React from 'react';
import useImage from 'use-image';
import { Stage, Layer, Rect, Image, Transformer } from 'react-konva';
import useKeyPress from '@/hooks/use-key-press';/*** 框選的圖片* @param url* @constructor*/
const BackgroundImage = ({ url }) => {const [image] = useImage(url);return <Image image={image} />;
};/*** 背景白板* @param width* @param height* @constructor*/
const BackgroundWhite = ({ width, height }) => {return (<Rectx={0}y={0}width={width}height={height}fill="#fff"id="rectangleBg"name="rectangleBg"/>);
};/*** 框選出來的框* @param canvas* @param shapeProps* @param onSelect* @param onChange* @constructor*/
const Rectangle = ({ canvas, shapeProps, onSelect, onChange }) => {const shapeRef = React.useRef();return (<RectonClick={() => onSelect(shapeRef)}onTap={() => onSelect(shapeRef)}ref={shapeRef}{...shapeProps}name="rectangle"draggableonMouseOver={() => {document.body.style.cursor = 'move';}}onMouseOut={() => {document.body.style.cursor = 'default';}}onDragEnd={(e) => {onChange({...shapeProps,x: e.target.x(),y: e.target.y(),});}}dragBoundFunc={(pos) => {const shapeWidth = shapeRef.current.attrs.width;const shapeHeight = shapeRef.current.attrs.height;let x = pos.x;if (x <= 0) {x = 0;} else if (x + shapeWidth >= canvas.width) {x = canvas.width - shapeWidth;}let y = pos.y;if (y < 0) {y = 0;} else if (y + shapeHeight > canvas.height) {y = canvas.height - shapeHeight;}return {x,y,};}}onTransformEnd={() => {// transformer is changing scale of the node// and NOT its width or height// but in the store we have only width and height// to match the data better we will reset scale on transform endconst node = shapeRef.current;const scaleX = node.scaleX();const scaleY = node.scaleY();// we will reset it backnode.scaleX(1);node.scaleY(1);onChange({...shapeProps,x: node.x(),y: node.y(),// set minimal valuewidth: Math.max(5, node.width() * scaleX),height: Math.max(node.height() * scaleY),});}}/>);
};/*** 主容器* @param props* @constructor*/
const KonvaContainer = (props) => {const [imageObject, setImageObject] = React.useState({width: props.width,height: props.height,url: props.url,});const [rectanglesField, setRectanglesField] = React.useState([]);const [selectedId, selectShape] = React.useState(null);const trRef = React.useRef();const layerRef = React.useRef();const Konva = window.Konva;const hideTransformer = () => {trRef.current.nodes([]);};/*** 初始化框選框* @param list*/const initRectangles = (list) => {const rects = list.map((item, index) => ({...item,id: `rect_${index}`,fill: 'rgb(160, 76,4, 0.3)',}));setRectanglesField(rects);};/*** 監聽prop值變換*/React.useEffect(() => {const {url = '',width = 0,height = 0,defaultValue = [],} = props || {};setImageObject({width,height,url,});hideTransformer();// 圖片地址不一致說明變更圖片,需要重置選框if (url !== imageObject.url) {setRectanglesField([]);selectShape(null);}initRectangles(defaultValue);}, [props.url, props.width, props.height, props.defaultValue]);/*** 更新框選框數據* @param rects*/const updateRectangles = (rects) => {setRectanglesField(rects);props.onChange(rects);};/*** 添加框選框*/const addRec = () => {const data = rectanglesField;const rects = data.slice();const id = `rect_${rects.length}`;rects[rects.length] = {id,...getSelectionObj(),};updateRectangles(rects);selectShape(id);};/*** 刪除框選框*/const delRec = () => {const data = rectanglesField;const rects = data.slice().filter((rect) => rect.id !== selectedId);updateRectangles(rects);hideTransformer();document.body.style.cursor = 'default';selectShape(null);};const selectionRectRef = React.useRef();const selection = React.useRef({visible: false,x1: 0,y1: 0,x2: 0,y2: 0,});/*** 高亮框選框* @param id*/const activeTransformer = (id) => {const activeRect =layerRef.current.find('.rectangle').find((elementNode) => elementNode.attrs.id === id) ||selectionRectRef.current;trRef.current.nodes([activeRect]);};/*** useKeyPress監聽鍵盤按鍵刪除鍵del和返回鍵backspace* 8  返回鍵* 46 刪除鍵*/useKeyPress([8, 46], (e) => {// disable click eventKonva.listenClickTap = false;if (e.target.style[0] === 'cursor') delRec();});/*** 獲取選中的框選框的信息*/const getSelectionObj = () => {return {x: Math.min(selection.current.x1, selection.current.x2),y: Math.min(selection.current.y1, selection.current.y2),width: Math.abs(selection.current.x1 - selection.current.x2),height: Math.abs(selection.current.y1 - selection.current.y2),fill: 'rgb(160, 76,4, 0.3)',};};/*** 更新框選框*/const updateSelectionRect = () => {const node = selectionRectRef.current;node.setAttrs({...getSelectionObj(),visible: selection.current.visible,});node.getLayer().batchDraw();};/*** 開始繪制框選框* @param e*/const onMouseDown = (e) => {const isTransformer = e.target.findAncestor('Transformer');if (isTransformer) {return;}hideTransformer();const pos = e.target.getStage().getPointerPosition();selection.current.visible = true;selection.current.x1 = pos.x;selection.current.y1 = pos.y;selection.current.x2 = pos.x;selection.current.y2 = pos.y;updateSelectionRect();};/*** 繪制框選框中* @param e*/const onMouseMove = (e) => {if (!selection.current.visible) {return;}const pos = e.target.getStage().getPointerPosition();selection.current.x2 = pos.x;selection.current.y2 = pos.y;updateSelectionRect();};/*** 結束繪制框選框* @param e*/const onMouseUp = (e) => {// 點擊Rect框時,會返回該Rect的id// 畫框時鼠標在Rect上松開,會返回該Rect的idconst dragId = e.target.getId();if (!selection.current.visible) {return;}// 是否鼠標拖動,并且偏移量大于10時才算拖動。拖動Rect沒有偏移量,畫框才有偏移量const { current: { x1 = 0, x2 = 0, y1 = 0, y2 = 0 } = {} } = selection || {};const isMove = (x1 !== x2 && Math.abs(x1 - x2) > 10) || (y1 !== y2 && Math.abs(y1 - y2) > 10);// 點擊后有拖動就添加Rect框,并且偏移量大于10時才算拖動if (isMove) {addRec();}// 設置可調節大小節點if (!!dragId && !isMove) {// 點擊已有的Rect框才設置,并且拖動小于10,也就是沒有拖動activeTransformer(dragId);} else if (isMove) {// 拖動大于10,生成新的Rect框activeTransformer();}selection.current.visible = false;// disable click eventKonva.listenClickTap = false;updateSelectionRect();};return (<Stagewidth={imageObject.width}height={imageObject.height}onMouseDown={onMouseDown}onMouseUp={onMouseUp}onMouseMove={onMouseMove}><Layer ref={layerRef}><BackgroundWhite {...imageObject} /><BackgroundImage {...imageObject} />{rectanglesField.map((rect, i) => {return (<Rectanglekey={i}getKey={i}canvas={imageObject}shapeProps={rect}isSelected={rect.id === selectedId}getLength={rectanglesField.length}onSelect={() => {selectShape(rect.id);}}onChange={(newAttrs) => {const rects = rectanglesField.slice();rects[i] = newAttrs;updateRectangles(rects);}}/>);})}<Transformerref={trRef}rotationSnaps={[0, 90, 180, 270]}keepRatio={false}anchorSize={4}anchorStroke='#a04c04'anchorFill="#fff"borderStroke='#a04c04'borderDash={[1, 1]}enabledAnchors={['top-left', 'top-right', 'bottom-left', 'bottom-right']}boundBoxFunc={(oldBox, newBox) => {// limit resize// newBox.rotation !== 0進入return oldBox,就可實現不讓旋轉if (newBox.width < 20 || newBox.height < 20) {return oldBox;}return newBox;}}/><Rect ref={selectionRectRef} /></Layer></Stage>);
};export default KonvaContainer;

3.2.2、use-key-press文件

用到了下面這個hook文件use-key-press

import { useCallback, useEffect, MutableRefObject } from 'react';type keyType = KeyboardEvent['keyCode'] | KeyboardEvent['key'];
type keyFilter = keyType | keyType[];
type EventHandler = (event: KeyboardEvent) => void;
type keyEvent = 'keydown' | 'keyup';
type BasicElement = HTMLElement | Element | Document | Window;
type TargetElement = BasicElement | MutableRefObject<null | undefined>;
type EventOptions = {events?: keyEvent[];target?: TargetElement;
};const modifierKey: any = {ctrl: (event: KeyboardEvent) => event.ctrlKey,shift: (event: KeyboardEvent) => event.shiftKey,alt: (event: KeyboardEvent) => event.altKey,meta: (event: KeyboardEvent) => event.metaKey,
};const defaultEvents: keyEvent[] = ['keydown'];/*** 判斷對象類型* @param obj 參數對象* @returns String*/
function isType<T>(obj: T): string {return Object.prototype.toString.call(obj).replace(/^\[object (.+)\]$/, '$1').toLowerCase();
}/*** 獲取當前元素* @param target TargetElement* @param defaultElement 默認綁定的元素*/
function getTargetElement(target?: TargetElement, defaultElement?: BasicElement) {if (!target) {return defaultElement;}if ('current' in target) {return target.current;}return target;
}/*** 按鍵是否激活* @param event 鍵盤事件* @param keyFilter 當前鍵*/
const keyActivated = (event: KeyboardEvent, keyFilter: any) => {const type = isType(keyFilter);const { keyCode } = event;if (type === 'number') {return keyCode === keyFilter;}const keyCodeArr = keyFilter.split('.');// 符合條件的長度let genLen = 0;// 組合鍵keyCodeArr.forEach((key) => {const genModifier = modifierKey[key];if ((genModifier && genModifier) || keyCode === key) {genLen++;}});return genLen === keyCodeArr.length;
};/*** 鍵盤按下預處理方法* @param event 鍵盤事件* @param keyFilter 鍵碼集*/
const genKeyFormate = (event: KeyboardEvent, keyFilter: any) => {const type = isType(keyFilter);if (type === 'string' || type === 'number') {return keyActivated(event, keyFilter);}// 多個鍵if (type === 'array') {return keyFilter.some((item: keyFilter) => keyActivated(event, item));}return false;
};/*** 監聽鍵盤按下/松開* @param keyCode* @param eventHandler* @param options*/
const useKeyPress = (keyCode: keyFilter,eventHandler?: EventHandler,options: EventOptions = {},
) => {const { target, events = defaultEvents } = options;const callbackHandler = useCallback((event) => {if (genKeyFormate(event, keyCode)) {typeof eventHandler === 'function' && eventHandler(event);}},[keyCode],);useEffect(() => {const el = getTargetElement(target, window)!;events.forEach((eventName) => {el.addEventListener(eventName, callbackHandler);});return () => {events.forEach((eventName) => {el.removeEventListener(eventName, callbackHandler);});};}, [keyCode, events, callbackHandler]);
};export default useKeyPress;

3.3、效果圖

頁面效果如下所示:

在這里插入圖片描述

四、最后

本人每篇文章都是一字一句碼出來,希望大佬們多提提意見。順手來個三連擊,點贊👍收藏💖關注?。創作不易,給我打打氣,加加油?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/211157.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/211157.shtml
英文地址,請注明出處:http://en.pswp.cn/news/211157.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

基礎課20——從0-1客服機器人生命周期

溫馨提示&#xff1a;篇幅較長&#xff0c;可點擊目錄查看對應節點。 1.機器人搭建期 搭建機器人包含&#xff1a;素材整理、問題提煉、相似問題補充、答案編輯、問題分配引擎等等步驟&#xff0c;不同廠商可能有所區別&#xff0c;但關鍵功能的實現離不開以下步驟。 1.1素材…

Flutter路由的幾種用法

Flutter路由跳轉 基本路由跳轉 ElevatedButton(onPressed: () {//基本路由跳轉Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {return const SearchPage();}),);},child: const Text("基本路由跳轉"), ), search.dart頁面 impo…

說說react的事件機制?

React的事件機制是一種用于處理用戶界面事件的方式&#xff0c;它建立在原生DOM事件的基礎上&#xff0c;提供了一種更高級、更一致的方式來處理事件。 1. 合成事件&#xff08;Synthetic Events&#xff09;&#xff1a;React引入了合成事件的概念&#xff0c;它是一種React自…

K8S學習指南(3)-minikube的安裝

這里寫自定義目錄標題 簡介Windows 系統安裝步驟 1&#xff1a;安裝 Hypervisor步驟 2&#xff1a;安裝 kubectl步驟 3&#xff1a;安裝 Minikube步驟 4&#xff1a;啟動 Minikube CentOS 系統安裝步驟 1&#xff1a;安裝 Hypervisor步驟 2&#xff1a;安裝 kubectl步驟 3&…

《形式語言與自動機理論(第4版)》筆記(三)

文章目錄 [toc]前導《形式語言與自動機理論&#xff08;第4版&#xff09;》筆記&#xff08;一&#xff09;《形式語言與自動機理論&#xff08;第4版&#xff09;》筆記&#xff08;二&#xff09; 第四章&#xff1a;正則表達式4.1|啟示4.2|正則表達式的形式定義正則表達式性…

排序算法之四:直接選擇排序

1.基本思想 每一次從待排序的數據元素中選出最小&#xff08;或最大&#xff09;的一個元素&#xff0c;存放在序列的起始位置&#xff0c;直到全部待排序的數據元素排完 。 2.直接選擇排序 在元素集合array[i]--array[n-1]中選擇關鍵碼最大(小)的數據元素 若它不是這組元素中的…

練習:最大公約數

1.什么是公約數 公約數&#xff0c;亦稱“公因數”。 它是指能同時整除幾個整數的數 。 如果一個整數同時是幾個整數的 約數 &#xff0c;稱這個整數為它們的“公約數”&#xff1b;公約數中最大的稱為最大公約數。 2.輾轉相除法 輾轉相除法之所以有效是因為其基于一個核心原…

給定有n個結點的樹和長度為n的排列,q次詢問:l, r, x, 若p[l, r]中存在至少一個結點是x的后代,輸出yes,否則輸出no

題目 #include<bits/stdc.h> using namespace std; const int maxn 1e6 5; int n, q; vector<int> G[maxn]; int L[maxn], R[maxn];//L[i]表示結點i的時間戳&#xff0c;R[i]表示結點i的后代中時間戳的最大值 int p[maxn]; int t[maxn]; struct Node{int id, fl…

MapReduce

1. 請解釋MapReduce的工作原理。 MapReduce是一種編程模型&#xff0c;主要用于大規模數據集&#xff08;特別是非結構化數據&#xff09;的并行處理。這個模型的核心思想是將大數據處理任務分解為兩個主要步驟&#xff1a;Map和Reduce。 在Map階段&#xff0c;輸入數據被分解…

ssm的健身房預約系統(有報告)。Javaee項目。ssm項目。

演示視頻&#xff1a; ssm的健身房預約系統&#xff08;有報告&#xff09;。Javaee項目。ssm項目。 項目介紹&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三層體系結構&#xff0c;通過Spring Spring…

AI模型平臺Hugging Face存在API令牌漏洞;大型語言模型與任務模型

&#x1f989; AI新聞 &#x1f680; AI模型平臺Hugging Face存在API令牌漏洞&#xff0c;黑客可竊取、修改模型 摘要&#xff1a;安全公司Lasso Security發現AI模型平臺Hugging Face上存在API令牌漏洞&#xff0c;黑客可獲取微軟、谷歌等公司的令牌&#xff0c;并能夠訪問模…

c++中的內聯函數和編譯器

內聯函數和編譯器&#xff1a; 內聯函數并不是何時何地都有效&#xff0c;為了理解內聯函數何時有效&#xff0c;應該要知道編譯器碰到內聯 函數會怎么處理&#xff1f; 對于任何類型的函數&#xff0c;編譯器會將函數類型(包括函數名字&#xff0c;參數類型&#xff0c;返回值…

Unknown parameter in InstanceGroups[0]: “Configurations“, must be ... 解決方法

使用 aws emr modify-instance-groups 更新集群配置時可能會遇到如下錯誤信息&#xff1a; Unknown parameter in InstanceGroups[0]: “Configurations”, must be one of: InstanceGroupId, InstanceCount, EC2InstanceIdsToTerminate, ShrinkPolicy 這一報錯其實和提供的j…

C語言進階之路之頂峰相見篇

目錄 一、學習目標 二、宏定義 預處理 宏的概念 帶參宏 無值宏定義 三、條件編譯 條件編譯 條件編譯的使用場景 四、頭文件 頭文件的作用 頭文件的內容 頭文件的基礎語句&#xff1a; GCC編譯器的4個編譯步驟&#xff1a; 總結 一、學習目標 掌握宏定義含義和用…

【Linux】系統初識之馮諾依曼體系結構與操作系統

&#x1f440;樊梓慕&#xff1a;個人主頁 &#x1f3a5;個人專欄&#xff1a;《C語言》《數據結構》《藍橋杯試題》《LeetCode刷題筆記》《實訓項目》《C》《Linux》 &#x1f31d;每一個不曾起舞的日子&#xff0c;都是對生命的辜負 目錄 前言 1.馮諾依曼體系結構 2.操作…

Springboot項目實現簡單的文件服務器,實現文件上傳+圖片及文件回顯

文章目錄 寫在前面一、配置1、application.properties2、webMvc配置3、查看效果 二、文件上傳 寫在前面 平常工作中的項目&#xff0c;上傳的文件一般都會傳到對象存儲云服務中。當接手一個小項目&#xff0c;如何自己動手搭建一個文件服務器&#xff0c;實現圖片、文件的回顯…

一篇文章帶你了解并使用mybatis框架

mybatis簡介&#xff1a; MyBatis 是一款優秀的持久層框架&#xff0c;它支持自定義 SQL、存儲過程以及高級映射。MyBatis 免除了幾乎所有的 JDBC 代碼以及設置參數和獲取結果集的工作。MyBatis 可以通過簡單的 XML 或注解來配置和映射原始類型、接口和 Java POJO&#xff08;P…

JavaScript中的發布訂閱和觀察者模式:如何優雅地處理事件和數據更新

?&#x1f308;個人主頁&#xff1a;前端青山 &#x1f525;系列專欄&#xff1a;JavaScript篇 &#x1f516;人終將被年少不可得之物困其一生 依舊青山,本期給大家帶來JavaScript篇專欄內容:JavaScript-訂閱觀察者模式 目錄 說說你對發布訂閱、觀察者模式的理解&#xff1f;…

用生命做事,無人能超越

今天看了《藝術人生——紅樓夢劇組20年再聚首》&#xff0c;然后搜索了一下里面的核心人物及其經歷。實話說&#xff0c;看完后我內心無法平靜&#xff0c;涌動著各種思緒。一是20多年前這群青澀演員的人生際遇&#xff0c;讓我感慨。很多人&#xff0c;用這樣的機會&#xff0…

‘ChatGLMTokenizer‘ object has no attribute ‘tokenizer‘解決方案

大家好,我是愛編程的喵喵。雙985碩士畢業,現擔任全棧工程師一職,熱衷于將數據思維應用到工作與生活中。從事機器學習以及相關的前后端開發工作。曾在阿里云、科大訊飛、CCF等比賽獲得多次Top名次。現為CSDN博客專家、人工智能領域優質創作者。喜歡通過博客創作的方式對所學的…