refactor: 页眉由 App 统一注入(ScreenLayout),三页共用一致页眉
- 新增 ScreenLayout 布局路由:注入共享 PageHeader + 全局左右键循环导航 - PageHeader 无 props,按路由自动识别状态(数据实时/3D 实时/媒体播放) - DataScreen / DigitalTwin / MediaScreen 移除各自页眉,避免重复 - 页脚按页面按需保留(数据大屏 + 媒体轮播,数字孪生无) - 管理后台不参与导航循环;旧 /screen2 路由移除 - 补齐 MediaScreen 音量/播放/翻页图标与浅色主题样式
This commit is contained in:
+1
-1
@@ -436,7 +436,7 @@ export default function Admin() {
|
||||
{fromScreen && (
|
||||
<button className="btn-return" onClick={handleReturnToScreen}>返回展播</button>
|
||||
)}
|
||||
<button className="btn-return" onClick={() => navigate('/screen2')}>数据大屏</button>
|
||||
<button className="btn-return" onClick={() => navigate('/')}>数据大屏</button>
|
||||
<button className="btn-logout" onClick={handleLogout}>退出</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import '../styles/datascreen.css';
|
||||
import Icon from '../components/Icons';
|
||||
import PageHeader from '../components/PageHeader';
|
||||
|
||||
/* =========================================================
|
||||
昆明市大学生创业园 · OPC 智能园区数字运营中心
|
||||
@@ -662,7 +660,7 @@ function MiddleRotator({ d }) {
|
||||
const views = [
|
||||
{
|
||||
key: 'ai',
|
||||
label: 'AI 智能应用',
|
||||
label: 'OPC 智能资源',
|
||||
icon: 'cpu',
|
||||
node: (
|
||||
<div className="bd-view bd-view-2">
|
||||
@@ -732,26 +730,9 @@ function LiveFeed({ feed, limit = 7 }) {
|
||||
========================================================= */
|
||||
export default function DataScreen() {
|
||||
const d = useParkSim();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 左右方向键:→ 进入数字孪生页,← 返回(管理页为左端)
|
||||
useEffect(() => {
|
||||
const onKey = (e) => {
|
||||
if (e.key === 'ArrowRight') navigate('/twin');
|
||||
else if (e.key === 'ArrowLeft') navigate('/admin');
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [navigate]);
|
||||
|
||||
return (
|
||||
<div className="bd-page">
|
||||
{/* 顶栏(共享组件,与数字孪生页风格一致) */}
|
||||
<PageHeader
|
||||
nav={{ to: '/twin', label: '数字孪生', icon: 'chevron-right', title: '按 → 进入数字孪生' }}
|
||||
status="数据实时"
|
||||
/>
|
||||
|
||||
{/* 园区今日概览(五个指标一行 + 标题右侧运行状态) */}
|
||||
<TodayOverview d={d} />
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { useState, useEffect, lazy, Suspense } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import '../styles/datascreen.css';
|
||||
import Icon from '../components/Icons';
|
||||
import PageHeader from '../components/PageHeader';
|
||||
|
||||
/* 3D 园区建筑懒加载 */
|
||||
const Building3D = lazy(() => import('../components/Building3D'));
|
||||
@@ -10,7 +8,7 @@ const Building3D = lazy(() => import('../components/Building3D'));
|
||||
/* =========================================================
|
||||
园区数字孪生 · 独立页面(/twin)
|
||||
全屏 3D 建筑模型 + 实时动态 + 企业分布
|
||||
左右方向键:← 返回数据大屏 · → 进入管理后台
|
||||
页眉由 ScreenLayout 统一注入,左右方向键切换页面
|
||||
========================================================= */
|
||||
|
||||
/* ---------- 企业数据(名称 / 区域 / 房间号) ---------- */
|
||||
@@ -45,7 +43,6 @@ const COMPANIES = [
|
||||
const ZONES = ['加速区', '国际区', '成长区'];
|
||||
|
||||
export default function DigitalTwin() {
|
||||
const navigate = useNavigate();
|
||||
const [feed, setFeed] = useState(initFeed());
|
||||
const [activeZone, setActiveZone] = useState('加速区');
|
||||
const [selected, setSelected] = useState(null);
|
||||
@@ -58,28 +55,12 @@ export default function DigitalTwin() {
|
||||
return () => clearInterval(iv);
|
||||
}, []);
|
||||
|
||||
// 左右方向键:← 返回数据大屏 · → 进入管理后台
|
||||
useEffect(() => {
|
||||
const onKey = (e) => {
|
||||
if (e.key === 'ArrowLeft') navigate('/screen2');
|
||||
else if (e.key === 'ArrowRight') navigate('/admin');
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [navigate]);
|
||||
|
||||
const list = COMPANIES.filter((c) => c.zone === activeZone);
|
||||
const zoneColors = { 加速区: '#4c8dff', 国际区: '#22d3ee', 成长区: '#34d399' };
|
||||
|
||||
return (
|
||||
<div className="bd-twin-page">
|
||||
{/* 顶栏(共享组件,与数据大屏风格一致) */}
|
||||
<PageHeader
|
||||
nav={{ to: '/screen2', label: '数据大屏', icon: 'chevron-left', title: '按 ← 返回数据大屏' }}
|
||||
status="3D 实时"
|
||||
/>
|
||||
|
||||
{/* 主体:3D 为主 + 侧边栏 */}
|
||||
{/* 主体:3D 为主 + 侧边栏(页眉由 ScreenLayout 统一注入) */}
|
||||
<main className="bd-twin-main">
|
||||
{/* 3D 场景 */}
|
||||
<section className="bd-twin-stage">
|
||||
|
||||
@@ -0,0 +1,509 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import * as api from '../utils/api';
|
||||
import { API_BASE } from '../utils/api';
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import { enable, disable } from '@tauri-apps/plugin-autostart';
|
||||
import Icon from '../components/Icons';
|
||||
import ParkOverviewStrip from '../components/ParkOverviewStrip';
|
||||
import { useParkSim } from '../utils/parkData';
|
||||
import '../styles/datascreen.css';
|
||||
import '../styles/mediascreen.css';
|
||||
|
||||
/* =========================================================
|
||||
MediaScreen —— 大屏媒体轮播
|
||||
页眉由 ScreenLayout 统一注入(所有页面一致),
|
||||
页脚(园区概览条)按需保留,中间为全屏媒体播放器
|
||||
========================================================= */
|
||||
|
||||
export default function MediaScreen() {
|
||||
const d = useParkSim();
|
||||
const navigate = useNavigate();
|
||||
const [list, setList] = useState([]);
|
||||
const [index, setIndex] = useState(0);
|
||||
const [volume, setVolume] = useState(0.8);
|
||||
const [playMode, setPlayMode] = useState('sequential');
|
||||
const [imageDuration, setImageDuration] = useState(5);
|
||||
const [paused, setPaused] = useState(false);
|
||||
const [soundBlocked, setSoundBlocked] = useState(false);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [loaderText, setLoaderText] = useState('加载播放列表中...');
|
||||
const [mediaKey, setMediaKey] = useState(0);
|
||||
const [showVideo, setShowVideo] = useState(true);
|
||||
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||
const [loginUsername, setLoginUsername] = useState('');
|
||||
const [loginPassword, setLoginPassword] = useState('');
|
||||
const [loginError, setLoginError] = useState('');
|
||||
const [fullscreenMode, setFullscreenMode] = useState(true);
|
||||
const [autostartEnabled, setAutostartEnabled] = useState(false);
|
||||
|
||||
const videoRef = useRef(null);
|
||||
const imgRef = useRef(null);
|
||||
const timerRef = useRef(null);
|
||||
const loginTimerRef = useRef(null);
|
||||
const loginInputRef = useRef(null);
|
||||
|
||||
const listRef = useRef(list);
|
||||
const indexRef = useRef(index);
|
||||
const pausedRef = useRef(paused);
|
||||
const playModeRef = useRef(playMode);
|
||||
const imageDurationRef = useRef(imageDuration);
|
||||
const soundBlockedRef = useRef(soundBlocked);
|
||||
|
||||
useEffect(() => { listRef.current = list; }, [list]);
|
||||
useEffect(() => { indexRef.current = index; }, [index]);
|
||||
useEffect(() => { pausedRef.current = paused; }, [paused]);
|
||||
useEffect(() => { playModeRef.current = playMode; }, [playMode]);
|
||||
useEffect(() => { imageDurationRef.current = imageDuration; }, [imageDuration]);
|
||||
useEffect(() => { soundBlockedRef.current = soundBlocked; }, [soundBlocked]);
|
||||
|
||||
const getUrl = useCallback((item) => {
|
||||
return item.source === 'url' ? item.relative_path : `${API_BASE}/file/${item.relative_path}`;
|
||||
}, []);
|
||||
|
||||
// ============ 核心播放控制 ============
|
||||
const next = useCallback(() => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
const len = listRef.current.length;
|
||||
if (len === 0) return;
|
||||
const mode = playModeRef.current;
|
||||
setIndex(prev => {
|
||||
if (mode === 'random') return Math.floor(Math.random() * len);
|
||||
return (prev + 1) % len;
|
||||
});
|
||||
setMediaKey(k => k + 1);
|
||||
}, []);
|
||||
|
||||
const skip = useCallback((delta) => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
const len = listRef.current.length;
|
||||
if (len === 0) return;
|
||||
const mode = playModeRef.current;
|
||||
setIndex(prev => {
|
||||
if (mode === 'random') return Math.floor(Math.random() * len);
|
||||
return (prev + delta + len) % len;
|
||||
});
|
||||
setMediaKey(k => k + 1);
|
||||
}, []);
|
||||
|
||||
// ============ 播放效果 ============
|
||||
useEffect(() => {
|
||||
if (!loaded || list.length === 0 || paused) return;
|
||||
const item = list[index];
|
||||
if (!item) return;
|
||||
|
||||
api.updateState({
|
||||
status: 'playing',
|
||||
index,
|
||||
name: item.name || '',
|
||||
type: item.type || '',
|
||||
}).catch(() => {});
|
||||
|
||||
if (item.type === 'video') {
|
||||
setShowVideo(true);
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
const url = getUrl(item);
|
||||
video.src = url;
|
||||
video.load();
|
||||
video.muted = soundBlocked;
|
||||
video.volume = volume;
|
||||
const p = video.play();
|
||||
if (p !== undefined) {
|
||||
p.catch(() => {
|
||||
setSoundBlocked(true);
|
||||
video.muted = true;
|
||||
video.play().catch(() => {});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setShowVideo(false);
|
||||
const url = getUrl(item);
|
||||
imgRef.current.src = url;
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
timerRef.current = setTimeout(next, imageDuration * 1000);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [index, mediaKey, loaded, paused]);
|
||||
|
||||
useEffect(() => {
|
||||
if (paused || !loaded || list.length === 0) return;
|
||||
const item = list[index];
|
||||
if (!item || item.type !== 'video') return;
|
||||
const video = videoRef.current;
|
||||
if (video && video.paused && video.src) {
|
||||
video.play().catch(() => {
|
||||
setSoundBlocked(true);
|
||||
video.muted = true;
|
||||
video.play().catch(() => {});
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [paused]);
|
||||
|
||||
// ============ 事件监听 ============
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
const handler = () => next();
|
||||
video.addEventListener('ended', handler);
|
||||
return () => video.removeEventListener('ended', handler);
|
||||
}, [next, mediaKey]);
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
const data = await api.getPlaylist();
|
||||
const files = data.files || [];
|
||||
const vol = (data.volume || 80) / 100;
|
||||
setList(files);
|
||||
setVolume(vol);
|
||||
setPlayMode(data.play_mode || 'sequential');
|
||||
setImageDuration(data.image_duration || 5);
|
||||
if (videoRef.current) videoRef.current.volume = vol;
|
||||
|
||||
if (files.length === 0) {
|
||||
setLoaderText('播放列表为空,<a href="/admin">前往管理后台添加</a>');
|
||||
return;
|
||||
}
|
||||
setLoaded(true);
|
||||
|
||||
const video = videoRef.current;
|
||||
if (video) {
|
||||
video.muted = false;
|
||||
const testPlay = video.play();
|
||||
if (testPlay !== undefined) {
|
||||
testPlay.then(() => {
|
||||
setSoundBlocked(false);
|
||||
video.pause();
|
||||
}).catch(() => {
|
||||
setSoundBlocked(true);
|
||||
video.muted = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {/* ignore */}
|
||||
};
|
||||
load();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const es = new EventSource(`${API_BASE}/api/events`);
|
||||
es.onmessage = (e) => {
|
||||
try {
|
||||
const msg = JSON.parse(e.data);
|
||||
switch (msg.action) {
|
||||
case 'pause':
|
||||
setPaused(true);
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
if (videoRef.current) videoRef.current.pause();
|
||||
break;
|
||||
case 'play':
|
||||
setPaused(false);
|
||||
break;
|
||||
case 'next':
|
||||
setPaused(false);
|
||||
skip(1);
|
||||
break;
|
||||
case 'prev':
|
||||
setPaused(false);
|
||||
skip(-1);
|
||||
break;
|
||||
case 'minimize_window':
|
||||
getCurrentWindow().minimize().catch(() => {});
|
||||
break;
|
||||
case 'playlist_changed':
|
||||
reloadPlaylist();
|
||||
break;
|
||||
case 'settings_changed':
|
||||
applySettings(msg);
|
||||
break;
|
||||
}
|
||||
} catch {/* ignore */}
|
||||
};
|
||||
return () => es.close();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [skip]);
|
||||
|
||||
const reloadPlaylist = useCallback(async () => {
|
||||
try {
|
||||
const data = await api.getPlaylist();
|
||||
const newList = data.files || [];
|
||||
const currentItem = listRef.current[indexRef.current] || null;
|
||||
|
||||
if (newList.length === 0) {
|
||||
setList([]);
|
||||
setLoaded(false);
|
||||
setLoaderText('播放列表已清空,<a href="/admin">前往管理后台添加</a>');
|
||||
const video = videoRef.current;
|
||||
if (video) video.pause();
|
||||
return;
|
||||
}
|
||||
|
||||
setLoaded(true);
|
||||
|
||||
let newIndex = -1;
|
||||
if (currentItem) {
|
||||
newIndex = newList.findIndex(item => item.relative_path === currentItem.relative_path);
|
||||
}
|
||||
if (newIndex >= 0) {
|
||||
setList(newList);
|
||||
setIndex(newIndex);
|
||||
} else {
|
||||
setList(newList);
|
||||
setIndex(prev => Math.min(prev, newList.length - 1));
|
||||
}
|
||||
} catch {/* ignore */}
|
||||
}, []);
|
||||
|
||||
const applySettings = useCallback((msg) => {
|
||||
if (msg.volume !== undefined) {
|
||||
const vol = msg.volume / 100;
|
||||
setVolume(vol);
|
||||
const video = videoRef.current;
|
||||
if (video) {
|
||||
video.volume = vol;
|
||||
if (soundBlockedRef.current) {
|
||||
video.muted = false;
|
||||
video.play().then(() => {
|
||||
setSoundBlocked(false);
|
||||
const hint = document.getElementById('soundHint');
|
||||
if (hint) hint.style.display = 'none';
|
||||
}).catch(() => {
|
||||
video.muted = true;
|
||||
video.play().catch(() => {});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (msg.play_mode !== undefined) setPlayMode(msg.play_mode);
|
||||
if (msg.image_duration !== undefined) setImageDuration(msg.image_duration);
|
||||
if (msg.fullscreen !== undefined) {
|
||||
setFullscreenMode(msg.fullscreen);
|
||||
getCurrentWindow().setFullscreen(msg.fullscreen).catch(() => {});
|
||||
}
|
||||
if (msg.autostart !== undefined) {
|
||||
setAutostartEnabled(msg.autostart);
|
||||
if (msg.autostart) {
|
||||
enable().catch(() => {});
|
||||
} else {
|
||||
disable().catch(() => {});
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const enableSound = useCallback(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
video.muted = false;
|
||||
video.play().then(() => {
|
||||
setSoundBlocked(false);
|
||||
const hint = document.getElementById('soundHint');
|
||||
if (hint) hint.style.display = 'none';
|
||||
}).catch(() => {
|
||||
video.muted = true;
|
||||
video.play().catch(() => {});
|
||||
});
|
||||
}, []);
|
||||
|
||||
// ============ 登录弹窗 ============
|
||||
const handleTitleClick = useCallback(() => {
|
||||
setLoginUsername('');
|
||||
setLoginPassword('');
|
||||
setLoginError('');
|
||||
setShowLoginModal(true);
|
||||
if (loginTimerRef.current) clearTimeout(loginTimerRef.current);
|
||||
loginTimerRef.current = setTimeout(() => {
|
||||
setShowLoginModal(false);
|
||||
}, 30000);
|
||||
setTimeout(() => {
|
||||
if (loginInputRef.current) loginInputRef.current.focus();
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
const handleCloseLogin = useCallback(() => {
|
||||
if (loginTimerRef.current) clearTimeout(loginTimerRef.current);
|
||||
setShowLoginModal(false);
|
||||
}, []);
|
||||
|
||||
const handleLoginSubmit = useCallback(async () => {
|
||||
try {
|
||||
const data = await api.login(loginUsername, loginPassword);
|
||||
if (data.success) {
|
||||
if (loginTimerRef.current) clearTimeout(loginTimerRef.current);
|
||||
localStorage.setItem('token', 'admin_logged');
|
||||
navigate('/admin?from=screen');
|
||||
} else {
|
||||
setLoginError('账号或密码错误');
|
||||
}
|
||||
} catch {
|
||||
setLoginError('登录失败,请重试');
|
||||
}
|
||||
}, [loginUsername, loginPassword, navigate]);
|
||||
|
||||
const handleLoginKeyDown = useCallback((e) => {
|
||||
if (e.key === 'Enter') handleLoginSubmit();
|
||||
}, [handleLoginSubmit]);
|
||||
|
||||
// ============ 播放/暂停 ============
|
||||
const togglePlayPause = useCallback(() => {
|
||||
if (pausedRef.current) {
|
||||
setPaused(false);
|
||||
} else {
|
||||
setPaused(true);
|
||||
const video = videoRef.current;
|
||||
if (video) video.pause();
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Global click for sound unblock
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
if (soundBlockedRef.current) enableSound();
|
||||
};
|
||||
document.addEventListener('click', handler);
|
||||
return () => document.removeEventListener('click', handler);
|
||||
}, [enableSound]);
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
const data = await api.getSettings();
|
||||
setFullscreenMode(data.fullscreen ?? true);
|
||||
setAutostartEnabled(data.autostart ?? false);
|
||||
} catch {/* ignore */}
|
||||
};
|
||||
load();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (showLoginModal) {
|
||||
handleCloseLogin();
|
||||
} else {
|
||||
getCurrentWindow().setFullscreen(false).catch(() => {});
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handler);
|
||||
return () => document.removeEventListener('keydown', handler);
|
||||
}, [showLoginModal, handleCloseLogin]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
if (loginTimerRef.current) clearTimeout(loginTimerRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const currentItem = list[index] || null;
|
||||
|
||||
return (
|
||||
<div className="ms-page">
|
||||
{/* 背景光晕 */}
|
||||
<div className="ms-orb orb-1"></div>
|
||||
<div className="ms-orb orb-2"></div>
|
||||
|
||||
{/* 主体:媒体播放器 + 控制层(页眉由 ScreenLayout 注入) */}
|
||||
<main className="ms-main">
|
||||
{/* 媒体播放器 */}
|
||||
<video
|
||||
ref={videoRef}
|
||||
id="player"
|
||||
key={mediaKey}
|
||||
autoPlay
|
||||
playsInline
|
||||
controlsList="nodownload"
|
||||
className="ms-media"
|
||||
style={{ display: showVideo && loaded ? 'block' : 'none' }}
|
||||
/>
|
||||
<img
|
||||
ref={imgRef}
|
||||
id="imgPlayer"
|
||||
alt=""
|
||||
className="ms-media"
|
||||
style={{ display: !showVideo && loaded ? 'block' : 'none' }}
|
||||
/>
|
||||
|
||||
{/* 加载中 */}
|
||||
{!loaded && (
|
||||
<div className="ms-loader">
|
||||
<div className="ms-loader-ring"></div>
|
||||
<div className="ms-loader-text" dangerouslySetInnerHTML={{ __html: loaderText }} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 中央播放控制(悬浮层) */}
|
||||
<div className={`ms-controls${loaded ? ' visible' : ''}`}>
|
||||
{/* 播放/暂停 */}
|
||||
<button className="ms-play" onClick={togglePlayPause} title={paused ? '播放' : '暂停'}>
|
||||
<Icon name={paused ? 'play' : 'pause'} size={18} />
|
||||
</button>
|
||||
{/* 上一段 / 下一段 */}
|
||||
<div className="ms-nav">
|
||||
<button onClick={() => skip(-1)} title="上一段">
|
||||
<Icon name="chevron-left" size={16} />
|
||||
</button>
|
||||
<span className="ms-nav-num">
|
||||
<b>{currentItem ? `${index + 1}` : '0'}</b>
|
||||
<em>/ {list.length}</em>
|
||||
</span>
|
||||
<button onClick={() => skip(1)} title="下一段">
|
||||
<Icon name="chevron-right" size={16} />
|
||||
</button>
|
||||
</div>
|
||||
{/* 当前播放名 + LIVE(点击进入后台登录) */}
|
||||
<div className="ms-now" onClick={handleTitleClick} title="点击进入后台" style={{ cursor: 'pointer' }}>
|
||||
<span className="ms-now-dot" />
|
||||
<span className="ms-now-name">{currentItem ? currentItem.name : '等待播放…'}</span>
|
||||
</div>
|
||||
{/* 声音 */}
|
||||
<div id="soundHint" className="ms-sound" style={{ display: soundBlocked && loaded ? 'flex' : 'none' }} onClick={enableSound}>
|
||||
<Icon name="volume" size={16} />
|
||||
点击开启声音
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* 页脚:园区概览(与数据大屏一致) */}
|
||||
<ParkOverviewStrip d={d} />
|
||||
|
||||
{/* 登录弹窗 */}
|
||||
<div className={`ms-login-overlay${showLoginModal ? ' visible' : ''}`} onClick={(e) => { if (e.target === e.currentTarget) handleCloseLogin(); }}>
|
||||
<div className="ms-login-modal" onClick={e => e.stopPropagation()}>
|
||||
<button className="ms-login-close" onClick={handleCloseLogin}>×</button>
|
||||
<div className="ms-login-brand">
|
||||
<h2>后台管理</h2>
|
||||
<p>请输入账号密码登录</p>
|
||||
</div>
|
||||
<div className="ms-login-field">
|
||||
<label>账号</label>
|
||||
<input
|
||||
ref={loginInputRef}
|
||||
value={loginUsername}
|
||||
onChange={e => { setLoginUsername(e.target.value); setLoginError(''); }}
|
||||
onKeyDown={handleLoginKeyDown}
|
||||
placeholder="请输入账号"
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
<div className="ms-login-field">
|
||||
<label>密码</label>
|
||||
<input
|
||||
type="password"
|
||||
value={loginPassword}
|
||||
onChange={e => { setLoginPassword(e.target.value); setLoginError(''); }}
|
||||
onKeyDown={handleLoginKeyDown}
|
||||
placeholder="请输入密码"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
{loginError && <div className="ms-login-error">{loginError}</div>}
|
||||
<button className="ms-login-btn" onClick={handleLoginSubmit}>登 录</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user