作者 李壮

mock change to api

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

要显示太多修改。

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

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