liujiaxin 8 시간 전
부모
커밋
b8b558ff04

+ 18 - 14
.hbuilderx/launch.json

@@ -1,16 +1,20 @@
-{ // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
-  // launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
-    "version": "0.0",
-    "configurations": [{
-     	"default" : 
-     	{
-     		"launchtype" : "local"
-     	},
-     	"mp-weixin" : 
-     	{
-     		"launchtype" : "local"
-     	},
-     	"type" : "uniCloud"
-     }
+{
+    // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
+    // launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
+    "version" : "0.0",
+    "configurations" : [
+        {
+            "default" : {
+                "launchtype" : "local"
+            },
+            "mp-weixin" : {
+                "launchtype" : "local"
+            },
+            "type" : "uniCloud"
+        },
+        {
+            "playground" : "standard",
+            "type" : "uni-app:app-android"
+        }
     ]
 }

+ 22 - 0
api/groupBuy.js

@@ -0,0 +1,22 @@
+import Request from '../common/request.js';
+let request = new Request().http;
+
+// 团购商品列表
+export function getActiveGroupBuyList(data) {
+    return request('/productGroupBuy/activeList', data, 'GET');
+}
+
+// 团购商品详情
+export function getGroupBuyDetail(id) {
+    return request(`/productGroupBuy/detail/${id}`, null, 'GET');
+}
+
+// 按商品id查询团购
+export function getGroupBuyByProductId(productId) {
+    return request(`/productGroupBuy/getByProductId/${productId}`, null, 'GET');
+}
+
+// 获取服务器时间
+export function getGroupBuyServerTime() {
+    return request('/productGroupBuy/serverTime', null, 'GET');
+}

+ 5 - 0
api/user.js

@@ -61,6 +61,11 @@ let request = new Request().http
  	 return request('/app/common/getConfigByKey',data,'GET');
  }
  
+  //查看可领取优惠券
+ export function getCouponList(data) {
+ 	 return request('/storeCoupon/list',data,'GET');
+ }
+ 
  
  
  

+ 3 - 1
common/request.js

@@ -8,7 +8,9 @@ export default class Request {
 			// let path ='http://p6c668f9.natappfree.cc/store'//本地
 		// let path ='https://storeuserapp.bjyjbao.com/store'//本地
 		// let path = 'https://userapp.bjyjbao.com/store'//医 健宝
-		let path = 'http://192.168.10.196:8113/store'//云联 融智
+		// let path = 'http://vef6cbdf.natappfree.cc/store'//张圆圆
+		let path = 'http://yd966f99.natappfree.cc/store'//汪于杰
+
 		let type = 0
 		uni.setStorageSync('requestPath',path)
 		// uni.showLoading({

+ 4 - 5
package-lock.json

@@ -9,7 +9,7 @@
       "version": "1.0.0",
       "license": "ISC",
       "dependencies": {
-        "dayjs": "^1.11.13",
+        "dayjs": "^1.11.20",
         "uview-ui": "^2.0.38",
         "vuex": "^4.1.0"
       }
@@ -193,10 +193,9 @@
       "peer": true
     },
     "node_modules/dayjs": {
-      "version": "1.11.13",
-      "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz",
-      "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
-      "license": "MIT"
+      "version": "1.11.20",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
+      "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="
     },
     "node_modules/entities": {
       "version": "4.5.0",

+ 1 - 1
package.json

@@ -10,7 +10,7 @@
   "license": "ISC",
   "description": "",
   "dependencies": {
-    "dayjs": "^1.11.13",
+    "dayjs": "^1.11.20",
     "uview-ui": "^2.0.38",
     "vuex": "^4.1.0"
   }

+ 17 - 0
pages.json

@@ -567,6 +567,15 @@
 						}
 					}
 				},
+				{
+					"path": "index/groupBuyList",
+					"style": {
+						"navigationBarTitleText": "今日团购",
+						"app-plus": {
+							"titleNView": false
+						}
+					}
+				},
 				{
 					"path": "index/activityProductDetail",
 					"style": {
@@ -590,6 +599,14 @@
 							"titleNView": false
 						}
 					}
+				},{
+					"path": "user/coupon",
+					"style": {
+						"navigationBarTitleText": "优惠券",
+						"app-plus": {
+							"titleNView": false
+						}
+					}
 				}, {
 					"path": "user/message",
 					"style": {

+ 335 - 0
pages/home/components/GroupBuy.vue

@@ -0,0 +1,335 @@
+<template>
+    <view class="group-goods" v-if="groupBuyList.length > 0">
+        <view class="title-box x-bc">
+            <view class="left-title">
+                <text class="title-icon">🔥</text>
+                <text class="title">今日团购</text>
+                <view class="home-countdown" v-if="globalCountdown > 0">
+                    <text class="time-block">{{ formatTimeObj(globalCountdown).h }}</text>
+                    <text class="time-colon">:</text>
+                    <text class="time-block">{{ formatTimeObj(globalCountdown).m }}</text>
+                    <text class="time-colon">:</text>
+                    <text class="time-block">{{ formatTimeObj(globalCountdown).s }}</text>
+                </view>
+            </view>
+            <view class="group-people x-f" @tap="navTo('/pages_index/index/groupBuyList')">
+                <text class="tip">更多</text>
+                <text class="cuIcon-right">></text>
+            </view>
+        </view>
+        <view class="goods-box">
+            <view class="min-goods" v-for="(item, index) in groupBuyList" :key="item.id" @tap="showProduct(item)">
+                <view class="img-box">
+                    <image class="img" :src="item.productImage" mode="aspectFill"></image>
+                    <view class="group-tag" v-if="item.groupNum">{{ item.groupNum }}人团</view>
+                </view>
+                <view class="info-content">
+                    <view class="title ellipsis">{{ item.productName }}</view>
+                    <view class="original-price">¥{{ item.originalPrice }}</view>
+                    <view class="price-section">
+                        <text class="current">¥{{ item.groupPrice }}</text>
+                        <view :class="['buy-btn', item.activityStatus === 'not_started' ? 'disabled' : '']">
+                            拼
+                        </view>
+                    </view>
+                </view>
+            </view>
+        </view>
+    </view>
+</template>
+
+<script>
+import { getActiveGroupBuyList, getGroupBuyServerTime } from '@/api/groupBuy.js';
+
+export default {
+    name: "GroupBuy",
+    data() {
+        return {
+            groupBuyList: [],
+            timer: null,
+            globalCountdown: 0,
+            serverTimestamp: 0
+        };
+    },
+    mounted() {
+        this.getGroupBuyData();
+    },
+    beforeDestroy() {
+        if (this.timer) {
+            clearInterval(this.timer);
+        }
+    },
+    methods: {
+        async getGroupBuyData() {
+            try {
+                // 并发请求列表和服务器时间
+                const [listRes, timeRes] = await Promise.all([
+                    getActiveGroupBuyList(),
+                    getGroupBuyServerTime()
+                ]);
+
+                let currentServerTime = Date.now();
+                if (timeRes.code === 0 || timeRes.code === 200) {
+                    currentServerTime = timeRes.serverTimestamp || timeRes.data?.serverTimestamp || Date.now();
+                } else if (listRes.serverTimestamp) {
+                    currentServerTime = listRes.serverTimestamp;
+                }
+                
+                // 将服务器时间转为秒级用于校准倒计时
+                const serverTimeSec = Math.floor(currentServerTime / 1000);
+
+                if (listRes.code === 0 || listRes.code === 200) {
+                    let list = listRes.data || [];
+                    this.groupBuyList = list.slice(0, 6);
+                    
+                    // 基于服务器时间校准倒计时
+                    this.groupBuyList.forEach(item => {
+                        let targetTime = 0;
+                        if (item.activityStatus === 'not_started') {
+                            targetTime = Math.floor(new Date(item.startTime.replace(/-/g, '/')).getTime() / 1000);
+                        } else if (item.activityStatus === 'ongoing') {
+                            targetTime = Math.floor(new Date(item.endTime.replace(/-/g, '/')).getTime() / 1000);
+                        }
+                        
+                        if (targetTime > 0) {
+                            let diff = targetTime - serverTimeSec;
+                            item.countdown = diff > 0 ? diff : 0;
+                        }
+                    });
+
+                    if (this.groupBuyList.length > 0) {
+                        this.globalCountdown = this.groupBuyList[0].countdown;
+                    }
+                    this.startCountdown();
+                }
+            } catch (error) {
+                console.error('获取团购数据失败', error);
+            }
+        },
+        startCountdown() {
+            if (this.timer) {
+                clearInterval(this.timer);
+            }
+            this.timer = setInterval(() => {
+                let hasCountdown = false;
+                if (this.globalCountdown > 0) {
+                    this.globalCountdown--;
+                }
+                this.groupBuyList.forEach(item => {
+                    if (item.countdown > 0) {
+                        item.countdown--;
+                        hasCountdown = true;
+                    } else if (item.countdown === 0 && item.activityStatus === 'not_started') {
+                        item.activityStatus = 'ongoing';
+                        // 到达开始时间后,将倒计时切换为距离结束时间的倒计时
+                        let endTimeSec = Math.floor(new Date(item.endTime.replace(/-/g, '/')).getTime() / 1000);
+                        let nowSec = Math.floor(Date.now() / 1000); // 粗略计算,可结合初始偏差
+                        let diff = endTimeSec - nowSec;
+                        item.countdown = diff > 0 ? diff : 0;
+                        if (item.countdown > 0) hasCountdown = true;
+                    } else if (item.countdown === 0 && item.activityStatus === 'ongoing') {
+                        item.activityStatus = 'ended';
+                    }
+                });
+                if (!hasCountdown && this.globalCountdown <= 0) {
+                    clearInterval(this.timer);
+                }
+            }, 1000);
+        },
+        formatTimeObj(seconds) {
+            if (!seconds || seconds <= 0) return { h: '00', m: '00', s: '00' };
+            let h = Math.floor(seconds / 3600);
+            let m = Math.floor((seconds % 3600) / 60);
+            let s = seconds % 60;
+            return {
+                h: h.toString().padStart(2, '0'),
+                m: m.toString().padStart(2, '0'),
+                s: s.toString().padStart(2, '0')
+            };
+        },
+        navTo(url) {
+            uni.navigateTo({
+                url: url
+            });
+        },
+        showProduct(item) {
+            uni.navigateTo({
+                url: '/pages_index/index/activityProductDetail?type=group&id=' + item.id
+            });
+        }
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+.group-goods {
+    position: relative;
+    z-index: 1;
+    background: linear-gradient(180deg, #f0f3ff 0%, #FFFFFF 20%);
+    border-radius: 16upx;
+    margin-bottom: 20upx;
+    margin-top: 20upx;
+    padding: 20upx;
+}
+
+.x-bc {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.x-f {
+    display: flex;
+    align-items: center;
+}
+
+.title-box {
+    padding-bottom: 20rpx;
+
+    .left-title {
+        display: flex;
+        align-items: center;
+        
+        .title-icon {
+            font-size: 36rpx;
+            color: #4C49E9;
+            margin-right: 10rpx;
+        }
+
+        .title {
+            font-size: 32rpx;
+            font-weight: bold;
+            color: #333;
+            margin-right: 16rpx;
+        }
+
+        .home-countdown {
+            display: flex;
+            align-items: center;
+
+            .time-block {
+                background: #4C49E9;
+                color: #fff;
+                font-size: 20rpx;
+                padding: 2rpx 6rpx;
+                border-radius: 6rpx;
+                line-height: 28rpx;
+            }
+
+            .time-colon {
+                color: #4C49E9;
+                font-size: 24rpx;
+                margin: 0 4rpx;
+                font-weight: bold;
+            }
+        }
+    }
+
+    .group-people {
+        .tip {
+            font-size: 24rpx;
+            color: #999999;
+        }
+
+        .cuIcon-right {
+            font-size: 24rpx;
+            color: #999;
+            margin-left: 4rpx;
+        }
+    }
+}
+
+.goods-box {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: start;
+
+    .min-goods {
+        width: 210rpx;
+        background: #fff;
+        margin-bottom: 20rpx;
+        margin: 0 14rpx;
+        border-radius: 12rpx;
+        overflow: hidden;
+
+        .img-box {
+            width: 210rpx;
+            height: 210rpx;
+            position: relative;
+            overflow: hidden;
+
+            .img {
+                width: 100%;
+                height: 100%;
+                background-color: #f5f5f5;
+            }
+
+            .group-tag {
+                position: absolute;
+                left: 0;
+                top: 0;
+                background: linear-gradient(90deg, #ff4a00, #ff6a00);
+                color: #fff;
+                font-size: 18rpx;
+                padding: 2rpx 10rpx;
+                border-bottom-right-radius: 12rpx;
+                z-index: 2;
+            }
+        }
+
+        .info-content {
+            padding: 10rpx 8rpx;
+
+            .title {
+                font-size: 24rpx;
+                color: #333;
+                line-height: 34rpx;
+                height: 34rpx;
+                margin-bottom: 4rpx;
+            }
+
+            .ellipsis {
+                white-space: nowrap;
+                overflow: hidden;
+                text-overflow: ellipsis;
+            }
+
+            .original-price {
+                font-size: 20rpx;
+                color: #999;
+                text-decoration: line-through;
+                margin-bottom: 4rpx;
+            }
+
+            .price-section {
+                display: flex;
+                justify-content: space-between;
+                align-items: center;
+                border: 1rpx solid #4C49E9;
+                border-radius: 6rpx;
+                overflow: hidden;
+
+                .current {
+                    font-size: 24rpx;
+                    font-weight: bold;
+                    color: #4C49E9;
+                    padding: 0 6rpx;
+                    flex: 1;
+                }
+
+                .buy-btn {
+                    background: #4C49E9;
+                    color: #fff;
+                    font-size: 22rpx;
+                    padding: 4rpx 12rpx;
+                    
+                    &.disabled {
+                        background: #ccc;
+                        color: #fff;
+                    }
+                }
+            }
+        }
+    }
+}
+</style>

+ 5 - 0
pages/home/index.vue

@@ -158,6 +158,9 @@
 			<!-- 限时秒杀 -->
 			<FlashSale />
 
+			<!-- 今日团购 -->
+			<GroupBuy />
+
 			<!-- 限时折扣 -->
 			<DiscountProduct />
 
@@ -284,6 +287,7 @@
 	import Menu from '@/components/Menu.vue'
 	import HotProduct from './components/HotProduct.vue'
 	import FlashSale from './components/FlashSale.vue'
+	import GroupBuy from './components/GroupBuy.vue'
 	import DiscountProduct from './components/DiscountProduct.vue'
 	import NewProduct from './components/NewProduct.vue'
 	import TuiProduct from '@/components/tuiProduct.vue'
@@ -298,6 +302,7 @@
 			Menu,
 			HotProduct,
 			FlashSale,
+			GroupBuy,
 			DiscountProduct,
 			NewProduct,
 			TuiProduct

+ 4 - 4
pages/shopping/confirmOrder.vue

@@ -237,8 +237,8 @@
 			},
 			confirm(item){
 				let data = {type:this.type,cartIds:this.cartIds};
-				// 若是从秒杀折扣跳入,需要通过 activityId 等传参,可做兼容
-				if (this.activityType === 'flash' || this.activityType === 'discount') {
+				// 若是从秒杀折扣或团购跳入,需要通过 activityId 等传参,可做兼容
+				if (this.activityType === 'flash' || this.activityType === 'discount' || this.activityType === 'group') {
 					// data.activityId = this.activityId;
 					// data.productId = this.productId;
 					data.productType=this.orderType
@@ -345,8 +345,8 @@
 					data.storeId=this.storeId;
 				}
 				
-				// 针对秒杀或折扣的特殊处理
-				if (this.activityType === 'flash' || this.activityType === 'discount' || this.orderType == 6 || this.orderType == 7) {
+				// 针对秒杀、折扣或团购的特殊处理
+				if (this.activityType === 'flash' || this.activityType === 'discount' || this.activityType === 'group' || this.orderType == 6 || this.orderType == 7 || this.orderType == 8) {
 					data.orderType = this.orderType;
 					data.associatedId = this.associatedId; // 传入活动ID
 					data.productId = this.productId;     // 确保能拿到商品信息

+ 8 - 1
pages/shopping/productDetails.vue

@@ -195,6 +195,7 @@
 	import {getProductDetails,getCartCount,addCart} from '@/api/product'
 	import {getFlashSaleByProductId} from '@/api/flashSale'
 	import {getDiscountByProductId} from '@/api/discount'
+	import {getGroupBuyByProductId} from '@/api/groupBuy'
 	import popupBottom from '@/components/px-popup-bottom/px-popup-bottom.vue'
 	export default {
 		components: {
@@ -266,7 +267,7 @@
 			this.getDicts();
 			this.productId = options.productId;
 			
-			// 检查是否参与了秒杀或折扣活动,若是则跳转到活动专属详情页
+			// 检查是否参与了秒杀、折扣或团购活动,若是则跳转到活动专属详情页
 			if (this.productId) {
 				getFlashSaleByProductId(this.productId).then(res => {
 					if (res.code === 0 && res.data) {
@@ -275,6 +276,12 @@
 						getDiscountByProductId(this.productId).then(res2 => {
 							if (res2.code === 0 && res2.data) {
 								uni.redirectTo({ url: `/pages_index/index/activityProductDetail?type=discount&id=${res2.data.id}` });
+							} else {
+								getGroupBuyByProductId(this.productId).then(res3 => {
+									if (res3.code === 0 && res3.data) {
+										uni.redirectTo({ url: `/pages_index/index/activityProductDetail?type=group&id=${res3.data.id}` });
+									}
+								});
 							}
 						});
 					}

+ 67 - 20
pages_index/index/activityProductDetail.vue

@@ -33,10 +33,13 @@
 		
 		<!-- 商品基础信息 -->
 		<view class="base-info">
-			<view class="title">{{ detailData.productName }}</view>
+			<view class="title">
+				<text class="group-tag" v-if="activityType === 'group' && detailData.groupNum">{{ detailData.groupNum }}人团</text>
+				{{ detailData.productName }}
+			</view>
 			<view class="sub-info">
 				<text class="sales">销量: {{ detailData.sales || 0 }}</text>
-				<text class="stock">剩余库存: {{ detailData.remainStock || 0 }}</text>
+				<text class="stock">剩余库存: {{ detailData.productStock || 0 }}</text>
 			</view>
 		</view>
 		
@@ -81,7 +84,7 @@
 						</view>
 						<view class="desc-box">
 							<text class="text">已选:{{ currentSpec.specName || '默认规格' }}</text>
-							<text class="text">库存:{{ currentSpec.remainStock !== undefined ? currentSpec.remainStock : detailData.remainStock }}</text>
+							<text class="text">库存:{{ currentSpec.stock !== undefined ? currentSpec.stock : detailData.productStock }}</text>
 						</view>
 					</view>
 				</view>
@@ -95,7 +98,7 @@
 							:key="index" 
 							:class="['item', specIndex === index ? 'active' : '']" 
 							@click="choseSpec(index)">
-							{{ item.specName }}
+							{{ item.specName || '默认规格' }}
 						</view>
 					</view>
 				</view>
@@ -122,6 +125,7 @@
 <script>
 import { getFlashSaleDetail, getFlashSaleServerTime } from '@/api/flashSale.js';
 import { getDiscountDetail, getDiscountServerTime } from '@/api/discount.js';
+import { getGroupBuyDetail, getGroupBuyServerTime } from '@/api/groupBuy.js';
 import { addCart } from '@/api/product.js';
 import popupBottom from '@/components/px-popup-bottom/px-popup-bottom.vue';
 
@@ -131,7 +135,7 @@ export default {
 	},
 	data() {
 		return {
-			activityType: 'flash', // 'flash' 或 'discount'
+			activityType: 'flash', // 'flash', 'discount' 或 'group'
 			activityId: '',
 			detailData: {},
 			timer: null,
@@ -145,8 +149,10 @@ export default {
 		currentPrice() {
 			if (this.activityType === 'flash') {
 				return this.detailData.flashPrice || 0;
+			} else if (this.activityType === 'discount') {
+				return this.detailData.discountPrice || this.detailData.groupPrice || 0;
 			} else {
-				return this.detailData.discountPrice || 0;
+				return this.detailData.groupPrice || 0;
 			}
 		},
 		btnText() {
@@ -154,8 +160,8 @@ export default {
 			switch (status) {
 				case 'not_started': 
 					let time = this.formatTime(this.detailData.countdown);
-					return `即将开 ${time.h}:${time.m}:${time.s}`;
-				case 'ongoing': return '立即抢购';
+					return `即将开 ${time.h}:${time.m}:${time.s}`;
+				case 'ongoing': return this.activityType === 'group' ? '立即参团' : '立即抢购';
 				case 'sold_out': return '已售罄';
 				case 'ended': return '已结束';
 				default: return '抢购';
@@ -177,7 +183,13 @@ export default {
 			if (this.detailData.specs && this.detailData.specs.length > 0) {
 				let spec = this.detailData.specs[this.specIndex];
 				if (spec) {
-					spec.price = this.activityType === 'flash' ? spec.flashPrice : spec.discountPrice;
+					if (this.activityType === 'flash') {
+						spec.price = spec.flashPrice;
+					} else if (this.activityType === 'discount') {
+						spec.price = spec.discountPrice || spec.groupPrice;
+					} else {
+						spec.price = spec.groupPrice;
+					}
 					return spec;
 				}
 			}
@@ -187,9 +199,11 @@ export default {
 	onLoad(options) {
 		if (options.type) {
 			this.activityType = options.type;
-			uni.setNavigationBarTitle({
-				title: this.activityType === 'flash' ? '秒杀商品详情' : '折扣商品详情'
-			});
+			let title = '商品详情';
+			if (this.activityType === 'flash') title = '秒杀商品详情';
+			else if (this.activityType === 'discount') title = '折扣商品详情';
+			else if (this.activityType === 'group') title = '团购商品详情';
+			uni.setNavigationBarTitle({ title });
 		}
 		if (options.id) {
 			this.activityId = options.id;
@@ -227,11 +241,16 @@ export default {
 						getFlashSaleDetail(this.activityId),
 						getFlashSaleServerTime()
 					]);
-				} else {
+				} else if (this.activityType === 'discount') {
 					[detailRes, timeRes] = await Promise.all([
 						getDiscountDetail(this.activityId),
 						getDiscountServerTime()
 					]);
+				} else {
+					[detailRes, timeRes] = await Promise.all([
+						getGroupBuyDetail(this.activityId),
+						getGroupBuyServerTime()
+					]);
 				}
 				
 				if (detailRes.code === 0 || detailRes.code === 200) {
@@ -301,14 +320,16 @@ export default {
 					let res;
 					if (this.activityType === 'flash') {
 						res = await getFlashSaleDetail(this.activityId);
-					} else {
+					} else if (this.activityType === 'discount') {
 						res = await getDiscountDetail(this.activityId);
+					} else {
+						res = await getGroupBuyDetail(this.activityId);
 					}
 					
 					if (res.code === 0 || res.code === 200) {
-						let remainStock = res.data?.remainStock || 0;
-						this.detailData.remainStock = remainStock;
-						if (remainStock <= 0) {
+						let productStock = res.data?.productStock || 0;
+						this.detailData.productStock = productStock;
+						if (productStock <= 0) {
 							this.detailData.activityStatus = 'sold_out';
 							this.clearStockPolling();
 						}
@@ -346,7 +367,7 @@ export default {
 				uni.showToast({ title: '抱歉,活动已结束', icon: 'none' });
 				return;
 			}
-			if (status === 'sold_out' || this.detailData.remainStock <= 0) {
+			if (status === 'sold_out' || this.detailData.productStock <= 0) {
 				uni.showToast({ title: '活动商品已售罄', icon: 'none' });
 				return;
 			}
@@ -362,7 +383,7 @@ export default {
 			this.specIndex = index;
 		},
 		submitSpec() {
-			let stock = this.currentSpec.remainStock !== undefined ? this.currentSpec.remainStock : this.detailData.remainStock;
+			let stock = this.currentSpec.stock !== undefined ? this.currentSpec.stock : this.detailData.productStock;
 			if (stock <= 0) {
 				uni.showToast({ title: '该规格库存不足', icon: 'none' });
 				return;
@@ -387,7 +408,11 @@ export default {
 				if (res.code === 200) {
 					this.specVisible = false; // 关闭弹窗
 					let cartIds = res.id;
-					let orderType = this.activityType === 'flash' ? 6 : 7;
+					let orderType = 6;
+					if (this.activityType === 'flash') orderType = 6;
+					else if (this.activityType === 'discount') orderType = 7;
+					else if (this.activityType === 'group') orderType = 8;
+					
 					// associatedId 为中间表 ID,即 specs 数组里的 id
 					let associatedId = this.detailData.specs && this.detailData.specs.length > 0 ? (this.detailData.specs[this.specIndex].id || this.activityId) : this.activityId;
 					
@@ -441,6 +466,10 @@ export default {
 	&.discount {
 		background: linear-gradient(90deg, #b835c9, #9c26b0);
 	}
+
+	&.group {
+		background: linear-gradient(90deg, #66b2ef, #4C49E9);
+	}
 	
 	.price-info {
 		display: flex;
@@ -504,6 +533,20 @@ export default {
 		font-weight: bold;
 		line-height: 44rpx;
 		margin-bottom: 20rpx;
+
+		.group-tag {
+			display: inline-block;
+			padding: 0 12rpx;
+			height: 34rpx;
+			line-height: 34rpx;
+			background: #4C49E9;
+			color: #fff;
+			font-size: 20rpx;
+			border-radius: 4rpx;
+			margin-right: 12rpx;
+			vertical-align: middle;
+			font-weight: normal;
+		}
 	}
 	
 	.sub-info {
@@ -574,6 +617,10 @@ export default {
 			&.discount {
 				background: linear-gradient(90deg, #b835c9, #9c26b0);
 			}
+
+			&.group {
+				background: linear-gradient(90deg, #66b2ef, #4C49E9);
+			}
 			
 			&.disabled {
 				background: #cccccc;

+ 314 - 0
pages_index/index/groupBuyList.vue

@@ -0,0 +1,314 @@
+<template>
+	<view class="container">
+		<!-- 商品列表 -->
+		<view class="goods-list">
+			<view class="goods-item" v-for="(item, index) in list" :key="item.id" @tap="showProduct(item)">
+				<image class="goods-img" :src="item.productImage" mode="aspectFill"></image>
+				
+				<view class="goods-info">
+					<view class="goods-name ellipsis2">
+						<text class="group-tag" v-if="item.groupNum">{{ item.groupNum }}人团</text>
+						{{ item.productName }}
+					</view>
+					
+					<view class="countdown-box" v-if="item.activityStatus !== 'ended' && item.activityStatus !== 'sold_out'">
+						<text class="status-text" style="margin-left: 0; margin-right: 10rpx;">{{ item.activityStatus === 'not_started' ? '即将开抢' : '团购中' }}</text>
+						<text class="time-text">{{ formatTime(item.countdown) }}</text>
+					</view>
+					
+					<view class="goods-bottom">
+						<view class="price-info">
+							<view class="group-buy-price">¥<text class="num">{{ item.groupPrice }}</text></view>
+							<view class="original-price">¥{{ item.originalPrice }}</view>
+						</view>
+						
+						<view class="btn-box">
+							<button 
+								class="grab-btn" 
+								:class="[
+									item.activityStatus === 'not_started' ? 'not-started' : '',
+									item.activityStatus === 'sold_out' ? 'sold-out' : '',
+									item.activityStatus === 'ended' ? 'ended' : ''
+								]"
+							>
+								{{ getBtnText(item.activityStatus) }}
+							</button>
+						</view>
+					</view>
+				</view>
+			</view>
+		</view>
+		
+		<view class="empty-box" v-if="list.length === 0 && !loading">
+			<text>暂无团购活动</text>
+		</view>
+	</view>
+</template>
+
+<script>
+import { getActiveGroupBuyList, getGroupBuyServerTime } from '@/api/groupBuy.js';
+
+export default {
+	data() {
+		return {
+			list: [],
+			timer: null,
+			loading: true,
+			serverTimestamp: 0
+		};
+	},
+	onLoad() {
+		this.fetchData();
+	},
+	onUnload() {
+		this.clearTimer();
+	},
+	methods: {
+		async fetchData() {
+			this.loading = true;
+			try {
+				const [listRes, timeRes] = await Promise.all([
+					getActiveGroupBuyList(),
+					getGroupBuyServerTime()
+				]);
+				
+				this.loading = false;
+
+				let currentServerTime = Date.now();
+				if (timeRes.code === 0 || timeRes.code === 200) {
+					currentServerTime = timeRes.serverTimestamp || timeRes.data?.serverTimestamp || Date.now();
+				} else if (listRes.serverTimestamp) {
+					currentServerTime = listRes.serverTimestamp;
+				}
+				
+				const serverTimeSec = Math.floor(currentServerTime / 1000);
+
+				if (listRes.code === 0 || listRes.code === 200) {
+					this.list = listRes.data || [];
+					
+					this.list.forEach(item => {
+						let targetTime = 0;
+						if (item.activityStatus === 'not_started') {
+							targetTime = Math.floor(new Date(item.startTime.replace(/-/g, '/')).getTime() / 1000);
+						} else if (item.activityStatus === 'ongoing') {
+							targetTime = Math.floor(new Date(item.endTime.replace(/-/g, '/')).getTime() / 1000);
+						}
+						
+						if (targetTime > 0) {
+							let diff = targetTime - serverTimeSec;
+							item.countdown = diff > 0 ? diff : 0;
+						}
+					});
+					
+					this.startTimer();
+				} else {
+					uni.showToast({
+						title: listRes.msg || '获取数据失败',
+						icon: 'none'
+					});
+				}
+			} catch (error) {
+				this.loading = false;
+				console.error(error);
+			}
+		},
+		startTimer() {
+			this.clearTimer();
+			this.timer = setInterval(() => {
+				let hasCountdown = false;
+				this.list.forEach(item => {
+					if (item.countdown > 0) {
+						item.countdown--;
+						hasCountdown = true;
+					} else if (item.countdown === 0 && item.activityStatus === 'not_started') {
+						item.activityStatus = 'ongoing';
+						let endTimeSec = Math.floor(new Date(item.endTime.replace(/-/g, '/')).getTime() / 1000);
+						let nowSec = Math.floor(Date.now() / 1000);
+						let diff = endTimeSec - nowSec;
+						item.countdown = diff > 0 ? diff : 0;
+						if (item.countdown > 0) hasCountdown = true;
+					} else if (item.countdown === 0 && item.activityStatus === 'ongoing') {
+						item.activityStatus = 'ended';
+					}
+				});
+				if (!hasCountdown) {
+					this.clearTimer();
+				}
+			}, 1000);
+		},
+		clearTimer() {
+			if (this.timer) {
+				clearInterval(this.timer);
+				this.timer = null;
+			}
+		},
+		formatTime(seconds) {
+			if (!seconds || seconds <= 0) return '00:00:00';
+			let h = Math.floor(seconds / 3600);
+			let m = Math.floor((seconds % 3600) / 60);
+			let s = seconds % 60;
+			return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
+		},
+		getBtnText(status) {
+			switch (status) {
+				case 'not_started': return '即将开始';
+				case 'ongoing': return '立即参团';
+				case 'sold_out': return '已售罄';
+				case 'ended': return '已结束';
+				default: return '团购';
+			}
+		},
+		showProduct(item) {
+			uni.navigateTo({
+				url: '/pages_index/index/activityProductDetail?type=group&id=' + item.id
+			});
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+.container {
+	min-height: 100vh;
+	background-color: #f5f5f5;
+	padding: 20rpx;
+}
+
+.goods-list {
+	.goods-item {
+		display: flex;
+		background-color: #fff;
+		border-radius: 16rpx;
+		padding: 20rpx;
+		margin-bottom: 20rpx;
+		
+		.goods-img {
+			width: 240rpx;
+			height: 240rpx;
+			border-radius: 12rpx;
+			background-color: #f0f0f0;
+			flex-shrink: 0;
+		}
+		
+		.goods-info {
+			flex: 1;
+			margin-left: 20rpx;
+			display: flex;
+			flex-direction: column;
+			justify-content: space-between;
+			
+			.goods-name {
+				font-size: 28rpx;
+				color: #333;
+				font-weight: bold;
+				line-height: 40rpx;
+
+				.group-tag {
+					display: inline-block;
+					padding: 0 12rpx;
+					height: 34rpx;
+					line-height: 34rpx;
+					background: linear-gradient(90deg, #ff4a00, #ff6a00);
+					color: #fff;
+					font-size: 20rpx;
+					border-radius: 4rpx;
+					margin-right: 12rpx;
+					vertical-align: middle;
+					font-weight: normal;
+				}
+			}
+			
+			.ellipsis2 {
+				display: -webkit-box;
+				-webkit-box-orient: vertical;
+				-webkit-line-clamp: 2;
+				overflow: hidden;
+			}
+			
+			.countdown-box {
+				display: flex;
+				align-items: center;
+				margin-top: 10rpx;
+				
+				.time-text {
+					font-size: 24rpx;
+					color: #ff4a00;
+					background-color: rgba(255, 74, 0, 0.1);
+					padding: 2rpx 10rpx;
+					border-radius: 6rpx;
+					font-weight: bold;
+				}
+				
+				.status-text {
+					font-size: 22rpx;
+					color: #999;
+					margin-left: 10rpx;
+				}
+			}
+			
+			.goods-bottom {
+				display: flex;
+				justify-content: space-between;
+				align-items: flex-end;
+				margin-top: 10rpx;
+				
+				.price-info {
+					.group-buy-price {
+						color: #ff4a00;
+						font-size: 24rpx;
+						font-weight: bold;
+						
+						.num {
+							font-size: 36rpx;
+						}
+					}
+					
+					.original-price {
+						color: #999;
+						font-size: 22rpx;
+						text-decoration: line-through;
+						margin-top: 4rpx;
+					}
+				}
+				
+				.btn-box {
+					.grab-btn {
+						margin: 0;
+						padding: 0 24rpx;
+						height: 56rpx;
+						line-height: 56rpx;
+						border-radius: 28rpx;
+						font-size: 24rpx;
+						color: #fff;
+						background: linear-gradient(90deg, #4C49E9, #6a67ff);
+						border: none;
+						
+						&::after {
+							display: none;
+						}
+						
+						&.not-started {
+							background: #cccccc;
+						}
+						
+						&.sold-out {
+							background: #cccccc;
+						}
+						
+						&.ended {
+							background: #cccccc;
+						}
+					}
+				}
+			}
+		}
+	}
+}
+
+.empty-box {
+	padding: 100rpx 0;
+	text-align: center;
+	color: #999;
+	font-size: 28rpx;
+}
+</style>

+ 316 - 0
pages_user/user/coupon.vue

@@ -0,0 +1,316 @@
+<template>
+  <view ref="container">
+    <view class="tui-coupon-list">
+      <view class="tui-coupon-item tui-top20" v-for="(item, index) in couponsList" :key="index">
+         <image src="https://bjyjb-1362704775.cos.ap-chongqing.myqcloud.com/purpleShop/bg_coupon_3x.png" class="tui-coupon-bg" mode="widthFix"></image>
+		<view class="tui-coupon-item-left">
+          <view class="tui-coupon-price-box" :class="{ 'tui-color-grey': item.receiveCount>0 }">
+            <view class="tui-coupon-price-sign">¥</view>
+            <view class="tui-coupon-price" :class="{ 'tui-price-small': false }">{{ item.couponPrice }}</view>
+          </view>
+          <view class="tui-coupon-intro">满{{ item.useMinPrice }}元可用</view>
+        </view>
+        <view class="tui-coupon-item-right">
+          <view class="tui-coupon-content">
+            <view class="tui-coupon-title-box">
+              <view class="tui-coupon-title">{{ item.couponName }}</view>
+            </view>
+            <view class="tui-coupon-rule">
+              <view class="tui-rule-box tui-padding-btm">
+                <view class="tui-coupon-circle"></view>
+                <view class="tui-coupon-text">不可叠加使用</view>
+              </view>
+              <view class="tui-rule-box">
+                <view class="tui-coupon-circle"></view>
+                <view class="tui-coupon-text">{{ item.limitTime }} 到期</view>
+            
+              </view>
+            </view>
+          </view>
+        </view>
+        <view class="tui-btn-box">
+			<view class="btn receive"   @click="show(item)">查看</view>
+        </view>
+      </view>
+    </view>
+	<Loading :loaded="loadend" :loading="loading"></Loading>
+	<!--暂无优惠券-->
+	<view v-if="couponsList.length == 0 && page > 1" class="no-data-box" >
+		<image src="https://bjyjb-1362704775.cos.ap-chongqing.myqcloud.com/purpleShop/no_data.png" mode="aspectFit"></image>
+		<view class="empty-title">暂无数据</view>
+	</view>
+  </view>
+</template>
+<script>
+import { getCouponList, receive } from '@/api/user'
+import Loading from '@/components/Loading'
+export default {
+  name: 'getCoupon',
+  components: {
+    Loading,
+  },
+  props: {},
+  data: function() {
+    return {
+		page: 1,
+		limit: 10,
+		couponsList: [],
+		loading: false,
+		loadend: false,
+    }
+  },
+  onLoad(options) {
+  },
+  mounted: function() {
+    
+  },
+  
+  onShow() {
+  	this.getCouponList()
+  },
+  onReachBottom() {
+    !this.loading && this.getCouponList()
+  },
+  methods: {
+    show(item){
+		uni.navigateTo({
+			url: './couponDetails?id=' +item.id
+		})
+	},
+    getCouponList() {
+      if (this.loading) return //阻止下次请求(false可以进行请求);
+      if (this.loadend) return //阻止结束当前请求(false可以进行请求);
+      this.loading = true
+      let q = { couponType:1,page: this.page, pageSize: this.limit }
+      getCouponList(q).then(res => {
+        this.loading = false
+        this.couponsList.push.apply(this.couponsList, res.data.list)
+        this.loadend = res.data.list.length < this.limit //判断所有数据是否加载完成;
+        this.page = this.page + 1
+      })
+    },
+  },
+}
+</script>
+
+<style lang="less" scoped>
+page {
+  background-color: #f5f5f5;
+}
+
+.container {
+  padding-bottom: env(safe-area-inset-bottom);
+}
+.tui-coupon-list {
+	width: 100%;
+	padding: 0 25rpx;
+	box-sizing: border-box;
+}
+
+.tui-coupon-banner {
+  width: 100%;
+}
+
+.tui-coupon-item {
+  width: 100%;
+  height: 210rpx;
+  position: relative;
+  display: flex;
+  align-items: center;
+  padding-right: 30rpx;
+  box-sizing: border-box;
+  overflow: hidden;
+ 
+}
+
+.tui-coupon-bg {
+  width: 100%;
+  height: 210rpx;
+  position: absolute;
+  left: 0;
+  top: 0;
+  z-index: 1;
+}
+
+.tui-coupon-sign {
+  height: 110rpx;
+  width: 110rpx;
+  position: absolute;
+  z-index: 9;
+  top: -30rpx;
+  right: 40rpx;
+}
+
+.tui-coupon-item-left {
+  width: 218rpx;
+  height: 210rpx;
+  position: relative;
+  z-index: 2;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+  flex-shrink: 0;
+}
+
+.tui-coupon-price-box {
+  display: flex;
+  color: #e41f19;
+  align-items: flex-end;
+}
+
+.tui-coupon-price-sign {
+  font-size: 30rpx;
+}
+
+.tui-coupon-price {
+  font-size: 70rpx;
+  line-height: 68rpx;
+  font-weight: bold;
+}
+
+.tui-price-small {
+  font-size: 58rpx !important;
+  line-height: 56rpx !important;
+}
+
+.tui-coupon-intro {
+  background: #f7f7f7;
+  padding: 8rpx 10rpx;
+  font-size: 26rpx;
+  line-height: 26rpx;
+  font-weight: 400;
+  color: #666;
+  margin-top: 18rpx;
+}
+
+.tui-coupon-item-right {
+  flex: 1;
+  height: 210rpx;
+  position: relative;
+  z-index: 2;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding-left: 24rpx;
+  box-sizing: border-box;
+  overflow: hidden;
+}
+
+.tui-coupon-content {
+  width: 82%;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+}
+
+.tui-coupon-title-box {
+  display: flex;
+  align-items: center;
+}
+
+.tui-coupon-btn {
+  padding: 6rpx;
+  background: #ffebeb;
+  color: #e41f19;
+  font-size: 25rpx;
+  line-height: 25rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transform: scale(0.9);
+  transform-origin: 0 center;
+  border-radius: 4rpx;
+  flex-shrink: 0;
+}
+
+.tui-color-grey {
+  color: #888 !important;
+}
+
+.tui-bg-grey {
+  background: #f0f0f0 !important;
+  color: #888 !important;
+}
+
+.tui-coupon-title {
+  width: 100%;
+  font-size: 26rpx;
+  color: #333;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.tui-coupon-rule {
+  padding-top: 52rpx;
+}
+
+.tui-rule-box {
+  display: flex;
+  align-items: center;
+  transform: scale(0.8);
+  transform-origin: 0 100%;
+}
+
+.tui-padding-btm {
+  padding-bottom: 6rpx;
+}
+
+.tui-coupon-circle {
+  width: 8rpx;
+  height: 8rpx;
+  background: rgb(160, 160, 160);
+  border-radius: 50%;
+}
+
+.tui-coupon-text {
+  font-size: 28rpx;
+  line-height: 28rpx;
+  font-weight: 400;
+  color: #666;
+  padding-left: 8rpx;
+  white-space: nowrap;
+}
+
+.tui-top20 {
+  margin-top: 20rpx;
+}
+
+.tui-coupon-title {
+  font-size: 28rpx;
+  line-height: 28rpx;
+}
+
+.tui-coupon-radio {
+  transform: scale(0.7);
+  transform-origin: 100% center;
+}
+
+.tui-btn-box {
+  position: absolute;
+  right: 20rpx;
+  bottom: 40rpx;
+  z-index: 10;
+  .btn{
+  	width: 155upx;
+  	height: 64upx;
+  	line-height: 64upx;
+  	font-size: 26upx;
+  	font-family: PingFang SC;
+  	font-weight: 500;
+  	text-align: center;
+  	border-radius: 32upx;
+  	margin-left: 15upx;
+
+  	&.cancel{
+  		border: 1px solid red;
+  		color: red;
+  	}
+  	&.receive{
+  		background: red;
+  		color: #FFFFFF;
+  	}
+  }
+}
+</style>

BIN
团购对接接口文档.docx