feat: 设计师绩效排行榜添加积分明细弹窗功能
- 后端添加 /api/analytics/designer-score-details API - 前端添加积分列点击事件,显示积分明细弹窗 - 显示子任务和主任务的完成积分列表
This commit is contained in:
parent
4c28716ab5
commit
6f03198833
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user