chore: retire demo technical debt
This commit is contained in:
@@ -7,9 +7,10 @@ import { useUserStore } from '../stores/user'
|
||||
import { useStorybookStore } from '../stores/storybook'
|
||||
import { api } from '../api/client'
|
||||
import BaseButton from './ui/BaseButton.vue'
|
||||
import BaseInput from './ui/BaseInput.vue'
|
||||
import BaseSelect from './ui/BaseSelect.vue'
|
||||
import BaseTextarea from './ui/BaseTextarea.vue'
|
||||
import BaseInput from './ui/BaseInput.vue'
|
||||
import BaseSelect from './ui/BaseSelect.vue'
|
||||
import BaseTextarea from './ui/BaseTextarea.vue'
|
||||
import AnalysisAnimation from './ui/AnalysisAnimation.vue'
|
||||
import {
|
||||
SparklesIcon,
|
||||
PencilSquareIcon,
|
||||
@@ -72,12 +73,37 @@ const themes: ThemeOption[] = [
|
||||
{ icon: HeartIcon, label: t('home.themeTolerance'), value: '包容' },
|
||||
]
|
||||
|
||||
const profileOptions = computed(() =>
|
||||
profiles.value.map(profile => ({ value: profile.id, label: profile.name })),
|
||||
)
|
||||
const universeOptions = computed(() =>
|
||||
universes.value.map(universe => ({ value: universe.id, label: universe.name })),
|
||||
)
|
||||
const profileOptions = computed(() =>
|
||||
profiles.value.map(profile => ({ value: profile.id, label: profile.name })),
|
||||
)
|
||||
const universeOptions = computed(() =>
|
||||
universes.value.map(universe => ({ value: universe.id, label: universe.name })),
|
||||
)
|
||||
const requestedOutputMode = computed<'story' | 'storybook'>(() =>
|
||||
inputType.value === 'full_story' ? 'story' : outputMode.value === 'storybook' ? 'storybook' : 'story',
|
||||
)
|
||||
const generationTitle = computed(() =>
|
||||
requestedOutputMode.value === 'storybook' ? '绘本排版中...' : '故事编织中...',
|
||||
)
|
||||
const generationSteps = computed(() => {
|
||||
if (requestedOutputMode.value === 'storybook') {
|
||||
return [
|
||||
'正在整理主题和成长目标...',
|
||||
'生成绘本分镜和每页文字...',
|
||||
'保存绘本主记录,确保刷新也能找回...',
|
||||
'补全封面和分页插图...',
|
||||
'马上进入可翻页阅读模式。',
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
'正在整理孩子档案和故事主题...',
|
||||
'生成可先阅读的故事正文...',
|
||||
'保存故事主记录,避免结果丢失...',
|
||||
'补全封面图,失败也可稍后重试...',
|
||||
'马上进入故事详情页。',
|
||||
]
|
||||
})
|
||||
|
||||
// Methods
|
||||
function close() {
|
||||
@@ -136,10 +162,8 @@ async function generateStory() {
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const requestedOutputMode =
|
||||
inputType.value === 'full_story' ? 'story' : outputMode.value === 'storybook' ? 'storybook' : 'story'
|
||||
const payload: Record<string, unknown> = {
|
||||
output_mode: requestedOutputMode,
|
||||
output_mode: requestedOutputMode.value,
|
||||
type: inputType.value,
|
||||
data: inputData.value,
|
||||
education_theme: educationTheme.value || undefined,
|
||||
@@ -149,7 +173,7 @@ async function generateStory() {
|
||||
if (selectedProfileId.value) payload.child_profile_id = selectedProfileId.value
|
||||
if (selectedUniverseId.value) payload.universe_id = selectedUniverseId.value
|
||||
|
||||
if (requestedOutputMode === 'storybook') {
|
||||
if (requestedOutputMode.value === 'storybook') {
|
||||
const response = await api.post<any>('/api/generations', payload)
|
||||
|
||||
storybookStore.setStorybook(response)
|
||||
@@ -187,14 +211,21 @@ async function generateStory() {
|
||||
v-if="modelValue"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
>
|
||||
<!-- 遮罩层 -->
|
||||
<div
|
||||
class="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
@click="close"
|
||||
></div>
|
||||
|
||||
<!-- 模态框内容 -->
|
||||
<div class="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto bg-[#1C2035] border border-gray-700/50 rounded-3xl shadow-2xl p-6 md:p-8">
|
||||
<!-- 遮罩层 -->
|
||||
<div
|
||||
class="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
@click="!loading && close()"
|
||||
></div>
|
||||
|
||||
<!-- 全屏加载动画 -->
|
||||
<AnalysisAnimation
|
||||
v-if="loading"
|
||||
:title="generationTitle"
|
||||
:steps="generationSteps"
|
||||
/>
|
||||
|
||||
<!-- 模态框内容 -->
|
||||
<div v-else class="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto bg-[#1C2035] border border-gray-700/50 rounded-3xl shadow-2xl p-6 md:p-8">
|
||||
<!-- 关闭按钮 -->
|
||||
<button
|
||||
@click="close"
|
||||
@@ -344,8 +375,19 @@ async function generateStory() {
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<BaseButton
|
||||
<!-- 提交按钮 -->
|
||||
<div class="mb-4 rounded-lg border border-amber-400/20 bg-amber-300/10 px-4 py-3 text-sm text-amber-100 leading-6">
|
||||
<div class="font-semibold mb-1">
|
||||
{{ requestedOutputMode === 'storybook' ? '绘本会先保存,再补全插图' : '故事会先可读,再补全封面' }}
|
||||
</div>
|
||||
<p>
|
||||
{{ requestedOutputMode === 'storybook'
|
||||
? '即使部分插图暂时失败,绘本文字也会保留在故事库,稍后可以继续补全。'
|
||||
: '封面或语音失败不会影响正文阅读,结果页会给出状态和重试入口。' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<BaseButton
|
||||
class="w-full"
|
||||
size="lg"
|
||||
:loading="loading"
|
||||
|
||||
141
admin-frontend/src/components/ui/AnalysisAnimation.vue
Normal file
141
admin-frontend/src/components/ui/AnalysisAnimation.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
title?: string
|
||||
steps?: string[]
|
||||
}>(), {
|
||||
title: '梦境编织中...',
|
||||
})
|
||||
|
||||
const defaultSteps = [
|
||||
'正在接收梦境信号...',
|
||||
'编织故事脉络...',
|
||||
'绘制精美插画 (需要一点点魔法时间)...',
|
||||
'撒上一些星光粉...',
|
||||
'即将完成独一无二的绘本!',
|
||||
]
|
||||
|
||||
const currentStepIndex = ref(0)
|
||||
const steps = computed(() => props.steps?.length ? props.steps : defaultSteps)
|
||||
let stepInterval: number | undefined
|
||||
|
||||
onMounted(() => {
|
||||
stepInterval = window.setInterval(() => {
|
||||
if (currentStepIndex.value < steps.value.length - 1) {
|
||||
currentStepIndex.value++
|
||||
}
|
||||
}, 2500)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (stepInterval) clearInterval(stepInterval)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed inset-0 z-[100] flex flex-col items-center justify-center bg-[#1C2035] overflow-hidden">
|
||||
<div class="absolute inset-0 overflow-hidden">
|
||||
<div v-for="i in 20" :key="i"
|
||||
class="absolute rounded-full bg-white animate-twinkle"
|
||||
:style="{
|
||||
top: `${Math.random() * 100}%`,
|
||||
left: `${Math.random() * 100}%`,
|
||||
width: `${Math.random() * 3 + 1}px`,
|
||||
height: `${Math.random() * 3 + 1}px`,
|
||||
animationDelay: `${Math.random() * 3}s`,
|
||||
opacity: Math.random() * 0.7 + 0.3
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="relative w-64 h-64 mb-12 flex items-center justify-center">
|
||||
<div class="absolute inset-0 border-4 border-amber-500/20 rounded-full animate-spin-slow"></div>
|
||||
<div class="absolute inset-2 border-2 border-amber-400/30 rounded-full animate-spin-reverse-slower"></div>
|
||||
|
||||
<div class="relative z-10 w-32 h-32 bg-gradient-to-br from-amber-300 to-orange-500 rounded-full shadow-[0_0_60px_rgba(245,158,11,0.5)] animate-pulse-glow flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-white animate-bounce-gentle" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.384-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="absolute inset-0 animate-spin-slow">
|
||||
<div class="absolute top-0 left-1/2 w-3 h-3 bg-amber-200 rounded-full shadow-lg blur-[1px]"></div>
|
||||
<div class="absolute bottom-10 right-10 w-2 h-2 bg-purple-300 rounded-full shadow-lg blur-[1px]"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="z-10 text-center space-y-4">
|
||||
<h3 class="text-3xl font-bold bg-gradient-to-r from-amber-200 to-orange-100 bg-clip-text text-transparent animate-gradient-x">
|
||||
{{ props.title }}
|
||||
</h3>
|
||||
<Transition mode="out-in" name="fade-slide">
|
||||
<p :key="currentStepIndex" class="text-stone-300 text-lg font-medium tracking-wide h-8">
|
||||
{{ steps[currentStepIndex] }}
|
||||
</p>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@keyframes spin-slow {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes spin-reverse-slower {
|
||||
from { transform: rotate(360deg); }
|
||||
to { transform: rotate(0deg); }
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% { transform: scale(1); box-shadow: 0 0 40px rgba(245,158,11,0.4); }
|
||||
50% { transform: scale(1.05); box-shadow: 0 0 70px rgba(245,158,11,0.7); }
|
||||
}
|
||||
|
||||
@keyframes bounce-gentle {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-5px); }
|
||||
}
|
||||
|
||||
@keyframes twinkle {
|
||||
0%, 100% { opacity: 0.3; transform: scale(0.8); }
|
||||
50% { opacity: 0.8; transform: scale(1.2); }
|
||||
}
|
||||
|
||||
.animate-spin-slow {
|
||||
animation: spin-slow 12s linear infinite;
|
||||
}
|
||||
|
||||
.animate-spin-reverse-slower {
|
||||
animation: spin-reverse-slower 20s linear infinite;
|
||||
}
|
||||
|
||||
.animate-pulse-glow {
|
||||
animation: pulse-glow 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-bounce-gentle {
|
||||
animation: bounce-gentle 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-twinkle {
|
||||
animation: twinkle 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.fade-slide-enter-active,
|
||||
.fade-slide-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.fade-slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
.fade-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user