593 lines
13 KiB
Vue
593 lines
13 KiB
Vue
<template>
|
||
<div class="parent-dashboard">
|
||
<!-- 欢迎横幅 -->
|
||
<div class="welcome-banner">
|
||
<div class="banner-content">
|
||
<div class="banner-text">
|
||
<h1>
|
||
<HomeOutlined class="banner-icon" />
|
||
<span class="banner-title-text">家长中心</span>
|
||
</h1>
|
||
<p>关注孩子的阅读成长,陪伴每一步进步!</p>
|
||
</div>
|
||
<div class="banner-decorations">
|
||
<span class="decoration"><BookOutlined /></span>
|
||
<span class="decoration"><StarOutlined /></span>
|
||
<span class="decoration"><HeartOutlined /></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<a-spin :spinning="loading">
|
||
<!-- 我的孩子 -->
|
||
<div class="section-card">
|
||
<div class="section-header">
|
||
<h3><TeamOutlined /> 我的孩子</h3>
|
||
</div>
|
||
<div class="children-grid" v-if="children.length > 0">
|
||
<div
|
||
v-for="child in children"
|
||
:key="child.id"
|
||
class="child-card"
|
||
@click="goToChildDetail(child.id)"
|
||
>
|
||
<div class="child-avatar">
|
||
<a-avatar :size="isMobile ? 56 : 64" :style="{ backgroundColor: getAvatarColor(child.id) }">
|
||
{{ child.name.charAt(0) }}
|
||
</a-avatar>
|
||
</div>
|
||
<div class="child-info">
|
||
<div class="child-name">
|
||
{{ child.name }}
|
||
<span class="relationship">{{ getRelationshipText(child.relationship) }}</span>
|
||
</div>
|
||
<div class="child-class">{{ child.class?.name || '未分班' }}</div>
|
||
<div class="child-stats">
|
||
<span><BookOutlined /> {{ child.readingCount }} 次阅读</span>
|
||
<span><ReadOutlined /> {{ child.lessonCount }} 节课</span>
|
||
</div>
|
||
</div>
|
||
<div class="card-arrow">
|
||
<RightOutlined />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="empty-state" v-else>
|
||
<InboxOutlined class="empty-icon" />
|
||
<p>暂无孩子信息</p>
|
||
<p class="empty-hint">请联系学校添加孩子信息</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 最近任务 -->
|
||
<div class="section-card">
|
||
<div class="section-header">
|
||
<h3><CheckSquareOutlined /> 最近任务</h3>
|
||
<a-button type="link" @click="goToTasks" class="view-all-btn">查看全部</a-button>
|
||
</div>
|
||
<div class="task-list" v-if="recentTasks.length > 0">
|
||
<div v-for="task in recentTasks" :key="task.id" class="task-item">
|
||
<div class="task-status" :class="getStatusClass(task.status)">
|
||
{{ getStatusText(task.status) }}
|
||
</div>
|
||
<div class="task-content">
|
||
<div class="task-title">{{ task.task.title }}</div>
|
||
<div class="task-deadline">
|
||
<ClockCircleOutlined />
|
||
<span>截止:{{ formatDate(task.task.endDate) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="empty-state compact" v-else>
|
||
<p>暂无任务</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 成长档案 -->
|
||
<div class="section-card">
|
||
<div class="section-header">
|
||
<h3><FileImageOutlined /> 成长档案</h3>
|
||
<a-button type="link" @click="goToGrowth" class="view-all-btn">查看全部</a-button>
|
||
</div>
|
||
<div class="growth-list" v-if="recentGrowth.length > 0">
|
||
<div v-for="record in recentGrowth" :key="record.id" class="growth-item">
|
||
<div class="growth-images" v-if="record.images && record.images.length > 0">
|
||
<img :src="record.images[0]" alt="成长照片" />
|
||
</div>
|
||
<div class="growth-content">
|
||
<div class="growth-title">{{ record.title }}</div>
|
||
<div class="growth-date">{{ formatDate(record.recordDate) }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="empty-state compact" v-else>
|
||
<p>暂无成长记录</p>
|
||
</div>
|
||
</div>
|
||
</a-spin>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted, onUnmounted } from 'vue';
|
||
import { useRouter } from 'vue-router';
|
||
import { message } from 'ant-design-vue';
|
||
import {
|
||
HomeOutlined,
|
||
BookOutlined,
|
||
StarOutlined,
|
||
HeartOutlined,
|
||
TeamOutlined,
|
||
ReadOutlined,
|
||
CheckSquareOutlined,
|
||
FileImageOutlined,
|
||
ClockCircleOutlined,
|
||
RightOutlined,
|
||
InboxOutlined,
|
||
} from '@ant-design/icons-vue';
|
||
import { getChildren, type ChildInfo } from '@/api/parent';
|
||
import dayjs from 'dayjs';
|
||
|
||
const router = useRouter();
|
||
const loading = ref(false);
|
||
const children = ref<ChildInfo[]>([]);
|
||
const recentTasks = ref<any[]>([]);
|
||
const recentGrowth = ref<any[]>([]);
|
||
const isMobile = ref(false);
|
||
|
||
const avatarColors = ['#52c41a', '#1890ff', '#fa8c16', '#eb2f96', '#722ed1'];
|
||
|
||
const getAvatarColor = (id: number) => avatarColors[id % avatarColors.length];
|
||
|
||
const getRelationshipText = (relationship: string) => {
|
||
const map: Record<string, string> = {
|
||
FATHER: '爸爸',
|
||
MOTHER: '妈妈',
|
||
GRANDFATHER: '爷爷',
|
||
GRANDMOTHER: '奶奶',
|
||
OTHER: '监护人',
|
||
};
|
||
return map[relationship] || relationship;
|
||
};
|
||
|
||
const statusMap: Record<string, { text: string; class: string }> = {
|
||
PENDING: { text: '待完成', class: 'status-pending' },
|
||
IN_PROGRESS: { text: '进行中', class: 'status-progress' },
|
||
COMPLETED: { text: '已完成', class: 'status-completed' },
|
||
};
|
||
|
||
const getStatusText = (status: string) => statusMap[status]?.text || status;
|
||
const getStatusClass = (status: string) => statusMap[status]?.class || '';
|
||
|
||
const formatDate = (date: string) => dayjs(date).format('YYYY-MM-DD');
|
||
|
||
const checkMobile = () => {
|
||
isMobile.value = window.innerWidth < 768;
|
||
};
|
||
|
||
const goToChildDetail = (childId: number) => {
|
||
router.push(`/parent/children/${childId}`);
|
||
};
|
||
|
||
const goToTasks = () => {
|
||
router.push('/parent/tasks');
|
||
};
|
||
|
||
const goToGrowth = () => {
|
||
router.push('/parent/growth');
|
||
};
|
||
|
||
const loadData = async () => {
|
||
loading.value = true;
|
||
try {
|
||
const data = await getChildren();
|
||
children.value = data;
|
||
|
||
// 如果有孩子,加载第一个孩子的任务和成长记录
|
||
if (data.length > 0) {
|
||
// 这里可以加载任务和成长记录
|
||
}
|
||
} catch (error: any) {
|
||
message.error(error.response?.data?.message || '加载数据失败');
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
onMounted(() => {
|
||
checkMobile();
|
||
window.addEventListener('resize', checkMobile);
|
||
loadData();
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
window.removeEventListener('resize', checkMobile);
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
$primary-color: #52c41a;
|
||
$primary-light: #f6ffed;
|
||
$primary-dark: #389e0d;
|
||
|
||
.parent-dashboard {
|
||
// 欢迎横幅
|
||
.welcome-banner {
|
||
background: linear-gradient(135deg, $primary-color 0%, #73d13d 100%);
|
||
border-radius: 16px;
|
||
padding: 24px 32px;
|
||
margin-bottom: 24px;
|
||
color: white;
|
||
|
||
.banner-content {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 24px;
|
||
margin: 0 0 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
|
||
.banner-icon {
|
||
font-size: 28px;
|
||
}
|
||
}
|
||
|
||
p {
|
||
margin: 0;
|
||
opacity: 0.9;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.banner-decorations {
|
||
display: flex;
|
||
gap: 16px;
|
||
font-size: 32px;
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
|
||
// 区块卡片
|
||
.section-card {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
margin-bottom: 24px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
|
||
h3 {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
color: #333;
|
||
}
|
||
|
||
.view-all-btn {
|
||
padding: 0;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 孩子卡片
|
||
.children-grid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.child-card {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 16px;
|
||
background: #fafafa;
|
||
border-radius: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
|
||
&:hover {
|
||
background: $primary-light;
|
||
|
||
.card-arrow {
|
||
color: $primary-color;
|
||
}
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.child-info {
|
||
flex: 1;
|
||
margin-left: 16px;
|
||
min-width: 0;
|
||
|
||
.child-name {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
|
||
.relationship {
|
||
margin-left: 8px;
|
||
font-size: 12px;
|
||
color: #999;
|
||
font-weight: normal;
|
||
}
|
||
}
|
||
|
||
.child-class {
|
||
font-size: 13px;
|
||
color: #666;
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.child-stats {
|
||
font-size: 12px;
|
||
color: #999;
|
||
display: flex;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
|
||
span {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.card-arrow {
|
||
color: #d9d9d9;
|
||
font-size: 16px;
|
||
transition: color 0.3s;
|
||
}
|
||
}
|
||
|
||
// 任务和成长列表
|
||
.task-list,
|
||
.growth-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.task-item,
|
||
.growth-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px;
|
||
background: #fafafa;
|
||
border-radius: 8px;
|
||
gap: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
|
||
&:hover {
|
||
background: $primary-light;
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.task-status {
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
|
||
&.status-pending {
|
||
background: #fff7e6;
|
||
color: #fa8c16;
|
||
}
|
||
|
||
&.status-progress {
|
||
background: #e6f7ff;
|
||
color: #1890ff;
|
||
}
|
||
|
||
&.status-completed {
|
||
background: $primary-light;
|
||
color: $primary-color;
|
||
}
|
||
}
|
||
|
||
.task-content,
|
||
.growth-content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
|
||
.task-title,
|
||
.growth-title {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.task-deadline,
|
||
.growth-date {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-top: 4px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
}
|
||
|
||
.growth-images {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
flex-shrink: 0;
|
||
|
||
img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 空状态
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 32px;
|
||
color: #999;
|
||
|
||
&.compact {
|
||
padding: 24px;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 48px;
|
||
color: #d9d9d9;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
p {
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.empty-hint {
|
||
font-size: 13px;
|
||
color: #bfbfbf;
|
||
}
|
||
}
|
||
}
|
||
|
||
// =============== 移动端响应式 ===============
|
||
@media screen and (max-width: 768px) {
|
||
.parent-dashboard {
|
||
.welcome-banner {
|
||
padding: 20px 16px;
|
||
margin-bottom: 16px;
|
||
border-radius: 12px;
|
||
|
||
h1 {
|
||
font-size: 20px;
|
||
|
||
.banner-icon {
|
||
font-size: 24px;
|
||
}
|
||
}
|
||
|
||
p {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.banner-decorations {
|
||
gap: 12px;
|
||
font-size: 24px;
|
||
}
|
||
}
|
||
|
||
.section-card {
|
||
padding: 16px;
|
||
margin-bottom: 16px;
|
||
border-radius: 12px;
|
||
|
||
.section-header {
|
||
margin-bottom: 12px;
|
||
|
||
h3 {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.view-all-btn {
|
||
font-size: 13px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.child-card {
|
||
padding: 14px;
|
||
|
||
.child-info {
|
||
margin-left: 14px;
|
||
|
||
.child-name {
|
||
font-size: 15px;
|
||
}
|
||
|
||
.child-class {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.child-stats {
|
||
font-size: 11px;
|
||
gap: 12px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.task-item,
|
||
.growth-item {
|
||
padding: 12px;
|
||
|
||
.task-status {
|
||
padding: 3px 6px;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.task-content,
|
||
.growth-content {
|
||
.task-title,
|
||
.growth-title {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.task-deadline,
|
||
.growth-date {
|
||
font-size: 11px;
|
||
}
|
||
}
|
||
|
||
.growth-images {
|
||
width: 40px;
|
||
height: 40px;
|
||
}
|
||
}
|
||
|
||
.empty-state {
|
||
padding: 24px;
|
||
|
||
&.compact {
|
||
padding: 20px;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 40px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 平板适配
|
||
@media screen and (min-width: 769px) and (max-width: 1024px) {
|
||
.parent-dashboard {
|
||
.welcome-banner {
|
||
padding: 22px 28px;
|
||
}
|
||
}
|
||
}
|
||
</style>
|