Bladeren bron

refactor(admin): 重新设计会员中心页面 - 消除所有 levelCode 空值访问风险

- 统计卡片增加图标,对齐 Dashboard 设计风格
- 所有表格数据源使用 safeLevels/safeMaintainUsers 过滤,杜绝 undefined.levelCode
- 升级路径用 el-tag 路径样式展示
- 权限详情对话框重构为 el-descriptions 布局
- 空状态用统一 .empty-state class
- loadOverview 合并 getMembershipStats + /api/admin/membership/overview 两次请求
iwt 3 weken geleden
bovenliggende
commit
550bccda5f
1 gewijzigde bestanden met toevoegingen van 359 en 343 verwijderingen
  1. 359 343
      cfc-web/src/views/admin/MembershipCenter.vue

+ 359 - 343
cfc-web/src/views/admin/MembershipCenter.vue

@@ -1,389 +1,322 @@
 <template>
   <div class="membership-center admin-page">
-    <!-- 统计卡片 -->
-    <el-row :gutter="20" style="margin-bottom:20px;">
-      <el-col :span="6">
-        <el-card shadow="hover" class="stat-card">
-          <div class="stat-value">{{ overview.freeCount }}</div>
-          <div class="stat-label">免费用户</div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card shadow="hover" class="stat-card stat-family">
-          <div class="stat-value">{{ overview.familyCount }}</div>
-          <div class="stat-label">FAMILY会员</div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card shadow="hover" class="stat-card stat-provider">
-          <div class="stat-value">{{ overview.providerCount }}</div>
-          <div class="stat-label">PROVIDER会员</div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card shadow="hover" class="stat-card stat-new">
-          <div class="stat-value">{{ overview.newFamilyThisMonth }}</div>
-          <div class="stat-label">本月新增</div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card shadow="hover" class="stat-card stat-lifetime">
-          <div class="stat-value">{{ overview.lifetimeCount }}</div>
-          <div class="stat-label">终身会员</div>
+
+    <!-- 页面头部 -->
+    <div class="admin-page-header">
+      <span class="admin-page-title">会员管理</span>
+      <el-button size="small" icon="el-icon-refresh" @click="refreshAll" :loading="overviewLoading">刷新</el-button>
+    </div>
+
+    <!-- 统计概览 -->
+    <el-row :gutter="16" style="margin-bottom:16px;">
+      <el-col :xs="12" :sm="8" :md="4" v-for="(card, idx) in statCards" :key="idx">
+        <el-card shadow="hover" class="stat-card" :class="'stat-' + card.color">
+          <div class="stat-icon-wrap"><i :class="card.icon"></i></div>
+          <div class="stat-body">
+            <div class="stat-value">{{ card.value }}</div>
+            <div class="stat-label">{{ card.label }}</div>
+          </div>
         </el-card>
       </el-col>
     </el-row>
 
     <!-- 会员等级配置 -->
-    <el-card style="margin-bottom:20px;">
-      <div slot="header">
-        <span>会员等级配置</span>
+    <el-card class="admin-card">
+      <div slot="header" class="admin-page-header">
+        <span class="admin-page-title">会员等级配置</span>
+        <span class="text-muted" style="font-size:12px;">共 {{ safeLevels.length }} 个等级</span>
+      </div>
+      <div class="table-scroll-wrap-lg">
+        <el-table :data="safeLevels" v-loading="levelsLoading" border stripe size="small" empty-text="暂无等级配置">
+          <el-table-column prop="levelCode" label="等级编码" width="110">
+            <template slot-scope="{ row }">
+              <el-tag :type="levelTagType(row.levelCode)" size="small">{{ row.levelCode }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="levelName" label="等级名称" width="130" />
+          <el-table-column prop="levelDesc" label="描述" min-width="160" show-overflow-tooltip />
+          <el-table-column label="月费" width="80">
+            <template slot-scope="{ row }">{{ formatPrice(row.priceMonthly) }}</template>
+          </el-table-column>
+          <el-table-column label="季费" width="80">
+            <template slot-scope="{ row }">{{ formatPrice(row.priceQuarterly) }}</template>
+          </el-table-column>
+          <el-table-column label="年费" width="80">
+            <template slot-scope="{ row }">{{ formatPrice(row.priceYearly) }}</template>
+          </el-table-column>
+          <el-table-column label="孩子上限" width="90" align="center">
+            <template slot-scope="{ row }">{{ row.maxChildren || '—' }}</template>
+          </el-table-column>
+          <el-table-column label="每日任务" width="90" align="center">
+            <template slot-scope="{ row }">{{ row.maxTasksPerDay || '—' }}</template>
+          </el-table-column>
+          <el-table-column label="AI审核" width="80" align="center">
+            <template slot-scope="{ row }">
+              <el-tag :type="row.aiReviewEnabled ? 'success' : 'info'" size="small">{{ row.aiReviewEnabled ? '是' : '否' }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="优先客服" width="80" align="center">
+            <template slot-scope="{ row }">
+              <el-tag :type="row.prioritySupport ? 'success' : 'info'" size="small">{{ row.prioritySupport ? '是' : '否' }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="100" fixed="right">
+            <template slot-scope="{ row }">
+              <el-button size="mini" type="primary" plain @click="handleViewFeatures(row)">权限</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
       </div>
-      <el-table :max-height="tableHeight" :data="levels" v-loading="levelsLoading" border stripe>
-        <el-table-column prop="levelCode" label="等级编码" width="120">
-          <template slot-scope="{ row }">
-            <el-tag v-if="row" :type="levelTagType(row.levelCode)" size="mini">{{ row.levelCode }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="levelName" label="等级名称" width="150" />
-        <el-table-column prop="levelDesc" label="描述" min-width="180" show-overflow-tooltip />
-        <el-table-column label="月费(元)" width="100">
-          <template slot-scope="{ row }">{{ row ? formatPrice(row.priceMonthly) : '—' }}</template>
-        </el-table-column>
-        <el-table-column label="季费(元)" width="100">
-          <template slot-scope="{ row }">{{ row ? formatPrice(row.priceQuarterly) : '—' }}</template>
-        </el-table-column>
-        <el-table-column label="年费(元)" width="100">
-          <template slot-scope="{ row }">{{ row ? formatPrice(row.priceYearly) : '—' }}</template>
-        </el-table-column>
-        <el-table-column prop="maxChildren" label="孩子上限" width="90" />
-        <el-table-column prop="maxTasksPerDay" label="每日任务" width="90" />
-        <el-table-column label="AI审核" width="80">
-          <template slot-scope="{ row }">
-            <el-tag v-if="row" :type="row.aiReviewEnabled ? 'success' : 'info'" size="mini">{{ row.aiReviewEnabled ? '是' : '否' }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="优先客服" width="80">
-          <template slot-scope="{ row }">
-            <el-tag v-if="row" :type="row.prioritySupport ? 'success' : 'info'" size="mini">{{ row.prioritySupport ? '是' : '否' }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" width="200" fixed="right">
-          <template slot-scope="{ row }">
-            <el-button v-if="row" size="mini" type="primary" @click="handleEditLevel(row)">编辑</el-button>
-            <el-dropdown v-if="row" trigger="hover" @command="(cmd) => handleLevelCmd(row, cmd)">
-              <el-button size="mini">
-                更多<i class="el-icon-arrow-down el-icon--right"></i>
-              </el-button>
-              <el-dropdown-menu slot="dropdown">
-                <el-dropdown-item command="features" icon="el-icon-view">查看权限</el-dropdown-item>
-              </el-dropdown-menu>
-            </el-dropdown>
-          </template>
-        </el-table-column>
-      </el-table>
     </el-card>
 
-    <!-- 升级记录 -->
-    <el-card>
-      <div slot="header">
-        <span>会员升级记录</span>
-    </div>
-    <el-table :data="upgradeRecords" v-loading="recordsLoading" border stripe :max-height="tableHeight">
-        <el-table-column prop="id" label="ID" width="70" />
-        <el-table-column label="用户" width="120">
-          <template slot-scope="{ row }">{{ row ? (row.userName || '用户' + row.userId) : '—' }}</template>
-        </el-table-column>
-        <el-table-column label="从" width="100">
-          <template slot-scope="{ row }">
-            <el-tag v-if="row" size="mini">{{ row.fromLevel || 'FREE' }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="到" width="100">
-          <template slot-scope="{ row }">
-            <el-tag v-if="row" type="success" size="mini">{{ row.toLevel }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="upgradeType" label="升级方式" width="120">
-          <template slot-scope="{ row }">
-            <el-tag v-if="row" :type="upgradeTypeTag(row.upgradeType)" size="mini">{{ upgradeTypeLabel(row.upgradeType) }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="orderId" label="订单ID" width="80" />
-        <el-table-column prop="expireTime" label="到期时间" width="160">
-          <template slot-scope="{ row }">{{ row ? formatTime(row.expireTime) : '—' }}</template>
-        </el-table-column>
-        <el-table-column prop="createdAt" label="创建时间" width="160">
-          <template slot-scope="{ row }">{{ row ? formatTime(row.createdAt) : '—' }}</template>
-        </el-table-column>
-    </el-table>
-    <el-pagination
-        v-if="recordsTotal > 0"
-        @size-change="handleRecordSizeChange"
-        @current-change="handleRecordPageChange"
-        :current-page="recordFilter.page"
-        :page-sizes="[10, 20, 50]"
-        :page-size="recordFilter.size"
-        layout="total, sizes, prev, pager, next, jumper"
-        :total="recordsTotal"
-        style="margin-top:16px;text-align:right"
-      />
+    <!-- 会员升级记录 -->
+    <el-card class="admin-card">
+      <div slot="header" class="admin-page-header">
+        <span class="admin-page-title">升级记录</span>
+      </div>
+      <div class="table-scroll-wrap-lg">
+        <el-table :data="upgradeRecords" v-loading="recordsLoading" border stripe size="small" empty-text="暂无升级记录">
+          <el-table-column prop="id" label="ID" width="60" />
+          <el-table-column label="用户" width="110">
+            <template slot-scope="{ row }">{{ row.userName || ('用户' + row.userId) }}</template>
+          </el-table-column>
+          <el-table-column label="升级路径" width="140">
+            <template slot-scope="{ row }">
+              <span class="level-path">
+                <el-tag size="mini" type="info">{{ row.fromLevel || 'FREE' }}</el-tag>
+                <i class="el-icon-right"></i>
+                <el-tag size="mini" type="success">{{ row.toLevel || '—' }}</el-tag>
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="升级方式" width="110">
+            <template slot-scope="{ row }">
+              <el-tag :type="upgradeTypeTag(row.upgradeType)" size="mini">{{ upgradeTypeLabel(row.upgradeType) }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="orderId" label="订单号" width="120" show-overflow-tooltip />
+          <el-table-column label="到期时间" width="150">
+            <template slot-scope="{ row }">{{ formatTime(row.expireTime) }}</template>
+          </el-table-column>
+          <el-table-column label="创建时间" width="150">
+            <template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template>
+          </el-table-column>
+        </el-table>
+      </div>
+      <div class="pagination-wrap" v-if="recordsTotal > 0">
+        <el-pagination
+          @size-change="handleRecordSizeChange"
+          @current-change="handleRecordPageChange"
+          :current-page="recordFilter.page"
+          :page-sizes="[10, 20, 50]"
+          :page-size="recordFilter.size"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="recordsTotal"
+        />
+      </div>
     </el-card>
 
     <!-- 会员维护 -->
-    <el-card style="margin-top:20px;">
-      <div slot="header">
-        <span>会员维护</span>
+    <el-card class="admin-card">
+      <div slot="header" class="admin-page-header">
+        <span class="admin-page-title">会员维护</span>
       </div>
-      <div class="maintain-row">
+      <div class="filter-bar">
         <el-input
           v-model="maintainSearch"
-          placeholder="输入手机号/昵称/用户ID搜索"
+          placeholder="手机号 / 昵称 / 用户ID"
           clearable
-          style="width:280px;margin-right:10px;"
+          style="width:240px;"
           @keyup.enter.native="handleMaintainSearch"
         />
-        <el-button type="primary" size="small" @click="handleMaintainSearch">搜索</el-button>
+        <el-button type="primary" size="small" icon="el-icon-search" @click="handleMaintainSearch" :loading="maintainSearching">搜索</el-button>
+        <span v-if="safeMaintainUsers.length" class="search-result-count">找到 {{ safeMaintainUsers.length }} 个用户</span>
       </div>
-      <div v-if="maintainUsers.length" style="margin-top:14px;">
-        <el-table :data="maintainUsers" border stripe size="small">
+      <div v-if="safeMaintainUsers.length" class="table-scroll-wrap-sm">
+        <el-table :data="safeMaintainUsers" border stripe size="small">
           <el-table-column prop="id" label="ID" width="70" />
-          <el-table-column prop="nickname" label="昵称" width="140" />
-          <el-table-column prop="phone" label="手机号" width="140" />
+          <el-table-column prop="nickname" label="昵称" width="130" />
+          <el-table-column prop="phone" label="手机号" width="130" />
           <el-table-column label="当前等级" width="110">
             <template slot-scope="{ row }">
-              <el-tag v-if="row" :type="levelTagType(row.memberLevel)" size="mini">{{ row.memberLevel || 'FREE' }}</el-tag>
+              <el-tag :type="levelTagType(row.memberLevel)" size="mini">{{ row.memberLevel || 'FREE' }}</el-tag>
             </template>
           </el-table-column>
           <el-table-column label="推荐人ID" width="100">
-            <template slot-scope="{ row }">{{ row ? (row.referrerId || '—') : '—' }}</template>
+            <template slot-scope="{ row }">{{ row.referrerId || '—' }}</template>
           </el-table-column>
-          <el-table-column label="操作" width="160">
+          <el-table-column label="操作" width="100" fixed="right">
             <template slot-scope="{ row }">
-              <el-button v-if="row" size="mini" type="warning" @click="openMaintainDialog(row)">维护</el-button>
+              <el-button size="mini" type="warning" @click="openMaintainDialog(row)">维护</el-button>
             </template>
           </el-table-column>
         </el-table>
       </div>
-    </el-card>
-
-    <!-- 会员维护对话框 -->
-    <el-dialog title="会员维护" :visible.sync="maintainVisible" width="520px" @close="maintainVisible = false">
-      <el-form :model="maintainForm" label-width="100px" size="small">
-        <el-form-item label="用户">
-          <span>{{ maintainForm.nickname }}(ID: {{ maintainForm.id }})</span>
-        </el-form-item>
-        <el-form-item label="会员等级">
-          <el-select v-model="maintainForm.memberLevel" style="width:200px;" placeholder="选择等级">
-            <el-option
-              v-for="lv in safeLevels"
-              :key="lv.levelCode"
-              :label="lv.levelName + ' (' + lv.levelCode + ')'"
-              :value="lv.levelCode"
-            />
-          </el-select>
-          <span style="color:#999;margin-left:8px;">选终身会员则永久有效</span>
-        </el-form-item>
-        <el-form-item label="推荐人ID">
-          <el-input v-model="maintainForm.referrerIdStr" placeholder="留空不变,填0清除" style="width:200px;" />
-          <span style="color:#999;margin-left:8px;">填目标用户ID</span>
-        </el-form-item>
-      </el-form>
-      <div slot="footer">
-        <el-button @click="maintainVisible = false">取消</el-button>
-        <el-button type="primary" :loading="maintainSubmitting" @click="handleMaintainSubmit">保存</el-button>
+      <div v-else-if="maintainSearched" class="empty-state">
+        <i class="el-icon-user-solid"></i>
+        <span>未找到匹配用户</span>
       </div>
-    </el-dialog>
+    </el-card>
 
     <!-- 权限详情对话框 -->
-    <el-dialog title="功能权限详情" :visible.sync="featuresVisible" width="500px">
-      <div v-if="currentLevel" class="features-content">
-        <h3>{{ currentLevel.levelName }} ({{ currentLevel.levelCode }})</h3>
-        <p style="color:#666;margin-bottom:16px;">{{ currentLevel.levelDesc }}</p>
-        <el-descriptions :column="1" border>
-          <el-descriptions-item label="孩子数量上限">{{ currentLevel.maxChildren }}</el-descriptions-item>
-          <el-descriptions-item label="每日任务上限">{{ currentLevel.maxTasksPerDay }}</el-descriptions-item>
-          <el-descriptions-item label="AI审核">{{ currentLevel.aiReviewEnabled ? '启用' : '未启用' }}</el-descriptions-item>
-          <el-descriptions-item label="优先客服">{{ currentLevel.prioritySupport ? '支持' : '不支持' }}</el-descriptions-item>
-          <el-descriptions-item label="老师咨询">{{ currentLevel.teacherConsultation ? '支持' : '不支持' }}</el-descriptions-item>
+    <el-dialog title="功能权限详情" :visible.sync="featuresVisible" width="520px" @close="featuresVisible = false">
+      <div v-if="currentLevel">
+        <div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
+          <el-tag :type="levelTagType(currentLevel.levelCode)" size="medium">{{ currentLevel.levelCode }}</el-tag>
+          <span style="font-size:16px;font-weight:600;">{{ currentLevel.levelName }}</span>
+        </div>
+        <p class="text-muted" style="margin:0 0 16px;">{{ currentLevel.levelDesc || '暂无描述' }}</p>
+        <el-descriptions :column="2" border size="small">
+          <el-descriptions-item label="月费">{{ formatPrice(currentLevel.priceMonthly) }} 元</el-descriptions-item>
+          <el-descriptions-item label="年费">{{ formatPrice(currentLevel.priceYearly) }} 元</el-descriptions-item>
+          <el-descriptions-item label="孩子上限">{{ currentLevel.maxChildren || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="每日任务上限">{{ currentLevel.maxTasksPerDay || '—' }}</el-descriptions-item>
+          <el-descriptions-item label="AI审核">
+            <el-tag :type="currentLevel.aiReviewEnabled ? 'success' : 'info'" size="mini">{{ currentLevel.aiReviewEnabled ? '启用' : '未启用' }}</el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="优先客服">
+            <el-tag :type="currentLevel.prioritySupport ? 'success' : 'info'" size="mini">{{ currentLevel.prioritySupport ? '支持' : '不支持' }}</el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="老师咨询" :span="2">
+            <el-tag :type="currentLevel.teacherConsultation ? 'success' : 'info'" size="mini">{{ currentLevel.teacherConsultation ? '支持' : '不支持' }}</el-tag>
+          </el-descriptions-item>
         </el-descriptions>
-        <div v-if="currentLevel.features" style="margin-top:16px;">
-          <div class="features-title">功能权限列表</div>
-          <div class="features-list">
-            <el-tag v-for="(f, i) in parseFeatures(currentLevel.features)" :key="i" size="mini" style="margin:4px;">{{ f }}</el-tag>
-            <span v-if="!parseFeatures(currentLevel.features).length" style="color:#999;">暂无特殊权限</span>
+        <div v-if="parseFeatures(currentLevel.features).length" style="margin-top:16px;">
+          <div style="font-weight:600;margin-bottom:8px;color:#606266;">功能权限列表</div>
+          <div style="display:flex;flex-wrap:wrap;gap:6px;">
+            <el-tag v-for="(f, i) in parseFeatures(currentLevel.features)" :key="i" size="small">{{ f }}</el-tag>
           </div>
         </div>
       </div>
+      <div slot="footer">
+        <el-button size="small" @click="featuresVisible = false">关闭</el-button>
+      </div>
     </el-dialog>
 
-    <!-- 编辑会员等级 -->
-    <el-dialog title="编辑会员等级" :visible.sync="editVisible" width="540px" @close="editVisible = false">
-      <el-form :model="editForm" label-width="110px" size="small">
-        <el-form-item label="等级编码">
-          <el-tag>{{ editForm.levelCode }}</el-tag>
-          <span style="color:#999;margin-left:8px;">编码不可修改</span>
-        </el-form-item>
-        <el-form-item label="等级名称" required>
-          <el-input v-model="editForm.levelName" placeholder="如:家庭会员" maxlength="32" show-word-limit />
-        </el-form-item>
-        <el-form-item label="等级描述">
-          <el-input v-model="editForm.levelDesc" type="textarea" :rows="2" maxlength="200" show-word-limit />
-        </el-form-item>
-        <el-form-item label="月费(元)">
-          <el-input-number v-model="editForm.priceMonthly" :min="0" :step="100" :precision="0" style="width:200px" />
-          <span style="color:#999;margin-left:8px;">单位:分</span>
-        </el-form-item>
-        <el-form-item label="季费(元)">
-          <el-input-number v-model="editForm.priceQuarterly" :min="0" :step="100" :precision="0" style="width:200px" />
-        </el-form-item>
-        <el-form-item label="年费(元)">
-          <el-input-number v-model="editForm.priceYearly" :min="0" :step="100" :precision="0" style="width:200px" />
-        </el-form-item>
-        <el-form-item label="孩子上限">
-          <el-input-number v-model="editForm.maxChildren" :min="0" :max="20" :step="1" style="width:200px" />
-        </el-form-item>
-        <el-form-item label="每日任务上限">
-          <el-input-number v-model="editForm.maxTasksPerDay" :min="0" :max="999" :step="1" style="width:200px" />
+    <!-- 会员维护对话框 -->
+    <el-dialog title="会员维护" :visible.sync="maintainVisible" width="480px" @close="maintainVisible = false">
+      <el-form :model="maintainForm" label-width="90px" size="small">
+        <el-form-item label="用户">
+          <span style="font-weight:500;">{{ maintainForm.nickname }}</span>
+          <span class="text-muted" style="margin-left:6px;">ID: {{ maintainForm.id }}</span>
         </el-form-item>
-        <el-form-item label="AI审核">
-          <el-switch v-model="editForm.aiReviewEnabled" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="关闭" />
+        <el-form-item label="会员等级" required>
+          <el-select v-model="maintainForm.memberLevel" style="width:180px;" placeholder="选择等级">
+            <el-option v-for="lv in safeLevels" :key="lv.levelCode" :label="lv.levelName + ' (' + lv.levelCode + ')'" :value="lv.levelCode" />
+          </el-select>
+          <span class="text-muted" style="margin-left:8px;font-size:12px;">选终身会员则永久有效</span>
         </el-form-item>
-        <el-form-item label="优先客服">
-          <el-switch v-model="editForm.prioritySupport" :active-value="1" :inactive-value="0" active-text="支持" inactive-text="不支持" />
+        <el-form-item label="推荐人ID">
+          <el-input v-model="maintainForm.referrerIdStr" placeholder="留空不变,填 0 清除" style="width:180px;" />
+          <span class="text-muted" style="margin-left:8px;font-size:12px;">填目标用户ID</span>
         </el-form-item>
       </el-form>
       <div slot="footer">
-        <el-button @click="editVisible = false">取消</el-button>
-        <el-button type="primary" :loading="submittingLevel" @click="handleSubmitLevel">保存</el-button>
+        <el-button size="small" @click="maintainVisible = false">取消</el-button>
+        <el-button type="primary" size="small" :loading="maintainSubmitting" @click="handleMaintainSubmit">保存</el-button>
       </div>
     </el-dialog>
+
   </div>
 </template>
 
 <script>
-import { searchUsers, updateMembershipUser } from '@/api/admin'
+import { searchUsers, updateMembershipUser, getMembershipStats } from '@/api/admin'
+
+const LEVEL_TAG_TYPE = { FREE: 'info', FAMILY: 'success', PROVIDER: 'warning', LIFETIME: 'danger' }
+const UPGRADE_TYPE_LABEL = { pay: '付费升级', consumption: '消费升级', admin: '管理员操作', trial: '试用激活', lifetime: '终身会员授予' }
+const UPGRADE_TYPE_TAG = { pay: 'success', consumption: 'primary', admin: 'warning', trial: 'info', lifetime: 'danger' }
 
 export default {
   name: 'MembershipCenter',
-  computed: {
-    tableHeight() {
-      return window.innerHeight - 300
-    },
-    safeLevels() {
-      const list = this.levels || []
-      return list.filter(function(l) { return l && typeof l === 'object' && l.levelCode })
-    }
-  },
   data() {
     return {
-      // overview
-      overview: {
-        freeCount: 0,
-        familyCount: 0,
-        providerCount: 0,
-        newFamilyThisMonth: 0,
-        totalFamilyMemberships: 0,
-        lifetimeCount: 0
-      },
-      // levels
+      // 统计卡片
+      overviewLoading: false,
+      statCards: [
+        { label: '免费用户', value: 0, icon: 'el-icon-user', color: 'info' },
+        { label: 'FAMILY 会员', value: 0, icon: 'el-icon-s-home', color: 'success' },
+        { label: 'PROVIDER 会员', value: 0, icon: 'el-icon-school', color: 'warning' },
+        { label: '本月新增', value: 0, icon: 'el-icon-trophy', color: 'primary' },
+        { label: '终身会员', value: 0, icon: 'el-icon-crown', color: 'danger' },
+        { label: '家庭会员总数', value: 0, icon: 'el-icon-collection', color: 'cyan' }
+      ],
+      // 等级配置
       levelsLoading: false,
       levels: [],
-      // upgrade records
+      // 升级记录
       recordsLoading: false,
       upgradeRecords: [],
       recordsTotal: 0,
-      recordFilter: {
-        page: 1,
-        size: 10
-      },
-      // dialogs
-      featuresVisible: false,
-      currentLevel: null,
-      // membership maintain
+      recordFilter: { page: 1, size: 10 },
+      // 会员维护
       maintainSearch: '',
+      maintainSearching: false,
+      maintainSearched: false,
       maintainUsers: [],
       maintainVisible: false,
       maintainSubmitting: false,
-      maintainForm: {
-        id: null,
-        nickname: '',
-        memberLevel: '',
-        referrerIdStr: ''
-      }
+      maintainForm: { id: null, nickname: '', memberLevel: '', referrerIdStr: '' },
+      // 权限详情
+      featuresVisible: false,
+      currentLevel: null
+    }
+  },
+  computed: {
+    safeLevels() {
+      return (this.levels || []).filter(function(l) { return l && typeof l === 'object' && l.levelCode })
+    },
+    safeMaintainUsers() {
+      return (this.maintainUsers || []).filter(function(u) { return u && typeof u === 'object' })
     }
   },
   created() {
-    this.loadOverview()
-    this.loadLevels()
-    this.loadUpgradeRecords()
+    this.refreshAll()
   },
   methods: {
-    formatPrice(price) {
-      if (price === null || price === undefined) return '—'
-      return (price / 100).toFixed(2)
-    },
-    formatTime(t) {
-      if (!t) return '-'
-      return t.replace ? t.replace('T', ' ').substring(0, 19) : t
-    },
-    levelTagType(code) {
-      if (code === 'FREE') return 'info'
-      if (code === 'FAMILY') return 'success'
-      if (code === 'PROVIDER') return 'warning'
-      if (code === 'LIFETIME') return 'danger' // 紫红色高亮终身会员
-      return 'primary'
-    },
-    upgradeTypeLabel(type) {
-      const map = { pay: '付费升级', consumption: '消费升级', admin: '管理员操作', trial: '试用激活', lifetime: '终身会员授予' }
-      return map[type] || type || '—'
-    },
-    upgradeTypeTag(type) {
-      if (type === 'pay') return 'success'
-      if (type === 'consumption') return 'primary'
-      if (type === 'admin') return 'warning'
-      if (type === 'trial') return 'info'
-      if (type === 'lifetime') return 'danger'
-      return ''
-    },
-    parseFeatures(features) {
-      if (!features) return []
-      try {
-        if (features.startsWith('[')) return JSON.parse(features)
-        return features.split(',').map(f => f.trim()).filter(Boolean)
-      } catch (e) {
-        return []
-      }
-    },
-    async _request(url, data) {
-      const axios = this.$axios || (await import('axios')).default
-      const baseURL = process.env.VUE_APP_BASE_API || ''
-      return axios.post(baseURL + url, data)
+    async refreshAll() {
+      this.overviewLoading = true
+      await Promise.all([this.loadOverview(), this.loadLevels(), this.loadUpgradeRecords()])
+      this.overviewLoading = false
     },
+
     async loadOverview() {
       try {
-        const res = await this._request('/api/admin/membership/overview')
-        if (res.data && res.data.code === 200) {
-          Object.assign(this.overview, res.data.data)
+        const [statsRes, overviewRes] = await Promise.all([
+          getMembershipStats(),
+          this._request('/api/admin/membership/overview')
+        ])
+        if (statsRes.code === 200 && statsRes.data) {
+          const s = statsRes.data
+          this.statCards[0].value = s.freeCount ?? 0
+          this.statCards[1].value = s.familyCount ?? 0
+          this.statCards[2].value = s.providerCount ?? 0
+        }
+        if (overviewRes.data && overviewRes.data.code === 200) {
+          const o = overviewRes.data.data || {}
+          this.statCards[3].value = o.newFamilyThisMonth ?? 0
+          this.statCards[4].value = o.lifetimeCount ?? 0
+          this.statCards[5].value = o.totalFamilyMemberships ?? 0
         }
       } catch (e) {
-        // ignore
+        console.error('加载会员统计失败', e)
       }
     },
+
     async loadLevels() {
       this.levelsLoading = true
       try {
-        const res = await this._request('/api/admin/membership/levels')
-        if (res.data && res.data.code === 200) {
-          const list = res.data.data || []
-          this.levels = Array.isArray(list) ? list.filter(function(l) { return l && typeof l === 'object' }) : []
+        const res = await listMembershipLevels()
+        if (res.code === 200) {
+          this.levels = Array.isArray(res.data) ? res.data.filter(function(l) { return l && typeof l === 'object' }) : []
         }
       } catch (e) {
-        this.$message && this.$message.error && this.$message.error('加载等级配置失败')
+        this.$message.error('加载等级配置失败')
       } finally {
         this.levelsLoading = false
       }
     },
+
     async loadUpgradeRecords() {
       this.recordsLoading = true
       try {
@@ -391,18 +324,19 @@ export default {
           page: this.recordFilter.page,
           size: this.recordFilter.size
         })
-if (res.data && res.data.code === 200) {
-            const records = (res.data.data && res.data.data.records) || []
-            this.upgradeRecords = Array.isArray(records) ? records.filter(function(r) { return r && typeof r === 'object' }) : []
-            this.recordsTotal = res.data.data.total || 0
-            await this.resolveUserNames(this.upgradeRecords, 'userId')
-          }
+        if (res.data && res.data.code === 200) {
+          const records = (res.data.data && res.data.data.records) || []
+          this.upgradeRecords = Array.isArray(records) ? records.filter(function(r) { return r && typeof r === 'object' }) : []
+          this.recordsTotal = res.data.data.total || 0
+          await this.resolveUserNames(this.upgradeRecords, 'userId')
+        }
       } catch (e) {
-        this.$message && this.$message.error && this.$message.error('加载升级记录失败')
+        this.$message.error('加载升级记录失败')
       } finally {
         this.recordsLoading = false
       }
     },
+
     handleRecordSizeChange(size) {
       this.recordFilter.size = size
       this.recordFilter.page = 1
@@ -412,17 +346,12 @@ if (res.data && res.data.code === 200) {
       this.recordFilter.page = page
       this.loadUpgradeRecords()
     },
-    handleEditLevel(row) {
-      this.$message.info('会员等级配置请在 "系统配置" 中编辑')
-    },
-    handleLevelCmd(row, cmd) {
-      switch (cmd) {
-        case 'features':
-          this.currentLevel = row
-          this.featuresVisible = true
-          break
-      }
+
+    handleViewFeatures(row) {
+      this.currentLevel = row
+      this.featuresVisible = true
     },
+
     resolveUserNames(items, idField) {
       if (!items || items.length === 0) return Promise.resolve()
       var ids = []
@@ -435,38 +364,44 @@ if (res.data && res.data.code === 200) {
           .then(function(res) {
             if (res.data && res.data.length > 0) {
               var u = res.data[0]
-              nameMap[id] = u.nickname || u.realName || '用户' + id
+              nameMap[id] = u.nickname || u.realName || ('用户' + id)
             } else {
-              nameMap[id] = '用户' + id
+              nameMap[id] = ('用户' + id)
             }
           })
-          .catch(function() {
-            nameMap[id] = '用户' + id
-          })
+          .catch(function() { nameMap[id] = ('用户' + id) })
       })).then(function() {
         items.forEach(function(item) {
-          item.userName = nameMap[item[idField]] || '用户' + item[idField]
+          item.userName = nameMap[item[idField]] || ('用户' + item[idField])
         })
       })
     },
+
     async handleMaintainSearch() {
       const kw = (this.maintainSearch || '').trim()
       if (!kw) {
         this.$message && this.$message.warning && this.$message.warning('请输入搜索关键词')
         return
       }
+      this.maintainSearching = true
+      this.maintainSearched = false
       try {
         const res = await searchUsers({ keyword: kw })
         if (res.data && res.data.length > 0) {
           this.maintainUsers = res.data
+          this.maintainSearched = true
         } else {
           this.maintainUsers = []
+          this.maintainSearched = true
           this.$message && this.$message.info && this.$message.info('未找到用户')
         }
       } catch (e) {
         this.$message && this.$message.error && this.$message.error('搜索失败')
+      } finally {
+        this.maintainSearching = false
       }
     },
+
     openMaintainDialog(row) {
       this.maintainForm = {
         id: row.id,
@@ -476,27 +411,22 @@ if (res.data && res.data.code === 200) {
       }
       this.maintainVisible = true
     },
+
     async handleMaintainSubmit() {
       if (!this.maintainForm.id) return
       this.maintainSubmitting = true
       try {
         const payload = { userId: this.maintainForm.id }
-        if (this.maintainForm.memberLevel) {
-          payload.memberLevel = this.maintainForm.memberLevel
-        }
+        if (this.maintainForm.memberLevel) payload.memberLevel = this.maintainForm.memberLevel
         const refStr = (this.maintainForm.referrerIdStr || '').trim()
-        if (refStr === '0') {
-          payload.referrerId = ''
-        } else if (refStr !== '') {
-          payload.referrerId = refStr
-        }
+        if (refStr === '0') payload.referrerId = ''
+        else if (refStr !== '') payload.referrerId = refStr
         const res = await updateMembershipUser(payload)
         if (res.code === 200) {
           this.$message && this.$message.success && this.$message.success(res.data || '更新成功')
           this.maintainVisible = false
           this.handleMaintainSearch()
-          this.loadOverview()
-          this.loadUpgradeRecords()
+          this.refreshAll()
         } else {
           this.$message && this.$message.error && this.$message.error(res.message || '更新失败')
         }
@@ -505,21 +435,107 @@ if (res.data && res.data.code === 200) {
       } finally {
         this.maintainSubmitting = false
       }
+    },
+
+    // ---- helpers ----
+    formatPrice(price) {
+      if (price === null || price === undefined) return '—'
+      return (price / 100).toFixed(2)
+    },
+    formatTime(t) {
+      if (!t) return '—'
+      return t.replace ? t.replace('T', ' ').substring(0, 19) : t
+    },
+    levelTagType(code) {
+      return LEVEL_TAG_TYPE[code] || 'primary'
+    },
+    upgradeTypeLabel(type) {
+      return UPGRADE_TYPE_LABEL[type] || type || '—'
+    },
+    upgradeTypeTag(type) {
+      return UPGRADE_TYPE_TAG[type] || ''
+    },
+    parseFeatures(features) {
+      if (!features) return []
+      try {
+        return features.startsWith('[') ? JSON.parse(features) : features.split(',').map(function(f) { return f.trim() }).filter(Boolean)
+      } catch (e) {
+        return []
+      }
+    },
+    async _request(url, data) {
+      const axios = this.$axios || (await import('axios')).default
+      const baseURL = process.env.VUE_APP_BASE_API || ''
+      return axios.post(baseURL + url, data)
     }
   }
 }
 </script>
 
 <style scoped>
-.membership-center { padding: 20px; }
-.stat-card { text-align: center; cursor: default; }
-.stat-card .stat-value { font-size: 32px; font-weight: bold; color: #303133; }
-.stat-card .stat-label { font-size: 14px; color: #909399; margin-top: 8px; }
-.stat-family .stat-value { color: #67C23A; }
-.stat-provider .stat-value { color: #E6A23C; }
-.stat-new .stat-value { color: #409EFF; }
-.stat-lifetime .stat-value { color: #F56C6C; }
-.features-content { padding: 10px 0; }
-.features-title { font-weight: bold; margin-bottom: 8px; color: #606266; }
-.features-list { display: flex; flex-wrap: wrap; }
+/* ---- 统计卡片 ---- */
+.stat-card {
+  display: flex;
+  align-items: center;
+  padding: 14px 12px;
+  cursor: default;
+  border-radius: 8px;
+  overflow: hidden;
+}
+.stat-icon-wrap {
+  width: 44px;
+  height: 44px;
+  border-radius: 10px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 20px;
+  flex-shrink: 0;
+  margin-right: 12px;
+}
+.stat-body { flex: 1; min-width: 0; }
+.stat-value {
+  font-size: 22px;
+  font-weight: 700;
+  color: #1e293b;
+  line-height: 1.2;
+}
+.stat-label {
+  font-size: 12px;
+  color: #94a3b8;
+  margin-top: 2px;
+}
+.stat-orange .stat-icon-wrap { background: #fef3c7; color: #f97316; }
+.stat-blue   .stat-icon-wrap { background: #dbeafe; color: #3b82f6; }
+.stat-cyan   .stat-icon-wrap { background: #cffafe; color: #06b6d4; }
+.stat-green  .stat-icon-wrap { background: #d1fae5; color: #10b981; }
+.stat-red    .stat-icon-wrap { background: #fee2e2; color: #ef4444; }
+.stat-purple .stat-icon-wrap { background: #ede9fe; color: #8b5cf6; }
+.stat-info   .stat-icon-wrap { background: #f1f5f9; color: #64748b; }
+.stat-primary .stat-icon-wrap { background: #dbeafe; color: #3b82f6; }
+
+/* ---- 升级路径 ---- */
+.level-path {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+}
+.level-path .el-icon-right {
+  color: #909399;
+  font-size: 12px;
+}
+
+/* ---- 空状态 ---- */
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 32px 0;
+  color: #c0c4cc;
+}
+.empty-state i {
+  font-size: 40px;
+  margin-bottom: 8px;
+}
 </style>