feat(video-edit): 支持选择缓存视频直接进入编辑模式
This commit is contained in:
1 parent
43206c261e
commit
8dda29e702
4 files changed
+198
-2
No files matched your search
@@ -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"]
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in new issue
Block a user