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,174 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { api } from '../api/client'
import BaseButton from '../components/ui/BaseButton.vue'
import BaseCard from '../components/ui/BaseCard.vue'
import BaseInput from '../components/ui/BaseInput.vue'
import BaseSelect from '../components/ui/BaseSelect.vue'
import EmptyState from '../components/ui/EmptyState.vue'
import LoadingSpinner from '../components/ui/LoadingSpinner.vue'
import { ExclamationCircleIcon, UserGroupIcon } from '@heroicons/vue/24/outline'
interface ChildProfile {
id: string
name: string
avatar_url: string | null
birth_date: string | null
gender: string | null
age: number | null
interests: string[]
growth_themes: string[]
stories_count: number
total_reading_time: number
}
interface ProfileListResponse {
profiles: ChildProfile[]
total: number
}
const profiles = ref<ChildProfile[]>([])
const total = ref(0)
const loading = ref(true)
const error = ref('')
const form = ref({
name: '',
birth_date: '',
gender: '',
interests: '',
growth_themes: '',
})
function parseTags(input: string) {
return input
.split(/[,]/)
.map(tag => tag.trim())
.filter(Boolean)
}
async function fetchProfiles() {
loading.value = true
error.value = ''
try {
const data = await api.get<ProfileListResponse>('/api/profiles')
profiles.value = data.profiles
total.value = data.total
} catch (e) {
error.value = e instanceof Error ? e.message : '加载失败'
} finally {
loading.value = false
}
}
async function createProfile() {
if (!form.value.name.trim()) {
error.value = '请输入孩子姓名'
return
}
error.value = ''
try {
await api.post<ChildProfile>('/api/profiles', {
name: form.value.name.trim(),
birth_date: form.value.birth_date || undefined,
gender: form.value.gender || undefined,
interests: parseTags(form.value.interests),
growth_themes: parseTags(form.value.growth_themes),
})
form.value = {
name: '',
birth_date: '',
gender: '',
interests: '',
growth_themes: '',
}
await fetchProfiles()
} catch (e) {
error.value = e instanceof Error ? e.message : '创建失败'
}
}
onMounted(fetchProfiles)
</script>
<template>
<div class="max-w-5xl 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>
<div class="text-sm text-gray-500"> {{ total }} 个档案</div>
</div>
<BaseCard class="mb-8" padding="lg">
<h2 class="text-lg font-semibold text-gray-700 mb-4">创建新档案</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<BaseInput v-model="form.name" placeholder="孩子姓名" />
<BaseInput v-model="form.birth_date" type="date" />
<BaseSelect
v-model="form.gender"
:options="[
{ value: '', label: '性别(可选)' },
{ value: 'male', label: '男' },
{ value: 'female', label: '女' },
{ value: 'other', label: '其他' },
]"
/>
<BaseInput v-model="form.interests" placeholder="兴趣标签(逗号分隔)" />
<BaseInput v-model="form.growth_themes" placeholder="成长主题(逗号分隔)" class="md:col-span-2" />
</div>
<div class="mt-4 flex items-center justify-between">
<span v-if="error" class="text-sm text-red-500">{{ error }}</span>
<BaseButton @click="createProfile">创建档案</BaseButton>
</div>
</BaseCard>
<div v-if="loading" class="py-10">
<LoadingSpinner text="加载中..." />
</div>
<div v-else-if="error" class="py-10">
<EmptyState
:icon="ExclamationCircleIcon"
title="加载失败"
:description="error"
/>
</div>
<div v-else-if="profiles.length === 0" class="py-10">
<EmptyState
:icon="UserGroupIcon"
title="暂无档案"
description="创建你的第一个孩子档案"
/>
</div>
<div v-else class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<router-link
v-for="profile in profiles"
:key="profile.id"
:to="`/profiles/${profile.id}`"
class="block"
>
<BaseCard hover>
<div class="flex items-center space-x-3">
<div class="w-12 h-12 rounded-full bg-gradient-to-br from-purple-400 to-pink-400 text-white font-bold flex items-center justify-center">
{{ profile.name.charAt(0) }}
</div>
<div>
<div class="font-semibold text-gray-800">{{ profile.name }}</div>
<div class="text-sm text-gray-500">{{ profile.age ?? '未知' }} </div>
</div>
</div>
<div class="mt-4 text-sm text-gray-500">
兴趣{{ profile.interests.length ? profile.interests.join('、') : '未设置' }}
</div>
<div class="mt-1 text-sm text-gray-500">
成长主题{{ profile.growth_themes.length ? profile.growth_themes.join('、') : '未设置' }}
</div>
</BaseCard>
</router-link>
</div>
</div>
</template>