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

243 lines
8.5 KiB
Vue
Raw Permalink 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">編輯人物{{ character?.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="character" class="space-y-6">
<!-- 基本資訊 -->
<div>
<h3 class="font-semibold text-gray-900 mb-4">基本資訊</h3>
<div class="space-y-4">
<BaseInput
v-model="editedCharacter.name"
label="角色名稱"
:required="true"
/>
<BaseTextarea
v-model="editedCharacter.description"
label="角色描述"
:rows="3"
/>
<BaseInput
v-model="editedCharacter.role"
label="角色定位"
/>
</div>
</div>
<!-- 外觀設定 -->
<div>
<h3 class="font-semibold text-gray-900 mb-4">外觀設定</h3>
<div class="space-y-4">
<BaseInput
v-model="editedCharacter.clothing"
label="服裝"
placeholder="例如T恤、牛仔褲"
/>
<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="(accessory, index) in editedCharacter.accessories"
:key="index"
class="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm"
>
{{ accessory }}
<button
@click="removeAccessory(index)"
class="text-blue-600 hover:text-blue-800"
>
×
</button>
</span>
</div>
<div class="flex gap-2">
<BaseInput
v-model="newAccessory"
placeholder="輸入配件名稱"
class="flex-1"
@keyup.enter="addAccessory"
/>
<BaseButton variant="outline" @click="addAccessory">新增</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="editedCharacter.colors.hair"
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="editedCharacter.colors.skin"
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="editedCharacter.colors.clothing"
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="grid grid-cols-3 gap-4">
<div class="text-center">
<div class="aspect-square bg-white border-2 border-gray-200 rounded-lg flex items-center justify-center mb-2">
<span v-if="!editedCharacter.frontView" class="text-gray-400 text-sm">正面</span>
<img v-else :src="editedCharacter.frontView" alt="正面" class="w-full h-full object-contain rounded-lg" />
</div>
<BaseButton variant="outline" @click="generateView('front')" class="text-sm px-3 py-1 w-full">
生成
</BaseButton>
</div>
<div class="text-center">
<div class="aspect-square bg-white border-2 border-gray-200 rounded-lg flex items-center justify-center mb-2">
<span v-if="!editedCharacter.sideView" class="text-gray-400 text-sm">側面</span>
<img v-else :src="editedCharacter.sideView" alt="側面" class="w-full h-full object-contain rounded-lg" />
</div>
<BaseButton variant="outline" @click="generateView('side')" class="text-sm px-3 py-1 w-full">
生成
</BaseButton>
</div>
<div class="text-center">
<div class="aspect-square bg-white border-2 border-gray-200 rounded-lg flex items-center justify-center mb-2">
<span v-if="!editedCharacter.backView" class="text-gray-400 text-sm">背面</span>
<img v-else :src="editedCharacter.backView" alt="背面" class="w-full h-full object-contain rounded-lg" />
</div>
<BaseButton variant="outline" @click="generateView('back')" class="text-sm px-3 py-1 w-full">
生成
</BaseButton>
</div>
</div>
</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 { CharacterAsset } from '~/types/storyboard'
import { useAIImageGeneration } from '~/composables/useAIImageGeneration'
interface Props {
isOpen: boolean
character: CharacterAsset | null
}
const props = defineProps<Props>()
const emit = defineEmits<{
close: []
save: [character: CharacterAsset]
}>()
const { generateCharacterView: generateCharacterViewImage, isLoading: isGenerating } = useAIImageGeneration()
const editedCharacter = ref<CharacterAsset | null>(null)
const newAccessory = ref('')
const generatingView = ref<'front' | 'side' | 'back' | null>(null)
watch(() => props.character, (newChar) => {
if (newChar) {
editedCharacter.value = {
...newChar,
accessories: [...(newChar.accessories || [])],
colors: {
hair: newChar.colors?.hair || '#000000',
skin: newChar.colors?.skin || '#FFDBAC',
clothing: newChar.colors?.clothing || '#000000'
}
}
}
}, { immediate: true })
function handleClose() {
emit('close')
}
function handleSave() {
if (editedCharacter.value) {
emit('save', editedCharacter.value)
handleClose()
}
}
function addAccessory() {
if (newAccessory.value.trim() && editedCharacter.value) {
if (!editedCharacter.value.accessories) {
editedCharacter.value.accessories = []
}
editedCharacter.value.accessories.push(newAccessory.value.trim())
newAccessory.value = ''
}
}
function removeAccessory(index: number) {
if (editedCharacter.value?.accessories) {
editedCharacter.value.accessories.splice(index, 1)
}
}
async function generateView(view: 'front' | 'side' | 'back') {
if (!editedCharacter.value) return
generatingView.value = view
try {
const style = '寫實風格' // 可以從 workflow 中取得
const imageUrl = await generateCharacterViewImage(
editedCharacter.value.name,
editedCharacter.value.description,
view,
style
)
if (view === 'front') {
editedCharacter.value.frontView = imageUrl
} else if (view === 'side') {
editedCharacter.value.sideView = imageUrl
} else if (view === 'back') {
editedCharacter.value.backView = imageUrl
}
} catch (err) {
console.error('生成視圖失敗:', err)
} finally {
generatingView.value = null
}
}
</script>