diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Item/MaterialIds.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Item/MaterialIds.cs
index 382c10b..4c5b170 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Item/MaterialIds.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Item/MaterialIds.cs
@@ -10,6 +10,18 @@ namespace Snap.Hutao.Model.Metadata.Item;
internal static class MaterialIds
{
+ ///
+ /// 材料统计右键「未完成条目」不追溯的材料(摩拉、经验书、精锻用魔矿)。
+ ///
+ public static bool IsExcludedFromStatisticsConsumerMenu(uint materialId)
+ {
+ return materialId is Mora
+ or WanderersAdvice
+ or AdventurersExperience
+ or HeroesWit
+ or MysticEnhancementOre;
+ }
+
public const uint Mora = 202U; // 摩拉
public const uint WanderersAdvice = 104001U; // 流浪者的经验
public const uint AdventurersExperience = 104002U; // 冒险家的经验
diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx
index 3dcd1bf..b32c8e9 100644
--- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx
+++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx
@@ -2435,6 +2435,15 @@ Space Available: {2}
Material Statistics
+
+ Unfinished: {0}
+
+
+ No unchecked entries
+
+
+ Mora, EXP books, and Mystic Enhancement Ore have no per-entry breakdown.
+
Go
diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
index 4a65ae1..769ed50 100644
--- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
+++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
@@ -2486,6 +2486,15 @@
材料统计
+
+ 未完成:{0}
+
+
+ 无未勾选条目
+
+
+ 摩拉、经验书与魔矿不显示未完成条目。
+
前往
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationRepository.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationRepository.cs
index 1a3082c..c1b01e9 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationRepository.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationRepository.cs
@@ -2,10 +2,14 @@
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
+using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Service.Abstraction;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
+using System.Linq;
namespace Snap.Hutao.Service.Cultivation;
@@ -96,4 +100,23 @@ internal sealed partial class CultivationRepository : ICultivationRepository
{
return this.Single(query => query.Where(entry => entry.InnerId == entryId).Select(entry => entry.InnerId));
}
+
+ public ImmutableArray<(CultivateEntry Entry, CultivateItem Item)> GetCultivateEntryItemPairsByProjectId(Guid projectId)
+ {
+ using (IServiceScope scope = ServiceProvider.CreateScope())
+ {
+ AppDbContext db = scope.GetAppDbContext();
+ IQueryable entries = db.Set().AsNoTracking().Where(e => e.ProjectId == projectId);
+ return [.. db.Set().AsNoTracking()
+ .Join(
+ entries,
+ item => item.EntryId,
+ entry => entry.InnerId,
+ (item, entry) => new { Entry = entry, Item = item })
+ .OrderBy(t => t.Entry.InnerId)
+ .ThenBy(t => t.Item.ItemId)
+ .ToList()
+ .Select(t => (t.Entry, t.Item))];
+ }
+ }
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs
index 6c71220..cd2badd 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs
@@ -2,18 +2,23 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
+using Snap.Hutao.Model;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata;
+using Snap.Hutao.Model.Metadata.Item;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Cultivation.Consumption;
using Snap.Hutao.Service.Inventory;
using Snap.Hutao.Service.Metadata.ContextAbstraction;
using Snap.Hutao.ViewModel.Cultivation;
using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Globalization;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
+using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ModelItem = Snap.Hutao.Model.Item;
@@ -130,6 +135,7 @@ internal sealed partial class CultivationService : ICultivationService
}
CultivationStatisticsSurplusMerge.Apply(resultItems, context, mergeOptions);
+ ApplyStatisticsConsumerMenuText(resultItems, projectId, context, cultivationRepository);
return new(resultItems);
}
@@ -305,4 +311,63 @@ internal sealed partial class CultivationService : ICultivationService
}
}
}
+
+ private static void ApplyStatisticsConsumerMenuText(
+ Dictionary resultItems,
+ Guid projectId,
+ ICultivationMetadataContext context,
+ ICultivationRepository cultivationRepository)
+ {
+ ImmutableArray<(CultivateEntry Entry, CultivateItem Item)> pairs = cultivationRepository.GetCultivateEntryItemPairsByProjectId(projectId);
+ Dictionary> unfinishedSegmentsByMaterial = [];
+
+ foreach ((CultivateEntry entry, CultivateItem item) in pairs.AsSpan())
+ {
+ if (item.IsFinished)
+ {
+ continue;
+ }
+
+ string name = FormatStatisticsConsumerEntryName(entry, context);
+ string segment = $"{name}×{item.Count}";
+ ref List? list = ref CollectionsMarshal.GetValueRefOrAddDefault(unfinishedSegmentsByMaterial, item.ItemId, out _);
+ list ??= [];
+ list.Add(segment);
+ }
+
+ foreach ((uint materialId, StatisticsCultivateItem stat) in resultItems)
+ {
+ if (MaterialIds.IsExcludedFromStatisticsConsumerMenu(materialId))
+ {
+ stat.StatisticsConsumerMenuText = SH.ViewPageCultivationStatisticsConsumerMenuExcluded;
+ continue;
+ }
+
+ if (unfinishedSegmentsByMaterial.TryGetValue(materialId, out List? segments))
+ {
+ segments.Sort(StringComparer.Ordinal);
+ stat.StatisticsConsumerMenuText = string.Format(
+ CultureInfo.CurrentCulture,
+ SH.ViewPageCultivationStatisticsUnfinishedConsumers,
+ string.Join(',', segments));
+ }
+ else
+ {
+ stat.StatisticsConsumerMenuText = string.Format(
+ CultureInfo.CurrentCulture,
+ SH.ViewPageCultivationStatisticsUnfinishedConsumers,
+ SH.ViewPageCultivationStatisticsUnfinishedConsumersEmptyList);
+ }
+ }
+ }
+
+ private static string FormatStatisticsConsumerEntryName(CultivateEntry entry, ICultivationMetadataContext context)
+ {
+ return entry.Type switch
+ {
+ CultivateType.AvatarAndSkill => context.GetAvatarItem(entry.Id).Name,
+ CultivateType.Weapon => context.GetWeaponItem(entry.Id).Name,
+ _ => Material.Default.Name,
+ };
+ }
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationRepository.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationRepository.cs
index 9f32114..a6b7754 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationRepository.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationRepository.cs
@@ -44,4 +44,9 @@ internal interface ICultivationRepository : IRepository GetCultivateEntryImmutableArrayByProjectIdAndItemId(Guid projectId, uint itemId);
Guid GetCultivateProjectIdByEntryId(Guid entryId);
+
+ ///
+ /// 联表查询某计划下所有养成物品及其所属条目(用于材料统计未勾选条目等)。
+ ///
+ ImmutableArray<(CultivateEntry Entry, CultivateItem Item)> GetCultivateEntryItemPairsByProjectId(Guid projectId);
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/CultivationPage.xaml b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/CultivationPage.xaml
index 4825003..f57814d 100644
--- a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/CultivationPage.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/CultivationPage.xaml
@@ -227,7 +227,13 @@
-
+
+
+
+
+
+
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/StatisticsCultivateItem.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/StatisticsCultivateItem.cs
index ff64dc0..7457426 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/StatisticsCultivateItem.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/StatisticsCultivateItem.cs
@@ -66,6 +66,11 @@ internal sealed class StatisticsCultivateItem
public bool IsToday { get => Inner.IsItemOfToday(offset, true); }
+ ///
+ /// 材料统计右键菜单中展示的「未完成」养成条目说明(含需求量)。
+ ///
+ public string StatisticsConsumerMenuText { get; set; } = string.Empty;
+
internal bool ExcludedFromPresentation { get; set; }
public static StatisticsCultivateItem Create(Material inner, TimeSpan offset)