kindergarten_java/reading-platform-frontend/src/views/school/courses/CourseDetailView.vue

1205 lines
32 KiB
Vue
Raw Normal View History

<template>
<div class="course-detail-view">
<!-- 顶部课程信息 -->
<div class="course-header">
<div class="header-left">
<a-button type="text" @click="goBackToList" class="back-btn">
<template #icon><LeftOutlined /></template>
返回
</a-button>
<div class="course-info">
<h1 class="course-title">{{ course.name || '课程详情' }}</h1>
<div class="course-meta">
<span class="meta-item">
<BookOutlined /> {{ course.pictureBookName || '关联绘本' }}
</span>
<span class="meta-item">
<ClockCircleOutlined /> {{ course.duration || 25 }} 分钟
</span>
<a-tag v-for="tag in course.gradeTags" :key="tag" :style="getGradeTagStyle(tag)">
{{ tag }}
</a-tag>
</div>
</div>
</div>
<div class="header-right">
<a-tag :color="course.authorized ? 'success' : 'warning'" class="auth-tag">
{{ course.authorized ? '已授权' : '未授权' }}
</a-tag>
</div>
</div>
<a-spin :spinning="loading">
<div class="detail-content">
<a-row :gutter="20">
<!-- 左侧课程信息和教学流程 -->
<a-col :span="16">
<!-- 课程封面和基本信息 -->
<div class="info-card">
<div class="card-header">
<div class="header-icon info-icon"><InfoCircleOutlined /></div>
<span>课程信息</span>
</div>
<div class="card-body">
<div class="cover-section" v-if="course.coverImagePath">
<img :src="getFileUrl(course.coverImagePath)" alt="课程封面" class="cover-image" />
</div>
<div class="info-grid">
<div class="info-item">
<span class="info-label">课程名称</span>
<span class="info-value">{{ course.name }}</span>
</div>
<div class="info-item">
<span class="info-label">绘本名称</span>
<span class="info-value">{{ course.pictureBookName }}</span>
</div>
<div class="info-item">
<span class="info-label">适用年级</span>
<div class="info-tags">
<a-tag v-for="tag in course.gradeTags" :key="tag" :style="getGradeTagStyle(tag)">
{{ tag }}
</a-tag>
</div>
</div>
<div class="info-item">
<span class="info-label">核心发展目标</span>
<div class="info-tags">
<a-tag v-for="tag in course.domainTags" :key="tag" :style="getDomainTagStyle(tag)">
{{ tag }}
</a-tag>
</div>
</div>
<div class="info-item">
<span class="info-label">课时长度</span>
<span class="info-value">{{ course.duration }} 分钟</span>
</div>
<div class="info-item">
<span class="info-label">课程版本</span>
<span class="info-value">{{ course.version }}</span>
</div>
<div class="info-item full-width" v-if="course.description">
<span class="info-label">课程描述</span>
<span class="info-value desc">{{ course.description }}</span>
</div>
</div>
</div>
</div>
<!-- 教学流程 -->
<div class="flow-card">
<div class="card-header">
<div class="header-icon flow-icon"><OrderedListOutlined /></div>
<span>教学流程</span>
<span class="step-count">{{ scripts.length }} 个环节</span>
</div>
<div class="card-body">
<div v-if="scripts && scripts.length > 0" class="flow-timeline">
<div v-for="(script, index) in scripts" :key="script.id" class="flow-item">
<div class="flow-indicator">
<div class="flow-number">{{ index + 1 }}</div>
<div class="flow-line" v-if="index < scripts.length - 1"></div>
</div>
<div class="flow-content">
<div class="flow-header">
<span class="flow-name">{{ script.stepName }}</span>
<span class="flow-duration"><ClockCircleOutlined /> {{ script.duration }} 分钟</span>
</div>
<div class="flow-objective" v-if="script.objective">
<AimOutlined /> {{ script.objective }}
</div>
<div class="flow-script" v-if="script.teacherScript">
<SoundOutlined /> 教师讲稿
<div class="script-text">{{ script.teacherScript }}</div>
</div>
<!-- 关联资源 -->
<div class="flow-resources" v-if="getScriptResources(script).length > 0">
<FolderOutlined /> 关联资源
<div class="resource-tags">
<a-tag v-for="res in getScriptResources(script)" :key="res.id" @click="previewFile(res)" class="resource-tag">
{{ res.icon }} {{ res.name }}
</a-tag>
</div>
</div>
<!-- 逐页脚本 -->
<div class="flow-pages" v-if="script.pages && script.pages.length > 0">
<FileTextOutlined /> 逐页脚本 ({{ script.pages.length }})
<div class="pages-list">
<a-collapse size="small" ghost :activeKey="getExpandedPages(script)">
<a-collapse-panel v-for="(page, pIndex) in script.pages" :key="pIndex">
<template #header>
<span class="page-header">
{{ page.pageNumber }}
<span v-if="hasPageContent(page)" class="content-dot"></span>
</span>
</template>
<div class="page-detail">
<div v-if="page.questions" class="page-row">
<span class="page-label">教师话术</span>
<span class="page-value">{{ page.questions }}</span>
</div>
<div v-if="page.teacherNotes" class="page-row">
<span class="page-label">教学备注</span>
<span class="page-value notes">{{ page.teacherNotes }}</span>
</div>
<div v-if="getPageResources(script, page).length > 0" class="page-row">
<span class="page-label">关联资源</span>
<div class="page-resources">
<a-tag v-for="res in getPageResources(script, page)" :key="res.id" size="small" @click="previewFile(res)" class="resource-tag">
{{ res.icon }} {{ res.name }}
</a-tag>
</div>
</div>
</div>
</a-collapse-panel>
</a-collapse>
</div>
</div>
</div>
</div>
</div>
<div v-else class="empty-flow">
<InboxOutlined />
<span>暂无教学流程</span>
</div>
</div>
</div>
<!-- 延伸活动 -->
<div class="activity-card" v-if="activities && activities.length > 0">
<div class="card-header">
<div class="header-icon activity-icon"><AppstoreOutlined /></div>
<span>延伸活动</span>
<span class="activity-count">{{ activities.length }} </span>
</div>
<div class="card-body">
<div class="activity-list">
<div v-for="activity in activities" :key="activity.id" class="activity-item">
<div class="activity-header">
<span class="activity-name">{{ activity.name }}</span>
<a-tag :style="getActivityTypeStyle(translateActivityType(activity.activityType))">
{{ translateActivityType(activity.activityType) }}
</a-tag>
</div>
<div class="activity-info">
<span v-if="activity.duration"><ClockCircleOutlined /> {{ activity.duration }}分钟</span>
</div>
<div class="activity-content" v-if="activity.content">{{ activity.content }}</div>
<div class="activity-materials" v-if="activity.materials">
<ToolOutlined /> {{ activity.materials }}
</div>
</div>
</div>
</div>
</div>
</a-col>
<!-- 右侧资源和材料 -->
<a-col :span="8">
<!-- 数字资源 -->
<div class="resource-card">
<div class="card-header">
<div class="header-icon resource-icon"><FolderOutlined /></div>
<span>数字资源</span>
</div>
<div class="card-body">
<!-- 电子绘本 -->
<div class="resource-section" v-if="ebookPaths && ebookPaths.length > 0">
<div class="section-title"><FilePdfOutlined /> 电子绘本</div>
<div v-for="(item, index) in ebookPaths" :key="index" class="resource-item" @click="previewResource(item)">
<FilePdfOutlined class="item-icon ebook" />
<span class="item-name">{{ item.name || `电子绘本${index + 1}` }}</span>
<EyeOutlined class="item-action" />
</div>
</div>
<!-- 音频 -->
<div class="resource-section" v-if="audioPaths && audioPaths.length > 0">
<div class="section-title"><SoundOutlined /> 音频资源</div>
<div v-for="(item, index) in audioPaths" :key="index" class="resource-item" @click="previewResource(item)">
<SoundOutlined class="item-icon audio" />
<span class="item-name">{{ item.name || `音频${index + 1}` }}</span>
<PlayCircleOutlined class="item-action" />
</div>
</div>
<!-- 视频 -->
<div class="resource-section" v-if="videoPaths && videoPaths.length > 0">
<div class="section-title"><PlayCircleOutlined /> 视频资源</div>
<div v-for="(item, index) in videoPaths" :key="index" class="resource-item" @click="previewResource(item)">
<PlayCircleOutlined class="item-icon video" />
<span class="item-name">{{ item.name || `视频${index + 1}` }}</span>
<PlayCircleOutlined class="item-action" />
</div>
</div>
<!-- 其他 -->
<div class="resource-section" v-if="otherResources && otherResources.length > 0">
<div class="section-title"><FileOutlined /> 其他素材</div>
<div v-for="(item, index) in otherResources" :key="index" class="resource-item" @click="previewResource(item)">
<FileOutlined class="item-icon other" />
<span class="item-name">{{ item.name || `素材${index + 1}` }}</span>
<EyeOutlined class="item-action" />
</div>
</div>
<!-- 空状态 -->
<div v-if="(!ebookPaths || !ebookPaths.length) && (!audioPaths || !audioPaths.length) && (!videoPaths || !videoPaths.length) && (!otherResources || !otherResources.length)" class="empty-resource">
<InboxOutlined />
<span>暂无数字资源</span>
</div>
</div>
</div>
<!-- 教学材料 -->
<div class="material-card">
<div class="card-header">
<div class="header-icon material-icon"><ToolOutlined /></div>
<span>教学材料</span>
</div>
<div class="card-body">
<!-- 教学PPT -->
<div class="resource-section" v-if="course.pptPath">
<div class="section-title"><FilePptOutlined /> 教学PPT</div>
<div class="resource-item" @click="previewResource({ path: course.pptPath, name: course.pptName || '教学PPT' })">
<FilePptOutlined class="item-icon ppt" />
<span class="item-name">{{ course.pptName || '教学PPT' }}</span>
<EyeOutlined class="item-action" />
</div>
</div>
<!-- 教学挂图 -->
<div class="resource-section" v-if="posterPaths && posterPaths.length > 0">
<div class="section-title"><PictureOutlined /> 教学挂图</div>
<div class="poster-grid">
<img v-for="(item, index) in posterPaths" :key="index"
:src="getFileUrl(item.path)"
:alt="item.name || `挂图${index + 1}`"
class="poster-thumb"
@click="previewImage(getFileUrl(item.path))"
/>
</div>
</div>
<!-- 学生材料 -->
<div class="resource-section" v-if="course.studentMaterials">
<div class="section-title"><FormOutlined /> 学生材料</div>
<div class="student-materials">{{ course.studentMaterials }}</div>
</div>
<!-- 空状态 -->
<div v-if="!course.pptPath && (!posterPaths || !posterPaths.length) && !course.studentMaterials" class="empty-resource">
<InboxOutlined />
<span>暂无教学材料</span>
</div>
</div>
</div>
<!-- 使用统计 -->
<div class="stats-card">
<div class="card-header">
<div class="header-icon stats-icon"><BarChartOutlined /></div>
<span>使用统计</span>
</div>
<div class="card-body">
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value">{{ course.teacherCount || 0 }}</div>
<div class="stat-label">使用教师</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ course.usageCount || 0 }}</div>
<div class="stat-label">使用次数</div>
</div>
</div>
</div>
</div>
</a-col>
</a-row>
</div>
</a-spin>
<!-- 文件预览弹窗 -->
<FilePreviewModal
v-model:open="previewModalVisible"
:file-url="previewFileUrl"
:file-name="previewFileName"
/>
<!-- 图片预览 -->
<a-modal v-model:open="imagePreviewVisible" :footer="null" centered>
<img :src="previewImageUrl" style="width: 100%;" />
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import {
LeftOutlined,
BookOutlined,
ClockCircleOutlined,
InfoCircleOutlined,
OrderedListOutlined,
AimOutlined,
SoundOutlined,
FolderOutlined,
FileTextOutlined,
InboxOutlined,
AppstoreOutlined,
ToolOutlined,
FilePdfOutlined,
PlayCircleOutlined,
FileOutlined,
FilePptOutlined,
PictureOutlined,
FormOutlined,
EyeOutlined,
BarChartOutlined,
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import * as schoolApi from '@/api/school';
import FilePreviewModal from '@/components/FilePreviewModal.vue';
import {
translateGradeTags,
translateDomainTags,
getGradeTagStyle,
getDomainTagStyle,
translateActivityType,
getActivityTypeStyle,
} from '@/utils/tagMaps';
const router = useRouter();
const route = useRoute();
const loading = ref(false);
// 获取完整的文件 URL
const getFileUrl = (filePath: string | null | undefined): string => {
if (!filePath) return '';
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
return filePath;
}
const SERVER_BASE = import.meta.env.VITE_SERVER_BASE_URL || 'http://localhost:3000';
return `${SERVER_BASE}${filePath}`;
};
// 文件预览相关
const previewModalVisible = ref(false);
const previewFileUrl = ref('');
const previewFileName = ref('');
// 图片预览
const imagePreviewVisible = ref(false);
const previewImageUrl = ref('');
const course = ref<any>({
id: 0,
name: '',
pictureBookName: '',
description: '',
gradeTags: [],
domainTags: [],
duration: 25,
version: '1.0',
usageCount: 0,
teacherCount: 0,
avgRating: 0,
pptPath: '',
pptName: '',
studentMaterials: '',
authorized: true,
});
const scripts = ref<any[]>([]);
const activities = ref<any[]>([]);
// 数字资源
const ebookPaths = ref<any[]>([]);
const audioPaths = ref<any[]>([]);
const videoPaths = ref<any[]>([]);
const otherResources = ref<any[]>([]);
// 教学材料
const posterPaths = ref<any[]>([]);
// 根据脚本的 resourceIds 获取资源详情列表
const getScriptResources = (script: any) => {
let resourceIds: string[] = [];
if (script.resourceIds) {
if (typeof script.resourceIds === 'string') {
try {
resourceIds = JSON.parse(script.resourceIds);
} catch {
resourceIds = [];
}
} else if (Array.isArray(script.resourceIds)) {
resourceIds = script.resourceIds;
}
}
if (!resourceIds.length) return [];
const resources: { id: string; name: string; typeName: string; icon: string }[] = [];
const typeInfo: Record<string, { name: string; icon: string }> = {
ebook: { name: '电子绘本', icon: '📄' },
audio: { name: '音频', icon: '🎵' },
video: { name: '视频', icon: '📹' },
ppt: { name: 'PPT', icon: '📊' },
poster: { name: '挂图', icon: '🖼️' },
};
resourceIds.forEach((resId: string) => {
const parts = resId.split('-');
if (parts.length !== 2) return;
const type = parts[0];
const index = parseInt(parts[1]);
const info = typeInfo[type] || { name: '资源', icon: '📁' };
let name = '';
switch (type) {
case 'ebook':
name = ebookPaths.value[index]?.name || `电子绘本${index + 1}`;
break;
case 'audio':
name = audioPaths.value[index]?.name || `音频${index + 1}`;
break;
case 'video':
name = videoPaths.value[index]?.name || `视频${index + 1}`;
break;
case 'ppt':
name = course.value.pptName || '教学PPT';
break;
case 'poster':
name = posterPaths.value[index]?.name || `挂图${index + 1}`;
break;
}
if (name) {
resources.push({ id: resId, name, typeName: info.name, icon: info.icon });
}
});
return resources;
};
// 获取逐页脚本的资源列表
const getPageResources = (script: any, page: any) => {
if (!page.resourceIds || page.resourceIds.length === 0) return [];
const scriptResources = getScriptResources(script);
let pageResourceIds: string[] = [];
if (typeof page.resourceIds === 'string') {
try {
pageResourceIds = JSON.parse(page.resourceIds);
} catch {
pageResourceIds = [];
}
} else if (Array.isArray(page.resourceIds)) {
pageResourceIds = page.resourceIds;
}
return scriptResources.filter((r: any) => pageResourceIds.includes(r.id));
};
// 检查页面是否有内容
const hasPageContent = (page: any) => {
return (page.questions && page.questions.trim()) ||
(page.teacherNotes && page.teacherNotes.trim()) ||
(page.resourceIds && page.resourceIds.length > 0);
};
// 获取有内容的页面索引
const getExpandedPages = (script: any) => {
if (!script.pages) return [];
return script.pages
.map((page: any, index: number) => hasPageContent(page) ? String(index) : null)
.filter((idx: string | null) => idx !== null);
};
const loadCourseDetail = async () => {
const courseId = route.params.id as string;
if (!courseId) return;
loading.value = true;
try {
const data = await schoolApi.getSchoolCourse(parseInt(courseId));
course.value = {
...data,
gradeTags: translateGradeTags(data.gradeTags || []),
domainTags: translateDomainTags(data.domainTags || []),
};
scripts.value = (data.scripts || []).map((script: any) => ({
...script,
pages: (script.pages || []).map((page: any) => ({
...page,
resourceIds: typeof page.resourceIds === 'string' ? JSON.parse(page.resourceIds) : (page.resourceIds || []),
})),
}));
activities.value = (data.activities || []).map((activity: any) => {
let content = '';
if (activity.onlineMaterials) {
if (typeof activity.onlineMaterials === 'object') {
content = activity.onlineMaterials.content || '';
} else if (typeof activity.onlineMaterials === 'string') {
try {
const parsed = JSON.parse(activity.onlineMaterials);
content = parsed.content || '';
} catch {
content = activity.onlineMaterials;
}
}
}
return {
...activity,
content: content,
materials: activity.offlineMaterials || '',
};
});
ebookPaths.value = Array.isArray(data.ebookPaths) ? data.ebookPaths : [];
audioPaths.value = Array.isArray(data.audioPaths) ? data.audioPaths : [];
videoPaths.value = Array.isArray(data.videoPaths) ? data.videoPaths : [];
otherResources.value = Array.isArray(data.otherResources) ? data.otherResources : [];
if (data.posterPaths) {
if (typeof data.posterPaths === 'string') {
try { posterPaths.value = JSON.parse(data.posterPaths); } catch { posterPaths.value = []; }
} else if (Array.isArray(data.posterPaths)) {
posterPaths.value = data.posterPaths;
}
}
} catch (error: any) {
message.error(error.response?.data?.message || '获取课程详情失败');
router.back();
} finally {
loading.value = false;
}
};
const goBackToList = () => {
router.push('/school/courses');
};
const previewResource = (resource: any) => {
if (!resource.path) {
message.warning('文件路径不存在');
return;
}
previewFileUrl.value = getFileUrl(resource.path);
previewFileName.value = resource.name || '未知文件';
previewModalVisible.value = true;
};
const previewFile = (res: any) => {
const resId = res.id;
const parts = resId.split('-');
if (parts.length !== 2) return;
const type = parts[0];
const index = parseInt(parts[1]);
let path = '';
switch (type) {
case 'ebook':
path = ebookPaths.value[index]?.path || '';
break;
case 'audio':
path = audioPaths.value[index]?.path || '';
break;
case 'video':
path = videoPaths.value[index]?.path || '';
break;
case 'ppt':
path = course.value.pptPath || '';
break;
case 'poster':
path = posterPaths.value[index]?.path || '';
break;
}
if (path) {
previewFileUrl.value = getFileUrl(path);
previewFileName.value = res.name;
previewModalVisible.value = true;
}
};
const previewImage = (url: string) => {
previewImageUrl.value = url;
imagePreviewVisible.value = true;
};
onMounted(() => {
loadCourseDetail();
});
</script>
<style scoped>
.course-detail-view {
min-height: 100vh;
background: linear-gradient(135deg, #F0FFF4 0%, #FFFFFF 50%, #F0FDF4 100%);
padding-bottom: 24px;
}
/* 顶部课程信息 */
.course-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
position: sticky;
top: 0;
z-index: 100;
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.back-btn {
color: #666;
font-size: 14px;
}
.back-btn:hover {
color: #43e97b;
}
.course-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.course-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin: 0;
}
.course-meta {
display: flex;
align-items: center;
gap: 16px;
color: #666;
font-size: 13px;
}
.meta-item {
display: flex;
align-items: center;
gap: 4px;
}
.auth-tag {
font-size: 14px;
padding: 4px 16px;
border-radius: 16px;
}
/* 内容区域 */
.detail-content {
padding: 20px 24px;
}
/* 卡片通用样式 */
.card-header {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 16px;
border-bottom: 1px solid #f0f0f0;
font-weight: 600;
font-size: 14px;
color: #333;
}
.header-icon {
width: 32px;
height: 32px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.card-body {
padding: 16px;
}
/* 课程信息卡片 */
.info-card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
overflow: hidden;
margin-bottom: 16px;
}
.info-icon {
background: linear-gradient(135deg, #E8F5E9 0%, #C8E6C9 100%);
color: #43A047;
}
.cover-section {
text-align: center;
margin-bottom: 16px;
}
.cover-image {
max-width: 100%;
max-height: 280px;
border-radius: 8px;
object-fit: cover;
}
.info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px 24px;
}
.info-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.info-item.full-width {
grid-column: span 2;
}
.info-label {
font-size: 12px;
color: #999;
}
.info-value {
font-size: 14px;
color: #333;
}
.info-value.desc {
line-height: 1.6;
white-space: pre-wrap;
}
.info-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
/* 教学流程卡片 */
.flow-card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
overflow: hidden;
margin-bottom: 16px;
}
.flow-icon {
background: linear-gradient(135deg, #E3F2FD 0%, #BBDEFB 100%);
color: #1976D2;
}
.step-count {
margin-left: auto;
font-size: 12px;
color: #999;
font-weight: 400;
}
.flow-timeline {
padding: 0 8px;
}
.flow-item {
display: flex;
gap: 16px;
padding: 12px 0;
}
.flow-indicator {
display: flex;
flex-direction: column;
align-items: center;
}
.flow-number {
width: 28px;
height: 28px;
border-radius: 50%;
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 600;
}
.flow-line {
width: 2px;
flex: 1;
background: #E8E8E8;
margin-top: 8px;
min-height: 40px;
}
.flow-content {
flex: 1;
padding-bottom: 12px;
}
.flow-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.flow-name {
font-size: 15px;
font-weight: 600;
color: #333;
}
.flow-duration {
font-size: 12px;
color: #999;
display: flex;
align-items: center;
gap: 4px;
}
.flow-objective {
font-size: 13px;
color: #666;
margin-bottom: 8px;
padding: 8px 12px;
background: #E8F5E9;
border-radius: 6px;
border-left: 3px solid #43e97b;
}
.flow-script {
font-size: 13px;
margin-bottom: 8px;
padding: 10px 14px;
background: #FFF8F0;
border-radius: 6px;
border-left: 3px solid #FF8C42;
color: #FF8C42;
}
.script-text {
margin-top: 8px;
color: #333;
white-space: pre-wrap;
line-height: 1.6;
}
.flow-resources,
.flow-pages {
font-size: 13px;
color: #666;
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed #E8E8E8;
}
.resource-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.resource-tag {
cursor: pointer;
transition: all 0.2s;
}
.resource-tag:hover {
border-color: #43e97b;
color: #43e97b;
}
.pages-list {
margin-top: 8px;
}
.page-header {
display: flex;
align-items: center;
gap: 8px;
}
.content-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #52c41a;
}
.page-detail {
padding: 8px 0;
}
.page-row {
margin-bottom: 8px;
font-size: 13px;
}
.page-label {
color: #999;
}
.page-value {
color: #333;
}
.page-value.notes {
color: #666;
font-style: italic;
}
.page-resources {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 4px;
}
.empty-flow {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px;
color: #999;
gap: 8px;
}
/* 延伸活动卡片 */
.activity-card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
overflow: hidden;
margin-bottom: 16px;
}
.activity-icon {
background: linear-gradient(135deg, #FFF3E0 0%, #FFE0B2 100%);
color: #F57C00;
}
.activity-count {
margin-left: auto;
font-size: 12px;
color: #999;
font-weight: 400;
}
.activity-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.activity-item {
padding: 12px;
background: #FAFAFA;
border-radius: 8px;
}
.activity-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.activity-name {
font-size: 14px;
font-weight: 500;
color: #333;
}
.activity-info {
font-size: 12px;
color: #999;
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 4px;
}
.activity-content {
font-size: 13px;
color: #666;
line-height: 1.5;
margin-bottom: 6px;
}
.activity-materials {
font-size: 12px;
color: #999;
}
/* 右侧资源卡片 */
.resource-card,
.material-card,
.stats-card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
overflow: hidden;
margin-bottom: 16px;
}
.resource-icon {
background: linear-gradient(135deg, #E3F2FD 0%, #BBDEFB 100%);
color: #1976D2;
}
.material-icon {
background: linear-gradient(135deg, #F3E5F5 0%, #E1BEE7 100%);
color: #8E24AA;
}
.stats-icon {
background: linear-gradient(135deg, #E8F5E9 0%, #C8E6C9 100%);
color: #43A047;
}
.resource-section {
margin-bottom: 16px;
}
.resource-section:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 13px;
font-weight: 500;
color: #666;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.resource-item {
display: flex;
align-items: center;
padding: 10px 12px;
background: #FAFAFA;
border-radius: 8px;
margin-bottom: 6px;
cursor: pointer;
transition: all 0.2s;
}
.resource-item:hover {
background: #F0FFF4;
}
.resource-item:hover .item-action {
opacity: 1;
}
.item-icon {
font-size: 18px;
margin-right: 10px;
}
.item-icon.ebook { color: #1890ff; }
.item-icon.audio { color: #52c41a; }
.item-icon.video { color: #722ed1; }
.item-icon.other { color: #fa8c16; }
.item-icon.ppt { color: #1890ff; }
.item-name {
flex: 1;
font-size: 13px;
color: #333;
}
.item-action {
color: #43e97b;
opacity: 0;
transition: opacity 0.2s;
}
.empty-resource {
display: flex;
flex-direction: column;
align-items: center;
padding: 24px;
color: #999;
gap: 8px;
}
.poster-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.poster-thumb {
width: 100%;
height: 60px;
object-fit: cover;
border-radius: 6px;
cursor: pointer;
transition: transform 0.2s;
}
.poster-thumb:hover {
transform: scale(1.05);
}
.student-materials {
font-size: 13px;
color: #666;
line-height: 1.6;
white-space: pre-wrap;
padding: 10px 12px;
background: #FAFAFA;
border-radius: 8px;
}
/* 使用统计 */
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.stat-item {
text-align: center;
padding: 16px;
background: #FAFAFA;
border-radius: 8px;
}
.stat-value {
font-size: 28px;
font-weight: 600;
color: #43e97b;
}
.stat-label {
font-size: 12px;
color: #999;
margin-top: 4px;
}
</style>