新增加 利润统计Window。

隐藏利润统计按钮
增加 Common 文件夹 内部是WPF公用类库
This commit is contained in:
“chfei” committed 2026-08-05 18:55:03 +08:00
1 parent 4a7af051b2
commit 7c6f91f207
25 files changed
+6462 -3554

No files matched your search

+46 -1
View File
@@ -68,7 +68,7 @@
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>..\..\KJD Cef138\Bin\</OutputPath>
<OutputPath>..\..\KJD\Bin\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
@@ -140,6 +140,12 @@
</Compile>
<Compile Include="BaseWindows\BaseWindow.cs" />
<Compile Include="CefHelper\WPFCEFBrowser.cs" />
<Compile Include="Common\DebugVisibility.cs" />
<Compile Include="Common\DelegateCommand.cs" />
<Compile Include="Common\DelegateCommandBase.cs" />
<Compile Include="Common\DelegateCommand{T}.cs" />
<Compile Include="Common\DialogWindowProperty.cs" />
<Compile Include="Common\Result.cs" />
<Compile Include="Converters\ImageUrlToBitmapImageConverter.cs" />
<Compile Include="Converters\HasValueToVisibilityConverter.cs" />
<Compile Include="Converters\ListBoxIndexConverter.cs" />
@@ -153,6 +159,9 @@
<Compile Include="Ctrls\BuyerCtrls\PDDBuyerSelectCtrl.xaml.cs">
<DependentUpon>PDDBuyerSelectCtrl.xaml</DependentUpon>
</Compile>
<Compile Include="Common\DialogWindow.xaml.cs">
<DependentUpon>DialogWindow.xaml</DependentUpon>
</Compile>
<Compile Include="Ctrls\OrderCtrls\OrderListRowCtrl.xaml.cs">
<DependentUpon>OrderListRowCtrl.xaml</DependentUpon>
</Compile>
@@ -168,6 +177,18 @@
<Compile Include="Ctrls\ShopCtrls\ShopListCtrl.xaml.cs">
<DependentUpon>ShopListCtrl.xaml</DependentUpon>
</Compile>
<Compile Include="Ctrls\Statistics\Charts\RevenueLineChart.xaml.cs">
<DependentUpon>RevenueLineChart.xaml</DependentUpon>
</Compile>
<Compile Include="Ctrls\Statistics\Charts\RevenuePieChart.xaml.cs">
<DependentUpon>RevenuePieChart.xaml</DependentUpon>
</Compile>
<Compile Include="Ctrls\Statistics\Models\ShopRevenueStatisticsModels.cs" />
<Compile Include="Ctrls\Statistics\Services\ShopRevenueStatisticsService.cs" />
<Compile Include="Ctrls\Statistics\ShopRevenueStatisticsView.xaml.cs">
<DependentUpon>ShopRevenueStatisticsView.xaml</DependentUpon>
</Compile>
<Compile Include="Ctrls\Statistics\ViewModels\ShopRevenueStatisticsViewModel.cs" />
<Compile Include="UserControls\UcWaterMarkTextBox.xaml.cs">
<DependentUpon>UcWaterMarkTextBox.xaml</DependentUpon>
</Compile>
@@ -202,6 +223,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Common\DialogWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Ctrls\OrderCtrls\OrderListRowCtrl.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
@@ -222,6 +247,26 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Ctrls\Statistics\Charts\Chart.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Ctrls\Statistics\Charts\RevenueLineChart.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Ctrls\Statistics\Charts\RevenuePieChart.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Ctrls\Statistics\General.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Ctrls\Statistics\ShopRevenueStatisticsView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UserControls\UcWaterMarkTextBox.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
+2 -2
View File
@@ -28,8 +28,8 @@
<ResourceDictionary Source="Styles/StaticColorBrushDictionary.xaml" />
<ResourceDictionary Source="Styles/StaticImageBrushDictionary.xaml" />
<!-- 可以添加更多资源字典 -->
</ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Ctrls/Statistics/General.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
+56
View File
@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace _29WxOrderWpf.Common
{
public static class DebugVisibility
{
// 定义一个附加属性
public static readonly DependencyProperty EnableProperty =
DependencyProperty.RegisterAttached(
"Enable",
typeof(bool),
typeof(DebugVisibility),
new FrameworkPropertyMetadata(
true, FrameworkPropertyMetadataOptions.AffectsArrange,
OnEnableChanged, OnEnableCoerceCall));
private static object OnEnableCoerceCall(DependencyObject d, object baseValue)
{
if (d is UIElement element)
{
#if DEBUG
// Debug 模式下,根据属性值决定可见性
element.Visibility = (bool)baseValue ? Visibility.Visible : Visibility.Collapsed;
#else
// Release 模式下,隐藏所有标记了 IsDebug 的元素
element.Visibility = Visibility.Collapsed;
#endif
}
return baseValue;
}
private static void OnEnableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is UIElement element)
{
#if DEBUG
// Debug 模式下,根据属性值决定可见性
element.Visibility = (bool)e.NewValue ? Visibility.Visible : Visibility.Collapsed;
#else
// Release 模式下,隐藏所有标记了 IsDebug 的元素
element.Visibility = Visibility.Collapsed;
#endif
}
}
public static bool GetEnable(DependencyObject obj) => (bool)obj.GetValue(EnableProperty);
public static void SetEnable(DependencyObject obj, bool value) => obj.SetValue(EnableProperty, value);
}
}
+103
View File
@@ -0,0 +1,103 @@
using System;
using System.Linq.Expressions;
using System.Windows.Input;
namespace _29WxOrderWpf.Commands
{
/// <summary>
/// An <see cref="ICommand"/> whose delegates do not take any parameters for <see cref="Execute()"/> and <see cref="CanExecute()"/>.
/// </summary>
/// <see cref="DelegateCommandBase"/>
/// <see cref="DelegateCommand{T}"/>
public class DelegateCommand : DelegateCommandBase
{
Action _executeMethod;
Func<bool> _canExecuteMethod;
/// <summary>
/// Creates a new instance of <see cref="DelegateCommand"/> with the <see cref="Action"/> to invoke on execution.
/// </summary>
/// <param name="executeMethod">The <see cref="Action"/> to invoke when <see cref="ICommand.Execute(object)"/> is called.</param>
public DelegateCommand(Action executeMethod)
: this(executeMethod, () => true)
{
}
/// <summary>
/// Creates a new instance of <see cref="DelegateCommand"/> with the <see cref="Action"/> to invoke on execution
/// and a <see langword="Func" /> to query for determining if the command can execute.
/// </summary>
/// <param name="executeMethod">The <see cref="Action"/> to invoke when <see cref="ICommand.Execute"/> is called.</param>
/// <param name="canExecuteMethod">The <see cref="Func{TResult}"/> to invoke when <see cref="ICommand.CanExecute"/> is called</param>
public DelegateCommand(Action executeMethod, Func<bool> canExecuteMethod)
: base()
{
if (executeMethod == null || canExecuteMethod == null)
throw new ArgumentNullException(nameof(executeMethod), "Delegate Command Delegates Cannot Be Null");
_executeMethod = executeMethod;
_canExecuteMethod = canExecuteMethod;
}
///<summary>
/// Executes the command.
///</summary>
public void Execute()
{
_executeMethod();
}
/// <summary>
/// Determines if the command can be executed.
/// </summary>
/// <returns>Returns <see langword="true"/> if the command can execute,otherwise returns <see langword="false"/>.</returns>
public bool CanExecute()
{
return _canExecuteMethod();
}
/// <summary>
/// Handle the internal invocation of <see cref="ICommand.Execute(object)"/>
/// </summary>
/// <param name="parameter">Command Parameter</param>
protected override void Execute(object parameter)
{
Execute();
}
/// <summary>
/// Handle the internal invocation of <see cref="ICommand.CanExecute(object)"/>
/// </summary>
/// <param name="parameter"></param>
/// <returns><see langword="true"/> if the Command Can Execute, otherwise <see langword="false" /></returns>
protected override bool CanExecute(object parameter)
{
return CanExecute();
}
/// <summary>
/// Observes a property that implements INotifyPropertyChanged, and automatically calls DelegateCommandBase.RaiseCanExecuteChanged on property changed notifications.
/// </summary>
/// <typeparam name="T">The object type containing the property specified in the expression.</typeparam>
/// <param name="propertyExpression">The property expression. Example: ObservesProperty(() => PropertyName).</param>
/// <returns>The current instance of DelegateCommand</returns>
public DelegateCommand ObservesProperty<T>(Expression<Func<T>> propertyExpression)
{
ObservesPropertyInternal(propertyExpression);
return this;
}
/// <summary>
/// Observes a property that is used to determine if this command can execute, and if it implements INotifyPropertyChanged it will automatically call DelegateCommandBase.RaiseCanExecuteChanged on property changed notifications.
/// </summary>
/// <param name="canExecuteExpression">The property expression. Example: ObservesCanExecute(() => PropertyName).</param>
/// <returns>The current instance of DelegateCommand</returns>
public DelegateCommand ObservesCanExecute(Expression<Func<bool>> canExecuteExpression)
{
_canExecuteMethod = canExecuteExpression.Compile();
ObservesPropertyInternal(canExecuteExpression);
return this;
}
}
}
+135
View File
@@ -0,0 +1,135 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
namespace _29WxOrderWpf.Commands
{
public abstract class DelegateCommandBase : ICommand
{
private bool _isActive;
private SynchronizationContext _synchronizationContext;
private readonly HashSet<string> _observedPropertiesExpressions = new HashSet<string>();
/// <summary>
/// Creates a new instance of a <see cref="DelegateCommandBase"/>, specifying both the execute action and the can execute function.
/// </summary>
protected DelegateCommandBase()
{
_synchronizationContext = SynchronizationContext.Current;
}
/// <summary>
/// Occurs when changes occur that affect whether or not the command should execute.
/// </summary>
public virtual event EventHandler CanExecuteChanged;
/// <summary>
/// Raises <see cref="ICommand.CanExecuteChanged"/> so every
/// command invoker can requery <see cref="ICommand.CanExecute"/>.
/// </summary>
protected virtual void OnCanExecuteChanged()
{
var handler = CanExecuteChanged;
if (handler != null)
{
if (_synchronizationContext != null && _synchronizationContext != SynchronizationContext.Current)
_synchronizationContext.Post((o) => handler.Invoke(this, EventArgs.Empty), null);
else
handler.Invoke(this, EventArgs.Empty);
}
}
/// <summary>
/// Raises <see cref="CanExecuteChanged"/> so every command invoker
/// can requery to check if the command can execute.
/// </summary>
/// <remarks>Note that this will trigger the execution of <see cref="CanExecuteChanged"/> once for each invoker.</remarks>
[SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate")]
public void RaiseCanExecuteChanged()
{
OnCanExecuteChanged();
}
void ICommand.Execute(object parameter)
{
Execute(parameter);
}
bool ICommand.CanExecute(object parameter)
{
return CanExecute(parameter);
}
/// <summary>
/// Handle the internal invocation of <see cref="ICommand.Execute(object)"/>
/// </summary>
/// <param name="parameter">Command Parameter</param>
protected abstract void Execute(object parameter);
/// <summary>
/// Handle the internal invocation of <see cref="ICommand.CanExecute(object)"/>
/// </summary>
/// <param name="parameter"></param>
/// <returns><see langword="true"/> if the Command Can Execute, otherwise <see langword="false" /></returns>
protected abstract bool CanExecute(object parameter);
/// <summary>
/// Observes a property that implements INotifyPropertyChanged, and automatically calls DelegateCommandBase.RaiseCanExecuteChanged on property changed notifications.
/// </summary>
/// <typeparam name="T">The object type containing the property specified in the expression.</typeparam>
/// <param name="propertyExpression">The property expression. Example: ObservesProperty(() => PropertyName).</param>
protected internal void ObservesPropertyInternal<T>(Expression<Func<T>> propertyExpression)
{
if (_observedPropertiesExpressions.Contains(propertyExpression.ToString()))
{
throw new ArgumentException($"{propertyExpression.ToString()} is already being observed.",
nameof(propertyExpression));
}
else
{
_observedPropertiesExpressions.Add(propertyExpression.ToString());
}
}
#region IsActive
/// <summary>
/// Gets or sets a value indicating whether the object is active.
/// </summary>
/// <value><see langword="true" /> if the object is active; otherwise <see langword="false" />.</value>
public bool IsActive
{
get { return _isActive; }
set
{
if (_isActive != value)
{
_isActive = value;
OnIsActiveChanged();
}
}
}
/// <summary>
/// Fired if the <see cref="IsActive"/> property changes.
/// </summary>
public virtual event EventHandler IsActiveChanged;
/// <summary>
/// This raises the <see cref="DelegateCommandBase.IsActiveChanged"/> event.
/// </summary>
protected virtual void OnIsActiveChanged()
{
IsActiveChanged?.Invoke(this, EventArgs.Empty);
}
#endregion
}
}
+119
View File
@@ -0,0 +1,119 @@
using System;
using System.Linq.Expressions;
using System.Reflection;
using System.Windows.Input;
namespace _29WxOrderWpf.Commands
{
public class DelegateCommand<T> : DelegateCommandBase
{
readonly Action<T> _executeMethod;
Func<T, bool> _canExecuteMethod;
/// <summary>
/// Initializes a new instance of <see cref="DelegateCommand{T}"/>.
/// </summary>
/// <param name="executeMethod">Delegate to execute when Execute is called on the command. This can be null to just hook up a CanExecute delegate.</param>
/// <remarks><see cref="CanExecute(T)"/> will always return true.</remarks>
public DelegateCommand(Action<T> executeMethod)
: this(executeMethod, (o) => true)
{
}
/// <summary>
/// Initializes a new instance of <see cref="DelegateCommand{T}"/>.
/// </summary>
/// <param name="executeMethod">Delegate to execute when Execute is called on the command. This can be null to just hook up a CanExecute delegate.</param>
/// <param name="canExecuteMethod">Delegate to execute when CanExecute is called on the command. This can be null.</param>
/// <exception cref="ArgumentNullException">When both <paramref name="executeMethod"/> and <paramref name="canExecuteMethod"/> are <see langword="null" />.</exception>
public DelegateCommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod)
: base()
{
if (executeMethod == null || canExecuteMethod == null)
throw new ArgumentNullException(nameof(executeMethod), "Delegate Command Delegates Cannot Be Null");
TypeInfo genericTypeInfo = typeof(T).GetTypeInfo();
// DelegateCommand allows object or Nullable<>.
// note: Nullable<> is a struct so we cannot use a class constraint.
if (genericTypeInfo.IsValueType)
{
if (!genericTypeInfo.IsGenericType || !typeof(Nullable<>).GetTypeInfo().IsAssignableFrom(genericTypeInfo.GetGenericTypeDefinition().GetTypeInfo()))
{
throw new InvalidCastException("Delegate CommandInvalid Generic Payload Type");
}
}
_executeMethod = executeMethod;
_canExecuteMethod = canExecuteMethod;
}
///<summary>
///Executes the command and invokes the <see cref="Action{T}"/> provided during construction.
///</summary>
///<param name="parameter">Data used by the command.</param>
public void Execute(T parameter)
{
_executeMethod(parameter);
}
///<summary>
///Determines if the command can execute by invoked the <see cref="Func{T,Bool}"/> provided during construction.
///</summary>
///<param name="parameter">Data used by the command to determine if it can execute.</param>
///<returns>
///<see langword="true" /> if this command can be executed; otherwise, <see langword="false" />.
///</returns>
public bool CanExecute(T parameter)
{
return _canExecuteMethod(parameter);
}
/// <summary>
/// Handle the internal invocation of <see cref="ICommand.Execute(object)"/>
/// </summary>
/// <param name="parameter">Command Parameter</param>
protected override void Execute(object parameter)
{
Execute((T)parameter);
}
/// <summary>
/// Handle the internal invocation of <see cref="ICommand.CanExecute(object)"/>
/// </summary>
/// <param name="parameter"></param>
/// <returns><see langword="true"/> if the Command Can Execute, otherwise <see langword="false" /></returns>
protected override bool CanExecute(object parameter)
{
return CanExecute((T)parameter);
}
/// <summary>
/// Observes a property that implements INotifyPropertyChanged, and automatically calls DelegateCommandBase.RaiseCanExecuteChanged on property changed notifications.
/// </summary>
/// <typeparam name="TType">The type of the return value of the method that this delegate encapsulates</typeparam>
/// <param name="propertyExpression">The property expression. Example: ObservesProperty(() => PropertyName).</param>
/// <returns>The current instance of DelegateCommand</returns>
public DelegateCommand<T> ObservesProperty<TType>(Expression<Func<TType>> propertyExpression)
{
ObservesPropertyInternal(propertyExpression);
return this;
}
/// <summary>
/// Observes a property that is used to determine if this command can execute, and if it implements INotifyPropertyChanged it will automatically call DelegateCommandBase.RaiseCanExecuteChanged on property changed notifications.
/// </summary>
/// <param name="canExecuteExpression">The property expression. Example: ObservesCanExecute(() => PropertyName).</param>
/// <returns>The current instance of DelegateCommand</returns>
public DelegateCommand<T> ObservesCanExecute(Expression<Func<bool>> canExecuteExpression)
{
Expression<Func<T, bool>> expression = Expression.Lambda<Func<T, bool>>(canExecuteExpression.Body, Expression.Parameter(typeof(T), "o"));
_canExecuteMethod = expression.Compile();
ObservesPropertyInternal(canExecuteExpression);
return this;
}
}
}
+24
View File
@@ -0,0 +1,24 @@
<Window x:Class="_29WxOrderWpf.Common.DialogWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:_29WxOrderWpf.Common"
mc:Ignorable="d"
Title="{Binding Title}"
d:DesignHeight="450"
d:DesignWidth="800"
Background="#FFFFFF"
WindowStartupLocation="CenterOwner">
<Window.Resources>
<Style x:Key="DialogWindowStyle" TargetType="Window">
<Setter Property="Tag" Value="Default"/>
<Setter Property="ResizeMode" Value="NoResize"/>
<Setter Property="ShowInTaskbar" Value="False"/>
<Setter Property="WindowChrome.GlassFrameThickness" Value="-1"/>
</Style>
</Window.Resources>
<Grid>
</Grid>
</Window>
+91
View File
@@ -0,0 +1,91 @@
using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace _29WxOrderWpf.Common
{
/// <summary>
/// DialogWindow.xaml 的交互逻辑
/// </summary>
public partial class DialogWindow : Window
{
private readonly Style dialogWindowStyle;
private bool isConstructorSuccessful = false;
public DialogWindow()
{
InitializeComponent();
Owner = Application.Current.MainWindow;
isConstructorSuccessful = true;
dialogWindowStyle = (Style)TryFindResource("DialogWindowStyle");
Loaded += DialogWindow_Loaded;
}
private void DialogWindow_Loaded(object sender, RoutedEventArgs e)
{
Loaded -= DialogWindow_Loaded;
}
protected override void OnContentChanged(object oldContent, object newContent)
{
this.Style = dialogWindowStyle;
if (newContent is System.Windows.Controls.UserControl control)
{
Setter widthSetter = this.Style.Setters.OfType<Setter>().FirstOrDefault(a => a.Property == WidthProperty);
if (widthSetter == null)
{
if (double.IsNaN(control.Width))
{
this.Width = DialogWindowProperty.GetWidth(control);
}
else
{
this.Width = control.Width;
control.Width = double.NaN;
}
}
Setter heightSetter = this.Style.Setters.OfType<Setter>().FirstOrDefault(a => a.Property == HeightProperty);
if (heightSetter == null)
{
if (double.IsNaN(control.Height))
{
this.Height = DialogWindowProperty.GetHeight(control);
}
else
{
this.Height = control.Height;
control.Height = double.NaN;
}
}
ResizeMode resizeExt = DialogWindowProperty.GetResizeMode(control);
if (resizeExt != ResizeMode.CanResize)
{
this.ResizeMode = resizeExt;
}
}
base.OnContentChanged(oldContent, newContent);
}
protected override void OnStyleChanged(Style oldStyle, Style newStyle)
{
if (newStyle.TargetType == typeof(Window))
{
if (isConstructorSuccessful)
{
//dialogWindowStyle.Merge(newStyle);
}
return;
}
base.OnStyleChanged(oldStyle, newStyle);
}
}
}
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace _29WxOrderWpf.Common
{
/// <summary>
/// 弹出窗口属性值
/// </summary>
public class DialogWindowProperty
{
public static readonly DependencyProperty WindowStartupLocationProperty = DependencyProperty.RegisterAttached(
"WindowStartupLocation", typeof(WindowStartupLocation), typeof(DialogWindowProperty), new PropertyMetadata(WindowStartupLocation.CenterScreen));
public static void SetWindowStartupLocation(DependencyObject element, WindowStartupLocation value)
=> element.SetValue(WindowStartupLocationProperty, value);
public static WindowStartupLocation GetWindowStartupLocation(DependencyObject element)
=> (WindowStartupLocation)element.GetValue(WindowStartupLocationProperty);
public static readonly DependencyProperty ResizeModeProperty = DependencyProperty.RegisterAttached(
"ResizeMode", typeof(ResizeMode), typeof(DialogWindowProperty), new PropertyMetadata(ResizeMode.CanResize));
public static void SetResizeMode(DependencyObject element, ResizeMode value)
=> element.SetValue(ResizeModeProperty, value);
public static ResizeMode GetResizeMode(DependencyObject element)
=> (ResizeMode)element.GetValue(ResizeModeProperty);
public static readonly DependencyProperty WidthProperty = DependencyProperty.RegisterAttached(
"Width", typeof(double), typeof(DialogWindowProperty), new PropertyMetadata(double.NaN));
public static void SetWidth(DependencyObject element, double value)
=> element.SetValue(WidthProperty, value);
public static double GetWidth(DependencyObject element)
=> (double)element.GetValue(WidthProperty);
public static readonly DependencyProperty HeightProperty = DependencyProperty.RegisterAttached(
"Height", typeof(double), typeof(DialogWindowProperty), new PropertyMetadata(double.NaN));
public static void SetHeight(DependencyObject element, double value)
=> element.SetValue(HeightProperty, value);
public static double GetHeight(DependencyObject element)
=> (double)element.GetValue(HeightProperty);
}
}
+31
View File
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace _29WxOrderWpf.Common
{
public class Result
{
public Result()
{
}
public Result(int code, string msg)
{
Code = code;
Msg = msg;
}
public int Code { get; set; }
public string Msg { get; set; }
}
public class Result<TData> : Result
{
public TData Data { get; set; }
}
}
@@ -2,6 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:c="clr-namespace:_29WxOrderWpf.Common"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:_29WxOrderWpf.Ctrls.OrderCtrls"
xmlns:utils="clr-namespace:_29WxOrderWpf.Utils"
@@ -37,7 +38,8 @@
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<usercontrols:UcWaterMarkTextBox Grid.Column="0" x:Name="txtOrderId" Margin="0,0,8,0" Height="28" WaterMark="店铺单号" />
@@ -93,7 +95,12 @@
</Image>
<CheckBox Content="自动读单" x:Name="chkAutoReadOrder" BorderBrush="{StaticResource mainColorBrush}" FontSize="12" HorizontalAlignment="Left"
Margin="2,0" VerticalAlignment="Center" Checked="chkAutoReadBuyerOrder_CheckedChanged" Unchecked="chkAutoReadBuyerOrder_CheckedChanged" IsChecked="true"/>
</WrapPanel>
<Button c:DebugVisibility.Enable="True" Grid.Column="11" Content="利润统计" Style="{StaticResource BasePageButtonStyle}" Width="60" Height="28" Click="btn_ShowShopRevenueStatisticsView" Padding="4">
</Button>
</Grid>
<Grid x:Name="gridListHeader" Margin="8,2,8,0" Grid.Row="2">
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,17 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="Ellipse" x:Key="LineEllipse">
<Setter Property="Width" Value="7"></Setter>
<Setter Property="Height" Value="7"></Setter>
<Setter Property="Fill" Value="White"></Setter>
<Setter Property="Stroke" Value="#1677ff"></Setter>
<Setter Property="StrokeThickness" Value="2"></Setter>
<Setter Property="ToolTipService.InitialShowDelay" Value="50"></Setter>
<Setter Property="ToolTipService.BetweenShowDelay" Value="0" />
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Fill" Value="White" />
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
@@ -0,0 +1,9 @@
<UserControl x:Class="_29WxOrderWpf.Ctrls.Statistics.Charts.RevenueLineChart"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="320" d:DesignWidth="640">
<Canvas x:Name="LayoutRoot" ClipToBounds="True"/>
</UserControl>
@@ -0,0 +1,242 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
using _29WxOrderWpf.Ctrls.Statistics.Models;
namespace _29WxOrderWpf.Ctrls.Statistics.Charts
{
/// <summary>
/// 销售趋势折线图(轻量自绘,无第三方依赖)。
/// 绑定 <see cref="Points"/>(IList&lt;RevenueTrendPoint&gt;)即可,数据变化时自动重绘。
/// 鼠标悬停在数据点圆圈上时显示该点数据(日期 / 销售额 / 订单数)。
/// </summary>
public partial class RevenueLineChart : UserControl
{
public static readonly DependencyProperty PointsProperty =
DependencyProperty.Register("Points", typeof(IList<RevenueTrendPoint>), typeof(RevenueLineChart),
new PropertyMetadata(null, OnRenderPropertyChanged));
/// <summary>趋势数据点</summary>
public IList<RevenueTrendPoint> Points
{
get { return (IList<RevenueTrendPoint>)GetValue(PointsProperty); }
set { SetValue(PointsProperty, value); }
}
public static readonly DependencyProperty LineBrushProperty =
DependencyProperty.Register("LineBrush", typeof(Brush), typeof(RevenueLineChart),
new PropertyMetadata(new SolidColorBrush(Color.FromRgb(22, 119, 255)), OnRenderPropertyChanged));
/// <summary>折线颜色</summary>
public Brush LineBrush
{
get { return (Brush)GetValue(LineBrushProperty); }
set { SetValue(LineBrushProperty, value); }
}
public static readonly DependencyProperty LineThicknessProperty =
DependencyProperty.Register("LineThickness", typeof(double), typeof(RevenueLineChart),
new PropertyMetadata(2d, OnRenderPropertyChanged));
/// <summary>折线粗细</summary>
public double LineThickness
{
get { return (double)GetValue(LineThicknessProperty); }
set { SetValue(LineThicknessProperty, value); }
}
public RevenueLineChart()
{
InitializeComponent();
Loaded += delegate { Render(); };
SizeChanged += delegate { Render(); };
}
private static void OnRenderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((RevenueLineChart)d).Render();
}
private void Render()
{
LayoutRoot.Children.Clear();
var points = Points;
if (points == null || points.Count == 0 || ActualWidth <= 20 || ActualHeight <= 20)
return;
const double marginLeft = 52;
const double marginRight = 16;
const double marginTop = 18;
const double marginBottom = 30;
double plotWidth = Math.Max(10, ActualWidth - marginLeft - marginRight);
double plotHeight = Math.Max(10, ActualHeight - marginTop - marginBottom);
var values = points.Select(p => (double)p.SalesAmount).ToList();
double maxValue = values.Max();
if (maxValue <= 0) maxValue = 1;
double yMax = maxValue * 1.15;
var lineBrush = LineBrush ?? new SolidColorBrush(Color.FromRgb(22, 119, 255));
Color lineColor = lineBrush is SolidColorBrush ? ((SolidColorBrush)lineBrush).Color : Color.FromRgb(22, 119, 255);
int count = points.Count;
double stepX = count <= 1 ? 0 : plotWidth / (count - 1);
Func<int, double> getX = i => marginLeft + stepX * i;
Func<double, double> getY = v => marginTop + plotHeight * (1 - v / yMax);
// 1. 横向网格线 + Y 轴刻度
for (int k = 0; k <= 4; k++)
{
double ratio = k / 4.0;
double y = marginTop + plotHeight * ratio;
double v = yMax * (1 - ratio);
LayoutRoot.Children.Add(new Line
{
X1 = marginLeft, Y1 = y, X2 = marginLeft + plotWidth, Y2 = y,
Stroke = new SolidColorBrush(Color.FromRgb(238, 238, 238)),
StrokeThickness = 1
});
var yLabel = new TextBlock
{
Text = FormatValue(v),
FontSize = 11,
Foreground = new SolidColorBrush(Color.FromRgb(153, 153, 153)),
Width = marginLeft - 6,
TextAlignment = TextAlignment.Right
};
LayoutRoot.Children.Add(yLabel);
Canvas.SetLeft(yLabel, 0);
Canvas.SetTop(yLabel, y - 8);
}
// 2. 面积填充
if (count > 1)
{
var figure = new PathFigure { StartPoint = new Point(getX(0), getY(values[0])) };
for (int i = 1; i < count; i++)
figure.Segments.Add(new LineSegment { Point = new Point(getX(i), getY(values[i])) });
figure.Segments.Add(new LineSegment { Point = new Point(getX(count - 1), marginTop + plotHeight) });
figure.Segments.Add(new LineSegment { Point = new Point(getX(0), marginTop + plotHeight) });
figure.IsClosed = true;
var gradient = new LinearGradientBrush
{
StartPoint = new Point(0, 0),
EndPoint = new Point(0, 1)
};
gradient.GradientStops.Add(new GradientStop(Color.FromArgb(64, lineColor.R, lineColor.G, lineColor.B), 0));
gradient.GradientStops.Add(new GradientStop(Color.FromArgb(8, lineColor.R, lineColor.G, lineColor.B), 1));
LayoutRoot.Children.Add(new Path
{
Data = new PathGeometry { Figures = { figure } },
Fill = gradient
});
}
// 3. 折线
var polylinePoints = new PointCollection();
for (int i = 0; i < count; i++)
polylinePoints.Add(new Point(getX(i), getY(values[i])));
LayoutRoot.Children.Add(new Polyline
{
Points = polylinePoints,
Stroke = lineBrush,
StrokeThickness = LineThickness,
StrokeLineJoin = PenLineJoin.Round
});
// 4. 数据点(点较多时省略;悬停显示该点数据)
if (count <= 31)
{
for (int i = 0; i < count; i++)
{
var dot = new Ellipse
{
ToolTip = CreateTooltip(points[i])
};
dot. Style = (Style)FindResource("LineEllipse");
//// 悬停时高亮圆点
//dot.MouseEnter += (s, e) => dot.Fill = lineBrush;
//dot.MouseLeave += (s, e) => dot.Fill = Brushes.White;
LayoutRoot.Children.Add(dot);
Canvas.SetLeft(dot, getX(i) - 3.5);
Canvas.SetTop(dot, getY(values[i]) - 3.5);
}
}
// 5. X 轴日期刻度(最多显示 8 个)
int labelStep = Math.Max(1, (int)Math.Ceiling(count / 8.0));
for (int i = 0; i < count; i += labelStep)
{
var tb = new TextBlock
{
Text = points[i].DateText,
FontSize = 11,
Foreground = new SolidColorBrush(Color.FromRgb(153, 153, 153)),
TextAlignment = TextAlignment.Center,
Width = 60
};
LayoutRoot.Children.Add(tb);
double x = getX(i) - 30;
x = Math.Max(0, Math.Min(ActualWidth - 60, x));
Canvas.SetLeft(tb, x);
Canvas.SetTop(tb, marginTop + plotHeight + 6);
}
}
/// <summary>生成数据点悬浮提示(日期 / 销售额 / 订单数)</summary>
private static object CreateTooltip(RevenueTrendPoint p)
{
var panel = new StackPanel();
var txtBrush = new SolidColorBrush(Color.FromRgb(51, 51, 51));
panel.Children.Add(new TextBlock
{
Text = p.Date.ToString("yyyy-MM-dd"),
FontSize = 11,
FontWeight = FontWeights.SemiBold,
Foreground = txtBrush
});
panel.Children.Add(new TextBlock
{
Text = "销售额:" + FormatValue((double)p.SalesAmount) + "元",
FontSize = 11,
Foreground = txtBrush
});
panel.Children.Add(new TextBlock
{
Text = "订单数:" + p.OrderCount + " 单",
FontSize = 11,
Foreground = txtBrush
});
return new Border
{
Background = Brushes.White,
CornerRadius = new CornerRadius(4),
Padding = new Thickness(8, 5, 8, 5),
Child = panel
};
}
private static string FormatValue(double value)
{
double abs = Math.Abs(value);
if (abs >= 100000000)
return (value / 100000000d).ToString("0.#") + "亿";
if (abs >= 10000)
return (value / 10000d).ToString("0.#") + "万";
return value.ToString("#,##0");
}
}
}
@@ -0,0 +1,9 @@
<UserControl x:Class="_29WxOrderWpf.Ctrls.Statistics.Charts.RevenuePieChart"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="320" d:DesignWidth="520">
<Canvas x:Name="LayoutRoot" ClipToBounds="True"/>
</UserControl>
@@ -0,0 +1,425 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
using _29WxOrderWpf.Ctrls.Statistics.Models;
namespace _29WxOrderWpf.Ctrls.Statistics.Charts
{
/// <summary>
/// 店铺销售占比饼图(轻量自绘,无第三方依赖)。
/// 绑定 <see cref="ItemsSource"/>(IList&lt;ShopSalesProportionItem&gt;)即可,数据变化时自动重绘。
/// 设置 <see cref="MaxItems"/>(&gt;0)可只显示前 N 项,其余项合并为一个“<see cref="OtherLabel"/>”扇区(百分比为剩余总和,整体仍为 100%)。
/// 设置 <see cref="Title"/> 可在环形图中央留白处显示标题(<see cref="TitlePlacement"/> 可切换为顶部),不会遮挡扇形;
/// 图例通过 <see cref="LegendPosition"/> 可放置到 4 个角,饼图区域由独立 panel 包裹,方便整体挪动和调整大小。
/// </summary>
public partial class RevenuePieChart : UserControl
{
/// <summary>标题显示位置</summary>
public enum PieTitlePlacement
{
/// <summary>居中显示在环形图中央留白处(默认,不遮挡扇形)</summary>
Center,
/// <summary>显示在图表顶部</summary>
Top
}
/// <summary>图例显示位置(4 个角)</summary>
public enum PieLegendPosition
{
/// <summary>左上角</summary>
TopLeft,
/// <summary>右上角(默认)</summary>
TopRight,
/// <summary>左下角</summary>
BottomLeft,
/// <summary>右下角</summary>
BottomRight
}
/// <summary>待绘制扇区(含颜色)</summary>
private sealed class DisplaySlice
{
public ShopSalesProportionItem Item;
public Brush Brush;
}
/// <summary>ECharts 默认配色</summary>
private static readonly Color[] Palette =
{
Color.FromRgb(84, 112, 198),
Color.FromRgb(145, 204, 117),
Color.FromRgb(250, 200, 88),
Color.FromRgb(238, 102, 102),
Color.FromRgb(115, 192, 222),
Color.FromRgb(59, 162, 114),
Color.FromRgb(252, 132, 82),
Color.FromRgb(154, 96, 180),
Color.FromRgb(234, 124, 204)
};
/// <summary>“其他”合并项默认颜色(灰色)</summary>
private static readonly Color OtherColor = Color.FromRgb(192, 196, 204);
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IList<ShopSalesProportionItem>), typeof(RevenuePieChart),
new PropertyMetadata(null, OnRenderPropertyChanged));
/// <summary>占比数据</summary>
public IList<ShopSalesProportionItem> ItemsSource
{
get { return (IList<ShopSalesProportionItem>)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public static readonly DependencyProperty MaxItemsProperty =
DependencyProperty.Register("MaxItems", typeof(int), typeof(RevenuePieChart),
new PropertyMetadata(0, OnRenderPropertyChanged));
/// <summary>最多显示的独立扇区数(&gt;0 时其余项合并为“其他”;&lt;=0 显示全部)</summary>
public int MaxItems
{
get { return (int)GetValue(MaxItemsProperty); }
set { SetValue(MaxItemsProperty, value); }
}
public static readonly DependencyProperty OtherLabelProperty =
DependencyProperty.Register("OtherLabel", typeof(string), typeof(RevenuePieChart),
new PropertyMetadata("其他", OnRenderPropertyChanged));
/// <summary>合并项的显示名称(默认“其他”)</summary>
public string OtherLabel
{
get { return (string)GetValue(OtherLabelProperty); }
set { SetValue(OtherLabelProperty, value); }
}
public static readonly DependencyProperty OtherBrushProperty =
DependencyProperty.Register("OtherBrush", typeof(Brush), typeof(RevenuePieChart),
new PropertyMetadata(new SolidColorBrush(OtherColor), OnRenderPropertyChanged));
/// <summary>合并项的颜色(默认灰色)</summary>
public Brush OtherBrush
{
get { return (Brush)GetValue(OtherBrushProperty); }
set { SetValue(OtherBrushProperty, value); }
}
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register("Title", typeof(string), typeof(RevenuePieChart),
new PropertyMetadata(null, OnRenderPropertyChanged));
/// <summary>标题文本(空/Null 不显示)</summary>
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
public static readonly DependencyProperty TitlePlacementProperty =
DependencyProperty.Register("TitlePlacement", typeof(PieTitlePlacement), typeof(RevenuePieChart),
new PropertyMetadata(PieTitlePlacement.Center, OnRenderPropertyChanged));
/// <summary>标题显示位置(默认居中显示在环形图留白处,不遮挡扇形)</summary>
public PieTitlePlacement TitlePlacement
{
get { return (PieTitlePlacement)GetValue(TitlePlacementProperty); }
set { SetValue(TitlePlacementProperty, value); }
}
public static readonly DependencyProperty TitleFontSizeProperty =
DependencyProperty.Register("TitleFontSize", typeof(double), typeof(RevenuePieChart),
new PropertyMetadata(14d, OnRenderPropertyChanged));
/// <summary>标题字号</summary>
public double TitleFontSize
{
get { return (double)GetValue(TitleFontSizeProperty); }
set { SetValue(TitleFontSizeProperty, value); }
}
public static readonly DependencyProperty TitleBrushProperty =
DependencyProperty.Register("TitleBrush", typeof(Brush), typeof(RevenuePieChart),
new PropertyMetadata(new SolidColorBrush(Color.FromRgb(51, 51, 51)), OnRenderPropertyChanged));
/// <summary>标题颜色</summary>
public Brush TitleBrush
{
get { return (Brush)GetValue(TitleBrushProperty); }
set { SetValue(TitleBrushProperty, value); }
}
public static readonly DependencyProperty LegendPositionProperty =
DependencyProperty.Register("LegendPosition", typeof(PieLegendPosition), typeof(RevenuePieChart),
new PropertyMetadata(PieLegendPosition.TopRight, OnRenderPropertyChanged));
/// <summary>图例显示位置(默认右上角,与饼图分列放置,不遮挡)</summary>
public PieLegendPosition LegendPosition
{
get { return (PieLegendPosition)GetValue(LegendPositionProperty); }
set { SetValue(LegendPositionProperty, value); }
}
public RevenuePieChart()
{
InitializeComponent();
Loaded += delegate { Render(); };
SizeChanged += delegate { Render(); };
}
private static void OnRenderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((RevenuePieChart)d).Render();
}
private void Render()
{
LayoutRoot.Children.Clear();
var items = ItemsSource;
if (items == null || items.Count == 0 || ActualWidth <= 20 || ActualHeight <= 20)
return;
double width = ActualWidth;
double height = ActualHeight;
// 图例列宽:图例占左/右一列,饼图占另一列,天然不会互相遮挡
double legendWidth = Math.Min(210, width * 0.38);
bool legendOnLeft = LegendPosition == PieLegendPosition.TopLeft
|| LegendPosition == PieLegendPosition.BottomLeft;
double pieLeft = legendOnLeft ? legendWidth : 0;
double pieWidth = Math.Max(10, width - legendWidth);
double pieHeight = height;
// 顶部标题:先预留高度,饼图相应下移,避免遮挡
double topTitleHeight = 0;
if (TitlePlacement == PieTitlePlacement.Top && !string.IsNullOrEmpty(Title))
{
var probe = CreateTitleBlock(pieWidth);
probe.Measure(new Size(pieWidth, double.PositiveInfinity));
topTitleHeight = Math.Ceiling(probe.DesiredSize.Height) + 8;
}
// 饼图区域用独立 Canvas 包裹,方便整体挪动(SetLeft/SetTop)和修改大小(Width/Height)
var piePanel = new Canvas { Width = pieWidth, Height = pieHeight };
LayoutRoot.Children.Add(piePanel);
Canvas.SetLeft(piePanel, pieLeft);
Canvas.SetTop(piePanel, 0);
double cx = pieWidth / 2;
double cy = (topTitleHeight + pieHeight) / 2;
double radius = Math.Max(10, Math.Min(pieWidth, pieHeight - topTitleHeight) / 2 - 18);
// 0. 生成实际绘制的扇区列表(MaxItems &gt; 0 时其余项合并为“其他”)
var slices = BuildSlices(items);
// 1. 绘制扇形
double startAngle = -90; // 从正上方开始,顺时针
foreach (var slice in slices)
{
double sweep = slice.Item.Percentage / 100d * 360d;
if (sweep < 0) sweep = 0;
var brush = slice.Brush;
if (sweep >= 359.9)
{
var circle = new Ellipse
{
Width = radius * 2,
Height = radius * 2,
Fill = brush
};
piePanel.Children.Add(circle);
Canvas.SetLeft(circle, cx - radius);
Canvas.SetTop(circle, cy - radius);
}
else if (sweep > 0.01)
{
piePanel.Children.Add(CreateSlice(cx, cy, radius, startAngle, sweep, brush));
}
startAngle += sweep;
}
// 2. 中间留白圆心
double holeRadius = radius * 0.55;
var hole = new Ellipse
{
Width = holeRadius * 2,
Height = holeRadius * 2,
Fill = Brushes.White
};
piePanel.Children.Add(hole);
Canvas.SetLeft(hole, cx - holeRadius);
Canvas.SetTop(hole, cy - holeRadius);
// 3. 标题(居中显示在环形图留白处,宽度限制在留白直径内,不遮挡扇形)
if (!string.IsNullOrEmpty(Title))
{
double maxW = TitlePlacement == PieTitlePlacement.Center
? Math.Max(20, holeRadius * 2 - 8)
: pieWidth;
var titleBlock = CreateTitleBlock(maxW);
titleBlock.Measure(new Size(maxW, double.PositiveInfinity));
double w = Math.Min(titleBlock.DesiredSize.Width, maxW);
double h = titleBlock.DesiredSize.Height;
piePanel.Children.Add(titleBlock);
Canvas.SetLeft(titleBlock, cx - w / 2);
Canvas.SetTop(titleBlock, TitlePlacement == PieTitlePlacement.Center ? cy - h / 2 : 0);
}
// 4. 图例(4 个角可设置,与饼图分列,不遮挡)
var legendPanel = CreateLegendPanel(slices, legendWidth - 16);
legendPanel.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
double legendW = legendPanel.DesiredSize.Width;
double legendH = legendPanel.DesiredSize.Height;
const double padding = 8;
double lx, ly;
if (LegendPosition == PieLegendPosition.TopLeft || LegendPosition == PieLegendPosition.TopRight)
ly = padding;
else
ly = Math.Max(padding, height - legendH - padding);
lx = legendOnLeft ? padding : width - legendW - padding;
LayoutRoot.Children.Add(legendPanel);
Canvas.SetLeft(legendPanel, lx);
Canvas.SetTop(legendPanel, ly);
}
/// <summary>
/// 根据 <see cref="MaxItems"/> 生成实际绘制的扇区列表:
/// 按占比从大到小取前 N 项单独显示,其余项合并为一个“其他”扇区(百分比为剩余总和,整体仍为 100%)。
/// </summary>
private List<DisplaySlice> BuildSlices(IList<ShopSalesProportionItem> items)
{
var result = new List<DisplaySlice>();
if (items == null || items.Count == 0)
return result;
var ordered = items
.OrderByDescending(i => i.Percentage)
.ThenByDescending(i => i.SalesAmount)
.ToList();
int max = MaxItems > 0 ? MaxItems : ordered.Count;
if (max >= ordered.Count)
{
for (int i = 0; i < ordered.Count; i++)
result.Add(new DisplaySlice
{
Item = ordered[i],
Brush = new SolidColorBrush(Palette[i % Palette.Length])
});
return result;
}
for (int i = 0; i < max; i++)
result.Add(new DisplaySlice
{
Item = ordered[i],
Brush = new SolidColorBrush(Palette[i % Palette.Length])
});
var rest = ordered.Skip(max).ToList();
result.Add(new DisplaySlice
{
Item = new ShopSalesProportionItem
{
ShopName = string.IsNullOrEmpty(OtherLabel) ? "其他" : OtherLabel,
Percentage = rest.Sum(r => r.Percentage),
SalesAmount = rest.Sum(r => r.SalesAmount)
},
Brush = OtherBrush ?? new SolidColorBrush(OtherColor)
});
return result;
}
private TextBlock CreateTitleBlock(double maxWidth)
{
var brush = TitleBrush ?? new SolidColorBrush(Color.FromRgb(51, 51, 51));
return new TextBlock
{
Text = Title,
FontSize = TitleFontSize,
FontWeight = FontWeights.Bold,
Foreground = brush,
TextAlignment = TextAlignment.Center,
TextWrapping = TextWrapping.Wrap,
TextTrimming = TextTrimming.CharacterEllipsis,
MaxWidth = maxWidth
};
}
private static StackPanel CreateLegendPanel(IList<DisplaySlice> slices, double maxWidth)
{
double maxLegendWidth = Math.Max(60, maxWidth);
var legendPanel = new StackPanel { MaxWidth = maxLegendWidth };
foreach (var slice in slices)
{
var legendRow = new StackPanel
{
Orientation = Orientation.Horizontal,
Margin = new Thickness(0, 4, 0, 4)
};
legendRow.Children.Add(new Rectangle
{
Width = 12,
Height = 12,
Fill = slice.Brush
});
var text = new TextBlock
{
Text = slice.Item.ShopName + " " + slice.Item.Percentage.ToString("0.0") + "%",
FontSize = 12,
Foreground = new SolidColorBrush(Color.FromRgb(102, 102, 102)),
Margin = new Thickness(6, 0, 0, 0),
VerticalAlignment = VerticalAlignment.Center,
MaxWidth = Math.Max(40, maxLegendWidth - 18),
TextTrimming = TextTrimming.CharacterEllipsis
};
legendRow.Children.Add(text);
legendPanel.Children.Add(legendRow);
}
return legendPanel;
}
private static Path CreateSlice(double cx, double cy, double radius, double startAngle, double sweep, Brush fill)
{
var start = PointOnCircle(cx, cy, radius, startAngle);
var end = PointOnCircle(cx, cy, radius, startAngle + sweep);
var figure = new PathFigure { StartPoint = new Point(cx, cy), IsClosed = true, IsFilled = true };
figure.Segments.Add(new LineSegment { Point = start });
figure.Segments.Add(new ArcSegment
{
Point = end,
Size = new Size(radius, radius),
IsLargeArc = sweep > 180,
SweepDirection = SweepDirection.Clockwise
});
return new Path
{
Data = new PathGeometry { Figures = { figure } },
Fill = fill,
Stroke = Brushes.White,
StrokeThickness = 1
};
}
private static Point PointOnCircle(double cx, double cy, double radius, double angle)
{
double rad = angle * Math.PI / 180d;
return new Point(cx + radius * Math.Cos(rad), cy + radius * Math.Sin(rad));
}
}
}
@@ -0,0 +1,8 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Charts\Chart.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
@@ -0,0 +1,256 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
namespace _29WxOrderWpf.Ctrls.Statistics.Models
{
/// <summary>
/// 店铺营收统计 - 查询参数
/// </summary>
public class ShopRevenueStatisticsQuery
{
/// <summary>开始时间</summary>
public DateTime StartDate { get; set; }
/// <summary>结束时间</summary>
public DateTime EndDate { get; set; }
/// <summary>店铺 app_key(A.app_key),空/Null 表示全部店铺</summary>
public string ShopKey { get; set; }
}
/// <summary>
/// 店铺营收统计 - 接口返回的原始明细行。
/// 对应接口返回字段:
/// A.app_key, B.create_time, B.order_id, B.status,
/// B.product_price, B.order_price, B.purchase_price, B.freight,
/// B.predict_commission_fee, B.commission_amount, B.discounted_price, B.profit,
/// B.purchase_status, c.refund_status
/// 其中 A 为店铺表,B 为订单表,c 为售后/退款表。
/// 金额字段单位均为“分”,展示时需除以 100。
/// </summary>
public class ShopRevenueStatisticsRow
{
/// <summary>A.app_key 店铺 app_key</summary>
[JsonProperty("app_key")]
public string AppKey { get; set; }
/// <summary>B.create_time 订单创建时间(接口为秒级时间戳,由 UnixDateTimeConverter 转换为 DateTime)</summary>
[JsonProperty("create_time")]
[JsonConverter(typeof(UnixDateTimeConverter))]
public DateTime CreateTime { get; set; }
/// <summary>B.order_id 订单号</summary>
[JsonProperty("order_id")]
public string OrderId { get; set; }
/// <summary>B.status 订单状态:10-待付款;20-待发货;21-部分发货;30-待收货;100-完成;200-全部商品售后之后订单取消;250-未付款用户主动取消或超时未付款订单自动取消</summary>
public int Status { get; set; }
/// <summary>B.product_price 商品总价(分)</summary>
[JsonProperty("product_price")]
public int ProductPrice { get; set; }
/// <summary>B.order_price 订单金额(分),计算公式:order_price = original_order_price - discounted_price - deduction_price - change_down_price</summary>
[JsonProperty("order_price")]
public int OrderPrice { get; set; }
/// <summary>B.purchase_price 采购价(分)</summary>
[JsonProperty("purchase_price")]
public int PurchasePrice { get; set; }
/// <summary>B.freight 运费(分)</summary>
public int Freight { get; set; }
/// <summary>B.predict_commission_fee 平台服务费(微信小店:预计技术服务费)(分)</summary>
[JsonProperty("predict_commission_fee")]
public int PredictCommissionFee { get; set; }
/// <summary>B.commission_amount 达人佣金(微信小店:分账金额)(分)</summary>
[JsonProperty("commission_amount")]
public int CommissionAmount { get; set; }
/// <summary>B.discounted_price 优惠券优惠金额(分)</summary>
[JsonProperty("discounted_price")]
public int DiscountedPrice { get; set; }
/// <summary>B.profit 利润(分),计算公式:实付金额 - 采购价 - 平台服务费 - 达人佣金(采购价、达人佣金为空值则按 0)</summary>
public int Profit { get; set; }
/// <summary>B.purchase_status 采购状态:0-未采购;1-采购(默认值 0,推送仓库时修改为 1)</summary>
[JsonProperty("purchase_status")]
public int PurchaseStatus { get; set; }
/// <summary>c.refund_status 退款状态(4/11 视为已退款,与 RevenueStatisticsAggregator.IsRefunded 保持一致)</summary>
[JsonProperty("refund_status")]
public int RefundStatus { get; set; }
/// <summary>店铺名称(由 app_key 关联店铺表得到,展示用,非接口原始字段)</summary>
[JsonProperty("shop_name")]
public string ShopName { get; set; }
}
/// <summary>
/// 顶部汇总卡片数据(金额单位:元)
/// </summary>
public class ShopRevenueSummary
{
/// <summary>销售额(最终有效销售额)</summary>
public decimal SalesAmount { get; set; }
/// <summary>有效订单数(已扣除退款订单)</summary>
public int ValidOrderCount { get; set; }
/// <summary>利润(实际利润)</summary>
public decimal Profit { get; set; }
/// <summary>退款金额(统计期间退款)</summary>
public decimal RefundAmount { get; set; }
/// <summary>商品销量(有效销量)</summary>
public int ProductSalesVolume { get; set; }
/// <summary>客户数(成交客户)</summary>
public int CustomerCount { get; set; }
/// <summary>客单价(平均每单金额)</summary>
public decimal AvgOrderAmount { get; set; }
/// <summary>退款率(退款订单占比,百分数,如 2.31 表示 2.31%)</summary>
public decimal RefundRate { get; set; }
}
/// <summary>
/// 销售趋势数据点(金额单位:元)
/// </summary>
public class RevenueTrendPoint
{
/// <summary>日期</summary>
public DateTime Date { get; set; }
/// <summary>日期文本(MM-dd)</summary>
public string DateText { get { return Date.ToString("MM-dd"); } }
/// <summary>当日销售额(元)</summary>
public decimal SalesAmount { get; set; }
/// <summary>当日有效订单数</summary>
public int OrderCount { get; set; }
}
/// <summary>
/// 店铺销售占比数据(金额单位:元)
/// </summary>
public class ShopSalesProportionItem
{
/// <summary>店铺名称</summary>
public string ShopName { get; set; }
/// <summary>销售额(元)</summary>
public decimal SalesAmount { get; set; }
/// <summary>占比(0~100)</summary>
public double Percentage { get; set; }
}
/// <summary>
/// 店铺统计表格行(金额单位:元)
/// </summary>
public class ShopStatisticsItem
{
/// <summary>店铺名称</summary>
public string ShopName { get; set; }
/// <summary>订单数</summary>
public int OrderCount { get; set; }
/// <summary>销售额(元)</summary>
public decimal SalesAmount { get; set; }
/// <summary>利润(元)</summary>
public decimal Profit { get; set; }
/// <summary>退款金额(元)</summary>
public decimal RefundAmount { get; set; }
/// <summary>客户数</summary>
public int CustomerCount { get; set; }
/// <summary>客单价(元)</summary>
public decimal AvgOrderAmount { get; set; }
}
/// <summary>
/// 店铺下拉选项
/// </summary>
public class ShopOption
{
/// <summary>店铺 app_key(A.app_key)</summary>
public string AppKey { get; set; }
/// <summary>店铺名称</summary>
public string ShopName { get; set; }
}
/// <summary>
/// 时间范围下拉选项
/// </summary>
public class TimeRangeOption
{
/// <summary>显示文本(如:今天、昨天、本周、本月、最近10天、自定义)</summary>
public string Text { get; set; }
/// <summary>值(today / yesterday / week / month / last10 / custom)</summary>
public string Value { get; set; }
}
class UnixDateTimeConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTime);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// 读取数值(支持 long 和 int 类型)
if (reader.TokenType == JsonToken.Integer)
{
long milliseconds = (long)reader.Value;
// Unix 纪元:1970-01-01 UTC
DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
if (milliseconds.ToString().Length == 13)
{
return epoch.AddMilliseconds(milliseconds);
}
return epoch.AddSeconds(milliseconds);
}
// 如果传入的是字符串(如 "1700000000000"),也支持
else if (reader.TokenType == JsonToken.String)
{
if (long.TryParse((string)reader.Value, out long milliseconds))
{
DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
if (milliseconds.ToString().Length == 13)
{
return epoch.AddMilliseconds(milliseconds);
}
return epoch.AddSeconds(milliseconds);
}
}
throw new JsonSerializationException($"无法将 {reader.Value} 转换为 DateTime");
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// 序列化时转回时间戳
DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
long milliseconds = (long)((DateTime)value - epoch).TotalMilliseconds;
writer.WriteValue(milliseconds);
}
}
}
@@ -0,0 +1,443 @@
using _29WxOrderWpf.Common;
using _29WxOrderWpf.Ctrls.Statistics.Models;
using AllBase;
using AllBase.Helper;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Work;
namespace _29WxOrderWpf.Ctrls.Statistics.Services
{
/// <summary>
/// 店铺营收统计 - 数据服务调用接口。
/// 页面只依赖此接口,接入真实数据源时新建实现类(如对接 HTTP 接口 / SQL 查询),
/// 并通过 ShopRevenueStatisticsViewModel 构造函数注入即可,无需改动页面。
/// </summary>
public interface IShopRevenueStatisticsService
{
/// <summary>
/// 查询原始统计明细(接口返回的 A.* / B.* / c.* 原始字段)。
/// 真实实现通常通过 SQL / HTTP 一次性返回这些行,再由聚合器计算。
/// </summary>
Task<List<ShopRevenueStatisticsRow>> QueryRowsAsync(ShopRevenueStatisticsQuery query);
/// <summary>获取顶部汇总卡片数据</summary>
Task<ShopRevenueSummary> GetSummaryAsync(ShopRevenueStatisticsQuery query);
/// <summary>获取销售趋势(按天)</summary>
Task<List<RevenueTrendPoint>> GetSalesTrendAsync(ShopRevenueStatisticsQuery query);
/// <summary>获取店铺销售占比(饼图)</summary>
Task<List<ShopSalesProportionItem>> GetShopSalesProportionAsync(ShopRevenueStatisticsQuery query);
/// <summary>获取店铺统计表格</summary>
Task<List<ShopStatisticsItem>> GetShopStatisticsAsync(ShopRevenueStatisticsQuery query);
/// <summary>获取店铺下拉列表(用于查询条件)</summary>
Task<List<ShopOption>> GetShopOptionsAsync();
}
/// <summary>
/// 统计聚合器:把接口返回的原始明细行聚合成页面所需的汇总/趋势/占比/表格数据。
/// 聚合口径说明(依据接口字段描述):
/// - 订单状态 status:10-待付款;20-待发货;21-部分发货;30-待收货;100-完成;
/// 200-全部商品售后之后订单取消;250-未付款用户主动取消或超时未付款订单自动取消。
/// - 有效订单:已付款(20/21/30/100)且未退款;10/250 未付款不计入销售额。
/// - 已退款:c.refund_status 为 4/11,或订单状态为 200(全部商品售后后取消)。
/// - 金额字段单位均为“分”,除以 100 转为元。
/// </summary>
public static class RevenueStatisticsAggregator
{
/// <summary>订单状态 - 待付款</summary>
public const int StatusPendingPay = 10;
/// <summary>订单状态 - 待发货</summary>
public const int StatusWaitSend = 20;
/// <summary>订单状态 - 部分发货</summary>
public const int StatusPartSend = 21;
/// <summary>订单状态 - 待收货</summary>
public const int StatusWaitReceive = 30;
/// <summary>订单状态 - 完成</summary>
public const int StatusFinished = 100;
/// <summary>订单状态 - 全部商品售后之后订单取消</summary>
public const int StatusCancelAfterAfterSale = 200;
/// <summary>订单状态 - 未付款用户主动取消/超时未付款自动取消</summary>
public const int StatusCancelNotPay = 250;
/// <summary>是否已付款的订单(20/21/30/100;10 待付款、250 未付款取消不计入)</summary>
public static bool IsPaidOrder(ShopRevenueStatisticsRow row)
{
if (row == null) return false;
return row.Status == StatusWaitSend
|| row.Status == StatusPartSend
|| row.Status == StatusWaitReceive
|| row.Status == StatusFinished;
}
/// <summary>是否已退款(c.refund_status 为 4/11,或订单状态为 200 全部商品售后后取消)</summary>
public static bool IsRefunded(ShopRevenueStatisticsRow row)
{
if (row == null) return false;
return row.RefundStatus == 4 || row.RefundStatus == 11 || row.Status == StatusCancelAfterAfterSale;
}
/// <summary>是否有效订单(已付款且未退款)</summary>
public static bool IsValidOrder(ShopRevenueStatisticsRow row)
{
return IsPaidOrder(row) && !IsRefunded(row);
}
/// <summary>汇总卡片</summary>
public static ShopRevenueSummary GetSummary(IEnumerable<ShopRevenueStatisticsRow> rows)
{
var list = (rows ?? new List<ShopRevenueStatisticsRow>()).ToList();
var valid = list.Where(IsValidOrder).ToList();
var refunded = list.Where(IsRefunded).ToList();
decimal salesAmount = valid.Sum(r => (decimal)r.OrderPrice) / 100m;
decimal profit = valid.Sum(r => (decimal)r.Profit) / 100m;
decimal refundAmount = refunded.Sum(r => (decimal)r.OrderPrice) / 100m;
int validOrderCount = valid.Select(r => r.OrderId).Distinct().Count();
// 总订单数(退款率分母)= 已付款订单 + 已退款订单(含售后取消 200),不含未付款的 10/250
int totalOrderCount = list.Where(r => IsPaidOrder(r) || IsRefunded(r)).Select(r => r.OrderId).Distinct().Count();
int refundOrderCount = refunded.Select(r => r.OrderId).Distinct().Count();
return new ShopRevenueSummary
{
SalesAmount = salesAmount,
ValidOrderCount = validOrderCount,
Profit = profit,
RefundAmount = refundAmount,
ProductSalesVolume = valid.Count,
CustomerCount = validOrderCount,
AvgOrderAmount = validOrderCount == 0 ? 0m : salesAmount / validOrderCount,
RefundRate = totalOrderCount == 0 ? 0m : (decimal)refundOrderCount * 100m / totalOrderCount
};
}
/// <summary>销售趋势(按天,缺失日期补 0,仅统计有效订单)</summary>
public static List<RevenueTrendPoint> GetSalesTrend(IEnumerable<ShopRevenueStatisticsRow> rows, DateTime startDate, DateTime endDate)
{
var list = (rows ?? new List<ShopRevenueStatisticsRow>()).Where(IsValidOrder).ToList();
var map = list
.GroupBy(r => r.CreateTime.Date)
.ToDictionary(
g => g.Key,
g => new RevenueTrendPoint
{
Date = g.Key,
SalesAmount = g.Sum(r => (decimal)r.OrderPrice) / 100m,
OrderCount = g.Select(r => r.OrderId).Distinct().Count()
});
var result = new List<RevenueTrendPoint>();
for (var d = startDate.Date; d <= endDate.Date; d = d.AddDays(1))
{
if (map.TryGetValue(d, out var point))
result.Add(point);
else
result.Add(new RevenueTrendPoint { Date = d, SalesAmount = 0m, OrderCount = 0 });
}
return result;
}
/// <summary>店铺销售占比(饼图,仅统计有效订单)</summary>
public static List<ShopSalesProportionItem> GetShopSalesProportion(IEnumerable<ShopRevenueStatisticsRow> rows)
{
var list = (rows ?? new List<ShopRevenueStatisticsRow>()).Where(IsValidOrder).ToList();
decimal total = list.Sum(r => (decimal)r.OrderPrice) / 100m;
return list
.GroupBy(r => r.ShopName)
.Select(g => new ShopSalesProportionItem
{
ShopName = string.IsNullOrEmpty(g.Key) ? "未知店铺" : g.Key,
SalesAmount = g.Sum(r => (decimal)r.OrderPrice) / 100m,
Percentage = total == 0m ? 0d : (double)(g.Sum(r => (decimal)r.OrderPrice) / 100m / total * 100m)
})
.OrderByDescending(i => i.SalesAmount)
.ToList();
}
/// <summary>店铺统计表格</summary>
public static List<ShopStatisticsItem> GetShopStatistics(IEnumerable<ShopRevenueStatisticsRow> rows)
{
var list = (rows ?? new List<ShopRevenueStatisticsRow>()).ToList();
return list
.GroupBy(r => r.ShopName)
.Select(g =>
{
var valid = g.Where(IsValidOrder).ToList();
var refunded = g.Where(IsRefunded).ToList();
decimal salesAmount = valid.Sum(r => (decimal)r.OrderPrice) / 100m;
int orderCount = valid.Select(r => r.OrderId).Distinct().Count();
return new ShopStatisticsItem
{
ShopName = string.IsNullOrEmpty(g.Key) ? "未知店铺" : g.Key,
OrderCount = orderCount,
SalesAmount = salesAmount,
Profit = valid.Sum(r => (decimal)r.Profit) / 100m,
RefundAmount = refunded.Sum(r => (decimal)r.OrderPrice) / 100m,
CustomerCount = orderCount,
AvgOrderAmount = orderCount == 0 ? 0m : salesAmount / orderCount
};
})
.OrderByDescending(i => i.SalesAmount)
.ToList();
}
}
/// <summary>
/// 默认实现:返回模拟数据,用于页面演示/联调。
/// 接入真实接口时,新建实现类并注入即可(见 IShopRevenueStatisticsService 注释)。
/// </summary>
public class ShopRevenueStatisticsService : IShopRevenueStatisticsService
{
/// <summary>
/// 明细数据缓存。
/// key = 店铺id|yyyyMMdd(店铺 × 天),value = 该店铺当天的明细行。
/// 查询按“店铺 × 日期”命中;完全未命中时所有店铺 + 整个时间范围一次请求,
/// 部分命中时只请求缺失的店铺/日期段。
/// </summary>
private readonly Dictionary<string, List<ShopRevenueStatisticsRow>> _rowCache =
new Dictionary<string, List<ShopRevenueStatisticsRow>>();
private readonly object _cacheLock = new object();
private sealed class DateRange
{
public DateTime Start;
public DateTime End;
}
private static string CacheKey(string shopId, DateTime day)
{
return shopId + "|" + day.ToString("yyyyMMdd");
}
public async Task<List<ShopRevenueStatisticsRow>> QueryRowsAsync(ShopRevenueStatisticsQuery query)
{
// 1. 展开本次查询涉及的店铺(ShopKey 为空 = 全部店铺)
List<string> shopIds;
if (string.IsNullOrEmpty(query.ShopKey))
shopIds = (await GetShopOptionsAsync()).Select(a => a.AppKey).ToList();
else
shopIds = new List<string> { query.ShopKey };
if (shopIds.Count == 0)
return new List<ShopRevenueStatisticsRow>();
var startDay = query.StartDate.Date;
var endDay = query.EndDate.Date;
int totalDays = (int)(endDay - startDay).Days + 1;
// 2. 命中检测:按 店铺 × 天 找出缺失项
var missingByShop = new Dictionary<string, List<DateTime>>();
int missingCount = 0;
lock (_cacheLock)
{
foreach (var shopId in shopIds)
{
var missingDays = new List<DateTime>();
for (var d = startDay; d <= endDay; d = d.AddDays(1))
{
if (!_rowCache.ContainsKey(CacheKey(shopId, d)))
missingDays.Add(d);
}
missingCount += missingDays.Count;
missingByShop[shopId] = missingDays;
}
}
// 3. 全部命中:直接从缓存拼装返回
if (missingCount == 0)
return GetCachedRows(shopIds, startDay, endDay);
// 4. 完全未命中:所有店铺 + 整个时间范围一次请求
if (missingCount == shopIds.Count * totalDays)
{
var allRows = await RequestRowsAsync(shopIds, startDay, endDay);
CacheResponse(shopIds, startDay, endDay, allRows, null);
var cached = GetCachedRows(shopIds, startDay, endDay);
// 若接口行内未返回 app_key 导致无法入库,则直接返回本次请求结果
return cached.Count > 0 ? cached : allRows;
}
// 5. 部分命中:只请求未命中的(店铺 × 连续缺失日期段)
foreach (var pair in missingByShop)
{
if (pair.Value.Count == 0)
continue;
foreach (var run in BuildRuns(pair.Value))
{
var rows = await RequestRowsAsync(new List<string> { pair.Key }, run.Start, run.End);
CacheResponse(new List<string> { pair.Key }, run.Start, run.End, rows, pair.Key);
}
}
return GetCachedRows(shopIds, startDay, endDay);
}
/// <summary>按 店铺 × 日期 从缓存拼装结果</summary>
private List<ShopRevenueStatisticsRow> GetCachedRows(List<string> shopIds, DateTime startDate, DateTime endDate)
{
var result = new List<ShopRevenueStatisticsRow>();
lock (_cacheLock)
{
for (var d = startDate.Date; d <= endDate.Date; d = d.AddDays(1))
{
foreach (var shopId in shopIds)
{
if (_rowCache.TryGetValue(CacheKey(shopId, d), out var list))
result.AddRange(list);
}
}
}
return result;
}
/// <summary>请求接口一次(可传多个店铺),返回原始明细行</summary>
private async Task<List<ShopRevenueStatisticsRow>> RequestRowsAsync(List<string> shopIds, DateTime startDate, DateTime endDate)
{
var url = BaseClass.Build29WxObjString("shop_revenue_statistics/get_shop_order_amount");
var sb = new StringBuilder();
sb.Append(UserLogin.GetSignaturex());
sb.Append("&shop_ids=").Append(string.Join(",", shopIds));
sb.Append("&start_time=").Append(BaseClass.ConvertDateTimeLong(startDate.Date));
sb.Append("&end_time=").Append(BaseClass.ConvertDateTimeLong(endDate.Date.AddDays(1).AddTicks(-1)));
var html = await GetHtml.PostUtf8Async(url, sb.ToString());
var ret = JsonConvert.DeserializeObject<Result<List<ShopRevenueStatisticsRow>>>(html, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
if (ret.Code != 200)
{
throw new Exception(ret.Msg);
}
return ret.Data ?? new List<ShopRevenueStatisticsRow>();
}
/// <summary>
/// 把一次请求的结果写入缓存(按 店铺id|日期 分桶)。
/// 除有数据的 店铺×天 外,会把本次请求覆盖但无数据的 店铺×天 也标记为已缓存(空列表),
/// 避免“该店铺当天无数据”被反复判定为未命中而重复请求数据库。
/// </summary>
private void CacheResponse(List<string> shopIds, DateTime startDate, DateTime endDate, List<ShopRevenueStatisticsRow> rows, string fallbackShopId)
{
if (rows == null)
rows = new List<ShopRevenueStatisticsRow>();
// 能否按 店铺×天 归属:
// 单店铺请求一定可以(用请求的店铺兜底);多店铺请求要求行内带 app_key;
// 响应为空时也视为可归属(说明覆盖的 店铺×天 都无数据)。
bool canAttribute = shopIds.Count == 1
|| rows.Count == 0
|| rows.Any(r => !string.IsNullOrEmpty(r.AppKey));
lock (_cacheLock)
{
if (canAttribute)
{
// 覆盖的 店铺×天 全部落缓存键(无数据的天记为空的已缓存)
for (var d = startDate.Date; d <= endDate.Date; d = d.AddDays(1))
{
foreach (var shopId in shopIds)
{
var key = CacheKey(shopId, d);
if (!_rowCache.TryGetValue(key, out var list))
{
list = new List<ShopRevenueStatisticsRow>();
_rowCache[key] = list;
}
}
}
}
// 有数据的行按 店铺×天 追加
foreach (var row in rows)
{
// 行内未带 app_key 时,用本次请求的店铺兜底
var shopId = string.IsNullOrEmpty(row.AppKey) ? fallbackShopId : row.AppKey;
if (string.IsNullOrEmpty(shopId))
continue;
var key = CacheKey(shopId, row.CreateTime.Date);
if (!_rowCache.TryGetValue(key, out var list))
{
list = new List<ShopRevenueStatisticsRow>();
_rowCache[key] = list;
}
list.Add(row);
}
}
}
/// <summary>把缺失日期合并为连续日期段(每段一次请求)</summary>
private static List<DateRange> BuildRuns(List<DateTime> missingDays)
{
var sorted = missingDays.OrderBy(d => d).ToList();
var runs = new List<DateRange>();
int i = 0;
while (i < sorted.Count)
{
var start = sorted[i];
var end = start;
i++;
while (i < sorted.Count && sorted[i] == end.AddDays(1))
{
end = sorted[i];
i++;
}
runs.Add(new DateRange { Start = start, End = end });
}
return runs;
}
public async Task<ShopRevenueSummary> GetSummaryAsync(ShopRevenueStatisticsQuery query)
{
var rows = await QueryRowsAsync(query);
return RevenueStatisticsAggregator.GetSummary(rows);
}
public async Task<List<RevenueTrendPoint>> GetSalesTrendAsync(ShopRevenueStatisticsQuery query)
{
var rows = await QueryRowsAsync(query);
return RevenueStatisticsAggregator.GetSalesTrend(rows, query.StartDate, query.EndDate);
}
public async Task<List<ShopSalesProportionItem>> GetShopSalesProportionAsync(ShopRevenueStatisticsQuery query)
{
var rows = await QueryRowsAsync(query);
return RevenueStatisticsAggregator.GetShopSalesProportion(rows);
}
public async Task<List<ShopStatisticsItem>> GetShopStatisticsAsync(ShopRevenueStatisticsQuery query)
{
var rows = await QueryRowsAsync(query);
return RevenueStatisticsAggregator.GetShopStatistics(rows);
}
public Task<List<ShopOption>> GetShopOptionsAsync()
{
var list = GlobalConfig.ShopLoginList.Select(a =>
new ShopOption()
{
AppKey = a.ShopId,
ShopName = a.ShopAccount
}).ToList();
return Task.FromResult(list);
}
}
}
@@ -0,0 +1,455 @@
<UserControl
x:Class="_29WxOrderWpf.Ctrls.Statistics.ShopRevenueStatisticsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:charts="clr-namespace:_29WxOrderWpf.Ctrls.Statistics.Charts"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:_29WxOrderWpf.Ctrls.Statistics.ViewModels"
xmlns:wpf="clr-namespace:_29WxOrderWpf.Common"
d:Background="#F5F7FB"
d:DataContext="{d:DesignInstance Type=vm:ShopRevenueStatisticsViewModel,IsDesignTimeCreatable=True}"
d:DesignHeight="900"
d:DesignWidth="1600"
wpf:DialogWindowProperty.ResizeMode="CanResizeWithGrip"
mc:Ignorable="d">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="boolToVisibilityConverter" />
<!-- 卡片样式(白底、圆角、阴影) -->
<Style x:Key="CardBorderStyle" TargetType="Border">
<Setter Property="Background" Value="White" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="18" />
<Setter Property="Margin" Value="4" />
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect
BlurRadius="10"
Opacity="0.08"
ShadowDepth="2"
Color="#000000" />
</Setter.Value>
</Setter>
</Style>
<!-- 表格单元格文字居中 -->
<Style x:Key="CenterCellTextStyle" TargetType="TextBlock">
<Setter Property="TextAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<!-- 面板标题 -->
<Style x:Key="PanelTitleStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="16" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Foreground" Value="#333333" />
</Style>
</UserControl.Resources>
<Grid Background="#F5F7FB">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="12">
<!-- ============ 查询条件工具栏 ============ -->
<Border
Margin="6,6,6,0"
Padding="14,10"
Style="{StaticResource CardBorderStyle}">
<WrapPanel>
<TextBlock
Margin="6,0"
VerticalAlignment="Center"
FontSize="14"
Foreground="#666666"
Text="时间" />
<ComboBox
Width="110"
Height="32"
VerticalContentAlignment="Center"
DisplayMemberPath="Text"
ItemsSource="{Binding TimeRangeOptions}"
SelectedItem="{Binding SelectedTimeRange, Mode=TwoWay}"
Style="{StaticResource BaseComboBoxStyle}" />
<TextBlock
Margin="14,0,6,0"
VerticalAlignment="Center"
FontSize="14"
Foreground="#666666"
Text="日期" />
<DatePicker
Height="32"
VerticalContentAlignment="Center"
SelectedDate="{Binding StartDate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock
Margin="6,0"
VerticalAlignment="Center"
FontSize="14"
Foreground="#666666"
Text="~" />
<DatePicker
Height="32"
VerticalContentAlignment="Center"
SelectedDate="{Binding EndDate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock
Margin="14,0,6,0"
VerticalAlignment="Center"
FontSize="14"
Foreground="#666666"
Text="店铺" />
<ComboBox
Width="130"
Height="32"
VerticalContentAlignment="Center"
DisplayMemberPath="ShopName"
ItemsSource="{Binding ShopOptions}"
SelectedItem="{Binding SelectedShop, Mode=TwoWay}"
Style="{StaticResource BaseComboBoxStyle}" />
<Button
Height="32"
Margin="18,0,0,0"
Padding="16,0"
Command="{Binding QueryCommand}"
Content=" 查询 "
Style="{StaticResource BasePageButtonStyle}" />
<Button
Height="32"
Margin="8,0,0,0"
Padding="16,0"
Command="{Binding ResetCommand}"
Content=" 重置 "
Style="{StaticResource BasePageButtonLightStyle}" />
</WrapPanel>
</Border>
<!-- ============ 汇总卡片(2 行 x 4 列) ============ -->
<Grid Margin="3,8,3,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 销售额 -->
<Border
Grid.Row="0"
Grid.Column="0"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="销售额" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.SalesAmount, StringFormat={}¥{0:N2}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="最终有效销售额" />
</StackPanel>
</Border>
<!-- 有效订单 -->
<Border
Grid.Row="0"
Grid.Column="1"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="有效订单" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.ValidOrderCount, StringFormat={}{0:N0}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="已扣除退款订单" />
</StackPanel>
</Border>
<!-- 利润 -->
<Border
Grid.Row="0"
Grid.Column="2"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="利润" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.Profit, StringFormat={}¥{0:N2}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="实际利润" />
</StackPanel>
</Border>
<!-- 退款金额 -->
<Border
Grid.Row="0"
Grid.Column="3"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="退款金额" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.RefundAmount, StringFormat={}¥{0:N2}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="统计期间退款" />
</StackPanel>
</Border>
<!-- 商品销量 -->
<Border
Grid.Row="1"
Grid.Column="0"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="商品销量" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.ProductSalesVolume, StringFormat={}{0:N0}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="有效销量" />
</StackPanel>
</Border>
<!-- 客户数 -->
<Border
Grid.Row="1"
Grid.Column="1"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="客户数" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.CustomerCount, StringFormat={}{0:N0}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="成交客户" />
</StackPanel>
</Border>
<!-- 客单价 -->
<Border
Grid.Row="1"
Grid.Column="2"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="客单价" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.AvgOrderAmount, StringFormat={}¥{0:N2}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="平均每单金额" />
</StackPanel>
</Border>
<!-- 退款率 -->
<Border
Grid.Row="1"
Grid.Column="3"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="退款率" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.RefundRate, StringFormat={}{0:0.00}%}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="退款订单占比" />
</StackPanel>
</Border>
</Grid>
<!-- ============ 图表区域 ============ -->
<Grid Margin="3,8,3,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<!-- 销售趋势 -->
<Border
Grid.Column="0"
Margin="6,6,3,6"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock Style="{StaticResource PanelTitleStyle}" Text="销售趋势" />
<charts:RevenueLineChart
Height="330"
Margin="0,10,0,0"
Points="{Binding TrendData}" />
</StackPanel>
</Border>
<!-- 店铺销售占比 -->
<Border
Grid.Column="1"
Margin="3,6,6,6"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock Style="{StaticResource PanelTitleStyle}" Text="店铺销售占比 TOP 5" />
<charts:RevenuePieChart
Height="330"
Margin="0,10,0,0" MaxItems="5"
ItemsSource="{Binding ShopProportionData}" />
</StackPanel>
</Border>
</Grid>
<!-- ============ 店铺统计表格 ============ -->
<Border Margin="6,8,6,12" Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
Margin="0,0,0,10"
Style="{StaticResource PanelTitleStyle}"
Text="店铺统计" />
<DataGrid
AlternatingRowBackground="#FAFAFA"
AutoGenerateColumns="False"
Background="Transparent"
BorderThickness="0"
CanUserAddRows="False"
CanUserDeleteRows="False"
CanUserReorderColumns="False"
CanUserResizeRows="False"
ColumnHeaderStyle="{StaticResource BaseDataGridColumnHeaderStyle}"
FontSize="13"
Foreground="#333333"
GridLinesVisibility="Horizontal"
HeadersVisibility="Column"
HorizontalGridLinesBrush="#EEEEEE"
IsReadOnly="True"
ItemsSource="{Binding ShopStatistics}"
RowHeight="46"
ScrollViewer.CanContentScroll="True"
SelectionMode="Single"
SelectionUnit="FullRow"
VerticalScrollBarVisibility="Disabled">
<DataGrid.Columns>
<DataGridTextColumn
Width="*"
Binding="{Binding ShopName}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="店铺" />
<DataGridTextColumn
Width="*"
Binding="{Binding OrderCount, StringFormat={}{0:N0}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="订单数" />
<DataGridTextColumn
Width="*"
Binding="{Binding SalesAmount, StringFormat={}¥{0:N0}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="销售额" />
<DataGridTextColumn
Width="*"
Binding="{Binding Profit, StringFormat={}¥{0:N0}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="利润" />
<DataGridTextColumn
Width="*"
Binding="{Binding RefundAmount, StringFormat={}¥{0:N0}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="退款金额" />
<DataGridTextColumn
Width="*"
Binding="{Binding CustomerCount, StringFormat={}{0:N0}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="客户数" />
<DataGridTextColumn
Width="*"
Binding="{Binding AvgOrderAmount, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="客单价" />
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
<!-- ============ 加载遮罩 ============ -->
<Border Background="#66FFFFFF" Visibility="{Binding IsLoading, Converter={StaticResource boolToVisibilityConverter}}" d:Visibility="Collapsed" >
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<ProgressBar
Width="240"
Height="8"
IsIndeterminate="True" />
<TextBlock
Margin="0,12,0,0"
HorizontalAlignment="Center"
FontSize="13"
Foreground="#888888"
Text="数据加载中..." />
</StackPanel>
</Border>
</Grid>
</UserControl>
@@ -0,0 +1,25 @@
using System.Windows.Controls;
using _29WxOrderWpf.Ctrls.Statistics.ViewModels;
namespace _29WxOrderWpf.Ctrls.Statistics
{
/// <summary>
/// 店铺营收统计页面(MVVM)。
/// 页面不写业务逻辑,只负责绑定 ShopRevenueStatisticsViewModel;
/// 如需注入自定义数据服务,请使用
/// new ShopRevenueStatisticsView(new ShopRevenueStatisticsViewModel(你的 IShopRevenueStatisticsService 实现))。
/// </summary>
public partial class ShopRevenueStatisticsView : UserControl
{
public ShopRevenueStatisticsView()
: this(new ShopRevenueStatisticsViewModel())
{
}
public ShopRevenueStatisticsView(ShopRevenueStatisticsViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
}
}
@@ -0,0 +1,288 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using AllBase.Model.MvvmBaseClasses;
using _29WxOrderWpf.Ctrls.Statistics.Models;
using _29WxOrderWpf.Ctrls.Statistics.Services;
using CefSharp;
using _29WxOrderWpf.Commands;
namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
{
/// <summary>
/// 店铺营收统计 - 页面 ViewModel(MVVM)。
/// 通过 IShopRevenueStatisticsService 调用数据接口;
/// 接入真实接口时传入自定义实现:new ShopRevenueStatisticsViewModel(你的服务实现)。
/// </summary>
public class ShopRevenueStatisticsViewModel : PropertyNotifyObject
{
private readonly IShopRevenueStatisticsService _service;
private ShopRevenueSummary _summary;
private ObservableCollection<RevenueTrendPoint> _trendData;
private ObservableCollection<ShopSalesProportionItem> _shopProportionData;
private ObservableCollection<ShopStatisticsItem> _shopStatistics;
private ShopOption _selectedShop;
private string _selectedPlatform;
public ShopRevenueStatisticsViewModel()
: this(new ShopRevenueStatisticsService())
{
}
public ShopRevenueStatisticsViewModel(IShopRevenueStatisticsService service)
{
_service = service ?? new ShopRevenueStatisticsService();
TimeRangeOptions = new ObservableCollection<TimeRangeOption>
{
new TimeRangeOption { Text = "昨天", Value = "yesterday" },
new TimeRangeOption { Text = "本周", Value = "week" },
new TimeRangeOption { Text = "本月", Value = "month" },
new TimeRangeOption { Text = "最近10天", Value = "last10" },
new TimeRangeOption { Text = "自定义", Value = "custom" }
};
ShopOptions = new ObservableCollection<ShopOption>();
QueryCommand = new DelegateCommand(async () => await LoadAsync());
ResetCommand = new DelegateCommand(Reset);
InitializeAsync();
}
#region 查询条件
/// <summary>时间范围选项</summary>
public ObservableCollection<TimeRangeOption> TimeRangeOptions { get; }
/// <summary>选中的时间范围</summary>
public TimeRangeOption SelectedTimeRange
{
get { return this.GetValue(x => x.SelectedTimeRange); }
set
{
this.SetValue(x => x.SelectedTimeRange, value);
ApplyTimeRange();
OnPropertyChanged(nameof(IsCustomDateRange));
}
}
/// <summary>是否为自定义时间范围</summary>
public bool IsCustomDateRange
{
get { return SelectedTimeRange != null && SelectedTimeRange.Value == "custom"; }
}
/// <summary>开始日期</summary>
public DateTime StartDate
{
get { return this.GetValue(x => x.StartDate); }
set { this.SetValue(x => x.StartDate, value); }
}
/// <summary>结束日期</summary>
public DateTime EndDate
{
get { return this.GetValue(x => x.EndDate); }
set { this.SetValue(x => x.EndDate, value); }
}
/// <summary>店铺选项(含“全部店铺”)</summary>
public ObservableCollection<ShopOption> ShopOptions { get; }
/// <summary>选中的店铺</summary>
public ShopOption SelectedShop
{
get { return _selectedShop; }
set
{
_selectedShop = value;
OnPropertyChanged(nameof(SelectedShop));
}
}
#endregion
#region 统计结果
/// <summary>汇总卡片数据</summary>
public ShopRevenueSummary Summary
{
get { return _summary; }
private set
{
_summary = value;
OnPropertyChanged(nameof(Summary));
}
}
/// <summary>销售趋势数据</summary>
public ObservableCollection<RevenueTrendPoint> TrendData
{
get { return _trendData; }
private set
{
_trendData = value;
OnPropertyChanged(nameof(TrendData));
}
}
/// <summary>店铺销售占比数据</summary>
public ObservableCollection<ShopSalesProportionItem> ShopProportionData
{
get { return _shopProportionData; }
private set
{
_shopProportionData = value;
OnPropertyChanged(nameof(ShopProportionData));
}
}
/// <summary>店铺统计表格数据</summary>
public ObservableCollection<ShopStatisticsItem> ShopStatistics
{
get { return _shopStatistics; }
private set
{
_shopStatistics = value;
OnPropertyChanged(nameof(ShopStatistics));
}
}
/// <summary>是否正在加载</summary>
public bool IsLoading
{
get { return this.GetValue(x => x.IsLoading); }
set { this.SetValue(x => x.IsLoading, value); }
}
#endregion
#region 命令
/// <summary>查询命令</summary>
public ICommand QueryCommand { get; }
/// <summary>重置命令</summary>
public ICommand ResetCommand { get; }
#endregion
#region 初始化
private async void InitializeAsync()
{
try
{
// 店铺选项
ShopOptions.Add(new ShopOption { AppKey = string.Empty, ShopName = "全部店铺" });
var shops = await _service.GetShopOptionsAsync();
foreach (var shop in shops)
ShopOptions.Add(shop);
SelectedShop = ShopOptions.FirstOrDefault();
// 默认时间范围:本月
SelectedTimeRange = TimeRangeOptions.FirstOrDefault(t => t.Value == "month");
ApplyTimeRange();
await LoadAsync();
}
catch (Exception)
{
// 初始化失败(如接口不可用),页面保持可查询状态;可在此接入日志/提示
}
}
#endregion
#region 业务逻辑
/// <summary>根据时间范围选项计算起止日期</summary>
private void ApplyTimeRange()
{
var now = DateTime.Now;
var today = now.Date;
var selected = SelectedTimeRange == null ? string.Empty : SelectedTimeRange.Value;
switch (selected)
{
case "yesterday":
StartDate = today.AddDays(-1);
EndDate = today.AddDays(-1).AddHours(23).AddMinutes(59).AddSeconds(59);
break;
case "week":
int diff = today.DayOfWeek == DayOfWeek.Sunday ? 6 : (int)today.DayOfWeek - 1;
StartDate = today.AddDays(-diff);
EndDate = now;
break;
case "month":
StartDate = new DateTime(today.Year, today.Month, 1);
EndDate = now;
break;
case "last10":
StartDate = today.AddDays(-9);
EndDate = now;
break;
default:
// 自定义:保持用户选择的日期不变
break;
}
}
/// <summary>执行查询,刷新页面所有统计数据</summary>
private async Task LoadAsync()
{
if (StartDate > EndDate)
{
var temp = StartDate;
StartDate = EndDate;
EndDate = temp;
}
IsLoading = true;
try
{
var query = new ShopRevenueStatisticsQuery
{
StartDate = StartDate,
EndDate = EndDate,
ShopKey = SelectedShop == null ? string.Empty : SelectedShop.AppKey,
};
Summary = await _service.GetSummaryAsync(query);
var trend = await _service.GetSalesTrendAsync(query);
TrendData = new ObservableCollection<RevenueTrendPoint>(trend);
var proportion = await _service.GetShopSalesProportionAsync(query);
ShopProportionData = new ObservableCollection<ShopSalesProportionItem>(proportion);
var table = await _service.GetShopStatisticsAsync(query);
ShopStatistics = new ObservableCollection<ShopStatisticsItem>(table);
}
catch (Exception)
{
// 查询异常:保留上一次数据,可在此接入日志/错误提示
}
finally
{
IsLoading = false;
}
}
/// <summary>重置查询条件并重新加载</summary>
private async void Reset()
{
SelectedShop = ShopOptions.FirstOrDefault();
SelectedTimeRange = TimeRangeOptions.FirstOrDefault(t => t.Value == "month");
ApplyTimeRange();
await LoadAsync();
}
#endregion
}
}
+1 -1
View File
@@ -631,7 +631,7 @@ namespace AllBase
/// <returns></returns>
public static long ConvertDateTimeLong(DateTime time)
{
TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
TimeSpan ts = time - new DateTime(1970, 1, 1, 0, 0, 0, 0);
return Convert.ToInt64(ts.TotalMilliseconds);
}
+2065 -2008
View File
File diff suppressed because it is too large. Load diff