作者 李壮

mock change to api

正在显示 41 个修改的文件 包含 2892 行增加881 行删除

要显示太多修改。

为保证性能只显示 41 of 41+ 个文件。

@@ -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); // 显示:主页|日志|管理员
  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 +}
  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 +}
  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" 2 +<ContentPage
4 x:Class="IndustrialControl.Pages.MoldOutboundExecutePage" 3 x:Class="IndustrialControl.Pages.MoldOutboundExecutePage"
5 - Title="出库执行">  
6 - 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"/> 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" />
13 </Style> 26 </Style>
14 - <Style TargetType="Grid" x:Key="TableRow">  
15 - <Setter Property="Padding" Value="8,6"/>  
16 - <Setter Property="ColumnSpacing" Value="8"/> 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" />
17 </Style> 33 </Style>
  34 + </ResourceDictionary>
18 </ContentPage.Resources> 35 </ContentPage.Resources>
19 36
20 - <Grid RowDefinitions="Auto,*,Auto"> 37 + <ScrollView>
  38 + <Grid Padding="14" RowDefinitions="Auto,Auto,Auto,*,Auto">
21 39
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> 40 + <!-- 工单基础信息(标题 + 顶部流程进度) -->
  41 + <Frame Grid.Row="0" Style="{StaticResource Card}">
  42 + <VerticalStackLayout Spacing="10">
28 43
29 - <!-- 主体 -->  
30 - <ScrollView Grid.Row="1">  
31 - <VerticalStackLayout Padding="12" Spacing="12"> 44 + <Label Text="工单基础信息" Style="{StaticResource SectionTitle}" />
32 45
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" /> 46 + <!-- 顶部流程进度:按 VM 的 IsActive/IsDone 高亮;Time 显示日期 -->
  47 + <CollectionView ItemsSource="{Binding WorkflowSteps}" SelectionMode="None">
  48 + <CollectionView.ItemsLayout>
  49 + <LinearItemsLayout Orientation="Horizontal" ItemSpacing="26"/>
  50 + </CollectionView.ItemsLayout>
40 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>
41 </Grid> 95 </Grid>
42 96
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> 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}"/>
58 </Grid> 114 </Grid>
  115 + </DataTemplate>
  116 + </CollectionView.ItemTemplate>
  117 + </CollectionView>
59 118
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> 119 </VerticalStackLayout>
74 </Frame> 120 </Frame>
75 121
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="出库库位" /> 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"/>
  163 +
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}" />
89 172
90 - <!-- 明细列表 -->  
91 - <CollectionView ItemsSource="{Binding ScanDetails}"> 173 + <CollectionView ItemsSource="{Binding ProcessTasks}" SelectionMode="None">
92 <CollectionView.ItemTemplate> 174 <CollectionView.ItemTemplate>
93 <DataTemplate> 175 <DataTemplate>
94 - <Grid ColumnDefinitions="40,Auto,*,*,Auto,*"  
95 - Style="{StaticResource TableRow}">  
96 - <!-- 背景根据 Selected 高亮为绿色 --> 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 着色:完成=绿,进行中=蓝,其它=灰 -->
97 <Grid.Triggers> 204 <Grid.Triggers>
98 - <DataTrigger TargetType="Grid" Binding="{Binding Selected}" Value="True">  
99 - <Setter Property="BackgroundColor" Value="#6EF593"/> 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}" />
100 </DataTrigger> 216 </DataTrigger>
101 </Grid.Triggers> 217 </Grid.Triggers>
102 218
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> 219 </Grid>
110 </DataTemplate> 220 </DataTemplate>
111 </CollectionView.ItemTemplate> 221 </CollectionView.ItemTemplate>
112 </CollectionView> 222 </CollectionView>
113 223
114 </VerticalStackLayout> 224 </VerticalStackLayout>
115 - </ScrollView> 225 + </Frame>
116 226
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}" /> 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}" />
138 </Grid> 231 </Grid>
139 232
140 </Grid> 233 </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; // 已就绪
32 } 43 }
33 } 44 }
34 45
35 - protected override void OnAppearing()  
36 - {  
37 - base.OnAppearing();  
38 - // ✅ 注册扫描回调并开始监听  
39 - _scanSvc.Scanned += OnScanned;  
40 - _scanSvc.StartListening();  
41 -  
42 - // ✅ 让“模具编码”输入框也能接收键盘/扫描枪字符  
43 - _scanSvc.Attach(MoldEntry);  
44 - MoldEntry.Focus();  
45 - } 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;
46 49
47 - protected override void OnDisappearing() 50 + if (query.TryGetValue("baseInfo", out var bi) && bi is string baseInfoJson && !string.IsNullOrWhiteSpace(baseInfoJson))
48 { 51 {
49 - // ✅ 页面离开时及时注销,避免多个页面抢占扫码  
50 - _scanSvc.Scanned -= OnScanned;  
51 - _scanSvc.StopListening();  
52 - base.OnDisappearing();  
53 - }  
54 -  
55 - // ✅ 硬件/广播 扫到码  
56 - private void OnScanned(string data, string type) 52 + try
57 { 53 {
58 - MainThread.BeginInvokeOnMainThread(async () => 54 + var items = JsonSerializer.Deserialize<List<BaseInfoItem>>(baseInfoJson);
  55 + if (items != null)
59 { 56 {
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 - }); 57 + // 让 VM 把 BaseInfos 落到固定属性上(见步骤 B)
  58 + _vm.SetFixedFieldsFromBaseInfos(items);
70 } 59 }
71 -  
72 - // ✅ 手动输入并按回车,也走同一套逻辑  
73 - private async void OnMoldCompleted(object? sender, EventArgs e) 60 + }
  61 + catch (Exception ex)
74 { 62 {
75 - var code = MoldEntry.Text?.Trim();  
76 - if (string.IsNullOrEmpty(code)) return; 63 + System.Diagnostics.Debug.WriteLine("[Execute] baseInfo JSON parse error: " + ex);
  64 + }
  65 + }
  66 + }
77 67
78 - // 可选:与 OnScanned 同步的处理  
79 - // await _vm.HandleMoldScannedAsync(code, "manual");  
80 68
81 - // 光标回到输入框,便于连续扫描  
82 - MoldEntry.CursorPosition = MoldEntry.Text?.Length ?? 0;  
83 - MoldEntry.Focus(); 69 + protected override async void OnAppearing()
  70 + {
  71 + base.OnAppearing();
  72 + if (!string.IsNullOrWhiteSpace(_vm.OrderNo))
  73 + await _vm.LoadAsync(_vm.OrderNo!, _vm.OrderId);
84 } 74 }
  75 +
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 + <TapGestureRecognizer
  69 + Command="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.GoExecuteCommand}"
68 CommandParameter="{Binding .}" /> 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 - { 51 +
  52 +private async void OnOrderTapped(object sender, TappedEventArgs e)
  53 +{
52 try 54 try
53 { 55 {
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 + var json = JsonSerializer.Serialize(item);
  59 +
58 await Shell.Current.GoToAsync( 60 await Shell.Current.GoToAsync(
59 - $"{nameof(MoldOutboundExecutePage)}?orderNo={Uri.EscapeDataString(item.OrderNo)}"); 61 + nameof(MoldOutboundExecutePage),
  62 + new Dictionary<string, object?>
  63 + {
  64 + // 执行页用 IQueryAttributable 接收:key 必须叫 "orderDto"
  65 + ["orderDto"] = json
  66 + });
60 } 67 }
61 catch (Exception ex) 68 catch (Exception ex)
62 { 69 {
63 await DisplayAlert("导航失败", ex.Message, "确定"); 70 await DisplayAlert("导航失败", ex.Message, "确定");
64 } 71 }
65 - } 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}" />
@@ -173,13 +184,14 @@ @@ -173,13 +184,14 @@
173 Text="{Binding Bin}" 184 Text="{Binding Bin}"
174 HorizontalTextAlignment="Center" 185 HorizontalTextAlignment="Center"
175 VerticalTextAlignment="Center" 186 VerticalTextAlignment="Center"
176 - TextColor="#007BFF" FontAttributes="Bold"> 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" 196 Keyboard="Numeric"
185 HorizontalTextAlignment="Center" 197 HorizontalTextAlignment="Center"
@@ -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 }
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 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
5 - x:Class="IndustrialControl.Pages.InboundMaterialSearchPage"  
6 - xmlns:conv="clr-namespace:IndustrialControl.Converters"  
7 - Title="仓储管理系统">  
8 -  
9 - <ContentPage.Resources>  
10 - <ResourceDictionary>  
11 - <!-- 空/非空转布尔:非空 => true(按钮可用) -->  
12 - <conv:NullToBoolConverter x:Key="NullToBoolConverter" />  
13 - </ResourceDictionary>  
14 - </ContentPage.Resources> 6 + x:Name="Page"
  7 + Title="入库单查询">
15 8
16 - <Grid RowDefinitions="Auto,*,Auto" Padding="16" BackgroundColor="#F6F7FB"> 9 + <ScrollView>
  10 + <VerticalStackLayout Spacing="12" Padding="12">
17 11
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行) --> 31 + <!-- 列表区域 -->
38 <CollectionView Grid.Row="1" 32 <CollectionView Grid.Row="1"
39 ItemsSource="{Binding Orders}" 33 ItemsSource="{Binding Orders}"
40 SelectionMode="Single" 34 SelectionMode="Single"
41 - SelectionChanged="OnOrderSelected"> 35 + SelectedItem="{Binding SelectedOrder, Mode=TwoWay}">
42 <CollectionView.ItemTemplate> 36 <CollectionView.ItemTemplate>
43 <DataTemplate> 37 <DataTemplate>
44 <Frame Margin="0,8,0,0" Padding="12" HasShadow="True" CornerRadius="10"> 38 <Frame Margin="0,8,0,0" Padding="12" HasShadow="True" CornerRadius="10">
45 - <!-- ⭐ 点击整卡片触发命令 --> 39 + <!-- 点击整卡片:直接调用 VM 的 GoInboundCommand,并把当前项作为参数 -->
46 <Frame.GestureRecognizers> 40 <Frame.GestureRecognizers>
47 <TapGestureRecognizer 41 <TapGestureRecognizer
48 - Command="{Binding BindingContext.OpenItemCommand, Source={x:Reference Page}}" 42 + Command="{Binding BindingContext.GoInboundCommand, Source={x:Reference Page}}"
49 CommandParameter="{Binding .}" /> 43 CommandParameter="{Binding .}" />
50 </Frame.GestureRecognizers> 44 </Frame.GestureRecognizers>
51 - <Grid RowDefinitions="Auto,Auto,Auto,Auto" 45 +
  46 + <Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto"
52 ColumnDefinitions="Auto,*" ColumnSpacing="8"> 47 ColumnDefinitions="Auto,*" ColumnSpacing="8">
53 <Label Grid.Row="0" Grid.Column="0" Text="入库单号:" FontAttributes="Bold"/> 48 <Label Grid.Row="0" Grid.Column="0" Text="入库单号:" FontAttributes="Bold"/>
54 - <Label Grid.Row="0" Grid.Column="1" Text="{Binding OrderNo}"/> 49 + <Label Grid.Row="0" Grid.Column="1" Text="{Binding instockNo}"/>
  50 +
  51 + <Label Grid.Row="1" Grid.Column="0" Text="入库单类型:" FontAttributes="Bold"/>
  52 + <Label Grid.Row="1" Grid.Column="1" Text="{Binding orderTypeName}" />
55 53
56 - <Label Grid.Row="1" Grid.Column="0" Text="入库类型:" FontAttributes="Bold"/>  
57 - <Label Grid.Row="1" Grid.Column="1" Text="{Binding InboundType}" /> 54 + <Label Grid.Row="2" Grid.Column="0" Text="供应商名称:" FontAttributes="Bold"/>
  55 + <Label Grid.Row="2" Grid.Column="1" Text="{Binding supplierName}" />
58 56
59 - <Label Grid.Row="2" Grid.Column="0" Text="供应商:" FontAttributes="Bold"/>  
60 - <Label Grid.Row="2" Grid.Column="1" Text="{Binding Supplier}" /> 57 + <Label Grid.Row="3" Grid.Column="0" Text="关联到货单号:" FontAttributes="Bold"/>
  58 + <Label Grid.Row="3" Grid.Column="1" Text="{Binding arrivalNo}" />
61 59
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}'}"/> 60 + <Label Grid.Row="4" Grid.Column="0" Text="创建日期:" FontAttributes="Bold"/>
  61 + <Label Grid.Row="4" Grid.Column="1" Text="{Binding createdTime}" />
64 </Grid> 62 </Grid>
65 </Frame> 63 </Frame>
66 </DataTemplate> 64 </DataTemplate>
67 </CollectionView.ItemTemplate> 65 </CollectionView.ItemTemplate>
68 </CollectionView> 66 </CollectionView>
69 67
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> 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 101
89 - /// <summary>  
90 - /// 取消扫描  
91 - /// </summary>  
92 - void OnCancelScanClicked(object sender, EventArgs e)  
93 - {  
94 - _vm.CancelSelectedScan();  
95 - }  
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}}" 50 + Command="{Binding BindingContext.GoInboundCommand, Source={x:Reference Page}}"
49 CommandParameter="{Binding .}" /> 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}"/>
55 56
56 - <Label Grid.Row="1" Grid.Column="0" Text="入库类型:" FontAttributes="Bold"/>  
57 - <Label Grid.Row="1" Grid.Column="1" Text="{Binding InboundType}" /> 57 + <Label Grid.Row="1" Grid.Column="0" Text="入库单类型:" FontAttributes="Bold"/>
  58 + <Label Grid.Row="1" Grid.Column="1" Text="{Binding orderTypeName}" />
58 59
59 - <Label Grid.Row="2" Grid.Column="0" Text="供应商:" FontAttributes="Bold"/>  
60 - <Label Grid.Row="2" Grid.Column="1" Text="{Binding Supplier}" /> 60 + <Label Grid.Row="2" Grid.Column="0" Text="产品名称:" FontAttributes="Bold"/>
  61 + <Label Grid.Row="2" Grid.Column="1" Text="{Binding supplierName}" />
61 62
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}'}"/> 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}" />
  68 +
  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}}" 55 + Command="{Binding BindingContext.GoOutboundCommand, Source={x:Reference Page}}"
49 CommandParameter="{Binding .}" /> 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,11 +136,67 @@ @@ -136,11 +136,67 @@
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>
@@ -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))  
32 - {  
33 - await _vm.LoadOrderAsync(OrderNo); 43 + // ✅ 用搜索页带过来的基础信息初始化页面,并拉取两张表
  44 + if (!string.IsNullOrWhiteSpace(OutstockId))
  45 + {
  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}}" 55 + Command="{Binding BindingContext.GoOutboundCommand, Source={x:Reference Page}}"
49 CommandParameter="{Binding .}" /> 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"
9 }, 13 },
10 - "logging": {  
11 - "level": "Information" 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"
12 } 23 }
  24 + },
  25 + "logging": { "level": "Information" }
13 } 26 }
  27 +
  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 +}
  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 +
  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 +}
  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 +}
  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 +
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 -}  
  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);  
18 - }  
19 - catch 21 + var t = await SecureStorage.GetAsync(Key);
  22 + if (!string.IsNullOrWhiteSpace(t))
20 { 23 {
21 - // 个别设备可能不支持安全存储,兜底返回 null  
22 - return null; 24 + System.Diagnostics.Debug.WriteLine($"[TokenStorage] Loaded from SecureStorage, len={t?.Length}");
  25 + return t;
23 } 26 }
24 } 27 }
25 - public static Task<string?> LoadAsync() =>  
26 - SecureStorage.GetAsync(Key);  
27 - public static Task ClearAsync()  
28 - => SecureStorage.SetAsync(Key, string.Empty); // 清空即可;也可用 Remove 28 + catch (Exception ex)
  29 + {
  30 + System.Diagnostics.Debug.WriteLine($"[TokenStorage] SecureStorage error: {ex.Message}");
  31 + }
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,38 +50,35 @@ public partial class LoginViewModel : ObservableObject @@ -51,38 +50,35 @@ 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); 63 + var result = JsonSerializer.Deserialize<ApiResponse<LoginResult>>(raw,
  64 + new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
  65 +
  66 + var ok = (result?.success == true) || (result?.code is 0 or 200);
66 var token = result?.result?.token; 67 var token = result?.result?.token;
67 68
68 - if (ok && !string.IsNullOrWhiteSpace(token)) 69 + if (!ok || string.IsNullOrWhiteSpace(token))
69 { 70 {
  71 + await Application.Current.MainPage.DisplayAlert("登录失败", result?.message ?? "登录返回无效", "确定");
  72 + return;
  73 + }
  74 +
  75 + // ★ 只需保存;后续所有经 DI 的 HttpClient 都会由 AuthHeaderHandler 自动加 Authorization 头
70 await TokenStorage.SaveAsync(token!); 76 await TokenStorage.SaveAsync(token!);
71 - ApiClient.SetBearer(token);  
72 - if (ok && !string.IsNullOrWhiteSpace(token))  
73 - {  
74 - await TokenStorage.SaveAsync(token);  
75 - ApiClient.SetBearer(token); 77 + System.Diagnostics.Debug.WriteLine("saved token len=" + token?.Length);
76 78
  79 + // 进入主壳
77 App.SwitchToLoggedInShell(); 80 App.SwitchToLoggedInShell();
78 } 81 }
79 -  
80 - }  
81 - else  
82 - {  
83 - await Application.Current.MainPage.DisplayAlert("登录失败", result?.message ?? "登录返回无效", "确定");  
84 - }  
85 - }  
86 catch (OperationCanceledException) 82 catch (OperationCanceledException)
87 { 83 {
88 await Application.Current.MainPage.DisplayAlert("超时", "登录请求超时,请检查网络", "确定"); 84 await Application.Current.MainPage.DisplayAlert("超时", "登录请求超时,请检查网络", "确定");
@@ -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();
  30 +
  31 + public MoldOutboundExecuteViewModel(IWorkOrderApi api)
  32 + {
  33 + _api = api;
  34 + }
12 35
13 - [RelayCommand]  
14 - public async Task LoadAsync(string orderNo) 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 {
  15 + public partial class WorkOrderSearchViewModel : ObservableObject
  16 + {
  17 + private readonly IWorkOrderApi _api;
  18 +
  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;
  31 +
10 public ObservableCollection<WorkOrderDto> Orders { get; } = new(); 32 public ObservableCollection<WorkOrderDto> Orders { get; } = new();
11 33
12 - private string? _keyword;  
13 - public string? Keyword 34 + public IAsyncRelayCommand SearchCommand { get; }
  35 + public IRelayCommand ClearCommand { get; }
  36 +
  37 + public WorkOrderSearchViewModel(IWorkOrderApi api)
14 { 38 {
15 - get => _keyword;  
16 - set => SetProperty(ref _keyword, value); 39 + _api = api;
  40 + SearchCommand = new AsyncRelayCommand(SearchAsync);
  41 + ClearCommand = new RelayCommand(ClearFilters);
  42 + _ = EnsureDictsLoadedAsync(); // fire-and-forget
17 } 43 }
  44 + private async Task EnsureDictsLoadedAsync()
  45 + {
  46 + if (_dictsLoaded) return;
18 47
19 - public DateTime StartDate { get; set; } = DateTime.Today.AddDays(-7);  
20 - public DateTime EndDate { get; set; } = DateTime.Today;  
21 -  
22 - public List<string> StatusList { get; } = new() { "待执行", "执行中", "已完成", "已取消" };  
23 - private string? _selectedStatus = "待执行";  
24 - public string? SelectedStatus 48 + try
25 { 49 {
26 - get => _selectedStatus;  
27 - set => SetProperty(ref _selectedStatus, value);  
28 - } 50 + var bundle = await _api.GetWorkOrderDictsAsync();
29 51
30 - private WorkOrderDto? _selectedOrder;  
31 - public WorkOrderDto? SelectedOrder 52 + // 1) 填充状态下拉
  53 + StatusOptions.Clear();
  54 + StatusOptions.Add(new StatusOption { Text = "全部", Value = null });
  55 + foreach (var d in bundle.AuditStatus)
32 { 56 {
33 - get => _selectedOrder;  
34 - set => SetProperty(ref _selectedOrder, value); 57 + if (string.IsNullOrWhiteSpace(d.dictItemValue)) continue;
  58 + StatusOptions.Add(new StatusOption
  59 + {
  60 + Text = d.dictItemName ?? d.dictItemValue!,
  61 + Value = d.dictItemValue
  62 + });
35 } 63 }
  64 + SelectedStatusOption ??= StatusOptions.FirstOrDefault();
  65 +
  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!;
36 71
37 - public ICommand SearchCommand { get; }  
38 - public ICommand ClearCommand { get; } 72 + _urgentMap.Clear();
  73 + foreach (var d in bundle.Urgent)
  74 + if (!string.IsNullOrWhiteSpace(d.dictItemValue))
  75 + _urgentMap[d.dictItemValue!] = d.dictItemName ?? d.dictItemValue!;
39 76
40 - public WorkOrderSearchViewModel() 77 + _dictsLoaded = true;
  78 + }
  79 + catch (Exception ex)
41 { 80 {
42 - SearchCommand = new AsyncRelayCommand(SearchAsync);  
43 - ClearCommand = new RelayCommand(Clear); 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 + }
44 } 88 }
45 89
46 public async Task SearchAsync() 90 public async Task SearchAsync()
47 { 91 {
48 - // TODO: 调后端 API;这里先用 Mock  
49 - await Task.Delay(120); 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 +
50 Orders.Clear(); 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);
51 124
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 - ); 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 + }
59 145
60 - foreach (var m in filtered) Orders.Add(m); 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;
61 } 151 }
62 152
63 - public void Clear() 153 +
  154 + private void ClearFilters()
64 { 155 {
65 Keyword = string.Empty; 156 Keyword = string.Empty;
  157 + SelectedStatus = "全部";
66 StartDate = DateTime.Today.AddDays(-7); 158 StartDate = DateTime.Today.AddDays(-7);
67 EndDate = DateTime.Today; 159 EndDate = DateTime.Today;
68 - SelectedStatus = "待执行"; 160 + PageIndex = 1;
  161 + SelectedStatusOption = StatusOptions.FirstOrDefault();
69 Orders.Clear(); 162 Orders.Clear();
70 } 163 }
71 164
72 - private IEnumerable<WorkOrderDto> MockOrders() 165 +
  166 +
  167 + private static DateTime? TryParseDt(string? s)
  168 + {
  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)
73 { 179 {
74 - yield return new WorkOrderDto 180 + if (item is null) return;
  181 +
  182 + // 基础信息列表(显示在执行页“工单基础信息表格”)
  183 + var baseInfos = new List<BaseInfoItem>
75 { 184 {
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  
84 - {  
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  
93 - {  
94 - OrderNo = "SCGD20250601004",  
95 - OrderName = "XXXXXXXXXX",  
96 - Status = "执行中",  
97 - ProductName = "梅林午餐肉罐头198g",  
98 - Quantity = 8000,  
99 - CreateDate = DateTime.Today.AddDays(-1) 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 ?? "" },
100 }; 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;  
29 -  
30 - // 初始化命令  
31 - ShowPendingCommand = new RelayCommand(() => SwitchTab(true));  
32 - ShowScannedCommand = new RelayCommand(() => SwitchTab(false));  
33 - ConfirmCommand = new AsyncRelayCommand(ConfirmInboundAsync);  
34 -  
35 - // 默认显示待入库明细  
36 - IsPendingVisible = true;  
37 - IsScannedVisible = false; 11 + private readonly IInboundMaterialService _warehouseSvc;
38 12
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 - }; 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;
45 21
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 - } 22 + // 列表数据源
  23 + public ObservableCollection<string> AvailableBins { get; } = new();
  24 + public ObservableCollection<OutScannedItem> ScannedList { get; } = new();
  25 + public ObservableCollection<PendingItem> PendingList { get; } = new();
52 26
53 - // 基础信息  
54 - [ObservableProperty] private string orderNo;  
55 - [ObservableProperty] private string linkedDeliveryNo;  
56 - [ObservableProperty] private string linkedPurchaseNo;  
57 - [ObservableProperty] private string supplier; 27 + [ObservableProperty] private OutScannedItem? selectedScanItem;
58 28
59 - // Tab 显示控制  
60 - [ObservableProperty] private bool isPendingVisible;  
61 - [ObservableProperty] private bool isScannedVisible; 29 + // Tab 控制
  30 + [ObservableProperty] private bool isPendingVisible = true;
  31 + [ObservableProperty] private bool isScannedVisible = false;
62 32
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();
  208 +
  209 + // 友好:若那条还在列表里,恢复选中
  210 + var hit = ScannedList.FirstOrDefault(x =>
  211 + string.Equals(x.Barcode, selectedBarcode, StringComparison.OrdinalIgnoreCase));
  212 + if (hit != null) { hit.IsSelected = true; SelectedScanItem = hit; }
171 213
172 - // 如果希望数量减到 0 时取消高亮,可打开下一行  
173 - // if (row.Qty == 0) row.IsSelected = false; 214 + await ShowTip("已取消扫描。");
174 } 215 }
175 216
  217 +
176 public async Task HandleScannedAsync(string data, string symbology) 218 public async Task HandleScannedAsync(string data, string symbology)
177 { 219 {
178 - // TODO: 你的规则(判断是订单号/物料码/包装码等)  
179 - // 然后:查询接口 / 加入列表 / 自动提交  
180 - await Task.CompletedTask; 220 + var barcode = (data ?? string.Empty).Trim();
  221 + if (string.IsNullOrWhiteSpace(barcode))
  222 + {
  223 + await ShowTip("无效条码。");
  224 + return;
181 } 225 }
182 - private Task ShowTip(string message) =>  
183 - Shell.Current?.DisplayAlert("提示", message, "确定") ?? Task.CompletedTask;  
184 226
185 - /// <summary>  
186 - /// 确认入库逻辑  
187 - /// </summary>  
188 - public async Task<bool> ConfirmInboundAsync() 227 + if (string.IsNullOrWhiteSpace(InstockId))
189 { 228 {
190 - if (string.IsNullOrWhiteSpace(OrderNo))  
191 - return false;  
192 -  
193 - var result = await _warehouseSvc.ConfirmInboundAsync(OrderNo,  
194 - ScannedList.Select(x => new ScanItem(0, x.Barcode, x.Bin, x.Qty)));  
195 -  
196 - return result.Succeeded; 229 + await ShowTip("缺少 InstockId,无法入库。请从查询页进入。");
  230 + return;
197 } 231 }
198 232
199 - public async Task LoadOrderAsync(string orderNo) 233 + // 调用扫码入库接口
  234 + var resp = await _warehouseSvc.InStockByBarcodeAsync(InstockId!, barcode);
  235 +
  236 + if (!resp.Succeeded)
200 { 237 {
201 - // 0) 清空旧数据  
202 - ClearAll(); 238 + await ShowTip(string.IsNullOrWhiteSpace(resp.Message) ? "入库失败,请重试或检查条码。" : resp.Message!);
  239 + return;
  240 + }
203 241
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 : ""; // 你没有就留空 242 + // 成功 → 刷新“待入库明细”和“已扫描明细”
  243 + await LoadPendingAsync();
  244 + await LoadScannedAsync();
210 245
211 - // 2) 预入库库位:从接口取(先 mock),用于行内 Picker  
212 - // (你已经在 IWarehouseDataService 里加了 ListInboundBinsAsync)  
213 - try 246 + // UI 友好:尝试高亮刚扫的那一条
  247 + var hit = ScannedList.FirstOrDefault(x => string.Equals(x.Barcode, barcode, StringComparison.OrdinalIgnoreCase));
  248 + if (hit != null)
214 { 249 {
215 - var bins = await _warehouseSvc.ListInboundBinsAsync(orderNo);  
216 - AvailableBins.Clear();  
217 - foreach (var b in bins) AvailableBins.Add(b); 250 + hit.IsSelected = true;
  251 + SelectedScanItem = hit;
  252 + // 切到“已扫描”页签更直观(可选)
  253 + // SwitchTab(false);
218 } 254 }
219 - catch  
220 - {  
221 - // 兜底:本地 mock,避免空集合导致 Picker 无选项  
222 - AvailableBins.Clear();  
223 - foreach (var b in new[] { "CK1_A201", "CK1_A202", "CK1_A203", "CK1_A204" })  
224 - AvailableBins.Add(b);  
225 } 255 }
226 - var defaultBin = AvailableBins.FirstOrDefault() ?? "CK1_A201";  
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 + }
257 294
  295 + // 成功:可选刷新一次,以服务端为准;随后按钮事件里会 ClearAll()
  296 + await LoadPendingAsync();
  297 + await LoadScannedAsync();
  298 + return true;
258 } 299 }
259 300
260 - // 待入库明细模型 301 + }
  302 +
  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