新增: 资源管理器文件粘贴到文件列表
- 后端 ReadClipboardFiles 读系统剪贴板 CF_HDROP 拿路径列表 - Ctrl+V 优先粘贴系统剪贴板文件,复用拖入导入链路(SFTP 上传/本机复制/目录递归) - 补齐设计文档记录的历史缺口(Wails 限制未做)
This commit is contained in:
@@ -0,0 +1,71 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
procIsClipboardFormatAvailable = windows.NewLazySystemDLL("user32.dll").NewProc("IsClipboardFormatAvailable")
|
||||||
|
procOpenClipboard = windows.NewLazySystemDLL("user32.dll").NewProc("OpenClipboard")
|
||||||
|
procCloseClipboard = windows.NewLazySystemDLL("user32.dll").NewProc("CloseClipboard")
|
||||||
|
procGetClipboardData = windows.NewLazySystemDLL("user32.dll").NewProc("GetClipboardData")
|
||||||
|
procDragQueryFileW = windows.NewLazySystemDLL("shell32.dll").NewProc("DragQueryFileW")
|
||||||
|
)
|
||||||
|
|
||||||
|
// cfHDrop 剪贴板文件列表格式(资源管理器复制文件/文件夹),Win32 稳定 ABI 值
|
||||||
|
const cfHDrop = 15
|
||||||
|
|
||||||
|
// ReadClipboardFiles 读取系统剪贴板中的文件/文件夹路径列表。
|
||||||
|
// 剪贴板为文本/截图等非文件内容时返回空列表,前端据此回落图片粘贴等原有链路。
|
||||||
|
func (a *App) ReadClipboardFiles() ([]string, error) {
|
||||||
|
// OpenClipboard 与 CloseClipboard 必须同线程,否则剪贴板死锁
|
||||||
|
runtime.LockOSThread()
|
||||||
|
defer runtime.UnlockOSThread()
|
||||||
|
|
||||||
|
if r, _, _ := procIsClipboardFormatAvailable.Call(cfHDrop); r == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 剪贴板是全局互斥资源,短重试避开其他进程的瞬时占用
|
||||||
|
opened := false
|
||||||
|
for i := 0; i < 20; i++ {
|
||||||
|
if r, _, _ := procOpenClipboard.Call(0); r != 0 {
|
||||||
|
opened = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
if !opened {
|
||||||
|
return nil, syscall.EBUSY
|
||||||
|
}
|
||||||
|
defer procCloseClipboard.Call()
|
||||||
|
|
||||||
|
h, _, _ := procGetClipboardData.Call(cfHDrop)
|
||||||
|
if h == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
count, _, _ := procDragQueryFileW.Call(h, 0xFFFFFFFF, 0, 0)
|
||||||
|
if count == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
paths := make([]string, 0, count)
|
||||||
|
for i := uintptr(0); i < count; i++ {
|
||||||
|
n, _, _ := procDragQueryFileW.Call(h, i, 0, 0)
|
||||||
|
if n == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
buf := make([]uint16, n+1)
|
||||||
|
procDragQueryFileW.Call(h, i, uintptr(unsafe.Pointer(&buf[0])), n+1)
|
||||||
|
paths = append(paths, syscall.UTF16ToString(buf))
|
||||||
|
}
|
||||||
|
return paths, nil
|
||||||
|
}
|
||||||
@@ -491,6 +491,16 @@ export function PickDirectory(): $CancellablePromise<string> {
|
|||||||
return $Call.ByID(1347829059);
|
return $Call.ByID(1347829059);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ReadClipboardFiles 读取系统剪贴板中的文件/文件夹路径列表。
|
||||||
|
* 剪贴板为文本/截图等非文件内容时返回空列表,前端据此回落图片粘贴等原有链路。
|
||||||
|
*/
|
||||||
|
export function ReadClipboardFiles(): $CancellablePromise<string[]> {
|
||||||
|
return $Call.ByID(2636780132).then(($result: any) => {
|
||||||
|
return $$createType7($result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ReadFile 读取文件
|
* ReadFile 读取文件
|
||||||
*/
|
*/
|
||||||
@@ -797,3 +807,4 @@ const $$createType3 = filesystem$0.FileOperationResult.createFrom;
|
|||||||
const $$createType4 = $Create.Nullable($$createType3);
|
const $$createType4 = $Create.Nullable($$createType3);
|
||||||
const $$createType5 = $Create.Array($$createType2);
|
const $$createType5 = $Create.Array($$createType2);
|
||||||
const $$createType6 = $Create.Map($Create.Any, $Create.Any);
|
const $$createType6 = $Create.Map($Create.Any, $Create.Any);
|
||||||
|
const $$createType7 = $Create.Array($Create.Any);
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
|||||||
import { getPathSeparator } from '@/utils/fileUtils'
|
import { getPathSeparator } from '@/utils/fileUtils'
|
||||||
import { Message, Modal } from '@arco-design/web-vue'
|
import { Message, Modal } from '@arco-design/web-vue'
|
||||||
import { On, Off } from '@wailsio/events'
|
import { On, Off } from '@wailsio/events'
|
||||||
|
import { ReadClipboardFiles } from '@bindings/u-desk/app'
|
||||||
import { renderMarkdown, parseMarkdown, isMarkedReady, preloadMarked, renderMermaidDiagrams, rerenderMermaidDiagrams, setCurrentFileDir, setFileServerBase } from '@/utils/markedExtensions'
|
import { renderMarkdown, parseMarkdown, isMarkedReady, preloadMarked, renderMermaidDiagrams, rerenderMermaidDiagrams, setCurrentFileDir, setFileServerBase } from '@/utils/markedExtensions'
|
||||||
import { useThemeStore } from '@/stores/theme'
|
import { useThemeStore } from '@/stores/theme'
|
||||||
import { useTransferStore } from '@/stores/transfer'
|
import { useTransferStore } from '@/stores/transfer'
|
||||||
@@ -2023,6 +2024,17 @@ const handlePaste = async (event: ClipboardEvent) => {
|
|||||||
const target = event.target as HTMLElement
|
const target = event.target as HTMLElement
|
||||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) return
|
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) return
|
||||||
|
|
||||||
|
// 系统剪贴板文件(资源管理器复制,文件+文件夹)优先:
|
||||||
|
// 后端读 CF_HDROP 拿真实路径(DOM paste 拿不到文件夹),复用拖入导入链路
|
||||||
|
try {
|
||||||
|
const sysPaths = await ReadClipboardFiles()
|
||||||
|
if (sysPaths && sysPaths.length > 0) {
|
||||||
|
event.preventDefault()
|
||||||
|
await handleExternalDrop(sysPaths)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch { /* 剪贴板被占用等,回落图片粘贴 */ }
|
||||||
|
|
||||||
const items = event.clipboardData?.items
|
const items = event.clipboardData?.items
|
||||||
if (!items) return
|
if (!items) return
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user