feat(studio): E6/U6 创作流改一步式——Create/Task/store 接 /studio/* 任务链
把创作流从旧两步(project.create + aigc.generate)改为一步式 studio.create(plan 002 U6):
- store/create.ts:重写为 studio 任务链编排——loadTemplates/create/modify/extend/pollTask(getTaskChain)/regenerate;终态集 {2完成,3失败}(studio 链无 4/5);currentTask→currentChain
- views/create/Create.vue:onSubmit 一次调 createStore.create({title,prompt,templateId}) 取任务链(含 gameId),去 projectStore 两步;模板类型 AigcTemplate→StudioTemplate
- views/create/Task.vue:轮询 studio 任务链(status 0初始/1生成中/2完成/3失败);去取消(studio 契约无 cancel 端点)、去 failureReason 逐项映射(链无该字段,通用兜底);retry→regenerate;成功→预览(versionId+gameId)
- 预览侧 GamePlayer 零改(已支持 versionId/gameId)
验证:`npm run build`(vue-tsc -b + vite build)绿;消费方仅 Create/Task(已同步)。运行态 create→poll→preview 全链 CDP 真机走查待 modify 入口落地后一并做。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c2b7a572a1
commit
9300ae2d6d
@ -1,25 +1,39 @@
|
||||
/**
|
||||
* useCreateStore —— 一句话生成(模板 + 异步任务 + 进度轮询)。
|
||||
* 对应 spec §3.2:state templates/currentTask/progress;actions loadTemplates/generate/pollTask/cancel/retry。
|
||||
* 异步生成走「提交→轮询 getTask→终态」,轮询计时器在终态/取消/卸载时务必清理(防泄漏)。
|
||||
* useCreateStore —— 一步式创作(studio.yaml /studio/*:模板 + 异步任务链 + 进度轮询)。
|
||||
* 对应 plan 002 U6 / execution §5.6:state templates/currentChain/progress;
|
||||
* actions loadTemplates/create/modify/extend/pollTask/regenerate/reset。
|
||||
*
|
||||
* 创作链路(改源不改产物,产新预览版):
|
||||
* create → 一步式(draft+generate 封装),返回任务链;
|
||||
* modify → 改源(deterministic 覆写 / regenerate-module 重生成单 behavior),产新预览版;
|
||||
* extend → additive 内容扩展;
|
||||
* 三者均返 StudioTaskChain,前端轮询 task/{id} 至终态(status=2 完成取 versionId / 3 失败)。
|
||||
* 轮询计时器在终态/卸载时务必清理(防泄漏)。studio 契约无「取消」端点,失败经 regenerate 重试。
|
||||
*/
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import * as aigcApi from '../api/aigc'
|
||||
import type { AigcTemplate, AigcTask, AigcTaskStatus } from '../api/types'
|
||||
import * as studioApi from '../api/studio'
|
||||
import type {
|
||||
StudioTemplate,
|
||||
StudioTaskChain,
|
||||
StudioTaskChainStatus,
|
||||
StudioCreateReq,
|
||||
StudioModifyReq,
|
||||
StudioExtendReq,
|
||||
} from '../api/types'
|
||||
|
||||
/** 终态集合:succeeded(2)/failed(3)/timed_out(4)/canceled(5) → 停止轮询 */
|
||||
const TERMINAL: ReadonlySet<AigcTaskStatus> = new Set<AigcTaskStatus>([2, 3, 4, 5])
|
||||
/** 终态集合:完成(2)/失败(3) → 停止轮询(studio 任务链无 4/5) */
|
||||
const TERMINAL: ReadonlySet<StudioTaskChainStatus> = new Set<StudioTaskChainStatus>([2, 3])
|
||||
|
||||
/** 轮询间隔(毫秒)。mock 进度递增节奏与之匹配(见 mock/aigc)。 */
|
||||
/** 轮询间隔(毫秒)。mock 进度递增节奏与之匹配(见 mock/studio)。 */
|
||||
const POLL_INTERVAL = 1200
|
||||
|
||||
export const useCreateStore = defineStore('create', () => {
|
||||
/** 玩法模板列表 */
|
||||
const templates = ref<AigcTemplate[]>([])
|
||||
/** 当前生成任务(轮询对象) */
|
||||
const currentTask = ref<AigcTask | null>(null)
|
||||
/** 进度百分比(0-100,进度条用,冗余自 currentTask.progress 便于直接绑定) */
|
||||
/** 玩法模板列表(透传 aigc 模板;一句话直生可不选) */
|
||||
const templates = ref<StudioTemplate[]>([])
|
||||
/** 当前创作任务链(轮询对象) */
|
||||
const currentChain = ref<StudioTaskChain | null>(null)
|
||||
/** 进度百分比(0-100,进度条用,冗余自 currentChain.progress 便于直接绑定) */
|
||||
const progress = ref<number>(0)
|
||||
/** 是否轮询中 */
|
||||
const polling = ref<boolean>(false)
|
||||
@ -38,18 +52,18 @@ export const useCreateStore = defineStore('create', () => {
|
||||
|
||||
/** 加载玩法模板列表 */
|
||||
async function loadTemplates() {
|
||||
templates.value = await aigcApi.getTemplates()
|
||||
templates.value = await studioApi.getTemplates()
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取一次任务状态并更新 state;命中终态自动停轮询。
|
||||
* @param taskId 任务 ID
|
||||
* 拉取一次任务链状态并更新 state;命中终态自动停轮询。
|
||||
* @param id 任务链 ID
|
||||
*/
|
||||
async function fetchOnce(taskId: number) {
|
||||
const task = await aigcApi.getTask(taskId)
|
||||
currentTask.value = task
|
||||
progress.value = task.progress
|
||||
if (TERMINAL.has(task.status)) {
|
||||
async function fetchOnce(id: number) {
|
||||
const chain = await studioApi.getTaskChain(id)
|
||||
currentChain.value = chain
|
||||
progress.value = chain.progress
|
||||
if (TERMINAL.has(chain.status)) {
|
||||
stopPolling()
|
||||
}
|
||||
}
|
||||
@ -57,81 +71,87 @@ export const useCreateStore = defineStore('create', () => {
|
||||
/**
|
||||
* 启动轮询(提交后或进入进度页时调用)。
|
||||
* 先立刻拉一次(避免首屏空白),再定时轮询直至终态。
|
||||
* @param taskId 任务 ID
|
||||
* @param id 任务链 ID
|
||||
*/
|
||||
async function pollTask(taskId: number) {
|
||||
async function pollTask(id: number) {
|
||||
// 切换任务前先清理旧轮询
|
||||
stopPolling()
|
||||
polling.value = true
|
||||
await fetchOnce(taskId)
|
||||
await fetchOnce(id)
|
||||
// 已是终态则无需起定时器
|
||||
if (!polling.value) return
|
||||
timer = setInterval(() => {
|
||||
// 轮询内异常已被 axios 拦截器 Toast;这里吞掉 reject 防止未捕获
|
||||
void fetchOnce(taskId).catch(() => {})
|
||||
void fetchOnce(id).catch(() => {})
|
||||
}, POLL_INTERVAL)
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交生成任务并自动开始轮询,返回 taskId(供路由跳转 /create/task/:taskId)。
|
||||
* @param prompt 一句话 Prompt
|
||||
* @param templateId 玩法模板 ID(可选,缺省 generic:W-CLEAN 废玩法模板层后由后端归一)
|
||||
* @param extra 关联草稿信息(gameId/versionId)
|
||||
* 一步式创作(draft+generate 封装),自动开始轮询,返回任务链(含 id/gameId)。
|
||||
* @param req StudioCreateReq(title/prompt 必填,templateId 缺省 generic,assetContext 六类素材)
|
||||
*/
|
||||
async function generate(
|
||||
prompt: string,
|
||||
templateId?: string,
|
||||
extra?: { gameId?: number; versionId?: number },
|
||||
): Promise<number> {
|
||||
const resp = await aigcApi.generate(prompt, templateId, extra)
|
||||
progress.value = 0
|
||||
// 不阻塞返回 taskId:轮询在后台进行
|
||||
void pollTask(resp.taskId)
|
||||
return resp.taskId
|
||||
async function create(req: StudioCreateReq): Promise<StudioTaskChain> {
|
||||
const chain = await studioApi.createGame(req)
|
||||
currentChain.value = chain
|
||||
progress.value = chain.progress
|
||||
void pollTask(chain.id)
|
||||
return chain
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消当前/指定任务(仅 queued/running 可取消)。成功后停轮询。
|
||||
* @param taskId 任务 ID
|
||||
* 改源(产新预览版,不动 currentVersion),自动轮询,返回任务链。
|
||||
* @param req StudioModifyReq(baseVersionId + mode + target + payload)
|
||||
*/
|
||||
async function cancel(taskId: number): Promise<boolean> {
|
||||
const ok = await aigcApi.cancelTask(taskId)
|
||||
if (ok) {
|
||||
stopPolling()
|
||||
// 立即刷新一次拿到 canceled 终态
|
||||
await fetchOnce(taskId).catch(() => {})
|
||||
}
|
||||
return ok
|
||||
async function modify(req: StudioModifyReq): Promise<StudioTaskChain> {
|
||||
const chain = await studioApi.modifyGame(req)
|
||||
currentChain.value = chain
|
||||
progress.value = chain.progress
|
||||
void pollTask(chain.id)
|
||||
return chain
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试任务(仅 failed/timed_out 可重试,新建任务),返回新 taskId 并自动轮询。
|
||||
* @param taskId 原任务 ID
|
||||
* 内容扩展(additive),自动轮询,返回任务链。
|
||||
* @param req StudioExtendReq(baseVersionId + intent)
|
||||
*/
|
||||
async function retry(taskId: number): Promise<number> {
|
||||
const resp = await aigcApi.retryTask(taskId)
|
||||
progress.value = 0
|
||||
void pollTask(resp.taskId)
|
||||
return resp.taskId
|
||||
async function extend(req: StudioExtendReq): Promise<StudioTaskChain> {
|
||||
const chain = await studioApi.extendGame(req)
|
||||
currentChain.value = chain
|
||||
progress.value = chain.progress
|
||||
void pollTask(chain.id)
|
||||
return chain
|
||||
}
|
||||
|
||||
/**
|
||||
* 重生成(失败/不满意时重试,新建任务链,retryOf 指原链),返回新任务链并自动轮询。
|
||||
* @param id 原任务链 ID
|
||||
*/
|
||||
async function regenerate(id: number): Promise<StudioTaskChain> {
|
||||
const chain = await studioApi.regenerate(id)
|
||||
currentChain.value = chain
|
||||
progress.value = chain.progress
|
||||
void pollTask(chain.id)
|
||||
return chain
|
||||
}
|
||||
|
||||
/** 重置(离开创作流时调用,清理状态与计时器) */
|
||||
function reset() {
|
||||
stopPolling()
|
||||
currentTask.value = null
|
||||
currentChain.value = null
|
||||
progress.value = 0
|
||||
}
|
||||
|
||||
return {
|
||||
templates,
|
||||
currentTask,
|
||||
currentChain,
|
||||
progress,
|
||||
polling,
|
||||
loadTemplates,
|
||||
generate,
|
||||
create,
|
||||
modify,
|
||||
extend,
|
||||
pollTask,
|
||||
cancel,
|
||||
retry,
|
||||
regenerate,
|
||||
reset,
|
||||
stopPolling,
|
||||
}
|
||||
|
||||
@ -3,23 +3,20 @@
|
||||
* 创作入口页(路由 /create,spec §3.1 / §4.2 创作者闭环第 1 步)
|
||||
* ----------------------------------------------------------------------------
|
||||
* 职责:
|
||||
* 1. 拉取玩法模板列表(aigc.getTemplates,契约 GET /app-api/aigc/template/list,MVP 3-5 个)。
|
||||
* 1. 拉取玩法模板列表(studio.getTemplates,契约 GET /app-api/studio/template/list,MVP 3-5 个)。
|
||||
* 2. 选模板 + 一句话自然语言描述输入 + 示例 Prompt 提示(点击示例可一键填入)。
|
||||
* 3. 提交生成:先建草稿项目(project.create,契约 POST /app-api/project)拿 projectId,
|
||||
* 再投递生成任务(create.generate → 契约 POST /app-api/aigc/generate)拿 taskId,
|
||||
* 最后跳进度页 /create/task/:taskId(携带 gameId 便于终态后续链路)。
|
||||
* 3. 一步式创作:create.create → 契约 POST /app-api/studio/create(draft+generate 封装,一次调用拿任务链),
|
||||
* 最后跳进度页 /create/task/:taskChainId(携带任务链返回的 gameId 便于终态后续链路)。
|
||||
*
|
||||
* 契约纪律:
|
||||
* - 仅消费 useCreateStore / useProjectStore(地基 store),不直连后端、不发明端点。
|
||||
* - 生成请求体严格贴 AigcGenerateReqVO:prompt + templateId(+ gameId 关联草稿)。
|
||||
* - 创建草稿请求体严格贴 ProjectCreateReqVO:title + templateId(均必填)。
|
||||
* - 仅消费 useCreateStore(地基 store),不直连后端、不发明端点。
|
||||
* - 创作请求体严格贴 StudioCreateReqVO:title + prompt 必填(templateId 缺省 generic,assetContext 六类素材)。
|
||||
*/
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useCreateStore } from '../../store/create'
|
||||
import { useProjectStore } from '../../store/project'
|
||||
import type { AigcTemplate } from '../../api/types'
|
||||
import type { StudioTemplate } from '../../api/types'
|
||||
import { LoadingBar, EmptyState, AppButton } from '../../components'
|
||||
import Icon from '../../components/Icon.vue'
|
||||
|
||||
@ -28,7 +25,6 @@ const { t } = useI18n()
|
||||
|
||||
const router = useRouter()
|
||||
const createStore = useCreateStore()
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
/** 当前选中的模板 ID(默认空,需用户主动选) */
|
||||
const selectedTemplateId = ref<string>('')
|
||||
@ -38,14 +34,14 @@ const prompt = ref<string>('')
|
||||
const loadingTpl = ref<boolean>(false)
|
||||
/** 模板加载失败标记(用于空态重试) */
|
||||
const tplError = ref<boolean>(false)
|
||||
/** 提交中(建草稿 + 投递生成期间禁用按钮,防重复提交) */
|
||||
/** 提交中(一步式创作期间禁用按钮,防重复提交) */
|
||||
const submitting = ref<boolean>(false)
|
||||
|
||||
/** Prompt 最大长度(贴契约 AigcGenerateReqVO.prompt maxLength=1000) */
|
||||
/** Prompt 最大长度(贴契约 StudioCreateReqVO.prompt maxLength=1000) */
|
||||
const PROMPT_MAX = 1000
|
||||
|
||||
/** 当前选中的模板对象(用于取示例 Prompt / 作为草稿标题兜底) */
|
||||
const selectedTemplate = computed<AigcTemplate | undefined>(() =>
|
||||
const selectedTemplate = computed<StudioTemplate | undefined>(() =>
|
||||
createStore.templates.find((t) => t.templateId === selectedTemplateId.value),
|
||||
)
|
||||
|
||||
@ -97,34 +93,29 @@ function goFeed() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交生成(创作者闭环核心动作)。
|
||||
* 步骤:建草稿项目 → 拿 projectId 作为 gameId → 投递生成任务 → 跳进度页。
|
||||
* 失败路径:任一步异常 → 错误码 Toast(拦截器)+ 解除提交锁,停留本页可重试。
|
||||
* 一步式创作(创作者闭环核心动作)。
|
||||
* 步骤:studio.create(draft+generate 封装)→ 拿任务链(id/gameId) → 跳进度页。
|
||||
* 失败路径:异常 → 错误码 Toast(拦截器)+ 解除提交锁,停留本页可重试。
|
||||
*/
|
||||
async function onSubmit() {
|
||||
if (!canSubmit.value) return
|
||||
submitting.value = true
|
||||
try {
|
||||
// generic 桥接(W-CLEAN 废玩法模板层后无需选模板):缺省 templateId 传 'generic'(后端亦会归一),
|
||||
// generic 桥接(一句话直生无需选模板):缺省 templateId 传 'generic'(后端亦归一),
|
||||
// 兼容用户未选模板(template/list 返空、空态)与显式选中两种情况。
|
||||
const templateId = selectedTemplateId.value || 'generic'
|
||||
|
||||
// 1) 建草稿项目(title 取 Prompt 前 30 字兜底)
|
||||
// 契约 ProjectCreateReqVO required:[title](templateId 可选,缺省 generic)
|
||||
// title 取 Prompt 前 30 字兜底(契约 StudioCreateReqVO.title 必填 ≤128)。
|
||||
const title = prompt.value.trim().slice(0, 30) || selectedTemplate.value?.name || '未命名作品'
|
||||
const projectId = await projectStore.create({
|
||||
title,
|
||||
templateId,
|
||||
})
|
||||
|
||||
// 2) 投递生成任务,关联草稿(gameId=projectId);store 内部已自动开始轮询。
|
||||
// 契约 AigcGenerateReqVO:prompt(+ gameId 关联草稿;templateId 可选,缺省 generic)。
|
||||
const taskId = await createStore.generate(prompt.value.trim(), templateId, {
|
||||
gameId: projectId,
|
||||
})
|
||||
// 一步式创作:POST /app-api/studio/create → 任务链(含 id/gameId);store 内部已自动开始轮询。
|
||||
const chain = await createStore.create({ title, prompt: prompt.value.trim(), templateId })
|
||||
|
||||
// 3) 跳进度页(携带 gameId,供终态后预览/发布链路使用)
|
||||
router.push({ name: 'create-task', params: { taskId: String(taskId) }, query: { gameId: String(projectId) } })
|
||||
// 跳进度页(携带任务链返回的 gameId,供终态后预览/发布链路使用)。
|
||||
router.push({
|
||||
name: 'create-task',
|
||||
params: { taskId: String(chain.id) },
|
||||
query: chain.gameId != null ? { gameId: String(chain.gameId) } : {},
|
||||
})
|
||||
} catch {
|
||||
// 错误已由拦截器 Toast;解除锁,停留本页
|
||||
submitting.value = false
|
||||
|
||||
@ -1,25 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 生成进度页(路由 /create/task/:taskId,spec §3.1 / §4.2 创作者闭环第 2 步)
|
||||
* 生成进度页(路由 /create/task/:taskId,taskId 语义=taskChainId,spec §3.1 / §4.2 创作者闭环第 2 步)
|
||||
* ----------------------------------------------------------------------------
|
||||
* 职责:
|
||||
* 1. 进入即对该 taskId 启动轮询(create.pollTask → 契约 GET /app-api/aigc/task/{id}),
|
||||
* 1. 进入即对该 taskChainId 启动轮询(create.pollTask → 契约 GET /app-api/studio/task/{id}),
|
||||
* 用 LoadingBar 展示「步骤文案 + 百分比」,状态机驱动 UI。
|
||||
* 2. 终态 succeeded(2):自动跳生成后预览页 /create/preview/:versionId(携带 gameId)。
|
||||
* 3. 终态 failed(3)/timed_out(4):展示失败态 + 失败原因(FailureReason 映射可读提示)
|
||||
* + 重试按钮(create.retry → 契约 POST /app-api/aigc/task/{id}/retry,新建任务)。
|
||||
* 4. 进行中 queued(0)/running(1):提供取消按钮(create.cancel → 契约 POST .../cancel)。
|
||||
* 2. 终态 完成(2):自动跳生成后预览页 /create/preview/:versionId(携带 gameId)。
|
||||
* 3. 终态 失败(3):展示失败态 + 重生成按钮(create.regenerate → 契约 POST /app-api/studio/task/{id}/regenerate,新建任务链)。
|
||||
* 4. 进行中 初始(0)/生成中(1):展示进度(studio 契约无「取消」端点,故不提供取消)。
|
||||
*
|
||||
* 契约纪律:
|
||||
* - 仅消费 useCreateStore(轮询/取消/重试逻辑封装其中),不直连后端、不发明端点/字段。
|
||||
* - 状态机取值与 AigcTaskRespVO.status 一致:0 queued/1 running/2 succeeded/3 failed/4 timed_out/5 canceled。
|
||||
* - FailureReason 映射严格取契约 aigc.yaml FailureReason 七种枚举,不私造。
|
||||
* - 仅消费 useCreateStore(轮询/重生成逻辑封装其中),不直连后端、不发明端点/字段。
|
||||
* - 状态机取值与 StudioTaskChainRespVO.status 一致:0 初始 / 1 生成中 / 2 完成 / 3 失败。
|
||||
*/
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useCreateStore } from '../../store/create'
|
||||
import type { AigcTaskStatus, AigcFailureReason } from '../../api/types'
|
||||
import type { StudioTaskChainStatus } from '../../api/types'
|
||||
import { LoadingBar, AppButton, EmptyState } from '../../components'
|
||||
import Icon from '../../components/Icon.vue'
|
||||
import taskflowZh from '../../locales/taskflow.zh'
|
||||
@ -38,26 +36,22 @@ const { t } = useI18n({
|
||||
const router = useRouter()
|
||||
const createStore = useCreateStore()
|
||||
|
||||
/** 是否正在执行取消(防重复点击) */
|
||||
const cancelling = ref<boolean>(false)
|
||||
/** 是否正在执行重试(防重复点击) */
|
||||
const retrying = ref<boolean>(false)
|
||||
/** 是否正在执行重生成(防重复点击) */
|
||||
const regenerating = ref<boolean>(false)
|
||||
|
||||
/** 当前 taskId(数字化) */
|
||||
const taskId = computed(() => Number(props.taskId))
|
||||
/** 当前任务链 ID(数字化,=taskChainId) */
|
||||
const chainId = computed(() => Number(props.taskId))
|
||||
|
||||
/** 当前任务(来自 store;轮询持续刷新) */
|
||||
const task = computed(() => createStore.currentTask)
|
||||
/** 当前状态机值(无任务时视为 queued 占位) */
|
||||
const status = computed<AigcTaskStatus>(() => task.value?.status ?? 0)
|
||||
/** 当前任务链(来自 store;轮询持续刷新) */
|
||||
const chain = computed(() => createStore.currentChain)
|
||||
/** 当前状态机值(无任务链时视为初始(0)占位) */
|
||||
const status = computed<StudioTaskChainStatus>(() => chain.value?.status ?? 0)
|
||||
|
||||
/** 是否终态成功 */
|
||||
/** 是否终态完成 */
|
||||
const isSucceeded = computed(() => status.value === 2)
|
||||
/** 是否终态失败(failed/timed_out) */
|
||||
const isFailed = computed(() => status.value === 3 || status.value === 4)
|
||||
/** 是否已取消 */
|
||||
const isCanceled = computed(() => status.value === 5)
|
||||
/** 是否进行中(queued/running) */
|
||||
/** 是否终态失败 */
|
||||
const isFailed = computed(() => status.value === 3)
|
||||
/** 是否进行中(初始/生成中) */
|
||||
const isRunning = computed(() => status.value === 0 || status.value === 1)
|
||||
|
||||
/**
|
||||
@ -79,33 +73,13 @@ const stepText = computed(() => {
|
||||
return t('task.step.succeeded')
|
||||
case 3:
|
||||
return t('task.step.failed')
|
||||
case 4:
|
||||
return t('task.step.timedOut')
|
||||
case 5:
|
||||
return t('task.step.canceled')
|
||||
default:
|
||||
return t('task.step.fallback')
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 失败原因可读映射(严格对齐契约 aigc.yaml FailureReason 七种枚举)。
|
||||
* 仅用于把结构化错误码翻译成用户提示,不新增枚举值。
|
||||
* 注:放在 computed 内构建,使文案随语言切换实时重算(key 即契约枚举值,不可改)。
|
||||
*/
|
||||
const failureText = computed(() => {
|
||||
const FAILURE_TEXT: Record<AigcFailureReason, string> = {
|
||||
unsafe_prompt: t('task.failure.unsafePrompt'),
|
||||
intent_unclear: t('task.failure.intentUnclear'),
|
||||
no_template_match: t('task.failure.noTemplateMatch'),
|
||||
config_invalid: t('task.failure.configInvalid'),
|
||||
llm_error: t('task.failure.llmError'),
|
||||
timeout: t('task.failure.timeout'),
|
||||
asset_gen_failed: t('task.failure.assetGenFailed'),
|
||||
}
|
||||
const reason = task.value?.failureReason
|
||||
return (reason && FAILURE_TEXT[reason]) || t('task.failure.fallback')
|
||||
})
|
||||
/** 失败提示文案(studio 任务链无 failureReason 结构化字段,用通用兜底提示)。 */
|
||||
const failureText = computed(() => t('task.failure.fallback'))
|
||||
|
||||
/** 进度条颜色:成功绿 / 失败红 / 进行中琥珀(裸旧令牌改语义名,主题切换只动语义层) */
|
||||
const barColor = computed(() => {
|
||||
@ -120,10 +94,10 @@ const barColor = computed(() => {
|
||||
* 发布跳转 /project/:id/publish 也需 projectId=gameId)。
|
||||
*/
|
||||
watch(
|
||||
() => [isSucceeded.value, task.value?.versionId] as const,
|
||||
() => [isSucceeded.value, chain.value?.versionId] as const,
|
||||
([ok, versionId]) => {
|
||||
if (ok && versionId != null) {
|
||||
const gameId = task.value?.gameId
|
||||
const gameId = chain.value?.gameId
|
||||
router.replace({
|
||||
name: 'create-preview',
|
||||
params: { versionId: String(versionId) },
|
||||
@ -133,41 +107,26 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
/** 取消生成任务(仅进行中可取消;成功后任务转 canceled 终态) */
|
||||
async function onCancel() {
|
||||
if (cancelling.value) return
|
||||
cancelling.value = true
|
||||
try {
|
||||
await createStore.cancel(taskId.value)
|
||||
} catch {
|
||||
// 错误已由拦截器 Toast(如终态拒绝 1-101-002-001)
|
||||
} finally {
|
||||
cancelling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试生成(仅 failed/timed_out 可重试,新建任务并跳到新任务进度页)。
|
||||
* 携带原 gameId 关联草稿(从当前任务取)。
|
||||
* 重生成(失败/不满意时,新建任务链并跳到新链进度页)。
|
||||
*/
|
||||
async function onRetry() {
|
||||
if (retrying.value) return
|
||||
retrying.value = true
|
||||
async function onRegenerate() {
|
||||
if (regenerating.value) return
|
||||
regenerating.value = true
|
||||
try {
|
||||
const newTaskId = await createStore.retry(taskId.value)
|
||||
const gameId = task.value?.gameId
|
||||
// 跳到新任务进度页(replace 避免回退到旧失败任务)
|
||||
const chainNew = await createStore.regenerate(chainId.value)
|
||||
// 跳到新任务链进度页(replace 避免回退到旧失败链)
|
||||
router.replace({
|
||||
name: 'create-task',
|
||||
params: { taskId: String(newTaskId) },
|
||||
query: gameId != null ? { gameId: String(gameId) } : {},
|
||||
params: { taskId: String(chainNew.id) },
|
||||
query: chainNew.gameId != null ? { gameId: String(chainNew.gameId) } : {},
|
||||
})
|
||||
} catch {
|
||||
retrying.value = false
|
||||
regenerating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回创作入口(取消/失败后可重新创作) */
|
||||
/** 返回创作入口(失败后可重新创作) */
|
||||
function backToCreate() {
|
||||
router.replace({ name: 'create' })
|
||||
}
|
||||
@ -175,11 +134,11 @@ function backToCreate() {
|
||||
/**
|
||||
* 进入页面即启动轮询(pollTask 幂等:会先清理旧轮询再起新的)。
|
||||
* 覆盖两种入口:① 从 Create 提交跳入(store 已在轮询,这里重入也安全);
|
||||
* ② 直接 URL/刷新进入(currentTask 为空,需主动起轮询)。
|
||||
* ② 直接 URL/刷新进入(currentChain 为空,需主动起轮询)。
|
||||
*/
|
||||
onMounted(() => {
|
||||
if (Number.isFinite(taskId.value)) {
|
||||
void createStore.pollTask(taskId.value)
|
||||
if (Number.isFinite(chainId.value)) {
|
||||
void createStore.pollTask(chainId.value)
|
||||
}
|
||||
})
|
||||
|
||||
@ -191,7 +150,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<div class="task-page">
|
||||
<!-- 中部状态区:进行中 / 成功(瞬时,随即跳转)/ 失败 / 取消 -->
|
||||
<!-- 中部状态区:进行中 / 成功(瞬时,随即跳转)/ 失败 -->
|
||||
<div class="task-body">
|
||||
<!-- 进行中 + 成功瞬间:展示进度环境 -->
|
||||
<template v-if="isRunning || isSucceeded">
|
||||
@ -211,14 +170,10 @@ onBeforeUnmount(() => {
|
||||
<p class="task-hint">
|
||||
{{ isSucceeded ? t('task.hintSucceeded') : t('task.hintRunning') }}
|
||||
</p>
|
||||
|
||||
<!-- 进行中可取消 -->
|
||||
<div v-if="isRunning" class="task-actions">
|
||||
<AppButton type="ghost" :loading="cancelling" @click="onCancel">{{ t('task.cancelAction') }}</AppButton>
|
||||
</div>
|
||||
<!-- studio 契约无「取消」端点:进行中不提供取消,等生成完成/失败 -->
|
||||
</template>
|
||||
|
||||
<!-- 失败态:原因 + 重试 / 返回(失败图标用 SVG close 替原 ✕ emoji,满足 no-emoji-icons 红线) -->
|
||||
<!-- 失败态:提示 + 重新生成 / 返回(失败图标用 SVG close 替原 ✕ emoji,满足 no-emoji-icons 红线) -->
|
||||
<EmptyState
|
||||
v-else-if="isFailed"
|
||||
icon-name="close"
|
||||
@ -228,21 +183,7 @@ onBeforeUnmount(() => {
|
||||
<template #action>
|
||||
<div class="task-actions">
|
||||
<AppButton type="ghost" @click="backToCreate">{{ t('common.action.back') }}</AppButton>
|
||||
<AppButton type="primary" :loading="retrying" @click="onRetry">{{ t('task.retryAction') }}</AppButton>
|
||||
</div>
|
||||
</template>
|
||||
</EmptyState>
|
||||
|
||||
<!-- 已取消态:返回创作(取消图标用 SVG ban 替原 ○ emoji) -->
|
||||
<EmptyState
|
||||
v-else-if="isCanceled"
|
||||
icon-name="ban"
|
||||
:title="t('task.canceledTitle')"
|
||||
:description="t('task.canceledDesc')"
|
||||
>
|
||||
<template #action>
|
||||
<div class="task-actions">
|
||||
<AppButton type="primary" @click="backToCreate">{{ t('task.backToCreate') }}</AppButton>
|
||||
<AppButton type="primary" :loading="regenerating" @click="onRegenerate">{{ t('task.retryAction') }}</AppButton>
|
||||
</div>
|
||||
</template>
|
||||
</EmptyState>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user