1205 lines
32 KiB
Vue
1205 lines
32 KiB
Vue
|
|
<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>
|