458 lines
13 KiB
C#
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;
|
|
}
|
|
}
|