215 lines
6.8 KiB
Vue
215 lines
6.8 KiB
Vue
|
|
<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>
|
|||
|
|
|