优化桌面端无用菜单和地磅数据可手动功能。

This commit is contained in:
geht
2026-05-18 15:55:11 +08:00
parent 5800b6b61c
commit c11f3104cb
18 changed files with 628 additions and 118 deletions

View File

@@ -19,6 +19,11 @@ namespace YY.Admin.ViewModels.Control
public MenuItem? Parent { get; set; } // 父节点引用
public ObservableCollection<MenuItem> Children { get; set; } = [];
/// <summary>
/// 是否为桌面端配置的默认首页
/// </summary>
public bool IsDefaultDesktopHome { get; set; }
private bool _isExpanded;
@@ -300,6 +305,7 @@ namespace YY.Admin.ViewModels.Control
target.Icon = ConvertHtmlEntityToUnicode(source?.Icon ?? "&#xe810;");
target.ViewName = ResolveViewName(source); // 将菜单路由映射到已注册的WPF视图
target.Parent = parent; // 设置父节点
target.IsDefaultDesktopHome = source?.IsDefaultDesktopHome == true;
// 添加子菜单(如果有)
if (source.Children != null && source.Children.Any())
@@ -389,8 +395,9 @@ namespace YY.Admin.ViewModels.Control
{
try
{
// 默认菜单
var defaultMenuItem = GetFirstLeaf(MenuItems.FirstOrDefault());
// 优先使用菜单管理中勾选的「默认首页」,否则取排序后的第一个叶子菜单
var defaultMenuItem = FindDefaultHomeLeaf(MenuItems)
?? GetFirstLeaf(MenuItems.FirstOrDefault());
if (defaultMenuItem != null)
{
// Tab不允许关闭
@@ -419,6 +426,31 @@ namespace YY.Admin.ViewModels.Control
return GetFirstLeaf(menu.Children.FirstOrDefault());
}
/// <summary>
/// 按先序遍历查找第一个标记为默认首页的叶子节点(与左侧树展示顺序一致)
/// </summary>
private static MenuItem? FindDefaultHomeLeaf(IEnumerable<MenuItem>? nodes)
{
if (nodes == null)
return null;
foreach (var m in nodes)
{
if (m.Children is not { Count: > 0 })
{
if (m.IsDefaultDesktopHome)
return m;
continue;
}
var sub = FindDefaultHomeLeaf(m.Children);
if (sub != null)
return sub;
}
return null;
}
public void ToggleParents(MenuItem? item, bool IsExpanded)
{
if (item == null)

View File

@@ -306,16 +306,7 @@ namespace YY.Admin.ViewModels
ViewName = "MenuTreeView",
Command = new DelegateCommand<NavItem>(it => _ = NavigateToViewAsync(CommonConst.MenuRegion, it.ViewName))
},
new NavItem {
Icon = "GamepadVariantOutline",
Name = "菜单区域",
Command = new DelegateCommand<NavItem>(it => _ = NavigateToViewAsync(CommonConst.MenuRegion, it.ViewName))
},
new NavItem {
Icon = "FoodAppleOutline",
Name = "Tab区域",
Command = new DelegateCommand<NavItem>(OnOpenOrActivateTab)
},
// 已隐藏开发调试用「菜单区域」「Tab区域」日常仅保留「功能菜单」
(_menuTreeToggleNavItem = new NavItem {
Icon = "ChevronDoubleLeft",
Name = "折叠菜单",

View File

@@ -1,5 +1,6 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using HandyControl.Controls;
using Prism.Commands;
using Prism.Mvvm;
@@ -11,21 +12,28 @@ using YY.Admin.Services.Service.Menu;
namespace YY.Admin.ViewModels.SysManage;
/// <summary>
/// 左侧列表一行(树形扁平展示)
/// 左侧列表一行(树形扁平展示,可见行受折叠状态控制
/// </summary>
public sealed class MenuFlatRow
{
public SysMenu Menu { get; }
public int Depth { get; }
public string IndentTitle { get; }
/// <summary>是否存在子节点(用于显示展开按钮)</summary>
public bool HasChildren { get; }
/// <summary>子节点当前是否展开</summary>
public bool IsExpanded { get; }
public Thickness LeadingMargin => new(Depth * 14, 0, 0, 0);
public string TitleText { get; }
public MenuFlatRow(SysMenu menu, int depth)
public MenuFlatRow(SysMenu menu, int depth, bool hasChildren, bool isExpanded)
{
Menu = menu;
Depth = depth;
var pad = new string(' ', depth);
HasChildren = hasChildren;
IsExpanded = isExpanded;
var tag = menu.Type == MenuTypeEnum.Dir ? "[目录] " : menu.Type == MenuTypeEnum.Btn ? "[按钮] " : "[菜单] ";
IndentTitle = pad + tag + menu.Title;
var homeMark = menu.IsDefaultDesktopHome ? "[默认首页] " : "";
TitleText = tag + homeMark + menu.Title;
}
}
@@ -49,13 +57,25 @@ public class MenuEditorModel : BindableBase
private bool _isHide;
private bool _isKeepAlive = true;
private bool _isAffix;
private bool _isDefaultDesktopHome;
private int _orderNo = 100;
private StatusEnum _status = StatusEnum.Enable;
private string? _remark;
public long Id { get => _id; set => SetProperty(ref _id, value); }
public long Pid { get => _pid; set => SetProperty(ref _pid, value); }
public MenuTypeEnum Type { get => _type; set => SetProperty(ref _type, value); }
public MenuTypeEnum Type
{
get => _type;
set
{
if (!SetProperty(ref _type, value))
return;
RaisePropertyChanged(nameof(CanSetDefaultDesktopHome));
if (value != MenuTypeEnum.Menu)
IsDefaultDesktopHome = false;
}
}
public string? Name { get => _name; set => SetProperty(ref _name, value); }
public string? Path { get => _path; set => SetProperty(ref _path, value); }
public string? Component { get => _component; set => SetProperty(ref _component, value); }
@@ -68,12 +88,20 @@ public class MenuEditorModel : BindableBase
public bool IsHide { get => _isHide; set => SetProperty(ref _isHide, value); }
public bool IsKeepAlive { get => _isKeepAlive; set => SetProperty(ref _isKeepAlive, value); }
public bool IsAffix { get => _isAffix; set => SetProperty(ref _isAffix, value); }
public bool IsDefaultDesktopHome
{
get => _isDefaultDesktopHome;
set => SetProperty(ref _isDefaultDesktopHome, value);
}
public int OrderNo { get => _orderNo; set => SetProperty(ref _orderNo, value); }
public StatusEnum Status { get => _status; set => SetProperty(ref _status, value); }
public string? Remark { get => _remark; set => SetProperty(ref _remark, value); }
public bool IsNew => Id == 0;
/// <summary>仅「菜单」类型可设为桌面默认首页</summary>
public bool CanSetDefaultDesktopHome => Type == MenuTypeEnum.Menu;
public void LoadFrom(SysMenu m)
{
Id = m.Id;
@@ -91,6 +119,7 @@ public class MenuEditorModel : BindableBase
IsHide = m.IsHide;
IsKeepAlive = m.IsKeepAlive;
IsAffix = m.IsAffix;
IsDefaultDesktopHome = m.Type == MenuTypeEnum.Menu && m.IsDefaultDesktopHome;
OrderNo = m.OrderNo;
Status = m.Status;
Remark = m.Remark;
@@ -115,6 +144,7 @@ public class MenuEditorModel : BindableBase
IsHide = IsHide,
IsKeepAlive = IsKeepAlive,
IsAffix = IsAffix,
IsDefaultDesktopHome = Type == MenuTypeEnum.Menu && IsDefaultDesktopHome,
OrderNo = OrderNo,
Status = Status,
Remark = Remark
@@ -138,6 +168,7 @@ public class MenuEditorModel : BindableBase
IsHide = false;
IsKeepAlive = true;
IsAffix = false;
IsDefaultDesktopHome = false;
OrderNo = 100;
Status = StatusEnum.Enable;
Remark = null;
@@ -147,6 +178,10 @@ public class MenuEditorModel : BindableBase
public class MenuManagementViewModel : BaseViewModel
{
private readonly ISysMenuService _menuService;
/// <summary>最近一次加载的全量菜单(折叠切换时仅重算列表,不重复读库)</summary>
private List<SysMenu> _allMenusCache = new();
/// <summary>已折叠节点 Id其子级不在扁平列表中展示</summary>
private readonly HashSet<long> _collapsedMenuIds = new();
public ObservableCollection<MenuFlatRow> FlatRows { get; } = new();
public ObservableCollection<KeyValuePair<long, string>> ParentOptions { get; } = new();
@@ -208,6 +243,9 @@ public class MenuManagementViewModel : BaseViewModel
public DelegateCommand AddChildCommand { get; }
public DelegateCommand SaveCommand { get; }
public DelegateCommand DeleteCommand { get; }
public DelegateCommand<MenuFlatRow?> ToggleExpandCommand { get; }
public DelegateCommand ExpandAllCommand { get; }
public DelegateCommand CollapseAllCommand { get; }
public MenuManagementViewModel(
ISysMenuService menuService,
@@ -224,6 +262,9 @@ public class MenuManagementViewModel : BaseViewModel
.ObservesProperty(() => Editor);
DeleteCommand = new DelegateCommand(async () => await DeleteAsync(), () => SelectedRow != null && SelectedRow.Menu.Id != 0)
.ObservesProperty(() => SelectedRow);
ToggleExpandCommand = new DelegateCommand<MenuFlatRow?>(OnToggleExpand);
ExpandAllCommand = new DelegateCommand(OnExpandAll);
CollapseAllCommand = new DelegateCommand(OnCollapseAll);
_ = RefreshAsync();
}
@@ -234,8 +275,11 @@ public class MenuManagementViewModel : BaseViewModel
{
IsLoading = true;
var all = await _menuService.GetAllMenusForManageAsync();
PruneCollapsedState(all);
_allMenusCache = all;
RebuildFlat(all);
RebuildParentOptions(all);
ResyncSelectionAfterRebuild();
}
catch (Exception ex)
{
@@ -250,18 +294,90 @@ public class MenuManagementViewModel : BaseViewModel
private void RebuildFlat(List<SysMenu> all)
{
FlatRows.Clear();
bool HasChild(long id) => all.Any(x => x.Pid == id);
void Walk(long pid, int depth)
{
foreach (var m in all.Where(x => x.Pid == pid).OrderBy(x => x.OrderNo).ThenBy(x => x.Id))
{
FlatRows.Add(new MenuFlatRow(m, depth));
Walk(m.Id, depth + 1);
var hasCh = HasChild(m.Id);
var expanded = !_collapsedMenuIds.Contains(m.Id);
FlatRows.Add(new MenuFlatRow(m, depth, hasCh, expanded));
if (hasCh && expanded)
Walk(m.Id, depth + 1);
}
}
Walk(0, 0);
}
/// <summary>删除已不存在的菜单 Id并去掉已无子级的折叠记录</summary>
private void PruneCollapsedState(List<SysMenu> all)
{
var validIds = new HashSet<long>(all.Select(m => m.Id));
foreach (var id in _collapsedMenuIds.ToList())
{
if (!validIds.Contains(id) || !all.Any(x => x.Pid == id))
_collapsedMenuIds.Remove(id);
}
}
private void OnToggleExpand(MenuFlatRow? row)
{
if (row?.HasChildren != true)
return;
var id = row.Menu.Id;
if (_collapsedMenuIds.Contains(id))
_collapsedMenuIds.Remove(id);
else
_collapsedMenuIds.Add(id);
RebuildFlat(_allMenusCache);
ResyncSelectionAfterRebuild();
}
private void OnExpandAll()
{
_collapsedMenuIds.Clear();
RebuildFlat(_allMenusCache);
ResyncSelectionAfterRebuild();
}
private void OnCollapseAll()
{
_collapsedMenuIds.Clear();
foreach (var m in _allMenusCache.Where(m => _allMenusCache.Any(c => c.Pid == m.Id)))
_collapsedMenuIds.Add(m.Id);
RebuildFlat(_allMenusCache);
ResyncSelectionAfterRebuild();
}
/// <summary>重建列表后,按 Id 恢复选中;若当前项被折叠隐藏则选中其可见祖先</summary>
private void ResyncSelectionAfterRebuild()
{
if (_selectedRow == null)
return;
var id = _selectedRow.Menu.Id;
MenuFlatRow? row = FlatRows.FirstOrDefault(r => r.Menu.Id == id);
var curId = id;
while (row == null && curId != 0)
{
var m = _allMenusCache.FirstOrDefault(x => x.Id == curId);
if (m == null)
break;
curId = m.Pid;
if (curId == 0)
break;
row = FlatRows.FirstOrDefault(r => r.Menu.Id == curId);
}
if (row != null)
SelectedRow = row;
}
private void RebuildParentOptions(List<SysMenu> all)
{
ParentOptions.Clear();

View File

@@ -168,9 +168,12 @@ public class WeightRecordEditDialogViewModel : BaseViewModel, IDialogResultable<
return;
}
// 净重自动计算
if (Record.GrossWeight.HasValue && Record.TareWeight.HasValue)
// 净重自动计算0 视为未称,不计算净重)
if (Record.GrossWeight.HasValue && Record.GrossWeight.Value > 0
&& Record.TareWeight.HasValue && Record.TareWeight.Value > 0)
Record.NetWeight = Math.Round(Record.GrossWeight.Value - Record.TareWeight.Value, 2);
else
Record.NetWeight = null;
// 写入密炼物料
Record.MixerMaterialIds = _mixerMaterialIds;

View File

@@ -98,6 +98,21 @@ public class WeightRecordOperationViewModel : BaseViewModel
private bool _tareWeightCaptured;
public bool TareWeightCaptured { get => _tareWeightCaptured; set => SetProperty(ref _tareWeightCaptured, value); }
/// <summary>
/// 手动输入毛重/皮重(仅用于测试联调,正式过磅请关闭)。
/// </summary>
private bool _isManualWeightEntry;
public bool IsManualWeightEntry
{
get => _isManualWeightEntry;
set
{
if (!SetProperty(ref _isManualWeightEntry, value)) return;
CaptureGrossWeightCommand.RaiseCanExecuteChanged();
CaptureTareWeightCommand.RaiseCanExecuteChanged();
}
}
// ─── 表单绑定属性 ───
private DateTime _weighDate = DateTime.Today;
@@ -234,14 +249,27 @@ public class WeightRecordOperationViewModel : BaseViewModel
public double? GrossWeight
{
get => _grossWeight;
set { SetProperty(ref _grossWeight, value); RecalcNetWeight(); }
set
{
if (!SetProperty(ref _grossWeight, value)) return;
// 测试用手动录入:有有效数值即视为已填写(用于「已采集」提示与保存)
if (IsManualWeightEntry)
GrossWeightCaptured = value.HasValue && value.Value > 0;
RecalcNetWeight();
}
}
private double? _tareWeight;
public double? TareWeight
{
get => _tareWeight;
set { SetProperty(ref _tareWeight, value); RecalcNetWeight(); }
set
{
if (!SetProperty(ref _tareWeight, value)) return;
if (IsManualWeightEntry)
TareWeightCaptured = value.HasValue && value.Value > 0;
RecalcNetWeight();
}
}
private double? _netWeight;
@@ -312,10 +340,12 @@ public class WeightRecordOperationViewModel : BaseViewModel
CaptureGrossWeightCommand = new DelegateCommand(CaptureGrossWeight, CanCaptureGrossWeight)
.ObservesProperty(() => IsWeightStable)
.ObservesProperty(() => IsManualWeightEntry)
.ObservesProperty(() => GrossWeightCaptured)
.ObservesProperty(() => TareWeightCaptured);
CaptureTareWeightCommand = new DelegateCommand(CaptureTareWeight, CanCaptureTareWeight)
.ObservesProperty(() => IsWeightStable)
.ObservesProperty(() => IsManualWeightEntry)
.ObservesProperty(() => GrossWeightCaptured)
.ObservesProperty(() => TareWeightCaptured);
SaveCommand = new DelegateCommand(async () => await SaveAsync());
@@ -386,7 +416,8 @@ public class WeightRecordOperationViewModel : BaseViewModel
{
_vehiclePresent = true;
_stableCountdown = 0;
_baseWeight = _rnd.Next(18000, 65000); // 18~65吨随机
// 同一张单第二次上磅:模拟重量与已采值协调(先毛后皮 → 皮重小于毛重;先皮后毛 → 毛重大于皮重)
PickBaseWeightForCurrentWeighingScenario();
AddLog("检测到车辆入场");
// 模拟摄像头识别车牌2秒后回传
@@ -411,6 +442,40 @@ public class WeightRecordOperationViewModel : BaseViewModel
leaveTimer.Start();
}
/// <summary>
/// 按当前单据已填毛重/皮重生成模拟磅台基准重量,保证净重合理。
/// </summary>
private void PickBaseWeightForCurrentWeighingScenario()
{
if (HasEffectiveWeighValue(GrossWeight) && !HasEffectiveWeighValue(TareWeight))
{
var g = GrossWeight!.Value;
// 皮重需明显小于毛重,留出净重空间
var maxTare = Math.Max(0, g - 30);
var minTare = Math.Min(maxTare * 0.22, maxTare - 80);
minTare = Math.Max(0, minTare);
if (maxTare - minTare < 25)
minTare = Math.Max(0, maxTare * 0.45);
if (maxTare - minTare < 1)
maxTare = Math.Max(minTare + 1, g - 5);
_baseWeight = minTare + _rnd.NextDouble() * Math.Max(1e-6, maxTare - minTare);
}
else if (HasEffectiveWeighValue(TareWeight) && !HasEffectiveWeighValue(GrossWeight))
{
var t = TareWeight!.Value;
var lower = t + Math.Max(100, t * 0.05);
var span = 8000 + _rnd.NextDouble() * 12000;
var upper = Math.Min(65000, lower + span);
if (upper - lower < 500)
upper = lower + 500;
_baseWeight = lower + _rnd.NextDouble() * (upper - lower);
}
else
{
_baseWeight = _rnd.Next(18000, 65000); // 18~65 吨随机(首次过磅或两称已齐)
}
}
private void SimulateVehicleLeave()
{
_vehiclePresent = false;
@@ -446,19 +511,25 @@ public class WeightRecordOperationViewModel : BaseViewModel
private bool CanCaptureGrossWeight()
{
return IsWeightStable && !GrossWeightCaptured;
if (GrossWeightCaptured) return false;
// 手动测试模式:无需等稳定即可点采集
if (IsManualWeightEntry) return true;
return IsWeightStable;
}
private bool CanCaptureTareWeight()
{
return IsWeightStable && !TareWeightCaptured;
if (TareWeightCaptured) return false;
if (IsManualWeightEntry) return true;
return IsWeightStable;
}
private void RecalcNetWeight()
{
if (GrossWeight.HasValue && TareWeight.HasValue)
// 与后端一致0 或占位视为“未称”,避免仅毛重时 net=tare(0) 即毛重、单据误判完成
if (HasEffectiveWeighValue(GrossWeight) && HasEffectiveWeighValue(TareWeight))
{
NetWeight = Math.Round(GrossWeight.Value - TareWeight.Value, 2);
NetWeight = Math.Round(GrossWeight!.Value - TareWeight!.Value, 2);
RaisePropertyChanged(nameof(NetWeightDisplay));
}
else
@@ -468,6 +539,8 @@ public class WeightRecordOperationViewModel : BaseViewModel
}
}
private static bool HasEffectiveWeighValue(double? kg) => kg.HasValue && kg.Value > 0;
private async Task SaveAsync()
{
if (string.IsNullOrWhiteSpace(PlateNumber))
@@ -481,12 +554,12 @@ public class WeightRecordOperationViewModel : BaseViewModel
var isOutbound = string.Equals(InoutDirection, "2", StringComparison.Ordinal);
if (!isCompleteSelectedRecord)
{
if (isOutbound && !TareWeight.HasValue)
if (isOutbound && !HasEffectiveWeighValue(TareWeight))
{
HandyControl.Controls.MessageBox.Warning("出厂流程请先采集皮重!");
return;
}
if (!isOutbound && !GrossWeight.HasValue)
if (!isOutbound && !HasEffectiveWeighValue(GrossWeight))
{
HandyControl.Controls.MessageBox.Warning("进厂流程请先采集毛重!");
return;
@@ -494,7 +567,7 @@ public class WeightRecordOperationViewModel : BaseViewModel
}
else
{
var needSecondWeightCaptured = isOutbound ? GrossWeight.HasValue : TareWeight.HasValue;
var needSecondWeightCaptured = isOutbound ? HasEffectiveWeighValue(GrossWeight) : HasEffectiveWeighValue(TareWeight);
if (!needSecondWeightCaptured)
{
HandyControl.Controls.MessageBox.Warning(isOutbound
@@ -558,6 +631,7 @@ public class WeightRecordOperationViewModel : BaseViewModel
ClearCustomerSelection();
ClearMixerMaterialSelection();
ClearVehicleMatch();
IsManualWeightEntry = false;
// 保存后立即按当前方向回填缓存,避免必须重进页面才显示
TryApplyCachedUnitByDirection(InoutDirection);
RaisePropertyChanged(nameof(NetWeightDisplay));
@@ -592,8 +666,9 @@ public class WeightRecordOperationViewModel : BaseViewModel
IsPlateNumberLocked = false;
ClearSupplierSelection();
ClearCustomerSelection();
ClearMixerMaterialSelection();
ClearMixerMaterialSelection();
ClearVehicleMatch();
IsManualWeightEntry = false;
RaisePropertyChanged(nameof(NetWeightDisplay));
AddLog("表单已清空");
}
@@ -845,15 +920,16 @@ public class WeightRecordOperationViewModel : BaseViewModel
// 按进出方向查询当天待补称单据:进厂=已称毛重,出厂=已称皮重。
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)))
var isOutbound = string.Equals(InoutDirection, "2", StringComparison.Ordinal);
bool effT(double? w) => w.HasValue && w.Value > 0;
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
? (effT(r.TareWeight) && !effT(r.GrossWeight))
: (effT(r.GrossWeight) && !effT(r.TareWeight))))
.OrderByDescending(r => r.CreateTime ?? DateTime.MinValue)
.ToList();
@@ -865,8 +941,8 @@ public class WeightRecordOperationViewModel : BaseViewModel
BillNo = record.BillNo ?? "-",
PlateNumber = record.PlateNumber ?? "-",
FirstWeightDisplay = isOutbound
? (record.TareWeight.HasValue ? $"{record.TareWeight.Value:N2}" : "-")
: (record.GrossWeight.HasValue ? $"{record.GrossWeight.Value:N2}" : "-")
? (HasEffectiveWeighValue(record.TareWeight) ? $"{record.TareWeight!.Value:N2}" : "-")
: (HasEffectiveWeighValue(record.GrossWeight) ? $"{record.GrossWeight!.Value:N2}" : "-")
});
}
@@ -914,14 +990,14 @@ public class WeightRecordOperationViewModel : BaseViewModel
if (isOutbound)
{
TareWeight = selected.Source.TareWeight;
TareWeightCaptured = TareWeight.HasValue;
TareWeightCaptured = HasEffectiveWeighValue(TareWeight);
GrossWeight = null;
GrossWeightCaptured = false;
}
else
{
GrossWeight = selected.Source.GrossWeight;
GrossWeightCaptured = GrossWeight.HasValue;
GrossWeightCaptured = HasEffectiveWeighValue(GrossWeight);
TareWeight = null;
TareWeightCaptured = false;
}
@@ -953,6 +1029,7 @@ public class WeightRecordOperationViewModel : BaseViewModel
ClearCustomerSelection();
ClearMixerMaterialSelection();
ClearVehicleMatch();
IsManualWeightEntry = false;
RaisePropertyChanged(nameof(NetWeightDisplay));
AddLog("已切换进出方向,当前表单数据已清空");
}