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:
Administrator
2025-12-18 00:43:28 +08:00
parent 412376009a
commit 92b38fc656
2 changed files with 310 additions and 10 deletions

View File

@@ -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();
}
}
}

View File

@@ -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>
<!-- 工具栏 -->