mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
add api cache
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Snap.Hutao.Extension;
|
||||||
|
|
||||||
namespace Snap.Hutao.Core.Database;
|
namespace Snap.Hutao.Core.Database;
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ namespace Snap.Hutao.Core.Database;
|
|||||||
/// <typeparam name="TMessage">消息的类型</typeparam>
|
/// <typeparam name="TMessage">消息的类型</typeparam>
|
||||||
internal class DbCurrent<TEntity, TMessage>
|
internal class DbCurrent<TEntity, TMessage>
|
||||||
where TEntity : class, ISelectable
|
where TEntity : class, ISelectable
|
||||||
where TMessage : Message.ValueChangedMessage<TEntity>
|
where TMessage : Message.ValueChangedMessage<TEntity>, new()
|
||||||
{
|
{
|
||||||
private readonly DbContext dbContext;
|
private readonly DbContext dbContext;
|
||||||
private readonly DbSet<TEntity> dbSet;
|
private readonly DbSet<TEntity> dbSet;
|
||||||
@@ -25,12 +26,12 @@ internal class DbCurrent<TEntity, TMessage>
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 构造一个新的数据库当前项
|
/// 构造一个新的数据库当前项
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dbContext">数据库上下文</param>
|
|
||||||
/// <param name="dbSet">数据集</param>
|
/// <param name="dbSet">数据集</param>
|
||||||
/// <param name="messenger">消息器</param>
|
/// <param name="messenger">消息器</param>
|
||||||
public DbCurrent(DbContext dbContext, DbSet<TEntity> dbSet, IMessenger messenger)
|
///
|
||||||
|
public DbCurrent(DbSet<TEntity> dbSet, IMessenger messenger)
|
||||||
{
|
{
|
||||||
this.dbContext = dbContext;
|
this.dbContext = dbSet.Context();
|
||||||
this.dbSet = dbSet;
|
this.dbSet = dbSet;
|
||||||
this.messenger = messenger;
|
this.messenger = messenger;
|
||||||
}
|
}
|
||||||
@@ -55,96 +56,18 @@ internal class DbCurrent<TEntity, TMessage>
|
|||||||
if (current != null)
|
if (current != null)
|
||||||
{
|
{
|
||||||
current.IsSelected = false;
|
current.IsSelected = false;
|
||||||
dbSet.Update(current);
|
dbSet.UpdateAndSave(current);
|
||||||
dbContext.SaveChanges();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TMessage message = (TMessage)Activator.CreateInstance(typeof(TMessage), current, value)!;
|
TMessage message = new() { OldValue = current, NewValue = value };
|
||||||
|
|
||||||
current = value;
|
current = value;
|
||||||
|
|
||||||
if (current != null)
|
if (current != null)
|
||||||
{
|
{
|
||||||
current.IsSelected = true;
|
current.IsSelected = true;
|
||||||
dbSet.Update(current);
|
dbSet.UpdateAndSave(current);
|
||||||
dbContext.SaveChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
messenger.Send(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 数据库当前项
|
|
||||||
/// 简化对数据库中选中项的管理
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TObservable">绑定类型</typeparam>
|
|
||||||
/// <typeparam name="TEntity">实体的类型</typeparam>
|
|
||||||
/// <typeparam name="TMessage">消息的类型</typeparam>
|
|
||||||
[SuppressMessage("", "SA1402")]
|
|
||||||
internal class DbCurrent<TObservable, TEntity, TMessage>
|
|
||||||
where TObservable : class
|
|
||||||
where TEntity : class, ISelectable
|
|
||||||
where TMessage : Message.ValueChangedMessage<TObservable>
|
|
||||||
{
|
|
||||||
private readonly DbContext dbContext;
|
|
||||||
private readonly DbSet<TEntity> dbSet;
|
|
||||||
private readonly IMessenger messenger;
|
|
||||||
private readonly Func<TObservable, TEntity> selector;
|
|
||||||
|
|
||||||
private TObservable? current;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 构造一个新的数据库当前项
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dbContext">数据库上下文</param>
|
|
||||||
/// <param name="dbSet">数据集</param>
|
|
||||||
/// <param name="selector">选择器</param>
|
|
||||||
/// <param name="messenger">消息器</param>
|
|
||||||
public DbCurrent(DbContext dbContext, DbSet<TEntity> dbSet, Func<TObservable, TEntity> selector, IMessenger messenger)
|
|
||||||
{
|
|
||||||
this.dbContext = dbContext;
|
|
||||||
this.dbSet = dbSet;
|
|
||||||
this.selector = selector;
|
|
||||||
this.messenger = messenger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 当前选中的项
|
|
||||||
/// </summary>
|
|
||||||
public TObservable? Current
|
|
||||||
{
|
|
||||||
get => current;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
// prevent useless sets
|
|
||||||
if (current == value)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// only update when not processing a deletion
|
|
||||||
if (value != null)
|
|
||||||
{
|
|
||||||
if (current != null)
|
|
||||||
{
|
|
||||||
TEntity entity = selector(current);
|
|
||||||
entity.IsSelected = false;
|
|
||||||
dbSet.Update(entity);
|
|
||||||
dbContext.SaveChanges();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TMessage message = (TMessage)Activator.CreateInstance(typeof(TMessage), current, value)!;
|
|
||||||
current = value;
|
|
||||||
|
|
||||||
if (current != null)
|
|
||||||
{
|
|
||||||
TEntity entity = selector(current);
|
|
||||||
entity.IsSelected = true;
|
|
||||||
dbSet.Update(entity);
|
|
||||||
dbContext.SaveChanges();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
messenger.Send(message);
|
messenger.Send(message);
|
||||||
|
|||||||
66
src/Snap.Hutao/Snap.Hutao/Extension/DbSetExtension.cs
Normal file
66
src/Snap.Hutao/Snap.Hutao/Extension/DbSetExtension.cs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Extension;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据库集合上下文
|
||||||
|
/// </summary>
|
||||||
|
public static class DbSetExtension
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取对应的数据库上下文
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||||
|
/// <param name="dbSet">数据库集</param>
|
||||||
|
/// <returns>对应的数据库上下文</returns>
|
||||||
|
public static DbContext Context<TEntity>(this DbSet<TEntity> dbSet)
|
||||||
|
where TEntity : class
|
||||||
|
{
|
||||||
|
return dbSet.GetService<ICurrentDbContext>().Context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或添加一个对应的实体
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||||
|
/// <param name="dbSet">数据库集</param>
|
||||||
|
/// <param name="predicate">谓词</param>
|
||||||
|
/// <param name="entityFactory">实体工厂</param>
|
||||||
|
/// <param name="added">是否添加</param>
|
||||||
|
/// <returns>实体</returns>
|
||||||
|
public static TEntity SingleOrAdd<TEntity>(this DbSet<TEntity> dbSet, Func<TEntity, bool> predicate, Func<TEntity> entityFactory, out bool added)
|
||||||
|
where TEntity : class
|
||||||
|
{
|
||||||
|
added = false;
|
||||||
|
TEntity? entry = dbSet.SingleOrDefault(predicate);
|
||||||
|
|
||||||
|
if (entry == null)
|
||||||
|
{
|
||||||
|
entry = entityFactory();
|
||||||
|
dbSet.Add(entry);
|
||||||
|
dbSet.Context().SaveChanges();
|
||||||
|
|
||||||
|
added = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新并保存
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||||
|
/// <param name="dbSet">数据库集</param>
|
||||||
|
/// <param name="entity">实体</param>
|
||||||
|
/// <returns>影响条数</returns>
|
||||||
|
public static int UpdateAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||||
|
where TEntity : class
|
||||||
|
{
|
||||||
|
dbSet.Update(entity);
|
||||||
|
return dbSet.Context().SaveChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,16 +8,7 @@ namespace Snap.Hutao.Message;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 成就存档切换消息
|
/// 成就存档切换消息
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
|
||||||
internal class AchievementArchiveChangedMessage : ValueChangedMessage<AchievementArchive>
|
internal class AchievementArchiveChangedMessage : ValueChangedMessage<AchievementArchive>
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// 构造一个新的用户切换消息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="oldArchive">老用户</param>
|
|
||||||
/// <param name="newArchive">新用户</param>
|
|
||||||
public AchievementArchiveChangedMessage(AchievementArchive? oldArchive, AchievementArchive? newArchive)
|
|
||||||
: base(oldArchive, newArchive)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -8,16 +8,7 @@ namespace Snap.Hutao.Message;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 祈愿记录存档切换消息
|
/// 祈愿记录存档切换消息
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
|
||||||
internal class GachaArchiveChangedMessage : ValueChangedMessage<GachaArchive>
|
internal class GachaArchiveChangedMessage : ValueChangedMessage<GachaArchive>
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// 构造一个新的用户切换消息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="oldArchive">老用户</param>
|
|
||||||
/// <param name="newArchive">新用户</param>
|
|
||||||
public GachaArchiveChangedMessage(GachaArchive? oldArchive, GachaArchive? newArchive)
|
|
||||||
: base(oldArchive, newArchive)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -8,15 +8,7 @@ namespace Snap.Hutao.Message;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用户切换消息
|
/// 用户切换消息
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
|
||||||
internal class UserChangedMessage : ValueChangedMessage<User>
|
internal class UserChangedMessage : ValueChangedMessage<User>
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// 构造一个新的用户切换消息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="oldUser">老用户</param>
|
|
||||||
/// <param name="newUser">新用户</param>
|
|
||||||
public UserChangedMessage(User? oldUser, User? newUser)
|
|
||||||
: base(oldUser, newUser)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -7,9 +7,17 @@ namespace Snap.Hutao.Message;
|
|||||||
/// 值变化消息
|
/// 值变化消息
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TValue">值的类型</typeparam>
|
/// <typeparam name="TValue">值的类型</typeparam>
|
||||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
|
||||||
internal abstract class ValueChangedMessage<TValue>
|
internal abstract class ValueChangedMessage<TValue>
|
||||||
where TValue : class
|
where TValue : class
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 动态访问
|
||||||
|
/// </summary>
|
||||||
|
public ValueChangedMessage()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 构造一个新的值变化消息
|
/// 构造一个新的值变化消息
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -24,10 +32,10 @@ internal abstract class ValueChangedMessage<TValue>
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 旧的值
|
/// 旧的值
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TValue? OldValue { get; private set; }
|
public TValue? OldValue { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 新的值
|
/// 新的值
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TValue? NewValue { get; private set; }
|
public TValue? NewValue { get; set; }
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ public class GachaStatistics
|
|||||||
public TypedWishSummary PermanentWish { get; set; } = default!;
|
public TypedWishSummary PermanentWish { get; set; } = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 历史
|
/// 历史卡池
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<HistoryWish> HistoryWishes { get; set; } = default!;
|
public List<HistoryWish> HistoryWishes { get; set; } = default!;
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ namespace Snap.Hutao.Model.Entity;
|
|||||||
[Table("settings")]
|
[Table("settings")]
|
||||||
public class SettingEntry
|
public class SettingEntry
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 游戏路径
|
||||||
|
/// </summary>
|
||||||
|
public const string GamePath = "GamePath";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 空的历史记录卡池是否可见
|
||||||
|
/// </summary>
|
||||||
|
public const string IsEmptyHistoryWishVisible = "IsEmptyHistoryWishVisible";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 构造一个新的设置入口
|
/// 构造一个新的设置入口
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ internal class SkillIconConverter : ValueConverterBase<string, Uri>
|
|||||||
private const string SkillUrl = "https://static.snapgenshin.com/Skill/{0}.png";
|
private const string SkillUrl = "https://static.snapgenshin.com/Skill/{0}.png";
|
||||||
private const string TalentUrl = "https://static.snapgenshin.com/Talent/{0}.png";
|
private const string TalentUrl = "https://static.snapgenshin.com/Talent/{0}.png";
|
||||||
|
|
||||||
|
private static readonly Uri UIIconNone = new("https://static.snapgenshin.com/Bg/UI_Icon_None.png");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 名称转Uri
|
/// 名称转Uri
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -20,6 +22,11 @@ internal class SkillIconConverter : ValueConverterBase<string, Uri>
|
|||||||
/// <returns>链接</returns>
|
/// <returns>链接</returns>
|
||||||
public static Uri IconNameToUri(string name)
|
public static Uri IconNameToUri(string name)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
return UIIconNone;
|
||||||
|
}
|
||||||
|
|
||||||
if (name.StartsWith("UI_Talent_"))
|
if (name.StartsWith("UI_Talent_"))
|
||||||
{
|
{
|
||||||
return new Uri(string.Format(TalentUrl, name));
|
return new Uri(string.Format(TalentUrl, name));
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ internal class AchievementService : IAchievementService
|
|||||||
this.appDbContext = appDbContext;
|
this.appDbContext = appDbContext;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
|
|
||||||
dbCurrent = new(appDbContext, appDbContext.AchievementArchives, messenger);
|
dbCurrent = new(appDbContext.AchievementArchives, messenger);
|
||||||
achievementDbOperation = new(appDbContext);
|
achievementDbOperation = new(appDbContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.AppCenter.Model.Log;
|
namespace Snap.Hutao.Service.AppCenter.Model.Log;
|
||||||
|
|
||||||
[SuppressMessage("", "SA1600")]
|
[SuppressMessage("", "SA1600")]
|
||||||
@@ -21,7 +23,7 @@ public static class LogHelper
|
|||||||
{
|
{
|
||||||
Type = exception.GetType().ToString(),
|
Type = exception.GetType().ToString(),
|
||||||
Message = exception.Message,
|
Message = exception.Message,
|
||||||
StackTrace = exception.ToString(),
|
StackTrace = exception.StackTrace,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (exception is AggregateException aggregateException)
|
if (exception is AggregateException aggregateException)
|
||||||
@@ -35,12 +37,21 @@ public static class LogHelper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (exception.InnerException != null)
|
|
||||||
|
if (exception.InnerException != null)
|
||||||
{
|
{
|
||||||
current.InnerExceptions ??= new();
|
current.InnerExceptions ??= new();
|
||||||
current.InnerExceptions.Add(Create(exception.InnerException));
|
current.InnerExceptions.Add(Create(exception.InnerException));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StackTrace stackTrace = new(exception, true);
|
||||||
|
StackFrame[] frames = stackTrace.GetFrames();
|
||||||
|
|
||||||
|
if (frames.Length > 0 && frames[0].HasNativeImage())
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Snap.Hutao.Context.Database;
|
||||||
using Snap.Hutao.Extension;
|
using Snap.Hutao.Extension;
|
||||||
using Snap.Hutao.Model.Binding.Gacha;
|
using Snap.Hutao.Model.Binding.Gacha;
|
||||||
using Snap.Hutao.Model.Entity;
|
using Snap.Hutao.Model.Entity;
|
||||||
@@ -19,14 +21,17 @@ namespace Snap.Hutao.Service.GachaLog.Factory;
|
|||||||
internal class GachaStatisticsFactory : IGachaStatisticsFactory
|
internal class GachaStatisticsFactory : IGachaStatisticsFactory
|
||||||
{
|
{
|
||||||
private readonly IMetadataService metadataService;
|
private readonly IMetadataService metadataService;
|
||||||
|
private readonly AppDbContext appDbContext;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 构造一个新的祈愿统计工厂
|
/// 构造一个新的祈愿统计工厂
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="metadataService">元数据服务</param>
|
/// <param name="metadataService">元数据服务</param>
|
||||||
public GachaStatisticsFactory(IMetadataService metadataService)
|
/// <param name="appDbContext">数据库上下文</param>
|
||||||
|
public GachaStatisticsFactory(IMetadataService metadataService, AppDbContext appDbContext)
|
||||||
{
|
{
|
||||||
this.metadataService = metadataService;
|
this.metadataService = metadataService;
|
||||||
|
this.appDbContext = appDbContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -41,15 +46,29 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
|
|||||||
List<GachaEvent> gachaevents = await metadataService.GetGachaEventsAsync().ConfigureAwait(false);
|
List<GachaEvent> gachaevents = await metadataService.GetGachaEventsAsync().ConfigureAwait(false);
|
||||||
List<HistoryWishBuilder> historyWishBuilders = gachaevents.Select(g => new HistoryWishBuilder(g, nameAvatarMap, nameWeaponMap)).ToList();
|
List<HistoryWishBuilder> historyWishBuilders = gachaevents.Select(g => new HistoryWishBuilder(g, nameAvatarMap, nameWeaponMap)).ToList();
|
||||||
|
|
||||||
|
SettingEntry? entry = await appDbContext.Settings
|
||||||
|
.SingleOrDefaultAsync(e => e.Key == SettingEntry.IsEmptyHistoryWishVisible)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (entry == null)
|
||||||
|
{
|
||||||
|
entry = new(SettingEntry.IsEmptyHistoryWishVisible, true.ToString());
|
||||||
|
appDbContext.Settings.Add(entry);
|
||||||
|
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isEmptyHistoryWishVisible = bool.Parse(entry.Value!);
|
||||||
|
|
||||||
IOrderedEnumerable<GachaItem> orderedItems = items.OrderBy(i => i.Id);
|
IOrderedEnumerable<GachaItem> orderedItems = items.OrderBy(i => i.Id);
|
||||||
return await Task.Run(() => CreateCore(orderedItems, historyWishBuilders, idAvatarMap, idWeaponMap)).ConfigureAwait(false);
|
return await Task.Run(() => CreateCore(orderedItems, historyWishBuilders, idAvatarMap, idWeaponMap, isEmptyHistoryWishVisible)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GachaStatistics CreateCore(
|
private static GachaStatistics CreateCore(
|
||||||
IOrderedEnumerable<GachaItem> items,
|
IOrderedEnumerable<GachaItem> items,
|
||||||
List<HistoryWishBuilder> historyWishBuilders,
|
List<HistoryWishBuilder> historyWishBuilders,
|
||||||
Dictionary<int, Avatar> avatarMap,
|
Dictionary<int, Avatar> avatarMap,
|
||||||
Dictionary<int, Weapon> weaponMap)
|
Dictionary<int, Weapon> weaponMap,
|
||||||
|
bool isEmptyHistoryWishVisible)
|
||||||
{
|
{
|
||||||
TypedWishSummaryBuilder permanentWishBuilder = new("奔行世间", TypedWishSummaryBuilder.PermanentWish, 90, 10);
|
TypedWishSummaryBuilder permanentWishBuilder = new("奔行世间", TypedWishSummaryBuilder.PermanentWish, 90, 10);
|
||||||
TypedWishSummaryBuilder avatarWishBuilder = new("角色活动", TypedWishSummaryBuilder.AvatarEventWish, 90, 10);
|
TypedWishSummaryBuilder avatarWishBuilder = new("角色活动", TypedWishSummaryBuilder.AvatarEventWish, 90, 10);
|
||||||
@@ -131,6 +150,7 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
|
|||||||
{
|
{
|
||||||
// history
|
// history
|
||||||
HistoryWishes = historyWishBuilders
|
HistoryWishes = historyWishBuilders
|
||||||
|
.Where(b => isEmptyHistoryWishVisible || (!b.IsEmpty))
|
||||||
.OrderByDescending(builder => builder.From)
|
.OrderByDescending(builder => builder.From)
|
||||||
.ThenBy(builder => builder.ConfigType, new GachaConfigTypeComparar())
|
.ThenBy(builder => builder.ConfigType, new GachaConfigTypeComparar())
|
||||||
.Select(builder => builder.ToHistoryWish()).ToList(),
|
.Select(builder => builder.ToHistoryWish()).ToList(),
|
||||||
|
|||||||
@@ -67,6 +67,14 @@ internal class HistoryWishBuilder
|
|||||||
get => gachaEvent.To;
|
get => gachaEvent.To;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 卡池是否为空
|
||||||
|
/// </summary>
|
||||||
|
public bool IsEmpty
|
||||||
|
{
|
||||||
|
get => totalCountTracker <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 计数五星角色
|
/// 计数五星角色
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
|||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.gachaStatisticsFactory = gachaStatisticsFactory;
|
this.gachaStatisticsFactory = gachaStatisticsFactory;
|
||||||
|
|
||||||
dbCurrent = new(appDbContext, appDbContext.GachaArchives, messenger);
|
dbCurrent = new(appDbContext.GachaArchives, messenger);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Snap.Hutao.Context.Database;
|
using Snap.Hutao.Context.Database;
|
||||||
using Snap.Hutao.Core.Threading;
|
using Snap.Hutao.Core.Threading;
|
||||||
|
using Snap.Hutao.Extension;
|
||||||
using Snap.Hutao.Model.Entity;
|
using Snap.Hutao.Model.Entity;
|
||||||
using Snap.Hutao.Service.Game.Locator;
|
using Snap.Hutao.Service.Game.Locator;
|
||||||
|
|
||||||
@@ -16,8 +17,6 @@ namespace Snap.Hutao.Service.Game;
|
|||||||
[Injection(InjectAs.Transient, typeof(IGameService))]
|
[Injection(InjectAs.Transient, typeof(IGameService))]
|
||||||
internal class GameService : IGameService
|
internal class GameService : IGameService
|
||||||
{
|
{
|
||||||
private const string GamePath = "GamePath";
|
|
||||||
|
|
||||||
private readonly AppDbContext appDbContext;
|
private readonly AppDbContext appDbContext;
|
||||||
private readonly IMemoryCache memoryCache;
|
private readonly IMemoryCache memoryCache;
|
||||||
private readonly IEnumerable<IGameLocator> gameLocators;
|
private readonly IEnumerable<IGameLocator> gameLocators;
|
||||||
@@ -38,7 +37,7 @@ internal class GameService : IGameService
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async ValueTask<ValueResult<bool, string>> GetGamePathAsync()
|
public async ValueTask<ValueResult<bool, string>> GetGamePathAsync()
|
||||||
{
|
{
|
||||||
string key = $"{nameof(GameService)}.Cache.{GamePath}";
|
string key = $"{nameof(GameService)}.Cache.{SettingEntry.GamePath}";
|
||||||
|
|
||||||
if (memoryCache.TryGetValue(key, out object? value))
|
if (memoryCache.TryGetValue(key, out object? value))
|
||||||
{
|
{
|
||||||
@@ -46,16 +45,11 @@ internal class GameService : IGameService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SettingEntry? entry = await appDbContext.Settings
|
SettingEntry entry = appDbContext.Settings.SingleOrAdd(e => e.Key == SettingEntry.GamePath, () => new(SettingEntry.GamePath, null), out bool added);
|
||||||
.SingleOrDefaultAsync(e => e.Key == GamePath)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
// Cannot find in setting
|
// Cannot find in setting
|
||||||
if (entry == null)
|
if (added)
|
||||||
{
|
{
|
||||||
// Create new setting
|
|
||||||
entry = new(GamePath, null);
|
|
||||||
|
|
||||||
// Try locate by registry
|
// Try locate by registry
|
||||||
IGameLocator locator = gameLocators.Single(l => l.Name == nameof(RegistryLauncherLocator));
|
IGameLocator locator = gameLocators.Single(l => l.Name == nameof(RegistryLauncherLocator));
|
||||||
ValueResult<bool, string> result = await locator.LocateGamePathAsync().ConfigureAwait(false);
|
ValueResult<bool, string> result = await locator.LocateGamePathAsync().ConfigureAwait(false);
|
||||||
@@ -71,7 +65,7 @@ internal class GameService : IGameService
|
|||||||
{
|
{
|
||||||
// Save result.
|
// Save result.
|
||||||
entry.Value = result.Value;
|
entry.Value = result.Value;
|
||||||
await appDbContext.Settings.AddAsync(entry).ConfigureAwait(false);
|
appDbContext.Settings.Update(entry);
|
||||||
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
|
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ internal class RegistryLauncherLocator : IGameLocator
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
string path = result.Value;
|
string? path = Path.GetDirectoryName(result.Value);
|
||||||
string configPath = Path.Combine(path, "config.ini");
|
string configPath = Path.Combine(path!, "config.ini");
|
||||||
string? escapedPath = null;
|
string? escapedPath = null;
|
||||||
using (FileStream stream = File.OpenRead(configPath))
|
using (FileStream stream = File.OpenRead(configPath))
|
||||||
{
|
{
|
||||||
@@ -40,7 +40,8 @@ internal class RegistryLauncherLocator : IGameLocator
|
|||||||
|
|
||||||
if (escapedPath != null)
|
if (escapedPath != null)
|
||||||
{
|
{
|
||||||
return Task.FromResult<ValueResult<bool, string>>(new(true, Unescape(escapedPath)));
|
string gamePath = Path.Combine(Unescape(escapedPath), "YuanShen.exe");
|
||||||
|
return Task.FromResult<ValueResult<bool, string>>(new(true, gamePath));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
160
src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoCache.cs
Normal file
160
src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoCache.cs
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Model.Binding.Hutao;
|
||||||
|
using Snap.Hutao.Model.Metadata.Avatar;
|
||||||
|
using Snap.Hutao.Model.Metadata.Weapon;
|
||||||
|
using Snap.Hutao.Service.Metadata;
|
||||||
|
using Snap.Hutao.Web.Hutao.Model;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Hutao;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 胡桃 API 缓存
|
||||||
|
/// </summary>
|
||||||
|
[Injection(InjectAs.Singleton, typeof(IHtaoCache))]
|
||||||
|
internal class HutaoCache : IHtaoCache
|
||||||
|
{
|
||||||
|
private readonly IHutaoService hutaoService;
|
||||||
|
private readonly IMetadataService metadataService;
|
||||||
|
|
||||||
|
private Dictionary<int, Avatar>? idAvatarExtendedMap;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造一个新的胡桃 API 缓存
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hutaoService">胡桃服务</param>
|
||||||
|
/// <param name="metadataService">元数据服务</param>
|
||||||
|
public HutaoCache(IHutaoService hutaoService, IMetadataService metadataService)
|
||||||
|
{
|
||||||
|
this.hutaoService = hutaoService;
|
||||||
|
this.metadataService = metadataService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public List<ComplexAvatarRank>? AvatarUsageRanks { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public List<ComplexAvatarRank>? AvatarAppearanceRanks { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public List<ComplexAvatarConstellationInfo>? AvatarConstellationInfos { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public List<ComplexTeamRank>? TeamAppearances { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public Overview? Overview { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public List<ComplexAvatarCollocation>? AvatarCollocations { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async ValueTask<bool> InitializeForDatabaseViewModelAsync()
|
||||||
|
{
|
||||||
|
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
Dictionary<int, Avatar> idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
Task avatarAppearanceRankTask = AvatarAppearanceRankAsync(idAvatarMap);
|
||||||
|
Task avatarUsageRank = AvatarUsageRanksAsync(idAvatarMap);
|
||||||
|
Task avatarConstellationInfoTask = AvatarConstellationInfosAsync(idAvatarMap);
|
||||||
|
Task teamAppearanceTask = TeamAppearancesAsync(idAvatarMap);
|
||||||
|
Task ovewviewTask = OverviewAsync();
|
||||||
|
|
||||||
|
await Task.WhenAll(
|
||||||
|
avatarAppearanceRankTask,
|
||||||
|
avatarUsageRank,
|
||||||
|
avatarConstellationInfoTask,
|
||||||
|
teamAppearanceTask,
|
||||||
|
ovewviewTask)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async ValueTask<bool> InitializeForWikiAvatarViewModelAsync()
|
||||||
|
{
|
||||||
|
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
Dictionary<int, Avatar> idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
|
||||||
|
Dictionary<int, Weapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
|
||||||
|
Dictionary<int, Model.Metadata.Reliquary.ReliquarySet> idReliquarySetMap = await metadataService.GetEquipAffixIdToReliquarySetMapAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
// AvatarCollocation
|
||||||
|
List<AvatarCollocation> avatarCollocationsRaw = await hutaoService.GetAvatarCollocationsAsync().ConfigureAwait(false);
|
||||||
|
AvatarCollocations = avatarCollocationsRaw.Select(co =>
|
||||||
|
{
|
||||||
|
return new ComplexAvatarCollocation(idAvatarMap[co.AvatarId])
|
||||||
|
{
|
||||||
|
Avatars = co.Avatars.Select(a => new ComplexAvatar(idAvatarMap[a.Item], a.Rate)).ToList(),
|
||||||
|
Weapons = co.Weapons.Select(w => new ComplexWeapon(idWeaponMap[w.Item], w.Rate)).ToList(),
|
||||||
|
ReliquarySets = co.Reliquaries.Select(r => new ComplexReliquarySet(r, idReliquarySetMap)).ToList(),
|
||||||
|
};
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<Dictionary<int, Avatar>> GetIdAvatarMapExtendedAsync()
|
||||||
|
{
|
||||||
|
if (idAvatarExtendedMap == null)
|
||||||
|
{
|
||||||
|
Dictionary<int, Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
|
||||||
|
idAvatarExtendedMap = new(idAvatarMap)
|
||||||
|
{
|
||||||
|
[10000005] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerBoy", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
|
||||||
|
[10000007] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerGirl", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return idAvatarExtendedMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AvatarAppearanceRankAsync(Dictionary<int, Avatar> idAvatarMap)
|
||||||
|
{
|
||||||
|
List<AvatarAppearanceRank> avatarAppearanceRanksRaw = await hutaoService.GetAvatarAppearanceRanksAsync().ConfigureAwait(false);
|
||||||
|
AvatarAppearanceRanks = avatarAppearanceRanksRaw.OrderByDescending(r => r.Floor).Select(rank => new ComplexAvatarRank
|
||||||
|
{
|
||||||
|
Floor = $"第 {rank.Floor} 层",
|
||||||
|
Avatars = rank.Ranks.OrderByDescending(r => r.Rate).Select(rank => new ComplexAvatar(idAvatarMap[rank.Item], rank.Rate)).ToList(),
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AvatarUsageRanksAsync(Dictionary<int, Avatar> idAvatarMap)
|
||||||
|
{
|
||||||
|
List<AvatarUsageRank> avatarUsageRanksRaw = await hutaoService.GetAvatarUsageRanksAsync().ConfigureAwait(false);
|
||||||
|
AvatarUsageRanks = avatarUsageRanksRaw.OrderByDescending(r => r.Floor).Select(rank => new ComplexAvatarRank
|
||||||
|
{
|
||||||
|
Floor = $"第 {rank.Floor} 层",
|
||||||
|
Avatars = rank.Ranks.OrderByDescending(r => r.Rate).Select(rank => new ComplexAvatar(idAvatarMap[rank.Item], rank.Rate)).ToList(),
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AvatarConstellationInfosAsync(Dictionary<int, Avatar> idAvatarMap)
|
||||||
|
{
|
||||||
|
List<AvatarConstellationInfo> avatarConstellationInfosRaw = await hutaoService.GetAvatarConstellationInfosAsync().ConfigureAwait(false);
|
||||||
|
AvatarConstellationInfos = avatarConstellationInfosRaw.OrderBy(i => i.HoldingRate).Select(info =>
|
||||||
|
{
|
||||||
|
return new ComplexAvatarConstellationInfo(idAvatarMap[info.AvatarId], info.HoldingRate, info.Constellations.Select(x => x.Rate));
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TeamAppearancesAsync(Dictionary<int, Avatar> idAvatarMap)
|
||||||
|
{
|
||||||
|
List<TeamAppearance> teamAppearancesRaw = await hutaoService.GetTeamAppearancesAsync().ConfigureAwait(false);
|
||||||
|
TeamAppearances = teamAppearancesRaw.OrderByDescending(t => t.Floor).Select(team => new ComplexTeamRank(team, idAvatarMap)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OverviewAsync()
|
||||||
|
{
|
||||||
|
Overview = await hutaoService.GetOverviewAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,10 @@
|
|||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Snap.Hutao.Service.Abstraction;
|
|
||||||
using Snap.Hutao.Web.Hutao;
|
using Snap.Hutao.Web.Hutao;
|
||||||
using Snap.Hutao.Web.Hutao.Model;
|
using Snap.Hutao.Web.Hutao.Model;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service;
|
namespace Snap.Hutao.Service.Hutao;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 胡桃 API 服务
|
/// 胡桃 API 服务
|
||||||
55
src/Snap.Hutao/Snap.Hutao/Service/Hutao/IHtaoCache.cs
Normal file
55
src/Snap.Hutao/Snap.Hutao/Service/Hutao/IHtaoCache.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Model.Binding.Hutao;
|
||||||
|
using Snap.Hutao.Web.Hutao.Model;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Hutao;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 胡桃 API 缓存
|
||||||
|
/// </summary>
|
||||||
|
internal interface IHtaoCache
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 角色使用率
|
||||||
|
/// </summary>
|
||||||
|
List<ComplexAvatarRank>? AvatarUsageRanks { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色上场率
|
||||||
|
/// </summary>
|
||||||
|
List<ComplexAvatarRank>? AvatarAppearanceRanks { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色命座信息
|
||||||
|
/// </summary>
|
||||||
|
List<ComplexAvatarConstellationInfo>? AvatarConstellationInfos { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 队伍出场
|
||||||
|
/// </summary>
|
||||||
|
List<ComplexTeamRank>? TeamAppearances { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总览数据
|
||||||
|
/// </summary>
|
||||||
|
Overview? Overview { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色搭配
|
||||||
|
/// </summary>
|
||||||
|
List<ComplexAvatarCollocation>? AvatarCollocations { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为数据库视图模型初始化
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>任务</returns>
|
||||||
|
ValueTask<bool> InitializeForDatabaseViewModelAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为Wiki角色视图模型初始化
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>任务</returns>
|
||||||
|
ValueTask<bool> InitializeForWikiAvatarViewModelAsync();
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
using Snap.Hutao.Web.Hutao.Model;
|
using Snap.Hutao.Web.Hutao.Model;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Abstraction;
|
namespace Snap.Hutao.Service.Hutao;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 胡桃 API 服务
|
/// 胡桃 API 服务
|
||||||
@@ -73,7 +73,7 @@ internal class UserService : IUserService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Message.UserChangedMessage message = new(currentUser, value);
|
Message.UserChangedMessage message = new() { OldValue = currentUser, NewValue = value };
|
||||||
|
|
||||||
// 当删除到无用户时也能正常反应状态
|
// 当删除到无用户时也能正常反应状态
|
||||||
currentUser = value;
|
currentUser = value;
|
||||||
|
|||||||
@@ -28,11 +28,7 @@
|
|||||||
<StartupObject>Snap.Hutao.Program</StartupObject>
|
<StartupObject>Snap.Hutao.Program</StartupObject>
|
||||||
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN;DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION;DISABLE_XAML_GENERATED_BINDING_DEBUG_OUTPUT</DefineConstants>
|
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN;DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION;DISABLE_XAML_GENERATED_BINDING_DEBUG_OUTPUT</DefineConstants>
|
||||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
<DebugSymbols>true</DebugSymbols>
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
|
||||||
<DebugType>embedded</DebugType>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
|
||||||
<DebugType>embedded</DebugType>
|
<DebugType>embedded</DebugType>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,17 @@
|
|||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Pivot>
|
<Pivot>
|
||||||
|
<Pivot.RightHeader>
|
||||||
|
<CommandBar>
|
||||||
|
<AppBarButton>
|
||||||
|
<AppBarButton.Flyout>
|
||||||
|
<Flyout>
|
||||||
|
|
||||||
|
</Flyout>
|
||||||
|
</AppBarButton.Flyout>
|
||||||
|
</AppBarButton>
|
||||||
|
</CommandBar>
|
||||||
|
</Pivot.RightHeader>
|
||||||
<PivotItem Header="角色使用">
|
<PivotItem Header="角色使用">
|
||||||
<Pivot ItemsSource="{Binding AvatarUsageRanks}">
|
<Pivot ItemsSource="{Binding AvatarUsageRanks}">
|
||||||
<Pivot.HeaderTemplate>
|
<Pivot.HeaderTemplate>
|
||||||
@@ -38,7 +49,7 @@
|
|||||||
<GridView
|
<GridView
|
||||||
SelectionMode="None"
|
SelectionMode="None"
|
||||||
ItemsSource="{Binding Avatars}"
|
ItemsSource="{Binding Avatars}"
|
||||||
Margin="12,12,0,0">
|
Margin="12,12,0,-12">
|
||||||
<GridView.ItemContainerStyle>
|
<GridView.ItemContainerStyle>
|
||||||
<Style TargetType="GridViewItem" BasedOn="{StaticResource DefaultGridViewItemStyle}">
|
<Style TargetType="GridViewItem" BasedOn="{StaticResource DefaultGridViewItemStyle}">
|
||||||
<Setter Property="Margin" Value="0,0,12,12"/>
|
<Setter Property="Margin" Value="0,0,12,12"/>
|
||||||
@@ -78,7 +89,7 @@
|
|||||||
<GridView
|
<GridView
|
||||||
SelectionMode="None"
|
SelectionMode="None"
|
||||||
ItemsSource="{Binding Avatars}"
|
ItemsSource="{Binding Avatars}"
|
||||||
Margin="12,12,0,0">
|
Margin="12,12,0,-12">
|
||||||
<GridView.ItemContainerStyle>
|
<GridView.ItemContainerStyle>
|
||||||
<Style TargetType="GridViewItem" BasedOn="{StaticResource DefaultGridViewItemStyle}">
|
<Style TargetType="GridViewItem" BasedOn="{StaticResource DefaultGridViewItemStyle}">
|
||||||
<Setter Property="Margin" Value="0,0,12,12"/>
|
<Setter Property="Margin" Value="0,0,12,12"/>
|
||||||
@@ -208,7 +219,7 @@
|
|||||||
<ColumnDefinition Width="auto"/>
|
<ColumnDefinition Width="auto"/>
|
||||||
<ColumnDefinition/>
|
<ColumnDefinition/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<ScrollViewer Grid.Column="0">
|
<ScrollViewer Grid.Column="0" Margin="0,12,0,0">
|
||||||
<ItemsControl ItemsSource="{Binding Up}">
|
<ItemsControl ItemsSource="{Binding Up}">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
@@ -219,7 +230,7 @@
|
|||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Border
|
<Border
|
||||||
CornerRadius="{StaticResource CompatCornerRadius}"
|
CornerRadius="{StaticResource CompatCornerRadius}"
|
||||||
Margin="12,12,12,0"
|
Margin="12,0,12,12"
|
||||||
Background="{StaticResource CardBackgroundFillColorDefault}">
|
Background="{StaticResource CardBackgroundFillColorDefault}">
|
||||||
<Grid Margin="6">
|
<Grid Margin="6">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
@@ -254,7 +265,7 @@
|
|||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
<ScrollViewer Grid.Column="1">
|
<ScrollViewer Grid.Column="1" Margin="0,12,0,0">
|
||||||
<ItemsControl ItemsSource="{Binding Down}">
|
<ItemsControl ItemsSource="{Binding Down}">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
@@ -265,7 +276,7 @@
|
|||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Border
|
<Border
|
||||||
CornerRadius="{StaticResource CompatCornerRadius}"
|
CornerRadius="{StaticResource CompatCornerRadius}"
|
||||||
Margin="12,12,12,0"
|
Margin="12,0,12,12"
|
||||||
Background="{StaticResource CardBackgroundFillColorDefault}">
|
Background="{StaticResource CardBackgroundFillColorDefault}">
|
||||||
<Grid Margin="6">
|
<Grid Margin="6">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
|
|||||||
@@ -73,10 +73,28 @@
|
|||||||
Severity="Informational"
|
Severity="Informational"
|
||||||
Message="都说了没有了"
|
Message="都说了没有了"
|
||||||
IsOpen="True"
|
IsOpen="True"
|
||||||
CornerRadius="0,0,4,4"/>
|
CornerRadius="0,0,4,4">
|
||||||
|
<InfoBar.ActionButton>
|
||||||
|
<Button HorizontalAlignment="Right" Width="1" Content="没用的按钮"/>
|
||||||
|
</InfoBar.ActionButton>
|
||||||
|
</InfoBar>
|
||||||
</sc:SettingExpander>
|
</sc:SettingExpander>
|
||||||
</sc:SettingsGroup>
|
</sc:SettingsGroup>
|
||||||
|
|
||||||
|
<sc:SettingsGroup Header="祈愿记录">
|
||||||
|
<sc:Setting
|
||||||
|
Icon=""
|
||||||
|
Header="无记录的历史祈愿活动"
|
||||||
|
Description="在祈愿记录页面显示或隐藏无记录的历史祈愿活动">
|
||||||
|
<ToggleSwitch
|
||||||
|
Style="{StaticResource ToggleSwitchSettingStyle}"
|
||||||
|
OnContent="显示"
|
||||||
|
OffContent="隐藏"
|
||||||
|
IsOn="{Binding IsEmptyHistoryWishVisible,Mode=TwoWay}"/>
|
||||||
|
</sc:Setting>
|
||||||
|
|
||||||
|
</sc:SettingsGroup>
|
||||||
|
|
||||||
<sc:SettingsGroup Header="测试功能">
|
<sc:SettingsGroup Header="测试功能">
|
||||||
<sc:Setting
|
<sc:Setting
|
||||||
Icon=""
|
Icon=""
|
||||||
|
|||||||
@@ -215,7 +215,7 @@
|
|||||||
Source="{Binding Selected,Converter={StaticResource AvatarNameCardPicConverter}}"/>
|
Source="{Binding Selected,Converter={StaticResource AvatarNameCardPicConverter}}"/>
|
||||||
|
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel Margin="0,0,20,16">
|
<StackPanel Margin="0,0,20,16" MaxWidth="800" HorizontalAlignment="Left">
|
||||||
<!--简介-->
|
<!--简介-->
|
||||||
<Grid
|
<Grid
|
||||||
Margin="16,16,0,16"
|
Margin="16,16,0,16"
|
||||||
|
|||||||
@@ -3,14 +3,9 @@
|
|||||||
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using Snap.Hutao.Control;
|
using Snap.Hutao.Control;
|
||||||
using Snap.Hutao.Core.Threading;
|
|
||||||
using Snap.Hutao.Factory.Abstraction;
|
using Snap.Hutao.Factory.Abstraction;
|
||||||
using Snap.Hutao.Model.Binding.Hutao;
|
using Snap.Hutao.Model.Binding.Hutao;
|
||||||
using Snap.Hutao.Model.Metadata.Avatar;
|
using Snap.Hutao.Service.Hutao;
|
||||||
using Snap.Hutao.Model.Metadata.Weapon;
|
|
||||||
using Snap.Hutao.Service.Abstraction;
|
|
||||||
using Snap.Hutao.Service.Metadata;
|
|
||||||
using Snap.Hutao.Web.Hutao.Model;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.ViewModel;
|
namespace Snap.Hutao.ViewModel;
|
||||||
|
|
||||||
@@ -20,8 +15,7 @@ namespace Snap.Hutao.ViewModel;
|
|||||||
[Injection(InjectAs.Transient)]
|
[Injection(InjectAs.Transient)]
|
||||||
internal class HutaoDatabaseViewModel : ObservableObject, ISupportCancellation
|
internal class HutaoDatabaseViewModel : ObservableObject, ISupportCancellation
|
||||||
{
|
{
|
||||||
private readonly IHutaoService hutaoService;
|
private readonly IHtaoCache hutaoCache;
|
||||||
private readonly IMetadataService metadataService;
|
|
||||||
|
|
||||||
private List<ComplexAvatarRank>? avatarUsageRanks;
|
private List<ComplexAvatarRank>? avatarUsageRanks;
|
||||||
private List<ComplexAvatarRank>? avatarAppearanceRanks;
|
private List<ComplexAvatarRank>? avatarAppearanceRanks;
|
||||||
@@ -31,13 +25,12 @@ internal class HutaoDatabaseViewModel : ObservableObject, ISupportCancellation
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 构造一个新的胡桃数据库视图模型
|
/// 构造一个新的胡桃数据库视图模型
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="hutaoService">胡桃服务</param>
|
/// <param name="hutaoCache">胡桃服务缓存</param>
|
||||||
/// <param name="metadataService">元数据服务</param>
|
/// <param name="metadataService">元数据服务</param>
|
||||||
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
|
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
|
||||||
public HutaoDatabaseViewModel(IHutaoService hutaoService, IMetadataService metadataService, IAsyncRelayCommandFactory asyncRelayCommandFactory)
|
public HutaoDatabaseViewModel(IHtaoCache hutaoCache, IAsyncRelayCommandFactory asyncRelayCommandFactory)
|
||||||
{
|
{
|
||||||
this.hutaoService = hutaoService;
|
this.hutaoCache = hutaoCache;
|
||||||
this.metadataService = metadataService;
|
|
||||||
|
|
||||||
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
|
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
|
||||||
}
|
}
|
||||||
@@ -72,80 +65,12 @@ internal class HutaoDatabaseViewModel : ObservableObject, ISupportCancellation
|
|||||||
|
|
||||||
private async Task OpenUIAsync()
|
private async Task OpenUIAsync()
|
||||||
{
|
{
|
||||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
if (await hutaoCache.InitializeForDatabaseViewModelAsync().ConfigureAwait(true))
|
||||||
{
|
{
|
||||||
Dictionary<int, Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
|
AvatarAppearanceRanks = hutaoCache.AvatarAppearanceRanks;
|
||||||
idAvatarMap = new(idAvatarMap)
|
AvatarUsageRanks = hutaoCache.AvatarUsageRanks;
|
||||||
{
|
AvatarConstellationInfos = hutaoCache.AvatarConstellationInfos;
|
||||||
[10000005] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerBoy", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
|
TeamAppearances = hutaoCache.TeamAppearances;
|
||||||
[10000007] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerGirl", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
|
|
||||||
};
|
|
||||||
|
|
||||||
Dictionary<int, Weapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
|
|
||||||
Dictionary<int, Model.Metadata.Reliquary.ReliquarySet> idReliquarySetMap = await metadataService.GetEquipAffixIdToReliquarySetMapAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
List<ComplexAvatarRank> avatarAppearanceRanksLocal = default!;
|
|
||||||
List<ComplexAvatarRank> avatarUsageRanksLocal = default!;
|
|
||||||
List<ComplexAvatarConstellationInfo> avatarConstellationInfosLocal = default!;
|
|
||||||
List<ComplexTeamRank> teamAppearancesLocal = default!;
|
|
||||||
|
|
||||||
Task avatarAppearanceRankTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
// AvatarAppearanceRank
|
|
||||||
List<AvatarAppearanceRank> avatarAppearanceRanksRaw = await hutaoService.GetAvatarAppearanceRanksAsync().ConfigureAwait(false);
|
|
||||||
avatarAppearanceRanksLocal = avatarAppearanceRanksRaw.OrderByDescending(r => r.Floor).Select(rank => new ComplexAvatarRank
|
|
||||||
{
|
|
||||||
Floor = $"第 {rank.Floor} 层",
|
|
||||||
Avatars = rank.Ranks.OrderByDescending(r => r.Rate).Select(rank => new ComplexAvatar(idAvatarMap[rank.Item], rank.Rate)).ToList(),
|
|
||||||
}).ToList();
|
|
||||||
});
|
|
||||||
|
|
||||||
Task avatarUsageRank = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
// AvatarUsageRank
|
|
||||||
List<AvatarUsageRank> avatarUsageRanksRaw = await hutaoService.GetAvatarUsageRanksAsync().ConfigureAwait(false);
|
|
||||||
avatarUsageRanksLocal = avatarUsageRanksRaw.OrderByDescending(r => r.Floor).Select(rank => new ComplexAvatarRank
|
|
||||||
{
|
|
||||||
Floor = $"第 {rank.Floor} 层",
|
|
||||||
Avatars = rank.Ranks.OrderByDescending(r => r.Rate).Select(rank => new ComplexAvatar(idAvatarMap[rank.Item], rank.Rate)).ToList(),
|
|
||||||
}).ToList();
|
|
||||||
});
|
|
||||||
|
|
||||||
Task avatarConstellationInfoTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
// AvatarConstellationInfo
|
|
||||||
List<AvatarConstellationInfo> avatarConstellationInfosRaw = await hutaoService.GetAvatarConstellationInfosAsync().ConfigureAwait(false);
|
|
||||||
avatarConstellationInfosLocal = avatarConstellationInfosRaw.OrderBy(i => i.HoldingRate).Select(info =>
|
|
||||||
{
|
|
||||||
return new ComplexAvatarConstellationInfo(idAvatarMap[info.AvatarId], info.HoldingRate, info.Constellations.Select(x => x.Rate));
|
|
||||||
}).ToList();
|
|
||||||
});
|
|
||||||
|
|
||||||
Task teamAppearanceTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
List<TeamAppearance> teamAppearancesRaw = await hutaoService.GetTeamAppearancesAsync().ConfigureAwait(false);
|
|
||||||
teamAppearancesLocal = teamAppearancesRaw.OrderByDescending(t => t.Floor).Select(team => new ComplexTeamRank(team, idAvatarMap)).ToList();
|
|
||||||
});
|
|
||||||
|
|
||||||
await Task.WhenAll(avatarAppearanceRankTask, avatarUsageRank, avatarConstellationInfoTask, teamAppearanceTask).ConfigureAwait(false);
|
|
||||||
|
|
||||||
await ThreadHelper.SwitchToMainThreadAsync();
|
|
||||||
AvatarAppearanceRanks = avatarAppearanceRanksLocal;
|
|
||||||
AvatarUsageRanks = avatarUsageRanksLocal;
|
|
||||||
AvatarConstellationInfos = avatarConstellationInfosLocal;
|
|
||||||
TeamAppearances = teamAppearancesLocal;
|
|
||||||
|
|
||||||
//// AvatarCollocation
|
|
||||||
//List<AvatarCollocation> avatarCollocationsRaw = await hutaoService.GetAvatarCollocationsAsync().ConfigureAwait(false);
|
|
||||||
//List<ComplexAvatarCollocation> avatarCollocationsLocal = avatarCollocationsRaw.Select(co =>
|
|
||||||
//{
|
|
||||||
// return new ComplexAvatarCollocation(idAvatarMap[co.AvatarId])
|
|
||||||
// {
|
|
||||||
// Avatars = co.Avatars.Select(a => new ComplexAvatar(idAvatarMap[a.Item], a.Rate)).ToList(),
|
|
||||||
// Weapons = co.Weapons.Select(w => new ComplexWeapon(idWeaponMap[w.Item], w.Rate)).ToList(),
|
|
||||||
// ReliquarySets = co.Reliquaries.Select(r => new ComplexReliquarySet(r, idReliquarySetMap)).ToList(),
|
|
||||||
// };
|
|
||||||
//}).ToList();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Snap.Hutao.Context.Database;
|
||||||
|
using Snap.Hutao.Extension;
|
||||||
|
using Snap.Hutao.Model.Entity;
|
||||||
|
|
||||||
namespace Snap.Hutao.ViewModel;
|
namespace Snap.Hutao.ViewModel;
|
||||||
|
|
||||||
@@ -11,13 +14,24 @@ namespace Snap.Hutao.ViewModel;
|
|||||||
[Injection(InjectAs.Transient)]
|
[Injection(InjectAs.Transient)]
|
||||||
internal class SettingViewModel : ObservableObject
|
internal class SettingViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
|
private readonly AppDbContext appDbContext;
|
||||||
|
private readonly SettingEntry isEmptyHistoryWishVisibleEntry;
|
||||||
|
|
||||||
|
private bool isEmptyHistoryWishVisible;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 构造一个新的测试视图模型
|
/// 构造一个新的测试视图模型
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="appDbContext">数据库上下文</param>
|
||||||
/// <param name="experimental">实验性功能</param>
|
/// <param name="experimental">实验性功能</param>
|
||||||
public SettingViewModel(ExperimentalFeaturesViewModel experimental)
|
public SettingViewModel(AppDbContext appDbContext, ExperimentalFeaturesViewModel experimental)
|
||||||
{
|
{
|
||||||
|
this.appDbContext = appDbContext;
|
||||||
Experimental = experimental;
|
Experimental = experimental;
|
||||||
|
|
||||||
|
isEmptyHistoryWishVisibleEntry = appDbContext.Settings
|
||||||
|
.SingleOrAdd(e => e.Key == SettingEntry.IsEmptyHistoryWishVisible, () => new(SettingEntry.IsEmptyHistoryWishVisible, true.ToString()), out _);
|
||||||
|
IsEmptyHistoryWishVisible = bool.Parse(isEmptyHistoryWishVisibleEntry.Value!);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -28,6 +42,20 @@ internal class SettingViewModel : ObservableObject
|
|||||||
get => Core.CoreEnvironment.Version.ToString();
|
get => Core.CoreEnvironment.Version.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 空的历史卡池是否可见
|
||||||
|
/// </summary>
|
||||||
|
public bool IsEmptyHistoryWishVisible
|
||||||
|
{
|
||||||
|
get => isEmptyHistoryWishVisible;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetProperty(ref isEmptyHistoryWishVisible, value);
|
||||||
|
isEmptyHistoryWishVisibleEntry.Value = value.ToString();
|
||||||
|
appDbContext.Settings.UpdateAndSave(isEmptyHistoryWishVisibleEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 实验性功能
|
/// 实验性功能
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -27,4 +27,19 @@ public class Overview
|
|||||||
/// 满星数
|
/// 满星数
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int SpiralAbyssFullStar { get; set; }
|
public int SpiralAbyssFullStar { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统计时间
|
||||||
|
/// </summary>
|
||||||
|
public long Timestamp { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总时间
|
||||||
|
/// </summary>
|
||||||
|
public double TimeTotal { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 平均时间
|
||||||
|
/// </summary>
|
||||||
|
public double TimeAverage { get; set; }
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user