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:
parent
9b96433bc2
commit
50a089170d
102
game-studio/package-lock.json
generated
102
game-studio/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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…',
|
||||
|
||||
@ -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: '正在创建…',
|
||||
|
||||
@ -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 命中触发词时落该结构,
|
||||
* 任务先正常推进进度 0→99,到达 GEN_DURATION 后落「失败/超时」终态而非 succeeded。
|
||||
* - status:3 failed / 4 timed_out;
|
||||
* - reason:契约 FailureReason 枚举,供失败态原因映射回显。
|
||||
* 注:这是「轮询查到的终态失败」(CommonResult code:0、data.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 起始
|
||||
|
||||
/** 玩法模板(贴 TemplateRespVO;templateId 取契约示例集合) */
|
||||
/**
|
||||
* 失败注入触发词解析(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
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩法模板(贴 TemplateRespVO;P-CRT-11 与真实后端 U7 R-TPL 五品类逐字段对齐)。
|
||||
* 同源 = game-cloud AigcTaskServiceImpl.getTemplateList() + AigcTemplateConstants.SUPPORTED_TEMPLATE_IDS:
|
||||
* templateId/name/description/examplePrompt 取真实后端字面值,确保 mock 与真连后端行为一致
|
||||
* (避免「mock 一套示例、真连后端另一套」的割裂)。coverUrl 后端为占位 PNG,mock 用内联 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) + packageUrl(aigc 只读透传)
|
||||
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,
|
||||
|
||||
@ -49,6 +49,19 @@ const selectedTemplate = computed<AigcTemplate | undefined>(() =>
|
||||
createStore.templates.find((t) => t.templateId === selectedTemplateId.value),
|
||||
)
|
||||
|
||||
/**
|
||||
* 生效示例 Prompt(P-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
|
||||
}
|
||||
|
||||
/** 点击「示例」一键填入示例 Prompt(P-CRT-11 示例 Prompt 库) */
|
||||
/**
|
||||
* 点击「示例」一键填入示例 Prompt(P-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);
|
||||
}
|
||||
|
||||
/* —— 底部提交 —— */
|
||||
|
||||
@ -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「生成失败」)+ 失败原因详情。
|
||||
* 与页面失败态 EmptyState(title=stepText、desc=failureText)口径一致,Toast 一句话带全。
|
||||
*/
|
||||
const failNoticeText = computed(() => `${stepText.value}:${failureText.value}`)
|
||||
|
||||
/**
|
||||
* 已就地通知过失败的 taskId(P-CRT-13(b) 去重):
|
||||
* 轮询查到终态失败属「业务成功响应(CommonResult code:0、data.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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user