Преглед на файлове

feat(review): 审核中心 12 Tab 统一详情抽屉+服务角色按角色分流合并,新增知识管理员 Tab

Xiaogang Liao преди 5 дни
родител
ревизия
08eedcb1a8
променени са 22 файла, в които са добавени 1172 реда и са изтрити 695 реда
  1. 48 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminButlerController.java
  2. 2 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCommissionController.java
  3. 17 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ButlerService.java
  4. 6 1
      cfc-backend/src/main/java/com/etotem/cfc/service/PendingRefundService.java
  5. 9 0
      cfc-web/src/api/butler.js
  6. 2 3
      cfc-web/src/router/index.js
  7. 0 1
      cfc-web/src/views/Layout.vue
  8. 8 2
      cfc-web/src/views/admin/ReviewCenter.vue
  9. 40 23
      cfc-web/src/views/admin/review/ActivityAudit.vue
  10. 43 13
      cfc-web/src/views/admin/review/ActivityRegistrationAudit.vue
  11. 26 57
      cfc-web/src/views/admin/review/ArticleAudit.vue
  12. 170 84
      cfc-web/src/views/admin/review/ButlerAudit.vue
  13. 133 64
      cfc-web/src/views/admin/review/GuideAudit.vue
  14. 38 23
      cfc-web/src/views/admin/review/HierarchyAudit.vue
  15. 156 0
      cfc-web/src/views/admin/review/KnowledgeAdminAudit.vue
  16. 139 57
      cfc-web/src/views/admin/review/NutritionistAudit.vue
  17. 40 73
      cfc-web/src/views/admin/review/PackageAudit.vue
  18. 39 9
      cfc-web/src/views/admin/review/PendingRefundAudit.vue
  19. 50 25
      cfc-web/src/views/admin/review/ProductAudit.vue
  20. 159 54
      cfc-web/src/views/admin/review/VendorAudit.vue
  21. 47 15
      cfc-web/src/views/admin/review/WithdrawAudit.vue
  22. 0 190
      cfc-web/src/views/service/ServiceRoleApplications.vue

+ 48 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminButlerController.java

@@ -6,14 +6,18 @@ import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.ButlerCommissionSettlement;
 import com.etotem.cfc.entity.ButlerProfile;
 import com.etotem.cfc.entity.ButlerServiceRecord;
+import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.ButlerCommissionSettlementMapper;
 import com.etotem.cfc.mapper.ButlerProfileMapper;
 import com.etotem.cfc.mapper.ButlerServiceRecordMapper;
+import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.ButlerService;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import com.etotem.cfc.util.SortUtil;
 import com.etotem.cfc.util.ParamUtils;
@@ -34,6 +38,50 @@ public class AdminButlerController {
     @Resource
     private ButlerCommissionSettlementMapper butlerCommissionSettlementMapper;
 
+    @Resource
+    private UserMapper userMapper;
+
+    /**
+     * 管家申请来源为 users.butler_status(小程序服务角色申请/管家申请写入)。
+     * 返回用户维度的管家申请列表,供审核中心管家审核 Tab 使用。
+     */
+    @PostMapping("/user-list")
+    public Result<Map<String, Object>> userList(@RequestBody Map<String, Object> params,
+                                                @RequestAttribute("role") String role) {
+        if (!"admin".equals(role) && !"finance".equals(role)) {
+            return Result.error("权限不足");
+        }
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
+        String status = (String) params.get("status");
+
+        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
+        if (status != null && !status.isEmpty()) {
+            wrapper.eq(User::getButlerStatus, status);
+        } else {
+            wrapper.isNotNull(User::getButlerStatus);
+        }
+        wrapper.orderByDesc(User::getUpdatedAt);
+
+        Page<User> result = userMapper.selectPage(new Page<>(page, size), wrapper);
+        List<Map<String, Object>> list = new ArrayList<>();
+        for (User u : result.getRecords()) {
+            Map<String, Object> m = new HashMap<>();
+            m.put("userId", u.getId());
+            m.put("nickname", u.getNickname());
+            m.put("realName", u.getRealName());
+            m.put("phone", u.getPhone());
+            m.put("butlerStatus", u.getButlerStatus());
+            m.put("butlerInfo", u.getButlerInfo());
+            m.put("updatedAt", u.getUpdatedAt());
+            list.add(m);
+        }
+        Map<String, Object> data = new HashMap<>();
+        data.put("list", list);
+        data.put("total", result.getTotal());
+        return Result.success(data);
+    }
+
     @PostMapping("/list")
     public Result<Map<String, Object>> list(@RequestBody Map<String, Object> params,
                                              @RequestAttribute("role") String role) {

+ 2 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCommissionController.java

@@ -97,7 +97,8 @@ public class AdminCommissionController {
         if (refundId == null) {
             return Result.error("refundId不能为空");
         }
-        pendingRefundService.cancelPendingRefund(refundId);
+        String reason = params.get("reason") != null ? params.get("reason").toString() : null;
+        pendingRefundService.cancelPendingRefund(refundId, reason);
         return Result.success("已中止退款");
     }
 }

+ 17 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ButlerService.java

@@ -97,6 +97,23 @@ public class ButlerService {
         userMapper.updateById(user);
         // B-2 审核通过回写 users.roles(旧端点兼容;butler 单向兼容权限由前端权限并集处理)
         userService.addRole(userId, "butler");
+        // 补齐/更新 butler_profiles,状态统一 approved(分配成员依赖该档案)
+        ButlerProfile profile = butlerProfileMapper.selectOne(new LambdaQueryWrapper<ButlerProfile>()
+                .eq(ButlerProfile::getUserId, userId));
+        if (profile == null) {
+            profile = new ButlerProfile();
+            profile.setUserId(userId);
+            profile.setStatus("approved");
+            profile.setApprovedAt(new Date());
+            profile.setCreatedAt(new Date());
+            profile.setUpdatedAt(new Date());
+            butlerProfileMapper.insert(profile);
+        } else {
+            profile.setStatus("approved");
+            profile.setApprovedAt(new Date());
+            profile.setUpdatedAt(new Date());
+            butlerProfileMapper.updateById(profile);
+        }
         return Result.success("审核通过");
     }
 

+ 6 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/PendingRefundService.java

@@ -133,10 +133,15 @@ public class PendingRefundService {
      */
     @Transactional
     public void cancelPendingRefund(Long refundId) {
+        cancelPendingRefund(refundId, null);
+    }
+
+    @Transactional
+    public void cancelPendingRefund(Long refundId, String remark) {
         PendingRefund refund = pendingRefundMapper.selectById(refundId);
         if (refund != null) {
             refund.setStatus("cancelled");
-            refund.setRemark("管理员中止退款");
+            refund.setRemark(remark != null && !remark.trim().isEmpty() ? remark.trim() : "管理员中止退款");
             refund.setUpdatedAt(new Date());
             pendingRefundMapper.updateById(refund);
 

+ 9 - 0
cfc-web/src/api/butler.js

@@ -9,6 +9,15 @@ export function getButlerList(params) {
   })
 }
 
+// 管家申请列表(用户维度,来源 users.butler_status)
+export function getButlerUserList(params) {
+  return request({
+    url: '/api/admin/butler/user-list',
+    method: 'post',
+    data: params
+  })
+}
+
 // 审核管家
 export function reviewButler(data) {
   return request({

+ 2 - 3
cfc-web/src/router/index.js

@@ -210,12 +210,11 @@ const routes = [
         component: () => import('@/views/admin/MenuManage.vue'),
         meta: { title: '菜单管理', perm: 'system:menu' }
       },
-      // 服务管理(管理员)
+      // 服务角色申请审核已合并进审核中心,旧路由重定向
       {
         path: 'service-role-applications',
         name: 'ServiceRoleApplications',
-        component: () => import('@/views/service/ServiceRoleApplications.vue'),
-        meta: { title: '服务角色申请审核', perm: 'audit' }
+        redirect: '/review-center?tab=vendor'
       },
       {
         path: 'teacher-messages',

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

@@ -202,7 +202,6 @@ export default {
         { title: '审核中心', icon: 'el-icon-s-check', perm: 'audit',
           children: [
             { path: '/review-center', label: '审核中心总览', icon: 'el-icon-s-check', perm: 'audit' },
-            { path: '/service-role-applications', label: '服务角色申请审核', icon: 'el-icon-s-check', perm: 'audit:service-role' },
             { path: '/activity-review', label: '活动审核', icon: 'el-icon-document-checked', perm: 'audit:activity' },
             { path: '/pending-refund', label: '待退款管理', icon: 'el-icon-warning', perm: 'audit:refund' },
             { path: '/family-earnings-withdraw', label: '收益提现审核', icon: 'el-icon-document-checked', perm: 'audit:withdraw' },

+ 8 - 2
cfc-web/src/views/admin/ReviewCenter.vue

@@ -51,6 +51,9 @@
         <el-tab-pane label="更换上级审核" name="hierarchy">
           <hierarchy-audit ref="hierarchy" />
         </el-tab-pane>
+        <el-tab-pane label="知识管理员审核" name="knowledge">
+          <knowledge-admin-audit ref="knowledge" />
+        </el-tab-pane>
       </el-tabs>
     </el-card>
   </div>
@@ -70,13 +73,14 @@ import WithdrawAudit from './review/WithdrawAudit.vue'
 import HierarchyAudit from './review/HierarchyAudit.vue'
 import PendingRefundAudit from './review/PendingRefundAudit.vue'
 import NutritionistAudit from './review/NutritionistAudit.vue'
+import KnowledgeAdminAudit from './review/KnowledgeAdminAudit.vue'
 
 export default {
   name: 'ReviewCenter',
   components: {
     ArticleAudit, ProductAudit, GuideAudit, VendorAudit, ButlerAudit,
     PackageAudit, ActivityAudit, ActivityRegistrationAudit, WithdrawAudit, HierarchyAudit,
-    NutritionistAudit, PendingRefundAudit
+    NutritionistAudit, PendingRefundAudit, KnowledgeAdminAudit
   },
   data() {
     return {
@@ -95,6 +99,7 @@ export default {
         { key: 'refund', label: '待退款审核', count: 0 },
         { key: 'hierarchy', label: '更换上级', count: 0 },
         { key: 'nutritionist', label: '营养师审核', count: 0 },
+        { key: 'knowledge', label: '知识管理员审核', count: 0 },
       ]
     }
   },
@@ -117,7 +122,8 @@ export default {
               nutritionist: 'nutritionistPending',
               activity: 'activityPending', activityRegistration: 'activityRegistrationPending',
               withdraw: 'withdrawPending', refund: 'refundPending',
-              hierarchy: 'hierarchyChangePending'
+              hierarchy: 'hierarchyChangePending',
+              knowledge: 'knowledgePending'
             }
             c.count = res.data[keyMap[c.key]] || 0
           })

+ 40 - 23
cfc-web/src/views/admin/review/ActivityAudit.vue

@@ -32,34 +32,50 @@
           <span class="sort-header" @click.stop="onSortClick('auditReason', $event)">原因<span v-if="sortIndex('auditReason') !== null" class="sort-index" :class="sortClass('auditReason') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('auditReason') }}</span>{{ sortIcon('auditReason') }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="操作" width="180" fixed="right">
+      <el-table-column label="操作" width="120" 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>
+          <el-button size="mini" type="primary" @click="showDetail(row)">{{ row.auditStatus === 'pending' ? '审核' : '查看' }}</el-button>
         </template>
       </el-table-column>
     </el-table>
     </div>
 
-    <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>
+    <audit-detail-drawer
+      :visible.sync="detailVisible"
+      title="活动审核详情"
+      :status="currentRow ? currentRow.auditStatus : ''"
+      :loading="auditLoading"
+      @approve="handleApprove"
+      @reject="handleReject">
+      <el-form v-if="currentRow" label-width="100px" size="small">
+        <el-form-item label="ID">{{ currentRow.id }}</el-form-item>
+        <el-form-item label="活动名称">{{ currentRow.title }}</el-form-item>
+        <el-form-item label="活动封面" v-if="currentRow.coverImage || currentRow.cover">
+          <img :src="currentRow.coverImage || currentRow.cover" style="max-width:220px;border-radius:4px;" />
+        </el-form-item>
+        <el-form-item label="开始时间">{{ currentRow.startTime || currentRow.activityStartTime || '-' }}</el-form-item>
+        <el-form-item label="结束时间">{{ currentRow.endTime || currentRow.activityEndTime || '-' }}</el-form-item>
+        <el-form-item label="活动地点">{{ currentRow.location || currentRow.address || '-' }}</el-form-item>
+        <el-form-item label="主办方">{{ currentRow.organizer || currentRow.vendorName || '-' }}</el-form-item>
+        <el-form-item label="活动内容">
+          <div style="white-space:pre-wrap;">{{ currentRow.description || currentRow.content || '-' }}</div>
+        </el-form-item>
+        <el-form-item label="驳回原因" v-if="currentRow.auditReason" style="color:#f56c6c;">
+          {{ currentRow.auditReason }}
+        </el-form-item>
+      </el-form>
+    </audit-detail-drawer>
   </div>
 </template>
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
 import { getActivityList, auditActivity } from '@/api/activity'
+import AuditDetailDrawer from './components/AuditDetailDrawer.vue'
 
 export default {
   name: 'ActivityAudit',
+  components: { AuditDetailDrawer },
   mixins: [SortMixin],
   computed: {
     tableHeight() {
@@ -70,7 +86,7 @@ export default {
     return {
       list: [], loading: false, keyword: '', statusFilter: 'pending',
       sortableColumns: { id: 'ID', title: '活动名称', auditReason: '原因' },
-      rejectDialogVisible: false, rejectTarget: null, rejectReason: ''
+      detailVisible: false, currentRow: null, auditLoading: false
     }
   },
   mounted() { this.loadData() },
@@ -89,18 +105,19 @@ export default {
       } catch (e) { this.$message.error('加载失败')
       } finally { this.loading = false }
     },
-    async handleReview(row, action) {
+    showDetail(row) { this.currentRow = row; this.detailVisible = true },
+    handleApprove() { this.handleReview('publish') },
+    handleReject(reason) { this.handleReview('reject', reason) },
+    async handleReview(action, reason) {
+      if (!this.currentRow) return
+      this.auditLoading = true
       try {
-        await auditActivity({ id: row.id, action, rejectReason: action === 'reject' ? this.rejectReason : '' })
+        await auditActivity({ id: this.currentRow.id, action, rejectReason: action === 'reject' ? reason : '' })
         this.$message.success('审核完成')
-        this.rejectDialogVisible = false
+        this.detailVisible = 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')
+      } catch (e) { this.$message.error('操作失败')
+      } finally { this.auditLoading = false }
     }
   }
 }

+ 43 - 13
cfc-web/src/views/admin/review/ActivityRegistrationAudit.vue

@@ -27,17 +27,35 @@
           <el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="操作" width="180" fixed="right">
+      <el-table-column label="操作" width="120" 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>
+          <el-button size="mini" type="primary" @click="showDetail(row)">{{ row.status === 'pending' ? '审核' : '查看' }}</el-button>
         </template>
       </el-table-column>
     </el-table>
     </div>
+
+    <audit-detail-drawer
+      :visible.sync="detailVisible"
+      title="活动报名审核详情"
+      :status="currentRow ? currentRow.status : ''"
+      :loading="auditLoading"
+      @approve="handleApprove"
+      @reject="handleReject">
+      <el-form v-if="currentRow" label-width="100px" size="small">
+        <el-form-item label="报名ID">{{ currentRow.id }}</el-form-item>
+        <el-form-item label="报名人">{{ currentRow.registrantName || currentRow.nickname || currentRow.userName || '-' }}</el-form-item>
+        <el-form-item label="联系方式">{{ currentRow.registrantPhone || currentRow.phone || '-' }}</el-form-item>
+        <el-form-item label="报名活动">{{ currentRow.activityTitle || currentRow.activityName || '-' }}</el-form-item>
+        <el-form-item label="报名时间">{{ currentRow.registeredAt || currentRow.createdAt || '-' }}</el-form-item>
+        <el-form-item label="审核状态">
+          <el-tag :type="statusType(currentRow.status)">{{ statusLabel(currentRow.status) }}</el-tag>
+        </el-form-item>
+        <el-form-item label="驳回原因" v-if="currentRow.rejectReason" style="color:#f56c6c;">
+          {{ currentRow.rejectReason }}
+        </el-form-item>
+      </el-form>
+    </audit-detail-drawer>
   </div>
 </template>
 
@@ -45,9 +63,11 @@
 import SortMixin from '@/mixins/SortMixin'
 import { getActivityList } from '@/api/activity'
 import { approveRegistration, rejectRegistration } from '@/api/activity'
+import AuditDetailDrawer from './components/AuditDetailDrawer.vue'
 
 export default {
   name: 'ActivityRegistrationAudit',
+  components: { AuditDetailDrawer },
   mixins: [SortMixin],
   computed: {
     tableHeight() {
@@ -57,7 +77,8 @@ export default {
   data() {
     return {
       list: [], loading: false, statusFilter: 'pending',
-      sortableColumns: { id: 'ID' }
+      sortableColumns: { id: 'ID' },
+      detailVisible: false, currentRow: null, auditLoading: false
     }
   },
   mounted() { this.loadData() },
@@ -87,19 +108,28 @@ export default {
       } catch (e) { this.$message.error('加载失败')
       } finally { this.loading = false }
     },
-    async handleApprove(row) {
+    showDetail(row) { this.currentRow = row; this.detailVisible = true },
+    async handleApprove() {
+      if (!this.currentRow) return
+      this.auditLoading = true
       try {
-        await approveRegistration({ id: row.id })
+        await approveRegistration({ registrationId: this.currentRow.id })
         this.$message.success('已通过')
+        this.detailVisible = false
         this.loadData()
-      } catch (e) { this.$message.error('操作失败') }
+      } catch (e) { this.$message.error('操作失败')
+      } finally { this.auditLoading = false }
     },
-    async handleReject(row) {
+    async handleReject(reason) {
+      if (!this.currentRow) return
+      this.auditLoading = true
       try {
-        await rejectRegistration({ id: row.id })
+        await rejectRegistration({ registrationId: this.currentRow.id, reason })
         this.$message.success('已拒绝')
+        this.detailVisible = false
         this.loadData()
-      } catch (e) { this.$message.error('操作失败') }
+      } catch (e) { this.$message.error('操作失败')
+      } finally { this.auditLoading = false }
     }
   }
 }

+ 26 - 57
cfc-web/src/views/admin/review/ArticleAudit.vue

@@ -41,14 +41,9 @@
           <span class="sort-header" @click.stop="onSortClick('createdAt', $event)">申请时间<span v-if="sortIndex('createdAt') !== null" class="sort-index" :class="sortClass('createdAt') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('createdAt') }}</span>{{ sortIcon('createdAt') }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="操作" width="200" fixed="right">
+      <el-table-column label="操作" width="120" fixed="right">
         <template slot-scope="{ row }">
-          <template v-if="row.auditStatus === 'pending'">
-            <el-button size="mini" type="primary" @click="showDetail(row)">查看</el-button>
-            <el-button size="mini" type="success" @click="handleAudit(row, 'approved')">通过</el-button>
-            <el-button size="mini" type="danger" @click="showRejectDialog(row)">驳回</el-button>
-          </template>
-          <el-button v-else size="mini" type="primary" @click="showDetail(row)">查看</el-button>
+          <el-button size="mini" type="primary" @click="showDetail(row)">{{ row.auditStatus === 'pending' ? '审核' : '查看' }}</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -64,8 +59,14 @@
       style="margin-top:12px;text-align:right;"
     />
 
-    <el-dialog title="文章详情" :visible.sync="detailVisible" width="800px" top="5vh">
-      <div v-if="currentRow" style="max-height:60vh;overflow-y:auto;">
+    <audit-detail-drawer
+      :visible.sync="detailVisible"
+      title="文章审核详情"
+      :status="currentRow ? currentRow.auditStatus : ''"
+      :loading="auditLoading"
+      @approve="handleApprove"
+      @reject="handleReject">
+      <div v-if="currentRow">
         <el-form label-width="100px" size="small">
           <el-form-item label="ID">{{ currentRow.id }}</el-form-item>
           <el-form-item label="标题">{{ currentRow.title }}</el-form-item>
@@ -83,23 +84,7 @@
           </el-form-item>
         </el-form>
       </div>
-      <span slot="footer" v-if="currentRow && currentRow.auditStatus === 'pending'">
-        <el-button @click="detailVisible = false">取消</el-button>
-        <el-button type="success" @click="handleAuditFromDialog">通过</el-button>
-        <el-button type="danger" @click="showRejectFromDialog">驳回</el-button>
-      </span>
-      <span slot="footer" v-else>
-        <el-button @click="detailVisible = false">关闭</el-button>
-      </span>
-    </el-dialog>
-
-    <el-dialog title="驳回原因" :visible.sync="rejectDialogVisible" width="450px">
-      <el-input v-model="rejectReason" type="textarea" :rows="3" placeholder="请输入驳回原因" />
-      <span slot="footer">
-        <el-button @click="rejectDialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="confirmReject">确认驳回</el-button>
-      </span>
-    </el-dialog>
+    </audit-detail-drawer>
 
     <el-dialog title="批量驳回" :visible.sync="batchRejectDialogVisible" width="450px">
       <el-input v-model="batchRejectReason" type="textarea" :rows="3" placeholder="请输入驳回原因" />
@@ -114,9 +99,11 @@
 <script>
 import SortMixin from '@/mixins/SortMixin'
 import { adminArticleList, adminArticleAudit } from '@/api/article'
+import AuditDetailDrawer from './components/AuditDetailDrawer.vue'
 
 export default {
   name: 'ArticleAudit',
+  components: { AuditDetailDrawer },
   mixins: [SortMixin],
   computed: {
     tableHeight() {
@@ -135,10 +122,9 @@ export default {
       sortableColumns: { id: 'ID', title: '标题', createdAt: '申请时间' },
       selectedRows: [],
       detailVisible: false,
-      rejectDialogVisible: false,
       batchRejectDialogVisible: false,
-      rejectReason: '',
       batchRejectReason: '',
+      auditLoading: false,
       currentRow: null
     }
   },
@@ -182,46 +168,29 @@ export default {
     },
     showDetail(row) {
       this.currentRow = row
-      this.rejectReason = ''
       this.detailVisible = true
     },
     handleView(row) {
       this.$router.push('/article-edit?id=' + row.id + '&readonly=1')
     },
-    handleAuditFromDialog() {
+    handleApprove() { this.handleAudit('approved') },
+    handleReject(reason) { this.handleAudit('rejected', reason) },
+    async handleAudit(action, reason) {
       if (!this.currentRow) return
-      this.handleAudit(this.currentRow, 'approved')
-      this.detailVisible = false
-    },
-    showRejectFromDialog() {
-      this.detailVisible = false
-      this.rejectDialogVisible = true
-    },
-    async handleAudit(row, action) {
+      this.auditLoading = true
       try {
-        await this.$confirm(action === 'approved' ? '确认通过审核?' : '确认驳回?', '提示', { type: 'warning' })
-        await adminArticleAudit({ id: row.id, auditStatus: action })
+        await adminArticleAudit({
+          id: this.currentRow.id,
+          auditStatus: action,
+          auditReason: action === 'rejected' ? reason : undefined
+        })
         this.$message.success(action === 'approved' ? '已通过' : '已驳回')
-        this.loadData()
-      } catch (e) {
-        if (e !== 'cancel') this.$message.error('操作失败')
-      }
-    },
-    showRejectDialog(row) {
-      this.currentRow = row
-      this.rejectReason = ''
-      this.rejectDialogVisible = true
-    },
-    async confirmReject() {
-      if (!this.rejectReason.trim()) return this.$message.warning('请填写驳回原因')
-      if (!this.currentRow) return
-      this.rejectDialogVisible = false
-      try {
-        await adminArticleAudit({ id: this.currentRow.id, auditStatus: 'rejected', auditReason: this.rejectReason })
-        this.$message.success('已驳回')
+        this.detailVisible = false
         this.loadData()
       } catch (e) {
         this.$message.error('操作失败')
+      } finally {
+        this.auditLoading = false
       }
     },
     showBatchRejectDialog() {

+ 170 - 84
cfc-web/src/views/admin/review/ButlerAudit.vue

@@ -10,23 +10,24 @@
       <el-button size="small" @click="loadData" :loading="loading">刷新</el-button>
     </div>
         <div class="adm-table-wrap">
-<el-table :data="list" v-loading="loading" border stripe style="min-width:900px;" height="100%">
-      <el-table-column prop="id" label="档案ID" width="80">
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('id', $event)">档案ID<span v-if="sortIndex('id') !== null" class="sort-index" :class="sortClass('id') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('id') }}</span>{{ sortIcon('id') }}</span>
-        </template>
+<el-table :data="pagedList" v-loading="loading" border stripe style="min-width:900px;" height="100%">
+      <el-table-column label="用户ID" width="80">
+        <template slot-scope="{ row }">{{ row.userId }}</template>
+      </el-table-column>
+      <el-table-column label="用户" width="120" show-overflow-tooltip>
+        <template slot-scope="{ row }">{{ row.nickname || row.realName || '用户' + row.userId }}</template>
       </el-table-column>
-      <el-table-column label="用户" width="120">
-          <template slot-scope="{ row }">{{ row.userName || '用户' + row.userId }}</template>
-        </el-table-column>
       <el-table-column prop="tier" label="等级" width="80">
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('tier', $event)">等级<span v-if="sortIndex('tier') !== null" class="sort-index" :class="sortClass('tier') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('tier') }}</span>{{ sortIcon('tier') }}</span>
-        </template>
+        <template slot-scope="{ row }">{{ row.tier || '-' }}</template>
       </el-table-column>
       <el-table-column prop="description" label="申请说明" min-width="200" show-overflow-tooltip>
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('description', $event)">申请说明<span v-if="sortIndex('description') !== null" class="sort-index" :class="sortClass('description') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('description') }}</span>{{ sortIcon('description') }}</span>
+        <template slot-scope="{ row }">{{ row.description || '-' }}</template>
+      </el-table-column>
+      <el-table-column label="来源" width="100">
+        <template slot-scope="{ row }">
+          <el-tag size="small" :type="row.source === 'service_role' ? 'success' : 'info'">
+            {{ row.source === 'service_role' ? '服务角色申请' : '用户资料' }}
+          </el-tag>
         </template>
       </el-table-column>
       <el-table-column label="审核状态" width="100">
@@ -34,55 +35,113 @@
           <el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="操作" width="180" fixed="right">
+      <el-table-column label="操作" width="120" 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>
+          <el-button size="mini" type="primary" @click="showDetail(row)">{{ row.status === 'pending' ? '审核' : '查看' }}</el-button>
         </template>
       </el-table-column>
       <el-table-column label="接单状态" width="110">
         <template slot-scope="{ row }">
-          <el-switch
-            :value="row.accepting !== 0"
-            active-color="#13ce66"
-            @change="onToggleAccepting(row)" />
-          <div v-if="row.accepting === 0" style="font-size: 12px; color: #f56c6c;">已停接</div>
+          <template v-if="row.status === 'approved'">
+            <el-switch
+              :value="row.accepting !== 0"
+              active-color="#13ce66"
+              @change="onToggleAccepting(row)" />
+            <div v-if="row.accepting === 0" style="font-size: 12px; color: #f56c6c;">已停接</div>
+          </template>
+          <span v-else style="color:#999">-</span>
         </template>
       </el-table-column>
     </el-table>
     </div>
 
-    <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-pagination
+      v-if="total > 0"
+      :current-page="page"
+      :page-size="size"
+      :total="total"
+      layout="total, prev, pager, next"
+      @current-change="onPageChange"
+      style="margin-top:12px;text-align:right;"
+    />
+
+    <audit-detail-drawer
+      :visible.sync="detailVisible"
+      title="管家审核详情"
+      :status="currentRow ? currentRow.status : ''"
+      :loading="auditLoading"
+      @approve="handleApprove"
+      @reject="handleReject">
+      <el-form v-if="currentRow" label-width="110px" size="small">
+        <el-form-item label="用户ID">{{ currentRow.userId }}</el-form-item>
+        <el-form-item label="昵称">{{ currentRow.nickname || '-' }}</el-form-item>
+        <el-form-item label="真实姓名">{{ currentRow.realName || '-' }}</el-form-item>
+        <el-form-item label="手机号">{{ currentRow.phone || '-' }}</el-form-item>
+        <el-form-item label="来源">{{ currentRow.source === 'service_role' ? '服务角色申请' : '用户资料' }}</el-form-item>
+        <el-form-item label="管家等级">{{ currentRow.tier || '-' }}</el-form-item>
+        <el-form-item label="服务说明">{{ currentRow.description || '-' }}</el-form-item>
+        <template v-if="currentRow.source === 'service_role'">
+          <el-form-item label="身份证号">{{ currentRow.idCard || '-' }}</el-form-item>
+          <el-form-item label="身份证照片">
+            <div v-if="currentRow.idCardImages && currentRow.idCardImages.length">
+              <img v-for="(img, i) in currentRow.idCardImages" :key="'id' + i" :src="img" class="audit-img" />
+            </div>
+            <span v-else>-</span>
+          </el-form-item>
+          <el-form-item label="资质证书">
+            <div v-if="currentRow.certificateImages && currentRow.certificateImages.length">
+              <img v-for="(img, i) in currentRow.certificateImages" :key="'cert' + i" :src="img" class="audit-img" />
+            </div>
+            <span v-else>-</span>
+          </el-form-item>
+          <el-form-item label="所在地区">{{ [currentRow.province, currentRow.city, currentRow.district, currentRow.street].filter(Boolean).join(' ') || '-' }}</el-form-item>
+          <el-form-item label="详细地址">{{ currentRow.addressDetail || '-' }}</el-form-item>
+        </template>
+        <el-form-item label="审核状态">
+          <el-tag :type="statusType(currentRow.status)">{{ statusLabel(currentRow.status) }}</el-tag>
+        </el-form-item>
+        <el-form-item label="驳回原因" v-if="currentRow.rejectReason" style="color:#f56c6c;">
+          {{ currentRow.rejectReason }}
+        </el-form-item>
+      </el-form>
+    </audit-detail-drawer>
   </div>
 </template>
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
-import { getButlerList, reviewButler, toggleButlerAccepting } from '@/api/butler'
-import { searchUsers } from '@/api/admin'
+import { getButlerUserList, reviewButler, toggleButlerAccepting } from '@/api/butler'
+import { listServiceRoleApplications, approveServiceRoleApplication, rejectServiceRoleApplication } from '@/api/service-role'
+import { mapServiceRoleRow, mergeAuditRows } from '@/utils/auditRowMapping'
+import AuditDetailDrawer from './components/AuditDetailDrawer.vue'
 
 export default {
   name: 'ButlerAudit',
+  components: { AuditDetailDrawer },
   mixins: [SortMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300
+    },
+    mergedList() {
+      let rows = mergeAuditRows(this.userRows, this.serviceRoleRows)
+      if (this.statusFilter) rows = rows.filter(r => r.status === this.statusFilter)
+      return rows
+    },
+    pagedList() {
+      const start = (this.page - 1) * this.size
+      return this.mergedList.slice(start, start + this.size)
+    },
+    total() {
+      return this.mergedList.length
     }
   },
   data() {
     return {
-      list: [], loading: false, statusFilter: 'pending',
-      sortableColumns: { id: '档案ID', tier: '等级', description: '申请说明' },
-      rejectDialogVisible: false, rejectTarget: null, rejectReason: ''
+      userRows: [], serviceRoleRows: [], loading: false, statusFilter: 'pending',
+      page: 1, size: 20,
+      sortableColumns: { id: '用户ID', tier: '等级', description: '申请说明' },
+      detailVisible: false, currentRow: null, auditLoading: false
     }
   },
   mounted() { this.loadData() },
@@ -90,6 +149,57 @@ export default {
     loadList() { this.loadData() },
     statusType(s) { return { pending: 'warning', approved: 'success', rejected: 'danger' }[s] || 'info' },
     statusLabel(s) { return { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[s] || s },
+    onPageChange(p) { this.page = p },
+    parseButlerInfo(info) {
+      if (!info) return {}
+      try {
+        return typeof info === 'string' ? JSON.parse(info) : info
+      } catch (e) {
+        return {}
+      }
+    },
+    serviceRoleStatusParam() {
+      if (this.statusFilter === 'pending') return 0
+      if (this.statusFilter === 'approved') return 1
+      if (this.statusFilter === 'rejected') return 2
+      return ''
+    },
+    async loadData() {
+      this.loading = true
+      this.page = 1
+      try {
+        const [userRes, srRes] = await Promise.all([
+          getButlerUserList({ status: this.statusFilter || undefined, page: 1, size: 500 }),
+          listServiceRoleApplications({ status: this.serviceRoleStatusParam(), role: 'butler' })
+        ])
+        const records = (userRes && userRes.data && userRes.data.list) || []
+        this.userRows = records.map(r => {
+          const info = this.parseButlerInfo(r.butlerInfo)
+          return {
+            source: 'user',
+            userId: r.userId,
+            nickname: r.nickname,
+            realName: r.realName,
+            phone: r.phone,
+            status: r.butlerStatus || 'pending',
+            rejectReason: info.rejectReason,
+            tier: info.tier,
+            description: info.description,
+            accepting: info.accepting,
+            submittedAt: r.updatedAt,
+            raw: r
+          }
+        })
+        const srData = (srRes && srRes.data) || []
+        this.serviceRoleRows = srData.map(a => mapServiceRoleRow(a))
+      } catch (e) {
+        console.error(e)
+        this.$message.error('加载失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    showDetail(row) { this.currentRow = row; this.detailVisible = true },
     async onToggleAccepting(row) {
       const next = row.accepting === 0 ? 1 : 0
       try {
@@ -104,60 +214,35 @@ export default {
         this.$message.error('操作失败')
       }
     },
-    async loadData() {
-      this.loading = true
+    async handleApprove() {
+      if (!this.currentRow) return
+      this.auditLoading = true
       try {
-        const params = { status: this.statusFilter || undefined, page: 1, size: 50 }
-        if (this.sortState.length > 0) params.sort = this.sortState
-        const res = await getButlerList(params)
-        this.list = res.data && res.data.records || res.data && res.data.list || res.data || []
-        await this.resolveUserNames(this.list, 'userId')
-      } catch (e) { this.$message.error('加载失败')
-      } finally { this.loading = false }
-    },
-    async handleReview(row, action) {
-      try {
-        await reviewButler({ id: row.id, action })
+        if (this.currentRow.source === 'service_role') {
+          await approveServiceRoleApplication(this.currentRow.applyId)
+        } else {
+          await reviewButler({ userId: this.currentRow.userId, action: 'approve' })
+        }
         this.$message.success('审核完成')
-        this.rejectDialogVisible = false
+        this.detailVisible = false
         this.loadData()
-      } catch (e) { this.$message.error('操作失败') }
+      } catch (e) { this.$message.error('操作失败')
+      } finally { this.auditLoading = false }
     },
-    showReject(row) { this.rejectTarget = row; this.rejectReason = ''; this.rejectDialogVisible = true },
-    resolveUserNames(items, idField) {
-      if (!items || items.length === 0) return Promise.resolve()
-      var ids = []
-      items.forEach(function(item) {
-        if (item[idField] && ids.indexOf(item[idField]) === -1) ids.push(item[idField])
-      })
-      var nameMap = {}
-      return Promise.all(ids.map(function(id) {
-        return searchUsers({ keyword: String(id) })
-          .then(function(res) {
-            if (res.data && res.data.length > 0) {
-              var u = res.data[0]
-              nameMap[id] = u.nickname || u.realName || '用户' + id
-            } else {
-              nameMap[id] = '用户' + id
-            }
-          })
-          .catch(function() {
-            nameMap[id] = '用户' + id
-          })
-      })).then(function() {
-        items.forEach(function(item) {
-          item.userName = nameMap[item[idField]] || '用户' + item[idField]
-        })
-      })
-    },
-    async confirmReject() {
-      if (!this.rejectReason) return this.$message.warning('请填写拒绝原因')
+    async handleReject(reason) {
+      if (!this.currentRow) return
+      this.auditLoading = true
       try {
-        await reviewButler({ id: this.rejectTarget.id, action: 'reject' })
+        if (this.currentRow.source === 'service_role') {
+          await rejectServiceRoleApplication(this.currentRow.applyId, reason)
+        } else {
+          await reviewButler({ userId: this.currentRow.userId, action: 'reject', reason })
+        }
         this.$message.success('已拒绝')
-        this.rejectDialogVisible = false
+        this.detailVisible = false
         this.loadData()
-      } catch (e) { this.$message.error('操作失败') }
+      } catch (e) { this.$message.error('操作失败')
+      } finally { this.auditLoading = false }
     }
   }
 }
@@ -171,4 +256,5 @@ export default {
 .sort-index { color: #fff; border-radius: 8px; font-size: 10px; padding: 0 4px; margin: 0 2px; }
 .sort-asc-badge { background: #f56c6c; }
 .sort-desc-badge { background: #409eff; }
-</style>
+.audit-img { max-width: 160px; max-height: 110px; border-radius: 4px; margin-right: 8px; border: 1px solid #eee; }
+</style>

+ 133 - 64
cfc-web/src/views/admin/review/GuideAudit.vue

@@ -11,39 +11,34 @@
       <el-button size="small" @click="loadData" :loading="loading">刷新</el-button>
     </div>
         <div class="adm-table-wrap">
-<el-table :data="list" v-loading="loading" border stripe style="min-width:900px;" height="100%">
-      <el-table-column prop="id" label="ID" width="60">
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('id', $event)">ID<span v-if="sortIndex('id') !== null" class="sort-index" :class="sortClass('id') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('id') }}</span>{{ sortIcon('id') }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column prop="nickname" label="昵称" width="120">
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('nickname', $event)">昵称<span v-if="sortIndex('nickname') !== null" class="sort-index" :class="sortClass('nickname') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('nickname') }}</span>{{ sortIcon('nickname') }}</span>
-        </template>
+<el-table :data="pagedList" v-loading="loading" border stripe style="min-width:900px;" height="100%">
+      <el-table-column label="ID" width="80">
+        <template slot-scope="{ row }">{{ row.userId }}</template>
       </el-table-column>
-      <el-table-column prop="realName" label="真实姓名" width="120">
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('realName', $event)">真实姓名<span v-if="sortIndex('realName') !== null" class="sort-index" :class="sortClass('realName') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('realName') }}</span>{{ sortIcon('realName') }}</span>
+      <el-table-column prop="nickname" label="昵称" width="120" show-overflow-tooltip />
+      <el-table-column prop="realName" label="真实姓名" width="120" show-overflow-tooltip />
+      <el-table-column prop="phone" label="手机号" width="140" />
+      <el-table-column label="来源" width="100">
+        <template slot-scope="{ row }">
+          <el-tag size="small" :type="row.source === 'service_role' ? 'success' : 'info'">
+            {{ row.source === 'service_role' ? '服务角色申请' : '用户资料' }}
+          </el-tag>
         </template>
       </el-table-column>
-      <el-table-column prop="phone" label="手机号" width="140">
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('phone', $event)">手机号<span v-if="sortIndex('phone') !== null" class="sort-index" :class="sortClass('phone') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('phone') }}</span>{{ sortIcon('phone') }}</span>
-        </template>
+      <el-table-column label="申请角色" width="100">
+        <template slot-scope="{ row }">{{ row.roleLabel || '规划师' }}</template>
       </el-table-column>
       <el-table-column label="审核状态" width="100">
         <template slot-scope="{ row }">
-          <el-tag :type="statusType(row.teacherStatus)">{{ statusLabel(row.teacherStatus) }}</el-tag>
+          <el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="操作" width="180" fixed="right">
+      <el-table-column label="提交时间" width="160">
+        <template slot-scope="{ row }">{{ row.submittedAt || '-' }}</template>
+      </el-table-column>
+      <el-table-column label="操作" width="120" 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>
+          <el-button size="mini" type="primary" @click="showDetail(row)">{{ row.status === 'pending' ? '审核' : '查看' }}</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -59,34 +54,86 @@
       style="margin-top:12px;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>
+    <audit-detail-drawer
+      :visible.sync="detailVisible"
+      title="规划师审核详情"
+      :status="currentRow ? currentRow.status : ''"
+      :loading="auditLoading"
+      @approve="handleApprove"
+      @reject="handleReject">
+      <el-form v-if="currentRow" label-width="110px" size="small">
+        <el-form-item label="用户ID">{{ currentRow.userId }}</el-form-item>
+        <el-form-item label="昵称">{{ currentRow.nickname || '-' }}</el-form-item>
+        <el-form-item label="真实姓名">{{ currentRow.realName || '-' }}</el-form-item>
+        <el-form-item label="手机号">{{ currentRow.phone || '-' }}</el-form-item>
+        <el-form-item label="申请角色">{{ currentRow.roleLabel || '规划师' }}</el-form-item>
+        <el-form-item label="来源">{{ currentRow.source === 'service_role' ? '服务角色申请' : '用户资料' }}</el-form-item>
+        <template v-if="currentRow.source === 'service_role'">
+          <el-form-item label="身份证号">{{ currentRow.idCard || '-' }}</el-form-item>
+          <el-form-item label="身份证照片">
+            <div v-if="currentRow.idCardImages && currentRow.idCardImages.length">
+              <img v-for="(img, i) in currentRow.idCardImages" :key="'id' + i" :src="img" class="audit-img" />
+            </div>
+            <span v-else>-</span>
+          </el-form-item>
+          <el-form-item label="资质证书">
+            <div v-if="currentRow.certificateImages && currentRow.certificateImages.length">
+              <img v-for="(img, i) in currentRow.certificateImages" :key="'cert' + i" :src="img" class="audit-img" />
+            </div>
+            <span v-else>-</span>
+          </el-form-item>
+          <el-form-item label="所在地区">{{ [currentRow.province, currentRow.city, currentRow.district, currentRow.street].filter(Boolean).join(' ') || '-' }}</el-form-item>
+          <el-form-item label="详细地址">{{ currentRow.addressDetail || '-' }}</el-form-item>
+        </template>
+        <el-form-item label="审核状态">
+          <el-tag :type="statusType(currentRow.status)">{{ statusLabel(currentRow.status) }}</el-tag>
+        </el-form-item>
+        <el-form-item label="驳回原因" v-if="currentRow.rejectReason" style="color:#f56c6c;">
+          {{ currentRow.rejectReason }}
+        </el-form-item>
+      </el-form>
+    </audit-detail-drawer>
   </div>
 </template>
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
 import { getGuideApplications, approveGuide, rejectGuide } from '@/api/admin'
+import { listServiceRoleApplications, approveServiceRoleApplication, rejectServiceRoleApplication } from '@/api/service-role'
+import { mapServiceRoleRow, mergeAuditRows } from '@/utils/auditRowMapping'
+import AuditDetailDrawer from './components/AuditDetailDrawer.vue'
 
 export default {
   name: 'GuideAudit',
+  components: { AuditDetailDrawer },
   mixins: [SortMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300
+    },
+    mergedList() {
+      let rows = mergeAuditRows(this.userRows, this.serviceRoleRows)
+      if (this.statusFilter) rows = rows.filter(r => r.status === this.statusFilter)
+      if (this.keyword) {
+        const kw = this.keyword.trim().toLowerCase()
+        rows = rows.filter(r => [r.nickname, r.realName, r.phone].some(v => v && String(v).toLowerCase().includes(kw)))
+      }
+      return rows
+    },
+    pagedList() {
+      const start = (this.page - 1) * this.size
+      return this.mergedList.slice(start, start + this.size)
+    },
+    total() {
+      return this.mergedList.length
     }
   },
   data() {
     return {
-      list: [], loading: false, keyword: '', statusFilter: 'pending',
-      page: 1, size: 20, total: 0,
+      userRows: [], serviceRoleRows: [], loading: false, keyword: '', statusFilter: 'pending',
+      page: 1, size: 20,
       sortableColumns: { id: 'ID', nickname: '昵称', realName: '真实姓名', phone: '手机号' },
-      rejectDialogVisible: false, rejectTarget: null, rejectReason: ''
+      detailVisible: false, currentRow: null, auditLoading: false
     }
   },
   mounted() { this.loadData() },
@@ -94,29 +141,36 @@ export default {
     loadList() { this.loadData() },
     statusType(s) { return { pending: 'warning', approved: 'success', rejected: 'danger' }[s] || 'info' },
     statusLabel(s) { return { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[s] || s },
-    handleSearch() { this.page = 1; this.loadData() },
-    onPageChange(p) { this.page = p; this.loadData() },
+    handleSearch() { this.page = 1 },
+    onPageChange(p) { this.page = p },
+    serviceRoleStatusParam() {
+      if (this.statusFilter === 'pending') return 0
+      if (this.statusFilter === 'approved') return 1
+      if (this.statusFilter === 'rejected') return 2
+      return ''
+    },
     async loadData() {
       this.loading = true
+      this.page = 1
       try {
-        const params = {
-          status: this.statusFilter || undefined,
-          keyword: this.keyword || undefined,
-          page: this.page,
-          size: this.size
-        }
-        if (this.sortState.length > 0) params.sort = this.sortState
-        const res = await getGuideApplications(params)
-        if (res.data && res.data.records) {
-          this.list = res.data.records
-          this.total = res.data.total || 0
-        } else if (res.data && Array.isArray(res.data)) {
-          this.list = res.data
-          this.total = res.data.length
-        } else {
-          this.list = []
-          this.total = 0
-        }
+        const [userRes, srRes] = await Promise.all([
+          getGuideApplications({ status: this.statusFilter || undefined, page: 1, size: 500 }),
+          listServiceRoleApplications({ status: this.serviceRoleStatusParam(), role: 'planner' })
+        ])
+        const records = (userRes && userRes.data && userRes.data.records) || (userRes && userRes.data && Array.isArray(userRes.data) ? userRes.data : []) || []
+        this.userRows = records.map(r => ({
+          source: 'user',
+          userId: r.id,
+          nickname: r.nickname,
+          realName: r.realName,
+          phone: r.phone,
+          status: r.teacherStatus || 'pending',
+          rejectReason: r.teacherRejectReason,
+          submittedAt: r.createdAt || r.updatedAt,
+          raw: r
+        }))
+        const srData = (srRes && srRes.data) || []
+        this.serviceRoleRows = srData.map(a => mapServiceRoleRow(a))
       } catch (e) {
         console.error(e)
         this.$message.error('加载失败')
@@ -124,22 +178,36 @@ export default {
         this.loading = false
       }
     },
-    async handleApprove(row) {
+    showDetail(row) { this.currentRow = row; this.detailVisible = true },
+    async handleApprove() {
+      if (!this.currentRow) return
+      this.auditLoading = true
       try {
-        await approveGuide(row.id)
+        if (this.currentRow.source === 'service_role') {
+          await approveServiceRoleApplication(this.currentRow.applyId)
+        } else {
+          await approveGuide(this.currentRow.userId)
+        }
         this.$message.success('已通过')
+        this.detailVisible = false
         this.loadData()
-      } catch (e) { this.$message.error('操作失败') }
+      } catch (e) { this.$message.error('操作失败')
+      } finally { this.auditLoading = false }
     },
-    showReject(row) { this.rejectTarget = row; this.rejectReason = ''; this.rejectDialogVisible = true },
-    async confirmReject() {
-      if (!this.rejectReason) return this.$message.warning('请填写拒绝原因')
+    async handleReject(reason) {
+      if (!this.currentRow) return
+      this.auditLoading = true
       try {
-        await rejectGuide(this.rejectTarget.id, this.rejectReason)
+        if (this.currentRow.source === 'service_role') {
+          await rejectServiceRoleApplication(this.currentRow.applyId, reason)
+        } else {
+          await rejectGuide(this.currentRow.userId, reason)
+        }
         this.$message.success('已拒绝')
-        this.rejectDialogVisible = false
+        this.detailVisible = false
         this.loadData()
-      } catch (e) { this.$message.error('操作失败') }
+      } catch (e) { this.$message.error('操作失败')
+      } finally { this.auditLoading = false }
     }
   }
 }
@@ -153,4 +221,5 @@ export default {
 .sort-index { color: #fff; border-radius: 8px; font-size: 10px; padding: 0 4px; margin: 0 2px; }
 .sort-asc-badge { background: #f56c6c; }
 .sort-desc-badge { background: #409eff; }
-</style>
+.audit-img { max-width: 160px; max-height: 110px; border-radius: 4px; margin-right: 8px; border: 1px solid #eee; }
+</style>

+ 38 - 23
cfc-web/src/views/admin/review/HierarchyAudit.vue

@@ -41,25 +41,37 @@
           <el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="操作" width="180" fixed="right">
+      <el-table-column label="操作" width="120" 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>
+          <el-button size="mini" type="primary" @click="showDetail(row)">{{ row.status === 'pending' ? '审核' : '查看' }}</el-button>
         </template>
       </el-table-column>
     </el-table>
     </div>
 
-    <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>
+    <audit-detail-drawer
+      :visible.sync="detailVisible"
+      title="更换上级审核详情"
+      :status="currentRow ? currentRow.status : ''"
+      :loading="auditLoading"
+      @approve="handleApprove"
+      @reject="handleReject">
+      <el-form v-if="currentRow" label-width="100px" size="small">
+        <el-form-item label="申请ID">{{ currentRow.id }}</el-form-item>
+        <el-form-item label="申请人">{{ currentRow.applicantName || '-' }}</el-form-item>
+        <el-form-item label="当前上级">{{ currentRow.currentSuperior || '-' }}</el-form-item>
+        <el-form-item label="目标上级">{{ currentRow.targetSuperior || '-' }}</el-form-item>
+        <el-form-item label="申请原因">
+          <div style="white-space:pre-wrap;">{{ currentRow.reason || '-' }}</div>
+        </el-form-item>
+        <el-form-item label="审核状态">
+          <el-tag :type="statusType(currentRow.status)">{{ statusLabel(currentRow.status) }}</el-tag>
+        </el-form-item>
+        <el-form-item label="驳回原因" v-if="currentRow.rejectReason" style="color:#f56c6c;">
+          {{ currentRow.rejectReason }}
+        </el-form-item>
+      </el-form>
+    </audit-detail-drawer>
   </div>
 </template>
 
@@ -67,9 +79,11 @@
 import SortMixin from '@/mixins/SortMixin'
 import request from '@/utils/request'
 import { reviewSupplyHierarchyChange } from '@/api/admin'
+import AuditDetailDrawer from './components/AuditDetailDrawer.vue'
 
 export default {
   name: 'HierarchyAudit',
+  components: { AuditDetailDrawer },
   mixins: [SortMixin],
   computed: {
     tableHeight() {
@@ -80,7 +94,7 @@ export default {
     return {
       list: [], loading: false, statusFilter: 'pending',
       sortableColumns: { id: 'ID', applicantName: '申请人', currentSuperior: '当前上级', targetSuperior: '目标上级', reason: '申请原因' },
-      rejectDialogVisible: false, rejectTarget: null, rejectReason: ''
+      detailVisible: false, currentRow: null, auditLoading: false
     }
   },
   mounted() { this.loadData() },
@@ -98,18 +112,19 @@ export default {
       } catch (e) { this.$message.error('加载失败')
       } finally { this.loading = false }
     },
-    async handleReview(row, status) {
+    showDetail(row) { this.currentRow = row; this.detailVisible = true },
+    handleApprove() { this.handleReview('approved') },
+    handleReject(reason) { this.handleReview('rejected', reason) },
+    async handleReview(status, reason) {
+      if (!this.currentRow) return
+      this.auditLoading = true
       try {
-        await reviewSupplyHierarchyChange({ requestId: row.id, status, rejectReason: status === 'rejected' ? this.rejectReason : '' })
+        await reviewSupplyHierarchyChange({ requestId: this.currentRow.id, status, rejectReason: status === 'rejected' ? reason : '' })
         this.$message.success('审核完成')
-        this.rejectDialogVisible = false
+        this.detailVisible = 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')
+      } catch (e) { this.$message.error('操作失败')
+      } finally { this.auditLoading = false }
     }
   }
 }

+ 156 - 0
cfc-web/src/views/admin/review/KnowledgeAdminAudit.vue

@@ -0,0 +1,156 @@
+<template>
+  <div>
+    <div class="filter-bar">
+      <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>
+    <div class="adm-table-wrap">
+      <el-table :data="list" v-loading="loading" border stripe style="min-width:900px;" height="100%">
+        <el-table-column label="申请ID" width="90">
+          <template slot-scope="{ row }">{{ row.applyId }}</template>
+        </el-table-column>
+        <el-table-column label="用户ID" width="90">
+          <template slot-scope="{ row }">{{ row.userId }}</template>
+        </el-table-column>
+        <el-table-column label="昵称" width="120" show-overflow-tooltip>
+          <template slot-scope="{ row }">{{ row.nickname || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="真实姓名" width="120" show-overflow-tooltip>
+          <template slot-scope="{ row }">{{ row.realName || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="手机号" width="140">
+          <template slot-scope="{ row }">{{ row.phone || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="申请角色" width="120">
+          <template slot-scope="{ row }">{{ row.roleLabel }}</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="170">
+          <template slot-scope="{ row }">{{ row.submittedAt || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="操作" width="120" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button size="mini" type="primary" @click="showDetail(row)">{{ row.status === 'pending' ? '审核' : '查看' }}</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <audit-detail-drawer
+      :visible.sync="detailVisible"
+      title="知识管理员审核详情"
+      :status="currentRow ? currentRow.status : ''"
+      :loading="auditLoading"
+      @approve="handleApprove"
+      @reject="handleReject">
+      <el-form v-if="currentRow" label-width="110px" size="small">
+        <el-form-item label="申请ID">{{ currentRow.applyId }}</el-form-item>
+        <el-form-item label="用户ID">{{ currentRow.userId }}</el-form-item>
+        <el-form-item label="昵称">{{ currentRow.nickname || '-' }}</el-form-item>
+        <el-form-item label="真实姓名">{{ currentRow.realName || '-' }}</el-form-item>
+        <el-form-item label="手机号">{{ currentRow.phone || '-' }}</el-form-item>
+        <el-form-item label="申请角色">{{ currentRow.roleLabel }}</el-form-item>
+        <el-form-item label="身份证号">{{ currentRow.idCard || '-' }}</el-form-item>
+        <el-form-item label="身份证照片">
+          <div v-if="currentRow.idCardImages && currentRow.idCardImages.length">
+            <img v-for="(img, i) in currentRow.idCardImages" :key="'id' + i" :src="img" class="audit-img" />
+          </div>
+          <span v-else>-</span>
+        </el-form-item>
+        <el-form-item label="资质证书">
+          <div v-if="currentRow.certificateImages && currentRow.certificateImages.length">
+            <img v-for="(img, i) in currentRow.certificateImages" :key="'cert' + i" :src="img" class="audit-img" />
+          </div>
+          <span v-else>-</span>
+        </el-form-item>
+        <el-form-item label="所在地区">{{ [currentRow.province, currentRow.city, currentRow.district, currentRow.street].filter(Boolean).join(' ') || '-' }}</el-form-item>
+        <el-form-item label="详细地址">{{ currentRow.addressDetail || '-' }}</el-form-item>
+        <el-form-item label="审核状态">
+          <el-tag :type="statusType(currentRow.status)">{{ statusLabel(currentRow.status) }}</el-tag>
+        </el-form-item>
+        <el-form-item label="驳回原因" v-if="currentRow.rejectReason" style="color:#f56c6c;">
+          {{ currentRow.rejectReason }}
+        </el-form-item>
+      </el-form>
+    </audit-detail-drawer>
+  </div>
+</template>
+
+<script>
+import { listServiceRoleApplications, approveServiceRoleApplication, rejectServiceRoleApplication } from '@/api/service-role'
+import { mapServiceRoleRow } from '@/utils/auditRowMapping'
+import AuditDetailDrawer from './components/AuditDetailDrawer.vue'
+
+export default {
+  name: 'KnowledgeAdminAudit',
+  components: { AuditDetailDrawer },
+  data() {
+    return {
+      list: [], loading: false, statusFilter: 'pending',
+      detailVisible: false, currentRow: null, auditLoading: false
+    }
+  },
+  mounted() { this.loadData() },
+  methods: {
+    loadList() { this.loadData() },
+    statusType(s) { return { pending: 'warning', approved: 'success', rejected: 'danger' }[s] || 'info' },
+    statusLabel(s) { return { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[s] || s },
+    statusParam() {
+      if (this.statusFilter === 'pending') return 0
+      if (this.statusFilter === 'approved') return 1
+      if (this.statusFilter === 'rejected') return 2
+      return ''
+    },
+    async loadData() {
+      this.loading = true
+      try {
+        const res = await listServiceRoleApplications({ status: this.statusParam(), role: 'article_admin' })
+        const data = (res && res.data) || []
+        this.list = data.map(a => mapServiceRoleRow(a))
+      } catch (e) {
+        console.error(e)
+        this.$message.error('加载失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    showDetail(row) { this.currentRow = row; this.detailVisible = true },
+    async handleApprove() {
+      if (!this.currentRow) return
+      this.auditLoading = true
+      try {
+        await approveServiceRoleApplication(this.currentRow.applyId)
+        this.$message.success('已通过')
+        this.detailVisible = false
+        this.loadData()
+      } catch (e) { this.$message.error('操作失败')
+      } finally { this.auditLoading = false }
+    },
+    async handleReject(reason) {
+      if (!this.currentRow) return
+      this.auditLoading = true
+      try {
+        await rejectServiceRoleApplication(this.currentRow.applyId, reason)
+        this.$message.success('已拒绝')
+        this.detailVisible = false
+        this.loadData()
+      } catch (e) { this.$message.error('操作失败')
+      } finally { this.auditLoading = false }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.filter-bar { display: flex; gap: 12px; align-items: center; margin-bottom: 12px; padding: 12px; background: #fff; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.08); }
+.audit-img { max-width: 160px; max-height: 110px; border-radius: 4px; margin-right: 8px; border: 1px solid #eee; }
+</style>

+ 139 - 57
cfc-web/src/views/admin/review/NutritionistAudit.vue

@@ -11,71 +11,125 @@
       <el-button size="small" @click="loadData" :loading="loading">刷新</el-button>
     </div>
         <div class="adm-table-wrap">
-<el-table :data="list" v-loading="loading" border stripe style="min-width:900px;" height="100%">
-      <el-table-column prop="id" label="ID" width="60">
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('id', $event)">ID<span v-if="sortIndex('id') !== null" class="sort-index" :class="sortClass('id') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('id') }}</span>{{ sortIcon('id') }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column prop="nickname" label="昵称" width="120">
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('nickname', $event)">昵称<span v-if="sortIndex('nickname') !== null" class="sort-index" :class="sortClass('nickname') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('nickname') }}</span>{{ sortIcon('nickname') }}</span>
-        </template>
+<el-table :data="pagedList" v-loading="loading" border stripe style="min-width:900px;" height="100%">
+      <el-table-column label="ID" width="80">
+        <template slot-scope="{ row }">{{ row.userId }}</template>
       </el-table-column>
-      <el-table-column prop="realName" label="真实姓名" width="120">
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('realName', $event)">真实姓名<span v-if="sortIndex('realName') !== null" class="sort-index" :class="sortClass('realName') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('realName') }}</span>{{ sortIcon('realName') }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column prop="phone" label="手机号" width="140">
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('phone', $event)">手机号<span v-if="sortIndex('phone') !== null" class="sort-index" :class="sortClass('phone') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('phone') }}</span>{{ sortIcon('phone') }}</span>
+      <el-table-column prop="nickname" label="昵称" width="120" show-overflow-tooltip />
+      <el-table-column prop="realName" label="真实姓名" width="120" show-overflow-tooltip />
+      <el-table-column prop="phone" label="手机号" width="140" />
+      <el-table-column label="来源" width="100">
+        <template slot-scope="{ row }">
+          <el-tag size="small" :type="row.source === 'service_role' ? 'success' : 'info'">
+            {{ row.source === 'service_role' ? '服务角色申请' : '用户资料' }}
+          </el-tag>
         </template>
       </el-table-column>
       <el-table-column label="审核状态" width="100">
         <template slot-scope="{ row }">
-          <el-tag :type="statusType(row.nutritionistStatus)">{{ statusLabel(row.nutritionistStatus) }}</el-tag>
+          <el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="操作" width="180" fixed="right">
+      <el-table-column label="提交时间" width="160">
+        <template slot-scope="{ row }">{{ row.submittedAt || '-' }}</template>
+      </el-table-column>
+      <el-table-column label="操作" width="120" fixed="right">
         <template slot-scope="{ row }">
-          <template v-if="row.nutritionistStatus === '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>
+          <el-button size="mini" type="primary" @click="showDetail(row)">{{ row.status === 'pending' ? '审核' : '查看' }}</el-button>
         </template>
       </el-table-column>
     </el-table>
     </div>
 
-    <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-pagination
+      v-if="total > 0"
+      :current-page="page"
+      :page-size="size"
+      :total="total"
+      layout="total, prev, pager, next"
+      @current-change="onPageChange"
+      style="margin-top:12px;text-align:right;"
+    />
+
+    <audit-detail-drawer
+      :visible.sync="detailVisible"
+      title="营养师审核详情"
+      :status="currentRow ? currentRow.status : ''"
+      :loading="auditLoading"
+      @approve="handleApprove"
+      @reject="handleReject">
+      <el-form v-if="currentRow" label-width="110px" size="small">
+        <el-form-item label="用户ID">{{ currentRow.userId }}</el-form-item>
+        <el-form-item label="昵称">{{ currentRow.nickname || '-' }}</el-form-item>
+        <el-form-item label="真实姓名">{{ currentRow.realName || '-' }}</el-form-item>
+        <el-form-item label="手机号">{{ currentRow.phone || '-' }}</el-form-item>
+        <el-form-item label="来源">{{ currentRow.source === 'service_role' ? '服务角色申请' : '用户资料' }}</el-form-item>
+        <template v-if="currentRow.source === 'service_role'">
+          <el-form-item label="身份证号">{{ currentRow.idCard || '-' }}</el-form-item>
+          <el-form-item label="身份证照片">
+            <div v-if="currentRow.idCardImages && currentRow.idCardImages.length">
+              <img v-for="(img, i) in currentRow.idCardImages" :key="'id' + i" :src="img" class="audit-img" />
+            </div>
+            <span v-else>-</span>
+          </el-form-item>
+          <el-form-item label="资质证书">
+            <div v-if="currentRow.certificateImages && currentRow.certificateImages.length">
+              <img v-for="(img, i) in currentRow.certificateImages" :key="'cert' + i" :src="img" class="audit-img" />
+            </div>
+            <span v-else>-</span>
+          </el-form-item>
+          <el-form-item label="所在地区">{{ [currentRow.province, currentRow.city, currentRow.district, currentRow.street].filter(Boolean).join(' ') || '-' }}</el-form-item>
+          <el-form-item label="详细地址">{{ currentRow.addressDetail || '-' }}</el-form-item>
+        </template>
+        <el-form-item label="审核状态">
+          <el-tag :type="statusType(currentRow.status)">{{ statusLabel(currentRow.status) }}</el-tag>
+        </el-form-item>
+        <el-form-item label="驳回原因" v-if="currentRow.rejectReason" style="color:#f56c6c;">
+          {{ currentRow.rejectReason }}
+        </el-form-item>
+      </el-form>
+    </audit-detail-drawer>
   </div>
 </template>
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
 import { getNutritionistApplications, approveNutritionist, rejectNutritionist } from '@/api/admin'
+import { listServiceRoleApplications, approveServiceRoleApplication, rejectServiceRoleApplication } from '@/api/service-role'
+import { mapServiceRoleRow, mergeAuditRows } from '@/utils/auditRowMapping'
+import AuditDetailDrawer from './components/AuditDetailDrawer.vue'
 
 export default {
   name: 'NutritionistAudit',
+  components: { AuditDetailDrawer },
   mixins: [SortMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300
+    },
+    mergedList() {
+      let rows = mergeAuditRows(this.userRows, this.serviceRoleRows)
+      if (this.statusFilter) rows = rows.filter(r => r.status === this.statusFilter)
+      if (this.keyword) {
+        const kw = this.keyword.trim().toLowerCase()
+        rows = rows.filter(r => [r.nickname, r.realName, r.phone].some(v => v && String(v).toLowerCase().includes(kw)))
+      }
+      return rows
+    },
+    pagedList() {
+      const start = (this.page - 1) * this.size
+      return this.mergedList.slice(start, start + this.size)
+    },
+    total() {
+      return this.mergedList.length
     }
   },
   data() {
     return {
-      list: [], loading: false, keyword: '', statusFilter: 'pending',
+      userRows: [], serviceRoleRows: [], loading: false, keyword: '', statusFilter: 'pending',
+      page: 1, size: 20,
       sortableColumns: { id: 'ID', nickname: '昵称', realName: '真实姓名', phone: '手机号' },
-      rejectDialogVisible: false, rejectTarget: null, rejectReason: ''
+      detailVisible: false, currentRow: null, auditLoading: false
     }
   },
   mounted() { this.loadData() },
@@ -83,23 +137,36 @@ export default {
     loadList() { this.loadData() },
     statusType(s) { return { pending: 'warning', approved: 'success', rejected: 'danger' }[s] || 'info' },
     statusLabel(s) { return { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[s] || s },
-    handleSearch() { this.loadData() },
+    handleSearch() { this.page = 1 },
+    onPageChange(p) { this.page = p },
+    serviceRoleStatusParam() {
+      if (this.statusFilter === 'pending') return 0
+      if (this.statusFilter === 'approved') return 1
+      if (this.statusFilter === 'rejected') return 2
+      return ''
+    },
     async loadData() {
       this.loading = true
+      this.page = 1
       try {
-        const params = {
-          status: this.statusFilter || undefined,
-          keyword: this.keyword || undefined, page: 1, size: 50
-        }
-        if (this.sortState.length > 0) params.sort = this.sortState
-        const res = await getNutritionistApplications(params)
-        if (res.data && res.data.records) {
-          this.list = res.data.records
-        } else if (res.data && Array.isArray(res.data)) {
-          this.list = res.data
-        } else {
-          this.list = []
-        }
+        const [userRes, srRes] = await Promise.all([
+          getNutritionistApplications({ status: this.statusFilter || undefined, page: 1, size: 500 }),
+          listServiceRoleApplications({ status: this.serviceRoleStatusParam(), role: 'nutritionist' })
+        ])
+        const records = (userRes && userRes.data && userRes.data.records) || (userRes && userRes.data && Array.isArray(userRes.data) ? userRes.data : []) || []
+        this.userRows = records.map(r => ({
+          source: 'user',
+          userId: r.id,
+          nickname: r.nickname,
+          realName: r.realName,
+          phone: r.phone,
+          status: r.nutritionistStatus || 'pending',
+          rejectReason: r.nutritionistRejectReason,
+          submittedAt: r.createdAt || r.updatedAt,
+          raw: r
+        }))
+        const srData = (srRes && srRes.data) || []
+        this.serviceRoleRows = srData.map(a => mapServiceRoleRow(a))
       } catch (e) {
         console.error(e)
         this.$message.error('加载失败')
@@ -107,22 +174,36 @@ export default {
         this.loading = false
       }
     },
-    async handleApprove(row) {
+    showDetail(row) { this.currentRow = row; this.detailVisible = true },
+    async handleApprove() {
+      if (!this.currentRow) return
+      this.auditLoading = true
       try {
-        await approveNutritionist(row.id)
+        if (this.currentRow.source === 'service_role') {
+          await approveServiceRoleApplication(this.currentRow.applyId)
+        } else {
+          await approveNutritionist(this.currentRow.userId)
+        }
         this.$message.success('已通过')
+        this.detailVisible = false
         this.loadData()
-      } catch (e) { this.$message.error('操作失败') }
+      } catch (e) { this.$message.error('操作失败')
+      } finally { this.auditLoading = false }
     },
-    showReject(row) { this.rejectTarget = row; this.rejectReason = ''; this.rejectDialogVisible = true },
-    async confirmReject() {
-      if (!this.rejectReason) return this.$message.warning('请填写拒绝原因')
+    async handleReject(reason) {
+      if (!this.currentRow) return
+      this.auditLoading = true
       try {
-        await rejectNutritionist(this.rejectTarget.id, this.rejectReason)
+        if (this.currentRow.source === 'service_role') {
+          await rejectServiceRoleApplication(this.currentRow.applyId, reason)
+        } else {
+          await rejectNutritionist(this.currentRow.userId, reason)
+        }
         this.$message.success('已拒绝')
-        this.rejectDialogVisible = false
+        this.detailVisible = false
         this.loadData()
-      } catch (e) { this.$message.error('操作失败') }
+      } catch (e) { this.$message.error('操作失败')
+      } finally { this.auditLoading = false }
     }
   }
 }
@@ -136,4 +217,5 @@ export default {
 .sort-index { color: #fff; border-radius: 8px; font-size: 10px; padding: 0 4px; margin: 0 2px; }
 .sort-asc-badge { background: #f56c6c; }
 .sort-desc-badge { background: #409eff; }
+.audit-img { max-width: 160px; max-height: 110px; border-radius: 4px; margin-right: 8px; border: 1px solid #eee; }
 </style>

+ 40 - 73
cfc-web/src/views/admin/review/PackageAudit.vue

@@ -52,14 +52,9 @@
           <span class="sort-header" @click.stop="onSortClick('createdAt', $event)">申请时间<span v-if="sortIndex('createdAt') !== null" class="sort-index" :class="sortClass('createdAt') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('createdAt') }}</span>{{ sortIcon('createdAt') }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="操作" width="200" fixed="right">
+      <el-table-column label="操作" width="120" fixed="right">
         <template slot-scope="{ row }">
-          <template v-if="row.status === 'pending'">
-            <el-button size="mini" type="primary" @click="showDetail(row)">查看</el-button>
-            <el-button size="mini" type="success" @click="handleApprove(row)">通过</el-button>
-            <el-button size="mini" type="danger" @click="showRejectDialog(row)">驳回</el-button>
-          </template>
-          <el-button v-else size="mini" type="primary" @click="showDetail(row)">查看</el-button>
+          <el-button size="mini" type="primary" @click="showDetail(row)">{{ row.status === 'pending' ? '审核' : '查看' }}</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -75,40 +70,28 @@
       style="margin-top:12px;text-align:right;"
     />
 
-    <el-dialog title="任务模板详情" :visible.sync="detailVisible" width="700px" top="5vh">
-      <div v-if="currentRow" style="max-height:60vh;overflow-y:auto;">
-        <el-form label-width="110px" size="small">
-          <el-form-item label="ID">{{ currentRow.id }}</el-form-item>
-          <el-form-item label="任务模板名称">{{ currentRow.name }}</el-form-item>
-          <el-form-item label="状态"><el-tag :type="statusType(currentRow.status)">{{ statusLabel(currentRow.status) }}</el-tag></el-form-item>
-          <el-form-item label="周期">{{ currentRow.cycleDays }} 天</el-form-item>
-          <el-form-item label="价格">{{ formatPrice(currentRow.price) }}</el-form-item>
-          <el-form-item label="创建时间">{{ currentRow.createdAt }}</el-form-item>
-          <el-form-item label="详细介绍" v-if="currentRow.description">
-            <div style="border:1px solid #eee;padding:10px;border-radius:4px;background:#fafafa;white-space:pre-wrap;">{{ currentRow.description }}</div>
-          </el-form-item>
-          <el-form-item label="拒绝原因" v-if="currentRow.rejectReason" style="color:#f56c6c;">
-            {{ currentRow.rejectReason }}
-          </el-form-item>
-        </el-form>
-      </div>
-      <span slot="footer" v-if="currentRow && currentRow.status === 'pending'">
-        <el-button @click="detailVisible = false">取消</el-button>
-        <el-button type="success" @click="handleApproveFromDialog">通过</el-button>
-        <el-button type="danger" @click="showRejectFromDialog">驳回</el-button>
-      </span>
-      <span slot="footer" v-else>
-        <el-button @click="detailVisible = false">关闭</el-button>
-      </span>
-    </el-dialog>
-
-    <el-dialog title="驳回原因" :visible.sync="rejectDialogVisible" width="450px">
-      <el-input v-model="rejectReason" type="textarea" :rows="3" placeholder="请输入驳回原因" />
-      <span slot="footer">
-        <el-button @click="rejectDialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="confirmReject">确认驳回</el-button>
-      </span>
-    </el-dialog>
+    <audit-detail-drawer
+      :visible.sync="detailVisible"
+      title="任务模板审核详情"
+      :status="currentRow ? currentRow.status : ''"
+      :loading="auditLoading"
+      @approve="handleApprove"
+      @reject="handleReject">
+      <el-form v-if="currentRow" label-width="110px" size="small">
+        <el-form-item label="ID">{{ currentRow.id }}</el-form-item>
+        <el-form-item label="任务模板名称">{{ currentRow.name }}</el-form-item>
+        <el-form-item label="状态"><el-tag :type="statusType(currentRow.status)">{{ statusLabel(currentRow.status) }}</el-tag></el-form-item>
+        <el-form-item label="周期">{{ currentRow.cycleDays }} 天</el-form-item>
+        <el-form-item label="价格">{{ formatPrice(currentRow.price) }}</el-form-item>
+        <el-form-item label="创建时间">{{ currentRow.createdAt }}</el-form-item>
+        <el-form-item label="详细介绍" v-if="currentRow.description">
+          <div style="border:1px solid #eee;padding:10px;border-radius:4px;background:#fafafa;white-space:pre-wrap;">{{ currentRow.description }}</div>
+        </el-form-item>
+        <el-form-item label="拒绝原因" v-if="currentRow.rejectReason" style="color:#f56c6c;">
+          {{ currentRow.rejectReason }}
+        </el-form-item>
+      </el-form>
+    </audit-detail-drawer>
 
     <el-dialog title="批量驳回" :visible.sync="batchRejectDialogVisible" width="450px">
       <el-input v-model="batchRejectReason" type="textarea" :rows="3" placeholder="请输入驳回原因" />
@@ -123,9 +106,11 @@
 <script>
 import SortMixin from '@/mixins/SortMixin'
 import { getPackageList, reviewPackage } from '@/api/package'
+import AuditDetailDrawer from './components/AuditDetailDrawer.vue'
 
 export default {
   name: 'PackageAudit',
+  components: { AuditDetailDrawer },
   mixins: [SortMixin],
   computed: {
     tableHeight() {
@@ -143,10 +128,9 @@ export default {
       sortableColumns: { id: 'ID', name: '任务模板名称', cycleDays: '周期', price: '价格', createdAt: '申请时间' },
       selectedRows: [],
       detailVisible: false,
-      rejectDialogVisible: false,
       batchRejectDialogVisible: false,
-      rejectReason: '',
       batchRejectReason: '',
+      auditLoading: false,
       currentRow: null
     }
   },
@@ -184,43 +168,26 @@ export default {
     },
     showDetail(row) {
       this.currentRow = row
-      this.rejectReason = ''
       this.detailVisible = true
     },
-    handleApproveFromDialog() {
+    handleApprove() { this.doReview('approve') },
+    handleReject(reason) { this.doReview('reject', reason) },
+    async doReview(action, reason) {
       if (!this.currentRow) return
-      this.handleApprove(this.currentRow)
-      this.detailVisible = false
-    },
-    showRejectFromDialog() {
-      this.detailVisible = false
-      this.rejectDialogVisible = true
-    },
-    async handleApprove(row) {
+      this.auditLoading = true
       try {
-        await this.$confirm('确认通过该任务模板?', '提示', { type: 'warning' })
-        await reviewPackage({ id: row.id, action: 'approve' })
-        this.$message.success('已通过')
-        this.loadData()
-      } catch (e) {
-        if (e !== 'cancel') this.$message.error('操作失败')
-      }
-    },
-    showRejectDialog(row) {
-      this.currentRow = row
-      this.rejectReason = ''
-      this.rejectDialogVisible = true
-    },
-    async confirmReject() {
-      if (!this.rejectReason.trim()) return this.$message.warning('请填写驳回原因')
-      if (!this.currentRow) return
-      this.rejectDialogVisible = false
-      try {
-        await reviewPackage({ id: this.currentRow.id, action: 'reject', reason: this.rejectReason })
-        this.$message.success('已驳回')
+        await reviewPackage({
+          id: this.currentRow.id,
+          action,
+          reason: action === 'reject' ? reason : undefined
+        })
+        this.$message.success(action === 'approve' ? '已通过' : '已驳回')
+        this.detailVisible = false
         this.loadData()
       } catch (e) {
         this.$message.error('操作失败')
+      } finally {
+        this.auditLoading = false
       }
     },
     showBatchRejectDialog() {

+ 39 - 9
cfc-web/src/views/admin/review/PendingRefundAudit.vue

@@ -89,10 +89,9 @@
         </el-table-column>
         <el-table-column label="操作" width="120" fixed="right">
           <template slot-scope="{ row }">
-            <el-button v-if="row.status === 'pending' || row.status === 'processing'" type="danger" size="mini" @click="handleCancel(row)">
-              中止退款
+            <el-button type="primary" size="mini" @click="showDetail(row)">
+              {{ (row.status === 'pending' || row.status === 'processing') ? '处理' : '查看' }}
             </el-button>
-            <span v-else style="color:#999">-</span>
           </template>
         </el-table-column>
       </el-table>
@@ -107,15 +106,42 @@
       @current-change="loadData"
       style="margin-top:12px;text-align:right;"
     />
+
+    <audit-detail-drawer
+      :visible.sync="detailVisible"
+      title="待退款处理详情"
+      :status="currentRow ? currentRow.status : ''"
+      :loading="auditLoading"
+      action-mode="cancel"
+      @cancel="handleCancel">
+      <el-form v-if="currentRow" label-width="110px" size="small">
+        <el-form-item label="记录ID">{{ currentRow.id }}</el-form-item>
+        <el-form-item label="类型">{{ currentRow.orderType === 'product' ? '商品退款' : '提现退款' }}</el-form-item>
+        <el-form-item label="订单号">{{ currentRow.orderNo || '-' }}</el-form-item>
+        <el-form-item label="提现申请ID">{{ currentRow.withdrawalRequestId || '-' }}</el-form-item>
+        <el-form-item label="用户">{{ currentRow.userName || '用户' + currentRow.userId }}</el-form-item>
+        <el-form-item label="退款金额" style="font-weight:bold;color:#f56c6c;">{{ formatPriceWithSymbol(currentRow.amount) }}</el-form-item>
+        <el-form-item label="实退金额">{{ currentRow.refundedAmount ? formatPriceWithSymbol(currentRow.refundedAmount) : '-' }}</el-form-item>
+        <el-form-item label="状态">
+          <el-tag :type="statusType(currentRow.status)">{{ statusLabel(currentRow.status) }}</el-tag>
+        </el-form-item>
+        <el-form-item label="尝试次数">{{ currentRow.attemptCount }}/{{ currentRow.maxAttempts }}</el-form-item>
+        <el-form-item label="失败原因" v-if="currentRow.failureReason" style="color:#f56c6c;">{{ currentRow.failureReason }}</el-form-item>
+        <el-form-item label="中止原因" v-if="currentRow.remark" style="color:#909399;">{{ currentRow.remark }}</el-form-item>
+        <el-form-item label="创建时间">{{ formatTime(currentRow.createdAt) }}</el-form-item>
+      </el-form>
+    </audit-detail-drawer>
   </div>
 </template>
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
 import { getPendingRefundList, getPendingRefundSummary, cancelPendingRefund, searchUsers } from '@/api/admin'
+import AuditDetailDrawer from './components/AuditDetailDrawer.vue'
 
 export default {
   name: 'PendingRefundAudit',
+  components: { AuditDetailDrawer },
   mixins: [SortMixin],
   data() {
     return {
@@ -127,6 +153,7 @@ export default {
       page: 1,
       pageSize: 20,
       total: 0,
+      detailVisible: false, currentRow: null, auditLoading: false,
       summary: {
         totalPendingAmount: 0,
         pendingCount: 0
@@ -196,20 +223,23 @@ export default {
         })
       })
     },
-    async handleCancel(row) {
+    showDetail(row) { this.currentRow = row; this.detailVisible = true },
+    async handleCancel(reason) {
+      if (!this.currentRow) return
+      this.auditLoading = true
       try {
-        await this.$confirm('确认中止该退款?订单将回到退款审核状态。', '确认中止', { type: 'warning' })
-        const res = await cancelPendingRefund({ refundId: row.id })
+        const res = await cancelPendingRefund({ refundId: this.currentRow.id, reason })
         if (res.code === 200) {
           this.$message.success('已中止退款')
+          this.detailVisible = false
           this.loadData()
         } else {
           this.$message.error(res.message || '操作失败')
         }
       } catch (e) {
-        if (e !== 'cancel') {
-          this.$message.error('操作失败')
-        }
+        this.$message.error('操作失败')
+      } finally {
+        this.auditLoading = false
       }
     },
 

+ 50 - 25
cfc-web/src/views/admin/review/ProductAudit.vue

@@ -31,34 +31,50 @@
           <span class="sort-header" @click.stop="onSortClick('rejectReason', $event)">原因<span v-if="sortIndex('rejectReason') !== null" class="sort-index" :class="sortClass('rejectReason') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('rejectReason') }}</span>{{ sortIcon('rejectReason') }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="操作" width="180" fixed="right">
+      <el-table-column label="操作" width="120" 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>
+          <el-button size="mini" type="primary" @click="showDetail(row)">{{ row.status === 'pending' ? '审核' : '查看' }}</el-button>
         </template>
       </el-table-column>
     </el-table>
     </div>
 
-    <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>
+    <audit-detail-drawer
+      :visible.sync="detailVisible"
+      title="商品审核详情"
+      :status="currentRow ? currentRow.status : ''"
+      :loading="auditLoading"
+      @approve="handleApprove"
+      @reject="handleReject">
+      <el-form v-if="currentRow" label-width="100px" size="small">
+        <el-form-item label="ID">{{ currentRow.id }}</el-form-item>
+        <el-form-item label="商品名称">{{ detail.name || currentRow.name }}</el-form-item>
+        <el-form-item label="商品图片" v-if="detail.image || detail.coverImage">
+          <img :src="detail.image || detail.coverImage" style="max-width:220px;border-radius:4px;" />
+        </el-form-item>
+        <el-form-item label="价格">{{ detail.price != null ? detail.price : '-' }}</el-form-item>
+        <el-form-item label="库存">{{ detail.stock != null ? detail.stock : '-' }}</el-form-item>
+        <el-form-item label="分类">{{ detail.categoryName || detail.category || '-' }}</el-form-item>
+        <el-form-item label="供应商">{{ detail.vendorName || detail.supplierName || '-' }}</el-form-item>
+        <el-form-item label="商品描述">
+          <div style="white-space:pre-wrap;">{{ detail.description || '-' }}</div>
+        </el-form-item>
+        <el-form-item label="驳回原因" v-if="currentRow.rejectReason" style="color:#f56c6c;">
+          {{ currentRow.rejectReason }}
+        </el-form-item>
+      </el-form>
+    </audit-detail-drawer>
   </div>
 </template>
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
-import { getProductList, reviewProduct } from '@/api/admin'
+import { getProductList, reviewProduct, getProduct } from '@/api/admin'
+import AuditDetailDrawer from './components/AuditDetailDrawer.vue'
 
 export default {
   name: 'ProductAudit',
+  components: { AuditDetailDrawer },
   mixins: [SortMixin],
   computed: {
     tableHeight() {
@@ -69,7 +85,7 @@ export default {
     return {
       list: [], loading: false, statusFilter: 'pending',
       sortableColumns: { id: 'ID', name: '商品名称', rejectReason: '原因' },
-      rejectDialogVisible: false, rejectTarget: null, rejectReason: ''
+      detailVisible: false, currentRow: null, detail: {}, auditLoading: false
     }
   },
   mounted() { this.loadData() },
@@ -87,18 +103,27 @@ export default {
       } catch (e) { this.$message.error('加载失败')
       } finally { this.loading = false }
     },
-    async handleReview(row, action) {
+    async showDetail(row) {
+      this.currentRow = row
+      this.detail = row
+      this.detailVisible = true
       try {
-        await reviewProduct(row.id, { action, rejectReason: action === 'reject' ? this.rejectReason : '' })
+        const res = await getProduct(row.id)
+        if (res && res.data) this.detail = res.data
+      } catch (e) { /* 详情获取失败时退化为列表行数据 */ }
+    },
+    handleApprove() { this.doReview('approve') },
+    handleReject(reason) { this.doReview('reject', reason) },
+    async doReview(action, reason) {
+      if (!this.currentRow) return
+      this.auditLoading = true
+      try {
+        await reviewProduct(this.currentRow.id, { action, rejectReason: action === 'reject' ? reason : '' })
         this.$message.success('审核完成')
-        this.rejectDialogVisible = false
+        this.detailVisible = 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')
+      } catch (e) { this.$message.error('操作失败')
+      } finally { this.auditLoading = false }
     }
   }
 }
@@ -112,4 +137,4 @@ export default {
 .sort-index { color: #fff; border-radius: 8px; font-size: 10px; padding: 0 4px; margin: 0 2px; }
 .sort-asc-badge { background: #f56c6c; }
 .sort-desc-badge { background: #409eff; }
-</style>
+</style>

+ 159 - 54
cfc-web/src/views/admin/review/VendorAudit.vue

@@ -11,109 +11,213 @@
       <el-button size="small" @click="loadData" :loading="loading">刷新</el-button>
     </div>
         <div class="adm-table-wrap">
-<el-table :data="list" v-loading="loading" border stripe style="min-width:900px;" height="100%">
-      <el-table-column prop="id" label="ID" width="60">
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('id', $event)">ID<span v-if="sortIndex('id') !== null" class="sort-index" :class="sortClass('id') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('id') }}</span>{{ sortIcon('id') }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column prop="nickname" label="昵称" width="120">
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('nickname', $event)">昵称<span v-if="sortIndex('nickname') !== null" class="sort-index" :class="sortClass('nickname') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('nickname') }}</span>{{ sortIcon('nickname') }}</span>
-        </template>
+<el-table :data="pagedList" v-loading="loading" border stripe style="min-width:900px;" height="100%">
+      <el-table-column label="ID" width="80">
+        <template slot-scope="{ row }">{{ row.userId }}</template>
       </el-table-column>
-      <el-table-column prop="realName" label="姓名" width="120">
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('realName', $event)">姓名<span v-if="sortIndex('realName') !== null" class="sort-index" :class="sortClass('realName') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('realName') }}</span>{{ sortIcon('realName') }}</span>
-        </template>
+      <el-table-column prop="nickname" label="昵称" width="120" show-overflow-tooltip />
+      <el-table-column prop="realName" label="姓名" width="120" show-overflow-tooltip />
+      <el-table-column prop="phone" label="手机号" width="140" />
+      <el-table-column label="类型" width="140">
+        <template slot-scope="{ row }">{{ row.typeLabel }}</template>
       </el-table-column>
-      <el-table-column prop="phone" label="手机号" width="140">
-        <template slot="header">
-          <span class="sort-header" @click.stop="onSortClick('phone', $event)">手机号<span v-if="sortIndex('phone') !== null" class="sort-index" :class="sortClass('phone') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('phone') }}</span>{{ sortIcon('phone') }}</span>
+      <el-table-column label="来源" width="100">
+        <template slot-scope="{ row }">
+          <el-tag size="small" :type="row.source === 'service_role' ? 'success' : 'info'">
+            {{ row.source === 'service_role' ? '服务角色申请' : '入驻申请' }}
+          </el-tag>
         </template>
       </el-table-column>
-      <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>
+          <el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="操作" width="180" fixed="right">
+      <el-table-column label="提交时间" width="160">
+        <template slot-scope="{ row }">{{ row.submittedAt || '-' }}</template>
+      </el-table-column>
+      <el-table-column label="操作" width="120" 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>
+          <el-button size="mini" type="primary" @click="showDetail(row)">{{ row.status === 'pending' ? '审核' : '查看' }}</el-button>
         </template>
       </el-table-column>
     </el-table>
     </div>
 
-    <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-pagination
+      v-if="total > 0"
+      :current-page="page"
+      :page-size="size"
+      :total="total"
+      layout="total, prev, pager, next"
+      @current-change="onPageChange"
+      style="margin-top:12px;text-align:right;"
+    />
+
+    <audit-detail-drawer
+      :visible.sync="detailVisible"
+      title="服务商审核详情"
+      :status="currentRow ? currentRow.status : ''"
+      :loading="auditLoading"
+      @approve="handleApprove"
+      @reject="handleReject">
+      <el-form v-if="currentRow" label-width="110px" size="small">
+        <el-form-item label="用户ID">{{ currentRow.userId }}</el-form-item>
+        <el-form-item label="昵称">{{ currentRow.nickname || '-' }}</el-form-item>
+        <el-form-item label="姓名">{{ currentRow.realName || '-' }}</el-form-item>
+        <el-form-item label="手机号">{{ currentRow.phone || '-' }}</el-form-item>
+        <el-form-item label="类型">{{ currentRow.typeLabel }}</el-form-item>
+        <el-form-item label="来源">{{ currentRow.source === 'service_role' ? '服务角色申请' : '入驻申请' }}</el-form-item>
+        <template v-if="currentRow.source === 'service_role'">
+          <el-form-item label="身份证号">{{ currentRow.idCard || '-' }}</el-form-item>
+          <el-form-item label="身份证照片">
+            <div v-if="currentRow.idCardImages && currentRow.idCardImages.length">
+              <img v-for="(img, i) in currentRow.idCardImages" :key="'id' + i" :src="img" class="audit-img" />
+            </div>
+            <span v-else>-</span>
+          </el-form-item>
+          <el-form-item label="资质证书">
+            <div v-if="currentRow.certificateImages && currentRow.certificateImages.length">
+              <img v-for="(img, i) in currentRow.certificateImages" :key="'cert' + i" :src="img" class="audit-img" />
+            </div>
+            <span v-else>-</span>
+          </el-form-item>
+          <el-form-item label="所在地区">{{ [currentRow.province, currentRow.city, currentRow.district, currentRow.street].filter(Boolean).join(' ') || '-' }}</el-form-item>
+          <el-form-item label="详细地址">{{ currentRow.addressDetail || '-' }}</el-form-item>
+        </template>
+        <template v-else>
+          <el-form-item label="公司名称">{{ currentRow.raw.companyName || currentRow.raw.company || '-' }}</el-form-item>
+          <el-form-item label="资质说明">{{ currentRow.raw.description || currentRow.raw.qualification || '-' }}</el-form-item>
+        </template>
+        <el-form-item label="审核状态">
+          <el-tag :type="statusType(currentRow.status)">{{ statusLabel(currentRow.status) }}</el-tag>
+        </el-form-item>
+        <el-form-item label="驳回原因" v-if="currentRow.rejectReason" style="color:#f56c6c;">
+          {{ currentRow.rejectReason }}
+        </el-form-item>
+      </el-form>
+    </audit-detail-drawer>
   </div>
 </template>
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
 import { getVendorList, reviewVendor } from '@/api/vendor'
+import { listServiceRoleApplications, approveServiceRoleApplication, rejectServiceRoleApplication } from '@/api/service-role'
+import { mapServiceRoleRow, mergeAuditRows } from '@/utils/auditRowMapping'
+import AuditDetailDrawer from './components/AuditDetailDrawer.vue'
 
 export default {
   name: 'VendorAudit',
+  components: { AuditDetailDrawer },
   mixins: [SortMixin],
   computed: {
     tableHeight() {
       return window.innerHeight - 300
+    },
+    mergedList() {
+      let rows = mergeAuditRows(this.userRows, this.serviceRoleRows)
+      if (this.statusFilter) rows = rows.filter(r => r.status === this.statusFilter)
+      if (this.keyword) {
+        const kw = this.keyword.trim().toLowerCase()
+        rows = rows.filter(r => [r.nickname, r.realName, r.phone].some(v => v && String(v).toLowerCase().includes(kw)))
+      }
+      return rows
+    },
+    pagedList() {
+      const start = (this.page - 1) * this.size
+      return this.mergedList.slice(start, start + this.size)
+    },
+    total() {
+      return this.mergedList.length
     }
   },
   data() {
     return {
-      list: [], loading: false, keyword: '', statusFilter: 'pending',
+      userRows: [], serviceRoleRows: [], loading: false, keyword: '', statusFilter: 'pending',
+      page: 1, size: 20,
       sortableColumns: { id: 'ID', nickname: '昵称', realName: '姓名', phone: '手机号' },
-      rejectDialogVisible: false, rejectTarget: null, rejectReason: ''
+      detailVisible: false, currentRow: null, auditLoading: false
     }
   },
   mounted() { this.loadData() },
   methods: {
     loadList() { this.loadData() },
-    vendorTypeLabel(t) { return { planner: '成长规划师', activity_provider: '活动提供商', product_supplier: '商品供应商' }[t] || t },
+    vendorTypeLabel(t) { return { planner: '成长规划师', activity_provider: '活动提供商', product_supplier: '商品供应商', vendor: '服务商' }[t] || t },
     statusType(s) { return { pending: 'warning', approved: 'success', rejected: 'danger' }[s] || 'info' },
     statusLabel(s) { return { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[s] || s },
-    handleSearch() { this.loadData() },
+    handleSearch() { this.page = 1 },
+    onPageChange(p) { this.page = p },
+    serviceRoleStatusParam() {
+      if (this.statusFilter === 'pending') return 0
+      if (this.statusFilter === 'approved') return 1
+      if (this.statusFilter === 'rejected') return 2
+      return ''
+    },
     async loadData() {
       this.loading = true
+      this.page = 1
       try {
-        const params = { status: this.statusFilter, keyword: this.keyword || undefined, page: 1, size: 50 }
-        if (this.sortState.length > 0) params.sort = this.sortState
-        const res = await getVendorList(params)
-        this.list = res.data && res.data.records || res.data && res.data.list || []
-      } catch (e) { this.$message.error('加载失败')
-      } finally { this.loading = false }
+        const [vendorRes, srRes] = await Promise.all([
+          getVendorList({ status: this.statusFilter || undefined, page: 1, size: 500 }),
+          listServiceRoleApplications({ status: this.serviceRoleStatusParam(), role: 'vendor,activity_provider' })
+        ])
+        const records = (vendorRes && vendorRes.data && vendorRes.data.records) || (vendorRes && vendorRes.data && vendorRes.data.list) || []
+        this.userRows = records.map(r => ({
+          source: 'user',
+          userId: r.id,
+          nickname: r.nickname,
+          realName: r.realName,
+          phone: r.phone,
+          status: r.vendorStatus || 'pending',
+          rejectReason: r.rejectReason,
+          typeLabel: this.vendorTypeLabel(r.vendorType),
+          submittedAt: r.createdAt || r.updatedAt,
+          raw: r
+        }))
+        const srData = (srRes && srRes.data) || []
+        this.serviceRoleRows = srData.map(a => {
+          const row = mapServiceRoleRow(a)
+          row.typeLabel = row.roleLabel || '服务商'
+          return row
+        })
+      } catch (e) {
+        console.error(e)
+        this.$message.error('加载失败')
+      } finally {
+        this.loading = false
+      }
     },
-    async handleReview(row, action) {
+    showDetail(row) { this.currentRow = row; this.detailVisible = true },
+    async handleApprove() {
+      if (!this.currentRow) return
+      this.auditLoading = true
       try {
-        await reviewVendor({ userId: row.id, vendorStatus: action === 'approve' ? 'approved' : 'rejected' })
+        if (this.currentRow.source === 'service_role') {
+          await approveServiceRoleApplication(this.currentRow.applyId)
+        } else {
+          await reviewVendor({ userId: this.currentRow.userId, vendorStatus: 'approved' })
+        }
         this.$message.success('审核完成')
+        this.detailVisible = false
         this.loadData()
-      } catch (e) { this.$message.error('操作失败') }
+      } catch (e) { this.$message.error('操作失败')
+      } finally { this.auditLoading = false }
     },
-    showReject(row) { this.rejectTarget = row; this.rejectReason = ''; this.rejectDialogVisible = true },
-    async confirmReject() {
-      if (!this.rejectReason) return this.$message.warning('请填写拒绝原因')
+    async handleReject(reason) {
+      if (!this.currentRow) return
+      this.auditLoading = true
       try {
-        await reviewVendor({ userId: this.rejectTarget.id, vendorStatus: 'rejected', rejectReason: this.rejectReason })
+        if (this.currentRow.source === 'service_role') {
+          await rejectServiceRoleApplication(this.currentRow.applyId, reason)
+        } else {
+          await reviewVendor({ userId: this.currentRow.userId, vendorStatus: 'rejected', rejectReason: reason })
+        }
         this.$message.success('已拒绝')
-        this.rejectDialogVisible = false
+        this.detailVisible = false
         this.loadData()
-      } catch (e) { this.$message.error('操作失败') }
+      } catch (e) { this.$message.error('操作失败')
+      } finally { this.auditLoading = false }
     }
   }
 }
@@ -127,4 +231,5 @@ export default {
 .sort-index { color: #fff; border-radius: 8px; font-size: 10px; padding: 0 4px; margin: 0 2px; }
 .sort-asc-badge { background: #f56c6c; }
 .sort-desc-badge { background: #409eff; }
-</style>
+.audit-img { max-width: 160px; max-height: 110px; border-radius: 4px; margin-right: 8px; border: 1px solid #eee; }
+</style>

+ 47 - 15
cfc-web/src/views/admin/review/WithdrawAudit.vue

@@ -36,13 +36,9 @@
           <span class="sort-header" @click.stop="onSortClick('createdAt', $event)">申请时间<span v-if="sortIndex('createdAt') !== null" class="sort-index" :class="sortClass('createdAt') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('createdAt') }}</span>{{ sortIcon('createdAt') }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="操作" width="180" fixed="right">
+      <el-table-column label="操作" width="120" 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 style="color:#999">-</span>
+          <el-button size="mini" type="primary" @click="showDetail(row)">{{ row.status === 'pending' ? '审核' : '查看' }}</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -57,15 +53,41 @@
       @current-change="loadData"
       style="margin-top:12px;text-align:right;"
     />
+
+    <audit-detail-drawer
+      :visible.sync="detailVisible"
+      title="提现审核详情"
+      :status="currentRow ? currentRow.status : ''"
+      :loading="auditLoading"
+      @approve="handleApprove"
+      @reject="handleReject">
+      <el-form v-if="currentRow" label-width="100px" size="small">
+        <el-form-item label="申请ID">{{ currentRow.id }}</el-form-item>
+        <el-form-item label="用户">{{ currentRow.nickname || currentRow.userName || '-' }}</el-form-item>
+        <el-form-item label="提现金额" style="font-weight:bold;color:#f56c6c;">{{ formatPrice(currentRow.amount) }}</el-form-item>
+        <el-form-item label="账户信息">
+          <div style="white-space:pre-wrap;word-break:break-all;">{{ currentRow.accountInfo || '-' }}</div>
+        </el-form-item>
+        <el-form-item label="申请时间">{{ currentRow.createdAt || '-' }}</el-form-item>
+        <el-form-item label="审核状态">
+          <el-tag :type="statusType(currentRow.status)">{{ statusLabel(currentRow.status) }}</el-tag>
+        </el-form-item>
+        <el-form-item label="驳回原因" v-if="currentRow.remark || currentRow.rejectReason" style="color:#f56c6c;">
+          {{ currentRow.remark || currentRow.rejectReason }}
+        </el-form-item>
+      </el-form>
+    </audit-detail-drawer>
   </div>
 </template>
 
 <script>
 import SortMixin from '@/mixins/SortMixin'
 import { getWithdrawalList, auditWithdrawal } from '@/api/admin'
+import AuditDetailDrawer from './components/AuditDetailDrawer.vue'
 
 export default {
   name: 'WithdrawAudit',
+  components: { AuditDetailDrawer },
   mixins: [SortMixin],
   computed: {
     tableHeight() {
@@ -81,7 +103,8 @@ export default {
       page: 1,
       size: 20,
       total: 0,
-      sortableColumns: { id: 'ID', createdAt: '申请时间' }
+      sortableColumns: { id: 'ID', createdAt: '申请时间' },
+      detailVisible: false, currentRow: null, auditLoading: false
     }
   },
   created() {
@@ -114,26 +137,35 @@ export default {
       this.page = 1
       this.loadData()
     },
-    async handleApprove(row) {
+    async handleApprove() {
+      if (!this.currentRow) return
+      this.auditLoading = true
       try {
-        await this.$confirm('确认通过该提现申请?', '提示', { type: 'warning' })
-        await auditWithdrawal(row.id, 'approved', '')
+        await auditWithdrawal(this.currentRow.id, 'approved', '')
         this.$message.success('已通过')
+        this.detailVisible = false
         this.loadData()
       } catch (e) {
-        if (e !== 'cancel') this.$message.error('操作失败')
+        this.$message.error('操作失败')
+      } finally {
+        this.auditLoading = false
       }
     },
-    async handleReject(row) {
+    async handleReject(reason) {
+      if (!this.currentRow) return
+      this.auditLoading = true
       try {
-        await this.$confirm('确认拒绝该提现申请?', '提示', { type: 'warning' })
-        await auditWithdrawal(row.id, 'rejected', '')
+        await auditWithdrawal(this.currentRow.id, 'rejected', reason)
         this.$message.success('已拒绝')
+        this.detailVisible = false
         this.loadData()
       } catch (e) {
-        if (e !== 'cancel') this.$message.error('操作失败')
+        this.$message.error('操作失败')
+      } finally {
+        this.auditLoading = false
       }
     },
+    showDetail(row) { this.currentRow = row; this.detailVisible = true },
     formatPrice(val) {
       if (val == null) return '-'
       return '¥' + (val / 100).toFixed(2)

+ 0 - 190
cfc-web/src/views/service/ServiceRoleApplications.vue

@@ -1,190 +0,0 @@
-<template>
-  <div class="page-container">
-    <el-card>
-      <div slot="header">
-        <span>服务角色申请审核</span>
-      </div>
-            <div class="adm-table-wrap">
-<el-table :data="applications" v-loading="loading" style="width: 100%" height="100%">
-        <el-table-column prop="id" label="ID" width="60" />
-        <el-table-column label="用户" width="120">
-          <template slot-scope="{ row }">{{ row.userName || '用户' + row.userId }}</template>
-        </el-table-column>
-        <el-table-column label="申请角色" width="200">
-          <template slot-scope="{ row }">
-            <el-tag v-for="role in parseRoles(row.roles)" :key="role" style="margin-right: 4px" size="mini">
-              {{ roleMap[role] || role }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="身份证" width="200">
-          <template slot-scope="{ row }">
-            <div>{{ row.idCard }}</div>
-            <div v-if="row.idCardImages">
-              <el-image
-                v-for="(img, idx) in parseImages(row.idCardImages)"
-                :key="idx"
-                :src="img"
-                style="width: 60px; height: 40px; margin-right: 4px; cursor: pointer;"
-                :preview-src-list="parseImages(row.idCardImages)"
-              />
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column label="资质证书" width="160">
-          <template slot-scope="{ row }">
-            <el-image
-              v-if="row.certificateImages"
-              v-for="(img, idx) in parseImages(row.certificateImages)"
-              :key="'cert-' + idx"
-              :src="img"
-              style="width: 60px; height: 40px; margin-right: 4px; cursor: pointer;"
-              :preview-src-list="parseImages(row.certificateImages)"
-            />
-          </template>
-        </el-table-column>
-        <el-table-column label="地址" width="200">
-          <template slot-scope="{ row }">
-            {{ row.province }}{{ row.city }}{{ row.district }}{{ row.street }} {{ row.addressDetail }}
-          </template>
-        </el-table-column>
-        <el-table-column prop="createdAt" label="申请时间" width="160" />
-        <el-table-column label="状态" width="80">
-          <template slot-scope="{ row }">
-            <el-tag :type="statusType(row.status)">{{ statusText(row.status) }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" width="160">
-          <template slot-scope="{ row }">
-            <el-button v-if="row.status === 0" type="success" size="mini" @click="handleApprove(row)">批准</el-button>
-            <el-button v-if="row.status === 0" type="danger" size="mini" @click="handleReject(row)">驳回</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-      </div>
-    </el-card>
-
-    <el-dialog title="驳回申请" :visible.sync="rejectDialogVisible" width="400px">
-      <el-form>
-        <el-form-item label="驳回原因">
-          <el-input type="textarea" v-model="rejectReason" rows="3" placeholder="请输入驳回原因" />
-        </el-form-item>
-      </el-form>
-      <div slot="footer">
-        <el-button @click="rejectDialogVisible = false">取消</el-button>
-        <el-button type="danger" @click="confirmReject">确认驳回</el-button>
-      </div>
-    </el-dialog>
-  </div>
-</template>
-
-<script>
-import request from '@/utils/request'
-
-async function searchUsers(keyword) {
-    return await request({ url: '/api/admin/user/search', method: 'post', data: { keyword } })
-  }
-
-export default {
-  data() {
-    return {
-      applications: [],
-      loading: false,
-      rejectDialogVisible: false,
-      rejectReason: '',
-      currentRejectId: null,
-      roleMap: {
-        planner: '规划师',
-        nutritionist: '营养师',
-        butler: '管家',
-        vendor: '服务商',
-        article_admin: '文章管理员',
-        activity_provider: '活动提供商'
-      }
-    }
-  },
-  mounted() {
-    this.fetchApplications()
-  },
-  methods: {
-    async fetchApplications() {
-      this.loading = true
-      try {
-        const res = await request({ url: '/api/service-role/admin/list', method: 'post', data: { status: 0 } })
-        this.applications = res.data || []
-        await this.resolveUserNames(this.applications, 'userId')
-      } catch (e) {
-        this.$message.error('获取列表失败')
-      } finally {
-        this.loading = false
-      }
-    },
-    parseRoles(roles) {
-      try { return JSON.parse(roles) } catch (e) { return [] }
-    },
-    parseImages(images) {
-      try { return JSON.parse(images) } catch (e) { return [] }
-    },
-    resolveUserNames(items, idField) {
-      if (!items || items.length === 0) return Promise.resolve()
-      var ids = []
-      items.forEach(function(item) {
-        if (item[idField] && ids.indexOf(item[idField]) === -1) ids.push(item[idField])
-      })
-      var nameMap = {}
-      return Promise.all(ids.map(function(id) {
-        return searchUsers(String(id))
-          .then(function(res) {
-            if (res.data && res.data.length > 0) {
-              var u = res.data[0]
-              nameMap[id] = u.nickname || u.realName || '用户' + id
-            } else {
-              nameMap[id] = '用户' + id
-            }
-          })
-          .catch(function() {
-            nameMap[id] = '用户' + id
-          })
-      })).then(function() {
-        items.forEach(function(item) {
-          item.userName = nameMap[item[idField]] || '用户' + item[idField]
-        })
-      })
-    },
-    statusType(status) {
-      return { 0: 'warning', 1: 'success', 2: 'danger' }[status] || 'info'
-    },
-    statusText(status) {
-      return { 0: '待审核', 1: '已通过', 2: '已驳回' }[status] || '未知'
-    },
-    async handleApprove(row) {
-      try {
-        await request({ url: '/api/service-role/admin/approve', method: 'post', data: { id: row.id } })
-        this.$message.success('已批准')
-        this.fetchApplications()
-      } catch (e) {
-        this.$message.error('操作失败')
-      }
-    },
-    handleReject(row) {
-      this.currentRejectId = row.id
-      this.rejectReason = ''
-      this.rejectDialogVisible = true
-    },
-    async confirmReject() {
-      try {
-        await request({
-          url: '/api/service-role/admin/reject',
-          method: 'post',
-          data: { id: this.currentRejectId, reason: this.rejectReason }
-        })
-        this.$message.success('已驳回')
-        this.rejectDialogVisible = false
-        this.fetchApplications()
-      } catch (e) {
-        this.$message.error('操作失败')
-      }
-    }
-  }
-}
-</script>