活动和精选资源以及师资力量页面的静态渲染

This commit is contained in:
username 2025-07-29 00:06:54 +08:00
parent 5bb1bbac26
commit c7453da854
50 changed files with 562 additions and 457 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -73,13 +73,13 @@ export const getCoursesExample = async () => {
const response = await CourseApi.getCourses({
page: 1,
pageSize: 20,
category: '前端开发',
level: 'intermediate',
sortBy: 'rating'
categoryId: 1,
difficulty: 1,
sortBy: 'createdAt'
})
if (response.code === 200) {
const { list, total, page, pageSize } = response.data
const { list, total } = response.data
console.log('课程列表:', list)
console.log('总数:', total)
return response.data
@ -98,7 +98,7 @@ export const searchCoursesExample = async () => {
level: 'intermediate',
price: 'paid',
rating: 4,
sortBy: 'rating',
sortBy: 'newest',
page: 1,
pageSize: 10
})

View File

@ -33,6 +33,7 @@ export class AuthApi {
nickname: '用户',
avatar: '',
role: 'student',
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
},

View File

@ -13,7 +13,6 @@ import type {
BackendCourseSection,
BackendCourseSectionListResponse,
Quiz,
QuizQuestion,
LearningProgress,
SearchRequest,
Instructor,
@ -30,12 +29,23 @@ export class CourseApi {
/**
* ISO字符串
*/
private static formatTimestamp(timestamp: number | null | undefined): string {
if (!timestamp || timestamp <= 0) {
private static formatTimestamp(timestamp: number | string | null | undefined): string {
if (!timestamp) {
return new Date().toISOString()
}
try {
// 如果是字符串,尝试解析
if (typeof timestamp === 'string') {
const date = new Date(timestamp)
return isNaN(date.getTime()) ? new Date().toISOString() : date.toISOString()
}
// 如果是数字时间戳
if (timestamp <= 0) {
return new Date().toISOString()
}
// 如果时间戳是秒级的,转换为毫秒级
const ms = timestamp < 10000000000 ? timestamp * 1000 : timestamp
const date = new Date(ms)
@ -181,8 +191,8 @@ export class CourseApi {
content: response.data.outline, // 使用 outline 作为课程内容
thumbnail: response.data.cover,
coverImage: response.data.cover,
price: parseFloat(response.data.price),
originalPrice: parseFloat(response.data.price),
price: parseFloat(response.data.price || '0'),
originalPrice: parseFloat(response.data.price || '0'),
currency: 'CNY',
rating: 4.5,
ratingCount: 0,
@ -214,8 +224,8 @@ export class CourseApi {
certifications: []
},
status: 'published' as const,
createdAt: this.formatTimestamp(response.data.createdAt),
updatedAt: this.formatTimestamp(response.data.updatedAt),
createdAt: this.formatTimestamp(response.data.createdTime),
updatedAt: this.formatTimestamp(response.data.updatedTime),
publishedAt: response.data.startTime
}

View File

@ -4,7 +4,7 @@ import type {
ApiResponse,
PaginationResponse,
Order,
OrderItem,
} from '../types'
/**

View File

@ -53,7 +53,7 @@ export class UploadApi {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
onUploadProgress: (progressEvent: any) => {
if (onProgress && progressEvent.total) {
const progress = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
@ -90,7 +90,7 @@ export class UploadApi {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
onUploadProgress: (progressEvent: any) => {
if (onProgress && progressEvent.total) {
const progress = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
@ -130,7 +130,7 @@ export class UploadApi {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
onUploadProgress: (progressEvent: any) => {
if (onProgress && progressEvent.total) {
const progress = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
@ -155,7 +155,7 @@ export class UploadApi {
error?: string
}>>> {
const formData = new FormData()
files.forEach((file, index) => {
files.forEach((file) => {
formData.append(`files`, file)
})
@ -163,7 +163,7 @@ export class UploadApi {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
onUploadProgress: (progressEvent: any) => {
if (onProgress && progressEvent.total) {
const progress = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
@ -255,7 +255,7 @@ export class UploadApi {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
onUploadProgress: (progressEvent: any) => {
if (onProgress && progressEvent.total) {
const progress = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
@ -291,7 +291,7 @@ export class UploadApi {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
onUploadProgress: (progressEvent: any) => {
if (onProgress && progressEvent.total) {
const progress = Math.round(
(progressEvent.loaded * 100) / progressEvent.total

View File

@ -1,7 +1,7 @@
// HTTP 请求封装文件
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosRequestConfig, AxiosResponse } from 'axios'
import { useUserStore } from '@/stores/user'
import router from '@/router'
// import router from '@/router'
import type { ApiResponse } from './types'
// 消息提示函数 - 使用window.alert作为fallback实际项目中应该使用UI库的消息组件
@ -27,21 +27,15 @@ const request: AxiosInstance = axios.create({
// 请求拦截器
request.interceptors.request.use(
(config: AxiosRequestConfig) => {
(config: InternalAxiosRequestConfig) => {
// 添加认证token
const userStore = useUserStore()
if (userStore.token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${userStore.token}`,
}
config.headers.Authorization = `Bearer ${userStore.token}`
}
// 添加请求时间戳
config.headers = {
...config.headers,
'X-Request-Time': Date.now().toString(),
}
config.headers['X-Request-Time'] = Date.now().toString()
// 开发环境下打印请求信息
if (import.meta.env.DEV) {
@ -77,7 +71,7 @@ request.interceptors.response.use(
// 检查业务状态码
if (data.code === 200 || data.code === 0) {
return data
return response
}
// 处理业务错误

View File

@ -23,6 +23,7 @@ export interface User {
username: string
email: string
phone?: string
nickname?: string
avatar?: string
role: 'student' | 'teacher' | 'admin'
status: 'active' | 'inactive' | 'banned'

View File

@ -57,7 +57,7 @@
</div>
<!-- 自定义控制栏 -->
<div v-if="showControls && !error" class="video-controls" :class="{ 'controls-visible': controlsVisible }">
<div v-if="controlsVisible && !error" class="video-controls" :class="{ 'controls-visible': controlsVisible }">
<!-- 进度条 -->
<div class="progress-container" @click="seekTo" @mousemove="showProgressPreview" @mouseleave="hideProgressPreview">
<div class="progress-track">
@ -336,7 +336,7 @@ const loadVideo = () => {
loading.value = false
})
hls.on(Hls.Events.ERROR, (event, data) => {
hls.on(Hls.Events.ERROR, (_event, data) => {
console.error('HLS error:', data)
if (data.fatal) {
error.value = true

View File

@ -1,27 +1,9 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { CourseApi } from '@/api/modules/course'
import type { Course as ApiCourse } from '@/api/types'
export interface Course {
id: number
title: string
description: string
instructor: string
instructorAvatar?: string
thumbnail: string
price: number
originalPrice?: number
rating: number
studentsCount: number
duration: string
level: 'beginner' | 'intermediate' | 'advanced'
category: string
tags: string[]
createdAt: string
updatedAt: string
isEnrolled?: boolean
progress?: number
}
export type Course = ApiCourse
export interface Lesson {
id: number
@ -54,12 +36,12 @@ export const useCourseStore = defineStore('course', () => {
filtered = filtered.filter(course =>
course.title.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
course.description.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
course.instructor.toLowerCase().includes(searchQuery.value.toLowerCase())
course.instructor.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
}
if (selectedCategory.value) {
filtered = filtered.filter(course => course.category === selectedCategory.value)
filtered = filtered.filter(course => course.category.name === selectedCategory.value)
}
if (selectedLevel.value) {
@ -90,53 +72,136 @@ export const useCourseStore = defineStore('course', () => {
id: 1,
title: 'Vue.js 3 完整教程',
description: '从零开始学习Vue.js 3包括Composition API、TypeScript集成等现代开发技术',
instructor: '李老师',
instructorAvatar: 'https://via.placeholder.com/50',
content: '详细的Vue.js 3课程内容',
instructor: {
id: 1,
name: '李老师',
title: '前端开发专家',
bio: '资深前端开发工程师',
avatar: 'https://via.placeholder.com/50',
rating: 4.8,
studentsCount: 1234,
coursesCount: 5,
experience: '5年前端开发经验',
education: ['计算机科学学士'],
certifications: ['Vue.js认证']
},
thumbnail: 'https://via.placeholder.com/300x200',
coverImage: 'https://via.placeholder.com/300x200',
price: 299,
originalPrice: 399,
currency: 'CNY',
rating: 4.8,
ratingCount: 100,
studentsCount: 1234,
duration: '12小时',
level: 'intermediate',
category: '前端开发',
category: {
id: 1,
name: '前端开发',
slug: 'frontend',
description: '前端开发相关课程'
},
tags: ['Vue.js', 'JavaScript', 'TypeScript'],
totalLessons: 20,
language: 'zh-CN',
skills: ['Vue.js', 'TypeScript', 'Composition API'],
requirements: ['JavaScript基础', 'HTML/CSS基础'],
objectives: ['掌握Vue.js 3核心概念', '学会使用Composition API', '理解TypeScript集成'],
status: 'published',
createdAt: '2024-01-01',
updatedAt: '2024-01-15'
updatedAt: '2024-01-15',
publishedAt: '2024-01-01'
},
{
id: 2,
title: 'React 18 实战开发',
description: '掌握React 18的新特性包括并发渲染、Suspense等高级功能',
instructor: '王老师',
instructorAvatar: 'https://via.placeholder.com/50',
thumbnail: 'https://via.placeholder.com/300x200',
price: 399,
content: '详细的React 18课程内容',
instructor: {
id: 2,
name: '王老师',
title: 'React专家',
bio: '资深React开发工程师',
avatar: 'https://via.placeholder.com/50',
rating: 4.9,
studentsCount: 2156,
coursesCount: 8,
experience: '6年React开发经验',
education: ['软件工程硕士'],
certifications: ['React认证']
},
thumbnail: 'https://via.placeholder.com/300x200',
coverImage: 'https://via.placeholder.com/300x200',
price: 399,
originalPrice: 499,
currency: 'CNY',
rating: 4.9,
ratingCount: 200,
studentsCount: 2156,
duration: '15小时',
level: 'advanced',
category: '前端开发',
category: {
id: 1,
name: '前端开发',
slug: 'frontend',
description: '前端开发相关课程'
},
tags: ['React', 'JavaScript', 'Hooks'],
totalLessons: 25,
language: 'zh-CN',
skills: ['React 18', 'Hooks', '并发渲染'],
requirements: ['JavaScript基础', 'React基础'],
objectives: ['掌握React 18新特性', '学会并发渲染', '理解Suspense'],
status: 'published',
createdAt: '2024-01-05',
updatedAt: '2024-01-20'
updatedAt: '2024-01-20',
publishedAt: '2024-01-05'
},
{
id: 3,
title: 'Node.js 后端开发',
description: '学习Node.js后端开发包括Express、数据库操作、API设计等',
instructor: '张老师',
instructorAvatar: 'https://via.placeholder.com/50',
thumbnail: 'https://via.placeholder.com/300x200',
price: 349,
content: '详细的Node.js课程内容',
instructor: {
id: 3,
name: '张老师',
title: 'Node.js专家',
bio: '资深后端开发工程师',
avatar: 'https://via.placeholder.com/50',
rating: 4.7,
studentsCount: 987,
coursesCount: 6,
experience: '7年后端开发经验',
education: ['计算机科学硕士'],
certifications: ['Node.js认证']
},
thumbnail: 'https://via.placeholder.com/300x200',
coverImage: 'https://via.placeholder.com/300x200',
price: 349,
originalPrice: 449,
currency: 'CNY',
rating: 4.7,
ratingCount: 150,
studentsCount: 987,
duration: '18小时',
level: 'intermediate',
category: '后端开发',
category: {
id: 2,
name: '后端开发',
slug: 'backend',
description: '后端开发相关课程'
},
tags: ['Node.js', 'Express', 'MongoDB'],
totalLessons: 30,
language: 'zh-CN',
skills: ['Node.js', 'Express', 'MongoDB', 'API设计'],
requirements: ['JavaScript基础', '编程基础'],
objectives: ['掌握Node.js后端开发', '学会Express框架', '理解数据库操作'],
status: 'published',
createdAt: '2024-01-10',
updatedAt: '2024-01-25'
updatedAt: '2024-01-25',
publishedAt: '2024-01-10'
}
]
courses.value = mockCourses

View File

@ -18,12 +18,12 @@ export const useUserStore = defineStore('user', () => {
const isAdmin = computed(() => user.value?.role === 'admin')
// 方法 - 简化版本,主要用于状态管理
const login = async (credentials: { email: string; password: string }) => {
const login = async (_credentials: { email: string; password: string }) => {
// 这个方法现在主要用于兼容性,实际登录逻辑在组件中处理
return { success: true, message: '请使用登录模态框进行登录' }
}
const register = async (userData: any) => {
const register = async (_userData: any) => {
// 这个方法现在主要用于兼容性,实际注册逻辑在组件中处理
return { success: true, message: '请使用注册模态框进行注册' }
}

View File

@ -42,49 +42,47 @@
class="activity-card"
>
<div class="card-header">
<div class="card-background">
<div class="year-badge">2025</div>
<div class="course-title">{{ activity.title }}</div>
<div class="course-subtitle">{{ activity.subtitle }}</div>
<div class="card-image">
<img
src="/images/activity/活动图1.png"
alt="活动图片"
class="activity-image"
/>
</div>
</div>
<div class="card-body">
<!-- 特色标签 -->
<div class="feature-tags">
<span
v-for="tag in activity.tags"
:key="tag"
class="feature-tag"
>
<i class="tag-icon"></i>
{{ tag }}
</span>
<!-- 状态标签 -->
<div class="status-badge">
<span class="status-text">进行中</span>
</div>
<!-- 课程信息 -->
<div class="course-info">
<div class="info-row">
<span class="info-label">{{ activity.courseTitle }}</span>
<!-- 课程标题 -->
<h3 class="course-main-title">{{ activity.courseTitle }}</h3>
<!-- 课程副标题 -->
<p class="course-subtitle-text">{{ activity.subtitle }}</p>
<!-- 课程详细信息 -->
<div class="course-details">
<div class="detail-item">
<span class="detail-label">活动时间</span>
<span class="detail-value">{{ activity.schedule.replace('开课时间:', '') }}</span>
</div>
<div class="info-row">
<span class="info-text">{{ activity.schedule }}</span>
<div class="detail-item">
<span class="detail-label">活动类型</span>
<span class="detail-value">激励类活动</span>
</div>
<div class="info-row">
<span class="info-text">{{ activity.duration }}</span>
</div>
<div class="info-row">
<span class="info-text">{{ activity.students }}</span>
</div>
<div class="info-row">
<span class="price">{{ activity.price }}</span>
<div class="detail-item">
<span class="detail-label">已报名</span>
<span class="detail-value">{{ activity.students.replace('已报名:', '') }}</span>
</div>
</div>
<!-- 查看详情按钮 -->
<div class="card-footer">
<button class="detail-btn" @click="viewDetail(activity.id)">
查看详情
查看活动
</button>
</div>
</div>
@ -103,7 +101,7 @@ import { ref, computed, onMounted } from 'vue'
const loading = ref(true)
// -
const bannerImageSrc = ref('')
const bannerImageSrc = ref('/images/activity/活动切图-轮播区.png')
//
const hasBannerImage = computed(() => bannerImageSrc.value.trim() !== '')
@ -114,7 +112,6 @@ const activities = ref([
id: 1,
title: '计算机二级',
subtitle: 'C语言讲练综合班',
tags: ['系统备考', '考点详解', '题考刷题'],
courseTitle: '计算机二级C语言程序设计证书',
schedule: '开课时间2025.07.26-2025.09.28',
duration: '适合年级:高校本科生',
@ -125,7 +122,6 @@ const activities = ref([
id: 2,
title: '计算机二级',
subtitle: 'C语言讲练综合班',
tags: ['系统备考', '考点详解', '题考刷题'],
courseTitle: '计算机二级C语言程序设计证书',
schedule: '开课时间2025.07.26-2025.09.28',
duration: '适合年级:高校本科生',
@ -136,7 +132,6 @@ const activities = ref([
id: 3,
title: '计算机二级',
subtitle: 'C语言讲练综合班',
tags: ['系统备考', '考点详解', '题考刷题'],
courseTitle: '计算机二级C语言程序设计证书',
schedule: '开课时间2025.07.26-2025.09.28',
duration: '适合年级:高校本科生',
@ -147,7 +142,6 @@ const activities = ref([
id: 4,
title: '计算机二级',
subtitle: 'C语言讲练综合班',
tags: ['系统备考', '考点详解', '题考刷题'],
courseTitle: '计算机二级C语言程序设计证书',
schedule: '开课时间2025.07.26-2025.09.28',
duration: '适合年级:高校本科生',
@ -158,7 +152,6 @@ const activities = ref([
id: 5,
title: '计算机二级',
subtitle: 'C语言讲练综合班',
tags: ['系统备考', '考点详解', '题考刷题'],
courseTitle: '计算机二级C语言程序设计证书',
schedule: '开课时间2025.07.26-2025.09.28',
duration: '适合年级:高校本科生',
@ -169,7 +162,6 @@ const activities = ref([
id: 6,
title: '计算机二级',
subtitle: 'C语言讲练综合班',
tags: ['系统备考', '考点详解', '题考刷题'],
courseTitle: '计算机二级C语言程序设计证书',
schedule: '开课时间2025.07.26-2025.09.28',
duration: '适合年级:高校本科生',
@ -186,9 +178,9 @@ const viewDetail = (id: number) => {
}
// 使
const setBannerImage = (imagePath: string) => {
bannerImageSrc.value = imagePath
}
// const setBannerImage = (imagePath: string) => {
// bannerImageSrc.value = imagePath
// }
//
onMounted(() => {
@ -318,51 +310,44 @@ onMounted(() => {
/* 卡片头部 */
.card-header {
position: relative;
height: 120px;
height: 180px;
overflow: hidden;
border-radius: 12px 12px 0 0;
}
.card-background {
.card-image {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #4ECDC4 0%, #44A08D 100%);
position: relative;
}
.activity-image {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
display: block;
}
.card-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
padding: 20px;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
}
.card-background::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 80px;
height: 80px;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
transform: translate(30px, -30px);
}
.card-background::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 60px;
height: 60px;
background: rgba(255, 255, 255, 0.08);
border-radius: 50%;
transform: translate(-20px, 20px);
}
.year-badge {
position: absolute;
top: 15px;
left: 20px;
background: rgba(255, 255, 255, 0.9);
color: #44A08D;
background: #9ACD32;
color: #333;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
@ -385,64 +370,62 @@ onMounted(() => {
z-index: 1;
}
/* 卡片主体 */
.card-body {
padding: 20px;
}
.feature-tags {
display: flex;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
/* 状态标签 */
.status-badge {
margin-bottom: 12px;
}
.feature-tag {
display: flex;
align-items: center;
gap: 4px;
background: #FFF2E6;
color: #FF6B35;
padding: 4px 8px;
border-radius: 4px;
.status-text {
display: inline-block;
background: #FF6B35;
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.tag-icon {
font-size: 10px;
font-style: normal;
}
.course-info {
margin-bottom: 20px;
}
.info-row {
margin-bottom: 8px;
font-size: 13px;
color: #666;
/* 课程标题 */
.course-main-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0 0 8px 0;
line-height: 1.4;
}
.info-row:first-child {
margin-bottom: 12px;
}
.info-label {
font-weight: 600;
color: #333;
/* 课程副标题 */
.course-subtitle-text {
font-size: 14px;
}
.info-text {
color: #666;
}
.price {
color: #FF6B35;
font-weight: 600;
font-size: 14px;
margin: 0 0 16px 0;
line-height: 1.4;
}
/* 课程详细信息 */
.course-details {
margin-bottom: 20px;
}
.detail-item {
margin-bottom: 8px;
font-size: 13px;
line-height: 1.4;
}
.detail-label {
color: #999;
}
.detail-value {
color: #666;
}
/* 卡片底部 */
@ -452,9 +435,9 @@ onMounted(() => {
.detail-btn {
width: 100%;
padding: 10px 20px;
background: #4A90E2;
color: white;
padding: 12px 20px;
background: #f5f5f5;
color: #666;
border: none;
border-radius: 6px;
font-size: 14px;
@ -464,7 +447,8 @@ onMounted(() => {
}
.detail-btn:hover {
background: #357ABD;
background: #4A90E2;
color: white;
transform: translateY(-1px);
}
@ -573,6 +557,10 @@ onMounted(() => {
.placeholder-text {
font-size: 20px;
}
.card-header {
height: 160px;
}
}
@media (max-width: 768px) {
@ -604,6 +592,10 @@ onMounted(() => {
.container {
padding: 0 16px;
}
.card-header {
height: 140px;
}
}
@media (max-width: 480px) {

View File

@ -87,6 +87,7 @@
:size="60"
/>
</div>
<div class="instructor-info">
<div class="instructor-name">{{ course.instructor.name }}</div>
<div class="instructor-title">{{ course.instructor.title }}</div>
@ -258,7 +259,7 @@
</span>
</div>
<div v-if="chapter.expanded" class="chapter-lessons">
<div v-for="(section, sectionIndex) in chapter.sections" :key="section.id" class="lesson-item">
<div v-for="section in chapter.sections" :key="section.id" class="lesson-item">
<div class="lesson-info" @click="handleSectionClick(section)">
<span class="lesson-type" :class="getLessonTypeClass(section)">
{{ getLessonTypeText(section) }}
@ -458,17 +459,17 @@ const generateChapterGroups = () => {
}
//
const getChapterTitle = (chapterIndex: number): string => {
const titles = [
'课前准备',
'程序设计基础知识',
'程序的控制结构',
'大话吉模型介绍',
'DeepSeek实际应用',
'DeepSeek实际应用'
]
return titles[chapterIndex - 1] || '课程内容'
}
// const getChapterTitle = (chapterIndex: number): string => {
// const titles = [
// '',
// '',
// '',
// '',
// 'DeepSeek',
// 'DeepSeek'
// ]
// return titles[chapterIndex - 1] || ''
// }
//
const previewModalVisible = ref(false)
@ -585,13 +586,13 @@ const toggleChapter = (chapterIndex: number) => {
}
//
const formatDuration = (sortOrder: number): string => {
//
const baseDuration = 5 + (sortOrder * 3) // 5 + *3
const minutes = baseDuration % 60
const seconds = (sortOrder * 17) % 60 //
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
}
// const formatDuration = (sortOrder: number): string => {
// //
// const baseDuration = 5 + (sortOrder * 3) // 5 + *3
// const minutes = baseDuration % 60
// const seconds = (sortOrder * 17) % 60 //
// return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
// }
//
const getLessonTypeClass = (section: CourseSection): string => {
@ -696,18 +697,18 @@ const navigateToEnrolledArea = (videoUrl: string, sectionName: string) => {
}
//
const updateVideoPlayer = (videoUrl: string, sectionName: string) => {
console.log('更新视频播放器:', { videoUrl, sectionName })
// const updateVideoPlayer = (videoUrl: string, sectionName: string) => {
// console.log(':', { videoUrl, sectionName })
//
// 线
// //
// // 线
//
const confirmed = confirm(`即将播放视频: ${sectionName}\n是否继续`)
if (confirmed) {
navigateToEnrolledArea(videoUrl, sectionName)
}
}
// //
// const confirmed = confirm(`: ${sectionName}\n`)
// if (confirmed) {
// navigateToEnrolledArea(videoUrl, sectionName)
// }
// }
//
const previewSection = (section: CourseSection) => {

View File

@ -597,21 +597,46 @@ const setDefaultCourseInfo = () => {
id: courseId.value,
title: 'C语言程序设计',
description: '<p>本课程将带你从零开始学习C语言程序设计掌握编程基础知识。</p>',
category: '信息技术',
instructor: '教师',
duration: 3600,
content: '详细的C语言课程内容',
category: {
id: 3,
name: '信息技术',
slug: 'it',
description: '信息技术相关课程'
},
instructor: {
id: 4,
name: '教师',
title: 'C语言专家',
bio: '资深C语言开发工程师',
avatar: '',
rating: 4.5,
studentsCount: 1000,
coursesCount: 3,
experience: '10年C语言教学经验',
education: ['计算机科学博士'],
certifications: ['C语言认证']
},
thumbnail: '',
coverImage: '',
duration: '60小时',
level: 'beginner',
price: 0,
originalPrice: 0,
currency: 'CNY',
rating: 4.5,
ratingCount: 100,
studentsCount: 1000,
lessonsCount: 20,
thumbnail: '',
totalLessons: 20,
language: 'zh-CN',
skills: ['C语言', '程序设计', '算法'],
requirements: ['计算机基础'],
objectives: ['掌握C语言语法', '学会程序设计', '理解算法思维'],
tags: ['编程', 'C语言'],
createdAt: Date.now(),
updatedAt: Date.now(),
isEnrolled: true,
progress: 0
status: 'published',
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
publishedAt: '2024-01-01'
}
}
}
@ -633,7 +658,7 @@ const onVideoEnded = () => {
}
}
const onVideoTimeUpdate = (time: number) => {
const onVideoTimeUpdate = (_time: number) => {
//
totalStudyTime.value += 1
}
@ -764,7 +789,7 @@ const replyToComment = (commentId: number) => {
const likeReply = (commentId: number, replyId: number) => {
const comment = comments.value.find(c => c.id === commentId)
if (comment && comment.replies) {
const reply = comment.replies.find(r => r.id === replyId)
const reply = comment.replies.find((r: any) => r.id === replyId)
if (reply) {
if (reply.liked) {
reply.likes = (reply.likes || 0) - 1

View File

@ -45,8 +45,12 @@
>
<div class="card-header">
<div class="avatar-container">
<!-- 头像占位 -->
<div class="avatar-placeholder"></div>
<!-- 师资头像 -->
<img
:src="teacher.avatar"
:alt="teacher.name"
class="teacher-avatar"
/>
<div v-if="teacher.featured" class="featured-badge">金牌讲师</div>
</div>
<div class="card-arrow">
@ -57,11 +61,9 @@
</div>
<div class="card-content">
<h3 class="teacher-name">{{ teacher.name }}</h3>
<p class="teacher-title">{{ teacher.title }}</p>
<p class="teacher-position">{{ teacher.position }}</p>
<p class="teacher-description">{{ teacher.description }}</p>
<div class="teacher-tags">
<span v-for="tag in teacher.tags" :key="tag" class="tag">{{ tag }}</span>
</div>
<div class="teacher-specialty">{{ teacher.specialty }}</div>
</div>
</div>
</div>
@ -94,13 +96,13 @@
import { ref, computed } from 'vue'
//
const bannerImageSrc = ref('')
const bannerImageSrc = ref('/images/Teachers/师资力量切图-轮播区.png')
const hasBannerImage = computed(() => bannerImageSrc.value.trim() !== '')
// 使
const setBannerImage = (imagePath: string) => {
bannerImageSrc.value = imagePath
}
// const setBannerImage = (imagePath: string) => {
// bannerImageSrc.value = imagePath
// }
//
const filterTabs = ref([
@ -121,59 +123,67 @@ const teachers = ref([
{
id: 1,
name: '黄天羽',
title: '注册国际人才测评师资格认证',
description: '注册国际企业学习设计师 认证',
tags: ['主讲', '资深人才测评师'],
featured: true
position: '北京理工大学计算机学院教授',
description: '北京市高等学校育年教学名师,博导',
specialty: '主讲 - 软件工程基础训练',
featured: true,
avatar: '/images/Teachers/师资力量1.png'
},
{
id: 2,
name: '蓝天',
title: '北京理工大学MBA企业文化专家顾问',
position: '北京理工大学MBA企业文化专家顾问',
description: '多家知名上市企业高管',
tags: ['主讲', 'MBA企业文化专家']
specialty: '主讲 - MBA企业文化专家',
avatar: '/images/Teachers/师资力量2.png'
},
{
id: 3,
name: '万精云',
title: '中国人事科学',
position: '中国人事科学研究院研究员',
description: '中国科学院博士',
tags: ['主讲', '人事专家']
specialty: '主讲 - 人事管理专家',
avatar: '/images/Teachers/师资力量3.png'
},
{
id: 4,
name: '张庆勋',
title: '北京大学博士',
description: '内蒙古财经大学',
tags: ['主讲', '金牌讲师']
position: '北京大学博士',
description: '内蒙古财经大学教授',
specialty: '主讲 - 金融学专家',
avatar: '/images/Teachers/师资力量4.png'
},
{
id: 5,
name: '程毅',
title: '中国科技大学博士研究生',
description: '',
tags: ['主讲', '科技专家']
position: '中国科技大学博士研究生导师',
description: '计算机科学与技术专业带头人',
specialty: '主讲 - 计算机科学专家',
avatar: '/images/Teachers/师资力量5.png'
},
{
id: 6,
name: '王德华',
title: '数字经济与金融研究中心专家',
position: '数字经济与金融研究中心专家',
description: '多家知名上市企业高级管理顾问',
tags: ['主讲', '数字经济专家']
specialty: '主讲 - 数字经济专家',
avatar: '/images/Teachers/师资力量6.png'
},
{
id: 7,
name: '马前程',
title: '清华大学管理学院',
position: '清华大学管理学院教授',
description: '多家一线互联网企业高级管理顾问',
tags: ['主讲', '清华大学管理专家']
specialty: '主讲 - 企业管理专家',
avatar: '/images/Teachers/师资力量7.png'
},
{
id: 8,
name: '陈宇',
title: '知名上市企业高级管理顾问专家',
position: '知名上市企业高级管理顾问专家',
description: '多家一线互联网企业高级管理顾问',
tags: ['主讲', '企业管理专家']
specialty: '主讲 - 企业战略专家',
avatar: '/images/Teachers/师资力量8.png'
}
])
@ -246,18 +256,15 @@ const goToPage = (page: number) => {
.banner-image-container {
width: 100%;
height: 400px;
height: auto;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.banner-image {
width: 100%;
height: 100%;
height: auto;
display: block;
object-fit: cover;
object-position: center;
}
.banner-placeholder {
@ -364,6 +371,7 @@ const goToPage = (page: number) => {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s;
cursor: pointer;
aspect-ratio: 3/4;
}
.faculty-card:hover {
@ -373,7 +381,7 @@ const goToPage = (page: number) => {
.card-header {
position: relative;
height: 200px;
height: 70%;
background: #f5f5f5;
display: flex;
align-items: center;
@ -386,26 +394,18 @@ const goToPage = (page: number) => {
height: 100%;
}
.avatar-placeholder {
.teacher-avatar {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #e0e0e0 0%, #f0f0f0 100%);
display: flex;
align-items: center;
justify-content: center;
}
.avatar-placeholder::after {
content: '头像占位';
color: #999;
font-size: 14px;
object-fit: cover;
object-position: center;
}
.featured-badge {
position: absolute;
top: 12px;
left: 12px;
background: #FF6B35;
top: 8px;
left: 8px;
background: #E53E3E;
color: white;
padding: 4px 8px;
border-radius: 4px;
@ -415,59 +415,60 @@ const goToPage = (page: number) => {
.card-arrow {
position: absolute;
top: 50%;
bottom: 16px;
right: 16px;
transform: translateY(-50%);
color: #4A90E2;
background: white;
width: 32px;
height: 32px;
color: white;
background: #4A90E2;
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 8px rgba(74, 144, 226, 0.3);
transition: all 0.3s;
}
.card-arrow:hover {
background: #357ABD;
transform: scale(1.1);
}
.card-content {
padding: 20px;
padding: 16px;
height: 30%;
display: flex;
flex-direction: column;
justify-content: flex-start;
text-align: left;
}
.teacher-name {
font-size: 18px;
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0 0 8px 0;
margin: 0 0 6px 0;
line-height: 1.2;
}
.teacher-title {
font-size: 14px;
.teacher-position {
font-size: 12px;
color: #666;
margin: 0 0 8px 0;
line-height: 1.4;
margin: 0 0 4px 0;
line-height: 1.3;
}
.teacher-description {
font-size: 13px;
color: #999;
margin: 0 0 12px 0;
line-height: 1.4;
min-height: 18px;
}
.teacher-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.tag {
background: #f0f8ff;
color: #4A90E2;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
border: 1px solid #e6f3ff;
color: #666;
margin: 0 0 6px 0;
line-height: 1.3;
}
.teacher-specialty {
font-size: 11px;
color: #999;
line-height: 1.3;
}
/* 分页组件 */
@ -528,10 +529,6 @@ const goToPage = (page: number) => {
}
@media (max-width: 1024px) {
.banner-image-container {
height: 350px;
}
.placeholder-icon {
font-size: 40px;
}
@ -557,10 +554,6 @@ const goToPage = (page: number) => {
font-size: 13px;
}
.banner-image-container {
height: 300px;
}
.placeholder-icon {
font-size: 36px;
}
@ -587,10 +580,6 @@ const goToPage = (page: number) => {
padding: 20px 0 40px;
}
.banner-image-container {
height: 250px;
}
.placeholder-icon {
font-size: 32px;
}

View File

@ -230,7 +230,7 @@ import RegisterModal from '@/components/auth/RegisterModal.vue'
const { t, locale } = useI18n()
const router = useRouter()
const courseStore = useCourseStore()
const { loginModalVisible, registerModalVisible, enrollCourse, handleAuthSuccess } = useAuth()
const { loginModalVisible, registerModalVisible, handleAuthSuccess } = useAuth()
//
const bannerImage = computed(() => {

View File

@ -1,23 +1,13 @@
<template>
<div class="resources-page">
<!-- 轮播图区域 -->
<!-- 横幅图区域 -->
<div class="banner-section">
<div class="banner-container">
<div class="banner-slide active">
<div class="banner-image">
<!-- 轮播图占位 -->
<div class="banner-placeholder"></div>
</div>
<div class="banner-content">
<h2 class="banner-title">海量资源聚合您的一站式数字资源库</h2>
</div>
</div>
<!-- 轮播指示器 -->
<div class="banner-indicators">
<span class="indicator active"></span>
<span class="indicator"></span>
<span class="indicator"></span>
</div>
<img
src="/images/Featured_resources/精选资源轮播.png"
alt="精选资源横幅"
class="banner-image"
/>
</div>
</div>
@ -34,7 +24,11 @@
class="featured-card"
>
<div class="card-image">
<div class="image-placeholder"></div>
<img
:src="video.image"
:alt="video.title"
class="video-thumbnail"
/>
<div class="play-button">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M8 5V19L19 12L8 5Z" fill="white"/>
@ -70,7 +64,11 @@
class="video-card"
>
<div class="card-image">
<div class="image-placeholder"></div>
<img
:src="video.image"
:alt="video.title"
class="video-thumbnail"
/>
<div class="play-button">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<path d="M8 5V19L19 12L8 5Z" fill="white"/>
@ -109,7 +107,11 @@
class="image-card"
>
<div class="card-image">
<div class="image-placeholder"></div>
<img
:src="image.image"
:alt="image.title"
class="image-thumbnail"
/>
</div>
<div class="card-content">
<h3 class="card-title">{{ image.title }}</h3>
@ -130,9 +132,21 @@ import { ref } from 'vue'
//
const featuredVideos = ref([
{ id: 1, title: '西安工业大学内部资源之一' },
{ id: 2, title: '华南工业大学内部资源之一' },
{ id: 3, title: '西安工业大学内部资源之一' }
{
id: 1,
title: '西安工业大学内部资源之一',
image: '/images/Featured_resources/精选视频1.png'
},
{
id: 2,
title: '华南工业大学内部资源之一',
image: '/images/Featured_resources/精选视频2.png'
},
{
id: 3,
title: '西安工业大学内部资源之一',
image: '/images/Featured_resources/精品视频3.png'
}
])
//
@ -148,14 +162,46 @@ const activeVideoTab = ref('all')
//
const allVideos = ref([
{ id: 1, title: '北京工业大学内部资源之一' },
{ id: 2, title: '北京工业大学内部资源之一' },
{ id: 3, title: '西安工业大学内部资源之一' },
{ id: 4, title: '北京工业大学内部资源之一' },
{ id: 5, title: '中国工业大学内部资源之一' },
{ id: 6, title: '西安工业大学内部资源之一' },
{ id: 7, title: '西安工业大学内部资源之一' },
{ id: 8, title: '内蒙古工业大学内部资源之一' }
{
id: 1,
title: '北京工业大学内部资源之一',
image: '/images/Featured_resources/全部视频1.png'
},
{
id: 2,
title: '北京工业大学内部资源之一',
image: '/images/Featured_resources/全部视频2.png'
},
{
id: 3,
title: '西安工业大学内部资源之一',
image: '/images/Featured_resources/全部视频3.png'
},
{
id: 4,
title: '北京工业大学内部资源之一',
image: '/images/Featured_resources/全部视频4.png'
},
{
id: 5,
title: '中国工业大学内部资源之一',
image: '/images/Featured_resources/全部视频5.png'
},
{
id: 6,
title: '西安工业大学内部资源之一',
image: '/images/Featured_resources/全部视频6.png'
},
{
id: 7,
title: '西安工业大学内部资源之一',
image: '/images/Featured_resources/全部视频7.png'
},
{
id: 8,
title: '内蒙古工业大学内部资源之一',
image: '/images/Featured_resources/全部视频8.png'
}
])
//
@ -171,14 +217,46 @@ const activeImageTab = ref('all')
//
const allImages = ref([
{ id: 1, title: '中国工业大学内部资源之一' },
{ id: 2, title: '西安工业大学内部资源之一' },
{ id: 3, title: '西安工业大学内部资源之一' },
{ id: 4, title: '内蒙古工业大学内部资源之一' },
{ id: 5, title: '北京工业大学内部资源之一' },
{ id: 6, title: '北京工业大学内部资源之一' },
{ id: 7, title: '西安工业大学内部资源之一' },
{ id: 8, title: '内蒙古工业大学内部资源之一' }
{
id: 1,
title: '中国工业大学内部资源之一',
image: '/images/Featured_resources/全部图片1.png'
},
{
id: 2,
title: '西安工业大学内部资源之一',
image: '/images/Featured_resources/全部图片2.png'
},
{
id: 3,
title: '西安工业大学内部资源之一',
image: '/images/Featured_resources/全部图片3.png'
},
{
id: 4,
title: '内蒙古工业大学内部资源之一',
image: '/images/Featured_resources/全部图片4.png'
},
{
id: 5,
title: '北京工业大学内部资源之一',
image: '/images/Featured_resources/全部图片5.png'
},
{
id: 6,
title: '北京工业大学内部资源之一',
image: '/images/Featured_resources/全部图片6.png'
},
{
id: 7,
title: '西安工业大学内部资源之一',
image: '/images/Featured_resources/全部图片7.png'
},
{
id: 8,
title: '内蒙古工业大学内部资源之一',
image: '/images/Featured_resources/全部图片8.png'
}
])
</script>
@ -188,89 +266,24 @@ const allImages = ref([
background: #f8f9fa;
}
/* 轮播图区域 */
/* 横幅图区域 */
.banner-section {
position: relative;
height: 400px;
width: 100%;
overflow: hidden;
}
.banner-container {
position: relative;
width: 100%;
height: 100%;
}
.banner-slide {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
height: auto;
}
.banner-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.banner-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
}
.banner-placeholder::after {
content: '轮播图占位';
color: rgba(255, 255, 255, 0.7);
font-size: 18px;
}
.banner-content {
position: relative;
z-index: 2;
text-align: center;
color: white;
max-width: 800px;
padding: 0 20px;
}
.banner-title {
font-size: 32px;
font-weight: 600;
margin: 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.banner-indicators {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
z-index: 3;
}
.indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.5);
cursor: pointer;
transition: all 0.3s;
}
.indicator.active {
background: white;
height: auto;
display: block;
object-fit: cover;
}
/* 主要内容区域 */
@ -289,6 +302,7 @@ const allImages = ref([
font-weight: 600;
color: #333;
margin: 0 0 30px 0;
text-align: center;
}
/* 精选视频区域 */
@ -323,6 +337,20 @@ const allImages = ref([
overflow: hidden;
}
.video-thumbnail {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.image-thumbnail {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.image-placeholder {
width: 100%;
height: 100%;
@ -378,35 +406,34 @@ const allImages = ref([
/* 筛选标签 */
.filter-tabs {
display: flex;
gap: 0;
margin-bottom: 30px;
background: white;
border-radius: 8px;
padding: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
gap: 20px;
margin: 0 auto 30px auto;
justify-content: center;
align-items: center;
width: fit-content;
}
.filter-tab {
padding: 8px 20px;
padding: 8px 16px;
border: none;
background: transparent;
color: #666;
color: #999;
font-size: 14px;
cursor: pointer;
border-radius: 6px;
border-radius: 4px;
transition: all 0.3s;
white-space: nowrap;
font-weight: 400;
}
.filter-tab:hover {
background: #f8f9fa;
color: #333;
color: #4A90E2;
}
.filter-tab.active {
background: #4A90E2;
color: white;
font-weight: 500;
}
/* 全部视频区域 */
@ -545,12 +572,14 @@ const allImages = ref([
.section-title {
font-size: 20px;
margin-bottom: 20px;
text-align: center;
}
.filter-tabs {
overflow-x: auto;
padding: 4px;
gap: 4px;
gap: 16px;
justify-content: flex-start;
padding: 0 20px;
}
.filter-tab {
@ -566,13 +595,9 @@ const allImages = ref([
}
@media (max-width: 480px) {
.banner-section {
height: 250px;
}
.banner-title {
font-size: 20px;
padding: 0 16px;
.banner-image {
max-height: 250px;
object-fit: cover;
}
.container {
@ -596,10 +621,12 @@ const allImages = ref([
.section-title {
font-size: 18px;
text-align: center;
}
.filter-tabs {
margin-bottom: 20px;
gap: 12px;
}
.filter-tab {

View File

@ -25,10 +25,10 @@
<div v-for="section in sections" :key="section.id" class="section-item">
<div class="section-info">
<strong>ID:</strong> {{ section.id }} <br>
<strong>标题:</strong> {{ section.title }} <br>
<strong>标题:</strong> {{ section.name }} <br>
<strong>课程ID:</strong> {{ section.lessonId }} <br>
<strong>排序:</strong> {{ section.sortOrder }} <br>
<strong>链接:</strong> {{ section.sectionId }} <br>
<strong>排序:</strong> {{ section.sort }} <br>
<strong>ID:</strong> {{ section.id }} <br>
<strong>层级:</strong> {{ section.level }}
</div>
</div>