This commit is contained in:
2026-04-10 15:47:09 +08:00
parent b251c899ca
commit a2fadc57b8
7 changed files with 622 additions and 255 deletions

View File

@ -1,196 +1,146 @@
<template> <template>
<view class="flex-row-start goods-item" @click.stop="$emit('clickCard', data)"> <view class="goods-item" >
<image class="goods-img" :src="data.product_pic" mode="aspectFill" /> <image @click.stop="handleBuyNow" class="goods-img" :src="data.product_pic" mode="aspectFill" />
<view class="goods-content"> <view class="fs-24 app-fc-main goods-name">
<view class="text-multi-ellipse fs-28 app-fc-main app-font-bold goods-name"> {{ data.product_name || "" }}
{{ data.product_name || "" }} </view>
</view> <view class="flex-row-start label">
<!-- <view class="flex-row-start label"> <image class="hot-icon" :src="`${imgPrefix}mall-hot.png`"></image>
<image class="hot-icon" :src="`${imgPrefix}mall-hot.png`"></image> <view class="fs-20 app-fc-main label-name">32人买过</view>
<view class="fs-20 app-fc-main label-name">{{data.sales}}人买过</view> </view>
</view> --> <view class="flex-row-between" style="margin-top: 12rpx; align-items: baseline;">
<view class="price-row"> <view class="price-wrapper">
<view class="flex-row-start"> <text class="fs-28" style="color: #FF19A0;">
<text class="fs-28 price-text"> ¥
¥ <text class="fs-28">{{
<text class="fs-28">{{data.prices[0].original_price / 100 || 0 }}</text> data.prices[0].original_price / 100 || 0
</text> }}</text>
<!-- <text class="fs-20 price-label">到手价</text> --> </text>
</view> </view>
<text class="fs-24 origin-price" v-if="minPrice.price_shichang"> <view class="buy-now-btn" @click.stop="handleBuyNow">
¥{{ minPrice.price_shichang || 0 }} 立即购买
</text> </view>
</view> </view>
<view class="buy-now-btn-wrapper" @click.stop="handleBuyNow"> </view>
<text class="buy-now-btn">立即购买1</text>
</view>
</view>
</view>
</template> </template>
<script> <script>
import { imgPrefix } from '@/utils/common'; import {
imgPrefix
} from '@/utils/common';
export default { export default {
props: { props: {
index: { index: {
type: Number, type: Number,
default: 0, default: 0,
}, },
data: { data: {
type: Object, type: Object,
default: () => {}, default: () => {},
}, },
}, },
data() { data() {
return { return {
imgPrefix, imgPrefix,
isAnimating: false, };
}; },
}, computed: {
computed: { labelList() {
// minPrice() { return (this.data?.label || "").split(",").filter((v) => !!v);
// let minPrice = {}; },
// let minPriceValue = 0; },
// this.data.price_list.map((v) => { mounted() {},
// if (!minPriceValue || minPriceValue > +v.price) { methods: {
// minPriceValue = +v.price; handleBuyNow() {
// minPrice = { ...v }; // 触发购买事件,通知父组件
// } this.$emit('addToCar', this.data);
// }); },
// return minPrice; triggerAddCartAnimation() {
// }, // 保留方法以兼容父组件调用
labelList() { }
return this.data.label.split(",").filter((v) => !!v); },
}, };
},
mounted() {},
methods: {
// 触发添加购物车动画
triggerAddCartAnimation() {
this.isAnimating = true;
setTimeout(() => {
this.isAnimating = false;
}, 600);
},
// 立即购买
handleBuyNow() {
this.$emit('buyNow', this.data);
},
},
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.goods-item { .goods-item {
background: #fff; background: #fff;
border-radius: 40rpx; border-radius: 16rpx;
width: 100%; padding: 20rpx;
padding: 20rpx; box-sizing: border-box;
box-sizing: border-box; margin-bottom: 22rpx;
margin-bottom: 20rpx; position: relative;
position: relative; width: 100%;
border-bottom: 1rpx solid #F5F5F5; min-width: 0;
overflow: hidden;
.goods-img { .goods-img {
border-radius: 20rpx; display: block;
width: 160rpx; width: 100%;
height: 160rpx; max-width: 100%;
} height: 260rpx;
border-radius: 16rpx;
background: #f5f5f5;
}
.goods-content { .goods-name {
flex: 1; margin: 16rpx 0 12rpx 0;
overflow: hidden; overflow: hidden;
margin-left: 20rpx; text-overflow: ellipsis;
} display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
word-wrap: break-word;
word-break: break-all;
font-size: 24rpx;
color: #3D3D3D;
}
.goods-name { .label {
margin: 0 0 12rpx; background-color: #ffecf3;
} display: inline-flex;
align-items: center;
border-radius: 4rpx;
padding: 4rpx 8rpx;
.label { .hot-icon {
background-color: #ffecf3; width: 28rpx;
display: inline-flex; height: 28rpx;
align-items: center; }
border-radius: 4rpx;
margin-bottom: 12rpx;
.hot-icon { .label-name {
width: 28rpx; padding-left: 4rpx;
height: 28rpx; color: #FF19A0;
margin-right: 6rpx; font-size: 20rpx;
} }
}
.label-name { .flex-row-between {
padding: 4rpx 8rpx; display: flex;
color: #FF19A0; justify-content: space-between;
} align-items: center;
} }
.price-row { .flex-row-start {
margin-top: 12rpx; display: flex;
display: flex; justify-content: flex-start;
flex-direction: column; align-items: center;
} }
.price-text { .price-wrapper {
color: #3D3D3D; display: flex;
} align-items: baseline;
}
.price-label { .buy-now-btn {
color: #999; background: #FF19A0;
margin-left: 8rpx; color: #fff;
} font-size: 20rpx;
padding: 10rpx 20rpx;
.origin-price { border-radius: 30rpx;
color: #999; font-weight: 500;
margin-top: 8rpx; flex-shrink: 0;
} }
}
.add-cart-icon { </style>
width: 44rpx;
height: 44rpx;
position: absolute;
bottom: 20rpx;
right: 20rpx;
transition: transform 0.3s ease;
&.add-cart-icon-animate {
animation: addCartBounce 0.6s ease;
}
}
.buy-now-btn-wrapper {
margin-top: 16rpx;
display: flex;
justify-content: flex-end;
.buy-now-btn {
background: linear-gradient(90deg, #FF19A0, #FF4DB8);
color: #FFFFFF;
font-size: 24rpx;
padding: 12rpx 32rpx;
border-radius: 24rpx;
font-weight: 500;
}
}
}
@keyframes addCartBounce {
0% {
transform: scale(1) rotate(0deg);
}
25% {
transform: scale(1.2) rotate(-10deg);
}
50% {
transform: scale(1.3) rotate(10deg);
}
75% {
transform: scale(1.1) rotate(-5deg);
}
100% {
transform: scale(1) rotate(0deg);
}
}
</style>

View File

@ -70,8 +70,14 @@
<scroll-view class="category-right" scroll-y :refresher-enabled="true" <scroll-view class="category-right" scroll-y :refresher-enabled="true"
:refresher-triggered="refreshTriggered" @refresherrefresh="onRefresh" @scrolltolower="onLoadMore"> :refresher-triggered="refreshTriggered" @refresherrefresh="onRefresh" @scrolltolower="onLoadMore">
<view class="goods-list"> <view class="goods-list">
<good-item v-for="good in goodsList" :key="good.product_id" :ref="`goodItem_${good.product_id}`" <view class="goods-list-item left">
:data="good" @addToCar="addToCar" @clickCard="jumpToDetail" /> <good-item v-for="(good, i) in leftColumnGoods" :key="2 * i" :ref="`goodItem_${good.product_id}`"
:data="good" @addToCar="addToCar" />
</view>
<view class="goods-list-item right">
<good-item v-for="(good, i) in rightColumnGoods" :key="2 * i + 1" :ref="`goodItem_${good.product_id}`"
:data="good" @addToCar="addToCar" />
</view>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
@ -109,6 +115,7 @@ import CategoryModal from "./components/CategoryModal.vue";
import { import {
getGoodsListData getGoodsListData
} from "../../../api/shop"; } from "../../../api/shop";
import { getCategoryGoodsWithCache } from "@/utils/goodsCache";
export default { export default {
components: { components: {
@ -179,6 +186,12 @@ export default {
) || {} ) || {}
); );
}, },
leftColumnGoods() {
return this.goodsList.filter((v, i) => i % 2 === 0);
},
rightColumnGoods() {
return this.goodsList.filter((v, i) => i % 2 === 1);
},
}, },
mounted() { mounted() {
this.getCategoryList(); this.getCategoryList();
@ -195,6 +208,10 @@ export default {
}, },
onShow() { onShow() {
this.getCartListData(); this.getCartListData();
// 页面显示时检查是否需要刷新商品数据
if (this.selectCategoryId) {
this.getShopList(false);
}
}, },
watch: { watch: {
selectCategoryId(val) { selectCategoryId(val) {
@ -224,27 +241,57 @@ export default {
} }
}); });
}, },
changeCateg(item) { changeCateg(item) {
this.changeId = item.id this.changeId = item.id
this.selectCategoryId = item.id; this.selectCategoryId = item.id;
this.page = 1; // 切换分类时重置分页
this.goodsList = []; // 清空当前商品列表
this.showAllCategory = false; this.showAllCategory = false;
// console.log(item,'--') this.getShopList(true); // 切换分类时强制刷新
}, },
// 商品列表 // 商品列表
getShopList() { getShopList(forceRefresh = false) {
getGoodsListData({ // 如果不是第一页,直接请求不使用缓存(分页加载)
if (this.page > 1) {
getGoodsListData({
type: this.changeId,
p: this.page,
num: this.size,
keyword: "",
is_tui: 0,
})
.then((res) => {
const list = res?.data || [];
this.goodsList = [...this.goodsList, ...list];
this.total = res?.count || 0;
})
.finally(() => {
this.isLoading = false;
this.refreshTriggered = false;
});
return;
}
// 第一页使用缓存
const params = {
type: this.changeId, type: this.changeId,
p: this.page, p: this.page,
num: this.size, num: this.size,
keyword: "", keyword: "",
is_tui: 0, is_tui: 0,
}) };
getCategoryGoodsWithCache(params, forceRefresh)
.then((res) => { .then((res) => {
const list = res?.data || []; const list = res?.data || [];
this.goodsList = // 只有当数据有变化时才更新商品列表,避免不必要的刷新
this.page === 1 ? list : [...this.goodsList, ...list]; if (res.hasChanged !== false || this.goodsList.length === 0) {
this.total = res?.count || 0; this.goodsList = list;
}
this.total = res?.count || list.length || 0;
})
.catch((err) => {
console.error('获取商品列表失败', err);
}) })
.finally(() => { .finally(() => {
this.isLoading = false; this.isLoading = false;
@ -349,12 +396,12 @@ export default {
url: `/pages/client/shop/details?product_id=${details.product_id}&petOrderId=${this.petOrderId}&petOrderAddressId=${this.petOrderAddressId}`, url: `/pages/client/shop/details?product_id=${details.product_id}&petOrderId=${this.petOrderId}&petOrderAddressId=${this.petOrderAddressId}`,
}); });
}, },
onRefresh() { onRefresh() {
this.refreshTriggered = true; this.refreshTriggered = true;
this.page = 1; this.page = 1;
this.size = 10; this.size = 10;
this.total = 0; this.total = 0;
this.getShopList(); this.getShopList(true); // 下拉刷新时强制刷新
}, },
onLoadMore() { onLoadMore() {
if (!this.isLoading && this.total > this.goodsList.length) { if (!this.isLoading && this.total > this.goodsList.length) {
@ -589,6 +636,24 @@ export default {
.category-right { .category-right {
flex: 1; flex: 1;
height: 100%; height: 100%;
.goods-list {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
margin-top: 20rpx;
padding: 0 20rpx;
.goods-list-item {
flex: 1;
min-width: 0;
&.left {
margin-right: 20rpx;
}
}
}
} }
} }
} }

View File

@ -48,6 +48,9 @@
<view class="loginBtn" @click="toLogin" v-if="!userInfo.userID"> <view class="loginBtn" @click="toLogin" v-if="!userInfo.userID">
注册/登陆 注册/登陆
</view> </view>
<view class="logoutBtn" @click="logout" v-if="userInfo.userID">
退出登录
</view>
</view> </view>
<view class="shadowBackground" /> <view class="shadowBackground" />
@ -59,11 +62,11 @@
</view> --> </view> -->
<view class="goods-list"> <view class="goods-list">
<view class="goods-list-item left"> <view class="goods-list-item left">
<good-item v-for="(good, i) in leftColumnGoods" :index="2 * i" :key="2 * i" :data="good" <good-item v-for="(good, i) in leftColumnGoods" :index="2 * i" :key="2 * i" :data="good" :isHome="true"
@addToCar="addToCar" /> @addToCar="addToCar" />
</view> </view>
<view class="goods-list-item right"> <view class="goods-list-item right">
<good-item v-for="(good, i) in rightColumnGoods" :index="2 * i + 1" :key="2 * i + 1" :data="good" <good-item v-for="(good, i) in rightColumnGoods" :index="2 * i + 1" :key="2 * i + 1" :data="good" :isHome="true"
@addToCar="addToCar" /> @addToCar="addToCar" />
</view> </view>
</view> </view>
@ -85,6 +88,9 @@ import {
import { import {
userWllet userWllet
} from "../../../api/login"; } from "../../../api/login";
import {
loginOut,
} from "../../../api/user";
import WeChatCopyModal from "@/components/WeChatCopyModal.vue"; import WeChatCopyModal from "@/components/WeChatCopyModal.vue";
import GoodItem from "../shop/components/GoodItem.vue"; import GoodItem from "../shop/components/GoodItem.vue";
import DraggableContact from "@/components/DraggableContact.vue"; import DraggableContact from "@/components/DraggableContact.vue";
@ -92,6 +98,7 @@ import {
getGoodsClassify, getGoodsClassify,
getGoodsListData getGoodsListData
} from "@/api/shop"; } from "@/api/shop";
import { getHomeGoodsWithCache } from "@/utils/goodsCache";
export default { export default {
name: "HomePage", name: "HomePage",
@ -174,19 +181,28 @@ export default {
created() { created() {
this.getGoodsList() this.getGoodsList()
}, },
onShow() {
// 页面显示时检查是否需要刷新商品数据
this.getGoodsList(false);
},
methods: { methods: {
getGoodsList() { getGoodsList(forceRefresh = false) {
if (this.isLoadingGoods) return; if (this.isLoadingGoods) return;
this.isLoadingGoods = true; this.isLoadingGoods = true;
const params = { const params = {
type: 0 type: 0
} }
getGoodsListData(params) getHomeGoodsWithCache(params, forceRefresh)
.then((res) => { .then((res) => {
const list = res?.data.data.products || []; const list = res?.data.data.products || [];
this.goodsList = list; // 只有当数据有变化时才更新商品列表,避免不必要的刷新
console.log(this.goodsList,'???') if (res.hasChanged !== false || this.goodsList.length === 0) {
this.goodsTotal = res?.count || 0; this.goodsList = list;
}
this.goodsTotal = res?.count || list.length || 0;
})
.catch((err) => {
console.error('获取商品列表失败', err);
}) })
.finally(() => { .finally(() => {
this.isLoadingGoods = false; this.isLoadingGoods = false;
@ -198,6 +214,17 @@ export default {
url: "/pages/client/auth/index", url: "/pages/client/auth/index",
}); });
}, },
logout() {
loginOut();
// 清除Vuex中的用户状态
this.$store.dispatch('user/deleteToken');
this.$store.dispatch('user/clearUserInfo');
// 清除本地缓存
uni.clearStorageSync();
uni.reLaunch({
url: "/pages/client/auth/index",
});
},
// 统一的 token 检查方法,未登录时弹窗让用户自主选择 // 统一的 token 检查方法,未登录时弹窗让用户自主选择
async checkTokenAndExecute(callback) { async checkTokenAndExecute(callback) {
const token = uni.getStorageSync('token'); const token = uni.getStorageSync('token');
@ -398,6 +425,7 @@ export default {
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
margin-top: 50rpx; margin-top: 50rpx;
padding: 0 20rpx;
.goods-list-item { .goods-list-item {
flex: 1; flex: 1;
@ -591,6 +619,16 @@ export default {
margin-left: 8rpx; margin-left: 8rpx;
} }
} }
.logoutBtn {
background: linear-gradient(270deg, #FF19A0 0%, #FF6BB3 100%);
border-radius: 218px;
color: #fff;
font-size: 23rpx;
padding: 16rpx 24rpx;
display: flex;
align-items: center;
}
} }
.shadowBackground { .shadowBackground {

View File

@ -14,12 +14,18 @@
{{ userInfo.userID && userInfo.username ? userInfo.username : '嗨,你好呀' }} {{ userInfo.userID && userInfo.username ? userInfo.username : '嗨,你好呀' }}
</view> </view>
</view> </view>
<view class="userPhone" v-if="userInfo.phone">
{{ userInfo.phone }}
</view>
<!-- <view class="vipWrapper"> <!-- <view class="vipWrapper">
<image class="lableImg" :src="`${imgPrefix}home-vipLabel.png`" mode=""></image> <image class="lableImg" :src="`${imgPrefix}home-vipLabel.png`" mode=""></image>
v{{ userInfo.vipLevel || 1 }}会员 v{{ userInfo.vipLevel || 1 }}会员
</view> --> </view> -->
</view> </view>
</view> </view>
<view class="logoutBtn" @click="showLogoutModal = true">
退出登录
</view>
<!-- <view class="userRight"> <!-- <view class="userRight">
<view class="userRgihtItemView" @click="jumpTo('/pages/client/recharge/index?tab=points')"> <view class="userRgihtItemView" @click="jumpTo('/pages/client/recharge/index?tab=points')">
<view class="num"> <view class="num">
@ -300,6 +306,10 @@ components: {
logout() { logout() {
loginOut(); loginOut();
this.showLogoutModal = false; this.showLogoutModal = false;
// 清除Vuex中的用户状态
this.$store.dispatch('user/deleteToken');
this.$store.dispatch('user/clearUserInfo');
// 清除本地缓存
uni.clearStorageSync(); uni.clearStorageSync();
uni.reLaunch({ uni.reLaunch({
url: "/pages/client/auth/index", url: "/pages/client/auth/index",
@ -390,6 +400,21 @@ title: '请添加客服号',
font-size: 28rpx; font-size: 28rpx;
font-weight: 500; font-weight: 500;
} }
.userPhone {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
}
.logoutBtn {
background: linear-gradient(270deg, #FF19A0 0%, #FF6BB3 100%);
color: #fff;
padding: 16rpx 32rpx;
border-radius: 50rpx;
font-size: 24rpx;
font-weight: 500;
} }
.userRight { .userRight {

View File

@ -14,23 +14,25 @@
mode="aspectFill" mode="aspectFill"
/> />
<view class="goods-content"> <view class="goods-content">
<view class="goods-row-first"> <view class="goods-name">{{
<view class="goods-name">{{ item.item_name || item.product_name
item.item_name || item.product_name }}</view>
}}</view> <view class="price-wrapper">
<text class="goods-price" <text class="final-price-label">到手价</text>
>¥{{ item.product_price || item.goods_price }}</text <text class="final-price">¥{{ item.product_price || item.goods_price }}</text>
> <text class="original-price">
¥{{ item.original_price || (item.product_price * 1.5).toFixed(2) }}
</text>
</view> </view>
<view class="goods-row-second"> <view class="sales-info">
<text class="goods-count">{{ item.number || 1 }}</text> <text class="sales-count">已售0</text>
</view> </view>
</view> </view>
</view> </view>
</template> </template>
</view> </view>
<view class="info-cell pay-cell"> <!-- <view class="info-cell pay-cell">
<view class="flex-row-between pay-info"> <view class="flex-row-between pay-info">
<text class="pay-label">商品金额</text> <text class="pay-label">商品金额</text>
<text class="pay-value">{{ payPrice }}</text> <text class="pay-value">{{ payPrice }}</text>
@ -52,15 +54,11 @@
/> />
</view> </view>
</view> </view>
<!-- <view class="flex-row-between pay-info">
<text class="pay-label">运费</text>
<text class="pay-value">{{ sliverFee ? `${sliverFee}` : "包邮" }}</text>
</view> -->
<view class="flex-row-between pay-price"> <view class="flex-row-between pay-price">
<text class="pay-label">需付款</text> <text class="pay-label">需付款</text>
<text class="pay-total">{{ payPrice }}</text> <text class="pay-total">{{ payPrice }}</text>
</view> </view>
</view> </view> -->
<!-- <view class="info-cell payment-method-cell"> <!-- <view class="info-cell payment-method-cell">
<view class="payment-item" @click.stop="selectOption1('1')"> <view class="payment-item" @click.stop="selectOption1('1')">
@ -517,7 +515,7 @@ export default {
.goods-item { .goods-item {
display: flex; display: flex;
align-items: center; align-items: flex-start;
width: 100%; width: 100%;
padding: 24rpx 20rpx; padding: 24rpx 20rpx;
box-sizing: border-box; box-sizing: border-box;
@ -528,61 +526,66 @@ export default {
} }
.goods-img { .goods-img {
width: 100rpx; width: 200rpx;
height: 100rpx; height: 200rpx;
border-radius: 16rpx; border-radius: 16rpx;
margin-right: 20rpx; margin-right: 20rpx;
flex-shrink: 0; flex-shrink: 0;
background: #f5f5f5;
} }
.goods-content { .goods-content {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: flex-start;
min-height: 100rpx; min-height: 200rpx;
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
padding-top: 8rpx;
.goods-row-first { .goods-name {
font-size: 28rpx;
color: #3d3d3d;
line-height: 40rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
margin-bottom: 16rpx;
}
.price-wrapper {
display: flex; display: flex;
align-items: center; align-items: baseline;
justify-content: space-between;
margin-bottom: 12rpx; margin-bottom: 12rpx;
.goods-name { .final-price-label {
flex: 1; font-size: 24rpx;
font-size: 28rpx; color: #ff19a0;
color: #3d3d3d; background: linear-gradient(to right, #ffeef7, #fff);
line-height: 40rpx; padding: 4rpx 12rpx;
overflow: hidden; border-radius: 4rpx 0 0 4rpx;
text-overflow: ellipsis; margin-right: 8rpx;
white-space: nowrap;
margin-right: 20rpx;
} }
.goods-price { .final-price {
flex-shrink: 0; font-size: 36rpx;
font-size: 28rpx; color: #ff19a0;
color: #3d3d3d; font-weight: 700;
font-weight: 500; margin-right: 12rpx;
}
.original-price {
font-size: 24rpx;
color: #999;
text-decoration: line-through;
} }
} }
.goods-row-second { .sales-info {
display: flex; .sales-count {
align-items: center;
justify-content: space-between;
.goods-spec {
flex: 1;
font-size: 24rpx;
color: #999;
margin-right: 20rpx;
}
.goods-count {
flex-shrink: 0;
font-size: 24rpx; font-size: 24rpx;
color: #999; color: #999;
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<view class="goods-item" > <view class="goods-item" :class="{ 'goods-item-home': isHome }">
<image @click.stop="handleBuyNow" class="goods-img" :src="getProductImage(data)" mode="aspectFill" /> <image @click.stop="handleBuyNow" class="goods-img" :src="getProductImage(data)" mode="aspectFill" />
<view class=" fs-24 app-fc-main goods-name"> <view class=" fs-24 app-fc-main goods-name">
{{ data.product.product_name || "" }} {{ data.product.product_name || "" }}
@ -34,7 +34,7 @@
} from '@/utils/common'; } from '@/utils/common';
export default { export default {
props: { props: {
index: { index: {
type: Number, type: Number,
default: 0, default: 0,
@ -43,6 +43,10 @@
type: Object, type: Object,
default: () => {}, default: () => {},
}, },
isHome: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {
@ -94,7 +98,7 @@
.goods-item { .goods-item {
background: #fff; background: #fff;
border-radius: 16rpx; border-radius: 16rpx;
padding: 20rpx; padding: 2rpx;
box-sizing: border-box; box-sizing: border-box;
margin-bottom: 22rpx; margin-bottom: 22rpx;
position: relative; position: relative;
@ -102,15 +106,24 @@
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
&.goods-item-home {
padding: 20rpx;
}
.goods-img { .goods-img {
display: block; display: block;
// width: 260rpx;
height: 260rpx;
width: 100%; width: 100%;
max-width: 100%; // max-width: 100%;
height: 320rpx;
border-radius: 16rpx; border-radius: 16rpx;
background: #f5f5f5; background: #f5f5f5;
} }
&.goods-item-home .goods-img {
height: 145px;
}
.goods-name { .goods-name {
margin: 20rpx 0; margin: 20rpx 0;
// overflow: hidden; // overflow: hidden;

273
src/utils/goodsCache.js Normal file
View File

@ -0,0 +1,273 @@
/**
* 商品数据缓存工具
* 用于首页和分类页的商品列表缓存,避免重复请求相同数据
*/
import { getGoodsListData } from '@/api/shop'
// 缓存存储对象
const goodsCache = {
// 首页商品缓存
home: null,
// 分类页商品缓存key为分类ID
category: {}
}
// 缓存过期时间5分钟单位毫秒
const CACHE_EXPIRE_TIME = 5 * 60 * 1000
/**
* 计算数据的哈希值,用于判断数据是否有变化
* @param {Array} data - 商品数据列表
* @returns {string} 数据哈希值
*/
function calculateDataHash(data) {
if (!data || !Array.isArray(data) || data.length === 0) {
return 'empty'
}
// 使用商品ID、价格、更新时间等关键信息计算哈希
const keyData = data.map(item => {
const product = item.product || item
return `${product.product_id || product.id}-${product.updated_at || Date.now()}-${item.sku?.actual_amount || product.price || 0}`
}).join('|')
// 简单的字符串哈希函数
let hash = 0
for (let i = 0; i < keyData.length; i++) {
const char = keyData.charCodeAt(i)
hash = ((hash << 5) - hash) + char
hash = hash & hash // 转换为32位整数
}
return hash.toString()
}
/**
* 获取缓存的首页商品数据
* @returns {Object|null} 缓存的商品数据,包含数据、哈希值和时间戳
*/
export function getHomeGoodsCache() {
return goodsCache.home
}
/**
* 设置首页商品缓存
* @param {Array} data - 商品数据列表
*/
export function setHomeGoodsCache(data) {
goodsCache.home = {
data,
hash: calculateDataHash(data),
timestamp: Date.now()
}
}
/**
* 获取指定分类的商品缓存
* @param {Number|String} categoryId - 分类ID
* @returns {Object|null} 缓存的商品数据
*/
export function getCategoryGoodsCache(categoryId) {
return goodsCache.category[categoryId] || null
}
/**
* 设置指定分类的商品缓存
* @param {Number|String} categoryId - 分类ID
* @param {Array} data - 商品数据列表
*/
export function setCategoryGoodsCache(categoryId, data) {
goodsCache.category[categoryId] = {
data,
hash: calculateDataHash(data),
timestamp: Date.now()
}
}
/**
* 检查缓存是否有效
* @param {Object} cache - 缓存对象
* @returns {Boolean} 缓存是否有效
*/
function isCacheValid(cache) {
if (!cache) return false
const now = Date.now()
// 检查缓存是否过期
if (now - cache.timestamp > CACHE_EXPIRE_TIME) {
return false
}
return true
}
/**
* 获取首页商品数据(带缓存)
* @param {Object} params - 请求参数
* @param {Boolean} forceRefresh - 是否强制刷新
* @returns {Promise} 商品数据
*/
export function getHomeGoodsWithCache(params, forceRefresh = false) {
return new Promise((resolve, reject) => {
const cache = getHomeGoodsCache()
// 如果有有效缓存且不强制刷新,先返回缓存数据
if (!forceRefresh && isCacheValid(cache)) {
// 先从缓存返回数据
resolve({
data: { data: { products: cache.data } },
fromCache: true
})
}
// 无论是否有缓存,都请求最新数据进行对比
getGoodsListData(params).then(res => {
const newData = res?.data.data.products || []
const newHash = calculateDataHash(newData)
// 检查数据是否有变化
const hasChanged = !cache || cache.hash !== newHash
if (hasChanged || forceRefresh) {
// 更新缓存
setHomeGoodsCache(newData)
// 如果没有缓存或者数据有变化,返回最新数据
if (!cache || forceRefresh) {
resolve({
...res,
fromCache: false,
hasChanged: true
})
} else {
// 如果已有缓存但数据有变化,通知更新
resolve({
...res,
fromCache: false,
hasChanged: true
})
}
} else {
// 数据没有变化,更新缓存时间戳
cache.timestamp = Date.now()
resolve({
data: { data: { products: cache.data } },
fromCache: true,
hasChanged: false
})
}
}).catch(err => {
// 如果请求失败但有缓存,返回缓存
if (cache) {
resolve({
data: { data: { products: cache.data } },
fromCache: true,
error: err
})
} else {
reject(err)
}
})
})
}
/**
* 获取分类页商品数据(带缓存)
* @param {Object} params - 请求参数
* @param {Boolean} forceRefresh - 是否强制刷新
* @returns {Promise} 商品数据
*/
export function getCategoryGoodsWithCache(params, forceRefresh = false) {
const categoryId = params.type || params.category_id || ''
return new Promise((resolve, reject) => {
const cache = getCategoryGoodsCache(categoryId)
// 如果有有效缓存且不强制刷新,先返回缓存数据
if (!forceRefresh && isCacheValid(cache)) {
resolve({
data: cache.data,
count: cache.data.length,
fromCache: true
})
}
// 无论是否有缓存,都请求最新数据进行对比
getGoodsListData(params).then(res => {
const newData = res?.data || []
const newHash = calculateDataHash(newData)
// 检查数据是否有变化
const hasChanged = !cache || cache.hash !== newHash
if (hasChanged || forceRefresh) {
// 更新缓存
setCategoryGoodsCache(categoryId, newData)
if (!cache || forceRefresh) {
resolve({
...res,
fromCache: false,
hasChanged: true
})
} else {
resolve({
...res,
fromCache: false,
hasChanged: true
})
}
} else {
// 数据没有变化,更新缓存时间戳
cache.timestamp = Date.now()
resolve({
data: cache.data,
count: cache.data.length,
fromCache: true,
hasChanged: false
})
}
}).catch(err => {
// 如果请求失败但有缓存,返回缓存
if (cache) {
resolve({
data: cache.data,
count: cache.data.length,
fromCache: true,
error: err
})
} else {
reject(err)
}
})
})
}
/**
* 清除所有商品缓存
*/
export function clearGoodsCache() {
goodsCache.home = null
goodsCache.category = {}
}
/**
* 清除指定分类的商品缓存
* @param {Number|String} categoryId - 分类ID
*/
export function clearCategoryGoodsCache(categoryId) {
if (goodsCache.category[categoryId]) {
delete goodsCache.category[categoryId]
}
}
export default {
getHomeGoodsCache,
setHomeGoodsCache,
getCategoryGoodsCache,
setCategoryGoodsCache,
getHomeGoodsWithCache,
getCategoryGoodsWithCache,
clearGoodsCache,
clearCategoryGoodsCache
}