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; /// /// ShowOrderDialogView.xaml 的交互逻辑 /// public partial class ShowOrderDialogView : UserControl { private readonly KeyEventHandler shortcutKeyHandler; public ShowOrderDialogView(List 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(); } /// 把快捷键处理器挂到所在窗口(隧道事件,焦点在窗口上的任意位置都能收到) 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); } /// /// 复制订单号到剪贴板 /// 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); } } } /// /// 弹窗级快捷键:Ctrl+A 全选(筛选后显示的)行,Ctrl+C 复制数据。 /// 焦点在文本框里(如订单号搜索框)时不拦截,保留文本自身的 Ctrl+A/Ctrl+C/Ctrl+V 行为。 /// 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; } } /// /// Ctrl+A:全选当前筛选后显示的所有行(被过滤掉的行不会选中)。 /// 选中行取自与复制同一份筛选结果,保证“选中的”和“复制的”完全一致。 /// private void SelectAllRows(ShowOrderDialogViewModel viewModel) { // 让键盘焦点回到表格,全选后仍可用键盘继续操作 dgOrders.Focus(); dgOrders.SelectedItems.Clear(); foreach (var row in viewModel.GetFilteredRows()) { dgOrders.SelectedItems.Add(row); } } /// 当前选中的行 private List GetSelectedRows() { return dgOrders.SelectedItems.OfType().ToList(); } } /// /// 订单明细表格行:把一个商铺订单按买手订单“展平”成多行(有几笔买手订单就几行, /// 没有买手订单时占一行且买手字段为空),不把多笔买手订单合并到同一行。 /// 商铺订单级字段(店名/订单号/创建时间/金额等)在多行之间重复,保证每行都是一条完整记录。 /// 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 long ProfitAmount { get; set; } /// /// 平台佣金 /// public long PlatformFee { get; set; } public int OutAmount { get; set; } public int UnrefundedAmount { get; set; } /// 买手订单表ID public long BuyerId { get; set; } /// 买手(外部)订单号;无买手订单时为空 public string OutOrderSn { get; set; } /// 买手订单金额(分) public int BuyerOutAmount { get; set; } /// /// 买手退款表 id /// public long BuyerRefundId { get; set; } /// 买手订单退款金额(分) public int BuyerRefundAmount { get; set; } /// 买手订单退款状态(更新成功后就地改为“退款成功”) public string BuyerRefundStatusText { get => GetValue(); set => SetValue(value); } public string Remark { get => GetValue(); set => SetValue(value); } /// 把接口返回的订单明细按买手订单展平成表格行 public static List Flatten(IEnumerable list, ShopOrderDetailType detailType) { var rows = new List(); 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 Data { get; set; } public ICollectionView DataView { get; private set; } public ShowOrderDialogViewModel(List list, ShopOrderDetailType detailType, IShopRevenueStatisticsService statisticsService) { IsBuyerUnrefunded = detailType == ShopOrderDetailType.BuyerUnrefunded; // 按买手订单展平:一笔买手订单一一行, 利润明细合并,只展示一条 Data = new ObservableCollection(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(); set => SetValue(value, () => DataView.Refresh()); } public bool IsBuyerUnrefunded { get => GetValue(); set => SetValue(value); } /// 当前筛选后显示的行(按表格显示顺序) public List GetFilteredRows() { return DataView.Cast().ToList(); } /// /// 复制指定行到剪贴板(带表头,制表符分隔)。 /// rows 为空时复制筛选后的全部数据;复制结果用提示条反馈。 /// public void CopyRows(IEnumerable rows) { List list = rows?.ToList() ?? new List(); 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(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(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 { } } /// 把行拼成制表符分隔的文本(第一行是表头) private string BuildCopyText(IEnumerable rows) { var sb = new StringBuilder(); bool includeRemark = _detailType == ShopOrderDetailType.BuyerUnrefunded; // 表头 List headers = ["店名", "订单", "创建时间", "金额", "退款金额", "平台费用", "利润", "买手单号", "外采金额", "买手退款", "退款状态"]; if (includeRemark) headers.Add("备注"); sb.AppendLine(string.Join("\t", headers)); // 数据行 foreach (var item in rows) { var cols = new List { 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; } }