Files
29WxOrder/29WxOrderWpf/Ctrls/Statistics/ShowOrderDialogView.xaml.cs
T

458 lines
13 KiB
C#

using _29WxOrderWpf.BaseWindows;
using WxOrder.Framework.Commons;
using _29WxOrderWpf.Ctrls.Statistics.Models;
using _29WxOrderWpf.Ctrls.Statistics.Services;
using _29WxOrderWpf.ToolCtrls;
using AllBase;
using NaiBa.Util.Extension;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
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;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Work.BaseForms;
using WPFCommon.Models;
namespace _29WxOrderWpf.Ctrls.Statistics;
/// <summary>
/// ShowOrderDialogView.xaml 的交互逻辑
/// </summary>
public partial class ShowOrderDialogView : UserControl
{
private readonly KeyEventHandler shortcutKeyHandler;
public ShowOrderDialogView(List<ShopOrderDetailResponse> list, ShopOrderDetailType detailType, IShopRevenueStatisticsService statisticsService)
{
InitializeComponent();
this.DataContext = new ShowOrderDialogViewModel(list, detailType, statisticsService);
// “操作”列(退款成功按钮)只在“买手未退回金额的订单”弹窗里显示
if (detailType == ShopOrderDetailType.BuyerUnrefunded)
{
colUpdateRefund.Visibility = Visibility.Visible;
//colRemark.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>
/// 复制订单号到剪贴板
/// </summary>
private void CopyOrderId_Click(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement element
&& element.DataContext is ShopOrderDetailRow row
&& !string.IsNullOrEmpty(row.OrderId))
{
Window window = Window.GetWindow(this);
try
{
Clipboard.SetDataObject(row.OrderId);
ToastHelper.Show(window, "复制成功");
}
catch (Exception ex)
{
ToastHelper.Show(window, "复制失败 " + ex.Message, type: ToastType.Error);
}
}
}
/// <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; }
/// <summary>
/// 利润
/// </summary>
public long ProfitAmount { get; set; }
/// <summary>
/// 平台佣金
/// </summary>
public long PlatformFee { 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);
}
public string Remark
{
get => GetValue<string>();
set => SetValue(value);
}
/// <summary>把接口返回的订单明细按买手订单展平成表格行</summary>
public static List<ShopOrderDetailRow> Flatten(IEnumerable<ShopOrderDetailResponse> list, ShopOrderDetailType detailType)
{
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;
}
if (detailType == ShopOrderDetailType.Profit)
{
// 利润订单只取最后一个买手订单
rows.Add(Create(item, item.BuyerOrders.LastOrDefault()));
}
else
{
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,
ProfitAmount = item.ProfitAmount,
PlatformFee = item.PlatformFee,
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,
Remark = buyerOrder == null ? null : buyerOrder.Remark,
};
}
}
class ShowOrderDialogViewModel : BindableBaseEx
{
private readonly ShopOrderDetailType _detailType;
private readonly IShopRevenueStatisticsService _statisticsService;
public ObservableCollection<ShopOrderDetailRow> Data { get; set; }
public ICollectionView DataView { get; private set; }
public ShowOrderDialogViewModel(List<ShopOrderDetailResponse> list, ShopOrderDetailType detailType, IShopRevenueStatisticsService statisticsService)
{
IsBuyerUnrefunded = detailType == ShopOrderDetailType.BuyerUnrefunded;
// 按买手订单展平:一笔买手订单一一行, 利润明细合并,只展示一条
Data = new ObservableCollection<ShopOrderDetailRow>(ShopOrderDetailRow.Flatten(list, detailType));
DataView = CollectionViewSource.GetDefaultView(Data);
DataView.Filter = FilterRow;
_detailType = detailType;
_statisticsService = statisticsService;
}
private bool FilterRow(object obj)
{
if (string.IsNullOrWhiteSpace(OrderId))
return true;
if (obj is ShopOrderDetailRow row)
{
return (row.OrderId ?? string.Empty).Contains(OrderId);
}
return false;
}
public string OrderId
{
get => GetValue<string>();
set => SetValue(value, () => DataView.Refresh());
}
public bool IsBuyerUnrefunded
{
get => GetValue<bool>();
set => SetValue(value);
}
/// <summary>当前筛选后显示的行(按表格显示顺序)</summary>
public List<ShopOrderDetailRow> GetFilteredRows()
{
return DataView.Cast<ShopOrderDetailRow>().ToList();
}
/// <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;
}
var result = MessageBox.Show($"确定要把订单:{row.OrderId}\n设为已退款成功状态?", "提示",
MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result != MessageBoxResult.Yes)
{
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
{
}
}
public ICommand UpdateRemarkCommand => new AsyncDelegateCommand<ShopOrderDetailRow>(UpdateRemark);
private async Task UpdateRemark(ShopOrderDetailRow row)
{
string? remark = InputDialog.ShowText("请输入备注:", "备注", row.Remark, allowEmpty: true);
if (remark == null)
return;
try
{
var ret = await _statisticsService.UpdateBuyerAfterOrderRemark(row.BuyerRefundId, remark);
if (ret)
{
row.Remark = remark;
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();
bool includeRemark = _detailType == ShopOrderDetailType.BuyerUnrefunded;
// 表头
List<string> headers = ["店名", "订单", "创建时间", "金额", "退款金额", "平台费用", "利润", "买手单号", "外采金额", "买手退款", "退款状态"];
if (includeRemark)
headers.Add("备注");
sb.AppendLine(string.Join("\t", headers));
// 数据行
foreach (var item in rows)
{
var cols = new List<string>
{
CsvEscape(item.ShopName),
item.OrderId,
item.CreateTime.ToString("yyyy-MM-dd HH:mm"),
(item.OrderPrice / 100m).ToString("0.00"),
(item.OrderRefundAmount / 100m).ToString("0.00"),
(item.PlatformFee / 100m).ToString("0.00"),
(item.ProfitAmount / 100m).ToString("0.00"),
CsvEscape(item.OutOrderSn),
(item.BuyerOutAmount / 100m).ToString("0.00"),
(item.BuyerRefundAmount / 100m).ToString("0.00"),
CsvEscape(item.BuyerRefundStatusText)
};
if (includeRemark)
cols.Add(CsvEscape(item.Remark));
sb.AppendLine(string.Join("\t", cols));
}
return sb.ToString();
}
private string CsvEscape(string value)
{
if (string.IsNullOrEmpty(value))
return "";
// 含逗号、引号、换行时,用双引号包起来,内部引号翻倍
if (value.Contains(',') || value.Contains('"') || value.Contains('\n') || value.Contains('\r'))
{
return "\"" + value.Replace("\"", "\"\"") + "\"";
}
return value;
}
}