功能修改优化

This commit is contained in:
18631081161 2026-01-19 23:30:01 +08:00
parent f946024a76
commit 599fa05ae6
21 changed files with 1526 additions and 228 deletions

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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 };

View File

@ -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" />

View File

@ -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;

View File

@ -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; }
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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": {

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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
}
}

View File

@ -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) {

View File

@ -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

View File

@ -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>

View File

@ -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;

View File

@ -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>

View File

@ -245,6 +245,7 @@ onMounted(() => {
align-items: center;
justify-content: center;
color: #909399;
margin: 0 auto;
}
.status-badge {

View File

@ -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)

View File

@ -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>