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;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using System.Windows.Threading;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
@@ -19,6 +21,9 @@ namespace PackagingMallShipper.ViewModels
|
|||||||
private readonly IShipService _shipService;
|
private readonly IShipService _shipService;
|
||||||
private readonly IExcelService _excelService;
|
private readonly IExcelService _excelService;
|
||||||
|
|
||||||
|
private DispatcherTimer _autoSyncTimer;
|
||||||
|
private const int AutoSyncIntervalMinutes = 30;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private ObservableCollection<Order> _orders = new ObservableCollection<Order>();
|
private ObservableCollection<Order> _orders = new ObservableCollection<Order>();
|
||||||
|
|
||||||
@@ -40,6 +45,15 @@ namespace PackagingMallShipper.ViewModels
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _syncProgress = "";
|
private string _syncProgress = "";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _autoSyncEnabled = true;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _nextSyncTime = "";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private DateTime? _lastSyncTime;
|
||||||
|
|
||||||
public OrderListViewModel(
|
public OrderListViewModel(
|
||||||
IOrderService orderService,
|
IOrderService orderService,
|
||||||
ISyncService syncService,
|
ISyncService syncService,
|
||||||
@@ -65,12 +79,257 @@ namespace PackagingMallShipper.ViewModels
|
|||||||
{
|
{
|
||||||
StatusMessage = $"发货中 {current}/{total}";
|
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()
|
public async Task InitializeAsync()
|
||||||
{
|
{
|
||||||
await RefreshOrdersAsync();
|
await RefreshOrdersAsync();
|
||||||
await UpdateCountsAsync();
|
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]
|
[RelayCommand]
|
||||||
@@ -119,9 +378,19 @@ namespace PackagingMallShipper.ViewModels
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await _syncService.SyncOrdersAsync(SyncMode.Incremental);
|
var result = await _syncService.SyncOrdersAsync(SyncMode.Incremental);
|
||||||
|
LastSyncTime = DateTime.Now;
|
||||||
await RefreshOrdersAsync();
|
await RefreshOrdersAsync();
|
||||||
await UpdateCountsAsync();
|
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)
|
catch (UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
@@ -137,6 +406,7 @@ namespace PackagingMallShipper.ViewModels
|
|||||||
{
|
{
|
||||||
IsBusy = false;
|
IsBusy = false;
|
||||||
SyncProgress = "";
|
SyncProgress = "";
|
||||||
|
UpdateNextSyncTimeDisplay();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,6 +424,7 @@ namespace PackagingMallShipper.ViewModels
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var syncResult = await _syncService.SyncOrdersAsync(SyncMode.Full);
|
var syncResult = await _syncService.SyncOrdersAsync(SyncMode.Full);
|
||||||
|
LastSyncTime = DateTime.Now;
|
||||||
await RefreshOrdersAsync();
|
await RefreshOrdersAsync();
|
||||||
await UpdateCountsAsync();
|
await UpdateCountsAsync();
|
||||||
StatusMessage = $"全量同步完成!共 {syncResult.TotalCount} 条";
|
StatusMessage = $"全量同步完成!共 {syncResult.TotalCount} 条";
|
||||||
@@ -165,6 +436,7 @@ namespace PackagingMallShipper.ViewModels
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
IsBusy = false;
|
IsBusy = false;
|
||||||
|
UpdateNextSyncTimeDisplay();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,5 +556,10 @@ namespace PackagingMallShipper.ViewModels
|
|||||||
{
|
{
|
||||||
// Debounce search - simple implementation
|
// 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">
|
<Border Grid.Row="0" Background="#F5F7FA" CornerRadius="4" Padding="15" Margin="0,0,0,15">
|
||||||
<StackPanel Orientation="Horizontal">
|
<Grid>
|
||||||
<StackPanel Margin="0,0,40,0">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
|
||||||
<TextBlock Text="待发货订单" FontSize="12" Foreground="#666"/>
|
<StackPanel Margin="0,0,40,0">
|
||||||
<TextBlock Text="{Binding PendingCount}" FontSize="24" FontWeight="Bold" Foreground="#1890FF"/>
|
<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>
|
||||||
<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>
|
||||||
</StackPanel>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- 工具栏 -->
|
<!-- 工具栏 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user