feat: 添加项目进度统计功能,支持在列表中显示各项目的进度步骤统计信息,以及跳转
This commit is contained in:
@@ -207,6 +207,11 @@ public class SysOaProjectBo extends BaseEntity {
|
||||
*/
|
||||
private String keyword;
|
||||
|
||||
/**
|
||||
* 为 true 时在列表结果中附带各项目进度步骤统计(综合看板等,不参与 SQL 条件)
|
||||
*/
|
||||
private Boolean scheduleStats;
|
||||
|
||||
//是否置顶
|
||||
private Integer isTop;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<OaProjectSch
|
||||
int deleteByScheduleIds(@Param("scheduleIds") Collection<Long> scheduleIds);
|
||||
|
||||
Page<OaProjectScheduleStepVo> selectVoPageNew(Page<Object> build,@Param(Constants.WRAPPER) QueryWrapper<OaProjectScheduleStep> lqw);
|
||||
|
||||
/**
|
||||
* 按项目汇总进度步骤:总数、已完成(2)、待验收(1)
|
||||
*/
|
||||
List<ProjectScheduleStepStatsDto> selectStepStatsGroupByProjectId(@Param("projectIds") Collection<Long> projectIds);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -192,6 +197,31 @@ public class SysOaProjectServiceImpl implements ISysOaProjectService {
|
||||
}
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(bo.getScheduleStats()) && result.getRecords() != null && !result.getRecords().isEmpty()) {
|
||||
List<Long> projectIds = result.getRecords().stream()
|
||||
.map(SysOaProjectVo::getProjectId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (!projectIds.isEmpty()) {
|
||||
List<ProjectScheduleStepStatsDto> statRows = oaProjectScheduleStepMapper.selectStepStatsGroupByProjectId(projectIds);
|
||||
Map<Long, ProjectScheduleStepStatsDto> 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<SysOaProject> buildAliasPQueryWrapper(SysOaProjectBo bo) {
|
||||
|
||||
@@ -282,5 +282,22 @@
|
||||
WHERE schedule_id = #{scheduleId}
|
||||
</select>
|
||||
|
||||
<select id="selectStepStatsGroupByProjectId"
|
||||
resultType="com.ruoyi.oa.domain.dto.ProjectScheduleStepStatsDto">
|
||||
SELECT
|
||||
sch.project_id AS projectId,
|
||||
COUNT(step.track_id) AS totalNodes,
|
||||
IFNULL(SUM(CASE WHEN step.status = 2 THEN 1 ELSE 0 END), 0) AS completedNodes,
|
||||
IFNULL(SUM(CASE WHEN step.status = 1 THEN 1 ELSE 0 END), 0) AS pendingAcceptNodes
|
||||
FROM oa_project_schedule sch
|
||||
INNER JOIN oa_project_schedule_step step ON step.schedule_id = sch.schedule_id
|
||||
WHERE sch.del_flag = '0'
|
||||
AND step.del_flag = '0'
|
||||
AND sch.project_id IN
|
||||
<foreach collection="projectIds" item="pid" open="(" separator="," close=")">
|
||||
#{pid}
|
||||
</foreach>
|
||||
GROUP BY sch.project_id
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -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)
|
||||
|
||||
139
ruoyi-ui/src/utils/oaMenuNavigate.js
Normal file
139
ruoyi-ui/src/utils/oaMenuNavigate.js
Normal file
@@ -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]
|
||||
}
|
||||
@@ -27,13 +27,18 @@
|
||||
class="project-item"
|
||||
:class="{ active: String(p.projectId) === String(currentProjectId) }"
|
||||
@click="handleSelectProject(p)"
|
||||
:title="p.projectName"
|
||||
>
|
||||
<div class="project-name text-ellipsis">{{ p.projectName }}</div>
|
||||
<div class="project-meta text-ellipsis">
|
||||
<span v-if="p.projectCode" class="code">{{ p.projectCode }}</span>
|
||||
<span class="time">{{ formatDate(p.beginTime) }} ~ {{ formatDate(p.finishTime) }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="project-progress-row"
|
||||
:title="'当前进度:' + projectListScheduleLine(p)"
|
||||
>
|
||||
<span class="project-progress-label">当前进度:</span><span class="project-progress-value">{{ projectListScheduleLine(p) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!projectList || projectList.length === 0" class="left-empty">
|
||||
@@ -99,7 +104,11 @@
|
||||
:row-config="{ isHover: true }"
|
||||
>
|
||||
<vxe-column field="projectCode" title="代号" width="72"></vxe-column>
|
||||
<vxe-column field="taskTitle" title="任务主题" min-width="96"></vxe-column>
|
||||
<vxe-column field="taskTitle" title="任务主题" min-width="96">
|
||||
<template #default="{ row }">
|
||||
<span class="dashboard-link" @click.stop="goToTaskCenter(row)">{{ row.taskTitle || '-' }}</span>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="scheduleProgress" title="对应进度" min-width="112">
|
||||
<template #default="{ row }">
|
||||
<div v-if="scheduleProgressUnlinked(row)" class="schedule-progress-wrap">
|
||||
@@ -109,6 +118,7 @@
|
||||
v-else
|
||||
class="schedule-progress-wrap schedule-progress-wrap--linked"
|
||||
:title="scheduleProgressTitle(row)"
|
||||
@click.stop="goToPaceFromTaskRow(row)"
|
||||
>
|
||||
<el-tag :type="scheduleStepTagType(row)" size="mini" class="schedule-progress-status-tag">
|
||||
{{ scheduleStepStatusLabel(row) }}
|
||||
@@ -167,12 +177,7 @@
|
||||
<el-col :span="12">
|
||||
<div style="font-size: small;">
|
||||
<span style="color:#d0d0d0 ">项目名:</span>
|
||||
<el-popover placement="bottom" trigger="hover" width="800">
|
||||
<template slot="reference">
|
||||
<span style="color: #409eff;">{{ projectDisplayName }}</span>
|
||||
</template>
|
||||
<ProjectInfo :info="projectDetail || {}" />
|
||||
</el-popover>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
@@ -217,7 +222,11 @@
|
||||
>
|
||||
<el-table-column prop="firstLevelNode" label="一级节点" min-width="112" show-overflow-tooltip />
|
||||
<el-table-column prop="secondLevelNode" label="二级节点" min-width="112" show-overflow-tooltip />
|
||||
<el-table-column prop="stepName" label="步骤名称" min-width="128" show-overflow-tooltip />
|
||||
<el-table-column prop="stepName" label="步骤名称" min-width="128" show-overflow-tooltip>
|
||||
<template slot-scope="{ row }">
|
||||
<el-button type="text" size="mini" @click="goToPaceFromStepRow(row)">{{ row.stepName || '-' }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="92" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag :type="stepStatusTagType(row)" size="mini">{{ stepStatusLabel(row) }}</el-tag>
|
||||
@@ -282,11 +291,10 @@
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import Xmind from '@/views/oa/project/pace/components/xmind.vue'
|
||||
import ProjectInfo from '@/components/fad-service/ProjectInfo/index.vue'
|
||||
|
||||
import { resolveOaPaceCenterPath, resolveOaTaskCenterPath } from '@/utils/oaMenuNavigate'
|
||||
export default {
|
||||
name: 'OaProjectDashboard2',
|
||||
components: { Xmind, ProjectInfo },
|
||||
components: { Xmind },
|
||||
dicts: ['sys_work_type', 'sys_sort_grade'],
|
||||
data () {
|
||||
return {
|
||||
@@ -497,6 +505,67 @@ export default {
|
||||
await this.refreshCurrent()
|
||||
},
|
||||
|
||||
goToTaskCenter (row) {
|
||||
if (!row || row.taskId == null || row.taskId === '') {
|
||||
return
|
||||
}
|
||||
const path = resolveOaTaskCenterPath(this)
|
||||
this.$router.push({ path, query: { taskId: String(row.taskId) } })
|
||||
},
|
||||
|
||||
resolveScheduleIdByTaskTrack (trackId) {
|
||||
if (trackId == null || trackId === '') {
|
||||
return null
|
||||
}
|
||||
const hit = (this.stepList || []).find((s) => String(s.trackId) === String(trackId))
|
||||
return hit && hit.scheduleId != null ? hit.scheduleId : null
|
||||
},
|
||||
|
||||
goToPaceFromTaskRow (row) {
|
||||
if (this.scheduleProgressUnlinked(row)) {
|
||||
this.$message.warning('该任务未关联进度')
|
||||
return
|
||||
}
|
||||
const scheduleId = this.resolveScheduleIdByTaskTrack(row.trackId)
|
||||
if (!scheduleId || !this.currentProjectId) {
|
||||
this.$message.warning('未找到对应进度主表')
|
||||
return
|
||||
}
|
||||
const path = resolveOaPaceCenterPath(this)
|
||||
this.$router.push({
|
||||
path,
|
||||
query: {
|
||||
projectId: String(this.currentProjectId),
|
||||
scheduleId: String(scheduleId),
|
||||
trackId: String(row.trackId),
|
||||
tabNode: row.tabNode != null ? String(row.tabNode) : '',
|
||||
firstLevelNode: row.firstLevelNode != null ? String(row.firstLevelNode) : ''
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
goToPaceFromStepRow (row) {
|
||||
if (!row || row.scheduleId == null || row.scheduleId === '' || !this.currentProjectId) {
|
||||
this.$message.warning('缺少进度或项目信息')
|
||||
return
|
||||
}
|
||||
const query = {
|
||||
projectId: String(this.currentProjectId),
|
||||
scheduleId: String(row.scheduleId)
|
||||
}
|
||||
if (row.trackId != null && row.trackId !== '') {
|
||||
query.trackId = String(row.trackId)
|
||||
}
|
||||
if (row.tabNode) {
|
||||
query.tabNode = String(row.tabNode)
|
||||
}
|
||||
if (row.firstLevelNode) {
|
||||
query.firstLevelNode = String(row.firstLevelNode)
|
||||
}
|
||||
const path = resolveOaPaceCenterPath(this)
|
||||
this.$router.push({ path, query })
|
||||
},
|
||||
|
||||
noop () {},
|
||||
|
||||
/** 无 track_id 或无步骤名称 → 未关联(后端联表 oa_project_schedule_step,use_flag=1) */
|
||||
@@ -542,6 +611,14 @@ export default {
|
||||
return this.scheduleStepTagType({ scheduleStatus: row && row.status })
|
||||
},
|
||||
|
||||
/** 左侧列表:与进度中心一致的「当前进度」文案(数据来自 list scheduleStats) */
|
||||
projectListScheduleLine (p) {
|
||||
const total = Number(p && p.scheduleStepTotal != null ? p.scheduleStepTotal : 0)
|
||||
const done = Number(p && p.scheduleStepCompleted != null ? p.scheduleStepCompleted : 0)
|
||||
const pend = Number(p && p.scheduleStepPendingAccept != null ? p.scheduleStepPendingAccept : 0)
|
||||
return `已完成(${done})+ 待验收(${pend}) / 总节点数(${total})`
|
||||
},
|
||||
|
||||
formatDate (val) {
|
||||
if (!val) return '-'
|
||||
const s = String(val)
|
||||
@@ -631,13 +708,34 @@ export default {
|
||||
border-bottom: 1px solid #eef0f3;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 2px;
|
||||
min-height: 42px;
|
||||
min-height: 52px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
/* 单行展示:缩小字号 + 不换行,仍过长则省略号(悬停 title 看全文) */
|
||||
.project-progress-row {
|
||||
margin-top: 3px;
|
||||
min-width: 0;
|
||||
font-size: 10px;
|
||||
line-height: 1.35;
|
||||
letter-spacing: -0.02em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.project-progress-label {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.project-progress-value {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.project-item:hover {
|
||||
background: #f0f2f5;
|
||||
}
|
||||
@@ -883,6 +981,16 @@ export default {
|
||||
|
||||
.schedule-progress-wrap--linked {
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dashboard-link {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dashboard-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.schedule-progress-status-tag {
|
||||
|
||||
@@ -87,6 +87,15 @@ export default {
|
||||
tabNode: this.defaultTabNode,
|
||||
firstLevelNode: this.defaultFirstLevelNode
|
||||
});
|
||||
},
|
||||
/** 外部(路由深链等)同步选中进度类别与一级分类,并通知父级更新筛选 */
|
||||
setSelection (tabNode, firstLevelNode) {
|
||||
this.defaultTabNode = tabNode != null ? String(tabNode) : "";
|
||||
this.defaultFirstLevelNode = firstLevelNode != null ? String(firstLevelNode) : "";
|
||||
this.$emit("change", {
|
||||
tabNode: this.defaultTabNode,
|
||||
firstLevelNode: this.defaultFirstLevelNode
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -107,6 +107,11 @@ export default {
|
||||
type: Boolean | Number,
|
||||
default: false
|
||||
},
|
||||
/** 打开进度详情时由路由传入:定位左侧分类与表格筛选(tabNode / firstLevelNode / trackId) */
|
||||
initialStepFocus: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
components: {
|
||||
StepTable,
|
||||
@@ -289,8 +294,44 @@ export default {
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
initialStepFocus: {
|
||||
handler () {
|
||||
this.$nextTick(() => this.applyInitialStepFocus());
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
applyInitialStepFocus () {
|
||||
const hint = this.initialStepFocus;
|
||||
if (!hint || !this.projectScheduleStepList.length) {
|
||||
return;
|
||||
}
|
||||
let tabNode = hint.tabNode != null ? String(hint.tabNode) : "";
|
||||
let firstLevelNode = hint.firstLevelNode != null ? String(hint.firstLevelNode) : "";
|
||||
if ((!tabNode || !firstLevelNode) && hint.trackId != null && hint.trackId !== "") {
|
||||
const st = this.projectScheduleStepList.find(
|
||||
(s) => String(s.trackId) === String(hint.trackId)
|
||||
);
|
||||
if (st) {
|
||||
tabNode = st.tabNode != null ? String(st.tabNode) : tabNode;
|
||||
firstLevelNode = st.firstLevelNode != null ? String(st.firstLevelNode) : firstLevelNode;
|
||||
}
|
||||
}
|
||||
if (!tabNode) {
|
||||
return;
|
||||
}
|
||||
this.viewMode = "table";
|
||||
this.$nextTick(() => {
|
||||
const menu = this.$refs.menuSelectRef;
|
||||
if (menu && typeof menu.setSelection === "function") {
|
||||
menu.setSelection(tabNode, firstLevelNode);
|
||||
} else {
|
||||
this.defaultTabNode = tabNode;
|
||||
this.defaultFirstLevelNode = firstLevelNode;
|
||||
}
|
||||
});
|
||||
},
|
||||
/** 查询项目进度步骤跟踪列表 */
|
||||
getList () {
|
||||
this.loading = true;
|
||||
@@ -298,6 +339,7 @@ export default {
|
||||
this.projectScheduleStepList = response.rows;
|
||||
this.total = response.total;
|
||||
this.loading = false;
|
||||
this.$nextTick(() => this.applyInitialStepFocus());
|
||||
});
|
||||
},
|
||||
handleOverview () {
|
||||
|
||||
@@ -140,7 +140,8 @@
|
||||
<div style="padding:0 20px">
|
||||
<project-schedule-step :scheduleId="scheduleDetail.scheduleId" :master="scheduleDetail.functionary"
|
||||
:projectName="scheduleDetail.projectName" :projectStatus="scheduleDetail.projectStatus"
|
||||
:isTop="scheduleDetail.isTop" :projectId="scheduleDetail.projectId" />
|
||||
:isTop="scheduleDetail.isTop" :projectId="scheduleDetail.projectId"
|
||||
:initial-step-focus="scheduleStepFocusHint" />
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
@@ -153,7 +154,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import { listProject } from "@/api/oa/project";
|
||||
import { addByProjectId, delProjectSchedule, listProjectSchedule, updateProjectSchedule } from "@/api/oa/projectSchedule";
|
||||
import { addByProjectId, delProjectSchedule, getProjectSchedule, listProjectSchedule, updateProjectSchedule } from "@/api/oa/projectSchedule";
|
||||
import { listUser } from "@/api/system/user";
|
||||
import ProjectSelect from "@/components/fad-service/ProjectSelect/index.vue";
|
||||
import UserSelect from "@/components/UserSelect/index.vue";
|
||||
@@ -194,12 +195,34 @@ export default {
|
||||
recentProjects: [],
|
||||
scheduleDetail: {},
|
||||
userList: [],
|
||||
postponeDrawer: false
|
||||
postponeDrawer: false,
|
||||
/** 综合看板等深链:打开抽屉后传给 step,用于选中进度类别/一级节点 */
|
||||
scheduleStepFocusHint: null
|
||||
};
|
||||
|
||||
},
|
||||
watch: {
|
||||
'$route.query': {
|
||||
handler (newQ, oldQ) {
|
||||
this.applyPaceRouteQueryBeforeFetch();
|
||||
const n = newQ || {};
|
||||
const o = oldQ || {};
|
||||
if (n.scheduleId != null && n.scheduleId !== '') {
|
||||
this.handleQuery();
|
||||
return;
|
||||
}
|
||||
const np = n.projectId != null ? String(n.projectId) : '';
|
||||
const op = o.projectId != null ? String(o.projectId) : '';
|
||||
if (np !== '' && np !== op) {
|
||||
this.handleQuery();
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.currentUser = this.$store.state.user
|
||||
this.applyPaceRouteQueryBeforeFetch();
|
||||
this.getList();
|
||||
this.getProjectList();
|
||||
this.getAllUser();
|
||||
@@ -209,8 +232,59 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
applyPaceRouteQueryBeforeFetch () {
|
||||
const q = this.$route.query || {};
|
||||
if (q.projectId != null && q.projectId !== '') {
|
||||
this.queryParams.projectId = q.projectId;
|
||||
}
|
||||
},
|
||||
clearPaceDeepLinkQuery () {
|
||||
const q = { ...(this.$route.query || {}) };
|
||||
delete q.scheduleId;
|
||||
delete q.trackId;
|
||||
delete q.tabNode;
|
||||
delete q.firstLevelNode;
|
||||
if (Object.keys(q).length) {
|
||||
this.$router.replace({ path: this.$route.path, query: q });
|
||||
} else {
|
||||
this.$router.replace({ path: this.$route.path });
|
||||
}
|
||||
},
|
||||
async maybeOpenScheduleFromRoute () {
|
||||
const q = this.$route.query || {};
|
||||
if (!q.scheduleId) {
|
||||
return;
|
||||
}
|
||||
let row = this.scheduleList.find((s) => String(s.scheduleId) === String(q.scheduleId));
|
||||
if (!row) {
|
||||
try {
|
||||
const res = await getProjectSchedule(q.scheduleId);
|
||||
row = res.data;
|
||||
} catch (e) {
|
||||
this.$modal.msgError('未找到该进度或无权访问');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
const hasFocus =
|
||||
(q.trackId != null && q.trackId !== '') ||
|
||||
(q.tabNode != null && q.tabNode !== '') ||
|
||||
(q.firstLevelNode != null && q.firstLevelNode !== '');
|
||||
this.scheduleStepFocusHint = hasFocus
|
||||
? {
|
||||
trackId: q.trackId != null ? String(q.trackId) : '',
|
||||
tabNode: q.tabNode != null ? String(q.tabNode) : '',
|
||||
firstLevelNode: q.firstLevelNode != null ? String(q.firstLevelNode) : ''
|
||||
}
|
||||
: null;
|
||||
this.getScheduleDetail(row);
|
||||
this.$nextTick(() => this.clearPaceDeepLinkQuery());
|
||||
},
|
||||
// 关闭细节窗口
|
||||
closeDetailShow (done) {
|
||||
this.scheduleStepFocusHint = null;
|
||||
this.getList();
|
||||
done()
|
||||
},
|
||||
@@ -273,6 +347,7 @@ export default {
|
||||
this.recentProjects = cache
|
||||
/* 3. 结束 loading */
|
||||
this.loading = false
|
||||
this.maybeOpenScheduleFromRoute()
|
||||
})
|
||||
},
|
||||
getProjectList () {
|
||||
@@ -293,6 +368,7 @@ export default {
|
||||
})
|
||||
},
|
||||
handleDetail (row) {
|
||||
this.scheduleStepFocusHint = null;
|
||||
// 把当前项目放到数组最前面,去重
|
||||
const list = [row, ...this.recentProjects.filter(p => p.projectId !== row.projectId)];
|
||||
// 只保留前 2 条
|
||||
|
||||
Reference in New Issue
Block a user