- 更新前端支持触控操作
This commit is contained in:
+157
-12
@@ -1,7 +1,9 @@
|
||||
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 '../styles/screen.css';
|
||||
|
||||
/* ============ Loader ============ */
|
||||
@@ -29,6 +31,7 @@ function Loader({ hidden, text }) {
|
||||
Screen Page
|
||||
========================================================= */
|
||||
export default function Screen() {
|
||||
const navigate = useNavigate();
|
||||
const [list, setList] = useState([]);
|
||||
const [index, setIndex] = useState(0);
|
||||
const [volume, setVolume] = useState(0.8);
|
||||
@@ -40,10 +43,16 @@ export default function Screen() {
|
||||
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 videoRef = useRef(null);
|
||||
const imgRef = useRef(null);
|
||||
const timerRef = useRef(null);
|
||||
const loginTimerRef = useRef(null);
|
||||
const loginInputRef = useRef(null);
|
||||
|
||||
// ref 存储可变值,避免回调中的闭包过期
|
||||
const listRef = useRef(list);
|
||||
@@ -53,6 +62,10 @@ export default function Screen() {
|
||||
const imageDurationRef = useRef(imageDuration);
|
||||
const soundBlockedRef = useRef(soundBlocked);
|
||||
|
||||
// 从后端同步的显示设置(Screen 上无需 UI 控制,但需要响应 SSE)
|
||||
const [fullscreenMode, setFullscreenMode] = useState(true);
|
||||
const [autostartEnabled, setAutostartEnabled] = useState(false);
|
||||
|
||||
useEffect(() => { listRef.current = list; }, [list]);
|
||||
useEffect(() => { indexRef.current = index; }, [index]);
|
||||
useEffect(() => { pausedRef.current = paused; }, [paused]);
|
||||
@@ -232,6 +245,9 @@ export default function Screen() {
|
||||
setPaused(false);
|
||||
skip(-1);
|
||||
break;
|
||||
case 'minimize_window':
|
||||
getCurrentWindow().minimize().catch(() => {});
|
||||
break;
|
||||
case 'playlist_changed':
|
||||
reloadPlaylist();
|
||||
break;
|
||||
@@ -298,6 +314,18 @@ export default function Screen() {
|
||||
}
|
||||
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(() => {
|
||||
@@ -314,6 +342,61 @@ export default function Screen() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
// ============ 点击标题弹出登录弹窗 ============
|
||||
const handleTitleClick = useCallback(() => {
|
||||
setLoginUsername('');
|
||||
setLoginPassword('');
|
||||
setLoginError('');
|
||||
setShowLoginModal(true);
|
||||
// 30 秒无操作自动关闭
|
||||
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]);
|
||||
|
||||
// ============ LIVE 点击切换播放/暂停 ============
|
||||
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 = () => {
|
||||
@@ -323,21 +406,38 @@ export default function Screen() {
|
||||
return () => document.removeEventListener('click', handler);
|
||||
}, [enableSound]);
|
||||
|
||||
// Escape 退出 Tauri 窗口全屏
|
||||
// 加载显示设置(大屏/小窗口、开机自启动)
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
const data = await api.getSettings();
|
||||
setFullscreenMode(data.fullscreen ?? true);
|
||||
setAutostartEnabled(data.autostart ?? false);
|
||||
} catch {/* ignore */}
|
||||
};
|
||||
load();
|
||||
}, []);
|
||||
|
||||
// Escape 退出 Tauri 窗口全屏 / 关闭登录弹窗
|
||||
useEffect(() => {
|
||||
const handler = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
getCurrentWindow().setFullscreen(false);
|
||||
if (showLoginModal) {
|
||||
handleCloseLogin();
|
||||
} else {
|
||||
getCurrentWindow().setFullscreen(false).catch(() => {});
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handler);
|
||||
return () => document.removeEventListener('keydown', handler);
|
||||
}, []);
|
||||
}, [showLoginModal, handleCloseLogin]);
|
||||
|
||||
// Cleanup timer on unmount
|
||||
// Cleanup timers on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
if (loginTimerRef.current) clearTimeout(loginTimerRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -373,24 +473,69 @@ export default function Screen() {
|
||||
/>
|
||||
|
||||
{/* Corner Info — Now Playing */}
|
||||
<div className={`corner-info${loaded && currentItem ? ' visible' : ''}`}>
|
||||
<div
|
||||
className={`corner-info${loaded && currentItem ? ' visible' : ''}`}
|
||||
onClick={handleTitleClick}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<div className="ci-dot"></div>
|
||||
<span className="ci-text">{'昆明市大学生创业园'}</span>
|
||||
</div>
|
||||
|
||||
{/* Connection Indicator */}
|
||||
<div className={`conn-indicator${loaded ? ' visible' : ''}`}>
|
||||
{/* Connection Indicator — 点击切换播放/暂停 */}
|
||||
<div className={`conn-indicator${loaded ? ' visible' : ''}`} onClick={togglePlayPause}>
|
||||
<div className="conn-rings">
|
||||
<div className="cr-inner"></div>
|
||||
<div className={`cr-inner${paused ? ' paused' : ''}`}></div>
|
||||
<div className="cr-outer"></div>
|
||||
</div>
|
||||
<span className="conn-label">Live</span>
|
||||
<span className="conn-label">{paused ? 'Paused' : 'Live'}</span>
|
||||
</div>
|
||||
|
||||
{/* Bottom Status */}
|
||||
{/* Bottom Status — 左侧点击上一段,右侧点击下一段 */}
|
||||
<div className={`status-bar${loaded ? ' visible' : ''}`}>
|
||||
<div className={`sb-dot${paused ? ' idle' : ''}`}></div>
|
||||
<span className="sb-text">{currentItem ? `${index + 1} / ${list.length}` : '0 / 0'}</span>
|
||||
<div className="sb-prev" onClick={() => skip(-1)}>
|
||||
<div className={`sb-dot${paused ? ' idle' : ''}`}></div>
|
||||
<span className="sb-text">{currentItem ? `${index + 1}` : '0'}</span>
|
||||
</div>
|
||||
<span className="sb-sep">/</span>
|
||||
<div className="sb-next" onClick={() => skip(1)}>
|
||||
<span className="sb-text">{list.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Login Modal */}
|
||||
<div className={`login-overlay${showLoginModal ? ' visible' : ''}`} onClick={(e) => { if (e.target === e.currentTarget) handleCloseLogin(); }}>
|
||||
<div className="login-modal" onClick={e => e.stopPropagation()}>
|
||||
<button className="login-modal-close" onClick={handleCloseLogin}>×</button>
|
||||
<div className="login-modal-brand">
|
||||
<h2>后台管理</h2>
|
||||
<p>请输入账号密码登录</p>
|
||||
</div>
|
||||
<div className="login-modal-field">
|
||||
<label>账号</label>
|
||||
<input
|
||||
ref={loginInputRef}
|
||||
value={loginUsername}
|
||||
onChange={e => { setLoginUsername(e.target.value); setLoginError(''); }}
|
||||
onKeyDown={handleLoginKeyDown}
|
||||
placeholder="请输入账号"
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
<div className="login-modal-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="login-modal-error">{loginError}</div>}
|
||||
<button className="login-modal-btn" onClick={handleLoginSubmit}>登 录</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Watermark */}
|
||||
|
||||
Reference in New Issue
Block a user