Initial commit: clean project structure

- Backend: FastAPI + SQLAlchemy + Celery (Python 3.11+)
- Frontend: Vue 3 + TypeScript + Pinia + Tailwind
- Admin Frontend: separate Vue 3 app for management
- Docker Compose: 9 services orchestration
- Specs: design prototypes, memory system PRD, product roadmap

Cleanup performed:
- Removed temporary debug scripts from backend root
- Removed deprecated admin_app.py (embedded UI)
- Removed duplicate docs from admin-frontend
- Updated .gitignore for Vite cache and egg-info
This commit is contained in:
zhangtuo
2026-01-20 18:20:03 +08:00
commit e9d7f8832a
241 changed files with 33070 additions and 0 deletions

View File

@@ -0,0 +1,189 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { api } from '../api/client'
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 {
BookOpenIcon,
ChevronRightIcon,
ExclamationCircleIcon,
PhotoIcon,
SparklesIcon,
PlusIcon,
} from '@heroicons/vue/24/outline'
interface StoryItem {
id: number
title: string
image_url: string | null
created_at: string
}
const router = useRouter()
const stories = ref<StoryItem[]>([])
const loading = ref(true)
const error = ref('')
const showCreateModal = ref(false)
async function fetchStories() {
try {
stories.value = await api.get<StoryItem[]>('/api/stories')
} catch (e) {
error.value = e instanceof Error ? e.message : '加载失败'
} finally {
loading.value = false
}
}
function formatDate(dateStr: string) {
const date = new Date(dateStr)
const now = new Date()
const diff = now.getTime() - date.getTime()
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
if (days === 0) return '今天'
if (days === 1) return '昨天'
if (days < 7) return `${days} 天前`
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
}
function goToCreate() {
showCreateModal.value = true
}
onMounted(() => {
fetchStories()
if (router.currentRoute.value.query.openCreate) {
showCreateModal.value = true
router.replace({ query: { ...router.currentRoute.value.query, openCreate: undefined } })
}
})
</script>
<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>
</div>
<BaseButton @click="goToCreate">
<SparklesIcon class="h-5 w-5 mr-2" />
创作新故事
</BaseButton>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="py-20">
<LoadingSpinner text="加载中..." />
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="py-10">
<EmptyState
:icon="ExclamationCircleIcon"
title="加载失败"
:description="error"
/>
</div>
<!-- 空状态 -->
<div v-else-if="stories.length === 0" class="py-10">
<EmptyState
:icon="BookOpenIcon"
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="text-3xl font-bold text-gray-800">{{ stories.length }}</div>
<div class="text-gray-500 text-sm mt-1">故事总数</div>
</div>
<div class="text-center px-4">
<div class="text-3xl font-bold text-gray-800">
{{ stories.filter(s => s.image_url).length }}
</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>
</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}`"
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"
:src="story.image_url"
:alt="story.title"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
<div
v-else
class="w-full h-full flex items-center justify-center text-gray-300"
>
<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>
</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">
{{ story.title }}
</h3>
<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>
</div>
</div>
</BaseCard>
</router-link>
</div>
</template>
<CreateStoryModal v-model="showCreateModal" />
</div>
</template>