feat: 添加了商品详情页的骨架屏请求效果

This commit is contained in:
2026-08-17 21:07:26 +08:00
parent d567d1a2c4
commit ebad453023
2 changed files with 186 additions and 67 deletions

View File

@@ -0,0 +1,64 @@
<template>
<view class="flex min-h-0 flex-1 flex-col overflow-hidden bg-[#f5f5f5]">
<view class="skeleton-block relative h-[300px] w-full shrink-0 overflow-hidden bg-[#e7ebef]" />
<view class="relative z-10 -mt-[30px] min-h-0 flex-1 rounded-t-[28px] bg-white px-[16px] pb-[24px] pt-[24px]">
<view class="skeleton-block relative h-[22px] w-[72%] overflow-hidden rounded-[6px] bg-[#e9edf1]" />
<view class="mt-[12px] flex gap-[8px]">
<view class="skeleton-block relative h-[20px] w-[64px] overflow-hidden rounded-[4px] bg-[#edf0f3]" />
<view class="skeleton-block relative h-[20px] w-[76px] overflow-hidden rounded-[4px] bg-[#edf0f3]" />
<view class="skeleton-block relative h-[20px] w-[56px] overflow-hidden rounded-[4px] bg-[#edf0f3]" />
</view>
<view class="mt-[18px] flex items-end">
<view class="skeleton-block relative h-[28px] w-[110px] overflow-hidden rounded-[6px] bg-[#e9edf1]" />
<view class="skeleton-block relative ml-[12px] h-[16px] w-[70px] overflow-hidden rounded-[4px] bg-[#f0f2f4]" />
</view>
<view class="mt-[24px] rounded-[12px] bg-[#f8f9fa] p-[14px]">
<view class="flex items-center">
<view class="skeleton-block relative h-[24px] w-[24px] overflow-hidden rounded-full bg-[#e5e9ed]" />
<view class="ml-[10px] min-w-0 flex-1">
<view class="skeleton-block relative h-[16px] w-[58%] overflow-hidden rounded-[4px] bg-[#e8ecef]" />
<view class="skeleton-block relative mt-[8px] h-[12px] w-[86%] overflow-hidden rounded-[4px] bg-[#eef1f3]" />
</view>
</view>
</view>
<view class="mt-[24px]">
<view class="skeleton-block relative h-[20px] w-[88px] overflow-hidden rounded-[5px] bg-[#e8ecef]" />
<view class="skeleton-block relative mt-[14px] h-[14px] w-full overflow-hidden rounded-[4px] bg-[#eef1f3]" />
<view class="skeleton-block relative mt-[10px] h-[14px] w-[92%] overflow-hidden rounded-[4px] bg-[#eef1f3]" />
<view class="skeleton-block relative mt-[10px] h-[14px] w-[76%] overflow-hidden rounded-[4px] bg-[#eef1f3]" />
</view>
</view>
<view class="flex shrink-0 items-center bg-white px-[12px] pb-[24px] pt-[12px]">
<view class="skeleton-block relative h-[28px] w-[92px] overflow-hidden rounded-[6px] bg-[#e9edf1]" />
<view class="skeleton-block relative ml-auto h-[48px] w-[120px] overflow-hidden rounded-[10px] bg-[#e4e8ec]" />
</view>
</view>
</template>
<style scoped lang="scss">
.skeleton-block {
&::after {
position: absolute;
inset: 0;
content: "";
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.7) 50%,
transparent 100%
);
transform: translateX(-100%);
animation: skeleton-shimmer 1.4s ease-in-out infinite;
}
}
@keyframes skeleton-shimmer {
100% {
transform: translateX(100%);
}
}
</style>

View File

@@ -1,58 +1,73 @@
<template>
<view class="goods-container bg-[#f5f5f5]">
<TopNavBar :title="navOpacity < 0.5 ? '' : '商品详情'" :background="`rgba(217, 238, 255, ${navOpacity})`"
:titleColor="navOpacity < 0.5 ? '#ffffff' : '#000000'"
:backIconColor="navOpacity < 0.5 ? '#ffffff' : '#000000'" />
<TopNavBar :title="solidNavBar || navOpacity >= 0.5 ? '商品详情' : ''"
:background="solidNavBar ? '#ffffff' : `rgba(217, 238, 255, ${navOpacity})`"
:titleColor="solidNavBar || navOpacity >= 0.5 ? '#000000' : '#ffffff'"
:backIconColor="solidNavBar || navOpacity >= 0.5 ? '#000000' : '#ffffff'" />
<!-- 滚动区域 -->
<scroll-view class="content-wrapper" scroll-y @scroll="handleScroll">
<ImageSwiper :border-radius="0" :height="300" :images="goodsData.commodityPhotoList" thumbnailBottom="42px" />
<GoodsDetailSkeleton v-if="loadStatus === 'loading'" />
<view class="goods-content">
<!-- 商品信息组件 -->
<GoodInfo :goodsData="goodsData" />
<!-- 地址区域 -->
<LocationCard :orderData="goodsData" />
<!-- 日期选择区域 -->
<DateSelector v-if="goodsData.orderType == 0" @showCalendar="showCalendar" :checkInDate="selectedDate.startDate"
:checkOutDate="selectedDate.endDate" :checkInDay="''" :checkOutDay="''" :nights="selectedDate.totalDays" />
<!-- 商品套餐组件 -->
<GoodPackage
v-if="goodsData.orderType != 0 && goodsData.commodityPackageConfig && goodsData.commodityPackageConfig.length > 0"
:goodsData="goodsData" />
<!-- 商品设施组件 -->
<GoodFacility v-if="goodsData.commodityEquipment && goodsData.commodityEquipment.length > 0"
:goodsData="goodsData" />
<!-- 商品详情组件 -->
<GoodDetail :goodsData="goodsData" />
</view>
</scroll-view>
<!-- 立即抢购 -->
<view class="footer border-top">
<view class="amt text-[20px] font-bold text-[#ff3d60] line-height-28 flex items-center mr-[8px]">
{{ calculatedTotalPrice }}
</view>
<view class="btn rounded-[10px] flex items-center ml-auto pl-[8px]" @click="navigateToPay(goodsData)">
<image class="icon" src="https://oss.nianxx.cn/mp/static/version_101/common/btn.png" />
<text class="text-[16px] font-[500] text-white">立即预定</text>
<view v-else-if="loadStatus === 'error' || loadStatus === 'empty'"
class="box-border flex min-h-0 flex-1 flex-col items-center justify-center px-[24px] pb-[80px] pt-[96px]">
<CustomEmpty :statusText="loadStatus === 'error' ? '商品加载失败' : '暂无商品数据'" />
<view v-if="loadStatus === 'error'"
class="mt-[20px] flex h-[40px] items-center justify-center rounded-full bg-[#ff3d60] px-[28px]"
@click="reloadGoods">
<text class="text-[14px] font-[500] text-white">重新加载</text>
</view>
</view>
<!-- 日历组件 -->
<Calender :visible="calendarVisible" mode="range" :range-require-price="true" :price-data="priceData"
@close="handleCalendarClose" @range-select="handleDateSelect" />
<template v-else>
<!-- 滚动区域 -->
<scroll-view class="content-wrapper" scroll-y @scroll="handleScroll">
<ImageSwiper :border-radius="0" :height="300" :images="goodsData.commodityPhotoList" thumbnailBottom="42px" />
<view class="goods-content">
<!-- 商品信息组件 -->
<GoodInfo :goodsData="goodsData" />
<!-- 地址区域 -->
<LocationCard :orderData="goodsData" />
<!-- 日期选择区域 -->
<DateSelector v-if="goodsData.orderType == 0" @showCalendar="showCalendar" :checkInDate="selectedDate.startDate"
:checkOutDate="selectedDate.endDate" :checkInDay="''" :checkOutDay="''" :nights="selectedDate.totalDays" />
<!-- 商品套餐组件 -->
<GoodPackage
v-if="goodsData.orderType != 0 && goodsData.commodityPackageConfig && goodsData.commodityPackageConfig.length > 0"
:goodsData="goodsData" />
<!-- 商品设施组件 -->
<GoodFacility v-if="goodsData.commodityEquipment && goodsData.commodityEquipment.length > 0"
:goodsData="goodsData" />
<!-- 商品详情组件 -->
<GoodDetail :goodsData="goodsData" />
</view>
</scroll-view>
<!-- 立即抢购 -->
<view class="footer border-top">
<view class="amt text-[20px] font-bold text-[#ff3d60] line-height-28 flex items-center mr-[8px]">
{{ calculatedTotalPrice }}
</view>
<view class="btn rounded-[10px] flex items-center ml-auto pl-[8px]" @click="navigateToPay(goodsData)">
<image class="icon" src="https://oss.nianxx.cn/mp/static/version_101/common/btn.png" />
<text class="text-[16px] font-[500] text-white">立即预定</text>
</view>
</view>
<!-- 日历组件 -->
<Calender :visible="calendarVisible" mode="range" :range-require-price="true" :price-data="priceData"
@close="handleCalendarClose" @range-select="handleDateSelect" />
</template>
</view>
</template>
<script setup>
import { ref } from "vue";
import { computed, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { goodsDetail, commodityDailyPriceList } from "@/request/api/GoodsApi";
import { DateUtils } from "@/utils";
@@ -65,12 +80,17 @@ import DateSelector from "./components/DateSelector/index.vue";
import GoodDetail from "@/components/GoodDetail/index.vue";
import GoodFacility from "./components/GoodFacility/index.vue";
import GoodPackage from "./components/GoodPackage/index.vue";
import GoodsDetailSkeleton from "./components/GoodsDetailSkeleton/index.vue";
import CustomEmpty from "@/components/CustomEmpty/index.vue";
import { useSelectedDateStore } from "@/store";
// 导航栏透明度 - 默认透明,随滚动变为不透明
const navOpacity = ref(0);
const calendarVisible = ref(false);
const goodsData = ref({});
const loadStatus = ref("loading");
const currentCommodityId = ref("");
const solidNavBar = computed(() => loadStatus.value !== "success");
// 处理滚动事件
const handleScroll = (e) => {
@@ -87,39 +107,67 @@ const selectedDate = ref({
totalDays: 1,
});
const priceData = ref([])
const priceData = ref([]);
// 计算的总价格
const calculatedTotalPrice = ref(0);
// 获取商品详情数据
const goodsInfo = async (params) => {
const res = await goodsDetail(params);
loadStatus.value = "loading";
goodsData.value = {};
calculatedTotalPrice.value = 0;
priceData.value = [];
goodsData.value = res.data;
try {
const res = await goodsDetail(params);
// 初始化计算价格
calculatedTotalPrice.value = goodsData.value.specificationPrice || 0;
if (res?.code !== 0) {
console.error("查询商品详情失败:", res);
loadStatus.value = "error";
return;
}
// 判断是酒店类型订单再获取获取商品日价格及库存
if (goodsData.value.orderType == 0) {
configGoodsData();
getGoodsDailyPrice({
commodityId: goodsData.value.commodityId,
});
}
if (
!res.data ||
typeof res.data !== "object" ||
Array.isArray(res.data) ||
Object.keys(res.data).length === 0 ||
!res.data.commodityId
) {
loadStatus.value = "empty";
return;
}
if (goodsData.value.commodityStatus !== "1") {
uni.showModal({
title: "温馨提示",
content: "您选的商品暂不可预订,请重新选择。",
success: (res) => {
if (res.confirm) {
uni.navigateBack({ delta: 1 });
}
},
});
return;
goodsData.value = res.data;
// 初始化计算价格
calculatedTotalPrice.value = goodsData.value.specificationPrice || 0;
// 判断是酒店类型订单再获取获取商品日价格及库存
if (goodsData.value.orderType == 0) {
configGoodsData();
getGoodsDailyPrice({
commodityId: goodsData.value.commodityId,
});
}
loadStatus.value = "success";
if (goodsData.value.commodityStatus !== "1") {
uni.showModal({
title: "温馨提示",
content: "您选的商品暂不可预订,请重新选择。",
success: (modalRes) => {
if (modalRes.confirm) {
uni.navigateBack({ delta: 1 });
}
},
});
}
} catch (error) {
console.error("查询商品详情失败:", error);
loadStatus.value = "error";
}
};
@@ -139,12 +187,19 @@ const getGoodsDailyPrice = async (params) => {
const selectedDateStore = useSelectedDateStore();
onLoad(({ commodityId = "1950766939442774018" }) => {
currentCommodityId.value = commodityId;
// 从store中获取选中的日期
selectedDate.value = selectedDateStore.selectedDate;
goodsInfo({ commodityId });
});
const reloadGoods = () => {
if (!currentCommodityId.value) return;
goodsInfo({ commodityId: currentCommodityId.value });
};
// 显示日历弹窗
const showCalendar = () => (calendarVisible.value = true);