Files
dreamweaver/frontend/src/views/ChildProfileDetail.vue
zhangtuo e9d7f8832a 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
2026-01-20 18:20:03 +08:00

275 lines
8.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>