美化右键菜单
This commit is contained in:
+6
-18
@@ -44,26 +44,14 @@ function createFloatingWindow(): void {
|
||||
}
|
||||
})
|
||||
|
||||
// Handle context menu
|
||||
ipcMain.on('show-context-menu', () => {
|
||||
if (floatingWindow) {
|
||||
const { Menu } = require('electron')
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: '设置',
|
||||
click: () => {
|
||||
// Handle open settings from renderer
|
||||
ipcMain.on('open-settings', () => {
|
||||
createSettingsWindow()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '退出',
|
||||
click: () => {
|
||||
})
|
||||
|
||||
// Handle quit app from renderer
|
||||
ipcMain.on('quit-app', () => {
|
||||
app.quit()
|
||||
}
|
||||
}
|
||||
])
|
||||
menu.popup({ window: floatingWindow })
|
||||
}
|
||||
})
|
||||
|
||||
// Load the floating window HTML
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import React from 'react'
|
||||
|
||||
interface ContextMenuProps {
|
||||
isOpen: boolean
|
||||
position: { x: number; y: number }
|
||||
onClose: () => void
|
||||
onSettings: () => void
|
||||
onQuit: () => void
|
||||
onMouseEnter?: () => void
|
||||
onMouseLeave?: () => void
|
||||
}
|
||||
|
||||
const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
isOpen,
|
||||
position,
|
||||
onClose,
|
||||
onSettings,
|
||||
onQuit,
|
||||
onMouseEnter,
|
||||
onMouseLeave
|
||||
}) => {
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 9999,
|
||||
pointerEvents: 'auto'
|
||||
}}
|
||||
>
|
||||
{/* Backdrop to catch clicks outside menu */}
|
||||
<div
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
pointerEvents: 'auto'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Menu container */}
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${position.x}px`,
|
||||
top: `${position.y}px`,
|
||||
pointerEvents: 'auto',
|
||||
minWidth: '180px',
|
||||
background: 'rgba(255, 255, 255, 0.98)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
WebkitBackdropFilter: 'blur(20px)',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.8)',
|
||||
padding: '8px',
|
||||
animation: 'menuFadeIn 0.15s ease-out',
|
||||
transformOrigin: 'top left'
|
||||
}}
|
||||
>
|
||||
<style>{`
|
||||
@keyframes menuFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
transition: all 0.15s ease;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: linear-gradient(135deg, rgba(33, 150, 243, 0.1) 0%, rgba(25, 118, 210, 0.15) 100%);
|
||||
color: #1976d2;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.menu-item:active {
|
||||
transform: translateX(2px) scale(0.98);
|
||||
}
|
||||
|
||||
.menu-item-danger:hover {
|
||||
background: linear-gradient(135deg, rgba(244, 67, 54, 0.1) 0%, rgba(211, 47, 47, 0.15) 100%);
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
.menu-divider {
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.08), transparent);
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* Settings menu item */}
|
||||
<div
|
||||
className="menu-item"
|
||||
onClick={() => {
|
||||
onSettings()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<div className="menu-icon">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M12 1v6m0 6v6m-9-9h6m6 0h6" />
|
||||
<path d="M4.22 4.22l4.24 4.24m7.08 0l4.24-4.24m0 15.56l-4.24-4.24m-7.08 0l-4.24 4.24" />
|
||||
</svg>
|
||||
</div>
|
||||
<span>设置</span>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="menu-divider" />
|
||||
|
||||
{/* Quit menu item */}
|
||||
<div
|
||||
className="menu-item menu-item-danger"
|
||||
onClick={() => {
|
||||
onQuit()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<div className="menu-icon">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||
<polyline points="16 17 21 12 16 7" />
|
||||
<line x1="21" y1="12" x2="9" y2="12" />
|
||||
</svg>
|
||||
</div>
|
||||
<span>退出</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContextMenu
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import { streamChat } from '../services/aiService'
|
||||
import ContextMenu from './ContextMenu'
|
||||
|
||||
const FloatingBall: React.FC = () => {
|
||||
const [isTooltipOpen, setIsTooltipOpen] = useState(false)
|
||||
@@ -8,6 +9,9 @@ const FloatingBall: React.FC = () => {
|
||||
const [inputValue, setInputValue] = useState<string>('')
|
||||
const [aiResponse, setAiResponse] = useState<string>('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false)
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 })
|
||||
const [isMouseOverBall, setIsMouseOverBall] = useState(false)
|
||||
const isDraggingRef = useRef(false)
|
||||
const startPosRef = useRef({ x: 0, y: 0 })
|
||||
const windowStartRef = useRef({ x: 0, y: 0 })
|
||||
@@ -109,15 +113,19 @@ const FloatingBall: React.FC = () => {
|
||||
}, [isTooltipOpen])
|
||||
|
||||
const handleMouseEnterBall = (): void => {
|
||||
setIsMouseOverBall(true)
|
||||
// When mouse enters the ball area, stop ignoring mouse events
|
||||
window.electron.ipcRenderer.send('set-ignore-mouse-events', false)
|
||||
}
|
||||
|
||||
const handleMouseLeaveBall = (): void => {
|
||||
setIsMouseOverBall(false)
|
||||
// When mouse leaves the ball area, always restore click-through
|
||||
// If mouse enters tooltip, the tooltip's onMouseEnter will disable click-through again
|
||||
if (!isContextMenuOpen) {
|
||||
window.electron.ipcRenderer.send('set-ignore-mouse-events', true, { forward: true })
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseEnterTooltip = (): void => {
|
||||
// When mouse enters tooltip, stop ignoring mouse events
|
||||
@@ -131,8 +139,33 @@ const FloatingBall: React.FC = () => {
|
||||
|
||||
const handleContextMenu = (e: React.MouseEvent): void => {
|
||||
e.preventDefault()
|
||||
// Show context menu via IPC
|
||||
window.electron.ipcRenderer.send('show-context-menu')
|
||||
// Show custom context menu at cursor position
|
||||
setContextMenuPosition({ x: e.clientX, y: e.clientY })
|
||||
setIsContextMenuOpen(true)
|
||||
// Disable mouse events pass-through when menu is open
|
||||
window.electron.ipcRenderer.send('set-ignore-mouse-events', false)
|
||||
}
|
||||
|
||||
const handleCloseContextMenu = (): void => {
|
||||
setIsContextMenuOpen(false)
|
||||
// Re-enable mouse events pass-through when menu closes, but only if mouse is not over the ball
|
||||
// Use setTimeout to ensure state update completes first
|
||||
setTimeout(() => {
|
||||
// Check if mouse is still over the ball or tooltip
|
||||
if (!isMouseOverBall && !isTooltipOpen) {
|
||||
window.electron.ipcRenderer.send('set-ignore-mouse-events', true, { forward: true })
|
||||
}
|
||||
}, 50)
|
||||
}
|
||||
|
||||
const handleSettingsClick = (): void => {
|
||||
// Send message to main process to open settings
|
||||
window.electron.ipcRenderer.send('open-settings')
|
||||
}
|
||||
|
||||
const handleQuitClick = (): void => {
|
||||
// Send message to main process to quit app
|
||||
window.electron.ipcRenderer.send('quit-app')
|
||||
}
|
||||
|
||||
const handleSendMessage = async (): Promise<void> => {
|
||||
@@ -236,6 +269,23 @@ const FloatingBall: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu
|
||||
isOpen={isContextMenuOpen}
|
||||
position={contextMenuPosition}
|
||||
onClose={handleCloseContextMenu}
|
||||
onSettings={handleSettingsClick}
|
||||
onQuit={handleQuitClick}
|
||||
onMouseEnter={() => {
|
||||
// Keep mouse events enabled when hovering over menu
|
||||
window.electron.ipcRenderer.send('set-ignore-mouse-events', false)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
// When mouse leaves menu, restore click-through
|
||||
if (!isMouseOverBall && !isTooltipOpen) {
|
||||
window.electron.ipcRenderer.send('set-ignore-mouse-events', true, { forward: true })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<style>{`
|
||||
.ai-response-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
|
||||
Reference in New Issue
Block a user