lili 6dbed41280 feat(studio): E6/U6 改进入口 Modify.vue——扩展/改玩法/调参 接 studio modify+extend
补齐 plan 002 U6 的 modify 入口(预览页可达,非孤儿):
- views/create/Modify.vue(新建,路由 /create/modify/:versionId,versionId=baseVersionId):三方式分段映射契约——
  扩展(extend,intent-only additive 零门槛 NL)/ 改玩法(modify regenerate-module,target.kind=behavior + intent,目标可空默认首个)/ 调参(modify deterministic,kind∈config/asset/level + path + value JSON,JSON 解析失败 Toast 不提交)
  提交→任务链→复用 /create/task 进度页→预览看改进结果
- views/create/Preview.vue:底部加「改进作品」按钮 → goModify 跳改进页(versionId 作 baseVersionId,带 gameId)
- router:注册 create-modify 路由
- locales/taskflow.{zh,en}:补 modify.* + preview.refine 文案(中英同构,禁裸中文)

验证:`npm run build`(vue-tsc -b + vite build)绿。全链 create→task→preview→modify CDP 真机走查下一步做(U6 execution note 必做项)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 15:37:23 -07:00

336 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
/**
* 改进作品页(路由 /create/modify/:versionIdplan 002 U6 modify 入口)
* ----------------------------------------------------------------------------
* 职责在某个产物版本baseVersionId=versionId上发起「改进」产新预览版后跳进度页。
* 三种改进方式映射 studio 契约:
* - 扩展(extend) → POST /app-api/studio/extendintent-onlyadditive 加内容,零门槛 NL
* - 改玩法(behavior) → POST /app-api/studio/modify mode=regenerate-moduletarget.kind=behavior + payload.intent
* - 调参(tweak) → POST /app-api/studio/modify mode=deterministictarget.kind∈config/asset/level + payload.value
*
* 契约纪律:
* - 仅消费 useCreateStoremodify/extend + 轮询封装其中),不直连后端、不发明端点/字段。
* - mode×target.kind 锁定映射由后端可信边界校验前端按方式构造合法请求deterministic 必带 value、regenerate-module 必带 intent
* - deterministic 须知源结构path/value属进阶零门槛主路径=扩展/改玩法NL
*/
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { showToast } from 'vant'
import { useCreateStore } from '../../store/create'
import type { StudioModifyKind } from '../../api/types'
import { AppButton } from '../../components'
import Icon from '../../components/Icon.vue'
import taskflowZh from '../../locales/taskflow.zh'
import taskflowEn from '../../locales/taskflow.en'
/** 路由 propspath 定义 props:trueversionId 即被改的 baseVersionId */
const props = defineProps<{ versionId: string }>()
const { t } = useI18n({
useScope: 'local',
messages: { 'zh-CN': taskflowZh, 'en-US': taskflowEn },
})
const router = useRouter()
const createStore = useCreateStore()
/** 被改的 base 产物版本 ID契约 modify/extend baseVersionId */
const baseVersionId = computed(() => Number(props.versionId))
/** 当前改进方式 tab */
type RefineTab = 'extend' | 'behavior' | 'tweak'
const tab = ref<RefineTab>('extend')
/** 扩展 / 改玩法 的自然语言意图 */
const intent = ref<string>('')
/** 改玩法的目标行为(可空,默认首个 behavior 路径) */
const targetInput = ref<string>('')
/** 调参目标类别config/asset/level */
const tweakKind = ref<Exclude<StudioModifyKind, 'behavior'>>('config')
/** 调参:源项目 JSON 指针路径 */
const tweakPath = ref<string>('')
/** 调参新值JSON 文本,提交时解析) */
const tweakValue = ref<string>('')
/** 提交中(防重复提交) */
const submitting = ref<boolean>(false)
const PROMPT_MAX = 1000
/** 调参的类别选项(排除 behaviorbehavior 走「改玩法」走 regenerate-module */
const TWEAK_KINDS: { kind: Exclude<StudioModifyKind, 'behavior'>; labelKey: string }[] = [
{ kind: 'config', labelKey: 'modify.kindConfig' },
{ kind: 'asset', labelKey: 'modify.kindAsset' },
{ kind: 'level', labelKey: 'modify.kindLevel' },
]
/** 是否可提交:按 tab 校验必填项 + 不在提交中 */
const canSubmit = computed(() => {
if (submitting.value) return false
if (tab.value === 'tweak') return tweakPath.value.trim().length > 0 && tweakValue.value.trim().length > 0
return intent.value.trim().length > 0 // extend / behavior 需意图
})
/** 切 tab清空与该 tab 无关的输入避免误带) */
function switchTab(next: RefineTab) {
tab.value = next
}
/** 返回(回上一页,通常是预览页) */
function goBack() {
router.back()
}
/**
* 提交改进:按 tab 构造合法请求 → 拿任务链 → 跳进度页(与 Create 同一进度/预览链路)。
* 失败路径:校验失败 Toast 不提交;请求异常由拦截器 Toast解除提交锁。
*/
async function onSubmit() {
if (!canSubmit.value) return
submitting.value = true
try {
let chain
if (tab.value === 'extend') {
// 扩展intent-only additive
chain = await createStore.extend({ baseVersionId: baseVersionId.value, intent: intent.value.trim() })
} else if (tab.value === 'behavior') {
// 改玩法regenerate-module + target.kind=behaviorpath 以 / 开头按路径,否则按 id空则默认首个 behavior 路径)
const raw = targetInput.value.trim()
const target = raw
? raw.startsWith('/')
? { kind: 'behavior' as const, path: raw }
: { kind: 'behavior' as const, id: raw }
: { kind: 'behavior' as const, path: '/gameDefinition/behaviors/0' }
chain = await createStore.modify({
baseVersionId: baseVersionId.value,
mode: 'regenerate-module',
target,
payload: { intent: intent.value.trim() },
})
} else {
// 调参deterministic + valueJSON 解析失败则 Toast 不提交)
let value: Record<string, unknown>
try {
value = JSON.parse(tweakValue.value)
} catch {
showToast(t('modify.valueInvalid'))
submitting.value = false
return
}
chain = await createStore.modify({
baseVersionId: baseVersionId.value,
mode: 'deterministic',
target: { kind: tweakKind.value, path: tweakPath.value.trim() },
payload: { value },
})
}
// 跳进度页(携带任务链返回的 gameId终态完成后进预览看改进结果
router.push({
name: 'create-task',
params: { taskId: String(chain.id) },
query: chain.gameId != null ? { gameId: String(chain.gameId) } : {},
})
} catch {
// 请求异常已由拦截器 Toast解除锁停留本页
submitting.value = false
}
}
</script>
<template>
<div class="modify-page">
<!-- 顶部状态条沉浸页无 TabBar -->
<header class="modify-head">
<button class="modify-back" type="button" @click="goBack" :aria-label="t('common.action.back')">
<Icon name="back" :size="22" />
</button>
<span class="modify-title">{{ t('modify.title') }}</span>
<span class="modify-base">{{ t('modify.baseLabel') }} #{{ baseVersionId }}</span>
</header>
<div class="modify-body">
<!-- 改进方式分段扩展 / 改玩法 / 调参 -->
<div class="seg">
<button class="seg-item" :class="{ 'is-on': tab === 'extend' }" type="button" @click="switchTab('extend')">{{ t('modify.tabExtend') }}</button>
<button class="seg-item" :class="{ 'is-on': tab === 'behavior' }" type="button" @click="switchTab('behavior')">{{ t('modify.tabBehavior') }}</button>
<button class="seg-item" :class="{ 'is-on': tab === 'tweak' }" type="button" @click="switchTab('tweak')">{{ t('modify.tabTweak') }}</button>
</div>
<!-- 扩展intent-only -->
<section v-if="tab === 'extend'" class="field">
<p class="field-hint">{{ t('modify.extendHint') }}</p>
<textarea v-model="intent" class="ta" :maxlength="PROMPT_MAX" rows="4" :placeholder="t('modify.extendPlaceholder')" />
</section>
<!-- 改玩法intent + 目标行为可空默认首个 -->
<section v-else-if="tab === 'behavior'" class="field">
<p class="field-hint">{{ t('modify.behaviorHint') }}</p>
<textarea v-model="intent" class="ta" :maxlength="PROMPT_MAX" rows="4" :placeholder="t('modify.behaviorPlaceholder')" />
<label class="field-label">{{ t('modify.targetLabel') }}</label>
<input v-model="targetInput" class="inp" type="text" :placeholder="t('modify.targetPlaceholder')" />
</section>
<!-- 调参kind + path + value(JSON) -->
<section v-else class="field">
<label class="field-label">{{ t('modify.tweakKindLabel') }}</label>
<div class="seg seg-sm">
<button
v-for="k in TWEAK_KINDS"
:key="k.kind"
class="seg-item"
:class="{ 'is-on': tweakKind === k.kind }"
type="button"
@click="tweakKind = k.kind"
>{{ t(k.labelKey) }}</button>
</div>
<label class="field-label">{{ t('modify.tweakPathLabel') }}</label>
<input v-model="tweakPath" class="inp" type="text" :placeholder="t('modify.tweakPathPlaceholder')" />
<label class="field-label">{{ t('modify.tweakValueLabel') }}</label>
<textarea v-model="tweakValue" class="ta ta-mono" rows="3" :placeholder="t('modify.tweakValuePlaceholder')" />
</section>
</div>
<!-- 底部提交 -->
<footer class="modify-foot">
<AppButton block size="large" :loading="submitting" :disabled="!canSubmit" @click="onSubmit">
{{ submitting ? t('modify.submitting') : t('modify.submit') }}
</AppButton>
</footer>
</div>
</template>
<style scoped>
/* D4 深色科技风token 来自全局 src/styles/tokens.css结构参照 Create.vue / Preview.vue */
.modify-page {
display: flex;
flex-direction: column;
height: 100%;
background: var(--bg);
color: var(--ink);
}
/* —— 顶部状态条 —— */
.modify-head {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 12px;
height: 48px;
padding: 0 12px;
background: var(--panel);
border-bottom: 1px solid var(--border);
}
.modify-back {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border: 0;
background: transparent;
color: var(--ink);
}
.modify-title {
flex: 1;
font-size: 15px;
font-weight: 600;
color: var(--ink);
}
.modify-base {
font-size: 12px;
padding: 3px 10px;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
color: var(--muted);
}
/* —— 主体 —— */
.modify-body {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 16px;
}
/* —— 分段控件 —— */
.seg {
display: flex;
gap: 4px;
padding: 4px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius-md);
}
.seg-sm {
margin-top: 8px;
}
.seg-item {
flex: 1;
padding: 8px 0;
border: 0;
background: transparent;
color: var(--muted);
font-size: 13px;
font-weight: 600;
border-radius: var(--radius-sm);
transition: background 0.15s ease, color 0.15s ease;
}
.seg-item.is-on {
color: var(--on-accent);
background: var(--accent);
}
/* —— 字段区 —— */
.field {
margin-top: 16px;
display: flex;
flex-direction: column;
}
.field-hint {
margin: 0 0 8px;
font-size: 13px;
color: var(--muted);
line-height: 1.5;
}
.field-label {
margin: 14px 0 6px;
font-size: 13px;
font-weight: 600;
color: var(--ink);
}
.ta,
.inp {
width: 100%;
border: 1px solid var(--border);
outline: 0;
background: var(--panel);
color: var(--ink);
font-size: 15px;
line-height: 1.6;
font-family: inherit;
border-radius: var(--radius-md);
padding: 12px;
}
.ta {
resize: none;
}
.ta-mono {
font-family: 'SF Mono', Menlo, Consolas, monospace;
font-size: 13px;
}
.ta::placeholder,
.inp::placeholder {
color: var(--muted);
opacity: 0.7;
}
/* —— 底部提交 —— */
.modify-foot {
flex: 0 0 auto;
padding: 12px 16px calc(12px + var(--safe-bottom));
background: var(--panel);
border-top: 1px solid var(--border);
}
</style>