增强打印预览功能,新增离线打印功能,新增缩放控制按钮以提升用户体验。优化打印数据准备逻辑,支持实时预览缩放,确保打印效果的一致性。同时,重构相关视图和服务以增强系统的可维护性和扩展性。
This commit is contained in:
@@ -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<string>("JeecgIntegration:BaseUrl") ?? "http://localhost:8080/jeecg-boot").TrimEnd('/');
|
||||
private int DefaultTenantId => (int?)_configuration.GetValue<long?>("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>(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<string>()?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(templateField)) continue;
|
||||
var bizField = obj["bizField"]?.GetValue<string>()?.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<string>(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<string> 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<string>()?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(key)) fields.Add(key!);
|
||||
}
|
||||
}
|
||||
|
||||
var bindField = obj["bindField"]?.GetValue<string>()?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(bindField)) fields.Add(bindField!);
|
||||
|
||||
if (obj["columns"] is JsonArray cols)
|
||||
{
|
||||
foreach (var c in cols)
|
||||
{
|
||||
var cBind = c?["bindField"]?.GetValue<string>()?.Trim();
|
||||
var cField = c?["field"]?.GetValue<string>()?.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<DateTime>(out var dt))
|
||||
{
|
||||
return JsonValue.Create(dt.ToString("yyyy-MM-dd HH:mm:ss"))!;
|
||||
}
|
||||
if (value.TryGetValue<DateTimeOffset>(out var dto))
|
||||
{
|
||||
return JsonValue.Create(dto.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"))!;
|
||||
}
|
||||
if (value.TryGetValue<string>(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<bool> UpdatePriorityAsync(string id, string priorityPickup, CancellationToken ct = default)
|
||||
|
||||
Reference in New Issue
Block a user