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,274 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
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 MemoryList, { type MemoryItem } from '../components/MemoryList.vue'
import AddMemoryModal from '../components/AddMemoryModal.vue'
import { ExclamationCircleIcon, UserCircleIcon, SparklesIcon, PlusIcon } 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 MemoryListResponse {
memories: MemoryItem[]
total: number
}
const route = useRoute()
const router = useRouter()
const profile = ref<ChildProfile | null>(null)
const loading = ref(true)
const error = ref('')
// 记忆相关状态
const memories = ref<MemoryItem[]>([])
const loadingMemories = ref(true)
const showAddMemoryModal = ref(false)
const form = ref({
name: '',
birth_date: '',
gender: '',
interests: '',
growth_themes: '',
})
function parseTags(input: string) {
return input
.split(/[,]/)
.map(tag => tag.trim())
.filter(Boolean)
}
function fillForm(data: ChildProfile) {
form.value = {
name: data.name,
birth_date: data.birth_date || '',
gender: data.gender || '',
interests: data.interests.join('、'),
growth_themes: data.growth_themes.join('、'),
}
}
async function fetchProfile() {
loading.value = true
error.value = ''
try {
const data = await api.get<ChildProfile>(`/api/profiles/${route.params.id}`)
profile.value = data
fillForm(data)
} catch (e) {
error.value = e instanceof Error ? e.message : '加载失败'
} finally {
loading.value = false
}
}
async function fetchMemories() {
loadingMemories.value = true
try {
const data = await api.get<MemoryListResponse>(`/api/profiles/${route.params.id}/memories`)
memories.value = data.memories
} catch (e) {
console.error('获取记忆失败:', e)
} finally {
loadingMemories.value = false
}
}
async function handleDeleteMemory(id: string) {
try {
await api.delete(`/api/profiles/${route.params.id}/memories/${id}`)
memories.value = memories.value.filter(m => m.id !== id)
} catch (e) {
console.error('删除记忆失败:', e)
}
}
async function handleAddMemory(data: { type: string; value: Record<string, unknown> }) {
try {
// 使用合适的端点 based on type
let newMemory: MemoryItem
if (data.type === 'favorite_character') {
newMemory = await api.post<MemoryItem>(`/api/profiles/${route.params.id}/memories/character`, {
name: data.value.name,
description: data.value.description || null,
})
} else if (data.type === 'scary_element') {
newMemory = await api.post<MemoryItem>(`/api/profiles/${route.params.id}/memories/scary`, {
keyword: data.value.keyword,
category: data.value.category || 'other',
})
} else {
// 通用类型
newMemory = await api.post<MemoryItem>(`/api/profiles/${route.params.id}/memories`, {
type: data.type,
value: data.value,
})
}
memories.value.unshift(newMemory)
showAddMemoryModal.value = false
} catch (e) {
console.error('添加记忆失败:', e)
}
}
async function updateProfile() {
if (!form.value.name.trim()) {
error.value = '姓名不能为空'
return
}
error.value = ''
try {
const data = await api.put<ChildProfile>(`/api/profiles/${route.params.id}`, {
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),
})
profile.value = data
fillForm(data)
} catch (e) {
error.value = e instanceof Error ? e.message : '更新失败'
}
}
async function deleteProfile() {
if (!window.confirm('确定删除这个档案吗?')) return
try {
await api.delete(`/api/profiles/${route.params.id}`)
router.push('/profiles')
} catch (e) {
error.value = e instanceof Error ? e.message : '删除失败'
}
}
onMounted(() => {
fetchProfile()
fetchMemories()
})
</script>
<template>
<div class="max-w-3xl 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="flex gap-4">
<BaseButton as="router-link" :to="`/profiles/${route.params.id}/timeline`" class="bg-gradient-to-r from-purple-500 to-pink-500 text-white shadow-lg hover:shadow-xl hover:-translate-y-0.5 transition-all">
查看成长足迹
</BaseButton>
<BaseButton as="router-link" to="/profiles" variant="ghost" class="text-purple-600">
返回列表
</BaseButton>
</div>
</div>
<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="profile" class="space-y-6">
<BaseCard>
<div class="flex items-center space-x-4">
<div class="w-14 h-14 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="text-xl font-semibold text-gray-800">{{ profile.name }}</div>
<div class="text-sm text-gray-500">{{ profile.age ?? '未知' }} · {{ profile.gender ?? '未设置' }}</div>
</div>
</div>
</BaseCard>
<BaseCard>
<div class="flex items-center gap-2 mb-4">
<UserCircleIcon class="h-5 w-5 text-purple-500" />
<h2 class="text-lg font-semibold text-gray-700">编辑信息</h2>
</div>
<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>
<div class="flex gap-3">
<BaseButton @click="updateProfile">保存</BaseButton>
<BaseButton variant="secondary" class="text-red-500 border-red-200" @click="deleteProfile">
删除档案
</BaseButton>
</div>
</div>
</BaseCard>
<!-- 记忆管理区块 -->
<BaseCard>
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<SparklesIcon class="h-5 w-5 text-purple-500" />
<h2 class="text-lg font-semibold text-gray-700">记忆管理</h2>
</div>
<BaseButton size="sm" @click="showAddMemoryModal = true">
<PlusIcon class="h-4 w-4 mr-1" />
添加记忆
</BaseButton>
</div>
<MemoryList
:memories="memories"
:loading="loadingMemories"
@delete="handleDeleteMemory"
/>
</BaseCard>
</div>
<!-- 添加记忆模态框 -->
<AddMemoryModal
:show="showAddMemoryModal"
@close="showAddMemoryModal = false"
@submit="handleAddMemory"
/>
</div>
</template>