From 8dda29e702a3e2fb938f0c475a77eaaea4fd0548 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 17 Feb 2026 17:57:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(video-edit):=20=E6=94=AF=E6=8C=81=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E7=BC=93=E5=AD=98=E8=A7=86=E9=A2=91=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E8=BF=9B=E5=85=A5=E7=BC=96=E8=BE=91=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/public_api/video.py | 10 +++ app/static/public/css/video.css | 38 ++++++++ app/static/public/js/video.js | 136 ++++++++++++++++++++++++++++- app/static/public/pages/video.html | 16 ++++ 4 files changed, 198 insertions(+), 2 deletions(-) diff --git a/app/api/v1/public_api/video.py b/app/api/v1/public_api/video.py index 2fc3d05..0a3976a 100644 --- a/app/api/v1/public_api/video.py +++ b/app/api/v1/public_api/video.py @@ -14,6 +14,7 @@ from app.core.logger import logger from app.services.grok.services.video import VideoService from app.services.grok.services.model import ModelService from app.api.v1.public_api import imagine as imagine_public_api +from app.services.grok.utils.cache import CacheService router = APIRouter() @@ -389,4 +390,13 @@ async def public_video_stop(data: VideoStopRequest): return {"status": "success", "removed": removed} +@router.get("/video/cache/list", dependencies=[Depends(verify_public_key)]) +async def public_video_cache_list(page: int = 1, page_size: int = 100): + page = max(1, int(page or 1)) + page_size = max(1, min(200, int(page_size or 100))) + cache_service = CacheService() + result = cache_service.list_files("video", page=page, page_size=page_size) + return {"status": "success", **result} + + __all__ = ["router"] diff --git a/app/static/public/css/video.css b/app/static/public/css/video.css index a8c252c..12c8e13 100644 --- a/app/static/public/css/video.css +++ b/app/static/public/css/video.css @@ -579,6 +579,44 @@ body { outline-offset: -1px; } +.cache-video-list { + max-height: 360px; + overflow: auto; + border: 1px solid var(--video-outline); + border-radius: 10px; + background: #fafafa; +} + +.cache-video-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 10px 12px; + border-bottom: 1px solid var(--video-outline); +} + +.cache-video-item:last-child { + border-bottom: none; +} + +.cache-video-meta { + min-width: 0; +} + +.cache-video-name { + font-size: 12px; + color: var(--accents-7); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.cache-video-sub { + font-size: 11px; + color: var(--accents-4); +} + @media (max-width: 900px) { .video-actions, .preview-actions, diff --git a/app/static/public/js/video.js b/app/static/public/js/video.js index cd60732..670c0f8 100644 --- a/app/static/public/js/video.js +++ b/app/static/public/js/video.js @@ -2,6 +2,10 @@ const startBtn = document.getElementById('startBtn'); const stopBtn = document.getElementById('stopBtn'); const clearBtn = document.getElementById('clearBtn'); + const pickCachedVideoBtn = document.getElementById('pickCachedVideoBtn'); + const cacheVideoModal = document.getElementById('cacheVideoModal'); + const closeCacheVideoModalBtn = document.getElementById('closeCacheVideoModalBtn'); + const cacheVideoList = document.getElementById('cacheVideoList'); const enterEditBtn = document.getElementById('enterEditBtn'); const editPanel = document.getElementById('editPanel'); const editHint = document.getElementById('editHint'); @@ -904,8 +908,10 @@ function openEditPanel() { if (!editPanel || !editHint || !editBody) return; const item = getSelectedVideoItem(); - const url = item ? String(item.dataset.url || '').trim() : ''; - if (!item || !url) { + const url = item + ? String(item.dataset.url || '').trim() + : String(selectedVideoUrl || '').trim(); + if (!url) { editPanel.classList.remove('hidden'); editHint.classList.remove('hidden'); editBody.classList.add('hidden'); @@ -925,6 +931,89 @@ editPanel.classList.add('hidden'); } + function openCacheVideoModal() { + if (!cacheVideoModal) return; + cacheVideoModal.classList.remove('hidden'); + cacheVideoModal.classList.add('is-open'); + } + + function closeCacheVideoModal() { + if (!cacheVideoModal) return; + cacheVideoModal.classList.remove('is-open'); + cacheVideoModal.classList.add('hidden'); + } + + function formatBytes(bytes) { + const n = Number(bytes || 0); + if (n <= 0) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB']; + const idx = Math.min(units.length - 1, Math.floor(Math.log(n) / Math.log(1024))); + const val = n / Math.pow(1024, idx); + return `${val.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`; + } + + function formatMtime(ms) { + const d = new Date(Number(ms || 0)); + if (!Number.isFinite(d.getTime())) return '-'; + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + const hh = String(d.getHours()).padStart(2, '0'); + const mm = String(d.getMinutes()).padStart(2, '0'); + return `${y}-${m}-${day} ${hh}:${mm}`; + } + + async function loadCachedVideos() { + const authHeader = await ensurePublicKey(); + if (authHeader === null) { + toast('请先配置 Public Key', 'error'); + window.location.href = '/login'; + return []; + } + const res = await fetch('/v1/public/video/cache/list?page=1&page_size=100', { + headers: buildAuthHeaders(authHeader), + }); + if (!res.ok) { + throw new Error(`load_cache_failed_${res.status}`); + } + const data = await res.json(); + return Array.isArray(data.items) ? data.items : []; + } + + function renderCachedVideoList(items) { + if (!cacheVideoList) return; + if (!items.length) { + cacheVideoList.innerHTML = '
暂无缓存视频
'; + return; + } + const html = items.map((item, idx) => { + const name = String(item.name || ''); + const url = String(item.view_url || ''); + const size = formatBytes(item.size_bytes); + const mtime = formatMtime(item.mtime_ms); + return `
+
+
${name || `video_${idx + 1}.mp4`}
+
${size} · ${mtime}
+
+ +
`; + }).join(''); + cacheVideoList.innerHTML = html; + } + + function useCachedVideo(url, name) { + const safeUrl = String(url || '').trim(); + if (!safeUrl) return; + selectedVideoItemId = `cache-${Date.now()}`; + selectedVideoUrl = safeUrl; + if (imageUrlInput) imageUrlInput.value = safeUrl; + if (imageFileName && name) imageFileName.textContent = name; + if (enterEditBtn) enterEditBtn.disabled = false; + closeCacheVideoModal(); + openEditPanel(); + } + function updateTimelineByVideoTime() { if (!editVideo || !editTimeline) return; const duration = Number(editVideo.duration || 0); @@ -1490,6 +1579,49 @@ }); } + if (pickCachedVideoBtn) { + pickCachedVideoBtn.addEventListener('click', async () => { + try { + openCacheVideoModal(); + if (cacheVideoList) { + cacheVideoList.innerHTML = '
正在读取缓存视频...
'; + } + const items = await loadCachedVideos(); + renderCachedVideoList(items); + } catch (e) { + if (cacheVideoList) { + cacheVideoList.innerHTML = '
读取失败,请稍后重试
'; + } + toast('读取缓存视频失败', 'error'); + } + }); + } + + if (closeCacheVideoModalBtn) { + closeCacheVideoModalBtn.addEventListener('click', () => { + closeCacheVideoModal(); + }); + } + + if (cacheVideoModal) { + cacheVideoModal.addEventListener('click', (event) => { + if (event.target === cacheVideoModal) { + closeCacheVideoModal(); + } + }); + } + + if (cacheVideoList) { + cacheVideoList.addEventListener('click', (event) => { + const target = event.target; + if (!(target instanceof HTMLElement)) return; + if (!target.classList.contains('cache-video-use')) return; + const row = target.closest('.cache-video-item'); + if (!row) return; + useCachedVideo(row.getAttribute('data-url') || '', row.getAttribute('data-name') || ''); + }); + } + if (videoStage) { videoStage.addEventListener('click', async (event) => { const target = event.target; diff --git a/app/static/public/pages/video.html b/app/static/public/pages/video.html index 712746b..5dab45f 100644 --- a/app/static/public/pages/video.html +++ b/app/static/public/pages/video.html @@ -177,6 +177,7 @@
视频预览
+
@@ -226,6 +227,21 @@
+ +