using System.Collections.ObjectModel; using System.Linq; using System.Windows; using HandyControl.Controls; using Prism.Commands; using Prism.Mvvm; using YY.Admin.Core; using YY.Admin.Core.Extension; using YY.Admin.Event; using YY.Admin.Services.Service.Menu; namespace YY.Admin.ViewModels.SysManage; /// /// 左侧列表一行(树形扁平展示,可见行受折叠状态控制) /// public sealed class MenuFlatRow { public SysMenu Menu { get; } public int Depth { get; } /// 是否存在子节点(用于显示展开按钮) public bool HasChildren { get; } /// 子节点当前是否展开 public bool IsExpanded { get; } public Thickness LeadingMargin => new(Depth * 14, 0, 0, 0); public string TitleText { get; } public MenuFlatRow(SysMenu menu, int depth, bool hasChildren, bool isExpanded) { Menu = menu; Depth = depth; HasChildren = hasChildren; IsExpanded = isExpanded; var tag = menu.Type == MenuTypeEnum.Dir ? "[目录] " : menu.Type == MenuTypeEnum.Btn ? "[按钮] " : "[菜单] "; var homeMark = menu.IsDefaultDesktopHome ? "[默认首页] " : ""; TitleText = tag + homeMark + menu.Title; } } /// /// 可编辑字段(与界面绑定) /// public class MenuEditorModel : BindableBase { private long _id; private long _pid; private MenuTypeEnum _type = MenuTypeEnum.Menu; private string? _name; private string? _path; private string? _component; private string? _redirect; private string? _permission; private string _title = string.Empty; private string? _icon; private bool _isIframe; private string? _outLink; 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 { 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); } public string? Redirect { get => _redirect; set => SetProperty(ref _redirect, value); } public string? Permission { get => _permission; set => SetProperty(ref _permission, value); } public string Title { get => _title; set => SetProperty(ref _title, value); } public string? Icon { get => _icon; set => SetProperty(ref _icon, value); } public bool IsIframe { get => _isIframe; set => SetProperty(ref _isIframe, value); } public string? OutLink { get => _outLink; set => SetProperty(ref _outLink, value); } 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; /// 仅「菜单」类型可设为桌面默认首页 public bool CanSetDefaultDesktopHome => Type == MenuTypeEnum.Menu; public void LoadFrom(SysMenu m) { Id = m.Id; Pid = m.Pid; Type = m.Type; Name = m.Name; Path = m.Path; Component = m.Component; Redirect = m.Redirect; Permission = m.Permission; Title = m.Title; Icon = m.Icon; IsIframe = m.IsIframe; OutLink = m.OutLink; 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; } public SysMenu ToSysMenu() { return new SysMenu { Id = Id, Pid = Pid, Type = Type, Name = Name, Path = Path, Component = Component, Redirect = Redirect, Permission = Permission, Title = Title.Trim(), Icon = Icon, IsIframe = IsIframe, OutLink = OutLink, IsHide = IsHide, IsKeepAlive = IsKeepAlive, IsAffix = IsAffix, IsDefaultDesktopHome = Type == MenuTypeEnum.Menu && IsDefaultDesktopHome, OrderNo = OrderNo, Status = Status, Remark = Remark }; } public void ResetForNew(long pid, MenuTypeEnum type) { Id = 0; Pid = pid; Type = type; Name = null; Path = null; Component = null; Redirect = null; Permission = null; Title = type == MenuTypeEnum.Dir ? "新目录" : type == MenuTypeEnum.Menu ? "新菜单" : "新按钮"; Icon = ""; IsIframe = false; OutLink = null; IsHide = false; IsKeepAlive = true; IsAffix = false; IsDefaultDesktopHome = false; OrderNo = 100; Status = StatusEnum.Enable; Remark = null; } } public class MenuManagementViewModel : BaseViewModel { private readonly ISysMenuService _menuService; /// 最近一次加载的全量菜单(折叠切换时仅重算列表,不重复读库) private List _allMenusCache = new(); /// 已折叠节点 Id(其子级不在扁平列表中展示) private readonly HashSet _collapsedMenuIds = new(); public ObservableCollection FlatRows { get; } = new(); public ObservableCollection> ParentOptions { get; } = new(); private MenuFlatRow? _selectedRow; public MenuFlatRow? SelectedRow { get => _selectedRow; set { if (SetProperty(ref _selectedRow, value)) { if (value == null) { Editor = null; RaisePropertyChanged(nameof(Editor)); RaisePropertyChanged(nameof(HasEditor)); } else { Editor ??= new MenuEditorModel(); Editor.LoadFrom(value.Menu); RaisePropertyChanged(nameof(Editor)); RaisePropertyChanged(nameof(HasEditor)); } } } } private MenuEditorModel? _editor; public MenuEditorModel? Editor { get => _editor; set { if (SetProperty(ref _editor, value)) { RaisePropertyChanged(nameof(HasEditor)); } } } public bool HasEditor => Editor != null; public List> StatusList => Enum.GetValues(typeof(StatusEnum)) .Cast() .Select(e => new KeyValuePair(e.GetDescription(), (int)e)) .ToList(); public List> MenuTypeList => Enum.GetValues(typeof(MenuTypeEnum)) .Cast() .Select(e => new KeyValuePair(e.GetDescription(), (int)e)) .ToList(); public DelegateCommand RefreshCommand { get; } public DelegateCommand AddRootCommand { get; } public DelegateCommand AddChildCommand { get; } public DelegateCommand SaveCommand { get; } public DelegateCommand DeleteCommand { get; } public DelegateCommand ToggleExpandCommand { get; } public DelegateCommand ExpandAllCommand { get; } public DelegateCommand CollapseAllCommand { get; } public MenuManagementViewModel( ISysMenuService menuService, IContainerExtension container, IRegionManager regionManager) : base(container, regionManager) { _menuService = menuService; RefreshCommand = new DelegateCommand(async () => await RefreshAsync()); AddRootCommand = new DelegateCommand(() => BeginNew(0, MenuTypeEnum.Dir)); AddChildCommand = new DelegateCommand(BeginNewChild, () => SelectedRow != null) .ObservesProperty(() => SelectedRow); SaveCommand = new DelegateCommand(async () => await SaveAsync(), () => Editor != null) .ObservesProperty(() => Editor); DeleteCommand = new DelegateCommand(async () => await DeleteAsync(), () => SelectedRow != null && SelectedRow.Menu.Id != 0) .ObservesProperty(() => SelectedRow); ToggleExpandCommand = new DelegateCommand(OnToggleExpand); ExpandAllCommand = new DelegateCommand(OnExpandAll); CollapseAllCommand = new DelegateCommand(OnCollapseAll); _ = RefreshAsync(); } private async Task RefreshAsync() { try { IsLoading = true; var all = await _menuService.GetAllMenusForManageAsync(); PruneCollapsedState(all); _allMenusCache = all; RebuildFlat(all); RebuildParentOptions(all); ResyncSelectionAfterRebuild(); } catch (Exception ex) { Growl.Error($"加载菜单失败:{ex.Message}"); } finally { IsLoading = false; } } private void RebuildFlat(List 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)) { 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); } /// 删除已不存在的菜单 Id,并去掉已无子级的折叠记录 private void PruneCollapsedState(List all) { var validIds = new HashSet(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(); } /// 重建列表后,按 Id 恢复选中;若当前项被折叠隐藏则选中其可见祖先 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 all) { ParentOptions.Clear(); ParentOptions.Add(new KeyValuePair(0, "(根目录)")); void Walk(long pid, int depth) { foreach (var m in all.Where(x => x.Pid == pid).OrderBy(x => x.OrderNo).ThenBy(x => x.Id)) { var indent = new string(' ', depth); ParentOptions.Add(new KeyValuePair(m.Id, indent + m.Title)); Walk(m.Id, depth + 1); } } Walk(0, 0); } private void BeginNew(long pid, MenuTypeEnum type) { // 先取消列表选中,避免 setter 把新建的 Editor 清空 _selectedRow = null; RaisePropertyChanged(nameof(SelectedRow)); Editor = new MenuEditorModel(); Editor.ResetForNew(pid, type); RaisePropertyChanged(nameof(Editor)); RaisePropertyChanged(nameof(HasEditor)); } private void BeginNewChild() { if (SelectedRow == null) return; BeginNew(SelectedRow.Menu.Id, MenuTypeEnum.Menu); } private async Task SaveAsync() { if (Editor == null) return; try { IsLoading = true; var entity = Editor.ToSysMenu(); if (Editor.IsNew) { var (ok, msg, id) = await _menuService.CreateMenuAsync(entity); if (ok) { Growl.Success(msg); _eventAggregator.GetEvent().Publish(true); await RefreshAsync(); SelectedRow = FlatRows.FirstOrDefault(r => r.Menu.Id == id); } else { Growl.Warning(msg); } } else { var (ok, msg) = await _menuService.UpdateMenuAsync(entity); if (ok) { Growl.Success(msg); _eventAggregator.GetEvent().Publish(true); var keepId = entity.Id; await RefreshAsync(); SelectedRow = FlatRows.FirstOrDefault(r => r.Menu.Id == keepId); } else { Growl.Warning(msg); } } } catch (Exception ex) { Growl.Error($"保存失败:{ex.Message}"); } finally { IsLoading = false; } } private async Task DeleteAsync() { if (SelectedRow == null) return; var r = System.Windows.MessageBox.Show( $"确定删除「{SelectedRow.Menu.Title}」?若存在子菜单将无法删除。", "确认删除", System.Windows.MessageBoxButton.OKCancel, System.Windows.MessageBoxImage.Question); if (r != System.Windows.MessageBoxResult.OK) return; try { IsLoading = true; var (ok, msg) = await _menuService.DeleteMenuAsync(SelectedRow.Menu.Id); if (ok) { Growl.Success(msg); _eventAggregator.GetEvent().Publish(true); Editor = null; SelectedRow = null; await RefreshAsync(); } else { Growl.Warning(msg); } } catch (Exception ex) { Growl.Error($"删除失败:{ex.Message}"); } finally { IsLoading = false; } } }