add:1.新增报功页面。2.新增后端报功功能(查询当前参与项目和任务详情,新增报功)。

This commit is contained in:
2026-07-13 17:25:20 +08:00
parent d8ee347915
commit 3fc52c9af2
6 changed files with 657 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
package org.nl.pms.projectProgressReport.controller;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.nl.pms.project.vo.ApiResponse;
import org.nl.pms.projectProgressReport.dto.ProjectProgressReportCreateDTO;
import org.nl.pms.projectProgressReport.service.ProjectProgressReportService;
import org.nl.pms.projectProgressReport.vo.ProjectProgressReportDetailVO;
import org.nl.pms.projectProgressReport.vo.ProjectProgressTaskListVO;
import org.springframework.web.bind.annotation.*;
/**
* @author liejiu
*/
@RestController
@RequestMapping("/api/project-progress-report")
@RequiredArgsConstructor
@CrossOrigin
public class ProjectProgressReportController {
private final ProjectProgressReportService projectProgressReportService;
@GetMapping("/my-tasks")
public ApiResponse<ProjectProgressTaskListVO> listMyProjectTasks() {
return ApiResponse.success(projectProgressReportService.listMyProjectTasks());
}
@GetMapping("/detail/{detailId}")
public ApiResponse<ProjectProgressReportDetailVO> getDetailReports(@PathVariable Long detailId) {
return ApiResponse.success(projectProgressReportService.getDetailReports(detailId));
}
@PostMapping
public ApiResponse<Void> create(@Valid @RequestBody ProjectProgressReportCreateDTO createDTO) {
projectProgressReportService.create(createDTO);
return ApiResponse.success("提交成功", null);
}
}

View File

@@ -0,0 +1,17 @@
package org.nl.pms.projectProgressReport.service;
import org.nl.pms.projectProgressReport.dto.ProjectProgressReportCreateDTO;
import org.nl.pms.projectProgressReport.vo.ProjectProgressReportDetailVO;
import org.nl.pms.projectProgressReport.vo.ProjectProgressTaskListVO;
/**
* @author liejiu
*/
public interface ProjectProgressReportService {
ProjectProgressTaskListVO listMyProjectTasks();
ProjectProgressReportDetailVO getDetailReports(Long detailId);
void create(ProjectProgressReportCreateDTO createDTO);
}

View File

@@ -0,0 +1,324 @@
package org.nl.pms.projectProgressReport.service.impl;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.nl.pms.common.exception.BadRequestException;
import org.nl.pms.common.utils.SecurityUtils;
import org.nl.pms.project.entity.Project;
import org.nl.pms.project.mapper.ProjectMapper;
import org.nl.pms.projectPlan.entity.ProjectPlanDetail;
import org.nl.pms.projectPlan.entity.ProjectPlanMaster;
import org.nl.pms.projectPlan.mapper.ProjectPlanDetailMapper;
import org.nl.pms.projectPlan.mapper.ProjectPlanMasterMapper;
import org.nl.pms.projectProgressReport.dto.ProjectProgressReportCreateDTO;
import org.nl.pms.projectProgressReport.entity.ProjectProgressReport;
import org.nl.pms.projectProgressReport.mapper.ProjectProgressReportMapper;
import org.nl.pms.projectProgressReport.service.ProjectProgressReportService;
import org.nl.pms.projectProgressReport.vo.ProjectProgressReportDetailVO;
import org.nl.pms.projectProgressReport.vo.ProjectProgressReportVO;
import org.nl.pms.projectProgressReport.vo.ProjectProgressTaskListVO;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* @author liejiu
*/
@Service
@RequiredArgsConstructor
public class ProjectProgressReportServiceImpl implements ProjectProgressReportService {
private final ProjectMapper projectMapper;
private final ProjectPlanMasterMapper masterMapper;
private final ProjectPlanDetailMapper detailMapper;
private final ProjectProgressReportMapper reportMapper;
private final ObjectMapper objectMapper;
@Override
public ProjectProgressTaskListVO listMyProjectTasks() {
String user = userName();
List<ProjectPlanDetail> mine = detailMapper.selectList(new LambdaQueryWrapper<ProjectPlanDetail>()).stream()
.filter(i -> members(i.getTaskMembers()).contains(user)).toList();
ProjectProgressTaskListVO vo = new ProjectProgressTaskListVO();
if (mine.isEmpty()) {
return vo;
}
Map<Long, ProjectProgressTaskListVO.ProjectTaskVO> map = new LinkedHashMap<>();
Map<Long, List<ProjectProgressReport>> reportMap = reportMap(mine.stream().map(ProjectPlanDetail::getId).toList());
for (ProjectPlanDetail d : mine) {
Project p = projectMapper.selectById(d.getProjectId());
ProjectPlanMaster m = masterMapper.selectById(d.getMasterId());
if (p == null || m == null) {
continue;
}
ProjectProgressTaskListVO.ProjectTaskVO pvo = map.computeIfAbsent(p.getId(), k -> projectVO(p));
pvo.getDetails().add(detailVO(d, m, reportMap.getOrDefault(d.getId(), Collections.emptyList())));
pvo.setDetailCount(pvo.getDetails().size());
}
vo.setProjects(new ArrayList<>(map.values()));
return vo;
}
@Override
public ProjectProgressReportDetailVO getDetailReports(Long detailId) {
ProjectPlanDetail d = detail(detailId);
checkMember(d);
Project p = projectMapper.selectById(d.getProjectId());
ProjectPlanMaster m = masterMapper.selectById(d.getMasterId());
if (p == null || m == null) {
throw new BadRequestException("任务关联项目数据不存在");
}
List<ProjectProgressReport> rs = reports(detailId);
ProjectProgressReportDetailVO vo = new ProjectProgressReportDetailVO();
vo.setDetailId(d.getId());
vo.setMasterId(d.getMasterId());
vo.setProjectId(d.getProjectId());
vo.setProjectCode(p.getProjectCode());
vo.setProjectName(p.getProjectName());
vo.setMasterName(m.getMasterName());
vo.setSubItemName(m.getSubItemName());
vo.setDetailSeq(d.getDetailSeq());
vo.setDetailName(d.getDetailName());
vo.setDetailDescription(d.getDetailDescription());
vo.setDevDays(d.getDevDays());
vo.setDevStartDate(d.getDevStartDate());
vo.setDevEndDate(d.getDevEndDate());
vo.setTaskMembers(members(d.getTaskMembers()));
vo.setStatus(d.getStatus());
vo.setStatusLabel(taskStatus(d.getStatus()));
vo.setPriority(d.getPriority());
vo.setPriorityLabel(priority(d.getPriority()));
vo.setProgress(d.getProgress());
vo.setProgressId(d.getProgressId());
vo.setRemark(d.getRemark());
vo.setCanCreateReport(canCreate(rs));
vo.setCanCreateReportReason(reason(rs));
vo.setReports(rs.stream().map(this::reportVO).toList());
return vo;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void create(ProjectProgressReportCreateDTO dto) {
ProjectPlanDetail d = detail(dto.getDetailId());
checkMember(d);
validate(dto, d);
List<ProjectProgressReport> rs = reports(dto.getDetailId());
if (!canCreate(rs)) {
throw new BadRequestException(reason(rs));
}
int current = d.getProgress() == null ? 0 : d.getProgress();
if (current != dto.getProgressBefore()) {
throw new BadRequestException("报功前进度与当前任务进度不一致,请刷新后重试");
}
ProjectProgressReport r = new ProjectProgressReport();
r.setDetailId(d.getId());
r.setProjectId(d.getProjectId());
r.setReportDate(dto.getReportDate());
r.setWorkHours(dto.getWorkHours().setScale(1, RoundingMode.HALF_UP));
r.setProgressBefore(dto.getProgressBefore());
r.setProgressIncrement(dto.getProgressIncrement());
r.setWorkContent(dto.getWorkContent());
r.setRemainingWork(dto.getRemainingWork());
r.setIsOvertime(dto.getIsOvertime());
r.setStatus(1);
r.setCreatedName(userName());
r.setCreatedTime(LocalDateTime.now());
r.setUpdatedTime(LocalDateTime.now());
reportMapper.insert(r);
int after = dto.getProgressBefore() + dto.getProgressIncrement();
d.setProgress(after);
if (after >= 100) {
d.setStatus(3);
} else if (after > 0 && (d.getStatus() == null || d.getStatus() == 1)) {
d.setStatus(2);
}
detailMapper.updateById(d);
}
private ProjectPlanDetail detail(Long id) {
ProjectPlanDetail d = detailMapper.selectById(id);
if (d == null) {
throw new BadRequestException("任务明细不存在");
}
return d;
}
private List<ProjectProgressReport> reports(Long detailId) {
return reportMapper.selectList(new LambdaQueryWrapper<ProjectProgressReport>()
.eq(ProjectProgressReport::getDetailId, detailId)
.orderByDesc(ProjectProgressReport::getCreatedTime)
.orderByDesc(ProjectProgressReport::getId));
}
private void checkMember(ProjectPlanDetail d) {
if (!members(d.getTaskMembers()).contains(userName())) {
throw new BadRequestException("您未参与该任务,无法操作报功数据");
}
}
private void validate(ProjectProgressReportCreateDTO dto, ProjectPlanDetail d) {
if (dto.getWorkHours().compareTo(BigDecimal.ZERO) <= 0) {
throw new BadRequestException("报功工时必须大于0");
}
if (dto.getWorkHours().compareTo(new BigDecimal("24")) > 0) {
throw new BadRequestException("报功工时不能大于24小时");
}
if (dto.getWorkHours().multiply(new BigDecimal("10")).stripTrailingZeros().scale() > 0) {
throw new BadRequestException("报功工时仅支持0.1小时精度");
}
if (dto.getIsOvertime() == null || (dto.getIsOvertime() != 0 && dto.getIsOvertime() != 1)) {
throw new BadRequestException("是否加班取值错误");
}
if (dto.getProgressBefore() + dto.getProgressIncrement() > 100) {
throw new BadRequestException("进度累计不能超过100");
}
if (d.getStatus() != null && d.getStatus() == 3) {
throw new BadRequestException("当前任务已完成,不能继续报功");
}
}
private Map<Long, List<ProjectProgressReport>> reportMap(List<Long> detailIds) {
if (detailIds.isEmpty()) {
return Collections.emptyMap();
}
Map<Long, List<ProjectProgressReport>> map = new LinkedHashMap<>();
for (ProjectProgressReport i : reportMapper.selectList(new LambdaQueryWrapper<ProjectProgressReport>().in(ProjectProgressReport::getDetailId, detailIds).orderByDesc(ProjectProgressReport::getCreatedTime).orderByDesc(ProjectProgressReport::getId))) {
map.computeIfAbsent(i.getDetailId(), k -> new ArrayList<>()).add(i);
}
return map;
}
private ProjectProgressTaskListVO.ProjectTaskVO projectVO(Project p) {
ProjectProgressTaskListVO.ProjectTaskVO vo = new ProjectProgressTaskListVO.ProjectTaskVO();
vo.setProjectId(p.getId());
vo.setProjectCode(p.getProjectCode());
vo.setProjectName(p.getProjectName());
vo.setProjectType(p.getProjectType());
vo.setDevelopLeader(p.getDevelopLeader());
vo.setProjectLeader(p.getProjectLeader());
vo.setDetailCount(0); return vo;
}
private ProjectProgressTaskListVO.TaskDetailVO detailVO(ProjectPlanDetail d, ProjectPlanMaster m, List<ProjectProgressReport> rs) {
ProjectProgressTaskListVO.TaskDetailVO vo = new ProjectProgressTaskListVO.TaskDetailVO();
vo.setDetailId(d.getId()); vo.setMasterId(d.getMasterId());
vo.setMasterName(m.getMasterName());
vo.setSubItemName(m.getSubItemName());
vo.setDetailSeq(d.getDetailSeq());
vo.setDetailName(d.getDetailName());
vo.setDetailDescription(d.getDetailDescription());
vo.setDevDays(d.getDevDays());
vo.setDevStartDate(d.getDevStartDate());
vo.setDevEndDate(d.getDevEndDate());
vo.setTaskMembers(members(d.getTaskMembers()));
vo.setStatus(d.getStatus());
vo.setStatusLabel(taskStatus(d.getStatus()));
vo.setPriority(d.getPriority());
vo.setPriorityLabel(priority(d.getPriority()));
vo.setProgress(d.getProgress());
vo.setProgressId(d.getProgressId());
vo.setRemark(d.getRemark());
vo.setCanCreateReport(canCreate(rs));
vo.setCanCreateReportReason(reason(rs));
vo.setReportCount(rs.size());
return vo;
}
private ProjectProgressReportVO reportVO(ProjectProgressReport r) {
ProjectProgressReportVO vo = new ProjectProgressReportVO();
vo.setId(r.getId()); vo.setDetailId(r.getDetailId());
vo.setProjectId(r.getProjectId());
vo.setReportDate(r.getReportDate());
vo.setWorkHours(r.getWorkHours());
vo.setProgressBefore(r.getProgressBefore());
vo.setProgressIncrement(r.getProgressIncrement());
vo.setProgressAfter((r.getProgressBefore() == null ? 0 : r.getProgressBefore()) + (r.getProgressIncrement() == null ? 0 : r.getProgressIncrement()));
vo.setWorkContent(r.getWorkContent()); vo.setRemainingWork(r.getRemainingWork());
vo.setIsOvertime(r.getIsOvertime());
vo.setIsOvertimeLabel(r.getIsOvertime() != null && r.getIsOvertime() == 1 ? "" : "");
vo.setStatus(r.getStatus());
vo.setStatusLabel(reportStatus(r.getStatus()));
vo.setCreatedName(r.getCreatedName());
vo.setCreatedTime(r.getCreatedTime());
vo.setUpdatedTime(r.getUpdatedTime());
return vo;
}
private boolean canCreate(List<ProjectProgressReport> rs) {
return rs == null || rs.isEmpty() || (rs.get(0).getStatus() != null && rs.get(0).getStatus() == 2);
}
private String reason(List<ProjectProgressReport> rs) {
return canCreate(rs) ? "可以新增报功" : "上一条报功审核未结束,暂不能发起当前报功";
}
private List<String> members(String json) {
if (!StringUtils.hasText(json)) {
return Collections.emptyList();
}
try {
return objectMapper.readValue(json, new TypeReference<List<String>>() {});
} catch (Exception e) {
throw new BadRequestException("任务成员数据解析失败");
}
}
private String userName() {
String n = SecurityUtils.getCurrentNickName();
if (!StringUtils.hasText(n)) {
n = SecurityUtils.getCurrentUsername();
}
if (!StringUtils.hasText(n)) {
throw new BadRequestException("未获取到当前登录用户信息");
}
return n;
}
private String taskStatus(Integer s) {
if (ObjectUtil.isEmpty(s)) {
return "未知";
}
return switch (s) {
case 1 -> "待开始";
case 2 -> "进行中";
case 3 -> "已完成";
case 4 -> "阻塞/延期";
default -> "未知";
};
}
private String priority(Integer p) {
if (ObjectUtil.isEmpty(p)) {
return "普通";
}
return switch (p) {
case 1 -> "紧急";
case 2 -> "普通";
case 3 -> "";
default -> "普通";
};
}
private String reportStatus(Integer s) {
if (ObjectUtil.isEmpty(s)) {
return "未知";
}
return switch (s) {
case 1 -> "待审核";
case 2 -> "已确认";
default -> "未知";
};
}
}

View File

@@ -139,6 +139,17 @@ const staticMenuData = [
icon: 'clipboard',
noCache: true
}
},
{
name: 'ProgressReport',
path: 'progress-report',
hidden: false,
component: 'project/progressReportIndex',
meta: {
title: '报功',
icon: 'clipboard',
noCache: true
}
}
]
},

View File

@@ -0,0 +1,25 @@
import request from '@/utils/request'
export function getMyTasks() {
return request({
url: 'api/project-progress-report/my-tasks',
method: 'get'
})
}
export function getReportsByDetail(detailId) {
return request({
url: `api/project-progress-report/detail/${detailId}`,
method: 'get'
})
}
export function addReport(data) {
return request({
url: 'api/project-progress-report',
method: 'post',
data
})
}
export default { getMyTasks, getReportsByDetail, addReport }

View File

@@ -0,0 +1,242 @@
<template>
<div class="app-container page">
<div class="left">
<el-card shadow="never" v-loading="loading">
<div slot="header" class="hd"><span>我参与的项目</span></div>
<div v-if="!projects.length && !loading" class="empty">暂无进行中的项目</div>
<div v-for="p in projects" :key="p.projectId" class="item" :class="{on:cp&&cp.projectId===p.projectId}" @click="pickProject(p)">
<div class="t">{{ p.projectName }}</div>
<div class="s">{{ p.projectCode }}</div>
<div class="s">开发负责人{{ p.developLeader || '-' }}</div>
<div class="s">任务明细{{ p.detailCount || 0 }}</div>
</div>
</el-card>
<el-card v-if="cp" shadow="never" class="mt16 tasks">
<div slot="header" class="hd"><span>工作明细</span><span class="sub">{{ tasks.length }}</span></div>
<div v-if="!tasks.length" class="empty">该项目下暂无分配给您的任务</div>
<div v-for="t in tasks" :key="t.detailId" class="item" :class="{on:ct&&ct.detailId===t.detailId}" @click="pickTask(t)">
<div class="row"><el-tag size="mini" :type="tTag(t.status)">{{ t.statusLabel || tLabel(t.status) }}</el-tag><el-tag size="mini" :type="priTag(t.priority)" style="margin-left:4px">{{ t.priorityLabel || priLabel(t.priority) }}</el-tag><span class="t" style="margin-left:6px">{{ t.detailName }}</span></div>
<div class="s">主项{{ t.masterName || '-' }} / 分项{{ t.subItemName || '-' }}</div>
<div class="s">{{ t.devStartDate || '-' }} ~ {{ t.devEndDate || '-' }}</div>
<div class="s">进度 {{ t.progress || 0 }}%</div>
<el-progress :percentage="t.progress || 0" :show-text="false" :stroke-width="4" />
</div>
</el-card>
</div>
<div class="right">
<el-card v-if="!ct" shadow="never" class="ph"><div class="empty big">请先选择工作明细</div></el-card>
<template v-else>
<el-card shadow="never">
<div class="top">
<div>
<div class="row"><span class="t">当前任务{{ ct.detailName }}</span><el-tag size="mini" :type="tTag(ct.status)" style="margin-left:8px">{{ ct.statusLabel || tLabel(ct.status) }}</el-tag><el-tag size="mini" :type="priTag(ct.priority)" style="margin-left:4px">{{ ct.priorityLabel || priLabel(ct.priority) }}</el-tag></div>
<div class="s">主项{{ ct.masterName || '-' }} / 分项{{ ct.subItemName || '-' }}</div>
<div class="s">开发周期{{ ct.devStartDate || '-' }} ~ {{ ct.devEndDate || '-' }}{{ ct.devDays || 0 }}</div>
<div class="s">成员{{ members(ct.taskMembers) }}</div>
</div>
<el-button type="primary" size="small" icon="el-icon-plus" :disabled="!canAdd" @click="open">新增报功</el-button>
</div>
<div v-if="!canAdd" class="warn">{{ ct.canCreateReportReason || '当前不能新增报功' }}</div>
<el-progress :percentage="ct.progress || 0" :stroke-width="10" />
</el-card>
<el-card shadow="never" class="mt16">
<div slot="header" class="hd"><span>报功记录</span><span class="sub">{{ reports.length }}</span></div>
<div v-loading="loadingReports">
<div v-if="!reports.length && !loadingReports" class="empty big">暂无报功记录</div>
<div v-for="(r,i) in reports" :key="r.id" class="rp">
<div class="rph">
<div class="row"><span class="t"> {{ reports.length-i }} 次报功</span><span class="s" style="margin-left:10px">{{ r.reportDate }}</span><el-tag size="mini" :type="r.status===2?'success':'warning'" style="margin-left:8px">{{ r.status===2?'已确认':'待确认' }}</el-tag></div>
</div>
<el-row :gutter="12" class="rpb">
<el-col :span="6"><div class="f"><span class="k">报工工时</span><span>{{ r.workHours }} h</span></div></el-col>
<el-col :span="6"><div class="f"><span class="k">报工前进度</span><span>{{ r.progressBefore }}%</span></div></el-col>
<el-col :span="6"><div class="f"><span class="k">本次进度增量</span><span class="ok">+{{ r.progressIncrement }}%</span></div></el-col>
<el-col :span="6"><div class="f"><span class="k">是否加班</span><span>{{ r.isOvertime?'是':'否' }}</span></div></el-col>
<el-col :span="12"><div class="f"><span class="k">本次工作内容</span><span>{{ r.workContent || '-' }}</span></div></el-col>
<el-col :span="12"><div class="f"><span class="k">剩余工作计划</span><span>{{ r.remainingWork || '-' }}</span></div></el-col>
<el-col :span="12"><div class="f"><span class="k">提交人</span><span>{{ r.createdName || '-' }}</span></div></el-col>
<el-col :span="12"><div class="f"><span class="k">提交时间</span><span>{{ r.createdTime || '-' }}</span></div></el-col>
</el-row>
</div>
</div>
</el-card>
</template>
</div>
<el-dialog title="新增报功" :visible.sync="show" width="580px" :close-on-click-modal="false" @close="reset">
<el-form ref="f" :model="form" :rules="rules" label-width="110px" size="small">
<el-form-item label="报工日期" prop="reportDate"><el-date-picker v-model="form.reportDate" type="date" value-format="yyyy-MM-dd" style="width:100%" /></el-form-item>
<el-form-item label="报工工时(h)" prop="workHours"><el-input-number v-model="form.workHours" :min="0.5" :max="24" :step="0.5" :precision="1" controls-position="right" style="width:100%" /></el-form-item>
<el-form-item label="报工前进度" prop="progressBefore"><el-input-number v-model="form.progressBefore" :min="0" :max="100" disabled controls-position="right" style="width:100%" /><div class="tip">自动取当前任务进度</div></el-form-item>
<el-form-item label="进度增量(%)" prop="progressIncrement"><el-input-number v-model="form.progressIncrement" :min="0" :max="100-form.progressBefore" :step="5" controls-position="right" style="width:100%" /><div class="tip">本次完成后预计总进度{{ form.progressBefore+form.progressIncrement }}%</div></el-form-item>
<el-form-item label="是否加班"><el-radio-group v-model="form.isOvertime"><el-radio :label="0">否</el-radio><el-radio :label="1"></el-radio></el-radio-group></el-form-item>
<el-form-item label="本次工作内容" prop="workContent"><el-input v-model="form.workContent" type="textarea" :rows="3" maxlength="500" show-word-limit /></el-form-item>
<el-form-item label="剩余工作计划"><el-input v-model="form.remainingWork" type="textarea" :rows="3" maxlength="500" show-word-limit /></el-form-item>
</el-form>
<div slot="footer"><el-button size="small" @click="show=false">取消</el-button><el-button type="primary" size="small" :loading="sub" @click="submit">提交报功</el-button></div>
</el-dialog>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { getMyTasks, getReportsByDetail, addReport } from '@/views/project/progressReportApi'
const df = { reportDate: '', workHours: 8, progressBefore: 0, progressIncrement: 0, isOvertime: 0, workContent: '', remainingWork: '' }
export default {
name: 'ProgressReportIndex',
data() {
return {
loading: false,
loadingReports: false,
sub: false,
show: false,
projects: [],
tasks: [],
reports: [],
cp: null,
ct: null,
form: { ...df },
rules: {
reportDate: [{ required: true, message: '请选择报工日期', trigger: 'change' }],
workHours: [{ required: true, message: '请输入报工工时', trigger: 'change' }],
progressIncrement: [{ required: true, message: '请输入本次进度增量', trigger: 'change' }],
workContent: [{ required: true, message: '请填写本次工作内容', trigger: 'blur' }]
}
}
},
computed: {
...mapGetters(['user']),
latest() { return this.reports.length ? this.reports[0] : null },
canAdd() {
if (!this.ct) return false
return this.ct.canCreateReport === true
}
},
created() {
this.loadMyTasks()
},
methods: {
loadMyTasks() {
this.loading = true
getMyTasks().then(res => {
const projects = (res.data && res.data.projects) ? res.data.projects : []
this.projects = projects
}).finally(() => {
this.loading = false
})
},
pickProject(p) {
if (this.cp && this.cp.projectId === p.projectId) return
this.cp = p
this.ct = null
this.reports = []
this.tasks = p.details || []
},
pickTask(t) {
this.ct = t
this.loadReports(t.detailId)
},
loadReports(detailId) {
this.loadingReports = true
getReportsByDetail(detailId).then(res => {
const detail = res.data || {}
const list = Array.isArray(detail.reports) ? detail.reports : []
this.reports = list.sort((a, b) => new Date(b.createdTime) - new Date(a.createdTime))
if (this.ct && this.ct.detailId === detailId) {
this.ct = { ...this.ct, ...detail }
}
const t = this.tasks.find(x => x.detailId === detailId)
if (t) {
Object.assign(t, detail)
}
}).finally(() => {
this.loadingReports = false
})
},
open() {
if (!this.canAdd) {
this.$message.warning(this.ct ? this.ct.canCreateReportReason : '暂无法新增报功')
return
}
this.form = { ...df, reportDate: this.today(), progressBefore: this.ct ? (this.ct.progress || 0) : 0 }
this.show = true
this.$nextTick(() => { this.$refs.f && this.$refs.f.clearValidate() })
},
reset() {
this.form = { ...df }
this.$refs.f && this.$refs.f.clearValidate()
},
submit() {
this.$refs.f.validate(v => {
if (!v) return
const max = 100 - this.form.progressBefore
if (this.form.progressIncrement > max) {
this.$message.warning('进度增量不能超过' + max + '%')
return
}
this.sub = true
addReport({
detailId: this.ct.detailId,
projectId: this.cp.projectId,
reportDate: this.form.reportDate,
workHours: this.form.workHours,
progressBefore: this.form.progressBefore,
progressIncrement: this.form.progressIncrement,
workContent: this.form.workContent,
remainingWork: this.form.remainingWork,
isOvertime: this.form.isOvertime
}).then(() => {
this.$message.success('提交成功')
const newProgress = this.form.progressBefore + this.form.progressIncrement
this.ct.progress = newProgress
const t = this.tasks.find(x => x.detailId === this.ct.detailId)
if (t) {
t.progress = newProgress
t.canCreateReport = false
t.canCreateReportReason = '上次报功尚未审核,不能发起新报功'
}
this.show = false
this.loadReports(this.ct.detailId)
}).finally(() => {
this.sub = false
})
})
},
today() {
const d = new Date()
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return y + '-' + m + '-' + day
},
members(v) { return Array.isArray(v) ? v.join('、') : '-' },
tLabel(v) { return ({ 1: '待开始', 2: '进行中', 3: '已完成', 4: '阻塞' })[v] || '未知' },
priLabel(v) { return ({ 1: '紧急', 2: '普通', 3: '低' })[v] || '-' },
tTag(v) { return ({ 1: 'info', 2: 'warning', 3: 'success', 4: 'danger' })[v] || 'info' },
priTag(v) { return ({ 1: 'danger', 2: 'warning', 3: 'info' })[v] || 'info' }
}
}
</script>
<style scoped>
.page{display:flex;gap:16px;align-items:flex-start;background:#f5f7fa;min-height:calc(100vh - 120px)}
.left{width:320px;flex-shrink:0}
.right{flex:1;min-width:0}
.mt16{margin-top:16px}
.hd,.top,.row,.rph{display:flex;align-items:center}
.hd,.top,.rph{justify-content:space-between}
.sub,.s,.k,.tip{font-size:12px;color:#909399}
.t{font-weight:600;color:#303133}
.item{padding:12px;border:1px solid #ebeef5;border-radius:6px;background:#fff;cursor:pointer;margin-bottom:10px;transition:.2s}
.item:hover,.item.on{border-color:#409eff;background:#ecf5ff}
.tasks{max-height:62vh;overflow:auto}
.ph{min-height:320px}
.empty{color:#c0c4cc;text-align:center;padding:24px 0}
.big{padding:120px 0}
.top{margin-bottom:12px;gap:12px}
.warn{font-size:12px;color:#e6a23c;margin-bottom:10px}
.rp{border:1px solid #ebeef5;border-radius:6px;overflow:hidden;margin-bottom:14px;background:#fafafa}
.rph{padding:12px 16px;background:#f5f7fa;border-bottom:1px solid #ebeef5}
.rpb{padding:12px 16px 2px}
.f{display:flex;flex-direction:column;gap:4px;margin-bottom:12px;word-break:break-all}
.ok{color:#67c23a;font-weight:700}
</style>