import React, { useState, useEffect, useCallback, useRef } from 'react' import { Monitor, GitBranch, Settings, Plus, Trash2, GripVertical, Folder, Code, Sparkles } from 'lucide-react' import { ConfigDialog } from './components/ConfigDialog' import { DeleteConfirmDialog } from '@/components/dialogs/DeleteConfirmDialog' import type { DeviceInfo, RemoteConfig, RemoteDevice } from './types' import { useTabContentContext, useScreenshot } from '@/stores' import { getRemoteConfig, saveRemoteConfig, getDeviceData } from './api' import type { FileItem } from '@/lib/api' const defaultConfig: RemoteConfig = { devices: [], } const createRemoteDesktopFileItem = (url: string, title: string, deviceName?: string): FileItem => { const id = deviceName ? `remote-${deviceName}` : 'remote-desktop' return { name: title, path: `remote-desktop://${id}?url=${encodeURIComponent(url)}&device=${encodeURIComponent(deviceName || '')}`, type: 'file' as const, size: 0, modified: new Date().toISOString(), } } const createRemoteGitFileItem = (url: string, title: string, deviceName?: string): FileItem => { const id = deviceName ? `remote-git-${deviceName}` : 'remote-git' return { name: title, path: `remote-git://${id}?url=${encodeURIComponent(url)}&device=${encodeURIComponent(deviceName || '')}`, type: 'file' as const, size: 0, modified: new Date().toISOString(), } } const createEmptyDevice = (): RemoteDevice => ({ id: Date.now().toString(), deviceName: '', serverHost: '', desktopPort: 3000, gitPort: 3001, }) export const RemotePage: React.FC = () => { const [selectedDeviceId, setSelectedDeviceId] = useState(null) const [dragIndex, setDragIndex] = useState(null) const [dragOverIndex, setDragOverIndex] = useState(null) const [showConfig, setShowConfig] = useState(false) const [config, setConfig] = useState(defaultConfig) const [loading, setLoading] = useState(true) const [editingDevice, setEditingDevice] = useState(null) const [deviceLastConnected, setDeviceLastConnected] = useState>({}) const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) const [deletingDeviceId, setDeletingDeviceId] = useState(null) // 自定义拖拽状态 const dragRef = useRef<{ isDragging: boolean startY: number startIndex: number currentIndex: number listRect: DOMRect | null itemRects: DOMRect[] }>({ isDragging: false, startY: 0, startIndex: -1, currentIndex: -1, listRect: null, itemRects: [], }) const listContainerRef = useRef(null) const { selectFile } = useTabContentContext() const { screenshot, lastConnected, loadScreenshots } = useScreenshot() const selectedConfig = config.devices.find(d => d.id === selectedDeviceId) || null useEffect(() => { const init = async () => { try { const savedConfig = await getRemoteConfig() const configWithDevices = savedConfig.devices ? savedConfig : { devices: [] } setConfig(configWithDevices) if (configWithDevices.devices.length > 0 && !selectedDeviceId) { setSelectedDeviceId(configWithDevices.devices[0].id) } // 加载所有设备的最后连接时间 const newData: Record = {} for (const device of configWithDevices.devices) { if (device.deviceName) { try { const data = await getDeviceData(device.deviceName) if (data?.lastConnected) { newData[device.deviceName] = data.lastConnected } } catch (err) { console.error('Failed to load device data:', err) } } } setDeviceLastConnected(newData) } catch (err) { console.error('Failed to load remote config:', err) } finally { setLoading(false) } } init() }, []) useEffect(() => { if (selectedConfig?.deviceName) { loadScreenshots(selectedConfig.deviceName) } }, [selectedDeviceId, selectedConfig?.deviceName, loadScreenshots]) // 当全局 lastConnected 更新时,同步更新 deviceLastConnected useEffect(() => { if (selectedConfig?.deviceName) { setDeviceLastConnected(prev => ({ ...prev, [selectedConfig.deviceName]: lastConnected })) } }, [lastConnected, selectedConfig?.deviceName]) // 当设备列表变化时,加载新增设备的最后连接时间 useEffect(() => { const loadNewDevices = async () => { for (const device of config.devices) { if (device.deviceName && !deviceLastConnected[device.deviceName]) { try { const data = await getDeviceData(device.deviceName) if (data?.lastConnected) { setDeviceLastConnected(prev => ({ ...prev, [device.deviceName]: data.lastConnected })) } } catch (err) { console.error('Failed to load device data:', err) } } } } if (!loading) { loadNewDevices() } }, [config.devices, loading, deviceLastConnected]) const devices: DeviceInfo[] = (config.devices || []).map(device => ({ id: device.id, name: device.deviceName, status: device.serverHost ? 'online' as const : 'offline' as const, lastConnected: deviceLastConnected[device.deviceName] || '-', })) const selectedDevice = devices.find(device => device.id === selectedDeviceId) || null const existingDeviceNames = config.devices.map(d => d.deviceName) const handleOpenRemoteDesktop = () => { if (!selectedConfig?.serverHost) { setShowConfig(true) return } const url = `http://${selectedConfig.serverHost}:${selectedConfig.desktopPort}` const deviceName = selectedConfig.deviceName ? ` - ${selectedConfig.deviceName}` : '' const fileItem = createRemoteDesktopFileItem(url, `远程桌面${deviceName}`, selectedConfig.deviceName) selectFile(fileItem) } const handleOpenGitRepo = () => { if (!selectedConfig?.serverHost) { setShowConfig(true) return } const url = `http://${selectedConfig.serverHost}:${selectedConfig.gitPort}` const deviceName = selectedConfig.deviceName ? ` - ${selectedConfig.deviceName}` : '' const fileItem = createRemoteGitFileItem(url, `远程 Git 仓库${deviceName}`, selectedConfig.deviceName) selectFile(fileItem) } const handleOpenFileTransfer = () => { if (!selectedConfig?.serverHost) { setShowConfig(true) return } const url = `file-transfer-panel?host=${encodeURIComponent(selectedConfig.serverHost)}&port=${selectedConfig.desktopPort}` const fileItem: FileItem = { name: `文件传输 - ${selectedConfig.deviceName}`, path: url, type: 'file', size: 0, modified: new Date().toISOString(), } selectFile(fileItem) } const handleOpenCode = () => { if (!selectedConfig?.serverHost) { setShowConfig(true) return } const url = `http://${selectedConfig.serverHost}:${selectedConfig.desktopPort}/opencode` const deviceName = selectedConfig.deviceName ? ` - ${selectedConfig.deviceName}` : '' const fileItem = createRemoteDesktopFileItem(url, `OpenCode${deviceName}`, selectedConfig.deviceName) selectFile(fileItem) } const handleOpenClaw = () => { if (!selectedConfig?.serverHost) { setShowConfig(true) return } const url = `http://${selectedConfig.serverHost}:${selectedConfig.desktopPort}/openclaw` const deviceName = selectedConfig.deviceName ? ` - ${selectedConfig.deviceName}` : '' const fileItem = createRemoteDesktopFileItem(url, `OpenClaw${deviceName}`, selectedConfig.deviceName) selectFile(fileItem) } const handleOpenConfig = () => { if (selectedConfig) { setEditingDevice(selectedConfig) } else { setEditingDevice(createEmptyDevice()) } setShowConfig(true) } const handleCloseConfig = () => { setShowConfig(false) setEditingDevice(null) } const handleSaveConfig = useCallback(async (device: RemoteDevice) => { const existingIndex = config.devices.findIndex(d => d.id === device.id) let newDevices: RemoteDevice[] if (existingIndex >= 0) { newDevices = [...config.devices] newDevices[existingIndex] = device } else { newDevices = [...config.devices, device] } const newConfig = { devices: newDevices } try { await saveRemoteConfig(newConfig) setConfig(newConfig) setSelectedDeviceId(device.id) } catch (err) { console.error('Failed to save config:', err) } setShowConfig(false) setEditingDevice(null) }, [config.devices]) const handleAddDevice = () => { setEditingDevice(createEmptyDevice()) setShowConfig(true) } const handleDeleteDevice = (deviceId: string) => { setDeletingDeviceId(deviceId) setDeleteDialogOpen(true) } const handleDeleteConfirm = async () => { if (!deletingDeviceId) return const newDevices = config.devices.filter(d => d.id !== deletingDeviceId) const newConfig = { devices: newDevices } try { await saveRemoteConfig(newConfig) setConfig(newConfig) if (selectedDeviceId === deletingDeviceId) { setSelectedDeviceId(newDevices.length > 0 ? newDevices[0].id : null) } } catch (err) { console.error('Failed to delete device:', err) } setDeleteDialogOpen(false) setDeletingDeviceId(null) } const handleDeleteCancel = () => { setDeleteDialogOpen(false) setDeletingDeviceId(null) } // 自定义拖拽处理 - 元素跟随鼠标移动 + 实时重排 const handleMouseDown = (e: React.MouseEvent, index: number) => { // 只允许左键拖拽 if (e.button !== 0) return // 阻止默认选择行为 e.preventDefault() e.stopPropagation() const listContainer = listContainerRef.current if (!listContainer) return const listRect = listContainer.getBoundingClientRect() // 获取被拖动元素的引用 const listContent = listContainer.querySelector('[data-drag-list]') const items = listContent?.querySelectorAll('[data-drag-item]') as NodeListOf | undefined const draggedElement = items?.[index] dragRef.current = { isDragging: true, startY: e.clientY, startIndex: index, currentIndex: index, listRect, itemRects: [], } setDragIndex(index) setDragOverIndex(index) // 直接操作DOM实现流畅拖动 if (draggedElement) { draggedElement.style.position = 'relative' draggedElement.style.zIndex = '100' draggedElement.style.transition = 'none' } // 添加全局鼠标事件 document.addEventListener('mousemove', handleMouseMove) document.addEventListener('mouseup', handleMouseUp) } const handleMouseMove = (e: MouseEvent) => { const drag = dragRef.current if (!drag.isDragging || !drag.listRect) return const { listRect, startIndex } = drag // 检测鼠标是否在列表区域内 const isInListArea = e.clientX >= listRect.left && e.clientX <= listRect.right && e.clientY >= listRect.top && e.clientY <= listRect.bottom // 如果鼠标移出列表区域,取消拖拽 if (!isInListArea) { handleMouseUp(e) return } // 直接修改DOM实现流畅移动 const listContent = listContainerRef.current?.querySelector('[data-drag-list]') const items = listContent?.querySelectorAll('[data-drag-item]') as NodeListOf | undefined const draggedElement = items?.[drag.startIndex] // 计算被拖动元素的偏移 const deltaY = e.clientY - drag.startY if (draggedElement) { draggedElement.style.transform = `translateY(${deltaY}px)` } const mouseY = e.clientY // 简单计算:基于鼠标在列表中的相对位置,加24px偏移使落点在缝隙中间 const relativeY = mouseY - listRect.top - 8 let newIndex = Math.floor(relativeY / 48) - 1 newIndex = Math.max(0, Math.min(newIndex, devices.length - 1)) // 限制newIndex范围 if (newIndex < 0) newIndex = 0 if (newIndex >= devices.length) newIndex = devices.length - 1 drag.currentIndex = newIndex setDragOverIndex(newIndex) // 直接操作其他元素DOM实现重排(去掉transition) if (items) { const itemHeight = 48 items.forEach((item, i) => { if (i === drag.startIndex) return // 跳过被拖动的元素 let transform = '' if (drag.startIndex < newIndex) { // 从上面拖到下面:原位置到目标位置之间的元素向上移动 if (i > drag.startIndex && i <= newIndex) { transform = `translateY(-${itemHeight}px)` } } else if (drag.startIndex > newIndex) { // 从下面拖到上面:目标位置到原位置之间的元素向下移动 if (i >= newIndex && i < drag.startIndex) { transform = `translateY(${itemHeight}px)` } } item.style.transform = transform }) } } const handleMouseUp = async (e: MouseEvent) => { const drag = dragRef.current if (!drag.isDragging) return drag.isDragging = false document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mouseup', handleMouseUp) // 清理所有元素的样式 const listContent = listContainerRef.current?.querySelector('[data-drag-list]') const items = listContent?.querySelectorAll('[data-drag-item]') as NodeListOf | undefined items?.forEach(item => { item.style.transform = '' item.style.position = '' item.style.zIndex = '' item.style.transition = '' }) const draggedElement = items?.[drag.startIndex] if (draggedElement) { draggedElement.style.transform = '' draggedElement.style.position = '' draggedElement.style.zIndex = '' } const { startIndex, currentIndex } = drag setDragIndex(null) setDragOverIndex(null) // 如果位置发生变化,保存新顺序 if (startIndex !== currentIndex && currentIndex >= 0) { const newDevices = [...config.devices] const [movedItem] = newDevices.splice(startIndex, 1) newDevices.splice(currentIndex, 0, movedItem) const newConfig = { devices: newDevices } try { await saveRemoteConfig(newConfig) setConfig(newConfig) } catch (err) { console.error('Failed to save reordered config:', err) } } dragRef.current = { isDragging: false, startY: 0, startIndex: -1, currentIndex: -1, listRect: null, itemRects: [], } } if (loading) { return (
加载中...
) } return (

远程

{devices.map((device, index) => { const isSelected = selectedDeviceId === device.id const isDragging = dragIndex === index return (
dragIndex === null && setSelectedDeviceId(device.id)} > handleMouseDown(e, index)} className="cursor-grab active:cursor-grabbing p-0.5 hover:bg-gray-200/50 dark:hover:bg-gray-600/50 rounded" > {device.name || '未命名设备'}
) })}
{devices.length === 0 && (
暂无设备,点击上方添加
)}
{selectedDevice && selectedConfig ? (

{selectedDevice.name || '未命名设备'}

{selectedDevice.status === 'online' ? '在线' : '离线'} 最后连接:{selectedDevice.lastConnected}
) : (
请选择设备
)}
{editingDevice && ( name !== editingDevice.deviceName)} isEdit={!!config.devices.find(d => d.id === editingDevice.id)} /> )} {deletingDeviceId && ( d.id === deletingDeviceId)?.deviceName || '未命名设备'}" 吗?`} expectedText="DELETE DEVICE" onConfirm={handleDeleteConfirm} onCancel={handleDeleteCancel} /> )}
) }