yjwang 3 päivää sitten
vanhempi
commit
4a8aad4acf

+ 34 - 0
src/api/hisStore/productGroupBuy.js

@@ -0,0 +1,34 @@
+import request from '@/utils/request'
+
+// 团购列表(分页)
+export function listProductGroupBuy(query) {
+  return request({
+    url: '/store/store/productGroupBuy/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 团购详情(含团员+订单状态)
+export function getProductGroupBuy(id) {
+  return request({
+    url: '/store/store/productGroupBuy/' + id,
+    method: 'get'
+  })
+}
+
+// 按商品ID查历史拼团
+export function listGroupBuyByProduct(productId) {
+  return request({
+    url: '/store/store/productGroupBuy/listByProduct/' + productId,
+    method: 'get'
+  })
+}
+
+// 团员列表(可单独查)
+export function listGroupBuyMembers(id) {
+  return request({
+    url: '/store/store/productGroupBuy/members/' + id,
+    method: 'get'
+  })
+}

+ 364 - 0
src/views/hisStore/productGroupBuy/index.vue

@@ -0,0 +1,364 @@
+<template>
+  <div class="app-container">
+    <!-- 查询区 -->
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="90px">
+      <el-form-item label="团购编号" prop="groupNo">
+        <el-input v-model="queryParams.groupNo" placeholder="请输入团购编号" clearable size="small" @keyup.enter.native="handleQuery" />
+      </el-form-item>
+      <el-form-item label="商品名称" prop="productName">
+        <el-input v-model="queryParams.productName" placeholder="请输入商品名称" clearable size="small" @keyup.enter.native="handleQuery" />
+      </el-form-item>
+      <el-form-item label="商品ID" prop="productId">
+        <el-input v-model="queryParams.productId" placeholder="请输入商品ID" clearable size="small" @keyup.enter.native="handleQuery" />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择" clearable size="small" style="width: 140px">
+          <el-option label="进行中" :value="0" />
+          <el-option label="拼团成功" :value="1" />
+          <el-option label="拼团失败" :value="2" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="开始时间" prop="beginTime">
+        <el-date-picker v-model="queryParams.beginTime" type="datetime" placeholder="开始时间" value-format="yyyy-MM-dd HH:mm:ss" size="small" style="width: 200px" />
+      </el-form-item>
+      <el-form-item label="结束时间" prop="endTime">
+        <el-date-picker v-model="queryParams.endTime" type="datetime" placeholder="结束时间" value-format="yyyy-MM-dd HH:mm:ss" size="small" style="width: 200px" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
+    </el-row>
+
+    <!-- 列表 -->
+    <el-table v-loading="loading" :data="list" border>
+      <el-table-column label="团购编号" align="center" prop="groupNo" width="160" />
+      <el-table-column label="商品信息" align="center" min-width="260">
+        <template slot-scope="scope">
+          <div style="display:flex;align-items:center;">
+            <el-image
+              v-if="scope.row.productImage"
+              style="width:50px;height:50px;margin-right:10px;border-radius:4px;"
+              :src="scope.row.productImage"
+              fit="cover"
+              :preview-src-list="[scope.row.productImage]"
+            />
+            <div style="text-align:left;">
+              <div>{{ scope.row.productName || '-' }}</div>
+              <div style="color:#909399;font-size:12px;">ID: {{ scope.row.productId }}</div>
+            </div>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column label="原价" align="center" width="100">
+        <template slot-scope="scope">
+          <span v-if="scope.row.originalPrice != null">¥{{ Number(scope.row.originalPrice).toFixed(2) }}</span>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="团购价" align="center" width="110">
+        <template slot-scope="scope">
+          <span style="color:#f56c6c;font-weight:bold;">¥{{ scope.row.groupPrice ? Number(scope.row.groupPrice).toFixed(2) : '0.00' }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="成团人数" align="center" prop="groupNum" width="90" />
+      <el-table-column label="进度(已付/参团/满团)" align="center" width="170">
+        <template slot-scope="scope">
+          <el-tag size="mini" type="success">已付 {{ scope.row.paidNum || 0 }}</el-tag>
+          <el-tag size="mini" type="warning" style="margin-left:4px;">参团 {{ scope.row.joinNum || 0 }}</el-tag>
+          <el-tag size="mini" style="margin-left:4px;">满 {{ scope.row.groupNum || 0 }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="开始时间" align="center" prop="startTime" width="160" />
+      <el-table-column label="结束时间" align="center" prop="endTime" width="160" />
+      <el-table-column label="状态" align="center" width="90">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.status === 0" type="warning">进行中</el-tag>
+          <el-tag v-else-if="scope.row.status === 1" type="success">拼团成功</el-tag>
+          <el-tag v-else-if="scope.row.status === 2" type="danger">拼团失败</el-tag>
+          <el-tag v-else type="info">未知</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-view" @click="handleDetail(scope.row)" v-hasPermi="['store:productGroupBuy:query']">查看详情</el-button>
+          <el-button size="mini" type="text" icon="el-icon-s-order" @click="handleHistory(scope.row)" v-hasPermi="['store:productGroupBuy:list']">历史拼团</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+
+    <!-- 详情抽屉:团购信息 + 商品信息 + 团员表格(含订单状态) -->
+    <el-drawer title="团购详情" :visible.sync="detailOpen" size="75%" destroy-on-close>
+      <div v-loading="detailLoading" style="padding: 0 20px;">
+        <template v-if="detail">
+          <el-descriptions title="团购信息" :column="3" border size="small">
+            <el-descriptions-item label="团购编号">{{ detail.groupNo }}</el-descriptions-item>
+            <el-descriptions-item label="状态">
+              <el-tag v-if="detail.status === 0" type="warning">进行中</el-tag>
+              <el-tag v-else-if="detail.status === 1" type="success">拼团成功</el-tag>
+              <el-tag v-else-if="detail.status === 2" type="danger">拼团失败</el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item label="成团人数">{{ detail.groupNum }}</el-descriptions-item>
+            <el-descriptions-item label="已参团">{{ detail.joinNum }}</el-descriptions-item>
+            <el-descriptions-item label="已付款">{{ detail.paidNum }}</el-descriptions-item>
+            <el-descriptions-item label="团购价">
+              <span style="color:#f56c6c;font-weight:bold;">¥{{ detail.groupPrice ? Number(detail.groupPrice).toFixed(2) : '0.00' }}</span>
+            </el-descriptions-item>
+            <el-descriptions-item label="原价">¥{{ detail.originalPrice ? Number(detail.originalPrice).toFixed(2) : '-' }}</el-descriptions-item>
+            <el-descriptions-item label="开始时间">{{ detail.startTime }}</el-descriptions-item>
+            <el-descriptions-item label="结束时间">{{ detail.endTime }}</el-descriptions-item>
+          </el-descriptions>
+
+          <el-descriptions title="商品信息" :column="2" border size="small" style="margin-top:16px;">
+            <el-descriptions-item label="商品">
+              <div style="display:flex;align-items:center;">
+                <el-image
+                  v-if="detail.productImage"
+                  style="width:50px;height:50px;margin-right:10px;border-radius:4px;"
+                  :src="detail.productImage"
+                  fit="cover"
+                  :preview-src-list="[detail.productImage]"
+                />
+                <span>{{ detail.productName || '-' }}</span>
+              </div>
+            </el-descriptions-item>
+            <el-descriptions-item label="商品ID">{{ detail.productId }}</el-descriptions-item>
+          </el-descriptions>
+
+          <div style="margin-top: 16px; display:flex; justify-content: space-between; align-items:center;">
+            <h3 style="margin:0;">参团用户(订单状态)</h3>
+            <span style="color:#909399;font-size:12px;">共 {{ detail.members ? detail.members.length : 0 }} 人</span>
+          </div>
+          <el-table :data="detail.members || []" border size="small" style="margin-top:8px;">
+            <el-table-column label="用户" align="center" min-width="180">
+              <template slot-scope="scope">
+                <div style="display:flex;align-items:center;">
+                  <el-avatar v-if="scope.row.avatar" :src="scope.row.avatar" size="small" style="margin-right:8px;" />
+                  <div style="text-align:left;">
+                    <div>{{ scope.row.nickName || '-' }}</div>
+                    <div style="color:#909399;font-size:12px;">ID: {{ scope.row.userId }}</div>
+                  </div>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column label="规格" align="center" prop="specName" min-width="140">
+              <template slot-scope="scope">
+                <span>{{ scope.row.specName || '-' }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="参团时间" align="center" prop="joinTime" width="160" />
+            <el-table-column label="订单号" align="center" prop="orderCode" min-width="180">
+              <template slot-scope="scope">
+                <el-link v-if="scope.row.orderCode" type="primary" @click="copyOrderCode(scope.row.orderCode)">{{ scope.row.orderCode }}</el-link>
+                <span v-else style="color:#c0c4cc;">-</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="付款状态" align="center" width="90">
+              <template slot-scope="scope">
+                <el-tag v-if="scope.row.payStatus === 1" type="success" size="mini">已付</el-tag>
+                <el-tag v-else type="info" size="mini">未付</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="订单状态" align="center" width="110">
+              <template slot-scope="scope">
+                <span>{{ formatOrderStatus(scope.row.orderStatus) }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="退款状态" align="center" width="100">
+              <template slot-scope="scope">
+                <span>{{ formatRefundStatus(scope.row.refundStatus) }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="支付金额" align="center" width="100">
+              <template slot-scope="scope">
+                <span v-if="scope.row.payMoney != null">¥{{ Number(scope.row.payMoney).toFixed(2) }}</span>
+                <span v-else>-</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="退款金额" align="center" width="100">
+              <template slot-scope="scope">
+                <span v-if="scope.row.refundPrice != null && Number(scope.row.refundPrice) > 0" style="color:#f56c6c;">¥{{ Number(scope.row.refundPrice).toFixed(2) }}</span>
+                <span v-else>-</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="付款时间" align="center" prop="payTime" width="160" />
+            <el-table-column label="操作" align="center" width="110">
+              <template slot-scope="scope">
+                <el-button size="mini" type="text" :disabled="!scope.row.orderCode" @click="goOrderDetail(scope.row)">查看订单</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </template>
+      </div>
+    </el-drawer>
+
+    <!-- 历史拼团抽屉:按商品ID查所有团购记录 -->
+    <el-drawer :title="`历史拼团 - ${historyProductName || ''}`" :visible.sync="historyOpen" size="70%" destroy-on-close>
+      <div v-loading="historyLoading" style="padding: 0 20px;">
+        <el-table :data="historyList" border size="small">
+          <el-table-column label="团购编号" align="center" prop="groupNo" width="160" />
+          <el-table-column label="团购价" align="center" width="100">
+            <template slot-scope="scope">
+              <span style="color:#f56c6c;">¥{{ scope.row.groupPrice ? Number(scope.row.groupPrice).toFixed(2) : '0.00' }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="成团人数" align="center" prop="groupNum" width="90" />
+          <el-table-column label="已付/参团" align="center" width="130">
+            <template slot-scope="scope">
+              <span>{{ scope.row.paidNum || 0 }} / {{ scope.row.joinNum || 0 }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="开始时间" align="center" prop="startTime" width="160" />
+          <el-table-column label="结束时间" align="center" prop="endTime" width="160" />
+          <el-table-column label="状态" align="center" width="100">
+            <template slot-scope="scope">
+              <el-tag v-if="scope.row.status === 0" type="warning" size="mini">进行中</el-tag>
+              <el-tag v-else-if="scope.row.status === 1" type="success" size="mini">拼团成功</el-tag>
+              <el-tag v-else-if="scope.row.status === 2" type="danger" size="mini">拼团失败</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" align="center" width="100">
+            <template slot-scope="scope">
+              <el-button size="mini" type="text" @click="handleDetail(scope.row)">查看详情</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </el-drawer>
+  </div>
+</template>
+
+<script>
+import {
+  listProductGroupBuy,
+  getProductGroupBuy,
+  listGroupBuyByProduct
+} from '@/api/hisStore/productGroupBuy'
+
+export default {
+  name: 'ProductGroupBuy',
+  data() {
+    return {
+      loading: false,
+      showSearch: true,
+      list: [],
+      total: 0,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        groupNo: null,
+        productId: null,
+        productName: null,
+        status: null,
+        beginTime: null,
+        endTime: null
+      },
+      // 详情抽屉
+      detailOpen: false,
+      detailLoading: false,
+      detail: null,
+      // 历史拼团抽屉
+      historyOpen: false,
+      historyLoading: false,
+      historyList: [],
+      historyProductName: ''
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      listProductGroupBuy(this.queryParams).then(response => {
+        this.list = response.rows || []
+        this.total = response.total || 0
+        this.loading = false
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    handleDetail(row) {
+      this.detailOpen = true
+      this.detailLoading = true
+      this.detail = null
+      getProductGroupBuy(row.id).then(response => {
+        this.detail = response.data || null
+        this.detailLoading = false
+      }).catch(() => {
+        this.detailLoading = false
+      })
+    },
+    handleHistory(row) {
+      this.historyOpen = true
+      this.historyLoading = true
+      this.historyProductName = row.productName || ''
+      this.historyList = []
+      listGroupBuyByProduct(row.productId).then(response => {
+        this.historyList = response.rows || []
+        this.historyLoading = false
+      }).catch(() => {
+        this.historyLoading = false
+      })
+    },
+    copyOrderCode(code) {
+      const input = document.createElement('input')
+      input.value = code
+      document.body.appendChild(input)
+      input.select()
+      try {
+        document.execCommand('copy')
+        this.msgSuccess('订单号已复制:' + code)
+      } catch (e) {
+        this.$message.warning('复制失败,请手动复制')
+      }
+      document.body.removeChild(input)
+    },
+    goOrderDetail(row) {
+      if (!row.orderCode) return
+      // 跳转到订单列表并自动按订单号筛选
+      const routeData = this.$router.resolve({
+        path: '/his/storeOrder',
+        query: { orderCode: row.orderCode }
+      })
+      window.open(routeData.href, '_blank')
+    },
+    formatOrderStatus(status) {
+      // fs_store_order_scrm.status(OrderInfoEnum):
+      // -1 申请退款 / -2 退款成功 / -3 已取消 / 0 待支付 / 1 待发货 / 2 待收货 / 3 已完成 / 4 待评价
+      const map = {
+        0: '待支付',
+        1: '待发货',
+        2: '待收货',
+        3: '已完成',
+        4: '待评价',
+        '-1': '申请退款',
+        '-2': '退款成功',
+        '-3': '已取消'
+      }
+      if (status === null || status === undefined) return '-'
+      return map[status] !== undefined ? map[status] : ('状态' + status)
+    },
+    formatRefundStatus(status) {
+      // 0 未退款 1 申请中 2 已退款
+      const map = { 0: '未退款', 1: '退款中', 2: '已退款' }
+      if (status === null || status === undefined) return '-'
+      return map[status] !== undefined ? map[status] : ('状态' + status)
+    }
+  }
+}
+</script>

+ 182 - 32
src/views/hisStore/storeProduct/index.vue

@@ -331,9 +331,17 @@
       <el-table-column label="库存" align="center" prop="stock" />
       <el-table-column label="库存" align="center" prop="stock" />
       <el-table-column label="活动类型" align="center" prop="activityType" width="100">
       <el-table-column label="活动类型" align="center" prop="activityType" width="100">
         <template slot-scope="scope">
         <template slot-scope="scope">
-          <el-tag v-if="scope.row.activityType === 6" type="danger" size="small">秒杀</el-tag>
-          <el-tag v-else-if="scope.row.activityType === 7" type="warning" size="small">限时折扣</el-tag>
-          <el-tag v-else type="info" size="small">无</el-tag>
+          <!-- 活动已过期(end_time < now)就当“无活动”展示,
+               避免列表里长期显示一个实际已结束的秒杀/团购 tag 误导运营 -->
+          <template v-if="isActivityExpired(scope.row)">
+            <el-tag type="info" size="small">无</el-tag>
+          </template>
+          <template v-else>
+            <el-tag v-if="scope.row.activityType === 6" type="danger" size="small">秒杀</el-tag>
+            <el-tag v-else-if="scope.row.activityType === 7" type="warning" size="small">限时折扣</el-tag>
+            <el-tag v-else-if="scope.row.activityType === 8" type="success" size="small">限时团购</el-tag>
+            <el-tag v-else type="info" size="small">无</el-tag>
+          </template>
         </template>
         </template>
       </el-table-column>
       </el-table-column>
       <el-table-column label="类型" align="center" prop="productType" >
       <el-table-column label="类型" align="center" prop="productType" >
@@ -364,6 +372,7 @@
             icon="el-icon-edit"
             icon="el-icon-edit"
             @click="handleUpdate(scope.row)"
             @click="handleUpdate(scope.row)"
             v-hasPermi="['store:storeProduct:edit']"
             v-hasPermi="['store:storeProduct:edit']"
+            v-if="!isProductInActivity(scope.row)"
           >修改</el-button>
           >修改</el-button>
           <el-button
           <el-button
             size="mini"
             size="mini"
@@ -379,6 +388,7 @@
             icon="el-icon-delete"
             icon="el-icon-delete"
             @click="handleDelete(scope.row)"
             @click="handleDelete(scope.row)"
             v-hasPermi="['store:storeProduct:remove']"
             v-hasPermi="['store:storeProduct:remove']"
+            v-if="!isProductInActivity(scope.row)"
           >删除</el-button>
           >删除</el-button>
         </template>
         </template>
       </el-table-column>
       </el-table-column>
@@ -980,7 +990,7 @@
       <!-- 活动进行中提示 -->
       <!-- 活动进行中提示 -->
       <el-alert
       <el-alert
         v-if="activityIsOngoing"
         v-if="activityIsOngoing"
-        :title="'当前商品正在' + (activityOngoingType === 6 ? '秒杀' : '限时折扣') + '活动中,活动进行期间无法修改活动设置'"
+        :title="'当前商品正在' + (activityOngoingType === 6 ? '秒杀' : (activityOngoingType === 8 ? '限时团购' : '限时折扣')) + '活动中,活动进行期间无法修改活动设置'"
         type="error"
         type="error"
         :closable="false"
         :closable="false"
         show-icon
         show-icon
@@ -998,11 +1008,14 @@
             <el-radio :label="7">
             <el-radio :label="7">
               <i class="el-icon-time" style="color:#E6A23C;margin-right:2px;"></i>限时折扣
               <i class="el-icon-time" style="color:#E6A23C;margin-right:2px;"></i>限时折扣
             </el-radio>
             </el-radio>
+            <el-radio :label="8">
+              <i class="el-icon-user-solid" style="color:#67C23A;margin-right:2px;"></i>限时团购
+            </el-radio>
           </el-radio-group>
           </el-radio-group>
         </el-form-item>
         </el-form-item>
 
 
         <!-- 统一活动时间(所有规格共用) -->
         <!-- 统一活动时间(所有规格共用) -->
-        <template v-if="activityForm.activityType === 6 || activityForm.activityType === 7">
+        <template v-if="activityForm.activityType === 6 || activityForm.activityType === 7 || activityForm.activityType === 8">
           <el-form-item label="活动时间" prop="startTime">
           <el-form-item label="活动时间" prop="startTime">
             <el-date-picker v-model="activityForm.startTime" type="datetime" placeholder="开始时间" value-format="yyyy-MM-dd HH:mm:ss" style="width:200px" size="small"/>
             <el-date-picker v-model="activityForm.startTime" type="datetime" placeholder="开始时间" value-format="yyyy-MM-dd HH:mm:ss" style="width:200px" size="small"/>
             <span style="margin:0 8px;color:#909399;">至</span>
             <span style="margin:0 8px;color:#909399;">至</span>
@@ -1010,7 +1023,15 @@
           </el-form-item>
           </el-form-item>
         </template>
         </template>
 
 
-        <template v-if="activityForm.activityType === 6 || activityForm.activityType === 7">
+        <!-- 团购人数(仅限时团购):2~5 人,默认 5;参团并付款的人数达到该值时自动成团并触发发货 -->
+        <template v-if="activityForm.activityType === 8">
+          <el-form-item label="团购人数" prop="groupNum">
+            <el-input-number v-model="activityForm.groupNum" :min="2" :max="5" :step="1" :precision="0" size="small" controls-position="right" style="width:200px"/>
+            <span style="margin-left:8px;color:#909399;font-size:12px;">(限 2~5 人,默认 5 人;达到该人数并全部付款后自动成团发货)</span>
+          </el-form-item>
+        </template>
+
+        <template v-if="activityForm.activityType === 6 || activityForm.activityType === 7 || activityForm.activityType === 8">
           <!-- 选择参与规格 -->
           <!-- 选择参与规格 -->
           <el-divider content-position="left">
           <el-divider content-position="left">
             <span style="font-size:14px;font-weight:500;">选择参与规格</span>
             <span style="font-size:14px;font-weight:500;">选择参与规格</span>
@@ -1019,7 +1040,7 @@
             <div
             <div
               v-for="(spec, idx) in activityAllSpecs"
               v-for="(spec, idx) in activityAllSpecs"
               :key="idx"
               :key="idx"
-              :class="['spec-select-item', spec.selected ? 'spec-select-active' : '']"
+              :class="['spec-select-item', spec.selected ? 'spec-select-active' : '', activityForm.activityType === 8 ? 'spec-select-disabled' : '']"
               @click="toggleSpecSelect(spec)"
               @click="toggleSpecSelect(spec)"
             >
             >
               <el-popover placement="right" trigger="hover" v-if="spec.image">
               <el-popover placement="right" trigger="hover" v-if="spec.image">
@@ -1049,7 +1070,7 @@
                       <img :src="spec.image" style="max-width:200px;max-height:200px;" />
                       <img :src="spec.image" style="max-width:200px;max-height:200px;" />
                     </el-popover>
                     </el-popover>
                     <span class="spec-name">{{ spec.specName }}</span>
                     <span class="spec-name">{{ spec.specName }}</span>
-                    <i class="el-icon-remove-outline" style="color:#F56C6C;font-size:16px;cursor:pointer;margin-left:8px;" title="移除" @click="toggleSpecSelect(spec)"></i>
+                    <i v-if="activityForm.activityType !== 8" class="el-icon-remove-outline" style="color:#F56C6C;font-size:16px;cursor:pointer;margin-left:8px;" title="移除" @click="toggleSpecSelect(spec)"></i>
                   </div>
                   </div>
                 </div>
                 </div>
                 <div class="spec-detail-row">
                 <div class="spec-detail-row">
@@ -1081,6 +1102,13 @@
                       <el-input-number v-model="spec.discountPrice" :precision="2" :min="0" size="small" placeholder="折扣价" style="width:100%" controls-position="right" @change="handleSpecDiscountPriceChange(spec)"/>
                       <el-input-number v-model="spec.discountPrice" :precision="2" :min="0" size="small" placeholder="折扣价" style="width:100%" controls-position="right" @change="handleSpecDiscountPriceChange(spec)"/>
                     </div>
                     </div>
                   </el-col>
                   </el-col>
+                  <!-- 团购价(activityType=8 时必填) -->
+                  <el-col :span="12" v-if="activityForm.activityType === 8">
+                    <div class="spec-field">
+                      <label>团购价 <span style="color:#F56C6C;">*</span></label>
+                      <el-input-number v-model="spec.groupPrice" :precision="2" :min="0" :max="spec.originalPrice" size="small" placeholder="团购价" style="width:100%" controls-position="right"/>
+                    </div>
+                  </el-col>
                 </el-row>
                 </el-row>
               </div>
               </div>
             </div>
             </div>
@@ -1145,6 +1173,12 @@ export default {
     },
     },
     drugImageArr: function(val) {
     drugImageArr: function(val) {
       this.form.drugImage = val.join(',');
       this.form.drugImage = val.join(',');
+    },
+    'activityForm.activityType': function(val) {
+      // 限时团购:默认全选所有规格,不允许手动取消
+      if (val === 8 && this.activityAllSpecs && this.activityAllSpecs.length > 0) {
+        this.activityAllSpecs.forEach(s => { s.selected = true; });
+      }
     }
     }
   },
   },
   computed: {
   computed: {
@@ -1241,6 +1275,8 @@ export default {
         activityType: 0,
         activityType: 0,
         startTime: null,
         startTime: null,
         endTime: null,
         endTime: null,
+        // 团购人数默认 5 人(系统允许范围 2~5)
+        groupNum: 5,
         remark: null
         remark: null
       },
       },
       activitySpecList: [],
       activitySpecList: [],
@@ -1981,6 +2017,26 @@ export default {
         this.msgSuccess("复制成功");
         this.msgSuccess("复制成功");
       }).catch(function() {});
       }).catch(function() {});
     },
     },
+    /** 判断商品活动是否“已过期”:activity_type 有值 且 end_time 在当前时间之前
+     *  给列表的“活动类型”列用——过期了就跳过 tag 展示,统一显示“无”。
+     *  注:存在 activityType 但 end_time 为空时算“未过期”(不误杀新活动的展示) */
+    isActivityExpired(row) {
+      if (!row || !row.activityType || row.activityType === 0) return false;
+      if (!row.activityEndTime) return false;
+      return new Date(row.activityEndTime).getTime() < Date.now();
+    },
+    /** 判断商品是否“活动进行中”:activity_type 存在 且 now 在 [start, end] 之间
+     *  活动中不允许修改/删除商品,重点防护后台 saveActivity 逻辑已锁定的商品,避免商品信息跟活动参数不一致 */
+    isProductInActivity(row) {
+      if (!row || !row.activityType || row.activityType === 0) return false;
+      // 后台返回的是 ISO 时间字符串 或 yyyy-MM-dd HH:mm:ss,new Date 都能解析
+      const start = row.activityStartTime ? new Date(row.activityStartTime).getTime() : null;
+      const end = row.activityEndTime ? new Date(row.activityEndTime).getTime() : null;
+      const now = Date.now();
+      // 两边都有才能认为「进行中」,缺一个就认为数据异常,不锁按钮(避免误伤)
+      if (start == null || end == null) return false;
+      return now >= start && now <= end;
+    },
     /** 设置活动按钮操作 */
     /** 设置活动按钮操作 */
     handleSetActivity(row) {
     handleSetActivity(row) {
       // // 校验审核状态:只有审核通过的商品才能设置活动
       // // 校验审核状态:只有审核通过的商品才能设置活动
@@ -1995,6 +2051,8 @@ export default {
         activityType: 0,
         activityType: 0,
         startTime: null,
         startTime: null,
         endTime: null,
         endTime: null,
+        // 团购人数默认 5 人(系统允许范围 2~5)
+        groupNum: 5,
         remark: null
         remark: null
       };
       };
       this.activitySpecList = [];
       this.activitySpecList = [];
@@ -2013,6 +2071,9 @@ export default {
           this.activityForm.remark = first.remark;
           this.activityForm.remark = first.remark;
           this.activityForm.startTime = first.startTime || null;
           this.activityForm.startTime = first.startTime || null;
           this.activityForm.endTime = first.endTime || null;
           this.activityForm.endTime = first.endTime || null;
+          if (first.groupNum != null) {
+            this.activityForm.groupNum = first.groupNum;
+          }
         } else {
         } else {
           // 从商品表获取activityType
           // 从商品表获取activityType
           this.activityForm.activityType = row.activityType || 0;
           this.activityForm.activityType = row.activityType || 0;
@@ -2054,12 +2115,17 @@ export default {
             discountValue: 10,
             discountValue: 10,
             discount: null,
             discount: null,
             discountPrice: null,
             discountPrice: null,
+            groupPrice: null,
             stock: null,
             stock: null,
             selected: false
             selected: false
           }));
           }));
 
 
           // 回显已有活动数据
           // 回显已有活动数据
           this.mergeExistActivity(specList, existActivityList);
           this.mergeExistActivity(specList, existActivityList);
+          // 限时团购:强制全选
+          if (this.activityForm.activityType === 8) {
+            specList.forEach(s => { s.selected = true; });
+          }
           this.activityAllSpecs = specList;
           this.activityAllSpecs = specList;
         })
         })
       });
       });
@@ -2067,36 +2133,40 @@ export default {
 
 
     /** 切换规格选中状态 */
     /** 切换规格选中状态 */
     toggleSpecSelect(spec) {
     toggleSpecSelect(spec) {
+      // 限时团购:默认全选且不允许更改
+      if (this.activityForm.activityType === 8) return;
       spec.selected = !spec.selected;
       spec.selected = !spec.selected;
     },
     },
 
 
-    /** 合并已有活动数据到规格列表 */
+    /** 合并已有活动数据到规格列表(含团购价回显)
+     *  注意:specId 后端是 Long,若 Jackson 配置了 Long→String 防 JS 精度丢失,
+     *  这里直接 === 会判不上,导致整行回显失败(团购价/秒杀价/折扣 都回显不出来)。
+     *  统一用 String() 比较兜底。 */
     mergeExistActivity(specList, existActivityList) {
     mergeExistActivity(specList, existActivityList) {
-      if (existActivityList && existActivityList.length > 0) {
-        specList.forEach(spec => {
-          const matched = existActivityList.find(a => {
-            if (spec.specId !== null) {
-              return a.specId === spec.specId;
-            } else {
-              return true;
-            }
-          });
-          if (matched) {
-            spec.selected = true;
-            spec.flashPrice = matched.flashPrice;
-            spec.discount = matched.discount;
-            spec.discountPrice = matched.discountPrice;
-            spec.stock = matched.stock;
-            spec.originalPrice = matched.originalPrice || spec.originalPrice;
-            spec.startTime = matched.startTime;
-            spec.endTime = matched.endTime;
-            // 折扣值转换:discount存的是0.8格式,滑块需要8格式
-            if (matched.discount) {
-              spec.discountValue = Number((matched.discount * 10).toFixed(1));
-            }
+      if (!existActivityList || existActivityList.length === 0) return;
+      specList.forEach(spec => {
+        const matched = existActivityList.find(a => {
+          if (spec.specId !== null && spec.specId !== undefined) {
+            return String(a.specId) === String(spec.specId);
           }
           }
+          return true;
         });
         });
-      }
+        if (!matched) return;
+        spec.selected = true;
+        spec.flashPrice = matched.flashPrice;
+        spec.discount = matched.discount;
+        spec.discountPrice = matched.discountPrice;
+        // 团购价:BigDecimal 序列化后可能是数字也可能是字符串,统一转 Number 给 el-input-number
+        spec.groupPrice = matched.groupPrice != null ? Number(matched.groupPrice) : null;
+        spec.stock = matched.stock;
+        spec.originalPrice = matched.originalPrice || spec.originalPrice;
+        spec.startTime = matched.startTime;
+        spec.endTime = matched.endTime;
+        // 折扣值转换:discount存的是0.8格式,滑块需要8格式
+        if (matched.discount) {
+          spec.discountValue = Number((matched.discount * 10).toFixed(1));
+        }
+      });
     },
     },
 
 
     /** 折扣滑块变化时计算折扣价 */
     /** 折扣滑块变化时计算折扣价 */
@@ -2154,6 +2224,78 @@ export default {
           this.$message.warning('请选择活动结束时间');
           this.$message.warning('请选择活动结束时间');
           return;
           return;
         }
         }
+        // 时间区间必须大于当前时间,开始时间必须小于结束时间
+        const _nowTs = Date.now();
+        const _startTs = new Date(this.activityForm.startTime.replace(/-/g, '/')).getTime();
+        const _endTs = new Date(this.activityForm.endTime.replace(/-/g, '/')).getTime();
+        if (isNaN(_startTs) || isNaN(_endTs)) {
+          this.$message.warning('活动时间格式有误');
+          return;
+        }
+        if (_startTs >= _endTs) {
+          this.$message.warning('活动开始时间必须早于结束时间');
+          return;
+        }
+        if (_endTs <= _nowTs) {
+          this.$message.warning('活动结束时间必须大于当前时间');
+          return;
+        }
+
+        // 限时团购:独立分支(不需秒杀价/折扣,全部规格参与,带 groupNum)
+        // 团购人数限制:2 ≤ groupNum ≤ 5,默认 5;达到该人数并全部付款后自动成团发货
+        if (activityType === 8) {
+          const gn = Number(this.activityForm.groupNum);
+          if (!gn || gn < 2 || gn > 5) {
+            this.$message.warning('请输入有效的团购人数(限 2 ~ 5 人)');
+            return;
+          }
+          // 团购价每个规格都必填
+          let groupPriceError = false;
+          for (const s of this.activityAllSpecs) {
+            if (s.groupPrice === null || s.groupPrice === undefined || s.groupPrice === '' || isNaN(Number(s.groupPrice)) || Number(s.groupPrice) <= 0) {
+              this.$message.warning('请填写规格【' + s.specName + '】的团购价(需大于0)');
+              groupPriceError = true;
+              break;
+            }
+            if (s.originalPrice && Number(s.groupPrice) >= Number(s.originalPrice)) {
+              this.$message.warning('规格【' + s.specName + '】的团购价应低于原价 ' + s.originalPrice);
+              groupPriceError = true;
+              break;
+            }
+          }
+          if (groupPriceError) return;
+          const groupList = this.activityAllSpecs.map(spec => ({
+            specId: spec.specId,
+            originalPrice: spec.originalPrice,
+            groupPrice: spec.groupPrice,
+            stock: spec.stock,
+            startTime: this.activityForm.startTime,
+            endTime: this.activityForm.endTime,
+            groupNum: this.activityForm.groupNum
+          }));
+          if (groupList.length === 0) {
+            this.$message.warning('当前商品没有可用规格');
+            return;
+          }
+          this.activitySubmitLoading = true;
+          saveActivity({
+            productId: this.activityCurrentProduct.productId,
+            activityType: 8,
+            groupNum: this.activityForm.groupNum,
+            activityList: groupList
+          }).then(res => {
+            if (res.code === 200) {
+              this.$message.success('活动设置保存成功');
+              this.activityDialogVisible = false;
+              this.getList();
+            } else {
+              this.$message.error(res.msg || '保存失败');
+            }
+          }).finally(() => {
+            this.activitySubmitLoading = false;
+          });
+          return;
+        }
 
 
         // 校验规格活动数据
         // 校验规格活动数据
         let hasError = false;
         let hasError = false;
@@ -2267,6 +2409,14 @@ export default {
   border-color: #409EFF;
   border-color: #409EFF;
   background: #ECF5FF;
   background: #ECF5FF;
 }
 }
+.spec-select-disabled {
+  cursor: not-allowed !important;
+  opacity: 0.85;
+}
+.spec-select-disabled:hover {
+  border-color: #409EFF;
+  background: #ECF5FF;
+}
 .spec-select-info {
 .spec-select-info {
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;