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:
2026-07-24 10:19:36 +08:00
parent ce66da772e
commit bdcd88608c
12 changed files with 1794 additions and 288 deletions
+106 -17
View File
@@ -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);
+44 -1
View File
@@ -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>
+137 -8
View File
@@ -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
View File
@@ -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))
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 绑定语义固定为 1UI 不展示数量 */
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: '成品下线',