diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0444c7d..7ffcc70 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,6 @@ import { useState, useCallback, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { message, Modal, Input } from 'antd'; +import { message } from 'antd'; import { useSpaceData } from './hooks/useSpaceData'; import { useHistoricalData } from './hooks/useHistoricalData'; import { useTrajectory } from './hooks/useTrajectory'; @@ -13,9 +13,8 @@ import { Loading } from './components/Loading'; import { InterstellarTicker } from './components/InterstellarTicker'; import { ControlPanel } from './components/ControlPanel'; import { AuthModal } from './components/AuthModal'; -import { DanmakuLayer } from './components/DanmakuLayer'; +import { MessageBoard } from './components/MessageBoard'; import { auth } from './utils/auth'; -import { request } from './utils/request'; import type { CelestialBody } from './types'; // Timeline configuration - will be fetched from backend later @@ -30,7 +29,7 @@ function App() { const [isTimelineMode, setIsTimelineMode] = useState(false); // Usually not persisted const [showOrbits, setShowOrbits] = useState(true); const [isSoundOn, setIsSoundOn] = useState(false); - const [showDanmaku, setShowDanmaku] = useState(true); + const [showMessageBoard, setShowMessageBoard] = useState(false); // Initialize state from localStorage useEffect(() => { @@ -40,7 +39,6 @@ function App() { const prefs = JSON.parse(savedPrefs); if (prefs.showOrbits !== undefined) setShowOrbits(prefs.showOrbits); if (prefs.isSoundOn !== undefined) setIsSoundOn(prefs.isSoundOn); - if (prefs.showDanmaku !== undefined) setShowDanmaku(prefs.showDanmaku); } catch (e) { console.error('Failed to parse preferences:', e); } @@ -52,10 +50,9 @@ function App() { const prefs = { showOrbits, isSoundOn, - showDanmaku }; localStorage.setItem(PREFS_KEY, JSON.stringify(prefs)); - }, [showOrbits, isSoundOn, showDanmaku]); + }, [showOrbits, isSoundOn]); const [selectedDate, setSelectedDate] = useState(null); const { takeScreenshot } = useScreenshot(); @@ -65,10 +62,6 @@ function App() { const [user, setUser] = useState(auth.getUser()); const [showAuthModal, setShowAuthModal] = useState(false); - // Danmaku state - const [isDanmakuInputVisible, setIsDanmakuInputVisible] = useState(false); - const [danmakuText, setDanmakuText] = useState(''); - // Use real-time data or historical data based on mode const { bodies: realTimeBodies, loading: realTimeLoading, error: realTimeError } = useSpaceData(); const { bodies: historicalBodies, loading: historicalLoading, error: historicalError } = useHistoricalData(selectedDate); @@ -118,29 +111,6 @@ function App() { takeScreenshot(nickname); }, [user, takeScreenshot]); - // Danmaku send handler - const handleSendDanmaku = async () => { - if (!danmakuText.trim()) return; - if (danmakuText.length > 20) { - message.warning("弹幕内容不能超过20字"); - return; - } - if (!user) { - message.warning("请先登录"); - setShowAuthModal(true); - return; - } - try { - await request.post('/danmaku/send', { text: danmakuText }); - message.success("发送成功"); - setDanmakuText(''); - setIsDanmakuInputVisible(false); - } catch (e) { - console.error(e); - message.error("发送失败"); - } - }; - // Auth handlers const handleLoginSuccess = (userData: any) => { setUser(userData); @@ -184,9 +154,6 @@ function App() { onNavigateToAdmin={() => navigate('/admin')} /> - {/* Danmaku Layer */} - - {/* Right Control Panel */} setShowOrbits(!showOrbits)} isSoundOn={isSoundOn} onToggleSound={() => setIsSoundOn(!isSoundOn)} - showDanmaku={showDanmaku} - onToggleDanmaku={() => setShowDanmaku(!showDanmaku)} - onOpenDanmakuInput={() => { - if (!user) { - message.warning("请先登录"); - setShowAuthModal(true); - } else { - setIsDanmakuInputVisible(true); - } - }} + showMessageBoard={showMessageBoard} + onToggleMessageBoard={() => setShowMessageBoard(!showMessageBoard)} onScreenshot={handleScreenshot} /> @@ -215,26 +174,11 @@ function App() { onLoginSuccess={handleLoginSuccess} /> - {/* Danmaku Input Modal */} - setIsDanmakuInputVisible(false)} - okText="发射" - cancelText="取消" - centered - > - setDanmakuText(e.target.value)} - onPressEnter={handleSendDanmaku} - autoFocus - /> - + {/* Message Board */} + setShowMessageBoard(false)} + /> {/* Probe List Sidebar */} void; isSoundOn: boolean; onToggleSound: () => void; - showDanmaku: boolean; - onToggleDanmaku: () => void; - onOpenDanmakuInput: () => void; + showMessageBoard: boolean; + onToggleMessageBoard: () => void; onScreenshot: () => void; } @@ -30,9 +28,8 @@ export function ControlPanel({ onToggleOrbits, isSoundOn, onToggleSound, - showDanmaku, - onToggleDanmaku, - onOpenDanmakuInput, + showMessageBoard, + onToggleMessageBoard, onScreenshot, }: ControlPanelProps) { const buttonClass = (isActive: boolean) => ` @@ -80,29 +77,16 @@ export function ControlPanel({ - {/* Danmaku Toggle */} -
- {showDanmaku && ( - - )} - -
+ {/* Message Board Toggle */} + {/* Screenshot Button */} + + } + > +
+ {messages.map((msg) => ( +
+
+ {new Date(msg.ts * 1000).toLocaleTimeString()} + {'<'}{msg.username}{'>'} +
+
+ {msg.text} +
+
+ ))} + {messages.length === 0 && !loading && ( +
+ NO SIGNALS DETECTED +
+ )} +
+
+ + ); +} diff --git a/frontend/src/components/TerminalModal.tsx b/frontend/src/components/TerminalModal.tsx new file mode 100644 index 0000000..2e913df --- /dev/null +++ b/frontend/src/components/TerminalModal.tsx @@ -0,0 +1,119 @@ +import { Modal, Spin } from 'antd'; +import { X } from 'lucide-react'; +import type { ReactNode } from 'react'; + +interface TerminalModalProps { + open: boolean; + onClose: () => void; + title: ReactNode; + loading?: boolean; + loadingText?: string; + children: ReactNode; + footer?: ReactNode; + width?: number; +} + +export function TerminalModal({ + open, + onClose, + title, + loading = false, + loadingText = "PROCESSING...", + children, + footer = null, + width = 800 +}: TerminalModalProps) { + + const terminalStyles = ` + .terminal-modal .ant-modal-content { + background-color: #0d1117 !important; + border: 1px solid #238636 !important; + box-shadow: 0 0 30px rgba(35, 134, 54, 0.15) !important; + color: #2ea043 !important; + padding: 0 !important; + overflow: hidden !important; + display: flex; + flex-direction: column; + } + .terminal-modal .ant-modal-body { + background-color: #0d1117 !important; + padding: 0 !important; + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + } + .terminal-modal .ant-modal-body .animate-in { + color: #2ea043 !important; + } + .terminal-modal .ant-modal-header { + background-color: #161b22 !important; + border-bottom: 1px solid #238636 !important; + padding: 12px 20px !important; + margin-bottom: 0 !important; + display: flex; + align-items: center; + flex-shrink: 0; + } + .terminal-modal .ant-modal-footer { + background-color: #161b22 !important; + border-top: 1px solid #238636 !important; + padding: 12px 20px !important; + margin-top: 0 !important; + flex-shrink: 0; + } + .terminal-modal .ant-modal-title { + color: #2ea043 !important; + } + .terminal-modal .ant-modal-close { + color: #2ea043 !important; + top: 12px !important; + } + .terminal-modal .ant-modal-close:hover { + background-color: rgba(35, 134, 54, 0.2) !important; + } + /* Scrollbar styling */ + .terminal-scroll::-webkit-scrollbar { + width: 8px; + background: #0d1117; + } + .terminal-scroll::-webkit-scrollbar-thumb { + background: #238636; + border-radius: 4px; + } + .terminal-scroll::-webkit-scrollbar-track { + background: #0d1117; + } + `; + + return ( + <> + + } + > + {loading ? ( +
+ +
{loadingText}
+
+ ) : ( +
+
+
+ {children} +
+
+
+ )} +
+ + ); +}