正在显示
27 个修改的文件
包含
886 行增加
和
167 行删除
ApiClient.cs
0 → 100644
| 1 | +using System.Net.Http.Headers; | ||
| 2 | + | ||
| 3 | +namespace IndustrialControl; | ||
| 4 | + | ||
| 5 | +public static class ApiClient | ||
| 6 | +{ | ||
| 7 | + public static readonly HttpClient Instance = new HttpClient(); | ||
| 8 | + | ||
| 9 | + public static void ConfigureBase(string ip, int port) | ||
| 10 | + => Instance.BaseAddress = new Uri($"http://{ip}:{port}"); | ||
| 11 | + | ||
| 12 | + public static void SetBearer(string? token) | ||
| 13 | + { | ||
| 14 | + if (string.IsNullOrWhiteSpace(token)) | ||
| 15 | + Instance.DefaultRequestHeaders.Authorization = null; | ||
| 16 | + else | ||
| 17 | + Instance.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); | ||
| 18 | + } | ||
| 19 | +} |
| @@ -27,7 +27,8 @@ | @@ -27,7 +27,8 @@ | ||
| 27 | </Setter.Value> | 27 | </Setter.Value> |
| 28 | </Setter> | 28 | </Setter> |
| 29 | </Style> | 29 | </Style> |
| 30 | - | 30 | + <converters:NullToBoolConverter x:Key="NullToBoolConverter" xmlns:converters="clr-namespace:IndustrialControl.Converters" /> |
| 31 | + <converters:IntConverter x:Key="IntConverter" xmlns:converters="clr-namespace:IndustrialControl.Converters" /> | ||
| 31 | </ResourceDictionary> | 32 | </ResourceDictionary> |
| 32 | </Application.Resources> | 33 | </Application.Resources> |
| 33 | </Application> | 34 | </Application> |
| 1 | +using Microsoft.Extensions.DependencyInjection; | ||
| 2 | + | ||
| 1 | namespace IndustrialControl; | 3 | namespace IndustrialControl; |
| 2 | 4 | ||
| 3 | public partial class App : Application | 5 | public partial class App : Application |
| 4 | { | 6 | { |
| 5 | - public App(AppShell shell) | 7 | + private readonly IConfigLoader _configLoader; |
| 8 | + public static IServiceProvider? Services { get; set; } | ||
| 9 | + public App(IServiceProvider sp, IConfigLoader configLoader) | ||
| 6 | { | 10 | { |
| 7 | InitializeComponent(); | 11 | InitializeComponent(); |
| 8 | - MainPage = shell; | 12 | + |
| 13 | + _configLoader = configLoader; | ||
| 14 | + // 1) 立刻给 MainPage 一个默认值,避免 null | ||
| 15 | + MainPage = new AppShell(authed: false); // 显示:登录|日志|管理员 | ||
| 16 | + // 启动根据是否已登录选择壳 | ||
| 17 | + _ = InitAsync(); | ||
| 18 | + | ||
| 19 | + } | ||
| 20 | + | ||
| 21 | + protected override async void OnStart() | ||
| 22 | + { | ||
| 23 | + base.OnStart(); | ||
| 24 | + | ||
| 25 | + // 1) 启动时确保配置已覆盖(这里才能 await) | ||
| 26 | + await _configLoader.EnsureLatestAsync(); | ||
| 27 | + | ||
| 28 | + // 2) 判断是否已登录 | ||
| 29 | + var token = await TokenStorage.GetAsync(); | ||
| 30 | + var isLoggedIn = !string.IsNullOrWhiteSpace(token); | ||
| 31 | + } | ||
| 32 | + | ||
| 33 | + private async Task InitAsync() | ||
| 34 | + { | ||
| 35 | + var token = await TokenStorage.LoadAsync(); | ||
| 36 | + if (!string.IsNullOrWhiteSpace(token)) | ||
| 37 | + { | ||
| 38 | + ApiClient.SetBearer(token); | ||
| 39 | + MainThread.BeginInvokeOnMainThread(() => | ||
| 40 | + { | ||
| 41 | + Current.MainPage = new AppShell(authed: true); // 显示:主页|日志|管理员 | ||
| 42 | + }); | ||
| 43 | + } | ||
| 9 | } | 44 | } |
| 45 | + | ||
| 46 | + public static void SwitchToLoggedInShell() => Current.MainPage = new AppShell(true); | ||
| 47 | + public static void SwitchToLoggedOutShell() => Current.MainPage = new AppShell(false); | ||
| 10 | } | 48 | } |
| 1 | using Microsoft.Extensions.DependencyInjection; | 1 | using Microsoft.Extensions.DependencyInjection; |
| 2 | -using Microsoft.Maui.Controls; | ||
| 3 | 2 | ||
| 4 | -namespace IndustrialControl | 3 | +namespace IndustrialControl; |
| 4 | + | ||
| 5 | +public partial class AppShell : Shell | ||
| 5 | { | 6 | { |
| 6 | - public partial class AppShell : Shell | 7 | + private readonly IServiceProvider _sp; |
| 8 | + | ||
| 9 | + public AppShell(bool authed) | ||
| 7 | { | 10 | { |
| 8 | - public AppShell(IServiceProvider sp) | ||
| 9 | - { | ||
| 10 | - InitializeComponent(); | 11 | + InitializeComponent(); |
| 12 | + _sp = App.Services ?? throw new InvalidOperationException("Services not ready"); | ||
| 13 | + Routing.RegisterRoute(nameof(Pages.InboundMaterialSearchPage), typeof(Pages.InboundMaterialSearchPage)); | ||
| 14 | + Routing.RegisterRoute(nameof(Pages.InboundMaterialPage), typeof(Pages.InboundMaterialPage)); | ||
| 15 | + Routing.RegisterRoute(nameof(Pages.InboundProductionPage), typeof(Pages.InboundProductionPage)); | ||
| 16 | + Routing.RegisterRoute(nameof(Pages.OutboundMaterialPage), typeof(Pages.OutboundMaterialPage)); | ||
| 17 | + Routing.RegisterRoute(nameof(Pages.OutboundFinishedPage), typeof(Pages.OutboundFinishedPage)); | ||
| 18 | + BuildTabs(authed); | ||
| 19 | + } | ||
| 11 | 20 | ||
| 12 | - // Tab: 登录 | ||
| 13 | - var login = new ShellContent | ||
| 14 | - { | ||
| 15 | - Title = "登录", | ||
| 16 | - Route = "Login", | ||
| 17 | - ContentTemplate = new DataTemplate(() => sp.GetRequiredService<Pages.LoginPage>()) | ||
| 18 | - }; | 21 | + private void BuildTabs(bool authed) |
| 22 | + { | ||
| 23 | + Items.Clear(); | ||
| 19 | 24 | ||
| 20 | - // Tab: 主页 | ||
| 21 | - var home = new ShellContent | 25 | + var bar = new TabBar(); |
| 26 | + | ||
| 27 | + // 公共页:日志 | ||
| 28 | + bar.Items.Add(new Tab | ||
| 29 | + { | ||
| 30 | + Title = "日志", | ||
| 31 | + Items = | ||
| 32 | + { | ||
| 33 | + new ShellContent | ||
| 22 | { | 34 | { |
| 23 | - Title = "主页", | ||
| 24 | - Route = "Home", | ||
| 25 | - ContentTemplate = new DataTemplate(() => sp.GetRequiredService<Pages.HomePage>()) | ||
| 26 | - }; | 35 | + Route = "Logs", |
| 36 | + ContentTemplate = new DataTemplate(() => _sp.GetRequiredService<Pages.LogsPage>()) | ||
| 37 | + } | ||
| 38 | + } | ||
| 39 | + }); | ||
| 27 | 40 | ||
| 28 | - // Tab: 管理员 | ||
| 29 | - var admin = new ShellContent | 41 | + // 公共页:管理员 |
| 42 | + bar.Items.Add(new Tab | ||
| 43 | + { | ||
| 44 | + Title = "管理员", | ||
| 45 | + Items = | ||
| 46 | + { | ||
| 47 | + new ShellContent | ||
| 30 | { | 48 | { |
| 31 | - Title = "管理员", | ||
| 32 | Route = "Admin", | 49 | Route = "Admin", |
| 33 | - ContentTemplate = new DataTemplate(() => sp.GetRequiredService<Pages.AdminPage>()) | ||
| 34 | - }; | 50 | + ContentTemplate = new DataTemplate(() => _sp.GetRequiredService<Pages.AdminPage>()) |
| 51 | + } | ||
| 52 | + } | ||
| 53 | + }); | ||
| 35 | 54 | ||
| 36 | - // Tab: 日志 | ||
| 37 | - var logs = new ShellContent | 55 | + if (authed) |
| 56 | + { | ||
| 57 | + // 已登录:插入主页到最前 | ||
| 58 | + bar.Items.Insert(0, new Tab | ||
| 38 | { | 59 | { |
| 39 | - Title = "日志", | ||
| 40 | - Route = "Logs", | ||
| 41 | - ContentTemplate = new DataTemplate(() => sp.GetRequiredService<Pages.LogsPage>()) | ||
| 42 | - }; | ||
| 43 | - | ||
| 44 | - var tabBar = new TabBar(); | ||
| 45 | - tabBar.Items.Add(login); | ||
| 46 | - tabBar.Items.Add(home); | ||
| 47 | - tabBar.Items.Add(admin); | ||
| 48 | - tabBar.Items.Add(logs); | 60 | + Title = "主页", |
| 61 | + Items = | ||
| 62 | + { | ||
| 63 | + new ShellContent | ||
| 64 | + { | ||
| 65 | + Route = "Home", | ||
| 66 | + ContentTemplate = new DataTemplate(() => _sp.GetRequiredService<Pages.HomePage>()) | ||
| 67 | + } | ||
| 68 | + } | ||
| 69 | + }); | ||
| 49 | 70 | ||
| 50 | - Items.Add(tabBar); | 71 | + Items.Add(bar); |
| 72 | + _ = GoToAsync("//Home"); | ||
| 73 | + } | ||
| 74 | + else | ||
| 75 | + { | ||
| 76 | + // 未登录:插入登录到最前 | ||
| 77 | + bar.Items.Insert(0, new Tab | ||
| 78 | + { | ||
| 79 | + Title = "登录", | ||
| 80 | + Items = | ||
| 81 | + { | ||
| 82 | + new ShellContent | ||
| 83 | + { | ||
| 84 | + Route = "Login", | ||
| 85 | + ContentTemplate = new DataTemplate(() => _sp.GetRequiredService<Pages.LoginPage>()) | ||
| 86 | + } | ||
| 87 | + } | ||
| 88 | + }); | ||
| 51 | 89 | ||
| 52 | - Routing.RegisterRoute(nameof(Pages.InboundMaterialPage), typeof(Pages.InboundMaterialPage)); | ||
| 53 | - Routing.RegisterRoute(nameof(Pages.InboundProductionPage), typeof(Pages.InboundProductionPage)); | ||
| 54 | - Routing.RegisterRoute(nameof(Pages.OutboundMaterialPage), typeof(Pages.OutboundMaterialPage)); | ||
| 55 | - Routing.RegisterRoute(nameof(Pages.OutboundFinishedPage), typeof(Pages.OutboundFinishedPage)); | 90 | + Items.Add(bar); |
| 91 | + _ = GoToAsync("//Login"); | ||
| 56 | } | 92 | } |
| 57 | } | 93 | } |
| 94 | + | ||
| 58 | } | 95 | } |
ConfigLoader.cs
0 → 100644
| 1 | +using System.Text.Json; | ||
| 2 | +using System.Text.Json.Nodes; | ||
| 3 | + | ||
| 4 | +public interface IConfigLoader | ||
| 5 | +{ | ||
| 6 | + Task EnsureLatestAsync(); | ||
| 7 | + JsonNode Load(); | ||
| 8 | + void Save(JsonNode node); | ||
| 9 | +} | ||
| 10 | + | ||
| 11 | +public class ConfigLoader : IConfigLoader | ||
| 12 | +{ | ||
| 13 | + public Task EnsureLatestAsync() => ConfigLoaderStatic.EnsureConfigIsLatestAsync(); | ||
| 14 | + public JsonNode Load() => ConfigLoaderStatic.Load(); | ||
| 15 | + public void Save(JsonNode node) => ConfigLoaderStatic.Save(node); | ||
| 16 | +} | ||
| 17 | + | ||
| 18 | +public static class ConfigLoaderStatic | ||
| 19 | +{ | ||
| 20 | + private const string FileName = "appconfig.json"; | ||
| 21 | + private static readonly JsonSerializerOptions JsonOpts = new() | ||
| 22 | + { | ||
| 23 | + WriteIndented = true, | ||
| 24 | + PropertyNameCaseInsensitive = true | ||
| 25 | + }; | ||
| 26 | + | ||
| 27 | + /// <summary> | ||
| 28 | + /// 用包内更高版本覆盖本地(不做任何字段保留/合并)。 | ||
| 29 | + /// 首次运行:直接复制包内文件到 AppData。 | ||
| 30 | + /// </summary> | ||
| 31 | + public static async Task EnsureConfigIsLatestAsync() | ||
| 32 | + { | ||
| 33 | + var appDataPath = Path.Combine(FileSystem.AppDataDirectory, FileName); | ||
| 34 | + | ||
| 35 | + // 读包内 | ||
| 36 | + JsonNode pkgNode; | ||
| 37 | + using (var s = await FileSystem.OpenAppPackageFileAsync(FileName)) | ||
| 38 | + using (var reader = new StreamReader(s)) | ||
| 39 | + { | ||
| 40 | + pkgNode = JsonNode.Parse(await reader.ReadToEndAsync())!; | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + // 本地不存在 → 直接落地 | ||
| 44 | + if (!File.Exists(appDataPath)) | ||
| 45 | + { | ||
| 46 | + await File.WriteAllTextAsync(appDataPath, pkgNode.ToJsonString(JsonOpts)); | ||
| 47 | + return; | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + // 读本地 | ||
| 51 | + JsonNode localNode; | ||
| 52 | + using (var fs = new FileStream(appDataPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) | ||
| 53 | + { | ||
| 54 | + localNode = JsonNode.Parse(fs)!; | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + int pkgVer = pkgNode?["schemaVersion"]?.GetValue<int?>() ?? 0; | ||
| 58 | + int localVer = localNode?["schemaVersion"]?.GetValue<int?>() ?? 0; | ||
| 59 | + | ||
| 60 | + // 包内版本更高 → 直接覆盖(整文件替换) | ||
| 61 | + if (pkgVer > localVer) | ||
| 62 | + { | ||
| 63 | + await File.WriteAllTextAsync(appDataPath, pkgNode.ToJsonString(JsonOpts)); | ||
| 64 | + } | ||
| 65 | + // 否则保持本地不动 | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + /// <summary>读取生效配置(AppData)</summary> | ||
| 69 | + public static JsonNode Load() => | ||
| 70 | + JsonNode.Parse(File.ReadAllText(Path.Combine(FileSystem.AppDataDirectory, FileName)))!; | ||
| 71 | + | ||
| 72 | + /// <summary>保存(如果你在设置页手动修改本地配置)</summary> | ||
| 73 | + public static void Save(JsonNode node) => | ||
| 74 | + File.WriteAllText(Path.Combine(FileSystem.AppDataDirectory, FileName), node.ToJsonString(JsonOpts)); | ||
| 75 | +} |
Converters/BoolNegationConverter.cs
0 → 100644
| 1 | +using System.Globalization; | ||
| 2 | + | ||
| 3 | +namespace IndustrialControl.Converters; | ||
| 4 | + | ||
| 5 | +public class BoolNegationConverter : IValueConverter | ||
| 6 | +{ | ||
| 7 | + public object Convert(object value, Type t, object p, CultureInfo c) | ||
| 8 | + => value is bool b ? !b : value; | ||
| 9 | + public object ConvertBack(object value, Type t, object p, CultureInfo c) | ||
| 10 | + => value is bool b ? !b : value; | ||
| 11 | +} | ||
| 12 | + | ||
| 13 | +public class BoolToShowHideTextConverter : IValueConverter | ||
| 14 | +{ | ||
| 15 | + public object Convert(object value, Type t, object p, CultureInfo c) | ||
| 16 | + => value is bool b && b ? "隐藏" : "显示"; | ||
| 17 | + public object ConvertBack(object value, Type t, object p, CultureInfo c) => throw new NotSupportedException(); | ||
| 18 | +} |
Converters/EyeGlyphConverter.cs
0 → 100644
| 1 | +using System.Globalization; | ||
| 2 | + | ||
| 3 | +namespace IndustrialControl.Converters; | ||
| 4 | + | ||
| 5 | +public class EyeGlyphConverter : IValueConverter | ||
| 6 | +{ | ||
| 7 | + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) | ||
| 8 | + { | ||
| 9 | + if (value is bool b && b) | ||
| 10 | + return "\uE70E"; // 眼睛(显示密码) | ||
| 11 | + return "\uEB11"; // 眼睛关闭(隐藏密码) | ||
| 12 | + } | ||
| 13 | + | ||
| 14 | + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) | ||
| 15 | + => throw new NotSupportedException(); | ||
| 16 | +} |
Converters/IntConverter.cs
0 → 100644
| 1 | +using System.Globalization; | ||
| 2 | + | ||
| 3 | +namespace IndustrialControl.Converters; | ||
| 4 | +public class IntConverter : IValueConverter | ||
| 5 | +{ | ||
| 6 | + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) | ||
| 7 | + => value?.ToString() ?? "0"; | ||
| 8 | + | ||
| 9 | + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) | ||
| 10 | + { | ||
| 11 | + var s = value?.ToString(); | ||
| 12 | + return int.TryParse(s, out var n) && n >= 0 ? n : 0; // 非法输入归零;需要可负数就去掉判断 | ||
| 13 | + } | ||
| 14 | +} |
Converters/NullToBoolConverter.cs
0 → 100644
| 1 | +using System.Globalization; | ||
| 2 | + | ||
| 3 | +namespace IndustrialControl.Converters; | ||
| 4 | +public class NullToBoolConverter : IValueConverter | ||
| 5 | +{ | ||
| 6 | + public bool Invert { get; set; } = false; | ||
| 7 | + | ||
| 8 | + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) | ||
| 9 | + => Invert ? value is null : value is not null; | ||
| 10 | + | ||
| 11 | + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) | ||
| 12 | + => throw new NotSupportedException(); | ||
| 13 | +} |
| @@ -54,11 +54,17 @@ | @@ -54,11 +54,17 @@ | ||
| 54 | 54 | ||
| 55 | <!-- Raw Assets (also remove the "Resources\Raw" prefix) --> | 55 | <!-- Raw Assets (also remove the "Resources\Raw" prefix) --> |
| 56 | <MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" /> | 56 | <MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" /> |
| 57 | + <AndroidResource Remove="Platforms\Android\Resources\xml\network_security_config.xml" /> | ||
| 58 | + | ||
| 59 | + <!-- 确保 appconfig.json 被打包到应用包,逻辑名=文件名 --> | ||
| 60 | + <MauiAsset Include="Resources\Raw\appconfig.json" LogicalName="appconfig.json"> | ||
| 61 | + <CopyToOutputDirectory>Always</CopyToOutputDirectory> | ||
| 62 | + </MauiAsset> | ||
| 57 | </ItemGroup> | 63 | </ItemGroup> |
| 58 | 64 | ||
| 59 | <ItemGroup> | 65 | <ItemGroup> |
| 60 | <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" /> | 66 | <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" /> |
| 61 | - <PackageReference Include="Microsoft.Maui.Controls" Version="8.0.0"/> | 67 | + <PackageReference Include="Microsoft.Maui.Controls" Version="8.0.0" /> |
| 62 | <PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="8.0.0" /> | 68 | <PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="8.0.0" /> |
| 63 | <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.1" /> | 69 | <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.1" /> |
| 64 | </ItemGroup> | 70 | </ItemGroup> |
| @@ -76,6 +82,9 @@ | @@ -76,6 +82,9 @@ | ||
| 76 | <MauiXaml Update="Pages\InboundMaterialPage.xaml"> | 82 | <MauiXaml Update="Pages\InboundMaterialPage.xaml"> |
| 77 | <Generator>MSBuild:Compile</Generator> | 83 | <Generator>MSBuild:Compile</Generator> |
| 78 | </MauiXaml> | 84 | </MauiXaml> |
| 85 | + <MauiXaml Update="Pages\InboundMaterialSearchPage.xaml"> | ||
| 86 | + <Generator>MSBuild:Compile</Generator> | ||
| 87 | + </MauiXaml> | ||
| 79 | <MauiXaml Update="Pages\InboundProductionPage.xaml"> | 88 | <MauiXaml Update="Pages\InboundProductionPage.xaml"> |
| 80 | <Generator>MSBuild:Compile</Generator> | 89 | <Generator>MSBuild:Compile</Generator> |
| 81 | </MauiXaml> | 90 | </MauiXaml> |
| @@ -17,6 +17,7 @@ Global | @@ -17,6 +17,7 @@ Global | ||
| 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution | 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution |
| 18 | {A54387CA-553D-4CE0-B86C-08A45497D703}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | 18 | {A54387CA-553D-4CE0-B86C-08A45497D703}.Debug|Any CPU.ActiveCfg = Debug|Any CPU |
| 19 | {A54387CA-553D-4CE0-B86C-08A45497D703}.Debug|Any CPU.Build.0 = Debug|Any CPU | 19 | {A54387CA-553D-4CE0-B86C-08A45497D703}.Debug|Any CPU.Build.0 = Debug|Any CPU |
| 20 | + {A54387CA-553D-4CE0-B86C-08A45497D703}.Debug|Any CPU.Deploy.0 = Debug|Any CPU | ||
| 20 | {A54387CA-553D-4CE0-B86C-08A45497D703}.Debug|x64.ActiveCfg = Debug|Any CPU | 21 | {A54387CA-553D-4CE0-B86C-08A45497D703}.Debug|x64.ActiveCfg = Debug|Any CPU |
| 21 | {A54387CA-553D-4CE0-B86C-08A45497D703}.Debug|x64.Build.0 = Debug|Any CPU | 22 | {A54387CA-553D-4CE0-B86C-08A45497D703}.Debug|x64.Build.0 = Debug|Any CPU |
| 22 | {A54387CA-553D-4CE0-B86C-08A45497D703}.Debug|x86.ActiveCfg = Debug|Any CPU | 23 | {A54387CA-553D-4CE0-B86C-08A45497D703}.Debug|x86.ActiveCfg = Debug|Any CPU |
| @@ -16,9 +16,12 @@ namespace IndustrialControl | @@ -16,9 +16,12 @@ namespace IndustrialControl | ||
| 16 | fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); | 16 | fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); |
| 17 | fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); | 17 | fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); |
| 18 | }); | 18 | }); |
| 19 | +#if DEBUG | ||
| 20 | + builder.Logging.AddDebug(); // ✅ 放在 Build 之前 | ||
| 21 | +#endif | ||
| 19 | builder.Services.AddSingleton<AppShell>(); | 22 | builder.Services.AddSingleton<AppShell>(); |
| 20 | // 注册 ConfigLoader | 23 | // 注册 ConfigLoader |
| 21 | - builder.Services.AddSingleton<Services.ConfigLoader>(); | 24 | + builder.Services.AddSingleton<IConfigLoader, ConfigLoader>(); |
| 22 | builder.Services.AddSingleton<Services.LogService>(); | 25 | builder.Services.AddSingleton<Services.LogService>(); |
| 23 | builder.Services.AddSingleton<IWarehouseDataService, MockWarehouseDataService>(); | 26 | builder.Services.AddSingleton<IWarehouseDataService, MockWarehouseDataService>(); |
| 24 | 27 | ||
| @@ -31,6 +34,7 @@ namespace IndustrialControl | @@ -31,6 +34,7 @@ namespace IndustrialControl | ||
| 31 | builder.Services.AddTransient<ViewModels.HomeViewModel>(); | 34 | builder.Services.AddTransient<ViewModels.HomeViewModel>(); |
| 32 | builder.Services.AddTransient<ViewModels.AdminViewModel>(); | 35 | builder.Services.AddTransient<ViewModels.AdminViewModel>(); |
| 33 | builder.Services.AddTransient<ViewModels.LogsViewModel>(); | 36 | builder.Services.AddTransient<ViewModels.LogsViewModel>(); |
| 37 | + builder.Services.AddTransient<ViewModels.InboundMaterialSearchViewModel>(); | ||
| 34 | builder.Services.AddTransient<ViewModels.InboundMaterialViewModel>(); | 38 | builder.Services.AddTransient<ViewModels.InboundMaterialViewModel>(); |
| 35 | builder.Services.AddTransient<ViewModels.InboundProductionViewModel>(); | 39 | builder.Services.AddTransient<ViewModels.InboundProductionViewModel>(); |
| 36 | builder.Services.AddTransient<ViewModels.OutboundMaterialViewModel>(); | 40 | builder.Services.AddTransient<ViewModels.OutboundMaterialViewModel>(); |
| @@ -43,16 +47,14 @@ namespace IndustrialControl | @@ -43,16 +47,14 @@ namespace IndustrialControl | ||
| 43 | builder.Services.AddTransient<Pages.LogsPage>(); | 47 | builder.Services.AddTransient<Pages.LogsPage>(); |
| 44 | 48 | ||
| 45 | // 注册需要路由的页面 | 49 | // 注册需要路由的页面 |
| 50 | + builder.Services.AddTransient<Pages.InboundMaterialSearchPage>(); | ||
| 46 | builder.Services.AddTransient<Pages.InboundMaterialPage>(); | 51 | builder.Services.AddTransient<Pages.InboundMaterialPage>(); |
| 47 | builder.Services.AddTransient<Pages.InboundProductionPage>(); | 52 | builder.Services.AddTransient<Pages.InboundProductionPage>(); |
| 48 | builder.Services.AddTransient<Pages.OutboundMaterialPage>(); | 53 | builder.Services.AddTransient<Pages.OutboundMaterialPage>(); |
| 49 | builder.Services.AddTransient<Pages.OutboundFinishedPage>(); | 54 | builder.Services.AddTransient<Pages.OutboundFinishedPage>(); |
| 50 | - | ||
| 51 | -#if DEBUG | ||
| 52 | - builder.Logging.AddDebug(); | ||
| 53 | -#endif | ||
| 54 | - | ||
| 55 | - return builder.Build(); | 55 | + var app = builder.Build(); |
| 56 | + App.Services = app.Services; | ||
| 57 | + return app; | ||
| 56 | } | 58 | } |
| 57 | } | 59 | } |
| 58 | } | 60 | } |
| @@ -2,12 +2,12 @@ namespace IndustrialControl.Models; | @@ -2,12 +2,12 @@ namespace IndustrialControl.Models; | ||
| 2 | 2 | ||
| 3 | public class ServerSettings | 3 | public class ServerSettings |
| 4 | { | 4 | { |
| 5 | - public string IpAddress { get; set; } = "192.168.1.100"; | 5 | + public string IpAddress { get; set; } |
| 6 | public int Port { get; set; } = 8080; | 6 | public int Port { get; set; } = 8080; |
| 7 | } | 7 | } |
| 8 | public class ApiEndpoints | 8 | public class ApiEndpoints |
| 9 | { | 9 | { |
| 10 | - public string Login { get; set; } = "/sso/login"; | 10 | + public string Login { get; set; } |
| 11 | } | 11 | } |
| 12 | public class LoggingSettings | 12 | public class LoggingSettings |
| 13 | { | 13 | { |
| @@ -2,6 +2,9 @@ | @@ -2,6 +2,9 @@ | ||
| 2 | xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" | 2 | xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" |
| 3 | x:Class="IndustrialControl.Pages.HomePage" | 3 | x:Class="IndustrialControl.Pages.HomePage" |
| 4 | Title="仓储作业"> | 4 | Title="仓储作业"> |
| 5 | + <ContentPage.ToolbarItems> | ||
| 6 | + <ToolbarItem Text="退出" Clicked="OnLogoutClicked" /> | ||
| 7 | + </ContentPage.ToolbarItems> | ||
| 5 | 8 | ||
| 6 | <VerticalStackLayout Padding="24" Spacing="16"> | 9 | <VerticalStackLayout Padding="24" Spacing="16"> |
| 7 | <Frame Style="{StaticResource BlueCardStyle}"> | 10 | <Frame Style="{StaticResource BlueCardStyle}"> |
| 1 | -namespace IndustrialControl.Pages | 1 | +namespace IndustrialControl.Pages |
| 2 | { | 2 | { |
| 3 | public partial class HomePage : ContentPage | 3 | public partial class HomePage : ContentPage |
| 4 | { | 4 | { |
| @@ -9,7 +9,7 @@ namespace IndustrialControl.Pages | @@ -9,7 +9,7 @@ namespace IndustrialControl.Pages | ||
| 9 | 9 | ||
| 10 | private async void OnInMat(object sender, EventArgs e) | 10 | private async void OnInMat(object sender, EventArgs e) |
| 11 | { | 11 | { |
| 12 | - await Shell.Current.GoToAsync(nameof(InboundMaterialPage)); | 12 | + await Shell.Current.GoToAsync(nameof(InboundMaterialSearchPage)); |
| 13 | } | 13 | } |
| 14 | 14 | ||
| 15 | private async void OnInProd(object sender, EventArgs e) | 15 | private async void OnInProd(object sender, EventArgs e) |
| @@ -26,5 +26,21 @@ namespace IndustrialControl.Pages | @@ -26,5 +26,21 @@ namespace IndustrialControl.Pages | ||
| 26 | { | 26 | { |
| 27 | await Shell.Current.GoToAsync(nameof(OutboundFinishedPage)); | 27 | await Shell.Current.GoToAsync(nameof(OutboundFinishedPage)); |
| 28 | } | 28 | } |
| 29 | + | ||
| 30 | + // 新增:退出登录 | ||
| 31 | + private async void OnLogoutClicked(object? sender, EventArgs e) | ||
| 32 | + { | ||
| 33 | + await TokenStorage.ClearAsync(); // 清除 token | ||
| 34 | + ApiClient.SetBearer(null); // 清空请求头 | ||
| 35 | + | ||
| 36 | + // 切换到未登录的 Shell:显示 登录|日志|管理员 | ||
| 37 | + MainThread.BeginInvokeOnMainThread(() => | ||
| 38 | + { | ||
| 39 | + App.SwitchToLoggedOutShell(); | ||
| 40 | + }); | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + | ||
| 44 | + | ||
| 29 | } | 45 | } |
| 30 | } | 46 | } |
| @@ -23,29 +23,53 @@ | @@ -23,29 +23,53 @@ | ||
| 23 | BackgroundColor="White" | 23 | BackgroundColor="White" |
| 24 | HeightRequest="40" | 24 | HeightRequest="40" |
| 25 | Text="{Binding ScanCode}" /> | 25 | Text="{Binding ScanCode}" /> |
| 26 | - <ImageButton Grid.Column="1" | ||
| 27 | - Source="scan.png" | ||
| 28 | - BackgroundColor="#E6F2FF" | ||
| 29 | - CornerRadius="4" | ||
| 30 | - Padding="10" | ||
| 31 | - Clicked="OnScanClicked"/> | 26 | + |
| 32 | </Grid> | 27 | </Grid> |
| 33 | 28 | ||
| 34 | <!-- 基础信息 --> | 29 | <!-- 基础信息 --> |
| 35 | - <Frame Grid.Row="2" Margin="16,0" Padding="8" BorderColor="#CCCCCC" BackgroundColor="White"> | ||
| 36 | - <Grid RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*,Auto,*" ColumnSpacing="8" RowSpacing="6"> | ||
| 37 | - | ||
| 38 | - <!-- 第一行 --> | ||
| 39 | - <Label Grid.Row="0" Grid.Column="0" Text="入库单号:" FontAttributes="Bold" /> | ||
| 40 | - <Label Grid.Row="0" Grid.Column="1" Text="{Binding OrderNo}" LineBreakMode="TailTruncation" /> | ||
| 41 | - <Label Grid.Row="0" Grid.Column="2" Text="关联到货单号:" FontAttributes="Bold" /> | ||
| 42 | - <Label Grid.Row="0" Grid.Column="3" Text="{Binding LinkedDeliveryNo}" LineBreakMode="TailTruncation" /> | ||
| 43 | - | ||
| 44 | - <!-- 第二行 --> | ||
| 45 | - <Label Grid.Row="1" Grid.Column="0" Text="关联采购单:" FontAttributes="Bold" /> | ||
| 46 | - <Label Grid.Row="1" Grid.Column="1" Text="{Binding LinkedPurchaseNo}" LineBreakMode="TailTruncation" /> | ||
| 47 | - <Label Grid.Row="1" Grid.Column="2" Text="供应商:" FontAttributes="Bold" /> | ||
| 48 | - <Label Grid.Row="1" Grid.Column="3" Text="{Binding Supplier}" LineBreakMode="TailTruncation" /> | 30 | + <Frame Grid.Row="2" Margin="8,0" Padding="8" BorderColor="#CCCCCC" BackgroundColor="White"> |
| 31 | + <Grid RowDefinitions="Auto,Auto" ColumnDefinitions="*,*" ColumnSpacing="16" RowSpacing="6"> | ||
| 32 | + | ||
| 33 | + <!-- 第一行左:入库单号 --> | ||
| 34 | + <Label Grid.Row="0" Grid.Column="0" FontSize="13" LineBreakMode="TailTruncation" MaxLines="1"> | ||
| 35 | + <Label.FormattedText> | ||
| 36 | + <FormattedString> | ||
| 37 | + <Span Text="入库单号:" FontAttributes="Bold"/> | ||
| 38 | + <Span Text="{Binding OrderNo}"/> | ||
| 39 | + </FormattedString> | ||
| 40 | + </Label.FormattedText> | ||
| 41 | + </Label> | ||
| 42 | + | ||
| 43 | + <!-- 第一行右:关联到货单号 --> | ||
| 44 | + <Label Grid.Row="0" Grid.Column="1" FontSize="13" LineBreakMode="TailTruncation" MaxLines="1"> | ||
| 45 | + <Label.FormattedText> | ||
| 46 | + <FormattedString> | ||
| 47 | + <Span Text="关联到货单号:" FontAttributes="Bold"/> | ||
| 48 | + <Span Text="{Binding LinkedDeliveryNo}"/> | ||
| 49 | + </FormattedString> | ||
| 50 | + </Label.FormattedText> | ||
| 51 | + </Label> | ||
| 52 | + | ||
| 53 | + <!-- 第二行左:关联采购单 --> | ||
| 54 | + <Label Grid.Row="1" Grid.Column="0" FontSize="13" LineBreakMode="TailTruncation" MaxLines="1"> | ||
| 55 | + <Label.FormattedText> | ||
| 56 | + <FormattedString> | ||
| 57 | + <Span Text="关联采购单:" FontAttributes="Bold"/> | ||
| 58 | + <Span Text="{Binding LinkedPurchaseNo}"/> | ||
| 59 | + </FormattedString> | ||
| 60 | + </Label.FormattedText> | ||
| 61 | + </Label> | ||
| 62 | + | ||
| 63 | + <!-- 第二行右:供应商 --> | ||
| 64 | + <Label Grid.Row="1" Grid.Column="1" FontSize="13" LineBreakMode="TailTruncation" MaxLines="1"> | ||
| 65 | + <Label.FormattedText> | ||
| 66 | + <FormattedString> | ||
| 67 | + <Span Text="供应商:" FontAttributes="Bold"/> | ||
| 68 | + <Span Text="{Binding Supplier}"/> | ||
| 69 | + </FormattedString> | ||
| 70 | + </Label.FormattedText> | ||
| 71 | + </Label> | ||
| 72 | + | ||
| 49 | </Grid> | 73 | </Grid> |
| 50 | </Frame> | 74 | </Frame> |
| 51 | 75 | ||
| @@ -119,7 +143,8 @@ | @@ -119,7 +143,8 @@ | ||
| 119 | <CollectionView Grid.Row="2" | 143 | <CollectionView Grid.Row="2" |
| 120 | ItemsSource="{Binding ScannedList}" | 144 | ItemsSource="{Binding ScannedList}" |
| 121 | IsVisible="{Binding IsScannedVisible}" | 145 | IsVisible="{Binding IsScannedVisible}" |
| 122 | - SelectionMode="Single"> | 146 | + SelectionMode="Single" |
| 147 | + SelectedItem="{Binding SelectedScanItem, Mode=TwoWay}" > | ||
| 123 | <CollectionView.ItemTemplate> | 148 | <CollectionView.ItemTemplate> |
| 124 | <DataTemplate> | 149 | <DataTemplate> |
| 125 | <Grid ColumnDefinitions="40,*,*,*,*,*" Padding="8" BackgroundColor="White"> | 150 | <Grid ColumnDefinitions="40,*,*,*,*,*" Padding="8" BackgroundColor="White"> |
| @@ -143,15 +168,18 @@ | @@ -143,15 +168,18 @@ | ||
| 143 | <Label Grid.Column="2" Text="{Binding Name}" /> | 168 | <Label Grid.Column="2" Text="{Binding Name}" /> |
| 144 | <Label Grid.Column="3" Text="{Binding Spec}" /> | 169 | <Label Grid.Column="3" Text="{Binding Spec}" /> |
| 145 | 170 | ||
| 146 | - <!-- 改成 Picker --> | 171 | + <!-- 预入库库位:改为下拉,可选 VM.AvailableBins,双向绑到 item.Bin --> |
| 147 | <Picker Grid.Column="4" | 172 | <Picker Grid.Column="4" |
| 148 | - ItemsSource="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.AvailableBins}" | ||
| 149 | - SelectedItem="{Binding Bin, Mode=TwoWay}" | ||
| 150 | - FontSize="14" | ||
| 151 | - Title="请选择" | ||
| 152 | - HorizontalOptions="FillAndExpand" /> | ||
| 153 | - | ||
| 154 | - <Label Grid.Column="5" Text="{Binding Qty}" /> | 173 | + Title="选择库位" |
| 174 | + ItemsSource="{Binding Source={x:Reference Page}, Path=BindingContext.AvailableBins}" | ||
| 175 | + SelectedItem="{Binding Bin, Mode=TwoWay}" /> | ||
| 176 | + | ||
| 177 | + <!-- 已扫描数量:可手动改,用数字键盘;建议加转换器容错 --> | ||
| 178 | + <Entry Grid.Column="5" | ||
| 179 | + Keyboard="Numeric" | ||
| 180 | + HorizontalTextAlignment="Center" | ||
| 181 | + WidthRequest="64" | ||
| 182 | + Text="{Binding Qty, Mode=TwoWay, Converter={StaticResource IntConverter}}" /> | ||
| 155 | </Grid> | 183 | </Grid> |
| 156 | </DataTemplate> | 184 | </DataTemplate> |
| 157 | </CollectionView.ItemTemplate> | 185 | </CollectionView.ItemTemplate> |
| @@ -3,10 +3,14 @@ using IndustrialControl.ViewModels; | @@ -3,10 +3,14 @@ using IndustrialControl.ViewModels; | ||
| 3 | 3 | ||
| 4 | namespace IndustrialControl.Pages; | 4 | namespace IndustrialControl.Pages; |
| 5 | 5 | ||
| 6 | +// NEW: 支持从查询页传参 | ||
| 7 | +[QueryProperty(nameof(OrderNo), "orderNo")] | ||
| 6 | public partial class InboundMaterialPage : ContentPage | 8 | public partial class InboundMaterialPage : ContentPage |
| 7 | { | 9 | { |
| 8 | private readonly ScanService _scanSvc; | 10 | private readonly ScanService _scanSvc; |
| 9 | private readonly InboundMaterialViewModel _vm; | 11 | private readonly InboundMaterialViewModel _vm; |
| 12 | + // NEW: 由 Shell 设置 | ||
| 13 | + public string? OrderNo { get; set; } | ||
| 10 | 14 | ||
| 11 | public InboundMaterialPage(InboundMaterialViewModel vm, ScanService scanSvc) | 15 | public InboundMaterialPage(InboundMaterialViewModel vm, ScanService scanSvc) |
| 12 | { | 16 | { |
| @@ -16,9 +20,16 @@ public partial class InboundMaterialPage : ContentPage | @@ -16,9 +20,16 @@ public partial class InboundMaterialPage : ContentPage | ||
| 16 | _vm = vm; | 20 | _vm = vm; |
| 17 | } | 21 | } |
| 18 | 22 | ||
| 19 | - protected override void OnAppearing() | 23 | + protected override async void OnAppearing() |
| 20 | { | 24 | { |
| 21 | base.OnAppearing(); | 25 | base.OnAppearing(); |
| 26 | + | ||
| 27 | + // 如果带有单号参数,则加载其明细,进入图2/图3的流程 | ||
| 28 | + if (!string.IsNullOrWhiteSpace(OrderNo)) | ||
| 29 | + { | ||
| 30 | + await _vm.LoadOrderAsync(OrderNo); | ||
| 31 | + } | ||
| 32 | + | ||
| 22 | _scanSvc.Attach(ScanEntry); | 33 | _scanSvc.Attach(ScanEntry); |
| 23 | ScanEntry.Focus(); | 34 | ScanEntry.Focus(); |
| 24 | } | 35 | } |
| @@ -41,21 +52,6 @@ public partial class InboundMaterialPage : ContentPage | @@ -41,21 +52,6 @@ public partial class InboundMaterialPage : ContentPage | ||
| 41 | await DisplayAlert("提示", "此按钮预留摄像头扫码;硬件扫描直接扣扳机。", "确定"); | 52 | await DisplayAlert("提示", "此按钮预留摄像头扫码;硬件扫描直接扣扳机。", "确定"); |
| 42 | } | 53 | } |
| 43 | 54 | ||
| 44 | - /// <summary> | ||
| 45 | - /// 扫描通过按钮点击 | ||
| 46 | - /// </summary> | ||
| 47 | - void OnPassScanClicked(object sender, EventArgs e) | ||
| 48 | - { | ||
| 49 | - _vm.PassSelectedScan(); | ||
| 50 | - } | ||
| 51 | - | ||
| 52 | - /// <summary> | ||
| 53 | - /// 取消扫描按钮点击 | ||
| 54 | - /// </summary> | ||
| 55 | - void OnCancelScanClicked(object sender, EventArgs e) | ||
| 56 | - { | ||
| 57 | - _vm.CancelSelectedScan(); | ||
| 58 | - } | ||
| 59 | 55 | ||
| 60 | /// <summary> | 56 | /// <summary> |
| 61 | /// 确认入库按钮点击 | 57 | /// 确认入库按钮点击 |
Pages/InboundMaterialSearchPage.xaml
0 → 100644
| 1 | +<?xml version="1.0" encoding="utf-8" ?> | ||
| 2 | +<ContentPage x:Name="Page" | ||
| 3 | + xmlns="http://schemas.microsoft.com/dotnet/2021/maui" | ||
| 4 | + xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" | ||
| 5 | + x:Class="IndustrialControl.Pages.InboundMaterialSearchPage" | ||
| 6 | + Title="仓储管理系统"> | ||
| 7 | + | ||
| 8 | + <Grid RowDefinitions="Auto,*,Auto" Padding="16" BackgroundColor="#F6F7FB"> | ||
| 9 | + | ||
| 10 | + <!-- 顶部:输入条件(第0行) --> | ||
| 11 | + <VerticalStackLayout Grid.Row="0" Spacing="10"> | ||
| 12 | + <Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto" ColumnSpacing="8" RowSpacing="8"> | ||
| 13 | + <Entry x:Name="OrderEntry" | ||
| 14 | + Grid.Row="0" Grid.Column="0" | ||
| 15 | + Placeholder="请输入入库单号/包裹条码" | ||
| 16 | + Text="{Binding SearchOrderNo}" /> | ||
| 17 | + | ||
| 18 | + <DatePicker Grid.Row="1" Grid.Column="0" | ||
| 19 | + Date="{Binding CreatedDate}" | ||
| 20 | + MinimumDate="2000-01-01" /> | ||
| 21 | + <Button Grid.Row="1" Grid.Column="1" | ||
| 22 | + Text="查询" | ||
| 23 | + Command="{Binding SearchCommand}" /> | ||
| 24 | + </Grid> | ||
| 25 | + </VerticalStackLayout> | ||
| 26 | + | ||
| 27 | + <!-- 中部:结果列表(第1行) --> | ||
| 28 | + <CollectionView Grid.Row="1" | ||
| 29 | + ItemsSource="{Binding Orders}" | ||
| 30 | + SelectionMode="Single" | ||
| 31 | + SelectionChanged="OnOrderSelected"> | ||
| 32 | + <CollectionView.ItemTemplate> | ||
| 33 | + <DataTemplate> | ||
| 34 | + <Frame Margin="0,8,0,0" Padding="12" HasShadow="True" CornerRadius="10"> | ||
| 35 | + <!-- ⭐ 点击整卡片触发命令 --> | ||
| 36 | + <Frame.GestureRecognizers> | ||
| 37 | + <TapGestureRecognizer | ||
| 38 | + Command="{Binding BindingContext.OpenItemCommand, Source={x:Reference Page}}" | ||
| 39 | + CommandParameter="{Binding .}" /> | ||
| 40 | + </Frame.GestureRecognizers> | ||
| 41 | + <Grid RowDefinitions="Auto,Auto,Auto,Auto" | ||
| 42 | + ColumnDefinitions="Auto,*" ColumnSpacing="8"> | ||
| 43 | + <Label Grid.Row="0" Grid.Column="0" Text="入库单号:" FontAttributes="Bold"/> | ||
| 44 | + <Label Grid.Row="0" Grid.Column="1" Text="{Binding OrderNo}"/> | ||
| 45 | + | ||
| 46 | + <Label Grid.Row="1" Grid.Column="0" Text="入库类型:" FontAttributes="Bold"/> | ||
| 47 | + <Label Grid.Row="1" Grid.Column="1" Text="{Binding InboundType}" /> | ||
| 48 | + | ||
| 49 | + <Label Grid.Row="2" Grid.Column="0" Text="供应商:" FontAttributes="Bold"/> | ||
| 50 | + <Label Grid.Row="2" Grid.Column="1" Text="{Binding Supplier}" /> | ||
| 51 | + | ||
| 52 | + <Label Grid.Row="3" Grid.Column="0" Text="创建日期:" FontAttributes="Bold"/> | ||
| 53 | + <Label Grid.Row="3" Grid.Column="1" Text="{Binding CreatedAt, StringFormat='{0:yyyy-M-d}'}"/> | ||
| 54 | + </Grid> | ||
| 55 | + </Frame> | ||
| 56 | + </DataTemplate> | ||
| 57 | + </CollectionView.ItemTemplate> | ||
| 58 | + </CollectionView> | ||
| 59 | + | ||
| 60 | + <!-- 底部:操作(第2行) --> | ||
| 61 | + <Grid Grid.Row="2" ColumnDefinitions="*,Auto" Padding="0,8,0,0"> | ||
| 62 | + <Label Text="{Binding Orders.Count, StringFormat='共 {0} 条'}" | ||
| 63 | + VerticalTextAlignment="Center" /> | ||
| 64 | + <Button Grid.Column="1" | ||
| 65 | + Text="进入入库" | ||
| 66 | + Command="{Binding GoInboundCommand}" | ||
| 67 | + IsEnabled="{Binding SelectedOrder, Converter={StaticResource NullToBoolConverter}}"/> | ||
| 68 | + </Grid> | ||
| 69 | + </Grid> | ||
| 70 | +</ContentPage> |
Pages/InboundMaterialSearchPage.xaml.cs
0 → 100644
| 1 | +using IndustrialControl.ViewModels; | ||
| 2 | + | ||
| 3 | +namespace IndustrialControl.Pages; | ||
| 4 | + | ||
| 5 | +public partial class InboundMaterialSearchPage : ContentPage | ||
| 6 | +{ | ||
| 7 | + public InboundMaterialSearchPage(InboundMaterialSearchViewModel vm) | ||
| 8 | + { | ||
| 9 | + InitializeComponent(); | ||
| 10 | + var sp = Application.Current?.Handler?.MauiContext?.Services | ||
| 11 | + ?? throw new InvalidOperationException("Services not ready"); | ||
| 12 | + BindingContext = sp.GetRequiredService<InboundMaterialSearchViewModel>(); | ||
| 13 | + } | ||
| 14 | + | ||
| 15 | + | ||
| 16 | + private async void OnOrderSelected(object sender, SelectionChangedEventArgs e) | ||
| 17 | + { | ||
| 18 | + var item = e.CurrentSelection?.FirstOrDefault() as InboundOrderSummary; | ||
| 19 | + if (item is null) return; | ||
| 20 | + | ||
| 21 | + // 导航到入库页并带上单号 | ||
| 22 | + await Shell.Current.GoToAsync($"{nameof(InboundMaterialPage)}?orderNo={Uri.EscapeDataString(item.OrderNo)}"); | ||
| 23 | + | ||
| 24 | + // 清除选择,避免返回后仍高亮 | ||
| 25 | + ((CollectionView)sender).SelectedItem = null; | ||
| 26 | + } | ||
| 27 | + | ||
| 28 | +} |
| 1 | <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" | 1 | <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" |
| 2 | xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" | 2 | xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" |
| 3 | - xmlns:pages="clr-namespace:IndustrialControl.Pages" | 3 | + xmlns:conv="clr-namespace:IndustrialControl.Converters" |
| 4 | x:Class="IndustrialControl.Pages.LoginPage" | 4 | x:Class="IndustrialControl.Pages.LoginPage" |
| 5 | Title="登录"> | 5 | Title="登录"> |
| 6 | - <Grid Padding="24" RowDefinitions="Auto,*"> | ||
| 7 | - <VerticalStackLayout Spacing="12"> | ||
| 8 | - <Label Text="联创数智管家" FontAttributes="Bold" FontSize="28"/> | ||
| 9 | - <Entry Placeholder="请输入用户名" Text="{Binding UserName}"/> | ||
| 10 | - <Entry Placeholder="请输入登录密码" IsPassword="True" Text="{Binding Password}"/> | ||
| 11 | - <Button Text="登录" Command="{Binding LoginCommand}"/> | ||
| 12 | - </VerticalStackLayout> | ||
| 13 | - </Grid> | 6 | + |
| 7 | + <ContentPage.Resources> | ||
| 8 | + <ResourceDictionary> | ||
| 9 | + <conv:BoolNegationConverter x:Key="NotBool" /> | ||
| 10 | + <conv:EyeGlyphConverter x:Key="EyeGlyphConverter" /> | ||
| 11 | + </ResourceDictionary> | ||
| 12 | + </ContentPage.Resources> | ||
| 13 | + | ||
| 14 | + <Grid Padding="24" RowDefinitions="Auto,*"> | ||
| 15 | + <VerticalStackLayout Spacing="12"> | ||
| 16 | + | ||
| 17 | + <Label Text="联创数智管家" FontAttributes="Bold" FontSize="28"/> | ||
| 18 | + | ||
| 19 | + <Entry Placeholder="请输入用户名" Text="{Binding UserName}"/> | ||
| 20 | + | ||
| 21 | + <!-- 密码输入 + 眼睛图标 --> | ||
| 22 | + <Grid ColumnDefinitions="*,Auto"> | ||
| 23 | + <Entry | ||
| 24 | + Grid.Column="0" | ||
| 25 | + Placeholder="请输入登录密码" | ||
| 26 | + Text="{Binding Password}" | ||
| 27 | + IsPassword="{Binding ShowPassword, Converter={StaticResource NotBool}}" /> | ||
| 28 | + | ||
| 29 | + <ImageButton Grid.Column="1" | ||
| 30 | + VerticalOptions="Center" | ||
| 31 | + BackgroundColor="Transparent" | ||
| 32 | + Command="{Binding TogglePasswordCommand}"> | ||
| 33 | + <ImageButton.Source> | ||
| 34 | + <FontImageSource Glyph="{Binding ShowPassword, Converter={StaticResource EyeGlyphConverter}}" | ||
| 35 | + FontFamily="MaterialIcons" Color="Gray" Size="20"/> | ||
| 36 | + </ImageButton.Source> | ||
| 37 | + </ImageButton> | ||
| 38 | + </Grid> | ||
| 39 | + | ||
| 40 | + <Button Text="登录" Command="{Binding LoginCommand}"/> | ||
| 41 | + </VerticalStackLayout> | ||
| 42 | + </Grid> | ||
| 14 | </ContentPage> | 43 | </ContentPage> |
| 1 | { | 1 | { |
| 2 | - "schemaVersion": 3, | 2 | + "schemaVersion": 6, |
| 3 | "server": { | 3 | "server": { |
| 4 | - "ipAddress": "192.168.1.100", | ||
| 5 | - "port": 8080 | 4 | + "ipAddress": "114.250.20.230", |
| 5 | + "port": 9128 | ||
| 6 | }, | 6 | }, |
| 7 | "apiEndpoints": { | 7 | "apiEndpoints": { |
| 8 | - "login": "/sso/login" | 8 | + "login": "/normalService/pda/auth/login" |
| 9 | }, | 9 | }, |
| 10 | "logging": { | 10 | "logging": { |
| 11 | "level": "Information" | 11 | "level": "Information" |
| 12 | } | 12 | } |
| 13 | -} | ||
| 13 | +} |
| 1 | +using IndustrialControl.ViewModels; | ||
| 2 | + | ||
| 1 | namespace IndustrialControl.Services; | 3 | namespace IndustrialControl.Services; |
| 2 | 4 | ||
| 3 | public interface IWarehouseDataService | 5 | public interface IWarehouseDataService |
| @@ -8,6 +10,10 @@ public interface IWarehouseDataService | @@ -8,6 +10,10 @@ public interface IWarehouseDataService | ||
| 8 | Task<SimpleOk> ConfirmInboundProductionAsync(string orderNo, IEnumerable<ScanItem> items); | 10 | Task<SimpleOk> ConfirmInboundProductionAsync(string orderNo, IEnumerable<ScanItem> items); |
| 9 | Task<SimpleOk> ConfirmOutboundMaterialAsync(string orderNo, IEnumerable<ScanItem> items); | 11 | Task<SimpleOk> ConfirmOutboundMaterialAsync(string orderNo, IEnumerable<ScanItem> items); |
| 10 | Task<SimpleOk> ConfirmOutboundFinishedAsync(string orderNo, IEnumerable<ScanItem> items); | 12 | Task<SimpleOk> ConfirmOutboundFinishedAsync(string orderNo, IEnumerable<ScanItem> items); |
| 13 | + Task<IEnumerable<string>> ListInboundBinsAsync(string orderNo); | ||
| 14 | + | ||
| 15 | + // NEW: 查询列表(图1) | ||
| 16 | + Task<IEnumerable<InboundOrderSummary>> ListInboundOrdersAsync(string? fuzzyOrderNo, DateTime? createdAt); | ||
| 11 | } | 17 | } |
| 12 | 18 | ||
| 13 | public record InboundOrder(string OrderNo, string Supplier, string LinkedNo, int ExpectedQty); | 19 | public record InboundOrder(string OrderNo, string Supplier, string LinkedNo, int ExpectedQty); |
| @@ -19,7 +25,7 @@ public class MockWarehouseDataService : IWarehouseDataService | @@ -19,7 +25,7 @@ public class MockWarehouseDataService : IWarehouseDataService | ||
| 19 | private readonly Random _rand = new(); | 25 | private readonly Random _rand = new(); |
| 20 | 26 | ||
| 21 | public Task<InboundOrder> GetInboundOrderAsync(string orderNo) | 27 | public Task<InboundOrder> GetInboundOrderAsync(string orderNo) |
| 22 | - => Task.FromResult(new InboundOrder(orderNo, "供应商/客户:XXXX", "关联单:CGD2736273", _rand.Next(10, 80))); | 28 | + => Task.FromResult(new InboundOrder(orderNo, "XXXX", "DHD_23326", _rand.Next(10, 80))); |
| 23 | 29 | ||
| 24 | public Task<SimpleOk> ConfirmInboundAsync(string orderNo, IEnumerable<ScanItem> items) | 30 | public Task<SimpleOk> ConfirmInboundAsync(string orderNo, IEnumerable<ScanItem> items) |
| 25 | => Task.FromResult(new SimpleOk(true, $"入库成功:{items.Count()} 条")); | 31 | => Task.FromResult(new SimpleOk(true, $"入库成功:{items.Count()} 条")); |
| @@ -32,4 +38,30 @@ public class MockWarehouseDataService : IWarehouseDataService | @@ -32,4 +38,30 @@ public class MockWarehouseDataService : IWarehouseDataService | ||
| 32 | 38 | ||
| 33 | public Task<SimpleOk> ConfirmOutboundFinishedAsync(string orderNo, IEnumerable<ScanItem> items) | 39 | public Task<SimpleOk> ConfirmOutboundFinishedAsync(string orderNo, IEnumerable<ScanItem> items) |
| 34 | => Task.FromResult(new SimpleOk(true, $"成品出库成功:{items.Count()} 条")); | 40 | => Task.FromResult(new SimpleOk(true, $"成品出库成功:{items.Count()} 条")); |
| 41 | + public Task<IEnumerable<InboundOrderSummary>> ListInboundOrdersAsync(string? fuzzyOrderNo, DateTime? createdAt) | ||
| 42 | + { | ||
| 43 | + // 模拟几条数据 | ||
| 44 | + var today = createdAt ?? DateTime.Today; | ||
| 45 | + var samples = new List<InboundOrderSummary> | ||
| 46 | + { | ||
| 47 | + new InboundOrderSummary("CGD20250302001", "退料入库", "供应商A", today), | ||
| 48 | + new InboundOrderSummary("CGD20250302002", "退库入库", "供应商B", today.AddDays(-1)), | ||
| 49 | + new InboundOrderSummary("CGD20250302003", "采购入库", "供应商C", today.AddDays(-2)) | ||
| 50 | + }; | ||
| 51 | + | ||
| 52 | + // 如果有模糊条件就过滤 | ||
| 53 | + if (!string.IsNullOrWhiteSpace(fuzzyOrderNo)) | ||
| 54 | + samples = samples | ||
| 55 | + .Where(s => s.OrderNo.Contains(fuzzyOrderNo, StringComparison.OrdinalIgnoreCase)) | ||
| 56 | + .ToList(); | ||
| 57 | + | ||
| 58 | + return Task.FromResult<IEnumerable<InboundOrderSummary>>(samples); | ||
| 59 | + } | ||
| 60 | + public Task<IEnumerable<string>> ListInboundBinsAsync(string orderNo) | ||
| 61 | + { | ||
| 62 | + var bins = new[] { "CK1_A201", "CK1_A202", "CK1_A203", "CK1_A204", "CK1_B101" }; | ||
| 63 | + return Task.FromResult<IEnumerable<string>>(bins); | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + | ||
| 35 | } | 67 | } |
TokenStorage.cs
0 → 100644
| 1 | +using System.Threading.Tasks; | ||
| 2 | +using Microsoft.Maui.Storage; | ||
| 3 | + | ||
| 4 | +namespace IndustrialControl; | ||
| 5 | + | ||
| 6 | +public static class TokenStorage | ||
| 7 | +{ | ||
| 8 | + private const string Key = "auth_token"; | ||
| 9 | + | ||
| 10 | + public static Task SaveAsync(string token) | ||
| 11 | + => SecureStorage.SetAsync(Key, token); | ||
| 12 | + | ||
| 13 | + public static async Task<string?> GetAsync() | ||
| 14 | + { | ||
| 15 | + try | ||
| 16 | + { | ||
| 17 | + return await SecureStorage.GetAsync(Key); | ||
| 18 | + } | ||
| 19 | + catch | ||
| 20 | + { | ||
| 21 | + // 个别设备可能不支持安全存储,兜底返回 null | ||
| 22 | + return null; | ||
| 23 | + } | ||
| 24 | + } | ||
| 25 | + public static Task<string?> LoadAsync() => | ||
| 26 | + SecureStorage.GetAsync(Key); | ||
| 27 | + public static Task ClearAsync() | ||
| 28 | + => SecureStorage.SetAsync(Key, string.Empty); // 清空即可;也可用 Remove | ||
| 29 | + | ||
| 30 | + | ||
| 31 | +} |
| 1 | using CommunityToolkit.Mvvm.ComponentModel; | 1 | using CommunityToolkit.Mvvm.ComponentModel; |
| 2 | using CommunityToolkit.Mvvm.Input; | 2 | using CommunityToolkit.Mvvm.Input; |
| 3 | -using IndustrialControl.Models; | ||
| 4 | -using IndustrialControl.Services; | 3 | +using System.Text.Json.Nodes; |
| 5 | 4 | ||
| 6 | namespace IndustrialControl.ViewModels; | 5 | namespace IndustrialControl.ViewModels; |
| 7 | 6 | ||
| 8 | public partial class AdminViewModel : ObservableObject | 7 | public partial class AdminViewModel : ObservableObject |
| 9 | { | 8 | { |
| 10 | - private readonly ConfigLoader _cfg; | 9 | + private readonly IConfigLoader _cfg; |
| 11 | 10 | ||
| 12 | [ObservableProperty] private int schemaVersion; | 11 | [ObservableProperty] private int schemaVersion; |
| 13 | [ObservableProperty] private string ipAddress = ""; | 12 | [ObservableProperty] private string ipAddress = ""; |
| 14 | [ObservableProperty] private int port; | 13 | [ObservableProperty] private int port; |
| 15 | [ObservableProperty] private string baseUrl = ""; | 14 | [ObservableProperty] private string baseUrl = ""; |
| 16 | 15 | ||
| 17 | - public AdminViewModel(ConfigLoader cfg) | 16 | + public AdminViewModel(IConfigLoader cfg) |
| 18 | { | 17 | { |
| 19 | _cfg = cfg; | 18 | _cfg = cfg; |
| 20 | LoadFromConfig(); | 19 | LoadFromConfig(); |
| 21 | - _cfg.ConfigChanged += () => LoadFromConfig(); | ||
| 22 | } | 20 | } |
| 23 | 21 | ||
| 24 | private void LoadFromConfig() | 22 | private void LoadFromConfig() |
| 25 | { | 23 | { |
| 26 | - var c = _cfg.Current; | ||
| 27 | - SchemaVersion = c.SchemaVersion; | ||
| 28 | - IpAddress = c.Server.IpAddress; | ||
| 29 | - Port = c.Server.Port; | ||
| 30 | - BaseUrl = _cfg.BaseUrl; | 24 | + JsonNode node = _cfg.Load(); |
| 25 | + | ||
| 26 | + SchemaVersion = node["schemaVersion"]?.GetValue<int?>() ?? 0; | ||
| 27 | + | ||
| 28 | + var server = node["server"] as JsonObject ?? new JsonObject(); | ||
| 29 | + IpAddress = server["ipAddress"]?.GetValue<string>() ?? ""; | ||
| 30 | + Port = server["port"]?.GetValue<int?>() ?? 80; | ||
| 31 | + | ||
| 32 | + BaseUrl = $"http://{IpAddress}:{Port}"; | ||
| 31 | } | 33 | } |
| 32 | 34 | ||
| 33 | [RelayCommand] | 35 | [RelayCommand] |
| 34 | - public async Task SaveAsync() | 36 | + public Task SaveAsync() |
| 35 | { | 37 | { |
| 36 | - var c = _cfg.Current; | ||
| 37 | - c.Server.IpAddress = IpAddress.Trim(); | ||
| 38 | - c.Server.Port = Port; | ||
| 39 | - await _cfg.SaveAsync(c); | ||
| 40 | - BaseUrl = _cfg.BaseUrl; | ||
| 41 | - await Shell.Current.DisplayAlert("已保存", "配置已保存,可立即生效。", "确定"); | 38 | + var node = _cfg.Load(); |
| 39 | + | ||
| 40 | + var server = node["server"] as JsonObject ?? new JsonObject(); | ||
| 41 | + server["ipAddress"] = IpAddress.Trim(); | ||
| 42 | + server["port"] = Port; | ||
| 43 | + node["server"] = server; | ||
| 44 | + | ||
| 45 | + _cfg.Save(node); | ||
| 46 | + | ||
| 47 | + BaseUrl = $"http://{IpAddress}:{Port}"; | ||
| 48 | + return Shell.Current.DisplayAlert("已保存", "配置已保存,可立即生效。", "确定"); | ||
| 42 | } | 49 | } |
| 43 | 50 | ||
| 44 | [RelayCommand] | 51 | [RelayCommand] |
| 45 | public async Task ResetToPackageAsync() | 52 | public async Task ResetToPackageAsync() |
| 46 | { | 53 | { |
| 47 | - await _cfg.EnsureConfigIsLatestAsync(); | ||
| 48 | - await _cfg.ReloadAsync(); | 54 | + await _cfg.EnsureLatestAsync(); // 包内 schemaVersion 高则触发合并覆盖 |
| 55 | + LoadFromConfig(); | ||
| 49 | await Shell.Current.DisplayAlert("已重载", "已从包内默认配置重载/合并。", "确定"); | 56 | await Shell.Current.DisplayAlert("已重载", "已从包内默认配置重载/合并。", "确定"); |
| 50 | } | 57 | } |
| 51 | } | 58 | } |
ViewModels/InboundMaterialSearchViewModel.cs
0 → 100644
| 1 | +using CommunityToolkit.Mvvm.ComponentModel; | ||
| 2 | +using CommunityToolkit.Mvvm.Input; | ||
| 3 | +using IndustrialControl.Services; | ||
| 4 | +using System.Collections.ObjectModel; | ||
| 5 | +using System; | ||
| 6 | +using IndustrialControl.Pages; | ||
| 7 | + | ||
| 8 | +namespace IndustrialControl.ViewModels; | ||
| 9 | + | ||
| 10 | +public partial class InboundMaterialSearchViewModel : ObservableObject | ||
| 11 | +{ | ||
| 12 | + private readonly IWarehouseDataService _dataSvc; | ||
| 13 | + [ObservableProperty] private string? searchOrderNo; | ||
| 14 | + [ObservableProperty] private DateTime _createdDate = DateTime.Today; | ||
| 15 | + [ObservableProperty] private InboundOrderSummary? selectedOrder; | ||
| 16 | + | ||
| 17 | + public InboundMaterialSearchViewModel(IWarehouseDataService dataSvc) | ||
| 18 | + { | ||
| 19 | + _dataSvc = dataSvc; | ||
| 20 | + Orders = new ObservableCollection<InboundOrderSummary>(); | ||
| 21 | + } | ||
| 22 | + | ||
| 23 | + | ||
| 24 | + | ||
| 25 | + public ObservableCollection<InboundOrderSummary> Orders { get; } | ||
| 26 | + | ||
| 27 | + [RelayCommand] | ||
| 28 | + private async Task SearchAsync() | ||
| 29 | + { | ||
| 30 | + Orders.Clear(); | ||
| 31 | + var list = await _dataSvc.ListInboundOrdersAsync(SearchOrderNo, CreatedDate); | ||
| 32 | + foreach (var o in list) | ||
| 33 | + Orders.Add(o); | ||
| 34 | + } | ||
| 35 | + // 打开明细(携带 orderNo 导航) | ||
| 36 | + [RelayCommand] | ||
| 37 | + private async Task OpenItemAsync(InboundOrderSummary item) | ||
| 38 | + { | ||
| 39 | + if (item is null) return; | ||
| 40 | + await Shell.Current.GoToAsync( | ||
| 41 | + $"{nameof(InboundMaterialPage)}?orderNo={Uri.EscapeDataString(item.OrderNo)}"); | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + [RelayCommand(CanExecute = nameof(CanGoInbound))] | ||
| 45 | + private async Task GoInboundAsync() | ||
| 46 | + { | ||
| 47 | + if (SelectedOrder == null) return; | ||
| 48 | + // 导航到原“入库明细/扫描”页面,并传入 orderNo | ||
| 49 | + await Shell.Current.GoToAsync($"//InboundMaterial?orderNo={Uri.EscapeDataString(SelectedOrder.OrderNo)}"); | ||
| 50 | + } | ||
| 51 | + public async Task LoadOrderAsync(string orderNo) | ||
| 52 | + { | ||
| 53 | + var order = await _dataSvc.GetInboundOrderAsync(orderNo); | ||
| 54 | + // TODO: 将 order 映射到页面“基础信息”“待入库明细”等绑定源,保持你现有绑定字段/集合不变 | ||
| 55 | + } | ||
| 56 | + private bool CanGoInbound() => SelectedOrder != null; | ||
| 57 | +} | ||
| 58 | + | ||
| 59 | +/// <summary>用于列表显示的精简 DTO</summary> | ||
| 60 | +public record InboundOrderSummary( | ||
| 61 | + string OrderNo, | ||
| 62 | + string InboundType, | ||
| 63 | + string Supplier, | ||
| 64 | + DateTime CreatedAt | ||
| 65 | +); |
| @@ -16,7 +16,11 @@ namespace IndustrialControl.ViewModels | @@ -16,7 +16,11 @@ namespace IndustrialControl.ViewModels | ||
| 16 | "CK1_A203", | 16 | "CK1_A203", |
| 17 | "CK1_A204" | 17 | "CK1_A204" |
| 18 | }; | 18 | }; |
| 19 | + public ObservableCollection<OutScannedItem> ScannedList { get; } | ||
| 20 | + public ObservableCollection<PendingItem> PendingList { get; set; } | ||
| 19 | 21 | ||
| 22 | + [ObservableProperty] | ||
| 23 | + private OutScannedItem? selectedScanItem; | ||
| 20 | 24 | ||
| 21 | public InboundMaterialViewModel(IWarehouseDataService warehouseSvc) | 25 | public InboundMaterialViewModel(IWarehouseDataService warehouseSvc) |
| 22 | { | 26 | { |
| @@ -62,8 +66,7 @@ namespace IndustrialControl.ViewModels | @@ -62,8 +66,7 @@ namespace IndustrialControl.ViewModels | ||
| 62 | [ObservableProperty] private string scannedTextColor = "#333333"; | 66 | [ObservableProperty] private string scannedTextColor = "#333333"; |
| 63 | 67 | ||
| 64 | // 列表数据 | 68 | // 列表数据 |
| 65 | - public ObservableCollection<PendingItem> PendingList { get; set; } | ||
| 66 | - public ObservableCollection<OutScannedItem> ScannedList { get; set; } | 69 | + |
| 67 | 70 | ||
| 68 | // 命令 | 71 | // 命令 |
| 69 | public IRelayCommand ShowPendingCommand { get; } | 72 | public IRelayCommand ShowPendingCommand { get; } |
| @@ -115,34 +118,63 @@ namespace IndustrialControl.ViewModels | @@ -115,34 +118,63 @@ namespace IndustrialControl.ViewModels | ||
| 115 | ScannedList.Clear(); | 118 | ScannedList.Clear(); |
| 116 | } | 119 | } |
| 117 | 120 | ||
| 118 | - /// <summary> | ||
| 119 | - /// 扫描通过逻辑 | ||
| 120 | - /// </summary> | ||
| 121 | - public void PassSelectedScan() | 121 | + [RelayCommand] |
| 122 | + private async Task PassScan() // 绑定到 XAML 的 PassScanCommand | ||
| 122 | { | 123 | { |
| 123 | - foreach (var item in ScannedList) | 124 | + var selected = ScannedList.Where(x => x.IsSelected).ToList(); |
| 125 | + | ||
| 126 | + if (selected.Count == 0) | ||
| 124 | { | 127 | { |
| 125 | - if (item.IsSelected) | ||
| 126 | - { | ||
| 127 | - // 这里可以做业务处理,比如更新状态 | ||
| 128 | - } | 128 | + await ShowTip("请先勾选一行记录。"); |
| 129 | + return; | ||
| 129 | } | 130 | } |
| 131 | + if (selected.Count > 1) | ||
| 132 | + { | ||
| 133 | + await ShowTip("一次只能操作一行,请只勾选一行。"); | ||
| 134 | + return; | ||
| 135 | + } | ||
| 136 | + | ||
| 137 | + var row = selected[0]; | ||
| 138 | + row.Qty += 1; | ||
| 139 | + | ||
| 140 | + // 确保本行已变绿(如果你的行颜色基于 IsSelected) | ||
| 141 | + if (!row.IsSelected) row.IsSelected = true; | ||
| 142 | + SelectedScanItem = row; | ||
| 143 | + | ||
| 130 | } | 144 | } |
| 131 | 145 | ||
| 132 | - /// <summary> | ||
| 133 | - /// 取消扫描逻辑 | ||
| 134 | - /// </summary> | ||
| 135 | - public void CancelSelectedScan() | 146 | + [RelayCommand] |
| 147 | + private async Task CancelScan() // 绑定到 XAML 的 CancelScanCommand | ||
| 136 | { | 148 | { |
| 137 | - for (int i = ScannedList.Count - 1; i >= 0; i--) | 149 | + var selected = ScannedList.Where(x => x.IsSelected).ToList(); |
| 150 | + | ||
| 151 | + if (selected.Count == 0) | ||
| 152 | + { | ||
| 153 | + await ShowTip("请先勾选一行记录。"); | ||
| 154 | + return; | ||
| 155 | + } | ||
| 156 | + if (selected.Count > 1) | ||
| 138 | { | 157 | { |
| 139 | - if (ScannedList[i].IsSelected) | ||
| 140 | - { | ||
| 141 | - ScannedList.RemoveAt(i); | ||
| 142 | - } | 158 | + await ShowTip("一次只能操作一行,请只勾选一行。"); |
| 159 | + return; | ||
| 143 | } | 160 | } |
| 161 | + | ||
| 162 | + var row = selected[0]; | ||
| 163 | + if (row.Qty <= 0) | ||
| 164 | + { | ||
| 165 | + await ShowTip("该行数量已为 0,无法再减少。"); | ||
| 166 | + return; | ||
| 167 | + } | ||
| 168 | + | ||
| 169 | + row.Qty -= 1; | ||
| 170 | + | ||
| 171 | + // 如果希望数量减到 0 时取消高亮,可打开下一行 | ||
| 172 | + // if (row.Qty == 0) row.IsSelected = false; | ||
| 144 | } | 173 | } |
| 145 | 174 | ||
| 175 | + private Task ShowTip(string message) => | ||
| 176 | + Shell.Current?.DisplayAlert("提示", message, "确定") ?? Task.CompletedTask; | ||
| 177 | + | ||
| 146 | /// <summary> | 178 | /// <summary> |
| 147 | /// 确认入库逻辑 | 179 | /// 确认入库逻辑 |
| 148 | /// </summary> | 180 | /// </summary> |
| @@ -156,6 +188,55 @@ namespace IndustrialControl.ViewModels | @@ -156,6 +188,55 @@ namespace IndustrialControl.ViewModels | ||
| 156 | 188 | ||
| 157 | return result.Succeeded; | 189 | return result.Succeeded; |
| 158 | } | 190 | } |
| 191 | + | ||
| 192 | + public async Task LoadOrderAsync(string orderNo) | ||
| 193 | + { | ||
| 194 | + // 0) 清空旧数据 | ||
| 195 | + ClearAll(); | ||
| 196 | + | ||
| 197 | + // 1) 基础信息 | ||
| 198 | + var order = await _warehouseSvc.GetInboundOrderAsync(orderNo); | ||
| 199 | + OrderNo = order.OrderNo; | ||
| 200 | + Supplier = order.Supplier ?? string.Empty; | ||
| 201 | + LinkedDeliveryNo = order.LinkedNo ?? string.Empty; // 到货/交货单 | ||
| 202 | + LinkedPurchaseNo = order is { } ? LinkedPurchaseNo : ""; // 你没有就留空 | ||
| 203 | + | ||
| 204 | + // 2) 预入库库位:从接口取(先 mock),用于行内 Picker | ||
| 205 | + // (你已经在 IWarehouseDataService 里加了 ListInboundBinsAsync) | ||
| 206 | + try | ||
| 207 | + { | ||
| 208 | + var bins = await _warehouseSvc.ListInboundBinsAsync(orderNo); | ||
| 209 | + AvailableBins.Clear(); | ||
| 210 | + foreach (var b in bins) AvailableBins.Add(b); | ||
| 211 | + } | ||
| 212 | + catch | ||
| 213 | + { | ||
| 214 | + // 兜底:本地 mock,避免空集合导致 Picker 无选项 | ||
| 215 | + AvailableBins.Clear(); | ||
| 216 | + foreach (var b in new[] { "CK1_A201", "CK1_A202", "CK1_A203", "CK1_A204" }) | ||
| 217 | + AvailableBins.Add(b); | ||
| 218 | + } | ||
| 219 | + var defaultBin = AvailableBins.FirstOrDefault() ?? "CK1_A201"; | ||
| 220 | + | ||
| 221 | + // 3) 待入库明细:如果暂时没有后端接口,这里先 mock 多行,便于 UI 联调 | ||
| 222 | + PendingList.Clear(); | ||
| 223 | + // 你原来只加了 1 行,这里多给两行方便测试 | ||
| 224 | + PendingList.Add(new PendingItem { Name = "物料X", Spec = "型号X", PendingQty = order.ExpectedQty, Bin = defaultBin, ScannedQty = 0 }); | ||
| 225 | + PendingList.Add(new PendingItem { Name = "物料Y", Spec = "型号Y", PendingQty = Math.Max(10, order.ExpectedQty / 2), Bin = defaultBin, ScannedQty = 0 }); | ||
| 226 | + PendingList.Add(new PendingItem { Name = "物料Z", Spec = "型号Z", PendingQty = 8, Bin = defaultBin, ScannedQty = 0 }); | ||
| 227 | + | ||
| 228 | + // 4) 扫描明细初始化(为空即可;也可以放 1~2 条便于测试) | ||
| 229 | + ScannedList.Clear(); | ||
| 230 | + // 可选:放两条测试数据,验证“库位下拉/数量可编辑” | ||
| 231 | + ScannedList.Add(new OutScannedItem { IsSelected = false, Barcode = "FSC2025060300001", Name = "物料X", Spec = "型号X", Bin = defaultBin, Qty = 1 }); | ||
| 232 | + ScannedList.Add(new OutScannedItem { IsSelected = false, Barcode = "FSC2025060300002", Name = "物料Y", Spec = "型号Y", Bin = defaultBin, Qty = 2 }); | ||
| 233 | + | ||
| 234 | + // 5) 默认展示“待入库明细”页签 | ||
| 235 | + SwitchTab(true); | ||
| 236 | + } | ||
| 237 | + | ||
| 238 | + | ||
| 239 | + | ||
| 159 | } | 240 | } |
| 160 | 241 | ||
| 161 | // 待入库明细模型 | 242 | // 待入库明细模型 |
| @@ -169,14 +250,14 @@ namespace IndustrialControl.ViewModels | @@ -169,14 +250,14 @@ namespace IndustrialControl.ViewModels | ||
| 169 | } | 250 | } |
| 170 | 251 | ||
| 171 | // 扫描明细模型 | 252 | // 扫描明细模型 |
| 172 | - public class OutScannedItem | 253 | + public partial class OutScannedItem : ObservableObject |
| 173 | { | 254 | { |
| 174 | - public bool IsSelected { get; set; } | ||
| 175 | - public string Barcode { get; set; } | ||
| 176 | - public string Name { get; set; } | ||
| 177 | - public string Spec { get; set; } | ||
| 178 | - public string Bin { get; set; } | ||
| 179 | - public int Qty { get; set; } | 255 | + [ObservableProperty] private bool isSelected; |
| 256 | + [ObservableProperty] private string barcode; | ||
| 257 | + [ObservableProperty] private string name; | ||
| 258 | + [ObservableProperty] private string spec; | ||
| 259 | + [ObservableProperty] private string bin; | ||
| 260 | + [ObservableProperty] private int qty; | ||
| 180 | } | 261 | } |
| 181 | 262 | ||
| 182 | } | 263 | } |
| 1 | using CommunityToolkit.Mvvm.ComponentModel; | 1 | using CommunityToolkit.Mvvm.ComponentModel; |
| 2 | using CommunityToolkit.Mvvm.Input; | 2 | using CommunityToolkit.Mvvm.Input; |
| 3 | +using System.Net.Http.Json; | ||
| 4 | +using System.Text.Json; | ||
| 3 | 5 | ||
| 4 | namespace IndustrialControl.ViewModels; | 6 | namespace IndustrialControl.ViewModels; |
| 5 | 7 | ||
| 6 | public partial class LoginViewModel : ObservableObject | 8 | public partial class LoginViewModel : ObservableObject |
| 7 | { | 9 | { |
| 10 | + private readonly IConfigLoader _cfg; | ||
| 11 | + | ||
| 8 | [ObservableProperty] private string userName = string.Empty; | 12 | [ObservableProperty] private string userName = string.Empty; |
| 9 | [ObservableProperty] private string password = string.Empty; | 13 | [ObservableProperty] private string password = string.Empty; |
| 10 | [ObservableProperty] private bool isBusy; | 14 | [ObservableProperty] private bool isBusy; |
| 15 | + [ObservableProperty] private bool showPassword; // false=默认隐藏 | ||
| 16 | + | ||
| 17 | + private static readonly JsonSerializerOptions _json = new() { PropertyNameCaseInsensitive = true }; | ||
| 18 | + | ||
| 19 | + public LoginViewModel(IConfigLoader cfg) | ||
| 20 | + { | ||
| 21 | + _cfg = cfg; | ||
| 22 | + } | ||
| 11 | 23 | ||
| 12 | [RelayCommand] | 24 | [RelayCommand] |
| 13 | public async Task LoginAsync() | 25 | public async Task LoginAsync() |
| 14 | { | 26 | { |
| 15 | - if (IsBusy) return; IsBusy = true; | 27 | + if (IsBusy) return; |
| 28 | + IsBusy = true; | ||
| 29 | + | ||
| 16 | try | 30 | try |
| 17 | { | 31 | { |
| 18 | - await Task.Delay(200); // mock | ||
| 19 | - await Shell.Current.GoToAsync("//Home"); | 32 | + // 启动时已 EnsureLatestAsync,这里只读取 AppData 中已生效配置 |
| 33 | + var cfg = _cfg.Load(); | ||
| 34 | + | ||
| 35 | + var host = cfg["server"]?["ipAddress"]?.GetValue<string>() ?? "127.0.0.1"; | ||
| 36 | + var port = cfg["server"]?["port"]?.GetValue<int?>() ?? 80; | ||
| 37 | + | ||
| 38 | + var path = cfg["apiEndpoints"]?["login"]?.GetValue<string>() ?? "normalService/pda/auth/login"; | ||
| 39 | + if (!path.StartsWith("/")) path = "/" + path; | ||
| 40 | + | ||
| 41 | + // 直接拼绝对 URL,避免修改 HttpClient.BaseAddress 引发异常 | ||
| 42 | + var fullUrl = new Uri($"http://{host}:{port}{path}"); | ||
| 43 | + System.Diagnostics.Debug.WriteLine($"[API] {fullUrl}"); | ||
| 44 | + | ||
| 45 | + if (string.IsNullOrWhiteSpace(UserName) || string.IsNullOrWhiteSpace(Password)) | ||
| 46 | + { | ||
| 47 | + await Application.Current.MainPage.DisplayAlert("提示", "请输入用户名和密码", "确定"); | ||
| 48 | + return; | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + var payload = new { username = UserName, password = Password }; | ||
| 52 | + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); | ||
| 53 | + | ||
| 54 | + var resp = await ApiClient.Instance.PostAsJsonAsync(fullUrl, payload, cts.Token); | ||
| 55 | + | ||
| 56 | + if (!resp.IsSuccessStatusCode) | ||
| 57 | + { | ||
| 58 | + var raw = await resp.Content.ReadAsStringAsync(cts.Token); | ||
| 59 | + await Application.Current.MainPage.DisplayAlert("登录失败", | ||
| 60 | + $"HTTP {(int)resp.StatusCode}: {raw}", "确定"); | ||
| 61 | + return; | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + var result = await resp.Content.ReadFromJsonAsync<ApiResponse<LoginResult>>(_json, cts.Token); | ||
| 65 | + bool ok = (result?.success == true) || (result?.code is 0 or 200); | ||
| 66 | + var token = result?.result?.token; | ||
| 67 | + | ||
| 68 | + if (ok && !string.IsNullOrWhiteSpace(token)) | ||
| 69 | + { | ||
| 70 | + await TokenStorage.SaveAsync(token!); | ||
| 71 | + ApiClient.SetBearer(token); | ||
| 72 | + if (ok && !string.IsNullOrWhiteSpace(token)) | ||
| 73 | + { | ||
| 74 | + await TokenStorage.SaveAsync(token); | ||
| 75 | + ApiClient.SetBearer(token); | ||
| 76 | + | ||
| 77 | + App.SwitchToLoggedInShell(); | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + } | ||
| 81 | + else | ||
| 82 | + { | ||
| 83 | + await Application.Current.MainPage.DisplayAlert("登录失败", result?.message ?? "登录返回无效", "确定"); | ||
| 84 | + } | ||
| 85 | + } | ||
| 86 | + catch (OperationCanceledException) | ||
| 87 | + { | ||
| 88 | + await Application.Current.MainPage.DisplayAlert("超时", "登录请求超时,请检查网络", "确定"); | ||
| 20 | } | 89 | } |
| 21 | catch (Exception ex) | 90 | catch (Exception ex) |
| 22 | { | 91 | { |
| 23 | - await Application.Current.MainPage.DisplayAlert("登录失败", ex.Message, "确定"); | 92 | + await Application.Current.MainPage.DisplayAlert("异常", ex.Message, "确定"); |
| 24 | } | 93 | } |
| 25 | - finally { IsBusy = false; } | 94 | + finally |
| 95 | + { | ||
| 96 | + IsBusy = false; | ||
| 97 | + } | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + | ||
| 101 | + [RelayCommand] | ||
| 102 | + private void TogglePassword() => ShowPassword = !ShowPassword; | ||
| 103 | + | ||
| 104 | + private sealed class ApiResponse<T> | ||
| 105 | + { | ||
| 106 | + public int code { get; set; } | ||
| 107 | + public bool success { get; set; } | ||
| 108 | + public string? message { get; set; } | ||
| 109 | + public T? result { get; set; } | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | + private sealed class LoginResult | ||
| 113 | + { | ||
| 114 | + public string? token { get; set; } | ||
| 115 | + public object? userInfo { get; set; } | ||
| 26 | } | 116 | } |
| 27 | } | 117 | } |
-
请 注册 或 登录 后发表评论