feat: 首页新增课程列表区块 + 全部课程列表页 + 课程详情页
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
import { request } from './request'
|
||||||
|
import { toPageQuery, toPagination } from './mappers'
|
||||||
|
|
||||||
|
const toCourseBrief = (backendCourse = {}) => ({
|
||||||
|
id: backendCourse.id ?? null,
|
||||||
|
title: backendCourse.title || '',
|
||||||
|
cover_image: backendCourse.cover_image || '',
|
||||||
|
updated_at: backendCourse.updated_at || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const toCourseDetail = (backendCourse = {}) => ({
|
||||||
|
...toCourseBrief(backendCourse),
|
||||||
|
detail: backendCourse.detail || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
export const courseInfoApi = {
|
||||||
|
getCourses(params = {}) {
|
||||||
|
return request({
|
||||||
|
url: '/api/courses',
|
||||||
|
data: toPageQuery(params)
|
||||||
|
}).then((data) => toPagination(data, toCourseBrief))
|
||||||
|
},
|
||||||
|
getCourseDetail(id) {
|
||||||
|
return request({
|
||||||
|
url: `/api/courses/${id}`
|
||||||
|
}).then(toCourseDetail)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -95,6 +95,19 @@
|
|||||||
"navigationBarTitleText": "课程详情"
|
"navigationBarTitleText": "课程详情"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/comprehensive/courses/list",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "全部课程",
|
||||||
|
"enablePullDownRefresh": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/comprehensive/courses/info-detail",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "课程详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/comprehensive/courses/membership-detail",
|
"path": "pages/comprehensive/courses/membership-detail",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<view class="article-detail container" v-if="course">
|
||||||
|
<text class="title">{{ course.title }}</text>
|
||||||
|
<text class="date">{{ formatDate(course.updated_at) }}</text>
|
||||||
|
<image v-if="course.cover_image" :src="course.cover_image" mode="aspectFill" class="cover" />
|
||||||
|
<view class="content-card card">
|
||||||
|
<rich-text :nodes="course.detail"></rich-text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="loading-state container">
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import { courseInfoApi } from '@/modules/comprehensive/api/courseInfo'
|
||||||
|
import { formatDate } from '@/modules/comprehensive/utils/date'
|
||||||
|
|
||||||
|
const course = ref(null)
|
||||||
|
|
||||||
|
onLoad(async (query) => {
|
||||||
|
course.value = await courseInfoApi.getCourseDetail(query.id)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.title,
|
||||||
|
.date {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: $text-color-main;
|
||||||
|
font-size: 42rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
margin-top: $spacing-sm;
|
||||||
|
color: $text-color-muted;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover {
|
||||||
|
width: 100%;
|
||||||
|
height: 360rpx;
|
||||||
|
margin: $spacing-lg 0;
|
||||||
|
border-radius: $border-radius-lg;
|
||||||
|
background: $bg-color-hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card {
|
||||||
|
color: $text-color-regular;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state {
|
||||||
|
padding: 200rpx 0;
|
||||||
|
text-align: center;
|
||||||
|
color: $text-color-light;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<view class="course-list-page container">
|
||||||
|
<view class="page-title-block">
|
||||||
|
<text class="page-title">全部课程</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="article-list">
|
||||||
|
<view v-for="course in courses" :key="course.id" class="article-card card" @tap="goDetail(course.id)">
|
||||||
|
<image :src="course.cover_image" mode="aspectFill" class="cover" />
|
||||||
|
<view class="article-info">
|
||||||
|
<text class="title">{{ course.title }}</text>
|
||||||
|
<text class="date">{{ formatDate(course.updated_at) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="isLoading" class="load-more-tip">加载中...</view>
|
||||||
|
<view v-else-if="courses.length === 0" class="empty-state">暂无课程</view>
|
||||||
|
<view v-else-if="!hasMore" class="load-more-tip">已加载全部</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||||
|
import { courseInfoApi } from '@/modules/comprehensive/api/courseInfo'
|
||||||
|
import { formatDate } from '@/modules/comprehensive/utils/date'
|
||||||
|
|
||||||
|
const courses = ref([])
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = 20
|
||||||
|
const total = ref(0)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const isLoadingMore = ref(false)
|
||||||
|
const hasMore = computed(() => courses.value.length < total.value)
|
||||||
|
|
||||||
|
onMounted(() => loadCourses(true))
|
||||||
|
|
||||||
|
onPullDownRefresh(() => {
|
||||||
|
loadCourses(true).finally(() => uni.stopPullDownRefresh())
|
||||||
|
})
|
||||||
|
|
||||||
|
onReachBottom(() => {
|
||||||
|
if (!hasMore.value || isLoadingMore.value) return
|
||||||
|
page.value += 1
|
||||||
|
loadCourses(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadCourses = async (reset) => {
|
||||||
|
if (reset) {
|
||||||
|
page.value = 1
|
||||||
|
isLoading.value = true
|
||||||
|
} else {
|
||||||
|
isLoadingMore.value = true
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data = await courseInfoApi.getCourses({ page: page.value, pageSize })
|
||||||
|
courses.value = reset ? data.items : [...courses.value, ...data.items]
|
||||||
|
total.value = data.total
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
isLoadingMore.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const goDetail = (id) => {
|
||||||
|
uni.navigateTo({ url: `/pages/comprehensive/courses/info-detail?id=${id}` })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page-title-block {
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title,
|
||||||
|
.title,
|
||||||
|
.date {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
color: $text-color-main;
|
||||||
|
font-size: 42rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-card {
|
||||||
|
display: flex;
|
||||||
|
gap: $spacing-md;
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover {
|
||||||
|
width: 190rpx;
|
||||||
|
height: 136rpx;
|
||||||
|
border-radius: $border-radius-base;
|
||||||
|
background: $bg-color-hover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: $text-color-main;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
margin-top: $spacing-md;
|
||||||
|
color: $text-color-muted;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 160rpx 0;
|
||||||
|
text-align: center;
|
||||||
|
color: $text-color-light;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -38,6 +38,24 @@
|
|||||||
<view v-else class="empty-placeholder">暂无公告活动</view>
|
<view v-else class="empty-placeholder">暂无公告活动</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view class="section-container">
|
||||||
|
<view class="section-header">
|
||||||
|
<text class="section-title">课程</text>
|
||||||
|
<text v-if="hasMoreCourses" class="section-more" @tap="go('/pages/comprehensive/courses/list')">查看全部</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="courses.length" class="activity-grid">
|
||||||
|
<view v-for="course in courses" :key="course.id" class="activity-card" @tap="go(`/pages/comprehensive/courses/info-detail?id=${course.id}`)">
|
||||||
|
<image :src="course.cover_image" mode="aspectFill" class="activity-cover" />
|
||||||
|
<view class="activity-info">
|
||||||
|
<text class="activity-title">{{ course.title }}</text>
|
||||||
|
<text class="activity-date">{{ formatDate(course.updated_at) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="empty-placeholder">暂无课程</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<VenueSwitchHeader venue-name="综合场馆" />
|
<VenueSwitchHeader venue-name="综合场馆" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -46,20 +64,28 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { contentApi } from '@/modules/comprehensive/api/content'
|
import { contentApi } from '@/modules/comprehensive/api/content'
|
||||||
|
import { courseInfoApi } from '@/modules/comprehensive/api/courseInfo'
|
||||||
import { formatDate } from '@/modules/comprehensive/utils/date'
|
import { formatDate } from '@/modules/comprehensive/utils/date'
|
||||||
import VenueSwitchHeader from '@/components/VenueSwitchHeader.vue'
|
import VenueSwitchHeader from '@/components/VenueSwitchHeader.vue'
|
||||||
|
|
||||||
const banners = ref([])
|
const banners = ref([])
|
||||||
const articles = ref([])
|
const articles = ref([])
|
||||||
|
const courses = ref([])
|
||||||
|
const hasMoreCourses = ref(false)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadPage()
|
loadPage()
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadPage = async () => {
|
const loadPage = async () => {
|
||||||
const home = await contentApi.getHomeContent({ banner_limit: 5, article_limit: 3 })
|
const [home, courseData] = await Promise.all([
|
||||||
|
contentApi.getHomeContent({ banner_limit: 5, article_limit: 3 }),
|
||||||
|
courseInfoApi.getCourses({ page: 1, pageSize: 5 })
|
||||||
|
])
|
||||||
banners.value = home.banners || []
|
banners.value = home.banners || []
|
||||||
articles.value = home.articles || []
|
articles.value = home.articles || []
|
||||||
|
courses.value = courseData.items || []
|
||||||
|
hasMoreCourses.value = courseData.total > courses.value.length
|
||||||
}
|
}
|
||||||
|
|
||||||
const go = (url) => {
|
const go = (url) => {
|
||||||
@@ -131,6 +157,11 @@ defineExpose({ refresh: loadPage })
|
|||||||
background-color: $text-color-main;
|
background-color: $text-color-main;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-more {
|
||||||
|
color: $text-color-muted;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.activity-grid {
|
.activity-grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Reference in New Issue
Block a user