diff --git a/backend/src/main/java/com/designhub/controller/AnalyticsController.java b/backend/src/main/java/com/designhub/controller/AnalyticsController.java index e952bbe..0ddfb3e 100644 --- a/backend/src/main/java/com/designhub/controller/AnalyticsController.java +++ b/backend/src/main/java/com/designhub/controller/AnalyticsController.java @@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; /** * 数据分析Controller @@ -100,6 +101,18 @@ public class AnalyticsController { return Result.success(analyticsService.getDesignerDeductionDetails(userId, month)); } + @Operation(summary = "获取设计师积分明细") + @GetMapping("/designer-score-details") + public Result> getDesignerScoreDetails( + @RequestHeader("Authorization") String authHeader, + @RequestParam Long userId, + @RequestParam(required = false) String month + ) { + log.info("获取设计师积分明细: userId={}, month={}", userId, month); + + return Result.success(analyticsService.getDesignerScoreDetails(userId, month)); + } + @Operation(summary = "获取多维度分析数据(类目/品牌/部门/平台)") @GetMapping("/dimension-analysis") public Result getDimensionAnalysis( diff --git a/backend/src/main/java/com/designhub/service/AnalyticsService.java b/backend/src/main/java/com/designhub/service/AnalyticsService.java index 1b27453..3ef84c8 100644 --- a/backend/src/main/java/com/designhub/service/AnalyticsService.java +++ b/backend/src/main/java/com/designhub/service/AnalyticsService.java @@ -1550,6 +1550,124 @@ public class AnalyticsService { return null; } + /** + * 获取设计师积分明细 + * @param userId 设计师ID + * @param month 月份 yyyy-MM + * @return 积分明细(任务列表及对应积分) + */ + public Map getDesignerScoreDetails(Long userId, String month) { + log.info("获取设计师积分明细: userId={}, month={}", userId, month); + + // 计算时间范围 + LocalDateTime startTime; + LocalDateTime endTime; + + if (month != null && !month.isEmpty()) { + try { + LocalDate monthDate = LocalDate.parse(month + "-01", DateTimeFormatter.ofPattern("yyyy-MM-dd")); + startTime = LocalDateTime.of(monthDate, LocalTime.MIN); + endTime = LocalDateTime.of(monthDate.with(TemporalAdjusters.lastDayOfMonth()), LocalTime.MAX); + } catch (Exception e) { + LocalDate now = LocalDate.now(); + startTime = LocalDateTime.of(now.withDayOfMonth(1), LocalTime.MIN); + endTime = LocalDateTime.now(); + } + } else { + LocalDate now = LocalDate.now(); + startTime = LocalDateTime.of(now.withDayOfMonth(1), LocalTime.MIN); + endTime = LocalDateTime.now(); + } + + // 获取用户信息 + User designer = userMapper.selectById(userId); + if (designer == null) { + log.warn("设计师不存在: userId={}", userId); + Map result = new HashMap<>(); + result.put("totalScore", BigDecimal.ZERO); + result.put("tasks", new ArrayList<>()); + result.put("subTasks", new ArrayList<>()); + return result; + } + + BigDecimal totalScore = BigDecimal.ZERO; + List> taskDetails = new ArrayList<>(); + List> subTaskDetails = new ArrayList<>(); + + // 获取时间范围内的已完成主任务 + List completedTasks = taskMapper.selectList( + new LambdaQueryWrapper() + .eq(Task::getReceiverId, userId) + .eq(Task::getStatus, "completed") + .isNotNull(Task::getActualEndTime) + .ge(Task::getActualEndTime, startTime) + .le(Task::getActualEndTime, endTime) + ); + + for (Task task : completedTasks) { + BigDecimal taskScore = task.getScore() != null ? task.getScore() : BigDecimal.ZERO; + totalScore = totalScore.add(taskScore); + + Map detail = new HashMap<>(); + detail.put("id", task.getId()); + detail.put("taskNo", task.getTaskNo()); + detail.put("title", task.getTitle()); + detail.put("score", taskScore); + detail.put("quantity", task.getQuantity() != null ? task.getQuantity() : 1); + detail.put("completeTime", task.getActualEndTime()); + detail.put("brandId", task.getBrandId()); + detail.put("categoryId", task.getCategoryId()); + detail.put("taskTypeId", task.getTaskTypeId()); + detail.put("type", "task"); + taskDetails.add(detail); + } + + // 获取时间范围内的已完成子任务 + List completedItems = taskItemMapper.selectList( + new LambdaQueryWrapper() + .eq(TaskItem::getReceiverId, userId) + .eq(TaskItem::getStatus, "completed") + .isNotNull(TaskItem::getCompleteTime) + .ge(TaskItem::getCompleteTime, startTime) + .le(TaskItem::getCompleteTime, endTime) + ); + + for (TaskItem item : completedItems) { + BigDecimal itemScore = item.getTotalScore() != null ? item.getTotalScore() : BigDecimal.ZERO; + totalScore = totalScore.add(itemScore); + + // 获取主任务信息 + Task mainTask = taskMapper.selectById(item.getTaskId()); + String mainTaskTitle = mainTask != null ? mainTask.getTitle() : ""; + String mainTaskNo = mainTask != null ? mainTask.getTaskNo() : ""; + + Map detail = new HashMap<>(); + detail.put("id", item.getId()); + detail.put("itemNo", item.getItemNo()); + detail.put("taskNo", mainTaskNo); + detail.put("title", mainTaskTitle); + detail.put("score", itemScore); + detail.put("quantity", item.getQuantity() != null ? item.getQuantity() : 1); + detail.put("completeTime", item.getCompleteTime()); + detail.put("taskTypeId", item.getTaskTypeId()); + detail.put("groupNo", item.getGroupNo()); + detail.put("type", "subTask"); + subTaskDetails.add(detail); + } + + Map result = new HashMap<>(); + result.put("totalScore", totalScore); + result.put("tasks", taskDetails); + result.put("subTasks", subTaskDetails); + result.put("designerName", designer.getRealName()); + result.put("deptName", designer.getDeptName()); + + log.info("设计师积分明细: userId={}, totalScore={}, tasksCount={}, subTasksCount={}", + userId, totalScore, taskDetails.size(), subTaskDetails.size()); + + return result; + } + /** * 获取设计师扣分明细 * @param userId 设计师ID diff --git a/frontend/src/api/analytics.ts b/frontend/src/api/analytics.ts index 5fda77d..3ed922d 100644 --- a/frontend/src/api/analytics.ts +++ b/frontend/src/api/analytics.ts @@ -414,6 +414,58 @@ export const getDesignerDeductionDetails = async ( }; }; +// ==================== 积分明细类型 ==================== + +export interface ScoreTaskItem { + id: number; + taskNo: string; + title: string; + score: number; + quantity: number; + completeTime: string; + type: 'task' | 'subTask'; + itemNo?: string; + groupNo?: number; +} + +export interface ScoreDetail { + totalScore: number; + tasks: ScoreTaskItem[]; + subTasks: ScoreTaskItem[]; + designerName: string; + deptName: string; +} + +/** + * 获取设计师积分明细 + */ +export const getDesignerScoreDetails = async ( + userId: number, + month?: string +): Promise => { + const params: any = { userId }; + if (month) params.month = month; + + const response = await request.get('/analytics/designer-score-details', { params }); + + if (response && typeof response === 'object') { + if ('data' in response && response.data !== undefined && response.data !== null) { + return response.data as ScoreDetail; + } + if ('totalScore' in response || 'tasks' in response) { + return response as unknown as ScoreDetail; + } + } + + return { + totalScore: 0, + tasks: [], + subTasks: [], + designerName: '', + deptName: '' + }; +}; + // ==================== 多维度分析类型 ==================== export interface CategoryAnalysis { diff --git a/frontend/src/views/Analytics.vue b/frontend/src/views/Analytics.vue index fe7ca8f..4cc108a 100644 --- a/frontend/src/views/Analytics.vue +++ b/frontend/src/views/Analytics.vue @@ -257,7 +257,16 @@ @@ -929,6 +1009,7 @@ import { getProcessEfficiency, getWarningTasks, getDesignerDeductionDetails, + getDesignerScoreDetails, getDimensionAnalysis, getPrimaryDeptStatistics, getPrimaryDeptRanking, @@ -1093,6 +1174,23 @@ const deductionDetail = ref({ progressDeductions: [] }) +// 积分明细弹窗 +const scoreDetailVisible = ref(false) +const scoreDetailLoading = ref(false) +const scoreDetail = ref<{ + totalScore: number; + tasks: any[]; + subTasks: any[]; + designerName: string; + deptName: string; +}>({ + totalScore: 0, + tasks: [], + subTasks: [], + designerName: '', + deptName: '' +}) + // 图表引用 const trendChartRef = ref() const typeChartRef = ref() @@ -1243,6 +1341,16 @@ const progressDeductionColumns = [ { title: '扣分原因', dataIndex: 'reason', key: 'reason' } ] +// 积分明细表格列定义 +const scoreDetailColumns = [ + { title: '任务编号', dataIndex: 'itemNo', key: 'itemNo', width: 200, customRender: ({ record }: any) => record.itemNo || record.taskNo }, + { title: '主任务编号', dataIndex: 'taskNo', key: 'taskNo', width: 200 }, + { title: '任务名称', dataIndex: 'title', key: 'title', ellipsis: true }, + { title: '数量', dataIndex: 'quantity', key: 'quantity', width: 80, align: 'center' }, + { title: '完成时间', key: 'completeTime', width: 170 }, + { title: '获得积分', key: 'score', width: 100, align: 'center' } +] + // 多维度分析表格列定义 const categoryColumns = [ { title: '类目名称', dataIndex: 'categoryName', key: 'categoryName', ellipsis: true }, @@ -1786,6 +1894,33 @@ const handleUserTableChange = (pagination: any) => { } } +// 显示积分明细 +const showScoreDetails = async (record: any) => { + console.log('点击积分数字:', record) + const score = record.totalScore || 0 + + if (!score || score <= 0) { + return // 没有积分,不显示弹窗 + } + + currentDesigner.value = record + scoreDetailVisible.value = true + scoreDetailLoading.value = true + + try { + const month = selectedMonth.value || undefined + console.log('请求积分明细,userId:', record.userId, 'month:', month) + const detail = await getDesignerScoreDetails(record.userId, month) + console.log('积分明细数据:', detail) + scoreDetail.value = detail + } catch (error) { + message.error('加载积分明细失败') + console.error('加载积分明细失败:', error) + } finally { + scoreDetailLoading.value = false + } +} + // 显示扣分明细 const showDeductionDetails = async (record: any) => { console.log('点击扣分数字:', record) @@ -2421,6 +2556,23 @@ onMounted(() => { } } +// 积分链接按钮样式 +:deep(.score-link) { + padding: 0 !important; + height: auto !important; + font-weight: 500; + color: #31F08B !important; + font-size: 14px; + text-decoration: underline; + text-decoration-style: dashed; + text-underline-offset: 2px; + + &:hover { + color: #5eff9e !important; + text-decoration-color: #5eff9e; + } +} + // 净积分样式 .net-score-value { font-weight: 600; @@ -2813,6 +2965,142 @@ onMounted(() => { color: $text-tertiary; } } + + // 积分明细弹窗内容样式 + .score-detail-content { + .designer-info { + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 1px solid $border-color; + + h4 { + color: $text-primary; + font-size: 18px; + font-weight: 600; + margin: 0 0 8px 0; + } + + .designer-dept { + color: $text-secondary; + font-size: 14px; + margin: 0 0 8px 0; + } + + .total-score { + color: $text-primary; + font-size: 16px; + margin: 0; + + .score-number { + color: #31F08B; + font-weight: 600; + font-size: 18px; + } + } + } + + .score-section { + margin-bottom: 24px; + + h5 { + color: $text-primary; + font-size: 16px; + font-weight: 600; + margin: 0 0 12px 0; + } + } + + .no-data { + text-align: center; + padding: 40px 0; + color: $text-tertiary; + } + } +} + +// 积分明细弹窗样式 +.score-detail-modal-wrap { + .ant-modal-content { + background-color: $surface-color; + border-radius: 12px; + } + + .ant-modal-header { + background-color: $surface-color; + border-bottom: 1px solid $border-color; + + .ant-modal-title { + color: $text-primary; + } + } + + .ant-modal-close { + color: $text-secondary; + + &:hover { + color: $text-primary; + } + } + + .ant-modal-body { + background-color: $surface-color; + padding: 24px; + } + + .ant-table { + background: transparent; + + .ant-table-thead > tr > th { + background: rgba(255, 255, 255, 0.05); + color: $text-secondary; + border-bottom: 1px solid $border-color; + } + + .ant-table-tbody > tr > td { + background: transparent; + color: $text-primary; + border-bottom: 1px solid $border-color; + } + + .ant-table-tbody > tr:hover > td { + background: rgba(255, 255, 255, 0.05); + } + } + + .ant-pagination { + .ant-pagination-item { + background: transparent; + border-color: $border-color; + + a { + color: $text-primary; + } + + &:hover, &-active { + border-color: $brand-color; + + a { + color: $brand-color; + } + } + } + + .ant-pagination-prev, .ant-pagination-next { + .ant-pagination-item-link { + background: transparent; + border-color: $border-color; + color: $text-primary; + } + } + + .ant-pagination-options { + .ant-select-selector { + background: transparent; + border-color: $border-color; + color: $text-primary; + } + } + } }