feat: 添加自动同步和新订单通知功能
- 每30分钟自动增量同步订单 - 新订单到达时播放提示音、任务栏闪烁 - 窗口不在前台时弹出Toast通知(5秒后自动关闭) - 界面右上角显示自动同步开关和倒计时(每秒更新) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Win32;
|
||||
@@ -19,6 +21,9 @@ namespace PackagingMallShipper.ViewModels
|
||||
private readonly IShipService _shipService;
|
||||
private readonly IExcelService _excelService;
|
||||
|
||||
private DispatcherTimer _autoSyncTimer;
|
||||
private const int AutoSyncIntervalMinutes = 30;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<Order> _orders = new ObservableCollection<Order>();
|
||||
|
||||
@@ -40,6 +45,15 @@ namespace PackagingMallShipper.ViewModels
|
||||
[ObservableProperty]
|
||||
private string _syncProgress = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _autoSyncEnabled = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _nextSyncTime = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private DateTime? _lastSyncTime;
|
||||
|
||||
public OrderListViewModel(
|
||||
IOrderService orderService,
|
||||
ISyncService syncService,
|
||||
@@ -65,12 +79,257 @@ namespace PackagingMallShipper.ViewModels
|
||||
{
|
||||
StatusMessage = $"发货中 {current}/{total}";
|
||||
};
|
||||
|
||||
// 初始化自动同步定时器
|
||||
InitializeAutoSyncTimer();
|
||||
}
|
||||
|
||||
private void InitializeAutoSyncTimer()
|
||||
{
|
||||
_autoSyncTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMinutes(AutoSyncIntervalMinutes)
|
||||
};
|
||||
_autoSyncTimer.Tick += async (s, e) => await AutoSyncAsync();
|
||||
|
||||
// 更新下次同步时间显示的定时器(每秒更新)
|
||||
var updateTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(1)
|
||||
};
|
||||
updateTimer.Tick += (s, e) => UpdateNextSyncTimeDisplay();
|
||||
updateTimer.Start();
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await RefreshOrdersAsync();
|
||||
await UpdateCountsAsync();
|
||||
|
||||
// 启动自动同步
|
||||
StartAutoSync();
|
||||
}
|
||||
|
||||
public void StartAutoSync()
|
||||
{
|
||||
if (_autoSyncTimer != null && AutoSyncEnabled)
|
||||
{
|
||||
_autoSyncTimer.Start();
|
||||
LastSyncTime = DateTime.Now;
|
||||
UpdateNextSyncTimeDisplay();
|
||||
StatusMessage = $"自动同步已启动,每 {AutoSyncIntervalMinutes} 分钟同步一次";
|
||||
}
|
||||
}
|
||||
|
||||
public void StopAutoSync()
|
||||
{
|
||||
_autoSyncTimer?.Stop();
|
||||
NextSyncTime = "已停止";
|
||||
}
|
||||
|
||||
private void UpdateNextSyncTimeDisplay()
|
||||
{
|
||||
if (!AutoSyncEnabled || LastSyncTime == null)
|
||||
{
|
||||
NextSyncTime = "已停止";
|
||||
return;
|
||||
}
|
||||
|
||||
var nextSync = LastSyncTime.Value.AddMinutes(AutoSyncIntervalMinutes);
|
||||
var remaining = nextSync - DateTime.Now;
|
||||
|
||||
if (remaining.TotalSeconds > 0)
|
||||
{
|
||||
NextSyncTime = $"{(int)remaining.TotalMinutes}分{remaining.Seconds}秒后同步";
|
||||
}
|
||||
else
|
||||
{
|
||||
NextSyncTime = "同步中...";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AutoSyncAsync()
|
||||
{
|
||||
if (IsBusy || !AutoSyncEnabled) return;
|
||||
|
||||
try
|
||||
{
|
||||
var previousPendingCount = PendingCount;
|
||||
|
||||
IsBusy = true;
|
||||
StatusMessage = "自动同步中...";
|
||||
|
||||
var result = await _syncService.SyncOrdersAsync(SyncMode.Incremental);
|
||||
LastSyncTime = DateTime.Now;
|
||||
|
||||
await RefreshOrdersAsync();
|
||||
await UpdateCountsAsync();
|
||||
|
||||
// 检查是否有新订单
|
||||
if (result.NewCount > 0)
|
||||
{
|
||||
// 发送系统通知
|
||||
ShowNewOrderNotification(result.NewCount);
|
||||
}
|
||||
|
||||
StatusMessage = $"自动同步完成 - 新增 {result.NewCount},更新 {result.UpdatedCount} ({DateTime.Now:HH:mm:ss})";
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
StatusMessage = "自动同步失败: 登录已过期";
|
||||
StopAutoSync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"自动同步失败: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
SyncProgress = "";
|
||||
UpdateNextSyncTimeDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowNewOrderNotification(int newOrderCount)
|
||||
{
|
||||
// 使用 Windows 系统托盘通知
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var mainWindow = Application.Current.MainWindow;
|
||||
|
||||
// 如果窗口最小化或不在前台,显示通知
|
||||
if (mainWindow != null)
|
||||
{
|
||||
// 闪烁任务栏
|
||||
FlashWindow(mainWindow);
|
||||
|
||||
// 播放系统提示音
|
||||
System.Media.SystemSounds.Exclamation.Play();
|
||||
|
||||
// 如果窗口不在前台,弹出消息提示
|
||||
if (!mainWindow.IsActive)
|
||||
{
|
||||
// 使用 Toast 样式的通知
|
||||
var notification = new Window
|
||||
{
|
||||
Title = "新订单通知",
|
||||
Width = 300,
|
||||
Height = 120,
|
||||
WindowStyle = WindowStyle.ToolWindow,
|
||||
Topmost = true,
|
||||
ShowInTaskbar = false,
|
||||
WindowStartupLocation = WindowStartupLocation.Manual,
|
||||
ResizeMode = ResizeMode.NoResize
|
||||
};
|
||||
|
||||
// 定位到屏幕右下角
|
||||
var workArea = SystemParameters.WorkArea;
|
||||
notification.Left = workArea.Right - notification.Width - 10;
|
||||
notification.Top = workArea.Bottom - notification.Height - 10;
|
||||
|
||||
var content = new System.Windows.Controls.StackPanel
|
||||
{
|
||||
Margin = new Thickness(15),
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
|
||||
content.Children.Add(new System.Windows.Controls.TextBlock
|
||||
{
|
||||
Text = $"📦 收到 {newOrderCount} 个新订单!",
|
||||
FontSize = 16,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Margin = new Thickness(0, 0, 0, 10)
|
||||
});
|
||||
|
||||
content.Children.Add(new System.Windows.Controls.TextBlock
|
||||
{
|
||||
Text = $"待发货订单:{PendingCount} 个",
|
||||
FontSize = 13,
|
||||
Foreground = System.Windows.Media.Brushes.Gray
|
||||
});
|
||||
|
||||
notification.Content = content;
|
||||
|
||||
// 5秒后自动关闭
|
||||
var closeTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(5)
|
||||
};
|
||||
closeTimer.Tick += (s, e) =>
|
||||
{
|
||||
closeTimer.Stop();
|
||||
notification.Close();
|
||||
};
|
||||
|
||||
notification.MouseLeftButtonDown += (s, e) =>
|
||||
{
|
||||
notification.Close();
|
||||
mainWindow.Activate();
|
||||
if (mainWindow.WindowState == WindowState.Minimized)
|
||||
mainWindow.WindowState = WindowState.Normal;
|
||||
};
|
||||
|
||||
notification.Show();
|
||||
closeTimer.Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 窗口在前台时,只显示状态栏消息
|
||||
StatusMessage = $"📦 收到 {newOrderCount} 个新订单!待发货 {PendingCount} 个";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void FlashWindow(Window window)
|
||||
{
|
||||
// 简单的窗口闪烁效果
|
||||
try
|
||||
{
|
||||
var hwnd = new System.Windows.Interop.WindowInteropHelper(window).Handle;
|
||||
if (hwnd != IntPtr.Zero)
|
||||
{
|
||||
var info = new FLASHWINFO
|
||||
{
|
||||
cbSize = Convert.ToUInt32(System.Runtime.InteropServices.Marshal.SizeOf(typeof(FLASHWINFO))),
|
||||
hwnd = hwnd,
|
||||
dwFlags = 3, // FLASHW_ALL
|
||||
uCount = 3,
|
||||
dwTimeout = 0
|
||||
};
|
||||
FlashWindowEx(ref info);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略闪烁失败
|
||||
}
|
||||
}
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
||||
private static extern bool FlashWindowEx(ref FLASHWINFO pwfi);
|
||||
|
||||
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
|
||||
private struct FLASHWINFO
|
||||
{
|
||||
public uint cbSize;
|
||||
public IntPtr hwnd;
|
||||
public uint dwFlags;
|
||||
public uint uCount;
|
||||
public uint dwTimeout;
|
||||
}
|
||||
|
||||
partial void OnAutoSyncEnabledChanged(bool value)
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
StartAutoSync();
|
||||
}
|
||||
else
|
||||
{
|
||||
StopAutoSync();
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -90,7 +349,7 @@ namespace PackagingMallShipper.ViewModels
|
||||
};
|
||||
|
||||
var orders = await _orderService.GetOrdersAsync(status, SearchText);
|
||||
|
||||
|
||||
Orders.Clear();
|
||||
foreach (var order in orders)
|
||||
{
|
||||
@@ -119,9 +378,19 @@ namespace PackagingMallShipper.ViewModels
|
||||
try
|
||||
{
|
||||
var result = await _syncService.SyncOrdersAsync(SyncMode.Incremental);
|
||||
LastSyncTime = DateTime.Now;
|
||||
await RefreshOrdersAsync();
|
||||
await UpdateCountsAsync();
|
||||
StatusMessage = $"同步完成!新增 {result.NewCount},更新 {result.UpdatedCount}";
|
||||
|
||||
if (result.NewCount > 0)
|
||||
{
|
||||
StatusMessage = $"同步完成!新增 {result.NewCount},更新 {result.UpdatedCount}";
|
||||
ShowNewOrderNotification(result.NewCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusMessage = $"同步完成!新增 {result.NewCount},更新 {result.UpdatedCount}";
|
||||
}
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
@@ -137,6 +406,7 @@ namespace PackagingMallShipper.ViewModels
|
||||
{
|
||||
IsBusy = false;
|
||||
SyncProgress = "";
|
||||
UpdateNextSyncTimeDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +424,7 @@ namespace PackagingMallShipper.ViewModels
|
||||
try
|
||||
{
|
||||
var syncResult = await _syncService.SyncOrdersAsync(SyncMode.Full);
|
||||
LastSyncTime = DateTime.Now;
|
||||
await RefreshOrdersAsync();
|
||||
await UpdateCountsAsync();
|
||||
StatusMessage = $"全量同步完成!共 {syncResult.TotalCount} 条";
|
||||
@@ -165,6 +436,7 @@ namespace PackagingMallShipper.ViewModels
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
UpdateNextSyncTimeDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,5 +556,10 @@ namespace PackagingMallShipper.ViewModels
|
||||
{
|
||||
// Debounce search - simple implementation
|
||||
}
|
||||
|
||||
public void Cleanup()
|
||||
{
|
||||
_autoSyncTimer?.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,16 +15,39 @@
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<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"/>
|
||||
<Grid>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
|
||||
<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>
|
||||
<StackPanel>
|
||||
<TextBlock Text="当前列表" FontSize="12" Foreground="#666"/>
|
||||
<TextBlock Text="{Binding TotalCount}" FontSize="24" FontWeight="Bold" Foreground="#52C41A"/>
|
||||
|
||||
<!-- 自动同步状态 -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<TextBlock Text="自动同步" FontSize="12" Foreground="#666" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<CheckBox IsChecked="{Binding AutoSyncEnabled}" VerticalAlignment="Center" Margin="0,0,15,0"/>
|
||||
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding NextSyncTime}" FontSize="11" Foreground="#999"/>
|
||||
<TextBlock FontSize="10" Foreground="#BBB">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Text" Value="每30分钟自动同步"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding AutoSyncEnabled}" Value="False">
|
||||
<Setter Property="Text" Value=""/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
|
||||
Reference in New Issue
Block a user