feat(studio): U8 创作可用性 P-CRT-10引导/11示例/13就地失败可重试 + mock失败路径

- P-CRT-10 创作引导: 创作页新手引导卡(三步, 可收起) + Prompt 输入近场写法提示;
  示例库按钮始终可用(未选模板回落通用示例 genericExample); 升级中空态补「一句话造游戏」引导+示例预告
- P-CRT-11 示例库: Create.vue 消费真实 TemplateRespVO.examplePrompt, 强化「示例库」语义;
  mock TEMPLATES 对齐真实后端 U7 五品类(business-sim/narrative/puzzle/trpg/heritage)逐字段同源
- P-CRT-13 就地失败可重试: Task.vue 监听 failed/timed_out 终态就地 showToast(一次, taskId 幂等去重),
  区分 status=4 超时与 status=3 失败文案; 终态失败为 CommonResult code:0 不触发拦截器网络错误 Toast(单 Toast 不重复);
  mock 失败注入: Prompt 含 test_fail→终态 failed(llm_error), test_timeout→终态 timed_out(timeout), 重试不继承失败(失败→重试→成功)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-18 13:10:29 -07:00
parent 9b96433bc2
commit 50a089170d
6 changed files with 402 additions and 99 deletions

View File

@ -12,6 +12,7 @@
"pinia": "^3.0.4",
"vant": "^4.9.24",
"vue": "^3.5.34",
"vue-i18n": "^10.0.0",
"vue-router": "^4.6.4"
},
"devDependencies": {
@ -103,6 +104,50 @@
"tslib": "^2.4.0"
}
},
"node_modules/@intlify/core-base": {
"version": "10.0.8",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.8.tgz",
"integrity": "sha512-FoHslNWSoHjdUBLy35bpm9PV/0LVI/DSv9L6Km6J2ad8r/mm0VaGg06C40FqlE8u2ADcGUM60lyoU7Myo4WNZQ==",
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "10.0.8",
"@intlify/shared": "10.0.8"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/message-compiler": {
"version": "10.0.8",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.8.tgz",
"integrity": "sha512-DV+sYXIkHVd5yVb2mL7br/NEUwzUoLBsMkV3H0InefWgmYa34NLZUvMCGi5oWX+Hqr2Y2qUxnVrnOWF4aBlgWg==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "10.0.8",
"source-map-js": "^1.0.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/shared": {
"version": "10.0.8",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.8.tgz",
"integrity": "sha512-BcmHpb5bQyeVNrptC3UhzpBZB/YHHDoEREOUERrmF2BRxsyOEuRrq+Z96C/D4+2KJb8kuHiouzAei7BXlG0YYw==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@ -231,9 +276,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -251,9 +293,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@ -271,9 +310,6 @@
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -291,9 +327,6 @@
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -311,9 +344,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -331,9 +361,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@ -1215,9 +1242,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -1239,9 +1263,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -1263,9 +1284,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -1287,9 +1305,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -1747,6 +1762,33 @@
}
}
},
"node_modules/vue-i18n": {
"version": "10.0.8",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.8.tgz",
"integrity": "sha512-mIjy4utxMz9lMMo6G9vYePv7gUFt4ztOMhY9/4czDJxZ26xPeJ49MAGa9wBAE3XuXbYCrtVPmPxNjej7JJJkZQ==",
"deprecated": "v9 and v10 no longer supported. please migrate to v11. about maintenance status, see https://vue-i18n.intlify.dev/guide/maintenance.html",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "10.0.8",
"@intlify/shared": "10.0.8",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue-i18n/node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/vue-router": {
"version": "4.6.4",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",

View File

@ -7,6 +7,14 @@ export default {
// —— 顶部标题区 ——
title: 'One sentence, one game',
subtitle: 'Pick a template, describe your idea in one line, and AI builds it',
// —— 新手引导P-CRT-10——
guide: {
title: 'Three steps to your game',
step1: 'Pick a direction (or just write)',
step2: 'Describe your idea in one line',
step3: 'Tap “Generate” and preview in seconds',
collapseAria: 'Collapse onboarding guide',
},
// —— 模板选择区 ——
pickTemplate: 'Pick a template',
templateLoading: 'Loading templates…',
@ -15,14 +23,21 @@ export default {
loadFailedDesc: 'Network error or service not ready, please retry',
reload: 'Reload',
// 空(非故障):玩法模板体系升级中
upgradingTitle: 'Template system is upgrading',
upgradingDesc: 'The new one-sentence creator is coming soon — explore the feed first',
goFeed: 'Go to feed',
upgradingTitle: 'Just one sentence makes a game',
upgradingDesc:
'Templates are upgrading, but you can already describe an idea in one line and let AI build it. Try the example below.',
goFeed: 'Explore the feed',
// —— 一句话描述输入区 ——
describe: 'Describe in one line',
promptPlaceholder: 'e.g. an idle game where tapping earns coins, faster and faster',
promptPlaceholder: 'e.g. a game about running a bubble tea shop',
// Prompt 输入引导P-CRT-10
promptHelper: 'Say “what + goal/gameplay”. The more specific, the closer the result.',
// —— 示例 Prompt 库P-CRT-11——
exampleLabel: 'Examples',
// 示例 Prompt 一键填入前缀(后接模板示例文案)
useExample: 'Use example: ',
// 未选模板时的通用示例
genericExample: 'a game about running a bubble tea shop',
// —— 底部提交 ——
submit: 'Generate',
submitting: 'Creating…',

View File

@ -8,6 +8,16 @@ export default {
// —— 顶部标题区 ——
title: '一句话,做个小游戏',
subtitle: '选一个玩法模板用一句话描述你的创意AI 帮你生成',
// —— 新手引导P-CRT-10极简三步贴「一句话」简单基调不过度引导——
guide: {
// 引导卡标题(首屏新人可见,点右上角可收起)
title: '三步,做出你的小游戏',
step1: '选一个玩法方向(或直接写)',
step2: '一句话描述你的创意',
step3: '点「开始生成」,几秒后预览试玩',
// 收起/展开无障碍标签
collapseAria: '收起新手引导',
},
// —— 模板选择区 ——
pickTemplate: '选择玩法模板',
templateLoading: '模板加载中…',
@ -15,15 +25,21 @@ export default {
loadFailedTitle: '模板加载失败',
loadFailedDesc: '网络异常或服务未就绪,请重试',
reload: '重新加载',
// 空(非故障):玩法模板体系升级中
upgradingTitle: '玩法模板体系升级中',
upgradingDesc: '新版「一句话创作」即将上线,先去游戏流逛逛吧',
goFeed: '去游戏流',
// 空(非故障):玩法模板体系升级中P-CRT-10补「一句话造游戏」引导 + 示例预告,避免空态像故障)
upgradingTitle: '直接一句话,就能造游戏',
upgradingDesc: '玩法模板正在升级,但你现在就能用一句话描述创意、让 AI 生成。试试下面的示例。',
goFeed: '去游戏流逛逛',
// —— 一句话描述输入区 ——
describe: '用一句话描述',
promptPlaceholder: '例如:做一个点击就能赚金币、越点越快的放置小游戏',
// 示例 Prompt 一键填入前缀(后接模板示例文案)
promptPlaceholder: '例如:做一个经营奶茶店的小游戏',
// Prompt 输入引导P-CRT-10(b)placeholder 之外的近场提示,告诉新人怎么写更好)
promptHelper: '写清「做什么 + 目标/玩法」,描述越具体,生成越贴你的想法。',
// —— 示例 Prompt 库P-CRT-11强化「示例库」语义——
exampleLabel: '示例库',
// 选中模板时用该模板示例未选模板时用通用示例保证一键填示例随时可用P-CRT-10(c)
useExample: '用示例:',
// 未选模板时的通用示例(让「一键填示例」在没选模板前也能用)
genericExample: '做一个经营奶茶店的小游戏',
// —— 底部提交 ——
submit: '开始生成',
submitting: '正在创建…',

View File

@ -13,7 +13,13 @@
*/
import type { MockRoute, MockRequest } from './shared'
import { MockError, genTraceId, nowISO } from './shared'
import type { AigcTemplate, AigcTask, GenerateResp, AigcTaskStatus } from '../api/types'
import type {
AigcTemplate,
AigcTask,
GenerateResp,
AigcTaskStatus,
AigcFailureReason,
} from '../api/types'
/** demo 产物版本 ID与 feed/runtime mock 对齐:生成成功后预览试玩同一 demo 包) */
const DEMO_VERSION_ID = 9001
@ -31,48 +37,74 @@ interface MockTask {
traceId: string
/** 手动终态覆盖(取消时设 canceled为空则按时间推进 */
forcedStatus?: AigcTaskStatus
/**
* P-CRT-13 Prompt
* 099 GEN_DURATION / succeeded
* - status3 failed / 4 timed_out
* - reason FailureReason
* CommonResult code:0data.status=3/4
* Toast axios Toast
*/
forcedFail?: { status: 3 | 4; reason: AigcFailureReason }
retryOf?: number
createTime: string
}
const tasks = new Map<number, MockTask>()
let taskSeq = 7000 // taskId 起始
/** 玩法模板(贴 TemplateRespVOtemplateId 取契约示例集合) */
/**
* P-CRT-13
* mock Prompt `test_fail` failed(3, llm_error) `test_timeout` timed_out(4, timeout)
* VITE_API_BASE mock undefined
*/
function parseForcedFail(prompt: string): MockTask['forcedFail'] {
const p = prompt.toLowerCase()
if (p.includes('test_timeout')) return { status: 4, reason: 'timeout' }
if (p.includes('test_fail')) return { status: 3, reason: 'llm_error' }
return undefined
}
/**
* TemplateRespVOP-CRT-11 U7 R-TPL
* = game-cloud AigcTaskServiceImpl.getTemplateList() + AigcTemplateConstants.SUPPORTED_TEMPLATE_IDS
* templateId/name/description/examplePrompt mock
* mock coverUrl PNGmock SVG 便
*/
const TEMPLATES: AigcTemplate[] = [
{
templateId: 'clicker',
name: '点击放置',
description: '点击累积数值的放置类玩法,适合“一句话”快速出成品',
coverUrl: tplCover('clicker', '#31e6ff'),
examplePrompt: '做一个点击就能赚金币、越点越快的放置小游戏',
templateId: 'business-sim',
name: '经营模拟',
description: '围绕资源、生产、收益与再投入的经济闭环,把摊位/小店经营到目标即通关。',
coverUrl: tplCover('经营模拟', '#31e6ff'),
examplePrompt: '做一个经营奶茶店的小游戏',
},
{
templateId: 'dodge',
name: '躲避闪避',
description: '控制角色躲避来袭障碍,考验反应',
coverUrl: tplCover('dodge', '#ff669d'),
examplePrompt: '一个躲避陨石的太空船小游戏,越往后越快',
},
{
templateId: 'runner',
name: '跑酷',
description: '横版自动奔跑 + 跳跃躲障碍',
coverUrl: tplCover('runner', '#7cf2a4'),
examplePrompt: '霓虹城市里的跑酷,跳过障碍吃金币',
templateId: 'narrative',
name: '剧情互动',
description: '以文本场景加分支选择推进的互动叙事,不同选择导向不同结局。',
coverUrl: tplCover('剧情互动', '#ff669d'),
examplePrompt: '做一个校园悬疑的剧情选择小游戏',
},
{
templateId: 'puzzle',
name: '消除益智',
description: '三消/连连看类益智玩法',
coverUrl: tplCover('puzzle', '#9d7cff'),
examplePrompt: '糖果主题的三消游戏,过关解锁新糖果',
name: '解谜闯关',
description: '一关一谜、有确定解的智力挑战,解出当前关推进到下一关、难度递增。',
coverUrl: tplCover('解谜闯关', '#9d7cff'),
examplePrompt: '做一个连线配对的解谜闯关小游戏',
},
{
templateId: 'shooter',
name: '射击',
description: '弹幕/射击类玩法',
coverUrl: tplCover('shooter', '#ffc857'),
examplePrompt: '飞机大战,打 boss 掉装备',
templateId: 'trpg',
name: 'TRPG',
description: '带角色属性与骰子检定的桌面角色扮演,掷骰加属性决定剧情成败分支。',
coverUrl: tplCover('TRPG', '#7cf2a4'),
examplePrompt: '做一个勇者闯地下城的跑团小游戏',
},
{
templateId: 'heritage',
name: '非遗科普',
description: '围绕一项非物质文化遗产寓教于乐,边玩边学传统文化知识点。',
coverUrl: tplCover('非遗科普', '#ffc857'),
examplePrompt: '做一个学剪纸的非遗科普小游戏',
},
]
@ -123,6 +155,25 @@ function projectTask(t: MockTask): AigcTask {
createTime: t.createTime,
}
}
// 终态失败注入P-CRT-13触发词命中时落 failed(3)/timed_out(4) 终态 + 失败原因,
// 先走完上面的进度推进、再翻失败终态完整演练「进行中→就地失败可重试」UI。
if (t.forcedFail) {
return {
id: t.id,
prompt: t.prompt,
templateId: t.templateId,
gameId: t.gameId,
versionId: t.versionId,
status: t.forcedFail.status,
// 失败终态进度回 0与取消终态一致语义
progress: 0,
failureReason: t.forcedFail.reason,
traceId: t.traceId,
retryOf: t.retryOf,
createTime: t.createTime,
finishTime: nowISO(),
}
}
// 终态succeeded回填产物 versionId + 生成质量分(0-1) + packageUrlaigc 只读透传)
return {
id: t.id,
@ -163,11 +214,14 @@ function generateHandler(req: MockRequest): GenerateResp {
tasks.set(id, {
id,
prompt: body.prompt,
templateId: body.templateId || 'clicker',
// 缺省 templateId 归一为 generic对齐真实后端 submitGenerate 行为,避免 mock 用旧模板 id
templateId: body.templateId || 'generic',
gameId: body.gameId,
versionId: body.versionId,
startMs: Date.now(),
traceId,
// P-CRT-13据 Prompt 触发词注入终态失败/超时test_fail / test_timeout
forcedFail: parseForcedFail(body.prompt),
createTime: nowISO(),
})
return { taskId: id, status: 0, traceId }
@ -208,6 +262,8 @@ function retryHandler(req: MockRequest): GenerateResp {
}
const newId = ++taskSeq
const traceId = genTraceId('gen')
// 重试新建任务「不」继承 forcedFail演练「失败→重试→成功」闭环否则会无限失败CDP 门走不通)。
// 想再演练失败请重新从创作页用触发词提交,而非靠重试。
tasks.set(newId, {
id: newId,
prompt: t.prompt,

View File

@ -49,6 +49,19 @@ const selectedTemplate = computed<AigcTemplate | undefined>(() =>
createStore.templates.find((t) => t.templateId === selectedTemplateId.value),
)
/**
* 生效示例 PromptP-CRT-10(c) + P-CRT-11
* 优先取选中模板的 examplePrompt真实来自 TemplateRespVO.examplePrompt
* 未选模板 template/list 返空的空态时回落通用示例 create.genericExample
* 保证一键填示例在任何时刻都可用不因没选模板而消失
*/
const effectiveExample = computed<string>(
() => selectedTemplate.value?.examplePrompt || t('create.genericExample'),
)
/** 新手引导卡是否展开P-CRT-10(a);默认展开,用户可收起,会话内记忆) */
const guideOpen = ref<boolean>(true)
/**
* 是否可提交Prompt 非空 + 不在提交中
* generic 桥接W-CLEAN 废玩法模板层后一句话创作无需选模板去掉必须选中模板条件
@ -81,9 +94,12 @@ function selectTemplate(id: string) {
selectedTemplateId.value = id
}
/** 点击「示例」一键填入示例 PromptP-CRT-11 示例 Prompt 库) */
/**
* 点击示例一键填入示例 PromptP-CRT-11 示例 Prompt + P-CRT-10(c)
* effectiveExample选中模板示例 通用示例兜底故未选模板时也能一键填入
*/
function useExample() {
const ex = selectedTemplate.value?.examplePrompt
const ex = effectiveExample.value
if (ex) prompt.value = ex
}
@ -143,6 +159,29 @@ onMounted(loadTemplates)
<p class="create-sub">{{ t('create.subtitle') }}</p>
</header>
<!-- 新手引导卡P-CRT-10(a)极简三步可收起一句话简单基调不过度引导 -->
<section v-if="guideOpen" class="create-guide">
<div class="guide-head">
<span class="guide-title">
<Icon name="spark" :size="14" />
{{ t('create.guide.title') }}
</span>
<button
class="guide-collapse"
type="button"
:aria-label="t('create.guide.collapseAria')"
@click="guideOpen = false"
>
<Icon name="close" :size="14" />
</button>
</div>
<ol class="guide-steps">
<li><span class="guide-step-no">1</span>{{ t('create.guide.step1') }}</li>
<li><span class="guide-step-no">2</span>{{ t('create.guide.step2') }}</li>
<li><span class="guide-step-no">3</span>{{ t('create.guide.step3') }}</li>
</ol>
</section>
<!-- 模板选择区 -->
<section class="create-section">
<div class="section-label">
@ -214,19 +253,23 @@ onMounted(loadTemplates)
:placeholder="t('create.promptPlaceholder')"
/>
<div class="prompt-foot">
<!-- 示例 Prompt 提示点击一键填入当前模板示例 -->
<button
v-if="selectedTemplate?.examplePrompt"
class="prompt-example"
type="button"
@click="useExample"
>
<Icon name="spark" :size="14" />
<span class="prompt-example-text">{{ t('create.useExample') }}{{ selectedTemplate.examplePrompt }}</span>
</button>
<!-- 输入引导P-CRT-10(b)placeholder 之外的近场写法提示 -->
<p class="prompt-helper">{{ t('create.promptHelper') }}</p>
<span class="prompt-count">{{ prompt.length }}/{{ PROMPT_MAX }}</span>
</div>
</div>
<!-- 示例 Prompt P-CRT-11 + P-CRT-10(c)始终可用
effectiveExample选中模板示例 通用示例兜底点击一键填入没选模板也能用 -->
<div class="example-lib">
<span class="example-lib-label">
<Icon name="spark" :size="13" />
{{ t('create.exampleLabel') }}
</span>
<button class="example-chip" type="button" @click="useExample">
<span class="example-chip-text">{{ t('create.useExample') }}{{ effectiveExample }}</span>
</button>
</div>
</section>
<!-- 底部提交固定底部主操作 -->
@ -271,6 +314,70 @@ onMounted(loadTemplates)
line-height: 1.5;
}
/* —— 新手引导卡P-CRT-10—— */
.create-guide {
margin-top: 14px;
padding: 12px 14px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius-md);
}
.guide-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.guide-title {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 600;
color: var(--accent);
}
.guide-collapse {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border: 0;
background: transparent;
color: var(--muted);
border-radius: var(--radius-sm);
}
.guide-steps {
margin: 10px 0 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 8px;
}
.guide-steps li {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--ink);
line-height: 1.4;
}
.guide-step-no {
flex: 0 0 auto;
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
color: var(--on-accent);
background: var(--accent);
/* 统一圆角矩形(禁正圆) */
border-radius: var(--radius-sm);
}
/* —— 区块通用 —— */
.create-section {
margin-top: 20px;
@ -400,36 +507,72 @@ onMounted(loadTemplates)
}
.prompt-foot {
display: flex;
align-items: center;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-top: 8px;
}
.prompt-example {
/* 输入引导文案P-CRT-10(b):近场写法提示,弱化但可读) */
.prompt-helper {
flex: 1;
min-width: 0;
display: inline-flex;
align-items: center;
gap: 4px;
border: 0;
background: transparent;
/* 警示/进行中语义色(原 --amber → --warning */
color: var(--warning);
margin: 0;
font-size: 12px;
text-align: left;
padding: 0;
}
/* 示例文案单行省略(图标不参与省略,故省略改挂内层文本) */
.prompt-example-text {
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--muted);
line-height: 1.5;
}
.prompt-count {
flex: 0 0 auto;
font-size: 12px;
color: var(--muted);
/* 与 helper 首行对齐 */
line-height: 1.5;
}
/* —— 示例 Prompt 库P-CRT-11 + P-CRT-10(c))—— */
.example-lib {
display: flex;
align-items: center;
gap: 10px;
margin-top: 12px;
}
.example-lib-label {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
font-weight: 600;
color: var(--muted);
}
.example-chip {
flex: 1;
min-width: 0;
display: inline-flex;
align-items: center;
padding: 7px 12px;
background: var(--panel);
border: 1px solid var(--border);
/* 圆角矩形(禁药丸/正圆),统一 --radius-sm */
border-radius: var(--radius-sm);
text-align: left;
transition: border-color 0.15s ease, transform 0.05s ease;
}
.example-chip:active {
transform: scale(0.99);
}
.example-chip:hover {
border-color: var(--accent);
}
/* 示例文案单行省略(点中可一键填入完整文案) */
.example-chip-text {
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 12px;
/* 警示/进行中语义色,呼应「点我试试」的引导色 */
color: var(--warning);
}
/* —— 底部提交 —— */

View File

@ -18,6 +18,7 @@
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { showToast } from 'vant'
import { useCreateStore } from '../../store/create'
import type { AigcTaskStatus, AigcFailureReason } from '../../api/types'
import { LoadingBar, AppButton, EmptyState } from '../../components'
@ -114,6 +115,36 @@ const barColor = computed(() => {
return 'var(--warning)'
})
/**
* 就地失败通知文案P-CRT-13(c)区分超时与一般失败
* = 状态级标题status 4生成超时/ status 3生成失败+ 失败原因详情
* 与页面失败态 EmptyStatetitle=stepTextdesc=failureText口径一致Toast 一句话带全
*/
const failNoticeText = computed(() => `${stepText.value}${failureText.value}`)
/**
* 已就地通知过失败的 taskIdP-CRT-13(b) 去重
* 轮询查到终态失败属业务成功响应CommonResult code:0data.status=3/4
* 不会触发 axios 拦截器的网络错误 Toast本页就地 Toast 是该失败的唯一提示
* taskId 作幂等键确保同一任务的失败只 Toast 一次 watcher 重入/computed 重算重复弹
*/
const notifiedFailTaskId = ref<number | null>(null)
/**
* 监听终态失败failed/timed_out 就地 Toast 一次P-CRT-13(a)(b)(c)
* 重试按钮始终在失败态 EmptyState #action 槽内可见无需此处额外处理
*/
watch(
() => [isFailed.value, task.value?.id] as const,
([failed, id]) => {
if (failed && id != null && notifiedFailTaskId.value !== id) {
notifiedFailTaskId.value = id
showToast({ message: failNoticeText.value, type: 'fail' })
}
},
{ immediate: true },
)
/**
* 监听终态成功 自动跳预览页
* 预览路由仅有 :versionId故把 gameId 通过 query 一并带上GamePlayer gameId