“陈博论” 9 месяцев назад
Родитель
Сommit
0a5c108cc0

+ 2 - 3
web/src/views/frontend/components/Me/enterprise.vue

@@ -112,7 +112,7 @@
 import { computed, onMounted, ref, watch } from 'vue'
 import { storeToRefs } from 'pinia'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { Close, Goods, Loading, OfficeBuilding, Plus } from '@element-plus/icons-vue'
+import {  Loading, OfficeBuilding, Plus } from '@element-plus/icons-vue'
 import { useUserInfo } from '/@/stores/userInfo'
 import CreateEnterpriseDialog from '../CreateEnterpriseDialog.vue'
 import AddMemberDialog from '../AddMemberDialog.vue'
@@ -153,7 +153,6 @@ const showAddMemberDialog = ref(false)
 const showProductSeatDialog = ref(false)
 const selectedMemberIdsForDialog = ref<number[]>([])
 
-const employeeTable = ref()
 const currentPage = ref(1)
 const pageSize = ref(10)
 const pageSizes = [10, 20, 30, 50]
@@ -204,7 +203,7 @@ const loadEnterpriseList = async () => {
     const membersRes = await members({
       company_id: companyId.value,
       page: currentPage.value,
-      limit: pageSize.value
+      pageSize: pageSize.value
     })
 
     employeeList.value = membersRes.data?.list || []

+ 110 - 58
web/src/views/frontend/components/Me/purchaseProduct.vue

@@ -31,47 +31,60 @@
 
           <!-- 产品信息 -->
           <div class="product-info">
-            <div class="product-name" :title="item.product_name">{{ item.product_name }}</div>
-
-            <!-- 套餐信息 -->
-            <div class="package-info">
-              <el-tag size="small" type="primary" effect="plain">{{ item.package_type_text }}</el-tag>
-              <el-tag size="small" type="warning" effect="plain">{{ item.card_type_text }}</el-tag>
-            </div>
-
-            <!-- 使用情况 -->
-            <div class="usage-info">
-              <!-- 次卡显示剩余次数 -->
-              <div v-if="item.card_type == 1" class="usage-item">
-                <el-icon>
-                  <Tickets />
-                </el-icon>
-                <span>剩余次数: <strong>{{ item.remaining_times }}</strong></span>
-              </div>
-
-              <!-- 积分卡显示剩余积分 -->
-              <div v-if="item.card_type == 2" class="usage-item">
-                <el-icon>
-                  <Coin />
-                </el-icon>
-                <span>剩余积分: <strong>{{ item.remaining_points }}</strong></span>
+            <div class="product-main">
+              <!-- 左侧:产品基本信息 -->
+              <div class="product-main-left">
+                <div class="product-name" :title="item.product_name">{{ item.product_name }}</div>
+
+                <!-- 套餐信息 -->
+                <div class="package-info">
+                  <el-tag size="small" type="primary" effect="plain">{{ item.package_type_text }}</el-tag>
+                  <el-tag size="small" type="warning" effect="plain">{{ item.card_type_text }}</el-tag>
+                </div>
+
+                <!-- 使用情况 -->
+                <div class="usage-info">
+                  <!-- 次卡显示剩余次数 -->
+                  <div v-if="item.card_type == 1" class="usage-item">
+                    <el-icon>
+                      <Tickets />
+                    </el-icon>
+                    <span>剩余次数: <strong>{{ item.remaining_times }}</strong></span>
+                  </div>
+
+                  <!-- 积分卡显示剩余积分 -->
+                  <div v-if="item.card_type == 2" class="usage-item">
+                    <el-icon>
+                      <Coin />
+                    </el-icon>
+                    <span>剩余积分: <strong>{{ item.remaining_points }}</strong></span>
+                  </div>
+
+                  <!-- 日期卡显示到期时间 -->
+                  <div v-if="[3, 4, 5, 6, 7].includes(item.card_type)" class="usage-item">
+                    <el-icon>
+                      <Calendar />
+                    </el-icon>
+                    <span>{{ item.display_info }}</span>
+                  </div>
+                </div>
               </div>
 
-              <!-- 日期卡显示到期时间 -->
-              <div v-if="[3, 4, 5, 6, 7].includes(item.card_type)" class="usage-item">
-                <el-icon>
-                  <Calendar />
-                </el-icon>
-                <span>{{ item.display_info }}</span>
-              </div>
-            </div>
-
-            <!-- 席位信息(企业套餐) -->
-            <div v-if="item.package_type == 2 && item.total_seats" class="seats-info">
-              <el-progress :percentage="getSeatPercentage(item)" :color="getSeatColor(item)" :stroke-width="6" />
-              <div class="seats-text">
-                <span>已分配: {{ item.assigned_seats || 0 }}/{{ item.total_seats }}</span>
-                <span class="available">可用: {{ item.available_seats || 0 }}</span>
+              <!-- 右侧:席位信息(企业套餐)- 仅日期卡显示(次卡/积分卡不展示“已分配”相关信息) -->
+              <div
+                v-if="item.package_type == 2 && item.total_seats && [3, 4, 5, 6, 7].includes(item.card_type)"
+                class="product-main-right"
+              >
+                <div class="seats-info">
+                  <div class="seats-row">
+                    <div class="seats-label">席位</div>
+                    <div class="seats-value">
+                      <span class="seats-number strong">{{ item.assigned_seats || 0 }}</span>
+                      <span class="seats-split">/</span>
+                      <span class="seats-number">{{ item.total_seats }}</span>
+                    </div>
+                  </div>
+                </div>
               </div>
             </div>
 
@@ -195,20 +208,6 @@ const getStatusClass = (status: number) => {
   return classMap[status] || ''
 }
 
-// 计算席位使用百分比
-const getSeatPercentage = (item: ProductItem) => {
-  if (!item.total_seats || item.total_seats === 0) return 0
-  return Math.round(((item.assigned_seats || 0) / item.total_seats) * 100)
-}
-
-// 获取席位进度条颜色
-const getSeatColor = (item: ProductItem) => {
-  const percentage = getSeatPercentage(item)
-  if (percentage >= 90) return '#f56c6c'
-  if (percentage >= 70) return '#e6a23c'
-  return '#67c23a'
-}
-
 // 格式化日期
 const formatDate = (dateStr: string) => {
   if (!dateStr) return ''
@@ -409,6 +408,28 @@ onMounted(() => {
       gap: 12px;
     }
 
+    .product-main {
+      display: flex;
+      gap: 12px;
+      align-items: flex-end;
+    }
+
+    .product-main-left {
+      flex: 1.3;
+      min-width: 0;
+      display: flex;
+      flex-direction: column;
+      gap: 10px;
+    }
+
+    .product-main-right {
+      flex: 1;
+      display: flex;
+      align-items: center;
+      justify-content: flex-end;
+      min-width: 150px;
+    }
+
     .product-name {
       font-size: 16px;
       font-weight: 600;
@@ -462,22 +483,53 @@ onMounted(() => {
     }
 
     .seats-info {
-      padding: 10px 12px;
+      width: 100%;
+      padding: 8px 12px;
       background: #f5f7fa;
       border-radius: 8px;
+      border: 1px solid #e0e6f1;
+      display: flex;
+      flex-direction: column;
 
-      .seats-text {
+      .seats-row {
         display: flex;
         justify-content: space-between;
-        margin-top: 8px;
+        align-items: center;
         font-size: 12px;
         color: #606266;
+      }
 
-        .available {
-          color: #67c23a;
+      .seats-label {
+        color: #909399;
+      }
+
+      .seats-value {
+        display: flex;
+        align-items: center;
+        font-size: 12px;
+        color: #606266;
+      }
+
+      .seats-number {
+        font-weight: 500;
+        color: #303133;
+
+        &.strong {
+          color: #409eff;
           font-weight: 600;
         }
       }
+
+      .seats-split {
+        margin: 0 4px;
+        color: #c0c4cc;
+      }
+
+      .seats-text {
+        margin-left: 6px;
+        font-size: 12px;
+        color: #909399;
+      }
     }
 
     .product-footer {

+ 3 - 5
web/src/views/frontend/components/Me/subordinates.vue

@@ -115,7 +115,7 @@ const loadData = async () => {
   try {
     const params: Record<string, any> = {
       page: currentPage.value,
-      page_size: pageSize.value
+      pageSize: pageSize.value
     }
 
     // 处理搜索条件
@@ -130,10 +130,8 @@ const loadData = async () => {
     }
 
     const res = await subordinateList(params)
-    if (res.data) {
-      tableData.value = res.data.list || []
-      total.value = res.data.total || 0
-    }
+    tableData.value = res.data.list || []
+    total.value = res.data.total || 0
   } catch (error: any) {
     ElMessage.error(error.message || '获取下级列表失败')
     tableData.value = []

+ 83 - 0
web/src/views/frontend/components/goods.vue

@@ -14,6 +14,10 @@
             <div class="auto-delivery-badge" :class="item.type === 1 ? 'badge-personal' : 'badge-enterprise'">
               <span>{{ item.type === 1 ? '个人套餐' : '企业套餐' }}</span>
             </div>
+            <!-- 卡类型标签(新结构:card_info 优先,兼容 card_type 映射) -->
+            <div class="card-type-badge" :class="getCardTypeBadgeClass(item)">
+              <span>{{ getCardTypeText(item) }}</span>
+            </div>
           </div>
   
           <!-- 商品信息 -->
@@ -56,6 +60,12 @@ type GoodsItem = {
   sold_count: number
   stock_text: GoodsStatus
   type: number
+  // 新卡类型结构:优先使用 card_info(接口可能仍返回 card_type 作为兼容字段)
+  card_type?: number
+  card_info?: {
+    name?: string
+    show_value?: string
+  }
 }
 
 const props = withDefaults(
@@ -94,6 +104,37 @@ const getInventoryClass = (status: GoodsStatus) => {
   return 'status-adequate'
 }
 
+const getCardTypeText = (item: GoodsItem) => {
+  // 新结构优先
+  if (item?.card_info?.name) {
+    const cardName = item.card_info.name
+    const showValue = item.card_info.show_value
+    return showValue ? `${cardName}(${showValue})` : cardName
+  }
+  // 兼容老结构
+  const map: Record<number, string> = {
+    1: '次卡',
+    2: '按积分',
+    3: '日卡',
+    4: '周卡',
+    5: '季卡',
+    6: '月卡',
+    7: '年卡'
+  }
+  const label = map[Number(item?.card_type)] || ''
+  return label || '未知类型'
+}
+
+const getCardTypeBadgeClass = (item: GoodsItem) => {
+  const t = Number(item?.card_type)
+  // 1/2(次卡/积分)一组色,3+(日期卡)一组色;未知则中性
+  if ([1, 2].includes(t)) return 'badge-usage'
+  if ([3, 4, 5, 6, 7].includes(t)) return 'badge-date'
+  // 如果只有 card_info 但没 card_type,也给一个日期卡风格(多数是日/周/月/年)
+  if (item?.card_info?.name) return 'badge-date'
+  return 'badge-unknown'
+}
+
 const handleCardClick = (item: GoodsItem) => {
   emit('handleClick', item)
 }
@@ -208,6 +249,48 @@ const handleCardClick = (item: GoodsItem) => {
           }
         }
       }
+
+      .card-type-badge {
+        position: absolute;
+        top: 12px;
+        left: 90px;
+        border-radius: 16px;
+        padding: 4px 12px;
+        transition: all 0.3s ease;
+
+        span {
+          color: #ffffff;
+          font-size: 12px;
+          font-weight: 500;
+          letter-spacing: 0.3px;
+        }
+
+        &.badge-usage {
+          background: linear-gradient(135deg, #67c23a 0%, #95d475 100%);
+          box-shadow: 0 2px 4px rgba(103, 194, 58, 0.3);
+        }
+
+        &.badge-date {
+          background: linear-gradient(135deg, #909399 0%, #b1b3b8 100%);
+          box-shadow: 0 2px 4px rgba(144, 147, 153, 0.3);
+        }
+
+        &.badge-unknown {
+          background: rgba(0, 0, 0, 0.45);
+          backdrop-filter: blur(2px);
+        }
+
+        @media (max-width: 768px) {
+          top: 8px;
+          right: 8px;
+          padding: 3px 10px;
+          border-radius: 12px;
+
+          span {
+            font-size: 11px;
+          }
+        }
+      }
     }
   
     .goods-info {

+ 6 - 0
web/src/views/frontend/index.vue

@@ -61,6 +61,12 @@ type GoodsItem = {
   sold_count: number
   stock_text: string // 库存状态 充足 即将售罄 售罄
   type: number // 类型 1.个人  2.企业
+  // 新卡类型结构:优先使用 card_info(接口可能仍返回 card_type 作为兼容字段)
+  card_type?: number
+  card_info?: {
+    name?: string
+    show_value?: string
+  }
 }
 
 const router = useRouter()