feat(erp/projectLedgerReport): 添加项目台账报表功能

- 新增项目台账报表页面,支持项目基本信息,合同信息,预算及成本项目的综合展示
dev
Yangk 1 week ago
parent 2f9d27b03f
commit 58c139b10c

@ -0,0 +1,405 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryFormRef" :inline="true" v-show="showSearch" label-width="80px">
<el-form-item label="项目编号" prop="projectCode">
<el-input v-model="queryParams.projectCode" placeholder="请输入项目编号" clearable style="width: 200px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="业务方向" prop="businessDirection">
<el-select v-model="queryParams.businessDirection" placeholder="请选择业务方向" clearable style="width: 200px">
<el-option v-for="dict in business_direction" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="项目状态" prop="projectStatus">
<el-select v-model="queryParams.projectStatus" placeholder="请选择项目状态" clearable style="width: 200px">
<el-option v-for="dict in project_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery"></el-button>
<el-button icon="Refresh" @click="resetQuery"></el-button>
<el-button type="warning" plain icon="Download" @click="handleExport"></el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="reportList" border @selection-change="handleSelectionChange">
<!-- ========== 基本信息 ========== -->
<el-table-column label="基本信息" align="center">
<el-table-column type="selection" width="55" align="center" fixed="left" />
<el-table-column label="项目编号" prop="projectCode" width="140" align="center" fixed="left" show-overflow-tooltip />
<el-table-column label="客户名称" width="150" align="center" show-overflow-tooltip>
<template #default="scope">
{{ scope.row._customerName || '-' }}
</template>
</el-table-column>
<el-table-column label="项目名称" prop="projectName" width="200" align="center" show-overflow-tooltip />
<el-table-column label="项目经理" prop="managerName" width="90" align="center" />
<el-table-column label="部门" prop="deptName" width="120" align="center" show-overflow-tooltip />
<el-table-column label="项目类型" prop="typeName" width="120" align="center" show-overflow-tooltip />
<el-table-column label="产品型号" width="100" align="center">
<template #default>-</template>
</el-table-column>
<el-table-column label="产品数量" width="100" align="center">
<template #default="scope">
{{ scope.row._productAmount != null ? scope.row._productAmount : '-' }}
</template>
</el-table-column>
<el-table-column label="项目阶段" width="100" align="center">
<template #default="scope">
<dict-tag v-if="scope.row._projectPhases" :options="project_phases" :value="scope.row._projectPhases" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="情况说明" width="120" align="center">
<template #default>-</template>
</el-table-column>
<el-table-column label="状态" width="80" align="center">
<template #default="scope">
<dict-tag :options="project_status" :value="scope.row.projectStatus" />
</template>
</el-table-column>
<el-table-column label="预计验收时间" width="120" align="center">
<template #default>-</template>
</el-table-column>
<el-table-column label="实际验收时间" width="120" align="center">
<template #default="scope">
{{ scope.row._acceptanceDate || '-' }}
</template>
</el-table-column>
<el-table-column label="是否异常启动" width="120" align="center">
<template #default>-</template>
</el-table-column>
<el-table-column label="异常启动原因" width="120" align="center">
<template #default>-</template>
</el-table-column>
</el-table-column>
<!-- ========== 合同信息 ========== -->
<el-table-column label="合同信息" align="center">
<el-table-column label="签订时间" width="110" align="center">
<template #default="scope">
{{ scope.row._contractDate || '-' }}
</template>
</el-table-column>
<el-table-column label="合同额" width="120" align="center">
<template #default="scope">
{{ scope.row._contractAmount != null ? formatNumber(scope.row._contractAmount) : '-' }}
</template>
</el-table-column>
<el-table-column label="客户经理" width="90" align="center">
<template #default="scope">
{{ scope.row._contractManagerName || '-' }}
</template>
</el-table-column>
<el-table-column label="付款方式" prop="paymentMethod" width="110" align="center" show-overflow-tooltip />
</el-table-column>
<!-- ========== 预算及成本 ========== -->
<el-table-column label="预算及成本" align="center">
<el-table-column label="预算" width="120" align="center">
<template #default="scope">
{{ scope.row._budgetCost != null ? formatNumber(scope.row._budgetCost) : '-' }}
</template>
</el-table-column>
<el-table-column label="预算毛利率" width="100" align="center">
<template #default="scope">
{{ scope.row._budgetRate != null ? scope.row._budgetRate + '%' : '-' }}
</template>
</el-table-column>
<el-table-column label="降成本后预算" width="130" align="center">
<template #default="scope">
{{ scope.row._reduceBudgetCost != null ? formatNumber(scope.row._reduceBudgetCost) : '-' }}
</template>
</el-table-column>
<el-table-column label="降成本后预算毛利率" width="150" align="center">
<template #default="scope">
{{ scope.row._reduceBudgetRate != null ? scope.row._reduceBudgetRate + '%' : '-' }}
</template>
</el-table-column>
<el-table-column label="累计工时" width="100" align="center">
<template #default="scope">
{{ scope.row._totalHours != null ? scope.row._totalHours : '-' }}
</template>
</el-table-column>
<el-table-column label="已发生成本" width="110" align="center">
<template #default>-</template>
</el-table-column>
<el-table-column label="收入(合同额/1.13" width="120" align="center">
<template #default="scope">
{{ scope.row._revenue != null ? formatNumber(scope.row._revenue) : '-' }}
</template>
</el-table-column>
<el-table-column label="已发生成本占收入比例" width="160" align="center">
<template #default>-</template>
</el-table-column>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</template>
<script setup lang="ts" name="ProjectLedgerReport">
import { listProjectInfo } from '@/api/oa/erp/projectInfo';
import { ProjectInfoVO, ProjectInfoQuery } from '@/api/oa/erp/projectInfo/types';
import { listErpBudgetInfo } from '@/api/oa/erp/budgetInfo';
import { listContractInfo } from '@/api/oa/erp/contractInfo';
import { listProjectAcceptance } from '@/api/oa/erp/projectAcceptance';
import { listProjectManHourReport } from '@/api/oa/erp/timesheetReport';
import { listContractMaterial } from '@/api/oa/erp/contractMaterial';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { business_direction, project_status, project_phases } = toRefs<any>(proxy?.useDict('business_direction', 'project_status', 'project_phases'));
const loading = ref(true);
const showSearch = ref(true);
const total = ref(0);
const reportList = ref<any[]>([]);
const selectedRows = ref<any[]>([]);
const queryFormRef = ref<ElFormInstance>();
const queryParams = reactive<ProjectInfoQuery>({
pageNum: 1,
pageSize: 20,
projectCode: undefined,
projectName: undefined,
businessDirection: undefined,
projectStatus: undefined,
params: {}
});
/** 格式化数字 */
const formatNumber = (num: number) => {
return num?.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
};
/** 查询报表列表 */
const getList = async () => {
loading.value = true;
try {
// 1. 9
const res = await listProjectInfo(queryParams);
const projects: any[] = (res.rows || []).filter((p: ProjectInfoVO) => p.projectCategory !== '9');
total.value = res.total;
//
projects.forEach((p) => {
p._customerName = '';
p._contractDate = '';
p._contractAmount = null;
p._contractManagerName = '';
p._budgetCost = null;
p._budgetRate = null;
p._reduceBudgetCost = null;
p._reduceBudgetRate = null;
p._budgetContractAmount = null;
p._revenue = null;
p._acceptanceDate = '';
p._projectPhases = '';
p._totalHours = null;
p._productAmount = null;
});
// 2.
const [budgetRes, acceptanceRes, contractRes, manHourRes, materialRes] = await Promise.all([
listErpBudgetInfo({ pageNum: 1, pageSize: 9999, budgetStatus: '3' } as any).catch(() => ({ rows: [] })),
listProjectAcceptance({ pageNum: 1, pageSize: 9999 } as any).catch(() => ({ rows: [] })),
listContractInfo({ pageNum: 1, pageSize: 9999 } as any).catch(() => ({ rows: [] })),
listProjectManHourReport({ pageNum: 1, pageSize: 9999 } as any).catch(() => ({ data: [] })),
listContractMaterial({ pageNum: 1, pageSize: 9999 } as any).catch(() => ({ rows: [] }))
]);
//
const budgetMap = new Map<string, any>();
((budgetRes as any).rows || []).forEach((b: any) => {
if (b.projectId) budgetMap.set(String(b.projectId), b);
});
const acceptanceMap = new Map<string, any>();
((acceptanceRes as any).rows || []).forEach((a: any) => {
if (a.projectId && a.flowStatus === 'finish') {
acceptanceMap.set(String(a.projectId), a);
}
});
const contractMap = new Map<string, any>();
((contractRes as any).rows || []).forEach((c: any) => {
if (c.contractId) contractMap.set(String(c.contractId), c);
});
const manHourMap = new Map<string, number>();
const manHourData = (manHourRes as any).data || (manHourRes as any).rows || [];
manHourData.forEach((m: any) => {
if (m.projectId) {
const pid = String(m.projectId);
manHourMap.set(pid, (manHourMap.get(pid) || 0) + Number(m.totalHours || 0));
}
});
// contractId amount
const materialAmountMap = new Map<string, number>();
((materialRes as any).rows || []).forEach((m: any) => {
if (m.contractId) {
const cid = String(m.contractId);
materialAmountMap.set(cid, (materialAmountMap.get(cid) || 0) + Number(m.amount || 0));
}
});
// 3.
projects.forEach((p) => {
const pid = String(p.projectId);
//
if (p.contractId) {
const contract = contractMap.get(String(p.contractId));
if (contract) {
p._customerName = contract.oneCustomerName || '';
p._contractDate = contract.contractDate || '';
p._contractManagerName = contract.contractManagerName || '';
}
//
const amt = materialAmountMap.get(String(p.contractId));
if (amt != null) {
p._productAmount = amt;
}
}
//
const budget = budgetMap.get(pid);
if (budget) {
p._budgetCost = budget.budgetCost;
p._budgetRate = budget.budgetRate;
p._reduceBudgetCost = budget.reduceBudgetCost;
p._reduceBudgetRate = budget.reduceBudgetRate;
p._budgetContractAmount = budget.contractAmount;
p._revenue = budget.contractAmount != null ? budget.contractAmount / 1.13 : null;
p._contractAmount = budget.contractAmount;
}
//
const acceptance = acceptanceMap.get(pid);
if (acceptance) {
p._acceptanceDate = acceptance.acceptanceDate || '';
}
//
const hours = manHourMap.get(pid);
if (hours != null) {
p._totalHours = hours;
}
});
reportList.value = projects;
} finally {
loading.value = false;
}
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection;
};
/** 获取字典标签 */
const getDictLabel = (dictOptions: any, value: string | number | undefined) => {
if (value == null || value === '') return '-';
const opts = dictOptions?.value || dictOptions || [];
const item = opts.find((d: any) => String(d.value) === String(value));
return item?.label || '-';
};
/** 导出按钮操作 */
const handleExport = () => {
const data = selectedRows.value.length > 0 ? selectedRows.value : reportList.value;
if (!data || data.length === 0) {
proxy?.$modal.msgWarning('没有可导出的数据');
return;
}
// HTML table
const html = `
<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40">
<head><meta charset="UTF-8"><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet>
<x:Name>项目台账报表</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions>
</x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--></head><body>
<table border="1" cellspacing="0" cellpadding="4" style="border-collapse:collapse; font-size:11px;">
<tr>
<td colspan="16" align="center" style="font-weight:bold; background:#CCE5FF;">基本信息</td>
<td colspan="4" align="center" style="font-weight:bold; background:#FFCCCC;">合同信息</td>
<td colspan="8" align="center" style="font-weight:bold; background:#CCFFCC;">预算及成本</td>
</tr>
<tr style="font-weight:bold; font-size:10px;">
<td>序号</td><td>项目编号</td><td>客户名称</td><td>项目名称</td><td>项目经理</td>
<td>部门</td><td>项目类型</td><td>产品型号</td><td>产品数量</td><td>项目阶段</td><td>情况说明</td>
<td>状态</td><td>预计验收时间</td><td>实际验收时间</td>
<td>是否异常启动</td><td>异常启动原因</td>
<td>签订时间</td><td>合同额</td><td>客户经理</td><td>付款方式</td>
<td>预算</td><td>预算毛利率</td><td>降成本后预算</td>
<td>降成本后预算毛利率</td><td>已发生成本</td><td>收入合同额/1.13</td><td>已发生成本占收入比例</td><td>累计工时</td>
</tr>
${data
.map(
(row: any, idx: number) => `<tr>
<td>${idx + 1}</td>
<td>${row.projectCode || '-'}</td>
<td>${row._customerName || '-'}</td>
<td>${row.projectName || '-'}</td>
<td>${row.managerName || '-'}</td>
<td>${row.deptName || '-'}</td>
<td>${row.typeName || '-'}</td>
<td>-</td>
<td>${row._productAmount != null ? row._productAmount : '-'}</td>
<td>${row._projectPhases ? getDictLabel(project_phases, row._projectPhases) : '-'}</td>
<td>-</td>
<td>${getDictLabel(project_status, row.projectStatus)}</td>
<td>-</td>
<td>${row._acceptanceDate || '-'}</td>
<td>-</td>
<td>-</td>
<td>${row._contractDate || '-'}</td>
<td>${row._contractAmount != null ? formatNumber(row._contractAmount) : '-'}</td>
<td>${row._contractManagerName || '-'}</td>
<td>${row.paymentMethod || '-'}</td>
<td>${row._budgetCost != null ? formatNumber(row._budgetCost) : '-'}</td>
<td>${row._budgetRate != null ? row._budgetRate + '%' : '-'}</td>
<td>${row._reduceBudgetCost != null ? formatNumber(row._reduceBudgetCost) : '-'}</td>
<td>${row._reduceBudgetRate != null ? row._reduceBudgetRate + '%' : '-'}</td>
<td>-</td>
<td>${row._revenue != null ? formatNumber(row._revenue) : '-'}</td>
<td>-</td>
<td>${row._totalHours != null ? row._totalHours : '-'}</td>
</tr>`
)
.join('')}
</table></body></html>`;
const blob = new Blob([html], { type: 'application/vnd.ms-excel;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `项目台账报表_${new Date().getTime()}.xls`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
onMounted(() => {
getList();
});
</script>
<style scoped>
:deep(.el-table .el-table__header th) {
font-weight: 600;
}
</style>
Loading…
Cancel
Save