feat: 新增采集与锄地一键采集工具页面

新增采集与锄地专用功能页面,支持通过素材名称匹配本地地图追踪任务并一键执行,具体改动如下:
- 新增GatheringAndFarmingPage视图与对应ViewModel,包含角色、食材与特产、掉落物、矿石四个标签页
- 在主窗口导航栏添加采集与锄地页面入口,并将页面注册到应用依赖注入容器
- 优化齿轮任务系统,添加任务组配置属性,实现配置修改自动保存,完善脚本执行参数传递逻辑
- 在任务列表页面新增任务组设置按钮与配置弹窗
- 修复部分代码格式与缩进问题,调整命名空间引用顺序
This commit is contained in:
辉鸭蛋
2026-05-18 02:02:29 +08:00
parent 5f9b8276dc
commit 3dcc9dde70
3 changed files with 991 additions and 0 deletions

View File

@@ -0,0 +1,528 @@
<UserControl x:Class="BetterGenshinImpact.View.Pages.GatheringAndFarmingPage"
x:Name="Root"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:pages="clr-namespace:BetterGenshinImpact.ViewModel.Pages"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
d:DataContext="{d:DesignInstance Type=pages:GatheringAndFarmingPageViewModel}"
d:DesignHeight="860"
d:DesignWidth="1200"
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
FontFamily="{StaticResource TextThemeFontFamily}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
mc:Ignorable="d">
<UserControl.Resources>
<Style x:Key="SummaryCardStyle" TargetType="Border">
<Setter Property="Background" Value="{ui:ThemeResource ControlFillColorSecondaryBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource SurfaceStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Padding" Value="16,14" />
</Style>
<Style x:Key="CharacterCardStyle" TargetType="Border">
<Setter Property="Width" Value="336" />
<Setter Property="Margin" Value="0,0,12,12" />
<Setter Property="Background" Value="{ui:ThemeResource ControlFillColorSecondaryBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource SurfaceStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Padding" Value="16" />
</Style>
<Style x:Key="MaterialButtonStyle" TargetType="Button">
<Setter Property="Margin" Value="0,0,0,10" />
<Setter Property="Padding" Value="10" />
<Setter Property="Background" Value="{ui:ThemeResource ControlFillColorDefaultBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource SurfaceStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border x:Name="RootBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="10"
SnapsToDevicePixels="True">
<ContentPresenter Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="RootBorder" Property="Background" Value="{ui:ThemeResource ControlFillColorSecondaryBrush}" />
<Setter TargetName="RootBorder" Property="BorderBrush" Value="{DynamicResource AccentFillColorDefaultBrush}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="RootBorder" Property="Background" Value="{ui:ThemeResource ControlFillColorDefaultBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="TrackTileStyle" TargetType="Button" BasedOn="{StaticResource MaterialButtonStyle}">
<Setter Property="Width" Value="220" />
<Setter Property="Margin" Value="0,0,12,12" />
<Setter Property="Padding" Value="14" />
</Style>
<Style x:Key="ConsistentTabControlStyle" TargetType="TabControl" BasedOn="{StaticResource {x:Type TabControl}}">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabControl}">
<Grid Background="{TemplateBinding Background}" ClipToBounds="True" SnapsToDevicePixels="True">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border Grid.Row="0"
Margin="0,0,0,10"
Padding="6"
Background="{ui:ThemeResource ControlFillColorSecondaryBrush}"
BorderBrush="{DynamicResource SurfaceStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="10">
<TabPanel IsItemsHost="True" Background="Transparent" />
</Border>
<Border Grid.Row="1"
Background="{ui:ThemeResource ControlFillColorSecondaryBrush}"
BorderBrush="{DynamicResource SurfaceStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="12">
<Border Margin="1"
Background="{ui:ThemeResource CardBackgroundFillColorSecondaryBrush}"
CornerRadius="11">
<ContentPresenter x:Name="PART_SelectedContentHost"
Margin="{TemplateBinding Padding}"
Content="{TemplateBinding SelectedContent}"
ContentStringFormat="{TemplateBinding SelectedContentStringFormat}"
ContentTemplate="{TemplateBinding SelectedContentTemplate}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Border>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ConsistentTabItemStyle" TargetType="TabItem" BasedOn="{StaticResource {x:Type TabItem}}">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
<Setter Property="Padding" Value="16,9" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Grid Background="Transparent" SnapsToDevicePixels="True">
<Border x:Name="mainBorder"
Margin="0,0,6,0"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="0"
CornerRadius="8">
<ContentPresenter Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Header}"
ContentStringFormat="{TemplateBinding HeaderStringFormat}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Border>
<Border x:Name="selectionIndicator"
Width="44"
Height="3"
Margin="0,0,5,5"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Background="{DynamicResource AccentFillColorDefaultBrush}"
CornerRadius="1.5"
Opacity="0" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}" />
<Setter TargetName="mainBorder" Property="Background" Value="{DynamicResource ControlFillColorDefaultBrush}" />
<Setter TargetName="mainBorder" Property="BorderBrush" Value="{DynamicResource ControlElevationBorderBrush}" />
<Setter TargetName="mainBorder" Property="BorderThickness" Value="1" />
<Setter TargetName="selectionIndicator" Property="Opacity" Value="1" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="mainBorder" Property="Background" Value="{DynamicResource ControlFillColorSecondaryBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Grid Margin="24,16,24,12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel>
<TextBlock FontSize="24" FontWeight="SemiBold" Text="采集与锄地" />
<TextBlock Margin="0,8,0,0"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap">
统一收纳角色培养素材、食材与特产、掉落物和矿石的快捷入口。角色页会展示 3 个常用升级素材,点击素材图标后会先向用户确认,再执行自动匹配到的地图追踪任务。
</TextBlock>
</StackPanel>
<StackPanel Grid.Column="1"
Orientation="Horizontal"
VerticalAlignment="Top">
<ui:Button Margin="0,0,8,0"
Command="{Binding OpenMapPathingCommand}"
Content="地图追踪"
Icon="{ui:SymbolIcon Map24}" />
<ui:Button Margin="0,0,8,0"
Command="{Binding OpenScriptRepoCommand}"
Content="脚本仓库" />
<ui:Button Command="{Binding RefreshPathingIndexCommand}"
Content="刷新路线索引" />
</StackPanel>
</Grid>
<Grid Grid.Row="1" Margin="0,16,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="12" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="12" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Style="{StaticResource SummaryCardStyle}">
<StackPanel>
<TextBlock Foreground="{DynamicResource TextFillColorSecondaryBrush}" Text="本地地图追踪任务" />
<TextBlock Margin="0,8,0,0"
FontSize="28"
FontWeight="SemiBold"
Text="{Binding AvailablePathingTaskCount}" />
<TextBlock Margin="0,8,0,0"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Text="{Binding TaskIndexSummary}" />
</StackPanel>
</Border>
<Border Grid.Column="2" Style="{StaticResource SummaryCardStyle}">
<StackPanel>
<TextBlock Foreground="{DynamicResource TextFillColorSecondaryBrush}" Text="素材入口数量" />
<TextBlock Margin="0,8,0,0"
FontSize="28"
FontWeight="SemiBold"
Text="{Binding TrackEntryCount}" />
<TextBlock Margin="0,8,0,0"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Text="角色页、素材页、掉落物页和矿石页共享同一套追踪确认逻辑。" />
</StackPanel>
</Border>
<Border Grid.Column="4" Style="{StaticResource SummaryCardStyle}">
<StackPanel>
<TextBlock Foreground="{DynamicResource TextFillColorSecondaryBrush}" Text="使用建议" />
<TextBlock Margin="0,8,0,0"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
TextWrapping="Wrap">
素材名称会优先按任务文件名精确匹配,其次按相对路径模糊匹配。若未找到路线,请先在脚本仓库或地图追踪页准备对应任务。
</TextBlock>
</StackPanel>
</Border>
</Grid>
<TabControl Grid.Row="2"
Style="{StaticResource ConsistentTabControlStyle}">
<TabItem Header="角色" Style="{StaticResource ConsistentTabItemStyle}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="16">
<TextBlock Margin="0,0,0,14"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="角色卡片默认展示 3 个常用升级素材。点击素材图标后会先弹出确认框,再执行匹配到的地图追踪任务。"
TextWrapping="Wrap" />
<ItemsControl ItemsSource="{Binding Characters}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Style="{StaticResource CharacterCardStyle}">
<StackPanel>
<Grid Margin="0,0,0,14">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Width="54"
Height="54"
Background="{Binding AccentSurfaceBrush}"
BorderBrush="{Binding AccentBrush}"
BorderThickness="1"
CornerRadius="27">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="22"
FontWeight="SemiBold"
Foreground="{Binding AccentBrush}"
Text="{Binding AvatarText}" />
</Border>
<StackPanel Grid.Column="1" Margin="12,0,0,0" VerticalAlignment="Center">
<TextBlock FontSize="18" FontWeight="SemiBold" Text="{Binding Name}" />
<TextBlock Margin="0,4,0,0"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="{Binding Subtitle}" />
</StackPanel>
</Grid>
<TextBlock Margin="0,0,0,10"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Text="常用升级素材"
TextWrapping="Wrap" />
<ItemsControl ItemsSource="{Binding Materials}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Style="{StaticResource MaterialButtonStyle}"
Command="{Binding DataContext.TrackMaterialCommand, ElementName=Root}"
CommandParameter="{Binding}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Border Width="40"
Height="40"
Background="{Binding AccentSurfaceBrush}"
BorderBrush="{Binding AccentBrush}"
BorderThickness="1"
CornerRadius="20">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="16"
FontWeight="SemiBold"
Foreground="{Binding AccentBrush}"
Text="{Binding ShortLabel}" />
</Border>
<StackPanel Grid.Column="1"
Margin="10,0,0,0"
VerticalAlignment="Center">
<TextBlock FontWeight="SemiBold" Text="{Binding Name}" />
<TextBlock Margin="0,3,0,0"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="{Binding Description}"
TextWrapping="Wrap" />
</StackPanel>
<TextBlock Grid.Column="2"
Margin="12,0,0,0"
VerticalAlignment="Center"
Foreground="{Binding AccentBrush}"
Text="追踪" />
</Grid>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</TabItem>
<TabItem Header="食材与特产" Style="{StaticResource ConsistentTabItemStyle}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding FoodAndSpecialtySections}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Margin="16,16,16,4">
<TextBlock FontSize="18" FontWeight="SemiBold" Text="{Binding Title}" />
<TextBlock Margin="0,6,0,12"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="{Binding Description}"
TextWrapping="Wrap" />
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Style="{StaticResource TrackTileStyle}"
Command="{Binding DataContext.TrackMaterialCommand, ElementName=Root}"
CommandParameter="{Binding}">
<StackPanel>
<Border Width="46"
Height="46"
Margin="0,0,0,12"
Background="{Binding AccentSurfaceBrush}"
BorderBrush="{Binding AccentBrush}"
BorderThickness="1"
CornerRadius="23">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="18"
FontWeight="SemiBold"
Foreground="{Binding AccentBrush}"
Text="{Binding ShortLabel}" />
</Border>
<TextBlock FontSize="16" FontWeight="SemiBold" Text="{Binding Name}" />
<TextBlock Margin="0,6,0,0"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="{Binding Description}"
TextWrapping="Wrap" />
<TextBlock Margin="0,10,0,0"
Foreground="{Binding AccentBrush}"
Text="点击开始追踪" />
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</TabItem>
<TabItem Header="掉落物" Style="{StaticResource ConsistentTabItemStyle}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="16">
<TextBlock Margin="0,0,0,12"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="适合快速补充怪物系列路线。这里推荐把常刷对象单独准备成地图追踪任务,方便一键执行。"
TextWrapping="Wrap" />
<ItemsControl ItemsSource="{Binding DropItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Style="{StaticResource TrackTileStyle}"
Command="{Binding DataContext.TrackMaterialCommand, ElementName=Root}"
CommandParameter="{Binding}">
<StackPanel>
<Border Width="46"
Height="46"
Margin="0,0,0,12"
Background="{Binding AccentSurfaceBrush}"
BorderBrush="{Binding AccentBrush}"
BorderThickness="1"
CornerRadius="23">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="18"
FontWeight="SemiBold"
Foreground="{Binding AccentBrush}"
Text="{Binding ShortLabel}" />
</Border>
<TextBlock FontSize="16" FontWeight="SemiBold" Text="{Binding Name}" />
<TextBlock Margin="0,6,0,0"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="{Binding Description}"
TextWrapping="Wrap" />
<TextBlock Margin="0,10,0,0"
Foreground="{Binding AccentBrush}"
Text="点击开始追踪" />
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</TabItem>
<TabItem Header="矿石" Style="{StaticResource ConsistentTabItemStyle}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="16">
<TextBlock Margin="0,0,0,12"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="矿石页更偏向长线补货,建议把常刷路线做成每日或隔日脚本。点击卡片后同样会先确认再执行。"
TextWrapping="Wrap" />
<ItemsControl ItemsSource="{Binding OreItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Style="{StaticResource TrackTileStyle}"
Command="{Binding DataContext.TrackMaterialCommand, ElementName=Root}"
CommandParameter="{Binding}">
<StackPanel>
<Border Width="46"
Height="46"
Margin="0,0,0,12"
Background="{Binding AccentSurfaceBrush}"
BorderBrush="{Binding AccentBrush}"
BorderThickness="1"
CornerRadius="23">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="18"
FontWeight="SemiBold"
Foreground="{Binding AccentBrush}"
Text="{Binding ShortLabel}" />
</Border>
<TextBlock FontSize="16" FontWeight="SemiBold" Text="{Binding Name}" />
<TextBlock Margin="0,6,0,0"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="{Binding Description}"
TextWrapping="Wrap" />
<TextBlock Margin="0,10,0,0"
Foreground="{Binding AccentBrush}"
Text="点击开始追踪" />
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
</Grid>
</UserControl>

View File

@@ -0,0 +1,15 @@
using BetterGenshinImpact.ViewModel.Pages;
using System.Windows.Controls;
namespace BetterGenshinImpact.View.Pages;
public partial class GatheringAndFarmingPage : UserControl
{
private GatheringAndFarmingPageViewModel ViewModel { get; }
public GatheringAndFarmingPage(GatheringAndFarmingPageViewModel viewModel)
{
DataContext = ViewModel = viewModel;
InitializeComponent();
}
}

View File

@@ -0,0 +1,448 @@
using BetterGenshinImpact.Core.Script;
using BetterGenshinImpact.Core.Script.Group;
using BetterGenshinImpact.Service.Interface;
using BetterGenshinImpact.View.Pages;
using BetterGenshinImpact.View.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Media;
using Wpf.Ui;
using Wpf.Ui.Violeta.Controls;
namespace BetterGenshinImpact.ViewModel.Pages;
public partial class GatheringAndFarmingPageViewModel : ViewModel
{
private readonly ILogger<GatheringAndFarmingPageViewModel> _logger = App.GetLogger<GatheringAndFarmingPageViewModel>();
private readonly IScriptService _scriptService;
private readonly INavigationService _navigationService;
private readonly List<PathingTaskIndexEntry> _pathingTaskIndex = [];
private bool _isInitialized;
[ObservableProperty] private ObservableCollection<GatherCharacterCard> _characters = [];
[ObservableProperty] private ObservableCollection<GatherTrackSection> _foodAndSpecialtySections = [];
[ObservableProperty] private ObservableCollection<GatherTrackItem> _dropItems = [];
[ObservableProperty] private ObservableCollection<GatherTrackItem> _oreItems = [];
[ObservableProperty] private int _availablePathingTaskCount;
[ObservableProperty] private string _taskIndexSummary = "尚未扫描路线";
public int TrackEntryCount =>
Characters.Sum(x => x.Materials.Count)
+ FoodAndSpecialtySections.Sum(x => x.Items.Count)
+ DropItems.Count
+ OreItems.Count;
public GatheringAndFarmingPageViewModel(IScriptService scriptService, INavigationService navigationService)
{
_scriptService = scriptService;
_navigationService = navigationService;
}
public override void OnNavigatedTo()
{
if (_isInitialized)
{
return;
}
BuildDesignData();
_ = RefreshPathingIndexAsync();
_isInitialized = true;
}
[RelayCommand]
private void OpenMapPathing()
{
_navigationService.Navigate(typeof(MapPathingPage));
}
[RelayCommand]
private void OpenScriptRepo()
{
ScriptRepoUpdater.Instance.OpenScriptRepoWindow();
}
[RelayCommand]
private async Task RefreshPathingIndexAsync()
{
try
{
var pathingRoot = MapPathingViewModel.PathJsonPath;
if (!Directory.Exists(pathingRoot))
{
_pathingTaskIndex.Clear();
AvailablePathingTaskCount = 0;
TaskIndexSummary = "未找到本地地图追踪目录,编译后首次运行会自动生成。";
return;
}
var files = await Task.Run(() => Directory
.EnumerateFiles(pathingRoot, "*.json", SearchOption.AllDirectories)
.Select(path =>
{
var relativePath = Path.GetRelativePath(pathingRoot, path);
return new PathingTaskIndexEntry(
path,
relativePath.Replace('/', '\\'),
Path.GetFileNameWithoutExtension(path));
})
.OrderBy(x => x.RelativePath, StringComparer.OrdinalIgnoreCase)
.ToList());
_pathingTaskIndex.Clear();
_pathingTaskIndex.AddRange(files);
AvailablePathingTaskCount = files.Count;
TaskIndexSummary = files.Count == 0
? "已扫描完成,但本地还没有可执行的地图追踪任务。"
: $"已完成扫描,可直接匹配 {files.Count} 条地图追踪任务。";
}
catch (Exception ex)
{
_logger.LogError(ex, "刷新采集与锄地路线索引失败");
TaskIndexSummary = $"路线索引刷新失败:{ex.Message}";
Toast.Error($"路线索引刷新失败:{ex.Message}");
}
}
[RelayCommand]
private async Task TrackMaterialAsync(GatherTrackItem? item)
{
if (item == null)
{
return;
}
if (_pathingTaskIndex.Count == 0)
{
await RefreshPathingIndexAsync();
}
var matchedTask = FindBestPathingTask(item.SearchKeywords);
if (matchedTask == null)
{
await ThemedMessageBox.WarningAsync(
$"未找到与“{item.Name}”匹配的地图追踪任务。\n\n你可以先在“脚本仓库”或“地图追踪”中准备对应路线然后再从这里一键执行。",
"未找到路线");
return;
}
var result = await ThemedMessageBox.QuestionAsync(
$"是否执行“{item.Name}”对应的地图追踪任务?\n\n匹配到的路线{matchedTask.RelativePath}",
"执行地图追踪",
System.Windows.MessageBoxButton.YesNo,
System.Windows.MessageBoxResult.No);
if (result != System.Windows.MessageBoxResult.Yes)
{
return;
}
try
{
var project = BuildPathingProject(matchedTask.FullPath);
await _scriptService.RunMulti([project]);
Toast.Success($"已开始执行路线:{Path.GetFileNameWithoutExtension(matchedTask.FullPath)}");
}
catch (Exception ex)
{
_logger.LogError(ex, "执行素材路线失败: {MaterialName}", item.Name);
await ThemedMessageBox.ErrorAsync($"执行“{item.Name}”失败:{ex.Message}", "执行失败");
}
}
private PathingTaskIndexEntry? FindBestPathingTask(IEnumerable<string> keywords)
{
var keywordList = keywords
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(x => x.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (keywordList.Count == 0)
{
return null;
}
return _pathingTaskIndex
.Select(entry => new
{
Entry = entry,
Score = keywordList.Max(keyword => ScorePathingTask(entry, keyword))
})
.Where(x => x.Score > 0)
.OrderByDescending(x => x.Score)
.ThenBy(x => x.Entry.RelativePath.Length)
.ThenBy(x => x.Entry.RelativePath, StringComparer.OrdinalIgnoreCase)
.Select(x => x.Entry)
.FirstOrDefault();
}
private static int ScorePathingTask(PathingTaskIndexEntry entry, string keyword)
{
if (entry.FileName.Equals(keyword, StringComparison.OrdinalIgnoreCase))
{
return 100;
}
if (entry.RelativePath.EndsWith(keyword + ".json", StringComparison.OrdinalIgnoreCase))
{
return 90;
}
if (entry.FileName.Contains(keyword, StringComparison.OrdinalIgnoreCase))
{
return 75;
}
if (entry.RelativePath.Contains(keyword, StringComparison.OrdinalIgnoreCase))
{
return 60;
}
return 0;
}
private static ScriptGroupProject BuildPathingProject(string fullPath)
{
var folder = Path.GetDirectoryName(fullPath) ?? MapPathingViewModel.PathJsonPath;
var relativeFolder = Path.GetRelativePath(MapPathingViewModel.PathJsonPath, folder);
if (relativeFolder == ".")
{
relativeFolder = string.Empty;
}
return ScriptGroupProject.BuildPathingProject(Path.GetFileName(fullPath), relativeFolder);
}
private void BuildDesignData()
{
Characters =
[
new GatherCharacterCard(
"纳西妲",
"须弥 · 草系主C/辅助",
"纳",
CreatePalette("#84C26E", "#1E3523"),
[
CreateTrack("月莲", "水边与林地特产路线", "月", "#84C26E", "#1E3523"),
CreateTrack("蕈兽", "浮游菌群常规掉落路线", "蕈", "#6BB9A0", "#1B342F", "蕈兽", "浮游菌"),
CreateTrack("树莓", "顺路补货的常用食材路线", "莓", "#D36B7F", "#3A1D25")
]),
new GatherCharacterCard(
"芙宁娜",
"枫丹 · 水系辅助",
"芙",
CreatePalette("#7EB7F3", "#1D2E45"),
[
CreateTrack("湖光铃兰", "枫丹湖区特产路线", "铃", "#7EB7F3", "#1D2E45"),
CreateTrack("原海异种", "海边通刷掉落路线", "海", "#5AA4D6", "#183247", "原海异种", "异海凝珠"),
CreateTrack("萃凝晶", "枫丹矿石补货路线", "晶", "#82C9FF", "#20354A")
]),
new GatherCharacterCard(
"雷电将军",
"稻妻 · 充能爆发核心",
"雷",
CreatePalette("#A887F7", "#2B2144"),
[
CreateTrack("天云草实", "清籁岛高密度特产路线", "云", "#A887F7", "#2B2144"),
CreateTrack("野伏众", "稻妻常刷掉落路线", "伏", "#E09A5E", "#442817", "野伏众", "刀镡"),
CreateTrack("紫晶块", "稻妻锻造矿石路线", "紫", "#B498FF", "#322650")
]),
new GatherCharacterCard(
"钟离",
"璃月 · 护盾辅助",
"钟",
CreatePalette("#D3A65C", "#43301A"),
[
CreateTrack("石珀", "璃月山体特产路线", "珀", "#D3A65C", "#43301A"),
CreateTrack("史莱姆", "通用掉落补货路线", "史", "#73C7C0", "#1C3737", "史莱姆", "史莱姆凝液"),
CreateTrack("白铁块", "璃月常用矿石路线", "铁", "#C7D0DA", "#2B3440")
]),
new GatherCharacterCard(
"那维莱特",
"枫丹 · 水系站场",
"那",
CreatePalette("#68C1E0", "#183743"),
[
CreateTrack("湖光铃兰", "角色专属特产路线", "铃", "#68C1E0", "#183743"),
CreateTrack("原海异种", "海边通刷掉落路线", "海", "#68C1E0", "#183743", "原海异种", "异海凝珠"),
CreateTrack("水晶块", "精锻魔矿主力矿线", "晶", "#8FD2F2", "#213A49")
]),
new GatherCharacterCard(
"娜维娅",
"枫丹 · 岩系输出",
"娜",
CreatePalette("#E0B765", "#45371A"),
[
CreateTrack("苍晶螺", "枫丹海岸特产路线", "螺", "#73C0D4", "#203743"),
CreateTrack("发条机关", "枫丹机械系掉落路线", "机", "#D6A86A", "#45301E"),
CreateTrack("白铁块", "锻造补货矿石路线", "铁", "#C7D0DA", "#2B3440")
])
];
FoodAndSpecialtySections =
[
new GatherTrackSection(
"地方特产",
"更适合按地区分批补货,适合角色突破前集中准备。",
[
CreateTrack("月莲", "须弥水边高密度路线", "月", "#84C26E", "#1E3523"),
CreateTrack("清心", "璃月高山特产路线", "清", "#7CC6E6", "#183543"),
CreateTrack("苍晶螺", "枫丹海岸特产路线", "螺", "#73C0D4", "#203743"),
CreateTrack("湖光铃兰", "枫丹湖区采集路线", "铃", "#7EB7F3", "#1D2E45"),
CreateTrack("石珀", "璃月矿壁采集路线", "珀", "#D3A65C", "#43301A"),
CreateTrack("天云草实", "清籁岛采集路线", "云", "#A887F7", "#2B2144")
]),
new GatherTrackSection(
"食材",
"适合烹饪、周本前补货或顺路囤积。",
[
CreateTrack("甜甜花", "通用食材顺路收集", "甜", "#E38B98", "#452128"),
CreateTrack("松茸", "林地食材补货路线", "茸", "#D6A86A", "#433118"),
CreateTrack("树莓", "前期高频食材路线", "莓", "#D36B7F", "#3A1D25"),
CreateTrack("莲蓬", "璃月水域食材路线", "莲", "#7DC0B7", "#1C3533"),
CreateTrack("日落果", "蒙德野外食材路线", "果", "#E49B67", "#472818"),
CreateTrack("鱼肉", "沿河补货路线", "鱼", "#6AB6E6", "#1C3042")
])
];
DropItems =
[
CreateTrack("丘丘人射手", "弓手掉落集中清线", "丘", "#C98A5B", "#3A2518", "丘丘人射手", "丘丘人"),
CreateTrack("史莱姆", "元素凝液快速补货", "史", "#73C7C0", "#1C3737", "史莱姆", "史莱姆凝液"),
CreateTrack("蕈兽", "真菌孢子常用路线", "蕈", "#6BB9A0", "#1B342F", "蕈兽", "浮游菌"),
CreateTrack("发条机关", "枫丹机械掉落路线", "机", "#D6A86A", "#45301E"),
CreateTrack("遗迹守卫", "机关核心补货路线", "遗", "#8E9EB5", "#26313F"),
CreateTrack("原海异种", "海边材料通刷路线", "海", "#5AA4D6", "#183247", "原海异种", "异海凝珠")
];
OreItems =
[
CreateTrack("白铁块", "日常锻造基础矿线", "铁", "#C7D0DA", "#2B3440"),
CreateTrack("水晶块", "精锻魔矿主力矿线", "晶", "#8FD2F2", "#213A49"),
CreateTrack("紫晶块", "稻妻矿石补货路线", "紫", "#B498FF", "#322650"),
CreateTrack("星银矿石", "雪山专属矿石路线", "银", "#9FC6DC", "#213545"),
CreateTrack("萃凝晶", "枫丹矿石补货路线", "凝", "#82C9FF", "#20354A"),
CreateTrack("铁块", "早期锻造补货路线", "块", "#B4BDC8", "#29323D")
];
OnPropertyChanged(nameof(TrackEntryCount));
}
private static GatherTrackItem CreateTrack(
string name,
string description,
string shortLabel,
string accentHex,
string surfaceHex,
params string[] keywords)
{
var palette = CreatePalette(accentHex, surfaceHex);
return new GatherTrackItem(
name,
description,
shortLabel,
palette.AccentBrush,
palette.SurfaceBrush,
keywords.Length == 0 ? [name] : keywords);
}
private static GatherPalette CreatePalette(string accentHex, string surfaceHex)
{
return new GatherPalette(CreateBrush(accentHex), CreateBrush(surfaceHex));
}
private static Brush CreateBrush(string hex)
{
var color = (Color)ColorConverter.ConvertFromString(hex)!;
var brush = new SolidColorBrush(color);
brush.Freeze();
return brush;
}
private sealed record PathingTaskIndexEntry(string FullPath, string RelativePath, string FileName);
}
public sealed class GatherPalette
{
public Brush AccentBrush { get; }
public Brush SurfaceBrush { get; }
public GatherPalette(Brush accentBrush, Brush surfaceBrush)
{
AccentBrush = accentBrush;
SurfaceBrush = surfaceBrush;
}
}
public sealed class GatherCharacterCard
{
public string Name { get; }
public string Subtitle { get; }
public string AvatarText { get; }
public Brush AccentBrush { get; }
public Brush AccentSurfaceBrush { get; }
public ObservableCollection<GatherTrackItem> Materials { get; }
public GatherCharacterCard(
string name,
string subtitle,
string avatarText,
GatherPalette palette,
IEnumerable<GatherTrackItem> materials)
{
Name = name;
Subtitle = subtitle;
AvatarText = avatarText;
AccentBrush = palette.AccentBrush;
AccentSurfaceBrush = palette.SurfaceBrush;
Materials = new ObservableCollection<GatherTrackItem>(materials);
}
}
public sealed class GatherTrackSection
{
public string Title { get; }
public string Description { get; }
public ObservableCollection<GatherTrackItem> Items { get; }
public GatherTrackSection(string title, string description, IEnumerable<GatherTrackItem> items)
{
Title = title;
Description = description;
Items = new ObservableCollection<GatherTrackItem>(items);
}
}
public sealed class GatherTrackItem
{
public string Name { get; }
public string Description { get; }
public string ShortLabel { get; }
public Brush AccentBrush { get; }
public Brush AccentSurfaceBrush { get; }
public IReadOnlyList<string> SearchKeywords { get; }
public GatherTrackItem(
string name,
string description,
string shortLabel,
Brush accentBrush,
Brush accentSurfaceBrush,
IReadOnlyList<string> searchKeywords)
{
Name = name;
Description = description;
ShortLabel = shortLabel;
AccentBrush = accentBrush;
AccentSurfaceBrush = accentSurfaceBrush;
SearchKeywords = searchKeywords;
}
}