feat: 设计师绩效排行榜添加积分明细弹窗功能

- 后端添加 /api/analytics/designer-score-details API
- 前端添加积分列点击事件,显示积分明细弹窗
- 显示子任务和主任务的完成积分列表
This commit is contained in:
Your Name 2026-01-30 15:38:48 +08:00
parent 4c28716ab5
commit 6f03198833
4 changed files with 472 additions and 1 deletions

View File

@ -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<Map<String, Object>> 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<DimensionAnalysisVO> getDimensionAnalysis(

View File

@ -1550,6 +1550,124 @@ public class AnalyticsService {
return null;
}
/**
* 获取设计师积分明细
* @param userId 设计师ID
* @param month 月份 yyyy-MM
* @return 积分明细(任务列表及对应积分)
*/
public Map<String, Object> 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<String, Object> 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<Map<String, Object>> taskDetails = new ArrayList<>();
List<Map<String, Object>> subTaskDetails = new ArrayList<>();
// 获取时间范围内的已完成主任务
List<Task> completedTasks = taskMapper.selectList(
new LambdaQueryWrapper<Task>()
.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<String, Object> 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<TaskItem> completedItems = taskItemMapper.selectList(
new LambdaQueryWrapper<TaskItem>()
.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<String, Object> 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<String, Object> 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

View File

@ -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<ScoreDetail> => {
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 {

View File

@ -257,7 +257,16 @@
<a-rate :value="record.avgRating" disabled allow-half :count="5" class="white-rate" />
</template>
<template v-else-if="column.key === 'totalScore'">
<span class="score-value">{{ formatScore(record.totalScore) }}</span>
<a-button
v-if="record.totalScore && record.totalScore > 0"
type="link"
class="score-link"
@click.stop="showScoreDetails(record)"
:style="{ padding: 0, height: 'auto', color: '#31F08B' }"
>
{{ formatScore(record.totalScore) }}
</a-button>
<span v-else class="score-value">{{ formatScore(record.totalScore) }}</span>
</template>
<template v-else-if="column.key === 'deductionScore'">
<a-button
@ -916,6 +925,77 @@
</div>
</a-spin>
</a-modal>
<!-- 积分明细弹窗 -->
<a-modal
v-model:open="scoreDetailVisible"
title="积分明细"
width="1200px"
:footer="null"
wrap-class-name="score-detail-modal-wrap"
class="score-detail-modal"
>
<a-spin :spinning="scoreDetailLoading">
<div v-if="currentDesigner" class="score-detail-content">
<div class="designer-info">
<h4>{{ currentDesigner.realName || currentDesigner.userName }}</h4>
<p class="designer-dept">部门:{{ currentDesigner.deptName }}</p>
<p class="total-score">总积分:<span class="score-number">{{ formatScore(scoreDetail.totalScore) }}</span> 分</p>
</div>
<!-- 子任务积分明细 -->
<div v-if="scoreDetail.subTasks && scoreDetail.subTasks.length > 0" class="score-section">
<h5>子任务积分明细 ({{ scoreDetail.subTasks.length }} 条)</h5>
<a-table
:columns="scoreDetailColumns"
:data-source="scoreDetail.subTasks"
:pagination="{ pageSize: 10, showSizeChanger: true, showTotal: (total: number) => `共 ${total} 条` }"
:scroll="{ y: 300 }"
row-key="id"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'score'">
<span class="text-success">+{{ formatScore(record.score) }} 分</span>
</template>
<template v-else-if="column.key === 'completeTime'">
{{ formatDateTime(record.completeTime) }}
</template>
</template>
</a-table>
</div>
<!-- 主任务积分明细 -->
<div v-if="scoreDetail.tasks && scoreDetail.tasks.length > 0" class="score-section">
<h5>主任务积分明细 ({{ scoreDetail.tasks.length }} 条)</h5>
<a-table
:columns="scoreDetailColumns"
:data-source="scoreDetail.tasks"
:pagination="{ pageSize: 10, showSizeChanger: true, showTotal: (total: number) => `共 ${total} 条` }"
:scroll="{ y: 300 }"
row-key="id"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'score'">
<span class="text-success">+{{ formatScore(record.score) }} 分</span>
</template>
<template v-else-if="column.key === 'completeTime'">
{{ formatDateTime(record.completeTime) }}
</template>
</template>
</a-table>
</div>
<!-- 无积分记录 -->
<div v-if="(!scoreDetail.tasks || scoreDetail.tasks.length === 0) &&
(!scoreDetail.subTasks || scoreDetail.subTasks.length === 0)"
class="no-data">
<p>暂无积分记录</p>
</div>
</div>
</a-spin>
</a-modal>
</DefaultLayout>
</template>
@ -929,6 +1009,7 @@ import {
getProcessEfficiency,
getWarningTasks,
getDesignerDeductionDetails,
getDesignerScoreDetails,
getDimensionAnalysis,
getPrimaryDeptStatistics,
getPrimaryDeptRanking,
@ -1093,6 +1174,23 @@ const deductionDetail = ref<DeductionDetail>({
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<HTMLElement>()
const typeChartRef = ref<HTMLElement>()
@ -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;
}
}
}
}
</style>