作者 李壮

add binpicker

... ... @@ -106,6 +106,12 @@
<MauiXaml Update="Pages\AdminPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Pages\BinListPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Pages\BinPickerPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Pages\HomePage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
... ...
... ... @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IndustrialControl", "IndustrialControl.csproj", "{A54387CA-553D-4CE0-B86C-08A45497D703}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IndustrialControl", "IndustrialControl.csproj", "{A54387CA-553D-4CE0-B86C-08A45497D703}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
... ...
... ... @@ -24,6 +24,8 @@ namespace IndustrialControl
builder.Services.AddSingleton<IConfigLoader, ConfigLoader>();
builder.Services.AddSingleton<Services.LogService>();
builder.Services.AddSingleton<IWarehouseDataService, MockWarehouseDataService>();
builder.Services.AddSingleton<IDialogService, DialogService>();
builder.Services.AddTransient<IndustrialControl.ViewModels.BinPickerViewModel>();
// 扫码服务
builder.Services.AddSingleton<ScanService>();
... ...
<?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"
xmlns:srv="clr-namespace:IndustrialControl.Services"
x:Class="IndustrialControl.Pages.BinListPage"
Title="选择库位"
BackgroundColor="White">
<Grid RowDefinitions="Auto,Auto,*" Padding="12">
<!-- 标题行 -->
<Grid ColumnDefinitions="*,Auto" Padding="0,0,0,8">
<Label Text="选择库位" FontSize="18" FontAttributes="Bold" />
<Button Text="关闭" Clicked="OnCloseClicked" BackgroundColor="Transparent" />
</Grid>
<!-- 表头 -->
<Grid BackgroundColor="#F5F6F7" Padding="8" ColumnDefinitions="*,*,*,*,*,*,80">
<Label Text="库位编号" FontAttributes="Bold"/>
<Label Grid.Column="1" Text="仓库名称" FontAttributes="Bold"/>
<Label Grid.Column="2" Text="仓库编码" FontAttributes="Bold"/>
<Label Grid.Column="3" Text="层数" FontAttributes="Bold"/>
<Label Grid.Column="4" Text="货架" FontAttributes="Bold"/>
<Label Grid.Column="5" Text="分区" FontAttributes="Bold"/>
<Label Grid.Column="6" Text="操作" FontAttributes="Bold" HorizontalTextAlignment="Center"/>
</Grid>
<!-- 列表 -->
<CollectionView Grid.Row="2" ItemsSource="{Binding Bins}" SelectionMode="None">
<CollectionView.EmptyView>
<Label Text="该货架下暂无库位" TextColor="#999" Margin="12"/>
</CollectionView.EmptyView>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="srv:BinInfo">
<Grid Padding="8" ColumnDefinitions="*,*,*,*,*,*,80" BackgroundColor="White">
<Label Text="{Binding BinCode}" />
<Label Grid.Column="1" Text="{Binding WarehouseName}" />
<Label Grid.Column="2" Text="{Binding WarehouseCode}" />
<Label Grid.Column="3" Text="{Binding Layer}" />
<Label Grid.Column="4" Text="{Binding Shelf}" />
<Label Grid.Column="5" Text="{Binding Zone}" />
<Button Grid.Column="6" Text="选择" Clicked="OnPickClicked" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
</ContentPage>
... ...
using System.Collections.ObjectModel;
using IndustrialControl.Services;
namespace IndustrialControl.Pages;
public partial class BinListPage : ContentPage
{
public ObservableCollection<BinInfo> Bins { get; } = new();
private readonly TaskCompletionSource<BinInfo?> _tcs = new();
private readonly bool _closeParent;
public BinListPage(IEnumerable<BinInfo> bins, bool closeParent)
{
InitializeComponent();
BindingContext = this;
_closeParent = closeParent;
foreach (var b in bins) Bins.Add(b);
}
public static async Task<BinInfo?> ShowAsync(IEnumerable<BinInfo> bins, bool closeParent = false)
{
var page = new BinListPage(bins, closeParent);
var nav = Shell.Current?.Navigation ?? Application.Current?.MainPage?.Navigation
?? throw new InvalidOperationException("Navigation not ready");
await nav.PushModalAsync(page);
return await page._tcs.Task;
}
private async void OnCloseClicked(object? sender, EventArgs e)
{
_tcs.TrySetResult(null);
var nav = Shell.Current?.Navigation ?? Application.Current?.MainPage?.Navigation;
if (nav != null) await nav.PopModalAsync(); // 仅关自己
}
private async void OnPickClicked(object? sender, EventArgs e)
{
if (sender is BindableObject bo && bo.BindingContext is BinInfo bin)
{
// 先把选择结果回传,解除 ShowAsync 的 await
_tcs.TrySetResult(bin);
var nav = Shell.Current?.Navigation ?? Application.Current?.MainPage?.Navigation;
if (nav == null) return;
// 1) 关闭自己(BinListPage)
await nav.PopModalAsync();
// 2) 如需要,再关闭父页(BinPickerPage)
if (_closeParent && nav.ModalStack.Count > 0 && nav.ModalStack.Last() is BinPickerPage)
await nav.PopModalAsync();
}
}
}
... ...
<?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"
xmlns:vm="clr-namespace:IndustrialControl.ViewModels"
x:Class="IndustrialControl.Pages.BinPickerPage"
x:Name="Root"
Title="选择库位"
BackgroundColor="White">
<Grid Padding="12" RowDefinitions="Auto,*">
<Grid ColumnDefinitions="*,Auto" Padding="0,0,0,8">
<Label Text="选择库位" FontSize="18" FontAttributes="Bold" />
<Button Text="关闭" BackgroundColor="Transparent" Clicked="OnCloseClicked"/>
</Grid>
<ScrollView Grid.Row="1">
<!-- ★ 绑定可见树 -->
<CollectionView ItemsSource="{Binding VisibleTree}" SelectionMode="None">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="vm:BinTreeNode">
<Grid ColumnDefinitions="24,*" Padding="4" Margin="{Binding Indent}">
<Label Grid.Column="0" Text="{Binding ToggleIcon}"
HorizontalTextAlignment="Center" VerticalTextAlignment="Center">
<Label.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding BindingContext.ToggleCommand, Source={x:Reference Root}}"
CommandParameter="{Binding .}" />
</Label.GestureRecognizers>
</Label>
<Label Grid.Column="1" Text="{Binding Name}" VerticalTextAlignment="Center">
<Label.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding BindingContext.SelectNodeCommand, Source={x:Reference Root}}"
CommandParameter="{Binding .}" />
</Label.GestureRecognizers>
</Label>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ScrollView>
</Grid>
</ContentPage>
... ...
using IndustrialControl.Services;
using IndustrialControl.ViewModels;
namespace IndustrialControl.Pages;
public partial class BinPickerPage : ContentPage
{
private readonly TaskCompletionSource<BinInfo?> _tcs = new();
public BinPickerPage(string? preselectBinCode, BinPickerViewModel vm)
{
InitializeComponent();
BindingContext = vm;
vm.Initialize(preselectBinCode);
}
public static async Task<BinInfo?> ShowAsync(string? preselectBinCode)
{
var sp = Application.Current!.Handler!.MauiContext!.Services!;
var vm = sp.GetRequiredService<BinPickerViewModel>();
var page = new BinPickerPage(preselectBinCode, vm);
var nav = Shell.Current?.Navigation ?? Application.Current?.MainPage?.Navigation
?? throw new InvalidOperationException("Navigation not ready");
vm.SetCloseHandlers(
onPicked: async (bin) =>
{
page._tcs.TrySetResult(bin);
await MainThread.InvokeOnMainThreadAsync(async () =>
{
var nav = Shell.Current?.Navigation ?? Application.Current?.MainPage?.Navigation;
if (nav != null && nav.ModalStack.Count > 0 && nav.ModalStack.Last() is BinPickerPage)
await nav.PopModalAsync();
});
},
onCanceled: async () =>
{
page._tcs.TrySetResult(null);
await MainThread.InvokeOnMainThreadAsync(async () =>
{
var nav = Shell.Current?.Navigation ?? Application.Current?.MainPage?.Navigation;
if (nav != null && nav.ModalStack.Count > 0 && nav.ModalStack.Last() is BinPickerPage)
await nav.PopModalAsync();
});
});
await nav.PushModalAsync(page);
return await page._tcs.Task;
}
private async void OnCloseClicked(object? sender, EventArgs e)
{
if (BindingContext is BinPickerViewModel vm)
await vm.CancelAsync();
}
}
... ...
... ... @@ -168,11 +168,16 @@
<Label Grid.Column="2" Text="{Binding Name}" />
<Label Grid.Column="3" Text="{Binding Spec}" />
<!-- 预入库库位:改为下拉,可选 VM.AvailableBins,双向绑到 item.Bin -->
<Picker Grid.Column="4"
Title="选择库位"
ItemsSource="{Binding Source={x:Reference Page}, Path=BindingContext.AvailableBins}"
SelectedItem="{Binding Bin, Mode=TwoWay}" />
<!-- 4 入库库位(可点击) -->
<Label Grid.Column="4"
Text="{Binding Bin}"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
TextColor="#007BFF" FontAttributes="Bold">
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="OnBinTapped" />
</Label.GestureRecognizers>
</Label>
<!-- 已扫描数量:可手动改,用数字键盘;建议加转换器容错 -->
<Entry Grid.Column="5"
... ...
... ... @@ -11,13 +11,15 @@ public partial class InboundMaterialPage : ContentPage
private readonly InboundMaterialViewModel _vm;
// NEW: 由 Shell 设置
public string? OrderNo { get; set; }
private readonly IDialogService _dialogs;
public InboundMaterialPage(InboundMaterialViewModel vm, ScanService scanSvc)
public InboundMaterialPage(InboundMaterialViewModel vm, ScanService scanSvc, IDialogService dialogs)
{
InitializeComponent();
BindingContext = vm;
_scanSvc = scanSvc;
_vm = vm;
_dialogs = dialogs;
// 可选:配置前后缀与防抖
_scanSvc.Prefix = null; // 例如 "}q" 之类的前缀;没有就留 null
// _scanSvc.Suffix = "\n"; // 如果设备会附带换行,可去掉;没有就设 null
... ... @@ -94,5 +96,23 @@ public partial class InboundMaterialPage : ContentPage
}
}
private async void OnBinTapped(object? sender, TappedEventArgs e)
{
var bindable = sender as BindableObject;
var row = bindable?.BindingContext;
if (row == null) return;
var type = row.GetType();
var currentBin = type.GetProperty("Bin")?.GetValue(row)?.ToString();
var picked = await _dialogs.SelectBinAsync(currentBin);
if (picked == null) return;
var binProp = type.GetProperty("Bin");
if (binProp is { CanWrite: true })
binProp.SetValue(row, picked.BinCode);
else
_vm.SetItemBin(row, picked.BinCode);
}
}
... ...
... ... @@ -2,9 +2,11 @@
<ContentPage x:Name="Page"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:zxing="clr-namespace:ZXing.Net.Maui.Controls;assembly=ZXing.Net.MAUI"
x:Class="IndustrialControl.Pages.InboundMaterialSearchPage"
xmlns:conv="clr-namespace:IndustrialControl.Converters"
Title="仓储管理系统">
<ContentPage.Resources>
<ResourceDictionary>
<!-- 空/非空转布尔:非空 => true(按钮可用) -->
... ... @@ -24,7 +26,6 @@
BackgroundColor="White"
Text="{Binding SearchOrderNo}" />
<DatePicker Grid.Row="1" Grid.Column="0"
Date="{Binding CreatedDate}"
MinimumDate="2000-01-01" />
... ...
using System.Threading;
using IndustrialControl.Services;
using IndustrialControl.Services;
using IndustrialControl.ViewModels;
namespace IndustrialControl.Pages;
public partial class InboundMaterialSearchPage : ContentPage
... ... @@ -14,26 +13,28 @@ public partial class InboundMaterialSearchPage : ContentPage
BindingContext = vm;
_scanSvc = scanSvc;
InitializeComponent();
// 可选:配置前后缀与防抖
_scanSvc.Prefix = null; // 例如 "}q" 之类的前缀;没有就留 null
// _scanSvc.Suffix = "\n"; // 如果设备会附带换行,可去掉;没有就设 null
// 可选:配置前后缀与防抖
_scanSvc.Prefix = null; // 例如 "}q" 之类的前缀;没有就留 null
// _scanSvc.Suffix = "\n"; // 如果设备会附带换行,可去掉;没有就设 null
//_scanSvc.DebounceMs = 250;
_scanSvc.Suffix = null; // 先关掉
_scanSvc.DebounceMs = 0; // 先关掉
_scanSvc.Suffix = null; // 先关掉
_scanSvc.DebounceMs = 0; // 先关掉
}
protected override async void OnAppearing()
{
base.OnAppearing();
// 动态注册广播接收器(只在当前页面前台时生效)
// 动态注册广播接收器(只在当前页面前台时生效)
_scanSvc.Scanned += OnScanned;
_scanSvc.StartListening();
//键盘输入
//键盘输入
_scanSvc.Attach(OrderEntry);
OrderEntry.Focus();
}
/// <summary>
/// 清空扫描记录
/// 清空扫描记录
/// </summary>
void OnClearClicked(object sender, EventArgs e)
{
... ... @@ -43,7 +44,7 @@ public partial class InboundMaterialSearchPage : ContentPage
protected override void OnDisappearing()
{
// 退出页面即注销(防止多个程序/页面抢处理)
// 退出页面即注销(防止多个程序/页面抢处理)
_scanSvc.Scanned -= OnScanned;
_scanSvc.StopListening();
... ... @@ -54,7 +55,7 @@ public partial class InboundMaterialSearchPage : ContentPage
{
MainThread.BeginInvokeOnMainThread(async () =>
{
// 常见处理:自动填入单号/条码并触发查询或加入明细
// 常见处理:自动填入单号/条码并触发查询或加入明细
_vm.SearchOrderNo = data;
});
}
... ... @@ -63,13 +64,15 @@ public partial class InboundMaterialSearchPage : ContentPage
var item = e.CurrentSelection?.FirstOrDefault() as InboundOrderSummary;
if (item is null) return;
// 二选一:A) 点选跳转到入库页
// 二选一:A) 点选跳转到入库页
await Shell.Current.GoToAsync(
$"{nameof(InboundMaterialPage)}?orderNo={Uri.EscapeDataString(item.OrderNo)}");
// 或 B) 只把单号写到输入框/VM(不跳转)
// 或 B) 只把单号写到输入框/VM(不跳转)
// if (BindingContext is InboundMaterialSearchViewModel vm) vm.SearchOrderNo = item.OrderNo;
((CollectionView)sender).SelectedItem = null; // 清除选中高亮
((CollectionView)sender).SelectedItem = null; // 清除选中高亮
}
}
... ...
... ... @@ -3,10 +3,12 @@ using IndustrialControl.ViewModels;
namespace IndustrialControl.Pages
{
[QueryProperty(nameof(OrderNo), "orderNo")]
public partial class InboundProductionPage : ContentPage
{
private readonly ScanService _scanSvc;
private readonly InboundProductionViewModel _vm;
public string? OrderNo { get; set; }
public InboundProductionPage(InboundProductionViewModel vm, ScanService scanSvc)
{
... ... @@ -14,14 +16,49 @@ namespace IndustrialControl.Pages
BindingContext = vm;
_scanSvc = scanSvc;
_vm = vm;
_scanSvc.Prefix = null; // 例如 "}q" 之类的前缀;没有就留 null
// _scanSvc.Suffix = "\n"; // 如果设备会附带换行,可去掉;没有就设 null
//_scanSvc.DebounceMs = 250;
_scanSvc.Suffix = null; // 先关掉
_scanSvc.DebounceMs = 0; // 先关掉
}
protected override void OnAppearing()
protected override async void OnAppearing()
{
base.OnAppearing();
// 如果带有单号参数,则加载其明细,进入图2/图3的流程
if (!string.IsNullOrWhiteSpace(OrderNo))
{
await _vm.LoadOrderAsync(OrderNo);
}
// 动态注册广播接收器(只在当前页面前台时生效)
_scanSvc.Scanned += OnScanned;
_scanSvc.StartListening();
//键盘输入
_scanSvc.Attach(ScanEntry);
ScanEntry.Focus();
}
protected override void OnDisappearing()
{
// 退出页面即注销(防止多个程序/页面抢处理)
_scanSvc.Scanned -= OnScanned;
_scanSvc.StopListening();
base.OnDisappearing();
}
private void OnScanned(string data, string type)
{
MainThread.BeginInvokeOnMainThread(async () =>
{
// 常见处理:自动填入单号/条码并触发查询或加入明细
_vm.ScanCode = data;
// 你原本的逻辑:若识别到是订单号 → 查询;若是包装码 → 加入列表等
await _vm.HandleScannedAsync(data, type);
});
}
/// <summary>
/// 清空扫描记录
... ... @@ -73,5 +110,7 @@ namespace IndustrialControl.Pages
await DisplayAlert("提示", "入库失败,请检查数据", "确定");
}
}
}
}
... ...
... ... @@ -3,10 +3,12 @@ using IndustrialControl.ViewModels;
namespace IndustrialControl.Pages
{
[QueryProperty(nameof(OrderNo), "orderNo")]
public partial class OutboundFinishedPage : ContentPage
{
private readonly ScanService _scanSvc;
private readonly OutboundFinishedViewModel _vm;
public string? OrderNo { get; set; }
public OutboundFinishedPage(OutboundFinishedViewModel vm, ScanService scanSvc)
{
... ... @@ -14,15 +16,51 @@ namespace IndustrialControl.Pages
BindingContext = vm;
_scanSvc = scanSvc;
_vm = vm;
_scanSvc.Prefix = null; // 例如 "}q" 之类的前缀;没有就留 null
// _scanSvc.Suffix = "\n"; // 如果设备会附带换行,可去掉;没有就设 null
//_scanSvc.DebounceMs = 250;
_scanSvc.Suffix = null; // 先关掉
_scanSvc.DebounceMs = 0; // 先关掉
}
protected override void OnAppearing()
protected override async void OnAppearing()
{
base.OnAppearing();
// 如果带有单号参数,则加载其明细,进入图2/图3的流程
if (!string.IsNullOrWhiteSpace(OrderNo))
{
await _vm.LoadOrderAsync(OrderNo);
}
// 动态注册广播接收器(只在当前页面前台时生效)
_scanSvc.Scanned += OnScanned;
_scanSvc.StartListening();
//键盘输入
_scanSvc.Attach(ScanEntry);
ScanEntry.Focus();
}
protected override void OnDisappearing()
{
// 退出页面即注销(防止多个程序/页面抢处理)
_scanSvc.Scanned -= OnScanned;
_scanSvc.StopListening();
base.OnDisappearing();
}
private void OnScanned(string data, string type)
{
MainThread.BeginInvokeOnMainThread(async () =>
{
// 常见处理:自动填入单号/条码并触发查询或加入明细
_vm.ScanCode = data;
// 你原本的逻辑:若识别到是订单号 → 查询;若是包装码 → 加入列表等
await _vm.HandleScannedAsync(data, type);
});
}
/// <summary>
/// 清空扫描记录
/// </summary>
... ...
... ... @@ -3,10 +3,12 @@ using IndustrialControl.ViewModels;
namespace IndustrialControl.Pages
{
[QueryProperty(nameof(OrderNo), "orderNo")]
public partial class OutboundMaterialPage : ContentPage
{
private readonly ScanService _scanSvc;
private readonly OutboundMaterialViewModel _vm;
public string? OrderNo { get; set; }
public OutboundMaterialPage(OutboundMaterialViewModel vm, ScanService scanSvc)
{
... ... @@ -14,15 +16,49 @@ namespace IndustrialControl.Pages
BindingContext = vm;
_scanSvc = scanSvc;
_vm = vm;
_scanSvc.Prefix = null; // 例如 "}q" 之类的前缀;没有就留 null
// _scanSvc.Suffix = "\n"; // 如果设备会附带换行,可去掉;没有就设 null
//_scanSvc.DebounceMs = 250;
_scanSvc.Suffix = null; // 先关掉
_scanSvc.DebounceMs = 0; // 先关掉
}
protected override void OnAppearing()
protected override async void OnAppearing()
{
base.OnAppearing();
// 如果带有单号参数,则加载其明细,进入图2/图3的流程
if (!string.IsNullOrWhiteSpace(OrderNo))
{
await _vm.LoadOrderAsync(OrderNo);
}
// 动态注册广播接收器(只在当前页面前台时生效)
_scanSvc.Scanned += OnScanned;
_scanSvc.StartListening();
//键盘输入
_scanSvc.Attach(ScanEntry);
ScanEntry.Focus();
}
protected override void OnDisappearing()
{
// 退出页面即注销(防止多个程序/页面抢处理)
_scanSvc.Scanned -= OnScanned;
_scanSvc.StopListening();
base.OnDisappearing();
}
private void OnScanned(string data, string type)
{
MainThread.BeginInvokeOnMainThread(async () =>
{
// 常见处理:自动填入单号/条码并触发查询或加入明细
_vm.ScanCode = data;
// 你原本的逻辑:若识别到是订单号 → 查询;若是包装码 → 加入列表等
await _vm.HandleScannedAsync(data, type);
});
}
// Tab切换
void OnPendingTabClicked(object sender, EventArgs e)
{
... ...
using Microsoft.Maui.Dispatching;
using IndustrialControl.Pages;
namespace IndustrialControl.Services;
public class DialogService : IDialogService
{
private static Page? CurrentPage => Application.Current?.MainPage;
public Task AlertAsync(string title, string message, string accept = "确定") =>
MainThread.InvokeOnMainThreadAsync(() =>
CurrentPage?.DisplayAlert(title, message, accept) ?? Task.CompletedTask);
public Task<bool> ConfirmAsync(string title, string message, string accept = "确定", string cancel = "取消") =>
MainThread.InvokeOnMainThreadAsync(() =>
CurrentPage?.DisplayAlert(title, message, accept, cancel) ?? Task.FromResult(false));
public Task<string?> PromptAsync(string title, string message, string? initial = null, string? placeholder = null,
int? maxLength = null, Keyboard? keyboard = null) =>
MainThread.InvokeOnMainThreadAsync(() =>
CurrentPage?.DisplayPromptAsync(title, message, "确定", "取消", placeholder,
maxLength ?? -1, keyboard ?? Keyboard.Text, initial ?? string.Empty) ?? Task.FromResult<string?>(null));
// ★ 新增:库位选择弹窗
public Task<BinInfo?> SelectBinAsync(string? preselectBinCode = null)
=> BinPickerPage.ShowAsync(preselectBinCode);
}
... ...
namespace IndustrialControl.Services;
public interface IDialogService
{
Task AlertAsync(string title, string message, string accept = "确定");
Task<bool> ConfirmAsync(string title, string message, string accept = "确定", string cancel = "取消");
Task<string?> PromptAsync(string title, string message, string? initial = null, string? placeholder = null,
int? maxLength = null, Keyboard? keyboard = null);
/// 选择库位(返回选中的库位信息;取消返回 null)
Task<BinInfo?> SelectBinAsync(string? preselectBinCode = null);
}
/// 表格右侧每行的数据模型(和你的截图列对应)
public class BinInfo
{
public string BinCode { get; set; } = ""; // 库位编号 A1-101
public string WarehouseName { get; set; } = ""; // 仓库名称 半成品库
public string WarehouseCode { get; set; } = ""; // 仓库编码 FBCPK
public string Layer { get; set; } = ""; // 层数 货架层A1-1
public string Shelf { get; set; } = ""; // 货架 货架A1
public string Zone { get; set; } = ""; // 分区 区域A
public string Status { get; set; } = "-"; // 存货状态
public string ShelfPath { get; set; } = "";
}
... ...
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using IndustrialControl.Services;
using IndustrialControl.Pages;
using System.Collections.ObjectModel;
namespace IndustrialControl.ViewModels;
public partial class BinPickerViewModel : ObservableObject
{
public ObservableCollection<BinTreeNode> Tree { get; } = new();
public ObservableCollection<BinInfo> AllBins { get; } = new();
public ObservableCollection<BinTreeNode> VisibleTree { get; } = new();
private Func<BinInfo, Task>? _onPicked;
private Func<Task>? _onCanceled;
public void SetCloseHandlers(Func<BinInfo, Task> onPicked, Func<Task> onCanceled)
{
_onPicked = onPicked;
_onCanceled = onCanceled;
}
public void Initialize(string? preselectBinCode)
{
BuildMockTree();
BuildMockBins();
// 根/区域默认展开,这样有下级
foreach (var root in Tree) root.IsExpanded = true;
RebuildVisible(); // ★ 初始化可见列表
}
// 展开/收起
[RelayCommand]
private void Toggle(BinTreeNode node)
{
node.IsExpanded = !node.IsExpanded;
RebuildVisible(); // ★ 每次切换都重建
}
// 选中树节点:Level==2 表示“货架” → 弹出库位列表;否则仅展开
[RelayCommand]
private async Task SelectNode(BinTreeNode node)
{
if (node.Level != 2)
{
node.IsExpanded = !node.IsExpanded;
RebuildVisible();
return;
}
var list = AllBins.Where(b => b.ShelfPath.Equals(node.Path, StringComparison.OrdinalIgnoreCase)).ToList();
// ★ 让 BinListPage 在选择后连关自己和父页
var picked = await BinListPage.ShowAsync(list, closeParent: true);
if (picked == null) return;
// 仍把结果回传,完成 BinPickerPage.ShowAsync 的 Task
if (_onPicked != null)
await _onPicked.Invoke(picked);
}
// ★ 递归把“当前应可见”的节点平铺出来
private void RebuildVisible()
{
VisibleTree.Clear();
foreach (var root in Tree)
AddVisible(root);
}
private void AddVisible(BinTreeNode n)
{
VisibleTree.Add(n);
if (!n.IsExpanded) return;
foreach (var c in n.Children)
AddVisible(c);
}
public Task CancelAsync() => _onCanceled?.Invoke() ?? Task.CompletedTask;
// ====== Mock(保持不变,只展示必要部分)======
private void BuildMockTree()
{
Tree.Clear();
var semi = new BinTreeNode("半成品库", 0, "半成品库") { IsExpanded = true };
var semiAreaA = new BinTreeNode("区域A", 1, "半成品库/区域A") { IsExpanded = true };
semiAreaA.Children.Add(new BinTreeNode("货架A1", 2, "半成品库/区域A/货架A1"));
semi.Children.Add(semiAreaA);
Tree.Add(semi);
}
private void BuildMockBins()
{
AllBins.Clear();
AllBins.Add(new BinInfo
{
BinCode = "A1-101",
WarehouseName = "半成品库",
WarehouseCode = "FBCPK",
Layer = "货架层A1-1",
Shelf = "货架A1",
Zone = "区域A",
Status = "-",
ShelfPath = "半成品库/区域A/货架A1"
});
AllBins.Add(new BinInfo
{
BinCode = "A1-102",
WarehouseName = "半成品库",
WarehouseCode = "FBCPK",
Layer = "货架层A1-1",
Shelf = "货架A1",
Zone = "区域A",
Status = "-",
ShelfPath = "半成品库/区域A/货架A1"
});
}
}
// 树节点
public partial class BinTreeNode : ObservableObject
{
public BinTreeNode(string name, int level, string path)
{ Name = name; Level = level; Path = path; Indent = new Thickness(level * 16, 0, 0, 0); }
[ObservableProperty] private string name;
[ObservableProperty] private bool isExpanded;
public int Level { get; }
public string Path { get; }
public Thickness Indent { get; }
public ObservableCollection<BinTreeNode> Children { get; } = new();
public string ToggleIcon => Children.Count == 0 ? "" : (IsExpanded ? "▾" : "▸");
}
... ...
... ... @@ -242,6 +242,17 @@ namespace IndustrialControl.ViewModels
SwitchTab(true);
}
public void SetItemBin(object row, string bin)
{
if (row is OutScannedItem item) { item.Bin = bin; return; }
var barcode = row?.GetType().GetProperty("Barcode")?.GetValue(row)?.ToString();
if (!string.IsNullOrWhiteSpace(barcode))
{
var target = ScannedList.FirstOrDefault(x => x.Barcode == barcode);
if (target != null) target.Bin = bin;
}
}
}
... ...
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
using IndustrialControl.Services;
using System.Threading.Tasks;
using System.Collections.ObjectModel;
namespace IndustrialControl.ViewModels
{
public partial class InboundProductionViewModel : ObservableObject
{
[ObservableProperty] private string? scanCode;
private readonly IWarehouseDataService _warehouseSvc;
public InboundProductionViewModel(IWarehouseDataService warehouseSvc)
... ... @@ -84,6 +84,37 @@ namespace IndustrialControl.ViewModels
return result.Succeeded;
}
public async Task HandleScannedAsync(string data, string symbology)
{
// TODO: 你的规则(判断是订单号/物料码/包装码等)
// 然后:查询接口 / 加入列表 / 自动提交
await Task.CompletedTask;
}
public async Task LoadOrderAsync(string orderNo)
{
// 0) 清空旧数据
ClearAll();
// 1) 基础信息
var order = await _warehouseSvc.GetInboundOrderAsync(orderNo);
OrderNo = order.OrderNo;
try
{
var bins = await _warehouseSvc.ListInboundBinsAsync(orderNo);
AvailableBins.Clear();
foreach (var b in bins) AvailableBins.Add(b);
}
catch
{
// 兜底:本地 mock,避免空集合导致 Picker 无选项
AvailableBins.Clear();
foreach (var b in new[] { "CK1_A201", "CK1_A202", "CK1_A203", "CK1_A204" })
AvailableBins.Add(b);
}
var defaultBin = AvailableBins.FirstOrDefault() ?? "CK1_A201";
}
}
public class ProductionScanItem
... ...
... ... @@ -13,6 +13,7 @@ namespace IndustrialControl.ViewModels
[ObservableProperty] private string deliveryNo;
[ObservableProperty] private string deliveryTime;
[ObservableProperty] private string remark;
[ObservableProperty] private string? scanCode;
#endregion
#region Tab 状态
... ... @@ -121,6 +122,38 @@ namespace IndustrialControl.ViewModels
}
#endregion
public async Task HandleScannedAsync(string data, string symbology)
{
// TODO: 你的规则(判断是订单号/物料码/包装码等)
// 然后:查询接口 / 加入列表 / 自动提交
await Task.CompletedTask;
}
public async Task LoadOrderAsync(string orderNo)
{
// 0) 清空旧数据
ClearAll();
// 1) 基础信息
//var order = await _warehouseSvc.GetInboundOrderAsync(orderNo);
//OrderNo = order.OrderNo;
//try
//{
// var bins = await _warehouseSvc.ListInboundBinsAsync(orderNo);
// AvailableBins.Clear();
// foreach (var b in bins) AvailableBins.Add(b);
//}
//catch
//{
// // 兜底:本地 mock,避免空集合导致 Picker 无选项
// AvailableBins.Clear();
// foreach (var b in new[] { "CK1_A201", "CK1_A202", "CK1_A203", "CK1_A204" })
// AvailableBins.Add(b);
//}
//var defaultBin = AvailableBins.FirstOrDefault() ?? "CK1_A201";
}
}
public class OutPendingItem
... ...
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Linq;
using System.Windows.Input;
using Microsoft.Maui.Graphics;
namespace IndustrialControl.ViewModels
{
... ... @@ -26,13 +21,13 @@ namespace IndustrialControl.ViewModels
[ObservableProperty] private Color pendingTextColor = Colors.White;
[ObservableProperty] private Color scannedTabColor = Colors.White;
[ObservableProperty] private Color scannedTextColor = Colors.Black;
[ObservableProperty] private string? scanCode;
// ===== 列表数据 =====
public ObservableCollection<OutboundPendingItem> PendingList { get; set; } = new();
public ObservableCollection<ScannedItem> ScannedList { get; set; } = new();
// ===== 扫码输入 =====
[ObservableProperty] private string scanCode;
public OutboundMaterialViewModel()
{
... ... @@ -95,6 +90,36 @@ namespace IndustrialControl.ViewModels
await Task.Delay(500); // 模拟网络延迟
return true;
}
public async Task HandleScannedAsync(string data, string symbology)
{
// TODO: 你的规则(判断是订单号/物料码/包装码等)
// 然后:查询接口 / 加入列表 / 自动提交
await Task.CompletedTask;
}
public async Task LoadOrderAsync(string orderNo)
{
// 0) 清空旧数据
ClearAll();
// 1) 基础信息
//var order = await _warehouseSvc.GetInboundOrderAsync(orderNo);
//OrderNo = order.OrderNo;
//try
//{
// var bins = await _warehouseSvc.ListInboundBinsAsync(orderNo);
// AvailableBins.Clear();
// foreach (var b in bins) AvailableBins.Add(b);
//}
//catch
//{
// // 兜底:本地 mock,避免空集合导致 Picker 无选项
// AvailableBins.Clear();
// foreach (var b in new[] { "CK1_A201", "CK1_A202", "CK1_A203", "CK1_A204" })
// AvailableBins.Add(b);
//}
//var defaultBin = AvailableBins.FirstOrDefault() ?? "CK1_A201";
}
}
// 出库单明细数据模型
... ...