Files
2026-11-13 11:46:25 +08:00

61 lines
4.3 KiB
Vue
Raw Permalink 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.
<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>