851 lines
23 KiB
Vue
851 lines
23 KiB
Vue
<template>
|
||
<div class="course-detail">
|
||
<div class="course-header">
|
||
<div class="container">
|
||
<div class="course-info">
|
||
<h1 class="course-title">{{ courseData.title }}</h1>
|
||
<p class="course-desc">{{ courseData.description }}</p>
|
||
<div class="course-meta">
|
||
<div class="meta-item">
|
||
<el-icon><User /></el-icon>
|
||
<span>讲师: {{ courseData.teacher }}</span>
|
||
</div>
|
||
<div class="meta-item">
|
||
<el-icon><View /></el-icon>
|
||
<span>{{ courseData.studentCount || 0 }}人学习</span>
|
||
</div>
|
||
<div class="meta-item">
|
||
<el-rate v-model="courseData.rating" disabled text-color="#ff9900" />
|
||
<span>({{ courseData.ratingCount || 0 }})</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="main-content">
|
||
<div class="container">
|
||
<el-row :gutter="20">
|
||
<el-col :span="18">
|
||
<div class="resource-player">
|
||
<div v-if="currentResource" class="player-container">
|
||
<video-player
|
||
v-if="currentResource.type === 'video'"
|
||
:src="currentResource.url"
|
||
:poster="courseData.coverImg"
|
||
@ended="handleResourceComplete"
|
||
@timeupdate="handleTimeUpdate"
|
||
/>
|
||
|
||
<pdf-viewer
|
||
v-else-if="currentResource.type === 'pdf'"
|
||
:pdf-url="currentResource.url"
|
||
/>
|
||
|
||
<ppt-viewer
|
||
v-else-if="currentResource.type === 'ppt'"
|
||
:slides="currentResource.slides"
|
||
@slide-change="handleSlideChange"
|
||
/>
|
||
|
||
<div class="resource-info">
|
||
<h3>{{ currentResource.title }}</h3>
|
||
<div class="resource-meta">
|
||
<span class="type-tag" :class="currentResource.type">
|
||
{{ getResourceTypeName(currentResource.type) }}
|
||
</span>
|
||
<span v-if="currentResource.duration" class="duration">
|
||
<el-icon><Timer /></el-icon>
|
||
{{ currentResource.duration }}
|
||
</span>
|
||
<el-button
|
||
v-if="currentResource.downloadable"
|
||
type="primary"
|
||
link
|
||
@click="downloadResource"
|
||
>
|
||
<el-icon><Download /></el-icon>
|
||
下载资料
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="course-tabs">
|
||
<el-tabs v-model="activeTab">
|
||
<el-tab-pane label="课程章节" name="chapters">
|
||
<div class="chapter-list">
|
||
<el-collapse v-model="activeChapters">
|
||
<el-collapse-item
|
||
v-for="chapter in courseData.chapters"
|
||
:key="chapter.id"
|
||
:title="chapter.title"
|
||
:name="chapter.id"
|
||
>
|
||
<div class="section-list">
|
||
<div
|
||
v-for="section in chapter.sections"
|
||
:key="section.id"
|
||
class="section-item"
|
||
:class="{
|
||
'active': currentResource?.id === section.id,
|
||
'completed': section.completed
|
||
}"
|
||
@click="playResource(section)"
|
||
>
|
||
<div class="section-info">
|
||
<el-icon>
|
||
<VideoCamera v-if="section.type === 'video'" />
|
||
<Document v-else-if="section.type === 'pdf'" />
|
||
<PictureFilled v-else-if="section.type === 'ppt'" />
|
||
<Files v-else />
|
||
</el-icon>
|
||
<span class="title">{{ section.title }}</span>
|
||
<el-tag
|
||
v-if="section.isFree"
|
||
size="small"
|
||
type="success"
|
||
>免费</el-tag>
|
||
</div>
|
||
<div class="section-meta">
|
||
<span class="duration">{{ section.duration }}</span>
|
||
<el-icon
|
||
v-if="section.completed"
|
||
class="completed-icon"
|
||
><Check /></el-icon>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-collapse-item>
|
||
</el-collapse>
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<el-tab-pane label="课程介绍" name="intro">
|
||
<div class="course-intro" v-html="courseData.description"></div>
|
||
</el-tab-pane>
|
||
|
||
<el-tab-pane label="课程评价" name="reviews">
|
||
<div class="course-reviews">
|
||
<div class="review-summary">
|
||
<div class="rating-overall">
|
||
<div class="rating-score">{{ courseData.rating.toFixed(1) }}</div>
|
||
<div class="rating-stars">
|
||
<el-rate v-model="courseData.rating" disabled text-color="#ff9900" />
|
||
<div class="rating-count">{{ courseData.ratingCount }}人评价</div>
|
||
</div>
|
||
</div>
|
||
<div class="rating-distribution">
|
||
<div v-for="(count, index) in courseData.ratingDistribution" :key="index" class="rating-bar">
|
||
<span class="rating-level">{{ 5 - index }}星</span>
|
||
<el-progress
|
||
:percentage="Math.round(count / courseData.ratingCount * 100)"
|
||
:stroke-width="12"
|
||
:show-text="false"
|
||
:color="'#ffc107'"
|
||
/>
|
||
<span class="rating-percent">{{ Math.round(count / courseData.ratingCount * 100) }}%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="review-list">
|
||
<div v-for="review in courseData.reviews" :key="review.id" class="review-item">
|
||
<div class="review-header">
|
||
<div class="reviewer-info">
|
||
<el-avatar :size="40" :src="review.avatar"></el-avatar>
|
||
<div class="reviewer-meta">
|
||
<div class="reviewer-name">{{ review.username }}</div>
|
||
<div class="review-date">{{ review.date }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="review-rating">
|
||
<el-rate v-model="review.rating" disabled text-color="#ff9900" />
|
||
</div>
|
||
</div>
|
||
<div class="review-content">{{ review.content }}</div>
|
||
<div v-if="review.reply" class="review-reply">
|
||
<div class="reply-header">讲师回复:</div>
|
||
<div class="reply-content">{{ review.reply }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
</div>
|
||
</el-col>
|
||
|
||
<el-col :span="6">
|
||
<div class="course-sidebar">
|
||
<div class="price-card">
|
||
<div class="price" :class="{ 'free': !courseData.price }">
|
||
{{ courseData.price ? `¥${courseData.price}` : '免费' }}
|
||
</div>
|
||
<el-button
|
||
type="primary"
|
||
size="large"
|
||
block
|
||
@click="handleEnroll"
|
||
>
|
||
{{ courseData.enrolled ? '继续学习' : '立即报名' }}
|
||
</el-button>
|
||
</div>
|
||
|
||
<div class="course-info-card">
|
||
<h4>课程信息</h4>
|
||
<ul>
|
||
<li>
|
||
<el-icon><VideoPlay /></el-icon>
|
||
<span>{{ courseData.videoCount || 0 }}个视频</span>
|
||
</li>
|
||
<li>
|
||
<el-icon><Document /></el-icon>
|
||
<span>{{ courseData.docCount || 0 }}份文档</span>
|
||
</li>
|
||
<li>
|
||
<el-icon><Timer /></el-icon>
|
||
<span>{{ courseData.totalDuration || '0小时' }}学时</span>
|
||
</li>
|
||
<li>
|
||
<el-icon><Connection /></el-icon>
|
||
<span>永久有效</span>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
import { ElMessage } from 'element-plus'
|
||
import {
|
||
User, View, VideoPlay, Document, Timer,
|
||
Check, Connection, VideoCamera, Files,
|
||
PictureFilled, Download
|
||
} from '@element-plus/icons-vue'
|
||
import VideoPlayer from '@/components/player/VideoPlayer.vue'
|
||
import PdfViewer from '@/components/player/PdfViewer.vue'
|
||
import PptViewer from '@/components/player/PptViewer.vue'
|
||
|
||
const route = useRoute()
|
||
const activeTab = ref('chapters')
|
||
const activeChapters = ref([])
|
||
|
||
// 模拟课程数据
|
||
const courseData = ref({
|
||
title: 'C++面向对象编程精讲',
|
||
description: '本课程深入讲解C++面向对象编程的核心概念和实践应用,帮助学习者掌握C++开发技能。',
|
||
teacher: '张教授',
|
||
price: 0, // 免费
|
||
rating: 4.8,
|
||
ratingCount: 245,
|
||
studentCount: 1234,
|
||
videoCount: 12,
|
||
docCount: 5,
|
||
totalDuration: '24小时',
|
||
enrolled: false,
|
||
coverImg: 'http://localhost:9000/file/c6d07740-7306-4fc1-b1f8-670684a73ed9/img/31c258e7-3fd4-402c-8f2f-c9455c6cfdaf.png',
|
||
chapters: [
|
||
{
|
||
id: 1,
|
||
title: '第一章:C++基础入门',
|
||
sections: [
|
||
{
|
||
id: 101,
|
||
title: '1.1 C++开发环境搭建',
|
||
type: 'video',
|
||
duration: '25:00',
|
||
url: 'http://localhost:9000/videos/ffffad37-9804-4765-ae18-3f8dcda9bea8.mp4',
|
||
isFree: true,
|
||
completed: false
|
||
},
|
||
{
|
||
id: 102,
|
||
title: '1.2 C++基本语法',
|
||
type: 'video',
|
||
duration: '30:00',
|
||
url: 'https://example.com/videos/2.mp4',
|
||
isFree: true,
|
||
completed: false
|
||
},
|
||
{
|
||
id: 103,
|
||
title: '第一章课件',
|
||
type: 'pdf',
|
||
url: 'http://localhost:9000/file/%E5%AE%9E%E6%96%BD%E7%94%A8.pdf',
|
||
downloadable: true,
|
||
isFree: true,
|
||
completed: false
|
||
}
|
||
]
|
||
},
|
||
{
|
||
id: 2,
|
||
title: '第二章:面向对象基础',
|
||
sections: [
|
||
{
|
||
id: 201,
|
||
title: '2.1 类与对象',
|
||
type: 'video',
|
||
duration: '45:00',
|
||
url: 'https://example.com/videos/3.mp4',
|
||
isFree: false,
|
||
completed: false
|
||
},
|
||
{
|
||
id: 202,
|
||
title: '2.2 封装与访问控制',
|
||
type: 'video',
|
||
duration: '40:00',
|
||
url: 'https://example.com/videos/4.mp4',
|
||
isFree: false,
|
||
completed: false
|
||
},
|
||
{
|
||
id: 203,
|
||
title: '第二章PPT',
|
||
type: 'ppt',
|
||
slides: [
|
||
{ url: 'http://localhost:9000/file/c6d07740-7306-4fc1-b1f8-670684a73ed9/img/31c258e7-3fd4-402c-8f2f-c9455c6cfdaf.png', title: '封面' },
|
||
{ url: 'http://localhost:9000/file/c6d07740-7306-4fc1-b1f8-670684a73ed9/img/4灰度.jpg', title: '目录' },
|
||
{ url: 'http://localhost:9000/file/c6d07740-7306-4fc1-b1f8-670684a73ed9/img/4.jpg', title: '内容' }
|
||
],
|
||
isFree: false,
|
||
completed: false
|
||
}
|
||
]
|
||
}
|
||
],
|
||
reviews: [
|
||
{
|
||
id: 1,
|
||
username: '学员小王',
|
||
avatar: 'https://example.com/avatar1.jpg',
|
||
rating: 5,
|
||
content: '课程内容非常详实,讲解清晰,很适合入门学习!',
|
||
date: '2024-01-15',
|
||
reply: '感谢您的评价,我们会继续努力!'
|
||
},
|
||
{
|
||
id: 2,
|
||
username: '学员小李',
|
||
avatar: 'https://example.com/avatar2.jpg',
|
||
rating: 4,
|
||
content: '整体不错,希望能多一些实战案例。',
|
||
date: '2024-01-10'
|
||
}
|
||
],
|
||
ratingDistribution: [150, 50, 30, 10, 5]
|
||
})
|
||
|
||
// 当前正在播放/查看的资源
|
||
const currentResource = ref(courseData.value.chapters[0].sections[0])
|
||
|
||
// 资源类型名称映射
|
||
const resourceTypes = {
|
||
video: '视频',
|
||
pdf: 'PDF文档',
|
||
ppt: 'PPT课件',
|
||
document: '文档资料'
|
||
}
|
||
|
||
const getResourceTypeName = (type) => {
|
||
return resourceTypes[type] || '其他'
|
||
}
|
||
|
||
// 播放资源
|
||
const playResource = (resource) => {
|
||
if (!courseData.value.enrolled && !resource.isFree) {
|
||
ElMessage.warning('请先报名课程')
|
||
return
|
||
}
|
||
currentResource.value = resource
|
||
}
|
||
|
||
// 处理资源完成
|
||
const handleResourceComplete = () => {
|
||
if (currentResource.value) {
|
||
currentResource.value.completed = true
|
||
ElMessage.success('学习完成!')
|
||
// TODO: 调用后端API更新学习进度
|
||
}
|
||
}
|
||
|
||
// 处理视频进度更新
|
||
const handleTimeUpdate = ({ currentTime, duration }) => {
|
||
const progress = (currentTime / duration) * 100
|
||
console.log('Progress:', progress)
|
||
// TODO: 定期保存播放进度到后端
|
||
if (progress > 90) {
|
||
handleResourceComplete()
|
||
}
|
||
}
|
||
|
||
// 处理PPT幻灯片切换
|
||
const handleSlideChange = (index) => {
|
||
// 保存PPT浏览进度
|
||
console.log('Current slide:', index)
|
||
}
|
||
|
||
// 下载资源
|
||
const downloadResource = () => {
|
||
if (currentResource.value?.url) {
|
||
window.open(currentResource.value.url, '_blank')
|
||
}
|
||
}
|
||
|
||
// 报名课程
|
||
const handleEnroll = async () => {
|
||
try {
|
||
// TODO: 调用报名API
|
||
courseData.value.enrolled = true
|
||
ElMessage.success('报名成功!')
|
||
} catch (error) {
|
||
ElMessage.error('报名失败:' + error.message)
|
||
}
|
||
}
|
||
|
||
onMounted(async () => {
|
||
const courseId = route.params.id
|
||
try {
|
||
// TODO: 获取课程详情
|
||
// const response = await getCourseDetail(courseId)
|
||
// courseData.value = response.data
|
||
|
||
// 默认展开第一章
|
||
activeChapters.value = [courseData.value.chapters[0].id]
|
||
} catch (error) {
|
||
ElMessage.error('获取课程信息失败')
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.course-detail {
|
||
background: #f8fafc;
|
||
min-height: 100vh;
|
||
|
||
.course-header {
|
||
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
|
||
padding: 60px 0;
|
||
color: white;
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 0 20px;
|
||
}
|
||
|
||
.course-info {
|
||
max-width: 800px;
|
||
|
||
.course-title {
|
||
font-size: 32px;
|
||
font-weight: 600;
|
||
margin-bottom: 16px;
|
||
color: white;
|
||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.course-desc {
|
||
font-size: 16px;
|
||
color: #e2e8f0;
|
||
margin-bottom: 24px;
|
||
line-height: 1.6;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.course-meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 24px;
|
||
|
||
.meta-item {
|
||
display: flex;
|
||
align-items: center;
|
||
color: #cbd5e1;
|
||
font-size: 15px;
|
||
|
||
.el-icon {
|
||
margin-right: 8px;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.el-rate {
|
||
margin-right: 4px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.main-content {
|
||
padding: 40px 20px;
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
}
|
||
|
||
.resource-player {
|
||
background: white;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||
|
||
.player-container {
|
||
position: relative;
|
||
width: 100%;
|
||
background: #000;
|
||
|
||
video {
|
||
width: 100%;
|
||
aspect-ratio: 16 / 9;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.resource-info {
|
||
background: white;
|
||
padding: 20px;
|
||
border-top: 1px solid #e2e8f0;
|
||
|
||
h3 {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #1e293b;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.resource-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
|
||
.type-tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 4px 12px;
|
||
border-radius: 20px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
|
||
.el-icon {
|
||
margin-right: 4px;
|
||
}
|
||
|
||
&.video {
|
||
background: #eff6ff;
|
||
color: #3b82f6;
|
||
}
|
||
&.pdf {
|
||
background: #fef2f2;
|
||
color: #ef4444;
|
||
}
|
||
&.ppt {
|
||
background: #fff7ed;
|
||
color: #f97316;
|
||
}
|
||
&.document {
|
||
background: #f0fdf4;
|
||
color: #22c55e;
|
||
}
|
||
}
|
||
|
||
.duration {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
color: #64748b;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.course-tabs {
|
||
margin-top: 24px;
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||
|
||
.chapter-list {
|
||
.el-collapse {
|
||
border: none;
|
||
|
||
.el-collapse-item {
|
||
&:not(:last-child) {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.el-collapse-item__header {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #1e293b;
|
||
background: #f8fafc;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
border: none;
|
||
}
|
||
|
||
.el-collapse-item__content {
|
||
padding: 16px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.section-item {
|
||
padding: 12px 16px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
margin-bottom: 8px;
|
||
|
||
&:hover {
|
||
background: #f1f5f9;
|
||
}
|
||
|
||
&.active {
|
||
background: #eff6ff;
|
||
|
||
.section-info {
|
||
color: #3b82f6;
|
||
}
|
||
}
|
||
|
||
&.completed {
|
||
.completed-icon {
|
||
color: #22c55e;
|
||
}
|
||
}
|
||
|
||
.section-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
color: #475569;
|
||
|
||
.el-icon {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.title {
|
||
flex: 1;
|
||
font-size: 15px;
|
||
}
|
||
|
||
.el-tag {
|
||
font-size: 12px;
|
||
border-radius: 4px;
|
||
}
|
||
}
|
||
|
||
.section-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
color: #94a3b8;
|
||
font-size: 13px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.course-reviews {
|
||
.review-summary {
|
||
display: flex;
|
||
gap: 40px;
|
||
padding: 24px;
|
||
background: #f8fafc;
|
||
border-radius: 12px;
|
||
margin-bottom: 24px;
|
||
|
||
.rating-overall {
|
||
display: flex;
|
||
gap: 20px;
|
||
align-items: center;
|
||
|
||
.rating-score {
|
||
font-size: 48px;
|
||
font-weight: 600;
|
||
color: #f59e0b;
|
||
}
|
||
|
||
.rating-stars {
|
||
.rating-count {
|
||
margin-top: 4px;
|
||
color: #64748b;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.rating-distribution {
|
||
flex: 1;
|
||
max-width: 400px;
|
||
|
||
.rating-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 8px;
|
||
|
||
.rating-level {
|
||
width: 40px;
|
||
color: #64748b;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.el-progress {
|
||
flex: 1;
|
||
}
|
||
|
||
.rating-percent {
|
||
width: 40px;
|
||
text-align: right;
|
||
color: #64748b;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.review-list {
|
||
.review-item {
|
||
padding: 20px;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.review-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
|
||
.reviewer-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
|
||
.reviewer-meta {
|
||
.reviewer-name {
|
||
font-weight: 500;
|
||
color: #1e293b;
|
||
}
|
||
|
||
.review-date {
|
||
font-size: 13px;
|
||
color: #64748b;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.review-content {
|
||
color: #475569;
|
||
line-height: 1.6;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.review-reply {
|
||
background: #f8fafc;
|
||
padding: 12px 16px;
|
||
border-radius: 8px;
|
||
margin-top: 12px;
|
||
|
||
.reply-header {
|
||
color: #64748b;
|
||
font-size: 13px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.reply-content {
|
||
color: #475569;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.course-sidebar {
|
||
position: sticky;
|
||
top: 24px;
|
||
|
||
.price-card {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||
|
||
.price {
|
||
font-size: 32px;
|
||
font-weight: 600;
|
||
color: #ef4444;
|
||
margin-bottom: 20px;
|
||
|
||
&.free {
|
||
color: #22c55e;
|
||
}
|
||
}
|
||
|
||
.el-button {
|
||
height: 44px;
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
.course-info-card {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||
|
||
h4 {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #1e293b;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
ul {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 0;
|
||
|
||
li {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px 0;
|
||
color: #475569;
|
||
font-size: 15px;
|
||
|
||
&:not(:last-child) {
|
||
border-bottom: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.el-icon {
|
||
margin-right: 12px;
|
||
font-size: 18px;
|
||
color: #3b82f6;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|