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": "课程详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/comprehensive/courses/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "全部课程",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/comprehensive/courses/info-detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "课程详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/comprehensive/courses/membership-detail",
|
||||
"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>
|
||||
|
||||
<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="综合场馆" />
|
||||
</view>
|
||||
</view>
|
||||
@@ -46,20 +64,28 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { contentApi } from '@/modules/comprehensive/api/content'
|
||||
import { courseInfoApi } from '@/modules/comprehensive/api/courseInfo'
|
||||
import { formatDate } from '@/modules/comprehensive/utils/date'
|
||||
import VenueSwitchHeader from '@/components/VenueSwitchHeader.vue'
|
||||
|
||||
const banners = ref([])
|
||||
const articles = ref([])
|
||||
const courses = ref([])
|
||||
const hasMoreCourses = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
loadPage()
|
||||
})
|
||||
|
||||
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 || []
|
||||
articles.value = home.articles || []
|
||||
courses.value = courseData.items || []
|
||||
hasMoreCourses.value = courseData.total > courses.value.length
|
||||
}
|
||||
|
||||
const go = (url) => {
|
||||
@@ -131,6 +157,11 @@ defineExpose({ refresh: loadPage })
|
||||
background-color: $text-color-main;
|
||||
}
|
||||
|
||||
.section-more {
|
||||
color: $text-color-muted;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.activity-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user