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