1
This commit is contained in:
@ -1,37 +1,35 @@
|
||||
<template>
|
||||
<view class="flex-row-start goods-item" @click.stop="$emit('clickCard', data)">
|
||||
<image class="goods-img" :src="data.product_pic" mode="aspectFill" />
|
||||
<view class="goods-content">
|
||||
<view class="text-multi-ellipse fs-28 app-fc-main app-font-bold goods-name">
|
||||
<view class="goods-item" >
|
||||
<image @click.stop="handleBuyNow" class="goods-img" :src="data.product_pic" mode="aspectFill" />
|
||||
<view class="fs-24 app-fc-main goods-name">
|
||||
{{ data.product_name || "" }}
|
||||
</view>
|
||||
<!-- <view class="flex-row-start label">
|
||||
<view class="flex-row-start label">
|
||||
<image class="hot-icon" :src="`${imgPrefix}mall-hot.png`"></image>
|
||||
<view class="fs-20 app-fc-main label-name">{{data.sales}}人买过</view>
|
||||
</view> -->
|
||||
<view class="price-row">
|
||||
<view class="flex-row-start">
|
||||
<text class="fs-28 price-text">
|
||||
<view class="fs-20 app-fc-main label-name">32人买过</view>
|
||||
</view>
|
||||
<view class="flex-row-between" style="margin-top: 12rpx; align-items: baseline;">
|
||||
<view class="price-wrapper">
|
||||
<text class="fs-28" style="color: #FF19A0;">
|
||||
¥
|
||||
<text class="fs-28">{{data.prices[0].original_price / 100 || 0 }}</text>
|
||||
</text>
|
||||
<!-- <text class="fs-20 price-label">到手价</text> -->
|
||||
</view>
|
||||
<text class="fs-24 origin-price" v-if="minPrice.price_shichang">
|
||||
¥{{ minPrice.price_shichang || 0 }}
|
||||
<text class="fs-28">{{
|
||||
data.prices[0].original_price / 100 || 0
|
||||
}}</text>
|
||||
</text>
|
||||
</view>
|
||||
<view class="buy-now-btn-wrapper" @click.stop="handleBuyNow">
|
||||
<text class="buy-now-btn">立即购买1</text>
|
||||
<view class="buy-now-btn" @click.stop="handleBuyNow">
|
||||
立即购买
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { imgPrefix } from '@/utils/common';
|
||||
import {
|
||||
imgPrefix
|
||||
} from '@/utils/common';
|
||||
|
||||
export default {
|
||||
export default {
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
@ -45,67 +43,58 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
imgPrefix,
|
||||
isAnimating: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// minPrice() {
|
||||
// let minPrice = {};
|
||||
// let minPriceValue = 0;
|
||||
// this.data.price_list.map((v) => {
|
||||
// if (!minPriceValue || minPriceValue > +v.price) {
|
||||
// minPriceValue = +v.price;
|
||||
// minPrice = { ...v };
|
||||
// }
|
||||
// });
|
||||
// return minPrice;
|
||||
// },
|
||||
labelList() {
|
||||
return this.data.label.split(",").filter((v) => !!v);
|
||||
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);
|
||||
// 触发购买事件,通知父组件
|
||||
this.$emit('addToCar', this.data);
|
||||
},
|
||||
triggerAddCartAnimation() {
|
||||
// 保留方法以兼容父组件调用
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.goods-item {
|
||||
.goods-item {
|
||||
background: #fff;
|
||||
border-radius: 40rpx;
|
||||
width: 100%;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20rpx;
|
||||
margin-bottom: 22rpx;
|
||||
position: relative;
|
||||
border-bottom: 1rpx solid #F5F5F5;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.goods-img {
|
||||
border-radius: 20rpx;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
}
|
||||
|
||||
.goods-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
margin-left: 20rpx;
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 260rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.goods-name {
|
||||
margin: 0 0 12rpx;
|
||||
margin: 16rpx 0 12rpx 0;
|
||||
overflow: hidden;
|
||||
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;
|
||||
}
|
||||
|
||||
.label {
|
||||
@ -113,84 +102,45 @@ export default {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 4rpx;
|
||||
margin-bottom: 12rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
|
||||
.hot-icon {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
margin-right: 6rpx;
|
||||
}
|
||||
|
||||
.label-name {
|
||||
padding: 4rpx 8rpx;
|
||||
padding-left: 4rpx;
|
||||
color: #FF19A0;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.price-row {
|
||||
margin-top: 12rpx;
|
||||
.flex-row-between {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.price-text {
|
||||
color: #3D3D3D;
|
||||
}
|
||||
|
||||
.price-label {
|
||||
color: #999;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.origin-price {
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.add-cart-icon {
|
||||
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;
|
||||
.flex-row-start {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.price-wrapper {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.buy-now-btn {
|
||||
background: linear-gradient(90deg, #FF19A0, #FF4DB8);
|
||||
color: #FFFFFF;
|
||||
font-size: 24rpx;
|
||||
padding: 12rpx 32rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #FF19A0;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
padding: 10rpx 20rpx;
|
||||
border-radius: 30rpx;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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>
|
||||
@ -70,8 +70,14 @@
|
||||
<scroll-view class="category-right" scroll-y :refresher-enabled="true"
|
||||
:refresher-triggered="refreshTriggered" @refresherrefresh="onRefresh" @scrolltolower="onLoadMore">
|
||||
<view class="goods-list">
|
||||
<good-item v-for="good in goodsList" :key="good.product_id" :ref="`goodItem_${good.product_id}`"
|
||||
:data="good" @addToCar="addToCar" @clickCard="jumpToDetail" />
|
||||
<view class="goods-list-item left">
|
||||
<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>
|
||||
</scroll-view>
|
||||
</view>
|
||||
@ -109,6 +115,7 @@ import CategoryModal from "./components/CategoryModal.vue";
|
||||
import {
|
||||
getGoodsListData
|
||||
} from "../../../api/shop";
|
||||
import { getCategoryGoodsWithCache } from "@/utils/goodsCache";
|
||||
|
||||
export default {
|
||||
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() {
|
||||
this.getCategoryList();
|
||||
@ -195,6 +208,10 @@ export default {
|
||||
},
|
||||
onShow() {
|
||||
this.getCartListData();
|
||||
// 页面显示时检查是否需要刷新商品数据
|
||||
if (this.selectCategoryId) {
|
||||
this.getShopList(false);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectCategoryId(val) {
|
||||
@ -227,12 +244,15 @@ export default {
|
||||
changeCateg(item) {
|
||||
this.changeId = item.id
|
||||
this.selectCategoryId = item.id;
|
||||
this.page = 1; // 切换分类时重置分页
|
||||
this.goodsList = []; // 清空当前商品列表
|
||||
this.showAllCategory = false;
|
||||
// console.log(item,'--')
|
||||
|
||||
this.getShopList(true); // 切换分类时强制刷新
|
||||
},
|
||||
// 商品列表
|
||||
getShopList() {
|
||||
getShopList(forceRefresh = false) {
|
||||
// 如果不是第一页,直接请求不使用缓存(分页加载)
|
||||
if (this.page > 1) {
|
||||
getGoodsListData({
|
||||
type: this.changeId,
|
||||
p: this.page,
|
||||
@ -242,14 +262,41 @@ export default {
|
||||
})
|
||||
.then((res) => {
|
||||
const list = res?.data || [];
|
||||
this.goodsList =
|
||||
this.page === 1 ? list : [...this.goodsList, ...list];
|
||||
this.goodsList = [...this.goodsList, ...list];
|
||||
this.total = res?.count || 0;
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false;
|
||||
this.refreshTriggered = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 第一页使用缓存
|
||||
const params = {
|
||||
type: this.changeId,
|
||||
p: this.page,
|
||||
num: this.size,
|
||||
keyword: "",
|
||||
is_tui: 0,
|
||||
};
|
||||
|
||||
getCategoryGoodsWithCache(params, forceRefresh)
|
||||
.then((res) => {
|
||||
const list = res?.data || [];
|
||||
// 只有当数据有变化时才更新商品列表,避免不必要的刷新
|
||||
if (res.hasChanged !== false || this.goodsList.length === 0) {
|
||||
this.goodsList = list;
|
||||
}
|
||||
this.total = res?.count || list.length || 0;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('获取商品列表失败', err);
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false;
|
||||
this.refreshTriggered = false;
|
||||
});
|
||||
},
|
||||
changeCategory(data) {
|
||||
console.log(data, '--=')
|
||||
@ -354,7 +401,7 @@ export default {
|
||||
this.page = 1;
|
||||
this.size = 10;
|
||||
this.total = 0;
|
||||
this.getShopList();
|
||||
this.getShopList(true); // 下拉刷新时强制刷新
|
||||
},
|
||||
onLoadMore() {
|
||||
if (!this.isLoading && this.total > this.goodsList.length) {
|
||||
@ -589,6 +636,24 @@ export default {
|
||||
.category-right {
|
||||
flex: 1;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,6 +48,9 @@
|
||||
<view class="loginBtn" @click="toLogin" v-if="!userInfo.userID">
|
||||
注册/登陆
|
||||
</view>
|
||||
<view class="logoutBtn" @click="logout" v-if="userInfo.userID">
|
||||
退出登录
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<view class="shadowBackground" />
|
||||
@ -59,11 +62,11 @@
|
||||
</view> -->
|
||||
<view class="goods-list">
|
||||
<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" />
|
||||
</view>
|
||||
<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" />
|
||||
</view>
|
||||
</view>
|
||||
@ -85,6 +88,9 @@ import {
|
||||
import {
|
||||
userWllet
|
||||
} from "../../../api/login";
|
||||
import {
|
||||
loginOut,
|
||||
} from "../../../api/user";
|
||||
import WeChatCopyModal from "@/components/WeChatCopyModal.vue";
|
||||
import GoodItem from "../shop/components/GoodItem.vue";
|
||||
import DraggableContact from "@/components/DraggableContact.vue";
|
||||
@ -92,6 +98,7 @@ import {
|
||||
getGoodsClassify,
|
||||
getGoodsListData
|
||||
} from "@/api/shop";
|
||||
import { getHomeGoodsWithCache } from "@/utils/goodsCache";
|
||||
|
||||
export default {
|
||||
name: "HomePage",
|
||||
@ -174,19 +181,28 @@ export default {
|
||||
created() {
|
||||
this.getGoodsList()
|
||||
},
|
||||
onShow() {
|
||||
// 页面显示时检查是否需要刷新商品数据
|
||||
this.getGoodsList(false);
|
||||
},
|
||||
methods: {
|
||||
getGoodsList() {
|
||||
getGoodsList(forceRefresh = false) {
|
||||
if (this.isLoadingGoods) return;
|
||||
this.isLoadingGoods = true;
|
||||
const params = {
|
||||
type: 0
|
||||
}
|
||||
getGoodsListData(params)
|
||||
getHomeGoodsWithCache(params, forceRefresh)
|
||||
.then((res) => {
|
||||
const list = res?.data.data.products || [];
|
||||
// 只有当数据有变化时才更新商品列表,避免不必要的刷新
|
||||
if (res.hasChanged !== false || this.goodsList.length === 0) {
|
||||
this.goodsList = list;
|
||||
console.log(this.goodsList,'???')
|
||||
this.goodsTotal = res?.count || 0;
|
||||
}
|
||||
this.goodsTotal = res?.count || list.length || 0;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('获取商品列表失败', err);
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoadingGoods = false;
|
||||
@ -198,6 +214,17 @@ export default {
|
||||
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 检查方法,未登录时弹窗让用户自主选择
|
||||
async checkTokenAndExecute(callback) {
|
||||
const token = uni.getStorageSync('token');
|
||||
@ -398,6 +425,7 @@ export default {
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
margin-top: 50rpx;
|
||||
padding: 0 20rpx;
|
||||
|
||||
.goods-list-item {
|
||||
flex: 1;
|
||||
@ -591,6 +619,16 @@ export default {
|
||||
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 {
|
||||
|
||||
@ -14,12 +14,18 @@
|
||||
{{ userInfo.userID && userInfo.username ? userInfo.username : '嗨,你好呀' }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="userPhone" v-if="userInfo.phone">
|
||||
{{ userInfo.phone }}
|
||||
</view>
|
||||
<!-- <view class="vipWrapper">
|
||||
<image class="lableImg" :src="`${imgPrefix}home-vipLabel.png`" mode=""></image>
|
||||
v{{ userInfo.vipLevel || 1 }}会员
|
||||
</view> -->
|
||||
</view>
|
||||
</view>
|
||||
<view class="logoutBtn" @click="showLogoutModal = true">
|
||||
退出登录
|
||||
</view>
|
||||
<!-- <view class="userRight">
|
||||
<view class="userRgihtItemView" @click="jumpTo('/pages/client/recharge/index?tab=points')">
|
||||
<view class="num">
|
||||
@ -300,6 +306,10 @@ components: {
|
||||
logout() {
|
||||
loginOut();
|
||||
this.showLogoutModal = false;
|
||||
// 清除Vuex中的用户状态
|
||||
this.$store.dispatch('user/deleteToken');
|
||||
this.$store.dispatch('user/clearUserInfo');
|
||||
// 清除本地缓存
|
||||
uni.clearStorageSync();
|
||||
uni.reLaunch({
|
||||
url: "/pages/client/auth/index",
|
||||
@ -390,6 +400,21 @@ title: '请添加客服号',
|
||||
font-size: 28rpx;
|
||||
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 {
|
||||
|
||||
@ -14,23 +14,25 @@
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="goods-content">
|
||||
<view class="goods-row-first">
|
||||
<view class="goods-name">{{
|
||||
item.item_name || item.product_name
|
||||
}}</view>
|
||||
<text class="goods-price"
|
||||
>¥{{ item.product_price || item.goods_price }}</text
|
||||
>
|
||||
<view class="price-wrapper">
|
||||
<text class="final-price-label">到手价</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 class="goods-row-second">
|
||||
<text class="goods-count">共{{ item.number || 1 }}件</text>
|
||||
<view class="sales-info">
|
||||
<text class="sales-count">已售0件</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<view class="info-cell pay-cell">
|
||||
<!-- <view class="info-cell pay-cell">
|
||||
<view class="flex-row-between pay-info">
|
||||
<text class="pay-label">商品金额</text>
|
||||
<text class="pay-value">¥{{ payPrice }}</text>
|
||||
@ -52,15 +54,11 @@
|
||||
/>
|
||||
</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">
|
||||
<text class="pay-label">需付款</text>
|
||||
<text class="pay-total">¥{{ payPrice }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view> -->
|
||||
|
||||
<!-- <view class="info-cell payment-method-cell">
|
||||
<view class="payment-item" @click.stop="selectOption1('1')">
|
||||
@ -517,7 +515,7 @@ export default {
|
||||
|
||||
.goods-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
padding: 24rpx 20rpx;
|
||||
box-sizing: border-box;
|
||||
@ -528,61 +526,66 @@ export default {
|
||||
}
|
||||
|
||||
.goods-img {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 16rpx;
|
||||
margin-right: 20rpx;
|
||||
flex-shrink: 0;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.goods-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-height: 100rpx;
|
||||
justify-content: flex-start;
|
||||
min-height: 200rpx;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.goods-row-first {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
padding-top: 8rpx;
|
||||
|
||||
.goods-name {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #3d3d3d;
|
||||
line-height: 40rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 20rpx;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.goods-price {
|
||||
flex-shrink: 0;
|
||||
font-size: 28rpx;
|
||||
color: #3d3d3d;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-row-second {
|
||||
.price-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.goods-spec {
|
||||
flex: 1;
|
||||
.final-price-label {
|
||||
font-size: 24rpx;
|
||||
color: #ff19a0;
|
||||
background: linear-gradient(to right, #ffeef7, #fff);
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 4rpx 0 0 4rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.final-price {
|
||||
font-size: 36rpx;
|
||||
color: #ff19a0;
|
||||
font-weight: 700;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.original-price {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-right: 20rpx;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-count {
|
||||
flex-shrink: 0;
|
||||
.sales-info {
|
||||
.sales-count {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<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" />
|
||||
<view class=" fs-24 app-fc-main goods-name">
|
||||
{{ data.product.product_name || "" }}
|
||||
@ -43,6 +43,10 @@
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
isHome: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -94,7 +98,7 @@
|
||||
.goods-item {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
padding: 2rpx;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 22rpx;
|
||||
position: relative;
|
||||
@ -102,15 +106,24 @@
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
|
||||
&.goods-item-home {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.goods-img {
|
||||
display: block;
|
||||
// width: 260rpx;
|
||||
height: 260rpx;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 320rpx;
|
||||
// max-width: 100%;
|
||||
border-radius: 16rpx;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&.goods-item-home .goods-img {
|
||||
height: 145px;
|
||||
}
|
||||
|
||||
.goods-name {
|
||||
margin: 20rpx 0;
|
||||
// overflow: hidden;
|
||||
|
||||
273
src/utils/goodsCache.js
Normal file
273
src/utils/goodsCache.js
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user