原子样式调整

This commit is contained in:
zhonghua 2026-03-03 13:59:02 +08:00
parent 9ad1c19385
commit 7b1260afd3
45 changed files with 2579 additions and 13133 deletions

View File

@ -1,5 +1,5 @@
<template>
<div class="not-found-view">
<div class="flex items-center justify-center min-h-screen bg-[#f0f2f5]">
<a-result
status="404"
title="404"
@ -29,13 +29,3 @@ const goHome = () => {
}
};
</script>
<style scoped>
.not-found-view {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: #f0f2f5;
}
</style>

View File

@ -1,72 +1,72 @@
<template>
<div class="dashboard">
<div>
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon" style="background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);">
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-5">
<div class="flex items-center p-5 bg-white rounded-2xl shadow-[0_1px_3px_rgba(0,0,0,0.04)] transition-all duration-300 hover:-translate-y-0.5 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)]">
<div class="w-14 h-14 rounded-[14px] flex items-center justify-center text-white mr-4 bg-[linear-gradient(135deg,#6366F1_0%,#4F46E5_100%)]">
<Building2 :size="24" :stroke-width="1.5" />
</div>
<div class="stat-content">
<span class="stat-value">{{ statsData.tenantCount }}</span>
<span class="stat-label">租户总数</span>
<div class="flex flex-col">
<span class="text-[28px] font-700 text-[#1F2937] leading-tight">{{ statsData.tenantCount }}</span>
<span class="text-sm text-[#6B7280] mt-1">租户总数</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: linear-gradient(135deg, #10B981 0%, #059669 100%);">
<div class="flex items-center p-5 bg-white rounded-2xl shadow-[0_1px_3px_rgba(0,0,0,0.04)] transition-all duration-300 hover:-translate-y-0.5 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)]">
<div class="w-14 h-14 rounded-[14px] flex items-center justify-center text-white mr-4 bg-[linear-gradient(135deg,#10B981_0%,#059669_100%)]">
<BookOpen :size="24" :stroke-width="1.5" />
</div>
<div class="stat-content">
<span class="stat-value">{{ statsData.courseCount }}</span>
<span class="stat-label">课程包总数</span>
<div class="flex flex-col">
<span class="text-[28px] font-700 text-[#1F2937] leading-tight">{{ statsData.courseCount }}</span>
<span class="text-sm text-[#6B7280] mt-1">课程包总数</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: linear-gradient(135deg, #F59E0B 0%, #D97706 100%);">
<div class="flex items-center p-5 bg-white rounded-2xl shadow-[0_1px_3px_rgba(0,0,0,0.04)] transition-all duration-300 hover:-translate-y-0.5 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)]">
<div class="w-14 h-14 rounded-[14px] flex items-center justify-center text-white mr-4 bg-[linear-gradient(135deg,#F59E0B_0%,#D97706_100%)]">
<PlayCircle :size="24" :stroke-width="1.5" />
</div>
<div class="stat-content">
<span class="stat-value">{{ statsData.monthlyLessons }}</span>
<span class="stat-label">月授课次数</span>
<div class="flex flex-col">
<span class="text-[28px] font-700 text-[#1F2937] leading-tight">{{ statsData.monthlyLessons }}</span>
<span class="text-sm text-[#6B7280] mt-1">月授课次数</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: linear-gradient(135deg, #EC4899 0%, #DB2777 100%);">
<div class="flex items-center p-5 bg-white rounded-2xl shadow-[0_1px_3px_rgba(0,0,0,0.04)] transition-all duration-300 hover:-translate-y-0.5 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)]">
<div class="w-14 h-14 rounded-[14px] flex items-center justify-center text-white mr-4 bg-[linear-gradient(135deg,#EC4899_0%,#DB2777_100%)]">
<Users :size="24" :stroke-width="1.5" />
</div>
<div class="stat-content">
<span class="stat-value">{{ statsData.studentCount }}</span>
<span class="stat-label">覆盖学生</span>
<div class="flex flex-col">
<span class="text-[28px] font-700 text-[#1F2937] leading-tight">{{ statsData.studentCount }}</span>
<span class="text-sm text-[#6B7280] mt-1">覆盖学生</span>
</div>
</div>
</div>
<!-- 趋势图和快捷入口 -->
<a-row :gutter="24" style="margin-top: 24px">
<a-row :gutter="24" class="mt-6">
<a-col :span="16">
<a-card title="使用趋势" :bordered="false" :loading="trendLoading" class="modern-card">
<div ref="trendChartRef" style="height: 300px"></div>
<a-card title="使用趋势" :bordered="false" :loading="trendLoading" class="rounded-2xl shadow-[0_1px_3px_rgba(0,0,0,0.04)] modern-card">
<div ref="trendChartRef" class="h-[300px]"></div>
</a-card>
</a-col>
<a-col :span="8">
<a-card title="快捷入口" :bordered="false" class="modern-card">
<div class="quick-actions">
<div class="quick-action-item" @click="$router.push('/admin/courses/create')">
<div class="action-icon">
<a-card title="快捷入口" :bordered="false" class="rounded-2xl shadow-[0_1px_3px_rgba(0,0,0,0.04)] modern-card">
<div class="flex flex-col gap-3">
<div class="flex items-center py-3 px-4 bg-[#F9FAFB] rounded-[10px] cursor-pointer transition-all duration-250 hover:bg-[#EEF2FF] hover:translate-x-1 group quick-action-item" @click="$router.push('/admin/courses/create')">
<div class="w-9 h-9 rounded-[10px] bg-white flex items-center justify-center mr-3 text-[#6366F1] group-hover:bg-[#6366F1] group-hover:text-white transition-all duration-250">
<Plus :size="20" :stroke-width="1.5" />
</div>
<span>创建课程包</span>
<span class="font-500 text-[#1F2937]">创建课程包</span>
</div>
<div class="quick-action-item" @click="$router.push('/admin/tenants')">
<div class="action-icon">
<div class="flex items-center py-3 px-4 bg-[#F9FAFB] rounded-[10px] cursor-pointer transition-all duration-250 hover:bg-[#EEF2FF] hover:translate-x-1 group quick-action-item" @click="$router.push('/admin/tenants')">
<div class="w-9 h-9 rounded-[10px] bg-white flex items-center justify-center mr-3 text-[#6366F1] group-hover:bg-[#6366F1] group-hover:text-white transition-all duration-250">
<Building2 :size="20" :stroke-width="1.5" />
</div>
<span>管理租户</span>
<span class="font-500 text-[#1F2937]">管理租户</span>
</div>
<div class="quick-action-item" @click="$router.push('/admin/resources')">
<div class="action-icon">
<div class="flex items-center py-3 px-4 bg-[#F9FAFB] rounded-[10px] cursor-pointer transition-all duration-250 hover:bg-[#EEF2FF] hover:translate-x-1 group quick-action-item" @click="$router.push('/admin/resources')">
<div class="w-9 h-9 rounded-[10px] bg-white flex items-center justify-center mr-3 text-[#6366F1] group-hover:bg-[#6366F1] group-hover:text-white transition-all duration-250">
<FolderOpen :size="20" :stroke-width="1.5" />
</div>
<span>资源库</span>
<span class="font-500 text-[#1F2937]">资源库</span>
</div>
</div>
</a-card>
@ -74,15 +74,20 @@
</a-row>
<!-- 活跃租户和热门课程 -->
<a-row :gutter="24" style="margin-top: 24px">
<a-row :gutter="24" class="mt-6">
<a-col :span="12">
<a-card title="活跃租户 TOP5" :bordered="false" :loading="tenantsLoading" class="modern-card">
<div v-if="activeTenants.length > 0" class="rank-list">
<div v-for="(item, index) in activeTenants" :key="item.id" class="rank-item" @click="viewTenantDetail(item.id)">
<div class="rank-number" :class="'rank-' + (index + 1)">{{ index + 1 }}</div>
<div class="rank-content">
<span class="rank-name">{{ item.name }}</span>
<span class="rank-desc">教师: {{ item.teacherCount }} | 学生: {{ item.studentCount }}</span>
<a-card title="活跃租户 TOP5" :bordered="false" :loading="tenantsLoading" class="rounded-2xl shadow-[0_1px_3px_rgba(0,0,0,0.04)] modern-card">
<div v-if="activeTenants.length > 0">
<div v-for="(item, index) in activeTenants" :key="item.id" class="flex items-center py-3 border-b border-[#E5E7EB] last:border-b-0 cursor-pointer transition-all duration-250 hover:bg-[#F9FAFB] -mx-4 px-4 rounded-lg" @click="viewTenantDetail(item.id)">
<div
class="w-7 h-7 rounded-lg flex items-center justify-center font-600 text-sm mr-3"
:class="index === 0 ? 'bg-[linear-gradient(135deg,#F59E0B_0%,#D97706_100%)] text-white' : index === 1 ? 'bg-[linear-gradient(135deg,#9CA3AF_0%,#6B7280_100%)] text-white' : index === 2 ? 'bg-[linear-gradient(135deg,#CD7F32_0%,#B8860B_100%)] text-white' : 'bg-[#F9FAFB] text-[#6B7280]'"
>
{{ index + 1 }}
</div>
<div class="flex-1 flex flex-col">
<span class="font-500 text-[#1F2937]">{{ item.name }}</span>
<span class="text-xs text-[#6B7280] mt-0.5">教师: {{ item.teacherCount }} | 学生: {{ item.studentCount }}</span>
</div>
<a-tag color="blue">{{ item.lessonCount }} </a-tag>
</div>
@ -91,13 +96,18 @@
</a-card>
</a-col>
<a-col :span="12">
<a-card title="热门课程包 TOP5" :bordered="false" :loading="coursesLoading" class="modern-card">
<div v-if="popularCourses.length > 0" class="rank-list">
<div v-for="(item, index) in popularCourses" :key="item.id" class="rank-item" @click="viewCourseDetail(item.id)">
<div class="rank-number" :class="'rank-' + (index + 1)">{{ index + 1 }}</div>
<div class="rank-content">
<span class="rank-name">{{ item.name }}</span>
<span class="rank-desc">使用教师: {{ item.teacherCount }}</span>
<a-card title="热门课程包 TOP5" :bordered="false" :loading="coursesLoading" class="rounded-2xl shadow-[0_1px_3px_rgba(0,0,0,0.04)] modern-card">
<div v-if="popularCourses.length > 0">
<div v-for="(item, index) in popularCourses" :key="item.id" class="flex items-center py-3 border-b border-[#E5E7EB] last:border-b-0 cursor-pointer transition-all duration-250 hover:bg-[#F9FAFB] -mx-4 px-4 rounded-lg" @click="viewCourseDetail(item.id)">
<div
class="w-7 h-7 rounded-lg flex items-center justify-center font-600 text-sm mr-3"
:class="index === 0 ? 'bg-[linear-gradient(135deg,#F59E0B_0%,#D97706_100%)] text-white' : index === 1 ? 'bg-[linear-gradient(135deg,#9CA3AF_0%,#6B7280_100%)] text-white' : index === 2 ? 'bg-[linear-gradient(135deg,#CD7F32_0%,#B8860B_100%)] text-white' : 'bg-[#F9FAFB] text-[#6B7280]'"
>
{{ index + 1 }}
</div>
<div class="flex-1 flex flex-col">
<span class="font-500 text-[#1F2937]">{{ item.name }}</span>
<span class="text-xs text-[#6B7280] mt-0.5">使用教师: {{ item.teacherCount }}</span>
</div>
<a-tag color="green">{{ item.usageCount }} </a-tag>
</div>
@ -108,20 +118,23 @@
</a-row>
<!-- 最近活动 -->
<a-row :gutter="24" style="margin-top: 24px">
<a-row :gutter="24" class="mt-6">
<a-col :span="24">
<a-card title="最近活动" :bordered="false" :loading="activitiesLoading" class="modern-card">
<div v-if="recentActivities.length > 0" class="activity-timeline">
<div v-for="activity in recentActivities" :key="activity.id" class="activity-item">
<div class="activity-dot" :class="'type-' + activity.type"></div>
<div class="activity-content">
<div class="activity-header">
<a-tag :color="getActivityTagColor(activity.type)" class="activity-tag">
<a-card title="最近活动" :bordered="false" :loading="activitiesLoading" class="rounded-2xl shadow-[0_1px_3px_rgba(0,0,0,0.04)] modern-card">
<div v-if="recentActivities.length > 0">
<div v-for="activity in recentActivities" :key="activity.id" class="flex items-start py-3 border-b border-[#E5E7EB] last:border-b-0">
<div
class="w-2.5 h-2.5 rounded-full mt-1.5 mr-4 shrink-0"
:class="activity.type === 'lesson' ? 'bg-[#6366F1]' : activity.type === 'tenant' ? 'bg-[#10B981]' : 'bg-[#F59E0B]'"
></div>
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<a-tag :color="getActivityTagColor(activity.type)" class="!m-0">
{{ getActivityTypeText(activity.type) }}
</a-tag>
<span class="activity-title">{{ activity.title }}</span>
<span class="text-[#1F2937]">{{ activity.title }}</span>
</div>
<span class="activity-time">{{ formatTime(activity.time) }}</span>
<span class="text-xs text-[#6B7280]">{{ formatTime(activity.time) }}</span>
</div>
</div>
</div>
@ -430,268 +443,12 @@ onUnmounted(() => {
});
</script>
<style scoped lang="scss">
//
$primary-color: #6366F1;
$primary-light: #EEF2FF;
$success-color: #10B981;
$warning-color: #F59E0B;
$pink-color: #EC4899;
$text-color: #1F2937;
$text-secondary: #6B7280;
$border-color: #E5E7EB;
$bg-light: #F9FAFB;
.dashboard {
//
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
.stat-card {
display: flex;
align-items: center;
padding: 20px;
background: white;
border-radius: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.stat-icon {
width: 56px;
height: 56px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
color: white;
margin-right: 16px;
}
.stat-content {
display: flex;
flex-direction: column;
.stat-value {
font-size: 28px;
font-weight: 700;
color: $text-color;
line-height: 1.2;
}
.stat-label {
font-size: 14px;
color: $text-secondary;
margin-top: 4px;
}
}
}
//
.modern-card {
border-radius: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
:deep(.ant-card-head) {
border-bottom: 1px solid $border-color;
.ant-card-head-title {
font-weight: 600;
color: $text-color;
}
}
}
//
.quick-actions {
display: flex;
flex-direction: column;
gap: 12px;
.quick-action-item {
display: flex;
align-items: center;
padding: 12px 16px;
background: $bg-light;
border-radius: 10px;
cursor: pointer;
transition: all 0.25s ease;
&:hover {
background: $primary-light;
transform: translateX(4px);
.action-icon {
background: $primary-color;
color: white;
}
}
.action-icon {
width: 36px;
height: 36px;
border-radius: 10px;
background: white;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
color: $primary-color;
transition: all 0.25s ease;
}
span {
font-weight: 500;
color: $text-color;
}
}
}
//
.rank-list {
.rank-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid $border-color;
cursor: pointer;
transition: all 0.25s ease;
&:last-child {
border-bottom: none;
}
&:hover {
background: $bg-light;
margin: 0 -16px;
padding: 12px 16px;
border-radius: 8px;
}
.rank-number {
width: 28px;
height: 28px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
margin-right: 12px;
background: $bg-light;
color: $text-secondary;
&.rank-1 {
background: linear-gradient(135deg, #F59E0B 0%, #D97706 100%);
color: white;
}
&.rank-2 {
background: linear-gradient(135deg, #9CA3AF 0%, #6B7280 100%);
color: white;
}
&.rank-3 {
background: linear-gradient(135deg, #CD7F32 0%, #B8860B 100%);
color: white;
}
}
.rank-content {
flex: 1;
display: flex;
flex-direction: column;
.rank-name {
font-weight: 500;
color: $text-color;
}
.rank-desc {
font-size: 12px;
color: $text-secondary;
margin-top: 2px;
}
}
}
}
// 线
.activity-timeline {
.activity-item {
display: flex;
align-items: flex-start;
padding: 12px 0;
border-bottom: 1px solid $border-color;
&:last-child {
border-bottom: none;
}
.activity-dot {
width: 10px;
height: 10px;
border-radius: 50%;
margin-top: 6px;
margin-right: 16px;
flex-shrink: 0;
&.type-lesson {
background: $primary-color;
}
&.type-tenant {
background: $success-color;
}
&.type-course {
background: $warning-color;
}
}
.activity-content {
flex: 1;
.activity-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
.activity-tag {
margin: 0;
}
.activity-title {
color: $text-color;
}
}
.activity-time {
font-size: 12px;
color: $text-secondary;
}
}
}
}
<style scoped>
.modern-card :deep(.ant-card-head) {
border-bottom: 1px solid #E5E7EB;
}
@media (max-width: 1200px) {
.dashboard .stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.dashboard .stats-grid {
grid-template-columns: 1fr;
}
.modern-card :deep(.ant-card-head-title) {
font-weight: 600;
color: #1F2937;
}
</style>

View File

@ -1,29 +1,29 @@
<template>
<a-layout class="admin-layout">
<a-layout class="admin-layout h-screen min-h-screen bg-[#F9FAFB] flex overflow-hidden">
<!-- 桌面端侧边栏 -->
<a-layout-sider
v-if="!isMobile"
v-model:collapsed="collapsed"
:trigger="null"
collapsible
class="admin-sider"
class="admin-sider bg-white! shadow-[2px_0_8px_rgba(0,0,0,0.04)] border-r border-[#E5E7EB] flex flex-col overflow-hidden"
>
<div class="sider-logo">
<img src="/logo.png" alt="Logo" class="logo-img" />
<div v-if="!collapsed" class="logo-text">
<span class="logo-title">少儿智慧阅读</span>
<span class="logo-subtitle">服务管理后台</span>
<div class="shrink-0 h-[72px] flex items-center justify-center py-3 px-4 border-b border-[#E5E7EB] bg-gradient-to-br from-[#F9FAFB] to-white">
<img src="/logo.png" alt="Logo" class="w-[50px] h-[50px] object-contain" />
<div v-if="!collapsed" class="flex flex-col ml-2.5 leading-tight">
<span class="text-[15px] font-semibold text-[#6366F1] whitespace-nowrap">少儿智慧阅读</span>
<span class="text-xs text-[#6B7280] whitespace-nowrap">服务管理后台</span>
</div>
</div>
<div class="sider-menu-wrap">
<div class="sider-menu-wrap flex-1 min-h-0 overflow-y-auto">
<a-menu
v-model:selectedKeys="selectedKeys"
mode="inline"
theme="light"
:inline-collapsed="collapsed"
@select="handleMenuSelect"
class="side-menu"
class="side-menu border-r-0! py-3 px-2"
>
<a-menu-item key="dashboard">
<template #icon>
@ -88,9 +88,9 @@
:body-style="{ padding: 0 }"
>
<template #title>
<div class="drawer-header">
<img src="/logo.png" alt="Logo" class="drawer-logo" />
<span class="drawer-title">服务管理后台</span>
<div class="flex items-center gap-3">
<img src="/logo.png" alt="Logo" class="w-9 h-9 object-contain" />
<span class="text-base font-semibold text-[#1F2937]">服务管理后台</span>
</div>
</template>
<a-menu
@ -98,7 +98,7 @@
mode="inline"
theme="light"
@select="handleDrawerMenuSelect"
class="drawer-menu"
class="drawer-menu border-r-0! py-2"
>
<a-menu-item key="dashboard">
<template #icon><LayoutDashboard :size="18" :stroke-width="1.5" /></template>
@ -131,18 +131,18 @@
</a-menu>
</a-drawer>
<a-layout class="admin-layout-right">
<a-layout class="flex-1 min-h-0 flex flex-col overflow-hidden">
<!-- 桌面端顶部 -->
<a-layout-header v-if="!isMobile" class="admin-header">
<a-layout-header v-if="!isMobile" class="shrink-0 sticky top-0 z-100 bg-white px-6 flex justify-between items-center shadow-sm border-b border-[#E5E7EB]">
<div class="header-left">
<MenuUnfoldOutlined
v-if="collapsed"
class="trigger"
class="text-lg cursor-pointer transition-colors text-[#6B7280] hover:text-[#6366F1]"
@click="collapsed = !collapsed"
/>
<MenuFoldOutlined
v-else
class="trigger"
class="text-lg cursor-pointer transition-colors text-[#6B7280] hover:text-[#6366F1]"
@click="collapsed = !collapsed"
/>
</div>
@ -152,11 +152,11 @@
<NotificationBell />
<a-dropdown>
<a-space class="user-info" style="cursor: pointer;">
<a-avatar :size="32" class="user-avatar">
<a-space class="px-3 cursor-pointer">
<a-avatar :size="32" class="bg-gradient-to-br from-[#6366F1] to-[#4F46E5]">
<template #icon><UserOutlined /></template>
</a-avatar>
<span class="user-name">{{ userStore.user?.name }}</span>
<span class="text-[#1F2937] font-medium">{{ userStore.user?.name }}</span>
<DownOutlined />
</a-space>
<template #overlay>
@ -178,12 +178,12 @@
</a-layout-header>
<!-- 移动端顶部 -->
<a-layout-header v-if="isMobile" class="admin-mobile-header">
<MenuOutlined class="menu-trigger" @click="drawerVisible = true" />
<span class="mobile-title">少儿智慧阅读</span>
<a-layout-header v-if="isMobile" class="shrink-0 sticky top-0 z-100 bg-white px-4 h-14 flex items-center justify-between shadow-sm border-b border-[#E5E7EB]">
<MenuOutlined class="text-[22px] text-[#6B7280] p-2 cursor-pointer" @click="drawerVisible = true" />
<span class="text-[17px] font-semibold text-[#1F2937]">少儿智慧阅读</span>
<a-dropdown>
<a-space class="user-info" style="cursor: pointer;">
<a-avatar :size="32" class="user-avatar">
<a-space class="cursor-pointer">
<a-avatar :size="32" class="bg-gradient-to-br from-[#6366F1] to-[#4F46E5]">
<template #icon><UserOutlined /></template>
</a-avatar>
</a-space>
@ -197,7 +197,7 @@
</a-dropdown>
</a-layout-header>
<a-layout-content :class="['admin-content', { 'admin-content-mobile': isMobile }]">
<a-layout-content :class="['flex-1 min-h-0 bg-white rounded-2xl shadow-sm overflow-y-auto overflow-x-hidden', isMobile ? 'm-3 p-4' : 'm-5 p-6']">
<router-view />
</a-layout-content>
</a-layout>
@ -298,270 +298,86 @@ const handleUserMenuClick = ({ key }: { key: string | number }) => {
</script>
<style scoped lang="scss">
//
$primary-color: #6366F1; // -
$primary-light: #EEF2FF;
$primary-dark: #4F46E5;
$accent-color: #10B981; // 绿
$text-color: #1F2937;
$text-secondary: #6B7280;
$border-color: #E5E7EB;
$bg-light: #F9FAFB;
$bg-dark: #111827;
.admin-layout {
height: 100vh;
min-height: 100vh;
background: $bg-light;
display: flex;
overflow: hidden;
}
.admin-layout-right {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.admin-sider {
background: white !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.04);
border-right: 1px solid $border-color;
display: flex;
flex-direction: column;
overflow: hidden;
:deep(.ant-layout-sider-children) {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
}
.sider-logo {
flex-shrink: 0;
height: 72px;
.sider-menu-wrap {
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.18);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.28);
}
}
.side-menu {
:deep(.ant-menu-item) {
margin: 4px 0;
padding-left: 12px !important;
padding-right: 12px !important;
border-radius: 10px;
height: 46px;
line-height: 46px;
color: #1F2937;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
padding: 12px 16px;
border-bottom: 1px solid $border-color;
background: linear-gradient(135deg, $bg-light 0%, white 100%);
.logo-img {
width: 50px;
height: 50px;
object-fit: contain;
}
.logo-text {
display: flex;
flex-direction: column;
margin-left: 10px;
line-height: 1.3;
.logo-title {
font-size: 15px;
font-weight: 600;
color: $primary-color;
white-space: nowrap;
}
.logo-subtitle {
font-size: 12px;
color: $text-secondary;
white-space: nowrap;
}
}
}
.sider-menu-wrap {
flex: 1;
min-height: 0;
overflow-y: auto;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.18);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.28);
}
}
.side-menu {
border-right: none !important;
padding: 12px 8px;
:deep(.ant-menu-item) {
margin: 4px 0;
padding-left: 12px !important;
padding-right: 12px !important;
border-radius: 10px;
height: 46px;
line-height: 46px;
color: $text-color;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
.ant-menu-title-content {
display: flex;
align-items: center;
.ant-menu-title-content {
display: flex;
align-items: center;
font-weight: 500;
}
// Lucide icon styles
svg {
transition: all 0.25s;
}
&:hover {
background: $primary-light;
color: $primary-color;
transform: translateX(2px);
svg {
color: $primary-color;
}
}
&.ant-menu-item-selected {
background: linear-gradient(135deg, $primary-color 0%, $primary-dark 100%);
color: white;
box-shadow: 0 4px 12px rgba($primary-color, 0.35);
&::after {
display: none;
}
svg {
color: white;
}
}
font-weight: 500;
}
}
}
.admin-header {
flex-shrink: 0;
position: sticky;
top: 0;
z-index: 100;
background: white;
padding: 0 24px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
border-bottom: 1px solid $border-color;
.trigger {
font-size: 18px;
cursor: pointer;
transition: color 0.25s;
color: $text-secondary;
svg {
transition: all 0.25s;
}
&:hover {
color: $primary-color;
background: #EEF2FF;
color: #6366F1;
transform: translateX(2px);
svg {
color: #6366F1;
}
}
}
.user-info {
padding: 0 12px;
}
&.ant-menu-item-selected {
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
color: white;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.35);
.user-avatar {
background: linear-gradient(135deg, $primary-color 0%, $primary-dark 100%);
}
&::after {
display: none;
}
.user-name {
color: $text-color;
font-weight: 500;
}
}
.admin-content {
flex: 1;
min-height: 0;
margin: 20px;
padding: 24px;
background: white;
border-radius: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
overflow-y: auto;
overflow-x: hidden;
}
.admin-content-mobile {
margin: 12px;
padding: 16px;
border-radius: 12px;
}
//
.admin-drawer {
.drawer-header {
display: flex;
align-items: center;
gap: 12px;
.drawer-logo {
width: 36px;
height: 36px;
object-fit: contain;
}
.drawer-title {
font-size: 16px;
font-weight: 600;
color: $text-color;
}
}
.drawer-menu {
border-right: none !important;
padding: 8px 0;
:deep(.ant-menu-item) {
margin: 4px 8px;
border-radius: 8px;
height: 48px;
line-height: 48px;
svg {
color: white;
}
}
}
}
.admin-mobile-header {
flex-shrink: 0;
position: sticky;
top: 0;
z-index: 100;
background: white;
padding: 0 16px;
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
border-bottom: 1px solid $border-color;
.menu-trigger {
font-size: 22px;
color: $text-secondary;
padding: 8px;
cursor: pointer;
}
.mobile-title {
font-size: 17px;
font-weight: 600;
color: $text-color;
}
.user-avatar {
background: linear-gradient(135deg, $primary-color 0%, $primary-dark 100%);
.drawer-menu {
:deep(.ant-menu-item) {
margin: 4px 8px;
border-radius: 8px;
height: 48px;
line-height: 48px;
}
}

View File

@ -20,7 +20,7 @@
>
<div v-if="fileList.length === 0">
<PlusOutlined />
<div style="margin-top: 8px">上传</div>
<div class="mt-2">上传</div>
</div>
</a-upload>
</a-form-item>
@ -60,10 +60,10 @@
:min="3"
:max="10"
/>
<span style="margin-left: 8px">次失败后锁定账号</span>
<span class="ml-2">次失败后锁定账号</span>
</a-form-item>
<a-form-item label="Token有效期">
<a-select v-model:value="securitySettings.tokenExpire" style="width: 200px">
<a-select v-model:value="securitySettings.tokenExpire" class="w-[200px]">
<a-select-option value="1d">1</a-select-option>
<a-select-option value="7d">7</a-select-option>
<a-select-option value="30d">30</a-select-option>
@ -129,7 +129,7 @@
:min="1"
:max="500"
/>
<span style="margin-left: 8px">MB</span>
<span class="ml-2">MB</span>
</a-form-item>
<a-form-item label="允许的文件类型">
<a-input v-model:value="storageSettings.allowedTypes" placeholder=".jpg,.png,.pdf,.doc,.docx" />
@ -290,14 +290,11 @@ onMounted(() => {
});
</script>
<style scoped lang="scss">
.settings-page {
:deep(.ant-card-head) {
border-bottom: 1px solid #f0f0f0;
}
:deep(.ant-form-item) {
margin-bottom: 24px;
}
<style scoped>
:deep(.ant-card-head) {
border-bottom: 1px solid #f0f0f0;
}
:deep(.ant-form-item) {
margin-bottom: 24px;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="package-detail-page">
<div class="p-6">
<a-card :bordered="false" :loading="loading">
<template #title>
<span>套餐详情</span>
@ -42,11 +42,11 @@
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'course'">
<div class="course-info">
<div class="flex items-center gap-3">
<img
v-if="record.course?.coverImagePath"
:src="record.course.coverImagePath"
class="course-cover"
class="w-12 h-12 object-cover rounded"
/>
<span>{{ record.course?.name }}</span>
</div>
@ -156,22 +156,3 @@ onMounted(() => {
fetchData();
});
</script>
<style scoped>
.package-detail-page {
padding: 24px;
}
.course-info {
display: flex;
align-items: center;
gap: 12px;
}
.course-cover {
width: 48px;
height: 48px;
object-fit: cover;
border-radius: 4px;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="package-edit-page">
<div class="p-6">
<a-card :bordered="false">
<template #title>
<span>{{ isEdit ? '编辑套餐' : '创建套餐' }}</span>
@ -23,22 +23,22 @@
</a-form-item>
<a-form-item label="价格(元)" name="price" :rules="[{ required: true, message: '请输入价格' }]">
<a-input-number v-model:value="form.price" :min="0" :precision="2" style="width: 200px" />
<a-input-number v-model:value="form.price" :min="0" :precision="2" class="w-[200px]" />
</a-form-item>
<a-form-item label="优惠价(元)" name="discountPrice">
<a-input-number v-model:value="form.discountPrice" :min="0" :precision="2" style="width: 200px" />
<a-input-number v-model:value="form.discountPrice" :min="0" :precision="2" class="w-[200px]" />
</a-form-item>
<a-form-item label="优惠类型" name="discountType">
<a-select v-model:value="form.discountType" placeholder="请选择优惠类型" allowClear style="width: 200px">
<a-select v-model:value="form.discountType" placeholder="请选择优惠类型" allowClear class="w-[200px]">
<a-select-option value="PERCENT">折扣</a-select-option>
<a-select-option value="FIXED">立减</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="适用年级" name="gradeLevels" :rules="[{ required: true, message: '请选择适用年级' }]">
<a-select v-model:value="form.gradeLevels" mode="multiple" placeholder="请选择适用年级" style="width: 300px">
<a-select v-model:value="form.gradeLevels" mode="multiple" placeholder="请选择适用年级" class="w-[300px]">
<a-select-option value="小班">小班</a-select-option>
<a-select-option value="中班">中班</a-select-option>
<a-select-option value="大班">大班</a-select-option>
@ -61,7 +61,7 @@
<a-input-number v-model:value="record.sortOrder" :min="0" size="small" />
</template>
<template v-else-if="column.key === 'gradeLevel'">
<a-select v-model:value="record.gradeLevel" size="small" style="width: 100px">
<a-select v-model:value="record.gradeLevel" size="small" class="w-[100px]">
<a-select-option value="小班">小班</a-select-option>
<a-select-option value="中班">中班</a-select-option>
<a-select-option value="大班">大班</a-select-option>
@ -72,7 +72,7 @@
</template>
</template>
</a-table>
<a-button type="dashed" block style="margin-top: 16px" @click="showCourseSelector = true">
<a-button type="dashed" block class="mt-4" @click="showCourseSelector = true">
<template #icon><PlusOutlined /></template>
添加课程包
</a-button>
@ -275,11 +275,5 @@ onMounted(() => {
</script>
<style scoped>
.package-edit-page {
padding: 24px;
}
.course-list {
width: 100%;
}
/* 仅保留 :deep / @keyframes / scrollbar / @media */
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="package-list-page">
<div class="p-6">
<a-card :bordered="false">
<template #title>
<span>课程套餐管理</span>
@ -12,7 +12,7 @@
</template>
<!-- 筛选 -->
<div class="filter-section">
<div class="mb-4">
<a-select
v-model:value="filters.status"
placeholder="状态筛选"
@ -221,13 +221,3 @@ onMounted(() => {
fetchData();
});
</script>
<style scoped>
.package-list-page {
padding: 24px;
}
.filter-section {
margin-bottom: 16px;
}
</style>

View File

@ -1,15 +1,15 @@
<template>
<div class="tenant-list-view">
<div class="bg-white p-6 rounded">
<a-page-header title="租户管理" sub-title="管理平台租户学校" />
<a-card :bordered="false" style="margin-top: 16px">
<a-card :bordered="false" class="mt-4">
<!-- 搜索表单 -->
<a-form layout="inline" :model="searchForm" style="margin-bottom: 16px">
<a-form layout="inline" :model="searchForm" class="mb-4">
<a-form-item label="关键词">
<a-input
v-model:value="searchForm.keyword"
placeholder="学校名称/账号/联系人"
allow-clear
style="width: 200px"
class="w-[200px]"
@pressEnter="handleSearch"
/>
</a-form-item>
@ -18,7 +18,7 @@
v-model:value="searchForm.status"
placeholder="全部状态"
allow-clear
style="width: 120px"
class="w-[120px]"
>
<a-select-option value="ACTIVE">生效中</a-select-option>
<a-select-option value="EXPIRED">已过期</a-select-option>
@ -30,7 +30,7 @@
v-model:value="searchForm.packageType"
placeholder="全部套餐"
allow-clear
style="width: 120px"
class="w-[120px]"
>
<a-select-option value="BASIC">基础版</a-select-option>
<a-select-option value="STANDARD">标准版</a-select-option>
@ -182,7 +182,7 @@
v-model:value="formData.teacherQuota"
:min="1"
:max="1000"
style="width: 100%"
class="w-full"
/>
</a-form-item>
<a-form-item label="学生配额" name="studentQuota">
@ -190,13 +190,13 @@
v-model:value="formData.studentQuota"
:min="1"
:max="10000"
style="width: 100%"
class="w-full"
/>
</a-form-item>
<a-form-item label="有效期" name="dateRange">
<a-range-picker
v-model:value="dateRange"
style="width: 100%"
class="w-full"
value-format="YYYY-MM-DD"
/>
</a-form-item>
@ -227,7 +227,7 @@
v-model:value="quotaForm.teacherQuota"
:min="currentTenant?.teacherCount || 1"
:max="1000"
style="width: 100%"
class="w-full"
/>
<div style="color: #999; font-size: 12px">
已使用: {{ currentTenant?.teacherCount || 0 }}
@ -238,7 +238,7 @@
v-model:value="quotaForm.studentQuota"
:min="currentTenant?.studentCount || 1"
:max="10000"
style="width: 100%"
class="w-full"
/>
<div style="color: #999; font-size: 12px">
已使用: {{ currentTenant?.studentCount || 0 }}
@ -699,9 +699,5 @@ onMounted(() => {
</script>
<style scoped>
.tenant-list-view {
background: #fff;
padding: 24px;
border-radius: 4px;
}
/* 仅保留 :deep / @keyframes / scrollbar / @media 等 */
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="theme-list-page">
<div class="p-6">
<a-card :bordered="false">
<template #title>
<span>主题字典管理</span>
@ -168,9 +168,3 @@ onMounted(() => {
fetchData();
});
</script>
<style scoped>
.theme-list-page {
padding: 24px;
}
</style>

View File

@ -1,28 +1,37 @@
<template>
<div class="login-container">
<div class="min-h-screen flex flex-col items-center justify-center bg-gradient-to-br from-[#FFF5F5] via-[#FFF0E6] to-[#F0F9FF] p-6 relative overflow-hidden">
<!-- 装饰元素 -->
<div class="decorations">
<div class="deco deco-1"></div>
<div class="deco deco-2"></div>
<div class="deco deco-3"></div>
<div class="absolute inset-0 pointer-events-none overflow-hidden">
<div class="absolute rounded-full opacity-60 w-[300px] h-[300px] bg-gradient-to-br from-[rgba(255,118,117,0.15)] to-[rgba(255,140,66,0.1)] -top-[100px] -right-[50px]"></div>
<div class="absolute rounded-full opacity-60 w-[200px] h-[200px] bg-gradient-to-br from-[rgba(116,185,255,0.15)] to-[rgba(108,92,231,0.1)] bottom-[100px] -left-[50px]"></div>
<div class="absolute rounded-full opacity-60 w-[150px] h-[150px] bg-gradient-to-br from-[rgba(0,217,165,0.12)] to-[rgba(116,185,255,0.08)] -bottom-[50px] right-[20%]"></div>
</div>
<!-- 登录卡片 -->
<div class="login-card">
<div class="w-full max-w-[400px] bg-white rounded-3xl py-10 px-9 shadow-lg relative z-[1] sm:p-8 sm:px-6 sm:rounded-[20px]">
<!-- Logo区域 -->
<div class="logo-section">
<img src="/logo.png" alt="Logo" class="logo-img" />
<div class="brand-info">
<h1>少儿智慧阅读</h1>
<p>读启智慧阅见未来</p>
<div class="flex flex-col items-center mb-8">
<img src="/logo.png" alt="Logo" class="w-[75px] h-[75px] object-contain mb-4 -mt-1" />
<div class="text-center">
<h1 class="text-[22px] font-semibold text-[#2D3436] m-0 mb-1 tracking-wide">少儿智慧阅读</h1>
<p class="text-[13px] text-[#636E72] m-0">读启智慧阅见未来</p>
</div>
</div>
<!-- 角色选择 -->
<div class="role-section">
<div v-for="role in roles" :key="role.value" :class="['role-btn', { active: formState.role === role.value }]"
@click="selectRole(role.value)">
<component :is="role.icon" />
<div class="grid grid-cols-4 gap-2 mb-6">
<div
v-for="role in roles"
:key="role.value"
:class="[
'flex flex-col items-center gap-1.5 py-3 px-2 bg-[#FAFAFA] border-[1.5px] border-transparent rounded-xl cursor-pointer transition-all duration-200 text-[12px] text-[#636E72]',
formState.role === role.value
? 'bg-[#FFF4EB] border-[#FF8C42] text-[#FF8C42] font-medium'
: 'hover:bg-[#FFF4EB] hover:border-[rgba(255,140,66,0.3)] hover:text-[#FF8C42]'
]"
@click="selectRole(role.value)"
>
<component :is="role.icon" class="text-[20px]" />
<span>{{ role.label }}</span>
</div>
</div>
@ -46,17 +55,22 @@
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit" size="large" block :loading="loading" class="login-btn">
<a-button type="primary" html-type="submit" size="large" block :loading="loading" class="h-11 rounded-xl text-[15px] font-medium bg-gradient-to-br from-[#FF8C42] to-[#FF9F5A] border-0 shadow-[0_4px_12px_rgba(255,140,66,0.25)] mt-2 hover:from-[#FF7B2E] hover:to-[#FF8C42] hover:-translate-y-px hover:shadow-[0_6px_16px_rgba(255,140,66,0.3)] active:translate-y-0">
登录
</a-button>
</a-form-item>
</a-form>
<!-- 测试账号 -->
<div class="test-section">
<div class="test-title">快速体验</div>
<div class="test-list">
<span v-for="acc in testAccounts" :key="acc.role" class="test-item" @click="fillAccount(acc)">
<div class="mt-6 pt-5 border-t border-[#F5F5F5]">
<div class="text-[12px] text-[#B2BEC3] mb-2.5 text-center">快速体验</div>
<div class="flex flex-wrap justify-center gap-2">
<span
v-for="acc in testAccounts"
:key="acc.role"
class="text-[11px] text-[#636E72] bg-[#F8F8F8] py-1 px-2.5 rounded-[20px] cursor-pointer transition-all duration-200 hover:bg-[#FFF4EB] hover:text-[#FF8C42]"
@click="fillAccount(acc)"
>
{{ acc.label }}: {{ acc.account }}
</span>
</div>
@ -64,7 +78,7 @@
</div>
<!-- 底部版权 -->
<div class="footer">
<div class="mt-6 text-[12px] text-[#B2BEC3]">
<span>© 2026 少儿智慧阅读服务平台</span>
</div>
</div>
@ -133,268 +147,45 @@ const handleLogin = async () => {
}
};
</script>
<style scoped lang="scss">
//
$primary: #FF8C42; //
$primary-light: #FFF4EB;
$secondary: #6C5CE7; //
$accent: #00D9A5; // 绿
$coral: #FF7675; //
$sky: #74B9FF; //
$text-dark: #2D3436;
$text-gray: #636E72;
$text-light: #B2BEC3;
$bg-cream: #FEFEFE;
.login-container {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #FFF5F5 0%, #FFF0E6 50%, #F0F9FF 100%);
padding: 24px;
position: relative;
overflow: hidden;
<style scoped>
/* Ant Design 表单覆盖,仅保留无法用原子类表达的 :deep 选择器 */
.login-form :deep(.ant-form-item) {
margin-bottom: 16px;
}
//
.decorations {
position: absolute;
inset: 0;
pointer-events: none;
overflow: hidden;
.login-form :deep(.ant-form-item-label) {
display: none;
}
.deco {
position: absolute;
border-radius: 50%;
opacity: 0.6;
&.deco-1 {
width: 300px;
height: 300px;
background: linear-gradient(135deg, rgba($coral, 0.15), rgba($primary, 0.1));
top: -100px;
right: -50px;
}
&.deco-2 {
width: 200px;
height: 200px;
background: linear-gradient(135deg, rgba($sky, 0.15), rgba($secondary, 0.1));
bottom: 100px;
left: -50px;
}
&.deco-3 {
width: 150px;
height: 150px;
background: linear-gradient(135deg, rgba($accent, 0.12), rgba($sky, 0.08));
bottom: -50px;
right: 20%;
}
}
//
.login-card {
width: 100%;
max-width: 400px;
background: white;
border-radius: 24px;
padding: 40px 36px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02);
position: relative;
z-index: 1;
@media (max-width: 480px) {
padding: 32px 24px;
border-radius: 20px;
}
}
// Logo
.logo-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 32px;
.logo-img {
width: 75px;
height: 75px;
object-fit: contain;
margin-bottom: 16px;
margin-top: -5px;
}
.brand-info {
text-align: center;
h1 {
font-size: 22px;
font-weight: 600;
color: $text-dark;
margin: 0 0 4px 0;
letter-spacing: 1px;
}
p {
font-size: 13px;
color: $text-gray;
margin: 0;
}
}
}
//
.role-section {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin-bottom: 24px;
}
.role-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 12px 8px;
background: #FAFAFA;
border: 1.5px solid transparent;
.login-form :deep(.ant-input-affix-wrapper),
.login-form :deep(.ant-input) {
height: 44px;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 12px;
color: $text-gray;
.anticon {
font-size: 20px;
}
&:hover {
background: $primary-light;
border-color: rgba($primary, 0.3);
color: $primary;
}
&.active {
background: $primary-light;
border-color: $primary;
color: $primary;
font-weight: 500;
}
border-color: #eee;
background: #fafafa;
font-size: 14px;
display: flex;
align-items: center;
}
//
.login-form {
:deep(.ant-form-item) {
margin-bottom: 16px;
.ant-form-item-label {
display: none;
}
.ant-input-affix-wrapper,
.ant-input {
height: 44px;
border-radius: 12px;
border-color: #EEE;
background: #FAFAFA;
font-size: 14px;
display: flex;
align-items: center;
&:hover,
&:focus,
&.ant-input-affix-wrapper-focused {
border-color: $primary;
background: white;
box-shadow: 0 0 0 3px rgba($primary, 0.08);
}
&::placeholder {
color: $text-light;
}
input {
height: 100%;
display: flex;
align-items: center;
}
}
.ant-input-prefix {
color: $text-light;
margin-right: 8px;
display: flex;
align-items: center;
}
}
.login-btn {
height: 44px;
border-radius: 12px;
font-size: 15px;
font-weight: 500;
background: linear-gradient(135deg, $primary 0%, #FF9F5A 100%);
border: none;
box-shadow: 0 4px 12px rgba($primary, 0.25);
margin-top: 8px;
&:hover {
background: linear-gradient(135deg, #FF7B2E 0%, $primary 100%);
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba($primary, 0.3);
}
&:active {
transform: translateY(0);
}
}
.login-form :deep(.ant-input-affix-wrapper:hover),
.login-form :deep(.ant-input-affix-wrapper:focus),
.login-form :deep(.ant-input-affix-wrapper.ant-input-affix-wrapper-focused),
.login-form :deep(.ant-input:hover),
.login-form :deep(.ant-input:focus) {
border-color: #ff8c42;
background: white;
box-shadow: 0 0 0 3px rgba(255, 140, 66, 0.08);
}
//
.test-section {
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid #F5F5F5;
.test-title {
font-size: 12px;
color: $text-light;
margin-bottom: 10px;
text-align: center;
}
.test-list {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
}
.test-item {
font-size: 11px;
color: $text-gray;
background: #F8F8F8;
padding: 4px 10px;
border-radius: 20px;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: $primary-light;
color: $primary;
}
}
.login-form :deep(.ant-input::placeholder) {
color: #b2bec3;
}
//
.footer {
margin-top: 24px;
font-size: 12px;
color: $text-light;
.login-form :deep(.ant-input-affix-wrapper input) {
height: 100%;
display: flex;
align-items: center;
}
.login-form :deep(.ant-input-prefix) {
color: #b2bec3;
margin-right: 8px;
display: flex;
align-items: center;
}
</style>

View File

@ -1,34 +1,34 @@
<template>
<div class="parent-dashboard">
<div class="parent-dashboard min-h-100vh p-4">
<!-- 欢迎横幅 -->
<div class="welcome-banner">
<div class="banner-content">
<div class="banner-text">
<h1>
<HomeOutlined class="banner-icon" />
<span class="banner-title-text">家长中心</span>
<div class="rounded-2xl py-6 px-8 mb-6 bg-[linear-gradient(135deg,#52c41a_0%,#73d13d_100%)] text-white welcome-banner">
<div class="flex justify-between items-center">
<div>
<h1 class="text-2xl m-0 mb-2 flex items-center gap-2">
<HomeOutlined class="text-[28px]" />
<span>家长中心</span>
</h1>
<p>关注孩子的阅读成长陪伴每一步进步</p>
<p class="m-0 opacity-90 text-sm">关注孩子的阅读成长陪伴每一步进步</p>
</div>
<div class="banner-decorations">
<span class="decoration"><BookOutlined /></span>
<span class="decoration"><StarOutlined /></span>
<span class="decoration"><HeartOutlined /></span>
<div class="flex gap-4 text-[32px] opacity-80 banner-decorations">
<span><BookOutlined /></span>
<span><StarOutlined /></span>
<span><HeartOutlined /></span>
</div>
</div>
</div>
<a-spin :spinning="loading">
<!-- 我的孩子 -->
<div class="section-card">
<div class="section-header">
<h3><TeamOutlined /> 我的孩子</h3>
<div class="bg-white rounded-xl p-5 mb-6 shadow-[0_2px_8px_rgba(0,0,0,0.04)] section-card">
<div class="flex justify-between items-center mb-4">
<h3 class="m-0 text-lg flex items-center gap-2 text-[#333]"><TeamOutlined /> 我的孩子</h3>
</div>
<div class="children-grid" v-if="children.length > 0">
<div class="flex flex-col gap-3 children-grid" v-if="children.length > 0">
<div
v-for="child in children"
:key="child.id"
class="child-card"
class="flex items-center py-4 px-4 bg-[#fafafa] rounded-xl cursor-pointer transition-all duration-300 hover:bg-[#f6ffed] active:scale-[0.98] group child-card"
@click="goToChildDetail(child.id)"
>
<div class="child-avatar">
@ -36,73 +36,76 @@
{{ child.name.charAt(0) }}
</a-avatar>
</div>
<div class="child-info">
<div class="child-name">
<div class="flex-1 ml-4 min-w-0 child-info">
<div class="text-base font-600 text-[#333] child-name">
{{ child.name }}
<span class="relationship">{{ getRelationshipText(child.relationship) }}</span>
<span class="ml-2 text-xs text-[#999] font-normal">{{ 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 class="text-[13px] text-[#666] my-1">{{ child.class?.name || '未分班' }}</div>
<div class="text-xs text-[#999] flex gap-4 flex-wrap child-stats">
<span class="flex items-center gap-1"><BookOutlined /> {{ child.readingCount }} 次阅读</span>
<span class="flex items-center gap-1"><ReadOutlined /> {{ child.lessonCount }} 节课</span>
</div>
</div>
<div class="card-arrow">
<div class="text-[#d9d9d9] text-base transition-colors duration-300 group-hover:text-[#52c41a] card-arrow">
<RightOutlined />
</div>
</div>
</div>
<div class="empty-state" v-else>
<InboxOutlined class="empty-icon" />
<p>暂无孩子信息</p>
<p class="empty-hint">请联系学校添加孩子信息</p>
<div class="text-center py-8 text-[#999] empty-state" v-else>
<InboxOutlined class="text-[48px] text-[#d9d9d9] mb-3 empty-icon" />
<p class="my-1">暂无孩子信息</p>
<p class="text-[13px] text-[#bfbfbf]">请联系学校添加孩子信息</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 class="bg-white rounded-xl p-5 mb-6 shadow-[0_2px_8px_rgba(0,0,0,0.04)] section-card">
<div class="flex justify-between items-center mb-4">
<h3 class="m-0 text-lg flex items-center gap-2 text-[#333]"><CheckSquareOutlined /> 最近任务</h3>
<a-button type="link" @click="goToTasks" class="!p-0 text-sm">查看全部</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)">
<div class="flex flex-col gap-3 task-list" v-if="recentTasks.length > 0">
<div v-for="task in recentTasks" :key="task.id" class="flex items-center py-3 px-3 bg-[#fafafa] rounded-lg gap-3 cursor-pointer transition-all duration-300 hover:bg-[#f6ffed] active:scale-[0.98] task-item">
<div
class="py-1 px-2 rounded text-xs font-500 whitespace-nowrap"
:class="getStatusClass(task.status)"
>
{{ getStatusText(task.status) }}
</div>
<div class="task-content">
<div class="task-title">{{ task.task.title }}</div>
<div class="task-deadline">
<div class="flex-1 min-w-0">
<div class="text-sm font-500 text-[#333] truncate">{{ task.task.title }}</div>
<div class="text-xs text-[#999] mt-1 flex items-center gap-1">
<ClockCircleOutlined />
<span>截止{{ formatDate(task.task.endDate) }}</span>
</div>
</div>
</div>
</div>
<div class="empty-state compact" v-else>
<p>暂无任务</p>
<div class="text-center py-6 text-[#999] empty-state compact" v-else>
<p class="m-0">暂无任务</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 class="bg-white rounded-xl p-5 mb-6 shadow-[0_2px_8px_rgba(0,0,0,0.04)] section-card">
<div class="flex justify-between items-center mb-4">
<h3 class="m-0 text-lg flex items-center gap-2 text-[#333]"><FileImageOutlined /> 成长档案</h3>
<a-button type="link" @click="goToGrowth" class="!p-0 text-sm">查看全部</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 class="flex flex-col gap-3 growth-list" v-if="recentGrowth.length > 0">
<div v-for="record in recentGrowth" :key="record.id" class="flex items-center py-3 px-3 bg-[#fafafa] rounded-lg gap-3 cursor-pointer transition-all duration-300 hover:bg-[#f6ffed] active:scale-[0.98] growth-item">
<div class="w-12 h-12 rounded-lg overflow-hidden shrink-0" v-if="record.images && record.images.length > 0">
<img :src="record.images[0]" alt="成长照片" class="w-full h-full object-cover" />
</div>
<div class="growth-content">
<div class="growth-title">{{ record.title }}</div>
<div class="growth-date">{{ formatDate(record.recordDate) }}</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-500 text-[#333] truncate">{{ record.title }}</div>
<div class="text-xs text-[#999] mt-1 flex items-center gap-1">{{ formatDate(record.recordDate) }}</div>
</div>
</div>
</div>
<div class="empty-state compact" v-else>
<p>暂无成长记录</p>
<div class="text-center py-6 text-[#999] empty-state compact" v-else>
<p class="m-0">暂无成长记录</p>
</div>
</div>
</a-spin>
@ -152,9 +155,9 @@ const getRelationshipText = (relationship: string) => {
};
const statusMap: Record<string, { text: string; class: string }> = {
PENDING: { text: '待完成', class: 'status-pending' },
IN_PROGRESS: { text: '进行中', class: 'status-progress' },
COMPLETED: { text: '已完成', class: 'status-completed' },
PENDING: { text: '待完成', class: 'bg-[#fff7e6] text-[#fa8c16]' },
IN_PROGRESS: { text: '进行中', class: 'bg-[#e6f7ff] text-[#1890ff]' },
COMPLETED: { text: '已完成', class: 'bg-[#f6ffed] text-[#52c41a]' },
};
const getStatusText = (status: string) => statusMap[status]?.text || status;
@ -206,387 +209,27 @@ onUnmounted(() => {
});
</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;
}
}
}
// =============== ===============
<style scoped>
@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;
}
}
.parent-dashboard { padding: 16px; }
.welcome-banner {
padding: 20px 16px !important;
margin-bottom: 16px !important;
border-radius: 12px !important;
}
.welcome-banner h1 { font-size: 20px; }
.welcome-banner .banner-decorations { gap: 12px; font-size: 24px; }
.section-card { padding: 16px !important; margin-bottom: 16px !important; }
.section-card .section-header { margin-bottom: 12px; }
.section-card .section-header h3 { font-size: 16px; }
.child-card { padding: 14px !important; }
.child-info { margin-left: 14px !important; }
.child-name { font-size: 15px; }
.empty-state { padding: 24px; }
.empty-state.compact { padding: 20px; }
.empty-state .empty-icon { font-size: 40px; }
}
//
@media screen and (min-width: 769px) and (max-width: 1024px) {
.parent-dashboard {
.welcome-banner {
padding: 22px 28px;
}
}
.welcome-banner { padding: 22px 28px !important; }
}
</style>

View File

@ -1,30 +1,30 @@
<template>
<a-layout class="parent-layout">
<a-layout class="parent-layout h-screen min-h-screen bg-[#FAFAFA] flex overflow-hidden">
<!-- 桌面端侧边栏 -->
<a-layout-sider
v-if="!isMobile"
v-model:collapsed="collapsed"
:trigger="null"
collapsible
class="parent-sider"
class="parent-sider bg-white! shadow-[2px_0_8px_rgba(0,0,0,0.06)] border-r border-[#E8E8E8] flex flex-col overflow-hidden"
:width="220"
>
<div class="sider-logo">
<img src="/logo.png" alt="Logo" class="logo-img" />
<div v-if="!collapsed" class="logo-text">
<span class="logo-title">少儿智慧阅读</span>
<span class="logo-subtitle">家长端</span>
<div class="shrink-0 h-20 flex items-center justify-center py-3 px-4 border-b border-[#E8E8E8] bg-gradient-to-br from-[#f6ffed] to-white">
<img src="/logo.png" alt="Logo" class="w-11 h-11 object-contain shrink-0" />
<div v-if="!collapsed" class="flex flex-col ml-3 leading-snug">
<span class="text-sm font-semibold text-[#333]">少儿智慧阅读</span>
<span class="text-[11px] text-[#666]">家长端</span>
</div>
</div>
<div class="sider-menu-wrap">
<div class="sider-menu-wrap flex-1 min-h-0 overflow-y-auto">
<a-menu
v-model:selectedKeys="selectedKeys"
mode="inline"
theme="light"
:inline-collapsed="collapsed"
@click="handleMenuClick"
class="side-menu"
class="side-menu border-r-0! py-2 px-3"
>
<a-menu-item key="dashboard">
<template #icon><HomeOutlined /></template>
@ -58,13 +58,14 @@
:closable="false"
:width="260"
class="mobile-drawer"
:body-style="{ padding: 0 }"
>
<template #title>
<div class="drawer-header">
<img src="/logo.png" alt="Logo" class="drawer-logo" />
<div class="drawer-title">
<span class="title-main">少儿智慧阅读</span>
<span class="title-sub">家长端</span>
<div class="flex items-center py-2">
<img src="/logo.png" alt="Logo" class="w-10 h-10 object-contain" />
<div class="flex flex-col ml-3">
<span class="text-[15px] font-semibold text-[#333]">少儿智慧阅读</span>
<span class="text-xs text-[#666]">家长端</span>
</div>
</div>
</template>
@ -73,7 +74,7 @@
mode="inline"
theme="light"
@click="handleMobileMenuClick"
class="drawer-menu"
class="drawer-menu border-r-0!"
>
<a-menu-item key="dashboard">
<template #icon><HomeOutlined /></template>
@ -97,25 +98,25 @@
</a-menu-item>
</a-menu>
<div class="drawer-footer">
<a-button block @click="handleLogout" class="logout-btn">
<div class="absolute bottom-0 left-0 right-0 p-4">
<a-button block @click="handleLogout" class="h-11 rounded-lg text-[#ff4d4f] border-[#ff4d4f] hover:text-[#ff7875] hover:border-[#ff7875]">
<LogoutOutlined /> 退出登录
</a-button>
</div>
</a-drawer>
<a-layout class="main-layout">
<a-layout class="flex-1 min-h-0 flex flex-col overflow-hidden">
<!-- 桌面端顶部栏 -->
<a-layout-header v-if="!isMobile" class="parent-header">
<a-layout-header v-if="!isMobile" class="shrink-0 sticky top-0 z-100 bg-white px-6 flex justify-between items-center shadow-sm border-b border-[#E8E8E8] h-16">
<div class="header-left">
<MenuUnfoldOutlined
v-if="collapsed"
class="trigger"
class="text-lg cursor-pointer transition-colors text-[#666] hover:text-[#52c41a]"
@click="collapsed = !collapsed"
/>
<MenuFoldOutlined
v-else
class="trigger"
class="text-lg cursor-pointer transition-colors text-[#666] hover:text-[#52c41a]"
@click="collapsed = !collapsed"
/>
</div>
@ -125,11 +126,11 @@
<NotificationBell />
<a-dropdown>
<a-space class="user-info" style="cursor: pointer;">
<a-avatar :size="32" class="user-avatar">
<a-space class="px-3 cursor-pointer">
<a-avatar :size="32" class="bg-gradient-to-br from-[#52c41a] to-[#389e0d]">
<template #icon><UserOutlined /></template>
</a-avatar>
<span class="user-name">{{ userName }}</span>
<span class="text-[#333] font-medium">{{ userName }}</span>
<DownOutlined />
</a-space>
<template #overlay>
@ -151,28 +152,29 @@
</a-layout-header>
<!-- 移动端顶部栏 -->
<a-layout-header v-if="isMobile" class="mobile-header">
<div class="mobile-header-content">
<MenuOutlined class="menu-icon" @click="drawerVisible = true" />
<span class="page-title">{{ pageTitle }}</span>
<a-layout-header v-if="isMobile" class="bg-white px-4 h-14 border-b border-[#E8E8E8] sticky top-0 z-100 md:hidden">
<div class="flex items-center justify-between h-full">
<MenuOutlined class="text-[22px] text-[#333] p-2 cursor-pointer" @click="drawerVisible = true" />
<span class="text-[17px] font-semibold text-[#333]">{{ pageTitle }}</span>
<NotificationBell icon-color="#333" />
</div>
</a-layout-header>
<a-layout-content :class="['parent-content', { 'mobile-content': isMobile }]">
<a-layout-content :class="['flex-1 min-h-0 bg-white rounded-xl shadow-sm overflow-y-auto', isMobile ? 'm-3 p-4 mb-[70px]' : 'm-5 p-6']">
<router-view />
</a-layout-content>
<!-- 移动端底部导航 -->
<div v-if="isMobile" class="mobile-bottom-nav">
<div v-if="isMobile" class="fixed bottom-0 left-0 right-0 h-[60px] bg-white flex justify-around items-center shadow-[0_-2px_10px_rgba(0,0,0,0.08)] border-t border-[#E8E8E8] z-1000 pb-[env(safe-area-inset-bottom)] md:hidden">
<div
v-for="nav in navItems"
:key="nav.key"
:class="['nav-item', { active: selectedKeys[0] === nav.key }]"
:class="['flex flex-col items-center justify-center flex-1 h-full cursor-pointer transition-all', selectedKeys[0] === nav.key ? 'text-[#52c41a]' : 'text-[#666]']"
@click="handleNavClick(nav.key)"
class="active:bg-black/5"
>
<component :is="nav.icon" class="nav-icon" />
<span class="nav-text">{{ nav.text }}</span>
<component :is="nav.icon" :class="['text-[22px] mb-0.5', selectedKeys[0] === nav.key ? 'scale-110' : '']" />
<span class="text-[11px]">{{ nav.text }}</span>
</div>
</div>
</a-layout>
@ -289,365 +291,85 @@ const handleLogout = () => {
</script>
<style scoped lang="scss">
$primary-color: #52c41a;
$primary-light: #f6ffed;
$primary-dark: #389e0d;
$text-color: #333333;
$text-secondary: #666666;
$border-color: #E8E8E8;
$bg-light: #FAFAFA;
.parent-layout {
height: 100vh;
min-height: 100vh;
background: $bg-light;
display: flex;
overflow: hidden;
}
.main-layout {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
//
.parent-sider {
background: white !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.06);
border-right: 1px solid $border-color;
display: flex;
flex-direction: column;
overflow: hidden;
:deep(.ant-layout-sider-children) {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
}
.sider-logo {
flex-shrink: 0;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
padding: 12px 16px;
border-bottom: 1px solid $border-color;
background: linear-gradient(135deg, $primary-light 0%, white 100%);
.logo-img {
width: 44px;
height: 44px;
object-fit: contain;
flex-shrink: 0;
}
.logo-text {
display: flex;
flex-direction: column;
margin-left: 12px;
line-height: 1.4;
.logo-title {
font-size: 14px;
font-weight: 600;
color: $text-color;
}
.logo-subtitle {
font-size: 11px;
color: $text-secondary;
}
}
.sider-menu-wrap {
&::-webkit-scrollbar {
width: 4px;
}
.sider-menu-wrap {
flex: 1;
min-height: 0;
overflow-y: auto;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.18);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.28);
}
&::-webkit-scrollbar-track {
background: transparent;
}
.side-menu {
border-right: none !important;
padding: 8px 12px;
:deep(.ant-menu-item) {
margin: 4px 0;
padding-left: 12px !important;
padding-right: 12px !important;
border-radius: 8px;
height: 44px;
line-height: 44px;
color: $text-color;
transition: all 0.3s;
&:hover {
background: $primary-light;
color: $primary-color;
}
&.ant-menu-item-selected {
background: linear-gradient(135deg, $primary-color 0%, $primary-dark 100%);
color: white;
&::after {
display: none;
}
.anticon {
color: white;
}
}
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.18);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.28);
}
}
//
.parent-header {
flex-shrink: 0;
position: sticky;
top: 0;
z-index: 100;
background: white;
padding: 0 24px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
border-bottom: 1px solid $border-color;
height: 64px;
.trigger {
font-size: 18px;
cursor: pointer;
transition: color 0.3s;
color: $text-secondary;
.side-menu {
:deep(.ant-menu-item) {
margin: 4px 0;
padding-left: 12px !important;
padding-right: 12px !important;
border-radius: 8px;
height: 44px;
line-height: 44px;
color: #333333;
transition: all 0.3s;
&:hover {
color: $primary-color;
}
}
.user-info {
padding: 0 12px;
}
.user-avatar {
background: linear-gradient(135deg, $primary-color 0%, $primary-dark 100%);
}
.user-name {
color: $text-color;
font-weight: 500;
}
}
//
.parent-content {
flex: 1;
min-height: 0;
margin: 20px;
padding: 24px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
overflow-y: auto;
}
// =============== ===============
//
.mobile-drawer {
.drawer-header {
display: flex;
align-items: center;
padding: 8px 0;
.drawer-logo {
width: 40px;
height: 40px;
object-fit: contain;
background: #f6ffed;
color: #52c41a;
}
.drawer-title {
display: flex;
flex-direction: column;
margin-left: 12px;
&.ant-menu-item-selected {
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
color: white;
.title-main {
font-size: 15px;
font-weight: 600;
color: $text-color;
&::after {
display: none;
}
.title-sub {
font-size: 12px;
color: $text-secondary;
}
}
}
.drawer-menu {
border-right: none !important;
:deep(.ant-menu-item) {
margin: 4px 0;
padding-left: 16px !important;
border-radius: 8px;
height: 48px;
line-height: 48px;
font-size: 15px;
&.ant-menu-item-selected {
background: $primary-light;
color: $primary-color;
&::after {
display: none;
}
}
}
}
.drawer-footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 16px;
.logout-btn {
height: 44px;
border-radius: 8px;
color: #ff4d4f;
border-color: #ff4d4f;
&:hover {
color: #ff7875;
border-color: #ff7875;
.anticon {
color: white;
}
}
}
}
//
.mobile-header {
background: white;
padding: 0 16px;
height: 56px;
border-bottom: 1px solid $border-color;
position: sticky;
top: 0;
z-index: 100;
.drawer-menu {
:deep(.ant-menu-item) {
margin: 4px 0;
padding-left: 16px !important;
border-radius: 8px;
height: 48px;
line-height: 48px;
font-size: 15px;
.mobile-header-content {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
&.ant-menu-item-selected {
background: #f6ffed;
color: #52c41a;
.menu-icon {
font-size: 22px;
color: $text-color;
padding: 8px;
cursor: pointer;
}
.page-title {
font-size: 17px;
font-weight: 600;
color: $text-color;
}
.notification-badge {
padding: 8px;
cursor: pointer;
}
}
}
//
.mobile-content {
margin: 12px;
padding: 16px;
border-radius: 12px;
margin-bottom: 70px;
}
//
.mobile-bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 60px;
background: white;
display: flex;
justify-content: space-around;
align-items: center;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.08);
border-top: 1px solid $border-color;
z-index: 1000;
padding-bottom: env(safe-area-inset-bottom);
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
height: 100%;
cursor: pointer;
transition: all 0.3s;
color: $text-secondary;
.nav-icon {
font-size: 22px;
margin-bottom: 2px;
}
.nav-text {
font-size: 11px;
}
&.active {
color: $primary-color;
.nav-icon {
transform: scale(1.1);
&::after {
display: none;
}
}
&:active {
background: rgba(0, 0, 0, 0.02);
}
}
}
//
@media screen and (max-width: 768px) {
.parent-layout {
.ant-layout-sider {
display: none;
}
.parent-layout :deep(.ant-layout-sider) {
display: none;
}
}
@ -655,20 +377,5 @@ $bg-light: #FAFAFA;
.mobile-drawer {
display: none;
}
.mobile-header {
display: none;
}
.mobile-bottom-nav {
display: none;
}
}
//
@media screen and (min-width: 769px) and (max-width: 1024px) {
.parent-content {
padding: 20px;
}
}
</style>

View File

@ -1,72 +1,72 @@
<template>
<div class="children-view">
<div class="page-header">
<h2><TeamOutlined /> 我的孩子</h2>
<p class="page-desc">查看孩子的学习信息和成长记录</p>
<div class="children-view min-h-100vh p-4">
<div class="mb-6 page-header">
<h2 class="m-0 mb-2 text-2xl flex items-center gap-2"><TeamOutlined /> 我的孩子</h2>
<p class="m-0 text-[#999]">查看孩子的学习信息和成长记录</p>
</div>
<a-spin :spinning="loading">
<div class="children-list" v-if="children.length > 0">
<div class="flex flex-col gap-4 children-list" v-if="children.length > 0">
<div
v-for="child in children"
:key="child.id"
class="child-card"
class="flex items-center py-6 px-6 bg-white border border-[#f0f0f0] rounded-2xl cursor-pointer transition-all duration-200 hover:border-[#52c41a] hover:shadow-[0_4px_12px_rgba(82,196,26,0.1)] active:scale-[0.98] group child-card"
@click="goToChildDetail(child.id)"
>
<div class="card-avatar">
<div class="relative mr-6 card-avatar">
<a-avatar :size="80" :style="{ backgroundColor: getAvatarColor(child.id) }">
{{ child.name.charAt(0) }}
</a-avatar>
<span class="gender-badge" v-if="child.gender">
<span class="absolute bottom-0 right-0 text-xl" v-if="child.gender">
{{ child.gender === '男' ? '👦' : '👧' }}
</span>
</div>
<div class="card-content">
<div class="card-header">
<h3 class="child-name">{{ child.name }}</h3>
<div class="flex-1 card-content">
<div class="flex items-center gap-3 mb-3">
<h3 class="m-0 text-xl font-600">{{ child.name }}</h3>
<a-tag color="green">{{ getRelationshipText(child.relationship) }}</a-tag>
</div>
<div class="card-info">
<div class="info-item">
<span class="label">班级</span>
<span class="value">{{ child.class?.name || '未分班' }}</span>
<div class="flex gap-6 mb-4 card-info">
<div class="flex flex-col gap-1">
<span class="text-xs text-[#999]">班级</span>
<span class="text-sm text-[#333]">{{ child.class?.name || '未分班' }}</span>
</div>
<div class="info-item">
<span class="label">年级</span>
<span class="value">{{ child.class?.grade || '-' }}</span>
<div class="flex flex-col gap-1">
<span class="text-xs text-[#999]">年级</span>
<span class="text-sm text-[#333]">{{ child.class?.grade || '-' }}</span>
</div>
<div class="info-item" v-if="child.birthDate">
<span class="label">生日</span>
<span class="value">{{ formatDate(child.birthDate) }}</span>
<div class="flex flex-col gap-1" v-if="child.birthDate">
<span class="text-xs text-[#999]">生日</span>
<span class="text-sm text-[#333]">{{ formatDate(child.birthDate) }}</span>
</div>
</div>
<div class="card-stats">
<div class="stat-item">
<BookOutlined />
<span class="stat-value">{{ child.readingCount }}</span>
<span class="stat-label">阅读次数</span>
<div class="flex gap-8 card-stats">
<div class="flex items-center gap-2 py-2 px-4 bg-[#fafafa] rounded-lg">
<BookOutlined class="text-[18px] text-[#52c41a]" />
<span class="text-xl font-600 text-[#333]">{{ child.readingCount }}</span>
<span class="text-xs text-[#999]">阅读次数</span>
</div>
<div class="stat-item">
<ReadOutlined />
<span class="stat-value">{{ child.lessonCount }}</span>
<span class="stat-label">上课次数</span>
<div class="flex items-center gap-2 py-2 px-4 bg-[#fafafa] rounded-lg">
<ReadOutlined class="text-[18px] text-[#52c41a]" />
<span class="text-xl font-600 text-[#333]">{{ child.lessonCount }}</span>
<span class="text-xs text-[#999]">上课次数</span>
</div>
</div>
</div>
<div class="card-action">
<div class="text-xl text-[#d9d9d9] py-0 px-3 transition-colors duration-200 group-hover:text-[#52c41a] card-action">
<RightOutlined />
</div>
</div>
</div>
<div class="empty-state" v-else>
<InboxOutlined class="empty-icon" />
<p>暂无孩子信息</p>
<p class="empty-hint">请联系学校添加孩子信息</p>
<div class="text-center py-[60px] px-5 text-[#999] empty-state" v-else>
<InboxOutlined class="text-[64px] text-[#d9d9d9] mb-4" />
<p class="my-1">暂无孩子信息</p>
<p class="text-[13px] text-[#bfbfbf]">请联系学校添加孩子信息</p>
</div>
</a-spin>
</div>
@ -127,281 +127,52 @@ onMounted(() => {
});
</script>
<style scoped lang="scss">
$primary-color: #52c41a;
$primary-light: #f6ffed;
.children-view {
.page-header {
margin-bottom: 24px;
h2 {
margin: 0 0 8px;
font-size: 24px;
display: flex;
align-items: center;
gap: 8px;
}
.page-desc {
margin: 0;
color: #999;
}
}
<style scoped>
@media screen and (max-width: 768px) {
.children-list {
display: flex;
flex-direction: column;
gap: 16px;
gap: 12px;
}
.child-card {
display: flex;
align-items: center;
padding: 24px;
background: white;
border: 1px solid #f0f0f0;
border-radius: 16px;
cursor: pointer;
transition: all 0.2s;
padding: 16px !important;
border-radius: 12px !important;
}
&:hover {
border-color: $primary-color;
box-shadow: 0 4px 12px rgba(82, 196, 26, 0.1);
.card-avatar {
margin-right: 14px !important;
}
.card-action {
color: $primary-color;
}
}
.card-avatar :deep(.ant-avatar) {
width: 56px !important;
height: 56px !important;
line-height: 56px !important;
}
&:active {
transform: scale(0.98);
}
.page-header h2 {
font-size: 18px;
}
.card-avatar {
position: relative;
margin-right: 24px;
.gender-badge {
position: absolute;
bottom: 0;
right: 0;
font-size: 20px;
}
}
.card-content {
flex: 1;
.card-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
.child-name {
margin: 0;
font-size: 20px;
font-weight: 600;
}
}
.card-info {
display: flex;
gap: 24px;
margin-bottom: 16px;
.info-item {
display: flex;
flex-direction: column;
gap: 4px;
.label {
font-size: 12px;
color: #999;
}
.value {
font-size: 14px;
color: #333;
}
}
}
.card-stats {
display: flex;
gap: 32px;
.stat-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: #fafafa;
border-radius: 8px;
.anticon {
color: $primary-color;
font-size: 18px;
}
.stat-value {
font-size: 20px;
font-weight: 600;
color: #333;
}
.stat-label {
font-size: 12px;
color: #999;
}
}
}
}
.card-action {
font-size: 20px;
color: #d9d9d9;
padding: 0 12px;
transition: color 0.2s;
}
.page-header .page-desc {
font-size: 13px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
padding: 40px 16px;
}
.empty-icon {
font-size: 64px;
color: #d9d9d9;
margin-bottom: 16px;
}
.empty-state .empty-icon {
font-size: 48px;
margin-bottom: 12px;
}
p {
margin: 4px 0;
}
.empty-hint {
font-size: 13px;
color: #bfbfbf;
}
.empty-hint {
font-size: 12px;
}
}
// =============== ===============
@media screen and (max-width: 768px) {
.children-view {
.page-header {
margin-bottom: 16px;
h2 {
font-size: 18px;
}
.page-desc {
font-size: 13px;
}
}
.children-list {
gap: 12px;
}
.child-card {
padding: 16px;
border-radius: 12px;
.card-avatar {
margin-right: 14px;
:deep(.ant-avatar) {
width: 56px !important;
height: 56px !important;
line-height: 56px !important;
}
.gender-badge {
font-size: 16px;
}
}
.card-content {
.card-header {
margin-bottom: 8px;
flex-wrap: wrap;
.child-name {
font-size: 16px;
}
}
.card-info {
gap: 16px;
margin-bottom: 10px;
flex-wrap: wrap;
.info-item {
.label {
font-size: 11px;
}
.value {
font-size: 13px;
}
}
}
.card-stats {
gap: 12px;
flex-wrap: wrap;
.stat-item {
padding: 6px 12px;
gap: 6px;
.anticon {
font-size: 14px;
}
.stat-value {
font-size: 16px;
}
.stat-label {
font-size: 11px;
}
}
}
}
.card-action {
font-size: 16px;
padding: 0 8px;
}
}
.empty-state {
padding: 40px 16px;
.empty-icon {
font-size: 48px;
margin-bottom: 12px;
}
.empty-hint {
font-size: 12px;
}
}
}
}
//
@media screen and (min-width: 769px) and (max-width: 1024px) {
.children-view {
.child-card {
padding: 20px;
}
.child-card {
padding: 20px !important;
}
}
</style>

View File

@ -1,29 +1,29 @@
<template>
<div class="growth-record-view">
<div class="page-header">
<h2><FileImageOutlined /> 成长档案</h2>
<p class="page-desc">记录孩子成长的每一个精彩瞬间</p>
<div class="min-h-100vh p-4">
<div class="mb-6 page-header">
<h2 class="m-0 mb-2 flex items-center gap-2"><FileImageOutlined /> 成长档案</h2>
<p class="m-0 text-[#999] page-desc">记录孩子成长的每一个精彩瞬间</p>
</div>
<a-spin :spinning="loading">
<div class="growth-grid" v-if="records.length > 0">
<div v-for="record in records" :key="record.id" class="growth-card">
<div class="card-image" v-if="record.images && record.images.length > 0">
<img :src="record.images[0]" alt="成长照片" />
<div class="image-count" v-if="record.images.length > 1">
<div class="grid gap-5 grid-cols-1 md:grid-cols-[repeat(auto-fill,minmax(280px,1fr))] growth-grid" v-if="records.length > 0">
<div v-for="record in records" :key="record.id" class="bg-white border border-[#f0f0f0] rounded-xl overflow-hidden transition-all duration-200 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] active:scale-[0.98] growth-card">
<div class="relative h-[180px] bg-[#fafafa] card-image" v-if="record.images && record.images.length > 0">
<img :src="record.images[0]" alt="成长照片" class="w-full h-full object-cover" />
<div v-if="record.images.length > 1" class="absolute bottom-2 right-2 bg-black/60 text-white py-1 px-2 rounded text-xs flex items-center gap-1 image-count">
<PictureOutlined /> {{ record.images.length }}
</div>
</div>
<div class="card-image placeholder" v-else>
<div class="relative h-[180px] bg-[#fafafa] flex items-center justify-center text-[48px] text-[#d9d9d9] card-image placeholder" v-else>
<FileImageOutlined />
</div>
<div class="card-content">
<h3>{{ record.title }}</h3>
<p v-if="record.content">{{ record.content }}</p>
<div class="card-meta">
<span><CalendarOutlined /> {{ formatDate(record.recordDate) }}</span>
<span v-if="record.class">
<div class="p-4 card-content">
<h3 class="m-0 mb-2 text-base overflow-hidden text-ellipsis whitespace-nowrap">{{ record.title }}</h3>
<p v-if="record.content" class="m-0 mb-3 text-sm text-[#666] leading-[1.5] line-clamp-2">{{ record.content }}</p>
<div class="flex gap-4 text-xs text-[#999] card-meta">
<span class="flex items-center gap-1"><CalendarOutlined /> {{ formatDate(record.recordDate) }}</span>
<span v-if="record.class" class="flex items-center gap-1">
<TeamOutlined /> {{ record.class.name }}
</span>
</div>
@ -31,13 +31,13 @@
</div>
</div>
<div class="empty-state" v-else>
<InboxOutlined class="empty-icon" />
<p>暂无成长记录</p>
<div class="text-center py-[60px] px-5 text-[#999] empty-state" v-else>
<InboxOutlined class="text-[64px] text-[#d9d9d9] block mb-4 empty-icon" />
<p class="m-0">暂无成长记录</p>
</div>
</a-spin>
<div class="pagination-section" v-if="total > pageSize">
<div class="mt-6 text-center pagination-section" v-if="total > pageSize">
<a-pagination
v-model:current="currentPage"
:total="total"
@ -115,216 +115,25 @@ onMounted(() => {
});
</script>
<style scoped lang="scss">
$primary-color: #52c41a;
$primary-light: #f6ffed;
.growth-record-view {
.page-header {
margin-bottom: 24px;
h2 {
margin: 0 0 8px;
display: flex;
align-items: center;
gap: 8px;
}
.page-desc {
margin: 0;
color: #999;
}
}
.growth-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.growth-card {
background: white;
border: 1px solid #f0f0f0;
border-radius: 12px;
overflow: hidden;
transition: all 0.2s;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
&:active {
transform: scale(0.98);
}
.card-image {
position: relative;
height: 180px;
background: #fafafa;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.image-count {
position: absolute;
bottom: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.6);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
}
&.placeholder {
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
color: #d9d9d9;
}
}
.card-content {
padding: 16px;
h3 {
margin: 0 0 8px;
font-size: 16px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
p {
margin: 0 0 12px;
font-size: 14px;
color: #666;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-meta {
display: flex;
gap: 16px;
font-size: 12px;
color: #999;
span {
display: flex;
align-items: center;
gap: 4px;
}
}
}
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
.empty-icon {
font-size: 64px;
color: #d9d9d9;
}
}
.pagination-section {
margin-top: 24px;
text-align: center;
}
}
// =============== ===============
<style scoped>
@media screen and (max-width: 768px) {
.growth-record-view {
.page-header {
margin-bottom: 16px;
h2 {
font-size: 18px;
}
.page-desc {
font-size: 13px;
}
}
.growth-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.growth-card {
border-radius: 10px;
.card-image {
height: 120px;
.image-count {
padding: 2px 6px;
font-size: 11px;
bottom: 6px;
right: 6px;
}
&.placeholder {
font-size: 32px;
}
}
.card-content {
padding: 12px;
h3 {
font-size: 14px;
margin-bottom: 6px;
}
p {
font-size: 12px;
margin-bottom: 8px;
-webkit-line-clamp: 2;
}
.card-meta {
flex-direction: column;
gap: 4px;
font-size: 11px;
}
}
}
.empty-state {
padding: 40px 16px;
.empty-icon {
font-size: 48px;
}
}
.pagination-section {
margin-top: 16px;
}
}
.page-header { margin-bottom: 16px; }
.page-header h2 { font-size: 18px; }
.page-desc { font-size: 13px; }
.growth-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
.growth-card { border-radius: 10px; }
.card-image { height: 120px; }
.card-image .image-count { padding: 2px 6px; font-size: 11px; bottom: 6px; right: 6px; }
.card-image.placeholder { font-size: 32px; }
.card-content { padding: 12px; }
.card-content h3 { font-size: 14px; margin-bottom: 6px; }
.card-content p { font-size: 12px; margin-bottom: 8px; }
.card-meta { flex-direction: column; gap: 4px; font-size: 11px; }
.empty-state { padding: 40px 16px; }
.empty-icon { font-size: 48px; }
.pagination-section { margin-top: 16px; }
}
//
@media screen and (min-width: 769px) and (max-width: 1024px) {
.growth-record-view {
.growth-grid {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
}
.growth-grid { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
}
</style>

View File

@ -1,21 +1,21 @@
<template>
<div class="task-list-view">
<div class="page-header">
<h2><CheckSquareOutlined /> 阅读任务</h2>
<p class="page-desc">查看孩子的阅读任务并提交反馈</p>
<div class="min-h-100vh p-4">
<div class="mb-6 page-header">
<h2 class="m-0 mb-2 flex items-center gap-2"><CheckSquareOutlined /> 阅读任务</h2>
<p class="m-0 text-[#999] page-desc">查看孩子的阅读任务并提交反馈</p>
</div>
<a-spin :spinning="loading">
<div class="task-list" v-if="tasks.length > 0">
<div v-for="task in tasks" :key="task.id" class="task-card">
<div class="card-header">
<div class="flex flex-col gap-4 task-list" v-if="tasks.length > 0">
<div v-for="task in tasks" :key="task.id" class="bg-white border border-[#f0f0f0] rounded-xl p-5 transition-all duration-300 active:scale-[0.98] task-card">
<div class="flex justify-between items-start mb-3 card-header">
<div class="task-info">
<h3>{{ task.task.title }}</h3>
<div class="task-meta">
<span v-if="task.task.course?.name">
<h3 class="m-0 text-base">{{ task.task.title }}</h3>
<div class="flex gap-4 mt-2 text-[13px] text-[#666] task-meta">
<span v-if="task.task.course?.name" class="flex items-center gap-1">
<BookOutlined /> {{ task.task.course.name }}
</span>
<span>
<span class="flex items-center gap-1">
<ClockCircleOutlined />
截止: {{ formatDate(task.task.endDate) }}
</span>
@ -26,14 +26,14 @@
</a-tag>
</div>
<div class="card-body" v-if="task.task.description">
<p>{{ task.task.description }}</p>
<div class="mb-3 card-body" v-if="task.task.description">
<p class="m-0 text-sm text-[#666] leading-[1.6]">{{ task.task.description }}</p>
</div>
<div class="card-footer">
<div class="pt-3 border-t border-[#f0f0f0] card-footer">
<div class="feedback-section" v-if="task.parentFeedback">
<span class="feedback-label">您的反馈:</span>
<span class="feedback-content">{{ task.parentFeedback }}</span>
<span class="text-[13px] text-[#999] mr-2 feedback-label">您的反馈:</span>
<span class="text-sm text-[#333] feedback-content">{{ task.parentFeedback }}</span>
</div>
<a-button
v-else
@ -46,9 +46,9 @@
</div>
</div>
<div class="empty-state" v-else>
<InboxOutlined class="empty-icon" />
<p>暂无阅读任务</p>
<div class="text-center py-16 text-[#999] empty-state" v-else>
<InboxOutlined class="text-[64px] text-[#d9d9d9] mb-4 block empty-icon" />
<p class="m-0">暂无阅读任务</p>
</div>
</a-spin>
@ -172,191 +172,25 @@ onMounted(() => {
});
</script>
<style scoped lang="scss">
$primary-color: #52c41a;
$primary-light: #f6ffed;
.task-list-view {
.page-header {
margin-bottom: 24px;
h2 {
margin: 0 0 8px;
display: flex;
align-items: center;
gap: 8px;
}
.page-desc {
margin: 0;
color: #999;
}
}
.task-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.task-card {
background: white;
border: 1px solid #f0f0f0;
border-radius: 12px;
padding: 20px;
transition: all 0.3s;
&:active {
transform: scale(0.98);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
h3 {
margin: 0;
font-size: 16px;
}
.task-meta {
display: flex;
gap: 16px;
margin-top: 8px;
font-size: 13px;
color: #666;
span {
display: flex;
align-items: center;
gap: 4px;
}
}
}
.card-body {
margin-bottom: 12px;
p {
margin: 0;
font-size: 14px;
color: #666;
line-height: 1.6;
}
}
.card-footer {
padding-top: 12px;
border-top: 1px solid #f0f0f0;
.feedback-section {
.feedback-label {
font-size: 13px;
color: #999;
margin-right: 8px;
}
.feedback-content {
font-size: 14px;
color: #333;
}
}
}
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
.empty-icon {
font-size: 64px;
color: #d9d9d9;
}
}
}
// =============== ===============
<style scoped>
@media screen and (max-width: 768px) {
.task-list-view {
.page-header {
margin-bottom: 16px;
h2 {
font-size: 18px;
}
.page-desc {
font-size: 13px;
}
}
.task-list {
gap: 12px;
}
.task-card {
padding: 14px;
border-radius: 10px;
.card-header {
flex-direction: column;
gap: 8px;
margin-bottom: 10px;
h3 {
font-size: 15px;
}
.task-meta {
flex-direction: column;
gap: 6px;
margin-top: 6px;
font-size: 12px;
}
}
.card-body {
margin-bottom: 10px;
p {
font-size: 13px;
}
}
.card-footer {
padding-top: 10px;
.feedback-section {
.feedback-label {
font-size: 12px;
}
.feedback-content {
font-size: 13px;
}
}
}
}
.empty-state {
padding: 40px 16px;
.empty-icon {
font-size: 48px;
}
}
}
.page-header { margin-bottom: 16px; }
.page-header h2 { font-size: 18px; }
.page-desc { font-size: 13px; }
.task-list { gap: 12px; }
.task-card { padding: 14px; border-radius: 10px; }
.card-header { flex-direction: column; gap: 8px; margin-bottom: 10px; }
.card-header h3 { font-size: 15px; }
.task-meta { flex-direction: column; gap: 6px; margin-top: 6px; font-size: 12px; }
.card-body { margin-bottom: 10px; }
.card-body p { font-size: 13px; }
.card-footer { padding-top: 10px; }
.feedback-section .feedback-label { font-size: 12px; }
.feedback-section .feedback-content { font-size: 13px; }
.empty-state { padding: 40px 16px; }
.empty-icon { font-size: 48px; }
}
//
@media screen and (min-width: 769px) and (max-width: 1024px) {
.task-list-view {
.task-card {
padding: 18px;
}
}
.task-card { padding: 18px; }
}
</style>

View File

@ -1,20 +1,20 @@
<template>
<div class="profile-view">
<div class="profile-card">
<div class="card-header">
<h2 class="card-title">个人信息</h2>
<div class="p-0">
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
<div class="flex justify-between items-center py-4 px-6 border-b border-[#f0f0f0]">
<h2 class="m-0 text-lg font-semibold text-[#333]">个人信息</h2>
<a-button type="primary" ghost size="small" :loading="loading" @click="refresh">
刷新
</a-button>
</div>
<a-spin :spinning="loading">
<div v-if="user" class="profile-body">
<div class="avatar-section">
<a-avatar :size="80" class="profile-avatar">
<div v-if="user" class="p-6">
<div class="flex items-center mb-6 pb-6 border-b border-[#f0f0f0]">
<a-avatar :size="80" class="bg-gradient-to-br from-[#e67635] to-[#c45a1e] mr-5">
<template #icon><UserOutlined /></template>
</a-avatar>
<div class="name-role">
<span class="display-name">{{ user.name }}</span>
<div class="flex flex-col gap-2">
<span class="text-xl font-semibold text-[#333]">{{ user.name }}</span>
<a-tag :color="roleTagColor">{{ roleLabel }}</a-tag>
</div>
</div>
@ -32,8 +32,8 @@
</a-descriptions-item>
</a-descriptions>
</div>
<div v-else class="empty-tip">
<UserOutlined />
<div v-else class="py-12 px-6 text-center text-[#999]">
<UserOutlined class="block text-5xl mb-3" />
<p>暂无个人信息请刷新重试</p>
</div>
</a-spin>
@ -86,75 +86,8 @@ onMounted(() => {
</script>
<style scoped>
.profile-view {
padding: 0;
}
.profile-card {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
overflow: hidden;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
border-bottom: 1px solid #f0f0f0;
}
.card-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.profile-body {
padding: 24px;
}
.avatar-section {
display: flex;
align-items: center;
margin-bottom: 24px;
padding-bottom: 24px;
border-bottom: 1px solid #f0f0f0;
}
.profile-avatar {
background: linear-gradient(135deg, #e67635 0%, #c45a1e 100%);
margin-right: 20px;
}
.name-role {
display: flex;
flex-direction: column;
gap: 8px;
}
.display-name {
font-size: 20px;
font-weight: 600;
color: #333;
}
.profile-desc :deep(.ant-descriptions-item-label) {
width: 100px;
color: #666;
}
.empty-tip {
padding: 48px 24px;
text-align: center;
color: #999;
}
.empty-tip .anticon {
font-size: 48px;
margin-bottom: 12px;
display: block;
}
</style>

View File

@ -1,84 +1,100 @@
<template>
<div class="school-dashboard">
<div class="p-0 min-h-screen bg-gradient-to-b from-[#FFF8F0] to-white">
<!-- 欢迎横幅 -->
<div class="welcome-banner">
<div class="banner-content">
<div class="banner-text">
<h1><HomeOutlined /> 校园阅读管理中心</h1>
<p>让每一个孩子都能享受阅读的快乐智慧成长每一天</p>
<div class="bg-gradient-to-br from-[#FF8C42] via-[#FFB347] to-[#FFD93D] rounded-[20px] py-8 px-10 mb-6 relative overflow-hidden sm:py-6 sm:px-6">
<div class="flex justify-between items-center relative z-[1]">
<div>
<h1 class="text-white text-[28px] font-bold m-0 mb-2 text-shadow-[0_2px_4px_rgba(0,0,0,0.1)] sm:text-[22px]"><HomeOutlined /> 校园阅读管理中心</h1>
<p class="text-white/90 text-base m-0">让每一个孩子都能享受阅读的快乐智慧成长每一天</p>
</div>
<div class="banner-decorations">
<span class="decoration"><BookOutlined /></span>
<span class="decoration"><StarOutlined /></span>
<span class="decoration"><BgColorsOutlined /></span>
<span class="decoration"><SmileOutlined /></span>
<div class="flex gap-4 sm:hidden">
<span class="text-4xl animate-float"><BookOutlined /></span>
<span class="text-4xl animate-float"><StarOutlined /></span>
<span class="text-4xl animate-float"><BgColorsOutlined /></span>
<span class="text-4xl animate-float"><SmileOutlined /></span>
</div>
</div>
</div>
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card" v-for="(stat, index) in statCards" :key="index">
<div class="stat-icon" :style="{ background: stat.gradient }">
<div class="grid grid-cols-4 gap-5 mb-6 lg:grid-cols-2 md:grid-cols-1">
<div
v-for="(stat, index) in statCards"
:key="index"
class="bg-white rounded-2xl p-6 flex items-center gap-4 shadow-md transition-all duration-300 hover:-translate-y-1 hover:shadow-xl"
>
<div
class="w-[60px] h-[60px] rounded-2xl flex items-center justify-center text-[28px] text-white shrink-0"
:style="{ background: stat.gradient }"
>
<component :is="stat.icon" />
</div>
<div class="stat-info">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
<div class="flex-1 min-w-0">
<div class="text-3xl font-bold text-[#2D3436] leading-tight">{{ stat.value }}</div>
<div class="text-sm text-[#636E72] mt-1">{{ stat.label }}</div>
</div>
</div>
</div>
<!-- 趋势图和分布图 -->
<div class="charts-grid">
<div class="content-card trend-card">
<div class="card-header">
<span class="card-icon"><LineChartOutlined /></span>
<h3>授课趋势</h3>
<div class="grid grid-cols-[3fr_2fr] gap-6 mb-6 lg:grid-cols-1">
<div class="bg-white rounded-[20px] overflow-hidden shadow-md">
<div class="flex items-center gap-3 py-5 px-6 border-b border-[#F5F5F5]">
<span class="text-2xl text-[#FF8C42] flex items-center"><LineChartOutlined /></span>
<h3 class="m-0 text-lg font-semibold text-[#2D3436] flex-1">授课趋势</h3>
</div>
<div class="card-body" :class="{ 'is-loading': trendLoading }">
<div
class="py-5 px-6 min-h-[200px] flex items-center justify-center"
:class="{ 'flex items-center justify-center': trendLoading }"
>
<a-spin v-if="trendLoading" />
<div v-else ref="trendChartRef" class="chart-container"></div>
<div v-else ref="trendChartRef" class="w-full h-[300px]"></div>
</div>
</div>
<div class="content-card distribution-card">
<div class="card-header">
<span class="card-icon"><BarChartOutlined /></span>
<h3>课程分布</h3>
<div class="bg-white rounded-[20px] overflow-hidden shadow-md">
<div class="flex items-center gap-3 py-5 px-6 border-b border-[#F5F5F5]">
<span class="text-2xl text-[#FF8C42] flex items-center"><BarChartOutlined /></span>
<h3 class="m-0 text-lg font-semibold text-[#2D3436] flex-1">课程分布</h3>
</div>
<div class="card-body" :class="{ 'is-loading': distributionLoading }">
<div
class="py-5 px-6 min-h-[200px] flex items-center justify-center"
:class="{ 'flex items-center justify-center': distributionLoading }"
>
<a-spin v-if="distributionLoading" />
<div v-else ref="distributionChartRef" class="chart-container"></div>
<div v-else ref="distributionChartRef" class="w-full h-[300px]"></div>
</div>
</div>
</div>
<!-- 主要内容区域 -->
<div class="content-grid">
<div class="grid grid-cols-2 gap-6 mb-6 lg:grid-cols-1">
<!-- 近期活动 -->
<div class="content-card activities-card">
<div class="card-header">
<span class="card-icon"><CalendarOutlined /></span>
<h3>近期课程活动</h3>
<div class="bg-white rounded-[20px] overflow-hidden shadow-md">
<div class="flex items-center gap-3 py-5 px-6 border-b border-[#F5F5F5]">
<span class="text-2xl text-[#FF8C42] flex items-center"><CalendarOutlined /></span>
<h3 class="m-0 text-lg font-semibold text-[#2D3436] flex-1">近期课程活动</h3>
</div>
<div class="card-body" :class="{ 'is-loading': loading }">
<div
class="py-5 px-6 min-h-[200px]"
:class="loading ? 'flex items-center justify-center' : ''"
>
<a-spin v-if="loading" />
<div v-else-if="recentActivities.length === 0" class="empty-state">
<span class="empty-icon"><InboxOutlined /></span>
<p>暂无近期活动</p>
<div v-else-if="recentActivities.length === 0" class="flex flex-col items-center justify-center py-10 text-[#B2BEC3]">
<span class="text-5xl mb-3 flex items-center justify-center text-[#B2BEC3]"><InboxOutlined /></span>
<p class="m-0 text-sm">暂无近期活动</p>
</div>
<div v-else class="activity-list">
<div v-else class="flex flex-col gap-4">
<div
v-for="item in recentActivities"
:key="item.id"
class="activity-item"
class="flex items-center gap-3 py-3 px-4 bg-[#FAFAFA] rounded-xl transition-all duration-200 hover:bg-[#FFF8F0]"
>
<div class="activity-avatar">
<div class="w-10 h-10 rounded-[10px] bg-gradient-to-br from-[#FF8C42] to-[#FFB347] flex items-center justify-center text-white shrink-0">
<BookOutlined />
</div>
<div class="activity-content">
<div class="activity-title">{{ item.title }}</div>
<div class="activity-time">{{ formatTime(item.time) }}</div>
<div class="min-w-0 flex-1">
<div class="text-sm font-medium text-[#2D3436]">{{ item.title }}</div>
<div class="text-xs text-[#B2BEC3] mt-1">{{ formatTime(item.time) }}</div>
</div>
</div>
</div>
@ -86,34 +102,45 @@
</div>
<!-- 教师活跃度排行 -->
<div class="content-card teachers-card">
<div class="card-header">
<span class="card-icon"><TrophyOutlined /></span>
<h3>教师活跃度排行</h3>
<div class="bg-white rounded-[20px] overflow-hidden shadow-md">
<div class="flex items-center gap-3 py-5 px-6 border-b border-[#F5F5F5]">
<span class="text-2xl text-[#FF8C42] flex items-center"><TrophyOutlined /></span>
<h3 class="m-0 text-lg font-semibold text-[#2D3436] flex-1">教师活跃度排行</h3>
</div>
<div class="card-body" :class="{ 'is-loading': loading }">
<div
class="py-5 px-6 min-h-[200px]"
:class="loading ? 'flex items-center justify-center' : ''"
>
<a-spin v-if="loading" />
<div v-else-if="activeTeachers.length === 0" class="empty-state">
<span class="empty-icon"><TeamOutlined /></span>
<p>暂无数据</p>
<div v-else-if="activeTeachers.length === 0" class="flex flex-col items-center justify-center py-10 text-[#B2BEC3]">
<span class="text-5xl mb-3 flex items-center justify-center text-[#B2BEC3]"><TeamOutlined /></span>
<p class="m-0 text-sm">暂无数据</p>
</div>
<div v-else class="teacher-list">
<div v-else class="flex flex-col gap-3">
<div
v-for="(item, index) in activeTeachers"
:key="item.id"
class="teacher-item"
class="flex items-center gap-3 py-3 px-4 bg-[#FAFAFA] rounded-xl transition-all duration-200 hover:bg-[#FFF8F0]"
>
<div class="rank-badge" :class="'rank-' + (index + 1)">
<div
class="w-7 h-7 rounded-lg flex items-center justify-center text-sm font-semibold text-white shrink-0"
:class="
index === 0 ? 'bg-gradient-to-br from-[#FFD700] to-[#FFA500]' :
index === 1 ? 'bg-gradient-to-br from-[#C0C0C0] to-[#A8A8A8]' :
index === 2 ? 'bg-gradient-to-br from-[#CD7F32] to-[#B8860B]' :
'bg-[#B2BEC3]'
"
>
{{ index + 1 }}
</div>
<div class="teacher-info">
<div class="teacher-name">{{ item.name }}</div>
<div class="teacher-lessons">
<span class="lesson-icon"><ReadOutlined /></span>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-[#2D3436]">{{ item.name }}</div>
<div class="text-xs text-[#636E72] mt-1 flex items-center gap-1">
<span class="text-sm text-[#636E72] flex items-center"><ReadOutlined /></span>
授课 {{ item.lessonCount }}
</div>
</div>
<div class="teacher-medal">
<div class="flex items-center justify-center shrink-0">
<component :is="getMedalIcon(index)" :style="getMedalStyle(index)" />
</div>
</div>
@ -123,47 +150,50 @@
</div>
<!-- 课程使用统计 -->
<div class="course-stats-card">
<div class="card-header">
<span class="card-icon"><BarChartOutlined /></span>
<h3>课程使用统计</h3>
<div class="header-extra">
<div class="mb-6 bg-white rounded-[20px] overflow-hidden shadow-md">
<div class="flex items-center gap-3 py-5 px-6 border-b border-[#F5F5F5]">
<span class="text-2xl text-[#FF8C42] flex items-center"><BarChartOutlined /></span>
<h3 class="m-0 text-lg font-semibold text-[#2D3436] flex-1">课程使用统计</h3>
<div class="ml-auto">
<a-range-picker
v-model:value="dateRange"
@change="loadCourseStats"
:placeholder="['开始日期', '结束日期']"
style="width: 240px;"
class="w-60"
/>
</div>
</div>
<div class="card-body" :class="{ 'is-loading': courseStatsLoading }">
<div
class="py-5 px-6 min-h-[200px]"
:class="courseStatsLoading ? 'flex items-center justify-center' : ''"
>
<a-spin v-if="courseStatsLoading" />
<div v-else-if="courseStats.length === 0" class="empty-state">
<span class="empty-icon"><LineChartOutlined /></span>
<p>暂无课程使用数据</p>
<div v-else-if="courseStats.length === 0" class="flex flex-col items-center justify-center py-10 text-[#B2BEC3]">
<span class="text-5xl mb-3 flex items-center justify-center text-[#B2BEC3]"><LineChartOutlined /></span>
<p class="m-0 text-sm">暂无课程使用数据</p>
</div>
<div v-else class="course-list">
<div v-else class="flex flex-col gap-3">
<div
v-for="(item, index) in courseStats"
:key="item.courseId"
class="course-item"
class="flex items-center gap-4 py-3 px-4 bg-[#FAFAFA] rounded-xl transition-all duration-200 hover:bg-[#FFF8F0]"
>
<div class="course-rank" :class="'top-' + (index + 1)">
<TrophyFilled v-if="index < 3" class="rank-crown" :style="getTrophyColor(index)" />
<div class="w-8 text-center text-sm font-semibold text-[#636E72] shrink-0">
<TrophyFilled v-if="index < 3" class="text-2xl" :style="getTrophyColor(index)" />
<span v-else>{{ index + 1 }}</span>
</div>
<div class="course-name">{{ item.courseName }}</div>
<div class="course-progress">
<div class="progress-bar">
<div class="flex-1 min-w-0 text-sm font-medium text-[#2D3436]">{{ item.courseName }}</div>
<div class="flex items-center gap-3 w-[200px] md:w-[120px]">
<div class="flex-1 h-2 bg-[#F0F0F0] rounded overflow-hidden">
<div
class="progress-fill"
class="h-full rounded transition-[width] duration-300"
:style="{
width: getUsagePercent(item.usageCount) + '%',
background: getProgressGradient(index)
}"
></div>
</div>
<span class="progress-value">{{ item.usageCount }}</span>
<span class="text-xs text-[#636E72] whitespace-nowrap min-w-[40px] text-right">{{ item.usageCount }}</span>
</div>
</div>
</div>
@ -171,39 +201,48 @@
</div>
<!-- 数据导出 -->
<div class="export-section">
<div class="content-card export-card">
<div class="card-header">
<span class="card-icon"><DownloadOutlined /></span>
<h3>数据导出</h3>
<div class="mb-6">
<div class="bg-white rounded-[20px] overflow-hidden shadow-md">
<div class="flex items-center gap-3 py-5 px-6 border-b border-[#F5F5F5]">
<span class="text-2xl text-[#FF8C42] flex items-center"><DownloadOutlined /></span>
<h3 class="m-0 text-lg font-semibold text-[#2D3436] flex-1">数据导出</h3>
</div>
<div class="card-body">
<div class="export-grid">
<div class="export-item" @click="handleExportLessons">
<div class="export-icon" style="background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);">
<div class="p-6">
<div class="grid grid-cols-3 gap-4 lg:grid-cols-1">
<div
class="flex items-center gap-4 p-5 bg-[#FAFAFA] rounded-xl cursor-pointer transition-all duration-300 border border-transparent hover:bg-[#FFF8F0] hover:border-[#FFD4B8] hover:-translate-y-0.5"
@click="handleExportLessons"
>
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-white text-xl shrink-0 bg-gradient-to-br from-[#FF8C42] to-[#FFB347]">
<ReadOutlined />
</div>
<div class="export-info">
<div class="export-title">授课记录</div>
<div class="export-desc">导出所有授课记录数据</div>
<div class="flex-1 min-w-0">
<div class="text-[15px] font-semibold text-[#2D3436]">授课记录</div>
<div class="text-xs text-[#636E72] mt-1">导出所有授课记录数据</div>
</div>
</div>
<div class="export-item" @click="handleExportTeacherStats">
<div class="export-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<div
class="flex items-center gap-4 p-5 bg-[#FAFAFA] rounded-xl cursor-pointer transition-all duration-300 border border-transparent hover:bg-[#FFF8F0] hover:border-[#FFD4B8] hover:-translate-y-0.5"
@click="handleExportTeacherStats"
>
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-white text-xl shrink-0 bg-gradient-to-br from-[#667eea] to-[#764ba2]">
<SolutionOutlined />
</div>
<div class="export-info">
<div class="export-title">教师绩效</div>
<div class="export-desc">导出教师绩效统计数据</div>
<div class="flex-1 min-w-0">
<div class="text-[15px] font-semibold text-[#2D3436]">教师绩效</div>
<div class="text-xs text-[#636E72] mt-1">导出教师绩效统计数据</div>
</div>
</div>
<div class="export-item" @click="handleExportStudentStats">
<div class="export-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
<div
class="flex items-center gap-4 p-5 bg-[#FAFAFA] rounded-xl cursor-pointer transition-all duration-300 border border-transparent hover:bg-[#FFF8F0] hover:border-[#FFD4B8] hover:-translate-y-0.5"
@click="handleExportStudentStats"
>
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-white text-xl shrink-0 bg-gradient-to-br from-[#4facfe] to-[#00f2fe]">
<UserOutlined />
</div>
<div class="export-info">
<div class="export-title">学生统计</div>
<div class="export-desc">导出学生统计数据</div>
<div class="flex-1 min-w-0">
<div class="text-[15px] font-semibold text-[#2D3436]">学生统计</div>
<div class="text-xs text-[#636E72] mt-1">导出学生统计数据</div>
</div>
</div>
</div>
@ -669,509 +708,15 @@ onUnmounted(() => {
</script>
<style scoped>
.school-dashboard {
padding: 0;
min-height: 100vh;
background: linear-gradient(180deg, #FFF8F0 0%, #FFFFFF 100%);
}
/* 欢迎横幅 */
.welcome-banner {
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 50%, #FFD93D 100%);
border-radius: 20px;
padding: 32px 40px;
margin-bottom: 24px;
position: relative;
overflow: hidden;
}
.banner-content {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
z-index: 1;
}
.banner-text h1 {
color: white;
font-size: 28px;
font-weight: 700;
margin: 0 0 8px 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.banner-text p {
color: rgba(255, 255, 255, 0.9);
font-size: 16px;
margin: 0;
}
.banner-decorations {
display: flex;
gap: 16px;
}
.decoration {
font-size: 36px;
animation: float 3s ease-in-out infinite;
}
.decoration:nth-child(2) { animation-delay: 0.5s; }
.decoration:nth-child(3) { animation-delay: 1s; }
.decoration:nth-child(4) { animation-delay: 1.5s; }
/* 横幅装饰浮动动画UnoCSS 无法表达 keyframes */
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
/* 统计卡片 */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 24px;
}
.stat-card {
background: white;
border-radius: 16px;
padding: 24px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
color: white;
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #2D3436;
line-height: 1.2;
}
.stat-label {
font-size: 14px;
color: #636E72;
margin-top: 4px;
}
/* 内容网格 */
.content-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
margin-bottom: 24px;
}
/* 图表网格 */
.charts-grid {
display: grid;
grid-template-columns: 3fr 2fr;
gap: 24px;
margin-bottom: 24px;
}
.trend-card {
grid-column: 1;
}
.distribution-card {
grid-column: 2;
}
.chart-container {
width: 100%;
height: 300px;
}
/* 卡片通用样式 */
.content-card,
.course-stats-card {
background: white;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
}
.card-header {
display: flex;
align-items: center;
gap: 12px;
padding: 20px 24px;
border-bottom: 1px solid #F5F5F5;
}
.card-icon {
font-size: 24px;
color: #FF8C42;
display: flex;
align-items: center;
}
.card-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #2D3436;
flex: 1;
}
.header-extra {
margin-left: auto;
}
.card-body {
padding: 20px 24px;
min-height: 200px;
}
.card-body.is-loading {
display: flex;
align-items: center;
justify-content: center;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
color: #B2BEC3;
}
.empty-icon {
font-size: 48px;
margin-bottom: 12px;
color: #B2BEC3;
display: flex;
align-items: center;
justify-content: center;
}
.empty-state p {
margin: 0;
font-size: 14px;
}
/* 活动列表 */
.activity-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.activity-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: #FAFAFA;
border-radius: 12px;
transition: all 0.2s ease;
}
.activity-item:hover {
background: #FFF8F0;
}
.activity-avatar {
width: 40px;
height: 40px;
border-radius: 10px;
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.activity-title {
font-size: 14px;
font-weight: 500;
color: #2D3436;
}
.activity-time {
font-size: 12px;
color: #B2BEC3;
margin-top: 4px;
}
/* 教师列表 */
.teacher-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.teacher-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: #FAFAFA;
border-radius: 12px;
transition: all 0.2s ease;
}
.teacher-item:hover {
background: #FFF8F0;
}
.rank-badge {
width: 28px;
height: 28px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
color: white;
background: #B2BEC3;
}
.rank-badge.rank-1 { background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%); }
.rank-badge.rank-2 { background: linear-gradient(135deg, #C0C0C0 0%, #A8A8A8 100%); }
.rank-badge.rank-3 { background: linear-gradient(135deg, #CD7F32 0%, #B8860B 100%); }
.teacher-info {
flex: 1;
}
.teacher-name {
font-size: 14px;
font-weight: 500;
color: #2D3436;
}
.teacher-lessons {
font-size: 12px;
color: #636E72;
margin-top: 4px;
display: flex;
align-items: center;
gap: 4px;
}
.lesson-icon {
font-size: 14px;
color: #636E72;
display: flex;
align-items: center;
}
.teacher-medal {
display: flex;
align-items: center;
justify-content: center;
}
/* 课程统计 */
.course-stats-card {
margin-bottom: 24px;
}
/* 导出区域 */
.export-section {
margin-bottom: 24px;
}
.export-card .card-body {
padding: 24px;
}
.export-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.export-item {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
background: #FAFAFA;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid transparent;
}
.export-item:hover {
background: #FFF8F0;
border-color: #FFD4B8;
transform: translateY(-2px);
}
.export-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
flex-shrink: 0;
}
.export-info {
flex: 1;
}
.export-title {
font-size: 15px;
font-weight: 600;
color: #2D3436;
}
.export-desc {
font-size: 12px;
color: #636E72;
margin-top: 4px;
}
.course-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.course-item {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 16px;
background: #FAFAFA;
border-radius: 12px;
transition: all 0.2s ease;
}
.course-item:hover {
background: #FFF8F0;
}
.course-rank {
width: 32px;
text-align: center;
font-size: 14px;
font-weight: 600;
color: #636E72;
}
.rank-crown {
font-size: 24px;
}
.course-name {
flex: 1;
font-size: 14px;
font-weight: 500;
color: #2D3436;
}
.course-progress {
display: flex;
align-items: center;
gap: 12px;
width: 200px;
}
.progress-bar {
flex: 1;
height: 8px;
background: #F0F0F0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
}
.progress-value {
font-size: 12px;
color: #636E72;
white-space: nowrap;
min-width: 40px;
text-align: right;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.charts-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.welcome-banner {
padding: 24px;
}
.banner-text h1 {
font-size: 22px;
}
.banner-decorations {
display: none;
}
.stats-grid {
grid-template-columns: 1fr;
}
.charts-grid {
grid-template-columns: 1fr;
}
.export-grid {
grid-template-columns: 1fr;
}
.charts-grid {
grid-template-columns: 1fr;
}
.content-grid {
grid-template-columns: 1fr;
}
.course-progress {
width: 120px;
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
.animate-float:nth-child(2) { animation-delay: 0.5s; }
.animate-float:nth-child(3) { animation-delay: 1s; }
.animate-float:nth-child(4) { animation-delay: 1.5s; }
</style>

View File

@ -1,22 +1,22 @@
<template>
<a-layout class="school-layout">
<a-layout class="school-layout h-screen min-h-screen bg-[#FAFAFA] flex overflow-hidden">
<a-layout-sider
v-if="!isMobile"
v-model:collapsed="collapsed"
:trigger="null"
collapsible
class="school-sider"
class="school-sider bg-white! shadow-[2px_0_8px_rgba(0,0,0,0.06)] border-r border-[#E8E8E8] flex flex-col overflow-hidden"
>
<div class="sider-logo">
<img src="/logo.png" alt="Logo" class="logo-img" />
<div v-if="!collapsed" class="logo-text">
<span class="logo-tenant">{{ tenantName }}</span>
<span class="logo-title">少儿智慧阅读</span>
<span class="logo-subtitle">管理后台</span>
<div class="shrink-0 h-20 flex items-center justify-center py-3 px-4 border-b border-[#E8E8E8] bg-gradient-to-br from-[#FFF4EC] to-white">
<img src="/logo.png" alt="Logo" class="w-11 h-11 object-contain shrink-0" />
<div v-if="!collapsed" class="flex flex-col ml-3 leading-snug max-w-[140px]">
<span class="text-[13px] font-semibold text-[#FF8C42] whitespace-nowrap overflow-hidden text-ellipsis">{{ tenantName }}</span>
<span class="text-sm font-semibold text-[#333] whitespace-nowrap">少儿智慧阅读</span>
<span class="text-[11px] text-[#666] whitespace-nowrap">管理后台</span>
</div>
</div>
<div class="sider-menu-wrap">
<div class="sider-menu-wrap flex-1 min-h-0 overflow-y-auto">
<a-menu
v-model:selectedKeys="selectedKeys"
v-model:openKeys="openKeys"
@ -24,7 +24,7 @@
theme="light"
:inline-collapsed="collapsed"
@click="handleMenuClick"
class="side-menu"
class="side-menu border-r-0! py-2 px-3"
>
<!-- 数据概览 - 独立一级菜单 -->
<a-menu-item key="dashboard">
@ -129,9 +129,9 @@
:body-style="{ padding: 0 }"
>
<template #title>
<div class="drawer-header">
<img src="/logo.png" alt="Logo" class="drawer-logo" />
<span class="drawer-title">管理后台</span>
<div class="flex items-center gap-3">
<img src="/logo.png" alt="Logo" class="w-9 h-9 object-contain" />
<span class="text-base font-semibold text-[#333]">管理后台</span>
</div>
</template>
<a-menu
@ -140,7 +140,7 @@
mode="inline"
theme="light"
@click="handleDrawerMenuClick"
class="drawer-menu"
class="drawer-menu border-r-0! py-2"
>
<a-menu-item key="dashboard"><template #icon><DashboardOutlined /></template><span>数据概览</span></a-menu-item>
<a-sub-menu key="staff"><template #icon><TeamOutlined /></template><template #title>人员管理</template>
@ -169,17 +169,17 @@
</a-menu>
</a-drawer>
<a-layout class="school-layout-right">
<a-layout-header v-if="!isMobile" class="school-header">
<a-layout class="flex-1 min-h-0 flex flex-col overflow-hidden">
<a-layout-header v-if="!isMobile" class="shrink-0 sticky top-0 z-100 bg-white px-6 flex justify-between items-center shadow-sm border-b border-[#E8E8E8]">
<div class="header-left">
<MenuUnfoldOutlined
v-if="collapsed"
class="trigger"
class="text-lg cursor-pointer transition-colors text-[#666] hover:text-[#FF8C42]"
@click="collapsed = !collapsed"
/>
<MenuFoldOutlined
v-else
class="trigger"
class="text-lg cursor-pointer transition-colors text-[#666] hover:text-[#FF8C42]"
@click="collapsed = !collapsed"
/>
</div>
@ -189,11 +189,11 @@
<NotificationBell />
<a-dropdown>
<a-space class="user-info" style="cursor: pointer;">
<a-avatar :size="32" class="user-avatar">
<a-space class="px-3 cursor-pointer">
<a-avatar :size="32" class="bg-gradient-to-br from-[#FF8C42] to-[#E67635]">
<template #icon><UserOutlined /></template>
</a-avatar>
<span class="user-name">{{ userName }}</span>
<span class="text-[#333] font-medium">{{ userName }}</span>
<DownOutlined />
</a-space>
<template #overlay>
@ -214,12 +214,12 @@
</div>
</a-layout-header>
<a-layout-header v-if="isMobile" class="school-mobile-header">
<MenuOutlined class="menu-trigger" @click="drawerVisible = true" />
<span class="mobile-title">少儿智慧阅读</span>
<a-layout-header v-if="isMobile" class="shrink-0 sticky top-0 z-100 bg-white px-4 h-14 flex items-center justify-between shadow-sm border-b border-[#E8E8E8]">
<MenuOutlined class="text-[22px] text-[#666] p-2 cursor-pointer" @click="drawerVisible = true" />
<span class="text-[17px] font-semibold text-[#333]">少儿智慧阅读</span>
<a-dropdown>
<a-space class="user-info" style="cursor: pointer;">
<a-avatar :size="32" class="user-avatar">
<a-space class="cursor-pointer">
<a-avatar :size="32" class="bg-gradient-to-br from-[#FF8C42] to-[#E67635]">
<template #icon><UserOutlined /></template>
</a-avatar>
</a-space>
@ -233,7 +233,7 @@
</a-dropdown>
</a-layout-header>
<a-layout-content :class="['school-content', { 'school-content-mobile': isMobile }]">
<a-layout-content :class="['flex-1 min-h-0 bg-white rounded-xl shadow-sm overflow-y-auto overflow-x-hidden', isMobile ? 'm-3 p-4' : 'm-5 p-6']">
<router-view />
</a-layout-content>
</a-layout>
@ -366,286 +366,120 @@ const handleUserMenuClick = ({ key }: { key: string | number }) => {
</script>
<style scoped lang="scss">
//
$primary-color: #FF8C42; //
$primary-light: #FFF4EC;
$primary-dark: #E67635;
$accent-color: #4CAF50; // 绿
$text-color: #333333;
$text-secondary: #666666;
$border-color: #E8E8E8;
$bg-light: #FAFAFA;
.school-layout {
height: 100vh;
min-height: 100vh;
background: $bg-light;
display: flex;
overflow: hidden;
}
.school-layout-right {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.school-sider {
background: white !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.06);
border-right: 1px solid $border-color;
display: flex;
flex-direction: column;
overflow: hidden;
:deep(.ant-layout-sider-children) {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
}
.sider-logo {
flex-shrink: 0;
height: 80px;
.sider-menu-wrap {
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.18);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.28);
}
}
.side-menu {
:deep(.ant-menu-item) {
margin: 4px 0;
padding-left: 12px !important;
padding-right: 12px !important;
border-radius: 8px;
height: 44px;
line-height: 44px;
color: #333333;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
padding: 12px 16px;
border-bottom: 1px solid $border-color;
background: linear-gradient(135deg, $primary-light 0%, white 100%);
.logo-img {
width: 44px;
height: 44px;
object-fit: contain;
flex-shrink: 0;
}
.logo-text {
display: flex;
flex-direction: column;
margin-left: 12px;
line-height: 1.4;
max-width: 140px;
.logo-tenant {
font-size: 13px;
font-weight: 600;
color: $primary-color;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.logo-title {
font-size: 14px;
font-weight: 600;
color: $text-color;
white-space: nowrap;
}
.logo-subtitle {
font-size: 11px;
color: $text-secondary;
white-space: nowrap;
}
}
}
.sider-menu-wrap {
flex: 1;
min-height: 0;
overflow-y: auto;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.18);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.28);
}
}
.side-menu {
border-right: none !important;
padding: 8px 12px;
:deep(.ant-menu-item) {
margin: 4px 0;
padding-left: 12px !important;
padding-right: 12px !important;
border-radius: 8px;
height: 44px;
line-height: 44px;
color: $text-color;
transition: all 0.3s;
.ant-menu-title-content {
display: flex;
align-items: center;
.ant-menu-title-content {
display: flex;
align-items: center;
}
&:hover {
background: $primary-light;
color: $primary-color;
}
&.ant-menu-item-selected {
background: linear-gradient(135deg, $primary-color 0%, $primary-dark 100%);
color: white;
&::after {
display: none;
}
.anticon {
color: white;
}
}
}
//
:deep(.ant-menu-submenu-title) {
margin: 4px 0;
padding-left: 12px !important;
padding-right: 12px !important;
border-radius: 8px;
height: 44px;
line-height: 44px;
color: $text-color;
transition: all 0.3s;
&:hover {
background: $primary-light;
color: $primary-color;
}
}
//
:deep(.ant-menu-submenu-open > .ant-menu-submenu-title) {
color: $primary-color;
font-weight: 500;
}
//
:deep(.ant-menu-sub) {
background: transparent;
padding-left: 8px !important;
.ant-menu-item {
height: 40px;
line-height: 40px;
margin: 2px 0;
padding-left: 44px !important;
font-size: 13px;
}
}
}
}
.school-header {
flex-shrink: 0;
position: sticky;
top: 0;
z-index: 100;
background: white;
padding: 0 24px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
border-bottom: 1px solid $border-color;
.trigger {
font-size: 18px;
cursor: pointer;
transition: color 0.3s;
color: $text-secondary;
&:hover {
color: $primary-color;
background: #FFF4EC;
color: #FF8C42;
}
&.ant-menu-item-selected {
background: linear-gradient(135deg, #FF8C42 0%, #E67635 100%);
color: white;
&::after {
display: none;
}
.anticon {
color: white;
}
}
}
.user-info {
padding: 0 12px;
:deep(.ant-menu-submenu-title) {
margin: 4px 0;
padding-left: 12px !important;
padding-right: 12px !important;
border-radius: 8px;
height: 44px;
line-height: 44px;
color: #333333;
transition: all 0.3s;
&:hover {
background: #FFF4EC;
color: #FF8C42;
}
}
.user-avatar {
background: linear-gradient(135deg, $primary-color 0%, $primary-dark 100%);
}
.user-name {
color: $text-color;
:deep(.ant-menu-submenu-open > .ant-menu-submenu-title) {
color: #FF8C42;
font-weight: 500;
}
}
.school-content {
flex: 1;
min-height: 0;
margin: 20px;
padding: 24px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
overflow-y: auto;
overflow-x: hidden;
}
:deep(.ant-menu-sub) {
background: transparent;
padding-left: 8px !important;
.school-content-mobile {
margin: 12px;
padding: 16px;
border-radius: 12px;
}
.school-drawer {
.drawer-header {
display: flex;
align-items: center;
gap: 12px;
.drawer-logo { width: 36px; height: 36px; object-fit: contain; }
.drawer-title { font-size: 16px; font-weight: 600; color: $text-color; }
}
.drawer-menu {
border-right: none !important;
padding: 8px 0;
:deep(.ant-menu-item),
:deep(.ant-menu-submenu-title) { margin: 4px 8px; border-radius: 8px; height: 48px; line-height: 48px; }
:deep(.ant-menu-sub) .ant-menu-item { height: 44px; line-height: 44px; }
.ant-menu-item {
height: 40px;
line-height: 40px;
margin: 2px 0;
padding-left: 44px !important;
font-size: 13px;
}
}
}
.school-mobile-header {
flex-shrink: 0;
position: sticky;
top: 0;
z-index: 100;
background: white;
padding: 0 16px;
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
border-bottom: 1px solid $border-color;
.menu-trigger { font-size: 22px; color: $text-secondary; padding: 8px; cursor: pointer; }
.mobile-title { font-size: 17px; font-weight: 600; color: $text-color; }
.user-avatar { background: linear-gradient(135deg, $primary-color 0%, $primary-dark 100%); }
.drawer-menu {
:deep(.ant-menu-item),
:deep(.ant-menu-submenu-title) {
margin: 4px 8px;
border-radius: 8px;
height: 48px;
line-height: 48px;
}
:deep(.ant-menu-sub) .ant-menu-item {
height: 44px;
line-height: 44px;
}
}
@media (max-width: 768px) {
.school-layout :deep(.ant-layout-sider) { display: none; }
.school-layout :deep(.ant-layout-sider) {
display: none;
}
}
</style>

View File

@ -1,15 +1,15 @@
<template>
<div class="package-view">
<div class="min-h-100vh bg-[linear-gradient(180deg,#F0FFF4_0%,#FFFFFF_100%)] p-6">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-content">
<div class="header-title">
<div class="title-icon-wrapper">
<GiftOutlined class="title-icon" />
<div class="mb-6">
<div class="flex justify-between items-center">
<div class="flex items-center gap-4">
<div class="w-14 h-14 flex items-center justify-center bg-[linear-gradient(135deg,#43e97b_0%,#38f9d7_100%)] rounded-[14px] shadow-[0_4px_12px_rgba(67,233,123,0.3)]">
<GiftOutlined class="text-[28px] text-white" />
</div>
<div class="title-text">
<h2>套餐管理</h2>
<p>查看学校已授权的课程套餐</p>
<div>
<h2 class="text-[#333] text-2xl font-700 m-0">套餐管理</h2>
<p class="text-[#666] text-sm mt-1 m-0">查看学校已授权的课程套餐</p>
</div>
</div>
<a-button type="primary" @click="showApplyModal">
@ -19,85 +19,88 @@
</div>
<!-- 统计概览 -->
<div class="stats-row" v-if="!loading">
<div class="stat-card">
<div class="stat-icon package">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6" v-if="!loading">
<div class="bg-white rounded-xl p-5 flex items-center gap-4 shadow-[0_2px_8px_rgba(0,0,0,0.06)]">
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-2xl text-white bg-[linear-gradient(135deg,#43e97b_0%,#38f9d7_100%)]">
<GiftOutlined />
</div>
<div class="stat-info">
<div class="stat-value">{{ tenantPackages.length }}</div>
<div class="stat-label">已授权套餐</div>
<div>
<div class="text-[28px] font-700 text-[#333]">{{ tenantPackages.length }}</div>
<div class="text-[13px] text-[#666]">已授权套餐</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon course">
<div class="bg-white rounded-xl p-5 flex items-center gap-4 shadow-[0_2px_8px_rgba(0,0,0,0.06)]">
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-2xl text-white bg-[linear-gradient(135deg,#667eea_0%,#764ba2_100%)]">
<BookOutlined />
</div>
<div class="stat-info">
<div class="stat-value">{{ totalCourseCount }}</div>
<div class="stat-label">可用课程包</div>
<div>
<div class="text-[28px] font-700 text-[#333]">{{ totalCourseCount }}</div>
<div class="text-[13px] text-[#666]">可用课程包</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon expiring" v-if="expiringCount > 0">
<div class="bg-white rounded-xl p-5 flex items-center gap-4 shadow-[0_2px_8px_rgba(0,0,0,0.06)]">
<div v-if="expiringCount > 0" class="w-12 h-12 rounded-xl flex items-center justify-center text-2xl text-white bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)]">
<WarningOutlined />
</div>
<div class="stat-icon normal" v-else>
<div v-else class="w-12 h-12 rounded-xl flex items-center justify-center text-2xl text-white bg-[linear-gradient(135deg,#4facfe_0%,#00f2fe_100%)]">
<CheckCircleOutlined />
</div>
<div class="stat-info">
<div class="stat-value" :class="{ warning: expiringCount > 0 }">{{ expiringCount }}</div>
<div class="stat-label">即将到期</div>
<div>
<div class="text-[28px] font-700" :class="expiringCount > 0 ? 'text-[#f5222d]' : 'text-[#333]'">{{ expiringCount }}</div>
<div class="text-[13px] text-[#666]">即将到期</div>
</div>
</div>
</div>
<!-- 加载状态 -->
<div class="loading-state" v-if="loading">
<div class="flex flex-col items-center justify-center py-20" v-if="loading">
<a-spin size="large" />
<p>加载套餐信息...</p>
<p class="text-[#666] mt-4">加载套餐信息...</p>
</div>
<!-- 套餐列表 -->
<div class="package-list" v-if="!loading">
<div v-if="!loading">
<a-alert
v-if="tenantPackages.length === 0"
type="info"
show-icon
message="暂无已授权的套餐"
description="请联系管理员申请课程套餐,以获取课程包的使用权限。"
style="margin-bottom: 24px;"
class="mb-6"
/>
<div class="package-grid">
<div class="grid gap-5 grid-cols-1 md:grid-cols-[repeat(auto-fill,minmax(360px,1fr))]">
<div
v-for="item in tenantPackages"
:key="item.id"
class="package-card"
:class="{ 'expiring': isExpiring(item.endDate), 'expired': isExpired(item.endDate) }"
class="bg-white rounded-2xl overflow-hidden shadow-[0_2px_8px_rgba(0,0,0,0.06)] transition-all duration-300 border hover:shadow-[0_4px_16px_rgba(0,0,0,0.12)] hover:-translate-y-0.5"
:class="[
isExpiring(item.endDate) ? 'border-[#faad14] bg-[linear-gradient(to_bottom,#fffbe6,white)]' : 'border-[#e8e8e8]',
isExpired(item.endDate) ? 'border-[#ff4d4f] opacity-80' : '',
]"
>
<div class="package-header">
<div class="package-name">{{ item.package.name }}</div>
<div class="flex justify-between items-center py-4 px-5 border-b border-[#f0f0f0] bg-[#fafafa]">
<div class="text-base font-600 text-[#333]">{{ item.package.name }}</div>
<a-tag :color="getStatusColor(item.endDate)">
{{ getStatusText(item.endDate) }}
</a-tag>
</div>
<div class="package-body">
<div class="p-5">
<div class="package-info">
<div class="info-item">
<span class="info-label">包含课程包</span>
<span class="info-value">{{ item.package.courseCount }} </span>
<div class="flex justify-between py-2 border-b border-dashed border-[#f0f0f0]">
<span class="text-[#666] text-[13px]">包含课程包</span>
<span class="text-[#333] text-[13px]">{{ item.package.courseCount }} </span>
</div>
<div class="info-item">
<span class="info-label">授权时间</span>
<span class="info-value">{{ formatDate(item.startDate) }}</span>
<div class="flex justify-between py-2 border-b border-dashed border-[#f0f0f0]">
<span class="text-[#666] text-[13px]">授权时间</span>
<span class="text-[#333] text-[13px]">{{ formatDate(item.startDate) }}</span>
</div>
<div class="info-item">
<span class="info-label">到期时间</span>
<span class="info-value" :class="{ warning: isExpiring(item.endDate) }">
<div class="flex justify-between py-2">
<span class="text-[#666] text-[13px]">到期时间</span>
<span class="text-[13px]" :class="isExpiring(item.endDate) ? 'text-[#faad14] font-500' : 'text-[#333]'">
{{ formatDate(item.endDate) }}
<span v-if="!isExpired(item.endDate)" class="days-left">
<span v-if="!isExpired(item.endDate)" class="text-[#999] text-xs ml-1">
({{ getDaysLeft(item.endDate) }})
</span>
</span>
@ -105,9 +108,9 @@
</div>
<!-- 课程包预览 -->
<div class="course-preview" v-if="item.package.courses && item.package.courses.length > 0">
<div class="preview-title">包含课程包</div>
<div class="preview-tags">
<div v-if="item.package.courses && item.package.courses.length > 0" class="mt-3 pt-3 border-t border-[#f0f0f0]">
<div class="text-xs text-[#666] mb-2">包含课程包</div>
<div class="flex flex-wrap gap-1">
<a-tag v-for="course in item.package.courses.slice(0, 5)" :key="course.id" color="blue">
{{ course.course.name }}
</a-tag>
@ -118,13 +121,13 @@
</div>
</div>
<div class="package-footer">
<div class="flex justify-end gap-2 py-3 px-5 border-t border-[#f0f0f0] bg-[#fafafa]">
<a-button type="link" @click="viewPackageDetail(item)">
<EyeOutlined /> 查看详情
</a-button>
<a-button
type="link"
:class="{ 'renew-btn': isExpiring(item.endDate) || isExpired(item.endDate) }"
:class="(isExpiring(item.endDate) || isExpired(item.endDate)) ? '!text-[#faad14] hover:!text-[#ff7a2a]' : ''"
@click="showRenewModal(item)"
>
<SyncOutlined /> 续订
@ -141,42 +144,42 @@
width="800px"
:footer="null"
>
<div class="detail-modal" v-if="selectedPackage">
<div class="detail-section">
<div class="section-title">
<div v-if="selectedPackage">
<div class="mb-6 last:mb-0">
<div class="text-[15px] font-600 text-[#333] mb-3 flex items-center gap-2 pb-2 border-b border-[#f0f0f0]">
<InfoCircleOutlined /> 套餐信息
</div>
<div class="section-content">
<div class="detail-row">
<span class="detail-label">套餐名称</span>
<span class="detail-value">{{ selectedPackage.package.name }}</span>
<div>
<div class="flex py-2">
<span class="w-[100px] text-[#666] text-[13px]">套餐名称</span>
<span class="flex-1 text-[#333] text-[13px]">{{ selectedPackage.package.name }}</span>
</div>
<div class="detail-row" v-if="selectedPackage.package.description">
<span class="detail-label">套餐描述</span>
<span class="detail-value">{{ selectedPackage.package.description }}</span>
<div class="flex py-2" v-if="selectedPackage.package.description">
<span class="w-[100px] text-[#666] text-[13px]">套餐描述</span>
<span class="flex-1 text-[#333] text-[13px]">{{ selectedPackage.package.description }}</span>
</div>
<div class="detail-row">
<span class="detail-label">授权时间</span>
<span class="detail-value">{{ formatDate(selectedPackage.startDate) }}</span>
<div class="flex py-2">
<span class="w-[100px] text-[#666] text-[13px]">授权时间</span>
<span class="flex-1 text-[#333] text-[13px]">{{ formatDate(selectedPackage.startDate) }}</span>
</div>
<div class="detail-row">
<span class="detail-label">到期时间</span>
<span class="detail-value">
<div class="flex py-2">
<span class="w-[100px] text-[#666] text-[13px]">到期时间</span>
<span class="flex-1 text-[13px]">
{{ formatDate(selectedPackage.endDate) }}
<a-tag v-if="!isExpired(selectedPackage.endDate)" :color="getStatusColor(selectedPackage.endDate)" style="margin-left: 8px;">
<a-tag v-if="!isExpired(selectedPackage.endDate)" :color="getStatusColor(selectedPackage.endDate)" class="ml-2">
{{ getDaysLeft(selectedPackage.endDate) }}天后到期
</a-tag>
<a-tag v-else color="red" style="margin-left: 8px;">已过期</a-tag>
<a-tag v-else color="red" class="ml-2">已过期</a-tag>
</span>
</div>
</div>
</div>
<div class="detail-section">
<div class="section-title">
<div class="mb-0">
<div class="text-[15px] font-600 text-[#333] mb-3 flex items-center gap-2 pb-2 border-b border-[#f0f0f0]">
<BookOutlined /> 包含课程包 ({{ selectedPackage.package.courses?.length || 0 }})
</div>
<div class="section-content">
<div>
<a-table
v-if="selectedPackage.package.courses && selectedPackage.package.courses.length > 0"
:dataSource="selectedPackage.package.courses"
@ -191,7 +194,7 @@
</template>
</template>
</a-table>
<div v-else class="empty-courses">暂无课程包</div>
<div v-else class="text-center py-6 text-[#999]">暂无课程包</div>
</div>
</div>
</div>
@ -205,19 +208,19 @@
@ok="handleRenew"
:confirmLoading="renewLoading"
>
<div class="renew-modal" v-if="selectedPackage">
<div class="renew-info">
<div class="info-row">
<span class="info-label">套餐名称</span>
<span class="info-value">{{ selectedPackage.package.name }}</span>
<div v-if="selectedPackage">
<div class="bg-[#f9f9f9] rounded-lg p-4">
<div class="flex justify-between py-2 border-b border-dashed border-[#e8e8e8]">
<span class="text-[#666]">套餐名称</span>
<span class="text-[#333] font-500">{{ selectedPackage.package.name }}</span>
</div>
<div class="info-row">
<span class="info-label">包含课程包</span>
<span class="info-value">{{ selectedPackage.package.courseCount }}</span>
<div class="flex justify-between py-2 border-b border-dashed border-[#e8e8e8]">
<span class="text-[#666]">包含课程包</span>
<span class="text-[#333] font-500">{{ selectedPackage.package.courseCount }}</span>
</div>
<div class="info-row">
<span class="info-label">当前到期</span>
<span class="info-value">{{ formatDate(selectedPackage.endDate) }}</span>
<div class="flex justify-between py-2">
<span class="text-[#666]">当前到期</span>
<span class="text-[#333] font-500">{{ formatDate(selectedPackage.endDate) }}</span>
</div>
</div>
@ -253,17 +256,17 @@
width="500px"
:footer="null"
>
<div class="apply-modal">
<div>
<a-alert
type="info"
show-icon
message="申请说明"
description="如需申请新的课程套餐,请联系平台管理员或客服进行咨询和办理。"
style="margin-bottom: 16px;"
class="mb-4"
/>
<div class="contact-info">
<p><PhoneOutlined /> 客服电话400-XXX-XXXX</p>
<p><MailOutlined /> 客服邮箱service@example.com</p>
<div class="bg-[#f9f9f9] rounded-lg p-4 mb-4">
<p class="my-2 text-[#333] flex items-center gap-2"><PhoneOutlined /> 客服电话400-XXX-XXXX</p>
<p class="my-2 text-[#333] flex items-center gap-2"><MailOutlined /> 客服邮箱service@example.com</p>
</div>
<a-button type="primary" block @click="applyModalVisible = false">
我知道了
@ -429,354 +432,9 @@ onMounted(() => {
});
</script>
<style scoped lang="scss">
.package-view {
min-height: 100vh;
background: linear-gradient(180deg, #F0FFF4 0%, #FFFFFF 100%);
padding: 24px;
}
<style scoped>
/* 仅保留无法用 UnoCSS 实现的部分 */
/* 页面头部 */
.page-header {
margin-bottom: 24px;
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
display: flex;
align-items: center;
gap: 16px;
}
.title-icon-wrapper {
width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
border-radius: 14px;
box-shadow: 0 4px 12px rgba(67, 233, 123, 0.3);
}
.title-icon {
font-size: 28px;
color: white;
}
.title-text h2 {
color: #333;
font-size: 24px;
font-weight: 700;
margin: 0;
}
.title-text p {
color: #666;
font-size: 14px;
margin: 4px 0 0 0;
}
}
/* 统计概览 */
.stats-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 24px;
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
}
.stat-card {
background: white;
border-radius: 12px;
padding: 20px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.stat-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
&.package {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
color: white;
}
&.course {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
&.expiring {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
&.normal {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
}
.stat-info {
.stat-value {
font-size: 28px;
font-weight: 700;
color: #333;
&.warning {
color: #f5222d;
}
}
.stat-label {
font-size: 13px;
color: #666;
}
}
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
p {
color: #666;
margin-top: 16px;
}
}
/* 套餐网格 */
.package-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
gap: 20px;
}
/* 套餐卡片 */
.package-card {
background: white;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
border: 1px solid #e8e8e8;
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
transform: translateY(-2px);
}
&.expiring {
border-color: #faad14;
background: linear-gradient(to bottom, #fffbe6, white);
}
&.expired {
border-color: #ff4d4f;
opacity: 0.8;
}
.package-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
.package-name {
font-size: 16px;
font-weight: 600;
color: #333;
}
}
.package-body {
padding: 16px 20px;
.package-info {
.info-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px dashed #f0f0f0;
&:last-child {
border-bottom: none;
}
.info-label {
color: #666;
font-size: 13px;
}
.info-value {
color: #333;
font-size: 13px;
&.warning {
color: #faad14;
font-weight: 500;
}
.days-left {
color: #999;
font-size: 12px;
margin-left: 4px;
}
}
}
}
.course-preview {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
.preview-title {
font-size: 12px;
color: #666;
margin-bottom: 8px;
}
.preview-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
}
}
.package-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 12px 20px;
border-top: 1px solid #f0f0f0;
background: #fafafa;
.renew-btn {
color: #faad14;
&:hover {
color: #ff7a2a;
}
}
}
}
/* 详情弹窗 */
.detail-modal {
.detail-section {
margin-bottom: 24px;
&:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.section-content {
.detail-row {
display: flex;
padding: 8px 0;
.detail-label {
width: 100px;
color: #666;
font-size: 13px;
}
.detail-value {
flex: 1;
color: #333;
font-size: 13px;
}
}
}
}
.empty-courses {
text-align: center;
padding: 24px;
color: #999;
}
}
/* 续订弹窗 */
.renew-modal {
.renew-info {
background: #f9f9f9;
border-radius: 8px;
padding: 16px;
.info-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px dashed #e8e8e8;
&:last-child {
border-bottom: none;
}
.info-label {
color: #666;
}
.info-value {
color: #333;
font-weight: 500;
}
}
}
}
/* 申请弹窗 */
.apply-modal {
.contact-info {
background: #f9f9f9;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
p {
margin: 8px 0;
color: #333;
display: flex;
align-items: center;
gap: 8px;
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -1,96 +1,99 @@
<template>
<div class="course-list-view">
<div class="min-h-100vh p-0 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)]">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-content">
<div class="header-title">
<span class="title-icon">
<BookOutlined />
<div class="rounded-[20px] py-6 px-8 mb-6 bg-[linear-gradient(135deg,#43e97b_0%,#38f9d7_100%)]">
<div class="flex justify-between items-center">
<div class="flex items-center gap-4">
<span class="flex items-center justify-center w-14 h-14 rounded-[14px] bg-white/25 backdrop-blur">
<BookOutlined class="text-[32px] text-white" />
</span>
<div class="title-text">
<h2>课程管理</h2>
<p>管理学校授权的智慧阅读课程</p>
<div>
<h2 class="text-white text-2xl font-700 m-0">课程管理</h2>
<p class="text-white/80 text-sm mt-1 m-0">管理学校授权的智慧阅读课程</p>
</div>
</div>
<div class="header-stats">
<div class="stat-item">
<span class="stat-value">{{ authorizedCount }}</span>
<span class="stat-label">已授权</span>
<div class="flex gap-8">
<div class="text-center">
<span class="block text-3xl font-700 text-white">{{ authorizedCount }}</span>
<span class="text-xs text-white/80">已授权</span>
</div>
<div class="stat-item">
<span class="stat-value active">{{ totalUsage }}</span>
<span class="stat-label">总使用次数</span>
<div class="text-center">
<span class="block text-3xl font-700 text-[#FFD93D]">{{ totalUsage }}</span>
<span class="text-xs text-white/80">总使用次数</span>
</div>
</div>
</div>
</div>
<!-- 年级切换Tab + 操作栏 -->
<div class="filter-action-bar">
<div class="grade-tabs">
<span class="tab-label">年级筛选</span>
<div class="tab-buttons">
<div class="flex flex-col gap-4 mb-6 py-5 px-6 bg-white rounded-2xl shadow-[0_2px_8px_rgba(0,0,0,0.04)]">
<div class="flex items-center gap-4">
<span class="text-sm font-600 text-[#333] whitespace-nowrap">年级筛选</span>
<div class="flex gap-2">
<div
v-for="grade in gradeOptions"
:key="grade.value"
class="grade-tab"
:class="{ active: selectedGrade === grade.value }"
class="py-2 px-5 rounded-[10px] text-sm font-500 cursor-pointer transition-all duration-300 border-none grade-tab"
:class="selectedGrade === grade.value ? 'bg-[linear-gradient(135deg,#43e97b_0%,#38f9d7_100%)] text-white shadow-[0_4px_12px_rgba(67,233,123,0.3)]' : 'bg-[#F5F5F5] text-[#666] hover:bg-[#E8F5E9] hover:text-[#43e97b]'"
@click="selectedGrade = grade.value"
>
{{ grade.label }}
</div>
</div>
</div>
<div class="action-row">
<div class="flex justify-between items-center">
<div class="search-box">
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索课程名称"
style="width: 280px;"
class="w-[280px]"
@search="handleSearch"
allow-clear
>
<template #prefix>
<SearchOutlined style="color: #B2BEC3;" />
<SearchOutlined class="text-[#B2BEC3]" />
</template>
</a-input-search>
</div>
<a-button type="primary" class="auth-btn" @click="showAuthModal">
<StarFilled class="btn-icon" />
<a-button type="primary" class="!bg-[linear-gradient(135deg,#43e97b_0%,#38f9d7_100%)] hover:!bg-[linear-gradient(135deg,#32d86c_0%,#2ed9c0_100%)] !border-0 rounded-xl h-10 px-6 font-600" @click="showAuthModal">
<StarFilled class="mr-2 text-sm" />
授权新课程
</a-button>
</div>
</div>
<!-- 课程卡片网格 -->
<div class="course-grid" v-if="!loading && filteredCourses.length > 0">
<div class="grid gap-5 mb-6 course-grid" style="grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));" v-if="!loading && filteredCourses.length > 0">
<div
v-for="course in filteredCourses"
:key="course.id"
class="course-card"
:class="{ 'unauthorized': !course.authorized }"
class="bg-white rounded-2xl overflow-hidden shadow-[0_4px_12px_rgba(0,0,0,0.05)] transition-all duration-300 hover:translate-y-[-4px] hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)]"
:class="!course.authorized ? 'opacity-80' : ''"
>
<div class="card-cover">
<div class="cover-placeholder" v-if="!course.pictureUrl">
<ReadOutlined class="cover-icon" />
<div class="relative h-[140px] bg-[linear-gradient(135deg,#43e97b_0%,#38f9d7_100%)] card-cover">
<div class="w-full h-full flex items-center justify-center" v-if="!course.pictureUrl">
<ReadOutlined class="text-[48px] text-white/90" />
</div>
<img v-else :src="course.pictureUrl" alt="cover" />
<div class="auth-badge" :class="course.authorized ? 'authorized' : 'unauthorized'">
<img v-else :src="course.pictureUrl" alt="cover" class="w-full h-full object-cover" />
<div
class="absolute top-3 right-3 py-1 px-3 rounded-xl text-[11px] font-600 flex items-center gap-1"
:class="course.authorized ? 'bg-white/90 text-[#43A047]' : 'bg-black/50 text-white'"
>
<CheckCircleOutlined v-if="course.authorized" />
<ClockCircleOutlined v-else />
{{ course.authorized ? '已授权' : '未授权' }}
</div>
</div>
<div class="card-body">
<h3 class="course-name">{{ course.name }}</h3>
<p class="course-book">{{ course.pictureBookName }}</p>
<div class="p-4">
<h3 class="text-base font-600 text-[#2D3436] m-0 mb-1">{{ course.name }}</h3>
<p class="text-xs text-[#636E72] m-0 mb-3">{{ course.pictureBookName }}</p>
<div class="course-tags">
<div class="flex flex-wrap gap-1.5 mb-3">
<span
v-for="tag in course.gradeTags.slice(0, 2)"
:key="tag"
class="tag grade"
class="py-0.5 px-2 rounded-lg text-[10px] tag"
:style="getGradeTagStyle(translateGradeTag(tag))"
>
{{ translateGradeTag(tag) }}
@ -98,27 +101,27 @@
<span
v-for="tag in course.domainTags.slice(0, 2)"
:key="tag"
class="tag domain"
class="py-0.5 px-2 rounded-lg text-[10px] tag"
:style="getDomainTagStyle(translateDomainTag(tag))"
>
{{ translateDomainTag(tag) }}
</span>
</div>
<div class="course-meta">
<div class="meta-item">
<ClockCircleOutlined class="meta-icon" />
<div class="flex gap-4">
<div class="flex items-center gap-1 text-xs text-[#636E72]">
<ClockCircleOutlined class="text-sm text-[#43e97b]" />
<span>{{ course.duration }}分钟</span>
</div>
<div class="meta-item">
<BarChartOutlined class="meta-icon" />
<div class="flex items-center gap-1 text-xs text-[#636E72]">
<BarChartOutlined class="text-sm text-[#43e97b]" />
<span>使用{{ course.usageCount }}</span>
</div>
</div>
</div>
<div class="card-actions">
<a-button type="link" size="small" @click="handleView(course)">
<div class="flex justify-end gap-2 py-3 px-4 border-t border-[#F0F0F0] bg-[#FAFAFA] card-actions">
<a-button type="link" size="small" @click="handleView(course)" class="!py-1 !px-2 !h-auto flex items-center gap-1">
<FileTextOutlined />
详情
</a-button>
@ -126,7 +129,7 @@
v-if="!course.authorized"
type="link"
size="small"
class="auth-action"
class="!text-[#43e97b]"
@click="handleAuthorize(course)"
>
<StarFilled />
@ -147,37 +150,36 @@
</div>
<!-- 空状态 -->
<div class="empty-state" v-if="!loading && filteredCourses.length === 0">
<div class="empty-icon-wrapper">
<BookOutlined class="empty-icon" />
<div class="flex flex-col items-center justify-center py-20 bg-white rounded-2xl empty-state" v-if="!loading && filteredCourses.length === 0">
<div class="flex items-center justify-center w-20 h-20 rounded-[20px] mb-4 bg-[linear-gradient(135deg,#43e97b_0%,#38f9d7_100%)]">
<BookOutlined class="text-[40px] text-white" />
</div>
<p>{{ searchKeyword ? '未找到匹配的课程' : '暂无课程数据' }}</p>
<a-button type="primary" @click="showAuthModal">
<p class="text-[#636E72] text-base mb-6">{{ searchKeyword ? '未找到匹配的课程' : '暂无课程数据' }}</p>
<a-button type="primary" class="!bg-[linear-gradient(135deg,#43e97b_0%,#38f9d7_100%)] !border-0 rounded-xl" @click="showAuthModal">
授权第一门课程
</a-button>
</div>
<!-- 加载状态 -->
<div class="loading-state" v-if="loading">
<div class="flex flex-col items-center justify-center py-20" v-if="loading">
<a-spin size="large" />
<p>加载中...</p>
<p class="text-[#636E72] mt-4">加载中...</p>
</div>
<!-- 授权课程模态框 -->
<a-modal
v-model:open="authModalVisible"
width="800px"
class="auth-modal"
@ok="handleAuthModalOk"
@cancel="authModalVisible = false"
>
<template #title>
<span class="modal-title">
<StarFilled class="modal-title-icon" />
<span class="flex items-center gap-2">
<StarFilled class="text-[#43e97b] text-lg" />
授权新课程
</span>
</template>
<div class="auth-content">
<div class="flex flex-col gap-5">
<div class="auth-search">
<a-input-search
v-model:value="searchKeyword"
@ -191,26 +193,29 @@
</a-input-search>
</div>
<div class="available-courses" v-if="!authLoading && availableCourses.length > 0">
<div class="grid grid-cols-2 gap-3 max-h-[400px] overflow-y-auto available-courses" v-if="!authLoading && availableCourses.length > 0">
<div
v-for="course in availableCourses"
:key="course.id"
class="available-course-item"
:class="{ 'selected': selectedCourseIds.includes(course.id) }"
class="flex gap-3 p-3 bg-[#F8F9FA] rounded-xl cursor-pointer transition-all duration-200 border-2 border-transparent available-course-item hover:bg-[#FFF8F0]"
:class="selectedCourseIds.includes(course.id) ? 'border-[#43e97b] bg-[#E8F5E9]' : ''"
@click="toggleCourseSelection(course.id)"
>
<div class="course-checkbox">
<CheckCircleOutlined v-if="selectedCourseIds.includes(course.id)" class="checkbox-check" />
<div
class="w-6 h-6 border-2 rounded-md flex items-center justify-center text-white text-sm course-checkbox"
:class="selectedCourseIds.includes(course.id) ? 'bg-[#43e97b] border-[#43e97b]' : 'border-[#E0E0E0]'"
>
<CheckCircleOutlined v-if="selectedCourseIds.includes(course.id)" />
</div>
<div class="course-cover-small">
<ReadOutlined v-if="!course.pictureUrl" class="cover-small-icon" />
<img v-else :src="course.pictureUrl" alt="cover" />
<div class="w-[50px] h-[50px] rounded-lg flex items-center justify-center text-2xl overflow-hidden bg-[linear-gradient(135deg,#43e97b_0%,#38f9d7_100%)]">
<ReadOutlined v-if="!course.pictureUrl" class="text-white" />
<img v-else :src="course.pictureUrl" alt="cover" class="w-full h-full object-cover" />
</div>
<div class="course-info">
<div class="course-name-small">{{ course.name }}</div>
<div class="course-book-small">{{ course.pictureBookName }}</div>
<div class="course-tags-small">
<span v-for="tag in course.gradeTags.slice(0, 2)" :key="tag" class="tag-small">
<div class="flex-1 min-w-0">
<div class="text-sm font-500 text-[#2D3436]">{{ course.name }}</div>
<div class="text-[11px] text-[#636E72] mt-0.5">{{ course.pictureBookName }}</div>
<div class="flex gap-1 mt-1.5">
<span v-for="tag in course.gradeTags.slice(0, 2)" :key="tag" class="py-0.5 px-1.5 bg-[#E3F2FD] text-[#1976D2] rounded text-[10px]">
{{ tag }}
</span>
</div>
@ -218,17 +223,17 @@
</div>
</div>
<div class="empty-available" v-if="!authLoading && availableCourses.length === 0">
<InboxOutlined class="empty-icon-small" />
<p>没有可授权的课程</p>
<div class="flex flex-col items-center py-10" v-if="!authLoading && availableCourses.length === 0">
<InboxOutlined class="text-[48px] text-[#B2BEC3] mb-3" />
<p class="text-[#636E72] mt-2">没有可授权的课程</p>
</div>
<div class="loading-available" v-if="authLoading">
<div class="flex flex-col items-center py-10" v-if="authLoading">
<a-spin />
<p>搜索课程中...</p>
<p class="text-[#636E72] mt-2">搜索课程中...</p>
</div>
<div class="selected-info" v-if="selectedCourseIds.length > 0">
<div class="text-center py-3 px-3 bg-[#E8F5E9] rounded-lg text-[#43A047]" v-if="selectedCourseIds.length > 0">
已选择 <strong>{{ selectedCourseIds.length }}</strong> 门课程
</div>
</div>
@ -438,602 +443,24 @@ onMounted(() => {
</script>
<style scoped>
.course-list-view {
min-height: 100vh;
background: linear-gradient(180deg, #FFF8F0 0%, #FFFFFF 100%);
padding: 0;
}
/* 页面头部 */
.page-header {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
border-radius: 20px;
padding: 24px 32px;
margin-bottom: 24px;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
display: flex;
align-items: center;
gap: 16px;
}
.title-icon {
display: flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
background: rgba(255, 255, 255, 0.25);
border-radius: 14px;
backdrop-filter: blur(8px);
}
.title-icon :deep(.anticon) {
font-size: 32px;
color: white;
}
.title-text h2 {
color: white;
font-size: 24px;
font-weight: 700;
margin: 0;
}
.title-text p {
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
margin: 4px 0 0 0;
}
.header-stats {
display: flex;
gap: 32px;
}
.stat-item {
text-align: center;
}
.stat-value {
display: block;
font-size: 32px;
font-weight: 700;
color: white;
}
.stat-value.active {
color: #FFD93D;
}
.stat-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
}
/* 筛选操作栏 */
.filter-action-bar {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 24px;
padding: 20px 24px;
background: white;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.grade-tabs {
display: flex;
align-items: center;
gap: 16px;
}
.tab-label {
font-size: 14px;
font-weight: 600;
color: #333;
white-space: nowrap;
}
.tab-buttons {
display: flex;
gap: 8px;
}
.grade-tab {
padding: 8px 20px;
border-radius: 10px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
border: none;
background: #F5F5F5;
color: #666;
}
.grade-tab:hover {
background: #E8F5E9;
color: #43e97b;
}
.grade-tab.active {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
color: white;
box-shadow: 0 4px 12px rgba(67, 233, 123, 0.3);
}
.action-row {
display: flex;
justify-content: space-between;
align-items: center;
}
/* 操作栏 */
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 16px 20px;
background: white;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.filter-section {
display: flex;
align-items: center;
gap: 16px;
}
.filter-tabs {
display: flex;
gap: 8px;
}
.filter-tab {
padding: 8px 20px;
border-radius: 10px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
border: none;
background: transparent;
color: #666;
}
.filter-tab:hover {
background: #F0FFF4;
color: #43e97b;
}
.filter-tab.active {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
color: white;
box-shadow: 0 4px 12px rgba(67, 233, 123, 0.3);
}
.action-buttons {
display: flex;
align-items: center;
gap: 12px;
}
.search-box :deep(.ant-input-affix-wrapper) {
border-radius: 12px;
border: 2px solid #F0F0F0;
}
.search-box :deep(.ant-input-affix-wrapper:hover) {
border-color: #43e97b;
}
.auth-btn {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
border: none;
border-radius: 12px;
height: 40px;
padding: 0 24px;
font-weight: 600;
}
.auth-btn:hover {
background: linear-gradient(135deg, #32d86c 0%, #2ed9c0 100%);
}
.btn-icon {
margin-right: 8px;
font-size: 14px;
}
/* 课程卡片网格 */
.course-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.course-card {
background: white;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.course-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.course-card.unauthorized {
opacity: 0.8;
}
.card-cover {
position: relative;
height: 140px;
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
.card-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.cover-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.cover-icon {
font-size: 48px;
color: rgba(255, 255, 255, 0.9);
}
.auth-badge {
position: absolute;
top: 12px;
right: 12px;
padding: 4px 12px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
display: flex;
align-items: center;
gap: 4px;
}
.auth-badge.authorized {
background: rgba(255, 255, 255, 0.9);
color: #43A047;
}
.auth-badge.unauthorized {
background: rgba(0, 0, 0, 0.5);
color: white;
}
.card-body {
padding: 16px;
}
.course-name {
font-size: 16px;
font-weight: 600;
color: #2D3436;
margin: 0 0 4px 0;
}
.course-book {
font-size: 12px;
color: #636E72;
margin: 0 0 12px 0;
}
.course-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 12px;
}
.tag {
padding: 2px 8px;
border-radius: 8px;
font-size: 10px;
}
.course-meta {
display: flex;
gap: 16px;
}
.meta-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #636E72;
}
.meta-icon {
font-size: 14px;
color: #43e97b;
}
.card-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 12px 16px;
border-top: 1px solid #F0F0F0;
background: #FAFAFA;
}
.card-actions :deep(.ant-btn-link) {
padding: 4px 8px;
height: auto;
display: flex;
align-items: center;
gap: 4px;
}
.auth-action {
color: #43e97b !important;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
background: white;
border-radius: 16px;
}
.empty-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
border-radius: 20px;
margin-bottom: 16px;
}
.empty-icon {
font-size: 40px;
color: white;
}
.empty-state p {
color: #636E72;
font-size: 16px;
margin-bottom: 24px;
}
.empty-state :deep(.ant-btn-primary) {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
border: none;
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
}
.loading-state p {
color: #636E72;
margin-top: 16px;
}
/* 授权模态框 */
.auth-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.auth-search :deep(.ant-input-affix-wrapper) {
border-radius: 12px;
}
.available-courses {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
max-height: 400px;
overflow-y: auto;
}
.available-course-item {
display: flex;
gap: 12px;
padding: 12px;
background: #F8F9FA;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease;
border: 2px solid transparent;
}
.available-course-item:hover {
background: #FFF8F0;
}
.available-course-item.selected {
border-color: #43e97b;
background: #E8F5E9;
}
.course-checkbox {
width: 24px;
height: 24px;
border: 2px solid #E0E0E0;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 14px;
}
.available-course-item.selected .course-checkbox {
background: #43e97b;
border-color: #43e97b;
}
.course-cover-small {
width: 50px;
height: 50px;
border-radius: 8px;
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
overflow: hidden;
}
.course-cover-small img {
width: 100%;
height: 100%;
object-fit: cover;
}
.course-info {
flex: 1;
}
.course-name-small {
font-size: 14px;
font-weight: 500;
color: #2D3436;
}
.course-book-small {
font-size: 11px;
color: #636E72;
margin-top: 2px;
}
.course-tags-small {
display: flex;
gap: 4px;
margin-top: 6px;
}
.tag-small {
padding: 1px 6px;
background: #E3F2FD;
color: #1976D2;
border-radius: 4px;
font-size: 10px;
}
.empty-available,
.loading-available {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 0;
}
.empty-available p,
.loading-available p {
color: #636E72;
margin-top: 8px;
}
.selected-info {
text-align: center;
padding: 12px;
background: #E8F5E9;
border-radius: 8px;
color: #43A047;
}
/* 模态框标题 */
.modal-title {
display: flex;
align-items: center;
gap: 8px;
}
.modal-title-icon {
color: #43e97b;
font-size: 18px;
}
/* 小封面图标 */
.cover-small-icon {
font-size: 24px;
color: white;
}
/* 复选框勾选 */
.checkbox-check {
font-size: 14px;
}
/* 空状态小图标 */
.empty-icon-small {
font-size: 48px;
color: #B2BEC3;
margin-bottom: 12px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 16px;
text-align: center;
}
.header-stats {
width: 100%;
justify-content: center;
}
.action-bar {
flex-direction: column;
gap: 12px;
}
.search-box :deep(.ant-input-search) {
width: 100% !important;
}
.course-grid {
grid-template-columns: 1fr;
grid-template-columns: 1fr !important;
}
.available-courses {
grid-template-columns: 1fr;
grid-template-columns: 1fr !important;
}
}
</style>

View File

@ -1,46 +1,46 @@
<template>
<div class="feedback-view">
<div class="min-h-100vh p-0 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)]">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-content">
<div class="header-title">
<div class="title-icon-wrapper">
<div class="rounded-[20px] py-6 px-8 mb-6 bg-[linear-gradient(135deg,#11998e_0%,#38ef7d_100%)]">
<div class="flex justify-between items-center">
<div class="flex items-center gap-4">
<div class="w-16 h-16 rounded-2xl flex items-center justify-center text-[32px] text-white bg-white/20">
<MessageOutlined />
</div>
<div class="title-text">
<h2>课程反馈</h2>
<p>查看教师课程反馈与评分情况</p>
<div>
<h2 class="text-white text-2xl font-700 m-0">课程反馈</h2>
<p class="text-white/80 text-sm mt-1 m-0">查看教师课程反馈与评分情况</p>
</div>
</div>
<div class="header-stats">
<div class="stat-item">
<span class="stat-value">{{ stats.totalFeedbacks }}</span>
<span class="stat-label">反馈总数</span>
<div class="flex gap-8">
<div class="text-center">
<span class="block text-[28px] font-700 text-white">{{ stats.totalFeedbacks }}</span>
<span class="text-xs text-white/80">反馈总数</span>
</div>
<div class="stat-item">
<span class="stat-value quality">{{ stats.avgDesignQuality.toFixed(1) }}</span>
<span class="stat-label">设计质量</span>
<div class="text-center">
<span class="block text-[28px] font-700 text-[#FFD93D]">{{ stats.avgDesignQuality.toFixed(1) }}</span>
<span class="text-xs text-white/80">设计质量</span>
</div>
<div class="stat-item">
<span class="stat-value participation">{{ stats.avgParticipation.toFixed(1) }}</span>
<span class="stat-label">参与度</span>
<div class="text-center">
<span class="block text-[28px] font-700 text-[#74b9ff]">{{ stats.avgParticipation.toFixed(1) }}</span>
<span class="text-xs text-white/80">参与度</span>
</div>
<div class="stat-item">
<span class="stat-value achievement">{{ stats.avgGoalAchievement.toFixed(1) }}</span>
<span class="stat-label">目标达成</span>
<div class="text-center">
<span class="block text-[28px] font-700 text-[#FFD93D]">{{ stats.avgGoalAchievement.toFixed(1) }}</span>
<span class="text-xs text-white/80">目标达成</span>
</div>
</div>
</div>
</div>
<!-- 操作栏 -->
<div class="action-bar">
<div class="filters">
<div class="flex justify-between items-center mb-6 py-4 px-5 bg-white rounded-2xl shadow-[0_2px_8px_rgba(0,0,0,0.04)]">
<div class="flex gap-3">
<a-select
v-model:value="filters.teacherId"
placeholder="选择教师"
allow-clear
style="width: 150px;"
class="w-[150px]"
@change="handleFilter"
>
<a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
@ -50,7 +50,7 @@
<a-input-search
v-model:value="filters.keyword"
placeholder="搜索课程名称"
style="width: 200px;"
class="w-[200px]"
@search="handleFilter"
allow-clear
/>
@ -58,77 +58,71 @@
</div>
<!-- 反馈卡片网格 -->
<div class="feedback-grid" v-if="!loading && feedbacks.length > 0">
<div class="grid gap-5 mb-6 feedback-grid" style="grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));" v-if="!loading && feedbacks.length > 0">
<div
v-for="feedback in feedbacks"
:key="feedback.id"
class="feedback-card"
class="bg-white rounded-2xl overflow-hidden shadow-[0_4px_12px_rgba(0,0,0,0.05)] transition-all duration-300 border-t-4 border-t-[#11998e] hover:translate-y-[-4px] hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)]"
>
<div class="card-header">
<div class="course-info">
<div class="course-icon-wrapper">
<div class="flex justify-between items-start py-4 px-4 bg-[linear-gradient(135deg,#F8F9FA_0%,#FFFFFF_100%)] border-b border-[#F0F0F0]">
<div class="flex items-center gap-3">
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-white text-[22px] bg-[linear-gradient(135deg,#11998e_0%,#38ef7d_100%)]">
<BookOutlined />
</div>
<div class="course-details">
<h4 class="course-name">{{ feedback.lesson?.course?.name }}</h4>
<p class="picture-book">{{ feedback.lesson?.course?.pictureBookName || '-' }}</p>
<div>
<h4 class="text-[15px] font-600 text-[#2D3436] m-0">{{ feedback.lesson?.course?.name }}</h4>
<p class="text-xs text-[#636E72] mt-1 m-0">{{ feedback.lesson?.course?.pictureBookName || '-' }}</p>
</div>
</div>
<div class="feedback-time">
<div class="flex items-center gap-1 text-[11px] text-[#B2BEC3]">
<ClockCircleOutlined />
<span>{{ formatDate(feedback.lesson?.startDatetime) }}</span>
</div>
</div>
<div class="card-body">
<div class="teacher-info">
<div class="teacher-avatar">
<div class="p-4">
<div class="flex items-center gap-3 mb-4 p-3 bg-[#F8F9FA] rounded-xl">
<div class="w-10 h-10 rounded-full flex items-center justify-center text-base font-600 text-white bg-[linear-gradient(135deg,#11998e_0%,#38ef7d_100%)]">
{{ feedback.teacher?.name?.charAt(0) || '师' }}
</div>
<div class="teacher-details">
<span class="teacher-name">{{ feedback.teacher?.name }}</span>
<span class="class-name">{{ feedback.lesson?.class?.name || '-' }}</span>
<div class="flex flex-col">
<span class="text-sm font-600 text-[#2D3436]">{{ feedback.teacher?.name }}</span>
<span class="text-xs text-[#636E72]">{{ feedback.lesson?.class?.name || '-' }}</span>
</div>
</div>
<div class="ratings-grid">
<div class="rating-item">
<div class="rating-header">
<BgColorsOutlined class="rating-icon design-icon" />
<span class="rating-label">设计质量</span>
</div>
<div class="rating-stars">
<a-rate :value="feedback.designQuality" disabled :count="5" style="font-size: 14px;" />
<div class="flex flex-col gap-2.5 mb-3">
<div class="flex justify-between items-center py-2 px-3 bg-[#FAFAFA] rounded-lg">
<div class="flex items-center gap-1.5">
<BgColorsOutlined class="text-sm text-[#667eea]" />
<span class="text-xs text-[#636E72]">设计质量</span>
</div>
<a-rate :value="feedback.designQuality" disabled :count="5" class="text-sm" />
</div>
<div class="rating-item">
<div class="rating-header">
<TeamOutlined class="rating-icon participation-icon" />
<span class="rating-label">参与度</span>
</div>
<div class="rating-stars">
<a-rate :value="feedback.participation" disabled :count="5" style="font-size: 14px;" />
<div class="flex justify-between items-center py-2 px-3 bg-[#FAFAFA] rounded-lg">
<div class="flex items-center gap-1.5">
<TeamOutlined class="text-sm text-[#f5576c]" />
<span class="text-xs text-[#636E72]">参与度</span>
</div>
<a-rate :value="feedback.participation" disabled :count="5" class="text-sm" />
</div>
<div class="rating-item">
<div class="rating-header">
<AimOutlined class="rating-icon achievement-icon" />
<span class="rating-label">目标达成</span>
</div>
<div class="rating-stars">
<a-rate :value="feedback.goalAchievement" disabled :count="5" style="font-size: 14px;" />
<div class="flex justify-between items-center py-2 px-3 bg-[#FAFAFA] rounded-lg">
<div class="flex items-center gap-1.5">
<AimOutlined class="text-sm text-[#FF8C42]" />
<span class="text-xs text-[#636E72]">目标达成</span>
</div>
<a-rate :value="feedback.goalAchievement" disabled :count="5" class="text-sm" />
</div>
</div>
<div class="feedback-summary" v-if="feedback.pros || feedback.suggestions">
<p class="summary-text">
<div class="pt-3 border-t border-[#F0F0F0]" v-if="feedback.pros || feedback.suggestions">
<p class="text-[13px] text-[#636E72] leading-normal m-0">
{{ (feedback.pros || feedback.suggestions || '')?.substring(0, 60) }}{{ (feedback.pros || feedback.suggestions || '').length > 60 ? '...' : '' }}
</p>
</div>
</div>
<div class="card-actions">
<div class="flex justify-end py-3 px-4 border-t border-[#F0F0F0] bg-[#FAFAFA]">
<a-button type="link" size="small" @click="handleView(feedback)">
<FileTextOutlined />
查看详情
@ -138,22 +132,22 @@
</div>
<!-- 空状态 -->
<div class="empty-state" v-if="!loading && feedbacks.length === 0">
<div class="empty-icon-wrapper">
<div class="flex flex-col items-center justify-center py-20 bg-white rounded-2xl" v-if="!loading && feedbacks.length === 0">
<div class="w-20 h-20 rounded-full flex items-center justify-center mb-4 text-[36px] text-white bg-[linear-gradient(135deg,#11998e_0%,#38ef7d_100%)]">
<MessageOutlined />
</div>
<p>暂无课程反馈</p>
<p class="empty-hint">教师完成课程后会在这里显示反馈记录</p>
<p class="text-[#636E72] text-base mb-2">暂无课程反馈</p>
<p class="text-[13px] text-[#B2BEC3]">教师完成课程后会在这里显示反馈记录</p>
</div>
<!-- 加载状态 -->
<div class="loading-state" v-if="loading">
<div class="flex flex-col items-center justify-center py-20" v-if="loading">
<a-spin size="large" />
<p>加载中...</p>
<p class="text-[#636E72] mt-4">加载中...</p>
</div>
<!-- 分页 -->
<div class="pagination-wrapper" v-if="feedbacks.length > 0">
<div class="flex justify-center py-6 bg-white rounded-2xl" v-if="feedbacks.length > 0">
<a-pagination
v-model:current="pagination.current"
v-model:pageSize="pagination.pageSize"
@ -169,94 +163,93 @@
v-model:open="detailModalVisible"
width="700px"
:footer="null"
class="feedback-detail-modal"
>
<template #title>
<div class="modal-title">
<MessageOutlined class="modal-title-icon" />
<div class="flex items-center gap-2">
<MessageOutlined class="text-[#11998e] text-lg" />
<span>{{ currentFeedback?.lesson?.course?.name || '反馈详情' }}</span>
</div>
</template>
<div class="detail-content" v-if="currentFeedback">
<div class="detail-header">
<div class="course-cover">
<ReadOutlined class="cover-icon" />
<div class="p-0" v-if="currentFeedback">
<div class="flex gap-5 py-5 px-5 rounded-2xl mb-6 bg-[linear-gradient(135deg,#11998e_0%,#38ef7d_100%)]">
<div class="w-20 h-20 rounded-xl flex items-center justify-center bg-white/20">
<ReadOutlined class="text-[36px] text-white" />
</div>
<div class="course-meta">
<h3>{{ currentFeedback.lesson?.course?.name }}</h3>
<p class="picture-book-name">{{ currentFeedback.lesson?.course?.pictureBookName || '-' }}</p>
<div class="meta-tags">
<span class="meta-tag"><UserOutlined /> {{ currentFeedback.teacher?.name }}</span>
<span class="meta-tag"><HomeOutlined /> {{ currentFeedback.lesson?.class?.name }}</span>
<span class="meta-tag"><CalendarOutlined /> {{ formatDate(currentFeedback.lesson?.startDatetime) }}</span>
<div class="flex-1 min-w-0">
<h3 class="text-white text-lg font-600 m-0 mb-1">{{ currentFeedback.lesson?.course?.name }}</h3>
<p class="text-white/80 text-[13px] m-0 mb-3">{{ currentFeedback.lesson?.course?.pictureBookName || '-' }}</p>
<div class="flex flex-wrap gap-2">
<span class="py-1 px-2.5 rounded-xl text-xs text-white bg-white/20 inline-flex items-center gap-1"><UserOutlined /> {{ currentFeedback.teacher?.name }}</span>
<span class="py-1 px-2.5 rounded-xl text-xs text-white bg-white/20 inline-flex items-center gap-1"><HomeOutlined /> {{ currentFeedback.lesson?.class?.name }}</span>
<span class="py-1 px-2.5 rounded-xl text-xs text-white bg-white/20 inline-flex items-center gap-1"><CalendarOutlined /> {{ formatDate(currentFeedback.lesson?.startDatetime) }}</span>
</div>
</div>
</div>
<div class="detail-ratings">
<div class="rating-card">
<div class="rating-score">
<span class="score-value">{{ currentFeedback.designQuality || 0 }}</span>
<span class="score-max">/5</span>
<div class="grid grid-cols-3 gap-4 mb-6 detail-ratings">
<div class="text-center py-5 px-4 bg-[#F8F9FA] rounded-2xl">
<div class="mb-3">
<span class="text-[32px] font-700 text-[#2D3436]">{{ currentFeedback.designQuality || 0 }}</span>
<span class="text-sm text-[#B2BEC3]">/5</span>
</div>
<div class="rating-icon-wrapper design">
<div class="w-12 h-12 mx-auto mb-3 rounded-full flex items-center justify-center text-[22px] text-white bg-[linear-gradient(135deg,#667eea_0%,#764ba2_100%)]">
<BgColorsOutlined />
</div>
<div class="rating-name">设计质量</div>
<a-rate :value="currentFeedback.designQuality" disabled :count="5" style="font-size: 12px;" />
<div class="text-sm font-600 text-[#2D3436] mb-2">设计质量</div>
<a-rate :value="currentFeedback.designQuality" disabled :count="5" class="text-xs" />
</div>
<div class="rating-card">
<div class="rating-score">
<span class="score-value">{{ currentFeedback.participation || 0 }}</span>
<span class="score-max">/5</span>
<div class="text-center py-5 px-4 bg-[#F8F9FA] rounded-2xl">
<div class="mb-3">
<span class="text-[32px] font-700 text-[#2D3436]">{{ currentFeedback.participation || 0 }}</span>
<span class="text-sm text-[#B2BEC3]">/5</span>
</div>
<div class="rating-icon-wrapper participation">
<div class="w-12 h-12 mx-auto mb-3 rounded-full flex items-center justify-center text-[22px] text-white bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)]">
<TeamOutlined />
</div>
<div class="rating-name">参与度</div>
<a-rate :value="currentFeedback.participation" disabled :count="5" style="font-size: 12px;" />
<div class="text-sm font-600 text-[#2D3436] mb-2">参与度</div>
<a-rate :value="currentFeedback.participation" disabled :count="5" class="text-xs" />
</div>
<div class="rating-card">
<div class="rating-score">
<span class="score-value">{{ currentFeedback.goalAchievement || 0 }}</span>
<span class="score-max">/5</span>
<div class="text-center py-5 px-4 bg-[#F8F9FA] rounded-2xl">
<div class="mb-3">
<span class="text-[32px] font-700 text-[#2D3436]">{{ currentFeedback.goalAchievement || 0 }}</span>
<span class="text-sm text-[#B2BEC3]">/5</span>
</div>
<div class="rating-icon-wrapper achievement">
<div class="w-12 h-12 mx-auto mb-3 rounded-full flex items-center justify-center text-[22px] text-white bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)]">
<AimOutlined />
</div>
<div class="rating-name">目标达成</div>
<a-rate :value="currentFeedback.goalAchievement" disabled :count="5" style="font-size: 12px;" />
<div class="text-sm font-600 text-[#2D3436] mb-2">目标达成</div>
<a-rate :value="currentFeedback.goalAchievement" disabled :count="5" class="text-xs" />
</div>
</div>
<div class="detail-section" v-if="currentFeedback.pros">
<div class="section-header">
<div class="section-icon-wrapper pros">
<div class="mb-5 p-4 bg-[#F8F9FA] rounded-xl" v-if="currentFeedback.pros">
<div class="flex items-center gap-2 mb-3">
<div class="w-7 h-7 rounded-lg flex items-center justify-center text-sm text-white bg-[linear-gradient(135deg,#FFD93D_0%,#FF9500_100%)]">
<StarFilled />
</div>
<h4>课程优点</h4>
<h4 class="m-0 text-[15px] font-600 text-[#2D3436]">课程优点</h4>
</div>
<p class="section-content">{{ currentFeedback.pros }}</p>
<p class="text-sm leading-[1.8] text-[#636E72] m-0">{{ currentFeedback.pros }}</p>
</div>
<div class="detail-section" v-if="currentFeedback.suggestions">
<div class="section-header">
<div class="section-icon-wrapper suggestions">
<div class="mb-5 p-4 bg-[#F8F9FA] rounded-xl" v-if="currentFeedback.suggestions">
<div class="flex items-center gap-2 mb-3">
<div class="w-7 h-7 rounded-lg flex items-center justify-center text-sm text-white bg-[linear-gradient(135deg,#667eea_0%,#764ba2_100%)]">
<BulbOutlined />
</div>
<h4>改进建议</h4>
<h4 class="m-0 text-[15px] font-600 text-[#2D3436]">改进建议</h4>
</div>
<p class="section-content">{{ currentFeedback.suggestions }}</p>
<p class="text-sm leading-[1.8] text-[#636E72] m-0">{{ currentFeedback.suggestions }}</p>
</div>
<div class="detail-section" v-if="currentFeedback.activitiesDone && currentFeedback.activitiesDone.length">
<div class="section-header">
<div class="section-icon-wrapper activities">
<div class="mb-5 p-4 bg-[#F8F9FA] rounded-xl" v-if="currentFeedback.activitiesDone && currentFeedback.activitiesDone.length">
<div class="flex items-center gap-2 mb-3">
<div class="w-7 h-7 rounded-lg flex items-center justify-center text-sm text-white bg-[linear-gradient(135deg,#11998e_0%,#38ef7d_100%)]">
<TrophyOutlined />
</div>
<h4>完成的活动</h4>
<h4 class="m-0 text-[15px] font-600 text-[#2D3436]">完成的活动</h4>
</div>
<div class="activity-tags">
<div class="flex flex-wrap gap-2">
<a-tag v-for="(activity, index) in currentFeedback.activitiesDone" :key="index" color="green">
{{ activity }}
</a-tag>
@ -379,560 +372,10 @@ onMounted(() => {
</script>
<style scoped>
.feedback-view {
min-height: 100vh;
background: linear-gradient(180deg, #FFF8F0 0%, #FFFFFF 100%);
padding: 0;
}
/* 页面头部 */
.page-header {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
border-radius: 20px;
padding: 24px 32px;
margin-bottom: 24px;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
display: flex;
align-items: center;
gap: 16px;
}
.title-icon-wrapper {
width: 64px;
height: 64px;
background: rgba(255, 255, 255, 0.2);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
color: white;
}
.title-text h2 {
color: white;
font-size: 24px;
font-weight: 700;
margin: 0;
}
.title-text p {
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
margin: 4px 0 0 0;
}
.header-stats {
display: flex;
gap: 32px;
}
.stat-item {
text-align: center;
}
.stat-value {
display: block;
font-size: 28px;
font-weight: 700;
color: white;
}
.stat-value.quality {
color: #FFD93D;
}
.stat-value.participation {
color: #74b9ff;
}
.stat-value.achievement {
color: #FFD93D;
}
.stat-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
}
/* 操作栏 */
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 16px 20px;
background: white;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.filters {
display: flex;
gap: 12px;
}
/* 反馈卡片网格 */
.feedback-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.feedback-card {
background: white;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
border-top: 4px solid #11998e;
}
.feedback-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 16px;
background: linear-gradient(135deg, #F8F9FA 0%, #FFFFFF 100%);
border-bottom: 1px solid #F0F0F0;
}
.course-info {
display: flex;
align-items: center;
gap: 12px;
}
.course-icon-wrapper {
width: 48px;
height: 48px;
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 22px;
}
.course-name {
font-size: 15px;
font-weight: 600;
color: #2D3436;
margin: 0;
}
.picture-book {
font-size: 12px;
color: #636E72;
margin: 4px 0 0 0;
}
.feedback-time {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: #B2BEC3;
}
.card-body {
padding: 16px;
}
.teacher-info {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
padding: 12px;
background: #F8F9FA;
border-radius: 12px;
}
.teacher-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 600;
color: white;
}
.teacher-details {
display: flex;
flex-direction: column;
}
.teacher-name {
font-size: 14px;
font-weight: 600;
color: #2D3436;
}
.class-name {
font-size: 12px;
color: #636E72;
}
.ratings-grid {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 12px;
}
.rating-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #FAFAFA;
border-radius: 8px;
}
.rating-header {
display: flex;
align-items: center;
gap: 6px;
}
.rating-icon {
font-size: 14px;
}
.design-icon {
color: #667eea;
}
.participation-icon {
color: #f5576c;
}
.achievement-icon {
color: #FF8C42;
}
.rating-label {
font-size: 12px;
color: #636E72;
}
.feedback-summary {
padding-top: 12px;
border-top: 1px solid #F0F0F0;
}
.summary-text {
font-size: 13px;
color: #636E72;
line-height: 1.5;
margin: 0;
}
.card-actions {
display: flex;
justify-content: flex-end;
padding: 12px 16px;
border-top: 1px solid #F0F0F0;
background: #FAFAFA;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
background: white;
border-radius: 16px;
}
.empty-icon-wrapper {
width: 80px;
height: 80px;
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
font-size: 36px;
color: white;
}
.empty-state p {
color: #636E72;
font-size: 16px;
margin-bottom: 8px;
}
.empty-hint {
font-size: 13px !important;
color: #B2BEC3 !important;
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
}
.loading-state p {
color: #636E72;
margin-top: 16px;
}
/* 分页 */
.pagination-wrapper {
display: flex;
justify-content: center;
padding: 24px;
background: white;
border-radius: 16px;
}
/* 详情弹窗 */
.modal-title {
display: flex;
align-items: center;
gap: 8px;
}
.modal-title-icon {
color: #11998e;
font-size: 18px;
}
.detail-content {
padding: 0;
}
.detail-header {
display: flex;
gap: 20px;
padding: 20px;
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
border-radius: 16px;
margin-bottom: 24px;
}
.course-cover {
width: 80px;
height: 80px;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.cover-icon {
font-size: 36px;
color: white;
}
.course-meta h3 {
color: white;
font-size: 18px;
font-weight: 600;
margin: 0 0 4px 0;
}
.picture-book-name {
color: rgba(255, 255, 255, 0.8);
font-size: 13px;
margin: 0 0 12px 0;
}
.meta-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.meta-tag {
padding: 4px 10px;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
font-size: 12px;
color: white;
display: inline-flex;
align-items: center;
gap: 4px;
}
.detail-ratings {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.rating-card {
text-align: center;
padding: 20px 16px;
background: #F8F9FA;
border-radius: 16px;
}
.rating-score {
margin-bottom: 12px;
}
.score-value {
font-size: 32px;
font-weight: 700;
color: #2D3436;
}
.score-max {
font-size: 14px;
color: #B2BEC3;
}
.rating-icon-wrapper {
width: 48px;
height: 48px;
margin: 0 auto 12px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
color: white;
}
.rating-icon-wrapper.design {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.rating-icon-wrapper.participation {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.rating-icon-wrapper.achievement {
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
}
.rating-name {
font-size: 14px;
font-weight: 600;
color: #2D3436;
margin-bottom: 8px;
}
.detail-section {
margin-bottom: 20px;
padding: 16px;
background: #F8F9FA;
border-radius: 12px;
}
.section-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.section-icon-wrapper {
width: 28px;
height: 28px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: white;
}
.section-icon-wrapper.pros {
background: linear-gradient(135deg, #FFD93D 0%, #FF9500 100%);
}
.section-icon-wrapper.suggestions {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.section-icon-wrapper.activities {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.section-header h4 {
margin: 0;
font-size: 15px;
font-weight: 600;
color: #2D3436;
}
.section-content {
font-size: 14px;
line-height: 1.8;
color: #636E72;
margin: 0;
}
.activity-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 16px;
text-align: center;
}
.header-stats {
flex-wrap: wrap;
justify-content: center;
}
.action-bar {
flex-direction: column;
gap: 12px;
}
.filters {
width: 100%;
flex-wrap: wrap;
}
.feedback-grid {
grid-template-columns: 1fr;
grid-template-columns: 1fr !important;
}
.detail-header {
flex-direction: column;
text-align: center;
}
.detail-ratings {
grid-template-columns: 1fr;
}

View File

@ -1,34 +1,34 @@
<template>
<div class="growth-record-view">
<div class="min-h-100vh p-0 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)]">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-content">
<div class="header-title">
<div class="title-icon-wrapper">
<CameraOutlined class="title-icon" />
<div class="rounded-[20px] py-6 px-8 mb-6 bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)]">
<div class="flex justify-between items-center max-md:flex-col max-md:gap-4 max-md:text-center">
<div class="flex items-center gap-4">
<div class="w-14 h-14 rounded-2xl flex items-center justify-center bg-white/20">
<CameraOutlined class="text-[28px] text-white" />
</div>
<div class="title-text">
<h2>成长档案</h2>
<p>记录学生的每一个成长瞬间</p>
<div>
<h2 class="text-white text-2xl font-700 m-0">成长档案</h2>
<p class="text-white/80 text-sm mt-1 m-0">记录学生的每一个成长瞬间</p>
</div>
</div>
<div class="header-stats">
<div class="stat-item">
<span class="stat-value">{{ records.length }}</span>
<span class="stat-label">档案总数</span>
<div class="flex gap-8 header-stats">
<div class="text-center stat-item">
<span class="block text-[32px] font-700 text-white">{{ records.length }}</span>
<span class="text-xs text-white/80">档案总数</span>
</div>
</div>
</div>
</div>
<!-- 操作栏 -->
<div class="action-bar">
<div class="filters">
<div class="flex justify-between items-center mb-6 py-4 px-5 bg-white rounded-2xl shadow-[0_2px_8px_rgba(0,0,0,0.04)] max-md:flex-col max-md:gap-3">
<div class="flex gap-3 max-md:w-full flex-wrap filters">
<a-select
v-model:value="filters.classId"
placeholder="选择班级"
allow-clear
style="width: 150px;"
class="w-[150px]"
@change="handleFilter"
>
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
@ -38,67 +38,70 @@
<a-input-search
v-model:value="filters.keyword"
placeholder="搜索标题"
style="width: 200px;"
class="w-[200px]"
@search="handleFilter"
allow-clear
/>
</div>
<a-button type="primary" class="add-btn" @click="showAddModal">
<PlusOutlined class="btn-icon" />
<a-button type="primary" class="!bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)] hover:!bg-[linear-gradient(135deg,#e080e8_0%,#e04a5d_100%)] !border-0 rounded-xl h-10 px-6 font-600" @click="showAddModal">
<PlusOutlined class="mr-2 text-sm" />
添加档案
</a-button>
</div>
<!-- 档案卡片网格 -->
<div class="record-grid" v-if="!loading && records.length > 0">
<div class="grid gap-5 mb-6 grid-cols-1 md:grid-cols-[repeat(auto-fill,minmax(300px,1fr))]" v-if="!loading && records.length > 0">
<div
v-for="record in records"
:key="record.id"
class="record-card"
class="bg-white rounded-2xl overflow-hidden shadow-[0_4px_12px_rgba(0,0,0,0.05)] transition-all duration-300 hover:translate-y-[-4px] hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)]"
>
<div class="card-cover">
<div v-if="record.images?.length" class="cover-image">
<img :src="getImageUrl(record.images[0])" alt="cover" />
<div class="image-count" v-if="record.images.length > 1">
<div class="relative h-[160px] bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)] card-cover">
<div v-if="record.images?.length" class="w-full h-full cover-image">
<img :src="getImageUrl(record.images[0])" alt="cover" class="w-full h-full object-cover" />
<div class="absolute bottom-2 right-2 py-1 px-2 rounded-lg text-xs text-white bg-black/60 flex items-center gap-1" v-if="record.images.length > 1">
<CameraOutlined /> {{ record.images.length }}
</div>
</div>
<div v-else class="cover-placeholder">
<FileTextOutlined class="placeholder-icon" />
<div v-else class="w-full h-full flex items-center justify-center bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)] cover-placeholder">
<FileTextOutlined class="text-[48px] text-white/80" />
</div>
<div class="type-badge" :class="record.recordType === 'STUDENT' ? 'personal' : 'class'">
<UserOutlined v-if="record.recordType === 'STUDENT'" class="badge-icon" />
<TeamOutlined v-else class="badge-icon" />
<div
class="absolute top-3 right-3 py-1 px-3 rounded-xl text-[11px] font-600 flex items-center gap-1"
:class="record.recordType === 'STUDENT' ? 'bg-[#E3F2FD] text-[#1976D2]' : 'bg-[#FCE4EC] text-[#E91E63]'"
>
<UserOutlined v-if="record.recordType === 'STUDENT'" class="text-xs" />
<TeamOutlined v-else class="text-xs" />
{{ record.recordType === 'STUDENT' ? ' 个人' : ' 班级' }}
</div>
</div>
<div class="card-body">
<h3 class="record-title">{{ record.title }}</h3>
<div class="record-meta">
<div class="meta-item">
<UserOutlined class="meta-icon" />
<div class="p-4 card-body">
<h3 class="text-base font-600 text-[#2D3436] m-0 mb-3">{{ record.title }}</h3>
<div class="flex flex-wrap gap-3 mb-3 record-meta">
<div class="flex items-center gap-1 text-xs text-[#636E72]">
<UserOutlined class="text-xs text-[#f5576c]" />
<span>{{ record.student?.name || '班级档案' }}</span>
</div>
<div class="meta-item">
<HomeOutlined class="meta-icon" />
<div class="flex items-center gap-1 text-xs text-[#636E72]">
<HomeOutlined class="text-xs text-[#f5576c]" />
<span>{{ record.class?.name || '-' }}</span>
</div>
<div class="meta-item">
<CalendarOutlined class="meta-icon" />
<div class="flex items-center gap-1 text-xs text-[#636E72]">
<CalendarOutlined class="text-xs text-[#f5576c]" />
<span>{{ formatDate(record.recordDate) }}</span>
</div>
</div>
<p class="record-content">{{ record.content?.substring(0, 80) }}{{ record.content?.length > 80 ? '...' : '' }}</p>
<p class="text-[13px] text-[#636E72] leading-[1.6] m-0">{{ record.content?.substring(0, 80) }}{{ record.content?.length > 80 ? '...' : '' }}</p>
</div>
<div class="card-actions">
<a-button type="link" size="small" @click="handleView(record)">
<div class="flex justify-end gap-2 py-3 px-4 border-t border-[#F0F0F0] bg-[#FAFAFA] card-actions">
<a-button type="link" size="small" @click="handleView(record)" class="!py-1 !px-2 !h-auto">
<EyeOutlined /> 查看
</a-button>
<a-button type="link" size="small" @click="handleEdit(record)">
<a-button type="link" size="small" @click="handleEdit(record)" class="!py-1 !px-2 !h-auto">
<EditOutlined /> 编辑
</a-button>
<a-popconfirm title="确定删除此档案?" @confirm="handleDelete(record.id)">
<a-button type="link" size="small" danger>
<a-button type="link" size="small" danger class="!py-1 !px-2 !h-auto">
<DeleteOutlined /> 删除
</a-button>
</a-popconfirm>
@ -107,20 +110,20 @@
</div>
<!-- 空状态 -->
<div class="empty-state" v-if="!loading && records.length === 0">
<div class="empty-icon-wrapper">
<FileTextOutlined class="empty-icon" />
<div class="flex flex-col items-center justify-center py-20 bg-white rounded-2xl empty-state" v-if="!loading && records.length === 0">
<div class="w-20 h-20 rounded-full flex items-center justify-center mb-4 text-[40px] text-white bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)]">
<FileTextOutlined />
</div>
<p>暂无成长档案</p>
<a-button type="primary" @click="showAddModal">
<p class="text-[#636E72] text-base mb-6">暂无成长档案</p>
<a-button type="primary" class="!bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)] !border-0 rounded-xl" @click="showAddModal">
创建第一份档案
</a-button>
</div>
<!-- 加载状态 -->
<div class="loading-state" v-if="loading">
<div class="flex flex-col items-center justify-center py-20 loading-state" v-if="loading">
<a-spin size="large" />
<p>加载中...</p>
<p class="text-[#636E72] mt-4">加载中...</p>
</div>
<!-- 添加/编辑档案弹窗 -->
@ -131,9 +134,9 @@
:confirm-loading="submitting"
>
<template #title>
<span class="modal-title">
<EditOutlined v-if="isEdit" class="modal-title-icon" />
<PlusOutlined v-else class="modal-title-icon" />
<span class="flex items-center gap-2 modal-title">
<EditOutlined v-if="isEdit" class="text-[#f5576c] text-lg" />
<PlusOutlined v-else class="text-[#f5576c] text-lg" />
{{ isEdit ? ' 编辑档案' : ' 添加档案' }}
</span>
</template>
@ -158,8 +161,8 @@
</a-form-item>
<a-form-item label="档案类型" name="recordType" v-if="!isEdit">
<a-radio-group v-model:value="formState.recordType">
<a-radio value="STUDENT"><UserOutlined class="radio-icon" /> 个人档案</a-radio>
<a-radio value="CLASS"><TeamOutlined class="radio-icon" /> 班级档案</a-radio>
<a-radio value="STUDENT"><UserOutlined class="mr-1 text-[#f5576c]" /> 个人档案</a-radio>
<a-radio value="CLASS"><TeamOutlined class="mr-1 text-[#f5576c]" /> 班级档案</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="标题" name="title">
@ -168,7 +171,7 @@
<a-form-item label="记录日期" name="recordDate">
<a-date-picker
v-model:value="formState.recordDateValue"
style="width: 100%;"
class="w-full"
value-format="YYYY-MM-DD"
/>
</a-form-item>
@ -204,38 +207,38 @@
:footer="null"
>
<template #title>
<span class="modal-title">
<CameraOutlined class="modal-title-icon" />
<span class="flex items-center gap-2">
<CameraOutlined class="text-[#f5576c] text-lg" />
{{ currentRecord?.title }}
</span>
</template>
<div class="detail-content" v-if="currentRecord">
<div class="detail-header">
<div class="student-info">
<div class="student-avatar">
<div class="p-0 detail-content" v-if="currentRecord">
<div class="flex justify-between items-center p-4 rounded-2xl mb-5 bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)] detail-header">
<div class="flex items-center gap-3 student-info">
<div class="w-12 h-12 rounded-full bg-white flex items-center justify-center text-xl font-600 text-[#f5576c]">
{{ currentRecord.student?.name?.charAt(0) || '班' }}
</div>
<div class="student-details">
<div class="student-name">{{ currentRecord.student?.name || '班级档案' }}</div>
<div class="student-class">{{ currentRecord.class?.name }}</div>
<div>
<div class="text-white text-base font-600">{{ currentRecord.student?.name || '班级档案' }}</div>
<div class="text-white/80 text-xs">{{ currentRecord.class?.name }}</div>
</div>
</div>
<div class="record-date">
<CalendarOutlined class="date-icon" />
<div class="flex items-center gap-2 text-white text-sm record-date">
<CalendarOutlined class="text-base" />
{{ formatDate(currentRecord.recordDate) }}
</div>
</div>
<div class="detail-body">
<p class="content-text">{{ currentRecord.content }}</p>
<div class="p-0 detail-body">
<p class="text-sm leading-[1.8] text-[#2D3436] mb-5">{{ currentRecord.content }}</p>
<div v-if="currentRecord.images?.length" class="image-gallery">
<div v-if="currentRecord.images?.length" class="p-4 bg-[#F8F9FA] rounded-xl image-gallery">
<a-image-preview-group>
<a-image
v-for="(img, index) in currentRecord.images"
:key="index"
:src="getImageUrl(img)"
style="width: 100px; height: 100px; object-fit: cover; margin-right: 8px; border-radius: 8px;"
class="w-[100px] h-[100px] object-cover mr-2 rounded-lg"
/>
</a-image-preview-group>
</div>
@ -244,7 +247,7 @@
</a-modal>
<!-- 分页 -->
<div class="pagination-wrapper" v-if="records.length > 0">
<div class="flex justify-center py-6 bg-white rounded-2xl pagination-wrapper" v-if="records.length > 0">
<a-pagination
v-model:current="pagination.current"
v-model:pageSize="pagination.pageSize"
@ -416,397 +419,9 @@ onMounted(() => {
</script>
<style scoped>
.growth-record-view {
min-height: 100vh;
background: linear-gradient(180deg, #FFF8F0 0%, #FFFFFF 100%);
padding: 0;
}
.page-header {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
border-radius: 20px;
padding: 24px 32px;
margin-bottom: 24px;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
display: flex;
align-items: center;
gap: 16px;
}
.title-icon-wrapper {
width: 56px;
height: 56px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
}
.title-icon {
font-size: 28px;
color: white;
}
.title-text h2 {
color: white;
font-size: 24px;
font-weight: 700;
margin: 0;
}
.title-text p {
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
margin: 4px 0 0 0;
}
.header-stats {
display: flex;
gap: 32px;
}
.stat-item {
text-align: center;
}
.stat-value {
display: block;
font-size: 32px;
font-weight: 700;
color: white;
}
.stat-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
}
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 16px 20px;
background: white;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.filters {
display: flex;
gap: 12px;
}
.add-btn {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
border: none;
border-radius: 12px;
height: 40px;
padding: 0 24px;
font-weight: 600;
}
.btn-icon {
margin-right: 8px;
font-size: 14px;
}
.record-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.record-card {
background: white;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.record-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.card-cover {
position: relative;
height: 160px;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.cover-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.image-count {
position: absolute;
bottom: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.6);
color: white;
.card-actions :deep(.ant-btn-link) {
padding: 4px 8px;
border-radius: 8px;
font-size: 12px;
height: auto;
}
.cover-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.placeholder-icon {
font-size: 48px;
color: rgba(255, 255, 255, 0.8);
}
.type-badge {
position: absolute;
top: 12px;
right: 12px;
padding: 4px 12px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
display: flex;
align-items: center;
gap: 4px;
}
.badge-icon {
font-size: 12px;
}
.type-badge.personal {
background: #E3F2FD;
color: #1976D2;
}
.type-badge.class {
background: #FCE4EC;
color: #E91E63;
}
.card-body {
padding: 16px;
}
.record-title {
font-size: 16px;
font-weight: 600;
color: #2D3436;
margin: 0 0 12px 0;
}
.record-meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 12px;
}
.meta-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #636E72;
}
.meta-icon {
font-size: 12px;
color: #f5576c;
}
.record-content {
font-size: 13px;
color: #636E72;
line-height: 1.6;
margin: 0;
}
.card-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 12px 16px;
border-top: 1px solid #F0F0F0;
background: #FAFAFA;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
background: white;
border-radius: 16px;
}
.empty-icon-wrapper {
width: 80px;
height: 80px;
border-radius: 50%;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
}
.empty-icon {
font-size: 40px;
color: white;
}
.empty-state p {
color: #636E72;
font-size: 16px;
margin-bottom: 24px;
}
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
}
.pagination-wrapper {
display: flex;
justify-content: center;
padding: 24px;
background: white;
border-radius: 16px;
}
.detail-content {
padding: 0;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
border-radius: 16px;
margin-bottom: 20px;
}
.student-info {
display: flex;
align-items: center;
gap: 12px;
}
.student-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 600;
color: #f5576c;
}
.student-name {
color: white;
font-size: 16px;
font-weight: 600;
}
.student-class {
color: rgba(255, 255, 255, 0.8);
font-size: 12px;
}
.record-date {
display: flex;
align-items: center;
gap: 8px;
color: white;
font-size: 14px;
}
.date-icon {
font-size: 16px;
}
.detail-body {
padding: 0;
}
.content-text {
font-size: 14px;
line-height: 1.8;
color: #2D3436;
margin-bottom: 20px;
}
.image-gallery {
padding: 16px;
background: #F8F9FA;
border-radius: 12px;
}
.modal-title {
display: flex;
align-items: center;
gap: 8px;
}
.modal-title-icon {
color: #f5576c;
font-size: 18px;
}
.radio-icon {
margin-right: 4px;
color: #f5576c;
}
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 16px;
text-align: center;
}
.action-bar {
flex-direction: column;
gap: 12px;
}
.filters {
width: 100%;
flex-wrap: wrap;
}
.record-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@ -1,107 +1,108 @@
<template>
<div class="parent-list-view">
<div class="min-h-100vh p-0 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)]">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-content">
<div class="header-title">
<div class="title-icon">
<IdcardOutlined />
<div class="rounded-[20px] py-6 px-8 mb-6 bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)]">
<div class="flex justify-between items-center max-md:flex-col max-md:gap-4 max-md:text-center">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-xl flex items-center justify-center bg-white/20">
<IdcardOutlined class="text-[28px] text-white" />
</div>
<div class="title-text">
<h2>家长管理</h2>
<p>管理家长账号与孩子关联</p>
<div>
<h2 class="text-white text-2xl font-700 m-0">家长管理</h2>
<p class="text-white/80 text-sm mt-1 m-0">管理家长账号与孩子关联</p>
</div>
</div>
<div class="header-stats">
<div class="stat-item">
<span class="stat-value">{{ parents.length }}</span>
<span class="stat-label">家长总数</span>
<div class="flex gap-8 max-md:w-full max-md:justify-center">
<div class="text-center stat-item">
<span class="block text-[32px] font-700 text-white">{{ parents.length }}</span>
<span class="text-xs text-white/80">家长总数</span>
</div>
<div class="stat-item">
<span class="stat-value active">{{ activeCount }}</span>
<span class="stat-label">活跃家长</span>
<div class="text-center stat-item">
<span class="block text-[32px] font-700 text-[#FFD93D]">{{ activeCount }}</span>
<span class="text-xs text-white/80">活跃家长</span>
</div>
</div>
</div>
</div>
<!-- 操作栏 -->
<div class="action-bar">
<div class="flex justify-between items-center mb-6 py-4 px-5 bg-white rounded-2xl shadow-[0_2px_8px_rgba(0,0,0,0.04)] max-md:flex-col max-md:gap-3">
<div class="search-box">
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索家长姓名/手机号/账号"
style="width: 280px;"
class="w-[280px]"
@search="handleSearch"
allow-clear
>
<template #prefix>
<SearchOutlined style="color: #B2BEC3;" />
<SearchOutlined class="text-[#B2BEC3]" />
</template>
</a-input-search>
</div>
<a-button type="primary" class="add-btn" @click="showAddModal">
<PlusOutlined class="btn-icon" />
<a-button type="primary" class="!bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)] hover:!bg-[linear-gradient(135deg,#FF7A2A_0%,#FFA030_100%)] !border-0 rounded-xl h-10 px-6 font-600" @click="showAddModal">
<PlusOutlined class="mr-2 text-sm" />
添加家长
</a-button>
</div>
<!-- 家长卡片列表 -->
<div class="parent-grid" v-if="!loading && parents.length > 0">
<div class="grid gap-5 mb-6 grid-cols-1 md:grid-cols-[repeat(auto-fill,minmax(320px,1fr))]" v-if="!loading && parents.length > 0">
<div
v-for="parent in parents"
:key="parent.id"
class="parent-card"
:class="{ 'inactive': parent.status !== 'ACTIVE' }"
class="bg-white rounded-2xl overflow-hidden shadow-[0_4px_12px_rgba(0,0,0,0.05)] transition-all duration-300 border-2 border-transparent hover:translate-y-[-4px] hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)] hover:border-[#FF8C42]"
:class="parent.status !== 'ACTIVE' ? 'opacity-70' : ''"
>
<div class="card-header">
<div class="parent-avatar">
<IdcardOutlined class="avatar-icon" />
<div class="flex items-center gap-3 py-4 px-5 bg-[linear-gradient(135deg,#F8F9FA_0%,#FFFFFF_100%)] border-b border-[#F0F0F0]">
<div class="w-12 h-12 rounded-xl flex items-center justify-center bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)]">
<IdcardOutlined class="text-2xl text-white" />
</div>
<div class="parent-basic">
<div class="parent-name">{{ parent.name }}</div>
<div class="parent-account">@{{ parent.loginAccount }}</div>
<div class="flex-1 min-w-0">
<div class="text-base font-600 text-[#2D3436]">{{ parent.name }}</div>
<div class="text-xs text-[#636E72] mt-0.5">@{{ parent.loginAccount }}</div>
</div>
<div class="status-badge" :class="parent.status === 'ACTIVE' ? 'active' : 'inactive'">
<span
class="py-1 px-3 rounded-[20px] text-xs font-500"
:class="parent.status === 'ACTIVE' ? 'bg-[#E8F5E9] text-[#43A047]' : 'bg-[#FFEBEE] text-[#E53935]'"
>
{{ parent.status === 'ACTIVE' ? '活跃' : '停用' }}
</div>
</span>
</div>
<div class="card-body">
<div class="info-row">
<PhoneOutlined class="info-icon" />
<span class="info-value">{{ parent.phone || '未设置' }}</span>
<div class="py-4 px-5 card-body">
<div class="flex items-center gap-2 mb-2 text-[13px]">
<PhoneOutlined class="text-sm text-[#FF8C42]" />
<span class="text-[#636E72]">{{ parent.phone || '未设置' }}</span>
</div>
<div class="info-row">
<MailOutlined class="info-icon" />
<span class="info-value">{{ parent.email || '未设置' }}</span>
<div class="flex items-center gap-2 mb-2 text-[13px]">
<MailOutlined class="text-sm text-[#FF8C42]" />
<span class="text-[#636E72]">{{ parent.email || '未设置' }}</span>
</div>
<div class="info-row">
<TeamOutlined class="info-icon" />
<span class="info-value children-tag">
<span v-if="parent.childrenCount > 0">
关联 <strong>{{ parent.childrenCount }}</strong> 个孩子
</span>
<span v-else class="no-children">未关联孩子</span>
<div class="flex items-center gap-2 text-[13px]">
<TeamOutlined class="text-sm text-[#FF8C42]" />
<span class="text-[#636E72]">
<span v-if="parent.childrenCount > 0">关联 <strong class="text-[#FF8C42]">{{ parent.childrenCount }}</strong> 个孩子</span>
<span v-else class="text-[#B2BEC3] italic">未关联孩子</span>
</span>
</div>
</div>
<div class="card-actions">
<a-button type="link" size="small" @click="handleEdit(parent)">
<div class="flex justify-end gap-1 py-3 px-4 border-t border-[#F0F0F0] bg-[#FAFAFA] card-actions">
<a-button type="link" size="small" @click="handleEdit(parent)" class="!py-1 !px-2 !h-auto !text-xs inline-flex items-center gap-1">
<EditOutlined /> 编辑
</a-button>
<a-button type="link" size="small" @click="handleManageChildren(parent)">
<a-button type="link" size="small" @click="handleManageChildren(parent)" class="!py-1 !px-2 !h-auto !text-xs inline-flex items-center gap-1">
<UserAddOutlined /> 孩子
</a-button>
<a-button type="link" size="small" @click="handleResetPassword(parent)">
<a-button type="link" size="small" @click="handleResetPassword(parent)" class="!py-1 !px-2 !h-auto !text-xs inline-flex items-center gap-1">
<KeyOutlined /> 重置
</a-button>
<a-popconfirm
title="确定要删除这位家长吗?"
@confirm="handleDelete(parent.id)"
>
<a-button type="link" size="small" danger>
<a-button type="link" size="small" danger class="!py-1 !px-2 !h-auto !text-xs">
<DeleteOutlined /> 删除
</a-button>
</a-popconfirm>
@ -110,22 +111,22 @@
</div>
<!-- 空状态 -->
<div class="empty-state" v-if="!loading && parents.length === 0">
<InboxOutlined class="empty-icon" />
<p>暂无家长数据</p>
<a-button type="primary" @click="showAddModal">
<div class="flex flex-col items-center justify-center py-20 bg-white rounded-2xl empty-state" v-if="!loading && parents.length === 0">
<InboxOutlined class="text-[64px] text-[#B2BEC3] mb-4" />
<p class="text-[#636E72] text-base mb-6">暂无家长数据</p>
<a-button type="primary" class="!bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)] !border-0 rounded-xl" @click="showAddModal">
添加第一位家长
</a-button>
</div>
<!-- 加载状态 -->
<div class="loading-state" v-if="loading">
<div class="flex flex-col items-center justify-center py-20 loading-state" v-if="loading">
<a-spin size="large" />
<p>加载中...</p>
<p class="text-[#636E72] mt-4">加载中...</p>
</div>
<!-- 分页 -->
<div class="pagination-wrapper" v-if="parents.length > 0">
<div class="flex justify-center py-6 bg-white rounded-2xl pagination-wrapper" v-if="parents.length > 0">
<a-pagination
v-model:current="pagination.current"
v-model:pageSize="pagination.pageSize"
@ -144,12 +145,11 @@
@cancel="handleModalCancel"
:confirm-loading="submitting"
:width="520"
class="parent-modal"
>
<template #title>
<span class="modal-title">
<EditOutlined v-if="isEdit" class="modal-title-icon" />
<PlusOutlined v-else class="modal-title-icon" />
<span class="flex items-center gap-2">
<EditOutlined v-if="isEdit" class="text-[#FF8C42]" />
<PlusOutlined v-else class="text-[#FF8C42]" />
{{ isEdit ? '编辑家长' : '添加家长' }}
</span>
</template>
@ -162,17 +162,17 @@
>
<a-form-item label="姓名" name="name">
<a-input v-model:value="formState.name" placeholder="请输入家长姓名">
<template #prefix><UserOutlined style="color: #B2BEC3;" /></template>
<template #prefix><UserOutlined class="text-[#B2BEC3]" /></template>
</a-input>
</a-form-item>
<a-form-item label="手机号" name="phone">
<a-input v-model:value="formState.phone" placeholder="请输入手机号">
<template #prefix><PhoneOutlined style="color: #B2BEC3;" /></template>
<template #prefix><PhoneOutlined class="text-[#B2BEC3]" /></template>
</a-input>
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input v-model:value="formState.email" placeholder="请输入邮箱(可选)">
<template #prefix><MailOutlined style="color: #B2BEC3;" /></template>
<template #prefix><MailOutlined class="text-[#B2BEC3]" /></template>
</a-input>
</a-form-item>
<a-form-item label="登录账号" name="loginAccount">
@ -181,12 +181,12 @@
placeholder="请输入登录账号"
:disabled="isEdit"
>
<template #prefix><KeyOutlined style="color: #B2BEC3;" /></template>
<template #prefix><KeyOutlined class="text-[#B2BEC3]" /></template>
</a-input>
</a-form-item>
<a-form-item v-if="!isEdit" label="密码" name="password">
<a-input-password v-model:value="formState.password" placeholder="请输入密码默认123456">
<template #prefix><LockOutlined style="color: #B2BEC3;" /></template>
<template #prefix><LockOutlined class="text-[#B2BEC3]" /></template>
</a-input-password>
</a-form-item>
</a-form>
@ -198,43 +198,41 @@
title="管理关联孩子"
:width="650"
:footer="null"
class="children-modal"
>
<template #title>
<span class="modal-title">
<TeamOutlined class="modal-title-icon" />
<span class="flex items-center gap-2">
<TeamOutlined class="text-[#FF8C42]" />
管理关联孩子 - {{ currentParent?.name }}
</span>
</template>
<!-- 添加孩子按钮 -->
<div class="add-child-section">
<a-button type="dashed" block @click="openSelectStudentModal" style="height: 48px;">
<div class="p-4 bg-[#F8F9FA] rounded-xl mb-4 add-child-section">
<a-button type="dashed" block @click="openSelectStudentModal" class="h-12">
<PlusOutlined /> 添加孩子
</a-button>
</div>
<!-- 已关联孩子列表 -->
<div class="children-list">
<div class="list-header">
<div class="border border-[#F0F0F0] rounded-xl overflow-hidden children-list">
<div class="py-3 px-4 bg-[#FAFAFA] border-b border-[#F0F0F0] font-500 text-[#2D3436] list-header">
<span>已关联孩子 ({{ parentChildren.length }})</span>
</div>
<a-list
:data-source="parentChildren"
:loading="childrenLoading"
class="children-list-ant"
>
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta>
<template #avatar>
<a-avatar style="background-color: #FF8C42;">
<a-avatar class="!bg-[#FF8C42]">
<UserOutlined />
</a-avatar>
</template>
<template #title>
<span>{{ item.name }}</span>
<span v-if="item.class?.name" class="child-class-text">{{ item.class.name }}</span>
<a-tag :color="getRelationshipColor(item.relationship)" class="child-relationship-tag">
<span v-if="item.class?.name" class="ml-1 text-[13px] text-[#999]">{{ item.class.name }}</span>
<a-tag :color="getRelationshipColor(item.relationship)" class="ml-2 text-xs">
关系{{ getRelationshipLabel(item.relationship) }}
</a-tag>
</template>
@ -252,7 +250,7 @@
</a-list-item>
</template>
</a-list>
<div v-if="parentChildren.length === 0 && !childrenLoading" class="empty-children">
<div v-if="parentChildren.length === 0 && !childrenLoading" class="py-10 text-center text-[#B2BEC3] empty-children">
<span>暂无关联的孩子</span>
</div>
</div>
@ -267,25 +265,24 @@
class="select-student-modal"
>
<template #title>
<span class="modal-title">
<UserAddOutlined class="modal-title-icon" />
<span class="flex items-center gap-2">
<UserAddOutlined class="text-[#FF8C42]" />
选择要关联的孩子
</span>
</template>
<!-- 搜索和筛选 -->
<div class="select-search-bar">
<div class="flex gap-3 mb-4 select-search-bar">
<a-input-search
v-model:value="studentSearchKeyword"
placeholder="搜索学生姓名"
style="width: 240px;"
class="w-[240px]"
@search="handleStudentSearch"
allow-clear
/>
<a-select
v-model:value="studentClassFilter"
placeholder="按班级筛选"
style="width: 160px;"
class="w-[160px]"
allow-clear
@change="handleStudentSearch"
>
@ -295,7 +292,6 @@
</a-select>
</div>
<!-- 学生表格 -->
<a-table
:columns="studentTableColumns"
:data-source="studentTableData"
@ -304,8 +300,8 @@
:row-selection="studentRowSelection"
row-key="id"
size="small"
class="mt-4 select-student-table"
@change="handleStudentTableChange"
style="margin-top: 16px;"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'gender'">
@ -314,15 +310,14 @@
</template>
</a-table>
<!-- 选择关系并确认 -->
<div class="select-footer" v-if="selectedStudent">
<div class="selected-info">
<div class="flex justify-between items-center mt-4 p-4 bg-[#F8F9FA] rounded-xl select-footer" v-if="selectedStudent">
<div class="flex items-center gap-2 selected-info">
<span>已选择</span>
<a-tag color="orange">{{ selectedStudent.name }}</a-tag>
<span class="selected-class">{{ selectedStudent.className }}</span>
<span class="text-[#666] text-[13px]">{{ selectedStudent.className }}</span>
</div>
<div class="select-actions">
<a-select v-model:value="addChildForm.relationship" style="width: 100px; margin-right: 12px;">
<div class="flex items-center gap-2 select-actions">
<a-select v-model:value="addChildForm.relationship" class="w-[100px] mr-3">
<a-select-option value="FATHER">父亲</a-select-option>
<a-select-option value="MOTHER">母亲</a-select-option>
<a-select-option value="GRANDFATHER">祖父</a-select-option>
@ -345,20 +340,20 @@
:width="400"
>
<template #title>
<span class="modal-title">
<KeyOutlined class="modal-title-icon" />
<span class="flex items-center gap-2">
<KeyOutlined class="text-[#FF8C42]" />
重置密码
</span>
</template>
<div class="reset-password-content">
<div class="reset-warning">
<WarningOutlined class="warning-icon" />
<p>确定要重置 <strong>{{ currentParent?.name }}</strong> 的密码吗</p>
<div class="text-center reset-password-content">
<div class="py-5 px-5 bg-[#FFF8F0] rounded-xl mb-5 reset-warning">
<WarningOutlined class="block text-[32px] text-[#FF8C42] mb-2" />
<p class="m-0 text-[#636E72]">确定要重置 <strong>{{ currentParent?.name }}</strong> 的密码吗</p>
</div>
<div v-if="newPassword" class="new-password-box">
<p>新密码</p>
<div class="password-display">
<a-typography-text copyable>{{ newPassword }}</a-typography-text>
<p class="mb-2 text-[#636E72]">新密码</p>
<div class="p-4 rounded-xl font-bold text-2xl text-white bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)] password-display">
<a-typography-text copyable class="!text-white">{{ newPassword }}</a-typography-text>
</div>
</div>
</div>
@ -745,502 +740,48 @@ onMounted(() => {
</script>
<style scoped>
.parent-list-view {
min-height: 100vh;
background: linear-gradient(180deg, #FFF8F0 0%, #FFFFFF 100%);
padding: 0;
}
/* 页面头部 */
.page-header {
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
border-radius: 20px;
padding: 24px 32px;
margin-bottom: 24px;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
display: flex;
align-items: center;
gap: 16px;
}
.title-icon {
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.title-icon :deep(svg) {
font-size: 28px;
color: white;
}
.title-text h2 {
color: white;
font-size: 24px;
font-weight: 700;
margin: 0;
}
.title-text p {
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
margin: 4px 0 0 0;
}
.header-stats {
display: flex;
gap: 32px;
}
.stat-item {
text-align: center;
}
.stat-value {
display: block;
font-size: 32px;
font-weight: 700;
color: white;
}
.stat-value.active {
color: #FFD93D;
}
.stat-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
}
/* 操作栏 */
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 16px 20px;
background: white;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.search-box :deep(.ant-input-affix-wrapper) {
border-radius: 12px;
border: 2px solid #F0F0F0;
}
.search-box :deep(.ant-input-affix-wrapper:hover) {
border-color: #FF8C42;
}
.add-btn {
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
border: none;
border-radius: 12px;
height: 40px;
padding: 0 24px;
font-weight: 600;
}
.add-btn:hover {
background: linear-gradient(135deg, #FF7A2A 0%, #FFA030 100%);
}
.btn-icon {
margin-right: 8px;
font-size: 14px;
}
/* 家长卡片网格 */
.parent-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.parent-card {
background: white;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
border: 2px solid transparent;
}
.parent-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
border-color: #FF8C42;
}
.parent-card.inactive {
opacity: 0.7;
}
.parent-card .card-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
background: linear-gradient(135deg, #F8F9FA 0%, #FFFFFF 100%);
border-bottom: 1px solid #F0F0F0;
}
.parent-avatar {
width: 48px;
height: 48px;
border-radius: 12px;
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
display: flex;
align-items: center;
justify-content: center;
}
.avatar-icon {
font-size: 24px;
color: white;
}
.parent-basic {
flex: 1;
}
.parent-name {
font-size: 16px;
font-weight: 600;
color: #2D3436;
}
.parent-account {
font-size: 12px;
color: #636E72;
margin-top: 2px;
}
.status-badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
}
.status-badge.active {
background: #E8F5E9;
color: #43A047;
}
.status-badge.inactive {
background: #FFEBEE;
color: #E53935;
}
.card-body {
padding: 16px 20px;
}
.info-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
font-size: 13px;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-icon {
font-size: 14px;
color: #FF8C42;
margin-right: 4px;
}
.info-value {
color: #636E72;
}
.info-value strong {
color: #FF8C42;
}
.no-children {
color: #B2BEC3;
font-style: italic;
}
.card-actions {
display: flex;
justify-content: flex-end;
gap: 4px;
padding: 12px 16px;
border-top: 1px solid #F0F0F0;
background: #FAFAFA;
}
.card-actions :deep(.ant-btn-link) {
padding: 4px 8px;
height: auto;
font-size: 12px;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
background: white;
border-radius: 16px;
}
.empty-icon {
font-size: 64px;
color: #B2BEC3;
margin-bottom: 16px;
}
.empty-state p {
color: #636E72;
font-size: 16px;
margin-bottom: 24px;
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
}
.loading-state p {
color: #636E72;
margin-top: 16px;
}
/* 分页 */
.pagination-wrapper {
display: flex;
justify-content: center;
padding: 24px;
background: white;
border-radius: 16px;
}
/* 重置密码弹窗 */
.reset-password-content {
text-align: center;
}
.reset-warning {
padding: 20px;
background: #FFF8F0;
border-radius: 12px;
margin-bottom: 20px;
}
.warning-icon {
font-size: 32px;
color: #FF8C42;
display: block;
margin-bottom: 8px;
}
.reset-warning p {
margin: 0;
color: #636E72;
}
.new-password-box p {
margin-bottom: 8px;
color: #636E72;
}
.password-display {
padding: 16px;
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
border-radius: 12px;
font-size: 24px;
font-weight: bold;
color: white;
}
/* Modal title styling */
.modal-title {
display: flex;
align-items: center;
gap: 8px;
}
.modal-title-icon {
color: #FF8C42;
}
/* 管理孩子弹窗 */
.add-child-section {
padding: 16px;
background: #F8F9FA;
border-radius: 12px;
margin-bottom: 16px;
}
.add-child-form {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.children-list {
border: 1px solid #F0F0F0;
border-radius: 12px;
overflow: hidden;
}
.children-list :deep(.ant-list-item) {
.children-list-ant :deep(.ant-list-item) {
display: flex;
align-items: center;
padding: 12px 16px;
}
.children-list :deep(.ant-list-item-meta) {
.children-list-ant :deep(.ant-list-item-meta) {
display: flex;
align-items: center;
margin-bottom: 0;
}
.children-list :deep(.ant-list-item-meta-content) {
.children-list-ant :deep(.ant-list-item-meta-content) {
display: flex;
align-items: center;
}
.children-list :deep(.ant-list-item-meta-title) {
.children-list-ant :deep(.ant-list-item-meta-title) {
margin-bottom: 0;
line-height: 1;
}
.list-header {
padding: 12px 16px;
background: #FAFAFA;
border-bottom: 1px solid #F0F0F0;
font-weight: 500;
color: #2D3436;
}
.empty-children {
padding: 40px;
text-align: center;
color: #B2BEC3;
}
.child-class-name {
margin-left: 12px;
padding: 2px 8px;
background: #F0F5FF;
border-radius: 4px;
font-size: 12px;
color: #1890FF;
font-weight: normal;
}
.child-class-text {
margin-left: 4px;
font-size: 13px;
color: #999;
font-weight: normal;
}
.child-relationship-tag {
margin-left: 8px;
font-size: 12px;
}
.child-relationship {
font-size: 13px;
color: #666;
}
/* 选择学生弹窗 */
.select-search-bar {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.select-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16px;
padding: 16px;
background: #F8F9FA;
border-radius: 12px;
}
.selected-info {
display: flex;
align-items: center;
gap: 8px;
}
.selected-class {
color: #666;
font-size: 13px;
}
.select-actions {
display: flex;
align-items: center;
gap: 8px;
}
.select-student-modal :deep(.ant-table-wrapper) {
max-height: 400px;
overflow: auto;
}
/* 响应式设计 */
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 16px;
text-align: center;
}
.header-stats {
width: 100%;
justify-content: center;
}
.action-bar {
flex-direction: column;
gap: 12px;
}
.search-box :deep(.ant-input-search) {
width: 100% !important;
}
.parent-grid {
grid-template-columns: 1fr;
}
.card-actions {
flex-wrap: wrap;
}

View File

@ -1,8 +1,8 @@
<template>
<div class="calendar-view">
<div class="page-header">
<div class="header-left">
<h2>日历视图</h2>
<div class="bg-white rounded-2xl p-6 min-h-[calc(100vh-200px)]">
<div class="flex justify-between items-center mb-6">
<div class="flex items-center gap-5">
<h2 class="m-0 text-xl font-600 text-[#2D3436]">日历视图</h2>
<a-radio-group v-model:value="viewType" button-style="solid" @change="handleViewChange">
<a-radio-button value="dayGridMonth"></a-radio-button>
<a-radio-button value="timeGridWeek"></a-radio-button>
@ -43,7 +43,7 @@
v-model:value="selectedClassId"
placeholder="筛选班级"
allowClear
style="width: 150px;"
class="w-[150px]"
@change="loadEvents"
>
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
@ -54,7 +54,7 @@
v-model:value="selectedTeacherId"
placeholder="筛选教师"
allowClear
style="width: 150px;"
class="w-[150px]"
@change="loadEvents"
>
<a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
@ -64,7 +64,7 @@
</a-space>
</div>
<div class="calendar-container">
<div class="min-h-[600px] calendar-container">
<FullCalendar
ref="calendarRef"
:options="calendarOptions"
@ -335,38 +335,7 @@ onMounted(() => {
</script>
<style scoped>
.calendar-view {
background: white;
border-radius: 16px;
padding: 24px;
min-height: calc(100vh - 200px);
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.header-left {
display: flex;
align-items: center;
gap: 20px;
}
.header-left h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #2D3436;
}
.calendar-container {
min-height: 600px;
}
/* FullCalendar 样式覆盖 */
/* FullCalendar 第三方样式覆盖 */
:deep(.fc) {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}

View File

@ -1,7 +1,7 @@
<template>
<div class="schedule-view">
<div class="page-header">
<h2>课程排期</h2>
<div>
<div class="flex justify-between items-center mb-5">
<h2 class="m-0">课程排期</h2>
<a-space>
<a-dropdown>
<a-button>
@ -51,13 +51,13 @@
</div>
<!-- 筛选区 -->
<div class="filter-section">
<div class="mb-5 p-4 bg-[#fafafa] rounded-lg">
<a-space wrap>
<a-select
v-model:value="filters.classId"
placeholder="选择班级"
allowClear
style="width: 150px"
class="w-[150px]"
@change="loadSchedules"
>
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
@ -68,7 +68,7 @@
v-model:value="filters.teacherId"
placeholder="选择教师"
allowClear
style="width: 150px"
class="w-[150px]"
@change="loadSchedules"
>
<a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
@ -83,7 +83,7 @@
v-model:value="filters.status"
placeholder="状态"
allowClear
style="width: 120px"
class="w-[120px]"
@change="loadSchedules"
>
<a-select-option value="ACTIVE">有效</a-select-option>
@ -104,7 +104,7 @@
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'scheduledDate'">
{{ formatDate(record.scheduledDate) }}
<span v-if="record.scheduledTime" class="time-slot">{{ record.scheduledTime }}</span>
<span v-if="record.scheduledTime" class="ml-2 text-[#666] text-xs">{{ record.scheduledTime }}</span>
</template>
<template v-if="column.key === 'repeatType'">
<a-tag v-if="record.repeatType === 'NONE'" color="default">单次</a-tag>
@ -217,7 +217,7 @@
:footer="null"
width="800px"
>
<div class="template-header">
<div class="mb-4 flex justify-end">
<a-button type="primary" size="small" @click="showCreateTemplateModal">
<PlusOutlined /> 新建模板
</a-button>
@ -325,7 +325,7 @@
show-icon
style="margin-bottom: 16px"
/>
<div class="batch-header">
<div class="mb-4">
<a-button type="dashed" @click="addBatchItem">
<PlusOutlined /> 添加排课
</a-button>
@ -925,40 +925,6 @@ onMounted(() => {
});
</script>
<style scoped lang="scss">
.schedule-view {
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h2 {
margin: 0;
}
}
.filter-section {
margin-bottom: 20px;
padding: 16px;
background: #fafafa;
border-radius: 8px;
}
.time-slot {
margin-left: 8px;
color: #666;
font-size: 12px;
}
.template-header {
margin-bottom: 16px;
display: flex;
justify-content: flex-end;
}
.batch-header {
margin-bottom: 16px;
}
}
<style scoped>
/* 仅保留无法用 UnoCSS 实现的部分 */
</style>

View File

@ -1,7 +1,7 @@
<template>
<div class="timetable-view">
<div class="page-header">
<h2>课表视图</h2>
<div>
<div class="flex justify-between items-center mb-5">
<h2 class="m-0">课表视图</h2>
<a-space>
<a-dropdown>
<a-button>
@ -61,7 +61,7 @@
</div>
<!-- 筛选区 -->
<div class="filter-section">
<div class="mb-5 p-4 bg-[#fafafa] rounded-lg">
<a-space>
<span>周次{{ weekRangeText }}</span>
<a-divider type="vertical" />
@ -69,7 +69,7 @@
v-model:value="filters.classId"
placeholder="选择班级"
allowClear
style="width: 150px"
class="w-[150px]"
@change="loadTimetable"
>
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
@ -80,7 +80,7 @@
v-model:value="filters.teacherId"
placeholder="选择教师"
allowClear
style="width: 150px"
class="w-[150px]"
@change="loadTimetable"
>
<a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
@ -91,48 +91,48 @@
</div>
<!-- 课表 -->
<div class="timetable-container">
<div class="timetable-header">
<div class="border border-[#e8e8e8] rounded-lg overflow-hidden">
<div class="grid grid-cols-7 bg-[linear-gradient(135deg,#FF8C42_0%,#E67635_100%)] text-white timetable-header">
<div
v-for="day in weekDays"
:key="day.date"
class="day-header"
:class="{ 'is-today': day.isToday }"
class="p-3 text-center border-r border-white/20 last:border-r-0"
:class="day.isToday ? 'bg-white/20' : ''"
>
<div class="day-name">{{ day.dayName }}</div>
<div class="day-date">{{ day.dateDisplay }}</div>
<div class="font-500">{{ day.dayName }}</div>
<div class="text-xs opacity-80">{{ day.dateDisplay }}</div>
</div>
</div>
<div class="timetable-body">
<div class="min-h-[400px]">
<a-spin :spinning="loading">
<div class="timetable-grid">
<div class="grid grid-cols-7 timetable-grid">
<div
v-for="day in weekDays"
:key="day.date"
class="day-column"
:class="{ 'is-today': day.isToday }"
class="min-h-[300px] p-2 border-r border-[#e8e8e8] bg-[#fafafa] last:border-r-0 align-top"
:class="day.isToday ? 'bg-[#fff4ec]' : ''"
>
<div
v-for="schedule in day.schedules"
:key="schedule.id"
class="schedule-card"
:class="{
'school-schedule': schedule.source === 'SCHOOL',
'teacher-schedule': schedule.source === 'TEACHER',
'cancelled': schedule.status === 'CANCELLED',
}"
class="bg-white rounded-lg p-2.5 mb-2 shadow-[0_1px_3px_rgba(0,0,0,0.1)] cursor-pointer transition-all duration-300 border-l-[3px] hover:shadow-[0_3px_8px_rgba(0,0,0,0.15)] hover:-translate-y-0.5"
:class="[
schedule.source === 'SCHOOL' ? 'border-l-[#FF8C42]' : '',
schedule.source === 'TEACHER' ? 'border-l-[#722ed1]' : '',
schedule.status === 'CANCELLED' ? 'opacity-50 border-l-[#999]' : '',
]"
@click="showScheduleDetail(schedule)"
>
<div class="schedule-time">{{ schedule.scheduledTime || '待定' }}</div>
<div class="schedule-course">{{ schedule.courseName }}</div>
<div class="schedule-class">{{ schedule.className }}</div>
<div v-if="schedule.teacherName" class="schedule-teacher">
<div class="text-xs text-[#666] mb-1">{{ schedule.scheduledTime || '待定' }}</div>
<div class="font-500 text-[#333] mb-1 overflow-hidden text-ellipsis whitespace-nowrap">{{ schedule.courseName }}</div>
<div class="text-xs text-[#666]">{{ schedule.className }}</div>
<div v-if="schedule.teacherName" class="text-xs text-[#999] mt-1">
{{ schedule.teacherName }}
</div>
<a-tag v-if="schedule.status === 'CANCELLED'" color="error" size="small">已取消</a-tag>
</div>
<div v-if="!day.schedules.length" class="empty-day">
<div v-if="!day.schedules.length" class="text-center text-[#999] py-5 text-xs">
暂无排课
</div>
</div>
@ -180,7 +180,7 @@
:footer="null"
width="800px"
>
<div style="margin-bottom: 16px; text-align: right;">
<div class="mb-4 text-right">
<a-button type="primary" size="small" @click="router.push('/school/schedule')">
<PlusOutlined /> 新建模板
</a-button>
@ -214,8 +214,8 @@
@ok="handleBatchSubmit"
width="900px"
>
<a-alert message="批量添加排课信息" type="info" show-icon style="margin-bottom: 16px" />
<div style="margin-bottom: 16px;">
<a-alert message="批量添加排课信息" type="info" show-icon class="mb-4" />
<div class="mb-4">
<a-button type="dashed" @click="addBatchItem">
<PlusOutlined /> 添加排课
</a-button>
@ -236,31 +236,31 @@
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'classId'">
<a-select v-model:value="record.classId" placeholder="班级" style="width: 100%">
<a-select v-model:value="record.classId" placeholder="班级" class="w-full">
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
{{ cls.name }}
</a-select-option>
</a-select>
</template>
<template v-if="column.key === 'courseId'">
<a-select v-model:value="record.courseId" placeholder="课程" style="width: 100%">
<a-select v-model:value="record.courseId" placeholder="课程" class="w-full">
<a-select-option v-for="course in courses" :key="course.id" :value="course.id">
{{ course.name }}
</a-select-option>
</a-select>
</template>
<template v-if="column.key === 'teacherId'">
<a-select v-model:value="record.teacherId" placeholder="教师" style="width: 100%" allowClear>
<a-select v-model:value="record.teacherId" placeholder="教师" class="w-full" allowClear>
<a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
{{ teacher.name }}
</a-select-option>
</a-select>
</template>
<template v-if="column.key === 'scheduledDate'">
<a-date-picker v-model:value="record.scheduledDate" style="width: 100%" />
<a-date-picker v-model:value="record.scheduledDate" class="w-full" />
</template>
<template v-if="column.key === 'scheduledTime'">
<a-input v-model:value="record.scheduledTime" placeholder="09:00-09:30" style="width: 100%" />
<a-input v-model:value="record.scheduledTime" placeholder="09:00-09:30" class="w-full" />
</template>
<template v-if="column.key === 'actions'">
<a-button type="link" size="small" danger @click="removeBatchItem(index)">
@ -280,14 +280,14 @@
>
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="选择模板">
<a-select v-model:value="selectedTemplateId" placeholder="选择排课模板" style="width: 100%">
<a-select v-model:value="selectedTemplateId" placeholder="选择排课模板" class="w-full">
<a-select-option v-for="tpl in templates" :key="tpl.id" :value="tpl.id">
{{ tpl.name }} - {{ tpl.courseName }} ({{ tpl.scheduledTime }})
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="排课日期">
<a-date-picker v-model:value="templateApplyDate" style="width: 100%" />
<a-date-picker v-model:value="templateApplyDate" class="w-full" />
</a-form-item>
</a-form>
</a-modal>
@ -592,148 +592,6 @@ onMounted(() => {
});
</script>
<style scoped lang="scss">
.timetable-view {
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h2 {
margin: 0;
}
}
.filter-section {
margin-bottom: 20px;
padding: 16px;
background: #fafafa;
border-radius: 8px;
}
.timetable-container {
border: 1px solid #e8e8e8;
border-radius: 8px;
overflow: hidden;
}
.timetable-header {
display: table;
width: 100%;
table-layout: fixed; //
background: linear-gradient(135deg, #FF8C42 0%, #E67635 100%);
color: white;
.day-header {
display: table-cell;
padding: 12px 8px;
text-align: center;
border-right: 1px solid rgba(255, 255, 255, 0.2);
vertical-align: middle;
&:last-child {
border-right: none;
}
&.is-today {
background: rgba(255, 255, 255, 0.2);
}
.day-name {
font-weight: 500;
}
.day-date {
font-size: 12px;
opacity: 0.8;
}
}
}
.timetable-body {
min-height: 400px;
}
.timetable-grid {
display: table;
width: 100%;
table-layout: fixed; //
.day-column {
display: table-cell;
min-height: 300px;
padding: 8px;
border-right: 1px solid #e8e8e8;
background: #fafafa;
vertical-align: top;
&:last-child {
border-right: none;
}
&.is-today {
background: #fff4ec;
}
}
}
.schedule-card {
background: white;
border-radius: 8px;
padding: 10px;
margin-bottom: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.3s;
border-left: 3px solid #FF8C42;
&:hover {
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
&.teacher-schedule {
border-left-color: #722ed1;
}
&.cancelled {
opacity: 0.5;
border-left-color: #999;
}
.schedule-time {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.schedule-course {
font-weight: 500;
color: #333;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.schedule-class {
font-size: 12px;
color: #666;
}
.schedule-teacher {
font-size: 12px;
color: #999;
margin-top: 4px;
}
}
.empty-day {
text-align: center;
color: #999;
padding: 20px;
font-size: 12px;
}
}
<style scoped>
/* 仅保留无法用 UnoCSS 实现的部分 */
</style>

View File

@ -1,19 +1,19 @@
<template>
<div class="school-course-detail-page">
<div class="min-h-100vh bg-[linear-gradient(135deg,#F0FFF4_0%,#FFFFFF_50%,#F0FDF4_100%)]">
<!-- 顶部导航 -->
<div class="detail-header">
<div class="header-left">
<div class="flex justify-between items-center py-4 px-6 bg-white border-b border-[#f0f0f0] sticky top-0 z-100">
<div class="flex items-center gap-3">
<a-button type="text" @click="router.back()">
<ArrowLeftOutlined />
</a-button>
<div class="course-title">
<h2>{{ detail?.name || '校本课程包详情' }}</h2>
<div class="flex items-center gap-3">
<h2 class="m-0 text-lg font-600">{{ detail?.name || '校本课程包详情' }}</h2>
<a-tag :color="detail?.status === 'ACTIVE' ? 'success' : 'default'">
{{ detail?.status === 'ACTIVE' ? '启用' : '禁用' }}
</a-tag>
</div>
</div>
<div class="header-actions">
<div class="flex gap-2">
<a-button @click="showReserveModal">
<CalendarOutlined /> 预约
</a-button>
@ -24,88 +24,88 @@
</div>
<a-spin :spinning="loading">
<div class="detail-content">
<div class="p-6 max-w-[1200px] mx-auto">
<!-- 基本信息 -->
<div class="section-card">
<div class="section-header">
<span class="section-title">
<div class="bg-white rounded-xl overflow-hidden shadow-[0_2px_8px_rgba(0,0,0,0.06)] mb-6">
<div class="py-4 px-6 border-b border-[#f0f0f0] flex justify-between items-center">
<span class="text-base font-600 text-[#333] flex items-center gap-2">
<InfoCircleOutlined /> 基本信息
</span>
</div>
<div class="section-body">
<div class="info-grid">
<div class="info-item">
<span class="info-label">校本课程包名称</span>
<span class="info-value">{{ detail?.name }}</span>
<div class="p-5 px-6">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="flex flex-col gap-1">
<span class="text-xs text-[#666]">校本课程包名称</span>
<span class="text-sm text-[#333] font-500">{{ detail?.name }}</span>
</div>
<div class="info-item">
<span class="info-label">基于课程包</span>
<span class="info-value">{{ detail?.sourceCourse?.name || '-' }}</span>
<div class="flex flex-col gap-1">
<span class="text-xs text-[#666]">基于课程包</span>
<span class="text-sm text-[#333] font-500">{{ detail?.sourceCourse?.name || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">创建者</span>
<span class="info-value">{{ detail?.creator?.name || '-' }}</span>
<div class="flex flex-col gap-1">
<span class="text-xs text-[#666]">创建者</span>
<span class="text-sm text-[#333] font-500">{{ detail?.creator?.name || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">使用次数</span>
<span class="info-value">
<div class="flex flex-col gap-1">
<span class="text-xs text-[#666]">使用次数</span>
<span class="text-sm text-[#333] font-500">
<a-badge :count="detail?.usageCount || 0" :number-style="{ backgroundColor: '#52c41a' }" />
</span>
</div>
<div class="info-item">
<span class="info-label">创建时间</span>
<span class="info-value">{{ formatDate(detail?.createdAt) }}</span>
<div class="flex flex-col gap-1">
<span class="text-xs text-[#666]">创建时间</span>
<span class="text-sm text-[#333] font-500">{{ formatDate(detail?.createdAt) }}</span>
</div>
<div class="info-item">
<span class="info-label">更新时间</span>
<span class="info-value">{{ formatDate(detail?.updatedAt) }}</span>
<div class="flex flex-col gap-1">
<span class="text-xs text-[#666]">更新时间</span>
<span class="text-sm text-[#333] font-500">{{ formatDate(detail?.updatedAt) }}</span>
</div>
</div>
<div class="info-full" v-if="detail?.description">
<span class="info-label">描述</span>
<span class="info-value">{{ detail?.description }}</span>
<div class="mt-4 pt-4 border-t border-dashed border-[#f0f0f0] flex flex-col gap-1" v-if="detail?.description">
<span class="text-xs text-[#666]">描述</span>
<span class="text-sm text-[#333] leading-[1.6] whitespace-pre-wrap">{{ detail?.description }}</span>
</div>
<div class="info-full" v-if="detail?.changesSummary">
<span class="info-label">修改说明</span>
<span class="info-value">{{ detail?.changesSummary }}</span>
<div class="mt-4 pt-4 border-t border-dashed border-[#f0f0f0] flex flex-col gap-1" v-if="detail?.changesSummary">
<span class="text-xs text-[#666]">修改说明</span>
<span class="text-sm text-[#333] leading-[1.6] whitespace-pre-wrap">{{ detail?.changesSummary }}</span>
</div>
</div>
</div>
<!-- 课程配置 -->
<div class="section-card" v-if="detail?.lessons && detail.lessons.length > 0">
<div class="section-header">
<span class="section-title">
<div class="bg-white rounded-xl overflow-hidden shadow-[0_2px_8px_rgba(0,0,0,0.06)] mb-6" v-if="detail?.lessons && detail.lessons.length > 0">
<div class="py-4 px-6 border-b border-[#f0f0f0] flex justify-between items-center">
<span class="text-base font-600 text-[#333] flex items-center gap-2">
<AppstoreOutlined /> 课程配置
</span>
<a-tag>{{ detail.lessons.length }} 个课程</a-tag>
</div>
<div class="section-body">
<div class="lesson-cards">
<div class="p-5 px-6">
<div class="grid gap-4">
<div
v-for="lesson in detail.lessons"
:key="lesson.id"
class="lesson-card"
class="border border-[#e8e8e8] rounded-xl overflow-hidden"
>
<div class="lesson-header">
<div class="py-3 px-4 bg-[#fafafa] border-b border-[#f0f0f0]">
<a-tag :color="getLessonTypeColor(lesson.lessonType)">
{{ getLessonTypeName(lesson.lessonType) }}
</a-tag>
</div>
<div class="lesson-body">
<div class="lesson-section" v-if="lesson.objectives">
<div class="lesson-section-title">教学目标</div>
<div class="lesson-section-content">{{ lesson.objectives }}</div>
<div class="p-4">
<div class="mb-3 last:mb-0" v-if="lesson.objectives">
<div class="text-xs text-[#666] mb-1">教学目标</div>
<div class="text-[13px] text-[#333] leading-[1.6] whitespace-pre-wrap">{{ lesson.objectives }}</div>
</div>
<div class="lesson-section" v-if="lesson.preparation">
<div class="lesson-section-title">教学准备</div>
<div class="lesson-section-content">{{ lesson.preparation }}</div>
<div class="mb-3 last:mb-0" v-if="lesson.preparation">
<div class="text-xs text-[#666] mb-1">教学准备</div>
<div class="text-[13px] text-[#333] leading-[1.6] whitespace-pre-wrap">{{ lesson.preparation }}</div>
</div>
<div class="lesson-section" v-if="lesson.changeNote">
<div class="lesson-section-title change-note">
<div class="mb-3 last:mb-0" v-if="lesson.changeNote">
<div class="text-xs text-[#1890ff] mb-1">
<EditOutlined /> 修改备注
</div>
<div class="lesson-section-content highlighted">{{ lesson.changeNote }}</div>
<div class="text-[13px] text-[#333] leading-[1.6] whitespace-pre-wrap bg-[#e6f7ff] py-2 px-3 rounded border-l-[3px] border-l-[#1890ff]">{{ lesson.changeNote }}</div>
</div>
</div>
</div>
@ -114,16 +114,16 @@
</div>
<!-- 预约/排课记录 -->
<div class="section-card">
<div class="section-header">
<span class="section-title">
<div class="bg-white rounded-xl overflow-hidden shadow-[0_2px_8px_rgba(0,0,0,0.06)] mb-6">
<div class="py-4 px-6 border-b border-[#f0f0f0] flex justify-between items-center">
<span class="text-base font-600 text-[#333] flex items-center gap-2">
<CalendarOutlined /> 预约/排课记录
</span>
<a-button type="link" size="small" @click="showReserveModal">
<PlusOutlined /> 新增预约
</a-button>
</div>
<div class="section-body">
<div class="p-5 px-6">
<a-tabs v-model:activeKey="reservationTab">
<a-tab-pane key="upcoming" :tab="`即将上课 (${upcomingReservations.length})`">
<a-table
@ -150,7 +150,7 @@
</template>
</template>
</a-table>
<div v-if="upcomingReservations.length === 0" class="empty-state">
<div v-if="upcomingReservations.length === 0" class="text-center py-10 text-[#999]">
暂无即将上课的预约
</div>
</a-tab-pane>
@ -172,7 +172,7 @@
</template>
</template>
</a-table>
<div v-if="historyReservations.length === 0" class="empty-state">
<div v-if="historyReservations.length === 0" class="text-center py-10 text-[#999]">
暂无历史记录
</div>
</a-tab-pane>
@ -190,10 +190,10 @@
@ok="handleReserve"
:confirmLoading="reserveLoading"
>
<div class="reserve-modal" v-if="detail">
<div class="course-info">
<span class="label">课程包名称</span>
<span class="value">{{ detail.name }}</span>
<div v-if="detail">
<div class="bg-[#f9f9f9] rounded-lg py-3 px-4 flex gap-2">
<span class="text-[#666]">课程包名称</span>
<span class="font-500">{{ detail.name }}</span>
</div>
<a-divider />
@ -219,7 +219,7 @@
show-time
format="YYYY-MM-DD HH:mm"
placeholder="选择预约时间"
style="width: 100%"
class="w-full"
/>
</a-form-item>
<a-form-item label="备注">
@ -452,206 +452,6 @@ onMounted(() => {
});
</script>
<style scoped lang="scss">
.school-course-detail-page {
min-height: 100vh;
background: linear-gradient(135deg, #F0FFF4 0%, #FFFFFF 50%, #F0FDF4 100%);
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
background: white;
border-bottom: 1px solid #f0f0f0;
position: sticky;
top: 0;
z-index: 100;
.header-left {
display: flex;
align-items: center;
gap: 12px;
.course-title {
display: flex;
align-items: center;
gap: 12px;
h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
}
}
.header-actions {
display: flex;
gap: 8px;
}
}
.detail-content {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.section-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin-bottom: 24px;
.section-header {
padding: 16px 24px;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
display: flex;
align-items: center;
gap: 8px;
}
}
.section-body {
padding: 20px 24px;
}
}
.info-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
}
.info-item {
display: flex;
flex-direction: column;
gap: 4px;
.info-label {
font-size: 12px;
color: #666;
}
.info-value {
font-size: 14px;
color: #333;
font-weight: 500;
}
}
.info-full {
margin-top: 16px;
padding-top: 16px;
border-top: 1px dashed #f0f0f0;
display: flex;
flex-direction: column;
gap: 4px;
.info-label {
font-size: 12px;
color: #666;
}
.info-value {
font-size: 14px;
color: #333;
line-height: 1.6;
white-space: pre-wrap;
}
}
/* 课程卡片 */
.lesson-cards {
display: grid;
gap: 16px;
}
.lesson-card {
border: 1px solid #e8e8e8;
border-radius: 12px;
overflow: hidden;
.lesson-header {
padding: 12px 16px;
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
}
.lesson-body {
padding: 16px;
}
.lesson-section {
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
.lesson-section-title {
font-size: 12px;
color: #666;
margin-bottom: 4px;
&.change-note {
color: #1890ff;
}
}
.lesson-section-content {
font-size: 13px;
color: #333;
line-height: 1.6;
white-space: pre-wrap;
&.highlighted {
background: #e6f7ff;
padding: 8px 12px;
border-radius: 4px;
border-left: 3px solid #1890ff;
}
}
}
}
.empty-state {
text-align: center;
padding: 40px;
color: #999;
}
/* 预约弹窗 */
.reserve-modal {
.course-info {
background: #f9f9f9;
border-radius: 8px;
padding: 12px 16px;
display: flex;
gap: 8px;
.label {
color: #666;
}
.value {
font-weight: 500;
}
}
}
<style scoped>
/* 仅保留第三方/无法用原子类实现的部分 */
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="school-course-edit-page">
<div class="p-6">
<a-card :bordered="false">
<template #title>
<span>{{ isEdit ? '编辑校本课程包' : '创建校本课程包' }}</span>
@ -154,9 +154,3 @@ onMounted(() => {
fetchDetail();
});
</script>
<style scoped>
.school-course-edit-page {
padding: 24px;
}
</style>

View File

@ -1,56 +1,54 @@
<template>
<div class="school-course-list-page">
<div class="min-h-100vh bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)] p-6">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-content">
<div class="header-title">
<div class="title-icon-wrapper">
<AppstoreOutlined class="title-icon" />
</div>
<div class="title-text">
<h2>校本课程包</h2>
<p>管理本校教师创建的校本课程包</p>
</div>
<div class="mb-6 flex justify-between items-center">
<div class="flex items-center gap-4">
<div class="w-14 h-14 flex items-center justify-center bg-[linear-gradient(135deg,#667eea_0%,#764ba2_100%)] rounded-[14px] shadow-[0_4px_12px_rgba(102,126,234,0.3)]">
<AppstoreOutlined class="text-[28px] text-white" />
</div>
<div>
<h2 class="m-0 text-2xl font-700 text-[#333]">校本课程包</h2>
<p class="text-[#666] text-sm mt-1 mb-0">管理本校教师创建的校本课程包</p>
</div>
<a-button type="primary" @click="handleCreate">
<PlusOutlined /> 创建校本课程包
</a-button>
</div>
<a-button type="primary" @click="handleCreate">
<PlusOutlined /> 创建校本课程包
</a-button>
</div>
<!-- 统计概览 -->
<div class="stats-row">
<div class="stat-card">
<div class="stat-icon total">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div class="bg-white rounded-xl p-5 flex items-center gap-4 shadow-[0_2px_8px_rgba(0,0,0,0.06)]">
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-2xl bg-[linear-gradient(135deg,#667eea_0%,#764ba2_100%)] text-white">
<AppstoreOutlined />
</div>
<div class="stat-info">
<div class="stat-value">{{ dataSource.length }}</div>
<div class="stat-label">校本课程包总数</div>
<div>
<div class="text-[28px] font-700 text-[#333]">{{ dataSource.length }}</div>
<div class="text-[13px] text-[#666]">校本课程包总数</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon usage">
<div class="bg-white rounded-xl p-5 flex items-center gap-4 shadow-[0_2px_8px_rgba(0,0,0,0.06)]">
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-2xl bg-[linear-gradient(135deg,#43e97b_0%,#38f9d7_100%)] text-white">
<BarChartOutlined />
</div>
<div class="stat-info">
<div class="stat-value">{{ totalUsage }}</div>
<div class="stat-label">本周使用次数</div>
<div>
<div class="text-[28px] font-700 text-[#333]">{{ totalUsage }}</div>
<div class="text-[13px] text-[#666]">本周使用次数</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon reservation">
<div class="bg-white rounded-xl p-5 flex items-center gap-4 shadow-[0_2px_8px_rgba(0,0,0,0.06)]">
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-2xl bg-[linear-gradient(135deg,#4facfe_0%,#00f2fe_100%)] text-white">
<CalendarOutlined />
</div>
<div class="stat-info">
<div class="stat-value">{{ pendingReservations }}</div>
<div class="stat-label">待上课预约</div>
<div>
<div class="text-[28px] font-700 text-[#333]">{{ pendingReservations }}</div>
<div class="text-[13px] text-[#666]">待上课预约</div>
</div>
</div>
</div>
<!-- 套餐列表 -->
<a-card :bordered="false" class="list-card">
<a-card :bordered="false" class="rounded-xl list-card">
<template #title>
<span>校本课程包列表</span>
</template>
@ -58,7 +56,7 @@
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索课程包名称"
style="width: 200px"
class="w-[200px]"
@search="handleSearch"
/>
</template>
@ -72,21 +70,21 @@
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<div class="course-name">
<span class="name">{{ record.name }}</span>
<span v-if="record.changesSummary" class="changes-hint">
<div class="flex flex-col">
<span class="font-500">{{ record.name }}</span>
<span v-if="record.changesSummary" class="text-xs text-[#999] mt-1">
{{ record.changesSummary }}
</span>
</div>
</template>
<template v-else-if="column.key === 'sourceCourse'">
<div class="source-info">
<div class="flex items-center gap-2">
<img
v-if="record.sourceCourse?.coverImagePath"
:src="getFileUrl(record.sourceCourse.coverImagePath)"
class="cover"
class="w-10 h-10 object-cover rounded"
/>
<div v-else class="cover-placeholder">
<div v-else class="w-10 h-10 bg-[#f0f0f0] rounded flex items-center justify-center text-[#999]">
<BookOutlined />
</div>
<span>{{ record.sourceCourse?.name || '-' }}</span>
@ -150,10 +148,10 @@
@ok="handleReserve"
:confirmLoading="reserveLoading"
>
<div class="reserve-modal" v-if="selectedCourse">
<div class="course-info">
<span class="label">课程包名称</span>
<span class="value">{{ selectedCourse.name }}</span>
<div v-if="selectedCourse">
<div class="bg-[#f9f9f9] rounded-lg py-3 px-4 flex gap-2">
<span class="text-[#666]">课程包名称</span>
<span class="font-500">{{ selectedCourse.name }}</span>
</div>
<a-divider />
@ -179,7 +177,7 @@
show-time
format="YYYY-MM-DD HH:mm"
placeholder="选择预约时间"
style="width: 100%"
class="w-full"
/>
</a-form-item>
<a-form-item label="备注">
@ -203,8 +201,8 @@
width="800px"
:footer="null"
>
<div class="schedule-modal" v-if="selectedCourse">
<div class="course-info-header">
<div v-if="selectedCourse">
<div class="flex justify-between items-center mb-4 py-3 px-4 bg-[#f9f9f9] rounded-lg">
<span>课程包{{ selectedCourse.name }}</span>
<a-button type="primary" size="small" @click="showReserveModal(selectedCourse)">
<PlusOutlined /> 新增排课
@ -232,7 +230,7 @@
</template>
</template>
</a-table>
<div v-if="upcomingReservations.length === 0" class="empty-state">
<div v-if="upcomingReservations.length === 0" class="text-center py-10 text-[#999]">
暂无即将上课的预约
</div>
</a-tab-pane>
@ -536,196 +534,8 @@ onMounted(() => {
});
</script>
<style scoped lang="scss">
.school-course-list-page {
min-height: 100vh;
background: linear-gradient(180deg, #FFF8F0 0%, #FFFFFF 100%);
padding: 24px;
}
/* 页面头部 */
.page-header {
margin-bottom: 24px;
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
display: flex;
align-items: center;
gap: 16px;
}
.title-icon-wrapper {
width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 14px;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.title-icon {
font-size: 28px;
color: white;
}
.title-text h2 {
color: #333;
font-size: 24px;
font-weight: 700;
margin: 0;
}
.title-text p {
color: #666;
font-size: 14px;
margin: 4px 0 0 0;
}
}
/* 统计概览 */
.stats-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: white;
border-radius: 12px;
padding: 20px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.stat-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
&.total {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
&.usage {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
color: white;
}
&.reservation {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
}
.stat-info {
.stat-value {
font-size: 28px;
font-weight: 700;
color: #333;
}
.stat-label {
font-size: 13px;
color: #666;
}
}
}
/* 列表卡片 */
.list-card {
border-radius: 12px;
}
/* 课程名称 */
.course-name {
display: flex;
flex-direction: column;
.name {
font-weight: 500;
}
.changes-hint {
font-size: 12px;
color: #999;
margin-top: 4px;
}
}
/* 源课程信息 */
.source-info {
display: flex;
align-items: center;
gap: 8px;
.cover {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 4px;
}
.cover-placeholder {
width: 40px;
height: 40px;
background: #f0f0f0;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: #999;
}
}
/* 预约弹窗 */
.reserve-modal {
.course-info {
background: #f9f9f9;
border-radius: 8px;
padding: 12px 16px;
display: flex;
gap: 8px;
.label {
color: #666;
}
.value {
font-weight: 500;
}
}
}
/* 排课弹窗 */
.schedule-modal {
.course-info-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 12px 16px;
background: #f9f9f9;
border-radius: 8px;
}
.empty-state {
text-align: center;
padding: 40px;
color: #999;
}
<style scoped>
.list-card :deep(.ant-card-head) {
border-bottom: 1px solid #f0f0f0;
}
</style>

View File

@ -1,17 +1,17 @@
<template>
<div class="operation-log-view">
<div class="page-header">
<h2>操作日志</h2>
<div class="min-h-100vh p-4">
<div class="flex justify-between items-center mb-5">
<h2 class="m-0 text-xl font-600 text-[#2D3436]">操作日志</h2>
</div>
<!-- 筛选区 -->
<div class="filter-section">
<div class="mb-5 p-4 bg-[#fafafa] rounded-lg filter-section">
<a-space wrap>
<a-select
v-model:value="filters.module"
placeholder="选择模块"
allowClear
style="width: 150px"
class="w-[150px]"
@change="loadLogs"
>
<a-select-option v-for="module in modules" :key="module" :value="module">
@ -22,7 +22,7 @@
v-model:value="filters.action"
placeholder="选择操作"
allowClear
style="width: 150px"
class="w-[150px]"
@change="loadLogs"
>
<a-select-option v-for="action in actions" :key="action" :value="action">
@ -41,7 +41,7 @@
</div>
<!-- 统计卡片 -->
<div class="stats-section">
<div class="mb-5 stats-section">
<a-row :gutter="16">
<a-col :span="6">
<a-card size="small">
@ -50,7 +50,7 @@
</a-col>
<a-col :span="18">
<a-card size="small" title="模块分布">
<div class="module-stats">
<div class="flex flex-wrap gap-2 module-stats">
<a-tag v-for="mod in stats.modules" :key="mod.name" color="blue">
{{ mod.name }}: {{ mod.count }}
</a-tag>
@ -119,10 +119,10 @@
{{ selectedLog.description }}
</a-descriptions-item>
<a-descriptions-item label="变更前数据" :span="2">
<pre class="json-data">{{ formatJson(selectedLog.oldValue) }}</pre>
<pre class="max-h-[200px] overflow-auto bg-[#f5f5f5] p-2 rounded text-xs m-0 whitespace-pre-wrap break-all">{{ formatJson(selectedLog.oldValue) }}</pre>
</a-descriptions-item>
<a-descriptions-item label="变更后数据" :span="2">
<pre class="json-data">{{ formatJson(selectedLog.newValue) }}</pre>
<pre class="max-h-[200px] overflow-auto bg-[#f5f5f5] p-2 rounded text-xs m-0 whitespace-pre-wrap break-all">{{ formatJson(selectedLog.newValue) }}</pre>
</a-descriptions-item>
</a-descriptions>
</a-modal>
@ -282,47 +282,3 @@ onMounted(() => {
loadStats();
});
</script>
<style scoped lang="scss">
.operation-log-view {
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h2 {
margin: 0;
}
}
.filter-section {
margin-bottom: 20px;
padding: 16px;
background: #fafafa;
border-radius: 8px;
}
.stats-section {
margin-bottom: 20px;
}
.module-stats {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.json-data {
max-height: 200px;
overflow: auto;
background: #f5f5f5;
padding: 8px;
border-radius: 4px;
font-size: 12px;
margin: 0;
white-space: pre-wrap;
word-break: break-all;
}
}
</style>

View File

@ -1,19 +1,19 @@
<template>
<div class="settings-view">
<div class="page-header">
<h1><SettingOutlined /> 系统设置</h1>
<p>配置学校基本信息和通知偏好</p>
<div class="p-0 min-h-screen bg-gradient-to-b from-[#FFF8F0] to-white">
<div class="mb-6">
<h1 class="text-2xl font-semibold text-[#2D3436] m-0 mb-2 flex items-center gap-3"><SettingOutlined /> 系统设置</h1>
<p class="text-sm text-[#636E72] m-0">配置学校基本信息和通知偏好</p>
</div>
<a-spin :spinning="loading">
<div class="settings-content">
<div class="flex flex-col gap-6">
<!-- 基本信息 -->
<div class="settings-card">
<div class="card-header">
<span class="card-icon"><HomeOutlined /></span>
<h3>基本信息</h3>
<div class="bg-white rounded-2xl overflow-hidden shadow-[0_4px_15px_rgba(0,0,0,0.05)]">
<div class="flex items-center gap-3 py-5 px-6 border-b border-[#F5F5F5] bg-[#FAFAFA]">
<span class="text-xl text-[#FF8C42] flex items-center"><HomeOutlined /></span>
<h3 class="m-0 text-base font-semibold text-[#2D3436]">基本信息</h3>
</div>
<div class="card-body">
<div class="p-6 max-md:p-4">
<a-form
:model="formData"
:label-col="{ span: 4 }"
@ -23,9 +23,9 @@
<a-input v-model:value="formData.schoolName" placeholder="请输入学校名称" />
</a-form-item>
<a-form-item label="学校Logo">
<div class="logo-upload">
<div class="logo-preview" v-if="formData.schoolLogo">
<img :src="formData.schoolLogo" alt="Logo" />
<div class="flex items-center">
<div class="flex items-center gap-4" v-if="formData.schoolLogo">
<img :src="formData.schoolLogo" alt="Logo" class="w-20 h-20 object-contain rounded-lg border border-[#E0E0E0]" />
<div class="logo-actions">
<a-button type="link" size="small" @click="formData.schoolLogo = ''">
<DeleteOutlined /> 删除
@ -38,9 +38,9 @@
:before-upload="handleLogoUpload"
accept="image/*"
>
<div class="upload-placeholder">
<div class="w-20 h-20 border-2 border-dashed border-[#D9D9D9] rounded-lg flex flex-col items-center justify-center cursor-pointer transition-all text-[#636E72] hover:border-[#FF8C42] hover:text-[#FF8C42]">
<PlusOutlined />
<span>上传Logo</span>
<span class="text-xs mt-1">上传Logo</span>
</div>
</a-upload>
</div>
@ -57,12 +57,12 @@
</div>
<!-- 通知设置 -->
<div class="settings-card">
<div class="card-header">
<span class="card-icon"><BellOutlined /></span>
<h3>通知设置</h3>
<div class="bg-white rounded-2xl overflow-hidden shadow-[0_4px_15px_rgba(0,0,0,0.05)]">
<div class="flex items-center gap-3 py-5 px-6 border-b border-[#F5F5F5] bg-[#FAFAFA]">
<span class="text-xl text-[#FF8C42] flex items-center"><BellOutlined /></span>
<h3 class="m-0 text-base font-semibold text-[#2D3436]">通知设置</h3>
</div>
<div class="card-body">
<div class="p-6 max-md:p-4">
<a-form
:label-col="{ span: 8 }"
:wrapper-col="{ span: 12 }"
@ -73,7 +73,7 @@
checked-children="开"
un-checked-children="关"
/>
<span class="switch-hint">当教师完成一次授课后发送通知</span>
<span class="ml-3 max-md:ml-0 max-md:mt-1 max-md:block text-[#636E72] text-xs">当教师完成一次授课后发送通知</span>
</a-form-item>
<a-form-item label="任务提醒通知">
<a-switch
@ -81,7 +81,7 @@
checked-children="开"
un-checked-children="关"
/>
<span class="switch-hint">当有新的阅读任务时发送通知</span>
<span class="ml-3 max-md:ml-0 max-md:mt-1 max-md:block text-[#636E72] text-xs">当有新的阅读任务时发送通知</span>
</a-form-item>
<a-form-item label="成长档案通知">
<a-switch
@ -89,14 +89,14 @@
checked-children="开"
un-checked-children="关"
/>
<span class="switch-hint">当更新学生成长档案时发送通知</span>
<span class="ml-3 max-md:ml-0 max-md:mt-1 max-md:block text-[#636E72] text-xs">当更新学生成长档案时发送通知</span>
</a-form-item>
</a-form>
</div>
</div>
<!-- 保存按钮 -->
<div class="action-bar">
<div class="flex justify-center p-6 bg-white rounded-2xl shadow-[0_4px_15px_rgba(0,0,0,0.05)]">
<a-button type="primary" size="large" :loading="saving" @click="handleSave">
<SaveOutlined /> 保存设置
</a-button>
@ -183,147 +183,12 @@ onMounted(() => {
</script>
<style scoped>
.settings-view {
padding: 0;
min-height: 100vh;
background: linear-gradient(180deg, #FFF8F0 0%, #FFFFFF 100%);
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
font-size: 24px;
font-weight: 600;
color: #2D3436;
margin: 0 0 8px 0;
display: flex;
align-items: center;
gap: 12px;
}
.page-header p {
font-size: 14px;
color: #636E72;
margin: 0;
}
.settings-content {
display: flex;
flex-direction: column;
gap: 24px;
}
.settings-card {
background: white;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
}
.card-header {
display: flex;
align-items: center;
gap: 12px;
padding: 20px 24px;
border-bottom: 1px solid #F5F5F5;
background: #FAFAFA;
}
.card-icon {
font-size: 20px;
color: #FF8C42;
display: flex;
align-items: center;
}
.card-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #2D3436;
}
.card-body {
padding: 24px;
}
.logo-upload {
display: flex;
align-items: center;
}
.logo-preview {
display: flex;
align-items: center;
gap: 16px;
}
.logo-preview img {
width: 80px;
height: 80px;
object-fit: contain;
border-radius: 8px;
border: 1px solid #E0E0E0;
}
.upload-placeholder {
width: 80px;
height: 80px;
border: 2px dashed #D9D9D9;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
color: #636E72;
}
.upload-placeholder:hover {
border-color: #FF8C42;
color: #FF8C42;
}
.upload-placeholder span {
font-size: 12px;
margin-top: 4px;
}
.switch-hint {
margin-left: 12px;
color: #636E72;
font-size: 12px;
}
.action-bar {
display: flex;
justify-content: center;
padding: 24px;
background: white;
border-radius: 16px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
}
@media (max-width: 768px) {
.card-body {
padding: 16px;
}
:deep(.ant-form-item-label) {
padding-bottom: 8px !important;
}
:deep(.ant-form-item) {
margin-bottom: 16px;
}
.switch-hint {
display: block;
margin-left: 0;
margin-top: 4px;
}
}
</style>

View File

@ -1,10 +1,10 @@
<template>
<div class="task-list-view">
<div class="min-h-100vh p-4 bg-[#fafafa]">
<!-- 页面标题 -->
<div class="page-header">
<div class="flex justify-between items-start mb-6 page-header">
<div class="header-left">
<h2>阅读任务</h2>
<p class="page-desc">管理全校阅读任务跟踪学生完成情况</p>
<h2 class="m-0 text-2xl font-600 text-[#333]">阅读任务</h2>
<p class="mt-1 mb-0 text-[#999] text-sm">管理全校阅读任务跟踪学生完成情况</p>
</div>
<div class="header-right">
<a-button type="primary" @click="showCreateModal">
@ -14,42 +14,42 @@
</div>
<!-- 统计卡片 -->
<div class="stats-cards">
<div class="stat-card">
<div class="stat-icon total">
<div class="flex gap-5 mb-6 stats-cards">
<div class="flex-1 flex items-center gap-4 p-5 bg-white rounded-xl shadow-[0_2px_8px_rgba(0,0,0,0.04)] stat-card">
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-2xl bg-[#e6f7ff] text-[#1890ff]">
<FileTextOutlined />
</div>
<div class="stat-info">
<span class="stat-value">{{ stats.totalTasks }}</span>
<span class="stat-label">全部任务</span>
<span class="block text-[28px] font-600 text-[#333]">{{ stats.totalTasks }}</span>
<span class="text-[13px] text-[#999]">全部任务</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon active">
<div class="flex-1 flex items-center gap-4 p-5 bg-white rounded-xl shadow-[0_2px_8px_rgba(0,0,0,0.04)] stat-card">
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-2xl bg-[#fff7e6] text-[#fa8c16]">
<SyncOutlined />
</div>
<div class="stat-info">
<span class="stat-value">{{ stats.publishedTasks }}</span>
<span class="stat-label">进行中</span>
<span class="block text-[28px] font-600 text-[#333]">{{ stats.publishedTasks }}</span>
<span class="text-[13px] text-[#999]">进行中</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon done">
<div class="flex-1 flex items-center gap-4 p-5 bg-white rounded-xl shadow-[0_2px_8px_rgba(0,0,0,0.04)] stat-card">
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-2xl bg-[#f6ffed] text-[#52c41a]">
<CheckCircleOutlined />
</div>
<div class="stat-info">
<span class="stat-value">{{ stats.completionRate }}%</span>
<span class="stat-label">完成率</span>
<span class="block text-[28px] font-600 text-[#333]">{{ stats.completionRate }}%</span>
<span class="text-[13px] text-[#999]">完成率</span>
</div>
</div>
</div>
<!-- 筛选区域 -->
<div class="filter-bar">
<div class="flex gap-3 mb-6 filter-bar">
<a-select
v-model:value="filters.status"
placeholder="任务状态"
style="width: 120px"
class="w-[120px]"
allowClear
@change="loadTasks"
>
@ -60,7 +60,7 @@
<a-select
v-model:value="filters.taskType"
placeholder="任务类型"
style="width: 120px"
class="w-[120px]"
allowClear
@change="loadTasks"
>
@ -71,7 +71,7 @@
<a-input-search
v-model:value="filters.keyword"
placeholder="搜索任务标题"
style="width: 200px"
class="w-[200px]"
@search="loadTasks"
allow-clear
/>
@ -79,43 +79,43 @@
<!-- 任务列表 -->
<a-spin :spinning="loading">
<div class="task-list" v-if="tasks.length > 0">
<div v-for="task in tasks" :key="task.id" class="task-card">
<div class="card-header">
<div class="task-title">
<div class="flex flex-col gap-4 task-list" v-if="tasks.length > 0">
<div v-for="task in tasks" :key="task.id" class="bg-white border border-[#f0f0f0] rounded-xl overflow-hidden task-card">
<div class="flex justify-between items-center py-4 px-5 bg-[#fafafa] border-b border-[#f0f0f0] card-header">
<div class="flex items-center gap-3 task-title">
<a-tag :color="getTypeColor(task.taskType)">{{ getTypeText(task.taskType) }}</a-tag>
<h3>{{ task.title }}</h3>
<h3 class="m-0 text-base font-600 text-[#333]">{{ task.title }}</h3>
</div>
<a-tag :color="getStatusColor(task.status)">{{ getStatusText(task.status) }}</a-tag>
</div>
<div class="card-body">
<p v-if="task.description">{{ task.description }}</p>
<div class="task-meta">
<span v-if="task.course">
<div class="py-4 px-5 card-body">
<p v-if="task.description" class="text-[#666] m-0 mb-3 leading-[1.6]">{{ task.description }}</p>
<div class="flex gap-6 text-[#999] text-[13px] task-meta">
<span v-if="task.course" class="flex items-center gap-1.5">
<BookOutlined /> {{ task.course.name }}
</span>
<span>
<span class="flex items-center gap-1.5">
<CalendarOutlined />
{{ formatDate(task.startDate) }} - {{ formatDate(task.endDate) }}
</span>
<span>
<span class="flex items-center gap-1.5">
<TeamOutlined /> {{ task.targetCount || 0 }} 个目标
</span>
</div>
</div>
<div class="card-footer">
<div class="progress-info">
<div class="flex justify-between items-center py-3 px-5 bg-[#fafafa] border-t border-[#f0f0f0] card-footer">
<div class="flex items-center gap-3 progress-info">
<a-progress
:percent="getCompletionRate(task)"
:stroke-color="{ '0%': '#52c41a', '100%': '#73d13d' }"
size="small"
style="width: 150px;"
class="w-[150px]"
/>
<span class="progress-text">{{ getCompletedCount(task) }}/{{ task.completionCount || 0 }} 人完成</span>
<span class="text-[13px] text-[#666]">{{ getCompletedCount(task) }}/{{ task.completionCount || 0 }} 人完成</span>
</div>
<div class="card-actions">
<div class="flex gap-2 card-actions">
<a-button type="link" size="small" @click="viewCompletionDetail(task)">
<EyeOutlined /> 完成情况
</a-button>
@ -133,15 +133,15 @@
</div>
<!-- 空状态 -->
<div class="empty-state" v-else>
<InboxOutlined class="empty-icon" />
<p>暂无阅读任务</p>
<div class="text-center py-[60px] px-5 text-[#999] empty-state" v-else>
<InboxOutlined class="text-[64px] text-[#d9d9d9] mb-4" />
<p class="my-1">暂无阅读任务</p>
<a-button type="primary" @click="showCreateModal">发布第一个任务</a-button>
</div>
</a-spin>
<!-- 分页 -->
<div class="pagination-section" v-if="total > pageSize">
<div class="flex justify-center mt-6 pagination-section" v-if="total > pageSize">
<a-pagination
v-model:current="currentPage"
:total="total"
@ -170,7 +170,7 @@
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="任务类型" required>
<a-select v-model:value="createForm.taskType" style="width: 100%;">
<a-select v-model:value="createForm.taskType" class="w-full">
<a-select-option value="READING">阅读</a-select-option>
<a-select-option value="ACTIVITY">活动</a-select-option>
<a-select-option value="HOMEWORK">作业</a-select-option>
@ -191,7 +191,7 @@
v-model:value="createForm.targetIds"
mode="multiple"
placeholder="请选择班级"
style="width: 100%;"
class="w-full"
>
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
{{ cls.name }} ({{ cls.grade }})
@ -201,7 +201,7 @@
<a-form-item label="任务时间" required>
<a-range-picker
v-model:value="createForm.dateRange"
style="width: 100%;"
class="w-full"
/>
</a-form-item>
</a-form>
@ -214,7 +214,7 @@
:footer="null"
width="700px"
>
<div class="completion-stats">
<div class="flex gap-2 mb-4 completion-stats">
<a-tag color="blue">{{ completionStats.pending }} 待完成</a-tag>
<a-tag color="orange">{{ completionStats.inProgress }} 进行中</a-tag>
<a-tag color="green">{{ completionStats.completed }} 已完成</a-tag>
@ -234,10 +234,10 @@
</a-tag>
</template>
<template v-if="column.key === 'feedback'">
<span v-if="record.parentFeedback" class="feedback-text">
<span v-if="record.parentFeedback" class="text-[#666] text-xs">
{{ record.parentFeedback.substring(0, 30) }}{{ record.parentFeedback.length > 30 ? '...' : '' }}
</span>
<span v-else class="no-feedback">暂无家长反馈</span>
<span v-else class="text-[#bfbfbf] text-xs">暂无家长反馈</span>
</template>
</template>
</a-table>
@ -532,212 +532,3 @@ onMounted(() => {
loadTasks();
});
</script>
<style scoped lang="scss">
.task-list-view {
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
.header-left {
h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
.page-desc {
margin: 4px 0 0;
color: #999;
font-size: 14px;
}
}
}
.stats-cards {
display: flex;
gap: 20px;
margin-bottom: 24px;
.stat-card {
flex: 1;
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
.stat-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
&.total {
background: #e6f7ff;
color: #1890ff;
}
&.active {
background: #fff7e6;
color: #fa8c16;
}
&.done {
background: #f6ffed;
color: #52c41a;
}
}
.stat-info {
.stat-value {
display: block;
font-size: 28px;
font-weight: 600;
color: #333;
}
.stat-label {
font-size: 13px;
color: #999;
}
}
}
}
.filter-bar {
display: flex;
gap: 12px;
margin-bottom: 24px;
}
.task-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.task-card {
background: white;
border: 1px solid #f0f0f0;
border-radius: 12px;
overflow: hidden;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
.task-title {
display: flex;
align-items: center;
gap: 12px;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
}
}
.card-body {
padding: 16px 20px;
p {
color: #666;
margin: 0 0 12px;
line-height: 1.6;
}
.task-meta {
display: flex;
gap: 24px;
color: #999;
font-size: 13px;
span {
display: flex;
align-items: center;
gap: 6px;
}
}
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background: #fafafa;
border-top: 1px solid #f0f0f0;
.progress-info {
display: flex;
align-items: center;
gap: 12px;
.progress-text {
font-size: 13px;
color: #666;
}
}
.card-actions {
display: flex;
gap: 8px;
}
}
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
.empty-icon {
font-size: 64px;
color: #d9d9d9;
margin-bottom: 16px;
}
p {
margin: 4px 0;
}
}
.pagination-section {
display: flex;
justify-content: center;
margin-top: 24px;
}
.completion-stats {
margin-bottom: 16px;
display: flex;
gap: 8px;
}
.feedback-text {
color: #666;
font-size: 12px;
}
.no-feedback {
color: #bfbfbf;
font-size: 12px;
}
}
</style>

View File

@ -1,10 +1,10 @@
<template>
<div class="task-template-view">
<div class="min-h-100vh p-4 bg-[#fafafa]">
<!-- 页面标题 -->
<div class="page-header">
<div class="flex justify-between items-start mb-6 page-header">
<div class="header-left">
<h2>任务模板</h2>
<p class="page-desc">创建和管理任务模板方便教师快速创建任务</p>
<h2 class="m-0 text-2xl font-600 text-[#333]">任务模板</h2>
<p class="mt-1 mb-0 text-[#999] text-sm">创建和管理任务模板方便教师快速创建任务</p>
</div>
<div class="header-right">
<a-button type="primary" @click="showCreateModal">
@ -14,11 +14,11 @@
</div>
<!-- 筛选区域 -->
<div class="filter-bar">
<div class="flex gap-4 mb-6 filter-bar">
<a-select
v-model:value="filters.taskType"
placeholder="任务类型"
style="width: 150px"
class="w-[150px]"
allow-clear
@change="loadTemplates"
>
@ -30,29 +30,29 @@
<a-input-search
v-model:value="filters.keyword"
placeholder="搜索模板名称"
style="width: 250px"
class="w-[250px]"
@search="loadTemplates"
/>
</div>
<!-- 模板列表 -->
<a-spin :spinning="loading">
<div class="template-list" v-if="templates.length > 0">
<div class="flex flex-col gap-4 template-list" v-if="templates.length > 0">
<div
v-for="template in templates"
:key="template.id"
class="template-card"
:class="{ 'is-default': template.isDefault }"
class="bg-white border rounded-xl overflow-hidden transition-all duration-300 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] template-card"
:class="template.isDefault ? 'border-[#faad14] bg-[linear-gradient(135deg,#fffbe6_0%,white_30%)]' : 'border-[#f0f0f0]'"
>
<div class="card-header">
<div class="template-info">
<div class="flex justify-between items-center py-4 px-5 bg-[#fafafa] border-b border-[#f0f0f0] card-header">
<div class="flex items-center gap-3 template-info">
<a-tag :color="getTaskTypeColor(template.taskType)">
{{ getTaskTypeText(template.taskType) }}
</a-tag>
<span class="template-name">{{ template.name }}</span>
<span class="text-base font-600 text-[#333]">{{ template.name }}</span>
<a-tag v-if="template.isDefault" color="gold">默认</a-tag>
</div>
<div class="template-actions">
<div class="flex gap-2 template-actions">
<a-button type="link" size="small" @click="showEditModal(template)">
编辑
</a-button>
@ -65,15 +65,15 @@
</div>
</div>
<div class="card-body">
<p class="template-desc" v-if="template.description">
<div class="py-4 px-5 card-body">
<p class="text-[#666] m-0 mb-3 leading-[1.6] template-desc" v-if="template.description">
{{ template.description }}
</p>
<div class="template-meta">
<span v-if="template.course">
<div class="flex gap-6 text-[#999] text-[13px] template-meta">
<span v-if="template.course" class="flex items-center gap-1.5">
<BookOutlined /> {{ template.course.name }}
</span>
<span>
<span class="flex items-center gap-1.5">
<ClockCircleOutlined /> 默认 {{ template.defaultDuration }}
</span>
</div>
@ -82,15 +82,15 @@
</div>
<!-- 空状态 -->
<div class="empty-state" v-else>
<InboxOutlined class="empty-icon" />
<p>暂无任务模板</p>
<p class="empty-hint">点击"新建模板"创建第一个任务模板</p>
<div class="text-center py-[60px] px-5 text-[#999] empty-state" v-else>
<InboxOutlined class="text-[64px] text-[#d9d9d9] mb-4" />
<p class="my-1">暂无任务模板</p>
<p class="text-[13px] text-[#bfbfbf]">点击"新建模板"创建第一个任务模板</p>
</div>
</a-spin>
<!-- 分页 -->
<div class="pagination-wrapper" v-if="total > pageSize">
<div class="flex justify-center mt-6 pagination-wrapper" v-if="total > pageSize">
<a-pagination
v-model:current="page"
:total="total"
@ -146,7 +146,7 @@
:max="30"
addon-after="天"
/>
<span class="form-hint">任务从开始到结束的默认天数</span>
<span class="ml-3 text-[#999] text-xs">任务从开始到结束的默认天数</span>
</a-form-item>
<a-form-item label="模板描述">
@ -160,7 +160,7 @@
<a-form-item label="设为默认">
<a-switch v-model:checked="form.isDefault" />
<span class="form-hint">设为默认后教师创建该类型任务时将自动填充</span>
<span class="ml-3 text-[#999] text-xs">设为默认后教师创建该类型任务时将自动填充</span>
</a-form-item>
</a-form>
</a-modal>
@ -340,135 +340,3 @@ onMounted(() => {
loadTemplates();
});
</script>
<style scoped lang="scss">
.task-template-view {
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
.header-left {
h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
.page-desc {
margin: 4px 0 0;
color: #999;
font-size: 14px;
}
}
}
.filter-bar {
display: flex;
gap: 16px;
margin-bottom: 24px;
}
.template-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.template-card {
background: white;
border: 1px solid #f0f0f0;
border-radius: 12px;
overflow: hidden;
transition: all 0.3s;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
&.is-default {
border-color: #faad14;
background: linear-gradient(135deg, #fffbe6 0%, white 30%);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
.template-info {
display: flex;
align-items: center;
gap: 12px;
.template-name {
font-size: 16px;
font-weight: 600;
color: #333;
}
}
}
.card-body {
padding: 16px 20px;
.template-desc {
color: #666;
margin: 0 0 12px;
line-height: 1.6;
}
.template-meta {
display: flex;
gap: 24px;
color: #999;
font-size: 13px;
span {
display: flex;
align-items: center;
gap: 6px;
}
}
}
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
.empty-icon {
font-size: 64px;
color: #d9d9d9;
margin-bottom: 16px;
}
p {
margin: 4px 0;
}
.empty-hint {
font-size: 13px;
color: #bfbfbf;
}
}
.pagination-wrapper {
display: flex;
justify-content: center;
margin-top: 24px;
}
.form-hint {
margin-left: 12px;
color: #999;
font-size: 12px;
}
}
</style>

View File

@ -1,109 +1,112 @@
<template>
<div class="teacher-list-view">
<div class="min-h-100vh p-0 bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)]">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-content">
<div class="header-title">
<div class="title-icon">
<SolutionOutlined />
<div class="rounded-[20px] py-6 px-8 mb-6 bg-[linear-gradient(135deg,#667eea_0%,#764ba2_100%)]">
<div class="flex justify-between items-center max-md:flex-col max-md:gap-4 max-md:text-center">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-xl flex items-center justify-center bg-white/20">
<SolutionOutlined class="text-[28px] text-white" />
</div>
<div class="title-text">
<h2>教师管理</h2>
<p>管理学校教师信息与班级分配</p>
<div>
<h2 class="text-white text-2xl font-700 m-0">教师管理</h2>
<p class="text-white/80 text-sm mt-1 m-0">管理学校教师信息与班级分配</p>
</div>
</div>
<div class="header-stats">
<div class="stat-item">
<span class="stat-value">{{ teachers.length }}</span>
<span class="stat-label">教师总数</span>
<div class="flex gap-8 max-md:w-full max-md:justify-center">
<div class="text-center stat-item">
<span class="block text-[32px] font-700 text-white stat-value">{{ teachers.length }}</span>
<span class="text-xs text-white/80 stat-label">教师总数</span>
</div>
<div class="stat-item">
<span class="stat-value active">{{ activeCount }}</span>
<span class="stat-label">在职教师</span>
<div class="text-center stat-item">
<span class="block text-[32px] font-700 text-[#FFD93D] stat-value">{{ activeCount }}</span>
<span class="text-xs text-white/80 stat-label">在职教师</span>
</div>
</div>
</div>
</div>
<!-- 操作栏 -->
<div class="action-bar">
<div class="flex justify-between items-center mb-6 py-4 px-5 bg-white rounded-2xl shadow-[0_2px_8px_rgba(0,0,0,0.04)] max-md:flex-col max-md:gap-3">
<div class="search-box">
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索教师姓名/手机号/账号"
style="width: 280px;"
class="w-[280px]"
@search="handleSearch"
allow-clear
>
<template #prefix>
<SearchOutlined style="color: #B2BEC3;" />
<SearchOutlined class="text-[#B2BEC3]" />
</template>
</a-input-search>
</div>
<a-button type="primary" class="add-btn" @click="showAddModal">
<PlusOutlined class="btn-icon" />
<a-button type="primary" class="!bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)] hover:!bg-[linear-gradient(135deg,#FF7A2A_0%,#FFA030_100%)] !border-0 rounded-xl h-10 px-6 font-600" @click="showAddModal">
<PlusOutlined class="mr-2 text-sm" />
添加教师
</a-button>
</div>
<!-- 教师卡片列表 -->
<div class="teacher-grid" v-if="!loading && teachers.length > 0">
<div class="grid gap-5 mb-6 grid-cols-1 md:grid-cols-[repeat(auto-fill,minmax(320px,1fr))]" v-if="!loading && teachers.length > 0">
<div
v-for="teacher in teachers"
:key="teacher.id"
class="teacher-card"
:class="{ 'inactive': teacher.status !== 'ACTIVE' }"
class="bg-white rounded-2xl overflow-hidden shadow-[0_4px_12px_rgba(0,0,0,0.05)] transition-all duration-300 border-2 border-transparent hover:translate-y-[-4px] hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)] hover:border-[#FF8C42]"
:class="teacher.status !== 'ACTIVE' ? 'opacity-70' : ''"
>
<div class="card-header">
<div class="teacher-avatar">
<SolutionOutlined class="avatar-icon" />
<div class="flex items-center gap-3 py-4 px-5 bg-[linear-gradient(135deg,#F8F9FA_0%,#FFFFFF_100%)] border-b border-[#F0F0F0]">
<div class="w-12 h-12 rounded-xl flex items-center justify-center bg-[linear-gradient(135deg,#667eea_0%,#764ba2_100%)]">
<SolutionOutlined class="text-2xl text-white" />
</div>
<div class="teacher-basic">
<div class="teacher-name">{{ teacher.name }}</div>
<div class="teacher-account">@{{ teacher.loginAccount }}</div>
<div class="flex-1 min-w-0">
<div class="text-base font-600 text-[#2D3436]">{{ teacher.name }}</div>
<div class="text-xs text-[#636E72] mt-0.5">@{{ teacher.loginAccount }}</div>
</div>
<div class="status-badge" :class="teacher.status === 'ACTIVE' ? 'active' : 'inactive'">
<span
class="py-1 px-3 rounded-[20px] text-xs font-500"
:class="teacher.status === 'ACTIVE' ? 'bg-[#E8F5E9] text-[#43A047]' : 'bg-[#FFEBEE] text-[#E53935]'"
>
{{ teacher.status === 'ACTIVE' ? '在职' : '离职' }}
</div>
</span>
</div>
<div class="card-body">
<div class="info-row">
<PhoneOutlined class="info-icon" />
<span class="info-value">{{ teacher.phone || '未设置' }}</span>
<div class="py-4 px-5 card-body">
<div class="flex items-center gap-2 mb-2 text-[13px]">
<PhoneOutlined class="text-sm text-[#667eea]" />
<span class="text-[#636E72]">{{ teacher.phone || '未设置' }}</span>
</div>
<div class="info-row">
<MailOutlined class="info-icon" />
<span class="info-value">{{ teacher.email || '未设置' }}</span>
<div class="flex items-center gap-2 mb-2 text-[13px]">
<MailOutlined class="text-sm text-[#667eea]" />
<span class="text-[#636E72]">{{ teacher.email || '未设置' }}</span>
</div>
<div class="info-row">
<BankOutlined class="info-icon" />
<span class="info-value classes-tag">
<div class="flex items-center gap-2 mb-2 text-[13px]">
<BankOutlined class="text-sm text-[#667eea]" />
<span class="text-[#636E72] classes-tag">
<span v-if="teacher.classNames && (Array.isArray(teacher.classNames) ? teacher.classNames.length > 0 : teacher.classNames)">
{{ Array.isArray(teacher.classNames) ? teacher.classNames.slice(0, 2).join('、') : teacher.classNames }}
<span v-if="Array.isArray(teacher.classNames) && teacher.classNames.length > 2">{{ teacher.classNames.length }}个班级</span>
</span>
<span v-else class="no-class">未分配班级</span>
<span v-else class="text-[#B2BEC3] italic">未分配班级</span>
</span>
</div>
<div class="info-row">
<BookOutlined class="info-icon" />
<span class="info-value">授课 <strong>{{ teacher.lessonCount || 0 }}</strong> </span>
<div class="flex items-center gap-2 text-[13px]">
<BookOutlined class="text-sm text-[#667eea]" />
<span class="text-[#636E72]">授课 <strong class="text-[#FF8C42]">{{ teacher.lessonCount || 0 }}</strong> </span>
</div>
</div>
<div class="card-actions">
<a-button type="link" size="small" @click="handleEdit(teacher)">
<div class="flex justify-end gap-2 py-3 px-5 border-t border-[#F0F0F0] bg-[#FAFAFA] card-actions">
<a-button type="link" size="small" @click="handleEdit(teacher)" class="!py-1 !px-2 !h-auto inline-flex items-center gap-1">
<EditOutlined /> 编辑
</a-button>
<a-button type="link" size="small" @click="handleResetPassword(teacher)">
<a-button type="link" size="small" @click="handleResetPassword(teacher)" class="!py-1 !px-2 !h-auto inline-flex items-center gap-1">
<KeyOutlined /> 重置密码
</a-button>
<a-popconfirm
title="确定要删除这位教师吗?"
@confirm="handleDelete(teacher.id)"
>
<a-button type="link" size="small" danger>
<a-button type="link" size="small" danger class="!py-1 !px-2 !h-auto">
<DeleteOutlined /> 删除
</a-button>
</a-popconfirm>
@ -112,22 +115,22 @@
</div>
<!-- 空状态 -->
<div class="empty-state" v-if="!loading && teachers.length === 0">
<InboxOutlined class="empty-icon" />
<p>暂无教师数据</p>
<a-button type="primary" @click="showAddModal">
<div class="flex flex-col items-center justify-center py-20 bg-white rounded-2xl empty-state" v-if="!loading && teachers.length === 0">
<InboxOutlined class="text-[64px] text-[#B2BEC3] mb-4" />
<p class="text-[#636E72] text-base mb-6">暂无教师数据</p>
<a-button type="primary" class="!bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)] !border-0 rounded-xl" @click="showAddModal">
添加第一位教师
</a-button>
</div>
<!-- 加载状态 -->
<div class="loading-state" v-if="loading">
<div class="flex flex-col items-center justify-center py-20 loading-state" v-if="loading">
<a-spin size="large" />
<p>加载中...</p>
<p class="text-[#636E72] mt-4">加载中...</p>
</div>
<!-- 分页 -->
<div class="pagination-wrapper" v-if="teachers.length > 0">
<div class="flex justify-center py-6 bg-white rounded-2xl pagination-wrapper" v-if="teachers.length > 0">
<a-pagination
v-model:current="pagination.current"
v-model:pageSize="pagination.pageSize"
@ -146,12 +149,11 @@
@cancel="handleModalCancel"
:confirm-loading="submitting"
:width="520"
class="teacher-modal"
>
<template #title>
<span class="modal-title">
<EditOutlined v-if="isEdit" class="modal-title-icon" />
<PlusOutlined v-else class="modal-title-icon" />
<span class="flex items-center gap-2 modal-title">
<EditOutlined v-if="isEdit" class="text-[#667eea]" />
<PlusOutlined v-else class="text-[#667eea]" />
{{ isEdit ? '编辑教师' : '添加教师' }}
</span>
</template>
@ -164,17 +166,17 @@
>
<a-form-item label="姓名" name="name">
<a-input v-model:value="formState.name" placeholder="请输入教师姓名">
<template #prefix><UserOutlined style="color: #B2BEC3;" /></template>
<template #prefix><UserOutlined class="text-[#B2BEC3]" /></template>
</a-input>
</a-form-item>
<a-form-item label="手机号" name="phone">
<a-input v-model:value="formState.phone" placeholder="请输入手机号">
<template #prefix><PhoneOutlined style="color: #B2BEC3;" /></template>
<template #prefix><PhoneOutlined class="text-[#B2BEC3]" /></template>
</a-input>
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input v-model:value="formState.email" placeholder="请输入邮箱(可选)">
<template #prefix><MailOutlined style="color: #B2BEC3;" /></template>
<template #prefix><MailOutlined class="text-[#B2BEC3]" /></template>
</a-input>
</a-form-item>
<a-form-item label="登录账号" name="loginAccount">
@ -183,12 +185,12 @@
placeholder="请输入登录账号"
:disabled="isEdit"
>
<template #prefix><KeyOutlined style="color: #B2BEC3;" /></template>
<template #prefix><KeyOutlined class="text-[#B2BEC3]" /></template>
</a-input>
</a-form-item>
<a-form-item v-if="!isEdit" label="密码" name="password">
<a-input-password v-model:value="formState.password" placeholder="请输入密码默认123456">
<template #prefix><LockOutlined style="color: #B2BEC3;" /></template>
<template #prefix><LockOutlined class="text-[#B2BEC3]" /></template>
</a-input-password>
</a-form-item>
<a-form-item label="负责班级" name="classIds">
@ -214,20 +216,20 @@
:width="400"
>
<template #title>
<span class="modal-title">
<KeyOutlined class="modal-title-icon" />
<span class="flex items-center gap-2 modal-title">
<KeyOutlined class="text-[#667eea]" />
重置密码
</span>
</template>
<div class="reset-password-content">
<div class="reset-warning">
<WarningOutlined class="warning-icon" />
<p>确定要重置 <strong>{{ currentTeacher?.name }}</strong> 的密码吗</p>
<div class="text-center reset-password-content">
<div class="py-5 px-5 bg-[#FFF8F0] rounded-xl mb-5 reset-warning">
<WarningOutlined class="block text-[32px] text-[#FF8C42] mb-2" />
<p class="m-0 text-[#636E72]">确定要重置 <strong>{{ currentTeacher?.name }}</strong> 的密码吗</p>
</div>
<div v-if="newPassword" class="new-password-box">
<p>新密码</p>
<div class="password-display">
<a-typography-text copyable>{{ newPassword }}</a-typography-text>
<p class="mb-2 text-[#636E72]">新密码</p>
<div class="p-4 rounded-xl font-bold text-2xl text-white bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)] password-display">
<a-typography-text copyable class="!text-white">{{ newPassword }}</a-typography-text>
</div>
</div>
</div>
@ -466,376 +468,22 @@ onMounted(() => {
</script>
<style scoped>
.teacher-list-view {
min-height: 100vh;
background: linear-gradient(180deg, #FFF8F0 0%, #FFFFFF 100%);
padding: 0;
}
/* 页面头部 */
.page-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20px;
padding: 24px 32px;
margin-bottom: 24px;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
display: flex;
align-items: center;
gap: 16px;
}
.title-icon {
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.title-icon :deep(svg) {
font-size: 28px;
color: white;
}
.title-text h2 {
color: white;
font-size: 24px;
font-weight: 700;
margin: 0;
}
.title-text p {
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
margin: 4px 0 0 0;
}
.header-stats {
display: flex;
gap: 32px;
}
.stat-item {
text-align: center;
}
.stat-value {
display: block;
font-size: 32px;
font-weight: 700;
color: white;
}
.stat-value.active {
color: #FFD93D;
}
.stat-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
}
/* 操作栏 */
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 16px 20px;
background: white;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.search-box :deep(.ant-input-affix-wrapper) {
border-radius: 12px;
border: 2px solid #F0F0F0;
}
.search-box :deep(.ant-input-affix-wrapper:hover) {
border-color: #FF8C42;
}
.add-btn {
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
border: none;
border-radius: 12px;
height: 40px;
padding: 0 24px;
font-weight: 600;
}
.add-btn:hover {
background: linear-gradient(135deg, #FF7A2A 0%, #FFA030 100%);
}
.btn-icon {
margin-right: 8px;
font-size: 14px;
}
/* 教师卡片网格 */
.teacher-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.teacher-card {
background: white;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
border: 2px solid transparent;
}
.teacher-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
border-color: #FF8C42;
}
.teacher-card.inactive {
opacity: 0.7;
}
.teacher-card .card-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
background: linear-gradient(135deg, #F8F9FA 0%, #FFFFFF 100%);
border-bottom: 1px solid #F0F0F0;
}
.teacher-avatar {
width: 48px;
height: 48px;
border-radius: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
}
.avatar-icon {
font-size: 24px;
color: white;
}
.teacher-basic {
flex: 1;
}
.teacher-name {
font-size: 16px;
font-weight: 600;
color: #2D3436;
}
.teacher-account {
font-size: 12px;
color: #636E72;
margin-top: 2px;
}
.status-badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
}
.status-badge.active {
background: #E8F5E9;
color: #43A047;
}
.status-badge.inactive {
background: #FFEBEE;
color: #E53935;
}
.card-body {
padding: 16px 20px;
}
.info-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
font-size: 13px;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-icon {
font-size: 14px;
color: #667eea;
margin-right: 4px;
}
.info-value {
color: #636E72;
}
.info-value strong {
color: #FF8C42;
}
.no-class {
color: #B2BEC3;
font-style: italic;
}
.card-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 12px 20px;
border-top: 1px solid #F0F0F0;
background: #FAFAFA;
}
.card-actions :deep(.ant-btn-link) {
padding: 4px 8px;
height: auto;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
background: white;
border-radius: 16px;
}
.empty-icon {
font-size: 64px;
color: #B2BEC3;
margin-bottom: 16px;
}
.empty-state p {
color: #636E72;
font-size: 16px;
margin-bottom: 24px;
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
}
.loading-state p {
color: #636E72;
margin-top: 16px;
}
/* 分页 */
.pagination-wrapper {
display: flex;
justify-content: center;
padding: 24px;
background: white;
border-radius: 16px;
}
/* 重置密码弹窗 */
.reset-password-content {
text-align: center;
}
.reset-warning {
padding: 20px;
background: #FFF8F0;
border-radius: 12px;
margin-bottom: 20px;
}
.warning-icon {
font-size: 32px;
color: #FF8C42;
display: block;
margin-bottom: 8px;
}
.reset-warning p {
margin: 0;
color: #636E72;
}
.new-password-box p {
margin-bottom: 8px;
color: #636E72;
}
.password-display {
padding: 16px;
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
border-radius: 12px;
font-size: 24px;
font-weight: bold;
color: white;
}
/* Modal title styling */
.modal-title {
display: flex;
align-items: center;
gap: 8px;
}
.modal-title-icon {
color: #667eea;
}
/* 响应式设计 */
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 16px;
text-align: center;
}
.header-stats {
width: 100%;
justify-content: center;
}
.action-bar {
flex-direction: column;
gap: 12px;
}
.search-box :deep(.ant-input-search) {
width: 100% !important;
}
.teacher-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@ -1,157 +1,137 @@
<template>
<div class="teacher-dashboard">
<div class="min-h-[calc(100vh-120px)] bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)] p-6 rounded-2xl teacher-dashboard">
<!-- 欢迎横幅 -->
<div class="welcome-banner">
<div class="banner-decoration">
<div class="deco-circle c1"></div>
<div class="deco-circle c2"></div>
<div class="deco-circle c3"></div>
<div class="deco-shape star">
<StarFilled />
</div>
<div class="deco-shape book">
<BookOutlined />
</div>
<div class="relative bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)] rounded-[20px] py-7 px-8 mb-6 overflow-hidden welcome-banner">
<div class="absolute top-0 right-0 w-full h-full pointer-events-none banner-decoration">
<div class="absolute rounded-full bg-white/12 w-[140px] h-[140px] -top-10 right-[60px] deco-circle c1"></div>
<div class="absolute rounded-full bg-white/12 w-20 h-20 top-[50px] right-[150px] deco-circle c2"></div>
<div class="absolute rounded-full bg-white/12 w-[50px] h-[50px] -bottom-4 right-[100px] deco-circle c3"></div>
<div class="absolute flex items-center justify-center w-9 h-9 rounded-lg bg-white/20 text-white top-5 right-[30px] text-base deco-shape star"><StarFilled /></div>
<div class="absolute flex items-center justify-center w-9 h-9 rounded-lg bg-white/20 text-white bottom-4 right-[200px] text-lg deco-shape book"><BookOutlined /></div>
</div>
<div class="banner-content">
<div class="flex justify-between items-center relative z-1 banner-content">
<div class="welcome-text">
<h1 class="welcome-title">你好老师</h1>
<p class="welcome-subtitle">今天也要开启美妙的阅读之旅哦</p>
<h1 class="text-[26px] font-700 text-white m-0 mb-1.5 welcome-title">你好老师</h1>
<p class="text-[15px] text-white/90 m-0 welcome-subtitle">今天也要开启美妙的阅读之旅哦</p>
</div>
<div class="welcome-date">
<CalendarOutlined class="date-icon" />
<span class="date-text">{{ currentDate }}</span>
<div class="flex items-center gap-2 bg-white/20 py-2.5 px-5 rounded-[24px] welcome-date">
<CalendarOutlined class="text-base text-white date-icon" />
<span class="text-[15px] text-white font-500 date-text">{{ currentDate }}</span>
</div>
</div>
</div>
<!-- 统计卡片 -->
<div class="stats-section">
<div class="stats-grid">
<div class="stat-card class-card">
<div class="stat-icon-wrapper orange">
<HomeOutlined />
</div>
<div class="stat-info">
<div class="stat-value">{{ stats.classCount }}</div>
<div class="stat-label">我的班级</div>
<div class="mb-6 stats-section">
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4 stats-grid">
<div class="relative bg-white rounded-2xl p-5 flex items-center gap-4 shadow-[0_4px_16px_rgba(0,0,0,0.06)] transition-all duration-300 overflow-hidden hover:translate-y-[-4px] hover:shadow-[0_8px_24px_rgba(255,140,66,0.15)] stat-card class-card border-l-4 border-l-[#FF8C42]">
<div class="w-[52px] h-[52px] rounded-[14px] flex items-center justify-center text-2xl flex-shrink-0 bg-[linear-gradient(135deg,#FFE8D6_0%,#FFF0E6_100%)] text-[#FF8C42] stat-icon-wrapper orange"><HomeOutlined /></div>
<div class="relative z-1 stat-info">
<div class="text-[32px] font-700 text-[#333] leading-tight stat-value">{{ stats.classCount }}</div>
<div class="text-sm text-[#888] mt-1 stat-label">我的班级</div>
</div>
</div>
<div class="stat-card student-card">
<div class="stat-icon-wrapper green">
<TeamOutlined />
</div>
<div class="stat-info">
<div class="stat-value">{{ stats.studentCount }}</div>
<div class="stat-label">我的学生</div>
<div class="relative bg-white rounded-2xl p-5 flex items-center gap-4 shadow-[0_4px_16px_rgba(0,0,0,0.06)] transition-all duration-300 overflow-hidden hover:translate-y-[-4px] hover:shadow-[0_8px_24px_rgba(255,140,66,0.15)] stat-card student-card border-l-4 border-l-[#52c41a]">
<div class="w-[52px] h-[52px] rounded-[14px] flex items-center justify-center text-2xl flex-shrink-0 bg-[linear-gradient(135deg,#D9F7BE_0%,#E8FFD4_100%)] text-[#52c41a] stat-icon-wrapper green"><TeamOutlined /></div>
<div class="relative z-1 stat-info">
<div class="text-[32px] font-700 text-[#333] leading-tight stat-value">{{ stats.studentCount }}</div>
<div class="text-sm text-[#888] mt-1 stat-label">我的学生</div>
</div>
</div>
<div class="stat-card lesson-card">
<div class="stat-icon-wrapper blue">
<ReadOutlined />
</div>
<div class="stat-info">
<div class="stat-value">{{ stats.lessonCount }}</div>
<div class="stat-label">授课次数</div>
<div class="relative bg-white rounded-2xl p-5 flex items-center gap-4 shadow-[0_4px_16px_rgba(0,0,0,0.06)] transition-all duration-300 overflow-hidden hover:translate-y-[-4px] hover:shadow-[0_8px_24px_rgba(255,140,66,0.15)] stat-card lesson-card border-l-4 border-l-[#1890ff]">
<div class="w-[52px] h-[52px] rounded-[14px] flex items-center justify-center text-2xl flex-shrink-0 bg-[linear-gradient(135deg,#D6E4FF_0%,#E8F0FF_100%)] text-[#1890ff] stat-icon-wrapper blue"><ReadOutlined /></div>
<div class="relative z-1 stat-info">
<div class="text-[32px] font-700 text-[#333] leading-tight stat-value">{{ stats.lessonCount }}</div>
<div class="text-sm text-[#888] mt-1 stat-label">授课次数</div>
</div>
</div>
<div class="stat-card course-card">
<div class="stat-icon-wrapper purple">
<AppstoreOutlined />
</div>
<div class="stat-info">
<div class="stat-value">{{ stats.courseCount }}</div>
<div class="stat-label">使用课程</div>
<div class="relative bg-white rounded-2xl p-5 flex items-center gap-4 shadow-[0_4px_16px_rgba(0,0,0,0.06)] transition-all duration-300 overflow-hidden hover:translate-y-[-4px] hover:shadow-[0_8px_24px_rgba(255,140,66,0.15)] stat-card course-card border-l-4 border-l-[#722ed1]">
<div class="w-[52px] h-[52px] rounded-[14px] flex items-center justify-center text-2xl flex-shrink-0 bg-[linear-gradient(135deg,#E8DAFF_0%,#F3EFFF_100%)] text-[#722ed1] stat-icon-wrapper purple"><AppstoreOutlined /></div>
<div class="relative z-1 stat-info">
<div class="text-[32px] font-700 text-[#333] leading-tight stat-value">{{ stats.courseCount }}</div>
<div class="text-sm text-[#888] mt-1 stat-label">使用课程</div>
</div>
</div>
</div>
</div>
<!-- 统计图表 -->
<div class="charts-section">
<div class="content-card trend-card">
<div class="card-header">
<div class="card-title">
<div class="title-icon-wrapper chart">
<LineChartOutlined />
</div>
<div class="grid grid-cols-1 lg:grid-cols-[3fr_2fr] gap-6 mb-6 charts-section">
<div class="bg-white rounded-2xl shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden trend-card content-card">
<div class="flex justify-between items-center py-4 px-5 border-b border-[#f0f0f0] card-header">
<div class="flex items-center gap-3 card-title">
<div class="w-10 h-10 rounded-xl flex items-center justify-center bg-[linear-gradient(135deg,#E8F0FF_0%,#F0F5FF_100%)] text-[#1890ff] title-icon-wrapper chart"><LineChartOutlined /></div>
<span>授课趋势</span>
</div>
</div>
<div class="card-body" :class="{ 'is-loading': trendLoading }">
<div class="p-5 card-body min-h-[280px]" :class="{ 'is-loading': trendLoading }">
<a-spin v-if="trendLoading" />
<div v-else ref="trendChartRef" class="chart-container"></div>
<div v-else ref="trendChartRef" class="w-full h-[280px] chart-container"></div>
</div>
</div>
<div class="content-card usage-card">
<div class="card-header">
<div class="card-title">
<div class="title-icon-wrapper chart">
<PieChartOutlined />
</div>
<div class="bg-white rounded-2xl shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden usage-card content-card">
<div class="flex justify-between items-center py-4 px-5 border-b border-[#f0f0f0] card-header">
<div class="flex items-center gap-3 card-title">
<div class="w-10 h-10 rounded-xl flex items-center justify-center bg-[linear-gradient(135deg,#E8F0FF_0%,#F0F5FF_100%)] text-[#1890ff] title-icon-wrapper chart"><PieChartOutlined /></div>
<span>课程使用</span>
</div>
</div>
<div class="card-body" :class="{ 'is-loading': usageLoading }">
<div class="p-5 card-body min-h-[280px]" :class="{ 'is-loading': usageLoading }">
<a-spin v-if="usageLoading" />
<div v-else ref="usageChartRef" class="chart-container"></div>
<div v-else ref="usageChartRef" class="w-full h-[280px] chart-container"></div>
</div>
</div>
</div>
<!-- 主内容区域 -->
<div class="main-content">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6 main-content">
<!-- 今日课程 -->
<div class="content-card today-lessons-card">
<div class="card-header">
<div class="card-title">
<div class="title-icon-wrapper sun">
<ClockCircleOutlined />
</div>
<div class="bg-white rounded-2xl shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden today-lessons-card content-card">
<div class="flex justify-between items-center py-4 px-5 border-b border-[#f0f0f0] card-header">
<div class="flex items-center gap-3 card-title">
<div class="w-10 h-10 rounded-xl flex items-center justify-center bg-[linear-gradient(135deg,#FFF7E6_0%,#FFFBE6_100%)] text-[#fa8c16] title-icon-wrapper sun"><ClockCircleOutlined /></div>
<span>今日课程</span>
</div>
<a-button type="link" class="view-all-btn" @click="router.push('/teacher/lessons')">
<a-button type="link" class="view-all-btn !p-0" @click="router.push('/teacher/lessons')">
查看全部 <RightOutlined />
</a-button>
</div>
<div class="card-body" :class="{ 'is-loading': loading }">
<div class="p-5 card-body min-h-[200px]" :class="{ 'is-loading': loading }">
<a-spin :spinning="loading">
<div v-if="todayLessons.length === 0" class="empty-state">
<div class="empty-icon-wrapper">
<InboxOutlined />
</div>
<p class="empty-text">今天没有安排课程</p>
<p class="empty-hint">快去课程中心备课吧</p>
<div v-if="todayLessons.length === 0" class="text-center py-12 text-[#999] empty-state">
<div class="w-14 h-14 rounded-2xl bg-[#fafafa] flex items-center justify-center text-3xl text-[#d9d9d9] mx-auto mb-3 empty-icon-wrapper"><InboxOutlined /></div>
<p class="m-0 text-[#666] empty-text">今天没有安排课程</p>
<p class="m-0 mt-1 text-sm text-[#999] empty-hint">快去课程中心备课吧</p>
</div>
<div v-else class="lesson-list">
<div v-else class="flex flex-col gap-3 lesson-list">
<div
v-for="lesson in todayLessons"
:key="lesson.id"
class="lesson-item"
:class="{ 'finished': lesson.status === 'FINISHED' }"
class="flex items-center gap-4 py-3 px-4 rounded-xl border border-[#f0f0f0] transition-colors lesson-item"
:class="lesson.status === 'FINISHED' ? 'bg-[#fafafa]' : 'bg-white hover:border-[#FF8C42]/30'"
>
<div class="lesson-time">
<div class="time-value">{{ formatTime(lesson.plannedDatetime) }}</div>
<div class="time-duration">{{ lesson.duration }}分钟</div>
<div class="flex flex-col items-center min-w-[60px] py-1 px-2 rounded-lg bg-[#f5f5f5] text-[#666] lesson-time">
<div class="text-sm font-600 text-[#333] time-value">{{ formatTime(lesson.plannedDatetime) }}</div>
<div class="text-xs time-duration">{{ lesson.duration }}分钟</div>
</div>
<div class="lesson-info">
<div class="lesson-name">{{ lesson.courseName }}</div>
<div class="lesson-class">
<div class="flex-1 min-w-0 lesson-info">
<div class="font-500 text-[#333] lesson-name">{{ lesson.courseName }}</div>
<div class="flex items-center gap-1.5 text-sm text-[#999] mt-0.5 lesson-class">
<EnvironmentOutlined />
<span>{{ lesson.className }}</span>
</div>
</div>
<div class="lesson-action">
<div class="flex-shrink-0 lesson-action">
<button
v-if="lesson.status === 'FINISHED'"
class="action-btn finished"
class="py-1.5 px-3 rounded-lg text-sm cursor-not-allowed bg-[#f5f5f5] text-[#999] border-0 action-btn finished"
disabled
>
<CheckOutlined /> 已结束
</button>
<button
v-else
class="action-btn start"
class="py-1.5 px-3 rounded-lg text-sm font-500 text-white bg-[#FF8C42] border-0 cursor-pointer hover:opacity-90 action-btn start"
@click="startLesson(lesson)"
>
<PlayCircleOutlined /> 开始上课
@ -164,53 +144,39 @@
</div>
<!-- 推荐课程 -->
<div class="content-card recommend-card">
<div class="card-header">
<div class="card-title">
<div class="title-icon-wrapper bulb">
<BulbOutlined />
</div>
<div class="bg-white rounded-2xl shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden recommend-card content-card">
<div class="flex justify-between items-center py-4 px-5 border-b border-[#f0f0f0] card-header">
<div class="flex items-center gap-3 card-title">
<div class="w-10 h-10 rounded-xl flex items-center justify-center bg-[linear-gradient(135deg,#E6FFFB_0%,#F0FFFC_100%)] text-[#13c2c2] title-icon-wrapper bulb"><BulbOutlined /></div>
<span>推荐课程</span>
</div>
<a-button type="link" class="view-all-btn" @click="router.push('/teacher/courses')">
<a-button type="link" class="view-all-btn !p-0" @click="router.push('/teacher/courses')">
查看全部 <RightOutlined />
</a-button>
</div>
<div class="card-body" :class="{ 'is-loading': loading }">
<div class="p-5 card-body min-h-[200px]" :class="{ 'is-loading': loading }">
<a-spin :spinning="loading">
<div v-if="recommendedCourses.length === 0" class="empty-state">
<div class="empty-icon-wrapper">
<SearchOutlined />
</div>
<p class="empty-text">暂无推荐课程</p>
<div v-if="recommendedCourses.length === 0" class="text-center py-12 text-[#999] empty-state">
<div class="w-14 h-14 rounded-2xl bg-[#fafafa] flex items-center justify-center text-3xl text-[#d9d9d9] mx-auto mb-3 empty-icon-wrapper"><SearchOutlined /></div>
<p class="m-0 text-[#666] empty-text">暂无推荐课程</p>
</div>
<div v-else class="recommend-list">
<div v-else class="grid grid-cols-1 gap-3 recommend-list">
<div
v-for="course in recommendedCourses"
:key="course.id"
class="recommend-item"
class="flex gap-3 p-3 rounded-xl border border-[#f0f0f0] cursor-pointer transition-colors hover:border-[#FF8C42]/40 hover:bg-[#FFF8F0]/50 recommend-item"
@click="viewCourse(course)"
>
<div class="recommend-cover">
<img
v-if="course.coverImagePath"
:src="getImageUrl(course.coverImagePath)"
class="cover-img"
/>
<div v-else class="cover-placeholder">
<BookFilled />
</div>
<div class="duration-tag">{{ course.duration }}分钟</div>
<div class="relative w-20 h-20 flex-shrink-0 rounded-lg overflow-hidden bg-[#f5f5f5] recommend-cover">
<img v-if="course.coverImagePath" :src="getImageUrl(course.coverImagePath)" class="w-full h-full object-cover cover-img" />
<div v-else class="w-full h-full flex items-center justify-center text-2xl text-[#d9d9d9] cover-placeholder"><BookFilled /></div>
<div class="absolute bottom-0 right-0 py-0.5 px-1.5 text-[10px] bg-black/50 text-white rounded-tl duration-tag">{{ course.duration }}分钟</div>
</div>
<div class="recommend-info">
<div class="recommend-name">{{ course.name }}</div>
<div class="recommend-meta">
<span class="meta-item">
<FireOutlined /> {{ course.usageCount }}次使用
</span>
<span v-if="course.avgRating > 0" class="meta-item">
<StarFilled class="star-icon" /> {{ course.avgRating.toFixed(1) }}
</span>
<div class="flex-1 min-w-0 recommend-info">
<div class="font-500 text-[#333] truncate recommend-name">{{ course.name }}</div>
<div class="flex flex-wrap gap-3 text-xs text-[#999] mt-1 recommend-meta">
<span class="meta-item flex items-center gap-1"><FireOutlined /> {{ course.usageCount }}次使用</span>
<span v-if="course.avgRating > 0" class="meta-item flex items-center gap-1"><StarFilled class="star-icon text-[#faad14]" /> {{ course.avgRating.toFixed(1) }}</span>
</div>
</div>
</div>
@ -222,36 +188,32 @@
<!-- 近期活动 -->
<div class="activity-section">
<div class="content-card activity-card">
<div class="card-header">
<div class="card-title">
<div class="title-icon-wrapper list">
<UnorderedListOutlined />
</div>
<div class="bg-white rounded-2xl shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden activity-card content-card">
<div class="flex justify-between items-center py-4 px-5 border-b border-[#f0f0f0] card-header">
<div class="flex items-center gap-3 card-title">
<div class="w-10 h-10 rounded-xl flex items-center justify-center bg-[linear-gradient(135deg,#F9F0FF_0%,#FDF0FF_100%)] text-[#722ed1] title-icon-wrapper list"><UnorderedListOutlined /></div>
<span>近期活动</span>
</div>
</div>
<div class="card-body" :class="{ 'is-loading': loading }">
<div class="p-5 card-body min-h-[120px]" :class="{ 'is-loading': loading }">
<a-spin :spinning="loading">
<div v-if="recentActivities.length === 0" class="empty-state-horizontal">
<div class="empty-icon-wrapper small">
<FileTextOutlined />
</div>
<p class="empty-text">暂无近期活动</p>
<div v-if="recentActivities.length === 0" class="flex items-center gap-3 py-6 text-[#999] empty-state-horizontal">
<div class="w-10 h-10 rounded-xl flex items-center justify-center bg-[#fafafa] text-xl text-[#d9d9d9] empty-icon-wrapper small"><FileTextOutlined /></div>
<p class="m-0 empty-text">暂无近期活动</p>
</div>
<div v-else class="activity-timeline">
<div v-else class="flex flex-col gap-0 activity-timeline">
<div
v-for="(item, index) in recentActivities"
:key="item.id"
class="activity-item"
class="flex gap-3 py-3 border-b border-[#f5f5f5] last:border-b-0 activity-item"
:class="'type-' + item.type"
>
<div class="activity-dot">
<div class="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 text-sm bg-[#f5f5f5] text-[#999] activity-dot">
<component :is="getActivityIcon(item.type)" />
</div>
<div class="activity-content">
<div class="activity-text">{{ item.description }}</div>
<div class="activity-time">{{ item.time }}</div>
<div class="flex-1 min-w-0 activity-content">
<div class="text-sm text-[#333] activity-text">{{ item.description }}</div>
<div class="text-xs text-[#999] mt-0.5 activity-time">{{ item.time }}</div>
</div>
</div>
</div>
@ -676,581 +638,15 @@ onUnmounted(() => {
</script>
<style scoped>
/* 整体布局 */
.teacher-dashboard {
min-height: calc(100vh - 120px);
background: linear-gradient(180deg, #FFF8F0 0%, #FFFFFF 100%);
padding: 24px;
border-radius: 16px;
}
/* 欢迎横幅 */
.welcome-banner {
position: relative;
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
border-radius: 20px;
padding: 28px 32px;
margin-bottom: 24px;
overflow: hidden;
}
.banner-decoration {
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.deco-circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.12);
}
.deco-circle.c1 {
width: 140px;
height: 140px;
top: -40px;
right: 60px;
}
.deco-circle.c2 {
width: 80px;
height: 80px;
top: 50px;
right: 150px;
}
.deco-circle.c3 {
width: 50px;
height: 50px;
bottom: -15px;
right: 100px;
}
.deco-shape {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.2);
color: white;
}
.deco-shape.star {
top: 20px;
right: 30px;
font-size: 16px;
}
.deco-shape.book {
bottom: 15px;
right: 200px;
font-size: 18px;
}
.banner-content {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
z-index: 1;
}
.welcome-title {
font-size: 26px;
font-weight: 700;
color: #fff;
margin: 0 0 6px;
}
.welcome-subtitle {
font-size: 15px;
color: rgba(255, 255, 255, 0.9);
margin: 0;
}
.welcome-date {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.2);
padding: 10px 20px;
border-radius: 24px;
}
.date-icon {
font-size: 16px;
color: white;
}
.date-text {
font-size: 15px;
color: #fff;
font-weight: 500;
}
/* 统计卡片 */
.stats-section {
margin-bottom: 24px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.stat-card {
position: relative;
background: #fff;
border-radius: 16px;
padding: 20px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
overflow: hidden;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(255, 140, 66, 0.15);
}
.stat-icon-wrapper {
width: 52px;
height: 52px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
flex-shrink: 0;
}
.stat-icon-wrapper.orange {
background: linear-gradient(135deg, #FFE8D6 0%, #FFF0E6 100%);
.view-all-btn :deep(.ant-btn) {
color: #FF8C42;
}
.stat-icon-wrapper.green {
background: linear-gradient(135deg, #D9F7BE 0%, #E8FFD4 100%);
color: #52c41a;
}
.stat-icon-wrapper.blue {
background: linear-gradient(135deg, #D6E4FF 0%, #E8F0FF 100%);
color: #1890ff;
}
.stat-icon-wrapper.purple {
background: linear-gradient(135deg, #E8DAFF 0%, #F3EFFF 100%);
color: #722ed1;
}
.stat-info {
position: relative;
z-index: 1;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #333;
line-height: 1.2;
}
.stat-label {
font-size: 14px;
color: #888;
margin-top: 4px;
}
.stat-card.class-card { border-left: 4px solid #FF8C42; }
.stat-card.student-card { border-left: 4px solid #52c41a; }
.stat-card.lesson-card { border-left: 4px solid #1890ff; }
.stat-card.course-card { border-left: 4px solid #722ed1; }
/* 图表区域 */
.charts-section {
display: grid;
grid-template-columns: 3fr 2fr;
gap: 24px;
margin-bottom: 24px;
}
.trend-card {
grid-column: 1;
}
.usage-card {
grid-column: 2;
}
.chart-container {
width: 100%;
height: 280px;
}
.title-icon-wrapper.chart {
background: linear-gradient(135deg, #E8F0FF 0%, #F0F5FF 100%);
color: #1890ff;
}
/* 主内容区域 */
.main-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-bottom: 24px;
}
/* 内容卡片 */
.content-card {
background: #fff;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
overflow: hidden;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #F5F5F5;
}
.card-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 17px;
font-weight: 600;
color: #333;
}
.title-icon-wrapper {
width: 32px;
height: 32px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.title-icon-wrapper.sun {
background: linear-gradient(135deg, #FFF3CD 0%, #FFFBEB 100%);
color: #FF8C42;
}
.title-icon-wrapper.bulb {
background: linear-gradient(135deg, #FFF3CD 0%, #FFFBEB 100%);
color: #FFB347;
}
.title-icon-wrapper.list {
background: linear-gradient(135deg, #E8F0FF 0%, #F0F5FF 100%);
color: #1890ff;
}
.view-all-btn {
font-size: 13px;
color: #FF8C42;
padding: 0;
display: flex;
align-items: center;
gap: 4px;
}
.view-all-btn:hover {
.view-all-btn :deep(.ant-btn:hover) {
color: #E67635;
}
.card-body {
padding: 16px;
min-height: 200px;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 40px 20px;
}
.empty-state-horizontal {
text-align: center;
padding: 30px 20px;
}
.empty-icon-wrapper {
width: 72px;
height: 72px;
margin: 0 auto 16px;
border-radius: 50%;
background: linear-gradient(135deg, #F5F5F5 0%, #FAFAFA 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
color: #BFBFBF;
}
.empty-icon-wrapper.small {
width: 56px;
height: 56px;
font-size: 22px;
}
.empty-text {
font-size: 15px;
color: #666;
margin: 0 0 4px;
}
.empty-hint {
font-size: 13px;
color: #999;
margin: 0;
}
/* 今日课程列表 */
.lesson-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.lesson-item {
display: flex;
align-items: center;
gap: 16px;
padding: 14px 16px;
background: linear-gradient(135deg, #FFF8F0 0%, #FFFBF5 100%);
border-radius: 12px;
transition: all 0.3s ease;
border: 1px solid transparent;
}
.lesson-item:hover {
border-color: #FFD4B8;
transform: translateX(4px);
}
.lesson-item.finished {
opacity: 0.6;
background: #F5F5F5;
}
.lesson-time {
text-align: center;
min-width: 60px;
}
.time-value {
font-size: 18px;
font-weight: 600;
color: #FF8C42;
}
.time-duration {
font-size: 12px;
color: #999;
margin-top: 2px;
}
.lesson-info {
flex: 1;
min-width: 0;
}
.lesson-name {
font-size: 15px;
font-weight: 500;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.lesson-class {
font-size: 13px;
color: #888;
margin-top: 4px;
display: flex;
align-items: center;
gap: 4px;
}
.lesson-action {
flex-shrink: 0;
}
.action-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border: none;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.action-btn.start {
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
color: #fff;
}
.action-btn.start:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(255, 140, 66, 0.3);
}
.action-btn.finished {
background: #E8E8E8;
color: #999;
cursor: not-allowed;
}
/* 推荐课程列表 */
.recommend-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.recommend-item {
display: flex;
gap: 14px;
padding: 12px;
background: #FAFAFA;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid transparent;
}
.recommend-item:hover {
background: #FFF8F0;
border-color: #FFD4B8;
}
.recommend-cover {
position: relative;
width: 80px;
height: 60px;
border-radius: 8px;
overflow: hidden;
flex-shrink: 0;
}
.cover-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.cover-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #FFE4C9 0%, #FFF0E0 100%);
font-size: 24px;
color: #FF8C42;
}
.duration-tag {
position: absolute;
bottom: 4px;
right: 4px;
background: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
}
.recommend-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
}
.recommend-name {
font-size: 14px;
font-weight: 500;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.recommend-meta {
display: flex;
gap: 12px;
margin-top: 6px;
}
.meta-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #888;
}
.star-icon {
color: #FFB347;
font-size: 11px;
}
/* 活动区域 */
.activity-section {
margin-bottom: 24px;
}
.activity-card .card-body {
min-height: auto;
}
.activity-timeline {
display: flex;
flex-direction: column;
gap: 16px;
}
.activity-item {
display: flex;
gap: 16px;
align-items: flex-start;
}
.activity-dot {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 14px;
}
/* 活动项按类型的圆点颜色(动态 type-* class */
.activity-item.type-lesson .activity-dot {
background: linear-gradient(135deg, #FFE4C9 0%, #FFF0E0 100%);
color: #FF8C42;
@ -1271,61 +667,13 @@ onUnmounted(() => {
color: #FA8C16;
}
.activity-content {
flex: 1;
padding-top: 6px;
}
.activity-text {
font-size: 14px;
color: #333;
line-height: 1.5;
}
.activity-time {
font-size: 12px;
color: #999;
margin-top: 4px;
}
/* 响应式 */
@media (max-width: 1200px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.charts-section {
grid-template-columns: 1fr;
}
}
@media (max-width: 992px) {
.main-content {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.teacher-dashboard {
padding: 16px;
}
.stats-grid {
grid-template-columns: 1fr;
}
.charts-section {
grid-template-columns: 1fr;
}
.teacher-dashboard { padding: 16px; }
.banner-content {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.welcome-date {
align-self: flex-start;
}
.welcome-date { align-self: flex-start; }
}
</style>

View File

@ -1,29 +1,29 @@
<template>
<a-layout class="teacher-layout">
<a-layout class="teacher-layout h-screen min-h-screen bg-[#FAFAFA] flex overflow-hidden">
<a-layout-sider
v-if="!isMobile"
v-model:collapsed="collapsed"
:trigger="null"
collapsible
class="teacher-sider"
class="teacher-sider bg-white! shadow-[2px_0_8px_rgba(0,0,0,0.06)] border-r border-[#E8E8E8] flex flex-col overflow-hidden"
>
<div class="sider-logo">
<img src="/logo.png" alt="Logo" class="logo-img" />
<div v-if="!collapsed" class="logo-text">
<span class="logo-tenant">{{ tenantName }}</span>
<span class="logo-title">少儿智慧阅读</span>
<span class="logo-subtitle">服务平台</span>
<div class="shrink-0 h-20 flex items-center justify-center py-3 px-4 border-b border-[#E8E8E8] bg-gradient-to-br from-[#FFF4EC] to-white">
<img src="/logo.png" alt="Logo" class="w-11 h-11 object-contain shrink-0" />
<div v-if="!collapsed" class="flex flex-col ml-3 leading-snug max-w-[140px]">
<span class="text-[13px] font-semibold text-[#FF8C42] whitespace-nowrap overflow-hidden text-ellipsis">{{ tenantName }}</span>
<span class="text-sm font-semibold text-[#333] whitespace-nowrap">少儿智慧阅读</span>
<span class="text-[11px] text-[#666] whitespace-nowrap">服务平台</span>
</div>
</div>
<div class="sider-menu-wrap">
<div class="sider-menu-wrap flex-1 min-h-0 overflow-y-auto">
<a-menu
v-model:selectedKeys="selectedKeys"
mode="inline"
theme="light"
:inline-collapsed="collapsed"
@click="handleMenuClick"
class="side-menu"
class="side-menu border-r-0! py-2 px-3"
>
<a-menu-item key="dashboard">
<template #icon><HomeOutlined /></template>
@ -75,9 +75,9 @@
:body-style="{ padding: 0 }"
>
<template #title>
<div class="drawer-header">
<img src="/logo.png" alt="Logo" class="drawer-logo" />
<span class="drawer-title">服务平台</span>
<div class="flex items-center gap-3">
<img src="/logo.png" alt="Logo" class="w-9 h-9 object-contain" />
<span class="text-base font-semibold text-[#333]">服务平台</span>
</div>
</template>
<a-menu
@ -85,7 +85,7 @@
mode="inline"
theme="light"
@click="handleDrawerMenuClick"
class="drawer-menu"
class="drawer-menu border-r-0! py-2"
>
<a-menu-item key="dashboard"><template #icon><HomeOutlined /></template><span>首页</span></a-menu-item>
<a-menu-item key="classes"><template #icon><TeamOutlined /></template><span>我的班级</span></a-menu-item>
@ -99,17 +99,17 @@
</a-menu>
</a-drawer>
<a-layout class="teacher-layout-right">
<a-layout-header v-if="!isMobile" class="teacher-header">
<a-layout class="flex-1 min-h-0 flex flex-col overflow-hidden">
<a-layout-header v-if="!isMobile" class="shrink-0 sticky top-0 z-100 bg-white px-6 flex justify-between items-center shadow-sm border-b border-[#E8E8E8]">
<div class="header-left">
<MenuUnfoldOutlined
v-if="collapsed"
class="trigger"
class="text-lg cursor-pointer transition-colors text-[#666] hover:text-[#FF8C42]"
@click="collapsed = !collapsed"
/>
<MenuFoldOutlined
v-else
class="trigger"
class="text-lg cursor-pointer transition-colors text-[#666] hover:text-[#FF8C42]"
@click="collapsed = !collapsed"
/>
</div>
@ -119,11 +119,11 @@
<NotificationBell />
<a-dropdown>
<a-space class="user-info" style="cursor: pointer;">
<a-avatar :size="32" class="user-avatar">
<a-space class="px-3 cursor-pointer">
<a-avatar :size="32" class="bg-gradient-to-br from-[#FF8C42] to-[#E67635]">
<template #icon><UserOutlined /></template>
</a-avatar>
<span class="user-name">{{ userName }}</span>
<span class="text-[#333] font-medium">{{ userName }}</span>
<DownOutlined />
</a-space>
<template #overlay>
@ -144,12 +144,12 @@
</div>
</a-layout-header>
<a-layout-header v-if="isMobile" class="teacher-mobile-header">
<MenuOutlined class="menu-trigger" @click="drawerVisible = true" />
<span class="mobile-title">少儿智慧阅读</span>
<a-layout-header v-if="isMobile" class="shrink-0 sticky top-0 z-100 bg-white px-4 h-14 flex items-center justify-between shadow-sm border-b border-[#E8E8E8]">
<MenuOutlined class="text-[22px] text-[#666] p-2 cursor-pointer" @click="drawerVisible = true" />
<span class="text-[17px] font-semibold text-[#333]">少儿智慧阅读</span>
<a-dropdown>
<a-space class="user-info" style="cursor: pointer;">
<a-avatar :size="32" class="user-avatar">
<a-space class="cursor-pointer">
<a-avatar :size="32" class="bg-gradient-to-br from-[#FF8C42] to-[#E67635]">
<template #icon><UserOutlined /></template>
</a-avatar>
</a-space>
@ -163,7 +163,7 @@
</a-dropdown>
</a-layout-header>
<a-layout-content :class="['teacher-content', { 'teacher-content-mobile': isMobile }]">
<a-layout-content :class="['flex-1 min-h-0 bg-white rounded-xl shadow-sm overflow-y-auto overflow-x-hidden', isMobile ? 'm-3 p-4' : 'm-5 p-6']">
<router-view />
</a-layout-content>
</a-layout>
@ -263,247 +263,81 @@ const handleUserMenuClick = ({ key }: { key: string | number }) => {
</script>
<style scoped lang="scss">
//
$primary-color: #FF8C42; //
$primary-light: #FFF4EC;
$primary-dark: #E67635;
$accent-color: #4CAF50; // 绿
$text-color: #333333;
$text-secondary: #666666;
$border-color: #E8E8E8;
$bg-light: #FAFAFA;
.teacher-layout {
height: 100vh;
min-height: 100vh;
background: $bg-light;
display: flex;
overflow: hidden;
}
.teacher-layout-right {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.teacher-sider {
background: white !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.06);
border-right: 1px solid $border-color;
display: flex;
flex-direction: column;
overflow: hidden;
:deep(.ant-layout-sider-children) {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
}
.sider-logo {
flex-shrink: 0;
height: 80px;
.sider-menu-wrap {
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.18);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.28);
}
}
.side-menu {
:deep(.ant-menu-item) {
margin: 4px 0;
padding-left: 12px !important;
padding-right: 12px !important;
border-radius: 8px;
height: 44px;
line-height: 44px;
color: #333333;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
padding: 12px 16px;
border-bottom: 1px solid $border-color;
background: linear-gradient(135deg, $primary-light 0%, white 100%);
.logo-img {
width: 44px;
height: 44px;
object-fit: contain;
flex-shrink: 0;
}
.logo-text {
display: flex;
flex-direction: column;
margin-left: 12px;
line-height: 1.4;
max-width: 140px;
.logo-tenant {
font-size: 13px;
font-weight: 600;
color: $primary-color;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.logo-title {
font-size: 14px;
font-weight: 600;
color: $text-color;
white-space: nowrap;
}
.logo-subtitle {
font-size: 11px;
color: $text-secondary;
white-space: nowrap;
}
}
}
.sider-menu-wrap {
flex: 1;
min-height: 0;
overflow-y: auto;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.18);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.28);
}
}
.side-menu {
border-right: none !important;
padding: 8px 12px;
:deep(.ant-menu-item) {
margin: 4px 0;
padding-left: 12px !important;
padding-right: 12px !important;
border-radius: 8px;
height: 44px;
line-height: 44px;
color: $text-color;
transition: all 0.3s;
.ant-menu-title-content {
display: flex;
align-items: center;
.ant-menu-title-content {
display: flex;
align-items: center;
}
&:hover {
background: $primary-light;
color: $primary-color;
}
&.ant-menu-item-selected {
background: linear-gradient(135deg, $primary-color 0%, $primary-dark 100%);
color: white;
&::after {
display: none;
}
.anticon {
color: white;
}
}
}
}
}
.teacher-header {
flex-shrink: 0;
position: sticky;
top: 0;
z-index: 100;
background: white;
padding: 0 24px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
border-bottom: 1px solid $border-color;
.trigger {
font-size: 18px;
cursor: pointer;
transition: color 0.3s;
color: $text-secondary;
&:hover {
color: $primary-color;
background: #FFF4EC;
color: #FF8C42;
}
&.ant-menu-item-selected {
background: linear-gradient(135deg, #FF8C42 0%, #E67635 100%);
color: white;
&::after {
display: none;
}
.anticon {
color: white;
}
}
}
.user-info {
padding: 0 12px;
}
.user-avatar {
background: linear-gradient(135deg, $primary-color 0%, $primary-dark 100%);
}
.user-name {
color: $text-color;
font-weight: 500;
}
}
.teacher-content {
flex: 1;
min-height: 0;
margin: 20px;
padding: 24px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
overflow-y: auto;
overflow-x: hidden;
}
.teacher-content-mobile {
margin: 12px;
padding: 16px;
border-radius: 12px;
}
.teacher-drawer {
.drawer-header {
display: flex;
align-items: center;
gap: 12px;
.drawer-logo { width: 36px; height: 36px; object-fit: contain; }
.drawer-title { font-size: 16px; font-weight: 600; color: $text-color; }
.drawer-menu {
:deep(.ant-menu-item) {
margin: 4px 8px;
border-radius: 8px;
height: 48px;
line-height: 48px;
}
.drawer-menu {
border-right: none !important;
padding: 8px 0;
:deep(.ant-menu-item) { margin: 4px 8px; border-radius: 8px; height: 48px; line-height: 48px; }
}
}
.teacher-mobile-header {
flex-shrink: 0;
position: sticky;
top: 0;
z-index: 100;
background: white;
padding: 0 16px;
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
border-bottom: 1px solid $border-color;
.menu-trigger { font-size: 22px; color: $text-secondary; padding: 8px; cursor: pointer; }
.mobile-title { font-size: 17px; font-weight: 600; color: $text-color; }
.user-avatar { background: linear-gradient(135deg, $primary-color 0%, $primary-dark 100%); }
}
@media (max-width: 768px) {
.teacher-layout :deep(.ant-layout-sider) { display: none; }
.teacher-layout :deep(.ant-layout-sider) {
display: none;
}
}
</style>

View File

@ -1,130 +1,118 @@
<template>
<div class="class-list-view">
<div class="min-h-[calc(100vh-120px)] bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)] p-6 rounded-2xl">
<!-- 页面标题区域 -->
<div class="page-header-wrapper">
<div class="page-header-decoration">
<div class="decoration-circle c1"></div>
<div class="decoration-circle c2"></div>
<div class="decoration-circle c3"></div>
<div class="relative bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)] rounded-2xl py-6 px-8 mb-6 overflow-hidden flex justify-between items-center">
<div class="absolute top-0 right-0 w-[200px] h-full pointer-events-none">
<div class="absolute rounded-full opacity-15 bg-white w-[120px] h-[120px] -top-[30px] right-5 c1"></div>
<div class="absolute rounded-full opacity-15 bg-white w-20 h-20 top-10 right-20 c2"></div>
<div class="absolute rounded-full opacity-15 bg-white w-10 h-10 bottom-2.5 right-10 c3"></div>
</div>
<div class="page-header-content">
<div class="icon-wrapper orange">
<HomeOutlined />
</div>
<div class="header-text">
<h1 class="page-title">我的班级</h1>
<p class="page-subtitle">管理您的班级开启精彩的阅读课堂</p>
<div class="flex items-center relative z-1">
<div class="w-14 h-14 rounded-[14px] flex items-center justify-center text-[26px] mr-4 flex-shrink-0 bg-white/25 text-white icon-wrapper orange"><HomeOutlined /></div>
<div class="text-white header-text">
<h1 class="text-[28px] font-700 m-0 text-white page-title">我的班级</h1>
<p class="text-sm mt-1 mb-0 opacity-90 text-white page-subtitle">管理您的班级开启精彩的阅读课堂</p>
</div>
</div>
<div class="header-stats">
<div class="stats-item">
<span class="stats-value">{{ classes.length }}</span>
<span class="stats-label">个班级</span>
<div class="flex items-center gap-5 relative z-1">
<div class="text-center text-white stats-item">
<span class="block text-[32px] font-700 leading-tight stats-value">{{ classes.length }}</span>
<span class="text-[13px] opacity-85 stats-label">个班级</span>
</div>
<div class="stats-divider"></div>
<div class="stats-item">
<span class="stats-value">{{ totalStudents }}</span>
<span class="stats-label">名学生</span>
<div class="w-px h-10 bg-white/30 stats-divider"></div>
<div class="text-center text-white stats-item">
<span class="block text-[32px] font-700 leading-tight stats-value">{{ totalStudents }}</span>
<span class="text-[13px] opacity-85 stats-label">名学生</span>
</div>
</div>
</div>
<!-- 班级列表 -->
<a-spin :spinning="loading">
<div class="class-grid" v-if="classes.length > 0">
<!-- 班级卡片 -->
<div
v-for="cls in classes"
:key="cls.id"
class="class-card"
:class="'grade-' + getGradeKey(cls.grade)"
@click="viewClassDetail(cls)"
>
<div class="grid gap-6 grid-cols-1 md:grid-cols-[repeat(auto-fill,minmax(300px,1fr))]" v-if="classes.length > 0">
<div
v-for="cls in classes"
:key="cls.id"
class="bg-white rounded-[20px] p-6 cursor-pointer transition-all duration-300 shadow-[0_4px_16px_rgba(0,0,0,0.06)] relative overflow-hidden border-2 border-transparent hover:translate-y-[-6px] hover:shadow-[0_12px_32px_rgba(255,140,66,0.15)] hover:border-[#FFD4B8] class-card"
:class="'grade-' + getGradeKey(cls.grade)"
@click="viewClassDetail(cls)"
>
<!-- 年级标签 -->
<div class="grade-badge" :class="'badge-' + getGradeKey(cls.grade)">
<span class="badge-icon">
<TagsOutlined />
</span>
<div class="grade-badge absolute top-4 right-4 flex items-center gap-1 py-1 px-3 rounded-[20px] text-xs font-500" :class="'badge-' + getGradeKey(cls.grade)">
<span class="text-sm badge-icon"><TagsOutlined /></span>
<span class="badge-text">{{ cls.grade }}</span>
</div>
<!-- 角色标签 -->
<div class="role-badge" :class="'role-' + cls.myRole?.toLowerCase()">
<div class="role-badge absolute top-4 left-4 flex items-center gap-1 py-1 px-2.5 rounded-lg text-[11px] font-500" :class="'role-' + cls.myRole?.toLowerCase()">
{{ getRoleLabel(cls.myRole) }}
<span v-if="cls.isPrimary" class="primary-mark">班主任</span>
</div>
<!-- 班级信息 -->
<div class="class-header">
<div class="class-avatar" :style="{ background: getGradeGradient(cls.grade) }">
<span class="avatar-text">{{ getGradeInitial(cls.grade) }}</span>
<div class="flex items-center gap-3.5 mb-5 class-header">
<div class="class-avatar w-14 h-14 rounded-2xl flex items-center justify-center shadow-[0_4px_12px_rgba(0,0,0,0.1)]" :style="{ background: getGradeGradient(cls.grade) }">
<span class="text-2xl font-700 text-white avatar-text">{{ getGradeInitial(cls.grade) }}</span>
</div>
<div class="class-name">{{ cls.name }}</div>
<div class="text-xl font-600 text-[#333] class-name">{{ cls.name }}</div>
</div>
<!-- 统计信息 -->
<div class="class-stats">
<div class="stat-item">
<div class="stat-icon-wrapper pink">
<TeamOutlined />
</div>
<div class="stat-content">
<div class="stat-number">{{ cls.studentCount }}</div>
<div class="stat-label">名学生</div>
<div class="flex items-center py-4 px-4 bg-[#FAFAFA] rounded-xl mb-4 class-stats">
<div class="flex-1 flex items-center gap-3 stat-item">
<div class="w-11 h-11 rounded-xl flex items-center justify-center text-xl flex-shrink-0 bg-[linear-gradient(135deg,#FFE4E1_0%,#FFF0EE_100%)] text-[#FF6B6B] stat-icon-wrapper pink"><TeamOutlined /></div>
<div class="stat-content flex-1">
<div class="text-2xl font-700 text-[#333] leading-tight stat-number">{{ cls.studentCount }}</div>
<div class="text-xs text-[#888] mt-0.5 stat-label">名学生</div>
</div>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<div class="stat-icon-wrapper blue">
<ReadOutlined />
</div>
<div class="stat-content">
<div class="stat-number">{{ cls.lessonCount }}</div>
<div class="stat-label">授课次数</div>
<div class="w-px h-10 bg-[#E8E8E8] mx-4 stat-divider"></div>
<div class="flex-1 flex items-center gap-3 stat-item">
<div class="w-11 h-11 rounded-xl flex items-center justify-center text-xl flex-shrink-0 bg-[linear-gradient(135deg,#D6E4FF_0%,#E8F0FF_100%)] text-[#1890ff] stat-icon-wrapper blue"><ReadOutlined /></div>
<div class="stat-content flex-1">
<div class="text-2xl font-700 text-[#333] leading-tight stat-number">{{ cls.lessonCount }}</div>
<div class="text-xs text-[#888] mt-0.5 stat-label">授课次数</div>
</div>
</div>
</div>
<!-- 进度条 -->
<div class="class-progress">
<div class="progress-label">本月授课进度</div>
<div class="progress-bar">
<div class="mb-5 class-progress">
<div class="text-xs text-[#888] mb-2 progress-label">本月授课进度</div>
<div class="h-2 bg-[#F0F0F0] rounded overflow-hidden progress-bar">
<div
class="progress-fill"
class="progress-fill h-full rounded transition-[width_0.3s_ease]"
:style="{
width: Math.min((cls.lessonCount / 20) * 100, 100) + '%',
background: getGradeGradient(cls.grade)
}"
></div>
</div>
<div class="progress-text">{{ Math.min(Math.round((cls.lessonCount / 20) * 100), 100) }}%</div>
<div class="text-xs text-[#FF8C42] font-500 mt-1.5 text-right progress-text">{{ Math.min(Math.round((cls.lessonCount / 20) * 100), 100) }}%</div>
</div>
<!-- 操作按钮 -->
<div class="class-actions">
<button class="action-btn primary" @click.stop="prepareLesson(cls)">
<EditOutlined />
备课
<div class="flex gap-2.5 class-actions">
<button type="button" class="flex-1 flex items-center justify-center gap-1.5 py-2 px-3 rounded-xl text-sm font-500 border-0 cursor-pointer bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)] text-white hover:opacity-90 action-btn primary" @click.stop="prepareLesson(cls)">
<EditOutlined /> 备课
</button>
<button class="action-btn" @click.stop="viewStudents(cls)">
<UsergroupAddOutlined />
学生
<button type="button" class="flex-1 flex items-center justify-center gap-1.5 py-2 px-3 rounded-xl text-sm font-500 border border-[#E8E8E8] cursor-pointer bg-white text-[#666] hover:border-[#FF8C42] hover:text-[#FF8C42] action-btn" @click.stop="viewStudents(cls)">
<UsergroupAddOutlined /> 学生
</button>
<button class="action-btn" @click.stop="viewHistory(cls)">
<BarChartOutlined />
记录
<button type="button" class="flex-1 flex items-center justify-center gap-1.5 py-2 px-3 rounded-xl text-sm font-500 border border-[#E8E8E8] cursor-pointer bg-white text-[#666] hover:border-[#FF8C42] hover:text-[#FF8C42] action-btn" @click.stop="viewHistory(cls)">
<BarChartOutlined /> 记录
</button>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="!loading && classes.length === 0" class="empty-state">
<div class="empty-icon-wrapper">
<div v-if="!loading && classes.length === 0" class="text-center py-20 px-5 bg-white rounded-[20px] empty-state">
<div class="w-20 h-20 rounded-full mx-auto mb-5 flex items-center justify-center text-4xl text-[#FF8C42] bg-[linear-gradient(135deg,#FFE4C9_0%,#FFF0E0_100%)] empty-icon-wrapper">
<HomeOutlined />
</div>
<h3 class="empty-title">暂无分配班级</h3>
<p class="empty-desc">请联系学校管理员为您分配班级</p>
<h3 class="text-xl text-[#333] m-0 mb-2 empty-title">暂无分配班级</h3>
<p class="text-sm text-[#888] m-0 mb-6 empty-desc">请联系学校管理员为您分配班级</p>
</div>
</a-spin>
</div>
@ -243,224 +231,31 @@ const viewHistory = (cls: ClassInfo) => {
</script>
<style scoped>
/* 整体布局 */
.class-list-view {
min-height: calc(100vh - 120px);
background: linear-gradient(180deg, #FFF8F0 0%, #FFFFFF 100%);
padding: 24px;
border-radius: 16px;
}
/* 页面标题区域 */
.page-header-wrapper {
position: relative;
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
border-radius: 16px;
padding: 24px 32px;
margin-bottom: 24px;
overflow: hidden;
display: flex;
justify-content: space-between;
align-items: center;
}
.page-header-decoration {
position: absolute;
top: 0;
right: 0;
width: 200px;
height: 100%;
}
.decoration-circle {
position: absolute;
border-radius: 50%;
opacity: 0.15;
background: #fff;
}
.decoration-circle.c1 {
width: 120px;
height: 120px;
top: -30px;
right: 20px;
}
.decoration-circle.c2 {
width: 80px;
height: 80px;
top: 40px;
right: 80px;
}
.decoration-circle.c3 {
width: 40px;
height: 40px;
bottom: 10px;
right: 40px;
}
.page-header-content {
display: flex;
align-items: center;
position: relative;
z-index: 1;
}
.icon-wrapper {
width: 56px;
height: 56px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 26px;
margin-right: 16px;
flex-shrink: 0;
}
.icon-wrapper.orange {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.15) 100%);
color: #fff;
}
.header-text {
color: #fff;
}
.page-title {
font-size: 28px;
font-weight: 700;
margin: 0;
color: #fff;
}
.page-subtitle {
font-size: 14px;
margin: 4px 0 0;
opacity: 0.9;
color: #fff;
}
.header-stats {
display: flex;
align-items: center;
gap: 20px;
position: relative;
z-index: 1;
}
.stats-item {
text-align: center;
color: #fff;
}
.stats-value {
display: block;
font-size: 32px;
font-weight: 700;
line-height: 1.2;
}
.stats-label {
font-size: 13px;
opacity: 0.85;
}
.stats-divider {
width: 1px;
height: 40px;
background: rgba(255, 255, 255, 0.3);
}
/* 班级网格 */
.class-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 24px;
}
/* 班级卡片 */
.class-card {
background: #fff;
border-radius: 20px;
padding: 24px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
position: relative;
overflow: hidden;
border: 2px solid transparent;
}
.class-card:hover {
transform: translateY(-6px);
box-shadow: 0 12px 32px rgba(255, 140, 66, 0.15);
border-color: #FFD4B8;
}
/* 年级标签 */
.grade-badge {
position: absolute;
top: 16px;
right: 16px;
display: flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
}
/* 年级/角色动态标签颜色 */
.badge-small {
background: linear-gradient(135deg, #F6FFED 0%, #D9F7BE 100%);
color: #52c41a;
}
.badge-middle {
background: linear-gradient(135deg, #E6F7FF 0%, #BAE7FF 100%);
color: #1890ff;
}
.badge-big {
background: linear-gradient(135deg, #FFF7E6 0%, #FFD591 100%);
color: #fa8c16;
}
.badge-icon {
font-size: 14px;
}
/* 角色标签 */
.role-badge {
position: absolute;
top: 16px;
left: 16px;
display: flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: 8px;
font-size: 11px;
font-weight: 500;
}
.role-badge.role-main {
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
color: white;
}
.role-badge.role-assist {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
.role-badge.role-care {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
color: #666;
}
.primary-mark {
margin-left: 4px;
padding: 1px 4px;
@ -469,371 +264,16 @@ const viewHistory = (cls: ClassInfo) => {
font-size: 10px;
}
/* 班级头部 */
.class-header {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 20px;
}
.class-avatar {
width: 56px;
height: 56px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.avatar-text {
font-size: 24px;
font-weight: 700;
color: #fff;
}
.class-name {
font-size: 20px;
font-weight: 600;
color: #333;
}
/* 统计信息 */
.class-stats {
display: flex;
align-items: center;
padding: 16px;
background: #FAFAFA;
border-radius: 12px;
margin-bottom: 16px;
}
.stat-item {
flex: 1;
display: flex;
align-items: center;
gap: 12px;
}
.stat-icon-wrapper {
width: 44px;
height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.stat-icon-wrapper.pink {
background: linear-gradient(135deg, #FFE4E1 0%, #FFF0EE 100%);
color: #FF6B6B;
}
.stat-icon-wrapper.blue {
background: linear-gradient(135deg, #D6E4FF 0%, #E8F0FF 100%);
color: #1890ff;
}
.stat-content {
flex: 1;
}
.stat-number {
font-size: 24px;
font-weight: 700;
color: #333;
line-height: 1.2;
}
.stat-label {
font-size: 12px;
color: #888;
margin-top: 2px;
}
.stat-divider {
width: 1px;
height: 40px;
background: #E8E8E8;
margin: 0 16px;
}
/* 进度条 */
.class-progress {
margin-bottom: 20px;
}
.progress-label {
font-size: 12px;
color: #888;
margin-bottom: 8px;
}
.progress-bar {
height: 8px;
background: #F0F0F0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
}
.progress-text {
font-size: 12px;
color: #FF8C42;
font-weight: 500;
margin-top: 6px;
text-align: right;
}
/* 操作按钮 */
.class-actions {
display: flex;
gap: 10px;
}
.action-btn {
flex: 1;
padding: 10px 12px;
border: 1px solid #E8E8E8;
border-radius: 10px;
background: #fff;
font-size: 13px;
color: #666;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
transition: all 0.3s ease;
}
.action-btn:hover {
border-color: #FFD4B8;
background: #FFF8F0;
color: #FF8C42;
}
.action-btn.primary {
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
border: none;
color: #fff;
}
.action-btn.primary:hover {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(255, 140, 66, 0.3);
}
/* 添加班级卡片 */
.add-card {
display: flex;
align-items: center;
justify-content: center;
min-height: 280px;
border: 2px dashed #FFD4B8;
background: linear-gradient(135deg, #FFF8F0 0%, #FFFBF5 100%);
}
.add-card:hover {
border-style: solid;
background: linear-gradient(135deg, #FFECD9 0%, #FFF5EB 100%);
}
.add-card-content {
text-align: center;
}
.add-icon {
width: 64px;
height: 64px;
margin: 0 auto 16px;
border-radius: 50%;
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
color: #fff;
box-shadow: 0 4px 16px rgba(255, 140, 66, 0.3);
transition: transform 0.3s ease;
}
.add-card:hover .add-icon {
transform: scale(1.1);
}
.add-text {
font-size: 16px;
font-weight: 500;
color: #FF8C42;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 80px 20px;
background: #fff;
border-radius: 20px;
}
.empty-icon-wrapper {
width: 80px;
height: 80px;
margin: 0 auto 20px;
border-radius: 50%;
background: linear-gradient(135deg, #FFE4C9 0%, #FFF0E0 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
color: #FF8C42;
}
.empty-title {
font-size: 20px;
color: #333;
margin: 0 0 8px;
}
.empty-desc {
font-size: 14px;
color: #888;
margin: 0 0 24px;
}
.empty-action {
padding: 12px 32px;
border: none;
border-radius: 24px;
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
color: #fff;
font-size: 15px;
font-weight: 500;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
}
.empty-action:hover {
transform: scale(1.05);
box-shadow: 0 6px 20px rgba(255, 140, 66, 0.3);
}
/* 模态框样式 */
.add-class-modal :deep(.ant-modal-content) {
border-radius: 16px;
}
.add-class-modal :deep(.ant-modal-header) {
border-radius: 16px 16px 0 0;
}
.grade-radio-group {
width: 100%;
display: flex;
gap: 12px;
}
.grade-radio-group :deep(.ant-radio-button-wrapper) {
flex: 1;
height: auto;
padding: 12px 16px;
border-radius: 12px !important;
border: 2px solid #E8E8E8 !important;
text-align: center;
}
.grade-radio-group :deep(.ant-radio-button-wrapper-checked) {
border-color: #FF8C42 !important;
background: #FFF8F0;
}
.grade-radio-group :deep(.ant-radio-button-wrapper::before) {
display: none;
}
.grade-option {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.grade-initial {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 600;
color: #fff;
}
.grade-initial.small {
background: linear-gradient(135deg, #95DE64 0%, #B7EB8F 100%);
}
.grade-initial.middle {
background: linear-gradient(135deg, #40A9FF 0%, #69C0FF 100%);
}
.grade-initial.big {
background: linear-gradient(135deg, #FFA940 0%, #FFC069 100%);
}
.form-actions {
margin-bottom: 0;
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* 响应式 */
@media (max-width: 768px) {
.class-list-view {
padding: 16px;
}
.page-header-wrapper {
flex-direction: column;
text-align: center;
gap: 20px;
}
.page-header-content {
flex-direction: column;
}
.icon-wrapper {
margin-right: 0;
margin-bottom: 12px;
}
.class-grid {
grid-template-columns: 1fr;
}
.class-stats {
flex-direction: column;
gap: 12px;
}
.stat-divider {
width: 100%;
height: 1px;
margin: 0;
}
.page-header-content { flex-direction: column; }
.icon-wrapper { margin-right: 0; margin-bottom: 12px; }
.class-stats { flex-direction: column; gap: 12px; }
.stat-divider { width: 100%; height: 1px; margin: 0; }
}
</style>

View File

@ -1,31 +1,29 @@
<template>
<div class="course-list-view">
<div class="min-h-[calc(100vh-120px)] bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)] p-6 rounded-2xl">
<!-- 页面标题区域 - 带装饰 -->
<div class="page-header-wrapper">
<div class="page-header-decoration">
<div class="decoration-circle c1"></div>
<div class="decoration-circle c2"></div>
<div class="decoration-circle c3"></div>
<div class="relative bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)] rounded-2xl py-6 px-8 mb-6 overflow-hidden">
<div class="absolute top-0 right-0 w-[200px] h-full">
<div class="absolute rounded-full opacity-15 bg-white w-[120px] h-[120px] -top-[30px] right-5 c1"></div>
<div class="absolute rounded-full opacity-15 bg-white w-20 h-20 top-10 right-20 c2"></div>
<div class="absolute rounded-full opacity-15 bg-white w-10 h-10 bottom-2.5 right-10 c3"></div>
</div>
<div class="page-header-content">
<div class="icon-wrapper">
<BookOutlined />
</div>
<div class="header-text">
<h1 class="page-title">课程中心</h1>
<p class="page-subtitle">发现精彩的绘本课程开启美妙的阅读之旅</p>
<div class="flex items-center relative z-1">
<div class="w-14 h-14 rounded-2xl flex items-center justify-center text-[28px] bg-white/20 text-white mr-4 icon-wrapper"><BookOutlined /></div>
<div class="text-white header-text">
<h1 class="text-[28px] font-700 m-0 text-white">课程中心</h1>
<p class="text-sm mt-1 mb-0 opacity-90 text-white">发现精彩的绘本课程开启美妙的阅读之旅</p>
</div>
</div>
</div>
<!-- 筛选栏 -->
<div class="filter-bar">
<div class="filter-item">
<span class="filter-label">年级</span>
<div class="flex items-center flex-wrap gap-4 bg-white py-4 px-5 rounded-xl mb-6 shadow-[0_2px_8px_rgba(0,0,0,0.04)] filter-bar">
<div class="flex items-center gap-2 filter-item">
<span class="text-sm text-[#666] font-500 filter-label">年级</span>
<a-select
v-model:value="filters.grade"
placeholder="全部年级"
style="width: 120px;"
class="w-[120px]"
allowClear
@change="handleFilterChange"
>
@ -41,12 +39,12 @@
<a-select-option value="混合">混合</a-select-option>
</a-select>
</div>
<div class="filter-item">
<span class="filter-label">领域</span>
<div class="flex items-center gap-2 filter-item">
<span class="text-sm text-[#666] font-500 filter-label">领域</span>
<a-select
v-model:value="filters.domain"
placeholder="全部领域"
style="width: 120px;"
class="w-[120px]"
allowClear
@change="handleFilterChange"
>
@ -57,22 +55,22 @@
<a-select-option value="艺术">艺术</a-select-option>
</a-select>
</div>
<div class="filter-item search-box">
<div class="flex-1 max-w-[300px] filter-item search-box">
<a-input-search
v-model:value="filters.keyword"
placeholder="搜索课程名称..."
style="width: 240px;"
class="w-full"
@search="handleFilterChange"
>
<template #prefix>
<SearchOutlined style="color: #FF8C42;" />
<SearchOutlined class="text-[#FF8C42]" />
</template>
</a-input-search>
</div>
<div class="filter-item filter-right">
<div class="filter-item ml-auto">
<a-select
v-model:value="filters.sort"
style="width: 130px;"
class="w-[130px]"
@change="handleFilterChange"
>
<a-select-option value="popular"><FireOutlined /> 最受欢迎</a-select-option>
@ -84,40 +82,39 @@
<!-- 课程列表 -->
<a-spin :spinning="loading">
<div class="course-grid">
<div class="grid gap-6 grid-cols-1 md:grid-cols-[repeat(auto-fill,minmax(280px,1fr))] course-grid">
<div
v-for="course in courses"
:key="course.id"
class="course-card"
class="bg-white rounded-2xl overflow-hidden cursor-pointer transition-all duration-300 shadow-[0_2px_12px_rgba(0,0,0,0.06)] border-2 border-transparent hover:translate-y-[-4px] hover:shadow-[0_8px_24px_rgba(255,140,66,0.15)] hover:border-[#FF8C42] course-card"
@click="viewCourseDetail(course)"
>
<!-- 封面区域 -->
<div class="course-cover">
<div class="relative h-[180px] overflow-hidden course-cover">
<img
v-if="course.pictureUrl"
:src="getImageUrl(course.pictureUrl)"
class="cover-image"
class="w-full h-full object-cover transition-transform duration-300 cover-image"
/>
<div v-else class="cover-placeholder">
<div class="placeholder-icon"><BookFilled /></div>
<div class="placeholder-text">精彩绘本</div>
<div v-else class="w-full h-full flex flex-col items-center justify-center bg-[linear-gradient(135deg,#FFE4C9_0%,#FFF0E0_100%)] cover-placeholder">
<div class="text-[56px] mb-2 flex items-center justify-center text-[#FF8C42] placeholder-icon"><BookFilled /></div>
<div class="text-sm text-[#FF8C42] font-500 placeholder-text">精彩绘本</div>
</div>
<!-- 评分徽章 -->
<div class="rating-badge" v-if="course.avgRating > 0">
<span class="rating-star"><StarFilled /></span>
<span class="rating-value">{{ course.avgRating.toFixed(1) }}</span>
<div v-if="course.avgRating > 0" class="absolute top-3 right-3 bg-white/95 py-1 px-2.5 rounded-[20px] flex items-center gap-1 shadow-[0_2px_8px_rgba(0,0,0,0.1)] rating-badge">
<span class="text-xs text-[#FFB347] flex items-center rating-star"><StarFilled /></span>
<span class="text-[13px] font-600 text-[#FF8C42] rating-value">{{ course.avgRating.toFixed(1) }}</span>
</div>
</div>
<!-- 卡片内容 -->
<div class="course-content">
<h3 class="course-title">{{ course.name }}</h3>
<p class="course-book" v-if="course.pictureBookName">
<div class="p-4 course-content">
<h3 class="text-base font-600 text-[#333] m-0 mb-1.5 leading-snug line-clamp-2 course-title">{{ course.name }}</h3>
<p class="text-[13px] text-[#888] m-0 mb-3 whitespace-nowrap overflow-hidden text-ellipsis flex items-center gap-1.5 course-book" v-if="course.pictureBookName">
<BookOutlined /> {{ course.pictureBookName }}
</p>
<!-- 标签区域 -->
<div class="course-tags">
<div class="flex flex-wrap gap-1.5 mb-3 course-tags">
<a-tag
v-for="tag in course.gradeTags"
:key="'g-' + tag"
@ -135,20 +132,18 @@
</div>
<!-- 底部信息 -->
<div class="course-meta">
<span class="meta-item">
<ClockCircleOutlined />
{{ course.duration }}分钟
<div class="flex gap-4 pt-3 border-t border-dashed border-[#EEE] mb-3 course-meta">
<span class="text-xs text-[#999] flex items-center gap-1 meta-item">
<ClockCircleOutlined /> {{ course.duration }}分钟
</span>
<span class="meta-item">
<TeamOutlined />
{{ course.usageCount }}
<span class="text-xs text-[#999] flex items-center gap-1 meta-item">
<TeamOutlined /> {{ course.usageCount }}
</span>
</div>
<!-- 操作按钮 -->
<button class="prepare-btn" @click.stop="prepareCourse(course)">
<span class="btn-icon"><EditOutlined /></span>
<button type="button" class="w-full py-2.5 px-4 border-0 rounded-[24px] bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)] text-white text-sm font-600 cursor-pointer flex items-center justify-center gap-1.5 transition-all duration-300 hover:opacity-95 prepare-btn" @click.stop="prepareCourse(course)">
<span class="text-base flex items-center btn-icon"><EditOutlined /></span>
开始备课
</button>
</div>
@ -156,16 +151,16 @@
</div>
<!-- 空状态 -->
<div v-if="courses.length === 0 && !loading" class="empty-state">
<div class="icon-wrapper">
<div v-if="courses.length === 0 && !loading" class="text-center py-[60px] px-5 empty-state">
<div class="w-[72px] h-[72px] rounded-full mx-auto mb-4 flex items-center justify-center text-[28px] text-[#BFBFBF] bg-[linear-gradient(135deg,#F5F5F5_0%,#FAFAFA_100%)] icon-wrapper">
<SearchOutlined />
</div>
<p class="empty-text">暂无符合条件的课程</p>
<p class="empty-hint">试试调整筛选条件吧</p>
<p class="text-base text-[#666] m-0 mb-2 empty-text">暂无符合条件的课程</p>
<p class="text-sm text-[#999] m-0 empty-hint">试试调整筛选条件吧</p>
</div>
<!-- 分页 -->
<div class="pagination-wrapper" v-if="pagination.total > pagination.pageSize">
<div class="flex justify-center mt-8 pt-6 border-t border-[#F0F0F0]" v-if="pagination.total > pagination.pageSize">
<a-pagination
v-model:current="pagination.current"
v-model:page-size="pagination.pageSize"
@ -309,380 +304,20 @@ onMounted(() => {
</script>
<style scoped>
/* 页面整体 */
.course-list-view {
min-height: calc(100vh - 120px);
background: linear-gradient(180deg, #FFF8F0 0%, #FFFFFF 100%);
padding: 24px;
border-radius: 16px;
}
/* 页面标题区域 */
.page-header-wrapper {
position: relative;
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
border-radius: 16px;
padding: 24px 32px;
margin-bottom: 24px;
overflow: hidden;
}
.page-header-decoration {
position: absolute;
top: 0;
right: 0;
width: 200px;
height: 100%;
}
.decoration-circle {
position: absolute;
border-radius: 50%;
opacity: 0.15;
}
.decoration-circle.c1 {
width: 120px;
height: 120px;
background: #fff;
top: -30px;
right: 20px;
}
.decoration-circle.c2 {
width: 80px;
height: 80px;
background: #fff;
top: 40px;
right: 80px;
}
.decoration-circle.c3 {
width: 40px;
height: 40px;
background: #fff;
bottom: 10px;
right: 40px;
}
.page-header-content {
display: flex;
align-items: center;
position: relative;
z-index: 1;
}
.header-icon {
font-size: 48px;
margin-right: 16px;
}
.icon-wrapper {
width: 56px;
height: 56px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
background: rgba(255, 255, 255, 0.2);
color: white;
margin-right: 16px;
}
.header-text {
color: #fff;
}
.page-title {
font-size: 28px;
font-weight: 700;
margin: 0;
color: #fff;
}
.page-subtitle {
font-size: 14px;
margin: 4px 0 0;
opacity: 0.9;
color: #fff;
}
/* 筛选栏 */
.filter-bar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 16px;
background: #fff;
padding: 16px 20px;
border-radius: 12px;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.filter-item {
display: flex;
align-items: center;
gap: 8px;
}
.filter-label {
font-size: 14px;
color: #666;
font-weight: 500;
}
.filter-right {
margin-left: auto;
}
.search-box {
flex: 1;
max-width: 300px;
}
/* 课程网格 */
.course-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
}
/* 课程卡片 */
.course-card {
background: #fff;
border-radius: 16px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
border: 2px solid transparent;
}
.course-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(255, 140, 66, 0.15);
border-color: #FF8C42;
}
/* 封面区域 */
.course-cover {
position: relative;
height: 180px;
overflow: hidden;
}
.cover-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.course-card:hover .cover-image {
transform: scale(1.05);
}
.cover-placeholder {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #FFE4C9 0%, #FFF0E0 100%);
}
.placeholder-icon {
font-size: 56px;
margin-bottom: 8px;
color: #FF8C42;
display: flex;
align-items: center;
justify-content: center;
}
.placeholder-text {
font-size: 14px;
color: #FF8C42;
font-weight: 500;
}
/* 评分徽章 */
.rating-badge {
position: absolute;
top: 12px;
right: 12px;
background: rgba(255, 255, 255, 0.95);
padding: 4px 10px;
border-radius: 20px;
display: flex;
align-items: center;
gap: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.rating-star {
font-size: 12px;
color: #FFB347;
display: flex;
align-items: center;
}
.rating-value {
font-size: 13px;
font-weight: 600;
color: #FF8C42;
}
/* 卡片内容 */
.course-content {
padding: 16px;
}
.course-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0 0 6px;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.course-book {
font-size: 13px;
color: #888;
margin: 0 0 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
align-items: center;
gap: 6px;
}
/* 标签 */
.course-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 12px;
}
/* 元信息 */
.course-meta {
display: flex;
gap: 16px;
padding-top: 12px;
border-top: 1px dashed #EEE;
margin-bottom: 12px;
}
.meta-item {
font-size: 12px;
color: #999;
display: flex;
align-items: center;
gap: 4px;
}
/* 备课按钮 */
.prepare-btn {
width: 100%;
padding: 10px 16px;
border: none;
border-radius: 24px;
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
color: #fff;
font-size: 14px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
transition: all 0.3s ease;
}
.prepare-btn:hover {
background: linear-gradient(135deg, #E67635 0%, #FF8C42 100%);
transform: scale(1.02);
}
.btn-icon {
font-size: 16px;
display: flex;
align-items: center;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 60px 20px;
}
.empty-state .icon-wrapper {
width: 72px;
height: 72px;
margin: 0 auto 16px;
border-radius: 50%;
background: linear-gradient(135deg, #F5F5F5 0%, #FAFAFA 100%);
font-size: 28px;
color: #BFBFBF;
}
.empty-icon {
font-size: 64px;
margin-bottom: 16px;
}
.empty-text {
font-size: 16px;
color: #666;
margin: 0 0 8px;
}
.empty-hint {
font-size: 14px;
color: #999;
margin: 0;
}
/* 分页 */
.pagination-wrapper {
display: flex;
justify-content: center;
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid #F0F0F0;
}
/* 响应式 */
@media (max-width: 768px) {
.course-grid {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 16px;
}
.filter-bar {
flex-direction: column;
align-items: stretch;
}
.filter-item {
width: 100%;
}
.filter-right {
margin-left: 0;
}
.search-box {
max-width: none;
}
.filter-item { width: 100%; }
.search-box { max-width: none; }
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="school-course-detail-page">
<div class="p-6">
<a-card :bordered="false" :loading="loading">
<template #title>
<span>校本课程包详情</span>
@ -102,9 +102,3 @@ onMounted(() => {
fetchData();
});
</script>
<style scoped>
.school-course-detail-page {
padding: 24px;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="school-course-edit-page">
<div class="p-6">
<a-card :bordered="false">
<template #title>
<span>{{ isEdit ? '编辑校本课程包' : '创建校本课程包' }}</span>
@ -154,9 +154,3 @@ onMounted(() => {
fetchDetail();
});
</script>
<style scoped>
.school-course-edit-page {
padding: 24px;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="school-course-list-page">
<div class="p-6">
<a-card :bordered="false">
<template #title>
<span>我的校本课程包</span>
@ -19,11 +19,11 @@
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'sourceCourse'">
<div class="source-info">
<div class="flex items-center gap-2">
<img
v-if="record.sourceCourse?.coverImagePath"
:src="record.sourceCourse.coverImagePath"
class="cover"
class="w-10 h-10 object-cover rounded"
/>
<span>{{ record.sourceCourse?.name }}</span>
</div>
@ -109,22 +109,3 @@ onMounted(() => {
fetchData();
});
</script>
<style scoped>
.school-course-list-page {
padding: 24px;
}
.source-info {
display: flex;
align-items: center;
gap: 8px;
}
.cover {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 4px;
}
</style>