From 29bdad316f8ae8c309be4bca63eaabdbd311ba8d Mon Sep 17 00:00:00 2001 From: Pine Date: Mon, 17 Aug 2026 22:45:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=92=AD=E6=94=BE=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E7=A7=BB=E5=85=A5=E9=A1=B5=E7=9C=89=20+=20=E5=AA=92=E4=BD=93?= =?UTF-8?q?=E9=93=BA=E6=BB=A1=E6=99=BA=E8=83=BD=E8=A3=81=E5=88=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 页眉播放控制(仅媒体轮播页): - 新增 MediaHeaderControls:播放/暂停、上一段/下一段、序号、当前文件名(点击进后台)、声音解锁 - 新增 mediaControl 共享状态桥:媒体页同步播放状态,页眉订阅渲染并下发指令 - 移除媒体页底部控制区,媒体区保持纯净全屏 媒体展示: - .ms-media 改为 object-fit: cover,填满整个容器并智能居中裁切(原 contain 留黑边) 修复: - MediaScreen useEffect 依赖数组引用后声明函数导致的 TDZ 崩溃 - 清理 MediaScreen 中不再使用的 Icon/cleanName/togglePlayPause --- src/components/MediaHeaderControls.jsx | 78 ++++++++++++++ src/components/PageHeader.jsx | 3 + src/pages/MediaScreen.jsx | 137 +++++++++---------------- src/styles/datascreen.css | 93 +++++++++++++++++ src/styles/mediascreen.css | 127 +---------------------- src/utils/mediaControl.js | 37 +++++++ 6 files changed, 266 insertions(+), 209 deletions(-) create mode 100644 src/components/MediaHeaderControls.jsx create mode 100644 src/utils/mediaControl.js diff --git a/src/components/MediaHeaderControls.jsx b/src/components/MediaHeaderControls.jsx new file mode 100644 index 0000000..f179e19 --- /dev/null +++ b/src/components/MediaHeaderControls.jsx @@ -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 ( +
+ +
+ ); + } + + const showName = cleanName(st.name) || '等待播放…'; + + return ( +
+ {/* 播放 / 暂停 */} + + {/* 上一段 */} + + {/* 序号 */} + + {st.index + 1}/{st.total} + + {/* 下一段 */} + + {/* 当前播放名(点击进入后台) */} + + {/* 声音解锁 */} + {st.soundBlocked && ( + + )} +
+ ); +} diff --git a/src/components/PageHeader.jsx b/src/components/PageHeader.jsx index 2374e76..e70bcbc 100644 --- a/src/components/PageHeader.jsx +++ b/src/components/PageHeader.jsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import Icon from './Icons'; +import MediaHeaderControls from './MediaHeaderControls'; import { PAGE_ORDER, usePageIndex } from '../utils/pageNav'; import { Avatar, AvatarImage, AvatarFallback } from '@appica/ui-react/avatar' @@ -52,6 +53,8 @@ export default function PageHeader() {
+ {/* 媒体轮播页:页眉内嵌播放控制 */} + {location.pathname === '/screen' && }
{left && (
)} - {/* 右下角轻量播放控制(弱化) */} -
- {/* 播放/暂停 */} - - {/* 上一段 / 下一段 */} -
- - - {currentItem ? `${index + 1}` : '0'} - / {list.length} - - -
- {/* 当前播放(仅文件名,不显示完整地址;点击进入后台) */} -
- - {currentItem ? cleanName(currentItem.name) : '等待播放…'} -
- {/* 声音 */} -
- - 点击开启声音 -
-
+ {/* 播放控制已上移页眉(PageHeader 内渲染,媒体区保持纯净全屏) */} {/* 页脚:园区概览(与数据大屏一致) */} diff --git a/src/styles/datascreen.css b/src/styles/datascreen.css index 14ed484..c66ec80 100644 --- a/src/styles/datascreen.css +++ b/src/styles/datascreen.css @@ -2461,3 +2461,96 @@ .bd-cell-jobs { grid-column: 5; } /* 底部园区概览(见 .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; } +} diff --git a/src/styles/mediascreen.css b/src/styles/mediascreen.css index 6758dc3..fd61b71 100644 --- a/src/styles/mediascreen.css +++ b/src/styles/mediascreen.css @@ -67,13 +67,14 @@ -webkit-backdrop-filter: blur(14px); } -/* 媒体播放器(铺满媒体区,等比适配) */ +/* 媒体播放器(填满整个媒体区,等比裁切铺满) */ .ms-media { position: absolute; inset: 0; width: 100%; height: 100%; - object-fit: contain; + object-fit: cover; + object-position: center; background: transparent; } @@ -109,127 +110,7 @@ border-bottom: 1px solid rgba(47, 107, 255, 0.3); } -/* 右下角轻量播放控制(弱化悬浮) */ -.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); } +/* 播放控制已上移页眉(见 datascreen.css .bd-hd-media) */ /* 页脚 */ .ms-page .bd-strip { z-index: 10; position: relative; } diff --git a/src/utils/mediaControl.js b/src/utils/mediaControl.js new file mode 100644 index 0000000..690fa0e --- /dev/null +++ b/src/utils/mediaControl.js @@ -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 } })); +}