Browse Source

feat: add web admin pages for article management, energy sandbox, withdraw audit, profit rate

New admin pages: ArticleManage, ArticleEdit, ArticleCategory, EnergySandbox, WithdrawAudit, ProductProfitRate
New API modules: article.js, energy.js
Updated: admin.js (new endpoints), router/index.js (new routes), Layout.vue (nav items)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
User 4 months ago
parent
commit
21ea165d65

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

@@ -480,3 +480,29 @@ export function deleteSysConfig(id) {
     method: 'delete'
     method: 'delete'
   })
   })
 }
 }
+
+// ========== 分佣裂变系统 API ==========
+
+export function getWithdrawalList(params) {
+  return request({
+    url: '/api/admin/commission/withdrawals',
+    method: 'post',
+    data: params
+  })
+}
+
+export function auditWithdrawal(requestId, status, remark) {
+  return request({
+    url: '/api/admin/commission/audit',
+    method: 'post',
+    data: { requestId, status, remark }
+  })
+}
+
+export function updateProductProfitRate(productId, profitRate) {
+  return request({
+    url: '/api/admin/commission/update-profit-rate',
+    method: 'post',
+    data: { productId, profitRate }
+  })
+}

+ 62 - 0
cfc-web/src/api/article.js

@@ -0,0 +1,62 @@
+import request from '@/utils/request'
+
+// ===== 文章管理 =====
+
+export function adminArticleList(data) {
+  return request({ url: '/api/admin/articles/list', method: 'post', data })
+}
+
+export function adminArticleCreate(data) {
+  return request({ url: '/api/admin/articles/create', method: 'post', data })
+}
+
+export function adminArticleUpdate(data) {
+  return request({ url: '/api/admin/articles/update', method: 'post', data })
+}
+
+export function adminArticleDelete(data) {
+  return request({ url: '/api/admin/articles/delete', method: 'post', data })
+}
+
+export function adminArticlePublish(data) {
+  return request({ url: '/api/admin/articles/publish', method: 'post', data })
+}
+
+export function adminArticleToggleFeatured(data) {
+  return request({ url: '/api/admin/articles/toggle-featured', method: 'post', data })
+}
+
+export function adminArticleUploadImage(file) {
+  const formData = new FormData()
+  formData.append('file', file)
+  return request({
+    url: '/api/admin/articles/upload/image',
+    method: 'post',
+    data: formData,
+    headers: { 'Content-Type': 'multipart/form-data' }
+  })
+}
+
+// ===== 文章分类管理 =====
+
+export function adminArticleCategoriesList() {
+  return request({ url: '/api/admin/articles/categories/list', method: 'post' })
+}
+
+export function adminArticleCategoryCreate(data) {
+  return request({ url: '/api/admin/articles/categories/create', method: 'post', data })
+}
+
+export function adminArticleCategoryUpdate(data) {
+  return request({ url: '/api/admin/articles/categories/update', method: 'post', data })
+}
+
+export function adminArticleCategoryDelete(data) {
+  return request({ url: '/api/admin/articles/categories/delete', method: 'post', data })
+}
+
+// ===== 文章详情 =====
+
+export function adminArticleDetail(data) {
+  return request({ url: '/api/admin/articles/detail', method: 'post', data })
+}

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

@@ -0,0 +1,10 @@
+import request from '@/utils/request'
+
+// 五维能量沙盘
+export function getFamilyEnergySandbox() {
+  return request({
+    url: '/api/energy/sandbox',
+    method: 'post',
+    data: {}
+  })
+}

+ 44 - 5
cfc-web/src/router/index.js

@@ -192,10 +192,48 @@ const routes = [
         meta: { title: '订单管理' }
         meta: { title: '订单管理' }
       },
       },
       {
       {
-        path: 'sys-config',
-        name: 'SysConfig',
-        component: () => import('@/views/admin/SysConfig.vue'),
-        meta: { title: '系统配置' }
+      path: 'sys-config',
+      name: 'SysConfig',
+      component: () => import('@/views/admin/SysConfig.vue'),
+      meta: { title: '系统配置' }
+    },
+    {
+      path: 'energy-sandbox',
+      name: 'EnergySandbox',
+      component: () => import('@/views/admin/EnergySandbox.vue'),
+      meta: { title: '五维能量' }
+    },
+      // ========== 文章管理 ==========
+      {
+        path: 'article-categories',
+        name: 'ArticleCategory',
+        component: () => import('@/views/admin/ArticleCategory.vue'),
+        meta: { title: '文章分类' }
+      },
+      {
+        path: 'article-manage',
+        name: 'ArticleManage',
+        component: () => import('@/views/admin/ArticleManage.vue'),
+        meta: { title: '文章管理' }
+      },
+      {
+        path: 'article-edit',
+        name: 'ArticleEdit',
+        component: () => import('@/views/admin/ArticleEdit.vue'),
+        meta: { title: '文章编辑' }
+      },
+      // ========== 分佣裂变系统 ==========
+      {
+        path: 'product-profit-rate',
+        name: 'ProductProfitRate',
+        component: () => import('@/views/admin/ProductProfitRate.vue'),
+        meta: { title: '产品利润率' }
+      },
+      {
+        path: 'withdraw-audit',
+        name: 'WithdrawAudit',
+        component: () => import('@/views/admin/WithdrawAudit.vue'),
+        meta: { title: '提现审核' }
       }
       }
     ]
     ]
   }
   }
@@ -285,7 +323,8 @@ router.beforeEach(async (to, from, next) => {
     'Users', 'PackageAudit', 'GuideAudit', 'OperationLogs',
     'Users', 'PackageAudit', 'GuideAudit', 'OperationLogs',
     'ServiceTypes', 'ServiceContents', 'PackageTemplates',
     'ServiceTypes', 'ServiceContents', 'PackageTemplates',
     'AssessmentAdmin', 'VendorReview',
     'AssessmentAdmin', 'VendorReview',
-    'ProductManage', 'OrderManage', 'SysConfig'
+    'ProductManage', 'OrderManage', 'SysConfig', 'EnergySandbox',
+    'ArticleCategory', 'ArticleManage', 'ArticleEdit'
   ]
   ]
   if (adminRoutes.includes(to.name) && role === 'teacher') {
   if (adminRoutes.includes(to.name) && role === 'teacher') {
     console.warn('Access denied: Admin role required for', to.name)
     console.warn('Access denied: Admin role required for', to.name)

+ 16 - 4
cfc-web/src/views/Layout.vue

@@ -153,11 +153,23 @@
               <i class="el-icon-s-order"></i>
               <i class="el-icon-s-order"></i>
               <span>订单管理</span>
               <span>订单管理</span>
             </el-menu-item>
             </el-menu-item>
-            <el-menu-item index="/sys-config">
-              <i class="el-icon-s-tools"></i>
-              <span>系统配置</span>
+            <el-menu-item index="/article-categories">
+              <i class="el-icon-document"></i>
+              <span>文章分类</span>
             </el-menu-item>
             </el-menu-item>
-          </el-submenu>
+            <el-menu-item index="/article-manage">
+              <i class="el-icon-document"></i>
+              <span>文章管理</span>
+            </el-menu-item>
+          <el-menu-item index="/sys-config">
+            <i class="el-icon-s-tools"></i>
+            <span>系统配置</span>
+          </el-menu-item>
+          <el-menu-item index="/energy-sandbox">
+            <i class="el-icon-data-line"></i>
+            <span>五维能量</span>
+          </el-menu-item>
+        </el-submenu>
         </template>
         </template>
       </el-menu>
       </el-menu>
     </el-aside>
     </el-aside>

+ 174 - 0
cfc-web/src/views/admin/ArticleCategory.vue

@@ -0,0 +1,174 @@
+<template>
+  <div class="article-category">
+    <el-card>
+      <div slot="header">
+        <span>文章分类管理</span>
+        <el-button style="float: right" type="primary" size="small" @click="handleCreate">+ 新增分类</el-button>
+      </div>
+      <el-table :data="categories" v-loading="loading" border stripe>
+        <el-table-column prop="id" label="ID" width="70" />
+        <el-table-column label="图标" width="80">
+          <template slot-scope="{ row }">
+            <span style="font-size: 22px">{{ row.icon || '📄' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="name" label="名称" min-width="140" />
+        <el-table-column label="颜色" width="100">
+          <template slot-scope="{ row }">
+            <span v-if="row.color" :style="{ display: 'inline-block', width: '20px', height: '20px', backgroundColor: row.color, borderRadius: '4px', verticalAlign: 'middle', border: '1px solid #ddd' }"></span>
+            <span style="margin-left: 6px; color: #999;">{{ row.color || '-' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="sortOrder" label="排序" width="70" />
+        <el-table-column label="状态" width="80">
+          <template slot-scope="{ row }">
+            <el-tag :type="row.status === 1 || row.status === 'ACTIVE' ? 'success' : 'danger'" size="small">
+              {{ row.status === 1 || row.status === 'ACTIVE' ? '启用' : '停用' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="160" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button size="mini" type="primary" plain @click="handleEdit(row)">编辑</el-button>
+            <el-button size="mini" type="danger" plain @click="handleDelete(row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+
+    <el-dialog :visible.sync="dialogVisible" :title="isEdit ? '编辑分类' : '新增分类'" width="500px" :close-on-click-modal="false">
+      <el-form :model="form" label-width="80px">
+        <el-form-item label="名称">
+          <el-input v-model="form.name" placeholder="请输入分类名称" />
+        </el-form-item>
+        <el-form-item label="图标">
+          <el-input v-model="form.icon" placeholder="如: 📚 或 emoji 图标" />
+        </el-form-item>
+        <el-form-item label="颜色">
+          <el-color-picker v-model="form.color" />
+        </el-form-item>
+        <el-form-item label="排序">
+          <el-input-number v-model="form.sortOrder" :min="0" :max="999" />
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-switch v-model="form.statusBool" active-text="启用" inactive-text="停用" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleSubmit" :loading="submitting">保存</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  adminArticleCategoriesList,
+  adminArticleCategoryCreate,
+  adminArticleCategoryUpdate,
+  adminArticleCategoryDelete
+} from '@/api/article.js'
+
+export default {
+  name: 'ArticleCategory',
+  data() {
+    return {
+      loading: false,
+      submitting: false,
+      categories: [],
+      dialogVisible: false,
+      isEdit: false,
+      form: {
+        id: null,
+        name: '',
+        icon: '',
+        color: '',
+        sortOrder: 0,
+        statusBool: true
+      }
+    }
+  },
+  created() {
+    this.loadData()
+  },
+  methods: {
+    async loadData() {
+      this.loading = true
+      try {
+        const res = await adminArticleCategoriesList()
+        if (res.data) {
+          this.categories = res.data
+        }
+      } catch (e) {
+        this.$message.error('加载分类列表失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    handleCreate() {
+      this.isEdit = false
+      this.form = { id: null, name: '', icon: '', color: '', sortOrder: 0, statusBool: true }
+      this.dialogVisible = true
+    },
+    handleEdit(row) {
+      this.isEdit = true
+      this.form = {
+        id: row.id,
+        name: row.name,
+        icon: row.icon,
+        color: row.color,
+        sortOrder: row.sortOrder,
+        statusBool: row.status === 1 || row.status === 'ACTIVE'
+      }
+      this.dialogVisible = true
+    },
+    async handleSubmit() {
+      this.submitting = true
+      try {
+        const data = {
+          name: this.form.name,
+          icon: this.form.icon,
+          color: this.form.color,
+          sortOrder: this.form.sortOrder,
+          status: this.form.statusBool ? 1 : 0
+        }
+        if (this.isEdit) {
+          data.id = this.form.id
+          await adminArticleCategoryUpdate(data)
+        } else {
+          await adminArticleCategoryCreate(data)
+        }
+        this.$message.success('保存成功')
+        this.dialogVisible = false
+        this.loadData()
+      } catch (e) {
+        this.$message.error(e.message || '保存失败')
+      } finally {
+        this.submitting = false
+      }
+    },
+    async handleDelete(row) {
+      this.$confirm(`确定删除分类「${row.name}」吗?`, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        try {
+          await adminArticleCategoryDelete({ id: row.id })
+          this.$message.success('删除成功')
+          this.loadData()
+        } catch (e) {
+          this.$message.error(e.message || '删除失败')
+        }
+      }).catch(() => {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.article-category {
+  padding: 20px;
+}
+</style>

+ 267 - 0
cfc-web/src/views/admin/ArticleEdit.vue

@@ -0,0 +1,267 @@
+<template>
+  <div class="article-edit">
+    <el-card>
+      <div slot="header">
+        <span>{{ isEdit ? '编辑文章' : '新建文章' }}</span>
+        <div style="float: right;">
+          <el-button @click="$router.push('/article-manage')">返回列表</el-button>
+          <el-button type="primary" @click="handleSave(false)" :loading="saving">保存草稿</el-button>
+          <el-button type="success" @click="handleSave(true)" :loading="publishing">发布</el-button>
+        </div>
+      </div>
+
+      <el-form :model="form" label-width="120px" v-loading="loading">
+        <el-form-item label="标题">
+          <el-input v-model="form.title" placeholder="请输入文章标题" maxlength="200" show-word-limit />
+        </el-form-item>
+
+        <el-form-item label="分类">
+          <el-select v-model="form.categoryId" placeholder="选择分类" style="width: 300px;">
+            <el-option v-for="cat in categories" :key="cat.id" :label="cat.name" :value="cat.id" />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="摘要">
+          <el-input v-model="form.summary" type="textarea" :rows="3" placeholder="请输入文章摘要" maxlength="500" show-word-limit />
+        </el-form-item>
+
+        <el-form-item label="封面图">
+          <el-upload
+            :action="uploadActionUrl"
+            :headers="uploadHeaders"
+            :on-success="handleUploadSuccess"
+            :on-error="handleUploadError"
+            :before-upload="beforeUpload"
+            :show-file-list="false"
+            accept="image/jpeg,image/png,image/gif,image/webp"
+          >
+            <img v-if="form.coverImage" :src="form.coverImage" class="cover-preview" />
+            <el-button v-else type="primary" plain>+ 上传封面</el-button>
+          </el-upload>
+          <el-button v-if="form.coverImage" size="mini" type="text" style="margin-left: 10px;" @click="form.coverImage = ''">清除</el-button>
+        </el-form-item>
+
+        <el-form-item label="内容">
+          <el-input v-model="form.content" type="textarea" :rows="15" placeholder="请输入文章内容(支持 HTML)&#10;&#10;提示:此为纯文本编辑器,后续将升级为富文本编辑器" />
+          <div style="color: #999; font-size: 12px; margin-top: 4px;">当前为纯文本输入,后续将升级为富文本编辑器</div>
+        </el-form-item>
+
+        <el-form-item label="标签">
+          <el-input v-model="form.tags" placeholder="逗号分隔,如: 育儿,亲子,心理" />
+        </el-form-item>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="作者">
+              <el-input v-model="form.author" placeholder="文章作者" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="阅读时间(分)">
+              <el-input-number v-model="form.readTime" :min="1" :max="999" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-form-item label="关联五维">
+          <el-checkbox-group v-model="form.relatedDimensions">
+            <el-checkbox label="智·金" />
+            <el-checkbox label="富·水" />
+            <el-checkbox label="行·木" />
+            <el-checkbox label="心·火" />
+            <el-checkbox label="身·土" />
+          </el-checkbox-group>
+        </el-form-item>
+
+        <el-form-item label="浏览权限">
+          <el-radio-group v-model="form.visibility">
+            <el-radio label="public">公开</el-radio>
+            <el-radio label="login">登录可见</el-radio>
+            <el-radio label="private">私密</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import {
+  adminArticleCreate,
+  adminArticleUpdate,
+  adminArticlePublish,
+  adminArticleCategoriesList,
+  adminArticleDetail
+} from '@/api/article.js'
+
+export default {
+  name: 'ArticleEdit',
+  data() {
+    return {
+      isEdit: false,
+      loading: false,
+      saving: false,
+      publishing: false,
+      categories: [],
+      form: {
+        id: null,
+        categoryId: '',
+        title: '',
+        summary: '',
+        coverImage: '',
+        content: '',
+        tags: '',
+        author: '',
+        readTime: 3,
+        relatedDimensions: [],
+        visibility: 'public',
+        visibleTo: []
+      }
+    }
+  },
+  computed: {
+    uploadActionUrl() {
+      const baseURL = process.env.VUE_APP_BASE_API || 'http://localhost:8080'
+      return baseURL + '/api/admin/articles/upload/image'
+    },
+    uploadHeaders() {
+      const token = localStorage.getItem('token')
+      return token ? { Authorization: 'Bearer ' + token } : {}
+    }
+  },
+  created() {
+    this.loadCategories()
+    const id = this.$route.query.id
+    if (id) {
+      this.isEdit = true
+      this.loadDetail(id)
+    }
+  },
+  methods: {
+    async loadCategories() {
+      try {
+        const res = await adminArticleCategoriesList()
+        if (res.data) {
+          this.categories = res.data
+        }
+      } catch (e) {
+        console.error('加载分类失败', e)
+      }
+    },
+    async loadDetail(id) {
+      this.loading = true
+      try {
+        const res = await adminArticleDetail({ id })
+        if (res.data) {
+          const d = res.data
+          this.form = {
+            id: d.id,
+            categoryId: d.categoryId,
+            title: d.title || '',
+            summary: d.summary || '',
+            coverImage: d.coverImage || '',
+            content: d.content || '',
+            tags: d.tags || '',
+            author: d.author || '',
+            readTime: d.readTime || 3,
+            relatedDimensions: d.relatedDimensions || [],
+            visibility: d.visibility || 'public',
+            visibleTo: d.visibleTo || []
+          }
+        }
+      } catch (e) {
+        this.$message.error('加载文章详情失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    beforeUpload(file) {
+      const isImage = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/gif' || file.type === 'image/webp'
+      if (!isImage) {
+        this.$message.error('只能上传 JPG/PNG/GIF/WebP 格式图片')
+        return false
+      }
+      const isLt5M = file.size / 1024 / 1024 < 5
+      if (!isLt5M) {
+        this.$message.error('图片大小不能超过 5MB')
+        return false
+      }
+      return true
+    },
+    handleUploadSuccess(res) {
+      if (res.code === 200 && res.data) {
+        this.form.coverImage = res.data.url || res.data
+        this.$message.success('封面上传成功')
+      } else {
+        this.$message.error(res.message || '封面上传失败')
+      }
+    },
+    handleUploadError() {
+      this.$message.error('封面上传失败,请重试')
+    },
+    buildSubmitData() {
+      return {
+        id: this.form.id,
+        categoryId: this.form.categoryId,
+        title: this.form.title,
+        summary: this.form.summary,
+        coverImage: this.form.coverImage,
+        content: this.form.content,
+        tags: this.form.tags,
+        author: this.form.author,
+        readTime: this.form.readTime,
+        relatedDimensions: this.form.relatedDimensions,
+        visibility: this.form.visibility,
+        visibleTo: this.form.visibleTo
+      }
+    },
+    async handleSave(publishNow) {
+      if (publishNow) {
+        this.publishing = true
+      } else {
+        this.saving = true
+      }
+      try {
+        const data = this.buildSubmitData()
+        let res
+        if (this.isEdit) {
+          await adminArticleUpdate(data)
+          if (publishNow) {
+            await adminArticlePublish({ id: this.form.id, status: 'published' })
+          }
+        } else {
+          if (publishNow) {
+            data.status = 'published'
+          }
+          res = await adminArticleCreate(data)
+          if (res.data && res.data.id) {
+            this.form.id = res.data.id
+            this.isEdit = true
+          }
+        }
+        this.$message.success(publishNow ? '文章已发布' : '草稿已保存')
+        this.$router.push('/article-manage')
+      } catch (e) {
+        this.$message.error(e.message || '保存失败')
+      } finally {
+        this.saving = false
+        this.publishing = false
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.article-edit {
+  padding: 20px;
+}
+.cover-preview {
+  width: 240px;
+  max-height: 160px;
+  object-fit: cover;
+  border-radius: 6px;
+  border: 1px solid #dcdfe6;
+  cursor: pointer;
+}
+</style>

+ 212 - 0
cfc-web/src/views/admin/ArticleManage.vue

@@ -0,0 +1,212 @@
+<template>
+  <div class="article-manage">
+    <el-card>
+      <div slot="header">
+        <span>文章管理</span>
+        <el-button style="float: right" type="primary" size="small" @click="$router.push('/article-edit')">+ 新建文章</el-button>
+      </div>
+
+      <!-- 搜索筛选栏 -->
+      <div class="filter-bar">
+        <el-select v-model="filters.status" placeholder="状态" clearable @change="loadList" style="width: 130px;">
+          <el-option label="全部" value="" />
+          <el-option label="草稿" value="draft" />
+          <el-option label="已发布" value="published" />
+          <el-option label="已归档" value="archived" />
+        </el-select>
+        <el-select v-model="filters.categoryId" placeholder="分类" clearable @change="loadList" style="width: 150px;">
+          <el-option label="全部分类" value="" />
+          <el-option v-for="cat in categories" :key="cat.id" :label="cat.name" :value="cat.id" />
+        </el-select>
+        <el-input v-model="filters.keyword" placeholder="搜索文章标题..." clearable style="width: 220px;" @keyup.enter.native="loadList" />
+        <el-button type="primary" icon="el-icon-search" @click="loadList">搜索</el-button>
+      </div>
+
+      <el-table :data="list" v-loading="loading" border stripe>
+        <el-table-column prop="id" label="ID" width="70" />
+        <el-table-column label="标题" min-width="220">
+          <template slot-scope="{ row }">
+            <span>{{ row.title }}</span>
+            <el-tag v-if="row.isFeatured === 1 || row.isFeatured === true" size="mini" type="warning" style="margin-left: 6px;">精选</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="分类" width="100">
+          <template slot-scope="{ row }">
+            {{ row.categoryName || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="可见性" width="90">
+          <template slot-scope="{ row }">
+            <el-tag v-if="row.visibility === 'public'" size="small" type="success">公开</el-tag>
+            <el-tag v-else-if="row.visibility === 'login'" size="small" type="primary">登录</el-tag>
+            <el-tag v-else size="small" type="info">私密</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="author" label="作者" width="100" />
+        <el-table-column label="状态" width="90">
+          <template slot-scope="{ row }">
+            <el-tag v-if="row.status === 'published'" size="small" type="success">已发布</el-tag>
+            <el-tag v-else-if="row.status === 'draft'" size="small" type="info">草稿</el-tag>
+            <el-tag v-else size="small" type="warning">已归档</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="viewCount" label="阅读" width="70" />
+        <el-table-column label="发布时间" width="160">
+          <template slot-scope="{ row }">
+            {{ row.publishedAt || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="280" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button size="mini" type="primary" plain @click="$router.push('/article-edit?id=' + row.id)">编辑</el-button>
+            <el-button v-if="row.status === 'draft'" size="mini" type="success" @click="handlePublish(row)">发布</el-button>
+            <el-button v-if="row.status === 'published'" size="mini" type="warning" @click="handleUnpublish(row)">下架</el-button>
+            <el-button
+              size="mini"
+              :type="row.isFeatured === 1 || row.isFeatured === true ? 'warning' : 'default'"
+              :plain="!(row.isFeatured === 1 || row.isFeatured === true)"
+              @click="handleToggleFeatured(row)"
+            >
+              {{ row.isFeatured === 1 || row.isFeatured === true ? '取消精选' : '精选' }}
+            </el-button>
+            <el-button size="mini" type="danger" @click="handleDelete(row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <el-pagination
+        @size-change="onPageChange"
+        @current-change="onPageChange"
+        :current-page="page"
+        :page-size="size"
+        :total="total"
+        layout="total, prev, pager, next"
+        style="margin-top: 20px; text-align: right;"
+      />
+    </el-card>
+  </div>
+</template>
+
+<script>
+import {
+  adminArticleList,
+  adminArticlePublish,
+  adminArticleToggleFeatured,
+  adminArticleDelete,
+  adminArticleCategoriesList
+} from '@/api/article.js'
+
+export default {
+  name: 'ArticleManage',
+  data() {
+    return {
+      loading: false,
+      list: [],
+      total: 0,
+      page: 1,
+      size: 20,
+      filters: {
+        status: '',
+        categoryId: '',
+        keyword: ''
+      },
+      categories: []
+    }
+  },
+  created() {
+    this.loadCategories()
+    this.loadList()
+  },
+  methods: {
+    async loadCategories() {
+      try {
+        const res = await adminArticleCategoriesList()
+        if (res.data) {
+          this.categories = res.data
+        }
+      } catch (e) {
+        console.error('加载分类失败', e)
+      }
+    },
+    async loadList() {
+      this.loading = true
+      try {
+        const res = await adminArticleList({
+          page: this.page,
+          size: this.size,
+          status: this.filters.status || undefined,
+          categoryId: this.filters.categoryId || undefined,
+          keyword: this.filters.keyword || undefined
+        })
+        if (res.data) {
+          this.list = res.data.records || res.data.list || res.data
+          this.total = res.data.total || 0
+        }
+      } catch (e) {
+        this.$message.error('加载文章列表失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    onPageChange() {
+      this.loadList()
+    },
+    async handlePublish(row) {
+      try {
+        await adminArticlePublish({ id: row.id, status: 'published' })
+        this.$message.success('文章已发布')
+        this.loadList()
+      } catch (e) {
+        this.$message.error(e.message || '发布失败')
+      }
+    },
+    async handleUnpublish(row) {
+      try {
+        await adminArticlePublish({ id: row.id, status: 'draft' })
+        this.$message.success('文章已下架')
+        this.loadList()
+      } catch (e) {
+        this.$message.error(e.message || '下架失败')
+      }
+    },
+    async handleToggleFeatured(row) {
+      try {
+        const newVal = row.isFeatured === 1 || row.isFeatured === true ? 0 : 1
+        await adminArticleToggleFeatured({ id: row.id, isFeatured: newVal })
+        this.$message.success(newVal ? '已设为精选' : '已取消精选')
+        this.loadList()
+      } catch (e) {
+        this.$message.error(e.message || '操作失败')
+      }
+    },
+    handleDelete(row) {
+      this.$confirm(`确定删除文章「${row.title}」吗?`, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        try {
+          await adminArticleDelete({ id: row.id })
+          this.$message.success('删除成功')
+          this.loadList()
+        } catch (e) {
+          this.$message.error(e.message || '删除失败')
+        }
+      }).catch(() => {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.article-manage {
+  padding: 20px;
+}
+.filter-bar {
+  margin-bottom: 18px;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  flex-wrap: wrap;
+}
+</style>

+ 181 - 0
cfc-web/src/views/admin/EnergySandbox.vue

@@ -0,0 +1,181 @@
+<template>
+  <div class="page-container">
+    <el-card>
+      <div slot="header">
+        <span>五维能量沙盘</span>
+        <el-button
+          type="primary"
+          size="small"
+          style="float: right"
+          @click="loadData"
+          :loading="loading"
+        >
+          刷新
+        </el-button>
+      </div>
+      <p class="page-desc">展示家庭在身、心、智、行、富五个维度的能量评分及综合指数</p>
+    </el-card>
+
+    <!-- 家庭能量总览 -->
+    <el-card class="section-card" v-loading="loading">
+      <div slot="header">
+        <span>家庭能量总览</span>
+      </div>
+      <el-row :gutter="16">
+        <el-col :span="4" v-for="item in dimensionList" :key="item.key">
+          <div class="metric-card" :style="{ borderTopColor: item.color }">
+            <div class="metric-name">{{ item.label }}</div>
+            <div class="metric-score">
+              <el-tag :color="item.color" effect="dark" size="medium" class="score-tag">
+                {{ sandboxData[item.key] != null ? sandboxData[item.key] : '-' }}
+              </el-tag>
+            </div>
+          </div>
+        </el-col>
+      </el-row>
+    </el-card>
+
+    <!-- 家庭成员数据 -->
+    <el-card class="section-card" v-loading="loading">
+      <div slot="header">
+        <span>家庭成员数据</span>
+      </div>
+      <el-table :data="sandboxData.members || []" border style="width: 100%">
+        <el-table-column prop="memberType" label="成员类型" width="100"></el-table-column>
+        <el-table-column prop="name" label="姓名" width="120"></el-table-column>
+        <el-table-column prop="bodyScore" label="身" width="80" align="center">
+          <template slot-scope="scope">
+            <el-tag size="small" color="#67C23A" effect="dark">{{ scope.row.bodyScore != null ? scope.row.bodyScore : '-' }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="mindScore" label="心" width="80" align="center">
+          <template slot-scope="scope">
+            <el-tag size="small" color="#9B59B6" effect="dark">{{ scope.row.mindScore != null ? scope.row.mindScore : '-' }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="wisdomScore" label="智" width="80" align="center">
+          <template slot-scope="scope">
+            <el-tag size="small" color="#409EFF" effect="dark">{{ scope.row.wisdomScore != null ? scope.row.wisdomScore : '-' }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="actionScore" label="行" width="80" align="center">
+          <template slot-scope="scope">
+            <el-tag size="small" color="#E6A23C" effect="dark">{{ scope.row.actionScore != null ? scope.row.actionScore : '-' }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="wealthScore" label="富" width="80" align="center">
+          <template slot-scope="scope">
+            <el-tag size="small" color="#F0C929" effect="dark">{{ scope.row.wealthScore != null ? scope.row.wealthScore : '-' }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="overallScore" label="综合" width="80" align="center">
+          <template slot-scope="scope">
+            <el-tag size="small" color="#6366F1" effect="dark">{{ scope.row.overallScore != null ? scope.row.overallScore : '-' }}</el-tag>
+          </template>
+        </el-table-column>
+        <template slot="empty">
+          <span>暂无数据</span>
+        </template>
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { getFamilyEnergySandbox } from '@/api/energy'
+
+export default {
+  name: 'EnergySandbox',
+  data() {
+    return {
+      loading: false,
+      sandboxData: {
+        bodyScore: null,
+        mindScore: null,
+        wisdomScore: null,
+        actionScore: null,
+        wealthScore: null,
+        overallScore: null,
+        members: []
+      },
+      dimensionList: [
+        { key: 'bodyScore', label: '身', color: '#67C23A' },
+        { key: 'mindScore', label: '心', color: '#9B59B6' },
+        { key: 'wisdomScore', label: '智', color: '#409EFF' },
+        { key: 'actionScore', label: '行', color: '#E6A23C' },
+        { key: 'wealthScore', label: '富', color: '#F0C929' },
+        { key: 'overallScore', label: '综合', color: '#6366F1' }
+      ]
+    }
+  },
+  mounted() {
+    this.loadData()
+  },
+  methods: {
+    async loadData() {
+      this.loading = true
+      try {
+        const res = await getFamilyEnergySandbox()
+        if (res.data) {
+          this.sandboxData = {
+            bodyScore: res.data.bodyScore,
+            mindScore: res.data.mindScore,
+            wisdomScore: res.data.wisdomScore,
+            actionScore: res.data.actionScore,
+            wealthScore: res.data.wealthScore,
+            overallScore: res.data.overallScore,
+            members: res.data.members || []
+          }
+        }
+      } catch (error) {
+        this.$message.error(error.message || '加载五维能量数据失败')
+      } finally {
+        this.loading = false
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page-container {
+  padding: 20px;
+}
+
+.page-desc {
+  color: #909399;
+  font-size: 13px;
+  margin: 0;
+}
+
+.section-card {
+  margin-top: 16px;
+}
+
+.metric-card {
+  background: #f8fafc;
+  border-radius: 8px;
+  border-top: 3px solid #ddd;
+  padding: 16px 8px;
+  text-align: center;
+}
+
+.metric-name {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+  margin-bottom: 10px;
+}
+
+.metric-score {
+  display: flex;
+  justify-content: center;
+}
+
+.score-tag {
+  font-size: 18px;
+  font-weight: 700;
+  min-width: 48px;
+  text-align: center;
+}
+</style>

+ 178 - 0
cfc-web/src/views/admin/ProductProfitRate.vue

@@ -0,0 +1,178 @@
+<template>
+  <div class="product-profit-rate">
+    <div class="header">
+      <h2>产品利润率设置</h2>
+      <div class="filters">
+        <el-input v-model="searchText" placeholder="商品名称搜索" style="width: 200px;" clearable @keyup.enter.native="loadList" />
+        <el-select v-model="typeFilter" placeholder="商品类型" @change="loadList" style="width: 140px;">
+          <el-option label="全部类型" value="" />
+          <el-option label="实物商品" value="physical" />
+          <el-option label="虚拟商品" value="virtual" />
+          <el-option label="优惠券" value="coupon" />
+        </el-select>
+        <el-button type="primary" @click="loadList">查询</el-button>
+      </div>
+    </div>
+
+    <el-table :data="list" v-loading="loading" border stripe>
+      <el-table-column prop="id" label="ID" width="70" />
+      <el-table-column label="商品图片" width="70">
+        <template slot-scope="{ row }">
+          <img v-if="row.coverImage" :src="row.coverImage" class="product-thumb" />
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="name" label="商品名称" min-width="160" show-overflow-tooltip />
+      <el-table-column label="类型" width="90">
+        <template slot-scope="{ row }">
+          {{ typeLabel(row.productType) }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="price" label="售价(元)" width="90">
+        <template slot-scope="{ row }">
+          ¥{{ (row.price / 100).toFixed(2) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="当前利润率" width="120">
+        <template slot-scope="{ row }">
+          <span v-if="row.profitRate != null">{{ (row.profitRate * 100).toFixed(1) }}%</span>
+          <span v-else class="text-muted">未设置</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="新利润率" width="180">
+        <template slot-scope="{ row }">
+          <el-input-number
+            v-if="editId === row.id"
+            v-model="editRate"
+            :min="0"
+            :max="1"
+            :step="0.01"
+            :precision="2"
+            size="small"
+            style="width: 140px;"
+          />
+          <span v-else class="text-muted">-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="150" fixed="right">
+        <template slot-scope="{ row }">
+          <el-button
+            v-if="editId !== row.id"
+            type="primary"
+            size="small"
+            @click="startEdit(row)"
+          >设置</el-button>
+          <template v-else>
+            <el-button type="success" size="small" @click="saveRate(row)">保存</el-button>
+            <el-button size="small" @click="cancelEdit">取消</el-button>
+          </template>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <el-pagination
+      @size-change="onPageChange"
+      @current-change="onPageChange"
+      :current-page="page"
+      :page-size="size"
+      :total="total"
+      layout="total, prev, pager, next"
+      style="margin-top: 20px; text-align: right;"
+    />
+  </div>
+</template>
+
+<script>
+import { getProductList, updateProductProfitRate } from '@/api/admin'
+
+export default {
+  data() {
+    return {
+      list: [],
+      loading: false,
+      page: 1,
+      size: 20,
+      total: 0,
+      searchText: '',
+      typeFilter: '',
+      editId: null,
+      editRate: 0
+    }
+  },
+  mounted() {
+    this.loadList()
+  },
+  methods: {
+    async loadList() {
+      this.loading = true
+      try {
+        const res = await getProductList({ status: '', keyword: this.searchText, productType: this.typeFilter })
+        this.list = res.data || []
+        this.total = (res.data || []).length
+      } catch (e) {
+        this.$message.error('加载失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    onPageChange(val) {
+      this.page = val
+      this.loadList()
+    },
+    typeLabel(type) {
+      const map = { physical: '实物商品', virtual: '虚拟商品', coupon: '优惠券' }
+      return map[type] || type
+    },
+    startEdit(row) {
+      this.editId = row.id
+      this.editRate = row.profitRate != null ? row.profitRate : 0
+    },
+    cancelEdit() {
+      this.editId = null
+      this.editRate = 0
+    },
+    async saveRate(row) {
+      if (this.editRate == null) {
+        return this.$message.warning('请设置利润率')
+      }
+      try {
+        await updateProductProfitRate(row.id, this.editRate)
+        this.$message.success('设置成功')
+        this.editId = null
+        this.loadList()
+      } catch (e) {
+        this.$message.error('设置失败')
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.product-profit-rate {
+  padding: 20px;
+}
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+.header h2 {
+  margin: 0;
+}
+.filters {
+  display: flex;
+  gap: 10px;
+  align-items: center;
+}
+.product-thumb {
+  width: 50px;
+  height: 50px;
+  object-fit: cover;
+  border-radius: 4px;
+}
+.text-muted {
+  color: #999;
+}
+</style>

+ 208 - 0
cfc-web/src/views/admin/WithdrawAudit.vue

@@ -0,0 +1,208 @@
+<template>
+  <div class="withdraw-audit">
+    <div class="header">
+      <h2>提现审核</h2>
+      <el-select v-model="statusFilter" placeholder="审核状态" @change="loadList" style="width: 150px;">
+        <el-option label="全部" value="" />
+        <el-option label="待审核" value="pending" />
+        <el-option label="已通过" value="approved" />
+        <el-option label="已拒绝" value="rejected" />
+      </el-select>
+    </div>
+
+    <el-table :data="list" v-loading="loading" border stripe>
+      <el-table-column prop="id" label="ID" width="70" />
+      <el-table-column label="用户" width="120">
+        <template slot-scope="{ row }">
+          {{ row.nickname || row.userName || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="提现金额" width="120">
+        <template slot-scope="{ row }">
+          <span class="amount">¥{{ (row.amount / 100).toFixed(2) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="账户信息" min-width="160" show-overflow-tooltip>
+        <template slot-scope="{ row }">
+          {{ row.accountInfo || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" width="100">
+        <template slot-scope="{ row }">
+          <el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="申请人备注" width="160" show-overflow-tooltip>
+        <template slot-scope="{ row }">
+          {{ row.remark || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="审核人" width="100">
+        <template slot-scope="{ row }">
+          {{ row.auditBy || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="审核时间" width="160">
+        <template slot-scope="{ row }">
+          {{ row.auditAt ? formatTime(row.auditAt) : '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="createTime" label="申请时间" width="160">
+        <template slot-scope="{ row }">
+          {{ formatTime(row.createTime) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="180" fixed="right">
+        <template slot-scope="{ row }">
+          <el-button v-if="row.status === 'pending'" type="success" size="small" @click="handleAudit(row, 'approved')">通过</el-button>
+          <el-button v-if="row.status === 'pending'" type="danger" size="small" @click="showRejectDialog(row)">拒绝</el-button>
+          <el-button type="info" size="small" @click="viewDetail(row)">详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <el-pagination
+      @size-change="onPageChange"
+      @current-change="onPageChange"
+      :current-page="page"
+      :page-size="size"
+      :total="total"
+      layout="total, prev, pager, next"
+      style="margin-top: 20px; text-align: right;"
+    />
+
+    <!-- 拒绝原因弹窗 -->
+    <el-dialog title="拒绝提现" :visible.sync="rejectDialogVisible" width="400px">
+      <el-input v-model="rejectReason" type="textarea" :rows="3" placeholder="请输入拒绝原因(选填)" />
+      <span slot="footer">
+        <el-button @click="rejectDialogVisible = false">取消</el-button>
+        <el-button type="danger" @click="confirmReject">确认拒绝</el-button>
+      </span>
+    </el-dialog>
+
+    <!-- 提现详情弹窗 -->
+    <el-dialog title="提现详情" :visible.sync="detailDialogVisible" width="600px">
+      <el-descriptions :column="2" border v-if="detail">
+        <el-descriptions-item label="申请ID">{{ detail.id }}</el-descriptions-item>
+        <el-descriptions-item label="用户昵称">{{ detail.nickname || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="提现金额">¥{{ (detail.amount / 100).toFixed(2) }}</el-descriptions-item>
+        <el-descriptions-item label="账户信息">{{ detail.accountInfo || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="申请备注">{{ detail.remark || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="审核状态">
+          <el-tag :type="statusType(detail.status)">{{ statusLabel(detail.status) }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="审核人">{{ detail.auditBy || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="审核备注">{{ detail.auditRemark || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="审核时间">{{ detail.auditAt ? formatTime(detail.auditAt) : '-' }}</el-descriptions-item>
+        <el-descriptions-item label="申请时间">{{ formatTime(detail.createTime) }}</el-descriptions-item>
+      </el-descriptions>
+      <span slot="footer">
+        <el-button @click="detailDialogVisible = false">关闭</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getWithdrawalList, auditWithdrawal } from '@/api/admin'
+
+export default {
+  data() {
+    return {
+      list: [],
+      loading: false,
+      page: 1,
+      size: 20,
+      total: 0,
+      statusFilter: '',
+      rejectDialogVisible: false,
+      rejectTarget: null,
+      rejectReason: '',
+      detailDialogVisible: false,
+      detail: null
+    }
+  },
+  mounted() {
+    this.loadList()
+  },
+  methods: {
+    async loadList() {
+      this.loading = true
+      try {
+        const res = await getWithdrawalList({ status: this.statusFilter })
+        this.list = res.data || []
+        this.total = (res.data || []).length
+      } catch (e) {
+        this.$message.error('加载失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    onPageChange(val) {
+      this.page = val
+      this.loadList()
+    },
+    statusType(status) {
+      const map = { pending: 'warning', approved: 'success', rejected: 'danger' }
+      return map[status] || 'info'
+    },
+    statusLabel(status) {
+      const map = { pending: '待审核', approved: '已通过', rejected: '已拒绝' }
+      return map[status] || status
+    },
+    formatTime(time) {
+      if (!time) return '-'
+      return new Date(time).toLocaleString()
+    },
+    async handleAudit(row, status) {
+      const actionLabel = status === 'approved' ? '通过' : '拒绝'
+      try {
+        await this.$confirm(`确认${actionLabel}该提现申请?`, '提示')
+        await auditWithdrawal(row.id, status, '')
+        this.$message.success('操作成功')
+        this.loadList()
+      } catch (e) {
+        if (e !== 'cancel') this.$message.error('操作失败')
+      }
+    },
+    showRejectDialog(row) {
+      this.rejectTarget = row
+      this.rejectReason = ''
+      this.rejectDialogVisible = true
+    },
+    async confirmReject() {
+      try {
+        await auditWithdrawal(this.rejectTarget.id, 'rejected', this.rejectReason)
+        this.$message.success('已拒绝')
+        this.rejectDialogVisible = false
+        this.loadList()
+      } catch (e) {
+        this.$message.error('操作失败')
+      }
+    },
+    viewDetail(row) {
+      this.detail = row
+      this.detailDialogVisible = true
+    }
+  }
+}
+</script>
+
+<style scoped>
+.withdraw-audit {
+  padding: 20px;
+}
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+.header h2 {
+  margin: 0;
+}
+.amount {
+  font-weight: bold;
+  color: #f56c6c;
+}
+</style>