Files
qhmes/yy-admin-master/YY.Admin/ViewModels/SysManage/MenuManagementViewModel.cs

507 lines
16 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>
/// 左侧列表一行(树形扁平展示,可见行受折叠状态控制)
/// </summary>
public sealed class MenuFlatRow
{
public SysMenu Menu { get; }
public int Depth { 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, 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;
}
}
/// <summary>
/// 可编辑字段(与界面绑定)
/// </summary>
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;
/// <summary>仅「菜单」类型可设为桌面默认首页</summary>
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 = "&#xe8f1;";
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;
/// <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();
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<KeyValuePair<string, int>> StatusList =>
Enum.GetValues(typeof(StatusEnum))
.Cast<StatusEnum>()
.Select(e => new KeyValuePair<string, int>(e.GetDescription(), (int)e))
.ToList();
public List<KeyValuePair<string, int>> MenuTypeList =>
Enum.GetValues(typeof(MenuTypeEnum))
.Cast<MenuTypeEnum>()
.Select(e => new KeyValuePair<string, int>(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<MenuFlatRow?> 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<MenuFlatRow?>(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<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))
{
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();
ParentOptions.Add(new KeyValuePair<long, string>(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<long, string>(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<MenuStructureChangedEvent>().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<MenuStructureChangedEvent>().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<MenuStructureChangedEvent>().Publish(true);
Editor = null;
SelectedRow = null;
await RefreshAsync();
}
else
{
Growl.Warning(msg);
}
}
catch (Exception ex)
{
Growl.Error($"删除失败:{ex.Message}");
}
finally
{
IsLoading = false;
}
}
}