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-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-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['oa/erp:erpProjectChange:edit']"></el-button>
</el-tooltip> </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-button link type="success" icon="Plus" @click="handleAddChange(scope.row)" v-hasPermi="['oa/erp:erpProjectChange:add']"></el-button>
</el-tooltip> </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') proxy?.useDict('active_flag', 'change_type', 'project_change_status', 'project_category', 'wf_business_status')
); );
//
const erpProjectChangeList = ref<ErpProjectChangeVO[]>([]); const erpProjectChangeList = ref<ErpProjectChangeVO[]>([]);
//
const latestCompletedChangeMap = ref<Record<string, number>>({});
const loading = ref(true); const loading = ref(true);
const showSearch = ref(true); const showSearch = ref(true);
const ids = ref<Array<string | number>>([]); const ids = ref<Array<string | number>>([]);
@ -248,39 +251,65 @@ const columns = ref<FieldOption[]>([
{ key: 29, label: `更新时间`, visible: false } { key: 29, label: `更新时间`, visible: false }
]); ]);
const queryParams = reactive<ErpProjectChangeQuery>({ // +
pageNum: 1, const data = reactive<{ queryParams: ErpProjectChangeQuery }>({
pageSize: 10, queryParams: {
projectId: undefined, pageNum: 1,
projectCode: undefined, pageSize: 10,
projectName: undefined, projectId: undefined,
projectCategory: undefined, projectCode: undefined,
changeType: undefined, projectName: undefined,
changeNumber: undefined, projectCategory: undefined,
projectManagerId: undefined, changeType: undefined,
projectManagerName: undefined, changeNumber: undefined,
deptHeadId: undefined, projectManagerId: undefined,
deptHeadName: undefined, projectManagerName: undefined,
responsibleVpId: undefined, deptHeadId: undefined,
responsibleVpName: undefined, deptHeadName: undefined,
applyChangeDate: undefined, responsibleVpId: undefined,
contractAmount: undefined, responsibleVpName: undefined,
contractNetAmount: undefined, applyChangeDate: undefined,
currentStatus: undefined, contractAmount: undefined,
changeReason: undefined, contractNetAmount: undefined,
followUpWork: undefined, currentStatus: undefined,
projectChangeStatus: undefined, changeReason: undefined,
flowStatus: undefined, followUpWork: undefined,
activeFlag: undefined, projectChangeStatus: undefined,
params: {} flowStatus: undefined,
activeFlag: undefined,
params: {}
}
}); });
const { queryParams } = toRefs(data);
/** 查询项目变更申请列表 */ /** 查询项目变更申请列表 */
const getList = async () => { const getList = async () => {
loading.value = true; loading.value = true;
const res = await listErpProjectChange(queryParams.value); const res = await listErpProjectChange(queryParams.value);
erpProjectChangeList.value = res.rows; erpProjectChangeList.value = res.rows;
total.value = res.total; 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; loading.value = false;
}; };
@ -322,6 +351,16 @@ const handleView = (row: ErpProjectChangeVO) => {
router.push(`/oa/erp/erpProjectChange/edit/${row.projectChangeId}?type=view`); 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) => { const handleAddChange = (row: ErpProjectChangeVO) => {
// projectId // projectId

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

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

Loading…
Cancel
Save