ソースを参照

feat(web): 统一审核中心 - ReviewCenter.vue + 10子组件 + 后端summary接口

Xiaogang Liao 2 ヶ月 前
コミット
c05af149d1

+ 98 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/ReviewCenterController.java

@@ -0,0 +1,98 @@
+package com.etotem.cfc.controller.admin;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.Activity;
+import com.etotem.cfc.entity.ActivityRegistration;
+import com.etotem.cfc.entity.Article;
+import com.etotem.cfc.entity.ButlerProfile;
+import com.etotem.cfc.entity.Product;
+import com.etotem.cfc.entity.SupplyHierarchyChangeRequest;
+import com.etotem.cfc.entity.TaskTemplatePackage;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.entity.WithdrawalRequest;
+import com.etotem.cfc.mapper.ActivityMapper;
+import com.etotem.cfc.mapper.ActivityRegistrationMapper;
+import com.etotem.cfc.mapper.ArticleMapper;
+import com.etotem.cfc.mapper.ButlerProfileMapper;
+import com.etotem.cfc.mapper.ProductMapper;
+import com.etotem.cfc.mapper.SupplyHierarchyChangeRequestMapper;
+import com.etotem.cfc.mapper.TaskTemplatePackageMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.mapper.WithdrawalRequestMapper;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/admin/review-center")
+public class ReviewCenterController {
+
+    @Resource
+    private ArticleMapper articleMapper;
+
+    @Resource
+    private ProductMapper productMapper;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private ActivityMapper activityMapper;
+
+    @Resource
+    private ActivityRegistrationMapper activityRegistrationMapper;
+
+    @Resource
+    private WithdrawalRequestMapper withdrawalRequestMapper;
+
+    @Resource
+    private TaskTemplatePackageMapper taskTemplatePackageMapper;
+
+    @Resource
+    private ButlerProfileMapper butlerProfileMapper;
+
+    @Resource
+    private SupplyHierarchyChangeRequestMapper supplyHierarchyChangeRequestMapper;
+
+    @PostMapping("/summary")
+    public Result<Map<String, Integer>> summary() {
+        Map<String, Integer> counts = new HashMap<>();
+
+        counts.put("articlePending", articleMapper.selectCount(
+                new QueryWrapper<Article>().eq("audit_status", "pending")).intValue());
+
+        counts.put("productPending", productMapper.selectCount(
+                new QueryWrapper<Product>().eq("status", "pending")).intValue());
+
+        counts.put("guidePending", userMapper.selectCount(
+                new QueryWrapper<User>().eq("teacher_status", "pending")).intValue());
+
+        counts.put("vendorPending", userMapper.selectCount(
+                new QueryWrapper<User>().eq("vendor_status", "pending")).intValue());
+
+        counts.put("butlerPending", butlerProfileMapper.selectCount(
+                new QueryWrapper<ButlerProfile>().eq("status", "pending")).intValue());
+
+        counts.put("packagePending", taskTemplatePackageMapper.selectCount(
+                new QueryWrapper<TaskTemplatePackage>().eq("status", "pending")).intValue());
+
+        counts.put("activityPending", activityMapper.selectCount(
+                new QueryWrapper<Activity>().eq("audit_status", "pending")).intValue());
+
+        counts.put("activityRegistrationPending", activityRegistrationMapper.selectCount(
+                new QueryWrapper<ActivityRegistration>().eq("status", "pending")).intValue());
+
+        counts.put("withdrawPending", withdrawalRequestMapper.selectCount(
+                new QueryWrapper<WithdrawalRequest>().eq("status", "pending")).intValue());
+
+        counts.put("hierarchyChangePending", supplyHierarchyChangeRequestMapper.selectCount(
+                new QueryWrapper<SupplyHierarchyChangeRequest>().eq("status", "pending")).intValue());
+
+        return Result.success(counts);
+    }
+}

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

@@ -167,6 +167,12 @@ const routes = [
         component: () => import('@/views/GuideAudit.vue'),
         meta: { title: '成长规划师审核', perm: 'audit:guide' }
       },
+      {
+        path: 'review-center',
+        name: 'ReviewCenter',
+        component: () => import('@/views/admin/ReviewCenter.vue'),
+        meta: { title: '审核中心', perm: 'audit' }
+      },
       {
         path: 'operation-logs',
         name: 'OperationLogs',

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

@@ -181,6 +181,8 @@ export default {
         // ===== 4. 审核中心 (admin) =====
         { title: '审核中心', icon: 'el-icon-s-check', perm: 'audit',
           children: [
+            { path: '/review-center', label: '审核中心(新版)', icon: 'el-icon-s-check', perm: 'audit' },
+            { path: '/article-audit', label: '文章审核', icon: 'el-icon-document-checked', perm: 'audit:article' },
             { path: '/package-audit', label: '套餐审核', icon: 'el-icon-document-checked', perm: 'audit:package' },
             { path: '/guide-audit', label: '规划师审核', icon: 'el-icon-document-checked', perm: 'audit:guide' },
             { path: '/vendor-review', label: '服务商审核', icon: 'el-icon-document-checked', perm: 'audit:vendor' },

+ 134 - 0
cfc-web/src/views/admin/ReviewCenter.vue

@@ -0,0 +1,134 @@
+<template>
+  <div class="review-center">
+    <!-- 概览统计卡片 -->
+    <el-row :gutter="16" class="summary-row">
+      <el-col v-for="card in summaryCards" :key="card.key" :span="4">
+        <el-card :class="['summary-card', { active: activeTab === card.key }]" shadow="hover" @click.native="activeTab = card.key">
+          <div class="summary-inner">
+            <span class="summary-icon">{{ card.icon }}</span>
+            <span class="summary-count">{{ card.count }}</span>
+            <span class="summary-label">{{ card.label }}</span>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 详情 Tab -->
+    <el-card class="detail-card">
+      <el-tabs v-model="activeTab" @tab-click="onTabClick">
+        <el-tab-pane label="文章审核" name="article">
+          <article-audit ref="article" />
+        </el-tab-pane>
+        <el-tab-pane label="商品审核" name="product">
+          <product-audit ref="product" />
+        </el-tab-pane>
+        <el-tab-pane label="规划师审核" name="guide">
+          <guide-audit ref="guide" />
+        </el-tab-pane>
+        <el-tab-pane label="服务商审核" name="vendor">
+          <vendor-audit ref="vendor" />
+        </el-tab-pane>
+        <el-tab-pane label="管家审核" name="butler">
+          <butler-audit ref="butler" />
+        </el-tab-pane>
+        <el-tab-pane label="套餐审核" name="package">
+          <package-audit ref="package" />
+        </el-tab-pane>
+        <el-tab-pane label="活动审核" name="activity">
+          <activity-audit ref="activity" />
+        </el-tab-pane>
+        <el-tab-pane label="活动报名审核" name="activityRegistration">
+          <activity-registration-audit ref="activityRegistration" />
+        </el-tab-pane>
+        <el-tab-pane label="提现审核" name="withdraw">
+          <withdraw-audit ref="withdraw" />
+        </el-tab-pane>
+        <el-tab-pane label="更换上级审核" name="hierarchy">
+          <hierarchy-audit ref="hierarchy" />
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import request from '@/utils/request'
+import ArticleAudit from './review/ArticleAudit.vue'
+import ProductAudit from './review/ProductAudit.vue'
+import GuideAudit from './review/GuideAudit.vue'
+import VendorAudit from './review/VendorAudit.vue'
+import ButlerAudit from './review/ButlerAudit.vue'
+import PackageAudit from './review/PackageAudit.vue'
+import ActivityAudit from './review/ActivityAudit.vue'
+import ActivityRegistrationAudit from './review/ActivityRegistrationAudit.vue'
+import WithdrawAudit from './review/WithdrawAudit.vue'
+import HierarchyAudit from './review/HierarchyAudit.vue'
+
+export default {
+  name: 'ReviewCenter',
+  components: {
+    ArticleAudit, ProductAudit, GuideAudit, VendorAudit, ButlerAudit,
+    PackageAudit, ActivityAudit, ActivityRegistrationAudit, WithdrawAudit, HierarchyAudit
+  },
+  data() {
+    return {
+      activeTab: 'article',
+      summaryData: {},
+      summaryCards: [
+        { key: 'article', label: '文章审核', icon: '📄', count: 0 },
+        { key: 'product', label: '商品审核', icon: '📦', count: 0 },
+        { key: 'guide', label: '规划师审核', icon: '👤', count: 0 },
+        { key: 'vendor', label: '服务商审核', icon: '🏪', count: 0 },
+        { key: 'butler', label: '管家审核', icon: '🤵', count: 0 },
+        { key: 'package', label: '套餐审核', icon: '📋', count: 0 },
+        { key: 'activity', label: '活动审核', icon: '🎯', count: 0 },
+        { key: 'activityRegistration', label: '报名审核', icon: '📝', count: 0 },
+        { key: 'withdraw', label: '提现审核', icon: '💰', count: 0 },
+        { key: 'hierarchy', label: '更换上级', icon: '🔗', count: 0 },
+      ]
+    }
+  },
+  created() {
+    this.loadSummary()
+  },
+  methods: {
+    async loadSummary() {
+      try {
+        const res = await request({ url: '/api/admin/review-center/summary', method: 'post', data: {} })
+        if (res.code === 200 && res.data) {
+          this.summaryData = res.data
+          this.summaryCards.forEach(c => {
+            const keyMap = {
+              article: 'articlePending', product: 'productPending', guide: 'guidePending',
+              vendor: 'vendorPending', butler: 'butlerPending', package: 'packagePending',
+              activity: 'activityPending', activityRegistration: 'activityRegistrationPending',
+              withdraw: 'withdrawPending', hierarchy: 'hierarchyChangePending'
+            }
+            c.count = res.data[keyMap[c.key]] || 0
+          })
+        }
+      } catch (e) {
+        console.error('加载审核概览失败', e)
+      }
+    },
+    onTabClick(tab) {
+      var ref = tab.name
+      if (this.$refs[ref] && this.$refs[ref].loadData) {
+        this.$refs[ref].loadData()
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.review-center { padding: 16px; }
+.summary-row { margin-bottom: 16px; }
+.summary-card { cursor: pointer; text-align: center; }
+.summary-card.active { border-color: #409EFF; }
+.summary-inner { display: flex; flex-direction: column; align-items: center; gap: 4px; }
+.summary-icon { font-size: 28px; }
+.summary-count { font-size: 24px; font-weight: bold; color: #409EFF; }
+.summary-label { font-size: 12px; color: #666; }
+.detail-card { min-height: 400px; }
+</style>

+ 85 - 0
cfc-web/src/views/admin/review/ActivityAudit.vue

@@ -0,0 +1,85 @@
+<template>
+  <div>
+    <div class="toolbar">
+      <el-select v-model="statusFilter" placeholder="审核状态" @change="loadData">
+        <el-option label="全部" value="" />
+        <el-option label="待审核" value="pending" />
+        <el-option label="已通过" value="approved" />
+        <el-option label="已拒绝" value="rejected" />
+      </el-select>
+      <el-input v-model="keyword" placeholder="搜索..." prefix-icon="el-icon-search" clearable @keyup.enter.native="handleSearch" @clear="handleSearch" style="width:200px" />
+      <el-button size="small" @click="loadData" :loading="loading">刷新</el-button>
+    </div>
+    <el-table :data="list" v-loading="loading" border stripe>
+      <el-table-column prop="id" label="ID" width="60" />
+      <el-table-column prop="title" label="活动名称" min-width="200" show-overflow-tooltip />
+      <el-table-column label="审核状态" width="100">
+        <template slot-scope="{ row }">
+          <el-tag :type="statusType(row.auditStatus)">{{ statusLabel(row.auditStatus) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="auditReason" label="原因" width="160" show-overflow-tooltip />
+      <el-table-column label="操作" width="180" fixed="right">
+        <template slot-scope="{ row }">
+          <template v-if="row.auditStatus === 'pending'">
+            <el-button size="mini" type="success" @click="handleReview(row, 'publish')">通过</el-button>
+            <el-button size="mini" type="danger" @click="showReject(row)">拒绝</el-button>
+          </template>
+          <span v-else class="no-op">—</span>
+        </template>
+      </el-table-column>
+    </el-table>
+    <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>
+  </div>
+</template>
+
+<script>
+import { getActivityList, auditActivity } from '@/api/activity'
+
+export default {
+  data() {
+    return {
+      list: [], loading: false, keyword: '', statusFilter: 'pending',
+      rejectDialogVisible: false, rejectTarget: null, rejectReason: ''
+    }
+  },
+  mounted() { this.loadData() },
+  methods: {
+    statusType(s) { return { pending: 'warning', approved: 'success', rejected: 'danger' }[s] || 'info' },
+    statusLabel(s) { return { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[s] || s },
+    handleSearch() { this.loadData() },
+    async loadData() {
+      this.loading = true
+      try {
+        const params = { auditStatus: this.statusFilter || undefined, keyword: this.keyword || undefined, page: 1, size: 50 }
+        const res = await getActivityList(params)
+        this.list = res.data && res.data.list || res.data || []
+      } catch (e) { this.$message.error('加载失败')
+      } finally { this.loading = false }
+    },
+    async handleReview(row, action) {
+      try {
+        await auditActivity({ id: row.id, action, rejectReason: action === 'reject' ? this.rejectReason : '' })
+        this.$message.success('审核完成')
+        this.rejectDialogVisible = false
+        this.loadData()
+      } catch (e) { this.$message.error('操作失败') }
+    },
+    showReject(row) { this.rejectTarget = row; this.rejectReason = ''; this.rejectDialogVisible = true },
+    confirmReject() {
+      if (!this.rejectReason) return this.$message.warning('请填写驳回原因')
+      this.handleReview(this.rejectTarget, 'reject')
+    }
+  }
+}
+</script>
+<style scoped>
+.toolbar { display: flex; gap: 12px; align-items: center; margin-bottom: 16px; }
+.no-op { color: #ccc; }
+</style>

+ 92 - 0
cfc-web/src/views/admin/review/ActivityRegistrationAudit.vue

@@ -0,0 +1,92 @@
+<template>
+  <div>
+    <div class="toolbar">
+      <el-select v-model="statusFilter" placeholder="审核状态" @change="loadData">
+        <el-option label="全部" value="" />
+        <el-option label="待审核" value="pending" />
+        <el-option label="已通过" value="approved" />
+        <el-option label="已拒绝" value="rejected" />
+      </el-select>
+      <el-button size="small" @click="loadData" :loading="loading">刷新</el-button>
+    </div>
+    <el-table :data="list" v-loading="loading" border stripe>
+      <el-table-column prop="id" label="ID" width="60" />
+      <el-table-column label="用户" width="120">
+        <template slot-scope="{ row }">{{ row.nickname || row.userName || '-' }}</template>
+      </el-table-column>
+      <el-table-column label="活动" min-width="180" show-overflow-tooltip>
+        <template slot-scope="{ row }">{{ row.activityTitle || row.activityName || '-' }}</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="180" fixed="right">
+        <template slot-scope="{ row }">
+          <template v-if="row.status === 'pending'">
+            <el-button size="mini" type="success" @click="handleApprove(row)">通过</el-button>
+            <el-button size="mini" type="danger" @click="handleReject(row)">拒绝</el-button>
+          </template>
+          <span v-else class="no-op">—</span>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+import { getActivityList } from '@/api/activity'
+import { approveRegistration, rejectRegistration } from '@/api/activity'
+
+export default {
+  data() {
+    return {
+      list: [], loading: false, statusFilter: 'pending'
+    }
+  },
+  mounted() { this.loadData() },
+  methods: {
+    statusType(s) { return { pending: 'warning', approved: 'success', rejected: 'danger' }[s] || 'info' },
+    statusLabel(s) { return { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[s] || s },
+    async loadData() {
+      this.loading = true
+      try {
+        const res = await getActivityList({ withRegistrations: true, page: 1, size: 50 })
+        var regs = []
+        var activities = res.data && res.data.list || res.data || []
+        for (var i = 0; i < activities.length; i++) {
+          var a = activities[i]
+          if (a.registrations) {
+            for (var j = 0; j < a.registrations.length; j++) {
+              var r = a.registrations[j]
+              r.activityTitle = a.title
+              regs.push(r)
+            }
+          }
+        }
+        this.list = this.statusFilter ? regs.filter(function(r) { return r.status === this.statusFilter }.bind(this)) : regs
+      } catch (e) { this.$message.error('加载失败')
+      } finally { this.loading = false }
+    },
+    async handleApprove(row) {
+      try {
+        await approveRegistration({ id: row.id })
+        this.$message.success('已通过')
+        this.loadData()
+      } catch (e) { this.$message.error('操作失败') }
+    },
+    async handleReject(row) {
+      try {
+        await rejectRegistration({ id: row.id })
+        this.$message.success('已拒绝')
+        this.loadData()
+      } catch (e) { this.$message.error('操作失败') }
+    }
+  }
+}
+</script>
+<style scoped>
+.toolbar { display: flex; gap: 12px; align-items: center; margin-bottom: 16px; }
+.no-op { color: #ccc; }
+</style>

+ 87 - 0
cfc-web/src/views/admin/review/ArticleAudit.vue

@@ -0,0 +1,87 @@
+<template>
+  <div>
+    <div class="toolbar">
+      <el-select v-model="statusFilter" placeholder="审核状态" @change="loadData">
+        <el-option label="全部" value="" />
+        <el-option label="待审核" value="pending" />
+        <el-option label="已通过" value="approved" />
+        <el-option label="已拒绝" value="rejected" />
+      </el-select>
+      <el-input v-model="keyword" placeholder="搜索标题..." prefix-icon="el-icon-search" clearable @keyup.enter.native="handleSearch" @clear="handleSearch" style="width:200px" />
+      <el-button size="small" @click="loadData" :loading="loading">刷新</el-button>
+    </div>
+    <el-table :data="list" v-loading="loading" border stripe>
+      <el-table-column prop="id" label="ID" width="60" />
+      <el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip />
+      <el-table-column label="审核状态" width="100">
+        <template slot-scope="{ row }">
+          <el-tag :type="statusType(row.auditStatus)">{{ statusLabel(row.auditStatus) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="auditReason" label="原因" width="160" show-overflow-tooltip />
+      <el-table-column label="操作" width="180" fixed="right">
+        <template slot-scope="{ row }">
+          <template v-if="row.auditStatus === 'pending'">
+            <el-button size="mini" type="success" @click="handleAudit(row, 'approved')">通过</el-button>
+            <el-button size="mini" type="danger" @click="showReject(row)">拒绝</el-button>
+          </template>
+          <span v-else class="no-op">—</span>
+        </template>
+      </el-table-column>
+    </el-table>
+    <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>
+  </div>
+</template>
+
+<script>
+import { adminArticleList, adminArticleAudit } from '@/api/article'
+
+export default {
+  data() {
+    return {
+      list: [], loading: false, keyword: '', statusFilter: 'pending',
+      rejectDialogVisible: false, rejectTarget: null, rejectReason: ''
+    }
+  },
+  mounted() { this.loadData() },
+  methods: {
+    statusType(s) { return { pending: 'warning', approved: 'success', rejected: 'danger' }[s] || 'info' },
+    statusLabel(s) { return { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[s] || s },
+    handleSearch() { this.loadData() },
+    async loadData() {
+      this.loading = true
+      try {
+        const res = await adminArticleList({
+          auditStatus: this.statusFilter || undefined,
+          keyword: this.keyword || undefined, page: 1, size: 50
+        })
+        this.list = res.data && res.data.records || []
+      } catch (e) { this.$message.error('加载失败')
+      } finally { this.loading = false }
+    },
+    async handleAudit(row, auditStatus) {
+      try {
+        await adminArticleAudit({ id: row.id, auditStatus, auditReason: auditStatus === 'rejected' ? this.rejectReason : '' })
+        this.$message.success('审核完成')
+        this.rejectDialogVisible = false
+        this.loadData()
+      } catch (e) { this.$message.error('操作失败') }
+    },
+    showReject(row) { this.rejectTarget = row; this.rejectReason = ''; this.rejectDialogVisible = true },
+    confirmReject() {
+      if (!this.rejectReason) return this.$message.warning('请填写驳回原因')
+      this.handleAudit(this.rejectTarget, 'rejected')
+    }
+  }
+}
+</script>
+<style scoped>
+.toolbar { display: flex; gap: 12px; align-items: center; margin-bottom: 16px; }
+.no-op { color: #ccc; }
+</style>

+ 87 - 0
cfc-web/src/views/admin/review/ButlerAudit.vue

@@ -0,0 +1,87 @@
+<template>
+  <div>
+    <div class="toolbar">
+      <el-select v-model="statusFilter" placeholder="审核状态" @change="loadData">
+        <el-option label="全部" value="" />
+        <el-option label="待审核" value="pending" />
+        <el-option label="已通过" value="approved" />
+        <el-option label="已拒绝" value="rejected" />
+      </el-select>
+      <el-button size="small" @click="loadData" :loading="loading">刷新</el-button>
+    </div>
+    <el-table :data="list" v-loading="loading" border stripe>
+      <el-table-column prop="id" label="档案ID" width="80" />
+      <el-table-column prop="userId" label="用户ID" width="80" />
+      <el-table-column prop="tier" label="等级" width="80" />
+      <el-table-column prop="description" label="申请说明" min-width="200" show-overflow-tooltip />
+      <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="180" fixed="right">
+        <template slot-scope="{ row }">
+          <template v-if="row.status === 'pending'">
+            <el-button size="mini" type="success" @click="handleReview(row, 'approve')">通过</el-button>
+            <el-button size="mini" type="danger" @click="showReject(row)">拒绝</el-button>
+          </template>
+          <span v-else class="no-op">—</span>
+        </template>
+      </el-table-column>
+    </el-table>
+    <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>
+  </div>
+</template>
+
+<script>
+import { getButlerList, reviewButler } from '@/api/butler'
+
+export default {
+  data() {
+    return {
+      list: [], loading: false, statusFilter: 'pending',
+      rejectDialogVisible: false, rejectTarget: null, rejectReason: ''
+    }
+  },
+  mounted() { this.loadData() },
+  methods: {
+    statusType(s) { return { pending: 'warning', approved: 'success', rejected: 'danger' }[s] || 'info' },
+    statusLabel(s) { return { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[s] || s },
+    async loadData() {
+      this.loading = true
+      try {
+        const res = await getButlerList({ status: this.statusFilter || undefined, page: 1, size: 50 })
+        this.list = res.data && res.data.list || res.data || []
+      } catch (e) { this.$message.error('加载失败')
+      } finally { this.loading = false }
+    },
+    async handleReview(row, action) {
+      try {
+        await reviewButler({ id: row.id, action })
+        this.$message.success('审核完成')
+        this.loadData()
+      } catch (e) { this.$message.error('操作失败') }
+    },
+    showReject(row) { this.rejectTarget = row; this.rejectReason = ''; this.rejectDialogVisible = true },
+    async confirmReject() {
+      if (!this.rejectReason) return this.$message.warning('请填写拒绝原因')
+      try {
+        await reviewButler({ id: this.rejectTarget.id, action: 'reject' })
+        this.$message.success('已拒绝')
+        this.rejectDialogVisible = false
+        this.loadData()
+      } catch (e) { this.$message.error('操作失败') }
+    }
+  }
+}
+</script>
+<style scoped>
+.toolbar { display: flex; gap: 12px; align-items: center; margin-bottom: 16px; }
+.no-op { color: #ccc; }
+</style>

+ 92 - 0
cfc-web/src/views/admin/review/GuideAudit.vue

@@ -0,0 +1,92 @@
+<template>
+  <div>
+    <div class="toolbar">
+      <el-select v-model="statusFilter" placeholder="审核状态" @change="loadData">
+        <el-option label="全部" value="" />
+        <el-option label="待审核" value="pending" />
+        <el-option label="已通过" value="approved" />
+        <el-option label="已拒绝" value="rejected" />
+      </el-select>
+      <el-input v-model="keyword" placeholder="搜索..." prefix-icon="el-icon-search" clearable @keyup.enter.native="handleSearch" @clear="handleSearch" style="width:200px" />
+      <el-button size="small" @click="loadData" :loading="loading">刷新</el-button>
+    </div>
+    <el-table :data="list" v-loading="loading" border stripe>
+      <el-table-column prop="id" label="ID" width="60" />
+      <el-table-column prop="nickname" label="昵称" width="120" />
+      <el-table-column prop="realName" label="真实姓名" width="120" />
+      <el-table-column prop="phone" label="手机号" width="140" />
+      <el-table-column label="审核状态" width="100">
+        <template slot-scope="{ row }">
+          <el-tag :type="statusType(row.teacherStatus)">{{ statusLabel(row.teacherStatus) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="180" fixed="right">
+        <template slot-scope="{ row }">
+          <template v-if="row.teacherStatus === 'pending'">
+            <el-button size="mini" type="success" @click="handleApprove(row)">通过</el-button>
+            <el-button size="mini" type="danger" @click="showReject(row)">拒绝</el-button>
+          </template>
+          <span v-else class="no-op">—</span>
+        </template>
+      </el-table-column>
+    </el-table>
+    <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>
+  </div>
+</template>
+
+<script>
+import { getGuideApplications, approveGuide, rejectGuide } from '@/api/admin'
+
+export default {
+  data() {
+    return {
+      list: [], loading: false, keyword: '', statusFilter: 'pending',
+      rejectDialogVisible: false, rejectTarget: null, rejectReason: ''
+    }
+  },
+  mounted() { this.loadData() },
+  methods: {
+    statusType(s) { return { pending: 'warning', approved: 'success', rejected: 'danger' }[s] || 'info' },
+    statusLabel(s) { return { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[s] || s },
+    handleSearch() { this.loadData() },
+    async loadData() {
+      this.loading = true
+      try {
+        const res = await getGuideApplications({
+          status: this.statusFilter || undefined,
+          keyword: this.keyword || undefined, page: 1, size: 50
+        })
+        this.list = res.data && res.data.list || res.data || []
+      } catch (e) { this.$message.error('加载失败')
+      } finally { this.loading = false }
+    },
+    async handleApprove(row) {
+      try {
+        await approveGuide(row.id)
+        this.$message.success('已通过')
+        this.loadData()
+      } catch (e) { this.$message.error('操作失败') }
+    },
+    showReject(row) { this.rejectTarget = row; this.rejectReason = ''; this.rejectDialogVisible = true },
+    async confirmReject() {
+      if (!this.rejectReason) return this.$message.warning('请填写拒绝原因')
+      try {
+        await rejectGuide(this.rejectTarget.id, this.rejectReason)
+        this.$message.success('已拒绝')
+        this.rejectDialogVisible = false
+        this.loadData()
+      } catch (e) { this.$message.error('操作失败') }
+    }
+  }
+}
+</script>
+<style scoped>
+.toolbar { display: flex; gap: 12px; align-items: center; margin-bottom: 16px; }
+.no-op { color: #ccc; }
+</style>

+ 85 - 0
cfc-web/src/views/admin/review/HierarchyAudit.vue

@@ -0,0 +1,85 @@
+<template>
+  <div>
+    <div class="toolbar">
+      <el-select v-model="statusFilter" placeholder="审核状态" @change="loadData">
+        <el-option label="全部" value="" />
+        <el-option label="待审核" value="pending" />
+        <el-option label="已通过" value="approved" />
+        <el-option label="已拒绝" value="rejected" />
+      </el-select>
+      <el-button size="small" @click="loadData" :loading="loading">刷新</el-button>
+    </div>
+    <el-table :data="list" v-loading="loading" border stripe>
+      <el-table-column prop="id" label="ID" width="60" />
+      <el-table-column prop="applicantName" label="申请人" width="120" />
+      <el-table-column prop="currentSuperior" label="当前上级" width="120" />
+      <el-table-column prop="targetSuperior" label="目标上级" width="120" />
+      <el-table-column prop="reason" label="申请原因" min-width="200" show-overflow-tooltip />
+      <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="180" fixed="right">
+        <template slot-scope="{ row }">
+          <template v-if="row.status === 'pending'">
+            <el-button size="mini" type="success" @click="handleReview(row, 'approved')">通过</el-button>
+            <el-button size="mini" type="danger" @click="showReject(row)">拒绝</el-button>
+          </template>
+          <span v-else class="no-op">—</span>
+        </template>
+      </el-table-column>
+    </el-table>
+    <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>
+  </div>
+</template>
+
+<script>
+import request from '@/utils/request'
+import { reviewSupplyHierarchyChange } from '@/api/admin'
+
+export default {
+  data() {
+    return {
+      list: [], loading: false, statusFilter: 'pending',
+      rejectDialogVisible: false, rejectTarget: null, rejectReason: ''
+    }
+  },
+  mounted() { this.loadData() },
+  methods: {
+    statusType(s) { return { pending: 'warning', approved: 'success', rejected: 'danger' }[s] || 'info' },
+    statusLabel(s) { return { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[s] || s },
+    async loadData() {
+      this.loading = true
+      try {
+        const res = await request({ url: '/api/admin/supply-hierarchy/change/list', method: 'post', data: { status: this.statusFilter || undefined } })
+        this.list = res.data || []
+      } catch (e) { this.$message.error('加载失败')
+      } finally { this.loading = false }
+    },
+    async handleReview(row, status) {
+      try {
+        await reviewSupplyHierarchyChange({ id: row.id, status, rejectReason: status === 'rejected' ? this.rejectReason : '' })
+        this.$message.success('审核完成')
+        this.rejectDialogVisible = false
+        this.loadData()
+      } catch (e) { this.$message.error('操作失败') }
+    },
+    showReject(row) { this.rejectTarget = row; this.rejectReason = ''; this.rejectDialogVisible = true },
+    confirmReject() {
+      if (!this.rejectReason) return this.$message.warning('请填写拒绝原因')
+      this.handleReview(this.rejectTarget, 'rejected')
+    }
+  }
+}
+</script>
+<style scoped>
+.toolbar { display: flex; gap: 12px; align-items: center; margin-bottom: 16px; }
+.no-op { color: #ccc; }
+</style>

+ 93 - 0
cfc-web/src/views/admin/review/PackageAudit.vue

@@ -0,0 +1,93 @@
+<template>
+  <div>
+    <div class="toolbar">
+      <el-select v-model="statusFilter" placeholder="审核状态" @change="loadData">
+        <el-option label="全部" value="" />
+        <el-option label="待审核" value="pending" />
+        <el-option label="已通过" value="approved" />
+        <el-option label="已拒绝" value="rejected" />
+      </el-select>
+      <el-button size="small" @click="loadData" :loading="loading">刷新</el-button>
+    </div>
+    <el-table :data="list" v-loading="loading" border stripe>
+      <el-table-column prop="id" label="ID" width="60" />
+      <el-table-column prop="name" label="套餐名称" min-width="180" show-overflow-tooltip />
+      <el-table-column prop="cycleDays" label="周期" width="80">
+        <template slot-scope="{ row }">{{ row.cycleDays }}天</template>
+      </el-table-column>
+      <el-table-column prop="price" label="价格" width="100">
+        <template slot-scope="{ row }">{{ formatPrice(row.price) }}</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="180" fixed="right">
+        <template slot-scope="{ row }">
+          <template v-if="row.status === 'pending'">
+            <el-button size="mini" type="success" @click="handleApprove(row)">通过</el-button>
+            <el-button size="mini" type="danger" @click="showReject(row)">拒绝</el-button>
+          </template>
+          <span v-else class="no-op">—</span>
+        </template>
+      </el-table-column>
+    </el-table>
+    <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>
+  </div>
+</template>
+
+<script>
+import { getPendingPackages, approvePackage, rejectPackage } from '@/api/package'
+
+export default {
+  data() {
+    return {
+      list: [], loading: false, statusFilter: 'pending',
+      rejectDialogVisible: false, rejectTarget: null, rejectReason: ''
+    }
+  },
+  mounted() { this.loadData() },
+  methods: {
+    statusType(s) { return { pending: 'warning', approved: 'success', rejected: 'danger' }[s] || 'info' },
+    statusLabel(s) { return { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[s] || s },
+    formatPrice(p) { if (p == null) return '-'; return '¥' + (p / 100).toFixed(2) },
+    async loadData() {
+      this.loading = true
+      try {
+        const res = await getPendingPackages()
+        var data = res.data || []
+        this.list = this.statusFilter ? data.filter(function(d) { return d.status === this.statusFilter }.bind(this)) : data
+      } catch (e) { this.$message.error('加载失败')
+      } finally { this.loading = false }
+    },
+    async handleApprove(row) {
+      try {
+        await approvePackage(row.id)
+        this.$message.success('已通过')
+        this.loadData()
+      } catch (e) { this.$message.error('操作失败') }
+    },
+    showReject(row) { this.rejectTarget = row; this.rejectReason = ''; this.rejectDialogVisible = true },
+    async confirmReject() {
+      if (!this.rejectReason) return this.$message.warning('请填写拒绝原因')
+      try {
+        await rejectPackage(this.rejectTarget.id, this.rejectReason)
+        this.$message.success('已拒绝')
+        this.rejectDialogVisible = false
+        this.loadData()
+      } catch (e) { this.$message.error('操作失败') }
+    }
+  }
+}
+</script>
+<style scoped>
+.toolbar { display: flex; gap: 12px; align-items: center; margin-bottom: 16px; }
+.no-op { color: #ccc; }
+</style>

+ 82 - 0
cfc-web/src/views/admin/review/ProductAudit.vue

@@ -0,0 +1,82 @@
+<template>
+  <div>
+    <div class="toolbar">
+      <el-select v-model="statusFilter" placeholder="审核状态" @change="loadData">
+        <el-option label="全部" value="" />
+        <el-option label="待审核" value="pending" />
+        <el-option label="已通过" value="approved" />
+        <el-option label="已拒绝" value="rejected" />
+      </el-select>
+      <el-button size="small" @click="loadData" :loading="loading">刷新</el-button>
+    </div>
+    <el-table :data="list" v-loading="loading" border stripe>
+      <el-table-column prop="id" label="ID" width="60" />
+      <el-table-column prop="name" label="商品名称" min-width="180" show-overflow-tooltip />
+      <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 prop="rejectReason" label="原因" width="160" show-overflow-tooltip />
+      <el-table-column label="操作" width="180" fixed="right">
+        <template slot-scope="{ row }">
+          <template v-if="row.status === 'pending'">
+            <el-button size="mini" type="success" @click="handleReview(row, 'approve')">通过</el-button>
+            <el-button size="mini" type="danger" @click="showReject(row)">拒绝</el-button>
+          </template>
+          <span v-else class="no-op">—</span>
+        </template>
+      </el-table-column>
+    </el-table>
+    <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>
+  </div>
+</template>
+
+<script>
+import { getProductList, reviewProduct } from '@/api/admin'
+
+export default {
+  data() {
+    return {
+      list: [], loading: false, statusFilter: 'pending',
+      rejectDialogVisible: false, rejectTarget: null, rejectReason: ''
+    }
+  },
+  mounted() { this.loadData() },
+  methods: {
+    statusType(s) { return { pending: 'warning', approved: 'success', rejected: 'danger' }[s] || 'info' },
+    statusLabel(s) { return { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[s] || s },
+    async loadData() {
+      this.loading = true
+      try {
+        const res = await getProductList({ status: this.statusFilter || undefined, page: 1, size: 50 })
+        this.list = res.data && res.data.list || res.data || []
+      } catch (e) { this.$message.error('加载失败')
+      } finally { this.loading = false }
+    },
+    async handleReview(row, action) {
+      try {
+        await reviewProduct(row.id, { action, rejectReason: action === 'reject' ? this.rejectReason : '' })
+        this.$message.success('审核完成')
+        this.rejectDialogVisible = false
+        this.loadData()
+      } catch (e) { this.$message.error('操作失败') }
+    },
+    showReject(row) { this.rejectTarget = row; this.rejectReason = ''; this.rejectDialogVisible = true },
+    confirmReject() {
+      if (!this.rejectReason) return this.$message.warning('请填写拒绝原因')
+      this.handleReview(this.rejectTarget, 'reject')
+    }
+  }
+}
+</script>
+<style scoped>
+.toolbar { display: flex; gap: 12px; align-items: center; margin-bottom: 16px; }
+.no-op { color: #ccc; }
+</style>

+ 94 - 0
cfc-web/src/views/admin/review/VendorAudit.vue

@@ -0,0 +1,94 @@
+<template>
+  <div>
+    <div class="toolbar">
+      <el-select v-model="statusFilter" placeholder="审核状态" @change="loadData">
+        <el-option label="全部" value="" />
+        <el-option label="待审核" value="pending" />
+        <el-option label="已通过" value="approved" />
+        <el-option label="已拒绝" value="rejected" />
+      </el-select>
+      <el-input v-model="keyword" placeholder="搜索..." prefix-icon="el-icon-search" clearable @keyup.enter.native="handleSearch" @clear="handleSearch" style="width:200px" />
+      <el-button size="small" @click="loadData" :loading="loading">刷新</el-button>
+    </div>
+    <el-table :data="list" v-loading="loading" border stripe>
+      <el-table-column prop="id" label="ID" width="60" />
+      <el-table-column prop="nickname" label="昵称" width="120" />
+      <el-table-column prop="realName" label="姓名" width="120" />
+      <el-table-column prop="phone" label="手机号" width="140" />
+      <el-table-column label="类型" width="120">
+        <template slot-scope="{ row }">{{ vendorTypeLabel(row.vendorType) }}</template>
+      </el-table-column>
+      <el-table-column label="审核状态" width="100">
+        <template slot-scope="{ row }">
+          <el-tag :type="statusType(row.vendorStatus)">{{ statusLabel(row.vendorStatus) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="180" fixed="right">
+        <template slot-scope="{ row }">
+          <template v-if="row.vendorStatus === 'pending'">
+            <el-button size="mini" type="success" @click="handleReview(row, 'approve')">通过</el-button>
+            <el-button size="mini" type="danger" @click="showReject(row)">拒绝</el-button>
+          </template>
+          <span v-else class="no-op">—</span>
+        </template>
+      </el-table-column>
+    </el-table>
+    <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>
+  </div>
+</template>
+
+<script>
+import { getVendorList, reviewVendor } from '@/api/vendor'
+
+export default {
+  data() {
+    return {
+      list: [], loading: false, keyword: '', statusFilter: 'pending',
+      rejectDialogVisible: false, rejectTarget: null, rejectReason: ''
+    }
+  },
+  mounted() { this.loadData() },
+  methods: {
+    vendorTypeLabel(t) { return { planner: '成长规划师', activity_provider: '活动提供商', product_supplier: '商品供应商' }[t] || t },
+    statusType(s) { return { pending: 'warning', approved: 'success', rejected: 'danger' }[s] || 'info' },
+    statusLabel(s) { return { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[s] || s },
+    handleSearch() { this.loadData() },
+    async loadData() {
+      this.loading = true
+      try {
+        const res = await getVendorList({ status: this.statusFilter, keyword: this.keyword || undefined, page: 1, size: 50 })
+        this.list = res.data && res.data.list || []
+        this.total = res.data && res.data.total || 0
+      } catch (e) { this.$message.error('加载失败')
+      } finally { this.loading = false }
+    },
+    async handleReview(row, action) {
+      try {
+        await reviewVendor({ userId: row.id, action })
+        this.$message.success('审核完成')
+        this.loadData()
+      } catch (e) { this.$message.error('操作失败') }
+    },
+    showReject(row) { this.rejectTarget = row; this.rejectReason = ''; this.rejectDialogVisible = true },
+    async confirmReject() {
+      if (!this.rejectReason) return this.$message.warning('请填写拒绝原因')
+      try {
+        await reviewVendor({ userId: this.rejectTarget.id, action: 'reject' })
+        this.$message.success('已拒绝')
+        this.rejectDialogVisible = false
+        this.loadData()
+      } catch (e) { this.$message.error('操作失败') }
+    }
+  }
+}
+</script>
+<style scoped>
+.toolbar { display: flex; gap: 12px; align-items: center; margin-bottom: 16px; }
+.no-op { color: #ccc; }
+</style>

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

@@ -0,0 +1,92 @@
+<template>
+  <div>
+    <div class="toolbar">
+      <el-select v-model="statusFilter" placeholder="审核状态" @change="loadData">
+        <el-option label="全部" value="" />
+        <el-option label="待审核" value="pending" />
+        <el-option label="已通过" value="approved" />
+        <el-option label="已拒绝" value="rejected" />
+      </el-select>
+      <el-input v-model="keyword" placeholder="搜索..." prefix-icon="el-icon-search" clearable @keyup.enter.native="handleSearch" @clear="handleSearch" style="width:200px" />
+      <el-button size="small" @click="loadData" :loading="loading">刷新</el-button>
+    </div>
+    <el-table :data="list" v-loading="loading" border stripe>
+      <el-table-column prop="id" label="ID" width="60" />
+      <el-table-column label="用户" width="120">
+        <template slot-scope="{ row }">{{ row.nickname || row.userName || '-' }}</template>
+      </el-table-column>
+      <el-table-column label="金额" width="100">
+        <template slot-scope="{ row }">{{ formatPrice(row.amount) }}</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="180" fixed="right">
+        <template slot-scope="{ row }">
+          <template v-if="row.status === 'pending'">
+            <el-button size="mini" type="success" @click="handleAudit(row, 'approved')">通过</el-button>
+            <el-button size="mini" type="danger" @click="showReject(row)">拒绝</el-button>
+          </template>
+          <span v-else class="no-op">—</span>
+        </template>
+      </el-table-column>
+    </el-table>
+    <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>
+  </div>
+</template>
+
+<script>
+import { getWithdrawalList, auditWithdrawal } from '@/api/admin'
+
+export default {
+  data() {
+    return {
+      list: [], loading: false, keyword: '', statusFilter: 'pending',
+      rejectDialogVisible: false, rejectTarget: null, rejectReason: ''
+    }
+  },
+  mounted() { this.loadData() },
+  methods: {
+    statusType(s) { return { pending: 'warning', approved: 'success', rejected: 'danger' }[s] || 'info' },
+    statusLabel(s) { return { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[s] || s },
+    formatPrice(p) { if (p == null) return '-'; return '¥' + (p / 100).toFixed(2) },
+    handleSearch() { this.loadData() },
+    async loadData() {
+      this.loading = true
+      try {
+        const res = await getWithdrawalList({ status: this.statusFilter, keyword: this.keyword || undefined })
+        this.list = res.data || []
+      } catch (e) { this.$message.error('加载失败')
+      } finally { this.loading = false }
+    },
+    async handleAudit(row, status) {
+      try {
+        await this.$confirm(status === 'approved' ? '确认通过该提现申请?' : '确认拒绝该提现申请?', '提示')
+        await auditWithdrawal(row.id, status, status === 'rejected' ? this.rejectReason : '')
+        this.$message.success('操作成功')
+        this.rejectDialogVisible = false
+        this.loadData()
+      } catch (e) {
+        if (e !== 'cancel') this.$message.error('操作失败')
+      }
+    },
+    showReject(row) { this.rejectTarget = row; this.rejectReason = ''; this.rejectDialogVisible = true },
+    confirmReject() { this.handleAudit(this.rejectTarget, 'rejected') }
+  }
+}
+</script>
+<style scoped>
+.toolbar { display: flex; gap: 12px; align-items: center; margin-bottom: 16px; }
+.no-op { color: #ccc; }
+</style>