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}"/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
按钮 Click 在 Command 之后执行,用于兜底刷新列宽(不重复切换状态)。
@@ -66,6 +78,7 @@ public partial class RawMaterialEntryOperationView : UserControl
EnsureVmAttached();
ApplySplitLayout();
+ ApplyPrintPreviewZoom();
if (DataContext is RawMaterialEntryOperationViewModel vm && !_initialized)
{
@@ -92,6 +105,7 @@ public partial class RawMaterialEntryOperationView : UserControl
try
{
await PrintPreviewWebView.EnsureCoreWebView2Async();
+ ApplyPrintPreviewZoom();
PrintPreviewWebView.NavigateToString(html ?? string.Empty);
}
catch
@@ -100,6 +114,15 @@ public partial class RawMaterialEntryOperationView : UserControl
}
}
+ private void ApplyPrintPreviewZoom()
+ {
+ var vm = _vm ?? DataContext as RawMaterialEntryOperationViewModel;
+ if (vm == null) return;
+ if (PrintPreviewWebView?.CoreWebView2 == null) return;
+ // 实时预览已关闭 HTML 内部 fitPage,自定义缩放直接映射到 WebView2 即可(+ 放大,- 缩小)。
+ PrintPreviewWebView.ZoomFactor = vm.PrintPreviewZoomFactor;
+ }
+
///
/// 根据 ViewModel 同步右侧栏与分割条:展开时使用持久化宽度;折叠时右栏与分割条占宽均为 0(完全隐藏)。
///