胶料快检添加离线模式

This commit is contained in:
2026-06-30 11:28:04 +08:00
parent efcd73a565
commit 840e68a450
19 changed files with 1053 additions and 343 deletions

View File

@@ -18,12 +18,25 @@ public interface IRubberQuickTestRecordService
Task<RubberQuickTestRecordSaveResult> SaveAsync(MesXslRubberQuickTestRecord entity, CancellationToken ct = default);
/// <summary>删除本地同步失败的快检记录(已同步或待同步不可删)</summary>
bool DeleteFailedLocal(string localId);
/// <summary>更新本地同步快检记录</summary>
Task<RubberQuickTestRecordSaveResult> UpdateLocalAsync(string localId, MesXslRubberQuickTestRecord entity, CancellationToken ct = default);
/// <summary>删除本地未同步的快检记录(已同步不可删)</summary>
bool DeleteUnsyncedLocal(string localId);
/// <summary>将本地未同步记录推送到 MES须已联网且密炼计划信息完整</summary>
Task<RubberQuickTestRecordSyncResult> SyncLocalAsync(string localId, CancellationToken ct = default);
string GenerateRecordNo(string rubberMaterialName);
}
public class RubberQuickTestRecordSyncResult
{
public bool Success { get; set; }
public string? Message { get; set; }
public string? RecordNo { get; set; }
}
public record RubberQuickTestRecordPageResult(
List<RubberQuickTestRecordListRow> Records,
long Total,

View File

@@ -23,10 +23,16 @@ public class RubberQuickTestRecordListRow
public string SyncStatusDisplay => SyncStatus switch
{
"Synced" => "已同步",
"Failed" => "失败",
_ => "同步"
"Failed" => "同步失败",
_ => "同步"
};
/// <summary>本地同步失败记录可删除</summary>
public bool CanDelete => SyncStatus == "Failed" && !string.IsNullOrWhiteSpace(LocalId);
/// <summary>本地同步记录可删除</summary>
public bool CanDelete => SyncStatus != "Synced" && !string.IsNullOrWhiteSpace(LocalId);
/// <summary>本地未同步记录可手动同步</summary>
public bool CanSync => SyncStatus != "Synced" && !string.IsNullOrWhiteSpace(LocalId);
/// <summary>本地未同步记录可编辑</summary>
public bool CanEdit => CanSync;
}

View File

@@ -0,0 +1,62 @@
using YY.Admin.Core;
namespace YY.Admin.Services.Service;
/// <summary>
/// 胶料快检相关 MES 只读数据(实验标准、密炼计划)专用轮询管理器。
/// </summary>
public class MesQuickTestDataPollManager : ISingletonDependency
{
public static readonly TimeSpan PollInterval = TimeSpan.FromSeconds(2);
private readonly List<(string Name, Func<Task> Task)> _tasks = [];
private readonly Timer _timer;
private readonly ILoggerService _logger;
private volatile bool _tickRunning;
public MesQuickTestDataPollManager(ILoggerService logger)
{
_logger = logger;
_timer = new Timer(OnTick, null, PollInterval, PollInterval);
_logger.Information($"[快检数据轮询] 已启动,间隔 {PollInterval.TotalSeconds} 秒");
}
public void Register(string name, Func<Task> pollTask)
{
lock (_tasks)
_tasks.Add((name, pollTask));
_logger.Information($"[快检数据轮询] 注册任务: {name}");
}
private void OnTick(object? _)
{
if (_tickRunning) return;
_ = Task.Run(async () =>
{
if (_tickRunning) return;
_tickRunning = true;
try
{
List<(string Name, Func<Task> Task)> snapshot;
lock (_tasks) snapshot = [.. _tasks];
foreach (var (name, task) in snapshot)
{
try
{
await task().ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.Warning($"[快检数据轮询] 任务 [{name}] 异常: {ex.Message}");
}
}
}
finally
{
_tickRunning = false;
}
});
}
}

View File

@@ -53,17 +53,13 @@ public class MixingProductionPlanService : IMixingProductionPlanService, ISingle
LoadCacheFromDisk();
_logger.Information($"[密炼计划] 服务初始化,缓存={_localCache.Count},在线={_networkMonitor.IsOnline}");
_networkMonitor.StatusChanged += OnNetworkStatusChanged;
if (_networkMonitor.IsOnline)
_ = Task.Run(() => SyncFromRemoteAsync(CancellationToken.None));
}
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<MixingProductionPlanPageResult> PageAsync(
public Task<MixingProductionPlanPageResult> PageAsync(
int pageNo, int pageSize,
DateTime? planDateFrom = null,
DateTime? planDateTo = null,
@@ -73,18 +69,7 @@ public class MixingProductionPlanService : IMixingProductionPlanService, ISingle
string? materialName = null,
CancellationToken ct = default)
{
if (_networkMonitor.IsOnline)
{
try
{
await SyncFromRemoteAsync(ct).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.Warning($"[密炼计划] 列表拉取失败,使用本地缓存:{ex.Message}");
}
}
ct.ThrowIfCancellationRequested();
List<MesXslMixingProductionPlan> source;
lock (_cacheLock)
source = _localCache.Select(Clone).ToList();
@@ -95,25 +80,14 @@ public class MixingProductionPlanService : IMixingProductionPlanService, ISingle
.Skip(Math.Max(0, (pageNo - 1) * pageSize))
.Take(pageSize)
.ToList();
return new MixingProductionPlanPageResult(records, total, pageNo, pageSize);
return Task.FromResult(new MixingProductionPlanPageResult(records, total, pageNo, pageSize));
}
public async Task<List<MesXslMixingProductionPlan>> GetAllCachedAsync(CancellationToken ct = default)
public Task<List<MesXslMixingProductionPlan>> GetAllCachedAsync(CancellationToken ct = default)
{
if (_networkMonitor.IsOnline)
{
try
{
await SyncFromRemoteAsync(ct).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.Warning($"[密炼计划] 全量拉取失败,使用本地缓存:{ex.Message}");
}
}
ct.ThrowIfCancellationRequested();
lock (_cacheLock)
return _localCache.Select(Clone).ToList();
return Task.FromResult(_localCache.Select(Clone).ToList());
}
public async Task<bool> SyncFromRemoteAsync(CancellationToken ct = default)
@@ -190,25 +164,6 @@ public class MixingProductionPlanService : IMixingProductionPlanService, ISingle
}
}
private void OnNetworkStatusChanged(bool isOnline)
{
if (!isOnline) return;
_ = Task.Run(async () =>
{
try
{
if (!await SyncFromRemoteAsync(CancellationToken.None).ConfigureAwait(false))
return;
_eventAggregator.GetEvent<MixingProductionPlanChangedEvent>()
.Publish(new MixingProductionPlanChangedPayload { Action = "reconnect" });
}
catch (Exception ex)
{
_logger.Warning($"[密炼计划] 重连同步失败:{ex.Message}");
}
});
}
private static bool IsPlanContentEqual(MesXslMixingProductionPlan a, MesXslMixingProductionPlan b) =>
string.Equals(GetPlanFingerprint(a), GetPlanFingerprint(b), StringComparison.Ordinal);

View File

@@ -3,33 +3,58 @@ using System.Text.Json;
using YY.Admin.Core;
using YY.Admin.Core.Events;
using YY.Admin.Core.Services;
using YY.Admin.Services.Service;
namespace YY.Admin.Services.Service.MixingProductionPlan;
public class MixingProductionPlanSyncCoordinator : ISingletonDependency
{
private readonly IEventAggregator _eventAggregator;
private readonly IMixingProductionPlanService _planService;
private readonly ILoggerService _logger;
public MixingProductionPlanSyncCoordinator(
IEventAggregator eventAggregator,
SyncPollManager pollManager,
IMixingProductionPlanService planService,
MesQuickTestDataPollManager pollManager,
ILoggerService logger)
{
_eventAggregator = eventAggregator;
_planService = planService;
_logger = logger;
_eventAggregator.GetEvent<RemoteCommandReceivedEvent>()
.Subscribe(OnRemoteCommand, ThreadOption.BackgroundThread);
pollManager.Register("密炼计划", () =>
{
_eventAggregator.GetEvent<MixingProductionPlanChangedEvent>()
.Publish(new MixingProductionPlanChangedPayload { Action = "poll" });
return Task.CompletedTask;
});
_eventAggregator.GetEvent<NetworkStatusChangedEvent>()
.Subscribe(OnNetworkStatusChanged, ThreadOption.BackgroundThread);
pollManager.Register("密炼计划", () => PollSyncAsync("poll"));
_logger.Information("[密炼计划] MixingProductionPlanSyncCoordinator 已启动");
_ = PollSyncAsync("startup");
}
private async Task PollSyncAsync(string action)
{
try
{
if (!await _planService.SyncFromRemoteAsync(CancellationToken.None).ConfigureAwait(false))
return;
_eventAggregator.GetEvent<MixingProductionPlanChangedEvent>()
.Publish(new MixingProductionPlanChangedPayload { Action = action });
}
catch (Exception ex)
{
_logger.Warning($"[密炼计划] 轮询同步失败:{ex.Message}");
}
}
private void OnNetworkStatusChanged(NetworkStatusChangedPayload payload)
{
if (!payload.IsOnline) return;
_ = Task.Run(() => PollSyncAsync("reconnect"));
}
private void OnRemoteCommand(RemoteCommandPayload payload)
@@ -53,9 +78,20 @@ public class MixingProductionPlanSyncCoordinator : ISingletonDependency
MixingProductionPlanId = idEl.ValueKind == JsonValueKind.String ? idEl.GetString() : null
};
_logger.Information($"[密炼计划] STOMP action={changed.Action}, id={changed.MixingProductionPlanId}");
_ = Task.Run(async () =>
{
try
{
if (await _planService.SyncFromRemoteAsync(CancellationToken.None).ConfigureAwait(false))
_eventAggregator.GetEvent<MixingProductionPlanChangedEvent>().Publish(changed);
}
catch (Exception ex)
{
_logger.Warning($"[密炼计划] STOMP 同步失败:{ex.Message}");
}
});
}
catch (Exception ex)
{
_logger.Warning($"[密炼计划] 处理 STOMP 命令失败:{ex.Message}");
}

View File

@@ -54,10 +54,6 @@ public class RubberQuickTestRecordService : IRubberQuickTestRecordService, ISing
LoadCacheFromDisk();
_logger.Information($"[快检记录同步] 初始化完成,本地记录={_localItems.Count}");
_networkMonitor.StatusChanged += OnNetworkStatusChanged;
if (_networkMonitor.IsOnline)
_ = Task.Run(() => PushPendingAsync(CancellationToken.None));
}
private string BaseUrl => (_configuration.GetValue<string>("JeecgIntegration:BaseUrl") ?? "http://localhost:8080/jeecg-boot").TrimEnd('/');
@@ -180,7 +176,79 @@ public class RubberQuickTestRecordService : IRubberQuickTestRecordService, ISing
};
}
public bool DeleteFailedLocal(string localId)
public async Task<RubberQuickTestRecordSaveResult> UpdateLocalAsync(string localId, MesXslRubberQuickTestRecord entity, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(localId))
throw new ArgumentException("无效的记录标识", nameof(localId));
var record = CloneRecord(entity);
RubberQuickTestRecordLocalItem? existing;
lock (_cacheLock)
{
existing = _localItems.FirstOrDefault(x =>
string.Equals(x.LocalId, localId.Trim(), StringComparison.OrdinalIgnoreCase));
}
if (existing == null)
throw new InvalidOperationException("未找到本地快检记录");
if (string.Equals(existing.SyncStatus, "Synced", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("已同步记录不可编辑");
record.CreateTime = existing.Record.CreateTime ?? record.CreateTime ?? DateTime.Now;
record.InspectTime = record.InspectTime ?? record.CreateTime;
if (string.IsNullOrWhiteSpace(record.RecordNo))
record.RecordNo = existing.Record.RecordNo;
var item = CloneLocalItem(existing);
item.Record = record;
item.SyncStatus = "Pending";
item.SyncError = null;
if (_networkMonitor.IsOnline && HasCompletePlanInfo(record))
{
try
{
var recordNo = await RemoteAddAsync(record, ct).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(recordNo))
{
item.Record.RecordNo = recordNo;
item.SyncStatus = "Synced";
}
}
catch (Exception ex)
{
item.SyncStatus = "Failed";
item.SyncError = ex.Message;
_logger.Warning($"[快检记录修改] 远端失败,保留本地:{ex.Message}");
}
}
lock (_cacheLock)
{
var target = _localItems.FirstOrDefault(x =>
string.Equals(x.LocalId, localId.Trim(), StringComparison.OrdinalIgnoreCase));
if (target == null)
throw new InvalidOperationException("未找到本地快检记录");
target.Record = CloneRecord(item.Record);
target.SyncStatus = item.SyncStatus;
target.SyncError = item.SyncError;
SaveCacheToDiskUnsafe();
}
_eventAggregator.GetEvent<RubberQuickTestRecordChangedEvent>()
.Publish(new RubberQuickTestRecordChangedPayload { Action = "update", RecordId = localId });
return new RubberQuickTestRecordSaveResult
{
LocalId = localId,
SyncStatus = item.SyncStatus,
Record = CloneRecord(item.Record)
};
}
public bool DeleteUnsyncedLocal(string localId)
{
if (string.IsNullOrWhiteSpace(localId)) return false;
@@ -189,7 +257,7 @@ public class RubberQuickTestRecordService : IRubberQuickTestRecordService, ISing
{
var item = _localItems.FirstOrDefault(x =>
string.Equals(x.LocalId, localId.Trim(), StringComparison.OrdinalIgnoreCase));
if (item == null || !string.Equals(item.SyncStatus, "Failed", StringComparison.OrdinalIgnoreCase))
if (item == null || string.Equals(item.SyncStatus, "Synced", StringComparison.OrdinalIgnoreCase))
return false;
removed = _localItems.Remove(item);
@@ -201,10 +269,92 @@ public class RubberQuickTestRecordService : IRubberQuickTestRecordService, ISing
_eventAggregator.GetEvent<RubberQuickTestRecordChangedEvent>()
.Publish(new RubberQuickTestRecordChangedPayload { Action = "delete", RecordId = localId });
_logger.Information($"[快检记录删除] 已删除同步失败本地记录 localId={localId}");
_logger.Information($"[快检记录删除] 已删除本地未同步记录 localId={localId}");
return true;
}
public async Task<RubberQuickTestRecordSyncResult> SyncLocalAsync(string localId, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(localId))
return new RubberQuickTestRecordSyncResult { Success = false, Message = "无效的记录标识" };
if (!_networkMonitor.IsOnline)
return new RubberQuickTestRecordSyncResult { Success = false, Message = "当前离线,无法同步" };
RubberQuickTestRecordLocalItem? item;
lock (_cacheLock)
{
item = _localItems.FirstOrDefault(x =>
string.Equals(x.LocalId, localId.Trim(), StringComparison.OrdinalIgnoreCase));
}
if (item == null)
return new RubberQuickTestRecordSyncResult { Success = false, Message = "未找到本地快检记录" };
if (string.Equals(item.SyncStatus, "Synced", StringComparison.OrdinalIgnoreCase))
return new RubberQuickTestRecordSyncResult { Success = true, Message = "记录已同步", RecordNo = item.Record.RecordNo };
if (!HasCompletePlanInfo(item.Record))
return new RubberQuickTestRecordSyncResult { Success = false, Message = PlanInfoValidationMessage };
if (!await _syncLock.WaitAsync(0, ct).ConfigureAwait(false))
return new RubberQuickTestRecordSyncResult { Success = false, Message = "同步正在进行中,请稍后再试" };
try
{
var recordNo = await RemoteAddAsync(item.Record, ct).ConfigureAwait(false);
lock (_cacheLock)
{
var target = _localItems.FirstOrDefault(x => x.LocalId == item.LocalId);
if (target == null)
return new RubberQuickTestRecordSyncResult { Success = false, Message = "记录已被删除" };
if (!string.IsNullOrWhiteSpace(recordNo))
target.Record.RecordNo = recordNo;
target.SyncStatus = "Synced";
target.SyncError = null;
SaveCacheToDiskUnsafe();
}
_eventAggregator.GetEvent<RubberQuickTestRecordChangedEvent>()
.Publish(new RubberQuickTestRecordChangedPayload { Action = "sync", RecordId = item.LocalId });
return new RubberQuickTestRecordSyncResult
{
Success = true,
Message = "同步成功",
RecordNo = recordNo ?? item.Record.RecordNo
};
}
catch (Exception ex)
{
lock (_cacheLock)
{
var target = _localItems.FirstOrDefault(x => x.LocalId == item.LocalId);
if (target != null)
{
target.SyncStatus = "Failed";
target.SyncError = ex.Message;
SaveCacheToDiskUnsafe();
}
}
_logger.Warning($"[快检记录同步] 手动同步失败 localId={localId}: {ex.Message}");
return new RubberQuickTestRecordSyncResult { Success = false, Message = ex.Message };
}
finally
{
_syncLock.Release();
}
}
private const string PlanInfoValidationMessage = "请维护密炼计划信息(密炼计划、密炼日期、密炼机台)";
private static bool HasCompletePlanInfo(MesXslRubberQuickTestRecord record) =>
!string.IsNullOrWhiteSpace(record.ProductionPlanNo)
&& record.ProductionDate != null
&& !string.IsNullOrWhiteSpace(record.ProdEquipmentName);
public string GenerateRecordNo(string rubberMaterialName)
{
var dateStr = DateTime.Now.ToString("yyyyMMdd");
@@ -327,61 +477,6 @@ public class RubberQuickTestRecordService : IRubberQuickTestRecordService, ISing
return result.GetProperty("records").Deserialize<List<MesXslRubberQuickTestRecord>>(_jsonOpts) ?? new();
}
private void OnNetworkStatusChanged(bool isOnline)
{
if (!isOnline) return;
_ = Task.Run(() => PushPendingAsync(CancellationToken.None));
}
private async Task PushPendingAsync(CancellationToken ct)
{
if (!await _syncLock.WaitAsync(0, ct).ConfigureAwait(false)) return;
try
{
List<RubberQuickTestRecordLocalItem> pending;
lock (_cacheLock)
{
pending = _localItems.Where(x => x.SyncStatus != "Synced").Select(CloneLocalItem).ToList();
}
foreach (var item in pending)
{
if (!_networkMonitor.IsOnline) break;
try
{
var recordNo = await RemoteAddAsync(item.Record, ct).ConfigureAwait(false);
lock (_cacheLock)
{
var target = _localItems.FirstOrDefault(x => x.LocalId == item.LocalId);
if (target == null) continue;
if (!string.IsNullOrWhiteSpace(recordNo))
target.Record.RecordNo = recordNo;
target.SyncStatus = "Synced";
target.SyncError = null;
SaveCacheToDiskUnsafe();
}
}
catch (Exception ex)
{
lock (_cacheLock)
{
var target = _localItems.FirstOrDefault(x => x.LocalId == item.LocalId);
if (target != null)
{
target.SyncStatus = "Failed";
target.SyncError = ex.Message;
SaveCacheToDiskUnsafe();
}
}
}
}
}
finally
{
_syncLock.Release();
}
}
private static List<RubberQuickTestRecordListRow> ApplyFilters(
List<RubberQuickTestRecordListRow> source,
string? filterRecordNo,

View File

@@ -54,35 +54,20 @@ public class RubberQuickTestStdService : IRubberQuickTestStdService, ISingletonD
LoadCacheFromDisk();
_logger.Information($"[快检实验标准] 服务初始化,缓存={_localCache.Count},在线={_networkMonitor.IsOnline}");
_networkMonitor.StatusChanged += OnNetworkStatusChanged;
if (_networkMonitor.IsOnline)
_ = Task.Run(() => SyncFromRemoteAsync(CancellationToken.None));
}
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<RubberQuickTestStdPageResult> PageAsync(
public Task<RubberQuickTestStdPageResult> PageAsync(
int pageNo, int pageSize,
string? stdName = null,
string? rubberMaterialName = null,
string? enableStatus = null,
CancellationToken ct = default)
{
if (_networkMonitor.IsOnline)
{
try
{
await SyncFromRemoteAsync(ct).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.Warning($"[快检实验标准] 列表拉取失败,使用本地缓存:{ex.Message}");
}
}
ct.ThrowIfCancellationRequested();
List<MesXslRubberQuickTestStd> source;
lock (_cacheLock)
source = _localCache.Select(CloneMain).ToList();
@@ -93,7 +78,7 @@ public class RubberQuickTestStdService : IRubberQuickTestStdService, ISingletonD
.Skip(Math.Max(0, (pageNo - 1) * pageSize))
.Take(pageSize)
.ToList();
return new RubberQuickTestStdPageResult(records, total, pageNo, pageSize);
return Task.FromResult(new RubberQuickTestStdPageResult(records, total, pageNo, pageSize));
}
public async Task<MesXslRubberQuickTestStd?> GetByIdAsync(string id, CancellationToken ct = default)
@@ -199,6 +184,8 @@ public class RubberQuickTestStdService : IRubberQuickTestStdService, ISingletonD
lock (_cacheLock)
localSnapshot = _localCache.Select(CloneMain).ToList();
await HydrateStdLinesAsync(records, localSnapshot, ct).ConfigureAwait(false);
var (merged, stats) = MesReadOnlyCacheMergeHelper.Merge(
localSnapshot,
records,
@@ -233,18 +220,76 @@ public class RubberQuickTestStdService : IRubberQuickTestStdService, ISingletonD
}
}
private void OnNetworkStatusChanged(bool isOnline)
private async Task HydrateStdLinesAsync(
List<MesXslRubberQuickTestStd> remoteRecords,
List<MesXslRubberQuickTestStd> localSnapshot,
CancellationToken ct)
{
if (!isOnline) return;
_ = Task.Run(async () =>
var localById = localSnapshot
.Where(x => !string.IsNullOrWhiteSpace(x.Id))
.ToDictionary(x => x.Id!, StringComparer.OrdinalIgnoreCase);
using var gate = new SemaphoreSlim(8);
var tasks = remoteRecords
.Where(r => !string.IsNullOrWhiteSpace(r.Id))
.Select(async record =>
{
if (!await SyncFromRemoteAsync(CancellationToken.None).ConfigureAwait(false))
await gate.WaitAsync(ct).ConfigureAwait(false);
try
{
localById.TryGetValue(record.Id!, out var local);
if (local != null
&& IsStdListContentEqual(record, local)
&& local.LineList is { Count: > 0 })
{
record.LineList = local.LineList.Select(CloneStdLine).ToList();
return;
_eventAggregator.GetEvent<RubberQuickTestStdChangedEvent>()
.Publish(new RubberQuickTestStdChangedPayload { Action = "reconnect" });
});
}
var lines = await FetchLinesByStdIdAsync(record.Id!, ct).ConfigureAwait(false);
record.LineList = lines.Count > 0
? lines
: local?.LineList?.Select(CloneStdLine).ToList();
}
finally
{
gate.Release();
}
});
await Task.WhenAll(tasks).ConfigureAwait(false);
}
private async Task<List<MesXslRubberQuickTestStdLine>> FetchLinesByStdIdAsync(string stdId, CancellationToken ct)
{
var url = $"{BaseUrl}/xslmes/mesXslRubberQuickTestStd/anon/queryLineListByStdId?id={Uri.EscapeDataString(stdId)}";
using var client = CreateClient();
using var resp = await client.GetAsync(url, ct).ConfigureAwait(false);
if (!resp.IsSuccessStatusCode)
return [];
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
using var doc = JsonDocument.Parse(json);
if (!doc.RootElement.TryGetProperty("result", out var resultEl))
return [];
return resultEl.Deserialize<List<MesXslRubberQuickTestStdLine>>(_jsonOpts) ?? [];
}
private static MesXslRubberQuickTestStdLine CloneStdLine(MesXslRubberQuickTestStdLine l) => new()
{
Id = l.Id,
StdId = l.StdId,
DataPointId = l.DataPointId,
PointName = l.PointName,
LowerLimit = l.LowerLimit,
UpperLimit = l.UpperLimit,
LowerWarn = l.LowerWarn,
UpperWarn = l.UpperWarn,
TargetValue = l.TargetValue,
SortNo = l.SortNo
};
private static List<MesXslRubberQuickTestStd> ApplyFilters(
List<MesXslRubberQuickTestStd> source,
string? stdName,

View File

@@ -2,33 +2,52 @@ using Prism.Events;
using System.Text.Json;
using YY.Admin.Core;
using YY.Admin.Core.Events;
using YY.Admin.Core.Services;
using YY.Admin.Services.Service;
namespace YY.Admin.Services.Service.RubberQuickTestStd;
public class RubberQuickTestStdSyncCoordinator : ISingletonDependency
{
private readonly IEventAggregator _eventAggregator;
private readonly IRubberQuickTestStdService _stdService;
private readonly ILoggerService _logger;
public RubberQuickTestStdSyncCoordinator(
IEventAggregator eventAggregator,
SyncPollManager pollManager,
IRubberQuickTestStdService stdService,
MesQuickTestDataPollManager pollManager,
ILoggerService logger)
{
_eventAggregator = eventAggregator;
_stdService = stdService;
_logger = logger;
_eventAggregator.GetEvent<RemoteCommandReceivedEvent>()
.Subscribe(OnRemoteCommand, ThreadOption.BackgroundThread);
pollManager.Register("胶料快检实验标准", () =>
{
_eventAggregator.GetEvent<RubberQuickTestStdChangedEvent>()
.Publish(new RubberQuickTestStdChangedPayload { Action = "poll" });
return Task.CompletedTask;
});
_eventAggregator.GetEvent<NetworkStatusChangedEvent>()
.Subscribe(OnNetworkStatusChanged, ThreadOption.BackgroundThread);
pollManager.Register("胶料快检实验标准", () => PollSyncAsync("poll"));
_logger.Information("[快检实验标准] RubberQuickTestStdSyncCoordinator 已启动");
_ = PollSyncAsync("startup");
}
private async Task PollSyncAsync(string action)
{
if (!await _stdService.SyncFromRemoteAsync(CancellationToken.None).ConfigureAwait(false))
return;
_eventAggregator.GetEvent<RubberQuickTestStdChangedEvent>()
.Publish(new RubberQuickTestStdChangedPayload { Action = action });
}
private void OnNetworkStatusChanged(NetworkStatusChangedPayload payload)
{
if (!payload.IsOnline) return;
_ = Task.Run(() => PollSyncAsync("reconnect"));
}
private void OnRemoteCommand(RemoteCommandPayload payload)
@@ -52,7 +71,11 @@ public class RubberQuickTestStdSyncCoordinator : ISingletonDependency
StdId = idEl.ValueKind == JsonValueKind.String ? idEl.GetString() : null
};
_logger.Information($"[快检实验标准] STOMP action={changed.Action}, stdId={changed.StdId}");
_ = Task.Run(async () =>
{
if (await _stdService.SyncFromRemoteAsync(CancellationToken.None).ConfigureAwait(false))
_eventAggregator.GetEvent<RubberQuickTestStdChangedEvent>().Publish(changed);
});
}
catch (Exception ex)
{

View File

@@ -0,0 +1,18 @@
namespace YY.Admin.Event
{
/// <summary>
/// 按 ViewName 关闭 Tab 并可激活指定列表页
/// </summary>
public class TabCloseRequestPayload
{
public string ViewNameToClose { get; set; } = string.Empty;
public string? ActivateViewName { get; set; }
public string? ActivateTabName { get; set; }
}
public class TabCloseRequestEvent : PubSubEvent<TabCloseRequestPayload>
{
}
}

View File

@@ -62,10 +62,10 @@ public class NetworkMonitor : INetworkMonitor
private async Task MonitorLoopAsync(CancellationToken cancellationToken)
{
// 启动后立即探活一次,避免首屏 10 秒内 IsOnline 恒为 false
// 启动后立即探活一次,避免首屏秒内 IsOnline 恒为 false
await RunHttpProbeAndRecomputeAsync(cancellationToken).ConfigureAwait(false);
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(10));
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false))
{
await RunHttpProbeAndRecomputeAsync(cancellationToken).ConfigureAwait(false);

View File

@@ -93,6 +93,8 @@ public class SyncModule : IModule
containerRegistry.RegisterSingleton<IRubberQuickTestRecordService, RubberQuickTestRecordService>();
containerRegistry.RegisterSingleton<IRubberQuickTestOperationService, RubberQuickTestOperationService>();
// 快检相关 MES 只读数据 2 秒轮询(须在协调器之前注册)
containerRegistry.RegisterSingleton<MesQuickTestDataPollManager>();
// 胶料快检实验标准MES 只读同步)
containerRegistry.RegisterSingleton<IRubberQuickTestStdService, RubberQuickTestStdService>();
containerRegistry.RegisterSingleton<RubberQuickTestStdSyncCoordinator>();
@@ -171,6 +173,8 @@ public class SyncModule : IModule
// 强制实例化打印模板同步协调器
_ = containerProvider.Resolve<PrintTemplateSyncCoordinator>();
_ = containerProvider.Resolve<PrintBizTemplateBindSyncCoordinator>();
// 快检数据轮询须在协调器之前实例化,确保 Register 能正常接收
_ = containerProvider.Resolve<MesQuickTestDataPollManager>();
// 胶料快检实验标准只读同步协调器
_ = containerProvider.Resolve<RubberQuickTestStdSyncCoordinator>();
// 密炼计划只读同步协调器

View File

@@ -195,6 +195,7 @@ namespace YY.Admin.ViewModels
private SubscriptionToken? _openOrActivateTabToken;
private SubscriptionToken? _tabClosedToken;
private SubscriptionToken? _tabCloseRequestToken;
private SubscriptionToken? _refreshTabToken;
private SubscriptionToken? _loginOutToken;
@@ -235,6 +236,7 @@ namespace YY.Admin.ViewModels
// 订阅事件
_openOrActivateTabToken = _eventAggregator.GetEvent<TabSourceSelectedEvent>().Subscribe(OnOpenOrActivateTab);
_tabClosedToken = _eventAggregator.GetEvent<TabClosedEvent>().Subscribe(OnTabClosed);
_tabCloseRequestToken = _eventAggregator.GetEvent<TabCloseRequestEvent>().Subscribe(OnTabCloseRequest, ThreadOption.UIThread);
_refreshTabToken = _eventAggregator.GetEvent<TabRefreshEvent>().Subscribe(OnRefreshTab);
_loginOutToken = _eventAggregator.GetEvent<SysUserEvents.LoginOutEvent>().Subscribe(Destroy);
@@ -489,6 +491,48 @@ namespace YY.Admin.ViewModels
_ = NavigateToViewAsync(CommonConst.ContentRegion, tabItemModel.ViewName, tabItemModel.TabSource.NavigationParameter);
}
/// <summary>
/// 按 ViewName 关闭 Tab并可选激活指定页面
/// </summary>
private void OnTabCloseRequest(TabCloseRequestPayload payload)
{
if (payload == null || string.IsNullOrWhiteSpace(payload.ViewNameToClose))
{
return;
}
var tabsToClose = OpenTabs
.Where(t => t.IsClosable
&& string.Equals(t.ViewName, payload.ViewNameToClose, StringComparison.OrdinalIgnoreCase))
.ToList();
foreach (var tab in tabsToClose)
{
_eventAggregator.GetEvent<TabClosedEvent>().Publish(tab);
OpenTabs.Remove(tab);
}
if (string.IsNullOrWhiteSpace(payload.ActivateViewName))
{
return;
}
var existingTab = OpenTabs.FirstOrDefault(t =>
string.Equals(t.ViewName, payload.ActivateViewName, StringComparison.OrdinalIgnoreCase));
if (existingTab != null)
{
SelectedTab = existingTab;
return;
}
OnOpenOrActivateTab(new TabSource
{
Name = payload.ActivateTabName ?? payload.ActivateViewName,
Icon = "\ue7de",
ViewName = payload.ActivateViewName
});
}
/// <summary>
/// TabItem关闭回调
/// </summary>
@@ -764,6 +808,15 @@ namespace YY.Admin.ViewModels
_tabClosedToken = null;
}
if (_tabCloseRequestToken != null)
{
_eventAggregator
.GetEvent<TabCloseRequestEvent>()
.Unsubscribe(_tabCloseRequestToken);
_tabCloseRequestToken = null;
}
if (_refreshTabToken != null)
{
_eventAggregator

View File

@@ -27,7 +27,7 @@ using YY.Admin.Core.Services;
using YY.Admin.Core.Session;
using YY.Admin.Event;
namespace YY.Admin.ViewModels.RubberQuickTest;
@@ -178,12 +178,17 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
private readonly IRubberQuickTestStdService _stdService;
private readonly INetworkMonitor _networkMonitor;
private readonly Random _rnd = new();
private bool _saveInProgress;
private string? _loadedDetailLocalId;
private string? _loadedDetailMesId;
private string? _editingLocalId;
private DateTime? _originalCreateTime;
private bool _isLoadingForm;
private int _navigationApplyVersion;
private List<MesXslMixingProductionPlan> _allPlans = new();
@@ -196,6 +201,8 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
private SubscriptionToken? _stdChangedToken;
private SubscriptionToken? _networkStatusToken;
private const int ChartPointCount = 5;
/// <summary>曲线横坐标时间点min0、0.5、1、1.5、2</summary>
@@ -215,6 +222,8 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
IRubberQuickTestStdService stdService,
INetworkMonitor networkMonitor,
IContainerExtension container,
IRegionManager regionManager) : base(container, regionManager)
@@ -227,20 +236,20 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
_stdService = stdService;
_networkMonitor = networkMonitor;
_inspectTimesText = "1";
AddInspectRowCommand = new DelegateCommand(AddInspectRow, () => DataPointColumns.Count > 0);
AddInspectRowCommand = new DelegateCommand(AddInspectRow, () => DataPointColumns.Count > 0 && !IsReadOnly);
RemoveInspectRowCommand = new DelegateCommand(() => RemoveInspectRow(SelectedInspectRow), () => SelectedInspectRow != null);
RemoveInspectRowCommand = new DelegateCommand(() => RemoveInspectRow(SelectedInspectRow), () => SelectedInspectRow != null && !IsReadOnly);
SaveCommand = new DelegateCommand(async () => await SaveAsync(), () => CanSave);
RefreshPlansCommand = new DelegateCommand(async () => await LoadLocalDataAsync(showSuccess: true));
RefreshChartDemoCommand = new DelegateCommand(FillRandomChartData);
@@ -268,9 +277,45 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
_stdChangedToken = _eventAggregator.GetEvent<RubberQuickTestStdChangedEvent>()
.Subscribe(async _ => await ReloadLocalDataQuietAsync(), ThreadOption.UIThread);
_networkStatusToken = _eventAggregator.GetEvent<NetworkStatusChangedEvent>()
.Subscribe(OnNetworkStatusChanged, ThreadOption.UIThread);
IsOfflineMode = !_networkMonitor.IsOnline;
RecalculateOverallInspectResult();
}
private void OnNetworkStatusChanged(NetworkStatusChangedPayload payload)
{
IsOfflineMode = !payload.IsOnline;
if (IsOfflineMode)
RefreshRubberMaterialOptions();
}
private bool _isOfflineMode;
/// <summary>断网时自动进入离线模式</summary>
public bool IsOfflineMode
{
get => _isOfflineMode;
private set
{
if (!SetProperty(ref _isOfflineMode, value)) return;
RaisePropertyChanged(nameof(PlanFieldsRequired));
RaisePropertyChanged(nameof(IsOnlineMode));
RaisePropertyChanged(nameof(RubberMaterialName));
if (value)
RefreshRubberMaterialOptions();
}
}
public bool IsOnlineMode => !IsOfflineMode;
public bool PlanFieldsRequired => !IsOfflineMode;
public string OfflineModeHint => IsOfflineMode
? "当前离线:密炼日期、机台、班次、密炼计划可选填;请手动选择胶料名称,保存后待联网同步"
: string.Empty;
private bool _isReadOnly;
public bool IsReadOnly
{
@@ -281,12 +326,16 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
RaisePropertyChanged(nameof(IsEditable));
RaisePropertyChanged(nameof(ShowSaveButton));
NotifySaveStateChanged();
AddInspectRowCommand.RaiseCanExecuteChanged();
RemoveInspectRowCommand.RaiseCanExecuteChanged();
InspectColumnsChanged?.Invoke();
}
}
public bool IsEditable => !IsReadOnly;
public bool ShowSaveButton => !IsReadOnly;
public bool IsEditMode => !string.IsNullOrWhiteSpace(_editingLocalId);
public string SaveButtonText => IsEditMode ? "保存修改" : "保存胶料快检记录";
public bool CanSave => InspectRows.Count > 0 && !_saveInProgress && !IsReadOnly;
private void NotifySaveStateChanged()
@@ -314,6 +363,7 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
{
if (parameters.TryGetValue<string>("localId", out var localId) && !string.IsNullOrWhiteSpace(localId))
{
_editingLocalId = null;
_loadedDetailLocalId = localId;
_loadedDetailMesId = null;
await LoadFromLocalItemAsync(localId);
@@ -322,6 +372,7 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
if (parameters.TryGetValue<string>("mesId", out var mesId) && !string.IsNullOrWhiteSpace(mesId))
{
_editingLocalId = null;
_loadedDetailMesId = mesId;
_loadedDetailLocalId = null;
await LoadFromMesIdAsync(mesId);
@@ -329,10 +380,22 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
}
}
if (parameters.TryGetValue<string>("editLocalId", out var editLocalId) && !string.IsNullOrWhiteSpace(editLocalId))
{
_editingLocalId = editLocalId;
_loadedDetailLocalId = null;
_loadedDetailMesId = null;
IsReadOnly = false;
await LoadForEditAsync(editLocalId);
return;
}
if (version != _navigationApplyVersion) return;
_loadedDetailLocalId = null;
_loadedDetailMesId = null;
_editingLocalId = null;
_originalCreateTime = null;
IsReadOnly = false;
ResetFormForNewEntry();
await LoadLocalDataAsync(showSuccess: false);
@@ -352,7 +415,10 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
return false;
}
return !IsReadOnly && _loadedDetailLocalId == null && _loadedDetailMesId == null;
if (parameters.TryGetValue<string>("editLocalId", out var editLocalId) && !string.IsNullOrWhiteSpace(editLocalId))
return !IsReadOnly && string.Equals(_editingLocalId, editLocalId, StringComparison.OrdinalIgnoreCase);
return !IsReadOnly && _editingLocalId == null && _loadedDetailLocalId == null && _loadedDetailMesId == null;
}
private void ResetFormForNewEntry()
@@ -360,6 +426,7 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
RecordNo = null;
TrainNo = null;
InspectTimesText = "1";
MixingDate = DateTime.Today;
ClearPlanAndStdSelection();
RecalculateOverallInspectResult();
NotifySaveStateChanged();
@@ -420,6 +487,8 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
public ObservableCollection<MesXslRubberQuickTestStd> StdOptions { get; } = new();
public ObservableCollection<string> RubberMaterialOptions { get; } = new();
public ObservableCollection<MesXslRubberQuickTestStdLine> DataPointColumns { get; } = new();
public ObservableCollection<QuickTestInspectRowViewModel> InspectRows { get; } = new();
@@ -448,9 +517,9 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
private DateTime _mixingDate = DateTime.Today;
private DateTime? _mixingDate = DateTime.Today;
public DateTime MixingDate
public DateTime? MixingDate
{
@@ -464,7 +533,8 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
RefreshMachineOptions();
ClearPlanAndStdSelection();
if (!_isLoadingForm)
OnPlanFilterChanged();
}
@@ -488,7 +558,8 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
RefreshShiftOptions();
ClearPlanAndStdSelection();
if (!_isLoadingForm)
OnPlanFilterChanged();
}
@@ -512,7 +583,8 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
RefreshPlanOptions();
ClearPlanAndStdSelection();
if (!_isLoadingForm)
OnPlanFilterChanged();
}
@@ -568,7 +640,30 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
public string? WorkShiftDisplay => SelectedShift?.Name ?? string.Empty;
public string? RubberMaterialName => _selectedPlan?.MaterialName ?? _selectedPlan?.FormulaName;
private string? _selectedRubberMaterial;
/// <summary>离线模式下手动选择的胶料名称</summary>
public string? SelectedRubberMaterial
{
get => _selectedRubberMaterial;
set
{
if (!SetProperty(ref _selectedRubberMaterial, value)) return;
RaisePropertyChanged(nameof(RubberMaterialName));
if (IsOfflineMode && _selectedPlan == null)
{
ClearStdSelection();
RefreshStdOptions();
}
}
}
public string? RubberMaterialName =>
(IsEditMode && !string.IsNullOrWhiteSpace(SelectedRubberMaterial))
? SelectedRubberMaterial
: _selectedPlan?.MaterialName
?? _selectedPlan?.FormulaName
?? (IsOfflineMode ? SelectedRubberMaterial : null);
@@ -699,8 +794,6 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
public DelegateCommand SaveCommand { get; }
public DelegateCommand RefreshPlansCommand { get; }
public DelegateCommand RefreshChartDemoCommand { get; }
@@ -720,6 +813,7 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
_allStds = await _stdService.GetAllCachedAsync();
RefreshMachineOptions();
RefreshRubberMaterialOptions();
if (showSuccess)
@@ -760,7 +854,11 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
_allStds = await _stdService.GetAllCachedAsync();
RefreshMachineOptions();
RefreshRubberMaterialOptions();
if (IsEditMode)
EnsureCurrentStdInOptions();
else
RefreshStdOptions();
}
@@ -777,9 +875,27 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
private IEnumerable<MesXslMixingProductionPlan> FilteredByDate =>
private void RefreshRubberMaterialOptions()
{
RubberMaterialOptions.Clear();
foreach (var name in _allStds
.Where(s => s.EnableStatus == "1" && s.AuditStatus == "1")
.Select(s => s.RubberMaterialName?.Trim())
.Where(n => !string.IsNullOrWhiteSpace(n))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(n => n, StringComparer.OrdinalIgnoreCase))
RubberMaterialOptions.Add(name!);
_allPlans.Where(p => p.PlanDate?.Date == MixingDate.Date);
if (SelectedRubberMaterial != null && !RubberMaterialOptions.Contains(SelectedRubberMaterial))
SelectedRubberMaterial = null;
}
private IEnumerable<MesXslMixingProductionPlan> FilteredByDate =>
MixingDate == null
? Enumerable.Empty<MesXslMixingProductionPlan>()
: _allPlans.Where(p => p.PlanDate?.Date == MixingDate.Value.Date);
@@ -797,7 +913,7 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
SelectedMachine = null;
if (SelectedMachine == null && MachineOptions.Count > 0)
if (SelectedMachine == null && MachineOptions.Count > 0 && !IsEditMode && !_isLoadingForm)
SelectedMachine = MachineOptions[0];
@@ -835,7 +951,7 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
SelectedShift = null;
if (SelectedShift == null && ShiftOptions.Count > 0)
if (SelectedShift == null && ShiftOptions.Count > 0 && !IsEditMode && !_isLoadingForm)
SelectedShift = ShiftOptions[0];
@@ -873,7 +989,7 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
SelectedPlan = null;
if (SelectedPlan == null && PlanOptions.Count > 0)
if (SelectedPlan == null && PlanOptions.Count > 0 && !IsEditMode && !_isLoadingForm)
SelectedPlan = PlanOptions[0];
@@ -881,6 +997,26 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
/// <summary>密炼日期/机台/班次变更:编辑模式仅重置计划,不清空实验标准与检验明细</summary>
private void OnPlanFilterChanged()
{
if (IsEditMode)
ResetPlanSelectionOnly();
else
ClearPlanAndStdSelection();
}
private void ResetPlanSelectionOnly()
{
_selectedPlan = null;
RaisePropertyChanged(nameof(SelectedPlan));
RaisePropertyChanged(nameof(MachineName));
RaisePropertyChanged(nameof(WorkShiftDisplay));
RaisePropertyChanged(nameof(RubberMaterialName));
}
private void ClearPlanAndStdSelection()
{
@@ -893,6 +1029,11 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
RaisePropertyChanged(nameof(WorkShiftDisplay));
if (!IsOfflineMode)
{
SelectedRubberMaterial = null;
}
RaisePropertyChanged(nameof(RubberMaterialName));
ClearStdSelection();
@@ -920,19 +1061,34 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
private void ApplySelectedPlan()
{
RaisePropertyChanged(nameof(MachineName));
RaisePropertyChanged(nameof(WorkShiftDisplay));
RaisePropertyChanged(nameof(RubberMaterialName));
ClearStdSelection();
if (IsEditMode)
{
EnsureCurrentStdInOptions();
return;
}
RefreshStdOptions();
}
/// <summary>编辑模式下保留已选实验标准,避免下拉选项刷新导致标准与明细被清空</summary>
private void EnsureCurrentStdInOptions()
{
if (_selectedStd == null || string.IsNullOrWhiteSpace(_selectedStd.StdName))
{
return;
}
if (!StdOptions.Any(s => string.Equals(s.Id, _selectedStd.Id, StringComparison.OrdinalIgnoreCase)))
{
StdOptions.Insert(0, _selectedStd);
}
RaisePropertyChanged(nameof(SelectedStd));
}
@@ -986,6 +1142,24 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
{
if (SelectedStd == null || string.IsNullOrWhiteSpace(SelectedStd.Id))
{
if (_currentStd != null || DataPointColumns.Count > 0 || InspectRows.Count > 0)
{
DataPointColumns.Clear();
InspectRows.Clear();
_currentStd = null;
TestMethodName = null;
InspectColumnsChanged?.Invoke();
RecalculateOverallInspectResult();
}
return;
}
if (string.Equals(SelectedStd.Id, _currentStd?.Id, StringComparison.OrdinalIgnoreCase)
&& DataPointColumns.Count > 0)
return;
DataPointColumns.Clear();
InspectRows.Clear();
@@ -998,10 +1172,6 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
RecalculateOverallInspectResult();
if (SelectedStd == null || string.IsNullOrWhiteSpace(SelectedStd.Id)) return;
IsLoading = true;
try
@@ -1213,6 +1383,8 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
return;
}
if (!IsOfflineMode)
{
if (SelectedPlan == null)
{
Growl.Warning("请选择密炼计划");
@@ -1224,6 +1396,12 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
Growl.Warning("请先选择密炼计划以带出胶料名称");
return;
}
}
else if (string.IsNullOrWhiteSpace(RubberMaterialName))
{
Growl.Warning("请选择胶料名称");
return;
}
if (SelectedStd == null || _currentStd == null)
{
@@ -1262,16 +1440,25 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
IsLoading = true;
var record = BuildSaveRecord(stdLineList, rawLineList, chartPointList, inspectTimes, user);
var saved = await _recordService.SaveAsync(record);
RubberQuickTestRecordSaveResult saved;
if (!string.IsNullOrWhiteSpace(_editingLocalId))
saved = await _recordService.UpdateLocalAsync(_editingLocalId, record);
else
saved = await _recordService.SaveAsync(record);
RecordNo = saved.Record.RecordNo;
var syncHint = saved.SyncStatus == "Synced" ? "并已同步到 MES" : "MES 同步待重试)";
var syncHint = saved.SyncStatus switch
{
"Synced" => "并已同步到 MES",
"Pending" => "(未同步,联网后可在列表中手动同步)",
_ => "MES 同步失败,可在列表中重试同步)"
};
var actionText = IsEditMode ? "修改已保存" : "胶料快检记录已保存";
Growl.Success(string.IsNullOrWhiteSpace(RecordNo)
? $"胶料快检记录已保存到本地{syncHint}"
: $"胶料快检记录已保存,快检记录号:{RecordNo}{syncHint}");
? $"{actionText}到本地{syncHint}"
: $"{actionText},快检记录号:{RecordNo}{syncHint}");
InspectRows.Clear();
NotifySaveStateChanged();
NavigateBackToRecordList();
}
catch (Exception ex)
{
@@ -1287,6 +1474,16 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
private void NavigateBackToRecordList()
{
_eventAggregator.GetEvent<TabCloseRequestEvent>().Publish(new TabCloseRequestPayload
{
ViewNameToClose = "RubberQuickTestOperationView",
ActivateViewName = "RubberQuickTestRecordListView",
ActivateTabName = "胶料快检记录"
});
}
private bool TryParseInspectTimes(out int value, out string error)
{
@@ -1501,11 +1698,13 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
InspectTime = DateTime.Now,
CreateTime = DateTime.Now,
InspectResult = OverallInspectResultCode,
RecordNo = _recordService.GenerateRecordNo(RubberMaterialName ?? string.Empty)
RecordNo = string.IsNullOrWhiteSpace(RecordNo)
? _recordService.GenerateRecordNo(RubberMaterialName ?? string.Empty)
: RecordNo,
CreateTime = _originalCreateTime ?? DateTime.Now
};
@@ -1545,16 +1744,24 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
if (!string.IsNullOrWhiteSpace(_selectedPlan?.MachineId))
var hasPlanContext = _selectedPlan != null || !IsOfflineMode;
if (hasPlanContext)
{
if (!string.IsNullOrWhiteSpace(_selectedPlan?.MachineId))
record.ProdEquipmentLedgerId = _selectedPlan.MachineId;
if (!string.IsNullOrWhiteSpace(MachineName))
record.ProdEquipmentName = MachineName;
if (SelectedShift != null && !string.IsNullOrWhiteSpace(SelectedShift.Code))
record.WorkShift = SelectedShift.Code;
if (!string.IsNullOrWhiteSpace(_selectedPlan?.PlanNo))
record.ProductionPlanNo = _selectedPlan.PlanNo;
}
if (MixingDate != null)
record.ProductionDate = MixingDate;
@@ -1565,22 +1772,10 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
if (SelectedShift != null && !string.IsNullOrWhiteSpace(SelectedShift.Code))
record.WorkShift = SelectedShift.Code;
record.InspectTimes = inspectTimes;
if (!string.IsNullOrWhiteSpace(_selectedPlan?.PlanNo))
record.ProductionPlanNo = _selectedPlan.PlanNo;
var inspectorId = user?.JeecgBizUserId ?? (user != null ? user.Id.ToString() : null);
if (!string.IsNullOrWhiteSpace(inspectorId))
@@ -1649,7 +1844,32 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
return;
}
await ApplyRecordDetailAsync(item.Record);
await ApplyRecordDetailAsync(item.Record, readOnly: true);
}
private async Task LoadForEditAsync(string localId)
{
var item = _recordService.GetByLocalId(localId);
if (item == null)
{
Growl.Warning("未找到快检记录");
return;
}
if (string.Equals(item.SyncStatus, "Synced", StringComparison.OrdinalIgnoreCase))
{
Growl.Warning("已同步记录不可编辑");
return;
}
_editingLocalId = localId;
_originalCreateTime = item.Record.CreateTime;
IsReadOnly = false;
RaisePropertyChanged(nameof(IsEditMode));
RaisePropertyChanged(nameof(SaveButtonText));
await LoadLocalDataAsync(showSuccess: false);
await ApplyRecordDetailAsync(item.Record, readOnly: false);
}
private async Task LoadFromMesIdAsync(string mesId)
@@ -1661,15 +1881,17 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
return;
}
await ApplyRecordDetailAsync(record);
await ApplyRecordDetailAsync(record, readOnly: true);
}
private async Task ApplyRecordDetailAsync(MesXslRubberQuickTestRecord r)
private async Task ApplyRecordDetailAsync(MesXslRubberQuickTestRecord r, bool readOnly)
{
IsReadOnly = true;
_isLoadingForm = true;
try
{
IsReadOnly = readOnly;
RecordNo = r.RecordNo;
MixingDate = r.ProductionDate ?? DateTime.Today;
SelectedMachine = r.ProdEquipmentName;
MixingDate = r.ProductionDate ?? r.CreateTime?.Date;
TrainNo = r.TrainNo;
InspectTimesText = r.InspectTimes?.ToString() ?? "1";
InspectorDisplay = r.InspectorRealname ?? ResolveInspectorDisplay();
@@ -1677,12 +1899,39 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
OverallInspectResultDisplay = string.Equals(r.InspectResult, "1", StringComparison.Ordinal) ? "合格" : "不合格";
TestMethodName = r.TestMethodName;
ClearPlanAndStdSelection();
SelectedRubberMaterial = r.RubberMaterialName;
MesXslMixingProductionPlan? matchedPlan = null;
if (!string.IsNullOrWhiteSpace(r.ProductionPlanNo))
{
matchedPlan = _allPlans.FirstOrDefault(p =>
string.Equals(p.PlanNo, r.ProductionPlanNo, StringComparison.OrdinalIgnoreCase));
}
if (matchedPlan != null)
{
RefreshMachineOptions();
SelectedMachine = matchedPlan.MachineName ?? r.ProdEquipmentName;
RefreshShiftOptions();
if (!string.IsNullOrWhiteSpace(r.WorkShift))
{
SelectedShift = ShiftOptions.FirstOrDefault(s => s.Code == r.WorkShift)
?? ShiftOptions.FirstOrDefault();
}
RefreshPlanOptions();
SelectedPlan = PlanOptions.FirstOrDefault(p => p.Id == matchedPlan.Id)
?? PlanOptions.FirstOrDefault(p => string.Equals(p.PlanNo, matchedPlan.PlanNo, StringComparison.OrdinalIgnoreCase));
}
else if (!string.IsNullOrWhiteSpace(r.ProdEquipmentName) || !string.IsNullOrWhiteSpace(r.ProductionPlanNo))
{
MachineOptions.Clear();
if (!string.IsNullOrWhiteSpace(r.ProdEquipmentName))
MachineOptions.Add(r.ProdEquipmentName);
SelectedMachine = r.ProdEquipmentName;
ShiftOptions.Clear();
var shift = new WorkShiftOption(r.WorkShift ?? "", r.WorkShift ?? "");
var shift = new WorkShiftOption(r.WorkShift ?? "", r.WorkShiftText ?? r.WorkShift ?? "");
ShiftOptions.Add(shift);
SelectedShift = shift;
@@ -1698,15 +1947,45 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
RaisePropertyChanged(nameof(SelectedPlan));
RaisePropertyChanged(nameof(MachineName));
RaisePropertyChanged(nameof(RubberMaterialName));
}
else if (!string.IsNullOrWhiteSpace(r.RubberMaterialName))
{
RefreshStdOptions();
}
_currentStd = null;
if (!string.IsNullOrWhiteSpace(r.StdId))
{
var cachedStd = await _stdService.GetWithLinesAsync(r.StdId);
if (cachedStd != null)
{
_currentStd = cachedStd;
StdOptions.Clear();
StdOptions.Add(cachedStd);
_selectedStd = cachedStd;
RaisePropertyChanged(nameof(SelectedStd));
TestMethodName = cachedStd.TestMethodName ?? r.TestMethodName;
}
}
if (_currentStd == null && !string.IsNullOrWhiteSpace(r.StdName))
{
var std = new MesXslRubberQuickTestStd { Id = r.StdId, StdName = r.StdName, TestMethodName = r.TestMethodName };
StdOptions.Clear();
StdOptions.Add(std);
_selectedStd = std;
RaisePropertyChanged(nameof(SelectedStd));
}
DataPointColumns.Clear();
InspectRows.Clear();
if (_currentStd?.LineList != null && _currentStd.LineList.Count > 0)
{
foreach (var line in _currentStd.LineList.OrderBy(x => x.SortNo ?? 0))
DataPointColumns.Add(line);
}
else
{
foreach (var sl in (r.StdLineList ?? new List<MesXslRubberQuickTestRecordStdLine>()).OrderBy(x => x.SortNo ?? 0))
{
DataPointColumns.Add(new MesXslRubberQuickTestStdLine
@@ -1721,6 +2000,7 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
SortNo = sl.SortNo
});
}
}
InspectColumnsChanged?.Invoke();
var grouped = (r.RawLineList ?? new List<MesXslRubberQuickTestRecordRawLine>())
@@ -1741,6 +2021,14 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
UpperLimit = col.UpperLimit,
Value = raw?.InspectValue
};
if (!readOnly)
{
cell.ValueChanged += _ =>
{
row.RecalculateResult();
RecalculateOverallInspectResult();
};
}
row.Cells.Add(cell);
}
row.RecalculateResult();
@@ -1761,6 +2049,14 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
if (LowerTempValues.Count > 0) LowerMoldTemp = LowerTempValues[^1].Y ?? 0;
if (TorqueValues.Count > 0) TorqueS = TorqueValues[^1].Y ?? 0;
RecalculateOverallInspectResult();
NotifySaveStateChanged();
}
finally
{
_isLoadingForm = false;
}
await Task.CompletedTask;
}
@@ -1859,6 +2155,12 @@ public class RubberQuickTestOperationViewModel : BaseViewModel, INavigationAware
}
if (_networkStatusToken != null)
{
_eventAggregator.GetEvent<NetworkStatusChangedEvent>().Unsubscribe(_networkStatusToken);
_networkStatusToken = null;
}
base.CleanUp();
}

View File

@@ -49,7 +49,9 @@ public class RubberQuickTestRecordListViewModel : BaseViewModel
public DelegateCommand ResetCommand { get; }
public DelegateCommand AddCommand { get; }
public DelegateCommand<RubberQuickTestRecordListRow> ViewDetailCommand { get; }
public DelegateCommand<RubberQuickTestRecordListRow> EditCommand { get; }
public DelegateCommand<RubberQuickTestRecordListRow> DeleteCommand { get; }
public DelegateCommand<RubberQuickTestRecordListRow> SyncCommand { get; }
public DelegateCommand PrevPageCommand { get; }
public DelegateCommand NextPageCommand { get; }
@@ -73,9 +75,13 @@ public class RubberQuickTestRecordListViewModel : BaseViewModel
});
AddCommand = new DelegateCommand(OpenAddPage);
ViewDetailCommand = new DelegateCommand<RubberQuickTestRecordListRow>(async r => await ShowDetailAsync(r));
EditCommand = new DelegateCommand<RubberQuickTestRecordListRow>(OpenEditPage, r => r != null && r.CanEdit);
DeleteCommand = new DelegateCommand<RubberQuickTestRecordListRow>(
async r => await DeleteAsync(r),
r => r != null && r.CanDelete);
SyncCommand = new DelegateCommand<RubberQuickTestRecordListRow>(
async r => await SyncAsync(r),
r => r != null && r.CanSync);
PrevPageCommand = new DelegateCommand(async () => { if (PageNo > 1) { PageNo--; await LoadAsync(); } });
NextPageCommand = new DelegateCommand(async () => { if ((long)PageNo * PageSize < Total) { PageNo++; await LoadAsync(); } });
@@ -137,6 +143,27 @@ public class RubberQuickTestRecordListViewModel : BaseViewModel
});
}
private void OpenEditPage(RubberQuickTestRecordListRow? row)
{
if (row == null || !row.CanEdit || string.IsNullOrWhiteSpace(row.LocalId))
return;
if (_recordService.GetByLocalId(row.LocalId) == null)
{
Growl.Warning("未找到快检记录");
return;
}
var label = row.RecordNo ?? row.LocalId;
_eventAggregator.GetEvent<TabSourceSelectedEvent>().Publish(new TabSource
{
Name = $"编辑快检记录 {label}",
Icon = "\ue7de",
ViewName = "RubberQuickTestOperationView",
NavigationParameter = new NavigationParameters { { "editLocalId", row.LocalId } }
});
}
private async Task DeleteAsync(RubberQuickTestRecordListRow? row)
{
if (row == null || !row.CanDelete || string.IsNullOrWhiteSpace(row.LocalId))
@@ -144,7 +171,7 @@ public class RubberQuickTestRecordListViewModel : BaseViewModel
var label = row.RecordNo ?? row.LocalId;
var confirm = System.Windows.MessageBox.Show(
$"确定删除同步失败的快检记录「{label}」?\n此操作仅删除本地记录不可恢复。",
$"确定删除同步的快检记录「{label}」?\n此操作仅删除本地记录不可恢复。",
"确认删除",
System.Windows.MessageBoxButton.YesNo,
System.Windows.MessageBoxImage.Warning);
@@ -153,14 +180,14 @@ public class RubberQuickTestRecordListViewModel : BaseViewModel
try
{
if (_recordService.DeleteFailedLocal(row.LocalId))
if (_recordService.DeleteUnsyncedLocal(row.LocalId))
{
Growl.Success("删除成功");
await LoadAsync();
}
else
{
Growl.Warning("仅同步失败的本地记录可删除");
Growl.Warning("仅同步的本地记录可删除");
}
}
catch (Exception ex)
@@ -169,6 +196,36 @@ public class RubberQuickTestRecordListViewModel : BaseViewModel
}
}
private async Task SyncAsync(RubberQuickTestRecordListRow? row)
{
if (row == null || !row.CanSync || string.IsNullOrWhiteSpace(row.LocalId))
return;
try
{
IsLoading = true;
var result = await _recordService.SyncLocalAsync(row.LocalId);
if (result.Success)
{
var no = result.RecordNo ?? row.RecordNo ?? row.LocalId;
Growl.Success($"同步成功,快检记录号:{no}");
await LoadAsync();
}
else
{
Growl.Warning(result.Message ?? "同步失败");
}
}
catch (Exception ex)
{
Growl.Error($"同步失败:{ex.Message}");
}
finally
{
IsLoading = false;
}
}
private async Task ShowDetailAsync(RubberQuickTestRecordListRow? row)
{
if (row == null) return;

View File

@@ -90,7 +90,7 @@
<TextBlock Text="重置" Style="{StaticResource IconButtonStyle}"/>
</StackPanel>
</Button>
<TextBlock Text="数据来自 MES 密炼生产计划维护,桌面端只读;断网时显示本地缓存,联网后自动刷新"
<TextBlock Text="数据来自 MES 密炼生产计划维护,每 2 秒自动同步;断网时显示本地缓存"
VerticalAlignment="Center"
Foreground="{DynamicResource SecondaryTextBrush}"
FontSize="12"/>

View File

@@ -50,7 +50,14 @@
<md:PackIcon Kind="Flask" Width="18" Height="18" Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<StackPanel VerticalAlignment="Center">
<StackPanel Orientation="Horizontal">
<TextBlock Text="无转子流变仪 MDR S3L" FontSize="18" FontWeight="Bold"/>
<Border Background="#fff7e6" BorderBrush="#ffa940" BorderThickness="1" CornerRadius="4"
Padding="8,2" Margin="12,0,0,0"
Visibility="{Binding IsOfflineMode, Converter={StaticResource Boolean2VisibilityConverter}}">
<TextBlock Text="离线模式" FontSize="12" FontWeight="SemiBold" Foreground="#d46b08"/>
</Border>
</StackPanel>
<TextBlock Text="胶料快检记录 · 密炼快检试验操作台" FontSize="11" Foreground="{DynamicResource SecondaryTextBrush}" Margin="0,2,0,0"/>
</StackPanel>
</StackPanel>
@@ -87,7 +94,8 @@
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,4">
<Border Width="4" Height="16" CornerRadius="2" Background="#1890ff"/>
<TextBlock Text="温度(℃)曲线图" Style="{StaticResource SectionTitleStyle}"/>
<Button Content="刷新演示" Command="{Binding RefreshChartDemoCommand}" Style="{StaticResource ButtonDefault}" Height="26" Padding="8,0" Margin="10,0,0,0"/>
<Button Content="刷新演示" Command="{Binding RefreshChartDemoCommand}" Style="{StaticResource ButtonDefault}" Height="26" Padding="8,0" Margin="10,0,0,0"
Visibility="{Binding IsEditable, Converter={StaticResource Boolean2VisibilityConverter}}"/>
</StackPanel>
<TextBlock Grid.Row="1" Text="{Binding ChartDemoHint}" FontSize="10" Foreground="{DynamicResource SecondaryTextBrush}" Margin="0,0,0,4"
TextTrimming="CharacterEllipsis"/>
@@ -132,7 +140,8 @@
<Border Width="4" Height="18" CornerRadius="2" Background="#1890ff"/>
<TextBlock Text="试验结果" Style="{StaticResource SectionTitleStyle}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right"
Visibility="{Binding IsEditable, Converter={StaticResource Boolean2VisibilityConverter}}">
<Button Content="新增检验行" Command="{Binding AddInspectRowCommand}" Style="{StaticResource ButtonPrimary}" Height="28" Padding="10,0" Margin="0,0,8,0"/>
<Button Content="删除选中行" Command="{Binding RemoveInspectRowCommand}" Style="{StaticResource ButtonDanger}" Height="28" Padding="10,0"/>
</StackPanel>
@@ -145,7 +154,7 @@
CanUserDeleteRows="False"
HeadersVisibility="Column"
SelectionMode="Single"
IsReadOnly="False"
IsReadOnly="{Binding IsReadOnly}"
RowHeight="30"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
@@ -153,7 +162,8 @@
Style="{StaticResource CusDataGridStyle}"
ColumnHeaderStyle="{StaticResource CusDataGridColumnHeaderStyle}">
</DataGrid>
<TextBlock Grid.Row="2" Text="请手填各数据点检测值,系统将根据数据标准上下限自动判定合格/不合格" FontSize="11" Foreground="{DynamicResource SecondaryTextBrush}" Margin="0,6,0,0"/>
<TextBlock Grid.Row="2" Text="请手填各数据点检测值,系统将根据数据标准上下限自动判定合格/不合格" FontSize="11" Foreground="{DynamicResource SecondaryTextBrush}" Margin="0,6,0,0"
Visibility="{Binding IsEditable, Converter={StaticResource Boolean2VisibilityConverter}}"/>
</Grid>
</Border>
</Grid>
@@ -212,9 +222,6 @@
<Border Width="4" Height="18" CornerRadius="2" Background="#1890ff"/>
<TextBlock Text="试验信息" Style="{StaticResource SectionTitleStyle}"/>
</StackPanel>
<Button Content="刷新本地数据" Command="{Binding RefreshPlansCommand}"
Style="{StaticResource ButtonDefault}" Height="26" Padding="8,0"
HorizontalAlignment="Right" VerticalAlignment="Center"/>
</Grid>
<hc:Row Gutter="10">
@@ -234,6 +241,7 @@
<!-- 密炼日期 -->
<hc:Col Span="12">
<hc:DatePicker SelectedDate="{Binding MixingDate}"
IsEnabled="{Binding IsEditable}"
hc:InfoElement.Title="密炼日期"
hc:InfoElement.TitleWidth="90"
hc:InfoElement.TitlePlacement="Left"
@@ -245,6 +253,7 @@
<hc:Col Span="12">
<hc:ComboBox ItemsSource="{Binding MachineOptions}"
SelectedItem="{Binding SelectedMachine}"
IsEnabled="{Binding IsEditable}"
hc:InfoElement.Title="密炼机台"
hc:InfoElement.TitleWidth="90"
hc:InfoElement.TitlePlacement="Left"
@@ -258,6 +267,7 @@
<hc:ComboBox ItemsSource="{Binding ShiftOptions}"
SelectedItem="{Binding SelectedShift}"
DisplayMemberPath="Name"
IsEnabled="{Binding IsEditable}"
hc:InfoElement.Title="班次"
hc:InfoElement.TitleWidth="90"
hc:InfoElement.TitlePlacement="Left"
@@ -271,6 +281,7 @@
<hc:ComboBox ItemsSource="{Binding PlanOptions}"
SelectedItem="{Binding SelectedPlan}"
DisplayMemberPath="PlanMaterialNo"
IsEnabled="{Binding IsEditable}"
hc:InfoElement.Title="密炼计划"
hc:InfoElement.TitleWidth="90"
hc:InfoElement.TitlePlacement="Left"
@@ -279,21 +290,34 @@
hc:InfoElement.Symbol="*"
Margin="0,0,0,8"/>
</hc:Col>
<!-- 胶料名称 -->
<!-- 胶料名称(在线:计划带出;离线:手动选择) -->
<hc:Col Span="12">
<Grid Margin="0,0,0,8">
<hc:TextBox Text="{Binding RubberMaterialName, Mode=OneWay}"
IsReadOnly="True"
hc:InfoElement.Title="胶料名称"
hc:InfoElement.TitleWidth="90"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.Placeholder="选择计划后自动带出"
Margin="0,0,0,8"/>
Visibility="{Binding IsOnlineMode, Converter={StaticResource Boolean2VisibilityConverter}}"/>
<hc:ComboBox ItemsSource="{Binding RubberMaterialOptions}"
SelectedItem="{Binding SelectedRubberMaterial}"
IsEnabled="{Binding IsEditable}"
hc:InfoElement.Title="胶料名称"
hc:InfoElement.TitleWidth="90"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.Placeholder="从本地标准选择胶料"
hc:InfoElement.Necessary="True"
hc:InfoElement.Symbol="*"
Visibility="{Binding IsOfflineMode, Converter={StaticResource Boolean2VisibilityConverter}}"/>
</Grid>
</hc:Col>
<!-- 实验标准 -->
<hc:Col Span="12">
<hc:ComboBox ItemsSource="{Binding StdOptions}"
SelectedItem="{Binding SelectedStd}"
DisplayMemberPath="StdName"
IsEnabled="{Binding IsEditable}"
hc:InfoElement.Title="实验标准"
hc:InfoElement.TitleWidth="90"
hc:InfoElement.TitlePlacement="Left"
@@ -315,25 +339,27 @@
<!-- 车次 -->
<hc:Col Span="12">
<hc:TextBox Text="{Binding TrainNo, UpdateSourceTrigger=PropertyChanged}"
IsReadOnly="{Binding IsReadOnly}"
hc:InfoElement.Title="车次"
hc:InfoElement.TitleWidth="90"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.Placeholder="手填车次"
hc:InfoElement.Necessary="True"
hc:InfoElement.Symbol="*"
hc:InfoElement.ShowClearButton="True"
hc:InfoElement.ShowClearButton="{Binding IsEditable}"
Margin="0,0,0,8"/>
</hc:Col>
<!-- 试验次数 -->
<hc:Col Span="12">
<hc:TextBox Text="{Binding InspectTimesText, UpdateSourceTrigger=PropertyChanged}"
IsReadOnly="{Binding IsReadOnly}"
hc:InfoElement.Title="试验次数"
hc:InfoElement.TitleWidth="90"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.Placeholder="默认 1可修改"
hc:InfoElement.Necessary="True"
hc:InfoElement.Symbol="*"
hc:InfoElement.ShowClearButton="True"
hc:InfoElement.ShowClearButton="{Binding IsEditable}"
Margin="0,0,0,8"/>
</hc:Col>
<!-- 检验结果(由试验结果区域自动判定,只读) -->
@@ -358,11 +384,18 @@
</hc:Col>
</hc:Row>
<TextBlock Text="密炼计划、实验标准均来自桌面端本地缓存;按密炼日期筛选计划,再按胶料名称选择实验标准"
<TextBlock Text="{Binding OfflineModeHint}"
FontSize="11"
Foreground="#d46b08"
Margin="0,8,0,0"
TextWrapping="Wrap"
Visibility="{Binding IsOfflineMode, Converter={StaticResource Boolean2VisibilityConverter}}"/>
<TextBlock Text="密炼计划、实验标准每 2 秒自动从 MES 同步到本地缓存;断网时使用本地数据"
FontSize="11"
Foreground="{DynamicResource SecondaryTextBrush}"
Margin="0,8,0,0"
TextWrapping="Wrap"/>
TextWrapping="Wrap"
Visibility="{Binding IsOnlineMode, Converter={StaticResource Boolean2VisibilityConverter}}"/>
</StackPanel>
</Border>
</Grid>
@@ -406,7 +439,7 @@
<!-- 底部操作 -->
<Border Grid.Row="2" Background="{DynamicResource RegionBrush}" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,1,0,0" Margin="-12,8,-12,-12">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="20,0">
<Button Content="保存胶料快检记录" Command="{Binding SaveCommand}" Style="{StaticResource ButtonPrimary}" Height="36" Width="160"
<Button Content="{Binding SaveButtonText}" Command="{Binding SaveCommand}" Style="{StaticResource ButtonPrimary}" Height="36" MinWidth="160" Padding="12,0"
IsEnabled="{Binding CanSave}"
Visibility="{Binding ShowSaveButton, Converter={StaticResource Boolean2VisibilityConverter}}"/>
</StackPanel>

View File

@@ -118,6 +118,8 @@ public partial class RubberQuickTestOperationView : UserControl
Width = 90,
ElementStyle = centerTextStyle
});
InspectResultGrid.IsReadOnly = vm.IsReadOnly;
}
private void InspectResultGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)

View File

@@ -82,12 +82,18 @@
<DataGridTextColumn Header="检验日期" Binding="{Binding InspectDate, StringFormat='yyyy-MM-dd HH:mm'}" Width="130"/>
<DataGridTextColumn Header="是否合格" Binding="{Binding InspectResultDisplay}" Width="80"/>
<DataGridTextColumn Header="同步状态" Binding="{Binding SyncStatusDisplay}" Width="80"/>
<DataGridTemplateColumn Header="操作" Width="150">
<DataGridTemplateColumn Header="操作" Width="270">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Button Content="详情" Command="{Binding DataContext.ViewDetailCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
CommandParameter="{Binding}" Style="{StaticResource ButtonPrimary}" Height="26" Padding="8,0" Margin="0,0,6,0"/>
<Button Content="编辑" Command="{Binding DataContext.EditCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
CommandParameter="{Binding}" Style="{StaticResource ButtonDefault}" Height="26" Padding="8,0" Margin="0,0,6,0"
Visibility="{Binding CanEdit, Converter={StaticResource Boolean2VisibilityConverter}}"/>
<Button Content="同步" Command="{Binding DataContext.SyncCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
CommandParameter="{Binding}" Style="{StaticResource ButtonSuccess}" Height="26" Padding="8,0" Margin="0,0,6,0"
Visibility="{Binding CanSync, Converter={StaticResource Boolean2VisibilityConverter}}"/>
<Button Content="删除" Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
CommandParameter="{Binding}" Style="{StaticResource ButtonDanger}" Height="26" Padding="8,0"
Visibility="{Binding CanDelete, Converter={StaticResource Boolean2VisibilityConverter}}"/>

View File

@@ -66,7 +66,7 @@
<TextBlock Text="重置" Style="{StaticResource IconButtonStyle}"/>
</StackPanel>
</Button>
<TextBlock Text="数据来自 MES桌面端只读;断网时显示本地缓存"
<TextBlock Text="数据来自 MES每 2 秒自动同步(含标准明细);断网时显示本地缓存"
VerticalAlignment="Center"
Foreground="{DynamicResource SecondaryTextBrush}"
FontSize="12"/>