ai-cut/app/components/feature/SceneEditor.vue

215 lines
6.8 KiB
Vue
Raw Normal View History

2025-12-16 10:08:51 +00:00
<template>
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" @click.self="handleClose">
<BaseCard variant="elevated" class="w-full max-w-2xl max-h-[90vh] overflow-y-auto m-4">
<template #header>
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-900">編輯場景{{ scene?.name }}</h2>
<button @click="handleClose" class="text-gray-500 hover:text-gray-700">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</template>
<div v-if="scene" class="space-y-6">
<!-- 基本資訊 -->
<div>
<h3 class="font-semibold text-gray-900 mb-4">基本資訊</h3>
<div class="space-y-4">
<BaseInput
v-model="editedScene.name"
label="場景名稱"
:required="true"
/>
<BaseTextarea
v-model="editedScene.description"
label="場景描述"
:rows="3"
/>
<BaseInput
v-model="editedScene.environment"
label="環境設定"
placeholder="例如:室內、室外、城市、森林"
/>
</div>
</div>
<!-- 場景設定 -->
<div>
<h3 class="font-semibold text-gray-900 mb-4">場景設定</h3>
<div class="space-y-4">
<BaseInput
v-model="editedScene.lighting"
label="光線設定"
placeholder="例如:自然光、人工光、昏暗"
/>
<div>
<label class="block mb-2 text-sm font-medium text-gray-700">道具</label>
<div class="flex flex-wrap gap-2 mb-2">
<span
v-for="(prop, index) in editedScene.props"
:key="index"
class="inline-flex items-center gap-1 px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm"
>
{{ prop }}
<button
@click="removeProp(index)"
class="text-green-600 hover:text-green-800"
>
×
</button>
</span>
</div>
<div class="flex gap-2">
<BaseInput
v-model="newProp"
placeholder="輸入道具名稱"
class="flex-1"
@keyup.enter="addProp"
/>
<BaseButton variant="outline" @click="addProp">新增</BaseButton>
</div>
</div>
</div>
</div>
<!-- 顏色設定 -->
<div>
<h3 class="font-semibold text-gray-900 mb-4">顏色設定</h3>
<div class="grid grid-cols-3 gap-4">
<div>
<label class="block mb-2 text-sm font-medium text-gray-700">主色</label>
<input
v-model="editedScene.colors.primary"
type="color"
class="w-full h-10 rounded-lg border border-gray-200"
/>
</div>
<div>
<label class="block mb-2 text-sm font-medium text-gray-700">次色</label>
<input
v-model="editedScene.colors.secondary"
type="color"
class="w-full h-10 rounded-lg border border-gray-200"
/>
</div>
<div>
<label class="block mb-2 text-sm font-medium text-gray-700">強調色</label>
<input
v-model="editedScene.colors.accent"
type="color"
class="w-full h-10 rounded-lg border border-gray-200"
/>
</div>
</div>
</div>
<!-- 場景圖片預覽 -->
<div>
<h3 class="font-semibold text-gray-900 mb-4">場景圖片</h3>
<div class="aspect-video bg-white border-2 border-gray-200 rounded-lg flex items-center justify-center mb-2">
<span v-if="!editedScene.image" class="text-gray-400">場景圖片</span>
<img v-else :src="editedScene.image" alt="場景" class="w-full h-full object-contain rounded-lg" />
</div>
<BaseButton
variant="outline"
@click="generateImage"
:disabled="isGenerating"
class="w-full"
>
{{ isGenerating ? '生成中...' : '生成場景圖片' }}
</BaseButton>
</div>
<!-- 操作按鈕 -->
<div class="flex justify-end gap-4 pt-4 border-t border-gray-200">
<BaseButton variant="outline" @click="handleClose">取消</BaseButton>
<BaseButton variant="primary" @click="handleSave">儲存</BaseButton>
</div>
</div>
</BaseCard>
</div>
</template>
<script setup lang="ts">
import type { SceneAsset } from '~/types/storyboard'
import { useAIImageGeneration } from '~/composables/useAIImageGeneration'
interface Props {
isOpen: boolean
scene: SceneAsset | null
}
const props = defineProps<Props>()
const emit = defineEmits<{
close: []
save: [scene: SceneAsset]
}>()
const { generateSceneImage: generateSceneImageUrl, isLoading: isGenerating } = useAIImageGeneration()
const editedScene = ref<SceneAsset | null>(null)
const newProp = ref('')
watch(() => props.scene, (newScene) => {
if (newScene) {
editedScene.value = {
...newScene,
props: [...(newScene.props || [])],
colors: {
primary: newScene.colors?.primary || '#FFFFFF',
secondary: newScene.colors?.secondary || '#CCCCCC',
accent: newScene.colors?.accent || '#000000'
}
}
}
}, { immediate: true })
function handleClose() {
emit('close')
}
function handleSave() {
if (editedScene.value) {
emit('save', editedScene.value)
handleClose()
}
}
function addProp() {
if (newProp.value.trim() && editedScene.value) {
if (!editedScene.value.props) {
editedScene.value.props = []
}
editedScene.value.props.push(newProp.value.trim())
newProp.value = ''
}
}
function removeProp(index: number) {
if (editedScene.value?.props) {
editedScene.value.props.splice(index, 1)
}
}
async function generateImage() {
if (!editedScene.value) return
try {
const style = '寫實風格' // 可以從 workflow 中取得
const imageUrl = await generateSceneImageUrl(
editedScene.value.name,
editedScene.value.description,
style
)
editedScene.value.image = imageUrl
} catch (err) {
console.error('生成場景圖片失敗:', err)
}
}
</script>