feat: persist story generation states and cache audio
Some checks failed
Build and Push Docker Images / changes (push) Has been cancelled
Build and Push Docker Images / build-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (push) Has been cancelled
Build and Push Docker Images / build-admin-frontend (push) Has been cancelled
Some checks failed
Build and Push Docker Images / changes (push) Has been cancelled
Build and Push Docker Images / build-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (push) Has been cancelled
Build and Push Docker Images / build-admin-frontend (push) Has been cancelled
This commit is contained in:
@@ -1,19 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { api } from '../api/client'
|
||||
import CreateStoryModal from '../components/CreateStoryModal.vue'
|
||||
import BaseButton from '../components/ui/BaseButton.vue'
|
||||
import BaseCard from '../components/ui/BaseCard.vue'
|
||||
import LoadingSpinner from '../components/ui/LoadingSpinner.vue'
|
||||
import EmptyState from '../components/ui/EmptyState.vue'
|
||||
import CreateStoryModal from '../components/CreateStoryModal.vue'
|
||||
import LoadingSpinner from '../components/ui/LoadingSpinner.vue'
|
||||
import { getAssetStatusMeta, getGenerationStatusMeta } from '../utils/storyStatus'
|
||||
import {
|
||||
BookOpenIcon,
|
||||
ChevronRightIcon,
|
||||
ExclamationCircleIcon,
|
||||
PhotoIcon,
|
||||
SparklesIcon,
|
||||
PlusIcon,
|
||||
SparklesIcon,
|
||||
} from '@heroicons/vue/24/outline'
|
||||
|
||||
interface StoryItem {
|
||||
@@ -21,6 +22,11 @@ interface StoryItem {
|
||||
title: string
|
||||
image_url: string | null
|
||||
created_at: string
|
||||
mode: string
|
||||
generation_status: string
|
||||
image_status: string
|
||||
audio_status: string
|
||||
last_error: string | null
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
@@ -29,6 +35,16 @@ const loading = ref(true)
|
||||
const error = ref('')
|
||||
const showCreateModal = ref(false)
|
||||
|
||||
const completedCount = computed(() =>
|
||||
stories.value.filter((story) => story.generation_status === 'completed').length,
|
||||
)
|
||||
|
||||
const attentionCount = computed(() =>
|
||||
stories.value.filter((story) =>
|
||||
['degraded_completed', 'failed'].includes(story.generation_status),
|
||||
).length,
|
||||
)
|
||||
|
||||
async function fetchStories() {
|
||||
try {
|
||||
stories.value = await api.get<StoryItem[]>('/api/stories')
|
||||
@@ -60,8 +76,13 @@ function goToCreate() {
|
||||
showCreateModal.value = true
|
||||
}
|
||||
|
||||
function getStoryLink(story: StoryItem) {
|
||||
return story.mode === 'storybook' ? `/storybook/view/${story.id}` : `/story/${story.id}`
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchStories()
|
||||
void fetchStories()
|
||||
|
||||
if (router.currentRoute.value.query.openCreate) {
|
||||
showCreateModal.value = true
|
||||
router.replace({ query: { ...router.currentRoute.value.query, openCreate: undefined } })
|
||||
@@ -71,11 +92,10 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="max-w-6xl mx-auto px-4">
|
||||
<!-- 页面标题 -->
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold gradient-text mb-2">我的故事</h1>
|
||||
<p class="text-gray-500">收藏的所有童话故事</p>
|
||||
<p class="text-gray-500">回看每个作品的生成质量、资源状态和可优化点。</p>
|
||||
</div>
|
||||
<BaseButton @click="goToCreate">
|
||||
<SparklesIcon class="h-5 w-5 mr-2" />
|
||||
@@ -83,12 +103,10 @@ onMounted(() => {
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="py-20">
|
||||
<LoadingSpinner text="加载中..." />
|
||||
<LoadingSpinner text="正在加载内容..." />
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="error" class="py-10">
|
||||
<EmptyState
|
||||
:icon="ExclamationCircleIcon"
|
||||
@@ -97,54 +115,53 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else-if="stories.length === 0" class="py-10">
|
||||
<EmptyState
|
||||
:icon="BookOpenIcon"
|
||||
title="开始你的创作之旅"
|
||||
description="还没有创作任何故事,现在就开始为孩子创作第一个专属童话故事吧!"
|
||||
title="从第一个作品开始"
|
||||
description="现在还没有故事或绘本,先做一个能完整跑通的版本,后面再持续优化。"
|
||||
>
|
||||
<template #action>
|
||||
<BaseButton @click="goToCreate">
|
||||
<PlusIcon class="h-5 w-5 mr-2" />
|
||||
创作第一个故事
|
||||
创作第一个作品
|
||||
</BaseButton>
|
||||
</template>
|
||||
</EmptyState>
|
||||
</div>
|
||||
|
||||
<!-- 故事列表 -->
|
||||
<template v-else>
|
||||
<!-- 统计卡片 -->
|
||||
<BaseCard class="mb-8" padding="lg">
|
||||
<div class="flex items-center justify-around divide-x divide-gray-100">
|
||||
<div class="text-center px-4">
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div class="text-center px-4 py-2">
|
||||
<div class="text-3xl font-bold text-gray-800">{{ stories.length }}</div>
|
||||
<div class="text-gray-500 text-sm mt-1">故事总数</div>
|
||||
<div class="text-gray-500 text-sm mt-1">内容总数</div>
|
||||
</div>
|
||||
<div class="text-center px-4">
|
||||
<div class="text-center px-4 py-2">
|
||||
<div class="text-3xl font-bold text-gray-800">
|
||||
{{ stories.filter(s => s.image_url).length }}
|
||||
{{ stories.filter((story) => story.mode === 'storybook').length }}
|
||||
</div>
|
||||
<div class="text-gray-500 text-sm mt-1">已配图</div>
|
||||
<div class="text-gray-500 text-sm mt-1">绘本数量</div>
|
||||
</div>
|
||||
<div class="text-center px-4">
|
||||
<BookOpenIcon class="h-8 w-8 text-purple-500 mx-auto" />
|
||||
<div class="text-gray-500 text-sm mt-1">继续阅读</div>
|
||||
<div class="text-center px-4 py-2">
|
||||
<div class="text-3xl font-bold text-gray-800">{{ completedCount }}</div>
|
||||
<div class="text-gray-500 text-sm mt-1">完整可用</div>
|
||||
</div>
|
||||
<div class="text-center px-4 py-2">
|
||||
<div class="text-3xl font-bold text-gray-800">{{ attentionCount }}</div>
|
||||
<div class="text-gray-500 text-sm mt-1">待补资源</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
|
||||
<!-- 故事网格 -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<router-link
|
||||
v-for="story in stories"
|
||||
:key="story.id"
|
||||
:to="`/story/${story.id}`"
|
||||
:to="getStoryLink(story)"
|
||||
class="block group"
|
||||
>
|
||||
<BaseCard hover padding="none" class="h-full overflow-hidden flex flex-col">
|
||||
<!-- 封面图 -->
|
||||
<div class="relative aspect-[4/3] overflow-hidden bg-gray-100">
|
||||
<img
|
||||
v-if="story.image_url"
|
||||
@@ -159,23 +176,64 @@ onMounted(() => {
|
||||
<PhotoIcon class="h-12 w-12" />
|
||||
</div>
|
||||
|
||||
<!-- 悬停阅读提示 -->
|
||||
<div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
|
||||
<span class="inline-flex items-center gap-1 px-4 py-2 bg-white/90 text-gray-900 rounded-full font-medium shadow-lg backdrop-blur-sm transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300">
|
||||
阅读故事 <ChevronRightIcon class="h-4 w-4" />
|
||||
</span>
|
||||
<div class="absolute top-4 left-4 flex flex-wrap gap-2">
|
||||
<span
|
||||
class="px-2.5 py-1 rounded-full text-xs font-medium backdrop-blur-sm"
|
||||
:class="story.mode === 'storybook' ? 'bg-amber-100/90 text-amber-800' : 'bg-violet-100/90 text-violet-800'"
|
||||
>
|
||||
{{ story.mode === 'storybook' ? '绘本' : '故事' }}
|
||||
</span>
|
||||
<span
|
||||
class="px-2.5 py-1 rounded-full text-xs font-medium backdrop-blur-sm"
|
||||
:class="getGenerationStatusMeta(story.generation_status).badgeClass"
|
||||
>
|
||||
{{ getGenerationStatusMeta(story.generation_status).label }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="absolute inset-0 bg-black/35 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
|
||||
<span class="inline-flex items-center gap-1 px-4 py-2 bg-white/90 text-gray-900 rounded-full font-medium shadow-lg backdrop-blur-sm transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300">
|
||||
{{ story.mode === 'storybook' ? '阅读绘本' : '阅读故事' }}
|
||||
<ChevronRightIcon class="h-4 w-4" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 信息区 -->
|
||||
<div class="p-5 flex-1 flex flex-col">
|
||||
<h3 class="font-bold text-xl text-gray-800 mb-2 line-clamp-2 group-hover:text-purple-600 transition-colors">
|
||||
<h3 class="font-bold text-xl text-gray-800 mb-3 line-clamp-2 group-hover:text-purple-600 transition-colors">
|
||||
{{ story.title }}
|
||||
</h3>
|
||||
|
||||
<p class="text-sm text-gray-500 mb-4 leading-6">
|
||||
{{ getGenerationStatusMeta(story.generation_status).description }}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<span
|
||||
class="px-2 py-1 rounded-lg text-xs font-medium"
|
||||
:class="getAssetStatusMeta(story.image_status).badgeClass"
|
||||
>
|
||||
封面:{{ getAssetStatusMeta(story.image_status).label }}
|
||||
</span>
|
||||
<span
|
||||
class="px-2 py-1 rounded-lg text-xs font-medium"
|
||||
:class="getAssetStatusMeta(story.audio_status).badgeClass"
|
||||
>
|
||||
音频:{{ getAssetStatusMeta(story.audio_status).label }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="story.last_error"
|
||||
class="mb-4 px-3 py-2 rounded-xl bg-amber-50 text-amber-700 text-sm line-clamp-2"
|
||||
>
|
||||
{{ story.last_error }}
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex items-center justify-between text-sm text-gray-500">
|
||||
<span>{{ formatDate(story.created_at) }}</span>
|
||||
<span v-if="story.image_url" class="flex items-center gap-1 text-green-600 bg-green-50 px-2 py-0.5 rounded text-xs font-medium">
|
||||
已配图
|
||||
<span>
|
||||
{{ story.image_url ? '已有封面' : '待补封面' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user