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>
|