更新:1.课程详情增加骨架屏;2.图书首页和图书详情增加骨架屏;

This commit is contained in:
2025-12-11 16:13:40 +08:00
parent b3d9b0c100
commit b8dd0584aa
27 changed files with 897 additions and 649 deletions

View File

@@ -24,189 +24,257 @@
<!-- 我的书单 & 推荐图书模块 -->
<view class="mine-block">
<!-- 我的书单 -->
<view class="mine-1">
<text class="mine-title">{{ $t('bookHome.block1') }}</text>
<view
v-if="myBooksList.length > 0"
class="mine-more"
@click="handleMoreClick"
>
{{ $t('bookHome.more') }}
<image src="@/static/icon/icon_right.png" />
</view>
<view v-if="myBooksList.length > 0" class="mine-1-list">
<view
v-for="(item, index) in myBooksList"
:key="index"
class="mine-item"
@click="handleMyBookClick(item.id)"
>
<image :src="item.images" />
<text>{{ item.name }}</text>
<Skeleton
ref="myBookSkeleton"
theme="image-card"
:request="getMyBooks"
:size="[{ height:'140px' }]"
class="flex-1 w-[50%]"
>
<template #content="{ data }" >
<view class="mine-1">
<text class="mine-title">{{ $t('bookHome.block1') }}</text>
<view
v-if="data.page.records.length > 0"
class="mine-more"
@click="handleMoreClick"
>
{{ $t('bookHome.more') }}
<image src="@/static/icon/icon_right.png" />
</view>
<view v-if="data.page.records.length > 0" class="mine-1-list">
<view
v-for="(item, index) in data.page.records"
:key="index"
class="mine-item"
@click="handleMyBookClick(item.id)"
>
<image :src="item.images" />
<text>{{ item.name }}</text>
</view>
</view>
<text v-else class="zanwu">{{ $t('common.data_null') }}</text>
</view>
</view>
<text v-else class="zanwu">{{ $t('common.data_null') }}</text>
</view>
</template>
</Skeleton>
<!-- 推荐图书 -->
<view class="mine-2">
<text class="mine-title">{{ $t('bookHome.block2') }}</text>
<swiper
v-if="recommendBooksList.length > 0"
autoplay
:interval="3000"
:duration="500"
class="recommend-list"
>
<swiper-item
v-for="(item, index) in recommendBooksList"
:key="index"
class="recommend-item"
@click="handleBookClick(item.id)"
>
<image :src="item.images" width="100%" height="100%" />
<text>{{ item.name }}</text>
</swiper-item>
</swiper>
</view>
<Skeleton
ref="recommendBooksSkeleton"
theme="image-card"
:request="getRecommendBooks"
:size="[{ height:'140px' }]"
class="flex-1 w-[50%]"
>
<template #content="{ data }" >
<view class="mine-2">
<text class="mine-title">{{ $t('bookHome.block2') }}</text>
<swiper
v-if="data.books.length > 0"
autoplay
:interval="3000"
:duration="500"
class="recommend-list"
>
<swiper-item
v-for="(item, index) in data.books"
:key="index"
class="recommend-item"
@click="handleBookClick(item.id)"
>
<image :src="item.images" width="100%" height="100%" />
<text>{{ item.name }}</text>
</swiper-item>
</swiper>
</view>
</template>
</Skeleton>
</view>
<!-- 活动图书模块 -->
<view v-if="showActivity" class="activity-block">
<text class="activity-title">{{ $t('bookHome.activityTitle') }}</text>
<scroll-view class="scroll-view" scroll-x :show-scrollbar="false">
<view class="activity-label-list">
<view
v-for="(item, index) in activityLabelList"
:key="index"
:class="[
'activity-label-item',
currentActivityIndex === index ? 'active-label' : ''
]"
@click="handleActivityLabelClick(item.id, index)"
>
<text>{{ item.title }}</text>
</view>
</view>
</scroll-view>
<scroll-view
v-if="activityList.length > 0"
class="scroll-view"
scroll-x
:show-scrollbar="false"
<view class="activity-block">
<Skeleton
theme="image-card"
:size="Array(5).fill({ height:'28px', width: '18%' })"
:request="getActivityLabels"
@success="getActivityLabelsSuccess"
>
<view class="activity-list">
<view
v-for="(item, index) in activityList"
:key="index"
class="activity-item"
@click="handleBookClick(item.bookId)"
<template #loading-top>
<wd-skeleton :row-col="[{ width: '45%', height: '35px' }]" animation="gradient" class="mb-2.5!" />
</template>
<template #content>
<text class="activity-title">{{ $t('bookHome.activityTitle') }}</text>
<scroll-view class="scroll-view" scroll-x :show-scrollbar="false">
<view class="activity-label-list">
<view
v-for="(item, index) in activityLabelList"
:key="index"
:class="[
'activity-label-item',
currentActivityIndex === index ? 'active-label' : ''
]"
@click="handleActivityLabelClick(item.id, index)"
>
<text>{{ item.title }}</text>
</view>
</view>
</scroll-view>
</template>
</Skeleton>
<Skeleton
ref="activityBooksSkeleton"
theme="image-card"
:auto="false"
:size="Array(4).fill({ height:'100px', width: '23%' })"
class="mt-[20rpx]!"
:request="getActivityBooks"
>
<template #content="{ data }">
<scroll-view
v-if="data.bookList.length > 0"
class="scroll-view"
scroll-x
:show-scrollbar="false"
>
<image :src="item.images" />
<text class="activity-text">{{ item.name }}</text>
</view>
</view>
</scroll-view>
<text v-else class="zanwu" style="padding: 80rpx 0">{{ $t('global.dataNull') }}</text>
<view class="activity-list">
<view
v-for="(item, index) in data.bookList"
:key="index"
class="activity-item"
@click="handleBookClick(item.bookId)"
>
<image :src="item.images" />
<text class="activity-text">{{ item.name }}</text>
</view>
</view>
</scroll-view>
<text v-else class="zanwu" style="padding: 80rpx 0">{{ $t('global.dataNull') }}</text>
</template>
</Skeleton>
</view>
<!-- 分类图书模块 -->
<view v-if="showCategory" class="book-block">
<!-- 一级分类标签 -->
<scroll-view class="scroll-view" scroll-x :show-scrollbar="false">
<view class="book-tab-one">
<view
v-for="(item, index) in categoryLevel1List"
:key="index"
:class="[
'tab-one-item',
currentLevel1Index === index ? 'tab-one-active' : ''
]"
@click="handleCategoryLevel1Click(item.id, index)"
>
<text>{{ item.title }}</text>
</view>
</view>
</scroll-view>
<!-- 二级分类标签 -->
<scroll-view
v-if="categoryLevel2List.length > 0"
class="scroll-view"
scroll-x
:show-scrollbar="false"
style="background: #fff; margin-top: 15rpx"
<view class="book-block">
<Skeleton
ref="categoryLevel1LabelSkeleton"
theme="image-card"
:size="Array(3).fill({ height:'75px', width: '32%' })"
:request="getCategoryLabels"
@success="getCategoryLabelsSuccess"
>
<view class="book-tab-two">
<view
v-for="(item, index) in categoryLevel2List"
:key="index"
:class="[
'tab-two-item',
currentLevel2Index === index ? 'tab-two-active' : ''
]"
@click="handleCategoryLevel2Click(item.id, index)"
<template #content>
<!-- 一级分类标签 -->
<scroll-view class="scroll-view" scroll-x :show-scrollbar="false">
<view class="book-tab-one">
<view
v-for="(item, index) in categoryLevel1List"
:key="index"
:class="[
'tab-one-item',
currentLevel1Index === index ? 'tab-one-active' : ''
]"
@click="handleCategoryLevel1Click(item.id, index)"
>
<text>{{ item.title }}</text>
</view>
</view>
</scroll-view>
</template>
</Skeleton>
<Skeleton
ref="categoryLevel2LabelSkeleton"
theme="image-card"
:auto="false"
:size="Array(4).fill({ height:'30px', width: '24%' })"
class="mt-[20rpx]!"
:request="getSubLabels"
@success="getSubLabelsSuccess"
>
<template #content>
<!-- 二级分类标签 -->
<scroll-view
v-if="categoryLevel2List.length > 0"
class="scroll-view"
scroll-x
:show-scrollbar="false"
style="background: #fff; margin-top: 15rpx"
>
<text>{{ item.title }}</text>
</view>
</view>
</scroll-view>
<view class="book-tab-two">
<view
v-for="(item, index) in categoryLevel2List"
:key="index"
:class="[
'tab-two-item',
currentLevel2Index === index ? 'tab-two-active' : ''
]"
@click="handleCategoryLevel2Click(item.id, index)"
>
<text>{{ item.title }}</text>
</view>
</view>
</scroll-view>
</template>
</Skeleton>
<!-- 分类图书列表 -->
<view v-if="categoryBookList.length > 0" class="book-list">
<view
v-for="(item, index) in categoryBookList"
:key="index"
class="book-item"
@click="handleBookClick(item.bookId)"
>
<image :src="item.images" />
<text class="book-text">{{ item.name }}</text>
<BookPrice :data="item" class="book-price-container" />
</view>
</view>
<text v-else class="zanwu" style="padding: 100rpx 0">{{ $t('global.dataNull') }}</text>
<Skeleton
ref="categoryBooksSkeleton"
theme="image-card"
:auto="false"
:size="Array(2).fill({ height:'240px', width: '49%' })"
:count="3"
class="mt-[20rpx]!"
:request="getBooksByLabel"
>
<template #content="{ data }">
<view v-if="data.bookList.length > 0" class="book-list">
<view
v-for="(item, index) in data.bookList"
:key="index"
class="book-item"
@click="handleBookClick(item.bookId)"
>
<image :src="item.images" />
<text class="book-text">{{ item.name }}</text>
<BookPrice :data="item" class="book-price-container" />
</view>
</view>
<text v-else class="zanwu" style="padding: 100rpx 0">{{ $t('global.dataNull') }}</text>
</template>
</Skeleton>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ref, computed } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { homeApi } from '@/api/modules/book_home'
import { bookHomeApi } from '@/api/modules/book_home'
import { getNotchHeight } from '@/utils/system'
import { useUserStore } from '@/stores/user'
import BookPrice from '@/components/book/BookPrice.vue'
import type {
IBook,
IBookWithStats,
ILabel,
IVipInfo
} from '@/types/book'
import type { ILabel } from '@/types/book'
const userStore = useUserStore()
// 状态定义
const showMyBooks = ref(false)
const showActivity = ref(false)
const showCategory = ref(false)
// 我的书单
const myBooksList = ref<IBook[]>([])
const myBookSkeleton = ref()
// 推荐图书
const recommendBooksList = ref<IBook[]>([])
const recommendBooksSkeleton = ref()
// 活动图书
const activityBooksSkeleton = ref()
const activityLabelList = ref<ILabel[]>([])
const activityList = ref<IBookWithStats[]>([])
const currentActivityIndex = ref(0)
// 分类图书
const categoryLevel1LabelSkeleton = ref()
const categoryLevel2LabelSkeleton = ref()
const categoryBooksSkeleton = ref()
const categoryLevel1List = ref<ILabel[]>([])
const categoryLevel2List = ref<ILabel[]>([])
const categoryBookList = ref<IBookWithStats[]>([])
const currentLevel1Index = ref(0)
const currentLevel2Index = ref(0)
@@ -216,96 +284,62 @@ const vipInfo = computed(() => userStore.userInfo?.userEbookVip?.[0] || null)
/**
* 获取我的书单
*/
const getMyBooks = async () => {
const res = await homeApi.getMyBooks(1, 10)
if (res && res.code === 0) {
showMyBooks.value = true
if (res.page.records && res.page.records.length > 0) {
myBooksList.value = res.page.records
}
} else {
// 未登录,跳转到登录页
uni.navigateTo({
url: '/pages/login/login'
})
}
}
const getMyBooks = () => bookHomeApi.getMyBooks(1, 10)
/**
* 获取推荐图书
*/
const getRecommendBooks = async () => {
const res = await homeApi.getRecommendBooks()
if (res.books && res.books.length > 0) {
recommendBooksList.value = res.books
}
}
const getRecommendBooks = () => bookHomeApi.getRecommendBooks()
/**
* 获取活动标签列表
* 获取活动图书
*/
const getActivityLabels = async () => {
const res = await homeApi.getBookLabelList(1)
showActivity.value = true
if (res.lableList && res.lableList.length > 0) {
activityLabelList.value = res.lableList
// 默认加载第一个标签的图书列表
await getBooksByLabel(res.lableList[0].id, 'activity')
}
// 获取活动图书标签列表
const getActivityLabels = () => bookHomeApi.getBookLabelList(1)
const getActivityLabelsSuccess = (res: any) => {
activityLabelList.value = res.lableList || []
activityBooksSkeleton.value.reload()
}
// 获取活动图书列表
const getActivityBooks = () => {
// 获取活动标签列表
const index = currentActivityIndex.value || 0
return bookHomeApi.getBooksByLabel(activityLabelList.value[index].id)
}
/**
* 获取分类标签列表
*/
const getCategoryLabels = async () => {
const res = await homeApi.getBookLabelList(0)
showCategory.value = true
if (res.lableList && res.lableList.length > 0) {
categoryLevel1List.value = res.lableList
// 默认加载第一个标签的二级标签
await getSubLabels(res.lableList[0].id, 0)
}
const getCategoryLabels = () => bookHomeApi.getBookLabelList(0)
const getCategoryLabelsSuccess = (res: any) => {
categoryLevel1List.value = res.lableList || []
currentLevel2Index.value = 0
categoryLevel2LabelSkeleton.value.reload()
}
/**
* 获取二级标签列表
*/
const getSubLabels = async (pid: number, index: number) => {
const res = await homeApi.getSubLabelList(pid)
currentLevel1Index.value = index
if (res.lableList && res.lableList.length > 0) {
categoryLevel2List.value = res.lableList
currentLevel2Index.value = 0
// 加载第一个二级标签的图书列表
await getBooksByLabel(res.lableList[0].id, 'category')
} else {
// 没有二级标签,直接加载一级标签的图书列表
categoryLevel2List.value = []
await getBooksByLabel(pid, 'category')
}
const getSubLabels = async () => {
await getCategoryLabels()
const pid = categoryLevel1List.value[currentLevel1Index.value]?.id || 0
return bookHomeApi.getSubLabelList(pid)
}
const getSubLabelsSuccess = (res: any) => {
categoryLevel2List.value = res.lableList || []
categoryBooksSkeleton.value.reload()
}
/**
* 根据标签获取图书列表
* 获取分类标签图书列表
*/
const getBooksByLabel = async (
labelId: number,
type: 'activity' | 'category'
) => {
const res = await homeApi.getBooksByLabel(labelId)
if (type === 'activity') {
if (res.bookList && res.bookList.length > 0) {
activityList.value = res.bookList
} else {
activityList.value = []
}
} else {
if (res.bookList && res.bookList.length > 0) {
categoryBookList.value = res.bookList
} else {
categoryBookList.value = []
}
}
const getBooksByLabel = () => {
// 获取当前需要加载的标签id
// 判断是否有一级标签当前选中值
const level1Id = categoryLevel1List.value[currentLevel1Index.value]?.id
// 判断是否有二级标签当前选中值
const level2Id = categoryLevel2List.value[currentLevel2Index.value]?.id
const labelId = level2Id || level1Id || 0
return bookHomeApi.getBooksByLabel(labelId)
}
/**
@@ -349,14 +383,17 @@ const handleMoreClick = () => {
*/
const handleActivityLabelClick = async (labelId: number, index: number) => {
currentActivityIndex.value = index
await getBooksByLabel(labelId, 'activity')
activityBooksSkeleton.value.reload()
}
/**
* 处理一级分类标签点击
*/
const handleCategoryLevel1Click = async (labelId: number, index: number) => {
await getSubLabels(labelId, index)
currentLevel1Index.value = index
currentLevel2Index.value = 0
categoryBooksSkeleton.value.loading = true
categoryLevel2LabelSkeleton.value.reload()
}
/**
@@ -364,27 +401,15 @@ const handleCategoryLevel1Click = async (labelId: number, index: number) => {
*/
const handleCategoryLevel2Click = async (labelId: number, index: number) => {
currentLevel2Index.value = index
await getBooksByLabel(labelId, 'category')
categoryBooksSkeleton.value.reload()
}
/**
* 页面加载
*/
onMounted(() => {
// 重置活动标签选中状态
currentActivityIndex.value = 0
showActivity.value = false
})
/**
* 页面显示
*/
onShow(() => {
// 刷新数据
getMyBooks()
getRecommendBooks()
getActivityLabels()
getCategoryLabels()
myBookSkeleton.value?.reload()
})
</script>
@@ -426,12 +451,12 @@ onShow(() => {
.mine-block {
padding: 20rpx;
display: flex;
gap: 20rpx;
align-items: center;
justify-content: space-between;
.mine-1,
.mine-2 {
width: 49%;
height: 290rpx;
border-radius: 15rpx;
padding: 30rpx;
@@ -584,7 +609,6 @@ onShow(() => {
.activity-list {
width: 100%;
margin-top: 20rpx;
padding-left: 10rpx;
display: flex;
align-items: center;
@@ -617,7 +641,6 @@ onShow(() => {
.book-block {
padding: 20rpx 20rpx 0;
.book-tab-one {
display: flex;
align-items: center;
@@ -700,7 +723,6 @@ onShow(() => {
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
margin-top: 20rpx;
.book-item {
width: 49%;