Просмотр исходного кода

Merge remote-tracking branch 'origin/cfclub' into cfclub

E2E Test Bot 2 месяцев назад
Родитель
Сommit
9360edb424
2 измененных файлов с 705 добавлено и 589 удалено
  1. 346 246
      cfc-frontend/pages/diet/preferences.vue
  2. 359 343
      cfc-web/src/views/admin/MembershipCenter.vue

+ 346 - 246
cfc-frontend/pages/diet/preferences.vue

@@ -1,170 +1,296 @@
 <template>
   <view class="preferences-page">
+    <!-- 顶部进度条 -->
+    <view class="progress-bar">
+      <view class="progress-track">
+        <view class="progress-fill" :style="'width:' + progressPercent + '%'"></view>
+      </view>
+      <view class="progress-labels">
+        <text class="progress-label" :class="{ 'progress-label-active': currentStep >= 1 }">忌口·过敏</text>
+        <text class="progress-label" :class="{ 'progress-label-active': currentStep >= 2 }">口味·偏好</text>
+        <text class="progress-label" :class="{ 'progress-label-active': currentStep >= 3 }">健康目标</text>
+      </view>
+    </view>
 
+    <!-- 步骤内容 -->
     <scroll-view class="content" scroll-y>
-      <!-- 硬性要求 -->
-      <view class="card">
-        <view class="card-accent"></view>
-        <view class="card-body">
-          <text class="card-title">硬性要求(必填)</text>
-          
-          <view class="form-group">
-            <text class="form-label">过敏原 *</text>
-            <view class="tag-group">
-              <view class="tag-btn" v-for="(tag, index) in allergies" :key="index" @tap="removeTag('allergies', index)">
-                <text>{{ tag }}</text>
-                <text class="tag-remove">×</text>
-              </view>
-            </view>
-            <view class="tag-input-row">
-              <input class="tag-input" placeholder="输入后回车添加" v-model="tagInput" @confirm="addTag('allergies')" />
-            </view>
+      <!-- ===== Step 1 忌口与过敏 ===== -->
+      <view v-if="currentStep === 1">
+        <view class="step-head">
+          <text class="step-icon">🚫</text>
+          <view class="step-head-text">
+            <text class="step-title">哪些食物绝对不能碰?</text>
+            <text class="step-sub">为了家人的安全,过敏原和忌口请务必填写</text>
           </view>
+        </view>
 
-          <view class="form-group">
-            <text class="form-label">绝对忌口 *</text>
-            <view class="tag-group">
-              <view class="tag-btn" v-for="(tag, index) in absoluteAvoid" :key="index" @tap="removeTag('absoluteAvoid', index)">
-                <text>{{ tag }}</text>
-                <text class="tag-remove">×</text>
-              </view>
+        <view class="card">
+          <text class="card-title">过敏原 *</text>
+          <view class="chip-group">
+            <view class="chip" v-for="(item, i) in presetAllergies" :key="getChipKey('a', i)"
+              :class="{ 'chip-active': allergies.indexOf(item) >= 0 }"
+              @tap="toggleChip('allergies', item)">
+              <text>{{ item }}</text>
             </view>
-            <view class="tag-input-row">
-              <input class="tag-input" placeholder="输入后回车添加" v-model="tagInput" @confirm="addTag('absoluteAvoid')" />
+          </view>
+          <view class="custom-row" v-if="customField === 'allergies'">
+            <input class="custom-input" placeholder="输入其他过敏原" v-model="customInput"
+              confirm-type="done" @confirm="addCustom" />
+            <view class="custom-confirm" @tap="addCustom"><text>添加</text></view>
+          </view>
+          <view class="custom-add" v-else @tap="openCustom('allergies')">
+            <text>+ 自定义添加</text>
+          </view>
+        </view>
+
+        <view class="card">
+          <text class="card-title">绝对忌口 *</text>
+          <view class="chip-group">
+            <view class="chip" v-for="(item, i) in presetAvoid" :key="getChipKey('v', i)"
+              :class="{ 'chip-active': absoluteAvoid.indexOf(item) >= 0 }"
+              @tap="toggleChip('absoluteAvoid', item)">
+              <text>{{ item }}</text>
             </view>
           </view>
+          <view class="custom-row" v-if="customField === 'absoluteAvoid'">
+            <input class="custom-input" placeholder="输入其他忌口" v-model="customInput"
+              confirm-type="done" @confirm="addCustom" />
+            <view class="custom-confirm" @tap="addCustom"><text>添加</text></view>
+          </view>
+          <view class="custom-add" v-else @tap="openCustom('absoluteAvoid')">
+            <text>+ 自定义添加</text>
+          </view>
+        </view>
 
-          <view class="form-group">
-            <text class="form-label">宗教饮食</text>
-            <view class="radio-group">
-              <view class="radio-item" :class="{'radio-active': religiousDiet === 'none'}" @tap="religiousDiet = 'none'">
-                <text>无限制</text>
-              </view>
-              <view class="radio-item" :class="{'radio-active': religiousDiet === 'halal'}" @tap="religiousDiet = 'halal'">
-                <text>清真</text>
-              </view>
-              <view class="radio-item" :class="{'radio-active': religiousDiet === 'vegetarian'}" @tap="religiousDiet = 'vegetarian'">
-                <text>素食</text>
-              </view>
-              <view class="radio-item" :class="{'radio-active': religiousDiet === 'vegan'}" @tap="religiousDiet = 'vegan'">
-                <text>纯素</text>
-              </view>
+        <view class="card">
+          <text class="card-title">宗教饮食</text>
+          <view class="chip-group">
+            <view class="chip" v-for="(opt, i) in religiousOptions" :key="getChipKey('r', i)"
+              :class="{ 'chip-active': religiousDiet === opt.value }"
+              @tap="religiousDiet = opt.value">
+              <text>{{ opt.label }}</text>
             </view>
           </view>
         </view>
       </view>
+      <!-- /Step 1 -->
 
-      <!-- 软性偏好 -->
-      <view class="card">
-        <view class="card-accent"></view>
-        <view class="card-body">
-          <text class="card-title">软性偏好(可选)</text>
-          
-          <view class="form-group">
-            <text class="form-label">辣度偏好</text>
-            <view class="slider-row">
-              <text class="slider-label">不辣</text>
-              <slider class="slider" min="0" max="5" :value="spiceLevel" @change="spiceLevel = $event.detail.value" show-value />
-              <text class="slider-label">特辣</text>
-            </view>
+      <!-- ===== Step 2 口味与偏好(4 卡片完整代码) ===== -->
+      <view v-if="currentStep === 2">
+        <view class="step-head">
+          <text class="step-icon">😋</text>
+          <view class="step-head-text">
+            <text class="step-title">口味和做法,说说看?</text>
+            <text class="step-sub">选得越细,推荐食谱越合家人口味</text>
           </view>
+        </view>
 
-          <view class="form-group">
-            <text class="form-label">口味偏好</text>
-            <view class="tag-group">
-              <view class="tag-btn" v-for="(tag, index) in flavorPref" :key="index" @tap="removeTag('flavorPref', index)">
-                <text>{{ tag }}</text>
-                <text class="tag-remove">×</text>
-              </view>
-            </view>
-            <view class="tag-input-row">
-              <input class="tag-input" placeholder="酸/甜/咸/鲜/苦" v-model="tagInput" @confirm="addTag('flavorPref')" />
+        <!-- 辣度 -->
+        <view class="card">
+          <text class="card-title">辣度偏好</text>
+          <view class="slider-row">
+            <text class="slider-label">不辣</text>
+            <slider class="slider" min="0" max="5" :value="spiceLevel" @change="spiceLevel = $event.detail.value" show-value />
+            <text class="slider-label">特辣</text>
+          </view>
+        </view>
+
+        <!-- 口味 -->
+        <view class="card">
+          <text class="card-title">口味偏好</text>
+          <view class="chip-group">
+            <view class="chip" v-for="(item, i) in presetFlavor" :key="getChipKey('f', i)"
+              :class="{ 'chip-active': flavorPref.indexOf(item) >= 0 }"
+              @tap="toggleChip('flavorPref', item)">
+              <text>{{ item }}</text>
             </view>
           </view>
+          <view class="custom-row" v-if="customField === 'flavorPref'">
+            <input class="custom-input" placeholder="输入其他口味" v-model="customInput"
+              confirm-type="done" @confirm="addCustom" />
+            <view class="custom-confirm" @tap="addCustom"><text>添加</text></view>
+          </view>
+          <view class="custom-add" v-else @tap="openCustom('flavorPref')">
+            <text>+ 自定义添加</text>
+          </view>
+        </view>
 
-          <view class="form-group">
-            <text class="form-label">菜系偏好</text>
-            <view class="tag-group">
-              <view class="tag-btn" v-for="(tag, index) in cuisinePref" :key="index" @tap="removeTag('cuisinePref', index)">
-                <text>{{ tag }}</text>
-                <text class="tag-remove">×</text>
-              </view>
+        <!-- 菜系 -->
+        <view class="card">
+          <text class="card-title">菜系偏好</text>
+          <view class="chip-group">
+            <view class="chip" v-for="(item, i) in presetCuisine" :key="getChipKey('c', i)"
+              :class="{ 'chip-active': cuisinePref.indexOf(item) >= 0 }"
+              @tap="toggleChip('cuisinePref', item)">
+              <text>{{ item }}</text>
             </view>
-            <view class="tag-input-row">
-              <input class="tag-input" placeholder="川/粤/淮扬/日料/西餐" v-model="tagInput" @confirm="addTag('cuisinePref')" />
+          </view>
+          <view class="custom-row" v-if="customField === 'cuisinePref'">
+            <input class="custom-input" placeholder="输入其他菜系" v-model="customInput"
+              confirm-type="done" @confirm="addCustom" />
+            <view class="custom-confirm" @tap="addCustom"><text>添加</text></view>
+          </view>
+          <view class="custom-add" v-else @tap="openCustom('cuisinePref')">
+            <text>+ 自定义添加</text>
+          </view>
+        </view>
+
+        <!-- 烹饪方式 -->
+        <view class="card">
+          <text class="card-title">烹饪方式</text>
+          <view class="chip-group">
+            <view class="chip" v-for="(item, i) in presetCooking" :key="getChipKey('k', i)"
+              :class="{ 'chip-active': cookingMethods.indexOf(item) >= 0 }"
+              @tap="toggleChip('cookingMethods', item)">
+              <text>{{ item }}</text>
             </view>
           </view>
+          <view class="custom-row" v-if="customField === 'cookingMethods'">
+            <input class="custom-input" placeholder="输入其他做法" v-model="customInput"
+              confirm-type="done" @confirm="addCustom" />
+            <view class="custom-confirm" @tap="addCustom"><text>添加</text></view>
+          </view>
+          <view class="custom-add" v-else @tap="openCustom('cookingMethods')">
+            <text>+ 自定义添加</text>
+          </view>
         </view>
       </view>
+      <!-- /Step 2 -->
 
-      <!-- 健康目标 -->
-      <view class="card">
-        <view class="card-accent"></view>
-        <view class="card-body">
-          <text class="card-title">健康目标</text>
-          <text class="hint-text" v-if="systemGoals.length > 0">系统推荐目标(基于健康报告):</text>
-          <view class="tag-group" v-if="systemGoals.length > 0">
-            <view class="tag-btn system-tag" v-for="(goal, index) in systemGoals" :key="index">
-              <text>{{ goal }}</text>
-            </view>
+      <!-- ===== Step 3 健康目标 ===== -->
+      <view v-if="currentStep === 3">
+        <view class="step-head">
+          <text class="step-icon">🎯</text>
+          <view class="step-head-text">
+            <text class="step-title">想优先改善哪方面?</text>
+            <text class="step-sub">没想好可以跳过,之后随时能改</text>
           </view>
-          <view class="form-group" style="margin-top: 20rpx;">
-            <text class="form-label">自定义目标</text>
-            <view class="tag-group">
-              <view class="tag-btn" v-for="(tag, index) in healthGoals" :key="index" @tap="removeTag('healthGoals', index)">
-                <text>{{ tag }}</text>
-                <text class="tag-remove">×</text>
-              </view>
-            </view>
-            <view class="tag-input-row">
-              <input class="tag-input" placeholder="减脂/增肌/控糖/肠道调理" v-model="tagInput" @confirm="addTag('healthGoals')" />
+        </view>
+
+        <view class="card">
+          <text class="card-title">必吃食物</text>
+          <view class="chip-group">
+            <view class="chip" v-for="(item, i) in presetMustEat" :key="getChipKey('m', i)"
+              :class="{ 'chip-active': mustEat.indexOf(item) >= 0 }"
+              @tap="toggleChip('mustEat', item)">
+              <text>{{ item }}</text>
             </view>
           </view>
-          <view class="confirm-btn" @tap="confirmGoals">
-            <text>确认目标</text>
+          <view class="custom-row" v-if="customField === 'mustEat'">
+            <input class="custom-input" placeholder="输入其他必吃" v-model="customInput"
+              confirm-type="done" @confirm="addCustom" />
+            <view class="custom-confirm" @tap="addCustom"><text>添加</text></view>
+          </view>
+          <view class="custom-add" v-else @tap="openCustom('mustEat')">
+            <text>+ 自定义添加</text>
           </view>
         </view>
-      </view>
 
-      <view class="submit-area">
-        <view class="submit-btn" @tap="savePreferences">
-          <text>{{ submitting ? '提交中...' : '保存偏好' }}</text>
+        <view class="card">
+          <text class="card-title">健康目标</text>
+          <view class="chip-group">
+            <view class="chip" v-for="(item, i) in presetGoals" :key="getChipKey('g', i)"
+              :class="{ 'chip-active': healthGoals.indexOf(item) >= 0 }"
+              @tap="toggleChip('healthGoals', item)">
+              <text>{{ item }}</text>
+            </view>
+          </view>
+          <view class="custom-row" v-if="customField === 'healthGoals'">
+            <input class="custom-input" placeholder="输入其他目标" v-model="customInput"
+              confirm-type="done" @confirm="addCustom" />
+            <view class="custom-confirm" @tap="addCustom"><text>添加</text></view>
+          </view>
+          <view class="custom-add" v-else @tap="openCustom('healthGoals')">
+            <text>+ 自定义添加</text>
+          </view>
         </view>
       </view>
+      <!-- /Step 3 -->
     </scroll-view>
+
+    <!-- 底部按钮栏 -->
+    <view class="footer-bar">
+      <view class="footer-btn footer-btn-ghost" v-if="currentStep > 1" @tap="goPrev">
+        <text>上一步</text>
+      </view>
+      <view class="footer-btn footer-btn-primary" @tap="goNext">
+        <text>{{ submitting ? '提交中...' : (isLastStep ? '完成' : '下一步') }}</text>
+      </view>
+    </view>
   </view>
 </template>
 
 <script>
 import { getDietPreferences, saveDietPreferences } from '@/utils/api.js'
 
+// 预设选项(常量,非响应式)
+const PRESET_ALLERGIES = ['海鲜', '花生', '乳糖', '鸡蛋', '牛奶', '豆类', '麸质', '坚果', '芒果', '其他']
+const PRESET_AVOID = ['牛肉', '羊肉', '猪肉', '内脏', '油腻', '辛辣', '生冷', '腌制']
+const PRESET_FLAVOR = ['酸', '甜', '咸', '鲜', '苦', '清淡', '浓郁']
+const PRESET_CUISINE = ['川', '粤', '鲁', '淮扬', '日料', '西餐', '东南亚', '韩餐']
+const PRESET_COOKING = ['蒸', '煮', '炒', '烤', '炖', '凉拌', '煎', '炸']
+const PRESET_MUSTEAT = ['蔬菜', '水果', '粗粮', '鱼类', '鸡蛋', '豆制品', '菌菇']
+const PRESET_GOALS = ['减脂', '增肌', '控糖', '肠道调理', '增强免疫']
+
 export default {
   data() {
     return {
       familyMemberId: null,
+      currentStep: 1,
+      // 数据字段(与后端 DTO 对齐)
       allergies: [],
       absoluteAvoid: [],
       religiousDiet: 'none',
       spiceLevel: 0,
       flavorPref: [],
       cuisinePref: [],
+      cookingMethods: [],
       healthGoals: [],
-      systemGoals: [],
-      tagInput: '',
-      submitting: false
+      mustEat: [],
+      goalConfirmed: 0,
+      // 自定义输入
+      customInput: '',
+      customField: null,
+      // UI 状态
+      submitting: false,
+      loading: true
     }
   },
-  onLoad() {
+  computed: {
+    // 预设选项映射(供模板 v-for)
+    presetAllergies: function() { return PRESET_ALLERGIES },
+    presetAvoid: function() { return PRESET_AVOID },
+    presetFlavor: function() { return PRESET_FLAVOR },
+    presetCuisine: function() { return PRESET_CUISINE },
+    presetCooking: function() { return PRESET_COOKING },
+    presetMustEat: function() { return PRESET_MUSTEAT },
+    presetGoals: function() { return PRESET_GOALS },
+    religiousOptions: function() {
+      return [
+        { value: 'none', label: '无限制' },
+        { value: 'halal', label: '清真' },
+        { value: 'vegetarian', label: '素食' },
+        { value: 'vegan', label: '纯素' }
+      ]
+    },
+    // 进度条百分比
+    progressPercent: function() {
+      var total = 3
+      var done = this.currentStep - 1
+      if (this.currentStep === 3) done = 3
+      return Math.round((done / total) * 100)
+    },
+    isLastStep: function() { return this.currentStep === 3 }
+  },
+  onLoad: function() {
     this.familyMemberId = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || uni.getStorageSync('userId')
     this.loadPreferences()
   },
   methods: {
-    goBack() {
-      uni.navigateBack()
-    },
-    loadPreferences() {
+    loadPreferences: function() {
       var self = this
       getDietPreferences({ memberId: self.familyMemberId }).then(function(res) {
+        self.loading = false
         if (res) {
           self.allergies = res.allergies || []
           self.absoluteAvoid = res.absoluteAvoid || []
@@ -172,30 +298,63 @@ export default {
           self.spiceLevel = res.spiceLevel || 0
           self.flavorPref = res.flavorPref || []
           self.cuisinePref = res.cuisinePref || []
+          self.cookingMethods = res.cookingMethods || []
           self.healthGoals = res.healthGoals || []
-          self.systemGoals = res.systemGoals || []
+          self.mustEat = res.mustEat || []
+          self.goalConfirmed = res.goalConfirmed || 0
         }
+      }).catch(function() {
+        self.loading = false
       })
     },
-    addTag(field) {
-      var value = this.tagInput.trim()
-      if (value) {
+    // ——— chip 选中态(模板用内联 indexOf,方法供 toggle 用) ———
+    getChipKey: function(prefix, index) {
+      return prefix + '-' + index
+    },
+    toggleChip: function(field, value) {
+      var idx = this[field].indexOf(value)
+      if (idx >= 0) {
+        this[field].splice(idx, 1)
+      } else {
         this[field].push(value)
-        this.tagInput = ''
       }
     },
-    removeTag(field, index) {
-      this[field].splice(index, 1)
+    // ——— 自定义输入追加(@confirm 触发) ———
+    openCustom: function(field) {
+      this.customField = field
+      this.customInput = ''
     },
-    confirmGoals() {
-      this.savePreferences()
+    addCustom: function() {
+      var value = (this.customInput || '').trim()
+      if (!value) return
+      if (this[this.customField].indexOf(value) < 0) {
+        this[this.customField].push(value)
+      }
+      this.customInput = ''
+      this.customField = null
     },
-    savePreferences() {
-      var self = this
-      if (this.allergies.length === 0 && this.absoluteAvoid.length === 0) {
-        uni.showToast({ title: '请填写过敏原或忌口', icon: 'none' })
-        return
+    // ——— 步骤导航 ———
+    goNext: function() {
+      if (this.currentStep === 1) {
+        if (this.allergies.length === 0 && this.absoluteAvoid.length === 0) {
+          uni.showToast({ title: '请至少填写一项过敏原或忌口', icon: 'none' })
+          return
+        }
+      }
+      if (this.currentStep < 3) {
+        this.currentStep += 1
+      } else {
+        this.savePreferences()
       }
+    },
+    goPrev: function() {
+      if (this.currentStep > 1) {
+        this.currentStep -= 1
+      }
+    },
+    // ——— 提交 ———
+    savePreferences: function() {
+      var self = this
       this.submitting = true
       saveDietPreferences({
         allergies: this.allergies,
@@ -204,7 +363,9 @@ export default {
         spiceLevel: this.spiceLevel,
         flavorPref: this.flavorPref,
         cuisinePref: this.cuisinePref,
-        healthGoals: this.healthGoals.concat(this.systemGoals),
+        cookingMethods: this.cookingMethods,
+        healthGoals: this.healthGoals,
+        mustEat: this.mustEat,
         goalConfirmed: 1
       }).then(function(res) {
         self.submitting = false
@@ -224,144 +385,83 @@ export default {
 <style scoped>
 .preferences-page {
   min-height: 100vh;
-  background-color: #F5F7FA;
+  background: linear-gradient(180deg, #FFF7ED 0%, #FFF3E6 100%);
   display: flex;
   flex-direction: column;
 }
-.nav-placeholder {
-  width: 80rpx;
+
+/* ===== 进度条 ===== */
+.progress-bar { padding: 30rpx 32rpx 10rpx; }
+.progress-track {
+  height: 12rpx; background: #FFE3C7; border-radius: 6rpx; overflow: hidden;
 }
-.content {
-  flex: 1;
-  padding: 30rpx;
+.progress-fill {
+  height: 100%; background: linear-gradient(90deg, #FF8C42, #FFB366);
+  border-radius: 6rpx; transition: width 0.3s ease;
 }
+.progress-labels { display: flex; justify-content: space-between; margin-top: 12rpx; }
+.progress-label { font-size: 22rpx; color: #C2A78A; }
+.progress-label-active { color: #FF8C42; font-weight: 600; }
+
+/* ===== 内容 ===== */
+.content { flex: 1; padding: 10rpx 32rpx 30rpx; box-sizing: border-box; }
+.step-head { display: flex; align-items: center; margin: 20rpx 0 24rpx; }
+.step-icon { font-size: 52rpx; margin-right: 20rpx; }
+.step-head-text { display: flex; flex-direction: column; }
+.step-title { font-size: 34rpx; font-weight: 700; color: #4A2E1A; }
+.step-sub { font-size: 24rpx; color: #A08B72; margin-top: 6rpx; }
+
+/* ===== 卡片 ===== */
 .card {
-  display: flex;
-  background-color: #FFFFFF;
-  border-radius: 20rpx;
-  margin-bottom: 30rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
-  overflow: hidden;
+  background: #FFFFFF; border-radius: 24rpx; padding: 28rpx;
+  margin-bottom: 24rpx; box-shadow: 0 4rpx 20rpx rgba(255, 140, 66, 0.08);
 }
-.card-accent {
-  width: 8rpx;
-  background-color: #FF8C42;
-  flex-shrink: 0;
-}
-.card-body {
-  flex: 1;
-  padding: 30rpx;
-}
-.card-title {
-  font-size: 30rpx;
-  font-weight: 600;
-  color: #333;
-  margin-bottom: 24rpx;
-  display: block;
-}
-.form-group {
-  margin-bottom: 24rpx;
-}
-.form-label {
-  font-size: 26rpx;
-  color: #333;
-  font-weight: 500;
-  margin-bottom: 12rpx;
-  display: block;
-}
-.hint-text {
-  font-size: 24rpx;
-  color: #999;
-  margin-bottom: 12rpx;
-  display: block;
-}
-.tag-group {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 16rpx;
-  margin-bottom: 12rpx;
-}
-.tag-btn {
-  padding: 12rpx 20rpx;
-  border: 1rpx solid #ddd;
-  border-radius: 30rpx;
-  font-size: 24rpx;
-  color: #666;
-  background-color: #FAFAFA;
-  display: flex;
-  align-items: center;
-  gap: 8rpx;
-}
-.tag-btn.system-tag {
-  background-color: #FFF5EB;
-  border-color: #FF8C42;
-  color: #FF8C42;
-}
-.tag-remove {
-  font-size: 28rpx;
-  color: #999;
-  line-height: 1;
-}
-.tag-input-row {
-  margin-top: 12rpx;
-}
-.tag-input {
-  border: 1rpx solid #ddd;
-  border-radius: 30rpx;
-  padding: 12rpx 20rpx;
-  font-size: 26rpx;
-  background-color: #FAFAFA;
-}
-.radio-group {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 16rpx;
+.card-title { font-size: 28rpx; font-weight: 600; color: #4A2E1A; margin-bottom: 20rpx; display: block; }
+
+/* ===== chip ===== */
+.chip-group { display: flex; flex-wrap: wrap; }
+.chip {
+  padding: 14rpx 28rpx; border-radius: 999rpx; background: #F5F0EA;
+  font-size: 26rpx; color: #6B5340; margin: 0 16rpx 16rpx 0;
+  border: 2rpx solid transparent;
 }
-.radio-item {
-  padding: 12rpx 24rpx;
-  border: 1rpx solid #ddd;
-  border-radius: 30rpx;
-  font-size: 24rpx;
-  color: #666;
-  background-color: #FAFAFA;
+.chip-active {
+  background: #FF8C42; color: #FFFFFF; font-weight: 600;
+  border-color: #E8732A;
 }
-.radio-active {
-  border-color: #FF8C42;
-  color: #FF8C42;
-  background-color: #FFF5EB;
+
+/* ===== 自定义添加 ===== */
+.custom-add {
+  padding: 14rpx 0; font-size: 26rpx; color: #FF8C42; font-weight: 500;
 }
-.slider-row {
-  display: flex;
-  align-items: center;
-  gap: 20rpx;
+.custom-row { display: flex; align-items: center; margin-top: 8rpx; }
+.custom-input {
+  flex: 1; border: 2rpx solid #FFD9B8; border-radius: 999rpx;
+  padding: 14rpx 24rpx; font-size: 26rpx; background: #FFF9F2;
 }
-.slider-label {
-  font-size: 24rpx;
-  color: #666;
-  width: 60rpx;
+.custom-confirm {
+  background: #FF8C42; color: #fff; font-size: 26rpx; font-weight: 600;
+  padding: 14rpx 32rpx; border-radius: 999rpx; margin-left: 16rpx;
 }
-.slider {
-  flex: 1;
+
+/* ===== 滑块 ===== */
+.slider-row { display: flex; align-items: center; }
+.slider-label { font-size: 24rpx; color: #A08B72; width: 72rpx; }
+.slider { flex: 1; }
+
+/* ===== 底栏 ===== */
+.footer-bar {
+  display: flex; padding: 20rpx 32rpx 30rpx; background: #FFFDF8;
+  border-top: 2rpx solid #FFE9D4;
 }
-.confirm-btn {
-  background-color: #E8F5E9;
-  color: #4CAF50;
-  text-align: center;
-  padding: 20rpx 0;
-  border-radius: 12rpx;
-  font-size: 26rpx;
-  margin-top: 20rpx;
+.footer-btn {
+  flex: 1; text-align: center; padding: 24rpx 0; border-radius: 999rpx;
+  font-size: 30rpx; font-weight: 600;
 }
-.submit-area {
-  padding: 40rpx 30rpx;
+.footer-btn-ghost {
+  background: #F5F0EA; color: #6B5340; margin-right: 20rpx;
 }
-.submit-btn {
-  background-color: #FF8C42;
-  color: #FFFFFF;
-  text-align: center;
-  padding: 28rpx 0;
-  border-radius: 16rpx;
-  font-size: 30rpx;
-  font-weight: 600;
+.footer-btn-primary {
+  background: linear-gradient(135deg, #FF8C42, #FFB366); color: #FFFFFF;
 }
 </style>

+ 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>