feat(video-edit): 支持选择缓存视频直接进入编辑模式

This commit is contained in:
root committed 2026-02-17 17:57:32 +08:00
1 parent 43206c261e
commit 8dda29e702
4 files changed
+198 -2

No files matched your search

+10
View File
@@ -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"]
+38
View File
@@ -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,
+134 -2
View File
@@ -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 = '<div class="video-empty">暂无缓存视频</div>';
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 `<div class="cache-video-item" data-url="${url}" data-name="${name}">
<div class="cache-video-meta">
<div class="cache-video-name">${name || `video_${idx + 1}.mp4`}</div>
<div class="cache-video-sub">${size} · ${mtime}</div>
</div>
<button class="geist-button-outline text-xs px-3 cache-video-use" type="button">使用</button>
</div>`;
}).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 = '<div class="video-empty">正在读取缓存视频...</div>';
}
const items = await loadCachedVideos();
renderCachedVideoList(items);
} catch (e) {
if (cacheVideoList) {
cacheVideoList.innerHTML = '<div class="video-empty">读取失败,请稍后重试</div>';
}
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;
+16
View File
@@ -177,6 +177,7 @@
<div class="video-preview-header">
<div class="card-title">视频预览</div>
<div class="preview-actions">
<button id="pickCachedVideoBtn" class="geist-button-outline text-xs px-3">选择缓存视频</button>
<button id="enterEditBtn" class="geist-button-outline text-xs px-3">进入编辑模式</button>
<button id="clearBtn" class="geist-button-outline text-xs px-3">清空</button>
</div>
@@ -226,6 +227,21 @@
</div>
</div>
</div>
<div id="cacheVideoModal" class="modal-overlay hidden">
<div class="modal-content modal-lg">
<div class="modal-header">
<h3 class="modal-title">选择缓存视频</h3>
<button id="closeCacheVideoModalBtn" class="modal-close">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div id="cacheVideoList" class="cache-video-list"></div>
</div>
</div>
</div>
</main>