增强任务选择窗口功能,支持按组引用和逐个添加任务,新增任务数量计算逻辑

This commit is contained in:
辉鸭蛋
2025-09-07 23:50:20 +08:00
parent 4a1842ea6b
commit df062088fc
3 changed files with 441 additions and 82 deletions

View File

@@ -20,6 +20,8 @@
<ui:FluentWindow.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<local:BooleanToVisibilityParameterConverter x:Key="BooleanToVisibilityParameterConverter" />
<local:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
<Style x:Key="IconImageStyle" TargetType="Image">
<Setter Property="Width" Value="16" />
<Setter Property="Height" Value="16" />
@@ -47,7 +49,6 @@
</Grid.RowDefinitions>
<!-- 主内容区域 -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
@@ -68,47 +69,48 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- 搜索框 -->
<ui:TextBox Grid.Row="0"
Text="{Binding SearchKeyword, UpdateSourceTrigger=PropertyChanged}"
PlaceholderText="搜索地图追踪任务..."
Icon="{ui:SymbolIcon Search24}"
Margin="0,0,0,8" />
<!-- TreeView -->
<TreeView Grid.Row="1"
ItemsSource="{Binding FilteredPathingTasks}"
Background="Transparent"
BorderThickness="0"
x:Name="TaskTreeView">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 图标 -->
<Grid Grid.Column="0" Width="20" Height="20" Margin="0,0,8,0"
VerticalAlignment="Center">
<!-- WPF图标 -->
<ui:SymbolIcon Symbol="{Binding IsDirectory, Converter={x:Static local:PathingTaskSelectionWindow.DirectoryToSymbolConverter}}"
FontSize="16"
VerticalAlignment="Center"
Visibility="{Binding UseSystemIcon, Converter={StaticResource BooleanToVisibilityConverter}}" />
<!-- 图标 -->
<Grid Grid.Column="0" Width="20" Height="20" Margin="0,0,8,0"
VerticalAlignment="Center">
<!-- WPF图标 -->
<ui:SymbolIcon
Symbol="{Binding IsDirectory, Converter={x:Static local:PathingTaskSelectionWindow.DirectoryToSymbolConverter}}"
FontSize="16"
VerticalAlignment="Center"
Visibility="{Binding UseSystemIcon, Converter={StaticResource BooleanToVisibilityConverter}}" />
</Grid>
<!-- 任务信息 -->
<StackPanel Grid.Column="1">
<ui:TextBlock Text="{Binding Name}"
FontWeight="Medium"
TextTrimming="CharacterEllipsis" />
</StackPanel>
</Grid>
<!-- 任务信息 -->
<StackPanel Grid.Column="1">
<ui:TextBlock Text="{Binding Name}"
FontWeight="Medium"
TextTrimming="CharacterEllipsis" />
</StackPanel>
</Grid>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
</ui:Border>
@@ -174,20 +176,69 @@
</ui:Border>
</Grid>
<!-- 底部按钮 -->
<StackPanel Grid.Row="2"
Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0,12,0,0">
<ui:Button Content="确定"
Appearance="Primary"
Click="OnConfirmClick"
Margin="0,0,8,0"
MinWidth="80" />
<ui:Button Content="取消"
Click="OnCancelClick"
MinWidth="80" />
</StackPanel>
<!-- 底部区域 -->
<Grid Grid.Row="2" Margin="0,12,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- 左侧:任务导入方式开关 -->
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
<ui:TextBlock Text="任务导入方式:" VerticalAlignment="Center" Margin="0,0,8,0" />
<ui:ToggleSwitch IsChecked="{Binding IsGroupImportMode}"
OnContent="按组引用"
OffContent="逐个添加"
VerticalAlignment="Center" />
</StackPanel>
<!-- 右侧:动态按钮 -->
<StackPanel Grid.Column="1" Orientation="Horizontal">
<!-- 按组引用模式的按钮 -->
<ui:Button Content="添加选中目录"
Appearance="Primary"
Command="{Binding AddFolderTaskCommand}"
Margin="0,0,8,0"
MinWidth="120"
Visibility="{Binding IsGroupImportMode, Converter={StaticResource BooleanToVisibilityParameterConverter}}" />
<!-- 添加文件夹引用,保持目录结构 -->
<ui:Button Content="保持目录结构添加"
Command="{Binding AddFolderTasksWithStructureCommand}"
Margin="0,0,8,0"
MinWidth="180"
Appearance="Primary"
Visibility="{Binding IsGroupImportMode, Converter={StaticResource BooleanToVisibilityParameterConverter}}" />
<!-- 添加目录下所有文件,不要目录结构 -->
<ui:Button Content="添加目录下所有文件任务"
Appearance="Primary"
Command="{Binding AddAllFileTasksCommand}"
Margin="0,0,8,0"
MinWidth="160"
Visibility="{Binding IsGroupImportMode, Converter={StaticResource BooleanToVisibilityParameterConverter}, ConverterParameter=Invert}" />
<!-- 保持目录结构添加所有目录下文件 -->
<ui:Button Command="{Binding AddFileTasksWithStructureCommand}"
Margin="0,0,8,0"
MinWidth="180"
Appearance="Primary"
Visibility="{Binding IsGroupImportMode, Converter={StaticResource BooleanToVisibilityParameterConverter}, ConverterParameter=Invert}">
<ui:Button.Content>
<ui:TextBlock>
<Run Text="保持目录结构添加所有文件任务" />
<Run Text="{Binding SelectedDirectoryTaskCount}" />
<Run Text="个任务" />
</ui:TextBlock>
</ui:Button.Content>
</ui:Button>
<ui:Button Content="取消"
Click="OnCancelClick"
MinWidth="80" />
</StackPanel>
</Grid>
</Grid>
</Grid>
</ui:FluentWindow>

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
@@ -8,6 +9,7 @@ using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using BetterGenshinImpact.ViewModel.Windows.GearTask;
using BetterGenshinImpact.ViewModel.Pages.Component;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Wpf.Ui.Controls;
@@ -27,15 +29,20 @@ public partial class PathingTaskSelectionWindow : FluentWindow
public PathingTaskSelectionViewModel ViewModel { get; }
/// <summary>
/// 选中的地图追踪任务
/// 选中的任务
/// </summary>
public PathingTaskInfo? SelectedTask => ViewModel.SelectedTask;
public PathingTaskInfo? SelectedTask { get; private set; }
/// <summary>
/// 对话框结果
/// </summary>
public bool DialogResult { get; private set; }
/// <summary>
/// 添加的任务列表
/// </summary>
public List<GearTaskViewModel> AddedTasks { get; private set; } = new();
/// <summary>
/// 目录到符号图标的转换器
/// </summary>
@@ -49,6 +56,20 @@ public partial class PathingTaskSelectionWindow : FluentWindow
// 绑定TreeView的选中项变化事件
TaskTreeView.SelectedItemChanged += OnTreeViewSelectedItemChanged;
// 订阅任务添加事件
ViewModel.OnTaskAdded += OnTaskAdded;
}
/// <summary>
/// 任务添加事件处理
/// </summary>
private void OnTaskAdded(List<GearTaskViewModel> tasks)
{
AddedTasks.AddRange(tasks);
// 添加任务后自动关闭窗口
CloseWithResult();
}
/// <summary>
@@ -63,16 +84,11 @@ public partial class PathingTaskSelectionWindow : FluentWindow
}
/// <summary>
/// 确定按钮点击事件
/// 任务添加完成后关闭窗口
/// </summary>
private void OnConfirmClick(object sender, RoutedEventArgs e)
public void CloseWithResult()
{
if (ViewModel.SelectedTask == null)
{
Toast.Warning("请选择一个地图追踪任务");
return;
}
SelectedTask = ViewModel.SelectedTask;
DialogResult = true;
Close();
}
@@ -104,6 +120,29 @@ public class DirectoryToSymbolConverter : IValueConverter
}
}
/// <summary>
/// 布尔值到可见性的转换器(支持参数反转)
/// </summary>
public class BooleanToVisibilityParameterConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
bool invert = parameter?.ToString() == "Invert";
if (invert)
boolValue = !boolValue;
return boolValue ? Visibility.Visible : Visibility.Collapsed;
}
return Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
/// <summary>
/// 布尔值到可见性的反向转换器
/// </summary>
@@ -124,6 +163,28 @@ public class BooleanToVisibilityInvertConverter : IValueConverter
}
}
/// <summary>
/// 空值到可见性的转换器
/// </summary>
public class NullToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
bool invert = parameter?.ToString() == "Invert";
bool isNull = value == null;
if (invert)
isNull = !isNull;
return isNull ? Visibility.Collapsed : Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
/// <summary>
/// 字符串到ImageSource的转换器处理空字符串情况支持WebP格式
/// </summary>

View File

@@ -1,18 +1,14 @@
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Data;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using BetterGenshinImpact.Core.Script;
using BetterGenshinImpact.Helpers;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using BetterGenshinImpact.Core.Config;
using System.Linq;
using BetterGenshinImpact.ViewModel.Pages.Component;
namespace BetterGenshinImpact.ViewModel.Windows.GearTask;
@@ -26,45 +22,48 @@ public partial class PathingTaskSelectionViewModel : ViewModel
/// <summary>
/// 地图追踪任务列表
/// </summary>
[ObservableProperty]
private ObservableCollection<PathingTaskInfo> _pathingTasks = new();
[ObservableProperty] private ObservableCollection<PathingTaskInfo> _pathingTasks = new();
/// <summary>
/// 过滤后的地图追踪任务列表
/// </summary>
[ObservableProperty]
private ObservableCollection<PathingTaskInfo> _filteredPathingTasks = new();
[ObservableProperty] private ObservableCollection<PathingTaskInfo> _filteredPathingTasks = new();
/// <summary>
/// 当前选中的地图追踪任务
/// </summary>
[ObservableProperty]
private PathingTaskInfo? _selectedTask;
[ObservableProperty] private PathingTaskInfo? _selectedTask;
/// <summary>
/// 搜索关键词
/// </summary>
[ObservableProperty]
private string _searchKeyword = string.Empty;
[ObservableProperty] private string _searchKeyword = string.Empty;
/// <summary>
/// 右侧显示的内容
/// </summary>
[ObservableProperty]
private string _displayContent = string.Empty;
[ObservableProperty] private string _displayContent = string.Empty;
/// <summary>
/// 右侧显示的内容类型JSON或README
/// </summary>
[ObservableProperty]
private string _displayContentType = string.Empty;
[ObservableProperty] private string _displayContentType = string.Empty;
/// <summary>
/// 图标字典
/// 任务导入方式true=按组引用false=逐个添加
/// </summary>
private Dictionary<string, string> _iconDictionary = new();
[ObservableProperty] private bool _isGroupImportMode = true;
/// <summary>
/// 选中目录下的任务数量
/// </summary>
[ObservableProperty] private int _selectedDirectoryTaskCount = 0;
// /// <summary>
// /// 图标字典
// /// </summary>
// private Dictionary<string, string> _iconDictionary = new();
public PathingTaskSelectionViewModel()
{
@@ -72,6 +71,8 @@ public partial class PathingTaskSelectionViewModel : ViewModel
LoadPathingTasks();
}
public Action<List<GearTaskViewModel>>? OnTaskAdded { get; set; }
// /// <summary>
// /// 加载图标字典
// /// </summary>
@@ -112,7 +113,7 @@ public partial class PathingTaskSelectionViewModel : ViewModel
try
{
PathingTasks.Clear();
var pathingPath = Path.Combine(ScriptRepoUpdater.CenterRepoPath, "repo", "pathing");
if (!Directory.Exists(pathingPath))
{
@@ -144,13 +145,13 @@ public partial class PathingTaskSelectionViewModel : ViewModel
{
IsDirectory = true
};
// 设置图标
SetTaskIcon(taskInfo);
// 递归加载子目录到当前任务的Children集合中
LoadDirectChildrenFromDirectory(dir, rootPath, taskInfo.Children);
parentCollection.Add(taskInfo);
}
@@ -161,10 +162,10 @@ public partial class PathingTaskSelectionViewModel : ViewModel
{
IsDirectory = false
};
// 设置图标
SetTaskIcon(taskInfo);
parentCollection.Add(taskInfo);
}
}
@@ -244,7 +245,7 @@ public partial class PathingTaskSelectionViewModel : ViewModel
private void FilterTasks()
{
FilteredPathingTasks.Clear();
foreach (var task in PathingTasks)
{
var filteredTask = FilterTaskRecursively(task);
@@ -262,8 +263,8 @@ public partial class PathingTaskSelectionViewModel : ViewModel
{
// 检查当前节点是否匹配搜索条件
bool currentMatches = string.IsNullOrWhiteSpace(SearchKeyword) ||
task.Name.Contains(SearchKeyword, StringComparison.OrdinalIgnoreCase) ||
task.RelativePath.Contains(SearchKeyword, StringComparison.OrdinalIgnoreCase);
task.Name.Contains(SearchKeyword, StringComparison.OrdinalIgnoreCase) ||
task.RelativePath.Contains(SearchKeyword, StringComparison.OrdinalIgnoreCase);
// 始终显示文件和目录(默认展示到文件级别)
bool modeMatches = true;
@@ -313,6 +314,7 @@ public partial class PathingTaskSelectionViewModel : ViewModel
{
DisplayContent = string.Empty;
DisplayContentType = string.Empty;
SelectedDirectoryTaskCount = 0;
return;
}
@@ -322,6 +324,8 @@ public partial class PathingTaskSelectionViewModel : ViewModel
LoadReadmeContentForDisplay(value);
DisplayContent = value.ReadmeContent ?? string.Empty;
DisplayContentType = "README";
// 计算选中目录下的任务数量
SelectedDirectoryTaskCount = CountTasksInDirectory(value);
}
else
{
@@ -329,6 +333,7 @@ public partial class PathingTaskSelectionViewModel : ViewModel
LoadJsonContentForDisplay(value);
DisplayContent = value.JsonContent ?? string.Empty;
DisplayContentType = "JSON";
SelectedDirectoryTaskCount = 0;
}
}
@@ -340,5 +345,247 @@ public partial class PathingTaskSelectionViewModel : ViewModel
FilterTasks();
}
/// <summary>
/// 计算目录下的任务数量递归计算所有子目录中的JSON文件
/// </summary>
private int CountTasksInDirectory(PathingTaskInfo directory)
{
if (!directory.IsDirectory)
return 0;
int count = 0;
// 计算当前目录下的JSON文件数量
try
{
count += Directory.GetFiles(directory.FullPath, "*.json").Length;
// 递归计算子目录
foreach (var subDir in Directory.GetDirectories(directory.FullPath))
{
count += CountTasksInDirectoryPath(subDir);
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"计算目录任务数量失败: {directory.FullPath}");
}
return count;
}
/// <summary>
/// 计算指定路径目录下的任务数量(递归)
/// </summary>
private int CountTasksInDirectoryPath(string directoryPath)
{
int count = 0;
try
{
count += Directory.GetFiles(directoryPath, "*.json").Length;
foreach (var subDir in Directory.GetDirectories(directoryPath))
{
count += CountTasksInDirectoryPath(subDir);
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"计算目录任务数量失败: {directoryPath}");
}
return count;
}
/// <summary>
/// 添加文件夹引用
/// </summary>
[RelayCommand]
private void AddFolderTask()
{
if (SelectedTask?.IsDirectory == true)
{
// 按组引用:添加选中目录作为一个任务组
var gearTaskViewModel = new GearTaskViewModel
{
Name = SelectedTask.Name,
Path = @$"{{pathingRepoFolder}}\{SelectedTask.RelativePath}\",
IsDirectory = true
};
// 触发添加事件或通过其他方式返回给调用方
OnTaskAdded?.Invoke([gearTaskViewModel]);
}
}
/// <summary>
/// 添加文件夹引用,保持目录结构
/// </summary>
[RelayCommand]
private void AddFolderTasksWithStructure()
{
if (SelectedTask?.IsDirectory == true)
{
var tasks = GetAllFolderTasksInDirectoryWithStructure(SelectedTask);
OnTaskAdded?.Invoke(tasks);
}
}
/// <summary>
/// 添加目录下所有文件,不要目录结构
/// </summary>
[RelayCommand]
private void AddAllFileTasks()
{
if (SelectedTask?.IsDirectory == true)
{
// 逐个添加添加目录下所有JSON文件作为独立任务
var taskInfos = GetAllJsonFilesInDirectory(SelectedTask);
var tasks = new List<GearTaskViewModel>();
foreach (var taskInfo in taskInfos)
{
var gearTaskViewModel = new GearTaskViewModel
{
Name = Path.GetFileNameWithoutExtension(taskInfo.Name),
Path = @$"{{pathingRepoFolder}}\{taskInfo.RelativePath}\",
IsDirectory = false
};
tasks.Add(gearTaskViewModel);
}
if (tasks.Count > 0)
{
OnTaskAdded?.Invoke(tasks);
}
}
}
/// <summary>
/// 保持目录结构添加所有目录下文件
/// </summary>
[RelayCommand]
private void AddFileTasksWithStructure()
{
if (SelectedTask?.IsDirectory == true)
{
// 保持目录结构添加所有子任务
var tasks = GetAllFileTasksInDirectoryWithStructure(SelectedTask);
OnTaskAdded?.Invoke(tasks);
}
}
/// <summary>
/// 获取目录下所有JSON文件
/// </summary>
private List<PathingTaskInfo> GetAllJsonFilesInDirectory(PathingTaskInfo directory)
{
var jsonFiles = new List<PathingTaskInfo>();
if (directory.Children is { Count: > 0 })
{
foreach (var child in directory.Children)
{
if (child.IsDirectory)
{
jsonFiles.AddRange(GetAllJsonFilesInDirectory(child));
}
else if (child.FullPath.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
{
jsonFiles.Add(child);
}
}
}
return jsonFiles;
}
/// <summary>
/// 获取目录下所有json文件任务并保持结构
/// </summary>
private List<GearTaskViewModel> GetAllFileTasksInDirectoryWithStructure(PathingTaskInfo directory)
{
var tasks = new List<GearTaskViewModel>();
if (directory.Children is { Count: > 0 })
{
foreach (var child in directory.Children)
{
if (child.IsDirectory)
{
// 添加子目录作为组
var groupTask = new GearTaskViewModel
{
Name = child.Name,
IsDirectory = true
};
tasks.Add(groupTask);
// 递归添加子任务
tasks.AddRange(GetAllFileTasksInDirectoryWithStructure(child));
}
else if (child.FullPath.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
{
// 添加JSON文件作为任务
var fileTask = new GearTaskViewModel
{
Name = Path.GetFileNameWithoutExtension(child.Name),
Path = @$"{{pathingRepoFolder}}\{child.RelativePath}\",
IsDirectory = false
};
tasks.Add(fileTask);
}
}
}
return tasks;
}
/// <summary>
/// 获取目录下所有文件夹任务并保持结构
/// </summary>
private List<GearTaskViewModel> GetAllFolderTasksInDirectoryWithStructure(PathingTaskInfo directory)
{
var tasks = new List<GearTaskViewModel>();
if (directory.Children is { Count: > 0 })
{
foreach (var child in directory.Children)
{
if (child.IsDirectory)
{
// 判断 child 的子节点是否是文件
var hasJsonFile = child.Children.Any(grandChild => !grandChild.IsDirectory && grandChild.FullPath.EndsWith(".json", StringComparison.OrdinalIgnoreCase));
if (hasJsonFile)
{
// 添加子目录作为任务
var groupTask = new GearTaskViewModel
{
Name = child.Name,
TaskType = "Pathing",
Path = @$"{{pathingRepoFolder}}\{child.RelativePath}\",
IsDirectory = false
};
tasks.Add(groupTask);
}
else
{
// 添加子目录作为组
var groupTask = new GearTaskViewModel
{
Name = child.Name,
IsDirectory = true
};
tasks.Add(groupTask);
// 递归添加子任务
tasks.AddRange(GetAllFolderTasksInDirectoryWithStructure(child));
}
}
}
}
return tasks;
}
}