Compare commits

...

2 Commits

12 changed files with 575 additions and 32 deletions

View File

@@ -207,6 +207,11 @@ public class SysOaProjectBo extends BaseEntity {
*/
private String keyword;
/**
* 为 true 时在列表结果中附带各项目进度步骤统计(综合看板等,不参与 SQL 条件)
*/
private Boolean scheduleStats;
//是否置顶
private Integer isTop;

View File

@@ -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;
}

View File

@@ -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;
@@ -14,7 +15,6 @@ import com.ruoyi.system.domain.SysOss;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.List;
@@ -285,13 +285,26 @@ public class SysOaProjectVo {
private Long deliveryOrderCount;
// 总览页面统计
/** 列表 SQL 聚合:任务/进度总览 */
@ExcelIgnore
private Long taskFinishCount;
@ExcelIgnore
private Long taskTotalCount;
@ExcelIgnore
private Long scheduleTotalCount;
@ExcelIgnore
private Long scheduleFinishCount;
/** 进度步骤汇总(列表请求 scheduleStats=true 时由服务层填充) */
@ExcelIgnore
private Long scheduleStepTotal;
@ExcelIgnore
private Long scheduleStepCompleted;
@ExcelIgnore
private Long scheduleStepPendingAccept;
}

View File

@@ -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);
}

View File

@@ -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<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) {

View File

@@ -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>

View File

@@ -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)

View 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]
}

View File

@@ -28,7 +28,6 @@
class="project-item"
:class="{ active: String(p.projectId) === String(currentProjectId) }"
@click="handleSelectProject(p)"
:title="p.projectName"
>
<div class="project-main">
<div class="project-name text-ellipsis">{{ p.projectName }}</div>
@@ -41,6 +40,12 @@
<span class="stat-item">任务 {{ p.taskFinishCount || 0 }}/{{ p.taskTotalCount || 0 }}</span>
<span class="stat-item">进度 {{ p.scheduleFinishCount || 0 }}/{{ p.scheduleTotalCount || 0 }}</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,9 +104,7 @@
<vxe-column field="projectCode" title="代号" width="72"></vxe-column>
<vxe-column field="taskTitle" title="任务主题" min-width="96">
<template #default="{ row }">
<el-link type="primary" :underline="false" @click="openTaskById(row.taskId)">
{{ row.taskTitle }}
</el-link>
<span class="dashboard-link" @click.stop="goToTaskCenter(row)">{{ row.taskTitle || '-' }}</span>
</template>
</vxe-column>
<vxe-column field="scheduleProgress" title="对应进度" min-width="112">
@@ -114,7 +117,7 @@
class="schedule-progress-wrap schedule-progress-wrap--linked"
:class="{ 'is-link': scheduleProgressClick(row) }"
:title="scheduleProgressTitle(row)"
@click.stop="goToPaceFromTaskRow(row)"
>
<el-tag :type="scheduleStepTagType(row)" size="mini" class="schedule-progress-status-tag">
{{ scheduleStepStatusLabel(row) }}
@@ -166,7 +169,40 @@
<!-- 进度板块 Tab1 进度明细表默认Tab2 原思维导图 + 图例 -->
<div class="panel schedule-panel">
<el-tabs v-model="scheduleViewTab" class="schedule-panel-tabs">
<div v-if="currentProjectId" class="schedule-board-summary schedule-board-summary--pace-like">
<el-row>
<div>
<el-row>
<el-col :span="12">
<div style="font-size: small;">
<span style="color:#d0d0d0 ">项目名</span>
<span style="color: #409eff;">{{ projectDisplayName }}</span>
</div>
</el-col>
<el-col :span="12">
<div style="font-size: small;">
<span style="color:#d0d0d0 ">项目负责人</span>
<span>{{ projectManagerName }}</span>
</div>
</el-col>
<el-col :span="12">
<div style="font-size: small;">
<span style="color:#d0d0d0 ">当前进度</span>
<span>{{ dashboardScheduleSummary }}</span>
</div>
</el-col>
<el-col :span="12">
<div style="font-size: small;">
<span style="color:#d0d0d0 ">项目状态</span>
<span v-if="projectIsTop" style="color: #ff4d4f;">重点关注</span>
<span v-else>一般项目</span>
</div>
</el-col>
</el-row>
</div>
</el-row>
</div>
<el-tabs v-model="scheduleViewTab" class="schedule-panel-tabs" @tab-click="onScheduleTabClick">
<el-tab-pane label="进度明细" name="list">
<!-- 进度数据表格自然撑开高度 .progress-table-scroll 单独承担纵向/横向滚动 -->
<div class="progress-table-pane" v-loading="xmindLoading">
@@ -185,7 +221,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>
@@ -218,6 +258,7 @@
<el-empty :image-size="80" description="暂无进度步骤数据"></el-empty>
</div>
<xmind
ref="dashboardMindXmind"
v-else-if="scheduleViewTab === 'mind'"
:list="stepList"
height="1200px"
@@ -256,7 +297,7 @@
<script>
import { mapState } from 'vuex'
import Xmind from '@/views/oa/project/pace/components/xmind.vue'
import { resolveOaPaceCenterPath, resolveOaTaskCenterPath } from '@/utils/oaMenuNavigate'
export default {
name: 'OaProjectDashboard2',
components: { Xmind },
@@ -278,8 +319,31 @@ export default {
'projectTotal',
'taskQuery',
'taskList',
'stepList'
'stepList',
'projectDetail'
]),
projectDisplayName () {
const p = this.projectDetail
return (p && p.projectName) ? String(p.projectName) : ''
},
projectManagerName () {
const p = this.projectDetail
return (p && p.functionary) ? String(p.functionary) : ''
},
projectIsTop () {
const p = this.projectDetail
if (!p) return false
const top = p.isTop
return top === true || top === 1 || top === '1'
},
/** 与 pace/step 中 scheduleSummary 文案规则一致 */
dashboardScheduleSummary () {
const list = this.stepList || []
const totalCount = list.length
const completedCount = list.filter((item) => item.status === 2).length
const pendingCount = list.filter((item) => item.status === 1).length
return `已完成(${completedCount}+ 待验收(${pendingCount} / 总节点数(${totalCount}`
},
filteredTaskList () {
const list = this.taskList || []
const code = (this.taskQuery.projectCode || '').trim().toLowerCase()
@@ -317,6 +381,18 @@ export default {
this.getProjectList()
},
methods: {
/** 进度导图 Tab 可见后触发 ECharts 重新测量 */
onScheduleTabClick (tab) {
if (!tab || tab.name !== 'mind') return
this.$nextTick(() => {
requestAnimationFrame(() => {
const comp = this.$refs.dashboardMindXmind
if (comp && typeof comp.scheduleResize === 'function') {
comp.scheduleResize()
}
})
})
},
async getProjectList () {
this.projectLoading = true
try {
@@ -346,6 +422,16 @@ export default {
} finally {
this.pageLoading = false
this.xmindLoading = false
if (this.scheduleViewTab === 'mind') {
this.$nextTick(() => {
requestAnimationFrame(() => {
const comp = this.$refs.dashboardMindXmind
if (comp && typeof comp.scheduleResize === 'function') {
comp.scheduleResize()
}
})
})
}
}
},
@@ -384,6 +470,16 @@ export default {
} finally {
this.pageLoading = false
this.xmindLoading = false
if (this.scheduleViewTab === 'mind') {
this.$nextTick(() => {
requestAnimationFrame(() => {
const comp = this.$refs.dashboardMindXmind
if (comp && typeof comp.scheduleResize === 'function') {
comp.scheduleResize()
}
})
})
}
}
},
@@ -435,6 +531,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_stepuse_flag=1 */
@@ -484,6 +641,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)
@@ -573,15 +738,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;
}
.taskFinishCount{
margin-right:4px;
/* 单行展示:缩小字号 + 不换行,仍过长则省略号(悬停 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;
}
@@ -698,6 +882,13 @@ export default {
padding: 10px;
}
.schedule-board-summary--pace-like {
flex-shrink: 0;
padding: 10px 12px 8px;
margin: -10px -10px 0;
border-bottom: 1px solid #ebeef5;
}
.schedule-panel-tabs {
flex: 1 1 0%;
min-height: 0;
@@ -813,6 +1004,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 {

View File

@@ -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
});
}
}
};

View File

@@ -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 () {

View File

@@ -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,21 +232,58 @@ export default {
}
},
methods: {
tryOpenDetail (trackId) {
if (!trackId) return;
const found = this.scheduleList.find(item => item.scheduleId === trackId || item.projectId === trackId);
if (found) {
this.getScheduleDetail(found);
} else {
this.$nextTick(() => {
const found = this.scheduleList.find(item => item.scheduleId === trackId || item.projectId === trackId);
if (found) {
this.getScheduleDetail(found);
}
});
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()
},
@@ -285,6 +345,7 @@ export default {
this.recentProjects = cache
/* 3. 结束 loading */
this.loading = false
this.maybeOpenScheduleFromRoute()
})
},
getProjectList () {
@@ -305,6 +366,7 @@ export default {
})
},
handleDetail (row) {
this.scheduleStepFocusHint = null;
// 把当前项目放到数组最前面,去重
const list = [row, ...this.recentProjects.filter(p => p.projectId !== row.projectId)];
// 只保留前 2 条