UI修改
This commit is contained in:
parent
8cada25804
commit
d7f39c31bc
|
|
@ -28,7 +28,9 @@ public class AllocationsController : BaseApiController
|
|||
/// 获取当前用户可见的所有物资配额
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetAll()
|
||||
public async Task<IActionResult> GetAll(
|
||||
[FromQuery] int pageNumber = 1,
|
||||
[FromQuery] int pageSize = 10)
|
||||
{
|
||||
var unitId = GetCurrentUnitId();
|
||||
var unitLevel = GetCurrentUnitLevel();
|
||||
|
|
@ -37,18 +39,31 @@ public class AllocationsController : BaseApiController
|
|||
return Unauthorized(new { message = "无法获取用户组织信息" });
|
||||
|
||||
// 师团级可以看到所有配额,其他级别只能看到分配给自己及下级的配额
|
||||
IEnumerable<Models.Entities.MaterialAllocation> allocations;
|
||||
IEnumerable<Models.Entities.MaterialAllocation> allAllocations;
|
||||
if (unitLevel == OrganizationalLevel.Division)
|
||||
{
|
||||
allocations = await _allocationService.GetAllAsync();
|
||||
allAllocations = await _allocationService.GetAllAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
allocations = await _allocationService.GetVisibleToUnitAsync(unitId.Value);
|
||||
allAllocations = await _allocationService.GetVisibleToUnitAsync(unitId.Value);
|
||||
}
|
||||
|
||||
var response = allocations.Select(MapToResponse);
|
||||
return Ok(response);
|
||||
var totalCount = allAllocations.Count();
|
||||
var items = allAllocations
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(MapToResponse)
|
||||
.ToList();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
items,
|
||||
totalCount,
|
||||
pageNumber,
|
||||
pageSize,
|
||||
totalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -132,7 +147,7 @@ public class AllocationsController : BaseApiController
|
|||
request.Unit,
|
||||
request.TotalQuota,
|
||||
unitId.Value,
|
||||
request.Distributions);
|
||||
request.GetDistributionsDictionary());
|
||||
|
||||
return CreatedAtAction(nameof(GetById), new { id = allocation.Id }, MapToResponse(allocation));
|
||||
}
|
||||
|
|
@ -251,12 +266,14 @@ public class AllocationsController : BaseApiController
|
|||
[HttpPost("validate")]
|
||||
public async Task<IActionResult> ValidateDistributions([FromBody] CreateAllocationRequest request)
|
||||
{
|
||||
var distributions = request.GetDistributionsDictionary();
|
||||
|
||||
var isValid = await _allocationService.ValidateDistributionQuotasAsync(
|
||||
request.TotalQuota,
|
||||
request.Distributions);
|
||||
distributions);
|
||||
|
||||
var targetUnitsExist = await _allocationService.ValidateTargetUnitsExistAsync(
|
||||
request.Distributions.Keys);
|
||||
distributions.Keys);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
|
||||
namespace MilitaryTrainingManagement.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 文件服务控制器
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/files")]
|
||||
public class FilesController : ControllerBase
|
||||
{
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
private readonly FileExtensionContentTypeProvider _contentTypeProvider;
|
||||
|
||||
public FilesController(IWebHostEnvironment environment)
|
||||
{
|
||||
_environment = environment;
|
||||
_contentTypeProvider = new FileExtensionContentTypeProvider();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取上传的文件
|
||||
/// </summary>
|
||||
[HttpGet("{*filePath}")]
|
||||
[AllowAnonymous]
|
||||
public IActionResult GetFile(string filePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
return BadRequest(new { message = "文件路径不能为空" });
|
||||
|
||||
// 安全检查:防止路径遍历攻击
|
||||
if (filePath.Contains("..") || filePath.Contains("~"))
|
||||
return BadRequest(new { message = "无效的文件路径" });
|
||||
|
||||
var uploadsPath = Path.Combine(_environment.ContentRootPath, "uploads");
|
||||
var fullPath = Path.Combine(uploadsPath, filePath);
|
||||
|
||||
// 确保文件在uploads目录内
|
||||
if (!fullPath.StartsWith(uploadsPath))
|
||||
return BadRequest(new { message = "无效的文件路径" });
|
||||
|
||||
if (!System.IO.File.Exists(fullPath))
|
||||
return NotFound(new { message = "文件不存在" });
|
||||
|
||||
// 获取文件的MIME类型
|
||||
if (!_contentTypeProvider.TryGetContentType(fullPath, out var contentType))
|
||||
{
|
||||
contentType = "application/octet-stream";
|
||||
}
|
||||
|
||||
var fileStream = System.IO.File.OpenRead(fullPath);
|
||||
return File(fileStream, contentType);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MilitaryTrainingManagement.Data;
|
||||
using MilitaryTrainingManagement.Models.DTOs;
|
||||
using MilitaryTrainingManagement.Models.Entities;
|
||||
using MilitaryTrainingManagement.Services.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MilitaryTrainingManagement.Controllers;
|
||||
|
||||
|
|
@ -12,10 +15,17 @@ namespace MilitaryTrainingManagement.Controllers;
|
|||
public class OrganizationsController : BaseApiController
|
||||
{
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IAuthenticationService _authService;
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public OrganizationsController(IOrganizationService organizationService)
|
||||
public OrganizationsController(
|
||||
IOrganizationService organizationService,
|
||||
IAuthenticationService authService,
|
||||
ApplicationDbContext context)
|
||||
{
|
||||
_organizationService = organizationService;
|
||||
_authService = authService;
|
||||
_context = context;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
|
|
@ -63,4 +73,39 @@ public class OrganizationsController : BaseApiController
|
|||
await _organizationService.DeleteAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为组织创建账户
|
||||
/// </summary>
|
||||
[HttpPost("{id}/accounts")]
|
||||
public async Task<IActionResult> CreateAccount(int id, [FromBody] CreateAccountRequest request)
|
||||
{
|
||||
var unit = await _organizationService.GetByIdAsync(id);
|
||||
if (unit == null)
|
||||
{
|
||||
return NotFound(new { message = "组织不存在" });
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
var existingUser = await _context.UserAccounts
|
||||
.FirstOrDefaultAsync(u => u.Username == request.Username);
|
||||
if (existingUser != null)
|
||||
{
|
||||
return BadRequest(new { message = "用户名已存在" });
|
||||
}
|
||||
|
||||
var account = new UserAccount
|
||||
{
|
||||
Username = request.Username,
|
||||
PasswordHash = _authService.HashPassword(request.Password),
|
||||
DisplayName = unit.Name + "管理员",
|
||||
OrganizationalUnitId = id,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.UserAccounts.Add(account);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return Ok(new { message = "账户创建成功", username = account.Username });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,10 @@ public class PersonnelController : BaseApiController
|
|||
/// 获取人员列表(基于层级权限控制)
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetPersonnel([FromQuery] bool includeSubordinates = false)
|
||||
public async Task<IActionResult> GetPersonnel(
|
||||
[FromQuery] int pageNumber = 1,
|
||||
[FromQuery] int pageSize = 10,
|
||||
[FromQuery] string? status = null)
|
||||
{
|
||||
var unitId = GetCurrentUnitId();
|
||||
var userLevel = GetCurrentUnitLevel();
|
||||
|
|
@ -42,8 +45,28 @@ public class PersonnelController : BaseApiController
|
|||
return Unauthorized();
|
||||
}
|
||||
|
||||
var personnel = await _personnelService.GetVisiblePersonnelAsync(unitId.Value, userLevel.Value);
|
||||
return Ok(personnel);
|
||||
var allPersonnel = await _personnelService.GetVisiblePersonnelAsync(unitId.Value, userLevel.Value);
|
||||
|
||||
// 状态筛选
|
||||
if (!string.IsNullOrEmpty(status) && Enum.TryParse<PersonnelStatus>(status, out var statusFilter))
|
||||
{
|
||||
allPersonnel = allPersonnel.Where(p => p.Status == statusFilter);
|
||||
}
|
||||
|
||||
var totalCount = allPersonnel.Count();
|
||||
var items = allPersonnel
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToList();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
items,
|
||||
totalCount,
|
||||
pageNumber,
|
||||
pageSize,
|
||||
totalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -136,8 +159,21 @@ public class PersonnelController : BaseApiController
|
|||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create([FromBody] CreatePersonnelRequest request)
|
||||
public async Task<IActionResult> Create([FromForm] CreatePersonnelRequest request,
|
||||
IFormFile? photo, IFormFile? supportingDocuments)
|
||||
{
|
||||
// 返回详细的验证错误
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
var errors = ModelState
|
||||
.Where(x => x.Value?.Errors.Count > 0)
|
||||
.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value?.Errors.Select(e => e.ErrorMessage).ToArray()
|
||||
);
|
||||
return BadRequest(new { message = "验证失败", errors });
|
||||
}
|
||||
|
||||
var unitId = GetCurrentUnitId();
|
||||
if (unitId == null)
|
||||
{
|
||||
|
|
@ -162,15 +198,27 @@ public class PersonnelController : BaseApiController
|
|||
SubmittedByUnitId = unitId.Value
|
||||
};
|
||||
|
||||
var created = await _personnelService.CreateAsync(personnel);
|
||||
return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
|
||||
try
|
||||
{
|
||||
var created = await _personnelService.SubmitPersonnelAsync(personnel, photo, supportingDocuments);
|
||||
return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return BadRequest(new { message = ex.Message });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新人员信息(仅限未审批的人员)
|
||||
/// </summary>
|
||||
[HttpPut("{id}")]
|
||||
public async Task<IActionResult> Update(int id, [FromBody] UpdatePersonnelRequest request)
|
||||
public async Task<IActionResult> Update(int id, [FromForm] UpdatePersonnelRequest request,
|
||||
IFormFile? photo, IFormFile? supportingDocuments)
|
||||
{
|
||||
var personnel = await _personnelService.GetByIdAsync(id);
|
||||
if (personnel == null)
|
||||
|
|
@ -206,6 +254,30 @@ public class PersonnelController : BaseApiController
|
|||
personnel.TrainingParticipation = request.TrainingParticipation;
|
||||
personnel.Achievements = request.Achievements;
|
||||
|
||||
// 处理照片上传
|
||||
if (photo != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(personnel.PhotoPath))
|
||||
{
|
||||
await _fileUploadService.DeleteFileAsync(personnel.PhotoPath);
|
||||
}
|
||||
personnel.PhotoPath = await _fileUploadService.UploadPhotoAsync(photo);
|
||||
}
|
||||
|
||||
// 处理文档上传
|
||||
if (supportingDocuments != null)
|
||||
{
|
||||
var documentPath = await _fileUploadService.UploadDocumentAsync(supportingDocuments);
|
||||
if (string.IsNullOrEmpty(personnel.SupportingDocuments))
|
||||
{
|
||||
personnel.SupportingDocuments = documentPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
personnel.SupportingDocuments += ";" + documentPath;
|
||||
}
|
||||
}
|
||||
|
||||
var updated = await _personnelService.UpdateAsync(personnel);
|
||||
return Ok(updated);
|
||||
}
|
||||
|
|
@ -356,7 +428,7 @@ public class PersonnelController : BaseApiController
|
|||
var canApprove = await _personnelService.CanApprovePersonnelAsync(userId.Value, id);
|
||||
if (!canApprove)
|
||||
{
|
||||
return Forbid("您没有权限审批此人员");
|
||||
return StatusCode(403, new { message = "您没有权限审批此人员" });
|
||||
}
|
||||
|
||||
try
|
||||
|
|
@ -366,7 +438,7 @@ public class PersonnelController : BaseApiController
|
|||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
return BadRequest(new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -387,7 +459,7 @@ public class PersonnelController : BaseApiController
|
|||
var canApprove = await _personnelService.CanApprovePersonnelAsync(userId.Value, id);
|
||||
if (!canApprove)
|
||||
{
|
||||
return Forbid("您没有权限拒绝此人员");
|
||||
return StatusCode(403, new { message = "您没有权限拒绝此人员" });
|
||||
}
|
||||
|
||||
var personnel = await _personnelService.RejectAsync(id, userId.Value);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,15 @@ using System.ComponentModel.DataAnnotations;
|
|||
|
||||
namespace MilitaryTrainingManagement.Models.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// 分配请求项
|
||||
/// </summary>
|
||||
public class DistributionRequestItem
|
||||
{
|
||||
public int TargetUnitId { get; set; }
|
||||
public decimal UnitQuota { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建物资配额请求
|
||||
/// </summary>
|
||||
|
|
@ -35,10 +44,19 @@ public class CreateAllocationRequest
|
|||
public decimal TotalQuota { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分配记录 (目标单位ID -> 配额)
|
||||
/// 分配记录
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "分配记录为必填项")]
|
||||
public Dictionary<int, decimal> Distributions { get; set; } = new();
|
||||
public List<DistributionRequestItem> Distributions { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 转换为字典格式
|
||||
/// </summary>
|
||||
public Dictionary<int, decimal> GetDistributionsDictionary()
|
||||
{
|
||||
return Distributions
|
||||
.Where(d => d.TargetUnitId > 0)
|
||||
.ToDictionary(d => d.TargetUnitId, d => d.UnitQuota);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -13,3 +13,9 @@ public class UpdateOrganizationRequest
|
|||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class CreateAccountRequest
|
||||
{
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,8 @@ builder.Services.AddControllers()
|
|||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles;
|
||||
// 将枚举序列化为字符串
|
||||
options.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
|
||||
});
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
|
|
|
|||
|
|
@ -11,13 +11,12 @@ public class FileUploadService : IFileUploadService
|
|||
private readonly IWebHostEnvironment _environment;
|
||||
private readonly ILogger<FileUploadService> _logger;
|
||||
|
||||
// 2寸照片标准尺寸(像素):35mm x 49mm,300dpi约为413x579像素
|
||||
// 允许一定的误差范围
|
||||
private const int MinPhotoWidth = 300;
|
||||
private const int MaxPhotoWidth = 600;
|
||||
private const int MinPhotoHeight = 400;
|
||||
private const int MaxPhotoHeight = 800;
|
||||
private const long MaxPhotoSize = 5 * 1024 * 1024; // 5MB
|
||||
// 照片尺寸限制(放宽限制以适应各种照片)
|
||||
private const int MinPhotoWidth = 100;
|
||||
private const int MaxPhotoWidth = 4000;
|
||||
private const int MinPhotoHeight = 100;
|
||||
private const int MaxPhotoHeight = 4000;
|
||||
private const long MaxPhotoSize = 10 * 1024 * 1024; // 10MB
|
||||
private const long MaxDocumentSize = 20 * 1024 * 1024; // 20MB
|
||||
|
||||
private static readonly string[] AllowedPhotoExtensions = { ".jpg", ".jpeg", ".png" };
|
||||
|
|
@ -73,7 +72,7 @@ public class FileUploadService : IFileUploadService
|
|||
var width = image.Width;
|
||||
var height = image.Height;
|
||||
|
||||
// 验证2寸照片尺寸要求
|
||||
// 验证照片尺寸(放宽限制)
|
||||
if (width < MinPhotoWidth || width > MaxPhotoWidth)
|
||||
{
|
||||
return PhotoValidationResult.Failure($"照片宽度应在{MinPhotoWidth}-{MaxPhotoWidth}像素之间,当前为{width}像素");
|
||||
|
|
@ -84,13 +83,7 @@ public class FileUploadService : IFileUploadService
|
|||
return PhotoValidationResult.Failure($"照片高度应在{MinPhotoHeight}-{MaxPhotoHeight}像素之间,当前为{height}像素");
|
||||
}
|
||||
|
||||
// 验证宽高比(2寸照片约为35:49 ≈ 0.71)
|
||||
var aspectRatio = (double)width / height;
|
||||
if (aspectRatio < 0.5 || aspectRatio > 0.9)
|
||||
{
|
||||
return PhotoValidationResult.Failure("照片宽高比不符合2寸照片要求(约35:49)");
|
||||
}
|
||||
|
||||
// 不再严格验证宽高比,允许各种照片
|
||||
return PhotoValidationResult.Success(width, height);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
|
|
@ -91,10 +91,11 @@ public class PersonnelService : IPersonnelService
|
|||
if (approvedByUnit == null)
|
||||
throw new ArgumentException("审批单位不存在");
|
||||
|
||||
// 验证人员等级与审批单位层级一致
|
||||
var expectedLevel = (PersonnelLevel)(int)approvedByUnit.Level;
|
||||
if (level != expectedLevel)
|
||||
throw new ArgumentException("人员等级必须与审批单位层级一致");
|
||||
// 验证审批单位层级必须高于或等于人员等级(数值越小层级越高)
|
||||
var unitLevelValue = (int)approvedByUnit.Level;
|
||||
var personnelLevelValue = (int)level;
|
||||
if (unitLevelValue > personnelLevelValue)
|
||||
throw new ArgumentException("审批单位层级不足以审批该等级人才");
|
||||
|
||||
var previousStatus = personnel.Status;
|
||||
var previousLevel = personnel.ApprovedLevel;
|
||||
|
|
@ -528,6 +529,10 @@ public class PersonnelService : IPersonnelService
|
|||
if (personnel == null || personnel.Status != PersonnelStatus.Pending)
|
||||
return false;
|
||||
|
||||
// 同一单位可以审批自己提交的人员
|
||||
if (user.OrganizationalUnitId == personnel.SubmittedByUnitId)
|
||||
return true;
|
||||
|
||||
// 检查用户的组织单位是否是提交单位的上级
|
||||
var isParent = await _organizationService.IsParentUnitAsync(user.OrganizationalUnitId, personnel.SubmittedByUnitId);
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -13,7 +13,7 @@ using System.Reflection;
|
|||
[assembly: System.Reflection.AssemblyCompanyAttribute("MilitaryTrainingManagement")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+dea416c1206a35a2f77e33f37406891d5377f1a6")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+8cada25804844f7e625d0f213b568324580ab12f")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("MilitaryTrainingManagement")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("MilitaryTrainingManagement")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
71761eb666e1de962287ab2d3efffb4d59f9a1ceb181540c49222eadb77fb760
|
||||
947693409f11b6d9a783dd5578eff75f0295de8dfc4524de39ae86b037f3a76a
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
9aa7a804a7b716c6d51cb391fd2292638fb67f08685ae7d440982f042b40fd5e
|
||||
33d3e5abc8ec2790adb522e907e856fe53e06ac3a2e49a9f8cb326340b3b3177
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 524 KiB |
|
|
@ -41,8 +41,7 @@ export const personnelApi = {
|
|||
|
||||
async approve(data: PersonnelApprovalRequest): Promise<Personnel> {
|
||||
const response = await apiClient.post<Personnel>(`/personnel/${data.personnelId}/approve`, {
|
||||
approved: data.approved,
|
||||
comments: data.comments
|
||||
level: data.level
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
|
|||
import { ref, computed } from 'vue'
|
||||
import { authApi } from '@/api/auth'
|
||||
import type { User, LoginRequest } from '@/types'
|
||||
import { OrganizationalLevel, OrganizationalLevelValue } from '@/types'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref<User | null>(null)
|
||||
|
|
@ -11,13 +12,17 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
// 只检查token是否存在
|
||||
const isAuthenticated = computed(() => !!token.value)
|
||||
|
||||
const organizationalLevel = computed(() => user.value?.organizationalLevel ?? 4)
|
||||
// 获取组织级别的数值(用于权限比较)
|
||||
const organizationalLevelNum = computed(() => {
|
||||
if (!user.value?.organizationalLevel) return 4
|
||||
return OrganizationalLevelValue[user.value.organizationalLevel] ?? 4
|
||||
})
|
||||
|
||||
const canManageSubordinates = computed(() => organizationalLevel.value < 4)
|
||||
const canManageSubordinates = computed(() => organizationalLevelNum.value < 4)
|
||||
|
||||
const canCreateAllocations = computed(() => organizationalLevel.value === 1)
|
||||
const canCreateAllocations = computed(() => organizationalLevelNum.value === 1)
|
||||
|
||||
const canApprove = computed(() => organizationalLevel.value < 4)
|
||||
const canApprove = computed(() => organizationalLevelNum.value < 4)
|
||||
|
||||
async function login(credentials: LoginRequest): Promise<boolean> {
|
||||
try {
|
||||
|
|
@ -80,7 +85,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
}
|
||||
|
||||
function hasPermission(requiredLevel: number): boolean {
|
||||
return organizationalLevel.value <= requiredLevel
|
||||
return organizationalLevelNum.value <= requiredLevel
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -88,7 +93,8 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
token,
|
||||
initialized,
|
||||
isAuthenticated,
|
||||
organizationalLevel,
|
||||
organizationalLevel: computed(() => user.value?.organizationalLevel),
|
||||
organizationalLevelNum,
|
||||
canManageSubordinates,
|
||||
canCreateAllocations,
|
||||
canApprove,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,24 @@
|
|||
// Enums
|
||||
export enum OrganizationalLevel {
|
||||
Division = 1, // 师团
|
||||
Regiment = 2, // 团
|
||||
Battalion = 3, // 营
|
||||
Company = 4 // 连
|
||||
Division = 'Division', // 师团
|
||||
Regiment = 'Regiment', // 团
|
||||
Battalion = 'Battalion', // 营
|
||||
Company = 'Company' // 连
|
||||
}
|
||||
|
||||
// 用于权限判断的级别数值映射
|
||||
export const OrganizationalLevelValue: Record<OrganizationalLevel, number> = {
|
||||
[OrganizationalLevel.Division]: 1,
|
||||
[OrganizationalLevel.Regiment]: 2,
|
||||
[OrganizationalLevel.Battalion]: 3,
|
||||
[OrganizationalLevel.Company]: 4
|
||||
}
|
||||
|
||||
export enum PersonnelLevel {
|
||||
Division = 1, // 师级人才
|
||||
Regiment = 2, // 团级人才
|
||||
Battalion = 3, // 营级人才
|
||||
Company = 4 // 连级人才
|
||||
Division = 'Division', // 师级人才
|
||||
Regiment = 'Regiment', // 团级人才
|
||||
Battalion = 'Battalion', // 营级人才
|
||||
Company = 'Company' // 连级人才
|
||||
}
|
||||
|
||||
export enum PersonnelStatus {
|
||||
|
|
@ -192,6 +200,7 @@ export interface PersonnelApprovalRequest {
|
|||
personnelId: number
|
||||
approved: boolean
|
||||
comments?: string
|
||||
level?: string
|
||||
}
|
||||
|
||||
// Approval types
|
||||
|
|
|
|||
|
|
@ -1,66 +1,202 @@
|
|||
<template>
|
||||
<div class="allocation-list">
|
||||
<el-card>
|
||||
<el-card class="list-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>物资配额列表</span>
|
||||
<el-button v-if="authStore.canCreateAllocations" type="primary" @click="$router.push('/allocations/create')">
|
||||
<el-icon><Plus /></el-icon>
|
||||
创建配额
|
||||
</el-button>
|
||||
<div class="header-title">
|
||||
<el-icon class="title-icon" :size="22"><Box /></el-icon>
|
||||
<span>物资配额管理</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索物资名称"
|
||||
clearable
|
||||
style="width: 180px; margin-right: 12px"
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-select v-model="categoryFilter" placeholder="类别筛选" clearable style="width: 120px; margin-right: 12px">
|
||||
<el-option label="弹药" value="弹药" />
|
||||
<el-option label="装备" value="装备" />
|
||||
<el-option label="物资" value="物资" />
|
||||
<el-option label="器材" value="器材" />
|
||||
</el-select>
|
||||
<el-button v-if="authStore.canCreateAllocations" type="primary" @click="$router.push('/allocations/create')">
|
||||
<el-icon><Plus /></el-icon>
|
||||
创建配额
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="allocations" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="category" label="类别" width="120" />
|
||||
<el-table-column prop="materialName" label="物资名称" />
|
||||
<el-table-column prop="unit" label="单位" width="80" />
|
||||
<el-table-column prop="totalQuota" label="总配额" width="120" />
|
||||
<el-table-column prop="createdByUnitName" label="创建单位" width="150" />
|
||||
<el-table-column prop="createdAt" label="创建时间" width="180">
|
||||
<el-table
|
||||
:data="filteredAllocations"
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
highlight-current-row
|
||||
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }"
|
||||
>
|
||||
<el-table-column prop="category" label="类别" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.createdAt) }}
|
||||
<el-tag :type="getCategoryTagType(row.category)" effect="plain" size="small">
|
||||
{{ row.category }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column prop="materialName" label="物资名称" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" text size="small" @click="handleViewDistribution(row)">查看分配</el-button>
|
||||
<el-button v-if="authStore.canCreateAllocations" type="warning" text size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="authStore.canCreateAllocations" type="danger" text size="small" @click="handleDelete(row)">删除</el-button>
|
||||
<span class="material-name">{{ row.materialName }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="unit" label="单位" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="unit-text">{{ row.unit }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalQuota" label="总配额" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="quota-value">{{ formatNumber(row.totalQuota) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分配情况" width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="distribution-info">
|
||||
<span class="dist-count">{{ row.distributions?.length || 0 }} 个单位</span>
|
||||
<el-progress
|
||||
:percentage="getDistributionPercentage(row)"
|
||||
:stroke-width="6"
|
||||
:show-text="false"
|
||||
style="width: 60px; margin-left: 8px"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdByUnitName" label="创建单位" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="createdAt" label="创建时间" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="time-cell">{{ formatDate(row.createdAt) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="action-buttons">
|
||||
<el-tooltip content="查看分配" placement="top">
|
||||
<el-button type="primary" link size="small" @click="handleViewDistribution(row)">
|
||||
<el-icon><View /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="authStore.canCreateAllocations" 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="authStore.canCreateAllocations" 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-pagination
|
||||
:current-page="pagination.pageNumber"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
class="pagination"
|
||||
@update:current-page="handlePageChange"
|
||||
@update:page-size="handleSizeChange"
|
||||
/>
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.pageNumber"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- Distribution Dialog -->
|
||||
<el-dialog v-model="showDistributionDialog" title="配额分配详情" width="800px">
|
||||
<el-table :data="distributions" style="width: 100%">
|
||||
<el-table-column prop="targetUnitName" label="目标单位" />
|
||||
<el-table-column prop="unitQuota" label="分配配额" width="120" />
|
||||
<el-table-column prop="actualCompletion" label="实际完成" width="120">
|
||||
<el-dialog
|
||||
v-model="showDistributionDialog"
|
||||
:title="`配额分配详情 - ${selectedAllocation?.materialName || ''}`"
|
||||
width="900px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="distribution-summary">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<div class="summary-item">
|
||||
<div class="summary-label">物资类别</div>
|
||||
<div class="summary-value">
|
||||
<el-tag :type="getCategoryTagType(selectedAllocation?.category)" size="small">
|
||||
{{ selectedAllocation?.category }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="summary-item">
|
||||
<div class="summary-label">总配额</div>
|
||||
<div class="summary-value highlight">{{ formatNumber(selectedAllocation?.totalQuota || 0) }} {{ selectedAllocation?.unit }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="summary-item">
|
||||
<div class="summary-label">已分配</div>
|
||||
<div class="summary-value">{{ formatNumber(getTotalDistributed()) }} {{ selectedAllocation?.unit }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="summary-item">
|
||||
<div class="summary-label">分配单位数</div>
|
||||
<div class="summary-value">{{ distributions.length }} 个</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="distributions"
|
||||
style="width: 100%"
|
||||
stripe
|
||||
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }"
|
||||
>
|
||||
<el-table-column prop="targetUnitName" label="目标单位" min-width="150">
|
||||
<template #default="{ row }">
|
||||
{{ row.actualCompletion ?? '-' }}
|
||||
<span class="unit-name">{{ row.targetUnitName }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="completionRate" label="完成率" width="120">
|
||||
<el-table-column prop="unitQuota" label="分配配额" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-progress :percentage="Math.round(row.completionRate * 100)" :status="getProgressStatus(row.completionRate)" />
|
||||
<span class="quota-value">{{ formatNumber(row.unitQuota) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reportedAt" label="上报时间" width="180">
|
||||
<el-table-column prop="actualCompletion" label="实际完成" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ row.reportedAt ? formatDate(row.reportedAt) : '-' }}
|
||||
<span :class="['completion-value', row.actualCompletion ? '' : 'no-data']">
|
||||
{{ row.actualCompletion ? formatNumber(row.actualCompletion) : '未上报' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="completionRate" label="完成率" width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="progress-cell">
|
||||
<el-progress
|
||||
:percentage="Math.round(row.completionRate * 100)"
|
||||
:status="getProgressStatus(row.completionRate)"
|
||||
:stroke-width="8"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reportedAt" label="上报时间" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="time-cell">{{ row.reportedAt ? formatDate(row.reportedAt) : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
|
@ -69,10 +205,10 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { Plus, Search, View, Edit, Delete, Box } from '@element-plus/icons-vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { allocationsApi } from '@/api'
|
||||
import type { MaterialAllocation, AllocationDistribution } from '@/types'
|
||||
|
|
@ -84,6 +220,9 @@ const allocations = ref<MaterialAllocation[]>([])
|
|||
const distributions = ref<AllocationDistribution[]>([])
|
||||
const loading = ref(false)
|
||||
const showDistributionDialog = ref(false)
|
||||
const searchKeyword = ref('')
|
||||
const categoryFilter = ref('')
|
||||
const selectedAllocation = ref<MaterialAllocation | null>(null)
|
||||
|
||||
const pagination = reactive({
|
||||
pageNumber: 1,
|
||||
|
|
@ -91,16 +230,56 @@ const pagination = reactive({
|
|||
total: 0
|
||||
})
|
||||
|
||||
const filteredAllocations = computed(() => {
|
||||
let result = allocations.value
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
result = result.filter(a => a.materialName.toLowerCase().includes(keyword))
|
||||
}
|
||||
if (categoryFilter.value) {
|
||||
result = result.filter(a => a.category === categoryFilter.value)
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
return new Date(dateStr).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
function formatNumber(num: number): string {
|
||||
return num.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
function getCategoryTagType(category: string): string {
|
||||
switch (category) {
|
||||
case '弹药': return 'danger'
|
||||
case '装备': return 'warning'
|
||||
case '物资': return 'success'
|
||||
case '器材': return 'info'
|
||||
default: return ''
|
||||
}
|
||||
}
|
||||
|
||||
function getProgressStatus(rate: number): string {
|
||||
if (rate >= 1) return 'success'
|
||||
if (rate >= 0.6) return ''
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
function getDistributionPercentage(allocation: MaterialAllocation): number {
|
||||
if (!allocation.distributions || allocation.distributions.length === 0) return 0
|
||||
const distributed = allocation.distributions.reduce((sum, d) => sum + (d.unitQuota || 0), 0)
|
||||
return Math.round((distributed / allocation.totalQuota) * 100)
|
||||
}
|
||||
|
||||
function getTotalDistributed(): number {
|
||||
return distributions.value.reduce((sum, d) => sum + (d.unitQuota || 0), 0)
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.pageNumber = 1
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
pagination.pageNumber = page
|
||||
loadAllocations()
|
||||
|
|
@ -129,12 +308,9 @@ async function loadAllocations() {
|
|||
}
|
||||
|
||||
async function handleViewDistribution(allocation: MaterialAllocation) {
|
||||
try {
|
||||
distributions.value = await allocationsApi.getDistributions(allocation.id)
|
||||
showDistributionDialog.value = true
|
||||
} catch {
|
||||
ElMessage.error('加载分配详情失败')
|
||||
}
|
||||
selectedAllocation.value = allocation
|
||||
distributions.value = allocation.distributions || []
|
||||
showDistributionDialog.value = true
|
||||
}
|
||||
|
||||
function handleEdit(allocation: MaterialAllocation) {
|
||||
|
|
@ -143,9 +319,16 @@ function handleEdit(allocation: MaterialAllocation) {
|
|||
|
||||
async function handleDelete(allocation: MaterialAllocation) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该配额吗?', '确认删除', {
|
||||
type: 'warning'
|
||||
})
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除物资配额 "${allocation.materialName}" 吗?此操作不可恢复。`,
|
||||
'确认删除',
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消',
|
||||
confirmButtonClass: 'el-button--danger'
|
||||
}
|
||||
)
|
||||
await allocationsApi.delete(allocation.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadAllocations()
|
||||
|
|
@ -160,14 +343,148 @@ onMounted(() => {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
.allocation-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.list-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
margin-right: 8px;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.material-name {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.unit-text {
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.quota-value {
|
||||
font-weight: 600;
|
||||
color: #409EFF;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.distribution-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dist-count {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.time-cell {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.action-buttons .el-button {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Distribution Dialog Styles */
|
||||
.distribution-summary {
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.summary-value.highlight {
|
||||
color: #409EFF;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.unit-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.completion-value {
|
||||
font-weight: 500;
|
||||
color: #67C23A;
|
||||
}
|
||||
|
||||
.completion-value.no-data {
|
||||
color: #C0C4CC;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.progress-cell {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
:deep(.el-table .el-table__row) {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
:deep(.el-table .el-table__row:hover) {
|
||||
background-color: #ecf5ff !important;
|
||||
}
|
||||
|
||||
:deep(.el-card__header) {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__header) {
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,35 @@
|
|||
<template>
|
||||
<div class="personnel-list">
|
||||
<el-card>
|
||||
<el-card class="list-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>人员管理</span>
|
||||
<div class="header-title">
|
||||
<el-icon class="title-icon" :size="22"><User /></el-icon>
|
||||
<span>人员管理</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索姓名/职位"
|
||||
clearable
|
||||
style="width: 180px; margin-right: 12px"
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-select v-model="statusFilter" placeholder="状态筛选" clearable style="width: 120px; margin-right: 12px">
|
||||
<el-option label="待审批" value="Pending" />
|
||||
<el-option label="已批准" value="Approved" />
|
||||
<el-option label="已拒绝" value="Rejected" />
|
||||
<el-option label="待审批" value="Pending">
|
||||
<el-tag type="warning" size="small">待审批</el-tag>
|
||||
</el-option>
|
||||
<el-option label="已批准" value="Approved">
|
||||
<el-tag type="success" size="small">已批准</el-tag>
|
||||
</el-option>
|
||||
<el-option label="已拒绝" value="Rejected">
|
||||
<el-tag type="danger" size="small">已拒绝</el-tag>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-button type="primary" @click="$router.push('/personnel/create')">
|
||||
<el-icon><Plus /></el-icon>
|
||||
|
|
@ -18,74 +39,108 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="personnel" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="name" label="姓名" width="100" />
|
||||
<el-table-column prop="position" label="职位" width="120" />
|
||||
<el-table-column prop="rank" label="军衔" width="100" />
|
||||
<el-table-column prop="gender" label="性别" width="60" />
|
||||
<el-table-column prop="age" label="年龄" width="60" />
|
||||
<el-table-column prop="submittedByUnitName" label="提交单位" width="120" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<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 }">
|
||||
<el-tag :type="getStatusTagType(row.status)">{{ getStatusName(row.status) }}</el-tag>
|
||||
<span class="name-cell">{{ row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="approvedLevel" label="人员等级" width="100">
|
||||
<el-table-column prop="gender" label="性别" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.approvedLevel" :type="getLevelTagType(row.approvedLevel)">
|
||||
<el-tag :type="row.gender === '男' ? '' : '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="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusTagType(row.status)" effect="dark" size="small">
|
||||
{{ getStatusName(row.status) }}
|
||||
</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>-</span>
|
||||
<span v-else class="no-data">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="submittedAt" label="提交时间" width="160">
|
||||
<el-table-column prop="submittedAt" label="提交时间" width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.submittedAt) }}
|
||||
<span class="time-cell">{{ formatDate(row.submittedAt) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" text size="small" @click="handleView(row)">查看</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'Pending' && authStore.canApprove"
|
||||
type="success"
|
||||
text
|
||||
size="small"
|
||||
@click="handleApprove(row)"
|
||||
>
|
||||
审批
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'Pending'"
|
||||
type="warning"
|
||||
text
|
||||
size="small"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" text size="small" @click="handleDelete(row)">删除</el-button>
|
||||
<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" 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 === 'Pending'" content="编辑" placement="top">
|
||||
<el-button type="warning" link size="small" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip 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-pagination
|
||||
:current-page="pagination.pageNumber"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
class="pagination"
|
||||
@update:current-page="handlePageChange"
|
||||
@update:page-size="handleSizeChange"
|
||||
/>
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.pageNumber"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- Approval Dialog -->
|
||||
<el-dialog v-model="showApprovalDialog" title="审批人员" width="500px">
|
||||
<el-dialog v-model="showApprovalDialog" title="审批人员" width="500px" :close-on-click-modal="false">
|
||||
<el-form :model="approvalForm" label-width="100px">
|
||||
<el-form-item label="人员姓名">
|
||||
<el-input :value="selectedPerson?.name" disabled />
|
||||
<el-descriptions :column="2" border class="approval-info">
|
||||
<el-descriptions-item label="姓名">{{ selectedPerson?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="职位">{{ selectedPerson?.position }}</el-descriptions-item>
|
||||
<el-descriptions-item label="军衔">{{ selectedPerson?.rank }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属单位">{{ selectedPerson?.submittedByUnitName }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-form-item label="人员等级" style="margin-top: 20px">
|
||||
<el-select v-model="approvalForm.level" placeholder="请选择人员等级" style="width: 100%">
|
||||
<el-option label="师级人才" value="Division" />
|
||||
<el-option label="团级人才" value="Regiment" />
|
||||
<el-option label="营级人才" value="Battalion" />
|
||||
<el-option label="连级人才" value="Company" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="审批意见">
|
||||
<el-input v-model="approvalForm.comments" type="textarea" rows="3" placeholder="请输入审批意见(可选)" />
|
||||
|
|
@ -93,8 +148,12 @@
|
|||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showApprovalDialog = false">取消</el-button>
|
||||
<el-button type="danger" :loading="approving" @click="submitApproval(false)">拒绝</el-button>
|
||||
<el-button type="success" :loading="approving" @click="submitApproval(true)">批准</el-button>
|
||||
<el-button type="danger" :loading="approving" @click="submitApproval(false)">
|
||||
<el-icon><Close /></el-icon> 拒绝
|
||||
</el-button>
|
||||
<el-button type="success" :loading="approving" @click="submitApproval(true)">
|
||||
<el-icon><Check /></el-icon> 批准
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
|
@ -104,7 +163,7 @@
|
|||
import { ref, reactive, watch, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { Plus, Search, View, Edit, Delete, Check, Close, User } from '@element-plus/icons-vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { personnelApi } from '@/api'
|
||||
import type { Personnel } from '@/types'
|
||||
|
|
@ -116,6 +175,7 @@ const authStore = useAuthStore()
|
|||
const personnel = ref<Personnel[]>([])
|
||||
const loading = ref(false)
|
||||
const statusFilter = ref('')
|
||||
const searchKeyword = ref('')
|
||||
const showApprovalDialog = ref(false)
|
||||
const approving = ref(false)
|
||||
const selectedPerson = ref<Personnel | null>(null)
|
||||
|
|
@ -127,7 +187,8 @@ const pagination = reactive({
|
|||
})
|
||||
|
||||
const approvalForm = reactive({
|
||||
comments: ''
|
||||
comments: '',
|
||||
level: 'Company'
|
||||
})
|
||||
|
||||
function getStatusName(status: PersonnelStatus): string {
|
||||
|
|
@ -172,6 +233,11 @@ function formatDate(dateStr: string): string {
|
|||
return new Date(dateStr).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.pageNumber = 1
|
||||
loadPersonnel()
|
||||
}
|
||||
|
||||
async function loadPersonnel() {
|
||||
loading.value = true
|
||||
try {
|
||||
|
|
@ -180,7 +246,16 @@ async function loadPersonnel() {
|
|||
pageSize: pagination.pageSize,
|
||||
status: statusFilter.value || undefined
|
||||
})
|
||||
personnel.value = response.items
|
||||
// 前端搜索过滤
|
||||
let items = response.items
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
items = items.filter(p =>
|
||||
p.name.toLowerCase().includes(keyword) ||
|
||||
p.position.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
personnel.value = items
|
||||
pagination.total = response.totalCount
|
||||
} catch {
|
||||
ElMessage.error('加载人员列表失败')
|
||||
|
|
@ -200,6 +275,7 @@ function handleEdit(person: Personnel) {
|
|||
function handleApprove(person: Personnel) {
|
||||
selectedPerson.value = person
|
||||
approvalForm.comments = ''
|
||||
approvalForm.level = 'Company'
|
||||
showApprovalDialog.value = true
|
||||
}
|
||||
|
||||
|
|
@ -212,7 +288,8 @@ async function submitApproval(approved: boolean) {
|
|||
await personnelApi.approve({
|
||||
personnelId: selectedPerson.value.id,
|
||||
approved: true,
|
||||
comments: approvalForm.comments
|
||||
comments: approvalForm.comments,
|
||||
level: approvalForm.level
|
||||
})
|
||||
ElMessage.success('审批通过')
|
||||
} else {
|
||||
|
|
@ -234,9 +311,16 @@ async function submitApproval(approved: boolean) {
|
|||
|
||||
async function handleDelete(person: Personnel) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该人员记录吗?', '确认删除', {
|
||||
type: 'warning'
|
||||
})
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除人员 "${person.name}" 的记录吗?此操作不可恢复。`,
|
||||
'确认删除',
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消',
|
||||
confirmButtonClass: 'el-button--danger'
|
||||
}
|
||||
)
|
||||
await personnelApi.delete(person.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadPersonnel()
|
||||
|
|
@ -267,19 +351,82 @@ onMounted(() => {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
.personnel-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.list-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
margin-right: 8px;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
.name-cell {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.time-cell {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.action-buttons .el-button {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.approval-info {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-table .el-table__row) {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
:deep(.el-table .el-table__row:hover) {
|
||||
background-color: #ecf5ff !important;
|
||||
}
|
||||
|
||||
:deep(.el-card__header) {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user