美化右键菜单

This commit is contained in:
2025-11-07 18:42:49 +08:00
parent 53c8befd0e
commit 1094191020
3 changed files with 251 additions and 23 deletions
+8 -20
View File
@@ -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: () => {
createSettingsWindow()
}
},
{
label: '退出',
click: () => {
app.quit()
}
}
])
menu.popup({ window: floatingWindow })
}
// Handle open settings from renderer
ipcMain.on('open-settings', () => {
createSettingsWindow()
})
// Handle quit app from renderer
ipcMain.on('quit-app', () => {
app.quit()
})
// Load the floating window HTML
+190
View File
@@ -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
+53 -3
View File
@@ -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,14 +113,18 @@ 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
window.electron.ipcRenderer.send('set-ignore-mouse-events', true, { forward: true })
if (!isContextMenuOpen) {
window.electron.ipcRenderer.send('set-ignore-mouse-events', true, { forward: true })
}
}
const handleMouseEnterTooltip = (): void => {
@@ -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;