feat(ui): 新增支持缩放、拖拽及键盘控制的图片预览组件并优化样式,更新安装程序支持 VC++ 运行库自动安装,多账号支持列表选择。

This commit is contained in:
Forrest
2026-01-16 22:16:55 +08:00
parent b7eb19aad6
commit 6eabd707f8
8 changed files with 456 additions and 44 deletions

View File

@@ -0,0 +1,46 @@
.image-preview-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
user-select: none;
}
.preview-image {
max-width: 90vw;
max-height: 90vh;
object-fit: contain;
transition: transform 0.15s ease-out;
&.dragging {
transition: none;
}
}
.image-preview-close {
position: absolute;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
width: 48px;
height: 48px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.3);
background: rgba(0, 0, 0, 0.7);
color: #fff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
backdrop-filter: blur(10px);
&:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.5);
transform: translateX(-50%) scale(1.1);
}
}

View File

@@ -0,0 +1,101 @@
import React, { useState, useRef, useCallback, useEffect } from 'react'
import { X } from 'lucide-react'
import { createPortal } from 'react-dom'
import './ImagePreview.scss'
interface ImagePreviewProps {
src: string
onClose: () => void
}
export const ImagePreview: React.FC<ImagePreviewProps> = ({ src, onClose }) => {
const [scale, setScale] = useState(1)
const [position, setPosition] = useState({ x: 0, y: 0 })
const [isDragging, setIsDragging] = useState(false)
const dragStart = useRef({ x: 0, y: 0 })
const positionStart = useRef({ x: 0, y: 0 })
const containerRef = useRef<HTMLDivElement>(null)
// 滚轮缩放
const handleWheel = useCallback((e: React.WheelEvent) => {
e.preventDefault()
const delta = e.deltaY > 0 ? 0.9 : 1.1
setScale(prev => Math.min(Math.max(prev * delta, 0.5), 5))
}, [])
// 开始拖动
const handleMouseDown = useCallback((e: React.MouseEvent) => {
if (scale <= 1) return
e.preventDefault()
setIsDragging(true)
dragStart.current = { x: e.clientX, y: e.clientY }
positionStart.current = { ...position }
}, [scale, position])
// 拖动中
const handleMouseMove = useCallback((e: React.MouseEvent) => {
if (!isDragging) return
const dx = e.clientX - dragStart.current.x
const dy = e.clientY - dragStart.current.y
setPosition({
x: positionStart.current.x + dx,
y: positionStart.current.y + dy
})
}, [isDragging])
// 结束拖动
const handleMouseUp = useCallback(() => {
setIsDragging(false)
}, [])
// 双击重置
const handleDoubleClick = useCallback(() => {
setScale(1)
setPosition({ x: 0, y: 0 })
}, [])
// 点击背景关闭
const handleOverlayClick = useCallback((e: React.MouseEvent) => {
if (e.target === containerRef.current) {
onClose()
}
}, [onClose])
// ESC 关闭
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [onClose])
return createPortal(
<div
ref={containerRef}
className="image-preview-overlay"
onClick={handleOverlayClick}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
>
<img
src={src}
alt="图片预览"
className={`preview-image ${isDragging ? 'dragging' : ''}`}
style={{
transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`,
cursor: scale > 1 ? (isDragging ? 'grabbing' : 'grab') : 'default'
}}
onWheel={handleWheel}
onMouseDown={handleMouseDown}
onDoubleClick={handleDoubleClick}
draggable={false}
/>
<button className="image-preview-close" onClick={onClose}>
<X size={20} />
</button>
</div>,
document.body
)
}