using AllBase.Helper;
using AllBase.Model.DBModel;
using Microsoft.Win32;
using OrderSupportTicket.Models;
using OrderSupportTicket.Models.Api;
using OrderSupportTicket.Services;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Threading;
using WPFCommon.Models;
using WxOrder.Framework.Commons;
using WxOrder.Framework.Commons.Dialogs;
using WxOrder.Framework.Controls;
namespace OrderSupportTicket.ViewModels;
///
/// 工单页总 ViewModel,负责工单查询、留言操作、新建工单和 SSE 刷新。
///
public sealed class OrderSupportTicketViewModel : BindableBaseEx, IDialogAware, IDisposable
{
///
/// 新建工单弹窗中传入 的参数名。
///
public const string OrderParameterKey = "order";
private readonly TicketService _ticketService;
private readonly WorkOrderCacheService _cacheService = new WorkOrderCacheService();
private readonly Dispatcher _dispatcher;
private readonly AsyncDelegateCommand _selectTicketCommand;
private readonly AsyncDelegateCommand _createTicketCommand;
private readonly AsyncDelegateCommand _sendMessageCommand;
private readonly AsyncDelegateCommand _removePendingAttachmentCommand;
private readonly AsyncDelegateCommand _loadEarlierMessagesCommand;
private readonly AsyncDelegateCommand _refreshMessagesCommand;
private readonly AsyncDelegateCommand _retryRealtimeCommand;
private readonly DelegateCommand _addImagesCommand;
private readonly HashSet _seenEventIds =
new HashSet(StringComparer.Ordinal);
private readonly Dictionary _lastReadSequences =
new Dictionary(StringComparer.Ordinal);
private Dictionary _savedWorkOrders =
new Dictionary(StringComparer.Ordinal);
// 按 APPID 和订单号隔离各工单消息,切换会话不丢失推送。
private readonly Dictionary> _conversationMessages =
new Dictionary>(StringComparer.Ordinal);
private readonly Dictionary _conversationPages = new Dictionary(StringComparer.Ordinal);
private readonly Dictionary _conversationLoads = new Dictionary(StringComparer.Ordinal);
private bool IsRealtimeConversation => _options?.ConversationMode == TicketConversationMode.Realtime;
private bool _hasSavedCache;
private TicketClientOptions _options;
private bool _configured;
private bool _initialized;
private bool _disposed;
private string _pendingOpenKey;
private WorkOrderSummaryViewModel _selectedTicket;
private TicketOrderOption _selectedOrder;
private ImageSource _selectedOrderImage;
private int _selectedOrderImageVersion;
private string _chatText;
private string _draftText;
private string _ticketSearchText = string.Empty;
private int _selectedTicketStatusIndex;
private bool _isDraft;
private bool _isBusy;
private bool _isLoading;
private bool _isLoadingMessages;
private bool _hasMoreMessages;
private string _nextBefore;
private string _noticeText;
private bool _isNoticeError;
private bool _hasNewData;
private string _newDataText;
private bool _isWindowActive;
private TicketRealtimeState _sseState;
private string _sseStatusText;
private int _messageScrollVersion;
private string _pendingCreateRequestId;
private string _pendingCreateFingerprint;
private string _pendingMessageId;
private string _pendingMessageFingerprint;
private int _selectionVersion;
public OrderSupportTicketViewModel(
TicketService ticketService,
TicketClientOptions options = null)
{
_ticketService = ticketService
?? throw new ArgumentNullException(nameof(ticketService));
_dispatcher = System.Windows.Application.Current?.Dispatcher
?? Dispatcher.CurrentDispatcher;
Tickets = new ObservableCollection();
Messages = new ObservableCollection();
PendingAttachments = new ObservableCollection();
TicketView = CollectionViewSource.GetDefaultView(Tickets);
TicketView.Filter = FilterTicket;
_selectTicketCommand = new AsyncDelegateCommand(
SelectTicketAsync,
ticket => ticket != null && !IsBusy);
_createTicketCommand = new AsyncDelegateCommand(
CreateTicketAsync,
CanCreateTicket);
_sendMessageCommand = new AsyncDelegateCommand(
SendMessageAsync,
CanSendMessage);
_removePendingAttachmentCommand =
new AsyncDelegateCommand(
RemovePendingAttachmentAsync,
attachment => attachment != null && !IsBusy);
_loadEarlierMessagesCommand = new AsyncDelegateCommand(
LoadEarlierMessagesAsync,
() => !IsBusy && SelectedTicket != null && HasMoreMessages);
_refreshMessagesCommand = new AsyncDelegateCommand(
RefreshMessagesAsync,
() => _configured && !IsBusy && !IsLoadingMessages && !IsDraft && SelectedTicket != null);
_retryRealtimeCommand = new AsyncDelegateCommand(
RestartRealtimeAsync,
() => _configured);
_addImagesCommand = new DelegateCommand(
AddImages,
_ => CanAddImage);
_ticketService.NotificationReceived += OnNotificationReceived;
_ticketService.StateChanged += OnRealtimeStateChanged;
SseState = _ticketService.RealtimeState;
SseStatusText = GetRealtimeStatusText(SseState);
if (options != null)
Configure(options);
}
///
/// 当前账号和 APPID 下的历史工单。
///
public ObservableCollection Tickets { get; }
///
/// 经过搜索条件过滤后的历史工单。
///
public ICollectionView TicketView { get; }
///
/// 当前选中工单的公开留言。
///
public ObservableCollection Messages { get; }
///
/// 选择并加载历史工单的命令。
///
public ICommand SelectTicketCommand => _selectTicketCommand;
///
/// 提交新工单的命令。
///
public ICommand CreateTicketCommand => _createTicketCommand;
///
/// 提交客户留言的命令。
///
public ICommand SendMessageCommand => _sendMessageCommand;
///
/// 移除待发送图片的命令。
///
public ICommand RemovePendingAttachmentCommand => _removePendingAttachmentCommand;
///
/// 当前留言待发送的本地图片,最多 3 张。
///
public ObservableCollection PendingAttachments { get; }
///
/// 是否已选择待发送图片。
///
public bool HasPendingAttachments => PendingAttachments.Count > 0;
///
/// 是否还可以继续选择图片。
///
public bool CanAddImage =>
!IsBusy
&& SelectedTicket != null
&& PendingAttachments.Count < TicketApiService.MaxImageCount;
///
/// 选择本地图片并加入当前留言的待发送列表。
///
public ICommand AddImagesCommand => _addImagesCommand;
///
/// 待发送图片数量展示文本。
///
public string AttachmentCountText =>
PendingAttachments.Count + " / " + TicketApiService.MaxImageCount;
///
/// 加载当前工单更早留言的命令。
///
public ICommand LoadEarlierMessagesCommand => _loadEarlierMessagesCommand;
/// 强制重新获取当前工单消息,两种会话模式均不使用缓存代替请求。
public ICommand RefreshMessagesCommand => _refreshMessagesCommand;
///
/// 手动重新连接 SSE 的命令。
///
public ICommand RetryRealtimeCommand => _retryRealtimeCommand;
///
/// 显示图片
///
public ICommand ShowImageWindowCommand => new DelegateCommand(ShowImageWindow);
///
/// 显示图片
///
///
private void ShowImageWindow(ChatAttachmentViewModel chatAttachment)
{
ImageViewerWindow.OpenDialog(chatAttachment.Image);
}
///
/// 当前正在查看的历史工单。
///
public WorkOrderSummaryViewModel SelectedTicket
{
get => _selectedTicket;
private set
{
if (SetValue(ref _selectedTicket, value))
{
RaisePropertyChanged(nameof(IsChatVisible));
RaisePropertyChanged(nameof(IsEmptyVisible));
RaisePropertyChanged(nameof(ComposerPlaceholder));
RaisePropertyChanged(nameof(CanAddImage));
_addImagesCommand.RaiseCanExecuteChanged();
RaiseSendCanExecuteChanged();
RaiseLoadEarlierCanExecuteChanged();
}
}
}
///
/// 新建工单时选中的关联订单。
///
public TicketOrderOption SelectedOrder
{
get => _selectedOrder;
set
{
if (SetValue(ref _selectedOrder, value))
{
RaisePropertyChanged(nameof(SelectedOrderDescription));
RaisePropertyChanged(nameof(SelectedOrderText));
RaiseCreateCanExecuteChanged();
_ = LoadSelectedOrderImageAsync(value);
}
}
}
///
/// 当前工单输入框中的留言文本。
///
public string ChatText
{
get => _chatText;
set
{
if (SetValue(ref _chatText, value ?? string.Empty))
{
RaisePropertyChanged(nameof(ChatCharacterCountText));
RaiseSendCanExecuteChanged();
}
}
}
///
/// 新建工单输入框中的问题描述。
///
public string DraftText
{
get => _draftText;
set
{
if (SetValue(ref _draftText, value ?? string.Empty))
{
RaisePropertyChanged(nameof(DraftCharacterCountText));
RaiseCreateCanExecuteChanged();
}
}
}
///
/// 历史工单搜索关键词,可匹配订单号、店铺、状态或最近动态。
///
public string TicketSearchText
{
get => _ticketSearchText;
set
{
if (SetValue(ref _ticketSearchText, value ?? string.Empty))
{
if (IsDraft)
CancelDraft();
TicketView?.Refresh();
}
}
}
///
/// 历史工单状态页签索引:0 全部、1 待客服回复、2 客服已回复、3 已完结。
///
public int SelectedTicketStatusIndex
{
get => _selectedTicketStatusIndex;
set
{
if (SetValue(ref _selectedTicketStatusIndex, value))
TicketView?.Refresh();
}
}
///
/// 当前是否处于新建工单草稿状态。
///
public bool IsDraft
{
get => _isDraft;
private set
{
if (SetValue(ref _isDraft, value))
{
RaisePropertyChanged(nameof(IsChatVisible));
RaisePropertyChanged(nameof(IsEmptyVisible));
RaisePropertyChanged(nameof(IsDraftVisible));
RaisePropertyChanged(nameof(CanAddImage));
_addImagesCommand.RaiseCanExecuteChanged();
RaiseCreateCanExecuteChanged();
}
}
}
///
/// 是否正在执行创建、留言或刷新操作。
///
public bool IsBusy
{
get => _isBusy;
private set
{
if (SetValue(ref _isBusy, value))
{
RaisePropertyChanged(nameof(IsNotBusy));
RaisePropertyChanged(nameof(CanAddImage));
RaiseSendCanExecuteChanged();
RaiseCreateCanExecuteChanged();
_removePendingAttachmentCommand.RaiseCanExecuteChanged();
RaiseLoadEarlierCanExecuteChanged();
_retryRealtimeCommand.RaiseCanExecuteChanged();
_selectTicketCommand.RaiseCanExecuteChanged();
_addImagesCommand.RaiseCanExecuteChanged();
if (!value && !string.IsNullOrWhiteSpace(_pendingOpenKey))
_ = SelectPendingTicketAsync();
}
}
}
///
/// 是否正在加载工单摘要列表。
///
public bool IsLoading
{
get => _isLoading;
private set => SetValue(ref _isLoading, value);
}
///
/// 是否正在加载当前工单留言。
///
public bool IsLoadingMessages
{
get => _isLoadingMessages;
private set
{
if (SetValue(ref _isLoadingMessages, value))
_refreshMessagesCommand.RaiseCanExecuteChanged();
}
}
///
/// 当前留言板是否还有更早留言。
///
public bool HasMoreMessages
{
get => _hasMoreMessages;
private set
{
if (SetValue(ref _hasMoreMessages, value))
RaiseLoadEarlierCanExecuteChanged();
}
}
///
/// 当前操作提示或错误信息。
///
public string NoticeText
{
get => _noticeText;
private set => SetValue(ref _noticeText, value);
}
///
/// 当前提示是否为错误类型。
///
public bool IsNoticeError
{
get => _isNoticeError;
private set => SetValue(ref _isNoticeError, value);
}
///
/// 当前页面是否检测到服务器有新的工单消息。
///
public bool HasNewData
{
get => _hasNewData;
private set => SetValue(ref _hasNewData, value);
}
///
/// 新数据提示文本。
///
public string NewDataText
{
get => _newDataText;
private set => SetValue(ref _newDataText, value);
}
///
/// 工单页所在窗口当前是否处于激活且可见状态。
/// 未激活时只刷新数据,不把当前工单标记为已读。
///
public bool IsWindowActive
{
get => _isWindowActive;
set
{
if (!SetValue(ref _isWindowActive, value))
return;
if (value && _initialized && !IsDraft && SelectedTicket != null)
_ = MarkSelectedTicketReadAsync();
}
}
///
/// SSE 当前连接状态。
///
public TicketRealtimeState SseState
{
get => _sseState;
private set => SetValue(ref _sseState, value);
}
///
/// SSE 当前状态的中文显示文本。
///
public string SseStatusText
{
get => _sseStatusText;
private set => SetValue(ref _sseStatusText, value);
}
///
/// 触发消息列表滚动到底部的版本号。
///
public int MessageScrollVersion
{
get => _messageScrollVersion;
private set => SetValue(ref _messageScrollVersion, value);
}
///
/// 是否显示当前工单的聊天面板。
///
public bool IsChatVisible => !IsDraft && SelectedTicket != null;
///
/// 是否显示新建工单面板。
///
public bool IsDraftVisible => IsDraft;
///
/// 是否显示未选择工单时的空状态。
///
public bool IsEmptyVisible => !IsDraft && SelectedTicket == null;
///
/// 当前是否没有执行中的操作。
///
public bool IsNotBusy => !IsBusy;
///
/// 留言输入框字数统计文本。
///
public string ChatCharacterCountText => (ChatText ?? string.Empty).Length + " / 4000";
///
/// 新建工单输入框字数统计文本。
///
public string DraftCharacterCountText => (DraftText ?? string.Empty).Length + " / 2000";
///
/// 当前工单输入框提示文本。
///
public string ComposerPlaceholder =>
SelectedTicket != null && SelectedTicket.IsCompleted
? "工单已完结,继续留言将重新开启"
: "输入留言";
///
/// 当前关联订单的商品、金额和下单时间说明。
///
public string SelectedOrderDescription =>
SelectedOrder?.ProductDescription ?? "未提供关联订单";
///
/// 当前关联订单的订单号和商品名摘要。
///
public string SelectedOrderText =>
SelectedOrder?.DisplayText ?? "未提供关联订单";
///
/// 当前关联订单的商品缩略图,加载完成前为空。
///
public ImageSource SelectedOrderImage
{
get => _selectedOrderImage;
private set => SetValue(ref _selectedOrderImage, value);
}
///
/// 弹窗标题。
///
public string Title => "客户工单";
///
/// 由 DialogService 绑定,页面本身不主动请求关闭。
///
#pragma warning disable CS0067
public event Action RequestClose;
#pragma warning restore CS0067
public bool CanCloseDialog()
{
return true;
}
public void OnDialogOpened(IDialogParameters parameters)
{
if (parameters == null)
return;
if (parameters.TryGetValue("options", out var options)
&& options != null)
{
Configure(options);
}
var order = GetOrderFromParameters(parameters);
if (order != null)
OpenNewTicket(order);
}
///
/// 从弹窗参数中取出需要新建工单的订单。优先使用订单对象,
/// 同时兼容按字段传入订单号、APPID 等信息的调用方式。
///
private static TicketOrderOption GetOrderFromParameters(
IDialogParameters parameters)
{
foreach (var key in new[]
{
OrderParameterKey,
"ticketOrder",
"TicketOrderOption"
})
{
if (parameters.TryGetValue(key, out var order)
&& order != null)
{
return order;
}
}
if (parameters.TryGetValue("options", out var options)
&& options?.Orders != null)
{
var optionOrder = options.Orders.FirstOrDefault(o => o != null);
if (optionOrder != null)
return optionOrder;
}
foreach (var key in parameters.Keys ?? Enumerable.Empty())
{
if (!parameters.TryGetValue