61 lines
4.3 KiB
Vue
61 lines
4.3 KiB
Vue
<template>
|
||
<section class="scene-lock-panel" aria-label="场景固定设定">
|
||
<div class="heading"><strong>场景固定设定</strong><span>{{ scene?.location || '未绑定场景' }}</span></div>
|
||
<p v-if="!scene" class="warning">请在参考素材中绑定场景后再生成。人物参考图不能代替场景背景。</p>
|
||
<template v-else>
|
||
<p>同一场景的所有分镜共用,生成时自动加入每条请求;换角度、特写也保留同一布景。</p>
|
||
<label>空间与固定陈设
|
||
<textarea v-model="draft" :disabled="saving" rows="4" maxlength="4000" placeholder="房间结构、门窗和墙面材质、家具与关键物件的相对位置。以场景为坐标描述,不固定所有机位的画面左右。" />
|
||
</label>
|
||
<p>时间:{{ scene.time || '未填写' }} · 光源:{{ scene.lighting || '未填写,请在场景资产中补充' }}</p>
|
||
<p v-if="scene.image_url || scene.imageUrl">已绑定场景图:普通参考模式自动携带;尾帧接续或高清重制使用对应视频帧。</p>
|
||
<p v-else class="warning">尚无场景参考图,目前仅用文字约束。上传或生成固定场景图可进一步减少背景漂移。</p>
|
||
<p v-if="info?.stale" class="warning">已有任务未使用当前设定。保存后用于下一次生成,已有视频保留。</p>
|
||
<p v-else-if="info?.used">最近任务已使用本场景设定{{ info.used.referenceMode === 'scene_image' ? '及场景参考图' : info.used.referenceMode === 'frame' ? '及视频参考帧' : '(文字约束)' }}。</p>
|
||
<p class="note">人物动作、机位与对白由分镜决定。剧情中移动道具或开关灯的变化,请写在镜头起止状态里。</p>
|
||
<div class="actions"><span>{{ dirty ? '有未保存的场景设定' : '已保存 · 同场景共用' }}</span><button class="btn btn-sm" :disabled="saving || !dirty" @click="save">{{ saving ? '保存中…' : '保存场景设定' }}</button></div>
|
||
</template>
|
||
<p v-if="error" class="warning" role="alert">{{ error }}</p>
|
||
</section>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { api, sceneAPI } from '~/composables/useApi'
|
||
const props = defineProps<{ shot: any; scene?: any }>()
|
||
const emit = defineEmits<{ saved: []; dirty: [value: boolean] }>()
|
||
const draft = ref(''), saved = ref(''), saving = ref(false), error = ref(''), info = ref<any>(null)
|
||
const dirty = computed(() => draft.value !== saved.value)
|
||
watch(dirty, value => emit('dirty', value))
|
||
async function load() {
|
||
const id = props.shot.id
|
||
try { const result = await api.get(`/storyboards/${id}/environment`); if (id === props.shot.id) info.value = result }
|
||
catch (e: any) { if (id === props.shot.id) error.value = e.message }
|
||
}
|
||
watch(() => [props.shot.id, props.scene?.id, props.scene?.continuity_prompt ?? props.scene?.continuityPrompt, props.scene?.prompt, props.scene?.lighting, props.scene?.image_url ?? props.scene?.imageUrl], () => {
|
||
if (dirty.value || saving.value) return
|
||
draft.value = props.scene?.continuity_prompt || props.scene?.continuityPrompt || props.scene?.prompt || ''
|
||
saved.value = draft.value; info.value = null; error.value = ''; void load()
|
||
}, { immediate: true })
|
||
async function save() {
|
||
if (!props.scene) return
|
||
const value = draft.value, id = props.scene.id
|
||
saving.value = true; error.value = ''
|
||
try {
|
||
await sceneAPI.update(id, { continuity_prompt: value })
|
||
saved.value = value; await load(); emit('saved')
|
||
} catch (e: any) { error.value = e.message }
|
||
finally { saving.value = false }
|
||
}
|
||
onBeforeUnmount(() => emit('dirty', false))
|
||
</script>
|
||
|
||
<style scoped>
|
||
.scene-lock-panel { display:flex; flex-direction:column; gap:10px; border:1px solid var(--border,#ddd); border-radius:14px; padding:16px; margin:12px 0; background:var(--bg-1,#fff); }
|
||
.heading,.actions { display:flex; align-items:center; justify-content:space-between; gap:8px; }
|
||
.heading span,.actions span,.note { color:var(--text-3,#777); font-size:12px; }
|
||
p { margin:0; font-size:12px; line-height:1.6; }
|
||
label { display:flex; flex-direction:column; gap:6px; font-size:12px; font-weight:600; }
|
||
textarea { width:100%; box-sizing:border-box; resize:vertical; border:1px solid var(--border,#ddd); border-radius:8px; background:var(--bg-0,#fff); color:var(--text-0,#222); padding:9px; font:inherit; font-weight:400; line-height:1.55; }
|
||
.warning { color:#a1490a; background:#fff3e8; padding:9px; border-radius:8px; }
|
||
</style>
|