feat(erp/timesheetReport): 新增项目人员工时统计报表功能

- 在项目工时报表中添加行样式高亮功能,突出显示跨部门工时
- 新增项目人员工时统计报表页面,支持按部门查询人员工时分布
- 实现动态人员列展示和工时数据统计汇总功能
- 添加跨部门工时预警样式和导出功能
- 新增项目人员工时统计API接口和数据模型定义
dev
Yangk 2 weeks ago
parent c0e6d124b5
commit 8fd0209cac

@ -53,4 +53,34 @@ export function exportProjectManHourReport(query: ProjectManHourReportQuery) {
params: query,
responseType: 'blob'
});
// ... existing export function ...
}
/**
* VO
*/
export interface ProjectPersonnelReportVO {
columns: Array<{
label: string;
prop: string;
userId?: number;
}>;
rows: Array<any>;
footer: {
totalDays: number;
deptHeadCount: number;
totalManDays: number;
};
}
/**
*
* @param query { deptId, startTime, endTime }
*/
export function getProjectPersonnelReport(query: { deptId: number | string; startTime?: string; endTime?: string }) {
return request({
url: '/oa/erp/timesheetReport/projectPersonnelReport',
method: 'get',
params: query
});
}

@ -51,6 +51,7 @@
show-summary
:summary-method="getSummary"
:span-method="objectSpanMethod"
:row-class-name="tableRowClassName"
border
@selection-change="handleSelectionChange"
>
@ -267,4 +268,18 @@ const objectSpanMethod = ({
};
}
};
/** 行样式 */
const tableRowClassName = ({ row }: { row: ProjectManHourReportVO }) => {
if (row.crossDeptHours && row.totalHours && Number(row.crossDeptHours) > Number(row.totalHours)) {
return 'warning-row';
}
return '';
};
</script>
<style scoped>
:deep(.warning-row) {
background-color: #ffffcc !important;
}
</style>

@ -0,0 +1,224 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="部门" prop="deptId">
<el-select v-model="queryParams.deptId" placeholder="请选择部门" filterable style="width: 240px" @change="handleQuery">
<el-option v-for="item in deptOptions" :key="item.deptId" :label="item.deptName" :value="item.deptId" />
</el-select>
</el-form-item>
<el-form-item label="起止日期">
<el-date-picker
v-model="dateRange[0]"
type="date"
value-format="YYYY-MM-DD"
placeholder="开始日期"
:disabled-date="disabledDateStart"
style="width: 150px"
/>
<span class="el-range-separator" style="margin: 0 5px">-</span>
<el-date-picker
v-model="dateRange[1]"
type="date"
value-format="YYYY-MM-DD"
placeholder="结束日期"
:disabled-date="disabledDateEnd"
style="width: 150px"
/>
</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="tableData"
border
:row-class-name="tableRowClassName"
style="width: 100%"
show-summary
:summary-method="getSummaries"
>
<el-table-column type="selection" width="60" align="center" />
<el-table-column label="部门" prop="deptName" align="center" width="120" fixed />
<el-table-column label="项目名称" prop="projectName" align="left" min-width="180" fixed />
<el-table-column label="项目编号" prop="projectCode" align="center" width="150" fixed />
<!-- 汇总列 -->
<el-table-column label="汇总" prop="rowTotal" align="center" width="80">
<template #default="scope">
{{ Number(scope.row.rowTotal) === 0 ? '' : Number(scope.row.rowTotal).toFixed(1) }}
</template>
</el-table-column>
<!-- 动态人员列 -->
<el-table-column v-for="col in columns" :key="col.prop" :label="col.label" :prop="col.prop" align="center" width="80">
<template #default="scope">
{{ formatHours(scope.row[col.prop]) }}
</template>
</el-table-column>
<!-- 备注列 (跨部门) -->
<el-table-column label="备注" prop="remark" align="center" width="100" fixed="right">
<template #default="scope">
<span v-if="scope.row.isCrossDept" style="color: black; font-weight: bold"></span>
</template>
</el-table-column>
</el-table>
<!-- 底部统计 -->
</div>
</template>
<script setup name="ProjectPersonnelReport" lang="ts">
import { listDept } from '@/api/system/dept';
import { getProjectPersonnelReport, ProjectPersonnelReportVO } from '@/api/oa/erp/timesheetReport';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const loading = ref(false);
const showSearch = ref(true);
const deptOptions = ref<any[]>([]);
const dateRange = ref<[string, string]>(['', '']);
const columns = ref<Array<{ label: string; prop: string }>>([]);
const tableData = ref<Array<any>>([]);
const queryParams = ref({
deptId: undefined
});
//
// watch(() => queryParams.value.deptId, (newVal) => {
// if (newVal) handleQuery();
// });
/** 获取部门 */
const getDeptList = async () => {
const res = await listDept();
deptOptions.value = res.data || res.rows || [];
};
/** 查询报表 */
const getList = async () => {
if (!queryParams.value.deptId) {
proxy?.$modal.msgWarning('请选择部门');
return;
}
loading.value = true;
const params = {
deptId: queryParams.value.deptId,
startTime: dateRange.value[0],
endTime: dateRange.value[1]
};
try {
const res = await getProjectPersonnelReport(params);
if (res.data) {
columns.value = res.data.columns || [];
tableData.value = res.data.rows || [];
} else {
tableData.value = [];
columns.value = [];
}
} finally {
loading.value = false;
}
};
const handleQuery = () => {
getList();
};
const resetQuery = () => {
dateRange.value = ['', ''];
(proxy as any)?.resetForm('queryRef');
tableData.value = [];
columns.value = [];
};
const handleExport = () => {
proxy?.download(
'oa/erp/timesheetReport/exportProjectPersonnel',
{
deptId: queryParams.value.deptId,
startTime: dateRange.value[0],
endTime: dateRange.value[1]
},
`项目人员工时统计报表_${new Date().getTime()}.xlsx`
);
};
/** 行样式 */
const tableRowClassName = ({ row }: { row: any }) => {
if (row.isCrossDept) {
return 'warning-row';
}
return '';
};
/** 格式化工时 */
const formatHours = (val: any) => {
if (val === undefined || val === null || val === '' || Number(val) === 0) return '';
return Number(val).toFixed(1);
};
/** 合计行逻辑 */
const getSummaries = (param: any) => {
const { columns, data } = param;
const sums: string[] = [];
columns.forEach((column: any, index: number) => {
if (index === 0) {
sums[index] = '总计';
return;
}
//
if (['projectName', 'projectCode', 'remark', 'deptName'].includes(column.property)) {
sums[index] = '';
return;
}
//
const values = data.map((item: any) => Number(item[column.property]));
if (!values.every((value: any) => Number.isNaN(value))) {
const sum = values.reduce((prev: any, curr: any) => {
const value = Number(curr);
if (!Number.isNaN(value)) {
return prev + curr;
} else {
return prev;
}
}, 0);
sums[index] = sum === 0 ? '' : sum.toFixed(1);
} else {
sums[index] = '';
}
});
return sums;
};
// /
const disabledDateStart = (date: Date) => {
return date.getDay() !== 1;
};
const disabledDateEnd = (date: Date) => {
return date.getDay() !== 0;
};
onMounted(() => {
getDeptList();
});
</script>
<style scoped>
:deep(.warning-row) {
background-color: #fcf6ec;
/* Element Plus Warning Light Color or Custom Yellow */
background-color: #ffffcc !important;
}
</style>
Loading…
Cancel
Save