动态表头报表的绘制与导出
目录
一、效果图
二、整体思路
三、代码区
一、效果图
根据选择的日期范围动态生成表头(eg:2025.2.24--2025.03.03)每个日期又分为白班、夜班;数据列表中对产线合并单元格。支持按原格式导出对应的报表excel。
点击空白区可新增工时数据;点击蓝色字体可以编辑工时数据。

二、整体思路
前端绘制报表,分成两部分,一部分固定列头,一部分动态列头。固定列头就按照table正常情况绘制;动态且多级列头部分:根据选择的日期范围在前端获取该范围内日期列表,另外对于后端传来的数据进行处理(生成动态字段(日期_班次),并将数据跟生成的动态表头字段一 一对应),参考el-table中多级表头的实现(Table 表格 | Element Plus)
后端引入NPOI,创建XSSFWorkbook工作簿,填充每个单元格的数据,生成excel表格。点击导出按钮,调用后端接口,生成报表excel。
三、代码区
html
<el-tabs type="border-card"><!-- tab1 --><el-tab-pane label="人力统计报表"><el-form :model="queryParams" label-position="right" inline ref="queryRef" :rules="rulesQueryReport"><!-- 查询条件 --><el-form-item label="生产线" prop="lineCode"><el-select clearable v-model="queryParams.lineCode" placeholder="请选择生产线" filterable@change="queryLineSelectChange" style="width: 100%;"><el-option v-for="item in options.mes_line_list" :key="item.id" :label="item.lineName":value="item.lineCode"><span class="fl">{{ item.innerLineCode }}</span><span class="fr" style="color: var(--el-text-color-secondary);">{{ item.lineName }}</span></el-option></el-select></el-form-item><el-form-item label="日期" prop="dateRangeWorkDate"><el-date-picker v-model="dateRangeWorkDate" type="daterange" range-separator="到" start-placeholder="开始日期"end-placeholder="结束日期" value-format="YYYY-MM-DD" :shortcuts="shortcuts" @change="handleDateChange":clearable="false" /></el-form-item><el-form-item><el-button icon="search" type="primary" @click="handleQuery">{{ $t('btn.search') }}</el-button><el-button icon="refresh" @click="resetQuery">{{ $t('btn.reset') }}</el-button></el-form-item></el-form><el-row :gutter="15" class="mb10"><el-col :span="1.5"><el-button type="primary" v-hasPermi="['datareport:meshumanreport:add']" plain icon="plus"@click="handleAdd">{{ $t('btn.add') }}</el-button></el-col><el-col :span="1.5"><el-button type="warning" plain icon="download" @click="handleExport"v-hasPermi="['datareport:meshumanreport:export']">{{ $t('btn.export') }}</el-button></el-col></el-row><!-- 添加横向滚动容器 --><div style="max-height:600px; overflow: hidden; position: relative;"><!-- 外层容器处理纵向滚动 --><div style="max-height: 600px; overflow-y: auto;"><!-- 内层容器处理横向滚动 --><div style="min-width: 100%; display: inline-block;"><!-- 动态表格 --><el-table :data="dataList" :span-method="spanMethod" v-loading="loading" ref="table" borderheader-cell-class-name="el-table-header-cell" highlight-current-row :max-height="600":cell-style="cellStyle" :header-cell-style="headerStyle" @cell-click="handleCellClick"><el-table-column prop="lineName" label="产线名称" fixed width="120" align="center" /><el-table-column prop="userName" label="姓名" fixed width="120" align="center" /><el-table-column prop="total" label="合计" fixed width="100" align="center" /><el-table-column prop="postNames" label="岗位" fixed width="100" align="center" /><el-table-column prop="userLaborAttribute" label="劳务属性" fixed width="100" align="center" /><!-- 动态日期列 --><el-table-column v-for="(dateCol, index) in dynamicHeaders" :key="index" :label="dateCol.date"align="center"><el-table-column v-for="shift in ['白班', '夜班']" :key="shift" :prop="`${dateCol.date}_${shift}`":label="shift" width="100" align="center"></el-table-column></el-table-column></el-table></div></div><!-- 添加底部阴影提示 --><div v-show="showHorizontalScrollHint"style="position: absolute; bottom: 0; width: 100%; height: 6px; background: linear-gradient(to top, rgba(0,0,0,0.1), transparent);"></div></div></el-tab-pane></el-tabs>
js
<script setup name="meshumanreport">
import {listMesHumanReport, getMesHumanReportList,addMesHumanReport, delMesHumanReport,updateMesHumanReport, updateMesHumanReportDetailPartial, getMesHumanReport,
}from '@/api/dataReport/meshumanreport.js'
import { getMesLineList } from '@/api/factoryManage/mesline.js'
import dayjs from 'dayjs';
import auth from '@/plugins/auth'const { proxy } = getCurrentInstance()
const ids = ref([])
const loading = ref(false)
const loadingLeft = ref(false)
const loadingRight = ref(false)
const showSearch = ref(true)
const queryParams = reactive({pageNum: 1,pageSize: 10,sort: '',sortType: 'asc',workShopCode: undefined,workShopName: undefined,lineCode: undefined,lineName: undefined,
})
const queryParamsUserNav = reactive({pageNum: 1,pageSize: 20,sortType: 'asc',deptId: undefined,postId: undefined,userId: undefined,
})const total = ref(0)
const totalLeft = ref(0)
const totalRight = ref(0)
const dataList = ref([])
const dataListLeft = ref([])
const dataListRight = ref([])
const dataListLeftChosed = ref([])
const dataListRightChosed = ref([])
const queryRef = ref()
const queryRefUserNav = ref()
const defaultTime = ref([new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)])
const deptOptions = ref([])
const postOptions = ref([])
const userOptions = ref([])
const headers = ref(null)
const standardDisabled = ref(true)
const tableHeight = ref(570)var dictParams = [{ dictType: "mes_classes_type" },
]proxy.getDicts(dictParams).then((response) => {response.data.forEach((element) => {state.options[element.dictType] = element.list})
})//当前时间
const now = new Date();
// 工作日期范围(默认当月)
// const dateRangeWorkDate = ref([])//设置:subtract往前推 add往后
const dateRangeWorkDate = ref([dayjs().subtract(1, 'day').format('YYYY-MM-DD'), dayjs().format('YYYY-MM-DD')])const shortcuts = [{text: '最近一周',value: () => {const end = new Date()const start = new Date()start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)return [start, end]},},{text: '最近一个月',value: () => {const end = new Date()const start = new Date()start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)return [start, end]},},{text: '最近一季度',value: () => {const end = new Date()const start = new Date()start.setTime(start.getTime() - 3600 * 1000 * 24 * 90)return [start, end]},},
]//日期范围变化时,触发表单校验(因为el-date-picker 选中的值不会自动通知表单验证状态,所以这里要单独处理)
function handleDateChange() {dataList.value = []// console.log(dateRangeWorkDate.value)// queryParams.dateRangeWorkDate = dateRangeWorkDate.value// // 查询报表数据// getList()
}// 动态表头生成
// const dateRangeWorkDate = ref([])
const dynamicHeaders = computed(() => {console.log(dateRangeWorkDate.value)if (!dateRangeWorkDate.value || dateRangeWorkDate.value.length !== 2) return []const dates = []const start = new Date(dateRangeWorkDate.value[0])const end = new Date(dateRangeWorkDate.value[1])while (start <= end) {dates.push({date: start.toISOString().split('T')[0],shifts: ['白班', '夜班']})start.setDate(start.getDate() + 1)}console.log(dates)return dates
})// 合并单元格逻辑
const spanMethod = ({ row, column, rowIndex, columnIndex }) => {if (column.property === 'lineName') {// 确保数据已按 lineName 排序const currentLine = row.lineName;let rowspan = 1;// 仅当是当前产线的第一行时计算合并行数if (rowIndex === 0 || dataList.value[rowIndex - 1].lineName !== currentLine) {for (let i = rowIndex + 1; i < dataList.value.length; i++) {if (dataList.value[i].lineName === currentLine) {rowspan++;} else {break;}}return { rowspan, colspan: 1 };} else {return { rowspan: 0, colspan: 0 };}}
};// 查询报表数据
function getList() {queryParams.dateRangeWorkDate = dateRangeWorkDate.valueproxy.addDateRange(queryParams, dateRangeWorkDate.value, 'WorkDate');console.log(queryParams)// 表单校验console.log(queryRef.value)// if (queryRef.value) {// proxy.$refs["queryRef"].validate((valid) => {// if (valid) {loading.value = truegetMesHumanReportList(queryParams).then(res => {const { code, data } = resconsole.log(res)if (code == 200) {// dataList.value = datadataList.value = processData(data)}loading.value = false}).catch(() => {loading.value = false})// }// })// }
}// 数据处理
const processData = (rawData) => {console.log(rawData)return rawData.map(item => {// const formatted = {// lineName: item.lineName,// userName: item.userName,// total: item.totalHours + 'h'// }const formatted = {...item}formatted.total = item.totalHours + 'h'item.details.forEach(d => {d.date = dayjs(d.date).format('YYYY-MM-DD');if (d.dayShift != null) {formatted[`${d.date}_白班`] = d.dayShift + 'h'}if (d.nightShift != null) {formatted[`${d.date}_夜班`] = d.nightShift + 'h'}})console.log(rawData)console.log(formatted)return formatted})
}// 查询
function handleQuery() {queryParams.pageNum = 1getList()
}// 重置查询操作
function resetQuery() {proxy.resetForm("queryRef")dateRangeWorkDate.value = [dayjs().subtract(1, 'day').format('YYYY-MM-DD'), dayjs().format('YYYY-MM-DD')]queryParams.dateRangeWorkDate = dateRangeWorkDate.valuehandleQuery()
}//单元格点击事件
function handleCellClick(row, column, cell, event) {if (auth.hasPermi('datareport:meshumanreport:edit')) {console.log(row)console.log(column)console.log(row[column.property])// 如果是需要编辑的列,则打开编辑框//没有数据的列不允许编辑,后续可以去掉row[column.property] != undefined限制,扩展点击空白地方新增数据if ((column.property.includes('白班') || column.property.includes('夜班'))) {// 显示编辑框openEditCell.value = true;formEdit.value = {}formEdit.value = {...row,workDate: column.property.split('_')[0],workShift: column.property.split('_')[1],}//标准工时//调用接口查主表数据,没有则让他手动输入,默认12hlistMesHumanReport({ lineCode: row.lineCode, workDate: formEdit.value.workDate, workShift: formEdit.value.workShift }).then(res => {const { code, data } = resconsole.log(res)if (code == 200) {if (data.result.length > 0) {standardDisabled.value = trueformEdit.value.standardWorkHours = data.result[0].standardWorkHours}else {standardDisabled.value = falseformEdit.value.standardWorkHours = 12}}})//实际工时if (row[column.property] != undefined && row[column.property] != null) {formEdit.value.actualWorkHours = row[column.property].split('h')[0];}}}
}// 导出按钮操作
function handleExport() {proxy.$confirm("是否确认导出人力报表数据?", "警告", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning",}).then(async () => {await proxy.downFile('/DataReport/MesHumanReport/exportReport', { ...queryParams })})
}function cellStyle(row, rowIndex) {console.log(row)if ((row.column.property.includes('白班') || row.column.property.includes('夜班'))) {// if (row.column.property === 'lineName') {console.log(row)return {color: '#409eff',}}
}/**************************************************** form操作 ****************************************************/
const formRef = ref()
const formRefEdit = ref()
const formRefUserNav = ref()
const title = ref('')
const titleUserNav = ref('')
// 操作类型 1、add 2、edit 3、view
const opertype = ref(0)
const open = ref(false)
const openUserNav = ref(false)
const openEditCell = ref(false)
const state = reactive({single: true,multiple: true,form: {},formUserNav: {},formEdit: {},rules: {//产线lineCode: [{ required: true, message: '请选择产线', trigger: 'change' },],//标准工时standardWorkHours: [{ required: true, message: '请输入标准工时', trigger: ['change', 'blur'] },],workDate: [{ required: true, message: '请选择日期范围', trigger: ['change', 'blur'] },],workShift: [{ required: true, message: '请选择班次', trigger: 'change' },],},rulesQueryReport: {//产线// lineCode: [{ required: true, message: '请选择产线', trigger: 'change' },],//日期dateRangeWorkDate: [{ required: true, message: '请选择日期范围', trigger: 'blur' },],},rulesEdit: {lineName: [{ required: true, message: '请输入产线', trigger: 'blur' },],userName: [{ required: true, message: '请输入人员', trigger: 'blur' },],workDate: [{ required: true, message: '请选择日期', trigger: 'change' },],workShift: [{ required: true, message: '请选择班次', trigger: 'change' },],standardWorkHours: [{ required: true, message: '请输入标准工时', trigger: 'change' },],actualWorkHours: [{ required: true, message: '请输入实际工时', trigger: 'change' },],},options: {// 产线列表mes_line_list: [],// 班次 选项列表 格式 eg:{ dictLabel: '标签', dictValue: '0'}mes_classes_type: [],}
})const { form, formUserNav, formEdit, rules, rulesQueryReport, rulesEdit, options, single, multiple } = toRefs(state)//保存编辑单元格数据
function editSubmit() {// 保存编辑单元格数据proxy.$refs["formRefEdit"].validate((valid) => {if (valid) {//提交表单console.log(formEdit.value)updateMesHumanReportDetailPartial(formEdit.value).then((res) => {console.log(res)openEditCell.value = falseif (res.code == 200) {proxy.$modal.msgSuccess("操作成功!")getList()} else {// proxy.$modal.msgError("操作失败")}}).catch(() => {proxy.$modal.msgError("操作失败!")})}})
}// 取消编辑单元格数据
function editCancel() {openEditCell.value = falseformEdit.value = {}
}function getDropDownList() {// 获取生产线列表getMesLineList().then(res => {console.log(res)const { code, data } = resif (code == 200) {state.options.mes_line_list = data}})handleQuery()
getDropDownList()
</script>
C#
Service层 导出功能
/// <summary>/// 导出人力报工报表数据/// </summary>/// <param name="parm"></param>/// <returns></returns>public (string, string) ExportMesHumanReport(MesHumanReportQueryDto parm){var data = GetMesHumanReportResponse(parm);List<DateTime> dateList = new List<DateTime>();var start = parm.BeginWorkDate.Value.Date;var end = parm.EndWorkDate.Value.Date;while (start <= end){dateList.Add(start);start = start.AddDays(1);}//创建工作簿IWorkbook workbook = new XSSFWorkbook();//创建工作表ISheet sheet = workbook.CreateSheet("人力报工统计");#region 表头//创建第一行表头IRow headerRow1 = sheet.CreateRow(0);headerRow1.CreateCell(0).SetCellValue("产线");headerRow1.CreateCell(1).SetCellValue("姓名");headerRow1.CreateCell(2).SetCellValue("合计(h)");headerRow1.CreateCell(3).SetCellValue("岗位");headerRow1.CreateCell(4).SetCellValue("劳务属性");int fixedColumnCount = 5;for (int i = 0, j = 0; i < dateList.Count; i++, j = j + 2){headerRow1.CreateCell(fixedColumnCount + j).SetCellValue(dateList[i].ToCommonDateString());headerRow1.CreateCell(fixedColumnCount + j + 1).SetCellValue(string.Empty);//合并表头列单元格sheet.AddMergedRegion(new CellRangeAddress(0, 0, fixedColumnCount + j, fixedColumnCount + j + 1));}//创建第二行表头IRow headerRow2 = sheet.CreateRow(1);for (int i = 0, j = 0; i < dateList.Count; i++, j = j + 2){headerRow2.CreateCell(fixedColumnCount + j).SetCellValue("白班");headerRow2.CreateCell(fixedColumnCount + j + 1).SetCellValue("夜班");}//合并表头行单元格for (int i = 0; i < fixedColumnCount; i++){sheet.AddMergedRegion(new CellRangeAddress(0, 1, i, i));}#endregion#region 表体//创建数据行int rowIndex = 2;//记录当前产线的起始行索引和结束行索引,为合并产线做准备int startRowIndex = 2;int lineCode = data.Count > 0 ? data.FirstOrDefault().LineCode ?? 0 : 0;int endRowIndex = 2;foreach (var item in data){IRow dataRow = sheet.CreateRow(rowIndex);dataRow.CreateCell(0).SetCellValue(item.LineName);dataRow.CreateCell(1).SetCellValue(item.UserName);dataRow.CreateCell(2).SetCellValue(item.TotalHours.ToString());dataRow.CreateCell(3).SetCellValue(item.PostNames);dataRow.CreateCell(4).SetCellValue(item.UserLaborAttribute);for (int i = 0, j = 0; i < dateList.Count; i++, j = j + 2){var dateData = item.Details.Where(x => x.Date == dateList[i]).ToList();if (dateData != null && dateData.Count > 0){var dayShift = dateData.FirstOrDefault(x => x.DayShift != null)?.DayShift;var nightShift = dateData.FirstOrDefault(x => x.NightShift != null)?.NightShift;dataRow.CreateCell(fixedColumnCount + j).SetCellValue(dayShift == null ? string.Empty : dayShift.ToString());dataRow.CreateCell(fixedColumnCount + j + 1).SetCellValue(nightShift == null ? string.Empty : nightShift.ToString());}else{dataRow.CreateCell(fixedColumnCount + j).SetCellValue(string.Empty);dataRow.CreateCell(fixedColumnCount + j + 1).SetCellValue(string.Empty);}}//合并产线单元格if (lineCode == item.LineCode){endRowIndex++;}else{if (endRowIndex - startRowIndex > 1){sheet.AddMergedRegion(new CellRangeAddress(startRowIndex, endRowIndex - 1, 0, 0));}startRowIndex = rowIndex;endRowIndex = rowIndex + 1; // 移到下一行lineCode = item.LineCode ?? 0;}rowIndex++;}// 处理最后一组合并if (endRowIndex - startRowIndex > 1){sheet.AddMergedRegion(new CellRangeAddress(startRowIndex, endRowIndex - 1, 0, 0));}#endregion//调整列宽,设置第一列自动宽度//sheet.AutoSizeColumn(0);//写入到内存流//MemoryStream ms = new MemoryStream();//workbook.Write(ms);//ms.Flush();//return ms;var fileName = "人力报工统计";IWebHostEnvironment webHostEnvironment = (IWebHostEnvironment)App.ServiceProvider.GetService(typeof(IWebHostEnvironment));var sFileName = $"{fileName}{DateTime.Now:MM-dd-HHmmss}.xlsx";string fullPath = Path.Combine(webHostEnvironment.WebRootPath, "export", sFileName);Directory.CreateDirectory(Path.GetDirectoryName(fullPath));using (var fileStream = new FileStream(fullPath, FileMode.Create)){workbook.Write(fileStream);}return (sFileName, fullPath);}
查询接口返回实体结构
/// <summary>/// 报表单条产线数据/// </summary>public class MesHumanReportLineResponse : MesHumanReportDetailDto{/// <summary>/// 总工时/// </summary>public decimal? TotalHours { get; set; }/// <summary>/// 每日工时详情/// </summary>public List<MesHumanReportDetailResponse> Details { get; set; }}public class MesHumanReportDetailResponse
{/// <summary>/// 日期/// </summary>public DateTime? Date { get; set; }/// <summary>/// 白班工时/// </summary>public decimal? DayShift { get; set; }/// <summary>/// 夜班工时/// </summary>public decimal? NightShift { get; set; } // 夜班
}/// <summary>
/// 人力报工-明细输入输出对象
/// </summary>
public class MesHumanReportDetailDto
{/// <summary>/// 主键Id /// </summary>[ExcelIgnore]public int Id { get; set; }/// <summary>/// 主表id /// </summary>[ExcelIgnore]public int MasterId { get; set; }/// <summary>/// 车间名称 /// </summary>[ExcelColumn(Name = "车间名称")][ExcelColumnName("车间名称")]public string WorkShopName { get; set; }/// <summary>/// 产线编号 /// </summary>[ExcelColumn(Name = "产线编号")][ExcelColumnName("产线编号")]public int? LineCode { get; set; }/// <summary>/// 产线名称 /// </summary>[ExcelColumn(Name = "产线名称")][ExcelColumnName("产线名称")]public string LineName { get; set; }/// <summary>/// 开始时间 /// </summary>[ExcelColumn(Name = "开始时间", Format = "yyyy-MM-dd HH:mm:ss")][ExcelColumnName("开始时间")]public DateTime? StartTime { get; set; }/// <summary>/// 结束时间 /// </summary>[ExcelColumn(Name = "结束时间", Format = "yyyy-MM-dd HH:mm:ss")][ExcelColumnName("结束时间")]public DateTime? EndTime { get; set; }/// <summary>/// 日期 /// </summary>[ExcelColumn(Name = "日期")][ExcelColumnName("日期")]public DateTime? WorkDate { get; set; }/// <summary>/// 班次 /// </summary>[ExcelColumn(Name = "班次")][ExcelColumnName("班次")]public string WorkShift { get; set; }/// <summary>/// 实际工时(h) /// </summary>[ExcelColumn(Name = "实际工时(h)")][ExcelColumnName("实际工时(h)")]public decimal? ActualWorkHours { get; set; }/// <summary>/// 标准工时(h) /// </summary>[ExcelColumn(Name = "标准工时(h)")][ExcelColumnName("标准工时(h)")]public decimal? StandardWorkHours { get; set; }/// <summary>/// 用户id /// </summary>[ExcelIgnore]public long UserId { get; set; }/// <summary>/// 用户工号 /// </summary>[ExcelColumn(Name = "用户工号")][ExcelColumnName("用户工号")]public string UserCode { get; set; }/// <summary>/// 用户姓名 /// </summary>[ExcelColumn(Name = "用户姓名")][ExcelColumnName("用户姓名")]public string UserName { get; set; }/// <summary>/// 部门id /// </summary>[ExcelIgnore]public long? DeptId { get; set; }/// <summary>/// 部门名称/// </summary>[ExcelIgnore]public string DeptName { get; set; }/// <summary>/// 岗位id (用逗号拼接)/// </summary>[ExcelIgnore]public string PostIds { get; set; }/// <summary>/// 岗位名称 (用逗号拼接)/// </summary>[ExcelColumn(Name = "岗位名称")][ExcelColumnName("岗位名称")]public string PostNames { get; set; }/// <summary>/// 用户劳务属性(合同工、劳务工、第三方)/// </summary>[ExcelIgnore]public string UserLaborAttribute { get; set; }/// <summary>/// 用户划分(分摊、专属)/// </summary>[ExcelIgnore]public string UserDivide { get; set; }/// <summary>/// 创建日期 /// </summary>[ExcelIgnore]public DateTime? CreateTime { get; set; }/// <summary>/// 创建者 /// </summary>[ExcelIgnore]public string CreateBy { get; set; }/// <summary>/// 更新时间 /// </summary>[ExcelIgnore]public DateTime? UpdateTime { get; set; }/// <summary>/// 更新者 /// </summary>[ExcelIgnore]public string UpdateBy { get; set; }/// <summary>/// 备注信息 /// </summary>[ExcelColumn(Name = "备注信息")][ExcelColumnName("备注信息")]public string Remark { get; set; }/// <summary>/// 是否删除(软删除标志) /// </summary>[ExcelIgnore]public bool IsDeleted { get; set; }
}
Service层 查询功能(该部分可以跳过,其中逻辑跟源项目相关,仅做记录)
/// <summary>/// 获取人力报工报表数据/// </summary>/// <param name="parm"></param>/// <returns></returns>public List<MesHumanReportLineResponse> GetMesHumanReportResponse(MesHumanReportQueryDto parm){var resultList = new List<MesHumanReportLineResponse>();try{if (parm.BeginWorkDate == null || parm.EndWorkDate == null){throw new ArgumentException("开始日期和结束日期不能为空!");}#region 条件查询var list = Queryable().Where(x => x.WorkDate >= parm.BeginWorkDate.Value.Date && x.WorkDate <= parm.EndWorkDate.Value.Date && !x.IsDeleted).WhereIF(parm.LineCode != null, x => x.LineCode == parm.LineCode).OrderBy(x => x.WorkDate).Includes(x => x.MesHumanReportDetailNav.Where(w => !w.IsDeleted).ToList()).ToList();var detaiList = list.SelectMany(x => x.MesHumanReportDetailNav).ToList();//根据产线分组var lineGroup = detaiList.GroupBy(x => x.LineCode).ToList();foreach (var item in lineGroup){//根据人员分组var userGroup = item.GroupBy(x => x.UserCode).ToList();foreach (var user in userGroup){var data = new MesHumanReportLineResponse();data.LineCode = item.Key;data.LineName = item.FirstOrDefault().LineName;data.UserCode = user.Key;data.UserId = user.FirstOrDefault().UserId;data.UserName = user.FirstOrDefault().UserName;data.DeptId = user.FirstOrDefault().DeptId;data.DeptName = user.FirstOrDefault().DeptName;data.PostIds = user.FirstOrDefault().PostIds;data.PostNames = user.FirstOrDefault().PostNames;List<MesHumanReportDetailResponse> detailList = new List<MesHumanReportDetailResponse>();data.Details = detailList;data.TotalHours = user.Sum(x => x.ActualWorkHours ?? 0);//根据日期分组var dateGroup = user.GroupBy(x => x.WorkDate).OrderBy(x => x.Key).ToList();foreach (var date in dateGroup){//根据班次分组var shiftGroup = date.GroupBy(x => x.WorkShift).OrderBy(x => x.Key).ToList();foreach (var shift in shiftGroup){//计算总工时var totalHours = shift.Sum(x => x.StandardWorkHours);//计算实际工时var actualHours = shift.Sum(x => x.ActualWorkHours);//计算缺勤工时//var absenceHours = shift.Sum(x => x.StandardWorkHours - x.ActualWorkHours);//计算出勤率//var attendanceRate = (actualHours / totalHours) * 100;//计算缺勤率//var absenceRate = (absenceHours / totalHours) * 100;var detail = new MesHumanReportDetailResponse();detail.Date = date.Key;if (shift.Key == "白班"){detail.DayShift = actualHours;}else{detail.NightShift = actualHours;}detailList.Add(detail);}}resultList.Add(data);}}//return resultList;#endregion}catch (Exception ex){logger.Error(ex, "获取人力报工报表数据异常!");throw ex;}try{//查询智能制造平台用户列表int pageIndex = 1; // 设置页码int pageSize = 9999; // 每页大小var apiResult = HttpHelper.HttpGet($"{_apiUrl}api/common/getUserListByPost?pageNum={pageIndex}&pageSize={pageSize}");//获取apiResult中data中的数据if (!string.IsNullOrEmpty(apiResult)){var json = JObject.Parse(apiResult);if ((int)json["code"] == 200){JArray resultArray = (JArray)json["data"]["result"]; // 👈 直接获取结果数组// 遍历 resultArray,并映射到 SysUserDto 列表var userList = new List<SysUserDto>();foreach (var item in resultArray){var user = new SysUserDto{UserId = (int)item["userId"],UserName = item["userName"].ToString(),NickName = item["nickName"].ToString(),Email = (string)item["email"], // 可能为 nullRemark = (string)item["remark"], // 可能为 nullPhonenumber = (string)item["phonenumber"], // 可能为 nullSex = (int)item["sex"],DeptId = (int)item["deptId"],DeptName = item["deptName"].ToString(),PostNames = item["postNames"].ToString(),UserLaborAttribute = item["userLaborAttribute"].ToString(),UserDivide = item["userDivide"].ToString()// 其他属性...};userList.Add(user);}//用户基础信息映射到报表数据resultList.ForEach(w =>{var user = userList.FirstOrDefault(x => w.UserId == x.UserId || w.UserCode == x.UserName);if (user != null){w.PostNames = user.PostNames;w.UserLaborAttribute = user.UserLaborAttribute;w.UserDivide = user.UserDivide;}});}}return resultList;}catch (Exception ex){logger.Error(ex, "获取智能制造平台用户列表异常!");throw ex;}}
相关文章:
动态表头报表的绘制与导出
目录 一、效果图 二、整体思路 三、代码区 一、效果图 根据选择的日期范围动态生成表头(eg:2025.2.24--2025.03.03)每个日期又分为白班、夜班;数据列表中对产线合并单元格。支持按原格式导出对应的报表excel。 点击空白区可新…...
DeepSeek 助力 Vue3 开发:打造丝滑的密码输入框(Password Input)
前言:哈喽,大家好,今天给大家分享一篇文章!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏关注哦 💕 目录 Deep…...
【解决】OnTriggerEnter/OnTriggerExit 调用匿名委托误区的问题
开发平台:Unity 开发语言:CSharp 6.0 开发工具:Visual Studio 2022 问题背景 public void OnTriggerEnter(Collider collider) {output.OnInteractionNoticed () > OnInteractionTriggered?.Invoke(); }public void OnTriggerExit(C…...
Linux 基础---文件权限
概念 文件权限是针对文件所有者、文件所属组、其他人这三类人而言的,对应的操作是chmod。设置方式:文字设定法、数字设定法。 文字设定法:r,w,x,- 来描述用户对文件的操作权限数字设定法:0,1,2,3,4,5,6,7 来描述用户对文件的操作…...
SpringBoot五:JSR303校验
精心整理了最新的面试资料和简历模板,有需要的可以自行获取 点击前往百度网盘获取 点击前往夸克网盘获取 松散绑定 意思是比如在yaml中写的是last-name,这个和lastName意思是一样的,-后的字母默认是大写的 JSR303校验 就是可以在字段增加…...
【计算机网络】考研复试高频知识点总结
文章目录 一、基础概念1、计算机⽹络的定义2、计算机⽹络的目标3、计算机⽹络的组成4、计算机⽹络的分类5、计算机⽹络的拓扑结构6、计算机⽹络的协议7、计算机⽹络的分层结构8、OSI 参考模型9、TCP/IP 参考模型10、五层协议体系结构 二、物理层1、物理层的功能2、传输媒体3、 …...
Error Density-dependent Empirical Risk Minimization
经验误差密度依赖的风险最小化 v.s. 经验风险最小化 论文: 《 Error Density-dependent Empirical Risk Minimization》 发表在: ESWA’24 相关代码: github.com/zxlml/EDERM 研究背景 传统的经验风险最小化(ERM)方…...
02_NLP文本预处理之文本张量表示法
文本张量表示法 概念 将文本使用张量进行表示,一般将词汇表示为向量,称为词向量,再由各个词向量按顺序组成矩阵形成文本表示 例如: ["人生", "该", "如何", "起头"]># 每个词对应矩阵中的一个向量 [[1.32, 4,32, 0,32, 5.2],[3…...
Spring Boot全局异常处理:“危机公关”团队
目录 一、全局异常处理的作用二、Spring Boot 实现全局异常处理(附上代码实例)三、总结: 🌟我的其他文章也讲解的比较有趣😁,如果喜欢博主的讲解方式,可以多多支持一下,感谢…...
Ubuntu 下 nginx-1.24.0 源码分析 - ngx_list_init
ngx_list_init 定义在 src\core\ngx_list.h static ngx_inline ngx_int_t ngx_list_init(ngx_list_t *list, ngx_pool_t *pool, ngx_uint_t n, size_t size) {list->part.elts ngx_palloc(pool, n * size);if (list->part.elts NULL) {return NGX_ERROR;}list->par…...
C# OnnxRuntime部署DAMO-YOLO香烟检测
目录 说明 效果 模型信息 项目 代码 下载 参考 说明 效果 模型信息 Model Properties ------------------------- --------------------------------------------------------------- Inputs ------------------------- name:input tensor:Floa…...
GitHub开源协议选择指南:如何为你的项目找到最佳“许可证”?
引言 当你站在GitHub仓库创建的十字路口时,是否曾被众多开源协议晃花了眼? 别担心!这篇指南将化身你的"协议导航仪",用一张流程图五个灵魂拷问,帮你轻松找到最佳选择。无论你是开发者、开源爱好者ÿ…...
[密码学实战]Java生成SM2根证书及用户证书
前言 在国密算法体系中,SM2是基于椭圆曲线密码(ECC)的非对称加密算法,广泛应用于数字证书、签名验签等场景。本文将结合代码实现,详细讲解如何通过Java生成SM2根证书及用户证书,并深入分析其核心原理。 一、证书验证 1.代码运行结果 2.根证书验证 3.用户证书验证 二、…...
安装 cnpm 出现 Unsupported URL Type “npm:“: npm:string-width@^4.2.0
Unsupported URL Type "npm:": npm:string-width^4.2.0 可能是 node 版本太低了,需要安装低版本的 cnpm 试试 npm cache clean --force npm config set strict-ssl false npm install -g cnpm --registryhttps://registry.npmmirror.com 改为 npm insta…...
探秘基带算法:从原理到5G时代的通信变革【九】QPSK调制/解调
文章目录 2.8 QPSK 调制 / 解调简介QPSK 发射机的实现与原理QPSK 接收机的实现与原理QPSK 性能仿真QPSK 变体分析 本博客为系列博客,主要讲解各基带算法的原理与应用,包括:viterbi解码、Turbo编解码、Polar编解码、CORDIC算法、CRC校验、FFT/…...
四、数据存储
在爬虫项目中,我们需要将目标站点数据进行持久化保存,一般数据保存的方式有两种: 文件保存数据库保存 在数据保存的过程中需要对数据完成去重操作,所有需要使用 redis 中的 set 数据类型完成去重。 1.CSV文件存储 1.1 什么是c…...
C# OnnxRuntime部署DAMO-YOLO人头检测
目录 说明 效果 模型信息 项目 代码 下载 参考 说明 效果 模型信息 Model Properties ------------------------- --------------------------------------------------------------- Inputs ------------------------- name:input tensor:Floa…...
Metal学习笔记七:片元函数
知道如何通过将顶点数据发送到 vertex 函数来渲染三角形、线条和点是一项非常巧妙的技能 — 尤其是因为您能够使用简单的单行片段函数为形状着色。但是,片段着色器能够执行更多操作。 ➤ 打开网站 https://shadertoy.com,在那里您会发现大量令人眼花缭乱…...
《一个端粒到端粒的参考基因组为木瓜中五环三萜类化合物生物合成提供了遗传学见解》
A telomere-to-telomere reference genome provides genetic insight into the pentacyclic triterpenoid biosynthesis in Chaenomeles speciosa Amplification of transposable elements 转座元件的扩增 Sequence mining disclosed that TEs were one main event in the ex…...
【Mac】2025-MacOS系统下常用的开发环境配置
早期版本的一个环境搭建参考 1、brew Mac自带终端运行: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" Installation successful!成功后运行三行命令后更新环境(xxx是mac的username&a…...
蓝桥杯web第三天
展开扇子题目, #box:hover #item1 { transform:rotate(-60deg); } 当悬浮在父盒子,子元素旋转 webkit display: -webkit-box:将元素设置为弹性伸缩盒子模型。-webkit-box-orient: vertical:设置伸缩盒子的子元素排列方…...
Qt基础入门-详解
前言 qt之路正式开启 💓 个人主页:普通young man-CSDN博客 ⏩ 文章专栏:C_普通young man的博客-CSDN博客 ⏩ 本人giee: 普通小青年 (pu-tong-young-man) - Gitee.com 若有问题 评论区见📝 🎉欢迎大家点赞ὄ…...
FPGA开发,使用Deepseek V3还是R1(3):系统级与RTL级
以下都是Deepseek生成的答案 FPGA开发,使用Deepseek V3还是R1(1):应用场景 FPGA开发,使用Deepseek V3还是R1(2):V3和R1的区别 FPGA开发,使用Deepseek V3还是R1&#x…...
移动端国际化翻译同步解决方案-V3
1.前言 因为软件出海,从在上上家公司就开始做翻译系统,到目前为止已经出了两个比较大的版本了,各个版本解决的痛点如下: V1版本: 主要针对的是AndroidiOS翻译不一致和翻译内容管理麻烦的问题,通过这个工具…...
多空狙击线-新指标-图文教程,多空分界买点以及强弱操盘技术教程,通达信炒股软件指标
“多空狙击线”指标 “多空狙击线”特色指标是量能型技术指标,主要用于分析股票市场中机构做多/做空力量的强程度。该指标的构成、定义与原理如下: “多空狙击线”指标,又称机构做多/做空能量线,通过计算和分析股票市场中机构做多/做空力量…...
零信任架构和传统网络安全模式的
零信任到底是一个什么类型的模型?什么类型的思想或思路,它是如何实现的,我们要做零信任,需要考虑哪些问题? 零信任最早是约翰金德瓦格提出的安全模型。早期这个模型也是因为在安全研究上考虑的一个新的信任式模型。他最…...
Oracle 11g的部署配置
1、进入官网下载所需版本的Oracle 2、安装 ①:选择setup.exe开始安装 ②:安装提示如下,直接忽略,选是 ③:配置安全更新 填写邮箱,并取消勾选 ④:如果点击下一步,提示什么代理啥的…...
下载b站视频音频
文章目录 方案一:jjdown如何使用 方案二:bilibili哔哩哔哩下载助手如何使用进入插件网站插件下载插件安装 使用插件下载视频音频:复制音频下载地址 方案三:bat命令下载单个音频下载单个视频下载单个音视频 方案一:jjdo…...
记录spring-boot 3.X版本整合RocketMq
版本信息 先把该次整合的版本信息列如下: spring-boot spring-cloud rocketmq-spring-boot-starter rocketmq-client rocketmq 3.0.13 2022.0.5 2.2.3 4.9.8 4.9.8 版本信息是如何选择的呢?看rocketMq官网springcloud alibaba版本声明 rock…...
《基于HarmonyOS NEXT API 12+,搭建新闻创作智能写作引擎》
在信息爆炸的时代,新闻行业对于内容生产的效率和质量有着极高的要求。AI技术的发展为新闻创作带来了新的变革契机,借助AI智能写作助手,新闻工作者可以快速生成新闻稿件的初稿,大大提高创作效率。本文将基于HarmonyOS NEXT API 12及…...
