243 lines
8.5 KiB
Vue
243 lines
8.5 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">編輯人物:{{ 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>
|
||
|