feat(video): 重构为视频工作区与右侧历史拼接栏

This commit is contained in:
root committed 2026-02-17 20:06:53 +08:00
1 parent 7aedee4e49
commit ebda949077
3 files changed
+111 -55

No files matched your search

+43
View File
@@ -78,10 +78,53 @@ body {
margin-bottom: 24px;
}
.video-workbench-grid {
display: grid;
grid-template-columns: minmax(0, 1.65fr) minmax(0, 1fr);
gap: 20px;
}
.history-panel .panel-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.history-panel .video-stage {
min-height: 260px;
max-height: 980px;
overflow: auto;
background: #fafafa;
border-color: var(--video-outline);
}
.history-panel .video-item {
background: #fff;
border: 1px solid var(--video-outline);
}
.history-panel .video-item-title,
.history-panel .video-item-bar {
color: var(--accents-7);
}
.history-panel .video-item-actions .geist-button-outline {
color: var(--accents-7);
border-color: var(--video-outline);
}
.history-panel .video-item-link {
color: var(--accents-4);
}
@media (max-width: 1024px) {
.video-top-grid {
grid-template-columns: 1fr;
}
.video-workbench-grid {
grid-template-columns: 1fr;
}
}
.video-card {
+29 -14
View File
@@ -62,6 +62,7 @@
const referencePreviewImg = document.getElementById('referencePreviewImg');
const referencePreviewMeta = document.getElementById('referencePreviewMeta');
const refDropZone = document.getElementById('refDropZone');
const historyCount = document.getElementById('historyCount');
let taskStates = new Map();
let activeTaskIds = [];
@@ -119,6 +120,12 @@
if (editFrameHash) editFrameHash.textContent = shortHash(lastFrameHash);
}
function updateHistoryCount() {
if (!historyCount || !videoStage) return;
const count = videoStage.querySelectorAll('.video-item').length;
historyCount.textContent = String(count);
}
function updateMergeLabels() {
if (mergeVideoA) {
mergeVideoA.textContent = selectedVideoUrl ? shortHash(selectedVideoUrl) : '-';
@@ -381,6 +388,10 @@
previewCount = 0;
selectedVideoItemId = '';
selectedVideoUrl = '';
if (editVideo) {
editVideo.removeAttribute('src');
editVideo.load();
}
mergeTargetVideoUrl = '';
mergeTargetVideoName = '';
mergeCutMsA = 0;
@@ -388,6 +399,7 @@
if (enterEditBtn) enterEditBtn.disabled = true;
closeEditPanel();
updateMergeLabels();
updateHistoryCount();
}
if (durationValue) {
durationValue.textContent = '耗时 -';
@@ -456,6 +468,7 @@
if (videoEmpty) {
videoEmpty.classList.add('hidden');
}
updateHistoryCount();
return item;
}
@@ -1008,6 +1021,9 @@
function bindEditVideoSource(url) {
const safeUrl = String(url || '').trim();
selectedVideoUrl = safeUrl;
if (editHint) {
editHint.classList.toggle('hidden', Boolean(safeUrl));
}
if (!editVideo) return;
editVideo.src = safeUrl;
editVideo.load();
@@ -1040,29 +1056,23 @@
}
function openEditPanel() {
if (!editPanel || !editHint || !editBody) return;
const item = getSelectedVideoItem();
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');
if (editHint) editHint.classList.remove('hidden');
toast('请先选中一个已生成视频', 'warning');
return;
}
editPanel.classList.remove('hidden');
editHint.classList.add('hidden');
editBody.classList.remove('hidden');
if (editHint) editHint.classList.add('hidden');
if (editBody) editBody.classList.remove('hidden');
bindEditVideoSource(url);
}
function closeEditPanel() {
if (!editPanel || !editHint || !editBody) return;
editHint.classList.remove('hidden');
editBody.classList.add('hidden');
editPanel.classList.add('hidden');
if (editHint) editHint.classList.remove('hidden');
if (editBody) editBody.classList.remove('hidden');
}
function openCacheVideoModal() {
@@ -1220,6 +1230,13 @@
} else if (info.url) {
renderVideoFromUrl(taskState, info.url);
}
const item = taskState.previewItem || null;
const itemUrl = item ? String(item.dataset.url || '').trim() : '';
if (item && itemUrl) {
selectedVideoItemId = String(item.dataset.index || '');
refreshVideoSelectionUi();
bindEditVideoSource(itemUrl);
}
}
return;
}
@@ -2081,9 +2098,7 @@
return;
}
if (!target.classList.contains('video-download')) {
if (editPanel && !editPanel.classList.contains('hidden')) {
openEditPanel();
}
bindEditVideoSource(selectedVideoUrl);
return;
}
event.preventDefault();
+39 -41
View File
@@ -18,7 +18,7 @@
<link href="https://cdn.jsdelivr.net/npm/geist@1.0.0/dist/fonts/geist-mono/style.css" rel="stylesheet">
<script src="/static/common/js/theme.js?v=0.1.0"></script>
<link href="/static/common/css/common.css?v=0.3.7" rel="stylesheet">
<link href="/static/public/css/video.css?v=0.3.7" rel="stylesheet">
<link href="/static/public/css/video.css?v=0.3.8" rel="stylesheet">
<link href="/static/common/css/toast.css" rel="stylesheet">
</head>
@@ -26,7 +26,7 @@
<div id="toast-container" class="toast-container"></div>
<div id="app-header"></div>
<main class="flex-1 container mx-auto max-w-5xl px-4 sm:px-6 py-5 sm:py-8 fade-in">
<main class="flex-1 container mx-auto max-w-6xl px-4 sm:px-6 py-5 sm:py-8 fade-in">
<div class="space-y-4 sm:space-y-6">
<div class="video-header-row">
<div>
@@ -173,31 +173,24 @@
</div>
</div>
<div class="video-card video-preview-card">
<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>
</div>
<div id="videoEmpty" class="video-empty">等待生成视频</div>
<div id="videoStage" class="video-stage hidden"></div>
</div>
<div id="editPanel" class="video-card edit-panel hidden">
<div class="video-preview-header">
<div class="card-title">拼接编辑</div>
<div class="preview-actions">
<button id="cancelEditBtn" class="geist-button-outline text-xs px-3">退出编辑</button>
</div>
</div>
<div id="editHint" class="video-empty">先在上方视频列表中选择一个视频,再进入编辑模式。</div>
<div id="editBody" class="hidden">
<div class="edit-preview-wrap">
<video id="editVideo" controls preload="metadata"></video>
<div class="video-workbench-grid">
<div id="editPanel" class="video-card edit-panel">
<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="clearBtn" class="geist-button-outline text-xs px-3">清空</button>
</div>
</div>
<div id="editHint" class="video-empty">请先选择或生成一个视频,工作区会始终展示当前视频。</div>
<div id="editBody">
<div class="meta-grid edit-meta-grid">
<div class="meta-item"><div class="meta-label">当前工作视频</div><div id="mergeVideoA" class="meta-value">-</div></div>
<div class="meta-item"><div class="meta-label">历史数量</div><div id="historyCount" class="meta-value">0</div></div>
</div>
<div class="edit-preview-wrap">
<video id="editVideo" controls preload="metadata"></video>
</div>
<div class="edit-slider-wrap">
<input id="editTimeline" type="range" min="0" max="100000" step="1" value="0">
<div class="progress-meta">
@@ -205,13 +198,14 @@
<span id="editDurationText">总时长 -</span>
</div>
</div>
<div class="meta-grid edit-meta-grid">
<div class="meta-item"><div class="meta-label">frame_index</div><div id="editFrameIndex" class="meta-value">-</div></div>
<div class="meta-item"><div class="meta-label">timestamp_ms</div><div id="editTimestampMs" class="meta-value">-</div></div>
<div class="meta-item"><div class="meta-label">frame_hash</div><div id="editFrameHash" class="meta-value">-</div></div>
<div class="meta-item">
<div class="meta-label">拼接时长</div>
<select id="editLengthSelect" class="geist-input">
<div class="meta-grid edit-meta-grid">
<div class="meta-item"><div class="meta-label">frame_index</div><div id="editFrameIndex" class="meta-value">-</div></div>
<div class="meta-item"><div class="meta-label">timestamp_ms</div><div id="editTimestampMs" class="meta-value">-</div></div>
<div class="meta-item"><div class="meta-label">frame_hash</div><div id="editFrameHash" class="meta-value">-</div></div>
<div class="meta-item"><div class="meta-label">视频2(B)</div><div id="mergeVideoB" class="meta-value">-</div></div>
<div class="meta-item">
<div class="meta-label">拼接时长</div>
<select id="editLengthSelect" class="geist-input">
<option value="6" selected>6 秒</option>
<option value="10">10 秒</option>
<option value="15">15 秒</option>
@@ -224,19 +218,15 @@
</div>
<div class="preview-actions">
<button id="spliceBtn" class="geist-button text-xs px-3">拼接视频</button>
</div>
<div class="video-card manual-merge-card">
<div class="video-preview-header">
</div>
<div class="video-card manual-merge-card">
<div class="video-preview-header">
<div class="card-title">手动两段拼接</div>
<div class="preview-actions">
<button id="pickMergeVideoBtn" class="geist-button-outline text-xs px-3" type="button">选择视频2</button>
<button id="directMergeBtn" class="geist-button-outline text-xs px-3" type="button">手动拼接</button>
</div>
</div>
<div class="meta-grid edit-meta-grid">
<div class="meta-item"><div class="meta-label">视频1(A)</div><div id="mergeVideoA" class="meta-value">-</div></div>
<div class="meta-item"><div class="meta-label">视频2(B)</div><div id="mergeVideoB" class="meta-value">-</div></div>
</div>
<div class="manual-merge-grid">
<div class="manual-merge-col">
<div class="meta-label">视频1拼接帧(取左侧)</div>
@@ -267,6 +257,14 @@
</div>
</div>
</div>
<aside class="video-card history-panel">
<div class="panel-head">
<div class="card-title">历史拼接视频</div>
</div>
<div id="videoEmpty" class="video-empty">拼接结果会在这里保存,点击可替换到工作区。</div>
<div id="videoStage" class="video-stage hidden"></div>
</aside>
</div>
<div id="cacheVideoModal" class="modal-overlay hidden">
@@ -315,7 +313,7 @@
<script src="/static/common/js/pwa.js?v=0.1.1"></script>
<script src="/static/vendor/ffmpeg/ffmpeg.js?v=0.1.0"></script>
<script src="/static/vendor/ffmpeg/ffmpeg-util.js?v=0.1.0"></script>
<script src="/static/public/js/video.js?v=0.4.9"></script>
<script src="/static/public/js/video.js?v=0.5.0"></script>
</body>
</html>