pnpm i tui- image- editor
pnpm i tui- color- picker
< el- dialog v- model= "imgshow"  destroy- on- close width= "40%"  draggable align- center : show- close= "true" 
				: close- on- click- modal= "false" > 
				< template #header> 
					< div style= "display: flex; justify-content: space-between" > 
						< div style= "display: flex" > 
							< h3> 图片< / h3> 
						< / div> 
					< / div> 
				< / template> 
				< div class = "newBox" > 
					< ! --  图片处理框 -- > 
					< SignImage v- if = "cropperObj.cVisible"  : dialogVisible= "cropperObj.cVisible"  : title= "cropperObj.ctitle" 
						: imgUrl= "cropperObj.previewsImgUrl"  @getNewImg= "cropperObj.getNewImg" 
						@closeCropperDialog= "cropperObj.closeCropperView" > < / SignImage> 
				< / div> 
			< / el- dialog> 
import  {  SignImage }  from  './index' ; 
let  imgshow =  ref< boolean> ( false ) ; 
const  cropperObj =  reactive ( { 
	cVisible :  true ,  
	ctitle :  "" ,  
	previewsImgUrl :  "https://img0.baidu.com/it/u=2759734465,3558448225&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto?sec=1708621200&t=acc59373dca9b8658e33e14a974f6c47" ,  
	
	openCropperView :  ( )  =>  { 
		cropperObj. ctitle =  "图片处理" 
		cropperObj. cVisible =  true 
	} , 
	
	closeCropperView :  ( data )  =>  { 
		cropperObj. cVisible =  false 
	} , 
	
	getNewImg :  ( val :  any)  =>  { 
		console. log ( "val" ,  val) ; 
		pictureAnnotationFun ( val) 
	} 
} ) 
async  function  pictureAnnotationFun ( file :  any)  { 
	const  formData =  new  FormData ( ) 
	formData. append ( "file" ,  file) ; 
	formData. append ( 'id' ,  xx) ; 
	formData. append ( 'bid' ,  xxx) ; 
	try  { 
		const  {  code,  msg,  data }  =  await  uploadimg ( formData) ; 
		if  ( code >  0 )  { 
			cropperObj. cVisible =  false ; 
			ElMessage ( { 
				showClose :  true , 
				message :  '上传成功' , 
				type :  'success' , 
			} ) ; 
		}  else  { 
			ElMessage ( { 
				showClose :  true , 
				message :  '上传失败'  +  msg, 
				type :  'error' , 
				duration :  0 , 
			} ) ; 
		} 
	}  catch  ( e)  { 
		ElMessage ( { 
			showClose :  true , 
			message :  '错误: '  +  e. message, 
			type :  'error' , 
			duration :  0 , 
		} ) ; 
	} 
} 
< template> 
	< div class = "drawing-container" > 
		< ! --  绘图组件容器DOM  -- > 
		< div id= "tui-image-editor" > < / div> 
		< el- button class = "save"  type= "primary"  @click= "save" > 保存< / el- button> 
	< / div> 
< / template> 
< script setup lang= "ts" > 
import  'tui-image-editor/dist/tui-image-editor.css' ; 
import  'tui-color-picker/dist/tui-color-picker.css' ; 
import  ImageEditor from  'tui-image-editor' ; 
import  {  nextTick,  onMounted,  ref }  from  'vue' ; 
const  locale_zh =  { 
	ZoomIn :  '放大' , 
	ZoomOut :  '缩小' , 
	Hand :  '手掌' , 
	History :  '历史' , 
	Resize :  '调整宽高' , 
	Crop :  '裁剪' , 
	DeleteAll :  '全部删除' , 
	Delete :  '删除' , 
	Undo :  '撤销' , 
	Redo :  '反撤销' , 
	Reset :  '重置' , 
	Flip :  '镜像' , 
	Rotate :  '旋转' , 
	Draw :  '画' , 
	Shape :  '形状标注' , 
	Icon :  '图标标注' , 
	Text :  '文字标注' , 
	Mask :  '遮罩' , 
	Filter :  '滤镜' , 
	Bold :  '加粗' , 
	Italic :  '斜体' , 
	Underline :  '下划线' , 
	Left :  '左对齐' , 
	Center :  '居中' , 
	Right :  '右对齐' , 
	Color :  '颜色' , 
	'Text size' :  '字体大小' , 
	Custom :  '自定义' , 
	Square :  '正方形' , 
	Apply :  '应用' , 
	Cancel :  '取消' , 
	'Flip X' :  'X 轴' , 
	'Flip Y' :  'Y 轴' , 
	Range :  '区间' , 
	Stroke :  '描边' , 
	Fill :  '填充' , 
	Circle :  '圆' , 
	Triangle :  '三角' , 
	Rectangle :  '矩形' , 
	Free :  '曲线' , 
	Straight :  '直线' , 
	Arrow :  '箭头' , 
	'Arrow-2' :  '箭头2' , 
	'Arrow-3' :  '箭头3' , 
	'Star-1' :  '星星1' , 
	'Star-2' :  '星星2' , 
	Polygon :  '多边形' , 
	Location :  '定位' , 
	Heart :  '心形' , 
	Bubble :  '气泡' , 
	'Custom icon' :  '自定义图标' , 
	'Load Mask Image' :  '加载蒙层图片' , 
	Grayscale :  '灰度' , 
	Blur :  '模糊' , 
	Sharpen :  '锐化' , 
	Emboss :  '浮雕' , 
	'Remove White' :  '除去白色' , 
	Distance :  '距离' , 
	Brightness :  '亮度' , 
	Noise :  '噪音' , 
	'Color Filter' :  '彩色滤镜' , 
	Sepia :  '棕色' , 
	Sepia2 :  '棕色2' , 
	Invert :  '负片' , 
	Pixelate :  '像素化' , 
	Threshold :  '阈值' , 
	Tint :  '色调' , 
	Multiply :  '正片叠底' , 
	Blend :  '混合色' , 
	Width :  '宽度' , 
	Height :  '高度' , 
	'Lock Aspect Ratio' :  '锁定宽高比例' , 
} ; 
const  customTheme =  { 
	'common.bi.image' :  '' ,  
	'common.bisize.width' :  '0px' , 
	'common.bisize.height' :  '0px' , 
	'common.backgroundImage' :  'none' , 
	'common.backgroundColor' :  '#f3f4f6' , 
	'common.border' :  '1px solid #333' , 
	
	'header.backgroundImage' :  'none' , 
	'header.backgroundColor' :  '#f3f4f6' , 
	'header.border' :  '0px' , 
	
	'loadButton.backgroundColor' :  '#fff' , 
	'loadButton.border' :  '1px solid #ddd' , 
	'loadButton.color' :  '#222' , 
	'loadButton.fontFamily' :  'NotoSans, sans-serif' , 
	'loadButton.fontSize' :  '12px' , 
	'loadButton.display' :  'none' ,  
	
	'downloadButton.backgroundColor' :  '#fdba3b' , 
	'downloadButton.border' :  '1px solid #fdba3b' , 
	'downloadButton.color' :  '#fff' , 
	'downloadButton.fontFamily' :  'NotoSans, sans-serif' , 
	'downloadButton.fontSize' :  '12px' , 
	'downloadButton.display' :  'none' ,  
	
	'menu.normalIcon.color' :  '#8a8a8a' , 
	'menu.activeIcon.color' :  '#555555' , 
	'menu.disabledIcon.color' :  '#ccc' , 
	'menu.hoverIcon.color' :  '#e9e9e9' , 
	'submenu.normalIcon.color' :  '#8a8a8a' , 
	'submenu.activeIcon.color' :  '#e9e9e9' , 
	'menu.iconSize.width' :  '24px' , 
	'menu.iconSize.height' :  '24px' , 
	'submenu.iconSize.width' :  '32px' , 
	'submenu.iconSize.height' :  '32px' , 
	
	'submenu.backgroundColor' :  '#1e1e1e' , 
	'submenu.partition.color' :  '#858585' , 
	
	'submenu.normalLabel.color' :  '#858585' , 
	'submenu.normalLabel.fontWeight' :  'lighter' , 
	'submenu.activeLabel.color' :  '#fff' , 
	'submenu.activeLabel.fontWeight' :  'lighter' , 
	
	'checkbox.border' :  '1px solid #ccc' , 
	'checkbox.backgroundColor' :  '#fff' , 
	
	'range.pointer.color' :  '#fff' , 
	'range.bar.color' :  '#666' , 
	'range.subbar.color' :  '#d1d1d1' , 
	'range.disabledPointer.color' :  '#414141' , 
	'range.disabledBar.color' :  '#282828' , 
	'range.disabledSubbar.color' :  '#414141' , 
	'range.value.color' :  '#fff' , 
	'range.value.fontWeight' :  'lighter' , 
	'range.value.fontSize' :  '11px' , 
	'range.value.border' :  '1px solid #353535' , 
	'range.value.backgroundColor' :  '#151515' , 
	'range.title.color' :  '#fff' , 
	'range.title.fontWeight' :  'lighter' , 
	
	'colorpicker.button.border' :  '1px solid #1e1e1e' , 
	'colorpicker.title.color' :  '#fff' , 
} ; 
const  props =  defineProps ( { 
	dialogVisible :  { 
		type :  Boolean, 
		default :  ( )  =>  { 
			return  false ; 
		} , 
	} , 
	title :  { 
		type :  String, 
		default :  '' , 
	} , 
	imgUrl :  { 
		type :  String, 
		default :  '' , 
	} , 
} ) ; 
const  emit =  defineEmits ( [ 'closeCropperDialog' ,  'getNewImg' ] ) ; 
const  instance =  ref< any> ( null ) ; 
onMounted ( ( )  =>  { 
	nextTick ( ( )  =>  { 
		init ( ) ;  
	} ) ; 
} ) ; 
const  closeDialog  =  ( )  =>  { 
	emit ( 'closeCropperDialog' ) ; 
	
} ; 
const  init  =  ( )  =>  { 
	instance. value =  new  ImageEditor ( document. querySelector ( '#tui-image-editor' ) ,  { 
		includeUI :  { 
			loadImage :  { 
				path :  props. imgUrl, 
				name :  'image' , 
			} , 
			menu :  [ 'resize' ,  'crop' ,  'rotate' ,  'draw' ,  'shape' ,  'icon' ,  'text' ,  'filter' ] ,  
			initMenu :  'draw' ,  
			menuBarPosition :  'bottom' ,  
			locale :  locale_zh,  
			theme :  customTheme,  
		} , 
		cssMaxWidth :  400 ,  
		cssMaxHeight :  500 ,  
	} ) ; 
	document. getElementsByClassName ( 'tui-image-editor-main' ) [ 0 ] . style. top =  '45px' ;  
	document. getElementsByClassName ( 'tie-btn-reset tui-image-editor-item help' ) [ 0 ] . style. display =  'none' ;  
} ; 
const  save  =  async  ( )  =>  { 
	const  base64String =  instance. value. toDataURL ( ) ;  
	const  data =  window. atob ( base64String. split ( ',' ) [ 1 ] ) ; 
	const  ia =  new  Uint8Array ( data. length) ; 
	for  ( let  i =  0 ;  i <  data. length;  i++ )  { 
		ia[ i]  =  data. charCodeAt ( i) ; 
	} 
	const  file =  new  File ( [ ia] ,  "打标.png" ,  {  type :  "image/png"  } ) ; 
	emit ( 'getNewImg' ,  file) ; 
} ; 
< / script> 
< style lang= "scss"  scoped> 
. drawing- container { 
	width :  100 % ; 
	height :  80vh; 
	position :  relative; 
	. save { 
		position :  absolute; 
		right :  50px; 
		top :  15px; 
	} 
} 
< / style>