diff --git a/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs b/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs index cbf3ea3..5a0cbc5 100644 --- a/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs +++ b/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs @@ -229,6 +229,39 @@ public class AllocationsController : BaseApiController } } + /// + /// 上报消耗 - 更新单个分配记录的实际完成数量 + /// 团部及以下单位可以上报自己的消耗数据 + /// + [HttpPut("distributions/{distributionId}")] + public async Task UpdateDistribution(int distributionId, [FromBody] UpdateDistributionRequest request) + { + var unitId = GetCurrentUnitId(); + var userId = GetCurrentUserId(); + + if (unitId == null || userId == null) + return Unauthorized(new { message = "无法获取用户信息" }); + + try + { + var distribution = await _allocationService.UpdateDistributionCompletionAsync( + distributionId, + unitId.Value, + userId.Value, + request.ActualCompletion); + + return Ok(MapDistributionToResponse(distribution)); + } + catch (UnauthorizedAccessException ex) + { + return Forbid(); + } + catch (ArgumentException ex) + { + return BadRequest(new { message = ex.Message }); + } + } + /// /// 删除物资配额 /// diff --git a/src/MilitaryTrainingManagement/Controllers/PersonnelController.cs b/src/MilitaryTrainingManagement/Controllers/PersonnelController.cs index 5d1ec56..79f43b4 100644 --- a/src/MilitaryTrainingManagement/Controllers/PersonnelController.cs +++ b/src/MilitaryTrainingManagement/Controllers/PersonnelController.cs @@ -162,6 +162,7 @@ public class PersonnelController : BaseApiController var personnel = new Personnel { Name = request.Name, + Unit = request.Unit, Position = request.Position, Rank = request.Rank, Gender = "男", // 默认值 @@ -235,6 +236,7 @@ public class PersonnelController : BaseApiController var personnel = new Personnel { Name = request.Name, + Unit = request.Unit, Position = request.Position, Rank = request.Rank, Gender = "男", // 默认值 diff --git a/src/MilitaryTrainingManagement/Models/DTOs/AllocationDTOs.cs b/src/MilitaryTrainingManagement/Models/DTOs/AllocationDTOs.cs index 7e52ddb..12ba5c3 100644 --- a/src/MilitaryTrainingManagement/Models/DTOs/AllocationDTOs.cs +++ b/src/MilitaryTrainingManagement/Models/DTOs/AllocationDTOs.cs @@ -92,6 +92,19 @@ public class UpdateAllocationRequest public decimal TotalQuota { get; set; } } +/// +/// 更新配额分配实际完成数量请求(上报消耗) +/// +public class UpdateDistributionRequest +{ + /// + /// 实际完成数量 + /// + [Required(ErrorMessage = "实际完成数量为必填项")] + [Range(0, double.MaxValue, ErrorMessage = "实际完成数量不能为负数")] + public decimal ActualCompletion { get; set; } +} + /// /// 物资配额响应 /// diff --git a/src/MilitaryTrainingManagement/Models/DTOs/PersonnelDTOs.cs b/src/MilitaryTrainingManagement/Models/DTOs/PersonnelDTOs.cs index 7df2208..b951588 100644 --- a/src/MilitaryTrainingManagement/Models/DTOs/PersonnelDTOs.cs +++ b/src/MilitaryTrainingManagement/Models/DTOs/PersonnelDTOs.cs @@ -12,6 +12,9 @@ public class CreatePersonnelRequest [StringLength(50, MinimumLength = 2, ErrorMessage = "姓名长度应在2-50个字符之间")] public string Name { get; set; } = string.Empty; + [StringLength(100, ErrorMessage = "单位长度不能超过100个字符")] + public string? Unit { get; set; } + [Required(ErrorMessage = "职位不能为空")] [StringLength(100, ErrorMessage = "职位长度不能超过100个字符")] public string Position { get; set; } = string.Empty; @@ -176,6 +179,9 @@ public class SubmitPersonnelRequest [StringLength(50, MinimumLength = 2, ErrorMessage = "姓名长度应在2-50个字符之间")] public string Name { get; set; } = string.Empty; + [StringLength(100, ErrorMessage = "单位长度不能超过100个字符")] + public string? Unit { get; set; } + [Required(ErrorMessage = "职位不能为空")] [StringLength(100, ErrorMessage = "职位长度不能超过100个字符")] public string Position { get; set; } = string.Empty; diff --git a/src/MilitaryTrainingManagement/Models/Entities/Personnel.cs b/src/MilitaryTrainingManagement/Models/Entities/Personnel.cs index d705412..e384099 100644 --- a/src/MilitaryTrainingManagement/Models/Entities/Personnel.cs +++ b/src/MilitaryTrainingManagement/Models/Entities/Personnel.cs @@ -10,6 +10,7 @@ public class Personnel public int Id { get; set; } public string Name { get; set; } = string.Empty; public string? PhotoPath { get; set; } + public string? Unit { get; set; } // 单位 public string Position { get; set; } = string.Empty; public string Rank { get; set; } = string.Empty; public string Gender { get; set; } = string.Empty; diff --git a/src/MilitaryTrainingManagement/Program.cs b/src/MilitaryTrainingManagement/Program.cs index ec02aa9..494617f 100644 --- a/src/MilitaryTrainingManagement/Program.cs +++ b/src/MilitaryTrainingManagement/Program.cs @@ -155,6 +155,25 @@ using (var scope = app.Services.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService(); var authService = scope.ServiceProvider.GetRequiredService(); + var environment = scope.ServiceProvider.GetRequiredService(); + + // 确保uploads目录存在并有写入权限 + try + { + var uploadsPath = Path.Combine(environment.WebRootPath ?? environment.ContentRootPath, "uploads"); + var photosPath = Path.Combine(uploadsPath, "photos"); + var documentsPath = Path.Combine(uploadsPath, "documents"); + + Directory.CreateDirectory(uploadsPath); + Directory.CreateDirectory(photosPath); + Directory.CreateDirectory(documentsPath); + + Console.WriteLine($"Uploads 目录已创建: {uploadsPath}"); + } + catch (Exception ex) + { + Console.WriteLine($"创建 uploads 目录时出错: {ex.Message}"); + } // 确保数据库已创建 context.Database.EnsureCreated(); @@ -187,6 +206,9 @@ using (var scope = app.Services.CreateScope()) try { context.Database.ExecuteSqlRaw(@" + IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('Personnel') AND name = 'Unit') + ALTER TABLE Personnel ADD Unit nvarchar(100) NULL; + IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('Personnel') AND name = 'Ethnicity') ALTER TABLE Personnel ADD Ethnicity nvarchar(50) NULL; diff --git a/src/MilitaryTrainingManagement/Services/Implementations/AllocationService.cs b/src/MilitaryTrainingManagement/Services/Implementations/AllocationService.cs index ec0b526..e580c5f 100644 --- a/src/MilitaryTrainingManagement/Services/Implementations/AllocationService.cs +++ b/src/MilitaryTrainingManagement/Services/Implementations/AllocationService.cs @@ -58,13 +58,25 @@ public class AllocationService : IAllocationService var allUnitIds = new HashSet(subordinateIds) { unitId }; // 获取分配给这些单位的配额 - return await _context.MaterialAllocations + var allocations = await _context.MaterialAllocations .Include(a => a.CreatedByUnit) .Include(a => a.Distributions) .ThenInclude(d => d.TargetUnit) + .Include(a => a.Distributions) + .ThenInclude(d => d.ReportedByUser) .Where(a => a.Distributions.Any(d => allUnitIds.Contains(d.TargetUnitId))) .OrderByDescending(a => a.CreatedAt) .ToListAsync(); + + // 过滤每个配额的分配记录,只保留分配给当前单位及其下级的记录 + foreach (var allocation in allocations) + { + allocation.Distributions = allocation.Distributions + .Where(d => allUnitIds.Contains(d.TargetUnitId)) + .ToList(); + } + + return allocations; } public async Task> GetDistributionsForUnitAsync(int unitId) @@ -229,6 +241,42 @@ public class AllocationService : IAllocationService return await GetByIdAsync(allocationId) ?? allocation; } + public async Task UpdateDistributionCompletionAsync( + int distributionId, + int unitId, + int userId, + decimal actualCompletion) + { + var distribution = await _context.AllocationDistributions + .Include(d => d.Allocation) + .Include(d => d.TargetUnit) + .Include(d => d.ReportedByUser) + .FirstOrDefaultAsync(d => d.Id == distributionId); + + if (distribution == null) + throw new ArgumentException("配额分配记录不存在"); + + // 验证权限:只能更新分配给自己单位的记录 + if (distribution.TargetUnitId != unitId) + throw new UnauthorizedAccessException("无权更新此配额分配记录"); + + // 验证实际完成数量 + if (actualCompletion < 0) + throw new ArgumentException("实际完成数量不能为负数"); + + if (actualCompletion > distribution.UnitQuota) + throw new ArgumentException($"实际完成数量不能超过分配配额({distribution.UnitQuota})"); + + // 更新实际完成数量 + distribution.ActualCompletion = actualCompletion; + distribution.ReportedAt = DateTime.UtcNow; + distribution.ReportedByUserId = userId; + + await _context.SaveChangesAsync(); + + return distribution; + } + public async Task DeleteAsync(int id) { var allocation = await _context.MaterialAllocations.FindAsync(id); diff --git a/src/MilitaryTrainingManagement/Services/Interfaces/IAllocationService.cs b/src/MilitaryTrainingManagement/Services/Interfaces/IAllocationService.cs index 7608a20..e72881b 100644 --- a/src/MilitaryTrainingManagement/Services/Interfaces/IAllocationService.cs +++ b/src/MilitaryTrainingManagement/Services/Interfaces/IAllocationService.cs @@ -52,6 +52,11 @@ public interface IAllocationService /// Task UpdateDistributionsAsync(int allocationId, Dictionary distributions); + /// + /// 更新单个分配记录的实际完成数量(上报消耗) + /// + Task UpdateDistributionCompletionAsync(int distributionId, int unitId, int userId, decimal actualCompletion); + /// /// 删除物资配额 /// diff --git a/src/MilitaryTrainingManagement/uploads/photos/4da4cfc5-3024-49de-880d-7a7aac153aa5.jpg b/src/MilitaryTrainingManagement/uploads/photos/4da4cfc5-3024-49de-880d-7a7aac153aa5.jpg new file mode 100644 index 0000000..eb12a19 Binary files /dev/null and b/src/MilitaryTrainingManagement/uploads/photos/4da4cfc5-3024-49de-880d-7a7aac153aa5.jpg differ diff --git a/src/frontend/src/router/index.ts b/src/frontend/src/router/index.ts index 89756f1..ad73abd 100644 --- a/src/frontend/src/router/index.ts +++ b/src/frontend/src/router/index.ts @@ -53,6 +53,12 @@ const routes: RouteRecordRaw[] = [ component: () => import('@/views/allocations/AllocationForm.vue'), meta: { title: '编辑配额', minLevel: 1 } }, + { + path: 'allocations/:id/report', + name: 'AllocationReport', + component: () => import('@/views/allocations/AllocationReport.vue'), + meta: { title: '上报消耗' } + }, { path: 'reports', name: 'Reports', diff --git a/src/frontend/src/views/allocations/AllocationList.vue b/src/frontend/src/views/allocations/AllocationList.vue index b1161ce..62d8ab7 100644 --- a/src/frontend/src/views/allocations/AllocationList.vue +++ b/src/frontend/src/views/allocations/AllocationList.vue @@ -203,6 +203,18 @@ {{ row.reportedAt ? formatDate(row.reportedAt) : '-' }} + + + @@ -280,6 +292,23 @@ function getTotalDistributed(): number { return distributions.value.reduce((sum, d) => sum + (d.unitQuota || 0), 0) } +function canReportConsumption(distribution: AllocationDistribution): boolean { + // 只有当前用户所属单位的分配才能上报消耗 + if (!authStore.user) return false + return distribution.targetUnitId === authStore.user.organizationalUnitId +} + +function handleReportConsumption(distribution: AllocationDistribution) { + if (!selectedAllocation.value) return + router.push({ + path: `/allocations/${selectedAllocation.value.id}/report`, + query: { + distributionId: distribution.id, + targetUnitId: distribution.targetUnitId + } + }) +} + function handleSearch() { pagination.pageNumber = 1 } diff --git a/src/frontend/src/views/allocations/AllocationReport.vue b/src/frontend/src/views/allocations/AllocationReport.vue new file mode 100644 index 0000000..7894777 --- /dev/null +++ b/src/frontend/src/views/allocations/AllocationReport.vue @@ -0,0 +1,393 @@ + + + + + diff --git a/src/frontend/src/views/personnel/PersonnelDetail.vue b/src/frontend/src/views/personnel/PersonnelDetail.vue index 39f8cc4..0b9e0d2 100644 --- a/src/frontend/src/views/personnel/PersonnelDetail.vue +++ b/src/frontend/src/views/personnel/PersonnelDetail.vue @@ -27,36 +27,38 @@ {{ person.name }} - {{ person.gender }} - {{ person.age }} - {{ person.idNumber }} - {{ person.contactInfo }} - {{ person.hometown }} - {{ person.position }} + {{ person.unit || '-' }} + {{ person.position }} {{ person.rank }} - {{ person.professionalTitle }} - {{ person.educationLevel }} - {{ person.height }} cm + {{ person.idNumber || '-' }} + {{ person.professionalTitle || '-' }} + {{ person.politicalStatus || '-' }} + {{ person.educationLevel || '-' }} + {{ person.ethnicity || '-' }} + {{ person.hometown || '-' }} + {{ person.birthDate || '-' }} + {{ person.enlistmentDate || '-' }} + {{ person.specialty || '-' }} {{ getLevelName(person.approvedLevel) }} - - {{ person.submittedByUnitName }} + {{ person.submittedByUnitName }} {{ person.approvedByUnitName || '-' }} - {{ formatDate(person.submittedAt) }} - {{ person.approvedAt ? formatDate(person.approvedAt) : '-' }} + {{ formatDate(person.submittedAt) }} + {{ person.approvedAt ? formatDate(person.approvedAt) : '-' }} -
{{ person.trainingParticipation }}
+
{{ person.trainingParticipation || '-' }}
-
{{ person.achievements }}
+
{{ person.achievements || '-' }}
@@ -195,4 +197,10 @@ onMounted(() => { margin-bottom: 12px; color: #303133; } + +/* 缩短标签列宽度 */ +:deep(.el-descriptions__label) { + width: 120px !important; + min-width: 120px !important; +} diff --git a/src/frontend/src/views/personnel/PersonnelForm.vue b/src/frontend/src/views/personnel/PersonnelForm.vue index 3998c5b..20978a3 100644 --- a/src/frontend/src/views/personnel/PersonnelForm.vue +++ b/src/frontend/src/views/personnel/PersonnelForm.vue @@ -33,32 +33,37 @@ + + + + + + + + - - - + + + - - - @@ -69,6 +74,9 @@ + + + @@ -82,22 +90,19 @@ - - - + + + - - - + + + + + + + + - - - - @@ -185,6 +194,7 @@ const documentList = ref([]) const form = reactive({ name: '', + unit: '', position: '', rank: '', idNumber: '', @@ -205,6 +215,10 @@ const rules: FormRules = { { required: true, message: '请输入姓名', trigger: 'blur' }, { min: 2, max: 50, message: '姓名长度应在2-50个字符之间', trigger: 'blur' } ], + unit: [ + { required: true, message: '请输入单位', trigger: 'blur' }, + { max: 100, message: '单位长度不能超过100个字符', trigger: 'blur' } + ], position: [ { required: true, message: '请输入部职别', trigger: 'blur' }, { max: 100, message: '部职别长度不能超过100个字符', trigger: 'blur' } @@ -283,6 +297,7 @@ async function loadPersonnel() { try { const person = await personnelApi.getById(Number(route.params.id)) form.name = person.name + form.unit = person.unit || '' form.position = person.position form.rank = person.rank form.idNumber = person.idNumber || '' @@ -338,6 +353,7 @@ async function handleSubmit() { formData.append('rank', form.rank) // 可选字段 - 只有非空时才添加 + if (form.unit) formData.append('unit', form.unit) if (form.idNumber) formData.append('idNumber', form.idNumber) if (form.professionalTitle) formData.append('professionalTitle', form.professionalTitle) if (form.politicalStatus) formData.append('politicalStatus', form.politicalStatus)