Merge branch 'main' of github.com:KJD2025/29WxOrder

This commit is contained in:
wule committed 2026-10-09 13:58:34 +08:00
commit 92fc8cf5b8
8 files changed
+1240 -713

No files matched your search

@@ -1,4 +1,4 @@
using Newtonsoft.Json;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using WPFCommon.Models;
@@ -6,152 +6,228 @@ using WPFCommon.Models;
namespace _29WxOrderWpf.Ctrls.Statistics.Models
{
/// <summary>
/// 店铺营收统计 - 查询参数
/// 店铺营收统计 - 查询参数。
/// 对应新接口 shop_revenue_statistics/get_shop_order_amount 与
/// shop_revenue_statistics/get_shop_stats_details 的请求体(StartDate / EndDate / ShopIds)。
/// </summary>
public class ShopRevenueStatisticsQuery
{
/// <summary>开始时间</summary>
/// <summary>开始日期</summary>
public DateTime StartDate { get; set; }
/// <summary>结束时间</summary>
/// <summary>结束日期</summary>
public DateTime EndDate { get; set; }
/// <summary>选中的店铺 app_key 列表;空/Null 表示全部店铺</summary>
public List<string> ShopKeys { get; set; }
/// <summary>
/// 选中的店铺 ERP 店铺ID(ShopLoginInfo.ErpShopId)列表;
/// 空/Null 表示全部店铺(服务层会取全部店铺的 ErpShopId)。
/// </summary>
public List<string> ShopIds { get; set; }
}
/// <summary>
/// 店铺营收统计 - 接口返回的原始明细行。
/// 与后端 WXOrderAmountInfo 保持一致(camelCase JSON):
/// shopName, createTime, orderId, status, productPrice, orderPrice, purchasePrice, freight,
/// predictCommissionFee, commissionAmount, discountedPrice, profit, purchaseStatus,
/// outAmount, refundStatus, refundStatusString, refundAmount, externalProcurement
/// 金额字段单位均为分(显示时需除以 100)。
/// 店铺统计记录(接口返回)。
/// 接口返回结构:
/// shop_revenue_statistics/GetShopStats -> Result&lt;Dictionary&lt;string, List&lt;ShopStatsResponse&gt;&gt;&gt;
/// Key = 店铺ID,Value = 该店铺按 <see cref="ShopStatsGroupType"/> 聚合后的记录列表
/// (汇总=1 条;天/周/月=每个自然日/自然周/自然月各 1 条)。
/// 注意:接口只返回按 GroupType 聚合后的记录,不返回“整段汇总”,
/// 页面上的汇总卡片由客户端对返回记录自行合计(GroupType=Summary 时每店铺一条,直接相加即可)。
/// 金额字段单位均为“分”,显示时需除以 100 转为元。
/// </summary>
public class ShopRevenueStatisticsRow
public class ShopStatsResponse
{
/// <summary>
/// 店铺名称
/// 店铺ID
/// </summary>
[JsonProperty("shop_name")]
public string ShopName { get; set; }
public string ShopId { get; set; }
/// <summary>
/// 下单时间(秒级时间戳)
/// 销售订单创建日期
/// </summary>
[JsonProperty("create_time")]
[JsonConverter(typeof(UnixDateTimeConverter))]
public DateTime CreateTime { get; set; }
public DateTime StatDate { get; set; }
/// <summary>
/// 订单号
/// 订单数(有效订单,不含退款订单)
/// </summary>
[JsonProperty("order_id")]
public string OrderId { get; set; }
public int OrderCount { get; set; }
/// <summary>
/// 订单状态
/// 销售总额(分)
/// </summary>
[JsonProperty("status")]
public int Status { get; set; }
/// <summary>
/// 商品总价(分)
/// </summary>
[JsonProperty("product_price")]
public int ProductPrice { get; set; }
/// <summary>
/// 订单金额(分)
/// </summary>
[JsonProperty("order_price")]
public int OrderPrice { get; set; }
/// <summary>
/// 采购价(分)
/// </summary>
[JsonProperty("purchase_price")]
public int PurchasePrice { get; set; }
/// <summary>
/// 运费(分)
/// </summary>
[JsonProperty("freight")]
public int Freight { get; set; }
/// <summary>
/// 平台服务费(预计技术服务费,分)
/// </summary>
[JsonProperty("predict_commission_fee")]
public int PredictCommissionFee { get; set; }
/// <summary>
/// 达人佣金(分)
/// </summary>
[JsonProperty("commission_amount")]
public int CommissionAmount { get; set; }
/// <summary>
/// 优惠金额(分)
/// </summary>
[JsonProperty("discounted_price")]
public int DiscountedPrice { get; set; }
public long SalesAmount { get; set; }
/// <summary>
/// 利润(分)
/// </summary>
[JsonProperty("profit")]
public int Profit { get; set; }
public long ProfitAmount { get; set; }
/// <summary>
/// 采购状态
/// 平台佣金(分)
/// </summary>
[JsonProperty("purchase_status")]
public int PurchaseStatus { get; set; }
public long PlatformFee { get; set; }
/// <summary>
/// 外采金额(分),out_amount为空或0时从中转仓订单表补全
/// 退款订单数
/// </summary>
public int RefundOrderCount { get; set; }
/// <summary>
/// 已成功退款金额(分,店铺端已退款成功)
/// </summary>
public long RefundDone { get; set; }
/// <summary>
/// 退款处理中金额(分,店铺端退款中)
/// </summary>
public long RefundPending { get; set; }
/// <summary>
/// 采购单数
/// </summary>
public int PurchaseCount { get; set; }
/// <summary>
/// 总花费金额(分,买手号采购付款金额)
/// </summary>
public long TotalPurchase { get; set; }
/// <summary>
/// 售后中买手总金额(分)
/// </summary>
public long TotalRefund => DoneRefund + PendingRefund + RejectedRefund;
/// <summary>
/// 已成功退款金额(分,买手号采购退款成功)
/// </summary>
public long DoneRefund { get; set; }
/// <summary>
/// 未退回金额(分)
/// </summary>
public long PendingRefund { get; set; }
/// <summary>
/// 被拒绝金额(分)
/// </summary>
public long RejectedRefund { get; set; }
}
/// <summary>
/// 销售趋势记录(接口返回):跨店铺合计后的按天趋势,供综合统计页的销售趋势折线图使用。
/// 接口返回结构:
/// shop_revenue_statistics/GetShopStatsTrend
/// -> Result&lt;List&lt;ShopStatsTrendResponse&gt;&gt;
/// 请求参数:StartDate / EndDate / ShopIds(不需要 GroupType,固定按天返回)。
/// 金额字段单位为“分”,显示时需除以 100 转为元。
/// </summary>
public class ShopStatsTrendResponse
{
/// <summary>
/// 统计日期(天)
/// </summary>
public DateTime StatDate { get; set; }
/// <summary>
/// 销售额(分)
/// </summary>
public long SalesAmount { get; set; }
/// <summary>
/// 订单数
/// </summary>
public int OrderCount { get; set; }
}
/// <summary>
/// 订单明细(接口返回):订单列表弹窗 ShowOrderDialogView 的数据行。
/// 接口返回结构:
/// shop_revenue_statistics/GetShopOrderDetails
/// -> Result&lt;List&lt;ShopOrderDetailResponse&gt;&gt;
/// 请求参数:StartDate / EndDate / ShopIds / OrderType(见 <see cref="ShopOrderDetailType"/>)。
/// 金额字段单位均为“分”,显示时需除以 100 转为元。
/// </summary>
public class ShopOrderDetailResponse
{
/// <summary>
/// 店铺ID
/// </summary>
public long ShopId { get; set; }
/// <summary>
/// 店铺名称
/// </summary>
public string ShopName { get; set; }
/// <summary>
/// 商铺订单号
/// </summary>
public string OrderId { get; set; }
/// <summary>
/// 订单创建时间
/// </summary>
public DateTime CreateTime { get; set; }
/// <summary>
/// 订单金额(分)
/// </summary>
public int OrderPrice { get; set; }
/// <summary>
/// 订单退款金额(分),即商铺订单商品明细的退款金额之和;有效订单为 0
/// </summary>
public int OrderRefundAmount { get; set; }
/// <summary>
/// 外采金额(分),即该订单下所有买手订单的购买金额之和
/// </summary>
[JsonProperty("out_amount")]
public int OutAmount { get; set; }
/// <summary>
/// 售后状态(订单明细 refund_status)
/// 买手未退回金额(分),即买手订单中退款未成功的退款金额之和;无售后时为 0
/// </summary>
[JsonProperty("refund_status")]
public int RefundStatus { get; set; }
public int UnrefundedAmount { get; set; }
/// <summary>
/// 售后状态(退款单 status)
/// 买手订单信息(一个订单号对应多个买手订单)
/// </summary>
[JsonProperty("refund_status_string")]
public string RefundStatusString { get; set; }
public List<BuyerOrderInfo> BuyerOrders { get; set; } = new List<BuyerOrderInfo>();
}
/// <summary>
/// 买手订单信息(订单明细下的子项,一个商铺订单可对应多个买手订单)
/// </summary>
public class BuyerOrderInfo
{
/// <summary>
/// 买手订单ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// 仓库类型 0,1直发
/// </summary>
public int PurchaseType { get; set; }
/// <summary>
/// 外部订单(买手订单)号
/// </summary>
public string OutOrderSn { get; set; }
/// <summary>
/// 售后退款金额(分)
/// 金额(分)
/// </summary>
public int OutAmount { get; set; }
public long RefundId { get; set; }
/// <summary>
/// 退款金额(分)
/// </summary>
[JsonProperty("refund_amount")]
public int RefundAmount { get; set; }
/// <summary>
/// 是否外采:0=未从中转仓订单表查到,1=查到
/// 退款状态
/// </summary>
[JsonProperty("external_procurement")]
public int ExternalProcurement { get; set; }
/// <summary>
/// 买手退款金额
/// </summary>
[JsonProperty("buyer_refund_amount")]
public int BuyerRefundAmount { get; set; }
/// <summary>
/// 买手退款状态
/// </summary>
[JsonProperty("buyer_refund_status")]
public string BuyerRefundStatus { get; set; }
public string RefundStatusText { get; set; }
}
/// <summary>
@@ -167,7 +243,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
/// <summary>利润(实际利润)</summary>
public decimal Profit { get; set; }
/// <summary>退款金额(统计期间退款)</summary>
/// <summary>退款金额(统计期间店铺端退款:已成功 + 处理中)</summary>
public decimal RefundAmount { get; set; }
/// <summary>退款订单数</summary>
@@ -176,7 +252,10 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
/// <summary>平台服务费总额(元)</summary>
public decimal PlatformFeeTotal { get; set; }
/// <summary>达人佣金(元)</summary>
/// <summary>
/// 达人佣金(元)。
/// 新接口未提供该字段,固定为 0,保留属性与页面展示,待后端补充字段后直接接上。
/// </summary>
public decimal Commission { get; set; }
/// <summary>客单价(平均每单金额)</summary>
@@ -239,7 +318,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
/// <summary>店铺名称</summary>
public string ShopName { get; set; }
/// <summary>订单数</summary>
/// <summary>订单数(有效订单,不含退款订单)</summary>
public int OrderCount { get; set; }
/// <summary>销售额(元)</summary>
@@ -255,7 +334,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
/// <summary>平台服务费总额(元)</summary>
public decimal PlatformFeeTotal { get; set; }
/// <summary>达人佣金(元)</summary>
/// <summary>达人佣金(元)。新接口未提供该字段,固定为 0。</summary>
public decimal Commission { get; set; }
/// <summary>客单价(元)</summary>
@@ -280,6 +359,51 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
Month = 2
}
/// <summary>
/// 接口聚合方式(请求参数 GroupType,按数字传值)。
/// 每种方式都返回 Dictionary&lt;店铺ID, List&lt;ShopStatsResponse&gt;&gt;:
/// 汇总时每个店铺 1 条,天/周/月时每个店铺每个周期 1 条。
/// </summary>
public enum ShopStatsGroupType
{
/// <summary>
/// 汇总:每个商铺只返回一条(整个查询范围合计)
/// </summary>
Summary = 0,
/// <summary>
/// 天:按自然日聚合,每个商铺每天一条
/// </summary>
Day = 1,
/// <summary>
/// 周:按自然周聚合(周一为一周开始),每个商铺每周一条
/// </summary>
Week = 2,
/// <summary>
/// 月:按自然月聚合,每个商铺每月一条
/// </summary>
Month = 3
}
/// <summary>
/// 订单明细类型(订单列表弹窗的入口类型,对应接口参数 OrderType,按数字传值)。
/// 数值与后端枚举保持一致:有效订单=1、退款订单=2、买手未退回金额的订单=3
/// (后端枚举里的“全部订单 All=0”本页不使用,故不定义)。
/// </summary>
public enum ShopOrderDetailType
{
/// <summary>有效订单:没有发生售后的订单(对应统计里的订单数 / 销售额 / 利润口径)</summary>
Valid = 1,
/// <summary>退款订单:发生过售后的订单(对应统计里的退款订单数口径)</summary>
Refund = 2,
/// <summary>买手未退回金额的订单:退款订单中买手款项还没退回的(对应统计里的未退回金额口径)</summary>
BuyerUnrefunded = 3
}
/// <summary>
/// 天/周/月周期统计表格行(金额单位:元)。
/// 按所选店铺(或全部店铺)统计:天统计每天一行;周统计每周一行;月统计每月一行。
@@ -292,7 +416,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
/// <summary>周期显示文本(天:yyyy-MM-dd;周:yyyy-MM-dd ~ yyyy-MM-dd;月:yyyy-MM)</summary>
public string Period { get; set; }
/// <summary>订单数</summary>
/// <summary>订单数(有效订单,不含退款订单)</summary>
public int OrderCount { get; set; }
/// <summary>销售额(元)</summary>
@@ -309,7 +433,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
/// <summary>平台服务费总额(元)</summary>
public decimal PlatformFeeTotal { get; set; }
/// <summary>达人佣金(元)</summary>
/// <summary>达人佣金(元)。新接口未提供该字段,固定为 0。</summary>
public decimal Commission { get; set; }
/// <summary>平均利润率(利润 / 采购金额,百分数,如 12.34 表示 12.34%)</summary>
@@ -322,14 +446,51 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
public decimal AvgOrderAmount { get; set; }
}
/// <summary>
/// 店铺营收统计 - 一次查询的全部聚合结果(综合统计页使用)。
/// 由两次请求组成:
/// shop_revenue_statistics/GetShopStats(GroupType=Summary)-> 卡片/店铺统计表/销售占比(每店铺一条,与区间长度无关)
/// shop_revenue_statistics/GetShopStatsTrend -> 销售趋势(跨店铺合计的按天序列)
/// </summary>
public class ShopRevenueStatisticsResult
{
/// <summary>汇总卡片数据</summary>
public ShopRevenueSummary Summary { get; set; }
/// <summary>销售趋势(按天,缺失日期补 0)</summary>
public List<RevenueTrendPoint> Trend { get; set; }
/// <summary>店铺销售占比(饼图)</summary>
public List<ShopSalesProportionItem> ShopProportions { get; set; }
/// <summary>店铺统计表格</summary>
public List<ShopStatisticsItem> ShopStatistics { get; set; }
}
/// <summary>
/// 店铺营收统计 - 天/周/月周期统计查询结果(周期统计页使用)。
/// 接口不返回整段汇总,Summary 由返回的周期数据在客户端合计得出。
/// </summary>
public class ShopPeriodStatisticsResult
{
/// <summary>汇总卡片数据(整个查询时间段,客户端合计)</summary>
public ShopRevenueSummary Summary { get; set; }
/// <summary>周期统计表格(天/周/月各一行)</summary>
public List<ShopPeriodStatisticsItem> Items { get; set; }
}
/// <summary>
/// 店铺下拉选项
/// </summary>
public class ShopOption
{
/// <summary>店铺 app_key(A.app_key)</summary>
public string AppKey { get; set; }
/// <summary>店铺ID(ShopLoginInfo.ShopId)</summary>
public string ShopId { get; set; }
/// <summary>ERP 店铺ID(ShopLoginInfo.ErpShopId),统计接口的 ShopIds 使用该字段</summary>
public string ErpShopId { get; set; }
/// <summary>店铺名称</summary>
public string ShopName { get; set; }
}
@@ -346,55 +507,4 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
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);
DateTime epoch = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1));
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);
DateTime epoch = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1));
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);
DateTime epoch = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1));
long milliseconds = (long)((DateTime)value - epoch).TotalMilliseconds;
writer.WriteValue(milliseconds);
}
}
}
File diff suppressed because it is too large. Load diff
@@ -1,23 +1,44 @@
<UserControl
<UserControl
x:Class="_29WxOrderWpf.Ctrls.Statistics.ShowOrderDialogView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:comn="clr-namespace:_29WxOrderWpf.Common"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:_29WxOrderWpf.Ctrls.Statistics"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:wpf="WPFCommon"
comn:DialogWindowProperty.Height="620"
comn:DialogWindowProperty.ResizeMode="CanResizeWithGrip"
comn:DialogWindowProperty.Width="1320"
d:Background="White"
d:DesignHeight="450"
d:DesignWidth="800"
d:DesignWidth="1000"
Focusable="True"
PreviewKeyDown="ShowOrderDialogView_PreviewKeyDown"
mc:Ignorable="d">
<UserControl.Resources>
<wpf:BindingProxy x:Key="ViewModel" Data="{Binding}" />
<!-- 单元格文字居中 -->
<Style x:Key="CenterCellTextStyle" TargetType="TextBlock">
<Setter Property="TextAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<!--
选中行高亮:用主题色填充选中行,并且不区分表格是否拥有焦点
(默认样式的选中色在焦点离开表格后会变成浅灰,点了“复制数据”就看不出选中了哪些行)
-->
<Style x:Key="SelectedCellStyle" TargetType="DataGridCell">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{StaticResource pageColorBrush}" />
<Setter Property="Foreground" Value="White" />
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Grid>
@@ -36,15 +57,15 @@
Width="180"
Placement="Left"
TitleVerticalAlignment="Center">
<TextBox Height="22" Text="{Binding OrderId, UpdateSourceTrigger=PropertyChanged}" />
<TextBox
Height="22"
Text="{Binding OrderId, UpdateSourceTrigger=PropertyChanged}"
ToolTip="按订单号筛选;表格内可用 Ctrl+A 全选、Ctrl+C 复制数据" />
</wpf:TitleContent>
<Button
Command="{Binding SelectAllCommand}"
Content="复制数据"
Style="{StaticResource BaseButtonLightStyle}" />
</wpf:SpacingStackPanel>
</Border>
<DataGrid
x:Name="dgOrders"
Grid.Row="1"
d:ItemsSource="{d:SampleData ItemCount=10}"
AlternatingRowBackground="#FAFAFA"
@@ -55,6 +76,7 @@
CanUserDeleteRows="False"
CanUserReorderColumns="False"
CanUserResizeRows="False"
CellStyle="{StaticResource SelectedCellStyle}"
ColumnHeaderStyle="{StaticResource BaseDataGridColumnHeaderStyle}"
GridLinesVisibility="Horizontal"
HeadersVisibility="Column"
@@ -62,14 +84,9 @@
IsReadOnly="True"
ItemsSource="{Binding DataView}"
RowHeight="44"
SelectionMode="Single"
SelectionMode="Extended"
SelectionUnit="FullRow"
VerticalScrollBarVisibility="Auto">
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<!-- 店名 -->
<DataGridTextColumn
@@ -93,7 +110,6 @@
<Button
Margin="5,0,0,0"
Click="CopyOrderId_Click"
Foreground="{StaticResource pageColorBrush}"
ToolTip="复制订单号">
<Button.Style>
<Style BasedOn="{StaticResource IconButtonStyle}" TargetType="Button">
@@ -113,6 +129,13 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- 创建时间 -->
<DataGridTextColumn
Width="1.4*"
Binding="{Binding CreateTime, StringFormat={}{0:yyyy-MM-dd HH:mm}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="创建时间" />
<!-- 金额(分转元) -->
<DataGridTextColumn
Width="*"
@@ -121,7 +144,7 @@
Header="金额" />
<DataGridTextColumn
Width="*"
Binding="{Binding RefundAmount, Converter={StaticResource FenToYuanConverter}, StringFormat={}¥{0:N2}}"
Binding="{Binding OrderRefundAmount, Converter={StaticResource FenToYuanConverter}, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="退款金额" />
<DataGridTextColumn
@@ -129,6 +152,54 @@
Binding="{Binding OutAmount, Converter={StaticResource FenToYuanConverter}, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="外采金额" />
<DataGridTextColumn
Width="*"
Binding="{Binding UnrefundedAmount, Converter={StaticResource FenToYuanConverter}, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="未退回金额" />
<!-- 买手订单:一笔买手订单占一行,不合并 -->
<DataGridTextColumn
Width="1.6*"
Binding="{Binding OutOrderSn}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="买手单号" />
<DataGridTextColumn
Width="*"
Binding="{Binding BuyerOutAmount, Converter={StaticResource FenToYuanConverter}, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="买手金额" />
<DataGridTextColumn
Width="*"
Binding="{Binding BuyerRefundAmount, Converter={StaticResource FenToYuanConverter}, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="买手退款" />
<DataGridTextColumn
Width="1.1*"
Binding="{Binding BuyerRefundStatusText}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="退款状态" />
<!-- 操作:仅“买手未退回金额的订单”弹窗显示(列可见性在构造函数里按订单明细类型设置) -->
<DataGridTemplateColumn
x:Name="colUpdateRefund"
Width="*"
d:Visibility="Visible"
Header="操作"
Visibility="Collapsed">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button
HorizontalAlignment="Center"
VerticalAlignment="Center"
Command="{Binding Source={StaticResource ViewModel}, Path=Data.UpdateBuyerRefundStatusCommand}"
CommandParameter="{Binding}"
Content="退款成功"
Style="{StaticResource BasePageButtonLightStyle}"
ToolTip="把该笔买手订单标记为退款成功" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
@@ -1,7 +1,10 @@
using _29WxOrderWpf.BaseWindows;
using _29WxOrderWpf.BaseWindows;
using _29WxOrderWpf.Commands;
using _29WxOrderWpf.Common;
using _29WxOrderWpf.Ctrls.Statistics.Models;
using _29WxOrderWpf.Ctrls.Statistics.Services;
using _29WxOrderWpf.ToolCtrls;
using NaiBa.Util.Extension;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -11,6 +14,7 @@ using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
@@ -27,10 +31,41 @@ namespace _29WxOrderWpf.Ctrls.Statistics;
/// </summary>
public partial class ShowOrderDialogView : UserControl
{
public ShowOrderDialogView(List<ShopRevenueStatisticsRow> list)
private readonly KeyEventHandler shortcutKeyHandler;
public ShowOrderDialogView(List<ShopOrderDetailResponse> list, ShopOrderDetailType detailType, IShopRevenueStatisticsService statisticsService)
{
InitializeComponent();
this.DataContext = new ShowOrderDialogViewModel(list);
this.DataContext = new ShowOrderDialogViewModel(list, statisticsService);
// “操作”列(退款成功按钮)只在“买手未退回金额的订单”弹窗里显示
if (detailType == ShopOrderDetailType.BuyerUnrefunded)
colUpdateRefund.Visibility = Visibility.Visible;
shortcutKeyHandler = ShowOrderDialogView_PreviewKeyDown;
// 快捷键必须挂到弹窗窗口上:弹窗刚打开时键盘焦点在窗口本身(不在本控件的可视树里),
// 事件路由中就不会有本控件,只在本控件上挂 PreviewKeyDown 的话,
// 必须先用鼠标点一下表格(焦点进到表格里)Ctrl+A/Ctrl+C 才会生效。
Loaded += (s, e) => AttachShortcutHandler();
Unloaded += (s, e) => DetachShortcutHandler();
}
/// <summary>把快捷键处理器挂到所在窗口(隧道事件,焦点在窗口上的任意位置都能收到)</summary>
private void AttachShortcutHandler()
{
Window window = Window.GetWindow(this);
if (window == null)
return;
// 先移除再挂,避免重复挂载后按一次键执行两次
window.RemoveHandler(PreviewKeyDownEvent, shortcutKeyHandler);
window.AddHandler(PreviewKeyDownEvent, shortcutKeyHandler);
}
private void DetachShortcutHandler()
{
Window.GetWindow(this)?.RemoveHandler(PreviewKeyDownEvent, shortcutKeyHandler);
}
/// <summary>
@@ -39,7 +74,7 @@ public partial class ShowOrderDialogView : UserControl
private void CopyOrderId_Click(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement element
&& element.DataContext is ShopRevenueStatisticsRow row
&& element.DataContext is ShopOrderDetailRow row
&& !string.IsNullOrEmpty(row.OrderId))
{
Window window = Window.GetWindow(this);
@@ -54,25 +89,170 @@ public partial class ShowOrderDialogView : UserControl
}
}
}
/// <summary>
/// 弹窗级快捷键:Ctrl+A 全选(筛选后显示的)行,Ctrl+C 复制数据。
/// 焦点在文本框里(如订单号搜索框)时不拦截,保留文本自身的 Ctrl+A/Ctrl+C/Ctrl+V 行为。
/// </summary>
private void ShowOrderDialogView_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Handled || Keyboard.Modifiers != ModifierKeys.Control)
return;
// 文本框(含搜索框)优先,走文本自己的全选/复制/粘贴
if (Keyboard.FocusedElement is TextBoxBase)
return;
if (DataContext is not ShowOrderDialogViewModel viewModel)
return;
if (e.Key == Key.A)
{
SelectAllRows(viewModel);
e.Handled = true;
}
else if (e.Key == Key.C)
{
// 有选中就复制选中的行,没有选中则复制筛选后的全部数据
viewModel.CopyRows(GetSelectedRows());
e.Handled = true;
}
}
/// <summary>
/// Ctrl+A:全选当前筛选后显示的所有行(被过滤掉的行不会选中)。
/// 选中行取自与复制同一份筛选结果,保证“选中的”和“复制的”完全一致。
/// </summary>
private void SelectAllRows(ShowOrderDialogViewModel viewModel)
{
// 让键盘焦点回到表格,全选后仍可用键盘继续操作
dgOrders.Focus();
dgOrders.SelectedItems.Clear();
foreach (var row in viewModel.GetFilteredRows())
{
dgOrders.SelectedItems.Add(row);
}
}
/// <summary>当前选中的行</summary>
private List<ShopOrderDetailRow> GetSelectedRows()
{
return dgOrders.SelectedItems.OfType<ShopOrderDetailRow>().ToList();
}
}
/// <summary>
/// 订单明细表格行:把一个商铺订单按买手订单“展平”成多行(有几笔买手订单就几行,
/// 没有买手订单时占一行且买手字段为空),不把多笔买手订单合并到同一行。
/// 商铺订单级字段(店名/订单号/创建时间/金额等)在多行之间重复,保证每行都是一条完整记录。
/// </summary>
class ShopOrderDetailRow : BindableBaseEx
{
public string ShopName { get; set; }
public string OrderId { get; set; }
public DateTime CreateTime { get; set; }
public int OrderPrice { get; set; }
public int OrderRefundAmount { get; set; }
public int OutAmount { get; set; }
public int UnrefundedAmount { get; set; }
/// <summary>买手订单表ID</summary>
public long BuyerId { get; set; }
/// <summary>买手(外部)订单号;无买手订单时为空</summary>
public string OutOrderSn { get; set; }
/// <summary>买手订单金额(分)</summary>
public int BuyerOutAmount { get; set; }
/// <summary>
/// 买手退款表 id
/// </summary>
public long BuyerRefundId { get; set; }
/// <summary>买手订单退款金额(分)</summary>
public int BuyerRefundAmount { get; set; }
/// <summary>买手订单退款状态(更新成功后就地改为“退款成功”)</summary>
public string BuyerRefundStatusText
{
get => GetValue<string>();
set => SetValue(value);
}
/// <summary>把接口返回的订单明细按买手订单展平成表格行</summary>
public static List<ShopOrderDetailRow> Flatten(IEnumerable<ShopOrderDetailResponse> list)
{
var rows = new List<ShopOrderDetailRow>();
if (list == null)
return rows;
foreach (var item in list)
{
if (item == null)
continue;
if (item.BuyerOrders == null || item.BuyerOrders.Count == 0)
{
rows.Add(Create(item, null));
continue;
}
foreach (var buyerOrder in item.BuyerOrders)
rows.Add(Create(item, buyerOrder));
}
return rows;
}
private static ShopOrderDetailRow Create(ShopOrderDetailResponse item, BuyerOrderInfo buyerOrder)
{
return new ShopOrderDetailRow
{
ShopName = item.ShopName,
OrderId = item.OrderId,
CreateTime = item.CreateTime,
OrderPrice = item.OrderPrice,
OrderRefundAmount = item.OrderRefundAmount,
OutAmount = item.OutAmount,
UnrefundedAmount = item.UnrefundedAmount,
BuyerId = buyerOrder == null ? 0 : buyerOrder.Id,
OutOrderSn = buyerOrder == null ? null : buyerOrder.OutOrderSn,
BuyerRefundId = buyerOrder == null ? 0 : buyerOrder.RefundId,
BuyerOutAmount = buyerOrder == null ? 0 : buyerOrder.OutAmount,
BuyerRefundAmount = buyerOrder == null ? 0 : buyerOrder.RefundAmount,
BuyerRefundStatusText = buyerOrder == null ? null : buyerOrder.RefundStatusText,
};
}
}
class ShowOrderDialogViewModel : BindableBaseEx
{
public ObservableCollection<ShopRevenueStatisticsRow> Data { get; set; }
private readonly IShopRevenueStatisticsService _statisticsService;
public ObservableCollection<ShopOrderDetailRow> Data { get; set; }
public ICollectionView DataView { get; private set; }
public ShowOrderDialogViewModel(List<ShopRevenueStatisticsRow> list)
public ShowOrderDialogViewModel(List<ShopOrderDetailResponse> list, IShopRevenueStatisticsService statisticsService)
{
Data = new ObservableCollection<ShopRevenueStatisticsRow>(list ?? new List<ShopRevenueStatisticsRow>());
// 按买手订单展平:一笔买手订单一一行,不合并
Data = new ObservableCollection<ShopOrderDetailRow>(ShopOrderDetailRow.Flatten(list));
DataView = CollectionViewSource.GetDefaultView(Data);
DataView.Filter = FilterRow;
_statisticsService = statisticsService;
}
private bool FilterRow(object obj)
{
if (string.IsNullOrWhiteSpace(OrderId))
return true;
if (obj is ShopRevenueStatisticsRow row)
if (obj is ShopOrderDetailRow row)
{
return row.OrderId.Contains(OrderId);
return (row.OrderId ?? string.Empty).Contains(OrderId);
}
return false;
}
@@ -83,33 +263,102 @@ class ShowOrderDialogViewModel : BindableBaseEx
set => SetValue(value, () => DataView.Refresh());
}
public ICommand SelectAllCommand => new DelegateCommand(SelectAll);
/// <summary>当前筛选后显示的行(按表格显示顺序)</summary>
public List<ShopOrderDetailRow> GetFilteredRows()
{
return DataView.Cast<ShopOrderDetailRow>().ToList();
}
private void SelectAll()
/// <summary>
/// 复制指定行到剪贴板(带表头,制表符分隔)。
/// rows 为空时复制筛选后的全部数据;复制结果用提示条反馈。
/// </summary>
public void CopyRows(IEnumerable<ShopOrderDetailRow> rows)
{
List<ShopOrderDetailRow> list = rows?.ToList() ?? new List<ShopOrderDetailRow>();
if (list.Count == 0)
{
// 没有选中行时,复制筛选后的全部数据
list = GetFilteredRows();
}
if (list.Count == 0)
{
ToastHelper.Show("没有可复制的数据", type: ToastType.Warning);
return;
}
try
{
string text = BuildCopyText(list);
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text));
ToastHelper.Show("复制成功");
}
catch (Exception ex)
{
ToastHelper.Show("复制失败 " + ex.Message, type: ToastType.Error);
}
}
public ICommand UpdateBuyerRefundStatusCommand => new AsyncDelegateCommand<ShopOrderDetailRow>(UpdateBuyerRefundStatus);
private async Task UpdateBuyerRefundStatus(ShopOrderDetailRow row)
{
if (row.BuyerRefundId == 0)
{
ToastHelper.Show("该行缺少买手订单ID,无法更新", type: ToastType.Warning);
return;
}
try
{
var ret = await _statisticsService.UpdateBuyerRefundStatusAsync(row.BuyerRefundId);
if (ret)
{
row.BuyerRefundStatusText = "退款成功";
ToastHelper.Show("操作成功");
}
else
throw new Exception("状态更改失败");
}
catch (Exception ex)
{
ToastHelper.Show("操作失败 " + ex.Message, type: ToastType.Error);
}
finally
{
}
}
/// <summary>把行拼成制表符分隔的文本(第一行是表头)</summary>
private string BuildCopyText(IEnumerable<ShopOrderDetailRow> rows)
{
var sb = new StringBuilder();
// 表头
sb.AppendLine("店名\t订单\t金额\t退款金额\t外采金额");
sb.AppendLine("店名\t订单\t创建时间\t金额\t退款金额\t外采金额\t未退回金额\t买手单号\t买手金额\t买手退款\t退款状态");
// 数据行
foreach (var item in Data)
foreach (var item in rows)
{
string shopName = CsvEscape(item.ShopName);
string orderId = item.OrderId;
string createTime = item.CreateTime.ToString("yyyy-MM-dd HH:mm");
string orderPrice = (item.OrderPrice / 100m).ToString("0.00");
string refundAmount = (item.RefundAmount / 100m).ToString("0.00");
string refundAmount = (item.OrderRefundAmount / 100m).ToString("0.00");
string outAmount = (item.OutAmount / 100m).ToString("0.00");
string unrefundedAmount = (item.UnrefundedAmount / 100m).ToString("0.00");
string buyerOrderSn = CsvEscape(item.OutOrderSn);
string buyerOutAmount = (item.BuyerOutAmount / 100m).ToString("0.00");
string buyerRefundAmount = (item.BuyerRefundAmount / 100m).ToString("0.00");
string buyerRefundStatus = CsvEscape(item.BuyerRefundStatusText);
sb.AppendLine($"{shopName}\t{orderId}\t{orderPrice}\t{refundAmount}\t{outAmount}");
sb.AppendLine($"{shopName}\t{orderId}\t{createTime}\t{orderPrice}\t{refundAmount}\t{outAmount}\t{unrefundedAmount}\t{buyerOrderSn}\t{buyerOutAmount}\t{buyerRefundAmount}\t{buyerRefundStatus}");
}
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(sb.ToString()));
var window = Application.Current.Windows
.OfType<Window>()
.FirstOrDefault(w => w.IsActive) ?? Application.Current.MainWindow;
ToastHelper.Show(window, "复制成功");
return sb.ToString();
}
private string CsvEscape(string value)
@@ -1,4 +1,4 @@
using _29WxOrderWpf.Commands;
using _29WxOrderWpf.Commands;
using _29WxOrderWpf.Common;
using _29WxOrderWpf.Ctrls.Statistics.Models;
using _29WxOrderWpf.Ctrls.Statistics.Services;
@@ -39,6 +39,12 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
private int _totalCount;
private string _pagerText = string.Empty;
/// <summary>是否已完成首次初始化(用于避免页签来回切换时重复初始化、重复查询)</summary>
private bool _initialized;
/// <summary>上一次真正生效(并据此计算过日期)的时间范围选项,避免同一个选项被重复应用</summary>
private TimeRangeOption _appliedTimeRange;
public PeriodRevenueStatisticsViewModel(PeriodStatisticType periodType)
: this(periodType, new ShopRevenueStatisticsService())
{
@@ -65,8 +71,18 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
}
/// <summary>
/// 页面首次显示时初始化查询条件并加载数据(由外层 TabControl 的选中事件调用)。
/// 重复调用(在“综合统计 / 天统计 / 周统计 / 月统计”页签之间来回切换)会被忽略:
/// 否则每次切回来都会把时间范围和店铺重置成默认值,并重新发起一次查询。
/// 需要刷新数据时点“查询”按钮即可。
/// </summary>
public void Init()
{
if (_initialized)
return;
_initialized = true;
InitializeAsync();
}
@@ -172,7 +188,16 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
set
{
this.SetValue(x => x.SelectedTimeRange, value);
ApplyTimeRange();
// 只有用户真正切换到“另一个”时间范围选项时才重新计算起止日期。
// 页签切换时下拉框会把同一个选项(甚至先 null 再回写)重放一遍,
// 若照样重算,就会把用户手工调过的日期覆盖掉(例如自定义区间被改回“本周”)。
if (value != null && !ReferenceEquals(_appliedTimeRange, value))
{
_appliedTimeRange = value;
ApplyTimeRange();
}
OnPropertyChanged(nameof(IsCustomDateRange));
}
}
@@ -481,25 +506,28 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
}
}
/// <summary>根据选中店铺生成查询店铺 key 列表:未选/全选/选中“全部店铺”时返回空列表(表示全部店铺)</summary>
private List<string> BuildSelectedShopKeys()
/// <summary>
/// 根据选中店铺生成查询用 ERP 店铺ID 列表(接口 ShopIds 使用 ShopLoginInfo.ErpShopId):
/// 未选/全选/选中“全部店铺”时返回空列表(表示全部店铺)。
/// </summary>
private List<string> BuildSelectedShopIds()
{
var selected = SelectedShops;
if (selected == null || selected.Count == 0)
return new List<string>();
var keys = selected.Select(s => s == null ? string.Empty : s.AppKey).ToList();
if (keys.Any(string.IsNullOrEmpty))
var ids = selected.Select(s => s == null ? string.Empty : s.ErpShopId).ToList();
if (ids.Any(string.IsNullOrEmpty))
return new List<string>();
if (selected.Count >= ShopOptions.Count)
return new List<string>();
return keys;
return ids;
}
/// <summary>执行查询,刷新周期统计表格并回到第一页</summary>
private async Task LoadAsync()
/// <summary>组装查询参数(起止日期已保证先后顺序)</summary>
private ShopRevenueStatisticsQuery BuildQuery()
{
if (StartDate > EndDate)
{
@@ -508,19 +536,23 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
EndDate = temp;
}
return new ShopRevenueStatisticsQuery
{
StartDate = StartDate,
EndDate = EndDate,
ShopIds = BuildSelectedShopIds(),
};
}
/// <summary>执行查询,刷新周期统计表格并回到第一页</summary>
private async Task LoadAsync()
{
IsLoading = true;
try
{
var query = new ShopRevenueStatisticsQuery
{
StartDate = StartDate,
EndDate = EndDate,
ShopKeys = BuildSelectedShopKeys(),
};
var table = await _service.GetPeriodStatisticsAsync(query, _periodType);
Summary = await _service.GetSummaryAsync(query);
_allItems = table ?? new List<ShopPeriodStatisticsItem>();
var result = await _service.QueryPeriodStatisticsAsync(BuildQuery(), _periodType);
Summary = result.Summary;
_allItems = result.Items ?? new List<ShopPeriodStatisticsItem>();
TotalCount = _allItems.Count;
TotalPages = TotalCount == 0 ? 1 : (int)Math.Ceiling(TotalCount / (double)PageSize);
GoToPage(1);
@@ -553,35 +585,51 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
private async Task ShowOrderList(string type)
{
var query = new ShopRevenueStatisticsQuery
var detailType = ParseDetailType(type);
List<ShopOrderDetailResponse> showList;
// 明细接口数据量大、耗时长,期间显示页面统一的加载遮罩,避免点了没反应
IsLoading = true;
try
{
StartDate = StartDate,
EndDate = EndDate,
ShopKeys = BuildSelectedShopKeys(),
};
var rows = await _service.QueryRowsAsync(query);
List<ShopRevenueStatisticsRow> showList = new List<ShopRevenueStatisticsRow>();
switch (type)
showList = await _service.QueryOrderRowsAsync(BuildQuery(), detailType)
?? new List<ShopOrderDetailResponse>();
}
catch (Exception ex)
{
case "ValidOrderCount":
showList = RevenueStatisticsAggregator.GetValidOrder(rows);
break; //有效订单
case "RefundOrderCount":
showList = RevenueStatisticsAggregator.GetRefundOrder(rows);
break;//退款订单
case "BuyerRefundRemainingAmount":
showList = RevenueStatisticsAggregator.GetBuyerRefundRemainingOrder(rows);
break;//待退回采购金额
default:
break;
MessageBox.Show("订单明细查询失败:" + ex.Message, "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
finally
{
IsLoading = false;
}
if (showList.Count == 0)
{
MessageBox.Show("未查询到符合条件的订单明细。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
new DialogWindow()
{
Title = "订单列表",
Content = new ShowOrderDialogView(showList),
DataContext = rows
Content = new ShowOrderDialogView(showList, detailType, _service),
DataContext = showList
}.ShowDialog();
}
/// <summary>卡片“明细”按钮参数 → 订单明细类型(对应接口参数 OrderType)</summary>
private static ShopOrderDetailType ParseDetailType(string type)
{
switch (type)
{
case "RefundOrderCount":
return ShopOrderDetailType.Refund; //退款订单
case "BuyerRefundRemainingAmount":
return ShopOrderDetailType.BuyerUnrefunded; //买手未退回金额的订单
default:
return ShopOrderDetailType.Valid; //有效订单
}
}
}
}
@@ -1,4 +1,4 @@
using _29WxOrderWpf.Commands;
using _29WxOrderWpf.Commands;
using _29WxOrderWpf.Common;
using _29WxOrderWpf.Ctrls.Statistics.Models;
using _29WxOrderWpf.Ctrls.Statistics.Services;
@@ -12,6 +12,7 @@ using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using Work.BaseForms;
namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
{
@@ -31,6 +32,9 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
private ObservableCollection<ShopOption> _selectedShops;
private string _selectedPlatform;
/// <summary>上一次真正生效(并据此计算过日期)的时间范围选项,避免同一个选项被重复应用</summary>
private TimeRangeOption _appliedTimeRange;
public ShopRevenueStatisticsViewModel()
: this(new ShopRevenueStatisticsService())
{
@@ -70,7 +74,16 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
set
{
this.SetValue(x => x.SelectedTimeRange, value);
ApplyTimeRange();
// 只有用户真正切换到“另一个”时间范围选项时才重新计算起止日期。
// 页签切换时下拉框会把同一个选项(甚至先 null 再回写)重放一遍,
// 若照样重算,就会把用户手工调过的日期覆盖掉。
if (value != null && !ReferenceEquals(_appliedTimeRange, value))
{
_appliedTimeRange = value;
ApplyTimeRange();
}
OnPropertyChanged(nameof(IsCustomDateRange));
}
}
@@ -235,57 +248,66 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
}
}
/// <summary>根据选中店铺生成查询店铺 key 列表:未选/全选/选中“全部店铺”时返回空列表(表示全部店铺)</summary>
private List<string> BuildSelectedShopKeys()
/// <summary>
/// 根据选中店铺生成查询用 ERP 店铺ID 列表(接口 ShopIds 使用 ShopLoginInfo.ErpShopId):
/// 未选/全选/选中“全部店铺”时返回空列表(表示全部店铺)。
/// </summary>
private List<string> BuildSelectedShopIds()
{
var selected = SelectedShops;
if (selected == null || selected.Count == 0)
return new List<string>();
var keys = selected.Select(s => s == null ? string.Empty : s.AppKey).ToList();
if (keys.Any(string.IsNullOrEmpty))
var ids = selected.Select(s => s == null ? string.Empty : s.ErpShopId).ToList();
if (ids.Any(string.IsNullOrEmpty))
return new List<string>();
if (selected.Count >= ShopOptions.Count)
return new List<string>();
return keys;
return ids;
}
/// <summary>执行查询,刷新页面所有统计数据</summary>
private async Task LoadAsync()
/// <summary>组装查询参数(起止日期已保证先后顺序)</summary>
private ShopRevenueStatisticsQuery BuildQuery()
{
if (StartDate > EndDate)
if (StartDate == EndDate)
{
EndDate = EndDate.AddDays(1).AddSeconds(-1);
}
else if (StartDate > EndDate)
{
var temp = StartDate;
StartDate = EndDate;
EndDate = temp;
}
return new ShopRevenueStatisticsQuery
{
StartDate = StartDate,
EndDate = EndDate,
ShopIds = BuildSelectedShopIds(),
};
}
/// <summary>执行查询,刷新页面所有统计数据</summary>
private async Task LoadAsync()
{
IsLoading = true;
try
{
var query = new ShopRevenueStatisticsQuery
{
StartDate = StartDate,
EndDate = EndDate,
ShopKeys = BuildSelectedShopKeys(),
};
var result = await _service.QueryAsync(BuildQuery());
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);
Summary = result.Summary;
TrendData = new ObservableCollection<RevenueTrendPoint>(result.Trend ?? new List<RevenueTrendPoint>());
ShopProportionData = new ObservableCollection<ShopSalesProportionItem>(result.ShopProportions ?? new List<ShopSalesProportionItem>());
ShopStatistics = new ObservableCollection<ShopStatisticsItem>(result.ShopStatistics ?? new List<ShopStatisticsItem>());
}
catch (Exception)
catch (Exception ex)
{
// 查询异常:保留上一次数据,可在此接入日志/错误提示
// 查询异常:保留上一次数据,并弹框提示
BaseMsgForm.ShowError("查询失败:" + ex.Message, "提示");
}
finally
{
@@ -309,35 +331,53 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
private async Task ShowOrderList(string type)
{
var query = new ShopRevenueStatisticsQuery
var detailType = ParseDetailType(type);
List<ShopOrderDetailResponse> showList;
// 明细接口数据量大、耗时长,期间显示页面统一的加载遮罩,避免点了没反应
IsLoading = true;
try
{
StartDate = StartDate,
EndDate = EndDate,
ShopKeys = BuildSelectedShopKeys(),
};
var rows = await _service.QueryRowsAsync(query);
List<ShopRevenueStatisticsRow> showList = new List<ShopRevenueStatisticsRow>();
switch (type)
{
case "ValidOrderCount":
showList = RevenueStatisticsAggregator.GetValidOrder(rows);
break; //有效订单
case "RefundOrderCount":
showList = RevenueStatisticsAggregator.GetRefundOrder(rows);
break;//退款订单
case "BuyerRefundRemainingAmount":
showList = RevenueStatisticsAggregator.GetBuyerRefundRemainingOrder(rows);
break;//待退回采购金额
default:
break;
showList = await _service.QueryOrderRowsAsync(BuildQuery(), detailType)
?? new List<ShopOrderDetailResponse>();
}
catch (Exception ex)
{
BaseMsgForm.ShowError("订单明细查询失败:" + ex.Message, "提示");
return;
}
finally
{
IsLoading = false;
}
if (showList.Count == 0)
{
BaseMsgForm.ShowInfo("未查询到符合条件的订单明细。", "提示");
return;
}
new DialogWindow(GetWindow())
{
Title="订单列表",
Content = new ShowOrderDialogView(showList),
DataContext = rows
Title = "订单列表",
Content = new ShowOrderDialogView(showList, detailType, _service),
DataContext = showList
}.Show();
}
/// <summary>卡片“明细”按钮参数 → 订单明细类型(对应接口参数 OrderType)</summary>
private static ShopOrderDetailType ParseDetailType(string type)
{
switch (type)
{
case "RefundOrderCount":
return ShopOrderDetailType.Refund; //退款订单
case "BuyerRefundRemainingAmount":
return ShopOrderDetailType.BuyerUnrefunded; //买手未退回金额的订单
default:
return ShopOrderDetailType.Valid; //有效订单
}
}
public Func<Window> GetWindow;
}
}
+8 -1
View File
@@ -93,7 +93,14 @@ public static class ToastHelper
};
toast.BeginAnimation(UIElement.OpacityProperty, anim);
}
public static void Show(string message,
ToastType type = ToastType.Info, int durationMs = 2000)
{
Window window = Application.Current.Windows
.OfType<Window>()
.FirstOrDefault(w => w.IsActive) ?? Application.Current.MainWindow;
Show(window, message, type, durationMs);
}
private static Border CreateToast(string message, ToastType type)
{
var color = type switch
+1 -1
View File
@@ -37,7 +37,7 @@ namespace AllBase
/// 版本ID
/// </summary>
public static int VersionCount = 306;
public static int VersionCount = 310;
public static string ServerUrl { get; set; } = "http://1.13.195.131:9000";
public static string DebugUrl { get; set; } = "http://localhost:9000";