新增原料入场记录功能,包含免密接口和数据同步,更新相关控制器、实体和服务,支持条码/批次号生成及管理,优化用户体验和系统实时数据处理能力。
This commit is contained in:
132
jeecg-boot/db/mes-mixer-material-import-from-image.sql
Normal file
132
jeecg-boot/db/mes-mixer-material-import-from-image.sql
Normal file
@@ -0,0 +1,132 @@
|
||||
-- 从截图识别生成:密炼物料导入 SQL(目标表:mes_mixer_material)
|
||||
-- 说明:
|
||||
-- 1) feed_manage_status=1(投管), use_status=1(使用), shelf_life_days=365
|
||||
-- 2) 物料大类/小类按分类字典映射到 sys_category.id
|
||||
-- 物料大类:名称「原辅材料」,类型编码 code = XSLMES_MATERIAL_RAW_AUX(sys_category.code)
|
||||
-- 物料小类:名称「炭黑」,且 pid 指向大类(若已维护父子关系)
|
||||
-- 3) tenant_id / sys_org_code 未知,先置空
|
||||
-- 4) material_name / material_desc / alias_name 中如包含 TODO_*,请在执行前按业务名称修正
|
||||
-- 5) 为避免重复,先按 material_code 删除后再插入
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- 大类:优先按类型编码匹配,否则按名称「原辅材料」
|
||||
SET @major_category_id := (
|
||||
SELECT sc.id
|
||||
FROM sys_category sc
|
||||
WHERE sc.code = 'XSLMES_MATERIAL_RAW_AUX' OR sc.name = '原辅材料'
|
||||
ORDER BY
|
||||
(CASE WHEN sc.code = 'XSLMES_MATERIAL_RAW_AUX' THEN 0 WHEN sc.name = '原辅材料' THEN 1 ELSE 2 END),
|
||||
sc.create_time DESC
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
-- 小类:名称「炭黑」,优先挂在大类下
|
||||
SET @minor_category_id := (
|
||||
SELECT sc.id
|
||||
FROM sys_category sc
|
||||
WHERE sc.name = '炭黑'
|
||||
AND (
|
||||
@major_category_id IS NULL
|
||||
OR sc.pid = @major_category_id
|
||||
OR sc.pid IS NULL
|
||||
OR sc.pid = '0'
|
||||
)
|
||||
ORDER BY (CASE WHEN sc.pid = @major_category_id THEN 0 ELSE 1 END), sc.create_time DESC
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
DELETE FROM mes_mixer_material
|
||||
WHERE material_code IN (
|
||||
'110110001','110110002','110110003','110110004','110110005','110110006','110110007','110110008',
|
||||
'110110009','110110010','110110011','110110012','110110013','110110014','110110015','110110016',
|
||||
'110110017','110110018','110110019','110110020','110110021','110110022','110110023','110110024',
|
||||
'110110025','110110026','110110027','110110028'
|
||||
);
|
||||
|
||||
INSERT INTO mes_mixer_material (
|
||||
id,
|
||||
material_code,
|
||||
material_name,
|
||||
erp_code,
|
||||
major_category_id,
|
||||
minor_category_id,
|
||||
material_desc,
|
||||
alias_name,
|
||||
feed_manage_status,
|
||||
use_status,
|
||||
specific_gravity,
|
||||
shelf_life_days,
|
||||
min_bake_minutes,
|
||||
total_safety_stock_kg,
|
||||
qualified_safety_stock_kg,
|
||||
remark,
|
||||
tenant_id,
|
||||
sys_org_code,
|
||||
create_by,
|
||||
create_time,
|
||||
del_flag
|
||||
)
|
||||
SELECT
|
||||
REPLACE(UUID(), '-', '') AS id,
|
||||
t.material_code,
|
||||
t.material_name,
|
||||
t.erp_code,
|
||||
@major_category_id AS major_category_id,
|
||||
@minor_category_id AS minor_category_id,
|
||||
t.material_desc,
|
||||
t.alias_name,
|
||||
1 AS feed_manage_status,
|
||||
1 AS use_status,
|
||||
t.specific_gravity,
|
||||
365 AS shelf_life_days,
|
||||
t.min_bake_minutes,
|
||||
NULL AS total_safety_stock_kg,
|
||||
NULL AS qualified_safety_stock_kg,
|
||||
'图片识别导入' AS remark,
|
||||
NULL AS tenant_id,
|
||||
NULL AS sys_org_code,
|
||||
'image_import' AS create_by,
|
||||
NOW() AS create_time,
|
||||
0 AS del_flag
|
||||
FROM (
|
||||
-- material_code(9位), material_name, erp_code, material_desc, alias_name, specific_gravity, min_bake_minutes
|
||||
SELECT '110110001' AS material_code, '炭黑-N220' AS material_name, '10201' AS erp_code, '炭黑-N220' AS material_desc, '炭黑-N220' AS alias_name, 1.82 AS specific_gravity, NULL AS min_bake_minutes
|
||||
UNION ALL SELECT '110110002', '炭黑-N330', '10203', '炭黑-N330', '炭黑-N330', 1.82, NULL
|
||||
UNION ALL SELECT '110110003', '炭黑-N660', '10205', '炭黑-N660', '炭黑-N660', 1.82, NULL
|
||||
UNION ALL SELECT '110110004', '裂解炭黑', '10206', '裂解炭黑', '裂解炭黑', 1.95, NULL
|
||||
UNION ALL SELECT '110110005', '陶土1', '10254', '填料-陶土1', '填料-陶土1', 2.60, NULL
|
||||
UNION ALL SELECT '110110006', '碳酸钙', '10253', '填料-碳酸钙', '填料-碳酸钙', 2.70, NULL
|
||||
UNION ALL SELECT '110110007', '硫酸钡', '10252', '填料-硫酸钡', '填料-硫酸钡', 4.20, NULL
|
||||
UNION ALL SELECT '110110008', '二氧化钛中阳', '10251', '二氧化钛中阳', '二氧化钛中阳', 2.20, NULL
|
||||
UNION ALL SELECT '110110009', '炭黑-N234', '10202', '炭黑-N234', '炭黑-N234', 1.85, NULL
|
||||
UNION ALL SELECT '110110010', '炭黑 N339', '10204', '炭黑 N339', '炭黑 N339', 1.85, NULL
|
||||
UNION ALL SELECT '110110011', '云母粉', '10255', '云母粉', '云母粉', 2.00, NULL
|
||||
UNION ALL SELECT '110110012', '硬脂酸2', '10543', '硬脂酸2', '硬脂酸2', 0.85, 0
|
||||
UNION ALL SELECT '110110013', 'FI-N339', '10204', 'FI-N339', 'FI-N339', 1.85, 4
|
||||
UNION ALL SELECT '110110014', 'IL N330', '10203', 'IL N330', 'IL N330', 1.85, 4
|
||||
UNION ALL SELECT '110110015', '卡博特N234', '11006', '卡博特N234', '卡博特N234', 1.10, 0
|
||||
UNION ALL SELECT '110110016', '卡博特234', '11006', '卡博特234', '卡博特234', 1.80, 8
|
||||
UNION ALL SELECT '110110017', '头条B-N220', '10201', '头条B-N220', '头条B-N220', 1.82, 0
|
||||
UNION ALL SELECT '110110018', '集采B-N330', '10203', '集采B-N330', '集采B-N330', 1.82, 24
|
||||
UNION ALL SELECT '110110019', '集采B-N660', '10205', '集采B-N660', '集采B-N660', 1.82, 24
|
||||
UNION ALL SELECT '110110020', '集采B-裂解炭黑', '10206', '集采B-裂解炭黑', '集采B-裂解炭黑', 1.82, 24
|
||||
UNION ALL SELECT '110110021', '集采B-碳酸钙', '10253', '集采B-碳酸钙', '集采B-碳酸钙', 1.82, 24
|
||||
UNION ALL SELECT '110110022', '集采B-陶土', '10254', '集采B-陶土', '集采B-陶土', 1.82, 24
|
||||
UNION ALL SELECT '110110023', '集采B-N234', '10204', '集采B-N234', '集采B-N234', 1.82, 24
|
||||
UNION ALL SELECT '110110024', '集采B-N339', '10204', '集采B-N339', '集采B-N339', 1.82, 24
|
||||
UNION ALL SELECT '110110025', '氧化锌2', '10256', '氧化锌2', '氧化锌2', 5.55, 0
|
||||
UNION ALL SELECT '110110026', '炭黑-N550', '10209', '炭黑-N550', '炭黑-N550', 1.82, 0
|
||||
UNION ALL SELECT '110110027', 'AH', '10606', 'AH', 'AH', 1.18, 0
|
||||
UNION ALL SELECT '110110028', '旺格-N220', '10201', '旺格-N220', '旺格-N220', 1.82, 0
|
||||
) t;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
-- 执行后可核对:
|
||||
-- SELECT @major_category_id AS major_category_id, @minor_category_id AS minor_category_id;
|
||||
-- SELECT material_code, material_name, erp_code, major_category_id, minor_category_id, specific_gravity, min_bake_minutes
|
||||
-- FROM mes_mixer_material
|
||||
-- WHERE material_code BETWEEN '110110001' AND '110110028'
|
||||
-- ORDER BY material_code;
|
||||
@@ -22,4 +22,9 @@ public class FillRuleConstant {
|
||||
*/
|
||||
public static final String CATEGORY = "category_code_rule";
|
||||
|
||||
/**
|
||||
* MES 原料入场记录 条码/批次号
|
||||
*/
|
||||
public static final String MES_RAW_MATERIAL_BARCODE = "mes_raw_material_barcode_rule";
|
||||
|
||||
}
|
||||
|
||||
@@ -203,6 +203,8 @@ public class ShiroConfig {
|
||||
filterChainDefinitionMap.put("/xslmes/mesXslSupplier/anon/**", "anon");
|
||||
// MES磅单管理免密接口(供桌面端调用)
|
||||
filterChainDefinitionMap.put("/xslmes/mesXslWeightRecord/anon/**", "anon");
|
||||
// MES原料入场记录免密接口(供桌面端调用)
|
||||
filterChainDefinitionMap.put("/xslmes/mesXslRawMaterialEntry/anon/**", "anon");
|
||||
// MES密炼物料管理免密接口(供桌面端调用)
|
||||
filterChainDefinitionMap.put("/mes/material/mixerMaterial/anon/**", "anon");
|
||||
// 系统分类字典免密接口(供桌面端调用)
|
||||
|
||||
@@ -14,10 +14,12 @@ import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.constant.MesXslCustomerBizStatus;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslCustomer;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialEntry;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslSupplier;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslVehicle;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWeightRecord;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslCustomerService;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialEntryService;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslSupplierService;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslVehicleService;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslWeightRecordService;
|
||||
@@ -26,6 +28,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* 桌面端免密接口 — 统一入口
|
||||
@@ -45,6 +48,7 @@ public class MesXslDesktopAnonController {
|
||||
private final IMesXslCustomerService customerService;
|
||||
private final IMesXslSupplierService supplierService;
|
||||
private final IMesXslWeightRecordService weightRecordService;
|
||||
private final IMesXslRawMaterialEntryService rawMaterialEntryService;
|
||||
private final MesXslStompNotifyService stompNotify;
|
||||
|
||||
// ═══════════════════════════ 车辆管理 ═══════════════════════════
|
||||
@@ -422,6 +426,82 @@ public class MesXslDesktopAnonController {
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
// ═══════════════════════════ 原料入场记录 ═══════════════════════════
|
||||
|
||||
@Operation(summary = "原料入场记录-免密分页列表查询")
|
||||
@GetMapping("/xslmes/mesXslRawMaterialEntry/anon/list")
|
||||
public Result<IPage<MesXslRawMaterialEntry>> rawMaterialEntryAnonList(
|
||||
MesXslRawMaterialEntry entity,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<MesXslRawMaterialEntry> qw = QueryGenerator.initQueryWrapper(entity, req.getParameterMap());
|
||||
qw.orderByDesc("create_time");
|
||||
IPage<MesXslRawMaterialEntry> page = rawMaterialEntryService.page(new Page<>(pageNo, pageSize), qw);
|
||||
return Result.OK(page);
|
||||
}
|
||||
|
||||
@Operation(summary = "原料入场记录-免密通过id查询")
|
||||
@GetMapping("/xslmes/mesXslRawMaterialEntry/anon/queryById")
|
||||
public Result<MesXslRawMaterialEntry> rawMaterialEntryAnonQueryById(@RequestParam(name = "id") String id) {
|
||||
MesXslRawMaterialEntry entity = rawMaterialEntryService.getById(id);
|
||||
return entity != null ? Result.OK(entity) : Result.error("未找到对应数据");
|
||||
}
|
||||
|
||||
@Operation(summary = "原料入场记录-免密生成条码/批次号")
|
||||
@GetMapping("/xslmes/mesXslRawMaterialEntry/anon/generateBarcode")
|
||||
public Result<String> rawMaterialEntryAnonGenerateBarcode(
|
||||
@RequestParam(name = "materialCode", defaultValue = "") String materialCode) {
|
||||
return Result.OK(rawMaterialEntryService.generateBarcode(materialCode));
|
||||
}
|
||||
|
||||
@Operation(summary = "原料入场记录-免密添加")
|
||||
@PostMapping("/xslmes/mesXslRawMaterialEntry/anon/add")
|
||||
public Result<String> rawMaterialEntryAnonAdd(@RequestBody MesXslRawMaterialEntry entity) {
|
||||
// 条码/批次号为空时服务端自动生成
|
||||
if (StringUtils.isBlank(entity.getBarcode())) {
|
||||
String code = rawMaterialEntryService.generateBarcode(
|
||||
StringUtils.defaultString(entity.getMaterialCode(), ""));
|
||||
entity.setBarcode(code);
|
||||
}
|
||||
if (StringUtils.isBlank(entity.getBatchNo())) {
|
||||
entity.setBatchNo(entity.getBarcode());
|
||||
}
|
||||
rawMaterialEntryService.save(entity);
|
||||
stompNotify.publishRawMaterialEntryChanged("add", entity.getId());
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
@Operation(summary = "原料入场记录-免密编辑")
|
||||
@RequestMapping(value = "/xslmes/mesXslRawMaterialEntry/anon/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
public Result<String> rawMaterialEntryAnonEdit(@RequestBody MesXslRawMaterialEntry entity) {
|
||||
if (oConvertUtils.isEmpty(entity.getId())) {
|
||||
return Result.error("主键不能为空");
|
||||
}
|
||||
boolean ok = rawMaterialEntryService.updateById(entity);
|
||||
if (!ok) {
|
||||
return Result.error("数据已被他人修改,请刷新后重试");
|
||||
}
|
||||
stompNotify.publishRawMaterialEntryChanged("edit", entity.getId());
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
@Operation(summary = "原料入场记录-免密删除")
|
||||
@DeleteMapping("/xslmes/mesXslRawMaterialEntry/anon/delete")
|
||||
public Result<String> rawMaterialEntryAnonDelete(@RequestParam(name = "id") String id) {
|
||||
rawMaterialEntryService.removeById(id);
|
||||
stompNotify.publishRawMaterialEntryChanged("delete", id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
@Operation(summary = "原料入场记录-免密批量删除")
|
||||
@DeleteMapping("/xslmes/mesXslRawMaterialEntry/anon/deleteBatch")
|
||||
public Result<String> rawMaterialEntryAnonDeleteBatch(@RequestParam(name = "ids") String ids) {
|
||||
rawMaterialEntryService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
stompNotify.publishRawMaterialEntryChanged("batchDelete", ids);
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
// ─────────────────────────── 车辆私有辅助 ────────────────────────────
|
||||
|
||||
private void applyWeightBillType(MesXslWeightRecord record) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialEntry;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialEntryService;
|
||||
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
@@ -36,6 +37,8 @@ public class MesXslRawMaterialEntryController extends JeecgController<MesXslRawM
|
||||
|
||||
@Autowired
|
||||
private IMesXslRawMaterialEntryService mesXslRawMaterialEntryService;
|
||||
@Autowired
|
||||
private MesXslStompNotifyService stompNotify;
|
||||
|
||||
@Operation(summary = "原料入场记录-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
@@ -55,6 +58,7 @@ public class MesXslRawMaterialEntryController extends JeecgController<MesXslRawM
|
||||
@PostMapping(value = "/add")
|
||||
public Result<String> add(@RequestBody MesXslRawMaterialEntry mesXslRawMaterialEntry) {
|
||||
mesXslRawMaterialEntryService.save(mesXslRawMaterialEntry);
|
||||
stompNotify.publishRawMaterialEntryChanged("add", mesXslRawMaterialEntry.getId());
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
@@ -64,6 +68,7 @@ public class MesXslRawMaterialEntryController extends JeecgController<MesXslRawM
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody MesXslRawMaterialEntry mesXslRawMaterialEntry) {
|
||||
mesXslRawMaterialEntryService.updateById(mesXslRawMaterialEntry);
|
||||
stompNotify.publishRawMaterialEntryChanged("edit", mesXslRawMaterialEntry.getId());
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
@@ -73,6 +78,7 @@ public class MesXslRawMaterialEntryController extends JeecgController<MesXslRawM
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
||||
mesXslRawMaterialEntryService.removeById(id);
|
||||
stompNotify.publishRawMaterialEntryChanged("delete", id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
@@ -82,6 +88,7 @@ public class MesXslRawMaterialEntryController extends JeecgController<MesXslRawM
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
|
||||
this.mesXslRawMaterialEntryService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
stompNotify.publishRawMaterialEntryChanged("batchDelete", ids);
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,10 @@ public class MesXslRawMaterialEntry implements Serializable {
|
||||
@Schema(description = "物料ID")
|
||||
private String materialId;
|
||||
|
||||
@Excel(name = "物料编码", width = 20)
|
||||
@Schema(description = "物料编码(用于生成条码/批次号)")
|
||||
private String materialCode;
|
||||
|
||||
@Excel(name = "物料名称", width = 20)
|
||||
@Schema(description = "物料名称")
|
||||
private String materialName;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.jeecg.modules.xslmes.rule;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.common.handler.IFillRuleHandler;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialEntryService;
|
||||
|
||||
/**
|
||||
* 填值规则:原料入场记录 条码/批次号生成
|
||||
* 格式:QH + 物料编码 + 日期(yyMMdd) + 当日序号(001起)
|
||||
* rule_code: mes_raw_material_barcode_rule
|
||||
*/
|
||||
@Slf4j
|
||||
public class RawMaterialBarcodeRule implements IFillRuleHandler {
|
||||
|
||||
@Override
|
||||
public Object execute(JSONObject params, JSONObject formData) {
|
||||
String materialCode = "";
|
||||
if (formData != null && StringUtils.isNotBlank(formData.getString("materialCode"))) {
|
||||
materialCode = formData.getString("materialCode").trim();
|
||||
} else if (params != null && StringUtils.isNotBlank(params.getString("materialCode"))) {
|
||||
materialCode = params.getString("materialCode").trim();
|
||||
}
|
||||
log.info("原料入场条码生成规则 materialCode={}", materialCode);
|
||||
IMesXslRawMaterialEntryService service =
|
||||
SpringContextUtils.getBean(IMesXslRawMaterialEntryService.class);
|
||||
return service.generateBarcode(materialCode);
|
||||
}
|
||||
}
|
||||
@@ -10,4 +10,13 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IMesXslRawMaterialEntryService extends IService<MesXslRawMaterialEntry> {
|
||||
|
||||
/**
|
||||
* 生成条码/批次号
|
||||
* 格式:QH + 物料编码 + 日期(yyMMdd) + 当日序号(001起)
|
||||
*
|
||||
* @param materialCode 物料编码
|
||||
* @return 生成的条码字符串
|
||||
*/
|
||||
String generateBarcode(String materialCode);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,11 @@ public class MesXslStompNotifyService {
|
||||
publish("/topic/sync/mes-weight-records", "MES_WEIGHT_RECORD_CHANGED", "weightRecordId", weightRecordId, action);
|
||||
}
|
||||
|
||||
/** 广播原料入场记录变更事件到 /topic/sync/mes-raw-material-entries */
|
||||
public void publishRawMaterialEntryChanged(String action, String entryId) {
|
||||
publish("/topic/sync/mes-raw-material-entries", "MES_RAW_MATERIAL_ENTRY_CHANGED", "entryId", entryId, action);
|
||||
}
|
||||
|
||||
/** 广播密炼物料数据变更事件到 /topic/sync/mes-mixer-materials */
|
||||
public void publishMixerMaterialChanged(String action, String mixerMaterialId) {
|
||||
publish("/topic/sync/mes-mixer-materials", "MIXER_MATERIAL_CHANGED", "mixerMaterialId", mixerMaterialId, action);
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package org.jeecg.modules.xslmes.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialEntry;
|
||||
import org.jeecg.modules.xslmes.mapper.MesXslRawMaterialEntryMapper;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialEntryService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @Description: 原料入场记录
|
||||
@@ -13,5 +17,20 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Service
|
||||
public class MesXslRawMaterialEntryServiceImpl extends ServiceImpl<MesXslRawMaterialEntryMapper, MesXslRawMaterialEntry> implements IMesXslRawMaterialEntryService {
|
||||
public class MesXslRawMaterialEntryServiceImpl
|
||||
extends ServiceImpl<MesXslRawMaterialEntryMapper, MesXslRawMaterialEntry>
|
||||
implements IMesXslRawMaterialEntryService {
|
||||
|
||||
@Override
|
||||
public String generateBarcode(String materialCode) {
|
||||
if (materialCode == null) materialCode = "";
|
||||
String dateStr = new SimpleDateFormat("yyMMdd").format(new Date());
|
||||
String prefix = "QH" + materialCode.trim() + dateStr;
|
||||
|
||||
LambdaQueryWrapper<MesXslRawMaterialEntry> qw = new LambdaQueryWrapper<>();
|
||||
qw.likeRight(MesXslRawMaterialEntry::getBarcode, prefix);
|
||||
long count = this.count(qw);
|
||||
|
||||
return prefix + String.format("%03d", count + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
-- ============================================================
|
||||
-- 1. 原料入场记录表:补充 material_code 列
|
||||
-- ============================================================
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'mes_xsl_raw_material_entry'
|
||||
AND COLUMN_NAME = 'material_code'
|
||||
);
|
||||
SET @ddl := IF(@col_exists = 0,
|
||||
'ALTER TABLE `mes_xsl_raw_material_entry` ADD COLUMN `material_code` varchar(100) DEFAULT NULL COMMENT ''物料编码'' AFTER `material_id`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
-- ============================================================
|
||||
-- 2. 注册填值规则 mes_raw_material_barcode_rule
|
||||
-- ============================================================
|
||||
INSERT INTO `sys_fill_rule` (`id`, `rule_name`, `rule_code`, `rule_class`, `rule_params`, `create_by`, `create_time`)
|
||||
SELECT '1920000000000001001',
|
||||
'MES原料入场条码/批次号规则',
|
||||
'mes_raw_material_barcode_rule',
|
||||
'org.jeecg.modules.xslmes.rule.RawMaterialBarcodeRule',
|
||||
NULL,
|
||||
'admin',
|
||||
NOW()
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `sys_fill_rule` WHERE `rule_code` = 'mes_raw_material_barcode_rule'
|
||||
);
|
||||
@@ -0,0 +1,13 @@
|
||||
using Prism.Events;
|
||||
|
||||
namespace YY.Admin.Core.Events;
|
||||
|
||||
public class RawMaterialEntryChangedPayload
|
||||
{
|
||||
public string? Action { get; set; }
|
||||
public string? EntryId { get; set; }
|
||||
}
|
||||
|
||||
public class RawMaterialEntryChangedEvent : PubSubEvent<RawMaterialEntryChangedPayload>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using YY.Admin.Core.Entity;
|
||||
|
||||
namespace YY.Admin.Core.Services;
|
||||
|
||||
public record RawMaterialEntryPageResult(List<MesXslRawMaterialEntry> Records, long Total, int PageNo, int PageSize);
|
||||
|
||||
public interface IRawMaterialEntryService
|
||||
{
|
||||
Task<RawMaterialEntryPageResult> PageAsync(
|
||||
int pageNo, int pageSize,
|
||||
string? barcode = null,
|
||||
string? batchNo = null,
|
||||
string? billNo = null,
|
||||
string? materialName = null,
|
||||
string? supplierName = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
Task<MesXslRawMaterialEntry?> GetByIdAsync(string id, CancellationToken ct = default);
|
||||
Task<bool> AddAsync(MesXslRawMaterialEntry entry, CancellationToken ct = default);
|
||||
Task<bool> EditAsync(MesXslRawMaterialEntry entry, CancellationToken ct = default);
|
||||
Task<bool> DeleteAsync(string id, CancellationToken ct = default);
|
||||
Task<bool> DeleteBatchAsync(string ids, CancellationToken ct = default);
|
||||
|
||||
/// <summary>调用后端接口生成条码/批次号(格式:QH+物料编码+yyMMdd+序号)</summary>
|
||||
Task<string?> GenerateBarcodeAsync(string materialCode, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
namespace YY.Admin.Core.Entity;
|
||||
|
||||
public class MesXslRawMaterialEntry
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? Barcode { get; set; }
|
||||
public string? BatchNo { get; set; }
|
||||
public DateTime? EntryTime { get; set; }
|
||||
public string? WeightRecordId { get; set; }
|
||||
public string? BillNo { get; set; }
|
||||
public string? MaterialId { get; set; }
|
||||
public string? MaterialCode { get; set; }
|
||||
public string? MaterialName { get; set; }
|
||||
public string? SupplyCustomer { get; set; }
|
||||
public string? SupplierId { get; set; }
|
||||
public string? SupplierName { get; set; }
|
||||
public string? ManufacturerMaterialName { get; set; }
|
||||
public string? ShelfLife { get; set; }
|
||||
public double? TotalWeight { get; set; }
|
||||
public int? TotalPortions { get; set; }
|
||||
public double? PortionWeight { get; set; }
|
||||
public int? PortionPackages { get; set; }
|
||||
|
||||
/// <summary>检测结果:0未检 1合格 2不合格</summary>
|
||||
public string? TestResult { get; set; }
|
||||
|
||||
/// <summary>检测状态:0送样 1已批准</summary>
|
||||
public string? TestStatus { get; set; }
|
||||
|
||||
/// <summary>打印标记:1已打印 0未打印</summary>
|
||||
public string? PrintFlag { get; set; }
|
||||
|
||||
/// <summary>入库结存:1是 0否</summary>
|
||||
public string? StockBalance { get; set; }
|
||||
|
||||
public string? WarehouseLocation { get; set; }
|
||||
public string? UnloadOperator { get; set; }
|
||||
|
||||
/// <summary>是否特采:1是 0否</summary>
|
||||
public string? IsSpecialAdoption { get; set; }
|
||||
|
||||
public string? SpecialAdoptionOperator { get; set; }
|
||||
public DateTime? SpecialAdoptionTime { get; set; }
|
||||
public string? SpecialAdoptionReason { get; set; }
|
||||
public string? Status { 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; }
|
||||
public int? TenantId { get; set; }
|
||||
|
||||
public string TestResultText => TestResult switch
|
||||
{
|
||||
"0" => "未检",
|
||||
"1" => "合格",
|
||||
"2" => "不合格",
|
||||
_ => TestResult ?? ""
|
||||
};
|
||||
|
||||
public string TestStatusText => TestStatus switch
|
||||
{
|
||||
"0" => "送样",
|
||||
"1" => "已批准",
|
||||
_ => TestStatus ?? ""
|
||||
};
|
||||
|
||||
public string PrintFlagText => PrintFlag switch
|
||||
{
|
||||
"1" => "已打印",
|
||||
"0" => "未打印",
|
||||
_ => PrintFlag ?? ""
|
||||
};
|
||||
|
||||
public string StockBalanceText => StockBalance switch
|
||||
{
|
||||
"1" => "是",
|
||||
"0" => "否",
|
||||
_ => StockBalance ?? ""
|
||||
};
|
||||
|
||||
public string IsSpecialAdoptionText => IsSpecialAdoption switch
|
||||
{
|
||||
"1" => "是",
|
||||
"0" => "否",
|
||||
_ => IsSpecialAdoption ?? ""
|
||||
};
|
||||
}
|
||||
@@ -38,6 +38,8 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
|
||||
new SysMenu{ Id=1300150010501, Pid=1300150000101, Title="地磅称重操作", Path="/xslmes/weightRecordOperation", Name="weightRecordOperation", Component="WeightRecordOperationView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=104 },
|
||||
// 密炼物料信息
|
||||
new SysMenu{ Id=1300150010601, Pid=1300150000101, Title="密炼物料信息", Path="/xslmes/mesMixerMaterial", Name="mesMixerMaterial", Component="MixerMaterialListView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=105 },
|
||||
// 原料入场记录
|
||||
new SysMenu{ Id=1300150010701, Pid=1300150000101, Title="原料入场记录", Path="/xslmes/mesXslRawMaterialEntry", Name="mesXslRawMaterialEntry", Component="RawMaterialEntryListView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=106 },
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ public class SysTenantMenuSeedData : ISqlSugarEntitySeedData<SysTenantMenu>
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010401},
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010501},
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010601},
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010701},
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200012101},
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200012111},
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200012121},
|
||||
|
||||
@@ -15,6 +15,7 @@ public class CategorySyncCoordinator : ISingletonDependency
|
||||
public CategorySyncCoordinator(
|
||||
IEventAggregator eventAggregator,
|
||||
IJeecgCategorySyncService categorySyncService,
|
||||
SyncPollManager pollManager,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
@@ -26,6 +27,8 @@ public class CategorySyncCoordinator : ISingletonDependency
|
||||
_eventAggregator.GetEvent<NetworkStatusChangedEvent>()
|
||||
.Subscribe(OnNetworkStatusChanged, ThreadOption.BackgroundThread);
|
||||
|
||||
pollManager.Register("分类字典", () => SyncAndPublishAsync("poll", null));
|
||||
|
||||
_logger.Information("[分类字典] CategorySyncCoordinator 已启动");
|
||||
_ = Task.Run(() => SyncAndPublishAsync("startup", null));
|
||||
}
|
||||
|
||||
@@ -11,7 +11,10 @@ public class CustomerSyncCoordinator : ISingletonDependency
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
public CustomerSyncCoordinator(IEventAggregator eventAggregator, ILoggerService logger)
|
||||
public CustomerSyncCoordinator(
|
||||
IEventAggregator eventAggregator,
|
||||
SyncPollManager pollManager,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
@@ -19,6 +22,14 @@ public class CustomerSyncCoordinator : ISingletonDependency
|
||||
.Subscribe(OnRemoteCommand, ThreadOption.BackgroundThread);
|
||||
_eventAggregator.GetEvent<NetworkStatusChangedEvent>()
|
||||
.Subscribe(OnNetworkStatusChanged, ThreadOption.BackgroundThread);
|
||||
|
||||
pollManager.Register("客户", () =>
|
||||
{
|
||||
_eventAggregator.GetEvent<CustomerChangedEvent>()
|
||||
.Publish(new CustomerChangedPayload { Action = "poll" });
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
_logger.Information("[客户推送] CustomerSyncCoordinator 已启动");
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ public class DictSyncCoordinator : ISingletonDependency
|
||||
public DictSyncCoordinator(
|
||||
IEventAggregator eventAggregator,
|
||||
IJeecgDictSyncService dictSyncService,
|
||||
SyncPollManager pollManager,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
@@ -26,6 +27,8 @@ public class DictSyncCoordinator : ISingletonDependency
|
||||
_eventAggregator.GetEvent<NetworkStatusChangedEvent>()
|
||||
.Subscribe(OnNetworkStatusChanged, ThreadOption.BackgroundThread);
|
||||
|
||||
pollManager.Register("数据字典", () => SyncAndPublishAsync("poll", null));
|
||||
|
||||
_logger.Information("[数据字典] DictSyncCoordinator 已启动");
|
||||
_ = Task.Run(() => SyncAndPublishAsync("startup", null));
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ public class JeecgDictSyncService : IJeecgDictSyncService, ISingletonDependency
|
||||
const int pageSize = 500;
|
||||
var pageNo = 1;
|
||||
var synced = 0;
|
||||
var seenIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
while (true)
|
||||
{
|
||||
@@ -111,6 +112,8 @@ public class JeecgDictSyncService : IJeecgDictSyncService, ISingletonDependency
|
||||
continue;
|
||||
}
|
||||
|
||||
seenIds.Add(id);
|
||||
|
||||
var existing = await _dbContext.Queryable<JeecgSysDictItem>()
|
||||
.ClearFilter()
|
||||
.Where(x => x.Id == id)
|
||||
@@ -161,6 +164,15 @@ public class JeecgDictSyncService : IJeecgDictSyncService, ISingletonDependency
|
||||
pageNo++;
|
||||
}
|
||||
|
||||
// 删除本地存在但后端已移除的字典项(如后端删除重建导致 ID 变化)
|
||||
if (seenIds.Count > 0)
|
||||
{
|
||||
var seenList = seenIds.ToList();
|
||||
await _dbContext.Deleteable<JeecgSysDictItem>()
|
||||
.Where(x => !seenList.Contains(x.Id))
|
||||
.ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
return synced;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ public class MixerMaterialService : IMixerMaterialService, ISingletonDependency
|
||||
}
|
||||
|
||||
private string BaseUrl => (_configuration.GetValue<string>("JeecgIntegration:BaseUrl") ?? "http://localhost:8080/jeecg-boot").TrimEnd('/');
|
||||
private int DefaultTenantId => (int?)_configuration.GetValue<long?>("JeecgIntegration:DefaultTenantId") ?? 1002;
|
||||
private HttpClient CreateClient() => _httpClientFactory.CreateClient("JeecgApi");
|
||||
|
||||
private async Task LoadCacheAsync()
|
||||
@@ -84,7 +85,8 @@ public class MixerMaterialService : IMixerMaterialService, ISingletonDependency
|
||||
|
||||
public async Task SyncFromRemoteAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (!await _syncLock.WaitAsync(0, ct).ConfigureAwait(false)) return;
|
||||
// 等待当前正在进行的同步结束,再执行本次同步(WaitAsync(0) 会静默跳过,导致 STOMP 通知丢失)
|
||||
await _syncLock.WaitAsync(ct).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var all = new List<MesMixerMaterial>();
|
||||
@@ -96,10 +98,15 @@ public class MixerMaterialService : IMixerMaterialService, ISingletonDependency
|
||||
var qs = HttpUtility.ParseQueryString(string.Empty);
|
||||
qs["pageNo"] = pageNo.ToString();
|
||||
qs["pageSize"] = pageSize.ToString();
|
||||
// mes_mixer_material 不在多租户隔离表中,不传 tenantId 可拉取全部记录
|
||||
var url = $"{BaseUrl}/mes/material/mixerMaterial/anon/list?{qs}";
|
||||
using var client = CreateClient();
|
||||
var resp = await client.GetAsync(url, ct).ConfigureAwait(false);
|
||||
if (!resp.IsSuccessStatusCode) break;
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.Warning($"[密炼物料] 同步请求失败,状态码:{(int)resp.StatusCode}");
|
||||
break;
|
||||
}
|
||||
|
||||
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
@@ -119,6 +126,10 @@ public class MixerMaterialService : IMixerMaterialService, ISingletonDependency
|
||||
await SaveCacheAsync(all).ConfigureAwait(false);
|
||||
_logger.Information($"[密炼物料] 同步完成,共 {all.Count} 条");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warning("[密炼物料] 同步返回 0 条,可能是 tenantId 配置有误或网络异常,保留原缓存");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Text.Json;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Events;
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.Services.Service;
|
||||
|
||||
namespace YY.Admin.Services.Service.MixerMaterial;
|
||||
|
||||
@@ -15,6 +16,7 @@ public class MixerMaterialSyncCoordinator : ISingletonDependency
|
||||
public MixerMaterialSyncCoordinator(
|
||||
IEventAggregator eventAggregator,
|
||||
IMixerMaterialService mixerMaterialService,
|
||||
SyncPollManager pollManager,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
@@ -26,6 +28,13 @@ public class MixerMaterialSyncCoordinator : ISingletonDependency
|
||||
_eventAggregator.GetEvent<NetworkStatusChangedEvent>()
|
||||
.Subscribe(OnNetworkStatusChanged, ThreadOption.BackgroundThread);
|
||||
|
||||
pollManager.Register("密炼物料", async () =>
|
||||
{
|
||||
await _mixerMaterialService.SyncFromRemoteAsync().ConfigureAwait(false);
|
||||
_eventAggregator.GetEvent<MixerMaterialChangedEvent>()
|
||||
.Publish(new MixerMaterialChangedPayload { Action = "poll" });
|
||||
});
|
||||
|
||||
_logger.Information("[密炼物料推送] MixerMaterialSyncCoordinator 已启动");
|
||||
_ = _mixerMaterialService.SyncFromRemoteAsync();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,626 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.Net.Http;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Web;
|
||||
using Prism.Events;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Events;
|
||||
using YY.Admin.Core.Services;
|
||||
|
||||
namespace YY.Admin.Services.Service.RawMaterialEntry;
|
||||
|
||||
public class RawMaterialEntryService : IRawMaterialEntryService, ISingletonDependency
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly INetworkMonitor _networkMonitor;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ILoggerService _logger;
|
||||
private readonly SemaphoreSlim _syncLock = new(1, 1);
|
||||
private readonly object _cacheLock = new();
|
||||
private readonly string _pendingOpsFilePath;
|
||||
private readonly string _cacheFilePath;
|
||||
private List<EntryPendingOperation> _pendingOps = new();
|
||||
private List<MesXslRawMaterialEntry> _localCache = new();
|
||||
|
||||
private static readonly JsonSerializerOptions _jsonOpts = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
Converters = { new NullableDateTimeJsonConverter() }
|
||||
};
|
||||
|
||||
public RawMaterialEntryService(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IConfiguration configuration,
|
||||
INetworkMonitor networkMonitor,
|
||||
IEventAggregator eventAggregator,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_configuration = configuration;
|
||||
_networkMonitor = networkMonitor;
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
|
||||
var appDataDir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"YY.Admin", "sync-cache");
|
||||
Directory.CreateDirectory(appDataDir);
|
||||
_pendingOpsFilePath = Path.Combine(appDataDir, "mes-xsl-raw-material-entry-pending-ops.json");
|
||||
_cacheFilePath = Path.Combine(appDataDir, "mes-xsl-raw-material-entry-cache.json");
|
||||
|
||||
LoadPendingOpsFromDisk();
|
||||
LoadCacheFromDisk();
|
||||
_logger.Information($"[原料入场] 服务初始化完成,缓存={_localCache.Count},待上传={_pendingOps.Count},在线={_networkMonitor.IsOnline}");
|
||||
|
||||
_networkMonitor.StatusChanged += OnNetworkStatusChanged;
|
||||
if (_networkMonitor.IsOnline)
|
||||
{
|
||||
_ = Task.Run(() => SyncAfterReconnectAsync(CancellationToken.None));
|
||||
}
|
||||
}
|
||||
|
||||
private const int MaxPendingRetries = 5;
|
||||
private string BaseUrl => (_configuration.GetValue<string>("JeecgIntegration:BaseUrl") ?? "http://localhost:8080/jeecg-boot").TrimEnd('/');
|
||||
private int DefaultTenantId => (int?)_configuration.GetValue<long?>("JeecgIntegration:DefaultTenantId") ?? 1002;
|
||||
|
||||
private HttpClient CreateClient() => _httpClientFactory.CreateClient("JeecgApi");
|
||||
|
||||
public async Task<RawMaterialEntryPageResult> PageAsync(
|
||||
int pageNo, int pageSize,
|
||||
string? barcode = null, string? batchNo = null, string? billNo = null,
|
||||
string? materialName = null, string? supplierName = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
List<MesXslRawMaterialEntry>? source = null;
|
||||
if (_networkMonitor.IsOnline)
|
||||
{
|
||||
try
|
||||
{
|
||||
source = await FetchRemoteListAsync(ct).ConfigureAwait(false);
|
||||
lock (_cacheLock)
|
||||
{
|
||||
_localCache = source.Select(Clone).ToList();
|
||||
SaveCacheToDiskUnsafe();
|
||||
}
|
||||
_logger.Information($"[原料入场列表] 远端拉取成功 count={source.Count}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
source = null;
|
||||
_logger.Warning($"[原料入场列表] 远端拉取失败,回退缓存:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
lock (_cacheLock)
|
||||
{
|
||||
source ??= _localCache.Select(Clone).ToList();
|
||||
source = ApplyPendingOpsSnapshotUnsafe(source);
|
||||
}
|
||||
|
||||
var filtered = ApplyFilters(source, barcode, batchNo, billNo, materialName, supplierName);
|
||||
var total = filtered.Count;
|
||||
var pageRecords = filtered.Skip(Math.Max(0, (pageNo - 1) * pageSize)).Take(pageSize).ToList();
|
||||
return new RawMaterialEntryPageResult(pageRecords, total, pageNo, pageSize);
|
||||
}
|
||||
|
||||
public async Task<MesXslRawMaterialEntry?> GetByIdAsync(string id, CancellationToken ct = default)
|
||||
{
|
||||
if (_networkMonitor.IsOnline)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}/xslmes/mesXslRawMaterialEntry/anon/queryById?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}";
|
||||
using var client = CreateClient();
|
||||
var resp = await client.GetAsync(url, ct).ConfigureAwait(false);
|
||||
if (!resp.IsSuccessStatusCode) return null;
|
||||
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (!doc.RootElement.TryGetProperty("result", out var resultEl)) return null;
|
||||
return resultEl.Deserialize<MesXslRawMaterialEntry>(_jsonOpts);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[原料入场详情] 远端查询失败,回退缓存 id={id}:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
lock (_cacheLock)
|
||||
{
|
||||
return _localCache.FirstOrDefault(e => string.Equals(e.Id, id, StringComparison.OrdinalIgnoreCase)) is { } found
|
||||
? Clone(found) : null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> AddAsync(MesXslRawMaterialEntry entry, CancellationToken ct = default)
|
||||
{
|
||||
if (!entry.TenantId.HasValue || entry.TenantId.Value <= 0) entry.TenantId = DefaultTenantId;
|
||||
var local = Clone(entry);
|
||||
if (string.IsNullOrWhiteSpace(local.Id)) local.Id = $"local-{Guid.NewGuid():N}";
|
||||
|
||||
if (_networkMonitor.IsOnline)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ok = await RemoteAddAsync(local, ct).ConfigureAwait(false);
|
||||
if (ok) { UpsertLocalCache(local); return true; }
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[原料入场新增] 远端失败,转离线 id={local.Id}:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
EnqueuePendingOperation(new EntryPendingOperation { OpType = EntryOpType.Add, EntryId = local.Id, Entry = local, CreatedAt = DateTime.UtcNow });
|
||||
UpsertLocalCache(local);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> EditAsync(MesXslRawMaterialEntry entry, CancellationToken ct = default)
|
||||
{
|
||||
if (!entry.TenantId.HasValue || entry.TenantId.Value <= 0) entry.TenantId = DefaultTenantId;
|
||||
var local = Clone(entry);
|
||||
|
||||
if (_networkMonitor.IsOnline)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (ok, _) = await RemoteEditAsync(local, ct).ConfigureAwait(false);
|
||||
if (ok) { UpsertLocalCache(local); return true; }
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[原料入场修改] 远端失败,转离线 id={local.Id}:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
EnqueuePendingOperation(new EntryPendingOperation { OpType = EntryOpType.Edit, EntryId = local.Id, Entry = local, AnchorUpdateTime = local.UpdateTime, CreatedAt = DateTime.UtcNow });
|
||||
UpsertLocalCache(local);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(string id, CancellationToken ct = default)
|
||||
{
|
||||
if (_networkMonitor.IsOnline)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ok = await RemoteDeleteAsync(id, ct).ConfigureAwait(false);
|
||||
if (ok) { RemoveFromLocalCache(id); return true; }
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[原料入场删除] 远端失败,转离线 id={id}:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
DateTime? anchor;
|
||||
lock (_cacheLock) { anchor = _localCache.FirstOrDefault(e => string.Equals(e.Id, id, StringComparison.OrdinalIgnoreCase))?.UpdateTime; }
|
||||
EnqueuePendingOperation(new EntryPendingOperation { OpType = EntryOpType.Delete, EntryId = id, AnchorUpdateTime = anchor, CreatedAt = DateTime.UtcNow });
|
||||
RemoveFromLocalCache(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteBatchAsync(string ids, CancellationToken ct = default)
|
||||
{
|
||||
var idList = ids.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
var allSuccess = true;
|
||||
foreach (var id in idList) allSuccess &= await DeleteAsync(id, ct).ConfigureAwait(false);
|
||||
return allSuccess;
|
||||
}
|
||||
|
||||
public async Task<string?> GenerateBarcodeAsync(string materialCode, CancellationToken ct = default)
|
||||
{
|
||||
var url = $"{BaseUrl}/xslmes/mesXslRawMaterialEntry/anon/generateBarcode?materialCode={Uri.EscapeDataString(materialCode ?? "")}";
|
||||
try
|
||||
{
|
||||
using var client = CreateClient();
|
||||
var resp = await client.GetAsync(url, ct).ConfigureAwait(false);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (doc.RootElement.TryGetProperty("result", out var el))
|
||||
return el.GetString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[原料入场] 生成条码失败: {ex.Message}");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── Remote ────────────────────────────────────────────────────────────────
|
||||
|
||||
private async Task<List<MesXslRawMaterialEntry>> FetchRemoteListAsync(CancellationToken ct)
|
||||
{
|
||||
var query = HttpUtility.ParseQueryString(string.Empty);
|
||||
query["pageNo"] = "1";
|
||||
query["pageSize"] = "10000";
|
||||
query["tenantId"] = DefaultTenantId.ToString();
|
||||
var url = $"{BaseUrl}/xslmes/mesXslRawMaterialEntry/anon/list?{query}";
|
||||
using var client = CreateClient();
|
||||
var resp = await client.GetAsync(url, ct).ConfigureAwait(false);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var result = doc.RootElement.GetProperty("result");
|
||||
return result.GetProperty("records").Deserialize<List<MesXslRawMaterialEntry>>(_jsonOpts) ?? new();
|
||||
}
|
||||
|
||||
private async Task<bool> RemoteAddAsync(MesXslRawMaterialEntry entry, CancellationToken ct)
|
||||
{
|
||||
var url = $"{BaseUrl}/xslmes/mesXslRawMaterialEntry/anon/add?tenantId={DefaultTenantId}";
|
||||
var payload = Clone(entry);
|
||||
if (IsLocalTempId(payload.Id)) payload.Id = null;
|
||||
return await PostJsonAsync(url, payload, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<(bool Ok, bool IsVersionConflict)> RemoteEditAsync(MesXslRawMaterialEntry entry, CancellationToken ct)
|
||||
{
|
||||
var url = $"{BaseUrl}/xslmes/mesXslRawMaterialEntry/anon/edit?tenantId={DefaultTenantId}";
|
||||
return await PostJsonCheckVersionAsync(url, entry, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<bool> RemoteDeleteAsync(string id, CancellationToken ct)
|
||||
{
|
||||
var url = $"{BaseUrl}/xslmes/mesXslRawMaterialEntry/anon/delete?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}";
|
||||
using var client = CreateClient();
|
||||
var resp = await client.DeleteAsync(url, ct).ConfigureAwait(false);
|
||||
return resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<bool> PostJsonAsync(string url, object body, CancellationToken ct)
|
||||
{
|
||||
var content = new StringContent(JsonSerializer.Serialize(body, _jsonOpts), Encoding.UTF8, "application/json");
|
||||
using var client = CreateClient();
|
||||
var resp = await client.PostAsync(url, content, ct).ConfigureAwait(false);
|
||||
return resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<(bool Ok, bool IsVersionConflict)> PostJsonCheckVersionAsync(string url, object body, CancellationToken ct)
|
||||
{
|
||||
var content = new StringContent(JsonSerializer.Serialize(body, _jsonOpts), Encoding.UTF8, "application/json");
|
||||
using var client = CreateClient();
|
||||
var resp = await client.PostAsync(url, content, ct).ConfigureAwait(false);
|
||||
if (!resp.IsSuccessStatusCode) return (false, false);
|
||||
try
|
||||
{
|
||||
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
int code = 200;
|
||||
if (doc.RootElement.TryGetProperty("code", out var codeEl)) code = codeEl.GetInt32();
|
||||
if (code == 200) return (true, false);
|
||||
if (doc.RootElement.TryGetProperty("message", out var msgEl))
|
||||
if ((msgEl.GetString() ?? "").Contains("已被他人修改")) return (false, true);
|
||||
return (false, false);
|
||||
}
|
||||
catch { return (true, false); }
|
||||
}
|
||||
|
||||
private static async Task<bool> IsSuccessResultAsync(HttpResponseMessage resp, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (doc.RootElement.TryGetProperty("code", out var code)) return code.GetInt32() == 200;
|
||||
if (doc.RootElement.TryGetProperty("success", out var success)) return success.GetBoolean();
|
||||
return true;
|
||||
}
|
||||
catch { return true; }
|
||||
}
|
||||
|
||||
// ─── Network / Reconnect ───────────────────────────────────────────────────
|
||||
|
||||
private void OnNetworkStatusChanged(bool isOnline)
|
||||
{
|
||||
if (!isOnline) return;
|
||||
_ = Task.Run(() => SyncAfterReconnectAsync(CancellationToken.None));
|
||||
}
|
||||
|
||||
private async Task SyncAfterReconnectAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var pushResult = await PushPendingOnReconnectAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (!_networkMonitor.IsOnline) return;
|
||||
try
|
||||
{
|
||||
var remote = await FetchRemoteListAsync(cancellationToken).ConfigureAwait(false);
|
||||
lock (_cacheLock) { _localCache = remote.Select(Clone).ToList(); SaveCacheToDiskUnsafe(); }
|
||||
_eventAggregator.GetEvent<RawMaterialEntryChangedEvent>().Publish(new RawMaterialEntryChangedPayload { Action = "pull" });
|
||||
_logger.Information($"[原料入场重连] 全量回拉成功 count={remote.Count}");
|
||||
}
|
||||
catch (Exception ex) { _logger.Warning($"[原料入场重连] 全量回拉失败:{ex.Message}"); }
|
||||
|
||||
if (pushResult.PushedCount > 0 || pushResult.ConflictCount > 0 || pushResult.NewRecordsPushed > 0)
|
||||
{
|
||||
_eventAggregator.GetEvent<SyncConflictEvent>().Publish(new SyncConflictPayload
|
||||
{
|
||||
EntityName = "原料入场",
|
||||
PushedCount = pushResult.PushedCount,
|
||||
ConflictCount = pushResult.ConflictCount,
|
||||
NewRecordsPushed = pushResult.NewRecordsPushed
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record PendingReplayResult(bool Ok, bool IsConflict, string? EntityId);
|
||||
|
||||
private async Task<PushPendingResult> PushPendingOnReconnectAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!await _syncLock.WaitAsync(0, cancellationToken).ConfigureAwait(false))
|
||||
return new PushPendingResult(0, 0, 0);
|
||||
try
|
||||
{
|
||||
List<EntryPendingOperation> snapshot;
|
||||
lock (_cacheLock) { snapshot = _pendingOps.OrderBy(x => x.CreatedAt).ToList(); }
|
||||
|
||||
int pushed = 0, conflicts = 0, newPushed = 0;
|
||||
foreach (var op in snapshot)
|
||||
{
|
||||
if (!_networkMonitor.IsOnline) break;
|
||||
lock (_cacheLock) { if (!_pendingOps.Any(x => x.Id == op.Id)) continue; }
|
||||
|
||||
var result = await ExecutePendingOperationAsync(op, cancellationToken).ConfigureAwait(false);
|
||||
if (!result.Ok)
|
||||
{
|
||||
lock (_cacheLock)
|
||||
{
|
||||
op.RetryCount++;
|
||||
if (op.RetryCount >= MaxPendingRetries) _pendingOps.RemoveAll(x => x.Id == op.Id);
|
||||
SavePendingOpsToDiskUnsafe();
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (result.IsConflict)
|
||||
{
|
||||
conflicts++;
|
||||
if (!string.IsNullOrWhiteSpace(result.EntityId)) RemovePendingOpsByEntryId(result.EntityId!);
|
||||
continue;
|
||||
}
|
||||
lock (_cacheLock)
|
||||
{
|
||||
if (op.OpType == EntryOpType.Add) newPushed++; else pushed++;
|
||||
_pendingOps.RemoveAll(x => x.Id == op.Id);
|
||||
SavePendingOpsToDiskUnsafe();
|
||||
}
|
||||
}
|
||||
return new PushPendingResult(pushed, conflicts, newPushed);
|
||||
}
|
||||
finally { _syncLock.Release(); }
|
||||
}
|
||||
|
||||
private async Task<PendingReplayResult> ExecutePendingOperationAsync(EntryPendingOperation op, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (op.OpType)
|
||||
{
|
||||
case EntryOpType.Add:
|
||||
var ok = op.Entry != null && await RemoteAddAsync(op.Entry, ct).ConfigureAwait(false);
|
||||
return ok ? new PendingReplayResult(true, false, op.EntryId) : new PendingReplayResult(false, false, null);
|
||||
|
||||
case EntryOpType.Edit:
|
||||
if (op.Entry == null || string.IsNullOrWhiteSpace(op.Entry.Id))
|
||||
return new PendingReplayResult(false, false, null);
|
||||
var id = op.Entry.Id;
|
||||
var remote = await FetchRemoteSingleAsync(id, ct).ConfigureAwait(false);
|
||||
if (remote != null && op.AnchorUpdateTime != null && remote.UpdateTime != op.AnchorUpdateTime)
|
||||
{
|
||||
UpsertLocalCache(remote);
|
||||
return new PendingReplayResult(true, true, id);
|
||||
}
|
||||
var (editOk, isConflict) = await RemoteEditAsync(op.Entry, ct).ConfigureAwait(false);
|
||||
if (isConflict) { var fresh = await FetchRemoteSingleAsync(id, ct).ConfigureAwait(false); if (fresh != null) UpsertLocalCache(fresh); return new PendingReplayResult(true, true, id); }
|
||||
return editOk ? new PendingReplayResult(true, false, id) : new PendingReplayResult(false, false, null);
|
||||
|
||||
case EntryOpType.Delete:
|
||||
if (string.IsNullOrWhiteSpace(op.EntryId)) return new PendingReplayResult(false, false, null);
|
||||
var delId = op.EntryId!;
|
||||
var delRemote = await FetchRemoteSingleAsync(delId, ct).ConfigureAwait(false);
|
||||
if (delRemote == null) return new PendingReplayResult(true, false, delId);
|
||||
if (op.AnchorUpdateTime != null && delRemote.UpdateTime != op.AnchorUpdateTime) { UpsertLocalCache(delRemote); return new PendingReplayResult(true, true, delId); }
|
||||
var delOk = await RemoteDeleteAsync(delId, ct).ConfigureAwait(false);
|
||||
return delOk ? new PendingReplayResult(true, false, delId) : new PendingReplayResult(false, false, null);
|
||||
|
||||
default:
|
||||
return new PendingReplayResult(true, false, null);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[原料入场推送] 执行异常 op={op.OpType},entryId={op.EntryId}:{ex.Message}");
|
||||
return new PendingReplayResult(false, false, null);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<MesXslRawMaterialEntry?> FetchRemoteSingleAsync(string id, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}/xslmes/mesXslRawMaterialEntry/anon/queryById?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}";
|
||||
using var client = CreateClient();
|
||||
var resp = await client.GetAsync(url, ct).ConfigureAwait(false);
|
||||
if (!resp.IsSuccessStatusCode) return null;
|
||||
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (doc.RootElement.TryGetProperty("result", out var resultEl))
|
||||
return resultEl.Deserialize<MesXslRawMaterialEntry>(_jsonOpts);
|
||||
return null;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
// ─── Local cache helpers ───────────────────────────────────────────────────
|
||||
|
||||
private static List<MesXslRawMaterialEntry> ApplyFilters(
|
||||
List<MesXslRawMaterialEntry> source,
|
||||
string? barcode, string? batchNo, string? billNo,
|
||||
string? materialName, string? supplierName)
|
||||
{
|
||||
IEnumerable<MesXslRawMaterialEntry> q = source;
|
||||
if (!string.IsNullOrWhiteSpace(barcode))
|
||||
q = q.Where(e => (e.Barcode ?? "").Contains(barcode, StringComparison.OrdinalIgnoreCase));
|
||||
if (!string.IsNullOrWhiteSpace(batchNo))
|
||||
q = q.Where(e => (e.BatchNo ?? "").Contains(batchNo, StringComparison.OrdinalIgnoreCase));
|
||||
if (!string.IsNullOrWhiteSpace(billNo))
|
||||
q = q.Where(e => (e.BillNo ?? "").Contains(billNo, StringComparison.OrdinalIgnoreCase));
|
||||
if (!string.IsNullOrWhiteSpace(materialName))
|
||||
q = q.Where(e => (e.MaterialName ?? "").Contains(materialName, StringComparison.OrdinalIgnoreCase));
|
||||
if (!string.IsNullOrWhiteSpace(supplierName))
|
||||
q = q.Where(e => (e.SupplierName ?? "").Contains(supplierName, StringComparison.OrdinalIgnoreCase));
|
||||
return q.OrderByDescending(e => e.EntryTime ?? e.CreateTime ?? DateTime.MinValue).ToList();
|
||||
}
|
||||
|
||||
private List<MesXslRawMaterialEntry> ApplyPendingOpsSnapshotUnsafe(List<MesXslRawMaterialEntry> source)
|
||||
{
|
||||
var map = source.Where(e => !string.IsNullOrWhiteSpace(e.Id))
|
||||
.ToDictionary(e => e.Id!, Clone, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var op in _pendingOps.OrderBy(x => x.CreatedAt))
|
||||
{
|
||||
switch (op.OpType)
|
||||
{
|
||||
case EntryOpType.Add:
|
||||
case EntryOpType.Edit:
|
||||
if (op.Entry != null && !string.IsNullOrWhiteSpace(op.Entry.Id))
|
||||
map[op.Entry.Id] = Clone(op.Entry);
|
||||
break;
|
||||
case EntryOpType.Delete:
|
||||
if (!string.IsNullOrWhiteSpace(op.EntryId))
|
||||
map.Remove(op.EntryId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return map.Values.ToList();
|
||||
}
|
||||
|
||||
private void EnqueuePendingOperation(EntryPendingOperation op)
|
||||
{
|
||||
lock (_cacheLock) { _pendingOps.Add(op); SavePendingOpsToDiskUnsafe(); }
|
||||
}
|
||||
|
||||
private void UpsertLocalCache(MesXslRawMaterialEntry entry)
|
||||
{
|
||||
lock (_cacheLock)
|
||||
{
|
||||
var idx = _localCache.FindIndex(e => string.Equals(e.Id, entry.Id, StringComparison.OrdinalIgnoreCase));
|
||||
if (idx >= 0) _localCache[idx] = Clone(entry);
|
||||
else _localCache.Insert(0, Clone(entry));
|
||||
SaveCacheToDiskUnsafe();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveFromLocalCache(string id)
|
||||
{
|
||||
lock (_cacheLock) { _localCache.RemoveAll(e => string.Equals(e.Id, id, StringComparison.OrdinalIgnoreCase)); SaveCacheToDiskUnsafe(); }
|
||||
}
|
||||
|
||||
private void RemovePendingOpsByEntryId(string entryId)
|
||||
{
|
||||
lock (_cacheLock)
|
||||
{
|
||||
_pendingOps.RemoveAll(x =>
|
||||
(!string.IsNullOrWhiteSpace(x.EntryId) && string.Equals(x.EntryId, entryId, StringComparison.OrdinalIgnoreCase)) ||
|
||||
(x.Entry?.Id != null && string.Equals(x.Entry.Id, entryId, StringComparison.OrdinalIgnoreCase)));
|
||||
SavePendingOpsToDiskUnsafe();
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadPendingOpsFromDisk()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_pendingOpsFilePath)) return;
|
||||
var data = JsonSerializer.Deserialize<List<EntryPendingOperation>>(File.ReadAllText(_pendingOpsFilePath), _jsonOpts);
|
||||
_pendingOps = data ?? new();
|
||||
}
|
||||
catch { _pendingOps = new(); }
|
||||
}
|
||||
|
||||
private void LoadCacheFromDisk()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_cacheFilePath)) return;
|
||||
var data = JsonSerializer.Deserialize<List<MesXslRawMaterialEntry>>(File.ReadAllText(_cacheFilePath), _jsonOpts);
|
||||
_localCache = data ?? new();
|
||||
}
|
||||
catch { _localCache = new(); }
|
||||
}
|
||||
|
||||
private void SavePendingOpsToDiskUnsafe() =>
|
||||
File.WriteAllText(_pendingOpsFilePath, JsonSerializer.Serialize(_pendingOps, _jsonOpts));
|
||||
|
||||
private void SaveCacheToDiskUnsafe() =>
|
||||
File.WriteAllText(_cacheFilePath, JsonSerializer.Serialize(_localCache, _jsonOpts));
|
||||
|
||||
private static MesXslRawMaterialEntry Clone(MesXslRawMaterialEntry e) => new()
|
||||
{
|
||||
Id = e.Id, Barcode = e.Barcode, BatchNo = e.BatchNo, EntryTime = e.EntryTime,
|
||||
WeightRecordId = e.WeightRecordId, BillNo = e.BillNo, MaterialId = e.MaterialId,
|
||||
MaterialName = e.MaterialName, SupplyCustomer = e.SupplyCustomer, SupplierId = e.SupplierId,
|
||||
SupplierName = e.SupplierName, ManufacturerMaterialName = e.ManufacturerMaterialName,
|
||||
ShelfLife = e.ShelfLife, TotalWeight = e.TotalWeight, TotalPortions = e.TotalPortions,
|
||||
PortionWeight = e.PortionWeight, PortionPackages = e.PortionPackages,
|
||||
TestResult = e.TestResult, TestStatus = e.TestStatus, PrintFlag = e.PrintFlag,
|
||||
StockBalance = e.StockBalance, WarehouseLocation = e.WarehouseLocation,
|
||||
UnloadOperator = e.UnloadOperator, IsSpecialAdoption = e.IsSpecialAdoption,
|
||||
SpecialAdoptionOperator = e.SpecialAdoptionOperator, SpecialAdoptionTime = e.SpecialAdoptionTime,
|
||||
SpecialAdoptionReason = e.SpecialAdoptionReason, Status = e.Status, Remark = e.Remark,
|
||||
CreateBy = e.CreateBy, CreateTime = e.CreateTime, UpdateBy = e.UpdateBy,
|
||||
UpdateTime = e.UpdateTime, TenantId = e.TenantId
|
||||
};
|
||||
|
||||
private static bool IsLocalTempId(string? id) =>
|
||||
!string.IsNullOrWhiteSpace(id) && id.StartsWith("local-", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private sealed class EntryPendingOperation
|
||||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||
public EntryOpType OpType { get; set; }
|
||||
public string? EntryId { get; set; }
|
||||
public MesXslRawMaterialEntry? Entry { get; set; }
|
||||
public DateTime? AnchorUpdateTime { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public int RetryCount { get; set; } = 0;
|
||||
}
|
||||
|
||||
private enum EntryOpType { Add = 1, Edit = 2, Delete = 3 }
|
||||
|
||||
private sealed class NullableDateTimeJsonConverter : JsonConverter<DateTime?>
|
||||
{
|
||||
private static readonly string[] SupportedFormats =
|
||||
[
|
||||
"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, SupportedFormats, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AssumeLocal, out var exact)) return exact;
|
||||
if (DateTime.TryParse(raw, out var fallback)) return fallback;
|
||||
}
|
||||
throw new JsonException($"无法将 JSON 值转换为 DateTime?,token={reader.TokenType}");
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value.HasValue) writer.WriteStringValue(value.Value.ToString("yyyy-MM-dd HH:mm:ss"));
|
||||
else writer.WriteNullValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using Prism.Events;
|
||||
using System.Text.Json;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Events;
|
||||
using YY.Admin.Core.Services;
|
||||
|
||||
namespace YY.Admin.Services.Service.RawMaterialEntry;
|
||||
|
||||
/// <summary>
|
||||
/// 监听 STOMP 收到的原料入场记录变更信号,转发为桌面端 Prism 事件,触发列表刷新。
|
||||
/// </summary>
|
||||
public class RawMaterialEntrySyncCoordinator : ISingletonDependency
|
||||
{
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ILoggerService _logger;
|
||||
private SubscriptionToken? _remoteCommandToken;
|
||||
private SubscriptionToken? _networkStatusToken;
|
||||
|
||||
public RawMaterialEntrySyncCoordinator(
|
||||
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<RawMaterialEntryChangedEvent>()
|
||||
.Publish(new RawMaterialEntryChangedPayload { Action = "poll" });
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
_logger.Information("[原料入场] RawMaterialEntrySyncCoordinator 已启动");
|
||||
}
|
||||
|
||||
private void OnNetworkStatusChanged(NetworkStatusChangedPayload payload)
|
||||
{
|
||||
if (!payload.IsOnline) return;
|
||||
_logger.Information("[原料入场] 网络恢复,触发补偿刷新");
|
||||
_eventAggregator.GetEvent<RawMaterialEntryChangedEvent>().Publish(new RawMaterialEntryChangedPayload { 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("MES_RAW_MATERIAL_ENTRY_CHANGED", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
doc.RootElement.TryGetProperty("action", out var actionEl);
|
||||
doc.RootElement.TryGetProperty("entryId", out var idEl);
|
||||
|
||||
var changedPayload = new RawMaterialEntryChangedPayload
|
||||
{
|
||||
Action = actionEl.GetString() ?? string.Empty,
|
||||
EntryId = idEl.ValueKind == JsonValueKind.String ? idEl.GetString() : null,
|
||||
};
|
||||
|
||||
_logger.Information($"收到原料入场变更信号: action={changedPayload.Action}, entryId={changedPayload.EntryId}");
|
||||
_eventAggregator.GetEvent<RawMaterialEntryChangedEvent>().Publish(changedPayload);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"处理 STOMP 原料入场变更信号失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ public class SupplierSyncCoordinator : ISingletonDependency
|
||||
public SupplierSyncCoordinator(
|
||||
IEventAggregator eventAggregator,
|
||||
ISupplierService supplierService,
|
||||
SyncPollManager pollManager,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
@@ -24,6 +25,13 @@ public class SupplierSyncCoordinator : ISingletonDependency
|
||||
.Subscribe(OnRemoteCommand, ThreadOption.BackgroundThread);
|
||||
_eventAggregator.GetEvent<NetworkStatusChangedEvent>()
|
||||
.Subscribe(OnNetworkStatusChanged, ThreadOption.BackgroundThread);
|
||||
|
||||
pollManager.Register("供应商", () =>
|
||||
{
|
||||
_eventAggregator.GetEvent<SupplierChangedEvent>()
|
||||
.Publish(new SupplierChangedPayload { Action = "poll" });
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
private async void OnNetworkStatusChanged(NetworkStatusChangedPayload payload)
|
||||
|
||||
58
yy-admin-master/YY.Admin.Services/Service/SyncPollManager.cs
Normal file
58
yy-admin-master/YY.Admin.Services/Service/SyncPollManager.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using YY.Admin.Core;
|
||||
|
||||
namespace YY.Admin.Services.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 统一后台轮询管理器。
|
||||
/// 各模块通过 <see cref="Register"/> 注册轮询任务,定时器统一触发。
|
||||
/// ★ 只需修改 <see cref="PollInterval"/> 即可调整所有模块的轮询间隔。
|
||||
/// </summary>
|
||||
public class SyncPollManager : ISingletonDependency
|
||||
{
|
||||
// ★ 唯一轮询间隔配置 — 修改这里即可调整所有模块的轮询频率
|
||||
public static readonly TimeSpan PollInterval = TimeSpan.FromMinutes(5);
|
||||
|
||||
private readonly List<(string Name, Func<Task> Task)> _tasks = [];
|
||||
private readonly Timer _timer;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
public SyncPollManager(ILoggerService logger)
|
||||
{
|
||||
_logger = logger;
|
||||
// 首次延迟一个完整周期,确保所有协调器启动后都已完成注册
|
||||
_timer = new Timer(OnTick, null, PollInterval, PollInterval);
|
||||
_logger.Information($"[轮询管理器] 已启动,间隔 {PollInterval.TotalMinutes} 分钟");
|
||||
}
|
||||
|
||||
/// <summary>注册一个需要定时执行的轮询任务</summary>
|
||||
/// <param name="name">任务名称,用于日志</param>
|
||||
/// <param name="pollTask">异步轮询委托</param>
|
||||
public void Register(string name, Func<Task> pollTask)
|
||||
{
|
||||
lock (_tasks)
|
||||
_tasks.Add((name, pollTask));
|
||||
_logger.Information($"[轮询管理器] 注册任务: {name}");
|
||||
}
|
||||
|
||||
private void OnTick(object? _)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
List<(string Name, Func<Task> Task)> snapshot;
|
||||
lock (_tasks) snapshot = [.. _tasks];
|
||||
|
||||
_logger.Debug($"[轮询管理器] 触发,执行 {snapshot.Count} 个任务");
|
||||
foreach (var (name, task) in snapshot)
|
||||
{
|
||||
try
|
||||
{
|
||||
await task().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[轮询管理器] 任务 [{name}] 异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -16,18 +16,28 @@ public class VehicleSyncCoordinator : ISingletonDependency
|
||||
private SubscriptionToken? _remoteCommandToken;
|
||||
private SubscriptionToken? _networkStatusToken;
|
||||
|
||||
public VehicleSyncCoordinator(IEventAggregator eventAggregator, ILoggerService logger)
|
||||
public VehicleSyncCoordinator(
|
||||
IEventAggregator eventAggregator,
|
||||
SyncPollManager pollManager,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
_remoteCommandToken = _eventAggregator
|
||||
.GetEvent<RemoteCommandReceivedEvent>()
|
||||
.Subscribe(OnRemoteCommand, ThreadOption.BackgroundThread);
|
||||
// 断线重连后补拉一次,覆盖离线期间漏掉的 STOMP 事件
|
||||
_networkStatusToken = _eventAggregator
|
||||
.GetEvent<NetworkStatusChangedEvent>()
|
||||
.Subscribe(OnNetworkStatusChanged, ThreadOption.BackgroundThread);
|
||||
_logger.Information("[车辆推送] VehicleSyncCoordinator 已启动,开始监听 RemoteCommandReceivedEvent");
|
||||
|
||||
pollManager.Register("车辆", () =>
|
||||
{
|
||||
_eventAggregator.GetEvent<VehicleChangedEvent>()
|
||||
.Publish(new VehicleChangedPayload { Action = "poll" });
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
_logger.Information("[车辆推送] VehicleSyncCoordinator 已启动");
|
||||
}
|
||||
|
||||
private void OnNetworkStatusChanged(NetworkStatusChangedPayload payload)
|
||||
|
||||
@@ -16,7 +16,10 @@ public class WeightRecordSyncCoordinator : ISingletonDependency
|
||||
private SubscriptionToken? _remoteCommandToken;
|
||||
private SubscriptionToken? _networkStatusToken;
|
||||
|
||||
public WeightRecordSyncCoordinator(IEventAggregator eventAggregator, ILoggerService logger)
|
||||
public WeightRecordSyncCoordinator(
|
||||
IEventAggregator eventAggregator,
|
||||
SyncPollManager pollManager,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
@@ -26,6 +29,14 @@ public class WeightRecordSyncCoordinator : ISingletonDependency
|
||||
_networkStatusToken = _eventAggregator
|
||||
.GetEvent<NetworkStatusChangedEvent>()
|
||||
.Subscribe(OnNetworkStatusChanged, ThreadOption.BackgroundThread);
|
||||
|
||||
pollManager.Register("磅单", () =>
|
||||
{
|
||||
_eventAggregator.GetEvent<MesXslWeightRecordChangedEvent>()
|
||||
.Publish(new MesXslWeightRecordChangedPayload { Action = "poll" });
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
_logger.Information("[磅单推送] WeightRecordSyncCoordinator 已启动");
|
||||
}
|
||||
|
||||
|
||||
@@ -150,6 +150,10 @@ public class StompWebSocketService : ISignalRService
|
||||
await SendFrameAsync(
|
||||
BuildSubscribeFrame("sub-sys-dicts", "/topic/sync/sys-dicts"),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
// 原料入场记录变更:订阅 /topic/sync/mes-raw-material-entries
|
||||
await SendFrameAsync(
|
||||
BuildSubscribeFrame("sub-mes-raw-material-entries", "/topic/sync/mes-raw-material-entries"),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// 订阅服务端 PONG 回复(应用层假在线检测)
|
||||
await SendFrameAsync(
|
||||
|
||||
@@ -13,6 +13,7 @@ using YY.Admin.Views.MixerMaterial;
|
||||
using YY.Admin.ViewModels.Vehicle;
|
||||
using YY.Admin.Views.Vehicle;
|
||||
using YY.Admin.Views.WeightRecord;
|
||||
using YY.Admin.Views.RawMaterialEntry;
|
||||
|
||||
namespace YY.Admin
|
||||
{
|
||||
@@ -70,6 +71,8 @@ namespace YY.Admin
|
||||
containerRegistry.RegisterForNavigation<WeightRecordOperationView>();
|
||||
// 密炼物料信息
|
||||
containerRegistry.RegisterForNavigation<MixerMaterialListView>();
|
||||
// 原料入场记录
|
||||
containerRegistry.RegisterForNavigation<RawMaterialEntryListView>();
|
||||
}
|
||||
}
|
||||
public class DialogWindow : Window, IDialogWindow
|
||||
|
||||
@@ -13,11 +13,13 @@ using YY.Admin.Infrastructure.Hubs;
|
||||
using YY.Admin.Infrastructure.Network;
|
||||
using YY.Admin.Infrastructure.Storage;
|
||||
using YY.Admin.Infrastructure.Sync;
|
||||
using YY.Admin.Services.Service;
|
||||
using YY.Admin.Services.Service.Category;
|
||||
using YY.Admin.Services.Service.Customer;
|
||||
using YY.Admin.Services.Service.Dict;
|
||||
using YY.Admin.Services.Service.MixerMaterial;
|
||||
using YY.Admin.Services.Service.Supplier;
|
||||
using YY.Admin.Services.Service.RawMaterialEntry;
|
||||
using YY.Admin.Services.Service.Vehicle;
|
||||
using YY.Admin.Services.Service.WeightRecord;
|
||||
|
||||
@@ -52,10 +54,15 @@ public class SyncModule : IModule
|
||||
// 密炼物料信息:API直连 + STOMP实时通知
|
||||
containerRegistry.RegisterSingleton<IMixerMaterialService, MixerMaterialService>();
|
||||
containerRegistry.RegisterSingleton<MixerMaterialSyncCoordinator>();
|
||||
// 原料入场记录:免密 API 直连 + STOMP 实时通知
|
||||
containerRegistry.RegisterSingleton<IRawMaterialEntryService, RawMaterialEntryService>();
|
||||
containerRegistry.RegisterSingleton<RawMaterialEntrySyncCoordinator>();
|
||||
// 分类字典:启动同步 + 断线重连补刷
|
||||
containerRegistry.RegisterSingleton<CategorySyncCoordinator>();
|
||||
// 数据字典:启动同步 + 断线重连补刷
|
||||
containerRegistry.RegisterSingleton<DictSyncCoordinator>();
|
||||
// 统一轮询管理器(修改 SyncPollManager.PollInterval 即可调整所有模块的轮询间隔)
|
||||
containerRegistry.RegisterSingleton<SyncPollManager>();
|
||||
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddTransient<DisconnectGuardHandler>();
|
||||
@@ -100,6 +107,8 @@ public class SyncModule : IModule
|
||||
_ = Task.Run(() => signalService.ConnectUnifiedDeviceChannelAsync(CancellationToken.None));
|
||||
// 强制实例化事件订阅器(单例,构造函数内完成订阅注册)
|
||||
_ = containerProvider.Resolve<SysUserEventSubscriber>();
|
||||
// 统一轮询管理器必须在各协调器之前实例化,确保 Register() 能正常接收注册
|
||||
_ = containerProvider.Resolve<SyncPollManager>();
|
||||
// 强制实例化车辆同步协调器(构造函数内订阅 STOMP 车辆变更事件)
|
||||
_ = containerProvider.Resolve<VehicleSyncCoordinator>();
|
||||
// 强制实例化客户同步协调器
|
||||
@@ -110,6 +119,8 @@ public class SyncModule : IModule
|
||||
_ = containerProvider.Resolve<WeightRecordSyncCoordinator>();
|
||||
// 强制实例化密炼物料同步协调器
|
||||
_ = containerProvider.Resolve<MixerMaterialSyncCoordinator>();
|
||||
// 强制实例化原料入场记录同步协调器
|
||||
_ = containerProvider.Resolve<RawMaterialEntrySyncCoordinator>();
|
||||
// 强制实例化分类字典同步协调器
|
||||
_ = containerProvider.Resolve<CategorySyncCoordinator>();
|
||||
// 强制实例化数据字典同步协调器
|
||||
|
||||
@@ -120,7 +120,12 @@ namespace YY.Admin.ViewModels.Control
|
||||
// 已实现页面:密炼物料信息
|
||||
["MixerMaterialListView"] = "MixerMaterialListView",
|
||||
["/xslmes/mesMixerMaterial"] = "MixerMaterialListView",
|
||||
["mesMixerMaterial"] = "MixerMaterialListView"
|
||||
["mesMixerMaterial"] = "MixerMaterialListView",
|
||||
|
||||
// 已实现页面:原料入场记录
|
||||
["RawMaterialEntryListView"] = "RawMaterialEntryListView",
|
||||
["/xslmes/mesXslRawMaterialEntry"] = "RawMaterialEntryListView",
|
||||
["mesXslRawMaterialEntry"] = "RawMaterialEntryListView"
|
||||
};
|
||||
|
||||
private MenuItem? _selectedMenuItem;
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
using HandyControl.Controls;
|
||||
using HandyControl.Tools.Extension;
|
||||
using System.Collections.ObjectModel;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.Services.Service;
|
||||
|
||||
namespace YY.Admin.ViewModels.RawMaterialEntry;
|
||||
|
||||
public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResultable<bool>
|
||||
{
|
||||
private readonly IRawMaterialEntryService _entryService;
|
||||
private readonly IJeecgDictSyncService _dictSyncService;
|
||||
private readonly IMixerMaterialService _mixerMaterialService;
|
||||
|
||||
// 加载完物料后用于回填 Edit 模式选中项
|
||||
private string? _pendingMaterialId;
|
||||
|
||||
private MesXslRawMaterialEntry? _entry;
|
||||
public MesXslRawMaterialEntry? Entry
|
||||
{
|
||||
get => _entry;
|
||||
set => SetProperty(ref _entry, value);
|
||||
}
|
||||
|
||||
private MesMixerMaterial? _selectedMaterial;
|
||||
public MesMixerMaterial? SelectedMaterial
|
||||
{
|
||||
get => _selectedMaterial;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _selectedMaterial, value) || value == null || Entry == null) return;
|
||||
Entry.MaterialId = value.Id;
|
||||
Entry.MaterialCode = value.MaterialCode;
|
||||
Entry.MaterialName = value.MaterialName;
|
||||
RaisePropertyChanged(nameof(Entry));
|
||||
|
||||
// 新增模式自动生成条码/批次号
|
||||
if (IsAddMode && !string.IsNullOrEmpty(value.MaterialCode))
|
||||
_ = AutoGenerateBarcodeAsync(value.MaterialCode);
|
||||
}
|
||||
}
|
||||
|
||||
private bool _isGenerating;
|
||||
public bool IsGenerating
|
||||
{
|
||||
get => _isGenerating;
|
||||
set => SetProperty(ref _isGenerating, value);
|
||||
}
|
||||
|
||||
public bool IsAddMode => string.IsNullOrWhiteSpace(Entry?.Id);
|
||||
public string DialogTitle => IsAddMode ? "新增原料入场记录" : "编辑原料入场记录";
|
||||
|
||||
public ObservableCollection<MesMixerMaterial> MaterialOptions { get; } = new();
|
||||
public ObservableCollection<KeyValuePair<string, string>> TestResultOptions { get; } = new();
|
||||
public ObservableCollection<KeyValuePair<string, string>> TestStatusOptions { get; } = new();
|
||||
public ObservableCollection<KeyValuePair<string, string>> PrintFlagOptions { get; } = new();
|
||||
public ObservableCollection<KeyValuePair<string, string>> StockBalanceOptions { get; } = new();
|
||||
public ObservableCollection<KeyValuePair<string, string>> IsSpecialAdoptionOptions { get; } = new();
|
||||
public ObservableCollection<KeyValuePair<string, string>> StatusOptions { get; } = new();
|
||||
|
||||
private bool _result;
|
||||
public bool Result { get => _result; set => SetProperty(ref _result, value); }
|
||||
public Action? CloseAction { get; set; }
|
||||
|
||||
public DelegateCommand SaveCommand { get; }
|
||||
public DelegateCommand CancelCommand { get; }
|
||||
|
||||
public RawMaterialEntryEditDialogViewModel(
|
||||
IRawMaterialEntryService entryService,
|
||||
IJeecgDictSyncService dictSyncService,
|
||||
IMixerMaterialService mixerMaterialService,
|
||||
IContainerExtension container,
|
||||
IRegionManager regionManager) : base(container, regionManager)
|
||||
{
|
||||
_entryService = entryService;
|
||||
_dictSyncService = dictSyncService;
|
||||
_mixerMaterialService = mixerMaterialService;
|
||||
SaveCommand = new DelegateCommand(async () => await SaveAsync());
|
||||
CancelCommand = new DelegateCommand(() => CloseAction?.Invoke());
|
||||
_ = LoadAllAsync();
|
||||
}
|
||||
|
||||
private async Task LoadAllAsync()
|
||||
{
|
||||
await Task.WhenAll(LoadDictOptionsAsync(), LoadMaterialOptionsAsync());
|
||||
}
|
||||
|
||||
private async Task LoadMaterialOptionsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 每次打开弹窗都主动拉一次,确保直接写库的数据也能同步到
|
||||
await _mixerMaterialService.SyncFromRemoteAsync();
|
||||
var result = await _mixerMaterialService.PageAsync(1, 2000);
|
||||
MaterialOptions.Clear();
|
||||
foreach (var m in result.Records)
|
||||
MaterialOptions.Add(m);
|
||||
|
||||
// Edit 模式:物料加载完后回填选中项
|
||||
if (_pendingMaterialId != null)
|
||||
{
|
||||
var match = MaterialOptions.FirstOrDefault(m =>
|
||||
string.Equals(m.Id, _pendingMaterialId, StringComparison.OrdinalIgnoreCase));
|
||||
if (match != null)
|
||||
SelectedMaterial = match;
|
||||
_pendingMaterialId = null;
|
||||
}
|
||||
}
|
||||
catch { /* 无法加载物料列表时不阻断表单 */ }
|
||||
}
|
||||
|
||||
private async Task LoadDictOptionsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var testResultOpts = await _dictSyncService.GetDictOptionsAsync("xslmes_test_result");
|
||||
var testStatusOpts = await _dictSyncService.GetDictOptionsAsync("xslmes_test_status");
|
||||
var printFlagOpts = await _dictSyncService.GetDictOptionsAsync("xslmes_print_flag");
|
||||
var ynOpts = await _dictSyncService.GetDictOptionsAsync("yn");
|
||||
var statusOpts = await _dictSyncService.GetDictOptionsAsync("xslmes_entry_status");
|
||||
|
||||
PopulateOptions(TestResultOptions, testResultOpts, new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("未检", "0"),
|
||||
new KeyValuePair<string, string>("合格", "1"),
|
||||
new KeyValuePair<string, string>("不合格", "2"),
|
||||
});
|
||||
PopulateOptions(TestStatusOptions, testStatusOpts, new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("送样", "0"),
|
||||
new KeyValuePair<string, string>("已批准", "1"),
|
||||
});
|
||||
PopulateOptions(PrintFlagOptions, printFlagOpts, new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("未打印", "0"),
|
||||
new KeyValuePair<string, string>("已打印", "1"),
|
||||
});
|
||||
PopulateOptions(StockBalanceOptions, ynOpts, new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("否", "0"),
|
||||
new KeyValuePair<string, string>("是", "1"),
|
||||
});
|
||||
PopulateOptions(IsSpecialAdoptionOptions, ynOpts, new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("否", "0"),
|
||||
new KeyValuePair<string, string>("是", "1"),
|
||||
});
|
||||
PopulateOptions(StatusOptions, statusOpts, Array.Empty<KeyValuePair<string, string>>());
|
||||
}
|
||||
catch { FillFallbackOptions(); }
|
||||
}
|
||||
|
||||
private static void PopulateOptions(
|
||||
ObservableCollection<KeyValuePair<string, string>> target,
|
||||
IEnumerable<KeyValuePair<string, string>> items,
|
||||
IEnumerable<KeyValuePair<string, string>> fallback)
|
||||
{
|
||||
target.Clear();
|
||||
var list = items.ToList();
|
||||
foreach (var item in list.Count > 0 ? list : fallback.ToList())
|
||||
target.Add(item);
|
||||
}
|
||||
|
||||
private void FillFallbackOptions()
|
||||
{
|
||||
PopulateOptions(TestResultOptions, Array.Empty<KeyValuePair<string, string>>(), new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("未检", "0"),
|
||||
new KeyValuePair<string, string>("合格", "1"),
|
||||
new KeyValuePair<string, string>("不合格", "2"),
|
||||
});
|
||||
PopulateOptions(TestStatusOptions, Array.Empty<KeyValuePair<string, string>>(), new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("送样", "0"),
|
||||
new KeyValuePair<string, string>("已批准", "1"),
|
||||
});
|
||||
PopulateOptions(PrintFlagOptions, Array.Empty<KeyValuePair<string, string>>(), new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("未打印", "0"),
|
||||
new KeyValuePair<string, string>("已打印", "1"),
|
||||
});
|
||||
var ynDefault = new[] { new KeyValuePair<string, string>("否", "0"), new KeyValuePair<string, string>("是", "1") };
|
||||
PopulateOptions(StockBalanceOptions, Array.Empty<KeyValuePair<string, string>>(), ynDefault);
|
||||
PopulateOptions(IsSpecialAdoptionOptions, Array.Empty<KeyValuePair<string, string>>(), ynDefault);
|
||||
}
|
||||
|
||||
private async Task AutoGenerateBarcodeAsync(string materialCode)
|
||||
{
|
||||
IsGenerating = true;
|
||||
try
|
||||
{
|
||||
var code = await _entryService.GenerateBarcodeAsync(materialCode);
|
||||
if (!string.IsNullOrEmpty(code) && Entry != null)
|
||||
{
|
||||
Entry.Barcode = code;
|
||||
Entry.BatchNo = code;
|
||||
RaisePropertyChanged(nameof(Entry));
|
||||
}
|
||||
}
|
||||
finally { IsGenerating = false; }
|
||||
}
|
||||
|
||||
public void InitializeForAdd()
|
||||
{
|
||||
_selectedMaterial = null;
|
||||
RaisePropertyChanged(nameof(SelectedMaterial));
|
||||
Entry = new MesXslRawMaterialEntry
|
||||
{
|
||||
TestResult = "0", TestStatus = "0", PrintFlag = "0",
|
||||
StockBalance = "0", IsSpecialAdoption = "0"
|
||||
};
|
||||
RaisePropertyChanged(nameof(IsAddMode));
|
||||
RaisePropertyChanged(nameof(DialogTitle));
|
||||
}
|
||||
|
||||
public void InitializeForEdit(MesXslRawMaterialEntry entry)
|
||||
{
|
||||
Entry = new MesXslRawMaterialEntry
|
||||
{
|
||||
Id = entry.Id, Barcode = entry.Barcode, BatchNo = entry.BatchNo, EntryTime = entry.EntryTime,
|
||||
WeightRecordId = entry.WeightRecordId, BillNo = entry.BillNo,
|
||||
MaterialId = entry.MaterialId, MaterialCode = entry.MaterialCode, MaterialName = entry.MaterialName,
|
||||
SupplyCustomer = entry.SupplyCustomer, SupplierId = entry.SupplierId, SupplierName = entry.SupplierName,
|
||||
ManufacturerMaterialName = entry.ManufacturerMaterialName,
|
||||
ShelfLife = entry.ShelfLife, TotalWeight = entry.TotalWeight, TotalPortions = entry.TotalPortions,
|
||||
PortionWeight = entry.PortionWeight, PortionPackages = entry.PortionPackages,
|
||||
TestResult = entry.TestResult, TestStatus = entry.TestStatus, PrintFlag = entry.PrintFlag,
|
||||
StockBalance = entry.StockBalance, WarehouseLocation = entry.WarehouseLocation,
|
||||
UnloadOperator = entry.UnloadOperator, IsSpecialAdoption = entry.IsSpecialAdoption,
|
||||
SpecialAdoptionOperator = entry.SpecialAdoptionOperator, SpecialAdoptionTime = entry.SpecialAdoptionTime,
|
||||
SpecialAdoptionReason = entry.SpecialAdoptionReason, Status = entry.Status, Remark = entry.Remark,
|
||||
TenantId = entry.TenantId,
|
||||
};
|
||||
|
||||
// 若物料列表已加载则直接回填,否则记录 pending 等加载完后回填
|
||||
if (MaterialOptions.Count > 0)
|
||||
{
|
||||
_selectedMaterial = MaterialOptions.FirstOrDefault(m =>
|
||||
string.Equals(m.Id, entry.MaterialId, StringComparison.OrdinalIgnoreCase));
|
||||
RaisePropertyChanged(nameof(SelectedMaterial));
|
||||
}
|
||||
else
|
||||
{
|
||||
_pendingMaterialId = entry.MaterialId;
|
||||
}
|
||||
|
||||
RaisePropertyChanged(nameof(IsAddMode));
|
||||
RaisePropertyChanged(nameof(DialogTitle));
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
if (Entry == null) return;
|
||||
try
|
||||
{
|
||||
bool ok;
|
||||
if (IsAddMode)
|
||||
{
|
||||
ok = await _entryService.AddAsync(Entry);
|
||||
if (ok) HandyControl.Controls.MessageBox.Success("新增成功!");
|
||||
else { HandyControl.Controls.MessageBox.Error("新增失败!"); return; }
|
||||
}
|
||||
else
|
||||
{
|
||||
ok = await _entryService.EditAsync(Entry);
|
||||
if (!ok) { HandyControl.Controls.MessageBox.Error("编辑失败!"); return; }
|
||||
}
|
||||
Result = ok;
|
||||
CloseAction?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Error($"操作失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
using HandyControl.Controls;
|
||||
using HandyControl.Tools.Extension;
|
||||
using Prism.Events;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Events;
|
||||
using YY.Admin.Core.Helper;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.Services.Service;
|
||||
using YY.Admin.Views.RawMaterialEntry;
|
||||
|
||||
namespace YY.Admin.ViewModels.RawMaterialEntry;
|
||||
|
||||
public class RawMaterialEntryListViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IRawMaterialEntryService _entryService;
|
||||
private readonly IJeecgDictSyncService _dictSyncService;
|
||||
private SubscriptionToken? _entryChangedToken;
|
||||
private SubscriptionToken? _syncConflictToken;
|
||||
|
||||
private ObservableCollection<MesXslRawMaterialEntry> _entries = new();
|
||||
public ObservableCollection<MesXslRawMaterialEntry> Entries
|
||||
{
|
||||
get => _entries;
|
||||
set => SetProperty(ref _entries, value);
|
||||
}
|
||||
|
||||
private long _total;
|
||||
public long Total { get => _total; set => SetProperty(ref _total, value); }
|
||||
|
||||
private int _pageNo = 1;
|
||||
public int PageNo { get => _pageNo; set => SetProperty(ref _pageNo, value); }
|
||||
|
||||
private int _pageSize = 20;
|
||||
public int PageSize { get => _pageSize; set => SetProperty(ref _pageSize, value); }
|
||||
|
||||
private string? _filterBarcode;
|
||||
public string? FilterBarcode { get => _filterBarcode; set => SetProperty(ref _filterBarcode, value); }
|
||||
|
||||
private string? _filterBatchNo;
|
||||
public string? FilterBatchNo { get => _filterBatchNo; set => SetProperty(ref _filterBatchNo, value); }
|
||||
|
||||
private string? _filterBillNo;
|
||||
public string? FilterBillNo { get => _filterBillNo; set => SetProperty(ref _filterBillNo, value); }
|
||||
|
||||
private string? _filterMaterialName;
|
||||
public string? FilterMaterialName { get => _filterMaterialName; set => SetProperty(ref _filterMaterialName, value); }
|
||||
|
||||
private string? _filterSupplierName;
|
||||
public string? FilterSupplierName { get => _filterSupplierName; set => SetProperty(ref _filterSupplierName, value); }
|
||||
|
||||
public DelegateCommand SearchCommand { get; }
|
||||
public DelegateCommand ResetCommand { get; }
|
||||
public DelegateCommand AddCommand { get; }
|
||||
public DelegateCommand<MesXslRawMaterialEntry> EditCommand { get; }
|
||||
public DelegateCommand<MesXslRawMaterialEntry> DeleteCommand { get; }
|
||||
public DelegateCommand PrevPageCommand { get; }
|
||||
public DelegateCommand NextPageCommand { get; }
|
||||
|
||||
public RawMaterialEntryListViewModel(
|
||||
IRawMaterialEntryService entryService,
|
||||
IJeecgDictSyncService dictSyncService,
|
||||
IContainerExtension container,
|
||||
IRegionManager regionManager) : base(container, regionManager)
|
||||
{
|
||||
_entryService = entryService;
|
||||
_dictSyncService = dictSyncService;
|
||||
|
||||
SearchCommand = new DelegateCommand(async () => { PageNo = 1; await LoadAsync(); });
|
||||
ResetCommand = new DelegateCommand(async () =>
|
||||
{
|
||||
FilterBarcode = null; FilterBatchNo = null; FilterBillNo = null;
|
||||
FilterMaterialName = null; FilterSupplierName = null;
|
||||
PageNo = 1;
|
||||
await LoadAsync();
|
||||
});
|
||||
AddCommand = new DelegateCommand(async () => await ShowAddDialogAsync());
|
||||
EditCommand = new DelegateCommand<MesXslRawMaterialEntry>(async e => await ShowEditDialogAsync(e));
|
||||
DeleteCommand = new DelegateCommand<MesXslRawMaterialEntry>(async e => await DeleteAsync(e));
|
||||
PrevPageCommand = new DelegateCommand(async () => { if (PageNo > 1) { PageNo--; await LoadAsync(); } });
|
||||
NextPageCommand = new DelegateCommand(async () => { if ((long)PageNo * PageSize < Total) { PageNo++; await LoadAsync(); } });
|
||||
|
||||
_entryChangedToken = _eventAggregator
|
||||
.GetEvent<RawMaterialEntryChangedEvent>()
|
||||
.Subscribe(async p => await OnEntryChangedAsync(p), ThreadOption.UIThread);
|
||||
|
||||
_syncConflictToken = _eventAggregator.GetEvent<SyncConflictEvent>()
|
||||
.Subscribe(OnSyncConflict, ThreadOption.UIThread);
|
||||
|
||||
_ = InitializeAsync();
|
||||
}
|
||||
|
||||
private async Task OnEntryChangedAsync(RawMaterialEntryChangedPayload payload)
|
||||
{
|
||||
if (payload.Action == "edit" && !string.IsNullOrWhiteSpace(payload.EntryId))
|
||||
await RefreshSingleAsync(payload.EntryId!);
|
||||
else
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
private async Task RefreshSingleAsync(string entryId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var updated = await _entryService.GetByIdAsync(entryId);
|
||||
if (updated == null) return;
|
||||
var idx = Entries.ToList().FindIndex(e => string.Equals(e.Id, entryId, StringComparison.OrdinalIgnoreCase));
|
||||
if (idx >= 0) Entries[idx] = updated;
|
||||
}
|
||||
catch (Exception ex) { Debug.WriteLine($"[原料入场] 单条刷新失败: {ex.Message}"); }
|
||||
}
|
||||
|
||||
private void OnSyncConflict(SyncConflictPayload payload)
|
||||
{
|
||||
if (!string.Equals(payload.EntityName, "原料入场", StringComparison.OrdinalIgnoreCase)) return;
|
||||
var parts = new List<string>();
|
||||
if (payload.PushedCount > 0) parts.Add($"已同步 {payload.PushedCount} 条本地改动到服务器");
|
||||
if (payload.NewRecordsPushed > 0) parts.Add($"已上传 {payload.NewRecordsPushed} 条本地新增记录");
|
||||
if (payload.ConflictCount > 0) parts.Add($"{payload.ConflictCount} 条记录与服务器版本冲突,已保留服务器版本");
|
||||
if (parts.Count == 0) return;
|
||||
var message = string.Join("\n", parts);
|
||||
if (payload.ConflictCount > 0) Growl.Warning(message); else Growl.Success(message);
|
||||
}
|
||||
|
||||
private async Task InitializeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await UIHelper.WaitForRenderAsync();
|
||||
await LoadAsync();
|
||||
}
|
||||
catch (Exception ex) { Debug.WriteLine($"原料入场列表初始化失败: {ex.Message}"); }
|
||||
}
|
||||
|
||||
public async Task LoadAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
var result = await _entryService.PageAsync(PageNo, PageSize,
|
||||
FilterBarcode, FilterBatchNo, FilterBillNo, FilterMaterialName, FilterSupplierName);
|
||||
Entries = new ObservableCollection<MesXslRawMaterialEntry>(result.Records);
|
||||
Total = result.Total;
|
||||
}
|
||||
catch (Exception ex) { Growl.Error($"加载原料入场记录失败:{ex.Message}"); }
|
||||
finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
private async Task ShowAddDialogAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await HandyControl.Controls.Dialog.Show<RawMaterialEntryEditDialogView>()
|
||||
.Initialize<RawMaterialEntryEditDialogViewModel>(vm => vm.InitializeForAdd())
|
||||
.GetResultAsync<bool>();
|
||||
if (result) await LoadAsync();
|
||||
}
|
||||
catch (Exception ex) { Growl.Error($"打开新增对话框失败:{ex.Message}"); }
|
||||
}
|
||||
|
||||
private async Task ShowEditDialogAsync(MesXslRawMaterialEntry entry)
|
||||
{
|
||||
if (entry == null) return;
|
||||
try
|
||||
{
|
||||
var result = await HandyControl.Controls.Dialog.Show<RawMaterialEntryEditDialogView>()
|
||||
.Initialize<RawMaterialEntryEditDialogViewModel>(vm => vm.InitializeForEdit(entry))
|
||||
.GetResultAsync<bool>();
|
||||
if (result) await LoadAsync();
|
||||
}
|
||||
catch (Exception ex) { Growl.Error($"打开编辑对话框失败:{ex.Message}"); }
|
||||
}
|
||||
|
||||
private async Task DeleteAsync(MesXslRawMaterialEntry entry)
|
||||
{
|
||||
if (entry?.Id == null) return;
|
||||
var confirm = System.Windows.MessageBox.Show(
|
||||
$"确定删除该原料入场记录(条码:{entry.Barcode})?此操作不可恢复!",
|
||||
"确认删除", MessageBoxButton.OKCancel, MessageBoxImage.Question);
|
||||
if (confirm != System.Windows.MessageBoxResult.OK) return;
|
||||
|
||||
var ok = await _entryService.DeleteAsync(entry.Id);
|
||||
if (ok) { Growl.Success("删除成功!"); await LoadAsync(); }
|
||||
else Growl.Error("删除失败!");
|
||||
}
|
||||
|
||||
protected override void CleanUp()
|
||||
{
|
||||
base.CleanUp();
|
||||
if (_entryChangedToken != null)
|
||||
{
|
||||
_eventAggregator.GetEvent<RawMaterialEntryChangedEvent>().Unsubscribe(_entryChangedToken);
|
||||
_entryChangedToken = null;
|
||||
}
|
||||
if (_syncConflictToken != null)
|
||||
{
|
||||
_eventAggregator.GetEvent<SyncConflictEvent>().Unsubscribe(_syncConflictToken);
|
||||
_syncConflictToken = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
<UserControl x:Class="YY.Admin.Views.RawMaterialEntry.RawMaterialEntryEditDialogView"
|
||||
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:prism="http://prismlibrary.com/"
|
||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||
mc:Ignorable="d"
|
||||
Width="1280"
|
||||
MinWidth="1100"
|
||||
MinHeight="500">
|
||||
|
||||
<Grid Background="{DynamicResource ThirdlyRegionBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<hc:SimplePanel Margin="24,20,24,12">
|
||||
<TextBlock FontSize="18" Foreground="{DynamicResource PrimaryTextBrush}" Text="{Binding DialogTitle}" HorizontalAlignment="Left" VerticalAlignment="Center"/>
|
||||
<Button Width="22" Height="22" Command="hc:ControlCommands.Close" Style="{StaticResource ButtonIcon}"
|
||||
Foreground="{DynamicResource PrimaryBrush}" hc:IconElement.Geometry="{StaticResource ErrorGeometry}"
|
||||
Padding="0" HorizontalAlignment="Right" VerticalAlignment="Center"/>
|
||||
</hc:SimplePanel>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<hc:ScrollViewer Grid.Row="1" IsInertiaEnabled="True">
|
||||
<StackPanel x:Name="RootPanel" Margin="24,8,24,8">
|
||||
<hc:Row Gutter="16">
|
||||
|
||||
<!-- 密炼物料选择(搜索下拉) -->
|
||||
<hc:Col Span="16">
|
||||
<DockPanel Margin="0,0,0,8" LastChildFill="True">
|
||||
<TextBlock DockPanel.Dock="Left" Text="密炼物料" Width="80"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"/>
|
||||
<hc:ComboBox ItemsSource="{Binding MaterialOptions}"
|
||||
SelectedItem="{Binding SelectedMaterial}"
|
||||
IsEditable="True"
|
||||
IsTextSearchEnabled="True"
|
||||
TextSearch.TextPath="MaterialCode"
|
||||
Height="34"
|
||||
hc:InfoElement.Placeholder="输入物料编码搜索">
|
||||
<hc:ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock>
|
||||
<Run Text="{Binding MaterialCode, Mode=OneWay}"/>
|
||||
<Run Text=" — "/>
|
||||
<Run Text="{Binding MaterialName, Mode=OneWay}"/>
|
||||
</TextBlock>
|
||||
</DataTemplate>
|
||||
</hc:ComboBox.ItemTemplate>
|
||||
</hc:ComboBox>
|
||||
</DockPanel>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 物料名称(只读回显) -->
|
||||
<hc:Col Span="8">
|
||||
<hc:TextBox Text="{Binding Entry.MaterialName}"
|
||||
Height="34"
|
||||
IsReadOnly="True"
|
||||
hc:InfoElement.Title="物料名称"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 条码 -->
|
||||
<hc:Col Span="8">
|
||||
<hc:TextBox Text="{Binding Entry.Barcode, UpdateSourceTrigger=PropertyChanged}"
|
||||
Height="34"
|
||||
hc:InfoElement.Title="条码"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入条码"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 批次号 -->
|
||||
<hc:Col Span="8">
|
||||
<hc:TextBox Text="{Binding Entry.BatchNo, UpdateSourceTrigger=PropertyChanged}"
|
||||
Height="34"
|
||||
hc:InfoElement.Title="批次号"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入批次号"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 入场时间 -->
|
||||
<hc:Col Span="8">
|
||||
<DockPanel Margin="0,0,0,8" LastChildFill="True">
|
||||
<TextBlock DockPanel.Dock="Left" Text="入场时间" Width="80"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"/>
|
||||
<hc:DateTimePicker SelectedDateTime="{Binding Entry.EntryTime}"
|
||||
hc:InfoElement.Placeholder="请选择入场时间"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Height="34"/>
|
||||
</DockPanel>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 榜单号 -->
|
||||
<hc:Col Span="8">
|
||||
<hc:TextBox Text="{Binding Entry.BillNo, UpdateSourceTrigger=PropertyChanged}"
|
||||
Height="34"
|
||||
hc:InfoElement.Title="榜单号"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入榜单号"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 供料客户 -->
|
||||
<hc:Col Span="8">
|
||||
<hc:TextBox Text="{Binding Entry.SupplyCustomer, UpdateSourceTrigger=PropertyChanged}"
|
||||
Height="34"
|
||||
hc:InfoElement.Title="供料客户"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入供料客户"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 供应商名称 -->
|
||||
<hc:Col Span="8">
|
||||
<hc:TextBox Text="{Binding Entry.SupplierName, UpdateSourceTrigger=PropertyChanged}"
|
||||
Height="34"
|
||||
hc:InfoElement.Title="供应商名称"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入供应商名称"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 厂家物料名称 -->
|
||||
<hc:Col Span="8">
|
||||
<hc:TextBox Text="{Binding Entry.ManufacturerMaterialName, UpdateSourceTrigger=PropertyChanged}"
|
||||
Height="34"
|
||||
hc:InfoElement.Title="厂家物料名称"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入厂家物料名称"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 保质期 -->
|
||||
<hc:Col Span="8">
|
||||
<hc:TextBox Text="{Binding Entry.ShelfLife, UpdateSourceTrigger=PropertyChanged}"
|
||||
Height="34"
|
||||
hc:InfoElement.Title="保质期"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入保质期"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 总重(KG) -->
|
||||
<hc:Col Span="8">
|
||||
<hc:NumericUpDown Value="{Binding Entry.TotalWeight}"
|
||||
Minimum="0"
|
||||
DecimalPlaces="2"
|
||||
Height="34"
|
||||
Style="{StaticResource NumericUpDownPlus}"
|
||||
hc:InfoElement.Title="总重(KG)"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入总重"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 总份数 -->
|
||||
<hc:Col Span="8">
|
||||
<hc:NumericUpDown Value="{Binding Entry.TotalPortions}"
|
||||
Minimum="0"
|
||||
DecimalPlaces="0"
|
||||
Height="34"
|
||||
Style="{StaticResource NumericUpDownPlus}"
|
||||
hc:InfoElement.Title="总份数"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入总份数"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 每份总重(KG) -->
|
||||
<hc:Col Span="8">
|
||||
<hc:NumericUpDown Value="{Binding Entry.PortionWeight}"
|
||||
Minimum="0"
|
||||
DecimalPlaces="2"
|
||||
Height="34"
|
||||
Style="{StaticResource NumericUpDownPlus}"
|
||||
hc:InfoElement.Title="每份总重(KG)"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入每份总重"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 每份包数 -->
|
||||
<hc:Col Span="8">
|
||||
<hc:NumericUpDown Value="{Binding Entry.PortionPackages}"
|
||||
Minimum="0"
|
||||
DecimalPlaces="0"
|
||||
Height="34"
|
||||
Style="{StaticResource NumericUpDownPlus}"
|
||||
hc:InfoElement.Title="每份包数"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入每份包数"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 检测结果 -->
|
||||
<hc:Col Span="8">
|
||||
<hc:ComboBox SelectedValuePath="Value"
|
||||
DisplayMemberPath="Key"
|
||||
ItemsSource="{Binding TestResultOptions}"
|
||||
SelectedValue="{Binding Entry.TestResult}"
|
||||
Height="34"
|
||||
hc:InfoElement.Title="检测结果"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请选择检测结果"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 检测状态 -->
|
||||
<hc:Col Span="8">
|
||||
<hc:ComboBox SelectedValuePath="Value"
|
||||
DisplayMemberPath="Key"
|
||||
ItemsSource="{Binding TestStatusOptions}"
|
||||
SelectedValue="{Binding Entry.TestStatus}"
|
||||
Height="34"
|
||||
hc:InfoElement.Title="检测状态"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请选择检测状态"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 打印标记 -->
|
||||
<hc:Col Span="8">
|
||||
<hc:ComboBox SelectedValuePath="Value"
|
||||
DisplayMemberPath="Key"
|
||||
ItemsSource="{Binding PrintFlagOptions}"
|
||||
SelectedValue="{Binding Entry.PrintFlag}"
|
||||
Height="34"
|
||||
hc:InfoElement.Title="打印标记"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请选择打印标记"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 入库结存 -->
|
||||
<hc:Col Span="8">
|
||||
<hc:ComboBox SelectedValuePath="Value"
|
||||
DisplayMemberPath="Key"
|
||||
ItemsSource="{Binding StockBalanceOptions}"
|
||||
SelectedValue="{Binding Entry.StockBalance}"
|
||||
Height="34"
|
||||
hc:InfoElement.Title="入库结存"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请选择入库结存"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 库位 -->
|
||||
<hc:Col Span="8">
|
||||
<hc:TextBox Text="{Binding Entry.WarehouseLocation, UpdateSourceTrigger=PropertyChanged}"
|
||||
Height="34"
|
||||
hc:InfoElement.Title="库位"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入库位"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 卸货人 -->
|
||||
<hc:Col Span="8">
|
||||
<hc:TextBox Text="{Binding Entry.UnloadOperator, UpdateSourceTrigger=PropertyChanged}"
|
||||
Height="34"
|
||||
hc:InfoElement.Title="卸货人"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入卸货人"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 是否特采 -->
|
||||
<hc:Col Span="8">
|
||||
<hc:ComboBox SelectedValuePath="Value"
|
||||
DisplayMemberPath="Key"
|
||||
ItemsSource="{Binding IsSpecialAdoptionOptions}"
|
||||
SelectedValue="{Binding Entry.IsSpecialAdoption}"
|
||||
Height="34"
|
||||
hc:InfoElement.Title="是否特采"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请选择是否特采"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 特采操作人 -->
|
||||
<hc:Col Span="8">
|
||||
<hc:TextBox Text="{Binding Entry.SpecialAdoptionOperator, UpdateSourceTrigger=PropertyChanged}"
|
||||
Height="34"
|
||||
hc:InfoElement.Title="特采操作人"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入特采操作人"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 特采时间 -->
|
||||
<hc:Col Span="8">
|
||||
<DockPanel Margin="0,0,0,8" LastChildFill="True">
|
||||
<TextBlock DockPanel.Dock="Left" Text="特采时间" Width="80"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"/>
|
||||
<hc:DateTimePicker SelectedDateTime="{Binding Entry.SpecialAdoptionTime}"
|
||||
hc:InfoElement.Placeholder="请选择特采时间"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Height="34"/>
|
||||
</DockPanel>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 状态 -->
|
||||
<hc:Col Span="8">
|
||||
<hc:ComboBox SelectedValuePath="Value"
|
||||
DisplayMemberPath="Key"
|
||||
ItemsSource="{Binding StatusOptions}"
|
||||
SelectedValue="{Binding Entry.Status}"
|
||||
Height="34"
|
||||
hc:InfoElement.Title="状态"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请选择状态"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 特采原因(全宽) -->
|
||||
<hc:Col Span="24">
|
||||
<hc:TextBox Text="{Binding Entry.SpecialAdoptionReason, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Title="特采原因"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入特采原因"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
TextWrapping="Wrap"
|
||||
AcceptsReturn="True"
|
||||
HorizontalAlignment="Stretch"
|
||||
Height="64"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 备注(全宽) -->
|
||||
<hc:Col Span="24">
|
||||
<hc:TextBox Text="{Binding Entry.Remark, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Title="备注"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入备注"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
TextWrapping="Wrap"
|
||||
AcceptsReturn="True"
|
||||
HorizontalAlignment="Stretch"
|
||||
Height="64"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
|
||||
</hc:Row>
|
||||
</StackPanel>
|
||||
</hc:ScrollViewer>
|
||||
|
||||
<!-- 按钮区域 -->
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,12,0,20">
|
||||
<Button Content="取消" Command="{Binding CancelCommand}" Style="{StaticResource ButtonDefault}" Margin="0,0,15,0" Width="100"/>
|
||||
<Button Content="确定" Command="{Binding SaveCommand}" Style="{StaticResource ButtonPrimary}" Width="100"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace YY.Admin.Views.RawMaterialEntry;
|
||||
|
||||
public partial class RawMaterialEntryEditDialogView : UserControl
|
||||
{
|
||||
public RawMaterialEntryEditDialogView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<UserControl x:Class="YY.Admin.Views.RawMaterialEntry.RawMaterialEntryListView"
|
||||
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=4, Xl=4}">
|
||||
<hc:TextBox Text="{Binding FilterBarcode, 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=4, Xl=4}">
|
||||
<hc:TextBox Text="{Binding FilterBatchNo, 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=4, Xl=4}">
|
||||
<hc:TextBox Text="{Binding FilterBillNo, 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=4, Xl=4}">
|
||||
<hc:TextBox Text="{Binding FilterMaterialName, 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=4, Xl=4}">
|
||||
<hc:TextBox Text="{Binding FilterSupplierName, UpdateSourceTrigger=PropertyChanged}"
|
||||
Margin="0 0 10 10"
|
||||
hc:InfoElement.Title="供应商"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.TitleWidth="65"
|
||||
hc:InfoElement.Placeholder="请输入供应商名称"
|
||||
hc:InfoElement.ShowClearButton="True"/>
|
||||
</hc:Col>
|
||||
</hc:Row>
|
||||
</Border>
|
||||
|
||||
<!-- 操作工具栏 -->
|
||||
<Border Grid.Row="1" Margin="0,10">
|
||||
<hc:UniformSpacingPanel Spacing="10">
|
||||
<Button Style="{StaticResource ButtonPrimary}" Command="{Binding SearchCommand}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="Search"/>
|
||||
<TextBlock Text="搜索" Style="{StaticResource IconButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ButtonDefault}" Command="{Binding ResetCommand}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="Refresh"/>
|
||||
<TextBlock Text="重置" Style="{StaticResource IconButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ButtonSuccess}" Command="{Binding AddCommand}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="Plus"/>
|
||||
<TextBlock Text="新增" Style="{StaticResource IconButtonStyle}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</hc:UniformSpacingPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<DataGrid Grid.Row="2"
|
||||
ItemsSource="{Binding Entries}"
|
||||
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 Barcode}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="140"/>
|
||||
<DataGridTextColumn Header="批次号" Binding="{Binding BatchNo}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="120"/>
|
||||
<DataGridTextColumn Header="入场时间" Binding="{Binding EntryTime, StringFormat='yyyy-MM-dd HH:mm'}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="140"/>
|
||||
<DataGridTextColumn Header="榜单号" Binding="{Binding BillNo}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="130"/>
|
||||
<DataGridTextColumn Header="物料名称" Binding="{Binding MaterialName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="140"/>
|
||||
<DataGridTextColumn Header="供料客户" Binding="{Binding SupplyCustomer}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="120"/>
|
||||
<DataGridTextColumn Header="供应商名称" Binding="{Binding SupplierName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="140"/>
|
||||
<DataGridTextColumn Header="总重(KG)" Binding="{Binding TotalWeight, StringFormat=N2}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="90"/>
|
||||
<DataGridTextColumn Header="总份数" Binding="{Binding TotalPortions}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="70"/>
|
||||
<DataGridTextColumn Header="检测结果" Binding="{Binding TestResultText}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="85"/>
|
||||
<DataGridTextColumn Header="检测状态" Binding="{Binding TestStatusText}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="85"/>
|
||||
<DataGridTextColumn Header="打印标记" Binding="{Binding PrintFlagText}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="85"/>
|
||||
<DataGridTextColumn Header="入库结存" Binding="{Binding StockBalanceText}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="75"/>
|
||||
<DataGridTextColumn Header="库位" Binding="{Binding WarehouseLocation}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="90"/>
|
||||
<DataGridTextColumn Header="卸货人" Binding="{Binding UnloadOperator}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="80"/>
|
||||
<DataGridTextColumn Header="是否特采" Binding="{Binding IsSpecialAdoptionText}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="75"/>
|
||||
<DataGridTextColumn Header="创建时间" Binding="{Binding CreateTime, StringFormat='yyyy-MM-dd HH:mm'}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="140"/>
|
||||
<DataGridTemplateColumn Header="操作" Width="140" CellStyle="{StaticResource CusDataGridCellStyle}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<hc:UniformSpacingPanel Spacing="6" HorizontalAlignment="Center">
|
||||
<Button Content="编辑" Command="{Binding DataContext.EditCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||
CommandParameter="{Binding}" Style="{StaticResource ButtonInfo}" Padding="8,2" FontSize="12"/>
|
||||
<Button Content="删除" Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||
CommandParameter="{Binding}" Style="{StaticResource ButtonDanger}" Padding="8,2" FontSize="12"/>
|
||||
</hc:UniformSpacingPanel>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<!-- 分页 -->
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
|
||||
<TextBlock Text="{Binding Total, StringFormat=共 {0} 条}" VerticalAlignment="Center" Margin="0,0,16,0"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"/>
|
||||
<Button Content="上一页" Command="{Binding PrevPageCommand}" Style="{StaticResource ButtonDefault}" Margin="0,0,4,0" Width="80"/>
|
||||
<TextBlock Text="{Binding PageNo, StringFormat=第 {0} 页}" VerticalAlignment="Center" Margin="8,0"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"/>
|
||||
<Button Content="下一页" Command="{Binding NextPageCommand}" Style="{StaticResource ButtonDefault}" Width="80"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace YY.Admin.Views.RawMaterialEntry;
|
||||
|
||||
public partial class RawMaterialEntryListView : UserControl
|
||||
{
|
||||
public RawMaterialEntryListView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user