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:
274
frontend/src/views/ChildProfileDetail.vue
Normal file
274
frontend/src/views/ChildProfileDetail.vue
Normal 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>
|
||||
Reference in New Issue
Block a user