新增打印模板管理功能,包含免密接口和实时通知机制,支持桌面端打印模板的查询和列表展示。更新相关控制器、服务和视图,优化用户体验并增强系统的实时数据同步能力。
This commit is contained in:
@@ -213,6 +213,8 @@ public class ShiroConfig {
|
||||
filterChainDefinitionMap.put("/xslmes/mesXslWarehouseArea/anon/**", "anon");
|
||||
// MES密炼物料管理免密接口(供桌面端调用)
|
||||
filterChainDefinitionMap.put("/mes/material/mixerMaterial/anon/**", "anon");
|
||||
// 打印模板免密接口(供桌面端调用)
|
||||
filterChainDefinitionMap.put("/print/template/anon/**", "anon");
|
||||
// 系统分类字典免密接口(供桌面端调用)
|
||||
filterChainDefinitionMap.put("/sys/category/anon/**", "anon");
|
||||
// 桌面端用户反同步批量上报(Outbox -> /sys/sync/batch)
|
||||
|
||||
@@ -198,6 +198,36 @@ public class PrintTemplateController extends JeecgController<PrintTemplate, IPri
|
||||
return Result.OK(t);
|
||||
}
|
||||
|
||||
// ═══════════════════════════ 桌面端免密接口 ═══════════════════════════
|
||||
|
||||
/**
|
||||
* 免密-通过编码查询(供 WPF 桌面端匿名调用)
|
||||
*/
|
||||
@Operation(summary = "打印模板-免密通过编码查询(桌面端)")
|
||||
@GetMapping(value = "/anon/queryByCode")
|
||||
public Result<PrintTemplate> anonQueryByCode(@RequestParam(name = "code") String code) {
|
||||
PrintTemplate t = service.getByCode(code);
|
||||
if (t == null) {
|
||||
return Result.error("未找到模板: " + code);
|
||||
}
|
||||
return Result.OK(t);
|
||||
}
|
||||
|
||||
/**
|
||||
* 免密-分页列表(供 WPF 桌面端匿名调用,用于打印设置页选择模板)
|
||||
*/
|
||||
@Operation(summary = "打印模板-免密分页列表(桌面端)")
|
||||
@GetMapping(value = "/anon/list")
|
||||
public Result<IPage<PrintTemplate>> anonList(PrintTemplate query,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "100") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<PrintTemplate> qw = QueryGenerator.initQueryWrapper(query, req.getParameterMap());
|
||||
qw.orderByAsc("template_code");
|
||||
Page<PrintTemplate> page = new Page<>(pageNo, pageSize);
|
||||
return Result.OK(service.page(page, qw));
|
||||
}
|
||||
|
||||
@AutoLog(value = "打印模板-图片分析生成原生JSON")
|
||||
@Operation(summary = "打印模板-上传图片分析为原生模板JSON(前端传 imageBase64;可接 OpenAI 兼容视觉模型)")
|
||||
@PostMapping(value = "/analyzeImageForNative")
|
||||
|
||||
@@ -70,6 +70,11 @@ public class MesXslStompNotifyService {
|
||||
publish("/topic/sync/mes-warehouse-areas", "MES_WAREHOUSE_AREA_CHANGED", "warehouseAreaId", warehouseAreaId, action);
|
||||
}
|
||||
|
||||
/** 广播打印模板变更事件到 /topic/sync/print-templates */
|
||||
public void publishPrintTemplateChanged(String action, String templateId) {
|
||||
publish("/topic/sync/print-templates", "PRINT_TEMPLATE_CHANGED", "templateId", templateId, action);
|
||||
}
|
||||
|
||||
// ─────────────────────────── 私有辅助 ────────────────────────────
|
||||
|
||||
private void publish(String topic, String cmd, String idKey, String idValue, String action) {
|
||||
|
||||
@@ -48,6 +48,8 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.jeecg.modules.print.ai.INativePrintTemplateImageAnalyzeService;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
|
||||
/**
|
||||
* 打印模板维护(Hiprint)
|
||||
@@ -63,6 +65,16 @@ public class PrintTemplateController extends JeecgController<PrintTemplate, IPri
|
||||
@Autowired
|
||||
private INativePrintTemplateImageAnalyzeService nativePrintTemplateImageAnalyzeService;
|
||||
|
||||
/**
|
||||
* STOMP 实时通知:广播打印模板变更到 /topic/sync/print-templates。
|
||||
* 直接用 SimpMessagingTemplate 内联推送,避免 jeecg-system-biz(核心模块)
|
||||
* 反向依赖 jeecg-module-xslmes(业务模块)造成的循环依赖。
|
||||
* 消息体格式与 MesXslStompNotifyService.publishPrintTemplateChanged 完全一致,
|
||||
* 桌面端订阅方无需任何改动。
|
||||
*/
|
||||
@Autowired
|
||||
private SimpMessagingTemplate messagingTemplate;
|
||||
|
||||
@Operation(summary = "打印模板-分页列表")
|
||||
@GetMapping(value = "/list")
|
||||
@RequiresPermissions("print:template:list")
|
||||
@@ -92,6 +104,7 @@ public class PrintTemplateController extends JeecgController<PrintTemplate, IPri
|
||||
entity.setTemplateJson("{}");
|
||||
}
|
||||
service.save(entity);
|
||||
publishPrintTemplateChanged("add", entity.getId());
|
||||
return Result.OK("添加成功");
|
||||
}
|
||||
|
||||
@@ -111,6 +124,7 @@ public class PrintTemplateController extends JeecgController<PrintTemplate, IPri
|
||||
}
|
||||
}
|
||||
service.updateById(entity);
|
||||
publishPrintTemplateChanged("edit", entity.getId());
|
||||
return Result.OK("修改成功");
|
||||
}
|
||||
|
||||
@@ -142,6 +156,7 @@ public class PrintTemplateController extends JeecgController<PrintTemplate, IPri
|
||||
@RequiresPermissions("print:template:delete")
|
||||
public Result<String> delete(@RequestParam(name = "id") String id) {
|
||||
service.removeById(id);
|
||||
publishPrintTemplateChanged("delete", id);
|
||||
return Result.OK("删除成功");
|
||||
}
|
||||
|
||||
@@ -153,7 +168,9 @@ public class PrintTemplateController extends JeecgController<PrintTemplate, IPri
|
||||
if (StringUtils.isBlank(ids)) {
|
||||
return Result.error("参数 ids 不能为空");
|
||||
}
|
||||
service.removeByIds(java.util.Arrays.asList(ids.split(",")));
|
||||
List<String> idList = java.util.Arrays.asList(ids.split(","));
|
||||
service.removeByIds(idList);
|
||||
idList.forEach(id -> publishPrintTemplateChanged("delete", id.trim()));
|
||||
return Result.OK("批量删除成功");
|
||||
}
|
||||
|
||||
@@ -450,6 +467,30 @@ public class PrintTemplateController extends JeecgController<PrintTemplate, IPri
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// ═══════════════════════════ 桌面端免密接口 ═══════════════════════════
|
||||
|
||||
@Operation(summary = "打印模板-免密通过编码查询(桌面端)")
|
||||
@GetMapping(value = "/anon/queryByCode")
|
||||
public Result<PrintTemplate> anonQueryByCode(@RequestParam(name = "code") String code) {
|
||||
PrintTemplate t = service.getByCode(code);
|
||||
if (t == null) {
|
||||
return Result.error("未找到模板: " + code);
|
||||
}
|
||||
return Result.OK(t);
|
||||
}
|
||||
|
||||
@Operation(summary = "打印模板-免密分页列表(桌面端)")
|
||||
@GetMapping(value = "/anon/list")
|
||||
public Result<IPage<PrintTemplate>> anonList(PrintTemplate query,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "100") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<PrintTemplate> qw = QueryGenerator.initQueryWrapper(query, req.getParameterMap());
|
||||
qw.orderByAsc("template_code");
|
||||
Page<PrintTemplate> page = new Page<>(pageNo, pageSize);
|
||||
return Result.OK(service.page(page, qw));
|
||||
}
|
||||
|
||||
private PrintService resolvePrintService(String printerName) {
|
||||
PrintService target = null;
|
||||
if (StringUtils.isNotBlank(printerName) && !"__system_default__".equals(printerName)) {
|
||||
@@ -469,4 +510,22 @@ public class PrintTemplateController extends JeecgController<PrintTemplate, IPri
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播打印模板变更事件到 /topic/sync/print-templates,桌面端订阅同步刷新本地缓存。
|
||||
* 消息体格式 = MesXslStompNotifyService.publishPrintTemplateChanged 的输出,
|
||||
* 内联实现避免反向依赖业务模块。
|
||||
*/
|
||||
private void publishPrintTemplateChanged(String action, String templateId) {
|
||||
try {
|
||||
Map<String, Object> event = new HashMap<>();
|
||||
event.put("cmd", "PRINT_TEMPLATE_CHANGED");
|
||||
event.put("action", action);
|
||||
event.put("templateId", templateId);
|
||||
event.put("timestamp", System.currentTimeMillis());
|
||||
messagingTemplate.convertAndSend("/topic/sync/print-templates", JSON.toJSONString(event));
|
||||
} catch (Exception e) {
|
||||
log.debug("广播 STOMP 事件失败 [PRINT_TEMPLATE_CHANGED]: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
using Prism.Events;
|
||||
|
||||
namespace YY.Admin.Core.Events;
|
||||
|
||||
public class PrintTemplateChangedPayload
|
||||
{
|
||||
public string Action { get; set; } = string.Empty; // add/edit/delete/reconnect/pull
|
||||
public string? TemplateId { get; set; }
|
||||
}
|
||||
|
||||
public class PrintTemplateChangedEvent : PubSubEvent<PrintTemplateChangedPayload> { }
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace YY.Admin.Core.Services;
|
||||
|
||||
public record PrintDotPrinter(string Name, bool IsDefault);
|
||||
|
||||
public interface IPrintDotService
|
||||
{
|
||||
/// <summary>获取 PrintDot 打印机列表,同时验证连通性。</summary>
|
||||
Task<IReadOnlyList<PrintDotPrinter>> GetPrintersAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>发送 PDF base64 打印任务(content 可含或不含 data:application/pdf;base64, 前缀)。</summary>
|
||||
Task PrintAsync(string printerName, string pdfBase64, string jobName = "QH-MES", int copies = 1, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using YY.Admin.Core.Entity;
|
||||
|
||||
namespace YY.Admin.Core.Services;
|
||||
|
||||
public interface IPrintTemplateService
|
||||
{
|
||||
Task<PrintTemplate?> GetByCodeAsync(string templateCode, CancellationToken ct = default);
|
||||
Task<IReadOnlyList<PrintTemplate>> ListAsync(CancellationToken ct = default);
|
||||
Task<IReadOnlyList<PrintTemplate>> RefreshCacheAsync(CancellationToken ct = default);
|
||||
IReadOnlyList<PrintTemplate> GetCached();
|
||||
}
|
||||
20
yy-admin-master/YY.Admin.Core/Entity/PrintTemplate.cs
Normal file
20
yy-admin-master/YY.Admin.Core/Entity/PrintTemplate.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace YY.Admin.Core.Entity;
|
||||
|
||||
/// <summary>打印模板(映射后端 print_template 表,桌面端只读)</summary>
|
||||
public class PrintTemplate
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? TemplateCode { get; set; }
|
||||
public string? TemplateName { get; set; }
|
||||
public string? Category { get; set; }
|
||||
public double? PaperWidthMm { get; set; }
|
||||
public double? PaperHeightMm { get; set; }
|
||||
public string? PaperOrientation { get; set; }
|
||||
/// <summary>原生设计器 JSON(engine=native)</summary>
|
||||
public string? TemplateJson { get; set; }
|
||||
public string? Remark { get; set; }
|
||||
public string? CreateBy { get; set; }
|
||||
public DateTime? CreateTime { get; set; }
|
||||
public string? UpdateBy { get; set; }
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
}
|
||||
@@ -77,6 +77,8 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
|
||||
|
||||
// 登录设置(桌面端会话与检查间隔)
|
||||
new SysMenu{ Id=1300200013001, Pid=1300200000101, Title="登录设置", Path="LoginSettingsView", Name="loginSettings", Component="LoginSettingsView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=107 },
|
||||
// 打印设置(PrintDot 桥接器配置)
|
||||
new SysMenu{ Id=1300200013101, Pid=1300200000101, Title="打印设置", Path="PrintSettingsView", Name="printSettings", Component="PrintSettingsView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=108 },
|
||||
|
||||
// 角色管理
|
||||
new SysMenu{ Id=1300200020101, Pid=1300200000101, Title="角色管理", Path="RoleManagementView", Name="sysRole", Component="/system/role/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 },
|
||||
|
||||
@@ -218,6 +218,7 @@ public class SysTenantMenuSeedData : ISqlSugarEntitySeedData<SysTenantMenu>
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId = 1300200010201},
|
||||
new SysTenantMenu() { TenantId = 1300000000001,MenuId = 1300600040101},
|
||||
new SysTenantMenu(){ TenantId=1300000000001, MenuId=1300150010901 },
|
||||
new SysTenantMenu(){ TenantId=1300000000001, MenuId=1300200013101 },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"PrintDot": {
|
||||
"Url": "ws://127.0.0.1:1122/ws" // PrintDot 桥接器 WebSocket 地址,可在打印设置页面覆盖
|
||||
},
|
||||
"AutoUpdate": {
|
||||
"RemoteConfigUrl": "http://14.103.155.227:8083/updates/version.xml" //更新文件地址
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,109 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using YY.Admin.Core.Services;
|
||||
|
||||
namespace YY.Admin.Services.Service.Print;
|
||||
|
||||
/// <summary>
|
||||
/// PrintDot 本地桥接器 WebSocket 客户端。
|
||||
/// URL 从 appsettings.json 的 PrintDot:Url 读取,可在运行时通过 <see cref="PrintDotSettings"/> 覆盖。
|
||||
/// </summary>
|
||||
public class PrintDotService : IPrintDotService
|
||||
{
|
||||
private readonly IConfiguration _config;
|
||||
private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||
|
||||
public PrintDotService(IConfiguration config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
private string ResolveWsUrl()
|
||||
{
|
||||
var url = PrintDotSettings.Current?.WsUrl
|
||||
?? _config.GetValue<string>("PrintDot:Url")
|
||||
?? "ws://127.0.0.1:1122/ws";
|
||||
return url.TrimEnd('/');
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PrintDotPrinter>> GetPrintersAsync(CancellationToken ct = default)
|
||||
{
|
||||
var wsUrl = ResolveWsUrl();
|
||||
using var ws = new ClientWebSocket();
|
||||
await ws.ConnectAsync(new Uri(wsUrl), ct);
|
||||
|
||||
// 连接后服务端立即推送 printer_list
|
||||
var json = await ReceiveTextAsync(ws, ct);
|
||||
var doc = JsonNode.Parse(json);
|
||||
if (doc?["type"]?.GetValue<string>() == "printer_list")
|
||||
{
|
||||
var arr = doc["data"]?.AsArray();
|
||||
if (arr != null)
|
||||
{
|
||||
return arr
|
||||
.Where(n => n != null)
|
||||
.Select(n => new PrintDotPrinter(
|
||||
Name: n!["name"]?.GetValue<string>()?.Trim() ?? string.Empty,
|
||||
IsDefault: n["isDefault"]?.GetValue<bool>() ?? false))
|
||||
.Where(p => !string.IsNullOrWhiteSpace(p.Name))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public async Task PrintAsync(string printerName, string pdfBase64, string jobName = "QH-MES", int copies = 1, CancellationToken ct = default)
|
||||
{
|
||||
// 去掉 data: 前缀
|
||||
var content = pdfBase64.Trim();
|
||||
var comma = content.IndexOf(',');
|
||||
if (content.StartsWith("data:", StringComparison.OrdinalIgnoreCase) && comma >= 0)
|
||||
content = content[(comma + 1)..];
|
||||
|
||||
var payload = new
|
||||
{
|
||||
printer = printerName,
|
||||
content,
|
||||
job = new { name = jobName, copies = Math.Max(1, copies) }
|
||||
};
|
||||
|
||||
var wsUrl = ResolveWsUrl();
|
||||
using var ws = new ClientWebSocket();
|
||||
await ws.ConnectAsync(new Uri(wsUrl), ct);
|
||||
|
||||
// 先等服务端推送 printer_list 再发任务
|
||||
await ReceiveTextAsync(ws, ct);
|
||||
|
||||
var msg = JsonSerializer.Serialize(payload, JsonOpts);
|
||||
await ws.SendAsync(Encoding.UTF8.GetBytes(msg), WebSocketMessageType.Text, true, ct);
|
||||
|
||||
// 等待打印结果(最多 3 分钟)
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
cts.CancelAfter(TimeSpan.FromMinutes(3));
|
||||
while (ws.State == WebSocketState.Open)
|
||||
{
|
||||
var response = await ReceiveTextAsync(ws, cts.Token);
|
||||
var resDoc = JsonNode.Parse(response);
|
||||
var status = resDoc?["status"]?.GetValue<string>();
|
||||
if (status == null) continue;
|
||||
if (status == "success") return;
|
||||
throw new InvalidOperationException(resDoc?["message"]?.GetValue<string>() ?? "PrintDot 打印失败");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> ReceiveTextAsync(ClientWebSocket ws, CancellationToken ct)
|
||||
{
|
||||
var buffer = new ArraySegment<byte>(new byte[64 * 1024]);
|
||||
using var ms = new System.IO.MemoryStream();
|
||||
WebSocketReceiveResult result;
|
||||
do
|
||||
{
|
||||
result = await ws.ReceiveAsync(buffer, ct);
|
||||
ms.Write(buffer.Array!, buffer.Offset, result.Count);
|
||||
} while (!result.EndOfMessage);
|
||||
return Encoding.UTF8.GetString(ms.ToArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
namespace YY.Admin.Services.Service.Print;
|
||||
|
||||
/// <summary>
|
||||
/// 运行时可修改的 PrintDot 连接设置(由设置页写入,PrintDotService 优先读取)。
|
||||
/// </summary>
|
||||
public class PrintDotSettings
|
||||
{
|
||||
private static readonly string SettingsPath = System.IO.Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"YY.Admin", "print-dot-settings.json");
|
||||
|
||||
public string WsUrl { get; set; } = "ws://127.0.0.1:1122/ws";
|
||||
public string SelectedPrinter { get; set; } = string.Empty;
|
||||
|
||||
private static PrintDotSettings? _current;
|
||||
public static PrintDotSettings? Current => _current;
|
||||
|
||||
public static PrintDotSettings Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (System.IO.File.Exists(SettingsPath))
|
||||
{
|
||||
var json = System.IO.File.ReadAllText(SettingsPath);
|
||||
_current = System.Text.Json.JsonSerializer.Deserialize<PrintDotSettings>(json) ?? new PrintDotSettings();
|
||||
return _current;
|
||||
}
|
||||
}
|
||||
catch { /* 读取失败使用默认值 */ }
|
||||
_current = new PrintDotSettings();
|
||||
return _current;
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
var dir = System.IO.Path.GetDirectoryName(SettingsPath)!;
|
||||
System.IO.Directory.CreateDirectory(dir);
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(this, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
|
||||
System.IO.File.WriteAllText(SettingsPath, json);
|
||||
_current = this;
|
||||
}
|
||||
catch { /* 忽略保存失败 */ }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Services;
|
||||
|
||||
namespace YY.Admin.Services.Service.Print;
|
||||
|
||||
public class PrintTemplateService : IPrintTemplateService
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly object _cacheLock = new();
|
||||
private List<PrintTemplate> _localCache = new();
|
||||
private readonly string _cacheFilePath;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOpts = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
Converters = { new NullableDateTimeJsonConverter() }
|
||||
};
|
||||
|
||||
private string BaseUrl =>
|
||||
(_configuration.GetValue<string>("JeecgIntegration:BaseUrl") ?? "http://localhost:8080/jeecg-boot").TrimEnd('/');
|
||||
|
||||
public PrintTemplateService(IHttpClientFactory httpClientFactory, IConfiguration configuration)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_configuration = configuration;
|
||||
|
||||
var appDataDir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"YY.Admin", "sync-cache");
|
||||
Directory.CreateDirectory(appDataDir);
|
||||
_cacheFilePath = Path.Combine(appDataDir, "print-template-cache.json");
|
||||
|
||||
LoadCacheFromDisk();
|
||||
}
|
||||
|
||||
public IReadOnlyList<PrintTemplate> GetCached()
|
||||
{
|
||||
lock (_cacheLock)
|
||||
{
|
||||
return _localCache.AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PrintTemplate?> GetByCodeAsync(string templateCode, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient("JeecgApi");
|
||||
var url = $"{BaseUrl}/print/template/anon/queryByCode?code={Uri.EscapeDataString(templateCode)}";
|
||||
var response = await client.GetAsync(url, ct).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode) return GetCachedByCode(templateCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<JeecgResult<PrintTemplate>>(JsonOpts, ct).ConfigureAwait(false);
|
||||
return result?.Success == true ? result.Result : GetCachedByCode(templateCode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return GetCachedByCode(templateCode);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PrintTemplate>> ListAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await RefreshCacheAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return GetCached();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PrintTemplate>> RefreshCacheAsync(CancellationToken ct = default)
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient("JeecgApi");
|
||||
var url = $"{BaseUrl}/print/template/anon/list?pageSize=200";
|
||||
var response = await client.GetAsync(url, ct).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<JeecgResult<JeecgPage<PrintTemplate>>>(JsonOpts, ct).ConfigureAwait(false);
|
||||
if (result?.Success != true)
|
||||
throw new InvalidOperationException(result?.Message ?? "后端返回失败");
|
||||
|
||||
var records = result.Result?.Records ?? new List<PrintTemplate>();
|
||||
lock (_cacheLock)
|
||||
{
|
||||
_localCache = records;
|
||||
SaveCacheToDiskUnsafe();
|
||||
}
|
||||
return records.AsReadOnly();
|
||||
}
|
||||
|
||||
private PrintTemplate? GetCachedByCode(string code)
|
||||
{
|
||||
lock (_cacheLock)
|
||||
{
|
||||
return _localCache.FirstOrDefault(t =>
|
||||
string.Equals(t.TemplateCode, code, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadCacheFromDisk()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_cacheFilePath)) return;
|
||||
var json = File.ReadAllText(_cacheFilePath);
|
||||
var data = JsonSerializer.Deserialize<List<PrintTemplate>>(json, JsonOpts);
|
||||
_localCache = data ?? new List<PrintTemplate>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_localCache = new List<PrintTemplate>();
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveCacheToDiskUnsafe()
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(_localCache, JsonOpts);
|
||||
File.WriteAllText(_cacheFilePath, json);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private record JeecgResult<T>(bool Success, T? Result, string? Message);
|
||||
private record JeecgPage<T>(List<T> Records, long Total);
|
||||
|
||||
private sealed class NullableDateTimeJsonConverter : JsonConverter<DateTime?>
|
||||
{
|
||||
private static readonly string[] Formats =
|
||||
[
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
"yyyy-MM-dd HH:mm:ss.fff",
|
||||
"yyyy-MM-ddTHH:mm:ss",
|
||||
"yyyy-MM-ddTHH:mm:ss.fff",
|
||||
"yyyy-MM-ddTHH:mm:ssZ",
|
||||
"yyyy-MM-ddTHH:mm:ss.fffZ"
|
||||
];
|
||||
|
||||
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null) return null;
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
var raw = reader.GetString();
|
||||
if (string.IsNullOrWhiteSpace(raw)) return null;
|
||||
if (DateTime.TryParseExact(raw, Formats, System.Globalization.CultureInfo.InvariantCulture,
|
||||
System.Globalization.DateTimeStyles.AssumeLocal, out var exact))
|
||||
return exact;
|
||||
if (DateTime.TryParse(raw, out var fallback)) return fallback;
|
||||
}
|
||||
throw new JsonException($"无法将 JSON 值转换为 DateTime?,token={reader.TokenType}");
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value.HasValue) writer.WriteStringValue(value.Value.ToString("yyyy-MM-dd HH:mm:ss"));
|
||||
else writer.WriteNullValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using Prism.Events;
|
||||
using System.Text.Json;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Events;
|
||||
using YY.Admin.Core.Services;
|
||||
|
||||
namespace YY.Admin.Services.Service.Print;
|
||||
|
||||
public class PrintTemplateSyncCoordinator : ISingletonDependency
|
||||
{
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IPrintTemplateService _printTemplateService;
|
||||
private readonly ILoggerService _logger;
|
||||
private SubscriptionToken? _remoteCommandToken;
|
||||
private SubscriptionToken? _networkStatusToken;
|
||||
|
||||
public PrintTemplateSyncCoordinator(
|
||||
IEventAggregator eventAggregator,
|
||||
IPrintTemplateService printTemplateService,
|
||||
SyncPollManager pollManager,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
_printTemplateService = printTemplateService;
|
||||
_logger = logger;
|
||||
|
||||
_remoteCommandToken = _eventAggregator
|
||||
.GetEvent<RemoteCommandReceivedEvent>()
|
||||
.Subscribe(OnRemoteCommand, ThreadOption.BackgroundThread);
|
||||
_networkStatusToken = _eventAggregator
|
||||
.GetEvent<NetworkStatusChangedEvent>()
|
||||
.Subscribe(OnNetworkStatusChanged, ThreadOption.BackgroundThread);
|
||||
|
||||
pollManager.Register("打印模板", () =>
|
||||
{
|
||||
_eventAggregator.GetEvent<PrintTemplateChangedEvent>()
|
||||
.Publish(new PrintTemplateChangedPayload { Action = "poll" });
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
_logger.Information("[打印模板推送] PrintTemplateSyncCoordinator 已启动");
|
||||
}
|
||||
|
||||
private void OnNetworkStatusChanged(NetworkStatusChangedPayload payload)
|
||||
{
|
||||
if (!payload.IsOnline) return;
|
||||
_logger.Information("[打印模板推送] 网络恢复,触发补偿刷新");
|
||||
_eventAggregator.GetEvent<PrintTemplateChangedEvent>()
|
||||
.Publish(new PrintTemplateChangedPayload { Action = "reconnect" });
|
||||
}
|
||||
|
||||
private void OnRemoteCommand(RemoteCommandPayload payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = payload.CommandJson ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(json)) return;
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (!doc.RootElement.TryGetProperty("cmd", out var cmdEl)) return;
|
||||
var cmd = cmdEl.GetString() ?? string.Empty;
|
||||
if (!cmd.Equals("PRINT_TEMPLATE_CHANGED", StringComparison.OrdinalIgnoreCase)) return;
|
||||
|
||||
doc.RootElement.TryGetProperty("action", out var actionEl);
|
||||
doc.RootElement.TryGetProperty("templateId", out var idEl);
|
||||
|
||||
var changedPayload = new PrintTemplateChangedPayload
|
||||
{
|
||||
Action = actionEl.GetString() ?? string.Empty,
|
||||
TemplateId = idEl.ValueKind == JsonValueKind.String ? idEl.GetString() : null
|
||||
};
|
||||
|
||||
_logger.Information($"[打印模板推送] 收到变更信号 action={changedPayload.Action}, templateId={changedPayload.TemplateId}");
|
||||
_eventAggregator.GetEvent<PrintTemplateChangedEvent>().Publish(changedPayload);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[打印模板推送] 处理 STOMP 信号失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -10,6 +10,7 @@ using YY.Admin.EventBus;
|
||||
using YY.Admin.Filter;
|
||||
using YY.Admin.Module;
|
||||
using YY.Admin.Properties;
|
||||
using YY.Admin.Services.Service.Print;
|
||||
using YY.Admin.Setup;
|
||||
using YY.Admin.ViewModels;
|
||||
using YY.Admin.Views;
|
||||
@@ -96,6 +97,9 @@ namespace YY.Admin
|
||||
|
||||
_logger.Information("应用程序已启动");
|
||||
|
||||
// 加载 PrintDot 本地设置(使 PrintDotService 在任何页面调用前已有配置)
|
||||
PrintDotSettings.Load();
|
||||
|
||||
// 启动断联续传同步模块
|
||||
_syncModule.OnInitialized(Container);
|
||||
}
|
||||
|
||||
@@ -162,6 +162,10 @@ public class StompWebSocketService : ISignalRService
|
||||
await SendFrameAsync(
|
||||
BuildSubscribeFrame("sub-mes-warehouse-areas", "/topic/sync/mes-warehouse-areas"),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
// 打印模板变更:订阅 /topic/sync/print-templates
|
||||
await SendFrameAsync(
|
||||
BuildSubscribeFrame("sub-print-templates", "/topic/sync/print-templates"),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// 订阅服务端 PONG 回复(应用层假在线检测)
|
||||
await SendFrameAsync(
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using Microsoft.Web.WebView2.Wpf;
|
||||
|
||||
namespace YY.Admin.Infrastructure.Print;
|
||||
|
||||
/// <summary>
|
||||
/// 使用隐藏 WebView2 窗口将 HTML 字符串渲染为 PDF base64。
|
||||
/// 所有调用必须最终在 WPF UI 线程执行,由内部 Dispatcher 保证。
|
||||
/// </summary>
|
||||
public static class HtmlToPdfRenderer
|
||||
{
|
||||
public static Task<string> RenderAsync(string html, double widthMm = 210, double heightMm = 297)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
Application.Current.Dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await RenderOnUiThreadAsync(html, widthMm, heightMm);
|
||||
tcs.SetResult(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.SetException(ex);
|
||||
}
|
||||
});
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private static async Task<string> RenderOnUiThreadAsync(string html, double widthMm, double heightMm)
|
||||
{
|
||||
var tempHtml = Path.ChangeExtension(Path.GetTempFileName(), ".html");
|
||||
var tempPdf = Path.ChangeExtension(Path.GetTempFileName(), ".pdf");
|
||||
Window? win = null;
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(tempHtml, html, Encoding.UTF8);
|
||||
|
||||
var env = await CoreWebView2Environment.CreateAsync();
|
||||
var wv = new WebView2();
|
||||
|
||||
win = new Window
|
||||
{
|
||||
Width = 1,
|
||||
Height = 1,
|
||||
Left = -99999,
|
||||
Top = -99999,
|
||||
ShowInTaskbar = false,
|
||||
WindowStyle = WindowStyle.None,
|
||||
Visibility = Visibility.Visible
|
||||
};
|
||||
win.Content = wv;
|
||||
win.Show();
|
||||
|
||||
await wv.EnsureCoreWebView2Async(env);
|
||||
|
||||
var navTcs = new TaskCompletionSource();
|
||||
wv.CoreWebView2.NavigationCompleted += (_, _) => navTcs.TrySetResult();
|
||||
wv.CoreWebView2.Navigate(new Uri(tempHtml).AbsoluteUri);
|
||||
await navTcs.Task;
|
||||
|
||||
// 短暂等待 JS/CSS 完成渲染
|
||||
await Task.Delay(200);
|
||||
|
||||
var settings = env.CreatePrintSettings();
|
||||
settings.PageWidth = widthMm / 25.4;
|
||||
settings.PageHeight = heightMm / 25.4;
|
||||
settings.MarginTop = 0;
|
||||
settings.MarginBottom = 0;
|
||||
settings.MarginLeft = 0;
|
||||
settings.MarginRight = 0;
|
||||
settings.ShouldPrintHeaderAndFooter = false;
|
||||
settings.ShouldPrintBackgrounds = true;
|
||||
|
||||
await wv.CoreWebView2.PrintToPdfAsync(tempPdf, settings);
|
||||
|
||||
var pdfBytes = File.ReadAllBytes(tempPdf);
|
||||
return Convert.ToBase64String(pdfBytes);
|
||||
}
|
||||
finally
|
||||
{
|
||||
win?.Close();
|
||||
try { File.Delete(tempHtml); } catch { }
|
||||
try { File.Delete(tempPdf); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ using YY.Admin.Views.WeightRecord;
|
||||
using YY.Admin.Views.RawMaterialCard;
|
||||
using YY.Admin.Views.WarehouseArea;
|
||||
using YY.Admin.Views.RawMaterialEntry;
|
||||
using YY.Admin.Views.Print;
|
||||
|
||||
namespace YY.Admin
|
||||
{
|
||||
@@ -81,6 +82,10 @@ namespace YY.Admin
|
||||
containerRegistry.RegisterForNavigation<RawMaterialCardListView>();
|
||||
// 库区管理
|
||||
containerRegistry.RegisterForNavigation<WarehouseAreaListView>();
|
||||
// 打印设置
|
||||
containerRegistry.RegisterForNavigation<PrintSettingsView>();
|
||||
// 打印模板列表
|
||||
containerRegistry.RegisterForNavigation<PrintTemplateListView>();
|
||||
}
|
||||
}
|
||||
public class DialogWindow : Window, IDialogWindow
|
||||
|
||||
@@ -25,6 +25,7 @@ using YY.Admin.Services.Service.Vehicle;
|
||||
using YY.Admin.Services.Service.Warehouse;
|
||||
using YY.Admin.Services.Service.WarehouseArea;
|
||||
using YY.Admin.Services.Service.WeightRecord;
|
||||
using YY.Admin.Services.Service.Print;
|
||||
|
||||
namespace YY.Admin.Module;
|
||||
|
||||
@@ -74,6 +75,10 @@ public class SyncModule : IModule
|
||||
containerRegistry.RegisterSingleton<DictSyncCoordinator>();
|
||||
// 统一轮询管理器(修改 SyncPollManager.PollInterval 即可调整所有模块的轮询间隔)
|
||||
containerRegistry.RegisterSingleton<SyncPollManager>();
|
||||
// 打印服务:PrintDot 桥接器 + 打印模板(含 STOMP 实时同步 + 本地缓存)
|
||||
containerRegistry.RegisterSingleton<IPrintDotService, PrintDotService>();
|
||||
containerRegistry.RegisterSingleton<IPrintTemplateService, PrintTemplateService>();
|
||||
containerRegistry.RegisterSingleton<PrintTemplateSyncCoordinator>();
|
||||
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddTransient<DisconnectGuardHandler>();
|
||||
@@ -140,6 +145,8 @@ public class SyncModule : IModule
|
||||
_ = containerProvider.Resolve<CategorySyncCoordinator>();
|
||||
// 强制实例化数据字典同步协调器
|
||||
_ = containerProvider.Resolve<DictSyncCoordinator>();
|
||||
// 强制实例化打印模板同步协调器
|
||||
_ = containerProvider.Resolve<PrintTemplateSyncCoordinator>();
|
||||
}
|
||||
|
||||
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
|
||||
|
||||
@@ -140,7 +140,18 @@ namespace YY.Admin.ViewModels.Control
|
||||
// 已实现页面:库区管理
|
||||
["WarehouseAreaListView"] = "WarehouseAreaListView",
|
||||
["/xslmes/mesXslWarehouseArea"] = "WarehouseAreaListView",
|
||||
["mesXslWarehouseArea"] = "WarehouseAreaListView"
|
||||
["mesXslWarehouseArea"] = "WarehouseAreaListView",
|
||||
|
||||
// 已实现页面:打印设置
|
||||
["PrintSettingsView"] = "PrintSettingsView",
|
||||
["/system/printSettings"] = "PrintSettingsView",
|
||||
["printSettings"] = "PrintSettingsView",
|
||||
|
||||
// 已实现页面:打印模板
|
||||
["PrintTemplateListView"] = "PrintTemplateListView",
|
||||
["/platform/print"] = "PrintTemplateListView",
|
||||
["print"] = "PrintTemplateListView",
|
||||
["printTemplate"] = "PrintTemplateListView"
|
||||
};
|
||||
|
||||
private MenuItem? _selectedMenuItem;
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using Prism.Commands;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.Services.Service.Print;
|
||||
using YY.Admin.Core;
|
||||
|
||||
namespace YY.Admin.ViewModels.Print;
|
||||
|
||||
public class PrintSettingsViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IPrintDotService _printDotService;
|
||||
private readonly IPrintTemplateService _printTemplateService;
|
||||
|
||||
// ── 连接设置 ──────────────────────────────────────────────────────────
|
||||
|
||||
private string _wsUrl = "ws://127.0.0.1:1122/ws";
|
||||
public string WsUrl
|
||||
{
|
||||
get => _wsUrl;
|
||||
set => SetProperty(ref _wsUrl, value);
|
||||
}
|
||||
|
||||
// ── 打印机列表 ──────────────────────────────────────────────────────────
|
||||
|
||||
public ObservableCollection<PrintDotPrinter> Printers { get; } = new();
|
||||
|
||||
private PrintDotPrinter? _selectedPrinter;
|
||||
public PrintDotPrinter? SelectedPrinter
|
||||
{
|
||||
get => _selectedPrinter;
|
||||
set => SetProperty(ref _selectedPrinter, value);
|
||||
}
|
||||
|
||||
// ── 模板列表 ────────────────────────────────────────────────────────────
|
||||
|
||||
public ObservableCollection<PrintTemplate> Templates { get; } = new();
|
||||
|
||||
// ── 状态 ────────────────────────────────────────────────────────────────
|
||||
|
||||
private bool _isBusy;
|
||||
public bool IsBusy
|
||||
{
|
||||
get => _isBusy;
|
||||
set => SetProperty(ref _isBusy, value);
|
||||
}
|
||||
|
||||
private string _statusMessage = string.Empty;
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
set => SetProperty(ref _statusMessage, value);
|
||||
}
|
||||
|
||||
// ── 命令 ────────────────────────────────────────────────────────────────
|
||||
|
||||
public DelegateCommand TestConnectionCommand { get; }
|
||||
public DelegateCommand SaveCommand { get; }
|
||||
public DelegateCommand RefreshTemplatesCommand { get; }
|
||||
|
||||
public PrintSettingsViewModel(
|
||||
IPrintDotService printDotService,
|
||||
IPrintTemplateService printTemplateService,
|
||||
IContainerExtension container,
|
||||
IRegionManager regionManager) : base(container, regionManager)
|
||||
{
|
||||
_printDotService = printDotService;
|
||||
_printTemplateService = printTemplateService;
|
||||
|
||||
TestConnectionCommand = new DelegateCommand(async () => await TestConnectionAsync());
|
||||
SaveCommand = new DelegateCommand(SaveSettings);
|
||||
RefreshTemplatesCommand = new DelegateCommand(async () => await RefreshTemplatesAsync());
|
||||
|
||||
// 加载已保存的设置
|
||||
var saved = PrintDotSettings.Load();
|
||||
WsUrl = saved.WsUrl;
|
||||
if (!string.IsNullOrWhiteSpace(saved.SelectedPrinter))
|
||||
_selectedPrinter = new PrintDotPrinter(saved.SelectedPrinter, false);
|
||||
}
|
||||
|
||||
private async Task TestConnectionAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
StatusMessage = "正在连接...";
|
||||
Printers.Clear();
|
||||
try
|
||||
{
|
||||
// 临时用当前输入的 URL 测试
|
||||
PrintDotSettings.Current!.WsUrl = WsUrl;
|
||||
var list = await _printDotService.GetPrintersAsync();
|
||||
foreach (var p in list) Printers.Add(p);
|
||||
|
||||
// 还原保存的已选打印机
|
||||
var saved = PrintDotSettings.Load();
|
||||
var match = list.FirstOrDefault(p => p.Name == saved.SelectedPrinter);
|
||||
SelectedPrinter = match ?? (list.Count > 0 ? list[0] : null);
|
||||
|
||||
StatusMessage = $"连接成功,共 {list.Count} 台打印机";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"连接失败:{ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveSettings()
|
||||
{
|
||||
var settings = new PrintDotSettings
|
||||
{
|
||||
WsUrl = WsUrl.Trim(),
|
||||
SelectedPrinter = SelectedPrinter?.Name ?? string.Empty
|
||||
};
|
||||
settings.Save();
|
||||
StatusMessage = "设置已保存";
|
||||
MessageBox.Show("PrintDot 设置已保存", "保存成功", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
private async Task RefreshTemplatesAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
Templates.Clear();
|
||||
try
|
||||
{
|
||||
var list = await _printTemplateService.ListAsync();
|
||||
foreach (var t in list) Templates.Add(t);
|
||||
StatusMessage = $"已加载 {list.Count} 个打印模板";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"加载模板失败:{ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using Prism.Commands;
|
||||
using Prism.Events;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Events;
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.Services.Service.Print;
|
||||
using YY.Admin.Views.Print;
|
||||
|
||||
namespace YY.Admin.ViewModels.Print;
|
||||
|
||||
public class PrintTemplateListViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IPrintTemplateService _printTemplateService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private SubscriptionToken? _changeToken;
|
||||
|
||||
private List<PrintTemplate> _allTemplates = new();
|
||||
|
||||
public ObservableCollection<PrintTemplate> Templates { get; } = new();
|
||||
|
||||
private string _statusMessage = string.Empty;
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
set => SetProperty(ref _statusMessage, value);
|
||||
}
|
||||
|
||||
private string? _filterCode;
|
||||
public string? FilterCode
|
||||
{
|
||||
get => _filterCode;
|
||||
set => SetProperty(ref _filterCode, value);
|
||||
}
|
||||
|
||||
private string? _filterName;
|
||||
public string? FilterName
|
||||
{
|
||||
get => _filterName;
|
||||
set => SetProperty(ref _filterName, value);
|
||||
}
|
||||
|
||||
private string? _filterCategory;
|
||||
public string? FilterCategory
|
||||
{
|
||||
get => _filterCategory;
|
||||
set => SetProperty(ref _filterCategory, value);
|
||||
}
|
||||
|
||||
public DelegateCommand SearchCommand { get; }
|
||||
public DelegateCommand ResetCommand { get; }
|
||||
public DelegateCommand<PrintTemplate> PreviewCommand { get; }
|
||||
|
||||
public PrintTemplateListViewModel(
|
||||
IPrintTemplateService printTemplateService,
|
||||
IEventAggregator eventAggregator,
|
||||
IContainerExtension container,
|
||||
IRegionManager regionManager) : base(container, regionManager)
|
||||
{
|
||||
_printTemplateService = printTemplateService;
|
||||
_eventAggregator = eventAggregator;
|
||||
|
||||
SearchCommand = new DelegateCommand(ApplyFilter);
|
||||
PreviewCommand = new DelegateCommand<PrintTemplate>(ShowPreview);
|
||||
ResetCommand = new DelegateCommand(() =>
|
||||
{
|
||||
FilterCode = null;
|
||||
FilterName = null;
|
||||
FilterCategory = null;
|
||||
ApplyFilter();
|
||||
});
|
||||
|
||||
_changeToken = _eventAggregator
|
||||
.GetEvent<PrintTemplateChangedEvent>()
|
||||
.Subscribe(_ => { RefreshSilentlyAsync().ConfigureAwait(false); }, ThreadOption.UIThread);
|
||||
|
||||
// 先用缓存立即填充,再后台静默刷新
|
||||
ShowCached();
|
||||
_ = RefreshSilentlyAsync();
|
||||
}
|
||||
|
||||
private void ShowCached()
|
||||
{
|
||||
var cached = _printTemplateService.GetCached();
|
||||
if (cached.Count == 0) return;
|
||||
_allTemplates = cached.ToList();
|
||||
ApplyFilter();
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
private async Task RefreshSilentlyAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await _printTemplateService.RefreshCacheAsync().ConfigureAwait(false);
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
_allTemplates = list.ToList();
|
||||
ApplyFilter();
|
||||
UpdateStatus();
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 静默失败:保留当前缓存内容不动,不显示错误
|
||||
var cached = _printTemplateService.GetCached();
|
||||
if (cached.Count > 0 && _allTemplates.Count == 0)
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
_allTemplates = cached.ToList();
|
||||
ApplyFilter();
|
||||
UpdateStatus();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyFilter()
|
||||
{
|
||||
IEnumerable<PrintTemplate> result = _allTemplates;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(FilterCode))
|
||||
result = result.Where(t => (t.TemplateCode ?? string.Empty)
|
||||
.Contains(FilterCode, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(FilterName))
|
||||
result = result.Where(t => (t.TemplateName ?? string.Empty)
|
||||
.Contains(FilterName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(FilterCategory))
|
||||
result = result.Where(t => (t.Category ?? string.Empty)
|
||||
.Contains(FilterCategory, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var filtered = result.ToList();
|
||||
|
||||
// 原地差量更新,避免滚动位置重置和闪烁
|
||||
for (int i = Templates.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (!filtered.Any(t => t.Id == Templates[i].Id))
|
||||
Templates.RemoveAt(i);
|
||||
}
|
||||
for (int i = 0; i < filtered.Count; i++)
|
||||
{
|
||||
var item = filtered[i];
|
||||
var existingIdx = -1;
|
||||
for (int j = 0; j < Templates.Count; j++)
|
||||
{
|
||||
if (Templates[j].Id == item.Id) { existingIdx = j; break; }
|
||||
}
|
||||
if (existingIdx < 0)
|
||||
Templates.Insert(i, item);
|
||||
else
|
||||
{
|
||||
if (existingIdx != i) Templates.Move(existingIdx, i);
|
||||
Templates[i] = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateStatus()
|
||||
{
|
||||
var hasFilter = !string.IsNullOrWhiteSpace(FilterCode)
|
||||
|| !string.IsNullOrWhiteSpace(FilterName)
|
||||
|| !string.IsNullOrWhiteSpace(FilterCategory);
|
||||
StatusMessage = hasFilter
|
||||
? $"筛选结果 {Templates.Count} / {_allTemplates.Count} 个"
|
||||
: _allTemplates.Count > 0
|
||||
? $"共 {_allTemplates.Count} 个模板"
|
||||
: "暂无模板";
|
||||
}
|
||||
|
||||
private void ShowPreview(PrintTemplate template)
|
||||
{
|
||||
if (template == null) return;
|
||||
ShowPreviewAsync(template);
|
||||
}
|
||||
|
||||
private async Task ShowPreviewAsync(PrintTemplate template)
|
||||
{
|
||||
// 列表缓存可能不含 templateJson(大字段),按需通过 queryByCode 单独拉取
|
||||
var json = template.TemplateJson;
|
||||
if (string.IsNullOrWhiteSpace(json) || json == "{}")
|
||||
{
|
||||
try
|
||||
{
|
||||
var full = await _printTemplateService.GetByCodeAsync(template.TemplateCode ?? "");
|
||||
json = full?.TemplateJson;
|
||||
}
|
||||
catch { /* 保持 json 为 null,预览窗口显示"尚未设计" */ }
|
||||
}
|
||||
|
||||
var win = new PrintPreviewWindow(template, json)
|
||||
{
|
||||
Owner = Application.Current.MainWindow
|
||||
};
|
||||
win.Show();
|
||||
}
|
||||
}
|
||||
125
yy-admin-master/YY.Admin/Views/Print/PrintPreviewWindow.xaml
Normal file
125
yy-admin-master/YY.Admin/Views/Print/PrintPreviewWindow.xaml
Normal file
@@ -0,0 +1,125 @@
|
||||
<hc:Window x:Class="YY.Admin.Views.Print.PrintPreviewWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
|
||||
Width="1200" Height="760"
|
||||
MinWidth="900" MinHeight="560"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ResizeMode="CanResize"
|
||||
Background="White"
|
||||
BorderBrush="#f0f0f0"
|
||||
BorderThickness="1"
|
||||
Title="打印预览">
|
||||
|
||||
<Window.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"/>
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="50"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 顶部工具栏 -->
|
||||
<Border Grid.Row="0"
|
||||
Background="#fafafa"
|
||||
BorderBrush="#f0f0f0"
|
||||
BorderThickness="0,0,0,1">
|
||||
<Grid Margin="16,0">
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock x:Name="TbTemplateName"
|
||||
FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="#333333"/>
|
||||
<TextBlock x:Name="TbTemplateCode"
|
||||
FontSize="11" Foreground="#888888" Margin="0,1,0,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock x:Name="TbStatus"
|
||||
FontSize="12" Foreground="#888888"
|
||||
VerticalAlignment="Center" Margin="0,0,16,0"/>
|
||||
<Button Content="关闭" Click="CloseButton_Click"
|
||||
Width="72" Height="30" FontSize="13"
|
||||
Style="{StaticResource ButtonDefault}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="360"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 左侧参数 JSON 区 -->
|
||||
<Border Grid.Column="0"
|
||||
BorderBrush="#f0f0f0"
|
||||
BorderThickness="0,0,1,0"
|
||||
Background="#fafafa"
|
||||
Padding="10">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Text="参数JSON"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#333333"/>
|
||||
|
||||
<StackPanel Grid.Row="1"
|
||||
Orientation="Horizontal"
|
||||
Margin="0,8,0,8">
|
||||
<Button Content="根据画布生成"
|
||||
Click="GenerateMockJson_Click"
|
||||
Height="28"
|
||||
Padding="10,0"
|
||||
FontSize="12"
|
||||
Style="{StaticResource ButtonDefault}"/>
|
||||
<Button Content="重新渲染"
|
||||
Click="RenderByParamJson_Click"
|
||||
Margin="8,0,0,0"
|
||||
Height="28"
|
||||
Padding="10,0"
|
||||
FontSize="12"
|
||||
Style="{StaticResource ButtonPrimary}"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBox x:Name="TbParamJson"
|
||||
Grid.Row="2"
|
||||
AcceptsReturn="True"
|
||||
AcceptsTab="True"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
FontFamily="Consolas"
|
||||
FontSize="12"
|
||||
TextWrapping="NoWrap"
|
||||
BorderBrush="#d9d9d9"
|
||||
BorderThickness="1"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<GridSplitter Grid.Column="1"
|
||||
Width="8"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ResizeBehavior="PreviousAndNext"
|
||||
ShowsPreview="True"
|
||||
Background="#f0f0f0"
|
||||
Cursor="SizeWE"/>
|
||||
|
||||
<!-- 右侧 WebView2 预览区 -->
|
||||
<wv2:WebView2 Grid.Column="2" x:Name="WebView" DefaultBackgroundColor="Transparent"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</hc:Window>
|
||||
240
yy-admin-master/YY.Admin/Views/Print/PrintPreviewWindow.xaml.cs
Normal file
240
yy-admin-master/YY.Admin/Views/Print/PrintPreviewWindow.xaml.cs
Normal file
@@ -0,0 +1,240 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Services.Service.Print;
|
||||
|
||||
namespace YY.Admin.Views.Print;
|
||||
|
||||
public partial class PrintPreviewWindow : HandyControl.Controls.Window
|
||||
{
|
||||
private readonly string _templateJson;
|
||||
|
||||
public PrintPreviewWindow(PrintTemplate template, string? templateJson)
|
||||
{
|
||||
InitializeComponent();
|
||||
_templateJson = templateJson ?? string.Empty;
|
||||
|
||||
TbTemplateName.Text = template.TemplateName ?? "(未命名)";
|
||||
TbTemplateCode.Text = $"编码:{template.TemplateCode} " +
|
||||
$"尺寸:{template.PaperWidthMm ?? 210}×{template.PaperHeightMm ?? 297} mm " +
|
||||
$"方向:{template.PaperOrientation ?? "纵向"}";
|
||||
|
||||
TbParamJson.Text = BuildMockParamJson(_templateJson);
|
||||
|
||||
Loaded += async (_, _) => await LoadPreviewAsync();
|
||||
}
|
||||
|
||||
private async Task LoadPreviewAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
TbStatus.Text = "加载中…";
|
||||
await WebView.EnsureCoreWebView2Async();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_templateJson) || _templateJson == "{}")
|
||||
{
|
||||
WebView.NavigateToString(BuildEmptyHtml());
|
||||
TbStatus.Text = "尚未设计模板内容";
|
||||
return;
|
||||
}
|
||||
|
||||
await RenderCurrentParamJsonAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TbStatus.Text = $"预览失败:{ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildEmptyHtml() => """
|
||||
<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8"/>
|
||||
<style>
|
||||
body { margin:0; background:#525659; display:flex;
|
||||
align-items:center; justify-content:center; height:100vh;
|
||||
font-family:"Microsoft YaHei",Arial,sans-serif; }
|
||||
.card { background:#fff; border-radius:8px; padding:48px 64px;
|
||||
text-align:center; box-shadow:0 6px 24px rgba(0,0,0,.4); }
|
||||
.icon { font-size:48px; color:#ccc; margin-bottom:16px; }
|
||||
.tip { font-size:15px; color:#888; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="icon">📄</div>
|
||||
<div class="tip">该模板尚未设计内容</div>
|
||||
</div>
|
||||
</body></html>
|
||||
""";
|
||||
|
||||
/// <summary>
|
||||
/// 左侧参数 JSON 重新渲染预览(与后端预览一致:用参数 JSON 驱动模板绑定字段)。
|
||||
/// </summary>
|
||||
private async Task RenderCurrentParamJsonAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
TbStatus.Text = "渲染中…";
|
||||
|
||||
JsonObject dataObj;
|
||||
var text = TbParamJson.Text?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
dataObj = new JsonObject();
|
||||
}
|
||||
else
|
||||
{
|
||||
var node = JsonNode.Parse(text);
|
||||
if (node is not JsonObject obj)
|
||||
{
|
||||
WebView.NavigateToString(BuildErrorHtml("参数JSON必须是对象(JSON Object)"));
|
||||
TbStatus.Text = "参数JSON格式错误";
|
||||
return;
|
||||
}
|
||||
dataObj = obj;
|
||||
}
|
||||
|
||||
var html = NativePrintRenderService.RenderToHtml(_templateJson, dataObj);
|
||||
WebView.NavigateToString(html);
|
||||
TbStatus.Text = string.Empty;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WebView.NavigateToString(BuildErrorHtml(ex.Message));
|
||||
TbStatus.Text = $"渲染失败:{ex.Message}";
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据模板绑定字段生成参数 JSON(便于用户直接编辑并预览)。
|
||||
/// </summary>
|
||||
private static string BuildMockParamJson(string templateJson)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(templateJson) || templateJson == "{}")
|
||||
return "{}";
|
||||
|
||||
try
|
||||
{
|
||||
var root = JsonNode.Parse(templateJson);
|
||||
var obj = new JsonObject();
|
||||
var elements = root?["elements"]?.AsArray() ?? new JsonArray();
|
||||
var fields = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var el in elements.OfType<JsonObject>())
|
||||
{
|
||||
var type = (el["type"]?.ToString() ?? string.Empty).Trim();
|
||||
if (type is "table" or "detailTable")
|
||||
{
|
||||
var source = (el["source"]?.ToString() ?? "mainTable").Trim();
|
||||
if (!obj.ContainsKey(source))
|
||||
{
|
||||
var rows = new JsonArray();
|
||||
var columns = el["columns"]?.AsArray() ?? new JsonArray();
|
||||
for (var i = 1; i <= 8; i++)
|
||||
{
|
||||
var row = new JsonObject();
|
||||
foreach (var col in columns.OfType<JsonObject>())
|
||||
{
|
||||
var field = (col["bindField"]?.ToString() ?? col["field"]?.ToString() ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(field)) continue;
|
||||
var contentType = (col["contentType"]?.ToString() ?? "text").Trim().ToLowerInvariant();
|
||||
row[field] = contentType switch
|
||||
{
|
||||
"number" => i * 123.45,
|
||||
"amount" => i * 24567.89,
|
||||
"qrcode" => $"QR_{field}_{i}",
|
||||
"barcode" => $"BAR_{field}_{i}",
|
||||
"image" => $"https://picsum.photos/seed/{Uri.EscapeDataString(field + "_" + i)}/260/120",
|
||||
_ => $"{field}_示例值_{i}"
|
||||
};
|
||||
fields.Add(field);
|
||||
}
|
||||
rows.Add(row);
|
||||
}
|
||||
obj[source] = rows;
|
||||
}
|
||||
}
|
||||
|
||||
var bind = (el["bindField"]?.ToString() ?? string.Empty).Trim();
|
||||
if (!string.IsNullOrWhiteSpace(bind))
|
||||
fields.Add(bind);
|
||||
|
||||
// 提取 text 中的 {{field}} 占位符(支持内嵌)
|
||||
var text = el["text"]?.ToString() ?? string.Empty;
|
||||
foreach (Match m in Regex.Matches(text, @"\{\{\s*([\w\.]+)\s*\}\}"))
|
||||
{
|
||||
var key = m.Groups[1].Value.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(key))
|
||||
fields.Add(key);
|
||||
}
|
||||
|
||||
CollectBindFields(el["cells"], fields);
|
||||
}
|
||||
|
||||
foreach (var f in fields.OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (f.Equals("pageNo", StringComparison.OrdinalIgnoreCase) || f.Equals("totalPages", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
if (!obj.ContainsKey(f))
|
||||
obj[f] = $"{f}_示例值";
|
||||
}
|
||||
|
||||
return obj.ToJsonString(new JsonSerializerOptions { WriteIndented = true });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
|
||||
private static void CollectBindFields(JsonNode? node, ISet<string> fields)
|
||||
{
|
||||
if (node == null) return;
|
||||
if (node is JsonObject o)
|
||||
{
|
||||
if (o.TryGetPropertyValue("bindField", out var bindNode))
|
||||
{
|
||||
var bind = bindNode?.ToString()?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(bind))
|
||||
fields.Add(bind);
|
||||
}
|
||||
foreach (var kv in o)
|
||||
CollectBindFields(kv.Value, fields);
|
||||
return;
|
||||
}
|
||||
if (node is JsonArray arr)
|
||||
{
|
||||
foreach (var it in arr)
|
||||
CollectBindFields(it, fields);
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildErrorHtml(string message)
|
||||
{
|
||||
var esc = message.Replace("&", "&").Replace("<", "<").Replace(">", ">");
|
||||
return "<html><head><meta charset=\"utf-8\"/><style>"
|
||||
+ "body{margin:0;background:#525659;display:flex;align-items:center;"
|
||||
+ "justify-content:center;height:100vh;font-family:'Microsoft YaHei',Arial,sans-serif;}"
|
||||
+ ".card{background:#fff;border-radius:8px;padding:32px 48px;text-align:center;"
|
||||
+ "box-shadow:0 6px 24px rgba(0,0,0,.4);max-width:560px;}"
|
||||
+ "</style></head><body><div class=\"card\">"
|
||||
+ "<div style=\"font-size:40px;margin-bottom:12px\">⚠️</div>"
|
||||
+ "<div style=\"font-size:13px;color:#e74c3c;word-break:break-all;\">渲染失败:" + esc + "</div>"
|
||||
+ "</div></body></html>";
|
||||
}
|
||||
|
||||
private async void RenderByParamJson_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await RenderCurrentParamJsonAsync();
|
||||
}
|
||||
|
||||
private async void GenerateMockJson_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
TbParamJson.Text = BuildMockParamJson(_templateJson);
|
||||
await RenderCurrentParamJsonAsync();
|
||||
}
|
||||
|
||||
private void CloseButton_Click(object sender, RoutedEventArgs e) => Close();
|
||||
}
|
||||
96
yy-admin-master/YY.Admin/Views/Print/PrintSettingsView.xaml
Normal file
96
yy-admin-master/YY.Admin/Views/Print/PrintSettingsView.xaml
Normal file
@@ -0,0 +1,96 @@
|
||||
<UserControl x:Class="YY.Admin.Views.Print.PrintSettingsView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="24" MaxWidth="680">
|
||||
|
||||
<!-- 标题 -->
|
||||
<TextBlock Text="打印设置" FontSize="18" FontWeight="Bold" Margin="0,0,0,20"/>
|
||||
|
||||
<!-- PrintDot 连接配置 -->
|
||||
<Border BorderThickness="1" BorderBrush="{DynamicResource BorderBrush}" CornerRadius="4" Padding="16" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="PrintDot 桥接器连接" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,12"/>
|
||||
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="120"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="WebSocket 地址" VerticalAlignment="Center" FontSize="13"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding WsUrl, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Placeholder="ws://192.168.x.x:1122/ws"
|
||||
FontSize="13" Padding="6,4"/>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="格式:ws://<IP>:1122/ws,支持局域网任意 IP" FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}" Margin="120,0,0,12"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
|
||||
<Button Content="测试连接并获取打印机" Command="{Binding TestConnectionCommand}"
|
||||
Style="{StaticResource ButtonPrimary}" Padding="12,6" FontSize="13" Margin="0,0,8,0"/>
|
||||
<Button Content="保存设置" Command="{Binding SaveCommand}"
|
||||
Style="{StaticResource ButtonDefault}" Padding="12,6" FontSize="13"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 状态提示 -->
|
||||
<TextBlock Text="{Binding StatusMessage}" FontSize="12" Foreground="{DynamicResource InfoBrush}"
|
||||
Visibility="{Binding StatusMessage, Converter={StaticResource String2VisibilityConverter}}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 打印机列表 -->
|
||||
<Border BorderThickness="1" BorderBrush="{DynamicResource BorderBrush}" CornerRadius="4" Padding="16" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="可用打印机" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,12"/>
|
||||
|
||||
<ListBox ItemsSource="{Binding Printers}" SelectedItem="{Binding SelectedPrinter}"
|
||||
MaxHeight="180" ScrollViewer.VerticalScrollBarVisibility="Auto">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" Margin="4,2">
|
||||
<TextBlock Text="{Binding Name}" FontSize="13"/>
|
||||
<TextBlock Text=" (默认)" FontSize="11" Foreground="{DynamicResource InfoBrush}"
|
||||
Visibility="{Binding IsDefault, Converter={StaticResource Boolean2VisibilityConverter}}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<TextBlock Margin="0,8,0,0" FontSize="12" Foreground="{DynamicResource SecondaryTextBrush}">
|
||||
已选打印机:<Run Text="{Binding SelectedPrinter.Name, FallbackValue='(未选择)'}"/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 打印模板列表 -->
|
||||
<Border BorderThickness="1" BorderBrush="{DynamicResource BorderBrush}" CornerRadius="4" Padding="16">
|
||||
<StackPanel>
|
||||
<DockPanel Margin="0,0,0,12">
|
||||
<Button DockPanel.Dock="Right" Content="刷新模板" Command="{Binding RefreshTemplatesCommand}"
|
||||
Style="{StaticResource ButtonDefault}" Padding="10,4" FontSize="12"/>
|
||||
<TextBlock Text="打印模板" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
|
||||
<DataGrid ItemsSource="{Binding Templates}" AutoGenerateColumns="False"
|
||||
IsReadOnly="True" MaxHeight="240" GridLinesVisibility="Horizontal"
|
||||
HeadersVisibility="Column" FontSize="12">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="模板编码" Binding="{Binding TemplateCode}" Width="160"/>
|
||||
<DataGridTextColumn Header="模板名称" Binding="{Binding TemplateName}" Width="*"/>
|
||||
<DataGridTextColumn Header="分类" Binding="{Binding Category}" Width="80"/>
|
||||
<DataGridTextColumn Header="纸宽(mm)" Binding="{Binding PaperWidthMm}" Width="70"/>
|
||||
<DataGridTextColumn Header="纸高(mm)" Binding="{Binding PaperHeightMm}" Width="70"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 忙碌指示 -->
|
||||
<hc:LoadingCircle Visibility="{Binding IsBusy, Converter={StaticResource Boolean2VisibilityConverter}}"
|
||||
HorizontalAlignment="Center" Margin="0,16,0,0"/>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace YY.Admin.Views.Print;
|
||||
|
||||
public partial class PrintSettingsView : UserControl
|
||||
{
|
||||
public PrintSettingsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
125
yy-admin-master/YY.Admin/Views/Print/PrintTemplateListView.xaml
Normal file
125
yy-admin-master/YY.Admin/Views/Print/PrintTemplateListView.xaml
Normal file
@@ -0,0 +1,125 @@
|
||||
<UserControl x:Class="YY.Admin.Views.Print.PrintTemplateListView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid Style="{StaticResource BaseViewStyle}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 搜索条件区域 -->
|
||||
<Border Grid.Row="0" CornerRadius="4" Margin="0 0 -10 0">
|
||||
<hc:Row>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
|
||||
<hc:TextBox Text="{Binding FilterCode, UpdateSourceTrigger=PropertyChanged}"
|
||||
Margin="0 0 10 10"
|
||||
hc:InfoElement.Title="模板编码"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.TitleWidth="65"
|
||||
hc:InfoElement.Placeholder="请输入模板编码"
|
||||
hc:InfoElement.ShowClearButton="True"/>
|
||||
</hc:Col>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
|
||||
<hc:TextBox Text="{Binding FilterName, UpdateSourceTrigger=PropertyChanged}"
|
||||
Margin="0 0 10 10"
|
||||
hc:InfoElement.Title="模板名称"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.TitleWidth="65"
|
||||
hc:InfoElement.Placeholder="请输入模板名称"
|
||||
hc:InfoElement.ShowClearButton="True"/>
|
||||
</hc:Col>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
|
||||
<hc:TextBox Text="{Binding FilterCategory, UpdateSourceTrigger=PropertyChanged}"
|
||||
Margin="0 0 10 10"
|
||||
hc:InfoElement.Title="分类"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.TitleWidth="65"
|
||||
hc:InfoElement.Placeholder="请输入分类"
|
||||
hc:InfoElement.ShowClearButton="True"/>
|
||||
</hc:Col>
|
||||
</hc:Row>
|
||||
</Border>
|
||||
|
||||
<!-- 操作工具栏 -->
|
||||
<Border Grid.Row="1" Margin="0,10">
|
||||
<hc:UniformSpacingPanel Spacing="10">
|
||||
<Button Style="{StaticResource ButtonPrimary}" Command="{Binding SearchCommand}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="Search"/>
|
||||
<TextBlock Text="搜索" Style="{StaticResource IconButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ButtonDefault}" Command="{Binding ResetCommand}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="Refresh"/>
|
||||
<TextBlock Text="重置" Style="{StaticResource IconButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</hc:UniformSpacingPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<DataGrid Grid.Row="2"
|
||||
ItemsSource="{Binding Templates}"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
CanUserAddRows="False"
|
||||
SelectionMode="Extended"
|
||||
SelectionUnit="FullRow"
|
||||
RowHeaderWidth="55"
|
||||
GridLinesVisibility="Horizontal"
|
||||
HorizontalGridLinesBrush="#FFEDEDED"
|
||||
VerticalGridLinesBrush="Transparent"
|
||||
HeadersVisibility="All"
|
||||
ColumnHeaderStyle="{StaticResource CusDataGridColumnHeaderStyle}"
|
||||
Style="{StaticResource CusDataGridStyle}"
|
||||
hc:DataGridAttach.ShowSelectAllButton="True"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Auto">
|
||||
<DataGrid.RowHeaderTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=DataGridRow}}"/>
|
||||
</DataTemplate>
|
||||
</DataGrid.RowHeaderTemplate>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="模板编码" Binding="{Binding TemplateCode}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="160"/>
|
||||
<DataGridTextColumn Header="模板名称" Binding="{Binding TemplateName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="*"/>
|
||||
<DataGridTextColumn Header="分类" Binding="{Binding Category}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="100"/>
|
||||
<DataGridTextColumn Header="纸宽(mm)" Binding="{Binding PaperWidthMm}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="90"/>
|
||||
<DataGridTextColumn Header="纸高(mm)" Binding="{Binding PaperHeightMm}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="90"/>
|
||||
<DataGridTextColumn Header="方向" Binding="{Binding PaperOrientation}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="80"/>
|
||||
<DataGridTextColumn Header="备注" Binding="{Binding Remark}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="180"/>
|
||||
<DataGridTextColumn Header="创建时间" Binding="{Binding CreateTime, StringFormat=yyyy-MM-dd HH:mm}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="130"/>
|
||||
<DataGridTemplateColumn Header="操作" Width="72" CanUserSort="False" CanUserResize="False">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Button Content="预览"
|
||||
Command="{Binding DataContext.PreviewCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||
CommandParameter="{Binding}"
|
||||
Style="{StaticResource ButtonPrimary}"
|
||||
Padding="0" Height="26" FontSize="12"
|
||||
Margin="4,0"/>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<!-- 底部状态栏 -->
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
|
||||
<TextBlock Text="{Binding StatusMessage}"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace YY.Admin.Views.Print;
|
||||
|
||||
public partial class PrintTemplateListView : UserControl
|
||||
{
|
||||
public PrintTemplateListView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3065.39" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http">
|
||||
<Version>10.0.7</Version>
|
||||
</PackageReference>
|
||||
|
||||
Reference in New Issue
Block a user