Procházet zdrojové kódy

feat(admin): 通关解锁管理端 - 关卡配置 CRUD/排序/启停/家庭进度管理

E2E Test Bot před 1 měsícem
rodič
revize
64a5626b39

+ 57 - 0
cfc-web/src/api/admin.js

@@ -817,6 +817,63 @@ export function deleteSysConfig(id) {
   })
   })
 }
 }
 
 
+// ========== 通关解锁关卡配置 API ==========
+
+export function getUnlockGateList() {
+  return request({
+    url: '/api/admin/unlock-gates/list',
+    method: 'post'
+  })
+}
+
+export function saveUnlockGate(data) {
+  return request({
+    url: '/api/admin/unlock-gates/save',
+    method: 'post',
+    data
+  })
+}
+
+export function deleteUnlockGate(id) {
+  return request({
+    url: '/api/admin/unlock-gates/delete',
+    method: 'post',
+    data: { id }
+  })
+}
+
+export function sortUnlockGates(sortList) {
+  return request({
+    url: '/api/admin/unlock-gates/sort',
+    method: 'post',
+    data: sortList
+  })
+}
+
+export function toggleUnlockGate(id, enabled) {
+  return request({
+    url: '/api/admin/unlock-gates/toggle',
+    method: 'post',
+    data: { id, enabled }
+  })
+}
+
+export function resetFamilyGates(familyId) {
+  return request({
+    url: '/api/admin/unlock-gates/reset-family',
+    method: 'post',
+    data: { familyId }
+  })
+}
+
+export function getFamilyGateProgress(familyId) {
+  return request({
+    url: '/api/admin/unlock-gates/family-progress',
+    method: 'post',
+    data: { familyId }
+  })
+}
+
 // ========== 佣金推荐系统 API ==========
 // ========== 佣金推荐系统 API ==========
 
 
 export function getWithdrawalList(params) {
 export function getWithdrawalList(params) {

+ 6 - 0
cfc-web/src/router/index.js

@@ -367,6 +367,12 @@ const routes = [
         component: () => import('@/views/admin/VirtualGoodsConfig.vue'),
         component: () => import('@/views/admin/VirtualGoodsConfig.vue'),
         meta: { title: '虚拟支付道具', perm: 'system:config' }
         meta: { title: '虚拟支付道具', perm: 'system:config' }
       },
       },
+      {
+        path: 'gates',
+        name: 'Gates',
+        component: () => import('@/views/admin/gates.vue'),
+        meta: { title: '关卡配置', perm: 'system:config' }
+      },
       // ========== 佣金推荐系统 ==========
       // ========== 佣金推荐系统 ==========
       {
       {
         path: 'product-profit-rate',
         path: 'product-profit-rate',

+ 1 - 0
cfc-web/src/views/Layout.vue

@@ -286,6 +286,7 @@ export default {
                 { path: '/membership-center', label: '会员中心', icon: 'el-icon-s-custom', perm: 'system:config' },
                 { path: '/membership-center', label: '会员中心', icon: 'el-icon-s-custom', perm: 'system:config' },
                 { path: '/badge-manage', label: '勋章管理', icon: 'el-icon-medal', perm: 'system:config' },
                 { path: '/badge-manage', label: '勋章管理', icon: 'el-icon-medal', perm: 'system:config' },
                 { path: '/virtual-goods-config', label: '虚拟支付道具', icon: 'el-icon-goods', perm: 'system:config' },
                 { path: '/virtual-goods-config', label: '虚拟支付道具', icon: 'el-icon-goods', perm: 'system:config' },
+                { path: '/gates', label: '关卡配置', icon: 'el-icon-key', perm: 'system:config' },
               ]},
               ]},
             // --- 配置中心 ---
             // --- 配置中心 ---
             { title: '配置中心', icon: 'el-icon-setting', perm: 'system:config',
             { title: '配置中心', icon: 'el-icon-setting', perm: 'system:config',

+ 403 - 0
cfc-web/src/views/admin/gates.vue

@@ -0,0 +1,403 @@
+<template>
+  <div class="gates-page">
+    <el-card>
+      <div slot="header">
+        <span>关卡配置</span>
+        <el-button type="primary" size="small" style="float:right" @click="handleAdd">新增关卡</el-button>
+      </div>
+
+      <el-table :data="gateList" border v-loading="loading" style="width:100%">
+        <el-table-column prop="sortOrder" label="排序" width="60" align="center">
+          <template slot-scope="scope">
+            <el-button size="mini" @click="moveUp(scope.$index)" :disabled="scope.$index === 0">▲</el-button>
+            <el-button size="mini" @click="moveDown(scope.$index)" :disabled="scope.$index === gateList.length-1">▼</el-button>
+          </template>
+        </el-table-column>
+        <el-table-column prop="icon" label="图标" width="60" align="center">
+          <template slot-scope="scope">{{ scope.row.icon || '—' }}</template>
+        </el-table-column>
+        <el-table-column prop="name" label="名称" width="140"></el-table-column>
+        <el-table-column prop="gateType" label="类型" width="140">
+          <template slot-scope="scope">{{ gateTypeLabel(scope.row.gateType) }}</template>
+        </el-table-column>
+        <el-table-column label="归属" width="80">
+          <template slot-scope="scope">
+            <el-tag :type="scope.row.isRequired === 1 ? 'primary' : 'warning'" size="small">
+              {{ scope.row.isRequired === 1 ? '主线' : '增值' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="形态" width="80">
+          <template slot-scope="scope">
+            <el-tag :type="scope.row.isSoft === 1 ? 'info' : 'danger'" size="small">
+              {{ scope.row.isSoft === 1 ? '软引导' : '硬门禁' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="unlockTarget" label="解锁目标" width="150">
+          <template slot-scope="scope">{{ unlockTargetLabel(scope.row.unlockTarget) }}</template>
+        </el-table-column>
+        <el-table-column prop="conditionParams" label="条件" width="120">
+          <template slot-scope="scope">{{ scope.row.conditionParams || '—' }}</template>
+        </el-table-column>
+        <el-table-column label="奖励" width="140">
+          <template slot-scope="scope">
+            <span v-if="scope.row.rewardPoints > 0" style="color:#B8860B">+{{ scope.row.rewardPoints }}积分</span>
+            <span v-if="scope.row.rewardEnergy > 0" style="color:#3A7CC2;margin-left:6px">+{{ scope.row.rewardEnergy }}能量</span>
+            <span v-if="!scope.row.rewardPoints && !scope.row.rewardEnergy">—</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" width="80">
+          <template slot-scope="scope">
+            <el-switch v-model="scope.row._enabled" @change="handleToggle(scope.row)"></el-switch>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="120" fixed="right">
+          <template slot-scope="scope">
+            <el-button size="mini" type="primary" @click="handleEdit(scope.row)">编辑</el-button>
+            <el-button size="mini" type="danger" @click="handleDelete(scope.row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+
+    <!-- 家庭进度管理 -->
+    <el-card style="margin-top:20px">
+      <div slot="header">
+        <span>家庭进度管理</span>
+      </div>
+      <el-form :inline="true">
+        <el-form-item label="家庭ID">
+          <el-input v-model="familyProgressSearch.familyId" placeholder="输入家庭ID" clearable></el-input>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleFamilyProgress">查看进度</el-button>
+          <el-button type="danger" @click="handleResetFamily">重置进度</el-button>
+        </el-form-item>
+      </el-form>
+      <el-table v-if="familyProgressList.length > 0" :data="familyProgressList" border size="small">
+        <el-table-column prop="gateId" label="关卡ID" width="80"></el-table-column>
+        <el-table-column prop="status" label="状态" width="100">
+          <template slot-scope="scope">
+            <el-tag :type="scope.row.status === 'UNLOCKED' ? 'success' : 'info'" size="small">
+              {{ scope.row.status === 'UNLOCKED' ? '已解锁' : '锁定' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="unlockedBy" label="解锁人" width="80"></el-table-column>
+        <el-table-column prop="unlockedAt" label="解锁时间" width="180"></el-table-column>
+      </el-table>
+    </el-card>
+
+    <!-- 新增/编辑弹窗 -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="600px">
+      <el-form :model="form" :rules="formRules" ref="form" label-width="100px">
+        <el-form-item label="关卡类型" prop="gateType">
+          <el-select v-model="form.gateType" style="width:100%">
+            <el-option label="五维自检" value="SELF_CHECK"></el-option>
+            <el-option label="邀请家人" value="INVITE_FAMILY"></el-option>
+            <el-option label="创建家人" value="CREATE_FAMILY_MEMBER"></el-option>
+            <el-option label="购买商品" value="PURCHASE"></el-option>
+            <el-option label="加入会员" value="MEMBERSHIP"></el-option>
+            <el-option label="上传报告" value="REPORT_UPLOAD"></el-option>
+            <el-option label="微行动次数" value="MICRO_ACTION"></el-option>
+            <el-option label="连续打卡" value="STREAK_DAYS"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="关卡名称" prop="name">
+          <el-input v-model="form.name" placeholder="如:五维自检"></el-input>
+        </el-form-item>
+        <el-form-item label="说明">
+          <el-input v-model="form.description" type="textarea" :rows="2" placeholder="关卡说明文字"></el-input>
+        </el-form-item>
+        <el-form-item label="图标">
+          <el-input v-model="form.icon" placeholder="如:📝" style="width:120px"></el-input>
+        </el-form-item>
+        <el-form-item label="排序号">
+          <el-input-number v-model="form.sortOrder" :min="0" :max="999"></el-input-number>
+        </el-form-item>
+        <el-form-item label="归属">
+          <el-radio-group v-model="form.isRequired">
+            <el-radio :label="1">主线</el-radio>
+            <el-radio :label="0">增值</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="形态">
+          <el-radio-group v-model="form.isSoft">
+            <el-radio :label="0">硬门禁(锁定功能)</el-radio>
+            <el-radio :label="1">软引导(推荐不锁)</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="解锁目标">
+          <el-select v-model="form.unlockTarget" style="width:100%">
+            <el-option label="个人沙盘" value="PERSONAL_SANDBOX"></el-option>
+            <el-option label="全家沙盘" value="FAMILY_SANDBOX"></el-option>
+            <el-option label="孩子微行动" value="CHILD_MICRO_ACTION"></el-option>
+            <el-option label="每日微行动推荐" value="MICRO_ACTION_DAILY"></el-option>
+            <el-option label="连续打卡里程碑" value="BADGE_MILESTONE"></el-option>
+            <el-option label="趋势对比" value="TREND_COMPARE"></el-option>
+            <el-option label="增值内容" value="PREMIUM_MODULES"></el-option>
+            <el-option label="健康洞察" value="HEALTH_INSIGHTS"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="条件参数">
+          <el-input v-model="form.conditionParams" placeholder='如:{"count":1} 或 {"days":3}'></el-input>
+          <div style="font-size:12px;color:#999;margin-top:4px">
+            SELF_CHECK: {"minCount":1} | MICRO_ACTION: {"count":1} | STREAK_DAYS: {"days":3}
+          </div>
+        </el-form-item>
+        <el-form-item label="积分奖励">
+          <el-input-number v-model="form.rewardPoints" :min="0" :max="10000"></el-input-number>
+        </el-form-item>
+        <el-form-item label="能量奖励">
+          <el-input-number v-model="form.rewardEnergy" :min="0" :max="10000"></el-input-number>
+        </el-form-item>
+        <el-form-item label="启用">
+          <el-switch v-model="form._enabled"></el-switch>
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submitForm" :loading="submitting">确定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getUnlockGateList, saveUnlockGate, deleteUnlockGate, sortUnlockGates, toggleUnlockGate, resetFamilyGates, getFamilyGateProgress } from '@/api/admin'
+
+const GATE_TYPE_LABELS = {
+  SELF_CHECK: '五维自检',
+  INVITE_FAMILY: '邀请家人',
+  CREATE_FAMILY_MEMBER: '创建家人',
+  PURCHASE: '购买商品',
+  MEMBERSHIP: '加入会员',
+  REPORT_UPLOAD: '上传报告',
+  MICRO_ACTION: '微行动次数',
+  STREAK_DAYS: '连续打卡'
+}
+
+const UNLOCK_TARGET_LABELS = {
+  PERSONAL_SANDBOX: '个人沙盘',
+  FAMILY_SANDBOX: '全家沙盘',
+  CHILD_MICRO_ACTION: '孩子微行动',
+  MICRO_ACTION_DAILY: '每日微行动',
+  BADGE_MILESTONE: '打卡里程碑',
+  TREND_COMPARE: '趋势对比',
+  PREMIUM_MODULES: '增值内容',
+  HEALTH_INSIGHTS: '健康洞察'
+}
+
+export default {
+  data() {
+    return {
+      loading: false,
+      submitting: false,
+      gateList: [],
+      dialogVisible: false,
+      dialogTitle: '新增关卡',
+      form: this.getEmptyForm(),
+      formRules: {
+        gateType: [{ required: true, message: '请选择关卡类型', trigger: 'change' }],
+        name: [{ required: true, message: '请输入关卡名称', trigger: 'blur' }]
+      },
+      familyProgressSearch: {
+        familyId: ''
+      },
+      familyProgressList: []
+    }
+  },
+  mounted() {
+    this.loadGates()
+  },
+  methods: {
+    gateTypeLabel(type) {
+      return GATE_TYPE_LABELS[type] || type
+    },
+    unlockTargetLabel(target) {
+      return UNLOCK_TARGET_LABELS[target] || target
+    },
+    getEmptyForm() {
+      return {
+        id: null,
+        gateType: '',
+        name: '',
+        description: '',
+        icon: '',
+        sortOrder: 0,
+        enabled: 1,
+        _enabled: true,
+        isRequired: 1,
+        isSoft: 0,
+        unlockTarget: '',
+        conditionParams: '',
+        rewardPoints: 0,
+        rewardEnergy: 0
+      }
+    },
+    async loadGates() {
+      this.loading = true
+      try {
+        const res = await getUnlockGateList()
+        this.gateList = (res.data || []).map(function(g) {
+          g._enabled = g.enabled === 1
+          return g
+        })
+      } catch (e) {
+        this.$message.error('加载关卡列表失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    handleAdd() {
+      this.dialogTitle = '新增关卡'
+      this.form = this.getEmptyForm()
+      // 默认排序为当前最大+1
+      if (this.gateList.length > 0) {
+        var maxSort = Math.max.apply(null, this.gateList.map(function(g) { return g.sortOrder || 0 }))
+        this.form.sortOrder = maxSort + 1
+      }
+      this.dialogVisible = true
+    },
+    handleEdit(row) {
+      this.dialogTitle = '编辑关卡'
+      this.form = {
+        id: row.id,
+        gateType: row.gateType,
+        name: row.name,
+        description: row.description || '',
+        icon: row.icon || '',
+        sortOrder: row.sortOrder || 0,
+        _enabled: row.enabled === 1,
+        enabled: row.enabled,
+        isRequired: row.isRequired,
+        isSoft: row.isSoft,
+        unlockTarget: row.unlockTarget || '',
+        conditionParams: row.conditionParams || '',
+        rewardPoints: row.rewardPoints || 0,
+        rewardEnergy: row.rewardEnergy || 0
+      }
+      if (this.$refs.form) this.$refs.form.clearValidate()
+      this.dialogVisible = true
+    },
+    handleDelete(row) {
+      this.$confirm('确定要删除关卡「' + row.name + '」吗?', '提示', { type: 'warning' }).then(async function() {
+        try {
+          await deleteUnlockGate(row.id)
+          this.$message.success('删除成功')
+          this.loadGates()
+        } catch (e) {
+          this.$message.error('删除失败')
+        }
+      }.bind(this)).catch(function() {})
+    },
+    async handleToggle(row) {
+      try {
+        await toggleUnlockGate(row.id, row._enabled ? 1 : 0)
+        this.$message.success(row._enabled ? '已启用' : '已禁用')
+      } catch (e) {
+        row._enabled = !row._enabled
+        this.$message.error('操作失败')
+      }
+    },
+    async submitForm() {
+      this.$refs.form.validate(async function(valid) {
+        if (!valid) return
+        this.submitting = true
+        try {
+          var payload = {
+            id: this.form.id,
+            gateType: this.form.gateType,
+            name: this.form.name,
+            description: this.form.description,
+            icon: this.form.icon,
+            sortOrder: this.form.sortOrder,
+            enabled: this.form._enabled ? 1 : 0,
+            isRequired: this.form.isRequired,
+            isSoft: this.form.isSoft,
+            unlockTarget: this.form.unlockTarget,
+            conditionParams: this.form.conditionParams,
+            rewardPoints: this.form.rewardPoints,
+            rewardEnergy: this.form.rewardEnergy
+          }
+          if (!this.form.id) delete payload.id
+          await saveUnlockGate(payload)
+          this.$message.success('保存成功')
+          this.dialogVisible = false
+          this.loadGates()
+        } catch (e) {
+          this.$message.error('保存失败')
+        } finally {
+          this.submitting = false
+        }
+      }.bind(this))
+    },
+    async moveUp(index) {
+      if (index <= 0) return
+      var arr = this.gateList
+      var temp = arr[index].sortOrder
+      arr[index].sortOrder = arr[index - 1].sortOrder
+      arr[index - 1].sortOrder = temp
+      await this.saveSort()
+    },
+    async moveDown(index) {
+      var arr = this.gateList
+      if (index >= arr.length - 1) return
+      var temp = arr[index].sortOrder
+      arr[index].sortOrder = arr[index + 1].sortOrder
+      arr[index + 1].sortOrder = temp
+      await this.saveSort()
+    },
+    async saveSort() {
+      var sortList = this.gateList.map(function(g) {
+        return { id: g.id, sortOrder: g.sortOrder }
+      })
+      try {
+        await sortUnlockGates(sortList)
+        this.loadGates()
+      } catch (e) {
+        this.$message.error('排序保存失败')
+      }
+    },
+    async handleFamilyProgress() {
+      var familyId = this.familyProgressSearch.familyId
+      if (!familyId) {
+        this.$message.warning('请输入家庭ID')
+        return
+      }
+      try {
+        var res = await getFamilyGateProgress(familyId)
+        this.familyProgressList = res.data || []
+        if (this.familyProgressList.length === 0) {
+          this.$message.info('该家庭暂无关卡进度记录')
+        }
+      } catch (e) {
+        this.$message.error('查询失败')
+      }
+    },
+    async handleResetFamily() {
+      var familyId = this.familyProgressSearch.familyId
+      if (!familyId) {
+        this.$message.warning('请输入家庭ID')
+        return
+      }
+      this.$confirm('确定要重置家庭 ' + familyId + ' 的全部关卡进度吗?', '提示', { type: 'warning' }).then(async function() {
+        try {
+          await resetFamilyGates(familyId)
+          this.$message.success('重置成功')
+          this.familyProgressList = []
+        } catch (e) {
+          this.$message.error('重置失败')
+        }
+      }.bind(this)).catch(function() {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.gates-page {
+  padding: 20px;
+}
+</style>