新增磅单管理功能,支持免密接口和数据同步,更新相关控制器、实体和服务,优化权限管理,确保系统的灵活性和可扩展性。
This commit is contained in:
@@ -201,6 +201,8 @@ public class ShiroConfig {
|
||||
filterChainDefinitionMap.put("/xslmes/mesXslCustomer/anon/**", "anon");
|
||||
// MES供应商管理免密接口(供桌面端调用)
|
||||
filterChainDefinitionMap.put("/xslmes/mesXslSupplier/anon/**", "anon");
|
||||
// MES磅单管理免密接口(供桌面端调用)
|
||||
filterChainDefinitionMap.put("/xslmes/mesXslWeightRecord/anon/**", "anon");
|
||||
// 桌面端用户反同步批量上报(Outbox -> /sys/sync/batch)
|
||||
filterChainDefinitionMap.put("/sys/sync/batch", "anon");
|
||||
|
||||
|
||||
@@ -16,9 +16,11 @@ import org.jeecg.modules.xslmes.constant.MesXslCustomerBizStatus;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslCustomer;
|
||||
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.IMesXslSupplierService;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslVehicleService;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslWeightRecordService;
|
||||
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -42,6 +44,7 @@ public class MesXslDesktopAnonController {
|
||||
private final IMesXslVehicleService vehicleService;
|
||||
private final IMesXslCustomerService customerService;
|
||||
private final IMesXslSupplierService supplierService;
|
||||
private final IMesXslWeightRecordService weightRecordService;
|
||||
private final MesXslStompNotifyService stompNotify;
|
||||
|
||||
// ═══════════════════════════ 车辆管理 ═══════════════════════════
|
||||
@@ -342,8 +345,97 @@ public class MesXslDesktopAnonController {
|
||||
return ok ? Result.OK("操作成功") : Result.error("操作失败");
|
||||
}
|
||||
|
||||
// ═══════════════════════════ 磅单管理 ═══════════════════════════
|
||||
|
||||
@Operation(summary = "磅单-免密分页列表查询")
|
||||
@GetMapping("/xslmes/mesXslWeightRecord/anon/list")
|
||||
public Result<IPage<MesXslWeightRecord>> weightRecordAnonList(
|
||||
MesXslWeightRecord mesXslWeightRecord,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<MesXslWeightRecord> qw = QueryGenerator.initQueryWrapper(mesXslWeightRecord, req.getParameterMap());
|
||||
qw.orderByDesc("create_time");
|
||||
IPage<MesXslWeightRecord> page = weightRecordService.page(new Page<>(pageNo, pageSize), qw);
|
||||
return Result.OK(page);
|
||||
}
|
||||
|
||||
@Operation(summary = "磅单-免密通过id查询")
|
||||
@GetMapping("/xslmes/mesXslWeightRecord/anon/queryById")
|
||||
public Result<MesXslWeightRecord> weightRecordAnonQueryById(@RequestParam(name = "id") String id) {
|
||||
MesXslWeightRecord entity = weightRecordService.getById(id);
|
||||
return entity != null ? Result.OK(entity) : Result.error("未找到对应数据");
|
||||
}
|
||||
|
||||
@Operation(summary = "磅单-免密添加")
|
||||
@PostMapping("/xslmes/mesXslWeightRecord/anon/add")
|
||||
public Result<String> weightRecordAnonAdd(@RequestBody MesXslWeightRecord mesXslWeightRecord) {
|
||||
if (oConvertUtils.isEmpty(mesXslWeightRecord.getPlateNumber())) {
|
||||
return Result.error("车牌号不能为空");
|
||||
}
|
||||
// 净重自动计算
|
||||
if (mesXslWeightRecord.getGrossWeight() != null && mesXslWeightRecord.getTareWeight() != null) {
|
||||
mesXslWeightRecord.setNetWeight(
|
||||
mesXslWeightRecord.getGrossWeight().subtract(mesXslWeightRecord.getTareWeight()));
|
||||
}
|
||||
applyWeightBillType(mesXslWeightRecord);
|
||||
weightRecordService.save(mesXslWeightRecord);
|
||||
stompNotify.publishWeightRecordChanged("add", mesXslWeightRecord.getId());
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
@Operation(summary = "磅单-免密编辑")
|
||||
@RequestMapping(value = "/xslmes/mesXslWeightRecord/anon/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
public Result<String> weightRecordAnonEdit(@RequestBody MesXslWeightRecord mesXslWeightRecord) {
|
||||
if (oConvertUtils.isEmpty(mesXslWeightRecord.getId())) {
|
||||
return Result.error("主键不能为空");
|
||||
}
|
||||
// 净重自动计算
|
||||
if (mesXslWeightRecord.getGrossWeight() != null && mesXslWeightRecord.getTareWeight() != null) {
|
||||
mesXslWeightRecord.setNetWeight(
|
||||
mesXslWeightRecord.getGrossWeight().subtract(mesXslWeightRecord.getTareWeight()));
|
||||
}
|
||||
applyWeightBillType(mesXslWeightRecord);
|
||||
boolean ok = weightRecordService.updateById(mesXslWeightRecord);
|
||||
if (!ok) {
|
||||
return Result.error("数据已被他人修改,请刷新后重试");
|
||||
}
|
||||
stompNotify.publishWeightRecordChanged("edit", mesXslWeightRecord.getId());
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
@Operation(summary = "磅单-免密删除")
|
||||
@DeleteMapping("/xslmes/mesXslWeightRecord/anon/delete")
|
||||
public Result<String> weightRecordAnonDelete(@RequestParam(name = "id") String id) {
|
||||
weightRecordService.removeById(id);
|
||||
stompNotify.publishWeightRecordChanged("delete", id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
@Operation(summary = "磅单-免密批量删除")
|
||||
@DeleteMapping("/xslmes/mesXslWeightRecord/anon/deleteBatch")
|
||||
public Result<String> weightRecordAnonDeleteBatch(@RequestParam(name = "ids") String ids) {
|
||||
weightRecordService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
stompNotify.publishWeightRecordChanged("batchDelete", ids);
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
// ─────────────────────────── 车辆私有辅助 ────────────────────────────
|
||||
|
||||
private void applyWeightBillType(MesXslWeightRecord record) {
|
||||
if (record.getGrossWeight() != null && record.getTareWeight() != null) {
|
||||
record.setBillType("2");
|
||||
return;
|
||||
}
|
||||
if (record.getGrossWeight() != null) {
|
||||
record.setBillType("1");
|
||||
return;
|
||||
}
|
||||
if (record.getTareWeight() != null) {
|
||||
record.setBillType("3");
|
||||
}
|
||||
}
|
||||
|
||||
private void applyVehicleBelong(MesXslVehicle v) {
|
||||
if (oConvertUtils.isEmpty(v.getVehicleBelong())) {
|
||||
return;
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslWeightRecord;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslWeightRecordService;
|
||||
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
@@ -36,6 +37,8 @@ public class MesXslWeightRecordController extends JeecgController<MesXslWeightRe
|
||||
|
||||
@Autowired
|
||||
private IMesXslWeightRecordService mesXslWeightRecordService;
|
||||
@Autowired
|
||||
private MesXslStompNotifyService stompNotifyService;
|
||||
|
||||
@Operation(summary = "地磅数据记录-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
@@ -61,8 +64,10 @@ public class MesXslWeightRecordController extends JeecgController<MesXslWeightRe
|
||||
String seq = String.format("%03d", new Random().nextInt(1000));
|
||||
mesXslWeightRecord.setBillNo("BDH-" + dateStr + seq);
|
||||
}
|
||||
computeBillType(mesXslWeightRecord);
|
||||
computeNetWeight(mesXslWeightRecord);
|
||||
mesXslWeightRecordService.save(mesXslWeightRecord);
|
||||
stompNotifyService.publishWeightRecordChanged("add", mesXslWeightRecord.getId());
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
@@ -71,8 +76,10 @@ public class MesXslWeightRecordController extends JeecgController<MesXslWeightRe
|
||||
@RequiresPermissions("xslmes:mes_xsl_weight_record:edit")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody MesXslWeightRecord mesXslWeightRecord) {
|
||||
computeBillType(mesXslWeightRecord);
|
||||
computeNetWeight(mesXslWeightRecord);
|
||||
mesXslWeightRecordService.updateById(mesXslWeightRecord);
|
||||
stompNotifyService.publishWeightRecordChanged("edit", mesXslWeightRecord.getId());
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
@@ -82,6 +89,7 @@ public class MesXslWeightRecordController extends JeecgController<MesXslWeightRe
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
||||
mesXslWeightRecordService.removeById(id);
|
||||
stompNotifyService.publishWeightRecordChanged("delete", id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
@@ -91,6 +99,7 @@ public class MesXslWeightRecordController extends JeecgController<MesXslWeightRe
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
|
||||
this.mesXslWeightRecordService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
stompNotifyService.publishWeightRecordChanged("deleteBatch", null);
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
@@ -124,4 +133,18 @@ public class MesXslWeightRecordController extends JeecgController<MesXslWeightRe
|
||||
record.setNetWeight(net.compareTo(BigDecimal.ZERO) >= 0 ? net : BigDecimal.ZERO);
|
||||
}
|
||||
}
|
||||
|
||||
private void computeBillType(MesXslWeightRecord record) {
|
||||
if (record.getGrossWeight() != null && record.getTareWeight() != null) {
|
||||
record.setBillType("2");
|
||||
return;
|
||||
}
|
||||
if (record.getGrossWeight() != null) {
|
||||
record.setBillType("1");
|
||||
return;
|
||||
}
|
||||
if (record.getTareWeight() != null) {
|
||||
record.setBillType("3");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,11 @@ public class MesXslWeightRecord extends JeecgEntity implements Serializable {
|
||||
@Schema(description = "手机号")
|
||||
private String driverPhone;
|
||||
|
||||
@Excel(name = "单据类型", width = 12, dicCode = "xslmes_weight_bill_type")
|
||||
@Dict(dicCode = "xslmes_weight_bill_type")
|
||||
@Schema(description = "单据类型:1已称毛重 2称重完成")
|
||||
private String billType;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
private Integer tenantId;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,11 @@ public class MesXslStompNotifyService {
|
||||
publish("/topic/sync/mes-suppliers", "MES_SUPPLIER_CHANGED", "supplierId", supplierId, action);
|
||||
}
|
||||
|
||||
/** 广播磅单数据变更事件到 /topic/sync/mes-weight-records */
|
||||
public void publishWeightRecordChanged(String action, String weightRecordId) {
|
||||
publish("/topic/sync/mes-weight-records", "MES_WEIGHT_RECORD_CHANGED", "weightRecordId", weightRecordId, action);
|
||||
}
|
||||
|
||||
// ─────────────────────────── 私有辅助 ────────────────────────────
|
||||
|
||||
private void publish(String topic, String cmd, String idKey, String idValue, String action) {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
-- 磅单新增单据类型字段 + 字典(幂等)
|
||||
|
||||
-- 1) 表结构:单据类型(1已称毛重 2称重完成 3已称皮重)
|
||||
SET @bill_type_exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'mes_xsl_weight_record'
|
||||
AND COLUMN_NAME = 'bill_type'
|
||||
);
|
||||
SET @ddl_sql := IF(
|
||||
@bill_type_exists = 0,
|
||||
'ALTER TABLE `mes_xsl_weight_record` ADD COLUMN `bill_type` varchar(10) DEFAULT NULL COMMENT ''单据类型(字典xslmes_weight_bill_type:1已称毛重 2称重完成 3已称皮重)'' AFTER `driver_phone`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt_bill_type FROM @ddl_sql;
|
||||
EXECUTE stmt_bill_type;
|
||||
DEALLOCATE PREPARE stmt_bill_type;
|
||||
|
||||
-- 2) 初始化历史数据(按当前重量信息推导)
|
||||
UPDATE `mes_xsl_weight_record`
|
||||
SET `bill_type` = CASE
|
||||
WHEN `gross_weight` IS NOT NULL AND `tare_weight` IS NOT NULL THEN '2'
|
||||
WHEN `gross_weight` IS NOT NULL THEN '1'
|
||||
WHEN `tare_weight` IS NOT NULL THEN '3'
|
||||
ELSE `bill_type`
|
||||
END
|
||||
WHERE `bill_type` IS NULL OR `bill_type` = '';
|
||||
|
||||
-- 3) 字典主表
|
||||
INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
|
||||
SELECT REPLACE(UUID(), '-', ''), 'MES磅单单据类型', 'xslmes_weight_bill_type', '磅单当前状态:已称毛重/称重完成/已称皮重', 0, 'admin', NOW(), 0, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_weight_bill_type' AND `del_flag` = 0);
|
||||
|
||||
-- 4) 字典项:已称毛重
|
||||
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
|
||||
SELECT REPLACE(UUID(), '-', ''), d.id, '已称毛重', '1', 1, 1, 'admin', NOW()
|
||||
FROM `sys_dict` d
|
||||
WHERE d.`dict_code` = 'xslmes_weight_bill_type'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `sys_dict_item` i
|
||||
WHERE i.`dict_id` = d.`id` AND i.`item_value` = '1'
|
||||
);
|
||||
|
||||
-- 5) 字典项:称重完成
|
||||
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
|
||||
SELECT REPLACE(UUID(), '-', ''), d.id, '称重完成', '2', 2, 1, 'admin', NOW()
|
||||
FROM `sys_dict` d
|
||||
WHERE d.`dict_code` = 'xslmes_weight_bill_type'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `sys_dict_item` i
|
||||
WHERE i.`dict_id` = d.`id` AND i.`item_value` = '2'
|
||||
);
|
||||
|
||||
-- 6) 字典项:已称皮重
|
||||
INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`)
|
||||
SELECT REPLACE(UUID(), '-', ''), d.id, '已称皮重', '3', 3, 1, 'admin', NOW()
|
||||
FROM `sys_dict` d
|
||||
WHERE d.`dict_code` = 'xslmes_weight_bill_type'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `sys_dict_item` i
|
||||
WHERE i.`dict_id` = d.`id` AND i.`item_value` = '3'
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
using Prism.Events;
|
||||
|
||||
namespace YY.Admin.Core.Events;
|
||||
|
||||
public class MesXslWeightRecordChangedPayload
|
||||
{
|
||||
public string Action { get; set; } = string.Empty; // add/edit/delete/reconnect/pull
|
||||
public string? WeightRecordId { get; set; }
|
||||
}
|
||||
|
||||
public class MesXslWeightRecordChangedEvent : PubSubEvent<MesXslWeightRecordChangedPayload> { }
|
||||
@@ -0,0 +1,23 @@
|
||||
using YY.Admin.Core.Entity;
|
||||
|
||||
namespace YY.Admin.Core.Services;
|
||||
|
||||
public interface IWeightRecordService
|
||||
{
|
||||
Task<WeightRecordPageResult> PageAsync(
|
||||
int pageNo, int pageSize,
|
||||
string? filterBillNo = null,
|
||||
string? filterPlateNumber = null,
|
||||
string? filterInoutDirection = null,
|
||||
string? filterGoodsName = null,
|
||||
string? filterDriverName = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
Task<MesXslWeightRecord?> GetByIdAsync(string id, CancellationToken ct = default);
|
||||
Task<bool> AddAsync(MesXslWeightRecord entity, CancellationToken ct = default);
|
||||
Task<bool> EditAsync(MesXslWeightRecord entity, CancellationToken ct = default);
|
||||
Task<bool> DeleteAsync(string id, CancellationToken ct = default);
|
||||
Task<bool> DeleteBatchAsync(string ids, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public record WeightRecordPageResult(List<MesXslWeightRecord> Records, long Total, int PageNo, int PageSize);
|
||||
61
yy-admin-master/YY.Admin.Core/Entity/MesXslWeightRecord.cs
Normal file
61
yy-admin-master/YY.Admin.Core/Entity/MesXslWeightRecord.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
namespace YY.Admin.Core.Entity;
|
||||
|
||||
public class MesXslWeightRecord
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
|
||||
/// <summary>磅单号</summary>
|
||||
public string? BillNo { get; set; }
|
||||
|
||||
/// <summary>称重日期</summary>
|
||||
public DateTime? WeighDate { get; set; }
|
||||
|
||||
/// <summary>进出方向:1进厂 2出厂</summary>
|
||||
public string? InoutDirection { get; set; }
|
||||
|
||||
/// <summary>车辆档案ID(可选,由车牌反查)</summary>
|
||||
public string? VehicleId { get; set; }
|
||||
|
||||
/// <summary>车牌号</summary>
|
||||
public string? PlateNumber { get; set; }
|
||||
|
||||
/// <summary>发货单位(进厂时为供应商名称)</summary>
|
||||
public string? SenderUnit { get; set; }
|
||||
|
||||
/// <summary>收货单位(出厂时为客户简称)</summary>
|
||||
public string? ReceiverUnit { get; set; }
|
||||
|
||||
/// <summary>货物名称</summary>
|
||||
public string? GoodsName { get; set; }
|
||||
|
||||
/// <summary>毛重(KG),实际称量</summary>
|
||||
public double? GrossWeight { get; set; }
|
||||
|
||||
/// <summary>皮重(KG),从车辆档案带出或单独采集</summary>
|
||||
public double? TareWeight { get; set; }
|
||||
|
||||
/// <summary>净重(KG)=毛重-皮重,自动计算</summary>
|
||||
public double? NetWeight { get; set; }
|
||||
|
||||
/// <summary>司机姓名</summary>
|
||||
public string? DriverName { get; set; }
|
||||
|
||||
/// <summary>司机手机号</summary>
|
||||
public string? DriverPhone { get; set; }
|
||||
|
||||
/// <summary>单据类型:1已称毛重 2称重完成</summary>
|
||||
public string? BillType { get; set; }
|
||||
|
||||
public int? TenantId { get; set; }
|
||||
public string? CreateBy { get; set; }
|
||||
public DateTime? CreateTime { get; set; }
|
||||
public string? UpdateBy { get; set; }
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
public string? SysOrgCode { get; set; }
|
||||
|
||||
/// <summary>进出方向显示文本(由 ViewModel 填充)</summary>
|
||||
public string InoutDirectionText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>单据类型显示文本(由 ViewModel 填充)</summary>
|
||||
public string BillTypeText { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -32,6 +32,10 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
|
||||
new SysMenu{ Id=1300150010201, Pid=1300150000101, Title="客户管理", Path="/xslmes/mesXslCustomer", Name="mesXslCustomer", Component="CustomerListView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=101 },
|
||||
// 供应商管理
|
||||
new SysMenu{ Id=1300150010301, Pid=1300150000101, Title="供应商管理", Path="/xslmes/mesXslSupplier", Name="mesXslSupplier", Component="SupplierListView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=102 },
|
||||
// 磅单记录管理(标准CRUD)
|
||||
new SysMenu{ Id=1300150010401, Pid=1300150000101, Title="磅单记录管理", Path="/xslmes/mesXslWeightRecord", Name="mesXslWeightRecord", Component="WeightRecordListView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=103 },
|
||||
// 地磅称重操作(操作台大页面)
|
||||
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 },
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ public class SysTenantMenuSeedData : ISqlSugarEntitySeedData<SysTenantMenu>
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150000101},
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010101},
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010201},
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010401},
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010501},
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200010701 },
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300100601 },
|
||||
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200090401 },
|
||||
|
||||
@@ -0,0 +1,710 @@
|
||||
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.WeightRecord;
|
||||
|
||||
public class WeightRecordService : IWeightRecordService, 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<WeightRecordPendingOperation> _pendingOps = new();
|
||||
private List<MesXslWeightRecord> _localCache = new();
|
||||
|
||||
private static readonly JsonSerializerOptions _jsonOpts = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
Converters = { new NullableDateTimeJsonConverter() }
|
||||
};
|
||||
|
||||
public WeightRecordService(
|
||||
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, "weight-record-pending-ops.json");
|
||||
_cacheFilePath = Path.Combine(appDataDir, "weight-record-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<WeightRecordPageResult> PageAsync(
|
||||
int pageNo, int pageSize,
|
||||
string? filterBillNo = null,
|
||||
string? filterPlateNumber = null,
|
||||
string? filterInoutDirection = null,
|
||||
string? filterGoodsName = null,
|
||||
string? filterDriverName = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
List<MesXslWeightRecord>? 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, filterBillNo, filterPlateNumber, filterInoutDirection, filterGoodsName, filterDriverName);
|
||||
var total = filtered.Count;
|
||||
var records = filtered.Skip(Math.Max(0, (pageNo - 1) * pageSize)).Take(pageSize).ToList();
|
||||
return new WeightRecordPageResult(records, total, pageNo, pageSize);
|
||||
}
|
||||
|
||||
public async Task<MesXslWeightRecord?> GetByIdAsync(string id, CancellationToken ct = default)
|
||||
{
|
||||
if (_networkMonitor.IsOnline)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}/xslmes/mesXslWeightRecord/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<MesXslWeightRecord>(_jsonOpts);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[磅单详情] 远端查询异常 id={id}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
lock (_cacheLock)
|
||||
{
|
||||
return _localCache.FirstOrDefault(v => string.Equals(v.Id, id, StringComparison.OrdinalIgnoreCase)) is { } found
|
||||
? Clone(found) : null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> AddAsync(MesXslWeightRecord entity, CancellationToken ct = default)
|
||||
{
|
||||
if (!entity.TenantId.HasValue || entity.TenantId.Value <= 0)
|
||||
entity.TenantId = DefaultTenantId;
|
||||
if (string.IsNullOrWhiteSpace(entity.BillNo))
|
||||
entity.BillNo = GenerateBillNo();
|
||||
if (string.IsNullOrWhiteSpace(entity.BillType))
|
||||
entity.BillType = ResolveBillType(entity);
|
||||
|
||||
var local = Clone(entity);
|
||||
if (string.IsNullOrWhiteSpace(local.Id))
|
||||
local.Id = $"local-{Guid.NewGuid():N}";
|
||||
|
||||
if (_networkMonitor.IsOnline)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Information($"[磅单新增] 尝试远端新增 id={local.Id}");
|
||||
var ok = await RemoteAddAsync(local, ct).ConfigureAwait(false);
|
||||
if (ok) { UpsertLocalCache(local); return true; }
|
||||
_logger.Warning($"[磅单新增] 远端返回失败 id={local.Id}");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[磅单新增] 远端异常,转离线入队:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
EnqueuePendingOperation(new WeightRecordPendingOperation
|
||||
{
|
||||
OpType = WeightRecordOperationType.Add,
|
||||
WeightRecordId = local.Id,
|
||||
Entity = local
|
||||
});
|
||||
UpsertLocalCache(local);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按后端规则生成磅单号:BDH-yyyyMMddHHmmss + 3位随机数
|
||||
/// </summary>
|
||||
private static string GenerateBillNo()
|
||||
{
|
||||
var dateStr = DateTime.Now.ToString("yyyyMMddHHmmss");
|
||||
var seq = Random.Shared.Next(0, 1000).ToString("D3");
|
||||
return $"BDH-{dateStr}{seq}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据称重数据推导单据类型:仅毛重=已称毛重;仅皮重=已称皮重;毛重+皮重=称重完成。
|
||||
/// </summary>
|
||||
private static string? ResolveBillType(MesXslWeightRecord entity)
|
||||
{
|
||||
if (entity.GrossWeight.HasValue && entity.TareWeight.HasValue) return "2";
|
||||
if (entity.GrossWeight.HasValue) return "1";
|
||||
if (entity.TareWeight.HasValue) return "3";
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<bool> EditAsync(MesXslWeightRecord entity, CancellationToken ct = default)
|
||||
{
|
||||
if (!entity.TenantId.HasValue || entity.TenantId.Value <= 0)
|
||||
entity.TenantId = DefaultTenantId;
|
||||
if (string.IsNullOrWhiteSpace(entity.BillType))
|
||||
entity.BillType = ResolveBillType(entity);
|
||||
|
||||
var local = Clone(entity);
|
||||
if (IsLocalTempId(local.Id))
|
||||
{
|
||||
// 本地临时ID表示该记录尚未上云:将“二次称重编辑”合并到同一条待上传新增,避免重连后产生冲突。
|
||||
if (TryMergeIntoPendingAdd(local))
|
||||
{
|
||||
UpsertLocalCache(local);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 兜底:若找不到待上传新增,则按新增入队,确保本地修改不丢失。
|
||||
EnqueuePendingOperation(new WeightRecordPendingOperation
|
||||
{
|
||||
OpType = WeightRecordOperationType.Add,
|
||||
WeightRecordId = local.Id,
|
||||
Entity = local
|
||||
});
|
||||
UpsertLocalCache(local);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_networkMonitor.IsOnline)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Information($"[磅单修改] 尝试远端修改 id={local.Id}");
|
||||
var (ok, _) = await RemoteEditAsync(local, ct).ConfigureAwait(false);
|
||||
if (ok) { UpsertLocalCache(local); return true; }
|
||||
_logger.Warning($"[磅单修改] 远端返回失败 id={local.Id}");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[磅单修改] 远端异常,转离线入队:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
EnqueuePendingOperation(new WeightRecordPendingOperation
|
||||
{
|
||||
OpType = WeightRecordOperationType.Edit,
|
||||
WeightRecordId = local.Id,
|
||||
Entity = local,
|
||||
AnchorUpdateTime = local.UpdateTime
|
||||
});
|
||||
UpsertLocalCache(local);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryMergeIntoPendingAdd(MesXslWeightRecord local)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(local.Id)) return false;
|
||||
lock (_cacheLock)
|
||||
{
|
||||
var pendingAdd = _pendingOps
|
||||
.Where(x => x.OpType == WeightRecordOperationType.Add)
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.FirstOrDefault(x =>
|
||||
string.Equals(x.WeightRecordId, local.Id, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(x.Entity?.Id, local.Id, StringComparison.OrdinalIgnoreCase));
|
||||
if (pendingAdd == null) return false;
|
||||
|
||||
pendingAdd.Entity = Clone(local);
|
||||
pendingAdd.WeightRecordId = local.Id;
|
||||
SavePendingOpsToDiskUnsafe();
|
||||
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($"[磅单删除] 远端异常,转离线入队:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
DateTime? anchor;
|
||||
lock (_cacheLock)
|
||||
anchor = _localCache.FirstOrDefault(v => string.Equals(v.Id, id, StringComparison.OrdinalIgnoreCase))?.UpdateTime;
|
||||
|
||||
EnqueuePendingOperation(new WeightRecordPendingOperation
|
||||
{
|
||||
OpType = WeightRecordOperationType.Delete,
|
||||
WeightRecordId = id,
|
||||
AnchorUpdateTime = anchor
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
// ─────────────────── 远端 HTTP ───────────────────
|
||||
|
||||
private async Task<List<MesXslWeightRecord>> FetchRemoteListAsync(CancellationToken ct)
|
||||
{
|
||||
var query = HttpUtility.ParseQueryString(string.Empty);
|
||||
query["pageNo"] = "1";
|
||||
query["pageSize"] = "10000";
|
||||
query["tenantId"] = DefaultTenantId.ToString();
|
||||
var url = $"{BaseUrl}/xslmes/mesXslWeightRecord/anon/list?{query}";
|
||||
using var client = CreateClient();
|
||||
_logger.Information($"[磅单远端] GET {url}");
|
||||
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<MesXslWeightRecord>>(_jsonOpts) ?? new();
|
||||
}
|
||||
|
||||
private async Task<bool> RemoteAddAsync(MesXslWeightRecord entity, CancellationToken ct)
|
||||
{
|
||||
var url = $"{BaseUrl}/xslmes/mesXslWeightRecord/anon/add?tenantId={DefaultTenantId}";
|
||||
var payload = Clone(entity);
|
||||
if (IsLocalTempId(payload.Id)) payload.Id = null;
|
||||
return await PostJsonAsync(url, payload, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<(bool Ok, bool IsVersionConflict)> RemoteEditAsync(MesXslWeightRecord entity, CancellationToken ct)
|
||||
{
|
||||
var url = $"{BaseUrl}/xslmes/mesXslWeightRecord/anon/edit?tenantId={DefaultTenantId}";
|
||||
return await PostJsonCheckVersionAsync(url, entity, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<bool> RemoteDeleteAsync(string id, CancellationToken ct)
|
||||
{
|
||||
var url = $"{BaseUrl}/xslmes/mesXslWeightRecord/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) && (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; }
|
||||
}
|
||||
|
||||
// ─────────────────── 重连同步 ───────────────────
|
||||
|
||||
private void OnNetworkStatusChanged(bool isOnline)
|
||||
{
|
||||
if (!isOnline) return;
|
||||
_ = Task.Run(() => SyncAfterReconnectAsync(CancellationToken.None));
|
||||
}
|
||||
|
||||
private async Task SyncAfterReconnectAsync(CancellationToken ct)
|
||||
{
|
||||
_logger.Information("[磅单重连] 开始重连同步");
|
||||
var pushResult = await PushPendingOnReconnectAsync(ct).ConfigureAwait(false);
|
||||
if (!_networkMonitor.IsOnline) return;
|
||||
try
|
||||
{
|
||||
var remote = await FetchRemoteListAsync(ct).ConfigureAwait(false);
|
||||
lock (_cacheLock) { _localCache = remote.Select(Clone).ToList(); SaveCacheToDiskUnsafe(); }
|
||||
_eventAggregator.GetEvent<MesXslWeightRecordChangedEvent>().Publish(new MesXslWeightRecordChangedPayload { Action = "pull" });
|
||||
_logger.Information($"[磅单重连] 全量回拉成功 count={remote.Count}");
|
||||
}
|
||||
catch (Exception ex) { _logger.Warning($"[磅单重连] 全量回拉失败:{ex.Message}"); }
|
||||
|
||||
var hasActivity = pushResult.PushedCount > 0 || pushResult.ConflictCount > 0 || pushResult.NewRecordsPushed > 0;
|
||||
if (hasActivity)
|
||||
_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 ct)
|
||||
{
|
||||
if (!await _syncLock.WaitAsync(0, ct).ConfigureAwait(false)) return new PushPendingResult(0, 0, 0);
|
||||
try
|
||||
{
|
||||
List<WeightRecordPendingOperation> snapshot;
|
||||
lock (_cacheLock) { snapshot = _pendingOps.OrderBy(x => x.CreatedAt).ToList(); }
|
||||
_logger.Information($"[磅单推送] 开始推送 pending={snapshot.Count}");
|
||||
|
||||
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, ct).ConfigureAwait(false);
|
||||
if (!result.Ok)
|
||||
{
|
||||
lock (_cacheLock)
|
||||
{
|
||||
op.RetryCount++;
|
||||
if (op.RetryCount >= MaxPendingRetries)
|
||||
{
|
||||
_pendingOps.RemoveAll(x => x.Id == op.Id);
|
||||
SavePendingOpsToDiskUnsafe();
|
||||
continue;
|
||||
}
|
||||
SavePendingOpsToDiskUnsafe();
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (result.IsConflict)
|
||||
{
|
||||
conflicts++;
|
||||
if (!string.IsNullOrWhiteSpace(result.EntityId)) RemovePendingOpsByEntityId(result.EntityId!);
|
||||
continue;
|
||||
}
|
||||
lock (_cacheLock)
|
||||
{
|
||||
if (op.OpType == WeightRecordOperationType.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(WeightRecordPendingOperation op, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (op.OpType)
|
||||
{
|
||||
case WeightRecordOperationType.Add:
|
||||
{
|
||||
var ok = op.Entity != null && await RemoteAddAsync(op.Entity, ct).ConfigureAwait(false);
|
||||
return ok ? new PendingReplayResult(true, false, op.WeightRecordId) : new PendingReplayResult(false, false, null);
|
||||
}
|
||||
case WeightRecordOperationType.Edit:
|
||||
{
|
||||
if (op.Entity?.Id == null) return new PendingReplayResult(false, false, null);
|
||||
var remote = await GetByIdAsync(op.Entity.Id, ct).ConfigureAwait(false);
|
||||
if (remote != null && op.AnchorUpdateTime != null && remote.UpdateTime != op.AnchorUpdateTime)
|
||||
{
|
||||
UpsertLocalCache(remote);
|
||||
return new PendingReplayResult(true, true, op.Entity.Id);
|
||||
}
|
||||
var (ok, isConflict) = await RemoteEditAsync(op.Entity, ct).ConfigureAwait(false);
|
||||
if (isConflict)
|
||||
{
|
||||
var fresh = await GetByIdAsync(op.Entity.Id, ct).ConfigureAwait(false);
|
||||
if (fresh != null) UpsertLocalCache(fresh);
|
||||
return new PendingReplayResult(true, true, op.Entity.Id);
|
||||
}
|
||||
return ok ? new PendingReplayResult(true, false, op.Entity.Id) : new PendingReplayResult(false, false, null);
|
||||
}
|
||||
case WeightRecordOperationType.Delete:
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(op.WeightRecordId)) return new PendingReplayResult(false, false, null);
|
||||
var id = op.WeightRecordId!;
|
||||
var remote = await GetByIdAsync(id, ct).ConfigureAwait(false);
|
||||
if (remote == null) return new PendingReplayResult(true, false, id);
|
||||
if (op.AnchorUpdateTime != null && remote.UpdateTime != op.AnchorUpdateTime)
|
||||
{
|
||||
UpsertLocalCache(remote);
|
||||
return new PendingReplayResult(true, true, id);
|
||||
}
|
||||
var ok = await RemoteDeleteAsync(id, ct).ConfigureAwait(false);
|
||||
return ok ? new PendingReplayResult(true, false, id) : new PendingReplayResult(false, false, null);
|
||||
}
|
||||
default: return new PendingReplayResult(true, false, null);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[磅单推送] 执行异常 op={op.OpType}: {ex.Message}");
|
||||
return new PendingReplayResult(false, false, null);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────── 过滤 / 缓存辅助 ───────────────────
|
||||
|
||||
private static List<MesXslWeightRecord> ApplyFilters(
|
||||
List<MesXslWeightRecord> source,
|
||||
string? billNo, string? plateNumber, string? inoutDirection, string? goodsName, string? driverName)
|
||||
{
|
||||
IEnumerable<MesXslWeightRecord> q = source;
|
||||
if (!string.IsNullOrWhiteSpace(billNo))
|
||||
q = q.Where(v => (v.BillNo ?? "").Contains(billNo, StringComparison.OrdinalIgnoreCase));
|
||||
if (!string.IsNullOrWhiteSpace(plateNumber))
|
||||
q = q.Where(v => (v.PlateNumber ?? "").Contains(plateNumber, StringComparison.OrdinalIgnoreCase));
|
||||
if (!string.IsNullOrWhiteSpace(inoutDirection))
|
||||
q = q.Where(v => string.Equals(v.InoutDirection, inoutDirection, StringComparison.OrdinalIgnoreCase));
|
||||
if (!string.IsNullOrWhiteSpace(goodsName))
|
||||
q = q.Where(v => (v.GoodsName ?? "").Contains(goodsName, StringComparison.OrdinalIgnoreCase));
|
||||
if (!string.IsNullOrWhiteSpace(driverName))
|
||||
q = q.Where(v => (v.DriverName ?? "").Contains(driverName, StringComparison.OrdinalIgnoreCase));
|
||||
return q.OrderByDescending(v => v.CreateTime ?? DateTime.MinValue).ToList();
|
||||
}
|
||||
|
||||
private List<MesXslWeightRecord> ApplyPendingOpsSnapshotUnsafe(List<MesXslWeightRecord> source)
|
||||
{
|
||||
var map = source.Where(v => !string.IsNullOrWhiteSpace(v.Id))
|
||||
.ToDictionary(v => v.Id!, Clone, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var op in _pendingOps.OrderBy(x => x.CreatedAt))
|
||||
{
|
||||
switch (op.OpType)
|
||||
{
|
||||
case WeightRecordOperationType.Add:
|
||||
case WeightRecordOperationType.Edit:
|
||||
if (op.Entity?.Id != null) map[op.Entity.Id] = Clone(op.Entity);
|
||||
break;
|
||||
case WeightRecordOperationType.Delete:
|
||||
if (!string.IsNullOrWhiteSpace(op.WeightRecordId)) map.Remove(op.WeightRecordId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return map.Values.ToList();
|
||||
}
|
||||
|
||||
private void EnqueuePendingOperation(WeightRecordPendingOperation op)
|
||||
{
|
||||
lock (_cacheLock) { _pendingOps.Add(op); SavePendingOpsToDiskUnsafe(); }
|
||||
}
|
||||
|
||||
private void UpsertLocalCache(MesXslWeightRecord entity)
|
||||
{
|
||||
lock (_cacheLock)
|
||||
{
|
||||
var idx = _localCache.FindIndex(v => string.Equals(v.Id, entity.Id, StringComparison.OrdinalIgnoreCase));
|
||||
if (idx >= 0) _localCache[idx] = Clone(entity); else _localCache.Insert(0, Clone(entity));
|
||||
SaveCacheToDiskUnsafe();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveFromLocalCache(string id)
|
||||
{
|
||||
lock (_cacheLock) { _localCache.RemoveAll(v => string.Equals(v.Id, id, StringComparison.OrdinalIgnoreCase)); SaveCacheToDiskUnsafe(); }
|
||||
}
|
||||
|
||||
private void RemovePendingOpsByEntityId(string id)
|
||||
{
|
||||
lock (_cacheLock)
|
||||
{
|
||||
_pendingOps.RemoveAll(x =>
|
||||
(!string.IsNullOrWhiteSpace(x.WeightRecordId) && string.Equals(x.WeightRecordId, id, StringComparison.OrdinalIgnoreCase)) ||
|
||||
(x.Entity?.Id != null && string.Equals(x.Entity.Id, id, StringComparison.OrdinalIgnoreCase)));
|
||||
SavePendingOpsToDiskUnsafe();
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadPendingOpsFromDisk()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_pendingOpsFilePath)) return;
|
||||
var data = JsonSerializer.Deserialize<List<WeightRecordPendingOperation>>(File.ReadAllText(_pendingOpsFilePath), _jsonOpts);
|
||||
_pendingOps = data ?? new();
|
||||
}
|
||||
catch { _pendingOps = new(); }
|
||||
}
|
||||
|
||||
private void LoadCacheFromDisk()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_cacheFilePath)) return;
|
||||
var data = JsonSerializer.Deserialize<List<MesXslWeightRecord>>(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 MesXslWeightRecord Clone(MesXslWeightRecord input) => new()
|
||||
{
|
||||
Id = input.Id,
|
||||
BillNo = input.BillNo,
|
||||
WeighDate = input.WeighDate,
|
||||
InoutDirection = input.InoutDirection,
|
||||
VehicleId = input.VehicleId,
|
||||
PlateNumber = input.PlateNumber,
|
||||
SenderUnit = input.SenderUnit,
|
||||
ReceiverUnit = input.ReceiverUnit,
|
||||
GoodsName = input.GoodsName,
|
||||
GrossWeight = input.GrossWeight,
|
||||
TareWeight = input.TareWeight,
|
||||
NetWeight = input.NetWeight,
|
||||
DriverName = input.DriverName,
|
||||
DriverPhone = input.DriverPhone,
|
||||
BillType = input.BillType,
|
||||
TenantId = input.TenantId,
|
||||
CreateBy = input.CreateBy,
|
||||
CreateTime = input.CreateTime,
|
||||
UpdateBy = input.UpdateBy,
|
||||
UpdateTime = input.UpdateTime,
|
||||
SysOrgCode = input.SysOrgCode,
|
||||
InoutDirectionText = input.InoutDirectionText,
|
||||
BillTypeText = input.BillTypeText
|
||||
};
|
||||
|
||||
private static bool IsLocalTempId(string? id) =>
|
||||
!string.IsNullOrWhiteSpace(id) && id.StartsWith("local-", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// ─────────────────── 内部数据结构 ───────────────────
|
||||
|
||||
private sealed class WeightRecordPendingOperation
|
||||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||
public WeightRecordOperationType OpType { get; set; }
|
||||
public string? WeightRecordId { get; set; }
|
||||
public MesXslWeightRecord? Entity { get; set; }
|
||||
public DateTime? AnchorUpdateTime { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public int RetryCount { get; set; } = 0;
|
||||
}
|
||||
|
||||
private enum WeightRecordOperationType { 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",
|
||||
"yyyy-MM-dd"
|
||||
];
|
||||
|
||||
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($"无法转换为 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"));
|
||||
else writer.WriteNullValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
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.WeightRecord;
|
||||
|
||||
/// <summary>
|
||||
/// 监听 STOMP 收到的磅单变更信号,转发为桌面端 Prism 事件,触发列表刷新。
|
||||
/// </summary>
|
||||
public class WeightRecordSyncCoordinator : ISingletonDependency
|
||||
{
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ILoggerService _logger;
|
||||
private SubscriptionToken? _remoteCommandToken;
|
||||
private SubscriptionToken? _networkStatusToken;
|
||||
|
||||
public WeightRecordSyncCoordinator(IEventAggregator eventAggregator, ILoggerService logger)
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
_remoteCommandToken = _eventAggregator
|
||||
.GetEvent<RemoteCommandReceivedEvent>()
|
||||
.Subscribe(OnRemoteCommand, ThreadOption.BackgroundThread);
|
||||
_networkStatusToken = _eventAggregator
|
||||
.GetEvent<NetworkStatusChangedEvent>()
|
||||
.Subscribe(OnNetworkStatusChanged, ThreadOption.BackgroundThread);
|
||||
_logger.Information("[磅单推送] WeightRecordSyncCoordinator 已启动");
|
||||
}
|
||||
|
||||
private void OnNetworkStatusChanged(NetworkStatusChangedPayload payload)
|
||||
{
|
||||
if (!payload.IsOnline) return;
|
||||
_logger.Information("[磅单推送] 网络恢复,触发补偿刷新");
|
||||
_eventAggregator.GetEvent<MesXslWeightRecordChangedEvent>().Publish(
|
||||
new MesXslWeightRecordChangedPayload { 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_WEIGHT_RECORD_CHANGED", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
doc.RootElement.TryGetProperty("action", out var actionEl);
|
||||
doc.RootElement.TryGetProperty("weightRecordId", out var idEl);
|
||||
|
||||
var changedPayload = new MesXslWeightRecordChangedPayload
|
||||
{
|
||||
Action = actionEl.GetString() ?? string.Empty,
|
||||
WeightRecordId = idEl.ValueKind == JsonValueKind.String ? idEl.GetString() : null
|
||||
};
|
||||
|
||||
_logger.Information($"[磅单推送] 收到变更信号 action={changedPayload.Action}, id={changedPayload.WeightRecordId}");
|
||||
_eventAggregator.GetEvent<MesXslWeightRecordChangedEvent>().Publish(changedPayload);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"[磅单推送] 处理 STOMP 信号失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,6 +134,10 @@ public class StompWebSocketService : ISignalRService
|
||||
await SendFrameAsync(
|
||||
BuildSubscribeFrame("sub-mes-suppliers", "/topic/sync/mes-suppliers"),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
// 磅单数据变更:订阅 /topic/sync/mes-weight-records
|
||||
await SendFrameAsync(
|
||||
BuildSubscribeFrame("sub-mes-weight-records", "/topic/sync/mes-weight-records"),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// 订阅服务端 PONG 回复(应用层假在线检测)
|
||||
await SendFrameAsync(
|
||||
|
||||
@@ -11,6 +11,7 @@ using YY.Admin.Views.Customer;
|
||||
using YY.Admin.Views.Supplier;
|
||||
using YY.Admin.ViewModels.Vehicle;
|
||||
using YY.Admin.Views.Vehicle;
|
||||
using YY.Admin.Views.WeightRecord;
|
||||
|
||||
namespace YY.Admin
|
||||
{
|
||||
@@ -61,6 +62,10 @@ namespace YY.Admin
|
||||
containerRegistry.RegisterForNavigation<CustomerListView>();
|
||||
// 供应商管理
|
||||
containerRegistry.RegisterForNavigation<SupplierListView>();
|
||||
// 磅单记录管理(标准CRUD列表)
|
||||
containerRegistry.RegisterForNavigation<WeightRecordListView>();
|
||||
// 地磅称重操作(大页面操作台)
|
||||
containerRegistry.RegisterForNavigation<WeightRecordOperationView>();
|
||||
}
|
||||
}
|
||||
public class DialogWindow : Window, IDialogWindow
|
||||
|
||||
@@ -16,6 +16,7 @@ using YY.Admin.Infrastructure.Sync;
|
||||
using YY.Admin.Services.Service.Customer;
|
||||
using YY.Admin.Services.Service.Supplier;
|
||||
using YY.Admin.Services.Service.Vehicle;
|
||||
using YY.Admin.Services.Service.WeightRecord;
|
||||
|
||||
namespace YY.Admin.Module;
|
||||
|
||||
@@ -42,6 +43,9 @@ public class SyncModule : IModule
|
||||
// 供应商管理:免密 API 直连 + STOMP 实时通知
|
||||
containerRegistry.RegisterSingleton<ISupplierService, SupplierService>();
|
||||
containerRegistry.RegisterSingleton<SupplierSyncCoordinator>();
|
||||
// 磅单管理:免密 API 直连 + STOMP 实时通知
|
||||
containerRegistry.RegisterSingleton<IWeightRecordService, WeightRecordService>();
|
||||
containerRegistry.RegisterSingleton<WeightRecordSyncCoordinator>();
|
||||
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddTransient<DisconnectGuardHandler>();
|
||||
@@ -92,6 +96,8 @@ public class SyncModule : IModule
|
||||
_ = containerProvider.Resolve<CustomerSyncCoordinator>();
|
||||
// 强制实例化供应商同步协调器
|
||||
_ = containerProvider.Resolve<SupplierSyncCoordinator>();
|
||||
// 强制实例化磅单同步协调器
|
||||
_ = containerProvider.Resolve<WeightRecordSyncCoordinator>();
|
||||
}
|
||||
|
||||
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
|
||||
|
||||
@@ -98,7 +98,17 @@ namespace YY.Admin.ViewModels.Control
|
||||
["SupplierListView"] = "SupplierListView",
|
||||
["/xslmes/mesXslSupplier"] = "SupplierListView",
|
||||
["/xslmes/supplier"] = "SupplierListView",
|
||||
["mesXslSupplier"] = "SupplierListView"
|
||||
["mesXslSupplier"] = "SupplierListView",
|
||||
|
||||
// 已实现页面:磅单记录管理
|
||||
["WeightRecordListView"] = "WeightRecordListView",
|
||||
["/xslmes/mesXslWeightRecord"] = "WeightRecordListView",
|
||||
["mesXslWeightRecord"] = "WeightRecordListView",
|
||||
|
||||
// 已实现页面:地磅称重操作
|
||||
["WeightRecordOperationView"] = "WeightRecordOperationView",
|
||||
["/xslmes/weightRecordOperation"] = "WeightRecordOperationView",
|
||||
["weightRecordOperation"] = "WeightRecordOperationView"
|
||||
};
|
||||
|
||||
private MenuItem? _selectedMenuItem;
|
||||
|
||||
@@ -382,8 +382,9 @@ namespace YY.Admin.ViewModels
|
||||
}
|
||||
else
|
||||
{
|
||||
var exMsg = result.Exception?.Message;
|
||||
_logger.Error($"导航失败: {viewName}, Region={regionName}, Exception={exMsg}");
|
||||
var exMsg = result.Exception?.ToString() ?? "null";
|
||||
var innerExMsg = result.Exception?.InnerException?.ToString() ?? "null";
|
||||
_logger.Error($"导航失败: {viewName}, Region={regionName}, Exception={exMsg}, InnerException={innerExMsg}");
|
||||
tcs.SetResult(false);
|
||||
}
|
||||
}, parameters);
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
using HandyControl.Tools.Extension;
|
||||
using System.Collections.ObjectModel;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Services;
|
||||
|
||||
namespace YY.Admin.ViewModels.WeightRecord;
|
||||
|
||||
public class CustomerPickerDialogViewModel : BaseViewModel, IDialogResultable<bool>
|
||||
{
|
||||
private readonly ICustomerService _customerService;
|
||||
|
||||
private string? _searchText;
|
||||
public string? SearchText { get => _searchText; set => SetProperty(ref _searchText, value); }
|
||||
|
||||
public ObservableCollection<MesXslCustomer> Customers { get; } = new();
|
||||
|
||||
private MesXslCustomer? _selectedCustomer;
|
||||
public MesXslCustomer? SelectedCustomer
|
||||
{
|
||||
get => _selectedCustomer;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _selectedCustomer, value);
|
||||
ConfirmCommand.RaiseCanExecuteChanged();
|
||||
RaisePropertyChanged(nameof(SelectedCustomerDisplay));
|
||||
RaisePropertyChanged(nameof(HasSelectedCustomer));
|
||||
}
|
||||
}
|
||||
|
||||
public string SelectedCustomerDisplay => _selectedCustomer != null
|
||||
? $"[{_selectedCustomer.CustomerCode}] {_selectedCustomer.CustomerName}"
|
||||
: "选中客户后点击「确认选择」";
|
||||
|
||||
public bool HasSelectedCustomer => _selectedCustomer != null;
|
||||
|
||||
private bool _result;
|
||||
public bool Result { get => _result; set => SetProperty(ref _result, value); }
|
||||
public Action? CloseAction { get; set; }
|
||||
|
||||
public DelegateCommand SearchCommand { get; }
|
||||
public DelegateCommand ConfirmCommand { get; }
|
||||
public DelegateCommand CancelCommand { get; }
|
||||
|
||||
public CustomerPickerDialogViewModel(
|
||||
ICustomerService customerService,
|
||||
IContainerExtension container,
|
||||
IRegionManager regionManager) : base(container, regionManager)
|
||||
{
|
||||
_customerService = customerService;
|
||||
SearchCommand = new DelegateCommand(async () => await LoadAsync());
|
||||
ConfirmCommand = new DelegateCommand(Confirm, () => SelectedCustomer != null);
|
||||
CancelCommand = new DelegateCommand(() => CloseAction?.Invoke());
|
||||
_ = LoadAsync();
|
||||
}
|
||||
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
var keyword = SearchText?.Trim();
|
||||
var result = await _customerService.PageAsync(1, 200, customerName: keyword);
|
||||
Customers.Clear();
|
||||
foreach (var c in result.Records)
|
||||
Customers.Add(c);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Customers.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Confirm()
|
||||
{
|
||||
if (SelectedCustomer == null) return;
|
||||
Result = true;
|
||||
CloseAction?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using HandyControl.Tools.Extension;
|
||||
using System.Collections.ObjectModel;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Services;
|
||||
|
||||
namespace YY.Admin.ViewModels.WeightRecord;
|
||||
|
||||
public class SupplierPickerDialogViewModel : BaseViewModel, IDialogResultable<bool>
|
||||
{
|
||||
private readonly ISupplierService _supplierService;
|
||||
|
||||
private string? _searchText;
|
||||
public string? SearchText { get => _searchText; set => SetProperty(ref _searchText, value); }
|
||||
|
||||
public ObservableCollection<MesXslSupplier> Suppliers { get; } = new();
|
||||
|
||||
private MesXslSupplier? _selectedSupplier;
|
||||
public MesXslSupplier? SelectedSupplier
|
||||
{
|
||||
get => _selectedSupplier;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _selectedSupplier, value);
|
||||
ConfirmCommand.RaiseCanExecuteChanged();
|
||||
RaisePropertyChanged(nameof(SelectedSupplierDisplay));
|
||||
RaisePropertyChanged(nameof(HasSelectedSupplier));
|
||||
}
|
||||
}
|
||||
|
||||
public string SelectedSupplierDisplay => _selectedSupplier != null
|
||||
? $"[{_selectedSupplier.SupplierCode}] {_selectedSupplier.SupplierName}"
|
||||
: "选中供应商后点击「确认选择」";
|
||||
|
||||
public bool HasSelectedSupplier => _selectedSupplier != null;
|
||||
|
||||
private bool _result;
|
||||
public bool Result { get => _result; set => SetProperty(ref _result, value); }
|
||||
public Action? CloseAction { get; set; }
|
||||
|
||||
public DelegateCommand SearchCommand { get; }
|
||||
public DelegateCommand ConfirmCommand { get; }
|
||||
public DelegateCommand CancelCommand { get; }
|
||||
|
||||
public SupplierPickerDialogViewModel(
|
||||
ISupplierService supplierService,
|
||||
IContainerExtension container,
|
||||
IRegionManager regionManager) : base(container, regionManager)
|
||||
{
|
||||
_supplierService = supplierService;
|
||||
SearchCommand = new DelegateCommand(async () => await LoadAsync());
|
||||
ConfirmCommand = new DelegateCommand(Confirm, () => SelectedSupplier != null);
|
||||
CancelCommand = new DelegateCommand(() => CloseAction?.Invoke());
|
||||
_ = LoadAsync();
|
||||
}
|
||||
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
var keyword = SearchText?.Trim();
|
||||
var result = await _supplierService.PageAsync(1, 200, supplierName: keyword);
|
||||
Suppliers.Clear();
|
||||
foreach (var s in result.Records)
|
||||
Suppliers.Add(s);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Suppliers.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Confirm()
|
||||
{
|
||||
if (SelectedSupplier == null) return;
|
||||
Result = true;
|
||||
CloseAction?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
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.WeightRecord;
|
||||
|
||||
public class WeightRecordEditDialogViewModel : BaseViewModel, IDialogResultable<bool>
|
||||
{
|
||||
private readonly IWeightRecordService _weightRecordService;
|
||||
private readonly IJeecgDictSyncService _dictSyncService;
|
||||
|
||||
private MesXslWeightRecord? _record;
|
||||
public MesXslWeightRecord? Record
|
||||
{
|
||||
get => _record;
|
||||
set => SetProperty(ref _record, value);
|
||||
}
|
||||
|
||||
public bool IsAddMode => string.IsNullOrWhiteSpace(Record?.Id);
|
||||
public string DialogTitle => IsAddMode ? "新增磅单" : "编辑磅单";
|
||||
|
||||
public ObservableCollection<KeyValuePair<string, string>> InoutDirectionOptions { 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 WeightRecordEditDialogViewModel(
|
||||
IWeightRecordService weightRecordService,
|
||||
IJeecgDictSyncService dictSyncService,
|
||||
IContainerExtension container,
|
||||
IRegionManager regionManager) : base(container, regionManager)
|
||||
{
|
||||
_weightRecordService = weightRecordService;
|
||||
_dictSyncService = dictSyncService;
|
||||
SaveCommand = new DelegateCommand(async () => await SaveAsync());
|
||||
CancelCommand = new DelegateCommand(() => CloseAction?.Invoke());
|
||||
_ = LoadDictOptionsAsync();
|
||||
}
|
||||
|
||||
private async Task LoadDictOptionsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var options = await _dictSyncService.GetDictOptionsAsync("xslmes_inout_direction");
|
||||
InoutDirectionOptions.Clear();
|
||||
foreach (var item in options) InoutDirectionOptions.Add(item);
|
||||
if (InoutDirectionOptions.Count == 0) AddDefaultDirectionOptions();
|
||||
}
|
||||
catch { AddDefaultDirectionOptions(); }
|
||||
}
|
||||
|
||||
private void AddDefaultDirectionOptions()
|
||||
{
|
||||
InoutDirectionOptions.Clear();
|
||||
InoutDirectionOptions.Add(new KeyValuePair<string, string>("进厂", "1"));
|
||||
InoutDirectionOptions.Add(new KeyValuePair<string, string>("出厂", "2"));
|
||||
}
|
||||
|
||||
public void InitializeForAdd()
|
||||
{
|
||||
Record = new MesXslWeightRecord
|
||||
{
|
||||
WeighDate = DateTime.Today,
|
||||
InoutDirection = "1"
|
||||
};
|
||||
RaisePropertyChanged(nameof(IsAddMode));
|
||||
RaisePropertyChanged(nameof(DialogTitle));
|
||||
}
|
||||
|
||||
public void InitializeForEdit(MesXslWeightRecord record)
|
||||
{
|
||||
Record = new MesXslWeightRecord
|
||||
{
|
||||
Id = record.Id,
|
||||
BillNo = record.BillNo,
|
||||
WeighDate = record.WeighDate,
|
||||
InoutDirection = record.InoutDirection,
|
||||
VehicleId = record.VehicleId,
|
||||
PlateNumber = record.PlateNumber,
|
||||
SenderUnit = record.SenderUnit,
|
||||
ReceiverUnit = record.ReceiverUnit,
|
||||
GoodsName = record.GoodsName,
|
||||
GrossWeight = record.GrossWeight,
|
||||
TareWeight = record.TareWeight,
|
||||
NetWeight = record.NetWeight,
|
||||
DriverName = record.DriverName,
|
||||
DriverPhone = record.DriverPhone,
|
||||
BillType = record.BillType,
|
||||
TenantId = record.TenantId,
|
||||
UpdateTime = record.UpdateTime
|
||||
};
|
||||
RaisePropertyChanged(nameof(IsAddMode));
|
||||
RaisePropertyChanged(nameof(DialogTitle));
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
if (Record == null) return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Record.PlateNumber))
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Warning("车牌号不能为空!");
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(Record.InoutDirection))
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Warning("进出方向不能为空!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 净重自动计算
|
||||
if (Record.GrossWeight.HasValue && Record.TareWeight.HasValue)
|
||||
Record.NetWeight = Math.Round(Record.GrossWeight.Value - Record.TareWeight.Value, 2);
|
||||
|
||||
try
|
||||
{
|
||||
bool ok;
|
||||
if (IsAddMode)
|
||||
{
|
||||
ok = await _weightRecordService.AddAsync(Record);
|
||||
if (ok) HandyControl.Controls.MessageBox.Success("新增磅单成功!");
|
||||
else { HandyControl.Controls.MessageBox.Error("新增磅单失败!"); return; }
|
||||
}
|
||||
else
|
||||
{
|
||||
ok = await _weightRecordService.EditAsync(Record);
|
||||
if (!ok) { HandyControl.Controls.MessageBox.Error("编辑磅单失败!"); return; }
|
||||
}
|
||||
Result = ok;
|
||||
CloseAction?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Error($"操作失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
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.WeightRecord;
|
||||
|
||||
namespace YY.Admin.ViewModels.WeightRecord;
|
||||
|
||||
public class WeightRecordListViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IWeightRecordService _weightRecordService;
|
||||
private readonly IJeecgDictSyncService _dictSyncService;
|
||||
private readonly IDialogService _dialogService;
|
||||
private SubscriptionToken? _changedToken;
|
||||
private SubscriptionToken? _conflictToken;
|
||||
|
||||
private ObservableCollection<MesXslWeightRecord> _records = new();
|
||||
public ObservableCollection<MesXslWeightRecord> Records
|
||||
{
|
||||
get => _records;
|
||||
set => SetProperty(ref _records, 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? _filterBillNo;
|
||||
public string? FilterBillNo { get => _filterBillNo; set => SetProperty(ref _filterBillNo, value); }
|
||||
|
||||
private string? _filterPlateNumber;
|
||||
public string? FilterPlateNumber { get => _filterPlateNumber; set => SetProperty(ref _filterPlateNumber, value); }
|
||||
|
||||
private string? _filterInoutDirection;
|
||||
public string? FilterInoutDirection { get => _filterInoutDirection; set => SetProperty(ref _filterInoutDirection, value); }
|
||||
|
||||
private string? _filterGoodsName;
|
||||
public string? FilterGoodsName { get => _filterGoodsName; set => SetProperty(ref _filterGoodsName, value); }
|
||||
|
||||
private string? _filterDriverName;
|
||||
public string? FilterDriverName { get => _filterDriverName; set => SetProperty(ref _filterDriverName, value); }
|
||||
|
||||
public ObservableCollection<KeyValuePair<string, string>> InoutDirectionOptions { get; } = new();
|
||||
public ObservableCollection<KeyValuePair<string, string>> BillTypeOptions { get; } = new();
|
||||
|
||||
public DelegateCommand SearchCommand { get; }
|
||||
public DelegateCommand ResetCommand { get; }
|
||||
public DelegateCommand AddCommand { get; }
|
||||
public DelegateCommand<MesXslWeightRecord> EditCommand { get; }
|
||||
public DelegateCommand<MesXslWeightRecord> DeleteCommand { get; }
|
||||
public DelegateCommand PrevPageCommand { get; }
|
||||
public DelegateCommand NextPageCommand { get; }
|
||||
|
||||
public WeightRecordListViewModel(
|
||||
IWeightRecordService weightRecordService,
|
||||
IJeecgDictSyncService dictSyncService,
|
||||
IContainerExtension container,
|
||||
IDialogService dialogService,
|
||||
IRegionManager regionManager) : base(container, regionManager)
|
||||
{
|
||||
_weightRecordService = weightRecordService;
|
||||
_dictSyncService = dictSyncService;
|
||||
_dialogService = dialogService;
|
||||
|
||||
SearchCommand = new DelegateCommand(async () => { PageNo = 1; await LoadAsync(); });
|
||||
ResetCommand = new DelegateCommand(async () =>
|
||||
{
|
||||
FilterBillNo = null; FilterPlateNumber = null; FilterInoutDirection = null;
|
||||
FilterGoodsName = null; FilterDriverName = null;
|
||||
PageNo = 1;
|
||||
await LoadAsync();
|
||||
});
|
||||
AddCommand = new DelegateCommand(async () => await ShowAddDialogAsync());
|
||||
EditCommand = new DelegateCommand<MesXslWeightRecord>(async r => await ShowEditDialogAsync(r));
|
||||
DeleteCommand = new DelegateCommand<MesXslWeightRecord>(async r => await DeleteAsync(r));
|
||||
PrevPageCommand = new DelegateCommand(async () => { if (PageNo > 1) { PageNo--; await LoadAsync(); } });
|
||||
NextPageCommand = new DelegateCommand(async () => { if ((long)PageNo * PageSize < Total) { PageNo++; await LoadAsync(); } });
|
||||
|
||||
_changedToken = _eventAggregator.GetEvent<MesXslWeightRecordChangedEvent>()
|
||||
.Subscribe(async _ => await LoadAsync(), ThreadOption.UIThread);
|
||||
_conflictToken = _eventAggregator.GetEvent<SyncConflictEvent>()
|
||||
.Subscribe(OnSyncConflict, ThreadOption.UIThread);
|
||||
|
||||
_ = InitializeAsync();
|
||||
}
|
||||
|
||||
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 LoadDictOptionsAsync();
|
||||
await UIHelper.WaitForRenderAsync();
|
||||
await LoadAsync();
|
||||
}
|
||||
catch (Exception ex) { Debug.WriteLine($"磅单列表初始化失败: {ex.Message}"); }
|
||||
}
|
||||
|
||||
private async Task LoadDictOptionsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var options = await _dictSyncService.GetDictOptionsAsync("xslmes_inout_direction", includeAll: true);
|
||||
InoutDirectionOptions.Clear();
|
||||
foreach (var item in options) InoutDirectionOptions.Add(item);
|
||||
if (InoutDirectionOptions.Count == 0) InoutDirectionOptions.Add(new KeyValuePair<string, string>("全部", ""));
|
||||
|
||||
var billTypeOptions = await _dictSyncService.GetDictOptionsAsync("xslmes_weight_bill_type", includeAll: true);
|
||||
BillTypeOptions.Clear();
|
||||
foreach (var item in billTypeOptions) BillTypeOptions.Add(item);
|
||||
if (BillTypeOptions.Count == 0) AddDefaultBillTypeOptions();
|
||||
}
|
||||
catch
|
||||
{
|
||||
InoutDirectionOptions.Clear();
|
||||
InoutDirectionOptions.Add(new KeyValuePair<string, string>("全部", ""));
|
||||
InoutDirectionOptions.Add(new KeyValuePair<string, string>("进厂", "1"));
|
||||
InoutDirectionOptions.Add(new KeyValuePair<string, string>("出厂", "2"));
|
||||
AddDefaultBillTypeOptions();
|
||||
}
|
||||
}
|
||||
|
||||
private void AddDefaultBillTypeOptions()
|
||||
{
|
||||
BillTypeOptions.Clear();
|
||||
BillTypeOptions.Add(new KeyValuePair<string, string>("全部", ""));
|
||||
BillTypeOptions.Add(new KeyValuePair<string, string>("已称毛重", "1"));
|
||||
BillTypeOptions.Add(new KeyValuePair<string, string>("称重完成", "2"));
|
||||
BillTypeOptions.Add(new KeyValuePair<string, string>("已称皮重", "3"));
|
||||
}
|
||||
|
||||
public async Task LoadAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
var result = await _weightRecordService.PageAsync(
|
||||
PageNo, PageSize, FilterBillNo, FilterPlateNumber, FilterInoutDirection, FilterGoodsName, FilterDriverName);
|
||||
|
||||
// 填充字典显示文本
|
||||
var dictMap = InoutDirectionOptions.ToDictionary(x => x.Value, x => x.Key);
|
||||
var billTypeMap = BillTypeOptions.ToDictionary(x => x.Value, x => x.Key);
|
||||
foreach (var r in result.Records)
|
||||
{
|
||||
r.InoutDirectionText = dictMap.TryGetValue(r.InoutDirection ?? "", out var txt) ? txt : r.InoutDirection ?? "";
|
||||
r.BillTypeText = billTypeMap.TryGetValue(r.BillType ?? "", out var billTypeTxt) ? billTypeTxt : r.BillType ?? "";
|
||||
}
|
||||
|
||||
Records = new ObservableCollection<MesXslWeightRecord>(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<WeightRecordEditDialogView>()
|
||||
.Initialize<WeightRecordEditDialogViewModel>(vm => vm.InitializeForAdd())
|
||||
.GetResultAsync<bool>();
|
||||
if (result) await LoadAsync();
|
||||
}
|
||||
catch (Exception ex) { Growl.Error($"打开新增对话框失败:{ex.Message}"); }
|
||||
}
|
||||
|
||||
private async Task ShowEditDialogAsync(MesXslWeightRecord record)
|
||||
{
|
||||
if (record == null) return;
|
||||
try
|
||||
{
|
||||
var result = await HandyControl.Controls.Dialog.Show<WeightRecordEditDialogView>()
|
||||
.Initialize<WeightRecordEditDialogViewModel>(vm => vm.InitializeForEdit(record))
|
||||
.GetResultAsync<bool>();
|
||||
if (result) await LoadAsync();
|
||||
}
|
||||
catch (Exception ex) { Growl.Error($"打开编辑对话框失败:{ex.Message}"); }
|
||||
}
|
||||
|
||||
private async Task DeleteAsync(MesXslWeightRecord record)
|
||||
{
|
||||
if (record?.Id == null) return;
|
||||
var confirm = System.Windows.MessageBox.Show(
|
||||
$"确定删除磅单 {record.BillNo ?? record.PlateNumber}?此操作不可恢复!",
|
||||
"确认删除", MessageBoxButton.OKCancel, MessageBoxImage.Question);
|
||||
if (confirm != System.Windows.MessageBoxResult.OK) return;
|
||||
|
||||
var ok = await _weightRecordService.DeleteAsync(record.Id);
|
||||
if (ok) { Growl.Success("删除成功!"); await LoadAsync(); }
|
||||
else Growl.Error("删除失败!");
|
||||
}
|
||||
|
||||
protected override void CleanUp()
|
||||
{
|
||||
base.CleanUp();
|
||||
if (_changedToken != null)
|
||||
{
|
||||
_eventAggregator.GetEvent<MesXslWeightRecordChangedEvent>().Unsubscribe(_changedToken);
|
||||
_changedToken = null;
|
||||
}
|
||||
if (_conflictToken != null)
|
||||
{
|
||||
_eventAggregator.GetEvent<SyncConflictEvent>().Unsubscribe(_conflictToken);
|
||||
_conflictToken = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,877 @@
|
||||
using HandyControl.Controls;
|
||||
using HandyControl.Tools.Extension;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows.Threading;
|
||||
using YY.Admin.Core;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.Services.Service;
|
||||
using YY.Admin.Views.WeightRecord;
|
||||
|
||||
namespace YY.Admin.ViewModels.WeightRecord;
|
||||
|
||||
/// <summary>
|
||||
/// 地磅称重操作页面 ViewModel
|
||||
/// 左侧:串口模拟实时数据(重量 + 车牌);右侧:称重信息录入表单。
|
||||
/// </summary>
|
||||
public class WeightRecordOperationViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IWeightRecordService _weightRecordService;
|
||||
private readonly IJeecgDictSyncService _dictSyncService;
|
||||
private readonly IVehicleService _vehicleService;
|
||||
|
||||
// ─── 车辆档案匹配 ───
|
||||
private MesXslVehicle? _matchedVehicle;
|
||||
private string? _lastLookedUpPlate;
|
||||
|
||||
private string _vehicleLookupStatus = "None"; // None | Searching | Matched | NotFound
|
||||
public string VehicleLookupStatus
|
||||
{
|
||||
get => _vehicleLookupStatus;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _vehicleLookupStatus, value);
|
||||
RaisePropertyChanged(nameof(VehicleMatchText));
|
||||
RaisePropertyChanged(nameof(ShowVehicleMatchHint));
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowVehicleMatchHint => _vehicleLookupStatus != "None";
|
||||
public string VehicleMatchText => _vehicleLookupStatus switch
|
||||
{
|
||||
"Searching" => "正在匹配车辆档案...",
|
||||
"Matched" => $"✓ 已匹配车辆档案({_matchedVehicle?.VehicleBelongText})",
|
||||
"NotFound" => "未在档案库中找到,保存后将自动录入",
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
// ─── 串口模拟定时器 ───
|
||||
private readonly DispatcherTimer _serialTimer;
|
||||
private readonly Random _rnd = new();
|
||||
private double _baseWeight = 35000.0; // 基准重量(模拟车辆在磅上)
|
||||
private int _stableCountdown = 0; // 稳定倒计时(连续稳定帧数)
|
||||
private bool _vehiclePresent = false; // 是否有车辆在磅上
|
||||
private int _vehicleDetectCooldown = 0; // 车辆检测冷却帧
|
||||
private bool _isApplyingSelectedRecord;
|
||||
|
||||
// ─── 实时数据属性 ───
|
||||
|
||||
private double _currentWeight;
|
||||
public double CurrentWeight
|
||||
{
|
||||
get => _currentWeight;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _currentWeight, value);
|
||||
RaisePropertyChanged(nameof(CurrentWeightDisplay));
|
||||
}
|
||||
}
|
||||
|
||||
public string CurrentWeightDisplay => $"{CurrentWeight:N2}";
|
||||
|
||||
private bool _isWeightStable;
|
||||
public bool IsWeightStable { get => _isWeightStable; set => SetProperty(ref _isWeightStable, value); }
|
||||
|
||||
private string _serialStatusText = "正在连接...";
|
||||
public string SerialStatusText { get => _serialStatusText; set => SetProperty(ref _serialStatusText, value); }
|
||||
|
||||
private bool _isSerialConnected;
|
||||
public bool IsSerialConnected { get => _isSerialConnected; set => SetProperty(ref _isSerialConnected, value); }
|
||||
|
||||
private string _detectedPlate = string.Empty;
|
||||
public string DetectedPlate { get => _detectedPlate; set => SetProperty(ref _detectedPlate, value); }
|
||||
|
||||
private bool _hasPlate;
|
||||
public bool HasPlate { get => _hasPlate; set => SetProperty(ref _hasPlate, value); }
|
||||
|
||||
public ObservableCollection<string> OperationLogs { get; } = new();
|
||||
|
||||
// ─── 采集状态 ───
|
||||
|
||||
private bool _grossWeightCaptured;
|
||||
public bool GrossWeightCaptured { get => _grossWeightCaptured; set => SetProperty(ref _grossWeightCaptured, value); }
|
||||
|
||||
private bool _tareWeightCaptured;
|
||||
public bool TareWeightCaptured { get => _tareWeightCaptured; set => SetProperty(ref _tareWeightCaptured, value); }
|
||||
|
||||
// ─── 表单绑定属性 ───
|
||||
|
||||
private DateTime _weighDate = DateTime.Today;
|
||||
public DateTime WeighDate { get => _weighDate; set => SetProperty(ref _weighDate, value); }
|
||||
|
||||
private string? _inoutDirection = "1";
|
||||
public string? InoutDirection
|
||||
{
|
||||
get => _inoutDirection;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _inoutDirection, value)) return;
|
||||
RaisePropertyChanged(nameof(CaptureGrossButtonText));
|
||||
RaisePropertyChanged(nameof(CaptureTareButtonText));
|
||||
CaptureGrossWeightCommand.RaiseCanExecuteChanged();
|
||||
CaptureTareWeightCommand.RaiseCanExecuteChanged();
|
||||
if (_isApplyingSelectedRecord) return;
|
||||
if (SelectedRecentWeightRecord != null)
|
||||
{
|
||||
ClearFormByDirectionSwitch();
|
||||
}
|
||||
_ = LoadRecentWeightRecordsAsync(PlateNumber);
|
||||
}
|
||||
}
|
||||
public string CaptureGrossButtonText => "采集毛重";
|
||||
public string CaptureTareButtonText => "采集皮重";
|
||||
|
||||
private string? _plateNumber;
|
||||
public string? PlateNumber
|
||||
{
|
||||
get => _plateNumber;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _plateNumber, value)) return;
|
||||
if (_isApplyingSelectedRecord) return;
|
||||
_ = LoadRecentWeightRecordsAsync(value);
|
||||
var trimmed = value?.Trim() ?? string.Empty;
|
||||
if (trimmed.Length >= 6)
|
||||
_ = LookupVehicleByPlateAsync(trimmed);
|
||||
else
|
||||
ClearVehicleMatch();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 发货/收货单位(含选择器状态) ───
|
||||
|
||||
private MesXslSupplier? _selectedSupplier;
|
||||
private MesXslCustomer? _selectedCustomer;
|
||||
|
||||
public bool HasSelectedSupplier => _selectedSupplier != null;
|
||||
public bool HasSelectedCustomer => _selectedCustomer != null;
|
||||
|
||||
public string SenderUnitDisplay => _selectedSupplier != null
|
||||
? $"[{_selectedSupplier.SupplierCode}] {_selectedSupplier.SupplierName}"
|
||||
: (!string.IsNullOrWhiteSpace(_senderUnit) ? _senderUnit : "点击右侧「选择」从供应商列表中选取");
|
||||
|
||||
public string ReceiverUnitDisplay => _selectedCustomer != null
|
||||
? $"[{_selectedCustomer.CustomerCode}] {_selectedCustomer.CustomerName}"
|
||||
: (!string.IsNullOrWhiteSpace(_receiverUnit) ? _receiverUnit : "点击右侧「选择」从客户列表中选取");
|
||||
|
||||
private string? _senderUnit;
|
||||
public string? SenderUnit
|
||||
{
|
||||
get => _senderUnit;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _senderUnit, value);
|
||||
RaisePropertyChanged(nameof(SenderUnitDisplay));
|
||||
}
|
||||
}
|
||||
|
||||
private string? _receiverUnit;
|
||||
public string? ReceiverUnit
|
||||
{
|
||||
get => _receiverUnit;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _receiverUnit, value);
|
||||
RaisePropertyChanged(nameof(ReceiverUnitDisplay));
|
||||
}
|
||||
}
|
||||
|
||||
private string? _goodsName;
|
||||
public string? GoodsName { get => _goodsName; set => SetProperty(ref _goodsName, value); }
|
||||
|
||||
private double? _grossWeight;
|
||||
public double? GrossWeight
|
||||
{
|
||||
get => _grossWeight;
|
||||
set { SetProperty(ref _grossWeight, value); RecalcNetWeight(); }
|
||||
}
|
||||
|
||||
private double? _tareWeight;
|
||||
public double? TareWeight
|
||||
{
|
||||
get => _tareWeight;
|
||||
set { SetProperty(ref _tareWeight, value); RecalcNetWeight(); }
|
||||
}
|
||||
|
||||
private double? _netWeight;
|
||||
public double? NetWeight { get => _netWeight; set => SetProperty(ref _netWeight, value); }
|
||||
|
||||
public string NetWeightDisplay => NetWeight.HasValue ? $"{NetWeight.Value:N2}" : "—";
|
||||
|
||||
private string? _driverName;
|
||||
public string? DriverName { get => _driverName; set => SetProperty(ref _driverName, value); }
|
||||
|
||||
private string? _driverPhone;
|
||||
public string? DriverPhone { get => _driverPhone; set => SetProperty(ref _driverPhone, value); }
|
||||
|
||||
// ─── 字典 ───
|
||||
public ObservableCollection<KeyValuePair<string, string>> InoutDirectionOptions { get; } = new();
|
||||
public ObservableCollection<WeightRecordSimpleItem> RecentWeightRecords { get; } = new();
|
||||
private bool _isPlateNumberLocked;
|
||||
public bool IsPlateNumberLocked
|
||||
{
|
||||
get => _isPlateNumberLocked;
|
||||
set => SetProperty(ref _isPlateNumberLocked, value);
|
||||
}
|
||||
private WeightRecordSimpleItem? _selectedRecentWeightRecord;
|
||||
public WeightRecordSimpleItem? SelectedRecentWeightRecord
|
||||
{
|
||||
get => _selectedRecentWeightRecord;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _selectedRecentWeightRecord, value)) return;
|
||||
IsPlateNumberLocked = value?.Source?.Id != null;
|
||||
if (value == null) return;
|
||||
ApplySelectedRecordToForm(value);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 命令 ───
|
||||
public DelegateCommand CaptureGrossWeightCommand { get; }
|
||||
public DelegateCommand CaptureTareWeightCommand { get; }
|
||||
public DelegateCommand SaveCommand { get; }
|
||||
public DelegateCommand ClearCommand { get; }
|
||||
public DelegateCommand UseDetectedPlateCommand { get; }
|
||||
public DelegateCommand OpenSupplierPickerCommand { get; }
|
||||
public DelegateCommand OpenCustomerPickerCommand { get; }
|
||||
public DelegateCommand ClearSupplierCommand { get; }
|
||||
public DelegateCommand ClearCustomerCommand { get; }
|
||||
|
||||
public WeightRecordOperationViewModel(
|
||||
IWeightRecordService weightRecordService,
|
||||
IJeecgDictSyncService dictSyncService,
|
||||
IVehicleService vehicleService,
|
||||
IContainerExtension container,
|
||||
IRegionManager regionManager) : base(container, regionManager)
|
||||
{
|
||||
_weightRecordService = weightRecordService;
|
||||
_dictSyncService = dictSyncService;
|
||||
_vehicleService = vehicleService;
|
||||
|
||||
CaptureGrossWeightCommand = new DelegateCommand(CaptureGrossWeight, CanCaptureGrossWeight)
|
||||
.ObservesProperty(() => IsWeightStable)
|
||||
.ObservesProperty(() => GrossWeightCaptured)
|
||||
.ObservesProperty(() => TareWeightCaptured);
|
||||
CaptureTareWeightCommand = new DelegateCommand(CaptureTareWeight, CanCaptureTareWeight)
|
||||
.ObservesProperty(() => IsWeightStable)
|
||||
.ObservesProperty(() => GrossWeightCaptured)
|
||||
.ObservesProperty(() => TareWeightCaptured);
|
||||
SaveCommand = new DelegateCommand(async () => await SaveAsync());
|
||||
ClearCommand = new DelegateCommand(ClearForm);
|
||||
UseDetectedPlateCommand = new DelegateCommand(() => PlateNumber = DetectedPlate, () => HasPlate)
|
||||
.ObservesProperty(() => HasPlate);
|
||||
OpenSupplierPickerCommand = new DelegateCommand(async () => await OpenSupplierPickerAsync());
|
||||
OpenCustomerPickerCommand = new DelegateCommand(async () => await OpenCustomerPickerAsync());
|
||||
ClearSupplierCommand = new DelegateCommand(ClearSupplierSelection);
|
||||
ClearCustomerCommand = new DelegateCommand(ClearCustomerSelection);
|
||||
|
||||
_ = LoadDictOptionsAsync();
|
||||
_ = LoadRecentWeightRecordsAsync(PlateNumber);
|
||||
|
||||
// 启动串口模拟定时器(200ms 刷新一次,约 5Hz)
|
||||
_serialTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) };
|
||||
_serialTimer.Tick += OnSerialTimerTick;
|
||||
_serialTimer.Start();
|
||||
|
||||
// 模拟1秒后串口连接成功
|
||||
var connectTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||||
connectTimer.Tick += (s, e) =>
|
||||
{
|
||||
((DispatcherTimer)s!).Stop();
|
||||
IsSerialConnected = true;
|
||||
SerialStatusText = "COM3 9600bps 已连接";
|
||||
AddLog("串口连接成功 COM3");
|
||||
// 3秒后模拟第一辆车到来
|
||||
var vehicleTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(3) };
|
||||
vehicleTimer.Tick += (s2, e2) => { ((DispatcherTimer)s2!).Stop(); SimulateVehicleArrival(); };
|
||||
vehicleTimer.Start();
|
||||
};
|
||||
connectTimer.Start();
|
||||
}
|
||||
|
||||
private void OnSerialTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
if (!IsSerialConnected) return;
|
||||
|
||||
if (_vehiclePresent)
|
||||
{
|
||||
// 车辆在磅上:在基准重量附近波动
|
||||
var noise = (_rnd.NextDouble() - 0.5) * 80;
|
||||
CurrentWeight = Math.Max(0, _baseWeight + noise);
|
||||
|
||||
// 稳定检测:连续10帧波动小于30kg视为稳定
|
||||
if (Math.Abs(noise) < 30) _stableCountdown = Math.Min(_stableCountdown + 1, 10);
|
||||
else _stableCountdown = Math.Max(_stableCountdown - 2, 0);
|
||||
IsWeightStable = _stableCountdown >= 8;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 无车辆:重量归零并稳定
|
||||
CurrentWeight = Math.Max(0, CurrentWeight - 500);
|
||||
if (CurrentWeight < 50) { CurrentWeight = 0; IsWeightStable = true; }
|
||||
else IsWeightStable = false;
|
||||
}
|
||||
|
||||
// 车辆检测冷却计数
|
||||
if (_vehicleDetectCooldown > 0) _vehicleDetectCooldown--;
|
||||
}
|
||||
|
||||
private void SimulateVehicleArrival()
|
||||
{
|
||||
_vehiclePresent = true;
|
||||
_stableCountdown = 0;
|
||||
_baseWeight = _rnd.Next(18000, 65000); // 18~65吨随机
|
||||
AddLog("检测到车辆入场");
|
||||
|
||||
// 模拟摄像头识别车牌(2秒后回传)
|
||||
var plateTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
|
||||
plateTimer.Tick += (s, e) =>
|
||||
{
|
||||
((DispatcherTimer)s!).Stop();
|
||||
var plates = new[] { "粤A88888", "粤B12345", "粤C98765", "湘A66666", "川B55555" };
|
||||
DetectedPlate = plates[_rnd.Next(plates.Length)];
|
||||
HasPlate = true;
|
||||
AddLog($"车牌识别:{DetectedPlate}");
|
||||
};
|
||||
plateTimer.Start();
|
||||
|
||||
// 模拟15秒后车辆离开(可手动操作)
|
||||
var leaveTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) };
|
||||
leaveTimer.Tick += (s, e) =>
|
||||
{
|
||||
((DispatcherTimer)s!).Stop();
|
||||
if (_vehiclePresent) SimulateVehicleLeave();
|
||||
};
|
||||
leaveTimer.Start();
|
||||
}
|
||||
|
||||
private void SimulateVehicleLeave()
|
||||
{
|
||||
_vehiclePresent = false;
|
||||
_stableCountdown = 0;
|
||||
HasPlate = false;
|
||||
AddLog("车辆离场");
|
||||
|
||||
// 8秒后下一辆车
|
||||
if (_vehicleDetectCooldown <= 0)
|
||||
{
|
||||
_vehicleDetectCooldown = 40; // 8秒 / 200ms = 40帧
|
||||
var nextTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(8) };
|
||||
nextTimer.Tick += (s, e) => { ((DispatcherTimer)s!).Stop(); SimulateVehicleArrival(); };
|
||||
nextTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureGrossWeight()
|
||||
{
|
||||
var weight = Math.Round(CurrentWeight, 2);
|
||||
GrossWeight = weight;
|
||||
GrossWeightCaptured = true;
|
||||
AddLog($"采集毛重:{GrossWeight:N2} kg");
|
||||
}
|
||||
|
||||
private void CaptureTareWeight()
|
||||
{
|
||||
var weight = Math.Round(CurrentWeight, 2);
|
||||
TareWeight = weight;
|
||||
TareWeightCaptured = true;
|
||||
AddLog($"采集皮重:{TareWeight:N2} kg");
|
||||
}
|
||||
|
||||
private bool CanCaptureGrossWeight()
|
||||
{
|
||||
return IsWeightStable && !GrossWeightCaptured;
|
||||
}
|
||||
|
||||
private bool CanCaptureTareWeight()
|
||||
{
|
||||
return IsWeightStable && !TareWeightCaptured;
|
||||
}
|
||||
|
||||
private void RecalcNetWeight()
|
||||
{
|
||||
if (GrossWeight.HasValue && TareWeight.HasValue)
|
||||
{
|
||||
NetWeight = Math.Round(GrossWeight.Value - TareWeight.Value, 2);
|
||||
RaisePropertyChanged(nameof(NetWeightDisplay));
|
||||
}
|
||||
else
|
||||
{
|
||||
NetWeight = null;
|
||||
RaisePropertyChanged(nameof(NetWeightDisplay));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(PlateNumber))
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Warning("车牌号不能为空!");
|
||||
return;
|
||||
}
|
||||
var selectedSource = SelectedRecentWeightRecord?.Source;
|
||||
var isCompleteSelectedRecord = !string.IsNullOrWhiteSpace(selectedSource?.Id);
|
||||
|
||||
var isOutbound = string.Equals(InoutDirection, "2", StringComparison.Ordinal);
|
||||
if (!isCompleteSelectedRecord)
|
||||
{
|
||||
if (isOutbound && !TareWeight.HasValue)
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Warning("出厂流程请先采集皮重!");
|
||||
return;
|
||||
}
|
||||
if (!isOutbound && !GrossWeight.HasValue)
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Warning("进厂流程请先采集毛重!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var needSecondWeightCaptured = isOutbound ? GrossWeight.HasValue : TareWeight.HasValue;
|
||||
if (!needSecondWeightCaptured)
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Warning(isOutbound
|
||||
? "当前为已称皮重单据补毛重,必须先采集毛重!"
|
||||
: "当前为已称毛重单据补皮重,必须先采集皮重!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var entity = new MesXslWeightRecord
|
||||
{
|
||||
Id = selectedSource?.Id,
|
||||
BillNo = selectedSource?.BillNo,
|
||||
WeighDate = WeighDate,
|
||||
InoutDirection = InoutDirection,
|
||||
PlateNumber = PlateNumber,
|
||||
SenderUnit = SenderUnit,
|
||||
ReceiverUnit = ReceiverUnit,
|
||||
GoodsName = GoodsName,
|
||||
GrossWeight = GrossWeight,
|
||||
TareWeight = TareWeight,
|
||||
NetWeight = NetWeight,
|
||||
DriverName = DriverName,
|
||||
DriverPhone = DriverPhone
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var ok = isCompleteSelectedRecord
|
||||
? await _weightRecordService.EditAsync(entity)
|
||||
: await _weightRecordService.AddAsync(entity);
|
||||
if (ok)
|
||||
{
|
||||
if (isCompleteSelectedRecord)
|
||||
{
|
||||
Growl.Success("皮重回填成功,单据已更新为称重完成!");
|
||||
AddLog(isOutbound
|
||||
? $"更新成功:{entity.BillNo ?? PlateNumber},毛重 {GrossWeight:N2} kg"
|
||||
: $"更新成功:{entity.BillNo ?? PlateNumber},皮重 {TareWeight:N2} kg");
|
||||
}
|
||||
else
|
||||
{
|
||||
Growl.Success("磅单保存成功!");
|
||||
AddLog($"保存成功:{PlateNumber},净重 {NetWeight:N2} kg");
|
||||
}
|
||||
_ = UpsertVehicleAsync();
|
||||
await LoadRecentWeightRecordsAsync(PlateNumber);
|
||||
// 保存后重置重量相关字段,保留日期/方向
|
||||
GrossWeight = null; TareWeight = null; NetWeight = null;
|
||||
GrossWeightCaptured = false; TareWeightCaptured = false;
|
||||
SelectedRecentWeightRecord = null;
|
||||
IsPlateNumberLocked = false;
|
||||
PlateNumber = null; DetectedPlate = string.Empty; HasPlate = false;
|
||||
SenderUnit = null; ReceiverUnit = null; GoodsName = null;
|
||||
DriverName = null; DriverPhone = null;
|
||||
ClearSupplierSelection();
|
||||
ClearCustomerSelection();
|
||||
ClearVehicleMatch();
|
||||
RaisePropertyChanged(nameof(NetWeightDisplay));
|
||||
}
|
||||
else
|
||||
{
|
||||
Growl.Error("磅单保存失败!");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Growl.Error($"保存失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearForm()
|
||||
{
|
||||
var confirm = System.Windows.MessageBox.Show(
|
||||
"确认清空所有填写内容?", "确认清空",
|
||||
System.Windows.MessageBoxButton.OKCancel,
|
||||
System.Windows.MessageBoxImage.Question);
|
||||
if (confirm != System.Windows.MessageBoxResult.OK) return;
|
||||
|
||||
WeighDate = DateTime.Today;
|
||||
InoutDirection = "1";
|
||||
PlateNumber = null; SenderUnit = null; ReceiverUnit = null;
|
||||
GoodsName = null; GrossWeight = null; TareWeight = null; NetWeight = null;
|
||||
DriverName = null; DriverPhone = null;
|
||||
GrossWeightCaptured = false; TareWeightCaptured = false;
|
||||
DetectedPlate = string.Empty; HasPlate = false;
|
||||
SelectedRecentWeightRecord = null;
|
||||
IsPlateNumberLocked = false;
|
||||
ClearSupplierSelection();
|
||||
ClearCustomerSelection();
|
||||
ClearVehicleMatch();
|
||||
RaisePropertyChanged(nameof(NetWeightDisplay));
|
||||
AddLog("表单已清空");
|
||||
}
|
||||
|
||||
private async Task OpenSupplierPickerAsync()
|
||||
{
|
||||
SupplierPickerDialogViewModel? pickerVm = null;
|
||||
bool confirmed;
|
||||
try
|
||||
{
|
||||
confirmed = await HandyControl.Controls.Dialog.Show<SupplierPickerDialogView>()
|
||||
.Initialize<SupplierPickerDialogViewModel>(vm => { pickerVm = vm; })
|
||||
.GetResultAsync<bool>();
|
||||
}
|
||||
catch { return; }
|
||||
|
||||
if (!confirmed || pickerVm?.SelectedSupplier == null) return;
|
||||
var supplier = pickerVm.SelectedSupplier;
|
||||
_selectedSupplier = supplier;
|
||||
SenderUnit = supplier.SupplierShortName ?? supplier.SupplierName;
|
||||
RaisePropertyChanged(nameof(SenderUnitDisplay));
|
||||
RaisePropertyChanged(nameof(HasSelectedSupplier));
|
||||
AddLog($"已选发货单位:{supplier.SupplierName}");
|
||||
}
|
||||
|
||||
private async Task OpenCustomerPickerAsync()
|
||||
{
|
||||
CustomerPickerDialogViewModel? pickerVm = null;
|
||||
bool confirmed;
|
||||
try
|
||||
{
|
||||
confirmed = await HandyControl.Controls.Dialog.Show<CustomerPickerDialogView>()
|
||||
.Initialize<CustomerPickerDialogViewModel>(vm => { pickerVm = vm; })
|
||||
.GetResultAsync<bool>();
|
||||
}
|
||||
catch { return; }
|
||||
|
||||
if (!confirmed || pickerVm?.SelectedCustomer == null) return;
|
||||
var customer = pickerVm.SelectedCustomer;
|
||||
_selectedCustomer = customer;
|
||||
ReceiverUnit = customer.CustomerShortName ?? customer.CustomerName;
|
||||
RaisePropertyChanged(nameof(ReceiverUnitDisplay));
|
||||
RaisePropertyChanged(nameof(HasSelectedCustomer));
|
||||
AddLog($"已选收货单位:{customer.CustomerName}");
|
||||
}
|
||||
|
||||
private void ClearSupplierSelection()
|
||||
{
|
||||
_selectedSupplier = null;
|
||||
SenderUnit = null;
|
||||
RaisePropertyChanged(nameof(SenderUnitDisplay));
|
||||
RaisePropertyChanged(nameof(HasSelectedSupplier));
|
||||
}
|
||||
|
||||
private void ClearCustomerSelection()
|
||||
{
|
||||
_selectedCustomer = null;
|
||||
ReceiverUnit = null;
|
||||
RaisePropertyChanged(nameof(ReceiverUnitDisplay));
|
||||
RaisePropertyChanged(nameof(HasSelectedCustomer));
|
||||
}
|
||||
|
||||
private void AddLog(string message)
|
||||
{
|
||||
var entry = $"{DateTime.Now:HH:mm:ss} {message}";
|
||||
OperationLogs.Insert(0, entry);
|
||||
while (OperationLogs.Count > 8) OperationLogs.RemoveAt(OperationLogs.Count - 1);
|
||||
}
|
||||
|
||||
private async Task LoadDictOptionsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var options = await _dictSyncService.GetDictOptionsAsync("xslmes_inout_direction");
|
||||
InoutDirectionOptions.Clear();
|
||||
foreach (var item in options) InoutDirectionOptions.Add(item);
|
||||
if (InoutDirectionOptions.Count == 0) AddDefaultDirectionOptions();
|
||||
}
|
||||
catch { AddDefaultDirectionOptions(); }
|
||||
}
|
||||
|
||||
private void AddDefaultDirectionOptions()
|
||||
{
|
||||
InoutDirectionOptions.Clear();
|
||||
InoutDirectionOptions.Add(new KeyValuePair<string, string>("进厂", "1"));
|
||||
InoutDirectionOptions.Add(new KeyValuePair<string, string>("出厂", "2"));
|
||||
}
|
||||
|
||||
private async Task LoadRecentWeightRecordsAsync(string? plateNumber)
|
||||
{
|
||||
try
|
||||
{
|
||||
var plate = (plateNumber ?? string.Empty).Trim();
|
||||
RecentWeightRecords.Clear();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(plate))
|
||||
{
|
||||
SelectedRecentWeightRecord = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 按进出方向查询当天待补称单据:进厂=已称毛重,出厂=已称皮重。
|
||||
var page = await _weightRecordService.PageAsync(1, 100, filterPlateNumber: plate);
|
||||
var today = DateTime.Today;
|
||||
var isOutbound = string.Equals(InoutDirection, "2", StringComparison.Ordinal);
|
||||
var candidates = page.Records
|
||||
.Where(r =>
|
||||
string.Equals((r.PlateNumber ?? string.Empty).Trim(), plate, StringComparison.OrdinalIgnoreCase) &&
|
||||
r.WeighDate.HasValue &&
|
||||
r.WeighDate.Value.Date == today &&
|
||||
(isOutbound
|
||||
? (r.TareWeight.HasValue && !r.GrossWeight.HasValue)
|
||||
: (r.GrossWeight.HasValue && !r.TareWeight.HasValue)))
|
||||
.OrderByDescending(r => r.CreateTime ?? DateTime.MinValue)
|
||||
.ToList();
|
||||
|
||||
foreach (var record in candidates)
|
||||
{
|
||||
RecentWeightRecords.Add(new WeightRecordSimpleItem
|
||||
{
|
||||
Source = record,
|
||||
BillNo = record.BillNo ?? "-",
|
||||
PlateNumber = record.PlateNumber ?? "-",
|
||||
FirstWeightDisplay = isOutbound
|
||||
? (record.TareWeight.HasValue ? $"{record.TareWeight.Value:N2}" : "-")
|
||||
: (record.GrossWeight.HasValue ? $"{record.GrossWeight.Value:N2}" : "-")
|
||||
});
|
||||
}
|
||||
|
||||
SelectedRecentWeightRecord = null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 查询失败时保持现有列表,不中断主流程
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplySelectedRecordToForm(WeightRecordSimpleItem selected)
|
||||
{
|
||||
if (selected.Source == null) return;
|
||||
|
||||
// 带入历史磅单时,不触发车辆重新查询
|
||||
_matchedVehicle = null;
|
||||
_lastLookedUpPlate = selected.Source.PlateNumber?.Trim();
|
||||
VehicleLookupStatus = "None";
|
||||
|
||||
_isApplyingSelectedRecord = true;
|
||||
try
|
||||
{
|
||||
// 自动带入基础信息,并按进出方向保留"待补称"的第二次称重位。
|
||||
WeighDate = selected.Source.WeighDate ?? DateTime.Today;
|
||||
InoutDirection = selected.Source.InoutDirection;
|
||||
PlateNumber = selected.Source.PlateNumber;
|
||||
_selectedSupplier = null;
|
||||
_selectedCustomer = null;
|
||||
SenderUnit = selected.Source.SenderUnit;
|
||||
ReceiverUnit = selected.Source.ReceiverUnit;
|
||||
RaisePropertyChanged(nameof(SenderUnitDisplay));
|
||||
RaisePropertyChanged(nameof(ReceiverUnitDisplay));
|
||||
RaisePropertyChanged(nameof(HasSelectedSupplier));
|
||||
RaisePropertyChanged(nameof(HasSelectedCustomer));
|
||||
GoodsName = selected.Source.GoodsName;
|
||||
DriverName = selected.Source.DriverName;
|
||||
DriverPhone = selected.Source.DriverPhone;
|
||||
|
||||
var isOutbound = string.Equals(InoutDirection, "2", StringComparison.Ordinal);
|
||||
if (isOutbound)
|
||||
{
|
||||
TareWeight = selected.Source.TareWeight;
|
||||
TareWeightCaptured = TareWeight.HasValue;
|
||||
GrossWeight = null;
|
||||
GrossWeightCaptured = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
GrossWeight = selected.Source.GrossWeight;
|
||||
GrossWeightCaptured = GrossWeight.HasValue;
|
||||
TareWeight = null;
|
||||
TareWeightCaptured = false;
|
||||
}
|
||||
AddLog($"已带入榜单:{selected.BillNo}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isApplyingSelectedRecord = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearFormByDirectionSwitch()
|
||||
{
|
||||
// 已选榜单后切换进出方向,清空数据防止串单。
|
||||
WeighDate = DateTime.Today;
|
||||
PlateNumber = null;
|
||||
SenderUnit = null;
|
||||
ReceiverUnit = null;
|
||||
GoodsName = null;
|
||||
DriverName = null;
|
||||
DriverPhone = null;
|
||||
GrossWeight = null;
|
||||
TareWeight = null;
|
||||
NetWeight = null;
|
||||
GrossWeightCaptured = false;
|
||||
TareWeightCaptured = false;
|
||||
SelectedRecentWeightRecord = null;
|
||||
IsPlateNumberLocked = false;
|
||||
ClearSupplierSelection();
|
||||
ClearCustomerSelection();
|
||||
ClearVehicleMatch();
|
||||
RaisePropertyChanged(nameof(NetWeightDisplay));
|
||||
AddLog("已切换进出方向,当前表单数据已清空");
|
||||
}
|
||||
|
||||
// ─── 车辆档案查询与回写 ───
|
||||
|
||||
private async Task LookupVehicleByPlateAsync(string plate)
|
||||
{
|
||||
if (string.Equals(plate, _lastLookedUpPlate, StringComparison.OrdinalIgnoreCase)) return;
|
||||
_lastLookedUpPlate = plate;
|
||||
|
||||
VehicleLookupStatus = "Searching";
|
||||
try
|
||||
{
|
||||
var result = await _vehicleService.PageAsync(1, 1, plateNumber: plate);
|
||||
var vehicle = result.Records.FirstOrDefault(v =>
|
||||
string.Equals(v.PlateNumber?.Trim(), plate, StringComparison.OrdinalIgnoreCase)
|
||||
&& v.Status != "1");
|
||||
|
||||
if (vehicle != null)
|
||||
{
|
||||
_matchedVehicle = vehicle;
|
||||
VehicleLookupStatus = "Matched";
|
||||
ApplyVehicleToForm(vehicle);
|
||||
}
|
||||
else
|
||||
{
|
||||
_matchedVehicle = null;
|
||||
VehicleLookupStatus = "NotFound";
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
_matchedVehicle = null;
|
||||
VehicleLookupStatus = "None";
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyVehicleToForm(MesXslVehicle vehicle)
|
||||
{
|
||||
// 司机信息优先带出
|
||||
if (!string.IsNullOrWhiteSpace(vehicle.DriverName)) DriverName = vehicle.DriverName;
|
||||
if (!string.IsNullOrWhiteSpace(vehicle.DriverPhone)) DriverPhone = vehicle.DriverPhone;
|
||||
|
||||
// 根据车辆归属自动判断进出方向与发收货单位
|
||||
if (vehicle.VehicleBelong == "2") // 供应商 → 进厂
|
||||
{
|
||||
if (!_isApplyingSelectedRecord) InoutDirection = "1";
|
||||
_selectedSupplier = null;
|
||||
SenderUnit = vehicle.SupplierShortName ?? vehicle.SupplierName;
|
||||
RaisePropertyChanged(nameof(SenderUnitDisplay));
|
||||
RaisePropertyChanged(nameof(HasSelectedSupplier));
|
||||
}
|
||||
else if (vehicle.VehicleBelong == "1") // 客户 → 出厂
|
||||
{
|
||||
if (!_isApplyingSelectedRecord) InoutDirection = "2";
|
||||
_selectedCustomer = null;
|
||||
ReceiverUnit = vehicle.CustomerShortName;
|
||||
RaisePropertyChanged(nameof(ReceiverUnitDisplay));
|
||||
RaisePropertyChanged(nameof(HasSelectedCustomer));
|
||||
}
|
||||
// VehicleBelong == "3"(本公司):方向不定,不自动切换
|
||||
|
||||
AddLog($"车辆档案匹配:{vehicle.PlateNumber}({vehicle.VehicleBelongText})");
|
||||
}
|
||||
|
||||
private void ClearVehicleMatch()
|
||||
{
|
||||
_matchedVehicle = null;
|
||||
_lastLookedUpPlate = null;
|
||||
VehicleLookupStatus = "None";
|
||||
}
|
||||
|
||||
private async Task UpsertVehicleAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(PlateNumber)) return;
|
||||
try
|
||||
{
|
||||
if (_matchedVehicle != null)
|
||||
{
|
||||
// 已有档案:仅在司机信息有变动时回写
|
||||
bool driverChanged =
|
||||
!string.Equals(DriverName, _matchedVehicle.DriverName) ||
|
||||
!string.Equals(DriverPhone, _matchedVehicle.DriverPhone);
|
||||
if (!driverChanged) return;
|
||||
|
||||
await _vehicleService.EditAsync(new MesXslVehicle
|
||||
{
|
||||
Id = _matchedVehicle.Id,
|
||||
PlateNumber = _matchedVehicle.PlateNumber,
|
||||
VehicleBelong = _matchedVehicle.VehicleBelong,
|
||||
TareWeightKg = _matchedVehicle.TareWeightKg,
|
||||
LoadCapacity = _matchedVehicle.LoadCapacity,
|
||||
SupplierId = _matchedVehicle.SupplierId,
|
||||
SupplierName = _matchedVehicle.SupplierName,
|
||||
SupplierShortName = _matchedVehicle.SupplierShortName,
|
||||
CustomerIds = _matchedVehicle.CustomerIds,
|
||||
CustomerShortName = _matchedVehicle.CustomerShortName,
|
||||
DriverName = DriverName,
|
||||
DriverPhone = DriverPhone,
|
||||
Status = _matchedVehicle.Status,
|
||||
TenantId = _matchedVehicle.TenantId,
|
||||
Version = _matchedVehicle.Version,
|
||||
});
|
||||
AddLog($"车辆档案已更新:{PlateNumber}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 无档案:根据表单信息新建车辆档案
|
||||
var vehicleBelong = InoutDirection == "2" ? "1" : "2"; // 出厂→客户 进厂→供应商
|
||||
var newVehicle = new MesXslVehicle
|
||||
{
|
||||
PlateNumber = PlateNumber?.Trim(),
|
||||
VehicleBelong = vehicleBelong,
|
||||
SupplierId = _selectedSupplier?.Id,
|
||||
SupplierName = _selectedSupplier?.SupplierName,
|
||||
SupplierShortName = _selectedSupplier?.SupplierShortName,
|
||||
CustomerShortName = _selectedCustomer?.CustomerShortName
|
||||
?? (InoutDirection == "2" ? ReceiverUnit : null),
|
||||
DriverName = DriverName,
|
||||
DriverPhone = DriverPhone,
|
||||
Status = "0",
|
||||
};
|
||||
await _vehicleService.AddAsync(newVehicle);
|
||||
AddLog($"已新建车辆档案:{PlateNumber}");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 车辆回写失败不影响磅单保存,静默处理
|
||||
}
|
||||
}
|
||||
|
||||
protected override void CleanUp()
|
||||
{
|
||||
base.CleanUp();
|
||||
_serialTimer.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
public class WeightRecordSimpleItem
|
||||
{
|
||||
public MesXslWeightRecord? Source { get; set; }
|
||||
public string BillNo { get; set; } = "-";
|
||||
public string PlateNumber { get; set; } = "-";
|
||||
public string FirstWeightDisplay { get; set; } = "-";
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<UserControl x:Class="YY.Admin.Views.WeightRecord.CustomerPickerDialogView"
|
||||
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"
|
||||
Width="640">
|
||||
|
||||
<Grid Background="{DynamicResource ThirdlyRegionBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="320"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<hc:SimplePanel Margin="20,16,20,12">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Border Width="32" Height="32" CornerRadius="6" Background="#52c41a" Margin="0,0,10,0">
|
||||
<md:PackIcon Kind="OfficeBuildingMarker" Width="18" Height="18" Foreground="White"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<TextBlock Text="选择客户(收货单位)" FontSize="16" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<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>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<Border Grid.Row="1" Background="{DynamicResource RegionBrush}" Padding="16,10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="80"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<hc:TextBox Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Placeholder="输入客户名称搜索..."
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,8,0">
|
||||
<hc:TextBox.InputBindings>
|
||||
<KeyBinding Key="Enter" Command="{Binding SearchCommand}"/>
|
||||
</hc:TextBox.InputBindings>
|
||||
</hc:TextBox>
|
||||
<Button Grid.Column="1" Command="{Binding SearchCommand}"
|
||||
IsEnabled="{Binding IsLoading, Converter={StaticResource Boolean2BooleanReConverter}}"
|
||||
Style="{StaticResource ButtonPrimary}" Height="32">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="Magnify" Width="14" Height="14" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||
<TextBlock Text="搜索" FontSize="13" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 客户列表 -->
|
||||
<DataGrid x:Name="CustomersGrid"
|
||||
Grid.Row="2"
|
||||
Margin="16,8,16,0"
|
||||
ItemsSource="{Binding Customers}"
|
||||
SelectedItem="{Binding SelectedCustomer}"
|
||||
AutoGenerateColumns="False"
|
||||
CanUserAddRows="False"
|
||||
CanUserDeleteRows="False"
|
||||
CanUserResizeRows="False"
|
||||
HeadersVisibility="Column"
|
||||
SelectionMode="Single"
|
||||
SelectionUnit="FullRow"
|
||||
IsReadOnly="True"
|
||||
GridLinesVisibility="Horizontal"
|
||||
HorizontalGridLinesBrush="#FFEDEFF2"
|
||||
VerticalGridLinesBrush="Transparent"
|
||||
Background="White"
|
||||
RowBackground="White"
|
||||
AlternatingRowBackground="#FAFCFF"
|
||||
RowHeight="32"
|
||||
ColumnHeaderHeight="34"
|
||||
RowHeaderWidth="0"
|
||||
MouseDoubleClick="CustomersGrid_MouseDoubleClick">
|
||||
<!-- 覆盖系统高亮刷,防止失焦后选中行变白 -->
|
||||
<DataGrid.Resources>
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="#EAF3FF"/>
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="#1F1F1F"/>
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="#EAF3FF"/>
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}" Color="#1F1F1F"/>
|
||||
</DataGrid.Resources>
|
||||
<DataGrid.ColumnHeaderStyle>
|
||||
<Style TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Background" Value="#F5F7FA"/>
|
||||
<Setter Property="Foreground" Value="#606266"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Padding" Value="8,0"/>
|
||||
<Setter Property="BorderBrush" Value="#EBEEF5"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1"/>
|
||||
</Style>
|
||||
</DataGrid.ColumnHeaderStyle>
|
||||
<DataGrid.RowStyle>
|
||||
<Style TargetType="DataGridRow">
|
||||
<Setter Property="Background" Value="White"/>
|
||||
<Setter Property="Foreground" Value="#262626"/>
|
||||
<Setter Property="BorderBrush" Value="#FFEDEFF2"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="#EAF3FF"/>
|
||||
<Setter Property="Foreground" Value="#1F1F1F"/>
|
||||
<Setter Property="BorderBrush" Value="#D6E8FF"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#F5F9FF"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</DataGrid.RowStyle>
|
||||
<DataGrid.CellStyle>
|
||||
<Style TargetType="DataGridCell">
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="8,0"/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
</Style>
|
||||
</DataGrid.CellStyle>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="客户编码" Binding="{Binding CustomerCode}" Width="130"/>
|
||||
<DataGridTextColumn Header="客户名称" Binding="{Binding CustomerName}" Width="*"/>
|
||||
<DataGridTextColumn Header="简称" Binding="{Binding CustomerShortName}" Width="100"/>
|
||||
<DataGridTextColumn Header="状态" Binding="{Binding StatusText}" Width="60"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<Border Grid.Row="3" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,1,0,0"
|
||||
Padding="16,12">
|
||||
<Grid>
|
||||
<!-- 已选提示 -->
|
||||
<TextBlock VerticalAlignment="Center" FontSize="13" MaxWidth="320"
|
||||
TextTrimming="CharacterEllipsis" HorizontalAlignment="Left"
|
||||
Text="{Binding SelectedCustomerDisplay}">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource SecondaryTextBrush}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasSelectedCustomer}" Value="True">
|
||||
<Setter Property="Foreground" Value="#52c41a"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<!-- 按钮 -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Content="取 消" Command="{Binding CancelCommand}"
|
||||
Style="{StaticResource ButtonDefault}" Width="88" Margin="0,0,12,0" Height="34"/>
|
||||
<Button Content="确认选择" Command="{Binding ConfirmCommand}"
|
||||
Style="{StaticResource ButtonSuccess}" Width="100" Height="34"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using YY.Admin.ViewModels.WeightRecord;
|
||||
|
||||
namespace YY.Admin.Views.WeightRecord;
|
||||
|
||||
public partial class CustomerPickerDialogView : UserControl
|
||||
{
|
||||
public CustomerPickerDialogView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void CustomersGrid_MouseDoubleClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (DataContext is CustomerPickerDialogViewModel vm && vm.ConfirmCommand.CanExecute())
|
||||
vm.ConfirmCommand.Execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<UserControl x:Class="YY.Admin.Views.WeightRecord.SupplierPickerDialogView"
|
||||
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"
|
||||
Width="640">
|
||||
|
||||
<Grid Background="{DynamicResource ThirdlyRegionBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="320"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<hc:SimplePanel Margin="20,16,20,12">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Border Width="32" Height="32" CornerRadius="6" Background="{DynamicResource PrimaryBrush}" Margin="0,0,10,0">
|
||||
<md:PackIcon Kind="TruckDelivery" Width="18" Height="18" Foreground="White"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<TextBlock Text="选择供应商(发货单位)" FontSize="16" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<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>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<Border Grid.Row="1" Background="{DynamicResource RegionBrush}" Padding="16,10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="80"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<hc:TextBox Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Placeholder="输入供应商名称搜索..."
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,8,0">
|
||||
<hc:TextBox.InputBindings>
|
||||
<KeyBinding Key="Enter" Command="{Binding SearchCommand}"/>
|
||||
</hc:TextBox.InputBindings>
|
||||
</hc:TextBox>
|
||||
<Button Grid.Column="1" Command="{Binding SearchCommand}"
|
||||
IsEnabled="{Binding IsLoading, Converter={StaticResource Boolean2BooleanReConverter}}"
|
||||
Style="{StaticResource ButtonPrimary}" Height="32">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="Magnify" Width="14" Height="14" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||
<TextBlock Text="搜索" FontSize="13" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 供应商列表 -->
|
||||
<DataGrid x:Name="SuppliersGrid"
|
||||
Grid.Row="2"
|
||||
Margin="16,8,16,0"
|
||||
ItemsSource="{Binding Suppliers}"
|
||||
SelectedItem="{Binding SelectedSupplier}"
|
||||
AutoGenerateColumns="False"
|
||||
CanUserAddRows="False"
|
||||
CanUserDeleteRows="False"
|
||||
CanUserResizeRows="False"
|
||||
HeadersVisibility="Column"
|
||||
SelectionMode="Single"
|
||||
SelectionUnit="FullRow"
|
||||
IsReadOnly="True"
|
||||
GridLinesVisibility="Horizontal"
|
||||
HorizontalGridLinesBrush="#FFEDEFF2"
|
||||
VerticalGridLinesBrush="Transparent"
|
||||
Background="White"
|
||||
RowBackground="White"
|
||||
AlternatingRowBackground="#FAFCFF"
|
||||
RowHeight="32"
|
||||
ColumnHeaderHeight="34"
|
||||
RowHeaderWidth="0"
|
||||
MouseDoubleClick="SuppliersGrid_MouseDoubleClick">
|
||||
<!-- 覆盖系统高亮刷,防止失焦后选中行变白 -->
|
||||
<DataGrid.Resources>
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="#EAF3FF"/>
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="#1F1F1F"/>
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="#EAF3FF"/>
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}" Color="#1F1F1F"/>
|
||||
</DataGrid.Resources>
|
||||
<DataGrid.ColumnHeaderStyle>
|
||||
<Style TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Background" Value="#F5F7FA"/>
|
||||
<Setter Property="Foreground" Value="#606266"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Padding" Value="8,0"/>
|
||||
<Setter Property="BorderBrush" Value="#EBEEF5"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1"/>
|
||||
</Style>
|
||||
</DataGrid.ColumnHeaderStyle>
|
||||
<DataGrid.RowStyle>
|
||||
<Style TargetType="DataGridRow">
|
||||
<Setter Property="Background" Value="White"/>
|
||||
<Setter Property="Foreground" Value="#262626"/>
|
||||
<Setter Property="BorderBrush" Value="#FFEDEFF2"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="#EAF3FF"/>
|
||||
<Setter Property="Foreground" Value="#1F1F1F"/>
|
||||
<Setter Property="BorderBrush" Value="#D6E8FF"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#F5F9FF"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</DataGrid.RowStyle>
|
||||
<DataGrid.CellStyle>
|
||||
<Style TargetType="DataGridCell">
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="8,0"/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
</Style>
|
||||
</DataGrid.CellStyle>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="供应商编码" Binding="{Binding SupplierCode}" Width="130"/>
|
||||
<DataGridTextColumn Header="供应商名称" Binding="{Binding SupplierName}" Width="*"/>
|
||||
<DataGridTextColumn Header="简称" Binding="{Binding SupplierShortName}" Width="100"/>
|
||||
<DataGridTextColumn Header="状态" Binding="{Binding StatusText}" Width="60"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<Border Grid.Row="3" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,1,0,0"
|
||||
Padding="16,12">
|
||||
<Grid>
|
||||
<!-- 已选提示 -->
|
||||
<TextBlock VerticalAlignment="Center" FontSize="13" MaxWidth="320"
|
||||
TextTrimming="CharacterEllipsis" HorizontalAlignment="Left"
|
||||
Text="{Binding SelectedSupplierDisplay}">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource SecondaryTextBrush}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasSelectedSupplier}" Value="True">
|
||||
<Setter Property="Foreground" Value="{DynamicResource PrimaryBrush}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<!-- 按钮 -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Content="取 消" Command="{Binding CancelCommand}"
|
||||
Style="{StaticResource ButtonDefault}" Width="88" Margin="0,0,12,0" Height="34"/>
|
||||
<Button Content="确认选择" Command="{Binding ConfirmCommand}"
|
||||
Style="{StaticResource ButtonPrimary}" Width="100" Height="34"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using YY.Admin.ViewModels.WeightRecord;
|
||||
|
||||
namespace YY.Admin.Views.WeightRecord;
|
||||
|
||||
public partial class SupplierPickerDialogView : UserControl
|
||||
{
|
||||
public SupplierPickerDialogView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void SuppliersGrid_MouseDoubleClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (DataContext is SupplierPickerDialogViewModel vm && vm.ConfirmCommand.CanExecute())
|
||||
vm.ConfirmCommand.Execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<UserControl x:Class="YY.Admin.Views.WeightRecord.WeightRecordEditDialogView"
|
||||
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="720"
|
||||
MinHeight="500">
|
||||
|
||||
<Grid Background="{DynamicResource ThirdlyRegionBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<hc:SimplePanel Margin="20">
|
||||
<TextBlock FontSize="18" Foreground="{DynamicResource PrimaryTextBrush}" Text="{Binding DialogTitle}" HorizontalAlignment="Left" VerticalAlignment="Top"/>
|
||||
<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="Top" Margin="0,4,4,0"/>
|
||||
</hc:SimplePanel>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<hc:ScrollViewer Grid.Row="1" IsInertiaEnabled="True">
|
||||
<StackPanel Margin="20,0,20,0">
|
||||
<hc:Row Gutter="10">
|
||||
|
||||
<!-- 磅单号 -->
|
||||
<hc:Col Span="12">
|
||||
<hc:TextBox Text="{Binding Record.BillNo, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Title="磅单号"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="留空则自动生成"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,16"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 称重日期 -->
|
||||
<hc:Col Span="12">
|
||||
<hc:DatePicker SelectedDate="{Binding Record.WeighDate}"
|
||||
hc:InfoElement.Title="称重日期"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Necessary="True"
|
||||
hc:InfoElement.Symbol="*"
|
||||
Margin="0,0,0,16"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 进出方向 -->
|
||||
<hc:Col Span="12">
|
||||
<hc:ComboBox SelectedValuePath="Value"
|
||||
DisplayMemberPath="Key"
|
||||
ItemsSource="{Binding InoutDirectionOptions}"
|
||||
SelectedValue="{Binding Record.InoutDirection}"
|
||||
hc:InfoElement.Title="进出方向"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Necessary="True"
|
||||
hc:InfoElement.Symbol="*"
|
||||
Margin="0,0,0,16"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 车牌号 -->
|
||||
<hc:Col Span="12">
|
||||
<hc:TextBox Text="{Binding Record.PlateNumber, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Title="车牌号"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入车牌号"
|
||||
hc:InfoElement.Necessary="True"
|
||||
hc:InfoElement.Symbol="*"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,16"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 发货单位 -->
|
||||
<hc:Col Span="12">
|
||||
<hc:TextBox Text="{Binding Record.SenderUnit, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Title="发货单位"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="进厂时为供应商名称"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,16"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 收货单位 -->
|
||||
<hc:Col Span="12">
|
||||
<hc:TextBox Text="{Binding Record.ReceiverUnit, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Title="收货单位"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="出厂时为客户简称"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,16"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 货物名称 -->
|
||||
<hc:Col Span="12">
|
||||
<hc:TextBox Text="{Binding Record.GoodsName, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Title="货物名称"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入货物名称"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,16"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 毛重 -->
|
||||
<hc:Col Span="12">
|
||||
<hc:NumericUpDown Value="{Binding Record.GrossWeight}"
|
||||
Minimum="0"
|
||||
DecimalPlaces="2"
|
||||
Style="{StaticResource NumericUpDownPlus}"
|
||||
hc:InfoElement.Title="毛重(KG)"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入毛重"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,16"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 皮重 -->
|
||||
<hc:Col Span="12">
|
||||
<hc:NumericUpDown Value="{Binding Record.TareWeight}"
|
||||
Minimum="0"
|
||||
DecimalPlaces="2"
|
||||
Style="{StaticResource NumericUpDownPlus}"
|
||||
hc:InfoElement.Title="皮重(KG)"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入皮重"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,16"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 净重(只读) -->
|
||||
<hc:Col Span="12">
|
||||
<hc:NumericUpDown Value="{Binding Record.NetWeight}"
|
||||
Minimum="0"
|
||||
DecimalPlaces="2"
|
||||
Style="{StaticResource NumericUpDownPlus}"
|
||||
IsEnabled="True"
|
||||
hc:InfoElement.Title="净重(KG)"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="自动计算"
|
||||
Margin="0,0,0,16"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 司机 -->
|
||||
<hc:Col Span="12">
|
||||
<hc:TextBox Text="{Binding Record.DriverName, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Title="司机姓名"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入司机姓名"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,16"/>
|
||||
</hc:Col>
|
||||
|
||||
<!-- 手机号 -->
|
||||
<hc:Col Span="12">
|
||||
<hc:TextBox Text="{Binding Record.DriverPhone, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Title="手机号码"
|
||||
hc:InfoElement.TitleWidth="80"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="请输入手机号"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,16"/>
|
||||
</hc:Col>
|
||||
|
||||
</hc:Row>
|
||||
</StackPanel>
|
||||
</hc:ScrollViewer>
|
||||
|
||||
<!-- 按钮区域 -->
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center" Margin="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,8 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace YY.Admin.Views.WeightRecord;
|
||||
|
||||
public partial class WeightRecordEditDialogView : UserControl
|
||||
{
|
||||
public WeightRecordEditDialogView() => InitializeComponent();
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<UserControl x:Class="YY.Admin.Views.WeightRecord.WeightRecordListView"
|
||||
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 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:TextBox.InputBindings>
|
||||
<KeyBinding Key="Enter" Command="{Binding SearchCommand}"/>
|
||||
</hc:TextBox.InputBindings>
|
||||
</hc:TextBox>
|
||||
</hc:Col>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=4, Xl=4}">
|
||||
<hc:TextBox Text="{Binding FilterPlateNumber, 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:TextBox.InputBindings>
|
||||
<KeyBinding Key="Enter" Command="{Binding SearchCommand}"/>
|
||||
</hc:TextBox.InputBindings>
|
||||
</hc:TextBox>
|
||||
</hc:Col>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=4, Xl=4}">
|
||||
<hc:ComboBox SelectedValuePath="Value"
|
||||
DisplayMemberPath="Key"
|
||||
ItemsSource="{Binding InoutDirectionOptions}"
|
||||
SelectedValue="{Binding FilterInoutDirection}"
|
||||
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 FilterGoodsName, 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:TextBox.InputBindings>
|
||||
<KeyBinding Key="Enter" Command="{Binding SearchCommand}"/>
|
||||
</hc:TextBox.InputBindings>
|
||||
</hc:TextBox>
|
||||
</hc:Col>
|
||||
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=4, Xl=4}">
|
||||
<hc:TextBox Text="{Binding FilterDriverName, 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:TextBox.InputBindings>
|
||||
<KeyBinding Key="Enter" Command="{Binding SearchCommand}"/>
|
||||
</hc:TextBox.InputBindings>
|
||||
</hc:TextBox>
|
||||
</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 Records}"
|
||||
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 BillNo}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="130"/>
|
||||
<DataGridTextColumn Header="称重日期" Binding="{Binding WeighDate, StringFormat='yyyy-MM-dd'}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="110"/>
|
||||
<DataGridTextColumn Header="进出方向" Binding="{Binding InoutDirectionText}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="90"/>
|
||||
<DataGridTextColumn Header="单据类型" Binding="{Binding BillTypeText}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="100"/>
|
||||
<DataGridTextColumn Header="车牌号" Binding="{Binding PlateNumber}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="110"/>
|
||||
<DataGridTextColumn Header="发货单位" Binding="{Binding SenderUnit}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="140"/>
|
||||
<DataGridTextColumn Header="收货单位" Binding="{Binding ReceiverUnit}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="140"/>
|
||||
<DataGridTextColumn Header="货物名称" Binding="{Binding GoodsName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="120"/>
|
||||
<DataGridTextColumn Header="毛重(KG)" Binding="{Binding GrossWeight, StringFormat=N2}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="100"/>
|
||||
<DataGridTextColumn Header="皮重(KG)" Binding="{Binding TareWeight, StringFormat=N2}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="100"/>
|
||||
<DataGridTextColumn Header="净重(KG)" Binding="{Binding NetWeight, StringFormat=N2}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="100"/>
|
||||
<DataGridTextColumn Header="司机" Binding="{Binding DriverName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="90"/>
|
||||
<DataGridTextColumn Header="手机号" Binding="{Binding DriverPhone}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="120"/>
|
||||
<DataGridTextColumn Header="创建时间" Binding="{Binding CreateTime, StringFormat='yyyy-MM-dd HH:mm'}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="130"/>
|
||||
<DataGridTemplateColumn Header="操作" Width="150" CellStyle="{StaticResource CusOperDataGridCellStyle}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<hc:UniformSpacingPanel Spacing="5">
|
||||
<Border Style="{DynamicResource DataGridOpeButtonStyle}">
|
||||
<Border.InputBindings>
|
||||
<MouseBinding Command="{Binding DataContext.EditCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||
CommandParameter="{Binding}"
|
||||
MouseAction="LeftClick"/>
|
||||
</Border.InputBindings>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="SquareEditOutline" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="修改" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border Style="{DynamicResource DataGridOpeButtonStyle}">
|
||||
<Border.InputBindings>
|
||||
<MouseBinding Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||
CommandParameter="{Binding}"
|
||||
MouseAction="LeftClick"/>
|
||||
</Border.InputBindings>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="TrashCanOutline" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="删除" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</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,8 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace YY.Admin.Views.WeightRecord;
|
||||
|
||||
public partial class WeightRecordListView : UserControl
|
||||
{
|
||||
public WeightRecordListView() => InitializeComponent();
|
||||
}
|
||||
@@ -0,0 +1,767 @@
|
||||
<UserControl x:Class="YY.Admin.Views.WeightRecord.WeightRecordOperationView"
|
||||
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">
|
||||
|
||||
<UserControl.Resources>
|
||||
<!-- 深色工业风面板背景 -->
|
||||
<SolidColorBrush x:Key="PanelDarkBrush" Color="#1a1a2e"/>
|
||||
<SolidColorBrush x:Key="WeightGreenBrush" Color="#00e676"/>
|
||||
<SolidColorBrush x:Key="WeightOrangeBrush" Color="#ff9800"/>
|
||||
<SolidColorBrush x:Key="SerialConnectedBrush" Color="#4caf50"/>
|
||||
<SolidColorBrush x:Key="SerialDisconnectedBrush" Color="#f44336"/>
|
||||
<SolidColorBrush x:Key="SectionBorderBrush" Color="#1890ff"/>
|
||||
|
||||
<!-- 分组标题样式(左侧蓝色色条) -->
|
||||
<Style x:Key="SectionTitleStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource PrimaryTextBrush}"/>
|
||||
<Setter Property="Margin" Value="10,0,0,0"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
|
||||
<!-- 分组容器样式 -->
|
||||
<Style x:Key="SectionBorderStyle" TargetType="Border">
|
||||
<Setter Property="CornerRadius" Value="8"/>
|
||||
<Setter Property="Background" Value="{DynamicResource ThirdlyRegionBrush}"/>
|
||||
<Setter Property="Padding" Value="16"/>
|
||||
<Setter Property="Margin" Value="0,0,0,12"/>
|
||||
</Style>
|
||||
|
||||
<!-- 采集按钮样式 -->
|
||||
<Style x:Key="CaptureButtonStyle" TargetType="Button" BasedOn="{StaticResource ButtonSuccess}">
|
||||
<Setter Property="Height" Value="36"/>
|
||||
<Setter Property="Padding" Value="16,0"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
</Style>
|
||||
<Style x:Key="CaptureGrossButtonStyle" TargetType="Button" BasedOn="{StaticResource ButtonPrimary}">
|
||||
<Setter Property="Height" Value="36"/>
|
||||
<Setter Property="Padding" Value="16,0"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
</Style>
|
||||
|
||||
<!-- 已采集标签样式 -->
|
||||
<Style x:Key="CapturedTagStyle" TargetType="Border">
|
||||
<Setter Property="Background" Value="#4caf50"/>
|
||||
<Setter Property="CornerRadius" Value="4"/>
|
||||
<Setter Property="Padding" Value="10,4"/>
|
||||
<Setter Property="Height" Value="36"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<!-- 标题区 -->
|
||||
<RowDefinition Height="70"/>
|
||||
<!-- 主体区 -->
|
||||
<RowDefinition Height="*"/>
|
||||
<!-- 操作栏 -->
|
||||
<RowDefinition Height="64"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ══════════════════════ Row 0: 页面标题区 ══════════════════════ -->
|
||||
<Border Grid.Row="0" Background="{DynamicResource RegionBrush}"
|
||||
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
|
||||
<Grid Margin="20,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 左侧标题 -->
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Border Width="40" Height="40" CornerRadius="8" Background="{DynamicResource PrimaryBrush}" Margin="0,0,12,0">
|
||||
<md:PackIcon Kind="ScaleBalance" Width="22" Height="22" Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock Text="地磅称重操作" FontSize="18" FontWeight="Bold" Foreground="{DynamicResource PrimaryTextBrush}"/>
|
||||
<TextBlock Text="实时称重数据录入" FontSize="12" Foreground="{DynamicResource SecondaryTextBrush}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 右侧状态 -->
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,8,0">
|
||||
<!-- 串口状态 -->
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,20,0">
|
||||
<Ellipse Width="10" Height="10" VerticalAlignment="Center" Margin="0,0,6,0">
|
||||
<Ellipse.Style>
|
||||
<Style TargetType="Ellipse">
|
||||
<Setter Property="Fill" Value="{StaticResource SerialDisconnectedBrush}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsSerialConnected}" Value="True">
|
||||
<Setter Property="Fill" Value="{StaticResource SerialConnectedBrush}"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Ellipse.Style>
|
||||
</Ellipse>
|
||||
<TextBlock Text="{Binding SerialStatusText}" FontSize="13"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ══════════════════════ Row 1: 主体区(左监控 + 右表单) ══════════════════════ -->
|
||||
<Grid Grid.Row="1" Margin="16,12,16,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2.2*"/>
|
||||
<ColumnDefinition Width="12"/>
|
||||
<ColumnDefinition Width="3*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- ───── 左侧:实时监控面板 ───── -->
|
||||
<ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
|
||||
<!-- 实时重量显示 -->
|
||||
<Border Style="{StaticResource SectionBorderStyle}">
|
||||
<StackPanel>
|
||||
<!-- 分组标题 -->
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
|
||||
<Border Width="4" Height="18" CornerRadius="2" Background="{StaticResource SectionBorderBrush}"/>
|
||||
<TextBlock Text="实时重量" Style="{StaticResource SectionTitleStyle}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 大屏重量显示 -->
|
||||
<Border CornerRadius="12" Background="{StaticResource PanelDarkBrush}" Padding="16,20">
|
||||
<StackPanel HorizontalAlignment="Center">
|
||||
<!-- 重量数字 -->
|
||||
<TextBlock Text="{Binding CurrentWeightDisplay}" FontFamily="Consolas"
|
||||
FontSize="52" FontWeight="Bold" HorizontalAlignment="Center">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{StaticResource WeightGreenBrush}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsWeightStable}" Value="False">
|
||||
<Setter Property="Foreground" Value="{StaticResource WeightOrangeBrush}"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding IsSerialConnected}" Value="False">
|
||||
<Setter Property="Foreground" Value="#666"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<TextBlock Text="kg" FontFamily="Consolas" FontSize="22" HorizontalAlignment="Center"
|
||||
Foreground="#888" Margin="0,0,0,8"/>
|
||||
<!-- 稳定状态 -->
|
||||
<Border CornerRadius="12" Padding="12,4" HorizontalAlignment="Center">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#33ff9800"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsWeightStable}" Value="True">
|
||||
<Setter Property="Background" Value="#3300e676"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Ellipse Width="8" Height="8" VerticalAlignment="Center" Margin="0,0,6,0">
|
||||
<Ellipse.Style>
|
||||
<Style TargetType="Ellipse">
|
||||
<Setter Property="Fill" Value="{StaticResource WeightOrangeBrush}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsWeightStable}" Value="True">
|
||||
<Setter Property="Fill" Value="{StaticResource WeightGreenBrush}"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Ellipse.Style>
|
||||
</Ellipse>
|
||||
<TextBlock VerticalAlignment="Center" FontSize="13">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Text" Value="数据波动中"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource WeightOrangeBrush}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsWeightStable}" Value="True">
|
||||
<Setter Property="Text" Value="数据稳定"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource WeightGreenBrush}"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 车牌识别 -->
|
||||
<Border Style="{StaticResource SectionBorderStyle}">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
|
||||
<Border Width="4" Height="18" CornerRadius="2" Background="{StaticResource SectionBorderBrush}"/>
|
||||
<TextBlock Text="车牌识别" Style="{StaticResource SectionTitleStyle}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 车牌显示区 -->
|
||||
<Border CornerRadius="8" Padding="12,10" MinHeight="60">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource RegionBrush}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasPlate}" Value="True">
|
||||
<Setter Property="Background" Value="#0D1890FF"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<Grid>
|
||||
<!-- 未识别提示 -->
|
||||
<TextBlock Text="等待车辆进入识别区域..." FontSize="13"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasPlate}" Value="True">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
|
||||
<!-- 车牌样式(仿中国车牌) -->
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<StackPanel.Style>
|
||||
<Style TargetType="StackPanel">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasPlate}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</StackPanel.Style>
|
||||
<Border Background="#003880" CornerRadius="6" Padding="16,8">
|
||||
<TextBlock Text="{Binding DetectedPlate}" FontSize="28" FontWeight="Bold"
|
||||
Foreground="White" FontFamily="Consolas"
|
||||
HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
<Button Content="使用此车牌" Command="{Binding UseDetectedPlateCommand}"
|
||||
Style="{StaticResource ButtonPrimary}"
|
||||
Height="32" Margin="0,8,0,0" FontSize="12"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 操作日志 -->
|
||||
<Border Style="{StaticResource SectionBorderStyle}">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||
<Border Width="4" Height="18" CornerRadius="2" Background="{StaticResource SectionBorderBrush}"/>
|
||||
<TextBlock Text="操作日志" Style="{StaticResource SectionTitleStyle}"/>
|
||||
</StackPanel>
|
||||
<ItemsControl ItemsSource="{Binding OperationLogs}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding}" FontSize="12" Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
Margin="0,2" FontFamily="Consolas" TextWrapping="NoWrap"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ───── 分隔线 ───── -->
|
||||
<Border Grid.Column="1" Width="1" Background="{DynamicResource BorderBrush}" Margin="0,0,0,0"/>
|
||||
|
||||
<!-- ───── 右侧:信息录入表单 ───── -->
|
||||
<Grid Grid.Column="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<Border Grid.Row="0" Style="{StaticResource SectionBorderStyle}" Margin="0,0,0,6" Padding="12,10">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
|
||||
<Border Width="4" Height="18" CornerRadius="2" Background="{StaticResource SectionBorderBrush}"/>
|
||||
<TextBlock Text="基本信息" Style="{StaticResource SectionTitleStyle}"/>
|
||||
</StackPanel>
|
||||
<hc:Row Gutter="10">
|
||||
<!-- 称重日期 -->
|
||||
<hc:Col Span="12">
|
||||
<hc:DatePicker SelectedDate="{Binding WeighDate}"
|
||||
hc:InfoElement.Title="称重日期"
|
||||
hc:InfoElement.TitleWidth="75"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Necessary="True"
|
||||
hc:InfoElement.Symbol="*"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
<!-- 进出方向 -->
|
||||
<hc:Col Span="12">
|
||||
<hc:ComboBox SelectedValuePath="Value"
|
||||
DisplayMemberPath="Key"
|
||||
ItemsSource="{Binding InoutDirectionOptions}"
|
||||
SelectedValue="{Binding InoutDirection}"
|
||||
hc:InfoElement.Title="进出方向"
|
||||
hc:InfoElement.TitleWidth="75"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Necessary="True"
|
||||
hc:InfoElement.Symbol="*"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
<!-- 车牌号 -->
|
||||
<hc:Col Span="24">
|
||||
<hc:TextBox Text="{Binding PlateNumber, UpdateSourceTrigger=PropertyChanged}"
|
||||
IsReadOnly="{Binding IsPlateNumberLocked}"
|
||||
hc:InfoElement.Title="车牌号"
|
||||
hc:InfoElement.TitleWidth="75"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="手动输入或点击「使用此车牌」"
|
||||
hc:InfoElement.Necessary="True"
|
||||
hc:InfoElement.Symbol="*"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,8"/>
|
||||
</hc:Col>
|
||||
<!-- 车辆档案匹配状态 -->
|
||||
<hc:Col Span="24">
|
||||
<hc:Col.Style>
|
||||
<Style TargetType="hc:Col">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ShowVehicleMatchHint}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</hc:Col.Style>
|
||||
<Border CornerRadius="4" Padding="10,5" Margin="0,-6,0,8">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding VehicleLookupStatus}" Value="Matched">
|
||||
<Setter Property="Background" Value="#0D4caf50"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding VehicleLookupStatus}" Value="NotFound">
|
||||
<Setter Property="Background" Value="#0Dff9800"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding VehicleLookupStatus}" Value="Searching">
|
||||
<Setter Property="Background" Value="#0D1890ff"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock Text="{Binding VehicleMatchText}" FontSize="12">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource SecondaryTextBrush}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding VehicleLookupStatus}" Value="Matched">
|
||||
<Setter Property="Foreground" Value="#4caf50"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding VehicleLookupStatus}" Value="NotFound">
|
||||
<Setter Property="Foreground" Value="#ff9800"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding VehicleLookupStatus}" Value="Searching">
|
||||
<Setter Property="Foreground" Value="#1890ff"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Border>
|
||||
</hc:Col>
|
||||
<!-- 发货单位(供应商弹窗选择) -->
|
||||
<hc:Col Span="24">
|
||||
<hc:Col.Style>
|
||||
<Style TargetType="hc:Col">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding InoutDirection}" Value="2">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</hc:Col.Style>
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="75"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="60"/>
|
||||
<ColumnDefinition Width="32"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="发货单位" VerticalAlignment="Center"
|
||||
FontSize="14" Foreground="{DynamicResource PrimaryTextBrush}"/>
|
||||
<Border Grid.Column="1" CornerRadius="4" Height="32"
|
||||
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"
|
||||
Background="{DynamicResource ThirdlyRegionBrush}">
|
||||
<TextBlock Text="{Binding SenderUnitDisplay}" VerticalAlignment="Center"
|
||||
Margin="8,0" FontSize="13" TextTrimming="CharacterEllipsis">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource SecondaryTextBrush}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasSelectedSupplier}" Value="True">
|
||||
<Setter Property="Foreground" Value="{DynamicResource PrimaryTextBrush}"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Border>
|
||||
<Button Grid.Column="2" Content="选 择"
|
||||
Command="{Binding OpenSupplierPickerCommand}"
|
||||
Style="{StaticResource ButtonPrimary}"
|
||||
Height="32" Margin="6,0,0,0" FontSize="12"/>
|
||||
<Button Grid.Column="3" Command="{Binding ClearSupplierCommand}"
|
||||
Style="{StaticResource ButtonIcon}"
|
||||
Height="32" Width="28" Margin="4,0,0,0" Padding="0"
|
||||
ToolTip="清除选择"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
hc:IconElement.Geometry="{StaticResource ErrorGeometry}"/>
|
||||
</Grid>
|
||||
</hc:Col>
|
||||
<!-- 收货单位(客户弹窗选择) -->
|
||||
<hc:Col Span="24">
|
||||
<hc:Col.Style>
|
||||
<Style TargetType="hc:Col">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding InoutDirection}" Value="2">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</hc:Col.Style>
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="75"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="60"/>
|
||||
<ColumnDefinition Width="32"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="收货单位" VerticalAlignment="Center"
|
||||
FontSize="14" Foreground="{DynamicResource PrimaryTextBrush}"/>
|
||||
<Border Grid.Column="1" CornerRadius="4" Height="32"
|
||||
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"
|
||||
Background="{DynamicResource ThirdlyRegionBrush}">
|
||||
<TextBlock Text="{Binding ReceiverUnitDisplay}" VerticalAlignment="Center"
|
||||
Margin="8,0" FontSize="13" TextTrimming="CharacterEllipsis">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource SecondaryTextBrush}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasSelectedCustomer}" Value="True">
|
||||
<Setter Property="Foreground" Value="{DynamicResource PrimaryTextBrush}"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Border>
|
||||
<Button Grid.Column="2" Content="选 择"
|
||||
Command="{Binding OpenCustomerPickerCommand}"
|
||||
Style="{StaticResource ButtonSuccess}"
|
||||
Height="32" Margin="6,0,0,0" FontSize="12"/>
|
||||
<Button Grid.Column="3" Command="{Binding ClearCustomerCommand}"
|
||||
Style="{StaticResource ButtonIcon}"
|
||||
Height="32" Width="28" Margin="4,0,0,0" Padding="0"
|
||||
ToolTip="清除选择"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
hc:IconElement.Geometry="{StaticResource ErrorGeometry}"/>
|
||||
</Grid>
|
||||
</hc:Col>
|
||||
<!-- 司机 -->
|
||||
<hc:Col Span="12">
|
||||
<hc:TextBox Text="{Binding DriverName, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Title="司机姓名"
|
||||
hc:InfoElement.TitleWidth="75"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="司机姓名"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,0"/>
|
||||
</hc:Col>
|
||||
<!-- 手机号 -->
|
||||
<hc:Col Span="12">
|
||||
<hc:TextBox Text="{Binding DriverPhone, UpdateSourceTrigger=PropertyChanged}"
|
||||
hc:InfoElement.Title="手机号码"
|
||||
hc:InfoElement.TitleWidth="75"
|
||||
hc:InfoElement.TitlePlacement="Left"
|
||||
hc:InfoElement.Placeholder="手机号码"
|
||||
hc:InfoElement.ShowClearButton="True"
|
||||
Margin="0,0,0,0"/>
|
||||
</hc:Col>
|
||||
</hc:Row>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 重量信息 -->
|
||||
<Border Grid.Row="1" Style="{StaticResource SectionBorderStyle}" Margin="0,0,0,0" Padding="12,10">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
|
||||
<Border Width="4" Height="18" CornerRadius="2" Background="{StaticResource SectionBorderBrush}"/>
|
||||
<TextBlock Text="重量信息" Style="{StaticResource SectionTitleStyle}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 毛重 / 皮重并排 -->
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="16"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 毛重 -->
|
||||
<StackPanel>
|
||||
<StackPanel.Style>
|
||||
<Style TargetType="StackPanel">
|
||||
<Setter Property="Grid.Column" Value="0"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding InoutDirection}" Value="2">
|
||||
<Setter Property="Grid.Column" Value="2"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</StackPanel.Style>
|
||||
<TextBlock Text="毛 重(KG)" FontSize="12" Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
Margin="0,0,0,4" HorizontalAlignment="Center"/>
|
||||
<Border CornerRadius="8" Padding="10,10" Background="{DynamicResource RegionBrush}">
|
||||
<TextBlock Text="{Binding GrossWeight, StringFormat=N2, FallbackValue=—, TargetNullValue=—}"
|
||||
HorizontalAlignment="Center" FontSize="20" FontWeight="Bold"
|
||||
FontFamily="Consolas">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource SecondaryTextBrush}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding GrossWeightCaptured}" Value="True">
|
||||
<Setter Property="Foreground" Value="#4caf50"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Border>
|
||||
<Border Margin="0,6,0,0" Height="32" CornerRadius="6">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding GrossWeightCaptured}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
<Setter Property="Background" Value="#1A4caf50"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock Text="✓ 已采集" Foreground="#4caf50" FontSize="13"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Button Command="{Binding CaptureGrossWeightCommand}"
|
||||
Margin="0,6,0,0"
|
||||
HorizontalAlignment="Stretch">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button" BasedOn="{StaticResource CaptureGrossButtonStyle}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding GrossWeightCaptured}" Value="True">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="Truck" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="{Binding CaptureGrossButtonText}" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 皮重 -->
|
||||
<StackPanel>
|
||||
<StackPanel.Style>
|
||||
<Style TargetType="StackPanel">
|
||||
<Setter Property="Grid.Column" Value="2"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding InoutDirection}" Value="2">
|
||||
<Setter Property="Grid.Column" Value="0"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</StackPanel.Style>
|
||||
<TextBlock Text="皮 重(KG)" FontSize="12" Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
Margin="0,0,0,4" HorizontalAlignment="Center"/>
|
||||
<Border CornerRadius="8" Padding="10,10" Background="{DynamicResource RegionBrush}">
|
||||
<TextBlock HorizontalAlignment="Center" FontSize="20" FontWeight="Bold"
|
||||
FontFamily="Consolas" Foreground="{DynamicResource SecondaryTextBrush}">
|
||||
<TextBlock.Text>
|
||||
<Binding Path="TareWeight" StringFormat="N2" FallbackValue="—" TargetNullValue="—"/>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</Border>
|
||||
<Border Margin="0,6,0,0" Height="32" CornerRadius="6">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding TareWeightCaptured}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
<Setter Property="Background" Value="#1A4caf50"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock Text="✓ 已采集" Foreground="#4caf50" FontSize="13"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Button Command="{Binding CaptureTareWeightCommand}"
|
||||
Margin="0,6,0,0"
|
||||
HorizontalAlignment="Stretch">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button" BasedOn="{StaticResource CaptureButtonStyle}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding TareWeightCaptured}" Value="True">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="TruckOutline" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="{Binding CaptureTareButtonText}" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 净重展示(横贯全宽) -->
|
||||
<StackPanel Margin="0,2,0,0">
|
||||
<TextBlock Text="净 重 = 毛重 - 皮重 (KG)" FontSize="12"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
Margin="0,0,0,6" HorizontalAlignment="Center"/>
|
||||
<Border CornerRadius="10" Padding="12,10" Background="#0D52c41a"
|
||||
BorderBrush="#52c41a" BorderThickness="1.5">
|
||||
<TextBlock Text="{Binding NetWeightDisplay}" FontSize="28" FontWeight="Bold"
|
||||
FontFamily="Consolas" HorizontalAlignment="Center"
|
||||
Foreground="#52c41a"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 最近称重榜单 -->
|
||||
<StackPanel Margin="0,8,0,0">
|
||||
<TextBlock Text="最近称重榜单"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource SecondaryTextBrush}"
|
||||
Margin="0,0,0,6"/>
|
||||
<Border CornerRadius="8"
|
||||
Background="White"
|
||||
BorderBrush="{DynamicResource BorderBrush}"
|
||||
BorderThickness="1"
|
||||
Padding="8,6">
|
||||
<DataGrid ItemsSource="{Binding RecentWeightRecords}"
|
||||
SelectedItem="{Binding SelectedRecentWeightRecord}"
|
||||
AutoGenerateColumns="False"
|
||||
CanUserAddRows="False"
|
||||
CanUserDeleteRows="False"
|
||||
CanUserResizeRows="False"
|
||||
HeadersVisibility="Column"
|
||||
SelectionMode="Single"
|
||||
SelectionUnit="FullRow"
|
||||
IsReadOnly="True"
|
||||
GridLinesVisibility="Horizontal"
|
||||
HorizontalGridLinesBrush="#FFEDEFF2"
|
||||
VerticalGridLinesBrush="Transparent"
|
||||
Background="White"
|
||||
RowBackground="White"
|
||||
AlternatingRowBackground="White"
|
||||
RowHeight="26"
|
||||
ColumnHeaderHeight="28"
|
||||
RowHeaderWidth="0"
|
||||
MinHeight="80"
|
||||
MaxHeight="80">
|
||||
<DataGrid.RowStyle>
|
||||
<Style TargetType="DataGridRow">
|
||||
<Setter Property="Background" Value="White"/>
|
||||
<Setter Property="Foreground" Value="#262626"/>
|
||||
<Setter Property="BorderBrush" Value="#FFEDEFF2"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="#FFEAF3FF"/>
|
||||
<Setter Property="Foreground" Value="#1F1F1F"/>
|
||||
<Setter Property="BorderBrush" Value="#FFD6E8FF"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</DataGrid.RowStyle>
|
||||
<DataGrid.CellStyle>
|
||||
<Style TargetType="DataGridCell">
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Foreground" Value="#262626"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="#FFEAF3FF"/>
|
||||
<Setter Property="Foreground" Value="#1F1F1F"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</DataGrid.CellStyle>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="榜单号" Binding="{Binding BillNo}" Width="2.2*"/>
|
||||
<DataGridTextColumn Header="车辆" Binding="{Binding PlateNumber}" Width="1.3*"/>
|
||||
<DataGridTextColumn Header="首称重量(KG)" Binding="{Binding FirstWeightDisplay}" Width="1*"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<!-- ══════════════════════ Row 2: 底部操作栏 ══════════════════════ -->
|
||||
<Border Grid.Row="2" Background="{DynamicResource RegionBrush}"
|
||||
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,1,0,0">
|
||||
<Grid Margin="20,0">
|
||||
<!-- 清空按钮(左) -->
|
||||
<Button HorizontalAlignment="Left" VerticalAlignment="Center"
|
||||
Command="{Binding ClearCommand}"
|
||||
Style="{StaticResource ButtonDefault}"
|
||||
Height="40" Padding="20,0">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="TrashCanOutline" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="清空表单" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- 保存按钮(右) -->
|
||||
<Button HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
Command="{Binding SaveCommand}"
|
||||
Style="{StaticResource ButtonSuccess}"
|
||||
Height="40" Padding="28,0" FontSize="15">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<md:PackIcon Kind="ContentSave" VerticalAlignment="Center" Margin="0,0,8,0" Width="20" Height="20"/>
|
||||
<TextBlock Text="保 存 记 录" VerticalAlignment="Center" FontWeight="SemiBold"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace YY.Admin.Views.WeightRecord;
|
||||
|
||||
public partial class WeightRecordOperationView : UserControl
|
||||
{
|
||||
public WeightRecordOperationView() => InitializeComponent();
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
// 缓存配置
|
||||
"Cache": {
|
||||
"Prefix": "yyadmin_", // 全局缓存前缀
|
||||
"CacheType": "Memory", // Memory、Redis
|
||||
"Redis": {
|
||||
"Configuration": "server=localhost;db=2;password=123456;", // Redis连接字符串
|
||||
"Prefix": "yyadmin_", // Redis前缀(目前没用)
|
||||
"MaxMessageSize": "1048576" // 最大消息大小 默认1024 * 1024
|
||||
}
|
||||
},
|
||||
// 数据库连接字符串参考地址:https://www.connectionstrings.com/
|
||||
"DbConnection": {
|
||||
"EnableConsoleSql": true, // 启用控制台打印SQL
|
||||
"ConnectionConfigs": [
|
||||
{
|
||||
"ConfigId": "1300000000001", // 默认库标识-禁止修改
|
||||
"DbType": "Sqlite", // MySql、SqlServer、Sqlite、Oracle、PostgreSQL、Dm、Kdbndp、Oscar、MySqlConnector、Access、OpenGauss、QuestDB、HG、ClickHouse、GBase、Odbc、Custom
|
||||
"DbNickName": "系统库",
|
||||
//"ConnectionString": "DataSource=./Admin.NET.db", // Sqlite
|
||||
"ConnectionString": "DataSource=./Admin.NET.db", // Sqlite
|
||||
//"ConnectionString": "PORT=5432;DATABASE=xxx;HOST=localhost;PASSWORD=xxx;USER ID=xxx", // PostgreSQL
|
||||
//"ConnectionString": "server= ;port=;database=;user=;password=;CharSet=utf8;sslmode=none;max pool size=1000;", // MySql,
|
||||
"DbSettings": {
|
||||
"EnableInitDb": true, // 启用库初始化(若实体没有变化建议关闭)
|
||||
"EnableInitView": false, // 启用视图初始化(若实体和视图没有变化建议关闭)
|
||||
"EnableDiffLog": false, // 启用库表差异日志
|
||||
"EnableUnderLine": true, // 启用驼峰转下划线
|
||||
"EnableConnEncrypt": false // 启用数据库连接串加密(国密SM2加解密)
|
||||
},
|
||||
"TableSettings": {
|
||||
"EnableInitTable": true, // 启用表初始化(若实体没有变化建议关闭)
|
||||
"EnableIncreTable": false // 启用表增量更新(只更新贴了特性[IncreTable]的实体表)
|
||||
},
|
||||
"SeedSettings": {
|
||||
"EnableInitSeed": true, // 启用种子初始化(若种子没有变化建议关闭)
|
||||
"EnableIncreSeed": false // 启用种子增量更新(只更新贴了特性[IncreSeed]的种子表)
|
||||
}
|
||||
},
|
||||
{
|
||||
"ConfigId": "Slave", // 从数据库
|
||||
"DbType": "Sqlite", // 数据库类型
|
||||
"DbNickName": "业务库",
|
||||
"ConnectionString": "Data Source=./Slave.db", // Sqlite
|
||||
"DbSettings": {
|
||||
"EnableInitDb": true, // 启用库初始化(若实体没有变化建议关闭)
|
||||
"EnableInitView": false, // 启用视图初始化(若实体和视图没有变化建议关闭)
|
||||
"EnableDiffLog": false, // 启用库表差异日志
|
||||
"EnableUnderLine": true, // 启用驼峰转下划线
|
||||
"EnableConnEncrypt": false // 启用数据库连接串加密(国密SM2加解密)
|
||||
},
|
||||
"TableSettings": {
|
||||
"EnableInitTable": true, // 启用表初始化(若实体没有变化建议关闭)
|
||||
"EnableIncreTable": false // 启用表增量更新(只更新贴了特性[IncreTable]的实体表)
|
||||
},
|
||||
"SeedSettings": {
|
||||
"EnableInitSeed": true, // 启用种子初始化(若种子没有变化建议关闭)
|
||||
"EnableIncreSeed": false // 启用种子增量更新(只更新贴了特性[IncreSeed]的种子表)
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"AutoUpdate": {
|
||||
"RemoteConfigUrl": "http://14.103.155.227:8083/updates/version.xml" //更新文件地址
|
||||
},
|
||||
"JeecgIntegration": {
|
||||
"Enabled": true, // 是否启用Jeecg(可与本地并存;见 PreferLocalLogin 决定先后顺序)
|
||||
"PreferLocalLogin": true, // true:先校验本地库再尝试 Jeecg(工控脱网时无需等待 MES,直接用本地账号登录)
|
||||
"FallbackToLocal": true, // false:仅 MES 登录,失败即结束(不推荐工控场景)
|
||||
"BaseUrl": "http://127.0.0.1:8080/jeecg-boot", // Jeecg后端地址(按实际环境修改)
|
||||
"LoginPath": "/sys/login", // Jeecg登录接口
|
||||
"UserInfoPath": "/sys/user/getUserInfo", // Jeecg用户信息接口
|
||||
"UserListPath": "/sys/user/scada/queryUser", // Jeecg 用户列表(SCADA:分页 + updatedAfter 增量,见文档)
|
||||
"ScadaUserPageSize": 500, // SCADA queryUser 每页条数,最大 1000
|
||||
"ScadaUserIncludeDetail": false, // true 时含部门/公司/租户明细,耗时更高
|
||||
"ScadaUseUpdatedAfter": true, // 后台/定时同步:有水位时用 updatedAfter 增量。登录触发的 SCADA 同步固定全量分页,避免只拉到少数变更用户
|
||||
"TenantListPath": "/sys/tenant/list", // Jeecg租户分页接口
|
||||
"UserPermissionPath": "/sys/permission/getUserPermissionByToken", // Jeecg当前用户菜单与按钮权限接口
|
||||
"ResetLocalIdentityDataOnJeecgLogin": false, // true 时 Jeecg 登录成功会清空本地用户/角色等(易丢失种子账号导致脱网无法登录)。工控独立运行建议保持 false
|
||||
"AutoProvisionLocalUser": true, // Jeecg认证成功后本地不存在账号时自动创建
|
||||
"SyncUserProfileToLocal": true, // 每次登录时同步Jeecg用户基础信息到本地
|
||||
"SyncAllUsersOnJeecgLogin": true, // true:Jeecg 登录成功后拉取用户列表并写入本地(关闭则不会同步用户表)
|
||||
"UseJeecgUserIdAsLocalPrimaryKey": true, // true 时本地 sys_user.id 与 Jeecg getUserInfo 的 id 一致(雪花 long)
|
||||
"UserSyncSkipUnchanged": true, // 与Jeecg updateTime 一致时跳过写库,减少重复保存
|
||||
"UserListUseUpdateTimeQuery": false, // 仅非 SCADA 的 /sys/user/list:true 时附带 updateTime_begin。SCADA 增量由 ScadaUseUpdatedAfter 控制
|
||||
"IncrementalSyncOverlapMinutes": 2, // 增量时间窗口重叠,避免时钟误差漏数据
|
||||
"BackgroundSyncIntervalMinutes": 30, // 主窗口在线时定时增量同步间隔(分钟)
|
||||
"AnonymousMode": true, // 工控机免密模式:优先走免登录接口与匿名WebSocket通道
|
||||
"WebSocketUrl": "", // 可选:Jeecg 或自建推送地址;免密模式下若误配到 /ws/device/websocket 会自动切回 /websocket/scada-sync
|
||||
"WebSocketPath": "/websocket/scada-sync", // 匿名实时推送通道
|
||||
"WebSocketInactivityReconnectSeconds": 0, // 0=关闭空闲强制重连;仅保留WS心跳保活,避免重连窗口丢推送
|
||||
"DefaultTenantId": 1002, // 自动创建本地用户时使用的默认租户ID
|
||||
"Captcha": "", // 如启用登录验证码,在此传入验证码
|
||||
"CheckKey": "" // 如启用登录验证码,在此传入验证码key
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
BIN
yy-admin-master/_publish/YY.Admin-win-x64/PenImc_cor3.dll
Normal file
BIN
yy-admin-master/_publish/YY.Admin-win-x64/PenImc_cor3.dll
Normal file
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<item>
|
||||
<version>1.2.0.0</version>
|
||||
<url>http://your-update-server.com/YourAppSetup.exe</url>
|
||||
<changelog>
|
||||
• 新增用户管理功能
|
||||
• 优化系统性能
|
||||
• 修复已知问题
|
||||
• 改进用户体验
|
||||
</changelog>
|
||||
<publishDate>2025-11-26</publishDate>
|
||||
<mandatory>true</mandatory>
|
||||
</item>
|
||||
BIN
yy-admin-master/_publish/YY.Admin-win-x64/WebView2Loader.dll
Normal file
BIN
yy-admin-master/_publish/YY.Admin-win-x64/WebView2Loader.dll
Normal file
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<configSections>
|
||||
<sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
|
||||
<section name="YY.Admin.Settings1" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
|
||||
</sectionGroup>
|
||||
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
|
||||
<section name="YY.Admin.Properties.AppSettings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
|
||||
</sectionGroup>
|
||||
</configSections>
|
||||
<userSettings>
|
||||
<YY.Admin.Properties.AppSettings>
|
||||
<setting name="SkinType" serializeAs="String">
|
||||
<value>0</value>
|
||||
</setting>
|
||||
</YY.Admin.Properties.AppSettings>
|
||||
</userSettings>
|
||||
</configuration>
|
||||
BIN
yy-admin-master/_publish/YY.Admin-win-x64/YY.Admin.exe
Normal file
BIN
yy-admin-master/_publish/YY.Admin-win-x64/YY.Admin.exe
Normal file
Binary file not shown.
BIN
yy-admin-master/_publish/YY.Admin-win-x64/e_sqlite3.dll
Normal file
BIN
yy-admin-master/_publish/YY.Admin-win-x64/e_sqlite3.dll
Normal file
Binary file not shown.
BIN
yy-admin-master/_publish/YY.Admin-win-x64/glfw3.dll
Normal file
BIN
yy-admin-master/_publish/YY.Admin-win-x64/glfw3.dll
Normal file
Binary file not shown.
BIN
yy-admin-master/_publish/YY.Admin-win-x64/libHarfBuzzSharp.dll
Normal file
BIN
yy-admin-master/_publish/YY.Admin-win-x64/libHarfBuzzSharp.dll
Normal file
Binary file not shown.
BIN
yy-admin-master/_publish/YY.Admin-win-x64/libSkiaSharp.dll
Normal file
BIN
yy-admin-master/_publish/YY.Admin-win-x64/libSkiaSharp.dll
Normal file
Binary file not shown.
BIN
yy-admin-master/_publish/YY.Admin-win-x64/vcruntime140_cor3.dll
Normal file
BIN
yy-admin-master/_publish/YY.Admin-win-x64/vcruntime140_cor3.dll
Normal file
Binary file not shown.
BIN
yy-admin-master/_publish/YY.Admin-win-x64/wpfgfx_cor3.dll
Normal file
BIN
yy-admin-master/_publish/YY.Admin-win-x64/wpfgfx_cor3.dll
Normal file
Binary file not shown.
Reference in New Issue
Block a user