Some checks are pending
Build and Push Docker Images / changes (push) Waiting to run
Build and Push Docker Images / build-backend (push) Blocked by required conditions
Build and Push Docker Images / build-frontend (push) Blocked by required conditions
Build and Push Docker Images / build-admin-frontend (push) Blocked by required conditions
175 lines
5.2 KiB
Vue
175 lines
5.2 KiB
Vue
<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>
|