feat: 播放控制移入页眉 + 媒体铺满智能裁切
页眉播放控制(仅媒体轮播页): - 新增 MediaHeaderControls:播放/暂停、上一段/下一段、序号、当前文件名(点击进后台)、声音解锁 - 新增 mediaControl 共享状态桥:媒体页同步播放状态,页眉订阅渲染并下发指令 - 移除媒体页底部控制区,媒体区保持纯净全屏 媒体展示: - .ms-media 改为 object-fit: cover,填满整个容器并智能居中裁切(原 contain 留黑边) 修复: - MediaScreen useEffect 依赖数组引用后声明函数导致的 TDZ 崩溃 - 清理 MediaScreen 中不再使用的 Icon/cleanName/togglePlayPause
This commit is contained in:
@@ -0,0 +1,78 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import Icon from './Icons';
|
||||||
|
import { onMediaUiState, mediaCommand } from '../utils/mediaControl';
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
页眉媒体控制区 —— 仅媒体轮播页显示
|
||||||
|
订阅媒体页播放状态(paused/index/total/name/soundBlocked),
|
||||||
|
点击通过 mediaCommand() 下发指令,由 MediaScreen 执行
|
||||||
|
========================================================= */
|
||||||
|
|
||||||
|
/** 展示名清洗:URL 只取文件名,不显示完整播放地址 */
|
||||||
|
function cleanName(name) {
|
||||||
|
if (!name) return '';
|
||||||
|
const text = String(name);
|
||||||
|
try {
|
||||||
|
const u = new URL(text);
|
||||||
|
const last = decodeURIComponent(u.pathname.split('/').filter(Boolean).pop() || '');
|
||||||
|
return last ? `${last}${u.search ? ' …' : ''}` : text;
|
||||||
|
} catch {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MediaHeaderControls() {
|
||||||
|
const [st, setSt] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => onMediaUiState(setSt), []);
|
||||||
|
|
||||||
|
if (!st || !st.loaded) {
|
||||||
|
return (
|
||||||
|
<div className="bd-hd-media" title="媒体播放控制">
|
||||||
|
<Icon name="player-play" size={13} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const showName = cleanName(st.name) || '等待播放…';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bd-hd-media">
|
||||||
|
{/* 播放 / 暂停 */}
|
||||||
|
<button
|
||||||
|
className={`bd-hd-media-btn ${st.paused ? 'primary' : ''}`}
|
||||||
|
onClick={() => mediaCommand(st.paused ? 'play' : 'pause')}
|
||||||
|
title={st.paused ? '播放' : '暂停'}
|
||||||
|
>
|
||||||
|
<Icon name={st.paused ? 'play' : 'pause'} size={14} />
|
||||||
|
</button>
|
||||||
|
{/* 上一段 */}
|
||||||
|
<button className="bd-hd-media-btn" onClick={() => mediaCommand('prev')} title="上一段">
|
||||||
|
<Icon name="chevron-left" size={13} />
|
||||||
|
</button>
|
||||||
|
{/* 序号 */}
|
||||||
|
<span className="bd-hd-media-num">
|
||||||
|
<b>{st.index + 1}</b>/<em>{st.total}</em>
|
||||||
|
</span>
|
||||||
|
{/* 下一段 */}
|
||||||
|
<button className="bd-hd-media-btn" onClick={() => mediaCommand('next')} title="下一段">
|
||||||
|
<Icon name="chevron-right" size={13} />
|
||||||
|
</button>
|
||||||
|
{/* 当前播放名(点击进入后台) */}
|
||||||
|
<button
|
||||||
|
className="bd-hd-media-name"
|
||||||
|
onClick={() => mediaCommand('open_login')}
|
||||||
|
title="点击进入后台"
|
||||||
|
>
|
||||||
|
<span className="bd-hd-media-dot" />
|
||||||
|
<span>{showName}</span>
|
||||||
|
</button>
|
||||||
|
{/* 声音解锁 */}
|
||||||
|
{st.soundBlocked && (
|
||||||
|
<button className="bd-hd-media-btn warn" onClick={() => mediaCommand('enable_sound')} title="点击开启声音">
|
||||||
|
<Icon name="volume" size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import Icon from './Icons';
|
import Icon from './Icons';
|
||||||
|
import MediaHeaderControls from './MediaHeaderControls';
|
||||||
import { PAGE_ORDER, usePageIndex } from '../utils/pageNav';
|
import { PAGE_ORDER, usePageIndex } from '../utils/pageNav';
|
||||||
import { Avatar, AvatarImage, AvatarFallback } from '@appica/ui-react/avatar'
|
import { Avatar, AvatarImage, AvatarFallback } from '@appica/ui-react/avatar'
|
||||||
|
|
||||||
@@ -52,6 +53,8 @@ export default function PageHeader() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bd-header-right">
|
<div className="bd-header-right">
|
||||||
|
{/* 媒体轮播页:页眉内嵌播放控制 */}
|
||||||
|
{location.pathname === '/screen' && <MediaHeaderControls />}
|
||||||
<div className="bd-nav-keys" title="左右方向键或点击切换页面">
|
<div className="bd-nav-keys" title="左右方向键或点击切换页面">
|
||||||
{left && (
|
{left && (
|
||||||
<button className="bd-nav-key" onClick={() => navigate(left.path)}>
|
<button className="bd-nav-key" onClick={() => navigate(left.path)}>
|
||||||
|
|||||||
+51
-86
@@ -4,9 +4,9 @@ import * as api from '../utils/api';
|
|||||||
import { API_BASE } from '../utils/api';
|
import { API_BASE } from '../utils/api';
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||||
import { enable, disable } from '@tauri-apps/plugin-autostart';
|
import { enable, disable } from '@tauri-apps/plugin-autostart';
|
||||||
import Icon from '../components/Icons';
|
|
||||||
import ParkOverviewStrip from '../components/ParkOverviewStrip';
|
import ParkOverviewStrip from '../components/ParkOverviewStrip';
|
||||||
import { useParkSim } from '../utils/parkData';
|
import { useParkSim } from '../utils/parkData';
|
||||||
|
import { setMediaUiState } from '../utils/mediaControl';
|
||||||
import '../styles/datascreen.css';
|
import '../styles/datascreen.css';
|
||||||
import '../styles/mediascreen.css';
|
import '../styles/mediascreen.css';
|
||||||
|
|
||||||
@@ -16,19 +16,6 @@ import '../styles/mediascreen.css';
|
|||||||
页脚(园区概览条)按需保留,中间为全屏媒体播放器
|
页脚(园区概览条)按需保留,中间为全屏媒体播放器
|
||||||
========================================================= */
|
========================================================= */
|
||||||
|
|
||||||
/** 展示名清洗:URL 只取文件名,不显示完整播放地址 */
|
|
||||||
function cleanName(name) {
|
|
||||||
if (!name) return '';
|
|
||||||
const text = String(name);
|
|
||||||
try {
|
|
||||||
const u = new URL(text);
|
|
||||||
const last = decodeURIComponent(u.pathname.split('/').filter(Boolean).pop() || '');
|
|
||||||
return last ? `${last}${u.search ? ' …' : ''}` : text;
|
|
||||||
} catch {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MediaScreen() {
|
export default function MediaScreen() {
|
||||||
const d = useParkSim();
|
const d = useParkSim();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -322,6 +309,48 @@ export default function MediaScreen() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 播放状态同步到页眉控制区
|
||||||
|
useEffect(() => {
|
||||||
|
const item = list[index];
|
||||||
|
setMediaUiState({
|
||||||
|
paused,
|
||||||
|
index,
|
||||||
|
total: list.length,
|
||||||
|
name: item ? item.name : '',
|
||||||
|
soundBlocked,
|
||||||
|
loaded,
|
||||||
|
});
|
||||||
|
}, [paused, index, list, soundBlocked, loaded]);
|
||||||
|
|
||||||
// MQTT 控制通道(后端 opc/display/command → window 事件)
|
// MQTT 控制通道(后端 opc/display/command → window 事件)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e) => {
|
const handler = (e) => {
|
||||||
@@ -352,6 +381,12 @@ export default function MediaScreen() {
|
|||||||
case 'minimize':
|
case 'minimize':
|
||||||
getCurrentWindow().minimize().catch(() => {});
|
getCurrentWindow().minimize().catch(() => {});
|
||||||
break;
|
break;
|
||||||
|
case 'open_login':
|
||||||
|
handleTitleClick();
|
||||||
|
break;
|
||||||
|
case 'enable_sound':
|
||||||
|
enableSound();
|
||||||
|
break;
|
||||||
case 'playlist_changed':
|
case 'playlist_changed':
|
||||||
reloadPlaylist();
|
reloadPlaylist();
|
||||||
break;
|
break;
|
||||||
@@ -364,36 +399,7 @@ export default function MediaScreen() {
|
|||||||
};
|
};
|
||||||
window.addEventListener('dpm:media-control', handler);
|
window.addEventListener('dpm:media-control', handler);
|
||||||
return () => window.removeEventListener('dpm:media-control', handler);
|
return () => window.removeEventListener('dpm:media-control', handler);
|
||||||
}, [skip, reloadPlaylist, applySettings, playTarget]);
|
}, [skip, reloadPlaylist, applySettings, playTarget, handleTitleClick, enableSound]);
|
||||||
|
|
||||||
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(() => {
|
const handleCloseLogin = useCallback(() => {
|
||||||
if (loginTimerRef.current) clearTimeout(loginTimerRef.current);
|
if (loginTimerRef.current) clearTimeout(loginTimerRef.current);
|
||||||
@@ -419,18 +425,6 @@ export default function MediaScreen() {
|
|||||||
if (e.key === 'Enter') handleLoginSubmit();
|
if (e.key === 'Enter') handleLoginSubmit();
|
||||||
}, [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
|
// Global click for sound unblock
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
@@ -509,36 +503,7 @@ export default function MediaScreen() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 右下角轻量播放控制(弱化) */}
|
{/* 播放控制已上移页眉(PageHeader 内渲染,媒体区保持纯净全屏) */}
|
||||||
<div className={`ms-controls${loaded ? ' visible' : ''}`}>
|
|
||||||
{/* 播放/暂停 */}
|
|
||||||
<button className="ms-play" onClick={togglePlayPause} title={paused ? '播放' : '暂停'}>
|
|
||||||
<Icon name={paused ? 'play' : 'pause'} size={15} />
|
|
||||||
</button>
|
|
||||||
{/* 上一段 / 下一段 */}
|
|
||||||
<div className="ms-nav">
|
|
||||||
<button onClick={() => skip(-1)} title="上一段">
|
|
||||||
<Icon name="chevron-left" size={13} />
|
|
||||||
</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={13} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/* 当前播放(仅文件名,不显示完整地址;点击进入后台) */}
|
|
||||||
<div className="ms-now" onClick={handleTitleClick} title="点击进入后台" style={{ cursor: 'pointer' }}>
|
|
||||||
<span className="ms-now-dot" />
|
|
||||||
<span className="ms-now-name">{currentItem ? cleanName(currentItem.name) : '等待播放…'}</span>
|
|
||||||
</div>
|
|
||||||
{/* 声音 */}
|
|
||||||
<div id="soundHint" className="ms-sound" style={{ display: soundBlocked && loaded ? 'flex' : 'none' }} onClick={enableSound}>
|
|
||||||
<Icon name="volume" size={13} />
|
|
||||||
点击开启声音
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* 页脚:园区概览(与数据大屏一致) */}
|
{/* 页脚:园区概览(与数据大屏一致) */}
|
||||||
|
|||||||
@@ -2461,3 +2461,96 @@
|
|||||||
.bd-cell-jobs { grid-column: 5; }
|
.bd-cell-jobs { grid-column: 5; }
|
||||||
|
|
||||||
/* 底部园区概览(见 .bd-strip 区块) */
|
/* 底部园区概览(见 .bd-strip 区块) */
|
||||||
|
|
||||||
|
/* ============ 页眉媒体播放控制(仅媒体轮播页) ============ */
|
||||||
|
.bd-hd-media {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
border: 1px solid rgba(47, 107, 255, 0.14);
|
||||||
|
border-radius: 9px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
.bd-hd-media-btn {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--bd-blue);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.bd-hd-media-btn:hover {
|
||||||
|
background: rgba(47, 107, 255, 0.12);
|
||||||
|
}
|
||||||
|
.bd-hd-media-btn.primary {
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(140deg, var(--bd-blue), var(--bd-cyan));
|
||||||
|
box-shadow: 0 3px 8px rgba(47, 107, 255, 0.3);
|
||||||
|
}
|
||||||
|
.bd-hd-media-btn.warn {
|
||||||
|
color: var(--bd-orange);
|
||||||
|
}
|
||||||
|
.bd-hd-media-num {
|
||||||
|
font-family: "JetBrains Mono", "Oswald", monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--bd-sub);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
.bd-hd-media-num b {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--bd-ink);
|
||||||
|
}
|
||||||
|
.bd-hd-media-num em {
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.bd-hd-media-name {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
max-width: 160px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: inherit;
|
||||||
|
color: var(--bd-sub);
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.bd-hd-media-name > span:last-child {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.bd-hd-media-name:hover {
|
||||||
|
background: rgba(47, 107, 255, 0.08);
|
||||||
|
color: var(--bd-ink);
|
||||||
|
}
|
||||||
|
.bd-hd-media-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--bd-green);
|
||||||
|
box-shadow: 0 0 6px rgba(23, 178, 106, 0.8);
|
||||||
|
animation: bdPulse 1.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
/* 窄屏:页眉控制区收起名称,保留核心按钮 */
|
||||||
|
@media (max-width: 1280px) {
|
||||||
|
.bd-hd-media-name { display: none; }
|
||||||
|
}
|
||||||
|
|||||||
+4
-123
@@ -67,13 +67,14 @@
|
|||||||
-webkit-backdrop-filter: blur(14px);
|
-webkit-backdrop-filter: blur(14px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 媒体播放器(铺满媒体区,等比适配) */
|
/* 媒体播放器(填满整个媒体区,等比裁切铺满) */
|
||||||
.ms-media {
|
.ms-media {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,127 +110,7 @@
|
|||||||
border-bottom: 1px solid rgba(47, 107, 255, 0.3);
|
border-bottom: 1px solid rgba(47, 107, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 右下角轻量播放控制(弱化悬浮) */
|
/* 播放控制已上移页眉(见 datascreen.css .bd-hd-media) */
|
||||||
.ms-controls {
|
|
||||||
position: absolute;
|
|
||||||
right: 16px;
|
|
||||||
bottom: 14px;
|
|
||||||
left: auto;
|
|
||||||
transform: none;
|
|
||||||
z-index: 10;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
background: rgba(255, 255, 255, 0.55);
|
|
||||||
border: 1px solid rgba(120, 160, 220, 0.16);
|
|
||||||
border-radius: 30px;
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
-webkit-backdrop-filter: blur(12px);
|
|
||||||
box-shadow: 0 6px 22px rgba(56, 100, 170, 0.12);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.5s ease;
|
|
||||||
}
|
|
||||||
.ms-controls.visible { opacity: 1; }
|
|
||||||
|
|
||||||
.ms-play {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: none;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: #fff;
|
|
||||||
background: linear-gradient(140deg, var(--bd-blue, #2f6bff), var(--bd-cyan, #4cc2ff));
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: 0 4px 12px rgba(47, 107, 255, 0.3);
|
|
||||||
transition: transform 0.2s;
|
|
||||||
}
|
|
||||||
.ms-play:hover { transform: scale(1.08); }
|
|
||||||
|
|
||||||
.ms-nav {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
.ms-nav button {
|
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1px solid rgba(47, 107, 255, 0.18);
|
|
||||||
background: rgba(255, 255, 255, 0.6);
|
|
||||||
color: var(--bd-blue, #2f6bff);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
.ms-nav button:hover {
|
|
||||||
background: rgba(47, 107, 255, 0.12);
|
|
||||||
border-color: rgba(47, 107, 255, 0.5);
|
|
||||||
}
|
|
||||||
.ms-nav-num {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 3px;
|
|
||||||
font-family: "JetBrains Mono", "Oswald", monospace;
|
|
||||||
}
|
|
||||||
.ms-nav-num b {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--bd-ink, #1d2c44);
|
|
||||||
}
|
|
||||||
.ms-nav-num em {
|
|
||||||
font-style: normal;
|
|
||||||
font-size: 10.5px;
|
|
||||||
color: var(--bd-sub, #7d8fa9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-now {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding-left: 10px;
|
|
||||||
border-left: 1px solid rgba(47, 107, 255, 0.12);
|
|
||||||
}
|
|
||||||
.ms-now-dot {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--bd-green, #17b26a);
|
|
||||||
box-shadow: 0 0 8px rgba(23, 178, 106, 0.8);
|
|
||||||
animation: msPulse 1.6s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
@keyframes msPulse {
|
|
||||||
0%, 100% { opacity: 1; transform: scale(1); }
|
|
||||||
50% { opacity: 0.4; transform: scale(0.75); }
|
|
||||||
}
|
|
||||||
.ms-now-name {
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--bd-sub, #7d8fa9);
|
|
||||||
max-width: 160px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 声音提示 */
|
|
||||||
.ms-sound {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 5px 11px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #4a5b76;
|
|
||||||
background: rgba(255, 255, 255, 0.75);
|
|
||||||
border: 1px solid rgba(47, 107, 255, 0.14);
|
|
||||||
border-radius: 30px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.ms-sound .appica-ico { color: var(--bd-blue, #2f6bff); }
|
|
||||||
|
|
||||||
/* 页脚 */
|
/* 页脚 */
|
||||||
.ms-page .bd-strip { z-index: 10; position: relative; }
|
.ms-page .bd-strip { z-index: 10; position: relative; }
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/* =========================================================
|
||||||
|
媒体播放控制桥 —— 页眉控制区与媒体页之间的共享状态
|
||||||
|
· 媒体页把播放状态同步进来(paused/index/total/name/soundBlocked/loaded)
|
||||||
|
· 页眉控制区订阅状态渲染,并通过 mediaCommand() 下发指令
|
||||||
|
========================================================= */
|
||||||
|
|
||||||
|
let state = {
|
||||||
|
paused: false,
|
||||||
|
index: 0,
|
||||||
|
total: 0,
|
||||||
|
name: '',
|
||||||
|
soundBlocked: false,
|
||||||
|
loaded: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const listeners = new Set();
|
||||||
|
|
||||||
|
export function setMediaUiState(patch) {
|
||||||
|
state = { ...state, ...patch };
|
||||||
|
listeners.forEach((fn) => fn(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMediaUiState() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 订阅状态,返回取消函数 */
|
||||||
|
export function onMediaUiState(fn) {
|
||||||
|
listeners.add(fn);
|
||||||
|
fn(state);
|
||||||
|
return () => listeners.delete(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 页眉控制区下发指令(媒体页监听 dpm:media-control 执行) */
|
||||||
|
export function mediaCommand(action, params) {
|
||||||
|
window.dispatchEvent(new CustomEvent('dpm:media-control', { detail: { action, params } }));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user