Преглед изворни кода

Signed-off-by: 李妹妹 <1639016684@qq.com>

李妹妹 пре 12 часа
родитељ
комит
da0ac2d2ad

+ 57 - 25
pages/home/newindex.vue

@@ -17,17 +17,19 @@
          	<view class="status_bar" :style="{height: statusBarHeight}"></view>
          </view>
 		<!-- 主横幅:两图横向 -->
-		<!-- <view class="banner-row" v-if="advList && advList.length > 0">
-			<view class="banner-item" v-for="(item, i) in advList.slice(0, 2)" :key="i" @tap="handleAdvClick(item)">
-				<image class="banner-img" :src="item.advImg || item.imageUrl" mode="aspectFill"></image>
+		<view class="banner-row-wrap" v-if="showBannerBlock">
+			<view class="banner-row banner-skeleton" v-if="showBannerSkeleton">
+				<view class="banner-sk-item" v-for="i in 2" :key="'banner-sk-' + i"></view>
 			</view>
-			<view class="banner-item" v-for="(item, i) in advList.slice(0, 2)" :key="i" @tap="handleAdvClick(item)">
-				<image class="banner-img" :src="item.advImg || item.imageUrl" mode="aspectFill"></image>
+			<view class="banner-row" v-else-if="showBannerContent">
+				<view class="banner-item" v-for="(item, i) in advList" :key="i" @tap="handleAdvClick(item)">
+					<image class="banner-img" :src="imageUrl[i]" mode="widthFix"></image>
+				</view>
 			</view>
-		</view> -->
+		</view>
 
     <!-- 推荐频道 -->
-    <ChannelEntry :list="channelList" :per-row="4" :rows="2" @click="onChannelClick" />
+    <!-- <ChannelEntry :list="channelList" :per-row="4" :rows="2" @click="onChannelClick" /> -->
 
 		<!-- 金刚区:分类图标 2 行 4 列,仅展示 -->
 		<CategoryTags :tags="categoryTagsData" :loading="categoryTagsLoading" @selectClick="onCategoryTagsSelect" />
@@ -78,7 +80,7 @@
 import zModal from '@/components/z-modal/z-modal.vue'
 import { getMenu, getIndexData, getCartCount } from '@/api/index'
 import { getStoreConfig } from '@/api/common'
-import { getHomeInit, getHomeRecommend, getHomeGoods } from '@/api/home.js'
+import { getHomeInit, getHomeRecommend, getHomeGoods,listPublicCourse } from '@/api/home.js'
 import HotProduct from './components/HotProduct.vue'
 //import TuiProduct from '@/components/tuiProduct.vue'
 import SearchBar from './components/SearchBar.vue'
@@ -131,10 +133,15 @@ export default {
 			goodsRequestId: 0,
 			cartCount: 0,
 			advList: [],
+			bannerLoading: true,
 			top: 0,
 			statusBarHeight: (uni.getStorageSync('menuInfo') && uni.getStorageSync('menuInfo').statusBarHeight) || 20,
 			userinfoa: [],
-			isuser: false
+			isuser: false,
+			imageUrl:[
+				'https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/class/page01.png',
+				'https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/class/page02.png'
+			]
 		}
 	},
 	computed: {
@@ -146,6 +153,15 @@ export default {
 				return ''
 			}
 		},
+		showBannerSkeleton() {
+			return this.bannerLoading && !(this.advList && this.advList.length)
+		},
+		showBannerContent() {
+			return !!(this.advList && this.advList.length)
+		},
+		showBannerBlock() {
+			return this.showBannerSkeleton || this.showBannerContent
+		},
 		// 瀑布流标签:后端 categoryTags(不包含“全部”)
 		goodsNavList() {
 			const tags = (this.goodsNav || []).map(t => ({
@@ -373,13 +389,21 @@ export default {
 			})
 		},
 		getIndexData() {
-			getIndexData({}).then(res => {
+			if (!this.advList.length) this.bannerLoading = true
+			const data = {
+				pageNum: 1,
+				pageSize: 2,
+				yxxTag: 3,
+				recommendSlot: 2,
+			}
+			listPublicCourse(data).then(res => {
 				if (res.code == 200) {
-					this.advList = res.data.advList || []
-					this.hotProductList = res.data.hotProductList || []
+					this.advList = res.data.list || []
 				} else {
 					uni.showToast({ icon: 'none', title: '请求失败' })
 				}
+			}).catch(() => {}).finally(() => {
+				this.bannerLoading = false
 			})
 		},
 		getUserInfo() {
@@ -438,14 +462,9 @@ export default {
 			}
 		},
 		handleAdvClick(item) {
-			if (item.showType == 1) {
-				uni.setStorageSync('url', item.advUrl)
-				uni.navigateTo({ url: 'h5' })
-			} else if (item.showType == 2) {
-				uni.navigateTo({ url: item.advUrl })
-			} else if (item.showType == 3) {
-				uni.setStorageSync('content', item.content)
-				uni.navigateTo({ url: 'content' })
+			if (item && item.courseId) {
+				uni.navigateTo({ url: '/pages_index/courseDetail?courseId=' + item.courseId})
+				return;
 			}
 		},
 		goAuthUrl(url) {
@@ -480,19 +499,32 @@ export default {
 	position: relative;
 	z-index: 10;
 }
+.banner-row-wrap {
+	position: relative;
+}
 .banner-row {
 	position: relative;
 	display: flex;
 	gap: 20rpx;
-	padding: 20rpx 24rpx;
-	// background: #fff;
+	padding:0 20rpx 24rpx;
 }
-.banner-item {
+.banner-skeleton {
+	min-height: 220rpx;
+}
+.banner-sk-item {
 	flex: 1;
 	height: 220rpx;
 	border-radius: 16rpx;
-	overflow: hidden;
-	background: #f5f5f5;
+	background: linear-gradient(90deg, #eee 25%, #f5f5f5 50%, #eee 75%);
+	background-size: 200% 100%;
+	animation: banner-shimmer 1.2s ease-in-out infinite;
+}
+@keyframes banner-shimmer {
+	0% { background-position: 100% 0; }
+	100% { background-position: -100% 0; }
+}
+.banner-item {
+	flex: 1;
 }
 .banner-img {
 	width: 100%;

+ 1 - 1
pages/shopping/productDetails.vue

@@ -212,7 +212,7 @@
 </template>
 
 <script>
-import {CustomToast} from '@/components/custom-toast.vue';
+import CustomToast from '@/components/custom-toast.vue';
 
 	import {
 		getDicts

+ 2 - 2
pages_company/managerOrder/productDetails.vue

@@ -212,7 +212,7 @@
 </template>
 
 <script>
-import {CustomToast} from '@/components/custom-toast.vue';
+import CustomToast from '@/components/custom-toast.vue';
 
 	import {
 		getDicts
@@ -466,7 +466,7 @@ import {CustomToast} from '@/components/custom-toast.vue';
 						this.loadding = false
 						if (res.code == 200) {
 							this.product = res.product;
-							this.remainingPurchaseLimit = res.product.purchaseLimit;
+							this.remainingPurchaseLimit = res.remainingPurchaseLimit;
 							this.singlePurchaseLimit = res.product.singlePurchaseLimit!==0?res.product.singlePurchaseLimit:null
 							// 如果接口返回了总限购数量和已购买数量,也保存
 							if (res.product.purchaseLimit !== null) {

+ 4 - 2
pages_course/components/goodsList - 副本.vue

@@ -23,7 +23,7 @@
 		</view>
 		<view v-if="treatmentPackage.length==0">
 			<view class="empty">
-				<text>暂未上新活动</text>
+				<text>~暂无更多~</text>
 			</view>
 		</view>
 	</view>
@@ -31,6 +31,7 @@
 
 <script>
 	import { getJumpStoreAppId } from "../api/courseAuto.js"
+	import {loginByMiniApp} from "@/api/courseLook.js"
 	export default {
 		props:['treatmentPackage','urlOption','source'],
 		data() {
@@ -53,9 +54,10 @@
 		},
 		methods: {
 			goBuy(item) {
+				//let data = uni.getStorageSync('urlInfo')
 				uni.navigateTo({
 					url: '/pages/shopping/productDetails?productId='+item.productId+'&companyId='+this.urlOption.companyId+ '&companyUserId='+this.urlOption.companyUserId+'&courseId='+this.urlOption.courseId+'&videoId='+this.urlOption.videoId
-					+'&projectId='+this.urlOption.projectId+'&periodId='+this.urlOption.periodId
+					+'&projectId='+(this.urlOption.projectId || '')+'&periodId='+ (this.urlOption.periodId || '')
 				})
 			},
 			goBuy2(item) {

+ 72 - 5
pages_course/components/goodsList.vue

@@ -1,9 +1,13 @@
 <template>
 	<view>
-		<view v-for="(item,index) in treatmentPackage" :key="index">
+		<view v-for="(item,index) in treatmentPackage" :key="getItemKey(item, index)">
 			<view class="goodsitem">
 				<view class="goodsitem-img">
 					<image :src="item.images" mode="aspectFill" style="height: 100%;width: 100%"></image>
+					<view class="goodsitem-hot" v-if="hasHotSaleTag(item) && getHotCount(item) > 0">
+						<image style="height: 56rpx;" src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/class/remai.png" mode="heightFix"></image>
+						<text class="goodsitem-hot-count">{{ getHotCount(item) }}</text>
+					</view>
 					<!-- <view class="goodsitem-status">讲解中</view> -->
 				</view>
 				<view class="goodsitem-r">
@@ -32,11 +36,23 @@
 <script>
 	import { getJumpStoreAppId } from "../api/courseAuto.js"
 	import {loginByMiniApp} from "@/api/courseLook.js"
+	import { hasHotSaleTag, getProductHotKey, clampHotSaleCount } from '../utils/productHotSale.js'
 	export default {
-		props:['treatmentPackage','urlOption','source'],
+		props: {
+			treatmentPackage: {
+				type: Array,
+				default: () => [],
+			},
+			urlOption: [Object, String],
+			source: String,
+			hotCountMap: {
+				type: Object,
+				default: () => ({}),
+			},
+		},
 		data() {
 			return {
-				list: []
+				list: [],
 			}
 		},
 		computed: {
@@ -53,6 +69,17 @@
 			}
 		},
 		methods: {
+			getItemKey(item, index) {
+				return item.productId || item.packageId || `idx_${index}`
+			},
+			hasHotSaleTag(item) {
+				return hasHotSaleTag(item)
+			},
+			getHotCount(item) {
+				if (!hasHotSaleTag(item)) return 0
+				const key = getProductHotKey(item)
+				return key ? clampHotSaleCount(this.hotCountMap[key]) : 0
+			},
 			goBuy(item) {
 				//let data = uni.getStorageSync('urlInfo')
 				uni.navigateTo({
@@ -115,15 +142,55 @@
 	.goodsitem {
 		display: flex;
 		padding: 24rpx;
-		overflow: hidden;
+		overflow: visible;
 		min-height: 200rpx;
 		&-img {
-			overflow: hidden;
+			overflow: visible;
 			flex-shrink: 0;
 			width: 200rpx;
 			height: 200rpx;
 			border-radius: 14rpx;
 			position: relative;
+
+			image {
+				border-radius: 14rpx;
+				overflow: hidden;
+			}
+		}
+
+		&-hot {
+			position: absolute;
+			top:0;
+			left:-20rpx;
+			z-index: 2;
+			display: flex;
+			flex-direction: row;
+			align-items: center;
+			height: 40rpx;
+		}
+
+		&-hot-flame {
+			font-size: 22rpx;
+			line-height: 1;
+			margin-right: 4rpx;
+		}
+
+		&-hot-label {
+			font-size: 22rpx;
+			font-weight: 600;
+			color: #FFFFFF;
+			line-height: 40rpx;
+		}
+
+		&-hot-count {
+			position: absolute;
+			left:130rpx;
+			font-family: PingFangSC, PingFang SC;
+			font-weight: 600;
+			font-size: 40rpx;
+			color: #FFFFFF;
+			line-height: 56rpx;
+			text-align: justify;
 		}
 		&-status {
 			position: absolute;

+ 19 - 0
pages_course/utils/productHotSale.js

@@ -0,0 +1,19 @@
+/** 热卖标签展示数量上限 */
+export const HOT_SALE_MAX = 9999
+
+/** 商品是否展示热卖标签 */
+export function hasHotSaleTag(item) {
+	return item != null && item.hotSaleTags != null
+}
+
+/** 热卖计数 map 的 key(按商品 id) */
+export function getProductHotKey(item) {
+	if (!item) return ''
+	return String(item.productId || item.packageId || '')
+}
+
+/** 限制热卖数量在 0 ~ HOT_SALE_MAX */
+export function clampHotSaleCount(count) {
+	const n = Number(count) || 0
+	return Math.min(HOT_SALE_MAX, Math.max(0, Math.floor(n)))
+}

+ 159 - 5
pages_course/video.vue

@@ -134,7 +134,10 @@
 			  <cover-view class="close-box" @click.stop="closeCardPopup">
 			    <cover-image src="/static/images/close.png"></cover-image>
 			  </cover-view>
-			
+			  <cover-view class="goods-cover-hot" v-if="hasHotSaleTag(currentCardItem) && getProductHotCount(currentCardItem) > 0">
+			   <cover-image style="height: 28px;" src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/class/remai.png" mode="heightFix"></cover-image>
+			     <cover-view class="goods-cover-hot-text">{{ getProductHotCount(currentCardItem) }}</cover-view>
+			   </cover-view>
 			  <cover-view class="goods-cover-inner">
 			    <!-- 商品主图 -->
 			    <cover-view class="goods-cover-img">
@@ -182,6 +185,10 @@
 			<view class="close-box" @click.stop="closeCardPopup">
 				<image src="/static/images/close.png"></image>
 			</view>
+			<view class="goods-card-hot" v-if="hasHotSaleTag(currentCardItem) && getProductHotCount(currentCardItem) > 0">
+				<image style="height: 56rpx;" src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/class/remai.png" mode="heightFix"></image>
+				<text class="goods-card-hot-count">{{ getProductHotCount(currentCardItem) }}</text>
+			</view>
 			<view class="goods-card-inner">
 				<image class="goods-card-img" :src="currentCardItem.images || currentCardItem.image" mode="aspectFill"></image>
 				<view class="goods-card-info">
@@ -513,7 +520,7 @@
 		<!-- 购物车弹窗 -->
 		<u-popup :show="isCart" @close="closeShop" round="20rpx" bgColor="#fff">
 			<scroll-view class="scroll-view" style="height:500rpx;box-sizing: border-box;padding: 24rpx;" scroll-y="true" enable-flex>
-				<goodsList ref="goodsList" :treatmentPackage="displayProductList" :urlOption="urlOption"></goodsList>
+				<goodsList ref="goodsList" :treatmentPackage="displayProductList" :hotCountMap="productHotCountMap" :urlOption="urlOption"></goodsList>
 			</scroll-view>
 		</u-popup>
 		<!-- 更多操作弹窗 -->
@@ -755,6 +762,7 @@
 	import descInfoNav from "./components/descInfoNav.vue"
 	import commentBox from "./components/commentBox.vue"
 	import goodsList from "./components/goodsList.vue"
+	import { hasHotSaleTag, getProductHotKey, clampHotSaleCount } from './utils/productHotSale.js'
 	import {TOKEN_KEYAuto,generateRandomString} from '@/utils/courseTool.js'
 	import { buildFakeOrderPool } from '@/utils/videovipFakeMarquee.js'
 	import ykscreenRecord from "@/components/yk-screenRecord/yk-screenRecord.vue"
@@ -973,6 +981,8 @@
 				displayProductList: [], // 根据上架/下架时间过滤后的展示列表(用于购物车弹窗)
 				cardPopup: false, // 商品卡片弹窗
 				currentCardItem: null,
+				productHotCountMap: {},
+				productHotTimer: null,
 				dismissedCardKey: '',
 				// 跑马灯数据就绪门禁:避免首次进入时使用旧状态导致闪现
 				marqueeDataReady: false,
@@ -1193,7 +1203,19 @@
 					this._closeFeaturedCommentComposerUi()
 					this.closeFeaturedMediaActionSheet()
 				}
-			}
+			},
+			displayProductList: {
+				handler() {
+					this.syncProductHotCounts()
+				},
+				deep: true,
+			},
+			cardPopup() {
+				this.syncProductHotCounts()
+			},
+			currentCardItem() {
+				this.syncProductHotCounts()
+			},
 		},
 		onLoad(option) {
 			this.getWebviewUrl()
@@ -1288,6 +1310,7 @@
 				this.stopFakeMarqueeLoop()
 			}
 			this.stopCountdown()
+			this.stopProductHotTimer()
 			// if (this.interval != null) {
 			// 	clearInterval(this.interval)
 			// 	this.interval = null
@@ -1313,6 +1336,7 @@
 			}
 			this.fullscreenToggleLock = false
 			this.stopCountdown()
+			this.stopProductHotTimer()
 			this.clearIntegral()
 			this.stopFakeMarqueeLoop()
 			this.fakeOrderPool = []
@@ -1344,6 +1368,7 @@
 			}
 			this.fullscreenToggleLock = false
 			this.stopCountdown()
+			this.stopProductHotTimer()
 			this.clearIntegral()
 			this.stopFakeMarqueeLoop()
 			// #ifndef H5
@@ -1679,6 +1704,74 @@
 				}
 				this.cardPopup = false
 			},
+			hasHotSaleTag(item) {
+				return hasHotSaleTag(item)
+			},
+			getProductHotCount(item) {
+				if (!hasHotSaleTag(item)) return 0
+				const key = getProductHotKey(item)
+				return key ? clampHotSaleCount(this.productHotCountMap[key]) : 0
+			},
+			_collectHotSaleProducts() {
+				const seen = new Set()
+				const list = []
+				const add = (item) => {
+					if (!hasHotSaleTag(item)) return
+					const key = getProductHotKey(item)
+					if (!key || seen.has(key)) return
+					seen.add(key)
+					list.push(item)
+				}
+				;(this.displayProductList || []).forEach(add)
+				if (this.cardPopup && this.currentCardItem) add(this.currentCardItem)
+				return list
+			},
+			_randomInt(min, max) {
+				return Math.floor(Math.random() * (max - min + 1)) + min
+			},
+			syncProductHotCounts() {
+				const products = this._collectHotSaleProducts()
+				const next = { ...this.productHotCountMap }
+				const activeKeys = new Set()
+				products.forEach((item) => {
+					const key = getProductHotKey(item)
+					if (!key) return
+					activeKeys.add(key)
+					if (!next[key]) {
+						next[key] = clampHotSaleCount(this._randomInt(100, 800))
+					}
+				})
+				Object.keys(next).forEach((key) => {
+					if (!activeKeys.has(key)) delete next[key]
+				})
+				this.productHotCountMap = next
+				if (activeKeys.size) {
+					this.startProductHotTimer()
+				} else {
+					this.stopProductHotTimer()
+				}
+			},
+			startProductHotTimer() {
+				if (this.productHotTimer) return
+				this.productHotTimer = setInterval(() => {
+					const products = this._collectHotSaleProducts()
+					if (!products.length) return
+					const next = { ...this.productHotCountMap }
+					products.forEach((item) => {
+						const key = getProductHotKey(item)
+						if (key && next[key]) {
+							next[key] = clampHotSaleCount(next[key] + this._randomInt(10, 20))
+						}
+					})
+					this.productHotCountMap = next
+				}, 30000)
+			},
+			stopProductHotTimer() {
+				if (this.productHotTimer) {
+					clearInterval(this.productHotTimer)
+					this.productHotTimer = null
+				}
+			},
 			openList(){
 				const wasShu = this.isShu
 				this.isShu=!this.isShu
@@ -2683,6 +2776,7 @@
 			this.cardPopup = !!activeCard && this.dismissedCardKey !== activeCardKey
 			//this.getVideoContainerHeight()
 			this.currentCardItem = activeCard
+			this.syncProductHotCounts()
 			this._syncFakeMarqueeIfEligibleChanged()
 		},
 			//播放时间更新事件方法
@@ -5462,11 +5556,35 @@
        z-index: 99999;
        background: #FFFFFF;
        border-radius: 10px;
-       overflow: hidden;
+       overflow: visible;
        bottom: 40px;
+
+       &-hot {
+        position: absolute;
+        top: -33px;
+        left: 15px;
+        z-index: 11;
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+       }
+       
+       &-hot-text {
+         position: absolute;
+         left:65px;
+         font-family: PingFangSC, PingFang SC;
+         font-weight: 600;
+         font-size: 20px;
+         color: #FFFFFF;
+         line-height: 28px;
+         text-align: justify;
+       }
+
        &-inner {
          display: flex;
          flex-direction:column;
+         border-radius: 10px;
+         overflow: hidden;
        }
      
        &-img {
@@ -5562,11 +5680,47 @@
 		width: 300rpx;
 		background: #FFFFFF;
 		border-radius: 20rpx 20rpx 0rpx 0rpx;
-		overflow: hidden;
+		overflow: visible;
+
+		&-hot {
+			position: absolute;
+			top: -66rpx;
+			left: 30rpx;
+			z-index: 11;
+			display: flex;
+			flex-direction: row;
+			align-items: center;
+		}
+
+		&-hot-flame {
+			font-size: 24rpx;
+			line-height: 1;
+			margin-right: 4rpx;
+		}
+
+		&-hot-label {
+			font-size: 24rpx;
+			font-weight: 600;
+			color: #FFFFFF;
+			line-height: 44rpx;
+		}
+
+		&-hot-count {
+			position: absolute;
+			left:130rpx;
+			font-family: PingFangSC, PingFang SC;
+			font-weight: 600;
+			font-size: 40rpx;
+			color: #FFFFFF;
+			line-height: 56rpx;
+			text-align: justify;
+		}
 
 		&-inner {
 			display: flex;
 			flex-direction: column;
+			border-radius: 20rpx 20rpx 0 0;
+			overflow: hidden;
 		}
 
 		&-img {

+ 159 - 3
pages_course/videovip.vue

@@ -116,6 +116,10 @@
 					  <cover-view class="close-box" @click.stop="closeCardPopup">
 					    <cover-image src="/static/images/close.png"></cover-image>
 					  </cover-view>
+					  <cover-view class="goods-cover-hot" v-if="hasHotSaleTag(currentCardItem) && getProductHotCount(currentCardItem) > 0">
+					  <cover-image style="height: 28px;" src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/class/remai.png" mode="heightFix"></cover-image>
+					    <cover-view class="goods-cover-hot-text">{{ getProductHotCount(currentCardItem) }}</cover-view>
+					  </cover-view>
 					
 					  <cover-view class="goods-cover-inner">
 					    <!-- 商品主图 -->
@@ -152,6 +156,10 @@
 			<view class="close-box" @click.stop="closeCardPopup">
 				<image src="/static/images/close.png"></image>
 			</view>
+			<view class="goods-card-hot" v-if="hasHotSaleTag(currentCardItem) && getProductHotCount(currentCardItem) > 0">
+				<image style="height: 56rpx;" src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/class/remai.png" mode="heightFix"></image>
+				<text class="goods-card-hot-count">{{ getProductHotCount(currentCardItem) }}</text>
+			</view>
 			<view class="goods-card-inner">
 				<image class="goods-card-img" :src="currentCardItem.images" mode="aspectFill"></image>
 				<view class="goods-card-info">
@@ -496,7 +504,7 @@
 		</u-popup>
 		<u-popup :show="isCart"  @close="closeShop" round="20rpx" bgColor="#fff">
 			<scroll-view class="scroll-view" style="height:500rpx;box-sizing: border-box;padding: 24rpx;" scroll-y="true" enable-flex>
-				<goodsList ref="goodsList" :treatmentPackage="displayProductList" :urlOption="urlOption"></goodsList>
+				<goodsList ref="goodsList" :treatmentPackage="displayProductList" :hotCountMap="productHotCountMap" :urlOption="urlOption"></goodsList>
 			</scroll-view>
 		</u-popup>
 		<u-popup :show="isMore"  @close="closeMore" round="20rpx" bgColor="#fff">
@@ -738,6 +746,7 @@
 
 <script>
 	import goodsList from "./components/goodsList.vue"
+	import { hasHotSaleTag, getProductHotKey, clampHotSaleCount } from './utils/productHotSale.js'
 	import {
 		generateRandomString
 	} from "@/utils/common.js"
@@ -962,6 +971,9 @@
 				// 商品卡片弹窗
 				cardPopup: false,
 				currentCardItem: null,
+				// 热卖标签数量(小黄车与弹窗卡片共用,按 productId)
+				productHotCountMap: {},
+				productHotTimer: null,
 				// 手动关闭后,当前卡片在其有效时段内不再重复弹出
 				dismissedCardKey: '',
 				// 完课积分倒计时(秒)
@@ -1237,6 +1249,18 @@
 			// 		this.closeFeaturedMediaActionSheet()
 			// 	}
 			// }
+			displayProductList: {
+				handler() {
+					this.syncProductHotCounts()
+				},
+				deep: true,
+			},
+			cardPopup() {
+				this.syncProductHotCounts()
+			},
+			currentCardItem() {
+				this.syncProductHotCounts()
+			},
 		},
 		onLoad(option) {
 			this.getWebviewUrl()
@@ -1374,6 +1398,7 @@
 			}
 			// 页面隐藏时停止完课积分倒计时
 			this.stopCountdown()
+			this.stopProductHotTimer()
 			// if (this.interval != null) {
 			// 	clearInterval(this.interval)
 			// 	this.interval = null
@@ -1400,6 +1425,7 @@
 			this.fullscreenToggleLock = false
 			// 页面卸载时清理完课积分倒计时
 			this.stopCountdown()
+			this.stopProductHotTimer()
 			this.clearIntegral()
 			this.stopFakeMarqueeLoop()
 			this.fakeOrderPool = []
@@ -1433,6 +1459,7 @@
 			this.fullscreenToggleLock = false
 			// 组件销毁前清理完课积分倒计时
 			this.stopCountdown()
+			this.stopProductHotTimer()
 			this.clearIntegral()
 			this.stopFakeMarqueeLoop()
 			// #ifndef H5
@@ -3187,6 +3214,7 @@
 				this.cardPopup = !!activeCard && this.dismissedCardKey !== activeCardKey
 				//this.getVideoContainerHeight()
 				this.currentCardItem = activeCard
+				this.syncProductHotCounts()
 				this._syncFakeMarqueeIfEligibleChanged()
 			},
 			closeCardPopup() {
@@ -3196,6 +3224,74 @@
 				}
 				this.cardPopup = false
 			},
+			hasHotSaleTag(item) {
+				return hasHotSaleTag(item)
+			},
+			getProductHotCount(item) {
+				if (!hasHotSaleTag(item)) return 0
+				const key = getProductHotKey(item)
+				return key ? clampHotSaleCount(this.productHotCountMap[key]) : 0
+			},
+			_collectHotSaleProducts() {
+				const seen = new Set()
+				const list = []
+				const add = (item) => {
+					if (!hasHotSaleTag(item)) return
+					const key = getProductHotKey(item)
+					if (!key || seen.has(key)) return
+					seen.add(key)
+					list.push(item)
+				}
+				;(this.displayProductList || []).forEach(add)
+				if (this.cardPopup && this.currentCardItem) add(this.currentCardItem)
+				return list
+			},
+			_randomInt(min, max) {
+				return Math.floor(Math.random() * (max - min + 1)) + min
+			},
+			syncProductHotCounts() {
+				const products = this._collectHotSaleProducts()
+				const next = { ...this.productHotCountMap }
+				const activeKeys = new Set()
+				products.forEach((item) => {
+					const key = getProductHotKey(item)
+					if (!key) return
+					activeKeys.add(key)
+					if (!next[key]) {
+						next[key] = clampHotSaleCount(this._randomInt(100, 800))
+					}
+				})
+				Object.keys(next).forEach((key) => {
+					if (!activeKeys.has(key)) delete next[key]
+				})
+				this.productHotCountMap = next
+				if (activeKeys.size) {
+					this.startProductHotTimer()
+				} else {
+					this.stopProductHotTimer()
+				}
+			},
+			startProductHotTimer() {
+				if (this.productHotTimer) return
+				this.productHotTimer = setInterval(() => {
+					const products = this._collectHotSaleProducts()
+					if (!products.length) return
+					const next = { ...this.productHotCountMap }
+					products.forEach((item) => {
+						const key = getProductHotKey(item)
+						if (key && next[key]) {
+							next[key] = clampHotSaleCount(next[key] + this._randomInt(10, 20))
+						}
+					})
+					this.productHotCountMap = next
+				}, 30000)
+			},
+			stopProductHotTimer() {
+				if (this.productHotTimer) {
+					clearInterval(this.productHotTimer)
+					this.productHotTimer = null
+				}
+			},
 			changeTime(that, e) {
 				that.playDurationSeek = 0
 			},
@@ -5237,11 +5333,47 @@
 		width: 300rpx;
 		background: #FFFFFF;
 		border-radius: 20rpx;
-		overflow: hidden;
+		overflow: visible;
+
+		&-hot {
+			position: absolute;
+			top: -66rpx;
+			left: 30rpx;
+			z-index: 11;
+			display: flex;
+			flex-direction: row;
+			align-items: center;
+		}
+
+		&-hot-flame {
+			font-size: 24rpx;
+			line-height: 1;
+			margin-right: 4rpx;
+		}
+
+		&-hot-label {
+			font-size: 24rpx;
+			font-weight: 600;
+			color: #FFFFFF;
+			line-height: 44rpx;
+		}
+
+		&-hot-count {
+			position: absolute;
+			left:130rpx;
+			font-family: PingFangSC, PingFang SC;
+			font-weight: 600;
+			font-size: 40rpx;
+			color: #FFFFFF;
+			line-height: 56rpx;
+			text-align: justify;
+		}
 
 		&-inner {
 			display: flex;
 			flex-direction:column;
+			border-radius: 20rpx;
+			overflow: hidden;
 		}
 
 		&-img {
@@ -5338,11 +5470,35 @@
      z-index: 99999;
      background: #FFFFFF;
      border-radius: 10px;
-     overflow: hidden;
+     overflow: visible;
      bottom: 40px;
+
+     &-hot {
+      position: absolute;
+      top: -33px;
+      left: 15px;
+      z-index: 11;
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+     }
+
+     &-hot-text {
+       position: absolute;
+       left:65px;
+       font-family: PingFangSC, PingFang SC;
+       font-weight: 600;
+       font-size: 20px;
+       color: #FFFFFF;
+       line-height: 28px;
+       text-align: justify;
+     }
+
      &-inner {
        display: flex;
        flex-direction:column;
+       border-radius: 10px;
+       overflow: hidden;
      }
    
      &-img {

+ 2 - 2
pages_index/courseSearch.vue

@@ -58,7 +58,7 @@
 					<view class="course-grid" :class="{ 'list-dimmed': courseRefreshing }">
 						<view
 							v-for="(course, idx) in courseList"
-							:key="course.courseId || idx"
+							:key="course.courseId"
 							class="course-card"
 							@click="onCourseClick(course)"
 						>
@@ -110,7 +110,7 @@
 					<view class="course-grid" style="padding: 0;">
 						<view
 							v-for="(course, idx) in recommendList"
-							:key="course.courseId || idx"
+							:key="course.courseId"
 							class="course-card"
 							@click="onCourseClick(course)"
 						>

+ 1 - 1
pages_user/user/address.vue

@@ -89,7 +89,7 @@
 			    // 获取上一页
 			    const prevPage = pages[pages.length - 2];
 			    // 判断上一页的路径 是否是 订单页
-			    if (prevPage.route === 'pages/shopping/confirmOrder') { // 👈 改成你真实的订单页路径
+			    if (prevPage.route !== 'pages/user/index') { // 👈 改成你真实的订单页路径
 			      uni.$emit('updateAddress',item);
 				  uni.navigateBack({ delta: 1 });
 			    } else {