From 599fa05ae6eb8d9e2e36e6d3e45337f887715e04 Mon Sep 17 00:00:00 2001 From: 18631081161 <2088094923@qq.com> Date: Mon, 19 Jan 2026 23:30:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E4=BF=AE=E6=94=B9=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/AllocationsController.cs | 66 ++- .../Controllers/PersonnelController.cs | 465 +++++++++++++++- .../Controllers/StatsController.cs | 83 +++ .../MilitaryTrainingManagement.csproj | 1 + .../Models/DTOs/PersonnelDTOs.cs | 1 + .../Models/DTOs/ReportDTOs.cs | 14 + .../Implementations/AllocationService.cs | 69 ++- .../Services/Interfaces/IAllocationService.cs | 7 +- src/frontend/package.json | 2 + src/frontend/src/api/allocations.ts | 34 +- src/frontend/src/api/index.ts | 2 +- src/frontend/src/api/personnel.ts | 56 +- src/frontend/src/api/stats.ts | 13 + src/frontend/src/layouts/MainLayout.vue | 7 +- src/frontend/src/types/index.ts | 6 + src/frontend/src/views/Dashboard.vue | 177 ++++-- .../src/views/allocations/AllocationList.vue | 140 ++++- .../views/organizations/OrganizationList.vue | 81 ++- .../src/views/personnel/PersonnelDetail.vue | 1 + .../src/views/personnel/PersonnelForm.vue | 6 +- .../src/views/personnel/PersonnelList.vue | 523 ++++++++++++++---- 21 files changed, 1526 insertions(+), 228 deletions(-) diff --git a/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs b/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs index 1c85689..66c80a5 100644 --- a/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs +++ b/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs @@ -507,7 +507,7 @@ public class AllocationsController : BaseApiController /// 获取按单位汇总的上报数据 /// [HttpGet("distributions/{distributionId}/summary")] - public async Task GetReportSummaryByUnit(int distributionId) + public async Task GetReportSummaryByUnit(int distributionId, [FromQuery] string? period = null) { var unitId = GetCurrentUnitId(); var unitLevel = GetCurrentUnitLevel(); @@ -520,12 +520,74 @@ public class AllocationsController : BaseApiController if (distribution == null) return NotFound(new { message = "配额分配记录不存在" }); + // 计算时间范围 + DateTime? startDate = null; + DateTime? endDate = null; + if (!string.IsNullOrEmpty(period) && period != "all") + { + endDate = DateTime.Now; + startDate = period switch + { + "week" => endDate.Value.AddDays(-7), + "month" => endDate.Value.AddMonths(-1), + "halfYear" => endDate.Value.AddMonths(-6), + "year" => endDate.Value.AddYears(-1), + _ => null + }; + } + // 获取按单位汇总的上报数据 - var summaries = await _allocationService.GetReportSummaryByUnitAsync(distributionId, unitId.Value, unitLevel.Value); + var summaries = await _allocationService.GetReportSummaryByUnitAsync(distributionId, unitId.Value, unitLevel.Value, startDate, endDate); return Ok(summaries); } + /// + /// 获取配额的消耗统计(按时间范围) + /// 师部账号可以按周、月、半年、年度查看所有团的总消耗 + /// + [HttpGet("{id}/consumption-stats")] + [Authorize(Policy = "DivisionLevel")] + public async Task GetConsumptionStats(int id, [FromQuery] string period = "month") + { + var unitId = GetCurrentUnitId(); + var unitLevel = GetCurrentUnitLevel(); + + if (unitId == null || unitLevel == null) + return Unauthorized(new { message = "无法获取用户组织信息" }); + + // 检查配额是否存在 + var allocation = await _allocationService.GetByIdAsync(id); + if (allocation == null) + return NotFound(new { message = "配额不存在" }); + + // 计算时间范围 + var endDate = DateTime.Now; + var startDate = period switch + { + "week" => endDate.AddDays(-7), + "month" => endDate.AddMonths(-1), + "halfYear" => endDate.AddMonths(-6), + "year" => endDate.AddYears(-1), + _ => endDate.AddMonths(-1) + }; + + // 获取按单位汇总的消耗统计 + var stats = await _allocationService.GetConsumptionStatsByPeriodAsync(id, startDate, endDate); + + return Ok(new + { + allocationId = id, + materialName = allocation.MaterialName, + unit = allocation.Unit, + totalQuota = allocation.TotalQuota, + period, + startDate, + endDate, + unitStats = stats + }); + } + /// /// 映射实体到响应DTO /// diff --git a/src/MilitaryTrainingManagement/Controllers/PersonnelController.cs b/src/MilitaryTrainingManagement/Controllers/PersonnelController.cs index 1da2269..f65a333 100644 --- a/src/MilitaryTrainingManagement/Controllers/PersonnelController.cs +++ b/src/MilitaryTrainingManagement/Controllers/PersonnelController.cs @@ -72,23 +72,65 @@ public class PersonnelController : BaseApiController public async Task GetPersonnel( [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10, - [FromQuery] string? status = null) + [FromQuery] string? status = null, + [FromQuery] int? unitId = null, + [FromQuery] string? position = null, + [FromQuery] string? hasTraining = null, + [FromQuery] string? hasAchievements = null) { - var unitId = GetCurrentUnitId(); + var currentUnitId = GetCurrentUnitId(); var userLevel = GetCurrentUnitLevel(); - if (unitId == null || userLevel == null) + if (currentUnitId == null || userLevel == null) { return Unauthorized(); } - var allPersonnel = await _personnelService.GetVisiblePersonnelAsync(unitId.Value, userLevel.Value); + var allPersonnel = await _personnelService.GetVisiblePersonnelAsync(currentUnitId.Value, userLevel.Value); // 状态筛选 if (!string.IsNullOrEmpty(status) && Enum.TryParse(status, out var statusFilter)) { allPersonnel = allPersonnel.Where(p => p.Status == statusFilter); } + + // 单位筛选 + if (unitId.HasValue) + { + allPersonnel = allPersonnel.Where(p => p.SubmittedByUnitId == unitId.Value); + } + + // 专业岗位筛选 + if (!string.IsNullOrEmpty(position)) + { + allPersonnel = allPersonnel.Where(p => p.Position.Contains(position)); + } + + // 是否参加过培训筛选 + if (!string.IsNullOrEmpty(hasTraining)) + { + if (hasTraining == "yes") + { + allPersonnel = allPersonnel.Where(p => !string.IsNullOrEmpty(p.TrainingParticipation)); + } + else if (hasTraining == "no") + { + allPersonnel = allPersonnel.Where(p => string.IsNullOrEmpty(p.TrainingParticipation)); + } + } + + // 是否有取得成绩筛选 + if (!string.IsNullOrEmpty(hasAchievements)) + { + if (hasAchievements == "yes") + { + allPersonnel = allPersonnel.Where(p => !string.IsNullOrEmpty(p.Achievements)); + } + else if (hasAchievements == "no") + { + allPersonnel = allPersonnel.Where(p => string.IsNullOrEmpty(p.Achievements)); + } + } var totalCount = allPersonnel.Count(); var items = allPersonnel @@ -107,6 +149,44 @@ public class PersonnelController : BaseApiController }); } + /// + /// 获取筛选选项(单位列表、岗位列表) + /// + [HttpGet("filter-options")] + public async Task GetFilterOptions() + { + var currentUnitId = GetCurrentUnitId(); + var userLevel = GetCurrentUnitLevel(); + + if (currentUnitId == null || userLevel == null) + { + return Unauthorized(); + } + + var allPersonnel = await _personnelService.GetVisiblePersonnelAsync(currentUnitId.Value, userLevel.Value); + + // 获取所有可见的单位 + var units = allPersonnel + .Select(p => new { id = p.SubmittedByUnitId, name = p.SubmittedByUnit?.Name ?? "" }) + .Distinct() + .OrderBy(u => u.name) + .ToList(); + + // 获取所有岗位 + var positions = allPersonnel + .Select(p => p.Position) + .Where(p => !string.IsNullOrEmpty(p)) + .Distinct() + .OrderBy(p => p) + .ToList(); + + return Ok(new + { + units, + positions + }); + } + /// /// 映射人员实体到响应DTO /// @@ -117,6 +197,7 @@ public class PersonnelController : BaseApiController Id = personnel.Id, Name = personnel.Name, PhotoPath = personnel.PhotoPath, + Unit = personnel.Unit, Position = personnel.Position, Rank = personnel.Rank, Gender = personnel.Gender, @@ -1014,4 +1095,380 @@ public class PersonnelController : BaseApiController return BadRequest(new { message = ex.Message }); } } + + /// + /// 导出人才列表到Excel + /// + [HttpGet("export")] + public async Task ExportToExcel( + [FromQuery] string? status = null, + [FromQuery] int? unitId = null, + [FromQuery] string? position = null, + [FromQuery] string? hasTraining = null, + [FromQuery] string? hasAchievements = null) + { + var currentUnitId = GetCurrentUnitId(); + var userLevel = GetCurrentUnitLevel(); + + if (currentUnitId == null || userLevel == null) + { + return Unauthorized(); + } + + var allPersonnel = await _personnelService.GetVisiblePersonnelAsync(currentUnitId.Value, userLevel.Value); + + // 应用筛选条件 + if (!string.IsNullOrEmpty(status) && Enum.TryParse(status, out var statusFilter)) + { + allPersonnel = allPersonnel.Where(p => p.Status == statusFilter); + } + if (unitId.HasValue) + { + allPersonnel = allPersonnel.Where(p => p.SubmittedByUnitId == unitId.Value); + } + if (!string.IsNullOrEmpty(position)) + { + allPersonnel = allPersonnel.Where(p => p.Position.Contains(position)); + } + if (!string.IsNullOrEmpty(hasTraining)) + { + if (hasTraining == "yes") + allPersonnel = allPersonnel.Where(p => !string.IsNullOrEmpty(p.TrainingParticipation)); + else if (hasTraining == "no") + allPersonnel = allPersonnel.Where(p => string.IsNullOrEmpty(p.TrainingParticipation)); + } + if (!string.IsNullOrEmpty(hasAchievements)) + { + if (hasAchievements == "yes") + allPersonnel = allPersonnel.Where(p => !string.IsNullOrEmpty(p.Achievements)); + else if (hasAchievements == "no") + allPersonnel = allPersonnel.Where(p => string.IsNullOrEmpty(p.Achievements)); + } + + var personnelList = allPersonnel.ToList(); + + using var workbook = new ClosedXML.Excel.XLWorkbook(); + var worksheet = workbook.Worksheets.Add("人才数据库"); + + // 标题行 + worksheet.Cell(1, 1).Value = "人才数据库"; + worksheet.Range(1, 1, 1, 17).Merge(); + worksheet.Cell(1, 1).Style.Font.Bold = true; + worksheet.Cell(1, 1).Style.Font.FontSize = 18; + worksheet.Cell(1, 1).Style.Alignment.Horizontal = ClosedXML.Excel.XLAlignmentHorizontalValues.Center; + + // 统计更新时间 + worksheet.Cell(2, 1).Value = $"统计更新时间:{DateTime.Now:yyyy.M.d}"; + worksheet.Range(2, 1, 2, 4).Merge(); + + // 设置表头(第3行) + var headers = new[] { "序号", "姓名", "照片", "单位", "部职别", "军衔", + "士兵证号", "专业岗位", "政治面貌", "文化程度", "民族", "籍贯", + "出生年月", "入伍年月", "特长", "参加培训、集训情况", "取得成绩(成果)" }; + + for (int i = 0; i < headers.Length; i++) + { + var cell = worksheet.Cell(3, i + 1); + cell.Value = headers[i]; + cell.Style.Font.Bold = true; + cell.Style.Fill.BackgroundColor = ClosedXML.Excel.XLColor.LightGray; + cell.Style.Alignment.Horizontal = ClosedXML.Excel.XLAlignmentHorizontalValues.Center; + cell.Style.Alignment.Vertical = ClosedXML.Excel.XLAlignmentVerticalValues.Center; + cell.Style.Border.OutsideBorder = ClosedXML.Excel.XLBorderStyleValues.Thin; + } + + // 填充数据(从第4行开始) + int row = 4; + int seq = 1; + foreach (var p in personnelList) + { + worksheet.Cell(row, 1).Value = seq++; + worksheet.Cell(row, 2).Value = p.Name; + worksheet.Cell(row, 3).Value = ""; // 照片列留空 + worksheet.Cell(row, 4).Value = p.Unit ?? p.SubmittedByUnit?.Name ?? ""; + worksheet.Cell(row, 5).Value = p.Position; + worksheet.Cell(row, 6).Value = p.Rank; + worksheet.Cell(row, 7).Value = p.IdNumber ?? ""; + worksheet.Cell(row, 8).Value = p.ProfessionalTitle ?? ""; // 专业岗位 + worksheet.Cell(row, 9).Value = p.PoliticalStatus ?? ""; + worksheet.Cell(row, 10).Value = p.EducationLevel ?? ""; + worksheet.Cell(row, 11).Value = p.Ethnicity ?? ""; + worksheet.Cell(row, 12).Value = p.Hometown ?? ""; + worksheet.Cell(row, 13).Value = p.BirthDate ?? ""; + worksheet.Cell(row, 14).Value = p.EnlistmentDate ?? ""; + worksheet.Cell(row, 15).Value = p.Specialty ?? ""; // 特长 + worksheet.Cell(row, 16).Value = p.TrainingParticipation ?? ""; + worksheet.Cell(row, 17).Value = p.Achievements ?? ""; + + // 设置边框 + for (int col = 1; col <= 17; col++) + { + worksheet.Cell(row, col).Style.Border.OutsideBorder = ClosedXML.Excel.XLBorderStyleValues.Thin; + } + row++; + } + + // 自动调整列宽 + worksheet.Columns().AdjustToContents(); + foreach (var column in worksheet.ColumnsUsed()) + { + if (column.Width < 10) + column.Width = 10; + else if (column.Width > 30) + column.Width = 30; + } + worksheet.Row(3).Height = 35; + + using var stream = new MemoryStream(); + workbook.SaveAs(stream); + stream.Position = 0; + + var fileName = $"人才列表_{DateTime.Now:yyyyMMddHHmmss}.xlsx"; + return File(stream.ToArray(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName); + } + + /// + /// 下载导入模板 + /// + [HttpGet("import-template")] + public IActionResult DownloadImportTemplate() + { + using var workbook = new ClosedXML.Excel.XLWorkbook(); + var worksheet = workbook.Worksheets.Add("人才数据库"); + + // 标题行 + worksheet.Cell(1, 1).Value = "人才数据库"; + worksheet.Range(1, 1, 1, 16).Merge(); + worksheet.Cell(1, 1).Style.Font.Bold = true; + worksheet.Cell(1, 1).Style.Font.FontSize = 18; + worksheet.Cell(1, 1).Style.Alignment.Horizontal = ClosedXML.Excel.XLAlignmentHorizontalValues.Center; + + // 统计更新时间 + worksheet.Cell(2, 1).Value = $"统计更新时间:{DateTime.Now:yyyy.M.d}"; + worksheet.Range(2, 1, 2, 4).Merge(); + + // 设置表头(第3行) + var headers = new[] { "序号", "姓名*", "照片(2寸以内)", "单位", "部职别*", "军衔*", + "士兵证号", "专业岗位*", "政治面貌", "文化程度", "民族", "籍贯", + "出生年月", "入伍年月", "特长", "参加培训、集训情况", "取得成绩(成果)可手填、可上传照片" }; + + for (int i = 0; i < headers.Length; i++) + { + var cell = worksheet.Cell(3, i + 1); + cell.Value = headers[i]; + cell.Style.Font.Bold = true; + cell.Style.Fill.BackgroundColor = ClosedXML.Excel.XLColor.LightGray; + cell.Style.Alignment.Horizontal = ClosedXML.Excel.XLAlignmentHorizontalValues.Center; + cell.Style.Alignment.Vertical = ClosedXML.Excel.XLAlignmentVerticalValues.Center; + cell.Style.Alignment.WrapText = true; + cell.Style.Border.OutsideBorder = ClosedXML.Excel.XLBorderStyleValues.Thin; + } + + // 添加示例数据(第4行) + worksheet.Cell(4, 1).Value = 1; + worksheet.Cell(4, 2).Value = "张三"; + worksheet.Cell(4, 3).Value = ""; + worksheet.Cell(4, 4).Value = "一营一连"; + worksheet.Cell(4, 5).Value = "班长"; + worksheet.Cell(4, 6).Value = "上士"; + worksheet.Cell(4, 7).Value = ""; + worksheet.Cell(4, 8).Value = "通信"; + worksheet.Cell(4, 9).Value = "党员"; + worksheet.Cell(4, 10).Value = "大专"; + worksheet.Cell(4, 11).Value = "汉族"; + worksheet.Cell(4, 12).Value = "北京市"; + worksheet.Cell(4, 13).Value = "1995-06"; + worksheet.Cell(4, 14).Value = "2015-09"; + worksheet.Cell(4, 15).Value = "计算机"; + worksheet.Cell(4, 16).Value = "参加过2023年度集训"; + worksheet.Cell(4, 17).Value = "获得优秀学员称号"; + + // 设置数据行边框 + for (int col = 1; col <= 17; col++) + { + worksheet.Cell(4, col).Style.Border.OutsideBorder = ClosedXML.Excel.XLBorderStyleValues.Thin; + } + + // 添加说明 + var noteSheet = workbook.Worksheets.Add("填写说明"); + noteSheet.Cell(1, 1).Value = "填写说明"; + noteSheet.Cell(1, 1).Style.Font.Bold = true; + noteSheet.Cell(2, 1).Value = "1. 带*号的字段为必填项(姓名、部职别、军衔、专业岗位)"; + noteSheet.Cell(3, 1).Value = "2. 照片列可留空,后续在系统中上传"; + noteSheet.Cell(4, 1).Value = "3. 出生年月格式:YYYY-MM,如 1995-06"; + noteSheet.Cell(5, 1).Value = "4. 入伍年月格式:YYYY-MM,如 2015-09"; + noteSheet.Cell(6, 1).Value = "5. 第3行为表头,请从第4行开始填写数据"; + noteSheet.Cell(7, 1).Value = "6. 示例数据行可以删除或覆盖"; + noteSheet.Cell(8, 1).Value = "7. 序号列可留空,系统会自动生成"; + + // 自动调整列宽 + worksheet.Columns().AdjustToContents(); + foreach (var column in worksheet.ColumnsUsed()) + { + if (column.Width < 10) + column.Width = 10; + else if (column.Width > 25) + column.Width = 25; + } + worksheet.Row(3).Height = 40; // 表头行高 + noteSheet.Columns().AdjustToContents(); + noteSheet.Column(1).Width = 50; + + using var stream = new MemoryStream(); + workbook.SaveAs(stream); + stream.Position = 0; + + return File(stream.ToArray(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "人才导入模板.xlsx"); + } + + /// + /// 从Excel导入人才 + /// + [HttpPost("import")] + public async Task ImportFromExcel(IFormFile file) + { + if (file == null || file.Length == 0) + { + return BadRequest(new { message = "请选择要导入的文件" }); + } + + var extension = Path.GetExtension(file.FileName).ToLowerInvariant(); + if (extension != ".xlsx" && extension != ".xls") + { + return BadRequest(new { message = "只支持Excel文件格式(.xlsx, .xls)" }); + } + + var unitId = GetCurrentUnitId(); + var userId = GetCurrentUserId(); + if (unitId == null || userId == null) + { + return Unauthorized(); + } + + var importResults = new List(); + var successCount = 0; + var failCount = 0; + + try + { + using var stream = file.OpenReadStream(); + using var workbook = new ClosedXML.Excel.XLWorkbook(stream); + var worksheet = workbook.Worksheet(1); + + // 跳过标题行(1)、时间行(2)、表头行(3),从第4行开始读取数据 + var rows = worksheet.RowsUsed().Where(r => r.RowNumber() >= 4); + + foreach (var row in rows) + { + try + { + // 新模板列顺序:序号(1), 姓名(2), 照片(3), 单位(4), 部职别(5), 军衔(6), + // 士兵证号(7), 专业岗位(8), 政治面貌(9), 文化程度(10), 民族(11), 籍贯(12), + // 出生年月(13), 入伍年月(14), 特长(15), 培训情况(16), 取得成绩(17) + + var name = row.Cell(2).GetString().Trim(); + var unit = GetNullIfEmpty(row.Cell(4).GetString()); + var position = row.Cell(5).GetString().Trim(); + var rank = row.Cell(6).GetString().Trim(); + var professionalTitle = row.Cell(8).GetString().Trim(); // 专业岗位 + + // 验证必填字段(姓名、部职别、军衔、专业岗位) + if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(position) || + string.IsNullOrEmpty(rank) || string.IsNullOrEmpty(professionalTitle)) + { + failCount++; + importResults.Add(new { row = row.RowNumber(), success = false, message = "必填字段不能为空(姓名、部职别、军衔、专业岗位)" }); + continue; + } + + var birthDate = GetNullIfEmpty(row.Cell(13).GetString()); + var age = CalculateAge(birthDate); + + var personnel = new Personnel + { + Name = name, + Gender = "男", // 默认性别,模板中没有性别列 + Age = age, + IdNumber = GetNullIfEmpty(row.Cell(7).GetString()) ?? "", // 士兵证号 + Unit = unit, + Position = position, + Rank = rank, + ProfessionalTitle = professionalTitle, // 专业岗位 + Specialty = GetNullIfEmpty(row.Cell(15).GetString()), // 特长 + PoliticalStatus = GetNullIfEmpty(row.Cell(9).GetString()), + EducationLevel = GetNullIfEmpty(row.Cell(10).GetString()), + Ethnicity = GetNullIfEmpty(row.Cell(11).GetString()), + Hometown = GetNullIfEmpty(row.Cell(12).GetString()), + BirthDate = birthDate, + EnlistmentDate = GetNullIfEmpty(row.Cell(14).GetString()), + TrainingParticipation = GetNullIfEmpty(row.Cell(16).GetString()), + Achievements = GetNullIfEmpty(row.Cell(17).GetString()), + SubmittedByUnitId = unitId.Value, + Status = PersonnelStatus.Pending, + SubmittedAt = DateTime.UtcNow + }; + + await _personnelService.CreateAsync(personnel); + successCount++; + importResults.Add(new { row = row.RowNumber(), success = true, name = name }); + } + catch (Exception ex) + { + failCount++; + importResults.Add(new { row = row.RowNumber(), success = false, message = ex.Message }); + } + } + + // 记录审计日志 + await _auditService.LogAsync( + "Personnel", + 0, + "Import", + null, + $"批量导入人才:成功{successCount}条,失败{failCount}条", + userId, + GetClientIpAddress()); + + return Ok(new + { + successCount, + failCount, + results = importResults + }); + } + catch (Exception ex) + { + return BadRequest(new { message = $"导入失败:{ex.Message}" }); + } + } + + private static string GetLevelName(PersonnelLevel? level) + { + return level switch + { + PersonnelLevel.Division => "师级", + PersonnelLevel.Regiment => "团级", + PersonnelLevel.Battalion => "营级", + PersonnelLevel.Company => "连级", + _ => "" + }; + } + + private static string GetStatusName(PersonnelStatus status) + { + return status switch + { + PersonnelStatus.Pending => "待审批", + PersonnelStatus.Approved => "已批准", + PersonnelStatus.Rejected => "已拒绝", + _ => "" + }; + } + + private static string? GetNullIfEmpty(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + return value.Trim(); + } } diff --git a/src/MilitaryTrainingManagement/Controllers/StatsController.cs b/src/MilitaryTrainingManagement/Controllers/StatsController.cs index 7b8f169..d56baf7 100644 --- a/src/MilitaryTrainingManagement/Controllers/StatsController.cs +++ b/src/MilitaryTrainingManagement/Controllers/StatsController.cs @@ -96,6 +96,89 @@ public class StatsController : BaseApiController }); } + /// + /// 获取各团物资配额统计(饼状图数据) + /// + [HttpGet("regiment-allocations")] + public async Task GetRegimentAllocationsStats() + { + var unitId = GetCurrentUnitId(); + var unitLevel = GetCurrentUnitLevel(); + + if (unitId == null || unitLevel == null) + return Unauthorized(new { message = "无法获取用户组织信息" }); + + // 获取所有团级单位 + List regimentIds; + if (unitLevel == OrganizationalLevel.Division) + { + // 师团级:获取所有下属团 + regimentIds = await _context.OrganizationalUnits + .Where(u => u.ParentId == unitId.Value && u.Level == OrganizationalLevel.Regiment) + .Select(u => u.Id) + .ToListAsync(); + } + else if (unitLevel == OrganizationalLevel.Regiment) + { + // 团级:只显示自己 + regimentIds = new List { unitId.Value }; + } + else + { + // 营部及以下:获取所属团 + var currentUnit = await _context.OrganizationalUnits.FindAsync(unitId.Value); + if (currentUnit == null) + return Ok(new List()); + + // 向上查找团级单位 + var parentUnit = currentUnit; + while (parentUnit != null && parentUnit.Level != OrganizationalLevel.Regiment) + { + if (parentUnit.ParentId == null) break; + parentUnit = await _context.OrganizationalUnits.FindAsync(parentUnit.ParentId); + } + + if (parentUnit?.Level == OrganizationalLevel.Regiment) + { + regimentIds = new List { parentUnit.Id }; + } + else + { + return Ok(new List()); + } + } + + var result = new List(); + + foreach (var regimentId in regimentIds) + { + var regiment = await _context.OrganizationalUnits.FindAsync(regimentId); + if (regiment == null) continue; + + // 获取该团及下级单位的所有ID + var unitIds = await GetUnitAndSubordinateIds(regimentId); + + // 获取分配给该团及下级单位的所有配额 + var distributions = await _context.AllocationDistributions + .Where(d => unitIds.Contains(d.TargetUnitId)) + .ToListAsync(); + + var totalQuota = distributions.Sum(d => d.UnitQuota); + var totalConsumed = distributions.Sum(d => d.ActualCompletion ?? 0); + + result.Add(new + { + regimentId = regimentId, + regimentName = regiment.Name, + totalQuota = totalQuota, + totalConsumed = totalConsumed, + percentage = totalQuota > 0 ? Math.Round((totalConsumed / totalQuota) * 100, 1) : 0 + }); + } + + return Ok(result); + } + private async Task> GetUnitAndSubordinateIds(int unitId) { var result = new List { unitId }; diff --git a/src/MilitaryTrainingManagement/MilitaryTrainingManagement.csproj b/src/MilitaryTrainingManagement/MilitaryTrainingManagement.csproj index 4bb021a..c16d089 100644 --- a/src/MilitaryTrainingManagement/MilitaryTrainingManagement.csproj +++ b/src/MilitaryTrainingManagement/MilitaryTrainingManagement.csproj @@ -9,6 +9,7 @@ + diff --git a/src/MilitaryTrainingManagement/Models/DTOs/PersonnelDTOs.cs b/src/MilitaryTrainingManagement/Models/DTOs/PersonnelDTOs.cs index 1e09331..40c9927 100644 --- a/src/MilitaryTrainingManagement/Models/DTOs/PersonnelDTOs.cs +++ b/src/MilitaryTrainingManagement/Models/DTOs/PersonnelDTOs.cs @@ -140,6 +140,7 @@ public class PersonnelResponse public int Id { get; set; } public string Name { get; set; } = string.Empty; public string? PhotoPath { get; set; } + public string? Unit { get; set; } public string Position { get; set; } = string.Empty; public string Rank { get; set; } = string.Empty; public string Gender { get; set; } = string.Empty; diff --git a/src/MilitaryTrainingManagement/Models/DTOs/ReportDTOs.cs b/src/MilitaryTrainingManagement/Models/DTOs/ReportDTOs.cs index 01d4cb5..9981358 100644 --- a/src/MilitaryTrainingManagement/Models/DTOs/ReportDTOs.cs +++ b/src/MilitaryTrainingManagement/Models/DTOs/ReportDTOs.cs @@ -117,3 +117,17 @@ public class SubordinateUnitSummary public decimal TotalActualCompletion { get; set; } public decimal CompletionRate { get; set; } } + +/// +/// 单位消耗统计(按时间范围) +/// +public class UnitConsumptionStat +{ + public int UnitId { get; set; } + public string UnitName { get; set; } = string.Empty; + public string UnitLevel { get; set; } = string.Empty; + public decimal UnitQuota { get; set; } + public decimal TotalConsumed { get; set; } + public decimal CompletionRate { get; set; } + public int ReportCount { get; set; } +} diff --git a/src/MilitaryTrainingManagement/Services/Implementations/AllocationService.cs b/src/MilitaryTrainingManagement/Services/Implementations/AllocationService.cs index 28418dc..c2ac9bf 100644 --- a/src/MilitaryTrainingManagement/Services/Implementations/AllocationService.cs +++ b/src/MilitaryTrainingManagement/Services/Implementations/AllocationService.cs @@ -456,14 +456,25 @@ public class AllocationService : IAllocationService /// /// 获取按单位汇总的上报数据(带可见性过滤) /// - public async Task> GetReportSummaryByUnitAsync(int distributionId, int userUnitId, Models.Enums.OrganizationalLevel userLevel) + public async Task> GetReportSummaryByUnitAsync(int distributionId, int userUnitId, Models.Enums.OrganizationalLevel userLevel, DateTime? startDate = null, DateTime? endDate = null) { // 获取所有上报记录(不做可见性过滤,因为师团级需要看到所有下级的汇总) - var allReports = await _context.ConsumptionReports + var query = _context.ConsumptionReports .Include(r => r.ReportedByUser) .ThenInclude(u => u.OrganizationalUnit) - .Where(r => r.AllocationDistributionId == distributionId) - .ToListAsync(); + .Where(r => r.AllocationDistributionId == distributionId); + + // 按时间范围过滤 + if (startDate.HasValue) + { + query = query.Where(r => r.ReportedAt >= startDate.Value); + } + if (endDate.HasValue) + { + query = query.Where(r => r.ReportedAt <= endDate.Value); + } + + var allReports = await query.ToListAsync(); // 按单位分组汇总 var summaries = allReports @@ -486,4 +497,54 @@ public class AllocationService : IAllocationService return summaries; } + + /// + /// 获取配额的消耗统计(按时间范围) + /// + public async Task> GetConsumptionStatsByPeriodAsync(int allocationId, DateTime startDate, DateTime endDate) + { + // 获取配额及其分配记录 + var allocation = await _context.MaterialAllocations + .Include(a => a.Distributions) + .ThenInclude(d => d.TargetUnit) + .FirstOrDefaultAsync(a => a.Id == allocationId); + + if (allocation == null) + { + return Enumerable.Empty(); + } + + // 获取所有分配记录的ID + var distributionIds = allocation.Distributions.Select(d => d.Id).ToList(); + + // 获取指定时间范围内的上报记录 + var reports = await _context.ConsumptionReports + .Include(r => r.AllocationDistribution) + .ThenInclude(d => d.TargetUnit) + .Where(r => distributionIds.Contains(r.AllocationDistributionId) + && r.ReportedAt >= startDate + && r.ReportedAt <= endDate) + .ToListAsync(); + + // 按目标单位(团)分组汇总 + var stats = allocation.Distributions + .Select(d => { + var unitReports = reports.Where(r => r.AllocationDistributionId == d.Id).ToList(); + var totalConsumed = unitReports.Sum(r => r.ReportedAmount); + return new UnitConsumptionStat + { + UnitId = d.TargetUnitId, + UnitName = d.TargetUnit?.Name ?? string.Empty, + UnitLevel = d.TargetUnit?.Level.ToString() ?? string.Empty, + UnitQuota = d.UnitQuota, + TotalConsumed = totalConsumed, + CompletionRate = d.UnitQuota > 0 ? totalConsumed / d.UnitQuota : 0, + ReportCount = unitReports.Count + }; + }) + .OrderByDescending(s => s.TotalConsumed) + .ToList(); + + return stats; + } } diff --git a/src/MilitaryTrainingManagement/Services/Interfaces/IAllocationService.cs b/src/MilitaryTrainingManagement/Services/Interfaces/IAllocationService.cs index 321fc86..6c5d923 100644 --- a/src/MilitaryTrainingManagement/Services/Interfaces/IAllocationService.cs +++ b/src/MilitaryTrainingManagement/Services/Interfaces/IAllocationService.cs @@ -99,5 +99,10 @@ public interface IAllocationService /// /// 获取按单位汇总的上报数据(带可见性过滤) /// - Task> GetReportSummaryByUnitAsync(int distributionId, int userUnitId, OrganizationalLevel userLevel); + Task> GetReportSummaryByUnitAsync(int distributionId, int userUnitId, OrganizationalLevel userLevel, DateTime? startDate = null, DateTime? endDate = null); + + /// + /// 获取配额的消耗统计(按时间范围) + /// + Task> GetConsumptionStatsByPeriodAsync(int allocationId, DateTime startDate, DateTime endDate); } diff --git a/src/frontend/package.json b/src/frontend/package.json index cf80004..85eaeec 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -13,9 +13,11 @@ }, "dependencies": { "axios": "^1.7.9", + "echarts": "^6.0.0", "element-plus": "^2.9.1", "pinia": "^2.3.0", "vue": "^3.5.13", + "vue-echarts": "^8.0.1", "vue-router": "^4.5.0" }, "devDependencies": { diff --git a/src/frontend/src/api/allocations.ts b/src/frontend/src/api/allocations.ts index 1ba314c..baf88a6 100644 --- a/src/frontend/src/api/allocations.ts +++ b/src/frontend/src/api/allocations.ts @@ -53,8 +53,17 @@ export const allocationsApi = { return response.data }, - async getReportSummaryByUnit(distributionId: number): Promise { - const response = await apiClient.get(`/allocations/distributions/${distributionId}/summary`) + async getReportSummaryByUnit(distributionId: number, period?: string): Promise { + const response = await apiClient.get(`/allocations/distributions/${distributionId}/summary`, { + params: period ? { period } : undefined + }) + return response.data + }, + + async getConsumptionStats(allocationId: number, period: string): Promise { + const response = await apiClient.get(`/allocations/${allocationId}/consumption-stats`, { + params: { period } + }) return response.data } } @@ -78,6 +87,27 @@ export interface UnitReportSummary { lastReportedAt?: string } +export interface UnitConsumptionStat { + unitId: number + unitName: string + unitLevel: string + unitQuota: number + totalConsumed: number + completionRate: number + reportCount: number +} + +export interface ConsumptionStatsResponse { + allocationId: number + materialName: string + unit: string + totalQuota: number + period: string + startDate: string + endDate: string + unitStats: UnitConsumptionStat[] +} + // 消耗记录删改申请相关 export interface ChangeRequest { id: number diff --git a/src/frontend/src/api/index.ts b/src/frontend/src/api/index.ts index 13353ad..6b91e51 100644 --- a/src/frontend/src/api/index.ts +++ b/src/frontend/src/api/index.ts @@ -1,6 +1,6 @@ export { authApi } from './auth' export { organizationsApi } from './organizations' -export { allocationsApi, changeRequestsApi, type UnitReportSummary, type ConsumptionReport, type ChangeRequest } from './allocations' +export { allocationsApi, changeRequestsApi, type UnitReportSummary, type UnitConsumptionStat, type ConsumptionStatsResponse, type ConsumptionReport, type ChangeRequest } from './allocations' export { materialCategoriesApi } from './materialCategories' export { reportsApi } from './reports' export { personnelApi } from './personnel' diff --git a/src/frontend/src/api/personnel.ts b/src/frontend/src/api/personnel.ts index 174b3e6..d3978d4 100644 --- a/src/frontend/src/api/personnel.ts +++ b/src/frontend/src/api/personnel.ts @@ -6,12 +6,30 @@ import type { PaginationParams } from '@/types' +export interface PersonnelFilterOptions { + units: { id: number; name: string }[] + positions: string[] +} + +export interface PersonnelQueryParams extends PaginationParams { + status?: string + unitId?: number + position?: string + hasTraining?: string + hasAchievements?: string +} + export const personnelApi = { - async getAll(params?: PaginationParams & { status?: string }): Promise> { + async getAll(params?: PersonnelQueryParams): Promise> { const response = await apiClient.get>('/personnel', { params }) return response.data }, + async getFilterOptions(): Promise { + const response = await apiClient.get('/personnel/filter-options') + return response.data + }, + async getById(id: number): Promise { const response = await apiClient.get(`/personnel/${id}`) return response.data @@ -93,9 +111,45 @@ export const personnelApi = { async getApprovalHistory(personnelId: number): Promise { const response = await apiClient.get(`/personnel/${personnelId}/approval-history`) return response.data + }, + + // 导出Excel + async exportExcel(params?: PersonnelQueryParams): Promise { + const response = await apiClient.get('/personnel/export', { + params, + responseType: 'blob' + }) + return response.data + }, + + // 下载导入模板 + async downloadImportTemplate(): Promise { + const response = await apiClient.get('/personnel/import-template', { + responseType: 'blob' + }) + return response.data + }, + + // 导入Excel + async importExcel(file: File): Promise { + const formData = new FormData() + formData.append('file', file) + const response = await apiClient.post('/personnel/import', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + return response.data } } +// 导入结果类型 +export interface ImportResult { + successCount: number + failCount: number + results: { row: number; success: boolean; name?: string; message?: string }[] +} + // 审批历史类型 export interface PersonnelApprovalHistory { id: number diff --git a/src/frontend/src/api/stats.ts b/src/frontend/src/api/stats.ts index ad2c7b1..1ce54ba 100644 --- a/src/frontend/src/api/stats.ts +++ b/src/frontend/src/api/stats.ts @@ -7,9 +7,22 @@ export interface DashboardStats { pendingApprovals: number } +export interface RegimentAllocationStats { + regimentId: number + regimentName: string + totalQuota: number + totalConsumed: number + percentage: number +} + export const statsApi = { async getDashboardStats(): Promise { const response = await apiClient.get('/stats/dashboard') return response.data + }, + + async getRegimentAllocationsStats(): Promise { + const response = await apiClient.get('/stats/regiment-allocations') + return response.data } } diff --git a/src/frontend/src/layouts/MainLayout.vue b/src/frontend/src/layouts/MainLayout.vue index 5ce1510..1551968 100644 --- a/src/frontend/src/layouts/MainLayout.vue +++ b/src/frontend/src/layouts/MainLayout.vue @@ -22,7 +22,7 @@ 组织管理 - + @@ -172,9 +196,40 @@ onMounted(() => { margin-top: 20px; } -.quick-actions { +.pie-charts-container { display: flex; - gap: 12px; + justify-content: space-around; flex-wrap: wrap; + gap: 20px; +} + +.pie-chart-item { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; + min-width: 180px; + max-width: 220px; +} + +.pie-chart { + width: 180px; + height: 180px; +} + +.regiment-name { + margin-top: 8px; + font-size: 16px; + color: #303133; + font-weight: 600; + text-align: center; +} + +.regiment-info { + display: flex; + gap: 16px; + font-size: 14px; + color: #606266; + margin-top: 6px; } diff --git a/src/frontend/src/views/allocations/AllocationList.vue b/src/frontend/src/views/allocations/AllocationList.vue index 0dfb218..bf21a6c 100644 --- a/src/frontend/src/views/allocations/AllocationList.vue +++ b/src/frontend/src/views/allocations/AllocationList.vue @@ -172,7 +172,7 @@ - + - + - + - + - + @@ -87,33 +95,32 @@
{{ selectedOrg.name }}
- - - + + + - - + - + - + - + @@ -151,7 +158,7 @@ const accountFormRef = ref() const form = reactive({ name: '', - level: 1, + level: OrganizationalLevel.Division as OrganizationalLevel, parentId: null as number | null }) @@ -221,7 +228,7 @@ function handleAddRoot() { editingOrg.value = null parentOrg.value = null form.name = '' - form.level = 1 + form.level = OrganizationalLevel.Division form.parentId = null showCreateDialog.value = true } @@ -230,11 +237,20 @@ function handleAddChild(parent: OrganizationalUnit) { editingOrg.value = null parentOrg.value = parent form.name = '' - form.level = parent.level + 1 + form.level = getChildLevel(parent.level) form.parentId = parent.id showCreateDialog.value = true } +function getChildLevel(parentLevel: OrganizationalLevel): OrganizationalLevel { + switch (parentLevel) { + case OrganizationalLevel.Division: return OrganizationalLevel.Regiment + case OrganizationalLevel.Regiment: return OrganizationalLevel.Battalion + case OrganizationalLevel.Battalion: return OrganizationalLevel.Company + default: return OrganizationalLevel.Company + } +} + function handleEdit(org: OrganizationalUnit) { editingOrg.value = org parentOrg.value = null @@ -317,7 +333,7 @@ async function handleSave() { editingOrg.value = null parentOrg.value = null form.name = '' - form.level = 1 + form.level = OrganizationalLevel.Division form.parentId = null await loadOrganizations() } catch { @@ -361,4 +377,11 @@ onMounted(() => { justify-content: space-between; align-items: center; } + +.action-buttons { + display: flex; + flex-wrap: nowrap; + gap: 4px; + align-items: center; +} diff --git a/src/frontend/src/views/personnel/PersonnelDetail.vue b/src/frontend/src/views/personnel/PersonnelDetail.vue index 533ac13..46fa216 100644 --- a/src/frontend/src/views/personnel/PersonnelDetail.vue +++ b/src/frontend/src/views/personnel/PersonnelDetail.vue @@ -245,6 +245,7 @@ onMounted(() => { align-items: center; justify-content: center; color: #909399; + margin: 0 auto; } .status-badge { diff --git a/src/frontend/src/views/personnel/PersonnelForm.vue b/src/frontend/src/views/personnel/PersonnelForm.vue index 36e66b6..9af5e40 100644 --- a/src/frontend/src/views/personnel/PersonnelForm.vue +++ b/src/frontend/src/views/personnel/PersonnelForm.vue @@ -197,6 +197,8 @@ const form = reactive({ unit: '', position: '', rank: '', + gender: '男', + age: 25, idNumber: '', professionalTitle: '', politicalStatus: '', @@ -353,8 +355,8 @@ async function handleSubmit() { formData.append('name', form.name) formData.append('position', form.position) formData.append('rank', form.rank) - formData.append('gender', form.gender) - formData.append('age', form.age.toString()) + formData.append('gender', form.gender || '男') + formData.append('age', (form.age || 25).toString()) // 可选字段 - 只有非空时才添加 if (form.unit) formData.append('unit', form.unit) diff --git a/src/frontend/src/views/personnel/PersonnelList.vue b/src/frontend/src/views/personnel/PersonnelList.vue index 5561263..8615976 100644 --- a/src/frontend/src/views/personnel/PersonnelList.vue +++ b/src/frontend/src/views/personnel/PersonnelList.vue @@ -12,7 +12,7 @@ v-model="searchKeyword" placeholder="搜索姓名/职位" clearable - style="width: 180px; margin-right: 12px" + style="width: 150px; margin-right: 12px" @clear="handleSearch" @keyup.enter="handleSearch" > @@ -20,17 +20,86 @@ - + + + + + + + + + + + + + + + 待审批 待上级审批 - - 已批准 - + + + Excel操作 + + + + 添加人才 @@ -38,98 +107,169 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
([]) const loading = ref(false) -const statusFilter = ref('') +const activeTab = ref('list') +const approvalStatusFilter = ref('') +const unitFilter = ref(undefined) +const positionFilter = ref('') +const trainingFilter = ref('') +const achievementsFilter = ref('') const searchKeyword = ref('') const showApprovalDialog = ref(false) const showUpgradeApprovalDialog = ref(false) @@ -224,6 +369,9 @@ const approving = ref(false) const approvingUpgrade = ref(false) const selectedPerson = ref(null) const upgradeInfo = ref<{ currentLevel: string; newLevel: string }>({ currentLevel: '', newLevel: '' }) +const filterOptions = ref({ units: [], positions: [] }) +const fileInputRef = ref(null) +const importing = ref(false) const pagination = reactive({ pageNumber: 1, @@ -357,21 +505,63 @@ function handleSearch() { async function loadPersonnel() { loading.value = true try { - // 处理状态筛选:PendingUpgrade 是前端特殊状态,需要单独处理 - const isPendingUpgradeFilter = statusFilter.value === 'PendingUpgrade' - const backendStatus = isPendingUpgradeFilter ? undefined : (statusFilter.value || undefined) + // 根据当前标签页决定加载哪些数据 + const isApprovalTab = activeTab.value === 'approval' + + // 人才列表只显示已批准的数据 + // 审批列表显示待审批和待上级审批的数据 + let backendStatus: string | undefined + if (isApprovalTab) { + // 审批列表:根据筛选条件决定 + const isPendingUpgradeFilter = approvalStatusFilter.value === 'PendingUpgrade' + backendStatus = isPendingUpgradeFilter ? undefined : (approvalStatusFilter.value || 'Pending') + } else { + // 人才列表:只显示已批准 + backendStatus = 'Approved' + } const response = await personnelApi.getAll({ pageNumber: pagination.pageNumber, pageSize: pagination.pageSize, - status: backendStatus + status: backendStatus, + unitId: unitFilter.value, + position: positionFilter.value || undefined, + hasTraining: trainingFilter.value || undefined, + hasAchievements: achievementsFilter.value || undefined }) let items = response.items - // 前端过滤:待上级审批(有 pendingUpgradeByUnitId 的记录) - if (isPendingUpgradeFilter) { - items = items.filter(p => p.pendingUpgradeByUnitId) + // 审批列表的前端过滤 + if (isApprovalTab) { + if (approvalStatusFilter.value === 'PendingUpgrade') { + // 待上级审批:有 pendingUpgradeByUnitId 的记录 + items = items.filter(p => p.pendingUpgradeByUnitId) + } else if (!approvalStatusFilter.value) { + // 无筛选时显示所有待审批状态(Pending + 有pendingUpgradeByUnitId的) + const pendingResponse = await personnelApi.getAll({ + pageNumber: 1, + pageSize: 100, + status: 'Pending', + unitId: unitFilter.value, + position: positionFilter.value || undefined, + hasTraining: trainingFilter.value || undefined, + hasAchievements: achievementsFilter.value || undefined + }) + // 获取有待升级标记的记录 + const allResponse = await personnelApi.getAll({ + pageNumber: 1, + pageSize: 100, + unitId: unitFilter.value, + position: positionFilter.value || undefined, + hasTraining: trainingFilter.value || undefined, + hasAchievements: achievementsFilter.value || undefined + }) + const pendingUpgradeItems = allResponse.items.filter(p => p.pendingUpgradeByUnitId) + // 合并去重 + const pendingIds = new Set(pendingResponse.items.map(p => p.id)) + items = [...pendingResponse.items, ...pendingUpgradeItems.filter(p => !pendingIds.has(p.id))] + } } // 前端搜索过滤 @@ -383,7 +573,7 @@ async function loadPersonnel() { ) } personnel.value = items - pagination.total = isPendingUpgradeFilter ? items.length : response.totalCount + pagination.total = isApprovalTab && !approvalStatusFilter.value ? items.length : response.totalCount } catch { ElMessage.error('加载人才列表失败') } finally { @@ -391,6 +581,106 @@ async function loadPersonnel() { } } +async function loadFilterOptions() { + try { + filterOptions.value = await personnelApi.getFilterOptions() + } catch { + console.error('加载筛选选项失败') + } +} + +function handleFilterChange() { + pagination.pageNumber = 1 + loadPersonnel() +} + +// Excel操作处理 +function handleExcelCommand(command: string) { + switch (command) { + case 'export': + handleExport() + break + case 'template': + handleDownloadTemplate() + break + case 'import': + fileInputRef.value?.click() + break + } +} + +async function handleExport() { + try { + const blob = await personnelApi.exportExcel({ + status: activeTab.value === 'list' ? 'Approved' : (approvalStatusFilter.value || undefined), + unitId: unitFilter.value, + position: positionFilter.value || undefined, + hasTraining: trainingFilter.value || undefined, + hasAchievements: achievementsFilter.value || undefined + }) + + const url = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = `人才列表_${new Date().toISOString().slice(0, 10)}.xlsx` + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + window.URL.revokeObjectURL(url) + + ElMessage.success('导出成功') + } catch { + ElMessage.error('导出失败') + } +} + +async function handleDownloadTemplate() { + try { + const blob = await personnelApi.downloadImportTemplate() + + const url = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = '人才导入模板.xlsx' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + window.URL.revokeObjectURL(url) + + ElMessage.success('模板下载成功') + } catch { + ElMessage.error('下载模板失败') + } +} + +async function handleFileChange(event: Event) { + const target = event.target as HTMLInputElement + const file = target.files?.[0] + if (!file) return + + importing.value = true + try { + const result = await personnelApi.importExcel(file) + + if (result.successCount > 0) { + ElMessage.success(`导入成功${result.successCount}条记录`) + await loadPersonnel() + await loadFilterOptions() + } + + if (result.failCount > 0) { + ElMessage.warning(`${result.failCount}条记录导入失败`) + console.log('导入失败详情:', result.results.filter(r => !r.success)) + } + } catch (error: any) { + ElMessage.error(error.response?.data?.message || '导入失败') + } finally { + importing.value = false + // 清空文件选择,允许重复选择同一文件 + target.value = '' + } +} + function handleView(person: Personnel) { router.push(`/personnel/${person.id}`) } @@ -458,7 +748,13 @@ function canApproveUpgrade(person: Personnel): boolean { // 条件:1. 不是本单位提交的 2. 用户层级高于提交单位层级 function canApprovePersonnel(person: Personnel): boolean { if (!authStore.user) return false - // 本单位提交的人才不能自己审批 + + // 师团级账号(最高级别)可以审批所有人才,包括自己提交的 + if (authStore.organizationalLevelNum === 1) { + return true + } + + // 本单位提交的人才不能自己审批(非师团级) if (person.submittedByUnitId === authStore.user.organizationalUnitId) return false // 用户层级必须高于提交单位层级(这里简化处理,假设能看到的数据都是下级提交的) // 实际权限检查由后端完成 @@ -677,11 +973,17 @@ async function handleDelete(person: Personnel) { } } -watch(statusFilter, () => { +watch(approvalStatusFilter, () => { pagination.pageNumber = 1 loadPersonnel() }) +function handleTabChange() { + pagination.pageNumber = 1 + approvalStatusFilter.value = '' + loadPersonnel() +} + function handlePageChange(page: number) { pagination.pageNumber = page loadPersonnel() @@ -695,6 +997,7 @@ function handleSizeChange(size: number) { onMounted(() => { loadPersonnel() + loadFilterOptions() })