fix: 修复剧本拆解结果滚动并收拢配置
This commit is contained in:
@@ -89,6 +89,7 @@ Oxfmt 不负责代码质量,Oxlint 不负责 Vue 的完整类型推导;Vue S
|
|||||||
- 工作流只保留左侧导航,项目标题下不再重复展示相同标签。主体身份、分镜设计、镜头生产和图库采用内容优先布局:常用筛选/剧集选择/刷新在固定的紧凑工具栏,列表占据剩余高度。
|
- 工作流只保留左侧导航,项目标题下不再重复展示相同标签。主体身份、分镜设计、镜头生产和图库采用内容优先布局:常用筛选/剧集选择/刷新在固定的紧凑工具栏,列表占据剩余高度。
|
||||||
- 项目列表采用固定表头表格;剧本、分镜、身份与生产页的目录和详情独立滚动,其他工作区只滚动内容区。
|
- 项目列表采用固定表头表格;剧本、分镜、身份与生产页的目录和详情独立滚动,其他工作区只滚动内容区。
|
||||||
- 目录、详情、执行记录、折叠操作区、图片历史和 JSON 查看区统一使用 Naive UI `NScrollbar`;外层只负责尺寸与裁切,不再设置原生 `overflow: auto`。内边距与横向排列放在 `content-class` 内容层,避免双滚动条。
|
- 目录、详情、执行记录、折叠操作区、图片历史和 JSON 查看区统一使用 Naive UI `NScrollbar`;外层只负责尺寸与裁切,不再设置原生 `overflow: auto`。内边距与横向排列放在 `content-class` 内容层,避免双滚动条。
|
||||||
|
- 拆解页固定精简状态栏与结果标签,设置、预览及恢复操作收进“拆解设置与恢复”面板;人物、场景、道具、分镜和任务明细共用独立结果滚动区,执行记录单独滚动。结果区占满剩余高度,切换标签回到顶部,轮询刷新或开关设置不重置阅读位置;失败与校验提示在面板关闭时仍然可见。
|
||||||
- 剧集正文按编号连续展示全部已写入剧集;点击目录定位正文,滚动正文同步高亮并保持当前目录项可见。目录分行显示集数和完整标题,执行记录可按需展开。切换角色/世界观等标签不会销毁阅读器或重置位置。
|
- 剧集正文按编号连续展示全部已写入剧集;点击目录定位正文,滚动正文同步高亮并保持当前目录项可见。目录分行显示集数和完整标题,执行记录可按需展开。切换角色/世界观等标签不会销毁阅读器或重置位置。
|
||||||
- 主体身份、分镜设计和镜头生产共用 `DirectoryItem`:编号、完整标题和状态分层展示,桌面目录为 240–280px,窄屏改为可横向滚动的条目。目录标题与筛选固定,只有列表滚动。拆解结果的导出按钮与标签同排右对齐;形态图片筛选按内容区宽度自动排成一行或两列网格。
|
- 主体身份、分镜设计和镜头生产共用 `DirectoryItem`:编号、完整标题和状态分层展示,桌面目录为 240–280px,窄屏改为可横向滚动的条目。目录标题与筛选固定,只有列表滚动。拆解结果的导出按钮与标签同排右对齐;形态图片筛选按内容区宽度自动排成一行或两列网格。
|
||||||
- 身份、分镜、生产与图库的批量配置、说明和诊断统一放入 `WorkspaceTools`(Naive UI Drawer),按需覆盖当前工作区,不挤压列表。面板支持 Esc 或关闭按钮;关闭不会取消任务或清除配置。历史回执可从入口查看,新回执主动打开诊断;查询错误始终在外层可见。
|
- 身份、分镜、生产与图库的批量配置、说明和诊断统一放入 `WorkspaceTools`(Naive UI Drawer),按需覆盖当前工作区,不挤压列表。面板支持 Esc 或关闭按钮;关闭不会取消任务或清除配置。历史回执可从入口查看,新回执主动打开诊断;查询错误始终在外层可见。
|
||||||
|
|||||||
@@ -588,6 +588,27 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
|
/* 拆解结果使用剩余高度,不能再用视口比例定高后隐藏长列表。 */
|
||||||
|
.breakdown-results-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.breakdown-results-section > .result-toolbar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.breakdown-results-section > .content-with-history {
|
||||||
|
flex: 1;
|
||||||
|
height: auto;
|
||||||
|
min-height: 0;
|
||||||
|
grid-template-rows: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
.breakdown-results-scroll .n-scrollbar-content {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
/* 连续阅读区保留双栏,执行记录按需展开,不挤占默认阅读宽度。 */
|
/* 连续阅读区保留双栏,执行记录按需展开,不挤占默认阅读宽度。 */
|
||||||
.script-section .n-tabs-tab__label {
|
.script-section .n-tabs-tab__label {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -1023,6 +1044,9 @@ body {
|
|||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
grid-template-rows: minmax(0, 1fr) 130px;
|
grid-template-rows: minmax(0, 1fr) 130px;
|
||||||
}
|
}
|
||||||
|
.breakdown-results-section > .content-with-history {
|
||||||
|
grid-template-rows: minmax(0, 1fr) min(25%, 130px);
|
||||||
|
}
|
||||||
.history-panel {
|
.history-panel {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|||||||
@@ -32,6 +32,17 @@ describe('管理后台组件边界', () => {
|
|||||||
const css = readFileSync('src/admin.css', 'utf8') + readFileSync('src/styles.css', 'utf8')
|
const css = readFileSync('src/admin.css', 'utf8') + readFileSync('src/styles.css', 'utf8')
|
||||||
expect(css).not.toMatch(/overflow(?:-[xy])?\s*:\s*(?:auto|scroll)\b/)
|
expect(css).not.toMatch(/overflow(?:-[xy])?\s*:\s*(?:auto|scroll)\b/)
|
||||||
})
|
})
|
||||||
|
it('拆解分栏跟随剩余高度收缩,移动端历史区不挤出结果区', () => {
|
||||||
|
// DOM 环境不计算几何,保护完整的 flex → grid → NScrollbar 高度链。
|
||||||
|
const css = readFileSync('src/admin.css', 'utf8')
|
||||||
|
expect(css).toMatch(/\.breakdown-results-section\s*\{[^}]*flex:\s*1;[^}]*min-height:\s*0/)
|
||||||
|
expect(css).toMatch(/\.breakdown-results-section > \.result-toolbar\s*\{[^}]*flex-shrink:\s*0/)
|
||||||
|
expect(css).toMatch(
|
||||||
|
/\.breakdown-results-section > \.content-with-history\s*\{[^}]*flex:\s*1;[^}]*height:\s*auto;[^}]*min-height:\s*0;[^}]*grid-template-rows:\s*minmax\(0, 1fr\)/
|
||||||
|
)
|
||||||
|
expect(css).toContain('grid-template-rows: minmax(0, 1fr) min(25%, 130px)')
|
||||||
|
expect(css).toMatch(/\.panel-scroll\.n-scrollbar\s*\{[^}]*height:\s*100%;[^}]*min-height:\s*0/)
|
||||||
|
})
|
||||||
it('标题与内容使用各自滚动容器,不随正文一起滚动', () => {
|
it('标题与内容使用各自滚动容器,不随正文一起滚动', () => {
|
||||||
wrapper = mount(WorkspacePage, {
|
wrapper = mount(WorkspacePage, {
|
||||||
slots: { header: '<h2>固定操作区</h2>', default: '<p>正文</p>' }
|
slots: { header: '<h2>固定操作区</h2>', default: '<p>正文</p>' }
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import BreakdownPage from './BreakdownPage.vue'
|
||||||
|
import { projectContextKey, type useProjectData } from '../projects/context'
|
||||||
|
import type { ProjectDetail } from '../projects/types'
|
||||||
|
import type { Checkpoint } from '../workflows/types'
|
||||||
|
import type { BreakdownModule, EpisodePlan } from './types'
|
||||||
|
|
||||||
|
let wrapper: VueWrapper | undefined
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper?.unmount()
|
||||||
|
wrapper = undefined
|
||||||
|
document.body.innerHTML = ''
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 长主体、长分镜与长任务列表,用真实 Naive 滚动组件验证内容边界。 */
|
||||||
|
function checkpoint(): Checkpoint {
|
||||||
|
const plan: EpisodePlan = {
|
||||||
|
episodeNo: 1,
|
||||||
|
episodeTitle: '长剧集',
|
||||||
|
storyGoal: '',
|
||||||
|
centralConflict: '',
|
||||||
|
emotionalArc: '',
|
||||||
|
pacing: '',
|
||||||
|
endingHook: '',
|
||||||
|
beats: Array.from({ length: 25 }, (_, i) => ({
|
||||||
|
beatNo: i + 1,
|
||||||
|
title: `节拍 ${i + 1}`,
|
||||||
|
purpose: 'action',
|
||||||
|
description: '节拍内容'.repeat(100),
|
||||||
|
visualFocus: '',
|
||||||
|
narrativeGoal: '',
|
||||||
|
emotionalTone: '',
|
||||||
|
estimatedDurationSeconds: 5,
|
||||||
|
subjectRefs: [],
|
||||||
|
isKeyBeat: false
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
checkpointId: 'long-breakdown',
|
||||||
|
workflowName: 'breakdown',
|
||||||
|
createdAt: '2026-08-28T00:00:00Z',
|
||||||
|
state: {
|
||||||
|
workflowExecution: { executionId: 'run', status: 'completed', startedAt: '2026-08-28T00:00:00Z' },
|
||||||
|
breakdownResult: {
|
||||||
|
subjectCandidates: (['character', 'scene', 'prop'] as BreakdownModule[]).flatMap(module =>
|
||||||
|
Array.from({ length: 30 }, (_, i) => ({
|
||||||
|
profileId: `${module}-${i}`,
|
||||||
|
name: `${module} 主体 ${i + 1}`,
|
||||||
|
ref: `@${module}${i}`,
|
||||||
|
description: '很长的主体描述。'.repeat(100),
|
||||||
|
module,
|
||||||
|
appearance_prompt: '外观描述'
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
subjectForms: [],
|
||||||
|
storyboardPlans: [plan],
|
||||||
|
storyboardEpisodeShots: [
|
||||||
|
{
|
||||||
|
episodeNo: 1,
|
||||||
|
episodePlan: plan,
|
||||||
|
beatShots: plan.beats.map(beat => ({
|
||||||
|
beatNo: beat.beatNo,
|
||||||
|
shots: [
|
||||||
|
{
|
||||||
|
shotNo: 1,
|
||||||
|
title: `镜头 ${beat.beatNo}`,
|
||||||
|
description: '镜头内容',
|
||||||
|
visualFocus: '',
|
||||||
|
subjectRefs: [],
|
||||||
|
durationSeconds: 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
tasks: Array.from({ length: 50 }, (_, i) => ({
|
||||||
|
taskId: `task-${i}`,
|
||||||
|
module: 'character',
|
||||||
|
group: {
|
||||||
|
groupId: `group-${i}`,
|
||||||
|
groupNo: i + 1,
|
||||||
|
startEpisodeNo: i + 1,
|
||||||
|
endEpisodeNo: i + 1,
|
||||||
|
episodes: []
|
||||||
|
},
|
||||||
|
status: 'failed',
|
||||||
|
attempt: 1,
|
||||||
|
errorMessage: `任务错误 ${i + 1}`
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提供响应式 checkpoint,模拟轮询更新但不连接后端或启动模型任务。 */
|
||||||
|
function mountPage(records = [checkpoint()], episodes = 1): ReturnType<typeof useProjectData> {
|
||||||
|
const project: ProjectDetail = {
|
||||||
|
id: 'breakdown-scroll-test',
|
||||||
|
title: '滚动回归',
|
||||||
|
topic: '',
|
||||||
|
style: '',
|
||||||
|
status: 'completed',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
characters: [],
|
||||||
|
world: null,
|
||||||
|
reviews: [],
|
||||||
|
tasks: [],
|
||||||
|
episodes: episodes ? [{ episode: 1, title: '第一集', content: '正文' }] : []
|
||||||
|
}
|
||||||
|
const data = ref({ project, checkpoints: records })
|
||||||
|
const provided: ReturnType<typeof useProjectData> = {
|
||||||
|
data,
|
||||||
|
project: computed(() => data.value.project),
|
||||||
|
checkpoints: computed(() => data.value.checkpoints),
|
||||||
|
loading: ref(false),
|
||||||
|
error: ref(''),
|
||||||
|
updatedAt: ref(''),
|
||||||
|
refresh: vi.fn<() => Promise<void>>().mockResolvedValue()
|
||||||
|
}
|
||||||
|
wrapper = mount(BreakdownPage, {
|
||||||
|
attachTo: document.body,
|
||||||
|
global: { provide: { [projectContextKey as symbol]: provided }, stubs: { RouterLink: true } }
|
||||||
|
})
|
||||||
|
return provided
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换真实标签,不直接修改组件内部状态。 */
|
||||||
|
async function selectTab(label: string) {
|
||||||
|
await wrapper!
|
||||||
|
.findAll('.result-toolbar .n-tabs-tab')
|
||||||
|
.find(tab => tab.text().startsWith(label))!
|
||||||
|
.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('拆解页内容滚动', () => {
|
||||||
|
it.each([
|
||||||
|
['人物', 'character 主体 30'],
|
||||||
|
['场景', 'scene 主体 30'],
|
||||||
|
['道具', 'prop 主体 30'],
|
||||||
|
['分镜', '镜头 25'],
|
||||||
|
['任务明细', '任务错误 50']
|
||||||
|
])('%s 的末项始终放在独立结果滚动容器内', async (label, lastItem) => {
|
||||||
|
mountPage()
|
||||||
|
await selectTab(label)
|
||||||
|
expect(wrapper!.find('.workspace-scroll').exists()).toBe(false)
|
||||||
|
const results = wrapper!.get('.breakdown-results-scroll .n-scrollbar-container')
|
||||||
|
expect(results.text()).toContain(lastItem)
|
||||||
|
expect(results.find('.result-toolbar').exists()).toBe(false)
|
||||||
|
expect(results.find('.history-panel').exists()).toBe(false)
|
||||||
|
expect(wrapper!.get('.history-panel .n-scrollbar-container').text()).toContain('执行记录')
|
||||||
|
expect(wrapper!.get('[data-workspace-tools]').attributes('aria-expanded')).toBe('false')
|
||||||
|
expect(results.find('.table-scroll').exists()).toBe(label === '任务明细')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('刷新和开关配置保留阅读位置,切换标签只重置结果区,不改变执行记录位置', async () => {
|
||||||
|
const fetcher = vi.fn<typeof fetch>()
|
||||||
|
vi.stubGlobal('fetch', fetcher)
|
||||||
|
const provided = mountPage()
|
||||||
|
const results = wrapper!.get<HTMLElement>('.breakdown-results-scroll .n-scrollbar-container').element
|
||||||
|
const history = wrapper!.get<HTMLElement>('.history-panel .n-scrollbar-container').element
|
||||||
|
results.scrollTop = 900
|
||||||
|
history.scrollTop = 120
|
||||||
|
await wrapper!.get('[data-workspace-tools]').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper!.get('.workspace-tools-drawer').element.closest('.workspace-split')).toBeNull()
|
||||||
|
await wrapper!.get('#group-size').setValue('2')
|
||||||
|
await wrapper!.get('[aria-label="关闭操作面板"]').trigger('click')
|
||||||
|
provided.data.value!.checkpoints = [checkpoint()]
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper!.get('.breakdown-results-scroll .n-scrollbar-container').element).toBe(results)
|
||||||
|
expect(results.scrollTop).toBe(900)
|
||||||
|
await selectTab('场景')
|
||||||
|
const next = wrapper!.get<HTMLElement>('.breakdown-results-scroll .n-scrollbar-container').element
|
||||||
|
expect(next).not.toBe(results)
|
||||||
|
expect(next.scrollTop).toBe(0)
|
||||||
|
expect(wrapper!.get('.history-panel .n-scrollbar-container').element).toBe(history)
|
||||||
|
expect(history.scrollTop).toBe(120)
|
||||||
|
await wrapper!.get('[data-workspace-tools]').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper!.get<HTMLInputElement>('#group-size').element.value).toBe('2')
|
||||||
|
expect(fetcher).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('关闭设置时错误仍可见,详细校验与恢复在面板内,不挤占结果高度', async () => {
|
||||||
|
const record = checkpoint()
|
||||||
|
record.state.workflowExecution!.status = 'failed'
|
||||||
|
record.state.workflowExecution!.errorMessage = '工作流中断'
|
||||||
|
record.state.breakdownResult!.storyboardShotValidation = {
|
||||||
|
valid: false,
|
||||||
|
issues: [{ episodeNo: 1, message: '缺少形态绑定' }]
|
||||||
|
}
|
||||||
|
mountPage([record])
|
||||||
|
const feedback = wrapper!.get('.workspace-feedback')
|
||||||
|
expect(feedback.text()).toContain('工作流中断')
|
||||||
|
expect(feedback.text()).toContain('分镜校验未通过,共 1 项问题')
|
||||||
|
expect(feedback.find('.breakdown-results-section').exists()).toBe(false)
|
||||||
|
await feedback.get('button').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper!.get('.workspace-tools-drawer').text()).toContain('缺少形态绑定')
|
||||||
|
expect(wrapper!.get('.workspace-tools-drawer').text()).toContain('重试失败抽取')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('没有正式剧集时保留可滚动的空状态,不显示拆解设置与结果', () => {
|
||||||
|
mountPage([], 0)
|
||||||
|
expect(wrapper!.get('.panel-scroll .n-scrollbar-container').text()).toContain('还没有可拆解的剧集')
|
||||||
|
expect(wrapper!.find('.breakdown-results-section').exists()).toBe(false)
|
||||||
|
expect(wrapper!.find('[data-workspace-tools]').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -3,7 +3,7 @@ import WorkspacePage from '../../components/ui/WorkspacePage.vue'
|
|||||||
import { NScrollbar, NAlert, NButton, NCheckbox, NInputNumber, NProgress, NTab, NTable, NTabs, NTag } from 'naive-ui'
|
import { NScrollbar, NAlert, NButton, NCheckbox, NInputNumber, NProgress, NTab, NTable, NTabs, NTag } from 'naive-ui'
|
||||||
import { computed, onScopeDispose, ref, watch } from 'vue'
|
import { computed, onScopeDispose, ref, watch } from 'vue'
|
||||||
import { Download, Layers, LoaderCircle, ArrowRight } from '@lucide/vue'
|
import { Download, Layers, LoaderCircle, ArrowRight } from '@lucide/vue'
|
||||||
import { EmptyState, StatusBadge } from '../../components/ui'
|
import { EmptyState, StatusBadge, WorkspaceTools } from '../../components/ui'
|
||||||
import { useProjectContext } from '../projects/context'
|
import { useProjectContext } from '../projects/context'
|
||||||
import { breakdownApi } from './api'
|
import { breakdownApi } from './api'
|
||||||
import type { BreakdownAction, BreakdownInput, BreakdownModule, BreakdownPreview } from './types'
|
import type { BreakdownAction, BreakdownInput, BreakdownModule, BreakdownPreview } from './types'
|
||||||
@@ -24,6 +24,7 @@ const preview = ref<BreakdownPreview | null>(null)
|
|||||||
const previewBusy = ref(false)
|
const previewBusy = ref(false)
|
||||||
const previewError = ref('')
|
const previewError = ref('')
|
||||||
const tab = ref('character')
|
const tab = ref('character')
|
||||||
|
const toolsOpen = ref(false)
|
||||||
const moduleOptions: { value: BreakdownModule; label: string; description: string }[] = [
|
const moduleOptions: { value: BreakdownModule; label: string; description: string }[] = [
|
||||||
{ value: 'character', label: '人物', description: '人物身份、外观与形态' },
|
{ value: 'character', label: '人物', description: '人物身份、外观与形态' },
|
||||||
{ value: 'scene', label: '场景', description: '故事空间与环境特征' },
|
{ value: 'scene', label: '场景', description: '故事空间与环境特征' },
|
||||||
@@ -132,117 +133,15 @@ function exportResult() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<WorkspacePage
|
<WorkspacePage split compact class="breakdown-workspace-page">
|
||||||
><template #header
|
<template #header>
|
||||||
><h2 class="text-lg font-semibold">剧本拆解</h2>
|
<div class="workspace-toolbar">
|
||||||
<p class="text-xs text-muted">分组抽取、主体整理与分镜结果</p></template
|
<h2 class="font-semibold">剧本拆解</h2>
|
||||||
>
|
<div v-if="snapshot" class="toolbar-summary">
|
||||||
<div v-if="!project?.episodes.length" class="panel">
|
<span v-if="summary"
|
||||||
<EmptyState
|
>抽取 {{ summary.completed }} / {{ summary.total }} · 失败 {{ summary.failed }}</span
|
||||||
title="还没有可拆解的剧集"
|
>
|
||||||
description="先完成剧本创作。拆解会读取数据库中的正式剧集,不需要上传 checkpoint。"
|
<span>主体 {{ subjects.length }} · 分镜剧集 {{ shots.length }} / {{ plans.length }}</span>
|
||||||
><RouterLink class="button button-primary" :to="`/projects/${project?.id}/create-drama`"
|
|
||||||
>前往剧本创作<ArrowRight :size="14" /></RouterLink
|
|
||||||
></EmptyState>
|
|
||||||
</div>
|
|
||||||
<template v-else>
|
|
||||||
<section class="panel p-5 lg:p-6">
|
|
||||||
<div class="mb-5 flex flex-wrap items-start justify-between gap-3">
|
|
||||||
<div>
|
|
||||||
<h2 class="flex items-center gap-2 font-semibold"><Layers :size="17" />拆解设置</h2>
|
|
||||||
<p class="mt-2 text-xs leading-5 text-muted">
|
|
||||||
读取已保存的
|
|
||||||
{{ project.episodes.length }} 集剧本。每个分组分别抽取所选模块,再生成主体与分镜。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<NTag size="small" :bordered="false">来源:正式剧集</NTag>
|
|
||||||
</div>
|
|
||||||
<div class="breakdown-config">
|
|
||||||
<div>
|
|
||||||
<label class="field-label" for="group-size">每组集数</label>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<NInputNumber
|
|
||||||
:disabled="operation.pending"
|
|
||||||
:input-props="{ id: 'group-size' }"
|
|
||||||
class="w-24"
|
|
||||||
:value="typeof groupSize === 'number' ? groupSize : null"
|
|
||||||
@update:value="groupSize = $event ?? 0"
|
|
||||||
:min="1"
|
|
||||||
:step="1"
|
|
||||||
></NInputNumber
|
|
||||||
><span class="text-sm text-muted">集 / 组</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<fieldset class="flex flex-wrap gap-5 border-0 p-0">
|
|
||||||
<legend class="field-label mb-3">抽取模块</legend>
|
|
||||||
<NCheckbox
|
|
||||||
:checked="modules.includes(option.value)"
|
|
||||||
:disabled="operation.pending"
|
|
||||||
:aria-label="option.label"
|
|
||||||
@update:checked="toggleModule(option.value, $event)"
|
|
||||||
v-for="option in moduleOptions"
|
|
||||||
:key="option.value"
|
|
||||||
class="flex cursor-pointer items-start gap-2.5"
|
|
||||||
><span
|
|
||||||
><span class="block text-sm font-medium">{{ option.label }}</span
|
|
||||||
><span class="mt-1 block text-[11px] text-muted">{{ option.description }}</span></span
|
|
||||||
></NCheckbox
|
|
||||||
>
|
|
||||||
</fieldset>
|
|
||||||
<NButton
|
|
||||||
:disabled="!configValid || previewBusy || operation.pending || !!error"
|
|
||||||
@click="loadPreview"
|
|
||||||
class="self-end"
|
|
||||||
><LoaderCircle v-if="previewBusy" :size="14" class="animate-spin" />预览分组
|
|
||||||
</NButton>
|
|
||||||
</div>
|
|
||||||
<p v-if="!configValid" class="mt-4 text-xs text-danger">
|
|
||||||
每组集数必须是正整数,并至少选择一个抽取模块。
|
|
||||||
</p>
|
|
||||||
<NAlert v-if="previewError" role="alert" type="error" :show-icon="false" class="mt-4">{{
|
|
||||||
previewError
|
|
||||||
}}</NAlert>
|
|
||||||
<div v-if="preview" class="group-preview">
|
|
||||||
<div class="mb-4 flex flex-wrap items-center justify-between gap-3">
|
|
||||||
<p class="text-sm">
|
|
||||||
<strong>{{ preview.groupCount }}</strong> 个分组 <span class="mx-2 text-faint">/</span>
|
|
||||||
<strong>{{ preview.estimatedTaskCount }}</strong> 个抽取任务
|
|
||||||
</p>
|
|
||||||
<ConfirmAction
|
|
||||||
:label="records.length ? '重新拆解' : '开始拆解'"
|
|
||||||
:description="
|
|
||||||
records.length
|
|
||||||
? '这会重新运行整个 Breakdown,可能替换已保存的主体与分镜。只想恢复失败阶段时,请使用下方对应恢复操作。'
|
|
||||||
: '将按当前预览配置启动完整的 Breakdown 工作流。抽取结束后仍需完成主体整理、分镜生成与入库。'
|
|
||||||
"
|
|
||||||
:acknowledgement="records.length > 0"
|
|
||||||
primary
|
|
||||||
:disabled="!canStart"
|
|
||||||
@confirm="run('start')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<div v-for="group in preview.groups" :key="group.groupId" class="group-chip">
|
|
||||||
<span class="text-muted">{{ String(group.groupNo).padStart(2, '0') }}</span
|
|
||||||
><span>第 {{ group.startEpisodeNo }}–{{ group.endEpisodeNo }} 集</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p v-if="project.status === 'generating'" class="mt-4 text-xs text-danger">
|
|
||||||
剧本仍在生成,暂不允许启动拆解,避免读取到不完整的剧集。
|
|
||||||
</p>
|
|
||||||
<p v-if="records.length && !taskStopped" class="mt-4 text-xs leading-5 text-muted">
|
|
||||||
后端尚无明确的完成或失败记录。为避免重复执行,已禁用整条重新拆解;如进程已中断,请先核实后台,再选择对应恢复操作。
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
<section v-if="snapshot" class="mt-5">
|
|
||||||
<div class="mb-3 flex flex-wrap items-center justify-between gap-3">
|
|
||||||
<h2 class="text-sm font-medium">
|
|
||||||
最近可用拆解快照
|
|
||||||
<span class="ml-2 text-xs font-normal text-muted">{{
|
|
||||||
nodeLabel(records.at(-1)?.metadata?.nodeName)
|
|
||||||
}}</span>
|
|
||||||
</h2>
|
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
:status="execution?.status || 'unknown'"
|
:status="execution?.status || 'unknown'"
|
||||||
:label="
|
:label="
|
||||||
@@ -250,70 +149,218 @@ function exportResult() {
|
|||||||
? '工作流已完成'
|
? '工作流已完成'
|
||||||
: execution?.status === 'failed'
|
: execution?.status === 'failed'
|
||||||
? '工作流失败'
|
? '工作流失败'
|
||||||
: '阶段快照 · 执行状态待确认'
|
: '执行状态待确认'
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="summary" class="flex flex-wrap items-center gap-x-6 gap-y-2 text-xs text-muted">
|
<div v-if="project?.episodes.length" class="toolbar-actions">
|
||||||
<span
|
<WorkspaceTools v-model:open="toolsOpen" title="拆解设置与恢复" label="拆解设置与恢复">
|
||||||
>抽取完成 <strong class="text-ink">{{ summary.completed }} / {{ summary.total }}</strong></span
|
<section class="panel p-5 lg:p-6">
|
||||||
><span>失败 {{ summary.failed }}</span
|
<div class="mb-5 flex flex-wrap items-start justify-between gap-3">
|
||||||
><span>主体 {{ subjects.length }}</span
|
<div>
|
||||||
><span>分镜剧集 {{ shots.length }} / {{ plans.length }}</span>
|
<h2 class="flex items-center gap-2 font-semibold"><Layers :size="17" />拆解设置</h2>
|
||||||
|
<p class="mt-2 text-xs leading-5 text-muted">
|
||||||
|
读取已保存的
|
||||||
|
{{ project.episodes.length }}
|
||||||
|
集剧本。每个分组分别抽取所选模块,再生成主体与分镜。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<NTag size="small" :bordered="false">来源:正式剧集</NTag>
|
||||||
|
</div>
|
||||||
|
<div class="breakdown-config">
|
||||||
|
<div>
|
||||||
|
<label class="field-label" for="group-size">每组集数</label>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<NInputNumber
|
||||||
|
:disabled="operation.pending"
|
||||||
|
:input-props="{ id: 'group-size' }"
|
||||||
|
class="w-24"
|
||||||
|
:value="typeof groupSize === 'number' ? groupSize : null"
|
||||||
|
@update:value="groupSize = $event ?? 0"
|
||||||
|
:min="1"
|
||||||
|
:step="1"
|
||||||
|
></NInputNumber
|
||||||
|
><span class="text-sm text-muted">集 / 组</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<fieldset class="flex flex-wrap gap-5 border-0 p-0">
|
||||||
|
<legend class="field-label mb-3">抽取模块</legend>
|
||||||
|
<NCheckbox
|
||||||
|
:checked="modules.includes(option.value)"
|
||||||
|
:disabled="operation.pending"
|
||||||
|
:aria-label="option.label"
|
||||||
|
@update:checked="toggleModule(option.value, $event)"
|
||||||
|
v-for="option in moduleOptions"
|
||||||
|
:key="option.value"
|
||||||
|
class="flex cursor-pointer items-start gap-2.5"
|
||||||
|
><span
|
||||||
|
><span class="block text-sm font-medium">{{ option.label }}</span
|
||||||
|
><span class="mt-1 block text-[11px] text-muted">{{
|
||||||
|
option.description
|
||||||
|
}}</span></span
|
||||||
|
></NCheckbox
|
||||||
|
>
|
||||||
|
</fieldset>
|
||||||
|
<NButton
|
||||||
|
:disabled="!configValid || previewBusy || operation.pending || !!error"
|
||||||
|
@click="loadPreview"
|
||||||
|
class="self-end"
|
||||||
|
><LoaderCircle v-if="previewBusy" :size="14" class="animate-spin" />预览分组
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
<p v-if="!configValid" class="mt-4 text-xs text-danger">
|
||||||
|
每组集数必须是正整数,并至少选择一个抽取模块。
|
||||||
|
</p>
|
||||||
|
<NAlert v-if="previewError" role="alert" type="error" :show-icon="false" class="mt-4">{{
|
||||||
|
previewError
|
||||||
|
}}</NAlert>
|
||||||
|
<div v-if="preview" class="group-preview">
|
||||||
|
<div class="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<p class="text-sm">
|
||||||
|
<strong>{{ preview.groupCount }}</strong> 个分组
|
||||||
|
<span class="mx-2 text-faint">/</span>
|
||||||
|
<strong>{{ preview.estimatedTaskCount }}</strong> 个抽取任务
|
||||||
|
</p>
|
||||||
|
<ConfirmAction
|
||||||
|
:label="records.length ? '重新拆解' : '开始拆解'"
|
||||||
|
:description="
|
||||||
|
records.length
|
||||||
|
? '这会重新运行整个 Breakdown,可能替换已保存的主体与分镜。只想恢复失败阶段时,请使用下方对应恢复操作。'
|
||||||
|
: '将按当前预览配置启动完整的 Breakdown 工作流。抽取结束后仍需完成主体整理、分镜生成与入库。'
|
||||||
|
"
|
||||||
|
:acknowledgement="records.length > 0"
|
||||||
|
primary
|
||||||
|
:disabled="!canStart"
|
||||||
|
@confirm="run('start')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<div v-for="group in preview.groups" :key="group.groupId" class="group-chip">
|
||||||
|
<span class="text-muted">{{ String(group.groupNo).padStart(2, '0') }}</span
|
||||||
|
><span>第 {{ group.startEpisodeNo }}–{{ group.endEpisodeNo }} 集</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-if="project.status === 'generating'" class="mt-4 text-xs text-danger">
|
||||||
|
剧本仍在生成,暂不允许启动拆解,避免读取到不完整的剧集。
|
||||||
|
</p>
|
||||||
|
<p v-if="records.length && !taskStopped" class="mt-4 text-xs leading-5 text-muted">
|
||||||
|
后端尚无明确的完成或失败记录。为避免重复执行,已禁用整条重新拆解;如进程已中断,请先核实后台,再选择对应恢复操作。
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section v-if="snapshot" class="mt-5">
|
||||||
|
<div class="mb-3 flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<h2 class="text-sm font-medium">
|
||||||
|
最近可用拆解快照
|
||||||
|
<span class="ml-2 text-xs font-normal text-muted">{{
|
||||||
|
nodeLabel(records.at(-1)?.metadata?.nodeName)
|
||||||
|
}}</span>
|
||||||
|
</h2>
|
||||||
|
<StatusBadge
|
||||||
|
:status="execution?.status || 'unknown'"
|
||||||
|
:label="
|
||||||
|
execution?.status === 'completed'
|
||||||
|
? '工作流已完成'
|
||||||
|
: execution?.status === 'failed'
|
||||||
|
? '工作流失败'
|
||||||
|
: '阶段快照 · 执行状态待确认'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="summary" class="flex flex-wrap items-center gap-x-6 gap-y-2 text-xs text-muted">
|
||||||
|
<span
|
||||||
|
>抽取完成
|
||||||
|
<strong class="text-ink"
|
||||||
|
>{{ summary.completed }} / {{ summary.total }}</strong
|
||||||
|
></span
|
||||||
|
><span>失败 {{ summary.failed }}</span
|
||||||
|
><span>主体 {{ subjects.length }}</span
|
||||||
|
><span>分镜剧集 {{ shots.length }} / {{ plans.length }}</span>
|
||||||
|
</div>
|
||||||
|
<NProgress
|
||||||
|
v-if="summary?.total"
|
||||||
|
aria-label="已完成抽取任务"
|
||||||
|
type="line"
|
||||||
|
:show-indicator="false"
|
||||||
|
:height="4"
|
||||||
|
:percentage="
|
||||||
|
Math.min(100, Math.max(0, (summary.completed / Math.max(1, summary.total)) * 100))
|
||||||
|
"
|
||||||
|
class="mt-3"
|
||||||
|
></NProgress>
|
||||||
|
<NAlert
|
||||||
|
v-if="execution?.errorMessage"
|
||||||
|
role="alert"
|
||||||
|
type="error"
|
||||||
|
:show-icon="false"
|
||||||
|
class="mt-4"
|
||||||
|
>{{ execution.errorMessage }}</NAlert
|
||||||
|
>
|
||||||
|
<NAlert v-if="validation && !validation.valid" type="error" :show-icon="false" class="mt-4"
|
||||||
|
><p class="font-medium">分镜校验未通过</p>
|
||||||
|
<ul class="mt-2 list-inside list-disc space-y-1">
|
||||||
|
<li v-for="(issue, index) in validation.issues" :key="index">
|
||||||
|
第 {{ issue.episodeNo }} 集<span v-if="issue.beatNo">
|
||||||
|
/ Beat {{ issue.beatNo }}</span
|
||||||
|
><span v-if="issue.shotNo"> / Shot {{ issue.shotNo }}</span
|
||||||
|
>:{{ issue.message }}
|
||||||
|
</li>
|
||||||
|
</ul></NAlert
|
||||||
|
>
|
||||||
|
<div v-if="showRecovery" class="recovery-strip mt-4">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium">从中断处继续</p>
|
||||||
|
<p class="mt-1 text-xs leading-5 text-muted">
|
||||||
|
抽取失败、镜头缺失、主体绑定异常分别处理。禁用表示近期 checkpoint
|
||||||
|
不具备所需数据。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<ConfirmAction
|
||||||
|
label="重试失败抽取"
|
||||||
|
description="只重试 character / scene / prop 抽取任务中的失败项。不能用于镜头阶段失败。"
|
||||||
|
acknowledgement
|
||||||
|
:disabled="!recovery.retry || operation.pending || !!error"
|
||||||
|
@confirm="run('retry')"
|
||||||
|
/><ConfirmAction
|
||||||
|
label="补齐缺失镜头"
|
||||||
|
description="复用已完成剧集的镜头,仅生成尚未完成的 Episode Shot,再进行校验与入库。"
|
||||||
|
acknowledgement
|
||||||
|
:disabled="!recovery.shots || operation.pending || !!error"
|
||||||
|
@confirm="run('resume-shots')"
|
||||||
|
/><ConfirmAction
|
||||||
|
label="修复分镜绑定"
|
||||||
|
description="复用已有镜头,修复 SubjectRef 与视觉 Form 绑定,再校验并保存。"
|
||||||
|
acknowledgement
|
||||||
|
:disabled="!recovery.storyboard || operation.pending || !!error"
|
||||||
|
@confirm="run('resume-storyboard')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</WorkspaceTools>
|
||||||
</div>
|
</div>
|
||||||
<NProgress
|
</div>
|
||||||
v-if="summary?.total"
|
</template>
|
||||||
aria-label="已完成抽取任务"
|
<NScrollbar v-if="!project?.episodes.length" class="panel-scroll panel">
|
||||||
type="line"
|
<EmptyState
|
||||||
:show-indicator="false"
|
title="还没有可拆解的剧集"
|
||||||
:height="4"
|
description="先完成剧本创作。拆解会读取数据库中的正式剧集,不需要上传 checkpoint。"
|
||||||
:percentage="Math.min(100, Math.max(0, (summary.completed / Math.max(1, summary.total)) * 100))"
|
><RouterLink class="button button-primary" :to="`/projects/${project?.id}/create-drama`"
|
||||||
class="mt-3"
|
>前往剧本创作<ArrowRight :size="14" /></RouterLink
|
||||||
></NProgress>
|
></EmptyState>
|
||||||
<NAlert v-if="execution?.errorMessage" role="alert" type="error" :show-icon="false" class="mt-4">{{
|
</NScrollbar>
|
||||||
|
<template v-else>
|
||||||
|
<NScrollbar v-if="execution?.errorMessage || (validation && !validation.valid)" class="workspace-feedback">
|
||||||
|
<NAlert v-if="execution?.errorMessage" role="alert" type="error" :show-icon="false">{{
|
||||||
execution.errorMessage
|
execution.errorMessage
|
||||||
}}</NAlert>
|
}}</NAlert>
|
||||||
<NAlert v-if="validation && !validation.valid" type="error" :show-icon="false" class="mt-4"
|
<NAlert v-if="validation && !validation.valid" role="alert" type="error" :show-icon="false">
|
||||||
><p class="font-medium">分镜校验未通过</p>
|
分镜校验未通过,共 {{ validation.issues.length }} 项问题。
|
||||||
<ul class="mt-2 list-inside list-disc space-y-1">
|
<NButton text @click="toolsOpen = true">查看详情与恢复操作</NButton>
|
||||||
<li v-for="(issue, index) in validation.issues" :key="index">
|
</NAlert>
|
||||||
第 {{ issue.episodeNo }} 集<span v-if="issue.beatNo"> / Beat {{ issue.beatNo }}</span
|
</NScrollbar>
|
||||||
><span v-if="issue.shotNo"> / Shot {{ issue.shotNo }}</span
|
<section class="breakdown-results-section">
|
||||||
>:{{ issue.message }}
|
|
||||||
</li>
|
|
||||||
</ul></NAlert
|
|
||||||
>
|
|
||||||
<div v-if="showRecovery" class="recovery-strip mt-4">
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium">从中断处继续</p>
|
|
||||||
<p class="mt-1 text-xs leading-5 text-muted">
|
|
||||||
抽取失败、镜头缺失、主体绑定异常分别处理。禁用表示近期 checkpoint 不具备所需数据。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<ConfirmAction
|
|
||||||
label="重试失败抽取"
|
|
||||||
description="只重试 character / scene / prop 抽取任务中的失败项。不能用于镜头阶段失败。"
|
|
||||||
acknowledgement
|
|
||||||
:disabled="!recovery.retry || operation.pending || !!error"
|
|
||||||
@confirm="run('retry')"
|
|
||||||
/><ConfirmAction
|
|
||||||
label="补齐缺失镜头"
|
|
||||||
description="复用已完成剧集的镜头,仅生成尚未完成的 Episode Shot,再进行校验与入库。"
|
|
||||||
acknowledgement
|
|
||||||
:disabled="!recovery.shots || operation.pending || !!error"
|
|
||||||
@confirm="run('resume-shots')"
|
|
||||||
/><ConfirmAction
|
|
||||||
label="修复分镜绑定"
|
|
||||||
description="复用已有镜头,修复 SubjectRef 与视觉 Form 绑定,再校验并保存。"
|
|
||||||
acknowledgement
|
|
||||||
:disabled="!recovery.storyboard || operation.pending || !!error"
|
|
||||||
@confirm="run('resume-storyboard')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<div class="mt-7">
|
|
||||||
<div class="result-toolbar">
|
<div class="result-toolbar">
|
||||||
<NTabs v-model:value="tab" type="line" size="small" aria-label="拆解结果"
|
<NTabs v-model:value="tab" type="line" size="small" aria-label="拆解结果"
|
||||||
><NTab v-for="option in moduleOptions" :key="option.value" :name="option.value"
|
><NTab v-for="option in moduleOptions" :key="option.value" :name="option.value"
|
||||||
@@ -324,75 +371,83 @@ function exportResult() {
|
|||||||
><Download :size="14" />导出 JSON
|
><Download :size="14" />导出 JSON
|
||||||
</NButton>
|
</NButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="content-with-history panel mt-4">
|
<div class="content-with-history panel">
|
||||||
<main class="min-w-0">
|
<main class="min-w-0">
|
||||||
<template v-for="option in moduleOptions" :key="option.value"
|
<!-- 所有结果标签共用有界的内容滚动;切换标签回到顶部,不继承上一列表的位置。 -->
|
||||||
><div v-if="tab === option.value">
|
<NScrollbar :key="tab" class="panel-scroll breakdown-results-scroll">
|
||||||
<SubjectList
|
<template v-for="option in moduleOptions" :key="option.value"
|
||||||
:project-id="project?.id"
|
><div v-if="tab === option.value">
|
||||||
:subjects="subjects.filter(item => item.module === option.value)"
|
<SubjectList
|
||||||
:forms="forms"
|
:project-id="project?.id"
|
||||||
/></div
|
:subjects="subjects.filter(item => item.module === option.value)"
|
||||||
></template>
|
:forms="forms"
|
||||||
<div v-if="tab === 'storyboard'">
|
/></div
|
||||||
<div class="mb-4 flex justify-end">
|
></template>
|
||||||
<RouterLink :to="`/projects/${project?.id}/storyboard`" class="text-button"
|
<div v-if="tab === 'storyboard'">
|
||||||
>进入分镜设计<ArrowRight :size="14"
|
<div class="mb-4 flex justify-end">
|
||||||
/></RouterLink>
|
<RouterLink :to="`/projects/${project?.id}/storyboard`" class="text-button"
|
||||||
|
>进入分镜设计<ArrowRight :size="14"
|
||||||
|
/></RouterLink>
|
||||||
|
</div>
|
||||||
|
<StoryboardList :plans="plans" :episodes="shots" />
|
||||||
</div>
|
</div>
|
||||||
<StoryboardList :plans="plans" :episodes="shots" />
|
<div v-if="tab === 'tasks'" class="p-5">
|
||||||
</div>
|
<NScrollbar x-scrollable v-if="snapshot?.tasks?.length" class="table-scroll">
|
||||||
<div v-if="tab === 'tasks'" class="p-5">
|
<NTable :single-line="false" class="project-table"
|
||||||
<NScrollbar x-scrollable v-if="snapshot?.tasks?.length" class="table-scroll">
|
><thead>
|
||||||
<NTable :single-line="false" class="project-table"
|
<tr>
|
||||||
><thead>
|
<th>分组</th>
|
||||||
<tr>
|
<th>模块</th>
|
||||||
<th>分组</th>
|
<th>状态</th>
|
||||||
<th>模块</th>
|
<th>尝试</th>
|
||||||
<th>状态</th>
|
|
||||||
<th>尝试</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<template v-for="task in snapshot.tasks" :key="task.taskId"
|
|
||||||
><tr>
|
|
||||||
<td>
|
|
||||||
第 {{ task.group.startEpisodeNo }}–{{ task.group.endEpisodeNo }} 集
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ moduleOptions.find(item => item.value === task.module)?.label }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<StatusBadge
|
|
||||||
:status="task.status"
|
|
||||||
:label="
|
|
||||||
{
|
|
||||||
pending: '等待中',
|
|
||||||
running: '执行中',
|
|
||||||
completed: '完成',
|
|
||||||
failed: '失败'
|
|
||||||
}[task.status]
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>{{ task.attempt }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="task.errorMessage">
|
</thead>
|
||||||
<td colspan="4" class="text-danger">{{ task.errorMessage }}</td>
|
<tbody>
|
||||||
</tr></template
|
<template v-for="task in snapshot.tasks" :key="task.taskId"
|
||||||
>
|
><tr>
|
||||||
</tbody></NTable
|
<td>
|
||||||
>
|
第 {{ task.group.startEpisodeNo }}–{{ task.group.endEpisodeNo }}
|
||||||
</NScrollbar>
|
集
|
||||||
<EmptyState
|
</td>
|
||||||
v-else
|
<td>
|
||||||
title="尚无抽取任务记录"
|
{{
|
||||||
description="启动拆解后,任务会在后端保存 checkpoint 时更新。预览中的任务尚未执行。"
|
moduleOptions.find(item => item.value === task.module)
|
||||||
/>
|
?.label
|
||||||
</div>
|
}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<StatusBadge
|
||||||
|
:status="task.status"
|
||||||
|
:label="
|
||||||
|
{
|
||||||
|
pending: '等待中',
|
||||||
|
running: '执行中',
|
||||||
|
completed: '完成',
|
||||||
|
failed: '失败'
|
||||||
|
}[task.status]
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>{{ task.attempt }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="task.errorMessage">
|
||||||
|
<td colspan="4" class="text-danger">{{ task.errorMessage }}</td>
|
||||||
|
</tr></template
|
||||||
|
>
|
||||||
|
</tbody></NTable
|
||||||
|
>
|
||||||
|
</NScrollbar>
|
||||||
|
<EmptyState
|
||||||
|
v-else
|
||||||
|
title="尚无抽取任务记录"
|
||||||
|
description="启动拆解后,任务会在后端保存 checkpoint 时更新。预览中的任务尚未执行。"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</NScrollbar>
|
||||||
</main>
|
</main>
|
||||||
<HistoryPanel :checkpoints="checkpoints" workflow="breakdown" />
|
<HistoryPanel :checkpoints="checkpoints" workflow="breakdown" />
|
||||||
</div>
|
</div>
|
||||||
</div> </template
|
</section>
|
||||||
></WorkspacePage>
|
</template>
|
||||||
|
</WorkspacePage>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1171,6 +1171,7 @@ describe('工作台页面交互', () => {
|
|||||||
attachTo: document.body,
|
attachTo: document.body,
|
||||||
global: { provide: { [projectContextKey as symbol]: provided } }
|
global: { provide: { [projectContextKey as symbol]: provided } }
|
||||||
})
|
})
|
||||||
|
await expandSections()
|
||||||
button('预览分组').click()
|
button('预览分组').click()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(button('开始拆解').disabled).toBe(false)
|
expect(button('开始拆解').disabled).toBe(false)
|
||||||
|
|||||||
Reference in New Issue
Block a user