作者 李壮

outbound

正在显示 31 个修改的文件 包含 1348 行增加1024 行删除
... ... @@ -59,13 +59,17 @@ public record InboundScannedRow(
public record OutboundPendingRow(
string? Barcode,
string? DetailId,
string? Location,
string? MaterialName,
int PendingQty, // instockQty
int ScannedQty, // qty
string? Spec);
string? MaterialCode,
string? Spec,
string? Location,
string? ProductionBatch,
string? StockBatch,
int OutstockQty,
int Qty
);
public record OutboundScannedRow(
string Barcode,
... ...
... ... @@ -2,7 +2,7 @@
<ContentPage x:Class="IndustrialControl.Pages.InboundMoldPage"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
Title="仓储管理系统">
Shell.NavBarIsVisible="True">
<Grid RowDefinitions="Auto,Auto,*,Auto,Auto" Padding="12" RowSpacing="12">
... ...
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="IndustrialControl.Pages.OutboundMoldPage"
BackgroundColor="White">
BackgroundColor="White" Shell.NavBarIsVisible="True">
<Grid RowDefinitions="Auto,Auto,Auto,*,Auto">
... ...
... ... @@ -2,7 +2,7 @@
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="IndustrialControl.Pages.OutboundMoldSearchPage"
Title="工单查询">
Shell.NavBarIsVisible="True">
<!-- 根布局:顶部(蓝条) / 条件区 / 列表 -->
<Grid RowDefinitions="Auto,Auto,*">
... ...
... ... @@ -4,7 +4,7 @@
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:conv="clr-namespace:IndustrialControl.Converters"
Title="模具出库执行">
Shell.NavBarIsVisible="True">
<ContentPage.Resources>
<ResourceDictionary>
<!-- 转换器 -->
... ...
... ... @@ -2,7 +2,7 @@
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="IndustrialControl.Pages.WorkOrderSearchPage"
Title="工单查询">
Shell.NavBarIsVisible="True">
<!-- 根布局:顶部(蓝条) / 条件区 / 列表 -->
<Grid RowDefinitions="Auto,Auto,*">
... ...
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="IndustrialControl.Pages.InboundMaterialPage"
BackgroundColor="White">
BackgroundColor="White"
Shell.NavBarIsVisible="True">
<Grid RowDefinitions="Auto,Auto,Auto,*,Auto">
... ... @@ -231,7 +232,7 @@
VerticalOptions="Fill"
HeightRequest="50">
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding ConfirmCommand}" />
<TapGestureRecognizer Tapped="OnConfirmClicked" />
</Grid.GestureRecognizers>
<StackLayout Orientation="Horizontal"
HorizontalOptions="Center"
... ...
... ... @@ -4,7 +4,7 @@
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Name="Page"
Title="入库单查询">
Shell.NavBarIsVisible="True">
<ScrollView>
<VerticalStackLayout Spacing="12" Padding="12">
... ...
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="IndustrialControl.Pages.InboundProductionPage"
BackgroundColor="White">
BackgroundColor="White" Shell.NavBarIsVisible="True">
<Grid RowDefinitions="Auto,Auto,Auto,*,Auto">
... ... @@ -45,7 +45,7 @@
<Label.FormattedText>
<FormattedString>
<Span Text="工单号:" FontAttributes="Bold"/>
<Span Text="{Binding ArrivalNo}"/>
<Span Text="{Binding WorkOrderNo}"/>
</FormattedString>
</Label.FormattedText>
</Label>
... ... @@ -55,7 +55,7 @@
<Label.FormattedText>
<FormattedString>
<Span Text="产品名称:" FontAttributes="Bold"/>
<Span Text="{Binding PurchaseNo}"/>
<Span Text="{Binding MaterialName}"/>
</FormattedString>
</Label.FormattedText>
</Label>
... ... @@ -65,7 +65,7 @@
<Label.FormattedText>
<FormattedString>
<Span Text="待入库数:" FontAttributes="Bold"/>
<Span Text="{Binding SupplierName}"/>
<Span Text="{Binding InstockQty}"/>
</FormattedString>
</Label.FormattedText>
</Label>
... ... @@ -212,7 +212,7 @@
VerticalOptions="Fill"
HeightRequest="50">
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding ConfirmCommand}" />
<TapGestureRecognizer Tapped="OnConfirmClicked" />
</Grid.GestureRecognizers>
<StackLayout Orientation="Horizontal"
HorizontalOptions="Center"
... ...
... ... @@ -9,6 +9,9 @@ namespace IndustrialControl.Pages;
[QueryProperty(nameof(OrderTypeName), "orderTypeName")]
[QueryProperty(nameof(PurchaseNo), "purchaseNo")]
[QueryProperty(nameof(SupplierName), "supplierName")]
[QueryProperty(nameof(WorkOrderNo), "workOrderNo")]
[QueryProperty(nameof(MaterialName), "materialName")]
[QueryProperty(nameof(InstockQty), "instockQty")]
[QueryProperty(nameof(CreatedTime), "createdTime")]
public partial class InboundProductionPage : ContentPage
{
... ... @@ -21,6 +24,10 @@ public partial class InboundProductionPage : ContentPage
public string? PurchaseNo { get; set; }
public string? SupplierName { get; set; }
public string? CreatedTime { get; set; }
public string? WorkOrderNo { get; set; }
public string? MaterialName { get; set; }
public int InstockQty { get; set; }
private readonly IDialogService _dialogs;
public InboundProductionPage(InboundProductionViewModel vm, ScanService scanSvc, IDialogService dialogs)
... ... @@ -53,6 +60,9 @@ public partial class InboundProductionPage : ContentPage
orderTypeName: OrderTypeName ?? "",
purchaseNo: PurchaseNo ?? "",
supplierName: SupplierName ?? "",
workOrderNo: WorkOrderNo ?? "",
materialName: MaterialName ?? "",
instockQty: InstockQty,
createdTime: CreatedTime ?? ""
);
}
... ...
... ... @@ -4,7 +4,7 @@
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Name="Page"
Title="入库单查询">
Shell.NavBarIsVisible="True">
<ScrollView>
<VerticalStackLayout Spacing="12" Padding="12">
... ...
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="IndustrialControl.Pages.OutboundFinishedPage"
BackgroundColor="White">
BackgroundColor="White" Shell.NavBarIsVisible="True">
<Grid RowDefinitions="Auto,Auto,Auto,*,Auto">
... ... @@ -14,216 +14,243 @@
FontAttributes="Bold"/>
</Grid>
<!-- 出库单/发货单扫描 -->
<!-- 入库单/条码扫描 -->
<Grid Grid.Row="1" ColumnDefinitions="*,60" Padding="16,8">
<Entry x:Name="ScanEntry"
Placeholder="请输入/扫描出库单号/产品条码"
Placeholder="请扫描出库单/产品/包装条码"
FontSize="14"
VerticalOptions="Center"
BackgroundColor="White"
HeightRequest="40"
Text="{Binding OrderNo}" />
<ImageButton Grid.Column="1"
Source="scan.png"
BackgroundColor="#E6F2FF"
CornerRadius="4"
Padding="10"
Clicked="OnScanClicked"/>
Text="{Binding ScanCode}" />
</Grid>
<!-- 基础信息 -->
<Frame Grid.Row="2" Margin="16,0" Padding="8" BorderColor="#CCCCCC" BackgroundColor="White">
<Grid RowDefinitions="Auto,Auto,Auto,Auto"
ColumnDefinitions="Auto,* ,Auto,*"
ColumnSpacing="8"
RowSpacing="6">
<!-- 出库单号(独占一行) -->
<Label Grid.Row="0" Grid.Column="0" Text="出库单号:" FontAttributes="Bold" VerticalOptions="Center" />
<Label Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="3"
Text="{Binding OrderNo}" VerticalOptions="Center" />
<!-- 发货单号 + 客户 -->
<Label Grid.Row="1" Grid.Column="0" Text="发货单号:" FontAttributes="Bold" VerticalOptions="Center" />
<Label Grid.Row="1" Grid.Column="1" Text="{Binding DeliveryNo}" VerticalOptions="Center" />
<Label Grid.Row="1" Grid.Column="2" Text="客户:" FontAttributes="Bold" VerticalOptions="Center" />
<Label Grid.Row="1" Grid.Column="3" Text="{Binding Customer}" VerticalOptions="Center" />
<!-- 要求发货时间 + 关联销售单 -->
<Label Grid.Row="2" Grid.Column="0" Text="要求发货时间:" FontAttributes="Bold" VerticalOptions="Center" />
<Label Grid.Row="2" Grid.Column="1" Text="{Binding DeliveryTime}" VerticalOptions="Center" />
<Label Grid.Row="2" Grid.Column="2" Text="关联销售单:" FontAttributes="Bold" VerticalOptions="Center" />
<Label Grid.Row="2" Grid.Column="3" Text="{Binding SalesOrder}" VerticalOptions="Center" />
<!-- 发货单备注(独占一行) -->
<Label Grid.Row="3" Grid.Column="0" Text="发货单备注:" FontAttributes="Bold" VerticalOptions="Center" />
<Label Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="3"
Text="{Binding Remark}" VerticalOptions="Center" />
<Frame Grid.Row="2" Margin="8,0" Padding="8" BorderColor="#CCCCCC" BackgroundColor="White">
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="*,*" ColumnSpacing="16" RowSpacing="6">
</Grid>
</Frame>
<!-- 出库单号 -->
<Label Grid.Row="0" Grid.Column="0" FontSize="13" LineBreakMode="TailTruncation" MaxLines="1">
<Label.FormattedText>
<FormattedString>
<Span Text="出库单号:" FontAttributes="Bold"/>
<Span Text="{Binding OutstockNo}"/>
</FormattedString>
</Label.FormattedText>
</Label>
<!-- 发货单号 -->
<Label Grid.Row="1" Grid.Column="0" FontSize="13" LineBreakMode="TailTruncation" MaxLines="1">
<Label.FormattedText>
<FormattedString>
<Span Text="发货单号:" FontAttributes="Bold"/>
<Span Text="{Binding DeliveryNo}"/>
</FormattedString>
</Label.FormattedText>
</Label>
<!-- 客户 -->
<Label Grid.Row="1" Grid.Column="1" FontSize="13" LineBreakMode="TailTruncation" MaxLines="1">
<Label.FormattedText>
<FormattedString>
<Span Text="客户:" FontAttributes="Bold"/>
<Span Text="{Binding PurchaseNo}"/>
</FormattedString>
</Label.FormattedText>
</Label>
<!-- Tab切换 + 表格 -->
<!-- 要求发货时间 -->
<Label Grid.Row="2" Grid.Column="0" FontSize="13" LineBreakMode="TailTruncation" MaxLines="1">
<Label.FormattedText>
<FormattedString>
<Span Text="要求发货时间:" FontAttributes="Bold"/>
<Span Text="{Binding SupplierName}"/>
</FormattedString>
</Label.FormattedText>
</Label>
<!-- 关联销售单 -->
<Label Grid.Row="2" Grid.Column="1" FontSize="13" LineBreakMode="TailTruncation" MaxLines="1">
<Label.FormattedText>
<FormattedString>
<Span Text="关联销售单:" FontAttributes="Bold"/>
<Span Text="{Binding SupplierName}"/>
</FormattedString>
</Label.FormattedText>
</Label>
<!-- 发货单备注 -->
<Label Grid.Row="3" Grid.Column="0" FontSize="13" LineBreakMode="TailTruncation" MaxLines="1">
<Label.FormattedText>
<FormattedString>
<Span Text="发货单备注:" FontAttributes="Bold"/>
<Span Text="{Binding SupplierName}"/>
</FormattedString>
</Label.FormattedText>
</Label>
</Grid>
</Frame>
<!-- Tab切换 -->
<Grid Grid.Row="3" RowDefinitions="Auto,Auto,*" Margin="0">
<!-- Tab -->
<Grid ColumnDefinitions="*,*" BackgroundColor="White">
<Button Text="出库单明细"
BackgroundColor="{Binding PendingTabColor}"
TextColor="{Binding PendingTextColor}"
Clicked="OnPendingTabClicked"/>
<Button Text="待入库明细"
Command="{Binding ShowPendingCommand}"
BackgroundColor="{Binding PendingTabColor}"
TextColor="{Binding PendingTextColor}" />
<Button Text="扫描明细"
Grid.Column="1"
BackgroundColor="{Binding ScannedTabColor}"
TextColor="{Binding ScannedTextColor}"
Clicked="OnScannedTabClicked"/>
Grid.Column="1"
Command="{Binding ShowScannedCommand}"
BackgroundColor="{Binding ScannedTabColor}"
TextColor="{Binding ScannedTextColor}" />
</Grid>
<!-- 出库单明细表头 -->
<!-- 出库单明细表头 -->
<Grid Grid.Row="1"
ColumnDefinitions="2*,2*,1.5*,2*,2*,1.5*,1.5*"
BackgroundColor="#F2F2F2"
IsVisible="{Binding IsPendingVisible}"
Padding="4"
HeightRequest="50">
<Label Text="产品名称" FontAttributes="Bold"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
LineBreakMode="WordWrap"/>
<Label Grid.Column="1" Text="产品编码" FontAttributes="Bold"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
LineBreakMode="WordWrap"/>
<Label Grid.Column="2" Text="规格" FontAttributes="Bold"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
LineBreakMode="WordWrap"/>
<Label Grid.Column="3" Text="出库库位" FontAttributes="Bold"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
LineBreakMode="WordWrap"/>
<Label Grid.Column="4" Text="生产批号" FontAttributes="Bold"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
LineBreakMode="WordWrap"/>
<Label Grid.Column="5" Text="出库数量" FontAttributes="Bold"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
LineBreakMode="WordWrap"/>
<Label Grid.Column="6" Text="已扫描数" FontAttributes="Bold"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
LineBreakMode="WordWrap"/>
<!-- 待出库明细表头 -->
<Grid Grid.Row="1" ColumnDefinitions="*,*,*,*,*,*,*" BackgroundColor="#F2F2F2" IsVisible="{Binding IsPendingVisible}" Padding="8">
<Label Text="产品名称" FontAttributes="Bold" />
<Label Grid.Column="1" Text="产品编码" FontAttributes="Bold" />
<Label Grid.Column="2" Text="规格" FontAttributes="Bold" />
<Label Grid.Column="3" Text="出库库位" FontAttributes="Bold" />
<Label Grid.Column="4" Text="生产批号" FontAttributes="Bold" />
<Label Grid.Column="5" Text="出库数量" FontAttributes="Bold" />
<Label Grid.Column="6" Text="已扫描数" FontAttributes="Bold" />
</Grid>
<!-- 出库单明细列表 -->
<!-- 待入库明细列表 -->
<CollectionView Grid.Row="2"
ItemsSource="{Binding PendingList}"
IsVisible="{Binding IsPendingVisible}">
ItemsSource="{Binding PendingList}"
IsVisible="{Binding IsPendingVisible}"
SelectionMode="Single">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="2*,2*,1.5*,2*,2*,1.5*,1.5*"
Padding="4"
BackgroundColor="White">
<Label Text="{Binding Name}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="1" Text="{Binding Code}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="2" Text="{Binding Spec}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="3" Text="{Binding Bin}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="4" Text="{Binding BatchNo}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="5" Text="{Binding OutQty}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="6" Text="{Binding ScannedQty}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Grid ColumnDefinitions="*,*,*,*,*,*,*" Padding="8" BackgroundColor="White">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="White"/>
</VisualState.Setters>
</VisualState>
<VisualState Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="#CCFFCC"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Label Text="{Binding Name}" />
<Label Grid.Column="1" Text="{Binding MaterialCode}" />
<Label Grid.Column="2" Text="{Binding Spec}" />
<Label Grid.Column="3" Text="{Binding Location}" />
<Label Grid.Column="4" Text="{Binding ProductionBatch}" />
<Label Grid.Column="5" Text="{Binding OutstockQty}" />
<Label Grid.Column="6" Text="{Binding Qty}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!-- 扫描明细表头 -->
<Grid Grid.Row="1"
ColumnDefinitions="40,*,*,*"
BackgroundColor="#F2F2F2"
IsVisible="{Binding IsScannedVisible}"
Padding="4"
HeightRequest="30">
<Label Text="选择" FontAttributes="Bold" HorizontalTextAlignment="Center" VerticalTextAlignment="Center" LineBreakMode="NoWrap"/>
<Label Grid.Column="1" Text="条码" FontAttributes="Bold" HorizontalTextAlignment="Center" VerticalTextAlignment="Center" LineBreakMode="NoWrap"/>
<Label Grid.Column="2" Text="产品名称" FontAttributes="Bold" HorizontalTextAlignment="Center" VerticalTextAlignment="Center" LineBreakMode="NoWrap"/>
<Label Grid.Column="3" Text="数量" FontAttributes="Bold" HorizontalTextAlignment="Center" VerticalTextAlignment="Center" LineBreakMode="NoWrap"/>
<Grid Grid.Row="1" ColumnDefinitions="40,*,*,*" BackgroundColor="#F2F2F2" IsVisible="{Binding IsScannedVisible}" Padding="8">
<Label Text="选择" FontAttributes="Bold" />
<Label Grid.Column="1" Text="物料名称" FontAttributes="Bold" />
<Label Grid.Column="2" Text="条码" FontAttributes="Bold" />
<Label Grid.Column="3" Text="数量" FontAttributes="Bold" />
</Grid>
<!-- 扫描明细列表 -->
<CollectionView Grid.Row="2"
ItemsSource="{Binding ScannedList}"
IsVisible="{Binding IsScannedVisible}">
ItemsSource="{Binding ScannedList}"
IsVisible="{Binding IsScannedVisible}"
SelectionMode="Single"
SelectedItem="{Binding SelectedScanItem, Mode=TwoWay}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="40,*,*,*" Padding="4" BackgroundColor="White">
<CheckBox IsChecked="{Binding IsSelected}" HorizontalOptions="Center" VerticalOptions="Center" />
<Label Grid.Column="1" Text="{Binding Barcode}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center" />
<Label Grid.Column="2" Text="{Binding Name}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center" />
<Label Grid.Column="3" Text="{Binding Qty}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center" />
<Grid ColumnDefinitions="40,*,*,*" Padding="8" BackgroundColor="White">
<Grid.Triggers>
<!-- ScanStatus = true → 绿色 -->
<DataTrigger TargetType="Grid" Binding="{Binding ScanStatus}" Value="True">
<Setter Property="BackgroundColor" Value="#E6FFE6" />
</DataTrigger>
</Grid.Triggers>
<CheckBox IsChecked="{Binding IsSelected}" />
<Label Grid.Column="1" Text="{Binding Name}" />
<Label Grid.Column="2" Text="{Binding Barcode}" />
<Entry Grid.Column="3"
Keyboard="Numeric"
HorizontalTextAlignment="Center"
WidthRequest="64"
Completed="OnQtyCompleted"
Text="{Binding Qty, Mode=TwoWay, Converter={StaticResource IntConverter}}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
</Grid>
<!-- 底部按钮 -->
<Grid Grid.Row="4" ColumnDefinitions="*,*,*" Padding="16,8" ColumnSpacing="10">
<!-- 扫描通过 -->
<HorizontalStackLayout BackgroundColor="#4CAF50"
Padding="10"
HorizontalOptions="FillAndExpand"
VerticalOptions="Center"
Grid.Column="0">
<Image Source="pass.png" WidthRequest="20" HeightRequest="20" />
<Label Text="扫描通过"
VerticalOptions="Center"
HorizontalOptions="Center"
TextColor="White"
Margin="5,0,0,0" />
<HorizontalStackLayout.GestureRecognizers>
<Grid BackgroundColor="#4CAF50"
HorizontalOptions="Fill"
VerticalOptions="Fill"
HeightRequest="50">
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding PassScanCommand}" />
</HorizontalStackLayout.GestureRecognizers>
</HorizontalStackLayout>
</Grid.GestureRecognizers>
<StackLayout Orientation="Horizontal"
HorizontalOptions="Center"
VerticalOptions="Center">
<Image Source="pass.png" HeightRequest="20" WidthRequest="20" />
<Label Text="扫描通过"
Margin="5,0,0,0"
VerticalOptions="Center"
TextColor="White" />
</StackLayout>
</Grid>
<!-- 取消扫描 -->
<HorizontalStackLayout BackgroundColor="#F44336"
Padding="10"
HorizontalOptions="FillAndExpand"
VerticalOptions="Center"
Grid.Column="1">
<Image Source="cancel.png" WidthRequest="20" HeightRequest="20" />
<Label Text="取消扫描"
VerticalOptions="Center"
HorizontalOptions="Center"
TextColor="White"
Margin="5,0,0,0" />
<HorizontalStackLayout.GestureRecognizers>
<Grid Grid.Column="1"
BackgroundColor="#F44336"
HorizontalOptions="Fill"
VerticalOptions="Fill"
HeightRequest="50">
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding CancelScanCommand}" />
</HorizontalStackLayout.GestureRecognizers>
</HorizontalStackLayout>
<!-- 确认出库 -->
<HorizontalStackLayout BackgroundColor="#2196F3"
Padding="10"
HorizontalOptions="FillAndExpand"
VerticalOptions="Center"
Grid.Column="2">
<Image Source="confirm.png" WidthRequest="20" HeightRequest="20" />
<Label Text="确认出库"
VerticalOptions="Center"
HorizontalOptions="Center"
TextColor="White"
Margin="5,0,0,0" />
<HorizontalStackLayout.GestureRecognizers>
<TapGestureRecognizer Command="{Binding ConfirmCommand}" />
</HorizontalStackLayout.GestureRecognizers>
</HorizontalStackLayout>
</Grid.GestureRecognizers>
<StackLayout Orientation="Horizontal"
HorizontalOptions="Center"
VerticalOptions="Center">
<Image Source="cancel.png" HeightRequest="20" WidthRequest="20" />
<Label Text="取消扫描"
Margin="5,0,0,0"
VerticalOptions="Center"
TextColor="White" />
</StackLayout>
</Grid>
<!-- 确认入库 -->
<Grid Grid.Column="2"
BackgroundColor="#2196F3"
HorizontalOptions="Fill"
VerticalOptions="Fill"
HeightRequest="50">
<Grid.GestureRecognizers>
<TapGestureRecognizer Tapped="OnConfirmClicked" />
</Grid.GestureRecognizers>
<StackLayout Orientation="Horizontal"
HorizontalOptions="Center"
VerticalOptions="Center">
<Image Source="confirm.png" HeightRequest="20" WidthRequest="20" />
<Label Text="确认出库"
Margin="5,0,0,0"
VerticalOptions="Center"
TextColor="White" />
</StackLayout>
</Grid>
</Grid>
... ...
using IndustrialControl.Services;
using IndustrialControl.ViewModels;
namespace IndustrialControl.Pages
namespace IndustrialControl.Pages;
[QueryProperty(nameof(OutstockId), "outstockId")]
[QueryProperty(nameof(OutstockNo), "outstockNo")]
[QueryProperty(nameof(OrderType), "orderType")]
[QueryProperty(nameof(OrderTypeName), "orderTypeName")]
[QueryProperty(nameof(RequisitionMaterialNo), "requisitionMaterialNo")]
[QueryProperty(nameof(ReturnNo), "returnNo")]
[QueryProperty(nameof(DeliveryNo), "deliveryNo")]
[QueryProperty(nameof(CreatedTime), "createdTime")]
public partial class OutboundFinishedPage : ContentPage
{
[QueryProperty(nameof(OutstockId), "outstockId")]
[QueryProperty(nameof(OutstockNo), "outstockNo")]
[QueryProperty(nameof(OrderType), "orderType")]
[QueryProperty(nameof(OrderTypeName), "orderTypeName")]
[QueryProperty(nameof(PurchaseNo), "purchaseNo")]
[QueryProperty(nameof(SupplierName), "supplierName")]
[QueryProperty(nameof(CreatedTime), "createdTime")]
public partial class OutboundFinishedPage : ContentPage
private readonly ScanService _scanSvc;
private readonly OutboundFinishedViewModel _vm;
public string? OutstockId { get; set; }
public string? OutstockNo { get; set; }
public string? OrderType { get; set; }
public string? OrderTypeName { get; set; }
public string? RequisitionMaterialNo { get; set; }
public string? ReturnNo { get; set; }
public string? DeliveryNo { get; set; }
public string? CreatedTime { get; set; }
private readonly IDialogService _dialogs;
public OutboundFinishedPage(OutboundFinishedViewModel vm, ScanService scanSvc, IDialogService dialogs)
{
private readonly ScanService _scanSvc;
private readonly OutboundFinishedViewModel _vm;
public string? OutstockId { get; set; }
public string? OutstockNo { get; set; }
public string? OrderType { get; set; }
public string? OrderTypeName { get; set; }
public string? PurchaseNo { get; set; }
public string? SupplierName { get; set; }
public string? CreatedTime { get; set; }
public OutboundFinishedPage(OutboundFinishedViewModel vm, ScanService scanSvc)
{
InitializeComponent();
BindingContext = vm;
_scanSvc = scanSvc;
_vm = vm;
_scanSvc.Prefix = null; // 例如 "}q" 之类的前缀;没有就留 null
// _scanSvc.Suffix = "\n"; // 如果设备会附带换行,可去掉;没有就设 null
//_scanSvc.DebounceMs = 250;
_scanSvc.Suffix = null; // 先关掉
_scanSvc.DebounceMs = 0; // 先关掉
}
InitializeComponent();
BindingContext = vm;
_scanSvc = scanSvc;
_vm = vm;
_dialogs = dialogs;
// 可选:配置前后缀与防抖
_scanSvc.Prefix = null; // 例如 "}q" 之类的前缀;没有就留 null
// _scanSvc.Suffix = "\n"; // 如果设备会附带换行,可去掉;没有就设 null
//_scanSvc.DebounceMs = 250;
_scanSvc.Suffix = null; // 先关掉
_scanSvc.DebounceMs = 0; // 先关掉
}
protected override async void OnAppearing()
{
base.OnAppearing();
protected override async void OnAppearing()
// ✅ 用搜索页带过来的基础信息初始化页面,并拉取两张表
if (!string.IsNullOrWhiteSpace(OutstockId))
{
base.OnAppearing();
// ✅ 用搜索页带过来的基础信息初始化页面,并拉取两张表
if (!string.IsNullOrWhiteSpace(OutstockId))
{
await _vm.InitializeFromSearchAsync(
outstockId: OutstockId ?? "",
outstockNo: OutstockNo ?? "",
orderType: OrderType ?? "",
orderTypeName: OrderTypeName ?? "",
purchaseNo: PurchaseNo ?? "",
supplierName: SupplierName ?? "",
createdTime: CreatedTime ?? ""
);
}
_scanSvc.Scanned += OnScanned;
_scanSvc.StartListening();
_scanSvc.Attach(ScanEntry);
ScanEntry.Focus();
await _vm.InitializeFromSearchAsync(
outstockId: OutstockId ?? "",
outstockNo: OutstockNo ?? "",
orderType: OrderType ?? "",
orderTypeName: OrderTypeName ?? "",
requisitionMaterialNo: RequisitionMaterialNo ?? "",
returnNo: ReturnNo ?? "",
deliveryNo: DeliveryNo ?? "",
createdTime: CreatedTime ?? ""
);
}
protected override void OnDisappearing()
{
// 退出页面即注销(防止多个程序/页面抢处理)
_scanSvc.Scanned -= OnScanned;
_scanSvc.StopListening();
_scanSvc.Scanned += OnScanned;
_scanSvc.StartListening();
_scanSvc.Attach(ScanEntry);
ScanEntry.Focus();
}
base.OnDisappearing();
}
private void OnScanned(string data, string type)
{
MainThread.BeginInvokeOnMainThread(async () =>
{
// 常见处理:自动填入单号/条码并触发查询或加入明细
_vm.ScanCode = data;
// 你原本的逻辑:若识别到是订单号 → 查询;若是包装码 → 加入列表等
await _vm.HandleScannedAsync(data, type);
});
}
/// <summary>
/// 清空扫描记录
/// </summary>
void OnClearClicked(object sender, EventArgs e)
{
_vm.ClearScan();
ScanEntry.Text = string.Empty;
ScanEntry.Focus();
}
/// <summary>
/// 清空扫描记录
/// </summary>
void OnClearClicked(object sender, EventArgs e)
{
_vm.ClearScan();
ScanEntry.Text = string.Empty;
ScanEntry.Focus();
}
protected override void OnDisappearing()
{
// 退出页面即注销(防止多个程序/页面抢处理)
_scanSvc.Scanned -= OnScanned;
_scanSvc.StopListening();
/// <summary>
/// 预留摄像头扫码
/// </summary>
async void OnScanClicked(object sender, EventArgs e)
{
await DisplayAlert("提示", "此按钮预留摄像头扫码;硬件扫描直接扣扳机。", "确定");
}
base.OnDisappearing();
}
/// <summary>
/// 点击“出库单明细”Tab
/// </summary>
void OnPendingTabClicked(object sender, EventArgs e)
private void OnScanned(string data, string type)
{
MainThread.BeginInvokeOnMainThread(async () =>
{
_vm.ShowPendingCommand.Execute(null);
}
// 常见处理:自动填入单号/条码并触发查询或加入明细
_vm.ScanCode = data;
// 你原本的逻辑:若识别到是订单号 → 查询;若是包装码 → 加入列表等
await _vm.HandleScannedAsync(data, type);
});
}
/// <summary>
/// 点击“扫描明细”Tab
/// </summary>
void OnScannedTabClicked(object sender, EventArgs e)
{
_vm.ShowScannedCommand.Execute(null);
}
/// <summary>
/// 扫描通过
/// </summary>
void OnPassScanClicked(object sender, EventArgs e)
/// <summary>
/// 确认入库按钮点击
/// </summary>
async void OnConfirmClicked(object sender, EventArgs e)
{
var ok = await _vm.ConfirmOutboundAsync();
if (ok)
{
_vm.PassScanCommand.Execute(null);
}
await DisplayAlert("提示", "入库成功", "确定");
_vm.ClearAll();
/// <summary>
/// 取消扫描
/// </summary>
void OnCancelScanClicked(object sender, EventArgs e)
// ✅ 返回到工单查询页面(InboundFinishedSearchPage)
await Shell.Current.GoToAsync($"//{nameof(OutboundFinishedSearchPage)}");
}
else
{
_vm.CancelScanCommand.Execute(null);
await DisplayAlert("提示", "入库失败,请检查数据", "确定");
}
}
/// <summary>
/// 确认出库
/// </summary>
async void OnConfirmClicked(object sender, EventArgs e)
private async void OnBinTapped(object? sender, TappedEventArgs e)
{
var bindable = sender as BindableObject;
var row = bindable?.BindingContext;
if (row == null) return;
var type = row.GetType();
var currentBin = type.GetProperty("Location")?.GetValue(row)?.ToString();
// 1) 打开公共组件选择库位(你已完成的组件)
var picked = await BinPickerPage.ShowAsync(currentBin);
if (picked == null) return;
// 2) 调用 VM:带上行对象 + 选中的 BinInfo,内部会调接口 & 回填行
await _vm.UpdateRowLocationAsync(row, picked);
}
private async void OnQtyCompleted(object sender, EventArgs e)
{
if (sender is not Entry entry) return;
if (entry.BindingContext is not IndustrialControl.ViewModels.OutScannedItem row) return;
// 只看 ScanStatus:未通过则不提交
if (!row.ScanStatus)
{
_vm.ConfirmCommand.Execute(null);
await DisplayAlert("提示", "该行尚未扫描通过,不能修改数量。", "确定");
return;
}
await _vm.UpdateQuantityForRowAsync(row);
}
}
... ...
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage x:Name="Page"
<ContentPage
x:Class="IndustrialControl.Pages.OutboundFinishedSearchPage"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="IndustrialControl.Pages.OutboundFinishedSearchPage"
xmlns:conv="clr-namespace:IndustrialControl.Converters"
Title="仓储管理系统">
<ContentPage.Resources>
<ResourceDictionary>
<!-- 空/非空转布尔:非空 => true(按钮可用) -->
<conv:NullToBoolConverter x:Key="NullToBoolConverter" />
</ResourceDictionary>
</ContentPage.Resources>
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Name="Page"
Shell.NavBarIsVisible="True">
<Grid RowDefinitions="Auto,*,Auto" Padding="16" BackgroundColor="#F6F7FB">
<ScrollView>
<VerticalStackLayout Spacing="12" Padding="12">
<!-- 顶部:输入条件(第0行) -->
<VerticalStackLayout Grid.Row="0" Spacing="10">
<!-- 查询区域 -->
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto" ColumnSpacing="8" RowSpacing="8">
<Entry x:Name="OrderEntry"
Grid.Row="0" Grid.Column="0"
Placeholder="请输入出库单条码"
VerticalOptions="Center"
BackgroundColor="White"
Placeholder="请扫描输入出库单条码"
Text="{Binding SearchOrderNo}" />
<!-- 开始日期 -->
<DatePicker Grid.Row="1" Grid.Column="0"
Date="{Binding StartDate}"
MinimumDate="2000-01-01"
MaximumDate="{Binding EndDate}" />
<!-- 结束日期 -->
<DatePicker Grid.Row="1" Grid.Column="1"
Date="{Binding EndDate}"
MinimumDate="{Binding StartDate}" />
<Button Grid.Row="1" Grid.Column="1"
<Button Grid.Row="0" Grid.Column="1"
Text="查询"
Command="{Binding SearchCommand}" />
<Grid Grid.Row="1" Grid.ColumnSpan="2" ColumnDefinitions="Auto,*,Auto,*" ColumnSpacing="8">
<Label Grid.Column="0" Text="开始:" VerticalTextAlignment="Center"/>
<DatePicker Grid.Column="1" Date="{Binding StartDate}" />
<Label Grid.Column="2" Text="结束:" VerticalTextAlignment="Center"/>
<DatePicker Grid.Column="3" Date="{Binding EndDate}" />
</Grid>
</Grid>
</VerticalStackLayout>
<!-- 中部:结果列表(第1行) -->
<CollectionView Grid.Row="1"
ItemsSource="{Binding Orders}"
SelectionMode="Single"
SelectedItem="{Binding SelectedOrder, Mode=TwoWay}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Frame Margin="0,8,0,0" Padding="12" HasShadow="True" CornerRadius="10">
<!-- ⭐ 点击整卡片触发命令 -->
<Frame.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding BindingContext.GoOutboundCommand, Source={x:Reference Page}}"
CommandParameter="{Binding .}" />
</Frame.GestureRecognizers>
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="Auto,*" ColumnSpacing="8">
<Label Grid.Row="0" Grid.Column="0" Text="出库单号:" FontAttributes="Bold"/>
<Label Grid.Row="0" Grid.Column="1" Text="{Binding outstockNo}"/>
<!-- 列表区域 -->
<CollectionView Grid.Row="1"
ItemsSource="{Binding Orders}"
SelectionMode="Single"
SelectedItem="{Binding SelectedOrder, Mode=TwoWay}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Frame Margin="0,8,0,0" Padding="12" HasShadow="True" CornerRadius="10">
<!-- 点击整卡片:直接调用 VM 的 GoOutboundCommand,并把当前项作为参数 -->
<Frame.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding BindingContext.GoOutboundCommand, Source={x:Reference Page}}"
CommandParameter="{Binding .}" />
</Frame.GestureRecognizers>
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto"
ColumnDefinitions="Auto,*" ColumnSpacing="8">
<Label Grid.Row="0" Grid.Column="0" Text="出库单号:" FontAttributes="Bold"/>
<Label Grid.Row="0" Grid.Column="1" Text="{Binding outstockNo}"/>
<Label Grid.Row="1" Grid.Column="0" Text="出库单类型:" FontAttributes="Bold"/>
<Label Grid.Row="1" Grid.Column="1" Text="{Binding orderTypeName}" />
<Label Grid.Row="1" Grid.Column="0" Text="出库单类型:" FontAttributes="Bold"/>
<Label Grid.Row="1" Grid.Column="1" Text="{Binding orderTypeName}" />
<Label Grid.Row="2" Grid.Column="0" Text="关联发货号:" FontAttributes="Bold"/>
<Label Grid.Row="2" Grid.Column="1" Text="{Binding deliveryNo}" />
<Label Grid.Row="2" Grid.Column="0" Text="关联发货号:" FontAttributes="Bold"/>
<Label Grid.Row="2" Grid.Column="1" Text="{Binding deliveryNo}" />
<Label Grid.Row="3" Grid.Column="0" Text="关联销售号:" FontAttributes="Bold"/>
<Label Grid.Row="3" Grid.Column="1" Text="{Binding arrivalNo}" />
<Label Grid.Row="3" Grid.Column="0" Text="关联销售号:" FontAttributes="Bold"/>
<Label Grid.Row="3" Grid.Column="1" Text="{Binding arrivalNo}" />
<Label Grid.Row="4" Grid.Column="0" Text="创建日期:" FontAttributes="Bold"/>
<Label Grid.Row="4" Grid.Column="1" Text="{Binding createdTime}" />
</Grid>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<Label Grid.Row="4" Grid.Column="0" Text="创建日期:" FontAttributes="Bold"/>
<Label Grid.Row="4" Grid.Column="1" Text="{Binding createdTime}" />
</Grid>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!-- 底部:操作(第2行) -->
<Grid Grid.Row="2" ColumnDefinitions="*,Auto" Padding="0,8,0,0">
<Label Text="{Binding Orders.Count, StringFormat='共 {0} 条'}"
VerticalTextAlignment="Center" />
</Grid>
</Grid>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
... ...
using System.Threading;
using IndustrialControl.Services;
using IndustrialControl.ViewModels;
namespace IndustrialControl.Pages;
... ... @@ -10,7 +9,7 @@ public partial class OutboundFinishedSearchPage : ContentPage
public OutboundFinishedSearchPage(OutboundFinishedSearchViewModel vm, ScanService scanSvc)
{
_vm = vm;
BindingContext = vm;
_scanSvc = scanSvc;
InitializeComponent();
... ... @@ -30,6 +29,8 @@ public partial class OutboundFinishedSearchPage : ContentPage
//键盘输入
_scanSvc.Attach(OrderEntry);
OrderEntry.Focus();
}
/// <summary>
... ... @@ -59,4 +60,5 @@ public partial class OutboundFinishedSearchPage : ContentPage
});
}
}
... ...
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="IndustrialControl.Pages.OutboundMaterialPage"
BackgroundColor="White">
BackgroundColor="White" Shell.NavBarIsVisible="True">
<Grid RowDefinitions="Auto,Auto,Auto,*,Auto">
<!-- 顶部蓝色标题栏 -->
<Grid BackgroundColor="#007BFF" HeightRequest="60" Padding="16,0">
<Label Text="仓储管理系统"
VerticalOptions="Center"
<Label Text="仓储管理系统"
VerticalOptions="Center"
TextColor="White"
FontSize="18"
FontSize="18"
FontAttributes="Bold"/>
</Grid>
<!-- 库单/条码扫描 -->
<!-- 库单/条码扫描 -->
<Grid Grid.Row="1" ColumnDefinitions="*,60" Padding="16,8">
<Entry x:Name="ScanEntry"
Placeholder="请扫描出库单/产品/包装条码"
<Entry x:Name="ScanEntry"
Placeholder="请扫描输入出库单条码"
FontSize="14"
VerticalOptions="Center"
BackgroundColor="White"
HeightRequest="40"
Text="{Binding ScanCode}" />
<ImageButton Grid.Column="1"
Source="scan.png"
BackgroundColor="#E6F2FF"
CornerRadius="4"
Padding="10"
Clicked="OnScanClicked"/>
</Grid>
<!-- 基础信息 -->
<Frame Grid.Row="2" Margin="16,0" Padding="8" BorderColor="#CCCCCC" BackgroundColor="White">
<Grid RowDefinitions="Auto,Auto,Auto,Auto" ColumnDefinitions="Auto,*" ColumnSpacing="8" RowSpacing="6">
<!-- 出库单号 -->
<Label Grid.Row="0" Grid.Column="0" Text="出库单号:" FontAttributes="Bold"/>
<Label Grid.Row="0" Grid.Column="1" Text="{Binding OrderNo}" LineBreakMode="CharacterWrap"/>
<!-- 发货单号 + 客户 -->
<Label Grid.Row="1" Grid.Column="0" Text="发货单号:" FontAttributes="Bold"/>
<Label Grid.Row="1" Grid.Column="1" Text="{Binding DeliveryNo}" LineBreakMode="CharacterWrap"/>
<Label Grid.Row="1" Grid.Column="2" Text="客户:" FontAttributes="Bold"/>
<Label Grid.Row="1" Grid.Column="3" Text="{Binding CustomerName}" LineBreakMode="CharacterWrap"/>
<!-- 发货时间 + 关联销售单 -->
<Label Grid.Row="2" Grid.Column="0" Text="发货时间:" FontAttributes="Bold"/>
<Label Grid.Row="2" Grid.Column="1" Text="{Binding DeliveryTime}" LineBreakMode="CharacterWrap"/>
<Label Grid.Row="2" Grid.Column="2" Text="关联销售单:" FontAttributes="Bold"/>
<Label Grid.Row="2" Grid.Column="3" Text="{Binding LinkedOrderNo}" LineBreakMode="CharacterWrap"/>
<!-- 发货单备注 -->
<Label Grid.Row="3" Grid.Column="0" Text="发货单备注:" FontAttributes="Bold"/>
<Label Grid.Row="3" Grid.Column="1" Text="{Binding Remark}" LineBreakMode="WordWrap"/>
<Frame Grid.Row="2" Margin="8,0" Padding="8" BorderColor="#CCCCCC" BackgroundColor="White">
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="*,*" ColumnSpacing="16" RowSpacing="6">
<!-- 第一行左:出库单号 -->
<Label Grid.Row="0" Grid.Column="0" FontSize="13" LineBreakMode="TailTruncation" MaxLines="1">
<Label.FormattedText>
<FormattedString>
<Span Text="出库单号:" FontAttributes="Bold"/>
<Span Text="{Binding OutstockNo}"/>
</FormattedString>
</Label.FormattedText>
</Label>
<!-- 第一行右:领料单号 -->
<Label Grid.Row="1" Grid.Column="0" FontSize="13" LineBreakMode="TailTruncation" MaxLines="1">
<Label.FormattedText>
<FormattedString>
<Span Text="领料单号:" FontAttributes="Bold"/>
<Span Text="{Binding RequisitionMaterialNo}"/>
</FormattedString>
</Label.FormattedText>
</Label>
<!-- 第二行左:工单号 -->
<Label Grid.Row="1" Grid.Column="1" FontSize="13" LineBreakMode="TailTruncation" MaxLines="1">
<Label.FormattedText>
<FormattedString>
<Span Text="工单号:" FontAttributes="Bold"/>
<Span Text="{Binding WorkOrderNo}"/>
</FormattedString>
</Label.FormattedText>
</Label>
<!-- 第二行右:出库单备注 -->
<Label Grid.Row="2" Grid.Column="0" FontSize="13" LineBreakMode="TailTruncation" MaxLines="1">
<Label.FormattedText>
<FormattedString>
<Span Text="出库单备注:" FontAttributes="Bold"/>
<Span Text="{Binding SupplierName}"/>
</FormattedString>
</Label.FormattedText>
</Label>
</Grid>
</Frame>
<!-- Tab + 列表 -->
<Grid Grid.Row="3" RowDefinitions="Auto,Auto,*">
<!-- Tab -->
<!-- Tab切换 -->
<Grid Grid.Row="3" RowDefinitions="Auto,Auto,*" Margin="0">
<Grid ColumnDefinitions="*,*" BackgroundColor="White">
<Button Text="出库单明细"
BackgroundColor="{Binding PendingTabColor}"
TextColor="{Binding PendingTextColor}"
Clicked="OnPendingTabClicked"/>
<Button Text="扫描明细"
<Button Text="出库单明细"
Command="{Binding ShowPendingCommand}"
BackgroundColor="{Binding PendingTabColor}"
TextColor="{Binding PendingTextColor}" />
<Button Text="扫描明细"
Grid.Column="1"
BackgroundColor="{Binding ScannedTabColor}"
TextColor="{Binding ScannedTextColor}"
Clicked="OnScannedTabClicked"/>
Command="{Binding ShowScannedCommand}"
BackgroundColor="{Binding ScannedTabColor}"
TextColor="{Binding ScannedTextColor}" />
</Grid>
<!-- 出库单明细表头 -->
<Grid Grid.Row="1"
ColumnDefinitions="2*,2*,1.5*,1.5*,1.5*,1.5*,1.5*"
BackgroundColor="#F2F2F2"
IsVisible="{Binding IsPendingVisible}"
Padding="4">
<Label Text="产品名称" FontAttributes="Bold" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="1" Text="产品编码" FontAttributes="Bold" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="2" Text="规格" FontAttributes="Bold" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="3" Text="出库库位" FontAttributes="Bold" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="4" Text="批次号" FontAttributes="Bold" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="5" Text="出库数量" FontAttributes="Bold" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="6" Text="已扫描数" FontAttributes="Bold" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<!-- 待入库明细表头 -->
<Grid Grid.Row="1" ColumnDefinitions="*,*,*,*,*,*,*" BackgroundColor="#F2F2F2" IsVisible="{Binding IsPendingVisible}" Padding="8">
<Label Text="物料名称" FontAttributes="Bold" />
<Label Grid.Column="1" Text="物料编码" FontAttributes="Bold" />
<Label Grid.Column="2" Text="规格" FontAttributes="Bold" />
<Label Grid.Column="3" Text="出库库位" FontAttributes="Bold" />
<Label Grid.Column="4" Text="批次号" FontAttributes="Bold" />
<Label Grid.Column="5" Text="出库数量" FontAttributes="Bold" />
<Label Grid.Column="6" Text="已扫描数量" FontAttributes="Bold" />
</Grid>
<!-- 出库单明细列表 -->
<!-- 待入库明细列表 -->
<CollectionView Grid.Row="2"
ItemsSource="{Binding PendingList}"
IsVisible="{Binding IsPendingVisible}">
IsVisible="{Binding IsPendingVisible}"
SelectionMode="Single">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="2*,2*,1.5*,1.5*,1.5*,1.5*,1.5*" Padding="4" BackgroundColor="White">
<Label Text="{Binding Name}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="1" Text="{Binding Code}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="2" Text="{Binding Spec}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="3" Text="{Binding Bin}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="4" Text="{Binding BatchNo}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="5" Text="{Binding OutQty}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="6" Text="{Binding ScannedQty}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Grid ColumnDefinitions="*,*,*,*,*,*,*" Padding="8" BackgroundColor="White">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="White"/>
</VisualState.Setters>
</VisualState>
<VisualState Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="#CCFFCC"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Label Text="{Binding Name}" />
<Label Grid.Column="1" Text="{Binding MaterialCode}" />
<Label Grid.Column="2" Text="{Binding Spec}" />
<Label Grid.Column="3" Text="{Binding Location}" />
<Label Grid.Column="4" Text="{Binding StockBatch}" />
<Label Grid.Column="5" Text="{Binding OutstockQty}" />
<Label Grid.Column="6" Text="{Binding Qty}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!-- 扫描明细表头 -->
<Grid Grid.Row="1"
ColumnDefinitions="40,*,*,*"
BackgroundColor="#F2F2F2"
IsVisible="{Binding IsScannedVisible}"
Padding="4">
<Label Text="选择" FontAttributes="Bold" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="1" Text="条码" FontAttributes="Bold" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="2" Text="物料名称" FontAttributes="Bold" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="3" Text="数量" FontAttributes="Bold" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Grid Grid.Row="1" ColumnDefinitions="40,*,*,*" BackgroundColor="#F2F2F2" IsVisible="{Binding IsScannedVisible}" Padding="8">
<Label Text="选择" FontAttributes="Bold" />
<Label Grid.Column="1" Text="物料名称" FontAttributes="Bold" />
<Label Grid.Column="2" Text="条码" FontAttributes="Bold" />
<Label Grid.Column="3" Text="数量" FontAttributes="Bold" />
</Grid>
<!-- 扫描明细列表 -->
<CollectionView Grid.Row="2"
ItemsSource="{Binding ScannedList}"
IsVisible="{Binding IsScannedVisible}">
ItemsSource="{Binding ScannedList}"
IsVisible="{Binding IsScannedVisible}"
SelectionMode="Single"
SelectedItem="{Binding SelectedScanItem, Mode=TwoWay}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="40,*,*,*" Padding="4" BackgroundColor="White">
<CheckBox IsChecked="{Binding IsSelected}" HorizontalOptions="Center" VerticalOptions="Center"/>
<Label Grid.Column="1" Text="{Binding Barcode}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="2" Text="{Binding Name}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Label Grid.Column="3" Text="{Binding Qty}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
<Grid ColumnDefinitions="40,*,*,*" Padding="8" BackgroundColor="White">
<Grid.Triggers>
<!-- ScanStatus = true → 绿色 -->
<DataTrigger TargetType="Grid" Binding="{Binding ScanStatus}" Value="True">
<Setter Property="BackgroundColor" Value="#E6FFE6" />
</DataTrigger>
</Grid.Triggers>
<CheckBox IsChecked="{Binding IsSelected}" />
<Label Grid.Column="1" Text="{Binding Name}" />
<Label Grid.Column="2" Text="{Binding Barcode}" />
<Entry Grid.Column="3"
Keyboard="Numeric"
HorizontalTextAlignment="Center"
WidthRequest="64"
Completed="OnQtyCompleted"
Text="{Binding Qty, Mode=TwoWay, Converter={StaticResource IntConverter}}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
<!-- 底部按钮 -->
<Grid Grid.Row="4" ColumnDefinitions="*,*,*" Padding="16,8" ColumnSpacing="10">
<!-- 扫描通过 -->
... ... @@ -177,20 +215,20 @@
</StackLayout>
</Grid>
<!-- 确认库 -->
<!-- 确认库 -->
<Grid Grid.Column="2"
BackgroundColor="#2196F3"
HorizontalOptions="Fill"
VerticalOptions="Fill"
HeightRequest="50">
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding ConfirmCommand}" />
<TapGestureRecognizer Tapped="OnConfirmClicked" />
</Grid.GestureRecognizers>
<StackLayout Orientation="Horizontal"
HorizontalOptions="Center"
VerticalOptions="Center">
<Image Source="confirm.png" HeightRequest="20" WidthRequest="20" />
<Label Text="确认库"
<Label Text="确认库"
Margin="5,0,0,0"
VerticalOptions="Center"
TextColor="White" />
... ... @@ -200,4 +238,4 @@
</Grid>
</Grid>
</ContentPage>
\ No newline at end of file
</ContentPage>
... ...
using IndustrialControl.Services;
using IndustrialControl.ViewModels;
namespace IndustrialControl.Pages
namespace IndustrialControl.Pages;
[QueryProperty(nameof(OutstockId), "outstockId")]
[QueryProperty(nameof(OutstockNo), "outstockNo")]
[QueryProperty(nameof(OrderType), "orderType")]
[QueryProperty(nameof(OrderTypeName), "orderTypeName")]
[QueryProperty(nameof(RequisitionMaterialNo), "requisitionMaterialNo")]
[QueryProperty(nameof(ReturnNo), "returnNo")]
[QueryProperty(nameof(DeliveryNo), "deliveryNo")]
[QueryProperty(nameof(CreatedTime), "createdTime")]
public partial class OutboundMaterialPage : ContentPage
{
[QueryProperty(nameof(OutstockId), "outstockId")]
[QueryProperty(nameof(OutstockNo), "outstockNo")]
[QueryProperty(nameof(OrderType), "orderType")]
[QueryProperty(nameof(OrderTypeName), "orderTypeName")]
[QueryProperty(nameof(PurchaseNo), "purchaseNo")]
[QueryProperty(nameof(SupplierName), "supplierName")]
[QueryProperty(nameof(CreatedTime), "createdTime")]
public partial class OutboundMaterialPage : ContentPage
private readonly ScanService _scanSvc;
private readonly OutboundMaterialViewModel _vm;
public string? OutstockId { get; set; }
public string? OutstockNo { get; set; }
public string? OrderType { get; set; }
public string? OrderTypeName { get; set; }
public string? RequisitionMaterialNo { get; set; }
public string? ReturnNo { get; set; }
public string? DeliveryNo { get; set; }
public string? CreatedTime { get; set; }
private readonly IDialogService _dialogs;
public OutboundMaterialPage(OutboundMaterialViewModel vm, ScanService scanSvc, IDialogService dialogs)
{
private readonly ScanService _scanSvc;
private readonly OutboundMaterialViewModel _vm;
public string? OutstockId { get; set; }
public string? OutstockNo { get; set; }
public string? OrderType { get; set; }
public string? OrderTypeName { get; set; }
public string? PurchaseNo { get; set; }
public string? SupplierName { get; set; }
public string? CreatedTime { get; set; }
public OutboundMaterialPage(OutboundMaterialViewModel vm, ScanService scanSvc)
{
InitializeComponent();
BindingContext = vm;
_scanSvc = scanSvc;
_vm = vm;
_scanSvc.Prefix = null; // 例如 "}q" 之类的前缀;没有就留 null
// _scanSvc.Suffix = "\n"; // 如果设备会附带换行,可去掉;没有就设 null
//_scanSvc.DebounceMs = 250;
_scanSvc.Suffix = null; // 先关掉
_scanSvc.DebounceMs = 0; // 先关掉
}
InitializeComponent();
BindingContext = vm;
_scanSvc = scanSvc;
_vm = vm;
_dialogs = dialogs;
// 可选:配置前后缀与防抖
_scanSvc.Prefix = null; // 例如 "}q" 之类的前缀;没有就留 null
// _scanSvc.Suffix = "\n"; // 如果设备会附带换行,可去掉;没有就设 null
//_scanSvc.DebounceMs = 250;
_scanSvc.Suffix = null; // 先关掉
_scanSvc.DebounceMs = 0; // 先关掉
}
protected override async void OnAppearing()
{
base.OnAppearing();
protected override async void OnAppearing()
// ✅ 用搜索页带过来的基础信息初始化页面,并拉取两张表
if (!string.IsNullOrWhiteSpace(OutstockId))
{
base.OnAppearing();
// ✅ 用搜索页带过来的基础信息初始化页面,并拉取两张表
if (!string.IsNullOrWhiteSpace(OutstockId))
{
await _vm.InitializeFromSearchAsync(
outstockId: OutstockId ?? "",
outstockNo: OutstockNo ?? "",
orderType: OrderType ?? "",
orderTypeName: OrderTypeName ?? "",
purchaseNo: PurchaseNo ?? "",
supplierName: SupplierName ?? "",
createdTime: CreatedTime ?? ""
);
}
_scanSvc.Scanned += OnScanned;
_scanSvc.StartListening();
_scanSvc.Attach(ScanEntry);
ScanEntry.Focus();
await _vm.InitializeFromSearchAsync(
outstockId: OutstockId ?? "",
outstockNo: OutstockNo ?? "",
orderType: OrderType ?? "",
orderTypeName: OrderTypeName ?? "",
requisitionMaterialNo: RequisitionMaterialNo ?? "",
returnNo: ReturnNo ?? "",
deliveryNo: DeliveryNo ?? "",
createdTime: CreatedTime ?? ""
);
}
protected override void OnDisappearing()
_scanSvc.Scanned += OnScanned;
_scanSvc.StartListening();
_scanSvc.Attach(ScanEntry);
ScanEntry.Focus();
}
/// <summary>
/// 清空扫描记录
/// </summary>
void OnClearClicked(object sender, EventArgs e)
{
_vm.ClearScan();
ScanEntry.Text = string.Empty;
ScanEntry.Focus();
}
protected override void OnDisappearing()
{
// 退出页面即注销(防止多个程序/页面抢处理)
_scanSvc.Scanned -= OnScanned;
_scanSvc.StopListening();
base.OnDisappearing();
}
private void OnScanned(string data, string type)
{
MainThread.BeginInvokeOnMainThread(async () =>
{
// 退出页面即注销(防止多个程序/页面抢处理)
_scanSvc.Scanned -= OnScanned;
_scanSvc.StopListening();
// 常见处理:自动填入单号/条码并触发查询或加入明细
_vm.ScanCode = data;
// 你原本的逻辑:若识别到是订单号 → 查询;若是包装码 → 加入列表等
await _vm.HandleScannedAsync(data, type);
});
}
base.OnDisappearing();
}
private void OnScanned(string data, string type)
/// <summary>
/// 确认入库按钮点击
/// </summary>
async void OnConfirmClicked(object sender, EventArgs e)
{
var ok = await _vm.ConfirmOutboundAsync();
if (ok)
{
MainThread.BeginInvokeOnMainThread(async () =>
{
// 常见处理:自动填入单号/条码并触发查询或加入明细
_vm.ScanCode = data;
// 你原本的逻辑:若识别到是订单号 → 查询;若是包装码 → 加入列表等
await _vm.HandleScannedAsync(data, type);
});
await DisplayAlert("提示", "入库成功", "确定");
_vm.ClearAll();
// ✅ 返回到工单查询页面(InboundMaterialSearchPage)
await Shell.Current.GoToAsync($"//{nameof(OutboundMaterialSearchPage)}");
}
// Tab切换
void OnPendingTabClicked(object sender, EventArgs e)
else
{
_vm.IsPendingVisible = true;
_vm.IsScannedVisible = false;
await DisplayAlert("提示", "入库失败,请检查数据", "确定");
}
}
void OnScannedTabClicked(object sender, EventArgs e)
{
_vm.IsPendingVisible = false;
_vm.IsScannedVisible = true;
}
// 清空扫描记录
void OnClearClicked(object sender, EventArgs e)
{
_vm.ClearScan();
ScanEntry.Text = string.Empty;
ScanEntry.Focus();
}
private async void OnBinTapped(object? sender, TappedEventArgs e)
{
var bindable = sender as BindableObject;
var row = bindable?.BindingContext;
if (row == null) return;
// 摄像头扫码按钮
async void OnScanClicked(object sender, EventArgs e)
{
await DisplayAlert("提示", "此按钮预留摄像头扫码;硬件扫描直接扣扳机。", "确定");
}
var type = row.GetType();
var currentBin = type.GetProperty("Location")?.GetValue(row)?.ToString();
// 1) 打开公共组件选择库位(你已完成的组件)
var picked = await BinPickerPage.ShowAsync(currentBin);
if (picked == null) return;
// 2) 调用 VM:带上行对象 + 选中的 BinInfo,内部会调接口 & 回填行
await _vm.UpdateRowLocationAsync(row, picked);
}
private async void OnQtyCompleted(object sender, EventArgs e)
{
if (sender is not Entry entry) return;
if (entry.BindingContext is not IndustrialControl.ViewModels.OutScannedItem row) return;
// 只看 ScanStatus:未通过则不提交
if (!row.ScanStatus)
{
await DisplayAlert("提示", "该行尚未扫描通过,不能修改数量。", "确定");
return;
}
await _vm.UpdateQuantityForRowAsync(row);
}
}
... ...
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage x:Name="Page"
<ContentPage
x:Class="IndustrialControl.Pages.OutboundMaterialSearchPage"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="IndustrialControl.Pages.OutboundMaterialSearchPage"
xmlns:conv="clr-namespace:IndustrialControl.Converters"
Title="仓储管理系统">
<ContentPage.Resources>
<ResourceDictionary>
<!-- 空/非空转布尔:非空 => true(按钮可用) -->
<conv:NullToBoolConverter x:Key="NullToBoolConverter" />
</ResourceDictionary>
</ContentPage.Resources>
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Name="Page"
Shell.NavBarIsVisible="True">
<Grid RowDefinitions="Auto,*,Auto" Padding="16" BackgroundColor="#F6F7FB">
<ScrollView>
<VerticalStackLayout Spacing="12" Padding="12">
<!-- 顶部:输入条件(第0行) -->
<VerticalStackLayout Grid.Row="0" Spacing="10">
<!-- 查询区域 -->
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto" ColumnSpacing="8" RowSpacing="8">
<Entry x:Name="OrderEntry"
Grid.Row="0" Grid.Column="0"
Placeholder="请输入出库单条码"
VerticalOptions="Center"
BackgroundColor="White"
Placeholder="请扫描输入出库单条码"
Text="{Binding SearchOrderNo}" />
<!-- 开始日期 -->
<DatePicker Grid.Row="1" Grid.Column="0"
Date="{Binding StartDate}"
MinimumDate="2000-01-01"
MaximumDate="{Binding EndDate}" />
<!-- 结束日期 -->
<DatePicker Grid.Row="1" Grid.Column="1"
Date="{Binding EndDate}"
MinimumDate="{Binding StartDate}" />
<Button Grid.Row="1" Grid.Column="1"
<Button Grid.Row="0" Grid.Column="1"
Text="查询"
Command="{Binding SearchCommand}" />
<Grid Grid.Row="1" Grid.ColumnSpan="2" ColumnDefinitions="Auto,*,Auto,*" ColumnSpacing="8">
<Label Grid.Column="0" Text="开始:" VerticalTextAlignment="Center"/>
<DatePicker Grid.Column="1" Date="{Binding StartDate}" />
<Label Grid.Column="2" Text="结束:" VerticalTextAlignment="Center"/>
<DatePicker Grid.Column="3" Date="{Binding EndDate}" />
</Grid>
</Grid>
</VerticalStackLayout>
<!-- 中部:结果列表(第1行) -->
<CollectionView Grid.Row="1"
ItemsSource="{Binding Orders}"
SelectionMode="Single"
SelectedItem="{Binding SelectedOrder, Mode=TwoWay}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Frame Margin="0,8,0,0" Padding="12" HasShadow="True" CornerRadius="10">
<!-- ⭐ 点击整卡片触发命令 -->
<Frame.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding BindingContext.GoOutboundCommand, Source={x:Reference Page}}"
CommandParameter="{Binding .}" />
</Frame.GestureRecognizers>
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="Auto,*" ColumnSpacing="8">
<Label Grid.Row="0" Grid.Column="0" Text="出库单号:" FontAttributes="Bold"/>
<Label Grid.Row="0" Grid.Column="1" Text="{Binding outstockNo}"/>
<!-- 列表区域 -->
<CollectionView Grid.Row="1"
ItemsSource="{Binding Orders}"
SelectionMode="Single"
SelectedItem="{Binding SelectedOrder, Mode=TwoWay}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Frame Margin="0,8,0,0" Padding="12" HasShadow="True" CornerRadius="10">
<!-- 点击整卡片:直接调用 VM 的 GoOutboundCommand,并把当前项作为参数 -->
<Frame.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding BindingContext.GoOutboundCommand, Source={x:Reference Page}}"
CommandParameter="{Binding .}" />
</Frame.GestureRecognizers>
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto"
ColumnDefinitions="Auto,*" ColumnSpacing="8">
<Label Grid.Row="0" Grid.Column="0" Text="出库单号:" FontAttributes="Bold"/>
<Label Grid.Row="0" Grid.Column="1" Text="{Binding outstockNo}"/>
<Label Grid.Row="1" Grid.Column="0" Text="出库单类型:" FontAttributes="Bold"/>
<Label Grid.Row="1" Grid.Column="1" Text="{Binding orderTypeName}" />
<Label Grid.Row="1" Grid.Column="0" Text="出库单类型:" FontAttributes="Bold"/>
<Label Grid.Row="1" Grid.Column="1" Text="{Binding orderTypeName}" />
<Label Grid.Row="2" Grid.Column="0" Text="关联领料单号:" FontAttributes="Bold"/>
<Label Grid.Row="2" Grid.Column="1" Text="{Binding requisitionMaterialNo}" />
<Label Grid.Row="2" Grid.Column="0" Text="关联领料单号:" FontAttributes="Bold"/>
<Label Grid.Row="2" Grid.Column="1" Text="{Binding requisitionMaterialNo}" />
<Label Grid.Row="3" Grid.Column="0" Text="关联工单号:" FontAttributes="Bold"/>
<Label Grid.Row="3" Grid.Column="1" Text="{Binding workOrderNo}" />
<Label Grid.Row="3" Grid.Column="0" Text="关联工单号:" FontAttributes="Bold"/>
<Label Grid.Row="3" Grid.Column="1" Text="{Binding workOrderNo}" />
<Label Grid.Row="4" Grid.Column="0" Text="创建日期:" FontAttributes="Bold"/>
<Label Grid.Row="4" Grid.Column="1" Text="{Binding createdTime}" />
</Grid>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<Label Grid.Row="4" Grid.Column="0" Text="创建日期:" FontAttributes="Bold"/>
<Label Grid.Row="4" Grid.Column="1" Text="{Binding createdTime}" />
</Grid>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!-- 底部:操作(第2行) -->
<Grid Grid.Row="2" ColumnDefinitions="*,Auto" Padding="0,8,0,0">
<Label Text="{Binding Orders.Count, StringFormat='共 {0} 条'}"
VerticalTextAlignment="Center" />
</Grid>
</Grid>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
... ...
using System.Threading;
using IndustrialControl.Services;
using IndustrialControl.ViewModels;
namespace IndustrialControl.Pages;
... ... @@ -10,7 +9,7 @@ public partial class OutboundMaterialSearchPage : ContentPage
public OutboundMaterialSearchPage(OutboundMaterialSearchViewModel vm, ScanService scanSvc)
{
_vm = vm;
BindingContext = vm;
_scanSvc = scanSvc;
InitializeComponent();
... ... @@ -30,6 +29,8 @@ public partial class OutboundMaterialSearchPage : ContentPage
//键盘输入
_scanSvc.Attach(OrderEntry);
OrderEntry.Focus();
}
/// <summary>
... ... @@ -59,4 +60,5 @@ public partial class OutboundMaterialSearchPage : ContentPage
});
}
}
... ...
using IndustrialControl.Models;
using IndustrialControl.ViewModels;
namespace IndustrialControl.Services
namespace IndustrialControl.Services;
public interface IOutboundMaterialService
{
public interface IOutboundMaterialService
{
// NEW: 查询列表(图1)
Task<IEnumerable<OutboundOrderSummary>> ListOutboundOrdersAsync(
string? orderNoOrBarcode,
DateTime startDate,
DateTime endDate,
string orderType,
string[] orderTypeList,
CancellationToken ct = default);
Task<IReadOnlyList<OutboundPendingRow>> GetOutStockDetailAsync(string outstockId, CancellationToken ct = default);
Task<IReadOnlyList<OutboundScannedRow>> GetOutStockScanDetailAsync(string outstockId, CancellationToken ct = default);
/// <summary>扫描条码入库</summary>
Task<SimpleOk> OutStockByBarcodeAsync(string outstockId, string barcode, CancellationToken ct = default);
/// <summary>PDA 扫描通过(确认当前入库单已扫描项)</summary>
Task<SimpleOk> ScanConfirmAsync(string outstockId, CancellationToken ct = default);
Task<SimpleOk> CancelScanAsync(string outstockId, CancellationToken ct = default);
Task<SimpleOk> ConfirmOutstockAsync(string outstockId, CancellationToken ct = default);
/// <summary>判断入库单明细是否全部扫码确认</summary>
Task<bool> JudgeOutstockDetailScanAllAsync(string outstockId, CancellationToken ct = default);
}
// NEW: 查询列表(图1)
Task<IEnumerable<OutboundOrderSummary>> ListOutboundOrdersAsync(
string? orderNoOrBarcode,
DateTime startDate,
DateTime endDate,
string orderType,
string[] orderTypeList,
CancellationToken ct = default);
Task<IReadOnlyList<OutboundPendingRow>> GetOutStockDetailAsync(string outstockId, CancellationToken ct = default);
Task<IReadOnlyList<OutboundScannedRow>> GetOutStockScanDetailAsync(string outstockId, CancellationToken ct = default);
/// <summary>扫描条码入库</summary>
Task<SimpleOk> OutStockByBarcodeAsync(string outstockId, string barcode, CancellationToken ct = default);
/// <summary>PDA 扫描通过(确认当前入库单已扫描项)</summary>
// IOutboundMaterialService.cs
Task<SimpleOk> ScanConfirmAsync(IEnumerable<(string barcode, string id)> items, CancellationToken ct = default);
Task<SimpleOk> CancelScanAsync(IEnumerable<(string barcode, string id)> items, CancellationToken ct = default);
Task<SimpleOk> ConfirmOutstockAsync(string outstockId, CancellationToken ct = default);
/// <summary>判断入库单明细是否全部扫码确认</summary>
Task<bool> JudgeOutstockDetailScanAllAsync(string outstockId, CancellationToken ct = default);
Task<SimpleOk> UpdateOutstockLocationAsync(
string detailId,
string id,
string outstockWarehouse,
string outstockWarehouseCode,
string location,
CancellationToken ct = default);
Task<SimpleOk> UpdateQuantityAsync(
string barcode, string detailId, string id, int quantity, CancellationToken ct = default);
}
... ...
... ... @@ -155,11 +155,13 @@ public sealed class InboundMaterialService : IInboundMaterialService
purchaseNo: x.purchaseNo ?? "",
arrivalNo: x.arrivalNo ?? "",
supplierName: x.supplierName ?? "",
workOrderNo: x.workOrderNo ?? "",
materialName: x.materialName ?? "",
instockQty: ToInt(x.instockQty),
createdTime: x.createdTime ?? ""
));
}
public async Task<IReadOnlyList<InboundPendingRow>> GetInStockDetailAsync(
string instockId, CancellationToken ct = default)
{
... ... @@ -281,7 +283,7 @@ public sealed class InboundMaterialService : IInboundMaterialService
public async Task<SimpleOk> ConfirmInstockAsync(string instockId, CancellationToken ct = default)
{
var bodyJson = JsonSerializer.Serialize(new { instockId });
var bodyJson = JsonSerializer.Serialize(new { id = instockId });
using var req = new HttpRequestMessage(HttpMethod.Post, _confirmInstockEndpoint)
{
Content = new StringContent(bodyJson, Encoding.UTF8, "application/json")
... ... @@ -577,6 +579,9 @@ public class GetInStockRecord
public string? supplierName { get; set; }
public string? arrivalNo { get; set; }
public string? purchaseNo { get; set; }
public string? workOrderNo { get; set; }
public string? materialName { get; set; }
public decimal? instockQty { get; set; }
public string? createdTime { get; set; }
}
public sealed class GetInStockScanDetailResp
... ...
// 文件:Services/WarehouseDataApiService.cs
using IndustrialControl.Models;
using IndustrialControl.ViewModels;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using IndustrialControl.Models;
using IndustrialControl.ViewModels;
using System.Text.Json.Serialization;
namespace IndustrialControl.Services;
... ... @@ -13,23 +13,25 @@ namespace IndustrialControl.Services;
/// </summary>
public sealed class OutboundMaterialService : IOutboundMaterialService
{
private readonly HttpClient _http;
private readonly string _outboundListEndpoint;
private readonly string _detailEndpoint;
private readonly string _scanDetailEndpoint;
public readonly HttpClient _http;
public readonly string _outboundListEndpoint;
public readonly string _detailEndpoint;
public readonly string _scanDetailEndpoint;
// 新增:扫码入库端点
private readonly string _scanByBarcodeEndpoint;
private readonly string _scanConfirmEndpoint;
private readonly string _cancelScanEndpoint;
private readonly string _confirmOutstockEndpoint;
private readonly string _judgeScanAllEndpoint;
public readonly string _scanByBarcodeEndpoint;
public readonly string _scanConfirmEndpoint;
public readonly string _cancelScanEndpoint;
public readonly string _confirmOutstockEndpoint;
public readonly string _judgeScanAllEndpoint;
private readonly JsonSerializerOptions _opt;
private readonly JsonSerializerOptions _json = new() { PropertyNameCaseInsensitive = true };
public OutboundMaterialService(HttpClient http, IConfigLoader configLoader)
{
_http = http;
// 和 WorkOrderApi 一样从 appconfig.json 读取端点,留兼容键名 + 兜底硬编码
_opt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
JsonNode cfg = configLoader.Load();
// ⭐ 新增:读取 baseUrl 或 ip+port
var baseUrl =
(string?)cfg?["server"]?["baseUrl"]
... ... @@ -40,32 +42,43 @@ public sealed class OutboundMaterialService : IOutboundMaterialService
if (_http.BaseAddress is null)
_http.BaseAddress = new Uri(baseUrl, UriKind.Absolute);
// 下面保持原来的相对路径读取(不变)
_outboundListEndpoint =
(string?)cfg?["apiEndpoints"]?["outbound"]?["list"] ??
(string?)cfg?["apiEndpoints"]?["getOutStock"] ??
"/normalService/pda/wmsMaterialOutstock/getOutStock";
_detailEndpoint = (string?)cfg?["apiEndpoints"]?["outbound"]?["detail"]
?? "/normalService/pda/wmsMaterialOutstock/getOutStockDetail";
_scanDetailEndpoint = (string?)cfg?["apiEndpoints"]?["outbound"]?["scanDetail"]
?? "/normalService/pda/wmsMaterialOutstock/getOutStockScanDetail";
_detailEndpoint =
(string?)cfg?["apiEndpoints"]?["outbound"]?["detail"] ??
"/normalService/pda/wmsMaterialOutstock/getOutStockDetail";
_scanDetailEndpoint =
(string?)cfg?["apiEndpoints"]?["outbound"]?["scanDetail"] ??
"/normalService/pda/wmsMaterialOutstock/getOutStockScanDetail";
_scanByBarcodeEndpoint =
(string?)cfg?["apiEndpoints"]?["outbound"]?["scanByBarcode"]
?? "/normalService/pda/wmsMaterialOutstock/getOutStockByBarcode";
(string?)cfg?["apiEndpoints"]?["outbound"]?["scanByBarcode"] ??
"/normalService/pda/wmsMaterialOutstock/getOutStockByBarcode";
_scanConfirmEndpoint =
(string?)cfg?["apiEndpoints"]?["outbound"]?["scanConfirm"]
?? "/normalService/pda/wmsMaterialOutstock/scanConfirm";
(string?)cfg?["apiEndpoints"]?["outbound"]?["scanConfirm"] ??
"/normalService/pda/wmsMaterialOutstock/scanOutConfirm";
_cancelScanEndpoint =
(string?)cfg?["apiEndpoints"]?["outbound"]?["cancelScan"]
?? "/normalService/pda/wmsMaterialOutstock/cancelScan";
(string?)cfg?["apiEndpoints"]?["outbound"]?["cancelScan"] ??
"/normalService/pda/wmsMaterialOutstock/cancelOutScan";
_confirmOutstockEndpoint =
(string?)cfg?["apiEndpoints"]?["outbound"]?["confirm"]
?? "/normalService/pda/wmsMaterialOutstock/confirm";
(string?)cfg?["apiEndpoints"]?["outbound"]?["confirm"] ??
"/normalService/pda/wmsMaterialOutstock/confirm";
_judgeScanAllEndpoint =
(string?)cfg?["apiEndpoints"]?["outbound"]?["judgeScanAll"]
?? "/normalService/pda/wmsMaterialOutstock/judgeOutstockDetailScanAll";
(string?)cfg?["apiEndpoints"]?["outbound"]?["judgeScanAll"] ??
"/normalService/pda/wmsMaterialOutstock/judgeOutstockDetailScanAll";
}
// ====== 你当前页面会调用的方法 ======
// ⭐ 新增:拼接 ip + port → baseUrl
private static string? BuildBaseUrl(JsonNode? ipNode, JsonNode? portNode)
{
string? ip = ipNode?.ToString().Trim();
... ... @@ -134,25 +147,21 @@ public sealed class OutboundMaterialService : IOutboundMaterialService
return records.Select(x => new OutboundOrderSummary(
outstockId: x.id ?? "",
outstockNo: x.outstockNo ?? "",
orderType: x.orderType ?? "",
orderTypeName: x.orderTypeName ?? "",
purchaseNo: x.purchaseNo ?? "",
supplierName: x.supplierName ?? "",
arrivalNo: x.arrivalNo ?? "",
createdTime: x.createdTime ?? "",
deliveryNo: x.deliveryNo ?? "",
requisitionMaterialNo:x.requisitionMaterialNo ?? "",
returnNo: x.returnNo ?? "",
workOrderNo:x.workOrderNo ?? ""
workOrderNo: x.workOrderNo ?? "",
requisitionMaterialNo: x.requisitionMaterialNo ?? "",
returnNo:x.returnNo ?? "",
deliveryNo:x.deliveryNo ?? "",
createdTime: x.createdTime ?? ""
));
}
public async Task<IReadOnlyList<OutboundPendingRow>> GetOutStockDetailAsync(
string outstockId, CancellationToken ct = default)
{
// ✅ 文档为 GET + x-www-form-urlencoded,参数名是小写 outstockId
var url = $"{_detailEndpoint}?outstockId={Uri.EscapeDataString(outstockId)}";
using var req = new HttpRequestMessage(HttpMethod.Get, url);
... ... @@ -166,27 +175,22 @@ public sealed class OutboundMaterialService : IOutboundMaterialService
if (dto?.success != true || dto.result is null || dto.result.Count == 0)
return Array.Empty<OutboundPendingRow>();
static int ToIntSafe(string? s)
{
if (string.IsNullOrWhiteSpace(s)) return 0;
s = s.Trim().Replace(",", "");
return int.TryParse(s, out var v) ? v : 0;
}
// ⚠️ 接口没有 barcode,这里先用空串;如需展示可以改成 x.materialCode 或 x.stockBatch
var list = dto.result.Select(x => new OutboundPendingRow(
Barcode: string.Empty, // 或 $"{x.materialCode}" / $"{x.stockBatch}"
DetailId: x.id ?? string.Empty, // ← 改为接口的 id
Location: x.location ?? string.Empty,
MaterialName: x.materialName ?? string.Empty,
PendingQty: ToIntSafe(x.outstockQty), // ← 预计数量
ScannedQty: ToIntSafe(x.qty), // ← 已扫描量
Spec: x.spec ?? string.Empty
MaterialCode: x.materialCode ?? string.Empty,
Spec: x.spec ?? string.Empty,
Location: x.location ?? string.Empty,
ProductionBatch: x.productionBatch ?? string.Empty,
StockBatch: x.stockBatch ?? string.Empty,
OutstockQty: ToInt(x.outstockQty), // 此处再转 int
Qty: ToInt(x.qty) // ← 已扫描量
)).ToList();
return list;
}
static int ToInt(decimal? v) => v.HasValue ? (int)Math.Round(v.Value, MidpointRounding.AwayFromZero) : 0;
public async Task<IReadOnlyList<OutboundScannedRow>> GetOutStockScanDetailAsync(
string outstockId,
CancellationToken ct = default)
... ... @@ -206,21 +210,13 @@ public sealed class OutboundMaterialService : IOutboundMaterialService
if (dto?.success != true || dto.result is null || dto.result.Count == 0)
return Array.Empty<OutboundScannedRow>();
static int ToIntSafe(string? s)
{
if (string.IsNullOrWhiteSpace(s)) return 0;
// 去除千分位、空格
s = s.Trim().Replace(",", "");
return int.TryParse(s, out var v) ? v : 0;
}
// 映射:OutstockId <- id(截图注释“入库单明细主键id”)
var list = dto.result.Select(x => new OutboundScannedRow(
Barcode: (x.barcode ?? string.Empty).Trim(),
DetailId: (x.id ?? string.Empty).Trim(),
Location: (x.location ?? string.Empty).Trim(),
MaterialName: (x.materialName ?? string.Empty).Trim(),
Qty: ToIntSafe(x.qty),
Qty: ToInt(x.qty),
Spec: (x.spec ?? string.Empty).Trim(),
ScanStatus: x.scanStatus ?? false,
WarehouseCode: x.warehouseCode?.Trim()
... ... @@ -228,10 +224,13 @@ public sealed class OutboundMaterialService : IOutboundMaterialService
return list;
}
// ========= 扫码入库实现 =========
public async Task<SimpleOk> OutStockByBarcodeAsync(string outstockId, string barcode, CancellationToken ct = default)
{
var body = JsonSerializer.Serialize(new { barcode, outstockId });
// 注意:接口要的是 id 不是 outstockId
var body = JsonSerializer.Serialize(new { barcode, id = outstockId });
using var req = new HttpRequestMessage(HttpMethod.Post, _scanByBarcodeEndpoint)
{
Content = new StringContent(body, Encoding.UTF8, "application/json")
... ... @@ -239,16 +238,16 @@ public sealed class OutboundMaterialService : IOutboundMaterialService
using var res = await _http.SendAsync(req, ct);
var json = await res.Content.ReadAsStringAsync(ct);
var opt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var dto = JsonSerializer.Deserialize<ScanByBarcodeResp>(json, opt);
// 按文档:以 success 判断;message 作为失败提示
var ok = dto?.success == true;
var dto = JsonSerializer.Deserialize<ScanByBarcodeResp>(json, _opt);
var ok = dto?.success == true || dto?.result?.ToString() == "true";
return new SimpleOk(ok, dto?.message);
}
public async Task<SimpleOk> ScanConfirmAsync(string outstockId, CancellationToken ct = default)
public async Task<SimpleOk> ScanConfirmAsync(IEnumerable<(string barcode, string id)> items, CancellationToken ct = default)
{
var bodyJson = JsonSerializer.Serialize(new { outstockId });
var payload = items.Select(x => new { barcode = x.barcode, id = x.id });
var bodyJson = JsonSerializer.Serialize(payload);
using var req = new HttpRequestMessage(HttpMethod.Post, _scanConfirmEndpoint)
{
Content = new StringContent(bodyJson, Encoding.UTF8, "application/json")
... ... @@ -256,17 +255,16 @@ public sealed class OutboundMaterialService : IOutboundMaterialService
using var res = await _http.SendAsync(req, ct);
var json = await res.Content.ReadAsStringAsync(ct);
var dto = JsonSerializer.Deserialize<ScanConfirmResp>(json, _opt);
var opt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var dto = JsonSerializer.Deserialize<ScanConfirmResp>(json, opt);
var ok = dto?.success == true;
var ok = dto?.success == true; // 你的接口:success=true 且 result=true
return new SimpleOk(ok, dto?.message);
}
public async Task<SimpleOk> CancelScanAsync(string outstockId, CancellationToken ct = default)
public async Task<SimpleOk> CancelScanAsync(IEnumerable<(string barcode, string id)> items, CancellationToken ct = default)
{
var bodyJson = JsonSerializer.Serialize(new { outstockId });
var payload = items.Select(x => new { barcode = x.barcode, id = x.id });
var bodyJson = JsonSerializer.Serialize(payload);
using var req = new HttpRequestMessage(HttpMethod.Post, _cancelScanEndpoint)
{
Content = new StringContent(bodyJson, Encoding.UTF8, "application/json")
... ... @@ -274,17 +272,16 @@ public sealed class OutboundMaterialService : IOutboundMaterialService
using var res = await _http.SendAsync(req, ct);
var json = await res.Content.ReadAsStringAsync(ct);
var opt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var dto = JsonSerializer.Deserialize<CancelScanResp>(json, opt);
var dto = JsonSerializer.Deserialize<CancelScanResp>(json, _opt);
var ok = dto?.success == true;
return new SimpleOk(ok, dto?.message);
}
public async Task<SimpleOk> ConfirmOutstockAsync(string outstockId, CancellationToken ct = default)
{
var bodyJson = JsonSerializer.Serialize(new { outstockId });
var bodyJson = JsonSerializer.Serialize(new { id = outstockId });
using var req = new HttpRequestMessage(HttpMethod.Post, _confirmOutstockEndpoint)
{
Content = new StringContent(bodyJson, Encoding.UTF8, "application/json")
... ... @@ -299,6 +296,12 @@ public sealed class OutboundMaterialService : IOutboundMaterialService
var ok = dto?.success == true;
return new SimpleOk(ok, dto?.message);
}
/// <summary>
/// 判断入库单明细是否已全部扫描确认
/// </summary>
/// <param name="outstockId"></param>
/// <param name="ct"></param>
/// <returns></returns>
public async Task<bool> JudgeOutstockDetailScanAllAsync(string outstockId, CancellationToken ct = default)
{
var url = $"{_judgeScanAllEndpoint}?id={Uri.EscapeDataString(outstockId)}";
... ... @@ -313,116 +316,147 @@ public sealed class OutboundMaterialService : IOutboundMaterialService
return dto?.result == true;
}
}
public class GetOutStockReq
{
public string? createdTime { get; set; }
public string? endTime { get; set; }
public string? outstockNo { get; set; }
public string? orderType { get; set; }
public string? startTime { get; set; }
}
public class GetOutStockResp
{
public int code { get; set; }
public long costTime { get; set; }
public string? message { get; set; }
public bool success { get; set; }
public List<GetOutStockItem>? result { get; set; }
}
public async Task<SimpleOk> UpdateOutstockLocationAsync(
string detailId, string id, string outstockWarehouse, string outstockWarehouseCode, string location, CancellationToken ct = default)
{
var url = "/normalService/pda/wmsMaterialOutstock/updateLocation";
var payload = new
{
detailId,
id,
outstockWarehouse,
outstockWarehouseCode,
location
};
public class GetOutStockItem
{
public string? arrivalNo { get; set; }
public string? createdTime { get; set; }
public string? outstockId { get; set; }
public string? outstockNo { get; set; }
public string? orderType { get; set; }
public string? purchaseNo { get; set; }
public string? supplierName { get; set; }
}
public sealed class GetOutStockDetailResp
{
public bool success { get; set; }
public string? message { get; set; }
public int? code { get; set; }
public List<GetOutStockDetailItem>? result { get; set; }
public int? costTime { get; set; }
}
public sealed class GetOutStockDetailItem
{
public string? id { get; set; } // 入库单明细主键id
public string? outstockNo { get; set; } // 入库单号
public string? materialCode { get; set; }
public string? materialName { get; set; }
public string? spec { get; set; }
public string? stockBatch { get; set; }
public string? outstockQty { get; set; } // 预计数量(字符串/可能为空)
public string? outstockWarehouseCode { get; set; } // 入库仓库编码
public string? location { get; set; } // 内点库位
public string? qty { get; set; } // 已扫描量(字符串/可能为空)
}
var json = JsonSerializer.Serialize(payload);
using var req = new HttpRequestMessage(HttpMethod.Post, url)
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
using var res = await _http.SendAsync(req, ct);
var body = await res.Content.ReadAsStringAsync(ct);
// 假设响应:{ code, message, result: true/false, success: true/false }
var dto = JsonSerializer.Deserialize<UpdateLocationResp>(body, _json);
var ok = dto?.success == true || dto?.result == true;
return new SimpleOk(ok, dto?.message);
}
public async Task<SimpleOk> UpdateQuantityAsync(
string barcode, string detailId, string id, int quantity, CancellationToken ct = default)
{
var url = "/normalService/pda/wmsMaterialOutstock/updateQuantity";
var payload = new { barcode, detailId, id, quantity };
var json = JsonSerializer.Serialize(payload);
using var req = new HttpRequestMessage(HttpMethod.Post, url)
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
using var res = await _http.SendAsync(req, ct);
var body = await res.Content.ReadAsStringAsync(ct);
public class GetOutStockPageResp
{
public int code { get; set; }
public long costTime { get; set; }
public string? message { get; set; }
public bool success { get; set; }
public GetOutStockPageData? result { get; set; }
}
// 响应格式:{ success, message, code, result, ... }(与截图一致)
var dto = JsonSerializer.Deserialize<ConfirmResp>(body, _json);
var ok = dto?.success == true || dto?.result == true;
return new SimpleOk(ok, dto?.message);
}
public class GetOutStockPageData
{
public int pageNo { get; set; }
public int pageSize { get; set; }
public long total { get; set; }
public List<GetOutStockRecord> records { get; set; } = new();
}
public class GetOutStockItem
{
public string? arrivalNo { get; set; }
public string? createdTime { get; set; }
public string? outstockId { get; set; }
public string? outstockNo { get; set; }
public string? orderType { get; set; }
public string? purchaseNo { get; set; }
public string? supplierName { get; set; }
}
public sealed class GetOutStockDetailResp
{
public bool success { get; set; }
public string? message { get; set; }
public int? code { get; set; }
public List<GetOutStockDetailItem>? result { get; set; }
public int? costTime { get; set; }
}
public sealed class GetOutStockDetailItem
{
public string? id { get; set; } // 入库单明细主键id
public string? outstockNo { get; set; } // 入库单号
public string? materialName { get; set; }
public string? outstockWarehouseCode { get; set; } // 入库仓库编码
public string? materialCode { get; set; } //产品编码
public string? spec { get; set; } //规格
public string? location { get; set; } //出库库位
public string? productionBatch { get; set; } //生产批号
public string? stockBatch { get; set; } //批次号
public int outstockQty { get; set; } //出库数量
public int qty { get; set; } //已扫描数
}
public class GetOutStockRecord
{
public string? id { get; set; }
public string? outstockNo { get; set; }
public string? orderType { get; set; }
public string? orderTypeName { get; set; }
public string? supplierName { get; set; }
public string? arrivalNo { get; set; }
public string? purchaseNo { get; set; }
public string? createdTime { get; set; }
public string? deliveryNo { get; set; }
public string? requisitionMaterialNo { get; set; }
public string? returnNo { get; set; }
public string? workOrderNo { get; set; }
}
public sealed class GetOutStockScanDetailResp
{
public bool success { get; set; }
public string? message { get; set; }
public int? code { get; set; }
public List<GetOutStockScanDetailItem>? result { get; set; }
public int? costTime { get; set; }
}
private sealed class UpdateLocationResp
{
public int code { get; set; }
public string? message { get; set; }
public bool? result { get; set; }
public bool? success { get; set; }
}
public sealed class GetOutStockScanDetailItem
{
public string? id { get; set; } // 入库单明细主键 id
public string? barcode { get; set; }
public string? materialName { get; set; }
public string? spec { get; set; }
public string? qty { get; set; } // 可能是 null 或 “数字字符串”
public string? warehouseCode { get; set; }
public string? location { get; set; }
public bool? scanStatus { get; set; } // 可能为 null,按 false 处理
}
public class GetOutStockPageResp
{
public int code { get; set; }
public long costTime { get; set; }
public string? message { get; set; }
public bool success { get; set; }
public GetOutStockPageData? result { get; set; }
}
public class GetOutStockPageData
{
public int pageNo { get; set; }
public int pageSize { get; set; }
public long total { get; set; }
public List<GetOutStockRecord> records { get; set; } = new();
}
public class GetOutStockRecord
{
public string? id { get; set; }
public string? outstockNo { get; set; }
public string? orderType { get; set; }
public string? orderTypeName { get; set; }
public string? workOrderNo { get; set; }
public string? materialName { get; set; }
public string? requisitionMaterialNo { get; set; }
public string? returnNo { get; set; }
public string? deliveryNo { get; set; }
public string? createdTime { get; set; }
}
public sealed class GetOutStockScanDetailResp
{
public bool success { get; set; }
public string? message { get; set; }
public int? code { get; set; }
public List<GetOutStockScanDetailItem>? result { get; set; }
public int? costTime { get; set; }
}
public class GetOutStockScanDetailItem
{
public string? id { get; set; } // 入库单明细主键 id
public string? barcode { get; set; }
public string? materialName { get; set; }
public string? spec { get; set; }
public decimal? qty { get; set; } // 可能是 null 或 “数字字符串”
public string? warehouseCode { get; set; }
public string? location { get; set; }
public bool? scanStatus { get; set; } // 可能为 null,按 false 处理
}
}
... ...
... ... @@ -22,7 +22,7 @@ namespace IndustrialControl.ViewModels
// 列表数据源
public ObservableCollection<string> AvailableBins { get; } = new();
public ObservableCollection<OutScannedItem> ScannedList { get; } = new();
public ObservableCollection<PendingItem> PendingList { get; } = new();
public ObservableCollection<OutPendingItem> PendingList { get; } = new();
[ObservableProperty] private OutScannedItem? selectedScanItem;
... ...
... ... @@ -100,5 +100,8 @@ public record InboundOrderSummary(
string purchaseNo,
string supplierName,
string arrivalNo,
string workOrderNo,
string materialName,
int instockQty,
string createdTime
);
... ...
... ... @@ -362,8 +362,11 @@ namespace IndustrialControl.ViewModels
// === 列表行模型 ===
public class PendingItem
{
public string Name { get; set; } = "";
public string Spec { get; set; } = "";
public string Name { get; set; } = ""; //产品名称
public string MaterialCode { get; set; } = ""; //产品编码
public string Spec { get; set; } = "";//规格
public string Location { get; set; } = "";//出库库位
public string ProductionBatch { get; set; } = "";//生产批号
public int PendingQty { get; set; }
public string Bin { get; set; } = "请选择";
public int ScannedQty { get; set; }
... ...
... ... @@ -81,7 +81,10 @@ public partial class InboundProductionSearchViewModel : ObservableObject
["purchaseNo"] = o.purchaseNo,
["supplierName"] = o.supplierName,
["arrivalNo"] = o.arrivalNo,
["createdTime"] = o.createdTime
["instockQty"] = o.instockQty,
["materialName"] = o.materialName,
["workOrderNo"] = o.workOrderNo,
["createdTime"] = o.createdTime
});
}
... ...
... ... @@ -18,6 +18,9 @@ namespace IndustrialControl.ViewModels
[ObservableProperty] private string? orderTypeName;
[ObservableProperty] private string? purchaseNo;
[ObservableProperty] private string? supplierName;
[ObservableProperty] private string? workOrderNo;
[ObservableProperty] private string? materialName;
[ObservableProperty] private int instockQty;
[ObservableProperty] private string? createdTime;
// 列表数据源
... ... @@ -41,7 +44,7 @@ namespace IndustrialControl.ViewModels
// ================ 初始化入口(页面 OnAppearing 调用) ================
public async Task InitializeFromSearchAsync(
string instockId, string instockNo, string orderType, string orderTypeName,
string purchaseNo, string supplierName, string createdTime)
string purchaseNo, string supplierName, string createdTime,string workOrderNo,string materialName,int instockQty)
{
// 1) 基础信息
InstockId = instockId;
... ... @@ -51,7 +54,9 @@ namespace IndustrialControl.ViewModels
PurchaseNo = purchaseNo;
SupplierName = supplierName;
CreatedTime = createdTime;
WorkOrderNo = workOrderNo;
MaterialName = materialName;
InstockQty = instockQty;
// 2) 下拉库位(如无接口可留空或使用后端返回的 location 聚合)
AvailableBins.Clear();
... ...
... ... @@ -8,19 +8,20 @@ namespace IndustrialControl.ViewModels;
public partial class OutboundFinishedSearchViewModel : ObservableObject
{
private readonly IOutboundMaterialService _dataSvc;
[ObservableProperty] private string? searchOrderNo;
[ObservableProperty] private string searchOrderNo;
[ObservableProperty] private DateTime startDate = DateTime.Today;
[ObservableProperty] private DateTime endDate = DateTime.Today;
[ObservableProperty] private OutboundOrderSummary? selectedOrder;
private CancellationTokenSource? _searchCts;
// 仅用于“高亮选中”
[ObservableProperty] private OutboundOrderSummary? selectedOrder;
public OutboundFinishedSearchViewModel(IOutboundMaterialService dataSvc)
{
_dataSvc = dataSvc;
Orders = new ObservableCollection<OutboundOrderSummary>();
}
public ObservableCollection<OutboundOrderSummary> Orders { get; }
[RelayCommand]
... ... @@ -31,9 +32,15 @@ public partial class OutboundFinishedSearchViewModel : ObservableObject
var ct = _searchCts.Token;
try
{
var list = await _dataSvc.ListOutboundOrdersAsync(SearchOrderNo, startDate, endDate, "out_delivery", null,ct);
var list = await _dataSvc.ListOutboundOrdersAsync(
searchOrderNo, // 单号/条码
startDate, // 开始日期
endDate, // 结束日期(Service 内会扩到 23:59:59)
"out_delivery", // 不传单值 orderType,用 null 更清晰
null, // 多类型数组
ct // ← 新增:取消令牌
);
// ★ 在主线程更新 ObservableCollection,避免看起来“没刷新”
await MainThread.InvokeOnMainThreadAsync(() =>
{
Orders.Clear();
... ... @@ -44,7 +51,6 @@ public partial class OutboundFinishedSearchViewModel : ObservableObject
}
});
// (排查辅助)无数据时提示一下,确认命令确实执行了
if (list == null || !list.Any())
await Shell.Current.DisplayAlert("提示", "未查询到任何入库单", "确定");
}
... ... @@ -54,38 +60,34 @@ public partial class OutboundFinishedSearchViewModel : ObservableObject
}
}
// === 方案A:命令接收“当前项”作为参数,不依赖 SelectedOrder ===
[RelayCommand(CanExecute = nameof(CanGoOutbound))]
private async Task GoOutboundAsync(OutboundOrderSummary? item)
{
if (SelectedOrder is null) return;
if (item is null) return;
static string E(string? v) => Uri.EscapeDataString(v ?? "");
var o = SelectedOrder; // 确保 SelectedOrder 包含以下字段
var url =
$"//OutboundFinished" +
$"?outstockId={E(o.outstockId)}" +
$"&outstockNo={E(searchOrderNo)}" +
$"&orderType={E(o.orderType)}" +
$"&orderTypeName={E(o.orderTypeName)}" +
$"&purchaseNo={E(o.purchaseNo)}" +
$"&supplierName={E(o.supplierName)}" +
$"&createdTime={E(o.createdTime)}";
await Shell.Current.GoToAsync(url);
var o = item;
await Shell.Current.GoToAsync(
nameof(Pages.OutboundFinishedPage),
new Dictionary<string, object>
{
["outstockId"] = o.outstockId,
["outstockNo"] = o.outstockNo,
["orderType"] = o.orderType,
["orderTypeName"] = o.orderTypeName,
["requisitionMaterialNo"] = o.requisitionMaterialNo,
["returnNo"] = o.returnNo,
["deliveryNo"] = o.deliveryNo,
["createdTime"] = o.createdTime
});
}
// 与命令同签名的 CanExecute
private bool CanGoOutbound(OutboundOrderSummary? item) => item != null;
}
public record OutboundFinishedSummary(
string outstockId,
string outstockNo,
string orderType,
string orderTypeName,
string purchaseNo,
string supplierName,
string createdTime
);
... ...
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using IndustrialControl.Services;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using IndustrialControl.Services;
using IndustrialControl.Models;
namespace IndustrialControl.ViewModels
{
... ... @@ -17,16 +16,15 @@ namespace IndustrialControl.ViewModels
[ObservableProperty] private string? outstockNo;
[ObservableProperty] private string? orderType;
[ObservableProperty] private string? orderTypeName;
[ObservableProperty] private string? purchaseNo;
[ObservableProperty] private string? supplierName;
[ObservableProperty] private string? createdTime;
[ObservableProperty] private string? requisitionMaterialNo;
[ObservableProperty] private string? returnNo;
[ObservableProperty] private string? workOrderNo;
[ObservableProperty] private string? deliveryNo;
[ObservableProperty] private string? createdTime;
// 列表数据源
public ObservableCollection<string> AvailableBins { get; } = new();
public ObservableCollection<OutScannedItem> ScannedList { get; } = new();
public ObservableCollection<PendingItem> PendingList { get; } = new();
public ObservableCollection<OutPendingItem> PendingList { get; } = new();
[ObservableProperty] private OutScannedItem? selectedScanItem;
... ... @@ -43,28 +41,27 @@ namespace IndustrialControl.ViewModels
// 命令
public IRelayCommand ShowPendingCommand { get; }
public IRelayCommand ShowScannedCommand { get; }
public IAsyncRelayCommand ConfirmCommand { get; }
public OutboundFinishedViewModel(IOutboundMaterialService warehouseSvc)
public OutboundFinishedViewModel(IOutboundMaterialService api)
{
_api = warehouseSvc;
_api = api;
ShowPendingCommand = new RelayCommand(() => SwitchTab(true));
ShowScannedCommand = new RelayCommand(() => SwitchTab(false));
//ConfirmCommand = new AsyncRelayCommand(ConfirmInboundAsync);
}
// ================ 初始化入口(页面 OnAppearing 调用) ================
public async Task InitializeFromSearchAsync(
string outstockId, string outstockNo, string orderType, string orderTypeName,
string purchaseNo, string supplierName, string createdTime)
string requisitionMaterialNo, string returnNo, string deliveryNo, string createdTime)
{
// 1) 基础信息
OutstockId = outstockId;
OutstockNo = outstockNo;
OrderType = orderType;
OrderTypeName = orderTypeName;
PurchaseNo = purchaseNo;
SupplierName = supplierName;
RequisitionMaterialNo = requisitionMaterialNo;
ReturnNo = returnNo;
DeliveryNo = deliveryNo;
CreatedTime = createdTime;
// 2) 下拉库位(如无接口可留空或使用后端返回的 location 聚合)
... ... @@ -102,15 +99,19 @@ namespace IndustrialControl.ViewModels
var rows = await _api.GetOutStockDetailAsync(OutstockId!);
foreach (var r in rows)
{
PendingList.Add(new PendingItem
PendingList.Add(new OutPendingItem
{
Name = r.MaterialName ?? "",
MaterialCode = r.MaterialCode ?? "",
Spec = r.Spec ?? "",
PendingQty = r.PendingQty,
Bin = string.IsNullOrWhiteSpace(r.Location) ? "请选择" : r.Location!,
ScannedQty = r.ScannedQty
Location = r.Location ?? "",
ProductionBatch = r.ProductionBatch ?? "",
StockBatch = r.StockBatch ?? "",
OutstockQty = r.OutstockQty,
Qty = r.Qty
});
// 聚合可选库位
if (!string.IsNullOrWhiteSpace(r.Location) && !AvailableBins.Contains(r.Location))
AvailableBins.Add(r.Location);
... ... @@ -132,7 +133,11 @@ namespace IndustrialControl.ViewModels
Name = r.MaterialName ?? "",
Spec = r.Spec ?? "",
Location = string.IsNullOrWhiteSpace(r.Location) ? "请选择" : r.Location!,
Qty = r.Qty
Qty = r.Qty,
ScanStatus = r.ScanStatus,
WarehouseCode = r.WarehouseCode ?? "",
DetailId = r.DetailId,
Id = OutstockId
});
if (!string.IsNullOrWhiteSpace(r.Location) && !AvailableBins.Contains(r.Location))
... ... @@ -140,83 +145,48 @@ namespace IndustrialControl.ViewModels
}
}
// OutboundFinishedViewModel.cs
[RelayCommand]
private async Task PassScan()
{
if (string.IsNullOrWhiteSpace(OutstockId))
{
await ShowTip("缺少 OutstockId,无法确认。请从查询页进入。");
return;
}
// 依旧要求只能选中一行,作为“操作目标”的 UI 约束(接口本身仅需 instockId)
var selected = ScannedList.Where(x => x.IsSelected).ToList();
if (selected.Count != 1)
{
await ShowTip(selected.Count == 0 ? "请先勾选一条已扫描记录。" : "一次只能操作一条记录。");
return;
}
var selectedBarcode = selected[0].Barcode;
var picks = ScannedList.Where(x => x.IsSelected).ToList();
if (picks.Count == 0) { await ShowTip("请先勾选至少一条已扫描记录。"); return; }
// 调用确认接口
var resp = await _api.ScanConfirmAsync(OutstockId!);
// 组装 [{ barcode, id }]
var items = picks.Select(x => (barcode: x.Barcode, id: x.Id)).ToList();
var resp = await _api.ScanConfirmAsync(items);
if (!resp.Succeeded)
{
await ShowTip(string.IsNullOrWhiteSpace(resp.Message) ? "扫描通过失败,请重试。" : resp.Message!);
return;
}
// 成功:刷新两张表
await LoadPendingAsync();
await LoadScannedAsync();
// 友好:恢复选中刚才那条(如果还在列表里)
var hit = ScannedList.FirstOrDefault(x =>
string.Equals(x.Barcode, selectedBarcode, StringComparison.OrdinalIgnoreCase));
if (hit != null) { hit.IsSelected = true; SelectedScanItem = hit; }
await ShowTip("已确认通过。");
}
[RelayCommand]
private async Task CancelScan()
{
if (string.IsNullOrWhiteSpace(OutstockId))
{
await ShowTip("缺少 OutstockId,无法取消。请从查询页进入。");
return;
}
// 依旧限制一次只操作一条(接口本身只要 instockId,这里是 UI 规范)
var selected = ScannedList.Where(x => x.IsSelected).ToList();
if (selected.Count != 1)
{
await ShowTip(selected.Count == 0 ? "请先勾选一条已扫描记录。" : "一次只能操作一条记录。");
return;
}
var selectedBarcode = selected[0].Barcode;
var resp = await _api.CancelScanAsync(OutstockId!);
var picks = ScannedList.Where(x => x.IsSelected).ToList();
if (picks.Count == 0) { await ShowTip("请先勾选至少一条记录。"); return; }
var items = picks.Select(x => (barcode: x.Barcode, id: x.Id)).ToList();
var resp = await _api.CancelScanAsync(items);
if (!resp.Succeeded)
{
await ShowTip(string.IsNullOrWhiteSpace(resp.Message) ? "取消扫描失败,请重试。" : resp.Message!);
return;
}
// 成功后以服务端为准刷新两张表
await LoadPendingAsync();
await LoadScannedAsync();
// 友好:若那条还在列表里,恢复选中
var hit = ScannedList.FirstOrDefault(x =>
string.Equals(x.Barcode, selectedBarcode, StringComparison.OrdinalIgnoreCase));
if (hit != null) { hit.IsSelected = true; SelectedScanItem = hit; }
await ShowTip("已取消扫描。");
}
public async Task HandleScannedAsync(string data, string symbology)
{
var barcode = (data ?? string.Empty).Trim();
... ... @@ -279,7 +249,12 @@ namespace IndustrialControl.ViewModels
}
}
public async Task<bool> ConfirmInboundAsync()
// OutboundFinishedViewModel.cs
private Task<bool> AskAsync(string title, string message, string ok = "是", string cancel = "否") =>
Shell.Current?.DisplayAlert(title, message, ok, cancel) ?? Task.FromResult(false);
public async Task<bool> ConfirmOutboundAsync()
{
if (string.IsNullOrWhiteSpace(OutstockId))
{
... ... @@ -287,6 +262,17 @@ namespace IndustrialControl.ViewModels
return false;
}
// ② 服务端权威校验:是否全部扫码确认,后端接口
bool serverAllOk = await _api.JudgeOutstockDetailScanAllAsync(OutstockId!);
// 任意一处不一致 → 提示是否继续
if (!serverAllOk)
{
bool goOn = await AskAsync("提示", "已扫描列表与待入库数量不一致,是否继续入库?");
if (!goOn) return false;
}
// ③ 调用确认入库接口
var r = await _api.ConfirmOutstockAsync(OutstockId!);
if (!r.Succeeded)
{
... ... @@ -294,12 +280,87 @@ namespace IndustrialControl.ViewModels
return false;
}
// 成功:可选刷新一次,以服务端为准;随后按钮事件里会 ClearAll()
await LoadPendingAsync();
await LoadScannedAsync();
return true;
}
public async Task<bool> UpdateRowLocationAsync(object row, BinInfo bin, CancellationToken ct = default)
{
if (row is null || bin is null) return false;
// 通过反射从行对象里取必要字段(兼容不同命名)
var t = row.GetType();
string detailId =
t.GetProperty("DetailId")?.GetValue(row)?.ToString()
?? t.GetProperty("detailId")?.GetValue(row)?.ToString()
?? string.Empty;
string id =
t.GetProperty("Id")?.GetValue(row)?.ToString()
?? t.GetProperty("id")?.GetValue(row)?.ToString()
?? string.Empty;
if (string.IsNullOrWhiteSpace(detailId) || string.IsNullOrWhiteSpace(id))
{
await MainThread.InvokeOnMainThreadAsync(async () =>
await Application.Current.MainPage.DisplayAlert("提示", "缺少必要字段:detailId 或 id。", "确定"));
return false;
}
// 组织参数
var outstockWarehouse = bin.WarehouseName ?? "";
var outstockWarehouseCode = bin.WarehouseCode ?? "";
var location = bin.Location ?? "";
// 调用接口
var ok = await _api.UpdateOutstockLocationAsync(
detailId, id, outstockWarehouse, outstockWarehouseCode, location, ct);
if (!ok.Succeeded)
{
await MainThread.InvokeOnMainThreadAsync(async () =>
await Application.Current.MainPage.DisplayAlert("提示", ok.Message ?? "更新库位失败", "确定"));
return false;
}
return true;
}
public async Task<bool> UpdateQuantityForRowAsync(OutScannedItem row, CancellationToken ct = default)
{
if (row is null) return false;
if (!row.ScanStatus)
{
await ShowTip("该行尚未扫描通过,不能修改数量。");
return false;
}
if (row.Qty < 0)
{
await ShowTip("数量不能为负数。");
return false;
}
var resp = await _api.UpdateQuantityAsync(row.Barcode, row.DetailId, row.Id, row.Qty, ct);
if (!resp.Succeeded)
{
await ShowTip(string.IsNullOrWhiteSpace(resp.Message) ? "更新数量失败" : resp.Message!);
return false;
}
// ✅ 成功:先提示,再刷新两张表
await ShowTip("数量修改成功");
// 记录一下当前行用于刷新后恢复选中
var keepBarcode = row.Barcode;
await LoadPendingAsync(); // 刷新“待入库明细”
await LoadScannedAsync(); // 刷新“已扫描明细”
var hit = ScannedList.FirstOrDefault(x => string.Equals(x.Barcode, keepBarcode, StringComparison.OrdinalIgnoreCase));
if (hit != null) { hit.IsSelected = true; SelectedScanItem = hit; }
return true;
}
}
... ...
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using IndustrialControl.Pages;
using IndustrialControl.Services;
using System;
using System.Collections.ObjectModel;
namespace IndustrialControl.ViewModels;
... ... @@ -10,19 +8,20 @@ namespace IndustrialControl.ViewModels;
public partial class OutboundMaterialSearchViewModel : ObservableObject
{
private readonly IOutboundMaterialService _dataSvc;
[ObservableProperty] private string? searchOrderNo;
[ObservableProperty] private string searchOrderNo;
[ObservableProperty] private DateTime startDate = DateTime.Today;
[ObservableProperty] private DateTime endDate = DateTime.Today;
[ObservableProperty] private OutboundOrderSummary? selectedOrder;
private CancellationTokenSource? _searchCts;
// 仅用于“高亮选中”
[ObservableProperty] private OutboundOrderSummary? selectedOrder;
public OutboundMaterialSearchViewModel(IOutboundMaterialService dataSvc)
{
_dataSvc = dataSvc;
Orders = new ObservableCollection<OutboundOrderSummary>();
}
public ObservableCollection<OutboundOrderSummary> Orders { get; }
[RelayCommand]
... ... @@ -33,10 +32,16 @@ public partial class OutboundMaterialSearchViewModel : ObservableObject
var ct = _searchCts.Token;
try
{
var orderTypeList = new[] { "out_return", "out_return", "out_other" };
var list = await _dataSvc.ListOutboundOrdersAsync(SearchOrderNo, startDate, endDate, null, orderTypeList,ct);
var orderTypeList = new[] { "out_return", "out_requisition", "out_other" };
var list = await _dataSvc.ListOutboundOrdersAsync(
searchOrderNo, // 单号/条码
startDate, // 开始日期
endDate, // 结束日期(Service 内会扩到 23:59:59)
null, // 不传单值 orderType,用 null 更清晰
orderTypeList, // 多类型数组
ct // ← 新增:取消令牌
);
// ★ 在主线程更新 ObservableCollection,避免看起来“没刷新”
await MainThread.InvokeOnMainThreadAsync(() =>
{
Orders.Clear();
... ... @@ -47,7 +52,6 @@ public partial class OutboundMaterialSearchViewModel : ObservableObject
}
});
// (排查辅助)无数据时提示一下,确认命令确实执行了
if (list == null || !list.Any())
await Shell.Current.DisplayAlert("提示", "未查询到任何入库单", "确定");
}
... ... @@ -57,40 +61,45 @@ public partial class OutboundMaterialSearchViewModel : ObservableObject
}
}
// === 方案A:命令接收“当前项”作为参数,不依赖 SelectedOrder ===
[RelayCommand(CanExecute = nameof(CanGoOutbound))]
private async Task GoOutboundAsync(OutboundOrderSummary? item)
{
if (SelectedOrder is null) return;
if (item is null) return;
static string E(string? v) => Uri.EscapeDataString(v ?? "");
var o = SelectedOrder; // 确保 SelectedOrder 包含以下字段
var url =
$"//OutboundMaterial" +
$"?outstockId={E(o.outstockId)}" +
$"&outstockNo={E(searchOrderNo)}" +
$"&orderType={E(o.orderType)}" +
$"&orderTypeName={E(o.orderTypeName)}" +
$"&purchaseNo={E(o.purchaseNo)}" +
$"&supplierName={E(o.supplierName)}" +
$"&createdTime={E(o.createdTime)}";
var o = item;
await Shell.Current.GoToAsync(
nameof(Pages.OutboundMaterialPage),
new Dictionary<string, object>
{
["outstockId"] = o.outstockId,
["outstockNo"] = o.outstockNo,
["orderType"] = o.orderType,
["orderTypeName"] = o.orderTypeName,
["requisitionMaterialNo"] = o.requisitionMaterialNo,
["returnNo"] = o.returnNo,
["deliveryNo"] = o.deliveryNo,
["createdTime"] = o.createdTime
});
await Shell.Current.GoToAsync(url);
}
// 与命令同签名的 CanExecute
private bool CanGoOutbound(OutboundOrderSummary? item) => item != null;
}
// 用于列表显示的精简 DTO
public record OutboundOrderSummary(
string outstockId,
string outstockNo,
string orderType,
string orderTypeName,
string purchaseNo,
string supplierName,
string arrivalNo,
string createdTime,
string workOrderNo,
string returnNo,
string deliveryNo,
string requisitionMaterialNo,
string returnNo,
string workOrderNo
string createdTime
);
... ...
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using IndustrialControl.Services;
using System.Collections.ObjectModel;
using IndustrialControl.Services;
using IndustrialControl.Models;
namespace IndustrialControl.ViewModels
{
... ... @@ -15,17 +16,15 @@ namespace IndustrialControl.ViewModels
[ObservableProperty] private string? outstockNo;
[ObservableProperty] private string? orderType;
[ObservableProperty] private string? orderTypeName;
[ObservableProperty] private string? purchaseNo;
[ObservableProperty] private string? supplierName;
[ObservableProperty] private string? createdTime;
[ObservableProperty] private string? requisitionMaterialNo;
[ObservableProperty] private string? returnNo;
[ObservableProperty] private string? workOrderNo;
[ObservableProperty] private string? deliveryNo;
[ObservableProperty] private string? createdTime;
// 列表数据源
public ObservableCollection<string> AvailableBins { get; } = new();
public ObservableCollection<OutScannedItem> ScannedList { get; } = new();
public ObservableCollection<PendingItem> PendingList { get; } = new();
public ObservableCollection<OutPendingItem> PendingList { get; } = new();
[ObservableProperty] private OutScannedItem? selectedScanItem;
... ... @@ -42,28 +41,27 @@ namespace IndustrialControl.ViewModels
// 命令
public IRelayCommand ShowPendingCommand { get; }
public IRelayCommand ShowScannedCommand { get; }
public IAsyncRelayCommand ConfirmCommand { get; }
public OutboundMaterialViewModel(IOutboundMaterialService warehouseSvc)
public OutboundMaterialViewModel(IOutboundMaterialService api)
{
_api = warehouseSvc;
_api = api;
ShowPendingCommand = new RelayCommand(() => SwitchTab(true));
ShowScannedCommand = new RelayCommand(() => SwitchTab(false));
//ConfirmCommand = new AsyncRelayCommand(ConfirmInboundAsync);
}
// ================ 初始化入口(页面 OnAppearing 调用) ================
public async Task InitializeFromSearchAsync(
string outstockId, string outstockNo, string orderType, string orderTypeName,
string purchaseNo, string supplierName, string createdTime)
string requisitionMaterialNo, string returnNo,string deliveryNo, string createdTime)
{
// 1) 基础信息
OutstockId = outstockId;
OutstockNo = outstockNo;
OrderType = orderType;
OrderTypeName = orderTypeName;
PurchaseNo = purchaseNo;
SupplierName = supplierName;
RequisitionMaterialNo = requisitionMaterialNo;
ReturnNo = returnNo;
DeliveryNo = deliveryNo;
CreatedTime = createdTime;
// 2) 下拉库位(如无接口可留空或使用后端返回的 location 聚合)
... ... @@ -101,13 +99,16 @@ namespace IndustrialControl.ViewModels
var rows = await _api.GetOutStockDetailAsync(OutstockId!);
foreach (var r in rows)
{
PendingList.Add(new PendingItem
PendingList.Add(new OutPendingItem
{
Name = r.MaterialName ?? "",
MaterialCode = r.MaterialCode ?? "",
Spec = r.Spec ?? "",
PendingQty = r.PendingQty,
Bin = string.IsNullOrWhiteSpace(r.Location) ? "请选择" : r.Location!,
ScannedQty = r.ScannedQty
Location = r.Location,
ProductionBatch = r.ProductionBatch,
StockBatch = r.StockBatch,
OutstockQty = r.OutstockQty,
Qty = r.Qty
});
// 聚合可选库位
... ... @@ -132,6 +133,8 @@ namespace IndustrialControl.ViewModels
Spec = r.Spec ?? "",
Location = string.IsNullOrWhiteSpace(r.Location) ? "请选择" : r.Location!,
Qty = r.Qty,
ScanStatus = r.ScanStatus,
WarehouseCode = r.WarehouseCode ?? "",
DetailId = r.DetailId,
Id = OutstockId
});
... ... @@ -141,83 +144,48 @@ namespace IndustrialControl.ViewModels
}
}
// OutboundMaterialViewModel.cs
[RelayCommand]
private async Task PassScan()
{
if (string.IsNullOrWhiteSpace(OutstockId))
{
await ShowTip("缺少 OutstockId,无法确认。请从查询页进入。");
return;
}
// 依旧要求只能选中一行,作为“操作目标”的 UI 约束(接口本身仅需 outstockId)
var selected = ScannedList.Where(x => x.IsSelected).ToList();
if (selected.Count != 1)
{
await ShowTip(selected.Count == 0 ? "请先勾选一条已扫描记录。" : "一次只能操作一条记录。");
return;
}
var selectedBarcode = selected[0].Barcode;
var picks = ScannedList.Where(x => x.IsSelected).ToList();
if (picks.Count == 0) { await ShowTip("请先勾选至少一条已扫描记录。"); return; }
// 调用确认接口
var resp = await _api.ScanConfirmAsync(OutstockId!);
// 组装 [{ barcode, id }]
var items = picks.Select(x => (barcode: x.Barcode, id: x.Id)).ToList();
var resp = await _api.ScanConfirmAsync(items);
if (!resp.Succeeded)
{
await ShowTip(string.IsNullOrWhiteSpace(resp.Message) ? "扫描通过失败,请重试。" : resp.Message!);
return;
}
// 成功:刷新两张表
await LoadPendingAsync();
await LoadScannedAsync();
// 友好:恢复选中刚才那条(如果还在列表里)
var hit = ScannedList.FirstOrDefault(x =>
string.Equals(x.Barcode, selectedBarcode, StringComparison.OrdinalIgnoreCase));
if (hit != null) { hit.IsSelected = true; SelectedScanItem = hit; }
await ShowTip("已确认通过。");
}
[RelayCommand]
private async Task CancelScan()
{
if (string.IsNullOrWhiteSpace(OutstockId))
{
await ShowTip("缺少 OutstockId,无法取消。请从查询页进入。");
return;
}
// 依旧限制一次只操作一条(接口本身只要 outstockId,这里是 UI 规范)
var selected = ScannedList.Where(x => x.IsSelected).ToList();
if (selected.Count != 1)
{
await ShowTip(selected.Count == 0 ? "请先勾选一条已扫描记录。" : "一次只能操作一条记录。");
return;
}
var selectedBarcode = selected[0].Barcode;
var resp = await _api.CancelScanAsync(OutstockId!);
var picks = ScannedList.Where(x => x.IsSelected).ToList();
if (picks.Count == 0) { await ShowTip("请先勾选至少一条记录。"); return; }
var items = picks.Select(x => (barcode: x.Barcode, id: x.Id)).ToList();
var resp = await _api.CancelScanAsync(items);
if (!resp.Succeeded)
{
await ShowTip(string.IsNullOrWhiteSpace(resp.Message) ? "取消扫描失败,请重试。" : resp.Message!);
return;
}
// 成功后以服务端为准刷新两张表
await LoadPendingAsync();
await LoadScannedAsync();
// 友好:若那条还在列表里,恢复选中
var hit = ScannedList.FirstOrDefault(x =>
string.Equals(x.Barcode, selectedBarcode, StringComparison.OrdinalIgnoreCase));
if (hit != null) { hit.IsSelected = true; SelectedScanItem = hit; }
await ShowTip("已取消扫描。");
}
public async Task HandleScannedAsync(string data, string symbology)
{
var barcode = (data ?? string.Empty).Trim();
... ... @@ -280,7 +248,12 @@ namespace IndustrialControl.ViewModels
}
}
public async Task<bool> ConfirmInboundAsync()
// OutboundMaterialViewModel.cs
private Task<bool> AskAsync(string title, string message, string ok = "是", string cancel = "否") =>
Shell.Current?.DisplayAlert(title, message, ok, cancel) ?? Task.FromResult(false);
public async Task<bool> ConfirmOutboundAsync()
{
if (string.IsNullOrWhiteSpace(OutstockId))
{
... ... @@ -288,6 +261,17 @@ namespace IndustrialControl.ViewModels
return false;
}
// ② 服务端权威校验:是否全部扫码确认,后端接口
bool serverAllOk = await _api.JudgeOutstockDetailScanAllAsync(OutstockId!);
// 任意一处不一致 → 提示是否继续
if (!serverAllOk)
{
bool goOn = await AskAsync("提示", "已扫描列表与待入库数量不一致,是否继续入库?");
if (!goOn) return false;
}
// ③ 调用确认入库接口
var r = await _api.ConfirmOutstockAsync(OutstockId!);
if (!r.Succeeded)
{
... ... @@ -295,13 +279,101 @@ namespace IndustrialControl.ViewModels
return false;
}
// 成功:可选刷新一次,以服务端为准;随后按钮事件里会 ClearAll()
await LoadPendingAsync();
await LoadScannedAsync();
return true;
}
}
public async Task<bool> UpdateRowLocationAsync(object row, BinInfo bin, CancellationToken ct = default)
{
if (row is null || bin is null) return false;
// 通过反射从行对象里取必要字段(兼容不同命名)
var t = row.GetType();
string detailId =
t.GetProperty("DetailId")?.GetValue(row)?.ToString()
?? t.GetProperty("detailId")?.GetValue(row)?.ToString()
?? string.Empty;
string id =
t.GetProperty("Id")?.GetValue(row)?.ToString()
?? t.GetProperty("id")?.GetValue(row)?.ToString()
?? string.Empty;
if (string.IsNullOrWhiteSpace(detailId) || string.IsNullOrWhiteSpace(id))
{
await MainThread.InvokeOnMainThreadAsync(async () =>
await Application.Current.MainPage.DisplayAlert("提示", "缺少必要字段:detailId 或 id。", "确定"));
return false;
}
// 组织参数
var outstockWarehouse = bin.WarehouseName ?? "";
var outstockWarehouseCode = bin.WarehouseCode ?? "";
var location = bin.Location ?? "";
// 调用接口
var ok = await _api.UpdateOutstockLocationAsync(
detailId, id, outstockWarehouse, outstockWarehouseCode, location, ct);
if (!ok.Succeeded)
{
await MainThread.InvokeOnMainThreadAsync(async () =>
await Application.Current.MainPage.DisplayAlert("提示", ok.Message ?? "更新库位失败", "确定"));
return false;
}
return true;
}
public async Task<bool> UpdateQuantityForRowAsync(OutScannedItem row, CancellationToken ct = default)
{
if (row is null) return false;
if (!row.ScanStatus)
{
await ShowTip("该行尚未扫描通过,不能修改数量。");
return false;
}
if (row.Qty < 0)
{
await ShowTip("数量不能为负数。");
return false;
}
var resp = await _api.UpdateQuantityAsync(row.Barcode, row.DetailId, row.Id, row.Qty, ct);
if (!resp.Succeeded)
{
await ShowTip(string.IsNullOrWhiteSpace(resp.Message) ? "更新数量失败" : resp.Message!);
return false;
}
// ✅ 成功:先提示,再刷新两张表
await ShowTip("数量修改成功");
// 记录一下当前行用于刷新后恢复选中
var keepBarcode = row.Barcode;
await LoadPendingAsync(); // 刷新“待入库明细”
await LoadScannedAsync(); // 刷新“已扫描明细”
var hit = ScannedList.FirstOrDefault(x => string.Equals(x.Barcode, keepBarcode, StringComparison.OrdinalIgnoreCase));
if (hit != null) { hit.IsSelected = true; SelectedScanItem = hit; }
return true;
}
}
public class OutPendingItem
{
public string Name { get; set; } = ""; //产品名称
public string MaterialCode { get; set; } = ""; //产品编码
public string Spec { get; set; } = "";//规格
public string Location { get; set; } = "";//出库库位
public string ProductionBatch { get; set; } = "";//生产批号
public string StockBatch { get; set; } = "";//批次号
public int OutstockQty { get; set; } //出库数量
public int Qty { get; set; } //已扫描数
public string Bin { get; set; } = "请选择";
}
}
... ...