From 296bc2a4b2769b45417b928d5977e996ad5b0a98 Mon Sep 17 00:00:00 2001 From: geht <2947093423@qq.com> Date: Thu, 14 May 2026 11:25:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E6=89=93=E5=8D=B0=E9=A2=84?= =?UTF-8?q?=E8=A7=88=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=96=B0=E5=A2=9E=E7=A6=BB?= =?UTF-8?q?=E7=BA=BF=E6=89=93=E5=8D=B0=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E7=BC=A9=E6=94=BE=E6=8E=A7=E5=88=B6=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E4=BB=A5=E6=8F=90=E5=8D=87=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C?= =?UTF-8?q?=E3=80=82=E4=BC=98=E5=8C=96=E6=89=93=E5=8D=B0=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=87=86=E5=A4=87=E9=80=BB=E8=BE=91=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=AE=9E=E6=97=B6=E9=A2=84=E8=A7=88=E7=BC=A9=E6=94=BE=EF=BC=8C?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E6=89=93=E5=8D=B0=E6=95=88=E6=9E=9C=E7=9A=84?= =?UTF-8?q?=E4=B8=80=E8=87=B4=E6=80=A7=E3=80=82=E5=90=8C=E6=97=B6=EF=BC=8C?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E7=9B=B8=E5=85=B3=E8=A7=86=E5=9B=BE=E5=92=8C?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E4=BB=A5=E5=A2=9E=E5=BC=BA=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E7=9A=84=E5=8F=AF=E7=BB=B4=E6=8A=A4=E6=80=A7=E5=92=8C=E6=89=A9?= =?UTF-8?q?=E5=B1=95=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Service/Print/NativePrintRenderService.cs | 51 ++-- .../RawMaterialCard/RawMaterialCardService.cs | 282 +++++++++++++++-- .../RawMaterialEntryService.cs | 284 ++++++++++++++++-- .../ViewModels/LoginWindowViewModel.cs | 31 ++ .../RawMaterialEntryOperationViewModel.cs | 30 +- .../RawMaterialEntryOperationView.xaml | 106 ++++--- .../RawMaterialEntryOperationView.xaml.cs | 27 +- 7 files changed, 702 insertions(+), 109 deletions(-) diff --git a/yy-admin-master/YY.Admin.Services/Service/Print/NativePrintRenderService.cs b/yy-admin-master/YY.Admin.Services/Service/Print/NativePrintRenderService.cs index a23a203..9c3b64c 100644 --- a/yy-admin-master/YY.Admin.Services/Service/Print/NativePrintRenderService.cs +++ b/yy-admin-master/YY.Admin.Services/Service/Print/NativePrintRenderService.cs @@ -23,7 +23,7 @@ public static class NativePrintRenderService /// 将模板 JSON + 数据对象渲染为完整的可打印 HTML 页面(自包含,无外部依赖)。 /// 屏幕样式与后端前端预览保持一致:深灰底(#525659)+ 白纸居中 + 页间分割线。 /// - public static string RenderToHtml(string templateJson, JsonObject data) + public static string RenderToHtml(string templateJson, JsonObject data, bool enableScreenAutoFit = true) { var schema = JsonNode.Parse(templateJson) ?? throw new ArgumentException("无效模板 JSON"); var page = schema["page"] ?? throw new ArgumentException("模板缺少 page 配置"); @@ -86,29 +86,32 @@ public static class NativePrintRenderService sb.Append(" .qhmes-native-screen-page-sep { display: none !important; }\n"); sb.Append(" }\n"); sb.Append("\n"); - sb.Append("\n"); + if (enableScreenAutoFit) + { + sb.Append("\n"); + } sb.Append("\n\n"); sb.Append($"
\n"); diff --git a/yy-admin-master/YY.Admin.Services/Service/RawMaterialCard/RawMaterialCardService.cs b/yy-admin-master/YY.Admin.Services/Service/RawMaterialCard/RawMaterialCardService.cs index 9c79fb2..c977298 100644 --- a/yy-admin-master/YY.Admin.Services/Service/RawMaterialCard/RawMaterialCardService.cs +++ b/yy-admin-master/YY.Admin.Services/Service/RawMaterialCard/RawMaterialCardService.cs @@ -3,6 +3,7 @@ using System.Net.Http; using System.IO; using System.Text; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Web; using Prism.Events; @@ -20,6 +21,8 @@ public class RawMaterialCardService : IRawMaterialCardService, ISingletonDepende private readonly INetworkMonitor _networkMonitor; private readonly IEventAggregator _eventAggregator; private readonly ILoggerService _logger; + private readonly IPrintBizTemplateBindService _printBizTemplateBindService; + private readonly IPrintTemplateService _printTemplateService; private readonly SemaphoreSlim _syncLock = new(1, 1); private readonly object _cacheLock = new(); private readonly string _pendingOpsFilePath; @@ -39,13 +42,17 @@ public class RawMaterialCardService : IRawMaterialCardService, ISingletonDepende IConfiguration configuration, INetworkMonitor networkMonitor, IEventAggregator eventAggregator, - ILoggerService logger) + ILoggerService logger, + IPrintBizTemplateBindService printBizTemplateBindService, + IPrintTemplateService printTemplateService) { _httpClientFactory = httpClientFactory; _configuration = configuration; _networkMonitor = networkMonitor; _eventAggregator = eventAggregator; _logger = logger; + _printBizTemplateBindService = printBizTemplateBindService; + _printTemplateService = printTemplateService; var appDataDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @@ -66,6 +73,7 @@ public class RawMaterialCardService : IRawMaterialCardService, ISingletonDepende } private const int MaxPendingRetries = 5; + private const string RawMaterialCardTemplateCode = "MES_RAW_MATERIAL_CARD"; private string BaseUrl => (_configuration.GetValue("JeecgIntegration:BaseUrl") ?? "http://localhost:8080/jeecg-boot").TrimEnd('/'); private int DefaultTenantId => (int?)_configuration.GetValue("JeecgIntegration:DefaultTenantId") ?? 1002; @@ -319,35 +327,263 @@ public class RawMaterialCardService : IRawMaterialCardService, ISingletonDepende public async Task<(string templateJson, string printDataJson, string? errorMessage)> PrepareNativePrintAsync(string id, CancellationToken ct = default) { - if (!_networkMonitor.IsOnline) - return (string.Empty, "{}", "当前离线,无法获取打印数据"); + if (_networkMonitor.IsOnline) + { + try + { + var url = $"{BaseUrl}/xslmes/mesXslRawMaterialCard/anon/prepareNativePrint?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}"; + using var client = CreateClient(); + var resp = await client.GetAsync(url, ct).ConfigureAwait(false); + var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + if (!root.TryGetProperty("code", out var codeEl) || codeEl.GetInt32() != 200) + { + var msg = root.TryGetProperty("message", out var msgEl) ? msgEl.GetString() : "未知错误"; + return (string.Empty, "{}", msg ?? "服务端返回错误"); + } + var result = root.GetProperty("result"); + var templateJson = result.TryGetProperty("templateJson", out var tjEl) ? tjEl.GetString() : null; + var printDataJson = result.TryGetProperty("printData", out var pdEl) + ? pdEl.GetRawText() + : "{}"; + if (string.IsNullOrWhiteSpace(templateJson)) + return (string.Empty, "{}", "服务端未返回模板 JSON,请先在「业务打印绑定」中配置原材料卡片"); + return (templateJson!, printDataJson, null); + } + catch (Exception ex) + { + _logger.Warning($"[原材料卡片] 远端准备打印数据失败,回退本地模板渲染 id={id}: {ex.Message}"); + } + } + + return await PrepareLocalNativePrintAsync(id, ct).ConfigureAwait(false); + } + + private async Task<(string templateJson, string printDataJson, string? errorMessage)> PrepareLocalNativePrintAsync(string id, CancellationToken ct) + { + var card = GetCardSnapshotById(id); + if (card == null) + { + return (string.Empty, "{}", "本地未找到原材料卡片,无法离线打印"); + } + + var bindList = _printBizTemplateBindService.GetCached(); + if (bindList.Count == 0) + { + try { bindList = await _printBizTemplateBindService.ListAsync(ct).ConfigureAwait(false); } catch { } + } + var bind = bindList.FirstOrDefault(x => string.Equals(x.TemplateCode, RawMaterialCardTemplateCode, StringComparison.OrdinalIgnoreCase)) + ?? bindList.FirstOrDefault(x => (x.BizName ?? string.Empty).Contains("原材料卡片", StringComparison.OrdinalIgnoreCase)); + if (bind == null) + { + return (string.Empty, "{}", "未找到本地业务打印绑定,请先在线同步「原材料卡片」模板配置"); + } + + var templates = _printTemplateService.GetCached(); + if (templates.Count == 0) + { + try { templates = await _printTemplateService.ListAsync(ct).ConfigureAwait(false); } catch { } + } + var templateCode = string.IsNullOrWhiteSpace(bind.TemplateCode) ? RawMaterialCardTemplateCode : bind.TemplateCode!; + var tpl = templates.FirstOrDefault(t => !string.IsNullOrWhiteSpace(bind.TemplateId) && string.Equals(t.Id, bind.TemplateId, StringComparison.OrdinalIgnoreCase)) + ?? templates.FirstOrDefault(t => string.Equals(t.TemplateCode, templateCode, StringComparison.OrdinalIgnoreCase)); + if (tpl == null || string.IsNullOrWhiteSpace(tpl.TemplateJson)) + { + return (string.Empty, "{}", "本地未找到打印模板,请先在线同步模板后再离线打印"); + } + + var mappingJson = string.IsNullOrWhiteSpace(bind.FieldMappingJson) ? "[]" : bind.FieldMappingJson!; + var printData = BuildPrintDataFromMapping(card, mappingJson, tpl.TemplateJson!); + return (tpl.TemplateJson!, printData.ToJsonString(), null); + } + + private MesXslRawMaterialCard? GetCardSnapshotById(string id) + { + if (string.IsNullOrWhiteSpace(id)) return null; + lock (_cacheLock) + { + var snapshot = ApplyPendingOpsSnapshotUnsafe(_localCache.Select(Clone).ToList()); + return snapshot.FirstOrDefault(c => string.Equals(c.Id, id, StringComparison.OrdinalIgnoreCase)) is { } found + ? Clone(found) + : null; + } + } + + private JsonObject BuildPrintDataFromMapping(T source, string mappingJson, string templateJson) + { + JsonObject printData = new(); + JsonNode? bizRoot = JsonSerializer.SerializeToNode(source, _jsonOpts); + try { - var url = $"{BaseUrl}/xslmes/mesXslRawMaterialCard/anon/prepareNativePrint?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}"; - using var client = CreateClient(); - var resp = await client.GetAsync(url, ct).ConfigureAwait(false); - var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); - using var doc = JsonDocument.Parse(json); - var root = doc.RootElement; - if (!root.TryGetProperty("code", out var codeEl) || codeEl.GetInt32() != 200) + var mappingNode = JsonNode.Parse(mappingJson) as JsonArray; + if (mappingNode != null) { - var msg = root.TryGetProperty("message", out var msgEl) ? msgEl.GetString() : "未知错误"; - return (string.Empty, "{}", msg ?? "服务端返回错误"); + foreach (var rule in mappingNode) + { + if (rule is not JsonObject obj) continue; + var templateField = obj["templateField"]?.GetValue()?.Trim(); + if (string.IsNullOrWhiteSpace(templateField)) continue; + var bizField = obj["bizField"]?.GetValue()?.Trim(); + JsonNode? value = string.IsNullOrWhiteSpace(bizField) + ? JsonValue.Create(string.Empty) + : ResolvePath(bizRoot, bizField!); + SetPath(printData, templateField!, NormalizePrintNodeValue(value)); + } } - var result = root.GetProperty("result"); - var templateJson = result.TryGetProperty("templateJson", out var tjEl) ? tjEl.GetString() : null; - var printDataJson = result.TryGetProperty("printData", out var pdEl) - ? pdEl.GetRawText() - : "{}"; - if (string.IsNullOrWhiteSpace(templateJson)) - return (string.Empty, "{}", "服务端未返回模板 JSON,请先在「业务打印绑定」中配置原材料卡片"); - return (templateJson!, printDataJson, null); } - catch (Exception ex) + catch { - _logger.Warning($"[原材料卡片] 准备打印数据失败 id={id}: {ex.Message}"); - return (string.Empty, "{}", $"获取打印数据失败:{ex.Message}"); + // 映射异常时继续按模板字段补空,避免影响整体打印 } + + try + { + var templateNode = JsonNode.Parse(templateJson); + var bindFields = new HashSet(StringComparer.OrdinalIgnoreCase); + CollectTemplateBindFields(templateNode, bindFields); + foreach (var key in bindFields) + { + if (!HasPath(printData, key)) + { + SetPath(printData, key, JsonValue.Create(string.Empty)!); + } + } + } + catch + { + // 模板结构异常时忽略补空逻辑 + } + + return printData; + } + + private static void CollectTemplateBindFields(JsonNode? node, HashSet fields) + { + if (node == null) return; + if (node is JsonObject obj) + { + if (obj["dataBinding"] is JsonObject db && db["params"] is JsonArray paramArr) + { + foreach (var p in paramArr) + { + var key = p?["key"]?.GetValue()?.Trim(); + if (!string.IsNullOrWhiteSpace(key)) fields.Add(key!); + } + } + + var bindField = obj["bindField"]?.GetValue()?.Trim(); + if (!string.IsNullOrWhiteSpace(bindField)) fields.Add(bindField!); + + if (obj["columns"] is JsonArray cols) + { + foreach (var c in cols) + { + var cBind = c?["bindField"]?.GetValue()?.Trim(); + var cField = c?["field"]?.GetValue()?.Trim(); + if (!string.IsNullOrWhiteSpace(cBind)) fields.Add(cBind!); + else if (!string.IsNullOrWhiteSpace(cField)) fields.Add(cField!); + } + } + + foreach (var kv in obj) + { + CollectTemplateBindFields(kv.Value, fields); + } + return; + } + + if (node is JsonArray arr) + { + foreach (var item in arr) + { + CollectTemplateBindFields(item, fields); + } + } + } + + private static JsonNode? ResolvePath(JsonNode? root, string path) + { + if (root == null || string.IsNullOrWhiteSpace(path)) return null; + var parts = path.Split('.', StringSplitOptions.RemoveEmptyEntries); + JsonNode? current = root; + foreach (var part in parts) + { + if (current == null) return null; + if (current is JsonArray arr) + { + if (int.TryParse(part, out var index)) + { + current = index >= 0 && index < arr.Count ? arr[index] : null; + } + else + { + current = arr.Count > 0 ? arr[0]?[part] : null; + } + } + else + { + current = current[part]; + } + } + return current; + } + + private static bool HasPath(JsonObject root, string path) + { + var parts = path.Split('.', StringSplitOptions.RemoveEmptyEntries); + JsonNode? current = root; + for (int i = 0; i < parts.Length; i++) + { + if (current is not JsonObject obj) return false; + if (!obj.TryGetPropertyValue(parts[i], out current)) return false; + if (i == parts.Length - 1) return true; + } + return false; + } + + private static JsonNode NormalizePrintNodeValue(JsonNode? node) + { + if (node == null) return JsonValue.Create(string.Empty)!; + if (node is JsonValue value) + { + if (value.TryGetValue(out var dt)) + { + return JsonValue.Create(dt.ToString("yyyy-MM-dd HH:mm:ss"))!; + } + if (value.TryGetValue(out var dto)) + { + return JsonValue.Create(dto.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"))!; + } + if (value.TryGetValue(out var text) && !string.IsNullOrWhiteSpace(text)) + { + if ((text.Contains('T') || text.EndsWith("Z", StringComparison.OrdinalIgnoreCase) || text.Contains('+')) + && DateTimeOffset.TryParse(text, out var parsed)) + { + return JsonValue.Create(parsed.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"))!; + } + } + } + return node.DeepClone(); + } + + private static void SetPath(JsonObject target, string path, JsonNode value) + { + var parts = path.Split('.', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0) return; + + JsonObject current = target; + for (int i = 0; i < parts.Length - 1; i++) + { + if (current[parts[i]] is not JsonObject child) + { + child = new JsonObject(); + current[parts[i]] = child; + } + current = child; + } + current[parts[^1]] = value; } public async Task UpdatePriorityAsync(string id, string priorityPickup, CancellationToken ct = default) diff --git a/yy-admin-master/YY.Admin.Services/Service/RawMaterialEntry/RawMaterialEntryService.cs b/yy-admin-master/YY.Admin.Services/Service/RawMaterialEntry/RawMaterialEntryService.cs index f8836a0..37ceef6 100644 --- a/yy-admin-master/YY.Admin.Services/Service/RawMaterialEntry/RawMaterialEntryService.cs +++ b/yy-admin-master/YY.Admin.Services/Service/RawMaterialEntry/RawMaterialEntryService.cs @@ -3,6 +3,7 @@ using System.Net.Http; using System.IO; using System.Text; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Web; using Prism.Events; @@ -20,6 +21,8 @@ public class RawMaterialEntryService : IRawMaterialEntryService, ISingletonDepen private readonly INetworkMonitor _networkMonitor; private readonly IEventAggregator _eventAggregator; private readonly ILoggerService _logger; + private readonly IPrintBizTemplateBindService _printBizTemplateBindService; + private readonly IPrintTemplateService _printTemplateService; private readonly SemaphoreSlim _syncLock = new(1, 1); private readonly object _cacheLock = new(); private readonly string _pendingOpsFilePath; @@ -39,13 +42,17 @@ public class RawMaterialEntryService : IRawMaterialEntryService, ISingletonDepen IConfiguration configuration, INetworkMonitor networkMonitor, IEventAggregator eventAggregator, - ILoggerService logger) + ILoggerService logger, + IPrintBizTemplateBindService printBizTemplateBindService, + IPrintTemplateService printTemplateService) { _httpClientFactory = httpClientFactory; _configuration = configuration; _networkMonitor = networkMonitor; _eventAggregator = eventAggregator; _logger = logger; + _printBizTemplateBindService = printBizTemplateBindService; + _printTemplateService = printTemplateService; var appDataDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @@ -66,6 +73,8 @@ public class RawMaterialEntryService : IRawMaterialEntryService, ISingletonDepen } private const int MaxPendingRetries = 5; + private const string RawMaterialEntryBizCode = "1900000000000000530"; + private const string RawMaterialEntryTemplateCode = "MES_RAW_MATERIAL_ENTRY"; private string BaseUrl => (_configuration.GetValue("JeecgIntegration:BaseUrl") ?? "http://localhost:8080/jeecg-boot").TrimEnd('/'); private int DefaultTenantId => (int?)_configuration.GetValue("JeecgIntegration:DefaultTenantId") ?? 1002; @@ -239,35 +248,264 @@ public class RawMaterialEntryService : IRawMaterialEntryService, ISingletonDepen public async Task<(string templateJson, string printDataJson, string? errorMessage)> PrepareNativePrintAsync(string id, CancellationToken ct = default) { - if (!_networkMonitor.IsOnline) - return (string.Empty, "{}", "当前离线,无法获取打印数据"); + if (_networkMonitor.IsOnline) + { + try + { + var url = $"{BaseUrl}/xslmes/mesXslRawMaterialEntry/anon/prepareNativePrint?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}"; + using var client = CreateClient(); + var resp = await client.GetAsync(url, ct).ConfigureAwait(false); + var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + if (!root.TryGetProperty("code", out var codeEl) || codeEl.GetInt32() != 200) + { + var msg = root.TryGetProperty("message", out var msgEl) ? msgEl.GetString() : "未知错误"; + return (string.Empty, "{}", msg ?? "服务端返回错误"); + } + var result = root.GetProperty("result"); + var templateJson = result.TryGetProperty("templateJson", out var tjEl) ? tjEl.GetString() : null; + var printDataJson = result.TryGetProperty("printData", out var pdEl) + ? pdEl.GetRawText() + : "{}"; + if (string.IsNullOrWhiteSpace(templateJson)) + return (string.Empty, "{}", "服务端未返回模板 JSON,请先在「业务打印绑定」中配置原料入场记录"); + return (templateJson!, printDataJson, null); + } + catch (Exception ex) + { + _logger.Warning($"[原料入场] 远端准备打印数据失败,回退本地模板渲染 id={id}: {ex.Message}"); + } + } + + return await PrepareLocalNativePrintAsync(id, ct).ConfigureAwait(false); + } + + private async Task<(string templateJson, string printDataJson, string? errorMessage)> PrepareLocalNativePrintAsync(string id, CancellationToken ct) + { + var entry = GetEntrySnapshotById(id); + if (entry == null) + { + return (string.Empty, "{}", "本地未找到入场记录,无法离线打印"); + } + + var bindList = _printBizTemplateBindService.GetCached(); + if (bindList.Count == 0) + { + try { bindList = await _printBizTemplateBindService.ListAsync(ct).ConfigureAwait(false); } catch { } + } + var bind = bindList.FirstOrDefault(x => string.Equals(x.BizCode, RawMaterialEntryBizCode, StringComparison.OrdinalIgnoreCase)) + ?? bindList.FirstOrDefault(x => string.Equals(x.TemplateCode, RawMaterialEntryTemplateCode, StringComparison.OrdinalIgnoreCase)) + ?? bindList.FirstOrDefault(x => (x.BizName ?? string.Empty).Contains("原料入场记录", StringComparison.OrdinalIgnoreCase)); + if (bind == null) + { + return (string.Empty, "{}", "未找到本地业务打印绑定,请先在线同步「原料入场记录」模板配置"); + } + + var templates = _printTemplateService.GetCached(); + if (templates.Count == 0) + { + try { templates = await _printTemplateService.ListAsync(ct).ConfigureAwait(false); } catch { } + } + var templateCode = string.IsNullOrWhiteSpace(bind.TemplateCode) ? RawMaterialEntryTemplateCode : bind.TemplateCode!; + var tpl = templates.FirstOrDefault(t => !string.IsNullOrWhiteSpace(bind.TemplateId) && string.Equals(t.Id, bind.TemplateId, StringComparison.OrdinalIgnoreCase)) + ?? templates.FirstOrDefault(t => string.Equals(t.TemplateCode, templateCode, StringComparison.OrdinalIgnoreCase)); + if (tpl == null || string.IsNullOrWhiteSpace(tpl.TemplateJson)) + { + return (string.Empty, "{}", "本地未找到打印模板,请先在线同步模板后再离线打印"); + } + + var mappingJson = string.IsNullOrWhiteSpace(bind.FieldMappingJson) ? "[]" : bind.FieldMappingJson!; + var printData = BuildPrintDataFromMapping(entry, mappingJson, tpl.TemplateJson!); + return (tpl.TemplateJson!, printData.ToJsonString(), null); + } + + private MesXslRawMaterialEntry? GetEntrySnapshotById(string id) + { + if (string.IsNullOrWhiteSpace(id)) return null; + lock (_cacheLock) + { + var snapshot = ApplyPendingOpsSnapshotUnsafe(_localCache.Select(Clone).ToList()); + return snapshot.FirstOrDefault(e => string.Equals(e.Id, id, StringComparison.OrdinalIgnoreCase)) is { } found + ? Clone(found) + : null; + } + } + + private JsonObject BuildPrintDataFromMapping(T source, string mappingJson, string templateJson) + { + JsonObject printData = new(); + JsonNode? bizRoot = JsonSerializer.SerializeToNode(source, _jsonOpts); + try { - var url = $"{BaseUrl}/xslmes/mesXslRawMaterialEntry/anon/prepareNativePrint?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}"; - using var client = CreateClient(); - var resp = await client.GetAsync(url, ct).ConfigureAwait(false); - var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); - using var doc = JsonDocument.Parse(json); - var root = doc.RootElement; - if (!root.TryGetProperty("code", out var codeEl) || codeEl.GetInt32() != 200) + var mappingNode = JsonNode.Parse(mappingJson) as JsonArray; + if (mappingNode != null) { - var msg = root.TryGetProperty("message", out var msgEl) ? msgEl.GetString() : "未知错误"; - return (string.Empty, "{}", msg ?? "服务端返回错误"); + foreach (var rule in mappingNode) + { + if (rule is not JsonObject obj) continue; + var templateField = obj["templateField"]?.GetValue()?.Trim(); + if (string.IsNullOrWhiteSpace(templateField)) continue; + var bizField = obj["bizField"]?.GetValue()?.Trim(); + JsonNode? value = string.IsNullOrWhiteSpace(bizField) + ? JsonValue.Create(string.Empty) + : ResolvePath(bizRoot, bizField!); + SetPath(printData, templateField!, NormalizePrintNodeValue(value)); + } } - var result = root.GetProperty("result"); - var templateJson = result.TryGetProperty("templateJson", out var tjEl) ? tjEl.GetString() : null; - var printDataJson = result.TryGetProperty("printData", out var pdEl) - ? pdEl.GetRawText() - : "{}"; - if (string.IsNullOrWhiteSpace(templateJson)) - return (string.Empty, "{}", "服务端未返回模板 JSON,请先在「业务打印绑定」中配置原料入场记录"); - return (templateJson!, printDataJson, null); } - catch (Exception ex) + catch { - _logger.Warning($"[原料入场] 准备打印数据失败 id={id}: {ex.Message}"); - return (string.Empty, "{}", $"获取打印数据失败:{ex.Message}"); + // 映射异常时继续按模板字段补空,避免影响整体打印 } + + try + { + var templateNode = JsonNode.Parse(templateJson); + var bindFields = new HashSet(StringComparer.OrdinalIgnoreCase); + CollectTemplateBindFields(templateNode, bindFields); + foreach (var key in bindFields) + { + if (!HasPath(printData, key)) + { + SetPath(printData, key, JsonValue.Create(string.Empty)!); + } + } + } + catch + { + // 模板结构异常时忽略补空逻辑 + } + + return printData; + } + + private static void CollectTemplateBindFields(JsonNode? node, HashSet fields) + { + if (node == null) return; + if (node is JsonObject obj) + { + if (obj["dataBinding"] is JsonObject db && db["params"] is JsonArray paramArr) + { + foreach (var p in paramArr) + { + var key = p?["key"]?.GetValue()?.Trim(); + if (!string.IsNullOrWhiteSpace(key)) fields.Add(key!); + } + } + + var bindField = obj["bindField"]?.GetValue()?.Trim(); + if (!string.IsNullOrWhiteSpace(bindField)) fields.Add(bindField!); + + if (obj["columns"] is JsonArray cols) + { + foreach (var c in cols) + { + var cBind = c?["bindField"]?.GetValue()?.Trim(); + var cField = c?["field"]?.GetValue()?.Trim(); + if (!string.IsNullOrWhiteSpace(cBind)) fields.Add(cBind!); + else if (!string.IsNullOrWhiteSpace(cField)) fields.Add(cField!); + } + } + + foreach (var kv in obj) + { + CollectTemplateBindFields(kv.Value, fields); + } + return; + } + + if (node is JsonArray arr) + { + foreach (var item in arr) + { + CollectTemplateBindFields(item, fields); + } + } + } + + private static JsonNode? ResolvePath(JsonNode? root, string path) + { + if (root == null || string.IsNullOrWhiteSpace(path)) return null; + var parts = path.Split('.', StringSplitOptions.RemoveEmptyEntries); + JsonNode? current = root; + foreach (var part in parts) + { + if (current == null) return null; + if (current is JsonArray arr) + { + if (int.TryParse(part, out var index)) + { + current = index >= 0 && index < arr.Count ? arr[index] : null; + } + else + { + current = arr.Count > 0 ? arr[0]?[part] : null; + } + } + else + { + current = current[part]; + } + } + return current; + } + + private static bool HasPath(JsonObject root, string path) + { + var parts = path.Split('.', StringSplitOptions.RemoveEmptyEntries); + JsonNode? current = root; + for (int i = 0; i < parts.Length; i++) + { + if (current is not JsonObject obj) return false; + if (!obj.TryGetPropertyValue(parts[i], out current)) return false; + if (i == parts.Length - 1) return true; + } + return false; + } + + private static JsonNode NormalizePrintNodeValue(JsonNode? node) + { + if (node == null) return JsonValue.Create(string.Empty)!; + if (node is JsonValue value) + { + if (value.TryGetValue(out var dt)) + { + return JsonValue.Create(dt.ToString("yyyy-MM-dd HH:mm:ss"))!; + } + if (value.TryGetValue(out var dto)) + { + return JsonValue.Create(dto.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"))!; + } + if (value.TryGetValue(out var text) && !string.IsNullOrWhiteSpace(text)) + { + if ((text.Contains('T') || text.EndsWith("Z", StringComparison.OrdinalIgnoreCase) || text.Contains('+')) + && DateTimeOffset.TryParse(text, out var parsed)) + { + return JsonValue.Create(parsed.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"))!; + } + } + } + return node.DeepClone(); + } + + private static void SetPath(JsonObject target, string path, JsonNode value) + { + var parts = path.Split('.', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0) return; + + JsonObject current = target; + for (int i = 0; i < parts.Length - 1; i++) + { + if (current[parts[i]] is not JsonObject child) + { + child = new JsonObject(); + current[parts[i]] = child; + } + current = child; + } + current[parts[^1]] = value; } public IReadOnlyList GetCachedSnapshot() diff --git a/yy-admin-master/YY.Admin/ViewModels/LoginWindowViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/LoginWindowViewModel.cs index ac4aede..2ae9053 100644 --- a/yy-admin-master/YY.Admin/ViewModels/LoginWindowViewModel.cs +++ b/yy-admin-master/YY.Admin/ViewModels/LoginWindowViewModel.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Configuration; using YY.Admin.Core.Helper; using YY.Admin.Core.Session; using YY.Admin.FluentValidation; +using YY.Admin.Helper; using YY.Admin.Services; using YY.Admin.Services.Service.Auth; using YY.Admin.Services.Service.Jeecg; @@ -220,6 +221,31 @@ namespace YY.Admin.ViewModels while (!cancellationToken.IsCancellationRequested) { bool connected = false; + var settings = ServerSettingsStore.Load(); + if (settings.DisconnectConnection) + { + try + { + await Application.Current.Dispatcher.InvokeAsync(() => + { + IsBackendConnected = false; + }); + } + catch + { + // 忽略窗口关闭后的调度异常 + } + + try + { + await Task.Delay(TimeSpan.FromSeconds(ConnectivityCheckIntervalSeconds), cancellationToken); + } + catch (OperationCanceledException) + { + break; + } + continue; + } try { // 每轮都重新读取配置,保存服务器设置后可即时生效 @@ -269,6 +295,11 @@ namespace YY.Admin.ViewModels { if (r.Result == ButtonResult.OK) { + var settings = ServerSettingsStore.Load(); + if (settings.DisconnectConnection) + { + IsBackendConnected = false; + } LoginMessage = "服务器配置已保存"; } }); diff --git a/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryOperationViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryOperationViewModel.cs index 7d6f117..c38b8fa 100644 --- a/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryOperationViewModel.cs +++ b/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryOperationViewModel.cs @@ -108,6 +108,9 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView public DelegateCommand ResplitCommand { get; } public DelegateCommand SaveAndPrintCommand { get; } public DelegateCommand RefreshPrintersCommand { get; } + public DelegateCommand ZoomOutPrintPreviewCommand { get; } + public DelegateCommand ZoomInPrintPreviewCommand { get; } + public DelegateCommand ResetPrintPreviewZoomCommand { get; } /// PrintDot 桥接器返回的打印机列表(与打印模板页一致)。 public ObservableCollection Printers { get; } = new(); @@ -169,6 +172,27 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView private set => SetProperty(ref _printPreviewStatus, value); } + private const double PrintPreviewMinZoom = 0.5d; + private const double PrintPreviewMaxZoom = 2.0d; + private const double PrintPreviewDefaultZoom = 0.7d; + private const double PrintPreviewZoomStep = 0.1d; + + private double _printPreviewZoomFactor = PrintPreviewDefaultZoom; + /// 打印预览缩放倍率(WebView2 ZoomFactor)。 + public double PrintPreviewZoomFactor + { + get => _printPreviewZoomFactor; + set + { + var clamped = Math.Round(Math.Clamp(value, PrintPreviewMinZoom, PrintPreviewMaxZoom), 2); + if (!SetProperty(ref _printPreviewZoomFactor, clamped)) return; + RaisePropertyChanged(nameof(PrintPreviewZoomText)); + } + } + + /// 打印预览缩放显示文本(百分比)。 + public string PrintPreviewZoomText => $"{Math.Round(PrintPreviewZoomFactor * 100):0}%"; + /// 由 View 订阅,在 UI 线程将 HTML 交给 WebView2。 public event EventHandler? PrintPreviewHtmlReady; @@ -220,6 +244,9 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView .ObservesProperty(() => Entry); SaveAndPrintCommand = new DelegateCommand(async () => await SaveAndPrintAsync()); RefreshPrintersCommand = new DelegateCommand(async () => await RefreshPrintersAsync(verbose: true)); + ZoomOutPrintPreviewCommand = new DelegateCommand(() => PrintPreviewZoomFactor -= PrintPreviewZoomStep); + ZoomInPrintPreviewCommand = new DelegateCommand(() => PrintPreviewZoomFactor += PrintPreviewZoomStep); + ResetPrintPreviewZoomCommand = new DelegateCommand(() => PrintPreviewZoomFactor = PrintPreviewDefaultZoom); // 集合变化:批量重订阅 item.PropertyChanged 监听 HasCard/Portions,并同步刷新两个 Can*。 SplitCodeDetails.CollectionChanged += OnSplitCodeDetailsCollectionChangedForCanFlags; _ = RefreshPrintersAsync(verbose: false); @@ -808,7 +835,8 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView string html; try { - html = NativePrintRenderService.RenderToHtml(_previewTemplateJson, dataObj); + // 实时预览关闭模板内部 fitPage 自适应,避免与 WebView2 外层缩放叠加后出现“越放大越小”。 + html = NativePrintRenderService.RenderToHtml(_previewTemplateJson, dataObj, enableScreenAutoFit: false); } catch (Exception ex) { diff --git a/yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialEntryOperationView.xaml b/yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialEntryOperationView.xaml index ba74c14..e68cc95 100644 --- a/yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialEntryOperationView.xaml +++ b/yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialEntryOperationView.xaml @@ -1057,42 +1057,76 @@ Margin="8,0,10,0" VerticalAlignment="Center" Foreground="{DynamicResource SecondaryTextBrush}"/> - - - - - - - - - - - - - - + + +