diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/SysOaProjectBo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/SysOaProjectBo.java index 96a4d06..6572457 100644 --- a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/SysOaProjectBo.java +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/SysOaProjectBo.java @@ -207,6 +207,11 @@ public class SysOaProjectBo extends BaseEntity { */ private String keyword; + /** + * 为 true 时在列表结果中附带各项目进度步骤统计(综合看板等,不参与 SQL 条件) + */ + private Boolean scheduleStats; + //是否置顶 private Integer isTop; diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/dto/ProjectScheduleStepStatsDto.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/dto/ProjectScheduleStepStatsDto.java new file mode 100644 index 0000000..c4b7eb8 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/dto/ProjectScheduleStepStatsDto.java @@ -0,0 +1,18 @@ +package com.ruoyi.oa.domain.dto; + +import lombok.Data; + +/** + * 项目维度进度步骤汇总(综合看板左侧列表等) + */ +@Data +public class ProjectScheduleStepStatsDto { + + private Long projectId; + + private Long totalNodes; + + private Long completedNodes; + + private Long pendingAcceptNodes; +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/SysOaProjectVo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/SysOaProjectVo.java index a075d6f..d44d20f 100644 --- a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/SysOaProjectVo.java +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/SysOaProjectVo.java @@ -5,6 +5,7 @@ import java.util.Date; import com.alibaba.excel.annotation.format.DateTimeFormat; import com.fasterxml.jackson.annotation.JsonFormat; +import com.alibaba.excel.annotation.ExcelIgnore; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import com.ruoyi.common.annotation.ExcelDictFormat; @@ -284,4 +285,14 @@ public class SysOaProjectVo { private Long processCardCount; private Long deliveryOrderCount; + + /** 进度步骤总数(列表请求 scheduleStats=true 时由后端填充) */ + @ExcelIgnore + private Long scheduleStepTotal; + + @ExcelIgnore + private Long scheduleStepCompleted; + + @ExcelIgnore + private Long scheduleStepPendingAccept; } diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaProjectScheduleStepMapper.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaProjectScheduleStepMapper.java index afcb3f9..bc1f1b6 100644 --- a/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaProjectScheduleStepMapper.java +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaProjectScheduleStepMapper.java @@ -3,6 +3,7 @@ package com.ruoyi.oa.mapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.oa.domain.OaProjectScheduleStep; +import com.ruoyi.oa.domain.dto.ProjectScheduleStepStatsDto; import com.ruoyi.oa.domain.vo.OaProjectScheduleStepVo; import com.ruoyi.common.core.mapper.BaseMapperPlus; import org.apache.ibatis.annotations.Param; @@ -44,4 +45,9 @@ public interface OaProjectScheduleStepMapper extends BaseMapperPlus scheduleIds); Page selectVoPageNew(Page build,@Param(Constants.WRAPPER) QueryWrapper lqw); + + /** + * 按项目汇总进度步骤:总数、已完成(2)、待验收(1) + */ + List selectStepStatsGroupByProjectId(@Param("projectIds") Collection projectIds); } diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/SysOaProjectServiceImpl.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/SysOaProjectServiceImpl.java index 44fb56a..872a2a6 100644 --- a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/SysOaProjectServiceImpl.java +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/SysOaProjectServiceImpl.java @@ -16,6 +16,7 @@ import com.ruoyi.oa.domain.bo.OaProjectScheduleBo; import com.ruoyi.oa.domain.bo.SysOaWarehouseDetailBo; import com.ruoyi.oa.domain.dto.ProjectActivityDTO; import com.ruoyi.oa.domain.dto.ProjectDataDTO; +import com.ruoyi.oa.domain.dto.ProjectScheduleStepStatsDto; import com.ruoyi.oa.domain.vo.*; import com.ruoyi.oa.service.CodeGeneratorService; import com.ruoyi.oa.service.IExchangeRateService; @@ -27,12 +28,14 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.ruoyi.oa.domain.bo.SysOaProjectBo; import com.ruoyi.oa.domain.SysOaProject; +import com.ruoyi.oa.mapper.OaProjectScheduleStepMapper; import com.ruoyi.oa.mapper.SysOaProjectMapper; import com.ruoyi.oa.service.ISysOaProjectService; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.*; +import java.util.function.Function; import java.util.stream.Collectors; /** @@ -53,6 +56,8 @@ public class SysOaProjectServiceImpl implements ISysOaProjectService { private final IOaProjectScheduleStepService oaProjectScheduleStepService; + private final OaProjectScheduleStepMapper oaProjectScheduleStepMapper; + @Autowired private CodeGeneratorService codeGeneratorService; @Autowired @@ -191,7 +196,32 @@ public class SysOaProjectServiceImpl implements ISysOaProjectService { vo.setFundsRmb(vo.getFunds()); } } - + + if (Boolean.TRUE.equals(bo.getScheduleStats()) && result.getRecords() != null && !result.getRecords().isEmpty()) { + List projectIds = result.getRecords().stream() + .map(SysOaProjectVo::getProjectId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + if (!projectIds.isEmpty()) { + List statRows = oaProjectScheduleStepMapper.selectStepStatsGroupByProjectId(projectIds); + Map statMap = statRows.stream() + .collect(Collectors.toMap(ProjectScheduleStepStatsDto::getProjectId, Function.identity(), (a, b) -> a)); + for (SysOaProjectVo vo : result.getRecords()) { + ProjectScheduleStepStatsDto s = statMap.get(vo.getProjectId()); + if (s != null) { + vo.setScheduleStepTotal(s.getTotalNodes()); + vo.setScheduleStepCompleted(s.getCompletedNodes()); + vo.setScheduleStepPendingAccept(s.getPendingAcceptNodes()); + } else { + vo.setScheduleStepTotal(0L); + vo.setScheduleStepCompleted(0L); + vo.setScheduleStepPendingAccept(0L); + } + } + } + } + return TableDataInfo.build(result); } private QueryWrapper buildAliasPQueryWrapper(SysOaProjectBo bo) { diff --git a/ruoyi-oa/src/main/resources/mapper/oa/OaProjectScheduleStepMapper.xml b/ruoyi-oa/src/main/resources/mapper/oa/OaProjectScheduleStepMapper.xml index de75a8f..56c117f 100644 --- a/ruoyi-oa/src/main/resources/mapper/oa/OaProjectScheduleStepMapper.xml +++ b/ruoyi-oa/src/main/resources/mapper/oa/OaProjectScheduleStepMapper.xml @@ -282,5 +282,22 @@ WHERE schedule_id = #{scheduleId} + diff --git a/ruoyi-ui/src/store/modules/oaProjectDashboard2.js b/ruoyi-ui/src/store/modules/oaProjectDashboard2.js index 3ea5955..6db4e82 100644 --- a/ruoyi-ui/src/store/modules/oaProjectDashboard2.js +++ b/ruoyi-ui/src/store/modules/oaProjectDashboard2.js @@ -87,7 +87,8 @@ const actions = { const query = { pageNum: merged.pageNum, pageSize: merged.pageSize, - keyword: (merged.keyword != null ? String(merged.keyword) : '').trim() + keyword: (merged.keyword != null ? String(merged.keyword) : '').trim(), + scheduleStats: true } commit('SET_PROJECT_QUERY', query) commit('SET_LOADING', true) diff --git a/ruoyi-ui/src/utils/oaMenuNavigate.js b/ruoyi-ui/src/utils/oaMenuNavigate.js new file mode 100644 index 0000000..da0564f --- /dev/null +++ b/ruoyi-ui/src/utils/oaMenuNavigate.js @@ -0,0 +1,139 @@ +import store from '@/store' + +function isHttpUrl (path) { + return path && /^(https?:|mailto:|tel:)/.test(path) +} + +function joinPaths (parentPath, segment) { + if (segment == null || segment === '') { + return parentPath || '/' + } + if (isHttpUrl(segment)) { + return segment + } + if (segment.startsWith('/')) { + return segment.replace(/\/+/g, '/') + } + const base = (parentPath || '').replace(/\/+$/, '') + const rel = segment.replace(/^\//, '') + if (!base) { + return '/' + rel + } + return (base + '/' + rel).replace(/\/+/g, '/') +} + +/** + * 遍历侧边栏路由树,得到所有带 meta.title 的叶子及其完整 path(与菜单渲染路径一致) + */ +export function flattenSidebarLeaves (routes, parentPath = '') { + const out = [] + if (!routes || !routes.length) { + return out + } + for (const r of routes) { + if (!r || r.hidden) { + continue + } + const current = joinPaths(parentPath, r.path) + if (r.children && r.children.length > 0) { + out.push(...flattenSidebarLeaves(r.children, current)) + } else if (r.meta && r.meta.title) { + out.push({ fullPath: current, title: r.meta.title, name: r.name }) + } + } + return out +} + +/** + * 按菜单名称(与 sys_menu.menu_name 一致)查找已注册的前端 path + */ +export function findMenuFullPathByTitles (titles) { + const set = new Set((titles || []).filter(Boolean)) + if (!set.size) { + return null + } + const routes = store.getters.sidebarRouters || [] + const leaves = flattenSidebarLeaves(routes) + const hit = leaves.find((l) => set.has(l.title)) + return hit ? hit.fullPath : null +} + +/** + * 将当前路由最后一级替换为另一段,用于「综合看板」与「任务」「进度」在同一父菜单下的场景 + */ +export function siblingPathReplaceLast (currentPath, newLastSegment) { + if (!currentPath || !newLastSegment) { + return null + } + const normalized = String(currentPath).replace(/\/+$/, '') + const idx = normalized.lastIndexOf('/') + if (idx < 0) { + return '/' + newLastSegment + } + return `${normalized.slice(0, idx)}/${newLastSegment}`.replace(/\/+/g, '/') +} + +/** + * 在候选 path 中选第一个 resolve 后不是 404 页的地址 + */ +export function pickExistingRoutePath (router, candidates) { + for (const p of candidates) { + if (!p) { + continue + } + try { + const { route } = router.resolve({ path: p }) + if (!route || !route.matched || route.matched.length === 0) { + continue + } + if (route.path === '/404' || (route.fullPath && route.fullPath.includes('/404'))) { + continue + } + return p + } catch (e) { + continue + } + } + return null +} + +function findLeafPathMatchingPath (predicate) { + const routes = store.getters.sidebarRouters || [] + const leaves = flattenSidebarLeaves(routes) + const hit = leaves.find((l) => predicate(l.fullPath)) + return hit ? hit.fullPath : null +} + +/** + * 解析「我的任务」对应前端 path(勿硬编码父级目录,避免项目中心与项目管理 path 不一致导致 404) + */ +export function resolveOaTaskCenterPath (vm) { + const fromMenuTitle = findMenuFullPathByTitles(['我的任务', '任务管理']) + const fromPathEndsTask = findLeafPathMatchingPath((p) => /\/task(\/|$)/i.test(p)) + const sib = siblingPathReplaceLast(vm.$route.path, 'task') + const candidates = [fromMenuTitle, fromPathEndsTask, sib, '/project/task'].filter(Boolean) + return pickExistingRoutePath(vm.$router, candidates) || candidates[candidates.length - 1] +} + +const PACE_MENU_TITLES = [ + '项目进度', + '进度管理', + '进度跟踪', + '绑定进度', + '进度中心' +] + +/** + * 解析进度中心(pace 列表+抽屉)页面前端 path + */ +export function resolveOaPaceCenterPath (vm) { + const fromMenu = findMenuFullPathByTitles(PACE_MENU_TITLES) + const fromPathEndsPace = findLeafPathMatchingPath((p) => /\/pace$/i.test(p)) + const base = vm.$route.path + const siblingSegs = ['pace', 'schedule', 'projectSchedule', 'project-schedule'] + const fromSiblings = siblingSegs + .map((s) => siblingPathReplaceLast(base, s)) + .filter(Boolean) + const candidates = [fromMenu, fromPathEndsPace, ...fromSiblings, '/project/pace'].filter(Boolean) + return pickExistingRoutePath(vm.$router, candidates) || candidates[candidates.length - 1] +} diff --git a/ruoyi-ui/src/views/oa/project/dashboard2/index.vue b/ruoyi-ui/src/views/oa/project/dashboard2/index.vue index 04a02e2..fecaa00 100644 --- a/ruoyi-ui/src/views/oa/project/dashboard2/index.vue +++ b/ruoyi-ui/src/views/oa/project/dashboard2/index.vue @@ -27,13 +27,18 @@ class="project-item" :class="{ active: String(p.projectId) === String(currentProjectId) }" @click="handleSelectProject(p)" - :title="p.projectName" >
{{ p.projectName }}
{{ p.projectCode }} {{ formatDate(p.beginTime) }} ~ {{ formatDate(p.finishTime) }}
+
+ 当前进度:{{ projectListScheduleLine(p) }} +
@@ -99,7 +104,11 @@ :row-config="{ isHover: true }" > - + + +