Merge branch 'main' of github.com:jtt-fan/29WxOrder

This commit is contained in:
wule committed 2026-08-06 12:04:03 +08:00
commit 4b8f6f7fb6
10 files changed
+1222 -19

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@
Title="{Binding Title}"
d:DesignHeight="450"
d:DesignWidth="800"
Background="#FFFFFF"
Background="#FFFFFF"
WindowStartupLocation="CenterOwner">
<Window.Resources>
<Style x:Key="DialogWindowStyle" TargetType="Window">
+37 -3
View File
@@ -1,8 +1,11 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Remoting.Contexts;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
@@ -15,25 +18,26 @@ namespace _29WxOrderWpf.Common
{
private readonly Style dialogWindowStyle;
private bool isConstructorSuccessful = false;
private UserControl _content;
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)
{
_content = control;
Setter widthSetter = this.Style.Setters.OfType<Setter>().FirstOrDefault(a => a.Property == WidthProperty);
if (widthSetter == null)
@@ -70,7 +74,11 @@ namespace _29WxOrderWpf.Common
{
this.ResizeMode = resizeExt;
}
WindowStyle wstyleExt = DialogWindowProperty.GetWindowStyle(control);
if (wstyleExt != WindowStyle.SingleBorderWindow)
{
this.WindowStyle = wstyleExt;
}
}
base.OnContentChanged(oldContent, newContent);
}
@@ -87,5 +95,31 @@ namespace _29WxOrderWpf.Common
}
base.OnStyleChanged(oldStyle, newStyle);
}
#region ShowMiniButton
private const int GWL_STYLE = -16;
private const int WS_MINIMIZEBOX = 0x00020000;
[DllImport("user32.dll")]
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
var hwnd = new WindowInteropHelper(this).Handle;
int style = GetWindowLong(hwnd, GWL_STYLE);
style &= ~WS_MINIMIZEBOX;
if (DialogWindowProperty.GetShowMinButton(this._content) == false)
{
SetWindowLong(hwnd, GWL_STYLE, style);
}
}
#endregion
}
}
@@ -27,6 +27,18 @@ namespace _29WxOrderWpf.Common
public static ResizeMode GetResizeMode(DependencyObject element)
=> (ResizeMode)element.GetValue(ResizeModeProperty);
public static readonly DependencyProperty WindowStyleProperty =
DependencyProperty.RegisterAttached(
"WindowStyle",
typeof(WindowStyle),
typeof(DialogWindowProperty),
new PropertyMetadata(WindowStyle.SingleBorderWindow));
public static void SetWindowStyle(DependencyObject element, WindowStyle value)
=> element.SetValue(WindowStyleProperty, value);
public static WindowStyle GetWindowStyle(DependencyObject element)
=> (WindowStyle)element.GetValue(WindowStyleProperty);
public static readonly DependencyProperty WidthProperty = DependencyProperty.RegisterAttached(
@@ -46,5 +58,47 @@ namespace _29WxOrderWpf.Common
=> (double)element.GetValue(HeightProperty);
// 1. ShowMinButton 附加属性
public static readonly DependencyProperty ShowMinButtonProperty =
DependencyProperty.RegisterAttached(
"ShowMinButton",
typeof(bool),
typeof(DialogWindowProperty),
new PropertyMetadata(true));
public static void SetShowMinButton(DependencyObject element, bool value)
=> element.SetValue(ShowMinButtonProperty, value);
public static bool GetShowMinButton(DependencyObject element)
=> (bool)element.GetValue(ShowMinButtonProperty);
// 2. ShowMaxButton 附加属性
public static readonly DependencyProperty ShowMaxButtonProperty =
DependencyProperty.RegisterAttached(
"ShowMaxButton",
typeof(bool),
typeof(DialogWindowProperty),
new PropertyMetadata(true));
public static void SetShowMaxButton(DependencyObject element, bool value)
=> element.SetValue(ShowMaxButtonProperty, value);
public static bool GetShowMaxButton(DependencyObject element)
=> (bool)element.GetValue(ShowMaxButtonProperty);
// 3. ShowCloseButton 附加属性
public static readonly DependencyProperty ShowCloseButtonProperty =
DependencyProperty.RegisterAttached(
"ShowCloseButton",
typeof(bool),
typeof(DialogWindowProperty),
new PropertyMetadata(true));
public static void SetShowCloseButton(DependencyObject element, bool value)
=> element.SetValue(ShowCloseButtonProperty, value);
public static bool GetShowCloseButton(DependencyObject element)
=> (bool)element.GetValue(ShowCloseButtonProperty);
}
}
@@ -233,10 +233,10 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Charts
{
double abs = Math.Abs(value);
if (abs >= 100000000)
return (value / 100000000d).ToString("0.#") + "亿";
return (value / 100000000d).ToString("0.00") + "亿";
if (abs >= 10000)
return (value / 10000d).ToString("0.#") + "万";
return value.ToString("#,##0");
return (value / 10000d).ToString("0.00") + "万";
return value.ToString("#,##0.00");
}
}
}
@@ -181,6 +181,52 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
public decimal AvgOrderAmount { get; set; }
}
/// <summary>
/// 统计周期类型:天 / 周 / 月
/// </summary>
public enum PeriodStatisticType
{
/// <summary>按天统计</summary>
Day = 0,
/// <summary>按周统计(周一起始)</summary>
Week = 1,
/// <summary>按月统计</summary>
Month = 2
}
/// <summary>
/// 天/周/月周期统计表格行(金额单位:元)。
/// 按所选店铺(或全部店铺)统计:天统计每天一行;周统计每周一行;月统计每月一行。
/// </summary>
public class ShopPeriodStatisticsItem
{
/// <summary>周期开始日期(用于排序)</summary>
public DateTime PeriodStart { get; set; }
/// <summary>周期显示文本(天:yyyy-MM-dd;周:yyyy-MM-dd ~ yyyy-MM-dd;月:yyyy-MM)</summary>
public string Period { 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>
@@ -0,0 +1,283 @@
<UserControl
x:Class="_29WxOrderWpf.Ctrls.Statistics.PeriodRevenueStatisticsView"
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:vm="clr-namespace:_29WxOrderWpf.Ctrls.Statistics.ViewModels"
d:Background="#F5F7FB"
d:DataContext="{d:DesignInstance Type=vm:PeriodRevenueStatisticsViewModel}"
d:DesignHeight="900"
d:DesignWidth="1600"
mc:Ignorable="d">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="boolToVisibilityConverter" />
<!-- 日期只允许从日历选择,禁止手工输入 -->
<Style TargetType="DatePickerTextBox">
<Setter Property="IsReadOnly" Value="True" />
</Style>
<!-- 卡片样式(白底、圆角、阴影) -->
<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">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- ============ 查询条件工具栏 ============ -->
<Border
Grid.Row="0"
Margin="6,6,6,0"
Padding="14,10"
Style="{StaticResource CardBorderStyle}">
<WrapPanel>
<TextBlock
Margin="6,0"
VerticalAlignment="Center"
FontSize="14"
Foreground="#666666"
Text="时间" />
<ComboBox IsEditable="False"
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 IsEditable="False"
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>
<!-- ============ 周期×店铺统计表格 ============ -->
<Border
Grid.Row="1"
Margin="6,8,6,12"
Style="{StaticResource CardBorderStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Margin="0,0,0,10"
Style="{StaticResource PanelTitleStyle}"
Text="{Binding PageTitle}" />
<DataGrid
Grid.Row="1"
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 Statistics}"
RowHeight="46"
ScrollViewer.CanContentScroll="True"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
SelectionMode="Single"
SelectionUnit="FullRow">
<DataGrid.Columns>
<DataGridTextColumn
Width="1.4*"
Binding="{Binding Period}"
ElementStyle="{StaticResource CenterCellTextStyle}">
<DataGridTextColumn.Header>
<TextBlock
FontSize="14"
FontWeight="Bold"
Foreground="{DynamicResource mainForegroundColorBrush}"
Text="{Binding DataContext.PeriodColumnHeader, RelativeSource={RelativeSource AncestorType=DataGrid}}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn
Width="*"
Binding="{Binding OrderCount, StringFormat={}{0:N0}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="订单数" />
<DataGridTextColumn
Width="*"
Binding="{Binding SalesAmount, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="销售额" />
<DataGridTextColumn
Width="*"
Binding="{Binding Profit, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="利润" />
<DataGridTextColumn
Width="*"
Binding="{Binding RefundAmount, StringFormat={}¥{0:N2}}"
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>
<!-- ============ 底部翻页 ============ -->
<Border
Grid.Row="2"
Margin="0,10,0,0"
Padding="0,4"
Background="#F8F9FB"
CornerRadius="6">
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<TextBlock
Margin="0,0,12,0"
VerticalAlignment="Center"
FontSize="13"
Foreground="#666666"
Text="{Binding PagerText}" />
<Button
Height="28"
Margin="4,0,0,0"
Padding="12,0"
Command="{Binding FirstPageCommand}"
Content="首页"
Style="{StaticResource BasePageButtonLightStyle}" />
<Button
Height="28"
Margin="4,0,0,0"
Padding="12,0"
Command="{Binding PrevPageCommand}"
Content="上一页"
Style="{StaticResource BasePageButtonLightStyle}" />
<Button
Height="28"
Margin="4,0,0,0"
Padding="12,0"
Command="{Binding NextPageCommand}"
Content="下一页"
Style="{StaticResource BasePageButtonLightStyle}" />
<Button
Height="28"
Margin="4,0,0,0"
Padding="12,0"
Command="{Binding LastPageCommand}"
Content="末页"
Style="{StaticResource BasePageButtonLightStyle}" />
</StackPanel>
</Border>
</Grid>
</Border>
</Grid>
<!-- ============ 加载遮罩 ============ -->
<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,45 @@
using System.Windows;
using System.Windows.Controls;
using _29WxOrderWpf.Ctrls.Statistics.Models;
using _29WxOrderWpf.Ctrls.Statistics.ViewModels;
namespace _29WxOrderWpf.Ctrls.Statistics
{
/// <summary>
/// 天/周/月周期统计页面(MVVM)。
/// 通过 PeriodType 指定统计周期:Day(天统计)/ Week(周统计)/ Month(月统计)。
/// </summary>
public partial class PeriodRevenueStatisticsView : UserControl
{
public PeriodRevenueStatisticsView()
{
InitializeComponent();
// 默认天统计;当 XAML 指定 Week/Month 时通过 PeriodType 回调切换
DataContext = new PeriodRevenueStatisticsViewModel(PeriodStatisticType.Day);
}
public static readonly DependencyProperty PeriodTypeProperty =
DependencyProperty.Register(
nameof(PeriodType),
typeof(PeriodStatisticType),
typeof(PeriodRevenueStatisticsView),
new PropertyMetadata(PeriodStatisticType.Day, OnPeriodTypeChanged));
/// <summary>统计周期(Day / Week / Month)</summary>
public PeriodStatisticType PeriodType
{
get { return (PeriodStatisticType)GetValue(PeriodTypeProperty); }
set { SetValue(PeriodTypeProperty, value); }
}
private static void OnPeriodTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((PeriodRevenueStatisticsView)d).ApplyPeriodType((PeriodStatisticType)e.NewValue);
}
private void ApplyPeriodType(PeriodStatisticType periodType)
{
DataContext = new PeriodRevenueStatisticsViewModel(periodType);
}
}
}
@@ -39,6 +39,9 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
/// <summary>获取店铺统计表格</summary>
Task<List<ShopStatisticsItem>> GetShopStatisticsAsync(ShopRevenueStatisticsQuery query);
/// <summary>获取天/周/月周期统计表格(每天/每周/每月各一行,按所选店铺或全部店铺统计)</summary>
Task<List<ShopPeriodStatisticsItem>> GetPeriodStatisticsAsync(ShopRevenueStatisticsQuery query, PeriodStatisticType periodType);
/// <summary>获取店铺下拉列表(用于查询条件)</summary>
Task<List<ShopOption>> GetShopOptionsAsync();
}
@@ -197,6 +200,113 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
.OrderByDescending(i => i.SalesAmount)
.ToList();
}
/// <summary>天/周/月周期统计表格(每天/每周/每月各一行,无数据的周期也补一行,金额单位:元)</summary>
public static List<ShopPeriodStatisticsItem> GetPeriodStatistics(IEnumerable<ShopRevenueStatisticsRow> rows, DateTime startDate, DateTime endDate, PeriodStatisticType periodType)
{
var list = (rows ?? new List<ShopRevenueStatisticsRow>()).ToList();
// 有数据的周期
var map = list
.GroupBy(r => GetPeriodStart(r.CreateTime, periodType))
.ToDictionary(
g => g.Key,
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 ShopPeriodStatisticsItem
{
PeriodStart = g.Key,
Period = GetPeriodText(g.Key, periodType),
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
};
});
// 按周期补齐整段时间:无数据的周期也生成一行(全 0)
var result = new List<ShopPeriodStatisticsItem>();
foreach (var periodStart in EnumeratePeriods(startDate, endDate, periodType))
{
if (map.TryGetValue(periodStart, out var item))
result.Add(item);
else
result.Add(new ShopPeriodStatisticsItem
{
PeriodStart = periodStart,
Period = GetPeriodText(periodStart, periodType),
OrderCount = 0,
SalesAmount = 0m,
Profit = 0m,
RefundAmount = 0m,
CustomerCount = 0,
AvgOrderAmount = 0m
});
}
// 倒序排列:最新的周期在最上面
result.Reverse();
return result;
}
/// <summary>枚举 startDate~endDate 覆盖的所有周期开始日期(天/周/月)</summary>
private static IEnumerable<DateTime> EnumeratePeriods(DateTime startDate, DateTime endDate, PeriodStatisticType periodType)
{
var start = GetPeriodStart(startDate, periodType);
var end = GetPeriodStart(endDate, periodType);
switch (periodType)
{
case PeriodStatisticType.Week:
for (var d = start; d <= end; d = d.AddDays(7))
yield return d;
break;
case PeriodStatisticType.Month:
for (var d = start; d <= end; d = d.AddMonths(1))
yield return d;
break;
default:
for (var d = start; d <= end; d = d.AddDays(1))
yield return d;
break;
}
}
/// <summary>周期开始日期:天=当天;周=周一起始;月=当月1号</summary>
private static DateTime GetPeriodStart(DateTime time, PeriodStatisticType periodType)
{
switch (periodType)
{
case PeriodStatisticType.Week:
var d = time.Date;
int diff = d.DayOfWeek == DayOfWeek.Sunday ? 6 : (int)d.DayOfWeek - 1;
return d.AddDays(-diff);
case PeriodStatisticType.Month:
return new DateTime(time.Year, time.Month, 1);
default:
return time.Date;
}
}
/// <summary>周期显示文本:天=yyyy-MM-dd;周=yyyy-MM-dd ~ yyyy-MM-dd;月=yyyy-MM</summary>
private static string GetPeriodText(DateTime periodStart, PeriodStatisticType periodType)
{
switch (periodType)
{
case PeriodStatisticType.Week:
return periodStart.ToString("yyyy-MM-dd") + " ~ " + periodStart.AddDays(6).ToString("yyyy-MM-dd");
case PeriodStatisticType.Month:
return periodStart.ToString("yyyy-MM");
default:
return periodStart.ToString("yyyy-MM-dd");
}
}
}
/// <summary>
@@ -317,15 +427,24 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
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
try
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
if (ret.Code != 200)
{
throw new Exception(ret.Msg);
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>();
}
return ret.Data ?? new List<ShopRevenueStatisticsRow>();
catch
{
throw new Exception("查询失败");
}
}
/// <summary>
@@ -427,6 +546,13 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
return RevenueStatisticsAggregator.GetShopStatistics(rows);
}
/// <summary>获取天/周/月周期统计表格</summary>
public async Task<List<ShopPeriodStatisticsItem>> GetPeriodStatisticsAsync(ShopRevenueStatisticsQuery query, PeriodStatisticType periodType)
{
var rows = await QueryRowsAsync(query);
return RevenueStatisticsAggregator.GetPeriodStatistics(rows, query.StartDate, query.EndDate, periodType);
}
public Task<List<ShopOption>> GetShopOptionsAsync()
{
var list = GlobalConfig.ShopLoginList.Select(a =>
@@ -6,12 +6,14 @@
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:stat="clr-namespace:_29WxOrderWpf.Ctrls.Statistics"
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"
wpf:DialogWindowProperty.ShowMinButton="False"
mc:Ignorable="d">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="boolToVisibilityConverter" />
@@ -45,10 +47,87 @@
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Foreground" Value="#333333" />
</Style>
<!-- TabControl 样式(页签 + 内容区) -->
<Style x:Key="StatisticsTabControlStyle" TargetType="TabControl">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TabControl">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border
Grid.Row="0"
Margin="6,0,0,0"
HorizontalAlignment="Left"
Background="Transparent">
<TabPanel
x:Name="HeaderPanel"
IsItemsHost="True"
Background="Transparent" />
</Border>
<Border
Grid.Row="1"
Background="Transparent"
CornerRadius="0,8,8,8">
<ContentPresenter
x:Name="PART_SelectedContentHost"
ContentSource="SelectedContent" />
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- TabItem 样式 -->
<Style x:Key="StatisticsTabItemStyle" TargetType="TabItem">
<Setter Property="Foreground" Value="#666666" />
<Setter Property="FontSize" Value="15" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TabItem">
<Border
x:Name="Bd"
Margin="0,0,4,0"
Padding="20,9"
Background="Transparent"
BorderBrush="#E0E4EA"
BorderThickness="1,1,1,0"
CornerRadius="8,8,0,0">
<ContentPresenter
HorizontalAlignment="Center"
VerticalAlignment="Center"
ContentSource="Header" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#F0F4FB" />
<Setter Property="Foreground" Value="#98638F" />
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Bd" Property="Background" Value="White" />
<Setter TargetName="Bd" Property="BorderBrush" Value="#E0E4EA" />
<Setter Property="Foreground" Value="#98638F" />
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Grid Background="#F5F7FB">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<TabControl Style="{StaticResource StatisticsTabControlStyle}">
<TabItem
Header="综合统计"
Style="{StaticResource StatisticsTabItemStyle}">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="12">
<!-- ============ 查询条件工具栏 ============ -->
@@ -406,17 +485,17 @@
Header="订单数" />
<DataGridTextColumn
Width="*"
Binding="{Binding SalesAmount, StringFormat={}¥{0:N0}}"
Binding="{Binding SalesAmount, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="销售额" />
<DataGridTextColumn
Width="*"
Binding="{Binding Profit, StringFormat={}¥{0:N0}}"
Binding="{Binding Profit, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="利润" />
<DataGridTextColumn
Width="*"
Binding="{Binding RefundAmount, StringFormat={}¥{0:N0}}"
Binding="{Binding RefundAmount, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="退款金额" />
<DataGridTextColumn
@@ -434,7 +513,27 @@
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</ScrollViewer>
</TabItem>
<TabItem
Header="天统计"
Style="{StaticResource StatisticsTabItemStyle}">
<stat:PeriodRevenueStatisticsView PeriodType="Day" />
</TabItem>
<TabItem
Header="周统计"
Style="{StaticResource StatisticsTabItemStyle}">
<stat:PeriodRevenueStatisticsView PeriodType="Week" />
</TabItem>
<TabItem
Header="月统计"
Style="{StaticResource StatisticsTabItemStyle}">
<stat:PeriodRevenueStatisticsView PeriodType="Month" />
</TabItem>
</TabControl>
<!-- ============ 加载遮罩 ============ -->
<Border Background="#66FFFFFF" Visibility="{Binding IsLoading, Converter={StaticResource boolToVisibilityConverter}}" d:Visibility="Collapsed" >
@@ -0,0 +1,516 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows;
using AllBase.Model.MvvmBaseClasses;
using _29WxOrderWpf.Ctrls.Statistics.Models;
using _29WxOrderWpf.Ctrls.Statistics.Services;
using _29WxOrderWpf.Commands;
namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
{
/// <summary>
/// 天/周/月周期统计 - 页面 ViewModel(MVVM)。
/// 通过 PeriodStatisticType 指定统计周期(Day/Week/Month),
/// 页面统一展示:筛选条件 + 周期×店铺统计表格 + 分页。
/// </summary>
public class PeriodRevenueStatisticsViewModel : PropertyNotifyObject
{
/// <summary>每页行数</summary>
private const int PageSize = 20;
private readonly IShopRevenueStatisticsService _service;
private readonly PeriodStatisticType _periodType;
private readonly DelegateCommand _firstPageCommand;
private readonly DelegateCommand _prevPageCommand;
private readonly DelegateCommand _nextPageCommand;
private readonly DelegateCommand _lastPageCommand;
private ShopOption _selectedShop;
private ObservableCollection<ShopPeriodStatisticsItem> _statistics;
private List<ShopPeriodStatisticsItem> _allItems = new List<ShopPeriodStatisticsItem>();
private int _pageIndex = 1;
private int _totalPages = 1;
private int _totalCount;
private string _pagerText = string.Empty;
public PeriodRevenueStatisticsViewModel(PeriodStatisticType periodType)
: this(periodType, new ShopRevenueStatisticsService())
{
}
public PeriodRevenueStatisticsViewModel(PeriodStatisticType periodType, IShopRevenueStatisticsService service)
{
_periodType = periodType;
_service = service ?? new ShopRevenueStatisticsService();
PageTitle = GetPageTitle(periodType);
PeriodColumnHeader = GetPeriodColumnHeader(periodType);
TimeRangeOptions = new ObservableCollection<TimeRangeOption>(GetTimeRangeOptions(periodType));
ShopOptions = new ObservableCollection<ShopOption>();
QueryCommand = new DelegateCommand(async () => await LoadAsync());
ResetCommand = new DelegateCommand(Reset);
_firstPageCommand = new DelegateCommand(() => GoToPage(1), () => HasPrevPage);
_prevPageCommand = new DelegateCommand(() => GoToPage(PageIndex - 1), () => HasPrevPage);
_nextPageCommand = new DelegateCommand(() => GoToPage(PageIndex + 1), () => HasNextPage);
_lastPageCommand = new DelegateCommand(() => GoToPage(TotalPages), () => HasNextPage);
InitializeAsync();
}
#region 页面标题
/// <summary>页面标题(天统计/周统计/月统计)</summary>
public string PageTitle { get; }
/// <summary>周期列表头(日期/周/月份)</summary>
public string PeriodColumnHeader { get; }
private static string GetPageTitle(PeriodStatisticType periodType)
{
switch (periodType)
{
case PeriodStatisticType.Week:
return "周统计";
case PeriodStatisticType.Month:
return "月统计";
default:
return "天统计";
}
}
private static string GetPeriodColumnHeader(PeriodStatisticType periodType)
{
switch (periodType)
{
case PeriodStatisticType.Week:
return "周";
case PeriodStatisticType.Month:
return "月份";
default:
return "日期";
}
}
#endregion
#region 时间范围选项
/// <summary>按统计周期返回对应的时间范围选项</summary>
private static List<TimeRangeOption> GetTimeRangeOptions(PeriodStatisticType periodType)
{
switch (periodType)
{
case PeriodStatisticType.Week:
return new List<TimeRangeOption>
{
new TimeRangeOption { Text = "本周", Value = "week" },
new TimeRangeOption { Text = "上周", Value = "lastweek" },
new TimeRangeOption { Text = "最近2周", Value = "last2weeks" },
new TimeRangeOption { Text = "最近4周", Value = "last4weeks" },
new TimeRangeOption { Text = "自定义", Value = "custom" }
};
case PeriodStatisticType.Month:
return new List<TimeRangeOption>
{
new TimeRangeOption { Text = "本月", Value = "month" },
new TimeRangeOption { Text = "上月", Value = "lastmonth" },
new TimeRangeOption { Text = "最近3个月", Value = "last3months" },
new TimeRangeOption { Text = "最近6个月", Value = "last6months" },
new TimeRangeOption { Text = "最近12个月", Value = "last12months" },
new TimeRangeOption { Text = "自定义", Value = "custom" }
};
default:
return new List<TimeRangeOption>
{
new TimeRangeOption { Text = "最近5天", Value = "last5" },
new TimeRangeOption { Text = "最近10天", Value = "last10" },
new TimeRangeOption { Text = "最近20天", Value = "last20" },
new TimeRangeOption { Text = "最近30天", Value = "last30" },
new TimeRangeOption { Text = "自定义", Value = "custom" }
};
}
}
/// <summary>按统计周期返回默认选中的时间范围值</summary>
private static string GetDefaultTimeRangeValue(PeriodStatisticType periodType)
{
switch (periodType)
{
case PeriodStatisticType.Week:
return "week";
case PeriodStatisticType.Month:
return "month";
default:
return "last10";
}
}
#endregion
#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 ObservableCollection<ShopPeriodStatisticsItem> Statistics
{
get { return _statistics; }
private set
{
_statistics = value;
OnPropertyChanged(nameof(Statistics));
}
}
/// <summary>是否正在加载</summary>
public bool IsLoading
{
get { return this.GetValue(x => x.IsLoading); }
set { this.SetValue(x => x.IsLoading, value); }
}
#endregion
#region 分页
/// <summary>总行数</summary>
public int TotalCount
{
get { return _totalCount; }
private set
{
_totalCount = value;
OnPropertyChanged(nameof(TotalCount));
}
}
/// <summary>当前页码(从 1 开始)</summary>
public int PageIndex
{
get { return _pageIndex; }
private set
{
_pageIndex = value;
OnPropertyChanged(nameof(PageIndex));
}
}
/// <summary>总页数</summary>
public int TotalPages
{
get { return _totalPages; }
private set
{
_totalPages = value;
OnPropertyChanged(nameof(TotalPages));
}
}
/// <summary>分页提示文本(共 X 条 第 Y / Z 页)</summary>
public string PagerText
{
get { return _pagerText; }
private set
{
_pagerText = value;
OnPropertyChanged(nameof(PagerText));
}
}
/// <summary>是否有上一页</summary>
public bool HasPrevPage
{
get { return PageIndex > 1; }
}
/// <summary>是否有下一页</summary>
public bool HasNextPage
{
get { return PageIndex < TotalPages; }
}
/// <summary>是否多页</summary>
public bool HasMultiplePages
{
get { return TotalPages > 1; }
}
/// <summary>首页命令</summary>
public ICommand FirstPageCommand
{
get { return _firstPageCommand; }
}
/// <summary>上一页命令</summary>
public ICommand PrevPageCommand
{
get { return _prevPageCommand; }
}
/// <summary>下一页命令</summary>
public ICommand NextPageCommand
{
get { return _nextPageCommand; }
}
/// <summary>末页命令</summary>
public ICommand LastPageCommand
{
get { return _lastPageCommand; }
}
/// <summary>跳转到指定页并刷新当前页数据</summary>
private void GoToPage(int page)
{
if (page < 1)
page = 1;
if (page > TotalPages)
page = TotalPages;
PageIndex = page;
ApplyPaging();
}
/// <summary>按当前页码截取当前页数据并刷新分页状态</summary>
private void ApplyPaging()
{
var pageItems = _allItems
.Skip((PageIndex - 1) * PageSize)
.Take(PageSize)
.ToList();
Statistics = new ObservableCollection<ShopPeriodStatisticsItem>(pageItems);
PagerText = string.Format("共 {0} 条 第 {1} / {2} 页", TotalCount, PageIndex, TotalPages);
OnPropertyChanged(nameof(HasPrevPage));
OnPropertyChanged(nameof(HasNextPage));
OnPropertyChanged(nameof(HasMultiplePages));
_firstPageCommand.RaiseCanExecuteChanged();
_prevPageCommand.RaiseCanExecuteChanged();
_nextPageCommand.RaiseCanExecuteChanged();
_lastPageCommand.RaiseCanExecuteChanged();
}
#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 == GetDefaultTimeRangeValue(_periodType));
ApplyTimeRange();
await LoadAsync();
}
catch (Exception ex)
{
MessageBox.Show("初始化数据失败:" + ex.Message, "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
#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 "last5":
StartDate = today.AddDays(-4);
EndDate = now;
break;
case "last10":
StartDate = today.AddDays(-9);
EndDate = now;
break;
case "last20":
StartDate = today.AddDays(-19);
EndDate = now;
break;
case "last30":
StartDate = today.AddDays(-29);
EndDate = now;
break;
case "week":
{
int diff = today.DayOfWeek == DayOfWeek.Sunday ? 6 : (int)today.DayOfWeek - 1;
StartDate = today.AddDays(-diff);
EndDate = now;
}
break;
case "lastweek":
{
int diff = today.DayOfWeek == DayOfWeek.Sunday ? 6 : (int)today.DayOfWeek - 1;
var thisWeekStart = today.AddDays(-diff);
StartDate = thisWeekStart.AddDays(-7);
EndDate = thisWeekStart.AddDays(-1).AddHours(23).AddMinutes(59).AddSeconds(59);
}
break;
case "last2weeks":
StartDate = today.AddDays(-13);
EndDate = now;
break;
case "last4weeks":
StartDate = today.AddDays(-27);
EndDate = now;
break;
case "month":
StartDate = new DateTime(today.Year, today.Month, 1);
EndDate = now;
break;
case "lastmonth":
{
var firstOfThisMonth = new DateTime(today.Year, today.Month, 1);
StartDate = firstOfThisMonth.AddMonths(-1);
EndDate = firstOfThisMonth.AddDays(-1).AddHours(23).AddMinutes(59).AddSeconds(59);
}
break;
case "last3months":
StartDate = new DateTime(today.Year, today.Month, 1).AddMonths(-2);
EndDate = now;
break;
case "last6months":
StartDate = new DateTime(today.Year, today.Month, 1).AddMonths(-5);
EndDate = now;
break;
case "last12months":
StartDate = new DateTime(today.Year, today.Month, 1).AddMonths(-11);
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,
};
var table = await _service.GetPeriodStatisticsAsync(query, _periodType);
_allItems = table ?? new List<ShopPeriodStatisticsItem>();
TotalCount = _allItems.Count;
TotalPages = TotalCount == 0 ? 1 : (int)Math.Ceiling(TotalCount / (double)PageSize);
GoToPage(1);
}
catch (Exception ex)
{
// 查询异常:保留上一次数据,并弹框提示
MessageBox.Show("查询失败:" + ex.Message, "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
}
finally
{
IsLoading = false;
}
}
/// <summary>重置查询条件并重新加载</summary>
private async void Reset()
{
SelectedShop = ShopOptions.FirstOrDefault();
SelectedTimeRange = TimeRangeOptions.FirstOrDefault(t => t.Value == GetDefaultTimeRangeValue(_periodType));
ApplyTimeRange();
await LoadAsync();
}
#endregion
}
}