作者 李壮

scan function success

... ... @@ -24,7 +24,7 @@
<ApplicationTitle>IndustrialControl</ApplicationTitle>
<!-- App Identifier -->
<ApplicationId>com.companyname.industrialcontrol</ApplicationId>
<ApplicationId>com.ty.industrialcontrol</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
... ... @@ -37,6 +37,24 @@
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
</PropertyGroup>
<!-- Debug 下:关裁剪、关AOT、关链接器;仅安卓生效 -->
<PropertyGroup Condition="'$(Configuration)'=='Debug' and $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
<PublishTrimmed>false</PublishTrimmed>
<RunAOTCompilation>false</RunAOTCompilation>
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
<AndroidLinkMode>None</AndroidLinkMode>
<!-- 旧设备是32位ARM,强制只打 v7a,避免64位/不匹配ABI引起崩溃 -->
<AndroidSupportedAbis>armeabi-v7a</AndroidSupportedAbis>
</PropertyGroup>
<!-- Release 也先保守一点(便于现场调试稳定),确认稳定再逐步打开 -->
<PropertyGroup Condition="'$(Configuration)'=='Release' and $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
<PublishTrimmed>false</PublishTrimmed>
<RunAOTCompilation>false</RunAOTCompilation>
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
<AndroidLinkMode>SdkOnly</AndroidLinkMode>
<AndroidSupportedAbis>armeabi-v7a</AndroidSupportedAbis>
</PropertyGroup>
<ItemGroup>
<!-- App Icon -->
... ...
... ... @@ -18,6 +18,13 @@ public partial class InboundMaterialPage : ContentPage
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 async void OnAppearing()
... ... @@ -29,11 +36,15 @@ public partial class InboundMaterialPage : ContentPage
{
await _vm.LoadOrderAsync(OrderNo);
}
// 动态注册广播接收器(只在当前页面前台时生效)
_scanSvc.Scanned += OnScanned;
_scanSvc.StartListening();
//键盘输入
_scanSvc.Attach(ScanEntry);
ScanEntry.Focus();
}
/// <summary>
/// 清空扫描记录
/// </summary>
... ... @@ -44,12 +55,25 @@ public partial class InboundMaterialPage : ContentPage
ScanEntry.Focus();
}
/// <summary>
/// 预留摄像头扫码
/// </summary>
async void OnScanClicked(object sender, EventArgs e)
protected override void OnDisappearing()
{
// 退出页面即注销(防止多个程序/页面抢处理)
_scanSvc.Scanned -= OnScanned;
_scanSvc.StopListening();
base.OnDisappearing();
}
private void OnScanned(string data, string type)
{
await DisplayAlert("提示", "此按钮预留摄像头扫码;硬件扫描直接扣扳机。", "确定");
MainThread.BeginInvokeOnMainThread(async () =>
{
// 常见处理:自动填入单号/条码并触发查询或加入明细
_vm.ScanCode = data;
// 你原本的逻辑:若识别到是订单号 → 查询;若是包装码 → 加入列表等
await _vm.HandleScannedAsync(data, type);
});
}
... ... @@ -69,4 +93,6 @@ public partial class InboundMaterialPage : ContentPage
await DisplayAlert("提示", "入库失败,请检查数据", "确定");
}
}
}
... ...
... ... @@ -3,7 +3,14 @@
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="仓储管理系统">
<ContentPage.Resources>
<ResourceDictionary>
<!-- 空/非空转布尔:非空 => true(按钮可用) -->
<conv:NullToBoolConverter x:Key="NullToBoolConverter" />
</ResourceDictionary>
</ContentPage.Resources>
<Grid RowDefinitions="Auto,*,Auto" Padding="16" BackgroundColor="#F6F7FB">
... ... @@ -13,8 +20,11 @@
<Entry x:Name="OrderEntry"
Grid.Row="0" Grid.Column="0"
Placeholder="请输入入库单号/包裹条码"
VerticalOptions="Center"
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.ViewModels;
namespace IndustrialControl.Pages;
public partial class InboundMaterialSearchPage : ContentPage
{
public InboundMaterialSearchPage(InboundMaterialSearchViewModel vm)
private readonly ScanService _scanSvc;
private readonly InboundMaterialSearchViewModel _vm;
public InboundMaterialSearchPage(InboundMaterialSearchViewModel vm, ScanService scanSvc)
{
_vm = vm;
BindingContext = vm;
_scanSvc = scanSvc;
InitializeComponent();
var sp = Application.Current?.Handler?.MauiContext?.Services
?? throw new InvalidOperationException("Services not ready");
BindingContext = sp.GetRequiredService<InboundMaterialSearchViewModel>();
// 可选:配置前后缀与防抖
_scanSvc.Prefix = null; // 例如 "}q" 之类的前缀;没有就留 null
// _scanSvc.Suffix = "\n"; // 如果设备会附带换行,可去掉;没有就设 null
//_scanSvc.DebounceMs = 250;
_scanSvc.Suffix = null; // 先关掉
_scanSvc.DebounceMs = 0; // 先关掉
}
protected override async void OnAppearing()
{
base.OnAppearing();
// 动态注册广播接收器(只在当前页面前台时生效)
_scanSvc.Scanned += OnScanned;
_scanSvc.StartListening();
//键盘输入
_scanSvc.Attach(OrderEntry);
OrderEntry.Focus();
}
/// <summary>
/// 清空扫描记录
/// </summary>
void OnClearClicked(object sender, EventArgs e)
{
OrderEntry.Text = string.Empty;
OrderEntry.Focus();
}
protected override void OnDisappearing()
{
// 退出页面即注销(防止多个程序/页面抢处理)
_scanSvc.Scanned -= OnScanned;
_scanSvc.StopListening();
base.OnDisappearing();
}
private void OnScanned(string data, string type)
{
MainThread.BeginInvokeOnMainThread(async () =>
{
// 常见处理:自动填入单号/条码并触发查询或加入明细
_vm.SearchOrderNo = data;
});
}
private async void OnOrderSelected(object sender, SelectionChangedEventArgs e)
{
var item = e.CurrentSelection?.FirstOrDefault() as InboundOrderSummary;
if (item is null) return;
// 导航到入库页并带上单号
await Shell.Current.GoToAsync($"{nameof(InboundMaterialPage)}?orderNo={Uri.EscapeDataString(item.OrderNo)}");
// 二选一:A) 点选跳转到入库页
await Shell.Current.GoToAsync(
$"{nameof(InboundMaterialPage)}?orderNo={Uri.EscapeDataString(item.OrderNo)}");
// 清除选择,避免返回后仍高亮
((CollectionView)sender).SelectedItem = null;
}
// 或 B) 只把单号写到输入框/VM(不跳转)
// if (BindingContext is InboundMaterialSearchViewModel vm) vm.SearchOrderNo = item.OrderNo;
((CollectionView)sender).SelectedItem = null; // 清除选中高亮
}
}
... ...
... ... @@ -13,7 +13,7 @@ public interface IWarehouseDataService
Task<IEnumerable<string>> ListInboundBinsAsync(string orderNo);
// NEW: 查询列表(图1)
Task<IEnumerable<InboundOrderSummary>> ListInboundOrdersAsync(string? fuzzyOrderNo, DateTime? createdAt);
Task<IEnumerable<InboundOrderSummary>> ListInboundOrdersAsync(string? orderNoOrBarcode, DateTime createdDate);
}
public record InboundOrder(string OrderNo, string Supplier, string LinkedNo, int ExpectedQty);
... ... @@ -38,25 +38,29 @@ public class MockWarehouseDataService : IWarehouseDataService
public Task<SimpleOk> ConfirmOutboundFinishedAsync(string orderNo, IEnumerable<ScanItem> items)
=> Task.FromResult(new SimpleOk(true, $"成品出库成功:{items.Count()} 条"));
public Task<IEnumerable<InboundOrderSummary>> ListInboundOrdersAsync(string? fuzzyOrderNo, DateTime? createdAt)
public Task<IEnumerable<InboundOrderSummary>> ListInboundOrdersAsync(string? orderNoOrBarcode, DateTime createdDate)
{
// 模拟几条数据
var today = createdAt ?? DateTime.Today;
var samples = new List<InboundOrderSummary>
{
new InboundOrderSummary("CGD20250302001", "退料入库", "供应商A", today),
new InboundOrderSummary("CGD20250302002", "退库入库", "供应商B", today.AddDays(-1)),
new InboundOrderSummary("CGD20250302003", "采购入库", "供应商C", today.AddDays(-2))
};
// 如果有模糊条件就过滤
if (!string.IsNullOrWhiteSpace(fuzzyOrderNo))
samples = samples
.Where(s => s.OrderNo.Contains(fuzzyOrderNo, StringComparison.OrdinalIgnoreCase))
.ToList();
return Task.FromResult<IEnumerable<InboundOrderSummary>>(samples);
// 简单过滤逻辑(按需改造)
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<string>> ListInboundBinsAsync(string orderNo)
{
var bins = new[] { "CK1_A201", "CK1_A202", "CK1_A203", "CK1_A204", "CK1_B101" };
... ...
using CommunityToolkit.Mvvm.Messaging;
using System;
using Microsoft.Maui.Controls;
namespace IndustrialControl.Services;
#if ANDROID
using Android.Content;
using Android.Util; // ✅ 用于 Log
using IndustrialControl.Droid; // 需要 DynamicScanReceiver
#endif
public record ScanMessage(string Data);
public class ScanService
namespace IndustrialControl.Services
{
public void Publish(string data)
public class ScanService
{
if (string.IsNullOrWhiteSpace(data)) return;
MainThread.BeginInvokeOnMainThread(() =>
WeakReferenceMessenger.Default.Send(new ScanMessage(data.Trim())));
}
public event Action<string, string>? Scanned;
public void Attach(Entry entry)
{
entry.Completed += (s, e) => Publish(((Entry)s!).Text ?? string.Empty);
public string? Prefix { get; set; }
public string? Suffix { get; set; }
public int DebounceMs { get; set; } = 250;
private string? _lastData;
private DateTime _lastAt = DateTime.MinValue;
public const string BroadcastAction = "lc";
public const string DataKey = "data";
public const string TypeKey = "SCAN_BARCODE_TYPE_NAME";
public void Attach(Entry entry)
{
entry.Completed += (s, e) =>
{
var data = entry.Text?.Trim();
if (!string.IsNullOrEmpty(data))
{
#if ANDROID
Log.Info("ScanService", $"[Attach] Entry.Completed -> {data}");
#endif
Scanned?.Invoke(data, "kbd");
entry.Text = string.Empty;
}
};
entry.TextChanged += (s, e) =>
{
if (string.IsNullOrEmpty(e.NewTextValue)) return;
if (e.NewTextValue.EndsWith("\n") || e.NewTextValue.EndsWith("\r"))
{
var data = e.NewTextValue.Trim();
#if ANDROID
Log.Info("ScanService", $"[Attach] Entry.TextChanged -> {data}");
#endif
Scanned?.Invoke(data, "kbd");
entry.Text = string.Empty;
}
};
}
public void Publish(string code, string type = "")
{
#if ANDROID
Log.Info("ScanService", $"[Publish] 模拟扫码 -> {code}, type={type}");
#endif
FilterAndRaise(code, type);
}
public void StartListening()
{
#if ANDROID
Android.Util.Log.Info("ScanService", "[StartListening] ENTER");
if (_receiver != null) return;
_receiver = new DynamicScanReceiver();
_receiver.OnScanned += OnScannedFromPlatform;
_filter = new IntentFilter(BroadcastAction);
Android.App.Application.Context.RegisterReceiver(_receiver, _filter);
Log.Info("ScanService", $"[StartListening] 已注册广播 Action={BroadcastAction}");
#endif
}
public void StopListening()
{
#if ANDROID
if (_receiver == null) return;
try
{
Android.App.Application.Context.UnregisterReceiver(_receiver);
Log.Info("ScanService", "[StopListening] 已注销广播");
}
catch (Exception ex)
{
Log.Warn("ScanService", $"[StopListening] 注销异常: {ex.Message}");
}
_receiver.OnScanned -= OnScannedFromPlatform;
_receiver = null;
_filter = null;
#endif
}
private bool FilterAndRaise(string data, string type)
{
if (!string.IsNullOrEmpty(Prefix) && data.StartsWith(Prefix))
data = data.Substring(Prefix.Length);
if (!string.IsNullOrEmpty(Suffix) && data.EndsWith(Suffix))
data = data.Substring(0, data.Length - Suffix.Length);
var now = DateTime.UtcNow;
if (_lastData == data && (now - _lastAt).TotalMilliseconds < DebounceMs)
{
#if ANDROID
Log.Info("ScanService", $"[FilterAndRaise] 数据去抖: {data}");
#endif
return false;
}
_lastData = data;
_lastAt = now;
#if ANDROID
Log.Info("ScanService", $"[FilterAndRaise] 最终触发 -> {data}, type={type}");
#endif
Scanned?.Invoke(data, type);
return true;
}
#if ANDROID
private DynamicScanReceiver? _receiver;
private IntentFilter? _filter;
private void OnScannedFromPlatform(string data, string type)
{
Log.Info("ScanService", $"[OnScannedFromPlatform] 原始数据 -> {data}, type={type}");
FilterAndRaise(data, type);
}
#endif
}
}
... ...
... ... @@ -27,11 +27,31 @@ public partial class InboundMaterialSearchViewModel : ObservableObject
[RelayCommand]
private async Task SearchAsync()
{
Orders.Clear();
var list = await _dataSvc.ListInboundOrdersAsync(SearchOrderNo, CreatedDate);
foreach (var o in list)
Orders.Add(o);
try
{
var list = await _dataSvc.ListInboundOrdersAsync(SearchOrderNo, CreatedDate);
// ★ 在主线程更新 ObservableCollection,避免看起来“没刷新”
await MainThread.InvokeOnMainThreadAsync(() =>
{
Orders.Clear();
if (list != null)
{
foreach (var o in list)
Orders.Add(o);
}
});
// (排查辅助)无数据时提示一下,确认命令确实执行了
if (list == null || !list.Any())
await Shell.Current.DisplayAlert("提示", "未查询到任何入库单", "确定");
}
catch (Exception ex)
{
await Shell.Current.DisplayAlert("查询失败", ex.Message, "确定");
}
}
// 打开明细(携带 orderNo 导航)
[RelayCommand]
private async Task OpenItemAsync(InboundOrderSummary item)
... ...
... ... @@ -8,6 +8,7 @@ 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>
{
... ... @@ -136,7 +137,7 @@ namespace IndustrialControl.ViewModels
var row = selected[0];
row.Qty += 1;
row.Qty -= 1;
// 确保本行已变绿(如果你的行颜色基于 IsSelected)
if (!row.IsSelected) row.IsSelected = true;
SelectedScanItem = row;
... ... @@ -172,6 +173,12 @@ namespace IndustrialControl.ViewModels
// if (row.Qty == 0) row.IsSelected = false;
}
public async Task HandleScannedAsync(string data, string symbology)
{
// TODO: 你的规则(判断是订单号/物料码/包装码等)
// 然后:查询接口 / 加入列表 / 自动提交
await Task.CompletedTask;
}
private Task ShowTip(string message) =>
Shell.Current?.DisplayAlert("提示", message, "确定") ?? Task.CompletedTask;
... ...