Keep current-shot video regeneration visible in short windows

This commit is contained in:
Codex
2026-11-12 13:32:06 +08:00
parent 45cf52351c
commit f5223075c5
6 changed files with 63 additions and 10 deletions
@@ -703,6 +703,7 @@
</div>
<aside class="video-task-inspector">
<div class="video-inspector-scroll">
<aside class="video-task-player">
<div class="video-player-head">
<div class="video-player-head-info">
@@ -815,8 +816,7 @@
</section>
</div>
<!-- 分镜时长 + 生成操作常驻底部:不随检查器内容滚动 -->
<div class="video-inspector-footer">
<div class="video-inspector-settings">
<p class="video-param-hint" style="margin-bottom:8px">仅设置当前镜头,其他镜头和批量默认不变。</p>
<ModelSelect v-model="shotVideoModel" label="当前镜头模型" :options="videoModelOptions" :show-config="true" default-label="跟随顶部视频默认" />
<p class="video-param-hint" style="margin:8px 0" data-testid="shot-actual-routing">{{ videoTaskRoutingLabel(latestVideoTask(selectedSb.id)) }}</p>
@@ -845,15 +845,20 @@
</div>
<div class="video-param-hint">{{ t('episode.inspector.durationHint') }}</div>
</section>
</div>
</div>
<!-- 只有配置小结和生成按钮常驻底部,其他内容滚动,避免低窗口裁掉按钮。 -->
<div class="video-inspector-footer">
<div class="video-inspector-effective">
{{ t('episode.inspector.effective', { model: effectiveShotModelLabel || t('episode.vid.defaultModel'), res: episodeResolutionShort, dur: effectiveVideoDuration }) }}
</div>
<button
class="btn btn-primary video-inspector-action"
title="使用所选模型和显卡,仅生成当前分镜,保留历史视频"
:disabled="videoSubmittingIds.includes(selectedSb.id) || (isPendingVideo(selectedSb.id) && !canReplaceVideo(selectedSb))"
@click="genVid(selectedSb, { forceNew: true, replace: canReplaceVideo(selectedSb) })"
>
{{ videoSubmittingIds.includes(selectedSb.id) ? '正在处理,请稍候…' : canReplaceVideo(selectedSb) ? '取消并换卡重新生成' : isPendingVideo(selectedSb.id) ? '正在提交或保存,请等待…' : '按所选模型 / 显卡生成' }}
{{ videoSubmittingIds.includes(selectedSb.id) ? '正在处理,请稍候…' : canReplaceVideo(selectedSb) ? '取消并换卡重新生成' : isPendingVideo(selectedSb.id) ? '正在提交或保存,请等待…' : hasVid(selectedSb) ? '重新生成当前分镜视频' : '生成当前分镜视频' }}
</button>
</div>
</aside>
@@ -5403,8 +5408,10 @@ button.video-task-metric.on { box-shadow: 0 0 0 2px var(--accent); }
}
.video-inspector-title { color: var(--text-0); font-size: 14px; font-weight: 700; }
.video-inspector-sub { margin-top: 2px; color: var(--text-3); font-size: 11px; }
.video-inspector-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; padding: 16px 18px 18px; }
/* 时长参数 + 生成操作常驻底部:不随检查器滚动 */
.video-inspector-scroll { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
.video-inspector-body { display: flex; flex-direction: column; gap: 16px; padding: 16px 18px 18px; }
.video-inspector-settings { display: flex; flex-direction: column; gap: 7px; padding: 9px 14px 11px; border-top: 1px solid var(--border); }
/* 生成入口始终可见,模型、显卡、时长等参数在上方滚动区域内。 */
.video-inspector-footer {
flex: none;
display: flex;
@@ -6371,7 +6378,8 @@ button.video-task-metric.on { box-shadow: 0 0 0 2px var(--accent); }
overflow: visible;
}
.video-inspector-body {
.video-inspector-scroll {
flex: none;
overflow: visible;
}
@@ -38,7 +38,7 @@ const fixture=JSON.parse(fs.readFileSync(root+'hd-test-panels/shot-ui-fixture.js
await actual.filter({hasText:'GPU2 · ComfyUI 8188'}).waitFor();
const top=page.locator('.studio-topbar .model-select').filter({has:page.locator('.model-select-label',{hasText:'视频默认'})});
const beforeDefault=await top.innerText();
const shotModel=page.locator('.video-inspector-footer .model-select');
const shotModel=page.locator('.video-inspector-settings .model-select');
await shotModel.locator('button').click();
await page.getByText('GPU1 · ComfyUI 8189 · MiniMax-H3',{exact:true}).last().click();
await gpu.selectOption('gpu1');
@@ -9,7 +9,7 @@ const {chromium}=require('playwright-core'),fs=require('node:fs'),assert=require
await page.route('**/api/**',r=>['GET','HEAD'].includes(r.request().method())?r.continue():r.abort());
await page.goto(base+'/drama/6/episode/1',{waitUntil:'networkidle'});
await page.getByText('视频制作',{exact:true}).first().click();
const panel=page.locator('.video-task-inspector > .task-progress');await panel.waitFor();
const panel=page.locator('.video-task-inspector .task-progress');await panel.waitFor();
await panel.scrollIntoViewIfNeeded();
const text=await panel.innerText();assert.match(text,/已提交/);assert.match(text,/采样|解码|编码|处理|保存/);
await page.screenshot({path:root+'/integration-work/progress-installed.png'});
@@ -23,7 +23,7 @@ const {chromium}=require('playwright-core'),assert=require('node:assert/strict')
async function open(){
await page.goto('http://127.0.0.1:18938/drama/6/episode/1',{waitUntil:'networkidle'});
await page.getByText('视频制作',{exact:true}).first().click();
const panel=page.locator('.video-task-inspector > .task-progress');await panel.waitFor();return panel;
const panel=page.locator('.video-task-inspector .task-progress');await panel.waitFor();return panel;
}
let panel=await open();let text=await panel.innerText();
assert.match(text,/8 \/ 20 步/);assert.match(text,/采样预计还需约 10–15 分钟/);
@@ -0,0 +1,45 @@
// Exercise visibility/hit testing at short window heights without submitting jobs.
const {chromium}=require('playwright-core'),assert=require('node:assert/strict'),fs=require('node:fs');
(async()=>{
const root='D:/web/dj',base=process.env.VERIFY_INSTALLED ? JSON.parse(fs.readFileSync(root+'/huobao-data/last-launch.json')).api.replace('/api/v1','') : 'http://127.0.0.1:18938';
const browser=await chromium.launch({channel:'chrome',headless:true});
try {
const page=await browser.newPage(),errors=[];let pending=false;
page.on('pageerror',e=>errors.push(e.message));
await page.addInitScript(()=>localStorage.setItem('huobao:tours',JSON.stringify(['episode'])));
await page.route('**/api/**',async route=>{
if(!['GET','HEAD'].includes(route.request().method()))return route.abort();
if(pending&&route.request().url().includes('/generation-tasks')){
const response=await route.fetch(),body=await response.json(),task=body.data.tasks.find(t=>t.id===237);
assert.ok(task);task.status='processing';task.stage='running';task.result_url=null;
task.params=JSON.stringify({...JSON.parse(task.params||'{}'),progress:{phase:'sampling',step:9,totalSteps:20,observedAt:Date.now(),stepUpdatedAt:Date.now(),samplingMinSeconds:600,samplingMaxSeconds:900,historySamples:3,historyMinSeconds:1200,historyMaxSeconds:1800}});
return route.fulfill({response,json:body});
}
return route.continue();
});
async function open(){
await page.goto(base+'/drama/6/episode/1',{waitUntil:'networkidle'});
await page.getByText('视频制作',{exact:true}).first().click();
await page.locator('.video-inspector-action').waitFor();
}
async function reachable(){
const result=await page.locator('.video-inspector-action').evaluate(el=>{
const r=el.getBoundingClientRect(),p=el.closest('.video-task-inspector').getBoundingClientRect();
return {visible:r.top>=Math.max(0,p.top)&&r.bottom<=Math.min(innerHeight,p.bottom)+1,hit:el.contains(document.elementFromPoint(r.x+r.width/2,r.y+r.height/2))};
});assert.deepEqual(result,{visible:true,hit:true});
}
for(const viewport of [{width:1600,height:1100},{width:1257,height:827},{width:1280,height:720},{width:1280,height:600}]){
await page.setViewportSize(viewport);await open();
assert.match(await page.locator('.video-inspector-action').innerText(),/重新生成当前分镜视频/);
assert.equal(await page.getByRole('button',{name:'重新拆分',exact:true}).count(),1);
await reachable();
await page.getByLabel('本次生成显卡',{exact:true}).scrollIntoViewIfNeeded();await reachable();
await page.locator('.video-inspector-scroll').evaluate(el=>el.scrollTop=0);await reachable();
if(viewport.height===827)await page.screenshot({path:root+'/integration-work/video-action-visible.png'});
}
pending=true;await open();await page.locator('.video-task-inspector .task-progress').waitFor();
assert.match(await page.locator('.video-inspector-action').innerText(),/取消并换卡重新生成/);
await reachable();await page.locator('.video-inspector-scroll').evaluate(el=>el.scrollTop=el.scrollHeight);await reachable();
assert.deepEqual(errors,[]);console.log('PASS: regeneration/replacement action visible and hit-testable at 4 window sizes, scroll top/bottom and active progress; no writes');
}finally{await browser.close()}
})().catch(e=>{console.error(e.stack);process.exit(1)});
+1 -1
View File
@@ -19,6 +19,6 @@ class ReadOnly(http.server.SimpleHTTPRequestHandler):
server=http.server.ThreadingHTTPServer(('127.0.0.1',18938),ReadOnly)
threading.Thread(target=server.serve_forever,daemon=True).start()
script = sys.argv[1] if len(sys.argv)>1 else 'verify_progress_ui.cjs'
if script not in {'verify_progress_ui.cjs','verify_cancel_ui.cjs','verify_english_ui.cjs'}:raise ValueError('Unknown UI test')
if script not in {'verify_progress_ui.cjs','verify_cancel_ui.cjs','verify_english_ui.cjs','verify_video_action_ui.cjs'}:raise ValueError('Unknown UI test')
try:subprocess.run(['node',str(root/'integration-work/browser-test'/script)],cwd=root,check=True)
finally:server.shutdown()