using AllBase.Helper; using OrderSupportTicket.Models.Api; using OrderSupportTicket.Services; using System; using System.Globalization; using System.Linq; using System.Threading.Tasks; using System.Windows.Media; using System.Windows.Threading; using WPFCommon.Models; namespace OrderSupportTicket.ViewModels; /// /// 历史工单列表中的单个工单摘要。 /// public sealed class WorkOrderSummaryViewModel : BindableBaseEx { private string _appId; private string _wechatOrderId; private string _status; private string _statusText; private DateTimeOffset _updatedAt; private DateTimeOffset _lastMessageAt; private DateTimeOffset? _lastReplyAt; private string _lastSenderType; private int _unreadCount; private bool _hasNewMessage; private string _version; private string _productName; private string _productImageUrl; private decimal? _productAmount; private ImageSource _productImage; private int _productImageVersion; private readonly Dispatcher _dispatcher = System.Windows.Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher; public WorkOrderSummaryViewModel(WorkOrderSummaryDto dto) { if (dto == null) throw new ArgumentNullException(nameof(dto)); Apply(dto); } /// /// 订单所属店铺 APPID。 /// public string AppId { get => _appId; private set { if (SetValue(ref _appId, value)) RaisePropertyChanged(nameof(ShopName)); } } /// /// 工单所属店铺的显示名称。列表中不直接展示 APPID。 /// public string ShopName { get { var shop = GlobalConfig.ShopLoginList?.FirstOrDefault(s => string.Equals(s.ShopId, AppId, StringComparison.Ordinal) || string.Equals(s.AppKey, AppId, StringComparison.Ordinal)); if (!string.IsNullOrWhiteSpace(shop?.ShopAccount)) return shop.ShopAccount; if (!string.IsNullOrWhiteSpace(shop?.CompanyName)) return shop.CompanyName; return "未知店铺"; } } /// /// 微信订单号,也是客户端使用的工单号。 /// public string WechatOrderId { get => _wechatOrderId; private set => SetValue(ref _wechatOrderId, value); } /// /// 工单状态编码。 /// public string Status { get => _status; private set { if (SetValue(ref _status, value)) RaisePropertyChanged(nameof(IsCompleted)); } } /// /// 工单状态中文文本。 /// public string StatusText { get => _statusText; private set => SetValue(ref _statusText, value); } /// /// 工单最后更新时间。 /// public DateTimeOffset UpdatedAt { get => _updatedAt; private set { if (SetValue(ref _updatedAt, value)) RaisePropertyChanged(nameof(UpdatedAtText)); } } /// /// 最近一条公开留言的时间。 /// public DateTimeOffset LastMessageAt { get => _lastMessageAt; private set => SetValue(ref _lastMessageAt, value); } /// /// 最近一次客服正式回复时间。 /// public DateTimeOffset? LastReplyAt { get => _lastReplyAt; private set => SetValue(ref _lastReplyAt, value); } /// /// 最近一条留言的发送方类型。 /// public string LastSenderType { get => _lastSenderType; private set { if (SetValue(ref _lastSenderType, value)) RaisePropertyChanged(nameof(PreviewText)); } } /// /// 当前工单的未读留言数量。 /// public int UnreadCount { get => _unreadCount; private set => SetValue(ref _unreadCount, value); } /// /// 本次服务器刷新是否检测到比本地缓存更新的公开消息。 /// public bool HasNewMessage { get => _hasNewMessage; private set { if (SetValue(ref _hasNewMessage, value)) RaisePropertyChanged(nameof(PreviewText)); } } /// /// 工单版本号字符串。 /// public string Version { get => _version; private set => SetValue(ref _version, value); } /// /// 商品名称,优先来自当前订单列表或本地工单缓存。 /// public string ProductName { get => _productName; private set { if (SetValue(ref _productName, value)) RaisePropertyChanged(nameof(ProductNameText)); } } /// /// 商品名称的展示文本,尚未同步到商品信息时给出提示。 /// public string ProductNameText => string.IsNullOrWhiteSpace(ProductName) ? "商品信息待同步" : ProductName; /// /// 商品金额,单位元;旧缓存没有金额时为 null。 /// public decimal? ProductAmount { get => _productAmount; private set { if (SetValue(ref _productAmount, value)) RaisePropertyChanged(nameof(ProductAmountText)); } } /// /// 商品金额展示文本。 /// public string ProductAmountText => ProductAmount.HasValue ? "¥ " + ProductAmount.Value.ToString( "N2", CultureInfo.GetCultureInfo("zh-CN")) : string.Empty; /// /// 商品缩略图地址。 /// public string ProductImageUrl { get => _productImageUrl; private set => SetValue(ref _productImageUrl, value); } /// /// 商品缩略图,加载完成前为空。 /// public ImageSource ProductImage { get => _productImage; private set => SetValue(ref _productImage, value); } /// /// 用于合并工单的 APPID 与订单号组合键。 /// public string Key => AppId + "|" + WechatOrderId; /// /// 工单是否已完结。 /// public bool IsCompleted => string.Equals(Status, "completed", StringComparison.OrdinalIgnoreCase); /// /// 适合列表展示的本地时间文本。 /// public string UpdatedAtText => UpdatedAt == default ? string.Empty : UpdatedAt.ToLocalTime().ToString("MM-dd HH:mm", CultureInfo.CurrentCulture); /// /// 不读取留言正文时使用的最近动态摘要。 /// public string PreviewText { get { if (HasNewMessage) return "有新消息,点击查看"; if (string.Equals( LastSenderType, "customer", StringComparison.OrdinalIgnoreCase)) { return "客户最近有新留言"; } if (LastReplyAt.HasValue) return "客服最近已回复"; return "暂无公开留言"; } } public bool TryApply(WorkOrderSummaryDto dto) { if (dto == null || !string.Equals(Key, dto.AppId + "|" + dto.WechatOrderId, StringComparison.Ordinal)) { return false; } if (!IsAtLeastCurrentVersion(dto.Version)) return false; Apply(dto); return true; } public void ApplySendResult(string status, string version) { if (!string.IsNullOrWhiteSpace(status)) { Status = status; StatusText = GetStatusText(status); } if (!string.IsNullOrWhiteSpace(version)) Version = version; UpdatedAt = DateTimeOffset.Now; LastMessageAt = UpdatedAt; LastSenderType = "customer"; HasNewMessage = false; } public void MarkRead() { UnreadCount = 0; HasNewMessage = false; } /// /// 标记检测到新消息,并确保列表显示未读角标。 /// public void MarkHasNewMessage() { HasNewMessage = true; if (UnreadCount <= 0) UnreadCount = 1; } /// /// 合并商品信息。空值不会覆盖已有数据,便于先应用缓存、再应用最新的订单信息。 /// public void ApplyProductInfo( string productName, string imageUrl, decimal? amount) { if (!string.IsNullOrWhiteSpace(productName)) ProductName = productName.Trim(); if (amount.HasValue) ProductAmount = amount; if (string.IsNullOrWhiteSpace(imageUrl)) return; var normalizedUrl = imageUrl.Trim(); if (string.Equals(ProductImageUrl, normalizedUrl, StringComparison.Ordinal)) return; ProductImageUrl = normalizedUrl; _ = LoadProductImageAsync(normalizedUrl); } private void Apply(WorkOrderSummaryDto dto) { AppId = dto.AppId; WechatOrderId = dto.WechatOrderId; Status = dto.Status; StatusText = string.IsNullOrWhiteSpace(dto.StatusText) ? GetStatusText(dto.Status) : dto.StatusText; UpdatedAt = ParseTimestamp(dto.UpdatedAt); LastMessageAt = ParseTimestamp(dto.LastMessageAt); LastReplyAt = ParseNullableTimestamp(dto.LastReplyAt); LastSenderType = dto.LastSenderType; UnreadCount = Math.Max(0, dto.UnreadCount); Version = dto.Version; } private async Task LoadProductImageAsync(string url) { try { var version = ++_productImageVersion; ProductImage = null; var image = await ProductImageLoader.LoadAsync(url); if (image == null || version != _productImageVersion) return; if (_dispatcher.CheckAccess()) { ProductImage = image; return; } await _dispatcher.InvokeAsync(() => { if (version == _productImageVersion) ProductImage = image; }); } catch { // 商品图加载失败不影响工单列表展示。 } } private bool IsAtLeastCurrentVersion(string incomingVersion) { if (string.IsNullOrWhiteSpace(incomingVersion)) return true; if (!long.TryParse(incomingVersion, out var incoming)) return true; if (!long.TryParse(Version, out var current)) return true; return incoming >= current; } private static DateTimeOffset ParseTimestamp(string value) { if (DateTimeOffset.TryParse( value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsed)) { return parsed; } return default; } private static DateTimeOffset? ParseNullableTimestamp(string value) { var parsed = ParseTimestamp(value); return parsed == default ? (DateTimeOffset?)null : parsed; } private static string GetStatusText(string status) { if (string.Equals(status, "waiting_reply", StringComparison.OrdinalIgnoreCase)) return "待客服回复"; if (string.Equals(status, "replied", StringComparison.OrdinalIgnoreCase)) return "客服已回复"; if (string.Equals(status, "completed", StringComparison.OrdinalIgnoreCase)) return "已完结"; return string.IsNullOrWhiteSpace(status) ? "状态未知" : status; } }