美化右键菜单
This commit is contained in:
+8
-20
@@ -44,26 +44,14 @@ function createFloatingWindow(): void {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Handle context menu
|
// Handle open settings from renderer
|
||||||
ipcMain.on('show-context-menu', () => {
|
ipcMain.on('open-settings', () => {
|
||||||
if (floatingWindow) {
|
createSettingsWindow()
|
||||||
const { Menu } = require('electron')
|
})
|
||||||
const menu = Menu.buildFromTemplate([
|
|
||||||
{
|
// Handle quit app from renderer
|
||||||
label: '设置',
|
ipcMain.on('quit-app', () => {
|
||||||
click: () => {
|
app.quit()
|
||||||
createSettingsWindow()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '退出',
|
|
||||||
click: () => {
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
])
|
|
||||||
menu.popup({ window: floatingWindow })
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Load the floating window HTML
|
// 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 React, { useState, useRef, useEffect } from 'react'
|
||||||
import { streamChat } from '../services/aiService'
|
import { streamChat } from '../services/aiService'
|
||||||
|
import ContextMenu from './ContextMenu'
|
||||||
|
|
||||||
const FloatingBall: React.FC = () => {
|
const FloatingBall: React.FC = () => {
|
||||||
const [isTooltipOpen, setIsTooltipOpen] = useState(false)
|
const [isTooltipOpen, setIsTooltipOpen] = useState(false)
|
||||||
@@ -8,6 +9,9 @@ const FloatingBall: React.FC = () => {
|
|||||||
const [inputValue, setInputValue] = useState<string>('')
|
const [inputValue, setInputValue] = useState<string>('')
|
||||||
const [aiResponse, setAiResponse] = useState<string>('')
|
const [aiResponse, setAiResponse] = useState<string>('')
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
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 isDraggingRef = useRef(false)
|
||||||
const startPosRef = useRef({ x: 0, y: 0 })
|
const startPosRef = useRef({ x: 0, y: 0 })
|
||||||
const windowStartRef = useRef({ x: 0, y: 0 })
|
const windowStartRef = useRef({ x: 0, y: 0 })
|
||||||
@@ -109,14 +113,18 @@ const FloatingBall: React.FC = () => {
|
|||||||
}, [isTooltipOpen])
|
}, [isTooltipOpen])
|
||||||
|
|
||||||
const handleMouseEnterBall = (): void => {
|
const handleMouseEnterBall = (): void => {
|
||||||
|
setIsMouseOverBall(true)
|
||||||
// When mouse enters the ball area, stop ignoring mouse events
|
// When mouse enters the ball area, stop ignoring mouse events
|
||||||
window.electron.ipcRenderer.send('set-ignore-mouse-events', false)
|
window.electron.ipcRenderer.send('set-ignore-mouse-events', false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseLeaveBall = (): void => {
|
const handleMouseLeaveBall = (): void => {
|
||||||
|
setIsMouseOverBall(false)
|
||||||
// When mouse leaves the ball area, always restore click-through
|
// When mouse leaves the ball area, always restore click-through
|
||||||
// If mouse enters tooltip, the tooltip's onMouseEnter will disable click-through again
|
// 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 => {
|
const handleMouseEnterTooltip = (): void => {
|
||||||
@@ -131,8 +139,33 @@ const FloatingBall: React.FC = () => {
|
|||||||
|
|
||||||
const handleContextMenu = (e: React.MouseEvent): void => {
|
const handleContextMenu = (e: React.MouseEvent): void => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
// Show context menu via IPC
|
// Show custom context menu at cursor position
|
||||||
window.electron.ipcRenderer.send('show-context-menu')
|
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> => {
|
const handleSendMessage = async (): Promise<void> => {
|
||||||
@@ -236,6 +269,23 @@ const FloatingBall: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
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>{`
|
<style>{`
|
||||||
.ai-response-container::-webkit-scrollbar {
|
.ai-response-container::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
|
|||||||
Reference in New Issue
Block a user