WMS主数据结构升级,支持仓库/物料类型等新实体
后端:重构WmsController/WmsService,新增仓库、库区、货位、物料类型等实体及其增删改查接口,支持批量生成网格货位、货位/容器锁定与启用、物料归档与批量清理。数据库模型新增Warehouse、MaterialType、StockEvent等表,调整索引和字段,支持SQLite自动补表/补列及数据迁移,接口兼容旧语义。 前端:wms.ts/types/wms.ts全面适配新结构,升级API路由和类型定义。WarehouseManagementView.vue重构,支持仓库、物料类型管理、货位网格可视化、库存事件展示等。WmsEntityDialog.vue支持新字段编辑及联动选择。新增AreaFacadePanel.vue实现库区网格立面可视化。wmsOptions.ts新增相关常量及UI优化。 兼容性:后端自动迁移旧数据,接口兼容旧绑定语义,提升灵活性、可维护性和可视化能力。
This commit is contained in:
@@ -22,14 +22,34 @@ public sealed class WmsController : ControllerBase
|
||||
_transportTasks = transportTasks;
|
||||
}
|
||||
|
||||
[HttpGet("warehouses")]
|
||||
public Task<List<Warehouse>> Warehouses([FromQuery] string? q) => _service.Warehouses(q);
|
||||
|
||||
[HttpPost("warehouses")]
|
||||
public async Task<IActionResult> SaveWarehouse([FromBody] MasterDataRequest req) =>
|
||||
Ok(await _service.SaveWarehouse(req, User.ActorName()));
|
||||
|
||||
[HttpPut("warehouses/{id:guid}")]
|
||||
public async Task<IActionResult> UpdateWarehouse(Guid id, [FromBody] MasterDataRequest req) =>
|
||||
Ok(await _service.SaveWarehouse(req with { Id = id }, User.ActorName()));
|
||||
|
||||
[HttpDelete("warehouses/{id:guid}")]
|
||||
public async Task<IActionResult> DeleteWarehouse(Guid id, [FromQuery] long? version)
|
||||
{
|
||||
await _service.DeleteEntity<Warehouse>(id, version, User.ActorName());
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("areas")]
|
||||
public Task<List<WarehouseArea>> Areas([FromQuery] string? q) => _service.Areas(q);
|
||||
public Task<List<WarehouseArea>> Areas([FromQuery] string? q, [FromQuery] Guid? warehouseId) =>
|
||||
_service.Areas(q, warehouseId);
|
||||
|
||||
[HttpPost("areas")]
|
||||
public async Task<IActionResult> SaveArea([FromBody] MasterDataRequest req) => Ok(await _service.SaveArea(req, User.ActorName()));
|
||||
public async Task<IActionResult> SaveArea([FromBody] AreaRequest req) =>
|
||||
Ok(await _service.SaveArea(req, User.ActorName()));
|
||||
|
||||
[HttpPut("areas/{id:guid}")]
|
||||
public async Task<IActionResult> UpdateArea(Guid id, [FromBody] MasterDataRequest req) =>
|
||||
public async Task<IActionResult> UpdateArea(Guid id, [FromBody] AreaRequest req) =>
|
||||
Ok(await _service.SaveArea(req with { Id = id }, User.ActorName()));
|
||||
|
||||
[HttpDelete("areas/{id:guid}")]
|
||||
@@ -40,10 +60,12 @@ public sealed class WmsController : ControllerBase
|
||||
}
|
||||
|
||||
[HttpGet("storages")]
|
||||
public Task<List<Storage>> Storages([FromQuery] string? q) => _service.Storages(q);
|
||||
public Task<List<Storage>> Storages([FromQuery] string? q, [FromQuery] Guid? areaId, [FromQuery] string? locationKind) =>
|
||||
_service.Storages(q, areaId, locationKind);
|
||||
|
||||
[HttpPost("storages")]
|
||||
public async Task<IActionResult> SaveStorage([FromBody] StorageRequest req) => Ok(await _service.SaveStorage(req, User.ActorName()));
|
||||
public async Task<IActionResult> SaveStorage([FromBody] StorageRequest req) =>
|
||||
Ok(await _service.SaveStorage(req, User.ActorName()));
|
||||
|
||||
[HttpPut("storages/{id:guid}")]
|
||||
public async Task<IActionResult> UpdateStorage(Guid id, [FromBody] StorageRequest req) =>
|
||||
@@ -56,14 +78,35 @@ public sealed class WmsController : ControllerBase
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("areas/{id:guid}/generate-bins")]
|
||||
public async Task<IActionResult> GenerateBins(Guid id, [FromBody] GenerateBinsRequest req) =>
|
||||
Ok(new { created = await _service.GenerateBins(id, req, User.ActorName()) });
|
||||
|
||||
[HttpPost("storages/{id:guid}/lock")]
|
||||
public async Task<IActionResult> LockStorage(Guid id, [FromQuery] long? version) =>
|
||||
Ok(await _service.SetStorageLock(id, true, version, User.ActorName()));
|
||||
|
||||
[HttpPost("storages/{id:guid}/unlock")]
|
||||
public async Task<IActionResult> UnlockStorage(Guid id, [FromQuery] long? version) =>
|
||||
Ok(await _service.SetStorageLock(id, false, version, User.ActorName()));
|
||||
|
||||
[HttpPost("storages/{id:guid}/enable")]
|
||||
public async Task<IActionResult> EnableStorage(Guid id, [FromQuery] long? version) =>
|
||||
Ok(await _service.SetStorageEnabled(id, true, version, User.ActorName()));
|
||||
|
||||
[HttpPost("storages/{id:guid}/disable")]
|
||||
public async Task<IActionResult> DisableStorage(Guid id, [FromQuery] long? version) =>
|
||||
Ok(await _service.SetStorageEnabled(id, false, version, User.ActorName()));
|
||||
|
||||
[HttpGet("containers")]
|
||||
public Task<List<Container>> Containers([FromQuery] string? q) => _service.Containers(q);
|
||||
|
||||
[HttpPost("containers")]
|
||||
public async Task<IActionResult> SaveContainer([FromBody] MasterDataRequest req) => Ok(await _service.SaveContainer(req, User.ActorName()));
|
||||
public async Task<IActionResult> SaveContainer([FromBody] ContainerRequest req) =>
|
||||
Ok(await _service.SaveContainer(req, User.ActorName()));
|
||||
|
||||
[HttpPut("containers/{id:guid}")]
|
||||
public async Task<IActionResult> UpdateContainer(Guid id, [FromBody] MasterDataRequest req) =>
|
||||
public async Task<IActionResult> UpdateContainer(Guid id, [FromBody] ContainerRequest req) =>
|
||||
Ok(await _service.SaveContainer(req with { Id = id }, User.ActorName()));
|
||||
|
||||
[HttpDelete("containers/{id:guid}")]
|
||||
@@ -73,11 +116,35 @@ public sealed class WmsController : ControllerBase
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("material-types")]
|
||||
public Task<List<MaterialType>> MaterialTypes([FromQuery] string? q) => _service.MaterialTypes(q);
|
||||
|
||||
[HttpPost("material-types")]
|
||||
public async Task<IActionResult> SaveMaterialType([FromBody] MaterialTypeRequest req) =>
|
||||
Ok(await _service.SaveMaterialType(req, User.ActorName()));
|
||||
|
||||
[HttpPut("material-types/{id:guid}")]
|
||||
public async Task<IActionResult> UpdateMaterialType(Guid id, [FromBody] MaterialTypeRequest req) =>
|
||||
Ok(await _service.SaveMaterialType(req with { Id = id }, User.ActorName()));
|
||||
|
||||
[HttpDelete("material-types/{id:guid}")]
|
||||
public async Task<IActionResult> DeleteMaterialType(Guid id, [FromQuery] long? version)
|
||||
{
|
||||
await _service.DeleteEntity<MaterialType>(id, version, User.ActorName());
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("materials")]
|
||||
public Task<List<Material>> Materials([FromQuery] string? q) => _service.Materials(q);
|
||||
public Task<List<Material>> Materials(
|
||||
[FromQuery] string? q,
|
||||
[FromQuery] string? lifecycle,
|
||||
[FromQuery] bool? onlyUnbound,
|
||||
[FromQuery] bool? onlyBound) =>
|
||||
_service.Materials(q, lifecycle, onlyUnbound, onlyBound);
|
||||
|
||||
[HttpPost("materials")]
|
||||
public async Task<IActionResult> SaveMaterial([FromBody] MaterialRequest req) => Ok(await _service.SaveMaterial(req, User.ActorName()));
|
||||
public async Task<IActionResult> SaveMaterial([FromBody] MaterialRequest req) =>
|
||||
Ok(await _service.SaveMaterial(req, User.ActorName()));
|
||||
|
||||
[HttpPut("materials/{id:guid}")]
|
||||
public async Task<IActionResult> UpdateMaterial(Guid id, [FromBody] MaterialRequest req) =>
|
||||
@@ -90,6 +157,17 @@ public sealed class WmsController : ControllerBase
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("materials/{id:guid}/archive")]
|
||||
public async Task<IActionResult> ArchiveMaterial(Guid id, [FromQuery] long? version) =>
|
||||
Ok(await _service.ArchiveMaterial(id, version, User.ActorName()));
|
||||
|
||||
[HttpPost("materials/purge-archived")]
|
||||
public async Task<IActionResult> PurgeArchivedMaterials([FromQuery] int take = 200)
|
||||
{
|
||||
await _service.PurgeArchivedMaterials(User.ActorName(), take);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("container-locations")]
|
||||
public Task<List<ContainerLocation>> ContainerLocations([FromQuery] string? locationType, [FromQuery] string? q) =>
|
||||
_service.ContainerLocations(locationType, q);
|
||||
@@ -110,23 +188,34 @@ public sealed class WmsController : ControllerBase
|
||||
}
|
||||
|
||||
[HttpGet("container-materials")]
|
||||
public Task<List<ContainerMaterial>> ContainerMaterials([FromQuery] string? q) => _service.ContainerMaterials(q);
|
||||
public Task<List<ContainerMaterial>> ContainerMaterials([FromQuery] string? q, [FromQuery] Guid? containerId) =>
|
||||
_service.ContainerMaterials(q, containerId);
|
||||
|
||||
[HttpPost("container-materials")]
|
||||
public async Task<IActionResult> SaveContainerMaterial([FromBody] ContainerMaterialRequest req) =>
|
||||
Ok(await _service.SaveContainerMaterial(req, User.ActorName()));
|
||||
public async Task<IActionResult> SaveContainerMaterial([FromBody] BindMaterialRequest req) =>
|
||||
Ok(await _service.BindMaterial(req, User.ActorName()));
|
||||
|
||||
[HttpPut("container-materials/{id:guid}")]
|
||||
public async Task<IActionResult> UpdateContainerMaterial(Guid id, [FromBody] ContainerMaterialRequest req) =>
|
||||
Ok(await _service.SaveContainerMaterial(req with { Id = id }, User.ActorName()));
|
||||
[HttpPost("container-materials/bind")]
|
||||
public async Task<IActionResult> BindMaterial([FromBody] BindMaterialRequest req) =>
|
||||
Ok(await _service.BindMaterial(req, User.ActorName()));
|
||||
|
||||
[HttpDelete("container-materials/{id:guid}")]
|
||||
public async Task<IActionResult> UnloadContainerMaterial(Guid id, [FromQuery] string? reason)
|
||||
public async Task<IActionResult> UnbindMaterial(Guid id, [FromQuery] string? reason, [FromQuery] bool archive = false)
|
||||
{
|
||||
await _service.UnloadMaterial(id, User.ActorName(), reason ?? "");
|
||||
await _service.UnbindMaterial(id, User.ActorName(), reason ?? "", archive);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("inventory/materials")]
|
||||
public Task<List<InventoryMaterialRow>> InventoryMaterials(
|
||||
[FromQuery] Guid? areaId, [FromQuery] Guid? storageId, [FromQuery] string? q) =>
|
||||
_service.InventoryMaterials(areaId, storageId, q);
|
||||
|
||||
[HttpGet("stock-events")]
|
||||
public Task<List<StockEvent>> StockEvents(
|
||||
[FromQuery] string? eventType, [FromQuery] Guid? materialId, [FromQuery] Guid? containerId) =>
|
||||
_service.StockEvents(eventType, materialId, containerId);
|
||||
|
||||
[HttpGet("container-location-history")]
|
||||
public Task<List<ContainerLocationHistory>> ContainerLocationHistory([FromQuery] Guid? containerId) =>
|
||||
_service.LocationHistory(containerId);
|
||||
|
||||
@@ -10,12 +10,15 @@ public sealed class PlatformDbContext : DbContext
|
||||
{
|
||||
public PlatformDbContext(DbContextOptions<PlatformDbContext> options) : base(options) { }
|
||||
|
||||
public DbSet<Warehouse> Warehouses => Set<Warehouse>();
|
||||
public DbSet<WarehouseArea> WarehouseAreas => Set<WarehouseArea>();
|
||||
public DbSet<Storage> Storages => Set<Storage>();
|
||||
public DbSet<Container> Containers => Set<Container>();
|
||||
public DbSet<MaterialType> MaterialTypes => Set<MaterialType>();
|
||||
public DbSet<Material> Materials => Set<Material>();
|
||||
public DbSet<ContainerLocation> ContainerLocations => Set<ContainerLocation>();
|
||||
public DbSet<ContainerMaterial> ContainerMaterials => Set<ContainerMaterial>();
|
||||
public DbSet<StockEvent> StockEvents => Set<StockEvent>();
|
||||
public DbSet<ContainerLocationHistory> ContainerLocationHistories => Set<ContainerLocationHistory>();
|
||||
public DbSet<ContainerMaterialHistory> ContainerMaterialHistories => Set<ContainerMaterialHistory>();
|
||||
public DbSet<WmsTransportRule> WmsTransportRules => Set<WmsTransportRule>();
|
||||
@@ -52,9 +55,11 @@ public sealed class PlatformDbContext : DbContext
|
||||
modelBuilder.Entity(entity.ClrType).Property(p.Name).HasConversion(nullableDateTimeOffset).HasMaxLength(40);
|
||||
}
|
||||
|
||||
ConfigureEntityBase<Warehouse>(modelBuilder, "wms_warehouses");
|
||||
ConfigureEntityBase<WarehouseArea>(modelBuilder, "wms_areas");
|
||||
ConfigureEntityBase<Storage>(modelBuilder, "wms_storages");
|
||||
ConfigureEntityBase<Container>(modelBuilder, "wms_containers");
|
||||
ConfigureEntityBase<MaterialType>(modelBuilder, "wms_material_types");
|
||||
ConfigureEntityBase<Material>(modelBuilder, "wms_materials");
|
||||
ConfigureEntityBase<ContainerLocation>(modelBuilder, "wms_container_locations");
|
||||
ConfigureEntityBase<ContainerMaterial>(modelBuilder, "wms_container_materials");
|
||||
@@ -64,16 +69,25 @@ public sealed class PlatformDbContext : DbContext
|
||||
|
||||
ConfigureHistory<ContainerLocationHistory>(modelBuilder, "wms_container_location_history");
|
||||
ConfigureHistory<ContainerMaterialHistory>(modelBuilder, "wms_container_material_history");
|
||||
ConfigureStockEvent(modelBuilder);
|
||||
ConfigureTransportTaskHistory(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<Warehouse>().HasIndex(x => x.Code).IsUnique();
|
||||
modelBuilder.Entity<WarehouseArea>().HasIndex(x => x.Code).IsUnique();
|
||||
modelBuilder.Entity<WarehouseArea>().HasIndex(x => x.WarehouseId);
|
||||
modelBuilder.Entity<Storage>().HasIndex(x => x.Code).IsUnique();
|
||||
modelBuilder.Entity<Storage>().HasIndex(x => x.AreaId);
|
||||
modelBuilder.Entity<Storage>().HasIndex(x => x.Barcode);
|
||||
modelBuilder.Entity<Container>().HasIndex(x => x.Code).IsUnique();
|
||||
modelBuilder.Entity<MaterialType>().HasIndex(x => x.Code).IsUnique();
|
||||
modelBuilder.Entity<Material>().HasIndex(x => x.Code).IsUnique();
|
||||
modelBuilder.Entity<Material>().HasIndex(x => x.Barcode);
|
||||
modelBuilder.Entity<Material>().HasIndex(x => new { x.LifecycleStatus, x.UpdatedAt });
|
||||
modelBuilder.Entity<Material>().HasIndex(x => x.TypeCode);
|
||||
modelBuilder.Entity<ContainerLocation>().HasIndex(x => x.ContainerId).IsUnique();
|
||||
modelBuilder.Entity<ContainerLocation>().HasIndex(x => new { x.LocationType, x.LocationId });
|
||||
modelBuilder.Entity<ContainerMaterial>().HasIndex(x => new { x.ContainerId, x.MaterialId, x.BatchNo, x.SerialNo }).IsUnique();
|
||||
modelBuilder.Entity<ContainerMaterial>().HasIndex(x => x.MaterialId).IsUnique();
|
||||
modelBuilder.Entity<ContainerMaterial>().HasIndex(x => x.ContainerId);
|
||||
|
||||
modelBuilder.Entity<WmsTransportRule>().HasIndex(x => x.Code).IsUnique();
|
||||
modelBuilder.Entity<WmsTransportRule>().HasIndex(x => new { x.TriggerType, x.Enabled, x.Priority });
|
||||
@@ -193,6 +207,35 @@ public sealed class PlatformDbContext : DbContext
|
||||
e.HasIndex(x => x.EventType);
|
||||
}
|
||||
|
||||
private static void ConfigureStockEvent(ModelBuilder modelBuilder)
|
||||
{
|
||||
var e = modelBuilder.Entity<StockEvent>();
|
||||
e.ToTable("wms_stock_events");
|
||||
e.HasKey(x => x.Id);
|
||||
e.Property(x => x.EventType).HasMaxLength(32);
|
||||
e.Property(x => x.MaterialCode).HasMaxLength(64);
|
||||
e.Property(x => x.MaterialName).HasMaxLength(128);
|
||||
e.Property(x => x.MaterialBarcode).HasMaxLength(128);
|
||||
e.Property(x => x.MaterialTypeCode).HasMaxLength(64);
|
||||
e.Property(x => x.ContainerCode).HasMaxLength(64);
|
||||
e.Property(x => x.ContainerName).HasMaxLength(128);
|
||||
e.Property(x => x.StorageCode).HasMaxLength(64);
|
||||
e.Property(x => x.StorageName).HasMaxLength(128);
|
||||
e.Property(x => x.AreaCode).HasMaxLength(64);
|
||||
e.Property(x => x.FromStorageCode).HasMaxLength(64);
|
||||
e.Property(x => x.FromStorageName).HasMaxLength(128);
|
||||
e.Property(x => x.ToStorageCode).HasMaxLength(64);
|
||||
e.Property(x => x.ToStorageName).HasMaxLength(128);
|
||||
e.Property(x => x.RefType).HasMaxLength(64);
|
||||
e.Property(x => x.RefCode).HasMaxLength(64);
|
||||
e.Property(x => x.Operator).HasMaxLength(128);
|
||||
e.Property(x => x.Reason).HasMaxLength(500);
|
||||
e.HasIndex(x => x.OperatedAt);
|
||||
e.HasIndex(x => x.EventType);
|
||||
e.HasIndex(x => x.MaterialId);
|
||||
e.HasIndex(x => x.ContainerId);
|
||||
}
|
||||
|
||||
private static void ConfigureTransportTaskHistory(ModelBuilder modelBuilder)
|
||||
{
|
||||
var e = modelBuilder.Entity<WmsTransportTaskHistory>();
|
||||
|
||||
@@ -58,6 +58,131 @@ public static class PlatformPersistence
|
||||
await EnsureSimpleFieldsTableAsync(db);
|
||||
await EnsureUserDashboardShortcutsTableAsync(db);
|
||||
await EnsureWmsTransportSchemaAsync(db);
|
||||
await EnsureWmsStructureSchemaAsync(db);
|
||||
await MigrateWmsLegacyAsync(scope.ServiceProvider);
|
||||
}
|
||||
|
||||
private static async Task MigrateWmsLegacyAsync(IServiceProvider sp)
|
||||
{
|
||||
try
|
||||
{
|
||||
var wms = sp.GetRequiredService<WmsService>();
|
||||
await wms.MigrateLegacyAsync("system");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 首次建库或缺列时忽略,后续请求可再触发
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>补建仓库/物料类型/库存事件及结构扩展列(幂等,SQLite)。</summary>
|
||||
private static async Task EnsureWmsStructureSchemaAsync(PlatformDbContext db)
|
||||
{
|
||||
if (!db.Database.IsSqlite()) return;
|
||||
|
||||
await db.Database.ExecuteSqlRawAsync("""
|
||||
CREATE TABLE IF NOT EXISTS wms_warehouses (
|
||||
Id TEXT NOT NULL CONSTRAINT PK_wms_warehouses PRIMARY KEY,
|
||||
Code TEXT NOT NULL,
|
||||
Name TEXT NOT NULL,
|
||||
Type TEXT NOT NULL,
|
||||
Enabled INTEGER NOT NULL,
|
||||
SortOrder INTEGER NOT NULL,
|
||||
CreatedAt TEXT NOT NULL,
|
||||
UpdatedAt TEXT NOT NULL,
|
||||
IsDeleted INTEGER NOT NULL,
|
||||
DeletedAt TEXT,
|
||||
DeletedBy TEXT NOT NULL DEFAULT '',
|
||||
Version INTEGER NOT NULL,
|
||||
IsLock INTEGER NOT NULL,
|
||||
CreatedBy TEXT NOT NULL DEFAULT '',
|
||||
UpdatedBy TEXT NOT NULL DEFAULT '',
|
||||
Remark TEXT NOT NULL DEFAULT '',
|
||||
Extend TEXT NOT NULL DEFAULT '{{}}'
|
||||
);
|
||||
""");
|
||||
await db.Database.ExecuteSqlRawAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_wms_warehouses_Code ON wms_warehouses (Code);");
|
||||
|
||||
await db.Database.ExecuteSqlRawAsync("""
|
||||
CREATE TABLE IF NOT EXISTS wms_material_types (
|
||||
Id TEXT NOT NULL CONSTRAINT PK_wms_material_types PRIMARY KEY,
|
||||
Code TEXT NOT NULL,
|
||||
Name TEXT NOT NULL,
|
||||
Spec TEXT NOT NULL DEFAULT '',
|
||||
Unit TEXT NOT NULL DEFAULT 'pcs',
|
||||
Category TEXT NOT NULL DEFAULT '',
|
||||
BarcodePrefix TEXT NOT NULL DEFAULT '',
|
||||
Enabled INTEGER NOT NULL,
|
||||
CreatedAt TEXT NOT NULL,
|
||||
UpdatedAt TEXT NOT NULL,
|
||||
IsDeleted INTEGER NOT NULL,
|
||||
DeletedAt TEXT,
|
||||
DeletedBy TEXT NOT NULL DEFAULT '',
|
||||
Version INTEGER NOT NULL,
|
||||
IsLock INTEGER NOT NULL,
|
||||
CreatedBy TEXT NOT NULL DEFAULT '',
|
||||
UpdatedBy TEXT NOT NULL DEFAULT '',
|
||||
Remark TEXT NOT NULL DEFAULT '',
|
||||
Extend TEXT NOT NULL DEFAULT '{{}}'
|
||||
);
|
||||
""");
|
||||
await db.Database.ExecuteSqlRawAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_wms_material_types_Code ON wms_material_types (Code);");
|
||||
|
||||
await db.Database.ExecuteSqlRawAsync("""
|
||||
CREATE TABLE IF NOT EXISTS wms_stock_events (
|
||||
Id TEXT NOT NULL CONSTRAINT PK_wms_stock_events PRIMARY KEY,
|
||||
EventType TEXT NOT NULL,
|
||||
MaterialId TEXT,
|
||||
MaterialCode TEXT NOT NULL DEFAULT '',
|
||||
MaterialName TEXT NOT NULL DEFAULT '',
|
||||
MaterialBarcode TEXT NOT NULL DEFAULT '',
|
||||
MaterialTypeCode TEXT NOT NULL DEFAULT '',
|
||||
ContainerId TEXT,
|
||||
ContainerCode TEXT NOT NULL DEFAULT '',
|
||||
ContainerName TEXT NOT NULL DEFAULT '',
|
||||
StorageId TEXT,
|
||||
StorageCode TEXT NOT NULL DEFAULT '',
|
||||
StorageName TEXT NOT NULL DEFAULT '',
|
||||
AreaCode TEXT NOT NULL DEFAULT '',
|
||||
FromStorageId TEXT,
|
||||
FromStorageCode TEXT NOT NULL DEFAULT '',
|
||||
FromStorageName TEXT NOT NULL DEFAULT '',
|
||||
ToStorageId TEXT,
|
||||
ToStorageCode TEXT NOT NULL DEFAULT '',
|
||||
ToStorageName TEXT NOT NULL DEFAULT '',
|
||||
RefType TEXT NOT NULL DEFAULT 'Manual',
|
||||
RefId TEXT,
|
||||
RefCode TEXT NOT NULL DEFAULT '',
|
||||
Operator TEXT NOT NULL DEFAULT '',
|
||||
OperatedAt TEXT NOT NULL,
|
||||
Reason TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
""");
|
||||
await db.Database.ExecuteSqlRawAsync("CREATE INDEX IF NOT EXISTS IX_wms_stock_events_OperatedAt ON wms_stock_events (OperatedAt);");
|
||||
|
||||
await EnsureSqliteColumnAsync(db, "wms_areas", "WarehouseId", "TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'");
|
||||
await EnsureSqliteColumnAsync(db, "wms_areas", "LayoutMode", "TEXT NOT NULL DEFAULT 'Flat'");
|
||||
await EnsureSqliteColumnAsync(db, "wms_areas", "State", "TEXT NOT NULL DEFAULT 'Default'");
|
||||
|
||||
await EnsureSqliteColumnAsync(db, "wms_storages", "LocationKind", "TEXT NOT NULL DEFAULT 'Station'");
|
||||
await EnsureSqliteColumnAsync(db, "wms_storages", "ColumnNo", "INTEGER NOT NULL DEFAULT 0");
|
||||
await EnsureSqliteColumnAsync(db, "wms_storages", "LevelNo", "INTEGER NOT NULL DEFAULT 1");
|
||||
await EnsureSqliteColumnAsync(db, "wms_storages", "DepthNo", "INTEGER NOT NULL DEFAULT 1");
|
||||
await EnsureSqliteColumnAsync(db, "wms_storages", "SiteCode", "TEXT NOT NULL DEFAULT ''");
|
||||
await EnsureSqliteColumnAsync(db, "wms_storages", "Barcode", "TEXT NOT NULL DEFAULT ''");
|
||||
|
||||
await EnsureSqliteColumnAsync(db, "wms_containers", "AreaId", "TEXT");
|
||||
await EnsureSqliteColumnAsync(db, "wms_containers", "Barcode", "TEXT NOT NULL DEFAULT ''");
|
||||
await EnsureSqliteColumnAsync(db, "wms_containers", "Length", "REAL NOT NULL DEFAULT 0");
|
||||
await EnsureSqliteColumnAsync(db, "wms_containers", "Width", "REAL NOT NULL DEFAULT 0");
|
||||
await EnsureSqliteColumnAsync(db, "wms_containers", "Height", "REAL NOT NULL DEFAULT 0");
|
||||
|
||||
await EnsureSqliteColumnAsync(db, "wms_materials", "TypeCode", "TEXT NOT NULL DEFAULT ''");
|
||||
await EnsureSqliteColumnAsync(db, "wms_materials", "Barcode", "TEXT NOT NULL DEFAULT ''");
|
||||
await EnsureSqliteColumnAsync(db, "wms_materials", "LifecycleStatus", "TEXT NOT NULL DEFAULT 'Active'");
|
||||
await EnsureSqliteColumnAsync(db, "wms_materials", "UnboundAt", "TEXT");
|
||||
|
||||
await EnsureSqliteColumnAsync(db, "wms_container_materials", "BoundAt", "TEXT NOT NULL DEFAULT ''");
|
||||
}
|
||||
|
||||
/// <summary>为已存在的数据库补建 simple_fields 表(幂等)。</summary>
|
||||
|
||||
@@ -19,11 +19,23 @@ public abstract class WarehouseHistoryBase
|
||||
public string Extend { get; set; } = "{}";
|
||||
}
|
||||
|
||||
public sealed class WarehouseArea : EntityBase
|
||||
public sealed class Warehouse : EntityBase
|
||||
{
|
||||
[MaxLength(64)] public string Code { get; set; } = "";
|
||||
[MaxLength(128)] public string Name { get; set; } = "";
|
||||
[MaxLength(64)] public string Type { get; set; } = "Default";
|
||||
public bool Enabled { get; set; } = true;
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WarehouseArea : EntityBase
|
||||
{
|
||||
public Guid WarehouseId { get; set; }
|
||||
[MaxLength(64)] public string Code { get; set; } = "";
|
||||
[MaxLength(128)] public string Name { get; set; } = "";
|
||||
[MaxLength(64)] public string Type { get; set; } = "Storage";
|
||||
[MaxLength(32)] public string LayoutMode { get; set; } = AreaLayoutModes.Flat;
|
||||
[MaxLength(32)] public string State { get; set; } = "Default";
|
||||
public bool Enabled { get; set; } = true;
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
@@ -34,9 +46,16 @@ public sealed class Storage : EntityBase
|
||||
[MaxLength(64)] public string Code { get; set; } = "";
|
||||
[MaxLength(128)] public string Name { get; set; } = "";
|
||||
[MaxLength(64)] public string StorageType { get; set; } = "Storage";
|
||||
[MaxLength(32)] public string LocationKind { get; set; } = LocationKinds.Station;
|
||||
public int ColumnNo { get; set; }
|
||||
public int LevelNo { get; set; } = 1;
|
||||
public int DepthNo { get; set; } = 1;
|
||||
[MaxLength(64)] public string SiteId { get; set; } = "";
|
||||
[MaxLength(64)] public string SiteCode { get; set; } = "";
|
||||
[MaxLength(128)] public string Barcode { get; set; } = "";
|
||||
/// <summary>已废弃:一货位一容器,保留列兼容旧库。</summary>
|
||||
public int Capacity { get; set; }
|
||||
[MaxLength(32)] public string Status { get; set; } = StorageStatuses.Available;
|
||||
[MaxLength(32)] public string Status { get; set; } = StorageStatuses.Empty;
|
||||
[MaxLength(64)] public string Usage { get; set; } = "";
|
||||
public int Priority { get; set; }
|
||||
[MaxLength(64)] public string ZoneCode { get; set; } = "";
|
||||
@@ -47,10 +66,26 @@ public sealed class Storage : EntityBase
|
||||
|
||||
public sealed class Container : EntityBase
|
||||
{
|
||||
public Guid? AreaId { get; set; }
|
||||
[MaxLength(64)] public string Code { get; set; } = "";
|
||||
[MaxLength(128)] public string Name { get; set; } = "";
|
||||
[MaxLength(64)] public string ContainerType { get; set; } = "Box";
|
||||
[MaxLength(64)] public string Status { get; set; } = "Idle";
|
||||
[MaxLength(64)] public string Status { get; set; } = ContainerStatuses.EmptyMaterial;
|
||||
[MaxLength(128)] public string Barcode { get; set; } = "";
|
||||
public double Length { get; set; }
|
||||
public double Width { get; set; }
|
||||
public double Height { get; set; }
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
|
||||
public sealed class MaterialType : EntityBase
|
||||
{
|
||||
[MaxLength(64)] public string Code { get; set; } = "";
|
||||
[MaxLength(128)] public string Name { get; set; } = "";
|
||||
[MaxLength(128)] public string Spec { get; set; } = "";
|
||||
[MaxLength(32)] public string Unit { get; set; } = "pcs";
|
||||
[MaxLength(64)] public string Category { get; set; } = "";
|
||||
[MaxLength(64)] public string BarcodePrefix { get; set; } = "";
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
|
||||
@@ -58,9 +93,13 @@ public sealed class Material : EntityBase
|
||||
{
|
||||
[MaxLength(64)] public string Code { get; set; } = "";
|
||||
[MaxLength(128)] public string Name { get; set; } = "";
|
||||
[MaxLength(64)] public string TypeCode { get; set; } = "";
|
||||
[MaxLength(128)] public string Barcode { get; set; } = "";
|
||||
[MaxLength(128)] public string Spec { get; set; } = "";
|
||||
[MaxLength(32)] public string Unit { get; set; } = "pcs";
|
||||
[MaxLength(64)] public string Category { get; set; } = "";
|
||||
[MaxLength(32)] public string LifecycleStatus { get; set; } = MaterialLifecycles.Active;
|
||||
public DateTimeOffset? UnboundAt { get; set; }
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
|
||||
@@ -75,18 +114,50 @@ public sealed class ContainerLocation : EntityBase
|
||||
public DateTimeOffset EnteredAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>容器-物料绑定(无数量语义;Quantity 列仅兼容旧库,固定为 1)。</summary>
|
||||
public sealed class ContainerMaterial : EntityBase
|
||||
{
|
||||
public Guid ContainerId { get; set; }
|
||||
public Guid MaterialId { get; set; }
|
||||
public decimal Quantity { get; set; }
|
||||
public decimal Quantity { get; set; } = 1;
|
||||
[MaxLength(64)] public string BatchNo { get; set; } = "";
|
||||
[MaxLength(64)] public string SerialNo { get; set; } = "";
|
||||
[MaxLength(32)] public string Status { get; set; } = ContainerMaterialStatuses.Loaded;
|
||||
[MaxLength(32)] public string Status { get; set; } = ContainerMaterialStatuses.Bound;
|
||||
public DateTimeOffset BoundAt { get; set; }
|
||||
public DateTimeOffset LoadedAt { get; set; }
|
||||
public DateTimeOffset? UnloadedAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed class StockEvent
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(32)] public string EventType { get; set; } = "";
|
||||
public Guid? MaterialId { get; set; }
|
||||
[MaxLength(64)] public string MaterialCode { get; set; } = "";
|
||||
[MaxLength(128)] public string MaterialName { get; set; } = "";
|
||||
[MaxLength(128)] public string MaterialBarcode { get; set; } = "";
|
||||
[MaxLength(64)] public string MaterialTypeCode { get; set; } = "";
|
||||
public Guid? ContainerId { get; set; }
|
||||
[MaxLength(64)] public string ContainerCode { get; set; } = "";
|
||||
[MaxLength(128)] public string ContainerName { get; set; } = "";
|
||||
public Guid? StorageId { get; set; }
|
||||
[MaxLength(64)] public string StorageCode { get; set; } = "";
|
||||
[MaxLength(128)] public string StorageName { get; set; } = "";
|
||||
[MaxLength(64)] public string AreaCode { get; set; } = "";
|
||||
public Guid? FromStorageId { get; set; }
|
||||
[MaxLength(64)] public string FromStorageCode { get; set; } = "";
|
||||
[MaxLength(128)] public string FromStorageName { get; set; } = "";
|
||||
public Guid? ToStorageId { get; set; }
|
||||
[MaxLength(64)] public string ToStorageCode { get; set; } = "";
|
||||
[MaxLength(128)] public string ToStorageName { get; set; } = "";
|
||||
[MaxLength(64)] public string RefType { get; set; } = "Manual";
|
||||
public Guid? RefId { get; set; }
|
||||
[MaxLength(64)] public string RefCode { get; set; } = "";
|
||||
[MaxLength(128)] public string Operator { get; set; } = "";
|
||||
public DateTimeOffset OperatedAt { get; set; }
|
||||
[MaxLength(500)] public string Reason { get; set; } = "";
|
||||
}
|
||||
|
||||
public sealed class ContainerLocationHistory : WarehouseHistoryBase
|
||||
{
|
||||
[MaxLength(32)] public string FromLocationType { get; set; } = "";
|
||||
@@ -101,6 +172,20 @@ public sealed class ContainerMaterialHistory : WarehouseHistoryBase
|
||||
public decimal QuantityDelta { get; set; }
|
||||
}
|
||||
|
||||
public static class AreaLayoutModes
|
||||
{
|
||||
public const string Flat = "Flat";
|
||||
public const string Grid = "Grid";
|
||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase) { Flat, Grid };
|
||||
}
|
||||
|
||||
public static class LocationKinds
|
||||
{
|
||||
public const string Grid = "Grid";
|
||||
public const string Station = "Station";
|
||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase) { Grid, Station };
|
||||
}
|
||||
|
||||
public static class ContainerLocationTypes
|
||||
{
|
||||
public const string Storage = "Storage";
|
||||
@@ -118,20 +203,43 @@ public static class ContainerLocationStatuses
|
||||
|
||||
public static class ContainerMaterialStatuses
|
||||
{
|
||||
public const string Bound = "Bound";
|
||||
// 兼容旧数据
|
||||
public const string Loaded = "Loaded";
|
||||
public const string Unloaded = "Unloaded";
|
||||
public const string Adjusted = "Adjusted";
|
||||
public const string Frozen = "Frozen";
|
||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase) { Loaded, Unloaded, Adjusted, Frozen };
|
||||
public static readonly HashSet<string> ActiveBind = new(StringComparer.OrdinalIgnoreCase) { Bound, Loaded, Adjusted, Frozen };
|
||||
}
|
||||
|
||||
public static class ContainerStatuses
|
||||
{
|
||||
public const string EmptyMaterial = "EmptyMaterial";
|
||||
public const string FullMaterial = "FullMaterial";
|
||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase) { EmptyMaterial, FullMaterial };
|
||||
}
|
||||
|
||||
public static class StorageStatuses
|
||||
{
|
||||
public const string Empty = "Empty";
|
||||
public const string EmptyContainer = "EmptyContainer";
|
||||
public const string FullContainer = "FullContainer";
|
||||
public const string Disabled = "Disabled";
|
||||
// 兼容旧值(读时映射,写时用新值)
|
||||
public const string Available = "Available";
|
||||
public const string Idle = "Idle";
|
||||
public const string Occupied = "Occupied";
|
||||
public const string Disabled = "Disabled";
|
||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase) { Available, Idle, Occupied, Disabled };
|
||||
|
||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase)
|
||||
{ Empty, EmptyContainer, FullContainer, Disabled };
|
||||
|
||||
public static string Normalize(string? status) => status switch
|
||||
{
|
||||
Available or Idle => Empty,
|
||||
Occupied => FullContainer,
|
||||
_ when string.IsNullOrWhiteSpace(status) => Empty,
|
||||
_ => status
|
||||
};
|
||||
}
|
||||
|
||||
public static class StorageTypes
|
||||
@@ -143,6 +251,20 @@ public static class StorageTypes
|
||||
public const string FinishedGoods = "FinishedGoods";
|
||||
}
|
||||
|
||||
public static class MaterialLifecycles
|
||||
{
|
||||
public const string Active = "Active";
|
||||
public const string Archived = "Archived";
|
||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase) { Active, Archived };
|
||||
}
|
||||
|
||||
public static class StockEventTypes
|
||||
{
|
||||
public const string Bind = "Bind";
|
||||
public const string Unbind = "Unbind";
|
||||
public const string ContainerMove = "ContainerMove";
|
||||
}
|
||||
|
||||
public static class WmsTransportTriggerTypes
|
||||
{
|
||||
public const string MaterialCall = "MaterialCall";
|
||||
@@ -194,6 +316,7 @@ public sealed class WmsTransportTask : EntityBase
|
||||
public Guid TargetStorageId { get; set; }
|
||||
public Guid ContainerId { get; set; }
|
||||
public Guid? MaterialId { get; set; }
|
||||
/// <summary>已废弃:单物料实体模型不再使用数量。</summary>
|
||||
public decimal? Quantity { get; set; }
|
||||
[MaxLength(32)] public string Status { get; set; } = WmsTransportTaskStatuses.Pending;
|
||||
[MaxLength(64)] public string DispatchMissionId { get; set; } = "";
|
||||
@@ -227,3 +350,9 @@ public sealed class WmsTransportTaskHistory
|
||||
[MaxLength(1000)] public string ErrorMessage { get; set; } = "";
|
||||
public string SnapshotJson { get; set; } = "{}";
|
||||
}
|
||||
|
||||
public static class WmsDefaults
|
||||
{
|
||||
public const string DefaultWarehouseCode = "DEFAULT";
|
||||
public const string DefaultWarehouseName = "默认仓库";
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@ public sealed class WmsReferenceValidator
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task EnsureWarehouseAsync(Guid id)
|
||||
{
|
||||
if (!await _db.Warehouses.AnyAsync(x => x.Id == id))
|
||||
throw new InvalidOperationException("仓库不存在");
|
||||
}
|
||||
|
||||
public async Task EnsureAreaAsync(Guid id)
|
||||
{
|
||||
if (!await _db.WarehouseAreas.AnyAsync(x => x.Id == id))
|
||||
@@ -36,6 +42,12 @@ public sealed class WmsReferenceValidator
|
||||
throw new InvalidOperationException("物料不存在");
|
||||
}
|
||||
|
||||
public async Task EnsureMaterialTypeCodeAsync(string typeCode)
|
||||
{
|
||||
if (!await _db.MaterialTypes.AnyAsync(x => x.Code == typeCode))
|
||||
throw new InvalidOperationException("物料类型不存在");
|
||||
}
|
||||
|
||||
public async Task<(string Code, string Name)> ResolveLocationSnapshotAsync(string locationType, string locationId)
|
||||
{
|
||||
if (!ContainerLocationTypes.All.Contains(locationType))
|
||||
|
||||
+621
-166
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,10 @@
|
||||
import http from './http'
|
||||
import type {
|
||||
WarehouseArea, Storage, Container, Material,
|
||||
Warehouse, WarehouseArea, Storage, Container, Material, MaterialType,
|
||||
ContainerLocation, ContainerMaterial, ContainerLocationHistory, ContainerMaterialHistory,
|
||||
MasterDataPayload, StoragePayload, MaterialPayload, ContainerLocationPayload, ContainerMaterialPayload,
|
||||
InventoryMaterialRow, StockEvent,
|
||||
MasterDataPayload, AreaPayload, StoragePayload, ContainerPayload, MaterialPayload, MaterialTypePayload,
|
||||
ContainerLocationPayload, BindMaterialPayload, GenerateBinsPayload,
|
||||
WmsTransportRule, WmsTransportTask, TransportRulePayload, TransportRequestQuery, TransportCandidatePreview
|
||||
} from '@/types/wms'
|
||||
|
||||
@@ -10,11 +12,25 @@ function q<T extends object>(params?: T) {
|
||||
return { params }
|
||||
}
|
||||
|
||||
export async function listAreas(keyword?: string) {
|
||||
const { data } = await http.get<WarehouseArea[]>('/wms/areas', q({ q: keyword }))
|
||||
export async function listWarehouses(keyword?: string) {
|
||||
const { data } = await http.get<Warehouse[]>('/wms/warehouses', q({ q: keyword }))
|
||||
return data
|
||||
}
|
||||
export async function saveArea(payload: MasterDataPayload) {
|
||||
export async function saveWarehouse(payload: MasterDataPayload) {
|
||||
const { data } = payload.id
|
||||
? await http.put<Warehouse>(`/wms/warehouses/${payload.id}`, payload)
|
||||
: await http.post<Warehouse>('/wms/warehouses', payload)
|
||||
return data
|
||||
}
|
||||
export async function deleteWarehouse(id: string, version?: number) {
|
||||
await http.delete(`/wms/warehouses/${id}`, q({ version }))
|
||||
}
|
||||
|
||||
export async function listAreas(keyword?: string, warehouseId?: string) {
|
||||
const { data } = await http.get<WarehouseArea[]>('/wms/areas', q({ q: keyword, warehouseId }))
|
||||
return data
|
||||
}
|
||||
export async function saveArea(payload: AreaPayload) {
|
||||
const { data } = payload.id
|
||||
? await http.put<WarehouseArea>(`/wms/areas/${payload.id}`, payload)
|
||||
: await http.post<WarehouseArea>('/wms/areas', payload)
|
||||
@@ -24,8 +40,8 @@ export async function deleteArea(id: string, version?: number) {
|
||||
await http.delete(`/wms/areas/${id}`, q({ version }))
|
||||
}
|
||||
|
||||
export async function listStorages(keyword?: string) {
|
||||
const { data } = await http.get<Storage[]>('/wms/storages', q({ q: keyword }))
|
||||
export async function listStorages(keyword?: string, areaId?: string, locationKind?: string) {
|
||||
const { data } = await http.get<Storage[]>('/wms/storages', q({ q: keyword, areaId, locationKind }))
|
||||
return data
|
||||
}
|
||||
export async function saveStorage(payload: StoragePayload) {
|
||||
@@ -37,12 +53,32 @@ export async function saveStorage(payload: StoragePayload) {
|
||||
export async function deleteStorage(id: string, version?: number) {
|
||||
await http.delete(`/wms/storages/${id}`, q({ version }))
|
||||
}
|
||||
export async function generateBins(areaId: string, payload: GenerateBinsPayload) {
|
||||
const { data } = await http.post<{ created: number }>(`/wms/areas/${areaId}/generate-bins`, payload)
|
||||
return data
|
||||
}
|
||||
export async function lockStorage(id: string, version?: number) {
|
||||
const { data } = await http.post<Storage>(`/wms/storages/${id}/lock`, null, q({ version }))
|
||||
return data
|
||||
}
|
||||
export async function unlockStorage(id: string, version?: number) {
|
||||
const { data } = await http.post<Storage>(`/wms/storages/${id}/unlock`, null, q({ version }))
|
||||
return data
|
||||
}
|
||||
export async function enableStorage(id: string, version?: number) {
|
||||
const { data } = await http.post<Storage>(`/wms/storages/${id}/enable`, null, q({ version }))
|
||||
return data
|
||||
}
|
||||
export async function disableStorage(id: string, version?: number) {
|
||||
const { data } = await http.post<Storage>(`/wms/storages/${id}/disable`, null, q({ version }))
|
||||
return data
|
||||
}
|
||||
|
||||
export async function listContainers(keyword?: string) {
|
||||
const { data } = await http.get<Container[]>('/wms/containers', q({ q: keyword }))
|
||||
return data
|
||||
}
|
||||
export async function saveContainer(payload: MasterDataPayload) {
|
||||
export async function saveContainer(payload: ContainerPayload) {
|
||||
const { data } = payload.id
|
||||
? await http.put<Container>(`/wms/containers/${payload.id}`, payload)
|
||||
: await http.post<Container>('/wms/containers', payload)
|
||||
@@ -52,8 +88,22 @@ export async function deleteContainer(id: string, version?: number) {
|
||||
await http.delete(`/wms/containers/${id}`, q({ version }))
|
||||
}
|
||||
|
||||
export async function listMaterials(keyword?: string) {
|
||||
const { data } = await http.get<Material[]>('/wms/materials', q({ q: keyword }))
|
||||
export async function listMaterialTypes(keyword?: string) {
|
||||
const { data } = await http.get<MaterialType[]>('/wms/material-types', q({ q: keyword }))
|
||||
return data
|
||||
}
|
||||
export async function saveMaterialType(payload: MaterialTypePayload) {
|
||||
const { data } = payload.id
|
||||
? await http.put<MaterialType>(`/wms/material-types/${payload.id}`, payload)
|
||||
: await http.post<MaterialType>('/wms/material-types', payload)
|
||||
return data
|
||||
}
|
||||
export async function deleteMaterialType(id: string, version?: number) {
|
||||
await http.delete(`/wms/material-types/${id}`, q({ version }))
|
||||
}
|
||||
|
||||
export async function listMaterials(params?: { q?: string; lifecycle?: string; onlyUnbound?: boolean; onlyBound?: boolean }) {
|
||||
const { data } = await http.get<Material[]>('/wms/materials', q(params))
|
||||
return data
|
||||
}
|
||||
export async function saveMaterial(payload: MaterialPayload) {
|
||||
@@ -65,6 +115,13 @@ export async function saveMaterial(payload: MaterialPayload) {
|
||||
export async function deleteMaterial(id: string, version?: number) {
|
||||
await http.delete(`/wms/materials/${id}`, q({ version }))
|
||||
}
|
||||
export async function archiveMaterial(id: string, version?: number) {
|
||||
const { data } = await http.post<Material>(`/wms/materials/${id}/archive`, null, q({ version }))
|
||||
return data
|
||||
}
|
||||
export async function purgeArchivedMaterials(take = 200) {
|
||||
await http.post('/wms/materials/purge-archived', null, q({ take }))
|
||||
}
|
||||
|
||||
export async function listContainerLocations(params?: { locationType?: string; q?: string }) {
|
||||
const { data } = await http.get<ContainerLocation[]>('/wms/container-locations', q(params))
|
||||
@@ -78,18 +135,29 @@ export async function deleteContainerLocation(containerId: string, reason?: stri
|
||||
await http.delete(`/wms/container-locations/${containerId}`, q({ reason }))
|
||||
}
|
||||
|
||||
export async function listContainerMaterials(keyword?: string) {
|
||||
const { data } = await http.get<ContainerMaterial[]>('/wms/container-materials', q({ q: keyword }))
|
||||
export async function listContainerMaterials(params?: { q?: string; containerId?: string }) {
|
||||
const { data } = await http.get<ContainerMaterial[]>('/wms/container-materials', q(params))
|
||||
return data
|
||||
}
|
||||
export async function saveContainerMaterial(payload: ContainerMaterialPayload) {
|
||||
const { data } = payload.id
|
||||
? await http.put<ContainerMaterial>(`/wms/container-materials/${payload.id}`, payload)
|
||||
: await http.post<ContainerMaterial>('/wms/container-materials', payload)
|
||||
export async function bindMaterial(payload: BindMaterialPayload) {
|
||||
const { data } = await http.post<ContainerMaterial>('/wms/container-materials/bind', payload)
|
||||
return data
|
||||
}
|
||||
export async function deleteContainerMaterial(id: string, reason?: string) {
|
||||
await http.delete(`/wms/container-materials/${id}`, q({ reason }))
|
||||
/** @deprecated 使用 bindMaterial */
|
||||
export async function saveContainerMaterial(payload: BindMaterialPayload) {
|
||||
return bindMaterial(payload)
|
||||
}
|
||||
export async function deleteContainerMaterial(id: string, reason?: string, archive = false) {
|
||||
await http.delete(`/wms/container-materials/${id}`, q({ reason, archive }))
|
||||
}
|
||||
|
||||
export async function listInventoryMaterials(params?: { areaId?: string; storageId?: string; q?: string }) {
|
||||
const { data } = await http.get<InventoryMaterialRow[]>('/wms/inventory/materials', q(params))
|
||||
return data
|
||||
}
|
||||
export async function listStockEvents(params?: { eventType?: string; materialId?: string; containerId?: string }) {
|
||||
const { data } = await http.get<StockEvent[]>('/wms/stock-events', q(params))
|
||||
return data
|
||||
}
|
||||
|
||||
export async function listContainerLocationHistory(containerId?: string) {
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface EntityBase {
|
||||
extend: string
|
||||
}
|
||||
|
||||
export interface WarehouseArea extends EntityBase {
|
||||
export interface Warehouse extends EntityBase {
|
||||
code: string
|
||||
name: string
|
||||
type: string
|
||||
@@ -21,12 +21,29 @@ export interface WarehouseArea extends EntityBase {
|
||||
sortOrder: number
|
||||
}
|
||||
|
||||
export interface WarehouseArea extends EntityBase {
|
||||
warehouseId: string
|
||||
code: string
|
||||
name: string
|
||||
type: string
|
||||
layoutMode: string
|
||||
state: string
|
||||
enabled: boolean
|
||||
sortOrder: number
|
||||
}
|
||||
|
||||
export interface Storage extends EntityBase {
|
||||
areaId: string
|
||||
code: string
|
||||
name: string
|
||||
storageType: string
|
||||
locationKind: string
|
||||
columnNo: number
|
||||
levelNo: number
|
||||
depthNo: number
|
||||
siteId: string
|
||||
siteCode: string
|
||||
barcode: string
|
||||
capacity: number
|
||||
status: string
|
||||
usage: string
|
||||
@@ -38,25 +55,43 @@ export interface Storage extends EntityBase {
|
||||
}
|
||||
|
||||
export interface Container extends EntityBase {
|
||||
areaId?: string
|
||||
code: string
|
||||
name: string
|
||||
containerType: string
|
||||
status: string
|
||||
barcode: string
|
||||
length: number
|
||||
width: number
|
||||
height: number
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface MaterialType extends EntityBase {
|
||||
code: string
|
||||
name: string
|
||||
spec: string
|
||||
unit: string
|
||||
category: string
|
||||
barcodePrefix: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface Material extends EntityBase {
|
||||
code: string
|
||||
name: string
|
||||
typeCode: string
|
||||
barcode: string
|
||||
spec: string
|
||||
unit: string
|
||||
category: string
|
||||
lifecycleStatus: string
|
||||
unboundAt?: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export type ContainerLocationType = 'Storage' | 'Car'
|
||||
export type ContainerLocationStatus = 'Active' | 'Locked' | 'Exception'
|
||||
export type ContainerMaterialStatus = 'Loaded' | 'Unloaded' | 'Adjusted' | 'Frozen'
|
||||
|
||||
export interface ContainerLocation extends EntityBase {
|
||||
containerId: string
|
||||
@@ -71,14 +106,64 @@ export interface ContainerLocation extends EntityBase {
|
||||
export interface ContainerMaterial extends EntityBase {
|
||||
containerId: string
|
||||
materialId: string
|
||||
/** @deprecated 绑定语义固定为 1,UI 不展示数量 */
|
||||
quantity: number
|
||||
batchNo: string
|
||||
serialNo: string
|
||||
status: ContainerMaterialStatus
|
||||
status: string
|
||||
boundAt: string
|
||||
loadedAt: string
|
||||
unloadedAt?: string
|
||||
}
|
||||
|
||||
export interface InventoryMaterialRow {
|
||||
materialId: string
|
||||
materialCode: string
|
||||
materialName: string
|
||||
materialBarcode: string
|
||||
typeCode: string
|
||||
containerId: string
|
||||
containerCode: string
|
||||
containerName: string
|
||||
storageId?: string
|
||||
storageCode: string
|
||||
storageName: string
|
||||
areaId?: string
|
||||
areaCode: string
|
||||
areaName: string
|
||||
locationType: string
|
||||
boundAt: string
|
||||
}
|
||||
|
||||
export interface StockEvent {
|
||||
id: string
|
||||
eventType: string
|
||||
materialId?: string
|
||||
materialCode: string
|
||||
materialName: string
|
||||
materialBarcode: string
|
||||
materialTypeCode: string
|
||||
containerId?: string
|
||||
containerCode: string
|
||||
containerName: string
|
||||
storageId?: string
|
||||
storageCode: string
|
||||
storageName: string
|
||||
areaCode: string
|
||||
fromStorageId?: string
|
||||
fromStorageCode: string
|
||||
fromStorageName: string
|
||||
toStorageId?: string
|
||||
toStorageCode: string
|
||||
toStorageName: string
|
||||
refType: string
|
||||
refId?: string
|
||||
refCode: string
|
||||
operator: string
|
||||
operatedAt: string
|
||||
reason: string
|
||||
}
|
||||
|
||||
export interface ContainerLocationHistory {
|
||||
id: string
|
||||
relationId?: string
|
||||
@@ -129,6 +214,22 @@ export interface MasterDataPayload {
|
||||
extend: string
|
||||
}
|
||||
|
||||
export interface AreaPayload {
|
||||
id?: string
|
||||
version?: number
|
||||
warehouseId?: string
|
||||
code: string
|
||||
name: string
|
||||
type: string
|
||||
layoutMode: string
|
||||
state: string
|
||||
enabled: boolean
|
||||
sortOrder: number
|
||||
isLock: boolean
|
||||
remark: string
|
||||
extend: string
|
||||
}
|
||||
|
||||
export interface StoragePayload {
|
||||
id?: string
|
||||
version?: number
|
||||
@@ -136,7 +237,13 @@ export interface StoragePayload {
|
||||
code: string
|
||||
name: string
|
||||
storageType: string
|
||||
locationKind: string
|
||||
columnNo: number
|
||||
levelNo: number
|
||||
depthNo: number
|
||||
siteId: string
|
||||
siteCode: string
|
||||
barcode: string
|
||||
capacity: number
|
||||
status: string
|
||||
usage: string
|
||||
@@ -150,7 +257,25 @@ export interface StoragePayload {
|
||||
extend: string
|
||||
}
|
||||
|
||||
export interface MaterialPayload {
|
||||
export interface ContainerPayload {
|
||||
id?: string
|
||||
version?: number
|
||||
areaId?: string
|
||||
code: string
|
||||
name: string
|
||||
type: string
|
||||
status: string
|
||||
barcode: string
|
||||
length: number
|
||||
width: number
|
||||
height: number
|
||||
enabled: boolean
|
||||
isLock: boolean
|
||||
remark: string
|
||||
extend: string
|
||||
}
|
||||
|
||||
export interface MaterialTypePayload {
|
||||
id?: string
|
||||
version?: number
|
||||
code: string
|
||||
@@ -158,6 +283,24 @@ export interface MaterialPayload {
|
||||
spec: string
|
||||
unit: string
|
||||
category: string
|
||||
barcodePrefix: string
|
||||
enabled: boolean
|
||||
isLock: boolean
|
||||
remark: string
|
||||
extend: string
|
||||
}
|
||||
|
||||
export interface MaterialPayload {
|
||||
id?: string
|
||||
version?: number
|
||||
code: string
|
||||
name: string
|
||||
typeCode: string
|
||||
barcode: string
|
||||
spec: string
|
||||
unit: string
|
||||
category: string
|
||||
lifecycleStatus: string
|
||||
enabled: boolean
|
||||
isLock: boolean
|
||||
remark: string
|
||||
@@ -179,17 +322,11 @@ export interface ContainerLocationPayload {
|
||||
extend: string
|
||||
}
|
||||
|
||||
export interface ContainerMaterialPayload {
|
||||
export interface BindMaterialPayload {
|
||||
id?: string
|
||||
version?: number
|
||||
containerId: string
|
||||
materialId: string
|
||||
quantity: number
|
||||
batchNo: string
|
||||
serialNo: string
|
||||
status: ContainerMaterialStatus
|
||||
loadedAt?: string
|
||||
unloadedAt?: string
|
||||
source: string
|
||||
reason: string
|
||||
isLock: boolean
|
||||
@@ -197,6 +334,16 @@ export interface ContainerMaterialPayload {
|
||||
extend: string
|
||||
}
|
||||
|
||||
export interface GenerateBinsPayload {
|
||||
columnFrom: number
|
||||
columnTo: number
|
||||
levelFrom: number
|
||||
levelTo: number
|
||||
depthFrom: number
|
||||
depthTo: number
|
||||
codePattern?: string
|
||||
}
|
||||
|
||||
export type WmsTransportTriggerType = 'MaterialCall' | 'FinishedGoodsOffline' | 'AutoTransfer'
|
||||
export type WmsTransportTaskStatus = 'Pending' | 'Reserved' | 'Dispatched' | 'InTransit' | 'Completed' | 'Failed' | 'Cancelled'
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>仓储管理</h2>
|
||||
<p>库区、库位、容器、物料主数据,以及容器位置和搬运任务配置。</p>
|
||||
<p>仓库→库区→货位;物料类型/实例绑定(无数量);在库与库存事件审计。</p>
|
||||
</div>
|
||||
<el-button :icon="Refresh" :loading="loading" @click="loadAll">刷新</el-button>
|
||||
</div>
|
||||
@@ -25,6 +25,7 @@
|
||||
<el-table-column prop="code" label="编码" width="120" />
|
||||
<el-table-column prop="name" label="名称" />
|
||||
<el-table-column prop="type" label="类型" width="100" />
|
||||
<el-table-column label="布局" width="80"><template #default="{ row }">{{ labelOf(AREA_LAYOUT_MODES, row.layoutMode) }}</template></el-table-column>
|
||||
<el-table-column prop="sortOrder" label="排序" width="70" />
|
||||
<el-table-column prop="enabled" label="启用" width="80"><template #default="{ row }"><el-tag :type="row.enabled ? 'success' : 'info'" size="small">{{ row.enabled ? '是' : '否' }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="isLock" label="锁定" width="80"><template #default="{ row }"><el-tag :type="row.isLock ? 'warning' : 'info'" size="small">{{ row.isLock ? '是' : '否' }}</el-tag></template></el-table-column>
|
||||
@@ -37,6 +38,7 @@
|
||||
<div class="toolbar">
|
||||
<el-input v-model="keyword" placeholder="搜索编码/名称/站点" clearable />
|
||||
<el-button type="primary" @click="openStorage()">新增库位</el-button>
|
||||
<el-button @click="openGenerateBins">批量生成网格</el-button>
|
||||
<el-button type="danger" :disabled="batchStorages.batchCount === 0" @click="batchDeleteStorages">批量删除 ({{ batchStorages.batchCount }})</el-button>
|
||||
</div>
|
||||
<el-table :data="filteredStorages" border size="small" @selection-change="batchStorages.onSelectionChange">
|
||||
@@ -46,7 +48,9 @@
|
||||
<el-table-column label="库区" width="100"><template #default="{ row }">{{ areaName(row.areaId) }}</template></el-table-column>
|
||||
<el-table-column label="类型" width="100"><template #default="{ row }">{{ labelOf(STORAGE_TYPES, row.storageType) }}</template></el-table-column>
|
||||
<el-table-column prop="siteId" label="站点" width="90" />
|
||||
<el-table-column label="状态" width="90"><template #default="{ row }">{{ labelOf(STORAGE_STATUSES, row.status) }}</template></el-table-column>
|
||||
<el-table-column label="状态" width="100"><template #default="{ row }"><el-tag :type="STORAGE_STATUS_TYPE[row.status] ?? 'info'" size="small">{{ labelOf(STORAGE_STATUSES, row.status) }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="种类" width="90"><template #default="{ row }">{{ labelOf(LOCATION_KINDS, row.locationKind) }}</template></el-table-column>
|
||||
<el-table-column label="列/层/深" width="100"><template #default="{ row }">{{ row.locationKind === 'Grid' ? `${row.columnNo}/${row.levelNo}/${row.depthNo}` : '—' }}</template></el-table-column>
|
||||
<el-table-column prop="priority" label="优先级" width="70" />
|
||||
<el-table-column prop="zoneCode" label="区域" width="80" />
|
||||
<el-table-column label="占用" width="70"><template #default="{ row }"><el-tag :type="isStorageOccupied(row.id) ? 'warning' : 'success'" size="small">{{ isStorageOccupied(row.id) ? '是' : '否' }}</el-tag></template></el-table-column>
|
||||
@@ -76,9 +80,31 @@
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="物料类型" name="materialTypes">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="keyword" placeholder="搜索编码/名称" clearable />
|
||||
<el-button type="primary" @click="openMaterialType()">新增类型</el-button>
|
||||
<el-button type="danger" :disabled="batchMaterialTypes.batchCount === 0" @click="batchDeleteMaterialTypes">批量删除 ({{ batchMaterialTypes.batchCount }})</el-button>
|
||||
</div>
|
||||
<el-table :data="filteredMaterialTypes" border size="small" @selection-change="batchMaterialTypes.onSelectionChange">
|
||||
<el-table-column type="selection" width="42" :selectable="batchMaterialTypes.selectable" />
|
||||
<el-table-column prop="code" label="编码" width="120" />
|
||||
<el-table-column prop="name" label="名称" />
|
||||
<el-table-column prop="spec" label="规格" />
|
||||
<el-table-column prop="unit" label="单位" width="70" />
|
||||
<el-table-column prop="category" label="类别" width="100" />
|
||||
<el-table-column prop="barcodePrefix" label="条码前缀" width="100" />
|
||||
<el-table-column prop="enabled" label="启用" width="70"><template #default="{ row }"><el-tag :type="row.enabled ? 'success' : 'info'" size="small">{{ row.enabled ? '是' : '否' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right"><template #default="{ row }"><el-button size="small" link :disabled="row.isLock" @click="openMaterialType(row)">编辑</el-button><el-button size="small" link type="danger" :disabled="row.isLock" @click="removeMaterialType(row)">删除</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="物料" name="materials">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="keyword" placeholder="搜索编码/名称/规格" clearable />
|
||||
<el-select v-model="materialLifecycleFilter" clearable placeholder="生命周期" style="width: 120px" @change="reloadMaterials">
|
||||
<el-option v-for="o in MATERIAL_LIFECYCLES" :key="o.value" :label="o.label" :value="o.value" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="openMaterial()">新增物料</el-button>
|
||||
<el-button type="danger" :disabled="batchMaterials.batchCount === 0" @click="batchDeleteMaterials">批量删除 ({{ batchMaterials.batchCount }})</el-button>
|
||||
</div>
|
||||
@@ -86,13 +112,25 @@
|
||||
<el-table-column type="selection" width="42" :selectable="batchMaterials.selectable" />
|
||||
<el-table-column prop="code" label="编码" width="120" />
|
||||
<el-table-column prop="name" label="名称" />
|
||||
<el-table-column prop="typeCode" label="类型" width="100" />
|
||||
<el-table-column prop="barcode" label="条码" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="spec" label="规格" />
|
||||
<el-table-column prop="unit" label="单位" width="70" />
|
||||
<el-table-column prop="category" label="类别" width="100" />
|
||||
<el-table-column label="生命周期" width="90"><template #default="{ row }"><el-tag :type="row.lifecycleStatus === 'Active' ? 'success' : 'info'" size="small">{{ labelOf(MATERIAL_LIFECYCLES, row.lifecycleStatus) }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="enabled" label="启用" width="70"><template #default="{ row }"><el-tag :type="row.enabled ? 'success' : 'info'" size="small">{{ row.enabled ? '是' : '否' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right"><template #default="{ row }"><el-button size="small" link :disabled="row.isLock" @click="openMaterial(row)">编辑</el-button><el-button size="small" link type="danger" :disabled="row.isLock" @click="removeMaterial(row)">删除</el-button></template></el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" link :disabled="row.isLock" @click="openMaterial(row)">编辑</el-button>
|
||||
<el-button v-if="row.lifecycleStatus === 'Active'" size="small" link :disabled="row.isLock" @click="archiveMaterial(row)">归档</el-button>
|
||||
<el-button size="small" link type="danger" :disabled="row.isLock" @click="removeMaterial(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="库区立面" name="facade" lazy>
|
||||
<AreaFacadePanel :areas="areas" :storages="storages" @edit="openStorage" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-tab-pane>
|
||||
|
||||
@@ -120,18 +158,50 @@
|
||||
<el-tab-pane label="容器物料" name="materials">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="keyword" placeholder="搜索" clearable />
|
||||
<el-button type="primary" @click="openContainerMaterial()">装料/调整</el-button>
|
||||
<el-button type="danger" :disabled="batchContainerMaterials.batchCount === 0" @click="batchUnloadMaterials">批量卸料 ({{ batchContainerMaterials.batchCount }})</el-button>
|
||||
<el-button type="primary" @click="openContainerMaterial()">绑定物料</el-button>
|
||||
<el-button type="danger" :disabled="batchContainerMaterials.batchCount === 0" @click="batchUnloadMaterials">批量解绑 ({{ batchContainerMaterials.batchCount }})</el-button>
|
||||
</div>
|
||||
<el-table :data="filteredContainerMaterials" border size="small" @selection-change="batchContainerMaterials.onSelectionChange">
|
||||
<el-table-column type="selection" width="42" :selectable="batchContainerMaterials.selectable" />
|
||||
<el-table-column label="容器" width="140"><template #default="{ row }">{{ containerName(row.containerId) }}</template></el-table-column>
|
||||
<el-table-column label="物料" width="140"><template #default="{ row }">{{ materialName(row.materialId) }}</template></el-table-column>
|
||||
<el-table-column prop="quantity" label="数量" width="90" />
|
||||
<el-table-column prop="batchNo" label="批次" width="120" />
|
||||
<el-table-column prop="serialNo" label="序列号" width="120" />
|
||||
<el-table-column label="状态" width="90"><template #default="{ row }">{{ labelOf(CONTAINER_MATERIAL_STATUSES, row.status) }}</template></el-table-column>
|
||||
<el-table-column label="操作" width="90" fixed="right"><template #default="{ row }"><el-button size="small" link type="danger" :disabled="row.isLock" @click="removeContainerMaterial(row)">卸料</el-button></template></el-table-column>
|
||||
<el-table-column label="物料" width="180"><template #default="{ row }">{{ materialName(row.materialId) }}</template></el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="90" />
|
||||
<el-table-column prop="boundAt" label="绑定时间" width="170" />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" link type="danger" :disabled="row.isLock" @click="removeContainerMaterial(row, false)">解绑</el-button>
|
||||
<el-button size="small" link type="warning" :disabled="row.isLock" @click="removeContainerMaterial(row, true)">解绑并归档</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="在库物料" name="inventory">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="keyword" placeholder="搜索物料/容器/货位" clearable />
|
||||
<el-button @click="loadInventory">刷新</el-button>
|
||||
</div>
|
||||
<el-table :data="filteredInventory" border size="small">
|
||||
<el-table-column prop="materialCode" label="物料编码" width="120" />
|
||||
<el-table-column prop="materialName" label="物料名称" />
|
||||
<el-table-column prop="containerCode" label="容器" width="100" />
|
||||
<el-table-column prop="storageCode" label="货位" width="100" />
|
||||
<el-table-column prop="areaCode" label="库区" width="90" />
|
||||
<el-table-column prop="boundAt" label="绑定时间" width="170" />
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="库存事件" name="stockEvents">
|
||||
<el-table :data="stockEvents" border size="small">
|
||||
<el-table-column prop="eventType" label="事件" width="120" />
|
||||
<el-table-column prop="materialCode" label="物料" width="120" />
|
||||
<el-table-column prop="containerCode" label="容器" width="100" />
|
||||
<el-table-column prop="storageCode" label="货位" width="100" />
|
||||
<el-table-column prop="fromStorageCode" label="从" width="90" />
|
||||
<el-table-column prop="toStorageCode" label="到" width="90" />
|
||||
<el-table-column prop="operator" label="操作人" width="100" />
|
||||
<el-table-column prop="operatedAt" label="时间" width="170" />
|
||||
<el-table-column prop="reason" label="原因" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
@@ -177,7 +247,6 @@
|
||||
<el-form-item label="触发类型"><el-select v-model="previewForm.triggerType"><el-option label="呼叫物料" value="MaterialCall" /><el-option label="成品下线" value="FinishedGoodsOffline" /><el-option label="自动转运" value="AutoTransfer" /></el-select></el-form-item>
|
||||
<el-form-item label="呼叫站点"><el-input v-model="previewForm.requestSiteId" placeholder="站点 ID" /></el-form-item>
|
||||
<el-form-item label="物料"><el-select v-model="previewForm.materialId" clearable><el-option v-for="m in materials" :key="m.id" :label="m.code" :value="m.id" /></el-select></el-form-item>
|
||||
<el-form-item label="数量"><el-input-number v-model="previewForm.quantity" :min="0" :precision="4" /></el-form-item>
|
||||
<el-form-item label="容器"><el-select v-model="previewForm.containerId" clearable><el-option v-for="c in containers" :key="c.id" :label="c.code" :value="c.id" /></el-select></el-form-item>
|
||||
<el-form-item><el-button type="primary" :loading="previewLoading" @click="runPreview">预览候选</el-button><el-button type="success" :loading="previewLoading" @click="generateFromPreview">生成任务</el-button></el-form-item>
|
||||
</el-form>
|
||||
@@ -187,7 +256,6 @@
|
||||
<el-table-column prop="targetStorageCode" label="终点库位" width="110" />
|
||||
<el-table-column prop="containerCode" label="容器" width="100" />
|
||||
<el-table-column prop="materialCode" label="物料" width="100" />
|
||||
<el-table-column prop="quantity" label="数量" width="80" />
|
||||
<el-table-column prop="score" label="评分" width="70" />
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
@@ -225,21 +293,52 @@
|
||||
|
||||
<WmsEntityDialog
|
||||
v-model:visible="dialogVisible"
|
||||
v-model:area-layout-mode="areaLayoutMode"
|
||||
v-model:container-barcode="containerBarcode"
|
||||
:kind="dialogKind"
|
||||
:saving="saving"
|
||||
:areas="areas"
|
||||
:storages="storages"
|
||||
:containers="containers"
|
||||
:materials="materials"
|
||||
:material-types="materialTypes"
|
||||
:occupied-storage-ids="occupiedStorageIds"
|
||||
:master-form="masterForm"
|
||||
:storage-form="storageForm"
|
||||
:material-form="materialForm"
|
||||
:material-type-form="materialTypeForm"
|
||||
:location-form="locationForm"
|
||||
:container-material-form="containerMaterialForm"
|
||||
:rule-form="ruleForm"
|
||||
@save="submitDialog"
|
||||
/>
|
||||
|
||||
<el-dialog v-model="generateBinsVisible" title="批量生成网格货位" width="480px">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="库区">
|
||||
<el-select v-model="generateBinsAreaId"><el-option v-for="a in areas" :key="a.id" :label="a.name" :value="a.id" /></el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="列范围">
|
||||
<el-input-number v-model="generateBinsForm.columnFrom" :min="1" />
|
||||
<span style="margin: 0 8px">~</span>
|
||||
<el-input-number v-model="generateBinsForm.columnTo" :min="1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="层范围">
|
||||
<el-input-number v-model="generateBinsForm.levelFrom" :min="1" />
|
||||
<span style="margin: 0 8px">~</span>
|
||||
<el-input-number v-model="generateBinsForm.levelTo" :min="1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="进深范围">
|
||||
<el-input-number v-model="generateBinsForm.depthFrom" :min="1" />
|
||||
<span style="margin: 0 8px">~</span>
|
||||
<el-input-number v-model="generateBinsForm.depthTo" :min="1" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="generateBinsVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submitGenerateBins">生成</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -249,16 +348,17 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import * as wmsApi from '@/api/wms'
|
||||
import WmsEntityDialog, { type DialogKind } from '@/views/admin/warehouse/WmsEntityDialog.vue'
|
||||
import AreaFacadePanel from '@/views/admin/warehouse/AreaFacadePanel.vue'
|
||||
import { useWmsBatchActions } from '@/views/admin/warehouse/useWmsBatchActions'
|
||||
import {
|
||||
STORAGE_TYPES, STORAGE_STATUSES, CONTAINER_TYPES, CONTAINER_STATUSES,
|
||||
CONTAINER_LOCATION_STATUSES, CONTAINER_MATERIAL_STATUSES,
|
||||
AREA_LAYOUT_MODES, STORAGE_TYPES, STORAGE_STATUSES, STORAGE_STATUS_TYPE, LOCATION_KINDS,
|
||||
CONTAINER_TYPES, CONTAINER_STATUSES, CONTAINER_LOCATION_STATUSES, MATERIAL_LIFECYCLES,
|
||||
TRANSPORT_TRIGGER_LABELS, TRANSPORT_TASK_STATUS_TYPE, labelOf, truncate
|
||||
} from '@/views/admin/warehouse/wmsOptions'
|
||||
import type {
|
||||
WarehouseArea, Storage, Container, Material, ContainerLocation, ContainerMaterial,
|
||||
ContainerLocationHistory, ContainerMaterialHistory, MasterDataPayload, StoragePayload,
|
||||
MaterialPayload, ContainerLocationPayload, ContainerMaterialPayload,
|
||||
WarehouseArea, Storage, Container, Material, MaterialType, ContainerLocation, ContainerMaterial,
|
||||
ContainerLocationHistory, ContainerMaterialHistory, InventoryMaterialRow, StockEvent,
|
||||
MasterDataPayload, StoragePayload, MaterialPayload, MaterialTypePayload, ContainerLocationPayload, BindMaterialPayload,
|
||||
WmsTransportRule, WmsTransportTask, TransportRulePayload, TransportRequestQuery, TransportCandidatePreview
|
||||
} from '@/types/wms'
|
||||
|
||||
@@ -270,19 +370,28 @@ const keyword = ref('')
|
||||
const locationFilter = ref('')
|
||||
const ruleTriggerFilter = ref('')
|
||||
const taskStatusFilter = ref('')
|
||||
const materialLifecycleFilter = ref('Active')
|
||||
const previewLoading = ref(false)
|
||||
const taskStatuses = ['Pending', 'Reserved', 'Dispatched', 'InTransit', 'Completed', 'Failed', 'Cancelled']
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogKind = ref<DialogKind>('area')
|
||||
const areaLayoutMode = ref('Flat')
|
||||
const containerBarcode = ref('')
|
||||
const generateBinsVisible = ref(false)
|
||||
const generateBinsAreaId = ref('')
|
||||
const generateBinsForm = reactive({ columnFrom: 1, columnTo: 5, levelFrom: 1, levelTo: 3, depthFrom: 1, depthTo: 1 })
|
||||
|
||||
const areas = ref<WarehouseArea[]>([])
|
||||
const storages = ref<Storage[]>([])
|
||||
const containers = ref<Container[]>([])
|
||||
const materialTypes = ref<MaterialType[]>([])
|
||||
const materials = ref<Material[]>([])
|
||||
const containerLocations = ref<ContainerLocation[]>([])
|
||||
const containerMaterials = ref<ContainerMaterial[]>([])
|
||||
const inventoryRows = ref<InventoryMaterialRow[]>([])
|
||||
const stockEvents = ref<StockEvent[]>([])
|
||||
const locationHistory = ref<ContainerLocationHistory[]>([])
|
||||
const materialHistory = ref<ContainerMaterialHistory[]>([])
|
||||
const transportRules = ref<WmsTransportRule[]>([])
|
||||
@@ -292,14 +401,16 @@ const previewResult = ref<TransportCandidatePreview | null>(null)
|
||||
const masterForm = reactive<MasterDataPayload>(baseMaster())
|
||||
const storageForm = reactive<StoragePayload>(baseStorage())
|
||||
const materialForm = reactive<MaterialPayload>(baseMaterial())
|
||||
const materialTypeForm = reactive<MaterialTypePayload>(baseMaterialType())
|
||||
const locationForm = reactive<ContainerLocationPayload>(baseLocation())
|
||||
const containerMaterialForm = reactive<ContainerMaterialPayload>(baseContainerMaterial())
|
||||
const containerMaterialForm = reactive<BindMaterialPayload>(baseContainerMaterial())
|
||||
const ruleForm = reactive<TransportRulePayload>(baseRule())
|
||||
const previewForm = reactive<TransportRequestQuery>({ triggerType: 'MaterialCall', priority: 0, reason: '' })
|
||||
|
||||
const batchAreas = useWmsBatchActions<WarehouseArea>()
|
||||
const batchStorages = useWmsBatchActions<Storage>()
|
||||
const batchContainers = useWmsBatchActions<Container>()
|
||||
const batchMaterialTypes = useWmsBatchActions<MaterialType>()
|
||||
const batchMaterials = useWmsBatchActions<Material>()
|
||||
const batchRules = useWmsBatchActions<WmsTransportRule>()
|
||||
const batchLocations = useWmsBatchActions<ContainerLocation>()
|
||||
@@ -317,11 +428,18 @@ const occupiedStorageIds = computed(() => {
|
||||
const filteredAreas = computed(() => filterRows(areas.value, keyword.value))
|
||||
const filteredStorages = computed(() => filterRows(storages.value, keyword.value))
|
||||
const filteredContainers = computed(() => filterRows(containers.value, keyword.value))
|
||||
const filteredMaterialTypes = computed(() => filterRows(materialTypes.value, keyword.value))
|
||||
const filteredMaterials = computed(() => filterRows(materials.value, keyword.value))
|
||||
const filteredContainerLocations = computed(() => containerLocations.value.filter((r) =>
|
||||
(!locationFilter.value || r.locationType === locationFilter.value) &&
|
||||
(!keyword.value || `${r.locationCode} ${r.locationName}`.toLowerCase().includes(keyword.value.toLowerCase()))))
|
||||
const filteredContainerMaterials = computed(() => filterRows(containerMaterials.value, keyword.value))
|
||||
const filteredInventory = computed(() => {
|
||||
if (!keyword.value) return inventoryRows.value
|
||||
const s = keyword.value.toLowerCase()
|
||||
return inventoryRows.value.filter((r) =>
|
||||
`${r.materialCode} ${r.materialName} ${r.containerCode} ${r.storageCode} ${r.areaCode}`.toLowerCase().includes(s))
|
||||
})
|
||||
const filteredRules = computed(() => transportRules.value.filter((r) => !ruleTriggerFilter.value || r.triggerType === ruleTriggerFilter.value))
|
||||
const historyRows = computed(() => [
|
||||
...locationHistory.value.map((x) => ({ ...x, kind: '位置' })),
|
||||
@@ -333,15 +451,24 @@ function taskSelectable(row: WmsTransportTask) {
|
||||
return !row.isLock && ['Pending', 'Reserved', 'Failed'].includes(row.status)
|
||||
}
|
||||
|
||||
async function reloadMaterials() {
|
||||
materials.value = await wmsApi.listMaterials({
|
||||
lifecycle: materialLifecycleFilter.value || undefined
|
||||
})
|
||||
}
|
||||
|
||||
async function loadAll() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [a, s, c, m, cl, cm, lh, mh] = await Promise.all([
|
||||
wmsApi.listAreas(), wmsApi.listStorages(), wmsApi.listContainers(), wmsApi.listMaterials(),
|
||||
wmsApi.listContainerLocations(), wmsApi.listContainerMaterials(), wmsApi.listContainerLocationHistory(), wmsApi.listContainerMaterialHistory()
|
||||
const [a, s, c, mt, m, cl, cm, lh, mh, inv, ev] = await Promise.all([
|
||||
wmsApi.listAreas(), wmsApi.listStorages(), wmsApi.listContainers(), wmsApi.listMaterialTypes(),
|
||||
wmsApi.listMaterials({ lifecycle: materialLifecycleFilter.value || undefined }),
|
||||
wmsApi.listContainerLocations(), wmsApi.listContainerMaterials(), wmsApi.listContainerLocationHistory(), wmsApi.listContainerMaterialHistory(),
|
||||
wmsApi.listInventoryMaterials(), wmsApi.listStockEvents()
|
||||
])
|
||||
areas.value = a; storages.value = s; containers.value = c; materials.value = m
|
||||
areas.value = a; storages.value = s; containers.value = c; materialTypes.value = mt; materials.value = m
|
||||
containerLocations.value = cl; containerMaterials.value = cm; locationHistory.value = lh; materialHistory.value = mh
|
||||
inventoryRows.value = inv; stockEvents.value = ev
|
||||
await loadTransportRules()
|
||||
await loadTransportTasks()
|
||||
} finally {
|
||||
@@ -349,18 +476,47 @@ async function loadAll() {
|
||||
}
|
||||
}
|
||||
|
||||
function openArea(row?: WarehouseArea) { dialogKind.value = 'area'; Object.assign(masterForm, baseMaster(row)); dialogVisible.value = true }
|
||||
async function loadInventory() {
|
||||
inventoryRows.value = await wmsApi.listInventoryMaterials()
|
||||
stockEvents.value = await wmsApi.listStockEvents()
|
||||
}
|
||||
|
||||
function openArea(row?: WarehouseArea) {
|
||||
dialogKind.value = 'area'
|
||||
Object.assign(masterForm, baseMaster(row))
|
||||
areaLayoutMode.value = row?.layoutMode ?? 'Flat'
|
||||
dialogVisible.value = true
|
||||
}
|
||||
function openStorage(row?: Storage) { dialogKind.value = 'storage'; Object.assign(storageForm, baseStorage(row)); dialogVisible.value = true }
|
||||
function openContainer(row?: Container) {
|
||||
dialogKind.value = 'container'
|
||||
Object.assign(masterForm, baseMaster(row))
|
||||
if (row) masterForm.type = row.containerType
|
||||
containerBarcode.value = row?.barcode ?? ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
function openMaterialType(row?: MaterialType) { dialogKind.value = 'materialType'; Object.assign(materialTypeForm, baseMaterialType(row)); dialogVisible.value = true }
|
||||
function openMaterial(row?: Material) { dialogKind.value = 'material'; Object.assign(materialForm, baseMaterial(row)); dialogVisible.value = true }
|
||||
function openLocation(row?: ContainerLocation) { dialogKind.value = 'location'; Object.assign(locationForm, baseLocation(row)); dialogVisible.value = true }
|
||||
function openContainerMaterial(row?: ContainerMaterial) { dialogKind.value = 'containerMaterial'; Object.assign(containerMaterialForm, baseContainerMaterial(row)); dialogVisible.value = true }
|
||||
function openRule(row?: WmsTransportRule) { dialogKind.value = 'rule'; Object.assign(ruleForm, baseRule(row)); dialogVisible.value = true }
|
||||
function openGenerateBins() {
|
||||
generateBinsAreaId.value = areas.value[0]?.id ?? ''
|
||||
generateBinsVisible.value = true
|
||||
}
|
||||
|
||||
async function submitGenerateBins() {
|
||||
if (!generateBinsAreaId.value) { ElMessage.warning('请选择库区'); return }
|
||||
saving.value = true
|
||||
try {
|
||||
const r = await wmsApi.generateBins(generateBinsAreaId.value, { ...generateBinsForm })
|
||||
ElMessage.success(`已生成 ${r.created} 个货位`)
|
||||
generateBinsVisible.value = false
|
||||
await loadAll()
|
||||
} catch (e) {
|
||||
ElMessage.error(e instanceof Error ? e.message : String(e))
|
||||
} finally { saving.value = false }
|
||||
}
|
||||
|
||||
async function loadTransportRules() { transportRules.value = await wmsApi.listTransportRules() }
|
||||
async function loadTransportTasks() { transportTasks.value = await wmsApi.listTransportTasks({ status: taskStatusFilter.value || undefined }) }
|
||||
@@ -388,10 +544,30 @@ async function submitDialog() {
|
||||
saving.value = true
|
||||
const kind = dialogKind.value
|
||||
try {
|
||||
JSON.parse((kind === 'storage' ? storageForm : kind === 'material' ? materialForm : kind === 'rule' ? ruleForm : masterForm).extend || '{}')
|
||||
if (kind === 'area') await wmsApi.saveArea(masterForm)
|
||||
else if (kind === 'container') await wmsApi.saveContainer(masterForm)
|
||||
const extendSrc = kind === 'storage' ? storageForm
|
||||
: kind === 'material' ? materialForm
|
||||
: kind === 'materialType' ? materialTypeForm
|
||||
: kind === 'rule' ? ruleForm
|
||||
: masterForm
|
||||
JSON.parse(extendSrc.extend || '{}')
|
||||
if (kind === 'area') {
|
||||
await wmsApi.saveArea({
|
||||
...masterForm,
|
||||
warehouseId: undefined,
|
||||
layoutMode: areaLayoutMode.value,
|
||||
state: 'Default'
|
||||
})
|
||||
}
|
||||
else if (kind === 'container') {
|
||||
await wmsApi.saveContainer({
|
||||
...masterForm,
|
||||
barcode: containerBarcode.value,
|
||||
length: 0, width: 0, height: 0,
|
||||
status: 'EmptyMaterial'
|
||||
})
|
||||
}
|
||||
else if (kind === 'storage') await wmsApi.saveStorage(storageForm)
|
||||
else if (kind === 'materialType') await wmsApi.saveMaterialType(materialTypeForm)
|
||||
else if (kind === 'material') await wmsApi.saveMaterial(materialForm)
|
||||
else if (kind === 'location') await wmsApi.saveContainerLocation(locationForm)
|
||||
else if (kind === 'rule') {
|
||||
@@ -400,7 +576,7 @@ async function submitDialog() {
|
||||
JSON.parse(ruleForm.taskOptionsJson || '{}')
|
||||
await wmsApi.saveTransportRule(ruleForm)
|
||||
}
|
||||
else await wmsApi.saveContainerMaterial(containerMaterialForm)
|
||||
else await wmsApi.bindMaterial(containerMaterialForm)
|
||||
ElMessage.success('已保存')
|
||||
dialogVisible.value = false
|
||||
await loadAll()
|
||||
@@ -419,9 +595,20 @@ async function confirmDelete(message: string) {
|
||||
async function removeArea(row: WarehouseArea) { await confirmDelete('删除该库区?'); await wmsApi.deleteArea(row.id, row.version); await loadAll() }
|
||||
async function removeStorage(row: Storage) { await confirmDelete('删除该库位?'); await wmsApi.deleteStorage(row.id, row.version); await loadAll() }
|
||||
async function removeContainer(row: Container) { await confirmDelete('删除该容器?'); await wmsApi.deleteContainer(row.id, row.version); await loadAll() }
|
||||
async function removeMaterialType(row: MaterialType) { await confirmDelete('删除该物料类型?'); await wmsApi.deleteMaterialType(row.id, row.version); await loadAll() }
|
||||
async function removeMaterial(row: Material) { await confirmDelete('删除该物料?'); await wmsApi.deleteMaterial(row.id, row.version); await loadAll() }
|
||||
async function archiveMaterial(row: Material) {
|
||||
await confirmDelete(`归档物料 ${row.code}?归档后仍保留主数据,可后续 purge。`)
|
||||
await wmsApi.archiveMaterial(row.id, row.version)
|
||||
ElMessage.success('已归档')
|
||||
await loadAll()
|
||||
}
|
||||
async function removeLocation(row: ContainerLocation) { await confirmDelete('解绑该容器位置?'); await wmsApi.deleteContainerLocation(row.containerId); await loadAll() }
|
||||
async function removeContainerMaterial(row: ContainerMaterial) { await confirmDelete('卸载该容器物料?'); await wmsApi.deleteContainerMaterial(row.id); await loadAll() }
|
||||
async function removeContainerMaterial(row: ContainerMaterial, archive = false) {
|
||||
await confirmDelete(archive ? '解绑并归档该物料?' : '解绑该容器物料?')
|
||||
await wmsApi.deleteContainerMaterial(row.id, undefined, archive)
|
||||
await loadAll()
|
||||
}
|
||||
async function removeRule(row: WmsTransportRule) { await confirmDelete('删除该规则?'); await wmsApi.deleteTransportRule(row.id, row.version); await loadTransportRules() }
|
||||
async function reserveTask(row: WmsTransportTask) { await wmsApi.reserveTransportTask(row.id); ElMessage.success('已预占'); await loadTransportTasks() }
|
||||
async function completeTask(row: WmsTransportTask) { await wmsApi.completeTransportTask(row.id); ElMessage.success('已完成'); await loadAll() }
|
||||
@@ -439,6 +626,10 @@ async function batchDeleteContainers() {
|
||||
const r = await batchContainers.runBatch({ actionLabel: '批量删除', confirmMessage: '确认删除选中的 {n} 个容器?', run: (row) => wmsApi.deleteContainer(row.id, row.version) })
|
||||
if (r.ok > 0) await loadAll()
|
||||
}
|
||||
async function batchDeleteMaterialTypes() {
|
||||
const r = await batchMaterialTypes.runBatch({ actionLabel: '批量删除', confirmMessage: '确认删除选中的 {n} 个物料类型?', run: (row) => wmsApi.deleteMaterialType(row.id, row.version) })
|
||||
if (r.ok > 0) await loadAll()
|
||||
}
|
||||
async function batchDeleteMaterials() {
|
||||
const r = await batchMaterials.runBatch({ actionLabel: '批量删除', confirmMessage: '确认删除选中的 {n} 个物料?', run: (row) => wmsApi.deleteMaterial(row.id, row.version) })
|
||||
if (r.ok > 0) await loadAll()
|
||||
@@ -500,13 +691,15 @@ function baseMaster(row?: WarehouseArea | Container): MasterDataPayload {
|
||||
function baseStorage(row?: Storage): StoragePayload {
|
||||
return {
|
||||
...baseCommon(row), areaId: row?.areaId ?? areas.value[0]?.id ?? '', code: row?.code ?? '', name: row?.name ?? '',
|
||||
storageType: row?.storageType ?? 'Storage', siteId: row?.siteId ?? '', capacity: row?.capacity ?? 0,
|
||||
status: row?.status ?? 'Available', usage: row?.usage ?? '', priority: row?.priority ?? 0, zoneCode: row?.zoneCode ?? '',
|
||||
storageType: row?.storageType ?? 'Storage', locationKind: row?.locationKind ?? 'Station',
|
||||
columnNo: row?.columnNo ?? 0, levelNo: row?.levelNo ?? 1, depthNo: row?.depthNo ?? 1,
|
||||
siteId: row?.siteId ?? '', siteCode: row?.siteCode ?? row?.siteId ?? '', barcode: row?.barcode ?? '', capacity: 1,
|
||||
status: row?.status ?? 'Empty', usage: row?.usage ?? '', priority: row?.priority ?? 0, zoneCode: row?.zoneCode ?? '',
|
||||
allowInbound: row?.allowInbound ?? true, allowOutbound: row?.allowOutbound ?? true, enabled: row?.enabled ?? true
|
||||
}
|
||||
}
|
||||
function baseRule(row?: WmsTransportRule): TransportRulePayload {
|
||||
const defaultSource = JSON.stringify({ schemaVersion: 1, storage: { requireOccupied: true, allowOutbound: true }, container: { excludeReserved: true }, material: { statusIn: ['Loaded'] }, ranking: [{ field: 'enteredAt', direction: 'asc' }] }, null, 2)
|
||||
const defaultSource = JSON.stringify({ schemaVersion: 1, storage: { requireOccupied: true, allowOutbound: true }, container: { excludeReserved: true }, material: {}, ranking: [{ field: 'enteredAt', direction: 'asc' }] }, null, 2)
|
||||
const defaultTarget = JSON.stringify({ schemaVersion: 1, storage: { requireEmpty: true, allowInbound: true, requireSiteId: true }, ranking: [{ field: 'storagePriority', direction: 'desc' }] }, null, 2)
|
||||
const defaultOptions = JSON.stringify({ autoReserve: true, requireManualConfirm: false, allowSameStorage: false, reservationTtlSeconds: 600, dispatchMode: 'Manual', maxCandidateCount: 20 }, null, 2)
|
||||
return {
|
||||
@@ -517,14 +710,31 @@ function baseRule(row?: WmsTransportRule): TransportRulePayload {
|
||||
taskOptionsJson: row?.taskOptionsJson ?? defaultOptions
|
||||
}
|
||||
}
|
||||
function baseMaterialType(row?: MaterialType): MaterialTypePayload {
|
||||
return {
|
||||
...baseCommon(row), code: row?.code ?? '', name: row?.name ?? '',
|
||||
spec: row?.spec ?? '', unit: row?.unit ?? 'pcs', category: row?.category ?? '',
|
||||
barcodePrefix: row?.barcodePrefix ?? '', enabled: row?.enabled ?? true
|
||||
}
|
||||
}
|
||||
function baseMaterial(row?: Material): MaterialPayload {
|
||||
return { ...baseCommon(row), code: row?.code ?? '', name: row?.name ?? '', spec: row?.spec ?? '', unit: row?.unit ?? 'pcs', category: row?.category ?? '', enabled: row?.enabled ?? true }
|
||||
return {
|
||||
...baseCommon(row), code: row?.code ?? '', name: row?.name ?? '', typeCode: row?.typeCode ?? materialTypes.value[0]?.code ?? '',
|
||||
barcode: row?.barcode ?? '', spec: row?.spec ?? '', unit: row?.unit ?? 'pcs', category: row?.category ?? '',
|
||||
lifecycleStatus: row?.lifecycleStatus ?? 'Active', enabled: row?.enabled ?? true
|
||||
}
|
||||
}
|
||||
function baseLocation(row?: ContainerLocation): ContainerLocationPayload {
|
||||
return { ...baseCommon(row), containerId: row?.containerId ?? containers.value[0]?.id ?? '', locationType: row?.locationType ?? 'Storage', locationId: row?.locationId ?? '', status: row?.status ?? 'Active', enteredAt: row?.enteredAt, source: 'Manual', reason: '' }
|
||||
}
|
||||
function baseContainerMaterial(row?: ContainerMaterial): ContainerMaterialPayload {
|
||||
return { ...baseCommon(row), containerId: row?.containerId ?? containers.value[0]?.id ?? '', materialId: row?.materialId ?? materials.value[0]?.id ?? '', quantity: row?.quantity ?? 1, batchNo: row?.batchNo ?? '', serialNo: row?.serialNo ?? '', status: row?.status ?? 'Loaded', loadedAt: row?.loadedAt, source: 'Manual', reason: '' }
|
||||
function baseContainerMaterial(row?: ContainerMaterial): BindMaterialPayload {
|
||||
return {
|
||||
...baseCommon(row),
|
||||
containerId: row?.containerId ?? containers.value[0]?.id ?? '',
|
||||
materialId: row?.materialId ?? materials.value[0]?.id ?? '',
|
||||
source: 'Manual',
|
||||
reason: ''
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadAll)
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div class="facade-panel">
|
||||
<div class="toolbar">
|
||||
<el-select v-model="areaId" placeholder="选择库区" style="width: 200px">
|
||||
<el-option v-for="a in areas" :key="a.id" :label="`${a.code} ${a.name}`" :value="a.id" />
|
||||
</el-select>
|
||||
<el-select v-model="depthNo" placeholder="进深" style="width: 120px">
|
||||
<el-option v-for="d in depthOptions" :key="d" :label="`进深 ${d}`" :value="d" />
|
||||
</el-select>
|
||||
<span class="hint">层×列立面(仅 Grid 货位);点击单元格可编辑</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!areaId" class="empty">请选择库区</div>
|
||||
<div v-else-if="columns.length === 0" class="empty">该库区暂无网格货位,可先「批量生成网格」</div>
|
||||
<div v-else class="facade-scroll">
|
||||
<table class="facade-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="corner">层\列</th>
|
||||
<th v-for="c in columns" :key="c">{{ c }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="lv in levelsDesc" :key="lv">
|
||||
<th>{{ lv }}</th>
|
||||
<td
|
||||
v-for="col in columns"
|
||||
:key="`${lv}-${col}`"
|
||||
:class="cellClass(cellAt(col, lv))"
|
||||
:title="cellTitle(cellAt(col, lv))"
|
||||
@click="onCellClick(cellAt(col, lv))"
|
||||
>
|
||||
<template v-if="cellAt(col, lv)">
|
||||
<div class="code">{{ cellAt(col, lv)!.code }}</div>
|
||||
<div class="status">{{ statusLabel(cellAt(col, lv)!.status) }}</div>
|
||||
</template>
|
||||
<span v-else class="miss">—</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { WarehouseArea, Storage } from '@/types/wms'
|
||||
import { STORAGE_STATUSES, labelOf } from './wmsOptions'
|
||||
|
||||
const props = defineProps<{
|
||||
areas: WarehouseArea[]
|
||||
storages: Storage[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
edit: [Storage]
|
||||
}>()
|
||||
|
||||
const areaId = ref('')
|
||||
const depthNo = ref(1)
|
||||
|
||||
watch(
|
||||
() => props.areas,
|
||||
(list) => {
|
||||
if (!areaId.value && list.length) areaId.value = list[0].id
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const gridBins = computed(() =>
|
||||
props.storages.filter(
|
||||
(s) => s.areaId === areaId.value && s.locationKind === 'Grid' && s.depthNo === depthNo.value
|
||||
)
|
||||
)
|
||||
|
||||
const depthOptions = computed(() => {
|
||||
const set = new Set(
|
||||
props.storages
|
||||
.filter((s) => s.areaId === areaId.value && s.locationKind === 'Grid')
|
||||
.map((s) => s.depthNo || 1)
|
||||
)
|
||||
const arr = [...set].sort((a, b) => a - b)
|
||||
return arr.length ? arr : [1]
|
||||
})
|
||||
|
||||
watch(depthOptions, (opts) => {
|
||||
if (!opts.includes(depthNo.value)) depthNo.value = opts[0]
|
||||
})
|
||||
|
||||
const columns = computed(() => {
|
||||
const set = new Set(gridBins.value.map((s) => s.columnNo || 0).filter((n) => n > 0))
|
||||
return [...set].sort((a, b) => a - b)
|
||||
})
|
||||
|
||||
const levelsDesc = computed(() => {
|
||||
const set = new Set(gridBins.value.map((s) => s.levelNo || 1))
|
||||
return [...set].sort((a, b) => b - a)
|
||||
})
|
||||
|
||||
const cellMap = computed(() => {
|
||||
const map = new Map<string, Storage>()
|
||||
for (const s of gridBins.value) {
|
||||
map.set(`${s.columnNo}:${s.levelNo}`, s)
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
function cellAt(col: number, level: number) {
|
||||
return cellMap.value.get(`${col}:${level}`)
|
||||
}
|
||||
|
||||
function statusLabel(status: string) {
|
||||
return labelOf(STORAGE_STATUSES, status)
|
||||
}
|
||||
|
||||
function cellClass(s?: Storage) {
|
||||
if (!s) return 'cell empty-slot'
|
||||
if (!s.enabled || s.status === 'Disabled') return 'cell disabled'
|
||||
if (s.status === 'FullContainer') return 'cell full'
|
||||
if (s.status === 'EmptyContainer') return 'cell empty-c'
|
||||
return 'cell vacant'
|
||||
}
|
||||
|
||||
function cellTitle(s?: Storage) {
|
||||
if (!s) return '无货位'
|
||||
return `${s.code} · ${s.name}\n列${s.columnNo} 层${s.levelNo} 深${s.depthNo}\n${statusLabel(s.status)}`
|
||||
}
|
||||
|
||||
function onCellClick(s?: Storage) {
|
||||
if (s) emit('edit', s)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.facade-panel { min-height: 280px; }
|
||||
.toolbar { display: flex; gap: 10px; align-items: center; margin-bottom: 12px; flex-wrap: wrap; }
|
||||
.hint { font-size: 12px; color: var(--mg-text-muted, #888); }
|
||||
.empty { padding: 48px; text-align: center; color: var(--mg-text-muted, #888); }
|
||||
.facade-scroll { overflow: auto; max-height: 520px; border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 6px; }
|
||||
.facade-table { border-collapse: collapse; font-size: 12px; min-width: 100%; }
|
||||
.facade-table th,
|
||||
.facade-table td { border: 1px solid rgba(0, 0, 0, 0.08); padding: 6px 8px; text-align: center; min-width: 72px; }
|
||||
.facade-table thead th,
|
||||
.facade-table tbody th { background: rgba(0, 0, 0, 0.03); font-weight: 600; position: sticky; }
|
||||
.facade-table thead th { top: 0; z-index: 2; }
|
||||
.facade-table tbody th { left: 0; z-index: 1; }
|
||||
.corner { left: 0; z-index: 3 !important; }
|
||||
.cell { cursor: pointer; transition: background 0.15s; }
|
||||
.cell:hover { outline: 2px solid rgba(64, 158, 255, 0.45); outline-offset: -2px; }
|
||||
.cell .code { font-weight: 600; line-height: 1.2; }
|
||||
.cell .status { margin-top: 2px; opacity: 0.75; font-size: 11px; }
|
||||
.miss { color: #bbb; }
|
||||
.vacant { background: #e8f5e9; }
|
||||
.empty-c { background: #fff8e1; }
|
||||
.full { background: #ffebee; }
|
||||
.disabled { background: #eceff1; color: #90a4ae; cursor: default; }
|
||||
.empty-slot { background: #fafafa; cursor: default; }
|
||||
</style>
|
||||
@@ -13,6 +13,7 @@
|
||||
<el-form-item label="编码" prop="code"><el-input v-model="masterForm.code" :disabled="isEditing" /></el-form-item>
|
||||
<el-form-item label="名称" prop="name"><el-input v-model="masterForm.name" /></el-form-item>
|
||||
<el-form-item label="类型"><el-select v-model="masterForm.type"><el-option v-for="o in AREA_TYPES" :key="o.value" :label="o.label" :value="o.value" /></el-select></el-form-item>
|
||||
<el-form-item label="布局"><el-select v-model="areaLayoutMode"><el-option v-for="o in AREA_LAYOUT_MODES" :key="o.value" :label="o.label" :value="o.value" /></el-select></el-form-item>
|
||||
<el-form-item label="排序"><el-input-number v-model="masterForm.sortOrder" :min="0" /></el-form-item>
|
||||
<el-form-item label="启用"><el-switch v-model="masterForm.enabled" /></el-form-item>
|
||||
</template>
|
||||
@@ -21,7 +22,7 @@
|
||||
<el-form-item label="编码" prop="code"><el-input v-model="masterForm.code" :disabled="isEditing" /></el-form-item>
|
||||
<el-form-item label="名称" prop="name"><el-input v-model="masterForm.name" /></el-form-item>
|
||||
<el-form-item label="容器类型"><el-select v-model="masterForm.type"><el-option v-for="o in CONTAINER_TYPES" :key="o.value" :label="o.label" :value="o.value" /></el-select></el-form-item>
|
||||
<el-form-item label="状态"><el-select v-model="masterForm.status"><el-option v-for="o in CONTAINER_STATUSES" :key="o.value" :label="o.label" :value="o.value" /></el-select></el-form-item>
|
||||
<el-form-item label="条码"><el-input v-model="containerBarcode" /></el-form-item>
|
||||
<el-form-item label="启用"><el-switch v-model="masterForm.enabled" /></el-form-item>
|
||||
</template>
|
||||
|
||||
@@ -29,7 +30,13 @@
|
||||
<el-form-item label="库区"><el-select v-model="storageForm.areaId"><el-option v-for="a in areas" :key="a.id" :label="a.name" :value="a.id" /></el-select></el-form-item>
|
||||
<el-form-item label="编码" prop="code"><el-input v-model="storageForm.code" :disabled="isEditing" /></el-form-item>
|
||||
<el-form-item label="名称" prop="name"><el-input v-model="storageForm.name" /></el-form-item>
|
||||
<el-form-item label="货位种类"><el-select v-model="storageForm.locationKind"><el-option v-for="o in LOCATION_KINDS" :key="o.value" :label="o.label" :value="o.value" /></el-select></el-form-item>
|
||||
<el-form-item label="库位类型"><el-select v-model="storageForm.storageType"><el-option v-for="o in STORAGE_TYPES" :key="o.value" :label="o.label" :value="o.value" /></el-select></el-form-item>
|
||||
<template v-if="storageForm.locationKind === 'Grid'">
|
||||
<el-form-item label="列"><el-input-number v-model="storageForm.columnNo" :min="0" /></el-form-item>
|
||||
<el-form-item label="层"><el-input-number v-model="storageForm.levelNo" :min="1" /></el-form-item>
|
||||
<el-form-item label="进深"><el-input-number v-model="storageForm.depthNo" :min="1" /></el-form-item>
|
||||
</template>
|
||||
<el-form-item label="站点">
|
||||
<el-select
|
||||
v-model="storageForm.siteId"
|
||||
@@ -39,27 +46,52 @@
|
||||
placeholder="搜索站点 ID 或名称"
|
||||
:filter-method="onSiteFilter"
|
||||
class="wms-field-control"
|
||||
@change="onSiteChange"
|
||||
>
|
||||
<el-option v-for="s in displayedSites" :key="s.id" :label="s.label" :value="s.id" />
|
||||
</el-select>
|
||||
<span v-if="sitesLoadError" class="field-hint field-hint-warn">{{ sitesLoadError }}</span>
|
||||
<span v-else class="field-hint">主数据来自 SimpleLite,保存值为站点 id</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态"><el-select v-model="storageForm.status"><el-option v-for="o in STORAGE_STATUSES" :key="o.value" :label="o.label" :value="o.value" /></el-select></el-form-item>
|
||||
<el-form-item label="条码"><el-input v-model="storageForm.barcode" /></el-form-item>
|
||||
<el-form-item label="优先级"><el-input-number v-model="storageForm.priority" :min="0" /></el-form-item>
|
||||
<el-form-item label="区域"><el-input v-model="storageForm.zoneCode" /></el-form-item>
|
||||
<el-form-item label="容量"><el-input-number v-model="storageForm.capacity" :min="0" /><span class="field-hint">展示用途;业务占用按 1 库位 1 容器</span></el-form-item>
|
||||
<el-form-item label="允许入库"><el-switch v-model="storageForm.allowInbound" /></el-form-item>
|
||||
<el-form-item label="允许出库"><el-switch v-model="storageForm.allowOutbound" /></el-form-item>
|
||||
<el-form-item label="启用"><el-switch v-model="storageForm.enabled" /></el-form-item>
|
||||
</template>
|
||||
|
||||
<template v-else-if="kind === 'materialType'">
|
||||
<el-form-item label="编码" prop="code"><el-input v-model="materialTypeForm.code" :disabled="isEditing" /></el-form-item>
|
||||
<el-form-item label="名称" prop="name"><el-input v-model="materialTypeForm.name" /></el-form-item>
|
||||
<el-form-item label="规格"><el-input v-model="materialTypeForm.spec" /></el-form-item>
|
||||
<el-form-item label="单位"><el-select v-model="materialTypeForm.unit" filterable allow-create><el-option v-for="o in MATERIAL_UNITS" :key="o.value" :label="o.label" :value="o.value" /></el-select></el-form-item>
|
||||
<el-form-item label="类别"><el-input v-model="materialTypeForm.category" /></el-form-item>
|
||||
<el-form-item label="条码前缀"><el-input v-model="materialTypeForm.barcodePrefix" /></el-form-item>
|
||||
<el-form-item label="启用"><el-switch v-model="materialTypeForm.enabled" /></el-form-item>
|
||||
</template>
|
||||
|
||||
<template v-else-if="kind === 'material'">
|
||||
<el-form-item label="编码" prop="code"><el-input v-model="materialForm.code" :disabled="isEditing" /></el-form-item>
|
||||
<el-form-item label="名称" prop="name"><el-input v-model="materialForm.name" /></el-form-item>
|
||||
<el-form-item label="物料类型">
|
||||
<el-select
|
||||
v-model="materialForm.typeCode"
|
||||
filterable
|
||||
clearable
|
||||
allow-create
|
||||
placeholder="选择或输入类型编码"
|
||||
class="wms-field-control"
|
||||
@change="onMaterialTypeChange"
|
||||
>
|
||||
<el-option v-for="t in materialTypes" :key="t.id" :label="`${t.code} ${t.name}`" :value="t.code" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="条码"><el-input v-model="materialForm.barcode" /></el-form-item>
|
||||
<el-form-item label="规格"><el-input v-model="materialForm.spec" /></el-form-item>
|
||||
<el-form-item label="单位"><el-select v-model="materialForm.unit" filterable allow-create><el-option v-for="o in MATERIAL_UNITS" :key="o.value" :label="o.label" :value="o.value" /></el-select></el-form-item>
|
||||
<el-form-item label="类别"><el-input v-model="materialForm.category" /></el-form-item>
|
||||
<el-form-item label="生命周期"><el-select v-model="materialForm.lifecycleStatus"><el-option v-for="o in MATERIAL_LIFECYCLES" :key="o.value" :label="o.label" :value="o.value" /></el-select></el-form-item>
|
||||
<el-form-item label="启用"><el-switch v-model="materialForm.enabled" /></el-form-item>
|
||||
</template>
|
||||
|
||||
@@ -78,11 +110,9 @@
|
||||
|
||||
<template v-else-if="kind === 'containerMaterial'">
|
||||
<el-form-item label="容器"><el-select v-model="containerMaterialForm.containerId" :disabled="isEditing"><el-option v-for="c in containers" :key="c.id" :label="c.code" :value="c.id" /></el-select></el-form-item>
|
||||
<el-form-item label="物料"><el-select v-model="containerMaterialForm.materialId" :disabled="isEditing"><el-option v-for="m in materials" :key="m.id" :label="m.code" :value="m.id" /></el-select></el-form-item>
|
||||
<el-form-item label="数量" prop="quantity"><el-input-number v-model="containerMaterialForm.quantity" :min="0.0001" :precision="4" /></el-form-item>
|
||||
<el-form-item label="批次"><el-input v-model="containerMaterialForm.batchNo" :disabled="isEditing" /></el-form-item>
|
||||
<el-form-item label="序列号"><el-input v-model="containerMaterialForm.serialNo" :disabled="isEditing" /></el-form-item>
|
||||
<el-form-item label="状态"><el-select v-model="containerMaterialForm.status"><el-option v-for="o in CONTAINER_MATERIAL_STATUSES" :key="o.value" :label="o.label" :value="o.value" /></el-select></el-form-item>
|
||||
<el-form-item label="物料"><el-select v-model="containerMaterialForm.materialId" filterable :disabled="isEditing"><el-option v-for="m in materials" :key="m.id" :label="`${m.code} ${m.name}`" :value="m.id" /></el-select></el-form-item>
|
||||
<el-form-item label="原因" prop="reason"><el-input v-model="containerMaterialForm.reason" placeholder="绑定原因" /></el-form-item>
|
||||
<p class="field-hint">按单个物料实体绑定,无数量。</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="kind === 'rule'">
|
||||
@@ -117,17 +147,17 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type {
|
||||
WarehouseArea, Storage, Container, Material,
|
||||
MasterDataPayload, StoragePayload, MaterialPayload,
|
||||
ContainerLocationPayload, ContainerMaterialPayload, TransportRulePayload
|
||||
WarehouseArea, Storage, Container, Material, MaterialType,
|
||||
MasterDataPayload, StoragePayload, MaterialPayload, MaterialTypePayload,
|
||||
ContainerLocationPayload, BindMaterialPayload, TransportRulePayload
|
||||
} from '@/types/wms'
|
||||
import {
|
||||
AREA_TYPES, STORAGE_TYPES, STORAGE_STATUSES, CONTAINER_TYPES, CONTAINER_STATUSES,
|
||||
CONTAINER_LOCATION_STATUSES, CONTAINER_MATERIAL_STATUSES, MATERIAL_UNITS
|
||||
AREA_TYPES, AREA_LAYOUT_MODES, STORAGE_TYPES, LOCATION_KINDS, CONTAINER_TYPES,
|
||||
CONTAINER_LOCATION_STATUSES, MATERIAL_UNITS, MATERIAL_LIFECYCLES
|
||||
} from './wmsOptions'
|
||||
import { useSimpleLiteSites } from './useSimpleLiteSites'
|
||||
|
||||
export type DialogKind = 'area' | 'storage' | 'container' | 'material' | 'location' | 'containerMaterial' | 'rule'
|
||||
export type DialogKind = 'area' | 'storage' | 'container' | 'materialType' | 'material' | 'location' | 'containerMaterial' | 'rule'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
@@ -137,12 +167,14 @@ const props = defineProps<{
|
||||
storages: Storage[]
|
||||
containers: Container[]
|
||||
materials: Material[]
|
||||
materialTypes: MaterialType[]
|
||||
occupiedStorageIds: Set<string>
|
||||
masterForm: MasterDataPayload
|
||||
storageForm: StoragePayload
|
||||
materialForm: MaterialPayload
|
||||
materialTypeForm: MaterialTypePayload
|
||||
locationForm: ContainerLocationPayload
|
||||
containerMaterialForm: ContainerMaterialPayload
|
||||
containerMaterialForm: BindMaterialPayload
|
||||
ruleForm: TransportRulePayload
|
||||
}>()
|
||||
|
||||
@@ -152,9 +184,24 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const areaLayoutMode = defineModel<string>('areaLayoutMode', { default: 'Flat' })
|
||||
const containerBarcode = defineModel<string>('containerBarcode', { default: '' })
|
||||
const { sites: simpleLiteSites, loading: sitesLoading, loadError: sitesLoadError, load: loadSimpleLiteSites } = useSimpleLiteSites()
|
||||
const siteSearch = ref('')
|
||||
|
||||
function onSiteChange(id: string) {
|
||||
props.storageForm.siteCode = id || ''
|
||||
}
|
||||
|
||||
function onMaterialTypeChange(code: string) {
|
||||
const t = props.materialTypes.find((x) => x.code === code)
|
||||
if (!t) return
|
||||
if (!props.materialForm.name) props.materialForm.name = t.name
|
||||
if (!props.materialForm.spec) props.materialForm.spec = t.spec
|
||||
if (!props.materialForm.unit) props.materialForm.unit = t.unit || 'pcs'
|
||||
if (!props.materialForm.category) props.materialForm.category = t.category
|
||||
}
|
||||
|
||||
const siteOptionsWithCurrent = computed(() => {
|
||||
const opts = [...simpleLiteSites.value]
|
||||
const current = props.storageForm.siteId?.trim()
|
||||
@@ -178,6 +225,7 @@ function onSiteFilter(query: string) {
|
||||
|
||||
const isEditing = computed(() => {
|
||||
if (props.kind === 'storage') return !!props.storageForm.id
|
||||
if (props.kind === 'materialType') return !!props.materialTypeForm.id
|
||||
if (props.kind === 'material') return !!props.materialForm.id
|
||||
if (props.kind === 'location') return !!props.locationForm.id
|
||||
if (props.kind === 'containerMaterial') return !!props.containerMaterialForm.id
|
||||
@@ -187,6 +235,7 @@ const isEditing = computed(() => {
|
||||
|
||||
const editCode = computed(() => {
|
||||
if (props.kind === 'storage') return props.storageForm.code
|
||||
if (props.kind === 'materialType') return props.materialTypeForm.code
|
||||
if (props.kind === 'material') return props.materialForm.code
|
||||
if (props.kind === 'rule') return props.ruleForm.code
|
||||
return props.masterForm.code
|
||||
@@ -194,7 +243,7 @@ const editCode = computed(() => {
|
||||
|
||||
const title = computed(() => {
|
||||
const names: Record<DialogKind, string> = {
|
||||
area: '库区', storage: '库位', container: '容器', material: '物料',
|
||||
area: '库区', storage: '库位', container: '容器', materialType: '物料类型', material: '物料',
|
||||
location: '容器位置', containerMaterial: '容器物料', rule: '搬运规则'
|
||||
}
|
||||
const code = editCode.value?.trim()
|
||||
@@ -207,6 +256,7 @@ const availableStorages = computed(() =>
|
||||
const formModel = computed(() => {
|
||||
switch (props.kind) {
|
||||
case 'storage': return props.storageForm
|
||||
case 'materialType': return props.materialTypeForm
|
||||
case 'material': return props.materialForm
|
||||
case 'location': return props.locationForm
|
||||
case 'containerMaterial': return props.containerMaterialForm
|
||||
@@ -231,8 +281,7 @@ const isLock = computed({
|
||||
const rules: FormRules = {
|
||||
code: [{ required: true, message: '请输入编码', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
reason: [{ required: true, message: '请填写原因', trigger: 'blur' }],
|
||||
quantity: [{ required: true, message: '请输入数量', trigger: 'change' }]
|
||||
reason: [{ required: true, message: '请填写原因', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
watch(() => props.visible, (v) => {
|
||||
|
||||
@@ -4,6 +4,11 @@ export const AREA_TYPES = [
|
||||
{ value: 'FinishedGoods', label: '成品区' }
|
||||
]
|
||||
|
||||
export const AREA_LAYOUT_MODES = [
|
||||
{ value: 'Flat', label: '平面' },
|
||||
{ value: 'Grid', label: '网格' }
|
||||
]
|
||||
|
||||
export const STORAGE_TYPES = [
|
||||
{ value: 'Storage', label: '普通库位' },
|
||||
{ value: 'LineSide', label: '线边库' },
|
||||
@@ -12,23 +17,33 @@ export const STORAGE_TYPES = [
|
||||
{ value: 'FinishedGoods', label: '成品库' }
|
||||
]
|
||||
|
||||
export const LOCATION_KINDS = [
|
||||
{ value: 'Station', label: '接驳位' },
|
||||
{ value: 'Grid', label: '网格货位' }
|
||||
]
|
||||
|
||||
export const STORAGE_STATUSES = [
|
||||
{ value: 'Available', label: '可用' },
|
||||
{ value: 'Idle', label: '空闲' },
|
||||
{ value: 'Occupied', label: '已占用' },
|
||||
{ value: 'Empty', label: '空货位' },
|
||||
{ value: 'EmptyContainer', label: '空容器' },
|
||||
{ value: 'FullContainer', label: '满容器' },
|
||||
{ value: 'Disabled', label: '停用' }
|
||||
]
|
||||
|
||||
export const STORAGE_STATUS_TYPE: Record<string, 'success' | 'warning' | 'danger' | 'info'> = {
|
||||
Empty: 'success',
|
||||
EmptyContainer: 'warning',
|
||||
FullContainer: 'danger',
|
||||
Disabled: 'info'
|
||||
}
|
||||
|
||||
export const CONTAINER_TYPES = [
|
||||
{ value: 'Box', label: '料箱' },
|
||||
{ value: 'Pallet', label: '托盘' }
|
||||
]
|
||||
|
||||
export const CONTAINER_STATUSES = [
|
||||
{ value: 'Idle', label: '空闲' },
|
||||
{ value: 'Loaded', label: '已装载' },
|
||||
{ value: 'Disabled', label: '停用' },
|
||||
{ value: 'Exception', label: '异常' }
|
||||
{ value: 'EmptyMaterial', label: '空物料' },
|
||||
{ value: 'FullMaterial', label: '有物料' }
|
||||
]
|
||||
|
||||
export const CONTAINER_LOCATION_STATUSES = [
|
||||
@@ -37,12 +52,6 @@ export const CONTAINER_LOCATION_STATUSES = [
|
||||
{ value: 'Exception', label: '异常' }
|
||||
]
|
||||
|
||||
export const CONTAINER_MATERIAL_STATUSES = [
|
||||
{ value: 'Loaded', label: '已装载' },
|
||||
{ value: 'Adjusted', label: '已调整' },
|
||||
{ value: 'Frozen', label: '冻结' }
|
||||
]
|
||||
|
||||
export const MATERIAL_UNITS = [
|
||||
{ value: 'pcs', label: 'pcs' },
|
||||
{ value: 'kg', label: 'kg' },
|
||||
@@ -50,6 +59,17 @@ export const MATERIAL_UNITS = [
|
||||
{ value: 'set', label: 'set' }
|
||||
]
|
||||
|
||||
export const MATERIAL_LIFECYCLES = [
|
||||
{ value: 'Active', label: '在用' },
|
||||
{ value: 'Archived', label: '已归档' }
|
||||
]
|
||||
|
||||
export const STOCK_EVENT_TYPES = [
|
||||
{ value: 'Bind', label: '绑定' },
|
||||
{ value: 'Unbind', label: '解绑' },
|
||||
{ value: 'ContainerMove', label: '容器移位' }
|
||||
]
|
||||
|
||||
export const TRANSPORT_TRIGGER_LABELS: Record<string, string> = {
|
||||
MaterialCall: '呼叫物料',
|
||||
FinishedGoodsOffline: '成品下线',
|
||||
|
||||
Reference in New Issue
Block a user