mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-26 03:00:28 +08:00
feat(Statistic): introduce Statistic and TimerStatistic components with formatting, theming, and responsive layout support
This commit is contained in:
+1
@@ -20,6 +20,7 @@ namespace AtomUIGallery.Localization
|
||||
DataDisplay_List,
|
||||
DataDisplay_QRCode,
|
||||
DataDisplay_Segmented,
|
||||
DataDisplay_Statistic,
|
||||
DataDisplay_Tag,
|
||||
DataDisplay_Timeline,
|
||||
DataDisplay_Tour,
|
||||
|
||||
@@ -46,6 +46,7 @@ public sealed class ShowCaseViewModule : IViewModule
|
||||
locator.Map<GroupBoxViewModel, GroupBoxShowCase>(() => new GroupBoxShowCase());
|
||||
locator.Map<ResultViewModel, ResultShowCase>(() => new ResultShowCase());
|
||||
locator.Map<BadgeViewModel, BadgeShowCase>(() => new BadgeShowCase());
|
||||
locator.Map<StatisticViewModel, StatisticShowCase>(() => new StatisticShowCase());
|
||||
locator.Map<TimelineViewModel, TimelineShowCase>(() => new TimelineShowCase());
|
||||
locator.Map<QRCodeViewModel, QRCodeShowCase>(() => new QRCodeShowCase());
|
||||
locator.Map<LineEditViewModel, LineEditShowCase>(() => new LineEditShowCase());
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using AtomUI.Controls;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace AtomUIGallery.ShowCases.ViewModels;
|
||||
|
||||
public class StatisticViewModel : ReactiveObject, IRoutableViewModel
|
||||
{
|
||||
public static EntityKey ID = "Statistic";
|
||||
|
||||
public IScreen HostScreen { get; }
|
||||
|
||||
public string? UrlPathSegment => ID.ToString();
|
||||
|
||||
private double _values = 112893;
|
||||
|
||||
public double Values
|
||||
{
|
||||
get => _values;
|
||||
set => this.RaiseAndSetIfChanged(ref _values, value);
|
||||
}
|
||||
|
||||
private DateTime _deadline;
|
||||
|
||||
public DateTime Deadline
|
||||
{
|
||||
get => _deadline;
|
||||
set => this.RaiseAndSetIfChanged(ref _deadline, value);
|
||||
}
|
||||
|
||||
private DateTime _tenSecondsLater;
|
||||
|
||||
public DateTime TenSecondsLater
|
||||
{
|
||||
get => _tenSecondsLater;
|
||||
set => this.RaiseAndSetIfChanged(ref _tenSecondsLater, value);
|
||||
}
|
||||
|
||||
private DateTime _before;
|
||||
|
||||
public DateTime Before
|
||||
{
|
||||
get => _before;
|
||||
set => this.RaiseAndSetIfChanged(ref _before, value);
|
||||
}
|
||||
|
||||
public StatisticViewModel(IScreen screen)
|
||||
{
|
||||
HostScreen = screen;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:atom="https://atomui.net"
|
||||
xmlns:antdicons="https://atomui.net/icons/antdesign"
|
||||
xmlns:gallery="https://atomui.net/oss-controls/gallery"
|
||||
xmlns:vm="using:AtomUIGallery.ShowCases.ViewModels"
|
||||
x:Class="AtomUIGallery.ShowCases.Views.StatisticShowCase"
|
||||
x:DataType="vm:StatisticViewModel">
|
||||
<gallery:ShowCasePanel>
|
||||
<gallery:ShowCaseItem Title="Basic" Description="Simplest Usage." IsOccupyEntireRow="True">
|
||||
<UniformGrid Columns="2" Rows="2">
|
||||
<atom:Statistic Header="Active Users" Value="112893" />
|
||||
<StackPanel Orientation="Vertical" Spacing="16">
|
||||
<atom:Statistic Header="Account Balance (CNY)" Value="112893" Precision="2" />
|
||||
<atom:Button ButtonType="Primary">Recharge</atom:Button>
|
||||
</StackPanel>
|
||||
<atom:Statistic Header="Active Users" Value="112893" IsLoading="True" />
|
||||
</UniformGrid>
|
||||
</gallery:ShowCaseItem>
|
||||
|
||||
<gallery:ShowCaseItem Title="Unit"
|
||||
Description="Add unit through prefix and suffix."
|
||||
IsOccupyEntireRow="True">
|
||||
<UniformGrid Columns="2" Rows="1">
|
||||
<atom:Statistic Header="Feedback" Value="1128" ValuePrefixAddOn="{antdicons:AntDesignIconProvider LikeOutlined}" />
|
||||
<atom:Statistic Header="Unmerged" Value="93" ValueSuffixAddOn="/ 100" />
|
||||
</UniformGrid>
|
||||
</gallery:ShowCaseItem>
|
||||
|
||||
<gallery:ShowCaseItem Title="In Card"
|
||||
Description="Display statistic data in Card."
|
||||
IsOccupyEntireRow="True">
|
||||
<Border Background="#F0F2F5" Padding="20, 30">
|
||||
<UniformGrid Columns="2" Rows="1" ColumnSpacing="20">
|
||||
<atom:Card SizeType="Large">
|
||||
<atom:Statistic Header="Active"
|
||||
Value="11.28"
|
||||
Precision="2"
|
||||
ValuePrefixAddOn="{antdicons:AntDesignIconProvider ArrowUpOutlined}"
|
||||
ValueSuffixAddOn="%"
|
||||
ContentForeground="#3f8600" />
|
||||
</atom:Card>
|
||||
<atom:Card SizeType="Large">
|
||||
<atom:Statistic Header="Idle"
|
||||
Value="9.3"
|
||||
Precision="2"
|
||||
ValuePrefixAddOn="{antdicons:AntDesignIconProvider ArrowDownOutlined}"
|
||||
ValueSuffixAddOn="%"
|
||||
ContentForeground="#cf1322" />
|
||||
</atom:Card>
|
||||
</UniformGrid>
|
||||
</Border>
|
||||
</gallery:ShowCaseItem>
|
||||
|
||||
<gallery:ShowCaseItem Title="Animated number" Description="Animated number with StatisticCountUp." IsOccupyEntireRow="True">
|
||||
<UniformGrid Columns="2" Rows="2">
|
||||
<atom:Statistic Header="Active Users" Value="112893">
|
||||
<atom:Statistic.Content>
|
||||
<atom:StatisticCountUp EndValue="{Binding Values}" Precision="2" />
|
||||
</atom:Statistic.Content>
|
||||
</atom:Statistic>
|
||||
<atom:Statistic Header="Account Balance (CNY)" Value="112893">
|
||||
<atom:Statistic.Content>
|
||||
<atom:StatisticCountUp EndValue="{Binding Values}" Precision="2" />
|
||||
</atom:Statistic.Content>
|
||||
</atom:Statistic>
|
||||
</UniformGrid>
|
||||
</gallery:ShowCaseItem>
|
||||
|
||||
<gallery:ShowCaseItem Title="Timer" Description="Timer component." IsOccupyEntireRow="True">
|
||||
<UniformGrid Columns="2" Rows="3" RowSpacing="10">
|
||||
<atom:TimerStatistic Value="{Binding Deadline}" />
|
||||
<atom:TimerStatistic Header="Million Seconds" Value="{Binding Deadline}" Format="hh\:mm\:ss\.fff" />
|
||||
<atom:TimerStatistic Header="Countdown" Value="{Binding TenSecondsLater}" />
|
||||
<atom:TimerStatistic Header="Countup" Value="{Binding Before}" />
|
||||
<atom:TimerStatistic Header="Day Level (Countdown)" Value="{Binding Deadline}" Format="d\ \天\ h\ \时\ m\ \分\ s\ \秒" />
|
||||
<atom:TimerStatistic Header="Day Level (Countup)" Value="{Binding Before}" Format="d\ \天\ h\ \时\ m\ \分\ s\ \秒" />
|
||||
</UniformGrid>
|
||||
</gallery:ShowCaseItem>
|
||||
</gallery:ShowCasePanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,22 @@
|
||||
using AtomUIGallery.ShowCases.ViewModels;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.Avalonia;
|
||||
|
||||
namespace AtomUIGallery.ShowCases.Views;
|
||||
|
||||
public partial class StatisticShowCase : ReactiveUserControl<StatisticViewModel>
|
||||
{
|
||||
public StatisticShowCase()
|
||||
{
|
||||
this.WhenActivated(disposables =>
|
||||
{
|
||||
if (DataContext is StatisticViewModel viewModel)
|
||||
{
|
||||
viewModel.Deadline = DateTime.Now.Add(TimeSpan.FromSeconds(60 * 60 * 24 * 2 + 30));
|
||||
viewModel.Before = DateTime.Now.Subtract(TimeSpan.FromSeconds(60 * 60 * 24 * 2 + 30));
|
||||
viewModel.TenSecondsLater = DateTime.Now.AddSeconds(10);
|
||||
}
|
||||
});
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,7 @@ internal class en_US : LanguageProvider
|
||||
public const string DataDisplay_Segmented = "Segmented";
|
||||
public const string DataDisplay_GroupBox = "GroupBox";
|
||||
public const string DataDisplay_Badge = "Badge";
|
||||
public const string DataDisplay_Statistic = "Statistic";
|
||||
public const string DataDisplay_Timeline = "Timeline";
|
||||
public const string DataDisplay_QRCode = "QRCode";
|
||||
public const string DataDisplay_InfoFlyout = "InfoFlyout";
|
||||
|
||||
@@ -54,6 +54,7 @@ internal class zh_CN : LanguageProvider
|
||||
public const string DataDisplay_Segmented = "Segmented 分段控制器";
|
||||
public const string DataDisplay_GroupBox = "GroupBox 分组框";
|
||||
public const string DataDisplay_Badge = "Badge 徽标";
|
||||
public const string DataDisplay_Statistic = "Statistic 统计数值";
|
||||
public const string DataDisplay_Timeline = "Timeline 时间轴";
|
||||
public const string DataDisplay_QRCode = "QRCode 二维码";
|
||||
public const string DataDisplay_InfoFlyout = "InfoFlyout 信息浮层";
|
||||
|
||||
@@ -91,6 +91,7 @@ public class CaseNavigationViewModel : ReactiveObject, IActivatableViewModel
|
||||
_showCaseViewModelFactories.Add(GroupBoxViewModel.ID, () => new GroupBoxViewModel(HostScreen));
|
||||
_showCaseViewModelFactories.Add(ResultViewModel.ID, () => new ResultViewModel(HostScreen));
|
||||
_showCaseViewModelFactories.Add(BadgeViewModel.ID, () => new BadgeViewModel(HostScreen));
|
||||
_showCaseViewModelFactories.Add(StatisticViewModel.ID, () => new StatisticViewModel(HostScreen));
|
||||
_showCaseViewModelFactories.Add(TimelineViewModel.ID, () => new TimelineViewModel(HostScreen));
|
||||
_showCaseViewModelFactories.Add(QRCodeViewModel.ID, () => new QRCodeViewModel(HostScreen));
|
||||
_showCaseViewModelFactories.Add(LineEditViewModel.ID, () => new LineEditViewModel(HostScreen));
|
||||
|
||||
@@ -111,6 +111,8 @@
|
||||
ItemKey="{x:Static viewmodels:GroupBoxViewModel.ID}" />
|
||||
<atom:NavMenuNode Header="{gallery:CaseNavigationLangResource DataDisplay_Badge}"
|
||||
ItemKey="{x:Static viewmodels:BadgeViewModel.ID}" />
|
||||
<atom:NavMenuNode Header="{gallery:CaseNavigationLangResource DataDisplay_Statistic}"
|
||||
ItemKey="{x:Static viewmodels:StatisticViewModel.ID}" />
|
||||
<atom:NavMenuNode Header="{gallery:CaseNavigationLangResource DataDisplay_Timeline}"
|
||||
ItemKey="{x:Static viewmodels:TimelineViewModel.ID}" />
|
||||
<atom:NavMenuNode Header="{gallery:CaseNavigationLangResource DataDisplay_QRCode}"
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<ResourceInclude Source="Spin/Themes/SpinThemes.axaml" />
|
||||
<ResourceInclude Source="Splitter/Themes/SplitterThemes.axaml" />
|
||||
<ResourceInclude Source="SplitView/Themes/SplitViewTheme.axaml" />
|
||||
<!-- <ResourceInclude Source="Statistic/Themes/StatisticThemes.axaml" /> -->
|
||||
<ResourceInclude Source="Statistic/Themes/StatisticThemes.axaml" />
|
||||
<!-- <ResourceInclude Source="Steps/Themes/StepsThemes.axaml" /> -->
|
||||
<ResourceInclude Source="Switch/Themes/ToggleSwitchThemes.axaml" />
|
||||
<ResourceInclude Source="TabControl/Themes/TabControlThemes.axaml" />
|
||||
|
||||
+1
@@ -63,6 +63,7 @@ namespace AtomUI.Theme
|
||||
tokenTypes.Add(typeof(AtomUI.Desktop.Controls.SpinToken));
|
||||
tokenTypes.Add(typeof(AtomUI.Desktop.Controls.SplitterToken));
|
||||
tokenTypes.Add(typeof(AtomUI.Desktop.Controls.SplitViewToken));
|
||||
tokenTypes.Add(typeof(AtomUI.Desktop.Controls.StatisticToken));
|
||||
tokenTypes.Add(typeof(AtomUI.Desktop.Controls.StepsToken));
|
||||
tokenTypes.Add(typeof(AtomUI.Desktop.Controls.TabControlToken));
|
||||
tokenTypes.Add(typeof(AtomUI.Desktop.Controls.TagToken));
|
||||
|
||||
+17
@@ -922,6 +922,12 @@ namespace AtomUI.Desktop.Controls.DesignTokens
|
||||
PaneOpenMotionDuration
|
||||
}
|
||||
|
||||
public enum StatisticTokenKind
|
||||
{
|
||||
ContentFontSize,
|
||||
TitleFontSize
|
||||
}
|
||||
|
||||
public enum StepsTokenKind
|
||||
{
|
||||
CustomIconFontSize,
|
||||
@@ -1773,6 +1779,17 @@ namespace AtomUI.Desktop.Controls.DesignTokens
|
||||
}
|
||||
}
|
||||
|
||||
public class StatisticTokenResourceExtension : TokenResourceExtension<StatisticTokenKind>
|
||||
{
|
||||
public StatisticTokenResourceExtension()
|
||||
{
|
||||
}
|
||||
|
||||
public StatisticTokenResourceExtension(StatisticTokenKind kind) : base(kind)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class StepsTokenResourceExtension : TokenResourceExtension<StepsTokenKind>
|
||||
{
|
||||
public StepsTokenResourceExtension()
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
using AtomUI.Controls;
|
||||
using AtomUI.Reflection;
|
||||
using AtomUI.Theme;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.LogicalTree;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Metadata;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
public abstract class AbstractStatistic : HeaderedContentControl
|
||||
{
|
||||
#region 公共属性定义
|
||||
|
||||
public static readonly StyledProperty<string> DecimalSeparatorProperty =
|
||||
AvaloniaProperty.Register<AbstractStatistic, string>(nameof(DecimalSeparator), ".");
|
||||
|
||||
public static readonly StyledProperty<string> GroupSeparatorProperty =
|
||||
AvaloniaProperty.Register<AbstractStatistic, string>(nameof(GroupSeparator), ",");
|
||||
|
||||
public static readonly StyledProperty<int> PrecisionProperty =
|
||||
AvaloniaProperty.Register<AbstractStatistic, int>(nameof(Precision), 0);
|
||||
|
||||
public static readonly StyledProperty<bool> IsLoadingProperty =
|
||||
AvaloniaProperty.Register<AbstractStatistic, bool>(nameof(IsLoading));
|
||||
|
||||
public static readonly StyledProperty<object?> ValuePrefixAddOnProperty =
|
||||
AvaloniaProperty.Register<AbstractStatistic, object?>(nameof(ValuePrefixAddOn));
|
||||
|
||||
public static readonly StyledProperty<IDataTemplate?> ValuePrefixAddOnTemplateProperty =
|
||||
AvaloniaProperty.Register<AbstractStatistic, IDataTemplate?>(nameof(ValuePrefixAddOnTemplate));
|
||||
|
||||
public static readonly StyledProperty<object?> ValueSuffixAddOnProperty =
|
||||
AvaloniaProperty.Register<AbstractStatistic, object?>(nameof(ValueSuffixAddOn));
|
||||
|
||||
public static readonly StyledProperty<IDataTemplate?> ValueSuffixAddOnTemplateProperty =
|
||||
AvaloniaProperty.Register<AbstractStatistic, IDataTemplate?>(nameof(ValueSuffixAddOnTemplate));
|
||||
|
||||
public static readonly StyledProperty<IBrush?> ContentForegroundProperty =
|
||||
AvaloniaProperty.Register<AbstractStatistic, IBrush?>(nameof(ContentForeground));
|
||||
|
||||
public static readonly StyledProperty<double> ContentFontSizeProperty =
|
||||
AvaloniaProperty.Register<AbstractStatistic, double>(nameof(ContentFontSize));
|
||||
|
||||
public string DecimalSeparator
|
||||
{
|
||||
get => GetValue(DecimalSeparatorProperty);
|
||||
set => SetValue(DecimalSeparatorProperty, value);
|
||||
}
|
||||
|
||||
public string GroupSeparator
|
||||
{
|
||||
get => GetValue(GroupSeparatorProperty);
|
||||
set => SetValue(GroupSeparatorProperty, value);
|
||||
}
|
||||
|
||||
public int Precision
|
||||
{
|
||||
get => GetValue(PrecisionProperty);
|
||||
set => SetValue(PrecisionProperty, value);
|
||||
}
|
||||
|
||||
public bool IsLoading
|
||||
{
|
||||
get => GetValue(IsLoadingProperty);
|
||||
set => SetValue(IsLoadingProperty, value);
|
||||
}
|
||||
|
||||
[DependsOn(nameof(ValuePrefixAddOnTemplate))]
|
||||
public object? ValuePrefixAddOn
|
||||
{
|
||||
get => GetValue(ValuePrefixAddOnProperty);
|
||||
set => SetValue(ValuePrefixAddOnProperty, value);
|
||||
}
|
||||
|
||||
public IDataTemplate? ValuePrefixAddOnTemplate
|
||||
{
|
||||
get => GetValue(ValuePrefixAddOnTemplateProperty);
|
||||
set => SetValue(ValuePrefixAddOnTemplateProperty, value);
|
||||
}
|
||||
|
||||
[DependsOn(nameof(ValueSuffixAddOnTemplate))]
|
||||
public object? ValueSuffixAddOn
|
||||
{
|
||||
get => GetValue(ValueSuffixAddOnProperty);
|
||||
set => SetValue(ValueSuffixAddOnProperty, value);
|
||||
}
|
||||
|
||||
public IDataTemplate? ValueSuffixAddOnTemplate
|
||||
{
|
||||
get => GetValue(ValueSuffixAddOnTemplateProperty);
|
||||
set => SetValue(ValueSuffixAddOnTemplateProperty, value);
|
||||
}
|
||||
|
||||
public IBrush? ContentForeground
|
||||
{
|
||||
get => GetValue(ContentForegroundProperty);
|
||||
set => SetValue(ContentForegroundProperty, value);
|
||||
}
|
||||
|
||||
public double ContentFontSize
|
||||
{
|
||||
get => GetValue(ContentFontSizeProperty);
|
||||
set => SetValue(ContentFontSizeProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
static AbstractStatistic()
|
||||
{
|
||||
HeaderProperty.Changed.AddClassHandler<AbstractStatistic>((x, e) => x.HandleChildChanged(e));
|
||||
ValuePrefixAddOnProperty.Changed.AddClassHandler<AbstractStatistic>((x, e) => x.HandleChildChanged(e));
|
||||
ValueSuffixAddOnProperty.Changed.AddClassHandler<AbstractStatistic>((x, e) => x.HandleChildChanged(e));
|
||||
AffectsMeasure<AbstractStatistic>(DecimalSeparatorProperty, GroupSeparatorProperty, PrecisionProperty);
|
||||
}
|
||||
|
||||
public AbstractStatistic()
|
||||
{
|
||||
this.RegisterTokenResourceScope(StatisticToken.ScopeProvider);
|
||||
}
|
||||
|
||||
private void HandleChildChanged(AvaloniaPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.OldValue is ILogical oldChild)
|
||||
{
|
||||
LogicalChildren.Remove(oldChild);
|
||||
if (oldChild is Icon icon)
|
||||
{
|
||||
icon.SetTemplatedParent(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (e.NewValue is ILogical newChild)
|
||||
{
|
||||
LogicalChildren.Add(newChild);
|
||||
if (newChild is Icon icon)
|
||||
{
|
||||
icon.SetTemplatedParent(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using Avalonia;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
public class Statistic : AbstractStatistic
|
||||
{
|
||||
#region 公共属性定义
|
||||
|
||||
public static readonly StyledProperty<object?> ValueProperty =
|
||||
AvaloniaProperty.Register<AbstractStatistic, object?>(nameof(Value));
|
||||
|
||||
public object? Value
|
||||
{
|
||||
get => GetValue(ValueProperty);
|
||||
set => SetValue(ValueProperty, value);
|
||||
}
|
||||
|
||||
public Func<Statistic, object?, string?>? Formatter { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 内部属性定义
|
||||
|
||||
internal static readonly DirectProperty<Statistic, string?> EffectiveValueProperty =
|
||||
AvaloniaProperty.RegisterDirect<Statistic, string?>(nameof(EffectiveValue),
|
||||
o => o.EffectiveValue,
|
||||
(o, v) => o.EffectiveValue = v);
|
||||
|
||||
private string? _effectiveValue;
|
||||
|
||||
internal string? EffectiveValue
|
||||
{
|
||||
get => _effectiveValue;
|
||||
set => SetAndRaise(EffectiveValueProperty, ref _effectiveValue, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
static Statistic()
|
||||
{
|
||||
ContentProperty.Changed.AddClassHandler<Statistic>((x, e) => x.HandleContentChanged(e));
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
if (change.Property == ValueProperty ||
|
||||
change.Property == DecimalSeparatorProperty ||
|
||||
change.Property == GroupSeparatorProperty ||
|
||||
change.Property == PrecisionProperty)
|
||||
{
|
||||
GenerateEffectiveValue();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
if (Content == null)
|
||||
{
|
||||
SetCurrentValue(ContentProperty, EffectiveValue);
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateEffectiveValue()
|
||||
{
|
||||
if (Formatter != null)
|
||||
{
|
||||
SetCurrentValue(EffectiveValueProperty, Formatter(this, Value));
|
||||
}
|
||||
else if (Value != null)
|
||||
{
|
||||
var effectiveValue = StatisticUtils.FormatNumber(Value, GroupSeparator, DecimalSeparator, Precision);
|
||||
SetCurrentValue(EffectiveValueProperty, effectiveValue);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleContentChanged(AvaloniaPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.NewValue is StatisticCountUp countUp)
|
||||
{
|
||||
countUp.DataContext = this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
using AtomUI.Theme;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
public class StatisticCountUp : TemplatedControl
|
||||
{
|
||||
#region 公共属性定义
|
||||
|
||||
public static readonly StyledProperty<double> EndValueProperty =
|
||||
AvaloniaProperty.Register<StatisticCountUp, double>(nameof(EndValue));
|
||||
|
||||
public static readonly StyledProperty<string> DecimalSeparatorProperty =
|
||||
AvaloniaProperty.Register<StatisticCountUp, string>(nameof(DecimalSeparator), ".");
|
||||
|
||||
public static readonly StyledProperty<string> GroupSeparatorProperty =
|
||||
AvaloniaProperty.Register<StatisticCountUp, string>(nameof(GroupSeparator), ",");
|
||||
|
||||
public static readonly StyledProperty<int> PrecisionProperty =
|
||||
AvaloniaProperty.Register<StatisticCountUp, int>(nameof(Precision), 0);
|
||||
|
||||
public double EndValue
|
||||
{
|
||||
get => GetValue(EndValueProperty);
|
||||
set => SetValue(EndValueProperty, value);
|
||||
}
|
||||
|
||||
public string DecimalSeparator
|
||||
{
|
||||
get => GetValue(DecimalSeparatorProperty);
|
||||
set => SetValue(DecimalSeparatorProperty, value);
|
||||
}
|
||||
|
||||
public string GroupSeparator
|
||||
{
|
||||
get => GetValue(GroupSeparatorProperty);
|
||||
set => SetValue(GroupSeparatorProperty, value);
|
||||
}
|
||||
|
||||
public int Precision
|
||||
{
|
||||
get => GetValue(PrecisionProperty);
|
||||
set => SetValue(PrecisionProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 内部属性定义
|
||||
|
||||
public static readonly StyledProperty<double> AnimatingValueProperty =
|
||||
AvaloniaProperty.Register<StatisticCountUp, double>(nameof(AnimatingValue), 0);
|
||||
|
||||
internal static readonly DirectProperty<StatisticCountUp, string?> FormattedValueProperty =
|
||||
AvaloniaProperty.RegisterDirect<StatisticCountUp, string?>(nameof(FormattedValue),
|
||||
o => o.FormattedValue,
|
||||
(o, v) => o.FormattedValue = v);
|
||||
|
||||
public double AnimatingValue
|
||||
{
|
||||
get => GetValue(AnimatingValueProperty);
|
||||
set => SetValue(AnimatingValueProperty, value);
|
||||
}
|
||||
|
||||
private string? _formattedValue;
|
||||
|
||||
internal string? FormattedValue
|
||||
{
|
||||
get => _formattedValue;
|
||||
set => SetAndRaise(FormattedValueProperty, ref _formattedValue, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
static StatisticCountUp()
|
||||
{
|
||||
AffectsMeasure<StatisticCountUp>(FormattedValueProperty);
|
||||
}
|
||||
|
||||
public StatisticCountUp()
|
||||
{
|
||||
this.RegisterTokenResourceScope(StatisticToken.ScopeProvider);
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
|
||||
if (this.IsAttachedToVisualTree())
|
||||
{
|
||||
if (change.Property == EndValueProperty)
|
||||
{
|
||||
SetCurrentValue(AnimatingValueProperty, EndValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (change.Property == AnimatingValueProperty ||
|
||||
change.Property == GroupSeparatorProperty ||
|
||||
change.Property == PrecisionProperty ||
|
||||
change.Property == DecimalSeparatorProperty)
|
||||
{
|
||||
var effectiveValue = StatisticUtils.FormatNumber(AnimatingValue, GroupSeparator, DecimalSeparator, Precision);
|
||||
SetCurrentValue(FormattedValueProperty, effectiveValue);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnLoaded(RoutedEventArgs e)
|
||||
{
|
||||
base.OnLoaded(e);
|
||||
AnimatingValue = EndValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using AtomUI.Desktop.Controls.DesignTokens;
|
||||
using AtomUI.Theme;
|
||||
using AtomUI.Theme.TokenSystem;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
[ControlDesignToken]
|
||||
internal class StatisticToken : AbstractControlDesignToken
|
||||
{
|
||||
public const string ID = "Statistic";
|
||||
public static readonly ControlTokenResourceScopeProvider ScopeProvider = new(ID);
|
||||
|
||||
/// <summary>
|
||||
/// 标题字体大小
|
||||
/// Title font size
|
||||
/// </summary>
|
||||
public double TitleFontSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内容字体大小
|
||||
/// Content font size
|
||||
/// </summary>
|
||||
public double ContentFontSize { get; set; }
|
||||
|
||||
public StatisticToken()
|
||||
: base(ID)
|
||||
{
|
||||
}
|
||||
|
||||
public override void CalculateTokenValues(bool isDarkMode)
|
||||
{
|
||||
base.CalculateTokenValues(isDarkMode);
|
||||
TitleFontSize = SharedToken.FontSize;
|
||||
ContentFontSize = SharedToken.FontSizeHeading3;
|
||||
}
|
||||
|
||||
protected override Type GetTokenKindType() => typeof(StatisticTokenKind);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
internal static class StatisticUtils
|
||||
{
|
||||
public static string? FormatNumber(object value, string groupSeparator, string decimalSeparator, int precision)
|
||||
{
|
||||
var strValue = value.ToString();
|
||||
var customFormat = new CultureInfo("en-US");
|
||||
customFormat.NumberFormat.NumberGroupSeparator = groupSeparator;
|
||||
customFormat.NumberFormat.NumberDecimalSeparator = decimalSeparator;
|
||||
customFormat.NumberFormat.NumberDecimalDigits = precision;
|
||||
string? effectiveValue = null;
|
||||
if (int.TryParse(strValue, out var intValue))
|
||||
{
|
||||
effectiveValue = intValue.ToString("N", customFormat);
|
||||
}
|
||||
else if (uint.TryParse(strValue, out var uintValue))
|
||||
{
|
||||
effectiveValue = uintValue.ToString("N", customFormat);
|
||||
}
|
||||
else if (long.TryParse(strValue, out var longValue))
|
||||
{
|
||||
effectiveValue = longValue.ToString("N", customFormat);
|
||||
}
|
||||
else if (long.TryParse(strValue, out var ulongValue))
|
||||
{
|
||||
effectiveValue = ulongValue.ToString("N", customFormat);
|
||||
}
|
||||
else if (float.TryParse(strValue, out var floatValue))
|
||||
{
|
||||
effectiveValue = floatValue.ToString("N", customFormat);
|
||||
}
|
||||
else if (double.TryParse(strValue, out var doubleValue))
|
||||
{
|
||||
effectiveValue = doubleValue.ToString("N", customFormat);
|
||||
}
|
||||
else if (decimal.TryParse(strValue, out var decimalValue))
|
||||
{
|
||||
effectiveValue = decimalValue.ToString("N", customFormat);
|
||||
}
|
||||
else
|
||||
{
|
||||
effectiveValue = strValue;
|
||||
}
|
||||
|
||||
return effectiveValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<ControlTheme TargetType="atom:AbstractStatistic"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:atom="https://atomui.net"
|
||||
x:Class="AtomUI.Desktop.Controls.Themes.AbstractStatisticTheme"
|
||||
x:ClassModifier="internal">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate TargetType="atom:AbstractStatistic">
|
||||
<Border Name="Frame"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<StackPanel Name="RootLayout">
|
||||
<ContentPresenter Name="HeaderPresenter"
|
||||
Content="{TemplateBinding Header}"
|
||||
ContentTemplate="{TemplateBinding HeaderTemplate}"
|
||||
IsVisible="{TemplateBinding Header, Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||
<atom:Skeleton IsLoading="{TemplateBinding IsLoading}" IsActive="True" IsShowParagraph="False">
|
||||
<StackPanel Orientation="Horizontal" Name="ContentLayout">
|
||||
<ContentPresenter Name="ValuePrefixAddOn"
|
||||
Foreground="{TemplateBinding ContentForeground}"
|
||||
FontSize="{TemplateBinding ContentFontSize}"
|
||||
Content="{TemplateBinding ValuePrefixAddOn}"
|
||||
ContentTemplate="{TemplateBinding ValuePrefixAddOnTemplate}"
|
||||
IsVisible="{TemplateBinding ValuePrefixAddOn, Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||
<ContentPresenter Name="ContentPresenter"
|
||||
Foreground="{TemplateBinding ContentForeground}"
|
||||
FontSize="{TemplateBinding ContentFontSize}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}" />
|
||||
<ContentPresenter Name="ValueSuffixAddOn"
|
||||
Foreground="{TemplateBinding ContentForeground}"
|
||||
FontSize="{TemplateBinding ContentFontSize}"
|
||||
Content="{TemplateBinding ValueSuffixAddOn}"
|
||||
ContentTemplate="{TemplateBinding ValueSuffixAddOnTemplate}"
|
||||
IsVisible="{TemplateBinding ValueSuffixAddOn, Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||
</StackPanel>
|
||||
</atom:Skeleton>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<Style Selector="^:is(atom|AbstractStatistic)">
|
||||
<Style Selector="^ /template/ ContentPresenter#HeaderPresenter">
|
||||
<Setter Property="Foreground" Value="{atom:SharedTokenResource ColorTextDescription}" />
|
||||
<Setter Property="FontSize" Value="{atom:StatisticTokenResource TitleFontSize}" />
|
||||
</Style>
|
||||
|
||||
<Setter Property="ContentForeground" Value="{atom:SharedTokenResource ColorTextHeading}" />
|
||||
<Setter Property="ContentFontSize" Value="{atom:StatisticTokenResource ContentFontSize}" />
|
||||
|
||||
<Style Selector="^ /template/ atom|Icon">
|
||||
<Setter Property="FillBrush" Value="{TemplateBinding ContentForeground}" />
|
||||
<Setter Property="StrokeBrush" Value="{TemplateBinding ContentForeground}" />
|
||||
<Setter Property="Width" Value="{atom:StatisticTokenResource ContentFontSize}" />
|
||||
<Setter Property="Height" Value="{atom:StatisticTokenResource ContentFontSize}" />
|
||||
</Style>
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
@@ -0,0 +1,7 @@
|
||||
using Avalonia.Styling;
|
||||
|
||||
namespace AtomUI.Desktop.Controls.Themes;
|
||||
|
||||
internal class AbstractStatisticTheme : ControlTheme
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<ResourceDictionary
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:atom="https://atomui.net"
|
||||
x:ClassModifier="internal">
|
||||
<ControlTheme x:Key="{x:Type atom:StatisticCountUp}" TargetType="atom:StatisticCountUp">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<TextBlock Text="{TemplateBinding FormattedValue}"/>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<Setter Property="Foreground" Value="{atom:SharedTokenResource ColorTextHeading}"/>
|
||||
<Setter Property="FontSize" Value="{atom:StatisticTokenResource ContentFontSize}"/>
|
||||
|
||||
<Style Selector="^">
|
||||
<Setter Property="Transitions">
|
||||
<Transitions>
|
||||
<DoubleTransition Property="AnimatingValue" Duration="{atom:SharedTokenResourceValue Kind=MotionDurationVerySlow}" Easing="ExponentialEaseOut" />
|
||||
</Transitions>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,26 @@
|
||||
<ResourceDictionary
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:atom="https://atomui.net"
|
||||
x:ClassModifier="internal">
|
||||
<ControlTheme x:Key="{x:Type atom:Statistic}" TargetType="atom:Statistic">
|
||||
<ControlTheme.BasedOn>
|
||||
<atom:AbstractStatisticTheme TargetType="atom:Statistic"/>
|
||||
</ControlTheme.BasedOn>
|
||||
|
||||
<Style Selector="^ /template/ StackPanel#RootLayout">
|
||||
<Setter Property="Spacing" Value="{atom:SharedTokenResource SpacingXXS}"/>
|
||||
</Style>
|
||||
|
||||
<Style Selector="^[IsLoading=True]">
|
||||
<Style Selector="^ /template/ StackPanel#RootLayout">
|
||||
<Setter Property="Spacing" Value="{atom:SharedTokenResource Spacing}"/>
|
||||
</Style>
|
||||
</Style>
|
||||
|
||||
<Style Selector="^ /template/ StackPanel#ContentLayout">
|
||||
<Setter Property="Spacing" Value="{atom:SharedTokenResource SpacingXXS}"/>
|
||||
</Style>
|
||||
|
||||
</ControlTheme>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,11 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:atom="https://atomui.net"
|
||||
x:ClassModifier="internal">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceInclude Source="StatisticTheme.axaml"/>
|
||||
<ResourceInclude Source="TimerStatisticTheme.axaml"/>
|
||||
<ResourceInclude Source="StatisticCountUpTheme.axaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<atom:AbstractStatisticTheme x:Key="{x:Type atom:AbstractStatistic}" TargetType="atom:AbstractStatistic"/>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,45 @@
|
||||
<ResourceDictionary
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:atom="https://atomui.net"
|
||||
x:ClassModifier="internal">
|
||||
<ControlTheme x:Key="{x:Type atom:TimerStatistic}" TargetType="atom:TimerStatistic">
|
||||
<ControlTheme.BasedOn>
|
||||
<atom:AbstractStatisticTheme TargetType="atom:TimerStatistic"/>
|
||||
</ControlTheme.BasedOn>
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border Name="Frame"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<StackPanel Name="RootLayout">
|
||||
<ContentPresenter Name="HeaderPresenter"
|
||||
Content="{TemplateBinding Header}"
|
||||
ContentTemplate="{TemplateBinding HeaderTemplate}"
|
||||
IsVisible="{TemplateBinding Header, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
<atom:Skeleton IsLoading="{TemplateBinding IsLoading}" IsActive="True" IsShowParagraph="False">
|
||||
<StackPanel Orientation="Horizontal" Name="ContentLayout">
|
||||
<ContentPresenter Name="ValuePrefixAddOn"
|
||||
Foreground="{TemplateBinding ContentForeground}"
|
||||
FontSize="{TemplateBinding ContentFontSize}"
|
||||
Content="{TemplateBinding ValuePrefixAddOn}"
|
||||
ContentTemplate="{TemplateBinding ValuePrefixAddOnTemplate}"
|
||||
IsVisible="{TemplateBinding ValuePrefixAddOn, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
<ContentPresenter Name="ContentPresenter"
|
||||
Foreground="{TemplateBinding ContentForeground}"
|
||||
FontSize="{TemplateBinding ContentFontSize}"
|
||||
Content="{TemplateBinding RemainingTimeText}"/>
|
||||
<ContentPresenter Name="ValueSuffixAddOn"
|
||||
Foreground="{TemplateBinding ContentForeground}"
|
||||
FontSize="{TemplateBinding ContentFontSize}"
|
||||
Content="{TemplateBinding ValueSuffixAddOn}"
|
||||
ContentTemplate="{TemplateBinding ValueSuffixAddOnTemplate}"
|
||||
IsVisible="{TemplateBinding ValueSuffixAddOn, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
</StackPanel>
|
||||
</atom:Skeleton>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,177 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
public class TimerStatistic : AbstractStatistic
|
||||
{
|
||||
#region 公共属性定义
|
||||
|
||||
public static readonly StyledProperty<DateTime> ValueProperty =
|
||||
AvaloniaProperty.Register<AbstractStatistic, DateTime>(nameof(Value));
|
||||
|
||||
public static readonly StyledProperty<string?> FormatProperty =
|
||||
AvaloniaProperty.Register<AbstractStatistic, string?>(nameof(Format));
|
||||
|
||||
public static readonly StyledProperty<TimeSpan> RefreshDurationProperty =
|
||||
AvaloniaProperty.Register<AbstractStatistic, TimeSpan>(nameof(RefreshDuration), TimeSpan.FromMilliseconds(10));
|
||||
|
||||
public DateTime Value
|
||||
{
|
||||
get => GetValue(ValueProperty);
|
||||
set => SetValue(ValueProperty, value);
|
||||
}
|
||||
|
||||
public string? Format
|
||||
{
|
||||
get => GetValue(FormatProperty);
|
||||
set => SetValue(FormatProperty, value);
|
||||
}
|
||||
|
||||
public TimeSpan RefreshDuration
|
||||
{
|
||||
get => GetValue(RefreshDurationProperty);
|
||||
set => SetValue(RefreshDurationProperty, value);
|
||||
}
|
||||
|
||||
|
||||
public Func<TimeSpan, string>? Formatter;
|
||||
#endregion
|
||||
|
||||
#region 公共事件定义
|
||||
|
||||
public event EventHandler? CountdownFinished;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 内部属性定义
|
||||
|
||||
internal static readonly DirectProperty<TimerStatistic, TimeSpan> RemainingTimeProperty =
|
||||
AvaloniaProperty.RegisterDirect<TimerStatistic, TimeSpan>(nameof(RemainingTime),
|
||||
o => o.RemainingTime,
|
||||
(o, v) => o.RemainingTime = v);
|
||||
|
||||
internal static readonly DirectProperty<TimerStatistic, string?> RemainingTimeTextProperty =
|
||||
AvaloniaProperty.RegisterDirect<TimerStatistic, string?>(nameof(RemainingTimeText),
|
||||
o => o.RemainingTimeText,
|
||||
(o, v) => o.RemainingTimeText = v);
|
||||
|
||||
private TimeSpan _remainingTime;
|
||||
|
||||
internal TimeSpan RemainingTime
|
||||
{
|
||||
get => _remainingTime;
|
||||
set => SetAndRaise(RemainingTimeProperty, ref _remainingTime, value);
|
||||
}
|
||||
|
||||
private string? _remainingTimeText;
|
||||
|
||||
internal string? RemainingTimeText
|
||||
{
|
||||
get => _remainingTimeText;
|
||||
set => SetAndRaise(RemainingTimeTextProperty, ref _remainingTimeText, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private DispatcherTimer? _timer;
|
||||
|
||||
static TimerStatistic()
|
||||
{
|
||||
AffectsMeasure<TimerStatistic>(ValueProperty, FormatProperty, RemainingTimeTextProperty);
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
_timer?.Start();
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnDetachedFromVisualTree(e);
|
||||
if (_timer != null)
|
||||
{
|
||||
_timer.Stop();
|
||||
_timer.Tick -= HandleTickElapsed;
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
if (change.Property == RemainingTimeProperty ||
|
||||
change.Property == FormatProperty)
|
||||
{
|
||||
GenerateRemainingTimeText();
|
||||
}
|
||||
|
||||
if (change.Property == ValueProperty ||
|
||||
change.Property == RefreshDurationProperty)
|
||||
{
|
||||
BuildTimer(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateRemainingTimeText()
|
||||
{
|
||||
string? formattedText;
|
||||
if (Formatter != null)
|
||||
{
|
||||
formattedText = Formatter(RemainingTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Format != null)
|
||||
{
|
||||
formattedText = RemainingTime.ToString(Format);
|
||||
}
|
||||
else if (RemainingTime.TotalHours >= 1)
|
||||
{
|
||||
formattedText = $"{(int)RemainingTime.TotalHours:00}:{RemainingTime.Minutes:00}:{RemainingTime.Seconds:00}";
|
||||
}
|
||||
else
|
||||
{
|
||||
formattedText = $"{(int)RemainingTime.TotalMinutes:00}:{RemainingTime.Seconds:00}";
|
||||
}
|
||||
}
|
||||
SetCurrentValue(RemainingTimeTextProperty, formattedText);
|
||||
}
|
||||
|
||||
private void BuildTimer(bool start)
|
||||
{
|
||||
// 先清理旧定时器
|
||||
if (_timer != null)
|
||||
{
|
||||
_timer.Stop();
|
||||
_timer.Tick -= HandleTickElapsed;
|
||||
}
|
||||
|
||||
_timer = new DispatcherTimer();
|
||||
_timer.Interval = RefreshDuration;
|
||||
_timer.Tick += HandleTickElapsed;
|
||||
if (start)
|
||||
{
|
||||
_timer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleTickElapsed(object? sender, EventArgs args)
|
||||
{
|
||||
if (Value > DateTime.Now)
|
||||
{
|
||||
RemainingTime = Value - DateTime.Now;
|
||||
}
|
||||
else
|
||||
{
|
||||
RemainingTime = DateTime.Now - Value;
|
||||
}
|
||||
if (RemainingTime <= TimeSpan.Zero)
|
||||
{
|
||||
RemainingTime = TimeSpan.Zero;
|
||||
_timer?.Stop();
|
||||
CountdownFinished?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user