Procházet zdrojové kódy

Merge branch 'refs/heads/cfclub-jiapu' into cfclub

# Conflicts:
#	cfc-frontend/pages.json
#	cfc-frontend/static/tab-wisdom-active.png
#	cfc-frontend/static/tab-wisdom.png
jiapu před 1 měsícem
rodič
revize
b92d0ebcdc

+ 104 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminSettlementController.java

@@ -0,0 +1,104 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.dto.Result;
+import com.etotem.cfc.entity.ServiceSettlement;
+import com.etotem.cfc.service.DanSettlementService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+/**
+ * DAN 测评结算管理控制器
+ * 提供管理端结算查询、确认打款、统计等功能
+ */
+@RestController
+@RequestMapping("/api/admin/dan-settlement")
+public class AdminSettlementController {
+
+    @Resource
+    private DanSettlementService settlementService;
+
+    /**
+     * 结算记录列表
+     * 服务端点:管理端查看所有结算记录
+     */
+    @PostMapping("/list")
+    public Result<List<ServiceSettlement>> list(@RequestBody SettlementListRequest req) {
+        List<ServiceSettlement> settlements = settlementService.getAllSettlements();
+        return Result.success(settlements);
+    }
+
+    /**
+     * 结算详情
+     * 服务端点:查看单条结算记录的详细信息
+     */
+    @GetMapping("/detail/{settlementId}")
+    public Result<ServiceSettlement> detail(@PathVariable Long settlementId) {
+        ServiceSettlement settlement = settlementService.getSettlementById(settlementId);
+        return Result.success(settlement);
+    }
+
+    /**
+     * 确认结算/打款
+     * 服务端点:管理员手动确认结算并标记为已打款
+     */
+    @PostMapping("/confirm/{settlementId}")
+    public Result<Void> confirm(@PathVariable Long settlementId) {
+        settlementService.updateSettlementStatus(settlementId, "settled");
+        return Result.success();
+    }
+
+    /**
+     * 结算统计
+     * 服务端点:获取结算统计数据(总金额、待结算、已结算)
+     */
+    @PostMapping("/stats")
+    public Result<SettlementStats> stats() {
+        SettlementStats stats = settlementService.getSettlementStats();
+        return Result.success(stats);
+    }
+}
+
+// ==================== DTO 定义 ====================
+
+/**
+ * 结算列表查询请求
+ */
+class SettlementListRequest {
+    private String status; // pending/settled/failed
+    private Long providerId;
+    private String providerType; // assessor/planner
+    
+    public String getStatus() { return status; }
+    public void setStatus(String status) { this.status = status; }
+    public Long getProviderId() { return providerId; }
+    public void setProviderId(Long providerId) { this.providerId = providerId; }
+    public String getProviderType() { return providerType; }
+    public void setProviderType(String providerType) { this.providerType = providerType; }
+}
+
+/**
+ * 结算统计数据
+ */
+class SettlementStats {
+    private Long totalAmount; // 总金额(分)
+    private Long pendingAmount; // 待结算金额(分)
+    private Long settledAmount; // 已结算金额(分)
+    private Integer totalCount; // 总笔数
+    private Integer pendingCount; // 待结算笔数
+    private Integer settledCount; // 已结算笔数
+
+    public Long getTotalAmount() { return totalAmount; }
+    public void setTotalAmount(Long totalAmount) { this.totalAmount = totalAmount; }
+    public Long getPendingAmount() { return pendingAmount; }
+    public void setPendingAmount(Long pendingAmount) { this.pendingAmount = pendingAmount; }
+    public Long getSettledAmount() { return settledAmount; }
+    public void setSettledAmount(Long settledAmount) { this.settledAmount = settledAmount; }
+    public Integer getTotalCount() { return totalCount; }
+    public void setTotalCount(Integer totalCount) { this.totalCount = totalCount; }
+    public Integer getPendingCount() { return pendingCount; }
+    public void setPendingCount(Integer pendingCount) { this.pendingCount = pendingCount; }
+    public Integer getSettledCount() { return settledCount; }
+    public void setSettledCount(Integer settledCount) { this.settledCount = settledCount; }
+}

+ 214 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/dan/DanAssessmentExecutionController.java

@@ -0,0 +1,214 @@
+package com.etotem.cfc.controller.dan;
+
+import com.etotem.cfc.dto.*;
+import com.etotem.cfc.service.DanAssessmentExecutionService;
+import com.etotem.cfc.util.JwtUtil;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+
+/**
+ * DAN 测评执行服务控制器
+ * 提供服务完成、用户确认、评价等核心流程 API
+ */
+@RestController
+@RequestMapping("/api/dan-execution")
+public class DanAssessmentExecutionController {
+
+    @Resource
+    private DanAssessmentExecutionService executionService;
+
+    @Resource
+    private JwtUtil jwtUtil;
+
+    /**
+     * 规划师/评估师完成服务
+     * 服务端点:规划师/评估师填写完成后的备注并标记完成
+     */
+    @PostMapping("/complete")
+    public Result<Void> complete(@RequestBody CompleteRequest req, HttpServletRequest request) {
+        String token = request.getHeader("Authorization");
+        if (token == null || !token.startsWith("Bearer ")) {
+            return Result.error("未提供认证 Token");
+        }
+        Long userId = jwtUtil.getUserIdFromToken(token.replace("Bearer ", ""));
+        if (userId == null) {
+            return Result.error("Token 无效");
+        }
+        
+        // 调用 service 完成服务(assessor/planner角色在 service 层判断)
+        executionService.completeExecution(req.getExecutionId(), "planner", req.getNotes());
+        return Result.success();
+    }
+
+    /**
+     * 用户确认服务完成
+     * 服务端点:用户确认服务已完成,触发自动结算
+     */
+    @PostMapping("/confirm")
+    public Result<Void> confirm(@RequestBody ConfirmRequest req, HttpServletRequest request) {
+        String token = request.getHeader("Authorization");
+        if (token == null || !token.startsWith("Bearer ")) {
+            return Result.error("未提供认证 Token");
+        }
+        Long userId = jwtUtil.getUserIdFromToken(token.replace("Bearer ", ""));
+        if (userId == null) {
+            return Result.error("Token 无效");
+        }
+        
+        executionService.confirmExecution(req.getExecutionId(), 1);
+        return Result.success();
+    }
+
+    /**
+     * 用户评价服务
+     * 服务端点:用户对评估师和规划师进行评分和评论
+     */
+    @PostMapping("/rate")
+    public Result<ServiceRatingDTO> rate(@RequestBody ServiceRateRequest req, HttpServletRequest request) {
+        String token = request.getHeader("Authorization");
+        if (token == null || !token.startsWith("Bearer ")) {
+            return Result.error("未提供认证 Token");
+        }
+        Long userId = jwtUtil.getUserIdFromToken(token.replace("Bearer ", ""));
+        if (userId == null) {
+            return Result.error("Token 无效");
+        }
+        
+        req.setUserId(userId);
+        ServiceRating rating = executionService.rateService(req);
+        return Result.success(new ServiceRatingDTO(rating));
+    }
+
+    /**
+     * 获取执行详情(含结算信息)
+     * 服务端点:查看当前执行记录的完整状态,包括结算进度
+     */
+    @GetMapping("/detail/{executionId}")
+    public Result<ExecutionDetailResponse> detail(@PathVariable Long executionId, HttpServletRequest request) {
+        ExecutionDetailResponse resp = executionService.getExecutionDetail(executionId);
+        return Result.success(resp);
+    }
+
+    /**
+     * 用户执行记录列表
+     * 服务端点:查看家庭所有服务的执行记录
+     */
+    @PostMapping("/my-list")
+    public Result<List<DanAssessmentExecution>> myList(@RequestBody MyExecutionFilter filter, HttpServletRequest request) {
+        Long userId = jwtUtil.getUserIdFromToken(request.getHeader("Authorization"));
+        List<DanAssessmentExecution> executions = executionService.getByFamilyId(filter.getFamilyId());
+        return Result.success(executions);
+    }
+
+    /**
+     * 规划师待执行任务列表
+     * 服务端点:规划师查看自己负责的服务执行任务
+     */
+    @GetMapping("/guide/my-tasks")
+    public Result<List<DanAssessmentExecution>> guideMyTasks(
+            @RequestParam Long plannerId, 
+            HttpServletRequest request) {
+        
+        List<DanAssessmentExecution> tasks = executionService.getByPlannerId(plannerId);
+        return Result.success(tasks);
+    }
+
+    /**
+     * 查询某订单对应的执行记录
+     * 服务端点:根据订单 ID 获取执行记录
+     */
+    @GetMapping("/by-order/{orderId}")
+    public Result<DanAssessmentExecution> getByOrder(@PathVariable Long orderId) {
+        DanAssessmentExecution exec = executionService.getByOrderId(orderId);
+        return Result.success(exec);
+    }
+}
+
+// ==================== DTO 定义 ====================
+
+/**
+ * 完成服务请求
+ */
+class CompleteRequest {
+    private Long executionId;
+    private String notes;
+    
+    public Long getExecutionId() { return executionId; }
+    public void setExecutionId(Long executionId) { this.executionId = executionId; }
+    public String getNotes() { return notes; }
+    public void setNotes(String notes) { this.notes = notes; }
+}
+
+/**
+ * 确认服务请求
+ */
+class ConfirmRequest {
+    private Long executionId;
+    
+    public Long getExecutionId() { return executionId; }
+    public void setExecutionId(Long executionId) { this.executionId = executionId; }
+}
+
+/**
+ * 查询执行记录过滤条件
+ */
+class MyExecutionFilter {
+    private Long familyId;
+    
+    public Long getFamilyId() { return familyId; }
+    public void setFamilyId(Long familyId) { this.familyId = familyId; }
+}
+
+/**
+ * 执行结果数据转换
+ */
+class ServiceRatingDTO {
+    private Long id;
+    private Long executionId;
+    private Long userId;
+    private Long assessorId;
+    private Integer assessorRating;
+    private String assessorComment;
+    private Long plannerId;
+    private Integer plannerRating;
+    private String plannerComment;
+    private String createdAt;
+
+    public ServiceRatingDTO(com.etotem.cfc.entity.ServiceRating rating) {
+        this.id = rating.getId();
+        this.executionId = rating.getExecutionId();
+        this.userId = rating.getUserId();
+        this.assessorId = rating.getAssessorId();
+        this.assessorRating = rating.getAssessorRating();
+        this.assessorComment = rating.getAssessorComment();
+        this.plannerId = rating.getPlannerId();
+        this.plannerRating = rating.getPlannerRating();
+        this.plannerComment = rating.getPlannerComment();
+        this.createdAt = rating.getCreatedAt().toString();
+    }
+
+    // Getters and Setters
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public Long getExecutionId() { return executionId; }
+    public void setExecutionId(Long executionId) { this.executionId = executionId; }
+    public Long getUserId() { return userId; }
+    public void setUserId(Long userId) { this.userId = userId; }
+    public Long getAssessorId() { return assessorId; }
+    public void setAssessorId(Long assessorId) { this.assessorId = assessorId; }
+    public Integer getAssessorRating() { return assessorRating; }
+    public void setAssessorRating(Integer assessorRating) { this.assessorRating = assessorRating; }
+    public String getAssessorComment() { return assessorComment; }
+    public void setAssessorComment(String assessorComment) { this.assessorComment = assessorComment; }
+    public Long getPlannerId() { return plannerId; }
+    public void setPlannerId(Long plannerId) { this.plannerId = plannerId; }
+    public Integer getPlannerRating() { return plannerRating; }
+    public void setPlannerRating(Integer plannerRating) { this.plannerRating = plannerRating; }
+    public String getPlannerComment() { return plannerComment; }
+    public void setPlannerComment(String plannerComment) { this.plannerComment = plannerComment; }
+    public String getCreatedAt() { return createdAt; }
+    public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
+}

+ 26 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/SettlementStats.java

@@ -0,0 +1,26 @@
+package com.etotem.cfc.dto;
+
+/**
+ * 结算统计数据
+ */
+public class SettlementStats {
+    private Long totalAmount; // 总金额(分)
+    private Long pendingAmount; // 待结算金额(分)
+    private Long settledAmount; // 已结算金额(分)
+    private Integer totalCount; // 总笔数
+    private Integer pendingCount; // 待结算笔数
+    private Integer settledCount; // 已结算笔数
+
+    public Long getTotalAmount() { return totalAmount; }
+    public void setTotalAmount(Long totalAmount) { this.totalAmount = totalAmount; }
+    public Long getPendingAmount() { return pendingAmount; }
+    public void setPendingAmount(Long pendingAmount) { this.pendingAmount = pendingAmount; }
+    public Long getSettledAmount() { return settledAmount; }
+    public void setSettledAmount(Long settledAmount) { this.settledAmount = settledAmount; }
+    public Integer getTotalCount() { return totalCount; }
+    public void setTotalCount(Integer totalCount) { this.totalCount = totalCount; }
+    public Integer getPendingCount() { return pendingCount; }
+    public void setPendingCount(Integer pendingCount) { this.pendingCount = pendingCount; }
+    public Integer getSettledCount() { return settledCount; }
+    public void setSettledCount(Integer settledCount) { this.settledCount = settledCount; }
+}

+ 10 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DanAssessmentExecutionService.java

@@ -165,4 +165,14 @@ public class DanAssessmentExecutionService {
                 .eq(DanAssessmentExecution::getFamilyId, familyId)
                 .orderByDesc(DanAssessmentExecution::getCreatedAt));
     }
+
+    /**
+     * 查询规划师的待执行任务列表
+     */
+    public List<DanAssessmentExecution> getByPlannerId(Long plannerId) {
+        return executionMapper.selectList(new LambdaQueryWrapper<DanAssessmentExecution>()
+                .eq(DanAssessmentExecution::getPlannerId, plannerId)
+                .ne(DanAssessmentExecution::getPlannerStatus, "completed")
+                .orderByAsc(DanAssessmentExecution::getCreatedAt));
+    }
 }

+ 56 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DanSettlementService.java

@@ -118,6 +118,11 @@ public class DanSettlementService {
     /**
      * 更新结算状态(确认/打款)
      */
+    @Transactional
+    public void updateSettlementStatus(Long settlementId, String status) {
+        updateSettlementStatus(settlementId, status, null);
+    }
+
     @Transactional
     public void updateSettlementStatus(Long settlementId, String status, String transactionId) {
         ServiceSettlement s = settlementMapper.selectById(settlementId);
@@ -133,4 +138,55 @@ public class DanSettlementService {
             settlementMapper.updateById(s);
         }
     }
+
+    /**
+     * 获取所有结算记录(管理端用)
+     */
+    public java.util.List<ServiceSettlement> getAllSettlements() {
+        return settlementMapper.selectList(new LambdaQueryWrapper<ServiceSettlement>()
+                .orderByDesc(ServiceSettlement::getCreatedAt));
+    }
+
+    /**
+     * 获取单条结算记录(管理端用)
+     */
+    public ServiceSettlement getSettlementById(Long settlementId) {
+        return settlementMapper.selectById(settlementId);
+    }
+
+    /**
+     * 获取结算统计数据(管理端用)
+     */
+    public com.etotem.cfc.dto.SettlementStats getSettlementStats() {
+        java.util.List<ServiceSettlement> all = getAllSettlements();
+        com.etotem.cfc.dto.SettlementStats stats = new com.etotem.cfc.dto.SettlementStats();
+        
+        long totalAmount = 0;
+        long pendingAmount = 0;
+        long settledAmount = 0;
+        int totalCount = all.size();
+        int pendingCount = 0;
+        int settledCount = 0;
+        
+        for (ServiceSettlement s : all) {
+            if (s.getCommissionAmount() != null) {
+                totalAmount += s.getCommissionAmount();
+                if ("pending".equals(s.getStatus())) {
+                    pendingAmount += s.getCommissionAmount();
+                    pendingCount++;
+                } else if ("settled".equals(s.getStatus())) {
+                    settledAmount += s.getCommissionAmount();
+                    settledCount++;
+                }
+            }
+        }
+        
+        stats.setTotalAmount(totalAmount);
+        stats.setPendingAmount(pendingAmount);
+        stats.setSettledAmount(settledAmount);
+        stats.setTotalCount(totalCount);
+        stats.setPendingCount(pendingCount);
+        stats.setSettledCount(settledCount);
+        return stats;
+    }
 }

+ 22 - 4
cfc-frontend/pages.json

@@ -76,6 +76,12 @@
           "style": {
             "navigationBarTitleText": "我的"
           }
+        },
+        {
+          "path": "service-tasks",
+          "style": {
+            "navigationBarTitleText": "待办服务任务"
+          }
         }
       ]
     },
@@ -350,6 +356,18 @@
           "style": {
             "navigationBarTitleText": "上传测评报告"
           }
+        },
+        {
+          "path": "execution",
+          "style": {
+            "navigationBarTitleText": "服务执行进度"
+          }
+        },
+        {
+          "path": "rating",
+          "style": {
+            "navigationBarTitleText": "服务评价"
+          }
         }
       ]
     },
@@ -1365,14 +1383,14 @@
       {
         "pagePath": "pages/mind/index",
         "text": "心",
-        "iconPath": "static/tab-mind.png",
-        "selectedIconPath": "static/tab-mind-active.png"
+        "iconPath": "/static/tab-mind.png",
+        "selectedIconPath": "/static/tab-mind-active.png"
       },
       {
         "pagePath": "pages/body/index",
         "text": "身",
-        "iconPath": "static/tab-body.png",
-        "selectedIconPath": "static/tab-body-active.png"
+        "iconPath": "/static/tab-body.png",
+        "selectedIconPath": "/static/tab-body-active.png"
       },
       {
         "pagePath": "pages/wisdom/index",

+ 590 - 0
cfc-frontend/pages/assessment/execution.vue

@@ -0,0 +1,590 @@
+<template>
+  <view class="container">
+    <!-- 执行状态头部 -->
+    <view class="header">
+      <text class="title">服务执行进度</text>
+      <text class="subtitle">查看测评服务完成情况</text>
+    </view>
+
+    <!-- 加载状态 -->
+    <view v-if="loading" class="loading">
+      <text>加载中...</text>
+    </view>
+
+    <!-- 执行记录详情 -->
+    <view v-else-if="execution" class="execution-detail">
+      <!-- 状态卡片 -->
+      <view class="status-card">
+        <view class="status-icon" :class="getStatusClass(execution)">
+          <view class="icon-circle"></view>
+          <view v-if="getStatusClass(execution) === 'completed'" class="icon-check"></view>
+          <view v-else-if="getStatusClass(execution) === 'ready'" class="icon-alert"></view>
+          <view v-else class="icon-dots"><view></view><view></view><view></view></view>
+        </view>
+        <text class="status-text">{{ getStatusText(execution) }}</text>
+      </view>
+
+      <!-- 进度时间线 -->
+      <view class="timeline">
+        <view class="timeline-item" :class="{ active: execution.assessorStatus === 'completed' }">
+          <view class="timeline-dot"><view v-if="execution.assessorStatus === 'completed'" class="dot-check"></view></view>
+          <view class="timeline-content">
+            <view class="timeline-header">
+              <view class="timeline-icon assessor-icon"></view>
+              <text class="timeline-title">评估师服务</text>
+            </view>
+            <text class="timeline-desc">{{ execution.assessorStatus === 'completed' ? '已完成' : '进行中' }}</text>
+            <text v-if="execution.assessorCompletedAt" class="timeline-time">
+              {{ formatTime(execution.assessorCompletedAt) }}
+            </text>
+          </view>
+        </view>
+
+        <view v-if="execution.plannerId" class="timeline-item" :class="{ active: execution.plannerStatus === 'completed' }">
+          <view class="timeline-dot"><view v-if="execution.plannerStatus === 'completed'" class="dot-check"></view></view>
+          <view class="timeline-content">
+            <view class="timeline-header">
+              <view class="timeline-icon planner-icon"></view>
+              <text class="timeline-title">规划师服务</text>
+            </view>
+            <text class="timeline-desc">{{ execution.plannerStatus === 'completed' ? '已完成' : '进行中' }}</text>
+            <text v-if="execution.plannerCompletedAt" class="timeline-time">
+              {{ formatTime(execution.plannerCompletedAt) }}
+            </text>
+          </view>
+        </view>
+
+        <view class="timeline-item" :class="{ active: execution.userConfirmed === 1 }">
+          <view class="timeline-dot"><view v-if="execution.userConfirmed === 1" class="dot-check"></view></view>
+          <view class="timeline-content">
+            <view class="timeline-header">
+              <view class="timeline-icon confirm-icon"></view>
+              <text class="timeline-title">用户确认</text>
+            </view>
+            <text class="timeline-desc">{{ execution.userConfirmed === 1 ? '已确认' : '待确认' }}</text>
+            <text v-if="execution.userConfirmedAt" class="timeline-time">
+              {{ formatTime(execution.userConfirmedAt) }}
+            </text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 服务备注 -->
+      <view v-if="execution.assessorNotes || execution.plannerNotes" class="notes-section">
+        <text class="section-title">服务备注</text>
+        <view v-if="execution.assessorNotes" class="note-item">
+          <text class="note-label">评估师:</text>
+          <text class="note-content">{{ execution.assessorNotes }}</text>
+        </view>
+        <view v-if="execution.plannerNotes" class="note-item">
+          <text class="note-label">规划师:</text>
+          <text class="note-content">{{ execution.plannerNotes }}</text>
+        </view>
+      </view>
+
+      <!-- 操作按钮 -->
+      <view class="actions">
+        <button v-if="canConfirm" class="btn-primary" @click="confirmService">
+          确认服务完成
+        </button>
+        <button v-if="canRate" class="btn-secondary" @click="goToRating">
+          评价服务
+        </button>
+        <button class="btn-outline" @click="goBack">
+          返回
+        </button>
+      </view>
+    </view>
+
+    <!-- 无数据 -->
+    <view v-else class="empty">
+      <text>暂无执行记录</text>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      executionId: null,
+      execution: null,
+      loading: true
+    }
+  },
+  computed: {
+    canConfirm() {
+      if (!this.execution) return false
+      // 评估师和规划师(如果有)都完成后才能确认
+      const assessorDone = this.execution.assessorStatus === 'completed'
+      const plannerDone = !this.execution.plannerId || this.execution.plannerStatus === 'completed'
+      return assessorDone && plannerDone && this.execution.userConfirmed === 0
+    },
+    canRate() {
+      if (!this.execution) return false
+      return this.execution.userConfirmed === 1
+    }
+  },
+  onLoad(options) {
+    if (options.executionId) {
+      this.executionId = options.executionId
+      this.loadExecution()
+    }
+  },
+  methods: {
+    async loadExecution() {
+      this.loading = true
+      try {
+        const res = await uni.request({
+          url: `${getApp().globalData.apiBase}/api/dan-execution/detail/${this.executionId}`,
+          method: 'GET',
+          header: {
+            'Authorization': `Bearer ${uni.getStorageSync('token')}`
+          }
+        })
+        if (res.data.code === 200) {
+          this.execution = res.data.data
+        } else {
+          uni.showToast({ title: res.data.message || '加载失败', icon: 'none' })
+        }
+      } catch (e) {
+        console.error('加载执行记录失败', e)
+        uni.showToast({ title: '网络错误', icon: 'none' })
+      } finally {
+        this.loading = false
+      }
+    },
+    async confirmService() {
+      uni.showModal({
+        title: '确认服务',
+        content: '确认服务已完成?确认后可以进行评价。',
+        success: async (res) => {
+          if (res.confirm) {
+            try {
+              const response = await uni.request({
+                url: `${getApp().globalData.apiBase}/api/dan-execution/confirm`,
+                method: 'POST',
+                header: {
+                  'Authorization': `Bearer ${uni.getStorageSync('token')}`,
+                  'Content-Type': 'application/json'
+                },
+                data: { executionId: this.executionId }
+              })
+              if (response.data.code === 200) {
+                uni.showToast({ title: '确认成功', icon: 'success' })
+                this.loadExecution()
+              } else {
+                uni.showToast({ title: response.data.message || '确认失败', icon: 'none' })
+              }
+            } catch (e) {
+              console.error('确认服务失败', e)
+              uni.showToast({ title: '网络错误', icon: 'none' })
+            }
+          }
+        }
+      })
+    },
+    goToRating() {
+      uni.navigateTo({
+        url: `/pages/assessment/rating?executionId=${this.executionId}`
+      })
+    },
+    goBack() {
+      uni.navigateBack()
+    },
+    formatTime(timeStr) {
+      if (!timeStr) return ''
+      const date = new Date(timeStr)
+      return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
+    },
+    getStatusClass(exec) {
+      if (exec.userConfirmed === 1) return 'completed'
+      if (exec.assessorStatus === 'completed' && (!exec.plannerId || exec.plannerStatus === 'completed')) return 'ready'
+      return 'pending'
+    },
+    getStatusIcon(exec) {
+      if (exec.userConfirmed === 1) return '✓'
+      if (exec.assessorStatus === 'completed' && (!exec.plannerId || exec.plannerStatus === 'completed')) return '!'
+      return '⋯'
+    },
+    getStatusText(exec) {
+      if (exec.userConfirmed === 1) return '服务已完成'
+      if (exec.assessorStatus === 'completed' && (!exec.plannerId || exec.plannerStatus === 'completed')) return '待您确认'
+      if (exec.assessorStatus === 'completed') return '规划师服务中'
+      return '评估师服务中'
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 40rpx;
+  background: #f5f5f5;
+  min-height: 100vh;
+}
+
+.header {
+  margin-bottom: 40rpx;
+}
+
+.title {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 10rpx;
+}
+
+.subtitle {
+  font-size: 28rpx;
+  color: #999;
+}
+
+.loading, .empty {
+  text-align: center;
+  padding: 100rpx 0;
+  color: #999;
+}
+
+.status-card {
+  background: white;
+  border-radius: 20rpx;
+  padding: 60rpx 40rpx;
+  text-align: center;
+  margin-bottom: 30rpx;
+  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
+}
+
+.status-icon {
+  width: 120rpx;
+  height: 120rpx;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin: 0 auto 30rpx;
+  position: relative;
+}
+
+.icon-circle {
+  width: 100%;
+  height: 100%;
+  border-radius: 50%;
+  position: absolute;
+  top: 0;
+  left: 0;
+}
+
+/* CSS 对勾图标 */
+.icon-check {
+  width: 40rpx;
+  height: 24rpx;
+  border-left: 6rpx solid #fff;
+  border-bottom: 6rpx solid #fff;
+  transform: rotate(-45deg);
+  margin-top: -6rpx;
+  z-index: 1;
+}
+
+/* CSS 感叹号图标 */
+.icon-alert {
+  width: 6rpx;
+  height: 36rpx;
+  background: #fff;
+  border-radius: 3rpx;
+  position: relative;
+  z-index: 1;
+}
+.icon-alert::after {
+  content: '';
+  position: absolute;
+  bottom: -20rpx;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 10rpx;
+  height: 10rpx;
+  background: #fff;
+  border-radius: 50%;
+}
+
+/* CSS 加载中三个点图标 */
+.icon-dots {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 10rpx;
+  z-index: 1;
+}
+.icon-dots view {
+  width: 12rpx;
+  height: 12rpx;
+  background: #fff;
+  border-radius: 50%;
+}
+
+.status-icon.pending {
+  background: linear-gradient(135deg, #ffe0b2, #ffcc80);
+}
+
+.status-icon.pending .icon-circle {
+  background: linear-gradient(135deg, #ff9800, #f57c00);
+}
+
+.status-icon.ready {
+  background: linear-gradient(135deg, #bbdefb, #90caf9);
+}
+
+.status-icon.ready .icon-circle {
+  background: linear-gradient(135deg, #2196f3, #1976d2);
+}
+
+.status-icon.completed {
+  background: linear-gradient(135deg, #c8e6c9, #a5d6a7);
+}
+
+.status-icon.completed .icon-circle {
+  background: linear-gradient(135deg, #4caf50, #388e3c);
+}
+
+.status-text {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.timeline {
+  background: white;
+  border-radius: 20rpx;
+  padding: 40rpx;
+  margin-bottom: 30rpx;
+}
+
+.timeline-item {
+  display: flex;
+  margin-bottom: 40rpx;
+  position: relative;
+}
+
+.timeline-item:last-child {
+  margin-bottom: 0;
+}
+
+.timeline-item::before {
+  content: '';
+  position: absolute;
+  left: 15rpx;
+  top: 30rpx;
+  bottom: -40rpx;
+  width: 2rpx;
+  background: #e0e0e0;
+}
+
+.timeline-item:last-child::before {
+  display: none;
+}
+
+.timeline-dot {
+  width: 30rpx;
+  height: 30rpx;
+  border-radius: 50%;
+  background: #e0e0e0;
+  margin-right: 30rpx;
+  flex-shrink: 0;
+  z-index: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.dot-check {
+  width: 12rpx;
+  height: 8rpx;
+  border-left: 3rpx solid #fff;
+  border-bottom: 3rpx solid #fff;
+  transform: rotate(-45deg);
+  margin-top: -2rpx;
+}
+
+.timeline-item.active .timeline-dot {
+  background: #4caf50;
+}
+
+/* 时间线头部(图标+标题) */
+.timeline-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 10rpx;
+}
+
+.timeline-icon {
+  width: 36rpx;
+  height: 36rpx;
+  border-radius: 8rpx;
+  margin-right: 16rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+}
+
+/* 评估师图标 */
+.assessor-icon {
+  background: linear-gradient(135deg, #e3f2fd, #bbdefb);
+}
+.assessor-icon::after {
+  content: '';
+  width: 16rpx;
+  height: 16rpx;
+  border: 3rpx solid #2196f3;
+  border-radius: 50%;
+  position: absolute;
+  top: 8rpx;
+  left: 8rpx;
+}
+.assessor-icon::before {
+  content: '';
+  width: 20rpx;
+  height: 3rpx;
+  background: #2196f3;
+  position: absolute;
+  bottom: 10rpx;
+  left: 8rpx;
+  border-radius: 2rpx;
+}
+
+/* 规划师图标 */
+.planner-icon {
+  background: linear-gradient(135deg, #e8f5e9, #c8e6c9);
+}
+.planner-icon::after {
+  content: '';
+  width: 16rpx;
+  height: 16rpx;
+  border: 3rpx solid #4caf50;
+  border-radius: 50%;
+  position: absolute;
+  top: 8rpx;
+  left: 8rpx;
+}
+.planner-icon::before {
+  content: '';
+  width: 20rpx;
+  height: 3rpx;
+  background: #4caf50;
+  position: absolute;
+  bottom: 10rpx;
+  left: 8rpx;
+  border-radius: 2rpx;
+}
+
+/* 确认图标 */
+.confirm-icon {
+  background: linear-gradient(135deg, #fff3e0, #ffe0b2);
+}
+.confirm-icon::after {
+  content: '';
+  width: 16rpx;
+  height: 16rpx;
+  border: 3rpx solid #ff9800;
+  border-radius: 50%;
+  position: absolute;
+  top: 8rpx;
+  left: 8rpx;
+}
+.confirm-icon::before {
+  content: '';
+  width: 20rpx;
+  height: 3rpx;
+  background: #ff9800;
+  position: absolute;
+  bottom: 10rpx;
+  left: 8rpx;
+  border-radius: 2rpx;
+}
+
+.timeline-content {
+  flex: 1;
+}
+
+.timeline-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 10rpx;
+}
+
+.timeline-desc {
+  font-size: 26rpx;
+  color: #666;
+  display: block;
+  margin-bottom: 5rpx;
+}
+
+.timeline-time {
+  font-size: 24rpx;
+  color: #999;
+}
+
+.notes-section {
+  background: white;
+  border-radius: 20rpx;
+  padding: 40rpx;
+  margin-bottom: 30rpx;
+}
+
+.section-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 20rpx;
+}
+
+.note-item {
+  margin-bottom: 20rpx;
+}
+
+.note-label {
+  font-size: 28rpx;
+  color: #666;
+  font-weight: bold;
+}
+
+.note-content {
+  font-size: 28rpx;
+  color: #333;
+}
+
+.actions {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+
+.btn-primary, .btn-secondary, .btn-outline {
+  border-radius: 20rpx;
+  font-size: 32rpx;
+  padding: 30rpx;
+  text-align: center;
+}
+
+.btn-primary {
+  background: #1E3A5F;
+  color: white;
+  border: none;
+}
+
+.btn-secondary {
+  background: #ff9800;
+  color: white;
+  border: none;
+}
+
+.btn-outline {
+  background: white;
+  color: #666;
+  border: 2rpx solid #ddd;
+}
+</style>

+ 364 - 0
cfc-frontend/pages/assessment/rating.vue

@@ -0,0 +1,364 @@
+<template>
+  <view class="container">
+    <!-- 头部 -->
+    <view class="header">
+      <text class="title">服务评价</text>
+      <text class="subtitle">请对我们的服务进行评价</text>
+    </view>
+
+    <!-- 加载状态 -->
+    <view v-if="loading" class="loading">
+      <text>加载中...</text>
+    </view>
+
+    <view v-else-if="execution" class="rating-form">
+      <!-- 评估师评价 -->
+      <view v-if="execution.assessorId" class="rating-section">
+        <view class="section-header">
+          <view class="section-icon assessor-section-icon"></view>
+          <text class="section-title">评估师服务评价</text>
+        </view>
+        <view class="rating-stars">
+          <text class="star-label">评分:</text>
+          <view class="stars">
+            <text 
+              v-for="i in 5" 
+              :key="i" 
+              class="star" 
+              :class="{ active: assessorRating >= i }"
+              @click="setAssessorRating(i)">
+              ★
+            </text>
+          </view>
+          <text v-if="assessorRating > 0" class="rating-text">{{ ratingTexts[assessorRating - 1] }}</text>
+        </view>
+        <view class="comment-input">
+          <textarea 
+            v-model="assessorComment" 
+            placeholder="请分享您的服务体验(选填)"
+            maxlength="500"
+            class="textarea"
+          />
+        </view>
+      </view>
+
+      <!-- 规划师评价 -->
+      <view v-if="execution.plannerId" class="rating-section">
+        <view class="section-header">
+          <view class="section-icon planner-section-icon"></view>
+          <text class="section-title">规划师服务评价</text>
+        </view>
+        <view class="rating-stars">
+          <text class="star-label">评分:</text>
+          <view class="stars">
+            <text 
+              v-for="i in 5" 
+              :key="i" 
+              class="star" 
+              :class="{ active: plannerRating >= i }"
+              @click="setPlannerRating(i)">
+              ★
+            </text>
+          </view>
+          <text v-if="plannerRating > 0" class="rating-text">{{ ratingTexts[plannerRating - 1] }}</text>
+        </view>
+        <view class="comment-input">
+          <textarea 
+            v-model="plannerComment" 
+            placeholder="请分享您的服务体验(选填)"
+            maxlength="500"
+            class="textarea"
+          />
+        </view>
+      </view>
+
+      <!-- 提交按钮 -->
+      <view class="actions">
+        <button class="btn-primary" @click="submitRating" :disabled="!canSubmit">
+          提交评价
+        </button>
+        <button class="btn-outline" @click="goBack">
+          返回
+        </button>
+      </view>
+
+    <!-- 无数据 -->
+    <view v-else class="empty">
+      <text>暂无可评价的服务</text>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      executionId: null,
+      execution: null,
+      loading: true,
+      assessorRating: 0,
+      assessorComment: '',
+      plannerRating: 0,
+      plannerComment: '',
+      ratingTexts: ['很差', '较差', '一般', '满意', '非常满意']
+    }
+  },
+  computed: {
+    canSubmit() {
+      // 至少评估师评分必须填写
+      return this.assessorRating > 0
+    }
+  },
+  onLoad(options) {
+    if (options.executionId) {
+      this.executionId = options.executionId
+      this.loadExecution()
+    }
+  },
+  methods: {
+    async loadExecution() {
+      this.loading = true
+      try {
+        const res = await uni.request({
+          url: `${getApp().globalData.apiBase}/api/dan-execution/detail/${this.executionId}`,
+          method: 'GET',
+          header: {
+            'Authorization': `Bearer ${uni.getStorageSync('token')}`
+          }
+        })
+        if (res.data.code === 200) {
+          this.execution = res.data.data
+        } else {
+          uni.showToast({ title: res.data.message || '加载失败', icon: 'none' })
+        }
+      } catch (e) {
+        console.error('加载执行记录失败', e)
+        uni.showToast({ title: '网络错误', icon: 'none' })
+      } finally {
+        this.loading = false
+      }
+    },
+    setAssessorRating(rating) {
+      this.assessorRating = rating
+    },
+    setPlannerRating(rating) {
+      this.plannerRating = rating
+    },
+    async submitRating() {
+      if (!this.canSubmit) {
+        uni.showToast({ title: '请至少为评估师评分', icon: 'none' })
+        return
+      }
+
+      try {
+        const data = {
+          executionId: this.executionId,
+          assessorId: this.execution.assessorId,
+          assessorRating: this.assessorRating,
+          assessorComment: this.assessorComment
+        }
+
+        if (this.execution.plannerId) {
+          data.plannerId = this.execution.plannerId
+          data.plannerRating = this.plannerRating
+          data.plannerComment = this.plannerComment
+        }
+
+        const res = await uni.request({
+          url: `${getApp().globalData.apiBase}/api/dan-execution/rate`,
+          method: 'POST',
+          header: {
+            'Authorization': `Bearer ${uni.getStorageSync('token')}`,
+            'Content-Type': 'application/json'
+          },
+          data: data
+        })
+
+        if (res.data.code === 200) {
+          uni.showToast({ title: '评价成功', icon: 'success' })
+          setTimeout(() => {
+            uni.navigateBack()
+          }, 1500)
+        } else {
+          uni.showToast({ title: res.data.message || '评价失败', icon: 'none' })
+        }
+      } catch (e) {
+        console.error('提交评价失败', e)
+        uni.showToast({ title: '网络错误', icon: 'none' })
+      }
+    },
+    goBack() {
+      uni.navigateBack()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 40rpx;
+  background: #f5f5f5;
+  min-height: 100vh;
+}
+
+.header {
+  margin-bottom: 40rpx;
+}
+
+.title {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 10rpx;
+}
+
+.subtitle {
+  font-size: 28rpx;
+  color: #999;
+}
+
+.loading, .empty {
+  text-align: center;
+  padding: 100rpx 0;
+  color: #999;
+}
+
+.rating-section {
+  background: white;
+  border-radius: 20rpx;
+  padding: 40rpx;
+  margin-bottom: 30rpx;
+}
+
+.section-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 30rpx;
+}
+
+.section-icon {
+  width: 48rpx;
+  height: 48rpx;
+  border-radius: 12rpx;
+  margin-right: 16rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+}
+
+.assessor-section-icon {
+  background: linear-gradient(135deg, #e3f2fd, #bbdefb);
+}
+.assessor-section-icon::after {
+  content: '';
+  width: 20rpx;
+  height: 20rpx;
+  border: 4rpx solid #2196f3;
+  border-radius: 50%;
+  position: absolute;
+}
+
+.planner-section-icon {
+  background: linear-gradient(135deg, #e8f5e9, #c8e6c9);
+}
+.planner-section-icon::after {
+  content: '';
+  width: 20rpx;
+  height: 20rpx;
+  border: 4rpx solid #4caf50;
+  border-radius: 50%;
+  position: absolute;
+}
+
+.section-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.rating-stars {
+  display: flex;
+  align-items: center;
+  margin-bottom: 30rpx;
+}
+
+.star-label {
+  font-size: 28rpx;
+  color: #666;
+  margin-right: 20rpx;
+}
+
+.stars {
+  display: flex;
+  gap: 12rpx;
+}
+
+.star {
+  font-size: 60rpx;
+  color: #e0e0e0;
+  transition: color 0.2s ease, text-shadow 0.2s ease;
+  line-height: 1;
+}
+
+.star:active {
+  transform: scale(1.15);
+}
+
+.star.active {
+  color: #ffc107;
+  text-shadow: 0 0 8rpx rgba(255, 193, 7, 0.4);
+}
+
+.rating-text {
+  font-size: 24rpx;
+  color: #ff9800;
+  margin-left: 16rpx;
+  font-weight: bold;
+}
+
+.comment-input {
+  margin-top: 20rpx;
+}
+
+.textarea {
+  width: 100%;
+  min-height: 200rpx;
+  padding: 20rpx;
+  border: 2rpx solid #e0e0e0;
+  border-radius: 10rpx;
+  font-size: 28rpx;
+  box-sizing: border-box;
+}
+
+.actions {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+  margin-top: 40rpx;
+}
+
+.btn-primary, .btn-outline {
+  border-radius: 20rpx;
+  font-size: 32rpx;
+  padding: 30rpx;
+  text-align: center;
+}
+
+.btn-primary {
+  background: #1E3A5F;
+  color: white;
+  border: none;
+}
+
+.btn-primary:disabled {
+  background: #ccc;
+}
+
+.btn-outline {
+  background: white;
+  color: #666;
+  border: 2rpx solid #ddd;
+}
+</style>

+ 427 - 0
cfc-frontend/pages/teacher/service-tasks.vue

@@ -0,0 +1,427 @@
+<template>
+  <view class="container">
+    <!-- 头部 -->
+    <view class="header">
+      <text class="title">待办服务任务</text>
+      <text class="subtitle">查看需要您处理的服务执行任务</text>
+    </view>
+
+    <!-- 加载状态 -->
+    <view v-if="loading" class="loading">
+      <text>加载中...</text>
+    </view>
+
+    <!-- 任务列表 -->
+    <view v-else-if="tasks.length > 0" class="task-list">
+      <view 
+        v-for="task in tasks" 
+        :key="task.id" 
+        class="task-card"
+        @click="viewTaskDetail(task)">
+        <view class="task-header">
+          <view class="task-title-row">
+            <view class="task-icon" :class="getStatusClass(task)">
+              <view v-if="getStatusClass(task) === 'completed'" class="icon-check"></view>
+              <view v-else-if="getStatusClass(task) === 'ready'" class="icon-alert"></view>
+              <view v-else class="icon-clock"></view>
+            </view>
+            <text class="task-title">服务执行 #{{ task.id }}</text>
+          </view>
+          <text class="task-status" :class="getStatusClass(task)">
+            {{ getStatusText(task) }}
+          </text>
+        </view>
+        <view class="task-info">
+          <view class="info-row">
+            <view class="info-icon order-icon"></view>
+            <text class="info-label">订单号</text>
+            <text class="info-value">{{ task.assessmentOrderId }}</text>
+          </view>
+          <view class="info-row">
+            <view class="info-icon time-icon"></view>
+            <text class="info-label">创建时间</text>
+            <text class="info-value">{{ formatTime(task.createdAt) }}</text>
+          </view>
+          <view class="info-row">
+            <view class="info-icon assessor-info-icon"></view>
+            <text class="info-label">评估师</text>
+            <text class="info-value">{{ task.assessorStatus === 'completed' ? '已完成' : '进行中' }}</text>
+          </view>
+          <view v-if="task.plannerId" class="info-row">
+            <view class="info-icon planner-info-icon"></view>
+            <text class="info-label">您的状态</text>
+            <text class="info-value">{{ task.plannerStatus === 'completed' ? '已完成' : '待处理' }}</text>
+          </view>
+        </view>
+        <view v-if="task.plannerStatus !== 'completed'" class="task-action">
+          <button class="btn-complete" @click.stop="completeTask(task)">
+            标记完成
+          </button>
+        </view>
+      </view>
+    </view>
+
+    <!-- 无任务 -->
+    <view v-else class="empty">
+      <text>暂无待办任务</text>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      tasks: [],
+      loading: true
+    }
+  },
+  onLoad() {
+    this.loadTasks()
+  },
+  onShow() {
+    this.loadTasks()
+  },
+  methods: {
+    async loadTasks() {
+      this.loading = true
+      try {
+        // 获取当前用户的 plannerId(这里假设当前用户就是规划师)
+        const userInfo = uni.getStorageSync('userInfo')
+        const plannerId = userInfo && userInfo.id
+        
+        if (!plannerId) {
+          uni.showToast({ title: '未登录', icon: 'none' })
+          return
+        }
+
+        const res = await uni.request({
+          url: `${getApp().globalData.apiBase}/api/dan-execution/guide/my-tasks`,
+          method: 'GET',
+          data: { plannerId: plannerId },
+          header: {
+            'Authorization': `Bearer ${uni.getStorageSync('token')}`
+          }
+        })
+
+        if (res.data.code === 200) {
+          this.tasks = res.data.data || []
+        } else {
+          uni.showToast({ title: res.data.message || '加载失败', icon: 'none' })
+        }
+      } catch (e) {
+        console.error('加载任务列表失败', e)
+        uni.showToast({ title: '网络错误', icon: 'none' })
+      } finally {
+        this.loading = false
+      }
+    },
+    viewTaskDetail(task) {
+      uni.navigateTo({
+        url: `/pages/assessment/execution?executionId=${task.id}`
+      })
+    },
+    completeTask(task) {
+      uni.showModal({
+        title: '标记完成',
+        content: '确认标记此服务为已完成?',
+        editable: true,
+        placeholderText: '请填写服务备注(选填)',
+        success: async (res) => {
+          if (res.confirm) {
+            try {
+              const response = await uni.request({
+                url: `${getApp().globalData.apiBase}/api/dan-execution/complete`,
+                method: 'POST',
+                header: {
+                  'Authorization': `Bearer ${uni.getStorageSync('token')}`,
+                  'Content-Type': 'application/json'
+                },
+                data: {
+                  executionId: task.id,
+                  notes: res.content || ''
+                }
+              })
+
+              if (response.data.code === 200) {
+                uni.showToast({ title: '标记成功', icon: 'success' })
+                this.loadTasks()
+              } else {
+                uni.showToast({ title: response.data.message || '操作失败', icon: 'none' })
+              }
+            } catch (e) {
+              console.error('标记完成失败', e)
+              uni.showToast({ title: '网络错误', icon: 'none' })
+            }
+          }
+        }
+      })
+    },
+    formatTime(timeStr) {
+      if (!timeStr) return ''
+      const date = new Date(timeStr)
+      return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
+    },
+    getStatusClass(task) {
+      if (task.plannerStatus === 'completed') return 'completed'
+      if (task.assessorStatus === 'completed') return 'ready'
+      return 'pending'
+    },
+    getStatusText(task) {
+      if (task.plannerStatus === 'completed') return '已完成'
+      if (task.assessorStatus === 'completed') return '待您处理'
+      return '进行中'
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 40rpx;
+  background: #f5f5f5;
+  min-height: 100vh;
+}
+
+.header {
+  margin-bottom: 40rpx;
+}
+
+.title {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 10rpx;
+}
+
+.subtitle {
+  font-size: 28rpx;
+  color: #999;
+}
+
+.loading, .empty {
+  text-align: center;
+  padding: 100rpx 0;
+  color: #999;
+}
+
+.task-list {
+  display: flex;
+  flex-direction: column;
+  gap: 30rpx;
+}
+
+.task-card {
+  background: white;
+  border-radius: 20rpx;
+  padding: 40rpx;
+  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
+}
+
+.task-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 30rpx;
+}
+
+.task-title-row {
+  display: flex;
+  align-items: center;
+}
+
+.task-icon {
+  width: 40rpx;
+  height: 40rpx;
+  border-radius: 50%;
+  margin-right: 16rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+}
+
+.task-icon.completed {
+  background: linear-gradient(135deg, #4caf50, #388e3c);
+}
+
+.task-icon.ready {
+  background: linear-gradient(135deg, #2196f3, #1976d2);
+}
+
+.task-icon.pending {
+  background: linear-gradient(135deg, #ff9800, #f57c00);
+}
+
+/* 对勾 */
+.task-icon .icon-check {
+  width: 16rpx;
+  height: 10rpx;
+  border-left: 4rpx solid #fff;
+  border-bottom: 4rpx solid #fff;
+  transform: rotate(-45deg);
+  margin-top: -2rpx;
+}
+
+/* 感叹号 */
+.task-icon .icon-alert {
+  width: 4rpx;
+  height: 16rpx;
+  background: #fff;
+  border-radius: 2rpx;
+}
+
+/* 时钟 */
+.task-icon .icon-clock {
+  width: 18rpx;
+  height: 18rpx;
+  border: 3rpx solid #fff;
+  border-radius: 50%;
+  position: relative;
+}
+.task-icon .icon-clock::before {
+  content: '';
+  position: absolute;
+  width: 3rpx;
+  height: 8rpx;
+  background: #fff;
+  top: 2rpx;
+  left: 50%;
+  transform: translateX(-50%);
+}
+.task-icon .icon-clock::after {
+  content: '';
+  position: absolute;
+  width: 6rpx;
+  height: 3rpx;
+  background: #fff;
+  top: 50%;
+  left: 50%;
+  transform: translateY(-50%);
+}
+
+.task-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.task-status {
+  font-size: 24rpx;
+  padding: 10rpx 20rpx;
+  border-radius: 20rpx;
+}
+
+.task-status.pending {
+  background: #fff3e0;
+  color: #ff9800;
+}
+
+.task-status.ready {
+  background: #e3f2fd;
+  color: #2196f3;
+}
+
+.task-status.completed {
+  background: #e8f5e9;
+  color: #4caf50;
+}
+
+.task-info {
+  margin-bottom: 20rpx;
+}
+
+.info-row {
+  display: flex;
+  align-items: center;
+  margin-bottom: 18rpx;
+}
+
+.info-icon {
+  width: 28rpx;
+  height: 28rpx;
+  border-radius: 6rpx;
+  margin-right: 12rpx;
+  flex-shrink: 0;
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+/* 订单图标 */
+.order-icon {
+  background: #e3f2fd;
+}
+.order-icon::after {
+  content: '';
+  width: 12rpx;
+  height: 14rpx;
+  border: 2rpx solid #2196f3;
+  border-radius: 2rpx;
+}
+
+/* 时间图标 */
+.time-icon {
+  background: #fff3e0;
+}
+.time-icon::after {
+  content: '';
+  width: 12rpx;
+  height: 12rpx;
+  border: 2rpx solid #ff9800;
+  border-radius: 50%;
+}
+
+/* 评估师图标 */
+.assessor-info-icon {
+  background: #e8f5e9;
+}
+.assessor-info-icon::after {
+  content: '';
+  width: 8rpx;
+  height: 8rpx;
+  background: #4caf50;
+  border-radius: 50%;
+}
+
+/* 规划师图标 */
+.planner-info-icon {
+  background: #fce4ec;
+}
+.planner-info-icon::after {
+  content: '';
+  width: 8rpx;
+  height: 8rpx;
+  background: #e91e63;
+  border-radius: 50%;
+}
+
+.info-label {
+  font-size: 26rpx;
+  color: #666;
+  width: 140rpx;
+}
+
+.info-value {
+  font-size: 26rpx;
+  color: #333;
+  flex: 1;
+}
+
+.task-action {
+  margin-top: 20rpx;
+}
+
+.btn-complete {
+  background: linear-gradient(135deg, #4caf50, #388e3c);
+  color: white;
+  border: none;
+  border-radius: 16rpx;
+  font-size: 28rpx;
+  padding: 24rpx;
+  width: 100%;
+  box-shadow: 0 4rpx 12rpx rgba(76, 175, 80, 0.3);
+}
+</style>

binární
cfc-frontend/static/tab-wisdom-active.png


binární
cfc-frontend/static/tab-wisdom.png


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

@@ -198,6 +198,18 @@ const routes = [
         component: () => import('@/views/admin/AssessmentOrders.vue'),
         meta: { title: '测评订单', perm: 'assessment:dan' }
       },
+      {
+        path: 'dan-execution',
+        name: 'DanExecution',
+        component: () => import('@/views/admin/DanExecution.vue'),
+        meta: { title: 'DAN服务执行', perm: 'assessment:dan' }
+      },
+      {
+        path: 'dan-settlement',
+        name: 'DanSettlement',
+        component: () => import('@/views/admin/DanSettlement.vue'),
+        meta: { title: 'DAN结算管理', perm: 'assessment:dan' }
+      },
       {
         path: 'product-manage',
         name: 'ProductManage',

+ 297 - 0
cfc-web/src/views/admin/DanExecution.vue

@@ -0,0 +1,297 @@
+<template>
+  <div class="dan-execution admin-page">
+    <el-card>
+      <div slot="header" class="admin-page-header">
+        <span class="admin-page-title">DAN测评服务执行管理</span>
+        <div class="filter-bar">
+          <el-select v-model="statusFilter" placeholder="执行状态" clearable @change="handleSearch" style="width: 140px; margin-right: 10px;">
+            <el-option label="全部状态" value="" />
+            <el-option label="进行中" value="pending" />
+            <el-option label="已完成" value="completed" />
+            <el-option label="已确认" value="confirmed" />
+          </el-select>
+          <el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
+        </div>
+      </div>
+
+      <el-table :data="list" v-loading="loading" border stripe empty-text="暂无执行记录">
+        <el-table-column label="ID" prop="id" width="80" />
+        <el-table-column label="订单ID" prop="assessmentOrderId" width="100" />
+        <el-table-column label="家庭ID" prop="familyId" width="100" />
+        <el-table-column label="孩子ID" prop="childId" width="100" />
+        <el-table-column label="评估师ID" prop="assessorId" width="100" />
+        <el-table-column label="评估师状态" width="130">
+          <template slot-scope="{ row }">
+            <div class="status-cell">
+              <i :class="executionIconClass(row.assessorStatus)" :style="{ color: executionIconColor(row.assessorStatus) }"></i>
+              <el-tag :type="executionStatusType(row.assessorStatus)" size="mini">
+                {{ executionStatusLabel(row.assessorStatus) }}
+              </el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="规划师ID" prop="plannerId" width="100" />
+        <el-table-column label="规划师状态" width="130">
+          <template slot-scope="{ row }">
+            <div v-if="row.plannerId" class="status-cell">
+              <i :class="executionIconClass(row.plannerStatus)" :style="{ color: executionIconColor(row.plannerStatus) }"></i>
+              <el-tag :type="executionStatusType(row.plannerStatus)" size="mini">
+                {{ executionStatusLabel(row.plannerStatus) }}
+              </el-tag>
+            </div>
+            <span v-else style="color: #999;">—</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="用户确认" width="110">
+          <template slot-scope="{ row }">
+            <div class="status-cell">
+              <i :class="row.userConfirmed === 1 ? 'el-icon-circle-check' : 'el-icon-time'"
+                 :style="{ color: row.userConfirmed === 1 ? '#67c23a' : '#909399' }"></i>
+              <el-tag :type="row.userConfirmed === 1 ? 'success' : 'info'" size="mini">
+                {{ row.userConfirmed === 1 ? '已确认' : '未确认' }}
+              </el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="创建时间" width="160">
+          <template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template>
+        </el-table-column>
+        <el-table-column label="操作" width="160" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button size="mini" type="primary" icon="el-icon-view" plain @click="viewDetail(row)">详情</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <el-pagination
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+        :current-page="page"
+        :page-sizes="[10, 20, 50]"
+        :page-size="size"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        class="pagination-wrap"
+      />
+    </el-card>
+
+    <!-- 详情 Dialog -->
+    <el-dialog title="执行记录详情" :visible.sync="detailDialogVisible" width="700px">
+      <div v-if="currentRecord" v-loading="detailLoading">
+        <el-descriptions :column="2" border size="small">
+          <el-descriptions-item label="执行ID">{{ currentRecord.id }}</el-descriptions-item>
+          <el-descriptions-item label="订单ID">{{ currentRecord.assessmentOrderId }}</el-descriptions-item>
+          <el-descriptions-item label="家庭ID">{{ currentRecord.familyId }}</el-descriptions-item>
+          <el-descriptions-item label="孩子ID">{{ currentRecord.childId }}</el-descriptions-item>
+          <el-descriptions-item label="评估师ID">{{ currentRecord.assessorId }}</el-descriptions-item>
+          <el-descriptions-item label="评估师状态">
+            <div class="status-cell">
+              <i :class="executionIconClass(currentRecord.assessorStatus)" :style="{ color: executionIconColor(currentRecord.assessorStatus) }"></i>
+              <el-tag :type="executionStatusType(currentRecord.assessorStatus)" size="mini">
+                {{ executionStatusLabel(currentRecord.assessorStatus) }}
+              </el-tag>
+            </div>
+          </el-descriptions-item>
+          <el-descriptions-item label="评估师完成时间">{{ formatTime(currentRecord.assessorCompletedAt) }}</el-descriptions-item>
+          <el-descriptions-item label="评估师备注">{{ currentRecord.assessorNotes || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="规划师ID">{{ currentRecord.plannerId || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="规划师状态">
+            <div v-if="currentRecord.plannerId" class="status-cell">
+              <i :class="executionIconClass(currentRecord.plannerStatus)" :style="{ color: executionIconColor(currentRecord.plannerStatus) }"></i>
+              <el-tag :type="executionStatusType(currentRecord.plannerStatus)" size="mini">
+                {{ executionStatusLabel(currentRecord.plannerStatus) }}
+              </el-tag>
+            </div>
+            <span v-else>无</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="规划师完成时间">{{ formatTime(currentRecord.plannerCompletedAt) }}</el-descriptions-item>
+          <el-descriptions-item label="规划师备注">{{ currentRecord.plannerNotes || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="用户确认">
+            <div class="status-cell">
+              <i :class="currentRecord.userConfirmed === 1 ? 'el-icon-circle-check' : 'el-icon-time'"
+                 :style="{ color: currentRecord.userConfirmed === 1 ? '#67c23a' : '#909399' }"></i>
+              <el-tag :type="currentRecord.userConfirmed === 1 ? 'success' : 'info'" size="mini">
+                {{ currentRecord.userConfirmed === 1 ? '已确认' : '未确认' }}
+              </el-tag>
+            </div>
+          </el-descriptions-item>
+          <el-descriptions-item label="确认时间">{{ formatTime(currentRecord.userConfirmedAt) }}</el-descriptions-item>
+          <el-descriptions-item label="创建时间">{{ formatTime(currentRecord.createdAt) }}</el-descriptions-item>
+          <el-descriptions-item label="更新时间">{{ formatTime(currentRecord.updatedAt) }}</el-descriptions-item>
+        </el-descriptions>
+
+        <div v-if="currentRecord.assessorSettlement || currentRecord.plannerSettlement" style="margin-top: 20px;">
+          <h4>结算信息</h4>
+          <el-descriptions :column="2" border size="small">
+            <el-descriptions-item v-if="currentRecord.assessorSettlement" label="评估师结算金额">
+              ¥{{ (currentRecord.assessorSettlement.commissionAmount / 100).toFixed(2) }}
+            </el-descriptions-item>
+            <el-descriptions-item v-if="currentRecord.assessorSettlement" label="评估师结算状态">
+              <el-tag :type="settlementStatusType(currentRecord.assessorSettlement.status)" size="mini">
+                {{ settlementStatusLabel(currentRecord.assessorSettlement.status) }}
+              </el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item v-if="currentRecord.plannerSettlement" label="规划师结算金额">
+              ¥{{ (currentRecord.plannerSettlement.commissionAmount / 100).toFixed(2) }}
+            </el-descriptions-item>
+            <el-descriptions-item v-if="currentRecord.plannerSettlement" label="规划师结算状态">
+              <el-tag :type="settlementStatusType(currentRecord.plannerSettlement.status)" size="mini">
+                {{ settlementStatusLabel(currentRecord.plannerSettlement.status) }}
+              </el-tag>
+            </el-descriptions-item>
+          </el-descriptions>
+        </div>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'DanExecution',
+  data() {
+    return {
+      list: [],
+      loading: false,
+      page: 1,
+      size: 10,
+      total: 0,
+      statusFilter: '',
+      detailDialogVisible: false,
+      detailLoading: false,
+      currentRecord: null
+    }
+  },
+  created() {
+    this.loadData()
+  },
+  methods: {
+    async loadData() {
+      this.loading = true
+      try {
+        // TODO: 调用后端 API 获取执行记录列表
+        // const res = await this.$axios.post('/api/admin/dan-execution/list', {
+        //   page: this.page,
+        //   size: this.size,
+        //   status: this.statusFilter
+        // })
+        // this.list = res.data.data.records
+        // this.total = res.data.data.total
+        this.list = []
+        this.total = 0
+      } catch (e) {
+        console.error('加载执行记录失败', e)
+        this.$message.error('加载失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    async viewDetail(row) {
+      this.detailDialogVisible = true
+      this.detailLoading = true
+      try {
+        const res = await this.$axios.post(`/api/dan-execution/detail/${row.id}`)
+        if (res.data.code === 200) {
+          this.currentRecord = res.data.data
+        } else {
+          this.$message.error(res.data.message || '加载失败')
+        }
+      } catch (e) {
+        console.error('加载详情失败', e)
+        this.$message.error('加载失败')
+      } finally {
+        this.detailLoading = false
+      }
+    },
+    handleSearch() {
+      this.page = 1
+      this.loadData()
+    },
+    handleSizeChange(val) {
+      this.size = val
+      this.loadData()
+    },
+    handleCurrentChange(val) {
+      this.page = val
+      this.loadData()
+    },
+    formatTime(time) {
+      if (!time) return '-'
+      return new Date(time).toLocaleString('zh-CN')
+    },
+    executionStatusType(status) {
+      if (status === 'completed') return 'success'
+      if (status === 'pending') return 'warning'
+      if (status === 'rejected') return 'danger'
+      return 'info'
+    },
+    executionStatusLabel(status) {
+      if (status === 'completed') return '已完成'
+      if (status === 'pending') return '进行中'
+      if (status === 'rejected') return '已拒绝'
+      return status || '-'
+    },
+    executionIconClass(status) {
+      if (status === 'completed') return 'el-icon-circle-check'
+      if (status === 'pending') return 'el-icon-loading'
+      if (status === 'rejected') return 'el-icon-circle-close'
+      return 'el-icon-info'
+    },
+    executionIconColor(status) {
+      if (status === 'completed') return '#67c23a'
+      if (status === 'pending') return '#e6a23c'
+      if (status === 'rejected') return '#f56c6c'
+      return '#909399'
+    },
+    settlementStatusType(status) {
+      if (status === 'settled') return 'success'
+      if (status === 'pending') return 'warning'
+      if (status === 'failed') return 'danger'
+      return 'info'
+    },
+    settlementStatusLabel(status) {
+      if (status === 'settled') return '已结算'
+      if (status === 'pending') return '待结算'
+      if (status === 'failed') return '失败'
+      return status || '-'
+    }
+  }
+}
+</script>
+
+<style scoped>
+.admin-page {
+  padding: 20px;
+}
+
+.admin-page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.admin-page-title {
+  font-size: 18px;
+  font-weight: bold;
+}
+
+.filter-bar {
+  display: flex;
+  align-items: center;
+}
+
+.status-cell {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.status-cell i {
+  font-size: 16px;
+}
+
+.pagination-wrap {
+  margin-top: 20px;
+  text-align: right;
+}
+</style>

+ 431 - 0
cfc-web/src/views/admin/DanSettlement.vue

@@ -0,0 +1,431 @@
+<template>
+  <div class="dan-settlement admin-page">
+    <!-- 统计卡片 -->
+    <el-row :gutter="20" style="margin-bottom: 20px;">
+      <el-col :span="8">
+        <el-card shadow="hover">
+          <div class="stat-card">
+            <div class="stat-icon-wrap total">
+              <i class="el-icon-money"></i>
+            </div>
+            <div class="stat-info">
+              <div class="stat-label">总结算金额</div>
+              <div class="stat-value">¥{{ (stats.totalAmount / 100).toFixed(2) }}</div>
+              <div class="stat-desc">共 {{ stats.totalCount }} 笔</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="8">
+        <el-card shadow="hover">
+          <div class="stat-card">
+            <div class="stat-icon-wrap pending">
+              <i class="el-icon-time"></i>
+            </div>
+            <div class="stat-info">
+              <div class="stat-label">待结算金额</div>
+              <div class="stat-value pending">¥{{ (stats.pendingAmount / 100).toFixed(2) }}</div>
+              <div class="stat-desc">共 {{ stats.pendingCount }} 笔</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="8">
+        <el-card shadow="hover">
+          <div class="stat-card">
+            <div class="stat-icon-wrap settled">
+              <i class="el-icon-circle-check"></i>
+            </div>
+            <div class="stat-info">
+              <div class="stat-label">已结算金额</div>
+              <div class="stat-value settled">¥{{ (stats.settledAmount / 100).toFixed(2) }}</div>
+              <div class="stat-desc">共 {{ stats.settledCount }} 笔</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 结算列表 -->
+    <el-card>
+      <div slot="header" class="admin-page-header">
+        <span class="admin-page-title">结算记录管理</span>
+        <div class="filter-bar">
+          <el-select v-model="statusFilter" placeholder="结算状态" clearable @change="handleSearch" style="width: 140px; margin-right: 10px;">
+            <el-option label="全部状态" value="" />
+            <el-option label="待结算" value="pending" />
+            <el-option label="已结算" value="settled" />
+            <el-option label="结算失败" value="failed" />
+          </el-select>
+          <el-select v-model="providerTypeFilter" placeholder="服务类型" clearable @change="handleSearch" style="width: 140px; margin-right: 10px;">
+            <el-option label="全部类型" value="" />
+            <el-option label="评估师" value="assessor" />
+            <el-option label="规划师" value="planner" />
+          </el-select>
+          <el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
+        </div>
+      </div>
+
+      <el-table :data="list" v-loading="loading" border stripe empty-text="暂无结算记录">
+        <el-table-column label="ID" prop="id" width="80" />
+        <el-table-column label="执行ID" prop="executionId" width="100" />
+        <el-table-column label="订单ID" prop="assessmentOrderId" width="100" />
+        <el-table-column label="服务方ID" prop="providerId" width="100" />
+        <el-table-column label="服务类型" width="110">
+          <template slot-scope="{ row }">
+            <div class="status-cell">
+              <i :class="row.providerType === 'assessor' ? 'el-icon-user' : 'el-icon-s-custom'"
+                 :style="{ color: row.providerType === 'assessor' ? '#409EFF' : '#67c23a' }"></i>
+              <el-tag :type="row.providerType === 'assessor' ? '' : 'success'" size="mini">
+                {{ row.providerType === 'assessor' ? '评估师' : '规划师' }}
+              </el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="服务费" width="100">
+          <template slot-scope="{ row }">
+            ¥{{ (row.serviceFee / 100).toFixed(2) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="佣金比例" width="100">
+          <template slot-scope="{ row }">
+            {{ row.commissionRate }}%
+          </template>
+        </el-table-column>
+        <el-table-column label="结算金额" width="120">
+          <template slot-scope="{ row }">
+            <span style="font-weight: bold; color: #409EFF;">
+              ¥{{ (row.commissionAmount / 100).toFixed(2) }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column label="结算状态" width="130">
+          <template slot-scope="{ row }">
+            <div class="status-cell">
+              <i :class="statusIconClass(row.status)" :style="{ color: statusIconColor(row.status) }"></i>
+              <el-tag :type="statusType(row.status)" size="mini">
+                {{ statusLabel(row.status) }}
+              </el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="结算时间" width="160">
+          <template slot-scope="{ row }">{{ formatTime(row.settledAt) }}</template>
+        </el-table-column>
+        <el-table-column label="交易ID" prop="transactionId" min-width="150" show-overflow-tooltip />
+        <el-table-column label="创建时间" width="160">
+          <template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template>
+        </el-table-column>
+        <el-table-column label="操作" width="150" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button
+              v-if="row.status === 'pending'"
+              size="mini"
+              type="success"
+              icon="el-icon-check"
+              plain
+              @click="confirmSettlement(row)"
+            >确认结算</el-button>
+            <el-button size="mini" type="primary" icon="el-icon-view" plain @click="viewDetail(row)">详情</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <el-pagination
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+        :current-page="page"
+        :page-sizes="[10, 20, 50]"
+        :page-size="size"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        class="pagination-wrap"
+      />
+    </el-card>
+
+    <!-- 详情 Dialog -->
+    <el-dialog title="结算详情" :visible.sync="detailDialogVisible" width="600px">
+      <div v-if="currentRecord" v-loading="detailLoading">
+        <el-descriptions :column="2" border size="small">
+          <el-descriptions-item label="结算ID">{{ currentRecord.id }}</el-descriptions-item>
+          <el-descriptions-item label="执行ID">{{ currentRecord.executionId }}</el-descriptions-item>
+          <el-descriptions-item label="订单ID">{{ currentRecord.assessmentOrderId }}</el-descriptions-item>
+          <el-descriptions-item label="服务方ID">{{ currentRecord.providerId }}</el-descriptions-item>
+          <el-descriptions-item label="服务类型">
+            <div class="status-cell">
+              <i :class="currentRecord.providerType === 'assessor' ? 'el-icon-user' : 'el-icon-s-custom'"
+                 :style="{ color: currentRecord.providerType === 'assessor' ? '#409EFF' : '#67c23a' }"></i>
+              <el-tag :type="currentRecord.providerType === 'assessor' ? '' : 'success'" size="mini">
+                {{ currentRecord.providerType === 'assessor' ? '评估师' : '规划师' }}
+              </el-tag>
+            </div>
+          </el-descriptions-item>
+          <el-descriptions-item label="服务费">¥{{ (currentRecord.serviceFee / 100).toFixed(2) }}</el-descriptions-item>
+          <el-descriptions-item label="佣金比例">{{ currentRecord.commissionRate }}%</el-descriptions-item>
+          <el-descriptions-item label="结算金额">
+            <span style="font-weight: bold; color: #409EFF;">
+              ¥{{ (currentRecord.commissionAmount / 100).toFixed(2) }}
+            </span>
+          </el-descriptions-item>
+          <el-descriptions-item label="结算状态">
+            <div class="status-cell">
+              <i :class="statusIconClass(currentRecord.status)" :style="{ color: statusIconColor(currentRecord.status) }"></i>
+              <el-tag :type="statusType(currentRecord.status)" size="mini">
+                {{ statusLabel(currentRecord.status) }}
+              </el-tag>
+            </div>
+          </el-descriptions-item>
+          <el-descriptions-item label="结算时间">{{ formatTime(currentRecord.settledAt) }}</el-descriptions-item>
+          <el-descriptions-item label="交易ID">{{ currentRecord.transactionId || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="创建时间">{{ formatTime(currentRecord.createdAt) }}</el-descriptions-item>
+          <el-descriptions-item label="更新时间">{{ formatTime(currentRecord.updatedAt) }}</el-descriptions-item>
+        </el-descriptions>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'DanSettlement',
+  data() {
+    return {
+      list: [],
+      loading: false,
+      page: 1,
+      size: 10,
+      total: 0,
+      statusFilter: '',
+      providerTypeFilter: '',
+      stats: {
+        totalAmount: 0,
+        pendingAmount: 0,
+        settledAmount: 0,
+        totalCount: 0,
+        pendingCount: 0,
+        settledCount: 0
+      },
+      detailDialogVisible: false,
+      detailLoading: false,
+      currentRecord: null
+    }
+  },
+  created() {
+    this.loadData()
+    this.loadStats()
+  },
+  methods: {
+    async loadData() {
+      this.loading = true
+      try {
+        const res = await this.$axios.post('/api/admin/dan-settlement/list', {
+          status: this.statusFilter,
+          providerType: this.providerTypeFilter
+        })
+        if (res.data.code === 200) {
+          this.list = res.data.data || []
+          this.total = this.list.length
+        } else {
+          this.$message.error(res.data.message || '加载失败')
+        }
+      } catch (e) {
+        console.error('加载结算记录失败', e)
+        this.$message.error('加载失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    async loadStats() {
+      try {
+        const res = await this.$axios.post('/api/admin/dan-settlement/stats')
+        if (res.data.code === 200) {
+          this.stats = res.data.data || this.stats
+        }
+      } catch (e) {
+        console.error('加载统计数据失败', e)
+      }
+    },
+    async viewDetail(row) {
+      this.detailDialogVisible = true
+      this.detailLoading = true
+      try {
+        const res = await this.$axios.get(`/api/admin/dan-settlement/detail/${row.id}`)
+        if (res.data.code === 200) {
+          this.currentRecord = res.data.data
+        } else {
+          this.$message.error(res.data.message || '加载失败')
+        }
+      } catch (e) {
+        console.error('加载详情失败', e)
+        this.$message.error('加载失败')
+      } finally {
+        this.detailLoading = false
+      }
+    },
+    async confirmSettlement(row) {
+      try {
+        await this.$confirm('确认将此结算记录标记为已结算?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        })
+
+        const res = await this.$axios.post(`/api/admin/dan-settlement/confirm/${row.id}`)
+        if (res.data.code === 200) {
+          this.$message.success('确认成功')
+          this.loadData()
+          this.loadStats()
+        } else {
+          this.$message.error(res.data.message || '操作失败')
+        }
+      } catch (e) {
+        if (e !== 'cancel') {
+          console.error('确认结算失败', e)
+          this.$message.error('操作失败')
+        }
+      }
+    },
+    handleSearch() {
+      this.page = 1
+      this.loadData()
+    },
+    handleSizeChange(val) {
+      this.size = val
+      this.loadData()
+    },
+    handleCurrentChange(val) {
+      this.page = val
+      this.loadData()
+    },
+    formatTime(time) {
+      if (!time) return '-'
+      return new Date(time).toLocaleString('zh-CN')
+    },
+    statusType(status) {
+      if (status === 'settled') return 'success'
+      if (status === 'pending') return 'warning'
+      if (status === 'failed') return 'danger'
+      return 'info'
+    },
+    statusLabel(status) {
+      if (status === 'settled') return '已结算'
+      if (status === 'pending') return '待结算'
+      if (status === 'failed') return '失败'
+      return status || '-'
+    },
+    statusIconClass(status) {
+      if (status === 'settled') return 'el-icon-circle-check'
+      if (status === 'pending') return 'el-icon-time'
+      if (status === 'failed') return 'el-icon-circle-close'
+      return 'el-icon-info'
+    },
+    statusIconColor(status) {
+      if (status === 'settled') return '#67c23a'
+      if (status === 'pending') return '#e6a23c'
+      if (status === 'failed') return '#f56c6c'
+      return '#909399'
+    }
+  }
+}
+</script>
+
+<style scoped>
+.admin-page {
+  padding: 20px;
+}
+
+.admin-page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.admin-page-title {
+  font-size: 18px;
+  font-weight: bold;
+}
+
+.filter-bar {
+  display: flex;
+  align-items: center;
+}
+
+.stat-card {
+  display: flex;
+  align-items: center;
+  padding: 10px 0;
+}
+
+.stat-icon-wrap {
+  width: 56px;
+  height: 56px;
+  border-radius: 14px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 16px;
+  flex-shrink: 0;
+}
+
+.stat-icon-wrap i {
+  font-size: 28px;
+  color: #fff;
+}
+
+.stat-icon-wrap.total {
+  background: linear-gradient(135deg, #409EFF, #2d8cf0);
+}
+
+.stat-icon-wrap.pending {
+  background: linear-gradient(135deg, #e6a23c, #d4941c);
+}
+
+.stat-icon-wrap.settled {
+  background: linear-gradient(135deg, #67c23a, #4fa62c);
+}
+
+.stat-info {
+  flex: 1;
+}
+
+.stat-label {
+  font-size: 13px;
+  color: #909399;
+  margin-bottom: 6px;
+}
+
+.stat-value {
+  font-size: 24px;
+  font-weight: bold;
+  color: #303133;
+  margin-bottom: 4px;
+}
+
+.stat-value.pending {
+  color: #e6a23c;
+}
+
+.stat-value.settled {
+  color: #67c23a;
+}
+
+.stat-desc {
+  font-size: 12px;
+  color: #909399;
+}
+
+.status-cell {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.status-cell i {
+  font-size: 16px;
+}
+
+.pagination-wrap {
+  margin-top: 20px;
+  text-align: right;
+}
+</style>