feat: 添加项目进度统计功能,支持在列表中显示各项目的进度步骤统计信息,以及跳转

This commit is contained in:
2026-04-23 12:47:23 +08:00
parent 335dc88a2a
commit db90e2a084
12 changed files with 480 additions and 18 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;
@@ -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;
}

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

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

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

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,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 条