正在显示
41 个修改的文件
包含
2987 行增加
和
976 行删除
| @@ -26,7 +26,7 @@ public partial class App : Application | @@ -26,7 +26,7 @@ public partial class App : Application | ||
| 26 | await _configLoader.EnsureLatestAsync(); | 26 | await _configLoader.EnsureLatestAsync(); |
| 27 | 27 | ||
| 28 | // 2) 判断是否已登录 | 28 | // 2) 判断是否已登录 |
| 29 | - var token = await TokenStorage.GetAsync(); | 29 | + var token = await TokenStorage.LoadAsync(); |
| 30 | var isLoggedIn = !string.IsNullOrWhiteSpace(token); | 30 | var isLoggedIn = !string.IsNullOrWhiteSpace(token); |
| 31 | } | 31 | } |
| 32 | 32 | ||
| @@ -35,7 +35,6 @@ public partial class App : Application | @@ -35,7 +35,6 @@ public partial class App : Application | ||
| 35 | var token = await TokenStorage.LoadAsync(); | 35 | var token = await TokenStorage.LoadAsync(); |
| 36 | if (!string.IsNullOrWhiteSpace(token)) | 36 | if (!string.IsNullOrWhiteSpace(token)) |
| 37 | { | 37 | { |
| 38 | - ApiClient.SetBearer(token); | ||
| 39 | MainThread.BeginInvokeOnMainThread(() => | 38 | MainThread.BeginInvokeOnMainThread(() => |
| 40 | { | 39 | { |
| 41 | Current.MainPage = new AppShell(authed: true); // 显示:主页|日志|管理员 | 40 | Current.MainPage = new AppShell(authed: true); // 显示:主页|日志|管理员 |
Converters/InverseBoolConverter.cs
0 → 100644
| 1 | +// Converters/InverseBoolConverter.cs | ||
| 2 | +using System.Globalization; | ||
| 3 | +namespace IndustrialControl.Converters | ||
| 4 | +{ | ||
| 5 | + public sealed class InverseBoolConverter : IValueConverter | ||
| 6 | + { | ||
| 7 | + public object Convert(object value, Type t, object p, CultureInfo c) | ||
| 8 | + => value is bool b ? !b : value; | ||
| 9 | + public object ConvertBack(object value, Type t, object p, CultureInfo c) | ||
| 10 | + => value is bool b ? !b : value; | ||
| 11 | + } | ||
| 12 | +} |
Converters/StepConverters.cs
0 → 100644
| 1 | +using System.Globalization; | ||
| 2 | +namespace IndustrialControl.Converters; | ||
| 3 | +public class BoolToColorConverter : IValueConverter | ||
| 4 | +{ | ||
| 5 | + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) | ||
| 6 | + => value is bool b && b ? Color.FromArgb("#3D7AFF") : Colors.Transparent; | ||
| 7 | + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) | ||
| 8 | + => throw new NotImplementedException(); | ||
| 9 | +} | ||
| 10 | + | ||
| 11 | +public class PlusOneConverter : IValueConverter | ||
| 12 | +{ | ||
| 13 | + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) | ||
| 14 | + => value is int i ? (i + 1).ToString() : ""; | ||
| 15 | + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) | ||
| 16 | + => throw new NotImplementedException(); | ||
| 17 | +} | ||
| 18 | + | ||
| 19 | +public class ActiveToTextColorConverter : IValueConverter | ||
| 20 | +{ | ||
| 21 | + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) | ||
| 22 | + => value is bool b && b ? Colors.Black : Color.FromArgb("#6B7280"); | ||
| 23 | + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) | ||
| 24 | + => throw new NotImplementedException(); | ||
| 25 | +} | ||
| 26 | + | ||
| 27 | +public class ActiveToFontConverter : IValueConverter | ||
| 28 | +{ | ||
| 29 | + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) | ||
| 30 | + => value is bool b && b ? FontAttributes.Bold : FontAttributes.None; | ||
| 31 | + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) | ||
| 32 | + => throw new NotImplementedException(); | ||
| 33 | +} |
Converters/StepStatusToColorConverter.cs
0 → 100644
| 1 | +using System.Globalization; | ||
| 2 | + | ||
| 3 | +namespace IndustrialControl.Converters | ||
| 4 | +{ | ||
| 5 | + public sealed class StepStatusToColorConverter : IValueConverter | ||
| 6 | + { | ||
| 7 | + // "完成" -> 绿;"进行中" -> 蓝;其他/空 -> 灰 | ||
| 8 | + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) | ||
| 9 | + { | ||
| 10 | + var s = (value as string)?.Trim(); | ||
| 11 | + if (string.Equals(s, "完成", StringComparison.OrdinalIgnoreCase)) return Color.FromArgb("#4CAF50"); | ||
| 12 | + if (string.Equals(s, "进行中", StringComparison.OrdinalIgnoreCase)) return Color.FromArgb("#3F88F7"); | ||
| 13 | + return Color.FromArgb("#BFC6D4"); | ||
| 14 | + } | ||
| 15 | + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotSupportedException(); | ||
| 16 | + } | ||
| 17 | + | ||
| 18 | + public sealed class StepStatusToTextColorConverter : IValueConverter | ||
| 19 | + { | ||
| 20 | + // 圆点内部数字的前景色:深色(完成/进行中)- 白;未开始 - #607080 | ||
| 21 | + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) | ||
| 22 | + { | ||
| 23 | + var s = (value as string)?.Trim(); | ||
| 24 | + if (string.Equals(s, "完成", StringComparison.OrdinalIgnoreCase)) return Colors.White; | ||
| 25 | + if (string.Equals(s, "进行中", StringComparison.OrdinalIgnoreCase)) return Colors.White; | ||
| 26 | + return Color.FromArgb("#607080"); | ||
| 27 | + } | ||
| 28 | + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotSupportedException(); | ||
| 29 | + } | ||
| 30 | +} |
| @@ -83,7 +83,9 @@ | @@ -83,7 +83,9 @@ | ||
| 83 | <PackageReference Include="CommunityToolkit.Maui" Version="8.0.1" /> | 83 | <PackageReference Include="CommunityToolkit.Maui" Version="8.0.1" /> |
| 84 | <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" /> | 84 | <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" /> |
| 85 | </ItemGroup> | 85 | </ItemGroup> |
| 86 | - | 86 | + <ItemGroup> |
| 87 | + <PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" /> | ||
| 88 | + </ItemGroup> | ||
| 87 | 89 | ||
| 88 | <ItemGroup> | 90 | <ItemGroup> |
| 89 | <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.1" /> | 91 | <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.1" /> |
| @@ -2,6 +2,7 @@ | @@ -2,6 +2,7 @@ | ||
| 2 | using Microsoft.Extensions.DependencyInjection; | 2 | using Microsoft.Extensions.DependencyInjection; |
| 3 | using IndustrialControl.Services; | 3 | using IndustrialControl.Services; |
| 4 | using CommunityToolkit.Maui; | 4 | using CommunityToolkit.Maui; |
| 5 | +using IndustrialControl.ViewModels; | ||
| 5 | 6 | ||
| 6 | namespace IndustrialControl | 7 | namespace IndustrialControl |
| 7 | { | 8 | { |
| @@ -24,10 +25,11 @@ namespace IndustrialControl | @@ -24,10 +25,11 @@ namespace IndustrialControl | ||
| 24 | builder.Services.AddSingleton<AppShell>(); | 25 | builder.Services.AddSingleton<AppShell>(); |
| 25 | // 注册 ConfigLoader | 26 | // 注册 ConfigLoader |
| 26 | builder.Services.AddSingleton<IConfigLoader, ConfigLoader>(); | 27 | builder.Services.AddSingleton<IConfigLoader, ConfigLoader>(); |
| 27 | - builder.Services.AddSingleton<Services.LogService>(); | ||
| 28 | - builder.Services.AddSingleton<IWarehouseDataService, MockWarehouseDataService>(); | 28 | + builder.Services.AddSingleton<LogService>(); |
| 29 | + builder.Services.AddSingleton<IInboundMaterialService, InboundMaterialService>(); | ||
| 30 | + builder.Services.AddSingleton<IOutboundMaterialService, OutboundMaterialService>(); | ||
| 29 | builder.Services.AddSingleton<IDialogService, DialogService>(); | 31 | builder.Services.AddSingleton<IDialogService, DialogService>(); |
| 30 | - builder.Services.AddTransient<IndustrialControl.ViewModels.BinPickerViewModel>(); | 32 | + builder.Services.AddTransient<BinPickerViewModel>(); |
| 31 | 33 | ||
| 32 | // 扫码服务 | 34 | // 扫码服务 |
| 33 | builder.Services.AddSingleton<ScanService>(); | 35 | builder.Services.AddSingleton<ScanService>(); |
| @@ -66,11 +68,33 @@ namespace IndustrialControl | @@ -66,11 +68,33 @@ namespace IndustrialControl | ||
| 66 | builder.Services.AddTransient<Pages.OutboundFinishedSearchPage>(); | 68 | builder.Services.AddTransient<Pages.OutboundFinishedSearchPage>(); |
| 67 | builder.Services.AddTransient<Pages.WorkOrderSearchPage>(); | 69 | builder.Services.AddTransient<Pages.WorkOrderSearchPage>(); |
| 68 | builder.Services.AddTransient<Pages.MoldOutboundExecutePage>(); | 70 | builder.Services.AddTransient<Pages.MoldOutboundExecutePage>(); |
| 71 | + // 先注册配置加载器 | ||
| 72 | + builder.Services.AddSingleton<IConfigLoader, ConfigLoader>(); | ||
| 73 | + | ||
| 74 | + // 授权处理器 | ||
| 75 | + builder.Services.AddTransient<AuthHeaderHandler>(); | ||
| 76 | + | ||
| 77 | + builder.Services.AddHttpClient<IWorkOrderApi, WorkOrderApi>(ConfigureBaseAddress) | ||
| 78 | + .AddHttpMessageHandler<AuthHeaderHandler>(); | ||
| 69 | 79 | ||
| 80 | + builder.Services.AddHttpClient<IInboundMaterialService, InboundMaterialService>(ConfigureBaseAddress) | ||
| 81 | + .AddHttpMessageHandler<AuthHeaderHandler>(); | ||
| 82 | + builder.Services.AddHttpClient<IOutboundMaterialService, OutboundMaterialService>(ConfigureBaseAddress) | ||
| 83 | + .AddHttpMessageHandler<AuthHeaderHandler>(); | ||
| 70 | 84 | ||
| 71 | var app = builder.Build(); | 85 | var app = builder.Build(); |
| 72 | App.Services = app.Services; | 86 | App.Services = app.Services; |
| 73 | return app; | 87 | return app; |
| 74 | } | 88 | } |
| 89 | + | ||
| 90 | + private static void ConfigureBaseAddress(IServiceProvider sp, HttpClient http) | ||
| 91 | + { | ||
| 92 | + var cfg = sp.GetRequiredService<IConfigLoader>().Load(); | ||
| 93 | + var scheme = (string?)cfg?["server"]?["scheme"] ?? "http"; | ||
| 94 | + var ip = (string?)cfg?["server"]?["ipAddress"] ?? "127.0.0.1"; | ||
| 95 | + var port = (int?)cfg?["server"]?["port"] ?? 80; | ||
| 96 | + var baseUrl = port is > 0 and < 65536 ? $"{scheme}://{ip}:{port}" : $"{scheme}://{ip}"; | ||
| 97 | + http.BaseAddress = new Uri(baseUrl); | ||
| 98 | + } | ||
| 75 | } | 99 | } |
| 76 | } | 100 | } |
| 1 | namespace IndustrialControl.ViewModels; | 1 | namespace IndustrialControl.ViewModels; |
| 2 | 2 | ||
| 3 | +// Models/WorkOrderDto.cs (文件路径随你工程) | ||
| 3 | public class WorkOrderDto | 4 | public class WorkOrderDto |
| 4 | { | 5 | { |
| 6 | + public string Id { get; set; } = ""; | ||
| 5 | public string OrderNo { get; set; } = ""; | 7 | public string OrderNo { get; set; } = ""; |
| 6 | public string OrderName { get; set; } = ""; | 8 | public string OrderName { get; set; } = ""; |
| 9 | + | ||
| 10 | + public string MaterialCode { get; set; } = ""; | ||
| 11 | + public string MaterialName { get; set; } = ""; | ||
| 12 | + public string LineName { get; set; } = ""; | ||
| 13 | + | ||
| 14 | + /// <summary>中文状态:待执行 / 执行中 / 入库中 / 已完成</summary> | ||
| 7 | public string Status { get; set; } = ""; | 15 | public string Status { get; set; } = ""; |
| 8 | - public string ProductName { get; set; } = ""; | ||
| 9 | - public int Quantity { get; set; } | ||
| 10 | - public DateTime CreateDate { get; set; } | 16 | + |
| 17 | + /// <summary>创建时间(已格式化字符串)</summary> | ||
| 18 | + public string CreateDate { get; set; } = ""; | ||
| 19 | + | ||
| 20 | + public string Urgent { get; set; } = ""; | ||
| 21 | + public int? CurQty { get; set; } | ||
| 22 | + public string? BomCode { get; set; } | ||
| 23 | + public string? RouteName { get; set; } | ||
| 24 | + public string? WorkShopName { get; set; } | ||
| 11 | } | 25 | } |
| 26 | + | ||
| 12 | public class WorkOrderSummary | 27 | public class WorkOrderSummary |
| 13 | { | 28 | { |
| 14 | public string OrderNo { get; set; } = ""; | 29 | public string OrderNo { get; set; } = ""; |
| @@ -19,3 +34,46 @@ public class WorkOrderSummary | @@ -19,3 +34,46 @@ public class WorkOrderSummary | ||
| 19 | public DateTime CreateDate { get; set; } | 34 | public DateTime CreateDate { get; set; } |
| 20 | } | 35 | } |
| 21 | 36 | ||
| 37 | +// 服务层 DTO(已把数量转成 int,便于前端直接用) | ||
| 38 | +public record InboundPendingRow( | ||
| 39 | + string? Barcode, | ||
| 40 | + string? DetailId, | ||
| 41 | + string? Location, | ||
| 42 | + string? MaterialName, | ||
| 43 | + int PendingQty, // instockQty | ||
| 44 | + int ScannedQty, // qty | ||
| 45 | + string? Spec); | ||
| 46 | + | ||
| 47 | +public record InboundScannedRow( | ||
| 48 | + string Barcode, | ||
| 49 | + string DetailId, | ||
| 50 | + string Location, | ||
| 51 | + string MaterialName, | ||
| 52 | + int Qty, | ||
| 53 | + string Spec, | ||
| 54 | + bool ScanStatus, | ||
| 55 | + string? WarehouseCode | ||
| 56 | + ); | ||
| 57 | + | ||
| 58 | + | ||
| 59 | + | ||
| 60 | + | ||
| 61 | +public record OutboundPendingRow( | ||
| 62 | + string? Barcode, | ||
| 63 | + string? DetailId, | ||
| 64 | + string? Location, | ||
| 65 | + string? MaterialName, | ||
| 66 | + int PendingQty, // instockQty | ||
| 67 | + int ScannedQty, // qty | ||
| 68 | + string? Spec); | ||
| 69 | + | ||
| 70 | +public record OutboundScannedRow( | ||
| 71 | + string Barcode, | ||
| 72 | + string DetailId, | ||
| 73 | + string Location, | ||
| 74 | + string MaterialName, | ||
| 75 | + int Qty, | ||
| 76 | + string Spec, | ||
| 77 | + bool ScanStatus, | ||
| 78 | + string? WarehouseCode | ||
| 79 | + ); |
| 1 | <?xml version="1.0" encoding="utf-8" ?> | 1 | <?xml version="1.0" encoding="utf-8" ?> |
| 2 | -<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" | ||
| 3 | - xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" | ||
| 4 | - x:Class="IndustrialControl.Pages.MoldOutboundExecutePage" | ||
| 5 | - Title="出库执行"> | ||
| 6 | - | 2 | +<ContentPage |
| 3 | + x:Class="IndustrialControl.Pages.MoldOutboundExecutePage" | ||
| 4 | + xmlns="http://schemas.microsoft.com/dotnet/2021/maui" | ||
| 5 | + xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" | ||
| 6 | + xmlns:conv="clr-namespace:IndustrialControl.Converters" | ||
| 7 | + Title="模具出库执行"> | ||
| 7 | <ContentPage.Resources> | 8 | <ContentPage.Resources> |
| 8 | - <!-- 表头与行样式 --> | ||
| 9 | - <Style TargetType="Grid" x:Key="TableHeader"> | ||
| 10 | - <Setter Property="BackgroundColor" Value="#F5F7FA"/> | ||
| 11 | - <Setter Property="Padding" Value="8,6"/> | ||
| 12 | - <Setter Property="ColumnSpacing" Value="8"/> | ||
| 13 | - </Style> | ||
| 14 | - <Style TargetType="Grid" x:Key="TableRow"> | ||
| 15 | - <Setter Property="Padding" Value="8,6"/> | ||
| 16 | - <Setter Property="ColumnSpacing" Value="8"/> | ||
| 17 | - </Style> | 9 | + <ResourceDictionary> |
| 10 | + <!-- 转换器 --> | ||
| 11 | + <conv:InverseBoolConverter x:Key="InverseBoolConverter" /> | ||
| 12 | + | ||
| 13 | + <!-- 颜色 --> | ||
| 14 | + <Color x:Key="ClrBlue">#3F88F7</Color> | ||
| 15 | + <Color x:Key="ClrGreen">#4CAF50</Color> | ||
| 16 | + <Color x:Key="ClrGray">#BFC6D4</Color> | ||
| 17 | + <Color x:Key="ClrLine">#E0E6EF</Color> | ||
| 18 | + <Color x:Key="ClrTextSub">#718096</Color> | ||
| 19 | + | ||
| 20 | + <!-- 卡片样式 --> | ||
| 21 | + <Style TargetType="Frame" x:Key="Card"> | ||
| 22 | + <Setter Property="HasShadow" Value="False" /> | ||
| 23 | + <Setter Property="BorderColor" Value="#E5EAF2" /> | ||
| 24 | + <Setter Property="Padding" Value="12" /> | ||
| 25 | + <Setter Property="Margin" Value="0,10,0,0" /> | ||
| 26 | + </Style> | ||
| 27 | + | ||
| 28 | + <Style TargetType="Label" x:Key="SectionTitle"> | ||
| 29 | + <Setter Property="FontAttributes" Value="Bold" /> | ||
| 30 | + <Setter Property="FontSize" Value="16" /> | ||
| 31 | + <Setter Property="TextColor" Value="#1F2D3D" /> | ||
| 32 | + <Setter Property="Margin" Value="0,0,0,8" /> | ||
| 33 | + </Style> | ||
| 34 | + </ResourceDictionary> | ||
| 18 | </ContentPage.Resources> | 35 | </ContentPage.Resources> |
| 19 | 36 | ||
| 20 | - <Grid RowDefinitions="Auto,*,Auto"> | ||
| 21 | - | ||
| 22 | - <!-- 顶部蓝条 --> | ||
| 23 | - <Grid Grid.Row="0" BackgroundColor="#007BFF" HeightRequest="56" Padding="16,0"> | ||
| 24 | - <Label Text="仓储管理系统" | ||
| 25 | - VerticalOptions="Center" | ||
| 26 | - TextColor="White" FontSize="18" FontAttributes="Bold"/> | ||
| 27 | - </Grid> | ||
| 28 | - | ||
| 29 | - <!-- 主体 --> | ||
| 30 | - <ScrollView Grid.Row="1"> | ||
| 31 | - <VerticalStackLayout Padding="12" Spacing="12"> | 37 | + <ScrollView> |
| 38 | + <Grid Padding="14" RowDefinitions="Auto,Auto,Auto,*,Auto"> | ||
| 39 | + | ||
| 40 | + <!-- 工单基础信息(标题 + 顶部流程进度) --> | ||
| 41 | + <Frame Grid.Row="0" Style="{StaticResource Card}"> | ||
| 42 | + <VerticalStackLayout Spacing="10"> | ||
| 43 | + | ||
| 44 | + <Label Text="工单基础信息" Style="{StaticResource SectionTitle}" /> | ||
| 45 | + | ||
| 46 | + <!-- 顶部流程进度:按 VM 的 IsActive/IsDone 高亮;Time 显示日期 --> | ||
| 47 | + <CollectionView ItemsSource="{Binding WorkflowSteps}" SelectionMode="None"> | ||
| 48 | + <CollectionView.ItemsLayout> | ||
| 49 | + <LinearItemsLayout Orientation="Horizontal" ItemSpacing="26"/> | ||
| 50 | + </CollectionView.ItemsLayout> | ||
| 51 | + | ||
| 52 | + <CollectionView.ItemTemplate> | ||
| 53 | + <DataTemplate> | ||
| 54 | + <!-- 三列:左连线 | 圆点块 | 右连线;三行:圆点/标题/日期 --> | ||
| 55 | + <Grid ColumnDefinitions="*,Auto,*" RowDefinitions="Auto,Auto,Auto" Padding="0,0,8,0" WidthRequest="120"> | ||
| 56 | + | ||
| 57 | + <!-- 左侧连接线(首节点隐藏;颜色随状态变) --> | ||
| 58 | + <BoxView x:Name="leftLine" Grid.Column="0" Grid.Row="0" | ||
| 59 | + HeightRequest="2" VerticalOptions="Center" Margin="0,13,8,0" | ||
| 60 | + Color="#E0E6EF" IsVisible="{Binding IsFirst, Converter={StaticResource InverseBoolConverter}}"> | ||
| 61 | + <BoxView.Triggers> | ||
| 62 | + <DataTrigger TargetType="BoxView" Binding="{Binding IsDone}" Value="True"> | ||
| 63 | + <Setter Property="Color" Value="#4CAF50"/> | ||
| 64 | + </DataTrigger> | ||
| 65 | + <DataTrigger TargetType="BoxView" Binding="{Binding IsActive}" Value="True"> | ||
| 66 | + <Setter Property="Color" Value="#3F88F7"/> | ||
| 67 | + </DataTrigger> | ||
| 68 | + </BoxView.Triggers> | ||
| 69 | + </BoxView> | ||
| 70 | + | ||
| 71 | + <!-- 圆点(中列) --> | ||
| 72 | + <Grid Grid.Column="1" Grid.Row="0" WidthRequest="26" HeightRequest="26"> | ||
| 73 | + <Ellipse Fill="#BFC6D4" Stroke="#E0E6EF" StrokeThickness="2"/> | ||
| 74 | + <Ellipse StrokeThickness="0"> | ||
| 75 | + <Ellipse.Triggers> | ||
| 76 | + <DataTrigger TargetType="Ellipse" Binding="{Binding IsActive}" Value="True"> | ||
| 77 | + <Setter Property="Fill" Value="#3F88F7"/> | ||
| 78 | + </DataTrigger> | ||
| 79 | + <DataTrigger TargetType="Ellipse" Binding="{Binding IsDone}" Value="True"> | ||
| 80 | + <Setter Property="Fill" Value="#4CAF50"/> | ||
| 81 | + </DataTrigger> | ||
| 82 | + </Ellipse.Triggers> | ||
| 83 | + </Ellipse> | ||
| 84 | + <!-- 圆内文本:已完成=✔;其余显示序号 --> | ||
| 85 | + <Label FontSize="14" HorizontalOptions="Center" VerticalOptions="Center" TextColor="White"> | ||
| 86 | + <Label.Triggers> | ||
| 87 | + <DataTrigger TargetType="Label" Binding="{Binding IsDone}" Value="True"> | ||
| 88 | + <Setter Property="Text" Value="✔"/> | ||
| 89 | + </DataTrigger> | ||
| 90 | + <DataTrigger TargetType="Label" Binding="{Binding IsDone}" Value="False"> | ||
| 91 | + <Setter Property="Text" Value="{Binding Index}"/> | ||
| 92 | + </DataTrigger> | ||
| 93 | + </Label.Triggers> | ||
| 94 | + </Label> | ||
| 95 | + </Grid> | ||
| 96 | + | ||
| 97 | + <!-- 右侧连接线(末节点隐藏,保持灰色) --> | ||
| 98 | + <BoxView Grid.Column="2" Grid.Row="0" | ||
| 99 | + HeightRequest="2" VerticalOptions="Center" Margin="8,13,0,0" | ||
| 100 | + Color="#E0E6EF" IsVisible="{Binding IsLast, Converter={StaticResource InverseBoolConverter}}"/> | ||
| 101 | + | ||
| 102 | + <!-- 标题 --> | ||
| 103 | + <Label Grid.Column="0" Grid.ColumnSpan="3" Grid.Row="1" | ||
| 104 | + Margin="0,6,0,0" | ||
| 105 | + FontSize="12" | ||
| 106 | + HorizontalTextAlignment="Center" | ||
| 107 | + Text="{Binding Title}" /> | ||
| 108 | + | ||
| 109 | + <!-- 日期(确保用 StringFormat;Time 为 null 时自然不显示文字) --> | ||
| 110 | + <Label Grid.Column="0" Grid.ColumnSpan="3" Grid.Row="2" | ||
| 111 | + FontSize="11" | ||
| 112 | + HorizontalTextAlignment="Center" | ||
| 113 | + Text="{Binding Time}"/> | ||
| 114 | + </Grid> | ||
| 115 | + </DataTemplate> | ||
| 116 | + </CollectionView.ItemTemplate> | ||
| 117 | + </CollectionView> | ||
| 118 | + | ||
| 119 | + </VerticalStackLayout> | ||
| 120 | + </Frame> | ||
| 121 | + | ||
| 122 | + <!-- 工单基础信息表格(来自搜索页 BaseInfos) --> | ||
| 123 | + <!-- 工单基础信息表格(固定字段) --> | ||
| 124 | + <Frame Grid.Row="1" Style="{StaticResource Card}"> | ||
| 125 | + <Grid ColumnDefinitions="80,100,80,80" | ||
| 126 | + RowDefinitions="Auto,Auto,Auto,Auto,Auto" | ||
| 127 | + RowSpacing="6" ColumnSpacing="8"> | ||
| 128 | + | ||
| 129 | + <!-- 第1行 --> | ||
| 130 | + <Label Grid.Row="0" Grid.Column="0" Text="工单编号:" FontAttributes="Bold"/> | ||
| 131 | + <Label Grid.Row="0" Grid.Column="1" Text="{Binding OrderNo}" LineBreakMode="TailTruncation"/> | ||
| 132 | + | ||
| 133 | + <Label Grid.Row="0" Grid.Column="2" Text="状态:" FontAttributes="Bold"/> | ||
| 134 | + <Label Grid.Row="0" Grid.Column="3" Text="{Binding StatusText}" LineBreakMode="TailTruncation"/> | ||
| 135 | + | ||
| 136 | + <!-- 第2行 --> | ||
| 137 | + <Label Grid.Row="1" Grid.Column="0" Text="工单名称:" FontAttributes="Bold"/> | ||
| 138 | + <Label Grid.Row="1" Grid.Column="1" Text="{Binding OrderName}" LineBreakMode="TailTruncation"/> | ||
| 139 | + | ||
| 140 | + <Label Grid.Row="1" Grid.Column="2" Text="优先级:" FontAttributes="Bold"/> | ||
| 141 | + <Label Grid.Row="1" Grid.Column="3" Text="{Binding Urgent}" LineBreakMode="TailTruncation"/> | ||
| 142 | + | ||
| 143 | + <!-- 第3行 --> | ||
| 144 | + <Label Grid.Row="2" Grid.Column="0" Text="产品名称:" FontAttributes="Bold"/> | ||
| 145 | + <Label Grid.Row="2" Grid.Column="1" Text="{Binding ProductName}" LineBreakMode="TailTruncation"/> | ||
| 146 | + | ||
| 147 | + <Label Grid.Row="2" Grid.Column="2" Text="生产数量:" FontAttributes="Bold"/> | ||
| 148 | + <Label Grid.Row="2" Grid.Column="3" Text="{Binding PlanQtyText}" /> | ||
| 149 | + | ||
| 150 | + <!-- 第4行 --> | ||
| 151 | + <Label Grid.Row="3" Grid.Column="0" Text="计划开始日期:" FontAttributes="Bold"/> | ||
| 152 | + <Label Grid.Row="3" Grid.Column="1" Text="{Binding PlanStartText}" /> | ||
| 153 | + | ||
| 154 | + <Label Grid.Row="3" Grid.Column="2" Text="创建日期:" FontAttributes="Bold"/> | ||
| 155 | + <Label Grid.Row="3" Grid.Column="3" Text="{Binding CreateDateText}" /> | ||
| 156 | + | ||
| 157 | + <!-- 第5行 --> | ||
| 158 | + <Label Grid.Row="4" Grid.Column="0" Text="BOM编号:" FontAttributes="Bold"/> | ||
| 159 | + <Label Grid.Row="4" Grid.Column="1" Text="{Binding BomCode}" LineBreakMode="TailTruncation"/> | ||
| 160 | + | ||
| 161 | + <Label Grid.Row="4" Grid.Column="2" Text="工艺路线名称:" FontAttributes="Bold"/> | ||
| 162 | + <Label Grid.Row="4" Grid.Column="3" Text="{Binding RouteName}" LineBreakMode="TailTruncation"/> | ||
| 32 | 163 | ||
| 33 | - <!-- 模具编码 --> | ||
| 34 | - <Grid ColumnDefinitions="Auto,*" ColumnSpacing="12"> | ||
| 35 | - <Label Text="模具编码" VerticalTextAlignment="Center" /> | ||
| 36 | - <Entry x:Name="MoldEntry" | ||
| 37 | - Grid.Column="1" | ||
| 38 | - Placeholder="请扫描模具或输入具编码" | ||
| 39 | - Completed="OnMoldCompleted" /> | ||
| 40 | - | ||
| 41 | - </Grid> | ||
| 42 | - | ||
| 43 | - <!-- 需求基础信息 --> | ||
| 44 | - <Label Text="需求基础信息" FontAttributes="Bold" /> | ||
| 45 | - <Frame Padding="0" HasShadow="False" BorderColor="#E0E6EF"> | ||
| 46 | - <VerticalStackLayout Spacing="0"> | ||
| 47 | - <!-- 顶部两列 --> | ||
| 48 | - <Grid ColumnDefinitions="*,*" Style="{StaticResource TableHeader}"> | ||
| 49 | - <Grid ColumnDefinitions="Auto,*"> | ||
| 50 | - <Label Text="工单号:" /> | ||
| 51 | - <Label Grid.Column="1" Text="{Binding OrderNo}" /> | ||
| 52 | - </Grid> | ||
| 53 | - <Grid Grid.Column="1" ColumnDefinitions="Auto,*"> | ||
| 54 | - <Label Text="产品名称:" /> | ||
| 55 | - <Label Grid.Column="1" Text="XXXXX" /> | ||
| 56 | - <!-- 需要时可绑定到 VM 的 ProductName --> | ||
| 57 | - </Grid> | ||
| 58 | - </Grid> | ||
| 59 | - | ||
| 60 | - <!-- 模具型号表(示例样式;后续可替换为绑定集合) --> | ||
| 61 | - <Grid Padding="8,6" ColumnDefinitions="Auto,*" BackgroundColor="#FFFFFF"> | ||
| 62 | - <Label Text="序号" FontAttributes="Bold"/> | ||
| 63 | - <Label Grid.Column="1" Text="模具型号" FontAttributes="Bold"/> | ||
| 64 | - </Grid> | ||
| 65 | - <Grid Padding="8,6" ColumnDefinitions="Auto,*"> | ||
| 66 | - <Label Text="1"/> | ||
| 67 | - <Label Grid.Column="1" Text="MU_DHJD_01"/> | ||
| 68 | - </Grid> | ||
| 69 | - <Grid Padding="8,6" ColumnDefinitions="Auto,*"> | ||
| 70 | - <Label Text="2"/> | ||
| 71 | - <Label Grid.Column="1" Text="MU_DHJD_02"/> | ||
| 72 | - </Grid> | ||
| 73 | - </VerticalStackLayout> | ||
| 74 | - </Frame> | ||
| 75 | - | ||
| 76 | - <!-- 扫描明细 --> | ||
| 77 | - <Label Text="扫描明细" TextColor="#17A673" FontAttributes="Bold" /> | ||
| 78 | - | ||
| 79 | - <!-- 表头 --> | ||
| 80 | - <Grid ColumnDefinitions="40,Auto,*,*,Auto,*" | ||
| 81 | - Style="{StaticResource TableHeader}"> | ||
| 82 | - <Label Text="#" /> | ||
| 83 | - <Label Grid.Column="1" Text="选择" /> | ||
| 84 | - <Label Grid.Column="2" Text="模具编码" /> | ||
| 85 | - <Label Grid.Column="3" Text="模具型号" /> | ||
| 86 | - <Label Grid.Column="4" Text="出库数量" /> | ||
| 87 | - <Label Grid.Column="5" Text="出库库位" /> | ||
| 88 | </Grid> | 164 | </Grid> |
| 165 | + </Frame> | ||
| 166 | + | ||
| 167 | + | ||
| 168 | + <!-- 工序任务进度(竖向时间轴) --> | ||
| 169 | + <Frame Grid.Row="3" Style="{StaticResource Card}"> | ||
| 170 | + <VerticalStackLayout Spacing="8"> | ||
| 171 | + <Label Text="工序任务进度" Style="{StaticResource SectionTitle}" /> | ||
| 172 | + | ||
| 173 | + <CollectionView ItemsSource="{Binding ProcessTasks}" SelectionMode="None"> | ||
| 174 | + <CollectionView.ItemTemplate> | ||
| 175 | + <DataTemplate> | ||
| 176 | + <!-- 左 40:圆点 + 竖线;右:名称 + 时间 + 状态 --> | ||
| 177 | + <Grid ColumnDefinitions="40,*" Padding="0,8"> | ||
| 178 | + | ||
| 179 | + <!-- 左侧:圆点 + 竖线 --> | ||
| 180 | + <Grid RowDefinitions="Auto,Auto" WidthRequest="40"> | ||
| 181 | + <Grid WidthRequest="26" HeightRequest="26" HorizontalOptions="Center"> | ||
| 182 | + <Ellipse Stroke="{StaticResource ClrLine}" StrokeThickness="2" /> | ||
| 183 | + <Ellipse StrokeThickness="0" x:Name="dotFill" Fill="{StaticResource ClrGray}"/> | ||
| 184 | + <Label Text="{Binding Index}" FontSize="12" TextColor="White" | ||
| 185 | + HorizontalOptions="Center" VerticalOptions="Center"/> | ||
| 186 | + </Grid> | ||
| 187 | + <BoxView Grid.Row="1" WidthRequest="2" HorizontalOptions="Center" | ||
| 188 | + Color="{StaticResource ClrLine}" HeightRequest="36"/> | ||
| 189 | + </Grid> | ||
| 190 | + | ||
| 191 | + <!-- 右侧内容 --> | ||
| 192 | + <Grid Grid.Column="1" RowDefinitions="Auto,Auto"> | ||
| 193 | + <Label Grid.Row="0" Text="{Binding Name}" FontAttributes="Bold" FontSize="14"/> | ||
| 194 | + | ||
| 195 | + <HorizontalStackLayout Grid.Row="1" Spacing="12"> | ||
| 196 | + <Label Text="{Binding Start, StringFormat='{}{0:yyyy-MM-dd}'}" FontSize="12" TextColor="{StaticResource ClrTextSub}"/> | ||
| 197 | + <Label Text="→" FontSize="12" TextColor="{StaticResource ClrTextSub}"/> | ||
| 198 | + <Label Text="{Binding End, StringFormat='{}{0:yyyy-MM-dd}'}" FontSize="12" TextColor="{StaticResource ClrTextSub}"/> | ||
| 199 | + <Label x:Name="statusLabel" Text="{Binding StatusText}" FontSize="12" /> | ||
| 200 | + </HorizontalStackLayout> | ||
| 201 | + </Grid> | ||
| 202 | + | ||
| 203 | + <!-- 根据 StatusText 着色:完成=绿,进行中=蓝,其它=灰 --> | ||
| 204 | + <Grid.Triggers> | ||
| 205 | + <DataTrigger TargetType="Grid" Binding="{Binding StatusText}" Value="完成"> | ||
| 206 | + <Setter TargetName="dotFill" Property="Ellipse.Fill" Value="{StaticResource ClrGreen}" /> | ||
| 207 | + <Setter TargetName="statusLabel" Property="Label.TextColor" Value="{StaticResource ClrGreen}" /> | ||
| 208 | + </DataTrigger> | ||
| 209 | + <DataTrigger TargetType="Grid" Binding="{Binding StatusText}" Value="进行中"> | ||
| 210 | + <Setter TargetName="dotFill" Property="Ellipse.Fill" Value="{StaticResource ClrBlue}" /> | ||
| 211 | + <Setter TargetName="statusLabel" Property="Label.TextColor" Value="{StaticResource ClrBlue}" /> | ||
| 212 | + </DataTrigger> | ||
| 213 | + <DataTrigger TargetType="Grid" Binding="{Binding StatusText}" Value="未开始"> | ||
| 214 | + <Setter TargetName="dotFill" Property="Ellipse.Fill" Value="{StaticResource ClrGray}" /> | ||
| 215 | + <Setter TargetName="statusLabel" Property="Label.TextColor" Value="{StaticResource ClrGray}" /> | ||
| 216 | + </DataTrigger> | ||
| 217 | + </Grid.Triggers> | ||
| 218 | + | ||
| 219 | + </Grid> | ||
| 220 | + </DataTemplate> | ||
| 221 | + </CollectionView.ItemTemplate> | ||
| 222 | + </CollectionView> | ||
| 223 | + | ||
| 224 | + </VerticalStackLayout> | ||
| 225 | + </Frame> | ||
| 226 | + | ||
| 227 | + <!-- 底部按钮 --> | ||
| 228 | + <Grid Grid.Row="4" ColumnDefinitions="*,*" ColumnSpacing="12" Margin="0,10,0,20"> | ||
| 229 | + <Button Text="取消" Command="{Binding CancelScanCommand}" /> | ||
| 230 | + <Button Grid.Column="1" Text="确认出库" Command="{Binding ConfirmCommand}" /> | ||
| 231 | + </Grid> | ||
| 89 | 232 | ||
| 90 | - <!-- 明细列表 --> | ||
| 91 | - <CollectionView ItemsSource="{Binding ScanDetails}"> | ||
| 92 | - <CollectionView.ItemTemplate> | ||
| 93 | - <DataTemplate> | ||
| 94 | - <Grid ColumnDefinitions="40,Auto,*,*,Auto,*" | ||
| 95 | - Style="{StaticResource TableRow}"> | ||
| 96 | - <!-- 背景根据 Selected 高亮为绿色 --> | ||
| 97 | - <Grid.Triggers> | ||
| 98 | - <DataTrigger TargetType="Grid" Binding="{Binding Selected}" Value="True"> | ||
| 99 | - <Setter Property="BackgroundColor" Value="#6EF593"/> | ||
| 100 | - </DataTrigger> | ||
| 101 | - </Grid.Triggers> | ||
| 102 | - | ||
| 103 | - <Label Text="{Binding Index}" /> | ||
| 104 | - <CheckBox Grid.Column="1" IsChecked="{Binding Selected, Mode=TwoWay}" /> | ||
| 105 | - <Label Grid.Column="2" Text="{Binding MoldCode}" /> | ||
| 106 | - <Label Grid.Column="3" Text="{Binding MoldModel}" /> | ||
| 107 | - <Label Grid.Column="4" Text="{Binding Qty}" HorizontalTextAlignment="Center" /> | ||
| 108 | - <Label Grid.Column="5" Text="{Binding Bin}" /> | ||
| 109 | - </Grid> | ||
| 110 | - </DataTemplate> | ||
| 111 | - </CollectionView.ItemTemplate> | ||
| 112 | - </CollectionView> | ||
| 113 | - | ||
| 114 | - </VerticalStackLayout> | ||
| 115 | - </ScrollView> | ||
| 116 | - | ||
| 117 | - <!-- 底部操作条 --> | ||
| 118 | - <Grid Grid.Row="2" BackgroundColor="White" Padding="16,8" ColumnDefinitions="*,Auto,*"> | ||
| 119 | - <!-- 取消扫描 --> | ||
| 120 | - <Button Grid.Column="0" | ||
| 121 | - Text="取消扫描" | ||
| 122 | - TextColor="#C62828" | ||
| 123 | - BorderColor="#C62828" | ||
| 124 | - BorderWidth="1" | ||
| 125 | - CornerRadius="8" | ||
| 126 | - Command="{Binding CancelScanCommand}" /> | ||
| 127 | - | ||
| 128 | - <!-- 占位间距 --> | ||
| 129 | - <BoxView Grid.Column="1" WidthRequest="16" /> | ||
| 130 | - | ||
| 131 | - <!-- 确认出库 --> | ||
| 132 | - <Button Grid.Column="2" | ||
| 133 | - Text="确认出库" | ||
| 134 | - BackgroundColor="#0EA5E9" | ||
| 135 | - TextColor="White" | ||
| 136 | - CornerRadius="8" | ||
| 137 | - Command="{Binding ConfirmCommand}" /> | ||
| 138 | </Grid> | 233 | </Grid> |
| 139 | - | ||
| 140 | - </Grid> | 234 | + </ScrollView> |
| 141 | </ContentPage> | 235 | </ContentPage> |
| 1 | -using IndustrialControl.ViewModels; | ||
| 2 | -using IndustrialControl.Services; // ✅ 引入扫描服务 | 1 | +using System.Text.Json; |
| 2 | +using IndustrialControl.ViewModels; | ||
| 3 | 3 | ||
| 4 | namespace IndustrialControl.Pages; | 4 | namespace IndustrialControl.Pages; |
| 5 | 5 | ||
| 6 | -[QueryProperty(nameof(OrderNo), "orderNo")] | ||
| 7 | -public partial class MoldOutboundExecutePage : ContentPage | 6 | +// 注意:不再使用 [QueryProperty],改用 IQueryAttributable |
| 7 | +public partial class MoldOutboundExecutePage : ContentPage, IQueryAttributable | ||
| 8 | { | 8 | { |
| 9 | - public string? OrderNo { get; set; } // Shell QueryProperty 接收 | ||
| 10 | - | ||
| 11 | private readonly MoldOutboundExecuteViewModel _vm; | 9 | private readonly MoldOutboundExecuteViewModel _vm; |
| 12 | - private readonly ScanService _scanSvc; // ✅ 扫描服务 | ||
| 13 | 10 | ||
| 14 | - public MoldOutboundExecutePage(MoldOutboundExecuteViewModel vm, ScanService scanSvc) | 11 | + // 这三个由搜索页传入 |
| 12 | + private string? _orderNo; | ||
| 13 | + private string? _orderId; | ||
| 14 | + private List<BaseInfoItem>? _baseInfos; | ||
| 15 | + | ||
| 16 | + public MoldOutboundExecutePage(MoldOutboundExecuteViewModel vm) | ||
| 15 | { | 17 | { |
| 16 | InitializeComponent(); | 18 | InitializeComponent(); |
| 17 | BindingContext = _vm = vm; | 19 | BindingContext = _vm = vm; |
| 18 | - _scanSvc = scanSvc; | ||
| 19 | - | ||
| 20 | - // 与项目里其它页保持一致的扫码配置 | ||
| 21 | - _scanSvc.Prefix = null; | ||
| 22 | - _scanSvc.Suffix = null; | ||
| 23 | - _scanSvc.DebounceMs = 0; | ||
| 24 | } | 20 | } |
| 25 | 21 | ||
| 26 | - protected override async void OnNavigatedTo(NavigatedToEventArgs args) | 22 | + // Shell 在导航时会调用这里,参数肯定已到 |
| 23 | + public void ApplyQueryAttributes(IDictionary<string, object> query) | ||
| 27 | { | 24 | { |
| 28 | - base.OnNavigatedTo(args); | ||
| 29 | - if (!string.IsNullOrWhiteSpace(OrderNo)) | 25 | + // 1) 优先:整条 WorkOrderDto(JSON 传参) |
| 26 | + if (query.TryGetValue("orderDto", out var obj) && obj is string json && !string.IsNullOrWhiteSpace(json)) | ||
| 30 | { | 27 | { |
| 31 | - await _vm.LoadAsync(OrderNo); // 维持你现有逻辑 | 28 | + var dto = JsonSerializer.Deserialize<WorkOrderDto>(json, |
| 29 | + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); | ||
| 30 | + if (dto != null) | ||
| 31 | + { | ||
| 32 | + _vm.OrderNo = dto.OrderNo; | ||
| 33 | + _vm.OrderId = dto.Id; | ||
| 34 | + _vm.StatusText = dto.Status; | ||
| 35 | + _vm.OrderName = dto.OrderName; | ||
| 36 | + _vm.Urgent = dto.Urgent; | ||
| 37 | + _vm.ProductName = dto.MaterialName; | ||
| 38 | + _vm.PlanQtyText = dto.CurQty?.ToString(); | ||
| 39 | + _vm.CreateDateText = dto.CreateDate; | ||
| 40 | + _vm.BomCode = dto.BomCode; | ||
| 41 | + _vm.RouteName = dto.RouteName; | ||
| 42 | + return; // 已就绪 | ||
| 43 | + } | ||
| 32 | } | 44 | } |
| 33 | - } | ||
| 34 | 45 | ||
| 35 | - protected override void OnAppearing() | ||
| 36 | - { | ||
| 37 | - base.OnAppearing(); | ||
| 38 | - // ✅ 注册扫描回调并开始监听 | ||
| 39 | - _scanSvc.Scanned += OnScanned; | ||
| 40 | - _scanSvc.StartListening(); | 46 | + // 2) 兼容:旧的 orderNo/orderId/baseInfo 三参数 |
| 47 | + if (query.TryGetValue("orderNo", out var ono)) _vm.OrderNo = ono as string; | ||
| 48 | + if (query.TryGetValue("orderId", out var oid)) _vm.OrderId = oid as string; | ||
| 41 | 49 | ||
| 42 | - // ✅ 让“模具编码”输入框也能接收键盘/扫描枪字符 | ||
| 43 | - _scanSvc.Attach(MoldEntry); | ||
| 44 | - MoldEntry.Focus(); | 50 | + if (query.TryGetValue("baseInfo", out var bi) && bi is string baseInfoJson && !string.IsNullOrWhiteSpace(baseInfoJson)) |
| 51 | + { | ||
| 52 | + try | ||
| 53 | + { | ||
| 54 | + var items = JsonSerializer.Deserialize<List<BaseInfoItem>>(baseInfoJson); | ||
| 55 | + if (items != null) | ||
| 56 | + { | ||
| 57 | + // 让 VM 把 BaseInfos 落到固定属性上(见步骤 B) | ||
| 58 | + _vm.SetFixedFieldsFromBaseInfos(items); | ||
| 59 | + } | ||
| 60 | + } | ||
| 61 | + catch (Exception ex) | ||
| 62 | + { | ||
| 63 | + System.Diagnostics.Debug.WriteLine("[Execute] baseInfo JSON parse error: " + ex); | ||
| 64 | + } | ||
| 65 | + } | ||
| 45 | } | 66 | } |
| 46 | 67 | ||
| 47 | - protected override void OnDisappearing() | ||
| 48 | - { | ||
| 49 | - // ✅ 页面离开时及时注销,避免多个页面抢占扫码 | ||
| 50 | - _scanSvc.Scanned -= OnScanned; | ||
| 51 | - _scanSvc.StopListening(); | ||
| 52 | - base.OnDisappearing(); | ||
| 53 | - } | ||
| 54 | 68 | ||
| 55 | - // ✅ 硬件/广播 扫到码 | ||
| 56 | - private void OnScanned(string data, string type) | 69 | + protected override async void OnAppearing() |
| 57 | { | 70 | { |
| 58 | - MainThread.BeginInvokeOnMainThread(async () => | ||
| 59 | - { | ||
| 60 | - // 1) 回填到输入框(UI 可见) | ||
| 61 | - MoldEntry.Text = data; | ||
| 62 | - | ||
| 63 | - // 2) 如需联动业务:在此调用 VM 方法(按需实现) | ||
| 64 | - // 例如:await _vm.HandleMoldScannedAsync(data, type); | ||
| 65 | - | ||
| 66 | - // 3) 如果只是想用扫描即“勾选相应行”,可在这里根据编码查找并置 Selected=true | ||
| 67 | - // ⚠ 你的行模型 MoldOutboundDetailRow 目前不是可通知对象,直接赋值 UI 不一定刷新; | ||
| 68 | - // 建议后续把它改成 ObservableObject 再做切换高亮。 | ||
| 69 | - }); | 71 | + base.OnAppearing(); |
| 72 | + if (!string.IsNullOrWhiteSpace(_vm.OrderNo)) | ||
| 73 | + await _vm.LoadAsync(_vm.OrderNo!, _vm.OrderId); | ||
| 70 | } | 74 | } |
| 71 | 75 | ||
| 72 | - // ✅ 手动输入并按回车,也走同一套逻辑 | ||
| 73 | - private async void OnMoldCompleted(object? sender, EventArgs e) | ||
| 74 | - { | ||
| 75 | - var code = MoldEntry.Text?.Trim(); | ||
| 76 | - if (string.IsNullOrEmpty(code)) return; | ||
| 77 | - | ||
| 78 | - // 可选:与 OnScanned 同步的处理 | ||
| 79 | - // await _vm.HandleMoldScannedAsync(code, "manual"); | ||
| 80 | - | ||
| 81 | - // 光标回到输入框,便于连续扫描 | ||
| 82 | - MoldEntry.CursorPosition = MoldEntry.Text?.Length ?? 0; | ||
| 83 | - MoldEntry.Focus(); | ||
| 84 | - } | ||
| 85 | } | 76 | } |
| @@ -46,8 +46,9 @@ | @@ -46,8 +46,9 @@ | ||
| 46 | <Picker Grid.Column="2" | 46 | <Picker Grid.Column="2" |
| 47 | Title="状态" | 47 | Title="状态" |
| 48 | WidthRequest="120" | 48 | WidthRequest="120" |
| 49 | - ItemsSource="{Binding StatusList}" | ||
| 50 | - SelectedItem="{Binding SelectedStatus}" /> | 49 | + ItemsSource="{Binding StatusOptions}" |
| 50 | + ItemDisplayBinding="{Binding Text}" | ||
| 51 | + SelectedItem="{Binding SelectedStatusOption}" /> | ||
| 51 | </Grid> | 52 | </Grid> |
| 52 | </VerticalStackLayout> | 53 | </VerticalStackLayout> |
| 53 | 54 | ||
| @@ -64,12 +65,13 @@ | @@ -64,12 +65,13 @@ | ||
| 64 | BorderColor="#E0E6EF"> | 65 | BorderColor="#E0E6EF"> |
| 65 | <!-- 点整卡片就跳转 --> | 66 | <!-- 点整卡片就跳转 --> |
| 66 | <Frame.GestureRecognizers> | 67 | <Frame.GestureRecognizers> |
| 67 | - <TapGestureRecognizer Tapped="OnOrderTapped" | ||
| 68 | - CommandParameter="{Binding .}" /> | 68 | + <TapGestureRecognizer |
| 69 | + Command="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.GoExecuteCommand}" | ||
| 70 | + CommandParameter="{Binding .}" /> | ||
| 69 | </Frame.GestureRecognizers> | 71 | </Frame.GestureRecognizers> |
| 70 | 72 | ||
| 71 | <!-- 列表项内容 --> | 73 | <!-- 列表项内容 --> |
| 72 | - <Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto"> | 74 | + <Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto"> |
| 73 | <Grid Grid.Row="0" ColumnDefinitions="Auto,*"> | 75 | <Grid Grid.Row="0" ColumnDefinitions="Auto,*"> |
| 74 | <Label Text="工单编号:" FontAttributes="Bold" /> | 76 | <Label Text="工单编号:" FontAttributes="Bold" /> |
| 75 | <Label Grid.Column="1" Text="{Binding OrderNo}" /> | 77 | <Label Grid.Column="1" Text="{Binding OrderNo}" /> |
| @@ -83,14 +85,18 @@ | @@ -83,14 +85,18 @@ | ||
| 83 | <Label Grid.Column="1" Text="{Binding Status}" /> | 85 | <Label Grid.Column="1" Text="{Binding Status}" /> |
| 84 | </Grid> | 86 | </Grid> |
| 85 | <Grid Grid.Row="3" ColumnDefinitions="Auto,*"> | 87 | <Grid Grid.Row="3" ColumnDefinitions="Auto,*"> |
| 86 | - <Label Text="产品名称:" FontAttributes="Bold" /> | ||
| 87 | - <Label Grid.Column="1" Text="{Binding ProductName}" /> | 88 | + <Label Text="优先级:" FontAttributes="Bold" /> |
| 89 | + <Label Grid.Column="1" Text="{Binding Urgent}" /> | ||
| 88 | </Grid> | 90 | </Grid> |
| 89 | <Grid Grid.Row="4" ColumnDefinitions="Auto,*"> | 91 | <Grid Grid.Row="4" ColumnDefinitions="Auto,*"> |
| 90 | - <Label Text="生产量:" FontAttributes="Bold" /> | ||
| 91 | - <Label Grid.Column="1" Text="{Binding Quantity}" /> | 92 | + <Label Text="产品名称:" FontAttributes="Bold" /> |
| 93 | + <Label Grid.Column="1" Text="{Binding MaterialName}" /> | ||
| 92 | </Grid> | 94 | </Grid> |
| 93 | <Grid Grid.Row="5" ColumnDefinitions="Auto,*"> | 95 | <Grid Grid.Row="5" ColumnDefinitions="Auto,*"> |
| 96 | + <Label Text="生产量:" FontAttributes="Bold" /> | ||
| 97 | + <Label Grid.Column="1" Text="{Binding CurQty}" /> | ||
| 98 | + </Grid> | ||
| 99 | + <Grid Grid.Row="6" ColumnDefinitions="Auto,*"> | ||
| 94 | <Label Text="创建时间:" FontAttributes="Bold" /> | 100 | <Label Text="创建时间:" FontAttributes="Bold" /> |
| 95 | <Label Grid.Column="1" | 101 | <Label Grid.Column="1" |
| 96 | Text="{Binding CreateDate, StringFormat='{0:yyyy-M-d}'}" /> | 102 | Text="{Binding CreateDate, StringFormat='{0:yyyy-M-d}'}" /> |
| 1 | using IndustrialControl.Services; | 1 | using IndustrialControl.Services; |
| 2 | using IndustrialControl.ViewModels; | 2 | using IndustrialControl.ViewModels; |
| 3 | +using System.Text.Json; | ||
| 3 | 4 | ||
| 4 | namespace IndustrialControl.Pages; | 5 | namespace IndustrialControl.Pages; |
| 5 | 6 | ||
| @@ -47,23 +48,30 @@ public partial class WorkOrderSearchPage : ContentPage | @@ -47,23 +48,30 @@ public partial class WorkOrderSearchPage : ContentPage | ||
| 47 | } | 48 | } |
| 48 | 49 | ||
| 49 | // 点整张卡片跳转(推荐) | 50 | // 点整张卡片跳转(推荐) |
| 50 | - private async void OnOrderTapped(object sender, TappedEventArgs e) | 51 | + |
| 52 | +private async void OnOrderTapped(object sender, TappedEventArgs e) | ||
| 53 | +{ | ||
| 54 | + try | ||
| 51 | { | 55 | { |
| 52 | - try | ||
| 53 | - { | ||
| 54 | - // 如果你的 ItemsSource 元素类型是 WorkOrderDto,请把 WorkOrderSummary 换成 WorkOrderDto | ||
| 55 | - if (e.Parameter is not WorkOrderDto item) return; | 56 | + if (e.Parameter is not WorkOrderDto item) return; |
| 56 | 57 | ||
| 57 | - // 跳到目标页并传参(示例为 MoldOutboundExecutePage;按需改成你的页名) | ||
| 58 | - await Shell.Current.GoToAsync( | ||
| 59 | - $"{nameof(MoldOutboundExecutePage)}?orderNo={Uri.EscapeDataString(item.OrderNo)}"); | ||
| 60 | - } | ||
| 61 | - catch (Exception ex) | ||
| 62 | - { | ||
| 63 | - await DisplayAlert("导航失败", ex.Message, "确定"); | ||
| 64 | - } | 58 | + var json = JsonSerializer.Serialize(item); |
| 59 | + | ||
| 60 | + await Shell.Current.GoToAsync( | ||
| 61 | + nameof(MoldOutboundExecutePage), | ||
| 62 | + new Dictionary<string, object?> | ||
| 63 | + { | ||
| 64 | + // 执行页用 IQueryAttributable 接收:key 必须叫 "orderDto" | ||
| 65 | + ["orderDto"] = json | ||
| 66 | + }); | ||
| 65 | } | 67 | } |
| 68 | + catch (Exception ex) | ||
| 69 | + { | ||
| 70 | + await DisplayAlert("导航失败", ex.Message, "确定"); | ||
| 71 | + } | ||
| 72 | +} | ||
| 73 | + | ||
| 66 | 74 | ||
| 67 | - private async void OnScanHintClicked(object sender, EventArgs e) | 75 | +private async void OnScanHintClicked(object sender, EventArgs e) |
| 68 | => await DisplayAlert("提示", "此按钮预留摄像头扫码;硬件扫描直接扣扳机。", "确定"); | 76 | => await DisplayAlert("提示", "此按钮预留摄像头扫码;硬件扫描直接扣扳机。", "确定"); |
| 69 | } | 77 | } |
| @@ -35,7 +35,7 @@ | @@ -35,7 +35,7 @@ | ||
| 35 | <Label.FormattedText> | 35 | <Label.FormattedText> |
| 36 | <FormattedString> | 36 | <FormattedString> |
| 37 | <Span Text="入库单号:" FontAttributes="Bold"/> | 37 | <Span Text="入库单号:" FontAttributes="Bold"/> |
| 38 | - <Span Text="{Binding OrderNo}"/> | 38 | + <Span Text="{Binding InstockNo}"/> |
| 39 | </FormattedString> | 39 | </FormattedString> |
| 40 | </Label.FormattedText> | 40 | </Label.FormattedText> |
| 41 | </Label> | 41 | </Label> |
| @@ -45,7 +45,7 @@ | @@ -45,7 +45,7 @@ | ||
| 45 | <Label.FormattedText> | 45 | <Label.FormattedText> |
| 46 | <FormattedString> | 46 | <FormattedString> |
| 47 | <Span Text="关联到货单号:" FontAttributes="Bold"/> | 47 | <Span Text="关联到货单号:" FontAttributes="Bold"/> |
| 48 | - <Span Text="{Binding LinkedDeliveryNo}"/> | 48 | + <Span Text="{Binding ArrivalNo}"/> |
| 49 | </FormattedString> | 49 | </FormattedString> |
| 50 | </Label.FormattedText> | 50 | </Label.FormattedText> |
| 51 | </Label> | 51 | </Label> |
| @@ -55,7 +55,7 @@ | @@ -55,7 +55,7 @@ | ||
| 55 | <Label.FormattedText> | 55 | <Label.FormattedText> |
| 56 | <FormattedString> | 56 | <FormattedString> |
| 57 | <Span Text="关联采购单:" FontAttributes="Bold"/> | 57 | <Span Text="关联采购单:" FontAttributes="Bold"/> |
| 58 | - <Span Text="{Binding LinkedPurchaseNo}"/> | 58 | + <Span Text="{Binding PurchaseNo}"/> |
| 59 | </FormattedString> | 59 | </FormattedString> |
| 60 | </Label.FormattedText> | 60 | </Label.FormattedText> |
| 61 | </Label> | 61 | </Label> |
| @@ -65,7 +65,7 @@ | @@ -65,7 +65,7 @@ | ||
| 65 | <Label.FormattedText> | 65 | <Label.FormattedText> |
| 66 | <FormattedString> | 66 | <FormattedString> |
| 67 | <Span Text="供应商:" FontAttributes="Bold"/> | 67 | <Span Text="供应商:" FontAttributes="Bold"/> |
| 68 | - <Span Text="{Binding Supplier}"/> | 68 | + <Span Text="{Binding SupplierName}"/> |
| 69 | </FormattedString> | 69 | </FormattedString> |
| 70 | </Label.FormattedText> | 70 | </Label.FormattedText> |
| 71 | </Label> | 71 | </Label> |
| @@ -139,30 +139,41 @@ | @@ -139,30 +139,41 @@ | ||
| 139 | </Grid> | 139 | </Grid> |
| 140 | 140 | ||
| 141 | <!-- 扫描明细列表 --> | 141 | <!-- 扫描明细列表 --> |
| 142 | - <!-- 扫描明细列表 --> | ||
| 143 | <CollectionView Grid.Row="2" | 142 | <CollectionView Grid.Row="2" |
| 144 | ItemsSource="{Binding ScannedList}" | 143 | ItemsSource="{Binding ScannedList}" |
| 145 | IsVisible="{Binding IsScannedVisible}" | 144 | IsVisible="{Binding IsScannedVisible}" |
| 146 | SelectionMode="Single" | 145 | SelectionMode="Single" |
| 147 | - SelectedItem="{Binding SelectedScanItem, Mode=TwoWay}" > | 146 | + SelectedItem="{Binding SelectedScanItem, Mode=TwoWay}"> |
| 148 | <CollectionView.ItemTemplate> | 147 | <CollectionView.ItemTemplate> |
| 149 | <DataTemplate> | 148 | <DataTemplate> |
| 149 | + <!-- 行容器 --> | ||
| 150 | <Grid ColumnDefinitions="40,*,*,*,*,*" Padding="8" BackgroundColor="White"> | 150 | <Grid ColumnDefinitions="40,*,*,*,*,*" Padding="8" BackgroundColor="White"> |
| 151 | + | ||
| 152 | + <!-- ✅ 选中态仍保留 --> | ||
| 151 | <VisualStateManager.VisualStateGroups> | 153 | <VisualStateManager.VisualStateGroups> |
| 152 | <VisualStateGroup Name="CommonStates"> | 154 | <VisualStateGroup Name="CommonStates"> |
| 153 | <VisualState Name="Normal"> | 155 | <VisualState Name="Normal"> |
| 154 | <VisualState.Setters> | 156 | <VisualState.Setters> |
| 155 | - <Setter Property="BackgroundColor" Value="White"/> | 157 | + <Setter Property="BackgroundColor" Value="White" /> |
| 156 | </VisualState.Setters> | 158 | </VisualState.Setters> |
| 157 | </VisualState> | 159 | </VisualState> |
| 158 | <VisualState Name="Selected"> | 160 | <VisualState Name="Selected"> |
| 159 | <VisualState.Setters> | 161 | <VisualState.Setters> |
| 160 | - <Setter Property="BackgroundColor" Value="#CCFFCC"/> | 162 | + <Setter Property="BackgroundColor" Value="#CCFFCC" /> |
| 161 | </VisualState.Setters> | 163 | </VisualState.Setters> |
| 162 | </VisualState> | 164 | </VisualState> |
| 163 | </VisualStateGroup> | 165 | </VisualStateGroup> |
| 164 | </VisualStateManager.VisualStateGroups> | 166 | </VisualStateManager.VisualStateGroups> |
| 165 | 167 | ||
| 168 | + <!-- ✅ 新增:ScanStatus 为 true 时,整行浅绿色 --> | ||
| 169 | + <Grid.Triggers> | ||
| 170 | + <DataTrigger TargetType="Grid" | ||
| 171 | + Binding="{Binding ScanStatus}" | ||
| 172 | + Value="True"> | ||
| 173 | + <Setter Property="BackgroundColor" Value="#E6FFE6" /> | ||
| 174 | + </DataTrigger> | ||
| 175 | + </Grid.Triggers> | ||
| 176 | + | ||
| 166 | <CheckBox IsChecked="{Binding IsSelected}" /> | 177 | <CheckBox IsChecked="{Binding IsSelected}" /> |
| 167 | <Label Grid.Column="1" Text="{Binding Barcode}" /> | 178 | <Label Grid.Column="1" Text="{Binding Barcode}" /> |
| 168 | <Label Grid.Column="2" Text="{Binding Name}" /> | 179 | <Label Grid.Column="2" Text="{Binding Name}" /> |
| @@ -170,21 +181,22 @@ | @@ -170,21 +181,22 @@ | ||
| 170 | 181 | ||
| 171 | <!-- 4 入库库位(可点击) --> | 182 | <!-- 4 入库库位(可点击) --> |
| 172 | <Label Grid.Column="4" | 183 | <Label Grid.Column="4" |
| 173 | - Text="{Binding Bin}" | ||
| 174 | - HorizontalTextAlignment="Center" | ||
| 175 | - VerticalTextAlignment="Center" | ||
| 176 | - TextColor="#007BFF" FontAttributes="Bold"> | 184 | + Text="{Binding Bin}" |
| 185 | + HorizontalTextAlignment="Center" | ||
| 186 | + VerticalTextAlignment="Center" | ||
| 187 | + TextColor="#007BFF" | ||
| 188 | + FontAttributes="Bold"> | ||
| 177 | <Label.GestureRecognizers> | 189 | <Label.GestureRecognizers> |
| 178 | <TapGestureRecognizer Tapped="OnBinTapped" /> | 190 | <TapGestureRecognizer Tapped="OnBinTapped" /> |
| 179 | </Label.GestureRecognizers> | 191 | </Label.GestureRecognizers> |
| 180 | </Label> | 192 | </Label> |
| 181 | 193 | ||
| 182 | - <!-- 已扫描数量:可手动改,用数字键盘;建议加转换器容错 --> | 194 | + <!-- 数量 --> |
| 183 | <Entry Grid.Column="5" | 195 | <Entry Grid.Column="5" |
| 184 | - Keyboard="Numeric" | ||
| 185 | - HorizontalTextAlignment="Center" | ||
| 186 | - WidthRequest="64" | ||
| 187 | - Text="{Binding Qty, Mode=TwoWay, Converter={StaticResource IntConverter}}" /> | 196 | + Keyboard="Numeric" |
| 197 | + HorizontalTextAlignment="Center" | ||
| 198 | + WidthRequest="64" | ||
| 199 | + Text="{Binding Qty, Mode=TwoWay, Converter={StaticResource IntConverter}}" /> | ||
| 188 | </Grid> | 200 | </Grid> |
| 189 | </DataTemplate> | 201 | </DataTemplate> |
| 190 | </CollectionView.ItemTemplate> | 202 | </CollectionView.ItemTemplate> |
| @@ -3,14 +3,24 @@ using IndustrialControl.ViewModels; | @@ -3,14 +3,24 @@ using IndustrialControl.ViewModels; | ||
| 3 | 3 | ||
| 4 | namespace IndustrialControl.Pages; | 4 | namespace IndustrialControl.Pages; |
| 5 | 5 | ||
| 6 | -// NEW: 支持从查询页传参 | ||
| 7 | -[QueryProperty(nameof(OrderNo), "orderNo")] | 6 | +[QueryProperty(nameof(InstockId), "instockId")] |
| 7 | +[QueryProperty(nameof(InstockNo), "instockNo")] | ||
| 8 | +[QueryProperty(nameof(OrderType), "orderType")] | ||
| 9 | +[QueryProperty(nameof(OrderTypeName), "orderTypeName")] | ||
| 10 | +[QueryProperty(nameof(PurchaseNo), "purchaseNo")] | ||
| 11 | +[QueryProperty(nameof(SupplierName), "supplierName")] | ||
| 12 | +[QueryProperty(nameof(CreatedTime), "createdTime")] | ||
| 8 | public partial class InboundMaterialPage : ContentPage | 13 | public partial class InboundMaterialPage : ContentPage |
| 9 | { | 14 | { |
| 10 | private readonly ScanService _scanSvc; | 15 | private readonly ScanService _scanSvc; |
| 11 | private readonly InboundMaterialViewModel _vm; | 16 | private readonly InboundMaterialViewModel _vm; |
| 12 | - // NEW: 由 Shell 设置 | ||
| 13 | - public string? OrderNo { get; set; } | 17 | + public string? InstockId { get; set; } |
| 18 | + public string? InstockNo { get; set; } | ||
| 19 | + public string? OrderType { get; set; } | ||
| 20 | + public string? OrderTypeName { get; set; } | ||
| 21 | + public string? PurchaseNo { get; set; } | ||
| 22 | + public string? SupplierName { get; set; } | ||
| 23 | + public string? CreatedTime { get; set; } | ||
| 14 | private readonly IDialogService _dialogs; | 24 | private readonly IDialogService _dialogs; |
| 15 | 25 | ||
| 16 | public InboundMaterialPage(InboundMaterialViewModel vm, ScanService scanSvc, IDialogService dialogs) | 26 | public InboundMaterialPage(InboundMaterialViewModel vm, ScanService scanSvc, IDialogService dialogs) |
| @@ -33,15 +43,22 @@ public partial class InboundMaterialPage : ContentPage | @@ -33,15 +43,22 @@ public partial class InboundMaterialPage : ContentPage | ||
| 33 | { | 43 | { |
| 34 | base.OnAppearing(); | 44 | base.OnAppearing(); |
| 35 | 45 | ||
| 36 | - // 如果带有单号参数,则加载其明细,进入图2/图3的流程 | ||
| 37 | - if (!string.IsNullOrWhiteSpace(OrderNo)) | 46 | + // ✅ 用搜索页带过来的基础信息初始化页面,并拉取两张表 |
| 47 | + if (!string.IsNullOrWhiteSpace(InstockId)) | ||
| 38 | { | 48 | { |
| 39 | - await _vm.LoadOrderAsync(OrderNo); | 49 | + await _vm.InitializeFromSearchAsync( |
| 50 | + instockId: InstockId ?? "", | ||
| 51 | + instockNo: InstockNo ?? "", | ||
| 52 | + orderType: OrderType ?? "", | ||
| 53 | + orderTypeName: OrderTypeName ?? "", | ||
| 54 | + purchaseNo: PurchaseNo ?? "", | ||
| 55 | + supplierName: SupplierName ?? "", | ||
| 56 | + createdTime: CreatedTime ?? "" | ||
| 57 | + ); | ||
| 40 | } | 58 | } |
| 41 | - // 动态注册广播接收器(只在当前页面前台时生效) | 59 | + |
| 42 | _scanSvc.Scanned += OnScanned; | 60 | _scanSvc.Scanned += OnScanned; |
| 43 | _scanSvc.StartListening(); | 61 | _scanSvc.StartListening(); |
| 44 | - //键盘输入 | ||
| 45 | _scanSvc.Attach(ScanEntry); | 62 | _scanSvc.Attach(ScanEntry); |
| 46 | ScanEntry.Focus(); | 63 | ScanEntry.Focus(); |
| 47 | } | 64 | } |
| @@ -82,7 +99,7 @@ public partial class InboundMaterialPage : ContentPage | @@ -82,7 +99,7 @@ public partial class InboundMaterialPage : ContentPage | ||
| 82 | /// <summary> | 99 | /// <summary> |
| 83 | /// 确认入库按钮点击 | 100 | /// 确认入库按钮点击 |
| 84 | /// </summary> | 101 | /// </summary> |
| 85 | - async void OnConfirmClicked(object sender, EventArgs e) | 102 | + async void OnConfirmClicked(object sender, EventArgs e) |
| 86 | { | 103 | { |
| 87 | var ok = await _vm.ConfirmInboundAsync(); | 104 | var ok = await _vm.ConfirmInboundAsync(); |
| 88 | if (ok) | 105 | if (ok) |
| 1 | <?xml version="1.0" encoding="utf-8" ?> | 1 | <?xml version="1.0" encoding="utf-8" ?> |
| 2 | -<ContentPage x:Name="Page" | 2 | +<ContentPage |
| 3 | + x:Class="IndustrialControl.Pages.InboundMaterialSearchPage" | ||
| 3 | xmlns="http://schemas.microsoft.com/dotnet/2021/maui" | 4 | xmlns="http://schemas.microsoft.com/dotnet/2021/maui" |
| 4 | - xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" | ||
| 5 | - x:Class="IndustrialControl.Pages.InboundMaterialSearchPage" | ||
| 6 | - xmlns:conv="clr-namespace:IndustrialControl.Converters" | ||
| 7 | - Title="仓储管理系统"> | 5 | + xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" |
| 6 | + x:Name="Page" | ||
| 7 | + Title="入库单查询"> | ||
| 8 | 8 | ||
| 9 | - <ContentPage.Resources> | ||
| 10 | - <ResourceDictionary> | ||
| 11 | - <!-- 空/非空转布尔:非空 => true(按钮可用) --> | ||
| 12 | - <conv:NullToBoolConverter x:Key="NullToBoolConverter" /> | ||
| 13 | - </ResourceDictionary> | ||
| 14 | - </ContentPage.Resources> | 9 | + <ScrollView> |
| 10 | + <VerticalStackLayout Spacing="12" Padding="12"> | ||
| 15 | 11 | ||
| 16 | - <Grid RowDefinitions="Auto,*,Auto" Padding="16" BackgroundColor="#F6F7FB"> | ||
| 17 | - | ||
| 18 | - <!-- 顶部:输入条件(第0行) --> | ||
| 19 | - <VerticalStackLayout Grid.Row="0" Spacing="10"> | 12 | + <!-- 查询区域 --> |
| 20 | <Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto" ColumnSpacing="8" RowSpacing="8"> | 13 | <Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto" ColumnSpacing="8" RowSpacing="8"> |
| 21 | <Entry x:Name="OrderEntry" | 14 | <Entry x:Name="OrderEntry" |
| 22 | Grid.Row="0" Grid.Column="0" | 15 | Grid.Row="0" Grid.Column="0" |
| 23 | - Placeholder="请输入入库单号/包裹条码" | ||
| 24 | - VerticalOptions="Center" | ||
| 25 | - BackgroundColor="White" | 16 | + Placeholder="请输入入库单号/条码" |
| 26 | Text="{Binding SearchOrderNo}" /> | 17 | Text="{Binding SearchOrderNo}" /> |
| 27 | 18 | ||
| 28 | - <DatePicker Grid.Row="1" Grid.Column="0" | ||
| 29 | - Date="{Binding CreatedDate}" | ||
| 30 | - MinimumDate="2000-01-01" /> | ||
| 31 | - <Button Grid.Row="1" Grid.Column="1" | 19 | + <Button Grid.Row="0" Grid.Column="1" |
| 32 | Text="查询" | 20 | Text="查询" |
| 33 | Command="{Binding SearchCommand}" /> | 21 | Command="{Binding SearchCommand}" /> |
| 22 | + | ||
| 23 | + <Grid Grid.Row="1" Grid.ColumnSpan="2" ColumnDefinitions="Auto,*,Auto,*" ColumnSpacing="8"> | ||
| 24 | + <Label Grid.Column="0" Text="开始:" VerticalTextAlignment="Center"/> | ||
| 25 | + <DatePicker Grid.Column="1" Date="{Binding StartDate}" /> | ||
| 26 | + <Label Grid.Column="2" Text="结束:" VerticalTextAlignment="Center"/> | ||
| 27 | + <DatePicker Grid.Column="3" Date="{Binding EndDate}" /> | ||
| 28 | + </Grid> | ||
| 34 | </Grid> | 29 | </Grid> |
| 35 | - </VerticalStackLayout> | ||
| 36 | 30 | ||
| 37 | - <!-- 中部:结果列表(第1行) --> | ||
| 38 | - <CollectionView Grid.Row="1" | ||
| 39 | - ItemsSource="{Binding Orders}" | ||
| 40 | - SelectionMode="Single" | ||
| 41 | - SelectionChanged="OnOrderSelected"> | ||
| 42 | - <CollectionView.ItemTemplate> | ||
| 43 | - <DataTemplate> | ||
| 44 | - <Frame Margin="0,8,0,0" Padding="12" HasShadow="True" CornerRadius="10"> | ||
| 45 | - <!-- ⭐ 点击整卡片触发命令 --> | ||
| 46 | - <Frame.GestureRecognizers> | ||
| 47 | - <TapGestureRecognizer | ||
| 48 | - Command="{Binding BindingContext.OpenItemCommand, Source={x:Reference Page}}" | ||
| 49 | - CommandParameter="{Binding .}" /> | ||
| 50 | - </Frame.GestureRecognizers> | ||
| 51 | - <Grid RowDefinitions="Auto,Auto,Auto,Auto" | ||
| 52 | - ColumnDefinitions="Auto,*" ColumnSpacing="8"> | ||
| 53 | - <Label Grid.Row="0" Grid.Column="0" Text="入库单号:" FontAttributes="Bold"/> | ||
| 54 | - <Label Grid.Row="0" Grid.Column="1" Text="{Binding OrderNo}"/> | 31 | + <!-- 列表区域 --> |
| 32 | + <CollectionView Grid.Row="1" | ||
| 33 | + ItemsSource="{Binding Orders}" | ||
| 34 | + SelectionMode="Single" | ||
| 35 | + SelectedItem="{Binding SelectedOrder, Mode=TwoWay}"> | ||
| 36 | + <CollectionView.ItemTemplate> | ||
| 37 | + <DataTemplate> | ||
| 38 | + <Frame Margin="0,8,0,0" Padding="12" HasShadow="True" CornerRadius="10"> | ||
| 39 | + <!-- 点击整卡片:直接调用 VM 的 GoInboundCommand,并把当前项作为参数 --> | ||
| 40 | + <Frame.GestureRecognizers> | ||
| 41 | + <TapGestureRecognizer | ||
| 42 | + Command="{Binding BindingContext.GoInboundCommand, Source={x:Reference Page}}" | ||
| 43 | + CommandParameter="{Binding .}" /> | ||
| 44 | + </Frame.GestureRecognizers> | ||
| 55 | 45 | ||
| 56 | - <Label Grid.Row="1" Grid.Column="0" Text="入库类型:" FontAttributes="Bold"/> | ||
| 57 | - <Label Grid.Row="1" Grid.Column="1" Text="{Binding InboundType}" /> | 46 | + <Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto" |
| 47 | + ColumnDefinitions="Auto,*" ColumnSpacing="8"> | ||
| 48 | + <Label Grid.Row="0" Grid.Column="0" Text="入库单号:" FontAttributes="Bold"/> | ||
| 49 | + <Label Grid.Row="0" Grid.Column="1" Text="{Binding instockNo}"/> | ||
| 58 | 50 | ||
| 59 | - <Label Grid.Row="2" Grid.Column="0" Text="供应商:" FontAttributes="Bold"/> | ||
| 60 | - <Label Grid.Row="2" Grid.Column="1" Text="{Binding Supplier}" /> | 51 | + <Label Grid.Row="1" Grid.Column="0" Text="入库单类型:" FontAttributes="Bold"/> |
| 52 | + <Label Grid.Row="1" Grid.Column="1" Text="{Binding orderTypeName}" /> | ||
| 61 | 53 | ||
| 62 | - <Label Grid.Row="3" Grid.Column="0" Text="创建日期:" FontAttributes="Bold"/> | ||
| 63 | - <Label Grid.Row="3" Grid.Column="1" Text="{Binding CreatedAt, StringFormat='{0:yyyy-M-d}'}"/> | ||
| 64 | - </Grid> | ||
| 65 | - </Frame> | ||
| 66 | - </DataTemplate> | ||
| 67 | - </CollectionView.ItemTemplate> | ||
| 68 | - </CollectionView> | 54 | + <Label Grid.Row="2" Grid.Column="0" Text="供应商名称:" FontAttributes="Bold"/> |
| 55 | + <Label Grid.Row="2" Grid.Column="1" Text="{Binding supplierName}" /> | ||
| 69 | 56 | ||
| 70 | - <!-- 底部:操作(第2行) --> | ||
| 71 | - <Grid Grid.Row="2" ColumnDefinitions="*,Auto" Padding="0,8,0,0"> | ||
| 72 | - <Label Text="{Binding Orders.Count, StringFormat='共 {0} 条'}" | ||
| 73 | - VerticalTextAlignment="Center" /> | ||
| 74 | - </Grid> | ||
| 75 | - </Grid> | 57 | + <Label Grid.Row="3" Grid.Column="0" Text="关联到货单号:" FontAttributes="Bold"/> |
| 58 | + <Label Grid.Row="3" Grid.Column="1" Text="{Binding arrivalNo}" /> | ||
| 59 | + | ||
| 60 | + <Label Grid.Row="4" Grid.Column="0" Text="创建日期:" FontAttributes="Bold"/> | ||
| 61 | + <Label Grid.Row="4" Grid.Column="1" Text="{Binding createdTime}" /> | ||
| 62 | + </Grid> | ||
| 63 | + </Frame> | ||
| 64 | + </DataTemplate> | ||
| 65 | + </CollectionView.ItemTemplate> | ||
| 66 | + </CollectionView> | ||
| 67 | + | ||
| 68 | + </VerticalStackLayout> | ||
| 69 | + </ScrollView> | ||
| 76 | </ContentPage> | 70 | </ContentPage> |
| @@ -59,20 +59,6 @@ public partial class InboundMaterialSearchPage : ContentPage | @@ -59,20 +59,6 @@ public partial class InboundMaterialSearchPage : ContentPage | ||
| 59 | _vm.SearchOrderNo = data; | 59 | _vm.SearchOrderNo = data; |
| 60 | }); | 60 | }); |
| 61 | } | 61 | } |
| 62 | - private async void OnOrderSelected(object sender, SelectionChangedEventArgs e) | ||
| 63 | - { | ||
| 64 | - var item = e.CurrentSelection?.FirstOrDefault() as InboundOrderSummary; | ||
| 65 | - if (item is null) return; | ||
| 66 | - | ||
| 67 | - // 二选一:A) 点选跳转到入库页 | ||
| 68 | - await Shell.Current.GoToAsync( | ||
| 69 | - $"{nameof(InboundMaterialPage)}?orderNo={Uri.EscapeDataString(item.OrderNo)}"); | ||
| 70 | - | ||
| 71 | - // 或 B) 只把单号写到输入框/VM(不跳转) | ||
| 72 | - // if (BindingContext is InboundMaterialSearchViewModel vm) vm.SearchOrderNo = item.OrderNo; | ||
| 73 | - | ||
| 74 | - ((CollectionView)sender).SelectedItem = null; // 清除选中高亮 | ||
| 75 | - } | ||
| 76 | 62 | ||
| 77 | 63 | ||
| 78 | } | 64 | } |
| @@ -32,17 +32,32 @@ | @@ -32,17 +32,32 @@ | ||
| 32 | </Grid> | 32 | </Grid> |
| 33 | 33 | ||
| 34 | <!-- 基础信息 --> | 34 | <!-- 基础信息 --> |
| 35 | - <Frame Grid.Row="2" Margin="16,0" Padding="8" BorderColor="#CCCCCC" BackgroundColor="White"> | ||
| 36 | - <Grid RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*,Auto,*" ColumnSpacing="8" RowSpacing="6"> | ||
| 37 | - <Label Grid.Row="0" Grid.Column="0" Text="入库单号:" FontAttributes="Bold" /> | ||
| 38 | - <Label Grid.Row="0" Grid.Column="1" Text="{Binding OrderNo}" LineBreakMode="TailTruncation" /> | ||
| 39 | - <Label Grid.Row="0" Grid.Column="2" Text="工单号:" FontAttributes="Bold" /> | ||
| 40 | - <Label Grid.Row="0" Grid.Column="3" Text="{Binding WorkOrderNo}" LineBreakMode="TailTruncation" /> | ||
| 41 | - | ||
| 42 | - <Label Grid.Row="1" Grid.Column="0" Text="产品名称:" FontAttributes="Bold" /> | ||
| 43 | - <Label Grid.Row="1" Grid.Column="1" Text="{Binding ProductName}" LineBreakMode="TailTruncation" /> | ||
| 44 | - <Label Grid.Row="1" Grid.Column="2" Text="待入库数:" FontAttributes="Bold" /> | ||
| 45 | - <Label Grid.Row="1" Grid.Column="3" Text="{Binding PendingQty}" LineBreakMode="TailTruncation" /> | 35 | + <Frame Margin="0,8,0,0" Padding="12" HasShadow="True" CornerRadius="10"> |
| 36 | + <!-- 点击整卡片:直接调用 VM 的 GoInboundCommand,并把当前项作为参数 --> | ||
| 37 | + <Frame.GestureRecognizers> | ||
| 38 | + <TapGestureRecognizer | ||
| 39 | + Command="{Binding BindingContext.GoInboundCommand, Source={x:Reference Page}}" | ||
| 40 | + CommandParameter="{Binding .}" /> | ||
| 41 | + </Frame.GestureRecognizers> | ||
| 42 | + | ||
| 43 | + <Grid RowDefinitions="Auto,Auto,Auto,Auto" | ||
| 44 | + ColumnDefinitions="Auto,*" ColumnSpacing="8"> | ||
| 45 | + <Label Grid.Row="0" Grid.Column="0" Text="入库单号:" FontAttributes="Bold"/> | ||
| 46 | + <Label Grid.Row="0" Grid.Column="1" Text="{Binding instockNo}"/> | ||
| 47 | + | ||
| 48 | + <Label Grid.Row="1" Grid.Column="0" Text="入库单类型:" FontAttributes="Bold"/> | ||
| 49 | + <Label Grid.Row="1" Grid.Column="1" Text="{Binding orderTypeName}" /> | ||
| 50 | + <Label Grid.Row="1" Grid.Column="0" Text="产品名称:" FontAttributes="Bold"/> | ||
| 51 | + <Label Grid.Row="1" Grid.Column="1" Text="{Binding orderTypeName}" /> | ||
| 52 | + | ||
| 53 | + <Label Grid.Row="2" Grid.Column="0" Text="供应商名称:" FontAttributes="Bold"/> | ||
| 54 | + <Label Grid.Row="2" Grid.Column="1" Text="{Binding supplierName}" /> | ||
| 55 | + | ||
| 56 | + <Label Grid.Row="2" Grid.Column="0" Text="关工单号:" FontAttributes="Bold"/> | ||
| 57 | + <Label Grid.Row="2" Grid.Column="1" Text="{Binding supplierName}" /> | ||
| 58 | + | ||
| 59 | + <Label Grid.Row="3" Grid.Column="0" Text="创建日期:" FontAttributes="Bold"/> | ||
| 60 | + <Label Grid.Row="3" Grid.Column="1" Text="{Binding createdTime}" /> | ||
| 46 | </Grid> | 61 | </Grid> |
| 47 | </Frame> | 62 | </Frame> |
| 48 | 63 |
| @@ -3,14 +3,27 @@ using IndustrialControl.ViewModels; | @@ -3,14 +3,27 @@ using IndustrialControl.ViewModels; | ||
| 3 | 3 | ||
| 4 | namespace IndustrialControl.Pages | 4 | namespace IndustrialControl.Pages |
| 5 | { | 5 | { |
| 6 | - [QueryProperty(nameof(OrderNo), "orderNo")] | 6 | + |
| 7 | + [QueryProperty(nameof(InstockId), "instockId")] | ||
| 8 | + [QueryProperty(nameof(InstockNo), "instockNo")] | ||
| 9 | + [QueryProperty(nameof(OrderType), "orderType")] | ||
| 10 | + [QueryProperty(nameof(OrderTypeName), "orderTypeName")] | ||
| 11 | + [QueryProperty(nameof(PurchaseNo), "purchaseNo")] | ||
| 12 | + [QueryProperty(nameof(SupplierName), "supplierName")] | ||
| 13 | + [QueryProperty(nameof(CreatedTime), "createdTime")] | ||
| 7 | public partial class InboundProductionPage : ContentPage | 14 | public partial class InboundProductionPage : ContentPage |
| 8 | { | 15 | { |
| 9 | private readonly ScanService _scanSvc; | 16 | private readonly ScanService _scanSvc; |
| 10 | - private readonly InboundProductionViewModel _vm; | ||
| 11 | - public string? OrderNo { get; set; } | 17 | + private readonly InboundMaterialViewModel _vm; |
| 18 | + public string? InstockId { get; set; } | ||
| 19 | + public string? InstockNo { get; set; } | ||
| 20 | + public string? OrderType { get; set; } | ||
| 21 | + public string? OrderTypeName { get; set; } | ||
| 22 | + public string? PurchaseNo { get; set; } | ||
| 23 | + public string? SupplierName { get; set; } | ||
| 24 | + public string? CreatedTime { get; set; } | ||
| 12 | 25 | ||
| 13 | - public InboundProductionPage(InboundProductionViewModel vm, ScanService scanSvc) | 26 | + public InboundProductionPage(InboundMaterialViewModel vm, ScanService scanSvc) |
| 14 | { | 27 | { |
| 15 | InitializeComponent(); | 28 | InitializeComponent(); |
| 16 | BindingContext = vm; | 29 | BindingContext = vm; |
| @@ -27,15 +40,22 @@ namespace IndustrialControl.Pages | @@ -27,15 +40,22 @@ namespace IndustrialControl.Pages | ||
| 27 | { | 40 | { |
| 28 | base.OnAppearing(); | 41 | base.OnAppearing(); |
| 29 | 42 | ||
| 30 | - // 如果带有单号参数,则加载其明细,进入图2/图3的流程 | ||
| 31 | - if (!string.IsNullOrWhiteSpace(OrderNo)) | 43 | + // ✅ 用搜索页带过来的基础信息初始化页面,并拉取两张表 |
| 44 | + if (!string.IsNullOrWhiteSpace(InstockId)) | ||
| 32 | { | 45 | { |
| 33 | - await _vm.LoadOrderAsync(OrderNo); | 46 | + await _vm.InitializeFromSearchAsync( |
| 47 | + instockId: InstockId ?? "", | ||
| 48 | + instockNo: InstockNo ?? "", | ||
| 49 | + orderType: OrderType ?? "", | ||
| 50 | + orderTypeName: OrderTypeName ?? "", | ||
| 51 | + purchaseNo: PurchaseNo ?? "", | ||
| 52 | + supplierName: SupplierName ?? "", | ||
| 53 | + createdTime: CreatedTime ?? "" | ||
| 54 | + ); | ||
| 34 | } | 55 | } |
| 35 | - // 动态注册广播接收器(只在当前页面前台时生效) | 56 | + |
| 36 | _scanSvc.Scanned += OnScanned; | 57 | _scanSvc.Scanned += OnScanned; |
| 37 | _scanSvc.StartListening(); | 58 | _scanSvc.StartListening(); |
| 38 | - //键盘输入 | ||
| 39 | _scanSvc.Attach(ScanEntry); | 59 | _scanSvc.Attach(ScanEntry); |
| 40 | ScanEntry.Focus(); | 60 | ScanEntry.Focus(); |
| 41 | } | 61 | } |
| @@ -78,21 +98,7 @@ namespace IndustrialControl.Pages | @@ -78,21 +98,7 @@ namespace IndustrialControl.Pages | ||
| 78 | await DisplayAlert("提示", "此按钮预留摄像头扫码;硬件扫描直接扣扳机。", "确定"); | 98 | await DisplayAlert("提示", "此按钮预留摄像头扫码;硬件扫描直接扣扳机。", "确定"); |
| 79 | } | 99 | } |
| 80 | 100 | ||
| 81 | - /// <summary> | ||
| 82 | - /// 扫描通过 | ||
| 83 | - /// </summary> | ||
| 84 | - void OnPassScanClicked(object sender, EventArgs e) | ||
| 85 | - { | ||
| 86 | - _vm.PassSelectedScan(); | ||
| 87 | - } | ||
| 88 | - | ||
| 89 | - /// <summary> | ||
| 90 | - /// 取消扫描 | ||
| 91 | - /// </summary> | ||
| 92 | - void OnCancelScanClicked(object sender, EventArgs e) | ||
| 93 | - { | ||
| 94 | - _vm.CancelSelectedScan(); | ||
| 95 | - } | 101 | + |
| 96 | 102 | ||
| 97 | /// <summary> | 103 | /// <summary> |
| 98 | /// 确认入库 | 104 | /// 确认入库 |
| @@ -23,14 +23,16 @@ | @@ -23,14 +23,16 @@ | ||
| 23 | VerticalOptions="Center" | 23 | VerticalOptions="Center" |
| 24 | BackgroundColor="White" | 24 | BackgroundColor="White" |
| 25 | Text="{Binding SearchOrderNo}" /> | 25 | Text="{Binding SearchOrderNo}" /> |
| 26 | - | ||
| 27 | - | ||
| 28 | - <DatePicker Grid.Row="1" Grid.Column="0" | ||
| 29 | - Date="{Binding CreatedDate}" | ||
| 30 | - MinimumDate="2000-01-01" /> | ||
| 31 | - <Button Grid.Row="1" Grid.Column="1" | 26 | + <Button Grid.Row="0" Grid.Column="1" |
| 32 | Text="查询" | 27 | Text="查询" |
| 33 | Command="{Binding SearchCommand}" /> | 28 | Command="{Binding SearchCommand}" /> |
| 29 | + | ||
| 30 | + <Grid Grid.Row="1" Grid.ColumnSpan="2" ColumnDefinitions="Auto,*,Auto,*" ColumnSpacing="8"> | ||
| 31 | + <Label Grid.Column="0" Text="开始:" VerticalTextAlignment="Center"/> | ||
| 32 | + <DatePicker Grid.Column="1" Date="{Binding StartDate}" /> | ||
| 33 | + <Label Grid.Column="2" Text="结束:" VerticalTextAlignment="Center"/> | ||
| 34 | + <DatePicker Grid.Column="3" Date="{Binding EndDate}" /> | ||
| 35 | + </Grid> | ||
| 34 | </Grid> | 36 | </Grid> |
| 35 | </VerticalStackLayout> | 37 | </VerticalStackLayout> |
| 36 | 38 | ||
| @@ -38,39 +40,39 @@ | @@ -38,39 +40,39 @@ | ||
| 38 | <CollectionView Grid.Row="1" | 40 | <CollectionView Grid.Row="1" |
| 39 | ItemsSource="{Binding Orders}" | 41 | ItemsSource="{Binding Orders}" |
| 40 | SelectionMode="Single" | 42 | SelectionMode="Single" |
| 41 | - SelectionChanged="OnOrderSelected"> | 43 | + SelectedItem="{Binding SelectedOrder, Mode=TwoWay}"> |
| 42 | <CollectionView.ItemTemplate> | 44 | <CollectionView.ItemTemplate> |
| 43 | <DataTemplate> | 45 | <DataTemplate> |
| 44 | <Frame Margin="0,8,0,0" Padding="12" HasShadow="True" CornerRadius="10"> | 46 | <Frame Margin="0,8,0,0" Padding="12" HasShadow="True" CornerRadius="10"> |
| 45 | <!-- ⭐ 点击整卡片触发命令 --> | 47 | <!-- ⭐ 点击整卡片触发命令 --> |
| 46 | <Frame.GestureRecognizers> | 48 | <Frame.GestureRecognizers> |
| 47 | <TapGestureRecognizer | 49 | <TapGestureRecognizer |
| 48 | - Command="{Binding BindingContext.OpenItemCommand, Source={x:Reference Page}}" | ||
| 49 | - CommandParameter="{Binding .}" /> | 50 | + Command="{Binding BindingContext.GoInboundCommand, Source={x:Reference Page}}" |
| 51 | + CommandParameter="{Binding .}" /> | ||
| 50 | </Frame.GestureRecognizers> | 52 | </Frame.GestureRecognizers> |
| 51 | - <Grid RowDefinitions="Auto,Auto,Auto,Auto" | ||
| 52 | - ColumnDefinitions="Auto,*" ColumnSpacing="8"> | 53 | + <Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="Auto,*" ColumnSpacing="8"> |
| 53 | <Label Grid.Row="0" Grid.Column="0" Text="入库单号:" FontAttributes="Bold"/> | 54 | <Label Grid.Row="0" Grid.Column="0" Text="入库单号:" FontAttributes="Bold"/> |
| 54 | - <Label Grid.Row="0" Grid.Column="1" Text="{Binding OrderNo}"/> | 55 | + <Label Grid.Row="0" Grid.Column="1" Text="{Binding instockNo}"/> |
| 56 | + | ||
| 57 | + <Label Grid.Row="1" Grid.Column="0" Text="入库单类型:" FontAttributes="Bold"/> | ||
| 58 | + <Label Grid.Row="1" Grid.Column="1" Text="{Binding orderTypeName}" /> | ||
| 55 | 59 | ||
| 56 | - <Label Grid.Row="1" Grid.Column="0" Text="入库类型:" FontAttributes="Bold"/> | ||
| 57 | - <Label Grid.Row="1" Grid.Column="1" Text="{Binding InboundType}" /> | 60 | + <Label Grid.Row="2" Grid.Column="0" Text="产品名称:" FontAttributes="Bold"/> |
| 61 | + <Label Grid.Row="2" Grid.Column="1" Text="{Binding supplierName}" /> | ||
| 58 | 62 | ||
| 59 | - <Label Grid.Row="2" Grid.Column="0" Text="供应商:" FontAttributes="Bold"/> | ||
| 60 | - <Label Grid.Row="2" Grid.Column="1" Text="{Binding Supplier}" /> | 63 | + <Label Grid.Row="3" Grid.Column="0" Text="供应商名称:" FontAttributes="Bold"/> |
| 64 | + <Label Grid.Row="3" Grid.Column="1" Text="{Binding supplierName}" /> | ||
| 65 | + | ||
| 66 | + <Label Grid.Row="4" Grid.Column="0" Text="关联到货单号:" FontAttributes="Bold"/> | ||
| 67 | + <Label Grid.Row="4" Grid.Column="1" Text="{Binding arrivalNo}" /> | ||
| 61 | 68 | ||
| 62 | - <Label Grid.Row="3" Grid.Column="0" Text="创建日期:" FontAttributes="Bold"/> | ||
| 63 | - <Label Grid.Row="3" Grid.Column="1" Text="{Binding CreatedAt, StringFormat='{0:yyyy-M-d}'}"/> | 69 | + <Label Grid.Row="5" Grid.Column="0" Text="创建日期:" FontAttributes="Bold"/> |
| 70 | + <Label Grid.Row="5" Grid.Column="1" Text="{Binding createdTime}" /> | ||
| 64 | </Grid> | 71 | </Grid> |
| 65 | </Frame> | 72 | </Frame> |
| 66 | </DataTemplate> | 73 | </DataTemplate> |
| 67 | </CollectionView.ItemTemplate> | 74 | </CollectionView.ItemTemplate> |
| 68 | </CollectionView> | 75 | </CollectionView> |
| 69 | 76 | ||
| 70 | - <!-- 底部:操作(第2行) --> | ||
| 71 | - <Grid Grid.Row="2" ColumnDefinitions="*,Auto" Padding="0,8,0,0"> | ||
| 72 | - <Label Text="{Binding Orders.Count, StringFormat='共 {0} 条'}" | ||
| 73 | - VerticalTextAlignment="Center" /> | ||
| 74 | - </Grid> | ||
| 75 | </Grid> | 77 | </Grid> |
| 76 | </ContentPage> | 78 | </ContentPage> |
| @@ -58,18 +58,5 @@ public partial class InboundProductionSearchPage : ContentPage | @@ -58,18 +58,5 @@ public partial class InboundProductionSearchPage : ContentPage | ||
| 58 | _vm.SearchOrderNo = data; | 58 | _vm.SearchOrderNo = data; |
| 59 | }); | 59 | }); |
| 60 | } | 60 | } |
| 61 | - private async void OnOrderSelected(object sender, SelectionChangedEventArgs e) | ||
| 62 | - { | ||
| 63 | - var item = e.CurrentSelection?.FirstOrDefault() as InboundOrderSummary; | ||
| 64 | - if (item is null) return; | ||
| 65 | - | ||
| 66 | - // 二选一:A) 点选跳转到入库页 | ||
| 67 | - await Shell.Current.GoToAsync( | ||
| 68 | - $"{nameof(InboundMaterialPage)}?orderNo={Uri.EscapeDataString(item.OrderNo)}"); | ||
| 69 | 61 | ||
| 70 | - // 或 B) 只把单号写到输入框/VM(不跳转) | ||
| 71 | - // if (BindingContext is InboundMaterialSearchViewModel vm) vm.SearchOrderNo = item.OrderNo; | ||
| 72 | - | ||
| 73 | - ((CollectionView)sender).SelectedItem = null; // 清除选中高亮 | ||
| 74 | - } | ||
| 75 | } | 62 | } |
| @@ -3,12 +3,25 @@ using IndustrialControl.ViewModels; | @@ -3,12 +3,25 @@ using IndustrialControl.ViewModels; | ||
| 3 | 3 | ||
| 4 | namespace IndustrialControl.Pages | 4 | namespace IndustrialControl.Pages |
| 5 | { | 5 | { |
| 6 | - [QueryProperty(nameof(OrderNo), "orderNo")] | 6 | + [QueryProperty(nameof(OutstockId), "outstockId")] |
| 7 | + [QueryProperty(nameof(OutstockNo), "outstockNo")] | ||
| 8 | + [QueryProperty(nameof(OrderType), "orderType")] | ||
| 9 | + [QueryProperty(nameof(OrderTypeName), "orderTypeName")] | ||
| 10 | + [QueryProperty(nameof(PurchaseNo), "purchaseNo")] | ||
| 11 | + [QueryProperty(nameof(SupplierName), "supplierName")] | ||
| 12 | + [QueryProperty(nameof(CreatedTime), "createdTime")] | ||
| 7 | public partial class OutboundFinishedPage : ContentPage | 13 | public partial class OutboundFinishedPage : ContentPage |
| 8 | { | 14 | { |
| 9 | private readonly ScanService _scanSvc; | 15 | private readonly ScanService _scanSvc; |
| 10 | private readonly OutboundFinishedViewModel _vm; | 16 | private readonly OutboundFinishedViewModel _vm; |
| 11 | - public string? OrderNo { get; set; } | 17 | + public string? OutstockId { get; set; } |
| 18 | + public string? OutstockNo { get; set; } | ||
| 19 | + public string? OrderType { get; set; } | ||
| 20 | + public string? OrderTypeName { get; set; } | ||
| 21 | + public string? PurchaseNo { get; set; } | ||
| 22 | + public string? SupplierName { get; set; } | ||
| 23 | + public string? CreatedTime { get; set; } | ||
| 24 | + | ||
| 12 | 25 | ||
| 13 | public OutboundFinishedPage(OutboundFinishedViewModel vm, ScanService scanSvc) | 26 | public OutboundFinishedPage(OutboundFinishedViewModel vm, ScanService scanSvc) |
| 14 | { | 27 | { |
| @@ -27,15 +40,22 @@ namespace IndustrialControl.Pages | @@ -27,15 +40,22 @@ namespace IndustrialControl.Pages | ||
| 27 | { | 40 | { |
| 28 | base.OnAppearing(); | 41 | base.OnAppearing(); |
| 29 | 42 | ||
| 30 | - // 如果带有单号参数,则加载其明细,进入图2/图3的流程 | ||
| 31 | - if (!string.IsNullOrWhiteSpace(OrderNo)) | 43 | + // ✅ 用搜索页带过来的基础信息初始化页面,并拉取两张表 |
| 44 | + if (!string.IsNullOrWhiteSpace(OutstockId)) | ||
| 32 | { | 45 | { |
| 33 | - await _vm.LoadOrderAsync(OrderNo); | 46 | + await _vm.InitializeFromSearchAsync( |
| 47 | + outstockId: OutstockId ?? "", | ||
| 48 | + outstockNo: OutstockNo ?? "", | ||
| 49 | + orderType: OrderType ?? "", | ||
| 50 | + orderTypeName: OrderTypeName ?? "", | ||
| 51 | + purchaseNo: PurchaseNo ?? "", | ||
| 52 | + supplierName: SupplierName ?? "", | ||
| 53 | + createdTime: CreatedTime ?? "" | ||
| 54 | + ); | ||
| 34 | } | 55 | } |
| 35 | - // 动态注册广播接收器(只在当前页面前台时生效) | 56 | + |
| 36 | _scanSvc.Scanned += OnScanned; | 57 | _scanSvc.Scanned += OnScanned; |
| 37 | _scanSvc.StartListening(); | 58 | _scanSvc.StartListening(); |
| 38 | - //键盘输入 | ||
| 39 | _scanSvc.Attach(ScanEntry); | 59 | _scanSvc.Attach(ScanEntry); |
| 40 | ScanEntry.Focus(); | 60 | ScanEntry.Focus(); |
| 41 | } | 61 | } |
| @@ -25,9 +25,16 @@ | @@ -25,9 +25,16 @@ | ||
| 25 | Text="{Binding SearchOrderNo}" /> | 25 | Text="{Binding SearchOrderNo}" /> |
| 26 | 26 | ||
| 27 | 27 | ||
| 28 | + <!-- 开始日期 --> | ||
| 28 | <DatePicker Grid.Row="1" Grid.Column="0" | 29 | <DatePicker Grid.Row="1" Grid.Column="0" |
| 29 | - Date="{Binding CreatedDate}" | ||
| 30 | - MinimumDate="2000-01-01" /> | 30 | + Date="{Binding StartDate}" |
| 31 | + MinimumDate="2000-01-01" | ||
| 32 | + MaximumDate="{Binding EndDate}" /> | ||
| 33 | + | ||
| 34 | + <!-- 结束日期 --> | ||
| 35 | + <DatePicker Grid.Row="1" Grid.Column="1" | ||
| 36 | + Date="{Binding EndDate}" | ||
| 37 | + MinimumDate="{Binding StartDate}" /> | ||
| 31 | <Button Grid.Row="1" Grid.Column="1" | 38 | <Button Grid.Row="1" Grid.Column="1" |
| 32 | Text="查询" | 39 | Text="查询" |
| 33 | Command="{Binding SearchCommand}" /> | 40 | Command="{Binding SearchCommand}" /> |
| @@ -38,29 +45,31 @@ | @@ -38,29 +45,31 @@ | ||
| 38 | <CollectionView Grid.Row="1" | 45 | <CollectionView Grid.Row="1" |
| 39 | ItemsSource="{Binding Orders}" | 46 | ItemsSource="{Binding Orders}" |
| 40 | SelectionMode="Single" | 47 | SelectionMode="Single" |
| 41 | - SelectionChanged="OnOrderSelected"> | 48 | + SelectedItem="{Binding SelectedOrder, Mode=TwoWay}"> |
| 42 | <CollectionView.ItemTemplate> | 49 | <CollectionView.ItemTemplate> |
| 43 | <DataTemplate> | 50 | <DataTemplate> |
| 44 | <Frame Margin="0,8,0,0" Padding="12" HasShadow="True" CornerRadius="10"> | 51 | <Frame Margin="0,8,0,0" Padding="12" HasShadow="True" CornerRadius="10"> |
| 45 | <!-- ⭐ 点击整卡片触发命令 --> | 52 | <!-- ⭐ 点击整卡片触发命令 --> |
| 46 | <Frame.GestureRecognizers> | 53 | <Frame.GestureRecognizers> |
| 47 | <TapGestureRecognizer | 54 | <TapGestureRecognizer |
| 48 | - Command="{Binding BindingContext.OpenItemCommand, Source={x:Reference Page}}" | ||
| 49 | - CommandParameter="{Binding .}" /> | 55 | + Command="{Binding BindingContext.GoOutboundCommand, Source={x:Reference Page}}" |
| 56 | + CommandParameter="{Binding .}" /> | ||
| 50 | </Frame.GestureRecognizers> | 57 | </Frame.GestureRecognizers> |
| 51 | - <Grid RowDefinitions="Auto,Auto,Auto,Auto" | ||
| 52 | - ColumnDefinitions="Auto,*" ColumnSpacing="8"> | ||
| 53 | - <Label Grid.Row="0" Grid.Column="0" Text="入库单号:" FontAttributes="Bold"/> | ||
| 54 | - <Label Grid.Row="0" Grid.Column="1" Text="{Binding OrderNo}"/> | 58 | + <Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="Auto,*" ColumnSpacing="8"> |
| 59 | + <Label Grid.Row="0" Grid.Column="0" Text="出库单号:" FontAttributes="Bold"/> | ||
| 60 | + <Label Grid.Row="0" Grid.Column="1" Text="{Binding outstockNo}"/> | ||
| 61 | + | ||
| 62 | + <Label Grid.Row="1" Grid.Column="0" Text="出库单类型:" FontAttributes="Bold"/> | ||
| 63 | + <Label Grid.Row="1" Grid.Column="1" Text="{Binding orderTypeName}" /> | ||
| 55 | 64 | ||
| 56 | - <Label Grid.Row="1" Grid.Column="0" Text="入库类型:" FontAttributes="Bold"/> | ||
| 57 | - <Label Grid.Row="1" Grid.Column="1" Text="{Binding InboundType}" /> | 65 | + <Label Grid.Row="2" Grid.Column="0" Text="关联发货号:" FontAttributes="Bold"/> |
| 66 | + <Label Grid.Row="2" Grid.Column="1" Text="{Binding deliveryNo}" /> | ||
| 58 | 67 | ||
| 59 | - <Label Grid.Row="2" Grid.Column="0" Text="供应商:" FontAttributes="Bold"/> | ||
| 60 | - <Label Grid.Row="2" Grid.Column="1" Text="{Binding Supplier}" /> | 68 | + <Label Grid.Row="3" Grid.Column="0" Text="关联销售号:" FontAttributes="Bold"/> |
| 69 | + <Label Grid.Row="3" Grid.Column="1" Text="{Binding arrivalNo}" /> | ||
| 61 | 70 | ||
| 62 | - <Label Grid.Row="3" Grid.Column="0" Text="创建日期:" FontAttributes="Bold"/> | ||
| 63 | - <Label Grid.Row="3" Grid.Column="1" Text="{Binding CreatedAt, StringFormat='{0:yyyy-M-d}'}"/> | 71 | + <Label Grid.Row="4" Grid.Column="0" Text="创建日期:" FontAttributes="Bold"/> |
| 72 | + <Label Grid.Row="4" Grid.Column="1" Text="{Binding createdTime}" /> | ||
| 64 | </Grid> | 73 | </Grid> |
| 65 | </Frame> | 74 | </Frame> |
| 66 | </DataTemplate> | 75 | </DataTemplate> |
| @@ -58,18 +58,5 @@ public partial class OutboundFinishedSearchPage : ContentPage | @@ -58,18 +58,5 @@ public partial class OutboundFinishedSearchPage : ContentPage | ||
| 58 | _vm.SearchOrderNo = data; | 58 | _vm.SearchOrderNo = data; |
| 59 | }); | 59 | }); |
| 60 | } | 60 | } |
| 61 | - private async void OnOrderSelected(object sender, SelectionChangedEventArgs e) | ||
| 62 | - { | ||
| 63 | - var item = e.CurrentSelection?.FirstOrDefault() as InboundOrderSummary; | ||
| 64 | - if (item is null) return; | ||
| 65 | - | ||
| 66 | - // 二选一:A) 点选跳转到入库页 | ||
| 67 | - await Shell.Current.GoToAsync( | ||
| 68 | - $"{nameof(OutboundFinishedPage)}?orderNo={Uri.EscapeDataString(item.OrderNo)}"); | ||
| 69 | 61 | ||
| 70 | - // 或 B) 只把单号写到输入框/VM(不跳转) | ||
| 71 | - // if (BindingContext is InboundMaterialSearchViewModel vm) vm.SearchOrderNo = item.OrderNo; | ||
| 72 | - | ||
| 73 | - ((CollectionView)sender).SelectedItem = null; // 清除选中高亮 | ||
| 74 | - } | ||
| 75 | } | 62 | } |
| @@ -136,12 +136,68 @@ | @@ -136,12 +136,68 @@ | ||
| 136 | </CollectionView> | 136 | </CollectionView> |
| 137 | </Grid> | 137 | </Grid> |
| 138 | 138 | ||
| 139 | - <!-- 底部按钮 --> | ||
| 140 | <Grid Grid.Row="4" ColumnDefinitions="*,*,*" Padding="16,8" ColumnSpacing="10"> | 139 | <Grid Grid.Row="4" ColumnDefinitions="*,*,*" Padding="16,8" ColumnSpacing="10"> |
| 141 | - <Button Text="扫描通过" BackgroundColor="#4CAF50" TextColor="White" Clicked="OnPassScanClicked"/> | ||
| 142 | - <Button Grid.Column="1" Text="取消扫描" BackgroundColor="#F44336" TextColor="White" Clicked="OnCancelScanClicked"/> | ||
| 143 | - <Button Grid.Column="2" Text="确认出库" BackgroundColor="#2196F3" TextColor="White" Clicked="OnConfirmClicked"/> | 140 | + |
| 141 | + <!-- 扫描通过 --> | ||
| 142 | + <Grid BackgroundColor="#4CAF50" | ||
| 143 | + HorizontalOptions="Fill" | ||
| 144 | + VerticalOptions="Fill" | ||
| 145 | + HeightRequest="50"> | ||
| 146 | + <Grid.GestureRecognizers> | ||
| 147 | + <TapGestureRecognizer Command="{Binding PassScanCommand}" /> | ||
| 148 | + </Grid.GestureRecognizers> | ||
| 149 | + <StackLayout Orientation="Horizontal" | ||
| 150 | + HorizontalOptions="Center" | ||
| 151 | + VerticalOptions="Center"> | ||
| 152 | + <Image Source="pass.png" HeightRequest="20" WidthRequest="20" /> | ||
| 153 | + <Label Text="扫描通过" | ||
| 154 | + Margin="5,0,0,0" | ||
| 155 | + VerticalOptions="Center" | ||
| 156 | + TextColor="White" /> | ||
| 157 | + </StackLayout> | ||
| 158 | + </Grid> | ||
| 159 | + | ||
| 160 | + <!-- 取消扫描 --> | ||
| 161 | + <Grid Grid.Column="1" | ||
| 162 | + BackgroundColor="#F44336" | ||
| 163 | + HorizontalOptions="Fill" | ||
| 164 | + VerticalOptions="Fill" | ||
| 165 | + HeightRequest="50"> | ||
| 166 | + <Grid.GestureRecognizers> | ||
| 167 | + <TapGestureRecognizer Command="{Binding CancelScanCommand}" /> | ||
| 168 | + </Grid.GestureRecognizers> | ||
| 169 | + <StackLayout Orientation="Horizontal" | ||
| 170 | + HorizontalOptions="Center" | ||
| 171 | + VerticalOptions="Center"> | ||
| 172 | + <Image Source="cancel.png" HeightRequest="20" WidthRequest="20" /> | ||
| 173 | + <Label Text="取消扫描" | ||
| 174 | + Margin="5,0,0,0" | ||
| 175 | + VerticalOptions="Center" | ||
| 176 | + TextColor="White" /> | ||
| 177 | + </StackLayout> | ||
| 178 | + </Grid> | ||
| 179 | + | ||
| 180 | + <!-- 确认入库 --> | ||
| 181 | + <Grid Grid.Column="2" | ||
| 182 | + BackgroundColor="#2196F3" | ||
| 183 | + HorizontalOptions="Fill" | ||
| 184 | + VerticalOptions="Fill" | ||
| 185 | + HeightRequest="50"> | ||
| 186 | + <Grid.GestureRecognizers> | ||
| 187 | + <TapGestureRecognizer Command="{Binding ConfirmCommand}" /> | ||
| 188 | + </Grid.GestureRecognizers> | ||
| 189 | + <StackLayout Orientation="Horizontal" | ||
| 190 | + HorizontalOptions="Center" | ||
| 191 | + VerticalOptions="Center"> | ||
| 192 | + <Image Source="confirm.png" HeightRequest="20" WidthRequest="20" /> | ||
| 193 | + <Label Text="确认入库" | ||
| 194 | + Margin="5,0,0,0" | ||
| 195 | + VerticalOptions="Center" | ||
| 196 | + TextColor="White" /> | ||
| 197 | + </StackLayout> | ||
| 198 | + </Grid> | ||
| 199 | + | ||
| 144 | </Grid> | 200 | </Grid> |
| 145 | 201 | ||
| 146 | </Grid> | 202 | </Grid> |
| 147 | -</ContentPage> | 203 | +</ContentPage> |
| @@ -3,12 +3,25 @@ using IndustrialControl.ViewModels; | @@ -3,12 +3,25 @@ using IndustrialControl.ViewModels; | ||
| 3 | 3 | ||
| 4 | namespace IndustrialControl.Pages | 4 | namespace IndustrialControl.Pages |
| 5 | { | 5 | { |
| 6 | - [QueryProperty(nameof(OrderNo), "orderNo")] | 6 | + [QueryProperty(nameof(OutstockId), "outstockId")] |
| 7 | + [QueryProperty(nameof(OutstockNo), "outstockNo")] | ||
| 8 | + [QueryProperty(nameof(OrderType), "orderType")] | ||
| 9 | + [QueryProperty(nameof(OrderTypeName), "orderTypeName")] | ||
| 10 | + [QueryProperty(nameof(PurchaseNo), "purchaseNo")] | ||
| 11 | + [QueryProperty(nameof(SupplierName), "supplierName")] | ||
| 12 | + [QueryProperty(nameof(CreatedTime), "createdTime")] | ||
| 7 | public partial class OutboundMaterialPage : ContentPage | 13 | public partial class OutboundMaterialPage : ContentPage |
| 8 | { | 14 | { |
| 9 | private readonly ScanService _scanSvc; | 15 | private readonly ScanService _scanSvc; |
| 10 | private readonly OutboundMaterialViewModel _vm; | 16 | private readonly OutboundMaterialViewModel _vm; |
| 11 | - public string? OrderNo { get; set; } | 17 | + public string? OutstockId { get; set; } |
| 18 | + public string? OutstockNo { get; set; } | ||
| 19 | + public string? OrderType { get; set; } | ||
| 20 | + public string? OrderTypeName { get; set; } | ||
| 21 | + public string? PurchaseNo { get; set; } | ||
| 22 | + public string? SupplierName { get; set; } | ||
| 23 | + public string? CreatedTime { get; set; } | ||
| 24 | + | ||
| 12 | 25 | ||
| 13 | public OutboundMaterialPage(OutboundMaterialViewModel vm, ScanService scanSvc) | 26 | public OutboundMaterialPage(OutboundMaterialViewModel vm, ScanService scanSvc) |
| 14 | { | 27 | { |
| @@ -27,15 +40,22 @@ namespace IndustrialControl.Pages | @@ -27,15 +40,22 @@ namespace IndustrialControl.Pages | ||
| 27 | { | 40 | { |
| 28 | base.OnAppearing(); | 41 | base.OnAppearing(); |
| 29 | 42 | ||
| 30 | - // 如果带有单号参数,则加载其明细,进入图2/图3的流程 | ||
| 31 | - if (!string.IsNullOrWhiteSpace(OrderNo)) | 43 | + // ✅ 用搜索页带过来的基础信息初始化页面,并拉取两张表 |
| 44 | + if (!string.IsNullOrWhiteSpace(OutstockId)) | ||
| 32 | { | 45 | { |
| 33 | - await _vm.LoadOrderAsync(OrderNo); | 46 | + await _vm.InitializeFromSearchAsync( |
| 47 | + outstockId: OutstockId ?? "", | ||
| 48 | + outstockNo: OutstockNo ?? "", | ||
| 49 | + orderType: OrderType ?? "", | ||
| 50 | + orderTypeName: OrderTypeName ?? "", | ||
| 51 | + purchaseNo: PurchaseNo ?? "", | ||
| 52 | + supplierName: SupplierName ?? "", | ||
| 53 | + createdTime: CreatedTime ?? "" | ||
| 54 | + ); | ||
| 34 | } | 55 | } |
| 35 | - // 动态注册广播接收器(只在当前页面前台时生效) | 56 | + |
| 36 | _scanSvc.Scanned += OnScanned; | 57 | _scanSvc.Scanned += OnScanned; |
| 37 | _scanSvc.StartListening(); | 58 | _scanSvc.StartListening(); |
| 38 | - //键盘输入 | ||
| 39 | _scanSvc.Attach(ScanEntry); | 59 | _scanSvc.Attach(ScanEntry); |
| 40 | ScanEntry.Focus(); | 60 | ScanEntry.Focus(); |
| 41 | } | 61 | } |
| @@ -86,31 +106,8 @@ namespace IndustrialControl.Pages | @@ -86,31 +106,8 @@ namespace IndustrialControl.Pages | ||
| 86 | await DisplayAlert("提示", "此按钮预留摄像头扫码;硬件扫描直接扣扳机。", "确定"); | 106 | await DisplayAlert("提示", "此按钮预留摄像头扫码;硬件扫描直接扣扳机。", "确定"); |
| 87 | } | 107 | } |
| 88 | 108 | ||
| 89 | - // 扫描通过 | ||
| 90 | - void OnPassScanClicked(object sender, EventArgs e) | ||
| 91 | - { | ||
| 92 | - _vm.PassSelectedScan(); | ||
| 93 | - } | ||
| 94 | 109 | ||
| 95 | - // 取消扫描 | ||
| 96 | - void OnCancelScanClicked(object sender, EventArgs e) | ||
| 97 | - { | ||
| 98 | - _vm.CancelSelectedScan(); | ||
| 99 | - } | ||
| 100 | 110 | ||
| 101 | - // 确认出库 | ||
| 102 | - async void OnConfirmClicked(object sender, EventArgs e) | ||
| 103 | - { | ||
| 104 | - var ok = await _vm.ConfirmOutboundAsync(); | ||
| 105 | - if (ok) | ||
| 106 | - { | ||
| 107 | - await DisplayAlert("提示", "出库成功", "确定"); | ||
| 108 | - _vm.ClearAll(); | ||
| 109 | - } | ||
| 110 | - else | ||
| 111 | - { | ||
| 112 | - await DisplayAlert("提示", "出库失败,请检查数据", "确定"); | ||
| 113 | - } | ||
| 114 | - } | 111 | + |
| 115 | } | 112 | } |
| 116 | } | 113 | } |
| @@ -25,9 +25,16 @@ | @@ -25,9 +25,16 @@ | ||
| 25 | Text="{Binding SearchOrderNo}" /> | 25 | Text="{Binding SearchOrderNo}" /> |
| 26 | 26 | ||
| 27 | 27 | ||
| 28 | + <!-- 开始日期 --> | ||
| 28 | <DatePicker Grid.Row="1" Grid.Column="0" | 29 | <DatePicker Grid.Row="1" Grid.Column="0" |
| 29 | - Date="{Binding CreatedDate}" | ||
| 30 | - MinimumDate="2000-01-01" /> | 30 | + Date="{Binding StartDate}" |
| 31 | + MinimumDate="2000-01-01" | ||
| 32 | + MaximumDate="{Binding EndDate}" /> | ||
| 33 | + | ||
| 34 | + <!-- 结束日期 --> | ||
| 35 | + <DatePicker Grid.Row="1" Grid.Column="1" | ||
| 36 | + Date="{Binding EndDate}" | ||
| 37 | + MinimumDate="{Binding StartDate}" /> | ||
| 31 | <Button Grid.Row="1" Grid.Column="1" | 38 | <Button Grid.Row="1" Grid.Column="1" |
| 32 | Text="查询" | 39 | Text="查询" |
| 33 | Command="{Binding SearchCommand}" /> | 40 | Command="{Binding SearchCommand}" /> |
| @@ -38,29 +45,31 @@ | @@ -38,29 +45,31 @@ | ||
| 38 | <CollectionView Grid.Row="1" | 45 | <CollectionView Grid.Row="1" |
| 39 | ItemsSource="{Binding Orders}" | 46 | ItemsSource="{Binding Orders}" |
| 40 | SelectionMode="Single" | 47 | SelectionMode="Single" |
| 41 | - SelectionChanged="OnOrderSelected"> | 48 | + SelectedItem="{Binding SelectedOrder, Mode=TwoWay}"> |
| 42 | <CollectionView.ItemTemplate> | 49 | <CollectionView.ItemTemplate> |
| 43 | <DataTemplate> | 50 | <DataTemplate> |
| 44 | <Frame Margin="0,8,0,0" Padding="12" HasShadow="True" CornerRadius="10"> | 51 | <Frame Margin="0,8,0,0" Padding="12" HasShadow="True" CornerRadius="10"> |
| 45 | <!-- ⭐ 点击整卡片触发命令 --> | 52 | <!-- ⭐ 点击整卡片触发命令 --> |
| 46 | <Frame.GestureRecognizers> | 53 | <Frame.GestureRecognizers> |
| 47 | <TapGestureRecognizer | 54 | <TapGestureRecognizer |
| 48 | - Command="{Binding BindingContext.OpenItemCommand, Source={x:Reference Page}}" | ||
| 49 | - CommandParameter="{Binding .}" /> | 55 | + Command="{Binding BindingContext.GoOutboundCommand, Source={x:Reference Page}}" |
| 56 | + CommandParameter="{Binding .}" /> | ||
| 50 | </Frame.GestureRecognizers> | 57 | </Frame.GestureRecognizers> |
| 51 | - <Grid RowDefinitions="Auto,Auto,Auto,Auto" | ||
| 52 | - ColumnDefinitions="Auto,*" ColumnSpacing="8"> | ||
| 53 | - <Label Grid.Row="0" Grid.Column="0" Text="入库单号:" FontAttributes="Bold"/> | ||
| 54 | - <Label Grid.Row="0" Grid.Column="1" Text="{Binding OrderNo}"/> | 58 | + <Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="Auto,*" ColumnSpacing="8"> |
| 59 | + <Label Grid.Row="0" Grid.Column="0" Text="出库单号:" FontAttributes="Bold"/> | ||
| 60 | + <Label Grid.Row="0" Grid.Column="1" Text="{Binding outstockNo}"/> | ||
| 61 | + | ||
| 62 | + <Label Grid.Row="1" Grid.Column="0" Text="出库单类型:" FontAttributes="Bold"/> | ||
| 63 | + <Label Grid.Row="1" Grid.Column="1" Text="{Binding orderTypeName}" /> | ||
| 55 | 64 | ||
| 56 | - <Label Grid.Row="1" Grid.Column="0" Text="入库类型:" FontAttributes="Bold"/> | ||
| 57 | - <Label Grid.Row="1" Grid.Column="1" Text="{Binding InboundType}" /> | 65 | + <Label Grid.Row="2" Grid.Column="0" Text="关联领料单号:" FontAttributes="Bold"/> |
| 66 | + <Label Grid.Row="2" Grid.Column="1" Text="{Binding requisitionMaterialNo}" /> | ||
| 58 | 67 | ||
| 59 | - <Label Grid.Row="2" Grid.Column="0" Text="供应商:" FontAttributes="Bold"/> | ||
| 60 | - <Label Grid.Row="2" Grid.Column="1" Text="{Binding Supplier}" /> | 68 | + <Label Grid.Row="3" Grid.Column="0" Text="关联工单号:" FontAttributes="Bold"/> |
| 69 | + <Label Grid.Row="3" Grid.Column="1" Text="{Binding workOrderNo}" /> | ||
| 61 | 70 | ||
| 62 | - <Label Grid.Row="3" Grid.Column="0" Text="创建日期:" FontAttributes="Bold"/> | ||
| 63 | - <Label Grid.Row="3" Grid.Column="1" Text="{Binding CreatedAt, StringFormat='{0:yyyy-M-d}'}"/> | 71 | + <Label Grid.Row="4" Grid.Column="0" Text="创建日期:" FontAttributes="Bold"/> |
| 72 | + <Label Grid.Row="4" Grid.Column="1" Text="{Binding createdTime}" /> | ||
| 64 | </Grid> | 73 | </Grid> |
| 65 | </Frame> | 74 | </Frame> |
| 66 | </DataTemplate> | 75 | </DataTemplate> |
| @@ -58,18 +58,5 @@ public partial class OutboundMaterialSearchPage : ContentPage | @@ -58,18 +58,5 @@ public partial class OutboundMaterialSearchPage : ContentPage | ||
| 58 | _vm.SearchOrderNo = data; | 58 | _vm.SearchOrderNo = data; |
| 59 | }); | 59 | }); |
| 60 | } | 60 | } |
| 61 | - private async void OnOrderSelected(object sender, SelectionChangedEventArgs e) | ||
| 62 | - { | ||
| 63 | - var item = e.CurrentSelection?.FirstOrDefault() as InboundOrderSummary; | ||
| 64 | - if (item is null) return; | ||
| 65 | - | ||
| 66 | - // 二选一:A) 点选跳转到入库页 | ||
| 67 | - await Shell.Current.GoToAsync( | ||
| 68 | - $"{nameof(OutboundMaterialPage)}?orderNo={Uri.EscapeDataString(item.OrderNo)}"); | ||
| 69 | 61 | ||
| 70 | - // 或 B) 只把单号写到输入框/VM(不跳转) | ||
| 71 | - // if (BindingContext is InboundMaterialSearchViewModel vm) vm.SearchOrderNo = item.OrderNo; | ||
| 72 | - | ||
| 73 | - ((CollectionView)sender).SelectedItem = null; // 清除选中高亮 | ||
| 74 | - } | ||
| 75 | } | 62 | } |
| @@ -5,9 +5,23 @@ | @@ -5,9 +5,23 @@ | ||
| 5 | "port": 9128 | 5 | "port": 9128 |
| 6 | }, | 6 | }, |
| 7 | "apiEndpoints": { | 7 | "apiEndpoints": { |
| 8 | - "login": "/normalService/pda/auth/login" | 8 | + "login": "/normalService/pda/auth/login", |
| 9 | + "workOrder": { | ||
| 10 | + "page": "/normalService/pda/pmsWorkOrder/pageWorkOrders", | ||
| 11 | + "workflow": "/normalService/pda/pmsWorkOrder/getWorkOrderWorkflow", | ||
| 12 | + "processTasks": "/normalService/pda/pmsWorkOrder/pageWorkProcessTasks" | ||
| 13 | + }, | ||
| 14 | + "inbound": { | ||
| 15 | + "list": "/normalService/pda/wmsMaterialInstock/getInStock", | ||
| 16 | + "detail": "/normalService/pda/wmsMaterialInstock/getInStockDetail", | ||
| 17 | + "scanDetail": "/normalService/pda/wmsMaterialInstock/getInStockScanDetail", | ||
| 18 | + "scanByBarcode": "/normalService/pda/wmsMaterialInstock/getInStockByBarcode", | ||
| 19 | + "scanConfirm": "/normalService/pda/wmsMaterialInstock/scanConfirm", | ||
| 20 | + "cancelScan": "/normalService/pda/wmsMaterialInstock/cancelScan", | ||
| 21 | + "confirm": "/normalService/pda/wmsMaterialInstock/confirm", | ||
| 22 | + "judgeScanAll": "/normalService/pda/wmsMaterialInstock/judgeInstockDetailScanAll" | ||
| 23 | + } | ||
| 9 | }, | 24 | }, |
| 10 | - "logging": { | ||
| 11 | - "level": "Information" | ||
| 12 | - } | 25 | + "logging": { "level": "Information" } |
| 13 | } | 26 | } |
| 27 | + |
Services/AuthHeaderHandler.cs
0 → 100644
| 1 | +// Services/AuthHeaderHandler.cs | ||
| 2 | +using System.Net; | ||
| 3 | +using System.Net.Http.Headers; | ||
| 4 | + | ||
| 5 | +namespace IndustrialControl.Services | ||
| 6 | +{ | ||
| 7 | + /// 统一为经 DI 创建的 HttpClient 附加鉴权头:token: <jwt> | ||
| 8 | + public sealed class AuthHeaderHandler : DelegatingHandler | ||
| 9 | + { | ||
| 10 | + | ||
| 11 | + | ||
| 12 | + protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | ||
| 13 | + { | ||
| 14 | + try | ||
| 15 | + { | ||
| 16 | + // 1) 取 token(按你项目实际来:SecureStorage/Preferences/ITokenProvider) | ||
| 17 | + var token = await TokenStorage.LoadAsync(); // 示例:请替换为你的实现 | ||
| 18 | + token = token?.Trim(); | ||
| 19 | + | ||
| 20 | + // 2) 写入头(只移除自己要覆盖的键) | ||
| 21 | + if (!string.IsNullOrEmpty(token)) | ||
| 22 | + { | ||
| 23 | + request.Headers.Remove("token"); | ||
| 24 | + request.Headers.Remove("satoken"); | ||
| 25 | + request.Headers.Remove("Authorization"); | ||
| 26 | + | ||
| 27 | + // 自定义头 | ||
| 28 | + request.Headers.TryAddWithoutValidation("token", token); | ||
| 29 | + | ||
| 30 | + // Sa-Token 常用 | ||
| 31 | + request.Headers.TryAddWithoutValidation("satoken", token); | ||
| 32 | + | ||
| 33 | + // JWT 常用 | ||
| 34 | + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + // 3) 日志:放在写入之后再打印,便于确认真的带上了 | ||
| 38 | + System.Diagnostics.Debug.WriteLine($"[AuthHeaderHandler] {request.RequestUri}"); | ||
| 39 | + System.Diagnostics.Debug.WriteLine( | ||
| 40 | + "[AuthHeaderHandler] headers: " + | ||
| 41 | + string.Join(" | ", request.Headers.Select(h => $"{h.Key}={string.Join(",", h.Value)}")) | ||
| 42 | + ); | ||
| 43 | + } | ||
| 44 | + catch (Exception ex) | ||
| 45 | + { | ||
| 46 | + System.Diagnostics.Debug.WriteLine($"[AuthHeaderHandler] error: {ex}"); | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + return await base.SendAsync(request, cancellationToken); | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + | ||
| 53 | + // 去引号/控制字符,并剥掉可能的 "Bearer " 前缀,得到裸 token | ||
| 54 | + private static string? CleanToken(string? raw) | ||
| 55 | + { | ||
| 56 | + if (string.IsNullOrWhiteSpace(raw)) return null; | ||
| 57 | + var s = raw.Trim().Trim('"', '\''); | ||
| 58 | + s = new string(s.Where(ch => !char.IsControl(ch)).ToArray()); | ||
| 59 | + if (s.StartsWith("Bearer", StringComparison.OrdinalIgnoreCase)) | ||
| 60 | + s = s.Substring(6).TrimStart(':', ' ', '\t'); | ||
| 61 | + s = s.Replace(" ", ""); | ||
| 62 | + return string.IsNullOrWhiteSpace(s) ? null : s; | ||
| 63 | + } | ||
| 64 | + } | ||
| 65 | +} |
Services/IInboundMaterialService.cs
0 → 100644
| 1 | +using IndustrialControl.ViewModels; | ||
| 2 | + | ||
| 3 | +namespace IndustrialControl.Services; | ||
| 4 | + | ||
| 5 | +public interface IInboundMaterialService | ||
| 6 | +{ | ||
| 7 | + | ||
| 8 | + // NEW: 查询列表(图1) | ||
| 9 | + Task<IEnumerable<InboundOrderSummary>> ListInboundOrdersAsync( | ||
| 10 | + string? orderNoOrBarcode, | ||
| 11 | + DateTime startDate, | ||
| 12 | + DateTime endDate, | ||
| 13 | + string orderType, | ||
| 14 | + string[] orderTypeList, | ||
| 15 | + CancellationToken ct = default); | ||
| 16 | + | ||
| 17 | + Task<IReadOnlyList<InboundPendingRow>> GetInStockDetailAsync(string instockId, CancellationToken ct = default); | ||
| 18 | + Task<IReadOnlyList<InboundScannedRow>> GetInStockScanDetailAsync(string instockId, CancellationToken ct = default); | ||
| 19 | + /// <summary>扫描条码入库</summary> | ||
| 20 | + Task<SimpleOk> InStockByBarcodeAsync(string instockId, string barcode, CancellationToken ct = default); | ||
| 21 | + /// <summary>PDA 扫描通过(确认当前入库单已扫描项)</summary> | ||
| 22 | + Task<SimpleOk> ScanConfirmAsync(string instockId, CancellationToken ct = default); | ||
| 23 | + | ||
| 24 | + Task<SimpleOk> CancelScanAsync(string instockId, CancellationToken ct = default); | ||
| 25 | + Task<SimpleOk> ConfirmInstockAsync(string instockId, CancellationToken ct = default); | ||
| 26 | + /// <summary>判断入库单明细是否全部扫码确认</summary> | ||
| 27 | + Task<bool> JudgeInstockDetailScanAllAsync(string instockId, CancellationToken ct = default); | ||
| 28 | +} | ||
| 29 | + | ||
| 30 | +public record InboundOrder(string OrderNo, string Supplier, string LinkedNo, int ExpectedQty); | ||
| 31 | + | ||
| 32 | +public record OutboundOrder(string OrderNo, string Supplier, string LinkedNo, int ExpectedQty); | ||
| 33 | +public record ScanItem(int Index, string Barcode, string? Bin, int Qty); | ||
| 34 | +public record SimpleOk(bool Succeeded, string? Message = null); | ||
| 35 | + | ||
| 36 | + |
Services/IOutboundMaterialService.cs
0 → 100644
| 1 | +using IndustrialControl.ViewModels; | ||
| 2 | + | ||
| 3 | +namespace IndustrialControl.Services | ||
| 4 | +{ | ||
| 5 | + public interface IOutboundMaterialService | ||
| 6 | + { | ||
| 7 | + // NEW: 查询列表(图1) | ||
| 8 | + Task<IEnumerable<OutboundOrderSummary>> ListOutboundOrdersAsync( | ||
| 9 | + string? orderNoOrBarcode, | ||
| 10 | + DateTime startDate, | ||
| 11 | + DateTime endDate, | ||
| 12 | + string orderType, | ||
| 13 | + string[] orderTypeList, | ||
| 14 | + CancellationToken ct = default); | ||
| 15 | + | ||
| 16 | + Task<IReadOnlyList<OutboundPendingRow>> GetOutStockDetailAsync(string outstockId, CancellationToken ct = default); | ||
| 17 | + Task<IReadOnlyList<OutboundScannedRow>> GetOutStockScanDetailAsync(string outstockId, CancellationToken ct = default); | ||
| 18 | + /// <summary>扫描条码入库</summary> | ||
| 19 | + Task<SimpleOk> OutStockByBarcodeAsync(string outstockId, string barcode, CancellationToken ct = default); | ||
| 20 | + /// <summary>PDA 扫描通过(确认当前入库单已扫描项)</summary> | ||
| 21 | + Task<SimpleOk> ScanConfirmAsync(string outstockId, CancellationToken ct = default); | ||
| 22 | + | ||
| 23 | + Task<SimpleOk> CancelScanAsync(string outstockId, CancellationToken ct = default); | ||
| 24 | + Task<SimpleOk> ConfirmOutstockAsync(string outstockId, CancellationToken ct = default); | ||
| 25 | + /// <summary>判断入库单明细是否全部扫码确认</summary> | ||
| 26 | + Task<bool> JudgeOutstockDetailScanAllAsync(string outstockId, CancellationToken ct = default); | ||
| 27 | + } | ||
| 28 | +} |
Services/IWorkOrderApi.cs
0 → 100644
| 1 | +using System.Net.Http.Json; | ||
| 2 | +using System.Text.Json; | ||
| 3 | +using System.Text.Json.Nodes; | ||
| 4 | + | ||
| 5 | +namespace IndustrialControl.Services | ||
| 6 | +{ | ||
| 7 | + // ===================== 接口定义 ===================== | ||
| 8 | + public interface IWorkOrderApi | ||
| 9 | + { | ||
| 10 | + // ① 工单分页列表 | ||
| 11 | + Task<WorkOrderPageResult> GetWorkOrdersAsync(WorkOrderQuery q, CancellationToken ct = default); | ||
| 12 | + | ||
| 13 | + Task<DictBundle> GetWorkOrderDictsAsync(CancellationToken ct = default); | ||
| 14 | + Task<WorkflowResp?> GetWorkOrderWorkflowAsync(string id, CancellationToken ct = default); | ||
| 15 | + Task<PageResp<ProcessTask>?> PageWorkProcessTasksAsync(string workOrderNo, int pageNo = 1, int pageSize = 50, CancellationToken ct = default); | ||
| 16 | + } | ||
| 17 | + | ||
| 18 | + // ===================== 实现 ===================== | ||
| 19 | + public class WorkOrderApi : IWorkOrderApi | ||
| 20 | + { | ||
| 21 | + private readonly HttpClient _http; | ||
| 22 | + | ||
| 23 | + // 统一由 appconfig.json 管理的端点路径 | ||
| 24 | + private readonly string _pageEndpoint; | ||
| 25 | + private readonly string _workflowEndpoint; | ||
| 26 | + private readonly string _processTasksEndpoint; | ||
| 27 | + private readonly string _dictEndpoint; | ||
| 28 | + | ||
| 29 | + public WorkOrderApi(HttpClient http, IConfigLoader configLoader) | ||
| 30 | + { | ||
| 31 | + _http = http; | ||
| 32 | + | ||
| 33 | + // 读取 appconfig.json(AppData 下的生效配置) | ||
| 34 | + JsonNode cfg = configLoader.Load(); | ||
| 35 | + | ||
| 36 | + // 优先新结构 apiEndpoints.workOrder.*;其次兼容旧键名;最后兜底硬编码 | ||
| 37 | + _pageEndpoint = | ||
| 38 | + (string?)cfg?["apiEndpoints"]?["workOrder"]?["page"] | ||
| 39 | + ?? (string?)cfg?["apiEndpoints"]?["pageWorkOrders"] | ||
| 40 | + ?? "/normalService/pda/pmsWorkOrder/pageWorkOrders"; | ||
| 41 | + | ||
| 42 | + _workflowEndpoint = | ||
| 43 | + (string?)cfg?["apiEndpoints"]?["workOrder"]?["workflow"] | ||
| 44 | + ?? (string?)cfg?["apiEndpoints"]?["getWorkOrderWorkflow"] | ||
| 45 | + ?? "/normalService/pda/pmsWorkOrder/getWorkOrderWorkflow"; | ||
| 46 | + | ||
| 47 | + _processTasksEndpoint = | ||
| 48 | + (string?)cfg?["apiEndpoints"]?["workOrder"]?["processTasks"] | ||
| 49 | + ?? (string?)cfg?["apiEndpoints"]?["pageWorkProcessTasks"] | ||
| 50 | + ?? "/normalService/pda/pmsWorkOrder/pageWorkProcessTasks"; | ||
| 51 | + _dictEndpoint = | ||
| 52 | + (string?)cfg?["apiEndpoints"]?["workOrder"]?["dictList"] | ||
| 53 | + ?? "/normalService/pda/pmsWorkOrder/getWorkOrderDictList"; | ||
| 54 | + } | ||
| 55 | + private static string BuildQuery(IDictionary<string, string> p) | ||
| 56 | + => string.Join("&", p.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}")); | ||
| 57 | + // using System.Text.Json; | ||
| 58 | + // using System.Text; | ||
| 59 | + | ||
| 60 | + public async Task<WorkOrderPageResult> GetWorkOrdersAsync(WorkOrderQuery q, CancellationToken ct = default) | ||
| 61 | + { | ||
| 62 | + // 1) 先把所有要传的参数放进字典(只加有值的) | ||
| 63 | + var p = new Dictionary<string, string> | ||
| 64 | + { | ||
| 65 | + ["pageNo"] = q.PageNo.ToString(), | ||
| 66 | + ["pageSize"] = q.PageSize.ToString() | ||
| 67 | + }; | ||
| 68 | + if (q.CreatedTimeStart.HasValue) p["createdTimeStart"] = q.CreatedTimeStart.Value.ToString("yyyy-MM-dd HH:mm:ss"); | ||
| 69 | + if (q.CreatedTimeEnd.HasValue) p["createdTimeEnd"] = q.CreatedTimeEnd.Value.ToString("yyyy-MM-dd HH:mm:ss"); | ||
| 70 | + if (!string.IsNullOrWhiteSpace(q.WorkOrderNo)) p["workOrderNo"] = q.WorkOrderNo!.Trim(); | ||
| 71 | + if (!string.IsNullOrWhiteSpace(q.MaterialName)) p["materialName"] = q.MaterialName!.Trim(); | ||
| 72 | + | ||
| 73 | + // 2) 逐项进行 Uri.EscapeDataString 编码,避免出现“空格没编码”的情况 | ||
| 74 | + string qs = string.Join("&", p.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}")); | ||
| 75 | + | ||
| 76 | + var url = _pageEndpoint + "?" + qs; | ||
| 77 | + | ||
| 78 | + using var req = new HttpRequestMessage(HttpMethod.Get, url); | ||
| 79 | + System.Diagnostics.Debug.WriteLine("[WorkOrderApi] GET " + (_http.BaseAddress?.ToString() ?? "") + url); | ||
| 80 | + | ||
| 81 | + using var httpResp = await _http.SendAsync(req, ct); | ||
| 82 | + var json = await httpResp.Content.ReadAsStringAsync(ct); | ||
| 83 | + System.Diagnostics.Debug.WriteLine("[WorkOrderApi] Resp: " + json[..Math.Min(300, json.Length)] + "..."); | ||
| 84 | + | ||
| 85 | + if (!httpResp.IsSuccessStatusCode) | ||
| 86 | + return new WorkOrderPageResult { success = false, message = $"HTTP {(int)httpResp.StatusCode}" }; | ||
| 87 | + | ||
| 88 | + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; | ||
| 89 | + var resp = JsonSerializer.Deserialize<WorkOrderPageResult>(json, options) ?? new WorkOrderPageResult(); | ||
| 90 | + | ||
| 91 | + // 兼容 result.records | ||
| 92 | + var nested = resp.result?.records; | ||
| 93 | + if (nested is not null && resp.result is not null) | ||
| 94 | + { | ||
| 95 | + if (resp.result.records is null || resp.result.records.Count == 0) | ||
| 96 | + resp.result.records = nested; | ||
| 97 | + if (resp.result.pageNo == 0) resp.result.pageNo = resp.result.list.pageNo; | ||
| 98 | + if (resp.result.pageSize == 0) resp.result.pageSize = resp.result.list.pageSize; | ||
| 99 | + if (resp.result.total == 0) resp.result.total = resp.result.list.total; | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + return resp; | ||
| 103 | + } | ||
| 104 | + | ||
| 105 | + | ||
| 106 | + /// <summary> | ||
| 107 | + /// 工单流程:/getWorkOrderWorkflow?id=... | ||
| 108 | + /// 返回 result 为数组(statusValue/statusName/statusTime) | ||
| 109 | + /// </summary> | ||
| 110 | + public async Task<WorkflowResp?> GetWorkOrderWorkflowAsync(string id, CancellationToken ct = default) | ||
| 111 | + { | ||
| 112 | + var p = new Dictionary<string, string> { ["id"] = id?.Trim() ?? "" }; | ||
| 113 | + var url = _workflowEndpoint + "?" + BuildQuery(p); | ||
| 114 | + | ||
| 115 | + using var req = new HttpRequestMessage(HttpMethod.Get, url); | ||
| 116 | + System.Diagnostics.Debug.WriteLine("[WorkOrderApi] GET " + url); | ||
| 117 | + | ||
| 118 | + using var httpResp = await _http.SendAsync(req, ct); | ||
| 119 | + var json = await httpResp.Content.ReadAsStringAsync(ct); | ||
| 120 | + System.Diagnostics.Debug.WriteLine("[WorkOrderApi] Resp(getWorkOrderWorkflow): " + json[..Math.Min(300, json.Length)] + "..."); | ||
| 121 | + | ||
| 122 | + if (!httpResp.IsSuccessStatusCode) | ||
| 123 | + return new WorkflowResp { success = false, message = $"HTTP {(int)httpResp.StatusCode}" }; | ||
| 124 | + | ||
| 125 | + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; | ||
| 126 | + var resp = JsonSerializer.Deserialize<WorkflowResp>(json, options) ?? new WorkflowResp(); | ||
| 127 | + return resp; | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + /// <summary> | ||
| 131 | + /// 工序分页:/pageWorkProcessTasks?pageNo=&pageSize=&workOrderNo= | ||
| 132 | + /// 返回分页结构,数据在 result.records[] | ||
| 133 | + /// </summary> | ||
| 134 | + public async Task<PageResp<ProcessTask>?> PageWorkProcessTasksAsync( | ||
| 135 | + string workOrderNo, int pageNo = 1, int pageSize = 50, CancellationToken ct = default) | ||
| 136 | + { | ||
| 137 | + var p = new Dictionary<string, string> | ||
| 138 | + { | ||
| 139 | + ["pageNo"] = pageNo.ToString(), | ||
| 140 | + ["pageSize"] = pageSize.ToString() | ||
| 141 | + }; | ||
| 142 | + if (!string.IsNullOrWhiteSpace(workOrderNo)) p["workOrderNo"] = workOrderNo.Trim(); | ||
| 143 | + | ||
| 144 | + var url = _processTasksEndpoint + "?" + BuildQuery(p); | ||
| 145 | + | ||
| 146 | + using var req = new HttpRequestMessage(HttpMethod.Get, url); | ||
| 147 | + System.Diagnostics.Debug.WriteLine("[WorkOrderApi] GET " + url); | ||
| 148 | + | ||
| 149 | + using var httpResp = await _http.SendAsync(req, ct); | ||
| 150 | + var json = await httpResp.Content.ReadAsStringAsync(ct); | ||
| 151 | + System.Diagnostics.Debug.WriteLine("[WorkOrderApi] Resp(pageWorkProcessTasks): " + json[..Math.Min(300, json.Length)] + "..."); | ||
| 152 | + | ||
| 153 | + if (!httpResp.IsSuccessStatusCode) | ||
| 154 | + return new PageResp<ProcessTask> { success = false, message = $"HTTP {(int)httpResp.StatusCode}" }; | ||
| 155 | + | ||
| 156 | + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; | ||
| 157 | + var resp = JsonSerializer.Deserialize<PageResp<ProcessTask>>(json, options) ?? new PageResp<ProcessTask>(); | ||
| 158 | + | ||
| 159 | + // 兼容 result.records(你的实际返回就是 records,结构示例如你发的 JSON) | ||
| 160 | + // 如果后端某些场景包在 result.list.records,也一并兼容 | ||
| 161 | + var nested = resp.result?.records ?? resp.result?.records; | ||
| 162 | + if (nested is not null && resp.result is not null) | ||
| 163 | + { | ||
| 164 | + if (resp.result.records is null || resp.result.records.Count == 0) | ||
| 165 | + resp.result.records = nested; | ||
| 166 | + | ||
| 167 | + if (resp.result.pageNo == 0 && resp.result is not null) resp.result.pageNo = resp.result.pageNo; | ||
| 168 | + if (resp.result.pageSize == 0 && resp.result is not null) resp.result.pageSize = resp.result.pageSize; | ||
| 169 | + if (resp.result.total == 0 && resp.result is not null) resp.result.total = resp.result.total; | ||
| 170 | + } | ||
| 171 | + | ||
| 172 | + return resp; | ||
| 173 | + } | ||
| 174 | + | ||
| 175 | +public async Task<DictBundle> GetWorkOrderDictsAsync(CancellationToken ct = default) | ||
| 176 | + { | ||
| 177 | + using var req = new HttpRequestMessage(HttpMethod.Get, _dictEndpoint); | ||
| 178 | + using var res = await _http.SendAsync(req, ct); | ||
| 179 | + var json = await res.Content.ReadAsStringAsync(ct); | ||
| 180 | + | ||
| 181 | + var opt = new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }; | ||
| 182 | + var dto = System.Text.Json.JsonSerializer.Deserialize<DictResponse>(json, opt); | ||
| 183 | + | ||
| 184 | + var all = dto?.result ?? new List<DictField>(); | ||
| 185 | + var audit = all.FirstOrDefault(f => string.Equals(f.field, "auditStatus", StringComparison.OrdinalIgnoreCase)) | ||
| 186 | + ?.dictItems ?? new List<DictItem>(); | ||
| 187 | + var urgent = all.FirstOrDefault(f => string.Equals(f.field, "urgent", StringComparison.OrdinalIgnoreCase)) | ||
| 188 | + ?.dictItems ?? new List<DictItem>(); | ||
| 189 | + | ||
| 190 | + return new DictBundle { AuditStatus = audit, Urgent = urgent }; | ||
| 191 | + } | ||
| 192 | + | ||
| 193 | + | ||
| 194 | + } | ||
| 195 | + | ||
| 196 | + // ===================== 请求模型 ===================== | ||
| 197 | + public class WorkOrderQuery | ||
| 198 | + { | ||
| 199 | + public int PageNo { get; set; } = 1; | ||
| 200 | + public int PageSize { get; set; } = 50; | ||
| 201 | + | ||
| 202 | + // 0 待执行;1 执行中;2 入库中;3 已完成 | ||
| 203 | + public string? AuditStatus { get; set; } | ||
| 204 | + | ||
| 205 | + public DateTime? CreatedTimeStart { get; set; } | ||
| 206 | + public DateTime? CreatedTimeEnd { get; set; } | ||
| 207 | + | ||
| 208 | + public string? WorkOrderNo { get; set; } | ||
| 209 | + public string? MaterialName { get; set; } | ||
| 210 | + } | ||
| 211 | + | ||
| 212 | + // ===================== 返回模型:分页 ===================== | ||
| 213 | + // 顶层:{ code, message, success, result: {...}, costTime } | ||
| 214 | + public class WorkOrderPageResult | ||
| 215 | + { | ||
| 216 | + public int code { get; set; } | ||
| 217 | + public string? message { get; set; } | ||
| 218 | + public bool success { get; set; } | ||
| 219 | + public WorkOrderPageData? result { get; set; } | ||
| 220 | + public long costTime { get; set; } | ||
| 221 | + } | ||
| 222 | + | ||
| 223 | + // result 可能是 {pageNo,pageSize,records} 或 {list:{pageNo,pageSize,records}} | ||
| 224 | + public class WorkOrderPageData | ||
| 225 | + { | ||
| 226 | + public WorkOrderPageList? list { get; set; } | ||
| 227 | + | ||
| 228 | + public int pageNo { get; set; } | ||
| 229 | + public int pageSize { get; set; } | ||
| 230 | + public long total { get; set; } | ||
| 231 | + public List<WorkOrderRecord> records { get; set; } = new(); | ||
| 232 | + } | ||
| 233 | + | ||
| 234 | + public class WorkOrderPageList | ||
| 235 | + { | ||
| 236 | + public int pageNo { get; set; } | ||
| 237 | + public int pageSize { get; set; } | ||
| 238 | + public long total { get; set; } | ||
| 239 | + public List<WorkOrderRecord> records { get; set; } = new(); | ||
| 240 | + } | ||
| 241 | + | ||
| 242 | + // 只列页面需要字段;后续按实际补 | ||
| 243 | + public class WorkOrderRecord | ||
| 244 | + { | ||
| 245 | + public string? id { get; set; } | ||
| 246 | + public string? workOrderNo { get; set; } | ||
| 247 | + public string? workOrderName { get; set; } | ||
| 248 | + | ||
| 249 | + public string? auditStatus { get; set; } // ★ "1" 这样的字符串 | ||
| 250 | + | ||
| 251 | + public decimal? curQty { get; set; } | ||
| 252 | + | ||
| 253 | + public string? materialCode { get; set; } | ||
| 254 | + public string? materialName { get; set; } | ||
| 255 | + | ||
| 256 | + public string? line { get; set; } | ||
| 257 | + public string? lineName { get; set; } | ||
| 258 | + public string? workShop { get; set; } | ||
| 259 | + public string? workShopName { get; set; } | ||
| 260 | + public string? urgent { get; set; } | ||
| 261 | + | ||
| 262 | + // ★ 这些时间都是 "yyyy-MM-dd HH:mm:ss" 字符串 | ||
| 263 | + public string? schemeStartDate { get; set; } | ||
| 264 | + public string? schemeEndDate { get; set; } | ||
| 265 | + public string? createdTime { get; set; } | ||
| 266 | + public string? modifiedTime { get; set; } | ||
| 267 | + public string? commitedTime { get; set; } | ||
| 268 | + | ||
| 269 | + public string? bomCode { get; set; } | ||
| 270 | + public string? routeName { get; set; } | ||
| 271 | + } | ||
| 272 | + | ||
| 273 | + // ===================== 返回模型:工作流 ===================== | ||
| 274 | + public class WorkOrderWorkflowResp | ||
| 275 | + { | ||
| 276 | + public int code { get; set; } | ||
| 277 | + public long costTime { get; set; } | ||
| 278 | + public string? message { get; set; } | ||
| 279 | + public bool success { get; set; } | ||
| 280 | + public WorkOrderWorkflow? result { get; set; } | ||
| 281 | + } | ||
| 282 | + | ||
| 283 | + public class WorkOrderWorkflow | ||
| 284 | + { | ||
| 285 | + public string? statusName { get; set; } // 待执行/执行中/入库中/已完成 | ||
| 286 | + public string? statusTime { get; set; } // 时间字符串 | ||
| 287 | + public int? statusValue { get; set; } // 0/1/2/3 | ||
| 288 | + } | ||
| 289 | + | ||
| 290 | + // ===================== 返回模型:工序节点 ===================== | ||
| 291 | + public class ProcessTasksPageResult | ||
| 292 | + { | ||
| 293 | + public int code { get; set; } | ||
| 294 | + public string? message { get; set; } | ||
| 295 | + public bool success { get; set; } | ||
| 296 | + public long costTime { get; set; } | ||
| 297 | + public ProcessTasksList? result { get; set; } | ||
| 298 | + } | ||
| 299 | + | ||
| 300 | + public class ProcessTasksList | ||
| 301 | + { | ||
| 302 | + public int pageNo { get; set; } | ||
| 303 | + public int pageSize { get; set; } | ||
| 304 | + public long total { get; set; } | ||
| 305 | + public List<ProcessTaskRecord> records { get; set; } = new(); | ||
| 306 | + } | ||
| 307 | + | ||
| 308 | + public class ProcessTaskRecord | ||
| 309 | + { | ||
| 310 | + public string? processName { get; set; } | ||
| 311 | + public string? startDate { get; set; } | ||
| 312 | + public string? endDate { get; set; } | ||
| 313 | + public int? sortNumber { get; set; } | ||
| 314 | + } | ||
| 315 | + public class DictResponse | ||
| 316 | + { | ||
| 317 | + public bool success { get; set; } | ||
| 318 | + public string? message { get; set; } | ||
| 319 | + public int code { get; set; } | ||
| 320 | + public List<DictField>? result { get; set; } | ||
| 321 | + public long costTime { get; set; } | ||
| 322 | + } | ||
| 323 | + | ||
| 324 | + public class DictField | ||
| 325 | + { | ||
| 326 | + public string? field { get; set; } | ||
| 327 | + public List<DictItem> dictItems { get; set; } = new(); | ||
| 328 | + } | ||
| 329 | + | ||
| 330 | + public class DictItem | ||
| 331 | + { | ||
| 332 | + public string? dictItemValue { get; set; } // 参数值("0"/"1"/"2"/"3"/"4"...) | ||
| 333 | + public string? dictItemName { get; set; } // 显示名("待执行"/"执行中"...) | ||
| 334 | + } | ||
| 335 | + public class DictBundle | ||
| 336 | + { | ||
| 337 | + public List<DictItem> AuditStatus { get; set; } = new(); | ||
| 338 | + public List<DictItem> Urgent { get; set; } = new(); | ||
| 339 | + } | ||
| 340 | +public sealed class WorkflowResp { public bool success { get; set; } public string? message { get; set; } public int code { get; set; } public List<WorkflowItem>? result { get; set; } } | ||
| 341 | +public sealed class WorkflowItem { public string? statusValue { get; set; } public string? statusName { get; set; } public string? statusTime { get; set; } } | ||
| 342 | + | ||
| 343 | +public sealed class PageResp<T> { public bool success { get; set; } public string? message { get; set; } public int code { get; set; } public PageResult<T>? result { get; set; } } | ||
| 344 | +public sealed class PageResult<T> { public int pageNo { get; set; } public int pageSize { get; set; } public int total { get; set; } public List<T>? records { get; set; } } | ||
| 345 | + | ||
| 346 | +public sealed class ProcessTask | ||
| 347 | +{ | ||
| 348 | + public string? id { get; set; } | ||
| 349 | + public string? processCode { get; set; } | ||
| 350 | + public string? processName { get; set; } | ||
| 351 | + public decimal? scheQty { get; set; } | ||
| 352 | + public decimal? completedQty { get; set; } | ||
| 353 | + public string? startDate { get; set; } | ||
| 354 | + public string? endDate { get; set; } | ||
| 355 | + public int? sortNumber { get; set; } | ||
| 356 | + public string? auditStatus { get; set; } | ||
| 357 | +} | ||
| 358 | +} |
Services/InboundMaterialService.cs
0 → 100644
| 1 | +// 文件:Services/WarehouseDataApiService.cs | ||
| 2 | +using System.Net.Http; | ||
| 3 | +using System.Text; | ||
| 4 | +using System.Text.Json; | ||
| 5 | +using System.Text.Json.Nodes; | ||
| 6 | +using IndustrialControl.ViewModels; | ||
| 7 | + | ||
| 8 | +namespace IndustrialControl.Services; | ||
| 9 | + | ||
| 10 | +/// <summary> | ||
| 11 | +/// 真实接口实现,风格对齐 WorkOrderApi | ||
| 12 | +/// </summary> | ||
| 13 | +public sealed class InboundMaterialService : IInboundMaterialService | ||
| 14 | +{ | ||
| 15 | + public readonly HttpClient _http; | ||
| 16 | + public readonly string _inboundListEndpoint; | ||
| 17 | + public readonly string _detailEndpoint; | ||
| 18 | + public readonly string _scanDetailEndpoint; | ||
| 19 | + // 新增:扫码入库端点 | ||
| 20 | + public readonly string _scanByBarcodeEndpoint; | ||
| 21 | + public readonly string _scanConfirmEndpoint; | ||
| 22 | + public readonly string _cancelScanEndpoint; | ||
| 23 | + public readonly string _confirmInstockEndpoint; | ||
| 24 | + public readonly string _judgeScanAllEndpoint; | ||
| 25 | + | ||
| 26 | + public InboundMaterialService(HttpClient http, IConfigLoader configLoader) | ||
| 27 | + { | ||
| 28 | + _http = http; | ||
| 29 | + | ||
| 30 | + JsonNode cfg = configLoader.Load(); | ||
| 31 | + | ||
| 32 | + // ⭐ 新增:读取 baseUrl 或 ip+port | ||
| 33 | + var baseUrl = | ||
| 34 | + (string?)cfg?["server"]?["baseUrl"] | ||
| 35 | + ?? BuildBaseUrl(cfg?["server"]?["ipAddress"], cfg?["server"]?["port"]); | ||
| 36 | + | ||
| 37 | + if (string.IsNullOrWhiteSpace(baseUrl)) | ||
| 38 | + throw new InvalidOperationException("后端基础地址未配置:请在 appconfig.json 配置 server.baseUrl 或 server.ipAddress + server.port"); | ||
| 39 | + | ||
| 40 | + if (_http.BaseAddress is null) | ||
| 41 | + _http.BaseAddress = new Uri(baseUrl, UriKind.Absolute); | ||
| 42 | + | ||
| 43 | + // 下面保持原来的相对路径读取(不变) | ||
| 44 | + _inboundListEndpoint = | ||
| 45 | + (string?)cfg?["apiEndpoints"]?["inbound"]?["list"] ?? | ||
| 46 | + (string?)cfg?["apiEndpoints"]?["getInStock"] ?? | ||
| 47 | + "/normalService/pda/wmsMaterialInstock/getInStock"; | ||
| 48 | + | ||
| 49 | + _detailEndpoint = | ||
| 50 | + (string?)cfg?["apiEndpoints"]?["inbound"]?["detail"] ?? | ||
| 51 | + "/normalService/pda/wmsMaterialInstock/getInStockDetail"; | ||
| 52 | + | ||
| 53 | + _scanDetailEndpoint = | ||
| 54 | + (string?)cfg?["apiEndpoints"]?["inbound"]?["scanDetail"] ?? | ||
| 55 | + "/normalService/pda/wmsMaterialInstock/getInStockScanDetail"; | ||
| 56 | + | ||
| 57 | + _scanByBarcodeEndpoint = | ||
| 58 | + (string?)cfg?["apiEndpoints"]?["inbound"]?["scanByBarcode"] ?? | ||
| 59 | + "/normalService/pda/wmsMaterialInstock/getInStockByBarcode"; | ||
| 60 | + | ||
| 61 | + _scanConfirmEndpoint = | ||
| 62 | + (string?)cfg?["apiEndpoints"]?["inbound"]?["scanConfirm"] ?? | ||
| 63 | + "/normalService/pda/wmsMaterialInstock/scanConfirm"; | ||
| 64 | + | ||
| 65 | + _cancelScanEndpoint = | ||
| 66 | + (string?)cfg?["apiEndpoints"]?["inbound"]?["cancelScan"] ?? | ||
| 67 | + "/normalService/pda/wmsMaterialInstock/cancelScan"; | ||
| 68 | + | ||
| 69 | + _confirmInstockEndpoint = | ||
| 70 | + (string?)cfg?["apiEndpoints"]?["inbound"]?["confirm"] ?? | ||
| 71 | + "/normalService/pda/wmsMaterialInstock/confirm"; | ||
| 72 | + | ||
| 73 | + _judgeScanAllEndpoint = | ||
| 74 | + (string?)cfg?["apiEndpoints"]?["inbound"]?["judgeScanAll"] ?? | ||
| 75 | + "/normalService/pda/wmsMaterialInstock/judgeInstockDetailScanAll"; | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + // ⭐ 新增:拼接 ip + port → baseUrl | ||
| 79 | + private static string? BuildBaseUrl(JsonNode? ipNode, JsonNode? portNode) | ||
| 80 | + { | ||
| 81 | + string? ip = ipNode?.ToString().Trim(); | ||
| 82 | + string? port = portNode?.ToString().Trim(); | ||
| 83 | + | ||
| 84 | + if (string.IsNullOrWhiteSpace(ip)) return null; | ||
| 85 | + | ||
| 86 | + // 如果没带 http:// 或 https://,默认 http:// | ||
| 87 | + var hasScheme = ip.StartsWith("http://", StringComparison.OrdinalIgnoreCase) | ||
| 88 | + || ip.StartsWith("https://", StringComparison.OrdinalIgnoreCase); | ||
| 89 | + var host = hasScheme ? ip : $"http://{ip}"; | ||
| 90 | + | ||
| 91 | + return string.IsNullOrEmpty(port) ? host : $"{host}:{port}"; | ||
| 92 | + } | ||
| 93 | + | ||
| 94 | + | ||
| 95 | + public async Task<IEnumerable<InboundOrderSummary>> ListInboundOrdersAsync( | ||
| 96 | + string? orderNoOrBarcode, | ||
| 97 | + DateTime startDate, | ||
| 98 | + DateTime endDate, | ||
| 99 | + string orderType, | ||
| 100 | + string[] orderTypeList, | ||
| 101 | + CancellationToken ct = default) | ||
| 102 | + { | ||
| 103 | + // 结束时间扩到当天 23:59:59,避免把当日数据排除 | ||
| 104 | + var begin = startDate.ToString("yyyy-MM-dd 00:00:00"); | ||
| 105 | + var end = endDate.ToString("yyyy-MM-dd 23:59:59"); | ||
| 106 | + | ||
| 107 | + // 用 KVP 列表(不要 Dictionary)→ 规避 WinRT generic + AOT 警告 | ||
| 108 | + var pairs = new List<KeyValuePair<string, string>> | ||
| 109 | + { | ||
| 110 | + new("createdTimeBegin", begin), | ||
| 111 | + new("createdTimeEnd", end), | ||
| 112 | + new("pageNo", "1"), | ||
| 113 | + new("pageSize","10") | ||
| 114 | + // 如需统计总数:new("searchCount", "true") | ||
| 115 | + }; | ||
| 116 | + | ||
| 117 | + if (!string.IsNullOrWhiteSpace(orderNoOrBarcode)) | ||
| 118 | + pairs.Add(new("instockNo", orderNoOrBarcode.Trim())); | ||
| 119 | + | ||
| 120 | + if (!string.IsNullOrWhiteSpace(orderType)) | ||
| 121 | + pairs.Add(new("orderType", orderType)); | ||
| 122 | + | ||
| 123 | + if (orderTypeList is { Length: > 0 }) | ||
| 124 | + pairs.Add(new("orderTypeList", string.Join(",", orderTypeList))); | ||
| 125 | + | ||
| 126 | + // 交给 BCL 编码(比手写 Escape 安全) | ||
| 127 | + using var form = new FormUrlEncodedContent(pairs); | ||
| 128 | + var qs = await form.ReadAsStringAsync(ct); | ||
| 129 | + var url = _inboundListEndpoint + "?" + qs; | ||
| 130 | + | ||
| 131 | + using var req = new HttpRequestMessage(HttpMethod.Get, url); | ||
| 132 | + using var resp = await _http.SendAsync(req, ct); | ||
| 133 | + var json = await resp.Content.ReadAsStringAsync(ct); | ||
| 134 | + | ||
| 135 | + if (!resp.IsSuccessStatusCode) | ||
| 136 | + return Enumerable.Empty<InboundOrderSummary>(); | ||
| 137 | + | ||
| 138 | + var opt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; | ||
| 139 | + var dto = JsonSerializer.Deserialize<GetInStockPageResp>(json, opt); | ||
| 140 | + | ||
| 141 | + var records = dto?.result?.records; | ||
| 142 | + if (dto?.success != true || records is null || records.Count == 0) | ||
| 143 | + return Enumerable.Empty<InboundOrderSummary>(); | ||
| 144 | + | ||
| 145 | + return records.Select(x => new InboundOrderSummary( | ||
| 146 | + instockId: x.id ?? "", | ||
| 147 | + instockNo: x.instockNo ?? "", | ||
| 148 | + orderType: x.orderType ?? "", | ||
| 149 | + orderTypeName: x.orderTypeName ?? "", | ||
| 150 | + purchaseNo: x.purchaseNo ?? "", | ||
| 151 | + arrivalNo: x.arrivalNo ?? "", | ||
| 152 | + supplierName: x.supplierName ?? "", | ||
| 153 | + createdTime: x.createdTime ?? "" | ||
| 154 | + )); | ||
| 155 | + } | ||
| 156 | + | ||
| 157 | + | ||
| 158 | + public async Task<IReadOnlyList<InboundPendingRow>> GetInStockDetailAsync( | ||
| 159 | + string instockId, CancellationToken ct = default) | ||
| 160 | + { | ||
| 161 | + // ✅ 文档为 GET + x-www-form-urlencoded,参数名是小写 instockId | ||
| 162 | + var url = $"{_detailEndpoint}?instockId={Uri.EscapeDataString(instockId)}"; | ||
| 163 | + using var req = new HttpRequestMessage(HttpMethod.Get, url); | ||
| 164 | + | ||
| 165 | + using var res = await _http.SendAsync(req, ct).ConfigureAwait(false); | ||
| 166 | + res.EnsureSuccessStatusCode(); | ||
| 167 | + | ||
| 168 | + var json = await res.Content.ReadAsStringAsync(ct).ConfigureAwait(false); | ||
| 169 | + var opt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; | ||
| 170 | + var dto = JsonSerializer.Deserialize<GetInStockDetailResp>(json, opt); | ||
| 171 | + | ||
| 172 | + if (dto?.success != true || dto.result is null || dto.result.Count == 0) | ||
| 173 | + return Array.Empty<InboundPendingRow>(); | ||
| 174 | + | ||
| 175 | + static int ToIntSafe(string? s) | ||
| 176 | + { | ||
| 177 | + if (string.IsNullOrWhiteSpace(s)) return 0; | ||
| 178 | + s = s.Trim().Replace(",", ""); | ||
| 179 | + return int.TryParse(s, out var v) ? v : 0; | ||
| 180 | + } | ||
| 181 | + | ||
| 182 | + // ⚠️ 接口没有 barcode,这里先用空串;如需展示可以改成 x.materialCode 或 x.stockBatch | ||
| 183 | + var list = dto.result.Select(x => new InboundPendingRow( | ||
| 184 | + Barcode: string.Empty, // 或 $"{x.materialCode}" / $"{x.stockBatch}" | ||
| 185 | + DetailId: x.id ?? string.Empty, // ← 改为接口的 id | ||
| 186 | + Location: x.location ?? string.Empty, | ||
| 187 | + MaterialName: x.materialName ?? string.Empty, | ||
| 188 | + PendingQty: ToIntSafe(x.instockQty), // ← 预计数量 | ||
| 189 | + ScannedQty: ToIntSafe(x.qty), // ← 已扫描量 | ||
| 190 | + Spec: x.spec ?? string.Empty | ||
| 191 | + )).ToList(); | ||
| 192 | + | ||
| 193 | + return list; | ||
| 194 | + } | ||
| 195 | + | ||
| 196 | + public async Task<IReadOnlyList<InboundScannedRow>> GetInStockScanDetailAsync( | ||
| 197 | + string instockId, | ||
| 198 | + CancellationToken ct = default) | ||
| 199 | + { | ||
| 200 | + // 文档为 GET + x-www-form-urlencoded,这里用 query 传递(关键在大小写常为 InstockId) | ||
| 201 | + var url = $"{_scanDetailEndpoint}?InstockId={Uri.EscapeDataString(instockId)}"; | ||
| 202 | + using var req = new HttpRequestMessage(HttpMethod.Get, url); | ||
| 203 | + | ||
| 204 | + using var res = await _http.SendAsync(req, ct).ConfigureAwait(false); | ||
| 205 | + res.EnsureSuccessStatusCode(); | ||
| 206 | + | ||
| 207 | + var json = await res.Content.ReadAsStringAsync(ct).ConfigureAwait(false); | ||
| 208 | + | ||
| 209 | + var opt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; | ||
| 210 | + var dto = JsonSerializer.Deserialize<GetInStockScanDetailResp>(json, opt); | ||
| 211 | + | ||
| 212 | + if (dto?.success != true || dto.result is null || dto.result.Count == 0) | ||
| 213 | + return Array.Empty<InboundScannedRow>(); | ||
| 214 | + | ||
| 215 | + static int ToIntSafe(string? s) | ||
| 216 | + { | ||
| 217 | + if (string.IsNullOrWhiteSpace(s)) return 0; | ||
| 218 | + // 去除千分位、空格 | ||
| 219 | + s = s.Trim().Replace(",", ""); | ||
| 220 | + return int.TryParse(s, out var v) ? v : 0; | ||
| 221 | + } | ||
| 222 | + | ||
| 223 | + // 映射:InstockId <- id(截图注释“入库单明细主键id”) | ||
| 224 | + var list = dto.result.Select(x => new InboundScannedRow( | ||
| 225 | + Barcode: (x.barcode ?? string.Empty).Trim(), | ||
| 226 | + DetailId: (x.id ?? string.Empty).Trim(), | ||
| 227 | + Location: (x.location ?? string.Empty).Trim(), | ||
| 228 | + MaterialName: (x.materialName ?? string.Empty).Trim(), | ||
| 229 | + Qty: ToIntSafe(x.qty), | ||
| 230 | + Spec: (x.spec ?? string.Empty).Trim(), | ||
| 231 | + ScanStatus :x.scanStatus ?? false, | ||
| 232 | + WarehouseCode :x.warehouseCode?.Trim() | ||
| 233 | + )).ToList(); | ||
| 234 | + | ||
| 235 | + return list; | ||
| 236 | + } | ||
| 237 | + | ||
| 238 | + // ========= 扫码入库实现 ========= | ||
| 239 | + public async Task<SimpleOk> InStockByBarcodeAsync(string instockId, string barcode, CancellationToken ct = default) | ||
| 240 | + { | ||
| 241 | + var body = JsonSerializer.Serialize(new { barcode, instockId }); | ||
| 242 | + using var req = new HttpRequestMessage(HttpMethod.Post, _scanByBarcodeEndpoint) | ||
| 243 | + { | ||
| 244 | + Content = new StringContent(body, Encoding.UTF8, "application/json") | ||
| 245 | + }; | ||
| 246 | + | ||
| 247 | + using var res = await _http.SendAsync(req, ct); | ||
| 248 | + var json = await res.Content.ReadAsStringAsync(ct); | ||
| 249 | + var opt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; | ||
| 250 | + var dto = JsonSerializer.Deserialize<ScanByBarcodeResp>(json, opt); | ||
| 251 | + | ||
| 252 | + // 按文档:以 success 判断;message 作为失败提示 | ||
| 253 | + var ok = dto?.success == true; | ||
| 254 | + return new SimpleOk(ok, dto?.message); | ||
| 255 | + } | ||
| 256 | + public async Task<SimpleOk> ScanConfirmAsync(string instockId, CancellationToken ct = default) | ||
| 257 | + { | ||
| 258 | + var bodyJson = JsonSerializer.Serialize(new { instockId }); | ||
| 259 | + using var req = new HttpRequestMessage(HttpMethod.Post, _scanConfirmEndpoint) | ||
| 260 | + { | ||
| 261 | + Content = new StringContent(bodyJson, Encoding.UTF8, "application/json") | ||
| 262 | + }; | ||
| 263 | + | ||
| 264 | + using var res = await _http.SendAsync(req, ct); | ||
| 265 | + var json = await res.Content.ReadAsStringAsync(ct); | ||
| 266 | + | ||
| 267 | + var opt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; | ||
| 268 | + var dto = JsonSerializer.Deserialize<ScanConfirmResp>(json, opt); | ||
| 269 | + | ||
| 270 | + var ok = dto?.success == true; | ||
| 271 | + return new SimpleOk(ok, dto?.message); | ||
| 272 | + } | ||
| 273 | + | ||
| 274 | + public async Task<SimpleOk> CancelScanAsync(string instockId, CancellationToken ct = default) | ||
| 275 | + { | ||
| 276 | + var bodyJson = JsonSerializer.Serialize(new { instockId }); | ||
| 277 | + using var req = new HttpRequestMessage(HttpMethod.Post, _cancelScanEndpoint) | ||
| 278 | + { | ||
| 279 | + Content = new StringContent(bodyJson, Encoding.UTF8, "application/json") | ||
| 280 | + }; | ||
| 281 | + | ||
| 282 | + using var res = await _http.SendAsync(req, ct); | ||
| 283 | + var json = await res.Content.ReadAsStringAsync(ct); | ||
| 284 | + | ||
| 285 | + var opt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; | ||
| 286 | + var dto = JsonSerializer.Deserialize<CancelScanResp>(json, opt); | ||
| 287 | + | ||
| 288 | + var ok = dto?.success == true; | ||
| 289 | + return new SimpleOk(ok, dto?.message); | ||
| 290 | + } | ||
| 291 | + | ||
| 292 | + public async Task<SimpleOk> ConfirmInstockAsync(string instockId, CancellationToken ct = default) | ||
| 293 | + { | ||
| 294 | + var bodyJson = JsonSerializer.Serialize(new { instockId }); | ||
| 295 | + using var req = new HttpRequestMessage(HttpMethod.Post, _confirmInstockEndpoint) | ||
| 296 | + { | ||
| 297 | + Content = new StringContent(bodyJson, Encoding.UTF8, "application/json") | ||
| 298 | + }; | ||
| 299 | + | ||
| 300 | + using var res = await _http.SendAsync(req, ct); | ||
| 301 | + var json = await res.Content.ReadAsStringAsync(ct); | ||
| 302 | + | ||
| 303 | + var opt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; | ||
| 304 | + var dto = JsonSerializer.Deserialize<ConfirmResp>(json, opt); | ||
| 305 | + | ||
| 306 | + var ok = dto?.success == true; | ||
| 307 | + return new SimpleOk(ok, dto?.message); | ||
| 308 | + } | ||
| 309 | + public async Task<bool> JudgeInstockDetailScanAllAsync(string instockId, CancellationToken ct = default) | ||
| 310 | + { | ||
| 311 | + var url = $"{_judgeScanAllEndpoint}?id={Uri.EscapeDataString(instockId)}"; | ||
| 312 | + using var req = new HttpRequestMessage(HttpMethod.Get, url); | ||
| 313 | + using var res = await _http.SendAsync(req, ct); | ||
| 314 | + var json = await res.Content.ReadAsStringAsync(ct); | ||
| 315 | + | ||
| 316 | + var opt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; | ||
| 317 | + var dto = JsonSerializer.Deserialize<JudgeScanAllResp>(json, opt); | ||
| 318 | + | ||
| 319 | + // 按文档:看 result(true/false);若接口异常或无 result,则返回 false 让前端提示/二次确认 | ||
| 320 | + return dto?.result == true; | ||
| 321 | + } | ||
| 322 | +} | ||
| 323 | + | ||
| 324 | + // ====== DTO(按接口示例字段) ====== | ||
| 325 | + public class GetInStockReq | ||
| 326 | + { | ||
| 327 | + public string? createdTime { get; set; } | ||
| 328 | + public string? endTime { get; set; } | ||
| 329 | + public string? instockNo { get; set; } | ||
| 330 | + public string? orderType { get; set; } | ||
| 331 | + public string? startTime { get; set; } | ||
| 332 | + } | ||
| 333 | + | ||
| 334 | + public class GetInStockResp | ||
| 335 | + { | ||
| 336 | + public int code { get; set; } | ||
| 337 | + public long costTime { get; set; } | ||
| 338 | + public string? message { get; set; } | ||
| 339 | + public bool success { get; set; } | ||
| 340 | + public List<GetInStockItem>? result { get; set; } | ||
| 341 | + } | ||
| 342 | + | ||
| 343 | + public class GetInStockItem | ||
| 344 | + { | ||
| 345 | + public string? arrivalNo { get; set; } | ||
| 346 | + public string? createdTime { get; set; } | ||
| 347 | + public string? instockId { get; set; } | ||
| 348 | + public string? instockNo { get; set; } | ||
| 349 | + public string? orderType { get; set; } | ||
| 350 | + public string? purchaseNo { get; set; } | ||
| 351 | + public string? supplierName { get; set; } | ||
| 352 | + } | ||
| 353 | +public sealed class GetInStockDetailResp | ||
| 354 | +{ | ||
| 355 | + public bool success { get; set; } | ||
| 356 | + public string? message { get; set; } | ||
| 357 | + public int? code { get; set; } | ||
| 358 | + public List<GetInStockDetailItem>? result { get; set; } | ||
| 359 | + public int? costTime { get; set; } | ||
| 360 | +} | ||
| 361 | +public sealed class GetInStockDetailItem | ||
| 362 | +{ | ||
| 363 | + public string? id { get; set; } // 入库单明细主键id | ||
| 364 | + public string? instockNo { get; set; } // 入库单号 | ||
| 365 | + public string? materialCode { get; set; } | ||
| 366 | + public string? materialName { get; set; } | ||
| 367 | + public string? spec { get; set; } | ||
| 368 | + public string? stockBatch { get; set; } | ||
| 369 | + public string? instockQty { get; set; } // 预计数量(字符串/可能为空) | ||
| 370 | + public string? instockWarehouseCode { get; set; } // 入库仓库编码 | ||
| 371 | + public string? location { get; set; } // 内点库位 | ||
| 372 | + public string? qty { get; set; } // 已扫描量(字符串/可能为空) | ||
| 373 | +} | ||
| 374 | + | ||
| 375 | + | ||
| 376 | +public class ScanRow | ||
| 377 | + { | ||
| 378 | + public string? barcode { get; set; } | ||
| 379 | + public string? instockId { get; set; } | ||
| 380 | + public string? location { get; set; } | ||
| 381 | + public string? materialName { get; set; } | ||
| 382 | + public string? qty { get; set; } | ||
| 383 | + public string? spec { get; set; } | ||
| 384 | + } | ||
| 385 | + | ||
| 386 | + public class ScanByBarcodeResp | ||
| 387 | + { | ||
| 388 | + public int code { get; set; } | ||
| 389 | + public long costTime { get; set; } | ||
| 390 | + public string? message { get; set; } | ||
| 391 | + public object? result { get; set; } // 文档里 result 只是 bool/无结构,这里占位 | ||
| 392 | + public bool success { get; set; } | ||
| 393 | + } | ||
| 394 | + public class ScanConfirmResp | ||
| 395 | + { | ||
| 396 | + public int code { get; set; } | ||
| 397 | + public long costTime { get; set; } | ||
| 398 | + public string? message { get; set; } | ||
| 399 | + public object? result { get; set; } | ||
| 400 | + public bool success { get; set; } | ||
| 401 | + } | ||
| 402 | + public class CancelScanResp | ||
| 403 | + { | ||
| 404 | + public int code { get; set; } | ||
| 405 | + public long costTime { get; set; } | ||
| 406 | + public string? message { get; set; } | ||
| 407 | + public object? result { get; set; } | ||
| 408 | + public bool success { get; set; } | ||
| 409 | + } | ||
| 410 | + public class ConfirmResp | ||
| 411 | + { | ||
| 412 | + public int code { get; set; } | ||
| 413 | + public long costTime { get; set; } | ||
| 414 | + public string? message { get; set; } | ||
| 415 | + public object? result { get; set; } | ||
| 416 | + public bool success { get; set; } | ||
| 417 | + } | ||
| 418 | + public class JudgeScanAllResp | ||
| 419 | + { | ||
| 420 | + public int code { get; set; } | ||
| 421 | + public long costTime { get; set; } | ||
| 422 | + public string? message { get; set; } | ||
| 423 | + public bool success { get; set; } | ||
| 424 | + public bool? result { get; set; } // 文档中为布尔 | ||
| 425 | + } | ||
| 426 | +public class GetInStockPageResp | ||
| 427 | +{ | ||
| 428 | + public int code { get; set; } | ||
| 429 | + public long costTime { get; set; } | ||
| 430 | + public string? message { get; set; } | ||
| 431 | + public bool success { get; set; } | ||
| 432 | + public GetInStockPageData? result { get; set; } | ||
| 433 | +} | ||
| 434 | + | ||
| 435 | +public class GetInStockPageData | ||
| 436 | +{ | ||
| 437 | + public int pageNo { get; set; } | ||
| 438 | + public int pageSize { get; set; } | ||
| 439 | + public long total { get; set; } | ||
| 440 | + public List<GetInStockRecord> records { get; set; } = new(); | ||
| 441 | +} | ||
| 442 | + | ||
| 443 | +public class GetInStockRecord | ||
| 444 | +{ | ||
| 445 | + public string? id { get; set; } | ||
| 446 | + public string? instockNo { get; set; } | ||
| 447 | + public string? orderType { get; set; } | ||
| 448 | + public string? orderTypeName { get; set; } | ||
| 449 | + public string? supplierName { get; set; } | ||
| 450 | + public string? arrivalNo { get; set; } | ||
| 451 | + public string? purchaseNo { get; set; } | ||
| 452 | + public string? createdTime { get; set; } | ||
| 453 | +} | ||
| 454 | +public sealed class GetInStockScanDetailResp | ||
| 455 | +{ | ||
| 456 | + public bool success { get; set; } | ||
| 457 | + public string? message { get; set; } | ||
| 458 | + public int? code { get; set; } | ||
| 459 | + public List<GetInStockScanDetailItem>? result { get; set; } | ||
| 460 | + public int? costTime { get; set; } | ||
| 461 | +} | ||
| 462 | + | ||
| 463 | +public sealed class GetInStockScanDetailItem | ||
| 464 | +{ | ||
| 465 | + public string? id { get; set; } // 入库单明细主键 id | ||
| 466 | + public string? barcode { get; set; } | ||
| 467 | + public string? materialName { get; set; } | ||
| 468 | + public string? spec { get; set; } | ||
| 469 | + public string? qty { get; set; } // 可能是 null 或 “数字字符串” | ||
| 470 | + public string? warehouseCode { get; set; } | ||
| 471 | + public string? location { get; set; } | ||
| 472 | + public bool? scanStatus { get; set; } // 可能为 null,按 false 处理 | ||
| 473 | +} | ||
| 474 | + | ||
| 475 | + |
Services/MockWarehouseDataService.cs
已删除
100644 → 0
| 1 | -using IndustrialControl.ViewModels; | ||
| 2 | - | ||
| 3 | -namespace IndustrialControl.Services; | ||
| 4 | - | ||
| 5 | -public interface IWarehouseDataService | ||
| 6 | -{ | ||
| 7 | - Task<InboundOrder> GetInboundOrderAsync(string orderNo); | ||
| 8 | - Task<OutboundOrder> GetOutboundOrderAsync(string orderNo); | ||
| 9 | - Task<SimpleOk> ConfirmInboundAsync(string orderNo, IEnumerable<ScanItem> items); | ||
| 10 | - | ||
| 11 | - Task<SimpleOk> ConfirmInboundProductionAsync(string orderNo, IEnumerable<ScanItem> items); | ||
| 12 | - Task<SimpleOk> ConfirmOutboundProductionAsync(string orderNo, IEnumerable<ScanItem> items); | ||
| 13 | - Task<SimpleOk> ConfirmOutboundMaterialAsync(string orderNo, IEnumerable<ScanItem> items); | ||
| 14 | - Task<SimpleOk> ConfirmOutboundFinishedAsync(string orderNo, IEnumerable<ScanItem> items); | ||
| 15 | - Task<IEnumerable<string>> ListInboundBinsAsync(string orderNo); | ||
| 16 | - Task<IEnumerable<string>> ListOutboundBinsAsync(string orderNo); | ||
| 17 | - | ||
| 18 | - // NEW: 查询列表(图1) | ||
| 19 | - Task<IEnumerable<InboundOrderSummary>> ListInboundOrdersAsync(string? orderNoOrBarcode, DateTime createdDate); | ||
| 20 | - // NEW: 查询列表(图1) | ||
| 21 | - Task<IEnumerable<OutboundOrderSummary>> ListOutboundOrdersAsync(string? orderNoOrBarcode, DateTime createdDate); | ||
| 22 | -} | ||
| 23 | - | ||
| 24 | -public record InboundOrder(string OrderNo, string Supplier, string LinkedNo, int ExpectedQty); | ||
| 25 | - | ||
| 26 | -public record OutboundOrder(string OrderNo, string Supplier, string LinkedNo, int ExpectedQty); | ||
| 27 | -public record ScanItem(int Index, string Barcode, string? Bin, int Qty); | ||
| 28 | -public record SimpleOk(bool Succeeded, string? Message = null); | ||
| 29 | - | ||
| 30 | -public class MockWarehouseDataService : IWarehouseDataService | ||
| 31 | -{ | ||
| 32 | - private readonly Random _rand = new(); | ||
| 33 | - | ||
| 34 | - public Task<InboundOrder> GetInboundOrderAsync(string orderNo) | ||
| 35 | - => Task.FromResult(new InboundOrder(orderNo, "XXXX", "DHD_23326", _rand.Next(10, 80))); | ||
| 36 | - | ||
| 37 | - public Task<OutboundOrder> GetOutboundOrderAsync(string orderNo) | ||
| 38 | - => Task.FromResult(new OutboundOrder(orderNo, "XXXX", "DHD_23326", _rand.Next(10, 80))); | ||
| 39 | - | ||
| 40 | - public Task<SimpleOk> ConfirmInboundAsync(string orderNo, IEnumerable<ScanItem> items) | ||
| 41 | - => Task.FromResult(new SimpleOk(true, $"入库成功:{items.Count()} 条")); | ||
| 42 | - | ||
| 43 | - public Task<SimpleOk> ConfirmOutboundAsync(string orderNo, IEnumerable<ScanItem> items) | ||
| 44 | - => Task.FromResult(new SimpleOk(true, $"出库库成功:{items.Count()} 条")); | ||
| 45 | - | ||
| 46 | - public Task<SimpleOk> ConfirmInboundProductionAsync(string orderNo, IEnumerable<ScanItem> items) | ||
| 47 | - => Task.FromResult(new SimpleOk(true, $"生产入库成功:{items.Count()} 条")); | ||
| 48 | - public Task<SimpleOk> ConfirmOutboundProductionAsync(string orderNo, IEnumerable<ScanItem> items) | ||
| 49 | - => Task.FromResult(new SimpleOk(true, $"生产出库成功:{items.Count()} 条")); | ||
| 50 | - public Task<SimpleOk> ConfirmOutboundMaterialAsync(string orderNo, IEnumerable<ScanItem> items) | ||
| 51 | - => Task.FromResult(new SimpleOk(true, $"物料出库成功:{items.Count()} 条")); | ||
| 52 | - | ||
| 53 | - public Task<SimpleOk> ConfirmOutboundFinishedAsync(string orderNo, IEnumerable<ScanItem> items) | ||
| 54 | - => Task.FromResult(new SimpleOk(true, $"成品出库成功:{items.Count()} 条")); | ||
| 55 | - public Task<IEnumerable<InboundOrderSummary>> ListInboundOrdersAsync(string? orderNoOrBarcode, DateTime createdDate) | ||
| 56 | - { | ||
| 57 | - // 简单过滤逻辑(按需改造) | ||
| 58 | - var today = DateTime.Today; | ||
| 59 | - var all = Enumerable.Range(1, 8).Select(i => | ||
| 60 | - new InboundOrderSummary( | ||
| 61 | - OrderNo: $"CGD{today:yyyyMMdd}-{i:000}", | ||
| 62 | - InboundType: (i % 2 == 0) ? "采购入库" : "生产入库", | ||
| 63 | - Supplier: $"供应商{i}", | ||
| 64 | - CreatedAt: today.AddDays(-i) | ||
| 65 | - )); | ||
| 66 | - | ||
| 67 | - IEnumerable<InboundOrderSummary> filtered = all; | ||
| 68 | - | ||
| 69 | - //if (!string.IsNullOrWhiteSpace(orderNoOrBarcode)) | ||
| 70 | - // filtered = filtered.Where(x => x.OrderNo.Contains(orderNoOrBarcode, StringComparison.OrdinalIgnoreCase)); | ||
| 71 | - | ||
| 72 | - //// 示例:按创建日期“同一天”过滤(你可以换成 >= 起始 && < 次日) | ||
| 73 | - //filtered = filtered.Where(x => x.CreatedAt.Date == createdDate.Date); | ||
| 74 | - | ||
| 75 | - return Task.FromResult(filtered); | ||
| 76 | - } | ||
| 77 | - public Task<IEnumerable<OutboundOrderSummary>> ListOutboundOrdersAsync(string? orderNoOrBarcode, DateTime createdDate) | ||
| 78 | - { | ||
| 79 | - // 简单过滤逻辑(按需改造) | ||
| 80 | - var today = DateTime.Today; | ||
| 81 | - var all = Enumerable.Range(1, 8).Select(i => | ||
| 82 | - new OutboundOrderSummary( | ||
| 83 | - OrderNo: $"CGD{today:yyyyMMdd}-{i:000}", | ||
| 84 | - InboundType: (i % 2 == 0) ? "采购入库" : "生产入库", | ||
| 85 | - Supplier: $"供应商{i}", | ||
| 86 | - CreatedAt: today.AddDays(-i) | ||
| 87 | - )); | ||
| 88 | - | ||
| 89 | - IEnumerable<OutboundOrderSummary> filtered = all; | ||
| 90 | - | ||
| 91 | - //if (!string.IsNullOrWhiteSpace(orderNoOrBarcode)) | ||
| 92 | - // filtered = filtered.Where(x => x.OrderNo.Contains(orderNoOrBarcode, StringComparison.OrdinalIgnoreCase)); | ||
| 93 | - | ||
| 94 | - //// 示例:按创建日期“同一天”过滤(你可以换成 >= 起始 && < 次日) | ||
| 95 | - //filtered = filtered.Where(x => x.CreatedAt.Date == createdDate.Date); | ||
| 96 | - | ||
| 97 | - return Task.FromResult(filtered); | ||
| 98 | - } | ||
| 99 | - | ||
| 100 | - public Task<IEnumerable<string>> ListInboundBinsAsync(string orderNo) | ||
| 101 | - { | ||
| 102 | - var bins = new[] { "CK1_A201", "CK1_A202", "CK1_A203", "CK1_A204", "CK1_B101" }; | ||
| 103 | - return Task.FromResult<IEnumerable<string>>(bins); | ||
| 104 | - } | ||
| 105 | - | ||
| 106 | - public Task<IEnumerable<string>> ListOutboundBinsAsync(string orderNo) | ||
| 107 | - { | ||
| 108 | - var bins = new[] { "CK1_A201", "CK1_A202", "CK1_A203", "CK1_A204", "CK1_B101" }; | ||
| 109 | - return Task.FromResult<IEnumerable<string>>(bins); | ||
| 110 | - } | ||
| 111 | -} |
Services/OutboundMaterialService.cs
0 → 100644
| 1 | +// 文件:Services/WarehouseDataApiService.cs | ||
| 2 | +using System.Net.Http; | ||
| 3 | +using System.Text; | ||
| 4 | +using System.Text.Json; | ||
| 5 | +using System.Text.Json.Nodes; | ||
| 6 | +using IndustrialControl.ViewModels; | ||
| 7 | + | ||
| 8 | +namespace IndustrialControl.Services; | ||
| 9 | + | ||
| 10 | +/// <summary> | ||
| 11 | +/// 真实接口实现,风格对齐 WorkOrderApi | ||
| 12 | +/// </summary> | ||
| 13 | +public sealed class OutboundMaterialService : IOutboundMaterialService | ||
| 14 | +{ | ||
| 15 | + private readonly HttpClient _http; | ||
| 16 | + private readonly string _outboundListEndpoint; | ||
| 17 | + private readonly string _detailEndpoint; | ||
| 18 | + private readonly string _scanDetailEndpoint; | ||
| 19 | + // 新增:扫码入库端点 | ||
| 20 | + private readonly string _scanByBarcodeEndpoint; | ||
| 21 | + private readonly string _scanConfirmEndpoint; | ||
| 22 | + private readonly string _cancelScanEndpoint; | ||
| 23 | + private readonly string _confirmOutstockEndpoint; | ||
| 24 | + private readonly string _judgeScanAllEndpoint; | ||
| 25 | + | ||
| 26 | + public OutboundMaterialService(HttpClient http, IConfigLoader configLoader) | ||
| 27 | + { | ||
| 28 | + _http = http; | ||
| 29 | + | ||
| 30 | + // 和 WorkOrderApi 一样从 appconfig.json 读取端点,留兼容键名 + 兜底硬编码 | ||
| 31 | + JsonNode cfg = configLoader.Load(); | ||
| 32 | + // ⭐ 新增:读取 baseUrl 或 ip+port | ||
| 33 | + var baseUrl = | ||
| 34 | + (string?)cfg?["server"]?["baseUrl"] | ||
| 35 | + ?? BuildBaseUrl(cfg?["server"]?["ipAddress"], cfg?["server"]?["port"]); | ||
| 36 | + | ||
| 37 | + if (string.IsNullOrWhiteSpace(baseUrl)) | ||
| 38 | + throw new InvalidOperationException("后端基础地址未配置:请在 appconfig.json 配置 server.baseUrl 或 server.ipAddress + server.port"); | ||
| 39 | + | ||
| 40 | + if (_http.BaseAddress is null) | ||
| 41 | + _http.BaseAddress = new Uri(baseUrl, UriKind.Absolute); | ||
| 42 | + _outboundListEndpoint = | ||
| 43 | + (string?)cfg?["apiEndpoints"]?["outbound"]?["list"] ?? | ||
| 44 | + (string?)cfg?["apiEndpoints"]?["getOutStock"] ?? | ||
| 45 | + "/normalService/pda/wmsMaterialOutstock/getOutStock"; | ||
| 46 | + _detailEndpoint = (string?)cfg?["apiEndpoints"]?["outbound"]?["detail"] | ||
| 47 | + ?? "/normalService/pda/wmsMaterialOutstock/getOutStockDetail"; | ||
| 48 | + _scanDetailEndpoint = (string?)cfg?["apiEndpoints"]?["outbound"]?["scanDetail"] | ||
| 49 | + ?? "/normalService/pda/wmsMaterialOutstock/getOutStockScanDetail"; | ||
| 50 | + _scanByBarcodeEndpoint = | ||
| 51 | + (string?)cfg?["apiEndpoints"]?["outbound"]?["scanByBarcode"] | ||
| 52 | + ?? "/normalService/pda/wmsMaterialOutstock/getOutStockByBarcode"; | ||
| 53 | + _scanConfirmEndpoint = | ||
| 54 | + (string?)cfg?["apiEndpoints"]?["outbound"]?["scanConfirm"] | ||
| 55 | + ?? "/normalService/pda/wmsMaterialOutstock/scanConfirm"; | ||
| 56 | + _cancelScanEndpoint = | ||
| 57 | + (string?)cfg?["apiEndpoints"]?["outbound"]?["cancelScan"] | ||
| 58 | + ?? "/normalService/pda/wmsMaterialOutstock/cancelScan"; | ||
| 59 | + _confirmOutstockEndpoint = | ||
| 60 | + (string?)cfg?["apiEndpoints"]?["outbound"]?["confirm"] | ||
| 61 | + ?? "/normalService/pda/wmsMaterialOutstock/confirm"; | ||
| 62 | + _judgeScanAllEndpoint = | ||
| 63 | + (string?)cfg?["apiEndpoints"]?["outbound"]?["judgeScanAll"] | ||
| 64 | + ?? "/normalService/pda/wmsMaterialOutstock/judgeOutstockDetailScanAll"; | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + // ====== 你当前页面会调用的方法 ====== | ||
| 68 | + private static string? BuildBaseUrl(JsonNode? ipNode, JsonNode? portNode) | ||
| 69 | + { | ||
| 70 | + string? ip = ipNode?.ToString().Trim(); | ||
| 71 | + string? port = portNode?.ToString().Trim(); | ||
| 72 | + | ||
| 73 | + if (string.IsNullOrWhiteSpace(ip)) return null; | ||
| 74 | + | ||
| 75 | + // 如果没带 http:// 或 https://,默认 http:// | ||
| 76 | + var hasScheme = ip.StartsWith("http://", StringComparison.OrdinalIgnoreCase) | ||
| 77 | + || ip.StartsWith("https://", StringComparison.OrdinalIgnoreCase); | ||
| 78 | + var host = hasScheme ? ip : $"http://{ip}"; | ||
| 79 | + | ||
| 80 | + return string.IsNullOrEmpty(port) ? host : $"{host}:{port}"; | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | + | ||
| 84 | + public async Task<IEnumerable<OutboundOrderSummary>> ListOutboundOrdersAsync( | ||
| 85 | + string? orderNoOrBarcode, | ||
| 86 | + DateTime startDate, | ||
| 87 | + DateTime endDate, | ||
| 88 | + string orderType, | ||
| 89 | + string[] orderTypeList, | ||
| 90 | + CancellationToken ct = default) | ||
| 91 | + { | ||
| 92 | + // 结束时间扩到当天 23:59:59,避免把当日数据排除 | ||
| 93 | + var begin = startDate.ToString("yyyy-MM-dd 00:00:00"); | ||
| 94 | + var end = endDate.ToString("yyyy-MM-dd 23:59:59"); | ||
| 95 | + | ||
| 96 | + // 用 KVP 列表(不要 Dictionary)→ 规避 WinRT generic + AOT 警告 | ||
| 97 | + var pairs = new List<KeyValuePair<string, string>> | ||
| 98 | + { | ||
| 99 | + new("createdTimeBegin", begin), | ||
| 100 | + new("createdTimeEnd", end), | ||
| 101 | + new("pageNo", "1"), | ||
| 102 | + new("pageSize","10") | ||
| 103 | + // 如需统计总数:new("searchCount", "true") | ||
| 104 | + }; | ||
| 105 | + | ||
| 106 | + if (!string.IsNullOrWhiteSpace(orderNoOrBarcode)) | ||
| 107 | + pairs.Add(new("outstockNo", orderNoOrBarcode.Trim())); | ||
| 108 | + | ||
| 109 | + if (!string.IsNullOrWhiteSpace(orderType)) | ||
| 110 | + pairs.Add(new("orderType", orderType)); | ||
| 111 | + | ||
| 112 | + if (orderTypeList is { Length: > 0 }) | ||
| 113 | + pairs.Add(new("orderTypeList", string.Join(",", orderTypeList))); | ||
| 114 | + | ||
| 115 | + // 交给 BCL 编码(比手写 Escape 安全) | ||
| 116 | + using var form = new FormUrlEncodedContent(pairs); | ||
| 117 | + var qs = await form.ReadAsStringAsync(ct); | ||
| 118 | + var url = _outboundListEndpoint + "?" + qs; | ||
| 119 | + | ||
| 120 | + using var req = new HttpRequestMessage(HttpMethod.Get, url); | ||
| 121 | + using var resp = await _http.SendAsync(req, ct); | ||
| 122 | + var json = await resp.Content.ReadAsStringAsync(ct); | ||
| 123 | + | ||
| 124 | + if (!resp.IsSuccessStatusCode) | ||
| 125 | + return Enumerable.Empty<OutboundOrderSummary>(); | ||
| 126 | + | ||
| 127 | + var opt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; | ||
| 128 | + var dto = JsonSerializer.Deserialize<GetOutStockPageResp>(json, opt); | ||
| 129 | + | ||
| 130 | + var records = dto?.result?.records; | ||
| 131 | + if (dto?.success != true || records is null || records.Count == 0) | ||
| 132 | + return Enumerable.Empty<OutboundOrderSummary>(); | ||
| 133 | + | ||
| 134 | + return records.Select(x => new OutboundOrderSummary( | ||
| 135 | + outstockId: x.id ?? "", | ||
| 136 | + orderType: x.orderType ?? "", | ||
| 137 | + orderTypeName: x.orderTypeName ?? "", | ||
| 138 | + purchaseNo: x.purchaseNo ?? "", | ||
| 139 | + supplierName: x.supplierName ?? "", | ||
| 140 | + arrivalNo: x.arrivalNo ?? "", | ||
| 141 | + createdTime: x.createdTime ?? "", | ||
| 142 | + deliveryNo: x.deliveryNo ?? "", | ||
| 143 | + requisitionMaterialNo:x.requisitionMaterialNo ?? "", | ||
| 144 | + returnNo: x.returnNo ?? "", | ||
| 145 | + workOrderNo:x.workOrderNo ?? "" | ||
| 146 | + | ||
| 147 | + | ||
| 148 | + )); | ||
| 149 | + } | ||
| 150 | + | ||
| 151 | + | ||
| 152 | + public async Task<IReadOnlyList<OutboundPendingRow>> GetOutStockDetailAsync( | ||
| 153 | + string outstockId, CancellationToken ct = default) | ||
| 154 | + { | ||
| 155 | + var url = $"{_detailEndpoint}?outstockId={Uri.EscapeDataString(outstockId)}"; | ||
| 156 | + using var req = new HttpRequestMessage(HttpMethod.Get, url); | ||
| 157 | + | ||
| 158 | + using var res = await _http.SendAsync(req, ct).ConfigureAwait(false); | ||
| 159 | + res.EnsureSuccessStatusCode(); | ||
| 160 | + | ||
| 161 | + var json = await res.Content.ReadAsStringAsync(ct).ConfigureAwait(false); | ||
| 162 | + var opt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; | ||
| 163 | + var dto = JsonSerializer.Deserialize<GetOutStockDetailResp>(json, opt); | ||
| 164 | + | ||
| 165 | + if (dto?.success != true || dto.result is null || dto.result.Count == 0) | ||
| 166 | + return Array.Empty<OutboundPendingRow>(); | ||
| 167 | + | ||
| 168 | + static int ToIntSafe(string? s) | ||
| 169 | + { | ||
| 170 | + if (string.IsNullOrWhiteSpace(s)) return 0; | ||
| 171 | + s = s.Trim().Replace(",", ""); | ||
| 172 | + return int.TryParse(s, out var v) ? v : 0; | ||
| 173 | + } | ||
| 174 | + | ||
| 175 | + // ⚠️ 接口没有 barcode,这里先用空串;如需展示可以改成 x.materialCode 或 x.stockBatch | ||
| 176 | + var list = dto.result.Select(x => new OutboundPendingRow( | ||
| 177 | + Barcode: string.Empty, // 或 $"{x.materialCode}" / $"{x.stockBatch}" | ||
| 178 | + DetailId: x.id ?? string.Empty, // ← 改为接口的 id | ||
| 179 | + Location: x.location ?? string.Empty, | ||
| 180 | + MaterialName: x.materialName ?? string.Empty, | ||
| 181 | + PendingQty: ToIntSafe(x.outstockQty), // ← 预计数量 | ||
| 182 | + ScannedQty: ToIntSafe(x.qty), // ← 已扫描量 | ||
| 183 | + Spec: x.spec ?? string.Empty | ||
| 184 | + )).ToList(); | ||
| 185 | + | ||
| 186 | + return list; | ||
| 187 | + } | ||
| 188 | + | ||
| 189 | + public async Task<IReadOnlyList<OutboundScannedRow>> GetOutStockScanDetailAsync( | ||
| 190 | + string outstockId, | ||
| 191 | + CancellationToken ct = default) | ||
| 192 | + { | ||
| 193 | + // 文档为 GET + x-www-form-urlencoded,这里用 query 传递(关键在大小写常为 OutstockId) | ||
| 194 | + var url = $"{_scanDetailEndpoint}?OutstockId={Uri.EscapeDataString(outstockId)}"; | ||
| 195 | + using var req = new HttpRequestMessage(HttpMethod.Get, url); | ||
| 196 | + | ||
| 197 | + using var res = await _http.SendAsync(req, ct).ConfigureAwait(false); | ||
| 198 | + res.EnsureSuccessStatusCode(); | ||
| 199 | + | ||
| 200 | + var json = await res.Content.ReadAsStringAsync(ct).ConfigureAwait(false); | ||
| 201 | + | ||
| 202 | + var opt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; | ||
| 203 | + var dto = JsonSerializer.Deserialize<GetOutStockScanDetailResp>(json, opt); | ||
| 204 | + | ||
| 205 | + if (dto?.success != true || dto.result is null || dto.result.Count == 0) | ||
| 206 | + return Array.Empty<OutboundScannedRow>(); | ||
| 207 | + | ||
| 208 | + static int ToIntSafe(string? s) | ||
| 209 | + { | ||
| 210 | + if (string.IsNullOrWhiteSpace(s)) return 0; | ||
| 211 | + // 去除千分位、空格 | ||
| 212 | + s = s.Trim().Replace(",", ""); | ||
| 213 | + return int.TryParse(s, out var v) ? v : 0; | ||
| 214 | + } | ||
| 215 | + | ||
| 216 | + // 映射:OutstockId <- id(截图注释“入库单明细主键id”) | ||
| 217 | + var list = dto.result.Select(x => new OutboundScannedRow( | ||
| 218 | + Barcode: (x.barcode ?? string.Empty).Trim(), | ||
| 219 | + DetailId: (x.id ?? string.Empty).Trim(), | ||
| 220 | + Location: (x.location ?? string.Empty).Trim(), | ||
| 221 | + MaterialName: (x.materialName ?? string.Empty).Trim(), | ||
| 222 | + Qty: ToIntSafe(x.qty), | ||
| 223 | + Spec: (x.spec ?? string.Empty).Trim(), | ||
| 224 | + ScanStatus: x.scanStatus ?? false, | ||
| 225 | + WarehouseCode: x.warehouseCode?.Trim() | ||
| 226 | + )).ToList(); | ||
| 227 | + | ||
| 228 | + return list; | ||
| 229 | + } | ||
| 230 | + // ========= 扫码入库实现 ========= | ||
| 231 | + public async Task<SimpleOk> OutStockByBarcodeAsync(string outstockId, string barcode, CancellationToken ct = default) | ||
| 232 | + { | ||
| 233 | + var body = JsonSerializer.Serialize(new { barcode, outstockId }); | ||
| 234 | + using var req = new HttpRequestMessage(HttpMethod.Post, _scanByBarcodeEndpoint) | ||
| 235 | + { | ||
| 236 | + Content = new StringContent(body, Encoding.UTF8, "application/json") | ||
| 237 | + }; | ||
| 238 | + | ||
| 239 | + using var res = await _http.SendAsync(req, ct); | ||
| 240 | + var json = await res.Content.ReadAsStringAsync(ct); | ||
| 241 | + var opt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; | ||
| 242 | + var dto = JsonSerializer.Deserialize<ScanByBarcodeResp>(json, opt); | ||
| 243 | + | ||
| 244 | + // 按文档:以 success 判断;message 作为失败提示 | ||
| 245 | + var ok = dto?.success == true; | ||
| 246 | + return new SimpleOk(ok, dto?.message); | ||
| 247 | + } | ||
| 248 | + public async Task<SimpleOk> ScanConfirmAsync(string outstockId, CancellationToken ct = default) | ||
| 249 | + { | ||
| 250 | + var bodyJson = JsonSerializer.Serialize(new { outstockId }); | ||
| 251 | + using var req = new HttpRequestMessage(HttpMethod.Post, _scanConfirmEndpoint) | ||
| 252 | + { | ||
| 253 | + Content = new StringContent(bodyJson, Encoding.UTF8, "application/json") | ||
| 254 | + }; | ||
| 255 | + | ||
| 256 | + using var res = await _http.SendAsync(req, ct); | ||
| 257 | + var json = await res.Content.ReadAsStringAsync(ct); | ||
| 258 | + | ||
| 259 | + var opt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; | ||
| 260 | + var dto = JsonSerializer.Deserialize<ScanConfirmResp>(json, opt); | ||
| 261 | + | ||
| 262 | + var ok = dto?.success == true; | ||
| 263 | + return new SimpleOk(ok, dto?.message); | ||
| 264 | + } | ||
| 265 | + | ||
| 266 | + public async Task<SimpleOk> CancelScanAsync(string outstockId, CancellationToken ct = default) | ||
| 267 | + { | ||
| 268 | + var bodyJson = JsonSerializer.Serialize(new { outstockId }); | ||
| 269 | + using var req = new HttpRequestMessage(HttpMethod.Post, _cancelScanEndpoint) | ||
| 270 | + { | ||
| 271 | + Content = new StringContent(bodyJson, Encoding.UTF8, "application/json") | ||
| 272 | + }; | ||
| 273 | + | ||
| 274 | + using var res = await _http.SendAsync(req, ct); | ||
| 275 | + var json = await res.Content.ReadAsStringAsync(ct); | ||
| 276 | + | ||
| 277 | + var opt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; | ||
| 278 | + var dto = JsonSerializer.Deserialize<CancelScanResp>(json, opt); | ||
| 279 | + | ||
| 280 | + var ok = dto?.success == true; | ||
| 281 | + return new SimpleOk(ok, dto?.message); | ||
| 282 | + } | ||
| 283 | + | ||
| 284 | + public async Task<SimpleOk> ConfirmOutstockAsync(string outstockId, CancellationToken ct = default) | ||
| 285 | + { | ||
| 286 | + var bodyJson = JsonSerializer.Serialize(new { outstockId }); | ||
| 287 | + using var req = new HttpRequestMessage(HttpMethod.Post, _confirmOutstockEndpoint) | ||
| 288 | + { | ||
| 289 | + Content = new StringContent(bodyJson, Encoding.UTF8, "application/json") | ||
| 290 | + }; | ||
| 291 | + | ||
| 292 | + using var res = await _http.SendAsync(req, ct); | ||
| 293 | + var json = await res.Content.ReadAsStringAsync(ct); | ||
| 294 | + | ||
| 295 | + var opt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; | ||
| 296 | + var dto = JsonSerializer.Deserialize<ConfirmResp>(json, opt); | ||
| 297 | + | ||
| 298 | + var ok = dto?.success == true; | ||
| 299 | + return new SimpleOk(ok, dto?.message); | ||
| 300 | + } | ||
| 301 | + public async Task<bool> JudgeOutstockDetailScanAllAsync(string outstockId, CancellationToken ct = default) | ||
| 302 | + { | ||
| 303 | + var url = $"{_judgeScanAllEndpoint}?id={Uri.EscapeDataString(outstockId)}"; | ||
| 304 | + using var req = new HttpRequestMessage(HttpMethod.Get, url); | ||
| 305 | + using var res = await _http.SendAsync(req, ct); | ||
| 306 | + var json = await res.Content.ReadAsStringAsync(ct); | ||
| 307 | + | ||
| 308 | + var opt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; | ||
| 309 | + var dto = JsonSerializer.Deserialize<JudgeScanAllResp>(json, opt); | ||
| 310 | + | ||
| 311 | + // 按文档:看 result(true/false);若接口异常或无 result,则返回 false 让前端提示/二次确认 | ||
| 312 | + return dto?.result == true; | ||
| 313 | + } | ||
| 314 | + | ||
| 315 | +} | ||
| 316 | + | ||
| 317 | +public class GetOutStockReq | ||
| 318 | +{ | ||
| 319 | + public string? createdTime { get; set; } | ||
| 320 | + public string? endTime { get; set; } | ||
| 321 | + public string? outstockNo { get; set; } | ||
| 322 | + public string? orderType { get; set; } | ||
| 323 | + public string? startTime { get; set; } | ||
| 324 | +} | ||
| 325 | + | ||
| 326 | +public class GetOutStockResp | ||
| 327 | +{ | ||
| 328 | + public int code { get; set; } | ||
| 329 | + public long costTime { get; set; } | ||
| 330 | + public string? message { get; set; } | ||
| 331 | + public bool success { get; set; } | ||
| 332 | + public List<GetOutStockItem>? result { get; set; } | ||
| 333 | +} | ||
| 334 | + | ||
| 335 | +public class GetOutStockItem | ||
| 336 | +{ | ||
| 337 | + public string? arrivalNo { get; set; } | ||
| 338 | + public string? createdTime { get; set; } | ||
| 339 | + public string? outstockId { get; set; } | ||
| 340 | + public string? outstockNo { get; set; } | ||
| 341 | + public string? orderType { get; set; } | ||
| 342 | + public string? purchaseNo { get; set; } | ||
| 343 | + public string? supplierName { get; set; } | ||
| 344 | +} | ||
| 345 | +public sealed class GetOutStockDetailResp | ||
| 346 | +{ | ||
| 347 | + public bool success { get; set; } | ||
| 348 | + public string? message { get; set; } | ||
| 349 | + public int? code { get; set; } | ||
| 350 | + public List<GetOutStockDetailItem>? result { get; set; } | ||
| 351 | + public int? costTime { get; set; } | ||
| 352 | +} | ||
| 353 | +public sealed class GetOutStockDetailItem | ||
| 354 | +{ | ||
| 355 | + public string? id { get; set; } // 入库单明细主键id | ||
| 356 | + public string? outstockNo { get; set; } // 入库单号 | ||
| 357 | + public string? materialCode { get; set; } | ||
| 358 | + public string? materialName { get; set; } | ||
| 359 | + public string? spec { get; set; } | ||
| 360 | + public string? stockBatch { get; set; } | ||
| 361 | + public string? outstockQty { get; set; } // 预计数量(字符串/可能为空) | ||
| 362 | + public string? outstockWarehouseCode { get; set; } // 入库仓库编码 | ||
| 363 | + public string? location { get; set; } // 内点库位 | ||
| 364 | + public string? qty { get; set; } // 已扫描量(字符串/可能为空) | ||
| 365 | +} | ||
| 366 | + | ||
| 367 | + | ||
| 368 | + | ||
| 369 | + | ||
| 370 | + | ||
| 371 | + | ||
| 372 | +public class GetOutStockPageResp | ||
| 373 | +{ | ||
| 374 | + public int code { get; set; } | ||
| 375 | + public long costTime { get; set; } | ||
| 376 | + public string? message { get; set; } | ||
| 377 | + public bool success { get; set; } | ||
| 378 | + public GetOutStockPageData? result { get; set; } | ||
| 379 | +} | ||
| 380 | + | ||
| 381 | +public class GetOutStockPageData | ||
| 382 | +{ | ||
| 383 | + public int pageNo { get; set; } | ||
| 384 | + public int pageSize { get; set; } | ||
| 385 | + public long total { get; set; } | ||
| 386 | + public List<GetOutStockRecord> records { get; set; } = new(); | ||
| 387 | +} | ||
| 388 | + | ||
| 389 | +public class GetOutStockRecord | ||
| 390 | +{ | ||
| 391 | + public string? id { get; set; } | ||
| 392 | + public string? outstockNo { get; set; } | ||
| 393 | + public string? orderType { get; set; } | ||
| 394 | + public string? orderTypeName { get; set; } | ||
| 395 | + public string? supplierName { get; set; } | ||
| 396 | + public string? arrivalNo { get; set; } | ||
| 397 | + public string? purchaseNo { get; set; } | ||
| 398 | + public string? createdTime { get; set; } | ||
| 399 | + public string? deliveryNo { get; set; } | ||
| 400 | + public string? requisitionMaterialNo { get; set; } | ||
| 401 | + public string? returnNo { get; set; } | ||
| 402 | + public string? workOrderNo { get; set; } | ||
| 403 | +} | ||
| 404 | +public sealed class GetOutStockScanDetailResp | ||
| 405 | +{ | ||
| 406 | + public bool success { get; set; } | ||
| 407 | + public string? message { get; set; } | ||
| 408 | + public int? code { get; set; } | ||
| 409 | + public List<GetOutStockScanDetailItem>? result { get; set; } | ||
| 410 | + public int? costTime { get; set; } | ||
| 411 | +} | ||
| 412 | + | ||
| 413 | +public sealed class GetOutStockScanDetailItem | ||
| 414 | +{ | ||
| 415 | + public string? id { get; set; } // 入库单明细主键 id | ||
| 416 | + public string? barcode { get; set; } | ||
| 417 | + public string? materialName { get; set; } | ||
| 418 | + public string? spec { get; set; } | ||
| 419 | + public string? qty { get; set; } // 可能是 null 或 “数字字符串” | ||
| 420 | + public string? warehouseCode { get; set; } | ||
| 421 | + public string? location { get; set; } | ||
| 422 | + public bool? scanStatus { get; set; } // 可能为 null,按 false 处理 | ||
| 423 | +} | ||
| 424 | + | ||
| 425 | + | ||
| 426 | + | ||
| 427 | + |
| 1 | -using System.Threading.Tasks; | 1 | +// Utils/TokenStorage.cs |
| 2 | using Microsoft.Maui.Storage; | 2 | using Microsoft.Maui.Storage; |
| 3 | 3 | ||
| 4 | namespace IndustrialControl; | 4 | namespace IndustrialControl; |
| @@ -7,25 +7,40 @@ public static class TokenStorage | @@ -7,25 +7,40 @@ public static class TokenStorage | ||
| 7 | { | 7 | { |
| 8 | private const string Key = "auth_token"; | 8 | private const string Key = "auth_token"; |
| 9 | 9 | ||
| 10 | - public static Task SaveAsync(string token) | ||
| 11 | - => SecureStorage.SetAsync(Key, token); | 10 | + public static async Task SaveAsync(string token) |
| 11 | + { | ||
| 12 | + await SecureStorage.SetAsync(Key, token); | ||
| 13 | + Preferences.Set(Key, token); // 兜底:极端设备 SecureStorage 取不到时从这里拿 | ||
| 14 | + System.Diagnostics.Debug.WriteLine($"[TokenStorage] Saved token len={token?.Length}"); | ||
| 15 | + } | ||
| 12 | 16 | ||
| 13 | - public static async Task<string?> GetAsync() | 17 | + public static async Task<string?> LoadAsync() |
| 14 | { | 18 | { |
| 15 | try | 19 | try |
| 16 | { | 20 | { |
| 17 | - return await SecureStorage.GetAsync(Key); | 21 | + var t = await SecureStorage.GetAsync(Key); |
| 22 | + if (!string.IsNullOrWhiteSpace(t)) | ||
| 23 | + { | ||
| 24 | + System.Diagnostics.Debug.WriteLine($"[TokenStorage] Loaded from SecureStorage, len={t?.Length}"); | ||
| 25 | + return t; | ||
| 26 | + } | ||
| 18 | } | 27 | } |
| 19 | - catch | 28 | + catch (Exception ex) |
| 20 | { | 29 | { |
| 21 | - // 个别设备可能不支持安全存储,兜底返回 null | ||
| 22 | - return null; | 30 | + System.Diagnostics.Debug.WriteLine($"[TokenStorage] SecureStorage error: {ex.Message}"); |
| 23 | } | 31 | } |
| 24 | - } | ||
| 25 | - public static Task<string?> LoadAsync() => | ||
| 26 | - SecureStorage.GetAsync(Key); | ||
| 27 | - public static Task ClearAsync() | ||
| 28 | - => SecureStorage.SetAsync(Key, string.Empty); // 清空即可;也可用 Remove | ||
| 29 | 32 | ||
| 33 | + // 兜底 | ||
| 34 | + var fallback = Preferences.Get(Key, null); | ||
| 35 | + System.Diagnostics.Debug.WriteLine($"[TokenStorage] Loaded from Preferences, len={fallback?.Length}"); | ||
| 36 | + return fallback; | ||
| 37 | + } | ||
| 30 | 38 | ||
| 39 | + public static Task ClearAsync() | ||
| 40 | + { | ||
| 41 | + try { SecureStorage.Remove(Key); } catch { } | ||
| 42 | + Preferences.Remove(Key); | ||
| 43 | + System.Diagnostics.Debug.WriteLine("[TokenStorage] Cleared"); | ||
| 44 | + return Task.CompletedTask; | ||
| 45 | + } | ||
| 31 | } | 46 | } |
| @@ -29,18 +29,17 @@ public partial class LoginViewModel : ObservableObject | @@ -29,18 +29,17 @@ public partial class LoginViewModel : ObservableObject | ||
| 29 | 29 | ||
| 30 | try | 30 | try |
| 31 | { | 31 | { |
| 32 | - // 启动时已 EnsureLatestAsync,这里只读取 AppData 中已生效配置 | 32 | + // 读取配置(App 启动时如果已 EnsureLatestAsync,这里 Load() 就够了) |
| 33 | var cfg = _cfg.Load(); | 33 | var cfg = _cfg.Load(); |
| 34 | - | 34 | + var scheme = cfg["server"]?["scheme"]?.GetValue<string>() ?? "http"; |
| 35 | var host = cfg["server"]?["ipAddress"]?.GetValue<string>() ?? "127.0.0.1"; | 35 | var host = cfg["server"]?["ipAddress"]?.GetValue<string>() ?? "127.0.0.1"; |
| 36 | - var port = cfg["server"]?["port"]?.GetValue<int?>() ?? 80; | 36 | + var port = cfg["server"]?["port"]?.GetValue<int?>(); |
| 37 | 37 | ||
| 38 | - var path = cfg["apiEndpoints"]?["login"]?.GetValue<string>() ?? "normalService/pda/auth/login"; | 38 | + var path = cfg["apiEndpoints"]?["login"]?.GetValue<string>() ?? "/normalService/pda/auth/login"; |
| 39 | if (!path.StartsWith("/")) path = "/" + path; | 39 | if (!path.StartsWith("/")) path = "/" + path; |
| 40 | 40 | ||
| 41 | - // 直接拼绝对 URL,避免修改 HttpClient.BaseAddress 引发异常 | ||
| 42 | - var fullUrl = new Uri($"http://{host}:{port}{path}"); | ||
| 43 | - System.Diagnostics.Debug.WriteLine($"[API] {fullUrl}"); | 41 | + var baseUrl = port is > 0 and < 65536 ? $"{scheme}://{host}:{port}" : $"{scheme}://{host}"; |
| 42 | + var fullUrl = new Uri(baseUrl + path); | ||
| 44 | 43 | ||
| 45 | if (string.IsNullOrWhiteSpace(UserName) || string.IsNullOrWhiteSpace(Password)) | 44 | if (string.IsNullOrWhiteSpace(UserName) || string.IsNullOrWhiteSpace(Password)) |
| 46 | { | 45 | { |
| @@ -51,37 +50,34 @@ public partial class LoginViewModel : ObservableObject | @@ -51,37 +50,34 @@ public partial class LoginViewModel : ObservableObject | ||
| 51 | var payload = new { username = UserName, password = Password }; | 50 | var payload = new { username = UserName, password = Password }; |
| 52 | using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); | 51 | using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); |
| 53 | 52 | ||
| 53 | + // 登录接口不需要带 Bearer;我们仍用 ApiClient 调 | ||
| 54 | var resp = await ApiClient.Instance.PostAsJsonAsync(fullUrl, payload, cts.Token); | 54 | var resp = await ApiClient.Instance.PostAsJsonAsync(fullUrl, payload, cts.Token); |
| 55 | + var raw = await resp.Content.ReadAsStringAsync(cts.Token); | ||
| 55 | 56 | ||
| 56 | if (!resp.IsSuccessStatusCode) | 57 | if (!resp.IsSuccessStatusCode) |
| 57 | { | 58 | { |
| 58 | - var raw = await resp.Content.ReadAsStringAsync(cts.Token); | ||
| 59 | - await Application.Current.MainPage.DisplayAlert("登录失败", | ||
| 60 | - $"HTTP {(int)resp.StatusCode}: {raw}", "确定"); | 59 | + await Application.Current.MainPage.DisplayAlert("登录失败", $"HTTP {(int)resp.StatusCode}: {raw}", "确定"); |
| 61 | return; | 60 | return; |
| 62 | } | 61 | } |
| 63 | 62 | ||
| 64 | - var result = await resp.Content.ReadFromJsonAsync<ApiResponse<LoginResult>>(_json, cts.Token); | ||
| 65 | - bool ok = (result?.success == true) || (result?.code is 0 or 200); | ||
| 66 | - var token = result?.result?.token; | 63 | + var result = JsonSerializer.Deserialize<ApiResponse<LoginResult>>(raw, |
| 64 | + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); | ||
| 67 | 65 | ||
| 68 | - if (ok && !string.IsNullOrWhiteSpace(token)) | ||
| 69 | - { | ||
| 70 | - await TokenStorage.SaveAsync(token!); | ||
| 71 | - ApiClient.SetBearer(token); | ||
| 72 | - if (ok && !string.IsNullOrWhiteSpace(token)) | ||
| 73 | - { | ||
| 74 | - await TokenStorage.SaveAsync(token); | ||
| 75 | - ApiClient.SetBearer(token); | ||
| 76 | - | ||
| 77 | - App.SwitchToLoggedInShell(); | ||
| 78 | - } | 66 | + var ok = (result?.success == true) || (result?.code is 0 or 200); |
| 67 | + var token = result?.result?.token; | ||
| 79 | 68 | ||
| 80 | - } | ||
| 81 | - else | 69 | + if (!ok || string.IsNullOrWhiteSpace(token)) |
| 82 | { | 70 | { |
| 83 | await Application.Current.MainPage.DisplayAlert("登录失败", result?.message ?? "登录返回无效", "确定"); | 71 | await Application.Current.MainPage.DisplayAlert("登录失败", result?.message ?? "登录返回无效", "确定"); |
| 72 | + return; | ||
| 84 | } | 73 | } |
| 74 | + | ||
| 75 | + // ★ 只需保存;后续所有经 DI 的 HttpClient 都会由 AuthHeaderHandler 自动加 Authorization 头 | ||
| 76 | + await TokenStorage.SaveAsync(token!); | ||
| 77 | + System.Diagnostics.Debug.WriteLine("saved token len=" + token?.Length); | ||
| 78 | + | ||
| 79 | + // 进入主壳 | ||
| 80 | + App.SwitchToLoggedInShell(); | ||
| 85 | } | 81 | } |
| 86 | catch (OperationCanceledException) | 82 | catch (OperationCanceledException) |
| 87 | { | 83 | { |
| @@ -98,6 +94,7 @@ public partial class LoginViewModel : ObservableObject | @@ -98,6 +94,7 @@ public partial class LoginViewModel : ObservableObject | ||
| 98 | } | 94 | } |
| 99 | 95 | ||
| 100 | 96 | ||
| 97 | + | ||
| 101 | [RelayCommand] | 98 | [RelayCommand] |
| 102 | private void TogglePassword() => ShowPassword = !ShowPassword; | 99 | private void TogglePassword() => ShowPassword = !ShowPassword; |
| 103 | 100 |
| 1 | using CommunityToolkit.Mvvm.ComponentModel; | 1 | using CommunityToolkit.Mvvm.ComponentModel; |
| 2 | using CommunityToolkit.Mvvm.Input; | 2 | using CommunityToolkit.Mvvm.Input; |
| 3 | +using IndustrialControl.Services; | ||
| 3 | using System.Collections.ObjectModel; | 4 | using System.Collections.ObjectModel; |
| 4 | 5 | ||
| 5 | namespace IndustrialControl.ViewModels; | 6 | namespace IndustrialControl.ViewModels; |
| 6 | 7 | ||
| 7 | public partial class MoldOutboundExecuteViewModel : ObservableObject | 8 | public partial class MoldOutboundExecuteViewModel : ObservableObject |
| 8 | { | 9 | { |
| 10 | + private readonly IWorkOrderApi _api; | ||
| 11 | + | ||
| 9 | [ObservableProperty] private string? orderNo; | 12 | [ObservableProperty] private string? orderNo; |
| 13 | + [ObservableProperty] private string? orderId; | ||
| 14 | + [ObservableProperty] private string? statusText; // 状态 | ||
| 15 | + [ObservableProperty] private string? orderName; // 工单名称 | ||
| 16 | + [ObservableProperty] private string? urgent; // 优先级(中文) | ||
| 17 | + [ObservableProperty] private string? productName; // 产品/物料名称 | ||
| 18 | + [ObservableProperty] private string? planQtyText; // 生产数量(文本) | ||
| 19 | + [ObservableProperty] private string? planStartText; // 计划开始日期(文本) | ||
| 20 | + [ObservableProperty] private string? createDateText; // 创建日期(文本) | ||
| 21 | + [ObservableProperty] private string? bomCode; // BOM编号 | ||
| 22 | + [ObservableProperty] private string? routeName; // 工艺路线名称 | ||
| 10 | 23 | ||
| 24 | + public ObservableCollection<BaseInfoItem> BaseInfos { get; } = new(); | ||
| 11 | public ObservableCollection<MoldOutboundDetailRow> ScanDetails { get; } = new(); | 25 | public ObservableCollection<MoldOutboundDetailRow> ScanDetails { get; } = new(); |
| 26 | + public ObservableCollection<WoStep> WorkflowSteps { get; } = new(); | ||
| 27 | + public ObservableCollection<UiProcessTask> ProcessTasks { get; } = new(); | ||
| 28 | + | ||
| 29 | + private Dictionary<string, string> _auditMap = new(); | ||
| 12 | 30 | ||
| 13 | - [RelayCommand] | ||
| 14 | - public async Task LoadAsync(string orderNo) | 31 | + public MoldOutboundExecuteViewModel(IWorkOrderApi api) |
| 32 | + { | ||
| 33 | + _api = api; | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + public async Task LoadAsync(string orderNo, string? orderId = null, IEnumerable<BaseInfoItem>? baseInfos = null) | ||
| 15 | { | 37 | { |
| 16 | OrderNo = orderNo; | 38 | OrderNo = orderNo; |
| 39 | + OrderId = orderId; | ||
| 40 | + | ||
| 41 | + await EnsureDictsLoadedAsync(); | ||
| 42 | + InitWorkflowStepsFromDict(); | ||
| 43 | + | ||
| 44 | + // 先把 BaseInfos(如果有)落位到固定字段,保证中间表格有值 | ||
| 45 | + if (baseInfos != null) | ||
| 46 | + { | ||
| 47 | + BaseInfos.Clear(); | ||
| 48 | + foreach (var it in baseInfos) BaseInfos.Add(it); | ||
| 49 | + PopulateFixedFieldsFromBaseInfos(); // ★ 新增调用(你之前缺少这句) | ||
| 50 | + } | ||
| 17 | 51 | ||
| 18 | - // TODO: 调接口获取:需求基础信息 + 扫描明细 | ||
| 19 | - await Task.Delay(80); | 52 | + var statusTextFromSearch = BaseInfos.FirstOrDefault(x => x.Key.Replace(":", "") == "状态")?.Value; |
| 53 | + ApplyStatusFromSearch(statusTextFromSearch); | ||
| 20 | 54 | ||
| 21 | - // mock 数据 | ||
| 22 | - ScanDetails.Clear(); | ||
| 23 | - ScanDetails.Add(new MoldOutboundDetailRow { Index = 1, MoldCode = "XXX", MoldModel = "MU_DHJD_01", Qty = 1, Bin = "A202", Selected = true }); | ||
| 24 | - ScanDetails.Add(new MoldOutboundDetailRow { Index = 2, MoldCode = "XXX", MoldModel = "MU_DHJD_01", Qty = 1, Bin = "A202" }); | ||
| 25 | - ScanDetails.Add(new MoldOutboundDetailRow { Index = 3, MoldCode = "XXX", MoldModel = "MU_DHJD_02", Qty = 1, Bin = "A202" }); | 55 | + await FillWorkflowTimesFromApi(); |
| 56 | + ProcessTasks.Clear(); | ||
| 57 | + await LoadProcessTasksAsync(); | ||
| 26 | } | 58 | } |
| 27 | 59 | ||
| 28 | - [RelayCommand] public Task ConfirmAsync() => Task.CompletedTask; // TODO | ||
| 29 | - [RelayCommand] public Task CancelScanAsync() => Task.CompletedTask; // TODO | 60 | + |
| 61 | + private async Task EnsureDictsLoadedAsync() | ||
| 62 | + { | ||
| 63 | + var bundle = await _api.GetWorkOrderDictsAsync(); | ||
| 64 | + _auditMap = bundle.AuditStatus | ||
| 65 | + .Where(d => !string.IsNullOrWhiteSpace(d.dictItemValue)) | ||
| 66 | + .ToDictionary(d => d.dictItemValue!, d => d.dictItemName ?? d.dictItemValue!); | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + public void SetFixedFieldsFromBaseInfos(IEnumerable<BaseInfoItem> items) | ||
| 70 | + { | ||
| 71 | + BaseInfos.Clear(); | ||
| 72 | + foreach (var it in items) BaseInfos.Add(it); | ||
| 73 | + PopulateFixedFieldsFromBaseInfos(); // ← 把值填到 StatusText/OrderName/... 等固定属性 | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + private async Task LoadProcessTasksAsync() | ||
| 77 | + { | ||
| 78 | + ProcessTasks.Clear(); | ||
| 79 | + if (string.IsNullOrWhiteSpace(OrderNo)) return; | ||
| 80 | + | ||
| 81 | + var page = await _api.PageWorkProcessTasksAsync(OrderNo!, 1, 50); | ||
| 82 | + var records = page?.result?.records; | ||
| 83 | + if (records == null || records.Count == 0) return; | ||
| 84 | + | ||
| 85 | + int i = 1; | ||
| 86 | + foreach (var t in records.OrderBy(x => x.sortNumber ?? int.MaxValue)) | ||
| 87 | + { | ||
| 88 | + var statusName = MapByDict(_auditMap, t.auditStatus); // e.g. "未开始"/"进行中"/"完成" | ||
| 89 | + ProcessTasks.Add(new UiProcessTask | ||
| 90 | + { | ||
| 91 | + Index = i++, | ||
| 92 | + Code = t.processCode ?? "", | ||
| 93 | + Name = t.processName ?? "", | ||
| 94 | + PlanQty = (t.scheQty ?? 0).ToString("0.####"), | ||
| 95 | + DoneQty = (t.completedQty ?? 0).ToString("0.####"), | ||
| 96 | + Start = ToShort(t.startDate), | ||
| 97 | + End = ToShort(t.endDate), | ||
| 98 | + StatusText = statusName | ||
| 99 | + }); | ||
| 100 | + } | ||
| 101 | + } | ||
| 102 | + // 用字典生成步骤后,记得打上首尾标记 | ||
| 103 | + private void InitWorkflowStepsFromDict() | ||
| 104 | + { | ||
| 105 | + WorkflowSteps.Clear(); | ||
| 106 | + foreach (var kv in _auditMap.OrderBy(kv => int.Parse(kv.Key))) | ||
| 107 | + { | ||
| 108 | + WorkflowSteps.Add(new WoStep | ||
| 109 | + { | ||
| 110 | + Index = int.Parse(kv.Key), | ||
| 111 | + Title = kv.Value | ||
| 112 | + }); | ||
| 113 | + } | ||
| 114 | + if (WorkflowSteps.Count > 0) | ||
| 115 | + { | ||
| 116 | + WorkflowSteps[0].IsFirst = true; | ||
| 117 | + WorkflowSteps[^1].IsLast = true; | ||
| 118 | + } | ||
| 119 | + } | ||
| 120 | + | ||
| 121 | + // 从接口把 statusTime 灌到对应节点上(保证 Time 有值就能显示) | ||
| 122 | + private async Task FillWorkflowTimesFromApi() | ||
| 123 | + { | ||
| 124 | + if (string.IsNullOrWhiteSpace(OrderId)) return; | ||
| 125 | + | ||
| 126 | + var resp = await _api.GetWorkOrderWorkflowAsync(OrderId!); | ||
| 127 | + var list = resp?.result; | ||
| 128 | + if (list is null || list.Count == 0) return; | ||
| 129 | + | ||
| 130 | + foreach (var step in WorkflowSteps) | ||
| 131 | + { | ||
| 132 | + var match = list.FirstOrDefault(x => x.statusValue == step.Index.ToString()); | ||
| 133 | + if (match != null && DateTime.TryParse(match.statusTime, out var dt)) | ||
| 134 | + step.Time = dt; | ||
| 135 | + } | ||
| 136 | + } | ||
| 137 | + | ||
| 138 | + | ||
| 139 | + | ||
| 140 | + private void ApplyStatusFromSearch(string? statusText) | ||
| 141 | + { | ||
| 142 | + if (string.IsNullOrWhiteSpace(statusText)) return; | ||
| 143 | + | ||
| 144 | + var match = _auditMap.FirstOrDefault(x => x.Value == statusText); | ||
| 145 | + if (string.IsNullOrEmpty(match.Key)) return; | ||
| 146 | + if (!int.TryParse(match.Key, out int cur)) return; | ||
| 147 | + | ||
| 148 | + foreach (var s in WorkflowSteps) | ||
| 149 | + { | ||
| 150 | + s.IsActive = (s.Index == cur); | ||
| 151 | + s.IsDone = (s.Index < cur); | ||
| 152 | + } | ||
| 153 | + } | ||
| 154 | + | ||
| 155 | + | ||
| 156 | + | ||
| 157 | + | ||
| 158 | + | ||
| 159 | + private static string MapByDict(Dictionary<string, string> map, string? code) | ||
| 160 | + { | ||
| 161 | + if (string.IsNullOrWhiteSpace(code)) return string.Empty; | ||
| 162 | + return map.TryGetValue(code, out var name) ? name : code; | ||
| 163 | + } | ||
| 164 | + | ||
| 165 | + private static string ToShort(string? s) | ||
| 166 | + => DateTime.TryParse(s, out var d) ? d.ToString("MM-dd HH:mm") : ""; | ||
| 167 | + | ||
| 168 | + [RelayCommand] public Task ConfirmAsync() => Task.CompletedTask; | ||
| 169 | + [RelayCommand] public Task CancelScanAsync() => Task.CompletedTask; | ||
| 170 | + private string GetInfo(params string[] keys) | ||
| 171 | + { | ||
| 172 | + if (BaseInfos.Count == 0) return ""; | ||
| 173 | + foreach (var key in keys) | ||
| 174 | + { | ||
| 175 | + var hit = BaseInfos.FirstOrDefault(x => | ||
| 176 | + string.Equals(x.Key?.Replace(":", ""), key, StringComparison.OrdinalIgnoreCase)); | ||
| 177 | + if (hit is not null) return hit.Value ?? ""; | ||
| 178 | + } | ||
| 179 | + return ""; | ||
| 180 | + } | ||
| 181 | + private void PopulateFixedFieldsFromBaseInfos() | ||
| 182 | + { | ||
| 183 | + StatusText = GetInfo("状态"); | ||
| 184 | + OrderName = GetInfo("工单名称", "工单名", "订单名称"); | ||
| 185 | + Urgent = GetInfo("优先级", "紧急程度"); | ||
| 186 | + ProductName = GetInfo("产品名称", "物料名称"); | ||
| 187 | + PlanQtyText = GetInfo("生产数量", "计划数量"); | ||
| 188 | + PlanStartText = GetInfo("计划开始日期", "计划开始时间", "计划开始"); | ||
| 189 | + CreateDateText = GetInfo("创建日期", "创建时间"); | ||
| 190 | + BomCode = GetInfo("BOM编号", "BOM码", "BOM"); | ||
| 191 | + RouteName = GetInfo("工艺路线名称", "工艺路线", "工艺名"); | ||
| 192 | + } | ||
| 30 | } | 193 | } |
| 31 | 194 | ||
| 32 | -public class MoldOutboundDetailRow | 195 | +// ==== 模型类 ==== |
| 196 | +public class BaseInfoItem { public string Key { get; set; } = ""; public string Value { get; set; } = ""; } | ||
| 197 | +public class MoldOutboundDetailRow { public int Index { get; set; } public bool Selected { get; set; } public string MoldCode { get; set; } = ""; public string MoldModel { get; set; } = ""; public int Qty { get; set; } public string Bin { get; set; } = ""; } | ||
| 198 | +public partial class WoStep : ObservableObject | ||
| 33 | { | 199 | { |
| 34 | - public int Index { get; set; } | ||
| 35 | - public bool Selected { get; set; } | ||
| 36 | - public string MoldCode { get; set; } = ""; | ||
| 37 | - public string MoldModel { get; set; } = ""; | ||
| 38 | - public int Qty { get; set; } | ||
| 39 | - public string Bin { get; set; } = ""; | 200 | + [ObservableProperty] private int index; |
| 201 | + [ObservableProperty] private string title = ""; | ||
| 202 | + | ||
| 203 | + // 当 Time 变化时,自动通知 TimeText 一起变更 | ||
| 204 | + [ObservableProperty, NotifyPropertyChangedFor(nameof(TimeText))] | ||
| 205 | + private DateTime? time; | ||
| 206 | + | ||
| 207 | + [ObservableProperty] private bool isActive; | ||
| 208 | + [ObservableProperty] private bool isDone; | ||
| 209 | + [ObservableProperty] private bool isFirst; // 用于隐藏左连接线 | ||
| 210 | + [ObservableProperty] private bool isLast; // 用于隐藏右连接线 | ||
| 211 | + | ||
| 212 | + // 给 XAML 用的已格式化文本(只保留到“日”) | ||
| 213 | + public string TimeText => time.HasValue ? time.Value.ToString("yyyy-MM-dd") : string.Empty; | ||
| 40 | } | 214 | } |
| 215 | + | ||
| 216 | +public sealed class UiProcessTask { public int Index { get; set; } public string Code { get; set; } = ""; public string Name { get; set; } = ""; public string PlanQty { get; set; } = ""; public string DoneQty { get; set; } = ""; public string Start { get; set; } = ""; public string End { get; set; } = ""; public string StatusText { get; set; } = ""; } |
| 1 | -using CommunityToolkit.Mvvm.ComponentModel; | 1 | +// ViewModels/WorkOrderSearchViewModel.cs |
| 2 | +using CommunityToolkit.Mvvm.ComponentModel; | ||
| 2 | using CommunityToolkit.Mvvm.Input; | 3 | using CommunityToolkit.Mvvm.Input; |
| 4 | +using IndustrialControl.Services; | ||
| 5 | +using System; | ||
| 6 | +using System.Collections.Generic; | ||
| 3 | using System.Collections.ObjectModel; | 7 | using System.Collections.ObjectModel; |
| 4 | -using System.Windows.Input; | 8 | +using System.Globalization; |
| 9 | +using System.Linq; | ||
| 10 | +using System.Text.Json; | ||
| 11 | +using System.Threading.Tasks; | ||
| 5 | 12 | ||
| 6 | -namespace IndustrialControl.ViewModels; | ||
| 7 | - | ||
| 8 | -public class WorkOrderSearchViewModel : ObservableObject | 13 | +namespace IndustrialControl.ViewModels |
| 9 | { | 14 | { |
| 10 | - public ObservableCollection<WorkOrderDto> Orders { get; } = new(); | ||
| 11 | - | ||
| 12 | - private string? _keyword; | ||
| 13 | - public string? Keyword | 15 | + public partial class WorkOrderSearchViewModel : ObservableObject |
| 14 | { | 16 | { |
| 15 | - get => _keyword; | ||
| 16 | - set => SetProperty(ref _keyword, value); | ||
| 17 | - } | 17 | + private readonly IWorkOrderApi _api; |
| 18 | 18 | ||
| 19 | - public DateTime StartDate { get; set; } = DateTime.Today.AddDays(-7); | ||
| 20 | - public DateTime EndDate { get; set; } = DateTime.Today; | 19 | + [ObservableProperty] private bool isBusy; |
| 20 | + [ObservableProperty] private string? keyword; | ||
| 21 | + [ObservableProperty] private DateTime startDate = DateTime.Today.AddDays(-7); | ||
| 22 | + [ObservableProperty] private DateTime endDate = DateTime.Today; | ||
| 23 | + [ObservableProperty] private string? selectedStatus = "全部"; | ||
| 24 | + [ObservableProperty] private int pageIndex = 1; | ||
| 25 | + [ObservableProperty] private int pageSize = 50; | ||
| 26 | + private readonly Dictionary<string, string> _auditMap = new(); // "1" -> "执行中" | ||
| 27 | + private readonly Dictionary<string, string> _urgentMap = new(); // "level2" -> "中" | ||
| 28 | + public ObservableCollection<StatusOption> StatusOptions { get; } = new(); | ||
| 29 | + [ObservableProperty] private StatusOption? selectedStatusOption; | ||
| 30 | + private bool _dictsLoaded = false; | ||
| 21 | 31 | ||
| 22 | - public List<string> StatusList { get; } = new() { "待执行", "执行中", "已完成", "已取消" }; | ||
| 23 | - private string? _selectedStatus = "待执行"; | ||
| 24 | - public string? SelectedStatus | ||
| 25 | - { | ||
| 26 | - get => _selectedStatus; | ||
| 27 | - set => SetProperty(ref _selectedStatus, value); | ||
| 28 | - } | 32 | + public ObservableCollection<WorkOrderDto> Orders { get; } = new(); |
| 29 | 33 | ||
| 30 | - private WorkOrderDto? _selectedOrder; | ||
| 31 | - public WorkOrderDto? SelectedOrder | ||
| 32 | - { | ||
| 33 | - get => _selectedOrder; | ||
| 34 | - set => SetProperty(ref _selectedOrder, value); | ||
| 35 | - } | 34 | + public IAsyncRelayCommand SearchCommand { get; } |
| 35 | + public IRelayCommand ClearCommand { get; } | ||
| 36 | 36 | ||
| 37 | - public ICommand SearchCommand { get; } | ||
| 38 | - public ICommand ClearCommand { get; } | 37 | + public WorkOrderSearchViewModel(IWorkOrderApi api) |
| 38 | + { | ||
| 39 | + _api = api; | ||
| 40 | + SearchCommand = new AsyncRelayCommand(SearchAsync); | ||
| 41 | + ClearCommand = new RelayCommand(ClearFilters); | ||
| 42 | + _ = EnsureDictsLoadedAsync(); // fire-and-forget | ||
| 43 | + } | ||
| 44 | + private async Task EnsureDictsLoadedAsync() | ||
| 45 | + { | ||
| 46 | + if (_dictsLoaded) return; | ||
| 39 | 47 | ||
| 40 | - public WorkOrderSearchViewModel() | ||
| 41 | - { | ||
| 42 | - SearchCommand = new AsyncRelayCommand(SearchAsync); | ||
| 43 | - ClearCommand = new RelayCommand(Clear); | ||
| 44 | - } | 48 | + try |
| 49 | + { | ||
| 50 | + var bundle = await _api.GetWorkOrderDictsAsync(); | ||
| 45 | 51 | ||
| 46 | - public async Task SearchAsync() | ||
| 47 | - { | ||
| 48 | - // TODO: 调后端 API;这里先用 Mock | ||
| 49 | - await Task.Delay(120); | ||
| 50 | - Orders.Clear(); | ||
| 51 | - | ||
| 52 | - // 伪数据(根据条件简单过滤) | ||
| 53 | - var all = MockOrders(); | ||
| 54 | - var filtered = all.Where(o => | ||
| 55 | - (string.IsNullOrWhiteSpace(Keyword) || o.OrderNo.Contains(Keyword!, StringComparison.OrdinalIgnoreCase)) && | ||
| 56 | - o.CreateDate.Date >= StartDate.Date && o.CreateDate.Date <= EndDate.Date && | ||
| 57 | - (string.IsNullOrWhiteSpace(SelectedStatus) || o.Status == SelectedStatus) | ||
| 58 | - ); | ||
| 59 | - | ||
| 60 | - foreach (var m in filtered) Orders.Add(m); | ||
| 61 | - } | 52 | + // 1) 填充状态下拉 |
| 53 | + StatusOptions.Clear(); | ||
| 54 | + StatusOptions.Add(new StatusOption { Text = "全部", Value = null }); | ||
| 55 | + foreach (var d in bundle.AuditStatus) | ||
| 56 | + { | ||
| 57 | + if (string.IsNullOrWhiteSpace(d.dictItemValue)) continue; | ||
| 58 | + StatusOptions.Add(new StatusOption | ||
| 59 | + { | ||
| 60 | + Text = d.dictItemName ?? d.dictItemValue!, | ||
| 61 | + Value = d.dictItemValue | ||
| 62 | + }); | ||
| 63 | + } | ||
| 64 | + SelectedStatusOption ??= StatusOptions.FirstOrDefault(); | ||
| 62 | 65 | ||
| 63 | - public void Clear() | ||
| 64 | - { | ||
| 65 | - Keyword = string.Empty; | ||
| 66 | - StartDate = DateTime.Today.AddDays(-7); | ||
| 67 | - EndDate = DateTime.Today; | ||
| 68 | - SelectedStatus = "待执行"; | ||
| 69 | - Orders.Clear(); | ||
| 70 | - } | 66 | + // 2) 建立两个码→名的映射表 |
| 67 | + _auditMap.Clear(); | ||
| 68 | + foreach (var d in bundle.AuditStatus) | ||
| 69 | + if (!string.IsNullOrWhiteSpace(d.dictItemValue)) | ||
| 70 | + _auditMap[d.dictItemValue!] = d.dictItemName ?? d.dictItemValue!; | ||
| 71 | 71 | ||
| 72 | - private IEnumerable<WorkOrderDto> MockOrders() | ||
| 73 | - { | ||
| 74 | - yield return new WorkOrderDto | 72 | + _urgentMap.Clear(); |
| 73 | + foreach (var d in bundle.Urgent) | ||
| 74 | + if (!string.IsNullOrWhiteSpace(d.dictItemValue)) | ||
| 75 | + _urgentMap[d.dictItemValue!] = d.dictItemName ?? d.dictItemValue!; | ||
| 76 | + | ||
| 77 | + _dictsLoaded = true; | ||
| 78 | + } | ||
| 79 | + catch (Exception ex) | ||
| 80 | + { | ||
| 81 | + System.Diagnostics.Debug.WriteLine($"[VM] Load dicts error: {ex}"); | ||
| 82 | + // 兜底:至少保证一个“全部” | ||
| 83 | + if (StatusOptions.Count == 0) | ||
| 84 | + StatusOptions.Add(new StatusOption { Text = "全部", Value = null }); | ||
| 85 | + SelectedStatusOption ??= StatusOptions.First(); | ||
| 86 | + _dictsLoaded = true; // 防止重复打接口 | ||
| 87 | + } | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + public async Task SearchAsync() | ||
| 75 | { | 91 | { |
| 76 | - OrderNo = "SCGD20250601002", | ||
| 77 | - OrderName = "XXXXXXXXXX", | ||
| 78 | - Status = "待执行", | ||
| 79 | - ProductName = "梅林午餐肉罐头198g", | ||
| 80 | - Quantity = 10000, | ||
| 81 | - CreateDate = DateTime.Today.AddDays(-3) | ||
| 82 | - }; | ||
| 83 | - yield return new WorkOrderDto | 92 | + if (IsBusy) return; |
| 93 | + IsBusy = true; | ||
| 94 | + try | ||
| 95 | + { | ||
| 96 | + await EnsureDictsLoadedAsync(); // ★ 先确保字典到位 | ||
| 97 | + | ||
| 98 | + var byOrderNo = !string.IsNullOrWhiteSpace(Keyword); | ||
| 99 | + DateTime? start = byOrderNo ? null : StartDate.Date; | ||
| 100 | + DateTime? end = byOrderNo ? null : EndDate.Date.AddDays(1); | ||
| 101 | + | ||
| 102 | + var q = new WorkOrderQuery | ||
| 103 | + { | ||
| 104 | + PageNo = PageIndex, | ||
| 105 | + PageSize = PageSize, | ||
| 106 | + AuditStatus = byOrderNo ? null : SelectedStatusOption?.Value, // ★ 直接传字典值("0"/"1"/...) | ||
| 107 | + CreatedTimeStart = start, | ||
| 108 | + CreatedTimeEnd = end, | ||
| 109 | + WorkOrderNo = byOrderNo ? Keyword!.Trim() : null | ||
| 110 | + }; | ||
| 111 | + | ||
| 112 | + var page = await _api.GetWorkOrdersAsync(q); | ||
| 113 | + var records = page?.result?.records | ||
| 114 | + ?? page?.result?.list?.records | ||
| 115 | + ?? new List<WorkOrderRecord>(); | ||
| 116 | + | ||
| 117 | + Orders.Clear(); | ||
| 118 | + foreach (var r in records) | ||
| 119 | + { | ||
| 120 | + // r.auditStatus 是 "0"/"1"/...,r.urgent 是 "level1/2/3" | ||
| 121 | + var statusName = MapByDict(_auditMap, r.auditStatus); | ||
| 122 | + var urgentName = MapByDict(_urgentMap, r.urgent); | ||
| 123 | + var createdAt = TryParseDt(r.createdTime); | ||
| 124 | + | ||
| 125 | + Orders.Add(new WorkOrderDto | ||
| 126 | + { | ||
| 127 | + Id = r.id ?? "", | ||
| 128 | + OrderNo = r.workOrderNo ?? "-", | ||
| 129 | + OrderName = r.workOrderName ?? "", | ||
| 130 | + MaterialCode = r.materialCode ?? "", | ||
| 131 | + MaterialName = r.materialName ?? "", | ||
| 132 | + LineName = r.lineName ?? "", | ||
| 133 | + Status = statusName, | ||
| 134 | + Urgent = urgentName, | ||
| 135 | + CurQty =(int?)r.curQty, | ||
| 136 | + CreateDate = createdAt?.ToString("yyyy-MM-dd") ?? (r.createdTime ?? ""), | ||
| 137 | + BomCode = r.bomCode, // e.g. "BOM00000006" | ||
| 138 | + RouteName = r.routeName, // e.g. "午餐肉罐头测试工序调整" | ||
| 139 | + WorkShopName = r.workShopName // e.g. "制造二组" | ||
| 140 | + }); | ||
| 141 | + } | ||
| 142 | + } | ||
| 143 | + finally { IsBusy = false; } | ||
| 144 | + } | ||
| 145 | + | ||
| 146 | + // 通用的码→名映射(字典里找不到就回退原码) | ||
| 147 | + private static string MapByDict(Dictionary<string, string> map, string? code) | ||
| 148 | + { | ||
| 149 | + if (string.IsNullOrWhiteSpace(code)) return string.Empty; | ||
| 150 | + return map.TryGetValue(code, out var name) ? name : code; | ||
| 151 | + } | ||
| 152 | + | ||
| 153 | + | ||
| 154 | + private void ClearFilters() | ||
| 155 | + { | ||
| 156 | + Keyword = string.Empty; | ||
| 157 | + SelectedStatus = "全部"; | ||
| 158 | + StartDate = DateTime.Today.AddDays(-7); | ||
| 159 | + EndDate = DateTime.Today; | ||
| 160 | + PageIndex = 1; | ||
| 161 | + SelectedStatusOption = StatusOptions.FirstOrDefault(); | ||
| 162 | + Orders.Clear(); | ||
| 163 | + } | ||
| 164 | + | ||
| 165 | + | ||
| 166 | + | ||
| 167 | + private static DateTime? TryParseDt(string? s) | ||
| 84 | { | 168 | { |
| 85 | - OrderNo = "SCGD20250601003", | ||
| 86 | - OrderName = "XXXXXXXXXX", | ||
| 87 | - Status = "待执行", | ||
| 88 | - ProductName = "梅林午餐肉罐头198g", | ||
| 89 | - Quantity = 5000, | ||
| 90 | - CreateDate = DateTime.Today.AddDays(-2) | ||
| 91 | - }; | ||
| 92 | - yield return new WorkOrderDto | 169 | + if (string.IsNullOrWhiteSpace(s)) return null; |
| 170 | + if (DateTime.TryParseExact(s.Trim(), "yyyy-MM-dd HH:mm:ss", | ||
| 171 | + CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt)) | ||
| 172 | + return dt; | ||
| 173 | + if (DateTime.TryParse(s, out dt)) return dt; | ||
| 174 | + return null; | ||
| 175 | + } | ||
| 176 | + // 点击一条工单进入执行页 | ||
| 177 | + [RelayCommand] | ||
| 178 | + private async Task GoExecuteAsync(WorkOrderDto? item) | ||
| 93 | { | 179 | { |
| 94 | - OrderNo = "SCGD20250601004", | ||
| 95 | - OrderName = "XXXXXXXXXX", | ||
| 96 | - Status = "执行中", | ||
| 97 | - ProductName = "梅林午餐肉罐头198g", | ||
| 98 | - Quantity = 8000, | ||
| 99 | - CreateDate = DateTime.Today.AddDays(-1) | ||
| 100 | - }; | 180 | + if (item is null) return; |
| 181 | + | ||
| 182 | + // 基础信息列表(显示在执行页“工单基础信息表格”) | ||
| 183 | + var baseInfos = new List<BaseInfoItem> | ||
| 184 | + { | ||
| 185 | + new() { Key = "工单编号", Value = item.OrderNo }, | ||
| 186 | + new() { Key = "状态", Value = item.Status }, | ||
| 187 | + new() { Key = "优先级", Value = item.Urgent }, | ||
| 188 | + new() { Key = "生产数量", Value = item.CurQty?.ToString() ?? "" }, | ||
| 189 | + new() { Key = "创建日期", Value = item.CreateDate }, | ||
| 190 | + new() { Key = "物料编码", Value = item.MaterialCode }, | ||
| 191 | + new() { Key = "物料名称", Value = item.MaterialName }, | ||
| 192 | + new() { Key = "产线", Value = item.LineName }, | ||
| 193 | + new() { Key = "BOM编号", Value = item.BomCode ?? "" }, | ||
| 194 | + new() { Key = "工艺路线", Value = item.RouteName ?? "" }, | ||
| 195 | + new() { Key = "车间", Value = item.WorkShopName ?? "" }, | ||
| 196 | + }; | ||
| 197 | + | ||
| 198 | + var baseInfoJson = JsonSerializer.Serialize(baseInfos); | ||
| 199 | + | ||
| 200 | + // 跳到执行页(把 orderId/orderNo/baseInfo 都带上) | ||
| 201 | + await Shell.Current.GoToAsync(nameof(Pages.MoldOutboundExecutePage), new Dictionary<string, object?> | ||
| 202 | + { | ||
| 203 | + ["orderNo"] = item.OrderNo, | ||
| 204 | + ["orderId"] = item.Id, | ||
| 205 | + ["baseInfo"] = baseInfoJson | ||
| 206 | + }); | ||
| 207 | + } | ||
| 208 | + } | ||
| 209 | + public class StatusOption | ||
| 210 | + { | ||
| 211 | + public string Text { get; set; } = ""; // 显示:dictItemName | ||
| 212 | + public string? Value { get; set; } // 参数:dictItemValue("0"/"1"...,全部用 null) | ||
| 213 | + public override string ToString() => Text; // 某些平台用 ToString() 展示 | ||
| 101 | } | 214 | } |
| 102 | } | 215 | } |
| @@ -2,36 +2,46 @@ | @@ -2,36 +2,46 @@ | ||
| 2 | using CommunityToolkit.Mvvm.Input; | 2 | using CommunityToolkit.Mvvm.Input; |
| 3 | using IndustrialControl.Services; | 3 | using IndustrialControl.Services; |
| 4 | using System.Collections.ObjectModel; | 4 | using System.Collections.ObjectModel; |
| 5 | -using System; | ||
| 6 | -using IndustrialControl.Pages; | ||
| 7 | 5 | ||
| 8 | namespace IndustrialControl.ViewModels; | 6 | namespace IndustrialControl.ViewModels; |
| 9 | 7 | ||
| 10 | public partial class InboundMaterialSearchViewModel : ObservableObject | 8 | public partial class InboundMaterialSearchViewModel : ObservableObject |
| 11 | { | 9 | { |
| 12 | - private readonly IWarehouseDataService _dataSvc; | ||
| 13 | - [ObservableProperty] private string? searchOrderNo; | ||
| 14 | - [ObservableProperty] private DateTime _createdDate = DateTime.Today; | 10 | + private readonly IInboundMaterialService _dataSvc; |
| 11 | + | ||
| 12 | + [ObservableProperty] private string searchOrderNo; | ||
| 13 | + [ObservableProperty] private DateTime startDate = DateTime.Today; | ||
| 14 | + [ObservableProperty] private DateTime endDate = DateTime.Today; | ||
| 15 | + private CancellationTokenSource? _searchCts; | ||
| 16 | + // 仅用于“高亮选中” | ||
| 15 | [ObservableProperty] private InboundOrderSummary? selectedOrder; | 17 | [ObservableProperty] private InboundOrderSummary? selectedOrder; |
| 16 | 18 | ||
| 17 | - public InboundMaterialSearchViewModel(IWarehouseDataService dataSvc) | 19 | + public InboundMaterialSearchViewModel(IInboundMaterialService dataSvc) |
| 18 | { | 20 | { |
| 19 | _dataSvc = dataSvc; | 21 | _dataSvc = dataSvc; |
| 20 | Orders = new ObservableCollection<InboundOrderSummary>(); | 22 | Orders = new ObservableCollection<InboundOrderSummary>(); |
| 21 | } | 23 | } |
| 22 | 24 | ||
| 23 | - | ||
| 24 | - | ||
| 25 | public ObservableCollection<InboundOrderSummary> Orders { get; } | 25 | public ObservableCollection<InboundOrderSummary> Orders { get; } |
| 26 | 26 | ||
| 27 | [RelayCommand] | 27 | [RelayCommand] |
| 28 | private async Task SearchAsync() | 28 | private async Task SearchAsync() |
| 29 | { | 29 | { |
| 30 | + _searchCts?.Cancel(); | ||
| 31 | + _searchCts = new CancellationTokenSource(); | ||
| 32 | + var ct = _searchCts.Token; | ||
| 30 | try | 33 | try |
| 31 | { | 34 | { |
| 32 | - var list = await _dataSvc.ListInboundOrdersAsync(SearchOrderNo, CreatedDate); | 35 | + var orderTypeList = new[] { "in_other", "in_purchase", "in_return" }; |
| 36 | + var list = await _dataSvc.ListInboundOrdersAsync( | ||
| 37 | + searchOrderNo, // 单号/条码 | ||
| 38 | + startDate, // 开始日期 | ||
| 39 | + endDate, // 结束日期(Service 内会扩到 23:59:59) | ||
| 40 | + null, // 不传单值 orderType,用 null 更清晰 | ||
| 41 | + orderTypeList, // 多类型数组 | ||
| 42 | + ct // ← 新增:取消令牌 | ||
| 43 | + ); | ||
| 33 | 44 | ||
| 34 | - // ★ 在主线程更新 ObservableCollection,避免看起来“没刷新” | ||
| 35 | await MainThread.InvokeOnMainThreadAsync(() => | 45 | await MainThread.InvokeOnMainThreadAsync(() => |
| 36 | { | 46 | { |
| 37 | Orders.Clear(); | 47 | Orders.Clear(); |
| @@ -42,7 +52,6 @@ public partial class InboundMaterialSearchViewModel : ObservableObject | @@ -42,7 +52,6 @@ public partial class InboundMaterialSearchViewModel : ObservableObject | ||
| 42 | } | 52 | } |
| 43 | }); | 53 | }); |
| 44 | 54 | ||
| 45 | - // (排查辅助)无数据时提示一下,确认命令确实执行了 | ||
| 46 | if (list == null || !list.Any()) | 55 | if (list == null || !list.Any()) |
| 47 | await Shell.Current.DisplayAlert("提示", "未查询到任何入库单", "确定"); | 56 | await Shell.Current.DisplayAlert("提示", "未查询到任何入库单", "确定"); |
| 48 | } | 57 | } |
| @@ -52,34 +61,44 @@ public partial class InboundMaterialSearchViewModel : ObservableObject | @@ -52,34 +61,44 @@ public partial class InboundMaterialSearchViewModel : ObservableObject | ||
| 52 | } | 61 | } |
| 53 | } | 62 | } |
| 54 | 63 | ||
| 55 | - // 打开明细(携带 orderNo 导航) | ||
| 56 | - [RelayCommand] | ||
| 57 | - private async Task OpenItemAsync(InboundOrderSummary item) | 64 | + // === 方案A:命令接收“当前项”作为参数,不依赖 SelectedOrder === |
| 65 | + [RelayCommand(CanExecute = nameof(CanGoInbound))] | ||
| 66 | + private async Task GoInboundAsync(InboundOrderSummary? item) | ||
| 58 | { | 67 | { |
| 59 | if (item is null) return; | 68 | if (item is null) return; |
| 60 | - await Shell.Current.GoToAsync( | ||
| 61 | - $"{nameof(InboundMaterialPage)}?orderNo={Uri.EscapeDataString(item.OrderNo)}"); | ||
| 62 | - } | ||
| 63 | 69 | ||
| 64 | - [RelayCommand(CanExecute = nameof(CanGoInbound))] | ||
| 65 | - private async Task GoInboundAsync() | ||
| 66 | - { | ||
| 67 | - if (SelectedOrder == null) return; | ||
| 68 | - // 导航到原“入库明细/扫描”页面,并传入 orderNo | ||
| 69 | - await Shell.Current.GoToAsync($"//InboundMaterial?orderNo={Uri.EscapeDataString(SelectedOrder.OrderNo)}"); | ||
| 70 | - } | ||
| 71 | - public async Task LoadOrderAsync(string orderNo) | 70 | + static string E(string? v) => Uri.EscapeDataString(v ?? ""); |
| 71 | + | ||
| 72 | + var o = item; | ||
| 73 | + | ||
| 74 | + await Shell.Current.GoToAsync( | ||
| 75 | + nameof(Pages.InboundMaterialPage), | ||
| 76 | + new Dictionary<string, object> | ||
| 72 | { | 77 | { |
| 73 | - var order = await _dataSvc.GetInboundOrderAsync(orderNo); | ||
| 74 | - // TODO: 将 order 映射到页面“基础信息”“待入库明细”等绑定源,保持你现有绑定字段/集合不变 | 78 | + ["instockId"] = o.instockId, |
| 79 | + ["instockNo"] = o.instockNo, | ||
| 80 | + ["orderType"] = o.orderType, | ||
| 81 | + ["orderTypeName"] = o.orderTypeName, | ||
| 82 | + ["purchaseNo"] = o.purchaseNo, | ||
| 83 | + ["supplierName"] = o.supplierName, | ||
| 84 | + ["arrivalNo"] = o.arrivalNo, | ||
| 85 | + ["createdTime"] = o.createdTime | ||
| 86 | + }); | ||
| 87 | + | ||
| 75 | } | 88 | } |
| 76 | - private bool CanGoInbound() => SelectedOrder != null; | 89 | + |
| 90 | + // 与命令同签名的 CanExecute | ||
| 91 | + private bool CanGoInbound(InboundOrderSummary? item) => item != null; | ||
| 77 | } | 92 | } |
| 78 | 93 | ||
| 79 | -/// <summary>用于列表显示的精简 DTO</summary> | 94 | +// 用于列表显示的精简 DTO |
| 80 | public record InboundOrderSummary( | 95 | public record InboundOrderSummary( |
| 81 | - string OrderNo, | ||
| 82 | - string InboundType, | ||
| 83 | - string Supplier, | ||
| 84 | - DateTime CreatedAt | 96 | + string instockId, |
| 97 | + string instockNo, | ||
| 98 | + string orderType, | ||
| 99 | + string orderTypeName, | ||
| 100 | + string purchaseNo, | ||
| 101 | + string supplierName, | ||
| 102 | + string arrivalNo, | ||
| 103 | + string createdTime | ||
| 85 | ); | 104 | ); |
| @@ -2,244 +2,268 @@ using CommunityToolkit.Mvvm.ComponentModel; | @@ -2,244 +2,268 @@ using CommunityToolkit.Mvvm.ComponentModel; | ||
| 2 | using CommunityToolkit.Mvvm.Input; | 2 | using CommunityToolkit.Mvvm.Input; |
| 3 | using System.Collections.ObjectModel; | 3 | using System.Collections.ObjectModel; |
| 4 | using IndustrialControl.Services; | 4 | using IndustrialControl.Services; |
| 5 | -using System.Threading.Tasks; | ||
| 6 | 5 | ||
| 7 | namespace IndustrialControl.ViewModels | 6 | namespace IndustrialControl.ViewModels |
| 8 | { | 7 | { |
| 9 | public partial class InboundMaterialViewModel : ObservableObject | 8 | public partial class InboundMaterialViewModel : ObservableObject |
| 10 | { | 9 | { |
| 11 | [ObservableProperty] private string? scanCode; | 10 | [ObservableProperty] private string? scanCode; |
| 12 | - private readonly IWarehouseDataService _warehouseSvc; | ||
| 13 | - public ObservableCollection<string> AvailableBins { get; set; } = new ObservableCollection<string> | ||
| 14 | -{ | ||
| 15 | - "CK1_A201", | ||
| 16 | - "CK1_A202", | ||
| 17 | - "CK1_A203", | ||
| 18 | - "CK1_A204" | ||
| 19 | -}; | ||
| 20 | - public ObservableCollection<OutScannedItem> ScannedList { get; } | ||
| 21 | - public ObservableCollection<PendingItem> PendingList { get; set; } | ||
| 22 | - | ||
| 23 | - [ObservableProperty] | ||
| 24 | - private OutScannedItem? selectedScanItem; | ||
| 25 | - | ||
| 26 | - public InboundMaterialViewModel(IWarehouseDataService warehouseSvc) | ||
| 27 | - { | ||
| 28 | - _warehouseSvc = warehouseSvc; | 11 | + private readonly IInboundMaterialService _warehouseSvc; |
| 29 | 12 | ||
| 30 | - // 初始化命令 | ||
| 31 | - ShowPendingCommand = new RelayCommand(() => SwitchTab(true)); | ||
| 32 | - ShowScannedCommand = new RelayCommand(() => SwitchTab(false)); | ||
| 33 | - ConfirmCommand = new AsyncRelayCommand(ConfirmInboundAsync); | 13 | + // === 基础信息(由搜索页带入) === |
| 14 | + [ObservableProperty] private string? instockId; | ||
| 15 | + [ObservableProperty] private string? instockNo; | ||
| 16 | + [ObservableProperty] private string? orderType; | ||
| 17 | + [ObservableProperty] private string? orderTypeName; | ||
| 18 | + [ObservableProperty] private string? purchaseNo; | ||
| 19 | + [ObservableProperty] private string? supplierName; | ||
| 20 | + [ObservableProperty] private string? createdTime; | ||
| 34 | 21 | ||
| 35 | - // 默认显示待入库明细 | ||
| 36 | - IsPendingVisible = true; | ||
| 37 | - IsScannedVisible = false; | 22 | + // 列表数据源 |
| 23 | + public ObservableCollection<string> AvailableBins { get; } = new(); | ||
| 24 | + public ObservableCollection<OutScannedItem> ScannedList { get; } = new(); | ||
| 25 | + public ObservableCollection<PendingItem> PendingList { get; } = new(); | ||
| 38 | 26 | ||
| 39 | - // 测试数据(上线可去掉) | ||
| 40 | - PendingList = new ObservableCollection<PendingItem> | ||
| 41 | - { | ||
| 42 | - new PendingItem { Name="物料1", Spec="XXX", PendingQty=100, Bin="CK1_A201", ScannedQty=80 }, | ||
| 43 | - new PendingItem { Name="物料2", Spec="YYY", PendingQty=50, Bin="CK1_A202", ScannedQty=0 } | ||
| 44 | - }; | 27 | + [ObservableProperty] private OutScannedItem? selectedScanItem; |
| 45 | 28 | ||
| 46 | - ScannedList = new ObservableCollection<OutScannedItem> | ||
| 47 | - { | ||
| 48 | - new OutScannedItem { IsSelected=false, Barcode="FSC2025060300001", Name="物料1", Spec="XXX", Bin="CK1_A201", Qty=1 }, | ||
| 49 | - new OutScannedItem { IsSelected=false, Barcode="FSC2025060300002", Name="物料1", Spec="XXX", Bin="请选择", Qty=1 } | ||
| 50 | - }; | ||
| 51 | - } | ||
| 52 | - | ||
| 53 | - // 基础信息 | ||
| 54 | - [ObservableProperty] private string orderNo; | ||
| 55 | - [ObservableProperty] private string linkedDeliveryNo; | ||
| 56 | - [ObservableProperty] private string linkedPurchaseNo; | ||
| 57 | - [ObservableProperty] private string supplier; | 29 | + // Tab 控制 |
| 30 | + [ObservableProperty] private bool isPendingVisible = true; | ||
| 31 | + [ObservableProperty] private bool isScannedVisible = false; | ||
| 58 | 32 | ||
| 59 | - // Tab 显示控制 | ||
| 60 | - [ObservableProperty] private bool isPendingVisible; | ||
| 61 | - [ObservableProperty] private bool isScannedVisible; | ||
| 62 | - | ||
| 63 | - // Tab 按钮颜色 | 33 | + // Tab 颜色 |
| 64 | [ObservableProperty] private string pendingTabColor = "#E6F2FF"; | 34 | [ObservableProperty] private string pendingTabColor = "#E6F2FF"; |
| 65 | [ObservableProperty] private string scannedTabColor = "White"; | 35 | [ObservableProperty] private string scannedTabColor = "White"; |
| 66 | [ObservableProperty] private string pendingTextColor = "#007BFF"; | 36 | [ObservableProperty] private string pendingTextColor = "#007BFF"; |
| 67 | [ObservableProperty] private string scannedTextColor = "#333333"; | 37 | [ObservableProperty] private string scannedTextColor = "#333333"; |
| 68 | 38 | ||
| 69 | - // 列表数据 | ||
| 70 | - | ||
| 71 | - | ||
| 72 | // 命令 | 39 | // 命令 |
| 73 | public IRelayCommand ShowPendingCommand { get; } | 40 | public IRelayCommand ShowPendingCommand { get; } |
| 74 | public IRelayCommand ShowScannedCommand { get; } | 41 | public IRelayCommand ShowScannedCommand { get; } |
| 75 | public IAsyncRelayCommand ConfirmCommand { get; } | 42 | public IAsyncRelayCommand ConfirmCommand { get; } |
| 76 | 43 | ||
| 77 | - /// <summary> | ||
| 78 | - /// 切换 Tab | ||
| 79 | - /// </summary> | 44 | + public InboundMaterialViewModel(IInboundMaterialService warehouseSvc) |
| 45 | + { | ||
| 46 | + _warehouseSvc = warehouseSvc; | ||
| 47 | + ShowPendingCommand = new RelayCommand(() => SwitchTab(true)); | ||
| 48 | + ShowScannedCommand = new RelayCommand(() => SwitchTab(false)); | ||
| 49 | + //ConfirmCommand = new AsyncRelayCommand(ConfirmInboundAsync); | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + // ================ 初始化入口(页面 OnAppearing 调用) ================ | ||
| 53 | + public async Task InitializeFromSearchAsync( | ||
| 54 | + string instockId, string instockNo, string orderType, string orderTypeName, | ||
| 55 | + string purchaseNo, string supplierName, string createdTime) | ||
| 56 | + { | ||
| 57 | + // 1) 基础信息 | ||
| 58 | + InstockId = instockId; | ||
| 59 | + InstockNo = instockNo; | ||
| 60 | + OrderType = orderType; | ||
| 61 | + OrderTypeName = orderTypeName; | ||
| 62 | + PurchaseNo = purchaseNo; | ||
| 63 | + SupplierName = supplierName; | ||
| 64 | + CreatedTime = createdTime; | ||
| 65 | + | ||
| 66 | + // 2) 下拉库位(如无接口可留空或使用后端返回的 location 聚合) | ||
| 67 | + AvailableBins.Clear(); | ||
| 68 | + | ||
| 69 | + // 3) 拉取两张表 | ||
| 70 | + await LoadPendingAsync(); | ||
| 71 | + await LoadScannedAsync(); | ||
| 72 | + | ||
| 73 | + // 默认显示“待入库明细” | ||
| 74 | + SwitchTab(true); | ||
| 75 | + } | ||
| 76 | + | ||
| 80 | private void SwitchTab(bool showPending) | 77 | private void SwitchTab(bool showPending) |
| 81 | { | 78 | { |
| 82 | IsPendingVisible = showPending; | 79 | IsPendingVisible = showPending; |
| 83 | IsScannedVisible = !showPending; | 80 | IsScannedVisible = !showPending; |
| 84 | - | ||
| 85 | if (showPending) | 81 | if (showPending) |
| 86 | { | 82 | { |
| 87 | - PendingTabColor = "#E6F2FF"; | ||
| 88 | - ScannedTabColor = "White"; | ||
| 89 | - PendingTextColor = "#007BFF"; | ||
| 90 | - ScannedTextColor = "#333333"; | 83 | + PendingTabColor = "#E6F2FF"; ScannedTabColor = "White"; |
| 84 | + PendingTextColor = "#007BFF"; ScannedTextColor = "#333333"; | ||
| 91 | } | 85 | } |
| 92 | else | 86 | else |
| 93 | { | 87 | { |
| 94 | - PendingTabColor = "White"; | ||
| 95 | - ScannedTabColor = "#E6F2FF"; | ||
| 96 | - PendingTextColor = "#333333"; | ||
| 97 | - ScannedTextColor = "#007BFF"; | 88 | + PendingTabColor = "White"; ScannedTabColor = "#E6F2FF"; |
| 89 | + PendingTextColor = "#333333"; ScannedTextColor = "#007BFF"; | ||
| 98 | } | 90 | } |
| 99 | } | 91 | } |
| 100 | 92 | ||
| 101 | - /// <summary> | ||
| 102 | - /// 清空扫描记录 | ||
| 103 | - /// </summary> | ||
| 104 | - public void ClearScan() | 93 | + private async Task LoadPendingAsync() |
| 105 | { | 94 | { |
| 106 | - ScannedList.Clear(); | 95 | + PendingList.Clear(); |
| 96 | + if (string.IsNullOrWhiteSpace(InstockId)) return; | ||
| 97 | + | ||
| 98 | + var rows = await _warehouseSvc.GetInStockDetailAsync(InstockId!); | ||
| 99 | + foreach (var r in rows) | ||
| 100 | + { | ||
| 101 | + PendingList.Add(new PendingItem | ||
| 102 | + { | ||
| 103 | + Name = r.MaterialName ?? "", | ||
| 104 | + Spec = r.Spec ?? "", | ||
| 105 | + PendingQty = r.PendingQty, | ||
| 106 | + Bin = string.IsNullOrWhiteSpace(r.Location) ? "请选择" : r.Location!, | ||
| 107 | + ScannedQty = r.ScannedQty | ||
| 108 | + }); | ||
| 109 | + | ||
| 110 | + // 聚合可选库位 | ||
| 111 | + if (!string.IsNullOrWhiteSpace(r.Location) && !AvailableBins.Contains(r.Location)) | ||
| 112 | + AvailableBins.Add(r.Location); | ||
| 113 | + } | ||
| 107 | } | 114 | } |
| 108 | 115 | ||
| 109 | - /// <summary> | ||
| 110 | - /// 清空所有数据 | ||
| 111 | - /// </summary> | ||
| 112 | - public void ClearAll() | 116 | + private async Task LoadScannedAsync() |
| 113 | { | 117 | { |
| 114 | - OrderNo = string.Empty; | ||
| 115 | - LinkedDeliveryNo = string.Empty; | ||
| 116 | - LinkedPurchaseNo = string.Empty; | ||
| 117 | - Supplier = string.Empty; | ||
| 118 | - PendingList.Clear(); | ||
| 119 | ScannedList.Clear(); | 118 | ScannedList.Clear(); |
| 119 | + if (string.IsNullOrWhiteSpace(InstockId)) return; | ||
| 120 | + | ||
| 121 | + var rows = await _warehouseSvc.GetInStockScanDetailAsync(InstockId!); | ||
| 122 | + foreach (var r in rows) | ||
| 123 | + { | ||
| 124 | + ScannedList.Add(new OutScannedItem | ||
| 125 | + { | ||
| 126 | + IsSelected = false, | ||
| 127 | + Barcode = r.Barcode ?? "", | ||
| 128 | + Name = r.MaterialName ?? "", | ||
| 129 | + Spec = r.Spec ?? "", | ||
| 130 | + Bin = string.IsNullOrWhiteSpace(r.Location) ? "请选择" : r.Location!, | ||
| 131 | + Qty = r.Qty, | ||
| 132 | + ScanStatus = r.ScanStatus, | ||
| 133 | + WarehouseCode = r.WarehouseCode ?? "", | ||
| 134 | + }); | ||
| 135 | + | ||
| 136 | + if (!string.IsNullOrWhiteSpace(r.Location) && !AvailableBins.Contains(r.Location)) | ||
| 137 | + AvailableBins.Add(r.Location); | ||
| 138 | + } | ||
| 120 | } | 139 | } |
| 121 | 140 | ||
| 122 | [RelayCommand] | 141 | [RelayCommand] |
| 123 | - private async Task PassScan() // 绑定到 XAML 的 PassScanCommand | 142 | + private async Task PassScan() |
| 124 | { | 143 | { |
| 125 | - var selected = ScannedList.Where(x => x.IsSelected).ToList(); | 144 | + if (string.IsNullOrWhiteSpace(InstockId)) |
| 145 | + { | ||
| 146 | + await ShowTip("缺少 InstockId,无法确认。请从查询页进入。"); | ||
| 147 | + return; | ||
| 148 | + } | ||
| 126 | 149 | ||
| 127 | - if (selected.Count == 0) | 150 | + // 依旧要求只能选中一行,作为“操作目标”的 UI 约束(接口本身仅需 instockId) |
| 151 | + var selected = ScannedList.Where(x => x.IsSelected).ToList(); | ||
| 152 | + if (selected.Count != 1) | ||
| 128 | { | 153 | { |
| 129 | - await ShowTip("请先勾选一行记录。"); | 154 | + await ShowTip(selected.Count == 0 ? "请先勾选一条已扫描记录。" : "一次只能操作一条记录。"); |
| 130 | return; | 155 | return; |
| 131 | } | 156 | } |
| 132 | - if (selected.Count > 1) | 157 | + var selectedBarcode = selected[0].Barcode; |
| 158 | + | ||
| 159 | + // 调用确认接口 | ||
| 160 | + var resp = await _warehouseSvc.ScanConfirmAsync(InstockId!); | ||
| 161 | + if (!resp.Succeeded) | ||
| 133 | { | 162 | { |
| 134 | - await ShowTip("一次只能操作一行,请只勾选一行。"); | 163 | + await ShowTip(string.IsNullOrWhiteSpace(resp.Message) ? "扫描通过失败,请重试。" : resp.Message!); |
| 135 | return; | 164 | return; |
| 136 | } | 165 | } |
| 137 | 166 | ||
| 138 | - var row = selected[0]; | ||
| 139 | - row.Qty += 1; | ||
| 140 | - row.Qty -= 1; | ||
| 141 | - // 确保本行已变绿(如果你的行颜色基于 IsSelected) | ||
| 142 | - if (!row.IsSelected) row.IsSelected = true; | ||
| 143 | - SelectedScanItem = row; | 167 | + // 成功:刷新两张表 |
| 168 | + await LoadPendingAsync(); | ||
| 169 | + await LoadScannedAsync(); | ||
| 170 | + | ||
| 171 | + // 友好:恢复选中刚才那条(如果还在列表里) | ||
| 172 | + var hit = ScannedList.FirstOrDefault(x => | ||
| 173 | + string.Equals(x.Barcode, selectedBarcode, StringComparison.OrdinalIgnoreCase)); | ||
| 174 | + if (hit != null) { hit.IsSelected = true; SelectedScanItem = hit; } | ||
| 144 | 175 | ||
| 176 | + await ShowTip("已确认通过。"); | ||
| 145 | } | 177 | } |
| 146 | 178 | ||
| 179 | + | ||
| 147 | [RelayCommand] | 180 | [RelayCommand] |
| 148 | - private async Task CancelScan() // 绑定到 XAML 的 CancelScanCommand | 181 | + private async Task CancelScan() |
| 149 | { | 182 | { |
| 150 | - var selected = ScannedList.Where(x => x.IsSelected).ToList(); | ||
| 151 | - | ||
| 152 | - if (selected.Count == 0) | 183 | + if (string.IsNullOrWhiteSpace(InstockId)) |
| 153 | { | 184 | { |
| 154 | - await ShowTip("请先勾选一行记录。"); | 185 | + await ShowTip("缺少 InstockId,无法取消。请从查询页进入。"); |
| 155 | return; | 186 | return; |
| 156 | } | 187 | } |
| 157 | - if (selected.Count > 1) | 188 | + |
| 189 | + // 依旧限制一次只操作一条(接口本身只要 instockId,这里是 UI 规范) | ||
| 190 | + var selected = ScannedList.Where(x => x.IsSelected).ToList(); | ||
| 191 | + if (selected.Count != 1) | ||
| 158 | { | 192 | { |
| 159 | - await ShowTip("一次只能操作一行,请只勾选一行。"); | 193 | + await ShowTip(selected.Count == 0 ? "请先勾选一条已扫描记录。" : "一次只能操作一条记录。"); |
| 160 | return; | 194 | return; |
| 161 | } | 195 | } |
| 196 | + var selectedBarcode = selected[0].Barcode; | ||
| 162 | 197 | ||
| 163 | - var row = selected[0]; | ||
| 164 | - if (row.Qty <= 0) | 198 | + var resp = await _warehouseSvc.CancelScanAsync(InstockId!); |
| 199 | + if (!resp.Succeeded) | ||
| 165 | { | 200 | { |
| 166 | - await ShowTip("该行数量已为 0,无法再减少。"); | 201 | + await ShowTip(string.IsNullOrWhiteSpace(resp.Message) ? "取消扫描失败,请重试。" : resp.Message!); |
| 167 | return; | 202 | return; |
| 168 | } | 203 | } |
| 169 | 204 | ||
| 170 | - row.Qty -= 1; | 205 | + // 成功后以服务端为准刷新两张表 |
| 206 | + await LoadPendingAsync(); | ||
| 207 | + await LoadScannedAsync(); | ||
| 171 | 208 | ||
| 172 | - // 如果希望数量减到 0 时取消高亮,可打开下一行 | ||
| 173 | - // if (row.Qty == 0) row.IsSelected = false; | ||
| 174 | - } | 209 | + // 友好:若那条还在列表里,恢复选中 |
| 210 | + var hit = ScannedList.FirstOrDefault(x => | ||
| 211 | + string.Equals(x.Barcode, selectedBarcode, StringComparison.OrdinalIgnoreCase)); | ||
| 212 | + if (hit != null) { hit.IsSelected = true; SelectedScanItem = hit; } | ||
| 175 | 213 | ||
| 176 | - public async Task HandleScannedAsync(string data, string symbology) | ||
| 177 | - { | ||
| 178 | - // TODO: 你的规则(判断是订单号/物料码/包装码等) | ||
| 179 | - // 然后:查询接口 / 加入列表 / 自动提交 | ||
| 180 | - await Task.CompletedTask; | 214 | + await ShowTip("已取消扫描。"); |
| 181 | } | 215 | } |
| 182 | - private Task ShowTip(string message) => | ||
| 183 | - Shell.Current?.DisplayAlert("提示", message, "确定") ?? Task.CompletedTask; | ||
| 184 | 216 | ||
| 185 | - /// <summary> | ||
| 186 | - /// 确认入库逻辑 | ||
| 187 | - /// </summary> | ||
| 188 | - public async Task<bool> ConfirmInboundAsync() | ||
| 189 | - { | ||
| 190 | - if (string.IsNullOrWhiteSpace(OrderNo)) | ||
| 191 | - return false; | ||
| 192 | 217 | ||
| 193 | - var result = await _warehouseSvc.ConfirmInboundAsync(OrderNo, | ||
| 194 | - ScannedList.Select(x => new ScanItem(0, x.Barcode, x.Bin, x.Qty))); | 218 | + public async Task HandleScannedAsync(string data, string symbology) |
| 219 | + { | ||
| 220 | + var barcode = (data ?? string.Empty).Trim(); | ||
| 221 | + if (string.IsNullOrWhiteSpace(barcode)) | ||
| 222 | + { | ||
| 223 | + await ShowTip("无效条码。"); | ||
| 224 | + return; | ||
| 225 | + } | ||
| 195 | 226 | ||
| 196 | - return result.Succeeded; | ||
| 197 | - } | 227 | + if (string.IsNullOrWhiteSpace(InstockId)) |
| 228 | + { | ||
| 229 | + await ShowTip("缺少 InstockId,无法入库。请从查询页进入。"); | ||
| 230 | + return; | ||
| 231 | + } | ||
| 198 | 232 | ||
| 199 | - public async Task LoadOrderAsync(string orderNo) | ||
| 200 | - { | ||
| 201 | - // 0) 清空旧数据 | ||
| 202 | - ClearAll(); | 233 | + // 调用扫码入库接口 |
| 234 | + var resp = await _warehouseSvc.InStockByBarcodeAsync(InstockId!, barcode); | ||
| 203 | 235 | ||
| 204 | - // 1) 基础信息 | ||
| 205 | - var order = await _warehouseSvc.GetInboundOrderAsync(orderNo); | ||
| 206 | - OrderNo = order.OrderNo; | ||
| 207 | - Supplier = order.Supplier ?? string.Empty; | ||
| 208 | - LinkedDeliveryNo = order.LinkedNo ?? string.Empty; // 到货/交货单 | ||
| 209 | - LinkedPurchaseNo = order is { } ? LinkedPurchaseNo : ""; // 你没有就留空 | ||
| 210 | - | ||
| 211 | - // 2) 预入库库位:从接口取(先 mock),用于行内 Picker | ||
| 212 | - // (你已经在 IWarehouseDataService 里加了 ListInboundBinsAsync) | ||
| 213 | - try | 236 | + if (!resp.Succeeded) |
| 214 | { | 237 | { |
| 215 | - var bins = await _warehouseSvc.ListInboundBinsAsync(orderNo); | ||
| 216 | - AvailableBins.Clear(); | ||
| 217 | - foreach (var b in bins) AvailableBins.Add(b); | 238 | + await ShowTip(string.IsNullOrWhiteSpace(resp.Message) ? "入库失败,请重试或检查条码。" : resp.Message!); |
| 239 | + return; | ||
| 218 | } | 240 | } |
| 219 | - catch | 241 | + |
| 242 | + // 成功 → 刷新“待入库明细”和“已扫描明细” | ||
| 243 | + await LoadPendingAsync(); | ||
| 244 | + await LoadScannedAsync(); | ||
| 245 | + | ||
| 246 | + // UI 友好:尝试高亮刚扫的那一条 | ||
| 247 | + var hit = ScannedList.FirstOrDefault(x => string.Equals(x.Barcode, barcode, StringComparison.OrdinalIgnoreCase)); | ||
| 248 | + if (hit != null) | ||
| 220 | { | 249 | { |
| 221 | - // 兜底:本地 mock,避免空集合导致 Picker 无选项 | ||
| 222 | - AvailableBins.Clear(); | ||
| 223 | - foreach (var b in new[] { "CK1_A201", "CK1_A202", "CK1_A203", "CK1_A204" }) | ||
| 224 | - AvailableBins.Add(b); | 250 | + hit.IsSelected = true; |
| 251 | + SelectedScanItem = hit; | ||
| 252 | + // 切到“已扫描”页签更直观(可选) | ||
| 253 | + // SwitchTab(false); | ||
| 225 | } | 254 | } |
| 226 | - var defaultBin = AvailableBins.FirstOrDefault() ?? "CK1_A201"; | 255 | + } |
| 227 | 256 | ||
| 228 | - // 3) 待入库明细:如果暂时没有后端接口,这里先 mock 多行,便于 UI 联调 | ||
| 229 | - PendingList.Clear(); | ||
| 230 | - // 你原来只加了 1 行,这里多给两行方便测试 | ||
| 231 | - PendingList.Add(new PendingItem { Name = "物料X", Spec = "型号X", PendingQty = order.ExpectedQty, Bin = defaultBin, ScannedQty = 0 }); | ||
| 232 | - PendingList.Add(new PendingItem { Name = "物料Y", Spec = "型号Y", PendingQty = Math.Max(10, order.ExpectedQty / 2), Bin = defaultBin, ScannedQty = 0 }); | ||
| 233 | - PendingList.Add(new PendingItem { Name = "物料Z", Spec = "型号Z", PendingQty = 8, Bin = defaultBin, ScannedQty = 0 }); | ||
| 234 | 257 | ||
| 235 | - // 4) 扫描明细初始化(为空即可;也可以放 1~2 条便于测试) | ||
| 236 | - ScannedList.Clear(); | ||
| 237 | - // 可选:放两条测试数据,验证“库位下拉/数量可编辑” | ||
| 238 | - ScannedList.Add(new OutScannedItem { IsSelected = false, Barcode = "FSC2025060300001", Name = "物料X", Spec = "型号X", Bin = defaultBin, Qty = 1 }); | ||
| 239 | - ScannedList.Add(new OutScannedItem { IsSelected = false, Barcode = "FSC2025060300002", Name = "物料Y", Spec = "型号Y", Bin = defaultBin, Qty = 2 }); | 258 | + private Task ShowTip(string message) => |
| 259 | + Shell.Current?.DisplayAlert("提示", message, "确定") ?? Task.CompletedTask; | ||
| 240 | 260 | ||
| 241 | - // 5) 默认展示“待入库明细”页签 | ||
| 242 | - SwitchTab(true); | 261 | + |
| 262 | + public void ClearScan() => ScannedList.Clear(); | ||
| 263 | + public void ClearAll() | ||
| 264 | + { | ||
| 265 | + PendingList.Clear(); | ||
| 266 | + ScannedList.Clear(); | ||
| 243 | } | 267 | } |
| 244 | 268 | ||
| 245 | public void SetItemBin(object row, string bin) | 269 | public void SetItemBin(object row, string bin) |
| @@ -253,29 +277,49 @@ namespace IndustrialControl.ViewModels | @@ -253,29 +277,49 @@ namespace IndustrialControl.ViewModels | ||
| 253 | } | 277 | } |
| 254 | } | 278 | } |
| 255 | 279 | ||
| 280 | + public async Task<bool> ConfirmInboundAsync() | ||
| 281 | + { | ||
| 282 | + if (string.IsNullOrWhiteSpace(InstockId)) | ||
| 283 | + { | ||
| 284 | + await ShowTip("缺少 InstockId,无法确认入库。请从查询页进入。"); | ||
| 285 | + return false; | ||
| 286 | + } | ||
| 256 | 287 | ||
| 288 | + var r = await _warehouseSvc.ConfirmInstockAsync(InstockId!); | ||
| 289 | + if (!r.Succeeded) | ||
| 290 | + { | ||
| 291 | + await ShowTip(string.IsNullOrWhiteSpace(r.Message) ? "确认入库失败,请重试。" : r.Message!); | ||
| 292 | + return false; | ||
| 293 | + } | ||
| 294 | + | ||
| 295 | + // 成功:可选刷新一次,以服务端为准;随后按钮事件里会 ClearAll() | ||
| 296 | + await LoadPendingAsync(); | ||
| 297 | + await LoadScannedAsync(); | ||
| 298 | + return true; | ||
| 299 | + } | ||
| 257 | 300 | ||
| 258 | } | 301 | } |
| 259 | 302 | ||
| 260 | - // 待入库明细模型 | 303 | + // === 列表行模型 === |
| 261 | public class PendingItem | 304 | public class PendingItem |
| 262 | { | 305 | { |
| 263 | - public string Name { get; set; } | ||
| 264 | - public string Spec { get; set; } | 306 | + public string Name { get; set; } = ""; |
| 307 | + public string Spec { get; set; } = ""; | ||
| 265 | public int PendingQty { get; set; } | 308 | public int PendingQty { get; set; } |
| 266 | - public string Bin { get; set; } | 309 | + public string Bin { get; set; } = "请选择"; |
| 267 | public int ScannedQty { get; set; } | 310 | public int ScannedQty { get; set; } |
| 268 | } | 311 | } |
| 269 | 312 | ||
| 270 | - // 扫描明细模型 | ||
| 271 | public partial class OutScannedItem : ObservableObject | 313 | public partial class OutScannedItem : ObservableObject |
| 272 | { | 314 | { |
| 273 | [ObservableProperty] private bool isSelected; | 315 | [ObservableProperty] private bool isSelected; |
| 274 | - [ObservableProperty] private string barcode; | ||
| 275 | - [ObservableProperty] private string name; | ||
| 276 | - [ObservableProperty] private string spec; | ||
| 277 | - [ObservableProperty] private string bin; | 316 | + [ObservableProperty] private string barcode = ""; |
| 317 | + [ObservableProperty] private string name = ""; | ||
| 318 | + [ObservableProperty] private string spec = ""; | ||
| 319 | + [ObservableProperty] private string bin = "请选择"; | ||
| 278 | [ObservableProperty] private int qty; | 320 | [ObservableProperty] private int qty; |
| 321 | + [ObservableProperty] private string detailId; | ||
| 322 | + [ObservableProperty] private bool scanStatus; | ||
| 323 | + [ObservableProperty] private string warehouseCode; | ||
| 279 | } | 324 | } |
| 280 | - | ||
| 281 | } | 325 | } |
| 1 | using CommunityToolkit.Mvvm.ComponentModel; | 1 | using CommunityToolkit.Mvvm.ComponentModel; |
| 2 | using CommunityToolkit.Mvvm.Input; | 2 | using CommunityToolkit.Mvvm.Input; |
| 3 | +using IndustrialControl.Pages; | ||
| 3 | using IndustrialControl.Services; | 4 | using IndustrialControl.Services; |
| 4 | -using System.Collections.ObjectModel; | ||
| 5 | using System; | 5 | using System; |
| 6 | -using IndustrialControl.Pages; | 6 | +using System.Collections.ObjectModel; |
| 7 | 7 | ||
| 8 | namespace IndustrialControl.ViewModels; | 8 | namespace IndustrialControl.ViewModels; |
| 9 | 9 | ||
| 10 | public partial class InboundProductionSearchViewModel : ObservableObject | 10 | public partial class InboundProductionSearchViewModel : ObservableObject |
| 11 | { | 11 | { |
| 12 | - private readonly IWarehouseDataService _dataSvc; | 12 | + private readonly IInboundMaterialService _dataSvc; |
| 13 | [ObservableProperty] private string? searchOrderNo; | 13 | [ObservableProperty] private string? searchOrderNo; |
| 14 | - [ObservableProperty] private DateTime _createdDate = DateTime.Today; | 14 | + [ObservableProperty] private DateTime startDate = DateTime.Today; |
| 15 | + [ObservableProperty] private DateTime endDate = DateTime.Today; | ||
| 15 | [ObservableProperty] private InboundOrderSummary? selectedOrder; | 16 | [ObservableProperty] private InboundOrderSummary? selectedOrder; |
| 17 | + private CancellationTokenSource? _searchCts; | ||
| 16 | 18 | ||
| 17 | - public InboundProductionSearchViewModel(IWarehouseDataService dataSvc) | 19 | + public InboundProductionSearchViewModel(IInboundMaterialService dataSvc) |
| 18 | { | 20 | { |
| 19 | _dataSvc = dataSvc; | 21 | _dataSvc = dataSvc; |
| 20 | Orders = new ObservableCollection<InboundOrderSummary>(); | 22 | Orders = new ObservableCollection<InboundOrderSummary>(); |
| @@ -27,10 +29,20 @@ public partial class InboundProductionSearchViewModel : ObservableObject | @@ -27,10 +29,20 @@ public partial class InboundProductionSearchViewModel : ObservableObject | ||
| 27 | [RelayCommand] | 29 | [RelayCommand] |
| 28 | private async Task SearchAsync() | 30 | private async Task SearchAsync() |
| 29 | { | 31 | { |
| 32 | + _searchCts?.Cancel(); | ||
| 33 | + _searchCts = new CancellationTokenSource(); | ||
| 34 | + var ct = _searchCts.Token; | ||
| 30 | try | 35 | try |
| 31 | { | 36 | { |
| 32 | - var list = await _dataSvc.ListInboundOrdersAsync(SearchOrderNo, CreatedDate); | ||
| 33 | - | 37 | + |
| 38 | + var list = await _dataSvc.ListInboundOrdersAsync( | ||
| 39 | + searchOrderNo, // 单号/条码 | ||
| 40 | + startDate, // 开始日期 | ||
| 41 | + endDate, // 结束日期(Service 内会扩到 23:59:59) | ||
| 42 | + "in_production", // 不传单值 orderType,用 null 更清晰 | ||
| 43 | + null, // 多类型数组 | ||
| 44 | + ct // ← 新增:取消令牌 | ||
| 45 | + ); | ||
| 34 | // ★ 在主线程更新 ObservableCollection,避免看起来“没刷新” | 46 | // ★ 在主线程更新 ObservableCollection,避免看起来“没刷新” |
| 35 | await MainThread.InvokeOnMainThreadAsync(() => | 47 | await MainThread.InvokeOnMainThreadAsync(() => |
| 36 | { | 48 | { |
| @@ -52,27 +64,29 @@ public partial class InboundProductionSearchViewModel : ObservableObject | @@ -52,27 +64,29 @@ public partial class InboundProductionSearchViewModel : ObservableObject | ||
| 52 | } | 64 | } |
| 53 | } | 65 | } |
| 54 | 66 | ||
| 55 | - // 打开明细(携带 orderNo 导航) | ||
| 56 | - [RelayCommand] | ||
| 57 | - private async Task OpenItemAsync(InboundOrderSummary item) | 67 | + [RelayCommand(CanExecute = nameof(CanGoInbound))] |
| 68 | + private async Task GoInboundAsync(InboundOrderSummary? item) | ||
| 58 | { | 69 | { |
| 59 | if (item is null) return; | 70 | if (item is null) return; |
| 60 | - await Shell.Current.GoToAsync( | ||
| 61 | - $"{nameof(InboundProductionPage)}?orderNo={Uri.EscapeDataString(item.OrderNo)}"); | ||
| 62 | - } | ||
| 63 | 71 | ||
| 64 | - [RelayCommand(CanExecute = nameof(CanGoInbound))] | ||
| 65 | - private async Task GoInboundAsync() | ||
| 66 | - { | ||
| 67 | - if (SelectedOrder == null) return; | ||
| 68 | - // 导航到原“入库明细/扫描”页面,并传入 orderNo | ||
| 69 | - await Shell.Current.GoToAsync($"//InboundProduction?orderNo={Uri.EscapeDataString(SelectedOrder.OrderNo)}"); | ||
| 70 | - } | ||
| 71 | - public async Task LoadOrderAsync(string orderNo) | ||
| 72 | - { | ||
| 73 | - var order = await _dataSvc.GetInboundOrderAsync(orderNo); | ||
| 74 | - // TODO: 将 order 映射到页面“基础信息”“待入库明细”等绑定源,保持你现有绑定字段/集合不变 | 72 | + static string E(string? v) => Uri.EscapeDataString(v ?? ""); |
| 73 | + | ||
| 74 | + var o = item; | ||
| 75 | + var url = | ||
| 76 | + $"//InboundProduction" + | ||
| 77 | + $"?instockId={E(o.instockId)}" + | ||
| 78 | + $"&instockNo={E(o.instockNo)}" + | ||
| 79 | + $"&orderType={E(o.orderType)}" + | ||
| 80 | + $"&orderTypeName={E(o.orderTypeName)}" + | ||
| 81 | + $"&purchaseNo={E(o.purchaseNo)}" + | ||
| 82 | + $"&supplierName={E(o.supplierName)}" + | ||
| 83 | + $"&createdTime={E(o.createdTime)}"; | ||
| 84 | + | ||
| 85 | + await Shell.Current.GoToAsync(url); | ||
| 75 | } | 86 | } |
| 76 | - private bool CanGoInbound() => SelectedOrder != null; | 87 | + |
| 88 | + // ✅ 与命令同签名的 CanExecute | ||
| 89 | + private bool CanGoInbound(InboundOrderSummary? item) => item != null; | ||
| 90 | + | ||
| 77 | } | 91 | } |
| 78 | 92 |
-
请 注册 或 登录 后发表评论