326 lines
10 KiB
C#
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;
|
|
}
|
|
}
|