refactor: 精简导航并扩大工作区内容显示面积
This commit is contained in:
@@ -86,16 +86,18 @@ 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 或关闭按钮;关闭不会取消任务或清除配置。历史回执可从入口查看,新回执主动打开诊断;查询错误始终在外层可见。
|
||||||
|
- 生产步骤的操作按钮保留上下间距;`ConfirmAction` 用稳定根元素承接外部间距,避免多根弹窗组件吞掉 `class`。
|
||||||
- 右上角提供浅色、暗黑、跟随系统三种模式,默认跟随系统,选择保存在本机浏览器。存储被禁用时仍可在当前会话切换。
|
- 右上角提供浅色、暗黑、跟随系统三种模式,默认跟随系统,选择保存在本机浏览器。存储被禁用时仍可在当前会话切换。
|
||||||
- 以黑白灰为主,仅错误、警告、成功等语义状态保留辅助色。Naive UI 和业务内容共用主题;加载前应用偏好,减少主题闪烁。
|
- 以黑白灰为主,仅错误、警告、成功等语义状态保留辅助色。Naive UI 和业务内容共用主题;加载前应用偏好,减少主题闪烁。
|
||||||
- 窄屏默认收起侧栏,目录与正文改用紧凑布局;执行记录仍可查看。长弹窗在自身内部滚动。
|
- 窄屏默认收起侧栏,目录与正文改用紧凑布局;执行记录仍可查看。长弹窗在自身内部滚动。
|
||||||
|
|
||||||
布局样式集中在 `src/admin.css`,业务排版保留在 `src/styles.css`。新增页面应使用 `WorkspacePage`,避免通过 `body` 或 `window.scrollTo` 管理滚动。
|
布局样式集中在 `src/admin.css`,业务排版保留在 `src/styles.css`。新增页面应使用 `WorkspacePage`(内容优先页开启 `compact`),低频操作使用 `WorkspaceTools`;避免通过 `body` 或 `window.scrollTo` 管理滚动。
|
||||||
|
|
||||||
本次 UI 重构保留前端 `dev@5551653` 的业务更新,包括过期形态图、主首帧和主视频的提示与更新入口。首帧默认只补当前剧集;存在过期主首帧时优先更新全项目过期项。非覆盖模式下的过期主视频重建完成后,会接替旧主视频;普通新增候选不自动替换主资产。
|
本次 UI 重构保留前端 `dev@5551653` 的业务更新,包括过期形态图、主首帧和主视频的提示与更新入口。首帧默认只补当前剧集;存在过期主首帧时优先更新全项目过期项。非覆盖模式下的过期主视频重建完成后,会接替旧主视频;普通新增候选不自动替换主资产。
|
||||||
|
|
||||||
|
|||||||
+131
-22
@@ -81,7 +81,7 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
height: 56px;
|
height: 44px;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
@@ -104,7 +104,7 @@ body {
|
|||||||
}
|
}
|
||||||
.project-header {
|
.project-header {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 14px 24px 0;
|
padding: 9px 20px;
|
||||||
background: var(--app-surface);
|
background: var(--app-surface);
|
||||||
border-bottom: 1px solid var(--app-border);
|
border-bottom: 1px solid var(--app-border);
|
||||||
}
|
}
|
||||||
@@ -117,12 +117,6 @@ body {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.project-tabs {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
.project-tabs .n-tabs-nav {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
.project-notices.n-scrollbar {
|
.project-notices.n-scrollbar {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
height: auto;
|
height: auto;
|
||||||
@@ -144,6 +138,7 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.workspace-page {
|
.workspace-page {
|
||||||
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -159,6 +154,116 @@ body {
|
|||||||
.workspace-heading {
|
.workspace-heading {
|
||||||
padding: 16px 24px 12px;
|
padding: 16px 24px 12px;
|
||||||
}
|
}
|
||||||
|
/* 常用工具固定在滚动区外;低频操作使用局部抽屉,不叠加占高面板。 */
|
||||||
|
.content-first-workspace .workspace-heading {
|
||||||
|
padding: 8px 20px;
|
||||||
|
}
|
||||||
|
.content-first-workspace .workspace-split {
|
||||||
|
padding: 0 20px 12px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.content-first-workspace .workspace-scroll-content {
|
||||||
|
padding: 0 20px 16px;
|
||||||
|
}
|
||||||
|
.workspace-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 16px;
|
||||||
|
min-height: 34px;
|
||||||
|
}
|
||||||
|
.toolbar-episode {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 0 1 340px;
|
||||||
|
min-width: 180px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.toolbar-episode > span {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--app-muted);
|
||||||
|
}
|
||||||
|
.toolbar-episode .n-select {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.toolbar-metrics,
|
||||||
|
.toolbar-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 16px;
|
||||||
|
color: var(--app-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.toolbar-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.toolbar-type-filter {
|
||||||
|
width: 152px;
|
||||||
|
}
|
||||||
|
.workspace-tools-drawer {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.workspace-tools-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.workspace-tools-body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.workspace-tools-body .storyboard-controls,
|
||||||
|
.workspace-tools-body .production-controls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-block: 20px;
|
||||||
|
}
|
||||||
|
.workspace-tools-body .storyboard-coverage {
|
||||||
|
margin-block: 20px;
|
||||||
|
}
|
||||||
|
.workspace-tools-body .generation-row {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.workspace-tools-body .generation-row > div + div {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
.workspace-tools-body .production-pipeline {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 230px), 1fr));
|
||||||
|
}
|
||||||
|
.workspace-inline-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px 16px;
|
||||||
|
padding: 7px 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 1px solid var(--app-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--app-subtle);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.confirm-action {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.production-pipeline-card > .confirm-action {
|
||||||
|
margin-block: 16px 4px;
|
||||||
|
}
|
||||||
|
.gallery-workspace-page .form-image-filter-region {
|
||||||
|
margin-block: 10px 0;
|
||||||
|
}
|
||||||
.workspace-heading h2 {
|
.workspace-heading h2 {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
@@ -194,18 +299,6 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.workspace-overview.n-scrollbar {
|
|
||||||
flex-shrink: 0;
|
|
||||||
height: auto;
|
|
||||||
max-height: min(36dvh, 420px);
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--app-surface);
|
|
||||||
border: 1px solid var(--app-border);
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
.overview-scroll-content {
|
|
||||||
padding: 10px 14px;
|
|
||||||
}
|
|
||||||
.workspace-feedback.n-scrollbar {
|
.workspace-feedback.n-scrollbar {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
height: auto;
|
height: auto;
|
||||||
@@ -845,13 +938,13 @@ body {
|
|||||||
}
|
}
|
||||||
.admin-sider:not(.n-layout-sider--collapsed) {
|
.admin-sider:not(.n-layout-sider--collapsed) {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 56px;
|
top: 44px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
box-shadow: 6px 0 24px var(--app-shadow);
|
box-shadow: 6px 0 24px var(--app-shadow);
|
||||||
}
|
}
|
||||||
.project-header {
|
.project-header {
|
||||||
padding: 10px 12px 0;
|
padding: 8px 12px;
|
||||||
}
|
}
|
||||||
.project-title {
|
.project-title {
|
||||||
max-width: 37vw;
|
max-width: 37vw;
|
||||||
@@ -866,6 +959,22 @@ body {
|
|||||||
.workspace-heading {
|
.workspace-heading {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
.content-first-workspace .workspace-heading {
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
.content-first-workspace .workspace-split,
|
||||||
|
.content-first-workspace .workspace-scroll-content {
|
||||||
|
padding: 0 12px 12px;
|
||||||
|
}
|
||||||
|
.toolbar-episode {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.toolbar-actions {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.workspace-tools-body {
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
.workspace-scroll-content,
|
.workspace-scroll-content,
|
||||||
.workspace-split {
|
.workspace-split {
|
||||||
padding: 0 12px 12px;
|
padding: 0 12px 12px;
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NScrollbar } from 'naive-ui'
|
import { NScrollbar } from 'naive-ui'
|
||||||
|
import { provide, ref } from 'vue'
|
||||||
|
import { workspaceHostKey } from './workspaceContext'
|
||||||
/** 工作区固定标题和操作区;仅内容容器滚动,不向 document 传播滚动。 */
|
/** 工作区固定标题和操作区;仅内容容器滚动,不向 document 传播滚动。 */
|
||||||
defineProps<{ split?: boolean }>()
|
defineProps<{ split?: boolean; compact?: boolean }>()
|
||||||
|
const host = ref<HTMLElement | null>(null)
|
||||||
|
provide(workspaceHostKey, host)
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<section class="workspace-page">
|
<section ref="host" class="workspace-page" :class="{ 'content-first-workspace': compact }">
|
||||||
<NScrollbar v-if="$slots.header" class="workspace-heading-scroll"
|
<NScrollbar v-if="$slots.header" class="workspace-heading-scroll"
|
||||||
><header class="workspace-heading"><slot name="header" /></header
|
><header class="workspace-heading"><slot name="header" /></header
|
||||||
></NScrollbar>
|
></NScrollbar>
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue'
|
||||||
|
import { NButton, NDrawer, NDrawerContent } from 'naive-ui'
|
||||||
|
import { Settings2, X } from '@lucide/vue'
|
||||||
|
import { workspaceHostKey } from './workspaceContext'
|
||||||
|
|
||||||
|
/** 低频配置、批量动作与回执在抽屉中查看,开关面板不影响页面查询和后台任务。 */
|
||||||
|
withDefaults(defineProps<{ title: string; label?: string; hasReceipt?: boolean }>(), { label: '操作与回执' })
|
||||||
|
const open = defineModel<boolean>('open', { default: false })
|
||||||
|
const host = inject(workspaceHostKey, undefined)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NButton
|
||||||
|
class="workspace-tools-trigger"
|
||||||
|
data-workspace-tools
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
:aria-expanded="open"
|
||||||
|
@click="open = true"
|
||||||
|
>
|
||||||
|
<template #icon><Settings2 :size="15" /></template>
|
||||||
|
{{ label }}<span v-if="hasReceipt" class="text-xs text-muted"> · 有回执</span>
|
||||||
|
</NButton>
|
||||||
|
<NDrawer
|
||||||
|
v-model:show="open"
|
||||||
|
:to="host ?? undefined"
|
||||||
|
width="min(960px, 100%)"
|
||||||
|
placement="right"
|
||||||
|
:block-scroll="false"
|
||||||
|
display-directive="show"
|
||||||
|
class="workspace-tools-drawer"
|
||||||
|
:aria-label="title"
|
||||||
|
>
|
||||||
|
<NDrawerContent :native-scrollbar="false" body-content-class="workspace-tools-body">
|
||||||
|
<template #header
|
||||||
|
><div class="workspace-tools-heading">
|
||||||
|
<span>{{ title }}</span
|
||||||
|
><NButton quaternary circle aria-label="关闭操作面板" @click="open = false"
|
||||||
|
><X :size="18"
|
||||||
|
/></NButton></div
|
||||||
|
></template>
|
||||||
|
<slot />
|
||||||
|
<template #footer><span class="text-xs text-muted">关闭面板不会取消已提交的后台任务。</span></template>
|
||||||
|
</NDrawerContent>
|
||||||
|
</NDrawer>
|
||||||
|
</template>
|
||||||
@@ -4,3 +4,4 @@ export { default as EmptyState } from './EmptyState.vue'
|
|||||||
export { default as StatusBadge } from './StatusBadge.vue'
|
export { default as StatusBadge } from './StatusBadge.vue'
|
||||||
export { default as AssetImage } from './AssetImage.vue'
|
export { default as AssetImage } from './AssetImage.vue'
|
||||||
export { default as DirectoryItem } from './DirectoryItem.vue'
|
export { default as DirectoryItem } from './DirectoryItem.vue'
|
||||||
|
export { default as WorkspaceTools } from './WorkspaceTools.vue'
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||||
import { afterEach, describe, expect, it } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { NConfigProvider, NModal, NScrollbar } from 'naive-ui'
|
import { NConfigProvider, NDrawer, NDrawerContent, NModal, NScrollbar } from 'naive-ui'
|
||||||
import App from '../../App.vue'
|
import App from '../../App.vue'
|
||||||
import WorkspacePage from './WorkspacePage.vue'
|
import WorkspacePage from './WorkspacePage.vue'
|
||||||
import AppDialog from './AppDialog.vue'
|
import AppDialog from './AppDialog.vue'
|
||||||
|
import WorkspaceTools from './WorkspaceTools.vue'
|
||||||
|
import ProjectLayout from '../../features/projects/ProjectLayout.vue'
|
||||||
|
import ConfirmAction from '../../features/workflows/ConfirmAction.vue'
|
||||||
|
import { projectsApi } from '../../features/projects/api'
|
||||||
import { readFileSync } from 'node:fs'
|
import { readFileSync } from 'node:fs'
|
||||||
|
|
||||||
let wrapper: VueWrapper | undefined
|
let wrapper: VueWrapper | undefined
|
||||||
@@ -14,6 +18,7 @@ afterEach(() => {
|
|||||||
wrapper = undefined
|
wrapper = undefined
|
||||||
localStorage.clear()
|
localStorage.clear()
|
||||||
document.body.innerHTML = ''
|
document.body.innerHTML = ''
|
||||||
|
vi.restoreAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('管理后台组件边界', () => {
|
describe('管理后台组件边界', () => {
|
||||||
@@ -93,4 +98,101 @@ describe('管理后台组件边界', () => {
|
|||||||
})
|
})
|
||||||
expect(document.querySelector('.dialog-body-scroll .n-scrollbar-container')).not.toBeNull()
|
expect(document.querySelector('.dialog-body-scroll .n-scrollbar-container')).not.toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('常用工具固定在列表外,批量抽屉挂载于工作区,开关保留列表与配置草稿', async () => {
|
||||||
|
wrapper = mount(WorkspacePage, {
|
||||||
|
attachTo: document.body,
|
||||||
|
props: { split: true, compact: true },
|
||||||
|
slots: {
|
||||||
|
header: () => h(WorkspaceTools, { title: '测试操作' }, () => h('input', { 'aria-label': '配置草稿' })),
|
||||||
|
default: '<article class="test-list">主体列表</article>'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const list = wrapper.get('.test-list').element
|
||||||
|
expect(wrapper.find('.workspace-tools-drawer').exists()).toBe(false)
|
||||||
|
expect(wrapper.get('.workspace-heading').find('[data-workspace-tools]').exists()).toBe(true)
|
||||||
|
await wrapper.get('[data-workspace-tools]').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.getComponent(NDrawer).props('to')).toBe(wrapper.get('.workspace-page').element)
|
||||||
|
expect(wrapper.getComponent(NDrawerContent).props('nativeScrollbar')).toBe(false)
|
||||||
|
expect(wrapper.get('.workspace-tools-drawer').element.closest('.workspace-split')).toBeNull()
|
||||||
|
expect(wrapper.find('.workspace-tools-drawer .n-scrollbar-container').exists()).toBe(true)
|
||||||
|
await wrapper.get('input[aria-label="配置草稿"]').setValue('保留配置')
|
||||||
|
await wrapper.get('[aria-label="关闭操作面板"]').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.getComponent(NDrawer).props('show')).toBe(false)
|
||||||
|
expect(wrapper.get('.test-list').element).toBe(list)
|
||||||
|
await wrapper.get('[data-workspace-tools]').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.get<HTMLInputElement>('input[aria-label="配置草稿"]').element.value).toBe('保留配置')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('移除内容区重复流程导航后,七条左侧链接、项目标题与刷新仍可用', async () => {
|
||||||
|
const detail = vi.spyOn(projectsApi, 'detail').mockResolvedValue({
|
||||||
|
id: 'navigation-test',
|
||||||
|
title: '导航测试项目',
|
||||||
|
topic: '测试主题',
|
||||||
|
style: null,
|
||||||
|
status: 'completed',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
episodes: [],
|
||||||
|
characters: [],
|
||||||
|
world: null,
|
||||||
|
reviews: [],
|
||||||
|
tasks: []
|
||||||
|
})
|
||||||
|
vi.spyOn(projectsApi, 'checkpoints').mockResolvedValue([])
|
||||||
|
const paths = [
|
||||||
|
'create-drama',
|
||||||
|
'breakdown',
|
||||||
|
'visual-style',
|
||||||
|
'subject-identity',
|
||||||
|
'subject-images',
|
||||||
|
'storyboard',
|
||||||
|
'production'
|
||||||
|
]
|
||||||
|
const router = createRouter({
|
||||||
|
history: createMemoryHistory(),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/projects/:projectId',
|
||||||
|
component: ProjectLayout,
|
||||||
|
children: paths.map(path => ({
|
||||||
|
path,
|
||||||
|
component: { render: () => h(WorkspacePage, {}, () => path) }
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
await router.push('/projects/navigation-test/create-drama')
|
||||||
|
wrapper = mount(App, { attachTo: document.body, global: { plugins: [router] } })
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.find('[aria-label="项目工作流"]').exists()).toBe(false)
|
||||||
|
expect(wrapper.get('.project-title').text()).toBe('导航测试项目')
|
||||||
|
for (const path of paths) {
|
||||||
|
await wrapper.get(`.n-menu a[href="/projects/navigation-test/${path}"]`).trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(router.currentRoute.value.path).toBe('/projects/navigation-test/' + path)
|
||||||
|
expect(wrapper.get('.project-view .workspace-page').text()).toBe(path)
|
||||||
|
}
|
||||||
|
expect(detail).toHaveBeenCalledOnce()
|
||||||
|
await wrapper.get('.project-header .n-button').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(detail).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('确认按钮有稳定根元素承接调用方间距,生产按钮显式保留上下留白', () => {
|
||||||
|
wrapper = mount(ConfirmAction, {
|
||||||
|
props: { label: '测试操作', description: '仅展示,不提交请求' },
|
||||||
|
attrs: { class: 'mt-4' }
|
||||||
|
})
|
||||||
|
expect(wrapper.element.tagName).toBe('SPAN')
|
||||||
|
expect(wrapper.classes()).toContain('confirm-action')
|
||||||
|
expect(wrapper.classes()).toContain('mt-4')
|
||||||
|
expect(wrapper.get('button').text()).toBe('测试操作')
|
||||||
|
expect(readFileSync('src/admin.css', 'utf8')).toMatch(
|
||||||
|
/\.production-pipeline-card > \.confirm-action\s*\{[^}]*margin-block:\s*16px 4px/
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import type { InjectionKey, Ref } from 'vue'
|
||||||
|
|
||||||
|
/** 操作抽屉挂载到当前工作区,覆盖内容而不挤压列表或遮住整个应用导航。 */
|
||||||
|
export const workspaceHostKey: InjectionKey<Ref<HTMLElement | null>> = Symbol('workspace-host')
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import WorkspacePage from '../../components/ui/WorkspacePage.vue'
|
import WorkspacePage from '../../components/ui/WorkspacePage.vue'
|
||||||
|
import WorkspaceTools from '../../components/ui/WorkspaceTools.vue'
|
||||||
import {
|
import {
|
||||||
NScrollbar,
|
NScrollbar,
|
||||||
NCollapse,
|
NCollapse,
|
||||||
@@ -104,33 +105,26 @@ function refreshProject() {
|
|||||||
void query.refresh()
|
void query.refresh()
|
||||||
}
|
}
|
||||||
/** 新回执自动展开,避免折叠区隐藏部分失败诊断。 */
|
/** 新回执自动展开,避免折叠区隐藏部分失败诊断。 */
|
||||||
const overview = ref<string[]>([])
|
const toolsOpen = ref(false)
|
||||||
|
const overview = ref<string[]>(['overview'])
|
||||||
watch(
|
watch(
|
||||||
() => session.value.receipt,
|
() => session.value.receipt,
|
||||||
receipt => {
|
receipt => {
|
||||||
if (receipt) overview.value = ['overview']
|
if (receipt) {
|
||||||
},
|
overview.value = ['overview']
|
||||||
{ immediate: true }
|
toolsOpen.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<WorkspacePage split
|
<WorkspacePage split compact
|
||||||
><template #header
|
><template #header
|
||||||
><div class="flex flex-wrap items-end justify-between gap-4">
|
><div class="workspace-toolbar">
|
||||||
<div>
|
<label class="toolbar-episode"
|
||||||
<h2 class="text-lg font-semibold">镜头生产</h2>
|
><span>剧集</span
|
||||||
<p class="mt-2 text-sm text-muted">从可生成的提示词和首帧,到异步视频任务与最终成片。</p>
|
><NSelect
|
||||||
</div>
|
|
||||||
<RouterLink :to="`/projects/${id}/storyboard`" class="text-button">
|
|
||||||
<ArrowLeft :size="14" />回到分镜设计
|
|
||||||
</RouterLink>
|
|
||||||
</div>
|
|
||||||
<section class="mt-3" aria-label="项目生产配置">
|
|
||||||
<div class="production-controls">
|
|
||||||
<label>
|
|
||||||
<span class="field-label">当前剧集</span>
|
|
||||||
<NSelect
|
|
||||||
:value="episodeNo"
|
:value="episodeNo"
|
||||||
aria-label="选择生产剧集"
|
aria-label="选择生产剧集"
|
||||||
@update:value="selectedEpisode = Number($event)"
|
@update:value="selectedEpisode = Number($event)"
|
||||||
@@ -142,8 +136,40 @@ watch(
|
|||||||
}))
|
}))
|
||||||
]"
|
]"
|
||||||
class="select-control"
|
class="select-control"
|
||||||
></NSelect>
|
></NSelect
|
||||||
</label>
|
></label>
|
||||||
|
<span class="toolbar-summary"
|
||||||
|
>本集 {{ shots.length }} 镜<span v-if="videoRunning">
|
||||||
|
· {{ videoRunning }} 个视频任务进行中</span
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<div class="toolbar-actions">
|
||||||
|
<NButton
|
||||||
|
:disabled="query.loading.value"
|
||||||
|
@click="query.refresh"
|
||||||
|
text
|
||||||
|
size="small"
|
||||||
|
class="mb-2 ml-auto"
|
||||||
|
><RefreshCw :size="13" :class="{ 'animate-spin': query.loading.value }" />刷新就绪状态
|
||||||
|
</NButton>
|
||||||
|
<WorkspaceTools
|
||||||
|
v-model:open="toolsOpen"
|
||||||
|
title="项目生产总览与批量操作"
|
||||||
|
:has-receipt="!!session.receipt"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-end justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold">镜头生产</h2>
|
||||||
|
<p class="mt-2 text-sm text-muted">
|
||||||
|
从可生成的提示词和首帧,到异步视频任务与最终成片。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<RouterLink :to="`/projects/${id}/storyboard`" class="text-button">
|
||||||
|
<ArrowLeft :size="14" />回到分镜设计
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
<section class="mt-3" aria-label="项目生产配置">
|
||||||
|
<div class="production-controls">
|
||||||
<label>
|
<label>
|
||||||
<span class="field-label">批量并发</span>
|
<span class="field-label">批量并发</span>
|
||||||
<NInputNumber
|
<NInputNumber
|
||||||
@@ -161,72 +187,87 @@ watch(
|
|||||||
class="flex items-center gap-2 pb-3 text-xs"
|
class="flex items-center gap-2 pb-3 text-xs"
|
||||||
>覆盖模式:为已有结果新增候选
|
>覆盖模式:为已有结果新增候选
|
||||||
</NCheckbox>
|
</NCheckbox>
|
||||||
<NButton
|
|
||||||
:disabled="query.loading.value"
|
|
||||||
@click="query.refresh"
|
|
||||||
text
|
|
||||||
size="small"
|
|
||||||
class="mb-2 ml-auto"
|
|
||||||
><RefreshCw :size="13" :class="{ 'animate-spin': query.loading.value }" />刷新就绪状态
|
|
||||||
</NButton>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-3 text-[11px] leading-6 text-muted">
|
<p class="mt-3 text-[11px] leading-6 text-muted">
|
||||||
提示词与视频批量操作面向全项目;首帧默认补当前剧集,存在过期主首帧时优先更新全项目过期项,覆盖模式新增全项目候选。Seedream
|
提示词与视频批量操作面向全项目;首帧默认补当前剧集,存在过期主首帧时优先更新全项目过期项,覆盖模式新增全项目候选。Seedream
|
||||||
与 Seedance 均可能产生费用。
|
与 Seedance 均可能产生费用。
|
||||||
</p>
|
</p>
|
||||||
</section></template
|
</section>
|
||||||
>
|
<div class="workspace-overview-panel">
|
||||||
<NScrollbar v-if="query.error.value" class="workspace-feedback"
|
<NCollapse v-model:expanded-names="overview"
|
||||||
><NAlert role="alert" type="error" :show-icon="false"
|
|
||||||
>{{ query.error.value
|
|
||||||
}}<span v-if="query.data.value"> 当前保留上次读取的数据,生产操作已暂停。</span></NAlert
|
|
||||||
></NScrollbar
|
|
||||||
>
|
|
||||||
<NScrollbar class="workspace-overview" content-class="overview-scroll-content"
|
|
||||||
><NCollapse v-model:expanded-names="overview"
|
|
||||||
><NCollapseItem name="overview" title="项目生产总览与批量操作"
|
><NCollapseItem name="overview" title="项目生产总览与批量操作"
|
||||||
><div class="overview-content">
|
><div class="overview-content">
|
||||||
<NAlert type="info" :show-icon="false" class="mt-5 text-xs">
|
<NAlert type="info" :show-icon="false" class="mt-5 text-xs">
|
||||||
每一步都按后端就绪检查执行。覆盖只会新增首帧/视频候选,不会替换当前主资产;上游参考资产变化后,下游过期结果会被标记并阻止继续使用,需要重新生成后才能恢复就绪。
|
每一步都按后端就绪检查执行。覆盖只会新增首帧/视频候选,不会替换当前主资产;上游参考资产变化后,下游过期结果会被标记并阻止继续使用,需要重新生成后才能恢复就绪。
|
||||||
</NAlert>
|
</NAlert>
|
||||||
<NAlert v-if="videoRunning" role="status" type="info" :show-icon="false" class="mt-4">
|
<NAlert
|
||||||
|
v-if="videoRunning"
|
||||||
|
role="status"
|
||||||
|
type="info"
|
||||||
|
:show-icon="false"
|
||||||
|
class="mt-4"
|
||||||
|
>
|
||||||
当前有 {{ videoRunning }} 个视频任务等待或生成中。后端正在轮询
|
当前有 {{ videoRunning }} 个视频任务等待或生成中。后端正在轮询
|
||||||
Provider;同一镜头不会重复提交活动任务。
|
Provider;同一镜头不会重复提交活动任务。
|
||||||
</NAlert>
|
</NAlert>
|
||||||
<NAlert v-if="identityBlocked" type="info" :show-icon="false" class="mt-4">
|
<NAlert v-if="identityBlocked" type="info" :show-icon="false" class="mt-4">
|
||||||
当前有 {{ identityBlocked }} 项角色身份前置问题。Character 必须生成
|
当前有 {{ identityBlocked }} 项角色身份前置问题。Character 必须生成
|
||||||
Identity、确认演员母版并锁定后,首帧才能就绪。
|
Identity、确认演员母版并锁定后,首帧才能就绪。
|
||||||
<RouterLink :to="`/projects/${id}/subject-identity`" class="text-button ml-2"
|
<RouterLink
|
||||||
|
:to="`/projects/${id}/subject-identity`"
|
||||||
|
class="text-button ml-2"
|
||||||
>前往角色选角 →</RouterLink
|
>前往角色选角 →</RouterLink
|
||||||
></NAlert
|
></NAlert
|
||||||
>
|
>
|
||||||
<NAlert v-if="staleKeyframes" role="status" type="info" :show-icon="false" class="mt-4">
|
<NAlert
|
||||||
|
v-if="staleKeyframes"
|
||||||
|
role="status"
|
||||||
|
type="info"
|
||||||
|
:show-icon="false"
|
||||||
|
class="mt-4"
|
||||||
|
>
|
||||||
当前有
|
当前有
|
||||||
{{ staleKeyframes }}
|
{{ staleKeyframes }}
|
||||||
个主首帧已过期。它们使用的主体参考资产已经变化,视频阶段会被阻止;可在下方逐镜重新生成,或使用“补齐
|
个主首帧已过期。它们使用的主体参考资产已经变化,视频阶段会被阻止;可在下方逐镜重新生成,或使用“补齐
|
||||||
/ 更新主首帧”批量处理。
|
/ 更新主首帧”批量处理。
|
||||||
</NAlert>
|
</NAlert>
|
||||||
<NAlert v-if="staleVideos" role="status" type="info" :show-icon="false" class="mt-4">
|
<NAlert
|
||||||
|
v-if="staleVideos"
|
||||||
|
role="status"
|
||||||
|
type="info"
|
||||||
|
:show-icon="false"
|
||||||
|
class="mt-4"
|
||||||
|
>
|
||||||
当前有
|
当前有
|
||||||
{{ staleVideos }}
|
{{ staleVideos }}
|
||||||
个主视频已过期。它们使用的主首帧或主体参考图已经变化;非覆盖模式批量生成时会只重建这些过期主视频。
|
个主视频已过期。它们使用的主首帧或主体参考图已经变化;非覆盖模式批量生成时会只重建这些过期主视频。
|
||||||
</NAlert>
|
</NAlert>
|
||||||
<ProductionReceipt v-if="session.receipt" :receipt="session.receipt" />
|
<ProductionReceipt v-if="session.receipt" :receipt="session.receipt" />
|
||||||
<div v-if="query.data.value" class="production-pipeline mt-5">
|
<div v-if="query.data.value" class="production-pipeline mt-5">
|
||||||
<article v-for="stage in stages" :key="stage.key" class="panel production-pipeline-card">
|
<article
|
||||||
|
v-for="stage in stages"
|
||||||
|
:key="stage.key"
|
||||||
|
class="panel production-pipeline-card"
|
||||||
|
>
|
||||||
<div class="flex items-start justify-between gap-3">
|
<div class="flex items-start justify-between gap-3">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<span class="production-step"><component :is="stage.icon" :size="17" /></span>
|
<span class="production-step"
|
||||||
|
><component :is="stage.icon" :size="17"
|
||||||
|
/></span>
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">STEP {{ stage.code }}</p>
|
<p class="eyebrow">STEP {{ stage.code }}</p>
|
||||||
<h3 class="mt-2 text-sm font-semibold">{{ stage.title }}</h3>
|
<h3 class="mt-2 text-sm font-semibold">
|
||||||
|
{{ stage.title }}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="font-mono text-xs text-muted"
|
<span class="font-mono text-xs text-muted"
|
||||||
>{{ stage.done }} / {{ stage.data?.total ?? 0 }}</span
|
>{{ stage.done }} / {{ stage.data?.total ?? 0 }}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-4 min-h-10 text-xs leading-5 text-muted">{{ stage.description }}</p>
|
<p class="mt-4 min-h-10 text-xs leading-5 text-muted">
|
||||||
|
{{ stage.description }}
|
||||||
|
</p>
|
||||||
<NProgress
|
<NProgress
|
||||||
:aria-label="`${stage.title}完成数量`"
|
:aria-label="`${stage.title}完成数量`"
|
||||||
type="line"
|
type="line"
|
||||||
@@ -237,7 +278,9 @@ watch(
|
|||||||
100,
|
100,
|
||||||
Math.max(
|
Math.max(
|
||||||
0,
|
0,
|
||||||
(stage.done / Math.max(1, Math.max(stage.data?.total ?? 0, 1))) * 100
|
(stage.done /
|
||||||
|
Math.max(1, Math.max(stage.data?.total ?? 0, 1))) *
|
||||||
|
100
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@@ -263,15 +306,20 @@ watch(
|
|||||||
>
|
>
|
||||||
已过期 {{ staleKeyframes }} · 会按当前主体参考资产重新生成
|
已过期 {{ staleKeyframes }} · 会按当前主体参考资产重新生成
|
||||||
</p>
|
</p>
|
||||||
<p v-if="stage.key === 'videos' && staleVideos" class="mt-3 text-[11px] leading-5">
|
<p
|
||||||
|
v-if="stage.key === 'videos' && staleVideos"
|
||||||
|
class="mt-3 text-[11px] leading-5"
|
||||||
|
>
|
||||||
主视频已过期 {{ staleVideos }} · 会按当前主首帧与参考资产重新生成
|
主视频已过期 {{ staleVideos }} · 会按当前主首帧与参考资产重新生成
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
v-if="stage.key === 'keyframes' && identityBlocked"
|
v-if="stage.key === 'keyframes' && identityBlocked"
|
||||||
class="mt-3 text-[11px] leading-5 text-danger"
|
class="mt-3 text-[11px] leading-5 text-danger"
|
||||||
>
|
>
|
||||||
身份缺失 {{ query.data.value?.keyframes.missingIdentity ?? 0 }} · 母版缺失
|
身份缺失 {{ query.data.value?.keyframes.missingIdentity ?? 0 }} ·
|
||||||
{{ query.data.value?.keyframes.missingIdentityAnchor ?? 0 }} · 未锁定
|
母版缺失
|
||||||
|
{{ query.data.value?.keyframes.missingIdentityAnchor ?? 0 }} ·
|
||||||
|
未锁定
|
||||||
{{ query.data.value?.keyframes.identityUnlocked ?? 0 }}
|
{{ query.data.value?.keyframes.identityUnlocked ?? 0 }}
|
||||||
</p>
|
</p>
|
||||||
<ConfirmAction
|
<ConfirmAction
|
||||||
@@ -303,7 +351,9 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
<ConfirmAction
|
<ConfirmAction
|
||||||
label="重试失败视频"
|
label="重试失败视频"
|
||||||
:disabled="blocked || !batchValid || !query.data.value.videoStatus.failed"
|
:disabled="
|
||||||
|
blocked || !batchValid || !query.data.value.videoStatus.failed
|
||||||
|
"
|
||||||
acknowledgement
|
acknowledgement
|
||||||
description="为当前最近一次状态为失败的镜头重新创建 Seedance 任务。成功镜头和进行中的镜头不会处理。"
|
description="为当前最近一次状态为失败的镜头重新创建 Seedance 任务。成功镜头和进行中的镜头不会处理。"
|
||||||
@confirm="run('retry-videos')"
|
@confirm="run('retry-videos')"
|
||||||
@@ -334,14 +384,21 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</div></NCollapseItem
|
</div></NCollapseItem
|
||||||
></NCollapse
|
></NCollapse
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</WorkspaceTools>
|
||||||
|
</div>
|
||||||
|
</div></template
|
||||||
|
>
|
||||||
|
<NScrollbar v-if="query.error.value" class="workspace-feedback"
|
||||||
|
><NAlert role="alert" type="error" :show-icon="false"
|
||||||
|
>{{ query.error.value
|
||||||
|
}}<span v-if="query.data.value"> 当前保留上次读取的数据,生产操作已暂停。</span></NAlert
|
||||||
></NScrollbar
|
></NScrollbar
|
||||||
>
|
>
|
||||||
<div class="my-6 flex flex-wrap items-center justify-between gap-4">
|
<div v-if="staleKeyframes || staleVideos || identityBlocked" class="workspace-inline-status">
|
||||||
<div>
|
<span>过期首帧 {{ staleKeyframes }} · 过期视频 {{ staleVideos }} · 身份前置问题 {{ identityBlocked }}</span>
|
||||||
<h3 class="text-sm font-semibold">第 {{ episodeNo || '—' }} 集 · 逐镜生产</h3>
|
<NButton text size="small" @click="toolsOpen = true">查看处理方式</NButton>
|
||||||
<p class="mt-2 text-[11px] text-muted">选择镜头查看候选首帧、视频任务、成片和实际生成规格。</p>
|
|
||||||
</div>
|
|
||||||
<span class="text-xs text-muted">{{ shots.length }} 个正式镜头</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="shot" class="panel production-workspace">
|
<div v-if="shot" class="panel production-workspace">
|
||||||
<nav class="production-shot-list directory-panel" aria-label="本集生产镜头">
|
<nav class="production-shot-list directory-panel" aria-label="本集生产镜头">
|
||||||
|
|||||||
@@ -1,32 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, provide } from 'vue'
|
import { computed, provide } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { NScrollbar, NAlert, NButton, NPageHeader, NSpin, NTab, NTabs, NEllipsis } from 'naive-ui'
|
import { NScrollbar, NAlert, NButton, NPageHeader, NSpin, NEllipsis } from 'naive-ui'
|
||||||
import { RefreshCw } from '@lucide/vue'
|
import { RefreshCw } from '@lucide/vue'
|
||||||
import { StatusBadge } from '../../components/ui'
|
import { StatusBadge } from '../../components/ui'
|
||||||
import { projectContextKey, useProjectData } from './context'
|
import { projectContextKey, useProjectData } from './context'
|
||||||
import { getOperation } from '../workflows/operations'
|
import { getOperation } from '../workflows/operations'
|
||||||
|
|
||||||
/** 项目共享查询保持不变,标题与导航固定,子工作区负责内容滚动。 */
|
/** 项目仅保留紧凑标题与刷新;工作流统一由左侧导航切换,子工作区负责内容滚动。 */
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const id = computed(() => String(route.params.projectId))
|
const id = computed(() => String(route.params.projectId))
|
||||||
const context = useProjectData(id)
|
const context = useProjectData(id)
|
||||||
provide(projectContextKey, context)
|
provide(projectContextKey, context)
|
||||||
const operation = computed(() => getOperation(id.value))
|
const operation = computed(() => getOperation(id.value))
|
||||||
const tabs = [
|
|
||||||
['create-drama', '剧本创作'],
|
|
||||||
['breakdown', '剧本拆解'],
|
|
||||||
['visual-style', '视觉风格'],
|
|
||||||
['subject-identity', '主体身份'],
|
|
||||||
['subject-images', '形态图片'],
|
|
||||||
['storyboard', '分镜设计'],
|
|
||||||
['production', '镜头生产']
|
|
||||||
] as const
|
|
||||||
/** 路由控制当前工作流,不额外缓存一份可能失步的标签状态。 */
|
|
||||||
function switchWorkflow(path: string) {
|
|
||||||
void router.push('/projects/' + id.value + '/' + path)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<section class="project-frame">
|
<section class="project-frame">
|
||||||
@@ -48,16 +35,6 @@ function switchWorkflow(path: string) {
|
|||||||
</div></template
|
</div></template
|
||||||
>
|
>
|
||||||
</NPageHeader>
|
</NPageHeader>
|
||||||
<NTabs
|
|
||||||
:value="String(route.path.split('/').at(-1))"
|
|
||||||
type="line"
|
|
||||||
size="small"
|
|
||||||
class="project-tabs"
|
|
||||||
aria-label="项目工作流"
|
|
||||||
@update:value="switchWorkflow"
|
|
||||||
>
|
|
||||||
<NTab v-for="[path, label] in tabs" :key="path" :name="path">{{ label }}</NTab>
|
|
||||||
</NTabs>
|
|
||||||
</header>
|
</header>
|
||||||
<NScrollbar
|
<NScrollbar
|
||||||
v-if="context.error.value || operation.pending || operation.error || operation.notice"
|
v-if="context.error.value || operation.pending || operation.error || operation.notice"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import WorkspacePage from '../../components/ui/WorkspacePage.vue'
|
import WorkspacePage from '../../components/ui/WorkspacePage.vue'
|
||||||
|
import WorkspaceTools from '../../components/ui/WorkspaceTools.vue'
|
||||||
import {
|
import {
|
||||||
NScrollbar,
|
NScrollbar,
|
||||||
NAlert,
|
NAlert,
|
||||||
@@ -81,32 +82,25 @@ function exportEpisode() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
/** 回执到达后打开诊断区;平时留出镜头检查空间。 */
|
/** 回执到达后打开诊断区;平时留出镜头检查空间。 */
|
||||||
const overview = ref<string[]>([])
|
const toolsOpen = ref(false)
|
||||||
|
const overview = ref<string[]>(['generation'])
|
||||||
watch(
|
watch(
|
||||||
() => session.value.receipt,
|
() => session.value.receipt,
|
||||||
receipt => {
|
receipt => {
|
||||||
if (receipt) overview.value = ['generation']
|
if (receipt) {
|
||||||
},
|
overview.value = ['generation']
|
||||||
{ immediate: true }
|
toolsOpen.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<WorkspacePage split class="storyboard-workspace-page"
|
<WorkspacePage split compact class="storyboard-workspace-page"
|
||||||
><template #header
|
><template #header
|
||||||
><div class="flex flex-wrap items-end justify-between gap-4">
|
><div class="workspace-toolbar">
|
||||||
<div>
|
<label class="toolbar-episode"
|
||||||
<h2 class="text-lg font-semibold">分镜设计</h2>
|
><span>剧集</span
|
||||||
<p class="mt-2 text-sm text-muted">从镜头拆解到摄影设计,再补齐每一镜的即时状态。</p>
|
|
||||||
</div>
|
|
||||||
<RouterLink :to="`/projects/${id}/breakdown`" class="text-button"
|
|
||||||
><ArrowLeft :size="14" />回到拆解</RouterLink
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<section class="mt-3" aria-label="分镜生成配置">
|
|
||||||
<div class="storyboard-controls">
|
|
||||||
<label
|
|
||||||
><span class="field-label">当前剧集</span
|
|
||||||
><NSelect
|
><NSelect
|
||||||
:value="episodeNo"
|
:value="episodeNo"
|
||||||
aria-label="选择分镜剧集"
|
aria-label="选择分镜剧集"
|
||||||
@@ -121,6 +115,36 @@ watch(
|
|||||||
class="select-control"
|
class="select-control"
|
||||||
></NSelect
|
></NSelect
|
||||||
></label>
|
></label>
|
||||||
|
<div class="toolbar-metrics">
|
||||||
|
<span v-for="item in coverage" :key="item.title"
|
||||||
|
>{{ item.title }} {{ item.count }} / {{ item.total }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-actions">
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<NButton :disabled="query.loading.value" @click="query.refresh" text size="small"
|
||||||
|
><RefreshCw :size="13" :class="{ 'animate-spin': query.loading.value }" />刷新分镜</NButton
|
||||||
|
><NButton :disabled="!shots.length" @click="exportEpisode" text size="small"
|
||||||
|
><Download :size="13" />导出本集
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<WorkspaceTools
|
||||||
|
v-model:open="toolsOpen"
|
||||||
|
title="分镜生成配置与回执"
|
||||||
|
:has-receipt="!!session.receipt"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-end justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold">分镜设计</h2>
|
||||||
|
<p class="mt-2 text-sm text-muted">从镜头拆解到摄影设计,再补齐每一镜的即时状态。</p>
|
||||||
|
</div>
|
||||||
|
<RouterLink :to="`/projects/${id}/breakdown`" class="text-button"
|
||||||
|
><ArrowLeft :size="14" />回到拆解</RouterLink
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<section class="mt-3" aria-label="分镜生成配置">
|
||||||
|
<div class="storyboard-controls">
|
||||||
<label
|
<label
|
||||||
><span class="field-label">批量并发</span
|
><span class="field-label">批量并发</span
|
||||||
><NInputNumber
|
><NInputNumber
|
||||||
@@ -131,7 +155,9 @@ watch(
|
|||||||
:min="1"
|
:min="1"
|
||||||
:step="1"
|
:step="1"
|
||||||
></NInputNumber
|
></NInputNumber
|
||||||
><span v-if="!batchValid" class="mt-1 block text-xs text-danger">请输入正整数</span></label
|
><span v-if="!batchValid" class="mt-1 block text-xs text-danger"
|
||||||
|
>请输入正整数</span
|
||||||
|
></label
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
><span class="field-label">状态自动修复次数</span
|
><span class="field-label">状态自动修复次数</span
|
||||||
@@ -154,16 +180,37 @@ watch(
|
|||||||
>批量覆盖已有结果</NCheckbox
|
>批量覆盖已有结果</NCheckbox
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</section></template
|
</section>
|
||||||
>
|
<div class="storyboard-coverage grid grid-cols-2 gap-3">
|
||||||
<NScrollbar class="workspace-overview" content-class="overview-scroll-content"
|
<div v-for="item in coverage" :key="item.title" class="panel px-3 py-2">
|
||||||
><NCollapse v-model:expanded-names="overview"
|
<div class="mb-3 flex items-center justify-between text-xs">
|
||||||
|
<span>{{ item.title }}</span
|
||||||
|
><span class="font-mono text-muted">{{ item.count }} / {{ item.total }} 镜头</span>
|
||||||
|
</div>
|
||||||
|
<NProgress
|
||||||
|
:aria-label="item.title + '已保存覆盖率'"
|
||||||
|
type="line"
|
||||||
|
:show-indicator="false"
|
||||||
|
:height="4"
|
||||||
|
:percentage="
|
||||||
|
Math.min(
|
||||||
|
100,
|
||||||
|
Math.max(0, (item.count / Math.max(1, Math.max(item.total, 1))) * 100)
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class=""
|
||||||
|
></NProgress>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="workspace-overview-panel">
|
||||||
|
<NCollapse v-model:expanded-names="overview"
|
||||||
><NCollapseItem name="generation" title="生成配置、批量操作与回执"
|
><NCollapseItem name="generation" title="生成配置、批量操作与回执"
|
||||||
><div class="overview-content">
|
><div class="overview-content">
|
||||||
<div class="generation-row">
|
<div class="generation-row">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-semibold">
|
<h3 class="text-sm font-semibold">
|
||||||
<span class="mr-2 font-mono text-xs text-muted">01</span>导演摄影设计
|
<span class="mr-2 font-mono text-xs text-muted">01</span
|
||||||
|
>导演摄影设计
|
||||||
</h3>
|
</h3>
|
||||||
<p class="mt-2 text-xs leading-6 text-muted">
|
<p class="mt-2 text-xs leading-6 text-muted">
|
||||||
景别、机位、运镜与构图。单集操作会重新生成并覆盖;批量默认跳过完整剧集。
|
景别、机位、运镜与构图。单集操作会重新生成并覆盖;批量默认跳过完整剧集。
|
||||||
@@ -192,7 +239,8 @@ watch(
|
|||||||
<div class="generation-row">
|
<div class="generation-row">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-semibold">
|
<h3 class="text-sm font-semibold">
|
||||||
<span class="mr-2 font-mono text-xs text-muted">02</span>即时视觉状态
|
<span class="mr-2 font-mono text-xs text-muted">02</span
|
||||||
|
>即时视觉状态
|
||||||
</h3>
|
</h3>
|
||||||
<p class="mt-2 text-xs leading-6 text-muted">
|
<p class="mt-2 text-xs leading-6 text-muted">
|
||||||
表情、姿态、位置、环境与连续性。每集需要完整导演设计;批量中缺少前置设计的剧集会单独报错。
|
表情、姿态、位置、环境与连续性。每集需要完整导演设计;批量中缺少前置设计的剧集会单独报错。
|
||||||
@@ -228,7 +276,9 @@ watch(
|
|||||||
<p class="my-3 text-xs leading-6 text-muted">
|
<p class="my-3 text-xs leading-6 text-muted">
|
||||||
后端会先校验 GenerationSpec
|
后端会先校验 GenerationSpec
|
||||||
与主体参考图。这里只生成提示词;首帧、视频任务、就绪诊断与失败重试已整理到
|
与主体参考图。这里只生成提示词;首帧、视频任务、就绪诊断与失败重试已整理到
|
||||||
<RouterLink :to="`/projects/${id}/production`" class="text-button mx-1"
|
<RouterLink
|
||||||
|
:to="`/projects/${id}/production`"
|
||||||
|
class="text-button mx-1"
|
||||||
>镜头生产</RouterLink
|
>镜头生产</RouterLink
|
||||||
>。
|
>。
|
||||||
</p>
|
</p>
|
||||||
@@ -246,9 +296,21 @@ watch(
|
|||||||
<GenerationReceipt
|
<GenerationReceipt
|
||||||
v-if="session.receipt"
|
v-if="session.receipt"
|
||||||
:receipt="session.receipt"
|
:receipt="session.receipt"
|
||||||
/></div></NCollapseItem></NCollapse
|
/></div></NCollapseItem
|
||||||
></NScrollbar>
|
></NCollapse>
|
||||||
<NScrollbar class="workspace-feedback">
|
</div>
|
||||||
|
</WorkspaceTools>
|
||||||
|
</div></div
|
||||||
|
></template>
|
||||||
|
<NScrollbar
|
||||||
|
v-if="
|
||||||
|
query.error.value ||
|
||||||
|
!prerequisites.directionEpisode ||
|
||||||
|
!prerequisites.visualEpisode ||
|
||||||
|
(data && !directionComplete && data.directions.shotCount)
|
||||||
|
"
|
||||||
|
class="workspace-feedback"
|
||||||
|
>
|
||||||
<NAlert v-if="query.error.value" role="alert" type="error" :show-icon="false" class="mt-4"
|
<NAlert v-if="query.error.value" role="alert" type="error" :show-icon="false" class="mt-4"
|
||||||
>{{ query.error.value }} 当前保留上次成功读取的数据,暂停生成操作。<NButton
|
>{{ query.error.value }} 当前保留上次成功读取的数据,暂停生成操作。<NButton
|
||||||
@click="query.refresh"
|
@click="query.refresh"
|
||||||
@@ -275,37 +337,6 @@ watch(
|
|||||||
本集导演设计尚未补齐,请先完成第 01 步,再生成本集即时状态。
|
本集导演设计尚未补齐,请先完成第 01 步,再生成本集即时状态。
|
||||||
</NAlert>
|
</NAlert>
|
||||||
</NScrollbar>
|
</NScrollbar>
|
||||||
<div class="my-6 flex flex-wrap items-center justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<h3 class="text-sm font-semibold">第 {{ episodeNo || '—' }} 集 · 镜头检查</h3>
|
|
||||||
<p class="mt-2 text-[11px] text-muted">
|
|
||||||
当前剧集数据库结果每 6 秒刷新。覆盖率不是批量任务进度;批量诊断在请求返回后显示。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-3">
|
|
||||||
<NButton :disabled="query.loading.value" @click="query.refresh" text size="small"
|
|
||||||
><RefreshCw :size="13" :class="{ 'animate-spin': query.loading.value }" />刷新分镜</NButton
|
|
||||||
><NButton :disabled="!shots.length" @click="exportEpisode" text size="small"
|
|
||||||
><Download :size="13" />导出本集
|
|
||||||
</NButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="storyboard-coverage grid grid-cols-2 gap-3">
|
|
||||||
<div v-for="item in coverage" :key="item.title" class="panel px-3 py-2">
|
|
||||||
<div class="mb-3 flex items-center justify-between text-xs">
|
|
||||||
<span>{{ item.title }}</span
|
|
||||||
><span class="font-mono text-muted">{{ item.count }} / {{ item.total }} 镜头</span>
|
|
||||||
</div>
|
|
||||||
<NProgress
|
|
||||||
:aria-label="item.title + '已保存覆盖率'"
|
|
||||||
type="line"
|
|
||||||
:show-indicator="false"
|
|
||||||
:height="4"
|
|
||||||
:percentage="Math.min(100, Math.max(0, (item.count / Math.max(1, Math.max(item.total, 1))) * 100))"
|
|
||||||
class=""
|
|
||||||
></NProgress>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="!data && query.loading.value" class="panel p-8 text-sm text-muted" role="status">
|
<div v-if="!data && query.loading.value" class="panel p-8 text-sm text-muted" role="status">
|
||||||
正在读取正式分镜数据……
|
正在读取正式分镜数据……
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import WorkspacePage from '../../components/ui/WorkspacePage.vue'
|
import WorkspacePage from '../../components/ui/WorkspacePage.vue'
|
||||||
|
import WorkspaceTools from '../../components/ui/WorkspaceTools.vue'
|
||||||
import {
|
import {
|
||||||
NScrollbar,
|
NScrollbar,
|
||||||
NAlert,
|
NAlert,
|
||||||
@@ -129,39 +130,55 @@ function chooseAnchor(imageId: string) {
|
|||||||
else void setAnchor(imageId)
|
else void setAnchor(imageId)
|
||||||
}
|
}
|
||||||
/** 新回执自动展开,避免折叠区隐藏部分失败诊断。 */
|
/** 新回执自动展开,避免折叠区隐藏部分失败诊断。 */
|
||||||
const overview = ref<string[]>([])
|
const toolsOpen = ref(false)
|
||||||
|
const overview = ref<string[]>(['casting'])
|
||||||
watch(
|
watch(
|
||||||
() => session.value.receipt,
|
() => session.value.receipt,
|
||||||
receipt => {
|
receipt => {
|
||||||
if (receipt) overview.value = ['casting']
|
if (receipt) {
|
||||||
},
|
overview.value = ['casting']
|
||||||
{ immediate: true }
|
toolsOpen.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<WorkspacePage split
|
<WorkspacePage split compact
|
||||||
><template #header
|
><template #header
|
||||||
><div class="flex flex-wrap items-end justify-between gap-4">
|
><div class="workspace-toolbar">
|
||||||
|
<div class="toolbar-type-filter">
|
||||||
|
<NSelect
|
||||||
|
aria-label="身份主体类型"
|
||||||
|
v-model:value="module"
|
||||||
|
:options="[
|
||||||
|
{ label: String('全部类型'), value: 'all' },
|
||||||
|
{ label: String('人物'), value: 'character' },
|
||||||
|
{ label: String('场景'), value: 'scene' },
|
||||||
|
{ label: String('道具'), value: 'prop' }
|
||||||
|
]"
|
||||||
|
></NSelect>
|
||||||
|
</div>
|
||||||
|
<span class="toolbar-summary">共 {{ subjects.length }} 个主体</span>
|
||||||
|
<div class="toolbar-actions">
|
||||||
|
<WorkspaceTools
|
||||||
|
v-model:open="toolsOpen"
|
||||||
|
title="角色选角总览与批量操作"
|
||||||
|
:has-receipt="!!session.receipt"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-end justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-lg font-semibold">主体身份</h2>
|
<h2 class="text-lg font-semibold">主体身份</h2>
|
||||||
<p class="mt-2 text-sm text-muted">为同一个人物、场景或道具固定稳定特征,再生成不同造型。</p>
|
<p class="mt-2 text-sm text-muted">
|
||||||
|
为同一个人物、场景或道具固定稳定特征,再生成不同造型。
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<RouterLink :to="`/projects/${projectId}/subject-images`" class="text-button"
|
<RouterLink :to="`/projects/${projectId}/subject-images`" class="text-button"
|
||||||
>下一步:形态图片 →</RouterLink
|
>下一步:形态图片 →</RouterLink
|
||||||
>
|
>
|
||||||
</div></template
|
</div>
|
||||||
>
|
<div class="workspace-overview-panel">
|
||||||
<NScrollbar
|
<NCollapse v-model:expanded-names="overview"
|
||||||
class="workspace-feedback"
|
|
||||||
v-if="catalog.error.value || styleQuery.error.value || castingQuery.error.value"
|
|
||||||
><NAlert role="alert" type="error" :show-icon="false"
|
|
||||||
>{{ catalog.error.value || styleQuery.error.value || castingQuery.error.value }}
|
|
||||||
当前相关操作已暂停,请刷新核对后端。
|
|
||||||
</NAlert></NScrollbar
|
|
||||||
>
|
|
||||||
<NScrollbar class="workspace-overview" content-class="overview-scroll-content"
|
|
||||||
><NCollapse v-model:expanded-names="overview"
|
|
||||||
><NCollapseItem name="casting" title="角色选角总览与批量操作"
|
><NCollapseItem name="casting" title="角色选角总览与批量操作"
|
||||||
><div class="overview-content">
|
><div class="overview-content">
|
||||||
<NAlert type="info" :show-icon="false" class="mt-5 text-xs">
|
<NAlert type="info" :show-icon="false" class="mt-5 text-xs">
|
||||||
@@ -169,7 +186,12 @@ watch(
|
|||||||
Identity,之后首帧才能就绪。未明确人物背景时,AI
|
Identity,之后首帧才能就绪。未明确人物背景时,AI
|
||||||
默认以中国人物作为选角基线;项目和角色的明确事实始终优先。场景和道具仍使用普通身份母版流程。
|
默认以中国人物作为选角基线;项目和角色的明确事实始终优先。场景和道具仍使用普通身份母版流程。
|
||||||
</NAlert>
|
</NAlert>
|
||||||
<NAlert v-if="styleQuery.data.value && !hasStyle" type="info" :show-icon="false" class="mt-4">
|
<NAlert
|
||||||
|
v-if="styleQuery.data.value && !hasStyle"
|
||||||
|
type="info"
|
||||||
|
:show-icon="false"
|
||||||
|
class="mt-4"
|
||||||
|
>
|
||||||
先创建项目视觉风格,才能 AI 生成身份或身份图片。<RouterLink
|
先创建项目视觉风格,才能 AI 生成身份或身份图片。<RouterLink
|
||||||
:to="`/projects/${projectId}/visual-style`"
|
:to="`/projects/${projectId}/visual-style`"
|
||||||
class="text-button ml-2"
|
class="text-button ml-2"
|
||||||
@@ -229,7 +251,9 @@ watch(
|
|||||||
<span class="field-label">本批上限</span>
|
<span class="field-label">本批上限</span>
|
||||||
<NInputNumber
|
<NInputNumber
|
||||||
:disabled="operation.pending"
|
:disabled="operation.pending"
|
||||||
:value="typeof candidateLimit === 'number' ? candidateLimit : null"
|
:value="
|
||||||
|
typeof candidateLimit === 'number' ? candidateLimit : null
|
||||||
|
"
|
||||||
@update:value="candidateLimit = $event ?? 0"
|
@update:value="candidateLimit = $event ?? 0"
|
||||||
:min="1"
|
:min="1"
|
||||||
:step="1"
|
:step="1"
|
||||||
@@ -243,7 +267,9 @@ watch(
|
|||||||
</NCheckbox>
|
</NCheckbox>
|
||||||
<ConfirmAction
|
<ConfirmAction
|
||||||
:label="force ? '重生成角色身份文本' : '补齐角色身份文本'"
|
:label="force ? '重生成角色身份文本' : '补齐角色身份文本'"
|
||||||
:disabled="blocked || !hasStyle || !batchValid || dirty || !casting.total"
|
:disabled="
|
||||||
|
blocked || !hasStyle || !batchValid || dirty || !casting.total
|
||||||
|
"
|
||||||
acknowledgement
|
acknowledgement
|
||||||
description="只生成 Character Identity 文本,不生成图片。已锁定角色始终跳过;覆盖文本不会自动更新已有候选图。"
|
description="只生成 Character Identity 文本,不生成图片。已锁定角色始终跳过;覆盖文本不会自动更新已有候选图。"
|
||||||
@confirm="generateProject(true)"
|
@confirm="generateProject(true)"
|
||||||
@@ -262,7 +288,10 @@ watch(
|
|||||||
@confirm="generateCastingCandidates"
|
@confirm="generateCastingCandidates"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!batchValid || !candidateLimitValid" class="mt-3 text-xs text-danger">
|
<p
|
||||||
|
v-if="!batchValid || !candidateLimitValid"
|
||||||
|
class="mt-3 text-xs text-danger"
|
||||||
|
>
|
||||||
并发数与本批上限都必须是正整数。
|
并发数与本批上限都必须是正整数。
|
||||||
</p>
|
</p>
|
||||||
<div class="casting-list mt-5">
|
<div class="casting-list mt-5">
|
||||||
@@ -273,11 +302,15 @@ watch(
|
|||||||
class="casting-item"
|
class="casting-item"
|
||||||
:class="{ selected: selectedId === item.subjectId }"
|
:class="{ selected: selectedId === item.subjectId }"
|
||||||
><span class="min-w-0">
|
><span class="min-w-0">
|
||||||
<span class="block truncate text-xs font-medium">{{ item.subjectName }}</span>
|
<span class="block truncate text-xs font-medium">{{
|
||||||
|
item.subjectName
|
||||||
|
}}</span>
|
||||||
<span class="mt-1 block font-mono text-[10px] text-muted">{{
|
<span class="mt-1 block font-mono text-[10px] text-muted">{{
|
||||||
item.subjectRef
|
item.subjectRef
|
||||||
}}</span> </span
|
}}</span> </span
|
||||||
><StatusBadge :status="item.status" :label="castingStatusLabel(item.status)"
|
><StatusBadge
|
||||||
|
:status="item.status"
|
||||||
|
:label="castingStatusLabel(item.status)"
|
||||||
/></NButton>
|
/></NButton>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -291,27 +324,41 @@ watch(
|
|||||||
<ConfirmAction
|
<ConfirmAction
|
||||||
class="mt-4"
|
class="mt-4"
|
||||||
:label="force ? '重生成全部未锁定身份' : '补齐全部主体身份文本'"
|
:label="force ? '重生成全部未锁定身份' : '补齐全部主体身份文本'"
|
||||||
:disabled="blocked || !hasStyle || !batchValid || dirty || !subjects.length"
|
:disabled="
|
||||||
|
blocked || !hasStyle || !batchValid || dirty || !subjects.length
|
||||||
|
"
|
||||||
acknowledgement
|
acknowledgement
|
||||||
description="生成全项目主体身份描述与提示词,不生成图片。覆盖不会自动更新旧身份图、形态图、首帧或视频。"
|
description="生成全项目主体身份描述与提示词,不生成图片。覆盖不会自动更新旧身份图、形态图、首帧或视频。"
|
||||||
@confirm="generateProject(false)" /></NCollapseItem
|
@confirm="generateProject(false)" /></NCollapseItem
|
||||||
></NCollapse>
|
></NCollapse>
|
||||||
<section v-if="session.receipt" class="panel mb-5 p-5" aria-label="身份文本批量回执">
|
<section
|
||||||
|
v-if="session.receipt"
|
||||||
|
class="panel mb-5 p-5"
|
||||||
|
aria-label="身份文本批量回执"
|
||||||
|
>
|
||||||
<div class="flex justify-between gap-3">
|
<div class="flex justify-between gap-3">
|
||||||
<h3 class="text-sm font-medium">{{ session.receipt.title }} · 本次回执</h3>
|
<h3 class="text-sm font-medium">
|
||||||
|
{{ session.receipt.title }} · 本次回执
|
||||||
|
</h3>
|
||||||
<NButton @click="exportReceipt" text size="small">导出诊断</NButton>
|
<NButton @click="exportReceipt" text size="small">导出诊断</NButton>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="session.receipt.kind === 'identities'" class="mt-3 text-xs text-muted">
|
<p
|
||||||
|
v-if="session.receipt.kind === 'identities'"
|
||||||
|
class="mt-3 text-xs text-muted"
|
||||||
|
>
|
||||||
共 {{ session.receipt.result.total }} 个主体 · 目标
|
共 {{ session.receipt.result.total }} 个主体 · 目标
|
||||||
{{ session.receipt.result.targetCount }} · 生成 {{ session.receipt.result.generated }} ·
|
{{ session.receipt.result.targetCount }} · 生成
|
||||||
跳过 {{ session.receipt.result.skipped }}(含锁定
|
{{ session.receipt.result.generated }} · 跳过
|
||||||
{{ session.receipt.result.skippedLocked }})· 失败 {{ session.receipt.result.failed }}
|
{{ session.receipt.result.skipped }}(含锁定
|
||||||
|
{{ session.receipt.result.skippedLocked }})· 失败
|
||||||
|
{{ session.receipt.result.failed }}
|
||||||
</p>
|
</p>
|
||||||
<p v-else class="mt-3 text-xs text-muted">
|
<p v-else class="mt-3 text-xs text-muted">
|
||||||
共 {{ session.receipt.result.totalCharacters }} 个角色 · 缺候选
|
共 {{ session.receipt.result.totalCharacters }} 个角色 · 缺候选
|
||||||
{{ session.receipt.result.missingAnchor }} · 本批目标
|
{{ session.receipt.result.missingAnchor }} · 本批目标
|
||||||
{{ session.receipt.result.targetCount }} · 已生成
|
{{ session.receipt.result.targetCount }} · 已生成
|
||||||
{{ session.receipt.result.generated }} · 失败 {{ session.receipt.result.failed }}
|
{{ session.receipt.result.generated }} · 失败
|
||||||
|
{{ session.receipt.result.failed }}
|
||||||
</p>
|
</p>
|
||||||
<NAlert
|
<NAlert
|
||||||
v-if="session.receipt.result.failed"
|
v-if="session.receipt.result.failed"
|
||||||
@@ -323,8 +370,13 @@ watch(
|
|||||||
本次存在部分失败,成功结果不会回滚。请检查逐角色原因后再重试。
|
本次存在部分失败,成功结果不会回滚。请检查逐角色原因后再重试。
|
||||||
</NAlert>
|
</NAlert>
|
||||||
<ul class="mt-3 space-y-2 text-xs text-danger">
|
<ul class="mt-3 space-y-2 text-xs text-danger">
|
||||||
<li v-for="failure in session.receipt.result.failures" :key="failure.subjectId">
|
<li
|
||||||
{{ failure.subjectRef }} · {{ failure.subjectId }}:{{ failure.error }}
|
v-for="failure in session.receipt.result.failures"
|
||||||
|
:key="failure.subjectId"
|
||||||
|
>
|
||||||
|
{{ failure.subjectRef }} · {{ failure.subjectId }}:{{
|
||||||
|
failure.error
|
||||||
|
}}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="mt-3 text-[11px] text-muted">
|
<p class="mt-3 text-[11px] text-muted">
|
||||||
@@ -333,7 +385,19 @@ watch(
|
|||||||
</section>
|
</section>
|
||||||
</div></NCollapseItem
|
</div></NCollapseItem
|
||||||
></NCollapse
|
></NCollapse
|
||||||
></NScrollbar
|
>
|
||||||
|
</div>
|
||||||
|
</WorkspaceTools>
|
||||||
|
</div>
|
||||||
|
</div></template
|
||||||
|
>
|
||||||
|
<NScrollbar
|
||||||
|
class="workspace-feedback"
|
||||||
|
v-if="catalog.error.value || styleQuery.error.value || castingQuery.error.value"
|
||||||
|
><NAlert role="alert" type="error" :show-icon="false"
|
||||||
|
>{{ catalog.error.value || styleQuery.error.value || castingQuery.error.value }}
|
||||||
|
当前相关操作已暂停,请刷新核对后端。
|
||||||
|
</NAlert></NScrollbar
|
||||||
>
|
>
|
||||||
<div v-if="subjects.length" class="identity-workspace">
|
<div v-if="subjects.length" class="identity-workspace">
|
||||||
<aside class="panel directory-panel identity-directory">
|
<aside class="panel directory-panel identity-directory">
|
||||||
@@ -360,16 +424,6 @@ watch(
|
|||||||
v-model:value="search"
|
v-model:value="search"
|
||||||
clearable
|
clearable
|
||||||
></NInput>
|
></NInput>
|
||||||
<NSelect
|
|
||||||
aria-label="身份主体类型"
|
|
||||||
v-model:value="module"
|
|
||||||
:options="[
|
|
||||||
{ label: String('全部类型'), value: 'all' },
|
|
||||||
{ label: String('人物'), value: 'character' },
|
|
||||||
{ label: String('场景'), value: 'scene' },
|
|
||||||
{ label: String('道具'), value: 'prop' }
|
|
||||||
]"
|
|
||||||
></NSelect>
|
|
||||||
</div>
|
</div>
|
||||||
<NScrollbar
|
<NScrollbar
|
||||||
class="identity-subject-list panel-scroll directory-scroll"
|
class="identity-subject-list panel-scroll directory-scroll"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import WorkspacePage from '../../components/ui/WorkspacePage.vue'
|
import WorkspacePage from '../../components/ui/WorkspacePage.vue'
|
||||||
|
import WorkspaceTools from '../../components/ui/WorkspaceTools.vue'
|
||||||
import { NAlert, NButton, NCheckbox, NCollapse, NCollapseItem, NInput, NInputNumber, NSelect, NTag } from 'naive-ui'
|
import { NAlert, NButton, NCheckbox, NCollapse, NCollapseItem, NInput, NInputNumber, NSelect, NTag } from 'naive-ui'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
@@ -31,6 +32,14 @@ const {
|
|||||||
generateProject,
|
generateProject,
|
||||||
generateStale
|
generateStale
|
||||||
} = useSubjectImages()
|
} = useSubjectImages()
|
||||||
|
const toolsOpen = ref(false)
|
||||||
|
/** 新批量回执到达时显示诊断,进入页面不自动遮挡图库。 */
|
||||||
|
watch(
|
||||||
|
() => session.value.receipt,
|
||||||
|
receipt => {
|
||||||
|
if (receipt) toolsOpen.value = true
|
||||||
|
}
|
||||||
|
)
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const module = ref('all')
|
const module = ref('all')
|
||||||
const onlyMissing = ref(false)
|
const onlyMissing = ref(false)
|
||||||
@@ -89,17 +98,32 @@ function resetFilters() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<WorkspacePage
|
<WorkspacePage compact class="gallery-workspace-page"
|
||||||
><template #header
|
><template #header>
|
||||||
|
<div class="workspace-toolbar">
|
||||||
|
<span class="toolbar-summary">主图 {{ completeCount }} / {{ forms.length }}</span>
|
||||||
|
<NButton v-if="staleForms.length" text type="error" size="small" @click="toolsOpen = true"
|
||||||
|
>{{ staleForms.length }} 个形态主图过期</NButton
|
||||||
|
>
|
||||||
|
<div class="toolbar-actions">
|
||||||
|
<NButton :disabled="query.loading.value" @click="query.refresh" text size="small"
|
||||||
|
><RefreshCw :size="13" :class="{ 'animate-spin': query.loading.value }" />刷新图库 </NButton
|
||||||
|
><WorkspaceTools
|
||||||
|
v-model:open="toolsOpen"
|
||||||
|
title="形态生图配置与回执"
|
||||||
|
:has-receipt="!!session.receipt"
|
||||||
><div class="flex flex-wrap items-end justify-between gap-4">
|
><div class="flex flex-wrap items-end justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-lg font-semibold">形态图片</h2>
|
<h2 class="text-lg font-semibold">形态图片</h2>
|
||||||
<p class="mt-2 text-sm text-muted">为人物、场景和道具准备造型参考图。已有图片直接从数据库读取。</p>
|
<p class="mt-2 text-sm text-muted">
|
||||||
|
为人物、场景和道具准备造型参考图。已有图片直接从数据库读取。
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<RouterLink :to="`/projects/${id}/storyboard`" class="text-button"
|
<RouterLink :to="`/projects/${id}/storyboard`" class="text-button"
|
||||||
>去检查分镜参考图<ArrowRight :size="14"
|
>去检查分镜参考图<ArrowRight :size="14"
|
||||||
/></RouterLink></div></template
|
/></RouterLink>
|
||||||
><NAlert type="info" :show-icon="false" class="mt-5 text-xs">
|
</div>
|
||||||
|
<NAlert type="info" :show-icon="false" class="mt-5 text-xs">
|
||||||
Character 形态图会记录生成时使用的 Identity
|
Character 形态图会记录生成时使用的 Identity
|
||||||
Anchor。更换演员母版后,旧主图会标记为“身份母版已变更”,且后端会阻止它继续进入分镜首帧;请基于当前母版重新生成候选并确认新的主参考图。
|
Anchor。更换演员母版后,旧主图会标记为“身份母版已变更”,且后端会阻止它继续进入分镜首帧;请基于当前母版重新生成候选并确认新的主参考图。
|
||||||
<RouterLink :to="`/projects/${id}/subject-identity`" class="text-button ml-2"
|
<RouterLink :to="`/projects/${id}/subject-identity`" class="text-button ml-2"
|
||||||
@@ -115,9 +139,6 @@ function resetFilters() {
|
|||||||
</span>
|
</span>
|
||||||
<span class="ml-3 text-xs text-muted">Seedream · 后端配置</span>
|
<span class="ml-3 text-xs text-muted">Seedream · 后端配置</span>
|
||||||
</p>
|
</p>
|
||||||
<NButton :disabled="query.loading.value" @click="query.refresh" text size="small"
|
|
||||||
><RefreshCw :size="13" :class="{ 'animate-spin': query.loading.value }" />刷新图库
|
|
||||||
</NButton>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-[11px] leading-6 text-muted">
|
<p class="mt-2 text-[11px] leading-6 text-muted">
|
||||||
每 6
|
每 6
|
||||||
@@ -126,8 +147,8 @@ function resetFilters() {
|
|||||||
<NAlert v-if="staleForms.length" type="error" :show-icon="false" class="mt-4"
|
<NAlert v-if="staleForms.length" type="error" :show-icon="false" class="mt-4"
|
||||||
><div class="flex flex-wrap items-center justify-between gap-3">
|
><div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<span class="flex items-center gap-2 text-xs">
|
<span class="flex items-center gap-2 text-xs">
|
||||||
<AlertTriangle :size="14" />检测到 {{ staleForms.length }} 个 Character Form 仍在使用旧 Identity
|
<AlertTriangle :size="14" />检测到 {{ staleForms.length }} 个 Character Form
|
||||||
Anchor。
|
仍在使用旧 Identity Anchor。
|
||||||
</span>
|
</span>
|
||||||
<ConfirmAction
|
<ConfirmAction
|
||||||
label="重新生成过期形态"
|
label="重新生成过期形态"
|
||||||
@@ -177,22 +198,26 @@ function resetFilters() {
|
|||||||
></NCollapse
|
></NCollapse
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<NAlert v-if="query.error.value" role="alert" type="error" :show-icon="false" class="mt-4"
|
|
||||||
>{{ query.error.value }}<span v-if="forms.length"> 当前展示上次读取的数据。</span></NAlert
|
|
||||||
><NAlert v-if="running" role="status" type="info" :show-icon="false" class="mt-4">
|
|
||||||
数据库中仍有排队或生成中的图片,已暂停新的生图操作。长时间无变化时,请先核对后台任务;刷新或关闭页面不会取消任务。
|
|
||||||
</NAlert>
|
|
||||||
<section v-if="session.receipt" class="panel mt-4 p-5" aria-label="生图批量回执">
|
<section v-if="session.receipt" class="panel mt-4 p-5" aria-label="生图批量回执">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<h3 class="text-sm font-medium">{{ session.receipt.title }} · 本次回执</h3>
|
<h3 class="text-sm font-medium">{{ session.receipt.title }} · 本次回执</h3>
|
||||||
<NButton @click="exportReceipt" text size="small"><Download :size="13" />导出诊断</NButton>
|
<NButton @click="exportReceipt" text size="small"
|
||||||
|
><Download :size="13" />导出诊断</NButton
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-3 text-xs text-muted">
|
<p class="mt-3 text-xs text-muted">
|
||||||
共 {{ session.receipt.result.total }} 个形态 · 本次目标 {{ session.receipt.result.targetCount }} · 生成
|
共 {{ session.receipt.result.total }} 个形态 · 本次目标
|
||||||
{{ session.receipt.result.generated }} · 跳过 {{ session.receipt.result.skipped }} · 失败
|
{{ session.receipt.result.targetCount }} · 生成 {{ session.receipt.result.generated }} ·
|
||||||
|
跳过 {{ session.receipt.result.skipped }} · 失败
|
||||||
{{ session.receipt.result.failed }}
|
{{ session.receipt.result.failed }}
|
||||||
</p>
|
</p>
|
||||||
<NAlert v-if="session.receipt.result.failed" role="alert" type="error" :show-icon="false" class="mt-3">
|
<NAlert
|
||||||
|
v-if="session.receipt.result.failed"
|
||||||
|
role="alert"
|
||||||
|
type="error"
|
||||||
|
:show-icon="false"
|
||||||
|
class="mt-3"
|
||||||
|
>
|
||||||
部分形态生图失败。已生成的图片保留;先查看失败原因,再按形态重新生图。
|
部分形态生图失败。已生成的图片保留;先查看失败原因,再按形态重新生图。
|
||||||
</NAlert>
|
</NAlert>
|
||||||
<NAlert
|
<NAlert
|
||||||
@@ -209,8 +234,13 @@ function resetFilters() {
|
|||||||
>:{{ failure.error }}
|
>:{{ failure.error }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="mt-3 text-[11px] text-muted">回执仅保留于当前浏览器会话;图片以刷新后的数据库记录为准。</p>
|
<p class="mt-3 text-[11px] text-muted">
|
||||||
|
回执仅保留于当前浏览器会话;图片以刷新后的数据库记录为准。
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
</WorkspaceTools>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-image-filter-region">
|
<div class="form-image-filter-region">
|
||||||
<div class="form-image-filters" role="group" aria-label="形态图片筛选">
|
<div class="form-image-filters" role="group" aria-label="形态图片筛选">
|
||||||
<NInput
|
<NInput
|
||||||
@@ -237,6 +267,12 @@ function resetFilters() {
|
|||||||
<span class="filter-result-count">{{ filtered.length }} 个形态</span>
|
<span class="filter-result-count">{{ filtered.length }} 个形态</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<NAlert v-if="query.error.value" role="alert" type="error" :show-icon="false" class="mt-4"
|
||||||
|
>{{ query.error.value }}<span v-if="forms.length"> 当前展示上次读取的数据。</span></NAlert
|
||||||
|
><NAlert v-if="running" role="status" type="info" :show-icon="false" class="mt-4">
|
||||||
|
数据库中仍有排队或生成中的图片,已暂停新的生图操作。长时间无变化时,请先核对后台任务;刷新或关闭页面不会取消任务。
|
||||||
|
</NAlert>
|
||||||
|
</template>
|
||||||
<p v-if="!query.data.value && query.loading.value" class="py-10 text-sm text-muted" role="status">
|
<p v-if="!query.data.value && query.loading.value" class="py-10 text-sm text-muted" role="status">
|
||||||
正在读取形态和已有图片…
|
正在读取形态和已有图片…
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ function confirm() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<span class="confirm-action">
|
||||||
|
<!-- 稳定的根元素承接调用方间距;AppDialog 含多个根节点,不能替按钮接收 class。 -->
|
||||||
<AppDialog v-model:open="open" :title="label" :description="description">
|
<AppDialog v-model:open="open" :title="label" :description="description">
|
||||||
<template #trigger
|
<template #trigger
|
||||||
><NButton :type="primary ? 'primary' : 'default'" :disabled="disabled" @click="open = true">{{
|
><NButton :type="primary ? 'primary' : 'default'" :disabled="disabled" @click="open = true">{{
|
||||||
@@ -52,4 +54,5 @@ function confirm() {
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</AppDialog>
|
</AppDialog>
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ function button(label: string): HTMLButtonElement {
|
|||||||
async function confirmGeneration(label: string) {
|
async function confirmGeneration(label: string) {
|
||||||
button(label).click()
|
button(label).click()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
const acknowledgement = document.querySelector<HTMLElement>('[role="dialog"] [role="checkbox"]')
|
const acknowledgement = document.querySelector<HTMLElement>('.app-dialog [role="checkbox"]')
|
||||||
expect(button('确认' + label).disabled).toBe(true)
|
expect(button('确认' + label).disabled).toBe(true)
|
||||||
acknowledgement!.click()
|
acknowledgement!.click()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
@@ -193,9 +193,10 @@ describe('内容级滚动下的生产反馈', () => {
|
|||||||
global: { provide: { [projectContextKey as symbol]: context() }, stubs: { RouterLink: true } }
|
global: { provide: { [projectContextKey as symbol]: context() }, stubs: { RouterLink: true } }
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.find('.workspace-overview .n-collapse-item--active').exists()).toBe(false)
|
expect(wrapper.get('[data-workspace-tools]').attributes('aria-expanded')).toBe('false')
|
||||||
|
expect(wrapper.find('.workspace-tools-drawer').exists()).toBe(false)
|
||||||
const alert = wrapper.get('.workspace-split [role="alert"]')
|
const alert = wrapper.get('.workspace-split [role="alert"]')
|
||||||
expect(alert.element.closest('.workspace-overview')).toBeNull()
|
expect(alert.element.closest('.workspace-tools-drawer')).toBeNull()
|
||||||
expect(alert.text()).toContain('无法连接后端')
|
expect(alert.text()).toContain('无法连接后端')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -212,7 +213,8 @@ describe('内容级滚动下的生产反馈', () => {
|
|||||||
result: { totalFailed: 1, retried: 0, failed: 1, failures: [{ shotId: 'shot-1', error: '供应商不可用' }] }
|
result: { totalFailed: 1, retried: 0, failed: 1, failures: [{ shotId: 'shot-1', error: '供应商不可用' }] }
|
||||||
}
|
}
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.find('.workspace-overview .n-collapse-item--active').exists()).toBe(true)
|
expect(wrapper.get('[data-workspace-tools]').attributes('aria-expanded')).toBe('true')
|
||||||
|
expect(wrapper.find('.workspace-tools-drawer .n-collapse-item--active').exists()).toBe(true)
|
||||||
expect(wrapper.text()).toContain('供应商不可用')
|
expect(wrapper.text()).toContain('供应商不可用')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -313,7 +315,7 @@ describe('视觉风格与主体身份工作区', () => {
|
|||||||
await mountAssets('style')
|
await mountAssets('style')
|
||||||
button('AI 重新生成风格').click()
|
button('AI 重新生成风格').click()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
document.querySelector<HTMLElement>('[role="dialog"] [role="checkbox"]')!.click()
|
document.querySelector<HTMLElement>('.app-dialog [role="checkbox"]')!.click()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(button('确认AI 重新生成风格').disabled).toBe(false)
|
expect(button('确认AI 重新生成风格').disabled).toBe(false)
|
||||||
button('取消').click()
|
button('取消').click()
|
||||||
@@ -872,6 +874,10 @@ describe('工作台页面交互', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
await confirmGeneration('生成本集导演设计')
|
await confirmGeneration('生成本集导演设计')
|
||||||
expect(button('补齐项目导演设计').disabled).toBe(true)
|
expect(button('补齐项目导演设计').disabled).toBe(true)
|
||||||
|
await wrapper!.get('[aria-label="关闭操作面板"]').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper!.get('[data-workspace-tools]').attributes('aria-expanded')).toBe('false')
|
||||||
|
expect(getOperation(fixture.id).pending).toBe(true)
|
||||||
selectControl(wrapper!, 'aria-label', '选择分镜剧集').vm.$emit('update:value', 2)
|
selectControl(wrapper!, 'aria-label', '选择分镜剧集').vm.$emit('update:value', 2)
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
wrapper!.unmount()
|
wrapper!.unmount()
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { NSelect } from 'naive-ui'
|
|||||||
|
|
||||||
/** 展开真实折叠面板后再测试内部操作,避免对不可见控件进行断言。 */
|
/** 展开真实折叠面板后再测试内部操作,避免对不可见控件进行断言。 */
|
||||||
export async function expandSections() {
|
export async function expandSections() {
|
||||||
|
const tools = document.querySelector<HTMLElement>('[data-workspace-tools][aria-expanded="false"]')
|
||||||
|
tools?.click()
|
||||||
|
await flushPromises()
|
||||||
for (let depth = 0; depth < 4; depth++) {
|
for (let depth = 0; depth < 4; depth++) {
|
||||||
const headers = document.querySelectorAll<HTMLElement>(
|
const headers = document.querySelectorAll<HTMLElement>(
|
||||||
'.n-collapse-item:not(.n-collapse-item--active) > .n-collapse-item__header .n-collapse-item__header-main'
|
'.n-collapse-item:not(.n-collapse-item--active) > .n-collapse-item__header .n-collapse-item__header-main'
|
||||||
|
|||||||
Reference in New Issue
Block a user