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

326 lines
10 KiB
C#

using _29WxOrderWpf.BaseWindows;
using _29WxOrderWpf.Commands;
using _29WxOrderWpf.Ctrls.Statistics.Models;
using _29WxOrderWpf.ToolCtrls;
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 WPFCommon.Models;
namespace _29WxOrderWpf.Ctrls.Statistics;
/// <summary>
/// ShowOrderDialogView.xaml 的交互逻辑
/// </summary>
public partial class ShowOrderDialogView : UserControl
{
private readonly KeyEventHandler shortcutKeyHandler;
public ShowOrderDialogView(List<ShopOrderDetailResponse> list)
{
InitializeComponent();
this.DataContext = new ShowOrderDialogViewModel(list);
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
{
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>买手(外部)订单号;无买手订单时为空</summary>
public string OutOrderSn { get; set; }
/// <summary>买手订单金额(分)</summary>
public int BuyerOutAmount { get; set; }
/// <summary>买手订单退款金额(分)</summary>
public int BuyerRefundAmount { get; set; }
/// <summary>买手订单退款状态</summary>
public string BuyerRefundStatusText { get; set; }
/// <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,
OutOrderSn = buyerOrder == null ? null : buyerOrder.OutOrderSn,
BuyerOutAmount = buyerOrder == null ? 0 : buyerOrder.OutAmount,
BuyerRefundAmount = buyerOrder == null ? 0 : buyerOrder.RefundAmount,
BuyerRefundStatusText = buyerOrder == null ? null : buyerOrder.RefundStatusText,
};
}
}
class ShowOrderDialogViewModel : BindableBaseEx
{
public ObservableCollection<ShopOrderDetailRow> Data { get; set; }
public ICollectionView DataView { get; private set; }
public ShowOrderDialogViewModel(List<ShopOrderDetailResponse> list)
{
// 按买手订单展平:一笔买手订单一一行,不合并
Data = new ObservableCollection<ShopOrderDetailRow>(ShopOrderDetailRow.Flatten(list));
DataView = CollectionViewSource.GetDefaultView(Data);
DataView.Filter = FilterRow;
}
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());
}
/// <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();
}
Window window = Application.Current.Windows
.OfType<Window>()
.FirstOrDefault(w => w.IsActive) ?? Application.Current.MainWindow;
if (list.Count == 0)
{
ToastHelper.Show(window, "没有可复制的数据", type: ToastType.Warning);
return;
}
try
{
string text = BuildCopyText(list);
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text));
ToastHelper.Show(window, "复制成功");
}
catch (Exception ex)
{
ToastHelper.Show(window, "复制失败 " + ex.Message, type: ToastType.Error);
}
}
/// <summary>把行拼成制表符分隔的文本(第一行是表头)</summary>
private string BuildCopyText(IEnumerable<ShopOrderDetailRow> rows)
{
var sb = new StringBuilder();
// 表头
sb.AppendLine("店名\t订单\t创建时间\t金额\t退款金额\t外采金额\t未退回金额\t买手单号\t买手金额\t买手退款\t退款状态");
// 数据行
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.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{createTime}\t{orderPrice}\t{refundAmount}\t{outAmount}\t{unrefundedAmount}\t{buyerOrderSn}\t{buyerOutAmount}\t{buyerRefundAmount}\t{buyerRefundStatus}");
}
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;
}
}