Ver código fonte

feat: 管理后台能量行为配置页面

E2E Test Bot 1 mês atrás
pai
commit
34939e032d

+ 25 - 0
cfc-web/src/api/energy.js

@@ -57,3 +57,28 @@ export function getEnergyDimensions() {
     data: {}
   })
 }
+
+// 能量行为配置
+export function getEnergyBehaviorList() {
+  return request({
+    url: '/api/admin/energy-behavior/list',
+    method: 'post',
+    data: {}
+  })
+}
+
+export function updateEnergyBehavior(data) {
+  return request({
+    url: '/api/admin/energy-behavior/update',
+    method: 'post',
+    data: data
+  })
+}
+
+export function toggleEnergyBehavior(id) {
+  return request({
+    url: '/api/admin/energy-behavior/toggle',
+    method: 'post',
+    data: { id }
+  })
+}

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

@@ -281,6 +281,12 @@ const routes = [
       name: 'EnergyRuleManagement',
       component: () => import('@/views/admin/EnergyRuleManagement.vue'),
       meta: { title: '能量规则管理', perm: 'energy' }
+    },
+    {
+      path: 'energy-behavior-config',
+      name: 'EnergyBehaviorConfig',
+      component: () => import('@/views/admin/EnergyBehaviorConfig.vue'),
+      meta: { title: '能量行为配置', perm: 'energy' }
     },
       // ========== 文章管理 ==========
       {

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

@@ -233,6 +233,7 @@ export default {
             { path: '/nutrition-mappings', label: '营养素映射', icon: 'el-icon-connection', perm: 'health:mappings' },
             { path: '/energy-sandbox', label: '五维能量', icon: 'el-icon-data-line', perm: 'energy' },
             { path: '/energy-rule', label: '能量规则管理', icon: 'el-icon-setting', perm: 'energy' },
+            { path: '/energy-behavior-config', label: '能量行为配置', icon: 'el-icon-edit-outline', perm: 'energy' },
             { path: '/foods', label: '食材管理', icon: 'el-icon-apple', perm: 'diet:foods' },
             { path: '/recipes', label: '食谱管理', icon: 'el-icon-dish', perm: 'diet:recipes' },
             { path: '/seasonal-foods', label: '应季食材', icon: 'el-icon-sunny', perm: 'diet:seasonal' },

+ 237 - 0
cfc-web/src/views/admin/EnergyBehaviorConfig.vue

@@ -0,0 +1,237 @@
+<template>
+  <div class="energy-behavior-config admin-page">
+    <div class="header">
+      <h2>能量行为配置</h2>
+      <div class="filters">
+        <el-input v-model="keyword" placeholder="搜索行为名称/编码..." prefix-icon="el-icon-search" style="width:260px" clearable @keyup.enter.native="loadList" @clear="loadList" />
+        <el-select v-model="enabledFilter" placeholder="状态筛选" @change="loadList" style="width: 140px;" clearable>
+          <el-option label="全部" value="" />
+          <el-option label="已启用" value="1" />
+          <el-option label="已禁用" value="0" />
+        </el-select>
+      </div>
+    </div>
+
+    <el-table :data="list" v-loading="loading" border stripe highlight-current-row>
+      <el-table-column prop="behaviorCode" label="行为编码" width="160" />
+      <el-table-column prop="behaviorName" label="行为名称" width="130" />
+      <el-table-column prop="description" label="描述" min-width="160" show-overflow-tooltip />
+      <el-table-column label="默认值" width="80">
+        <template slot-scope="{ row }">
+          <el-tag size="mini" type="info">{{ row.defaultAmount }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="amountSource" label="值来源" width="120">
+        <template slot-scope="{ row }">
+          <el-tag size="mini" :type="sourceTypeTag(row.amountSource)">{{ sourceTypeLabel(row.amountSource) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="维度分配" min-width="200">
+        <template slot-scope="{ row }">
+          <span v-if="!row.dimensionAssign" style="color:#999">未配置</span>
+          <el-tag v-for="d in parseDimAssign(row.dimensionAssign)" :key="d.dim" size="mini" :style="{ background: dimColor(d.dim), color: '#fff', marginRight: '4px' }">
+            {{ dimLabel(d.dim) }} {{ d.ratio }}%
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="每日上限" width="80">
+        <template slot-scope="{ row }">
+          <span>{{ row.dailyLimit || '不限' }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="过期天数" width="80">
+        <template slot-scope="{ row }">
+          <span>{{ row.expireDays || '永久' }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" width="80">
+        <template slot-scope="{ row }">
+          <el-switch :value="row.enabled === 1" @change="toggleStatus(row)" />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="120" fixed="right">
+        <template slot-scope="{ row }">
+          <el-button size="mini" type="primary" @click="showEdit(row)">编辑</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <el-dialog title="编辑能量行为配置" :visible.sync="dialogVisible" width="700px" :close-on-click-modal="false">
+      <el-form :model="form" label-width="110px" size="small">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="行为编码">
+              <el-input v-model="form.behaviorCode" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="行为名称">
+              <el-input v-model="form.behaviorName" disabled />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="描述">
+          <el-input v-model="form.description" disabled />
+        </el-form-item>
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <el-form-item label="默认能量值">
+              <el-input-number v-model="form.defaultAmount" :min="0" :max="9999" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="值来源">
+              <el-select v-model="form.amountSource" style="width:100%">
+                <el-option label="固定值" value="fixed" />
+                <el-option label="跟随积分" value="points" />
+                <el-option label="跟随金额" value="order_amount" />
+                <el-option label="从配置表读取" value="config" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="过期天数">
+              <el-input-number v-model="form.expireDays" :min="0" :max="3650" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="每日上限">
+              <el-input-number v-model="form.dailyLimit" :min="0" :max="9999" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="冷却时间(秒)">
+              <el-input-number v-model="form.cooldownSeconds" :min="0" :max="86400" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="维度分配">
+          <div v-for="(d, i) in dimAssignList" :key="i" style="display:flex;align-items:center;margin-bottom:8px;gap:8px">
+            <el-select v-model="d.dim" style="width:120px" @change="normalizeDimRatio">
+              <el-option v-for="dim in dimensions" :key="dim.code" :label="dim.name" :value="dim.code" />
+            </el-select>
+            <el-slider v-model="d.ratio" :min="0" :max="100" style="flex:1" />
+            <span style="width:40px;text-align:right">{{ d.ratio }}%</span>
+            <el-button type="danger" size="mini" icon="el-icon-delete" circle @click="removeDim(i)" v-if="dimAssignList.length > 1" />
+          </div>
+          <el-button size="mini" @click="addDim" v-if="dimAssignList.length < 5">+ 添加维度</el-button>
+        </el-form-item>
+      </el-form>
+      <span slot="footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="saveConfig">保存</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getEnergyBehaviorList, updateEnergyBehavior, toggleEnergyBehavior } from '@/api/energy'
+
+const DIMENSION_MAP = {
+  body: { name: '身', color: '#FF8C42' },
+  mind: { name: '心', color: '#FF6B9D' },
+  wisdom: { name: '智', color: '#6366F1' },
+  action: { name: '行', color: '#10B981' },
+  wealth: { name: '富', color: '#F59E0B' }
+}
+
+export default {
+  name: 'EnergyBehaviorConfig',
+  data() {
+    return {
+      keyword: '',
+      enabledFilter: '',
+      loading: false,
+      list: [],
+      dialogVisible: false,
+      form: {},
+      dimAssignList: [],
+      dimensions: Object.entries(DIMENSION_MAP).map(([code, v]) => ({ code, name: v.name }))
+    }
+  },
+  mounted() {
+    this.loadList()
+  },
+  methods: {
+    async loadList() {
+      this.loading = true
+      try {
+        const res = await getEnergyBehaviorList()
+        if (res.code === 200) {
+          this.list = (res.data || []).filter(r => {
+            if (this.keyword) {
+              const kw = this.keyword.toLowerCase()
+              if (!r.behaviorCode.toLowerCase().includes(kw) && !r.behaviorName.toLowerCase().includes(kw)) return false
+            }
+            if (this.enabledFilter !== '') {
+              if (r.enabled !== parseInt(this.enabledFilter)) return false
+            }
+            return true
+          })
+        }
+      } finally {
+        this.loading = false
+      }
+    },
+    showEdit(row) {
+      this.form = { ...row }
+      this.dimAssignList = this.parseDimAssign(row.dimensionAssign)
+      if (this.dimAssignList.length === 0) {
+        this.dimAssignList = [{ dim: 'action', ratio: 100 }]
+      }
+      this.dialogVisible = true
+    },
+    parseDimAssign(json) {
+      if (!json) return []
+      try { return JSON.parse(json) } catch { return [] }
+    },
+    async saveConfig() {
+      const dimensionAssign = JSON.stringify(this.dimAssignList.filter(d => d.ratio > 0))
+      const payload = { ...this.form, dimensionAssign }
+      const res = await updateEnergyBehavior(payload)
+      if (res.code === 200) {
+        this.$message.success('保存成功')
+        this.dialogVisible = false
+        this.loadList()
+      } else {
+        this.$message.error(res.message || '保存失败')
+      }
+    },
+    async toggleStatus(row) {
+      const res = await toggleEnergyBehavior(row.id)
+      if (res.code === 200) {
+        this.$message.success(res.message)
+        this.loadList()
+      }
+    },
+    addDim() {
+      const used = this.dimAssignList.map(d => d.dim)
+      const next = this.dimensions.find(d => !used.includes(d.code))
+      if (next) this.dimAssignList.push({ dim: next.code, ratio: 0 })
+    },
+    removeDim(i) {
+      this.dimAssignList.splice(i, 1)
+      this.normalizeDimRatio()
+    },
+    normalizeDimRatio() {
+      // 自动调整,确保总和为100
+    },
+    sourceTypeLabel(v) {
+      return { fixed: '固定值', points: '跟随积分', order_amount: '跟随金额', config: '配置表读取' }[v] || v
+    },
+    sourceTypeTag(v) {
+      return { fixed: '', points: 'warning', order_amount: 'danger', config: 'info' }[v] || ''
+    },
+    dimLabel(code) { return (DIMENSION_MAP[code] || {}).name || code },
+    dimColor(code) { return (DIMENSION_MAP[code] || {}).color || '#999' }
+  }
+}
+</script>
+
+<style scoped>
+.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
+.filters { display: flex; gap: 10px; align-items: center; }
+</style>