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

243 lines
8.5 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>
<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>