@@ -1,5 +1,6 @@
import { useState , useEffect , useCallback , useRef } from 'react' ;
import * as api from '../utils/api' ;
import { API _BASE } from '../utils/api' ;
import '../styles/admin.css' ;
/* ============ SVG Icons ============ */
@@ -67,7 +68,7 @@ function PreviewModal({ item, onClose, onAddToPlaylist, onDelete }) {
if ( ! item ) return null ;
const previewUrl = item . source === 'url' ? item . relative _path : ` /file/ ${ item . relative _path } ` ;
const previewUrl = item . source === 'url' ? item . relative _path : ` ${ API _BASE } /file/ ${ item . relative _path } ` ;
const overlayClass = ` modal-overlay ${ closing ? ' closing' : '' } ` ;
return (
@@ -101,14 +102,17 @@ function PreviewModal({ item, onClose, onAddToPlaylist, onDelete }) {
/* ============ Media Card ============ */
function MediaCard ( { item , onPreview , onAddToPlaylist , onDelete , showRemove , onRemove } ) {
const thumbUrl = item . source === 'url' ? item . relative _path : ` /file/ ${ item . relative _path } ` ;
const thumbUrl = item . source === 'url' ? item . relative _path : ` ${ API _BASE } /file/ ${ item . relative _path } ` ;
return (
< div className = "media-card" >
{ item . type === 'image' ? (
< img src = { thumbUrl } loading = "lazy" className = "thumb" alt = { item . name } onClick = { ( ) => onPreview ( item ) } / >
) : (
< video src = { thumbUrl } preload = "metadata" className = "thumb" onClick = { ( ) => onPreview ( item ) } / >
< div className = "thumb-video-wrap " onClick = { ( ) => onPreview ( item ) } >
< video src = { ` ${ thumbUrl } #t=0.5 ` } preload = "auto" muted playsInline className = "thumb" / >
< span className = "play-badge" > ▶ < / span >
< / div >
) }
< div className = "body" >
< span className = "name" title = { item . name } > { item . name } < / span >
@@ -153,6 +157,7 @@ export default function Admin() {
const [ previewItem , setPreviewItem ] = useState ( null ) ;
const [ toasts , setToasts ] = useState ( [ ] ) ;
const toastIdRef = useRef ( 0 ) ;
const volTimerRef = useRef ( null ) ;
// ============ Toast ============
const addToast = useCallback ( ( msg , type = 'success' ) => {
@@ -221,7 +226,7 @@ export default function Admin() {
// ============ SSE ============
useEffect ( ( ) => {
if ( ! isLoggedIn ) return ;
const es = new EventSource ( ' /api/events' ) ;
const es = new EventSource ( ` ${ API _BASE } /api/events ` ) ;
es . onmessage = ( e ) => {
try {
const msg = JSON . parse ( e . data ) ;
@@ -270,10 +275,6 @@ export default function Admin() {
loadAll ( ) ;
} ;
const handleSaveSettings = async ( ) => {
await api . updateSettings ( { play _mode : playMode , image _duration : imageDuration , volume } ) ;
} ;
const handleAddToPlaylist = async ( path ) => {
await api . addToPlaylist ( path ) ;
addToast ( '已加入播放列表' ) ;
@@ -369,24 +370,24 @@ export default function Admin() {
{ /* Settings Card */ }
< div className = "card" >
< div className = "card-header" >
< div className = "card-icon" > { '\u2699' } < / div >
< div className = "card-icon" > < svg t = "1778605826535" class = "icon" viewBox = "0 0 1024 1024" version = "1.1" xmlns = "http://www.w3.org/2000/svg" p - id = "2889" width = "200" height = "200" > < path d = "M564 771.47H171c-16.57 0-30-13.43-30-30V566.78c0-78.12 63.55-141.67 141.67-141.67H564c16.57 0 30 13.43 30 30v286.36c0 16.57-13.43 30-30 30z m-363-60h333V485.11H282.67c-45.03 0-81.67 36.64-81.67 81.67v144.69z" fill = "#9BC5ED" p - id = "2890" > < / path > < path d = "M830.72 212.82l-77.32-44.64c-30.39-17.55-68.38 4.39-68.38 39.48v230.42c-35.84-37.33-86.24-60.58-142.08-60.58-108.76 0-196.93 88.17-196.93 196.93s88.17 196.93 196.93 196.93 196.93-88.17 196.93-196.93c0-1.32-0.02-2.63-0.05-3.95 0.03-0.51 0.05-1.02 0.05-1.53V341.6a46.01 46.01 0 0 0 13.53-5.19l77.32-44.64c30.39-17.55 30.39-61.42 0-78.96z" fill = "#1D5DCE" p - id = "2891" > < / path > < path d = "M630.69 501.22m-30.86 0a30.86 30.86 0 1 0 61.72 0 30.86 30.86 0 1 0-61.72 0Z" fill = "#FFFFFF" p - id = "2892" > < / path > < path d = "M865.63 671.66l-146.65-84.67c-28.18-16.27-63.41 4.07-63.41 36.61v169.34c0 32.54 35.23 52.88 63.41 36.61l146.65-84.67c28.18-16.27 28.18-56.95 0-73.22z" fill = "#9BC5ED" p - id = "2893" > < / path > < / svg > < / div >
< h3 > 播放设置 < / h3 >
< / div >
< div className = "form-inline" >
< div className = "form-group" >
< label > 播放模式 < / label >
< select value = { playMode } onChange = { e => { setPlayMode ( e . target . value ) ; setTimeout ( handleSaveSettings , 0 ) ; } } >
< select value = { playMode } onChange = { e => { const v = e . target . value ; setPlayMode ( v ) ; api . updateSettings ( { play _mode : v } ) ; } } >
< option value = "sequential" > 顺序播放 < / option >
< option value = "random" > 随机播放 < / option >
< / select >
< / div >
< div className = "form-group" >
< label > 图片时长 ( 秒 ) < / label >
< input type = "number" value = { imageDuration } min = "1" max = "300" onChange = { e => { setImageDuration ( Number ( e . target . value ) ) ; setTimeout ( handleSaveSettings , 0 ) ; } } / >
< input type = "number" value = { imageDuration } min = "1" max = "300" onChange = { e => { const v = Number ( e . target . value ) ; setImageDuration ( v ) ; api . updateSettings ( { image _duration : v } ) ; } } / >
< / div >
< div className = "form-group" >
< label > 音量 < / label >
< input type = "range" min = "0" max = "100" value = { volume } onChange = { e => { setVolume ( Number ( e . target . value ) ) ; setTimeout ( handleSaveSettings , 0 ) ; } } / >
< input type = "range" min = "0" max = "100" value = { volume } onChange = { e => { const v = Number ( e . target . value ) ; setVolume ( v ) ; if ( volTimerRef . current ) clearTimeout ( volTimerRef . current ) ; volTimerRef . current = setTimeout ( ( ) => api . updateSettings ( { volume : v } ) , 200 ) ; } } / >
< div className = "volume-label" > { volume } % < / div >
< / div >
< / div >
@@ -395,7 +396,7 @@ export default function Admin() {
{ /* Upload Card */ }
< div className = "card" >
< div className = "card-header" >
< div className = "card-icon" > { '\uD83D\uDCE4' } < / div >
< div className = "card-icon" > < svg t = "1778605682023" class = "icon" viewBox = "0 0 1024 1024" version = "1.1" xmlns = "http://www.w3.org/2000/svg" p - id = "2373" width = "200" height = "200" > < path d = "M171.47 182.43m102.33 0l479.61 0q102.33 0 102.33 102.33l0 327.32q0 102.33-102.33 102.33l-479.61 0q-102.33 0-102.33-102.33l0-327.32q0-102.33 102.33-102.33Z" fill = "#1D5DCE" p - id = "2374" > < / path > < path d = "M369.83 789.15m24 0l239.55 0q24 0 24 24l0 3.81q0 24-24 24l-239.55 0q-24 0-24-24l0-3.81q0-24 24-24Z" fill = "#9BC5ED" p - id = "2375" > < / path > < path d = "M602.01 409.07l-131.36-75.84c-30.29-17.49-68.16 4.37-68.16 39.35v151.68c0 34.98 37.87 56.84 68.16 39.35l131.36-75.84c30.29-17.49 30.29-61.22 0-78.71z" fill = "#FFFFFF" p - id = "2376" > < / path > < / svg > < / div >
< h3 > 添加媒体 < / h3 >
< / div >
< div className = "form-group" >
@@ -417,7 +418,7 @@ export default function Admin() {
{ /* Media Library */ }
< div className = "card full" >
< div className = "card-header" >
< div className = "card-icon" > { '\uD83D\uDCC1' } < / div >
< div className = "card-icon" > < svg t = "1778605713121" class = "icon" viewBox = "0 0 1024 1024" version = "1.1" xmlns = "http://www.w3.org/2000/svg" p - id = "2544" width = "200" height = "200" > < path d = "M271.77 238.65m57.41 0l411.19 0q57.41 0 57.41 57.41l0 337.03q0 57.41-57.41 57.41l-411.19 0q-57.41 0-57.41-57.41l0-337.03q0-57.41 57.41-57.41Z" fill = "#9BC5ED" p - id = "2545" > < / path > < path d = "M774.49 824H252.84c-36.18 0-65.51-29.33-65.51-65.51V437.66c0-36.18 29.33-65.51 65.51-65.51h224.04c22.54 0 43.49-11.58 55.47-30.67l68.98-109.82c11.99-19.08 32.94-30.67 55.47-30.67h117.68c36.18 0 65.51 29.33 65.51 65.51v491.98c0 36.18-29.33 65.51-65.51 65.51z" fill = "#1D5DCE" p - id = "2546" > < / path > < path d = "M577.25 672.42m24 0l97.53 0q24 0 24 24l0 11.11q0 24-24 24l-97.53 0q-24 0-24-24l0-11.11q0-24 24-24Z" fill = "#FFFFFF" p - id = "2547" > < / path > < / svg > < / div >
< h3 > 媒体库 < / h3 >
< / div >
< div className = "media-grid" >
@@ -443,7 +444,7 @@ export default function Admin() {
{ /* Playlist */ }
< div className = "card full" >
< div className = "card-header" >
< div className = "card-icon" > { '\u25B6' } < / div >
< div className = "card-icon" > < svg t = "1778605746078" class = "icon" viewBox = "0 0 1024 1024" version = "1.1" xmlns = "http://www.w3.org/2000/svg" p - id = "2715" width = "200" height = "200" > < path d = "M279.22 176m86.62 0l311.75 0q86.62 0 86.62 86.62l0 464.37q0 86.62-86.62 86.62l-311.75 0q-86.62 0-86.62-86.62l0-464.37q0-86.62 86.62-86.62Z" fill = "#1D5DCE" p - id = "2716" > < / path > < path d = "M199.22 570.82l-0.08 231.85c0 29.45 23.86 53.33 53.31 53.33h395.81c54.36 0 73.85-71.82 26.95-99.3L279.47 524.85c-35.53-20.82-80.24 4.8-80.25 45.98z" fill = "#9BC5ED" p - id = "2717" > < / path > < path d = "M844.62 570.82l0.08 231.85c0 29.45-23.86 53.33-53.31 53.33H395.58c-54.36 0-73.85-71.82-26.95-99.3l395.74-231.85c35.53-20.82 80.24 4.8 80.25 45.98z" fill = "#9BC5ED" p - id = "2718" > < / path > < path d = "M397.94 308.15m24 0l209.55 0q24 0 24 24l0 3.81q0 24-24 24l-209.55 0q-24 0-24-24l0-3.81q0-24 24-24Z" fill = "#FFFFFF" p - id = "2719" > < / path > < path d = "M397.94 419.15m24 0l129.55 0q24 0 24 24l0 3.81q0 24-24 24l-129.55 0q-24 0-24-24l0-3.81q0-24 24-24Z" fill = "#FFFFFF" p - id = "2720" > < / path > < path d = "M502.13 752.53l-31.42 36.81c-14.42 16.89-2.41 42.91 19.79 42.91h62.85c22.2 0 34.2-26.02 19.79-42.91l-31.42-36.81c-10.39-12.17-29.19-12.17-39.58 0z" fill = "#1D5DCE" p - id = "2721" > < / path > < / svg > < / div >
< h3 > 播放列表 < / h3 >
< / div >
< div className = "media-grid" id = "playlistGrid" >