438 lines
12 KiB
C#
438 lines
12 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 历史工单列表中的单个工单摘要。
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 订单所属店铺 APPID。
|
|
/// </summary>
|
|
public string AppId
|
|
{
|
|
get => _appId;
|
|
private set
|
|
{
|
|
if (SetValue(ref _appId, value))
|
|
RaisePropertyChanged(nameof(ShopName));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 工单所属店铺的显示名称。列表中不直接展示 APPID。
|
|
/// </summary>
|
|
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 "未知店铺";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 微信订单号,也是客户端使用的工单号。
|
|
/// </summary>
|
|
public string WechatOrderId
|
|
{
|
|
get => _wechatOrderId;
|
|
private set => SetValue(ref _wechatOrderId, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 工单状态编码。
|
|
/// </summary>
|
|
public string Status
|
|
{
|
|
get => _status;
|
|
private set
|
|
{
|
|
if (SetValue(ref _status, value))
|
|
RaisePropertyChanged(nameof(IsCompleted));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 工单状态中文文本。
|
|
/// </summary>
|
|
public string StatusText
|
|
{
|
|
get => _statusText;
|
|
private set => SetValue(ref _statusText, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 工单最后更新时间。
|
|
/// </summary>
|
|
public DateTimeOffset UpdatedAt
|
|
{
|
|
get => _updatedAt;
|
|
private set
|
|
{
|
|
if (SetValue(ref _updatedAt, value))
|
|
RaisePropertyChanged(nameof(UpdatedAtText));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 最近一条公开留言的时间。
|
|
/// </summary>
|
|
public DateTimeOffset LastMessageAt
|
|
{
|
|
get => _lastMessageAt;
|
|
private set => SetValue(ref _lastMessageAt, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 最近一次客服正式回复时间。
|
|
/// </summary>
|
|
public DateTimeOffset? LastReplyAt
|
|
{
|
|
get => _lastReplyAt;
|
|
private set => SetValue(ref _lastReplyAt, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 最近一条留言的发送方类型。
|
|
/// </summary>
|
|
public string LastSenderType
|
|
{
|
|
get => _lastSenderType;
|
|
private set
|
|
{
|
|
if (SetValue(ref _lastSenderType, value))
|
|
RaisePropertyChanged(nameof(PreviewText));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 当前工单的未读留言数量。
|
|
/// </summary>
|
|
public int UnreadCount
|
|
{
|
|
get => _unreadCount;
|
|
private set => SetValue(ref _unreadCount, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 本次服务器刷新是否检测到比本地缓存更新的公开消息。
|
|
/// </summary>
|
|
public bool HasNewMessage
|
|
{
|
|
get => _hasNewMessage;
|
|
private set
|
|
{
|
|
if (SetValue(ref _hasNewMessage, value))
|
|
RaisePropertyChanged(nameof(PreviewText));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 工单版本号字符串。
|
|
/// </summary>
|
|
public string Version
|
|
{
|
|
get => _version;
|
|
private set => SetValue(ref _version, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 商品名称,优先来自当前订单列表或本地工单缓存。
|
|
/// </summary>
|
|
public string ProductName
|
|
{
|
|
get => _productName;
|
|
private set
|
|
{
|
|
if (SetValue(ref _productName, value))
|
|
RaisePropertyChanged(nameof(ProductNameText));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 商品名称的展示文本,尚未同步到商品信息时给出提示。
|
|
/// </summary>
|
|
public string ProductNameText =>
|
|
string.IsNullOrWhiteSpace(ProductName) ? "商品信息待同步" : ProductName;
|
|
|
|
/// <summary>
|
|
/// 商品金额,单位元;旧缓存没有金额时为 null。
|
|
/// </summary>
|
|
public decimal? ProductAmount
|
|
{
|
|
get => _productAmount;
|
|
private set
|
|
{
|
|
if (SetValue(ref _productAmount, value))
|
|
RaisePropertyChanged(nameof(ProductAmountText));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 商品金额展示文本。
|
|
/// </summary>
|
|
public string ProductAmountText =>
|
|
ProductAmount.HasValue
|
|
? "¥ " + ProductAmount.Value.ToString(
|
|
"N2",
|
|
CultureInfo.GetCultureInfo("zh-CN"))
|
|
: string.Empty;
|
|
|
|
/// <summary>
|
|
/// 商品缩略图地址。
|
|
/// </summary>
|
|
public string ProductImageUrl
|
|
{
|
|
get => _productImageUrl;
|
|
private set => SetValue(ref _productImageUrl, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 商品缩略图,加载完成前为空。
|
|
/// </summary>
|
|
public ImageSource ProductImage
|
|
{
|
|
get => _productImage;
|
|
private set => SetValue(ref _productImage, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 用于合并工单的 APPID 与订单号组合键。
|
|
/// </summary>
|
|
public string Key => AppId + "|" + WechatOrderId;
|
|
|
|
/// <summary>
|
|
/// 工单是否已完结。
|
|
/// </summary>
|
|
public bool IsCompleted =>
|
|
string.Equals(Status, "completed", StringComparison.OrdinalIgnoreCase);
|
|
|
|
/// <summary>
|
|
/// 适合列表展示的本地时间文本。
|
|
/// </summary>
|
|
public string UpdatedAtText =>
|
|
UpdatedAt == default
|
|
? string.Empty
|
|
: UpdatedAt.ToLocalTime().ToString("MM-dd HH:mm", CultureInfo.CurrentCulture);
|
|
|
|
/// <summary>
|
|
/// 不读取留言正文时使用的最近动态摘要。
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 标记检测到新消息,并确保列表显示未读角标。
|
|
/// </summary>
|
|
public void MarkHasNewMessage()
|
|
{
|
|
HasNewMessage = true;
|
|
if (UnreadCount <= 0)
|
|
UnreadCount = 1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 合并商品信息。空值不会覆盖已有数据,便于先应用缓存、再应用最新的订单信息。
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|