feat: 添加产品管理功能,订单导出支持货品编号
产品管理功能: - 新增 products_cache 表存储产品数据 - 从 API 同步产品数据到本地(支持增量/全量同步) - 本地维护"货品编号"字段(API 中不存在) - 产品列表支持搜索、筛选、编辑货品编号 - 主界面添加产品管理 Tab 页 订单导出增强: - 订单关联产品的货品编号 - Excel 导出新增"货品编号"列 - 支持与其他系统对接 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,82 @@ namespace PackagingMallShipper.Data
|
||||
SQLiteConnection.CreateFile(_dbPath);
|
||||
CreateTables();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 对现有数据库进行升级(添加新表)
|
||||
UpgradeDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpgradeDatabase()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var conn = GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
// 检查 products_cache 表是否存在,不存在则创建
|
||||
var checkSql = "SELECT name FROM sqlite_master WHERE type='table' AND name='products_cache'";
|
||||
using (var cmd = new SQLiteCommand(checkSql, conn))
|
||||
{
|
||||
var result = cmd.ExecuteScalar();
|
||||
if (result == null)
|
||||
{
|
||||
Debug.WriteLine($"[数据库升级] 创建 products_cache 表");
|
||||
using (var createCmd = new SQLiteCommand(GetProductsTableSchema(), conn))
|
||||
{
|
||||
createCmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[数据库升级] 升级失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetProductsTableSchema()
|
||||
{
|
||||
return @"
|
||||
-- 产品缓存表
|
||||
CREATE TABLE IF NOT EXISTS products_cache (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
bar_code TEXT,
|
||||
yy_id TEXT,
|
||||
item_code TEXT,
|
||||
min_price REAL,
|
||||
original_price REAL,
|
||||
stores INTEGER DEFAULT 0,
|
||||
status INTEGER DEFAULT 0,
|
||||
category_id INTEGER,
|
||||
unit TEXT,
|
||||
pics_json TEXT,
|
||||
is_local_only INTEGER DEFAULT 0,
|
||||
local_note TEXT,
|
||||
local_created_at DATETIME,
|
||||
local_updated_at DATETIME,
|
||||
synced_at DATETIME,
|
||||
pickup_price REAL,
|
||||
express_price REAL,
|
||||
pack_price REAL,
|
||||
pack_quantity INTEGER,
|
||||
product_series TEXT,
|
||||
placement_method TEXT,
|
||||
thickness TEXT,
|
||||
hole_size TEXT,
|
||||
box_size TEXT,
|
||||
applicable_type TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_products_name ON products_cache(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_products_bar_code ON products_cache(bar_code);
|
||||
CREATE INDEX IF NOT EXISTS idx_products_yy_id ON products_cache(yy_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_products_item_code ON products_cache(item_code);
|
||||
CREATE INDEX IF NOT EXISTS idx_products_status ON products_cache(status);
|
||||
";
|
||||
}
|
||||
|
||||
public static SQLiteConnection GetConnection()
|
||||
@@ -254,5 +330,11 @@ CREATE INDEX IF NOT EXISTS idx_sync_logs_status ON sync_logs(status);
|
||||
var ordinal = reader.GetOrdinal(column);
|
||||
return reader.IsDBNull(ordinal) ? (int?)null : reader.GetInt32(ordinal);
|
||||
}
|
||||
|
||||
public static decimal? GetDecimalNullable(this SQLiteDataReader reader, string column)
|
||||
{
|
||||
var ordinal = reader.GetOrdinal(column);
|
||||
return reader.IsDBNull(ordinal) ? (decimal?)null : reader.GetDecimal(ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,38 @@ namespace PackagingMallShipper.Models
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<int> GetGoodsIds()
|
||||
{
|
||||
if (string.IsNullOrEmpty(GoodsJson)) return new List<int>();
|
||||
try
|
||||
{
|
||||
var goods = JsonConvert.DeserializeObject<List<GoodsItem>>(GoodsJson);
|
||||
return goods.Select(g => g.GoodsId).Distinct().ToList();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<int>();
|
||||
}
|
||||
}
|
||||
|
||||
public string GetItemCodesInfo(Dictionary<int, string> itemCodeMap)
|
||||
{
|
||||
if (string.IsNullOrEmpty(GoodsJson) || itemCodeMap == null) return "";
|
||||
try
|
||||
{
|
||||
var goods = JsonConvert.DeserializeObject<List<GoodsItem>>(GoodsJson);
|
||||
var codes = goods
|
||||
.Where(g => itemCodeMap.ContainsKey(g.GoodsId))
|
||||
.Select(g => $"{itemCodeMap[g.GoodsId]}x{g.Number}")
|
||||
.Where(c => !string.IsNullOrEmpty(c));
|
||||
return string.Join("; ", codes);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class GoodsItem
|
||||
|
||||
302
PackagingMallShipper/Models/Product.cs
Normal file
302
PackagingMallShipper/Models/Product.cs
Normal file
@@ -0,0 +1,302 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PackagingMallShipper.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 本地产品模型
|
||||
/// </summary>
|
||||
public class Product
|
||||
{
|
||||
/// <summary>
|
||||
/// 商品ID(来自API)
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 条码编号(来自API的barCode)
|
||||
/// </summary>
|
||||
public string BarCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 外部商品编号(来自API的yyId)
|
||||
/// </summary>
|
||||
public string YyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本地自定义货品编号(用户可编辑)
|
||||
/// 格式如:1222003x12+13222003x24+1322003x12+0000000x12
|
||||
/// </summary>
|
||||
public string ItemCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 现价
|
||||
/// </summary>
|
||||
public decimal MinPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 原价
|
||||
/// </summary>
|
||||
public decimal OriginalPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 库存
|
||||
/// </summary>
|
||||
public int Stores { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态(0:上架, 1:下架)
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类ID
|
||||
/// </summary>
|
||||
public int? CategoryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 单位
|
||||
/// </summary>
|
||||
public string Unit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 图片数组JSON
|
||||
/// </summary>
|
||||
public string PicsJson { get; set; }
|
||||
|
||||
// ========== 本地管理字段 ==========
|
||||
|
||||
/// <summary>
|
||||
/// 是否为纯本地产品(未同步到远程)
|
||||
/// </summary>
|
||||
public bool IsLocalOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本地备注
|
||||
/// </summary>
|
||||
public string LocalNote { get; set; }
|
||||
|
||||
public DateTime? LocalCreatedAt { get; set; }
|
||||
public DateTime? LocalUpdatedAt { get; set; }
|
||||
public DateTime? SyncedAt { get; set; }
|
||||
|
||||
// ========== 扩展价格字段(对应产品报价表)==========
|
||||
|
||||
/// <summary>
|
||||
/// 自提开票价(元/套)
|
||||
/// </summary>
|
||||
public decimal? PickupPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 快递开票价(元/套)
|
||||
/// </summary>
|
||||
public decimal? ExpressPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 包价格(元/箱)
|
||||
/// </summary>
|
||||
public decimal? PackPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 打包数量(套/箱)
|
||||
/// </summary>
|
||||
public int? PackQuantity { get; set; }
|
||||
|
||||
// ========== 规格参数 ==========
|
||||
|
||||
/// <summary>
|
||||
/// 产品系列
|
||||
/// </summary>
|
||||
public string ProductSeries { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 放置方式
|
||||
/// </summary>
|
||||
public string PlacementMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 厚度
|
||||
/// </summary>
|
||||
public string Thickness { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 孔径尺寸
|
||||
/// </summary>
|
||||
public string HoleSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 纸箱尺寸
|
||||
/// </summary>
|
||||
public string BoxSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 适用蛋型
|
||||
/// </summary>
|
||||
public string ApplicableType { get; set; }
|
||||
|
||||
// ========== 计算属性 ==========
|
||||
|
||||
/// <summary>
|
||||
/// 状态文本
|
||||
/// </summary>
|
||||
public string StatusText => Status == 0 ? "上架" : "下架";
|
||||
|
||||
/// <summary>
|
||||
/// 首张图片URL
|
||||
/// </summary>
|
||||
public string FirstPic
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(PicsJson)) return null;
|
||||
try
|
||||
{
|
||||
var pics = JsonConvert.DeserializeObject<List<string>>(PicsJson);
|
||||
return pics?.Count > 0 ? pics[0] : null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否已设置货品编号
|
||||
/// </summary>
|
||||
public bool HasItemCode => !string.IsNullOrWhiteSpace(ItemCode);
|
||||
|
||||
/// <summary>
|
||||
/// 用于UI选择
|
||||
/// </summary>
|
||||
public bool IsSelected { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 产品状态枚举
|
||||
/// </summary>
|
||||
public static class ProductStatus
|
||||
{
|
||||
public const int OnSale = 0; // 上架
|
||||
public const int OffSale = 1; // 下架
|
||||
|
||||
public static string GetStatusText(int status)
|
||||
{
|
||||
return status == OnSale ? "上架" : "下架";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 产品列表API响应数据
|
||||
/// </summary>
|
||||
public class ProductListData
|
||||
{
|
||||
[JsonProperty("result")]
|
||||
public List<ProductDto> Result { get; set; }
|
||||
|
||||
[JsonProperty("list")]
|
||||
public List<ProductDto> List { get; set; }
|
||||
|
||||
[JsonProperty("totalRow")]
|
||||
public int TotalRow { get; set; }
|
||||
|
||||
[JsonProperty("total")]
|
||||
public int Total { get; set; }
|
||||
|
||||
[JsonProperty("totalPage")]
|
||||
public int TotalPage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取产品列表(兼容多种返回格式)
|
||||
/// </summary>
|
||||
public List<ProductDto> GetProducts()
|
||||
{
|
||||
return Result ?? List ?? new List<ProductDto>();
|
||||
}
|
||||
|
||||
public int GetTotalRow()
|
||||
{
|
||||
return TotalRow > 0 ? TotalRow : Total;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// API返回的产品DTO
|
||||
/// </summary>
|
||||
public class ProductDto
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("barCode")]
|
||||
public string BarCode { get; set; }
|
||||
|
||||
[JsonProperty("yyId")]
|
||||
public string YyId { get; set; }
|
||||
|
||||
[JsonProperty("minPrice")]
|
||||
public decimal MinPrice { get; set; }
|
||||
|
||||
[JsonProperty("originalPrice")]
|
||||
public decimal OriginalPrice { get; set; }
|
||||
|
||||
[JsonProperty("stores")]
|
||||
public int Stores { get; set; }
|
||||
|
||||
[JsonProperty("status")]
|
||||
public int Status { get; set; }
|
||||
|
||||
[JsonProperty("categoryId")]
|
||||
public int? CategoryId { get; set; }
|
||||
|
||||
[JsonProperty("unit")]
|
||||
public string Unit { get; set; }
|
||||
|
||||
[JsonProperty("pics")]
|
||||
public List<string> Pics { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 转换为本地Product模型
|
||||
/// </summary>
|
||||
public Product ToProduct(Product existingProduct = null)
|
||||
{
|
||||
return new Product
|
||||
{
|
||||
Id = Id,
|
||||
Name = Name,
|
||||
BarCode = BarCode,
|
||||
YyId = YyId,
|
||||
ItemCode = existingProduct?.ItemCode,
|
||||
MinPrice = MinPrice,
|
||||
OriginalPrice = OriginalPrice,
|
||||
Stores = Stores,
|
||||
Status = Status,
|
||||
CategoryId = CategoryId,
|
||||
Unit = Unit,
|
||||
PicsJson = Pics != null ? JsonConvert.SerializeObject(Pics) : null,
|
||||
IsLocalOnly = false,
|
||||
LocalNote = existingProduct?.LocalNote,
|
||||
PickupPrice = existingProduct?.PickupPrice,
|
||||
ExpressPrice = existingProduct?.ExpressPrice,
|
||||
PackPrice = existingProduct?.PackPrice,
|
||||
PackQuantity = existingProduct?.PackQuantity,
|
||||
ProductSeries = existingProduct?.ProductSeries,
|
||||
PlacementMethod = existingProduct?.PlacementMethod,
|
||||
Thickness = existingProduct?.Thickness,
|
||||
HoleSize = existingProduct?.HoleSize,
|
||||
BoxSize = existingProduct?.BoxSize,
|
||||
ApplicableType = existingProduct?.ApplicableType,
|
||||
SyncedAt = DateTime.Now
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,17 +12,25 @@ namespace PackagingMallShipper.Services
|
||||
{
|
||||
private readonly IOrderService _orderService;
|
||||
private readonly IShipService _shipService;
|
||||
private readonly IProductService _productService;
|
||||
|
||||
public ExcelService(IOrderService orderService, IShipService shipService)
|
||||
public ExcelService(IOrderService orderService, IShipService shipService, IProductService productService)
|
||||
{
|
||||
_orderService = orderService;
|
||||
_shipService = shipService;
|
||||
_productService = productService;
|
||||
}
|
||||
|
||||
public async Task<int> ExportPendingOrdersAsync(string filePath)
|
||||
{
|
||||
var orders = await _orderService.GetOrdersAsync(status: 1);
|
||||
|
||||
// 收集所有订单中的商品ID
|
||||
var allGoodsIds = orders.SelectMany(o => o.GetGoodsIds()).Distinct().ToList();
|
||||
|
||||
// 批量查询货品编号
|
||||
var itemCodeMap = await _productService.GetItemCodesByGoodsIdsAsync(allGoodsIds);
|
||||
|
||||
using (var workbook = new XLWorkbook())
|
||||
{
|
||||
var worksheet = workbook.Worksheets.Add("待发货订单");
|
||||
@@ -31,7 +39,7 @@ namespace PackagingMallShipper.Services
|
||||
{
|
||||
"订单号", "下单时间", "收件人", "联系电话",
|
||||
"省份", "城市", "区县", "收货地址",
|
||||
"商品信息", "数量", "快递公司", "快递单号"
|
||||
"商品信息", "货品编号", "数量", "快递公司", "快递单号"
|
||||
};
|
||||
|
||||
for (int i = 0; i < headers.Length; i++)
|
||||
@@ -57,9 +65,10 @@ namespace PackagingMallShipper.Services
|
||||
worksheet.Cell(row, 7).Value = order.LogisticsDistrict;
|
||||
worksheet.Cell(row, 8).Value = order.FullAddress;
|
||||
worksheet.Cell(row, 9).Value = order.GoodsInfo;
|
||||
worksheet.Cell(row, 10).Value = order.TotalQuantity;
|
||||
worksheet.Cell(row, 11).Value = "";
|
||||
worksheet.Cell(row, 10).Value = order.GetItemCodesInfo(itemCodeMap);
|
||||
worksheet.Cell(row, 11).Value = order.TotalQuantity;
|
||||
worksheet.Cell(row, 12).Value = "";
|
||||
worksheet.Cell(row, 13).Value = "";
|
||||
}
|
||||
|
||||
worksheet.Column(1).Width = 20;
|
||||
@@ -67,7 +76,8 @@ namespace PackagingMallShipper.Services
|
||||
worksheet.Column(4).Width = 15;
|
||||
worksheet.Column(8).Width = 50;
|
||||
worksheet.Column(9).Width = 30;
|
||||
worksheet.Column(12).Width = 20;
|
||||
worksheet.Column(10).Width = 40;
|
||||
worksheet.Column(13).Width = 20;
|
||||
|
||||
worksheet.SheetView.FreezeRows(1);
|
||||
|
||||
@@ -92,8 +102,8 @@ namespace PackagingMallShipper.Services
|
||||
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();
|
||||
var expressCompanyName = row.Cell(12).GetString().Trim();
|
||||
var trackingNumber = row.Cell(13).GetString().Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(orderNumber))
|
||||
continue;
|
||||
|
||||
@@ -46,4 +46,25 @@ namespace PackagingMallShipper.Services
|
||||
Task<int> ExportPendingOrdersAsync(string filePath);
|
||||
Task<ImportShipResult> ImportAndShipAsync(string filePath);
|
||||
}
|
||||
|
||||
public interface IProductService
|
||||
{
|
||||
Task<List<Product>> GetProductsAsync(int? status = null, string keyword = null);
|
||||
Task<Product> GetProductByIdAsync(int productId);
|
||||
Task<Product> GetProductByItemCodeAsync(string itemCode);
|
||||
Task<Product> GetProductByBarCodeAsync(string barCode);
|
||||
Task<bool> SaveProductAsync(Product product);
|
||||
Task<bool> DeleteProductAsync(int productId);
|
||||
Task<bool> UpdateItemCodeAsync(int productId, string itemCode);
|
||||
Task<int> GetProductCountAsync(int? status = null);
|
||||
Task<int> BatchUpdateItemCodesAsync(Dictionary<int, string> productItemCodes);
|
||||
Task<Dictionary<int, string>> GetItemCodesByGoodsIdsAsync(List<int> goodsIds);
|
||||
}
|
||||
|
||||
public interface IProductSyncService
|
||||
{
|
||||
Task<SyncResult> SyncProductsAsync(SyncMode mode = SyncMode.Incremental);
|
||||
event Action<int, int> OnSyncProgress;
|
||||
event Action<string> OnSyncMessage;
|
||||
}
|
||||
}
|
||||
|
||||
350
PackagingMallShipper/Services/ProductService.cs
Normal file
350
PackagingMallShipper/Services/ProductService.cs
Normal file
@@ -0,0 +1,350 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.SQLite;
|
||||
using System.Threading.Tasks;
|
||||
using PackagingMallShipper.Data;
|
||||
using PackagingMallShipper.Models;
|
||||
|
||||
namespace PackagingMallShipper.Services
|
||||
{
|
||||
public class ProductService : IProductService
|
||||
{
|
||||
public Task<List<Product>> GetProductsAsync(int? status = null, string keyword = null)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var products = new List<Product>();
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
var sql = "SELECT * FROM products_cache WHERE 1=1";
|
||||
|
||||
if (status.HasValue)
|
||||
sql += " AND status = @status";
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(keyword))
|
||||
sql += " AND (name LIKE @keyword OR bar_code LIKE @keyword OR yy_id LIKE @keyword OR item_code LIKE @keyword)";
|
||||
|
||||
sql += " ORDER BY local_updated_at DESC, synced_at DESC";
|
||||
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
if (status.HasValue)
|
||||
cmd.Parameters.AddWithValue("@status", status.Value);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(keyword))
|
||||
cmd.Parameters.AddWithValue("@keyword", $"%{keyword}%");
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
products.Add(MapProduct(reader));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return products;
|
||||
});
|
||||
}
|
||||
|
||||
public Task<Product> GetProductByIdAsync(int productId)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
var sql = "SELECT * FROM products_cache WHERE id = @id";
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@id", productId);
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
if (reader.Read())
|
||||
return MapProduct(reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public Task<Product> GetProductByItemCodeAsync(string itemCode)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
var sql = "SELECT * FROM products_cache WHERE item_code = @itemCode";
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@itemCode", itemCode);
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
if (reader.Read())
|
||||
return MapProduct(reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public Task<Product> GetProductByBarCodeAsync(string barCode)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
var sql = "SELECT * FROM products_cache WHERE bar_code = @barCode";
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@barCode", barCode);
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
if (reader.Read())
|
||||
return MapProduct(reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public Task<bool> SaveProductAsync(Product product)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
// 检查是否存在
|
||||
var checkSql = "SELECT id FROM products_cache WHERE id = @id";
|
||||
bool exists = false;
|
||||
using (var cmd = new SQLiteCommand(checkSql, conn))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@id", product.Id);
|
||||
exists = cmd.ExecuteScalar() != null;
|
||||
}
|
||||
|
||||
var sql = exists
|
||||
? @"UPDATE products_cache SET
|
||||
name = @name, bar_code = @barCode, yy_id = @yyId, item_code = @itemCode,
|
||||
min_price = @minPrice, original_price = @originalPrice, stores = @stores,
|
||||
status = @status, category_id = @categoryId, unit = @unit, pics_json = @picsJson,
|
||||
is_local_only = @isLocalOnly, local_note = @localNote, local_updated_at = @localUpdatedAt,
|
||||
pickup_price = @pickupPrice, express_price = @expressPrice, pack_price = @packPrice,
|
||||
pack_quantity = @packQuantity, product_series = @productSeries,
|
||||
placement_method = @placementMethod, thickness = @thickness, hole_size = @holeSize,
|
||||
box_size = @boxSize, applicable_type = @applicableType
|
||||
WHERE id = @id"
|
||||
: @"INSERT INTO products_cache
|
||||
(id, name, bar_code, yy_id, item_code, min_price, original_price, stores, status,
|
||||
category_id, unit, pics_json, is_local_only, local_note, local_created_at, local_updated_at,
|
||||
pickup_price, express_price, pack_price, pack_quantity, product_series,
|
||||
placement_method, thickness, hole_size, box_size, applicable_type)
|
||||
VALUES
|
||||
(@id, @name, @barCode, @yyId, @itemCode, @minPrice, @originalPrice, @stores, @status,
|
||||
@categoryId, @unit, @picsJson, @isLocalOnly, @localNote, @localCreatedAt, @localUpdatedAt,
|
||||
@pickupPrice, @expressPrice, @packPrice, @packQuantity, @productSeries,
|
||||
@placementMethod, @thickness, @holeSize, @boxSize, @applicableType)";
|
||||
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@id", product.Id);
|
||||
cmd.Parameters.AddWithValue("@name", product.Name ?? "");
|
||||
cmd.Parameters.AddWithValue("@barCode", product.BarCode ?? "");
|
||||
cmd.Parameters.AddWithValue("@yyId", product.YyId ?? "");
|
||||
cmd.Parameters.AddWithValue("@itemCode", product.ItemCode ?? "");
|
||||
cmd.Parameters.AddWithValue("@minPrice", product.MinPrice);
|
||||
cmd.Parameters.AddWithValue("@originalPrice", product.OriginalPrice);
|
||||
cmd.Parameters.AddWithValue("@stores", product.Stores);
|
||||
cmd.Parameters.AddWithValue("@status", product.Status);
|
||||
cmd.Parameters.AddWithValue("@categoryId", (object)product.CategoryId ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@unit", product.Unit ?? "");
|
||||
cmd.Parameters.AddWithValue("@picsJson", product.PicsJson ?? "");
|
||||
cmd.Parameters.AddWithValue("@isLocalOnly", product.IsLocalOnly ? 1 : 0);
|
||||
cmd.Parameters.AddWithValue("@localNote", product.LocalNote ?? "");
|
||||
cmd.Parameters.AddWithValue("@localCreatedAt", product.LocalCreatedAt ?? DateTime.Now);
|
||||
cmd.Parameters.AddWithValue("@localUpdatedAt", DateTime.Now);
|
||||
cmd.Parameters.AddWithValue("@pickupPrice", (object)product.PickupPrice ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@expressPrice", (object)product.ExpressPrice ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@packPrice", (object)product.PackPrice ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@packQuantity", (object)product.PackQuantity ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@productSeries", product.ProductSeries ?? "");
|
||||
cmd.Parameters.AddWithValue("@placementMethod", product.PlacementMethod ?? "");
|
||||
cmd.Parameters.AddWithValue("@thickness", product.Thickness ?? "");
|
||||
cmd.Parameters.AddWithValue("@holeSize", product.HoleSize ?? "");
|
||||
cmd.Parameters.AddWithValue("@boxSize", product.BoxSize ?? "");
|
||||
cmd.Parameters.AddWithValue("@applicableType", product.ApplicableType ?? "");
|
||||
|
||||
return cmd.ExecuteNonQuery() > 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Task<bool> DeleteProductAsync(int productId)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
var sql = "DELETE FROM products_cache WHERE id = @id AND is_local_only = 1";
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@id", productId);
|
||||
return cmd.ExecuteNonQuery() > 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Task<bool> UpdateItemCodeAsync(int productId, string itemCode)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
var sql = "UPDATE products_cache SET item_code = @itemCode, local_updated_at = @now WHERE id = @id";
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@id", productId);
|
||||
cmd.Parameters.AddWithValue("@itemCode", itemCode ?? "");
|
||||
cmd.Parameters.AddWithValue("@now", DateTime.Now);
|
||||
return cmd.ExecuteNonQuery() > 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Task<int> GetProductCountAsync(int? status = null)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
var sql = "SELECT COUNT(*) FROM products_cache";
|
||||
if (status.HasValue)
|
||||
sql += " WHERE status = @status";
|
||||
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
if (status.HasValue)
|
||||
cmd.Parameters.AddWithValue("@status", status.Value);
|
||||
|
||||
return Convert.ToInt32(cmd.ExecuteScalar());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Task<int> BatchUpdateItemCodesAsync(Dictionary<int, string> productItemCodes)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
int updated = 0;
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
using (var transaction = conn.BeginTransaction())
|
||||
{
|
||||
try
|
||||
{
|
||||
var sql = "UPDATE products_cache SET item_code = @itemCode, local_updated_at = @now WHERE id = @id";
|
||||
foreach (var kvp in productItemCodes)
|
||||
{
|
||||
using (var cmd = new SQLiteCommand(sql, conn, transaction))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@id", kvp.Key);
|
||||
cmd.Parameters.AddWithValue("@itemCode", kvp.Value ?? "");
|
||||
cmd.Parameters.AddWithValue("@now", DateTime.Now);
|
||||
updated += cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
transaction.Commit();
|
||||
}
|
||||
catch
|
||||
{
|
||||
transaction.Rollback();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
|
||||
public Task<Dictionary<int, string>> GetItemCodesByGoodsIdsAsync(List<int> goodsIds)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var result = new Dictionary<int, string>();
|
||||
if (goodsIds == null || goodsIds.Count == 0) return result;
|
||||
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
var idList = string.Join(",", goodsIds);
|
||||
var sql = $"SELECT id, item_code FROM products_cache WHERE id IN ({idList})";
|
||||
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
var id = reader.GetInt32(0);
|
||||
var itemCode = reader.GetStringSafe("item_code");
|
||||
if (!string.IsNullOrEmpty(itemCode))
|
||||
{
|
||||
result[id] = itemCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private Product MapProduct(SQLiteDataReader reader)
|
||||
{
|
||||
return new Product
|
||||
{
|
||||
Id = reader.GetInt32(reader.GetOrdinal("id")),
|
||||
Name = reader.GetStringSafe("name"),
|
||||
BarCode = reader.GetStringSafe("bar_code"),
|
||||
YyId = reader.GetStringSafe("yy_id"),
|
||||
ItemCode = reader.GetStringSafe("item_code"),
|
||||
MinPrice = reader.GetDecimalSafe("min_price"),
|
||||
OriginalPrice = reader.GetDecimalSafe("original_price"),
|
||||
Stores = reader.GetInt32Safe("stores"),
|
||||
Status = reader.GetInt32Safe("status"),
|
||||
CategoryId = reader.GetInt32Nullable("category_id"),
|
||||
Unit = reader.GetStringSafe("unit"),
|
||||
PicsJson = reader.GetStringSafe("pics_json"),
|
||||
IsLocalOnly = reader.GetInt32Safe("is_local_only") == 1,
|
||||
LocalNote = reader.GetStringSafe("local_note"),
|
||||
LocalCreatedAt = reader.GetDateTimeSafe("local_created_at"),
|
||||
LocalUpdatedAt = reader.GetDateTimeSafe("local_updated_at"),
|
||||
SyncedAt = reader.GetDateTimeSafe("synced_at"),
|
||||
PickupPrice = reader.GetDecimalNullable("pickup_price"),
|
||||
ExpressPrice = reader.GetDecimalNullable("express_price"),
|
||||
PackPrice = reader.GetDecimalNullable("pack_price"),
|
||||
PackQuantity = reader.GetInt32Nullable("pack_quantity"),
|
||||
ProductSeries = reader.GetStringSafe("product_series"),
|
||||
PlacementMethod = reader.GetStringSafe("placement_method"),
|
||||
Thickness = reader.GetStringSafe("thickness"),
|
||||
HoleSize = reader.GetStringSafe("hole_size"),
|
||||
BoxSize = reader.GetStringSafe("box_size"),
|
||||
ApplicableType = reader.GetStringSafe("applicable_type")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
191
PackagingMallShipper/Services/ProductSyncService.cs
Normal file
191
PackagingMallShipper/Services/ProductSyncService.cs
Normal file
@@ -0,0 +1,191 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.SQLite;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using PackagingMallShipper.Data;
|
||||
using PackagingMallShipper.Models;
|
||||
|
||||
namespace PackagingMallShipper.Services
|
||||
{
|
||||
public class ProductSyncService : IProductSyncService
|
||||
{
|
||||
private readonly IAuthService _authService;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public event Action<int, int> OnSyncProgress;
|
||||
public event Action<string> OnSyncMessage;
|
||||
|
||||
public ProductSyncService(IAuthService authService)
|
||||
{
|
||||
_authService = authService;
|
||||
_httpClient = new HttpClient();
|
||||
_httpClient.Timeout = TimeSpan.FromSeconds(60);
|
||||
}
|
||||
|
||||
public async Task<SyncResult> SyncProductsAsync(SyncMode mode = SyncMode.Incremental)
|
||||
{
|
||||
var token = _authService.GetToken();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
throw new UnauthorizedAccessException("未登录,请先登录");
|
||||
|
||||
var result = new SyncResult();
|
||||
|
||||
try
|
||||
{
|
||||
int page = 1;
|
||||
int pageSize = 50;
|
||||
bool hasMore = true;
|
||||
int totalPages = 1;
|
||||
|
||||
while (hasMore)
|
||||
{
|
||||
OnSyncMessage?.Invoke($"正在同步产品第 {page}/{totalPages} 页...");
|
||||
|
||||
var url = $"https://user.api.it120.cc/user/apiExtShopGoods/list?page={page}&pageSize={pageSize}";
|
||||
|
||||
Debug.WriteLine($"[产品同步] URL: {url}");
|
||||
|
||||
_httpClient.DefaultRequestHeaders.Clear();
|
||||
_httpClient.DefaultRequestHeaders.Add("X-Token", token);
|
||||
|
||||
var response = await _httpClient.PostAsync(url, null);
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Debug.WriteLine($"[产品同步] HTTP状态码: {response.StatusCode}");
|
||||
Debug.WriteLine($"[产品同步] 响应长度: {json?.Length ?? 0}");
|
||||
|
||||
var data = JsonConvert.DeserializeObject<ApiResponse<ProductListData>>(json);
|
||||
|
||||
if (data?.Code != 0)
|
||||
{
|
||||
if (data?.Msg?.Contains("token") == true || data?.Msg?.Contains("登录") == true)
|
||||
throw new UnauthorizedAccessException("当前登录token无效,请重新登录");
|
||||
throw new Exception(data?.Msg ?? "获取产品列表失败");
|
||||
}
|
||||
|
||||
var products = data.Data?.GetProducts() ?? new List<ProductDto>();
|
||||
totalPages = data.Data?.TotalPage ?? 1;
|
||||
if (totalPages == 0) totalPages = 1;
|
||||
var totalRow = data.Data?.GetTotalRow() ?? 0;
|
||||
|
||||
Debug.WriteLine($"[产品同步] 产品数量: {products.Count}, 总记录: {totalRow}, 总页数: {totalPages}");
|
||||
|
||||
if (products.Count == 0) break;
|
||||
|
||||
foreach (var productDto in products)
|
||||
{
|
||||
var isNew = await SaveProductAsync(productDto);
|
||||
if (isNew) result.NewCount++;
|
||||
else result.UpdatedCount++;
|
||||
}
|
||||
|
||||
result.TotalCount += products.Count;
|
||||
OnSyncProgress?.Invoke(page, totalPages);
|
||||
|
||||
if (page >= totalPages) hasMore = false;
|
||||
else page++;
|
||||
}
|
||||
|
||||
OnSyncMessage?.Invoke($"产品同步完成!新增 {result.NewCount},更新 {result.UpdatedCount}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[产品同步异常] {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Task<bool> SaveProductAsync(ProductDto dto)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
using (var conn = SqliteHelper.GetConnection())
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
// 检查是否存在,并获取现有的本地字段
|
||||
string existingItemCode = null;
|
||||
string existingLocalNote = null;
|
||||
decimal? existingPickupPrice = null;
|
||||
decimal? existingExpressPrice = null;
|
||||
decimal? existingPackPrice = null;
|
||||
int? existingPackQuantity = null;
|
||||
string existingProductSeries = null;
|
||||
string existingPlacementMethod = null;
|
||||
string existingThickness = null;
|
||||
string existingHoleSize = null;
|
||||
string existingBoxSize = null;
|
||||
string existingApplicableType = null;
|
||||
bool exists = false;
|
||||
|
||||
var checkSql = "SELECT * FROM products_cache WHERE id = @id";
|
||||
using (var cmd = new SQLiteCommand(checkSql, conn))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@id", dto.Id);
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
if (reader.Read())
|
||||
{
|
||||
exists = true;
|
||||
existingItemCode = reader.GetStringSafe("item_code");
|
||||
existingLocalNote = reader.GetStringSafe("local_note");
|
||||
existingPickupPrice = reader.GetDecimalNullable("pickup_price");
|
||||
existingExpressPrice = reader.GetDecimalNullable("express_price");
|
||||
existingPackPrice = reader.GetDecimalNullable("pack_price");
|
||||
existingPackQuantity = reader.GetInt32Nullable("pack_quantity");
|
||||
existingProductSeries = reader.GetStringSafe("product_series");
|
||||
existingPlacementMethod = reader.GetStringSafe("placement_method");
|
||||
existingThickness = reader.GetStringSafe("thickness");
|
||||
existingHoleSize = reader.GetStringSafe("hole_size");
|
||||
existingBoxSize = reader.GetStringSafe("box_size");
|
||||
existingApplicableType = reader.GetStringSafe("applicable_type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isNew = !exists;
|
||||
var picsJson = dto.Pics != null ? JsonConvert.SerializeObject(dto.Pics) : "";
|
||||
|
||||
var sql = isNew
|
||||
? @"INSERT INTO products_cache
|
||||
(id, name, bar_code, yy_id, item_code, min_price, original_price, stores, status,
|
||||
category_id, unit, pics_json, is_local_only, synced_at, local_created_at)
|
||||
VALUES
|
||||
(@id, @name, @barCode, @yyId, @itemCode, @minPrice, @originalPrice, @stores, @status,
|
||||
@categoryId, @unit, @picsJson, 0, @syncedAt, @syncedAt)"
|
||||
: @"UPDATE products_cache SET
|
||||
name = @name, bar_code = @barCode, yy_id = @yyId,
|
||||
min_price = @minPrice, original_price = @originalPrice, stores = @stores,
|
||||
status = @status, category_id = @categoryId, unit = @unit, pics_json = @picsJson,
|
||||
synced_at = @syncedAt
|
||||
WHERE id = @id";
|
||||
|
||||
using (var cmd = new SQLiteCommand(sql, conn))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@id", dto.Id);
|
||||
cmd.Parameters.AddWithValue("@name", dto.Name ?? "");
|
||||
cmd.Parameters.AddWithValue("@barCode", dto.BarCode ?? "");
|
||||
cmd.Parameters.AddWithValue("@yyId", dto.YyId ?? "");
|
||||
cmd.Parameters.AddWithValue("@itemCode", existingItemCode ?? "");
|
||||
cmd.Parameters.AddWithValue("@minPrice", dto.MinPrice);
|
||||
cmd.Parameters.AddWithValue("@originalPrice", dto.OriginalPrice);
|
||||
cmd.Parameters.AddWithValue("@stores", dto.Stores);
|
||||
cmd.Parameters.AddWithValue("@status", dto.Status);
|
||||
cmd.Parameters.AddWithValue("@categoryId", (object)dto.CategoryId ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@unit", dto.Unit ?? "");
|
||||
cmd.Parameters.AddWithValue("@picsJson", picsJson);
|
||||
cmd.Parameters.AddWithValue("@syncedAt", DateTime.Now);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
return isNew;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,22 @@ namespace PackagingMallShipper.ViewModels
|
||||
[ObservableProperty]
|
||||
private OrderListViewModel _orderListViewModel;
|
||||
|
||||
[ObservableProperty]
|
||||
private ProductListViewModel _productListViewModel;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _selectedTabIndex = 0;
|
||||
|
||||
public event Action OnLogout;
|
||||
|
||||
public MainViewModel(IAuthService authService, OrderListViewModel orderListViewModel)
|
||||
public MainViewModel(
|
||||
IAuthService authService,
|
||||
OrderListViewModel orderListViewModel,
|
||||
ProductListViewModel productListViewModel)
|
||||
{
|
||||
_authService = authService;
|
||||
_orderListViewModel = orderListViewModel;
|
||||
_productListViewModel = productListViewModel;
|
||||
|
||||
if (_authService.CurrentSession != null)
|
||||
{
|
||||
|
||||
237
PackagingMallShipper/ViewModels/ProductListViewModel.cs
Normal file
237
PackagingMallShipper/ViewModels/ProductListViewModel.cs
Normal file
@@ -0,0 +1,237 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using PackagingMallShipper.Models;
|
||||
using PackagingMallShipper.Services;
|
||||
|
||||
namespace PackagingMallShipper.ViewModels
|
||||
{
|
||||
public partial class ProductListViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IProductService _productService;
|
||||
private readonly IProductSyncService _productSyncService;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<Product> _products = new ObservableCollection<Product>();
|
||||
|
||||
[ObservableProperty]
|
||||
private Product _selectedProduct;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _selectedStatusIndex = 0; // 0:全部, 1:上架, 2:下架
|
||||
|
||||
[ObservableProperty]
|
||||
private string _searchText = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private int _totalCount;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _onSaleCount;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _syncProgress = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private DateTime? _lastSyncTime;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isEditing;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _editingItemCode = "";
|
||||
|
||||
public ProductListViewModel(IProductService productService, IProductSyncService productSyncService)
|
||||
{
|
||||
_productService = productService;
|
||||
_productSyncService = productSyncService;
|
||||
|
||||
_productSyncService.OnSyncProgress += (current, total) =>
|
||||
{
|
||||
SyncProgress = $"同步中 {current}/{total}";
|
||||
};
|
||||
|
||||
_productSyncService.OnSyncMessage += (msg) =>
|
||||
{
|
||||
StatusMessage = msg;
|
||||
};
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await RefreshProductsAsync();
|
||||
await UpdateCountsAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RefreshProductsAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
StatusMessage = "加载中...";
|
||||
|
||||
try
|
||||
{
|
||||
int? status = SelectedStatusIndex switch
|
||||
{
|
||||
1 => ProductStatus.OnSale,
|
||||
2 => ProductStatus.OffSale,
|
||||
_ => null
|
||||
};
|
||||
|
||||
var products = await _productService.GetProductsAsync(status, SearchText);
|
||||
|
||||
Products.Clear();
|
||||
foreach (var product in products)
|
||||
{
|
||||
Products.Add(product);
|
||||
}
|
||||
|
||||
TotalCount = products.Count;
|
||||
StatusMessage = $"共 {TotalCount} 个产品";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"加载失败: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SyncProductsAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
StatusMessage = "正在同步产品...";
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _productSyncService.SyncProductsAsync(SyncMode.Full);
|
||||
LastSyncTime = DateTime.Now;
|
||||
await RefreshProductsAsync();
|
||||
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 SearchAsync()
|
||||
{
|
||||
await RefreshProductsAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void StartEditItemCode()
|
||||
{
|
||||
if (SelectedProduct == null)
|
||||
{
|
||||
MessageBox.Show("请先选择一个产品", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
EditingItemCode = SelectedProduct.ItemCode ?? "";
|
||||
IsEditing = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SaveItemCodeAsync()
|
||||
{
|
||||
if (SelectedProduct == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
await _productService.UpdateItemCodeAsync(SelectedProduct.Id, EditingItemCode);
|
||||
SelectedProduct.ItemCode = EditingItemCode;
|
||||
StatusMessage = "货品编号已更新";
|
||||
|
||||
IsEditing = false;
|
||||
EditingItemCode = "";
|
||||
|
||||
await RefreshProductsAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"更新失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CancelEditItemCode()
|
||||
{
|
||||
IsEditing = false;
|
||||
EditingItemCode = "";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task DeleteProductAsync()
|
||||
{
|
||||
if (SelectedProduct == null) return;
|
||||
|
||||
if (!SelectedProduct.IsLocalOnly)
|
||||
{
|
||||
MessageBox.Show("只能删除本地创建的产品,不能删除从API同步的产品", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = MessageBox.Show($"确定要删除产品 [{SelectedProduct.Name}] 吗?", "确认删除",
|
||||
MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||
|
||||
if (result != MessageBoxResult.Yes) return;
|
||||
|
||||
try
|
||||
{
|
||||
await _productService.DeleteProductAsync(SelectedProduct.Id);
|
||||
await RefreshProductsAsync();
|
||||
await UpdateCountsAsync();
|
||||
StatusMessage = "删除成功";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"删除失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateCountsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
TotalCount = await _productService.GetProductCountAsync();
|
||||
OnSaleCount = await _productService.GetProductCountAsync(ProductStatus.OnSale);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSelectedStatusIndexChanged(int value)
|
||||
{
|
||||
_ = RefreshProductsAsync();
|
||||
}
|
||||
|
||||
partial void OnSearchTextChanged(string value)
|
||||
{
|
||||
// 可添加防抖逻辑
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,9 +49,14 @@ namespace PackagingMallShipper.Views
|
||||
var orderService = new OrderService();
|
||||
var syncService = new SyncService(_authService);
|
||||
var shipService = new ShipService(_authService);
|
||||
var excelService = new ExcelService(orderService, shipService);
|
||||
var productService = new ProductService();
|
||||
var excelService = new ExcelService(orderService, shipService, productService);
|
||||
var orderListViewModel = new OrderListViewModel(orderService, syncService, shipService, excelService);
|
||||
var mainViewModel = new MainViewModel(_authService, orderListViewModel);
|
||||
|
||||
var productSyncService = new ProductSyncService(_authService);
|
||||
var productListViewModel = new ProductListViewModel(productService, productSyncService);
|
||||
|
||||
var mainViewModel = new MainViewModel(_authService, orderListViewModel, productListViewModel);
|
||||
|
||||
var mainWindow = new MainWindow(mainViewModel);
|
||||
mainViewModel.OnLogout += () =>
|
||||
|
||||
@@ -5,32 +5,32 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:PackagingMallShipper.Views"
|
||||
mc:Ignorable="d"
|
||||
Title="包装商城发货助手"
|
||||
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="📦 包装商城发货助手"
|
||||
<TextBlock Text="包装商城发货助手"
|
||||
Foreground="White" FontSize="16" FontWeight="Bold"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding UserName}"
|
||||
<TextBlock Text="{Binding UserName}"
|
||||
Foreground="White" FontSize="13"
|
||||
VerticalAlignment="Center" Margin="0,0,15,0"/>
|
||||
<Button Content="退出登录"
|
||||
<Button Content="退出登录"
|
||||
Command="{Binding LogoutCommand}"
|
||||
Background="Transparent" Foreground="White"
|
||||
BorderThickness="1" BorderBrush="White"
|
||||
@@ -38,19 +38,44 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<local:OrderListView Grid.Row="1"
|
||||
DataContext="{Binding OrderListViewModel}"/>
|
||||
|
||||
|
||||
<!-- 主内容区 - TabControl -->
|
||||
<TabControl Grid.Row="1" SelectedIndex="{Binding SelectedTabIndex}">
|
||||
<TabItem Header="订单管理">
|
||||
<local:OrderListView DataContext="{Binding OrderListViewModel}"/>
|
||||
</TabItem>
|
||||
<TabItem Header="产品管理">
|
||||
<local:ProductListView DataContext="{Binding ProductListViewModel}"/>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
|
||||
<!-- 状态栏 -->
|
||||
<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"/>
|
||||
<TextBlock VerticalAlignment="Center" FontSize="12">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Text" Value="{Binding OrderListViewModel.StatusMessage}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding SelectedTabIndex}" Value="1">
|
||||
<Setter Property="Text" Value="{Binding ProductListViewModel.StatusMessage}"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<TextBlock HorizontalAlignment="Right" VerticalAlignment="Center" FontSize="12">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Text" Value="{Binding OrderListViewModel.SyncProgress}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding SelectedTabIndex}" Value="1">
|
||||
<Setter Property="Text" Value="{Binding ProductListViewModel.SyncProgress}"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace PackagingMallShipper.Views
|
||||
Loaded += async (s, e) =>
|
||||
{
|
||||
await viewModel.OrderListViewModel.InitializeAsync();
|
||||
await viewModel.ProductListViewModel.InitializeAsync();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
200
PackagingMallShipper/Views/ProductListView.xaml
Normal file
200
PackagingMallShipper/Views/ProductListView.xaml
Normal file
@@ -0,0 +1,200 @@
|
||||
<UserControl x:Class="PackagingMallShipper.Views.ProductListView"
|
||||
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="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<Border Grid.Row="0" Background="#F5F7FA" CornerRadius="4" Padding="15" Margin="0,0,0,15">
|
||||
<Grid>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
|
||||
<StackPanel Margin="0,0,40,0">
|
||||
<TextBlock Text="产品总数" FontSize="12" Foreground="#666"/>
|
||||
<TextBlock Text="{Binding TotalCount}" FontSize="24" FontWeight="Bold" Foreground="#1890FF"/>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,40,0">
|
||||
<TextBlock Text="上架中" FontSize="12" Foreground="#666"/>
|
||||
<TextBlock Text="{Binding OnSaleCount}" FontSize="24" FontWeight="Bold" Foreground="#52C41A"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<TextBlock Text="上次同步:" FontSize="12" Foreground="#999" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding LastSyncTime, StringFormat=yyyy-MM-dd HH:mm, TargetNullValue=未同步}"
|
||||
FontSize="12" Foreground="#666" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</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="250" 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 SearchCommand}"
|
||||
Width="70" Height="30" Margin="0,0,10,0"/>
|
||||
|
||||
<Separator Width="1" Background="#DDD" Margin="10,5"/>
|
||||
|
||||
<Button Content="同步产品" Command="{Binding SyncProductsCommand}"
|
||||
Width="90" Height="30" Margin="0,0,5,0"
|
||||
Background="#1890FF" Foreground="White" BorderThickness="0"
|
||||
IsEnabled="{Binding IsBusy, Converter={StaticResource InverseBool}}"/>
|
||||
|
||||
<Separator Width="1" Background="#DDD" Margin="10,5"/>
|
||||
|
||||
<Button Content="编辑货品编号" Command="{Binding StartEditItemCodeCommand}"
|
||||
Width="100" Height="30" Margin="0,0,5,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 产品列表 -->
|
||||
<DataGrid Grid.Row="2"
|
||||
ItemsSource="{Binding Products}"
|
||||
SelectedItem="{Binding SelectedProduct}"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
SelectionMode="Single"
|
||||
CanUserAddRows="False"
|
||||
CanUserDeleteRows="False"
|
||||
GridLinesVisibility="Horizontal"
|
||||
HorizontalGridLinesBrush="#EEE"
|
||||
RowHeight="45"
|
||||
HeadersVisibility="Column">
|
||||
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="ID" Binding="{Binding Id}" Width="60"/>
|
||||
|
||||
<DataGridTextColumn Header="商品名称" Binding="{Binding Name}" Width="200"/>
|
||||
|
||||
<DataGridTemplateColumn Header="货品编号" Width="280">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Padding="5,0">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Text" Value="{Binding ItemCode}"/>
|
||||
<Setter Property="Foreground" Value="#1890FF"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasItemCode}" Value="False">
|
||||
<Setter Property="Text" Value="(未设置)"/>
|
||||
<Setter Property="Foreground" Value="#CCC"/>
|
||||
<Setter Property="FontStyle" Value="Italic"/>
|
||||
<Setter Property="FontWeight" Value="Normal"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTextColumn Header="条码" Binding="{Binding BarCode}" Width="120"/>
|
||||
|
||||
<DataGridTextColumn Header="外部编号" Binding="{Binding YyId}" Width="100"/>
|
||||
|
||||
<DataGridTextColumn Header="现价" Binding="{Binding MinPrice, StringFormat=¥{0:F2}}" Width="80"/>
|
||||
|
||||
<DataGridTextColumn Header="库存" Binding="{Binding Stores}" Width="60"/>
|
||||
|
||||
<DataGridTextColumn Header="单位" Binding="{Binding Unit}" Width="50"/>
|
||||
|
||||
<DataGridTemplateColumn Header="状态" Width="60">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding StatusText}" Padding="5,0">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Status}" Value="0">
|
||||
<Setter Property="Foreground" Value="#52C41A"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Status}" Value="1">
|
||||
<Setter Property="Foreground" Value="#999"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTextColumn Header="同步时间"
|
||||
Binding="{Binding SyncedAt, StringFormat=MM-dd HH:mm}"
|
||||
Width="100"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<!-- 编辑货品编号面板 -->
|
||||
<Border Grid.Row="3" Background="#F0F5FF" CornerRadius="4" Padding="15" Margin="0,10,0,0"
|
||||
Visibility="{Binding IsEditing, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="编辑货品编号" FontWeight="Bold" Margin="0,0,0,10"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="产品:" VerticalAlignment="Center" Width="50"/>
|
||||
<TextBlock Text="{Binding SelectedProduct.Name}" VerticalAlignment="Center"
|
||||
FontWeight="SemiBold" Foreground="#1890FF"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||
<TextBlock Text="货品编号:" VerticalAlignment="Center" Width="70"/>
|
||||
<TextBox Text="{Binding EditingItemCode, UpdateSourceTrigger=PropertyChanged}"
|
||||
Width="400" Height="30" Padding="5" VerticalContentAlignment="Center"/>
|
||||
<Button Content="保存" Command="{Binding SaveItemCodeCommand}"
|
||||
Width="70" Height="30" Margin="10,0,0,0"
|
||||
Background="#52C41A" Foreground="White" BorderThickness="0"/>
|
||||
<Button Content="取消" Command="{Binding CancelEditItemCodeCommand}"
|
||||
Width="70" Height="30" Margin="5,0,0,0"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="格式示例:1222003x12+13222003x24+1322003x12+0000000x12"
|
||||
FontSize="11" Foreground="#999" Margin="70,5,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 加载遮罩 -->
|
||||
<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>
|
||||
19
PackagingMallShipper/Views/ProductListView.xaml.cs
Normal file
19
PackagingMallShipper/Views/ProductListView.xaml.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Windows.Controls;
|
||||
using PackagingMallShipper.ViewModels;
|
||||
|
||||
namespace PackagingMallShipper.Views
|
||||
{
|
||||
public partial class ProductListView : UserControl
|
||||
{
|
||||
public ProductListView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public ProductListView(ProductListViewModel viewModel) : this()
|
||||
{
|
||||
DataContext = viewModel;
|
||||
Loaded += async (s, e) => await viewModel.InitializeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user