主页增加发票助手

This commit is contained in:
chfei committed 2026-08-21 14:15:16 +08:00
1 parent 5456e6611d
commit a15f97ac2e
22 files changed
+3825 -309

No files matched your search

+2
View File
@@ -63,8 +63,10 @@
<!-- 资源文件:SDK 样式工程默认不会把图片/图标作为 Resource,这里按原工程显式声明 -->
<ItemGroup>
<Resource Include="Resources\**\*" />
<None Remove="Ctrls\PDDInvoice\Resources\invoice.js" />
<Resource Include="29wx.ico" />
<Resource Include="29_common_icon.ico" />
<Resource Include="Ctrls\PDDInvoice\Resources\invoice.js" />
<Resource Include="gear.ico" />
</ItemGroup>
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -14,27 +15,38 @@ namespace _29WxOrderWpf.Converters
{
public class ImageUrlToBitmapImageConverter : IValueConverter
{
/// <summary> 缓存上限,防止大量缩略图重复下载导致内存膨胀 </summary>
private const int MaxCacheCount = 200;
private readonly ConcurrentDictionary<string, BitmapImage> _imageCache = new ConcurrentDictionary<string, BitmapImage>();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string url && Uri.IsWellFormedUriString(url, UriKind.Absolute))
{
if (_imageCache.TryGetValue(url, out BitmapImage cached))
{
return cached;
}
try
{
// 直接加载绑定如果遇到有问题的网络图片会直接崩溃。
//var bitMap = new BitmapImage(new Uri(url));
BitmapImage bitMap = null;
//bitMap = LoadImageFromUrl(url + "?imageView2/1/w/100/format/webp|imageMogr2/auto-orient=");
bitMap = AllBase.ImageBuild.LoadBitmipImageFromUrl(url);
BitmapImage bitMap = AllBase.ImageBuild.LoadBitmipImageFromUrl(url);
if (bitMap != null)
{
if (_imageCache.Count >= MaxCacheCount)
{
_imageCache.Clear();
}
_imageCache[url] = bitMap;
}
return bitMap;
//string errStr = "";
//var bitmapImg = GetUrlImg(url, out errStr);
//return bitmapImg;
}
catch
{
}
//return Task.Run(() => new BitmapImage(new Uri(url))); // 使用 Task.Run 来异步加载图片。
}
return null;
}
@@ -0,0 +1,318 @@
using _29WxOrderWpf.BaseWindows;
using _29WxOrderWpf.Commands;
using _29WxOrderWpf.Common;
using _29WxOrderWpf.Ctrls.PDDInvoice;
using _29WxOrderWpf.Ctrls.Statistics;
using _29WxOrderWpf.Services;
using AllBase.Helper;
using AllBase.Model.DBModel;
using NaiBa.Util.Extension;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using Work._29Tools.BLL;
using static _29WxOrderWpf.Services.ShipGiftService;
namespace _29WxOrderWpf.Ctrls.OrderCtrls;
public partial class OrderListCtrl
{
#region 统计
private DialogWindow _statisticsDialog;
public ICommand ShowShopRevenueStatisticsDialogViewCommand => new DelegateCommand(ShowShopRevenueStatisticsDialogView);
private void ShowShopRevenueStatisticsDialogView()
{
if (_statisticsDialog == null)
{
_statisticsDialog = new DialogWindow()
{
Title = "统计",
Content = new ShopRevenueStatisticsView(),
};
_statisticsDialog.Closed += (s, e) => _statisticsDialog = null;
}
_statisticsDialog.Show();
}
#endregion
#region 礼物
public ICommand MultipleShipGiftCommand => new AsyncDelegateCommand(MultipleShipGift);
/// <summary>
/// 批量发送礼品单
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async Task MultipleShipGift()
{
try
{
var list = GlobalConfig.lstFilterOrder.Where(a => a.IsSelect).ToList();
int count = list.Count;
if (count == 0)
{
BaseMsgWindow.ShowError("请选取要发货的订单", "Error");
return;
}
GiftType giftType = GiftType._160;
if (cb_gift.SelectedIndex == 0)
{
giftType = GiftType._155;
}
else
{
giftType = GiftType._160;
}
int index = 1;
int sucessCount = 0;
int errorCount = 0;
foreach (oms_wei_order_info item in list)
{
try
{
var rst = await WxApiBLL.WxSearchAndDropshipCancelAsync(item.Shop, item);
if (rst == "success")
{
var ret = await _shipGiftService.Send(item.order_id, giftType);
if (ret)
{
sucessCount++;
item.gift_order_status = 1;
this.FilterOneOrder(item, true);
}
else
{
errorCount++;
}
}
else
{
errorCount++;
}
}
catch (Exception ex)
{
errorCount++;
}
finally
{
item.IsSelect = false;
}
SetProgress(index, count);
BottomMsg = $"礼品单发送进度:{index}/{count}\t\t{item.orderItems.FirstOrDefault()?.title ?? ""}";
index++;
}
BottomMsg = $"礼品单发送完成 成功:{sucessCount} 失败:{errorCount} 总数:{count}";
}
catch (Exception ex)
{
BaseMsgWindow.ShowError("礼品单发货失败\n" + ex.Message, "Error");
}
finally
{
}
}
#endregion
#region 同步订单
/// <summary>
/// 拉取微信订单到任务列表
/// </summary>
public ICommand PullWXOrderCommand => new AsyncDelegateCommand(PullWXOrder);
private async Task PullWXOrder()
{
try
{
var isAfterSales = GlobalConfig.OrderTabHeaderStr.StartsWith("需处理售后单");//是否售后
var selectedShops = GlobalConfig.ShopLoginList.Where(a => a.IsSelect).ToList();
if (selectedShops.Count == 0)
{
selectedShops = GlobalConfig.ShopLoginList.ToList();
}
var selectedOrders = GlobalConfig.lstFilterOrder.Where(a => a.IsSelect).ToList();
int count = selectedOrders.Count;
if (count > 0 && isAfterSales == false)//非售后订单
{
if (BaseMsgWindow.ShowConfirm($"已选取:{count}个订单", "同步订单", MainWindow.Instance, MsgWindowLevel.MEDIUM).DialogResult == false) return;
BottomMsg = $"订单同步进度....";
int index = 1;
int sucessCount = 0;
int errorCount = 0;
foreach (var item in selectedOrders)
{
var ret = await PullWXOrderService.Instance.PullOrderDetailAsync(item.shop_id, item.order_id); ;
if (ret)
{
sucessCount++;
this.GetOrder(item.order_id);
SetProgress(index, count);
item.IsSelect = false;
}
else
{
errorCount++;
}
BottomMsg = $"订单同步进度:{index}/{count}\t\t{item.orderItems.FirstOrDefault()?.title ?? ""}";
index++;
}
BottomMsg = $"订单同步进度完成 成功:{sucessCount} 失败:{errorCount} 总数:{count}";
}
else if (count > 0 && isAfterSales == true)//售后订单
{
if (BaseMsgWindow.ShowConfirm($"已选取:{count}个售后订单", "同步订单", MainWindow.Instance, MsgWindowLevel.MEDIUM).DialogResult == false) return;
//var dictionaryResult = selectedOrders
// .Where(o => o?.refundItems != null)
// .SelectMany(o => o.refundItems
// .Where(item => item != null)
// .Select(item => new { o.shop_id, item.after_sale_order_id }))
// .GroupBy(x => x.shop_id)
// .ToDictionary(
// g => g.Key,
// g => g.Select(x => x.after_sale_order_id).ToArray()
// );
//BottomMsg = $"售后订单同步进度....";
//foreach (var item in dictionaryResult)
//{
// var ret = await PullWXOrderService.Instance.PullAfterSaleOrdersAsync(item.Key, item.Value);
//}
//BottomMsg = $"售后同步任务已发送,等待同步完成";
int index = 1;
int sucessCount = 0;
int errorCount = 0;
BottomMsg = $"售后订单同步进度....";
foreach (KeyValuePair<long, List<oms_wei_order_info>> item in selectedOrders.GroupBy(a => a.shop_id).ToDictionary(a => a.Key, a => a.ToList()))
{
long shopid = item.Key;
string shopName = item.Value.FirstOrDefault()?.shop_name ?? "";
string[] afterSaleOrderIds = item.Value.SelectMany(a => a.refundItems).Where(a => a != null).Select(a => a.after_sale_order_id).ToArray();
var ret = await PullWXOrderService.Instance.PullAfterSaleOrdersAsync(shopid, afterSaleOrderIds);
if (ret)
{
sucessCount++;
SetProgress(index, count);
item.Value.ForEach(a => a.IsSelect = false);
}
else
{
errorCount++;
}
BottomMsg = $"售后订单同步进度:{index}/{count}\t\t{shopName}";
index++;
}
BottomMsg = $"售后同步任务已发送,等待同步完成";
}
else
{
count = selectedShops.Count;
string prefix = $"已选取:{count}个商铺";
if (count == GlobalConfig.ShopLoginList.Count)
{
prefix = "已选取全部商铺";
}
var startTime = dateStart.SelectedDate.Value;
var endTime = dateEnd.SelectedDate.Value;
string msg = "";
if (isAfterSales)
{
msg = $"{prefix}";
}
else
{
msg = $"{prefix}\n\n时间范围:{startTime.ToString("yyyy-MM-dd")}-{endTime.ToString("yyyy-MM-dd")}";
}
if (BaseMsgWindow.ShowConfirm(msg, $"{(isAfterSales ? "售后" : "")}同步订单", MainWindow.Instance, MsgWindowLevel.MEDIUM).DialogResult == false) return;
if ((endTime - startTime).Days > 7)
{
throw new Exception("时间范围最大7天");
}
var ret = false;
if (isAfterSales)
{
ret = await PullWXOrderService.Instance.PullRefundOrderByShopsAsync(selectedShops.Select(a => a.ErpShopId.ParseToLong()).ToArray(), startTime, endTime);
}
else
{
ret = await PullWXOrderService.Instance.PullOrderByShopIdsAndTimeRangeAsync(selectedShops.Select(a => a.ErpShopId.ParseToLong()).ToArray(), startTime, endTime);
}
if (ret)
{
BaseMsgWindow.ShowInfo("同步任务已发送,等待同步完成\n任务详情查看任务列表");
}
else
{
BaseMsgWindow.ShowError("同步任务失败");
}
}
}
catch (Exception ex)
{
BaseMsgWindow.ShowError("同步订单失败\n" + ex.Message, "Error");
}
//var service = PullWXOrderService.Instance;
//foreach (var item in list)
//{
// await service.PullOrderDetailAsync(item.shop_id, item.order_id);
//}
}
#endregion
#region 显示任务列表
private DialogWindow _taskDialog;
public ICommand ShowTaskDialogViewCommand => new DelegateCommand(ShowTaskDialogView);
private void ShowTaskDialogView()
{
if (_taskDialog == null)
{
_taskDialog = new DialogWindow()
{
Title = "任务列表",
Content = new OrderTaskDialogView(),
};
_taskDialog.Closed += (s, e) => _taskDialog = null;
}
_taskDialog.Show();
}
#endregion
#region 拼多多发票
public ICommand ShowPDDInvoiceDialogCommand => new DelegateCommand(ShowPDDInvoiceDialog);
private void ShowPDDInvoiceDialog()
{
new DialogWindow
{
Title = "发票助手",
Content = new PDDInvoiceView()
}.Show();
}
#endregion
}
@@ -25,7 +25,8 @@
<RowDefinition Height="*" />
<RowDefinition Height="18" />
</Grid.RowDefinitions>
<Grid x:Name="gridSearch" Margin="8,0,8,-2">
<Grid x:Name="gridSearch" Margin="8,0,290,-2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="1.2*" />
@@ -40,11 +41,8 @@
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="1.4*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
@@ -205,7 +203,8 @@
Padding="4"
Click="btnFilter_Click"
Content="过滤"
Style="{StaticResource BasePageButtonStyle}" />
Style="{StaticResource BasePageButtonStyle}"
ToolTip="过滤" />
<Button
x:Name="btnSearch"
Grid.Column="10"
@@ -274,16 +273,29 @@
IsChecked="true" />
</WrapPanel>
<wpf:SpacingStackPanel Grid.Column="13" HorizontalSpacing="5">
<Button
Width="30"
Height="28"
Padding="4"
HorizontalAlignment="Center"
Command="{Binding ShowShopRevenueStatisticsDialogViewCommand}"
Style="{StaticResource BasePageButtonStyle}"
ToolTip="利润统计">
<TextBlock Style="{StaticResource TextBlockIcon}" Text="{StaticResource txt-data_pie}" />
</Button>
<Button
Grid.Column="13"
Width="60"
Height="28"
Padding="4"
HorizontalAlignment="Center"
Click="btn_ShowShopRevenueStatisticsView"
Content="利润统计"
Style="{StaticResource BasePageButtonStyle}" />
<Button
Width="30"
Height="28"
Padding="4"
HorizontalAlignment="Center"
Command="{Binding ShowPDDInvoiceDialogCommand}"
Style="{StaticResource BasePageButtonStyle}"
ToolTip="发票助手">
<TextBlock Style="{StaticResource TextBlockIcon}" Text="{StaticResource txt-fapiao}" />
</Button>
</wpf:SpacingStackPanel>
</Grid>
<Border
@@ -2149,22 +2149,7 @@ namespace _29WxOrderWpf.Ctrls.OrderCtrls
FilterOrders();
}
private DialogWindow _statisticsWindow;
private void btn_ShowShopRevenueStatisticsView(object sender, RoutedEventArgs e)
{
if (_statisticsWindow == null)
{
_statisticsWindow = new DialogWindow()
{
Title = "统计",
Content = new ShopRevenueStatisticsView(),
};
_statisticsWindow.Closed += (s, e) => _statisticsWindow = null;
}
_statisticsWindow.Show();
}
/// <summary>
///
@@ -2268,263 +2253,7 @@ namespace _29WxOrderWpf.Ctrls.OrderCtrls
// set => SetProperty(ref _IsShipGift, value);
//}
public ICommand MultipleShipGiftCommand => new AsyncDelegateCommand(MultipleShipGift);
/// <summary>
/// 批量发送礼品单
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async Task MultipleShipGift()
{
try
{
var list = GlobalConfig.lstFilterOrder.Where(a => a.IsSelect).ToList();
int count = list.Count;
if (count == 0)
{
BaseMsgWindow.ShowError("请选取要发货的订单", "Error");
return;
}
GiftType giftType = GiftType._160;
if (cb_gift.SelectedIndex == 0)
{
giftType = GiftType._155;
}
else
{
giftType = GiftType._160;
}
int index = 1;
int sucessCount = 0;
int errorCount = 0;
foreach (oms_wei_order_info item in list)
{
try
{
var rst = await WxApiBLL.WxSearchAndDropshipCancelAsync(item.Shop, item);
if (rst == "success")
{
var ret = await _shipGiftService.Send(item.order_id, giftType);
if (ret)
{
sucessCount++;
item.gift_order_status = 1;
this.FilterOneOrder(item, true);
}
else
{
errorCount++;
}
}
else
{
errorCount++;
}
}
catch (Exception ex)
{
errorCount++;
}
finally
{
item.IsSelect = false;
}
SetProgress(index, count);
BottomMsg = $"礼品单发送进度:{index}/{count}\t\t{item.orderItems.FirstOrDefault()?.title ?? ""}";
index++;
}
BottomMsg = $"礼品单发送完成 成功:{sucessCount} 失败:{errorCount} 总数:{count}";
}
catch (Exception ex)
{
BaseMsgWindow.ShowError("礼品单发货失败\n" + ex.Message, "Error");
}
finally
{
}
}
/// <summary>
/// 拉取微信订单到任务列表
/// </summary>
public ICommand PullWXOrderCommand => new AsyncDelegateCommand(PullWXOrder);
private async Task PullWXOrder()
{
try
{
var isAfterSales = GlobalConfig.OrderTabHeaderStr.StartsWith("需处理售后单");//是否售后
var selectedShops = GlobalConfig.ShopLoginList.Where(a => a.IsSelect).ToList();
if (selectedShops.Count == 0)
{
selectedShops = GlobalConfig.ShopLoginList.ToList();
}
var selectedOrders = GlobalConfig.lstFilterOrder.Where(a => a.IsSelect).ToList();
int count = selectedOrders.Count;
if (count > 0 && isAfterSales == false)//非售后订单
{
if (BaseMsgWindow.ShowConfirm($"已选取:{count}个订单", "同步订单", MainWindow.Instance, MsgWindowLevel.MEDIUM).DialogResult == false) return;
BottomMsg = $"订单同步进度....";
int index = 1;
int sucessCount = 0;
int errorCount = 0;
foreach (var item in selectedOrders)
{
var ret = await PullWXOrderService.Instance.PullOrderDetailAsync(item.shop_id, item.order_id); ;
if (ret)
{
sucessCount++;
this.GetOrder(item.order_id);
SetProgress(index, count);
item.IsSelect = false;
}
else
{
errorCount++;
}
BottomMsg = $"订单同步进度:{index}/{count}\t\t{item.orderItems.FirstOrDefault()?.title ?? ""}";
index++;
}
BottomMsg = $"订单同步进度完成 成功:{sucessCount} 失败:{errorCount} 总数:{count}";
}
else if (count > 0 && isAfterSales == true)//售后订单
{
if (BaseMsgWindow.ShowConfirm($"已选取:{count}个售后订单", "同步订单", MainWindow.Instance, MsgWindowLevel.MEDIUM).DialogResult == false) return;
//var dictionaryResult = selectedOrders
// .Where(o => o?.refundItems != null)
// .SelectMany(o => o.refundItems
// .Where(item => item != null)
// .Select(item => new { o.shop_id, item.after_sale_order_id }))
// .GroupBy(x => x.shop_id)
// .ToDictionary(
// g => g.Key,
// g => g.Select(x => x.after_sale_order_id).ToArray()
// );
//BottomMsg = $"售后订单同步进度....";
//foreach (var item in dictionaryResult)
//{
// var ret = await PullWXOrderService.Instance.PullAfterSaleOrdersAsync(item.Key, item.Value);
//}
//BottomMsg = $"售后同步任务已发送,等待同步完成";
int index = 1;
int sucessCount = 0;
int errorCount = 0;
BottomMsg = $"售后订单同步进度....";
foreach (KeyValuePair<long, List<oms_wei_order_info>> item in selectedOrders.GroupBy(a => a.shop_id).ToDictionary(a => a.Key, a => a.ToList()))
{
long shopid = item.Key;
string shopName = item.Value.FirstOrDefault()?.shop_name ?? "";
string[] afterSaleOrderIds = item.Value.SelectMany(a => a.refundItems).Where(a => a != null).Select(a => a.after_sale_order_id).ToArray();
var ret = await PullWXOrderService.Instance.PullAfterSaleOrdersAsync(shopid, afterSaleOrderIds);
if (ret)
{
sucessCount++;
SetProgress(index, count);
item.Value.ForEach(a => a.IsSelect = false);
}
else
{
errorCount++;
}
BottomMsg = $"售后订单同步进度:{index}/{count}\t\t{shopName}";
index++;
}
BottomMsg = $"售后同步任务已发送,等待同步完成";
}
else
{
count = selectedShops.Count;
string prefix = $"已选取:{count}个商铺";
if (count == GlobalConfig.ShopLoginList.Count)
{
prefix = "已选取全部商铺";
}
var startTime = dateStart.SelectedDate.Value;
var endTime = dateEnd.SelectedDate.Value;
string msg = "";
if (isAfterSales)
{
msg = $"{prefix}";
}
else
{
msg = $"{prefix}\n\n时间范围:{startTime.ToString("yyyy-MM-dd")}-{endTime.ToString("yyyy-MM-dd")}";
}
if (BaseMsgWindow.ShowConfirm(msg, $"{(isAfterSales ? "售后" : "")}同步订单", MainWindow.Instance, MsgWindowLevel.MEDIUM).DialogResult == false) return;
if ((endTime - startTime).Days > 7)
{
throw new Exception("时间范围最大7天");
}
var ret = false;
if (isAfterSales)
{
ret = await PullWXOrderService.Instance.PullRefundOrderByShopsAsync(selectedShops.Select(a => a.ErpShopId.ParseToLong()).ToArray(), startTime, endTime);
}
else
{
ret = await PullWXOrderService.Instance.PullOrderByShopIdsAndTimeRangeAsync(selectedShops.Select(a => a.ErpShopId.ParseToLong()).ToArray(), startTime, endTime);
}
if (ret)
{
BaseMsgWindow.ShowInfo("同步任务已发送,等待同步完成\n任务详情查看任务列表");
}
else
{
BaseMsgWindow.ShowError("同步任务失败");
}
}
}
catch (Exception ex)
{
BaseMsgWindow.ShowError("同步订单失败\n" + ex.Message, "Error");
}
//var service = PullWXOrderService.Instance;
//foreach (var item in list)
//{
// await service.PullOrderDetailAsync(item.shop_id, item.order_id);
//}
}
private DialogWindow _taskDialog;
public ICommand ShowTaskDialogViewCommand => new DelegateCommand(ShowTaskDialogView);
private void ShowTaskDialogView()
{
if (_taskDialog == null)
{
_taskDialog = new DialogWindow()
{
Title = "任务列表",
Content = new OrderTaskDialogView(),
};
_taskDialog.Closed += (s, e) => _taskDialog = null;
}
_taskDialog.Show();
}
}
public class OrderFilterConfig
@@ -0,0 +1,868 @@
<UserControl
x:Class="_29WxOrderWpf.Ctrls.PDDInvoice.PDDInvoiceView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:comm="clr-namespace:_29WxOrderWpf.Common"
xmlns:common="clr-namespace:_29WxOrderWpf.Common"
xmlns:converters="clr-namespace:_29WxOrderWpf.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:_29WxOrderWpf.Ctrls.PDDInvoice.ViewModels"
xmlns:wpf="clr-namespace:WPFCommon.Controls;assembly=WPFCommon"
comm:DialogWindowProperty.ResizeMode="CanResizeWithGrip"
comm:DialogWindowProperty.ShowMinButton="False"
common:DialogWindowProperty.Height="720"
common:DialogWindowProperty.Width="1260"
d:DataContext="{d:DesignInstance Type=vm:PDDInvoiceViewModel,
IsDesignTimeCreatable=True}"
d:DesignHeight="720"
d:DesignWidth="1260"
Background="White"
mc:Ignorable="d">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibility" />
<converters:ImageUrlToBitmapImageConverter x:Key="ImageUrlToBitmapImage" />
<converters:InverseVisibleToVisibilityConverter x:Key="InverseBooleanToVisibility" />
<!-- 原型配色 -->
<SolidColorBrush x:Key="PageBgBrush" Color="#F5F6F8" />
<SolidColorBrush x:Key="CardBgBrush" Color="#FFFFFF" />
<SolidColorBrush x:Key="RowBgBrush" Color="#FAFAFA" />
<SolidColorBrush x:Key="SplitLineBrush" Color="#EBEEF5" />
<SolidColorBrush x:Key="InputBorderBrush" Color="#DCDFE6" />
<SolidColorBrush x:Key="PrimaryBrush" Color="#409EFF" />
<SolidColorBrush x:Key="PrimaryHoverBrush" Color="#66B1FF" />
<SolidColorBrush x:Key="TextPrimaryBrush" Color="#333333" />
<SolidColorBrush x:Key="TextNormalBrush" Color="#666666" />
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#909399" />
<SolidColorBrush x:Key="TextPlaceholderBrush" Color="#C0C4CC" />
<SolidColorBrush x:Key="AmountBrush" Color="#F56C6C" />
<SolidColorBrush x:Key="SuccessBrush" Color="#67C23A" />
<SolidColorBrush x:Key="WarningBrush" Color="#E6A23C" />
<!-- 表头单元格 -->
<Style x:Key="HeaderCellTextStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="13" />
<Setter Property="Foreground" Value="{StaticResource TextNormalBrush}" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<!-- 搜索 / 表单输入框 -->
<Style x:Key="SearchTextBoxStyle" TargetType="TextBox">
<Setter Property="Height" Value="36" />
<Setter Property="Padding" Value="12,0" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="13" />
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
<Setter Property="Background" Value="White" />
<Setter Property="BorderBrush" Value="{StaticResource InputBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border
x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4">
<ScrollViewer x:Name="PART_ContentHost" Margin="{TemplateBinding Padding}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter TargetName="border" Property="BorderBrush" Value="{StaticResource PrimaryBrush}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="border" Property="Opacity" Value="0.6" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 主按钮 -->
<Style x:Key="PrimaryButtonStyle" TargetType="Button">
<Setter Property="Height" Value="36" />
<Setter Property="Padding" Value="18,0" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Background" Value="{StaticResource PrimaryBrush}" />
<Setter Property="FontSize" Value="13" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border
x:Name="border"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
CornerRadius="4">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Background" Value="{StaticResource PrimaryHoverBrush}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Opacity" Value="0.8" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="border" Property="Opacity" Value="0.5" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 默认按钮 -->
<Style x:Key="DefaultButtonStyle" TargetType="Button">
<Setter Property="Height" Value="36" />
<Setter Property="Padding" Value="18,0" />
<Setter Property="Foreground" Value="{StaticResource TextNormalBrush}" />
<Setter Property="Background" Value="White" />
<Setter Property="BorderBrush" Value="{StaticResource InputBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="FontSize" Value="13" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border
x:Name="border"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Background" Value="#F5F7FA" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Opacity" Value="0.8" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 文字链接按钮 -->
<Style x:Key="LinkButtonStyle" TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Foreground" Value="{StaticResource PrimaryBrush}" />
<Setter Property="FontSize" Value="13" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Padding" Value="0" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="{StaticResource PrimaryHoverBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 弹窗遮罩 -->
<Style x:Key="ModalMaskStyle" TargetType="Border">
<Setter Property="Background" Value="#80000000" />
<Setter Property="Panel.ZIndex" Value="100" />
</Style>
<!-- 发票详情 标签 / 值 -->
<Style x:Key="DetailLabelStyle" TargetType="TextBlock">
<Setter Property="Width" Value="100" />
<Setter Property="FontSize" Value="14" />
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style x:Key="DetailValueStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="14" />
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</UserControl.Resources>
<Border Padding="10,10,10,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- 功能区域 -->
<Border Grid.Row="0">
<wpf:SpacingStackPanel Background="{StaticResource CardBgBrush}" HorizontalSpacing="10">
<Grid Width="300">
<TextBox
x:Name="txtSearch"
Height="30"
Padding="5,0,0,0"
KeyDown="txtSearch_KeyDown"
Style="{StaticResource SearchTextBoxStyle}"
Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock
Padding="12,0,0,0"
VerticalAlignment="Center"
FontSize="13"
Foreground="{StaticResource TextPlaceholderBrush}"
IsHitTestVisible="False"
Text="请输入订单号 / 商品名称"
Visibility="{Binding Text, ElementName=txtSearch, Converter={StaticResource NotHasTextVisibility}}" />
</Grid>
<Button
Width="50"
Height="30"
Command="{Binding SearchCommand}"
Content="搜索"
Style="{StaticResource BasePageButtonStyle}" />
<Button
Height="30"
Command="{Binding ResetCommand}"
Content="重置"
Style="{StaticResource DefaultButtonStyle}" />
<wpf:TitleContent
Title="买手号:"
Placement="Left"
TitleVerticalAlignment="Center">
<ComboBox
Width="150"
Height="28"
DisplayMemberPath="buyer_nick"
ItemsSource="{Binding BuyerLoginList}"
SelectedItem="{Binding SelectedBuyer}"
Style="{StaticResource RoundedComboBox}" />
</wpf:TitleContent>
<Button
Height="30"
Padding="5,0"
Command="{Binding ReadOrderCommand}"
Content="读取订单"
Style="{StaticResource BasePageButtonStyle}"
Visibility="{Binding IsReadOrder, Converter={StaticResource Boolean2VisibilityReConverter}}" />
<Button
Height="30"
Padding="5,0"
Command="{Binding StopReadOrderCommand}"
Content="停止读单"
Style="{StaticResource BasePageButtonStyle}"
Visibility="{Binding IsReadOrder, Converter={StaticResource Boolean2VisibilityConverter}}" />
<Button
Height="30"
Padding="10,0"
Command="{Binding BatchInvoiceCommand}"
Content="批量开票"
Style="{StaticResource BasePageButtonStyle}" />
<wpf:TitleContent
Title="公司:"
Placement="Left"
TitleVerticalAlignment="Center">
<TextBox
Width="150"
Height="30"
Padding="5,0"
VerticalContentAlignment="Center"
Text="{Binding InvoiceTitle}" />
</wpf:TitleContent>
<wpf:TitleContent
Title="税号:"
Placement="Left"
TitleVerticalAlignment="Center">
<TextBox
Width="150"
Height="30"
Padding="5,0"
VerticalContentAlignment="Center"
Text="{Binding TaxNo}" />
</wpf:TitleContent>
<CheckBox Content="显示订单窗口" IsChecked="{Binding IsOrderListDialogOpen}" />
</wpf:SpacingStackPanel>
</Border>
<!-- 订单列表(已移除分页与订单分组) -->
<Border
Grid.Row="1"
Height="Auto"
Margin="0,10,0,0"
VerticalAlignment="Stretch"
Background="{StaticResource CardBgBrush}"
CornerRadius="6">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- 表头 -->
<Border
Grid.Row="0"
Padding="20,14"
Background="{StaticResource RowBgBrush}"
BorderBrush="{StaticResource SplitLineBrush}"
BorderThickness="0,0,0,1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="80" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1.2*" />
<ColumnDefinition Width="1.5*" />
</Grid.ColumnDefinitions>
<StackPanel
Grid.Column="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<CheckBox
VerticalAlignment="Center"
FontSize="13"
Foreground="{StaticResource TextNormalBrush}"
IsChecked="{Binding IsSelectAll}" />
<!--<Button
Margin="8,0,0,0"
VerticalAlignment="Center"
Command="{Binding InvertSelectionCommand}"
Content="反选"
Style="{StaticResource LinkButtonStyle}" />-->
</StackPanel>
<TextBlock
Grid.Column="1"
Style="{StaticResource HeaderCellTextStyle}"
Text="商品信息" />
<TextBlock
Grid.Column="3"
Style="{StaticResource HeaderCellTextStyle}"
Text="数量" />
<TextBlock
Grid.Column="4"
Style="{StaticResource HeaderCellTextStyle}"
Text="实付金额" />
<TextBlock
Grid.Column="5"
Style="{StaticResource HeaderCellTextStyle}"
Text="发票" />
<TextBlock
Grid.Column="6"
Style="{StaticResource HeaderCellTextStyle}"
Text="操作" />
</Grid>
</Border>
<Grid Grid.Row="1">
<Border Background="White">
<!-- 列表(虚拟化滚动加载) -->
<ItemsControl
Margin="0,2,0,-2"
Padding="0"
HorizontalContentAlignment="Stretch"
ItemsSource="{Binding Orders}">
<ItemsControl.Template>
<ControlTemplate TargetType="ItemsControl">
<ScrollViewer
CanContentScroll="True"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<ItemsPresenter />
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel
CacheLength="1,1"
CacheLengthUnit="Page"
IsVirtualizing="True"
ScrollUnit="Pixel"
VirtualizationMode="Recycling" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="{StaticResource SplitLineBrush}" BorderThickness="0,0,0,1">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="Transparent" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter Property="Background" Value="#ECF5FF" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<StackPanel>
<!-- 订单信息栏 -->
<Border Padding="20,12" Background="{StaticResource RowBgBrush}">
<StackPanel Orientation="Horizontal">
<TextBlock
FontSize="12"
Foreground="{StaticResource TextNormalBrush}"
Text="{Binding order_sn, StringFormat=订单号:{0}}" />
<TextBlock
Margin="28,0,0,0"
FontSize="12"
Foreground="{StaticResource TextNormalBrush}"
Text="{Binding OrderTime, StringFormat=下单时间:{0:yyyy-MM-dd}}" />
<TextBlock
Margin="28,0,0,0"
FontSize="12"
Foreground="{StaticResource TextNormalBrush}"
Text="{Binding order_status_prompt}" />
</StackPanel>
</Border>
<!-- 订单内容 -->
<Border Padding="20">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="80" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1.2*" />
<ColumnDefinition Width="1.5*" />
</Grid.ColumnDefinitions>
<!-- 勾选 -->
<CheckBox
Grid.Column="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsChecked="{Binding IsSelected}" />
<!-- 商品 -->
<Grid Grid.Column="1" VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="72" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border
Width="72"
Height="72"
Background="#EEEEEE"
CornerRadius="5">
<Image Source="{Binding ThumbUrl, Converter={StaticResource ImageUrlToBitmapImage}, IsAsync=True}" Stretch="UniformToFill" />
</Border>
<StackPanel
Grid.Column="1"
Margin="14,0,0,0"
VerticalAlignment="Center">
<TextBlock
MaxHeight="44"
FontSize="14"
Foreground="{StaticResource TextPrimaryBrush}"
LineHeight="22"
Text="{Binding GoodsName}"
TextTrimming="CharacterEllipsis"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
<!-- 数量 -->
<TextBlock
Grid.Column="3"
VerticalAlignment="Center"
FontSize="13"
Foreground="{StaticResource TextPrimaryBrush}"
Text="{Binding Quantity, StringFormat=×{0}}" />
<!-- 实付金额 -->
<TextBlock
Grid.Column="4"
VerticalAlignment="Center"
FontSize="16"
FontWeight="SemiBold"
Foreground="{StaticResource AmountBrush}"
Text="{Binding DisplayAmount}" />
<!-- 发票 -->
<StackPanel Grid.Column="5" VerticalAlignment="Center">
<TextBlock FontSize="13" Text="{Binding InvoiceStatusText}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{StaticResource WarningBrush}" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsInvoiced}" Value="True">
<Setter Property="Foreground" Value="{StaticResource SuccessBrush}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
<!-- 操作 -->
<StackPanel Grid.Column="6" VerticalAlignment="Center">
<Button
d:Visibility="Visible"
Command="{Binding DataContext.ShowInvoiceCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
Content="查看发票"
Style="{StaticResource LinkButtonStyle}"
Visibility="{Binding IsInvoiced, Converter={StaticResource BooleanToVisibility}}" />
<Button
Margin="0,5"
Command="{Binding DataContext.OpenInvoiceCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
Content="申请开票"
Style="{StaticResource LinkButtonStyle}"
Visibility="{Binding IsInvoiced, Converter={StaticResource InverseBooleanToVisibility}}" />
<Button
Command="{Binding DataContext.ViewOrderCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
Content="查看订单"
Style="{StaticResource LinkButtonStyle}" />
</StackPanel>
</Grid>
</Border>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
<Border
Grid.Row="1"
Panel.ZIndex="{Binding BrowserZIndex}"
d:Panel.ZIndex="-10"
Style="{StaticResource ModalMaskStyle}">
<Grid>
<ContentControl
Grid.Row="0"
Width="400"
Height="600"
Background="White"
Content="{Binding Browser}" />
</Grid>
</Border>
<Border
Grid.Row="1"
Panel.ZIndex="{Binding BrowserDetailZIndex}"
d:Panel.ZIndex="-10"
Style="{StaticResource ModalMaskStyle}">
<Grid
Width="400"
Height="600"
Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border
Padding="10"
BorderBrush="LightGray"
BorderThickness="0,0,0,1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="50" />
</Grid.ColumnDefinitions>
<TextBlock
FontSize="16"
FontWeight="Black"
Text="{Binding SelectedOrder.GoodsName}" />
<StackPanel
Grid.Column="1"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Command="{Binding CloseInvoiceDetailCommand}"
Content="关闭"
Style="{StaticResource LinkButtonStyle}" />
</StackPanel>
</Grid>
</Border>
<ContentControl Grid.Row="1" Content="{Binding BrowserDetail}" />
</Grid>
</Border>
</Grid>
</Grid>
</Border>
<Grid
Grid.Row="2"
Height="18"
Margin="-10,5,-10,0"
Background="{StaticResource pageColorBrush}">
<ProgressBar
VerticalAlignment="Stretch"
d:IsIndeterminate="False"
d:Maximum="100"
d:Value="10"
Background="Transparent"
BorderThickness="0"
Foreground="#FFA680"
Maximum="{Binding ProgressMaximum}"
Value="{Binding ProgressValue}" />
<Grid Margin="10,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="100*" />
</Grid.ColumnDefinitions>
<TextBox
Grid.Row="2"
Grid.Column="1"
Margin="4,0,0,0"
VerticalAlignment="Center"
d:Text="共计:{}"
Background="Transparent"
BorderThickness="0"
Foreground="White"
IsReadOnly="True"
Text="{Binding BottomMsg}" />
</Grid>
</Grid>
<!-- 开票弹窗 -->
<!--<Border
Grid.RowSpan="3"
Style="{StaticResource ModalMaskStyle}"
Visibility="{Binding IsInvoiceDialogOpen, Converter={StaticResource BooleanToVisibility}}">
<Border
Width="520"
Padding="24"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="White"
CornerRadius="8">
<StackPanel>
<TextBlock
Margin="0,0,0,24"
FontSize="18"
FontWeight="SemiBold"
Foreground="{StaticResource TextPrimaryBrush}"
Text="开具发票" />
<Grid Margin="0,0,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="90" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
VerticalAlignment="Center"
FontSize="13"
Foreground="{StaticResource TextNormalBrush}"
Text="订单号" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
FontSize="13"
Foreground="{StaticResource TextPrimaryBrush}"
Text="{Binding SelectedOrder.order_sn}" />
</Grid>
<Grid Margin="0,0,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="90" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
VerticalAlignment="Center"
FontSize="13"
Foreground="{StaticResource TextNormalBrush}"
Text="发票类型" />
<ComboBox
Grid.Column="1"
Height="36"
Padding="10,0"
VerticalContentAlignment="Center"
FontSize="13"
SelectedValue="{Binding InvoiceType}"
SelectedValuePath="Content">
<ComboBoxItem Content="电子普通发票" />
<ComboBoxItem Content="增值税专用发票" />
</ComboBox>
</Grid>
<Grid Margin="0,0,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="90" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
VerticalAlignment="Center"
FontSize="13"
Foreground="{StaticResource TextNormalBrush}"
Text="发票抬头" />
<TextBox
Grid.Column="1"
Style="{StaticResource SearchTextBoxStyle}"
Text="{Binding InvoiceTitle}" />
</Grid>
<Grid Margin="0,0,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="90" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
VerticalAlignment="Center"
FontSize="13"
Foreground="{StaticResource TextNormalBrush}"
Text="税号" />
<TextBox
Grid.Column="1"
Style="{StaticResource SearchTextBoxStyle}"
Text="{Binding TaxNo}" />
</Grid>
<StackPanel
Margin="0,25,0,0"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Command="{Binding CloseInvoiceDialogCommand}"
Content="取消"
Style="{StaticResource DefaultButtonStyle}" />
<Button
Margin="10,0,0,0"
Command="{Binding ConfirmInvoiceCommand}"
Content="确认开票"
Style="{StaticResource PrimaryButtonStyle}" />
</StackPanel>
</StackPanel>
</Border>
</Border>-->
<!-- 发票信息弹窗 -->
<!--<Border
Grid.RowSpan="3"
Style="{StaticResource ModalMaskStyle}"
Visibility="{Binding IsInvoiceDetailOpen, Converter={StaticResource BooleanToVisibility}}">
<Border
Width="520"
Padding="24"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="White"
CornerRadius="8">
<StackPanel>
<TextBlock
Margin="0,0,0,24"
FontSize="18"
FontWeight="SemiBold"
Foreground="{StaticResource TextPrimaryBrush}"
Text="发票信息" />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="30" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Style="{StaticResource DetailLabelStyle}"
Text="订单号:" />
<TextBlock
Grid.Row="0"
Grid.Column="1"
Style="{StaticResource DetailValueStyle}"
Text="{Binding SelectedOrder.OrderNo}" />
<TextBlock
Grid.Row="1"
Style="{StaticResource DetailLabelStyle}"
Text="发票类型:" />
<TextBlock
Grid.Row="1"
Grid.Column="1"
Style="{StaticResource DetailValueStyle}"
Text="{Binding SelectedOrder.InvoiceType}" />
<TextBlock
Grid.Row="2"
Style="{StaticResource DetailLabelStyle}"
Text="发票号码:" />
<TextBlock
Grid.Row="2"
Grid.Column="1"
Style="{StaticResource DetailValueStyle}"
Text="{Binding SelectedOrder.InvoiceNo}" />
<TextBlock
Grid.Row="3"
Style="{StaticResource DetailLabelStyle}"
Text="发票抬头:" />
<TextBlock
Grid.Row="3"
Grid.Column="1"
Style="{StaticResource DetailValueStyle}"
Text="{Binding SelectedOrder.InvoiceTitle}" />
<TextBlock
Grid.Row="4"
Style="{StaticResource DetailLabelStyle}"
Text="税号:" />
<TextBlock
Grid.Row="4"
Grid.Column="1"
Style="{StaticResource DetailValueStyle}"
Text="{Binding SelectedOrder.TaxNo}" />
<TextBlock
Grid.Row="5"
Style="{StaticResource DetailLabelStyle}"
Text="开票金额:" />
<TextBlock
Grid.Row="5"
Grid.Column="1"
Style="{StaticResource DetailValueStyle}"
Text="{Binding SelectedOrder.InvoiceAmountText}" />
<TextBlock
Grid.Row="6"
Style="{StaticResource DetailLabelStyle}"
Text="开票时间:" />
<TextBlock
Grid.Row="6"
Grid.Column="1"
Style="{StaticResource DetailValueStyle}"
Text="{Binding SelectedOrder.InvoiceTime}" />
</Grid>
<StackPanel
Margin="0,25,0,0"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Command="{Binding CloseInvoiceDetailCommand}"
Content="关闭"
Style="{StaticResource DefaultButtonStyle}" />
</StackPanel>
</StackPanel>
</Border>
</Border>-->
</Grid>
</Border>
</UserControl>
@@ -0,0 +1,37 @@
using _29WxOrderWpf.Ctrls.PDDInvoice.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
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;
namespace _29WxOrderWpf.Ctrls.PDDInvoice;
/// <summary>
/// PDDInvoiceView.xaml 的交互逻辑
/// </summary>
public partial class PDDInvoiceView : UserControl
{
public PDDInvoiceView()
{
InitializeComponent();
DataContext = new PDDInvoiceViewModel();
}
private void txtSearch_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter && DataContext is PDDInvoiceViewModel viewModel)
{
viewModel.SearchCommand.Execute(null);
}
}
}
@@ -0,0 +1,134 @@
// 填写信息
function changeReactInputValue(inputDom, newText) {
let lastValue = inputDom.value;
inputDom.value = newText;
let event = new Event('input', { bubbles: true });
event.simulated = true;
let tracker = inputDom._valueTracker;
if (tracker) {
tracker.setValue(lastValue);
}
inputDom.dispatchEvent(event);
}
// 异步 sleep
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 查找元素(支持选择器或XPath)
function findElement(selector, isXPath = true) {
if (isXPath) {
return document.evaluate(
selector,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
} else {
return document.querySelector(selector);
}
}
// 完整流程
async function fillInvoiceAndApply(data = {}) {
const {
title = '某某科技有限公司',
taxNumber = '91110000123456789X'
} = data;
console.log('📝 开始填充发票信息...');
// 1. 点击"企业"按钮
const invoiceType = findElement('//*[contains(text(), "企业")]');
if (!invoiceType) {
console.warn('⚠️ 找不到"企业"按钮');
return false;
}
invoiceType.click();
console.log('✅ 已点击"企业"按钮');
await sleep(500);
// 2. 查找并填充发票抬头
const invoiceTitle = findElement('//label[contains(text(), "发票抬头")]/following-sibling::textarea[1]') ||
findElement('//*[contains(text(), "发票抬头")]/following-sibling::textarea[1]');
if (invoiceTitle) {
changeReactInputValue(invoiceTitle, title);
console.log('✅ 发票抬头已填充');
} else {
console.warn('⚠️ 未找到发票抬头控件');
}
// 3. 查找并填充税号
const taxNumberInput = findElement('//label[contains(text(), "税号")]/following-sibling::input[1]') ||
findElement('//*[contains(text(), "税号")]/following-sibling::input[1]');
if (taxNumberInput) {
changeReactInputValue(taxNumberInput, taxNumber);
console.log('✅ 税号已填充');
} else {
console.warn('⚠️ 未找到税号控件');
}
await sleep(300);
// 4. 查找并点击"申请发票"按钮
const applyBtn = findElement('//*[contains(text(), "申请发票")]');
if (!applyBtn) {
console.error('❌ 未找到"申请发票"按钮');
return false;
}
applyBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(300);
applyBtn.click();
console.log('✅ 已点击"申请发票"按钮');
// 5. 等待弹窗/状态出现
await sleep(300);
// 6. 判断状态
// 检查是否有"已经申请过"的提示
const alreadyApplied = findElement('//*[contains(text(), "您已经提交过发票申请了")]')
if (alreadyApplied) {
console.log('✅ 已经申请过,无需重复操作');
return true;
}
const successElement = findElement('//*[text()="确认您的发票信息无误后再提交"]');
if (successElement == null) {
return false;
}
// 检查是否有"确认开具"按钮或提示
const confirmBtn = findElement('//*[text()= "确认"]');
if (confirmBtn) {
console.log('✅ 找到"确认"按钮,准备点击...');
await sleep(200);
confirmBtn.click();
console.log('✅ 已点击"确认"按钮');
return true;
}
// 如果都没有找到
console.warn('⚠️ 未找到"已经申请过"或"确认"按钮,请手动检查');
return false;
}
// ========== 调用 ==========
(async function () {
try {
const result = await fillInvoiceAndApply({
title: '{公司}',
taxNumber: '{税号}'
});
console.log('执行结果:', result);
// 批量开票时,C# 端轮询该变量获取执行结果
window.__invoiceApplyResult = result;
} catch (ex) {
console.error('执行异常:', ex);
window.__invoiceApplyResult = false;
}
})();
@@ -0,0 +1,94 @@
using AllBase.Model.Result;
using System;
using System.Collections.Generic;
using System.Linq;
using WPFCommon.Helpers;
using WPFCommon.Models;
namespace _29WxOrderWpf.Ctrls.PDDInvoice.ViewModels;
/// <summary>
/// 订单发票Model
/// </summary>
public class PddBrowserOrderInvoiceViewModel : BindableBaseEx
{
private bool _isSelected;
/// <summary>
/// 是否被勾选(用于批量开票)
/// </summary>
public bool IsSelected
{
get => _isSelected;
set => SetProperty(ref _isSelected, value, nameof(IsSelected));
}
public string buyerId { get; set; } // 买手Id
public string buyerName { get; set; } // 买手名称
/// <summary>
/// 订单价
/// </summary>
public int order_amount { get; set; }
/// <summary>
/// 实付款
/// </summary>
public string display_amount { get; set; }
/// <summary>
/// 下单时间
/// </summary>
public long order_time { get; set; }
/// <summary>
/// 订单号
/// </summary>
public string order_sn { get; set; }
public int status { get; set; }
public int pay_status { get; set; }
public string order_status_prompt { get; set; }
/// <summary>
/// 订单详情
/// </summary>
public string order_link_url { get; set; }
public List<order_good> order_Goods { get; set; }
public List<OrderButtonItem> order_buttons { get; set; }
public string GoodsName { get => order_Goods?.FirstOrDefault()?.goods_name ?? ""; }
/// <summary>
/// 是否已开票
/// </summary>
public bool IsInvoiced
{
get => GetValue<bool>();
set
{
SetValue(value);
base.RaisePropertyChanged(nameof(InvoiceStatusText));
}
}
public int Quantity => order_Goods?.Sum(a => a.goods_number.Value) ?? 0;
/// <summary> 发票状态文案:已开票 / 待开票 </summary>
public string InvoiceStatusText => IsInvoiced ? "已开票" : "待开票";
public DateTime OrderTime
{
get => DateTimeHelper.ConvertFromUnixTimestampInSeconds(order_time);
}
public string ThumbUrl => order_Goods?.FirstOrDefault()?.thumb_url ?? "";
public string DisplayAmount => (order_amount / 100.0).ToString("F2");
}
public class OrderButtonItem
{
public int button_code { get; set; }
public string brief_prompt { get; set; }
public ButtonTypeValue type_value { get; set; }
}
public class ButtonTypeValue
{
public string Url { get; set; }
}
@@ -0,0 +1,250 @@
using _29WxOrderWpf.CefHelper;
using _29WxOrderWpf.Commands;
using AllBase;
using CefSharp;
using CefSharp.Handler;
using CefSharp.Wpf;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using Work._29Tools.CefTools;
using WxOrder.Framework.Helpers;
namespace _29WxOrderWpf.Ctrls.PDDInvoice.ViewModels;
public partial class PDDInvoiceViewModel
{
/// <summary>
/// 订单列表浏览器
/// </summary>
public ChromiumWebBrowser Browser { get; private set; }
/// <summary>
/// 内容浏览器
/// 订单详情
/// 发票详情
/// 申请发票
/// </summary>
public ChromiumWebBrowser BrowserDetail { get; private set; }
partial void InitBrowser()
{
BrowserZIndex = -1;
BrowserDetailZIndex = -1;
Browser = WPFCEFBrowser.CreateChromiumWebBrowser("about:blank", false);
LocalResourceRequestHandler resourceRequestHandler = new LocalResourceRequestHandler();
//resourceRequestHandler.BeforeResourceLoadEvent += RequestHandler_BeforeResourceLoad;
resourceRequestHandler.ResourceLoadCompleteEvent += RequestHandler_ResourceLoadComplete;
Browser.RequestHandler = new LocalRequestHandler(resourceRequestHandler);
Browser.LifeSpanHandler = new LocalLifeSpanHandler(PopWindowType.SelfWindow);
Browser.KeyboardHandler = new KeyboardHandler();
Browser.LoadingStateChanged += Browser_LoadingStateChanged;
BrowserDetail = WPFCEFBrowser.CreateChromiumWebBrowser("about:blank", false);
LocalResourceRequestHandler detailResourceRequestHandler = new LocalResourceRequestHandler();
detailResourceRequestHandler.ResourceLoadCompleteEvent += DetailRequestHandler_ResourceLoadComplete;
BrowserDetail.RequestHandler = new LocalRequestHandler(resourceRequestHandler);
BrowserDetail.LifeSpanHandler = new LocalLifeSpanHandler(PopWindowType.SelfWindow);
BrowserDetail.KeyboardHandler = new KeyboardHandler();
BrowserDetail.LoadingStateChanged += BrowserDetail_LoadingStateChanged;
}
private void Browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e)
{
if (e.IsLoading == false)
{
var b = (ChromiumWebBrowser)sender;
b.Dispatcher.InvokeAsync(async () =>
{
if (b.Address != null && b.Address.StartsWith("https://mobile.yangkeduo.com/orders.html"))
{
StartAutoScroll();
}
});
}
}
private void BrowserDetail_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e)
{
if (e.IsLoading == false)
{
var b = (ChromiumWebBrowser)sender;
b.Dispatcher.InvokeAsync(async () =>
{
if (b.Address != null && b.Address.StartsWith("https://mobile.yangkeduo.com/transac_return_compensation.html"))
{
string html = await b.GetBrowser().GetSourceAsync();
if (html.Contains("window.rawData"))
{
await ExecuteInvoiceApplyScriptAsync(b);
}
else
{
// 页面异常(未渲染出发票表单)时直接判定失败,避免批量开票长时间等待
_currentInvoiceTcs?.TrySetResult(false);
}
}
});
}
}
/// <summary>
/// 注入发票填写脚本并执行。
/// 单张申请模式保持原有行为;批量开票模式下轮询脚本写入的
/// <c>window.__invoiceApplyResult</c>,完成后通过 <see cref="_currentInvoiceTcs"/> 回传结果。
/// </summary>
private async Task ExecuteInvoiceApplyScriptAsync(ChromiumWebBrowser b)
{
string content = ResourceHelper.GetResourceText("/Ctrls/PDDInvoice/Resources/invoice.js", "29WxFindOrder");
content = content.Replace("{公司}", InvoiceTitle);
content = content.Replace("{税号}", TaxNo);
if (string.IsNullOrEmpty(content)) return;
await b.EvaluateScriptAsync(content);
if (_currentInvoiceTcs == null) return; // 单张申请模式:保持原有行为
// 批量模式:等待异步脚本执行完成
// 捕获当前 TCS,避免批量循环超时进入下一个订单后,迟到的轮询误触发新订单的结果
var tcs = _currentInvoiceTcs;
for (int i = 0; i < 120; i++)
{
await Task.Delay(500);
try
{
var res = await b.EvaluateScriptAsync("window.__invoiceApplyResult");
if (res.Success && res.Result != null)
{
tcs.TrySetResult(Convert.ToBoolean(res.Result));
return;
}
}
catch
{
}
}
tcs.TrySetResult(false);
}
public ICommand CloseInvoiceDetailCommand => new DelegateCommand(HidenOrderBrowserDetail);
public int BrowserZIndex
{
get => GetValue<int>();
set => SetValue(value);
}
public int BrowserDetailZIndex
{
get => GetValue<int>();
set => SetValue(value);
}
/// <summary>
/// 显示主浏览器
/// </summary>
public void ShowOrderBrowser()
{
BrowserZIndex = 10;
BrowserDetailZIndex = -1;
}
/// <summary>
/// 显示子浏览器
/// </summary>
public void ShowOrderBrowserDetail()
{
BrowserZIndex = -1;
BrowserDetailZIndex = 10;
}
public void HidenOrderBrowser()
{
BrowserZIndex = -1;
}
public void HidenOrderBrowserDetail()
{
BrowserDetailZIndex = -1;
}
public bool IsReadOrder
{
get => GetValue<bool>();
set => SetValue(value);
}
private int top = 0;
private DateTime lastGetOrderDataTime = DateTime.Now;
private void RequestHandler_ResourceLoadComplete(object sender, string url, string postData, string responseData, IRequest request)
{
if (url.StartsWith("https://mobile.yangkeduo.com/orders.html"))
{
//if (isAutoScroll == false)
//{
// StartAutoScroll();
//}
StartAutoScroll();
}
if (url.StartsWith("https://mobile.yangkeduo.com/proxy/api/api/aristotle/order_list_v"))
{
lastGetOrderDataTime = DateTime.Now;
parserData(responseData);
//File.WriteAllText(@$"E:\{Guid.NewGuid().ToString()}.txt", responseData);
}
}
public void StartAutoScroll()
{
if (IsReadOrder) return;
IsReadOrder = true;
_ = autoScroll(_cancellationTokenSource.Token);
}
// 停止滚动
public void StopAutoScroll()
{
IsReadOrder = false;
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
}
private async Task autoScroll(CancellationToken token)
{
while (IsReadOrder && !token.IsCancellationRequested)
{
if ((DateTime.Now - lastGetOrderDataTime).Seconds > 30) StopAutoScroll();
top += Randomx.GetId(450, 600);
var jsStr = @"window.scroll({
top: " + top + @",
left: 0,
behavior: 'smooth' // 可选,使滚动平滑
});";
var client = Browser.GetDevToolsClient();
if (client != null)
{
var result = client.Runtime.EvaluateAsync(jsStr, null, true, null, null, true, null, true);
result.Wait(5000);
}
await Task.Delay(Randomx.GetId(100, 300));
//Thread.Sleep();
}
}
private void DetailRequestHandler_ResourceLoadComplete(object sender, string url, string postData, string responseData, IRequest request)
{
if (url.StartsWith("https://mobile.yangkeduo.com/transac_return_compensation.html"))
{
}
}
}
@@ -0,0 +1,633 @@
using _29WxOrderWpf.BaseWindows;
using _29WxOrderWpf.CefHelper;
using _29WxOrderWpf.Commands;
using _29WxOrderWpf.Common;
using AllBase;
using AllBase.Helper;
using AllBase.Model.DBModel;
using AllBase.Model.Result;
using AllBase.Util;
using CefSharp;
using CefSharp.Handler;
using CefSharp.Wpf;
using NaiBa.Util.Extension;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
using Work._29Tools.CefTools;
using WPFCommon.Converters;
using WPFCommon.Models;
using WxOrder.Framework.Helpers;
namespace _29WxOrderWpf.Ctrls.PDDInvoice.ViewModels;
/// <summary>
/// 拼多多发票助手 视图模型
/// </summary>
public partial class PDDInvoiceViewModel : BindableBaseEx
{
private readonly ObservableCollection<PddBrowserOrderInvoiceViewModel> _allOrders = new();
private readonly HashSet<string> _orderSns = new();
private string _searchText = "";
private PddBrowserOrderInvoiceViewModel _selectedOrder;
private bool _isInvoiceDialogOpen;
private bool _isInvoiceDetailOpen;
private string _invoiceTitle = "";
private string _taxNo = "";
private CancellationTokenSource _cancellationTokenSource; public PDDInvoiceViewModel()
{
Orders = new ObservableCollection<PddBrowserOrderInvoiceViewModel>();
Orders.CollectionChanged += Orders_CollectionChanged;
BuyerLoginList = GlobalConfig.PDDBuyerLoginList;
//Orders = new ObservableCollection<PDDInvoiceOrderModel>(_allOrders);
//_ = InitSampleData();
InitBrowser();
}
partial void InitBrowser();
/// <summary> 列表增删时订阅/退订订单勾选状态,用于刷新表头全选状态 </summary>
private void Orders_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (PddBrowserOrderInvoiceViewModel item in e.NewItems)
{
item.PropertyChanged += Order_PropertyChanged;
}
}
if (e.OldItems != null)
{
foreach (PddBrowserOrderInvoiceViewModel item in e.OldItems)
{
item.PropertyChanged -= Order_PropertyChanged;
}
}
RefreshSelectAllState();
}
private void Order_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(PddBrowserOrderInvoiceViewModel.IsSelected))
{
RefreshSelectAllState();
}
}
public ObservableCollectionThreadSafe<BuyerLoginInfo> BuyerLoginList { get; set; } = new ObservableCollectionThreadSafe<BuyerLoginInfo>();
public BuyerLoginInfo SelectedBuyer
{
get => GetProperty(() => SelectedBuyer);
set => SetProperty(() => SelectedBuyer, value, SelectedBuyerChanged);
}
private void SelectedBuyerChanged()
{
WPFCEFBrowser.SetCookieAsync(SelectedBuyer.cookie, Browser.GetCookieManager()).GetAwaiter();
WPFCEFBrowser.SetCookieAsync(SelectedBuyer.cookie, BrowserDetail.GetCookieManager()).GetAwaiter();
}
/// <summary> 订单列表 </summary>
public ObservableCollection<PddBrowserOrderInvoiceViewModel> Orders { get; }
/// <summary> 搜索关键字:订单号 / 商品名称 / 买家 </summary>
public string SearchText
{
get => _searchText;
set => SetProperty(ref _searchText, value, nameof(SearchText));
}
/// <summary> 开票弹窗是否打开 </summary>
public bool IsOrderListDialogOpen
{
get => GetValue<bool>();
set
{
SetValue(value);
if (value)
{
ShowOrderBrowser();
}
else
{
HidenOrderBrowser();
}
}
}
/// <summary> 发票详情弹窗是否打开 </summary>
public bool IsInvoiceDetailOpen
{
get => _isInvoiceDetailOpen;
set => SetProperty(ref _isInvoiceDetailOpen, value, nameof(IsInvoiceDetailOpen));
}
/// <summary> 当前操作的订单 </summary>
public PddBrowserOrderInvoiceViewModel SelectedOrder
{
get => _selectedOrder;
set => SetProperty(ref _selectedOrder, value, nameof(SelectedOrder));
}
/// <summary> 发票抬头 </summary>
public string InvoiceTitle
{
get => _invoiceTitle;
set => SetProperty(ref _invoiceTitle, value, nameof(InvoiceTitle));
}
/// <summary> 税号 </summary>
public string TaxNo
{
get => _taxNo;
set => SetProperty(ref _taxNo, value, nameof(TaxNo));
}
public ICommand SearchCommand => new DelegateCommand(Search);
public ICommand ResetCommand => new DelegateCommand(ResetSearch);
public ICommand OpenInvoiceCommand => new DelegateCommand<PddBrowserOrderInvoiceViewModel>(OpenInvoice);
public ICommand ShowInvoiceCommand => new DelegateCommand<PddBrowserOrderInvoiceViewModel>(ShowInvoice);
public ICommand ViewOrderCommand => new DelegateCommand<PddBrowserOrderInvoiceViewModel>(ViewOrder);
public ICommand ReadOrderCommand => new AsyncDelegateCommand(ReadOrder);
private async Task ReadOrder()
{
if (SelectedBuyer == null)
{
BaseMsgWindow.ShowError("未填选择买手号");
return;
}
_cancellationTokenSource = new CancellationTokenSource();
Debug.WriteLine("开始读单");
BottomMsg = $"开始读取订单";
StartProgress();
try
{
if (!IsReadOrder)
{
_orderSns.Clear();
_allOrders.Clear();
Orders.Clear();
Browser.Load("https://mobile.yangkeduo.com/orders.html");
}
await Task.Delay(Timeout.Infinite, _cancellationTokenSource.Token);
}
catch (TaskCanceledException)
{
Debug.WriteLine("停止读单");
}
//ShowOrderBrowser();
}
public ICommand StopReadOrderCommand => new AsyncDelegateCommand(StopReadOrder);
private async Task StopReadOrder()
{
StopAutoScroll();
CompleteProgress();
}
#region 勾选 / 全选 / 反选
private bool? _isSelectAll;
private bool _isUpdatingSelectAll;
/// <summary> 表头全选状态(三态:null 表示部分选中) </summary>
public bool? IsSelectAll
{
get => _isSelectAll;
set
{
if (_isSelectAll == value) return;
_isSelectAll = value;
RaisePropertyChanged(nameof(IsSelectAll));
if (_isUpdatingSelectAll) return;
if (value == true)
{
foreach (var order in Orders) order.IsSelected = true;
}
else if (value == false)
{
foreach (var order in Orders) order.IsSelected = false;
}
RefreshSelectAllState();
}
}
public ICommand InvertSelectionCommand => new DelegateCommand(InvertSelection);
/// <summary> 反选当前可见列表 </summary>
private void InvertSelection()
{
foreach (var order in Orders) order.IsSelected = !order.IsSelected;
RefreshSelectAllState();
}
/// <summary> 根据当前可见列表的勾选情况刷新表头全选状态 </summary>
private void RefreshSelectAllState()
{
_isUpdatingSelectAll = true;
try
{
if (Orders.Count == 0)
{
IsSelectAll = false;
return;
}
int selected = Orders.Count(o => o.IsSelected);
IsSelectAll = selected == 0 ? false : selected == Orders.Count ? true : (bool?)null;
}
finally
{
_isUpdatingSelectAll = false;
}
}
#endregion
#region 批量开票
private bool _isBatchInvoicing;
/// <summary> 是否正在批量开票 </summary>
public bool IsBatchInvoicing
{
get => _isBatchInvoicing;
set => SetProperty(ref _isBatchInvoicing, value, nameof(IsBatchInvoicing));
}
/// <summary> 当前正在批量处理的订单,页面脚本执行完成后通过它回传结果 </summary>
private TaskCompletionSource<bool> _currentInvoiceTcs;
public ICommand BatchInvoiceCommand => new AsyncDelegateCommand(BatchInvoice);
/// <summary>
/// 批量开票:统计勾选的待开票订单 → 确认 → 后台逐单自动开票
/// </summary>
private async Task BatchInvoice()
{
if (IsBatchInvoicing) return;
if (string.IsNullOrEmpty(InvoiceTitle) || string.IsNullOrEmpty(TaxNo))
{
BaseMsgWindow.ShowError("未填写公司或税号");
return;
}
var targets = Orders.Where(o => o.IsSelected && !o.IsInvoiced).ToList();
if (targets.Count == 0)
{
BaseMsgWindow.ShowError("请先勾选需要开票的订单");
return;
}
var confirm = BaseMsgWindow.ShowConfirm(
$"已勾选 {targets.Count} 个待开票订单,确定开始批量开票吗?\n请确认发票抬头与税号无误。",
"批量开票");
if (confirm.DialogResult != true) return;
await BatchInvoiceAsync(targets);
}
/// <summary>
/// 后台批量开票:逐单加载开票页并自动填写提交,底部进度条实时更新,成功后更新列表状态
/// </summary>
private async Task BatchInvoiceAsync(List<PddBrowserOrderInvoiceViewModel> targets)
{
IsBatchInvoicing = true;
int success = 0, fail = 0;
try
{
ProgressMaximum = targets.Count;
ProgressValue = 0;
BottomMsg = $"正在批量开票:0/{targets.Count}";
for (int i = 0; i < targets.Count; i++)
{
var order = targets[i];
BottomMsg = $"正在批量开票:{i}/{targets.Count},当前订单:{order.order_sn}";
ProgressValue = i;
_currentInvoiceTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
BrowserDetail.Load($"https://mobile.yangkeduo.com/transac_return_compensation.html?_t_timestamp=transac_modify_invoice&order_sn={order.order_sn}");
// 等待页面脚本执行完成(最长 60 秒)
var done = await Task.WhenAny(_currentInvoiceTcs.Task, Task.Delay(TimeSpan.FromSeconds(60)));
bool ok = done == _currentInvoiceTcs.Task && _currentInvoiceTcs.Task.Result;
if (ok)
{
order.IsInvoiced = true;
success++;
}
else
{
fail++;
}
ProgressValue = success + fail;
}
}
catch (Exception ex)
{
fail = targets.Count - success;
BaseMsgWindow.ShowError("批量开票异常:" + ex.Message);
}
finally
{
_currentInvoiceTcs = null;
ProgressMaximum = targets.Count;
ProgressValue = targets.Count;
BottomMsg = $"批量开票完成:成功 {success} 个,失败 {fail} 个";
IsBatchInvoicing = false;
}
}
#endregion
/// <summary> 按 订单号 / 商品名称 过滤订单 </summary>
private void Search()
{
var keyword = SearchText?.Trim();
Orders.Clear();
foreach (var order in _allOrders.Where(o => IsKeywordMatch(o, keyword)))
{
Orders.Add(order);
}
}
private static bool IsKeywordMatch(PddBrowserOrderInvoiceViewModel order, string keyword)
{
return string.IsNullOrEmpty(keyword)
|| (!string.IsNullOrEmpty(order.order_sn) && order.order_sn.Contains(keyword))
|| (!string.IsNullOrEmpty(order.GoodsName) && order.GoodsName.Contains(keyword));
}
private void ResetSearch()
{
SearchText = "";
Search();
}
/// <summary>
/// 申请开票
/// </summary>
/// <param name="order"></param>
private void OpenInvoice(PddBrowserOrderInvoiceViewModel order)
{
SelectedOrder = order;
if (string.IsNullOrEmpty(InvoiceTitle) || string.IsNullOrEmpty(TaxNo))
{
BaseMsgWindow.ShowError("未填写公司或税号");
return;
}
BrowserDetail.Load($"https://mobile.yangkeduo.com/transac_return_compensation.html?_t_timestamp=transac_modify_invoice&order_sn={order.order_sn}");
ShowOrderBrowserDetail();
}
private void ShowInvoice(PddBrowserOrderInvoiceViewModel order)
{
SelectedOrder = order;
BrowserDetail.Load($"https://mobile.yangkeduo.com/transac_return_compensation.html?_t_timestamp=transac_invoice&order_sn={order.order_sn}");
ShowOrderBrowserDetail();
}
private void ViewOrder(PddBrowserOrderInvoiceViewModel order)
{
SelectedOrder = order;
BrowserDetail.Load($"https://mobile.yangkeduo.com/{order.order_link_url}");
ShowOrderBrowserDetail();
//MessageBox.Show($"查看订单:{order.OrderNo}");
}
#region 数据解析
private void parserData(string data)
{
List<PddBrowserOrderInvoiceViewModel> list = JsonHelper.DeserializeJsonToList<PddBrowserOrderInvoiceViewModel>(JsonHelper.JObjectParse(data)["orders"].ParseToString());
if (list.Count == 0)
{
StopAutoScroll();
return;
}
var matched = list.Where(item => item.status == 4 && item.pay_status == 2).ToList();
if (matched.Count == 0)
{
return;
}
// CEF 回调运行在后台线程,ObservableCollection 的更新需要切回 UI 线程
var dispatcher = Application.Current?.Dispatcher;
if (dispatcher == null || dispatcher.CheckAccess())
{
AddOrders(matched);
}
else
{
dispatcher.BeginInvoke(new Action(() =>
{
AddOrders(matched);
BottomMsg = $"正在读取订单,读取日期{matched.Last().OrderTime.ToString("yyyy-MM-dd")}";
}));
}
}
/// <summary> 将新订单写入总表,并按当前搜索关键字决定是否加入可见列表(自动去重) </summary>
private void AddOrders(IEnumerable<PddBrowserOrderInvoiceViewModel> items)
{
var keyword = SearchText?.Trim();
foreach (var item in items)
{
if (!string.IsNullOrEmpty(item.order_sn) && !_orderSns.Add(item.order_sn))
{
continue;
}
_allOrders.Add(item);
if (IsKeywordMatch(item, keyword))
{
item.IsInvoiced = item.order_buttons?.Any(a => a.brief_prompt == "发票详情") ?? false;
Orders.Add(item);
}
}
}
#endregion
/// <summary>
/// 原型演示数据(后续可替换为真实接口数据)
/// </summary>
private Task InitSampleData()
{
string directoryPath = @"E:\820";
// 3. 获取所有 txt 文件
var txtFiles = Directory.GetFiles(directoryPath, "*.txt");
if (txtFiles.Length == 0)
{
Console.WriteLine("未找到任何 .txt 文件");
return Task.CompletedTask;
}
foreach (var filePath in txtFiles)
{
try
{
string jsonContent = File.ReadAllText(filePath);
parserData(jsonContent);
}
catch (Exception ex)
{
}
}
return Task.CompletedTask;
}
#region 进度条
private string _bottomMsg = "";
public string BottomMsg
{
get => _bottomMsg;
set => SetValue(ref _bottomMsg, value);
}
private double _progressValue;
public double ProgressValue
{
get => _progressValue;
set => SetValue(ref _progressValue, value);
}
private double _progressMaximum = 100;
public double ProgressMaximum
{
get => _progressMaximum;
set => SetValue(ref _progressMaximum, value);
}
public void SetProgress(int value, int maxValue)
{
StopUnknownTotalProgressTimer();
ProgressValue = value;
ProgressMaximum = maxValue;
}
#region 未知总数百分比模拟进度
private System.Threading.Timer _unknownTotalProgressTimer;
private DateTime _unknownTotalProgressStartTime;
/// <summary>
/// 开始未知总数的百分比模拟进度:80% 之前走得快,越到最后走得越慢,
/// 理论上永远走不到 95% 以上,直到调用 <see cref="CompleteProgress"/> 才会变成 100%。
/// </summary>
public void StartProgress()
{
StopUnknownTotalProgressTimer();
ProgressMaximum = 100;
ProgressValue = 0;
_unknownTotalProgressStartTime = DateTime.Now;
_unknownTotalProgressTimer = new System.Threading.Timer(_ => UpdateUnknownTotalProgress(), null, 0, 200);
}
/// <summary>
/// 结束进度:停止模拟,进度条立即变为 100%。
/// </summary>
public void CompleteProgress()
{
StopUnknownTotalProgressTimer();
ProgressMaximum = 100;
ProgressValue = 100;
}
private void UpdateUnknownTotalProgress()
{
try
{
System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
var elapsed = (DateTime.Now - _unknownTotalProgressStartTime).TotalSeconds;
double progress;
if (elapsed <= 10)
{
// 前 10 秒内快速推进到 80%
progress = Math.Max(0, Math.Min(80, elapsed * 8));
}
else
{
// 超过 80% 后指数衰减越来越慢,渐近线为 95%,理论上永远走不到 95% 以上
progress = 95.0 - 15.0 * Math.Exp(-(elapsed - 10.0) / 20.0);
}
if (progress > 95) progress = 95;
ProgressValue = progress;
}));
}
catch
{
}
}
private void StopUnknownTotalProgressTimer()
{
if (_unknownTotalProgressTimer == null) return;
try
{
_unknownTotalProgressTimer.Dispose();
}
catch
{
}
_unknownTotalProgressTimer = null;
}
#endregion
#endregion
}
@@ -122,7 +122,7 @@
</UserControl.Resources>
<Grid Background="#F5F7FB">
<TabControl Style="{StaticResource StatisticsTabControlStyle}" SelectionChanged="TabControl_SelectionChanged">
<TabControl SelectionChanged="TabControl_SelectionChanged" Style="{StaticResource StatisticsTabControlStyle}">
<TabItem Header="综合统计" Style="{StaticResource StatisticsTabItemStyle}">
<ScrollViewer
wpf:ScrollViewerBehavior.FixMouseWheel="True"
@@ -283,12 +283,19 @@
BorderThickness="1"
Cursor="Hand"
MouseLeftButtonDown="help_mouseLeftButtonDown">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontWeight="Black"
Foreground="White"
Text="利润计算逻辑" />
<WrapPanel>
<TextBlock
Margin="0,0,10,0"
Foreground="White"
Style="{StaticResource TextBlockIcon}"
Text="{StaticResource txt-chat_help}" />
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontWeight="Black"
Foreground="White"
Text="利润计算逻辑" />
</WrapPanel>
</Border>
<!-- Popup 弹出层 -->
@@ -307,11 +314,13 @@
Margin="0,-10,-10,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Background="Transparent" Click="close"
Background="Transparent"
Click="close"
Content="x" />
<TextBlock Margin="0,10,0,0"
<TextBlock
x:Name="PopupText"
MaxWidth="500"
Margin="0,10,0,0"
FontSize="16"
Foreground="White"
LineHeight="25"
@@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WxOrder.Framework.Helpers;
/// <summary>
/// Cookie对象模型
/// </summary>
public class CookieItem
{
public string Name { get; set; }
public string Value { get; set; }
public string Domain { get; set; }
public string Path { get; set; }
public bool Secure { get; set; }
public bool HttpOnly { get; set; }
public DateTime? Expires { get; set; }
public DateTime? Creation { get; set; }
public DateTime? LastAccess { get; set; }
public int SameSite { get; set; }
public int Priority { get; set; }
}
/// <summary>
/// Cookie转换帮助类
/// </summary>
public static class CookieConvertHelper
{
/// <summary>
/// 将CookieItem列表转换为Cookie字符串
/// </summary>
/// <param name="cookieItems">Cookie对象列表</param>
/// <returns>Cookie字符串,格式:name1=value1; name2=value2</returns>
public static string ToCookieString(List<CookieItem> cookieItems)
{
if (cookieItems == null || cookieItems.Count == 0)
return string.Empty;
var builder = new StringBuilder();
for (int i = 0; i < cookieItems.Count; i++)
{
var item = cookieItems[i];
if (!string.IsNullOrEmpty(item.Name) && item.Value != null)
{
builder.Append($"{item.Name}={item.Value}");
if (i < cookieItems.Count - 1)
builder.Append("; ");
}
}
return builder.ToString();
}
/// <summary>
/// 将单个CookieItem转换为Cookie字符串
/// </summary>
/// <param name="cookieItem">Cookie对象</param>
/// <returns>Cookie字符串,格式:name=value</returns>
public static string ToCookieString(CookieItem cookieItem)
{
if (cookieItem == null || string.IsNullOrEmpty(cookieItem.Name))
return string.Empty;
return $"{cookieItem.Name}={cookieItem.Value}";
}
/// <summary>
/// 将CookieItem列表转换为字典
/// </summary>
/// <param name="cookieItems">Cookie对象列表</param>
/// <returns>Cookie字典</returns>
public static Dictionary<string, string> ToDictionary(List<CookieItem> cookieItems)
{
var dict = new Dictionary<string, string>();
if (cookieItems == null || cookieItems.Count == 0)
return dict;
foreach (var item in cookieItems)
{
if (!string.IsNullOrEmpty(item.Name) && !dict.ContainsKey(item.Name))
{
dict.Add(item.Name, item.Value);
}
}
return dict;
}
/// <summary>
/// 将CookieItem列表转换为System.Net.CookieCollection
/// </summary>
/// <param name="cookieItems">Cookie对象列表</param>
/// <param name="domain">域名</param>
/// <returns>CookieCollection</returns>
public static System.Net.CookieCollection ToCookieCollection(List<CookieItem> cookieItems, string domain = "mobile.yangkeduo.com")
{
var cookieCollection = new System.Net.CookieCollection();
if (cookieItems == null || cookieItems.Count == 0)
return cookieCollection;
foreach (var item in cookieItems)
{
if (string.IsNullOrEmpty(item.Name))
continue;
var cookie = new System.Net.Cookie
{
Name = item.Name,
Value = item.Value ?? string.Empty,
Domain = string.IsNullOrEmpty(item.Domain) ? domain : item.Domain,
Path = string.IsNullOrEmpty(item.Path) ? "/" : item.Path,
Secure = item.Secure,
HttpOnly = item.HttpOnly,
Expires = item.Expires ?? DateTime.MinValue
};
cookieCollection.Add(cookie);
}
return cookieCollection;
}
/// <summary>
/// 从JSON数组字符串转换为Cookie字符串
/// </summary>
/// <param name="jsonArray">JSON数组字符串</param>
/// <returns>Cookie字符串</returns>
public static string FromJsonToCookieString(string cookie)
{
try
{
var cookieItems = Newtonsoft.Json.JsonConvert.DeserializeObject<List<CookieItem>>(cookie);
return ToCookieString(cookieItems);
}
catch
{
return string.Empty;
}
}
/// <summary>
/// 从JSON数组字符串转换为字典
/// </summary>
/// <param name="jsonArray">JSON数组字符串</param>
/// <returns>Cookie字典</returns>
public static Dictionary<string, string> FromJsonToDictionary(string jsonArray)
{
try
{
var cookieItems = Newtonsoft.Json.JsonConvert.DeserializeObject<List<CookieItem>>(jsonArray);
return ToDictionary(cookieItems);
}
catch
{
return new Dictionary<string, string>();
}
}
}
+839
View File
@@ -0,0 +1,839 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace WxOrder.Framework.Helpers
{
/// <summary>
/// Http连接操作帮助类
/// </summary>
public class HttpHelper
{
public static HttpResult Get(string Url)
{
return new HttpHelper().GetHtml(new HttpItem
{
URL = Url,
Method = MethodType.Get,
});
}
public static HttpResult Post(string Url, string PostData)
{
return new HttpHelper().GetHtml(new HttpItem
{
URL = Url,
Method = MethodType.Get,
Postdata = PostData,
PostDataType = PostDataType.String
});
}
public static HttpResult Request(HttpItem item)
{
return new HttpHelper().GetHtml(item);
}
/// <summary>
/// 下载文件
/// </summary>
/// <param name="address"></param>
/// <param name="fileName"></param>
public static byte[] DownloadFile(string address)
{
HttpItem item = new HttpItem();
item.URL = address;
item.Timeout = 3000;
item.ResultType = ResultType.Byte;
HttpHelper http = new HttpHelper();
HttpResult result = http.GetHtml(item);
if (result.StatusCode == HttpStatusCode.OK)
{
return result.ResultByte;
}
else
{
throw new HttpListenerException((int)result.StatusCode, result.StatusDescription);
}
}
/// <summary>
/// 下载文件
/// </summary>
/// <param name="address"></param>
/// <param name="fileName"></param>
public static bool DownloadFile(string address, string fileName)
{
HttpItem item = new HttpItem();
item.URL = address;
item.Timeout = 3000;
item.ResultType = ResultType.Byte;
HttpHelper http = new HttpHelper();
HttpResult result = http.GetHtml(item);
if (result.StatusCode == HttpStatusCode.OK)
{
FileStream fs = new FileStream(fileName, FileMode.CreateNew);
BinaryWriter bw = new BinaryWriter(fs);
bw.Write(result.ResultByte);
fs.Close();
bw.Close();
}
else
{
throw new HttpListenerException((int)result.StatusCode, result.StatusDescription);
}
return true;
}
#region 预定义方法或者变更
//默认的编码
private Encoding encoding = Encoding.Default;
//Post数据编码
private Encoding postencoding = Encoding.Default;
//HttpWebRequest对象用来发起请求
private HttpWebRequest request = null;
//获取影响流的数据对象
private HttpWebResponse response = null;
/// <summary>
/// 根据相传入的数据,得到相应页面数据
/// </summary>
/// <param name="item">参数类对象</param>
/// <returns>返回HttpResult类型</returns>
public HttpResult GetHtml(HttpItem item)
{
//返回参数
HttpResult result = new HttpResult();
result.item = item;
try
{
//准备参数
SetRequest(item);
}
catch (Exception ex)
{
result = new HttpResult();
result.Cookie = string.Empty;
result.Header = null;
result.Html = ex.Message;
result.StatusDescription = "配置参数时出错:" + ex.Message;
return result;
}
try
{
#region 得到请求的response
using (response = (HttpWebResponse)request.GetResponse())
{
result.StatusCode = response.StatusCode;
result.StatusDescription = response.StatusDescription;
result.Header = response.Headers;
if (response.Cookies != null) result.CookieCollection = response.Cookies;
if (response.Headers["set-cookie"] != null) result.Cookie = response.Headers["set-cookie"];
MemoryStream _stream = new MemoryStream();
//GZIIP处理
if (response.ContentEncoding != null && response.ContentEncoding.Equals("gzip", StringComparison.InvariantCultureIgnoreCase))
{
//开始读取流并设置编码方式
//new GZipStream(response.GetResponseStream(), CompressionMode.Decompress).CopyTo(_stream, 10240);
//.net4.0以下写法
_stream = GetMemoryStream(new GZipStream(response.GetResponseStream(), CompressionMode.Decompress));
}
else
{
//开始读取流并设置编码方式
//response.GetResponseStream().CopyTo(_stream, 10240);
//.net4.0以下写法
_stream = GetMemoryStream(response.GetResponseStream());
}
//获取Byte
byte[] ResponseByte = _stream.ToArray();
_stream.Close();
if (ResponseByte != null & ResponseByte.Length > 0)
{
//是否返回Byte类型数据
if (item.ResultType == ResultType.Byte) result.ResultByte = ResponseByte;
//从这里开始我们要无视编码了
if (encoding == null)
{
Match meta = Regex.Match(Encoding.Default.GetString(ResponseByte), "<meta([^<]*)charset=([^<]*)[\"']", RegexOptions.IgnoreCase);
string c = (meta.Groups.Count > 1) ? meta.Groups[2].Value.ToLower().Trim() : string.Empty;
if (c.Length > 2)
{
try
{
if (c.IndexOf(" ") > 0) c = c.Substring(0, c.IndexOf(" "));
encoding = Encoding.GetEncoding(c.Replace("\"", "").Replace("'", "").Replace(";", "").Replace("iso-8859-1", "gbk").Trim());
}
catch
{
if (string.IsNullOrEmpty(response.CharacterSet)) encoding = Encoding.UTF8;
else encoding = Encoding.GetEncoding(response.CharacterSet);
}
}
else
{
if (string.IsNullOrEmpty(response.CharacterSet)) encoding = Encoding.UTF8;
else encoding = Encoding.GetEncoding(response.CharacterSet);
}
}
//得到返回的HTML
result.Html = encoding.GetString(ResponseByte);
}
else
{
//得到返回的HTML
result.Html = "本次请求并未返回任何数据";
}
}
#endregion
}
catch (WebException ex)
{
//这里是在发生异常时返回的错误信息
response = (HttpWebResponse)ex.Response;
result.Html = ex.Message;
if (response != null)
{
result.StatusCode = response.StatusCode;
result.StatusDescription = response.StatusDescription;
}
}
catch (Exception ex)
{
result.Html = ex.Message;
}
if (item.IsToLower) result.Html = result.Html.ToLower();
return result;
}
/// <summary>
/// 4.0以下.net版本取数据使用
/// </summary>
/// <param name="streamResponse">流</param>
private MemoryStream GetMemoryStream(Stream streamResponse)
{
MemoryStream _stream = new MemoryStream();
int Length = 256;
Byte[] buffer = new Byte[Length];
int bytesRead = streamResponse.Read(buffer, 0, Length);
while (bytesRead > 0)
{
_stream.Write(buffer, 0, bytesRead);
bytesRead = streamResponse.Read(buffer, 0, Length);
}
return _stream;
}
/// <summary>
/// 为请求准备参数
/// </summary>
///<param name="item">参数列表</param>
private void SetRequest(HttpItem item)
{
// 验证证书
SetCer(item);
//设置Header参数
if (item.Header != null && item.Header.Count > 0) foreach (string key in item.Header.AllKeys)
{
request.Headers.Add(key, item.Header[key]);
}
// 设置代理
SetProxy(item);
if (item.ProtocolVersion != null) request.ProtocolVersion = item.ProtocolVersion;
request.ServicePoint.Expect100Continue = item.Expect100Continue;
//请求方式Get或者Post
request.Method = item.Method.ToString();
request.Timeout = item.Timeout;
request.KeepAlive = item.KeepAlive;
request.ReadWriteTimeout = item.ReadWriteTimeout;
//Accept
request.Accept = item.Accept;
//ContentType返回类型
request.ContentType = item.ContentType;
//UserAgent客户端的访问类型,包括浏览器版本和操作系统信息
request.UserAgent = item.UserAgent;
// 编码
encoding = item.Encoding;
//设置安全凭证
request.Credentials = item.ICredentials;
//设置Cookie
SetCookie(item);
//来源地址
request.Referer = item.Referer;
//是否执行跳转功能
request.AllowAutoRedirect = item.Allowautoredirect;
//设置Post数据
SetPostData(item);
//设置最大连接
if (item.Connectionlimit > 0) request.ServicePoint.ConnectionLimit = item.Connectionlimit;
}
/// <summary>
/// 设置证书
/// </summary>
/// <param name="item"></param>
private void SetCer(HttpItem item)
{
if (!string.IsNullOrEmpty(item.CerPath))
{
//这一句一定要写在创建连接的前面。使用回调的方法进行证书验证。
ServicePointManager.ServerCertificateValidationCallback = new System.Net.Security.RemoteCertificateValidationCallback(CheckValidationResult);
//初始化对像,并设置请求的URL地址
request = (HttpWebRequest)WebRequest.Create(item.URL);
SetCerList(item);
//将证书添加到请求里
request.ClientCertificates.Add(new X509Certificate(item.CerPath));
}
else
{
//初始化对像,并设置请求的URL地址
request = (HttpWebRequest)WebRequest.Create(item.URL);
SetCerList(item);
}
}
/// <summary>
/// 设置多个证书
/// </summary>
/// <param name="item"></param>
private void SetCerList(HttpItem item)
{
if (item.ClentCertificates != null && item.ClentCertificates.Count > 0)
{
foreach (X509Certificate c in item.ClentCertificates)
{
request.ClientCertificates.Add(c);
}
}
}
/// <summary>
/// 设置Cookie
/// </summary>
/// <param name="item">Http参数</param>
private void SetCookie(HttpItem item)
{
if (!string.IsNullOrEmpty(item.Cookie)) request.Headers[HttpRequestHeader.Cookie] = item.Cookie;
//设置CookieCollection
if (item.ResultCookieType == ResultCookieType.CookieCollection)
{
request.CookieContainer = new CookieContainer();
if (item.CookieCollection != null && item.CookieCollection.Count > 0)
request.CookieContainer.Add(item.CookieCollection);
}
}
/// <summary>
/// 设置Post数据
/// </summary>
/// <param name="item">Http参数</param>
private void SetPostData(HttpItem item)
{
//验证在得到结果时是否有传入数据
if (request.Method.Trim().ToLower().Contains("post"))
{
if (item.PostEncoding != null)
{
postencoding = item.PostEncoding;
}
byte[] buffer = null;
//写入Byte类型
if (item.PostDataType == PostDataType.Byte && item.PostdataByte != null && item.PostdataByte.Length > 0)
{
//验证在得到结果时是否有传入数据
buffer = item.PostdataByte;
}//写入文件
else if (item.PostDataType == PostDataType.FilePath && !string.IsNullOrEmpty(item.Postdata))
{
StreamReader r = new StreamReader(item.Postdata, postencoding);
buffer = postencoding.GetBytes(r.ReadToEnd());
r.Close();
} //写入字符串
else if (!string.IsNullOrEmpty(item.Postdata))
{
buffer = postencoding.GetBytes(item.Postdata);
}
if (buffer != null)
{
request.ContentLength = buffer.Length;
request.GetRequestStream().Write(buffer, 0, buffer.Length);
}
}
}
/// <summary>
/// 设置代理
/// </summary>
/// <param name="item">参数对象</param>
private void SetProxy(HttpItem item)
{
bool isIeProxy = item.ProxyIp.ToLower().Contains("ieproxy");
if (!string.IsNullOrEmpty(item.ProxyIp) && !isIeProxy)
{
//设置代理服务器
if (item.ProxyIp.Contains(":"))
{
string[] plist = item.ProxyIp.Split(':');
WebProxy myProxy = new WebProxy(plist[0].Trim(),int.Parse(plist[1].Trim()));
//建议连接
myProxy.Credentials = new NetworkCredential(item.ProxyUserName, item.ProxyPwd);
//给当前请求对象
request.Proxy = myProxy;
}
else
{
WebProxy myProxy = new WebProxy(item.ProxyIp, false);
//建议连接
myProxy.Credentials = new NetworkCredential(item.ProxyUserName, item.ProxyPwd);
//给当前请求对象
request.Proxy = myProxy;
}
}
else if (isIeProxy)
{
//设置为IE代理
}
else
{
request.Proxy = item.WebProxy;
}
}
/// <summary>
/// 回调验证证书问题
/// </summary>
/// <param name="sender">流对象</param>
/// <param name="certificate">证书</param>
/// <param name="chain">X509Chain</param>
/// <param name="errors">SslPolicyErrors</param>
/// <returns>bool</returns>
public bool CheckValidationResult(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) { return true; }
#endregion
}
/// <summary>
/// Http请求参考类
/// </summary>
public class HttpItem
{
string _URL = string.Empty;
/// <summary>
/// 请求URL必须填写
/// </summary>
public string URL
{
get { return _URL; }
set { _URL = value; }
}
MethodType _Method = MethodType.Get;
/// <summary>
/// 请求方式默认为GET方式,当为POST方式时必须设置Postdata的值
public MethodType Method
/// </summary>
{
get { return _Method; }
set
{
_Method = value;
if (_Method == MethodType.Post)
{
_ContentType = "application/x-www-form-urlencoded";
}
}
}
int _Timeout = 100000;
/// <summary>
/// 默认请求超时时间
/// </summary>
public int Timeout
{
get { return _Timeout; }
set { _Timeout = value; }
}
int _ReadWriteTimeout = 30000;
/// <summary>
/// 默认写入Post数据超时间
/// </summary>
public int ReadWriteTimeout
{
get { return _ReadWriteTimeout; }
set { _ReadWriteTimeout = value; }
}
Boolean _KeepAlive = true;
/// <summary>
/// 获取或设置一个值,该值指示是否与 Internet 资源建立持久性连接默认为true。
/// </summary>
public Boolean KeepAlive
{
get { return _KeepAlive; }
set { _KeepAlive = value; }
}
string _Accept = "text/html, application/xhtml+xml, */*";
/// <summary>
/// 请求标头值 默认为text/html, application/xhtml+xml, */*
/// </summary>
public string Accept
{
get { return _Accept; }
set { _Accept = value; }
}
string _ContentType = "text/html";
/// <summary>
/// 请求返回类型默认 text/html
/// </summary>
public string ContentType
{
get { return _ContentType; }
set { _ContentType = value; }
}
string _UserAgent = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)";
/// <summary>
/// 客户端访问信息默认Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)
/// </summary>
public string UserAgent
{
get { return _UserAgent; }
set { _UserAgent = value; }
}
Encoding _Encoding = null;
/// <summary>
/// 返回数据编码默认为NUll,可以自动识别,一般为utf-8,gbk,gb2312
/// </summary>
public Encoding Encoding
{
get { return _Encoding; }
set { _Encoding = value; }
}
private PostDataType _PostDataType = PostDataType.String;
/// <summary>
/// Post的数据类型
/// </summary>
public PostDataType PostDataType
{
get { return _PostDataType; }
set { _PostDataType = value; }
}
string _Postdata = string.Empty;
/// <summary>
/// Post请求时要发送的字符串Post数据
/// </summary>
public string Postdata
{
get { return _Postdata; }
set { _Postdata = value; }
}
private byte[] _PostdataByte = null;
/// <summary>
/// Post请求时要发送的Byte类型的Post数据
/// </summary>
public byte[] PostdataByte
{
get { return _PostdataByte; }
set { _PostdataByte = value; }
}
private WebProxy _WebProxy;
/// <summary>
/// 设置代理对象,不想使用IE默认配置就设置为Null,而且不要设置ProxyIp
/// </summary>
public WebProxy WebProxy
{
get { return _WebProxy; }
set { _WebProxy = value; }
}
CookieCollection cookiecollection = null;
/// <summary>
/// Cookie对象集合
/// </summary>
public CookieCollection CookieCollection
{
get { return cookiecollection; }
set { cookiecollection = value; }
}
string _Cookie = string.Empty;
/// <summary>
/// 请求时的Cookie
/// </summary>
public string Cookie
{
get { return _Cookie; }
set { _Cookie = value; }
}
string _Referer = string.Empty;
/// <summary>
/// 来源地址,上次访问地址
/// </summary>
public string Referer
{
get { return _Referer; }
set { _Referer = value; }
}
string _CerPath = string.Empty;
/// <summary>
/// 证书绝对路径
/// </summary>
public string CerPath
{
get { return _CerPath; }
set { _CerPath = value; }
}
private Boolean isToLower = false;
/// <summary>
/// 是否设置为全文小写,默认为不转化
/// </summary>
public Boolean IsToLower
{
get { return isToLower; }
set { isToLower = value; }
}
private Boolean allowautoredirect = true;
/// <summary>
/// 支持跳转页面,查询结果将是跳转后的页面
/// </summary>
public Boolean Allowautoredirect
{
get { return allowautoredirect; }
set { allowautoredirect = value; }
}
private int connectionlimit = 1024;
/// <summary>
/// 最大连接数
/// </summary>
public int Connectionlimit
{
get { return connectionlimit; }
set { connectionlimit = value; }
}
private string proxyusername = string.Empty;
/// <summary>
/// 代理Proxy 服务器用户名
/// </summary>
public string ProxyUserName
{
get { return proxyusername; }
set { proxyusername = value; }
}
private string proxypwd = string.Empty;
/// <summary>
/// 代理 服务器密码
/// </summary>
public string ProxyPwd
{
get { return proxypwd; }
set { proxypwd = value; }
}
private string proxyip = string.Empty;
/// <summary>
/// 代理 服务IP ,如果要使用IE代理就设置为ieproxy
/// </summary>
public string ProxyIp
{
get { return proxyip; }
set { proxyip = value; }
}
private ResultType resulttype = ResultType.String;
/// <summary>
/// 设置返回类型String和Byte
/// </summary>
public ResultType ResultType
{
get { return resulttype; }
set { resulttype = value; }
}
private WebHeaderCollection header = new WebHeaderCollection();
/// <summary>
/// header对象
/// </summary>
public WebHeaderCollection Header
{
get { return header; }
set { header = value; }
}
private Version _ProtocolVersion;
/// <summary>
// 获取或设置用于请求的 HTTP 版本。返回结果:用于请求的 HTTP 版本。默认为 System.Net.HttpVersion.Version11。
/// </summary>
public Version ProtocolVersion
{
get { return _ProtocolVersion; }
set { _ProtocolVersion = value; }
}
private Boolean _expect100continue = true;
/// <summary>
/// 获取或设置一个 System.Boolean 值,该值确定是否使用 100-Continue 行为。如果 POST 请求需要 100-Continue 响应,则为 true;否则为 false。默认值为 true。
/// </summary>
public Boolean Expect100Continue
{
get { return _expect100continue; }
set { _expect100continue = value; }
}
private X509CertificateCollection _ClentCertificates;
/// <summary>
/// 设置509证书集合
/// </summary>
public X509CertificateCollection ClentCertificates
{
get { return _ClentCertificates; }
set { _ClentCertificates = value; }
}
private Encoding _PostEncoding;
/// <summary>
/// 设置或获取Post参数编码,默认的为Default编码
/// </summary>
public Encoding PostEncoding
{
get { return _PostEncoding; }
set { _PostEncoding = value; }
}
private ResultCookieType _ResultCookieType = ResultCookieType.String;
/// <summary>
/// Cookie返回类型,默认的是只返回字符串类型
/// </summary>
public ResultCookieType ResultCookieType
{
get { return _ResultCookieType; }
set { _ResultCookieType = value; }
}
private ICredentials _ICredentials = CredentialCache.DefaultCredentials;
/// <summary>
/// 获取或设置请求的身份验证信息。
/// </summary>
public ICredentials ICredentials
{
get { return _ICredentials; }
set { _ICredentials = value; }
}
}
/// <summary>
/// Http返回参数类
/// </summary>
public class HttpResult
{
private string _Cookie;
/// <summary>
/// Http请求返回的Cookie
/// </summary>
public string Cookie
{
get { return _Cookie; }
set { _Cookie = value; }
}
private CookieCollection _CookieCollection;
/// <summary>
/// Cookie对象集合
/// </summary>
public CookieCollection CookieCollection
{
get { return _CookieCollection; }
set { _CookieCollection = value; }
}
private string _Html;
/// <summary>
/// 返回的String类型数据 只有ResultType.String时才返回数据,其它情况为空
/// </summary>
public string Html
{
get { return _Html; }
set { _Html = value; }
}
private byte[] _ResultByte;
/// <summary>
/// 返回的Byte数组 只有ResultType.Byte时才返回数据,其它情况为空
/// </summary>
public byte[] ResultByte
{
get { return _ResultByte; }
set { _ResultByte = value; }
}
private WebHeaderCollection _Header;
/// <summary>
/// header对象
/// </summary>
public WebHeaderCollection Header
{
get { return _Header; }
set { _Header = value; }
}
private string _StatusDescription;
/// <summary>
/// 返回状态说明
/// </summary>
public string StatusDescription
{
get { return _StatusDescription; }
set { _StatusDescription = value; }
}
private HttpStatusCode _StatusCode;
/// <summary>
/// 返回状态码,默认为OK
/// </summary>
public HttpStatusCode StatusCode
{
get { return _StatusCode; }
set { _StatusCode = value; }
}
/// <summary>
/// HttpItem类,用于存储实际执行所用到的HttpItem对象
/// </summary>
public HttpItem item { get; set; }
}
/// <summary>
/// 返回类型
/// </summary>
public enum ResultType
{
/// <summary>
/// 表示只返回字符串 只有Html有数据
/// </summary>
String,
/// <summary>
/// 表示返回字符串和字节流 ResultByte和Html都有数据返回
/// </summary>
Byte
}
/// <summary>
/// Post的数据格式默认为string
/// </summary>
public enum PostDataType
{
/// <summary>
/// 字符串类型,这时编码Encoding可不设置
/// </summary>
String,
/// <summary>
/// Byte类型,需要设置PostdataByte参数的值编码Encoding可设置为空
/// </summary>
Byte,
/// <summary>
/// 传文件,Postdata必须设置为文件的绝对路径,必须设置Encoding的值
/// </summary>
FilePath
}
/// <summary>
/// Cookie返回类型
/// </summary>
public enum ResultCookieType
{
/// <summary>
/// 只返回字符串类型的Cookie
/// </summary>
String,
/// <summary>
/// CookieCollection格式的Cookie集合同时也返回String类型的cookie
/// </summary>
CookieCollection
}
public enum MethodType
{
Get,
Post
}
}
+145
View File
@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Resources;
namespace WxOrder.Framework.Helpers;
/// <summary>
/// WPF 资源读取帮助类
/// 支持 Resource、Embedded Resource、Content 三种方式
/// </summary>
public static class ResourceHelper
{
/// <summary>
/// 从 WPF 应用程序资源中读取文本内容(Build Action = Resource)
/// </summary>
/// <param name="relativePath">相对路径,如 "Ctrls/PDDInvoice/Resources/invoice.js"</param>
/// <param name="assemblyName">程序集名称,可选,默认使用当前程序集</param>
/// <returns>文件内容字符串</returns>
public static string GetResourceText(string relativePath, string assemblyName = null)
{
try
{
// 构建 pack URI
string uriString;
if (string.IsNullOrEmpty(assemblyName))
{
// 当前程序集
uriString = $"pack://application:,,,/{relativePath}";
}
else
{
// 指定程序集
uriString = $"pack://application:,,,/{assemblyName};component/{relativePath}";
}
var uri = new Uri(uriString, UriKind.Absolute);
StreamResourceInfo resourceInfo = Application.GetResourceStream(uri);
if (resourceInfo == null)
{
throw new FileNotFoundException($"资源未找到: {relativePath}");
}
using (var reader = new StreamReader(resourceInfo.Stream))
{
return reader.ReadToEnd();
}
}
catch (Exception ex)
{
return "";
}
}
/// <summary>
/// 从嵌入资源中读取文本内容(Build Action = Embedded Resource)
/// </summary>
/// <param name="resourceName">完整资源名称,如 "YourNamespace.Ctrls.PDDInvoice.Resources.invoice.js"</param>
/// <param name="assembly">程序集,默认使用当前程序集</param>
/// <returns>文件内容字符串</returns>
public static string GetEmbeddedResourceText(string resourceName, Assembly assembly = null)
{
try
{
assembly ??= Assembly.GetExecutingAssembly();
using (var stream = assembly.GetManifestResourceStream(resourceName))
{
if (stream == null)
{
throw new FileNotFoundException($"嵌入资源未找到: {resourceName}");
}
using (var reader = new StreamReader(stream))
{
return reader.ReadToEnd();
}
}
}
catch (Exception ex)
{
throw new InvalidOperationException($"读取嵌入资源失败: {resourceName}", ex);
}
}
/// <summary>
/// 从嵌入资源中读取文本内容,自动查找资源名称(支持部分匹配)
/// </summary>
/// <param name="fileName">文件名,如 "invoice.js"</param>
/// <param name="assembly">程序集,默认使用当前程序集</param>
/// <returns>文件内容字符串</returns>
public static string GetEmbeddedResourceTextByFileName(string fileName, Assembly assembly = null)
{
assembly ??= Assembly.GetExecutingAssembly();
// 查找匹配的资源名称
var resourceNames = assembly.GetManifestResourceNames();
var matchedName = Array.Find(resourceNames, name => name.EndsWith(fileName));
if (string.IsNullOrEmpty(matchedName))
{
throw new FileNotFoundException($"未找到名为 {fileName} 的嵌入资源");
}
return GetEmbeddedResourceText(matchedName, assembly);
}
/// <summary>
/// 从文件系统读取文本内容(Build Action = Content)
/// </summary>
/// <param name="filePath">文件物理路径或相对路径</param>
/// <returns>文件内容字符串</returns>
public static string GetFileText(string filePath)
{
try
{
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"文件未找到: {filePath}");
}
return File.ReadAllText(filePath, System.Text.Encoding.UTF8);
}
catch (Exception ex)
{
throw new InvalidOperationException($"读取文件失败: {filePath}", ex);
}
}
/// <summary>
/// 读取 JS 资源(专门针对 JS 文件的便捷方法)
/// </summary>
/// <param name="relativePath">JS 文件相对路径</param>
/// <returns>JS 文件内容</returns>
public static string GetJavaScriptResource(string relativePath)
{
return GetResourceText(relativePath);
}
}
+249
View File
@@ -0,0 +1,249 @@
using System;
using System.Net;
using System.Text;
namespace WxOrder.Framework.Helpers
{
/// <summary>
/// 拼多多API请求类
/// </summary>
public class PddApi
{
private const string BASE_URL = "https://mobile.yangkeduo.com";
/// <summary>
/// 获取发票详情页
/// </summary>
/// <param name="orderSn">订单号</param>
/// <param name="cookie">用户Cookie</param>
/// <returns>返回页面HTML内容</returns>
public static string GetReturnCompensation(string orderSn, string cookie)
{
string url = $"{BASE_URL}/transac_return_compensation.html?_t_timestamp=transac_modify_invoice&order_sn={orderSn}";
var item = new HttpItem
{
URL = url,
Method = MethodType.Get,
Timeout = 30000,
ReadWriteTimeout = 30000,
KeepAlive = true,
Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
ContentType = "text/html",
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
Cookie = cookie,
Referer = BASE_URL,
Allowautoredirect = true,
Encoding = Encoding.UTF8,
ResultType = ResultType.String
};
// 添加自定义Header
item.Header.Add("sec-ch-ua", "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\"");
item.Header.Add("sec-ch-ua-mobile", "?0");
item.Header.Add("sec-ch-ua-platform", "\"Windows\"");
item.Header.Add("upgrade-insecure-requests", "1");
item.Header.Add("sec-fetch-site", "none");
item.Header.Add("sec-fetch-mode", "navigate");
item.Header.Add("sec-fetch-user", "?1");
item.Header.Add("sec-fetch-dest", "document");
item.Header.Add("priority", "u=0, i");
item.ProxyIp = "127.0.0.1:8080";
var result = new HttpHelper().GetHtml(item);
if (result.StatusCode != HttpStatusCode.OK)
{
throw new Exception($"请求失败: {result.StatusDescription}, 状态码: {result.StatusCode}");
}
return result.Html;
}
/// <summary>
/// 异步获取退货补偿页面信息
/// </summary>
/// <param name="orderSn">订单号</param>
/// <param name="cookie">用户Cookie</param>
/// <returns>返回页面HTML内容</returns>
public static async System.Threading.Tasks.Task<string> GetReturnCompensationAsync(string orderSn, string cookie)
{
return await System.Threading.Tasks.Task.Run(() => GetReturnCompensation(orderSn, cookie));
}
/// <summary>
/// 获取退货补偿页面信息(带完整Cookie字符串)
/// </summary>
/// <param name="orderSn">订单号</param>
/// <param name="accessToken">PDD访问令牌</param>
/// <param name="userId">用户ID</param>
/// <param name="userUin">用户UIN</param>
/// <param name="extraCookies">额外Cookie键值对</param>
/// <returns>返回页面HTML内容</returns>
public static string GetReturnCompensationWithCookies(
string orderSn,
string accessToken,
string userId,
string userUin,
System.Collections.Generic.Dictionary<string, string> extraCookies = null)
{
// 构建Cookie字符串
var cookieBuilder = new StringBuilder();
cookieBuilder.Append($"PDDAccessToken={accessToken}; ");
cookieBuilder.Append($"pdd_user_id={userId}; ");
cookieBuilder.Append($"pdd_user_uin={userUin}; ");
// 添加额外Cookie
if (extraCookies != null)
{
foreach (var kv in extraCookies)
{
cookieBuilder.Append($"{kv.Key}={kv.Value}; ");
}
}
// 添加一些常用的固定Cookie
cookieBuilder.Append("api_uid=CiNLVGqFh3IfyACAene3Ag==; ");
cookieBuilder.Append("webp=1; ");
cookieBuilder.Append("dilx=3EDqgJtFtp4E9OYvXLozM; ");
string cookie = cookieBuilder.ToString().TrimEnd(' ', ';');
return GetReturnCompensation(orderSn, cookie);
}
/// <summary>
/// 构建完整的拼多多请求Cookie
/// </summary>
/// <param name="cookies">Cookie键值对</param>
/// <returns>Cookie字符串</returns>
public static string BuildCookieString(System.Collections.Generic.Dictionary<string, string> cookies)
{
if (cookies == null || cookies.Count == 0)
return string.Empty;
var builder = new StringBuilder();
foreach (var kv in cookies)
{
builder.Append($"{kv.Key}={kv.Value}; ");
}
return builder.ToString().TrimEnd(' ', ';');
}
/// <summary>
/// 通用拼多多请求方法
/// </summary>
/// <param name="path">请求路径</param>
/// <param name="queryParams">查询参数</param>
/// <param name="cookie">Cookie字符串</param>
/// <param name="method">请求方法</param>
/// <param name="postData">POST数据</param>
/// <returns>返回结果</returns>
public static string Request(
string path,
System.Collections.Generic.Dictionary<string, string> queryParams = null,
string cookie = null,
MethodType method = MethodType.Get,
string postData = null)
{
// 构建完整URL
var urlBuilder = new StringBuilder(BASE_URL);
urlBuilder.Append(path);
if (queryParams != null && queryParams.Count > 0)
{
urlBuilder.Append('?');
int index = 0;
foreach (var kv in queryParams)
{
if (index > 0) urlBuilder.Append('&');
urlBuilder.Append($"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}");
index++;
}
}
var item = new HttpItem
{
URL = urlBuilder.ToString(),
Method = method,
Timeout = 30000,
ReadWriteTimeout = 30000,
KeepAlive = true,
Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
ContentType = "application/x-www-form-urlencoded",
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
Cookie = cookie,
Referer = BASE_URL,
Allowautoredirect = true,
Encoding = Encoding.UTF8,
ResultType = ResultType.String,
Postdata = postData
};
// 添加安全相关Header
item.Header.Add("sec-ch-ua", "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\"");
item.Header.Add("sec-ch-ua-mobile", "?0");
item.Header.Add("sec-ch-ua-platform", "\"Windows\"");
item.Header.Add("upgrade-insecure-requests", "1");
item.Header.Add("sec-fetch-site", "none");
item.Header.Add("sec-fetch-mode", "navigate");
item.Header.Add("sec-fetch-dest", "document");
var result = new HttpHelper().GetHtml(item);
if (result.StatusCode != HttpStatusCode.OK)
{
throw new Exception($"请求失败: {result.StatusDescription}, 状态码: {result.StatusCode}");
}
return result.Html;
}
}
/// <summary>
/// 拼多多Cookie构建帮助类
/// </summary>
public static class PddCookieHelper
{
/// <summary>
/// 从各个Cookie片段构建完整Cookie
/// </summary>
public static string BuildCookie(PddCookieInfo info)
{
var builder = new StringBuilder();
if (!string.IsNullOrEmpty(info.ApiUid))
builder.Append($"api_uid={info.ApiUid}; ");
if (!string.IsNullOrEmpty(info.NanoFp))
builder.Append($"_nano_fp={info.NanoFp}; ");
if (!string.IsNullOrEmpty(info.AccessToken))
builder.Append($"PDDAccessToken={info.AccessToken}; ");
if (!string.IsNullOrEmpty(info.UserId))
builder.Append($"pdd_user_id={info.UserId}; ");
if (!string.IsNullOrEmpty(info.UserUin))
builder.Append($"pdd_user_uin={info.UserUin}; ");
if (!string.IsNullOrEmpty(info.PddVds))
builder.Append($"pdd_vds={info.PddVds}; ");
// 添加固定值
builder.Append("webp=1; ");
builder.Append("dilx=3EDqgJtFtp4E9OYvXLozM; ");
return builder.ToString().TrimEnd(' ', ';');
}
}
/// <summary>
/// 拼多多Cookie信息类
/// </summary>
public class PddCookieInfo
{
public string ApiUid { get; set; } = "CiNLVGqFh3IfyACAene3Ag==";
public string NanoFp { get; set; } = "XpmJnqgqnpCynqEYnC_vqOqYD6xToHisZXFzP~Qy";
public string AccessToken { get; set; }
public string UserId { get; set; }
public string UserUin { get; set; }
public string PddVds { get; set; }
public string Jrpl { get; set; } = "m4Tc1seqEgg0YoYo7U4GeUxzylfpG8W7";
public string Njrpl { get; set; } = "m4Tc1seqEgg0YoYo7U4GeUxzylfpG8W7";
}
}
@@ -16,6 +16,7 @@
>
<sys:String x:Key="txt-fapiao">&#xed35;</sys:String>
<sys:String x:Key="txt-calendar_work_week">&#xe850;</sys:String>
<sys:String x:Key="txt-document_endnote">&#xe950;</sys:String>
<sys:String x:Key="txt-leaf_three">&#xea50;</sys:String>
@@ -1522,6 +1523,9 @@
<sys:String x:Key="txt-task_list_square_ltr">&#xec4f;</sys:String>
<Geometry x:Key="icon-fapiao" PresentationOptions:Freeze="True">
M170.666667 868.693333l98.346666-94.72a21.333333 21.333333 0 0 1 30.72 1.173334l98.176 110.122666 98.176-110.122666a21.333333 21.333333 0 0 1 31.829334 0l98.176 110.122666 98.176-110.122666a21.333333 21.333333 0 0 1 30.72-1.173334L853.333333 868.693333V128H170.666667v740.693333z m-42.666667 49.386667V106.709333C128 94.890667 137.706667 85.333333 149.333333 85.333333h725.333334c11.776 0 21.333333 9.642667 21.333333 21.184v811.541334a21.333333 21.333333 0 0 1-36.117333 16.149333l-118.506667-114.133333-99.349333 111.445333a21.333333 21.333333 0 0 1-31.850667 0L512 821.397333l-98.176 110.122667a21.333333 21.333333 0 0 1-31.850667 0l-99.349333-111.445333-118.485333 114.133333A21.333333 21.333333 0 0 1 128 918.08zM298.666667 320c0-11.776 9.493333-21.333333 21.184-21.333333h341.632c11.690667 0 21.184 9.472 21.184 21.333333 0 11.776-9.493333 21.333333-21.184 21.333333H319.850667A21.205333 21.205333 0 0 1 298.666667 320z m0 128c0-11.776 9.6-21.333333 21.482666-21.333333h255.701334c11.861333 0 21.482667 9.472 21.482666 21.333333 0 11.776-9.6 21.333333-21.482666 21.333333H320.149333A21.354667 21.354667 0 0 1 298.666667 448z m0 128c0-11.776 9.6-21.333333 21.482666-21.333333h255.701334c11.861333 0 21.482667 9.472 21.482666 21.333333 0 11.776-9.6 21.333333-21.482666 21.333333H320.149333A21.354667 21.354667 0 0 1 298.666667 576z
</Geometry>
<Geometry x:Key="icon-calendar_work_week" PresentationOptions:Freeze="True">
M330.666667 298.666667a32 32 0 0 0-32 32v170.666666c0 17.664 14.336 32 32 32h362.666666a32 32 0 0 0 32-32v-170.666666a32 32 0 0 0-32-32h-362.666666z m32 170.666666V362.666667h298.666666V469.333333h-298.666666z
M757.333333 896A138.666667 138.666667 0 0 0 896 757.333333V266.666667A138.666667 138.666667 0 0 0 757.333333 128H266.666667A138.666667 138.666667 0 0 0 128 266.666667v490.666666A138.666667 138.666667 0 0 0 266.666667 896h490.666666zM832 266.666667v490.666666a74.666667 74.666667 0 0 1-74.666667 74.666667H266.666667a74.666667 74.666667 0 0 1-74.666667-74.666667V266.666667c0-41.216 33.450667-74.666667 74.666667-74.666667h490.666666c41.216 0 74.666667 33.450667 74.666667 74.666667z
@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 5223831 */
src: url('iconfont.woff2?t=1786935726171') format('woff2'),
url('iconfont.woff?t=1786935726171') format('woff'),
url('iconfont.ttf?t=1786935726171') format('truetype');
src: url('iconfont.woff2?t=1787127285848') format('woff2'),
url('iconfont.woff?t=1787127285848') format('woff'),
url('iconfont.ttf?t=1787127285848') format('truetype');
}
.iconfont {
@@ -13,6 +13,10 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-fapiao:before {
content: "\ed35";
}
.icon-calendar_work_week:before {
content: "\e850";
}
@@ -1 +1 @@
window._iconfont_svg_string_5223831='<svg><symbol id="icon-calendar_work_week" viewBox="0 0 1024 1024"><path d="M330.666667 298.666667a32 32 0 0 0-32 32v170.666666c0 17.664 14.336 32 32 32h362.666666a32 32 0 0 0 32-32v-170.666666a32 32 0 0 0-32-32h-362.666666z m32 170.666666V362.666667h298.666666V469.333333h-298.666666z" ></path><path d="M757.333333 896A138.666667 138.666667 0 0 0 896 757.333333V266.666667A138.666667 138.666667 0 0 0 757.333333 128H266.666667A138.666667 138.666667 0 0 0 128 266.666667v490.666666A138.666667 138.666667 0 0 0 266.666667 896h490.666666zM832 266.666667v490.666666a74.666667 74.666667 0 0 1-74.666667 74.666667H266.666667a74.666667 74.666667 0 0 1-74.666667-74.666667V266.666667c0-41.216 33.450667-74.666667 74.666667-74.666667h490.666666c41.216 0 74.666667 33.450667 74.666667 74.666667z" ></path></symbol><symbol id="icon-document_endnote" viewBox="0 0 1024 1024"><path d="M234.666667 181.333333a32 32 0 0 1 32-32H512V341.333333a85.333333 85.333333 0 0 0 85.333333 85.333334h192v416a32 32 0 0 1-32 32H554.666667V938.666667h202.666666A96 96 0 0 0 853.333333 842.666667V412.330667c0-19.797333-7.850667-38.784-21.888-52.778667l-252.330666-252.330667A74.666667 74.666667 0 0 0 526.336 85.333333H266.666667A96 96 0 0 0 170.666667 181.333333V512h64V181.333333z m341.333333 160V194.56l168.106667 168.106667H597.333333a21.333333 21.333333 0 0 1-21.333333-21.333334zM192 586.666667a32 32 0 0 1-32 32H106.666667v298.666666h53.333333a32 32 0 0 1 0 64H106.666667A64 64 0 0 1 42.666667 917.333333v-298.666666A64 64 0 0 1 106.666667 554.666667h53.333333a32 32 0 0 1 32 32z" ></path><path d="M394.666667 618.666667a32 32 0 0 1 0-64h53.333333a64 64 0 0 1 64 64v298.666666a64 64 0 0 1-64 64H394.666667a32 32 0 0 1 0-64h53.333333v-298.666666H394.666667zM277.333333 714.666667a32 32 0 0 1 32 32V896a32 32 0 0 1-64 0v-149.333333a32 32 0 0 1 32-32zM277.333333 682.666667a32 32 0 1 0 0-64 32 32 0 0 0 0 64z" ></path></symbol><symbol id="icon-leaf_three" viewBox="0 0 1024 1024"><path d="M810.666667 320c0 22.186667-3.072 43.648-8.832 64h69.12C908.373333 384 938.666667 414.293333 938.666667 451.669333V661.333333a277.333333 277.333333 0 0 1-449.493334 217.429334L438.613333 929.28a32 32 0 1 1-45.226666-45.226667l50.517333-50.56a277.077333 277.077333 0 0 1-40.704-70.613333A256 256 0 0 1 96 512V320.597333C96 284.928 124.928 256 160.597333 256h189.568A234.752 234.752 0 0 1 576 85.333333h173.226667c33.92 0 61.44 27.52 61.44 61.44V320zM414.805333 263.765333a256.554667 256.554667 0 0 1 165.504 132.266667A277.290667 277.290667 0 0 1 661.333333 384h72.96c7.978667-19.754667 12.373333-41.386667 12.373334-64v-170.666667h-170.666667a170.752 170.752 0 0 0-161.194667 114.432z m120.021334 569.344A213.333333 213.333333 0 0 0 874.666667 661.333333v-209.664a3.669333 3.669333 0 0 0-3.669334-3.669333H661.333333a213.333333 213.333333 0 0 0-171.776 339.84l181.162667-181.12a32 32 0 1 1 45.226667 45.226667l-181.12 181.162666zM384 661.333333c0-25.258667 3.413333-49.749333 9.728-73.002666L276.053333 470.613333a32 32 0 1 1 45.226667-45.226666l98.901333 98.858666a278.698667 278.698667 0 0 1 101.333334-102.485333A192 192 0 0 0 352 320H160.384l-0.213333 0.170667a0.938667 0.938667 0 0 0-0.128 0.213333l-0.042667 0.213333V512a192 192 0 0 0 226.816 188.842667A279.765333 279.765333 0 0 1 384 661.333333z" ></path></symbol><symbol id="icon-re_order_dots_horizontal" viewBox="0 0 1024 1024"><path d="M298.666667 661.333333a64 64 0 1 1-128 0 64 64 0 0 1 128 0z m0-298.666666a64 64 0 1 1-128 0 64 64 0 0 1 128 0z m298.666666 298.666666a64 64 0 1 1-128 0 64 64 0 0 1 128 0z m0-298.666666a64 64 0 1 1-128 0 64 64 0 0 1 128 0z m298.666667 298.666666a64 64 0 1 1-128 0 64 64 0 0 1 128 0z m0-298.666666a64 64 0 1 1-128 0 64 64 0 0 1 128 0z" ></path></symbol><symbol id="icon-task_list_square_rtl" viewBox="0 0 1024 1024"><path d="M277.333333 629.333333a32 32 0 0 1 32-32h149.333334a32 32 0 0 1 0 64h-149.333334a32 32 0 0 1-32-32z m32-266.666666a32 32 0 0 0 0 64h149.333334a32 32 0 0 0 0-64h-149.333334z m449.28-33.28a32 32 0 0 1 0 45.226666l-85.333333 85.333334a32 32 0 0 1-45.226667 0l-42.666666-42.666667a32 32 0 0 1 45.226666-45.226667l20.053334 20.053334 62.72-62.72a32 32 0 0 1 45.226666 0z m0 279.893333a32 32 0 1 0-45.226666-45.226667l-62.72 62.72-20.053334-20.053333a32 32 0 1 0-45.226666 45.226667l42.666666 42.666666a32 32 0 0 0 45.226667 0l85.333333-85.333333zM224 128A96 96 0 0 0 128 224v576A96 96 0 0 0 224 896h576A96 96 0 0 0 896 800V224A96 96 0 0 0 800 128H224zM192 224a32 32 0 0 1 32-32h576a32 32 0 0 1 32 32v576a32 32 0 0 1-32 32H224a32 32 0 0 1-32-32V224z" ></path></symbol><symbol id="icon-calendar_sync" viewBox="0 0 1024 1024"><path d="M757.333333 128A138.666667 138.666667 0 0 1 896 266.666667v246.272a276.096 276.096 0 0 0-64-30.250667V362.666667h-640v394.666666c0 41.216 33.450667 74.666667 74.666667 74.666667h216.021333c7.381333 22.784 17.578667 44.245333 30.250667 64H266.666667A138.666667 138.666667 0 0 1 128 757.333333V266.666667A138.666667 138.666667 0 0 1 266.666667 1Line truncated
window._iconfont_svg_string_5223831='<svg><symbol id="icon-fapiao" viewBox="0 0 1024 1024"><path d="M170.666667 868.693333l98.346666-94.72a21.333333 21.333333 0 0 1 30.72 1.173334l98.176 110.122666 98.176-110.122666a21.333333 21.333333 0 0 1 31.829334 0l98.176 110.122666 98.176-110.122666a21.333333 21.333333 0 0 1 30.72-1.173334L853.333333 868.693333V128H170.666667v740.693333z m-42.666667 49.386667V106.709333C128 94.890667 137.706667 85.333333 149.333333 85.333333h725.333334c11.776 0 21.333333 9.642667 21.333333 21.184v811.541334a21.333333 21.333333 0 0 1-36.117333 16.149333l-118.506667-114.133333-99.349333 111.445333a21.333333 21.333333 0 0 1-31.850667 0L512 821.397333l-98.176 110.122667a21.333333 21.333333 0 0 1-31.850667 0l-99.349333-111.445333-118.485333 114.133333A21.333333 21.333333 0 0 1 128 918.08zM298.666667 320c0-11.776 9.493333-21.333333 21.184-21.333333h341.632c11.690667 0 21.184 9.472 21.184 21.333333 0 11.776-9.493333 21.333333-21.184 21.333333H319.850667A21.205333 21.205333 0 0 1 298.666667 320z m0 128c0-11.776 9.6-21.333333 21.482666-21.333333h255.701334c11.861333 0 21.482667 9.472 21.482666 21.333333 0 11.776-9.6 21.333333-21.482666 21.333333H320.149333A21.354667 21.354667 0 0 1 298.666667 448z m0 128c0-11.776 9.6-21.333333 21.482666-21.333333h255.701334c11.861333 0 21.482667 9.472 21.482666 21.333333 0 11.776-9.6 21.333333-21.482666 21.333333H320.149333A21.354667 21.354667 0 0 1 298.666667 576z" fill="#3D3D3D" ></path></symbol><symbol id="icon-calendar_work_week" viewBox="0 0 1024 1024"><path d="M330.666667 298.666667a32 32 0 0 0-32 32v170.666666c0 17.664 14.336 32 32 32h362.666666a32 32 0 0 0 32-32v-170.666666a32 32 0 0 0-32-32h-362.666666z m32 170.666666V362.666667h298.666666V469.333333h-298.666666z" ></path><path d="M757.333333 896A138.666667 138.666667 0 0 0 896 757.333333V266.666667A138.666667 138.666667 0 0 0 757.333333 128H266.666667A138.666667 138.666667 0 0 0 128 266.666667v490.666666A138.666667 138.666667 0 0 0 266.666667 896h490.666666zM832 266.666667v490.666666a74.666667 74.666667 0 0 1-74.666667 74.666667H266.666667a74.666667 74.666667 0 0 1-74.666667-74.666667V266.666667c0-41.216 33.450667-74.666667 74.666667-74.666667h490.666666c41.216 0 74.666667 33.450667 74.666667 74.666667z" ></path></symbol><symbol id="icon-document_endnote" viewBox="0 0 1024 1024"><path d="M234.666667 181.333333a32 32 0 0 1 32-32H512V341.333333a85.333333 85.333333 0 0 0 85.333333 85.333334h192v416a32 32 0 0 1-32 32H554.666667V938.666667h202.666666A96 96 0 0 0 853.333333 842.666667V412.330667c0-19.797333-7.850667-38.784-21.888-52.778667l-252.330666-252.330667A74.666667 74.666667 0 0 0 526.336 85.333333H266.666667A96 96 0 0 0 170.666667 181.333333V512h64V181.333333z m341.333333 160V194.56l168.106667 168.106667H597.333333a21.333333 21.333333 0 0 1-21.333333-21.333334zM192 586.666667a32 32 0 0 1-32 32H106.666667v298.666666h53.333333a32 32 0 0 1 0 64H106.666667A64 64 0 0 1 42.666667 917.333333v-298.666666A64 64 0 0 1 106.666667 554.666667h53.333333a32 32 0 0 1 32 32z" ></path><path d="M394.666667 618.666667a32 32 0 0 1 0-64h53.333333a64 64 0 0 1 64 64v298.666666a64 64 0 0 1-64 64H394.666667a32 32 0 0 1 0-64h53.333333v-298.666666H394.666667zM277.333333 714.666667a32 32 0 0 1 32 32V896a32 32 0 0 1-64 0v-149.333333a32 32 0 0 1 32-32zM277.333333 682.666667a32 32 0 1 0 0-64 32 32 0 0 0 0 64z" ></path></symbol><symbol id="icon-leaf_three" viewBox="0 0 1024 1024"><path d="M810.666667 320c0 22.186667-3.072 43.648-8.832 64h69.12C908.373333 384 938.666667 414.293333 938.666667 451.669333V661.333333a277.333333 277.333333 0 0 1-449.493334 217.429334L438.613333 929.28a32 32 0 1 1-45.226666-45.226667l50.517333-50.56a277.077333 277.077333 0 0 1-40.704-70.613333A256 256 0 0 1 96 512V320.597333C96 284.928 124.928 256 160.597333 256h189.568A234.752 234.752 0 0 1 576 85.333333h173.226667c33.92 0 61.44 27.52 61.44 61.44V320zM414.805333 263.765333a256.554667 256.554667 0 0 1 165.504 132.266667A277.290667 277.290667 0 0 1 661.333333 384h72.96c7.978667-19.754667 12.373333-41.386667 12.373334-64v-170.666667h-170.666667a170.752 170.752 0 0 0-161.194667 114.432z m120.021334 569.344A213.333333 213.333333 0 0 0 874.666667 661.333333v-209.664a3.669333 3.669333 0 0 0-3.669334-3.669333H661.333333a213.333333 213.333333 0 0 0-171.776 339.84l181.162667-181.12a32 32 0 1 1 45.226667 45.226667l-181.12 181.162666zM384 661.333333c0-25.258667 3.413333-49.749333 9.728-73.002666L276.053333 470.613333a32 32 0 1 1 45.226667-45.226666l98.901333 98.858666a278.698667 278.698667 0 0 1 101.333334-102.485333A192 192 0 0 0 352 320H160.384l-0.213333 0.170667a0.938667 0.938667 0 0 0-0.128 0.213333l-0.042667 0.213333V512a192 192 0 0 0 226.816 188.842667A279.765333 279.765333 0 0 1 384 661.333333z" ></path></symbol><symbol id="icon-re_order_dots_horizontal" viewBox="0 0 1024 1024"><path d="M298.666667 661.333333a64 64 0 1 1-128 0 64 64 0 0 1 128 0z m0-298.666666a64 64 0 1 1-128 0 64 64 0 0 1 128 0z m298.666666 298.666666a64 64 0 1 1-128 0 64 64 0 0 1 128 Line truncated
@@ -5,6 +5,13 @@
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "5068538",
"name": "发票",
"font_class": "fapiao",
"unicode": "ed35",
"unicode_decimal": 60725
},
{
"icon_id": "44033866",
"name": "calendar_work_week",
Binary file not shown.
+15 -1
View File
@@ -6,7 +6,9 @@
<ImplicitUsings>true</ImplicitUsings>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
</ItemGroup>
<ItemGroup>
<None Remove="Resources\Fonts\iconfont.ttf" />
</ItemGroup>
@@ -31,4 +33,16 @@
</None>
</ItemGroup>
<ItemGroup>
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
</ItemGroup>
<ItemGroup>
<Page Update="Resources\Fonts\FontGeometry.xaml">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>FontGeometry.tt</DependentUpon>
</Page>
</ItemGroup>
</Project>