feat: 设计师绩效增加延误扣分、目标积分和目标完成率;系统设置增加设计师目标积分配置
This commit is contained in:
parent
60b68e0490
commit
505efce1c6
@ -0,0 +1,57 @@
|
||||
package com.designhub.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.designhub.common.Result;
|
||||
import com.designhub.dto.DesignerTargetScoreDTO;
|
||||
import com.designhub.entity.Department;
|
||||
import com.designhub.service.DesignerTargetScoreService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/designer-target-score")
|
||||
@Tag(name = "设计师目标积分管理")
|
||||
@RequiredArgsConstructor
|
||||
public class DesignerTargetScoreController {
|
||||
|
||||
private final DesignerTargetScoreService designerTargetScoreService;
|
||||
|
||||
@Operation(summary = "获取目标积分配置列表")
|
||||
@GetMapping("/list")
|
||||
public Result<Page<Map<String, Object>>> getList(
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "20") int pageSize,
|
||||
@RequestParam(required = false) Long deptId,
|
||||
@RequestParam(required = false) String month
|
||||
) {
|
||||
return Result.success(designerTargetScoreService.getTargetScoreList(page, pageSize, deptId, month));
|
||||
}
|
||||
|
||||
@Operation(summary = "保存目标积分配置")
|
||||
@PostMapping("/save")
|
||||
public Result<String> save(@RequestBody DesignerTargetScoreDTO dto) {
|
||||
designerTargetScoreService.saveTargetScore(dto);
|
||||
return Result.success("保存成功");
|
||||
}
|
||||
|
||||
@Operation(summary = "删除目标积分配置")
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<String> delete(@PathVariable Long id) {
|
||||
designerTargetScoreService.deleteTargetScore(id);
|
||||
return Result.success("删除成功");
|
||||
}
|
||||
|
||||
@Operation(summary = "获取所有接单部门")
|
||||
@GetMapping("/receiving-departments")
|
||||
public Result<List<Department>> getReceivingDepartments() {
|
||||
return Result.success(designerTargetScoreService.getReceivingDepartments());
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
package com.designhub.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
public class DesignerTargetScoreDTO {
|
||||
private Long id;
|
||||
private Long deptId;
|
||||
private Long userId;
|
||||
private BigDecimal targetScore;
|
||||
private String effectiveMonth;
|
||||
private BigDecimal deductionRate;
|
||||
}
|
||||
|
||||
@ -0,0 +1,56 @@
|
||||
package com.designhub.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 设计师目标积分配置表
|
||||
*/
|
||||
@Data
|
||||
@TableName("designer_target_score")
|
||||
public class DesignerTargetScore {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 部门ID(接单部门)
|
||||
*/
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 用户ID(可选,如果为空则为部门通用配置)
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 目标积分
|
||||
*/
|
||||
private BigDecimal targetScore;
|
||||
|
||||
/**
|
||||
* 生效年月(格式:yyyy-MM)
|
||||
*/
|
||||
private String effectiveMonth;
|
||||
|
||||
/**
|
||||
* 延误扣分比例(每延误1小时扣除积分百分比,默认1%)
|
||||
*/
|
||||
private BigDecimal deductionRate;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
package com.designhub.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.designhub.entity.DesignerTargetScore;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface DesignerTargetScoreMapper extends BaseMapper<DesignerTargetScore> {
|
||||
}
|
||||
|
||||
@ -36,6 +36,7 @@ public class AnalyticsService {
|
||||
private final TeamScorePoolMapper teamScorePoolMapper;
|
||||
private final TaskReviewMapper taskReviewMapper;
|
||||
private final RoleService roleService;
|
||||
private final DesignerTargetScoreMapper designerTargetScoreMapper;
|
||||
|
||||
/**
|
||||
* 获取需求统计数据(根据角色返回不同维度的数据)
|
||||
@ -676,19 +677,63 @@ public class AnalyticsService {
|
||||
log.warn("获取评价数据失败: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// 计算获得积分(使用 score 字段,score_reward 可能未设置)
|
||||
// 计算获得积分和延误扣分
|
||||
BigDecimal totalScore = BigDecimal.ZERO;
|
||||
BigDecimal deductionScore = BigDecimal.ZERO;
|
||||
|
||||
// 获取扣分比例配置(默认每延误1小时扣任务积分的1%)
|
||||
BigDecimal deductionRate = getDeductionRate(designer.getDeptId(), currentMonth);
|
||||
|
||||
for (Task task : designerTasks) {
|
||||
if ("completed".equals(task.getStatus())) {
|
||||
// 优先使用 scoreReward,如果为空或0则使用 score
|
||||
// 计算任务积分
|
||||
BigDecimal taskScore = BigDecimal.ZERO;
|
||||
if (task.getScoreReward() != null && task.getScoreReward().compareTo(BigDecimal.ZERO) > 0) {
|
||||
totalScore = totalScore.add(task.getScoreReward());
|
||||
taskScore = task.getScoreReward();
|
||||
} else if (task.getScore() != null) {
|
||||
totalScore = totalScore.add(BigDecimal.valueOf(task.getScore()));
|
||||
taskScore = BigDecimal.valueOf(task.getScore());
|
||||
}
|
||||
totalScore = totalScore.add(taskScore);
|
||||
|
||||
// 计算延误扣分(如果超时完成)
|
||||
if (task.getDeadline() != null && task.getActualEndTime() != null
|
||||
&& task.getActualEndTime().isAfter(task.getDeadline())) {
|
||||
// 计算延误小时数
|
||||
long overdueHours = Duration.between(task.getDeadline(), task.getActualEndTime()).toHours();
|
||||
if (overdueHours > 0 && taskScore.compareTo(BigDecimal.ZERO) > 0) {
|
||||
// 扣分 = 任务积分 * 延误小时数 * 扣分比例
|
||||
BigDecimal taskDeduction = taskScore
|
||||
.multiply(BigDecimal.valueOf(overdueHours))
|
||||
.multiply(deductionRate)
|
||||
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
|
||||
// 扣分不能超过任务积分
|
||||
if (taskDeduction.compareTo(taskScore) > 0) {
|
||||
taskDeduction = taskScore;
|
||||
}
|
||||
deductionScore = deductionScore.add(taskDeduction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 净积分 = 总积分 - 扣分
|
||||
BigDecimal netScore = totalScore.subtract(deductionScore);
|
||||
if (netScore.compareTo(BigDecimal.ZERO) < 0) {
|
||||
netScore = BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
// 获取目标积分
|
||||
BigDecimal targetScore = getTargetScore(designer.getId(), designer.getDeptId(), currentMonth);
|
||||
|
||||
// 计算目标完成率
|
||||
Double targetCompletionRate = 0.0;
|
||||
if (targetScore != null && targetScore.compareTo(BigDecimal.ZERO) > 0) {
|
||||
targetCompletionRate = netScore.divide(targetScore, 4, RoundingMode.HALF_UP)
|
||||
.multiply(BigDecimal.valueOf(100))
|
||||
.doubleValue();
|
||||
targetCompletionRate = Math.round(targetCompletionRate * 10) / 10.0;
|
||||
}
|
||||
|
||||
// 本周完成数
|
||||
int weekCompletedCount = (int) designerTasks.stream()
|
||||
.filter(t -> "completed".equals(t.getStatus()) &&
|
||||
@ -716,6 +761,10 @@ public class AnalyticsService {
|
||||
.fiveStarCount(fiveStarCount)
|
||||
.satisfactionRate(Math.round(satisfactionRate * 10) / 10.0)
|
||||
.totalScore(totalScore)
|
||||
.deductionScore(deductionScore)
|
||||
.netScore(netScore)
|
||||
.targetScore(targetScore)
|
||||
.targetCompletionRate(targetCompletionRate)
|
||||
.weekCompletedCount(weekCompletedCount)
|
||||
.monthCompletedCount(completedCount)
|
||||
.build());
|
||||
@ -1067,5 +1116,101 @@ public class AnalyticsService {
|
||||
default: return status;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取延误扣分比例
|
||||
* @param deptId 部门ID
|
||||
* @param month 月份 yyyy-MM
|
||||
* @return 扣分比例(默认1%)
|
||||
*/
|
||||
private BigDecimal getDeductionRate(Long deptId, String month) {
|
||||
if (deptId == null) {
|
||||
return BigDecimal.ONE; // 默认1%
|
||||
}
|
||||
|
||||
try {
|
||||
// 先查找指定月份的配置
|
||||
DesignerTargetScore config = designerTargetScoreMapper.selectOne(
|
||||
new LambdaQueryWrapper<DesignerTargetScore>()
|
||||
.eq(DesignerTargetScore::getDeptId, deptId)
|
||||
.isNull(DesignerTargetScore::getUserId) // 部门通用配置
|
||||
.eq(DesignerTargetScore::getEffectiveMonth, month)
|
||||
);
|
||||
|
||||
if (config != null && config.getDeductionRate() != null) {
|
||||
return config.getDeductionRate();
|
||||
}
|
||||
|
||||
// 查找通用配置(没有指定月份的)
|
||||
config = designerTargetScoreMapper.selectOne(
|
||||
new LambdaQueryWrapper<DesignerTargetScore>()
|
||||
.eq(DesignerTargetScore::getDeptId, deptId)
|
||||
.isNull(DesignerTargetScore::getUserId)
|
||||
.isNull(DesignerTargetScore::getEffectiveMonth)
|
||||
);
|
||||
|
||||
if (config != null && config.getDeductionRate() != null) {
|
||||
return config.getDeductionRate();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("获取扣分比例配置失败: deptId={}, month={}", deptId, month, e);
|
||||
}
|
||||
|
||||
return BigDecimal.ONE; // 默认1%
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设计师目标积分
|
||||
* @param userId 用户ID
|
||||
* @param deptId 部门ID
|
||||
* @param month 月份 yyyy-MM
|
||||
* @return 目标积分
|
||||
*/
|
||||
private BigDecimal getTargetScore(Long userId, Long deptId, String month) {
|
||||
try {
|
||||
// 先查找用户个人配置
|
||||
if (userId != null) {
|
||||
DesignerTargetScore config = designerTargetScoreMapper.selectOne(
|
||||
new LambdaQueryWrapper<DesignerTargetScore>()
|
||||
.eq(DesignerTargetScore::getUserId, userId)
|
||||
.eq(DesignerTargetScore::getEffectiveMonth, month)
|
||||
);
|
||||
|
||||
if (config != null && config.getTargetScore() != null) {
|
||||
return config.getTargetScore();
|
||||
}
|
||||
}
|
||||
|
||||
// 查找部门通用配置
|
||||
if (deptId != null) {
|
||||
DesignerTargetScore config = designerTargetScoreMapper.selectOne(
|
||||
new LambdaQueryWrapper<DesignerTargetScore>()
|
||||
.eq(DesignerTargetScore::getDeptId, deptId)
|
||||
.isNull(DesignerTargetScore::getUserId)
|
||||
.eq(DesignerTargetScore::getEffectiveMonth, month)
|
||||
);
|
||||
|
||||
if (config != null && config.getTargetScore() != null) {
|
||||
return config.getTargetScore();
|
||||
}
|
||||
|
||||
// 查找部门默认配置(没有指定月份)
|
||||
config = designerTargetScoreMapper.selectOne(
|
||||
new LambdaQueryWrapper<DesignerTargetScore>()
|
||||
.eq(DesignerTargetScore::getDeptId, deptId)
|
||||
.isNull(DesignerTargetScore::getUserId)
|
||||
.isNull(DesignerTargetScore::getEffectiveMonth)
|
||||
);
|
||||
|
||||
if (config != null && config.getTargetScore() != null) {
|
||||
return config.getTargetScore();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("获取目标积分配置失败: userId={}, deptId={}, month={}", userId, deptId, month, e);
|
||||
}
|
||||
|
||||
return BigDecimal.ZERO; // 默认0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,158 @@
|
||||
package com.designhub.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.designhub.dto.DesignerTargetScoreDTO;
|
||||
import com.designhub.entity.Department;
|
||||
import com.designhub.entity.DesignerTargetScore;
|
||||
import com.designhub.entity.User;
|
||||
import com.designhub.mapper.DepartmentMapper;
|
||||
import com.designhub.mapper.DesignerTargetScoreMapper;
|
||||
import com.designhub.mapper.UserMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DesignerTargetScoreService {
|
||||
|
||||
private final DesignerTargetScoreMapper designerTargetScoreMapper;
|
||||
private final DepartmentMapper departmentMapper;
|
||||
private final UserMapper userMapper;
|
||||
|
||||
/**
|
||||
* 获取目标积分配置列表
|
||||
*/
|
||||
public Page<Map<String, Object>> getTargetScoreList(int page, int pageSize, Long deptId, String month) {
|
||||
Page<DesignerTargetScore> pageParam = new Page<>(page, pageSize);
|
||||
|
||||
LambdaQueryWrapper<DesignerTargetScore> wrapper = new LambdaQueryWrapper<>();
|
||||
if (deptId != null) {
|
||||
wrapper.eq(DesignerTargetScore::getDeptId, deptId);
|
||||
}
|
||||
if (month != null && !month.isEmpty()) {
|
||||
wrapper.eq(DesignerTargetScore::getEffectiveMonth, month);
|
||||
}
|
||||
wrapper.orderByDesc(DesignerTargetScore::getCreateTime);
|
||||
|
||||
Page<DesignerTargetScore> result = designerTargetScoreMapper.selectPage(pageParam, wrapper);
|
||||
|
||||
// 转换为包含部门名称和用户名的结果
|
||||
Page<Map<String, Object>> resultPage = new Page<>(page, pageSize);
|
||||
resultPage.setTotal(result.getTotal());
|
||||
|
||||
List<Map<String, Object>> records = result.getRecords().stream().map(config -> {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("id", config.getId());
|
||||
map.put("deptId", config.getDeptId());
|
||||
map.put("userId", config.getUserId());
|
||||
map.put("targetScore", config.getTargetScore());
|
||||
map.put("effectiveMonth", config.getEffectiveMonth());
|
||||
map.put("deductionRate", config.getDeductionRate());
|
||||
map.put("createTime", config.getCreateTime());
|
||||
|
||||
// 获取部门名称
|
||||
if (config.getDeptId() != null) {
|
||||
Department dept = departmentMapper.selectById(config.getDeptId());
|
||||
map.put("deptName", dept != null ? dept.getDeptName() : "");
|
||||
}
|
||||
|
||||
// 获取用户名
|
||||
if (config.getUserId() != null) {
|
||||
User user = userMapper.selectById(config.getUserId());
|
||||
map.put("userName", user != null ? user.getRealName() : "");
|
||||
} else {
|
||||
map.put("userName", "部门通用配置");
|
||||
}
|
||||
|
||||
return map;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
resultPage.setRecords(records);
|
||||
return resultPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存目标积分配置
|
||||
*/
|
||||
@Transactional
|
||||
public void saveTargetScore(DesignerTargetScoreDTO dto) {
|
||||
DesignerTargetScore config;
|
||||
|
||||
if (dto.getId() != null) {
|
||||
// 更新
|
||||
config = designerTargetScoreMapper.selectById(dto.getId());
|
||||
if (config == null) {
|
||||
config = new DesignerTargetScore();
|
||||
}
|
||||
} else {
|
||||
// 新增 - 检查是否已存在
|
||||
LambdaQueryWrapper<DesignerTargetScore> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(DesignerTargetScore::getDeptId, dto.getDeptId());
|
||||
|
||||
if (dto.getUserId() != null) {
|
||||
wrapper.eq(DesignerTargetScore::getUserId, dto.getUserId());
|
||||
} else {
|
||||
wrapper.isNull(DesignerTargetScore::getUserId);
|
||||
}
|
||||
|
||||
if (dto.getEffectiveMonth() != null && !dto.getEffectiveMonth().isEmpty()) {
|
||||
wrapper.eq(DesignerTargetScore::getEffectiveMonth, dto.getEffectiveMonth());
|
||||
} else {
|
||||
wrapper.isNull(DesignerTargetScore::getEffectiveMonth);
|
||||
}
|
||||
|
||||
config = designerTargetScoreMapper.selectOne(wrapper);
|
||||
if (config == null) {
|
||||
config = new DesignerTargetScore();
|
||||
config.setCreateTime(LocalDateTime.now());
|
||||
}
|
||||
}
|
||||
|
||||
config.setDeptId(dto.getDeptId());
|
||||
config.setUserId(dto.getUserId());
|
||||
config.setTargetScore(dto.getTargetScore());
|
||||
config.setEffectiveMonth(dto.getEffectiveMonth());
|
||||
config.setDeductionRate(dto.getDeductionRate());
|
||||
config.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
if (config.getId() != null) {
|
||||
designerTargetScoreMapper.updateById(config);
|
||||
} else {
|
||||
designerTargetScoreMapper.insert(config);
|
||||
}
|
||||
|
||||
log.info("保存设计师目标积分配置: deptId={}, userId={}, targetScore={}, month={}",
|
||||
dto.getDeptId(), dto.getUserId(), dto.getTargetScore(), dto.getEffectiveMonth());
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除目标积分配置
|
||||
*/
|
||||
public void deleteTargetScore(Long id) {
|
||||
designerTargetScoreMapper.deleteById(id);
|
||||
log.info("删除设计师目标积分配置: id={}", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有接单部门
|
||||
*/
|
||||
public List<Department> getReceivingDepartments() {
|
||||
return departmentMapper.selectList(
|
||||
new LambdaQueryWrapper<Department>()
|
||||
.eq(Department::getDeptType, "receiving")
|
||||
.eq(Department::getStatus, 1)
|
||||
.orderByAsc(Department::getSortOrder)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,6 +57,10 @@ public class DesignerPerformanceVO {
|
||||
private Integer fiveStarCount; // 5星评价数
|
||||
private Double satisfactionRate; // 好评率
|
||||
private BigDecimal totalScore; // 获得总积分
|
||||
private BigDecimal deductionScore; // 延误扣分
|
||||
private BigDecimal netScore; // 净积分(总积分-扣分)
|
||||
private BigDecimal targetScore; // 当月目标积分
|
||||
private Double targetCompletionRate;// 目标完成率
|
||||
private Integer weekCompletedCount; // 本周完成数
|
||||
private Integer monthCompletedCount;// 本月完成数
|
||||
}
|
||||
|
||||
@ -97,3 +97,49 @@ export const initDefaultConfigs = () => {
|
||||
return request.post<any>('/system-config/init')
|
||||
}
|
||||
|
||||
// ==================== 设计师目标积分配置 ====================
|
||||
|
||||
export interface DesignerTargetScore {
|
||||
id?: number
|
||||
deptId: number
|
||||
userId?: number
|
||||
targetScore: number
|
||||
effectiveMonth?: string
|
||||
deductionRate?: number
|
||||
deptName?: string
|
||||
userName?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目标积分配置列表
|
||||
*/
|
||||
export const getDesignerTargetScoreList = (params: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
deptId?: number
|
||||
month?: string
|
||||
}) => {
|
||||
return request.get<any>('/designer-target-score/list', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存目标积分配置
|
||||
*/
|
||||
export const saveDesignerTargetScore = (data: DesignerTargetScore) => {
|
||||
return request.post<any>('/designer-target-score/save', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除目标积分配置
|
||||
*/
|
||||
export const deleteDesignerTargetScore = (id: number) => {
|
||||
return request.delete<any>(`/designer-target-score/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有接单部门
|
||||
*/
|
||||
export const getReceivingDepartments = () => {
|
||||
return request.get<any>('/designer-target-score/receiving-departments')
|
||||
}
|
||||
|
||||
|
||||
@ -225,6 +225,20 @@
|
||||
<template v-else-if="column.key === 'totalScore'">
|
||||
<span class="score-value">{{ record.totalScore || 0 }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'deductionScore'">
|
||||
<span class="deduction-value">{{ record.deductionScore || 0 }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'netScore'">
|
||||
<span class="net-score-value">{{ record.netScore || 0 }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'targetScore'">
|
||||
<span>{{ record.targetScore || '-' }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'targetCompletionRate'">
|
||||
<span :class="getTargetRateClass(record.targetCompletionRate)">
|
||||
{{ record.targetScore > 0 ? (record.targetCompletionRate || 0) + '%' : '-' }}
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
@ -577,15 +591,18 @@ const userColumns = [
|
||||
]
|
||||
|
||||
const designerColumns = [
|
||||
{ title: '排名', key: 'rank', width: 80, align: 'center' },
|
||||
{ title: '设计师', dataIndex: 'realName', key: 'realName' },
|
||||
{ title: '部门', dataIndex: 'deptName', key: 'deptName' },
|
||||
{ title: '接单量', dataIndex: 'receivedCount', key: 'receivedCount', sorter: (a: any, b: any) => a.receivedCount - b.receivedCount, align: 'center' },
|
||||
{ title: '完成量', dataIndex: 'completedCount', key: 'completedCount', align: 'center' },
|
||||
{ title: '完成率', dataIndex: 'completionRate', key: 'completionRate', customRender: ({ text }: any) => text + '%', align: 'center' },
|
||||
{ title: '准时率', key: 'onTimeRate', align: 'center' },
|
||||
{ title: '积分', dataIndex: 'totalScore', key: 'totalScore', sorter: (a: any, b: any) => (a.totalScore || 0) - (b.totalScore || 0), align: 'center' },
|
||||
{ title: '评分', key: 'avgRating', width: 140, align: 'center' }
|
||||
{ title: '排名', key: 'rank', width: 60, align: 'center' },
|
||||
{ title: '设计师', dataIndex: 'realName', key: 'realName', width: 80 },
|
||||
{ title: '部门', dataIndex: 'deptName', key: 'deptName', width: 100 },
|
||||
{ title: '接单', dataIndex: 'receivedCount', key: 'receivedCount', sorter: (a: any, b: any) => a.receivedCount - b.receivedCount, align: 'center', width: 60 },
|
||||
{ title: '完成', dataIndex: 'completedCount', key: 'completedCount', align: 'center', width: 60 },
|
||||
{ title: '准时率', key: 'onTimeRate', align: 'center', width: 70 },
|
||||
{ title: '获得积分', key: 'totalScore', align: 'center', width: 80 },
|
||||
{ title: '延误扣分', key: 'deductionScore', align: 'center', width: 80 },
|
||||
{ title: '净积分', key: 'netScore', sorter: (a: any, b: any) => (a.netScore || 0) - (b.netScore || 0), align: 'center', width: 70 },
|
||||
{ title: '目标积分', key: 'targetScore', align: 'center', width: 80 },
|
||||
{ title: '目标完成率', key: 'targetCompletionRate', align: 'center', width: 90 },
|
||||
{ title: '评分', key: 'avgRating', width: 120, align: 'center' }
|
||||
]
|
||||
|
||||
const efficiencyColumns = [
|
||||
@ -776,6 +793,15 @@ const getRankClass = (rank: number) => {
|
||||
return ''
|
||||
}
|
||||
|
||||
// 目标完成率样式
|
||||
const getTargetRateClass = (rate: number | null | undefined) => {
|
||||
if (rate === null || rate === undefined) return ''
|
||||
if (rate >= 100) return 'text-success' // 完成目标 - 绿色
|
||||
if (rate >= 80) return 'text-primary' // 接近目标 - 蓝色
|
||||
if (rate >= 50) return 'text-warning' // 进度一般 - 黄色
|
||||
return 'text-danger' // 进度落后 - 红色
|
||||
}
|
||||
|
||||
const getProgressColor = (rate: number) => {
|
||||
if (rate >= 80) return '#31f08b'
|
||||
if (rate >= 60) return '#3B82F6'
|
||||
@ -1371,6 +1397,25 @@ onMounted(() => {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// 延误扣分样式
|
||||
.deduction-value {
|
||||
font-weight: 500;
|
||||
color: #ef4444;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// 净积分样式
|
||||
.net-score-value {
|
||||
font-weight: 600;
|
||||
color: #60a5fa;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// 文字颜色
|
||||
.text-primary {
|
||||
color: #60a5fa !important;
|
||||
}
|
||||
|
||||
// 全宽卡片
|
||||
.full-width {
|
||||
width: 100%;
|
||||
|
||||
@ -1414,6 +1414,86 @@
|
||||
</a-table>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 设计师目标积分设置 -->
|
||||
<a-tab-pane key="designer-target" tab="设计师目标积分">
|
||||
<div class="settings-section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">设计师目标积分配置</h2>
|
||||
<a-button type="primary" @click="showDesignerTargetModal()">
|
||||
<template #icon><Icon name="plus" size="sm" /></template>
|
||||
添加配置
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-alert
|
||||
message="配置说明"
|
||||
description="为接单部门设计师设置月度目标积分和延误扣分比例。延误扣分 = 任务积分 × 延误小时数 × 扣分比例%。"
|
||||
type="info"
|
||||
show-icon
|
||||
style="margin-bottom: 16px;"
|
||||
/>
|
||||
|
||||
<div class="filter-bar" style="margin-bottom: 16px; display: flex; gap: 16px;">
|
||||
<a-select
|
||||
v-model:value="designerTargetFilter.deptId"
|
||||
placeholder="筛选部门"
|
||||
style="width: 200px"
|
||||
allow-clear
|
||||
@change="loadDesignerTargetList"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="dept in receivingDepartments"
|
||||
:key="dept.id"
|
||||
:value="dept.id"
|
||||
>
|
||||
{{ dept.deptName }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-date-picker
|
||||
v-model:value="designerTargetFilter.monthValue"
|
||||
picker="month"
|
||||
placeholder="筛选月份"
|
||||
format="YYYY-MM"
|
||||
value-format="YYYY-MM"
|
||||
@change="handleDesignerTargetMonthChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="designerTargetColumns"
|
||||
:data-source="designerTargetList"
|
||||
:loading="loadingDesignerTarget"
|
||||
row-key="id"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'targetScore'">
|
||||
<span class="target-score">{{ record.targetScore }} 分</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'deductionRate'">
|
||||
<span>{{ record.deductionRate || 1 }}%</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'effectiveMonth'">
|
||||
<span>{{ record.effectiveMonth || '通用配置' }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="showDesignerTargetModal(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定删除?"
|
||||
@confirm="handleDeleteDesignerTarget(record.id)"
|
||||
>
|
||||
<a-button type="link" danger size="small">删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<!-- 需求类型弹窗 -->
|
||||
@ -2093,6 +2173,70 @@
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 设计师目标积分配置弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="designerTargetModalVisible"
|
||||
:title="designerTargetModalTitle"
|
||||
width="500px"
|
||||
@ok="handleSaveDesignerTarget"
|
||||
@cancel="designerTargetModalVisible = false"
|
||||
>
|
||||
<a-form :model="designerTargetForm" layout="vertical">
|
||||
<a-form-item label="接单部门" required>
|
||||
<a-select
|
||||
v-model:value="designerTargetForm.deptId"
|
||||
placeholder="请选择接单部门"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="dept in receivingDepartments"
|
||||
:key="dept.id"
|
||||
:value="dept.id"
|
||||
>
|
||||
{{ dept.deptName }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="目标积分" required>
|
||||
<a-input-number
|
||||
v-model:value="designerTargetForm.targetScore"
|
||||
:min="0"
|
||||
:max="10000"
|
||||
style="width: 100%"
|
||||
placeholder="每月目标积分,例如:500"
|
||||
/>
|
||||
<div style="font-size: 12px; color: #888; margin-top: 4px;">
|
||||
设计师每月需完成的目标积分数
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="延误扣分比例 (%)">
|
||||
<a-input-number
|
||||
v-model:value="designerTargetForm.deductionRate"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:precision="1"
|
||||
style="width: 100%"
|
||||
placeholder="默认1%"
|
||||
/>
|
||||
<div style="font-size: 12px; color: #888; margin-top: 4px;">
|
||||
每延误1小时,扣除任务积分的百分比。例如:设置为1%,延误2小时,100分任务扣除 2 分
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="生效月份">
|
||||
<a-date-picker
|
||||
v-model:value="designerTargetForm.effectiveMonth"
|
||||
picker="month"
|
||||
placeholder="留空则为通用配置"
|
||||
format="YYYY-MM"
|
||||
value-format="YYYY-MM"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div style="font-size: 12px; color: #888; margin-top: 4px;">
|
||||
留空表示通用配置,适用于所有月份
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
@ -2134,7 +2278,12 @@ import {
|
||||
batchUpdateSystemConfigs,
|
||||
createSystemConfig,
|
||||
updateSystemConfig,
|
||||
type SystemConfig
|
||||
getDesignerTargetScoreList,
|
||||
saveDesignerTargetScore,
|
||||
deleteDesignerTargetScore,
|
||||
getReceivingDepartments,
|
||||
type SystemConfig,
|
||||
type DesignerTargetScore
|
||||
} from '@/api/system'
|
||||
import {
|
||||
getAllUsers,
|
||||
@ -2304,6 +2453,35 @@ const thirdPartyDbColumns = [
|
||||
{ title: '操作', key: 'actions', width: 260 }
|
||||
]
|
||||
|
||||
// 设计师目标积分配置相关
|
||||
const designerTargetList = ref<any[]>([])
|
||||
const loadingDesignerTarget = ref(false)
|
||||
const receivingDepartments = ref<any[]>([])
|
||||
const designerTargetFilter = reactive({
|
||||
deptId: undefined as number | undefined,
|
||||
month: undefined as string | undefined,
|
||||
monthValue: undefined as any
|
||||
})
|
||||
const designerTargetForm = reactive<DesignerTargetScore>({
|
||||
deptId: 0,
|
||||
userId: undefined,
|
||||
targetScore: 0,
|
||||
effectiveMonth: undefined,
|
||||
deductionRate: 1
|
||||
})
|
||||
const designerTargetModalVisible = ref(false)
|
||||
const designerTargetModalTitle = ref('添加目标积分配置')
|
||||
|
||||
const designerTargetColumns = [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 60 },
|
||||
{ title: '部门', dataIndex: 'deptName', key: 'deptName' },
|
||||
{ title: '适用对象', dataIndex: 'userName', key: 'userName' },
|
||||
{ title: '目标积分', key: 'targetScore', width: 100 },
|
||||
{ title: '延误扣分比例', key: 'deductionRate', width: 120 },
|
||||
{ title: '生效月份', key: 'effectiveMonth', width: 120 },
|
||||
{ title: '操作', key: 'actions', width: 150 }
|
||||
]
|
||||
|
||||
const defaultDepartmentForm = (): DepartmentForm => ({
|
||||
deptName: '',
|
||||
deptCode: '',
|
||||
@ -4431,6 +4609,9 @@ onMounted(() => {
|
||||
loadDelayPenaltyConfig()
|
||||
// 加载第三方素材API配置
|
||||
loadExternalAssetApiConfig()
|
||||
// 加载设计师目标积分配置
|
||||
loadReceivingDepartments()
|
||||
loadDesignerTargetList()
|
||||
})
|
||||
|
||||
// S3配置相关方法
|
||||
@ -4623,6 +4804,99 @@ const handleDisableThirdPartyDb = async (id: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 设计师目标积分配置相关方法 ====================
|
||||
|
||||
const loadReceivingDepartments = async () => {
|
||||
try {
|
||||
const response = await getReceivingDepartments()
|
||||
receivingDepartments.value = response.data || []
|
||||
} catch (error: any) {
|
||||
console.error('加载接单部门失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const loadDesignerTargetList = async () => {
|
||||
loadingDesignerTarget.value = true
|
||||
try {
|
||||
const params: any = { page: 1, pageSize: 100 }
|
||||
if (designerTargetFilter.deptId) {
|
||||
params.deptId = designerTargetFilter.deptId
|
||||
}
|
||||
if (designerTargetFilter.month) {
|
||||
params.month = designerTargetFilter.month
|
||||
}
|
||||
const response = await getDesignerTargetScoreList(params)
|
||||
designerTargetList.value = response.data?.records || []
|
||||
} catch (error: any) {
|
||||
console.error('加载目标积分配置失败:', error)
|
||||
message.error(error.response?.data?.message || '加载目标积分配置失败')
|
||||
} finally {
|
||||
loadingDesignerTarget.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleDesignerTargetMonthChange = (value: any) => {
|
||||
designerTargetFilter.month = value
|
||||
loadDesignerTargetList()
|
||||
}
|
||||
|
||||
const showDesignerTargetModal = (record?: any) => {
|
||||
if (record) {
|
||||
designerTargetModalTitle.value = '编辑目标积分配置'
|
||||
Object.assign(designerTargetForm, {
|
||||
id: record.id,
|
||||
deptId: record.deptId,
|
||||
userId: record.userId,
|
||||
targetScore: record.targetScore,
|
||||
effectiveMonth: record.effectiveMonth,
|
||||
deductionRate: record.deductionRate || 1
|
||||
})
|
||||
} else {
|
||||
designerTargetModalTitle.value = '添加目标积分配置'
|
||||
Object.assign(designerTargetForm, {
|
||||
id: undefined,
|
||||
deptId: receivingDepartments.value[0]?.id || 0,
|
||||
userId: undefined,
|
||||
targetScore: 500,
|
||||
effectiveMonth: undefined,
|
||||
deductionRate: 1
|
||||
})
|
||||
}
|
||||
designerTargetModalVisible.value = true
|
||||
}
|
||||
|
||||
const handleSaveDesignerTarget = async () => {
|
||||
if (!designerTargetForm.deptId) {
|
||||
message.warning('请选择部门')
|
||||
return
|
||||
}
|
||||
if (!designerTargetForm.targetScore || designerTargetForm.targetScore <= 0) {
|
||||
message.warning('请输入有效的目标积分')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await saveDesignerTargetScore(designerTargetForm)
|
||||
message.success('保存成功')
|
||||
designerTargetModalVisible.value = false
|
||||
loadDesignerTargetList()
|
||||
} catch (error: any) {
|
||||
console.error('保存目标积分配置失败:', error)
|
||||
message.error(error.response?.data?.message || '保存目标积分配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteDesignerTarget = async (id: number) => {
|
||||
try {
|
||||
await deleteDesignerTargetScore(id)
|
||||
message.success('删除成功')
|
||||
loadDesignerTargetList()
|
||||
} catch (error: any) {
|
||||
console.error('删除目标积分配置失败:', error)
|
||||
message.error(error.response?.data?.message || '删除目标积分配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
watch(activeTab, (value) => {
|
||||
// 切换到用户与部门 tab 时,始终刷新数据以获取最新状态
|
||||
if (value === 'users') {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user