修复综合看板

This commit is contained in:
2026-04-22 18:44:14 +08:00
parent 50527f68e0
commit 602928dc0b
9 changed files with 168 additions and 89 deletions

View File

@@ -284,4 +284,14 @@ public class SysOaProjectVo {
private Long processCardCount; private Long processCardCount;
private Long deliveryOrderCount; private Long deliveryOrderCount;
// 总览页面统计
private Long taskFinishCount;
private Long taskTotalCount;
private Long scheduleTotalCount;
private Long scheduleFinishCount;
} }

View File

@@ -221,6 +221,7 @@ public class SysOaProjectServiceImpl implements ISysOaProjectService {
} }
qw.eq(bo.getCustomerId() != null, "p.customer_id", bo.getCustomerId()); qw.eq(bo.getCustomerId() != null, "p.customer_id", bo.getCustomerId());
qw.orderByDesc("p.is_top").orderByDesc("p.create_time"); qw.orderByDesc("p.is_top").orderByDesc("p.create_time");
qw.groupBy("p.project_id");
return qw; return qw;
} }

View File

@@ -154,6 +154,7 @@ public class SysOaTaskServiceImpl implements ISysOaTaskService {
QueryWrapper<SysOaTask> lqw = Wrappers.query(); QueryWrapper<SysOaTask> lqw = Wrappers.query();
lqw.eq("sot.del_flag", 0); lqw.eq("sot.del_flag", 0);
lqw.eq(bo.getProjectId()!=null, "sot.project_id", bo.getProjectId()); lqw.eq(bo.getProjectId()!=null, "sot.project_id", bo.getProjectId());
lqw.eq(bo.getTaskId()!=null, "sot.task_id", bo.getTaskId());
lqw.eq(bo.getCreateUserId()!=null, "sot.create_user_id", bo.getCreateUserId()); lqw.eq(bo.getCreateUserId()!=null, "sot.create_user_id", bo.getCreateUserId());
lqw.eq(bo.getWorkerId()!=null, "sot.worker_id", bo.getWorkerId()); lqw.eq(bo.getWorkerId()!=null, "sot.worker_id", bo.getWorkerId());
lqw.eq(bo.getProjectId()!=null, "sot.project_id", bo.getProjectId()); lqw.eq(bo.getProjectId()!=null, "sot.project_id", bo.getProjectId());

View File

@@ -310,9 +310,15 @@
TIMESTAMPDIFF(DAY, NOW(), p.postpone_time) AS remainTime, TIMESTAMPDIFF(DAY, NOW(), p.postpone_time) AS remainTime,
p.customer_id AS customerId, p.customer_id AS customerId,
c.name AS customerName, c.name AS customerName,
p.is_top AS isTop p.is_top AS isTop,
COUNT(sot.task_id) AS taskTotalCount ,
SUM(CASE WHEN sot.status = 1 THEN 1 ELSE 0 END) AS taskFinishCount,
COUNT(ops.schedule_id) AS scheduleTotalCount,
SUM(CASE WHEN ops.status = 2 THEN 1 ELSE 0 END) AS scheduleFinishCount
FROM sys_oa_project p FROM sys_oa_project p
LEFT JOIN oa_customer c ON p.customer_id = c.customer_id LEFT JOIN oa_customer c ON p.customer_id = c.customer_id
left join sys_oa_task sot on sot.project_id = p.project_id and sot.del_flag = 0
left join oa_project_schedule ops on ops.project_id = p.project_id and ops.del_flag = 0
${ew.getCustomSqlSegment} ${ew.getCustomSqlSegment}
</select> </select>

View File

@@ -24,7 +24,7 @@ const state = {
projectQuery: { projectQuery: {
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 15,
keyword: '' keyword: ''
}, },
projectList: [], projectList: [],

View File

@@ -4,6 +4,7 @@
--> -->
<template> <template>
<div class="app-container dashboard2" v-loading="pageLoading"> <div class="app-container dashboard2" v-loading="pageLoading">
<div class="dashboard-shell">
<div class="layout"> <div class="layout">
<!-- 左侧 20%项目列表 --> <!-- 左侧 20%项目列表 -->
<div class="left"> <div class="left">
@@ -29,29 +30,26 @@
@click="handleSelectProject(p)" @click="handleSelectProject(p)"
:title="p.projectName" :title="p.projectName"
> >
<div class="project-main">
<div class="project-name text-ellipsis">{{ p.projectName }}</div> <div class="project-name text-ellipsis">{{ p.projectName }}</div>
<div class="project-meta text-ellipsis"> <div class="project-meta text-ellipsis">
<span v-if="p.projectCode" class="code">{{ p.projectCode }}</span> <span v-if="p.projectCode" class="code">{{ p.projectCode }}</span>
<span class="time">{{ formatDate(p.beginTime) }} ~ {{ formatDate(p.finishTime) }}</span> <span class="time">{{ formatDate(p.beginTime) }} ~ {{ formatDate(p.finishTime) }}</span>
</div> </div>
</div> </div>
<div class="project-stats">
<span class="stat-item">任务 {{ p.taskFinishCount || 0 }}/{{ p.taskTotalCount || 0 }}</span>
<span class="stat-item">进度 {{ p.scheduleFinishCount || 0 }}/{{ p.scheduleTotalCount || 0 }}</span>
</div>
</div>
<div v-if="!projectList || projectList.length === 0" class="left-empty"> <div v-if="!projectList || projectList.length === 0" class="left-empty">
<el-empty :image-size="72" description="暂无项目"></el-empty> <el-empty :image-size="72" description="暂无项目"></el-empty>
</div> </div>
</div> </div>
<!-- 分页组件 -->
<div class="left-pager"> <pagination :total="projectTotal" :page.sync="projectQuery.pageNum" :limit.sync="projectQuery.pageSize" layout="prev,pager, next" pagerCount="3"
<el-pagination @pagination="getProjectList" style="margin-top: 10px;" />
small
background
layout="total, prev, pager, next"
:total="projectTotal"
:current-page.sync="projectQuery.pageNum"
:page-size.sync="projectQuery.pageSize"
@current-change="getProjectList"
/>
</div>
</div> </div>
<!-- 右侧 80% --> <!-- 右侧 80% -->
@@ -99,7 +97,13 @@
:row-config="{ isHover: true }" :row-config="{ isHover: true }"
> >
<vxe-column field="projectCode" title="代号" width="72"></vxe-column> <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 }">
<el-link type="primary" :underline="false" @click="openTaskById(row.taskId)">
{{ row.taskTitle }}
</el-link>
</template>
</vxe-column>
<vxe-column field="scheduleProgress" title="对应进度" min-width="112"> <vxe-column field="scheduleProgress" title="对应进度" min-width="112">
<template #default="{ row }"> <template #default="{ row }">
<div v-if="scheduleProgressUnlinked(row)" class="schedule-progress-wrap"> <div v-if="scheduleProgressUnlinked(row)" class="schedule-progress-wrap">
@@ -108,7 +112,9 @@
<div <div
v-else v-else
class="schedule-progress-wrap schedule-progress-wrap--linked" class="schedule-progress-wrap schedule-progress-wrap--linked"
:class="{ 'is-link': scheduleProgressClick(row) }"
:title="scheduleProgressTitle(row)" :title="scheduleProgressTitle(row)"
> >
<el-tag :type="scheduleStepTagType(row)" size="mini" class="schedule-progress-status-tag"> <el-tag :type="scheduleStepTagType(row)" size="mini" class="schedule-progress-status-tag">
{{ scheduleStepStatusLabel(row) }} {{ scheduleStepStatusLabel(row) }}
@@ -172,7 +178,7 @@
<div v-else class="progress-table-scroll"> <div v-else class="progress-table-scroll">
<el-table <el-table
:data="scheduleStepsForTable" :data="scheduleStepsForTable"
size="mini"
border border
stripe stripe
class="progress-step-el-table" class="progress-step-el-table"
@@ -194,6 +200,12 @@
<el-table-column label="负责人" min-width="96" show-overflow-tooltip> <el-table-column label="负责人" min-width="96" show-overflow-tooltip>
<template slot-scope="{ row }">{{ formatStepResponsible(row) }}</template> <template slot-scope="{ row }">{{ formatStepResponsible(row) }}</template>
</el-table-column> </el-table-column>
<el-table-column label="操作">
<template slot-scope="{ row }">
<el-button size="text" @click="openStepById(row.trackId)" >详情 </el-button>
</template>
</el-table-column>
</el-table> </el-table>
</div> </div>
</div> </div>
@@ -208,7 +220,7 @@
<xmind <xmind
v-else-if="scheduleViewTab === 'mind'" v-else-if="scheduleViewTab === 'mind'"
:list="stepList" :list="stepList"
height="100%" height="1200px"
dashboard-mode dashboard-mode
@refresh="onXmindRefresh" @refresh="onXmindRefresh"
/> />
@@ -238,6 +250,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>
<script> <script>
@@ -336,6 +349,17 @@ export default {
} }
}, },
openTaskById (taskId) {
if (!taskId) return
this.$router.push({ path: '/task/task/allocation', query: { taskId } })
},
openStepById (trackId) {
if (!trackId) return
const projectId = this.currentProjectId
this.$router.push({ path: '/step/files', query: { trackId, projectId } })
},
resetTaskPage () { resetTaskPage () {
this.$store.commit('oaProjectDashboard2/SET_TASK_QUERY', { pageNum: 1 }) this.$store.commit('oaProjectDashboard2/SET_TASK_QUERY', { pageNum: 1 })
}, },
@@ -448,6 +472,10 @@ export default {
return `${this.scheduleStepStatusLabel(row)} ${this.scheduleProgressPath(row)}`.trim() return `${this.scheduleStepStatusLabel(row)} ${this.scheduleProgressPath(row)}`.trim()
}, },
scheduleProgressClick (row) {
return !!(row && row.trackId != null && String(row.trackId).trim() !== '')
},
/** oa_project_schedule_step.status与任务表「对应进度」标签一致 */ /** oa_project_schedule_step.status与任务表「对应进度」标签一致 */
stepStatusLabel (row) { stepStatusLabel (row) {
return this.scheduleStepStatusLabel({ scheduleStatus: row && row.status }) return this.scheduleStepStatusLabel({ scheduleStatus: row && row.status })
@@ -551,7 +579,9 @@ export default {
justify-content: center; justify-content: center;
transition: background 0.15s ease; transition: background 0.15s ease;
} }
.taskFinishCount{
margin-right:4px;
}
.project-item:hover { .project-item:hover {
background: #f0f2f5; background: #f0f2f5;
} }
@@ -581,7 +611,9 @@ export default {
margin-right: 8px; margin-right: 8px;
color: #409eff; color: #409eff;
} }
.stat-item{
margin-right:5px;
}
.left-pager { .left-pager {
padding-top: 8px; padding-top: 8px;
margin-top: 4px; margin-top: 4px;
@@ -663,7 +695,7 @@ export default {
min-height: 0; min-height: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0; padding: 10px;
} }
.schedule-panel-tabs { .schedule-panel-tabs {
@@ -726,7 +758,7 @@ export default {
flex: 1 1 0%; flex: 1 1 0%;
min-height: 0; min-height: 0;
width: 100%; width: 100%;
height:300px; height:600px;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -810,7 +842,7 @@ export default {
padding: 0; padding: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 320px; min-height: 1600px;
} }
.xmind-wrap :deep(.xmind-box--dashboard) { .xmind-wrap :deep(.xmind-box--dashboard) {
@@ -818,11 +850,12 @@ export default {
min-height: 0; min-height: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 1600px;
} }
.xmind-wrap :deep(.xmind-container) { .xmind-wrap :deep(.xmind-container) {
flex: 1; flex: 1;
min-height: 300px !important; min-height: 1580px !important;
} }
.mind-legend-aside { .mind-legend-aside {

View File

@@ -90,10 +90,10 @@ export default {
type: Array, type: Array,
default: () => [] default: () => []
}, },
/** 容器高度,综合看板等场景可传 100% 以撑满父级 */ /** 容器高度 */
height: { height: {
type: String, type: String,
default: '800px' default: '4800px'
}, },
/** 综合看板:紧凑、防重叠、三色状态、小圆点 */ /** 综合看板:紧凑、防重叠、三色状态、小圆点 */
dashboardMode: { dashboardMode: {
@@ -106,7 +106,7 @@ export default {
return { return {
width: '100%', width: '100%',
height: this.height, height: this.height,
minHeight: this.dashboardMode ? '300px' : '240px' minHeight: this.dashboardMode ? '1200px' : '240px'
} }
} }
}, },
@@ -332,24 +332,24 @@ export default {
type: 'tree', type: 'tree',
data: [treeData], data: [treeData],
/* 与折线图区域一致:留白、白底在容器上 */ /* 与折线图区域一致:留白、白底在容器上 */
...(dm ? { left: '1%', right: '5%', top: '2%', bottom: '2%' } : {}), ...(dm ? { left: '3%', right: '8%', top: '4%', bottom: '4%' } : {}),
symbol: 'circle', symbol: 'circle',
...(dm ? {} : { symbolSize: 6 }), ...(dm ? {} : { symbolSize: 6 }),
edgeShape: dm ? 'polyline' : 'curve', edgeShape: dm ? 'polyline' : 'curve',
edgeForkPosition: dm ? '74%' : '50%', edgeForkPosition: dm ? '68%' : '50%',
orient: 'LR', orient: 'LR',
initialTreeDepth: 4, initialTreeDepth: 3,
roam: true, roam: true,
scaleLimit: dm ? { min: 0.22, max: 5 } : undefined, expandAndCollapse: false,
scaleLimit: dm ? { min: 0.3, max: 4 } : undefined,
label: { label: {
show: true, show: true,
fontSize: dm ? 11 : 12, fontSize: dm ? 11 : 12,
fontWeight: 400, fontWeight: 400,
position: 'left', position: 'top',
verticalAlign: 'middle', verticalAlign: 'middle',
...(dm ? { align: 'right' } : {}), distance: 6,
distance: 8, overflow: 'break',
overflow: 'none',
lineHeight: dm ? 15 : 14, lineHeight: dm ? 15 : 14,
color: dm ? '#606266' : undefined color: dm ? '#606266' : undefined
}, },
@@ -358,46 +358,59 @@ export default {
*/ */
levels: dm levels: dm
? [ ? [
{
symbolSize: 18,
itemStyle: { borderWidth: 2, borderColor: '#fff' },
label: {
position: 'top',
distance: 8,
fontSize: 13,
fontWeight: 600,
width: 140,
overflow: 'break',
lineHeight: 16,
padding: [4, 8, 4, 8]
}
},
{
symbolSize: 14,
itemStyle: { borderWidth: 1.5, borderColor: '#fff' },
label: {
position: 'top',
distance: 6,
fontSize: 12,
width: 180,
overflow: 'break',
lineHeight: 15,
padding: [3, 6, 3, 6]
}
},
{
symbolSize: 10,
itemStyle: { borderWidth: 1, borderColor: '#fff', shadowBlur: 2, shadowColor: 'rgba(0,0,0,0.1)' },
label: {
position: 'top',
verticalAlign: 'bottom',
distance: 6,
fontSize: 11,
width: 180,
overflow: 'break',
lineHeight: 14,
padding: [3, 6, 3, 6]
}
},
{ {
symbolSize: 6, symbolSize: 6,
itemStyle: { borderWidth: 1, borderColor: '#fff' }, itemStyle: { borderWidth: 1, borderColor: '#fff' },
label: { label: {
position: 'left', position: 'bottom',
distance: 8, verticalAlign: 'top',
fontSize: 11, distance: 4,
width: 118, fontSize: 10,
width: 140,
overflow: 'break', overflow: 'break',
lineHeight: 14, lineHeight: 13,
padding: [2, 6, 2, 6] padding: [2, 4, 2, 4]
}
},
{
symbolSize: 5,
itemStyle: { borderWidth: 1, borderColor: '#fff' },
label: {
position: 'left',
distance: 10,
fontSize: 11,
width: 160,
overflow: 'break',
lineHeight: 14,
padding: [2, 8, 2, 6]
}
},
{
symbolSize: 4,
itemStyle: { borderWidth: 1, borderColor: '#fff', shadowBlur: 2, shadowColor: 'rgba(0,0,0,0.1)' },
label: {
position: 'right',
verticalAlign: 'middle',
align: 'left',
distance: 12,
fontSize: 11,
width: 232,
overflow: 'break',
lineHeight: 15,
padding: [2, 8, 2, 8],
formatter: (p) => this.wrapLabelText(p.name, 17)
} }
} }
] ]
@@ -408,17 +421,17 @@ export default {
{ symbolSize: 3 } { symbolSize: 3 }
], ],
lineStyle: { lineStyle: {
width: dm ? 1 : 1.2, width: 1,
curveness: dm ? 0.1 : 0.3, curveness: 0.5,
color: dm ? '#e4e7ed' : '#ccc' color: '#c0c4cc'
}, },
emphasis: { emphasis: {
focus: 'descendant', focus: 'descendant',
lineStyle: { width: 2, color: '#409eff' }, lineStyle: { width: 2, color: '#409eff' },
itemStyle: dm ? { shadowBlur: 6, shadowColor: 'rgba(64,158,255,0.45)' } : undefined itemStyle: { shadowBlur: 10, shadowColor: 'rgba(64,158,255,0.5)' }
}, },
expandAndCollapse: true,
animationDuration: 280 animationDuration: 300
} }
] ]
}; };

View File

@@ -209,7 +209,20 @@ export default {
} }
}, },
methods: { 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);
}
});
}
},
closeDetailShow (done) { closeDetailShow (done) {
this.getList(); this.getList();
done() done()
@@ -245,14 +258,13 @@ export default {
}, },
getList () { getList () {
this.loading = true this.loading = true
console.log(this.queryParams, this.searchTime)
/* 日期搜索条件 */ /* 日期搜索条件 */
if (this.searchTime && this.searchTime.length) { if (this.searchTime && this.searchTime.length) {
this.queryParams.startTime = this.getDateStr(this.searchTime[0]) this.queryParams.startTime = this.getDateStr(this.searchTime[0])
this.queryParams.endTime = this.getDateStr(this.searchTime[1]) this.queryParams.endTime = this.getDateStr(this.searchTime[1])
} }
this.queryParams.projectId = this.$route.query.projectId?this.$route.query.projectId:null
this.queryParams.trackId = this.$route.query.trackId?this.$route.query.trackId:null
listProjectSchedule(this.queryParams).then(res => { listProjectSchedule(this.queryParams).then(res => {
this.scheduleList = res.rows this.scheduleList = res.rows
this.total = res.total this.total = res.total

View File

@@ -499,6 +499,7 @@ export default {
} }
}, },
methods: { methods: {
// 转换数据为级联选择器格式 // 转换数据为级联选择器格式
convertToCascader (rows) { convertToCascader (rows) {
if (!rows || !rows.length) return []; if (!rows || !rows.length) return [];
@@ -614,13 +615,15 @@ export default {
}, },
/** 查询项目管理列表 */ /** 查询项目管理列表 */
getList () { getList () {
this.loading = true; this.loading = true;
const payload = { const payload = {
...this.queryParams, ...this.queryParams,
taskId:this.$route.query.taskId!=null?this.$route.query.taskId:null,
beginTime: this.queryParams.searchTime[0] ? this.queryParams.searchTime[0] + ' 00:00:00' : undefined, beginTime: this.queryParams.searchTime[0] ? this.queryParams.searchTime[0] + ' 00:00:00' : undefined,
finishTime: this.queryParams.searchTime[1] ? this.queryParams.searchTime[1] + ' 23:59:59' : undefined finishTime: this.queryParams.searchTime[1] ? this.queryParams.searchTime[1] + ' 23:59:59' : undefined
} }
console.log(payload, this.queryParams, 'payload');
listTask(payload).then(response => { listTask(payload).then(response => {
this.taskList = response.rows; this.taskList = response.rows;
this.total = response.total; this.total = response.total;