新增生产-前缫看台记录,并导入车头工/挡车工看台记录历史数据
功能: - 后端新增 KtjlCtgs / KtjlDcgs 两张表、KtjlService 与 api/prod/ktjl 接口 (按日期+车间+班次查询、整组先删后建保存、整组删除、按看台排班带出默认记录、工种下拉) - 挡车数按 F9 口径自动重算(与他人接壤的边界车位各扣 0.5),机组由车位反查机组车台 - 前端新增 /prod/qianxian/ktjl 看台记录页:挡车工/车头工双页签、删除、录入(新增)、录入(指定日期) - 录入弹窗校验对齐 F9:个人时长不超标准、挡车数/看台时长匹配、车头工每机组≥2 人且时长为标准时长整数倍 - 生产-前缫菜单新增「看台记录」,权限 prod:ktjl:list 数据导入: - 新增内嵌资源 biz_prod_qs_ktjl_ctg.csv / biz_prod_qs_ktjl_dcg.csv(约 5 万行,采用紧凑 CSV) - 剔除软删除行、日期异常行、部门无法映射与职工不存在的行;日期由 Excel 序列号转换 - 车头工 12693 行、挡车工 38048 行入库,日期范围 2025-12-16 ~ 2026-09-11 - 表内已有数据则整体跳过,分批 1000 行插入 其他:同步工作进度-13 文档与开发数据库快照
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
using F9WebAiTest.Application.Common;
|
||||
using F9WebAiTest.Application.Dtos.Prod;
|
||||
using F9WebAiTest.Application.Services.Prod;
|
||||
using F9WebAiTest.Domain.Common;
|
||||
using F9WebAiTest.Domain.Enums.SYS.Employees;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace F9WebAiTest.Api.Controllers.Prod;
|
||||
|
||||
/// <summary>看台记录(生产-前缫):车头工 / 挡车工</summary>
|
||||
[ApiController]
|
||||
[Route("api/prod/ktjl")]
|
||||
[Authorize]
|
||||
public class KtjlController : ControllerBase
|
||||
{
|
||||
private readonly IKtjlService _svc;
|
||||
public KtjlController(IKtjlService svc) => _svc = svc;
|
||||
|
||||
// ---------- 查询 ----------
|
||||
|
||||
/// <summary>挡车工看台记录(某日某车间某班次)</summary>
|
||||
[HttpGet("dcg")]
|
||||
public async Task<IActionResult> GetDcg([FromQuery] DateTime date, [FromQuery] long depId,
|
||||
[FromQuery] Prod_Banci_Enum banci = Prod_Banci_Enum.甲, [FromQuery] string like = "")
|
||||
=> Ok(ApiResult<List<KtjldcgReadDto_View>>.Ok(
|
||||
await _svc.GetDcgAsync(date, depId, banci, like ?? string.Empty)));
|
||||
|
||||
/// <summary>车头工看台记录</summary>
|
||||
[HttpGet("ctg")]
|
||||
public async Task<IActionResult> GetCtg([FromQuery] DateTime date, [FromQuery] long depId,
|
||||
[FromQuery] Prod_Banci_Enum banci = Prod_Banci_Enum.甲, [FromQuery] string like = "")
|
||||
=> Ok(ApiResult<List<KtjlctgReadDto_View>>.Ok(
|
||||
await _svc.GetCtgAsync(date, depId, banci, like ?? string.Empty)));
|
||||
|
||||
// ---------- 录入(新增):按看台排班带出默认记录 ----------
|
||||
|
||||
[HttpGet("dcg/defaults")]
|
||||
public async Task<IActionResult> GetDcgDefaults([FromQuery] DateTime date, [FromQuery] long depId,
|
||||
[FromQuery] Prod_Banci_Enum banci = Prod_Banci_Enum.甲)
|
||||
{
|
||||
try { return Ok(ApiResult<List<KtjldcgReadDto_View>>.Ok(await _svc.BuildDcgDefaultsAsync(date, depId, banci))); }
|
||||
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
|
||||
}
|
||||
|
||||
[HttpGet("ctg/defaults")]
|
||||
public async Task<IActionResult> GetCtgDefaults([FromQuery] DateTime date, [FromQuery] long depId,
|
||||
[FromQuery] Prod_Banci_Enum banci = Prod_Banci_Enum.甲)
|
||||
{
|
||||
try { return Ok(ApiResult<List<KtjlctgReadDto_View>>.Ok(await _svc.BuildCtgDefaultsAsync(date, depId, banci))); }
|
||||
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
|
||||
}
|
||||
|
||||
// ---------- 整组保存 ----------
|
||||
|
||||
[HttpPost("dcg/batch")]
|
||||
public async Task<IActionResult> SaveDcg([FromBody] KtjldcgBatchDto dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var count = await _svc.SaveDcgAsync(dto);
|
||||
return Ok(ApiResult<object>.Ok(count, count > 0 ? $"已保存 {count} 条看台记录" : "已清空该班次看台记录"));
|
||||
}
|
||||
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
|
||||
}
|
||||
|
||||
[HttpPost("ctg/batch")]
|
||||
public async Task<IActionResult> SaveCtg([FromBody] KtjlctgBatchDto dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var count = await _svc.SaveCtgAsync(dto);
|
||||
return Ok(ApiResult<object>.Ok(count, count > 0 ? $"已保存 {count} 条看台记录" : "已清空该班次看台记录"));
|
||||
}
|
||||
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
|
||||
}
|
||||
|
||||
// ---------- 整组删除 ----------
|
||||
|
||||
[HttpDelete("dcg")]
|
||||
public async Task<IActionResult> DeleteDcg([FromQuery] DateTime date, [FromQuery] long depId,
|
||||
[FromQuery] Prod_Banci_Enum banci = Prod_Banci_Enum.甲)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _svc.DeleteDcgAsync(date, depId, banci);
|
||||
return Ok(ApiResult<object>.Ok(null, "删除成功"));
|
||||
}
|
||||
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
|
||||
}
|
||||
|
||||
[HttpDelete("ctg")]
|
||||
public async Task<IActionResult> DeleteCtg([FromQuery] DateTime date, [FromQuery] long depId,
|
||||
[FromQuery] Prod_Banci_Enum banci = Prod_Banci_Enum.甲)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _svc.DeleteCtgAsync(date, depId, banci);
|
||||
return Ok(ApiResult<object>.Ok(null, "删除成功"));
|
||||
}
|
||||
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
|
||||
}
|
||||
|
||||
/// <summary>工种下拉</summary>
|
||||
[HttpGet("worktypes")]
|
||||
public async Task<IActionResult> GetWorktypes()
|
||||
=> Ok(ApiResult<List<ProdWorktypeOptionDto>>.Ok(await _svc.GetWorktypeOptionsAsync()));
|
||||
}
|
||||
Binary file not shown.
@@ -28,6 +28,7 @@ public static class DependencyInjection
|
||||
services.AddScoped<IHbjyService, HbjyService>();
|
||||
services.AddScoped<IChejianBanciService, ChejianBanciService>();
|
||||
services.AddScoped<IKtpbService, KtpbService>();
|
||||
services.AddScoped<IKtjlService, KtjlService>();
|
||||
services.AddScoped<INotificationService, NotificationService>();
|
||||
services.AddScoped<IReportService, ReportService>();
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
using F9WebAiTest.Domain.Enums.SYS.Employees;
|
||||
|
||||
namespace F9WebAiTest.Application.Dtos.Prod;
|
||||
|
||||
// ==================== 车头工看台记录 ====================
|
||||
|
||||
public class KtjlctgReadDto_View
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
public long DepId { get; set; }
|
||||
public string DepName { get; set; } = "";
|
||||
public Prod_Banci_Enum Banci { get; set; }
|
||||
public string BanciName { get; set; } = "";
|
||||
public int Jizu { get; set; }
|
||||
public decimal Hours { get; set; }
|
||||
public long EmployeeId { get; set; }
|
||||
public string EmployeeName { get; set; } = "";
|
||||
public long EmployeeDepId { get; set; }
|
||||
public string EmployeeDepName { get; set; } = "";
|
||||
public long WorktypeId { get; set; }
|
||||
public string WorktypeName { get; set; } = "";
|
||||
public bool NoChanliang { get; set; }
|
||||
}
|
||||
|
||||
// ==================== 挡车工看台记录 ====================
|
||||
|
||||
public class KtjldcgReadDto_View
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
public long DepId { get; set; }
|
||||
public string DepName { get; set; } = "";
|
||||
public Prod_Banci_Enum Banci { get; set; }
|
||||
public string BanciName { get; set; } = "";
|
||||
public int Jizu { get; set; }
|
||||
public int ChetaiStart { get; set; }
|
||||
public int ChetaiEnd { get; set; }
|
||||
public decimal DangcheNum { get; set; }
|
||||
public decimal Hours { get; set; }
|
||||
public long EmployeeId { get; set; }
|
||||
public string EmployeeName { get; set; } = "";
|
||||
public long EmployeeDepId { get; set; }
|
||||
public string EmployeeDepName { get; set; } = "";
|
||||
public long WorktypeId { get; set; }
|
||||
public string WorktypeName { get; set; } = "";
|
||||
public bool NoChanliang { get; set; }
|
||||
}
|
||||
|
||||
// ==================== 整组保存 ====================
|
||||
|
||||
/// <summary>看台记录整组保存(按 日期+车间+班次 先删后建,对齐 F9 的 SaveCommand)</summary>
|
||||
public class KtjlctgBatchDto
|
||||
{
|
||||
public DateTime Date { get; set; }
|
||||
public long DepId { get; set; }
|
||||
public Prod_Banci_Enum Banci { get; set; }
|
||||
public List<KtjlctgReadDto_View> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
public class KtjldcgBatchDto
|
||||
{
|
||||
public DateTime Date { get; set; }
|
||||
public long DepId { get; set; }
|
||||
public Prod_Banci_Enum Banci { get; set; }
|
||||
public List<KtjldcgReadDto_View> Items { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
using F9WebAiTest.Application.Common;
|
||||
using F9WebAiTest.Application.Dtos.Prod;
|
||||
using F9WebAiTest.Domain.Common;
|
||||
using F9WebAiTest.Domain.Entities.Base;
|
||||
using F9WebAiTest.Domain.Entities.Prod;
|
||||
using F9WebAiTest.Domain.Enums.SYS.Employees;
|
||||
using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments;
|
||||
using SysEmp = F9WebAiTest.Domain.Entities.SYS.Employees;
|
||||
using SysWt = F9WebAiTest.Domain.Entities.SYS.WorkTypes;
|
||||
|
||||
namespace F9WebAiTest.Application.Services.Prod;
|
||||
|
||||
/// <summary>
|
||||
/// 看台记录(生产-前缫):按「日期 + 车间 + 班次」维护车头工 / 挡车工的实际看台情况。
|
||||
/// 对应 F9 Page_Ktjl(列表)、Page_Ktjl_Dcg_Edit / Page_Ktjl_Ctg_Edit(录入弹窗)与
|
||||
/// Ktjl_Dcg_SaveCommand / Ktjl_Ctg_SaveCommand(整组先删后建)。
|
||||
/// </summary>
|
||||
public interface IKtjlService
|
||||
{
|
||||
// 查询
|
||||
Task<List<KtjlctgReadDto_View>> GetCtgAsync(DateTime date, long depId, Prod_Banci_Enum banci, string like);
|
||||
Task<List<KtjldcgReadDto_View>> GetDcgAsync(DateTime date, long depId, Prod_Banci_Enum banci, string like);
|
||||
|
||||
// 保存 / 删除(整组)
|
||||
Task<int> SaveCtgAsync(KtjlctgBatchDto dto);
|
||||
Task<int> SaveDcgAsync(KtjldcgBatchDto dto);
|
||||
Task DeleteCtgAsync(DateTime date, long depId, Prod_Banci_Enum banci);
|
||||
Task DeleteDcgAsync(DateTime date, long depId, Prod_Banci_Enum banci);
|
||||
|
||||
/// <summary>录入(新增):按看台排班 + 该车间工种职工,带出默认记录</summary>
|
||||
Task<List<KtjlctgReadDto_View>> BuildCtgDefaultsAsync(DateTime date, long depId, Prod_Banci_Enum banci);
|
||||
Task<List<KtjldcgReadDto_View>> BuildDcgDefaultsAsync(DateTime date, long depId, Prod_Banci_Enum banci);
|
||||
|
||||
/// <summary>工种下拉(用于筛选/展示)</summary>
|
||||
Task<List<ProdWorktypeOptionDto>> GetWorktypeOptionsAsync();
|
||||
}
|
||||
|
||||
public class ProdWorktypeOptionDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
}
|
||||
|
||||
public class KtjlService : IKtjlService
|
||||
{
|
||||
private const decimal DefaultHours = 9m;
|
||||
|
||||
private readonly IRepository<KtjlCtg> _ctgRepo;
|
||||
private readonly IRepository<KtjlDcg> _dcgRepo;
|
||||
private readonly IRepository<KtpbCtg> _ktpbCtgRepo;
|
||||
private readonly IRepository<KtpbDcg> _ktpbDcgRepo;
|
||||
private readonly IRepository<JizuChetai> _jzctRepo;
|
||||
private readonly IRepository<SysDept.Department> _deptRepo;
|
||||
private readonly IRepository<SysEmp.Employee> _empRepo;
|
||||
private readonly IRepository<SysWt.WorkType> _wtRepo;
|
||||
|
||||
public KtjlService(IRepository<KtjlCtg> ctgRepo, IRepository<KtjlDcg> dcgRepo,
|
||||
IRepository<KtpbCtg> ktpbCtgRepo, IRepository<KtpbDcg> ktpbDcgRepo,
|
||||
IRepository<JizuChetai> jzctRepo, IRepository<SysDept.Department> deptRepo,
|
||||
IRepository<SysEmp.Employee> empRepo, IRepository<SysWt.WorkType> wtRepo)
|
||||
{
|
||||
_ctgRepo = ctgRepo;
|
||||
_dcgRepo = dcgRepo;
|
||||
_ktpbCtgRepo = ktpbCtgRepo;
|
||||
_ktpbDcgRepo = ktpbDcgRepo;
|
||||
_jzctRepo = jzctRepo;
|
||||
_deptRepo = deptRepo;
|
||||
_empRepo = empRepo;
|
||||
_wtRepo = wtRepo;
|
||||
}
|
||||
|
||||
// ==================== 查询 ====================
|
||||
|
||||
public async Task<List<KtjlctgReadDto_View>> GetCtgAsync(DateTime date, long depId, Prod_Banci_Enum banci,
|
||||
string like)
|
||||
{
|
||||
var d = date.Date;
|
||||
var ctx = await BuildContextAsync();
|
||||
var rows = (await _ctgRepo.FindAsync(x => x.Date == d && x.DeptId == depId && x.Banci == banci))
|
||||
.OrderBy(x => x.Jizu).ThenBy(x => x.Hours).ToList();
|
||||
var views = rows.Select(x => ToCtgView(x, ctx)).ToList();
|
||||
return Filter(views, like, v => $"{v.EmployeeName}{v.WorktypeName}{v.EmployeeDepName}{v.Jizu}");
|
||||
}
|
||||
|
||||
public async Task<List<KtjldcgReadDto_View>> GetDcgAsync(DateTime date, long depId, Prod_Banci_Enum banci,
|
||||
string like)
|
||||
{
|
||||
var d = date.Date;
|
||||
var ctx = await BuildContextAsync();
|
||||
var rows = (await _dcgRepo.FindAsync(x => x.Date == d && x.DeptId == depId && x.Banci == banci))
|
||||
.OrderBy(x => x.Jizu).ThenBy(x => x.ChetaiStart).ThenBy(x => x.Hours).ToList();
|
||||
var views = rows.Select(x => ToDcgView(x, ctx)).ToList();
|
||||
return Filter(views, like,
|
||||
v => $"{v.EmployeeName}{v.WorktypeName}{v.EmployeeDepName}{v.Jizu}{v.ChetaiStart}{v.ChetaiEnd}");
|
||||
}
|
||||
|
||||
private static List<T> Filter<T>(List<T> rows, string like, Func<T, string> selector)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(like)) return rows;
|
||||
var kw = like.Trim();
|
||||
return rows.Where(x => selector(x).Contains(kw, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
|
||||
// ==================== 保存 ====================
|
||||
|
||||
public async Task<int> SaveCtgAsync(KtjlctgBatchDto dto)
|
||||
{
|
||||
if (dto.DepId <= 0) throw new ServiceException("请选择车间");
|
||||
var items = dto.Items ?? new List<KtjlctgReadDto_View>();
|
||||
foreach (var it in items)
|
||||
{
|
||||
if (it.EmployeeId <= 0) throw new ServiceException("不能有空数据(存在未选择职工的记录)");
|
||||
if (it.Hours < 0) throw new ServiceException($"「{it.EmployeeName}」的时长不能为负数");
|
||||
}
|
||||
|
||||
var d = dto.Date.Date;
|
||||
await DeleteCtgCoreAsync(d, dto.DepId, dto.Banci);
|
||||
|
||||
foreach (var it in items)
|
||||
{
|
||||
await _ctgRepo.AddAsync(new KtjlCtg
|
||||
{
|
||||
Date = d,
|
||||
DeptId = dto.DepId,
|
||||
Banci = dto.Banci,
|
||||
Jizu = it.Jizu,
|
||||
Hours = it.Hours,
|
||||
EmployeeId = it.EmployeeId,
|
||||
EmployeeDepId = it.EmployeeDepId,
|
||||
WorktypeId = it.WorktypeId,
|
||||
WorktypeName = it.WorktypeName ?? string.Empty,
|
||||
NoChanliang = it.NoChanliang
|
||||
});
|
||||
}
|
||||
await _ctgRepo.SaveChangesAsync();
|
||||
return items.Count;
|
||||
}
|
||||
|
||||
public async Task<int> SaveDcgAsync(KtjldcgBatchDto dto)
|
||||
{
|
||||
if (dto.DepId <= 0) throw new ServiceException("请选择车间");
|
||||
var items = dto.Items ?? new List<KtjldcgReadDto_View>();
|
||||
foreach (var it in items)
|
||||
{
|
||||
if (it.EmployeeId <= 0) throw new ServiceException("不能有空数据(存在未选择职工的记录)");
|
||||
if (it.ChetaiStart <= 0) throw new ServiceException($"「{it.EmployeeName}」请填写开始车位");
|
||||
if (it.ChetaiEnd < it.ChetaiStart)
|
||||
throw new ServiceException($"「{it.EmployeeName}」结束车位不能小于开始车位");
|
||||
if (it.Hours < 0) throw new ServiceException($"「{it.EmployeeName}」的时长不能为负数");
|
||||
}
|
||||
|
||||
var d = dto.Date.Date;
|
||||
var jzcts = (await _jzctRepo.FindAsync(x => x.DepartmentId == dto.DepId)).OrderBy(x => x.Jizu).ToList();
|
||||
|
||||
// 挡车数 / 机组 由服务端重算(对齐 F9 CalDangcheNum + CalJizu)
|
||||
foreach (var it in items)
|
||||
{
|
||||
var startShared = items.Any(x => x.EmployeeId != it.EmployeeId && x.ChetaiEnd == it.ChetaiStart);
|
||||
var endShared = items.Any(x => x.EmployeeId != it.EmployeeId && x.ChetaiStart == it.ChetaiEnd);
|
||||
it.DangcheNum = it.ChetaiEnd + 1 - it.ChetaiStart
|
||||
- (startShared ? 0.5m : 0m) - (endShared ? 0.5m : 0m);
|
||||
|
||||
var jzct = jzcts.FirstOrDefault(x => x.ChetaiStart <= it.ChetaiEnd && x.ChetaiEnd >= it.ChetaiStart);
|
||||
if (jzct != null) it.Jizu = jzct.Jizu;
|
||||
}
|
||||
|
||||
await DeleteDcgCoreAsync(d, dto.DepId, dto.Banci);
|
||||
|
||||
foreach (var it in items)
|
||||
{
|
||||
await _dcgRepo.AddAsync(new KtjlDcg
|
||||
{
|
||||
Date = d,
|
||||
DeptId = dto.DepId,
|
||||
Banci = dto.Banci,
|
||||
Jizu = it.Jizu,
|
||||
ChetaiStart = it.ChetaiStart,
|
||||
ChetaiEnd = it.ChetaiEnd,
|
||||
DangcheNum = it.DangcheNum,
|
||||
Hours = it.Hours,
|
||||
EmployeeId = it.EmployeeId,
|
||||
EmployeeDepId = it.EmployeeDepId,
|
||||
WorktypeId = it.WorktypeId,
|
||||
WorktypeName = it.WorktypeName ?? string.Empty,
|
||||
NoChanliang = it.NoChanliang
|
||||
});
|
||||
}
|
||||
await _dcgRepo.SaveChangesAsync();
|
||||
return items.Count;
|
||||
}
|
||||
|
||||
public async Task DeleteCtgAsync(DateTime date, long depId, Prod_Banci_Enum banci)
|
||||
{
|
||||
if (depId <= 0) throw new ServiceException("请选择车间");
|
||||
await DeleteCtgCoreAsync(date.Date, depId, banci);
|
||||
await _ctgRepo.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteDcgAsync(DateTime date, long depId, Prod_Banci_Enum banci)
|
||||
{
|
||||
if (depId <= 0) throw new ServiceException("请选择车间");
|
||||
await DeleteDcgCoreAsync(date.Date, depId, banci);
|
||||
await _dcgRepo.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task DeleteCtgCoreAsync(DateTime date, long depId, Prod_Banci_Enum banci)
|
||||
{
|
||||
var olds = await _ctgRepo.FindAsync(x => x.Date == date && x.DeptId == depId && x.Banci == banci);
|
||||
if (olds.Count > 0) await _ctgRepo.HardDeleteRangeAsync(olds);
|
||||
}
|
||||
|
||||
private async Task DeleteDcgCoreAsync(DateTime date, long depId, Prod_Banci_Enum banci)
|
||||
{
|
||||
var olds = await _dcgRepo.FindAsync(x => x.Date == date && x.DeptId == depId && x.Banci == banci);
|
||||
if (olds.Count > 0) await _dcgRepo.HardDeleteRangeAsync(olds);
|
||||
}
|
||||
|
||||
// ==================== 默认记录(录入(新增)) ====================
|
||||
|
||||
public async Task<List<KtjlctgReadDto_View>> BuildCtgDefaultsAsync(DateTime date, long depId,
|
||||
Prod_Banci_Enum banci)
|
||||
{
|
||||
var ctx = await BuildContextAsync();
|
||||
var deptName = ctx.DeptNames.GetValueOrDefault(depId, "");
|
||||
var plans = (await _ktpbCtgRepo.FindAsync(x => x.DeptId == depId && x.Banci == banci)).OrderBy(x => x.Jizu);
|
||||
var result = new List<KtjlctgReadDto_View>();
|
||||
|
||||
foreach (var p in plans)
|
||||
{
|
||||
foreach (var ygId in new[] { p.Ygid1, p.Ygid2 })
|
||||
{
|
||||
if (ygId <= 0 || !ctx.Emps.TryGetValue(ygId, out var emp)) continue;
|
||||
result.Add(new KtjlctgReadDto_View
|
||||
{
|
||||
Date = date.Date,
|
||||
DepId = depId,
|
||||
DepName = deptName,
|
||||
Banci = banci,
|
||||
BanciName = banci.ToString(),
|
||||
Jizu = p.Jizu,
|
||||
Hours = DefaultHours,
|
||||
EmployeeId = emp.Id,
|
||||
EmployeeName = emp.Name,
|
||||
EmployeeDepId = emp.DepId,
|
||||
EmployeeDepName = ctx.DeptNames.GetValueOrDefault(emp.DepId, ""),
|
||||
WorktypeId = emp.WorktypeId,
|
||||
WorktypeName = ctx.WorktypeNames.GetValueOrDefault(emp.WorktypeId, ""),
|
||||
NoChanliang = !emp.IsZhuanzheng
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<KtjldcgReadDto_View>> BuildDcgDefaultsAsync(DateTime date, long depId,
|
||||
Prod_Banci_Enum banci)
|
||||
{
|
||||
var ctx = await BuildContextAsync();
|
||||
var deptName = ctx.DeptNames.GetValueOrDefault(depId, "");
|
||||
var plans = (await _ktpbDcgRepo.FindAsync(x => x.DeptId == depId && x.Banci == banci))
|
||||
.OrderBy(x => x.Jizu).ThenBy(x => x.ChetaiStart);
|
||||
var result = new List<KtjldcgReadDto_View>();
|
||||
|
||||
foreach (var p in plans)
|
||||
{
|
||||
if (p.YgId <= 0 || !ctx.Emps.TryGetValue(p.YgId, out var emp)) continue;
|
||||
result.Add(new KtjldcgReadDto_View
|
||||
{
|
||||
Date = date.Date,
|
||||
DepId = depId,
|
||||
DepName = deptName,
|
||||
Banci = banci,
|
||||
BanciName = banci.ToString(),
|
||||
Jizu = p.Jizu,
|
||||
ChetaiStart = p.ChetaiStart,
|
||||
ChetaiEnd = p.ChetaiEnd,
|
||||
DangcheNum = (decimal)p.Dangcheshu,
|
||||
Hours = DefaultHours,
|
||||
EmployeeId = emp.Id,
|
||||
EmployeeName = emp.Name,
|
||||
EmployeeDepId = emp.DepId,
|
||||
EmployeeDepName = ctx.DeptNames.GetValueOrDefault(emp.DepId, ""),
|
||||
WorktypeId = emp.WorktypeId,
|
||||
WorktypeName = ctx.WorktypeNames.GetValueOrDefault(emp.WorktypeId, ""),
|
||||
NoChanliang = !emp.IsZhuanzheng
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<ProdWorktypeOptionDto>> GetWorktypeOptionsAsync()
|
||||
=> (await _wtRepo.GetAllAsync()).OrderBy(x => x.SortOrder).ThenBy(x => x.Id)
|
||||
.Select(x => new ProdWorktypeOptionDto { Id = x.Id, Name = x.WorkName })
|
||||
.ToList();
|
||||
|
||||
// ==================== 上下文 ====================
|
||||
|
||||
private sealed class Ctx
|
||||
{
|
||||
public Dictionary<long, string> DeptNames { get; init; } = new();
|
||||
public Dictionary<long, string> WorktypeNames { get; init; } = new();
|
||||
public Dictionary<long, EmpInfo> Emps { get; init; } = new();
|
||||
}
|
||||
|
||||
private sealed class EmpInfo
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public string Name { get; init; } = "";
|
||||
public long DepId { get; init; }
|
||||
public long WorktypeId { get; init; }
|
||||
public bool IsZhuanzheng { get; init; }
|
||||
}
|
||||
|
||||
private async Task<Ctx> BuildContextAsync()
|
||||
{
|
||||
var depts = await _deptRepo.GetAllAsync();
|
||||
var wts = await _wtRepo.GetAllAsync();
|
||||
var emps = await _empRepo.GetAllAsync();
|
||||
return new Ctx
|
||||
{
|
||||
DeptNames = depts.ToDictionary(d => d.Id, d => d.Name),
|
||||
WorktypeNames = wts.ToDictionary(w => w.Id, w => w.WorkName),
|
||||
Emps = emps.ToDictionary(e => e.Id, e => new EmpInfo
|
||||
{
|
||||
Id = e.Id,
|
||||
Name = e.Name,
|
||||
DepId = e.DepId,
|
||||
WorktypeId = e.WorktypeId,
|
||||
IsZhuanzheng = e.IsZhuanzheng
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
private static KtjlctgReadDto_View ToCtgView(KtjlCtg x, Ctx ctx) => new()
|
||||
{
|
||||
Id = x.Id,
|
||||
Date = x.Date,
|
||||
DepId = x.DeptId,
|
||||
DepName = ctx.DeptNames.GetValueOrDefault(x.DeptId, ""),
|
||||
Banci = x.Banci,
|
||||
BanciName = x.Banci.ToString(),
|
||||
Jizu = x.Jizu,
|
||||
Hours = x.Hours,
|
||||
EmployeeId = x.EmployeeId,
|
||||
EmployeeName = ctx.Emps.GetValueOrDefault(x.EmployeeId)?.Name ?? "",
|
||||
EmployeeDepId = x.EmployeeDepId,
|
||||
EmployeeDepName = ctx.DeptNames.GetValueOrDefault(x.EmployeeDepId, ""),
|
||||
WorktypeId = x.WorktypeId,
|
||||
WorktypeName = string.IsNullOrWhiteSpace(x.WorktypeName)
|
||||
? ctx.WorktypeNames.GetValueOrDefault(x.WorktypeId, "")
|
||||
: x.WorktypeName,
|
||||
NoChanliang = x.NoChanliang
|
||||
};
|
||||
|
||||
private static KtjldcgReadDto_View ToDcgView(KtjlDcg x, Ctx ctx) => new()
|
||||
{
|
||||
Id = x.Id,
|
||||
Date = x.Date,
|
||||
DepId = x.DeptId,
|
||||
DepName = ctx.DeptNames.GetValueOrDefault(x.DeptId, ""),
|
||||
Banci = x.Banci,
|
||||
BanciName = x.Banci.ToString(),
|
||||
Jizu = x.Jizu,
|
||||
ChetaiStart = x.ChetaiStart,
|
||||
ChetaiEnd = x.ChetaiEnd,
|
||||
DangcheNum = x.DangcheNum,
|
||||
Hours = x.Hours,
|
||||
EmployeeId = x.EmployeeId,
|
||||
EmployeeName = ctx.Emps.GetValueOrDefault(x.EmployeeId)?.Name ?? "",
|
||||
EmployeeDepId = x.EmployeeDepId,
|
||||
EmployeeDepName = ctx.DeptNames.GetValueOrDefault(x.EmployeeDepId, ""),
|
||||
WorktypeId = x.WorktypeId,
|
||||
WorktypeName = string.IsNullOrWhiteSpace(x.WorktypeName)
|
||||
? ctx.WorktypeNames.GetValueOrDefault(x.WorktypeId, "")
|
||||
: x.WorktypeName,
|
||||
NoChanliang = x.NoChanliang
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using FreeSql.DataAnnotations;
|
||||
using F9WebAiTest.Domain.Common;
|
||||
using F9WebAiTest.Domain.Enums.SYS.Employees;
|
||||
|
||||
namespace F9WebAiTest.Domain.Entities.Prod;
|
||||
|
||||
// 看台记录(对应 F9 生产-前缫 的 KTJL 两张表:车头工 / 挡车工)
|
||||
// 记录 = 某日某车间某班次的实际看台情况(由看台排班带出后再调整:车位、时长、代班、不计产量)。
|
||||
|
||||
/// <summary>车头工看台记录(biz_prod_qs_ktjl_ctg)</summary>
|
||||
[Table(Name = "KtjlCtgs")]
|
||||
public class KtjlCtg : BaseEntity
|
||||
{
|
||||
public DateTime Date { get; set; } // 日期
|
||||
public long DeptId { get; set; } // 车间
|
||||
public Prod_Banci_Enum Banci { get; set; } // 班次(甲/乙)
|
||||
public int Jizu { get; set; } // 机组
|
||||
public decimal Hours { get; set; } // 时长(小时)
|
||||
public long EmployeeId { get; set; } // 车头工
|
||||
public long EmployeeDepId { get; set; } // 职工所属部门
|
||||
public long WorktypeId { get; set; } // 工种
|
||||
public string WorktypeName { get; set; } = ""; // 工种名称(历史快照)
|
||||
public bool NoChanliang { get; set; } // 不计产量
|
||||
}
|
||||
|
||||
/// <summary>挡车工看台记录(biz_prod_qs_ktjl_dcg)</summary>
|
||||
[Table(Name = "KtjlDcgs")]
|
||||
public class KtjlDcg : BaseEntity
|
||||
{
|
||||
public DateTime Date { get; set; } // 日期
|
||||
public long DeptId { get; set; } // 车间
|
||||
public Prod_Banci_Enum Banci { get; set; } // 班次
|
||||
public int Jizu { get; set; } // 机组
|
||||
public int ChetaiStart { get; set; } // 开始车位
|
||||
public int ChetaiEnd { get; set; } // 结束车位
|
||||
public decimal DangcheNum { get; set; } // 挡车数(边界车位与他人共用时各计 0.5)
|
||||
public decimal Hours { get; set; } // 时长(小时)
|
||||
public long EmployeeId { get; set; } // 挡车工
|
||||
public long EmployeeDepId { get; set; } // 职工所属部门
|
||||
public long WorktypeId { get; set; } // 工种
|
||||
public string WorktypeName { get; set; } = ""; // 工种名称(历史快照)
|
||||
public bool NoChanliang { get; set; } // 不计产量
|
||||
}
|
||||
@@ -737,14 +737,16 @@ public static class DbSeeder
|
||||
("机组车台", "/prod/qianxian/jizuchetai", "base/jizuchetai/index", "prod:jizuchetai:list", "SetUp", 1),
|
||||
("落丝记录", "/prod/qianxian/lsjl", "prod/lsjl/index", "prod:lsjl:list", "Document", 2),
|
||||
("车间班次", "/prod/qianxian/chejianbanci", "base/chejianbanci/index", "prod:chejianbanci:list", "Timer", 3),
|
||||
("看台排班", "/prod/qianxian/ktpb", "prod/ktpb/index", "prod:ktpb:list", "Grid", 4)
|
||||
("看台排班", "/prod/qianxian/ktpb", "prod/ktpb/index", "prod:ktpb:list", "Grid", 4),
|
||||
("看台记录", "/prod/qianxian/ktjl", "prod/ktjl/index", "prod:ktjl:list", "Tickets", 5)
|
||||
});
|
||||
|
||||
// 子菜单顺序归一(EnsureTopMenuWithChildren 只补新菜单,不调整已存在菜单的排序)
|
||||
foreach (var (path, sort) in new (string Path, int Sort)[]
|
||||
{
|
||||
("/prod/qianxian/jizuchetai", 1), ("/prod/qianxian/lsjl", 2),
|
||||
("/prod/qianxian/chejianbanci", 3), ("/prod/qianxian/ktpb", 4)
|
||||
("/prod/qianxian/chejianbanci", 3), ("/prod/qianxian/ktpb", 4),
|
||||
("/prod/qianxian/ktjl", 5)
|
||||
})
|
||||
{
|
||||
fsql.Update<SysMenu>()
|
||||
|
||||
@@ -8,6 +8,7 @@ using F9WebAiTest.Domain.Entities.Prod;
|
||||
using F9WebAiTest.Domain.Enums.SYS.Employees;
|
||||
using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments;
|
||||
using SysEmp = F9WebAiTest.Domain.Entities.SYS.Employees;
|
||||
using SysWt = F9WebAiTest.Domain.Entities.SYS.WorkTypes;
|
||||
|
||||
namespace F9WebAiTest.Infrastructure.Data;
|
||||
|
||||
@@ -24,6 +25,8 @@ public static class ProdSeedData
|
||||
private const string KtpbCtgResource = "F9WebAiTest.Infrastructure.Data.SeedData.biz_prod_qs_ktpb_ctg.json";
|
||||
private const string KtpbDcgResource = "F9WebAiTest.Infrastructure.Data.SeedData.biz_prod_qs_ktpb_dcg.json";
|
||||
private const string KtpbZbzResource = "F9WebAiTest.Infrastructure.Data.SeedData.biz_prod_qs_ktpb_zbz.json";
|
||||
private const string KtjlCtgResource = "F9WebAiTest.Infrastructure.Data.SeedData.biz_prod_qs_ktjl_ctg.csv";
|
||||
private const string KtjlDcgResource = "F9WebAiTest.Infrastructure.Data.SeedData.biz_prod_qs_ktjl_dcg.csv";
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOpts = new()
|
||||
{
|
||||
@@ -39,6 +42,9 @@ public static class ProdSeedData
|
||||
SeedKtpbCtg(fsql);
|
||||
SeedKtpbDcg(fsql);
|
||||
SeedKtpbZbz(fsql);
|
||||
// 看台记录(历史流水,一次性导入)
|
||||
SeedKtjlCtg(fsql);
|
||||
SeedKtjlDcg(fsql);
|
||||
}
|
||||
|
||||
// ==================== 机组车台 ====================
|
||||
@@ -310,6 +316,146 @@ public static class ProdSeedData
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 看台记录 ====================
|
||||
|
||||
/// <summary>
|
||||
/// 车头工看台记录(历史流水):一次性导入,表内已有数据则跳过。
|
||||
/// 数据格式:date|depId|banci|jizu|hours|employeeId|employeeDepId|worktypeId|noChanliang
|
||||
/// </summary>
|
||||
private static void SeedKtjlCtg(IFreeSql fsql)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (fsql.Select<KtjlCtg>().Any()) { return; }
|
||||
var lines = LoadCsv(KtjlCtgResource);
|
||||
if (lines.Count == 0) return;
|
||||
|
||||
var deptMap = BuildDeptMap(fsql);
|
||||
var empIds = BuildEmpIdSet(fsql);
|
||||
var wtNames = fsql.Select<SysWt.WorkType>().ToList().ToDictionary(w => w.Id, w => w.WorkName);
|
||||
|
||||
var batch = new List<KtjlCtg>(1000);
|
||||
int added = 0, skipped = 0;
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var f = line.Split('|');
|
||||
if (f.Length < 9) { skipped++; continue; }
|
||||
if (!DateTime.TryParse(f[0], out var date)) { skipped++; continue; }
|
||||
if (!deptMap.TryGetValue(long.Parse(f[1]), out var depId)) { skipped++; continue; }
|
||||
if (!empIds.Contains(long.Parse(f[5]))) { skipped++; continue; }
|
||||
|
||||
var wtId = long.Parse(f[7]);
|
||||
batch.Add(new KtjlCtg
|
||||
{
|
||||
Date = date.Date,
|
||||
DeptId = depId,
|
||||
Banci = (Prod_Banci_Enum)int.Parse(f[2]),
|
||||
Jizu = int.Parse(f[3]),
|
||||
Hours = decimal.Parse(f[4]),
|
||||
EmployeeId = long.Parse(f[5]),
|
||||
EmployeeDepId = deptMap.TryGetValue(long.Parse(f[6]), out var ed) ? ed : depId,
|
||||
WorktypeId = wtId,
|
||||
WorktypeName = wtNames.GetValueOrDefault(wtId, ""),
|
||||
NoChanliang = f[8] == "1" || f[8].Equals("true", StringComparison.OrdinalIgnoreCase)
|
||||
});
|
||||
if (batch.Count >= 1000)
|
||||
{
|
||||
fsql.Insert(batch).ExecuteAffrows();
|
||||
added += batch.Count;
|
||||
batch.Clear();
|
||||
}
|
||||
}
|
||||
if (batch.Count > 0)
|
||||
{
|
||||
fsql.Insert(batch).ExecuteAffrows();
|
||||
added += batch.Count;
|
||||
}
|
||||
Console.WriteLine($"[ProdSeed] 车头工看台记录:来源 {lines.Count} 行,导入 {added} 行,跳过 {skipped} 行");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[ProdSeed] 车头工看台记录导入跳过:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 挡车工看台记录(历史流水):一次性导入,表内已有数据则跳过。
|
||||
/// 数据格式:date|depId|banci|jizu|chetaiStart|chetaiEnd|dangcheNum|hours|employeeId|employeeDepId|worktypeId|noChanliang
|
||||
/// </summary>
|
||||
private static void SeedKtjlDcg(IFreeSql fsql)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (fsql.Select<KtjlDcg>().Any()) { return; }
|
||||
var lines = LoadCsv(KtjlDcgResource);
|
||||
if (lines.Count == 0) return;
|
||||
|
||||
var deptMap = BuildDeptMap(fsql);
|
||||
var empIds = BuildEmpIdSet(fsql);
|
||||
var wtNames = fsql.Select<SysWt.WorkType>().ToList().ToDictionary(w => w.Id, w => w.WorkName);
|
||||
|
||||
var batch = new List<KtjlDcg>(1000);
|
||||
int added = 0, skipped = 0;
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var f = line.Split('|');
|
||||
if (f.Length < 12) { skipped++; continue; }
|
||||
if (!DateTime.TryParse(f[0], out var date)) { skipped++; continue; }
|
||||
if (!deptMap.TryGetValue(long.Parse(f[1]), out var depId)) { skipped++; continue; }
|
||||
if (!empIds.Contains(long.Parse(f[8]))) { skipped++; continue; }
|
||||
|
||||
var wtId = long.Parse(f[10]);
|
||||
batch.Add(new KtjlDcg
|
||||
{
|
||||
Date = date.Date,
|
||||
DeptId = depId,
|
||||
Banci = (Prod_Banci_Enum)int.Parse(f[2]),
|
||||
Jizu = int.Parse(f[3]),
|
||||
ChetaiStart = int.Parse(f[4]),
|
||||
ChetaiEnd = int.Parse(f[5]),
|
||||
DangcheNum = decimal.Parse(f[6]),
|
||||
Hours = decimal.Parse(f[7]),
|
||||
EmployeeId = long.Parse(f[8]),
|
||||
EmployeeDepId = deptMap.TryGetValue(long.Parse(f[9]), out var ed) ? ed : depId,
|
||||
WorktypeId = wtId,
|
||||
WorktypeName = wtNames.GetValueOrDefault(wtId, ""),
|
||||
NoChanliang = f[11] == "1" || f[11].Equals("true", StringComparison.OrdinalIgnoreCase)
|
||||
});
|
||||
if (batch.Count >= 1000)
|
||||
{
|
||||
fsql.Insert(batch).ExecuteAffrows();
|
||||
added += batch.Count;
|
||||
batch.Clear();
|
||||
}
|
||||
}
|
||||
if (batch.Count > 0)
|
||||
{
|
||||
fsql.Insert(batch).ExecuteAffrows();
|
||||
added += batch.Count;
|
||||
}
|
||||
Console.WriteLine($"[ProdSeed] 挡车工看台记录:来源 {lines.Count} 行,导入 {added} 行,跳过 {skipped} 行");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[ProdSeed] 挡车工看台记录导入跳过:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static List<string> LoadCsv(string resourceName)
|
||||
{
|
||||
var asm = Assembly.GetExecutingAssembly();
|
||||
using var stream = asm.GetManifestResourceStream(resourceName);
|
||||
if (stream == null) return new List<string>();
|
||||
using var reader = new StreamReader(stream);
|
||||
var list = new List<string>();
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
var line = reader.ReadLine();
|
||||
if (!string.IsNullOrWhiteSpace(line)) list.Add(line);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
// ==================== 公共 ====================
|
||||
|
||||
/// <summary>本系统存在的职工 Id 集合(F9 职工导出时保留了原始雪花 Id,可直接比对)</summary>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,8 @@
|
||||
<ItemGroup>
|
||||
<!-- 工种基础数据种子(来源于 F9 系统 biz_base_worktype 三张表导出) -->
|
||||
<EmbeddedResource Include="Data\SeedData\*.json" />
|
||||
<!-- 看台记录历史流水(体量较大,采用紧凑 CSV 内嵌) -->
|
||||
<EmbeddedResource Include="Data\SeedData\*.csv" />
|
||||
<!-- 报表中心示例 .grf 模板(GR6.8.5.0 格式;GR7 设计器向下兼容,可直接打开编辑) -->
|
||||
<EmbeddedResource Include="Assets\ReportTemplates\*.grf" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -66,6 +66,7 @@ export const componentMap = {
|
||||
'prod/xdjy/index': () => import('../views/prod/xdjy/index.vue'),
|
||||
'prod/hbjy/index': () => import('../views/prod/hbjy/index.vue'),
|
||||
'prod/ktpb/index': () => import('../views/prod/ktpb/index.vue'),
|
||||
'prod/ktjl/index': () => import('../views/prod/ktjl/index.vue'),
|
||||
'notification/messages/index': () => import('../views/notification/messages/index.vue'),
|
||||
'notification/subscribe/index': () => import('../views/notification/subscribe/index.vue'),
|
||||
'notification/type/index': () => import('../views/notification/type/index.vue'),
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 看台记录录入弹窗(对应 F9 Page_Ktjl_Dcg_Edit / Page_Ktjl_Ctg_Edit)
|
||||
* - 挡车工:可编辑机组/起止车位/挡车数/时长,挡车数按接壤车位自动计算
|
||||
* - 车头工:可编辑机组/时长
|
||||
* - 校验口径与 F9 一致:个人时长不超标准、挡车数/看台时长匹配、车头工每机组≥2 人且时长为标准时长整数倍
|
||||
*/
|
||||
import { ref, reactive, computed, watch } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import request from '../../../utils/request';
|
||||
import EmployeeSelect from '../../../components/select/EmployeeSelect.vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
type: { type: String, default: 'dcg' }, // dcg | ctg
|
||||
date: { type: String, default: '' },
|
||||
depId: { type: Number, default: 0 },
|
||||
depName: { type: String, default: '' },
|
||||
banci: { type: Number, default: 1 },
|
||||
banciName: { type: String, default: '甲' },
|
||||
/** 预置行(录入(新增) 的排班带出 / 指定日期复制),库中无记录时使用 */
|
||||
presetRows: { type: Array, default: () => [] },
|
||||
});
|
||||
const emit = defineEmits(['update:modelValue', 'saved']);
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const rows = ref([]);
|
||||
const hoursStandard = ref(9);
|
||||
const jizuOptions = ref([]);
|
||||
const empPicker = reactive({ visible: false });
|
||||
const selected = ref([]);
|
||||
|
||||
const title = computed(() => (props.type === 'dcg' ? '挡车工看台记录' : '车头工看台记录'));
|
||||
|
||||
watch(() => props.modelValue, async (v) => {
|
||||
visible.value = v;
|
||||
if (v) await load();
|
||||
});
|
||||
watch(visible, (v) => emit('update:modelValue', v));
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await request.get(`/prod/ktjl/${props.type}`, {
|
||||
params: { date: props.date, depId: props.depId, banci: props.banci },
|
||||
});
|
||||
const list = (res.data || []).length ? res.data : props.presetRows || [];
|
||||
rows.value = list.map((x) => ({ ...x, selected: false }));
|
||||
const jz = await request.get('/prod/ktpb/jizu-options', { params: { depId: props.depId } });
|
||||
jizuOptions.value = jz.data || [];
|
||||
hoursStandard.value = rows.value.length ? Math.max(...rows.value.map((x) => Number(x.hours) || 0)) : 9;
|
||||
if (props.type === 'dcg') recalcDcg();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 由车位推导机组(与后端 CalJizu 一致) */
|
||||
function jizuOf(start, end) {
|
||||
const hit = jizuOptions.value.find((j) => j.chetaiStart <= end && j.chetaiEnd >= start);
|
||||
return hit ? hit.jizu : 0;
|
||||
}
|
||||
|
||||
/** 挡车数:车位数 - 与他人在边界车位各分 0.5(与后端 CalDangcheNum 一致) */
|
||||
function recalcDcg() {
|
||||
const list = rows.value;
|
||||
list.forEach((it) => {
|
||||
const s = Number(it.chetaiStart) || 0;
|
||||
const e = Number(it.chetaiEnd) || 0;
|
||||
if (!s || e < s) {
|
||||
it.dangcheNum = 0;
|
||||
return;
|
||||
}
|
||||
const startShared = list.some((x) => x !== it && x.employeeId !== it.employeeId && Number(x.chetaiEnd) === s);
|
||||
const endShared = list.some((x) => x !== it && x.employeeId !== it.employeeId && Number(x.chetaiStart) === e);
|
||||
it.dangcheNum = e + 1 - s - (startShared ? 0.5 : 0) - (endShared ? 0.5 : 0);
|
||||
it.jizu = jizuOf(s, e);
|
||||
});
|
||||
}
|
||||
|
||||
function onChetaiChange() {
|
||||
recalcDcg();
|
||||
}
|
||||
|
||||
/** 添加职工(多选) */
|
||||
async function openEmpPicker() {
|
||||
empPicker.visible = true;
|
||||
}
|
||||
async function onEmployeesPicked(list) {
|
||||
const exist = new Set(rows.value.map((x) => String(x.employeeId)));
|
||||
const picked = Array.isArray(list) ? list : [list];
|
||||
picked.forEach((e) => {
|
||||
if (exist.has(String(e.id))) return;
|
||||
rows.value.push({
|
||||
id: 0,
|
||||
employeeId: e.id,
|
||||
employeeName: e.name,
|
||||
employeeDepId: e.depId,
|
||||
employeeDepName: e.depName,
|
||||
worktypeId: e.worktypeId,
|
||||
worktypeName: e.worktypeName,
|
||||
jizu: props.type === 'dcg' ? 0 : 1,
|
||||
chetaiStart: 0,
|
||||
chetaiEnd: 0,
|
||||
dangcheNum: 0,
|
||||
hours: Number(hoursStandard.value) || 9,
|
||||
noChanliang: false,
|
||||
selected: false,
|
||||
});
|
||||
});
|
||||
if (props.type === 'dcg') recalcDcg();
|
||||
}
|
||||
|
||||
function removeSelected() {
|
||||
const picked = rows.value.filter((x) => x.selected);
|
||||
if (!picked.length) return ElMessage.warning('请选择要删除的记录');
|
||||
rows.value = rows.value.filter((x) => !x.selected);
|
||||
if (props.type === 'dcg') recalcDcg();
|
||||
}
|
||||
|
||||
async function batchHours() {
|
||||
const picked = rows.value.filter((x) => x.selected);
|
||||
if (!picked.length) return ElMessage.warning('请选择要修改的记录');
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt('批量设置时长(小时)', '设置时长', {
|
||||
inputValue: String(hoursStandard.value),
|
||||
inputPattern: /^\d+(\.\d+)?$/,
|
||||
inputErrorMessage: '请输入数字',
|
||||
});
|
||||
picked.forEach((x) => {
|
||||
x.hours = Number(value);
|
||||
});
|
||||
} catch {
|
||||
/* 取消 */
|
||||
}
|
||||
}
|
||||
|
||||
/** 校验(对齐 F9 ValidCustom),返回提示列表 */
|
||||
function validate() {
|
||||
const warns = [];
|
||||
const std = Number(hoursStandard.value) || 0;
|
||||
|
||||
if (rows.value.some((x) => !x.employeeId)) {
|
||||
return { fatal: '不能有空数据(存在未选择职工的记录)', warns: [] };
|
||||
}
|
||||
|
||||
if (props.type === 'dcg') {
|
||||
const over = [];
|
||||
const byEmp = {};
|
||||
rows.value.forEach((x) => {
|
||||
const key = String(x.employeeId);
|
||||
byEmp[key] = byEmp[key] || { name: x.employeeName, hours: 0 };
|
||||
byEmp[key].hours += Number(x.hours) || 0;
|
||||
});
|
||||
Object.values(byEmp).forEach((v) => {
|
||||
if (std > 0 && v.hours > std) over.push(v.name);
|
||||
});
|
||||
if (over.length) return { fatal: `${over.join('、')} 超过标准时长(${std} 小时)`, warns: [] };
|
||||
|
||||
rows.value.forEach((x) => {
|
||||
const s = Number(x.chetaiStart);
|
||||
const e = Number(x.chetaiEnd);
|
||||
const shared = rows.value.some((y) => y.employeeId !== x.employeeId && (Number(y.chetaiEnd) === s || Number(y.chetaiStart) === e));
|
||||
if (shared && Number(x.dangcheNum) * 10 % 10 === 0) {
|
||||
warns.push(`${x.jizu}组${e}台,多人看台挡车数不匹配`);
|
||||
}
|
||||
if (Number(x.dangcheNum) === 0) warns.push(`${x.jizu}组${e}台,挡车数不能为 0`);
|
||||
});
|
||||
|
||||
// 看台时长校验
|
||||
const jizus = [...new Set(rows.value.map((x) => x.jizu))];
|
||||
jizus.forEach((jz) => {
|
||||
const items = rows.value.filter((x) => x.jizu === jz);
|
||||
if (!items.length) return;
|
||||
const min = Math.min(...items.map((x) => Number(x.chetaiStart)));
|
||||
const max = Math.max(...items.map((x) => Number(x.chetaiEnd)));
|
||||
for (let i = min; i <= max; i++) {
|
||||
const cover = items.filter((x) => Number(x.chetaiStart) <= i && Number(x.chetaiEnd) >= i);
|
||||
if (!cover.length) continue;
|
||||
const hasStart = cover.some((x) => Number(x.chetaiStart) === i);
|
||||
const hasEnd = cover.some((x) => Number(x.chetaiEnd) === i);
|
||||
if (hasStart && hasEnd) {
|
||||
const up = cover.filter((x) => Number(x.chetaiStart) === i).reduce((a, b) => a + Number(b.hours), 0);
|
||||
const down = cover.filter((x) => Number(x.chetaiEnd) === i).reduce((a, b) => a + Number(b.hours), 0);
|
||||
if (up === down) {
|
||||
if (std > 0 && up !== std) warns.push(`${jz}组${i}台,看台时长有误`);
|
||||
} else {
|
||||
warns.push(`${jz}组${i}台,看台时长不一致`);
|
||||
}
|
||||
} else {
|
||||
const sum = cover.reduce((a, b) => a + Number(b.hours), 0);
|
||||
if (std > 0 && sum !== std) warns.push(`${jz}组${i}台,看台时长有误`);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const jizus = [...new Set(rows.value.map((x) => x.jizu))];
|
||||
jizus.forEach((jz) => {
|
||||
const items = rows.value.filter((x) => x.jizu === jz);
|
||||
if (items.length < 2) warns.push(`${jz}组不足两人`);
|
||||
if (items.some((x) => Number(x.hours) === 0)) warns.push(`${jz}组时长有误`);
|
||||
const sum = items.reduce((a, b) => a + Number(b.hours), 0);
|
||||
if (std > 0 && (sum < std * 2 || sum % std !== 0)) warns.push(`${jz}组时长有误`);
|
||||
});
|
||||
}
|
||||
|
||||
return { fatal: '', warns };
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const { fatal, warns } = validate();
|
||||
if (fatal) return ElMessage.warning(fatal);
|
||||
if (warns.length) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`${[...new Set(warns)].join('\n')}\n是否继续保存?`, '验证提示', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '继续保存',
|
||||
cancelButtonText: '返回修改',
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
const res = await request.post(`/prod/ktjl/${props.type}/batch`, {
|
||||
date: props.date,
|
||||
depId: props.depId,
|
||||
banci: props.banci,
|
||||
items: rows.value.map((x) => ({
|
||||
employeeId: x.employeeId,
|
||||
employeeDepId: x.employeeDepId,
|
||||
worktypeId: x.worktypeId,
|
||||
worktypeName: x.worktypeName,
|
||||
jizu: Number(x.jizu) || 0,
|
||||
chetaiStart: Number(x.chetaiStart) || 0,
|
||||
chetaiEnd: Number(x.chetaiEnd) || 0,
|
||||
dangcheNum: Number(x.dangcheNum) || 0,
|
||||
hours: Number(x.hours) || 0,
|
||||
noChanliang: !!x.noChanliang,
|
||||
})),
|
||||
});
|
||||
ElMessage.success(res.message || '保存成功');
|
||||
visible.value = false;
|
||||
emit('saved');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="title" width="1080px" destroy-on-close append-to-body>
|
||||
<div class="dlg-head">
|
||||
<el-tag type="info">{{ date }}</el-tag>
|
||||
<el-tag type="info">{{ depName }}</el-tag>
|
||||
<el-tag type="info">{{ banciName }}班</el-tag>
|
||||
<span class="lbl">标准时长</span>
|
||||
<el-input-number v-model="hoursStandard" :min="1" :max="24" :step="0.5" :controls="false" style="width: 80px" />
|
||||
<span class="tip">共 {{ rows.length }} 条</span>
|
||||
</div>
|
||||
|
||||
<div class="dlg-toolbar">
|
||||
<el-button type="primary" size="small" @click="openEmpPicker">添加职工</el-button>
|
||||
<el-button size="small" @click="removeSelected">删除选中</el-button>
|
||||
<el-button size="small" @click="batchHours">批量修改时长</el-button>
|
||||
<span class="hint">
|
||||
{{ type === 'dcg'
|
||||
? '挡车数 = 车位数 −(与他人接壤的边界车位各 0.5);机组按车位自动归属'
|
||||
: '每个机组至少 2 人,机组时长合计需为标准时长的整数倍' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="rows" border size="small" height="420" @selection-change="selected = $event">
|
||||
<el-table-column type="selection" width="42" />
|
||||
<el-table-column type="index" label="序号" width="52" />
|
||||
<el-table-column prop="employeeDepName" label="部门" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="worktypeName" label="工种" width="90" />
|
||||
<el-table-column prop="employeeName" label="姓名" width="120" show-overflow-tooltip />
|
||||
<el-table-column label="班次" width="70" align="center">
|
||||
<template #default>{{ banciName }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="机组" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.jizu" :min="0" :max="99" :controls="false" size="small" style="width: 60px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template v-if="type === 'dcg'">
|
||||
<el-table-column label="开始车位" width="96" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.chetaiStart"
|
||||
:min="1"
|
||||
:max="999"
|
||||
:controls="false"
|
||||
size="small"
|
||||
style="width: 76px"
|
||||
@change="onChetaiChange"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结束车位" width="96" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.chetaiEnd"
|
||||
:min="1"
|
||||
:max="999"
|
||||
:controls="false"
|
||||
size="small"
|
||||
style="width: 76px"
|
||||
@change="onChetaiChange"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="dangcheNum" label="挡车数" width="80" align="right" />
|
||||
</template>
|
||||
<el-table-column label="时长" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.hours" :min="0" :max="24" :step="0.5" :controls="false" size="small" style="width: 70px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="不计产量" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.noChanliang" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
|
||||
</template>
|
||||
|
||||
<EmployeeSelect v-model="empPicker.visible" multiple :dep-id="depId" @confirm="onEmployeesPicked" />
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dlg-head { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
|
||||
.dlg-head .lbl { color: var(--el-text-color-regular); font-size: 13px; margin-left: 6px; }
|
||||
.dlg-head .tip { margin-left: auto; color: var(--el-text-color-secondary); font-size: 13px; }
|
||||
.dlg-toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
||||
.dlg-toolbar .hint { color: var(--el-text-color-secondary); font-size: 12px; }
|
||||
</style>
|
||||
@@ -0,0 +1,260 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 看台记录(生产-前缫)
|
||||
* 对应 WinForms 端 WinClient\F9.Business.Prod\QS\KTJL\Page_Ktjl
|
||||
* - 两个页签:挡车工 / 车头工
|
||||
* - 过滤:日期 + 车间 + 班次(关键字)
|
||||
* - 按钮:删除(按日期+车间+班次整批)、录入(新增)(按看台排班带出)、录入(指定日期)(复制某日记录)
|
||||
*/
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import request from '../../../utils/request';
|
||||
import KtjlEditDialog from './KtjlEditDialog.vue';
|
||||
|
||||
const TABS = [
|
||||
{ name: 'dcg', label: '挡车工' },
|
||||
{ name: 'ctg', label: '车头工' },
|
||||
];
|
||||
const BANCI = [
|
||||
{ value: 1, label: '甲班' },
|
||||
{ value: 2, label: '乙班' },
|
||||
];
|
||||
|
||||
function today() {
|
||||
const d = new Date();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(d.getDate()).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${m}-${dd}`;
|
||||
}
|
||||
|
||||
const tab = ref('dcg');
|
||||
const deps = ref([]);
|
||||
const filter = reactive({ date: today(), depId: 0, banci: 1, keyword: '' });
|
||||
const state = reactive({
|
||||
dcg: { rows: [], loading: false },
|
||||
ctg: { rows: [], loading: false },
|
||||
});
|
||||
const cur = computed(() => state[tab.value]);
|
||||
const banciName = computed(() => (filter.banci === 2 ? '乙' : '甲'));
|
||||
const depName = computed(() => deps.value.find((d) => d.id === filter.depId)?.name || '');
|
||||
|
||||
const dlg = reactive({ visible: false, type: 'dcg', presetRows: [] });
|
||||
const copyDlg = reactive({ visible: false, date: '' });
|
||||
|
||||
async function loadDeps() {
|
||||
const res = await request.get('/prod/ktpb/qianxian-deps');
|
||||
deps.value = res.data || [];
|
||||
if (deps.value.length && !filter.depId) filter.depId = deps.value[0].id;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!filter.depId) return;
|
||||
const s = cur.value;
|
||||
s.loading = true;
|
||||
try {
|
||||
const res = await request.get(`/prod/ktjl/${tab.value}`, {
|
||||
params: { date: filter.date, depId: filter.depId, banci: filter.banci, like: filter.keyword },
|
||||
});
|
||||
s.rows = res.data || [];
|
||||
} finally {
|
||||
s.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadDeps();
|
||||
await load();
|
||||
});
|
||||
|
||||
async function onTabChange() {
|
||||
await load();
|
||||
}
|
||||
|
||||
/** 录入(新增):已有记录则继续编辑,否则按看台排班带出默认记录 */
|
||||
async function openNew() {
|
||||
if (!filter.depId) return ElMessage.warning('请先选择车间');
|
||||
const res = await request.get(`/prod/ktjl/${tab.value}`, {
|
||||
params: { date: filter.date, depId: filter.depId, banci: filter.banci },
|
||||
});
|
||||
const exist = res.data || [];
|
||||
if (exist.length) {
|
||||
dlg.presetRows = [];
|
||||
} else {
|
||||
const def = await request.get(`/prod/ktjl/${tab.value}/defaults`, {
|
||||
params: { date: filter.date, depId: filter.depId, banci: filter.banci },
|
||||
});
|
||||
dlg.presetRows = def.data || [];
|
||||
if (!dlg.presetRows.length) {
|
||||
ElMessage.warning('该车间该班次暂无看台排班,请先维护「看台排班」或手工添加职工');
|
||||
}
|
||||
}
|
||||
dlg.type = tab.value;
|
||||
dlg.visible = true;
|
||||
}
|
||||
|
||||
/** 录入(指定日期):把某一天的记录复制到当前日期再编辑 */
|
||||
async function openCopy() {
|
||||
if (!filter.depId) return ElMessage.warning('请先选择车间');
|
||||
copyDlg.date = '';
|
||||
copyDlg.visible = true;
|
||||
}
|
||||
|
||||
async function doCopy() {
|
||||
if (!copyDlg.date) return ElMessage.warning('请选择来源日期');
|
||||
const res = await request.get(`/prod/ktjl/${tab.value}`, {
|
||||
params: { date: copyDlg.date, depId: filter.depId, banci: filter.banci },
|
||||
});
|
||||
const list = (res.data || []).map((x) => ({ ...x, id: 0 }));
|
||||
if (!list.length) return ElMessage.warning('来源日期没有看台记录');
|
||||
copyDlg.visible = false;
|
||||
dlg.presetRows = list;
|
||||
dlg.type = tab.value;
|
||||
dlg.visible = true;
|
||||
}
|
||||
|
||||
async function removeGroup() {
|
||||
if (!filter.depId) return ElMessage.warning('请先选择车间');
|
||||
if (!cur.value.rows.length) return ElMessage.warning('当前没有可删除的记录');
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认删除 ${filter.date},${depName.value},${banciName.value}班 的全部${tab.value === 'dcg' ? '挡车工' : '车头工'}看台记录?`,
|
||||
'提示',
|
||||
{ type: 'warning' }
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const res = await request.delete(`/prod/ktjl/${tab.value}`, {
|
||||
params: { date: filter.date, depId: filter.depId, banci: filter.banci },
|
||||
});
|
||||
ElMessage.success(res.message || '删除成功');
|
||||
await load();
|
||||
}
|
||||
|
||||
async function onSaved() {
|
||||
await load();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ktjl-page">
|
||||
<el-card shadow="never">
|
||||
<el-tabs v-model="tab" @tab-change="onTabChange">
|
||||
<el-tab-pane v-for="t in TABS" :key="t.name" :label="t.label" :name="t.name" />
|
||||
</el-tabs>
|
||||
|
||||
<div class="toolbar">
|
||||
<span class="lbl">日期</span>
|
||||
<el-date-picker
|
||||
v-model="filter.date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
:clearable="false"
|
||||
style="width: 150px"
|
||||
@change="load"
|
||||
/>
|
||||
<span class="lbl">车间</span>
|
||||
<el-select v-model="filter.depId" placeholder="请选择车间" style="width: 160px" @change="load">
|
||||
<el-option v-for="d in deps" :key="d.id" :label="d.name" :value="d.id" />
|
||||
</el-select>
|
||||
<span class="lbl">班次</span>
|
||||
<el-select v-model="filter.banci" style="width: 110px" @change="load">
|
||||
<el-option v-for="b in BANCI" :key="b.value" :label="b.label" :value="b.value" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="filter.keyword"
|
||||
placeholder="姓名 / 工种 / 机组"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
@keyup.enter="load"
|
||||
@clear="load"
|
||||
/>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<div class="right">
|
||||
<el-button type="danger" plain @click="removeGroup">删除</el-button>
|
||||
<el-button type="primary" @click="openNew">录入(新增)</el-button>
|
||||
<el-button @click="openCopy">录入(指定日期)</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 挡车工 -->
|
||||
<el-table v-if="tab === 'dcg'" v-loading="state.dcg.loading" :data="state.dcg.rows" border size="small">
|
||||
<el-table-column type="index" label="序号" width="55" />
|
||||
<el-table-column prop="depName" label="部门" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column label="班次" width="70" align="center">
|
||||
<template #default="{ row }">{{ row.banciName }}班</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="employeeName" label="姓名" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="worktypeName" label="工种" width="90" />
|
||||
<el-table-column prop="employeeDepName" label="职工部门" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="jizu" label="机组" width="70" align="center" />
|
||||
<el-table-column prop="chetaiStart" label="开始车位" width="90" align="center" />
|
||||
<el-table-column prop="chetaiEnd" label="结束车位" width="90" align="center" />
|
||||
<el-table-column prop="dangcheNum" label="挡车数" width="80" align="right" />
|
||||
<el-table-column prop="hours" label="时长" width="70" align="right" />
|
||||
<el-table-column label="不计产量" width="90" align="center">
|
||||
<template #default="{ row }">{{ row.noChanliang ? '√' : '' }}</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<span class="empty">当日无记录,可点击「录入(新增)」按看台排班带出</span>
|
||||
</template>
|
||||
</el-table>
|
||||
|
||||
<!-- 车头工 -->
|
||||
<el-table v-else v-loading="state.ctg.loading" :data="state.ctg.rows" border size="small">
|
||||
<el-table-column type="index" label="序号" width="55" />
|
||||
<el-table-column prop="depName" label="部门" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column label="班次" width="70" align="center">
|
||||
<template #default="{ row }">{{ row.banciName }}班</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="employeeName" label="姓名" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="worktypeName" label="工种" width="90" />
|
||||
<el-table-column prop="employeeDepName" label="职工部门" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="jizu" label="机组" width="70" align="center" />
|
||||
<el-table-column prop="hours" label="时长" width="70" align="right" />
|
||||
<el-table-column label="不计产量" width="90" align="center">
|
||||
<template #default="{ row }">{{ row.noChanliang ? '√' : '' }}</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<span class="empty">当日无记录,可点击「录入(新增)」按看台排班带出</span>
|
||||
</template>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<KtjlEditDialog
|
||||
v-model="dlg.visible"
|
||||
:type="dlg.type"
|
||||
:date="filter.date"
|
||||
:dep-id="filter.depId"
|
||||
:dep-name="depName"
|
||||
:banci="filter.banci"
|
||||
:banci-name="banciName"
|
||||
:preset-rows="dlg.presetRows"
|
||||
@saved="onSaved"
|
||||
/>
|
||||
|
||||
<el-dialog v-model="copyDlg.visible" title="选择来源日期" width="360px" append-to-body>
|
||||
<el-date-picker
|
||||
v-model="copyDlg.date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择要复制的日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div class="copy-tip">将把该日期「{{ depName }} · {{ banciName }}班」的{{ tab === 'dcg' ? '挡车工' : '车头工' }}看台记录复制到 {{ filter.date }} 再编辑保存。</div>
|
||||
<template #footer>
|
||||
<el-button @click="copyDlg.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="doCopy">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ktjl-page :deep(.el-card__body) { padding-top: 6px; }
|
||||
.toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
|
||||
.toolbar .lbl { color: var(--el-text-color-regular); font-size: 13px; }
|
||||
.toolbar .right { margin-left: auto; }
|
||||
.empty { color: var(--el-text-color-secondary); font-size: 13px; }
|
||||
.copy-tip { margin-top: 10px; color: var(--el-text-color-secondary); font-size: 12px; line-height: 1.6; }
|
||||
</style>
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
# 工作进度-13:生产-前缫「看台记录」+ 历史数据导入
|
||||
|
||||
> 时间:2026-10-22(承接工作进度-12)
|
||||
> 需求:仿照 `D:\AntaiProject - 副本` 生产-前缫的「看台记录」,在本系统落地并与「看台排班」联动,同时导入两张表数据:
|
||||
> `biz_prod_qs_ktjl_ctg`(车头工看台记录)、`biz_prod_qs_ktjl_dcg`(挡车工看台记录)。
|
||||
> 参考实现:`WinClient\F9.Business.Prod\QS\KTJL\`(Page_Ktjl、Page_Ktjl_Dcg_Edit、Page_Ktjl_Ctg_Edit)、
|
||||
> `Application\CQRS\Business\PROD\QS\KTJL\`(Save/Del/Query)、`Domain\Entities\Business\PROD\QS\KTJL\`。
|
||||
|
||||
---
|
||||
|
||||
## 一、功能设计
|
||||
|
||||
```
|
||||
生产-前缫
|
||||
├── 机组车台
|
||||
├── 落丝记录
|
||||
├── 车间班次
|
||||
├── 看台排班
|
||||
└── 看台记录(本次新增) /prod/qianxian/ktjl
|
||||
过滤:日期 + 车间(职责类型=前缫)+ 班次(甲/乙)+ 关键字
|
||||
页签:挡车工 / 车头工
|
||||
按钮:删除(按 日期+车间+班次 整批)、录入(新增)、录入(指定日期)
|
||||
```
|
||||
|
||||
| F9 行为 | 本系统实现 |
|
||||
|---|---|
|
||||
| 列表按 日期+车间+班次 查询,无分页 | 同左(单日数据量小,一次展示) |
|
||||
| `删除`:按 日期+车间+班次 整批软删 | `DELETE /api/prod/ktjl/{type}?date=&depId=&banci=` |
|
||||
| `录入(新增)`:库中无记录时,按**看台排班**带出该车间该班次的车头工/挡车工 | `GET .../{type}/defaults`,行默认时长 9 小时、未转正职工默认「不计产量」 |
|
||||
| `录入(指定日期)`:把某日记录复制到当前日期再编辑 | 前端选来源日期 → 读取 → 重映射日期 → 弹窗编辑 |
|
||||
| `保存`:GroupBy(date, dep, banci) 先删后建 | 同左(服务端事务内先删后建) |
|
||||
| 挡车数:`结束-开始+1`,与他人接壤的边界车位各扣 0.5 | 前后端同一算法(服务端保存时重算) |
|
||||
| 机组:由车位范围反查机组车台 | 同左(服务端保存时校正) |
|
||||
|
||||
录入弹窗校验(对齐 F9 ValidCustom):
|
||||
- 车头工:不能有空职工;每机组人数 ≥ 2;每机组时长合计为「标准时长」的整数倍且 ≥ 2 倍标准。
|
||||
- 挡车工:不能有空职工;个人时长合计不超过标准时长;挡车数与多人看台接壤关系需匹配(挡车数不能为 0);每台车位的看台时长合计需等于标准时长(多人看台按上下游分别校验)。
|
||||
- 校验不通过分「拦截」和「提示后询问是否继续保存」两级,与 F9 一致。
|
||||
|
||||
---
|
||||
|
||||
## 二、后端
|
||||
|
||||
### 2.1 实体(`Domain/Entities/Prod/Ktjl.cs`)
|
||||
|
||||
| 类 | 表 | 字段 |
|
||||
|---|---|---|
|
||||
| `KtjlCtg` | `KtjlCtgs` | Date、DeptId、Banci、Jizu、Hours、EmployeeId、EmployeeDepId、WorktypeId、WorktypeName、NoChanliang |
|
||||
| `KtjlDcg` | `KtjlDcgs` | 同上 + ChetaiStart、ChetaiEnd、DangcheNum |
|
||||
|
||||
> `WorktypeName` 为历史快照(F9 是查询时联表),避免工种改名后历史记录显示漂移。
|
||||
|
||||
### 2.2 服务与接口(`KtjlService` / `KtjlController`,路由 `api/prod/ktjl`)
|
||||
|
||||
```
|
||||
GET /api/prod/ktjl/dcg?date=&depId=&banci=&like=
|
||||
GET /api/prod/ktjl/ctg?date=&depId=&banci=&like=
|
||||
GET /api/prod/ktjl/dcg/defaults?date=&depId=&banci= 按看台排班带出默认记录
|
||||
GET /api/prod/ktjl/ctg/defaults?date=&depId=&banci=
|
||||
POST /api/prod/ktjl/dcg/batch 整组保存(先删后建)
|
||||
POST /api/prod/ktjl/ctg/batch
|
||||
DELETE /api/prod/ktjl/dcg?date=&depId=&banci=
|
||||
DELETE /api/prod/ktjl/ctg?date=&depId=&banci=
|
||||
GET /api/prod/ktjl/worktypes 工种下拉
|
||||
```
|
||||
|
||||
### 2.3 数据导入(`ProdSeedData` 扩展)
|
||||
|
||||
- 新增两个内嵌资源:`Data/SeedData/biz_prod_qs_ktjl_ctg.csv`、`biz_prod_qs_ktjl_dcg.csv`
|
||||
(共 5 万行,JSON 体积过大,改用紧凑的 `|` 分隔 CSV;csproj 增加 `*.csv` 内嵌规则);
|
||||
- 清洗规则:剔除 `deleted_at` 非空的软删除行、日期序列号异常行(如 25569=1970-01-01)、部门无法映射行、职工不存在的行;
|
||||
- 部门:xlsx 的 `dep_id` 是 F9 部门 Id,经 `BusinessSeedData.DeptData`(F9 Id → 部门名)映射;
|
||||
职工:F9 职工导出保留了原始雪花 Id,与本系统 `Employees.Id` 一致,直接关联;
|
||||
工种:本系统工种保留 F9 雪花 Id,直接对应,并冗余 `WorktypeName`;
|
||||
- 日期:Excel 序列号(如 46007)转 `yyyy-MM-dd`;
|
||||
- 幂等:表内已有数据则整体跳过(历史流水快照,不重复导入),分批 1000 行插入。
|
||||
|
||||
导入结果(本次启动日志):
|
||||
|
||||
```
|
||||
[ProdSeed] 车头工看台记录:来源 12693 行,导入 12693 行,跳过 0 行
|
||||
[ProdSeed] 挡车工看台记录:来源 38048 行,导入 38048 行,跳过 0 行
|
||||
```
|
||||
|
||||
原始数据:车头工 16727 行(已删 4034)、挡车工 59744 行(已删 21692、日期异常 1、职工缺失 3);日期范围 **2025-12-16 ~ 2026-09-11**,覆盖前缫一~四车间、甲/乙班、机组 1~6。
|
||||
|
||||
### 2.4 菜单
|
||||
|
||||
`DbSeeder.SeedProdMenus` 在生产-前缫下新增第 5 个子菜单「看台记录」(`/prod/qianxian/ktjl`,权限 `prod:ktjl:list`),并做排序归一。
|
||||
|
||||
---
|
||||
|
||||
## 三、前端
|
||||
|
||||
| 文件 | 说明 |
|
||||
|---|---|
|
||||
| `views/prod/ktjl/index.vue` | 看台记录列表(双页签 + 日期/车间/班次过滤 + 删除/录入/复制日期) |
|
||||
| `views/prod/ktjl/KtjlEditDialog.vue` | 录入弹窗(可编辑机组/车位/挡车数/时长/不计产量,含 F9 同口径校验) |
|
||||
| `router/index.js` | 注册 `prod/ktjl/index` 路由组件 |
|
||||
|
||||
交互要点:
|
||||
- 车位改动即时重算挡车数与机组;
|
||||
- 支持勾选行「批量修改时长」、「删除选中」,可追加职工(多选职工弹窗,按车间过滤);
|
||||
- 校验提示以对话框列出(如「1组5台,看台时长有误」),可「继续保存」或「返回修改」;
|
||||
- 列表为空时提示可点「录入(新增)」按排班带出。
|
||||
|
||||
---
|
||||
|
||||
## 四、验证记录
|
||||
|
||||
| 项 | 结果 |
|
||||
|---|---|
|
||||
| `dotnet build` | ✅ 0 错误 |
|
||||
| `vite build` | ✅ 通过 |
|
||||
| 菜单 | ✅ 生产-前缫 5 个子菜单,看台记录排第 5 |
|
||||
| 历史数据 | ✅ 车头工 12693 行、挡车工 38048 行入库(日期 2025-12-16 ~ 2026-09-11) |
|
||||
| 查询 | ✅ 任意日期+车间+班次 返回正确行数与姓名/工种/机组/车位/挡车数/时长 |
|
||||
| 录入(新增) 默认带出 | ✅ 前缫一车间甲班:挡车工 42 行、车头工 12 行(来自看台排班,时长默认 9) |
|
||||
| 整组保存 | ✅ 保存 6 条后回读,挡车数 3.5/3.5/3/3.5… 与排班一致 |
|
||||
| 整组删除 | ✅ 删除后该日该班次 0 行 |
|
||||
| 车头工保存/回读 | ✅ 4 条,时长 4.5 正常写入 |
|
||||
|
||||
---
|
||||
|
||||
## 五、后续可做
|
||||
|
||||
1. **按职工汇总**:F9 另有 `Ktjl_*_GetAllGroupbyEmployee`(按职工汇总挡车数/时长),可用于工资计件;如需要可加 `GET /api/prod/ktjl/summary?dateFrom=&dateTo=&depId=`。
|
||||
2. **代班**:F9 录入页支持「他人代班 / 车间内代班」(拆分时长),本版通过「添加职工 + 修改时长 + 调整车位」可等价实现,未做专门按钮。
|
||||
3. **与落丝记录/工资联动**:看台记录是计件工资的输入之一,后续接入薪酬模块时可直接复用 `KtjlDcgs` 的 `DangcheNum × Hours`。
|
||||
4. **批量维护**:目前单日单班次维护,若需要跨日期批量复制(如整月排班),可在 `录入(指定日期)` 基础上扩展多选日期。
|
||||
Reference in New Issue
Block a user