新增业务打印绑定功能,整合打印模板与业务数据的映射配置,优化打印数据生成逻辑。新增免密接口,支持桌面端打印模板的查询与列表展示,提升用户体验和系统的实时数据同步能力。同时,重构相关控制器以增强系统的可维护性和扩展性。

This commit is contained in:
geht
2026-05-14 10:43:51 +08:00
parent 642cecb04d
commit 8bcc34aee0
649 changed files with 18804 additions and 70 deletions

View File

@@ -215,6 +215,7 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/mes/material/mixerMaterial/anon/**", "anon"); filterChainDefinitionMap.put("/mes/material/mixerMaterial/anon/**", "anon");
// 打印模板免密接口(供桌面端调用) // 打印模板免密接口(供桌面端调用)
filterChainDefinitionMap.put("/print/template/anon/**", "anon"); filterChainDefinitionMap.put("/print/template/anon/**", "anon");
filterChainDefinitionMap.put("/print/bizTemplateBind/anon/**", "anon");
// 系统分类字典免密接口(供桌面端调用) // 系统分类字典免密接口(供桌面端调用)
filterChainDefinitionMap.put("/sys/category/anon/**", "anon"); filterChainDefinitionMap.put("/sys/category/anon/**", "anon");
// 桌面端用户反同步批量上报Outbox -> /sys/sync/batch // 桌面端用户反同步批量上报Outbox -> /sys/sync/batch

View File

@@ -636,6 +636,7 @@ public class MesXslDesktopAnonController {
ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson()); ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson());
JsonNode bizRoot = objectMapper.valueToTree(card); JsonNode bizRoot = objectMapper.valueToTree(card);
ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping); ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping);
PrintBizDataMappingUtil.fillMissingDataBindingParamKeys(printData, tpl.getTemplateJson());
Map<String, Object> out = new HashMap<>(8); Map<String, Object> out = new HashMap<>(8);
out.put("cardId", card.getId()); out.put("cardId", card.getId());
out.put("templateCode", bind.getTemplateCode()); out.put("templateCode", bind.getTemplateCode());
@@ -662,6 +663,7 @@ public class MesXslDesktopAnonController {
ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson()); ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson());
JsonNode bizRoot = objectMapper.valueToTree(entry); JsonNode bizRoot = objectMapper.valueToTree(entry);
ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping); ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping);
PrintBizDataMappingUtil.fillMissingDataBindingParamKeys(printData, tpl.getTemplateJson());
Map<String, Object> out = new HashMap<>(8); Map<String, Object> out = new HashMap<>(8);
out.put("entryId", entry.getId()); out.put("entryId", entry.getId());
out.put("templateCode", bind.getTemplateCode()); out.put("templateCode", bind.getTemplateCode());

View File

@@ -194,6 +194,7 @@ public class MesXslRawMaterialCardController extends JeecgController<MesXslRawMa
PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson()); PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson());
JsonNode bizRoot = objectMapper.valueToTree(card); JsonNode bizRoot = objectMapper.valueToTree(card);
ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping); ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping);
PrintBizDataMappingUtil.fillMissingDataBindingParamKeys(printData, tpl.getTemplateJson());
Map<String, Object> out = new HashMap<>(8); Map<String, Object> out = new HashMap<>(8);
out.put("cardId", card.getId()); out.put("cardId", card.getId());
out.put("templateCode", bind.getTemplateCode()); out.put("templateCode", bind.getTemplateCode());

View File

@@ -180,6 +180,7 @@ public class MesXslRawMaterialEntryController extends JeecgController<MesXslRawM
ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson()); ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson());
JsonNode bizRoot = objectMapper.valueToTree(entry); JsonNode bizRoot = objectMapper.valueToTree(entry);
ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping); ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping);
PrintBizDataMappingUtil.fillMissingDataBindingParamKeys(printData, tpl.getTemplateJson());
Map<String, Object> out = new HashMap<>(8); Map<String, Object> out = new HashMap<>(8);
out.put("entryId", entry.getId()); out.put("entryId", entry.getId());
out.put("templateCode", bind.getTemplateCode()); out.put("templateCode", bind.getTemplateCode());

View File

@@ -9,12 +9,14 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import com.alibaba.fastjson.JSON;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
@@ -36,11 +38,13 @@ import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
import org.jeecg.modules.print.vo.PrintBizTypeVO; import org.jeecg.modules.print.vo.PrintBizTypeVO;
import org.jeecg.modules.print.vo.PrintTemplateFieldItemVO; import org.jeecg.modules.print.vo.PrintTemplateFieldItemVO;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
/** /**
* 业务与打印模板绑定:可视化配置字段映射 * 业务与打印模板绑定:可视化配置字段映射
*/ */
@Slf4j
@Tag(name = "业务打印绑定") @Tag(name = "业务打印绑定")
@RestController @RestController
@RequestMapping("/print/bizTemplateBind") @RequestMapping("/print/bizTemplateBind")
@@ -51,6 +55,7 @@ public class PrintBizTemplateBindController extends JeecgController<PrintBizTemp
@Autowired private IPrintTemplateService printTemplateService; @Autowired private IPrintTemplateService printTemplateService;
@Autowired private IPrintBizBindPermWhitelistService printBizBindPermWhitelistService; @Autowired private IPrintBizBindPermWhitelistService printBizBindPermWhitelistService;
@Autowired private IPrintBizPermEntityService printBizPermEntityService; @Autowired private IPrintBizPermEntityService printBizPermEntityService;
@Autowired private SimpMessagingTemplate messagingTemplate;
@Operation(summary = "业务打印绑定-分页列表") @Operation(summary = "业务打印绑定-分页列表")
@GetMapping("/list") @GetMapping("/list")
@@ -81,6 +86,7 @@ public class PrintBizTemplateBindController extends JeecgController<PrintBizTemp
} }
normalizeMappingJson(entity); normalizeMappingJson(entity);
service.save(entity); service.save(entity);
publishPrintBizTemplateBindChanged("add", entity.getId());
return Result.OK("添加成功"); return Result.OK("添加成功");
} }
@@ -103,6 +109,7 @@ public class PrintBizTemplateBindController extends JeecgController<PrintBizTemp
} }
normalizeMappingJson(entity); normalizeMappingJson(entity);
service.updateById(entity); service.updateById(entity);
publishPrintBizTemplateBindChanged("edit", entity.getId());
return Result.OK("编辑成功"); return Result.OK("编辑成功");
} }
@@ -111,7 +118,9 @@ public class PrintBizTemplateBindController extends JeecgController<PrintBizTemp
@DeleteMapping("/delete") @DeleteMapping("/delete")
@RequiresPermissions("print:bizBind:delete") @RequiresPermissions("print:bizBind:delete")
public Result<String> delete(@RequestParam(name = "id") String id) { public Result<String> delete(@RequestParam(name = "id") String id) {
service.removeById(id); if (service.removeById(id)) {
publishPrintBizTemplateBindChanged("delete", id);
}
return Result.OK("删除成功"); return Result.OK("删除成功");
} }
@@ -239,6 +248,10 @@ public class PrintBizTemplateBindController extends JeecgController<PrintBizTemp
ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson()); ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson());
JsonNode bizRoot = PrintBizDataMappingUtil.parseBizJson(body.getBizDataJson()); JsonNode bizRoot = PrintBizDataMappingUtil.parseBizJson(body.getBizDataJson());
ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping); ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping);
PrintTemplate tpl = printTemplateService.getById(bind.getTemplateId());
if (tpl != null && StringUtils.isNotBlank(tpl.getTemplateJson())) {
PrintBizDataMappingUtil.fillMissingDataBindingParamKeys(printData, tpl.getTemplateJson());
}
Map<String, Object> res = new HashMap<>(4); Map<String, Object> res = new HashMap<>(4);
res.put("templateCode", bind.getTemplateCode()); res.put("templateCode", bind.getTemplateCode());
res.put("templateId", bind.getTemplateId()); res.put("templateId", bind.getTemplateId());
@@ -308,4 +321,43 @@ public class PrintBizTemplateBindController extends JeecgController<PrintBizTemp
public static class PermWhitelistBody { public static class PermWhitelistBody {
private java.util.List<String> permIds; private java.util.List<String> permIds;
} }
// ═══════════════════════════ 桌面端免密只读 ═══════════════════════════
@Operation(summary = "业务打印绑定-免密分页列表(桌面端缓存同步)")
@GetMapping("/anon/list")
public Result<IPage<PrintBizTemplateBind>> anonList(
PrintBizTemplateBind query,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "100") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<PrintBizTemplateBind> qw =
QueryGenerator.initQueryWrapper(query, req.getParameterMap());
qw.orderByDesc("create_time");
Page<PrintBizTemplateBind> page = new Page<>(pageNo, pageSize);
return Result.OK(service.page(page, qw));
}
@Operation(summary = "业务打印绑定-免密按 id 查询(桌面端)")
@GetMapping("/anon/queryById")
public Result<PrintBizTemplateBind> anonQueryById(@RequestParam(name = "id") String id) {
PrintBizTemplateBind row = service.getById(id);
return row != null ? Result.OK(row) : Result.error("未找到记录");
}
/**
* 广播到 /topic/sync/print-biz-binds与桌面端 PrintBizTemplateBindSyncCoordinator 对应。
*/
private void publishPrintBizTemplateBindChanged(String action, String bindId) {
try {
Map<String, Object> event = new HashMap<>();
event.put("cmd", "PRINT_BIZ_TEMPLATE_BIND_CHANGED");
event.put("action", action);
event.put("bindId", bindId);
event.put("timestamp", System.currentTimeMillis());
messagingTemplate.convertAndSend("/topic/sync/print-biz-binds", JSON.toJSONString(event));
} catch (Exception e) {
log.debug("广播 STOMP 事件失败 [PRINT_BIZ_TEMPLATE_BIND_CHANGED]: {}", e.getMessage());
}
}
} }

View File

@@ -4,7 +4,9 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.List;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.jeecg.modules.print.vo.PrintTemplateFieldItemVO;
/** 按映射规则把业务 JSON 转为模板打印数据(键为模板 bindField */ /** 按映射规则把业务 JSON 转为模板打印数据(键为模板 bindField */
public final class PrintBizDataMappingUtil { public final class PrintBizDataMappingUtil {
@@ -24,15 +26,75 @@ public final class PrintBizDataMappingUtil {
} }
String templateField = text(rule, "templateField"); String templateField = text(rule, "templateField");
String bizField = text(rule, "bizField"); String bizField = text(rule, "bizField");
if (StringUtils.isAnyBlank(templateField, bizField)) { // 仅要求模板字段名;业务字段为空表示「不参与取数」,仍向 printData 写入空字符串,避免模板占位符缺键
if (StringUtils.isBlank(templateField)) {
continue; continue;
} }
JsonNode val = resolvePath(bizRoot, bizField); JsonNode val;
if (StringUtils.isBlank(bizField)) {
val = MAPPER.getNodeFactory().textNode("");
} else {
val = resolvePath(bizRoot, bizField);
}
setPath(printData, templateField, val); setPath(printData, templateField, val);
} }
return printData; return printData;
} }
/**
* 按模板中已声明的绑定路径({@code dataBinding.params}、画布/表格等元素的 {@code bindField},与
* {@link PrintNativeTemplateFieldExtractor} 一致),向 printData 补齐缺失路径(空字符串)。
*
* <p>避免字段映射未包含某键时 API 缺键,桌面端渲染把「设计稿占位 text」当成数据显示。
*/
public static ObjectNode fillMissingDataBindingParamKeys(ObjectNode printData, String templateJson) {
if (printData == null) {
printData = MAPPER.createObjectNode();
}
if (StringUtils.isBlank(templateJson)) {
return printData;
}
try {
List<PrintTemplateFieldItemVO> fields = PrintNativeTemplateFieldExtractor.extract(templateJson);
for (PrintTemplateFieldItemVO item : fields) {
if (item == null || StringUtils.isBlank(item.getBindField())) {
continue;
}
String bf = item.getBindField().trim();
if (!hasPath(printData, bf)) {
setPath(printData, bf, MAPPER.getNodeFactory().textNode(""));
}
}
} catch (Exception ignored) {
// 模板解析异常时不阻断打印
}
return printData;
}
/** 判断 printData 上是否存在该点分路径(含嵌套对象) */
private static boolean hasPath(ObjectNode root, String path) {
if (StringUtils.isBlank(path)) {
return false;
}
String[] parts = path.split("\\.");
JsonNode cur = root;
for (int i = 0; i < parts.length; i++) {
if (cur == null || !cur.isObject()) {
return false;
}
ObjectNode obj = (ObjectNode) cur;
String p = parts[i];
if (p.isEmpty() || !obj.has(p)) {
return false;
}
if (i == parts.length - 1) {
return true;
}
cur = obj.get(p);
}
return false;
}
private static JsonNode resolvePath(JsonNode root, String path) { private static JsonNode resolvePath(JsonNode root, String path) {
if (root == null || StringUtils.isBlank(path)) { if (root == null || StringUtils.isBlank(path)) {
return null; return null;

View File

@@ -48,6 +48,7 @@ public class JeecgSystemApplication extends SpringBootServletInitializer {
String port = env.getProperty("server.port"); String port = env.getProperty("server.port");
String path = oConvertUtils.getString(env.getProperty("server.servlet.context-path")); String path = oConvertUtils.getString(env.getProperty("server.servlet.context-path"));
log.info("\n----------------------------------------------------------\n\t" + log.info("\n----------------------------------------------------------\n\t" +
"Application Jeecg-Boot is running! Access URLs:\n\t" + "Application Jeecg-Boot is running! Access URLs:\n\t" +
"Local: \t\thttp://localhost:" + port + path + "\n\t" + "Local: \t\thttp://localhost:" + port + path + "\n\t" +
"External: \thttp://" + ip + ":" + port + path + "/doc.html\n\t" + "External: \thttp://" + ip + ":" + port + path + "/doc.html\n\t" +

View File

@@ -129,6 +129,7 @@
> >
同名自动匹配 同名自动匹配
</a-button> </a-button>
<a-button size="small" @click="addPlaceholderParamRow">添加空占位</a-button>
</a-space> </a-space>
</template> </template>
@@ -590,14 +591,35 @@
function rebuildMappingRows() { function rebuildMappingRows() {
const saved = unref(savedMappingRef); const saved = unref(savedMappingRef);
mappingRows.value = unref(tplFields).map((t) => { const tplList = unref(tplFields);
const templateField = t.bindField; const tplByField = new Map(tplList.map((t) => [t.bindField, t]));
// 先按已保存 JSON 的顺序收录键(含「模板未再声明」的占位如 Parameter3 + 空 bizField避免仅依赖解析结果而丢行
const orderedKeys: string[] = [];
const seen = new Set<string>();
for (const s of saved) {
const k = (s.templateField || '').trim();
if (!k || seen.has(k)) continue;
seen.add(k);
orderedKeys.push(k);
}
for (const t of tplList) {
const k = (t.bindField || '').trim();
if (!k || seen.has(k)) continue;
seen.add(k);
orderedKeys.push(k);
}
mappingRows.value = orderedKeys.map((templateField) => {
const t = tplByField.get(templateField);
const hit = saved.find((x) => x.templateField === templateField); const hit = saved.find((x) => x.templateField === templateField);
return { return {
templateField, templateField,
bizField: hit?.bizField, bizField: hit?.bizField ?? '',
elementType: t.elementType, elementType: t?.elementType || 'param',
titleHint: t.titleHint, titleHint:
t?.titleHint ||
(!t ? '已保存映射(当前模板 JSON 未声明该占位,仍可按空业务字段输出)' : ''),
}; };
}); });
} }
@@ -615,10 +637,26 @@
mappingRows.value = [...unref(mappingRows)]; mappingRows.value = [...unref(mappingRows)];
} }
/** 手动增加仅输出空值的模板占位(写入 savedMappingRef 并重建行,避免再次「解析模板」时丢失) */
function addPlaceholderParamRow() {
const raw = window.prompt(
'请输入模板参数 bindField如 Parameter3。业务字段将固定为空字符串不参与业务 JSON 取值。',
'Parameter3',
);
const k = (raw || '').trim();
if (!k) return;
if (unref(savedMappingRef).some((x) => x.templateField === k) || unref(mappingRows).some((r) => r.templateField === k)) {
createMessage.warning('该占位已存在');
return;
}
savedMappingRef.value = [...unref(savedMappingRef), { templateField: k, bizField: '' }];
rebuildMappingRows();
}
function buildFieldMappingJson() { function buildFieldMappingJson() {
const arr = unref(mappingRows) const arr = unref(mappingRows)
.filter((r) => r.bizField) .filter((r) => r.templateField)
.map((r) => ({ templateField: r.templateField, bizField: r.bizField })); .map((r) => ({ templateField: r.templateField, bizField: r.bizField ?? '' }));
return JSON.stringify(arr); return JSON.stringify(arr);
} }

View File

@@ -0,0 +1,11 @@
using Prism.Events;
namespace YY.Admin.Core.Events;
public class PrintBizTemplateBindChangedPayload
{
public string Action { get; set; } = string.Empty;
public string? BindId { get; set; }
}
public class PrintBizTemplateBindChangedEvent : PubSubEvent<PrintBizTemplateBindChangedPayload> { }

View File

@@ -0,0 +1,12 @@
using YY.Admin.Core.Entity;
namespace YY.Admin.Core.Services;
/// <summary>业务打印绑定:免密拉取 + 本地缓存(只读)</summary>
public interface IPrintBizTemplateBindService
{
IReadOnlyList<PrintBizTemplateBind> GetCached();
Task<IReadOnlyList<PrintBizTemplateBind>> ListAsync(CancellationToken ct = default);
Task<IReadOnlyList<PrintBizTemplateBind>> RefreshCacheAsync(CancellationToken ct = default);
Task<PrintBizTemplateBind?> GetByIdAsync(string id, CancellationToken ct = default);
}

View File

@@ -24,6 +24,9 @@ public interface IRawMaterialEntryService
/// <summary>调用后端接口生成条码/批次号格式QH+物料编码+yyMMdd+序号)</summary> /// <summary>调用后端接口生成条码/批次号格式QH+物料编码+yyMMdd+序号)</summary>
Task<string?> GenerateBarcodeAsync(string materialCode, CancellationToken ct = default); Task<string?> GenerateBarcodeAsync(string materialCode, CancellationToken ct = default);
/// <summary>按业务打印绑定准备模板 JSON 与 printData与后端 prepareNativePrint 一致,免密 anon。</summary>
Task<(string templateJson, string printDataJson, string? errorMessage)> PrepareNativePrintAsync(string id, CancellationToken ct = default);
/// <summary> /// <summary>
/// 同步读取本地缓存的「全量入场记录」快照(深拷贝),不会触发远端拉取。 /// 同步读取本地缓存的「全量入场记录」快照(深拷贝),不会触发远端拉取。
/// 主要用于「磅单已入场重量」等跨表实时聚合,且需要保持与后端相同口径的场景。 /// 主要用于「磅单已入场重量」等跨表实时聚合,且需要保持与后端相同口径的场景。

View File

@@ -0,0 +1,19 @@
namespace YY.Admin.Core.Entity;
/// <summary>业务打印绑定print_biz_template_bind桌面端只读缓存</summary>
public class PrintBizTemplateBind
{
public string? Id { get; set; }
/// <summary>业务编码(菜单 permission id</summary>
public string? BizCode { get; set; }
public string? BizName { get; set; }
public string? TemplateId { get; set; }
public string? TemplateCode { get; set; }
/// <summary>字段映射 JSON</summary>
public string? FieldMappingJson { 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; }
}

View File

@@ -223,6 +223,9 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
new SysMenu{ Id=1300300110401, Pid=1300300110101, Title="增加", Permission="sysPrint:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, new SysMenu{ Id=1300300110401, Pid=1300300110101, Title="增加", Permission="sysPrint:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
new SysMenu{ Id=1300300110501, Pid=1300300110101, Title="删除", Permission="sysPrint:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, new SysMenu{ Id=1300300110501, Pid=1300300110101, Title="删除", Permission="sysPrint:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
// 业务打印绑定桌面端只读列表Web 端可在「打印模板」模块维护)
new SysMenu{ Id=1300300110701, Pid=1300300000101, Title="业务打印绑定", Path="/platform/printBizBind", Name="printBizBind", Component="PrintBizTemplateBindListView", Icon="&#xe824;", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=195 },
// 系统配置 // 系统配置
new SysMenu{ Id=1300300140101, Pid=1300300000101, Title="系统配置", Path="/platform/infoSetting", Name="sysInfoSetting", Component="/system/infoSetting/index", Icon="&#xe7a3;", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=220 }, new SysMenu{ Id=1300300140101, Pid=1300300000101, Title="系统配置", Path="/platform/infoSetting", Name="sysInfoSetting", Component="/system/infoSetting/index", Icon="&#xe7a3;", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=220 },
#endregion #endregion

View File

@@ -117,6 +117,7 @@ public class SysTenantMenuSeedData : ISqlSugarEntitySeedData<SysTenantMenu>
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200050401 }, new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200050401 },
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300010501 }, new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300010501 },
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300110501 }, new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300110501 },
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300110701 },
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200080401 }, new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200080401 },
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200020501 }, new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200020501 },
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300120501 }, new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300120501 },

View File

@@ -244,8 +244,10 @@ public static class NativePrintRenderService
double fs, string fw, string color, string align, double lh, double fs, string fw, string color, string align, double lh,
double designY, double pageHeightMm, int totalPages, bool bandRepeat = false) double designY, double pageHeightMm, int totalPages, bool bandRepeat = false)
{ {
var type = ReadAsString(el["type"], "text"); var type = ReadAsString(el["type"], "text");
var bindField = ReadAsString(el["bindField"]); // 设计器默认 bindField 为空字符串 "",须视为「未绑定」,否则误走数据分支导致标题等只剩空串
var bindFieldRaw = ReadAsString(el["bindField"]);
var bindField = string.IsNullOrWhiteSpace(bindFieldRaw) ? null : bindFieldRaw.Trim();
string text; string text;
if (type == "date") if (type == "date")
@@ -258,7 +260,8 @@ public static class NativePrintRenderService
} }
else if (bindField != null) else if (bindField != null)
{ {
text = ResolveField(data, bindField)?.ToString() ?? ReadAsString(el["text"], string.Empty); // 已绑定数据字段:缺键或解析不到时留空,不回退到画布上的设计占位 text否则会误显示「采购订单」等
text = ResolveField(data, bindField)?.ToString() ?? string.Empty;
} }
else else
{ {
@@ -291,10 +294,11 @@ public static class NativePrintRenderService
private static string RenderQrCode(JsonNode el, JsonObject data, string posStyle, double w, double h, private static string RenderQrCode(JsonNode el, JsonObject data, string posStyle, double w, double h,
bool bandRepeat = false, double designY = 0, double pageHeightMm = 0, int totalPages = 1) bool bandRepeat = false, double designY = 0, double pageHeightMm = 0, int totalPages = 1)
{ {
var bindField = ReadAsString(el["bindField"]); var bindFieldRaw = ReadAsString(el["bindField"]);
var value = ReadAsString(el["value"], string.Empty); var bindField = string.IsNullOrWhiteSpace(bindFieldRaw) ? null : bindFieldRaw.Trim();
var value = ReadAsString(el["value"], string.Empty);
if (bindField != null) if (bindField != null)
value = ResolveField(data, bindField)?.ToString() ?? value; value = ResolveField(data, bindField)?.ToString() ?? string.Empty;
if (string.IsNullOrWhiteSpace(value)) return string.Empty; if (string.IsNullOrWhiteSpace(value)) return string.Empty;
string inner; string inner;
@@ -332,10 +336,11 @@ public static class NativePrintRenderService
private static string RenderBarcode(JsonNode el, JsonObject data, string posStyle, double w, double h, private static string RenderBarcode(JsonNode el, JsonObject data, string posStyle, double w, double h,
bool bandRepeat = false, double designY = 0, double pageHeightMm = 0, int totalPages = 1) bool bandRepeat = false, double designY = 0, double pageHeightMm = 0, int totalPages = 1)
{ {
var bindField = ReadAsString(el["bindField"]); var bindFieldRaw = ReadAsString(el["bindField"]);
var value = ReadAsString(el["value"], string.Empty); var bindField = string.IsNullOrWhiteSpace(bindFieldRaw) ? null : bindFieldRaw.Trim();
var value = ReadAsString(el["value"], string.Empty);
if (bindField != null) if (bindField != null)
value = ResolveField(data, bindField)?.ToString() ?? value; value = ResolveField(data, bindField)?.ToString() ?? string.Empty;
if (string.IsNullOrWhiteSpace(value)) return string.Empty; if (string.IsNullOrWhiteSpace(value)) return string.Empty;
// 从元素配置取格式/显示文字开关;元素未设时按 Code128 + 显示文字默认 // 从元素配置取格式/显示文字开关;元素未设时按 Code128 + 显示文字默认
@@ -714,10 +719,11 @@ public static class NativePrintRenderService
private static string RenderImage(JsonNode el, JsonObject data, string posStyle, private static string RenderImage(JsonNode el, JsonObject data, string posStyle,
bool bandRepeat = false, double designY = 0, double pageHeightMm = 0, int totalPages = 1) bool bandRepeat = false, double designY = 0, double pageHeightMm = 0, int totalPages = 1)
{ {
var bindField = ReadAsString(el["bindField"]); var bindFieldRaw = ReadAsString(el["bindField"]);
var src = ReadAsString(el["src"], string.Empty); var bindField = string.IsNullOrWhiteSpace(bindFieldRaw) ? null : bindFieldRaw.Trim();
var src = ReadAsString(el["src"], string.Empty);
if (bindField != null) if (bindField != null)
src = ResolveField(data, bindField)?.ToString() ?? src; src = ResolveField(data, bindField)?.ToString() ?? string.Empty;
var fit = ReadAsString(el["fit"], "contain"); var fit = ReadAsString(el["fit"], "contain");
var objFit = fit switch { "fill" => "fill", "cover" => "cover", _ => "contain" }; var objFit = fit switch { "fill" => "fill", "cover" => "cover", _ => "contain" };
var inner = $"<img src=\"{EscapeHtml(src)}\" style=\"width:100%;height:100%;object-fit:{objFit};\" />"; var inner = $"<img src=\"{EscapeHtml(src)}\" style=\"width:100%;height:100%;object-fit:{objFit};\" />";
@@ -1147,7 +1153,7 @@ public static class NativePrintRenderService
// 解析原始值 // 解析原始值
string rawValue; string rawValue;
if (!string.IsNullOrWhiteSpace(cell.BindField)) if (!string.IsNullOrWhiteSpace(cell.BindField))
rawValue = ResolveField(data, cell.BindField!)?.ToString() ?? cell.Text; rawValue = ResolveField(data, cell.BindField!)?.ToString() ?? string.Empty;
else else
rawValue = cell.Text; rawValue = cell.Text;

View File

@@ -0,0 +1,172 @@
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 Microsoft.Extensions.Http;
using YY.Admin.Core.Entity;
using YY.Admin.Core.Services;
namespace YY.Admin.Services.Service.Print;
/// <summary>业务打印绑定:免密列表 + 本地缓存(只读)</summary>
public class PrintBizTemplateBindService : IPrintBizTemplateBindService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _configuration;
private readonly object _cacheLock = new();
private List<PrintBizTemplateBind> _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 PrintBizTemplateBindService(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-biz-bind-cache.json");
LoadCacheFromDisk();
}
public IReadOnlyList<PrintBizTemplateBind> GetCached()
{
lock (_cacheLock)
{
return _localCache.AsReadOnly();
}
}
public async Task<IReadOnlyList<PrintBizTemplateBind>> ListAsync(CancellationToken ct = default)
{
try
{
return await RefreshCacheAsync(ct).ConfigureAwait(false);
}
catch
{
return GetCached();
}
}
public async Task<IReadOnlyList<PrintBizTemplateBind>> RefreshCacheAsync(CancellationToken ct = default)
{
var client = _httpClientFactory.CreateClient("JeecgApi");
var url = $"{BaseUrl}/print/bizTemplateBind/anon/list?pageSize=500&pageNo=1";
var response = await client.GetAsync(url, ct).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JeecgResult<JeecgPage<PrintBizTemplateBind>>>(JsonOpts, ct).ConfigureAwait(false);
if (result?.Success != true)
throw new InvalidOperationException(result?.Message ?? "后端返回失败");
var records = result.Result?.Records ?? new List<PrintBizTemplateBind>();
lock (_cacheLock)
{
_localCache = records;
SaveCacheToDiskUnsafe();
}
return records.AsReadOnly();
}
public async Task<PrintBizTemplateBind?> GetByIdAsync(string id, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(id)) return null;
try
{
var client = _httpClientFactory.CreateClient("JeecgApi");
var url = $"{BaseUrl}/print/bizTemplateBind/anon/queryById?id={Uri.EscapeDataString(id)}";
var response = await client.GetAsync(url, ct).ConfigureAwait(false);
if (!response.IsSuccessStatusCode) return GetCachedById(id);
var result = await response.Content.ReadFromJsonAsync<JeecgResult<PrintBizTemplateBind>>(JsonOpts, ct).ConfigureAwait(false);
return result?.Success == true ? result.Result : GetCachedById(id);
}
catch
{
return GetCachedById(id);
}
}
private PrintBizTemplateBind? GetCachedById(string id)
{
lock (_cacheLock)
{
return _localCache.FirstOrDefault(x => string.Equals(x.Id, id, StringComparison.OrdinalIgnoreCase));
}
}
private void LoadCacheFromDisk()
{
try
{
if (!File.Exists(_cacheFilePath)) return;
var json = File.ReadAllText(_cacheFilePath);
var data = JsonSerializer.Deserialize<List<PrintBizTemplateBind>>(json, JsonOpts);
_localCache = data ?? new List<PrintBizTemplateBind>();
}
catch
{
_localCache = new List<PrintBizTemplateBind>();
}
}
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();
}
}
}

View File

@@ -0,0 +1,77 @@
using Prism.Events;
using System.Text.Json;
using YY.Admin.Core;
using YY.Admin.Core.Events;
namespace YY.Admin.Services.Service.Print;
/// <summary>订阅 STOMPPRINT_BIZ_TEMPLATE_BIND_CHANGED驱动列表静默刷新网络恢复补偿。</summary>
public class PrintBizTemplateBindSyncCoordinator : ISingletonDependency
{
private readonly IEventAggregator _eventAggregator;
private readonly ILoggerService _logger;
private SubscriptionToken? _remoteCommandToken;
private SubscriptionToken? _networkStatusToken;
public PrintBizTemplateBindSyncCoordinator(
IEventAggregator eventAggregator,
SyncPollManager pollManager,
ILoggerService logger)
{
_eventAggregator = eventAggregator;
_logger = logger;
_remoteCommandToken = _eventAggregator
.GetEvent<RemoteCommandReceivedEvent>()
.Subscribe(OnRemoteCommand, ThreadOption.BackgroundThread);
_networkStatusToken = _eventAggregator
.GetEvent<NetworkStatusChangedEvent>()
.Subscribe(OnNetworkStatusChanged, ThreadOption.BackgroundThread);
pollManager.Register("业务打印绑定", () =>
{
_eventAggregator.GetEvent<PrintBizTemplateBindChangedEvent>()
.Publish(new PrintBizTemplateBindChangedPayload { Action = "poll" });
return Task.CompletedTask;
});
_logger.Information("[业务打印绑定推送] PrintBizTemplateBindSyncCoordinator 已启动");
}
private void OnNetworkStatusChanged(NetworkStatusChangedPayload payload)
{
if (!payload.IsOnline) return;
_logger.Information("[业务打印绑定推送] 网络恢复,触发补偿刷新");
_eventAggregator.GetEvent<PrintBizTemplateBindChangedEvent>()
.Publish(new PrintBizTemplateBindChangedPayload { 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_BIZ_TEMPLATE_BIND_CHANGED", StringComparison.OrdinalIgnoreCase)) return;
doc.RootElement.TryGetProperty("action", out var actionEl);
doc.RootElement.TryGetProperty("bindId", out var idEl);
var changedPayload = new PrintBizTemplateBindChangedPayload
{
Action = actionEl.GetString() ?? string.Empty,
BindId = idEl.ValueKind == JsonValueKind.String ? idEl.GetString() : null
};
_logger.Information($"[业务打印绑定推送] 收到变更 action={changedPayload.Action}, bindId={changedPayload.BindId}");
_eventAggregator.GetEvent<PrintBizTemplateBindChangedEvent>().Publish(changedPayload);
}
catch (Exception ex)
{
_logger.Warning($"[业务打印绑定推送] 处理 STOMP 信号失败: {ex.Message}");
}
}
}

View File

@@ -237,6 +237,39 @@ public class RawMaterialEntryService : IRawMaterialEntryService, ISingletonDepen
return null; return null;
} }
public async Task<(string templateJson, string printDataJson, string? errorMessage)> PrepareNativePrintAsync(string id, CancellationToken ct = default)
{
if (!_networkMonitor.IsOnline)
return (string.Empty, "{}", "当前离线,无法获取打印数据");
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 (string.Empty, "{}", $"获取打印数据失败:{ex.Message}");
}
}
public IReadOnlyList<MesXslRawMaterialEntry> GetCachedSnapshot() public IReadOnlyList<MesXslRawMaterialEntry> GetCachedSnapshot()
{ {
// 注意:不允许直接返回 _localCache 引用,避免外部修改污染缓存;用 Clone 做深拷贝。 // 注意:不允许直接返回 _localCache 引用,避免外部修改污染缓存;用 Clone 做深拷贝。

View File

@@ -166,6 +166,10 @@ public class StompWebSocketService : ISignalRService
await SendFrameAsync( await SendFrameAsync(
BuildSubscribeFrame("sub-print-templates", "/topic/sync/print-templates"), BuildSubscribeFrame("sub-print-templates", "/topic/sync/print-templates"),
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
// 业务打印绑定变更:订阅 /topic/sync/print-biz-binds
await SendFrameAsync(
BuildSubscribeFrame("sub-print-biz-binds", "/topic/sync/print-biz-binds"),
cancellationToken).ConfigureAwait(false);
// 订阅服务端 PONG 回复(应用层假在线检测) // 订阅服务端 PONG 回复(应用层假在线检测)
await SendFrameAsync( await SendFrameAsync(

View File

@@ -86,6 +86,8 @@ namespace YY.Admin
containerRegistry.RegisterForNavigation<PrintSettingsView>(); containerRegistry.RegisterForNavigation<PrintSettingsView>();
// 打印模板列表 // 打印模板列表
containerRegistry.RegisterForNavigation<PrintTemplateListView>(); containerRegistry.RegisterForNavigation<PrintTemplateListView>();
// 业务打印绑定(只读缓存)
containerRegistry.RegisterForNavigation<PrintBizTemplateBindListView>();
} }
} }
public class DialogWindow : Window, IDialogWindow public class DialogWindow : Window, IDialogWindow

View File

@@ -78,7 +78,9 @@ public class SyncModule : IModule
// 打印服务PrintDot 桥接器 + 打印模板(含 STOMP 实时同步 + 本地缓存) // 打印服务PrintDot 桥接器 + 打印模板(含 STOMP 实时同步 + 本地缓存)
containerRegistry.RegisterSingleton<IPrintDotService, PrintDotService>(); containerRegistry.RegisterSingleton<IPrintDotService, PrintDotService>();
containerRegistry.RegisterSingleton<IPrintTemplateService, PrintTemplateService>(); containerRegistry.RegisterSingleton<IPrintTemplateService, PrintTemplateService>();
containerRegistry.RegisterSingleton<IPrintBizTemplateBindService, PrintBizTemplateBindService>();
containerRegistry.RegisterSingleton<PrintTemplateSyncCoordinator>(); containerRegistry.RegisterSingleton<PrintTemplateSyncCoordinator>();
containerRegistry.RegisterSingleton<PrintBizTemplateBindSyncCoordinator>();
var serviceCollection = new ServiceCollection(); var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<DisconnectGuardHandler>(); serviceCollection.AddTransient<DisconnectGuardHandler>();
@@ -147,6 +149,7 @@ public class SyncModule : IModule
_ = containerProvider.Resolve<DictSyncCoordinator>(); _ = containerProvider.Resolve<DictSyncCoordinator>();
// 强制实例化打印模板同步协调器 // 强制实例化打印模板同步协调器
_ = containerProvider.Resolve<PrintTemplateSyncCoordinator>(); _ = containerProvider.Resolve<PrintTemplateSyncCoordinator>();
_ = containerProvider.Resolve<PrintBizTemplateBindSyncCoordinator>();
} }
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy() private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()

View File

@@ -151,7 +151,12 @@ namespace YY.Admin.ViewModels.Control
["PrintTemplateListView"] = "PrintTemplateListView", ["PrintTemplateListView"] = "PrintTemplateListView",
["/platform/print"] = "PrintTemplateListView", ["/platform/print"] = "PrintTemplateListView",
["print"] = "PrintTemplateListView", ["print"] = "PrintTemplateListView",
["printTemplate"] = "PrintTemplateListView" ["printTemplate"] = "PrintTemplateListView",
// 已实现页面:业务打印绑定(桌面端)
["PrintBizTemplateBindListView"] = "PrintBizTemplateBindListView",
["/platform/printBizBind"] = "PrintBizTemplateBindListView",
["printBizBind"] = "PrintBizTemplateBindListView"
}; };
private MenuItem? _selectedMenuItem; private MenuItem? _selectedMenuItem;

View File

@@ -0,0 +1,192 @@
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.Views.Print;
namespace YY.Admin.ViewModels.Print;
/// <summary>业务打印绑定列表(只读,本地缓存 + 同步)</summary>
public class PrintBizTemplateBindListViewModel : BaseViewModel
{
private readonly IPrintBizTemplateBindService _bindService;
private SubscriptionToken? _changeToken;
private List<PrintBizTemplateBind> _all = new();
public ObservableCollection<PrintBizTemplateBind> Items { get; } = new();
private string _statusMessage = string.Empty;
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
private string? _filterBizCode;
public string? FilterBizCode
{
get => _filterBizCode;
set => SetProperty(ref _filterBizCode, value);
}
private string? _filterBizName;
public string? FilterBizName
{
get => _filterBizName;
set => SetProperty(ref _filterBizName, value);
}
private string? _filterTemplateCode;
public string? FilterTemplateCode
{
get => _filterTemplateCode;
set => SetProperty(ref _filterTemplateCode, value);
}
public DelegateCommand SearchCommand { get; }
public DelegateCommand ResetCommand { get; }
public DelegateCommand<PrintBizTemplateBind> DetailCommand { get; }
public PrintBizTemplateBindListViewModel(
IPrintBizTemplateBindService bindService,
IContainerExtension container,
IRegionManager regionManager) : base(container, regionManager)
{
_bindService = bindService;
SearchCommand = new DelegateCommand(ApplyFilter);
ResetCommand = new DelegateCommand(() =>
{
FilterBizCode = null;
FilterBizName = null;
FilterTemplateCode = null;
ApplyFilter();
});
DetailCommand = new DelegateCommand<PrintBizTemplateBind>(OpenDetail);
_changeToken = _eventAggregator
.GetEvent<PrintBizTemplateBindChangedEvent>()
.Subscribe(_ => { RefreshSilentlyAsync().ConfigureAwait(false); }, ThreadOption.UIThread);
ShowCached();
_ = RefreshSilentlyAsync();
}
private void ShowCached()
{
var cached = _bindService.GetCached();
if (cached.Count == 0) return;
_all = cached.ToList();
ApplyFilter();
UpdateStatus();
}
private async Task RefreshSilentlyAsync()
{
try
{
var list = await _bindService.RefreshCacheAsync().ConfigureAwait(false);
await Application.Current.Dispatcher.InvokeAsync(() =>
{
_all = list.ToList();
ApplyFilter();
UpdateStatus();
});
}
catch
{
var cached = _bindService.GetCached();
if (cached.Count > 0 && _all.Count == 0)
{
await Application.Current.Dispatcher.InvokeAsync(() =>
{
_all = cached.ToList();
ApplyFilter();
UpdateStatus();
});
}
}
}
private void ApplyFilter()
{
IEnumerable<PrintBizTemplateBind> result = _all;
if (!string.IsNullOrWhiteSpace(FilterBizCode))
result = result.Where(x => (x.BizCode ?? string.Empty)
.Contains(FilterBizCode, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(FilterBizName))
result = result.Where(x => (x.BizName ?? string.Empty)
.Contains(FilterBizName, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(FilterTemplateCode))
result = result.Where(x => (x.TemplateCode ?? string.Empty)
.Contains(FilterTemplateCode, StringComparison.OrdinalIgnoreCase));
var filtered = result.ToList();
for (int i = Items.Count - 1; i >= 0; i--)
{
if (!filtered.Any(t => t.Id == Items[i].Id))
Items.RemoveAt(i);
}
for (int i = 0; i < filtered.Count; i++)
{
var item = filtered[i];
var existingIdx = -1;
for (int j = 0; j < Items.Count; j++)
{
if (Items[j].Id == item.Id) { existingIdx = j; break; }
}
if (existingIdx < 0)
Items.Insert(i, item);
else
{
if (existingIdx != i) Items.Move(existingIdx, i);
Items[i] = item;
}
}
}
private void UpdateStatus()
{
var hasFilter = !string.IsNullOrWhiteSpace(FilterBizCode)
|| !string.IsNullOrWhiteSpace(FilterBizName)
|| !string.IsNullOrWhiteSpace(FilterTemplateCode);
StatusMessage = hasFilter
? $"筛选结果 {Items.Count} / {_all.Count} 条"
: _all.Count > 0
? $"共 {_all.Count} 条绑定(缓存于本地,可离线查看)"
: "暂无数据(联网后将自动同步)";
}
private async void OpenDetail(PrintBizTemplateBind? row)
{
if (row == null) return;
PrintBizTemplateBind model = row;
try
{
var fresh = await _bindService.GetByIdAsync(row.Id ?? "").ConfigureAwait(false);
if (fresh != null) model = fresh;
}
catch { /* 使用列表行数据 */ }
// 网络请求 continuation 可能在非 STA 线程Window 必须在 UI 线程创建
var app = Application.Current;
if (app?.Dispatcher == null) return;
await app.Dispatcher.InvokeAsync(() =>
{
var win = new PrintBizTemplateBindDetailWindow(model)
{
Owner = app.MainWindow
};
win.ShowDialog();
});
}
}

View File

@@ -366,44 +366,56 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
AddSplitDetailCommand.RaiseCanExecuteChanged(); AddSplitDetailCommand.RaiseCanExecuteChanged();
} }
/// <summary>校验并提交新增/编辑;不弹关闭、不执行独立页的 InitializeForAdd。成功时 Result=true。</summary>
protected async Task<bool> PersistEntryCoreAsync()
{
if (Entry == null) return false;
ApplySplitDetailsToEntry();
ApplyDefaultEntryStatusForAdd();
ApplyHiddenFieldDefaultsForAdd();
var missing = new List<string>();
if (string.IsNullOrWhiteSpace(Entry.MaterialId)) missing.Add("密炼物料");
if (string.IsNullOrWhiteSpace(Entry.BillNo)) missing.Add("榜单号");
if (string.IsNullOrWhiteSpace(Entry.UnloadOperator)) missing.Add("卸货人");
if (string.IsNullOrWhiteSpace(Entry.SupplierName)) missing.Add("供应商名称");
if (missing.Count > 0)
{
HandyControl.Controls.MessageBox.Warning($"以下必填项不能为空:{string.Join("", missing)}");
return false;
}
bool ok;
if (IsAddMode)
{
ok = await _entryService.AddAsync(Entry);
if (!ok) { HandyControl.Controls.MessageBox.Error("新增失败!"); return false; }
}
else
{
ok = await _entryService.EditAsync(Entry);
if (!ok) { HandyControl.Controls.MessageBox.Error("编辑失败!"); return false; }
}
Result = ok;
return true;
}
protected virtual async Task SaveAsync() protected virtual async Task SaveAsync()
{ {
if (Entry == null) return; if (Entry == null) return;
try try
{ {
ApplySplitDetailsToEntry(); if (!await PersistEntryCoreAsync()) return;
ApplyDefaultEntryStatusForAdd();
ApplyHiddenFieldDefaultsForAdd();
var missing = new List<string>();
if (string.IsNullOrWhiteSpace(Entry.MaterialId)) missing.Add("密炼物料");
if (string.IsNullOrWhiteSpace(Entry.BillNo)) missing.Add("榜单号");
if (string.IsNullOrWhiteSpace(Entry.UnloadOperator)) missing.Add("卸货人");
if (string.IsNullOrWhiteSpace(Entry.SupplierName)) missing.Add("供应商名称");
if (missing.Count > 0)
{
HandyControl.Controls.MessageBox.Warning($"以下必填项不能为空:{string.Join("", missing)}");
return;
}
bool ok;
if (IsAddMode) if (IsAddMode)
{ {
ok = await _entryService.AddAsync(Entry); HandyControl.Controls.MessageBox.Success("新增成功!");
if (ok) HandyControl.Controls.MessageBox.Success("新增成功!"); if (CloseAction == null)
else { HandyControl.Controls.MessageBox.Error("新增失败!"); return; } {
} // 独立新增页面:保存成功后自动清空表单,便于连续录入
else InitializeForAdd();
{ return;
ok = await _entryService.EditAsync(Entry); }
if (!ok) { HandyControl.Controls.MessageBox.Error("编辑失败!"); return; }
}
Result = ok;
if (IsAddMode && CloseAction == null)
{
// 独立新增页面:保存成功后自动清空表单,便于连续录入
InitializeForAdd();
return;
} }
CloseAction?.Invoke(); CloseAction?.Invoke();

View File

@@ -1,10 +1,16 @@
using Prism.Commands; using Prism.Commands;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Net;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes;
using System.Windows;
using System.Windows.Threading;
using YY.Admin.Core.Entity; using YY.Admin.Core.Entity;
using YY.Admin.Core.Services; using YY.Admin.Core.Services;
using YY.Admin.Infrastructure.Print;
using YY.Admin.Services.Service; using YY.Admin.Services.Service;
using YY.Admin.Services.Service.Print;
namespace YY.Admin.ViewModels.RawMaterialEntry; namespace YY.Admin.ViewModels.RawMaterialEntry;
@@ -14,7 +20,12 @@ namespace YY.Admin.ViewModels.RawMaterialEntry;
public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogViewModel public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogViewModel
{ {
private const int TodayListFetchSize = 5000; private const int TodayListFetchSize = 5000;
private const string RawMaterialEntryBizCode = "1900000000000000530";
private const string RawMaterialEntryTemplateCode = "MES_RAW_MATERIAL_ENTRY";
private readonly IRawMaterialCardService _rawMaterialCardService; private readonly IRawMaterialCardService _rawMaterialCardService;
private readonly IPrintDotService _printDotService;
private readonly IPrintBizTemplateBindService _printBizTemplateBindService;
private readonly IPrintTemplateService _printTemplateService;
private static readonly JsonSerializerOptions LayoutJsonOpts = new() private static readonly JsonSerializerOptions LayoutJsonOpts = new()
{ {
@@ -95,25 +106,131 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
public DelegateCommand GenerateRawMaterialCardsCommand { get; } public DelegateCommand GenerateRawMaterialCardsCommand { get; }
/// <summary>「重新拆码」:清除已生成的卡片 + 清空明细,仅在编辑态可用。</summary> /// <summary>「重新拆码」:清除已生成的卡片 + 清空明细,仅在编辑态可用。</summary>
public DelegateCommand ResplitCommand { get; } public DelegateCommand ResplitCommand { get; }
public DelegateCommand SaveAndPrintCommand { get; }
public DelegateCommand RefreshPrintersCommand { get; }
/// <summary>PrintDot 桥接器返回的打印机列表(与打印模板页一致)。</summary>
public ObservableCollection<PrintDotPrinter> Printers { get; } = new();
private bool _suppressPrinterSave;
private PrintDotPrinter? _selectedPrinter;
public PrintDotPrinter? SelectedPrinter
{
get => _selectedPrinter;
set
{
if (!SetProperty(ref _selectedPrinter, value)) return;
if (_suppressPrinterSave) return;
var s = PrintDotSettings.Load();
s.SelectedPrinter = value?.Name ?? string.Empty;
s.Save();
}
}
private string _printerStatus = string.Empty;
public string PrinterStatus
{
get => _printerStatus;
set => SetProperty(ref _printerStatus, value);
}
private static readonly JsonSerializerOptions PreviewSnapshotJsonOpts = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
private readonly DispatcherTimer _printPreviewTimer;
private string _lastPreviewSnapshot = string.Empty;
private int _previewRequestGeneration;
private bool _printPreviewBusy;
private bool _previewTemplateLoaded;
private string _previewTemplateJson = string.Empty;
private string _previewTemplateName = "原料入场记录";
private string _previewTemplateCode = RawMaterialEntryTemplateCode;
private string _previewFieldMappingJson = "[]";
private bool _isPrintPreviewExpanded = true;
/// <summary>右侧下方「入场标签打印预览」折叠面板是否展开。</summary>
public bool IsPrintPreviewExpanded
{
get => _isPrintPreviewExpanded;
set
{
if (!SetProperty(ref _isPrintPreviewExpanded, value)) return;
if (value) _lastPreviewSnapshot = string.Empty;
}
}
private string _printPreviewStatus = string.Empty;
/// <summary>预览区状态提示(如离线、加载中、错误摘要)。</summary>
public string PrintPreviewStatus
{
get => _printPreviewStatus;
private set => SetProperty(ref _printPreviewStatus, value);
}
/// <summary>由 View 订阅,在 UI 线程将 HTML 交给 WebView2。</summary>
public event EventHandler<string>? PrintPreviewHtmlReady;
private bool _isActionBusy;
/// <summary>底部动作按钮(保存/保存并打印/生成卡片)是否处于执行中。</summary>
public bool IsActionBusy
{
get => _isActionBusy;
private set
{
if (SetProperty(ref _isActionBusy, value))
{
RaisePropertyChanged(nameof(IsNotActionBusy));
}
}
}
public bool IsNotActionBusy => !IsActionBusy;
private string _actionBusyText = "处理中...";
/// <summary>动作执行中的遮罩提示文案。</summary>
public string ActionBusyText
{
get => _actionBusyText;
private set => SetProperty(ref _actionBusyText, value);
}
public RawMaterialEntryOperationViewModel( public RawMaterialEntryOperationViewModel(
IRawMaterialEntryService entryService, IRawMaterialEntryService entryService,
IJeecgDictSyncService dictSyncService, IJeecgDictSyncService dictSyncService,
IMixerMaterialService mixerMaterialService, IMixerMaterialService mixerMaterialService,
IRawMaterialCardService rawMaterialCardService, IRawMaterialCardService rawMaterialCardService,
IPrintDotService printDotService,
IPrintBizTemplateBindService printBizTemplateBindService,
IPrintTemplateService printTemplateService,
IContainerExtension container, IContainerExtension container,
IRegionManager regionManager) IRegionManager regionManager)
: base(entryService, dictSyncService, mixerMaterialService, container, regionManager) : base(entryService, dictSyncService, mixerMaterialService, container, regionManager)
{ {
_rawMaterialCardService = rawMaterialCardService; _rawMaterialCardService = rawMaterialCardService;
_printDotService = printDotService;
_printBizTemplateBindService = printBizTemplateBindService;
_printTemplateService = printTemplateService;
LoadLayoutState(); LoadLayoutState();
ToggleRightPanelCommand = new DelegateCommand(() => IsRightPanelExpanded = !IsRightPanelExpanded); ToggleRightPanelCommand = new DelegateCommand(() => IsRightPanelExpanded = !IsRightPanelExpanded);
RefreshTodayEntriesCommand = new DelegateCommand(async () => await LoadTodayEntriesAsync()); RefreshTodayEntriesCommand = new DelegateCommand(async () => await LoadTodayEntriesAsync());
GenerateRawMaterialCardsCommand = new DelegateCommand(async () => await GenerateRawMaterialCardsAsync()); GenerateRawMaterialCardsCommand = new DelegateCommand(async () => await GenerateRawMaterialCardsAsync());
ResplitCommand = new DelegateCommand(async () => await ResplitAsync(), () => CanResplit) ResplitCommand = new DelegateCommand(async () => await ResplitAsync(), () => CanResplit)
.ObservesProperty(() => Entry); .ObservesProperty(() => Entry);
SaveAndPrintCommand = new DelegateCommand(async () => await SaveAndPrintAsync());
RefreshPrintersCommand = new DelegateCommand(async () => await RefreshPrintersAsync(verbose: true));
// 集合变化:批量重订阅 item.PropertyChanged 监听 HasCard/Portions并同步刷新两个 Can*。 // 集合变化:批量重订阅 item.PropertyChanged 监听 HasCard/Portions并同步刷新两个 Can*。
SplitCodeDetails.CollectionChanged += OnSplitCodeDetailsCollectionChangedForCanFlags; SplitCodeDetails.CollectionChanged += OnSplitCodeDetailsCollectionChangedForCanFlags;
_ = RefreshPrintersAsync(verbose: false);
var dispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
_printPreviewTimer = new DispatcherTimer(
TimeSpan.FromMilliseconds(480),
DispatcherPriority.Background,
(_, _) => _ = OnPrintPreviewTickAsync(),
dispatcher);
_printPreviewTimer.Start();
} }
/// <summary> /// <summary>
@@ -160,6 +277,7 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
public override void InitializeForAdd() public override void InitializeForAdd()
{ {
_lastPreviewSnapshot = string.Empty;
_suppressTodaySelectionReaction = true; _suppressTodaySelectionReaction = true;
_selectedTodayEntry = null; _selectedTodayEntry = null;
RaisePropertyChanged(nameof(SelectedTodayEntry)); RaisePropertyChanged(nameof(SelectedTodayEntry));
@@ -168,6 +286,149 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
RaisePropertyChanged(nameof(CanGenerateCards)); RaisePropertyChanged(nameof(CanGenerateCards));
} }
/// <summary>保存后按业务打印绑定拉取模板与 printData直接发送到 PrintDot 打印机(不弹预览窗)。</summary>
private async Task SaveAndPrintAsync()
{
if (IsActionBusy) return;
if (Entry == null) return;
var wasEdit = !IsAddMode;
var barcode = Entry.Barcode;
BeginActionBusy("保存并打印中...");
try
{
IsLoading = true;
if (!await PersistEntryCoreAsync()) return;
string? entryId = Entry.Id;
if (string.IsNullOrWhiteSpace(entryId) && !string.IsNullOrWhiteSpace(barcode))
{
var page = await EntryService.PageAsync(1, 50, barcode: barcode);
entryId = page.Records
.FirstOrDefault(e => string.Equals(e.Barcode, barcode, StringComparison.OrdinalIgnoreCase))?.Id
?? page.Records
.OrderByDescending(e => e.EntryTime ?? e.CreateTime ?? DateTime.MinValue)
.FirstOrDefault()?.Id;
}
if (string.IsNullOrWhiteSpace(entryId))
{
HandyControl.Controls.MessageBox.Warning("保存成功,但未能解析记录主键,无法打印。请从右侧列表选中该条后再试。");
}
else
{
var (templateJson, printDataJson, err) = await EntryService.PrepareNativePrintAsync(entryId);
if (!string.IsNullOrWhiteSpace(err))
{
HandyControl.Controls.MessageBox.Error($"保存成功,但打印准备失败:{err}");
}
else
{
var selectedPrinterName = SelectedPrinter?.Name?.Trim();
if (string.IsNullOrWhiteSpace(selectedPrinterName))
{
HandyControl.Controls.MessageBox.Warning("保存成功,但未选择打印机。请先选择打印机后再试。");
}
else
{
JsonObject? dataObj = JsonNode.Parse(printDataJson) as JsonObject;
dataObj ??= new JsonObject();
var html = NativePrintRenderService.RenderToHtml(templateJson, dataObj);
var tpl = BuildPrintTemplateForEntry(templateJson);
var pdfBase64 = await HtmlToPdfRenderer.RenderAsync(
html,
tpl.PaperWidthMm ?? 210d,
tpl.PaperHeightMm ?? 297d);
var jobName = string.IsNullOrWhiteSpace(barcode) ? "原料入场记录" : $"原料入场记录-{barcode}";
await _printDotService.PrintAsync(selectedPrinterName, pdfBase64, jobName, 1);
}
}
}
if (wasEdit)
HandyControl.Controls.MessageBox.Success("编辑成功!");
else
HandyControl.Controls.MessageBox.Success("新增成功!");
Result = true;
RaisePropertyChanged(nameof(CanGenerateCards));
await LoadTodayEntriesAsync();
InitializeForAdd();
}
catch (Exception ex)
{
HandyControl.Controls.MessageBox.Error($"保存或打印失败:{ex.Message}");
}
finally
{
IsLoading = false;
EndActionBusy();
}
}
/// <summary>通过 PrintDot 桥接器拉取打印机列表(与打印模板页一致)。</summary>
private async Task RefreshPrintersAsync(bool verbose)
{
if (verbose) PrinterStatus = "刷新打印机中...";
var savedName = PrintDotSettings.Load().SelectedPrinter;
try
{
var list = await _printDotService.GetPrintersAsync();
Application.Current.Dispatcher.Invoke(() =>
{
Printers.Clear();
foreach (var p in list) Printers.Add(p);
var match = list.FirstOrDefault(p => p.Name == savedName)
?? list.FirstOrDefault(p => p.IsDefault)
?? (list.Count > 0 ? list[0] : null);
_suppressPrinterSave = true;
SelectedPrinter = match;
_suppressPrinterSave = false;
PrinterStatus = list.Count > 0 ? $"共 {list.Count} 台打印机" : "未检测到打印机";
});
}
catch (Exception ex)
{
Application.Current.Dispatcher.Invoke(() =>
{
Printers.Clear();
_suppressPrinterSave = true;
SelectedPrinter = null;
_suppressPrinterSave = false;
PrinterStatus = verbose
? $"PrintDot 未连接:{ex.Message}"
: "PrintDot 未连接";
});
}
}
private static PrintTemplate BuildPrintTemplateForEntry(string? templateJson)
{
try
{
if (string.IsNullOrWhiteSpace(templateJson) || templateJson == "{}")
return new PrintTemplate { TemplateName = "原料入场记录", TemplateCode = "MES_RAW_MATERIAL_ENTRY" };
var root = JsonDocument.Parse(templateJson).RootElement;
double w = 210, h = 297;
if (root.TryGetProperty("page", out var page))
{
if (page.TryGetProperty("width", out var wEl)) w = wEl.GetDouble();
if (page.TryGetProperty("height", out var hEl)) h = hEl.GetDouble();
}
return new PrintTemplate
{
TemplateName = "原料入场记录",
TemplateCode = "MES_RAW_MATERIAL_ENTRY",
PaperWidthMm = w,
PaperHeightMm = h,
PaperOrientation = w > h ? "横向" : "纵向",
};
}
catch
{
return new PrintTemplate { TemplateName = "原料入场记录", TemplateCode = "MES_RAW_MATERIAL_ENTRY" };
}
}
/// <summary>页面首次加载时拉取「今日」列表(由 View Loaded 调用)。</summary> /// <summary>页面首次加载时拉取「今日」列表(由 View Loaded 调用)。</summary>
public async Task LoadTodayEntriesOnFirstShowAsync() => await LoadTodayEntriesAsync(); public async Task LoadTodayEntriesOnFirstShowAsync() => await LoadTodayEntriesAsync();
@@ -232,6 +493,7 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
// 若物料列表此前未加载导致选中项未回填,则补一次拉取(与编辑弹窗逻辑一致) // 若物料列表此前未加载导致选中项未回填,则补一次拉取(与编辑弹窗逻辑一致)
if (_pendingMaterialId != null) if (_pendingMaterialId != null)
await LoadMaterialOptionsAsync(); await LoadMaterialOptionsAsync();
_lastPreviewSnapshot = string.Empty;
} }
/// <summary> /// <summary>
@@ -240,21 +502,30 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
/// </summary> /// </summary>
protected override async Task SaveAsync() protected override async Task SaveAsync()
{ {
if (IsActionBusy) return;
BeginActionBusy("保存中...");
var wasEdit = !IsAddMode; var wasEdit = !IsAddMode;
await base.SaveAsync(); try
RaisePropertyChanged(nameof(CanGenerateCards));
if (!Result || CloseAction != null)
{ {
return; await base.SaveAsync();
RaisePropertyChanged(nameof(CanGenerateCards));
if (!Result || CloseAction != null)
{
return;
}
await LoadTodayEntriesAsync();
if (wasEdit)
{
HandyControl.Controls.MessageBox.Success("编辑成功!");
InitializeForAdd();
}
} }
finally
await LoadTodayEntriesAsync();
if (wasEdit)
{ {
HandyControl.Controls.MessageBox.Success("编辑成功!"); EndActionBusy();
InitializeForAdd();
} }
} }
@@ -348,6 +619,7 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
/// </summary> /// </summary>
private async Task GenerateRawMaterialCardsAsync() private async Task GenerateRawMaterialCardsAsync()
{ {
if (IsActionBusy) return;
if (Entry == null || string.IsNullOrWhiteSpace(Entry.Id)) if (Entry == null || string.IsNullOrWhiteSpace(Entry.Id))
{ {
HandyControl.Controls.MessageBox.Warning("请先保存入场记录再生成原材料卡片!"); HandyControl.Controls.MessageBox.Warning("请先保存入场记录再生成原材料卡片!");
@@ -395,6 +667,7 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
} }
} }
BeginActionBusy("生成中...");
try try
{ {
IsLoading = true; IsLoading = true;
@@ -481,9 +754,336 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
finally finally
{ {
IsLoading = false; IsLoading = false;
EndActionBusy();
} }
} }
private void BeginActionBusy(string text)
{
ActionBusyText = string.IsNullOrWhiteSpace(text) ? "处理中..." : text;
IsActionBusy = true;
}
private void EndActionBusy()
{
IsActionBusy = false;
ActionBusyText = "处理中...";
}
/// <summary>页面卸载时停止轮询,避免后台仍请求已释放的 WebView。</summary>
public void StopPrintPreviewTimer() => _printPreviewTimer.Stop();
/// <summary>视图重新附加时恢复轮询(与 <see cref="StopPrintPreviewTimer"/> 配对)。</summary>
public void StartPrintPreviewTimer()
{
if (!_printPreviewTimer.IsEnabled) _printPreviewTimer.Start();
}
private async Task OnPrintPreviewTickAsync()
{
if (!IsPrintPreviewExpanded || Entry == null || _printPreviewBusy) return;
try
{
ApplySplitDetailsToEntry();
var snap = JsonSerializer.Serialize(Entry, PreviewSnapshotJsonOpts);
if (string.Equals(snap, _lastPreviewSnapshot, StringComparison.Ordinal)) return;
_lastPreviewSnapshot = snap;
_printPreviewBusy = true;
PrintPreviewStatus = "更新预览中…";
var myGen = ++_previewRequestGeneration;
if (!await EnsureLocalPreviewTemplateLoadedAsync().ConfigureAwait(false))
{
PostPrintPreviewHtml(BuildPrintPreviewPlaceholderHtml("未找到本地打印模板缓存,请先联网完成一次“业务打印绑定/打印模板”同步。"));
PrintPreviewStatus = "未找到本地模板缓存";
return;
}
if (string.IsNullOrWhiteSpace(_previewTemplateJson) || _previewTemplateJson == "{}")
{
PostPrintPreviewHtml(BuildPrintPreviewPlaceholderHtml("尚未配置业务打印绑定或模板内容为空"));
PrintPreviewStatus = "未配置模板";
return;
}
var dataObj = BuildLocalPreviewData(Entry);
if (myGen != _previewRequestGeneration) return;
string html;
try
{
html = NativePrintRenderService.RenderToHtml(_previewTemplateJson, dataObj);
}
catch (Exception ex)
{
html = BuildPrintPreviewErrorHtml(ex.Message);
PrintPreviewStatus = "渲染失败";
PostPrintPreviewHtml(html);
return;
}
PostPrintPreviewHtml(html);
PrintPreviewStatus = string.Empty;
}
catch (Exception ex)
{
PostPrintPreviewHtml(BuildPrintPreviewErrorHtml(ex.Message));
PrintPreviewStatus = "预览异常";
}
finally
{
_printPreviewBusy = false;
}
}
private void PostPrintPreviewHtml(string html)
{
var app = Application.Current;
if (app?.Dispatcher == null)
{
PrintPreviewHtmlReady?.Invoke(this, html);
return;
}
if (app.Dispatcher.CheckAccess()) PrintPreviewHtmlReady?.Invoke(this, html);
else app.Dispatcher.Invoke(() => PrintPreviewHtmlReady?.Invoke(this, html));
}
private static string BuildPrintPreviewErrorHtml(string message)
{
var esc = WebUtility.HtmlEncode(message ?? string.Empty);
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:28px 40px;text-align:center;"
+ "box-shadow:0 6px 24px rgba(0,0,0,.4);max-width:520px;}"
+ "</style></head><body><div class=\"card\">"
+ "<div style=\"font-size:36px;margin-bottom:10px\">⚠️</div>"
+ "<div style=\"font-size:13px;color:#e74c3c;word-break:break-all;\">预览:" + esc + "</div>"
+ "</div></body></html>";
}
private static string BuildPrintPreviewPlaceholderHtml(string tip)
{
var esc = WebUtility.HtmlEncode(tip ?? string.Empty);
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;}"
+ "</style></head><body><div class=\"card\">"
+ "<div style=\"font-size:14px;color:#888;\">" + esc + "</div></div></body></html>";
}
private async Task<bool> EnsureLocalPreviewTemplateLoadedAsync()
{
if (_previewTemplateLoaded && !string.IsNullOrWhiteSpace(_previewTemplateJson))
{
return true;
}
// 离线优先用本地缓存;若当前在线再尝试刷新缓存
var bindList = _printBizTemplateBindService.GetCached();
if (bindList.Count == 0)
{
try { bindList = await _printBizTemplateBindService.ListAsync().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)
|| (x.BizName ?? string.Empty).Contains("原材料流转卡片", StringComparison.OrdinalIgnoreCase));
if (bind == null) return false;
_previewFieldMappingJson = string.IsNullOrWhiteSpace(bind.FieldMappingJson) ? "[]" : bind.FieldMappingJson!;
_previewTemplateCode = string.IsNullOrWhiteSpace(bind.TemplateCode) ? RawMaterialEntryTemplateCode : bind.TemplateCode!;
var templates = _printTemplateService.GetCached();
if (templates.Count == 0)
{
try { templates = await _printTemplateService.ListAsync().ConfigureAwait(false); } catch { /* 忽略 */ }
}
var tpl = templates.FirstOrDefault(t => !string.IsNullOrWhiteSpace(bind.TemplateId) && string.Equals(t.Id, bind.TemplateId, StringComparison.OrdinalIgnoreCase))
?? templates.FirstOrDefault(t => string.Equals(t.TemplateCode, _previewTemplateCode, StringComparison.OrdinalIgnoreCase));
if (tpl == null || string.IsNullOrWhiteSpace(tpl.TemplateJson)) return false;
_previewTemplateJson = tpl.TemplateJson!;
_previewTemplateName = string.IsNullOrWhiteSpace(tpl.TemplateName) ? "原料入场记录" : tpl.TemplateName!;
_previewTemplateLoaded = true;
return true;
}
private JsonObject BuildLocalPreviewData(MesXslRawMaterialEntry entry)
{
JsonObject printData = new();
JsonNode? bizRoot = JsonSerializer.SerializeToNode(entry, PreviewSnapshotJsonOpts);
try
{
var mappingNode = JsonNode.Parse(string.IsNullOrWhiteSpace(_previewFieldMappingJson) ? "[]" : _previewFieldMappingJson) as JsonArray;
if (mappingNode != null)
{
foreach (var rule in mappingNode)
{
try
{
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? val = string.IsNullOrWhiteSpace(bizField)
? JsonValue.Create(string.Empty)
: ResolvePath(bizRoot, bizField!);
// JsonNode 不能同时挂在两个父节点,必须深拷贝后再写入 printData
var normalized = NormalizePreviewNodeValue(val);
SetPath(printData, templateField!, normalized);
}
catch
{
// 单条映射异常不应影响其余字段,继续后续规则
}
}
}
}
catch
{
// 忽略 mapping JSON 异常,继续尝试按模板字段补空
}
try
{
var root = JsonNode.Parse(_previewTemplateJson);
var fields = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
CollectTemplateBindFields(root, fields);
foreach (var key in fields)
{
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 paramsArr)
{
foreach (var p in paramsArr)
{
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 bool HasPath(JsonObject root, string path)
{
var parts = path.Split('.', StringSplitOptions.RemoveEmptyEntries);
JsonNode? cur = root;
for (int i = 0; i < parts.Length; i++)
{
if (cur is not JsonObject obj) return false;
if (!obj.TryGetPropertyValue(parts[i], out cur)) return false;
if (i == parts.Length - 1) return true;
}
return false;
}
private static JsonNode? ResolvePath(JsonNode? root, string path)
{
if (root == null || string.IsNullOrWhiteSpace(path)) return null;
var parts = path.Split('.', StringSplitOptions.RemoveEmptyEntries);
JsonNode? cur = root;
foreach (var p in parts)
{
if (cur == null) return null;
if (cur is JsonArray arr)
{
if (int.TryParse(p, out var idx))
{
cur = idx >= 0 && idx < arr.Count ? arr[idx] : null;
}
else
{
cur = arr.Count > 0 ? arr[0]?[p] : null;
}
}
else
{
cur = cur[p];
}
}
return cur;
}
/// <summary>
/// 本地实时预览与后端保持一致DateTime/DateTimeOffset 统一输出 yyyy-MM-dd HH:mm:ss不带时区后缀
/// </summary>
private static JsonNode NormalizePreviewNodeValue(JsonNode? node)
{
if (node == null) return JsonValue.Create(string.Empty)!;
if (node is JsonValue v)
{
if (v.TryGetValue<DateTime>(out var dt))
{
return JsonValue.Create(dt.ToString("yyyy-MM-dd HH:mm:ss"))!;
}
if (v.TryGetValue<DateTimeOffset>(out var dto))
{
return JsonValue.Create(dto.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"))!;
}
if (v.TryGetValue<string>(out var s) && !string.IsNullOrWhiteSpace(s))
{
// 仅处理明显的 ISO 时间串,避免误伤普通文本
if ((s.Contains('T') || s.EndsWith("Z", StringComparison.OrdinalIgnoreCase) || s.Contains('+'))
&& DateTimeOffset.TryParse(s, 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 cur = target;
for (int i = 0; i < parts.Length - 1; i++)
{
if (cur[parts[i]] is not JsonObject child)
{
child = new JsonObject();
cur[parts[i]] = child;
}
cur = child;
}
cur[parts[^1]] = value;
}
private void LoadLayoutState() private void LoadLayoutState()
{ {
try try

View File

@@ -0,0 +1,65 @@
<Window x:Class="YY.Admin.Views.Print.PrintBizTemplateBindDetailWindow"
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"
Title="业务打印绑定详情"
Width="760" Height="580"
WindowStartupLocation="CenterOwner"
ResizeMode="CanResize">
<DockPanel Margin="16">
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0 12 0 0">
<TextBlock x:Name="TxtMeta" Foreground="#666" VerticalAlignment="Center" Margin="0 0 16 0"/>
<Button Content="关闭" Width="88" Height="32" IsCancel="True" IsDefault="True" Click="OnCloseClick"/>
</StackPanel>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel>
<hc:Row Margin="0 0 0 8">
<hc:Col Span="12">
<hc:TextBox x:Name="TxtId" hc:InfoElement.Title="主键" hc:InfoElement.TitlePlacement="Left" hc:InfoElement.TitleWidth="80"
IsReadOnly="True"/>
</hc:Col>
</hc:Row>
<hc:Row Margin="0 0 0 8">
<hc:Col Layout="{hc:ColLayout Xs=12, Md=6}">
<hc:TextBox x:Name="TxtBizCode" hc:InfoElement.Title="业务编码" hc:InfoElement.TitlePlacement="Left" hc:InfoElement.TitleWidth="80"
IsReadOnly="True"/>
</hc:Col>
<hc:Col Layout="{hc:ColLayout Xs=12, Md=6}">
<hc:TextBox x:Name="TxtBizName" hc:InfoElement.Title="业务名称" hc:InfoElement.TitlePlacement="Left" hc:InfoElement.TitleWidth="80"
IsReadOnly="True"/>
</hc:Col>
</hc:Row>
<hc:Row Margin="0 0 0 8">
<hc:Col Layout="{hc:ColLayout Xs=12, Md=6}">
<hc:TextBox x:Name="TxtTemplateId" hc:InfoElement.Title="模板Id" hc:InfoElement.TitlePlacement="Left" hc:InfoElement.TitleWidth="80"
IsReadOnly="True"/>
</hc:Col>
<hc:Col Layout="{hc:ColLayout Xs=12, Md=6}">
<hc:TextBox x:Name="TxtTemplateCode" hc:InfoElement.Title="模板编码" hc:InfoElement.TitlePlacement="Left" hc:InfoElement.TitleWidth="80"
IsReadOnly="True"/>
</hc:Col>
</hc:Row>
<hc:Row Margin="0 0 0 12">
<hc:Col Span="12">
<hc:TextBox x:Name="TxtRemark" hc:InfoElement.Title="备注" hc:InfoElement.TitlePlacement="Left" hc:InfoElement.TitleWidth="80"
IsReadOnly="True"/>
</hc:Col>
</hc:Row>
<GroupBox Header="字段映射 JSON">
<TextBox x:Name="TxtMappingJson"
MinHeight="260"
AcceptsReturn="True"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
FontFamily="Consolas"
FontSize="12"
IsReadOnly="True"
BorderThickness="0"/>
</GroupBox>
</StackPanel>
</ScrollViewer>
</DockPanel>
</Window>

View File

@@ -0,0 +1,46 @@
using System.Text.Json;
using System.Windows;
using YY.Admin.Core.Entity;
namespace YY.Admin.Views.Print;
public partial class PrintBizTemplateBindDetailWindow
{
public PrintBizTemplateBindDetailWindow(PrintBizTemplateBind model)
{
InitializeComponent();
TxtId.Text = model.Id ?? "";
TxtBizCode.Text = model.BizCode ?? "";
TxtBizName.Text = model.BizName ?? "";
TxtTemplateId.Text = model.TemplateId ?? "";
TxtTemplateCode.Text = model.TemplateCode ?? "";
TxtRemark.Text = model.Remark ?? "";
var raw = model.FieldMappingJson ?? "";
if (!string.IsNullOrWhiteSpace(raw))
{
try
{
using var doc = JsonDocument.Parse(raw);
TxtMappingJson.Text = JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true });
}
catch
{
TxtMappingJson.Text = raw;
}
}
else
{
TxtMappingJson.Text = "[]";
}
var ct = model.CreateTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-";
var ut = model.UpdateTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-";
TxtMeta.Text = $"创建:{ct} 更新:{ut} 创建人:{model.CreateBy ?? "-"} 更新人:{model.UpdateBy ?? "-"}";
}
private void OnCloseClick(object sender, RoutedEventArgs e)
{
Close();
}
}

View File

@@ -0,0 +1,118 @@
<UserControl x:Class="YY.Admin.Views.Print.PrintBizTemplateBindListView"
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 FilterBizCode, UpdateSourceTrigger=PropertyChanged}"
Margin="0 0 10 10"
hc:InfoElement.Title="业务编码"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.TitleWidth="65"
hc:InfoElement.Placeholder="菜单权限 id 或编码"
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 FilterBizName, 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 FilterTemplateCode, 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" VerticalAlignment="Center">
<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 Items}"
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 BizCode}" Width="140" CellStyle="{StaticResource CusDataGridCellStyle}"/>
<DataGridTextColumn Header="业务名称" Binding="{Binding BizName}" Width="*" MinWidth="120" CellStyle="{StaticResource CusDataGridCellStyle}"/>
<DataGridTextColumn Header="模板编码" Binding="{Binding TemplateCode}" Width="140" CellStyle="{StaticResource CusDataGridCellStyle}"/>
<DataGridTextColumn Header="备注" Binding="{Binding Remark}" Width="160" CellStyle="{StaticResource CusDataGridCellStyle}"/>
<DataGridTextColumn Header="创建时间" Binding="{Binding CreateTime, StringFormat=yyyy-MM-dd HH:mm}" Width="130" CellStyle="{StaticResource CusDataGridCellStyle}"/>
<DataGridTemplateColumn Header="操作" Width="88" CanUserSort="False" CanUserResize="False">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="详情"
Command="{Binding DataContext.DetailCommand, 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>

View File

@@ -0,0 +1,9 @@
namespace YY.Admin.Views.Print;
public partial class PrintBizTemplateBindListView
{
public PrintBizTemplateBindListView()
{
InitializeComponent();
}
}

View File

@@ -6,6 +6,7 @@
xmlns:hc="https://handyorg.github.io/handycontrol" xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:ctrls="clr-namespace:YY.Admin.Controls" xmlns:ctrls="clr-namespace:YY.Admin.Controls"
xmlns:core="clr-namespace:YY.Admin.Core.Entity;assembly=YY.Admin.Core" xmlns:core="clr-namespace:YY.Admin.Core.Entity;assembly=YY.Admin.Core"
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
xmlns:prism="http://prismlibrary.com/" xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True" prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -38,6 +39,7 @@
<Grid Style="{StaticResource BaseViewStyle}"> <Grid Style="{StaticResource BaseViewStyle}">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
@@ -73,7 +75,30 @@
</Button> </Button>
</Grid> </Grid>
<Grid Grid.Row="1" x:Name="MainSplitRoot"> <!-- 打印机PrintDot 桥接器,与「打印模板」页一致 -->
<Border Grid.Row="1" Margin="24,0,24,8" Padding="0,4,0,4" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="打印机:" VerticalAlignment="Center" FontSize="13" Foreground="#333333" Margin="0,0,8,0"/>
<ComboBox ItemsSource="{Binding Printers}"
SelectedItem="{Binding SelectedPrinter}"
DisplayMemberPath="Name"
MinWidth="220" Height="30"
VerticalContentAlignment="Center"
hc:InfoElement.Placeholder="请选择打印机"/>
<Button Command="{Binding RefreshPrintersCommand}"
Height="30" Padding="10,0" Margin="10,0,0,0"
FontSize="12"
Style="{StaticResource ButtonDefault}"
Content="刷新打印机"/>
<TextBlock Text="{Binding PrinterStatus}"
Margin="12,0,0,0"
VerticalAlignment="Center"
FontSize="12"
Foreground="{DynamicResource SecondaryTextBrush}"/>
</StackPanel>
</Border>
<Grid Grid.Row="2" x:Name="MainSplitRoot">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="320"/> <ColumnDefinition Width="*" MinWidth="320"/>
<ColumnDefinition x:Name="SplitterCol" Width="4"/> <ColumnDefinition x:Name="SplitterCol" Width="4"/>
@@ -893,7 +918,13 @@
BorderBrush="{DynamicResource BorderBrush}" BorderBrush="{DynamicResource BorderBrush}"
BorderThickness="1,0,0,0" BorderThickness="1,0,0,0"
Background="{DynamicResource RegionBrush}"> Background="{DynamicResource RegionBrush}">
<DockPanel LastChildFill="True"> <Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" MinHeight="120"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<DockPanel Grid.Row="0" LastChildFill="True">
<!-- ===== 标题 + 日期筛选 ===== --> <!-- ===== 标题 + 日期筛选 ===== -->
<Border DockPanel.Dock="Top" BorderBrush="{DynamicResource BorderBrush}" <Border DockPanel.Dock="Top" BorderBrush="{DynamicResource BorderBrush}"
@@ -998,12 +1029,100 @@
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
</DockPanel> </DockPanel>
<!-- ===== 下方:入场标签实时打印预览(可折叠,仅画布无 JSON ===== -->
<StackPanel Grid.Row="1" Margin="0,4,0,0" VerticalAlignment="Bottom">
<Border BorderBrush="{DynamicResource BorderBrush}"
BorderThickness="1"
CornerRadius="6,6,0,0"
Background="{DynamicResource SecondaryRegionBrush}"
Padding="10,8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="入场标签打印预览"
FontWeight="SemiBold"
FontSize="12"
VerticalAlignment="Center"/>
<TextBlock Grid.Column="1"
Text="{Binding PrintPreviewStatus}"
TextTrimming="CharacterEllipsis"
FontSize="10"
Opacity="0.72"
Margin="8,0,10,0"
VerticalAlignment="Center"
Foreground="{DynamicResource SecondaryTextBrush}"/>
<ToggleButton Grid.Column="2"
IsChecked="{Binding IsPrintPreviewExpanded, Mode=TwoWay}"
MinWidth="56"
Height="24"
Background="Transparent"
BorderThickness="1"
Padding="8,0"
FocusVisualStyle="{x:Null}"
Cursor="Hand"
VerticalAlignment="Center">
<ToggleButton.Template>
<ControlTemplate TargetType="ToggleButton">
<Border Background="{TemplateBinding Background}"
BorderBrush="{DynamicResource BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="2"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</ToggleButton.Template>
<TextBlock FontSize="12"
VerticalAlignment="Center"
HorizontalAlignment="Center">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="展开"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsChecked, RelativeSource={RelativeSource AncestorType=ToggleButton}}" Value="True">
<Setter Property="Text" Value="折叠"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</ToggleButton>
</Grid>
</Border>
<Border BorderBrush="{DynamicResource BorderBrush}"
BorderThickness="1,0,1,1"
CornerRadius="0,0,2,2"
Padding="0"
MinHeight="200"
MaxHeight="420"
Background="#525659"
Visibility="{Binding IsPrintPreviewExpanded, Converter={StaticResource Boolean2VisibilityConverter}}">
<wv2:WebView2 x:Name="PrintPreviewWebView"
DefaultBackgroundColor="#FF525659"/>
</Border>
</StackPanel>
</Grid>
</Border> </Border>
</Grid> </Grid>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,12,0,20"> <StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,12,0,20">
<Button Content="保存" Command="{Binding SaveCommand}" Style="{StaticResource ButtonPrimary}" Width="100" Margin="0,0,15,0"/> <Button Content="保存并打印入场记录"
Command="{Binding SaveAndPrintCommand}"
Style="{StaticResource ButtonPrimary}"
IsEnabled="{Binding IsNotActionBusy}"
Width="168" Margin="0,0,15,0"
ToolTip="保存成功后直接发送到已选打印机(不弹预览窗口)"/>
<Button Content="保存"
Command="{Binding SaveCommand}"
Style="{StaticResource ButtonDefault}"
IsEnabled="{Binding IsNotActionBusy}"
Width="100" Margin="0,0,15,0"/>
<Button Content="生成原材料卡片" <Button Content="生成原材料卡片"
Command="{Binding GenerateRawMaterialCardsCommand}" Command="{Binding GenerateRawMaterialCardsCommand}"
Style="{StaticResource ButtonDefault}" Style="{StaticResource ButtonDefault}"
@@ -1011,5 +1130,29 @@
IsEnabled="{Binding CanGenerateCards}" IsEnabled="{Binding CanGenerateCards}"
ToolTip="根据拆码明细生成原材料卡片(需先保存入场记录)"/> ToolTip="根据拆码明细生成原材料卡片(需先保存入场记录)"/>
</StackPanel> </StackPanel>
<!-- 底部动作按钮忙碌遮罩:保存/保存并打印/生成时显示,禁止重复点击 -->
<Border Grid.RowSpan="4"
Panel.ZIndex="999"
Background="#66000000"
Visibility="{Binding IsActionBusy, Converter={StaticResource Boolean2VisibilityConverter}}">
<Border Width="220"
Height="120"
CornerRadius="8"
Background="#F2FFFFFF"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Padding="16">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<hc:LoadingCircle Width="42" Height="42"/>
<TextBlock Text="{Binding ActionBusyText}"
Margin="0,14,0,0"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryTextBrush}"
HorizontalAlignment="Center"/>
</StackPanel>
</Border>
</Border>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -26,6 +26,8 @@ public partial class RawMaterialEntryOperationView : UserControl
{ {
_vm = vm; _vm = vm;
vm.PropertyChanged += OnVmPropertyChanged; vm.PropertyChanged += OnVmPropertyChanged;
vm.PrintPreviewHtmlReady += OnPrintPreviewHtmlReady;
vm.StartPrintPreviewTimer();
} }
ApplySplitLayout(); ApplySplitLayout();
@@ -36,6 +38,8 @@ public partial class RawMaterialEntryOperationView : UserControl
if (_vm != null) if (_vm != null)
{ {
_vm.PropertyChanged -= OnVmPropertyChanged; _vm.PropertyChanged -= OnVmPropertyChanged;
_vm.PrintPreviewHtmlReady -= OnPrintPreviewHtmlReady;
_vm.StopPrintPreviewTimer();
_vm = null; _vm = null;
} }
} }
@@ -78,6 +82,21 @@ public partial class RawMaterialEntryOperationView : UserControl
{ {
_vm = vm; _vm = vm;
vm.PropertyChanged += OnVmPropertyChanged; vm.PropertyChanged += OnVmPropertyChanged;
vm.PrintPreviewHtmlReady += OnPrintPreviewHtmlReady;
vm.StartPrintPreviewTimer();
}
}
private async void OnPrintPreviewHtmlReady(object? sender, string html)
{
try
{
await PrintPreviewWebView.EnsureCoreWebView2Async();
PrintPreviewWebView.NavigateToString(html ?? string.Empty);
}
catch
{
/* WebView2 未就绪或宿主已释放时忽略 */
} }
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,99 @@
{
// 缓存配置
"Cache": {
"Prefix": "yyadmin_", // 全局缓存前缀
"CacheType": "Memory", // Memory、Redis
"Redis": {
"Configuration": "server=localhost;db=2;password=123456;", // Redis连接字符串
"Prefix": "yyadmin_", // Redis前缀目前没用
"MaxMessageSize": "1048576" // 最大消息大小 默认1024 * 1024
}
},
// 数据库连接字符串参考地址https://www.connectionstrings.com/
"DbConnection": {
"EnableConsoleSql": true, // 启用控制台打印SQL
"ConnectionConfigs": [
{
"ConfigId": "1300000000001", // 默认库标识-禁止修改
"DbType": "Sqlite", // MySql、SqlServer、Sqlite、Oracle、PostgreSQL、Dm、Kdbndp、Oscar、MySqlConnector、Access、OpenGauss、QuestDB、HG、ClickHouse、GBase、Odbc、Custom
"DbNickName": "系统库",
//"ConnectionString": "DataSource=./Admin.NET.db", // Sqlite
"ConnectionString": "DataSource=./Admin.NET.db", // Sqlite
//"ConnectionString": "PORT=5432;DATABASE=xxx;HOST=localhost;PASSWORD=xxx;USER ID=xxx", // PostgreSQL
//"ConnectionString": "server= ;port=;database=;user=;password=;CharSet=utf8;sslmode=none;max pool size=1000;", // MySql,
"DbSettings": {
"EnableInitDb": true, // 启用库初始化(若实体没有变化建议关闭)
"EnableInitView": false, // 启用视图初始化(若实体和视图没有变化建议关闭)
"EnableDiffLog": false, // 启用库表差异日志
"EnableUnderLine": true, // 启用驼峰转下划线
"EnableConnEncrypt": false // 启用数据库连接串加密国密SM2加解密
},
"TableSettings": {
"EnableInitTable": true, // 启用表初始化(若实体没有变化建议关闭)
"EnableIncreTable": false // 启用表增量更新(只更新贴了特性[IncreTable]的实体表)
},
"SeedSettings": {
"EnableInitSeed": true, // 启用种子初始化(若种子没有变化建议关闭)
"EnableIncreSeed": false // 启用种子增量更新(只更新贴了特性[IncreSeed]的种子表)
}
},
{
"ConfigId": "Slave", // 从数据库
"DbType": "Sqlite", // 数据库类型
"DbNickName": "业务库",
"ConnectionString": "Data Source=./Slave.db", // Sqlite
"DbSettings": {
"EnableInitDb": true, // 启用库初始化(若实体没有变化建议关闭)
"EnableInitView": false, // 启用视图初始化(若实体和视图没有变化建议关闭)
"EnableDiffLog": false, // 启用库表差异日志
"EnableUnderLine": true, // 启用驼峰转下划线
"EnableConnEncrypt": false // 启用数据库连接串加密国密SM2加解密
},
"TableSettings": {
"EnableInitTable": true, // 启用表初始化(若实体没有变化建议关闭)
"EnableIncreTable": false // 启用表增量更新(只更新贴了特性[IncreTable]的实体表)
},
"SeedSettings": {
"EnableInitSeed": true, // 启用种子初始化(若种子没有变化建议关闭)
"EnableIncreSeed": false // 启用种子增量更新(只更新贴了特性[IncreSeed]的种子表)
}
}
]
},
"PrintDot": {
"Url": "ws://127.0.0.1:1122/ws" // PrintDot 桥接器 WebSocket 地址,可在打印设置页面覆盖
},
"AutoUpdate": {
"RemoteConfigUrl": "http://14.103.155.227:8083/updates/version.xml" //更新文件地址
},
"JeecgIntegration": {
"Enabled": true, // 是否启用Jeecg可与本地并存见 PreferLocalLogin 决定先后顺序)
"PreferLocalLogin": true, // true先校验本地库再尝试 Jeecg工控脱网时无需等待 MES直接用本地账号登录
"FallbackToLocal": true, // false仅 MES 登录,失败即结束(不推荐工控场景)
"BaseUrl": "http://127.0.0.1:8080/jeecg-boot", // Jeecg后端地址按实际环境修改
"LoginPath": "/sys/login", // Jeecg登录接口
"UserInfoPath": "/sys/user/getUserInfo", // Jeecg用户信息接口
"UserListPath": "/sys/user/scada/queryUser", // Jeecg 用户列表SCADA分页 + updatedAfter 增量,见文档)
"ScadaUserPageSize": 500, // SCADA queryUser 每页条数,最大 1000
"ScadaUserIncludeDetail": false, // true 时含部门/公司/租户明细,耗时更高
"ScadaUseUpdatedAfter": true, // 后台/定时同步:有水位时用 updatedAfter 增量。登录触发的 SCADA 同步固定全量分页,避免只拉到少数变更用户
"TenantListPath": "/sys/tenant/list", // Jeecg租户分页接口
"UserPermissionPath": "/sys/permission/getUserPermissionByToken", // Jeecg当前用户菜单与按钮权限接口
"ResetLocalIdentityDataOnJeecgLogin": false, // true 时 Jeecg 登录成功会清空本地用户/角色等(易丢失种子账号导致脱网无法登录)。工控独立运行建议保持 false
"AutoProvisionLocalUser": true, // Jeecg认证成功后本地不存在账号时自动创建
"SyncUserProfileToLocal": true, // 每次登录时同步Jeecg用户基础信息到本地
"SyncAllUsersOnJeecgLogin": true, // trueJeecg 登录成功后拉取用户列表并写入本地(关闭则不会同步用户表)
"UseJeecgUserIdAsLocalPrimaryKey": true, // true 时本地 sys_user.id 与 Jeecg getUserInfo 的 id 一致(雪花 long
"UserSyncSkipUnchanged": true, // 与Jeecg updateTime 一致时跳过写库,减少重复保存
"UserListUseUpdateTimeQuery": false, // 仅非 SCADA 的 /sys/user/listtrue 时附带 updateTime_begin。SCADA 增量由 ScadaUseUpdatedAfter 控制
"IncrementalSyncOverlapMinutes": 2, // 增量时间窗口重叠,避免时钟误差漏数据
"BackgroundSyncIntervalMinutes": 30, // 主窗口在线时定时增量同步间隔(分钟)
"AnonymousMode": true, // 工控机免密模式优先走免登录接口与匿名WebSocket通道
"WebSocketUrl": "", // 可选Jeecg 或自建推送地址;免密模式下若误配到 /ws/device/websocket 会自动切回 /websocket/scada-sync
"WebSocketPath": "/websocket/scada-sync", // 匿名实时推送通道
"WebSocketInactivityReconnectSeconds": 0, // 0=关闭空闲强制重连仅保留WS心跳保活避免重连窗口丢推送
"DefaultTenantId": 1002, // 自动创建本地用户时使用的默认租户ID
"Captcha": "", // 如启用登录验证码,在此传入验证码
"CheckKey": "" // 如启用登录验证码在此传入验证码key
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,504 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>Microsoft.Web.WebView2.WinForms</name>
</assembly>
<members>
<member name="T:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties">
<summary>
This class is a bundle of the most common parameters used to create <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Environment"/> and <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Controller"/> instances.
Its main purpose is to be set to <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CreationProperties"/> in order to customize the environment and/or controller used by a <see cref="T:Microsoft.Web.WebView2.WinForms.WebView2"/> during implicit initialization.
</summary>
<remarks>
This class isn't intended to contain all possible environment or controller customization options.
If you need complete control over the environment and/or controller used by a WebView2 control then you'll need to initialize the control explicitly by
creating your own environment (with <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(System.String,System.String,Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions)"/>) and/or controller options (with <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateCoreWebView2ControllerOptions"/>) and passing them to <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.EnsureCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment,Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions)"/>
*before* you set the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property to anything.
See the <see cref="T:Microsoft.Web.WebView2.WinForms.WebView2"/> class documentation for an initialization overview.
</remarks>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.#ctor">
<summary>
Creates a new instance of <see cref="T:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties"/> with default data for all properties.
</summary>
</member>
<member name="P:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.BrowserExecutableFolder">
<summary>
Gets or sets the value to pass as the browserExecutableFolder parameter of <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(System.String,System.String,Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions)"/> when creating an environment with this instance.
</summary>
</member>
<member name="P:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.UserDataFolder">
<summary>
Gets or sets the value to pass as the userDataFolder parameter of <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(System.String,System.String,Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions)"/> when creating an environment with this instance.
</summary>
</member>
<member name="P:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.Language">
<summary>
Gets or sets the value to use for the Language property of the CoreWebView2EnvironmentOptions parameter passed to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(System.String,System.String,Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions)"/> when creating an environment with this instance.
</summary>
</member>
<member name="P:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.ProfileName">
<summary>
Gets or sets the value to use for the ProfileName property of the CoreWebView2ControllerOptions parameter passed to CreateCoreWebView2ControllerWithOptionsAsync when creating an controller with this instance.
</summary>
</member>
<member name="P:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.AdditionalBrowserArguments">
<summary>
Gets or sets the value to pass as the AdditionalBrowserArguments parameter of <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions"/> which is passed to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(System.String,System.String,Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions)"/> when creating an environment with this instance.
</summary>
</member>
<member name="P:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.IsInPrivateModeEnabled">
<summary>
Gets or sets the value to use for the IsInPrivateModeEnabled property of the CoreWebView2ControllerOptions parameter passed to CreateCoreWebView2ControllerWithOptionsAsync when creating an controller with this instance.
</summary>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.CreateEnvironmentAsync">
<summary>
Create a <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Environment"/> using the current values of this instance's properties.
</summary>
<returns>A task which will provide the created environment on completion, or null if no environment-related options are set.</returns>
<remarks>
As long as no other properties on this instance are changed, repeated calls to this method will return the same task/environment as earlier calls.
If some other property is changed then the next call to this method will return a different task/environment.
</remarks>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.CreateCoreWebView2ControllerOptions(Microsoft.Web.WebView2.Core.CoreWebView2Environment)">
<summary>
Creates a <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions"/> using the current values of this instance's properties.
</summary>
<returns>A <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions"/> object or null if no controller-related properties are set.</returns>
<exception cref="T:System.NullReferenceException">Thrown if the parameter environment is null.</exception>
</member>
<member name="T:Microsoft.Web.WebView2.WinForms.WebView2">
<summary>
Control to embed WebView2 in WinForms.
</summary>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.#ctor">
<summary>
Create a new WebView2 WinForms control.
After construction the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> property is <c>null</c>.
Call <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.EnsureCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment,Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions)"/> to initialize the underlying <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2"/>.
</summary>
<remarks>
This control is effectively a wrapper around the WebView2 COM API, which you can find documentation for here: https://aka.ms/webview2
You can directly access the underlying ICoreWebView2 interface and all of its functionality by accessing the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> property.
Some of the most common COM functionality is also accessible directly through wrapper methods/properties/events on the control.
Upon creation, the control's CoreWebView2 property will be null.
This is because creating the CoreWebView2 is an expensive operation which involves things like launching Edge browser processes.
There are two ways to cause the CoreWebView2 to be created:
1) Call the <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.EnsureCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment,Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions)"/> method. This is referred to as explicit initialization.
2) Set the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property. This is referred to as implicit initialization.
Either option will start initialization in the background and return back to the caller without waiting for it to finish.
To specify options regarding the initialization process, either pass your own <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Environment"/> to EnsureCoreWebView2Async or set the control's <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CreationProperties"/> property prior to initialization.
When initialization has finished (regardless of how it was triggered) then the following things will occur, in this order:
1) The control's <see cref="E:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2InitializationCompleted"/> event will be invoked. If you need to perform one time setup operations on the CoreWebView2 prior to its use then you should do so in a handler for that event.
2) If a Uri has been set to the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property then the control will start navigating to it in the background (i.e. these steps will continue without waiting for the navigation to finish).
3) The Task returned from <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.EnsureCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment,Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions)"/> will complete.
For more details about any of the methods/properties/events involved in the initialization process, see its specific documentation.
Accelerator key presses (e.g. Ctrl+P) that occur within the control will
fire standard key press events such as OnKeyDown. You can suppress the
control's default implementation of an accelerator key press (e.g.
printing, in the case of Ctrl+P) by setting the Handled property of its
EventArgs to true. Also note that the underlying browser process is
blocked while these handlers execute, so:
<list type="number">
<item>
You should avoid doing a lot of work in these handlers.
</item>
<item>
Some of the WebView2 and CoreWebView2 APIs may throw errors if
invoked within these handlers due to being unable to communicate with
the browser process.
</item>
</list>
If you need to do a lot of work and/or invoke WebView2 APIs in response to
accelerator keys then consider kicking off a background task or queuing
the work for later execution on the UI thread.
</remarks>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.Dispose(System.Boolean)">
<summary>
Cleans up any resources being used.
</summary>
<param name="disposing"><c>true</c> if managed resources should be disposed; otherwise, <c>false</c>.</param>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.OnPaint(System.Windows.Forms.PaintEventArgs)">
<summary>
Overrides the base OnPaint event to have custom actions
in designer mode
</summary>
<param name="e">The graphics devices which is the source</param>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.WndProc(System.Windows.Forms.Message@)">
<summary>
Overrides the base WndProc events to handle specific window messages.
</summary>
<param name="m">The Message object containing the HWND window message and parameters</param>
</member>
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.CreationProperties">
<summary>
Gets or sets a bag of options which are used during initialization of the control's <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/>.
This property cannot be modified (an exception will be thrown) after initialization of the control's CoreWebView2 has started.
</summary>
<exception cref="T:System.InvalidOperationException">Thrown if initialization of the control's CoreWebView2 has already started.</exception>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.EnsureCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment,Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions)">
<summary>
Explicitly trigger initialization of the control's <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/>.
</summary>
<param name="environment">
A pre-created <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Environment"/> that should be used to create the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/>.
Creating your own environment gives you control over several options that affect how the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is initialized.
If you pass <c>null</c> (the default value) then a default environment will be created and used automatically.
</param>
<param name="controllerOptions">
A pre-created <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions"/> that should be used to create the <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2"/>.
Creating your own controller options gives you control over several options that affect how the <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2"/> is initialized.
If you pass a controllerOptions to this method then it will override any settings specified on the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CreationProperties"/> property.
If you pass <c>null</c> (the default value) and no value has been set to <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CreationProperties"/> then a default controllerOptions will be created and used automatically.
</param>
<returns>
A Task that represents the background initialization process.
When the task completes then the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> property will be available for use (i.e. non-null).
Note that the control's <see cref="E:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2InitializationCompleted"/> event will be invoked before the task completes
or on exceptions.
</returns>
<remarks>
Unless previous initialization has already failed, calling this method additional times with the same parameter will have no effect (any specified environment is ignored) and return the same Task as the first call.
Unless previous initialization has already failed, calling this method after initialization has been implicitly triggered by setting the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property will have no effect if no environment is given
and simply return a Task representing that initialization already in progress.
Unless previous initialization has already failed, calling this method with a different environment after initialization has begun will result in an <see cref="T:System.ArgumentException"/>. For example, this can happen if you begin initialization
by setting the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property and then call this method with a new environment, if you begin initialization with <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CreationProperties"/> and then call this method with a new
environment, or if you begin initialization with one environment and then call this method with no environment specified.
When this method is called after previous initialization has failed, it will trigger initialization of the control's <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> again.
Note that even though this method is asynchronous and returns a Task, it still must be called on the UI thread like most public functionality of most UI controls.
<para>
The following summarizes the possible error values and a description of why these errors occur.
<list type="table">
<listheader>
<description>Error Value</description>
<description>Description</description>
</listheader>
<item>
<description><c>HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)</c></description>
<description>*\\Edge\\Application* path used in browserExecutableFolder.</description>
</item>
<item>
<description><c>HRESULT_FROM_WIN32(ERROR_INVALID_STATE)</c></description>
<description>Specified options do not match the options of the WebViews that are currently running in the shared browser process.</description>
</item>
<item>
<description><c>HRESULT_FROM_WIN32(ERROR_INVALID_WINDOW_HANDLE)</c></description>
<description>WebView2 Initialization failed due to an invalid host HWND parentWindow.</description>
</item>
<item>
<description><c>HRESULT_FROM_WIN32(ERROR_DISK_FULL)</c></description>
<description>WebView2 Initialization failed due to reaching the maximum number of installed runtime versions.</description>
</item>
<item>
<description><c>HRESULT_FROM_WIN32(ERROR_PRODUCT_UNINSTALLED</c></description>
<description>If the Webview depends upon an installed WebView2 Runtime version and it is uninstalled.</description>
</item>
<item>
<description><c>HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)</c></description>
<description>Could not find Edge installation.</description>
</item>
<item>
<description><c>HRESULT_FROM_WIN32(ERROR_FILE_EXISTS)</c></description>
<description>User data folder cannot be created because a file with the same name already exists.</description>
</item>
<item>
<description><c>E_ACCESSDENIED</c></description>
<description>Unable to create user data folder, Access Denied.</description>
</item>
<item>
<description><c>E_FAIL</c></description>
<description>Edge runtime unable to start.</description>
</item>
</list>
</para>
</remarks>
<exception cref="T:System.ArgumentException">
Thrown if this method is called with a different environment than when it was initialized. See Remarks for more info.
</exception>
<exception cref="T:System.InvalidOperationException">
Thrown if this instance of <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is already disposed, or if the calling thread isn't the thread which created this object (usually the UI thread). See <see cref="P:System.Windows.Forms.Control.InvokeRequired"/> for more info.
May also be thrown if the browser process has crashed unexpectedly and left the control in an invalid state. We are considering throwing a different type of exception for this case in the future.
</exception>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.EnsureCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment)">
<summary>
Explicitly trigger initialization of the control's <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/>.
</summary>
<param name="environment">
A pre-created <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Environment"/> that should be used to create the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/>.
Creating your own environment gives you control over several options that affect how the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is initialized.
If you pass <c>null</c> then a default environment will be created and used automatically.
</param>
<returns>
A Task that represents the background initialization process.
When the task completes then the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> property will be available for use (i.e. non-null).
Note that the control's <see cref="E:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2InitializationCompleted"/> event will be invoked before the task completes
or on exceptions.
</returns>
<remarks>
Unless previous initialization has already failed, calling this method additional times with the same parameter will have no effect (any specified environment is ignored) and return the same Task as the first call.
Unless previous initialization has already failed, calling this method after initialization has been implicitly triggered by setting the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property will have no effect if no environment is given
and simply return a Task representing that initialization already in progress.
Unless previous initialization has already failed, calling this method with a different environment after initialization has begun will result in an <see cref="T:System.ArgumentException"/>. For example, this can happen if you begin initialization
by setting the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property and then call this method with a new environment, if you begin initialization with <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CreationProperties"/> and then call this method with a new
environment, or if you begin initialization with one environment and then call this method with no environment specified.
When this method is called after previous initialization has failed, it will trigger initialization of the control's <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> again.
Note that even though this method is asynchronous and returns a Task, it still must be called on the UI thread like most public functionality of most UI controls.
</remarks>
<exception cref="T:System.ArgumentException">
Thrown if this method is called with a different environment than when it was initialized. See Remarks for more info.
</exception>
<exception cref="T:System.InvalidOperationException">
Thrown if this instance of <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is already disposed, or if the calling thread isn't the thread which created this object (usually the UI thread). See <see cref="P:System.Windows.Forms.Control.InvokeRequired"/> for more info.
May also be thrown if the browser process has crashed unexpectedly and left the control in an invalid state. We are considering throwing a different type of exception for this case in the future.
</exception>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.InitCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment,Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions)">
<summary>
This is the private function which implements the actual background initialization task.
Cannot be called if the control is already initialized or has been disposed.
</summary>
<param name="environment">
The environment to use to create the <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Controller"/>.
If that is null then a default environment is created with <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(System.String,System.String,Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions)"/> and its default parameters.
</param>
<param name="controllerOptions">
The controllerOptions to use to create the <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Controller"/>.
If that is null then a default controllerOptions is created with its default parameters.
</param>
<returns>A task representing the background initialization process.</returns>
<remarks>All the event handlers added here need to be removed in <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.Dispose(System.Boolean)"/>.</remarks>
</member>
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.CreateParams">
<summary>
Protected CreateParams property. Used to set custom window styles to the forms HWND.
</summary>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.OnVisibleChanged(System.EventArgs)">
<summary>
Protected VisibilityChanged handler.
</summary>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.OnSizeChanged(System.EventArgs)">
<summary>
Protected SizeChanged handler.
</summary>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.Select(System.Boolean,System.Boolean)">
<summary>
Protected Select method: override this to capture tab direction when WebView control is activated
</summary>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.OnGotFocus(System.EventArgs)">
<summary>
Protected OnGotFocus handler.
</summary>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.OnParentChanged(System.EventArgs)">
<summary>
Protected OnParentChanged handler.
</summary>
</member>
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.IsInitialized">
<summary>
True if initialization finished successfully and the control is not disposed yet.
</summary>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.GetSitedParentSite(System.Windows.Forms.Control)">
<summary>
Recursive retrieval of the parent control
</summary>
<param name="control">The control to get the parent for</param>
<returns>The root parent control</returns>
</member>
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2">
<summary>
The underlying CoreWebView2. Use this property to perform more operations on the WebView2 content than is exposed
on the WebView2. This value is null until it is initialized and the object itself has undefined behaviour once the control is disposed.
You can force the underlying CoreWebView2 to
initialize via the <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.EnsureCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment,Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions)"/> method.
</summary>
<exception cref="T:System.InvalidOperationException">Thrown if the calling thread isn't the thread which created this object (usually the UI thread). See <see cref="P:System.Windows.Forms.Control.InvokeRequired"/> for more info.</exception>
</member>
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.ZoomFactor">
<summary>
The zoom factor for the WebView.
</summary>
</member>
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.AllowExternalDrop">
<summary>
Enable/disable external drop.
</summary>
</member>
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.Source">
<summary>
The Source property is the URI of the top level document of the
WebView2. Setting the Source is equivalent to calling <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.Navigate(System.String)"/>.
Setting the Source will trigger initialization of the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/>, if not already initialized.
The default value of Source is <c>null</c>, indicating that the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized.
</summary>
<exception cref="T:System.ArgumentException">Specified value is not an absolute <see cref="T:System.Uri"/>.</exception>
<exception cref="T:System.NotImplementedException">Specified value is <c>null</c> and the control is initialized.</exception>
<seealso cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.Navigate(System.String)"/>
</member>
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.CanGoForward">
<summary>
Returns true if the webview can navigate to a next page in the
navigation history via the <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.GoForward"/> method.
This is equivalent to the <see cref="P:Microsoft.Web.WebView2.Core.CoreWebView2.CanGoForward"/>.
If the underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized, this property is <c>false</c>.
</summary>
<seealso cref="P:Microsoft.Web.WebView2.Core.CoreWebView2.CanGoForward"/>
</member>
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.CanGoBack">
<summary>
Returns <c>true</c> if the webview can navigate to a previous page in the
navigation history via the <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.GoBack"/> method.
This is equivalent to the <see cref="P:Microsoft.Web.WebView2.Core.CoreWebView2.CanGoBack"/>.
If the underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized, this property is <c>false</c>.
</summary>
<seealso cref="P:Microsoft.Web.WebView2.Core.CoreWebView2.CanGoBack"/>
</member>
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.DefaultBackgroundColor">
<summary>
The default background color for the WebView.
</summary>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.ExecuteScriptAsync(System.String)">
<summary>
Executes the provided script in the top level document of the <see cref="T:Microsoft.Web.WebView2.WinForms.WebView2"/>.
This is equivalent to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.ExecuteScriptAsync(System.String)"/>.
</summary>
<exception cref="T:System.InvalidOperationException">The underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized.</exception>
<exception cref="T:System.InvalidOperationException">Thrown when browser process has unexpectedly and left this control in an invalid state. We are considering throwing a different type of exception for this case in the future.</exception>
<seealso cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.ExecuteScriptAsync(System.String)"/>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.Reload">
<summary>
Reloads the top level document of the <see cref="T:Microsoft.Web.WebView2.WinForms.WebView2"/>.
This is equivalent to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.Reload"/>.
</summary>
<exception cref="T:System.InvalidOperationException">The underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized.</exception>
<exception cref="T:System.InvalidOperationException">Thrown when browser process has unexpectedly and left this control in an invalid state. We are considering throwing a different type of exception for this case in the future.</exception>
<seealso cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.Reload"/>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.GoForward">
<summary>
Navigates to the next page in navigation history.
This is equivalent to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.GoForward"/>.
If the underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized, this method does nothing.
</summary>
<seealso cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.GoForward"/>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.GoBack">
<summary>
Navigates to the previous page in navigation history.
This is equivalent to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.GoBack"/>.
If the underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized, this method does nothing.
</summary>
<seealso cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.GoBack"/>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.NavigateToString(System.String)">
<summary>
Renders the provided HTML as the top level document of the <see cref="T:Microsoft.Web.WebView2.WinForms.WebView2"/>.
This is equivalent to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.NavigateToString(System.String)"/>.
</summary>
<exception cref="T:System.InvalidOperationException">The underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized.</exception>
<exception cref="T:System.InvalidOperationException">Thrown when browser process has unexpectedly and left this control in an invalid state. We are considering throwing a different type of exception for this case in the future.</exception>
<remarks>The <c>htmlContent</c> parameter may not be larger than 2 MB (2 * 1024 * 1024 bytes) in total size. The origin of the new page is <c>about:blank</c>.</remarks>
<seealso cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.NavigateToString(System.String)"/>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.Stop">
<summary>
Stops any in progress navigation in the <see cref="T:Microsoft.Web.WebView2.WinForms.WebView2"/>.
This is equivalent to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.Stop"/>.
If the underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized, this method does nothing.
</summary>
<seealso cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.Stop"/>
</member>
<member name="E:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2InitializationCompleted">
<summary>
This event is triggered either 1) when the control's <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> has finished being initialized (regardless of how it was triggered or whether it succeeded) but before it is used for anything
OR 2) the initialization failed.
You should handle this event if you need to perform one time setup operations on the CoreWebView2 which you want to affect all of its usages
(e.g. adding event handlers, configuring settings, installing document creation scripts, adding host objects).
</summary>
<remarks>
This sender will be the WebView2 control, whose CoreWebView2 property will now be valid (i.e. non-null) for the first time
if <see cref="P:Microsoft.Web.WebView2.Core.CoreWebView2InitializationCompletedEventArgs.IsSuccess"/> is true.
Unlikely this event can fire second time (after reporting initialization success first)
if the initialization is followed by navigation which fails.
</remarks>
</member>
<member name="E:Microsoft.Web.WebView2.WinForms.WebView2.NavigationStarting">
<summary>
NavigationStarting dispatches before a new navigate starts for the top
level document of the <see cref="T:Microsoft.Web.WebView2.WinForms.WebView2"/>.
This is equivalent to the <see cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.NavigationStarting"/> event.
</summary>
<seealso cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.NavigationStarting"/>
</member>
<member name="E:Microsoft.Web.WebView2.WinForms.WebView2.NavigationCompleted">
<summary>
NavigationCompleted dispatches after a navigate of the top level
document completes rendering either successfully or not.
This is equivalent to the <see cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.NavigationCompleted"/> event.
</summary>
<seealso cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.NavigationCompleted"/>
</member>
<member name="E:Microsoft.Web.WebView2.WinForms.WebView2.WebMessageReceived">
<summary>
WebMessageReceived dispatches after web content sends a message to the
app host via <c>chrome.webview.postMessage</c>.
This is equivalent to the <see cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.WebMessageReceived"/> event.
</summary>
<seealso cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.WebMessageReceived"/>
</member>
<member name="E:Microsoft.Web.WebView2.WinForms.WebView2.SourceChanged">
<summary>
SourceChanged dispatches after the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property changes. This may happen
during a navigation or if otherwise the script in the page changes the
URI of the document.
This is equivalent to the <see cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.SourceChanged"/> event.
</summary>
<seealso cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.SourceChanged"/>
</member>
<member name="E:Microsoft.Web.WebView2.WinForms.WebView2.ContentLoading">
<summary>
ContentLoading dispatches after a navigation begins to a new URI and the
content of that URI begins to render.
This is equivalent to the <see cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.ContentLoading"/> event.
</summary>
<seealso cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.ContentLoading"/>
</member>
<member name="E:Microsoft.Web.WebView2.WinForms.WebView2.ZoomFactorChanged">
<summary>
ZoomFactorChanged dispatches when the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.ZoomFactor"/> property changes.
This is equivalent to the <see cref="E:Microsoft.Web.WebView2.Core.CoreWebView2Controller.ZoomFactorChanged"/> event.
</summary>
<seealso cref="E:Microsoft.Web.WebView2.Core.CoreWebView2Controller.ZoomFactorChanged"/>
</member>
<member name="F:Microsoft.Web.WebView2.WinForms.WebView2.components">
<summary>
Required designer variable.
</summary>
</member>
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.InitializeComponent">
<summary>
Required method for Designer support - do not modify
the contents of this method with the code editor.
</summary>
</member>
</members>
</doc>

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More