功能修改优化
This commit is contained in:
parent
f946024a76
commit
599fa05ae6
|
|
@ -507,7 +507,7 @@ public class AllocationsController : BaseApiController
|
|||
/// 获取按单位汇总的上报数据
|
||||
/// </summary>
|
||||
[HttpGet("distributions/{distributionId}/summary")]
|
||||
public async Task<IActionResult> GetReportSummaryByUnit(int distributionId)
|
||||
public async Task<IActionResult> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取配额的消耗统计(按时间范围)
|
||||
/// 师部账号可以按周、月、半年、年度查看所有团的总消耗
|
||||
/// </summary>
|
||||
[HttpGet("{id}/consumption-stats")]
|
||||
[Authorize(Policy = "DivisionLevel")]
|
||||
public async Task<IActionResult> 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
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 映射实体到响应DTO
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -72,23 +72,65 @@ public class PersonnelController : BaseApiController
|
|||
public async Task<IActionResult> 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<PersonnelStatus>(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
|
|||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取筛选选项(单位列表、岗位列表)
|
||||
/// </summary>
|
||||
[HttpGet("filter-options")]
|
||||
public async Task<IActionResult> 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
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 映射人员实体到响应DTO
|
||||
/// </summary>
|
||||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出人才列表到Excel
|
||||
/// </summary>
|
||||
[HttpGet("export")]
|
||||
public async Task<IActionResult> 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<PersonnelStatus>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载导入模板
|
||||
/// </summary>
|
||||
[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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从Excel导入人才
|
||||
/// </summary>
|
||||
[HttpPost("import")]
|
||||
public async Task<IActionResult> 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<object>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,89 @@ public class StatsController : BaseApiController
|
|||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取各团物资配额统计(饼状图数据)
|
||||
/// </summary>
|
||||
[HttpGet("regiment-allocations")]
|
||||
public async Task<IActionResult> GetRegimentAllocationsStats()
|
||||
{
|
||||
var unitId = GetCurrentUnitId();
|
||||
var unitLevel = GetCurrentUnitLevel();
|
||||
|
||||
if (unitId == null || unitLevel == null)
|
||||
return Unauthorized(new { message = "无法获取用户组织信息" });
|
||||
|
||||
// 获取所有团级单位
|
||||
List<int> 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<int> { unitId.Value };
|
||||
}
|
||||
else
|
||||
{
|
||||
// 营部及以下:获取所属团
|
||||
var currentUnit = await _context.OrganizationalUnits.FindAsync(unitId.Value);
|
||||
if (currentUnit == null)
|
||||
return Ok(new List<object>());
|
||||
|
||||
// 向上查找团级单位
|
||||
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<int> { parentUnit.Id };
|
||||
}
|
||||
else
|
||||
{
|
||||
return Ok(new List<object>());
|
||||
}
|
||||
}
|
||||
|
||||
var result = new List<object>();
|
||||
|
||||
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<List<int>> GetUnitAndSubordinateIds(int unitId)
|
||||
{
|
||||
var result = new List<int> { unitId };
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.22" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -117,3 +117,17 @@ public class SubordinateUnitSummary
|
|||
public decimal TotalActualCompletion { get; set; }
|
||||
public decimal CompletionRate { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单位消耗统计(按时间范围)
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -456,14 +456,25 @@ public class AllocationService : IAllocationService
|
|||
/// <summary>
|
||||
/// 获取按单位汇总的上报数据(带可见性过滤)
|
||||
/// </summary>
|
||||
public async Task<IEnumerable<UnitReportSummary>> GetReportSummaryByUnitAsync(int distributionId, int userUnitId, Models.Enums.OrganizationalLevel userLevel)
|
||||
public async Task<IEnumerable<UnitReportSummary>> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取配额的消耗统计(按时间范围)
|
||||
/// </summary>
|
||||
public async Task<IEnumerable<UnitConsumptionStat>> 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<UnitConsumptionStat>();
|
||||
}
|
||||
|
||||
// 获取所有分配记录的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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,5 +99,10 @@ public interface IAllocationService
|
|||
/// <summary>
|
||||
/// 获取按单位汇总的上报数据(带可见性过滤)
|
||||
/// </summary>
|
||||
Task<IEnumerable<Models.DTOs.UnitReportSummary>> GetReportSummaryByUnitAsync(int distributionId, int userUnitId, OrganizationalLevel userLevel);
|
||||
Task<IEnumerable<Models.DTOs.UnitReportSummary>> GetReportSummaryByUnitAsync(int distributionId, int userUnitId, OrganizationalLevel userLevel, DateTime? startDate = null, DateTime? endDate = null);
|
||||
|
||||
/// <summary>
|
||||
/// 获取配额的消耗统计(按时间范围)
|
||||
/// </summary>
|
||||
Task<IEnumerable<Models.DTOs.UnitConsumptionStat>> GetConsumptionStatsByPeriodAsync(int allocationId, DateTime startDate, DateTime endDate);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -53,8 +53,17 @@ export const allocationsApi = {
|
|||
return response.data
|
||||
},
|
||||
|
||||
async getReportSummaryByUnit(distributionId: number): Promise<UnitReportSummary[]> {
|
||||
const response = await apiClient.get<UnitReportSummary[]>(`/allocations/distributions/${distributionId}/summary`)
|
||||
async getReportSummaryByUnit(distributionId: number, period?: string): Promise<UnitReportSummary[]> {
|
||||
const response = await apiClient.get<UnitReportSummary[]>(`/allocations/distributions/${distributionId}/summary`, {
|
||||
params: period ? { period } : undefined
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
async getConsumptionStats(allocationId: number, period: string): Promise<ConsumptionStatsResponse> {
|
||||
const response = await apiClient.get<ConsumptionStatsResponse>(`/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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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<PaginatedResponse<Personnel>> {
|
||||
async getAll(params?: PersonnelQueryParams): Promise<PaginatedResponse<Personnel>> {
|
||||
const response = await apiClient.get<PaginatedResponse<Personnel>>('/personnel', { params })
|
||||
return response.data
|
||||
},
|
||||
|
||||
async getFilterOptions(): Promise<PersonnelFilterOptions> {
|
||||
const response = await apiClient.get<PersonnelFilterOptions>('/personnel/filter-options')
|
||||
return response.data
|
||||
},
|
||||
|
||||
async getById(id: number): Promise<Personnel> {
|
||||
const response = await apiClient.get<Personnel>(`/personnel/${id}`)
|
||||
return response.data
|
||||
|
|
@ -93,9 +111,45 @@ export const personnelApi = {
|
|||
async getApprovalHistory(personnelId: number): Promise<PersonnelApprovalHistory[]> {
|
||||
const response = await apiClient.get<PersonnelApprovalHistory[]>(`/personnel/${personnelId}/approval-history`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 导出Excel
|
||||
async exportExcel(params?: PersonnelQueryParams): Promise<Blob> {
|
||||
const response = await apiClient.get('/personnel/export', {
|
||||
params,
|
||||
responseType: 'blob'
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 下载导入模板
|
||||
async downloadImportTemplate(): Promise<Blob> {
|
||||
const response = await apiClient.get('/personnel/import-template', {
|
||||
responseType: 'blob'
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 导入Excel
|
||||
async importExcel(file: File): Promise<ImportResult> {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const response = await apiClient.post<ImportResult>('/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
|
||||
|
|
|
|||
|
|
@ -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<DashboardStats> {
|
||||
const response = await apiClient.get<DashboardStats>('/stats/dashboard')
|
||||
return response.data
|
||||
},
|
||||
|
||||
async getRegimentAllocationsStats(): Promise<RegimentAllocationStats[]> {
|
||||
const response = await apiClient.get<RegimentAllocationStats[]>('/stats/regiment-allocations')
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
<span>组织管理</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-sub-menu index="allocations">
|
||||
<el-sub-menu v-if="canViewAllocations" index="allocations">
|
||||
<template #title>
|
||||
<el-icon><Box /></el-icon>
|
||||
<span>物资配额</span>
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
<div class="header-right">
|
||||
<span class="user-info">
|
||||
<el-icon><User /></el-icon>
|
||||
{{ authStore.user?.displayName }} ({{ levelName }})
|
||||
{{ authStore.user?.username }} ({{ levelName }})
|
||||
</span>
|
||||
<el-button type="danger" text @click="handleLogout">
|
||||
<el-icon><SwitchButton /></el-icon>
|
||||
|
|
@ -104,6 +104,9 @@ const authStore = useAuthStore()
|
|||
const activeMenu = computed(() => route.path)
|
||||
const currentRoute = computed(() => route)
|
||||
|
||||
// 只有师本部和团本部可以查看物资配额(营、连级隐藏)
|
||||
const canViewAllocations = computed(() => authStore.organizationalLevelNum <= 2)
|
||||
|
||||
const levelName = computed(() => {
|
||||
const level = authStore.user?.organizationalLevel
|
||||
switch (level) {
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ export interface Personnel {
|
|||
id: number
|
||||
name: string
|
||||
photoPath: string
|
||||
unit: string
|
||||
position: string
|
||||
rank: string
|
||||
gender: string
|
||||
|
|
@ -169,6 +170,11 @@ export interface Personnel {
|
|||
trainingParticipation: string
|
||||
achievements: string
|
||||
supportingDocuments: string
|
||||
ethnicity: string
|
||||
politicalStatus: string
|
||||
birthDate: string
|
||||
enlistmentDate: string
|
||||
specialty: string
|
||||
submittedByUnitId: number
|
||||
submittedByUnitName: string
|
||||
approvedLevel: PersonnelLevel | null
|
||||
|
|
|
|||
|
|
@ -39,57 +39,33 @@
|
|||
<div class="stat-label">人员总数</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="authStore.organizationalLevelNum <= 2 ? 6 : 12">
|
||||
<el-card class="stat-card">
|
||||
</el-row>
|
||||
|
||||
<!-- 饼状图区域 -->
|
||||
<el-row :gutter="20" class="mt-20">
|
||||
<el-col :span="24">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon :size="24" color="#F56C6C"><Checked /></el-icon>
|
||||
<span>待审批</span>
|
||||
<el-icon :size="24" color="#409EFF"><PieChart /></el-icon>
|
||||
<span>各团物资消耗情况</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="stat-value">{{ stats.pendingApprovals }}</div>
|
||||
<div class="stat-label">待处理审批</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" class="mt-20">
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>快捷操作</span>
|
||||
</template>
|
||||
<div class="quick-actions">
|
||||
<el-button type="primary" @click="$router.push('/allocations')">
|
||||
<el-icon><Box /></el-icon>
|
||||
查看配额
|
||||
</el-button>
|
||||
<el-button type="success" @click="$router.push('/reports')">
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
上报数据
|
||||
</el-button>
|
||||
<el-button type="warning" @click="$router.push('/personnel')">
|
||||
<el-icon><User /></el-icon>
|
||||
人才管理
|
||||
</el-button>
|
||||
<el-button v-if="authStore.canApprove" type="danger" @click="$router.push('/approvals')">
|
||||
<el-icon><Checked /></el-icon>
|
||||
审批管理
|
||||
</el-button>
|
||||
<div v-if="regimentStats.length > 0" class="pie-charts-container">
|
||||
<div
|
||||
v-for="regiment in regimentStats"
|
||||
:key="regiment.regimentId"
|
||||
class="pie-chart-item"
|
||||
>
|
||||
<v-chart :option="getPieOption(regiment)" autoresize class="pie-chart" />
|
||||
<div class="regiment-name">{{ regiment.regimentName }}</div>
|
||||
<div class="regiment-info">
|
||||
<span>配额: {{ regiment.totalQuota }}</span>
|
||||
<span>消耗: {{ regiment.totalConsumed }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>当前用户信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="用户名">{{ authStore.user?.username }}</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名">{{ authStore.user?.displayName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属单位">{{ authStore.user?.organizationalUnitName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="组织层级">{{ levelName }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-empty v-else description="暂无团级物资配额数据" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
|
@ -97,12 +73,18 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { statsApi } from '@/api'
|
||||
import { OrganizationalLevel } from '@/types'
|
||||
import { Box, DataAnalysis, User, Checked } from '@element-plus/icons-vue'
|
||||
import { statsApi, type RegimentAllocationStats } from '@/api/stats'
|
||||
import { Box, DataAnalysis, User, PieChart } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import VChart from 'vue-echarts'
|
||||
import { use } from 'echarts/core'
|
||||
import { PieChart as EchartsPie } from 'echarts/charts'
|
||||
import { TitleComponent, TooltipComponent, LegendComponent } from 'echarts/components'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
|
||||
use([EchartsPie, TitleComponent, TooltipComponent, LegendComponent, CanvasRenderer])
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
|
|
@ -113,18 +95,49 @@ const stats = ref({
|
|||
pendingApprovals: 0
|
||||
})
|
||||
|
||||
const regimentStats = ref<RegimentAllocationStats[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const levelName = computed(() => {
|
||||
const level = authStore.user?.organizationalLevel
|
||||
switch (level) {
|
||||
case OrganizationalLevel.Division: return '师团级'
|
||||
case OrganizationalLevel.Regiment: return '团级'
|
||||
case OrganizationalLevel.Battalion: return '营级'
|
||||
case OrganizationalLevel.Company: return '连级'
|
||||
default: return ''
|
||||
function getPieOption(regiment: RegimentAllocationStats) {
|
||||
const consumed = regiment.totalConsumed
|
||||
const remaining = regiment.totalQuota - consumed
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'center',
|
||||
formatter: `${regiment.percentage}%`,
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: '#303133'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: [
|
||||
{ value: consumed, name: '已消耗', itemStyle: { color: '#67C23A' } },
|
||||
{ value: remaining > 0 ? remaining : 0, name: '剩余', itemStyle: { color: '#E4E7ED' } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
loading.value = true
|
||||
|
|
@ -137,8 +150,19 @@ async function loadStats() {
|
|||
}
|
||||
}
|
||||
|
||||
async function loadRegimentStats() {
|
||||
try {
|
||||
const data = await statsApi.getRegimentAllocationsStats()
|
||||
console.log('Regiment stats loaded:', data)
|
||||
regimentStats.value = data
|
||||
} catch (error) {
|
||||
console.error('加载团级统计数据失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStats()
|
||||
loadRegimentStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<!-- 师团级显示总配额、总消耗、分配单位数 -->
|
||||
<!-- 师团级显示总配额、总消耗、总进度 -->
|
||||
<template v-if="authStore.canCreateAllocations">
|
||||
<el-col :span="6">
|
||||
<div class="summary-item">
|
||||
|
|
@ -194,10 +194,15 @@
|
|||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="summary-item">
|
||||
<div class="summary-label">分配单位数</div>
|
||||
<div class="summary-value units">
|
||||
<span class="number">{{ distributions.length }}</span>
|
||||
<span class="unit">个</span>
|
||||
<div class="summary-label">总进度</div>
|
||||
<div class="summary-value">
|
||||
<el-progress
|
||||
type="circle"
|
||||
:percentage="getTotalProgressPercentage()"
|
||||
:width="60"
|
||||
:stroke-width="6"
|
||||
:status="getProgressStatus(getTotalProgressPercentage() / 100)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
|
@ -248,10 +253,23 @@
|
|||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 师部账号:时间范围筛选 -->
|
||||
<div v-if="authStore.canCreateAllocations" class="period-filter">
|
||||
<span class="filter-label">查看时间范围:</span>
|
||||
<el-radio-group v-model="selectedPeriod" size="small" @change="handlePeriodChange">
|
||||
<el-radio-button value="week">本周</el-radio-button>
|
||||
<el-radio-button value="month">本月</el-radio-button>
|
||||
<el-radio-button value="halfYear">半年</el-radio-button>
|
||||
<el-radio-button value="year">年度</el-radio-button>
|
||||
<el-radio-button value="all">全部</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 师团级和团级显示分配列表,营部及以下隐藏 -->
|
||||
<el-table
|
||||
v-if="authStore.organizationalLevelNum <= 2"
|
||||
:data="distributions"
|
||||
:data="filteredDistributions"
|
||||
v-loading="loadingStats"
|
||||
style="width: 100%"
|
||||
stripe
|
||||
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 'bold', fontSize: '14px' }"
|
||||
|
|
@ -340,8 +358,8 @@
|
|||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="report-stat">
|
||||
<div class="stat-label">累计消耗</div>
|
||||
<div class="stat-value consumed">{{ formatNumber(selectedDistribution?.actualCompletion || 0) }}</div>
|
||||
<div class="stat-label">{{ reportsPeriod === 'all' ? '累计消耗' : '期间消耗' }}</div>
|
||||
<div class="stat-value consumed">{{ formatNumber(getReportsTotalConsumed()) }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
|
|
@ -359,6 +377,18 @@
|
|||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 师部账号:时间范围筛选 -->
|
||||
<div v-if="authStore.canCreateAllocations" class="period-filter">
|
||||
<span class="filter-label">查看时间范围:</span>
|
||||
<el-radio-group v-model="reportsPeriod" size="small" @change="handleReportsPeriodChange">
|
||||
<el-radio-button value="week">本周</el-radio-button>
|
||||
<el-radio-button value="month">本月</el-radio-button>
|
||||
<el-radio-button value="halfYear">半年</el-radio-button>
|
||||
<el-radio-button value="year">年度</el-radio-button>
|
||||
<el-radio-button value="all">全部</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="reportsTabActive" class="reports-tabs">
|
||||
<el-tab-pane label="按单位汇总" name="summary">
|
||||
<el-table
|
||||
|
|
@ -449,7 +479,7 @@ import { useRouter } from 'vue-router'
|
|||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Search, View, Edit, Delete, Box, Setting, OfficeBuilding, Clock } from '@element-plus/icons-vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { allocationsApi, type UnitReportSummary } from '@/api'
|
||||
import { allocationsApi, type UnitReportSummary, type UnitConsumptionStat } from '@/api'
|
||||
import type { MaterialAllocation, AllocationDistribution } from '@/types'
|
||||
|
||||
const router = useRouter()
|
||||
|
|
@ -459,15 +489,19 @@ const allocations = ref<MaterialAllocation[]>([])
|
|||
const distributions = ref<AllocationDistribution[]>([])
|
||||
const consumptionReports = ref<any[]>([])
|
||||
const unitSummaries = ref<UnitReportSummary[]>([])
|
||||
const consumptionStats = ref<UnitConsumptionStat[]>([])
|
||||
const reportsTabActive = ref('summary')
|
||||
const loading = ref(false)
|
||||
const loadingReports = ref(false)
|
||||
const loadingStats = ref(false)
|
||||
const showDistributionDialog = ref(false)
|
||||
const showReportsDialog = ref(false)
|
||||
const searchKeyword = ref('')
|
||||
const categoryFilter = ref('')
|
||||
const selectedAllocation = ref<MaterialAllocation | null>(null)
|
||||
const selectedDistribution = ref<AllocationDistribution | null>(null)
|
||||
const selectedPeriod = ref('all')
|
||||
const reportsPeriod = ref('all')
|
||||
|
||||
const pagination = reactive({
|
||||
pageNumber: 1,
|
||||
|
|
@ -590,6 +624,10 @@ function getTotalDistributed(): number {
|
|||
}
|
||||
|
||||
function getTotalConsumed(): number {
|
||||
// 如果有按时间范围筛选的统计数据,使用统计数据
|
||||
if (authStore.canCreateAllocations && selectedPeriod.value !== 'all' && consumptionStats.value.length > 0) {
|
||||
return consumptionStats.value.reduce((sum: number, s) => sum + s.totalConsumed, 0)
|
||||
}
|
||||
// 营部及以下级别使用后端计算的可见范围内的上报总和
|
||||
if (authStore.organizationalLevelNum >= 3 && selectedAllocation.value) {
|
||||
return (selectedAllocation.value as any).visibleActualCompletion || 0
|
||||
|
|
@ -597,6 +635,50 @@ function getTotalConsumed(): number {
|
|||
return distributions.value.reduce((sum, d) => sum + (d.actualCompletion || 0), 0)
|
||||
}
|
||||
|
||||
function getTotalProgressPercentage(): number {
|
||||
if (!selectedAllocation.value || !selectedAllocation.value.totalQuota) return 0
|
||||
const consumed = getTotalConsumed()
|
||||
return Math.round((consumed / selectedAllocation.value.totalQuota) * 100)
|
||||
}
|
||||
|
||||
// 根据时间范围筛选后的分配数据
|
||||
const filteredDistributions = computed(() => {
|
||||
if (!authStore.canCreateAllocations || selectedPeriod.value === 'all') {
|
||||
return distributions.value
|
||||
}
|
||||
// 使用统计数据更新分配记录的消耗数据
|
||||
return distributions.value.map(d => {
|
||||
const stat = consumptionStats.value.find((s: UnitConsumptionStat) => s.unitId === d.targetUnitId)
|
||||
if (stat) {
|
||||
return {
|
||||
...d,
|
||||
actualCompletion: stat.totalConsumed,
|
||||
completionRate: stat.completionRate
|
||||
}
|
||||
}
|
||||
return { ...d, actualCompletion: 0, completionRate: 0 }
|
||||
})
|
||||
})
|
||||
|
||||
async function handlePeriodChange() {
|
||||
if (!selectedAllocation.value || selectedPeriod.value === 'all') {
|
||||
consumptionStats.value = []
|
||||
return
|
||||
}
|
||||
|
||||
loadingStats.value = true
|
||||
try {
|
||||
const response = await allocationsApi.getConsumptionStats(selectedAllocation.value.id, selectedPeriod.value)
|
||||
consumptionStats.value = response.unitStats
|
||||
} catch (error) {
|
||||
console.error('加载消耗统计失败', error)
|
||||
ElMessage.error('加载消耗统计失败')
|
||||
consumptionStats.value = []
|
||||
} finally {
|
||||
loadingStats.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function canReportConsumption(distribution: AllocationDistribution): boolean {
|
||||
if (!authStore.user) return false
|
||||
// 如果分配目标是当前用户所属单位,则可以上报消耗
|
||||
|
|
@ -636,6 +718,7 @@ async function handleViewReports(distribution: AllocationDistribution) {
|
|||
selectedDistribution.value = distribution
|
||||
showReportsDialog.value = true
|
||||
reportsTabActive.value = 'summary'
|
||||
reportsPeriod.value = 'all'
|
||||
loadingReports.value = true
|
||||
try {
|
||||
// 同时加载汇总数据和明细数据
|
||||
|
|
@ -655,6 +738,28 @@ async function handleViewReports(distribution: AllocationDistribution) {
|
|||
}
|
||||
}
|
||||
|
||||
// 计算上报记录的总消耗
|
||||
function getReportsTotalConsumed(): number {
|
||||
return unitSummaries.value.reduce((sum, s) => sum + s.totalReported, 0)
|
||||
}
|
||||
|
||||
// 上报记录时间范围筛选变化
|
||||
async function handleReportsPeriodChange() {
|
||||
if (!selectedDistribution.value) return
|
||||
|
||||
loadingReports.value = true
|
||||
try {
|
||||
const period = reportsPeriod.value === 'all' ? undefined : reportsPeriod.value
|
||||
const summaries = await allocationsApi.getReportSummaryByUnit(selectedDistribution.value.id, period)
|
||||
unitSummaries.value = summaries
|
||||
} catch (error) {
|
||||
console.error('加载上报记录失败', error)
|
||||
ElMessage.error('加载上报记录失败')
|
||||
} finally {
|
||||
loadingReports.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.pageNumber = 1
|
||||
}
|
||||
|
|
@ -688,6 +793,8 @@ async function loadAllocations() {
|
|||
|
||||
async function handleViewDistribution(allocation: MaterialAllocation) {
|
||||
selectedAllocation.value = allocation
|
||||
selectedPeriod.value = 'all'
|
||||
consumptionStats.value = []
|
||||
// 对于非师团级账号,只显示本单位及下级单位的分配记录
|
||||
if (authStore.canCreateAllocations) {
|
||||
// 师团级显示所有分配
|
||||
|
|
@ -809,6 +916,21 @@ onMounted(() => {
|
|||
color: #606266;
|
||||
}
|
||||
|
||||
.period-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.consumption-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -11,37 +11,45 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="organizations" style="width: 100%" row-key="id" :tree-props="{ children: 'children' }">
|
||||
<el-table
|
||||
:data="organizations"
|
||||
style="width: 100%"
|
||||
row-key="id"
|
||||
:tree-props="{ children: 'children' }"
|
||||
:header-cell-style="{ textAlign: 'center' }"
|
||||
>
|
||||
<el-table-column prop="name" label="组织名称" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ paddingLeft: (row.level - 1) * 20 + 'px' }">{{ row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="level" label="层级" width="100">
|
||||
<el-table-column prop="level" label="层级" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getLevelTagType(row.level)" size="small">{{ getLevelName(row.level) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" width="160">
|
||||
<el-table-column prop="createdAt" label="创建时间" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="320" fixed="right">
|
||||
<el-table-column label="操作" width="380" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.level < 4"
|
||||
type="primary"
|
||||
text
|
||||
size="small"
|
||||
@click="handleAddChild(row)"
|
||||
>
|
||||
添加{{ getChildLevelName(row.level) }}
|
||||
</el-button>
|
||||
<el-button type="warning" text size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="info" text size="small" @click="handleViewAccounts(row)">查看账号</el-button>
|
||||
<el-button type="success" text size="small" @click="handleCreateAccount(row)">创建账户</el-button>
|
||||
<el-button type="danger" text size="small" @click="handleDelete(row)">删除</el-button>
|
||||
<div class="action-buttons">
|
||||
<el-button
|
||||
v-if="row.level !== OrganizationalLevel.Company"
|
||||
type="primary"
|
||||
text
|
||||
size="small"
|
||||
@click="handleAddChild(row)"
|
||||
>
|
||||
添加{{ getChildLevelName(row.level) }}
|
||||
</el-button>
|
||||
<el-button type="warning" text size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="info" text size="small" @click="handleViewAccounts(row)">查看账号</el-button>
|
||||
<el-button type="success" text size="small" @click="handleCreateAccount(row)">创建账户</el-button>
|
||||
<el-button type="danger" text size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
|
@ -87,33 +95,32 @@
|
|||
<div v-if="selectedOrg" style="margin-bottom: 16px;">
|
||||
<el-tag type="info">{{ selectedOrg.name }}</el-tag>
|
||||
</div>
|
||||
<el-table :data="accounts" v-loading="loadingAccounts">
|
||||
<el-table-column prop="username" label="用户名" min-width="120" />
|
||||
<el-table-column prop="plainPassword" label="密码" min-width="120">
|
||||
<el-table :data="accounts" v-loading="loadingAccounts" :header-cell-style="{ textAlign: 'center' }">
|
||||
<el-table-column prop="username" label="用户名" min-width="120" align="center" />
|
||||
<el-table-column prop="plainPassword" label="密码" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.plainPassword">{{ row.plainPassword }}</span>
|
||||
<el-tag v-else type="info" size="small">未记录</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="displayName" label="显示名称" min-width="140" />
|
||||
<el-table-column prop="isActive" label="状态" width="80">
|
||||
<el-table-column prop="isActive" label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isActive ? 'success' : 'danger'" size="small">
|
||||
{{ row.isActive ? '激活' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" min-width="160">
|
||||
<el-table-column prop="createdAt" label="创建时间" min-width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="lastLoginAt" label="最后登录" min-width="160">
|
||||
<el-table-column prop="lastLoginAt" label="最后登录" min-width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.lastLoginAt ? formatDate(row.lastLoginAt) : '从未登录' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" fixed="right">
|
||||
<el-table-column label="操作" width="80" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button type="danger" text size="small" @click="handleDeleteAccount(row)">删除</el-button>
|
||||
</template>
|
||||
|
|
@ -151,7 +158,7 @@ const accountFormRef = ref<FormInstance>()
|
|||
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -245,6 +245,7 @@ onMounted(() => {
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-select v-model="statusFilter" placeholder="状态筛选" clearable style="width: 140px; margin-right: 12px">
|
||||
<el-select
|
||||
v-model="unitFilter"
|
||||
placeholder="单位筛选"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 140px; margin-right: 12px"
|
||||
@change="handleFilterChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="unit in filterOptions.units"
|
||||
:key="unit.id"
|
||||
:label="unit.name"
|
||||
:value="unit.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="positionFilter"
|
||||
placeholder="岗位筛选"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 140px; margin-right: 12px"
|
||||
@change="handleFilterChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="pos in filterOptions.positions"
|
||||
:key="pos"
|
||||
:label="pos"
|
||||
:value="pos"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="trainingFilter"
|
||||
placeholder="培训筛选"
|
||||
clearable
|
||||
style="width: 120px; margin-right: 12px"
|
||||
@change="handleFilterChange"
|
||||
>
|
||||
<el-option label="参加过培训" value="yes" />
|
||||
<el-option label="未参加培训" value="no" />
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="achievementsFilter"
|
||||
placeholder="成绩筛选"
|
||||
clearable
|
||||
style="width: 120px; margin-right: 12px"
|
||||
@change="handleFilterChange"
|
||||
>
|
||||
<el-option label="有成绩" value="yes" />
|
||||
<el-option label="无成绩" value="no" />
|
||||
</el-select>
|
||||
<el-select v-if="activeTab === 'approval'" v-model="approvalStatusFilter" placeholder="状态筛选" clearable style="width: 120px; margin-right: 12px" @change="handleFilterChange">
|
||||
<el-option label="待审批" value="Pending">
|
||||
<el-tag type="warning" size="small">待审批</el-tag>
|
||||
</el-option>
|
||||
<el-option label="待上级审批" value="PendingUpgrade">
|
||||
<el-tag type="primary" size="small">待上级审批</el-tag>
|
||||
</el-option>
|
||||
<el-option label="已批准" value="Approved">
|
||||
<el-tag type="success" size="small">已批准</el-tag>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-dropdown style="margin-right: 12px" @command="handleExcelCommand">
|
||||
<el-button>
|
||||
Excel操作
|
||||
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="export">
|
||||
<el-icon><Download /></el-icon>
|
||||
导出Excel
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="template">
|
||||
<el-icon><Document /></el-icon>
|
||||
下载导入模板
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="import">
|
||||
<el-icon><Upload /></el-icon>
|
||||
导入Excel
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button type="primary" @click="$router.push('/personnel/create')">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加人才
|
||||
|
|
@ -38,98 +107,169 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
:data="personnel"
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
highlight-current-row
|
||||
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }"
|
||||
>
|
||||
<el-table-column prop="name" label="姓名" width="100">
|
||||
<template #default="{ row }">
|
||||
<span class="name-cell">{{ row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="gender" label="性别" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.gender === '男' ? 'primary' : 'danger'" size="small" effect="plain">
|
||||
{{ row.gender }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="age" label="年龄" width="70" align="center" />
|
||||
<el-table-column prop="position" label="职位" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="rank" label="军衔" width="100" show-overflow-tooltip />
|
||||
<el-table-column prop="submittedByUnitName" label="所属单位" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getDisplayStatusType(row)" effect="dark" size="small">
|
||||
{{ getDisplayStatus(row) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="approvedLevel" label="人员等级" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.approvedLevel" :type="getLevelTagType(row.approvedLevel)" size="small">
|
||||
{{ getLevelName(row.approvedLevel) }}
|
||||
</el-tag>
|
||||
<span v-else class="no-data">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="submittedAt" label="提交时间" width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="time-cell">{{ formatDate(row.submittedAt) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="action-buttons">
|
||||
<el-tooltip content="查看详情" placement="top">
|
||||
<el-button type="primary" link size="small" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="row.status === 'Pending' && authStore.canApprove && canApprovePersonnel(row)" content="审批" placement="top">
|
||||
<el-button type="success" link size="small" @click="handleApprove(row)">
|
||||
<el-icon><Check /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="row.status === 'Approved' && authStore.canApprove && canUpgrade(row)" content="向上申报" placement="top">
|
||||
<el-button type="warning" link size="small" @click="handleRequestUpgrade(row)">
|
||||
<el-icon><Top /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="row.status === 'Approved' && authStore.canApprove && canDirectUpgrade(row)" content="直接升级" placement="top">
|
||||
<el-button type="success" link size="small" @click="handleDirectUpgrade(row)">
|
||||
<el-icon><Top /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="row.status === 'Approved' && row.pendingUpgradeByUnitId && authStore.canApprove && canApproveUpgrade(row)" content="审批向上申报" placement="top">
|
||||
<el-button type="success" link size="small" @click="handleApproveUpgrade(row)">
|
||||
<el-icon><Check /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="row.status === 'Pending' || (row.status === 'Rejected' && canResubmit(row))" content="编辑" placement="top">
|
||||
<el-button type="warning" link size="small" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="row.status === 'Rejected' && canResubmit(row)" content="重新提交审批" placement="top">
|
||||
<el-button type="primary" link size="small" @click="handleResubmit(row)">
|
||||
<el-icon><Upload /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="canDelete(row)" content="删除" placement="top">
|
||||
<el-button type="danger" link size="small" @click="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 隐藏的文件上传input -->
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
accept=".xlsx,.xls"
|
||||
style="display: none"
|
||||
@change="handleFileChange"
|
||||
/>
|
||||
|
||||
<!-- 标签页 -->
|
||||
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
|
||||
<el-tab-pane label="人才列表" name="list">
|
||||
<el-table
|
||||
:data="personnel"
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
highlight-current-row
|
||||
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }"
|
||||
>
|
||||
<el-table-column prop="name" label="姓名" width="100">
|
||||
<template #default="{ row }">
|
||||
<span class="name-cell">{{ row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="gender" label="性别" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.gender === '男' ? 'primary' : 'danger'" size="small" effect="plain">
|
||||
{{ row.gender }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="age" label="年龄" width="70" align="center" />
|
||||
<el-table-column prop="position" label="职位" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="rank" label="军衔" width="100" show-overflow-tooltip />
|
||||
<el-table-column prop="submittedByUnitName" label="所属单位" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="approvedLevel" label="人员等级" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.approvedLevel" :type="getLevelTagType(row.approvedLevel)" size="small">
|
||||
{{ getLevelName(row.approvedLevel) }}
|
||||
</el-tag>
|
||||
<span v-else class="no-data">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="submittedAt" label="提交时间" width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="time-cell">{{ formatDate(row.submittedAt) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="action-buttons">
|
||||
<el-tooltip content="查看详情" placement="top">
|
||||
<el-button type="primary" link size="small" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="row.status === 'Approved' && authStore.canApprove && canUpgrade(row)" content="向上申报" placement="top">
|
||||
<el-button type="warning" link size="small" @click="handleRequestUpgrade(row)">
|
||||
<el-icon><Top /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="row.status === 'Approved' && authStore.canApprove && canDirectUpgrade(row)" content="直接升级" placement="top">
|
||||
<el-button type="success" link size="small" @click="handleDirectUpgrade(row)">
|
||||
<el-icon><Top /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="canDelete(row)" content="删除" placement="top">
|
||||
<el-button type="danger" link size="small" @click="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="审批列表" name="approval">
|
||||
<el-table
|
||||
:data="personnel"
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
highlight-current-row
|
||||
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }"
|
||||
>
|
||||
<el-table-column prop="name" label="姓名" width="100">
|
||||
<template #default="{ row }">
|
||||
<span class="name-cell">{{ row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="gender" label="性别" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.gender === '男' ? 'primary' : 'danger'" size="small" effect="plain">
|
||||
{{ row.gender }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="age" label="年龄" width="70" align="center" />
|
||||
<el-table-column prop="position" label="职位" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="rank" label="军衔" width="100" show-overflow-tooltip />
|
||||
<el-table-column prop="submittedByUnitName" label="所属单位" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getDisplayStatusType(row)" effect="dark" size="small">
|
||||
{{ getDisplayStatus(row) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="approvedLevel" label="人员等级" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.approvedLevel" :type="getLevelTagType(row.approvedLevel)" size="small">
|
||||
{{ getLevelName(row.approvedLevel) }}
|
||||
</el-tag>
|
||||
<span v-else class="no-data">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="submittedAt" label="提交时间" width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="time-cell">{{ formatDate(row.submittedAt) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="action-buttons">
|
||||
<el-tooltip content="查看详情" placement="top">
|
||||
<el-button type="primary" link size="small" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="row.status === 'Pending' && authStore.canApprove && canApprovePersonnel(row)" content="审批" placement="top">
|
||||
<el-button type="success" link size="small" @click="handleApprove(row)">
|
||||
<el-icon><Check /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="row.status === 'Approved' && row.pendingUpgradeByUnitId && authStore.canApprove && canApproveUpgrade(row)" content="审批向上申报" placement="top">
|
||||
<el-button type="success" link size="small" @click="handleApproveUpgrade(row)">
|
||||
<el-icon><Check /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="row.status === 'Pending' || (row.status === 'Rejected' && canResubmit(row))" content="编辑" placement="top">
|
||||
<el-button type="warning" link size="small" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="row.status === 'Rejected' && canResubmit(row)" content="重新提交审批" placement="top">
|
||||
<el-button type="primary" link size="small" @click="handleResubmit(row)">
|
||||
<el-icon><Upload /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="canDelete(row)" content="删除" placement="top">
|
||||
<el-button type="danger" link size="small" @click="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
|
|
@ -205,9 +345,9 @@
|
|||
import { ref, reactive, watch, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Search, View, Edit, Delete, Check, Close, User, Top, Upload } from '@element-plus/icons-vue'
|
||||
import { Plus, Search, View, Edit, Delete, Check, Close, User, Top, Upload, Download, Document, ArrowDown } from '@element-plus/icons-vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { personnelApi } from '@/api'
|
||||
import { personnelApi, type PersonnelFilterOptions } from '@/api/personnel'
|
||||
import type { Personnel } from '@/types'
|
||||
import { PersonnelStatus, PersonnelLevel } from '@/types'
|
||||
|
||||
|
|
@ -216,7 +356,12 @@ const authStore = useAuthStore()
|
|||
|
||||
const personnel = ref<Personnel[]>([])
|
||||
const loading = ref(false)
|
||||
const statusFilter = ref('')
|
||||
const activeTab = ref('list')
|
||||
const approvalStatusFilter = ref('')
|
||||
const unitFilter = ref<number | undefined>(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<Personnel | null>(null)
|
||||
const upgradeInfo = ref<{ currentLevel: string; newLevel: string }>({ currentLevel: '', newLevel: '' })
|
||||
const filterOptions = ref<PersonnelFilterOptions>({ units: [], positions: [] })
|
||||
const fileInputRef = ref<HTMLInputElement | null>(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()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user