From d25374956e9f6e0138bd8b2b3a59b76a65d2b239 Mon Sep 17 00:00:00 2001 From: chfei Date: Fri, 7 Aug 2026 18:52:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A9=E6=B6=A6=E7=BB=9F=E8=AE=A1=20?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=BC=80=E6=94=BE=20=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=A2=9E=E5=8A=A0=E6=96=B0=E7=9A=84=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 29WxOrderWpf/29WxOrderWpf.csproj.user | 21 -- 29WxOrderWpf/Common/ScrollViewerBehavior.cs | 109 ++++++ .../Ctrls/OrderCtrls/OrderListCtrl.xaml | 2 +- .../Ctrls/OrderCtrls/OrderListCtrl.xaml.cs | 4 +- .../Models/ShopRevenueStatisticsModels.cs | 32 +- .../PeriodRevenueStatisticsView.xaml | 20 +- .../Ctrls/Statistics/RevenueSummaryCards.xaml | 298 ++++++++------- .../Services/ShopRevenueStatisticsService.cs | 57 ++- .../Statistics/ShopRevenueStatisticsView.xaml | 341 ++++++++++-------- .../ShopRevenueStatisticsView.xaml.cs | 10 + 10 files changed, 571 insertions(+), 323 deletions(-) delete mode 100644 29WxOrderWpf/29WxOrderWpf.csproj.user create mode 100644 29WxOrderWpf/Common/ScrollViewerBehavior.cs diff --git a/29WxOrderWpf/29WxOrderWpf.csproj.user b/29WxOrderWpf/29WxOrderWpf.csproj.user deleted file mode 100644 index 607b9ea..0000000 --- a/29WxOrderWpf/29WxOrderWpf.csproj.user +++ /dev/null @@ -1,21 +0,0 @@ - - - - publish\ - - - - - - zh-CN - false - ProjectFiles - Production - - - ProjectDebugger - - - ProjectDebugger - - \ No newline at end of file diff --git a/29WxOrderWpf/Common/ScrollViewerBehavior.cs b/29WxOrderWpf/Common/ScrollViewerBehavior.cs new file mode 100644 index 0000000..9a564d4 --- /dev/null +++ b/29WxOrderWpf/Common/ScrollViewerBehavior.cs @@ -0,0 +1,109 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +namespace _29WxOrderWpf.Common +{ + /// + /// 让页面级 ScrollViewer 在鼠标悬停在子控件(如 DataGrid、图表等)上时也能随鼠标滚轮滚动。 + /// 用法:在 ScrollViewer 上设置 common:ScrollViewerBehavior.FixMouseWheel="True"。 + /// + public static class ScrollViewerBehavior + { + public static readonly DependencyProperty FixMouseWheelProperty = + DependencyProperty.RegisterAttached( + "FixMouseWheel", + typeof(bool), + typeof(ScrollViewerBehavior), + new PropertyMetadata(false, OnFixMouseWheelChanged)); + + public static bool GetFixMouseWheel(DependencyObject obj) + => (bool)obj.GetValue(FixMouseWheelProperty); + + public static void SetFixMouseWheel(DependencyObject obj, bool value) + => obj.SetValue(FixMouseWheelProperty, value); + + private static void OnFixMouseWheelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (!(d is ScrollViewer scrollViewer)) + { + return; + } + + if ((bool)e.NewValue) + { + scrollViewer.PreviewMouseWheel += OnPreviewMouseWheel; + } + else + { + scrollViewer.PreviewMouseWheel -= OnPreviewMouseWheel; + } + } + + private static void OnPreviewMouseWheel(object sender, MouseWheelEventArgs e) + { + if (e.Handled || !(sender is ScrollViewer scrollViewer) || scrollViewer.ScrollableHeight <= 0) + { + return; + } + + if (!(e.OriginalSource is DependencyObject original)) + { + return; + } + + // 从鼠标所在控件向上查找,确认它属于当前 ScrollViewer + var current = original; + var inside = false; + while (current != null) + { + if (ReferenceEquals(current, scrollViewer)) + { + inside = true; + break; + } + + // 若鼠标位于一个能自行纵向滚动的内部 ScrollViewer 上(例如下拉列表、 + // 启用了滚动的表格),则交给内部处理,避免抢走它的滚动。 + if (current is ScrollViewer inner && + inner.ComputedVerticalScrollBarVisibility == Visibility.Visible && + inner.ScrollableHeight > 0) + { + return; + } + + current = VisualTreeHelper.GetParent(current); + } + + if (!inside) + { + return; + } + + // 按系统默认的滚轮行数滚动当前页面 + var lines = (int)(e.Delta / Mouse.MouseWheelDeltaForOneLine * SystemParameters.WheelScrollLines); + if (lines == 0) + { + lines = e.Delta > 0 ? 1 : -1; + } + + if (lines < 0) + { + for (var i = 0; i < -lines; i++) + { + scrollViewer.LineDown(); + } + } + else + { + for (var i = 0; i < lines; i++) + { + scrollViewer.LineUp(); + } + } + + e.Handled = true; + } + } +} diff --git a/29WxOrderWpf/Ctrls/OrderCtrls/OrderListCtrl.xaml b/29WxOrderWpf/Ctrls/OrderCtrls/OrderListCtrl.xaml index a89046b..5fc1405 100644 --- a/29WxOrderWpf/Ctrls/OrderCtrls/OrderListCtrl.xaml +++ b/29WxOrderWpf/Ctrls/OrderCtrls/OrderListCtrl.xaml @@ -98,7 +98,7 @@ - diff --git a/29WxOrderWpf/Ctrls/OrderCtrls/OrderListCtrl.xaml.cs b/29WxOrderWpf/Ctrls/OrderCtrls/OrderListCtrl.xaml.cs index b5ae969..0c6108f 100644 --- a/29WxOrderWpf/Ctrls/OrderCtrls/OrderListCtrl.xaml.cs +++ b/29WxOrderWpf/Ctrls/OrderCtrls/OrderListCtrl.xaml.cs @@ -1001,7 +1001,7 @@ namespace _29WxOrderWpf.Ctrls.OrderCtrls if (chkEndDate.IsChecked == true && order.UpdateDateTime > dateEnd.SelectedDate.Value) return true; } - if (tb.Text == "创建时间") + if (tb.Text == "成交时间") { if (order.CreateDateTime < dateStart.SelectedDate.Value) return true; @@ -1707,7 +1707,7 @@ namespace _29WxOrderWpf.Ctrls.OrderCtrls Title = "统计", Content = new ShopRevenueStatisticsView(), - }.ShowDialog(); + }.Show(); } /// diff --git a/29WxOrderWpf/Ctrls/Statistics/Models/ShopRevenueStatisticsModels.cs b/29WxOrderWpf/Ctrls/Statistics/Models/ShopRevenueStatisticsModels.cs index 7a5a382..77ec3fa 100644 --- a/29WxOrderWpf/Ctrls/Statistics/Models/ShopRevenueStatisticsModels.cs +++ b/29WxOrderWpf/Ctrls/Statistics/Models/ShopRevenueStatisticsModels.cs @@ -137,6 +137,20 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models /// [JsonProperty("external_procurement")] public int ExternalProcurement { get; set; } + + + /// + /// 买手退款金额 + /// + [JsonProperty("buyer_refund_amount")] + public int BuyerRefundAmount { get; set; } + + /// + /// 买手退款状态 + /// + [JsonProperty("buyer_refund_status")] + public string BuyerRefundStatus { get; set; } + } /// @@ -155,8 +169,8 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models /// 退款金额(统计期间退款) public decimal RefundAmount { get; set; } - /// 商品销量(有效销量) - public int ProductSalesVolume { get; set; } + /// 退款订单数 + public int RefundOrderCount { get; set; } /// 平台服务费总额(元) public decimal PlatformFeeTotal { get; set; } @@ -172,6 +186,15 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models /// 平均利润率(利润 / 采购金额,百分数,如 12.34 表示 12.34%) public decimal AverageProfitRate { get; set; } + /// 买手号付款金额(采购金额,元) + public decimal BuyerPaymentAmount { get; set; } + + /// 买手号退款金额(采购退款金额,元) + public decimal BuyerRefundAmount { get; set; } + /// 买手号退款成功金额(采购退款金额,元) + public decimal BuyerRefundSuccessedAmount { get; set; } + /// 买手号剩余退款金额(尚未成功退款的金额,元) + public decimal BuyerRefundRemainingAmount => BuyerRefundAmount - BuyerRefundSuccessedAmount; } /// @@ -220,7 +243,8 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models /// 销售额(元) public decimal SalesAmount { get; set; } - + /// 采购金额(元) + public decimal BuyerPaymentAmount { get; set; } /// 利润(元) public decimal Profit { get; set; } @@ -272,6 +296,8 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models /// 销售额(元) public decimal SalesAmount { get; set; } + /// 买手号付款金额(采购金额,元) + public decimal BuyerPaymentAmount { get; set; } /// 利润(元) public decimal Profit { get; set; } diff --git a/29WxOrderWpf/Ctrls/Statistics/PeriodRevenueStatisticsView.xaml b/29WxOrderWpf/Ctrls/Statistics/PeriodRevenueStatisticsView.xaml index 477222e..98175af 100644 --- a/29WxOrderWpf/Ctrls/Statistics/PeriodRevenueStatisticsView.xaml +++ b/29WxOrderWpf/Ctrls/Statistics/PeriodRevenueStatisticsView.xaml @@ -2,6 +2,7 @@ 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:common="clr-namespace:_29WxOrderWpf.Common" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:stat="clr-namespace:_29WxOrderWpf.Ctrls.Statistics" @@ -46,7 +47,10 @@ - + @@ -56,9 +60,7 @@ - + @@ -119,6 +121,11 @@ Binding="{Binding SalesAmount, StringFormat={}¥{0:N2}}" ElementStyle="{StaticResource CenterCellTextStyle}" Header="销售额" /> + - + @@ -26,25 +27,14 @@ - - - - - - - - - - - - - + + - + - + + + + + + + + + + - + - - - + + + Text="平均利润率" /> + Text="{Binding Summary.AverageProfitRate, StringFormat={}{0:0.00}%}" /> + Text="利润 / 采购金额" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + Text="达人佣金" /> + Text="{Binding Summary.Commission, StringFormat={}¥{0:N2}}" /> + Text="佣金支出" /> - + + \ No newline at end of file diff --git a/29WxOrderWpf/Ctrls/Statistics/Services/ShopRevenueStatisticsService.cs b/29WxOrderWpf/Ctrls/Statistics/Services/ShopRevenueStatisticsService.cs index 5ff652f..c0eba8a 100644 --- a/29WxOrderWpf/Ctrls/Statistics/Services/ShopRevenueStatisticsService.cs +++ b/29WxOrderWpf/Ctrls/Statistics/Services/ShopRevenueStatisticsService.cs @@ -92,30 +92,39 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services public static bool IsRefunded(ShopRevenueStatisticsRow row) { if (row == null) return false; - return row.RefundStatus == 4 || row.RefundStatus == 11 || row.Status == StatusCancelAfterAfterSale; + return row.RefundStatus > 1 || row.RefundStatusString == "MERCHANT_REFUND_SUCCESS" || row.RefundStatusString == "MERCHANT_RETURN_SUCCESS"; } /// 是否有效订单(已付款且未退款) public static bool IsValidOrder(ShopRevenueStatisticsRow row) { - return IsPaidOrder(row) && !IsRefunded(row); + return !IsRefunded(row); + } + /// 买手号退款成功 + public static bool IsBuyerRefundSuccessful(ShopRevenueStatisticsRow row) + { + return row.BuyerRefundStatus == "退款成功"; } - /// 汇总卡片 public static ShopRevenueSummary GetSummary(IEnumerable rows) { - var list = (rows ?? new List()).ToList(); + var list = (rows ?? new List()).Where(a => a.OutAmount > 0).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 salesAmount = list.Sum(r => (decimal)(r.OrderPrice)) / 100m; + decimal profit = valid.Concat(refunded).Sum(r => (decimal)(r.Profit)) / 100m; decimal refundAmount = refunded.Sum(r => (decimal)(r.OrderPrice)) / 100m; decimal platformFeeTotal = valid.Sum(r => (decimal)(r.PredictCommissionFee)) / 100m; decimal commission = valid.Sum(r => (decimal)(r.CommissionAmount)) / 100m; - decimal purchaseAmount = valid.Sum(r => (decimal)(r.PurchasePrice)) / 100m; + decimal purchaseAmount = valid.Sum(r => (decimal)(r.OutAmount)) / 100m; + decimal buyerPaymentAmount = list.Sum(r => (decimal)(r.OutAmount)) / 100m; //买手号付款金额(采购金额) + decimal buyerRefundAmount = refunded.Sum(r => (decimal)(r.OutAmount)) / 100m; //买手号退款金额(采购退款金额) + decimal buyerRefundSuccessedAmount = refunded.Where(IsBuyerRefundSuccessful).Sum(r => (decimal)(r.BuyerRefundAmount)) / 100m; //买手号退款成功金额 + int validOrderCount = valid.Select(r => r.OrderId).Distinct().Count(); - // 总订单数(退款率分母)= 已付款订单 + 已退款订单(含售后取消 200),不含未付款的 10/250 + + // 总订单数(退款率分母)= 已付款订单 + 已退款订单,不含未付款的 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(); @@ -125,12 +134,15 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services ValidOrderCount = validOrderCount, Profit = profit, RefundAmount = refundAmount, - ProductSalesVolume = valid.Count, + RefundOrderCount = refundOrderCount, PlatformFeeTotal = platformFeeTotal, Commission = commission, AvgOrderAmount = validOrderCount == 0 ? 0m : salesAmount / validOrderCount, RefundRate = totalOrderCount == 0 ? 0m : (decimal)refundOrderCount * 100m / totalOrderCount, - AverageProfitRate = purchaseAmount == 0m ? 0m : profit / purchaseAmount * 100m + AverageProfitRate = purchaseAmount == 0m ? 0m : profit / purchaseAmount * 100m, + BuyerPaymentAmount = buyerPaymentAmount, + BuyerRefundAmount = buyerRefundAmount, + BuyerRefundSuccessedAmount = buyerRefundSuccessedAmount, }; } @@ -164,14 +176,14 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services public static List GetShopSalesProportion(IEnumerable rows) { var list = (rows ?? new List()).Where(IsValidOrder).ToList(); - decimal total = list.Sum(r => (decimal)(r.OrderPrice )) / 100m; + 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, + 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) @@ -181,7 +193,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services /// 店铺统计表格 public static List GetShopStatistics(IEnumerable rows) { - var list = (rows ?? new List()).ToList(); + var list = (rows ?? new List()).Where(a => a.OutAmount > 0).ToList(); //买手付钱后的订单 return list .GroupBy(r => r.ShopName) @@ -189,15 +201,17 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services { var valid = g.Where(IsValidOrder).ToList(); var refunded = g.Where(IsRefunded).ToList(); - decimal salesAmount = valid.Sum(r => (decimal)(r.OrderPrice)) / 100m; + decimal salesAmount = g.Sum(r => (decimal)(r.OrderPrice)) / 100m; //与 GetSummary 一致:含已退款订单的销售额 int orderCount = valid.Select(r => r.OrderId).Distinct().Count(); - decimal profit = valid.Sum(r => (decimal)(r.Profit)) / 100m; - decimal purchaseAmount = valid.Sum(r => (decimal)(r.PurchasePrice)) / 100m; + decimal profit = valid.Concat(refunded).Sum(r => (decimal)(r.Profit)) / 100m; //与 GetSummary 一致:有效+退款订单利润 + decimal purchaseAmount = valid.Sum(r => (decimal)(r.OutAmount)) / 100m; //与 GetSummary 一致:买手付款金额(采购金额) + decimal buyerPaymentAmount = list.Sum(r => (decimal)(r.OutAmount)) / 100m; //买手号付款金额(采购金额) return new ShopStatisticsItem { ShopName = string.IsNullOrEmpty(g.Key) ? "未知店铺" : g.Key, OrderCount = orderCount, SalesAmount = salesAmount, + BuyerPaymentAmount = buyerPaymentAmount, Profit = profit, RefundAmount = refunded.Sum(r => (decimal)(r.OrderPrice)) / 100m, PlatformFeeTotal = valid.Sum(r => (decimal)(r.PredictCommissionFee)) / 100m, @@ -213,7 +227,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services /// 天/周/月周期统计表格(每天/每周/每月各一行,无数据的周期也补一行,金额单位:元) public static List GetPeriodStatistics(IEnumerable rows, DateTime startDate, DateTime endDate, PeriodStatisticType periodType) { - var list = (rows ?? new List()).ToList(); + var list = (rows ?? new List()).Where(a => a.OutAmount > 0).ToList(); //买手付钱后的订单 // 有数据的周期 var map = list @@ -224,16 +238,19 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services { var valid = g.Where(IsValidOrder).ToList(); var refunded = g.Where(IsRefunded).ToList(); - decimal salesAmount = valid.Sum(r => (decimal)(r.OrderPrice)) / 100m; - decimal profit = valid.Sum(r => (decimal)(r.Profit)) / 100m; - decimal purchaseAmount = valid.Sum(r => (decimal)(r.PurchasePrice)) / 100m; + decimal salesAmount = g.Sum(r => (decimal)(r.OrderPrice)) / 100m; //与 GetSummary 一致:含已退款订单的销售额 + decimal profit = valid.Concat(refunded).Sum(r => (decimal)(r.Profit)) / 100m; //与 GetSummary 一致:有效+退款订单利润 + decimal purchaseAmount = valid.Sum(r => (decimal)(r.OutAmount)) / 100m; //与 GetSummary 一致:买手付款金额(采购金额) int orderCount = valid.Select(r => r.OrderId).Distinct().Count(); + decimal buyerPaymentAmount = list.Sum(r => (decimal)(r.OutAmount)) / 100m; //买手号付款金额(采购金额) + return new ShopPeriodStatisticsItem { PeriodStart = g.Key, Period = GetPeriodText(g.Key, periodType), OrderCount = orderCount, SalesAmount = salesAmount, + BuyerPaymentAmount = buyerPaymentAmount, Profit = profit, RefundAmount = refunded.Sum(r => (decimal)(r.OrderPrice)) / 100m, PlatformFeeTotal = valid.Sum(r => (decimal)(r.PredictCommissionFee)) / 100m, diff --git a/29WxOrderWpf/Ctrls/Statistics/ShopRevenueStatisticsView.xaml b/29WxOrderWpf/Ctrls/Statistics/ShopRevenueStatisticsView.xaml index 8613351..5a20933 100644 --- a/29WxOrderWpf/Ctrls/Statistics/ShopRevenueStatisticsView.xaml +++ b/29WxOrderWpf/Ctrls/Statistics/ShopRevenueStatisticsView.xaml @@ -5,11 +5,12 @@ 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:stat="clr-namespace:_29WxOrderWpf.Ctrls.Statistics" + 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:DataContext="{d:DesignInstance Type=vm:ShopRevenueStatisticsViewModel, + IsDesignTimeCreatable=True}" d:DesignHeight="900" d:DesignWidth="1600" wpf:DialogWindowProperty.ResizeMode="CanResizeWithGrip" @@ -67,16 +68,14 @@ Background="Transparent"> + Background="Transparent" + IsItemsHost="True" /> - + @@ -124,158 +123,208 @@ - - - + + + - - + + - - + + - - - - - - + + + + + + - - - - - - - + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - + + - - - + + + - - - + + + - - - + + + + + + + + + + + +