From d1bf5db4c5e1ce182ed9e507e3b3aace8ed4c693 Mon Sep 17 00:00:00 2001 From: hzm <934585316@qq.com> Date: Tue, 24 Feb 2026 08:20:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20OCR=E8=AF=86=E5=88=AB=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=8B=96=E6=8B=BD=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/ScanPickModal.jsx | 71 +++++++++++++++++++++++++++++++- app/globals.css | 27 +++++++++++- app/page.jsx | 14 +++++-- 3 files changed, 106 insertions(+), 6 deletions(-) diff --git a/app/components/ScanPickModal.jsx b/app/components/ScanPickModal.jsx index 0dda62a..3a54dbe 100644 --- a/app/components/ScanPickModal.jsx +++ b/app/components/ScanPickModal.jsx @@ -1,8 +1,54 @@ 'use client'; +import { useState, useCallback } from 'react'; import { motion } from 'framer-motion'; -export default function ScanPickModal({ onClose, onPick, isScanning }) { +const IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp']; + +function getDroppedImageFiles(dataTransfer) { + if (!dataTransfer?.files?.length) return []; + return Array.from(dataTransfer.files).filter((f) => + IMAGE_TYPES.includes(f.type) + ); +} + +export default function ScanPickModal({ onClose, onPick, onFilesDrop, isScanning }) { + const [isDragging, setIsDragging] = useState(false); + + const handleDragOver = useCallback((e) => { + e.preventDefault(); + e.stopPropagation(); + if (!isScanning) setIsDragging(true); + }, [isScanning]); + + const handleDragLeave = useCallback((e) => { + e.preventDefault(); + e.stopPropagation(); + if (!e.currentTarget.contains(e.relatedTarget)) setIsDragging(false); + }, []); + + const handleDrop = useCallback((e) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + if (isScanning || !onFilesDrop) return; + const files = getDroppedImageFiles(e.dataTransfer); + if (files.length) onFilesDrop(files); + }, [isScanning, onFilesDrop]); + + const dropZoneStyle = { + marginBottom: 12, + padding: '20px 16px', + borderRadius: 12, + border: `2px dashed ${isDragging ? 'var(--primary)' : 'var(--border)'}`, + background: isDragging + ? 'rgba(34, 211, 238, 0.08)' + : 'rgba(255, 255, 255, 0.02)', + transition: 'border-color 0.2s ease, background 0.2s ease', + cursor: isScanning ? 'not-allowed' : 'pointer', + pointerEvents: isScanning ? 'none' : 'auto', + }; + return ( 选择持仓截图
- 从相册选择一张或多张持仓截图,系统将自动识别其中的基金代码(6位数字),并支持批量导入。 + 从相册选择一张或多张持仓截图,系统将自动识别其中的基金代码(6位数字),并支持批量导入。 +
+
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + if (!isScanning) onPick?.(); + } + }} + > +
+ {isDragging ? '松开即可导入' : '拖拽图片到此处,或点击选择'} +
diff --git a/app/globals.css b/app/globals.css index 708b0f5..f85073a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -22,8 +22,33 @@ body { body { margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; - background: radial-gradient(1200px 600px at 10% -10%, rgba(96, 165, 250, 0.15), transparent 40%), radial-gradient(1000px 500px at 90% 0%, rgba(34, 211, 238, 0.12), transparent 45%), var(--bg); + background: var(--bg); color: var(--text); + position: relative; +} + +/* 渐变层固定为视口大小,随宽高变化自动重绘,保证任意尺寸下都连贯 */ +body::before { + content: ''; + position: fixed; + inset: 0; + z-index: -1; + pointer-events: none; + background: + radial-gradient( + ellipse 140% 140% at 10% -10%, + rgba(96, 165, 250, 0.14) 0%, + rgba(96, 165, 250, 0.06) 35%, + rgba(96, 165, 250, 0.02) 55%, + transparent 72% + ), + radial-gradient( + ellipse 140% 140% at 90% 0%, + rgba(34, 211, 238, 0.12) 0%, + rgba(34, 211, 238, 0.05) 38%, + rgba(34, 211, 238, 0.01) 58%, + transparent 75% + ); } .container { diff --git a/app/page.jsx b/app/page.jsx index 34515e6..689bfc1 100644 --- a/app/page.jsx +++ b/app/page.jsx @@ -984,9 +984,8 @@ export default function HomePage() { if (fileInputRef.current) fileInputRef.current.value = ''; }; - const handleFilesUpload = async (event) => { - const files = Array.from(event.target.files || []); - if (!files.length) return; + const processFiles = async (files) => { + if (!files?.length) return; setIsScanning(true); setScanModalOpen(false); // 关闭选择弹窗 @@ -1129,6 +1128,14 @@ export default function HomePage() { } }; + const handleFilesUpload = (event) => { + processFiles(Array.from(event.target.files || [])); + }; + + const handleFilesDrop = (files) => { + processFiles(files); + }; + const toggleScannedCode = (code) => { setSelectedScannedCodes(prev => { const next = new Set(prev); @@ -4089,6 +4096,7 @@ export default function HomePage() { setScanModalOpen(false)} onPick={handleScanPick} + onFilesDrop={handleFilesDrop} isScanning={isScanning} /> )}