source
This commit is contained in:
19
PackagingMallShipper.sln
Normal file
19
PackagingMallShipper.sln
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.30114.105
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PackagingMallShipper", "PackagingMallShipper\PackagingMallShipper.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
18
PackagingMallShipper/App.config
Normal file
18
PackagingMallShipper/App.config
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||
</startup>
|
||||
<appSettings>
|
||||
<!-- API工厂配置 -->
|
||||
<add key="ApiBaseUrl" value="https://user.api.it120.cc" />
|
||||
<add key="SubDomain" value="vv125s" />
|
||||
|
||||
<!-- 同步配置 -->
|
||||
<add key="SyncPageSize" value="50" />
|
||||
<add key="ShipConcurrency" value="3" />
|
||||
|
||||
<!-- Token有效期(小时) -->
|
||||
<add key="TokenExpireHours" value="24" />
|
||||
</appSettings>
|
||||
</configuration>
|
||||
13
PackagingMallShipper/App.xaml
Normal file
13
PackagingMallShipper/App.xaml
Normal file
@@ -0,0 +1,13 @@
|
||||
<Application x:Class="PackagingMallShipper.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:PackagingMallShipper"
|
||||
StartupUri="Views/LoginWindow.xaml">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="Resources/Styles.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
25
PackagingMallShipper/App.xaml.cs
Normal file
25
PackagingMallShipper/App.xaml.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using PackagingMallShipper.Data;
|
||||
|
||||
namespace PackagingMallShipper
|
||||
{
|
||||
public partial class App : Application
|
||||
{
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
base.OnStartup(e);
|
||||
|
||||
try
|
||||
{
|
||||
SqliteHelper.Initialize();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"数据库初始化失败: {ex.Message}", "错误",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
Shutdown(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
PackagingMallShipper/Converters/BoolToVisibilityConverter.cs
Normal file
66
PackagingMallShipper/Converters/BoolToVisibilityConverter.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace PackagingMallShipper.Converters
|
||||
{
|
||||
public class BoolToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is bool boolValue)
|
||||
{
|
||||
return boolValue ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is Visibility visibility)
|
||||
{
|
||||
return visibility == Visibility.Visible;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public class InverseBoolConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is bool boolValue)
|
||||
{
|
||||
return !boolValue;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is bool boolValue)
|
||||
{
|
||||
return !boolValue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public class StringToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is string strValue)
|
||||
{
|
||||
return string.IsNullOrEmpty(strValue) ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
33
PackagingMallShipper/Converters/StatusToColorConverter.cs
Normal file
33
PackagingMallShipper/Converters/StatusToColorConverter.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace PackagingMallShipper.Converters
|
||||
{
|
||||
public class StatusToColorConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is int status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case -1: return new SolidColorBrush(Color.FromRgb(0x99, 0x99, 0x99)); // 已关闭 - 灰色
|
||||
case 0: return new SolidColorBrush(Color.FromRgb(0xFF, 0x4D, 0x4F)); // 待支付 - 红色
|
||||
case 1: return new SolidColorBrush(Color.FromRgb(0xFA, 0x8C, 0x16)); // 待发货 - 橙色
|
||||
case 2: return new SolidColorBrush(Color.FromRgb(0x18, 0x90, 0xFF)); // 待收货 - 蓝色
|
||||
case 3: return new SolidColorBrush(Color.FromRgb(0x72, 0x2E, 0xD1)); // 待评价 - 紫色
|
||||
case 4: return new SolidColorBrush(Color.FromRgb(0x52, 0xC4, 0x1A)); // 已完成 - 绿色
|
||||
default: return new SolidColorBrush(Colors.Black);
|
||||
}
|
||||
}
|
||||
return new SolidColorBrush(Colors.Black);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
24
PackagingMallShipper/Converters/StatusToTextConverter.cs
Normal file
24
PackagingMallShipper/Converters/StatusToTextConverter.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using PackagingMallShipper.Models;
|
||||
|
||||
namespace PackagingMallShipper.Converters
|
||||
{
|
||||
public class StatusToTextConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is int status)
|
||||
{
|
||||
return OrderStatus.GetStatusText(status);
|
||||
}
|
||||
return "未知";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
94
PackagingMallShipper/Data/Resources/schema.sql
Normal file
94
PackagingMallShipper/Data/Resources/schema.sql
Normal file
@@ -0,0 +1,94 @@
|
||||
-- 包装商城发货助手 - SQLite数据库初始化脚本
|
||||
|
||||
-- 1. 本地用户会话
|
||||
CREATE TABLE IF NOT EXISTS local_session (
|
||||
id INTEGER PRIMARY KEY,
|
||||
mobile TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
uid INTEGER,
|
||||
nickname TEXT,
|
||||
enterprise_id INTEGER,
|
||||
last_login_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
token_expires_at DATETIME
|
||||
);
|
||||
|
||||
-- 2. 订单缓存表
|
||||
CREATE TABLE IF NOT EXISTS orders_cache (
|
||||
id INTEGER PRIMARY KEY,
|
||||
order_number TEXT NOT NULL UNIQUE,
|
||||
status INTEGER NOT NULL,
|
||||
amount REAL,
|
||||
amount_real REAL,
|
||||
|
||||
uid INTEGER,
|
||||
user_mobile TEXT,
|
||||
|
||||
logistics_name TEXT,
|
||||
logistics_mobile TEXT,
|
||||
logistics_province TEXT,
|
||||
logistics_city TEXT,
|
||||
logistics_district TEXT,
|
||||
logistics_address TEXT,
|
||||
|
||||
goods_json TEXT,
|
||||
|
||||
express_company_id INTEGER,
|
||||
express_company_name TEXT,
|
||||
tracking_number TEXT,
|
||||
date_ship DATETIME,
|
||||
|
||||
sync_status TEXT DEFAULT 'synced',
|
||||
local_updated_at DATETIME,
|
||||
|
||||
date_add DATETIME,
|
||||
date_pay DATETIME,
|
||||
date_update DATETIME,
|
||||
synced_at DATETIME
|
||||
);
|
||||
|
||||
-- 3. 发货队列表
|
||||
CREATE TABLE IF NOT EXISTS ship_queue (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_id INTEGER NOT NULL,
|
||||
order_number TEXT NOT NULL,
|
||||
express_company_id INTEGER NOT NULL,
|
||||
tracking_number TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'pending',
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
error_message TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME
|
||||
);
|
||||
|
||||
-- 4. 同步日志表
|
||||
CREATE TABLE IF NOT EXISTS sync_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sync_type TEXT NOT NULL,
|
||||
sync_mode TEXT,
|
||||
sync_start DATETIME NOT NULL,
|
||||
sync_end DATETIME,
|
||||
total_count INTEGER DEFAULT 0,
|
||||
new_count INTEGER DEFAULT 0,
|
||||
updated_count INTEGER DEFAULT 0,
|
||||
failed_count INTEGER DEFAULT 0,
|
||||
status TEXT NOT NULL,
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
-- 5. 操作日志表
|
||||
CREATE TABLE IF NOT EXISTS operation_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
operation_type TEXT NOT NULL,
|
||||
target_id INTEGER,
|
||||
target_number TEXT,
|
||||
details TEXT,
|
||||
result TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX IF NOT EXISTS idx_orders_status ON orders_cache(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_orders_date_add ON orders_cache(date_add);
|
||||
CREATE INDEX IF NOT EXISTS idx_orders_order_number ON orders_cache(order_number);
|
||||
CREATE INDEX IF NOT EXISTS idx_ship_queue_status ON ship_queue(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_sync_logs_status ON sync_logs(status);
|
||||
118
PackagingMallShipper/Data/SqliteHelper.cs
Normal file
118
PackagingMallShipper/Data/SqliteHelper.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.Data.SQLite;
|
||||
using System.IO;
|
||||
using PackagingMallShipper.Helpers;
|
||||
|
||||
namespace PackagingMallShipper.Data
|
||||
{
|
||||
public static class SqliteHelper
|
||||
{
|
||||
private static string _connectionString;
|
||||
private static string _dbPath;
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
_dbPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"PackagingMallShipper",
|
||||
"data.db"
|
||||
);
|
||||
|
||||
var dir = Path.GetDirectoryName(_dbPath);
|
||||
if (!Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
_connectionString = $"Data Source={_dbPath};Version=3;";
|
||||
|
||||
if (!File.Exists(_dbPath))
|
||||
{
|
||||
SQLiteConnection.CreateFile(_dbPath);
|
||||
CreateTables();
|
||||
}
|
||||
}
|
||||
|
||||
public static SQLiteConnection GetConnection()
|
||||
{
|
||||
return new SQLiteConnection(_connectionString);
|
||||
}
|
||||
|
||||
public static string DbPath => _dbPath;
|
||||
|
||||
private static void CreateTables()
|
||||
{
|
||||
using (var conn = GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
var sql = ResourceHelper.GetEmbeddedResource("schema.sql");
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ExecuteNonQuery(string sql, params SQLiteParameter[] parameters)
|
||||
{
|
||||
using (var conn = GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
if (parameters != null)
|
||||
cmd.Parameters.AddRange(parameters);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static T ExecuteScalar<T>(string sql, params SQLiteParameter[] parameters)
|
||||
{
|
||||
using (var conn = GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
if (parameters != null)
|
||||
cmd.Parameters.AddRange(parameters);
|
||||
var result = cmd.ExecuteScalar();
|
||||
if (result == null || result == DBNull.Value)
|
||||
return default(T);
|
||||
return (T)Convert.ChangeType(result, typeof(T));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class DataReaderExtensions
|
||||
{
|
||||
public static string GetStringSafe(this SQLiteDataReader reader, string column)
|
||||
{
|
||||
var ordinal = reader.GetOrdinal(column);
|
||||
return reader.IsDBNull(ordinal) ? "" : reader.GetString(ordinal);
|
||||
}
|
||||
|
||||
public static int GetInt32Safe(this SQLiteDataReader reader, string column, int defaultValue = 0)
|
||||
{
|
||||
var ordinal = reader.GetOrdinal(column);
|
||||
return reader.IsDBNull(ordinal) ? defaultValue : reader.GetInt32(ordinal);
|
||||
}
|
||||
|
||||
public static decimal GetDecimalSafe(this SQLiteDataReader reader, string column, decimal defaultValue = 0)
|
||||
{
|
||||
var ordinal = reader.GetOrdinal(column);
|
||||
return reader.IsDBNull(ordinal) ? defaultValue : reader.GetDecimal(ordinal);
|
||||
}
|
||||
|
||||
public static DateTime? GetDateTimeSafe(this SQLiteDataReader reader, string column)
|
||||
{
|
||||
var ordinal = reader.GetOrdinal(column);
|
||||
return reader.IsDBNull(ordinal) ? (DateTime?)null : reader.GetDateTime(ordinal);
|
||||
}
|
||||
|
||||
public static int? GetInt32Nullable(this SQLiteDataReader reader, string column)
|
||||
{
|
||||
var ordinal = reader.GetOrdinal(column);
|
||||
return reader.IsDBNull(ordinal) ? (int?)null : reader.GetInt32(ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
PackagingMallShipper/Helpers/AppConfig.cs
Normal file
27
PackagingMallShipper/Helpers/AppConfig.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Configuration;
|
||||
|
||||
namespace PackagingMallShipper.Helpers
|
||||
{
|
||||
public static class AppConfig
|
||||
{
|
||||
public static string ApiBaseUrl =>
|
||||
ConfigurationManager.AppSettings["ApiBaseUrl"] ?? "https://user.api.it120.cc";
|
||||
|
||||
public static string SubDomain =>
|
||||
ConfigurationManager.AppSettings["SubDomain"] ?? "vv125s";
|
||||
|
||||
public static int SyncPageSize =>
|
||||
int.TryParse(ConfigurationManager.AppSettings["SyncPageSize"], out var size) ? size : 50;
|
||||
|
||||
public static int ShipConcurrency =>
|
||||
int.TryParse(ConfigurationManager.AppSettings["ShipConcurrency"], out var c) ? c : 3;
|
||||
|
||||
public static int TokenExpireHours =>
|
||||
int.TryParse(ConfigurationManager.AppSettings["TokenExpireHours"], out var h) ? h : 24;
|
||||
|
||||
public static string GetApiUrl(string endpoint)
|
||||
{
|
||||
return $"{ApiBaseUrl}/{SubDomain}{endpoint}";
|
||||
}
|
||||
}
|
||||
}
|
||||
57
PackagingMallShipper/Helpers/ExpressCompanies.cs
Normal file
57
PackagingMallShipper/Helpers/ExpressCompanies.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace PackagingMallShipper.Helpers
|
||||
{
|
||||
public static class ExpressCompanies
|
||||
{
|
||||
public static readonly List<ExpressCompany> All = new List<ExpressCompany>
|
||||
{
|
||||
new ExpressCompany(1, "顺丰速运", "SF"),
|
||||
new ExpressCompany(2, "中通快递", "ZTO"),
|
||||
new ExpressCompany(3, "圆通速递", "YTO"),
|
||||
new ExpressCompany(4, "韵达快递", "YD"),
|
||||
new ExpressCompany(5, "申通快递", "STO"),
|
||||
new ExpressCompany(6, "极兔速递", "JTSD"),
|
||||
new ExpressCompany(7, "邮政快递包裹", "YZPY"),
|
||||
new ExpressCompany(8, "EMS", "EMS"),
|
||||
new ExpressCompany(9, "京东快递", "JD"),
|
||||
new ExpressCompany(10, "德邦快递", "DBL"),
|
||||
new ExpressCompany(-1, "其他/自配送", "OTHER")
|
||||
};
|
||||
|
||||
public static string GetName(int id)
|
||||
{
|
||||
return All.FirstOrDefault(e => e.Id == id)?.Name ?? "其他";
|
||||
}
|
||||
|
||||
public static int GetIdByName(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) return -1;
|
||||
var express = All.FirstOrDefault(e =>
|
||||
e.Name.Contains(name) || name.Contains(e.Name));
|
||||
return express?.Id ?? -1;
|
||||
}
|
||||
|
||||
public static ExpressCompany GetById(int id)
|
||||
{
|
||||
return All.FirstOrDefault(e => e.Id == id) ?? All.Last();
|
||||
}
|
||||
}
|
||||
|
||||
public class ExpressCompany
|
||||
{
|
||||
public int Id { get; }
|
||||
public string Name { get; }
|
||||
public string Code { get; }
|
||||
|
||||
public ExpressCompany(int id, string name, string code)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
Code = code;
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
}
|
||||
25
PackagingMallShipper/Helpers/ResourceHelper.cs
Normal file
25
PackagingMallShipper/Helpers/ResourceHelper.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace PackagingMallShipper.Helpers
|
||||
{
|
||||
public static class ResourceHelper
|
||||
{
|
||||
public static string GetEmbeddedResource(string resourceName)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var fullName = $"PackagingMallShipper.Data.Resources.{resourceName}";
|
||||
|
||||
using (var stream = assembly.GetManifestResourceStream(fullName))
|
||||
{
|
||||
if (stream == null)
|
||||
throw new FileNotFoundException($"嵌入资源未找到: {fullName}");
|
||||
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
122
PackagingMallShipper/Models/ApiResponses.cs
Normal file
122
PackagingMallShipper/Models/ApiResponses.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PackagingMallShipper.Models
|
||||
{
|
||||
public class ApiResponse<T>
|
||||
{
|
||||
[JsonProperty("code")]
|
||||
public int Code { get; set; }
|
||||
|
||||
[JsonProperty("msg")]
|
||||
public string Msg { get; set; }
|
||||
|
||||
[JsonProperty("data")]
|
||||
public T Data { get; set; }
|
||||
}
|
||||
|
||||
public class LoginData
|
||||
{
|
||||
[JsonProperty("token")]
|
||||
public string Token { get; set; }
|
||||
|
||||
[JsonProperty("uid")]
|
||||
public int Uid { get; set; }
|
||||
}
|
||||
|
||||
public class UserInfo
|
||||
{
|
||||
[JsonProperty("nick")]
|
||||
public string Nick { get; set; }
|
||||
|
||||
[JsonProperty("mobile")]
|
||||
public string Mobile { get; set; }
|
||||
|
||||
[JsonProperty("avatarUrl")]
|
||||
public string AvatarUrl { get; set; }
|
||||
}
|
||||
|
||||
public class LoginResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Token { get; set; }
|
||||
public string Message { get; set; }
|
||||
public UserInfo UserInfo { get; set; }
|
||||
}
|
||||
|
||||
public class OrderListData
|
||||
{
|
||||
[JsonProperty("orderList")]
|
||||
public List<OrderDto> OrderList { get; set; }
|
||||
|
||||
[JsonProperty("totalRow")]
|
||||
public int TotalRow { get; set; }
|
||||
|
||||
[JsonProperty("totalPage")]
|
||||
public int TotalPage { get; set; }
|
||||
}
|
||||
|
||||
public class OrderDto
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("orderNumber")]
|
||||
public string OrderNumber { get; set; }
|
||||
|
||||
[JsonProperty("status")]
|
||||
public int Status { get; set; }
|
||||
|
||||
[JsonProperty("amount")]
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
[JsonProperty("amountReal")]
|
||||
public decimal AmountReal { get; set; }
|
||||
|
||||
[JsonProperty("uid")]
|
||||
public int Uid { get; set; }
|
||||
|
||||
[JsonProperty("userMobile")]
|
||||
public string UserMobile { get; set; }
|
||||
|
||||
[JsonProperty("linkMan")]
|
||||
public string LogisticsName { get; set; }
|
||||
|
||||
[JsonProperty("mobile")]
|
||||
public string LogisticsMobile { get; set; }
|
||||
|
||||
[JsonProperty("provinceStr")]
|
||||
public string LogisticsProvince { get; set; }
|
||||
|
||||
[JsonProperty("cityStr")]
|
||||
public string LogisticsCity { get; set; }
|
||||
|
||||
[JsonProperty("areaStr")]
|
||||
public string LogisticsDistrict { get; set; }
|
||||
|
||||
[JsonProperty("address")]
|
||||
public string LogisticsAddress { get; set; }
|
||||
|
||||
[JsonProperty("goods")]
|
||||
public List<GoodsItem> Goods { get; set; }
|
||||
|
||||
[JsonProperty("shipperId")]
|
||||
public int? ShipperId { get; set; }
|
||||
|
||||
[JsonProperty("shipperCode")]
|
||||
public string ShipperCode { get; set; }
|
||||
|
||||
[JsonProperty("dateShip")]
|
||||
public DateTime? DateShip { get; set; }
|
||||
|
||||
[JsonProperty("dateAdd")]
|
||||
public DateTime DateAdd { get; set; }
|
||||
|
||||
[JsonProperty("datePay")]
|
||||
public DateTime? DatePay { get; set; }
|
||||
|
||||
[JsonProperty("dateUpdate")]
|
||||
public DateTime DateUpdate { get; set; }
|
||||
}
|
||||
}
|
||||
16
PackagingMallShipper/Models/LocalSession.cs
Normal file
16
PackagingMallShipper/Models/LocalSession.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
|
||||
namespace PackagingMallShipper.Models
|
||||
{
|
||||
public class LocalSession
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Mobile { get; set; }
|
||||
public string Token { get; set; }
|
||||
public int Uid { get; set; }
|
||||
public string Nickname { get; set; }
|
||||
public int? EnterpriseId { get; set; }
|
||||
public DateTime LastLoginAt { get; set; }
|
||||
public DateTime TokenExpiresAt { get; set; }
|
||||
}
|
||||
}
|
||||
101
PackagingMallShipper/Models/Order.cs
Normal file
101
PackagingMallShipper/Models/Order.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PackagingMallShipper.Models
|
||||
{
|
||||
public class Order
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string OrderNumber { get; set; }
|
||||
public int Status { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public decimal AmountReal { get; set; }
|
||||
|
||||
public int Uid { get; set; }
|
||||
public string UserMobile { get; set; }
|
||||
|
||||
public string LogisticsName { get; set; }
|
||||
public string LogisticsMobile { get; set; }
|
||||
public string LogisticsProvince { get; set; }
|
||||
public string LogisticsCity { get; set; }
|
||||
public string LogisticsDistrict { get; set; }
|
||||
public string LogisticsAddress { get; set; }
|
||||
|
||||
public string GoodsJson { get; set; }
|
||||
|
||||
public int? ExpressCompanyId { get; set; }
|
||||
public string ExpressCompanyName { get; set; }
|
||||
public string TrackingNumber { get; set; }
|
||||
public DateTime? DateShip { get; set; }
|
||||
|
||||
public string SyncStatus { get; set; }
|
||||
public DateTime? LocalUpdatedAt { get; set; }
|
||||
|
||||
public DateTime? DateAdd { get; set; }
|
||||
public DateTime? DatePay { get; set; }
|
||||
public DateTime? DateUpdate { get; set; }
|
||||
public DateTime? SyncedAt { get; set; }
|
||||
|
||||
public bool IsSelected { get; set; }
|
||||
|
||||
public string FullAddress => $"{LogisticsProvince}{LogisticsCity}{LogisticsDistrict}{LogisticsAddress}";
|
||||
|
||||
public string StatusText => OrderStatus.GetStatusText(Status);
|
||||
|
||||
public string GoodsInfo
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(GoodsJson)) return "";
|
||||
try
|
||||
{
|
||||
var goods = JsonConvert.DeserializeObject<List<GoodsItem>>(GoodsJson);
|
||||
return string.Join("; ", goods.ConvertAll(g => $"{g.GoodsName}x{g.Number}"));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class GoodsItem
|
||||
{
|
||||
[JsonProperty("goodsName")]
|
||||
public string GoodsName { get; set; }
|
||||
|
||||
[JsonProperty("number")]
|
||||
public int Number { get; set; }
|
||||
|
||||
[JsonProperty("price")]
|
||||
public decimal Price { get; set; }
|
||||
}
|
||||
|
||||
public static class OrderStatus
|
||||
{
|
||||
public static readonly Dictionary<int, string> Map = new Dictionary<int, string>
|
||||
{
|
||||
{ -1, "已关闭" },
|
||||
{ 0, "待支付" },
|
||||
{ 1, "待发货" },
|
||||
{ 2, "待收货" },
|
||||
{ 3, "待评价" },
|
||||
{ 4, "已完成" }
|
||||
};
|
||||
|
||||
public static string GetStatusText(int status)
|
||||
{
|
||||
return Map.TryGetValue(status, out var text) ? text : "未知";
|
||||
}
|
||||
}
|
||||
|
||||
public enum SyncStatusEnum
|
||||
{
|
||||
Synced,
|
||||
PendingShip,
|
||||
Shipping,
|
||||
Failed
|
||||
}
|
||||
}
|
||||
43
PackagingMallShipper/Models/ShipModels.cs
Normal file
43
PackagingMallShipper/Models/ShipModels.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PackagingMallShipper.Models
|
||||
{
|
||||
public class ShipOrderRequest
|
||||
{
|
||||
public int OrderId { get; set; }
|
||||
public string OrderNumber { get; set; }
|
||||
public int ExpressCompanyId { get; set; }
|
||||
public string TrackingNumber { get; set; }
|
||||
}
|
||||
|
||||
public class ShipResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
public class ShipOrderResult
|
||||
{
|
||||
public int OrderId { get; set; }
|
||||
public string OrderNumber { get; set; }
|
||||
public bool Success { get; set; }
|
||||
public string Error { get; set; }
|
||||
}
|
||||
|
||||
public class BatchShipResult
|
||||
{
|
||||
public int Total { get; set; }
|
||||
public int SuccessCount { get; set; }
|
||||
public int FailedCount { get; set; }
|
||||
public List<ShipOrderResult> Results { get; set; } = new List<ShipOrderResult>();
|
||||
}
|
||||
|
||||
public class ImportShipResult
|
||||
{
|
||||
public int TotalRows { get; set; }
|
||||
public int ValidOrders { get; set; }
|
||||
public int SuccessCount { get; set; }
|
||||
public int FailedCount { get; set; }
|
||||
public List<string> Errors { get; set; } = new List<string>();
|
||||
}
|
||||
}
|
||||
33
PackagingMallShipper/Models/SyncLog.cs
Normal file
33
PackagingMallShipper/Models/SyncLog.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
|
||||
namespace PackagingMallShipper.Models
|
||||
{
|
||||
public class SyncLog
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string SyncType { get; set; }
|
||||
public string SyncMode { get; set; }
|
||||
public DateTime SyncStart { get; set; }
|
||||
public DateTime? SyncEnd { get; set; }
|
||||
public int TotalCount { get; set; }
|
||||
public int NewCount { get; set; }
|
||||
public int UpdatedCount { get; set; }
|
||||
public int FailedCount { get; set; }
|
||||
public string Status { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class SyncResult
|
||||
{
|
||||
public int TotalCount { get; set; }
|
||||
public int NewCount { get; set; }
|
||||
public int UpdatedCount { get; set; }
|
||||
public int FailedCount { get; set; }
|
||||
}
|
||||
|
||||
public enum SyncMode
|
||||
{
|
||||
Full,
|
||||
Incremental
|
||||
}
|
||||
}
|
||||
139
PackagingMallShipper/PackagingMallShipper.csproj
Normal file
139
PackagingMallShipper/PackagingMallShipper.csproj
Normal file
@@ -0,0 +1,139 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</ProjectGuid>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>PackagingMallShipper</RootNamespace>
|
||||
<AssemblyName>PackagingMallShipper</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<Deterministic>true</Deterministic>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<ApplicationIcon>Resources\Icons\app.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="System.Xaml">
|
||||
<RequiredTargetFramework>4.0</RequiredTargetFramework>
|
||||
</Reference>
|
||||
<Reference Include="WindowsBase" />
|
||||
<Reference Include="PresentationCore" />
|
||||
<Reference Include="PresentationFramework" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ApplicationDefinition Include="App.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</ApplicationDefinition>
|
||||
<Page Include="Views\MainWindow.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\LoginWindow.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\OrderListView.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\ShippingDialog.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Resources\Styles.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Compile Include="App.xaml.cs">
|
||||
<DependentUpon>App.xaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Converters\BoolToVisibilityConverter.cs" />
|
||||
<Compile Include="Converters\StatusToColorConverter.cs" />
|
||||
<Compile Include="Converters\StatusToTextConverter.cs" />
|
||||
<Compile Include="Data\SqliteHelper.cs" />
|
||||
<Compile Include="Helpers\AppConfig.cs" />
|
||||
<Compile Include="Helpers\ExpressCompanies.cs" />
|
||||
<Compile Include="Helpers\ResourceHelper.cs" />
|
||||
<Compile Include="Models\ApiResponses.cs" />
|
||||
<Compile Include="Models\LocalSession.cs" />
|
||||
<Compile Include="Models\Order.cs" />
|
||||
<Compile Include="Models\ShipModels.cs" />
|
||||
<Compile Include="Models\SyncLog.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Services\AuthService.cs" />
|
||||
<Compile Include="Services\ExcelService.cs" />
|
||||
<Compile Include="Services\Interfaces.cs" />
|
||||
<Compile Include="Services\OrderService.cs" />
|
||||
<Compile Include="Services\ShipService.cs" />
|
||||
<Compile Include="Services\SyncService.cs" />
|
||||
<Compile Include="ViewModels\LoginViewModel.cs" />
|
||||
<Compile Include="ViewModels\MainViewModel.cs" />
|
||||
<Compile Include="ViewModels\OrderListViewModel.cs" />
|
||||
<Compile Include="ViewModels\ViewModelBase.cs" />
|
||||
<Compile Include="Views\LoginWindow.xaml.cs">
|
||||
<DependentUpon>LoginWindow.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\MainWindow.xaml.cs">
|
||||
<DependentUpon>MainWindow.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\OrderListView.xaml.cs">
|
||||
<DependentUpon>OrderListView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\ShippingDialog.xaml.cs">
|
||||
<DependentUpon>ShippingDialog.xaml</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Data\Resources\schema.sql" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Icons\app.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
||||
<PackageReference Include="System.Data.SQLite" Version="1.0.118" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="ClosedXML" Version="0.102.2" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
22
PackagingMallShipper/Properties/AssemblyInfo.cs
Normal file
22
PackagingMallShipper/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
|
||||
[assembly: AssemblyTitle("PackagingMallShipper")]
|
||||
[assembly: AssemblyDescription("包装商城发货助手 - 轻量级订单发货客户端")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("PackagingMallShipper")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2025")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
ResourceDictionaryLocation.None,
|
||||
ResourceDictionaryLocation.SourceAssembly
|
||||
)]
|
||||
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
38
PackagingMallShipper/Properties/Settings.Designer.cs
generated
Normal file
38
PackagingMallShipper/Properties/Settings.Designer.cs
generated
Normal file
@@ -0,0 +1,38 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace PackagingMallShipper.Properties {
|
||||
|
||||
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.0.0.0")]
|
||||
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
|
||||
|
||||
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
|
||||
|
||||
public static Settings Default {
|
||||
get {
|
||||
return defaultInstance;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("")]
|
||||
public string SavedMobile {
|
||||
get {
|
||||
return ((string)(this["SavedMobile"]));
|
||||
}
|
||||
set {
|
||||
this["SavedMobile"] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
PackagingMallShipper/Properties/Settings.settings
Normal file
9
PackagingMallShipper/Properties/Settings.settings
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="PackagingMallShipper.Properties" GeneratedClassName="Settings">
|
||||
<Profiles />
|
||||
<Settings>
|
||||
<Setting Name="SavedMobile" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)" />
|
||||
</Setting>
|
||||
</Settings>
|
||||
</SettingsFile>
|
||||
99
PackagingMallShipper/Resources/Styles.xaml
Normal file
99
PackagingMallShipper/Resources/Styles.xaml
Normal file
@@ -0,0 +1,99 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="clr-namespace:PackagingMallShipper.Converters">
|
||||
|
||||
<!-- 转换器 -->
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibility"/>
|
||||
<converters:InverseBoolConverter x:Key="InverseBool"/>
|
||||
<converters:StringToVisibilityConverter x:Key="StringToVisibility"/>
|
||||
<converters:StatusToColorConverter x:Key="StatusToColor"/>
|
||||
<converters:StatusToTextConverter x:Key="StatusToText"/>
|
||||
|
||||
<!-- 颜色定义 -->
|
||||
<SolidColorBrush x:Key="PrimaryColor" Color="#1890FF"/>
|
||||
<SolidColorBrush x:Key="SuccessColor" Color="#52C41A"/>
|
||||
<SolidColorBrush x:Key="WarningColor" Color="#FA8C16"/>
|
||||
<SolidColorBrush x:Key="DangerColor" Color="#FF4D4F"/>
|
||||
<SolidColorBrush x:Key="TextColor" Color="#333333"/>
|
||||
<SolidColorBrush x:Key="SecondaryTextColor" Color="#666666"/>
|
||||
<SolidColorBrush x:Key="BorderColor" Color="#D9D9D9"/>
|
||||
<SolidColorBrush x:Key="BackgroundColor" Color="#F5F5F5"/>
|
||||
|
||||
<!-- 按钮样式 -->
|
||||
<Style x:Key="PrimaryButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="{StaticResource PrimaryColor}"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="15,8"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="4"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#40A9FF"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="#096DD9"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Background" Value="#BFBFBF"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 文本框样式 -->
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="8,5"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsFocused" Value="True">
|
||||
<Setter Property="BorderBrush" Value="{StaticResource PrimaryColor}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- 下拉框样式 -->
|
||||
<Style TargetType="ComboBox">
|
||||
<Setter Property="Padding" Value="8,5"/>
|
||||
</Style>
|
||||
|
||||
<!-- DataGrid样式 -->
|
||||
<Style TargetType="DataGrid">
|
||||
<Setter Property="Background" Value="White"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="RowBackground" Value="White"/>
|
||||
<Setter Property="AlternatingRowBackground" Value="#FAFAFA"/>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Background" Value="#FAFAFA"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Padding" Value="10,8"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderColor}"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1"/>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="DataGridCell">
|
||||
<Setter Property="Padding" Value="10,5"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="#E6F7FF"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource TextColor}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
162
PackagingMallShipper/Services/AuthService.cs
Normal file
162
PackagingMallShipper/Services/AuthService.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using System;
|
||||
using System.Data.SQLite;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using PackagingMallShipper.Data;
|
||||
using PackagingMallShipper.Helpers;
|
||||
using PackagingMallShipper.Models;
|
||||
|
||||
namespace PackagingMallShipper.Services
|
||||
{
|
||||
public class AuthService : IAuthService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private LocalSession _currentSession;
|
||||
|
||||
public AuthService()
|
||||
{
|
||||
_httpClient = new HttpClient();
|
||||
_httpClient.Timeout = TimeSpan.FromSeconds(30);
|
||||
LoadSession();
|
||||
}
|
||||
|
||||
public LocalSession CurrentSession => _currentSession;
|
||||
|
||||
public bool IsLoggedIn => !string.IsNullOrEmpty(GetToken());
|
||||
|
||||
public async Task<LoginResult> LoginAsync(string mobile, string password)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = AppConfig.GetApiUrl($"/user/m/login?mobile={mobile}&pwd={password}");
|
||||
|
||||
var response = await _httpClient.PostAsync(url, null);
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<ApiResponse<LoginData>>(json);
|
||||
|
||||
if (result?.Code != 0)
|
||||
{
|
||||
return new LoginResult
|
||||
{
|
||||
Success = false,
|
||||
Message = result?.Msg ?? "登录失败"
|
||||
};
|
||||
}
|
||||
|
||||
var token = result.Data.Token;
|
||||
var uid = result.Data.Uid;
|
||||
|
||||
_httpClient.DefaultRequestHeaders.Clear();
|
||||
_httpClient.DefaultRequestHeaders.Add("X-Token", token);
|
||||
|
||||
var userInfoUrl = AppConfig.GetApiUrl("/user/detail");
|
||||
var userResponse = await _httpClient.GetAsync(userInfoUrl);
|
||||
var userJson = await userResponse.Content.ReadAsStringAsync();
|
||||
var userResult = JsonConvert.DeserializeObject<ApiResponse<UserInfo>>(userJson);
|
||||
|
||||
_currentSession = new LocalSession
|
||||
{
|
||||
Id = 1,
|
||||
Mobile = mobile,
|
||||
Token = token,
|
||||
Uid = uid,
|
||||
Nickname = userResult?.Data?.Nick ?? mobile,
|
||||
LastLoginAt = DateTime.Now,
|
||||
TokenExpiresAt = DateTime.Now.AddHours(AppConfig.TokenExpireHours)
|
||||
};
|
||||
|
||||
SaveSession(_currentSession);
|
||||
|
||||
return new LoginResult
|
||||
{
|
||||
Success = true,
|
||||
Token = token,
|
||||
UserInfo = userResult?.Data
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new LoginResult
|
||||
{
|
||||
Success = false,
|
||||
Message = $"网络错误: {ex.Message}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string GetToken()
|
||||
{
|
||||
if (_currentSession == null)
|
||||
LoadSession();
|
||||
|
||||
if (_currentSession == null)
|
||||
return null;
|
||||
if (_currentSession.TokenExpiresAt < DateTime.Now)
|
||||
return null;
|
||||
|
||||
return _currentSession.Token;
|
||||
}
|
||||
|
||||
public void Logout()
|
||||
{
|
||||
_currentSession = null;
|
||||
SqliteHelper.ExecuteNonQuery("DELETE FROM local_session WHERE id = 1");
|
||||
}
|
||||
|
||||
private void SaveSession(LocalSession session)
|
||||
{
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
var sql = @"INSERT OR REPLACE INTO local_session
|
||||
(id, mobile, token, uid, nickname, last_login_at, token_expires_at)
|
||||
VALUES (@id, @mobile, @token, @uid, @nickname, @lastLogin, @expires)";
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@id", session.Id);
|
||||
cmd.Parameters.AddWithValue("@mobile", session.Mobile);
|
||||
cmd.Parameters.AddWithValue("@token", session.Token);
|
||||
cmd.Parameters.AddWithValue("@uid", session.Uid);
|
||||
cmd.Parameters.AddWithValue("@nickname", session.Nickname ?? "");
|
||||
cmd.Parameters.AddWithValue("@lastLogin", session.LastLoginAt);
|
||||
cmd.Parameters.AddWithValue("@expires", session.TokenExpiresAt);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadSession()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
var sql = "SELECT * FROM local_session WHERE id = 1";
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
if (reader.Read())
|
||||
{
|
||||
_currentSession = new LocalSession
|
||||
{
|
||||
Id = reader.GetInt32(reader.GetOrdinal("id")),
|
||||
Mobile = reader.GetStringSafe("mobile"),
|
||||
Token = reader.GetStringSafe("token"),
|
||||
Uid = reader.GetInt32Safe("uid"),
|
||||
Nickname = reader.GetStringSafe("nickname"),
|
||||
LastLoginAt = reader.GetDateTimeSafe("last_login_at") ?? DateTime.MinValue,
|
||||
TokenExpiresAt = reader.GetDateTimeSafe("token_expires_at") ?? DateTime.MinValue
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
_currentSession = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
149
PackagingMallShipper/Services/ExcelService.cs
Normal file
149
PackagingMallShipper/Services/ExcelService.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ClosedXML.Excel;
|
||||
using PackagingMallShipper.Helpers;
|
||||
using PackagingMallShipper.Models;
|
||||
|
||||
namespace PackagingMallShipper.Services
|
||||
{
|
||||
public class ExcelService : IExcelService
|
||||
{
|
||||
private readonly IOrderService _orderService;
|
||||
private readonly IShipService _shipService;
|
||||
|
||||
public ExcelService(IOrderService orderService, IShipService shipService)
|
||||
{
|
||||
_orderService = orderService;
|
||||
_shipService = shipService;
|
||||
}
|
||||
|
||||
public async Task<int> ExportPendingOrdersAsync(string filePath)
|
||||
{
|
||||
var orders = await _orderService.GetOrdersAsync(status: 1);
|
||||
|
||||
using (var workbook = new XLWorkbook())
|
||||
{
|
||||
var worksheet = workbook.Worksheets.Add("待发货订单");
|
||||
|
||||
var headers = new[]
|
||||
{
|
||||
"订单号", "下单时间", "收件人", "联系电话",
|
||||
"省份", "城市", "区县", "详细地址",
|
||||
"商品信息", "订单金额", "快递公司", "快递单号"
|
||||
};
|
||||
|
||||
for (int i = 0; i < headers.Length; i++)
|
||||
{
|
||||
var cell = worksheet.Cell(1, i + 1);
|
||||
cell.Value = headers[i];
|
||||
cell.Style.Font.Bold = true;
|
||||
cell.Style.Fill.BackgroundColor = XLColor.LightGray;
|
||||
}
|
||||
|
||||
for (int i = 0; i < orders.Count; i++)
|
||||
{
|
||||
var order = orders[i];
|
||||
var row = i + 2;
|
||||
worksheet.Cell(row, 1).Value = order.OrderNumber;
|
||||
worksheet.Cell(row, 2).Value = order.DateAdd?.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
worksheet.Cell(row, 3).Value = order.LogisticsName;
|
||||
worksheet.Cell(row, 4).Value = order.LogisticsMobile;
|
||||
worksheet.Cell(row, 4).SetDataType(XLDataType.Text);
|
||||
worksheet.Cell(row, 5).Value = order.LogisticsProvince;
|
||||
worksheet.Cell(row, 6).Value = order.LogisticsCity;
|
||||
worksheet.Cell(row, 7).Value = order.LogisticsDistrict;
|
||||
worksheet.Cell(row, 8).Value = order.LogisticsAddress;
|
||||
worksheet.Cell(row, 9).Value = order.GoodsInfo;
|
||||
worksheet.Cell(row, 10).Value = order.AmountReal;
|
||||
worksheet.Cell(row, 11).Value = "";
|
||||
worksheet.Cell(row, 12).Value = "";
|
||||
}
|
||||
|
||||
worksheet.Column(1).Width = 20;
|
||||
worksheet.Column(2).Width = 18;
|
||||
worksheet.Column(4).Width = 15;
|
||||
worksheet.Column(8).Width = 40;
|
||||
worksheet.Column(9).Width = 30;
|
||||
worksheet.Column(12).Width = 20;
|
||||
|
||||
worksheet.SheetView.FreezeRows(1);
|
||||
|
||||
workbook.SaveAs(filePath);
|
||||
}
|
||||
|
||||
return orders.Count;
|
||||
}
|
||||
|
||||
public async Task<ImportShipResult> ImportAndShipAsync(string filePath)
|
||||
{
|
||||
var ordersToShip = new List<ShipOrderRequest>();
|
||||
int totalRows = 0;
|
||||
var errors = new List<string>();
|
||||
|
||||
using (var workbook = new XLWorkbook(filePath))
|
||||
{
|
||||
var worksheet = workbook.Worksheet(1);
|
||||
var rows = worksheet.RangeUsed().RowsUsed().Skip(1);
|
||||
totalRows = rows.Count();
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var orderNumber = row.Cell(1).GetString().Trim();
|
||||
var expressCompanyName = row.Cell(11).GetString().Trim();
|
||||
var trackingNumber = row.Cell(12).GetString().Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(orderNumber))
|
||||
continue;
|
||||
|
||||
if (string.IsNullOrEmpty(trackingNumber))
|
||||
{
|
||||
errors.Add($"订单 {orderNumber}: 缺少快递单号");
|
||||
continue;
|
||||
}
|
||||
|
||||
var order = await _orderService.GetOrderByNumberAsync(orderNumber);
|
||||
if (order == null)
|
||||
{
|
||||
errors.Add($"订单 {orderNumber}: 未找到");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (order.Status != 1)
|
||||
{
|
||||
errors.Add($"订单 {orderNumber}: 状态不是待发货");
|
||||
continue;
|
||||
}
|
||||
|
||||
var expressId = ExpressCompanies.GetIdByName(expressCompanyName);
|
||||
if (expressId == 0)
|
||||
expressId = -1;
|
||||
|
||||
ordersToShip.Add(new ShipOrderRequest
|
||||
{
|
||||
OrderId = order.Id,
|
||||
OrderNumber = orderNumber,
|
||||
ExpressCompanyId = expressId,
|
||||
TrackingNumber = trackingNumber
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var shipResult = await _shipService.BatchShipOrdersAsync(ordersToShip);
|
||||
|
||||
return new ImportShipResult
|
||||
{
|
||||
TotalRows = totalRows,
|
||||
ValidOrders = ordersToShip.Count,
|
||||
SuccessCount = shipResult.SuccessCount,
|
||||
FailedCount = shipResult.FailedCount,
|
||||
Errors = errors.Concat(
|
||||
shipResult.Results
|
||||
.Where(r => !r.Success)
|
||||
.Select(r => $"订单 {r.OrderNumber}: {r.Error}")
|
||||
).ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
48
PackagingMallShipper/Services/Interfaces.cs
Normal file
48
PackagingMallShipper/Services/Interfaces.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using PackagingMallShipper.Models;
|
||||
|
||||
namespace PackagingMallShipper.Services
|
||||
{
|
||||
public interface IAuthService
|
||||
{
|
||||
Task<LoginResult> LoginAsync(string mobile, string password);
|
||||
string GetToken();
|
||||
bool IsLoggedIn { get; }
|
||||
LocalSession CurrentSession { get; }
|
||||
void Logout();
|
||||
}
|
||||
|
||||
public interface IOrderService
|
||||
{
|
||||
Task<List<Order>> GetOrdersAsync(int? status = null, string keyword = null);
|
||||
Task<Order> GetOrderByIdAsync(int orderId);
|
||||
Task<Order> GetOrderByNumberAsync(string orderNumber);
|
||||
Task<int> GetOrderCountAsync(int? status = null);
|
||||
}
|
||||
|
||||
public interface ISyncService
|
||||
{
|
||||
Task<SyncResult> SyncOrdersAsync(SyncMode mode = SyncMode.Incremental);
|
||||
event Action<int, int> OnSyncProgress;
|
||||
event Action<string> OnSyncMessage;
|
||||
}
|
||||
|
||||
public interface IShipService
|
||||
{
|
||||
Task<ShipResult> ShipOrderAsync(ShipOrderRequest request);
|
||||
Task<BatchShipResult> BatchShipOrdersAsync(
|
||||
List<ShipOrderRequest> orders,
|
||||
int concurrency = 3,
|
||||
CancellationToken cancellationToken = default);
|
||||
event Action<int, int> OnShipProgress;
|
||||
}
|
||||
|
||||
public interface IExcelService
|
||||
{
|
||||
Task<int> ExportPendingOrdersAsync(string filePath);
|
||||
Task<ImportShipResult> ImportAndShipAsync(string filePath);
|
||||
}
|
||||
}
|
||||
147
PackagingMallShipper/Services/OrderService.cs
Normal file
147
PackagingMallShipper/Services/OrderService.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data.SQLite;
|
||||
using System.Threading.Tasks;
|
||||
using PackagingMallShipper.Data;
|
||||
using PackagingMallShipper.Models;
|
||||
|
||||
namespace PackagingMallShipper.Services
|
||||
{
|
||||
public class OrderService : IOrderService
|
||||
{
|
||||
public Task<List<Order>> GetOrdersAsync(int? status = null, string keyword = null)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var orders = new List<Order>();
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
var sql = "SELECT * FROM orders_cache WHERE 1=1";
|
||||
|
||||
if (status.HasValue && status.Value >= 0)
|
||||
sql += " AND status = @status";
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(keyword))
|
||||
sql += " AND (order_number LIKE @keyword OR logistics_name LIKE @keyword OR logistics_mobile LIKE @keyword)";
|
||||
|
||||
sql += " ORDER BY date_add DESC";
|
||||
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
if (status.HasValue && status.Value >= 0)
|
||||
cmd.Parameters.AddWithValue("@status", status.Value);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(keyword))
|
||||
cmd.Parameters.AddWithValue("@keyword", $"%{keyword}%");
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
orders.Add(MapOrder(reader));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return orders;
|
||||
});
|
||||
}
|
||||
|
||||
public Task<Order> GetOrderByIdAsync(int orderId)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
var sql = "SELECT * FROM orders_cache WHERE id = @id";
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@id", orderId);
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
if (reader.Read())
|
||||
return MapOrder(reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public Task<Order> GetOrderByNumberAsync(string orderNumber)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
var sql = "SELECT * FROM orders_cache WHERE order_number = @orderNumber";
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@orderNumber", orderNumber);
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
if (reader.Read())
|
||||
return MapOrder(reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public Task<int> GetOrderCountAsync(int? status = null)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
var sql = "SELECT COUNT(*) FROM orders_cache";
|
||||
if (status.HasValue && status.Value >= 0)
|
||||
sql += " WHERE status = @status";
|
||||
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
if (status.HasValue && status.Value >= 0)
|
||||
cmd.Parameters.AddWithValue("@status", status.Value);
|
||||
|
||||
return Convert.ToInt32(cmd.ExecuteScalar());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Order MapOrder(SQLiteDataReader reader)
|
||||
{
|
||||
return new Order
|
||||
{
|
||||
Id = reader.GetInt32(reader.GetOrdinal("id")),
|
||||
OrderNumber = reader.GetStringSafe("order_number"),
|
||||
Status = reader.GetInt32Safe("status"),
|
||||
Amount = reader.GetDecimalSafe("amount"),
|
||||
AmountReal = reader.GetDecimalSafe("amount_real"),
|
||||
Uid = reader.GetInt32Safe("uid"),
|
||||
UserMobile = reader.GetStringSafe("user_mobile"),
|
||||
LogisticsName = reader.GetStringSafe("logistics_name"),
|
||||
LogisticsMobile = reader.GetStringSafe("logistics_mobile"),
|
||||
LogisticsProvince = reader.GetStringSafe("logistics_province"),
|
||||
LogisticsCity = reader.GetStringSafe("logistics_city"),
|
||||
LogisticsDistrict = reader.GetStringSafe("logistics_district"),
|
||||
LogisticsAddress = reader.GetStringSafe("logistics_address"),
|
||||
GoodsJson = reader.GetStringSafe("goods_json"),
|
||||
ExpressCompanyId = reader.GetInt32Nullable("express_company_id"),
|
||||
ExpressCompanyName = reader.GetStringSafe("express_company_name"),
|
||||
TrackingNumber = reader.GetStringSafe("tracking_number"),
|
||||
DateShip = reader.GetDateTimeSafe("date_ship"),
|
||||
SyncStatus = reader.GetStringSafe("sync_status"),
|
||||
LocalUpdatedAt = reader.GetDateTimeSafe("local_updated_at"),
|
||||
DateAdd = reader.GetDateTimeSafe("date_add"),
|
||||
DatePay = reader.GetDateTimeSafe("date_pay"),
|
||||
DateUpdate = reader.GetDateTimeSafe("date_update"),
|
||||
SyncedAt = reader.GetDateTimeSafe("synced_at")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
179
PackagingMallShipper/Services/ShipService.cs
Normal file
179
PackagingMallShipper/Services/ShipService.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.SQLite;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using PackagingMallShipper.Data;
|
||||
using PackagingMallShipper.Helpers;
|
||||
using PackagingMallShipper.Models;
|
||||
|
||||
namespace PackagingMallShipper.Services
|
||||
{
|
||||
public class ShipService : IShipService
|
||||
{
|
||||
private readonly IAuthService _authService;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public event Action<int, int> OnShipProgress;
|
||||
|
||||
public ShipService(IAuthService authService)
|
||||
{
|
||||
_authService = authService;
|
||||
_httpClient = new HttpClient();
|
||||
_httpClient.Timeout = TimeSpan.FromSeconds(30);
|
||||
}
|
||||
|
||||
public async Task<ShipResult> ShipOrderAsync(ShipOrderRequest request)
|
||||
{
|
||||
var token = _authService.GetToken();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
throw new UnauthorizedAccessException("未登录");
|
||||
|
||||
UpdateLocalOrderStatus(request.OrderId, "shipping");
|
||||
|
||||
try
|
||||
{
|
||||
var url = AppConfig.GetApiUrl($"/order/delivery") +
|
||||
$"?orderId={request.OrderId}" +
|
||||
$"&expressType={request.ExpressCompanyId}" +
|
||||
$"&shipperCode={Uri.EscapeDataString(request.TrackingNumber)}";
|
||||
|
||||
_httpClient.DefaultRequestHeaders.Clear();
|
||||
_httpClient.DefaultRequestHeaders.Add("X-Token", token);
|
||||
|
||||
var response = await _httpClient.PostAsync(url, null);
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<ApiResponse<object>>(json);
|
||||
|
||||
if (result?.Code != 0)
|
||||
throw new Exception(result?.Msg ?? "发货失败");
|
||||
|
||||
UpdateLocalOrderAfterShip(request);
|
||||
|
||||
return new ShipResult { Success = true };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateLocalOrderStatus(request.OrderId, "failed", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<BatchShipResult> BatchShipOrdersAsync(
|
||||
List<ShipOrderRequest> orders,
|
||||
int concurrency = 3,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = new BatchShipResult
|
||||
{
|
||||
Total = orders.Count,
|
||||
Results = new List<ShipOrderResult>()
|
||||
};
|
||||
|
||||
int completed = 0;
|
||||
|
||||
using (var semaphore = new SemaphoreSlim(concurrency))
|
||||
{
|
||||
var tasks = orders.Select(async order =>
|
||||
{
|
||||
await semaphore.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await ShipOrderAsync(order);
|
||||
return new ShipOrderResult
|
||||
{
|
||||
OrderId = order.OrderId,
|
||||
OrderNumber = order.OrderNumber,
|
||||
Success = true
|
||||
};
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new ShipOrderResult
|
||||
{
|
||||
OrderId = order.OrderId,
|
||||
OrderNumber = order.OrderNumber,
|
||||
Success = false,
|
||||
Error = "已取消"
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ShipOrderResult
|
||||
{
|
||||
OrderId = order.OrderId,
|
||||
OrderNumber = order.OrderNumber,
|
||||
Success = false,
|
||||
Error = ex.Message
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
Interlocked.Increment(ref completed);
|
||||
OnShipProgress?.Invoke(completed, orders.Count);
|
||||
}
|
||||
});
|
||||
|
||||
var shipResults = await Task.WhenAll(tasks);
|
||||
result.Results.AddRange(shipResults);
|
||||
}
|
||||
|
||||
result.SuccessCount = result.Results.Count(r => r.Success);
|
||||
result.FailedCount = result.Results.Count(r => !r.Success);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void UpdateLocalOrderStatus(int orderId, string status, string errorMsg = null)
|
||||
{
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
var sql = @"UPDATE orders_cache
|
||||
SET sync_status = @status, local_updated_at = @now
|
||||
WHERE id = @id";
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@status", status);
|
||||
cmd.Parameters.AddWithValue("@now", DateTime.Now);
|
||||
cmd.Parameters.AddWithValue("@id", orderId);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateLocalOrderAfterShip(ShipOrderRequest request)
|
||||
{
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
var sql = @"UPDATE orders_cache SET
|
||||
status = 2,
|
||||
sync_status = 'synced',
|
||||
express_company_id = @expressId,
|
||||
express_company_name = @expressName,
|
||||
tracking_number = @trackingNumber,
|
||||
date_ship = @dateShip,
|
||||
local_updated_at = @now
|
||||
WHERE id = @id";
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@expressId", request.ExpressCompanyId);
|
||||
cmd.Parameters.AddWithValue("@expressName",
|
||||
ExpressCompanies.GetName(request.ExpressCompanyId));
|
||||
cmd.Parameters.AddWithValue("@trackingNumber", request.TrackingNumber);
|
||||
cmd.Parameters.AddWithValue("@dateShip", DateTime.Now);
|
||||
cmd.Parameters.AddWithValue("@now", DateTime.Now);
|
||||
cmd.Parameters.AddWithValue("@id", request.OrderId);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
226
PackagingMallShipper/Services/SyncService.cs
Normal file
226
PackagingMallShipper/Services/SyncService.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.SQLite;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using PackagingMallShipper.Data;
|
||||
using PackagingMallShipper.Helpers;
|
||||
using PackagingMallShipper.Models;
|
||||
|
||||
namespace PackagingMallShipper.Services
|
||||
{
|
||||
public class SyncService : ISyncService
|
||||
{
|
||||
private readonly IAuthService _authService;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public event Action<int, int> OnSyncProgress;
|
||||
public event Action<string> OnSyncMessage;
|
||||
|
||||
public SyncService(IAuthService authService)
|
||||
{
|
||||
_authService = authService;
|
||||
_httpClient = new HttpClient();
|
||||
_httpClient.Timeout = TimeSpan.FromSeconds(60);
|
||||
}
|
||||
|
||||
public async Task<SyncResult> SyncOrdersAsync(SyncMode mode = SyncMode.Incremental)
|
||||
{
|
||||
var token = _authService.GetToken();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
throw new UnauthorizedAccessException("未登录,请先登录");
|
||||
|
||||
var result = new SyncResult();
|
||||
var syncLog = new SyncLog
|
||||
{
|
||||
SyncType = "manual",
|
||||
SyncMode = mode.ToString().ToLower(),
|
||||
SyncStart = DateTime.Now,
|
||||
Status = "running"
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
DateTime? lastSyncTime = null;
|
||||
if (mode == SyncMode.Incremental)
|
||||
{
|
||||
lastSyncTime = GetLastSuccessSyncTime();
|
||||
}
|
||||
|
||||
int page = 1;
|
||||
int pageSize = AppConfig.SyncPageSize;
|
||||
bool hasMore = true;
|
||||
int totalPages = 1;
|
||||
|
||||
while (hasMore)
|
||||
{
|
||||
OnSyncMessage?.Invoke($"正在同步第 {page}/{totalPages} 页...");
|
||||
|
||||
var url = AppConfig.GetApiUrl($"/order/list?page={page}&pageSize={pageSize}");
|
||||
if (lastSyncTime.HasValue)
|
||||
url += $"&dateUpdateBegin={lastSyncTime:yyyy-MM-dd HH:mm:ss}";
|
||||
|
||||
_httpClient.DefaultRequestHeaders.Clear();
|
||||
_httpClient.DefaultRequestHeaders.Add("X-Token", token);
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var data = JsonConvert.DeserializeObject<ApiResponse<OrderListData>>(json);
|
||||
|
||||
if (data?.Code != 0)
|
||||
throw new Exception(data?.Msg ?? "获取订单失败");
|
||||
|
||||
var orders = data.Data?.OrderList ?? new List<OrderDto>();
|
||||
totalPages = data.Data?.TotalPage ?? 1;
|
||||
|
||||
if (orders.Count == 0) break;
|
||||
|
||||
foreach (var order in orders)
|
||||
{
|
||||
var isNew = await SaveOrderAsync(order);
|
||||
if (isNew) result.NewCount++;
|
||||
else result.UpdatedCount++;
|
||||
}
|
||||
|
||||
result.TotalCount += orders.Count;
|
||||
OnSyncProgress?.Invoke(page, totalPages);
|
||||
|
||||
if (page >= totalPages) hasMore = false;
|
||||
else page++;
|
||||
}
|
||||
|
||||
syncLog.Status = "success";
|
||||
syncLog.SyncEnd = DateTime.Now;
|
||||
syncLog.TotalCount = result.TotalCount;
|
||||
syncLog.NewCount = result.NewCount;
|
||||
syncLog.UpdatedCount = result.UpdatedCount;
|
||||
|
||||
OnSyncMessage?.Invoke($"同步完成!新增 {result.NewCount},更新 {result.UpdatedCount}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
syncLog.Status = "failed";
|
||||
syncLog.ErrorMessage = ex.Message;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
SaveSyncLog(syncLog);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<bool> SaveOrderAsync(OrderDto dto)
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
var checkSql = "SELECT id FROM orders_cache WHERE order_number = @orderNumber";
|
||||
bool exists = false;
|
||||
using (var cmd = new SQLiteCommand(checkSql, conn))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@orderNumber", dto.OrderNumber);
|
||||
exists = cmd.ExecuteScalar() != null;
|
||||
}
|
||||
|
||||
var sql = exists
|
||||
? @"UPDATE orders_cache SET
|
||||
status = @status, amount = @amount, amount_real = @amountReal,
|
||||
logistics_name = @logisticsName, logistics_mobile = @logisticsMobile,
|
||||
logistics_province = @province, logistics_city = @city,
|
||||
logistics_district = @district, logistics_address = @address,
|
||||
goods_json = @goodsJson, express_company_id = @expressId,
|
||||
tracking_number = @trackingNumber, date_ship = @dateShip,
|
||||
date_update = @dateUpdate, synced_at = @syncedAt
|
||||
WHERE order_number = @orderNumber"
|
||||
: @"INSERT INTO orders_cache
|
||||
(id, order_number, status, amount, amount_real, uid, user_mobile,
|
||||
logistics_name, logistics_mobile, logistics_province, logistics_city,
|
||||
logistics_district, logistics_address, goods_json, express_company_id,
|
||||
tracking_number, date_ship, date_add, date_pay, date_update, synced_at)
|
||||
VALUES
|
||||
(@id, @orderNumber, @status, @amount, @amountReal, @uid, @userMobile,
|
||||
@logisticsName, @logisticsMobile, @province, @city, @district, @address,
|
||||
@goodsJson, @expressId, @trackingNumber, @dateShip, @dateAdd, @datePay,
|
||||
@dateUpdate, @syncedAt)";
|
||||
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@id", dto.Id);
|
||||
cmd.Parameters.AddWithValue("@orderNumber", dto.OrderNumber);
|
||||
cmd.Parameters.AddWithValue("@status", dto.Status);
|
||||
cmd.Parameters.AddWithValue("@amount", dto.Amount);
|
||||
cmd.Parameters.AddWithValue("@amountReal", dto.AmountReal);
|
||||
cmd.Parameters.AddWithValue("@uid", dto.Uid);
|
||||
cmd.Parameters.AddWithValue("@userMobile", dto.UserMobile ?? "");
|
||||
cmd.Parameters.AddWithValue("@logisticsName", dto.LogisticsName ?? "");
|
||||
cmd.Parameters.AddWithValue("@logisticsMobile", dto.LogisticsMobile ?? "");
|
||||
cmd.Parameters.AddWithValue("@province", dto.LogisticsProvince ?? "");
|
||||
cmd.Parameters.AddWithValue("@city", dto.LogisticsCity ?? "");
|
||||
cmd.Parameters.AddWithValue("@district", dto.LogisticsDistrict ?? "");
|
||||
cmd.Parameters.AddWithValue("@address", dto.LogisticsAddress ?? "");
|
||||
cmd.Parameters.AddWithValue("@goodsJson", JsonConvert.SerializeObject(dto.Goods ?? new List<GoodsItem>()));
|
||||
cmd.Parameters.AddWithValue("@expressId", (object)dto.ShipperId ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@trackingNumber", dto.ShipperCode ?? "");
|
||||
cmd.Parameters.AddWithValue("@dateShip", (object)dto.DateShip ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@dateAdd", dto.DateAdd);
|
||||
cmd.Parameters.AddWithValue("@datePay", (object)dto.DatePay ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@dateUpdate", dto.DateUpdate);
|
||||
cmd.Parameters.AddWithValue("@syncedAt", DateTime.Now);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
return !exists;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private DateTime? GetLastSuccessSyncTime()
|
||||
{
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
var sql = @"SELECT sync_end FROM sync_logs
|
||||
WHERE status = 'success'
|
||||
ORDER BY sync_end DESC LIMIT 1";
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
var result = cmd.ExecuteScalar();
|
||||
return result as DateTime?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveSyncLog(SyncLog log)
|
||||
{
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
var sql = @"INSERT INTO sync_logs
|
||||
(sync_type, sync_mode, sync_start, sync_end, total_count,
|
||||
new_count, updated_count, failed_count, status, error_message)
|
||||
VALUES
|
||||
(@type, @mode, @start, @end, @total, @new, @updated, @failed, @status, @error)";
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@type", log.SyncType);
|
||||
cmd.Parameters.AddWithValue("@mode", log.SyncMode);
|
||||
cmd.Parameters.AddWithValue("@start", log.SyncStart);
|
||||
cmd.Parameters.AddWithValue("@end", (object)log.SyncEnd ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@total", log.TotalCount);
|
||||
cmd.Parameters.AddWithValue("@new", log.NewCount);
|
||||
cmd.Parameters.AddWithValue("@updated", log.UpdatedCount);
|
||||
cmd.Parameters.AddWithValue("@failed", log.FailedCount);
|
||||
cmd.Parameters.AddWithValue("@status", log.Status);
|
||||
cmd.Parameters.AddWithValue("@error", log.ErrorMessage ?? "");
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
109
PackagingMallShipper/ViewModels/LoginViewModel.cs
Normal file
109
PackagingMallShipper/ViewModels/LoginViewModel.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using PackagingMallShipper.Services;
|
||||
|
||||
namespace PackagingMallShipper.ViewModels
|
||||
{
|
||||
public partial class LoginViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IAuthService _authService;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _mobile = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _password = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _errorMessage = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _rememberMe = true;
|
||||
|
||||
public event Action OnLoginSuccess;
|
||||
|
||||
public LoginViewModel(IAuthService authService)
|
||||
{
|
||||
_authService = authService;
|
||||
LoadSavedCredentials();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task LoginAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Mobile))
|
||||
{
|
||||
ErrorMessage = "请输入手机号";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Password))
|
||||
{
|
||||
ErrorMessage = "请输入密码";
|
||||
return;
|
||||
}
|
||||
|
||||
IsBusy = true;
|
||||
ErrorMessage = "";
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _authService.LoginAsync(Mobile, Password);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
if (RememberMe)
|
||||
{
|
||||
SaveCredentials();
|
||||
}
|
||||
OnLoginSuccess?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
ErrorMessage = result.Message ?? "登录失败";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = $"登录异常: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadSavedCredentials()
|
||||
{
|
||||
try
|
||||
{
|
||||
var mobile = Properties.Settings.Default.SavedMobile;
|
||||
if (!string.IsNullOrEmpty(mobile))
|
||||
{
|
||||
Mobile = mobile;
|
||||
RememberMe = true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveCredentials()
|
||||
{
|
||||
try
|
||||
{
|
||||
Properties.Settings.Default.SavedMobile = Mobile;
|
||||
Properties.Settings.Default.Save();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
PackagingMallShipper/ViewModels/MainViewModel.cs
Normal file
38
PackagingMallShipper/ViewModels/MainViewModel.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using PackagingMallShipper.Services;
|
||||
|
||||
namespace PackagingMallShipper.ViewModels
|
||||
{
|
||||
public partial class MainViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IAuthService _authService;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _userName = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private OrderListViewModel _orderListViewModel;
|
||||
|
||||
public event Action OnLogout;
|
||||
|
||||
public MainViewModel(IAuthService authService, OrderListViewModel orderListViewModel)
|
||||
{
|
||||
_authService = authService;
|
||||
_orderListViewModel = orderListViewModel;
|
||||
|
||||
if (_authService.CurrentSession != null)
|
||||
{
|
||||
UserName = _authService.CurrentSession.Nickname ?? _authService.CurrentSession.Mobile;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Logout()
|
||||
{
|
||||
_authService.Logout();
|
||||
OnLogout?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
288
PackagingMallShipper/ViewModels/OrderListViewModel.cs
Normal file
288
PackagingMallShipper/ViewModels/OrderListViewModel.cs
Normal file
@@ -0,0 +1,288 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Win32;
|
||||
using PackagingMallShipper.Helpers;
|
||||
using PackagingMallShipper.Models;
|
||||
using PackagingMallShipper.Services;
|
||||
|
||||
namespace PackagingMallShipper.ViewModels
|
||||
{
|
||||
public partial class OrderListViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IOrderService _orderService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly IShipService _shipService;
|
||||
private readonly IExcelService _excelService;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<Order> _orders = new ObservableCollection<Order>();
|
||||
|
||||
[ObservableProperty]
|
||||
private Order _selectedOrder;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _selectedStatusIndex = 0;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _searchText = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private int _totalCount;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _pendingCount;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _syncProgress = "";
|
||||
|
||||
public OrderListViewModel(
|
||||
IOrderService orderService,
|
||||
ISyncService syncService,
|
||||
IShipService shipService,
|
||||
IExcelService excelService)
|
||||
{
|
||||
_orderService = orderService;
|
||||
_syncService = syncService;
|
||||
_shipService = shipService;
|
||||
_excelService = excelService;
|
||||
|
||||
_syncService.OnSyncProgress += (current, total) =>
|
||||
{
|
||||
SyncProgress = $"同步中 {current}/{total}";
|
||||
};
|
||||
|
||||
_syncService.OnSyncMessage += (msg) =>
|
||||
{
|
||||
StatusMessage = msg;
|
||||
};
|
||||
|
||||
_shipService.OnShipProgress += (current, total) =>
|
||||
{
|
||||
StatusMessage = $"发货中 {current}/{total}";
|
||||
};
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await RefreshOrdersAsync();
|
||||
await UpdateCountsAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RefreshOrdersAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
StatusMessage = "加载中...";
|
||||
|
||||
try
|
||||
{
|
||||
int? status = SelectedStatusIndex switch
|
||||
{
|
||||
0 => 1, // 待发货
|
||||
1 => 2, // 已发货
|
||||
2 => null, // 全部
|
||||
_ => null
|
||||
};
|
||||
|
||||
var orders = await _orderService.GetOrdersAsync(status, SearchText);
|
||||
|
||||
Orders.Clear();
|
||||
foreach (var order in orders)
|
||||
{
|
||||
Orders.Add(order);
|
||||
}
|
||||
|
||||
TotalCount = orders.Count;
|
||||
StatusMessage = $"共 {TotalCount} 条订单";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"加载失败: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SyncOrdersAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
StatusMessage = "正在同步订单...";
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _syncService.SyncOrdersAsync(SyncMode.Incremental);
|
||||
await RefreshOrdersAsync();
|
||||
await UpdateCountsAsync();
|
||||
StatusMessage = $"同步完成!新增 {result.NewCount},更新 {result.UpdatedCount}";
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
StatusMessage = "登录已过期,请重新登录";
|
||||
MessageBox.Show("登录已过期,请重新登录", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"同步失败: {ex.Message}";
|
||||
MessageBox.Show($"同步失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
SyncProgress = "";
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task FullSyncAsync()
|
||||
{
|
||||
var result = MessageBox.Show("全量同步将重新获取所有订单,可能需要较长时间,确定继续?",
|
||||
"确认", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||
|
||||
if (result != MessageBoxResult.Yes) return;
|
||||
|
||||
IsBusy = true;
|
||||
StatusMessage = "正在全量同步...";
|
||||
|
||||
try
|
||||
{
|
||||
var syncResult = await _syncService.SyncOrdersAsync(SyncMode.Full);
|
||||
await RefreshOrdersAsync();
|
||||
await UpdateCountsAsync();
|
||||
StatusMessage = $"全量同步完成!共 {syncResult.TotalCount} 条";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"同步失败: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ExportExcelAsync()
|
||||
{
|
||||
var dialog = new SaveFileDialog
|
||||
{
|
||||
Filter = "Excel文件|*.xlsx",
|
||||
FileName = $"待发货订单_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx"
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() != true) return;
|
||||
|
||||
IsBusy = true;
|
||||
StatusMessage = "正在导出...";
|
||||
|
||||
try
|
||||
{
|
||||
var count = await _excelService.ExportPendingOrdersAsync(dialog.FileName);
|
||||
StatusMessage = $"导出成功!共 {count} 条订单";
|
||||
MessageBox.Show($"导出成功!共 {count} 条订单\n\n文件位置:{dialog.FileName}",
|
||||
"导出成功", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"导出失败: {ex.Message}";
|
||||
MessageBox.Show($"导出失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ImportAndShipAsync()
|
||||
{
|
||||
var dialog = new OpenFileDialog
|
||||
{
|
||||
Filter = "Excel文件|*.xlsx;*.xls",
|
||||
Title = "选择发货单号文件"
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() != true) return;
|
||||
|
||||
IsBusy = true;
|
||||
StatusMessage = "正在导入并发货...";
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _excelService.ImportAndShipAsync(dialog.FileName);
|
||||
await RefreshOrdersAsync();
|
||||
await UpdateCountsAsync();
|
||||
|
||||
var message = $"导入完成!\n" +
|
||||
$"总行数: {result.TotalRows}\n" +
|
||||
$"有效订单: {result.ValidOrders}\n" +
|
||||
$"成功: {result.SuccessCount}\n" +
|
||||
$"失败: {result.FailedCount}";
|
||||
|
||||
if (result.Errors.Any())
|
||||
{
|
||||
message += $"\n\n错误详情:\n{string.Join("\n", result.Errors.Take(10))}";
|
||||
if (result.Errors.Count > 10)
|
||||
message += $"\n...还有 {result.Errors.Count - 10} 个错误";
|
||||
}
|
||||
|
||||
MessageBox.Show(message, "导入结果", MessageBoxButton.OK,
|
||||
result.FailedCount > 0 ? MessageBoxImage.Warning : MessageBoxImage.Information);
|
||||
|
||||
StatusMessage = $"发货完成: 成功 {result.SuccessCount},失败 {result.FailedCount}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"导入失败: {ex.Message}";
|
||||
MessageBox.Show($"导入失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task BatchShipSelectedAsync()
|
||||
{
|
||||
var selectedOrders = Orders.Where(o => o.IsSelected && o.Status == 1).ToList();
|
||||
if (!selectedOrders.Any())
|
||||
{
|
||||
MessageBox.Show("请先选择待发货的订单", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
MessageBox.Show($"批量发货功能需要先填写快递信息,请使用「导出Excel → 填写快递单号 → 导入发货」流程",
|
||||
"提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
private async Task UpdateCountsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
PendingCount = await _orderService.GetOrderCountAsync(1);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSelectedStatusIndexChanged(int value)
|
||||
{
|
||||
_ = RefreshOrdersAsync();
|
||||
}
|
||||
|
||||
partial void OnSearchTextChanged(string value)
|
||||
{
|
||||
// Debounce search - simple implementation
|
||||
}
|
||||
}
|
||||
}
|
||||
21
PackagingMallShipper/ViewModels/ViewModelBase.cs
Normal file
21
PackagingMallShipper/ViewModels/ViewModelBase.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace PackagingMallShipper.ViewModels
|
||||
{
|
||||
public abstract class ViewModelBase : ObservableObject
|
||||
{
|
||||
private bool _isBusy;
|
||||
public bool IsBusy
|
||||
{
|
||||
get => _isBusy;
|
||||
set => SetProperty(ref _isBusy, value);
|
||||
}
|
||||
|
||||
private string _statusMessage;
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
set => SetProperty(ref _statusMessage, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
60
PackagingMallShipper/Views/LoginWindow.xaml
Normal file
60
PackagingMallShipper/Views/LoginWindow.xaml
Normal file
@@ -0,0 +1,60 @@
|
||||
<Window x:Class="PackagingMallShipper.Views.LoginWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Title="包装商城发货助手 - 登录"
|
||||
Height="400" Width="350"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="NoResize"
|
||||
Background="#F5F5F5">
|
||||
|
||||
<Grid>
|
||||
<Border Background="White" CornerRadius="8" Margin="20" Padding="30">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="20" ShadowDepth="2" Opacity="0.1"/>
|
||||
</Border.Effect>
|
||||
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock Text="📦 包装商城发货助手"
|
||||
FontSize="20" FontWeight="Bold"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,30"/>
|
||||
|
||||
<TextBlock Text="手机号" Margin="0,0,0,5" FontSize="13"/>
|
||||
<TextBox x:Name="MobileTextBox"
|
||||
Text="{Binding Mobile, UpdateSourceTrigger=PropertyChanged}"
|
||||
Height="35" FontSize="14" Padding="10,5"
|
||||
Margin="0,0,0,15"/>
|
||||
|
||||
<TextBlock Text="密码" Margin="0,0,0,5" FontSize="13"/>
|
||||
<PasswordBox x:Name="PasswordBox"
|
||||
Height="35" FontSize="14" Padding="10,5"
|
||||
Margin="0,0,0,10"
|
||||
PasswordChanged="PasswordBox_PasswordChanged"/>
|
||||
|
||||
<CheckBox Content="记住手机号"
|
||||
IsChecked="{Binding RememberMe}"
|
||||
Margin="0,0,0,20"/>
|
||||
|
||||
<TextBlock Text="{Binding ErrorMessage}"
|
||||
Foreground="Red"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,0,0,10"
|
||||
Visibility="{Binding ErrorMessage, Converter={StaticResource StringToVisibility}}"/>
|
||||
|
||||
<Button Content="登 录"
|
||||
Command="{Binding LoginCommand}"
|
||||
Height="40" FontSize="15"
|
||||
Background="#1890FF" Foreground="White"
|
||||
BorderThickness="0"
|
||||
Cursor="Hand"
|
||||
IsEnabled="{Binding IsBusy, Converter={StaticResource InverseBool}}"/>
|
||||
|
||||
<ProgressBar IsIndeterminate="True" Height="3" Margin="0,10,0,0"
|
||||
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibility}}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
63
PackagingMallShipper/Views/LoginWindow.xaml.cs
Normal file
63
PackagingMallShipper/Views/LoginWindow.xaml.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using PackagingMallShipper.Services;
|
||||
using PackagingMallShipper.ViewModels;
|
||||
|
||||
namespace PackagingMallShipper.Views
|
||||
{
|
||||
public partial class LoginWindow : Window
|
||||
{
|
||||
private readonly LoginViewModel _viewModel;
|
||||
private readonly IAuthService _authService;
|
||||
|
||||
public LoginWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_authService = new AuthService();
|
||||
_viewModel = new LoginViewModel(_authService);
|
||||
DataContext = _viewModel;
|
||||
|
||||
_viewModel.OnLoginSuccess += OnLoginSuccess;
|
||||
|
||||
if (_authService.IsLoggedIn)
|
||||
{
|
||||
OpenMainWindow();
|
||||
}
|
||||
}
|
||||
|
||||
private void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is PasswordBox pb)
|
||||
{
|
||||
_viewModel.Password = pb.Password;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLoginSuccess()
|
||||
{
|
||||
OpenMainWindow();
|
||||
}
|
||||
|
||||
private void OpenMainWindow()
|
||||
{
|
||||
var orderService = new OrderService();
|
||||
var syncService = new SyncService(_authService);
|
||||
var shipService = new ShipService(_authService);
|
||||
var excelService = new ExcelService(orderService, shipService);
|
||||
var orderListViewModel = new OrderListViewModel(orderService, syncService, shipService, excelService);
|
||||
var mainViewModel = new MainViewModel(_authService, orderListViewModel);
|
||||
|
||||
var mainWindow = new MainWindow(mainViewModel);
|
||||
mainViewModel.OnLogout += () =>
|
||||
{
|
||||
var loginWindow = new LoginWindow();
|
||||
loginWindow.Show();
|
||||
mainWindow.Close();
|
||||
};
|
||||
|
||||
mainWindow.Show();
|
||||
this.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
57
PackagingMallShipper/Views/MainWindow.xaml
Normal file
57
PackagingMallShipper/Views/MainWindow.xaml
Normal file
@@ -0,0 +1,57 @@
|
||||
<Window x:Class="PackagingMallShipper.Views.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:PackagingMallShipper.Views"
|
||||
mc:Ignorable="d"
|
||||
Title="包装商城发货助手"
|
||||
Height="700" Width="1100"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
MinHeight="500" MinWidth="800">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="50"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="25"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 顶部工具栏 -->
|
||||
<Border Grid.Row="0" Background="#1890FF">
|
||||
<Grid Margin="15,0">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="📦 包装商城发货助手"
|
||||
Foreground="White" FontSize="16" FontWeight="Bold"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding UserName}"
|
||||
Foreground="White" FontSize="13"
|
||||
VerticalAlignment="Center" Margin="0,0,15,0"/>
|
||||
<Button Content="退出登录"
|
||||
Command="{Binding LogoutCommand}"
|
||||
Background="Transparent" Foreground="White"
|
||||
BorderThickness="1" BorderBrush="White"
|
||||
Padding="10,5" Cursor="Hand"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<local:OrderListView Grid.Row="1"
|
||||
DataContext="{Binding OrderListViewModel}"/>
|
||||
|
||||
<!-- 状态栏 -->
|
||||
<Border Grid.Row="2" Background="#F0F0F0">
|
||||
<Grid Margin="10,0">
|
||||
<TextBlock Text="{Binding OrderListViewModel.StatusMessage}"
|
||||
VerticalAlignment="Center" FontSize="12"/>
|
||||
<TextBlock Text="{Binding OrderListViewModel.SyncProgress}"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center" FontSize="12"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
19
PackagingMallShipper/Views/MainWindow.xaml.cs
Normal file
19
PackagingMallShipper/Views/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Windows;
|
||||
using PackagingMallShipper.ViewModels;
|
||||
|
||||
namespace PackagingMallShipper.Views
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow(MainViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = viewModel;
|
||||
|
||||
Loaded += async (s, e) =>
|
||||
{
|
||||
await viewModel.OrderListViewModel.InitializeAsync();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
149
PackagingMallShipper/Views/OrderListView.xaml
Normal file
149
PackagingMallShipper/Views/OrderListView.xaml
Normal file
@@ -0,0 +1,149 @@
|
||||
<UserControl x:Class="PackagingMallShipper.Views.OrderListView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Background="White">
|
||||
|
||||
<Grid Margin="15">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<Border Grid.Row="0" Background="#F5F7FA" CornerRadius="4" Padding="15" Margin="0,0,0,15">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<StackPanel Margin="0,0,40,0">
|
||||
<TextBlock Text="待发货订单" FontSize="12" Foreground="#666"/>
|
||||
<TextBlock Text="{Binding PendingCount}" FontSize="24" FontWeight="Bold" Foreground="#1890FF"/>
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="当前列表" FontSize="12" Foreground="#666"/>
|
||||
<TextBlock Text="{Binding TotalCount}" FontSize="24" FontWeight="Bold" Foreground="#52C41A"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,0,0,10">
|
||||
<ComboBox Width="100" SelectedIndex="{Binding SelectedStatusIndex}" Height="30">
|
||||
<ComboBoxItem Content="待发货"/>
|
||||
<ComboBoxItem Content="已发货"/>
|
||||
<ComboBoxItem Content="全部"/>
|
||||
</ComboBox>
|
||||
|
||||
<TextBox Width="200" Margin="10,0"
|
||||
Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
|
||||
VerticalContentAlignment="Center" Height="30"
|
||||
Padding="5,0">
|
||||
<TextBox.Style>
|
||||
<Style TargetType="TextBox">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="Text" Value="">
|
||||
<Setter Property="Background">
|
||||
<Setter.Value>
|
||||
<VisualBrush Stretch="None" AlignmentX="Left">
|
||||
<VisualBrush.Visual>
|
||||
<TextBlock Text="搜索订单号/收件人/电话" Foreground="Gray" Margin="5,0"/>
|
||||
</VisualBrush.Visual>
|
||||
</VisualBrush>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBox.Style>
|
||||
</TextBox>
|
||||
|
||||
<Button Content="🔍 搜索" Command="{Binding RefreshOrdersCommand}"
|
||||
Width="70" Height="30" Margin="0,0,10,0"/>
|
||||
|
||||
<Separator Width="1" Background="#DDD" Margin="10,5"/>
|
||||
|
||||
<Button Content="🔄 同步订单" Command="{Binding SyncOrdersCommand}"
|
||||
Width="90" Height="30" Margin="0,0,5,0"
|
||||
IsEnabled="{Binding IsBusy, Converter={StaticResource InverseBool}}"/>
|
||||
|
||||
<Button Content="📥 全量同步" Command="{Binding FullSyncCommand}"
|
||||
Width="90" Height="30" Margin="0,0,10,0"
|
||||
IsEnabled="{Binding IsBusy, Converter={StaticResource InverseBool}}"/>
|
||||
|
||||
<Separator Width="1" Background="#DDD" Margin="10,5"/>
|
||||
|
||||
<Button Content="📤 导出Excel" Command="{Binding ExportExcelCommand}"
|
||||
Width="90" Height="30" Margin="0,0,5,0"
|
||||
IsEnabled="{Binding IsBusy, Converter={StaticResource InverseBool}}"/>
|
||||
|
||||
<Button Content="📥 导入发货" Command="{Binding ImportAndShipCommand}"
|
||||
Width="90" Height="30"
|
||||
Background="#52C41A" Foreground="White" BorderThickness="0"
|
||||
IsEnabled="{Binding IsBusy, Converter={StaticResource InverseBool}}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<DataGrid Grid.Row="2"
|
||||
ItemsSource="{Binding Orders}"
|
||||
SelectedItem="{Binding SelectedOrder}"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
SelectionMode="Extended"
|
||||
CanUserAddRows="False"
|
||||
CanUserDeleteRows="False"
|
||||
GridLinesVisibility="Horizontal"
|
||||
HorizontalGridLinesBrush="#EEE"
|
||||
RowHeight="40"
|
||||
HeadersVisibility="Column">
|
||||
|
||||
<DataGrid.Columns>
|
||||
<DataGridCheckBoxColumn Binding="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged}"
|
||||
Width="40" Header=""/>
|
||||
|
||||
<DataGridTextColumn Header="订单号" Binding="{Binding OrderNumber}" Width="150"/>
|
||||
|
||||
<DataGridTextColumn Header="状态" Binding="{Binding StatusText}" Width="70">
|
||||
<DataGridTextColumn.ElementStyle>
|
||||
<Style TargetType="TextBlock">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Status}" Value="1">
|
||||
<Setter Property="Foreground" Value="#FA8C16"/>
|
||||
<Setter Property="FontWeight" Value="Bold"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Status}" Value="2">
|
||||
<Setter Property="Foreground" Value="#52C41A"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</DataGridTextColumn.ElementStyle>
|
||||
</DataGridTextColumn>
|
||||
|
||||
<DataGridTextColumn Header="下单时间"
|
||||
Binding="{Binding DateAdd, StringFormat=MM-dd HH:mm}"
|
||||
Width="100"/>
|
||||
|
||||
<DataGridTextColumn Header="收件人" Binding="{Binding LogisticsName}" Width="80"/>
|
||||
|
||||
<DataGridTextColumn Header="电话" Binding="{Binding LogisticsMobile}" Width="110"/>
|
||||
|
||||
<DataGridTextColumn Header="地址" Binding="{Binding FullAddress}" Width="*"/>
|
||||
|
||||
<DataGridTextColumn Header="金额"
|
||||
Binding="{Binding AmountReal, StringFormat=¥{0:F2}}"
|
||||
Width="70"/>
|
||||
|
||||
<DataGridTextColumn Header="快递单号" Binding="{Binding TrackingNumber}" Width="150"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<!-- 加载遮罩 -->
|
||||
<Border Grid.Row="2" Background="#80FFFFFF"
|
||||
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<TextBlock Text="⏳" FontSize="30" HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding StatusMessage}" FontSize="14" Margin="0,10,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
12
PackagingMallShipper/Views/OrderListView.xaml.cs
Normal file
12
PackagingMallShipper/Views/OrderListView.xaml.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace PackagingMallShipper.Views
|
||||
{
|
||||
public partial class OrderListView : UserControl
|
||||
{
|
||||
public OrderListView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
49
PackagingMallShipper/Views/ShippingDialog.xaml
Normal file
49
PackagingMallShipper/Views/ShippingDialog.xaml
Normal file
@@ -0,0 +1,49 @@
|
||||
<Window x:Class="PackagingMallShipper.Views.ShippingDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="填写发货信息"
|
||||
Height="280" Width="400"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ResizeMode="NoResize"
|
||||
ShowInTaskbar="False">
|
||||
|
||||
<Grid Margin="20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 订单信息 -->
|
||||
<StackPanel Grid.Row="0" Margin="0,0,0,15">
|
||||
<TextBlock Text="订单号:" FontWeight="Bold"/>
|
||||
<TextBlock x:Name="OrderNumberText" FontSize="14" Margin="0,5,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 快递公司 -->
|
||||
<StackPanel Grid.Row="1" Margin="0,0,0,15">
|
||||
<TextBlock Text="快递公司" Margin="0,0,0,5"/>
|
||||
<ComboBox x:Name="ExpressComboBox" Height="30"
|
||||
DisplayMemberPath="Name"
|
||||
SelectedValuePath="Id"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 快递单号 -->
|
||||
<StackPanel Grid.Row="2" Margin="0,0,0,15">
|
||||
<TextBlock Text="快递单号" Margin="0,0,0,5"/>
|
||||
<TextBox x:Name="TrackingNumberTextBox" Height="30"
|
||||
VerticalContentAlignment="Center" Padding="5,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Content="取消" Width="80" Height="30" Margin="0,0,10,0"
|
||||
Click="CancelButton_Click"/>
|
||||
<Button Content="确认发货" Width="100" Height="30"
|
||||
Background="#1890FF" Foreground="White" BorderThickness="0"
|
||||
Click="ConfirmButton_Click"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
50
PackagingMallShipper/Views/ShippingDialog.xaml.cs
Normal file
50
PackagingMallShipper/Views/ShippingDialog.xaml.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System.Windows;
|
||||
using PackagingMallShipper.Helpers;
|
||||
using PackagingMallShipper.Models;
|
||||
|
||||
namespace PackagingMallShipper.Views
|
||||
{
|
||||
public partial class ShippingDialog : Window
|
||||
{
|
||||
public int SelectedExpressId { get; private set; }
|
||||
public string TrackingNumber { get; private set; }
|
||||
|
||||
public ShippingDialog(Order order)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
OrderNumberText.Text = order.OrderNumber;
|
||||
ExpressComboBox.ItemsSource = ExpressCompanies.All;
|
||||
ExpressComboBox.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
private void ConfirmButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var trackingNumber = TrackingNumberTextBox.Text.Trim();
|
||||
if (string.IsNullOrEmpty(trackingNumber))
|
||||
{
|
||||
MessageBox.Show("请输入快递单号", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ExpressComboBox.SelectedItem is ExpressCompany express)
|
||||
{
|
||||
SelectedExpressId = express.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedExpressId = -1;
|
||||
}
|
||||
|
||||
TrackingNumber = trackingNumber;
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void CancelButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DialogResult = false;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
1653
轻量级订单发货客户端方案.md
Normal file
1653
轻量级订单发货客户端方案.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user