فهرست منبع

V1.0.14 拖拽复制

liuq 1 ماه پیش
والد
کامیت
7d06ddf466
2فایلهای تغییر یافته به همراه140 افزوده شده و 25 حذف شده
  1. 25 14
      src/renderer/src/App.tsx
  2. 115 11
      src/renderer/src/pages/ChatPage.tsx

+ 25 - 14
src/renderer/src/App.tsx

@@ -862,20 +862,31 @@ function App(): JSX.Element {
             className="context-menu" 
             style={{ top: contextMenu.y, left: contextMenu.x }}
           >
-            <div className="context-menu-item" onClick={() => { alert('已复制'); setContextMenu(null) }}>复制</div>
-            <div className="context-menu-item" onClick={() => { alert('已转发'); setContextMenu(null) }}>转发</div>
-            <div className="context-menu-item" onClick={() => { alert('已收藏'); setContextMenu(null) }}>收藏</div>
-            <div style={{ height: '1px', backgroundColor: '#eee', margin: '4px 0' }}></div>
-            <div className="context-menu-item" onClick={() => { 
-              if (activeContactId) {
-                setMessages(prev => ({
-                  ...prev,
-                  [activeContactId]: (prev[activeContactId] || []).filter(m => m.id !== contextMenu.msgId)
-                }))
-              }
-              setContextMenu(null)
-            }}>删除</div>
-            <div className="context-menu-item" onClick={() => { alert('撤回功能开发中'); setContextMenu(null) }}>撤回</div>
+            <div
+              className="context-menu-item"
+              onClick={async () => {
+                if (activeContactId != null && contextMenu) {
+                  const list = messages[activeContactId] || []
+                  const msg = list.find(m => m.id === contextMenu.msgId)
+                  if (msg) {
+                    const text =
+                      msg.type === 'text' || msg.type === 'notification'
+                        ? msg.content
+                        : [msg.title, msg.content].filter(Boolean).join('\n')
+                    try {
+                      await navigator.clipboard.writeText(text)
+                    } catch (err) {
+                      logger.error('App: copy message to clipboard failed', {
+                        error: err instanceof Error ? err.message : String(err)
+                      })
+                    }
+                  }
+                }
+                setContextMenu(null)
+              }}
+            >
+              复制
+            </div>
           </div>
         </>
       )}

+ 115 - 11
src/renderer/src/pages/ChatPage.tsx

@@ -29,6 +29,41 @@ interface ChatPageProps {
   onLoadMoreMessages: (contactId: number) => void
 }
 
+const MAX_FILE_SIZE_BYTES = 50 * 1024 * 1024
+
+function isFileSizeAllowed(file: File): boolean {
+  if (file.size > MAX_FILE_SIZE_BYTES) {
+    alert('文件大小不能超过 50MB')
+    return false
+  }
+  return true
+}
+
+function getFileFromClipboardEvent(e: React.ClipboardEvent): File | null {
+  const dt = e.clipboardData
+  if (!dt) return null
+  if (dt.files.length > 0) {
+    return dt.files[0]
+  }
+  for (let i = 0; i < dt.items.length; i++) {
+    const item = dt.items[i]
+    if (item.kind === 'file') {
+      const f = item.getAsFile()
+      if (f) return f
+    }
+  }
+  return null
+}
+
+function dragEventHasFilePayload(e: React.DragEvent): boolean {
+  const types = e.dataTransfer?.types
+  if (!types) return false
+  for (let i = 0; i < types.length; i++) {
+    if (types[i] === 'Files') return true
+  }
+  return false
+}
+
 export const ChatPage: React.FC<ChatPageProps> = ({
   contacts,
   activeContact,
@@ -55,6 +90,7 @@ export const ChatPage: React.FC<ChatPageProps> = ({
   const isFirstLoadRef = useRef(true)
   const prevScrollHeightRef = useRef(0)
   const isLoadingMoreRef = useRef(false)
+  const [isFileDragOver, setIsFileDragOver] = useState(false)
 
   useEffect(() => {
     const container = chatContainerRef.current
@@ -117,17 +153,70 @@ export const ChatPage: React.FC<ChatPageProps> = ({
   const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
     const file = event.target.files?.[0]
     if (!file) return
-    
-    const maxSize = 50 * 1024 * 1024
-    if (file.size > maxSize) {
-      alert('文件大小不能超过 50MB')
-      return
-    }
-    
+    if (!isFileSizeAllowed(file)) return
     onFileSelect(file)
     event.target.value = ''
   }
 
+  const handlePaste = useCallback(
+    (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
+      const file = getFileFromClipboardEvent(e)
+      if (!file) return
+      e.preventDefault()
+      if (!isFileSizeAllowed(file)) return
+      onFileSelect(file)
+    },
+    [onFileSelect]
+  )
+
+  const isAppNotificationSession = activeContactId != null && activeContactId < 0
+
+  const handleChatDragEnter = useCallback(
+    (e: React.DragEvent) => {
+      if (isAppNotificationSession) return
+      if (!dragEventHasFilePayload(e)) return
+      e.preventDefault()
+      setIsFileDragOver(true)
+    },
+    [isAppNotificationSession]
+  )
+
+  const handleChatDragLeave = useCallback(
+    (e: React.DragEvent) => {
+      if (isAppNotificationSession) return
+      if (!dragEventHasFilePayload(e)) return
+      e.preventDefault()
+      const el = e.currentTarget as HTMLElement
+      const rel = e.relatedTarget as Node | null
+      if (rel && el.contains(rel)) return
+      setIsFileDragOver(false)
+    },
+    [isAppNotificationSession]
+  )
+
+  const handleChatDragOver = useCallback(
+    (e: React.DragEvent) => {
+      if (isAppNotificationSession) return
+      if (!dragEventHasFilePayload(e)) return
+      e.preventDefault()
+      e.dataTransfer.dropEffect = 'copy'
+    },
+    [isAppNotificationSession]
+  )
+
+  const handleChatDrop = useCallback(
+    (e: React.DragEvent) => {
+      e.preventDefault()
+      setIsFileDragOver(false)
+      if (isAppNotificationSession) return
+      const file = e.dataTransfer.files[0]
+      if (!file) return
+      if (!isFileSizeAllowed(file)) return
+      onFileSelect(file)
+    },
+    [isAppNotificationSession, onFileSelect]
+  )
+
   if (contacts.length === 0) {
     return (
       <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#999', flexDirection: 'column' }}>
@@ -146,12 +235,26 @@ export const ChatPage: React.FC<ChatPageProps> = ({
     )
   }
 
-  /** 负 id:应用系统通知会话(-app_id),只读,不展示发消息/发文件 */
-  const isAppNotificationSession = activeContactId != null && activeContactId < 0
   const headerRemark = formatConversationRemark(activeContact.remarks)
 
   return (
-    <>
+    <div
+      style={{
+        flex: 1,
+        display: 'flex',
+        flexDirection: 'column',
+        minHeight: 0,
+        minWidth: 0,
+        position: 'relative',
+        boxSizing: 'border-box',
+        outline: isFileDragOver ? '2px dashed #1aad19' : 'none',
+        outlineOffset: isFileDragOver ? -2 : 0
+      }}
+      onDragEnter={handleChatDragEnter}
+      onDragLeave={handleChatDragLeave}
+      onDragOver={handleChatDragOver}
+      onDrop={handleChatDrop}
+    >
       <div style={{ padding: '0 20px', height: '60px', display: 'flex', alignItems: 'center', borderBottom: '1px solid #e7e7e7', WebkitAppRegion: 'drag' } as React.CSSProperties}>
         <div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
           <span style={{ fontWeight: 'bold', fontSize: '16px' }}>
@@ -405,6 +508,7 @@ export const ChatPage: React.FC<ChatPageProps> = ({
             }}
             value={inputValue}
             onChange={(e) => setInputValue(e.target.value)}
+            onPaste={handlePaste}
             onKeyDown={(e) => {
               if (e.key === 'Enter' && !e.shiftKey) {
                 e.preventDefault()
@@ -442,6 +546,6 @@ export const ChatPage: React.FC<ChatPageProps> = ({
           </div>
         </div>
       )}
-    </>
+    </div>
   )
 }