- 图标优化、视频缩略图
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "dpm",
|
"productName": "昆明大学生创业园展播系统",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"identifier": "com.pine.dpm",
|
"identifier": "com.pine.dpm",
|
||||||
"build": {
|
"build": {
|
||||||
|
|||||||
+16
-15
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import * as api from '../utils/api';
|
import * as api from '../utils/api';
|
||||||
|
import { API_BASE } from '../utils/api';
|
||||||
import '../styles/admin.css';
|
import '../styles/admin.css';
|
||||||
|
|
||||||
/* ============ SVG Icons ============ */
|
/* ============ SVG Icons ============ */
|
||||||
@@ -67,7 +68,7 @@ function PreviewModal({ item, onClose, onAddToPlaylist, onDelete }) {
|
|||||||
|
|
||||||
if (!item) return null;
|
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' : ''}`;
|
const overlayClass = `modal-overlay${closing ? ' closing' : ''}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -101,14 +102,17 @@ function PreviewModal({ item, onClose, onAddToPlaylist, onDelete }) {
|
|||||||
|
|
||||||
/* ============ Media Card ============ */
|
/* ============ Media Card ============ */
|
||||||
function MediaCard({ item, onPreview, onAddToPlaylist, onDelete, showRemove, onRemove }) {
|
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 (
|
return (
|
||||||
<div className="media-card">
|
<div className="media-card">
|
||||||
{item.type === 'image' ? (
|
{item.type === 'image' ? (
|
||||||
<img src={thumbUrl} loading="lazy" className="thumb" alt={item.name} onClick={() => onPreview(item)} />
|
<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">
|
<div className="body">
|
||||||
<span className="name" title={item.name}>{item.name}</span>
|
<span className="name" title={item.name}>{item.name}</span>
|
||||||
@@ -153,6 +157,7 @@ export default function Admin() {
|
|||||||
const [previewItem, setPreviewItem] = useState(null);
|
const [previewItem, setPreviewItem] = useState(null);
|
||||||
const [toasts, setToasts] = useState([]);
|
const [toasts, setToasts] = useState([]);
|
||||||
const toastIdRef = useRef(0);
|
const toastIdRef = useRef(0);
|
||||||
|
const volTimerRef = useRef(null);
|
||||||
|
|
||||||
// ============ Toast ============
|
// ============ Toast ============
|
||||||
const addToast = useCallback((msg, type = 'success') => {
|
const addToast = useCallback((msg, type = 'success') => {
|
||||||
@@ -221,7 +226,7 @@ export default function Admin() {
|
|||||||
// ============ SSE ============
|
// ============ SSE ============
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoggedIn) return;
|
if (!isLoggedIn) return;
|
||||||
const es = new EventSource('/api/events');
|
const es = new EventSource(`${API_BASE}/api/events`);
|
||||||
es.onmessage = (e) => {
|
es.onmessage = (e) => {
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(e.data);
|
const msg = JSON.parse(e.data);
|
||||||
@@ -270,10 +275,6 @@ export default function Admin() {
|
|||||||
loadAll();
|
loadAll();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveSettings = async () => {
|
|
||||||
await api.updateSettings({ play_mode: playMode, image_duration: imageDuration, volume });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddToPlaylist = async (path) => {
|
const handleAddToPlaylist = async (path) => {
|
||||||
await api.addToPlaylist(path);
|
await api.addToPlaylist(path);
|
||||||
addToast('已加入播放列表');
|
addToast('已加入播放列表');
|
||||||
@@ -369,24 +370,24 @@ export default function Admin() {
|
|||||||
{/* Settings Card */}
|
{/* Settings Card */}
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header">
|
<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>
|
<h3>播放设置</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-inline">
|
<div className="form-inline">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>播放模式</label>
|
<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="sequential">顺序播放</option>
|
||||||
<option value="random">随机播放</option>
|
<option value="random">随机播放</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>图片时长(秒)</label>
|
<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>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>音量</label>
|
<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 className="volume-label">{volume}%</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -395,7 +396,7 @@ export default function Admin() {
|
|||||||
{/* Upload Card */}
|
{/* Upload Card */}
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header">
|
<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>
|
<h3>添加媒体</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
@@ -417,7 +418,7 @@ export default function Admin() {
|
|||||||
{/* Media Library */}
|
{/* Media Library */}
|
||||||
<div className="card full">
|
<div className="card full">
|
||||||
<div className="card-header">
|
<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>
|
<h3>媒体库</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="media-grid">
|
<div className="media-grid">
|
||||||
@@ -443,7 +444,7 @@ export default function Admin() {
|
|||||||
{/* Playlist */}
|
{/* Playlist */}
|
||||||
<div className="card full">
|
<div className="card full">
|
||||||
<div className="card-header">
|
<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>
|
<h3>播放列表</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="media-grid" id="playlistGrid">
|
<div className="media-grid" id="playlistGrid">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import * as api from '../utils/api';
|
import * as api from '../utils/api';
|
||||||
|
import { API_BASE } from '../utils/api';
|
||||||
import '../styles/screen.css';
|
import '../styles/screen.css';
|
||||||
|
|
||||||
/* ============ Loader ============ */
|
/* ============ Loader ============ */
|
||||||
@@ -59,7 +60,7 @@ export default function Screen() {
|
|||||||
useEffect(() => { soundBlockedRef.current = soundBlocked; }, [soundBlocked]);
|
useEffect(() => { soundBlockedRef.current = soundBlocked; }, [soundBlocked]);
|
||||||
|
|
||||||
const getUrl = useCallback((item) => {
|
const getUrl = useCallback((item) => {
|
||||||
return item.source === 'url' ? item.relative_path : `/file/${item.relative_path}`;
|
return item.source === 'url' ? item.relative_path : `${API_BASE}/file/${item.relative_path}`;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ============ 核心播放控制 ============
|
// ============ 核心播放控制 ============
|
||||||
@@ -103,12 +104,12 @@ export default function Screen() {
|
|||||||
const item = list[index];
|
const item = list[index];
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|
||||||
// 同步状态到后端
|
// 同步状态到后端(字段名 type 与 Rust PlaybackState 的 #[serde(rename = "type")] 匹配)
|
||||||
api.updateState({
|
api.updateState({
|
||||||
status: 'playing',
|
status: 'playing',
|
||||||
index,
|
index,
|
||||||
name: item.name || '',
|
name: item.name || '',
|
||||||
media_type: item.type || '',
|
type: item.type || '',
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
|
|
||||||
if (item.type === 'video') {
|
if (item.type === 'video') {
|
||||||
@@ -208,7 +209,7 @@ export default function Screen() {
|
|||||||
|
|
||||||
// SSE 连接
|
// SSE 连接
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const es = new EventSource('/api/events');
|
const es = new EventSource(`${API_BASE}/api/events`);
|
||||||
es.onmessage = (e) => {
|
es.onmessage = (e) => {
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(e.data);
|
const msg = JSON.parse(e.data);
|
||||||
@@ -362,7 +363,7 @@ export default function Screen() {
|
|||||||
{/* Corner Info — Now Playing */}
|
{/* Corner Info — Now Playing */}
|
||||||
<div className={`corner-info${loaded && currentItem ? ' visible' : ''}`}>
|
<div className={`corner-info${loaded && currentItem ? ' visible' : ''}`}>
|
||||||
<div className="ci-dot"></div>
|
<div className="ci-dot"></div>
|
||||||
<span className="ci-text">{currentItem ? currentItem.name : '就绪'}</span>
|
<span className="ci-text">{'昆明市大学生创业园'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Connection Indicator */}
|
{/* Connection Indicator */}
|
||||||
@@ -382,7 +383,7 @@ export default function Screen() {
|
|||||||
|
|
||||||
{/* Sound Hint */}
|
{/* Sound Hint */}
|
||||||
<div id="soundHint" style={{ display: soundBlocked && loaded ? 'block' : 'none' }} onClick={enableSound}>
|
<div id="soundHint" style={{ display: soundBlocked && loaded ? 'block' : 'none' }} onClick={enableSound}>
|
||||||
点击启用声音
|
声音
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -696,6 +696,46 @@ button { font-family: "Poppins", "PingFang SC", sans-serif; cursor: pointer; tra
|
|||||||
display: block;
|
display: block;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.thumb-video-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 108px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
.thumb-video-wrap .thumb {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
.play-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0,0,0,0.55);
|
||||||
|
color: rgba(255,255,255,0.85);
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
border: 1.5px solid rgba(255,255,255,0.15);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.thumb-video-wrap:hover .play-badge {
|
||||||
|
background: rgba(0, 189, 125, 0.75);
|
||||||
|
color: #fff;
|
||||||
|
transform: translate(-50%, -50%) scale(1.1);
|
||||||
|
border-color: rgba(255,255,255,0.3);
|
||||||
|
}
|
||||||
.media-card .thumb-placeholder {
|
.media-card .thumb-placeholder {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 108px;
|
height: 108px;
|
||||||
@@ -926,6 +966,7 @@ button { font-family: "Poppins", "PingFang SC", sans-serif; cursor: pointer; tra
|
|||||||
.form-inline { flex-direction: column; gap: var(--space-16); }
|
.form-inline { flex-direction: column; gap: var(--space-16); }
|
||||||
.media-grid { grid-template-columns: repeat(2, 1fr); gap: var(--space-8); }
|
.media-grid { grid-template-columns: repeat(2, 1fr); gap: var(--space-8); }
|
||||||
.media-card .thumb { height: 88px; }
|
.media-card .thumb { height: 88px; }
|
||||||
|
.thumb-video-wrap { height: 88px; }
|
||||||
.media-card .body { padding: var(--space-8); }
|
.media-card .body { padding: var(--space-8); }
|
||||||
.media-card .body .name { font-size: 12px; }
|
.media-card .body .name { font-size: 12px; }
|
||||||
.media-card .body .actions button { font-size: 11px; padding: var(--space-4) var(--space-8); }
|
.media-card .body .actions button { font-size: 11px; padding: var(--space-4) var(--space-8); }
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
// API 基地址:Tauri 生产环境使用绝对路径,浏览器环境使用相对路径
|
// API 基地址:Tauri 生产环境使用绝对路径,浏览器环境使用相对路径
|
||||||
|
// 导出供 SSE、媒体文件等非 fetch 场景使用
|
||||||
const BASE = window.location.protocol === 'http:' || window.location.protocol === 'https:'
|
const BASE = window.location.protocol === 'http:' || window.location.protocol === 'https:'
|
||||||
? '' // 浏览器环境 — 同源请求
|
? '' // 浏览器环境 — 同源请求
|
||||||
: 'http://localhost:10801'; // Tauri 生产环境 (tauri://)
|
: 'http://localhost:10801'; // Tauri 生产环境 (tauri://)
|
||||||
|
export const API_BASE = BASE;
|
||||||
|
|
||||||
async function request(url, options = {}) {
|
async function request(url, options = {}) {
|
||||||
const res = await fetch(`${BASE}${url}`, {
|
const res = await fetch(`${BASE}${url}`, {
|
||||||
|
|||||||
Reference in New Issue
Block a user