feat(erp): 优化项目计划变更展示与新增变更控制

- 调整项目计划编辑页变更列表显示,变更新旧逻辑更清晰
- 变更时间截取日期部分,统一改为显示年月日格式
- 变更字段列宽和样式优化,使用浅灰色显示无数据占位
- 变更列表在查看模式或审批完成状态均加载,提升数据一致性
- 甘特图增加项目计划变更支持,显示变更前后时间对比
- 甘特图任务列表支持变更信息标记及优先显示变更后时间
- 项目变更列表中仅在最新且已完成变更记录显示“新增变更”按钮
- 变更列表查询处理新增缓存最新完成变更次数,控制按钮显隐
dev
zangch@mesnac.com 4 weeks ago
parent 2d7d7a4225
commit e46811163a

@ -165,8 +165,8 @@
<el-tooltip v-if="scope.row.projectChangeStatus === '1'" content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['oa/erp:erpProjectChange:edit']"></el-button>
</el-tooltip>
<!-- 新增变更按钮完成状态显示 -->
<el-tooltip v-if="scope.row.projectChangeStatus === '3'" content="新增变更" placement="top">
<!-- 新增变更按钮仅在该项目最新且已完成的变更记录上显示 -->
<el-tooltip v-if="isLatestCompletedChange(scope.row)" content="新增变更" placement="top">
<el-button link type="success" icon="Plus" @click="handleAddChange(scope.row)" v-hasPermi="['oa/erp:erpProjectChange:add']"></el-button>
</el-tooltip>
<!-- 审批记录按钮审批中或完成状态显示 -->
@ -204,7 +204,10 @@ const { active_flag, change_type, project_change_status, project_category, wf_bu
proxy?.useDict('active_flag', 'change_type', 'project_change_status', 'project_category', 'wf_business_status')
);
//
const erpProjectChangeList = ref<ErpProjectChangeVO[]>([]);
//
const latestCompletedChangeMap = ref<Record<string, number>>({});
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
@ -248,39 +251,65 @@ const columns = ref<FieldOption[]>([
{ key: 29, label: `更新时间`, visible: false }
]);
const queryParams = reactive<ErpProjectChangeQuery>({
pageNum: 1,
pageSize: 10,
projectId: undefined,
projectCode: undefined,
projectName: undefined,
projectCategory: undefined,
changeType: undefined,
changeNumber: undefined,
projectManagerId: undefined,
projectManagerName: undefined,
deptHeadId: undefined,
deptHeadName: undefined,
responsibleVpId: undefined,
responsibleVpName: undefined,
applyChangeDate: undefined,
contractAmount: undefined,
contractNetAmount: undefined,
currentStatus: undefined,
changeReason: undefined,
followUpWork: undefined,
projectChangeStatus: undefined,
flowStatus: undefined,
activeFlag: undefined,
params: {}
// +
const data = reactive<{ queryParams: ErpProjectChangeQuery }>({
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: undefined,
projectCode: undefined,
projectName: undefined,
projectCategory: undefined,
changeType: undefined,
changeNumber: undefined,
projectManagerId: undefined,
projectManagerName: undefined,
deptHeadId: undefined,
deptHeadName: undefined,
responsibleVpId: undefined,
responsibleVpName: undefined,
applyChangeDate: undefined,
contractAmount: undefined,
contractNetAmount: undefined,
currentStatus: undefined,
changeReason: undefined,
followUpWork: undefined,
projectChangeStatus: undefined,
flowStatus: undefined,
activeFlag: undefined,
params: {}
}
});
const { queryParams } = toRefs(data);
/** 查询项目变更申请列表 */
const getList = async () => {
loading.value = true;
const res = await listErpProjectChange(queryParams.value);
erpProjectChangeList.value = res.rows;
total.value = res.total;
// changeNumber
const latestNumberMap: Record<string, number> = {};
erpProjectChangeList.value.forEach((item) => {
const pid = String(item.projectId);
const num = item.changeNumber ?? 0;
if (!latestNumberMap[pid] || num > latestNumberMap[pid]) {
latestNumberMap[pid] = num;
}
});
// (3)
const completedMap: Record<string, number> = {};
erpProjectChangeList.value.forEach((item) => {
if (item.projectChangeStatus === '3') {
const pid = String(item.projectId);
const num = item.changeNumber ?? 0;
if (latestNumberMap[pid] === num) {
completedMap[pid] = num;
}
}
});
latestCompletedChangeMap.value = completedMap;
loading.value = false;
};
@ -322,6 +351,16 @@ const handleView = (row: ErpProjectChangeVO) => {
router.push(`/oa/erp/erpProjectChange/edit/${row.projectChangeId}?type=view`);
};
//
//
const isLatestCompletedChange = (row: ErpProjectChangeVO) => {
if (row.projectChangeStatus !== '3') {
return false;
}
const pid = String(row.projectId);
return latestCompletedChangeMap.value[pid] === row.changeNumber;
};
/** 新增变更按钮操作 */
const handleAddChange = (row: ErpProjectChangeVO) => {
// projectId

@ -210,71 +210,58 @@
<el-button type="danger" link icon="Delete" @click="handleDeleteStage(scope.$index)"></el-button>
</template>
</el-table-column>
<!-- 动态变更合并列每次变更为一组子列 -->
<!-- <el-table-column
<!-- 动态变更多列合并展示 -->
<el-table-column
v-for="(change, changeIndex) in projectChangeList"
:key="`change-${changeIndex}`"
:label="`第${changeIndex + 1}次变更`"
header-align="center"
:key="`change-${change.projectChangeId || changeIndex}`"
:label="`第${change.changeNumber || changeIndex + 1}次变更`"
align="center"
>
<el-table-column label="原起" min-width="120" align="center">
<el-table-column label="变更开始" width="110" align="center">
<template #default="scope">
<span v-if="getChangeProgressByStage(change, scope.row.planStageId)" class="change-highlight">
{{ getChangeProgressByStage(change, scope.row.planStageId).originalStart || '-' }}
{{ (getChangeProgressByStage(change, scope.row.planStageId).changedStart || '').substring(0, 10) || '-' }}
</span>
<span v-else class="text-gray-400">-</span>
<span v-else class="text-gray-300">-</span>
</template>
</el-table-column>
<el-table-column label="原止" min-width="120" align="center">
<el-table-column label="变更结束" width="110" align="center">
<template #default="scope">
<span v-if="getChangeProgressByStage(change, scope.row.planStageId)" class="change-highlight">
{{ getChangeProgressByStage(change, scope.row.planStageId).originalEnd || '-' }}
{{ (getChangeProgressByStage(change, scope.row.planStageId).changedEnd || '').substring(0, 10) || '-' }}
</span>
<span v-else class="text-gray-400">-</span>
<span v-else class="text-gray-300">-</span>
</template>
</el-table-column>
<el-table-column label="变更起" min-width="120" align="center">
<template #default="scope">
<span v-if="getChangeProgressByStage(change, scope.row.planStageId)" class="change-highlight">
{{ getChangeProgressByStage(change, scope.row.planStageId).changedStart || '-' }}
</span>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
<el-table-column label="变更止" min-width="120" align="center">
<template #default="scope">
<span v-if="getChangeProgressByStage(change, scope.row.planStageId)" class="change-highlight">
{{ getChangeProgressByStage(change, scope.row.planStageId).changedEnd || '-' }}
</span>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
<el-table-column label="里程碑" min-width="140" align="center">
<el-table-column label="里程碑" width="100" align="center">
<template #default="scope">
<span v-if="getChangeProgressByStage(change, scope.row.planStageId)" class="change-highlight">
{{ getChangeProgressByStage(change, scope.row.planStageId).milestoneName || '-' }}
</span>
<span v-else class="text-gray-400">-</span>
<span v-else class="text-gray-300">-</span>
</template>
</el-table-column>
<el-table-column label="完成度(%)" min-width="110" align="center">
<el-table-column label="完成度" width="80" align="center">
<template #default="scope">
<span v-if="getChangeProgressByStage(change, scope.row.planStageId)" class="change-highlight">
{{ getChangeProgressByStage(change, scope.row.planStageId).completionDegree ?? '-' }}
{{
getChangeProgressByStage(change, scope.row.planStageId).completionDegree != null
? getChangeProgressByStage(change, scope.row.planStageId).completionDegree + '%'
: '-'
}}
</span>
<span v-else class="text-gray-400">-</span>
<span v-else class="text-gray-300">-</span>
</template>
</el-table-column>
<el-table-column label="备注" min-width="160" align="left">
<el-table-column label="备注" width="120" align="center" show-overflow-tooltip>
<template #default="scope">
<span v-if="getChangeProgressByStage(change, scope.row.planStageId)" class="change-highlight">
{{ getChangeProgressByStage(change, scope.row.planStageId).remark || '-' }}
</span>
<span v-else class="text-gray-400">-</span>
<span v-else class="text-gray-300">-</span>
</template>
</el-table-column>
</el-table-column>-->
</el-table-column>
</el-table>
</el-form>
</el-card>
@ -820,8 +807,8 @@ const loadFormData = async () => {
syncPaymentMethod: false
});
}
//
if (isViewMode.value) {
//
if (isViewMode.value || form.value.projectPlanStatus === '3') {
await loadProjectChangeList();
}
} else {

@ -3,11 +3,7 @@
<el-card shadow="never">
<div class="mb-3 flex items-center justify-between">
<div class="flex flex-wrap items-center gap-2">
<el-switch
v-model="showActual"
active-text="显示实际进度"
inactive-text="隐藏实际进度"
/>
<el-switch v-model="showActual" active-text="" inactive-text="" />
</div>
<div>
<el-button @click="goBack"></el-button>
@ -35,13 +31,7 @@
</el-descriptions-item>
</el-descriptions>
<el-alert
title="任务数据来源于项目阶段,仅供查看。"
type="info"
show-icon
:closable="false"
class="mb-3"
/>
<el-alert title="任务数据来源于项目阶段,仅供查看。" type="info" show-icon :closable="false" class="mb-3" />
<el-divider content-position="left">项目甘特图</el-divider>
<div ref="chartContainerRef" class="gantt-chart" />
@ -57,12 +47,7 @@
<el-table-column prop="planEnd" label="计划结束" min-width="120" show-overflow-tooltip />
<el-table-column prop="realStart" label="实际开始" min-width="120" show-overflow-tooltip />
<el-table-column prop="realEnd" label="实际结束" min-width="120" show-overflow-tooltip />
<el-table-column
prop="dependencyLabels"
label="依赖任务"
min-width="160"
show-overflow-tooltip
/>
<el-table-column prop="dependencyLabels" label="依赖任务" min-width="160" show-overflow-tooltip />
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
</el-table>
</el-card>
@ -76,6 +61,7 @@ import * as echarts from 'echarts';
import { useResizeObserver } from '@vueuse/core';
import { getErpProjectPlan } from '@/api/oa/erp/erpProjectPlan';
import type { ErpProjectPlanVO } from '@/api/oa/erp/erpProjectPlan/types';
import { queryProjectChangeByProjectPlanId } from '@/api/oa/erp/erpProjectChange';
interface GanttTask {
taskId: string;
@ -88,14 +74,15 @@ interface GanttTask {
realEnd?: string;
dependencyLabels?: string;
remark?: string;
originalPlanStart?: string; //
originalPlanEnd?: string; //
hasChange?: boolean; //
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const route = useRoute();
const router = useRouter();
const { project_plan_status, project_phases } = toRefs<any>(
proxy?.useDict('project_plan_status', 'project_phases')
);
const { project_plan_status, project_phases } = toRefs<any>(proxy?.useDict('project_plan_status', 'project_phases'));
const loading = ref(false);
const showActual = ref(true);
@ -134,7 +121,7 @@ const lastPlanEndDate = computed(() => {
return '-';
}
let maxTs: number | null = null;
taskList.value.forEach(task => {
taskList.value.forEach((task) => {
const ts = toTimestamp(task.planEnd);
if (ts !== null) {
if (maxTs === null || ts > maxTs) {
@ -195,7 +182,7 @@ const updateChart = () => {
return;
}
const categories = taskList.value.map(task => task.taskName || '未命名任务');
const categories = taskList.value.map((task) => task.taskName || '未命名任务');
let min = Number.POSITIVE_INFINITY;
let max = Number.NEGATIVE_INFINITY;
@ -314,13 +301,16 @@ const updateChart = () => {
if (!task) {
return params.name;
}
const planText = `计划:${task.planStart || '-'}${task.planEnd || '-'}`;
let planText = `计划:${task.planStart || '-'}${task.planEnd || '-'}`;
if (task.hasChange) {
planText += `<br/><span style="color:#e6a23c;font-size:12px;">(最新变更) 原计划:${task.originalPlanStart || '-'}${task.originalPlanEnd || '-'}</span>`;
}
const actualText = `实际:${task.realStart || '-'}${task.realEnd || '-'}`;
return [params.marker + (task.taskName || '未命名任务'), planText, actualText].join('<br/>');
}
},
legend: {
data: series.map(s => s.name),
data: series.map((s) => s.name),
top: 0
},
grid: {
@ -363,25 +353,35 @@ watch(showActual, () => {
updateChart();
});
const buildTaskListFromStages = (stages: any[] = []): GanttTask[] =>
stages.map(stage => ({
taskId: String(stage.planStageId ?? generateTaskId()),
taskName:
phaseMap.value[stage.projectPhases as string] ||
stage.projectPhases ||
stage.scheduleRemark ||
'未命名阶段',
phaseName: phaseMap.value[stage.projectPhases as string] || '-',
ownerName: planDetail.value?.managerName,
planStart: normalizeDate(stage.planStartTime),
planEnd: normalizeDate(stage.planEndTime),
realStart: normalizeDate(stage.realStartTime),
realEnd: normalizeDate(stage.realEndTime),
dependencyLabels: Array.isArray(stage.dependencyNames)
? stage.dependencyNames.join('、')
: stage.dependencyNames || '-',
remark: stage.scheduleRemark || '-'
}));
const buildTaskListFromStages = (stages: any[] = [], changeMap: Map<any, any> = new Map()): GanttTask[] =>
stages.map((stage) => {
const originalStart = normalizeDate(stage.planStartTime);
const originalEnd = normalizeDate(stage.planEndTime);
//
const changeInfo = changeMap.get(stage.planStageId);
const hasChange = !!changeInfo;
// 使
const planStart = hasChange && changeInfo.changedStart ? normalizeDate(changeInfo.changedStart) : originalStart;
const planEnd = hasChange && changeInfo.changedEnd ? normalizeDate(changeInfo.changedEnd) : originalEnd;
return {
taskId: String(stage.planStageId ?? generateTaskId()),
taskName: phaseMap.value[stage.projectPhases as string] || stage.projectPhases || stage.scheduleRemark || '未命名阶段',
phaseName: phaseMap.value[stage.projectPhases as string] || '-',
ownerName: planDetail.value?.managerName,
planStart,
planEnd,
realStart: normalizeDate(stage.realStartTime),
realEnd: normalizeDate(stage.realEndTime),
dependencyLabels: Array.isArray(stage.dependencyNames) ? stage.dependencyNames.join('、') : stage.dependencyNames || '-',
remark: stage.scheduleRemark || '-',
originalPlanStart: originalStart,
originalPlanEnd: originalEnd,
hasChange
};
});
const loadPlanDetail = async () => {
if (!projectPlanId.value || projectPlanId.value === '0') {
@ -390,13 +390,37 @@ const loadPlanDetail = async () => {
}
loading.value = true;
try {
const res = await getErpProjectPlan(projectPlanId.value);
//
const [res, changeRes] = await Promise.all([
getErpProjectPlan(projectPlanId.value),
queryProjectChangeByProjectPlanId(projectPlanId.value).catch(() => ({ data: [] }))
]);
planDetail.value = res.data;
//
const changeList = changeRes.data || [];
const changeMap = new Map();
if (changeList.length > 0) {
// IDsort
//
const lastChange = changeList[changeList.length - 1];
if (lastChange && lastChange.progressList) {
lastChange.progressList.forEach((p: any) => {
if (p.planStageId) {
changeMap.set(p.planStageId, p);
}
});
}
}
await nextTick();
taskList.value = buildTaskListFromStages(res.data.planStageList || []);
taskList.value = buildTaskListFromStages(res.data.planStageList || [], changeMap);
updateChart();
} catch (error) {
proxy?.$modal.msgError('获取项目计划失败');
proxy?.$modal.msgError('获取项目计划数据失败');
console.error(error);
} finally {
loading.value = false;
}

Loading…
Cancel
Save