feat: 关闭已有刷新页面的定时轮询
This commit is contained in:
@@ -98,7 +98,7 @@ Oxfmt 不负责代码质量,Oxlint 不负责 Vue 的完整类型推导;Vue S
|
|||||||
|
|
||||||
- 外层应用固定为视口高度;侧栏、顶栏、项目标题和工作流导航不会随正文滚动。
|
- 外层应用固定为视口高度;侧栏、顶栏、项目标题和工作流导航不会随正文滚动。
|
||||||
- 工作流只保留左侧导航,项目标题下不再重复展示相同标签。主体身份、分镜设计、镜头生产和图库采用内容优先布局:常用筛选/剧集选择/刷新在固定的紧凑工具栏,列表占据剩余高度。
|
- 工作流只保留左侧导航,项目标题下不再重复展示相同标签。主体身份、分镜设计、镜头生产和图库采用内容优先布局:常用筛选/剧集选择/刷新在固定的紧凑工具栏,列表占据剩余高度。
|
||||||
- 项目标题栏只显示返回入口和名称,不再放置状态标签与刷新按钮;除形态图库使用手动刷新外,其他项目工作区每 6 秒自动刷新,读取失败时在错误提示内提供重试入口。
|
- 项目标题栏只显示返回入口和名称,不再放置状态标签与刷新按钮;已有刷新按钮的工作区不再定时轮询,剧本创作和拆解仍每 6 秒同步项目状态,读取失败时在错误提示内提供重试入口。
|
||||||
- 只有当前项目正式状态为 `completed` 才解锁下游工作区;草稿、生成中、待审核、失败和读取期间默认禁用下游导航。直接访问下游链接只显示返回剧本创作的引导,不挂载操作组件;轮询确认完成后自动解锁,切换项目立即重新判定。页面内提交守卫同步检查完成状态;这是前端交互限制,不代替后端权限与业务校验。
|
- 只有当前项目正式状态为 `completed` 才解锁下游工作区;草稿、生成中、待审核、失败和读取期间默认禁用下游导航。直接访问下游链接只显示返回剧本创作的引导,不挂载操作组件;轮询确认完成后自动解锁,切换项目立即重新判定。页面内提交守卫同步检查完成状态;这是前端交互限制,不代替后端权限与业务校验。
|
||||||
- 项目列表采用固定表头表格;剧本、分镜、身份与生产页的目录和详情独立滚动,其他工作区只滚动内容区。
|
- 项目列表采用固定表头表格;剧本、分镜、身份与生产页的目录和详情独立滚动,其他工作区只滚动内容区。
|
||||||
- “我的剧本”状态筛选采用中号控件,与搜索框等高并扩大点击区域;刷新、主题、折叠和关闭等单图标按钮统一使用直角正方形,加载时保持尺寸不变,保留可访问名称。
|
- “我的剧本”状态筛选采用中号控件,与搜索框等高并扩大点击区域;刷新、主题、折叠和关闭等单图标按钮统一使用直角正方形,加载时保持尺寸不变,保留可访问名称。
|
||||||
@@ -201,7 +201,7 @@ API 层也提供 `state` 与 `breakdown/latest` 方法。页面通过共享 chec
|
|||||||
|
|
||||||
1. `POST /projects` 返回顶层 `202 { projectId, status }`,其他接口通常返回 `{ data }`;HTTP 层分别兼容。
|
1. `POST /projects` 返回顶层 `202 { projectId, status }`,其他接口通常返回 `{ data }`;HTTP 层分别兼容。
|
||||||
2. Breakdown、Storyboard 和恢复接口目前是**长 HTTP 请求**。不设客户端短超时,不自动重试 POST。关闭页面只会丢失请求连接,**不会取消后端任务**。
|
2. Breakdown、Storyboard 和恢复接口目前是**长 HTTP 请求**。不设客户端短超时,不自动重试 POST。关闭页面只会丢失请求连接,**不会取消后端任务**。
|
||||||
3. 项目列表每 12 秒、项目详情每 6 秒串行刷新。形态图库暂停父布局和页面定时查询,返回其他工作区恢复轮询;图库直接链接被未完成状态拦截时可手动刷新项目状态。隐藏页面暂停自动查询。切换项目或离开布局时取消查询,旧响应不会覆盖新项目。
|
3. 项目列表、视觉风格、主体身份、分镜、镜头生产、单镜头资产和运行诊断已有刷新按钮,只在进入、切换目标和手动刷新时读取。剧本创作与拆解仍每 6 秒同步项目详情和 checkpoint。形态图库暂停父布局和页面定时查询;图库直接链接被未完成状态拦截时可手动刷新项目状态。隐藏页面暂停自动查询。切换项目或离开布局时取消查询,旧响应不会覆盖新项目。
|
||||||
4. 抽取任务进度不是全流程进度。抽取完成之后还需要主体合并、形态、节拍、镜头和持久化。
|
4. 抽取任务进度不是全流程进度。抽取完成之后还需要主体合并、形态、节拍、镜头和持久化。
|
||||||
5. 后端失败 checkpoint 可能只有错误。页面回看最近可用的完整快照以保留成果,同时使用最新记录的 execution 状态。显示的是**最近可用快照**,并不保证全部成果来自最后一次执行。
|
5. 后端失败 checkpoint 可能只有错误。页面回看最近可用的完整快照以保留成果,同时使用最新记录的 execution 状态。显示的是**最近可用快照**,并不保证全部成果来自最后一次执行。
|
||||||
6. 后端并不为每个阶段提供持久化的 running 状态。没有明确终态时,页面显示“阶段快照 · 执行状态待确认”,不会猜测成功或失败。
|
6. 后端并不为每个阶段提供持久化的 running 状态。没有明确终态时,页面显示“阶段快照 · 执行状态待确认”,不会猜测成功或失败。
|
||||||
@@ -217,7 +217,7 @@ API 层也提供 `state` 与 `breakdown/latest` 方法。页面通过共享 chec
|
|||||||
5. 批量默认 `force: false`,跳过完整剧集。重新点击“补齐”用于重试未完成项,没有虚构独立 retry 接口。`force: true` 才覆盖全部。VisualState 可设置非负整数修复次数,`0` 表示不修复。
|
5. 批量默认 `force: false`,跳过完整剧集。重新点击“补齐”用于重试未完成项,没有虚构独立 retry 接口。`force: true` 才覆盖全部。VisualState 可设置非负整数修复次数,`0` 表示不修复。
|
||||||
6. VisualState 单集生成需要该集所有正式镜头都有 Direction;批量接口由后端逐集检查,缺少前置条件的集数会单独失败。HTTP 200 也可能包含校验失败或部分失败,页面展示逐集诊断和修复次数,不视为全成功。
|
6. VisualState 单集生成需要该集所有正式镜头都有 Direction;批量接口由后端逐集检查,缺少前置条件的集数会单独失败。HTTP 200 也可能包含校验失败或部分失败,页面展示逐集诊断和修复次数,不视为全成功。
|
||||||
7. 上游重生成**不会自动使下游旧数据失效**。覆盖 Direction 后请重生成 VisualState,修改设计/状态/参考图后按需覆盖 Prompt。覆盖率只说明字段已保存,不能证明下游与新版本一致。
|
7. 上游重生成**不会自动使下游旧数据失效**。覆盖 Direction 后请重生成 VisualState,修改设计/状态/参考图后按需覆盖 Prompt。覆盖率只说明字段已保存,不能证明下游与新版本一致。
|
||||||
8. 每 6 秒查询当前剧集的两类正式结果;这不是全项目实时进度。后端没有独立 Storyboard running / executionId 查询。回执只保留当前浏览器会话,切换页面不丢失,刷新浏览器会丢失。
|
8. 分镜页用刷新按钮查询当前剧集的两类正式结果;这不是全项目实时进度。后端没有独立 Storyboard running / executionId 查询。回执只保留当前浏览器会话,切换页面不丢失,刷新浏览器会丢失。
|
||||||
9. 参考图和 GenerationSpec 按需 GET,切换镜头或设计变化时取消旧查询。GenerationSpec 需要有效的 Direction、VisualState 和形态关联,不生成图片或视频。
|
9. 参考图和 GenerationSpec 按需 GET,切换镜头或设计变化时取消旧查询。GenerationSpec 需要有效的 Direction、VisualState 和形态关联,不生成图片或视频。
|
||||||
10. Video Prompt、Keyframe 与 Video 都有项目级 readiness。`force` 会重写 Prompt,普通首帧/视频新增候选;**过期首帧即使 force 也会更新主图**,非 force 的过期视频完成后会接替旧主视频。确认文案明确此差异。首帧支持可选 `limit` 和成对 `width`/`height`,只影响首帧,不污染提示词和视频参数。视频任务创建成功不代表成片完成,最终状态以后端轮询和持久化资产为准。
|
10. Video Prompt、Keyframe 与 Video 都有项目级 readiness。`force` 会重写 Prompt,普通首帧/视频新增候选;**过期首帧即使 force 也会更新主图**,非 force 的过期视频完成后会接替旧主视频。确认文案明确此差异。首帧支持可选 `limit` 和成对 `width`/`height`,只影响首帧,不污染提示词和视频参数。视频任务创建成功不代表成片完成,最终状态以后端轮询和持久化资产为准。
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -229,7 +229,7 @@ const menuOptions = computed<MenuOption[]>(() => [
|
|||||||
<dt>生产环境</dt>
|
<dt>生产环境</dt>
|
||||||
<dd>为 /api、/storage 配置反向代理,并为长工作流设置足够的超时。</dd>
|
<dd>为 /api、/storage 配置反向代理,并为长工作流设置足够的超时。</dd>
|
||||||
<dt>状态更新</dt>
|
<dt>状态更新</dt>
|
||||||
<dd>形态图库使用手动刷新,其他项目工作区每 6 秒更新;关闭页面不会取消后端任务。</dd>
|
<dd>已有刷新按钮的页面使用手动更新;剧本创作和拆解仍每 6 秒同步项目状态。关闭页面不会取消后端任务。</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</AppDialog>
|
</AppDialog>
|
||||||
</NConfigProvider>
|
</NConfigProvider>
|
||||||
|
|||||||
@@ -40,15 +40,20 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
const emit = defineEmits<{ changed: [] }>()
|
const emit = defineEmits<{ changed: [] }>()
|
||||||
const key = computed(() => props.shot.shotId)
|
const key = computed(() => props.shot.shotId)
|
||||||
const query = usePolling(key, async (shotId, signal) => {
|
/** 单镜头资产已有刷新入口,不再定时轮询。 */
|
||||||
const [keyframes, videos] = await Promise.all([
|
const query = usePolling(
|
||||||
productionApi.listKeyframes(shotId, signal),
|
key,
|
||||||
productionApi.listVideos(shotId, signal)
|
async (shotId, signal) => {
|
||||||
])
|
const [keyframes, videos] = await Promise.all([
|
||||||
if (keyframes.some(item => item.shotId !== shotId) || videos.some(item => item.shotId !== shotId))
|
productionApi.listKeyframes(shotId, signal),
|
||||||
throw new Error('资产记录与当前镜头不匹配,请刷新后重试。')
|
productionApi.listVideos(shotId, signal)
|
||||||
return { keyframes, videos }
|
])
|
||||||
})
|
if (keyframes.some(item => item.shotId !== shotId) || videos.some(item => item.shotId !== shotId))
|
||||||
|
throw new Error('资产记录与当前镜头不匹配,请刷新后重试。')
|
||||||
|
return { keyframes, videos }
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
const keyframes = computed(() => query.data.value?.keyframes ?? [])
|
const keyframes = computed(() => query.data.value?.keyframes ?? [])
|
||||||
const videos = computed(() => query.data.value?.videos ?? [])
|
const videos = computed(() => query.data.value?.videos ?? [])
|
||||||
const currentKeyframe = computed(() => primaryKeyframe(keyframes.value))
|
const currentKeyframe = computed(() => primaryKeyframe(keyframes.value))
|
||||||
|
|||||||
@@ -42,20 +42,25 @@ export function useProduction() {
|
|||||||
0
|
0
|
||||||
)
|
)
|
||||||
const queryKey = computed(() => JSON.stringify([id.value, episodeNo.value, force.value]))
|
const queryKey = computed(() => JSON.stringify([id.value, episodeNo.value, force.value]))
|
||||||
const query = usePolling(queryKey, async (key, signal) => {
|
/** 生产页已有刷新按钮,就绪状态改为手动更新。 */
|
||||||
const [projectId, number, overwrite] = JSON.parse(key) as [string, number, boolean]
|
const query = usePolling(
|
||||||
if (!projectId || !number) return null
|
queryKey,
|
||||||
const [directions, prompts, keyframes, videos, videoStatus] = await Promise.all([
|
async (key, signal) => {
|
||||||
storyboardApi.directions(projectId, number, signal),
|
const [projectId, number, overwrite] = JSON.parse(key) as [string, number, boolean]
|
||||||
productionApi.promptReadiness(projectId, overwrite, signal),
|
if (!projectId || !number) return null
|
||||||
productionApi.keyframeReadiness(projectId, overwrite, signal),
|
const [directions, prompts, keyframes, videos, videoStatus] = await Promise.all([
|
||||||
productionApi.videoReadiness(projectId, overwrite, signal),
|
storyboardApi.directions(projectId, number, signal),
|
||||||
productionApi.projectVideoStatus(projectId, signal)
|
productionApi.promptReadiness(projectId, overwrite, signal),
|
||||||
])
|
productionApi.keyframeReadiness(projectId, overwrite, signal),
|
||||||
if (directions.projectId !== projectId || directions.episodeNo !== number)
|
productionApi.videoReadiness(projectId, overwrite, signal),
|
||||||
throw new Error('镜头生产查询返回了不匹配的项目或剧集,请刷新后重试。')
|
productionApi.projectVideoStatus(projectId, signal)
|
||||||
return { directions, prompts, keyframes, videos, videoStatus }
|
])
|
||||||
})
|
if (directions.projectId !== projectId || directions.episodeNo !== number)
|
||||||
|
throw new Error('镜头生产查询返回了不匹配的项目或剧集,请刷新后重试。')
|
||||||
|
return { directions, prompts, keyframes, videos, videoStatus }
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
const source = computed(() => sourceEpisodes.value.find(item => item.episodeNo === episodeNo.value))
|
const source = computed(() => sourceEpisodes.value.find(item => item.episodeNo === episodeNo.value))
|
||||||
const shots = computed(() => mergeDesignedShots(query.data.value?.directions ?? null, null, source.value))
|
const shots = computed(() => mergeDesignedShots(query.data.value?.directions ?? null, null, source.value))
|
||||||
const shot = computed(() => shots.value.find(item => item.shotId === selectedShot.value) ?? shots.value[0])
|
const shot = computed(() => shots.value.find(item => item.shotId === selectedShot.value) ?? shots.value[0])
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import CreateProjectDialog from './components/CreateProjectDialog.vue'
|
|||||||
|
|
||||||
/** 项目表格使用固定表头与独立滚动;搜索筛选仍作用于真实接口数据。 */
|
/** 项目表格使用固定表头与独立滚动;搜索筛选仍作用于真实接口数据。 */
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const query = usePolling(ref('projects'), (_, signal) => projectsApi.list(signal), 12_000)
|
/** 列表已有刷新按钮,只在进入、筛选切换和手动刷新时读取。 */
|
||||||
|
const query = usePolling(ref('projects'), (_, signal) => projectsApi.list(signal), false)
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const filter = ref('all')
|
const filter = ref('all')
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
|
|||||||
@@ -38,23 +38,28 @@ export function useStoryboard() {
|
|||||||
0
|
0
|
||||||
)
|
)
|
||||||
const queryKey = computed(() => JSON.stringify([id.value, episodeNo.value]))
|
const queryKey = computed(() => JSON.stringify([id.value, episodeNo.value]))
|
||||||
const query = usePolling(queryKey, async (key, signal) => {
|
/** 分镜页已有刷新按钮,不再定时轮询当前剧集。 */
|
||||||
const [projectId, number] = JSON.parse(key) as [string, number]
|
const query = usePolling(
|
||||||
if (!projectId || !number) return null
|
queryKey,
|
||||||
const [directions, visualStates] = await Promise.all([
|
async (key, signal) => {
|
||||||
storyboardApi.directions(projectId, number, signal),
|
const [projectId, number] = JSON.parse(key) as [string, number]
|
||||||
storyboardApi.visualStates(projectId, number, signal)
|
if (!projectId || !number) return null
|
||||||
])
|
const [directions, visualStates] = await Promise.all([
|
||||||
if (
|
storyboardApi.directions(projectId, number, signal),
|
||||||
directions.projectId !== projectId ||
|
storyboardApi.visualStates(projectId, number, signal)
|
||||||
visualStates.projectId !== projectId ||
|
])
|
||||||
directions.episodeNo !== number ||
|
if (
|
||||||
visualStates.episodeNo !== number
|
directions.projectId !== projectId ||
|
||||||
) {
|
visualStates.projectId !== projectId ||
|
||||||
throw new Error('分镜查询返回了不匹配的项目或剧集,请刷新后重试。')
|
directions.episodeNo !== number ||
|
||||||
}
|
visualStates.episodeNo !== number
|
||||||
return { directions, visualStates }
|
) {
|
||||||
})
|
throw new Error('分镜查询返回了不匹配的项目或剧集,请刷新后重试。')
|
||||||
|
}
|
||||||
|
return { directions, visualStates }
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
const data = query.data
|
const data = query.data
|
||||||
const source = computed(() => sourceEpisodes.value.find(item => item.episodeNo === episodeNo.value))
|
const source = computed(() => sourceEpisodes.value.find(item => item.episodeNo === episodeNo.value))
|
||||||
const shots = computed(() =>
|
const shots = computed(() =>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ function save() {
|
|||||||
>
|
>
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<span class="text-xs text-muted">{{
|
<span class="text-xs text-muted">{{
|
||||||
dirty ? '有未保存修改,自动刷新不会覆盖草稿。' : '图片以已保存的身份提示词为准。'
|
dirty ? '有未保存修改,刷新不会覆盖草稿。' : '图片以已保存的身份提示词为准。'
|
||||||
}}</span
|
}}</span
|
||||||
><NButton :disabled="disabled" type="primary" attr-type="submit">保存主体身份</NButton>
|
><NButton :disabled="disabled" type="primary" attr-type="submit">保存主体身份</NButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,39 +30,59 @@ export function useSubjectIdentity() {
|
|||||||
const concurrency = ref(3)
|
const concurrency = ref(3)
|
||||||
const candidateLimit = ref(3)
|
const candidateLimit = ref(3)
|
||||||
const force = ref(false)
|
const force = ref(false)
|
||||||
const catalog = usePolling(projectId, async (id, signal) => {
|
/** 目录与身份已有刷新按钮,查询只在进入、切换和手动刷新时读取。 */
|
||||||
const forms = await subjectImagesApi.listForms(id, signal)
|
const catalog = usePolling(
|
||||||
if (
|
projectId,
|
||||||
forms.some(
|
async (id, signal) => {
|
||||||
form => form.subject.projectId !== id || form.images.some(image => image.subjectFormId !== form.id)
|
const forms = await subjectImagesApi.listForms(id, signal)
|
||||||
|
if (
|
||||||
|
forms.some(
|
||||||
|
form => form.subject.projectId !== id || form.images.some(image => image.subjectFormId !== form.id)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
throw new Error('主体目录与当前项目不匹配,请刷新后重试。')
|
||||||
throw new Error('主体目录与当前项目不匹配,请刷新后重试。')
|
return {
|
||||||
return { subjects: groupIdentitySubjects(forms), running: forms.some(form => hasRunningImages(form.images)) }
|
subjects: groupIdentitySubjects(forms),
|
||||||
})
|
running: forms.some(form => hasRunningImages(form.images))
|
||||||
const styleQuery = usePolling(projectId, async (id, signal) => {
|
}
|
||||||
const style = await visualStyleApi.get(id, signal)
|
},
|
||||||
if (style && style.projectId !== id) throw new Error('视觉风格与当前项目不匹配。')
|
false
|
||||||
return { style }
|
)
|
||||||
})
|
const styleQuery = usePolling(
|
||||||
const castingQuery = usePolling(projectId, async (id, signal) => {
|
projectId,
|
||||||
const readiness = await subjectIdentityApi.castingReadiness(id, signal)
|
async (id, signal) => {
|
||||||
return readiness
|
const style = await visualStyleApi.get(id, signal)
|
||||||
})
|
if (style && style.projectId !== id) throw new Error('视觉风格与当前项目不匹配。')
|
||||||
|
return { style }
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
const castingQuery = usePolling(
|
||||||
|
projectId,
|
||||||
|
async (id, signal) => {
|
||||||
|
const readiness = await subjectIdentityApi.castingReadiness(id, signal)
|
||||||
|
return readiness
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
const subjects = computed(() =>
|
const subjects = computed(() =>
|
||||||
mergeCastingSubjects(catalog.data.value?.subjects ?? [], castingQuery.data.value?.items ?? [], projectId.value)
|
mergeCastingSubjects(catalog.data.value?.subjects ?? [], castingQuery.data.value?.items ?? [], projectId.value)
|
||||||
)
|
)
|
||||||
const subject = computed(() => subjects.value.find(item => item.id === selectedId.value))
|
const subject = computed(() => subjects.value.find(item => item.id === selectedId.value))
|
||||||
const selectionKey = computed(() => subject.value?.id ?? '')
|
const selectionKey = computed(() => subject.value?.id ?? '')
|
||||||
const detail = usePolling(selectionKey, async (id, signal) => {
|
const detail = usePolling(
|
||||||
if (!id) return null
|
selectionKey,
|
||||||
const identity = await subjectIdentityApi.get(id, signal)
|
async (id, signal) => {
|
||||||
if (!identity) return { identity: null, images: [] as IdentityImage[] }
|
if (!id) return null
|
||||||
assertIdentity(identity, id)
|
const identity = await subjectIdentityApi.get(id, signal)
|
||||||
const images = await subjectIdentityApi.listImages(id, signal)
|
if (!identity) return { identity: null, images: [] as IdentityImage[] }
|
||||||
if (images.some(image => image.identityId !== identity.id)) throw new Error('身份图片与当前主体不匹配。')
|
assertIdentity(identity, id)
|
||||||
return { identity, images }
|
const images = await subjectIdentityApi.listImages(id, signal)
|
||||||
})
|
if (images.some(image => image.identityId !== identity.id)) throw new Error('身份图片与当前主体不匹配。')
|
||||||
|
return { identity, images }
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
const identity = computed(() => detail.data.value?.identity ?? null)
|
const identity = computed(() => detail.data.value?.identity ?? null)
|
||||||
const images = computed(() => detail.data.value?.images ?? [])
|
const images = computed(() => detail.data.value?.images ?? [])
|
||||||
const castingItem = computed(() =>
|
const castingItem = computed(() =>
|
||||||
|
|||||||
@@ -15,11 +15,16 @@ import StyleImages from './components/StyleImages.vue'
|
|||||||
|
|
||||||
/** 项目视觉风格工作区:文本编辑、锁定和风格图记录分开管理。 */
|
/** 项目视觉风格工作区:文本编辑、锁定和风格图记录分开管理。 */
|
||||||
const { projectId, blocked } = useProjectMutationGuard()
|
const { projectId, blocked } = useProjectMutationGuard()
|
||||||
const query = usePolling(projectId, async (id, signal) => {
|
/** 风格页已有刷新按钮,不再定时轮询。 */
|
||||||
const style = await visualStyleApi.get(id, signal)
|
const query = usePolling(
|
||||||
assertProject(style, id)
|
projectId,
|
||||||
return { style }
|
async (id, signal) => {
|
||||||
})
|
const style = await visualStyleApi.get(id, signal)
|
||||||
|
assertProject(style, id)
|
||||||
|
return { style }
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
const style = computed(() => query.data.value?.style ?? null)
|
const style = computed(() => query.data.value?.style ?? null)
|
||||||
const disabled = computed(() => blocked.value || !!query.error.value || !query.data.value)
|
const disabled = computed(() => blocked.value || !!query.error.value || !query.data.value)
|
||||||
const editorRevision = ref(0)
|
const editorRevision = ref(0)
|
||||||
@@ -143,7 +148,7 @@ function removeImage(image: VisualStyleImage) {
|
|||||||
>{{ query.error.value }} 请确认后端 dev 已更新并重启;查询失败不等同于尚未创建。
|
>{{ query.error.value }} 请确认后端 dev 已更新并重启;查询失败不等同于尚未创建。
|
||||||
</NAlert>
|
</NAlert>
|
||||||
<p v-if="dirty" class="mb-4 text-xs text-muted">
|
<p v-if="dirty" class="mb-4 text-xs text-muted">
|
||||||
有未保存修改,AI 重生成暂不可用。自动刷新不会覆盖编辑内容;离开页面不会自动保存。
|
有未保存修改,AI 重生成暂不可用。刷新不会覆盖编辑内容;离开页面不会自动保存。
|
||||||
</p>
|
</p>
|
||||||
<DetailDisclosure title="风格生成规则" class="mb-5">
|
<DetailDisclosure title="风格生成规则" class="mb-5">
|
||||||
AI
|
AI
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import type { Checkpoint } from './types'
|
|||||||
const props = defineProps<{ projectId: string; checkpoints: Checkpoint[] }>()
|
const props = defineProps<{ projectId: string; checkpoints: Checkpoint[] }>()
|
||||||
const open = defineModel<boolean>('open', { default: false })
|
const open = defineModel<boolean>('open', { default: false })
|
||||||
const key = computed(() => (open.value ? props.projectId : ''))
|
const key = computed(() => (open.value ? props.projectId : ''))
|
||||||
const query = usePolling(key, (id, signal) => (id ? loadWorkflowDiagnostics(id, signal) : Promise.resolve(null)), 12000)
|
/** 诊断弹窗已有刷新按钮,打开后只读取一次。 */
|
||||||
|
const query = usePolling(key, (id, signal) => (id ? loadWorkflowDiagnostics(id, signal) : Promise.resolve(null)), false)
|
||||||
const tab = ref('timeline')
|
const tab = ref('timeline')
|
||||||
const workflow = ref('all')
|
const workflow = ref('all')
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
|
|||||||
Reference in New Issue
Block a user