@@ -84,6 +84,8 @@ import {
8484 Equal ,
8585 Save ,
8686} from "lucide-react" ;
87+ import { Image as TauriImage } from "@tauri-apps/api/image" ;
88+ import { writeImage } from "@tauri-apps/plugin-clipboard-manager" ;
8789import { useTranslation } from "react-i18next" ;
8890import { toast } from "sonner" ;
8991import tailwindColors from "tailwindcss/colors" ;
@@ -1025,67 +1027,137 @@ export default function MyContextMenuContent() {
10251027
10261028 { /* 存在选中 ImageNode */ }
10271029 { p . stageManager . getSelectedEntities ( ) . filter ( ( it ) => it instanceof ImageNode ) . length > 0 && (
1028- < Item
1029- onClick = { async ( ) => {
1030- // 检查是否是草稿模式
1031- if ( p . isDraft ) {
1032- toast . error ( "请先保存项目后再导出图片" ) ;
1033- return ;
1034- }
1030+ < >
1031+ < Item
1032+ onClick = { async ( ) => {
1033+ // 获取所有选中的 ImageNode
1034+ const selectedImageNodes = p . stageManager
1035+ . getSelectedEntities ( )
1036+ . filter ( ( it ) => it instanceof ImageNode ) as ImageNode [ ] ;
10351037
1036- // 获取所有选中的 ImageNode
1037- const selectedImageNodes = p . stageManager
1038- . getSelectedEntities ( )
1039- . filter ( ( it ) => it instanceof ImageNode ) as ImageNode [ ] ;
1038+ if ( selectedImageNodes . length === 0 ) {
1039+ toast . error ( "请选中图片节点" ) ;
1040+ return ;
1041+ }
10401042
1041- if ( selectedImageNodes . length === 0 ) {
1042- toast . error ( "请选中图片节点" ) ;
1043- return ;
1044- }
1043+ // 复制第一张图片到剪贴板(如果有多张图片,只复制第一张)
1044+ const imageNode = selectedImageNodes [ 0 ] ;
1045+ const blob = p . attachments . get ( imageNode . attachmentId ) ;
1046+ if ( blob ) {
1047+ try {
1048+ const arrayBuffer = await blob . arrayBuffer ( ) ;
1049+ const tauriImage = await TauriImage . fromBytes ( new Uint8Array ( arrayBuffer ) ) ;
1050+ await writeImage ( tauriImage ) ;
1051+ if ( selectedImageNodes . length === 1 ) {
1052+ toast . success ( "已将选中的图片复制到系统剪贴板" ) ;
1053+ } else {
1054+ toast . success ( `已将第1张图片复制到系统剪贴板(共${ selectedImageNodes . length } 张)` ) ;
1055+ }
1056+ } catch ( error ) {
1057+ console . error ( "复制图片到剪贴板失败:" , error ) ;
1058+ toast . error ( "复制图片到剪贴板失败" ) ;
1059+ }
1060+ } else {
1061+ toast . error ( "无法获取图片数据" ) ;
1062+ }
1063+ } }
1064+ >
1065+ < Clipboard />
1066+ 复制图片到系统剪贴板
1067+ </ Item >
1068+ < Item
1069+ onClick = { ( ) => {
1070+ // 获取所有选中的 ImageNode
1071+ const selectedImageNodes = p . stageManager
1072+ . getSelectedEntities ( )
1073+ . filter ( ( it ) => it instanceof ImageNode ) as ImageNode [ ] ;
10451074
1046- // 根据图片数量决定提示信息
1047- const isBatch = selectedImageNodes . length > 1 ;
1048- const promptMessage = isBatch
1049- ? `请输入文件名(不含扩展名,将为 ${ selectedImageNodes . length } 张图片添加数字后缀)`
1050- : `请输入文件名(不含扩展名,将自动添加扩展名)` ;
1075+ if ( selectedImageNodes . length === 0 ) {
1076+ toast . error ( "请选中图片节点" ) ;
1077+ return ;
1078+ }
10511079
1052- // 弹出输入框 - 只弹出一次
1053- const fileName = await Dialog . input ( "另存图片" , promptMessage , {
1054- placeholder : "image" ,
1055- } ) ;
1080+ // 对每张图片进行红蓝通道对调
1081+ for ( const imageNode of selectedImageNodes ) {
1082+ imageNode . swapRedBlueChannels ( ) ;
1083+ }
10561084
1057- if ( ! fileName ) {
1058- return ; // 用户取消
1059- }
1085+ // 记录历史步骤
1086+ p . historyManager . recordStep ( ) ;
10601087
1061- // 验证文件名是否合法
1062- const invalidChars = / [ / \\ : * ? " < > | ] / ;
1063- if ( invalidChars . test ( fileName ) ) {
1064- toast . error ( '文件名包含非法字符:/ \\ : * ? " < > |' ) ;
1065- return ;
1066- }
1088+ // 显示提示信息
1089+ if ( selectedImageNodes . length === 1 ) {
1090+ toast . success ( "已对调图片的红蓝通道" ) ;
1091+ } else {
1092+ toast . success ( `已对调 ${ selectedImageNodes . length } 张图片的红蓝通道` ) ;
1093+ }
1094+ } }
1095+ >
1096+ < ArrowLeftRight />
1097+ 对调图片红蓝通道
1098+ </ Item >
1099+ < Item
1100+ onClick = { async ( ) => {
1101+ // 检查是否是草稿模式
1102+ if ( p . isDraft ) {
1103+ toast . error ( "请先保存项目后再导出图片" ) ;
1104+ return ;
1105+ }
1106+
1107+ // 获取所有选中的 ImageNode
1108+ const selectedImageNodes = p . stageManager
1109+ . getSelectedEntities ( )
1110+ . filter ( ( it ) => it instanceof ImageNode ) as ImageNode [ ] ;
1111+
1112+ if ( selectedImageNodes . length === 0 ) {
1113+ toast . error ( "请选中图片节点" ) ;
1114+ return ;
1115+ }
10671116
1068- // 调用工具函数导出图片
1069- const { successCount, failedCount } = await exportImagesToProjectDirectory (
1070- selectedImageNodes ,
1071- p . uri . fsPath ,
1072- p . attachments ,
1073- fileName ,
1074- ) ;
1117+ // 根据图片数量决定提示信息
1118+ const isBatch = selectedImageNodes . length > 1 ;
1119+ const promptMessage = isBatch
1120+ ? `请输入文件名(不含扩展名,将为 ${ selectedImageNodes . length } 张图片添加数字后缀)`
1121+ : `请输入文件名(不含扩展名,将自动添加扩展名)` ;
10751122
1076- // 显示结果提示
1077- if ( successCount > 0 && failedCount === 0 ) {
1078- toast . success ( `成功保存 ${ successCount } 张图片` ) ;
1079- } else if ( successCount > 0 && failedCount > 0 ) {
1080- toast . warning ( `成功保存 ${ successCount } 张图片,${ failedCount } 张失败` ) ;
1081- } else {
1082- toast . error ( `保存失败,请检查文件名或文件权限` ) ;
1083- }
1084- } }
1085- >
1086- < Save />
1087- 另存图片到当前prg所在目录下
1088- </ Item >
1123+ // 弹出输入框 - 只弹出一次
1124+ const fileName = await Dialog . input ( "另存图片" , promptMessage , {
1125+ placeholder : "image" ,
1126+ } ) ;
1127+
1128+ if ( ! fileName ) {
1129+ return ; // 用户取消
1130+ }
1131+
1132+ // 验证文件名是否合法
1133+ const invalidChars = / [ / \\ : * ? " < > | ] / ;
1134+ if ( invalidChars . test ( fileName ) ) {
1135+ toast . error ( '文件名包含非法字符:/ \\ : * ? " < > |' ) ;
1136+ return ;
1137+ }
1138+
1139+ // 调用工具函数导出图片
1140+ const { successCount, failedCount } = await exportImagesToProjectDirectory (
1141+ selectedImageNodes ,
1142+ p . uri . fsPath ,
1143+ p . attachments ,
1144+ fileName ,
1145+ ) ;
1146+
1147+ // 显示结果提示
1148+ if ( successCount > 0 && failedCount === 0 ) {
1149+ toast . success ( `成功保存 ${ successCount } 张图片` ) ;
1150+ } else if ( successCount > 0 && failedCount > 0 ) {
1151+ toast . warning ( `成功保存 ${ successCount } 张图片,${ failedCount } 张失败` ) ;
1152+ } else {
1153+ toast . error ( `保存失败,请检查文件名或文件权限` ) ;
1154+ }
1155+ } }
1156+ >
1157+ < Save />
1158+ 另存图片到当前prg所在目录下
1159+ </ Item >
1160+ </ >
10891161 ) }
10901162 </ Content >
10911163 ) ;
0 commit comments